メインコンテンツへスキップ
実践ガイドBasic

MMM結果が「おかしい」と感じたときの実践デバッグ手順

テレビCMの係数がマイナス、ROASが1000%超え、ベースラインが90%以上…MMMでよくある「おかしい結果」のパターン別に、原因の特定と修正手順を体系的に解説します。

MMM Lab 編集部2026/3/18分で読める13

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つずつ追加/除外して係数の安定性を確認
       └─ データ期間を変えて結果の頑健性を確認

続きはBasicプランで読めます

この先の内容(詳細な分析結果・具体的な数値・施策の全容)はBasicプラン以上のメンバー限定です。

関連記事