本文へ移動
効果検証SQL 更新日: 2026年5月2日 約26分で読めます

アップリフトモデリングをSQLで近似する方法|施策が効く顧客だけを抽出する


「クーポンを配ると売上が上がる」は罠だ

マーケティングでよくある落とし穴がある。

クーポンを全顧客に配った。転換率が上がった。「クーポンは効果があった」と結論付ける。
しかしよく考えると、顧客は次の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_groupcustomer_countconvertersconversion_rate_pctavg_revenue_per_customer
control4,21850612.001,440
treatment4,18971217.002,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_segmentprice_segmenttreatment_crcontrol_cruplift_pctrevenue_upliftuplift_type
離脱リスク(高頻度・遠い)高単価28.48.2+20.2+3,840Persuadable(高アップリフト)
新規(初回のみ)低単価22.15.8+16.3+1,240Persuadable(高アップリフト)
活性(中頻度・直近あり)中単価18.98.4+10.5+1,580Persuadable(高アップリフト)
ロイヤル(高頻度・高直近)高単価19.817.2+2.6+380Persuadable(低アップリフト)
ロイヤル(高頻度・高直近)低単価15.414.8+0.6+90効果なし
休眠(低頻度・遠い)低単価4.17.2-3.1-420Do 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;

出力イメージ

rankrfm_segmentuplift_pctcumulative_customer_pctcumulative_uplift_pct
1離脱リスク・高単価+20.212.4%38.2%
2新規・低単価+16.324.8%67.4%
3活性・中単価+10.548.1%89.3%
4ロイヤル・高単価+2.661.2%95.1%
5ロイヤル・低単価+0.678.4%97.8%
6休眠・低単価-3.1100.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;

出力イメージ

strategytargeted_customersexpected_revenuecoupon_costnet_revenueroi_pct
全員配布8,4071,452,4801,975,808-523,328-26.5%
上位3セグメント(48%)4,0401,295,0641,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による近似実装をまとめる。

  1. A/Bテストで処置群・対照群を設計する
  2. セグメント別にT-Learner近似を計算する:各セグメントで 処置群CR − 対照群CR をアップリフトとする
  3. アップリフトでランキングし、累積カバー率で「どこまで配布するか」の閾値を決める
  4. 4タイプ(Persuadable・Sure Thing・Lost Cause・Do Not Disturb)に分類して、Persuadables だけを施策対象にする
  5. ROI を比較して「全員配布 vs アップリフト選別配布」の差を定量化する

「クーポンを配ると売上が上がる」から「クーポンが効く顧客だけに配ると費用対効果が最大になる」への転換が、このクエリが提供する最大の価値だ。

マーケティング予算を「ばらまき」から「狙い撃ち」へと進化させる、データドリブンな施策設計の核心だ。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む