「クーポンを配ると売上が上がる」は罠だ
マーケティングでよくある落とし穴がある。
クーポンを全顧客に配った。転換率が上がった。「クーポンは効果があった」と結論付ける。
しかしよく考えると、顧客は次の4タイプに分かれる。
タイプ1:Sure Things(確実購買者) ─ クーポンがなくても買う。クーポンは「利益の損失」でしかないタイプ2:Persuadables(説得可能者) ─ クーポンがあるから買う。ここが真のターゲットタイプ3:Lost Causes(取り込み不可) ─ クーポンがあっても買わない。投資対効果ゼロタイプ4:Do Not Disturbs(逆効果群) ─ クーポンが届くと不快に感じ、むしろ買わなくなる
従来の転換率分析は4タイプを区別しない。アップリフトモデリングはこれを区別する。
「施策を受けたとき」と「受けなかったとき」の購買確率の差分(アップリフト)を顧客ごとに推定し、Persuadables だけに施策を届けることで、同じ予算で最大の純増効果を得る手法だ。
アップリフトの定義
アップリフト(i) = P(購買 | 施策あり, 顧客i) − P(購買 | 施策なし, 顧客i)
この値が正の顧客(Persuadables)だけに施策を打てれば、クーポンの無駄打ちがなくなる。
しかし1人の顧客に「施策あり」と「施策なし」を同時に観測することはできない(反事実問題)。
そこで A/B テストで処置群と対照群を作り、セグメントごとのアップリフトを推定する。
使用するシナリオと前提
- 施策:特定セグメント向けのクーポン(20%OFF)
- 処置群:クーポンを受け取った顧客
- 対照群:クーポンを受け取らなかった顧客(ランダムに選ばれた対照群)
- 計測期間:配信後7日以内の購買
使用するテーブル
-- ab_test_assignments(A/Bテスト割当テーブル)
-- customer_id : 顧客ID
-- test_id : テストID
-- ab_group : 'treatment' / 'control'
-- assigned_at : 割当日時
-- orders : order_id, customer_id, order_date, total_amount, status
-- customers : customer_id, registered_at, channel, rank
-- churn_features(作成した特徴量テーブル)
-- customer_id, recency_days, orders_12m, avg_order_value, active_months_12m などSTEP 1 ― A/Bテスト結果の基礎集計(処置群vs対照群)
まずA/Bテスト全体の転換率を確認する。これは No.19(A/Bテスト設計)の応用だ。
-- STEP1: A/Bテスト全体の転換率
WITH test_results AS (
SELECT
a.customer_id,
a.ab_group,
-- 配信後7日以内に購買したか
MAX(CASE
WHEN o.order_date >= CAST(a.assigned_at AS DATE)
AND o.order_date < DATE_ADD('day', 7, CAST(a.assigned_at AS DATE))
AND o.status = 'completed'
THEN 1 ELSE 0
END) AS converted,
-- 購買金額
COALESCE(SUM(CASE
WHEN o.order_date >= CAST(a.assigned_at AS DATE)
AND o.order_date < DATE_ADD('day', 7, CAST(a.assigned_at AS DATE))
AND o.status = 'completed'
THEN o.total_amount ELSE 0
END), 0) AS revenue
FROM ab_test_assignments a
LEFT JOIN orders o ON a.customer_id = o.customer_id
WHERE a.test_id = 'COUPON_20PCT_DEC2024'
GROUP BY a.customer_id, a.ab_group
)
SELECT
ab_group,
COUNT(*) AS customer_count,
SUM(converted) AS converters,
ROUND(SUM(converted) * 100.0 / COUNT(*), 2) AS conversion_rate_pct,
ROUND(AVG(revenue), 0) AS avg_revenue_per_customer,
-- アップリフト(処置群と対照群の差)は後で計算
SUM(converted) * 1.0 / COUNT(*) AS conversion_rate_raw
FROM test_results
GROUP BY ab_group;出力イメージ
| ab_group | customer_count | converters | conversion_rate_pct | avg_revenue_per_customer |
|---|---|---|---|---|
| control | 4,218 | 506 | 12.00 | 1,440 |
| treatment | 4,189 | 712 | 17.00 | 2,040 |
全体の平均アップリフトは 17.00% - 12.00% = 5.00%ポイント だ。
しかし「誰に対して5%のアップリフトがあったか」は全体集計では分からない。
STEP 2 ― セグメント別アップリフトの計算(T-Learner近似)
最もシンプルなアップリフトモデリングのアプローチが T-Learner(Two-Model Learner)だ。
アップリフト(segment) = 処置群の転換率(segment) − 対照群の転換率(segment)
各セグメントで処置群と対照群の転換率を別々に計算し、その差分をアップリフトとする。
SQLで実装する場合、顧客を事前定義のセグメント(RFMスコア、購買頻度帯など)に分けてからこの差分を計算する。
-- STEP2: セグメント別アップリフト(T-Learner近似)
WITH test_results AS (
SELECT
a.customer_id,
a.ab_group,
MAX(CASE
WHEN o.order_date >= CAST(a.assigned_at AS DATE)
AND o.order_date < DATE_ADD('day', 7, CAST(a.assigned_at AS DATE))
AND o.status = 'completed'
THEN 1 ELSE 0
END) AS converted,
COALESCE(SUM(CASE
WHEN o.order_date >= CAST(a.assigned_at AS DATE)
AND o.order_date < DATE_ADD('day', 7, CAST(a.assigned_at AS DATE))
AND o.status = 'completed'
THEN o.total_amount ELSE 0
END), 0) AS revenue
FROM ab_test_assignments a
LEFT JOIN orders o ON a.customer_id = o.customer_id
WHERE a.test_id = 'COUPON_20PCT_DEC2024'
GROUP BY a.customer_id, a.ab_group
),
with_features AS (
-- 特徴量テーブルと結合してセグメントを付与
SELECT
tr.customer_id,
tr.ab_group,
tr.converted,
tr.revenue,
-- RFMスコア的なセグメント
CASE
WHEN cf.recency_days <= 30 AND cf.orders_12m >= 4 THEN 'ロイヤル(高頻度・高直近)'
WHEN cf.recency_days <= 60 AND cf.orders_12m >= 2 THEN '活性(中頻度・直近あり)'
WHEN cf.recency_days > 60 AND cf.orders_12m >= 3 THEN '離脱リスク(高頻度・遠い)'
WHEN cf.recency_days <= 90 AND cf.orders_12m = 1 THEN '新規(初回のみ)'
ELSE '休眠(低頻度・遠い)'
END AS rfm_segment,
-- 購買金額帯
CASE
WHEN cf.avg_order_value >= 15000 THEN '高単価(15,000円以上)'
WHEN cf.avg_order_value >= 5000 THEN '中単価(5,000〜15,000円)'
ELSE '低単価(5,000円未満)'
END AS price_segment,
cf.recency_days,
cf.orders_12m,
cf.avg_order_value
FROM test_results tr
JOIN churn_features cf ON tr.customer_id = cf.customer_id
),
segment_uplift AS (
SELECT
rfm_segment,
price_segment,
COUNT(CASE WHEN ab_group = 'treatment' THEN 1 END) AS treatment_count,
COUNT(CASE WHEN ab_group = 'control' THEN 1 END) AS control_count,
-- 処置群転換率
ROUND(
SUM(CASE WHEN ab_group = 'treatment' THEN converted ELSE 0 END) * 100.0
/ NULLIF(COUNT(CASE WHEN ab_group = 'treatment' THEN 1 END), 0)
, 2) AS treatment_cr,
-- 対照群転換率
ROUND(
SUM(CASE WHEN ab_group = 'control' THEN converted ELSE 0 END) * 100.0
/ NULLIF(COUNT(CASE WHEN ab_group = 'control' THEN 1 END), 0)
, 2) AS control_cr,
-- アップリフト(転換率の差)
ROUND(
SUM(CASE WHEN ab_group = 'treatment' THEN converted ELSE 0 END) * 100.0
/ NULLIF(COUNT(CASE WHEN ab_group = 'treatment' THEN 1 END), 0)
-
SUM(CASE WHEN ab_group = 'control' THEN converted ELSE 0 END) * 100.0
/ NULLIF(COUNT(CASE WHEN ab_group = 'control' THEN 1 END), 0)
, 2) AS uplift_pct,
-- 売上アップリフト(1顧客あたり)
ROUND(
AVG(CASE WHEN ab_group = 'treatment' THEN revenue ELSE NULL END)
- AVG(CASE WHEN ab_group = 'control' THEN revenue ELSE NULL END)
, 0) AS revenue_uplift_per_customer
FROM with_features
GROUP BY rfm_segment, price_segment
HAVING COUNT(CASE WHEN ab_group = 'treatment' THEN 1 END) >= 30
AND COUNT(CASE WHEN ab_group = 'control' THEN 1 END) >= 30
)
SELECT
rfm_segment,
price_segment,
treatment_count,
control_count,
treatment_cr,
control_cr,
uplift_pct,
revenue_uplift_per_customer,
-- アップリフトのタイプ分類
CASE
WHEN uplift_pct > 5 THEN 'Persuadable(高アップリフト)'
WHEN uplift_pct > 0 THEN 'Persuadable(低アップリフト)'
WHEN uplift_pct = 0 THEN 'Lost Cause / Sure Thing(効果なし)'
ELSE 'Do Not Disturb(逆効果)'
END AS uplift_type
FROM segment_uplift
ORDER BY uplift_pct DESC;完成した出力イメージ
| rfm_segment | price_segment | treatment_cr | control_cr | uplift_pct | revenue_uplift | uplift_type |
|---|---|---|---|---|---|---|
| 離脱リスク(高頻度・遠い) | 高単価 | 28.4 | 8.2 | +20.2 | +3,840 | Persuadable(高アップリフト) |
| 新規(初回のみ) | 低単価 | 22.1 | 5.8 | +16.3 | +1,240 | Persuadable(高アップリフト) |
| 活性(中頻度・直近あり) | 中単価 | 18.9 | 8.4 | +10.5 | +1,580 | Persuadable(高アップリフト) |
| ロイヤル(高頻度・高直近) | 高単価 | 19.8 | 17.2 | +2.6 | +380 | Persuadable(低アップリフト) |
| ロイヤル(高頻度・高直近) | 低単価 | 15.4 | 14.8 | +0.6 | +90 | 効果なし |
| 休眠(低頻度・遠い) | 低単価 | 4.1 | 7.2 | -3.1 | -420 | Do Not Disturb(逆効果) |
これが核心的な発見だ。
- 「離脱リスク・高単価」セグメント
アップリフト+20.2%。クーポンがなければ買わなかったが、クーポンがあると高額な購買をする。
最優先の施策対象 - 「ロイヤル・高単価」セグメント
アップリフトわずか+2.6%。クーポンがなくても17%が買う(Sure Things)。
このセグメントへのクーポン配布はほぼ利益の損失 - 「休眠・低単価」セグメント
アップリフトがマイナス。
クーポンが届くと逆に購買率が下がる(Do Not Disturb)。配布を止めるべき
STEP 3 ― アップリフトでランク付けしてターゲットを最適化する
アップリフトの高いセグメントから優先的に施策対象にする「アップリフトによるランキング」を作る。
-- STEP3: アップリフトランキングと累積効果の計算
WITH uplift_ranked AS (
-- STEP2の結果にランク付け
SELECT
rfm_segment,
price_segment,
treatment_count + control_count AS total_customers,
uplift_pct,
revenue_uplift_per_customer,
-- アップリフト降順でランク付け
ROW_NUMBER() OVER (ORDER BY uplift_pct DESC) AS uplift_rank
FROM segment_uplift -- STEP2の結果テーブル
),
with_cumulative AS (
SELECT
uplift_rank,
rfm_segment,
price_segment,
total_customers,
uplift_pct,
revenue_uplift_per_customer,
-- 累積顧客数
SUM(total_customers) OVER (ORDER BY uplift_rank ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
AS cumulative_customers,
-- 累積売上アップリフト(全顧客への配布と比較した効率)
SUM(revenue_uplift_per_customer * total_customers) OVER (
ORDER BY uplift_rank ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue_uplift,
-- 全セグメントへの配布で得られる総アップリフト
SUM(revenue_uplift_per_customer * total_customers) OVER () AS total_revenue_uplift,
-- 全顧客数
SUM(total_customers) OVER () AS total_customers_all
FROM uplift_ranked
)
SELECT
uplift_rank,
rfm_segment,
price_segment,
total_customers,
ROUND(uplift_pct, 1) AS uplift_pct,
ROUND(revenue_uplift_per_customer, 0) AS rev_uplift_per_cust,
cumulative_customers,
ROUND(cumulative_customers * 100.0 / total_customers_all, 1) AS cumulative_customer_pct,
ROUND(cumulative_revenue_uplift, 0) AS cumulative_rev_uplift,
ROUND(cumulative_revenue_uplift * 100.0 / NULLIF(total_revenue_uplift, 0), 1)
AS cumulative_uplift_pct -- 全体のアップリフトのうち何%をカバーしているか
FROM with_cumulative
ORDER BY uplift_rank;出力イメージ
| rank | rfm_segment | uplift_pct | cumulative_customer_pct | cumulative_uplift_pct |
|---|---|---|---|---|
| 1 | 離脱リスク・高単価 | +20.2 | 12.4% | 38.2% |
| 2 | 新規・低単価 | +16.3 | 24.8% | 67.4% |
| 3 | 活性・中単価 | +10.5 | 48.1% | 89.3% |
| 4 | ロイヤル・高単価 | +2.6 | 61.2% | 95.1% |
| 5 | ロイヤル・低単価 | +0.6 | 78.4% | 97.8% |
| 6 | 休眠・低単価 | -3.1 | 100.0% | 100.0% |
顧客全体の48%(ランク1〜3)に絞って配布するだけで、全員配布で得られるアップリフトの89%が達成できる。
残りの52%への配布はアップリフトをわずか11%しか追加しないうえ、最下位セグメントは逆効果だ。
これが「Persuadables だけを狙い撃ちする」ことの定量的な根拠だ。
STEP 4 ― ROI の比較(全員配布 vs アップリフト選別配布)
-- STEP4: 配布戦略別のROI比較
WITH
-- クーポンのコスト仮定
coupon_cost AS (
SELECT
0.20 AS discount_rate, -- 20%割引クーポン
200 AS coupon_unit_cost -- クーポン発行・配信コスト(1件あたり)
),
strategy_comparison AS (
SELECT
'全員配布' AS strategy,
8407 AS targeted_customers, -- 全処置群相当
712 AS expected_converters, -- 処置群の転換者数
712 * 2040 AS expected_revenue, -- 転換者の売上
-- クーポンコスト:転換者への割引 + 全員への配信コスト
712 * 2040 * 0.20 + 8407 * 200 AS coupon_cost_total,
-- 対照群との差分(純増売上)
(712 * 2040) - (506 * 1440) AS incremental_revenue
UNION ALL
SELECT
'上位3セグメント(48%)',
4040, -- ランク1〜3の顧客数
4040 * 0.155, -- 加重平均転換率(約15.5%)
4040 * 0.155 * 2200, -- 加重平均売上単価
4040 * 0.155 * 2200 * 0.20 + 4040 * 200,
4040 * 0.155 * 2200 * 0.89 -- アップリフトの89%を回収
)
SELECT
strategy,
targeted_customers,
ROUND(expected_revenue, 0) AS expected_revenue,
ROUND(coupon_cost_total, 0) AS coupon_cost,
ROUND(expected_revenue - coupon_cost_total, 0) AS net_revenue,
ROUND((expected_revenue - coupon_cost_total) * 100.0 / NULLIF(coupon_cost_total, 0), 1)
AS roi_pct
FROM strategy_comparison;出力イメージ
| strategy | targeted_customers | expected_revenue | coupon_cost | net_revenue | roi_pct |
|---|---|---|---|---|---|
| 全員配布 | 8,407 | 1,452,480 | 1,975,808 | -523,328 | -26.5% |
| 上位3セグメント(48%) | 4,040 | 1,295,064 | 1,355,048 | -59,984 | -4.4% |
全員配布では ROI がマイナス26.5%(コストが回収できない)だが、アップリフト選別配布では ROI を大幅に改善できる。
さらに「割引コストを削減しながら純増売上の89%を確保」という効率的な施策設計が実現する。
実務での運用ヒント
① セグメント数と各セグメントのサンプル数のトレードオフ
セグメントを細かく切るほどアップリフトの精度が上がるが、各セグメントのサンプル数が少なくなり推定が不安定になる。HAVING COUNT >= 30 という制限は最低限の目安だ。プロダクション環境では各セグメント100件以上を目標にする。
② X-Learner への発展
T-Learner では処置群と対照群のサンプル数が大きく異なると推定が偏る。X-Learner はこれを補正する発展的なアプローチだ。
SQLでの実装は複雑になるが、機械学習モデル(Python の causalml ライブラリ)を使って個人レベルのアップリフトを推定し、結果をTDにロードしてSQLで集計・活用するワークフローが現実的だ。
③ Do Not Disturb の検証と活用
アップリフトがマイナスのセグメントは「クーポンが届くとうんざりして離れる」可能性がある。
このセグメントへの施策を止めるだけでなく、「なぜ逆効果になるか」を分析することも重要だ。
セグメントの特性(購買頻度・在籍期間・クーポン使用歴)を掘り下げることで、「過剰な施策が顧客を疲弊させている」という組織的な問題が見えることがある。
まとめ
アップリフトモデリングのSQLによる近似実装をまとめる。
- A/Bテストで処置群・対照群を設計する
- セグメント別にT-Learner近似を計算する:各セグメントで
処置群CR − 対照群CRをアップリフトとする - アップリフトでランキングし、累積カバー率で「どこまで配布するか」の閾値を決める
- 4タイプ(Persuadable・Sure Thing・Lost Cause・Do Not Disturb)に分類して、Persuadables だけを施策対象にする
- ROI を比較して「全員配布 vs アップリフト選別配布」の差を定量化する
「クーポンを配ると売上が上がる」から「クーポンが効く顧客だけに配ると費用対効果が最大になる」への転換が、このクエリが提供する最大の価値だ。
マーケティング予算を「ばらまき」から「狙い撃ち」へと進化させる、データドリブンな施策設計の核心だ。