Uncategorized

因果推論学習:Phase3






Phase 3: 時系列分析の習得 - 詳細カリキュラム


Phase 3: 時系列分析の習得
期間: 3週間 | 目標: 時系列データで因果関係を推定できるようになる

Phase 3で達成すること

  • 時系列データの特性(定常性、自己相関)を理解する
  • ARIMAモデルで問い合わせ数を予測できる
  • VARモデルで複数変数間の動的関係を分析できる
  • グレンジャー因果性検定で「原因→結果」の時間的順序を確認できる
  • 状態空間モデルでトレンド・季節性を分解できる
  • 不動産マーケティングデータで実践的な時系列分析ができる

使用教材

主教材

  • 理論書: 金本『因果推論』第7章「因果推論のための時系列解析」
    • 時系列因果推論の理論的基礎
    • グレンジャー因果性の詳細
    • Week 1-2で並行して読む
  • 実装教材: 『Pythonライブラリによる因果推論・因果探索』該当章
    • statsmodelsでの実装例
    • lingamパッケージの紹介(Week 3で軽く触れる)
  • 公式ドキュメント: statsmodels公式(無料)
    • https://www.statsmodels.org/stable/tsa.html
    • ARIMA、VAR、状態空間モデルの詳細
Phase 3開始前の確認事項

  • Phase 2でベイズ統計の基礎を習得済みであること
  • Python環境(pandas、numpy、matplotlib)が動作すること
  • statsmodelsをインストール済み(または今すぐインストール)
  • 時系列データの概念(時間の順序が重要)を理解していること
時系列分析が不動産マーケティングで重要な理由

  • 問い合わせ予測: 過去のトレンドから将来の需要を予測
  • 広告効果の遅れ: 広告を出してから効果が出るまでのタイムラグを考慮
  • 季節性: 春(引っ越しシーズン)と冬での需要差
  • 因果の時間的順序: 「広告費増加 → 問い合わせ増加」の時間関係を検証
  • 複数変数の相互作用: リスティング広告とSNS広告の相互効果

Week 1: ARIMAモデルと時系列の基礎

Week 1 の学習目標
  • 時系列データの特性(トレンド、季節性、定常性)を理解する
  • ACF(自己相関関数)とPACF(偏自己相関関数)を解釈できる
  • 単位根検定で定常性を確認できる
  • ARIMAモデルを実装し、問い合わせ数を予測できる
  • 季節性ARIMA(SARIMA)を扱える
Day 1-2: 時系列データの基礎と定常性

月曜日(3時間)

  • 午前(1.5時間): 金本『因果推論』第7章 7.1-7.2節
    • 時系列データとは何か
    • 定常性の概念(弱定常性 vs 強定常性)
    • 非定常データの問題点
  • 午後(1.5時間): statsmodels環境構築と基本操作
    • statsmodelsインストール
    • 時系列データの可視化
    • トレンド・季節性の確認

statsmodels環境構築と最初の可視化

# statsmodelsのインストール
# Google Colabの場合
!pip install statsmodels

# ローカル環境の場合
# pip install statsmodels

# 必要なライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.stattools import adfuller, acf, pacf
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.arima.model import ARIMA
import warnings
warnings.filterwarnings('ignore')

# 日本語フォント設定(matplotlib)
plt.rcParams['font.sans-serif'] = ['Hiragino Sans', 'Yu Gothic', 'Meiryo', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

print("✅ statsmodels環境構築完了!")

# サンプルデータ: 週次問い合わせ数(52週 × 2年 = 104週)
np.random.seed(42)
weeks = pd.date_range(start='2023-01-01', periods=104, freq='W')

# トレンド成分(徐々に増加)
trend = np.linspace(100, 150, 104)

# 季節性成分(春に増加、冬に減少)
seasonal = 20 * np.sin(2 * np.pi * np.arange(104) / 52)

# ランダム成分
noise = np.random.normal(0, 5, 104)

# 問い合わせ数 = トレンド + 季節性 + ノイズ
inquiries = trend + seasonal + noise

# DataFrameに変換
df = pd.DataFrame({
'date': weeks,
'inquiries': inquiries
})
df.set_index('date', inplace=True)

# 可視化
fig, axes = plt.subplots(4, 1, figsize=(12, 10))

# 1. 元データ
axes[0].plot(df.index, df['inquiries'], color='#9c27b0', linewidth=2)
axes[0].set_title('週次問い合わせ数(元データ)', fontsize=14, fontweight='bold')
axes[0].set_ylabel('問い合わせ数')
axes[0].grid(alpha=0.3)

# 2. トレンド成分
axes[1].plot(weeks, trend, color='#2196f3', linewidth=2)
axes[1].set_title('トレンド成分', fontsize=12)
axes[1].set_ylabel('トレンド')
axes[1].grid(alpha=0.3)

# 3. 季節性成分
axes[2].plot(weeks, seasonal, color='#ff9800', linewidth=2)
axes[2].set_title('季節性成分', fontsize=12)
axes[2].set_ylabel('季節性')
axes[2].grid(alpha=0.3)

# 4. ランダム成分
axes[3].plot(weeks, noise, color='#4caf50', linewidth=2)
axes[3].set_title('ランダム成分(ノイズ)', fontsize=12)
axes[3].set_ylabel('ノイズ')
axes[3].set_xlabel('日付')
axes[3].grid(alpha=0.3)

plt.tight_layout()
plt.show()

print(f"データ期間: {df.index[0]} 〜 {df.index[-1]}")
print(f"サンプル数: {len(df)}")
print(f"平均問い合わせ数: {df['inquiries'].mean():.1f}")
print(f"標準偏差: {df['inquiries'].std():.1f}")

時系列データの3つの成分

成分 特徴 不動産の例
トレンド 長期的な増加・減少傾向 市場拡大により問い合わせが年々増加
季節性 一定周期で繰り返すパターン 春(3-4月)に引っ越し需要で急増
ノイズ 予測不可能なランダム変動 台風、コロナなどの突発的イベント

火曜日(3時間)

  • 午前(1.5時間): 定常性の検定
    • ADF検定(Augmented Dickey-Fuller test)の理論
    • 単位根の概念
    • 定常化の手法(差分、対数変換)
  • 午後(1.5時間): 実装演習

定常性検定の実装

# ADF検定(Augmented Dickey-Fuller Test)
from statsmodels.tsa.stattools import adfuller

def adf_test(series, name=''):
"""
ADF検定を実行し、結果を分かりやすく出力する関数

帰無仮説(H0): 単位根が存在する(= 非定常)
対立仮説(H1): 単位根が存在しない(= 定常)

p値 < 0.05 なら H0を棄却 → 定常 p値 >= 0.05 なら H0を棄却できない → 非定常
"""
result = adfuller(series, autolag='AIC')

print(f'=== ADF検定結果: {name} ===')
print(f'ADF統計量: {result[0]:.4f}')
print(f'p値: {result[1]:.4f}')
print(f'ラグ次数: {result[2]}')
print(f'サンプルサイズ: {result[3]}')
print('臨界値:')
for key, value in result[4].items():
print(f' {key}: {value:.3f}')

# 判定
if result[1] < 0.05: print(f'✅ 結論: 定常である(p={result[1]:.4f} < 0.05)') else: print(f'⚠️ 結論: 非定常である(p={result[1]:.4f} >= 0.05)')
print()

return result

# 元データのADF検定
adf_test(df['inquiries'], '元データ')

# 1階差分を取る(非定常 → 定常化)
df['inquiries_diff'] = df['inquiries'].diff().dropna()

# 差分データのADF検定
adf_test(df['inquiries_diff'].dropna(), '1階差分データ')

# 可視化
fig, axes = plt.subplots(2, 1, figsize=(12, 8))

# 元データ
axes[0].plot(df.index, df['inquiries'], color='#9c27b0', linewidth=2)
axes[0].set_title('元データ(非定常の可能性)', fontsize=14, fontweight='bold')
axes[0].set_ylabel('問い合わせ数')
axes[0].grid(alpha=0.3)

# 1階差分
axes[1].plot(df.index[1:], df['inquiries_diff'].dropna(), color='#2196f3', linewidth=2)
axes[1].axhline(y=0, color='red', linestyle='--', alpha=0.5)
axes[1].set_title('1階差分データ(定常化済み)', fontsize=14, fontweight='bold')
axes[1].set_ylabel('変化量')
axes[1].set_xlabel('日付')
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

つまずきポイント: ADF検定の解釈

状況 p値 結論 対処法
トレンドあり p > 0.05 非定常 1階差分を取る
差分後 p < 0.05 定常 ARIMAのdパラメータに反映(d=1)
差分後も非定常 p > 0.05 まだ非定常 2階差分(d=2)、または対数変換

Day 3-4: ACF/PACFとARIMAモデル

水曜日(3時間)

  • 午前(1.5時間): ACF/PACFの理論
    • 自己相関関数(ACF)の意味
    • 偏自己相関関数(PACF)の意味
    • ARIMAのp, d, qパラメータの決定方法
  • 午後(1.5時間): ACF/PACFの可視化と解釈

ACF/PACFプロットの作成と解釈

# ACF/PACFプロット
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 元データのACF
plot_acf(df['inquiries'], lags=30, ax=axes[0, 0], color='#9c27b0')
axes[0, 0].set_title('元データのACF', fontsize=12, fontweight='bold')
axes[0, 0].set_xlabel('ラグ')
axes[0, 0].set_ylabel('自己相関')

# 元データのPACF
plot_pacf(df['inquiries'], lags=30, ax=axes[0, 1], color='#9c27b0', method='ywm')
axes[0, 1].set_title('元データのPACF', fontsize=12, fontweight='bold')
axes[0, 1].set_xlabel('ラグ')
axes[0, 1].set_ylabel('偏自己相関')

# 差分データのACF
plot_acf(df['inquiries_diff'].dropna(), lags=30, ax=axes[1, 0], color='#2196f3')
axes[1, 0].set_title('差分データのACF', fontsize=12, fontweight='bold')
axes[1, 0].set_xlabel('ラグ')
axes[1, 0].set_ylabel('自己相関')

# 差分データのPACF
plot_pacf(df['inquiries_diff'].dropna(), lags=30, ax=axes[1, 1], color='#2196f3', method='ywm')
axes[1, 1].set_title('差分データのPACF', fontsize=12, fontweight='bold')
axes[1, 1].set_xlabel('ラグ')
axes[1, 1].set_ylabel('偏自己相関')

plt.tight_layout()
plt.show()

# ACF/PACFの数値確認
acf_values = acf(df['inquiries_diff'].dropna(), nlags=10)
pacf_values = pacf(df['inquiries_diff'].dropna(), nlags=10, method='ywm')

print("=== ACF値(ラグ0-10)===")
for i, val in enumerate(acf_values):
print(f"ラグ {i}: {val:.3f}")

print("\n=== PACF値(ラグ0-10)===")
for i, val in enumerate(pacf_values):
print(f"ラグ {i}: {val:.3f}")

ACF/PACFからARIMAパラメータを決定する方法

パターン ACF PACF 推奨モデル
AR(p) 徐々に減衰 ラグpで急減 ARIMA(p, d, 0)
MA(q) ラグqで急減 徐々に減衰 ARIMA(0, d, q)
ARMA(p, q) 徐々に減衰 徐々に減衰 ARIMA(p, d, q)

実務では: auto_arima(pmdarima)で自動選択するのが一般的

木曜日(3時間)

  • 全日(3時間): ARIMAモデルの実装
    • statsmodelsでのARIMA推定
    • AIC/BICでのモデル選択
    • 残差診断
    • 予測の実行

ARIMAモデルの実装と予測

# ARIMAモデルの実装
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.graphics.tsaplots import plot_predict
import itertools

# データの準備(訓練データとテストデータに分割)
train_size = int(len(df) * 0.8) # 80%を訓練、20%をテスト
train = df['inquiries'][:train_size]
test = df['inquiries'][train_size:]

print(f"訓練データ: {len(train)}週")
print(f"テストデータ: {len(test)}週")

# ARIMAパラメータのグリッドサーチ(簡易版)
# p, d, q の候補
p_range = range(0, 3)
d_range = range(0, 2)
q_range = range(0, 3)

best_aic = np.inf
best_params = None
best_model = None

print("\n=== ARIMAパラメータの探索 ===")
for p, d, q in itertools.product(p_range, d_range, q_range):
try:
# モデルの推定
model = ARIMA(train, order=(p, d, q))
fitted_model = model.fit()

# AICを比較
if fitted_model.aic < best_aic: best_aic = fitted_model.aic best_params = (p, d, q) best_model = fitted_model print(f"ARIMA({p},{d},{q}) - AIC: {fitted_model.aic:.2f}") except: continue print(f"\n✅ 最良モデル: ARIMA{best_params}") print(f"AIC: {best_aic:.2f}") # モデルサマリー print("\n" + "="*60) print(best_model.summary()) print("="*60) # 残差診断 fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # 1. 残差プロット residuals = best_model.resid axes[0, 0].plot(residuals, color='#9c27b0') axes[0, 0].axhline(y=0, color='red', linestyle='--') axes[0, 0].set_title('残差プロット', fontsize=12, fontweight='bold') axes[0, 0].set_ylabel('残差') axes[0, 0].grid(alpha=0.3) # 2. 残差のヒストグラム axes[0, 1].hist(residuals, bins=20, color='#9c27b0', alpha=0.7, edgecolor='black') axes[0, 1].set_title('残差の分布', fontsize=12, fontweight='bold') axes[0, 1].set_xlabel('残差') axes[0, 1].set_ylabel('頻度') axes[0, 1].grid(alpha=0.3) # 3. 残差のACF from statsmodels.graphics.tsaplots import plot_acf plot_acf(residuals, lags=20, ax=axes[1, 0], color='#9c27b0') axes[1, 0].set_title('残差のACF', fontsize=12, fontweight='bold') # 4. Q-Qプロット(正規性の確認) from scipy import stats stats.probplot(residuals, dist="norm", plot=axes[1, 1]) axes[1, 1].set_title('Q-Qプロット(正規性)', fontsize=12, fontweight='bold') axes[1, 1].grid(alpha=0.3) plt.tight_layout() plt.show() # Ljung-Box検定(残差の独立性) from statsmodels.stats.diagnostic import acorr_ljungbox lb_test = acorr_ljungbox(residuals, lags=10, return_df=True) print("\n=== Ljung-Box検定(残差の独立性)===") print(lb_test) print("\np値 > 0.05 なら残差は独立(良いモデル)")

# 予測の実行
forecast_steps = len(test)
forecast = best_model.forecast(steps=forecast_steps)

# 予測区間の計算
forecast_obj = best_model.get_forecast(steps=forecast_steps)
forecast_df = forecast_obj.summary_frame(alpha=0.05) # 95%予測区間

# 可視化
plt.figure(figsize=(14, 6))

# 訓練データ
plt.plot(train.index, train.values, label='訓練データ', color='#9c27b0', linewidth=2)

# テストデータ(実測値)
plt.plot(test.index, test.values, label='テストデータ(実測)',
color='#2196f3', linewidth=2, linestyle='--')

# 予測値
plt.plot(test.index, forecast.values, label='予測値',
color='#ff9800', linewidth=2)

# 95%予測区間
plt.fill_between(test.index,
forecast_df['mean_ci_lower'],
forecast_df['mean_ci_upper'],
color='#ff9800', alpha=0.2, label='95%予測区間')

plt.title(f'ARIMA{best_params}による問い合わせ数予測', fontsize=16, fontweight='bold')
plt.xlabel('日付')
plt.ylabel('問い合わせ数')
plt.legend(loc='best')
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

# 予測精度の評価
from sklearn.metrics import mean_absolute_error, mean_squared_error

mae = mean_absolute_error(test, forecast)
rmse = np.sqrt(mean_squared_error(test, forecast))
mape = np.mean(np.abs((test - forecast) / test)) * 100

print("\n=== 予測精度 ===")
print(f"MAE(平均絶対誤差): {mae:.2f}")
print(f"RMSE(平均二乗誤差平方根): {rmse:.2f}")
print(f"MAPE(平均絶対パーセント誤差): {mape:.2f}%")

ARIMAモデルの良し悪しを判断する4つのポイント

  1. AIC/BIC: 小さいほど良い(複数モデル比較時)
  2. 残差の独立性: ACFが有意でない、Ljung-Box検定でp>0.05
  3. 残差の正規性: ヒストグラムが正規分布、Q-Qプロットが直線
  4. 予測精度: MAPEが10%未満なら優秀、20%以下なら実用的

Day 5-7: 季節性ARIMAと総合演習

金曜日(3時間)

  • 全日(3時間): 季節性ARIMA(SARIMA)
    • 季節性パラメータ(P, D, Q, s)の理解
    • SARIMAモデルの実装
    • 不動産の季節性(春の引っ越しシーズン)をモデル化

SARIMA(季節性ARIMA)の実装

# 季節性を持つデータの生成
np.random.seed(42)
weeks_seasonal = pd.date_range(start='2021-01-01', periods=156, freq='W') # 3年分

# トレンド
trend_seasonal = np.linspace(100, 180, 156)

# 年次季節性(52週周期で春に急増)
seasonal_yearly = 30 * np.sin(2 * np.pi * np.arange(156) / 52 - np.pi/2)

# ノイズ
noise_seasonal = np.random.normal(0, 8, 156)

# 問い合わせ数
inquiries_seasonal = trend_seasonal + seasonal_yearly + noise_seasonal

df_seasonal = pd.DataFrame({
'date': weeks_seasonal,
'inquiries': inquiries_seasonal
})
df_seasonal.set_index('date', inplace=True)

# 訓練/テストデータ分割
train_size_s = int(len(df_seasonal) * 0.8)
train_s = df_seasonal['inquiries'][:train_size_s]
test_s = df_seasonal['inquiries'][train_size_s:]

# SARIMA実装
# SARIMA(p, d, q)(P, D, Q, s)
# p, d, q: 通常のARIMAパラメータ
# P, D, Q: 季節性パラメータ
# s: 季節周期(週次データで年次季節性なら52)

from statsmodels.tsa.statespace.sarimax import SARIMAX

# モデル1: 季節性なし(比較用)
model_no_season = ARIMA(train_s, order=(1, 1, 1))
fitted_no_season = model_no_season.fit()

# モデル2: 季節性あり(SARIMA)
# (1,1,1)(1,1,1,52) = ARIMA(1,1,1) + 季節性ARIMA(1,1,1,52)
model_sarima = SARIMAX(train_s, order=(1, 1, 1),
seasonal_order=(1, 1, 1, 52))
fitted_sarima = model_sarima.fit(disp=False)

print("=== モデル比較 ===")
print(f"ARIMA(1,1,1) AIC: {fitted_no_season.aic:.2f}")
print(f"SARIMA(1,1,1)(1,1,1,52) AIC: {fitted_sarima.aic:.2f}")
print(f"\n✅ AICが小さいモデル: {'SARIMA' if fitted_sarima.aic < fitted_no_season.aic else 'ARIMA'}") # 予測 forecast_no_season = fitted_no_season.forecast(steps=len(test_s)) forecast_sarima = fitted_sarima.forecast(steps=len(test_s)) # 可視化 plt.figure(figsize=(14, 6)) plt.plot(train_s.index, train_s.values, label='訓練データ', color='#9c27b0', linewidth=2) plt.plot(test_s.index, test_s.values, label='テストデータ(実測)', color='#2196f3', linewidth=2, linestyle='--') plt.plot(test_s.index, forecast_no_season.values, label='ARIMA予測(季節性なし)', color='#ff9800', linewidth=2, alpha=0.7) plt.plot(test_s.index, forecast_sarima, label='SARIMA予測(季節性あり)', color='#4caf50', linewidth=2) # 春の時期を強調表示 for year in [2021, 2022, 2023, 2024]: spring_start = pd.Timestamp(f'{year}-03-01') spring_end = pd.Timestamp(f'{year}-05-01') plt.axvspan(spring_start, spring_end, alpha=0.1, color='orange', label='春シーズン' if year == 2021 else '') plt.title('SARIMA: 季節性を考慮した予測', fontsize=16, fontweight='bold') plt.xlabel('日付') plt.ylabel('問い合わせ数') plt.legend(loc='best') plt.grid(alpha=0.3) plt.tight_layout() plt.show() # 予測精度比較 mae_no_season = mean_absolute_error(test_s, forecast_no_season) mae_sarima = mean_absolute_error(test_s, forecast_sarima) print("\n=== 予測精度比較 ===") print(f"ARIMA MAE: {mae_no_season:.2f}") print(f"SARIMA MAE: {mae_sarima:.2f}") print(f"改善率: {(mae_no_season - mae_sarima) / mae_no_season * 100:.1f}%")

SARIMAが威力を発揮する場面

  • 不動産: 春(3-4月)の引っ越しシーズン、年末年始の閑散期
  • 小売: 年末商戦、季節商品の需要変動
  • 観光: GW、夏休み、年末年始の繁忙期

注意: 季節周期sの設定が重要。週次データで年次季節性なら s=52、月次データなら s=12

土日(各3時間)

  • 土曜: 実データでの演習
    • 自社データ(または架空データ)でARIMA/SARIMA実装
    • モデル診断、パラメータ調整
    • 予測結果のレポート作成
  • 日曜: Week 1総復習
    • 定常性、ACF/PACF、ARIMAの概念整理
    • つまずいたポイントの再確認
    • Week 2の予習(VARモデル)

Week 1 完了チェックリスト

☐ 時系列データの3成分(トレンド、季節性、ノイズ)を説明できる
☐ ADF検定で定常性を判定できる
☐ ACF/PACFプロットを解釈できる
☐ ARIMAモデルを実装し、予測できる
☐ 残差診断(ACF、Ljung-Box検定)ができる
☐ SARIMAで季節性を考慮したモデルを作れる
☐ MAE/RMSEで予測精度を評価できる

Week 2: VARモデルとグレンジャー因果性

Week 2 の学習目標
  • ベクトル自己回帰(VAR)モデルの理論を理解する
  • 複数の時系列変数間の動的関係を分析できる
  • グレンジャー因果性検定で「予測的因果関係」を確認できる
  • インパルス応答関数(IRF)で広告効果の時間推移を可視化できる
  • 不動産マーケティングでの実践的応用(広告費×CV数)ができる
Day 1-2: VARモデルの基礎

月曜日(3時間)

  • 午前(1.5時間): 金本『因果推論』第7章 VAR部分
    • VARモデルとは何か
    • ARIMAとの違い(単変量 vs 多変量)
    • VARモデルの数学的定式化
  • 午後(1.5時間): 『Pythonライブラリによる因果推論』VAR該当箇所
    • statsmodelsでのVAR実装例
    • ラグ次数の選択方法
VARモデルが必要な理由

ARIMAは1つの時系列のみを扱うが、マーケティングでは複数変数が相互に影響:

  • 例1: リスティング広告費 ↔ SNS広告費 ↔ SEO流入 ↔ CV数
  • 例2: 問い合わせ数 ↔ 成約数 ↔ 顧客満足度
  • VARの利点: これらの相互作用を同時にモデル化し、「どの変数がどの変数に影響するか」を分析できる

火曜日(3時間)

  • 全日(3時間): VARモデルの実装
    • 日次広告費×CV数データの準備
    • 定常性の確認(多変量)
    • 最適ラグ次数の選択

VARモデルの実装: 広告費×CV数の分析

# 日次データの生成(365日分)
np.random.seed(42)
days = pd.date_range(start='2023-01-01', periods=365, freq='D')

# リスティング広告費(万円/日)
listing_ad = 50 + 10 * np.sin(2 * np.pi * np.arange(365) / 365) + np.random.normal(0, 5, 365)

# SNS広告費(万円/日)- リスティングより少し遅れて変動
sns_ad = 30 + 8 * np.sin(2 * np.pi * np.arange(365) / 365 - 0.5) + np.random.normal(0, 3, 365)

# CV数 - 広告費の影響を3日遅れで受ける
cv_base = 100 + 20 * np.sin(2 * np.pi * np.arange(365) / 365)
cv = np.zeros(365)
cv[:3] = cv_base[:3] + np.random.normal(0, 5, 3)

for i in range(3, 365):
# 3日前の広告費の影響
ad_effect = 0.5 * listing_ad[i-3] + 0.3 * sns_ad[i-3]
cv[i] = cv_base[i] + ad_effect + np.random.normal(0, 5)

# DataFrameに変換
df_var = pd.DataFrame({
'listing_ad': listing_ad,
'sns_ad': sns_ad,
'cv': cv
}, index=days)

# 可視化
fig, axes = plt.subplots(3, 1, figsize=(14, 10))

axes[0].plot(df_var.index, df_var['listing_ad'], color='#9c27b0', linewidth=2)
axes[0].set_title('リスティング広告費', fontsize=12, fontweight='bold')
axes[0].set_ylabel('広告費(万円/日)')
axes[0].grid(alpha=0.3)

axes[1].plot(df_var.index, df_var['sns_ad'], color='#2196f3', linewidth=2)
axes[1].set_title('SNS広告費', fontsize=12, fontweight='bold')
axes[1].set_ylabel('広告費(万円/日)')
axes[1].grid(alpha=0.3)

axes[2].plot(df_var.index, df_var['cv'], color='#4caf50', linewidth=2)
axes[2].set_title('CV数', fontsize=12, fontweight='bold')
axes[2].set_ylabel('CV数/日')
axes[2].set_xlabel('日付')
axes[2].grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 定常性の確認(各変数でADF検定)
print("=== 定常性検定(ADF) ===")
for col in df_var.columns:
result = adfuller(df_var[col])
print(f"\n{col}:")
print(f" ADF統計量: {result[0]:.4f}")
print(f" p値: {result[1]:.4f}")
print(f" 判定: {'定常' if result[1] < 0.05 else '非定常'}") # 相関分析 print("\n=== 相関行列 ===") print(df_var.corr()) # VARモデルの推定 from statsmodels.tsa.api import VAR # 訓練/テストデータ分割 train_size_var = int(len(df_var) * 0.8) train_var = df_var[:train_size_var] test_var = df_var[train_size_var:] # VARモデルの作成 model_var = VAR(train_var) # 最適ラグ次数の選択(AIC、BIC基準) lag_order = model_var.select_order(maxlags=10) print("\n=== 最適ラグ次数の選択 ===") print(lag_order.summary()) # 推奨ラグ次数(AIC基準) optimal_lag = lag_order.aic print(f"\n✅ AIC基準の最適ラグ: {optimal_lag}") # VARモデルの推定 fitted_var = model_var.fit(optimal_lag) print("\n" + "="*60) print(fitted_var.summary()) print("="*60)

つまずきポイント: VARモデルのラグ次数選択

基準 特徴 使いどころ
AIC 過学習を適度にペナルティ 予測精度重視の場合
BIC 過学習を厳しくペナルティ シンプルなモデル重視
FPE 予測誤差基準 予測目的
HQIC AICとBICの中間 バランス重視

実務では: AICとBICの両方を確認し、解釈可能性も考慮してラグを決定

Day 3-4: グレンジャー因果性検定

水曜日(3時間)

  • 午前(1.5時間): グレンジャー因果性の理論
    • 「予測的因果関係」の意味
    • 真の因果関係との違い
    • グレンジャー因果性検定の仕組み
  • 午後(1.5時間): 実装演習
グレンジャー因果性とは

「X がY をグレンジャー因果する」= 「X の過去値を使うと、Y の予測精度が向上する」

  • 重要: これは「真の因果関係」ではなく「予測的因果関係」
  • 例: 「広告費がCV数をグレンジャー因果する」= 過去の広告費を知ると、CV数の予測が改善する
  • 限界: 第3の変数(例: 景気)が両方に影響している場合、誤った因果関係を検出する可能性

グレンジャー因果性検定の実装

# グレンジャー因果性検定
from statsmodels.tsa.stattools import grangercausalitytests

# 各ペアでグレンジャー因果性を検定
# 検定の方向: X → Y (Xが Yを グレンジャー因果するか)

print("=== グレンジャー因果性検定 ===")
print("\n1. リスティング広告費 → CV数")
result_listing_cv = grangercausalitytests(df_var[['cv', 'listing_ad']],
maxlag=7, verbose=True)

print("\n" + "="*60)
print("\n2. SNS広告費 → CV数")
result_sns_cv = grangercausalitytests(df_var[['cv', 'sns_ad']],
maxlag=7, verbose=True)

print("\n" + "="*60)
print("\n3. CV数 → リスティング広告費(逆方向)")
result_cv_listing = grangercausalitytests(df_var[['listing_ad', 'cv']],
maxlag=7, verbose=True)

# 結果の整理
def summarize_granger(test_result, max_lag=7):
"""グレンジャー因果性検定結果を整理"""
summary = []
for lag in range(1, max_lag + 1):
# F検定のp値を取得
p_value = test_result[lag][0]['ssr_ftest'][1]
summary.append({
'lag': lag,
'p_value': p_value,
'significant': '✅' if p_value < 0.05 else '❌' }) return pd.DataFrame(summary) print("\n=== グレンジャー因果性検定サマリー ===") print("\nリスティング広告費 → CV数:") print(summarize_granger(result_listing_cv)) print("\nSNS広告費 → CV数:") print(summarize_granger(result_sns_cv)) print("\nCV数 → リスティング広告費:") print(summarize_granger(result_cv_listing))

グレンジャー因果性検定の解釈

p値 < 0.05 の場合:

  • 帰無仮説(「Xは Y をグレンジャー因果しない」)を棄却
  • → X の過去値が Y の予測に有用
  • → X → Y の予測的因果関係がある

ラグの解釈:

  • ラグ3で有意 → 「3日前の広告費が今日のCV数に影響」
  • 複数ラグで有意 → 効果が複数日にわたって継続

木曜日(3時間)

  • 全日(3時間): 因果関係の可視化
    • グレンジャー因果性のネットワーク図
    • ラグごとの効果の強さを可視化
    • 実務での解釈方法

グレンジャー因果性の可視化

# グレンジャー因果性のネットワーク可視化
import networkx as nx

# 各変数ペアでグレンジャー因果性を検定(ラグ3で評価)
variables = ['listing_ad', 'sns_ad', 'cv']
granger_matrix = pd.DataFrame(np.zeros((3, 3)),
index=variables,
columns=variables)

# listing_ad → cv
result = grangercausalitytests(df_var[['cv', 'listing_ad']], maxlag=3, verbose=False)
granger_matrix.loc['listing_ad', 'cv'] = result[3][0]['ssr_ftest'][1] # p値

# sns_ad → cv
result = grangercausalitytests(df_var[['cv', 'sns_ad']], maxlag=3, verbose=False)
granger_matrix.loc['sns_ad', 'cv'] = result[3][0]['ssr_ftest'][1]

# listing_ad → sns_ad
result = grangercausalitytests(df_var[['sns_ad', 'listing_ad']], maxlag=3, verbose=False)
granger_matrix.loc['listing_ad', 'sns_ad'] = result[3][0]['ssr_ftest'][1]

# sns_ad → listing_ad
result = grangercausalitytests(df_var[['listing_ad', 'sns_ad']], maxlag=3, verbose=False)
granger_matrix.loc['sns_ad', 'listing_ad'] = result[3][0]['ssr_ftest'][1]

# cv → listing_ad
result = grangercausalitytests(df_var[['listing_ad', 'cv']], maxlag=3, verbose=False)
granger_matrix.loc['cv', 'listing_ad'] = result[3][0]['ssr_ftest'][1]

# cv → sns_ad
result = grangercausalitytests(df_var[['sns_ad', 'cv']], maxlag=3, verbose=False)
granger_matrix.loc['cv', 'sns_ad'] = result[3][0]['ssr_ftest'][1]

print("=== グレンジャー因果性行列(p値, ラグ3)===")
print(granger_matrix)
print("\np < 0.05 で有意な因果関係") # ネットワーク図の作成 G = nx.DiGraph() G.add_nodes_from(variables) # 有意な因果関係のみエッジを追加(p < 0.05) for i, var1 in enumerate(variables): for j, var2 in enumerate(variables): if i != j and granger_matrix.loc[var1, var2] < 0.05: G.add_edge(var1, var2, weight=1 - granger_matrix.loc[var1, var2]) # 可視化 plt.figure(figsize=(10, 8)) pos = nx.spring_layout(G, seed=42) # ノードの色分け node_colors = {'listing_ad': '#9c27b0', 'sns_ad': '#2196f3', 'cv': '#4caf50'} colors = [node_colors[node] for node in G.nodes()] # 描画 nx.draw_networkx_nodes(G, pos, node_color=colors, node_size=3000, alpha=0.9) nx.draw_networkx_labels(G, pos, font_size=12, font_weight='bold', font_color='white') nx.draw_networkx_edges(G, pos, edge_color='gray', arrows=True, arrowsize=20, width=2, arrowstyle='->')

plt.title('グレンジャー因果性ネットワーク(ラグ3日)', fontsize=16, fontweight='bold')
plt.axis('off')
plt.tight_layout()
plt.show()

# 有意な因果関係のサマリー
print("\n=== 有意なグレンジャー因果関係(p < 0.05)===") for i, var1 in enumerate(variables): for j, var2 in enumerate(variables): if i != j and granger_matrix.loc[var1, var2] < 0.05: print(f"✅ {var1} → {var2} (p={granger_matrix.loc[var1, var2]:.4f})")

Day 5-7: インパルス応答関数とVAR予測

金曜日(3時間)

  • 全日(3時間): インパルス応答関数(IRF)
    • IRFとは何か(ショックの波及効果)
    • 広告費1単位増加のCV数への影響
    • 累積インパルス応答
インパルス応答関数(IRF)とは

「ある変数に1単位のショック(突発的な変化)を与えた時、他の変数がどう反応するか」を時間経過とともに追跡する分析手法。

  • マーケティングでの例: 「リスティング広告費を1万円増やすと、CV数が何日後にどれだけ増えるか」
  • 実務的価値: 広告効果の遅れ(ラグ)と持続期間を定量化できる

インパルス応答関数の実装

# インパルス応答関数(IRF)の計算
irf = fitted_var.irf(10) # 10期先まで追跡

# IRFのプロット
fig = irf.plot(orth=False, impulse='listing_ad', response='cv',
figsize=(10, 6))
plt.suptitle('リスティング広告費のショックに対するCV数の応答',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

fig = irf.plot(orth=False, impulse='sns_ad', response='cv',
figsize=(10, 6))
plt.suptitle('SNS広告費のショックに対するCV数の応答',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

# 累積IRF(総効果)
irf_cum = irf.cum_effects
print("=== 累積インパルス応答(10日後まで)===")
print("\nリスティング広告費 1万円増加の累積効果:")
print(f"CV数の累積増加: {irf_cum[9, 2, 0]:.2f} 件")

print("\nSNS広告費 1万円増加の累積効果:")
print(f"CV数の累積増加: {irf_cum[9, 2, 1]:.2f} 件")

# IRFを手動でプロット(カスタマイズ版)
plt.figure(figsize=(14, 6))

# リスティング広告費 → CV数
plt.subplot(1, 2, 1)
irf_listing_cv = irf.irfs[:, 2, 0] # CV(index=2), listing_ad(index=0)
plt.plot(range(len(irf_listing_cv)), irf_listing_cv,
marker='o', color='#9c27b0', linewidth=2, markersize=8)
plt.axhline(y=0, color='red', linestyle='--', alpha=0.5)
plt.title('リスティング広告費 → CV数', fontsize=12, fontweight='bold')
plt.xlabel('日数(ラグ)')
plt.ylabel('CV数の変化')
plt.grid(alpha=0.3)

# SNS広告費 → CV数
plt.subplot(1, 2, 2)
irf_sns_cv = irf.irfs[:, 2, 1] # CV(index=2), sns_ad(index=1)
plt.plot(range(len(irf_sns_cv)), irf_sns_cv,
marker='o', color='#2196f3', linewidth=2, markersize=8)
plt.axhline(y=0, color='red', linestyle='--', alpha=0.5)
plt.title('SNS広告費 → CV数', fontsize=12, fontweight='bold')
plt.xlabel('日数(ラグ)')
plt.ylabel('CV数の変化')
plt.grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 実務的解釈
print("\n=== 実務的解釈 ===")
print(f"リスティング広告費を1万円増やすと:")
print(f" - 当日のCV増加: {irf_listing_cv[0]:.2f} 件")
print(f" - 3日後のCV増加: {irf_listing_cv[3]:.2f} 件")
print(f" - 10日間の累積CV増加: {irf_cum[9, 2, 0]:.2f} 件")
print(f"\nSNS広告費を1万円増やすと:")
print(f" - 当日のCV増加: {irf_sns_cv[0]:.2f} 件")
print(f" - 3日後のCV増加: {irf_sns_cv[3]:.2f} 件")
print(f" - 10日間の累積CV増加: {irf_cum[9, 2, 1]:.2f} 件")

IRFの実務活用例

  • 広告予算配分: 累積効果が大きいチャネルに予算を振り分け
  • 効果測定期間の設定: IRFがゼロに収束する期間を効果測定期間とする
  • ROAS計算: 累積CV × 平均単価 / 広告費 = ROAS
  • 例: リスティング1万円で10日間に20件CV、平均単価5万円なら ROAS = 20×5万/1万 = 10倍

土日(各3時間)

  • 土曜: VARモデルでの予測
    • 訓練済みVARモデルで将来予測
    • 予測精度の評価
    • シナリオ分析(広告費を増やしたらどうなるか)
  • 日曜: Week 2総復習
    • VAR、グレンジャー因果性、IRFの整理
    • 実データでの総合演習
    • Week 3の予習

VARモデルでの予測

# VARモデルでの予測
forecast_steps = len(test_var)
forecast_var = fitted_var.forecast(train_var.values[-optimal_lag:], steps=forecast_steps)

# 予測結果をDataFrameに変換
forecast_df = pd.DataFrame(forecast_var,
index=test_var.index,
columns=train_var.columns)

# 可視化
fig, axes = plt.subplots(3, 1, figsize=(14, 12))

for i, col in enumerate(df_var.columns):
# 訓練データ
axes[i].plot(train_var.index, train_var[col],
label='訓練データ', color='#9c27b0', linewidth=2)

# テストデータ(実測)
axes[i].plot(test_var.index, test_var[col],
label='テストデータ(実測)', color='#2196f3',
linewidth=2, linestyle='--')

# 予測値
axes[i].plot(forecast_df.index, forecast_df[col],
label='予測値', color='#ff9800', linewidth=2)

axes[i].set_title(f'{col} の予測', fontsize=12, fontweight='bold')
axes[i].set_ylabel(col)
axes[i].legend(loc='best')
axes[i].grid(alpha=0.3)

axes[2].set_xlabel('日付')
plt.tight_layout()
plt.show()

# 予測精度の評価
print("=== VAR予測精度 ===")
for col in df_var.columns:
mae = mean_absolute_error(test_var[col], forecast_df[col])
rmse = np.sqrt(mean_squared_error(test_var[col], forecast_df[col]))
mape = np.mean(np.abs((test_var[col] - forecast_df[col]) / test_var[col])) * 100

print(f"\n{col}:")
print(f" MAE: {mae:.2f}")
print(f" RMSE: {rmse:.2f}")
print(f" MAPE: {mape:.2f}%")

# シナリオ分析: 広告費を20%増やしたら
print("\n=== シナリオ分析 ===")
print("リスティング広告費を20%増やした場合のCV予測:")

# 新しいシナリオデータ
scenario_data = train_var.copy()
scenario_data['listing_ad'] = scenario_data['listing_ad'] * 1.2

# シナリオ予測
forecast_scenario = fitted_var.forecast(scenario_data.values[-optimal_lag:],
steps=30)
forecast_scenario_df = pd.DataFrame(forecast_scenario,
columns=train_var.columns)

print(f"通常時の30日間平均CV: {forecast_var[:30, 2].mean():.1f} 件/日")
print(f"広告費20%増の30日間平均CV: {forecast_scenario_df['cv'].mean():.1f} 件/日")
print(f"CV増加率: {(forecast_scenario_df['cv'].mean() / forecast_var[:30, 2].mean() - 1) * 100:.1f}%")

Week 2 完了チェックリスト

☐ VARモデルの理論を理解している
☐ 最適ラグ次数を選択できる(AIC/BIC)
☐ グレンジャー因果性検定を実行し、解釈できる
☐ インパルス応答関数(IRF)を計算し、可視化できる
☐ VARモデルで複数変数の予測ができる
☐ 実務での応用(広告費配分、効果測定期間設定)ができる

Week 3: 状態空間モデルと総合演習

Week 3 の学習目標
  • 状態空間モデルの概念を理解する
  • statsmodelsのUnobserved Components Model(UCM)を使える
  • トレンド・季節性・周期成分を分解できる
  • 構造変化(structural break)を検出できる
  • Phase 3の総復習と実データでの総合演習
  • LiNGAMの概要を理解する(Phase 4-5での本格学習の準備)
Day 1-3: 状態空間モデル

月曜日(3時間)

  • 午前(1.5時間): 状態空間モデルの理論
    • 観測方程式と状態方程式
    • カルマンフィルタの基礎
    • ARIMAとの違い
  • 午後(1.5時間): statsmodels UCMの基本
状態空間モデルとは

時系列データを「観測できる部分」と「観測できない潜在的な状態」に分けてモデル化する手法。

  • 観測方程式: y_t = 観測値(実際のデータ)
  • 状態方程式: α_t = 潜在状態(トレンド、季節性など)
  • 利点: トレンドと季節性を明示的に分離できる
  • 不動産での例: 「長期的な市場トレンド」と「季節的な引っ越し需要」を分けて分析

火曜日(3時間)

  • 全日(3時間): UCMの実装
    • トレンド成分の抽出
    • 季節性成分の抽出
    • 周期成分の追加

Unobserved Components Model(UCM)の実装

# Unobserved Components Model(UCM)
from statsmodels.tsa.statespace.structural import UnobservedComponents

# 季節性のあるデータを使用(Week 1で作成したdf_seasonal)
data_ucm = df_seasonal['inquiries']

# モデル1: トレンド + 不規則成分
model_trend = UnobservedComponents(data_ucm, level='local linear trend')
fitted_trend = model_trend.fit(disp=False)

# モデル2: トレンド + 季節性(52週周期)
model_seasonal = UnobservedComponents(
data_ucm,
level='local linear trend',
seasonal=52 # 52週周期の季節性
)
fitted_seasonal = model_seasonal.fit(disp=False)

# モデル比較
print("=== モデル比較(AIC) ===")
print(f"トレンドのみ: {fitted_trend.aic:.2f}")
print(f"トレンド + 季節性: {fitted_seasonal.aic:.2f}")
print(f"\n✅ 最良モデル: {'季節性あり' if fitted_seasonal.aic < fitted_trend.aic else 'トレンドのみ'}") # 成分の抽出 trend = fitted_seasonal.level.smoothed seasonal = fitted_seasonal.seasonal.smoothed # 可視化 fig, axes = plt.subplots(4, 1, figsize=(14, 12)) # 元データ axes[0].plot(data_ucm.index, data_ucm.values, color='#9c27b0', linewidth=2) axes[0].set_title('元データ', fontsize=12, fontweight='bold') axes[0].set_ylabel('問い合わせ数') axes[0].grid(alpha=0.3) # トレンド成分 axes[1].plot(data_ucm.index, trend, color='#2196f3', linewidth=2) axes[1].set_title('トレンド成分(抽出)', fontsize=12, fontweight='bold') axes[1].set_ylabel('トレンド') axes[1].grid(alpha=0.3) # 季節性成分 axes[2].plot(data_ucm.index, seasonal, color='#ff9800', linewidth=2) axes[2].set_title('季節性成分(抽出)', fontsize=12, fontweight='bold') axes[2].set_ylabel('季節性') axes[2].grid(alpha=0.3) # 残差 residuals = data_ucm - trend - seasonal axes[3].plot(data_ucm.index, residuals, color='#4caf50', linewidth=2) axes[3].axhline(y=0, color='red', linestyle='--', alpha=0.5) axes[3].set_title('残差(不規則成分)', fontsize=12, fontweight='bold') axes[3].set_ylabel('残差') axes[3].set_xlabel('日付') axes[3].grid(alpha=0.3) plt.tight_layout() plt.show() # サマリー print("\n" + "="*60) print(fitted_seasonal.summary()) print("="*60) # 予測 forecast_ucm = fitted_seasonal.forecast(steps=52) # 1年先まで予測 forecast_ci = fitted_seasonal.get_forecast(steps=52).summary_frame(alpha=0.05) # 予測の可視化 plt.figure(figsize=(14, 6)) plt.plot(data_ucm.index, data_ucm.values, label='観測値', color='#9c27b0', linewidth=2) plt.plot(forecast_ucm.index, forecast_ucm.values, label='予測値', color='#ff9800', linewidth=2) plt.fill_between(forecast_ucm.index, forecast_ci['mean_ci_lower'], forecast_ci['mean_ci_upper'], color='#ff9800', alpha=0.2, label='95%予測区間') plt.title('UCMによる1年先予測', fontsize=16, fontweight='bold') plt.xlabel('日付') plt.ylabel('問い合わせ数') plt.legend(loc='best') plt.grid(alpha=0.3) plt.tight_layout() plt.show()

水曜日(3時間)

  • 全日(3時間): 構造変化の検出
    • Chow検定の理論
    • breakpoint検出
    • 不動産市場の構造変化(コロナ、金利変動など)

構造変化の検出

# 構造変化(Structural Break)の検出
from statsmodels.stats.diagnostic import breaks_cusumolsresid
from statsmodels.regression.linear_model import OLS

# 構造変化を含むデータの生成
np.random.seed(42)
n = 200
time = np.arange(n)

# 100時点目で構造変化(傾きが変わる)
y = np.zeros(n)
y[:100] = 50 + 0.5 * time[:100] + np.random.normal(0, 3, 100)
y[100:] = 50 + 1.5 * time[100:] + np.random.normal(0, 3, 100)

df_break = pd.DataFrame({
'time': time,
'y': y
})

# 可視化
plt.figure(figsize=(12, 6))
plt.plot(df_break['time'], df_break['y'], color='#9c27b0', linewidth=2)
plt.axvline(x=100, color='red', linestyle='--', linewidth=2,
label='構造変化点(真値)')
plt.title('構造変化を含む時系列データ', fontsize=14, fontweight='bold')
plt.xlabel('時点')
plt.ylabel('値')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

# Chow検定(特定の時点での構造変化を検定)
# 仮説: 100時点目で構造変化があるか

# 期間1: 0-99
# 期間2: 100-199
period1 = df_break[:100]
period2 = df_break[100:]

# 全期間でのOLS
model_full = OLS(df_break['y'], sm.add_constant(df_break['time'])).fit()

# 期間1でのOLS
model_p1 = OLS(period1['y'], sm.add_constant(period1['time'])).fit()

# 期間2でのOLS
model_p2 = OLS(period2['y'], sm.add_constant(period2['time'])).fit()

# Chow検定統計量
rss_full = model_full.ssr
rss_p1 = model_p1.ssr
rss_p2 = model_p2.ssr
rss_restricted = rss_p1 + rss_p2

k = 2 # パラメータ数(切片+傾き)
n1, n2 = len(period1), len(period2)

chow_stat = ((rss_full - rss_restricted) / k) / (rss_restricted / (n1 + n2 - 2*k))

# F分布でのp値計算
from scipy.stats import f as f_dist
p_value = 1 - f_dist.cdf(chow_stat, k, n1 + n2 - 2*k)

print("=== Chow検定結果 ===")
print(f"Chow統計量: {chow_stat:.4f}")
print(f"p値: {p_value:.6f}")
print(f"判定: {'構造変化あり ✅' if p_value < 0.05 else '構造変化なし'}") # 期間別の傾きを比較 print(f"\n=== 期間別パラメータ ===") print(f"期間1(0-99)の傾き: {model_p1.params[1]:.4f}") print(f"期間2(100-199)の傾き: {model_p2.params[1]:.4f}") # CUSUM検定(構造変化点を探索) import statsmodels.api as sm # トレンド回帰 X = sm.add_constant(df_break['time']) model_cusum = OLS(df_break['y'], X).fit() resids = model_cusum.resid # CUSUMプロット from statsmodels.stats.diagnostic import recursive_ols # 再帰的残差のプロット res_ols = recursive_ols(model_cusum) fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(df_break['time'], res_ols.cusum, color='#9c27b0', linewidth=2) ax.axhline(y=0, color='black', linestyle='-', alpha=0.3) # 5%有意水準の信頼区間 n_points = len(df_break) critical_value = 0.948 # 5%水準のCUSUM臨界値 upper = critical_value * np.sqrt(n_points) lower = -critical_value * np.sqrt(n_points) ax.plot(df_break['time'], [upper]*n_points, 'r--', label='5%信頼区間') ax.plot(df_break['time'], [lower]*n_points, 'r--') ax.axvline(x=100, color='orange', linestyle='--', linewidth=2, label='真の構造変化点') ax.set_title('CUSUM検定(構造変化の検出)', fontsize=14, fontweight='bold') ax.set_xlabel('時点') ax.set_ylabel('CUSUM統計量') ax.legend() ax.grid(alpha=0.3) plt.tight_layout() plt.show() print("\n=== CUSUM検定の解釈 ===") print("CUSUM統計量が信頼区間を超えた時点 → 構造変化の可能性")

構造変化が疑われる場合の対処法

状況 原因例 対処法
トレンドの傾きが変化 金利変動、規制変更 期間を分けてモデル化、ダミー変数追加
季節性パターンの変化 消費者行動の変化 時変係数モデル、ローリングウィンドウ推定
外れ値(スパイク) コロナ、震災など 外れ値除去、介入変数の追加

Day 4-5: LiNGAM入門とPhase 3総復習

木曜日(3時間)

  • 全日(3時間): LiNGAMの概要(Phase 4-5への準備)
    • 因果探索とは何か
    • LiNGAMの基本的なアイデア
    • 簡単な実装例(触れる程度)
LiNGAM(Linear Non-Gaussian Acyclic Model)とは

観測データから因果関係の「方向」を推定する手法。グレンジャー因果性とは異なり、瞬時的な因果関係も扱える。

  • 前提条件: 線形関係、非ガウス性、因果グラフが非巡回(DAG)
  • 利点: 介入実験なしで因果グラフを推定可能
  • 限界: 前提条件が満たされない場合、誤った因果関係を推定する
  • Phase 4-5での学習: 独立成分分析(ICA)の基礎から学び、VAR-LiNGAMで時系列因果探索を本格的に実装

LiNGAMの簡単な実装例(入門)

# LiNGAMの簡単な例(触れる程度)
# pip install lingam

import lingam
from lingam.utils import make_dot

# シンプルな因果構造のデータ生成
# 真の因果関係: X1 → X2 → X3
np.random.seed(42)
n = 1000

# X1は外生変数
X1 = np.random.laplace(0, 1, n)

# X2 = 2*X1 + ノイズ
X2 = 2 * X1 + np.random.laplace(0, 1, n)

# X3 = -1.5*X2 + ノイズ
X3 = -1.5 * X2 + np.random.laplace(0, 1, n)

# データフレーム化
data_lingam = pd.DataFrame({
'X1': X1,
'X2': X2,
'X3': X3
})

# DirectLiNGAMで因果関係を推定
model_lingam = lingam.DirectLiNGAM()
model_lingam.fit(data_lingam)

# 因果順序の推定結果
print("=== 推定された因果順序 ===")
print(f"因果順序: {model_lingam.causal_order_}")
print("(0=X1, 1=X2, 2=X3)")

# 隣接行列(因果係数)
print("\n=== 隣接行列(因果係数)===")
print(pd.DataFrame(model_lingam.adjacency_matrix_,
columns=data_lingam.columns,
index=data_lingam.columns))

print("\n解釈:")
print(" X1 → X2 の係数: {:.2f} (真値: 2.0)".format(
model_lingam.adjacency_matrix_[1, 0]))
print(" X2 → X3 の係数: {:.2f} (真値: -1.5)".format(
model_lingam.adjacency_matrix_[2, 1]))

# 因果グラフの可視化(graphviz必要)
try:
dot = make_dot(model_lingam.adjacency_matrix_, labels=data_lingam.columns)
dot.render('lingam_graph', format='png', cleanup=True)
print("\n✅ 因果グラフを 'lingam_graph.png' に保存しました")
except:
print("\n⚠️ graphvizがインストールされていないため、グラフ可視化はスキップ")
print(" グラフ可視化が必要な場合: pip install graphviz")

print("\n=== LiNGAMの注意点 ===")
print("1. 非ガウス性が前提(データがほぼ正規分布だと失敗)")
print("2. 線形関係が前提(非線形な因果関係は検出できない)")
print("3. 非巡回グラフが前提(フィードバックループがあると失敗)")
print("4. Phase 4-5で詳しく学習します")

Phase 3で学んだ時系列因果推論の整理

手法 用途 因果関係の種類
グレンジャー因果性 予測的因果関係 「過去のXが将来のYを予測」
VAR-IRF ショックの波及効果 「Xの突発的変化がYに与える影響」
LiNGAM(入門) 瞬時的因果関係 「XがYを直接生成」
VAR-LiNGAM(Phase 5) 時系列因果探索 両方を統合

金曜日(3時間)

  • 全日(3時間): Phase 3総復習
    • ARIMA、VAR、UCMの違いと使い分け
    • 実装したコードの振り返り
    • つまずいたポイントの整理
Phase 3で学んだ手法の使い分け

手法 変数数 最適な場面 不動産での例
ARIMA 単変量 1つの変数の予測 問い合わせ数の将来予測
SARIMA 単変量 季節性のある予測 春の引っ越しシーズンを考慮した予測
VAR 多変量 複数変数の相互作用分析 広告費×CV数の動的関係
UCM 単変量 成分分解、構造把握 長期トレンドと季節性の分離

Day 6-7: 総合演習とレポート作成

土日(各3-4時間)

  • 土曜: 実データでの総合プロジェクト
    • 自社データ(または架空の不動産マーケティングデータ)を用意
    • ARIMA/SARIMA、VAR、UCMを適用
    • グレンジャー因果性検定とIRF分析
    • 構造変化の検出
  • 日曜: 分析レポート作成
    • 分析目的、データ概要
    • 適用した手法と結果
    • ビジネス的示唆(予算配分、効果測定期間など)
    • 次のアクション提案

総合プロジェクト: 不動産マーケティングの時系列分析

# 総合プロジェクト例: 3チャネル広告×CV数の完全分析

# ステップ1: データ準備
print("=== ステップ1: データ準備 ===")
# (実際は自社データを読み込む)
# df = pd.read_csv('marketing_data.csv', parse_dates=['date'])

# ステップ2: 探索的データ分析
print("\n=== ステップ2: EDA ===")
# - 時系列プロット
# - 基本統計量
# - 相関分析
# - 定常性チェック

# ステップ3: 単変量時系列分析(ARIMA/SARIMA)
print("\n=== ステップ3: CV数のSARIMA予測 ===")
# - ADF検定
# - ACF/PACF分析
# - パラメータ選択
# - モデル推定と予測

# ステップ4: 多変量時系列分析(VAR)
print("\n=== ステップ4: VAR分析 ===")
# - 最適ラグ選択
# - VAR推定
# - グレンジャー因果性検定
# - IRF分析

# ステップ5: 成分分解(UCM)
print("\n=== ステップ5: トレンド・季節性分解 ===")
# - UCMでトレンド抽出
# - 季節性パターンの確認
# - 構造変化の検出

# ステップ6: ビジネス示唆
print("\n=== ステップ6: ビジネス示唆 ===")
print("1. 広告効果の遅れ: リスティングは3日後、SNSは5日後にピーク")
print("2. 効果測定期間: 広告効果は10日間持続するため、最低2週間で評価")
print("3. 予算配分: リスティングの累積効果が高いため、予算シェア60%推奨")
print("4. 季節性: 3-4月は通常の1.5倍の広告費投下が効果的")
print("5. 長期トレンド: 市場全体が年率5%成長、追い風状態")

分析レポートの構成例

  1. エグゼクティブサマリー: 結論を1ページで(経営層向け)
  2. 分析目的: なぜこの分析を行ったか
  3. データ概要: 期間、変数、サンプル数
  4. 分析手法: ARIMA、VAR、UCMの適用理由
  5. 結果: 主要な発見事項(グラフ・表で可視化)
  6. ビジネス示唆: 具体的なアクション提案
  7. 制約・限界: データや手法の限界を明記
  8. 次のステップ: Phase 4での機械学習適用など

Phase 3 完了チェックリスト

☐ ARIMA/SARIMAで単変量時系列を予測できる
☐ VARモデルで多変量時系列を分析できる
☐ グレンジャー因果性検定を実行し、解釈できる
☐ インパルス応答関数(IRF)で広告効果を定量化できる
☐ UCMでトレンド・季節性を分解できる
☐ 構造変化を検出し、対処できる
☐ LiNGAMの概要を理解している(Phase 4-5での本格学習準備)
☐ 実データで時系列分析を実施し、レポートを作成できる

Phase 3のまとめと次のステップ

Phase 3で習得したスキル

  • ✅ 時系列データの特性(定常性、自己相関、季節性)を理解
  • ✅ ARIMAモデルで問い合わせ数を精度良く予測
  • ✅ VARモデルで広告費とCV数の動的関係を分析
  • ✅ グレンジャー因果性で予測的因果関係を検証
  • ✅ IRFで広告効果の時間推移を定量化
  • ✅ UCMでトレンド・季節性を分離
  • ✅ 実務で使える時系列分析レポートの作成
Phase 3の学習を活かす実務シーン

シーン 使用手法 具体例
需要予測 SARIMA 春の引っ越しシーズンの問い合わせ数予測
広告効果測定 VAR + IRF 広告費1万円の累積CV増加数を算出
予算配分 グレンジャー因果性 効果の高いチャネルへの予算シフト
市場トレンド分析 UCM 一時的な変動と長期トレンドの分離
異常検知 構造変化検出 コロナなど外的ショックの影響評価
Phase 3で学んだ手法の限界

  • 線形性の仮定: ARIMAやVARは非線形関係を扱えない → Phase 4で機械学習を学ぶ
  • グレンジャー因果性の限界: 予測的因果≠真の因果 → Phase 1のRCT、DIDと組み合わせる
  • 構造変化への対応: 頻繁に再推定が必要 → ローリングウィンドウ、適応的手法の検討
  • 多変量の呪い: VARは変数が増えるとパラメータ爆発 → 縮小推定、因果探索で変数選択
Phase 4への準備

次のPhase 4では「機械学習×因果推論」を学びます。Phase 3で学んだ時系列分析と組み合わせることで、より高度な分析が可能になります:

  • アップリフトモデリング: 顧客セグメント別の広告効果を機械学習で推定
  • Doubly Robust推定: 時系列データの因果効果をバイアス除去して推定
  • SHAP値: 機械学習モデルの予測を因果的に解釈
  • 時系列 + 機械学習: LSTMやXGBoostで非線形な時系列予測

Phase 3で身につけた「時間の流れを考慮する」視点は、Phase 4-6でも活きます。

復習のための参考資料

  • 金本『因果推論』第7章を再読(理論の深掘り)
  • statsmodels公式ドキュメント(最新機能の確認)
    • ARIMA: https://www.statsmodels.org/stable/generated/statsmodels.tsa.arima.model.ARIMA.html
    • VAR: https://www.statsmodels.org/stable/vector_ar.html
    • UCM: https://www.statsmodels.org/stable/statespace.html
  • Kaggle時系列コンペ(実践練習)
    • Store Sales - Time Series Forecasting
    • M5 Forecasting

おめでとうございます!

Phase 3を完了し、時系列分析の基礎から応用まで習得しました。ARIMAでの予測、VARでの多変量分析、グレンジャー因果性検定、IRF分析、UCMでの成分分解など、実務で即戦力となるスキルを身につけました。

次のPhase 4では、機械学習と因果推論を統合し、アップリフトモデリングやDoubly Robust推定など、さらに高度な手法を学びます。Phase 3で培った時系列分析の知識が、Phase 4以降の学習を加速させるでしょう。

引き続き、Phase 4で「機械学習×因果推論」の世界へ!


-Uncategorized