MMM結果が「おかしい」と感じたときの実践デバッグ手順
MMMモデルを構築したとき、「この結果、本当に正しいのか?」と感じることは珍しくありません。むしろ、最初のモデリングで完璧な結果が出ることの方が稀です。本記事では、よくある「おかしい結果」パターンごとに、原因の特定方法と具体的な修正手順を解説します。
よくある「おかしい結果」6パターン
パターン1:テレビCMの係数がマイナス
症状: テレビCMに投資するほど売上が下がるという結果が出る。
想定される原因:
| 原因 | 確率 | 確認方法 |
|---|---|---|
| 多重共線性 | 高 | VIF(分散拡大係数)を計算 |
| 内生性 | 中 | 売上が落ちた時期にCM増加していないか確認 |
| Adstock設定の不備 | 中 | 減衰パラメータを確認 |
| スケーリングの問題 | 低 | 変数のスケールを確認 |
デバッグ手順:
import pandas as pd
from statsmodels.stats.outliers_influence import variance_inflation_factor
# VIFの計算
def calculate_vif(X_df):
vif_data = pd.DataFrame()
vif_data["変数"] = X_df.columns
vif_data["VIF"] = [
variance_inflation_factor(X_df.values, i)
for i in range(X_df.shape[1])
]
return vif_data.sort_values("VIF", ascending=False)
vif_result = calculate_vif(X_df)
print(vif_result)
# VIF > 10 の変数は多重共線性の疑いあり
修正方法:
- VIFが高い変数ペアを特定し、一方を削除またはグループ化
- Ridge回帰(L2正則化)を適用して共線性の影響を緩和
- ベイズモデルで広告係数に非負制約(HalfNormal事前分布)を設定
パターン2:ROASが異常に高い(1000%以上)
症状: あるチャネルのROASが10倍以上と計算される。現実的にありえない数値。
想定される原因:
| 原因 | 確率 | 確認方法 |
|---|---|---|
| 変数のスケール不整合 | 高 | 広告費の単位を確認(円 vs 千円 vs 百万円) |
| 変数定義のミス | 高 | GRP vs 金額、impression vs click |
| 低投資チャネルの外挿 | 中 | 投資額のレンジを確認 |
| 飽和曲線の未適用 | 中 | 線形 vs 非線形モデルの比較 |
デバッグ手順:
# 各チャネルのスケール確認
for col in media_columns:
print(f"{col}:")
print(f" 平均: {df[col].mean():,.0f}")
print(f" 最小: {df[col].min():,.0f}")
print(f" 最大: {df[col].max():,.0f}")
print(f" 単位: {'要確認'}")
print()
# ROASの計算と妥当性チェック
def calculate_roas(model, X, media_spend):
"""各チャネルのROASを計算し妥当性を検証"""
results = []
for i, channel in enumerate(media_columns):
contribution = model.coef_[i] * X[channel].sum()
spend = media_spend[channel].sum()
roas = contribution / spend if spend > 0 else 0
results.append({
"チャネル": channel,
"貢献売上": f"{contribution:,.0f}",
"投資額": f"{spend:,.0f}",
"ROAS": f"{roas:.1%}",
"判定": "⚠ 要確認" if roas > 5.0 else "OK"
})
return pd.DataFrame(results)
修正方法:
- 全変数を同一の金額単位(例:百万円)に統一
- 飽和曲線(Hill関数やログ変換)を適用し、収穫逓減を反映
- 投資額が極端に小さいチャネルは、結果の解釈に注意書きを添える
パターン3:ベースラインが90%以上
症状: モデルによると売上の90%以上がベースライン(広告なしでも発生する売上)で、マーケティング活動の寄与がほとんどないと出る。
想定される原因:
| 原因 | 確率 | 確認方法 |
|---|---|---|
| 切片が大きすぎる | 高 | 切片の値を売上平均と比較 |
| コントロール変数が多すぎる | 高 | コントロール変数の寄与を確認 |
| 目的変数のスケール問題 | 中 | 売上の単位と変動幅を確認 |
| トレンド成分の過剰吸収 | 中 | トレンド変数を除外して比較 |
デバッグ手順:
# ベースラインと各成分の分解
def decompose_contributions(model, X, feature_names):
intercept_contribution = model.intercept_
total_prediction = model.predict(X).sum()
print(f"総予測売上: {total_prediction:,.0f}")
print(f"切片(ベースライン): {intercept_contribution * len(X):,.0f}")
print(f"ベースライン比率: {intercept_contribution * len(X) / total_prediction:.1%}")
print("\n各変数の貢献:")
for i, name in enumerate(feature_names):
contrib = (model.coef_[i] * X[:, i]).sum()
pct = contrib / total_prediction
print(f" {name}: {contrib:,.0f} ({pct:.1%})")
修正方法:
- トレンド変数(線形トレンド、月次ダミー)を減らしてメディア効果の吸収を改善
- 切片を固定せず、時変ベースラインモデルを検討
- コントロール変数を段階的に追加し、メディア係数への影響を確認
パターン4:特定チャネルの信頼区間が極端に広い
症状: ベイズMMMで、あるチャネルの係数の95%信頼区間が「-0.5 〜 3.2」のように非常に広く、効果があるのかないのか判断できない。
想定される原因:
- データ不足(そのチャネルの投資がまばら)
- 他チャネルとの共線性(同時期に投資が集中)
- Adstockパラメータの識別不足
デバッグ手順:
import arviz as az
# 事後分布の確認
az.plot_posterior(trace, var_names=["beta_media"])
# R-hatとESS(有効サンプルサイズ)の確認
summary = az.summary(trace, var_names=["beta_media"])
print(summary[["mean", "sd", "hdi_3%", "hdi_97%", "r_hat", "ess_bulk"]])
# r_hat > 1.05 または ess_bulk < 400 なら収束不良
修正方法:
- より情報的な事前分布を設定(業界平均のROASを参考に)
- 投資パターンが類似するチャネルをグループ化
- データ期間を延長するか、地域別データで情報量を増やす
パターン5:季節パターンが逆転
症状: 「夏に売上が上がるはずの商品なのに、モデルでは夏の季節係数がマイナス」のように、既知の季節パターンと矛盾する。
想定される原因:
- 季節性とメディア投資の交絡(夏にメディア投資も増加している場合、季節効果がメディアに吸収される)
- 祝日変数や特殊イベント変数の設定ミス
- フーリエ級数の次数が不適切
デバッグ手順:
import matplotlib.pyplot as plt
# 季節パターンの可視化
df['month'] = df['date'].dt.month
monthly_avg = df.groupby('month')['sales'].mean()
plt.figure(figsize=(10, 5))
plt.bar(monthly_avg.index, monthly_avg.values)
plt.xlabel('月')
plt.ylabel('平均売上')
plt.title('月別平均売上(実データ)')
plt.show()
# モデルの季節係数と実データのパターンを比較
修正方法:
- メディア変数を一時的に除外して季節パターンのみのモデルを構築し、正しいパターンが出るか確認
- フーリエ級数の次数を調整(通常は2〜3次で十分)
- 季節変数とメディア変数を段階的に投入して交絡の影響を確認
パターン6:予測が実績と大きく乖離
症状: モデルのフィットは良いが、直近数週間の予測が実績と大幅に乖離する。
想定される原因:
- 過学習(訓練期間ではフィットするが汎化しない)
- 構造変化(COVID-19、大規模リニューアルなど)
- 外れ値の影響
# 残差分析
import numpy as np
residuals = y - model.predict(X)
# 残差の時系列プロット
plt.figure(figsize=(12, 4))
plt.plot(dates, residuals)
plt.axhline(y=0, color='r', linestyle='--')
plt.title('残差の時系列推移')
plt.ylabel('残差')
plt.show()
# 外れ値検出(±2σ以上)
outlier_threshold = 2 * np.std(residuals)
outliers = np.abs(residuals) > outlier_threshold
print(f"外れ値候補: {np.sum(outliers)}件")
for date, res in zip(dates[outliers], residuals[outliers]):
print(f" {date}: 残差 = {res:,.0f}")
デバッグフローチャート
以下の順序で体系的にデバッグを進めることをお勧めします。
結果が「おかしい」
│
├─ Step 1: データの基本チェック
│ ├─ 変数の単位は正しいか?
│ ├─ 欠損値はないか?
│ └─ 外れ値は処理済みか?
│
├─ Step 2: 多重共線性チェック
│ ├─ VIF > 10 の変数はあるか?
│ └─ 相関行列で r > 0.8 のペアはあるか?
│
├─ Step 3: モデル構造チェック
│ ├─ Adstockパラメータは妥当か?
│ ├─ 飽和曲線は適用されているか?
│ └─ 季節性は適切にモデル化されているか?
│
├─ Step 4: 過学習チェック
│ ├─ 訓練/テスト誤差の乖離は?
│ └─ 交差検証スコアは安定しているか?
│
└─ Step 5: 感度分析
├─ 変数を1つずつ追加/除外して係数の安定性を確認
└─ データ期間を変えて結果の頑健性を確認