配信の出し分け ― セグメントを施策に変え、holdoutを最初から
予測スコアやセグメントを、メール・LINE・オンサイトのパーソナライズとして出し分ける方法を扱います。最初からholdout(非配信群)を設計に組み込み、後から効果を測れる状態で配信する——活性化と効果検証を分離しない作法を、SOLNAのCRMで具体化します。
by Akisame
セグメントを配信に変えるとき、効果検証はいつ仕込むのか。答えは「配信の設計段階で、非配信群(holdout)を切っておく」です。後からでは、送った群と送らなかった群を比較可能な形で取り出せません。本記事では、warehouse のセグメントや予測スコアをメール・LINE・オンサイトの出し分けに変える手順と、最初から holdout を組み込む作法を、過去の受託案件をもとに設計した架空企業 SOLNA の CRM で具体化します。
前回(第4回 / 成果を最適化に戻す)は、媒体側の最適化を実成果で動かす運用でした。今回はその一つ手前、自社チャネル(メール・LINE・オンサイト)でのセグメント配信です。前提として、配信対象は Reverse ETL で同期済み(第2回)、同意・識別の基盤は計測設計の連載(なぜ計測は静かに壊れるのか ― 計測設計という土台)で用意済みとします。
セグメントから配信へ(メール / LINE / オンサイト)
活性化の最後の一区間は、warehouse に置いた「誰に効きそうか」を、実際の配信トリガーに変えるところです。第2回で作ったモデル駆動オーディエンス——マートの定義がそのままオーディエンスになる発想——を、ここでは配信面に流し込みます。
面は大きく3つあり、コスト構造と割り込みの強さが違います。
- メール: 送信コストがほぼゼロ、割り込みが弱い。誤爆の害が比較的小さく、検証の実験面として扱いやすい。
- LINE: 開封・到達が高い反面、割り込みが強く、過剰配信の害(ブロック・通報)が一気に出る。1通の重みが大きい。
- オンサイト / アプリ内: その場の文脈を使えるパーソナライズ。プッシュではなくプル寄りで、配信疲れは出にくいが、訪問しないと届かない。
配信設計は「誰に・何を・どの面で・どの頻度で」の4つに分解できます。前半2つ(誰に・何を)はセグメントとスコアの仕事、後半2つ(面・頻度)は本記事後半の主題です。
まずは「誰に」。SOLNA の典型は、解約リスクの高い定期会員への引き止め配信です。churn スコアは Kaggle 連載・dbt 連載の出力(mart)を入力として受け取る前提で、しきい値で対象を切ります。
-- 配信対象セグメント: 解約リスクが高く、まだ解約していない定期会員
-- scores_churn は上流(Kaggle連載・dbt連載)の出力martを入力として受け取る前提
WITH base AS (
SELECT
c.customer_id,
c.membership_tier,
c.ltv_to_date,
s.churn_score -- 予測スコア(0〜1, 較正済み)
FROM `solna.customers` AS c
JOIN `solna.scores_churn` AS s USING (customer_id)
WHERE c.churned_flag = FALSE
)
SELECT customer_id, membership_tier, ltv_to_date, churn_score
FROM base
WHERE churn_score >= 0.6 -- しきい値は較正済みスコアで決める(評価指標の回と接続)ここで多くの現場が踏む地雷が、「対象全員に送って、配信前後で効果を見る」です。前後比較は、季節やトレンド、もともと解約しにくい層を選んでいたこと(セレクションバイアス)と、施策の効果を区別できません。これは因果編で繰り返し解いた誤解——「上がった」と「効いた」は別物——そのものです。配信に変える瞬間に、検証可能性を一緒に設計しておく必要があります。
holdout を最初から設計する
効果を主張するには反実仮想、つまり「送らなかったら、この人たちはどうなっていたか」という比較対象が要ります。観測できるのは「送って、こうなった」という一本の事実だけなので、送らなかった世界を自分で確保しておくしかありません。それが holdout(非配信群)です。
やることは単純で、配信対象セグメントの一部を、ランダムに「あえて送らない」群として取り分け、その割り付けを送信時に記録します。SOLNA の messages テーブルは、はじめからこの設計を織り込んでいます。カラムは message_id, customer_id, channel, sent_at, campaign, holdout_flag, opened, clicked, converted で、channel は email か line、holdout_flag が「送った / あえて送らなかった」の記録を兼ねます。
holdout_flag = TRUE の行は「対象だが、あえて送らなかった」記録です。sent_at は holdout では NULL、opened / clicked / converted は自然発生分だけが入ります。treatment(holdout_flag = FALSE)の行だけを配信ツールへ同期し、holdout も含めた全行を messages に残す——これが後から効果を測れる状態の最小条件です。
割り付けはランダムに、かつ再現可能(冪等)にします。配信が途中で止まって再実行されても同じ群に落ちるよう、乱数ではなく決定的ハッシュを使います。
-- campaign 単位で treatment / holdout を決定(決定的ハッシュで再現可能・冪等)
DECLARE campaign_id STRING DEFAULT 'win_back_2026_06';
DECLARE holdout_ratio FLOAT64 DEFAULT 0.10; -- 5〜10% が定石
WITH segment AS (
SELECT c.customer_id, c.membership_tier, c.ltv_to_date
FROM `solna.customers` AS c
JOIN `solna.scores_churn` AS s USING (customer_id)
WHERE c.churned_flag = FALSE
AND s.churn_score >= 0.6
),
assigned AS (
SELECT
customer_id,
membership_tier,
-- campaign × customer_id から [0,1) の安定した一様乱数を生成
MOD(ABS(FARM_FINGERPRINT(CONCAT(campaign_id, '|', CAST(customer_id AS STRING)))), 10000)
/ 10000.0 AS u
FROM segment
)
SELECT
customer_id,
campaign_id AS campaign,
u < holdout_ratio AS holdout_flag -- TRUE = あえて送らない(記録のみ)
FROM assignedholdout 比率は 5〜10% が定石です。広げるほど効果の検出力(小さな増分を見分ける力)は上がりますが、その分だけ「効くはずの施策を当てない機会損失」が増えます。SOLNA では、機会損失の大きい高 LTV 層は holdout を小さめに、一般層は標準的に、と層ごとに比率を変えています。
配信後の確認は、messages を holdout_flag で分けて converted の率を比べるだけです。ただし——ここが本記事で最も強調したい点ですが——この素朴比較は「効果を測れる状態を作れた」ことの確認であって、増分そのものの厳密な推定ではありません。サンプルが小さければ差はばらつきに飲まれますし、群間のわずかな偏りも残ります。厳密な増分の見方は最終回(第6回 / 効果検証でループを閉じる)と因果編に送ります。
なお、ここで切れる holdout は「ユーザー単位でランダムに分けられる」配信面に限られます。クーポンが口コミで横に漏れる、店舗施策が地域全体に効く——といったユーザー単位で分けられない介入は、地域単位(geo)や時間で切り替える switchback の出番で、これは実験の連載(ユーザー単位で分けられないとき ― 地域実験と switchback)の主題です。配信面で holdout を仕込めること自体が、観測手法を因果手法に格上げできる数少ない梃子だと捉えてください。
出し分けの粒度と頻度(過剰配信の害)
セグメントを細かく割るほどパーソナライズは効きますが、運用コストと検証コストが膨らみ、各群の holdout が小さくなって検出力が落ちます。最初は粗いセグメント(2〜4本)から始め、配信が回って holdout で増分が見え始めてから刻むのが順序です。「先に細かく割る」は、第2回で触れた古いセグメントの乱立と同じ失敗を、配信側で繰り返すことになります。
「どの頻度で」は、過剰配信の害と直結します。最低限の防御として、直近の配信回数で対象を絞る頻度キャップを入れます。
-- 直近14日に2回以上「実際に送った」顧客を、今回の配信対象から除外
WITH recent AS (
SELECT customer_id, COUNT(*) AS n_sent_14d
FROM `solna.messages`
WHERE sent_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 14 DAY)
AND holdout_flag = FALSE -- 実際に送った分だけ数える(holdoutは送っていない)
GROUP BY customer_id
)
SELECT t.customer_id
FROM target_list AS t -- 前ブロックの treatment 行
LEFT JOIN recent AS r USING (customer_id)
WHERE COALESCE(r.n_sent_14d, 0) < 2 -- 直近2回未満のみ送る過剰配信の害は、短期 CV を取りに行った結果として現れます。開封率の低下、配信停止(opt-out)、LINE のブロック、そしてブランドへの不信です。短期の反応率を最大化する設計は、長期の到達基盤そのものを削ることがあります。
SOLNA の運用で観測している配信指標を、レンジで示します。これらは 記述指標であって効果(増分)ではない点に注意してください。配信を増やせばクリックの絶対数は増えますが、それが「効いた」かどうかは holdout との比較を待つ必要があります。
| 指標 | 観測レンジ(記述) | メモ |
|---|---|---|
| メール開封率 | 20〜35% | 件名・配信時間帯で上下 |
| メールクリック率 | 2〜5% | セグメント精度で差が出る |
| LINE 既読到達 | 50〜70% | メールより高いが割り込みも強い |
| LINE クリック率 | 8〜15% | 反応は高いが疲労も早い |
| opt-out(メール / 通) | 0.1〜0.4% | 過剰配信時に上振れ |
| LINE ブロック率(月) | 0.5〜1.5% | 頻度過多で跳ねる早期警報 |
配信疲れと長期影響
短期指標(開封・クリック・即時 CV)だけで配信を最適化すると、長期指標(LTV・継続・配信停止)を静かに削ります。今日のクリックを1つ増やす配信が、半年後の解約を1つ増やしているなら、それは活性化ではなく前借りです。
ここで holdout の二つ目の価値が出ます。短い窓だけでなく、長い窓を持つ holdout(long-run holdout)を一定割合で維持しておくと、累積的な配信疲れ・カニバリ・解約への影響を、長期で観測できます。SOLNA では、キャンペーン単位の短期 holdout とは別に、一定の顧客を数か月単位で配信から外した長期 holdout を薄く確保し、opt-out 率と LINE ブロック率を監視 KPI に置いています。これらが跳ね始めたら、配信頻度を落とす判断のトリガーです。
実務的な含意は素っ気ないものです。「送れば送るほど良い」ではなく、「送らない判断」も施策のうちだ、ということです。頻度キャップと holdout は、その「送らない」を設計に組み込むための道具です。
最後に限界を明示しておきます。本記事のクエリは最小構成であり、messages の群間比較は前後比較よりはマシというだけで、それ単体で因果を断定できるものではありません。サンプルが小さい SOLNA の一部セグメントでは、差がばらつきに飲まれることも珍しくありません。増分を正しく主張する作法は、次回に譲ります。
章末チェックリスト
- セグメント・スコアから、配信対象を SQL で定義できる(しきい値は較正済みスコアで決めた)
- 配信前に holdout(非配信群)を切り、
holdout_flagをmessagesに記録している - 割り付けは決定的ハッシュで再現可能・冪等になっている
- holdout 比率(5〜10%)を、機会損失と検出力のトレードで決めた
- 頻度キャップを面ごとに設定し、過剰配信を防いでいる
- 短期指標だけでなく、opt-out・ブロック率を長期の監視 KPI に置いた
- 「前後比較は効果ではない」を理解し、増分の判断は次回・因果編に預けた
holdout を切ったら、次は「本当に効いたか」を正しく測ります。素朴比較の限界と、ユーザー単位で分けられないときの考え方は最終回(第6回 / 効果検証でループを閉じる)へ。厳密な実験設計(geo / switchback)は実験の連載、観測データからの因果は因果編へと送り出します。ここで仕込んだ holdout が、ループを閉じるための燃料になります。