D ビジネス/PM — 販促クーポンの「効果報告書」を増分利益で書き直す(利用者 vs 非利用者比較の自己選択バイアス × 眠っていた10%ホールドアウトでITT比較 × 売上ではなく粗利−値引き−配信費 × 前倒し購入を拾う60日窓 × 信頼区間とSRM検査 × 事前登録の判断ルールと次回3群実験の検出力設計)

2026-09-24 木曜 木曜 D: ビジネス/PM ★★★★☆ ホールドアウト / ITT / 増分利益 SRM / CUPED / 事前登録

概要

🎯

比べる相手は「配られなかった人」

「使わなかった人」との比較には、誰が使うかという自己選択が混ざる。行動と無関係な割付で比べる。

💴

売上ではなく利益

増分粗利 − 値引き − 配信費。値引きは「どうせ買った人」にも払っている。

📆

窓は有効期限より長く

期限付き値引きは購入を前に寄せる。期限後の落ち込みまで入れて初めて「新しく生まれた購入」が見える。

📏

幅で出し、ルールは先に

95%CI と SRM 検査。継続・停止・保留の条件は結果を見る前に合意する。

問題

MOps 販促システムチームの新任 PdM が、8 月の誕生日クーポン施策の効果報告と来期予算の提案を経営会議に出そうとしている。あなたはレビューを頼まれた。

レビュー中に、配信基盤のコードで次の仕様を見つけた。

member_id のハッシュ下1桁が 0 の会員(約10%)は、2025年のメール負荷対策の名残で全キャンペーンの配信対象から外れている。割付ログは mart.fct_campaign_assignments(campaign_id, member_id, arm, assigned_at) に arm = 'treated' / 'holdout' で残っている。

この「偶然のホールドアウト群」の集計(会員単位、購入しなかった会員は 0 円として含む。値引き前・返品控除後):

群会員数観測窓1人あたり売上 平均同 分散(円²)
配信(treated)594,120配信後30日1,372.0 円20,523,216
ホールドアウト65,880配信後30日1,140.0 円17,210,400
配信(treated)594,120配信後60日2,422.0 円36,640,716
ホールドアウト65,880配信後60日2,248.8 円34,126,083

問題点を全て洗い出し(7点)、報告書と集計を書き直してください。

制約・前提条件

  • 誕生月の会員は月 約66万人(会員800万人 ÷ 12)。クーポンは 1,000円引き(5,000円以上で利用可、配信後30日有効)
  • 粗利率 35%(値引き前売上に対して。仮定)
  • 値引きは全額当社負担。配信費はメール1通1円(仮定)
  • 表の数字はすべて本問のための架空データ。以下の計算はこの表からの導出
  • 経営会議は「来期予算を増やすか・維持か・止めるか」を決める場。読み手は CFO と販促部長
  • 分析基盤: BigQuery、Python 3.13

期待する回答形式: (1) 問題点の列挙 (2) 改善後の集計 SQL と分析コード(Google スタイル docstring・インラインコメント・名前付き定数)(3) 実行例 (4) 書き直した報告書の冒頭(結論先出し)(5) 次回実験の設計 (6) 採らなかった案とその理由

悪い報告書 (Before)

この集計と報告書には 7つの問題 が隠れています。見つけてから先へ進んでください。
集計 SQL — 利用者だけ・14日・売上
-- 8月 誕生日クーポン効果集計
SELECT
  COUNT(DISTINCT r.member_id)          AS coupon_users,   -- 47,530
  SUM(o.order_amount)                  AS coupon_gmv,     -- 570,360,000
  SUM(r.discount_amount)               AS discount_cost,  -- 47,530,000
  SAFE_DIVIDE(SUM(o.order_amount) - SUM(r.discount_amount),
              SUM(r.discount_amount))  AS roi             -- 11.0
FROM `mops-project.mart.fct_coupon_redemptions` AS r
JOIN `mops-project.mart.fct_orders` AS o USING (order_id)
WHERE r.campaign_id = 'birthday_2026_08'
  AND o.ordered_at < TIMESTAMP_ADD(r.distributed_at, INTERVAL 14 DAY)
報告書の本文

8月 誕生日クーポン施策 効果報告

  • クーポン経由売上 5.70億円(利用者 47,530人 × 平均 12,000円)
  • 利用者の平均購入額 12,000円は、同月に購入した非利用者(9,500円)より 26%高い → クーポンが客単価を押し上げた
  • ROI 1,100%((売上 − 値引き) ÷ 値引き)
  • 提案: 来期はクーポン額を 1,000円 → 2,000円に倍増し、ハーフバースデーにも配信。年間値引き予算 5.7億円 → 22.8億円。ROI が同じなら年間クーポン経由売上は 68.4億円 → 273.6億円
問題点サマリー(7点)
1-2比較の相手 — 利用者 vs 非利用者の自己選択 / あるホールドアウトを使わない
3-5効果の定義 — 売上で見て利益で見ない / どうせ買った人への値引き / 14日窓で前倒しを見ない
6-7判断の作法 — 幅と SRM なし / 線形外挿・停止基準なし

ヒント(段階的開示)

ヒント1 — 方向性
知りたいのは「クーポンを使った人がいくら買ったか」ではなく、「クーポンを配らなかったら失われていた利益はいくらか」。比べる相手は「使わなかった人」ではなく「配られなかった人」。
ヒント2 — アプローチ
  • 「使った人」は自分で選んで使う。もともと買うつもりの人ほど使う(自己選択)。5,000円以上条件があるので購入額も機械的に底上げされる
  • ホールドアウトはハッシュで決まる=行動と無関係。配信群全員 vs ホールドアウト全員を比べる(ITT)。0円の人も落とさない
  • 価値は 増分粗利 − 値引き − 配信費。値引きは「クーポンが無くても買った人」にも払っている
  • 有効期限30日。前倒しなら 31〜60日目に配信群の購入がホールドアウトより減る
  • 信頼区間と、割付比 9:1 が保たれているか(SRM)を先に確かめる
ヒント3 — 骨格
1人あたり増分利益 = 粗利率 × (配信群の1人あたり売上 − ホールドアウトの1人あたり売上)
                  − 値引き総額 ÷ 配信群人数 − 配信単価
標準誤差(売上差) = sqrt(分散_t / n_t + 分散_h / n_h)
95%CI = 点推定 ± 1.96 × 粗利率 × 標準誤差

判断ルール(結果を見る前に決める):
  CI 下限 > 0 → 継続 / CI 上限 < 0 → 現行設計は停止 / それ以外 → 保留
30日窓と60日窓の両方で計算し、符号が変わるかを見る

問題点分析(7点)

#問題点分類何が起きているか改善方法
1利用者 vs 非利用者の比較比較の相手12,000円 vs 9,500円の26%差は「誰が使うか」と「5,000円条件」の差割付単位(配信/ホールドアウト)で全員比較
2対照群が無い(あるのに使わない)比較の相手10%のホールドアウトが割付ログにあるのに無視fct_campaign_assignments の arm で ITT
3売上を効果と呼び利益で見ない効果の定義ROI 式が原価を引いていない。粗利率35%なら5.70億円でも粗利2.0億円粗利率 × 増分売上 − 値引き − 配信費
4どうせ買った人への値引き効果の定義配信群の購入者の約86%は配らなくても買った計算(12.0% ÷ 14.0%)値引きは利用者全員分を費用に計上
514日窓・前倒し購入を無視効果の定義有効期限30日より短い窓。期限後の落ち込みが見えない60日窓(期限30日+戻り30日)
6信頼区間・SRM検査なし判断の作法偶然の差か、割付が壊れていないか分からない95%CI + χ² > 10.83 なら停止
7線形外挿・停止基準なし判断の作法額面への反応を測らずに予算4倍(22.8億円)事前登録の判断ルール + 3群実験

比較設計と利益の分解 — Bad vs Good(SVG)

① 誰と誰を比べるか Bad: 使った人 vs 使わなかった人(自己選択) 配信 594,120人 (ホールドアウトは無視) 本人が選ぶ 利用者 47,530人 平均12,000円 もともと買う人 + 5,000円以上条件で底上げ 非利用の購入者 平均9,500円 買う気が弱い人が多い 差 26% = クーポンの効果? → 人の違いが混ざる しかも「利用者の売上 5.70億円」全額を効果と数える Good: 配られた全員 vs 配られなかった全員(ITT) 誕生月 660,000人 hash(member_id) 下1桁 行動と無関係 配信 594,120人(90%) 使った人も使わない人も 0円の人も全員 ホールドアウト 65,880人(10%) 「配らなかったら」の世界 差 = 施策だけの効果(SRM χ²=0.24 で割付OK) 1人あたり売上差 × 配信人数 = 増分売上 ② 配信対象1人あたり増分利益の分解(円/人、導出) 0 +80 −40 30日窓 増分粗利 +81.2 0.35×232円 値引き −80.0 47,530×1,000÷594,120 配信 −1.0 +0.2 95%CI −11.6〜+12.0 → 判断保留 60日窓(前倒しの戻りを含む) 増分粗利 +60.6 0.35×173.2円 値引き −80.0 配信 −1.0 −20.4 95%CI −36.9〜−3.9 → 現行設計は停止 売上リフトは 30日 +232円 → 60日 +173.2円(31〜60日目に −58.8円=前倒し購入の戻り)。値引き 80円/人は固定なので、利益の符号がマイナスに変わる。

上段: Bad は本人の選択で分かれた2群を比べ、Good は行動と無関係に分かれた2群を比べる。下段: 同じデータでも 30日で止めると「±0」、60日まで見ると「マイナスが確定」する。

模範解答

黄色 = 設計意図のコメント、緑 = Bad の問題番号を直した箇所。

Before — 利用者だけ・14日・売上
SELECT
  COUNT(DISTINCT r.member_id)          AS coupon_users,
  SUM(o.order_amount)                  AS coupon_gmv,   -- ③④ 売上全額を効果に
  SUM(r.discount_amount)               AS discount_cost,
  SAFE_DIVIDE(SUM(o.order_amount) - SUM(r.discount_amount),
              SUM(r.discount_amount))  AS roi  -- ③ 原価を引かない
FROM `mops-project.mart.fct_coupon_redemptions` AS r  -- ①② 使った人だけ
JOIN `mops-project.mart.fct_orders` AS o USING (order_id)
WHERE r.campaign_id = 'birthday_2026_08'
  AND o.ordered_at < TIMESTAMP_ADD(r.distributed_at, INTERVAL 14 DAY)  -- ⑤ 14日
-- ⑥ 群も分散も出ない → 幅も SRM も計算できない
After — 割付単位で全員・60日・平均と分散
-- ①② 配信対象に割り付けられた「全員」を群ごとに集計する(ITT)。
-- クーポンを使ったかどうかでは分けない。
DECLARE campaign STRING DEFAULT 'birthday_2026_08';
DECLARE window_days INT64 DEFAULT 60;  -- ⑤ 有効期限30日 + 前倒しの戻りを見る30日

WITH assigned AS (
  -- 割付はハッシュを再計算せず、配信基盤が実際に記録したログを使う
  SELECT member_id, arm, assigned_at
  FROM `mops-project.mart.fct_campaign_assignments`
  WHERE campaign_id = campaign
),
member_gmv AS (
  SELECT
    a.member_id,
    a.arm,
    -- ① 買わなかった会員は 0円。LEFT JOIN と COALESCE を外すと選択バイアスが戻る
    COALESCE(SUM(o.gross_amount - o.refunded_amount), 0) AS gmv_yen  -- 値引き前・返品控除後
  FROM assigned AS a
  LEFT JOIN `mops-project.mart.fct_orders` AS o
    ON o.member_id = a.member_id
   AND o.ordered_at >= a.assigned_at
   AND o.ordered_at <  TIMESTAMP_ADD(a.assigned_at, INTERVAL window_days DAY)
   AND o.status != 'cancelled'
  GROUP BY a.member_id, a.arm
)
SELECT
  arm,
  COUNT(*)          AS members,       -- ⑥ SRM 検査の入力
  AVG(gmv_yen)      AS mean_gmv_yen,
  VAR_SAMP(gmv_yen) AS var_gmv         -- ⑥ 信頼区間の入力
FROM member_gmv
GROUP BY arm;

-- 値引き総額の元になる利用者数(配信群のみ)
SELECT COUNT(DISTINCT member_id) AS redeemed
FROM `mops-project.mart.fct_coupon_redemptions`
WHERE campaign_id = campaign;
Before — 報告書の計算
coupon_gmv = 47_530 * 12_000        # 5.70億円を「効果」と呼ぶ ①③④
roi = (coupon_gmv - 47_530_000) / 47_530_000   # 11.0 → 1,100% ③
uplift_vs_nonuser = 12_000 / 9_500 - 1         # 26% ①

# ⑦ 額面2倍 × 回数2倍 → 売上も4倍と外挿
next_budget = 47_530_000 * 12 * 2 * 2          # 22.8億円
next_gmv    = coupon_gmv * 12 * 4              # 273.6億円
# ⑥ 幅なし・SRM なし・止める条件なし
After — coupon_lift.py(増分利益・95%CI・判断ルール)
"""誕生日クーポン施策の増分利益を、ホールドアウト群との比較(ITT)で推定する。

入力は BigQuery の集計結果(群ごとの会員数・1会員あたり売上の平均と分散)。
「クーポンを使った人」ではなく「配信対象に割り付けられた全員」を比べるので、
自己選択バイアス(もともと買う人ほど使う)が入らない。
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from enum import StrEnum

# ③ 粗利率(値引き前売上に対する比率)。経理の月次粗利率実績を使う(本問では仮定値)
GROSS_MARGIN_RATE = 0.35
# クーポン額面。値引きは全額が当社負担(メーカー協賛なし)
COUPON_FACE_VALUE_YEN = 1_000
# 配信1通あたりの費用(メール配信 SaaS の従量単価。仮定値)
SEND_COST_PER_MEMBER_YEN = 1.0
# ⑥ 95% 信頼区間の係数(正規近似。各群 6 万人以上あるので妥当)
Z_95 = 1.96
# 配信基盤が member_id ハッシュ下1桁=0 を配信対象外にしている比率
EXPECTED_HOLDOUT_RATIO = 0.10
# ⑥ SRM(割付比の崩れ)検査の閾値。自由度1のカイ二乗で p=0.001 に相当
SRM_CHI2_THRESHOLD = 10.83


class Decision(StrEnum):
    """事前に決めた判断ルールの出力。"""

    CONTINUE = "継続(95%CI の下限が 0 円超)"
    STOP = "現行設計は停止(95%CI の上限が 0 円未満)"
    INCONCLUSIVE = "判断保留(0 円を含む。検出力を上げて再測定)"


@dataclass(frozen=True, slots=True)
class ArmStats:
    """1群分の集計。購入しなかった会員も売上 0 円として含む。

    Attributes:
        members: 群に割り付けられた会員数。
        mean_gmv_yen: 観測期間内の 1 会員あたり値引き前売上の平均。
        var_gmv: 同じく 1 会員あたり売上の標本分散(円^2)。
    """

    members: int
    mean_gmv_yen: float
    var_gmv: float


@dataclass(frozen=True, slots=True)
class Estimate:
    """点推定と 95% 信頼区間(円/配信対象1人)。"""

    point: float
    low: float
    high: float


def srm_chi2(treated: int, holdout: int, expected_holdout_ratio: float) -> float:
    """割付比が設計通りかを確かめるカイ二乗統計量を返す。

    これが閾値を超えたら、割付か集計のどこかが壊れている。効果の数字を読む前に止める。
    """
    total = treated + holdout
    exp_h = total * expected_holdout_ratio
    exp_t = total - exp_h
    return (holdout - exp_h) ** 2 / exp_h + (treated - exp_t) ** 2 / exp_t


def incremental_profit_per_member(
    treated: ArmStats, holdout: ArmStats, redeemed: int
) -> Estimate:
    """配信対象1人あたりの増分利益(粗利 − 値引き − 配信費)を推定する。

    値引きは「使った全員分」を差し引く。クーポンが無くても買った人への値引きも
    ここで費用として効く(Bad の報告ではこれが「効果」に数えられていた)。

    Args:
        treated: 配信群の集計。
        holdout: ホールドアウト群の集計。
        redeemed: 配信群でクーポンを使った会員数。

    Returns:
        増分利益の点推定と 95% 信頼区間。値引き額と配信費は確定値として扱う。
    """
    lift_gmv = treated.mean_gmv_yen - holdout.mean_gmv_yen  # ①② 割付単位の差
    se_gmv = math.sqrt(treated.var_gmv / treated.members + holdout.var_gmv / holdout.members)
    discount_per_member = redeemed * COUPON_FACE_VALUE_YEN / treated.members  # ④
    point = GROSS_MARGIN_RATE * lift_gmv - discount_per_member - SEND_COST_PER_MEMBER_YEN  # ③
    half = Z_95 * GROSS_MARGIN_RATE * se_gmv
    return Estimate(point=point, low=point - half, high=point + half)


def decide(est: Estimate) -> Decision:
    """事前登録した判断ルール。結果を見てからルールを変えない。"""
    # ⑦ 継続・停止・保留の条件を先に決めておく
    if est.low > 0:
        return Decision.CONTINUE
    if est.high < 0:
        return Decision.STOP
    return Decision.INCONCLUSIVE


if __name__ == "__main__":
    REDEEMED = 47_530
    chi2 = srm_chi2(594_120, 65_880, EXPECTED_HOLDOUT_RATIO)
    print(f"SRM chi2 = {chi2:.2f} (閾値 {SRM_CHI2_THRESHOLD}) -> {'OK' if chi2 < SRM_CHI2_THRESHOLD else 'STOP'}")
    windows = {  # ⑤ 30日と60日を並べ、符号が変わるかを見る
        "30日": (ArmStats(594_120, 1_372.0, 20_523_216), ArmStats(65_880, 1_140.0, 17_210_400)),
        "60日": (ArmStats(594_120, 2_422.0, 36_640_716), ArmStats(65_880, 2_248.8, 34_126_083)),
    }
    for label, (t, h) in windows.items():
        est = incremental_profit_per_member(t, h, REDEEMED)
        total = [round(x * t.members / 10_000) for x in (est.point, est.low, est.high)]
        print(
            f"[{label}] 売上リフト {t.mean_gmv_yen - h.mean_gmv_yen:+.1f} 円/人 | "
            f"増分利益 {est.point:+.1f} 円/人 (95%CI {est.low:+.1f} 〜 {est.high:+.1f}) | "
            f"総額 {total[0]:+,} 万円 ({total[1]:+,} 〜 {total[2]:+,}) | {decide(est)}"
        )
問題直し方効果
① 利用者 vs 非利用者割付ログの arm で全員比較、0円を含む自己選択が消える
② 対照群を使わないfct_campaign_assignments の holdout を使用反実仮想が手に入る(追加費用0円)
③ 売上で評価粗利率 × 増分売上 − 値引き − 配信費5.70億円「効果」→ 利益 −1,211万円
④ どうせ買った人への値引き値引きを利用者全員分、費用に計上80円/人のコストが見える
⑤ 14日窓60日窓(期限30日+戻り30日)31〜60日目の −58.8円/人が見える
⑥ 幅・SRMなし95%CI + χ² 検査「保留」と「停止」を区別できる
⑦ 外挿・停止基準なし事前登録の判断ルール + 3群実験予算 22.8億円の提案を根拠つきで取り下げ

実行例(input → output)

input: 問題文の表(2窓 × 2群)と利用者数 47,530人。

output(python3 coupon_lift.py を 2026-09-24 に実行した実際の出力 実測):

SRM chi2 = 0.24 (閾値 10.83) -> OK
[30日] 売上リフト +232.0 円/人 | 増分利益 +0.2 円/人 (95%CI -11.6 〜 +12.0) | 総額 +12 万円 (-689 〜 +713) | 判断保留(0 円を含む。検出力を上げて再測定)
[60日] 売上リフト +173.2 円/人 | 増分利益 -20.4 円/人 (95%CI -36.9 〜 -3.9) | 総額 -1,211 万円 (-2,192 〜 -230) | 現行設計は停止(95%CI の上限が 0 円未満)
指標Bad の報告Good(30日)Good(60日)
「効果」の定義利用者の売上 5.70億円増分売上 232円 × 594,120人 ≒ 1.38億円増分売上 173円 × 594,120人 ≒ 1.03億円
利益記載なし(ROI 1,100%)+12万円(−689〜+713万円)−1,211万円(−2,192〜−230万円)
判断予算4倍保留現行設計は停止
  • 30日では売上は確かに増えている(+232円/人、CI も 0 を含まない)。しかし粗利 81.2円/人に対し値引きが 80.0円/人で、利益はほぼ 0
  • 31〜60日目は配信群の売上がホールドアウトを下回る(1,050円 vs 1,108.8円 → −58.8円/人)。前倒し購入の戻り
  • Bad が「効果」と呼んだ 5.70億円と増分売上 1.38億円の差 4.3億円強は「配らなくても起きていた購入」

書き直した報告書の冒頭(結論先出し)

8月 誕生日クーポン(1,000円引き): 60日間の増分利益は −1,211万円(95%信頼区間 −2,192万〜−230万円)。現行設計のまま拡大する提案は取り下げます。

  • 比較方法: 配信対象外だった10%の会員(65,880人、割付はハッシュで行動と無関係)と、配信した594,120人を、使ったかどうかに関係なく全員で比べました。割付比は設計通り(SRM検査 χ²=0.24)
  • 売上は増えました(30日で1人 +232円)。ただし配らなくても買った人への値引きが大きく、粗利の増分をほぼ打ち消しました。さらに31〜60日目に買い控えが出ています(前倒し購入)
  • 以前の報告の「ROI 1,100%」は、クーポンを使った人の売上をすべて効果に数えた数字で、比較の相手がいませんでした
  • お願い: 9〜10月の2か月で「配信なし/500円/1,000円」の3群比較を行い、11月末の会議で継続・減額・停止を決めさせてください。追加費用は0円(予算は現行の範囲内)、判断ルールは本日この場で合意したいです

次回実験の設計

項目設計理由
群配信なし 20% / 500円 40% / 1,000円 40%額面への反応(弾力性)を測らないと「倍増」も「減額」も判断できない
期間9月・10月の誕生月会員(計 約132万人)1か月分では検出力が足りない(下記)
主要指標60日の ITT 増分利益(円/配信対象1人)前倒しの戻りを含む
ガードレール配信停止率、誕生月の退会率、問い合わせ件数利益が出ても顧客体験を壊していないか
判断ルールCI下限>0 → 採用 / 両腕ともCI上限<0 → 金銭クーポン停止 / それ以外 → 500円で継続し再測定結果を見る前に経営会議で合意(事前登録)
割付検査毎日 SRM を計算、χ² > 10.83 なら集計停止割付が壊れた数字で判断しない
検出力(導出): 60日の1人あたり売上の分散 3,664万円²(今回の実績)で、1,000円群 264,000人 vs 配信なし 132,000人(1か月分)を比べると、検出力80%・有意水準5%の MDE(見つけられる最小の差)は売上 57円/利益 20円。今回の −20円とほぼ同じ大きさで、真の差がちょうど −20円なら1か月では約2割の確率で見逃す。2か月分にすると利益 MDE は約14円。前期間の購入額を共変量にする CUPED でさらに縮む(縮む割合は自社データで要実測)。

迷った案と採らなかった理由

案長所採らなかった理由前提が変わったら
A. 前年同月比・施策前後比較データがすぐある季節・他施策・価格改定と区別できない。ランダムな対照群が手元にあるホールドアウトを取れない施策(TV CM・全店値下げ)なら地域単位の差の差分析(DiD)
B. アップリフトモデル(効きそうな会員だけに配る)どうせ買う人への値引きを減らせる本命学習にランダム割付データが要る。1か月分では各群の人数が足りない。まず平均効果の符号を確かめる3群実験の2か月分が溜まったら学習し、4群目「モデルが選んだ会員だけ」で検証
C. 利用者だけで効果を見る(TOT)「使った人にどれだけ効いたか」が分かる経営が決めるのは「配るかどうか」なので ITT が意思決定の単位。TOT は ITT ÷ 利用率で後から出せる(232 ÷ 0.08 ≒ 2,900円/利用者)利用率を上げる施策(リマインド配信)の評価では TOT も並べる
D. 30日窓で判断判断が1か月早い30日では「保留」、60日で符号がマイナスに確定。短い窓は前倒しを効果に数える定期購入など前倒しが起きにくい商材なら30日で足りる
E. ホールドアウトをやめて全員に配る特典が無い会員の不満が出ない測れなくなる。10%に配らないことで失う利益は今回マイナスだった効果が確かめられた後はホールドアウトを2〜5%に縮めて継続監視(規約の記載を法務に確認)

ポイント解説

1比べる相手は「配られなかった人」 — 利用者と非利用者の差には「どんな人が使うか」が混ざる。行動と無関係な割付で比べて初めて差を施策のせいと言える。
2売上リフトと利益リフトは符号が変わりうる — 売上 +232円でも、値引きは「どうせ買った人」にも払うので利益は ±0。
3観測窓は有効期限より長く — 期限後の落ち込みまで入れて初めて「新しく生まれた購入」が見える。
4幅で出し、判断ルールは先に決める — 結果を見てからルールを変えると、どんな結果でも「効果あり」と言えてしまう。
5SRM を効果より先に見る — 9:1 が崩れていたら除外ロジックか JOIN が壊れている。その数字は読まない。

実務への応用

  • カート放棄メール・休眠会員クーポン・ポイント2倍デーにも「ハッシュ下1桁=0」除外が効いている可能性が高い。arm='holdout' がある全キャンペーンに同じ SQL と Python を回せば、予算の付け替え表がそのまま作れる
  • 9/23 の消去処理で member_id が ERASED になった注文はどちらの群からも落ちる。割付はランダムなので両群から同じ割合で落ち、比較は歪まない(ただし「クーポンメールが嫌で退会」のように消去が片方の群に偏る仕組みがあれば歪む。退会率をガードレールに入れた理由の一つ)
  • 経営会議の資料は「数字 → 幅 → 判断 → お願い」の順。CFO が知りたいのは「いくら儲かったか・どれくらい確かか・何を決めればよいか」

扱わなかった範囲(と理由)

  • 長期の顧客価値(LTV)・ブランドへの影響 — 60日では測れない。退会率・配信停止率をガードレールにして監視し、半年後のリピート率比較を発展課題に
  • 景品表示法 — 値引きは原則として景品類に当たらない(消費者庁「景品類等の指定の告示の運用基準」)。抽選型に変えるなら懸賞規制の確認が要る
  • 会計処理 — クーポン値引きは売上高から控除(収益認識に関する会計基準)。本問の利益額は P/L 表示と同じ金額になる。表示の詳細は E カテゴリで
  • 商品別の粗利率の違い — 粗利率35%は全体平均の仮定。低粗利商品に偏れば増分利益はさらに下がる。fct_order_items の SKU 別粗利で精緻化できる
  • 同時配信キャンペーンの干渉 — ホールドアウトは全施策共通なので「全施策まとめた効果」は測れるが、個別の切り分けは別設計が要る

一次資料(参照日 2026-09-24)

今日のまとめ

販促の効果は「使った人がいくら買ったか」ではなく「配らなかったら失われていた利益」であり、それは行動と無関係に割り付けた対照群とだけ比べて測れる。 売上ではなく粗利−値引き−配信費で、有効期限より長い窓で、幅つきで出し、判断ルールは結果を見る前に決める。

次のステップ

  • 発展問題: 3群実験の結果、500円群が60日で +8円/人(95%CI −4〜+20)、1,000円群が −18円/人(−30〜−6)だった。判断ルールに従うとどうなるか。さらに「500円を休眠会員(過去180日購入なし)だけに配る」アップリフト施策を提案するなら、どんな実験を追加し何人必要か
  • 参考: ITT / TOT、SRM、CUPED、post-promotion dip、アップリフトモデリング、OEC とガードレール指標、事前登録

自己評価(あとで記入)