メインコンテンツまでスキップ

DiD(差の差法)とは?

概要 (画像は、Geminiで作成されたものです)

DiDの概要

DiD(Difference-in-Differences:差の差法) は、計量経済学や統計学の分野で古くから用いられている、政策やビジネス施策の「因果効果」を測るための代表的な疑似実験アプローチです。

高度な時系列手法であるCausalImpactのベースとなった手法でもあり、CausalImpactのベースとなる論文内でも「広く使われている『差の差の分析(DID)』を状態空間モデルへと拡張」したと述べられています。DiDは、シンプルながら非常に強力なロジックで「反事実(もし施策を行っていなかったらどうなっていたか)」を推定します。

施策の効果を測る際、私たちはよく以下のような「単純な比較」をしてしまいがちです。

  1. 前後比較: 施策を実施したグループの「施策前」と「施策後」の数値を比べる。
  2. グループ間比較: 施策を実施した時期に、「実施したグループ」と「実施していないグループ」の数値を比べる。

しかし、これらの単純な比較には大きな落とし穴があります。前後比較では「季節要因や市場全体のトレンド変化(例:たまたま繁忙期だった)」という外部要因を施策効果と誤認してしまいますし、グループ間比較では「そもそも両グループが持っていた潜在的な実力差や特性の違い」を無視してしまいます。

DiDは、この2つの比較の弱点を補い合うため、データを 「介入群(施策を受けたグループ)」「統制群(施策を受けていないグループ)」 の2つに分け、さらにそれらを 「施策前」「施策後」 の2つの期間で観測します。

統制群の「時間的な変化(トレンド)」を観察することで、市場全体の自然な成長や季節要因によるノイズを把握し、それを介入群の「時間的な変化」から引き算(差の差をとる)します。これにより、「もし施策を行っていなかったら、介入群も統制群と同じトレンドを辿っていたはずだ」という仮定(平行トレンドの仮定)のもと、ノイズを取り除いた「純粋な施策の効果」を浮き彫りにするのです。

DiDの基本ロジック:「平行トレンドの仮定」と「差の差」の数理

DiDがやっていることを一言で言えば、「統制群(施策をしていないグループ)の成長幅を、介入群(施策をしたグループ)の成長幅から引き算する」ことです。

具体的な効果測定のメカニズムと、それを支える数理モデルの構造を紐解いていきましょう。

施策効果を導く「差の差」のシンプルな引き算

DiDにおいて、施策による因果効果(Treatment Effect) τ\tau は、以下のような非常にシンプルな四則演算(差の差)で定義されます。

τ=(YT,PostYT,Pre)(YC,PostYC,Pre)\tau = (Y_{T, Post} - Y_{T, Pre}) - (Y_{C, Post} - Y_{C, Pre})

  • YT,PostY_{T, Post} :介入群(Treated)の、施策Post)の平均値
  • YT,PreY_{T, Pre} :介入群(Treated)の、施策Pre)の平均値
  • YC,PostY_{C, Post} :統制群(Control)の、施策Post)の平均値
  • YC,PreY_{C, Pre} :統制群(Control)の、施策Pre)の平均値

前半の括弧 (YT,PostYT,Pre)(Y_{T, Post} - Y_{T, Pre}) は、介入群の「施策前後の変化量」を表します。しかし、この変化量には「市場全体の自然な成長」や「季節要因」が含まれています。 そこで、後半の括弧 (YC,PostYC,Pre)(Y_{C, Post} - Y_{C, Pre}) で計算した統制群の「施策前後の変化量(自然な成長分)」を差し引くことで、純粋な施策効果 τ\tau を導き出すのです。

DiDの最重要ルール:「平行トレンドの仮定」

この美しい引き算が成立するためには、一つの強力な前提条件を満たしている必要があります。それが「平行トレンドの仮定(Parallel Trend Assumption)」です。

“もし施策が行われていなかったとしたら、介入群と統制群は、時間の経過とともに「同じ傾き(平行)」で変化していたはずだ”

という仮定です。 言い換えれば、両グループの間で「元々の売上の規模感(ベースライン)」に差があっても構いませんが、「成長のペース(トレンド)」は施策前において同じでなければならず、施策がなかった場合、施策後もその平行な関係が維持される、と考えます。DiDにおける「反事実(施策がなかった場合の介入群の姿)」は、この仮定によって描かれることになります。

回帰モデルによるDiDの表現

実務では、単なる引き算ではなく、「回帰分析(最小二乗法など)」を使ってDiDを表現することが一般的です。回帰モデルを使うことで、他の要因(共変量)を統制したり、効果の「統計的な有意差(p値)」を簡単に計算できたりするメリットがあるためです。

基本となるDiDの回帰方程式は以下のようになります。

Yi=β0+β1Treati+β2Posti+β3(Treati×Posti)+ϵiY_i = \beta_0 + \beta_1 Treat_i + \beta_2 Post_i + \beta_3 (Treat_i \times Post_i) + \epsilon_i
  • TreatiTreat_i :介入群なら1、統制群なら0となるダミー変数(グループ間の元々の実力差 β1\beta_1 を吸収する)
  • PostiPost_i :施策後なら1、施策前なら0となるダミー変数(市場全体の時間的なトレンド β2\beta_2 を吸収する)
  • (Treati×Posti)(Treat_i \times Post_i) :介入群かつ施策後である場合のみ1となる交差項

この回帰式が、先ほど定義した4つの状態(YC,PreY_{C, Pre}YC,PostY_{C, Post}YT,PreY_{T, Pre}YT,PostY_{T, Post})とどのように対応しているのか、ダミー変数にそれぞれ 01 を当てはめて確認してみましょう。 (※ここでは誤差項 ϵi\epsilon_i の期待値を0として、各グループの平均値を導き出します)

① 統制群・施策前(YC,PreY_{C, Pre}
Treat=0,Post=0Treat = 0, Post = 0 の場合

YC,Pre=β0+β1(0)+β2(0)+β3(0×0)=β0Y_{C, Pre} = \beta_0 + \beta_1(0) + \beta_2(0) + \beta_3(0 \times 0) = \beta_0

② 統制群・施策後(YC,PostY_{C, Post}
Treat=0,Post=1Treat = 0, Post = 1 の場合

YC,Post=β0+β1(0)+β2(1)+β3(0×1)=β0+β2Y_{C, Post} = \beta_0 + \beta_1(0) + \beta_2(1) + \beta_3(0 \times 1) = \beta_0 + \beta_2

③ 介入群・施策前(YT,PreY_{T, Pre}
Treat=1,Post=0Treat = 1, Post = 0 の場合

YT,Pre=β0+β1(1)+β2(0)+β3(1×0)=β0+β1Y_{T, Pre} = \beta_0 + \beta_1(1) + \beta_2(0) + \beta_3(1 \times 0) = \beta_0 + \beta_1

④ 介入群・施策後(YT,PostY_{T, Post}
Treat=1,Post=1Treat = 1, Post = 1 の場合

YT,Post=β0+β1(1)+β2(1)+β3(1×1)=β0+β1+β2+β3Y_{T, Post} = \beta_0 + \beta_1(1) + \beta_2(1) + \beta_3(1 \times 1) = \beta_0 + \beta_1 + \beta_2 + \beta_3

これら4つの式を使って、先ほどの「差の差(DiD)」の計算式をもう一度組み立ててみます。

DiD=(YT,PostYT,Pre)(YC,PostYC,Pre)={(β0+β1+β2+β3)(β0+β1)}{(β0+β2)β0}=(β2+β3)(β2)=β3\begin{aligned} DiD &= (Y_{T, Post} - Y_{T, Pre}) - (Y_{C, Post} - Y_{C, Pre}) \\ &= \{ (\beta_0 + \beta_1 + \beta_2 + \beta_3) - (\beta_0 + \beta_1) \} - \{ (\beta_0 + \beta_2) - \beta_0 \} \\ &= (\beta_2 + \beta_3) - (\beta_2) \\ &= \beta_3 \end{aligned}

このように、ベースライン(β0\beta_0)、グループ間の元々の実力差(β1\beta_1)、時間経過によるトレンド変化(β2\beta_2)が綺麗に相殺され、最後に交差項の係数である β3\beta_3 だけが残ります。 この数理的な構造があるからこそ、回帰モデルにおいて「β3\beta_3(交差項の係数)こそが、私たちが知りたい純粋な因果効果 τ\tau となる」のです。

検証用ダミーデータの生成コード

今回作成するデータは、先ほど解説した「平行トレンドの仮定」を満たす時系列データです。全期間を100日間とし、70日目を施策の実行日として定義するシナリオを想定します。

データの生成プロセスは以下の数式に基づいています。時点を ttt=0,1,,99t = 0, 1, \dots, 99)とします。

① 共通のトレンド(自然成長 + 季節要因)
市場全体が影響を受けるベースのトレンド TrendtTrend_t は、時間経過による右肩上がりの成長と、30日周期の波(Sinカーブ)を足し合わせたものです。

Trendt=0.5t+5sin(2πt30)Trend_t = 0.5t + 5 \sin\left(\frac{2\pi t}{30}\right)

② 統制群(Control)のデータ生成式
統制群 yC,ty_{C, t} は、共通トレンドにベースライン値 1010 と、標準偏差 22 の観測ノイズ ϵC,t\epsilon_{C, t} を加えたものです。施策の有無にかかわらず、全期間この数式で推移します。

yC,t=10+Trendt+ϵC,t,ϵC,tN(0,22)y_{C, t} = 10 + Trend_t + \epsilon_{C, t}, \quad \epsilon_{C, t} \sim \mathcal{N}(0, 2^2)

③ 介入群(Treated)のデータ生成式
介入群 yT,ty_{T, t} は、統制群と同じ共通トレンドを持ちつつも、元々のベースライン値が 3030 と高い設定です。これが「グループ間の実力差」となります。さらに、施策が行われる70日目(t70t \ge 70)以降は一律で +15+15 の施策効果(True Effect)が加わります。

施策前(t<70t < 70):

yT,t=30+Trendt+ϵT,t,ϵT,tN(0,22)y_{T, t} = 30 + Trend_t + \epsilon_{T, t}, \quad \epsilon_{T, t} \sim \mathcal{N}(0, 2^2)

施策後(t70t \ge 70):

yT,t=30+Trendt+True Effect+ϵT,t=30+Trendt+15+ϵT,t\begin{aligned} y_{T, t} &= 30 + Trend_t + \text{True Effect} + \epsilon_{T, t} \\ &= 30 + Trend_t + 15 + \epsilon_{T, t} \end{aligned}

以下のPythonコードで、このデータ生成プロセスをシミュレーションします。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib

# 1. 乱数シードの固定
np.random.seed(123)

# 2. 期間パラメータの設定
n_periods = 100 # 全期間(100日間)
intervention_point = 70 # 施策実行日(70日目)
time = np.arange(n_periods)

# 3. 共通のトレンド(自然成長 + 季節要因)の作成
# 両グループが同じ影響を受ける市場全体のトレンド (Trend_t)
common_trend = 0.5 * time + 5 * np.sin(2 * np.pi * time / 30)

# 4. 統制群(Control: 施策なし)のデータ生成
# 共通トレンドに、ベースライン(10)と観測ノイズを加える
y_control = 10 + common_trend + np.random.normal(loc=0, scale=2, size=n_periods)

# 5. 介入群(Treated: 70日目から施策あり)のベースデータ生成
# 共通トレンドに、ベースライン(30)と観測ノイズを加える
# (この時点ではControlと平行なトレンドを描く反事実のデータ)
y_treated_base = 30 + common_trend + np.random.normal(loc=0, scale=2, size=n_periods)
y_treated = y_treated_base.copy()

# 6. 施策効果(介入効果)の注入
# 70日目以降の介入群データにのみ、一律で「+15」の効果を加える
true_effect = 15
y_treated[intervention_point:] += true_effect

# DataFrameにまとめる
df = pd.DataFrame({
'Time': time,
'Control': y_control,
'Treated': y_treated,
'Treated_Counterfactual': y_treated_base # 反事実(正解データ)
})

# 7. 生成したデータの可視化
plt.figure(figsize=(10, 6))

# 実際の観測値のプロット
plt.plot(df['Time'], df['Treated'], label='Treated (介入群: 施策あり)', color='darkorange', linewidth=2)
plt.plot(df['Time'], df['Control'], label='Control (統制群: 施策なし)', color='royalblue', linewidth=2)

# 介入群の反事実(もし施策がなかったら)のプロット
plt.plot(df['Time'][intervention_point:], df['Treated_Counterfactual'][intervention_point:],
label='Counterfactual (介入群の反事実)', color='darkorange', linestyle='--', alpha=0.7)

# 施策実行日の縦線
plt.axvline(x=intervention_point, color='red', linestyle=':', linewidth=2, label='Intervention (施策実行日)')

plt.title('Generated Dummy Data for DiD (差の差法)')
plt.xlabel('Time (Days)')
plt.ylabel('Value (Target KPI)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

ここでは、DiDの検証に用いるダミーデータの生成と可視化を行っています。 生成データ

まず、common_trend として、時間経過に比例して上昇する要素(0.5 * time)と、30日周期で波打つ季節要因(5 * np.sin(...))を組み合わせた市場全体のトレンドを作成します。 次に、統制群(Control)はこの共通トレンドにベースライン値 10 とノイズを足して生成します。一方、介入群(Treated)の本来の推移である y_treated_base は、同じ共通トレンドにベースライン値 30 とノイズを足して生成しています。これにより、施策前の期間において両グループの波形が完全に「平行(Parallel)」になる状態を作り出しています。

そして、施策効果を反映するため、施策日である70日目以降の介入群のデータに対してのみ、y_treated[intervention_point:] += true_effect で一律に +15 の底上げ(True Effect)を注入しています。

上記のコードを実行すると以下の結果が得られます。グラフでは、70日目以降の介入群が、点線で示された「反事実(本来の推移)」から上に +15 分だけ乖離している様子が視覚的に確認できます。

statsmodelsを用いたDiDの実装と結果の解釈

データが準備できたところで、Pythonの統計モデリングライブラリである statsmodels を用いて、DiDの回帰分析を実行してみましょう。

回帰分析を行うためには、先ほど作成した「横持ち(Wide Format)」のデータを、1行に1つの観測値が入る「縦持ち(Long Format)」に変換し、ダミー変数(Treat, Post)を追加する必要があります。

回帰分析の実行コード

以下のコードでは、データの整形から回帰分析の実行、そして結果サマリーの出力までを行います。

import statsmodels.formula.api as smf

# 1. データを縦持ち(Long Format)に変換
# ['Time', 'Control', 'Treated'] のデータフレームを、1行1観測になるように整形
df_melted = pd.melt(df, id_vars=['Time'], value_vars=['Control', 'Treated'],
var_name='Group', value_name='Y')

# 2. ダミー変数の作成
# Treat: 介入群(Treated)なら1、統制群(Control)なら0
df_melted['Treat'] = (df_melted['Group'] == 'Treated').astype(int)

# Post: 施策実行日(70日目)以降なら1、それより前なら0
df_melted['Post'] = (df_melted['Time'] >= intervention_point).astype(int)

# 3. 回帰分析(最小二乗法: OLS)の実行
# formula(数式)で 'Y ~ Treat + Post + Treat:Post' と指定します。
# `:` は交差項(掛け算)を意味し、これがDiDにおける施策効果になります。
model = smf.ols(formula='Y ~ Treat + Post + Treat:Post', data=df_melted)
results = model.fit()

# 4. 結果のサマリーを表示
print(results.summary())

ここでは、pd.melt を使用してデータを縦持ち(Long Format)に変換し、回帰分析に適した構造にしています。
その後、介入群であれば 1 となるダミー変数 Treat と、施策日以降であれば 1 となるダミー変数 Post を作成します。
モデルの定義には statsmodels.formula.api を用いることで、R言語のように直感的な数式(Y ~ Treat + Post + Treat:Post)でモデルを記述できます。 ここで指定した Treat:Post が、先ほど数式で解説した交差項 (Treati×Posti)(Treat_i \times Post_i) に該当します。最後に model.fit() で最小二乗法(OLS)によるパラメータ推定を行います。

実行結果の解釈上記のコードを実行すると、以下のような回帰分析のサマリーテーブルが出力されます(※数値は乱数シードにより多少前後する場合があります)。

OLS Regression Results
==============================================================================
Dep. Variable: Y R-squared: 0.798
Model: OLS Adj. R-squared: 0.795
Method: Least Squares F-statistic: 258.5
Date: Sun, 12 Jul 2026 Prob (F-statistic): 7.39e-68
Time: 01:35:56 Log-Likelihood: -737.53
No. Observations: 200 AIC: 1483.
Df Residuals: 196 BIC: 1496.
Df Model: 3
Covariance Type: nonrobust
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
Intercept 27.8897 1.167 23.895 0.000 25.588 30.192
Treat 19.9613 1.651 12.093 0.000 16.706 23.217
Post 24.1654 2.131 11.340 0.000 19.963 28.368
Treat:Post 14.8182 3.014 4.917 0.000 8.875 20.761
==============================================================================
Omnibus: 10.631 Durbin-Watson: 0.206
Prob(Omnibus): 0.005 Jarque-Bera (JB): 7.074
Skew: 0.322 Prob(JB): 0.0291
Kurtosis: 2.341 Cond. No. 6.35
==============================================================================

私たちが一番注目すべきは、下部のパラメータ一覧にある Treat:Post の行です。

  • coef(偏回帰係数): 14.8182 となっており、データ生成時に仕込んだ「True Effect( +15+15 )」をほぼ完璧に逆算(推定)できていることが分かります。
  • P>|t|(p値): 0.000 となっており、統計的有意水準である0.05を大きく下回っています。つまり、「この +15+15 という差は、偶然のばらつきによって生まれたものではない(統計的に有意である)」と結論づけることができます。
  • ちなみに Treat の coef(19.9613)は、データ生成時に設定した「統制群(ベース10)と介入群(ベース30)の元々の実力差( +20+20 )」を正確に捉えています。

このように、回帰分析を用いることで、単なる引き算では分からない「推定の確からしさ(p値や信頼区間)」まで同時に算出できるのがDiDの大きな強みです。

CausalImpactとの使い分け・比較

ここまで解説した「DiD(差の差法)」とは別に、時系列データを用いた因果推論の強力な手法として「CausalImpact」があります。

CausalImpactは2015年にGoogleの研究者らによって提案された手法であり、DiDのアプローチを「ベイズ構造時系列モデル(BSTS)」へと拡張したものです。DiDのように特定の「統制群」を用意するのではなく、施策の影響を受けていない他のデータ(共変量:検索トレンドや競合の動きなど)をモデルに組み込みます。 これにより、「もし施策を行っていなかったら今頃どうなっていたか」というパラレルワールドのタイムライン(反事実)を高精度に予測し、実際の観測値とのギャップから効果を測定します。

どちらも「反事実」を推定して因果効果を測るアプローチですが、実務ではどのように使い分ければ良いのでしょうか?

両者の特徴を比較テーブルにまとめました。

比較項目DiD (差の差法)CausalImpact (BSTS)
ベースとなるアプローチ頻度論(主に回帰分析)ベイズ統計(状態空間モデル)
必要なデータ介入群と 「平行トレンドを満たす」統制群ターゲット変数と 「施策の影響を受けない」共変量
トレンド・季節性の扱いダミー変数による固定的な平均のシフトとして捉える(時間変数を入れない限り柔軟性はない)ランダムウォークや季節成分として、時間経過に伴う動的な変化を柔軟にモデリングする
不確実性の表現p値、標準誤差による信頼区間信用区間(事後分布)による直感的な予測幅の提示
モデルの解釈性極めてシンプルで、ステークホルダーへの説明が容易内部構造が複雑(ブラックボックス化しやすい)

どちらを選ぶべきか?

💡 DiDをおすすめするケース

  • 完璧な「比較対象」が用意できる場合: 例えば「A県ではテレビCMを流し、属性がそっくりなB県では流さない」といった、明確なコントロールグループが存在する場合は、シンプルで強力なDiDが適しています。
  • 施策前後の「平均的なリフト」を手っ取り早く知りたい場合: モデルが軽量で直感的なため、経営陣などへロジックを説明する際のコストが低く済みます。

💡 CausalImpactをおすすめするケース

  • 「平行トレンド」を満たす統制群が見つからない場合: 現実のビジネスでは、全く同じトレンドを描く別グループを見つけるのは困難です。CausalImpactは「複数地域のデータ」や「検索トレンド」「競合の動き」などを共変量として複数組み合わせ、疑似的に精度の高いコントロールを作ることができます。
  • 季節要因やトレンドの変化が激しい場合: DiDでは長期間のトレンド変化を取りこぼすリスクがありますが、CausalImpactは「状態空間モデル」によって日々のトレンド変動を柔軟に学習・予測できます。

まとめ

本記事では、因果効果測定の代表的な手法である DiD(差の差法) について、その基本ロジックからPythonによる実装、そしてCausalImpactとの比較までを解説しました。

  • DiDの基本: 介入群の変化量から、統制群の変化量(自然なトレンド)を差し引くことで、純粋な施策効果を炙り出すシンプルなアプローチです。
  • 平行トレンドの仮定: この手法が成立するためには、「もし施策がなければ、両グループは同じ傾きで成長していたはずだ」という強力な前提が不可欠です。
  • 回帰モデルによる実装: ダミー変数を用いた回帰分析により、交差項(Treat:Post)から効果量を推定し、p値による統計的な有意性も同時に評価できます。
  • CausalImpactとの使い分け: 明確な統制群が存在し、ロジックの透明性が求められる場合はDiDが、平行トレンドを満たす統制群を見つけるのが難しい場合や、複雑な時系列変動をモデル化したい場合はCausalImpactが適しています。

DiDは、シンプルながらも「反事実をどのように推定するか」という因果推論のコアとなる考え方を学べる素晴らしいフレームワークです。データの性質や満たすべき仮定を正しく理解し、状況に応じて適切な手法を選択することが重要です。

本記事の文章・構成の一部に生成AIを使用しています。