ユーザー単位で分けられないとき ― 地域実験と switchback
テレビCMのように誰に当たったかを記録できない広告、配送枠や在庫のような供給側の制約、市場全体に一斉に効く価格改定。こうしたユーザー単位でランダムに割り付けられない施策の効果を測る方法を扱います。地域を割付単位にする地域実験と、時間で切り替えるswitchbackの設計を整理し、因果連載のDiD・合成コントロールと地続きであることを、SOLNAのデータとPythonの最小構成で示します。
by Shin
テレビCMのように誰に当たったかを記録できない広告、配送枠や在庫のような供給側の制約、市場全体に一斉に効く価格改定——こうした「ユーザー単位でランダムに割り付けられない」施策は、どう効果を測ればいいのか。答えは、割付の単位(処置をどの単位に当てるか)をユーザーから上にずらすことです。地域で分ける、あるいは時間で切り替える(switchback)。地域差・時間差を反実仮想の代わりに使い、そこから効果を取り出します。この記事では、架空のD2C企業SOLNAのデータを使って 地域実験 と switchback の設計を最小構成のコードまで落とし、最後に因果連載のDiD・合成コントロールと地続きであることを示します。
棲み分けを最初に宣言しておきます。因果連載は「観測データから因果に迫る」側で、割付は自然に起きたものを後から拾います。本連載は「介入を設計して因果を測る」側で、割付を自分で作ります。地域実験は推定式こそ因果連載のDiDと同じですが、地域の処置を自分でランダムに割り当てる点が決定的に違います。観測側の手前の判断は因果連載に譲ります。
ユーザー単位で分けられない施策とは
A/Bの前提は、ユーザーをランダムに2群へ分け、片方だけに施策を当てて、群間の差を効果とみなすことです。これが崩れるのは、次の3つのどれかが起きるときです。
- 干渉(spillover)がある。あるユーザーへの処置が別のユーザーの結果に漏れる。ブランド広告は世帯・地域で共有され、配送枠や在庫のような供給側の制約は他の注文の体験を変え、価格や検索順位は市場全体に効きます。これはSUTVA(処置の独立性)の破れで、ユーザーで分けても処置群と対照群が汚染し合います。
- 広告接触時点でユーザーを識別できない。テレビ・OOH・ブランド検索・店頭のように、誰に当たったかを個人IDで記録できない面では、そもそもユーザー単位の割付ログが作れません。
- 介入の単位がユーザーでない。「地域配信のオン/オフ」「催事の出店」「市場単位の価格変更」など、施策のスイッチが地域や市場の側に付いている。
この3つに共通する処方箋は1つで、割付の単位を上にずらすことです。ユーザーで分けられないなら地域で分ける(地域実験)、地域でも分けられないなら時間で分ける(switchback)。代償は検出力です。割付単位が数百万ユーザーから数十地域、数十の時間ブロックへ落ちるので、実効サンプルは桁違いに小さくなります。検出できるのは大きな効果だけになる、という前提から設計を始めます。
地域実験(地域を割付単位にする)
地域実験は、地域を処置群と対照群にランダムに分け、処置地域だけで施策(ここでは地域配信)をオンにし、地域×期間のパネルで差を取ります。SOLNAは催事と百貨店カウンターで全国に20前後の地域接点を持つので、地域配信の効果検証はこの設計に乗ります。
推定は二元固定効果のDiD(差分の差分)です。地域の固定効果でベースラインの高低を、週の固定効果で全国共通の季節・トレンドを吸収し、残った treated × post の係数を効果として読みます。観測のDiDと式は同じですが、ここでは処置の割付を自分でランダムにかけているので、平行トレンドは「仮定」ではなく「設計でほぼ担保したもの」になります。これが地域実験の強みです。
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
# SOLNA: region x week のパネル(orders を地域×週で集計したもの想定)
# 列: region, week, sales, treated(地域割付), post(介入週以降=1)
# treated は事前に「ランダムかつ事前期間の水準でバランス」させて割り付ける
m = smf.ols(
"sales ~ treated:post + C(region) + C(week)",
data=geo,
).fit(
cov_type="cluster", # 地域内の相関を許す
cov_kwds={"groups": geo["region"]}, # クラスタは「地域」
)
lift = m.params["treated:post"]
ci = m.conf_int().loc["treated:post"]
base = geo.loc[(geo.treated == 1) & (geo.post == 0), "sales"].mean()
print(f"abs lift = {lift:.2f} ({100 * lift / base:.1f}% of baseline)")
print(f"95% CI = [{ci[0]:.2f}, {ci[1]:.2f}] p = {m.pvalues['treated:post']:.3f}")設計で外してはいけない点が3つあります。
第一に、標準誤差は地域でクラスタすること。同じ地域の週次観測は相関するので、観測を独立とみなすと標準誤差を過小評価し、効果を過信します。cov_type="cluster" で地域をクラスタに置くのはこのためです。ただしクラスタ数(=地域数)が20前後と少ないと、クラスタロバスト標準誤差自体が不安定になります。地域数が一桁台なら、wild cluster bootstrap など別の対処が要ります。
第二に、割付前にバランスを取ること。地域はもともと売上水準も成長率もバラバラなので、単純なランダム割付では処置群と対照群が事前期間で偏ることがあります。事前期間の水準・トレンドが揃うように層化して割り付ける(matched-market設計)と、推定の分散が大きく下がります。これは第5回の分散削減(CUPED)を地域版にした発想で、事前共変量で結果の分散を削る点で同型です。
第三に、検出力は地域数で決まること。地域実験の最小検出効果(MDE)は、ざっくり ( は地域数)で効きます。地域が20なら、検出できるのは数%以上の比較的大きな効果に限られます。SOLNAの地域配信検証では、推定リフトが5〜8%のレンジに乗り、95%信頼区間の下限が0を割り込む手前で踏みとどまる、という程度の解像度でした。「効いた/効かない」の二択は出せても、「何%効いた」を狭い区間で言うには地域が足りません。これは限界として正直に報告します。
switchback(時間で切り替える)
地域でも分けられない施策があります。市場が単一に集中している、あるいは施策が市場全体に同時に効く(価格・供給・検索順位など)場合です。このときは、時間を割付単位にします。一定の時間ブロックごとに施策をオン/オフで切り替え、onブロックとoffブロックの結果差を効果とみなす——これがswitchbackです。
最大の敵はキャリーオーバー(持ち越し)と自己相関です。あるブロックの処置が次のブロックに尾を引くと、offブロックがonの持ち越しで汚染され、効果が薄まって見えます。対策は、ブロック間に効果が消えるだけのウォッシュアウト期間を挟むことと、ブロックを十分に長くとること。標準誤差は時系列の相関を許すHAC(Newey–West)か、ブロッククラスタで取ります。
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
# SOLNA: 単一市場で 6時間ブロック x 14日 = 56ブロック
# 列: block(時刻), treat(ブロック単位のランダム割付 0/1), y(成果)
sb["hour"] = sb["block"].dt.hour
sb["dow"] = sb["block"].dt.dayofweek
# 時刻・曜日の周期を固定効果で吸収し、treat の係数を効果として読む
m = smf.ols("y ~ treat + C(hour) + C(dow)", data=sb).fit(
cov_type="HAC", cov_kwds={"maxlags": 4} # 時系列の相関に頑健な標準誤差
)
eff = m.params["treat"]
ci = m.conf_int().loc["treat"]
print(f"effect = {eff:.2f}")
print(f"95% CI = [{ci[0]:.2f}, {ci[1]:.2f}] p = {m.pvalues['treat']:.3f}")このコードを、真の効果を +3.0/持ち越し係数 0.3 で生成したデータに当てると、推定効果は +2.3 前後に減衰します。キャリーオーバーを設計で抑え込まない限り、switchbackは効果を過小評価する、という事実がそのまま数字に出ます。誇張の逆で、こちらは「効いているのに小さく見える」方向の事故です。ウォッシュアウトを入れ忘れたまま「効果は小さい」と結論づけるのが、この設計でいちばん多い読み違えです。
switchbackが成立する前提は、効果がブロック長に対して短命であることです。持ち越しがブロックをまたいで長く残る施策(ブランド想起の蓄積など)では、オン/オフの境界が意味を失い、この設計は壊れます。施策の効果の時定数を見積もってから、ブロック長とウォッシュアウトを決める順番を守ります。
因果連載との地続き(DiD・合成コントロール)
ここまでの推定の中身は、因果連載で扱う観測手法とほぼ同じ機械です。地域実験のDiDは因果連載のDiDと同じ式で、switchbackの時間パネルも構造としては時系列の介入分析です。違いは割付の出どころだけ——本連載は処置を自分でランダムに割り当て(介入)、因果連載は自然に起きた処置を後から拾う(観測)。だからこそ、本記事は設計の側に力点を置きます。
そして、地域をランダムに分けられないときの逃げ道が合成コントロール(とCausalImpact)です。処置地域に対して、複数の対照地域を重み付けで合成し、「もし処置がなかったら」の系列を作り出す。地域実験が対照地域をランダムに割り当てるのに対し、合成コントロールは対照を構築する。目的は同じ反実仮想で、ルートが違うだけです。因果連載で扱った取引が少なく単一市場のB2B——対照地域も明確な閾値も乏しい構造——では、ランダム化できないぶん、この構築型が現実解になります。
反実仮想を得る2つのルート
介入(本連載) : 地域をランダムに分ける → DiD で群間差を読む
観測(因果連載): 対照を重みで合成する → 合成コントロール/CausalImpact実務の順序としては、ランダム化できる構造なら必ず地域実験を選びます。割付を自分で作れる時点で、平行トレンドが仮定ではなく設計になり、推定の信頼性が一段上がるからです。それができない構造に落ちて初めて、観測ベースの合成コントロールに降ります。どちらの結論にも交絡の取りこぼしと反実仮想の質という限界が残るので、報告ではその限界を必ず添えます。
参考文献は最小限に。オンライン制御実験の軸はKohavi ほか『A/Bテスト実践ガイド』で、地域実験・干渉・割付単位の章が本記事の下敷きです。DiDの推定そのものは効果検証入門(ホクソエム)が手を動かす土台になります。
章末チェックリスト
- 分けられない理由を見分けたか(干渉・広告接触時点の非識別・介入単位がユーザーでない)
- 地域実験とswitchbackを、施策の効果の時定数と市場構造で使い分けたか
- 地域実験で標準誤差を地域でクラスタし、クラスタ数の少なさを意識したか
- 割付前に事前期間でバランス(matched-market)を取ったか
- switchbackでウォッシュアウトを設計し、キャリーオーバーによる過小評価を点検したか
- DiD/合成コントロールとの関係(介入 vs 観測)を言えるか
- 結論に交絡・反実仮想の質という限界を添えたか
地域実験も合成コントロールも、求めているのは同じ反実仮想です。割付を自分で作れるなら介入で、作れないなら観測で取りに行く——その判断と、観測側の手法の前提・順序は、対になる因果編で扱っています。最終回は、こうして測った結果を単発で終わらせず、意思決定と文化に組み込む話に進みます。