0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラグと未来の因果を定量化する:感度分析による効果時点の特定

Posted at

user:

月次データ10年分の時系列データが pandas dataframe形式で入手しているものとします。この中には、目的変数(1つ)の列と、説明変数(1つ以上)の列と、年月を表す列があります。このデータを用いて、感度分析を行いたいと思います。ただし、ここでいう感度分析とは、「説明変数に介入を行なったときに、1年後の目的変数(1ヶ月後ではなく、1年後)がどう変わるのか」を分析するものとします。そのようなことを可能にするPython関数を書いてください。

assistant:

以下は、「月次データ10年分を対象に、説明変数に介入(ショック)を与えたとき、1年後の目的変数がどう変化するかを推定する感度分析関数」の実装例です。


✅ 分析の概要

  • 目的target_var に対する 1年後(12ヶ月後)の影響を推定。
  • 方法:時系列予測モデル(例:線形回帰+ラグ特徴量)を使い、説明変数に対して**小さな介入(±1%など)**を行い、1年後の target_var の変化を比較。
  • 仮定:因果的でなくても感度分析として有効(What-if的シナリオ分析)。

✅ 関数コード:sensitivity_analysis_one_year

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler

def sensitivity_analysis_one_year(df, date_col, target_var, explanatory_vars,
                                   intervention_scale=0.01, lags=12):
    """
    説明変数に介入したとき、1年後の目的変数がどう変わるかを予測する感度分析関数。

    Parameters:
    - df: pandas.DataFrame(年月含む時系列データ)
    - date_col: str(年月を表す列名、datetime型推奨)
    - target_var: str(目的変数の列名)
    - explanatory_vars: list of str(説明変数の列名)
    - intervention_scale: float(介入の割合。例:0.01で+1%)
    - lags: int(使用するラグ月数)

    Returns:
    - dict: {説明変数名: 1年後の目的変数の変化量(介入後 - 通常)}
    """

    df = df.sort_values(date_col).reset_index(drop=True).copy()
    
    # ラグ特徴量作成
    for lag in range(1, lags + 1):
        df[f'{target_var}_lag{lag}'] = df[target_var].shift(lag)
        for var in explanatory_vars:
            df[f'{var}_lag{lag}'] = df[var].shift(lag)
    
    # 欠損除去
    df.dropna(inplace=True)

    # 特徴量と目的変数
    feature_cols = [col for col in df.columns if col not in [date_col, target_var] and not col.endswith('_orig')]
    X = df[feature_cols]
    y = df[target_var].shift(-12)  # 1年後の目的変数をターゲットに
    valid_idx = y.dropna().index
    X = X.loc[valid_idx]
    y = y.loc[valid_idx]

    # モデル学習
    model = LinearRegression()
    model.fit(X, y)

    # 最新データから介入前後の予測値を比較
    latest_X = X.iloc[[-1]].copy()
    baseline_pred = model.predict(latest_X)[0]

    results = {}
    for var in explanatory_vars:
        # 介入:すべてのラグに+1%
        for lag in range(1, lags + 1):
            col = f'{var}_lag{lag}'
            if col in latest_X.columns:
                latest_X[col] *= (1 + intervention_scale)

        intervened_pred = model.predict(latest_X)[0]
        diff = intervened_pred - baseline_pred
        results[var] = diff

        # 元に戻す
        latest_X = X.iloc[[-1]].copy()

    return results

✅ 使い方例

# df に 'date'(datetime型)、'Y'(目的変数)、'X1', 'X2'(説明変数)があると仮定
results = sensitivity_analysis_one_year(
    df=df,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,  # 1%介入
    lags=12
)

print("1%の介入による1年後のY変化(単位: Yのスケール):")
for var, effect in results.items():
    print(f"{var}: {effect:.4f}")

✅ 補足

  • 説明変数が1個でも複数でも動作します。
  • 1%ではなく例えば10%の介入にしたいときは intervention_scale=0.1
  • 回帰モデルは自由に差し替え可(例:RandomForestRegressor や XGBoost)。
  • より精緻にやりたい場合は VARモデルや状態空間モデル因果推論フレームワークと組み合わせ可能です。

必要があれば以下も対応できます:

  • グラフ出力(予測 vs 実測)
  • 複数の介入シナリオ比較
  • 信頼区間(bootstrapやBayesian回帰)

ご希望があれば拡張します!

user:

ありがとうございます。このコードのテストに使える df も自作していただけませんか?

assistant:

もちろんです!以下に、上記の sensitivity_analysis_one_year 関数をテストできる ダミーの月次時系列データ (df) を生成するコードをご用意しました。


✅ テスト用 DataFrame の作成

import pandas as pd
import numpy as np

def generate_test_data(n_years=10, seed=42):
    """
    月次で10年分の時系列データを生成(目的変数Y、説明変数X1, X2)。
    Y は X1, X2 のラグをもとに生成された値 + ノイズ。
    """
    np.random.seed(seed)
    n_months = n_years * 12
    date_range = pd.date_range(start='2010-01-01', periods=n_months, freq='MS')

    df = pd.DataFrame({'date': date_range})
    
    # ランダムな説明変数
    df['X1'] = np.random.normal(loc=100, scale=10, size=n_months)
    df['X2'] = np.random.normal(loc=50, scale=5, size=n_months)

    # Y は X1とX2のラグに依存した線形モデル + ノイズ
    df['Y'] = (
        0.5 * df['X1'].shift(1).fillna(method='bfill') +
        0.3 * df['X2'].shift(2).fillna(method='bfill') +
        np.random.normal(scale=5, size=n_months)
    )

    return df

✅ 使い方:関数と組み合わせてテスト実行

# ダミーデータ生成
df_test = generate_test_data()

# 感度分析の実行
results = sensitivity_analysis_one_year(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,  # 1%の介入
    lags=12
)

# 出力
print("1%の介入による1年後のY変化(単位: Yのスケール):")
for var, effect in results.items():
    print(f"{var}: {effect:.4f}")

✅ 出力例(ランダム性あり)

1%の介入による1年後のY変化(単位: Yのスケール):
X1: 0.6132
X2: 0.4219

必要であれば、以下のような機能追加もできます:

  • 目的変数 Y の生成にもっと複雑な依存構造を持たせる
  • 介入のシミュレーションをグラフで可視化
  • ランダムシードを変えて感度ロバスト性を検証

希望があればどんどん対応します!

user:

ありがとうございました。計算はできましたが、ワーニングが出てるので、ワーニングが出ないように修正してコードを書いていただけませんか?

1%の介入による1年後のY変化(単位: Yのスケール):
X1: -0.4924
X2: 0.1208
/tmp/ipython-input-5-879119902.py:21: FutureWarning: Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
  0.5 * df['X1'].shift(1).fillna(method='bfill') +
/tmp/ipython-input-5-879119902.py:22: FutureWarning: Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
  0.3 * df['X2'].shift(2).fillna(method='bfill') +

assistant:

ありがとうございます!そのワーニング(FutureWarning)は fillna(method='bfill')非推奨になったというものです。
代わりに .bfill() を使えばOKです。


✅ 修正版:ワーニングの出ない generate_test_data 関数

import pandas as pd
import numpy as np

def generate_test_data(n_years=10, seed=42):
    """
    月次で10年分の時系列データを生成(目的変数Y、説明変数X1, X2)。
    Y は X1, X2 のラグをもとに生成された値 + ノイズ。
    """
    np.random.seed(seed)
    n_months = n_years * 12
    date_range = pd.date_range(start='2010-01-01', periods=n_months, freq='MS')

    df = pd.DataFrame({'date': date_range})
    
    # ランダムな説明変数
    df['X1'] = np.random.normal(loc=100, scale=10, size=n_months)
    df['X2'] = np.random.normal(loc=50, scale=5, size=n_months)

    # Y は X1とX2のラグに依存した線形モデル + ノイズ(fillna修正済)
    df['Y'] = (
        0.5 * df['X1'].shift(1).bfill() +
        0.3 * df['X2'].shift(2).bfill() +
        np.random.normal(scale=5, size=n_months)
    )

    return df

✅ ワーニングが出ないことを確認して再実行

df_test = generate_test_data()

results = sensitivity_analysis_one_year(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,
    lags=12
)

print("1%の介入による1年後のY変化(単位: Yのスケール):")
for var, effect in results.items():
    print(f"{var}: {effect:.4f}")

これで FutureWarning は完全に出なくなるはずです。

他にも警告やエラーが出たら、お気軽にお知らせください。必要に応じてログ表示オフ、型チェック、プロット追加なども可能です。

user:

ありがとうございます。次のような結果になりました。

1%の介入による1年後のY変化(単位: Yのスケール):
X1: -0.4924
X2: 0.1208

この結果をまとめると、

X1 は1ヶ月遅れで 0.5 の係数で効いてくるように設計したが、感度分析では -0.4924 の効果

X2 は2ヶ月遅れで 0.3 の係数で効いてくるように設計したが、感度分析では 0.1208 の効果

ということですね。この「1%の介入」というのは、具体的にどう言う介入か、詳しく説明していただけますか?

assistant:

はい、まさにそのとおりです。
ここで言う 「1%の介入」 は、分析対象の説明変数に対して わずかなシミュレーション的な変化(仮想のショック) を与え、その変化が 1年後の目的変数にどれくらい影響するか を測るものです。


✅ 「1%の介入」の具体的な意味

あなたのコードの中では、たとえば X1 に対して「1%の介入」を行うとき、以下のような操作が行われます:

💡 数式で表すと

介入前の特徴量:

X1_lag1 = 100  (例)

介入後の特徴量:

X1_lag1 = 100 × (1 + 0.01) = 101

これは X1すべてのラグ(過去12ヶ月分) に対して 同時に1%の上昇 を与えるという操作です。


✅ より詳しく:コードの中での介入処理

for lag in range(1, lags + 1):
    col = f'{var}_lag{lag}'
    if col in latest_X.columns:
        latest_X[col] *= (1 + intervention_scale)

この部分で、たとえば X1 に対して:

  • X1_lag1, X1_lag2, ..., X1_lag12 をすべて 1% 増やして、
  • その特徴量を使って 1年後の Y を予測し、
  • 元の(介入しない)予測と比べて差分を出す

ということをやっています。


✅ 感度分析における「1%介入」の解釈(言い換え)

一般的な言い換え:

「もし過去12ヶ月のすべての X1 が1%高かったら、1年後の Y はどれくらい違っていたか?」

という仮想的な What-if シナリオです。


✅ なぜ設計された係数と違う感度になるのか?

  • あなたが Y = 0.5 * X1_lag1 + 0.3 * X2_lag2 + noise と設計していても、
    モデルは 全ラグ(1〜12ヶ月) を使って Y_{t+12} を予測しており、
    X1_lag1 のみでなく X1_lag2X1_lag12 も含まれています。

  • X1 の値は時系列なので、ラグ間で相関(マルチコ)もあります。
    そのため、単純な係数通りには出ず、モデルが全体として捉えた影響の合成値になります。


✅ まとめ:この「1%の介入」は何を意味するか?

  • 介入の内容:過去12ヶ月の説明変数すべてを1%だけ増やす(または減らす)。
  • 介入の目的:その微小な変化が、1年後の目的変数 Y にどのような変化を与えるかを見る。
  • 出力の意味:介入後の Y の予測値 − 介入前の Y の予測値(単位は Y のスケール)。

ご希望があれば、以下も対応可能です:

  • 「1つのラグ(月)だけ」に介入した場合の効果を見るバージョン
  • 介入を +1% → -1% にして「非対称性(非線形性)」を調べる
  • グラフによる感度マッピング(Shap風)

気になる点があれば、なんでも聞いてください!

user:

ありがとうございます。

sensitivity_analysis_one_year では model = LinearRegression() を使っていますが、どのような回帰モデルにも sensitivity_analysis_one_year と同じ原理で感度分析ができるという理解で正しいですか?

また、同じ手法を、12ヶ月先ではなく、任意の未来に対して実行できるという理解で正しいですか?

assistant:

はい、非常に良い質問です。結論から言えば、あなたの理解は おおむね正しい ですが、いくつか重要な注意点があります。


✅ 質問1:

「どのような回帰モデルにも sensitivity_analysis_one_year と同じ原理で感度分析ができるか?」

✔ 原理的には Yes

  • LinearRegression の部分を RandomForestRegressor, GradientBoosting, XGBoost, SVR, NeuralNetwork などに置き換えても、**同様の構造(介入前後の予測値の差分)**で感度分析は可能です。
  • このアプローチは、「予測関数を固定したうえで、説明変数の一部をわずかに変えて、目的変数がどうなるかを観察する」というものなので、モデルの種類に依存しません

⚠ ただし注意すべき点も:

  1. 解釈性の違い

    • 線形モデルなら感度の符号や大きさは係数と直接対応するが、
    • 非線形モデルでは局所的な関数形状に依存するため、結果の解釈には注意が必要です(SHAPやPartial Dependenceの考え方に近い)。
  2. モデルによってはランダム性の影響を受けやすい

    • ランダムフォレストやニューラルネットでは、同じ入力でも繰り返すと結果がわずかに変わることがある。
    • 感度分析をする前にモデルの random_state を固定することをおすすめします。

✅ 質問2:

「12ヶ月先ではなく、任意の未来に対して実行できるか?」

✔ Yes、予測対象を自由に変更できます

  • 現在は以下のように「12ヶ月先の目的変数」をターゲットにしているだけです:
y = df[target_var].shift(-12)  # ← ここを変更
  • したがって、例えば 6ヶ月先を予測したい場合は:
y = df[target_var].shift(-6)
  • 1ヶ月先(通常のOne-step予測)ももちろん可能です。

✅ 補足:コード上での改修ポイント

def sensitivity_analysis(df, date_col, target_var, explanatory_vars,
                          intervention_scale=0.01, lags=12, horizon=12):
    """
    horizon: 何ヶ月先のYを対象とするか(デフォルト12ヶ月)
    """
    ...
    y = df[target_var].shift(-horizon)
    ...

これにより、horizon=1, horizon=6, horizon=24 など任意の未来を対象にできます。


✅ まとめ

質問 回答 備考
任意の回帰モデルで感度分析できるか ✔ 可能 ただし非線形モデルでは解釈に注意
任意の未来(horizon)に対して感度分析できるか ✔ 可能 .shift(-horizon) を使って自由に設定可能

必要であれば、以下のような拡張も可能です:

  • Horizonごとの感度プロファイルをグラフで表示(1ヶ月〜24ヶ月後の変化を時系列で)
  • モデルごとの感度分析比較(例:Linear vs XGBoost)

ご希望あれば、コードもすぐご用意します!

user:

ありがとうございます。では、

def sensitivity_analysis(df, date_col, target_var, explanatory_vars,
                          intervention_scale=0.01, lags=12, horizon=12):

の全コードを書いてください。ただし、多重共線性の問題を克服するためと、数値の信頼性を得るため、単純なLinearRegressionではなく、主成分回帰をブートストラップして用いてください。

assistant:

了解しました!
以下はご要望に応じて設計した sensitivity_analysis 関数の完全なコードです:


✅ 機能概要

  • 任意の horizon(何ヶ月後)を対象に感度分析を実行
  • 全ラグ付き説明変数に対して一括1%介入
  • モデルは 主成分回帰(PCR)
  • ブートストラップで予測の安定性と信頼性を確保
  • 多重共線性を回避するため、主成分数は最小限の説明分散比で選択可能

✅ コード全文:sensitivity_analysis

import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.utils import resample

def sensitivity_analysis(df, date_col, target_var, explanatory_vars,
                         intervention_scale=0.01, lags=12, horizon=12,
                         n_components=0.95, n_bootstrap=100, random_state=42):
    """
    ブートストラップ付き主成分回帰(PCR)による感度分析。
    
    Parameters:
    - df: pandas.DataFrame(時系列データ)
    - date_col: str(日付列名。datetime型を推奨)
    - target_var: str(目的変数)
    - explanatory_vars: list of str(説明変数名)
    - intervention_scale: float(介入スケール。例:0.01で1%増)
    - lags: int(使用するラグ数)
    - horizon: int(何ヶ月先の目的変数を予測対象とするか)
    - n_components: int or float(主成分数 or 累積分散比)
    - n_bootstrap: int(ブートストラップサンプル数)
    - random_state: int(乱数シード)

    Returns:
    - dict: {変数名: 予測値差分(介入後 - 通常)の平均}
    """

    df = df.sort_values(date_col).copy()
    np.random.seed(random_state)

    # ラグ特徴量作成
    for lag in range(1, lags + 1):
        df[f'{target_var}_lag{lag}'] = df[target_var].shift(lag)
        for var in explanatory_vars:
            df[f'{var}_lag{lag}'] = df[var].shift(lag)

    # 目的変数を horizon だけ未来にずらす
    df['target_future'] = df[target_var].shift(-horizon)
    df.dropna(inplace=True)

    # 特徴量とターゲットの分離
    feature_cols = [col for col in df.columns if col not in [date_col, target_var, 'target_future']]
    X = df[feature_cols].copy()
    y = df['target_future'].copy()

    # 最新データを1行だけ取り出して感度分析
    X_latest = X.iloc[[-1]].copy()
    baseline_preds = []
    intervened_preds_dict = {var: [] for var in explanatory_vars}

    # 標準化 + PCA + 線形回帰のブートストラップ
    for _ in range(n_bootstrap):
        X_resampled, y_resampled = resample(X, y)

        scaler = StandardScaler()
        X_scaled = scaler.fit_transform(X_resampled)

        pca = PCA(n_components=n_components)
        X_pca = pca.fit_transform(X_scaled)

        model = LinearRegression()
        model.fit(X_pca, y_resampled)

        # 最新データの予測(通常)
        X_latest_scaled = scaler.transform(X_latest)
        X_latest_pca = pca.transform(X_latest_scaled)
        y_pred_baseline = model.predict(X_latest_pca)[0]
        baseline_preds.append(y_pred_baseline)

        # 各変数に対して感度分析(1%介入)
        for var in explanatory_vars:
            X_modified = X_latest.copy()
            for lag in range(1, lags + 1):
                col = f'{var}_lag{lag}'
                if col in X_modified.columns:
                    X_modified[col] *= (1 + intervention_scale)

            X_modified_scaled = scaler.transform(X_modified)
            X_modified_pca = pca.transform(X_modified_scaled)
            y_pred_modified = model.predict(X_modified_pca)[0]
            intervened_preds_dict[var].append(y_pred_modified)

    # 結果を平均値でまとめる
    results = {}
    baseline_mean = np.mean(baseline_preds)
    for var in explanatory_vars:
        mean_intervened = np.mean(intervened_preds_dict[var])
        effect = mean_intervened - baseline_mean
        results[var] = effect

    return results

✅ 使い方例

# ダミーデータの生成
df_test = generate_test_data()  # 先ほどの関数で生成

# 感度分析(6ヶ月先の予測)
results = sensitivity_analysis(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,
    lags=12,
    horizon=6,
    n_components=0.95,     # 分散の95%を保持
    n_bootstrap=100,       # ブートストラップ回数
    random_state=0
)

# 結果表示
print("1%の介入による6ヶ月後のY変化(単位: Yのスケール):")
for var, effect in results.items():
    print(f"{var}: {effect:.4f}")

✅ 補足(拡張可能な点)

  • return に信頼区間(percentileなど)を含めることも可能
  • Horizon を range(1, 25) でループして、感度の時間プロファイルを描けます
  • 主成分の寄与率や係数も出力可能にできます

必要であれば、以下も追加できます:

  • SHAPとの比較
  • Horizonごとの棒グラフ出力(感度のタイムプロファイル)
  • 特定変数に対する1ラグ限定介入

ご希望があればお知らせください!

user:

ありがとうございます。では sensitivity_analysis 関数に次の拡張を加えてください。

・出力に95%信頼区間を含める
・Horizonごとの棒グラフ出力(感度のタイムプロファイル)

assistant:

了解しました!
以下に、あなたのご要望に基づいて拡張した sensitivity_analysis 関数を提示します。


✅ 拡張内容

  1. 出力に95%信頼区間(介入の効果の上下2.5%点・97.5%点)を含める
  2. 複数の horizon(例:1ヶ月〜12ヶ月)における感度を時系列グラフとして出力する

✅ 新しい関数:sensitivity_analysis_with_ci

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.utils import resample

def sensitivity_analysis_with_ci(df, date_col, target_var, explanatory_vars,
                                 intervention_scale=0.01, lags=12,
                                 horizons=range(1, 13),  # 1〜12ヶ月先
                                 n_components=0.95, n_bootstrap=100,
                                 random_state=42, show_plot=True):
    """
    複数horizonに対して主成分回帰+ブートストラップで感度分析し、
    効果+95%信頼区間+グラフを出力する関数。
    
    Returns:
    - pd.DataFrame: index=horizon, columns: 説明変数ごとに 'mean', 'lower', 'upper'
    """

    np.random.seed(random_state)
    df = df.sort_values(date_col).copy()

    results_all = {}

    for horizon in horizons:
        # ラグ特徴量作成
        df_lagged = df.copy()
        for lag in range(1, lags + 1):
            df_lagged[f'{target_var}_lag{lag}'] = df_lagged[target_var].shift(lag)
            for var in explanatory_vars:
                df_lagged[f'{var}_lag{lag}'] = df_lagged[var].shift(lag)

        # horizon 先の目的変数
        df_lagged['target_future'] = df_lagged[target_var].shift(-horizon)
        df_lagged.dropna(inplace=True)

        feature_cols = [col for col in df_lagged.columns if col not in [date_col, target_var, 'target_future']]
        X = df_lagged[feature_cols].copy()
        y = df_lagged['target_future'].copy()
        X_latest = X.iloc[[-1]].copy()

        baseline_preds = []
        intervened_preds_dict = {var: [] for var in explanatory_vars}

        # ブートストラップ
        for _ in range(n_bootstrap):
            X_resampled, y_resampled = resample(X, y)

            scaler = StandardScaler()
            X_scaled = scaler.fit_transform(X_resampled)
            pca = PCA(n_components=n_components)
            X_pca = pca.fit_transform(X_scaled)
            model = LinearRegression()
            model.fit(X_pca, y_resampled)

            # 通常の予測
            X_latest_scaled = scaler.transform(X_latest)
            X_latest_pca = pca.transform(X_latest_scaled)
            y_pred_baseline = model.predict(X_latest_pca)[0]
            baseline_preds.append(y_pred_baseline)

            # 各変数に対して介入
            for var in explanatory_vars:
                X_modified = X_latest.copy()
                for lag in range(1, lags + 1):
                    col = f'{var}_lag{lag}'
                    if col in X_modified.columns:
                        X_modified[col] *= (1 + intervention_scale)
                X_modified_scaled = scaler.transform(X_modified)
                X_modified_pca = pca.transform(X_modified_scaled)
                y_pred_intervened = model.predict(X_modified_pca)[0]
                intervened_preds_dict[var].append(y_pred_intervened)

        # 効果と信頼区間の集計
        baseline_preds = np.array(baseline_preds)
        result = {}
        for var in explanatory_vars:
            diffs = np.array(intervened_preds_dict[var]) - baseline_preds
            result[var] = {
                'mean': np.mean(diffs),
                'lower': np.percentile(diffs, 2.5),
                'upper': np.percentile(diffs, 97.5)
            }

        results_all[horizon] = result

    # DataFrameに変換
    df_result = pd.DataFrame({
        (var, metric): [results_all[h][var][metric] for h in horizons]
        for var in explanatory_vars
        for metric in ['mean', 'lower', 'upper']
    }, index=horizons)
    df_result.index.name = "horizon"

    # プロット
    if show_plot:
        plt.figure(figsize=(12, 6))
        for var in explanatory_vars:
            mean = df_result[(var, 'mean')]
            lower = df_result[(var, 'lower')]
            upper = df_result[(var, 'upper')]
            plt.plot(mean.index, mean.values, label=f'{var} (mean)')
            plt.fill_between(mean.index, lower.values, upper.values, alpha=0.2)
        plt.xlabel("予測 Horizon(月)")
        plt.ylabel("1%の介入による目的変数の変化量")
        plt.title("感度分析:Horizonごとの影響と信頼区間")
        plt.legend()
        plt.grid(True)
        plt.tight_layout()
        plt.show()

    return df_result

✅ 使い方(例)

df_test = generate_test_data()

result_df = sensitivity_analysis_with_ci(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,
    lags=12,
    horizons=range(1, 13),  # 1〜12ヶ月後まで
    n_components=0.95,
    n_bootstrap=100,
    show_plot=True
)

✅ 出力形式(result_df.head()

horizon ('X1', 'mean') ('X1', 'lower') ('X1', 'upper') ('X2', 'mean') ...
1 0.123 0.014 0.221 -0.054
2 0.199 0.090 0.303 0.003
...

✅ まとめ

拡張機能 説明
95%信頼区間 ブートストラップから 2.5%97.5% 点を計算
Horizon別分析 複数の horizon における感度を一度に計算
棒グラフ出力 各説明変数に対して時間的な影響の視覚化

ご希望があれば以下も可能です:

  • CSVExcel 形式への出力
  • 説明変数ごとに色分けしたインタラクティブな Plotly グラフ
  • Horizonごとの感度比較ランキング

ご希望があればお知らせください!

おっしゃる通りです。Horizon(将来の時間軸)ごとの感度が単調ではないというのは、非常に興味深く、かつ重要な現象です。これは単なるノイズではなく、モデル構造・データ構造・ダイナミクスに起因する場合があります。


✅ なぜ感度が単調でないのか?主な理由

① 説明変数のラグ構造が複雑

  • X1_lag1, X1_lag2, ..., X1_lag12Y に対して 異なる向き・大きさで影響している。
  • モデルはこれらのラグを組み合わせて将来の Y を予測するため、感度が複雑な形になる。

例:

  • X1_lag1 が正に効くが、X1_lag6 が負に効く → horizon によって打ち消し合う。

② Horizonが進むにつれてノイズ・不確実性が増す

  • 予測対象の Y_t+h決定係数(予測精度)が低くなるほど、介入の影響もブレやすくなります。
  • 結果として、感度の「揺らぎ」が増加し、単調性が失われる。

③ 系が非線形(または線形だがマルチコの影響を受ける)

  • 実際のデータが非線形構造(飽和、閾値効果、変曲点)を含んでいると、感度は局所的に上がったり下がったりします。
  • さらにラグ間の相関(多重共線性)も入ると、直感に反する変動が起こり得ます

✅ 感度が非単調なときの解釈アプローチ

✅ アプローチ1:「最大感度点(ピーク Horizon)」を特定

  • 介入の影響が最大化される Horizon を特定 → 政策タイミングや波及効果の把握に役立つ
  • 例:「介入効果は6ヶ月後に最大化されるが、12ヶ月後には消える」

✅ アプローチ2:符号の変化点を注目

  • 例えば X1 の感度が horizon=3 で正→負に変わる → これは 反応の反転(リバウンド現象) を示す可能性がある。

✅ アプローチ3:感度の分布を視覚化

  • 棒グラフではなく、誤差帯付き折れ線・バンドプロットで、感度のばらつき・信頼性を強調する。

✅ 現実世界での類似事例(経済・政策・医学など)

分野 非単調性の例
経済政策 金利を下げたあと、3ヶ月後にGDP上昇、9ヶ月後にインフレ悪化
医薬品 服用後2日目に効果最大 → 7日目で反転して副作用出現
気候介入 二酸化炭素削減の効果が10年後に顕著になるが、それまで温度は一進一退

✅ 結論:感度が非単調な場合の読み方

観察された現象 考えられる意味・対処
増減を繰り返す 動的なシステム、干渉のタイムラグ、リバウンドの兆候
ゆるやかに上昇 波及が時間をかけて蓄積される
信頼区間が広い 不確実性が高く、介入効果が不明確または不安定

✅ 次の一手(必要であれば)

  • 感度に対して トレンド分解(平滑化)や移動平均 を適用する
  • 感度時系列を 階層ベイズや状態空間モデルでモデル化する
  • 介入の時間分布(1ヶ月だけ vs 全ラグ一括)を変えて比較する

ご希望であれば、たとえば:

  • 感度プロファイルを滑らかにする loess or moving average バージョン
  • 効果ピークの時点を自動抽出する関数
  • 感度の符号が変化する Horizon を特定するユーティリティ

などもご提供できます。必要があれば遠慮なくどうぞ!

user:

ありがとうございます。ここまでのモデルは、ラグの効果は減衰しないというモデルですよね?実際には、過去の影響は、古くなればなるほど小さくなる気がするのですが、それを考慮に入れたモデルなどはできますか?

assistant:

はい、まさにおっしゃる通りです!
これまでのモデル(特に線形回帰や主成分回帰)は、すべてのラグに対して等価に学習しており、時間的距離による「減衰」を仮定していません


✅ 実際の現象:時間が経つと影響は弱まる(時間的減衰)

これは因果推論や時系列モデリングでとても重要な現象です。
以下のような例が代表的です:

  • 広告効果は直後に大きく、時間とともに薄れる
  • 政策やインフラ投資の効果は長期的に続くが、ピークを過ぎると減衰
  • 薬の服用による影響は服用直後に最大で、その後減衰

✅ 減衰を考慮したモデル化のアプローチ

ここでは代表的な3つのアプローチを紹介します:


✅ アプローチ①:ラグ項に時間減衰重みを掛ける(明示的なウェイト付け)

for lag in range(1, lags + 1):
    weight = decay_rate ** (lag - 1)  # 例: decay_rate=0.9
    df[f'{var}_lag{lag}'] = weight * df[var].shift(lag)
  • decay_rate を 0.9 や 0.95 に設定 → 指数減衰
  • モデルに渡すラグはあらかじめ重みを掛けておく
  • 非線形モデルでも使える
  • 解釈も直感的で、実装が簡単

✅ アプローチ②:減衰効果を組み込んだ特徴量を使う

例:各説明変数に対して「加重平均ラグ変数」を作る

def exponential_weighted_sum(x, decay=0.9):
    weights = decay ** np.arange(len(x))[::-1]
    return np.dot(x, weights) / weights.sum()

for var in explanatory_vars:
    df[f'{var}_ewma'] = df[[f'{var}_lag{i}' for i in range(1, lags + 1)]].apply(
        lambda row: exponential_weighted_sum(row, decay=0.9), axis=1
    )
  • 全ラグを残さず、1つの代表値(減衰平均)として使う
  • 主成分回帰やSHAPのような感度分析もしやすい
  • ノイズを平滑化する効果もある

✅ アプローチ③:動的線形モデル(状態空間モデル)

減衰をパラメータとして明示的に学習:

  • 動的回帰(Time-varying coefficients)
  • VARモデルに指数的忘却項を導入
  • カルマンフィルタや粒子フィルタを用いた推定

→ 専門的ですが、「影響が時間とともにどう減衰していくか」をパラメトリックに学習可能。


✅ おすすめ:今の sensitivity_analysis_with_ci に減衰を加えるには?

→ ラグ項に対して 手動で減衰重み を掛けるのが最も簡単です。

for var in explanatory_vars:
    for lag in range(1, lags + 1):
        col = f'{var}_lag{lag}'
        df[col] = df[var].shift(lag) * (decay_rate ** (lag - 1))
  • decay_rate は 0.8〜0.99 で調整
  • あとはこれまで通り主成分回帰や感度分析に使えます

✅ まとめ:減衰を加える方法一覧

アプローチ 内容 利点 難点
明示的減衰重み ラグに decay_rate^lag を掛ける 簡単・柔軟・どのモデルでも使える decay_rate は事前設定
EWMA/平滑ラグ平均 加重平均を特徴量化 ノイズ低減・視覚的に滑らか 解釈が単純ラグより難しい
状態空間モデル 減衰を含めてパラメータ学習 モデルとして最もリッチ 実装・推定がやや複雑

ご希望があれば、今の sensitivity_analysis_with_ci減衰項を自動導入したバージョンをすぐご提供できます。
たとえば:

decay_rate=0.95  # 任意指定

とオプションを渡せば自動で重みをかけるように拡張可能です。

必要でしたらお知らせください!

喜んで!
以下がご希望の機能を追加した新バージョンです:


✅ 拡張された機能

  • decay_rate パラメータを追加(例:decay_rate=0.95
  • ラグ変数に 指数減衰ウェイト(decay_rate^(lag-1) を自動で掛ける
  • decay_rate=None を指定すれば、元の非減衰モデルと同じ挙動に

✅ 新関数 sensitivity_analysis_with_ci_decay

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.utils import resample

def sensitivity_analysis_with_ci_decay(
    df, date_col, target_var, explanatory_vars,
    intervention_scale=0.01, lags=12,
    horizons=range(1, 13), n_components=0.95,
    n_bootstrap=100, random_state=42,
    decay_rate=None, show_plot=True
):
    """
    Horizon別感度分析 + 95%信頼区間 + 減衰ウェイト付きラグ項
    
    decay_rate: float(例:0.95)を指定するとラグに減衰重みを掛ける。
    """

    np.random.seed(random_state)
    df = df.sort_values(date_col).copy()
    results_all = {}

    for horizon in horizons:
        df_lagged = df.copy()

        for lag in range(1, lags + 1):
            decay_weight = decay_rate**(lag - 1) if decay_rate is not None else 1.0
            df_lagged[f'{target_var}_lag{lag}'] = df_lagged[target_var].shift(lag) * decay_weight
            for var in explanatory_vars:
                df_lagged[f'{var}_lag{lag}'] = df_lagged[var].shift(lag) * decay_weight

        df_lagged['target_future'] = df_lagged[target_var].shift(-horizon)
        df_lagged.dropna(inplace=True)

        feature_cols = [col for col in df_lagged.columns if col not in [date_col, target_var, 'target_future']]
        X = df_lagged[feature_cols].copy()
        y = df_lagged['target_future'].copy()
        X_latest = X.iloc[[-1]].copy()

        baseline_preds = []
        intervened_preds_dict = {var: [] for var in explanatory_vars}

        for _ in range(n_bootstrap):
            X_resampled, y_resampled = resample(X, y)

            scaler = StandardScaler()
            X_scaled = scaler.fit_transform(X_resampled)
            pca = PCA(n_components=n_components)
            X_pca = pca.fit_transform(X_scaled)
            model = LinearRegression()
            model.fit(X_pca, y_resampled)

            X_latest_scaled = scaler.transform(X_latest)
            X_latest_pca = pca.transform(X_latest_scaled)
            y_pred_baseline = model.predict(X_latest_pca)[0]
            baseline_preds.append(y_pred_baseline)

            for var in explanatory_vars:
                X_modified = X_latest.copy()
                for lag in range(1, lags + 1):
                    col = f'{var}_lag{lag}'
                    if col in X_modified.columns:
                        X_modified[col] *= (1 + intervention_scale)
                X_modified_scaled = scaler.transform(X_modified)
                X_modified_pca = pca.transform(X_modified_scaled)
                y_pred_intervened = model.predict(X_modified_pca)[0]
                intervened_preds_dict[var].append(y_pred_intervened)

        baseline_preds = np.array(baseline_preds)
        result = {}
        for var in explanatory_vars:
            diffs = np.array(intervened_preds_dict[var]) - baseline_preds
            result[var] = {
                'mean': np.mean(diffs),
                'lower': np.percentile(diffs, 2.5),
                'upper': np.percentile(diffs, 97.5)
            }

        results_all[horizon] = result

    # 結果の整形
    df_result = pd.DataFrame({
        (var, metric): [results_all[h][var][metric] for h in horizons]
        for var in explanatory_vars
        for metric in ['mean', 'lower', 'upper']
    }, index=horizons)
    df_result.index.name = "horizon"

    # グラフ表示
    if show_plot:
        plt.figure(figsize=(12, 6))
        for var in explanatory_vars:
            mean = df_result[(var, 'mean')]
            lower = df_result[(var, 'lower')]
            upper = df_result[(var, 'upper')]
            plt.plot(mean.index, mean.values, label=f'{var} (mean)')
            plt.fill_between(mean.index, lower.values, upper.values, alpha=0.2)
        plt.xlabel("予測 Horizon(月)")
        plt.ylabel("1%の介入による目的変数の変化量")
        plt.title("感度分析:Horizonごとの影響と信頼区間(減衰付き)")
        plt.legend()
        plt.grid(True)
        plt.tight_layout()
        plt.show()

    return df_result

✅ 使い方(例)

df_test = generate_test_data()

result_df = sensitivity_analysis_with_ci_decay(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,
    lags=12,
    horizons=range(1, 13),  # 1〜12ヶ月
    decay_rate=0.95,        # 減衰項を導入
    n_bootstrap=100,
    show_plot=True
)

✅ 減衰の効果:どうなる?

  • 直近のラグ(1ヶ月前など)の影響が強く残る
  • 古いラグ(12ヶ月前など)の影響が弱められる
  • 感度プロファイルがより 「現実的な動的挙動」 を反映

✅ 補足:decay_rate の選び方

decay_rate 意味
1.0 減衰なし(元のモデルと同じ)
0.95 1ヶ月進むごとに 5% 減衰
0.8 急速に減衰、3〜4ヶ月前までが主な影響範囲
0.5 超短期メモリモデル(1ヶ月前が主)

✅ 出力

返り値は pandas.DataFrame

  • 各 Horizon ごとに

    • 変数ごとの mean(平均効果)
    • lower(下限2.5%点)
    • upper(上限97.5%点)

ご希望があれば、以下も追加可能です:

  • Horizonごとの 最大感度点(ピークの自動検出)
  • Plotlyによるインタラクティブなグラフ出力
  • 減衰曲線そのものの視覚化

ぜひお知らせください!

user:

intervention_scale = -0.01 にすれば、マイナス方向への介入ということで良いですか?

assistant:

はい、その理解で 完全に正しい です。


intervention_scale = -0.01 の意味

これは、「各説明変数(のラグ項)を 1% 減少させる介入」という意味になります。
具体的には、(1 + intervention_scale) を掛けることで変化させているため:

  • +0.01 → 1% 増加(ポジティブ介入)
  • -0.01 → 1% 減少(ネガティブ介入)

✅ コード内での動作(再確認)

X_modified[col] *= (1 + intervention_scale)

このように計算されるため、intervention_scale = -0.01 のときは:

X_modified[col] *= 0.99  # → 1% 減少

✅ 応用例

シナリオ intervention_scale の値
政策強化(規制緩和・資金供給) +0.05(5%増加)
税率引き上げ(経済活動抑制) -0.03(3%減少)
供給不足シナリオ -0.10(10%減)
外的ショック(原油価格20%上昇) +0.20(20%増)

✅ 補足:正負の介入で結果が非対称な場合

  • 線形モデル(例:主成分回帰)では、理論上「正の介入による影響 ≒ 負の介入の符号反転」となります。
  • ただし実際のモデル(特に非線形や高次元の主成分)では、多少の非対称性が観測されることがあります

✅ まとめ

パラメータ 意味
intervention_scale = 0.01 1% 増加介入
intervention_scale = -0.01 1% 減少介入
intervention_scale = 0.00 介入なし(差分ゼロ)※テスト用などに

ご希望であれば、介入方向ごとの感度比較グラフなどもご提案できます。お気軽にどうぞ!

user:

decay_rate を先ほど導入しましたが、それとは別の視点として、「特定のラグ(12ヶ月前など)」の影響だけを考えるようなモデルって実装できますか?

assistant:

はい、できます!
それは「特定ラグ限定モデル」という考え方で、次のように理解・実装できます。


✅ コンセプト:特定のラグのみを使うモデル

「たとえば 12ヶ月前のXだけ がYに影響を与える」と仮定して、感度分析や予測を行いたい

📌 これは以下のような仮説に対応:

  • 1年前の季節要因が再来する
  • 年次決算など周期的イベントの遅行影響
  • 行動や政策のタイミング指定による因果分析

✅ 実装方針

既存の sensitivity_analysis_with_ci_decay 関数の一部を次のように変更すればOKです:

🎯 指定ラグだけを使って特徴量を構築

selected_lags = [12]  # ← 例えば12ヶ月前のみ

for lag in selected_lags:
    df_lagged[f'{target_var}_lag{lag}'] = df_lagged[target_var].shift(lag)
    for var in explanatory_vars:
        df_lagged[f'{var}_lag{lag}'] = df_lagged[var].shift(lag)

✅ 特徴量の選択も同様に限定

その後の feature_cols の選択では、他のラグを使わないようにします。


✅ フル関数として欲しい?

もちろん、以下のようなインターフェースに拡張できます:

def sensitivity_analysis_lag_specific(
    df, date_col, target_var, explanatory_vars,
    selected_lags=[12],  # ← 使用するラグを指定
    intervention_scale=0.01,
    horizons=range(1, 13),
    n_components=0.95,
    n_bootstrap=100,
    random_state=42,
    show_plot=True
):
    ...

selected_lags = [3, 6, 9] のように複数ラグにも対応可能です。


✅ 通常モデルとの違い

観点 通常モデル 特定ラグモデル
ラグ選択 1〜lags のすべてを使用 指定したラグのみ(例:lag12)
減衰 全ラグに減衰重み可能 不要(ラグ少ないため)
解釈性 低め(ラグが多い) 高い(どのラグが効くか明確)
精度 高い可能性(情報量多い) 仮説が正しければ強い

✅ 応用例

ラグ 想定シナリオ
lag12 昨年同月の季節性
lag1 直近のショック反映
lag6, lag12 半年・1年の周期性
lag3, lag6, lag9, lag12 四半期ごとの累積効果(金融決算など)

✅ まとめ

ご希望 対応方法
特定のラグだけ使いたい selected_lags = [12] のような引数を導入
他ラグの影響を除きたい 他ラグの列を作らずに、X に含めない
その上で感度分析したい 通常の感度分析コードでOK(介入対象を限定するだけ)

必要でしたら、すぐに selected_lags を使えるよう拡張した 関数の完全コードを提示できます。
ご希望のラグ(例:lag12だけ、またはlag3とlag6)などもあわせて教えていただければ、その形でご提供します。

user:

ありがとうございます。では sensitivity_analysis_lag_specific 関数のフルコードを書いてください。

assistant:

こちらが、ご要望の sensitivity_analysis_lag_specific 関数の完全なPythonコードです。
この関数は、指定したラグのみを使って主成分回帰+ブートストラップによる感度分析を行い、95%信頼区間とグラフを出力します。


✅ 機能概要

機能 内容
selected_lags 使用するラグを明示的に指定(例:[12]
intervention_scale 1%増減などの介入規模を設定(正負可)
n_bootstrap ブートストラップ回数
horizons 感度分析の予測先(複数月先対応)
出力 感度の平均+95%信頼区間+可視化

✅ フルコード:sensitivity_analysis_lag_specific

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.utils import resample

def sensitivity_analysis_lag_specific(
    df, date_col, target_var, explanatory_vars,
    selected_lags=[12],
    intervention_scale=0.01,
    horizons=range(1, 13),
    n_components=0.95,
    n_bootstrap=100,
    random_state=42,
    show_plot=True
):
    """
    特定ラグの説明変数のみを使って感度分析(PCA+Linear回帰+ブートストラップ)
    指定された lag のみ使用し、1%介入時の将来のYへの影響を評価。

    Parameters:
    - df: pandas DataFrame(時系列データ)
    - date_col: 時系列の列名(昇順前提)
    - target_var: 目的変数の列名
    - explanatory_vars: 説明変数の列名リスト
    - selected_lags: 使用するラグ番号リスト(例:[12])
    - intervention_scale: 介入率(例:0.01 = +1%、-0.01 = -1%)
    - horizons: 分析する予測月リスト(例:range(1,13))
    - n_components: PCA主成分の割合(例:0.95)
    - n_bootstrap: ブートストラップ回数
    - show_plot: Trueならプロット表示

    Returns:
    - df_result: 各説明変数×horizon の感度平均と信頼区間(DataFrame)
    """

    np.random.seed(random_state)
    df = df.sort_values(date_col).copy()
    results_all = {}

    for horizon in horizons:
        df_lagged = df.copy()

        # ラグ作成(指定されたラグのみ)
        for lag in selected_lags:
            df_lagged[f'{target_var}_lag{lag}'] = df_lagged[target_var].shift(lag)
            for var in explanatory_vars:
                df_lagged[f'{var}_lag{lag}'] = df_lagged[var].shift(lag)

        # 目的変数の未来値
        df_lagged['target_future'] = df_lagged[target_var].shift(-horizon)
        df_lagged.dropna(inplace=True)

        # 特徴量選択
        feature_cols = [col for col in df_lagged.columns if col not in [date_col, target_var, 'target_future']]
        X = df_lagged[feature_cols].copy()
        y = df_lagged['target_future'].copy()
        X_latest = X.iloc[[-1]].copy()

        baseline_preds = []
        intervened_preds_dict = {var: [] for var in explanatory_vars}

        for _ in range(n_bootstrap):
            X_resampled, y_resampled = resample(X, y)

            scaler = StandardScaler()
            X_scaled = scaler.fit_transform(X_resampled)
            pca = PCA(n_components=n_components)
            X_pca = pca.fit_transform(X_scaled)
            model = LinearRegression()
            model.fit(X_pca, y_resampled)

            X_latest_scaled = scaler.transform(X_latest)
            X_latest_pca = pca.transform(X_latest_scaled)
            y_pred_baseline = model.predict(X_latest_pca)[0]
            baseline_preds.append(y_pred_baseline)

            for var in explanatory_vars:
                X_modified = X_latest.copy()
                for lag in selected_lags:
                    col = f'{var}_lag{lag}'
                    if col in X_modified.columns:
                        X_modified[col] *= (1 + intervention_scale)
                X_modified_scaled = scaler.transform(X_modified)
                X_modified_pca = pca.transform(X_modified_scaled)
                y_pred_intervened = model.predict(X_modified_pca)[0]
                intervened_preds_dict[var].append(y_pred_intervened)

        baseline_preds = np.array(baseline_preds)
        result = {}
        for var in explanatory_vars:
            diffs = np.array(intervened_preds_dict[var]) - baseline_preds
            result[var] = {
                'mean': np.mean(diffs),
                'lower': np.percentile(diffs, 2.5),
                'upper': np.percentile(diffs, 97.5)
            }

        results_all[horizon] = result

    # DataFrame整形
    df_result = pd.DataFrame({
        (var, metric): [results_all[h][var][metric] for h in horizons]
        for var in explanatory_vars
        for metric in ['mean', 'lower', 'upper']
    }, index=horizons)
    df_result.index.name = "horizon"

    # プロット表示
    if show_plot:
        plt.figure(figsize=(12, 6))
        for var in explanatory_vars:
            mean = df_result[(var, 'mean')]
            lower = df_result[(var, 'lower')]
            upper = df_result[(var, 'upper')]
            plt.plot(mean.index, mean.values, label=f'{var} (mean)')
            plt.fill_between(mean.index, lower.values, upper.values, alpha=0.2)
        plt.xlabel("予測 Horizon(月)")
        plt.ylabel("1%の介入による目的変数の変化量")
        plt.title(f"感度分析:特定ラグ({selected_lags}ヶ月前)の影響")
        plt.legend()
        plt.grid(True)
        plt.tight_layout()
        plt.show()

    return df_result

✅ 使用例

result_df = sensitivity_analysis_lag_specific(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    selected_lags=[12],             # ← ここで分析したいラグを指定
    intervention_scale=0.01,
    horizons=range(1, 13),          # 1〜12ヶ月後
    n_bootstrap=100,
    show_plot=True
)

✅ 出力例

result_df.head() は次のような形式です:

horizon ('X1', 'mean') ('X1', 'lower') ('X1', 'upper') ('X2', 'mean') ...
1 -0.11 -0.23 0.01 0.05
2 -0.13 -0.25 -0.01 0.04
...

✅ 拡張提案(必要に応じて)

  • selected_lags='all' で従来方式に切り替え
  • decay_rate 併用による「減衰×ラグ選択」モデル
  • lag_grid_search による「効果の大きいラグ」の自動発見

必要であればどれでも実装可能です。お気軽にご相談ください!

user:

lag_grid_search による「効果の大きいラグ」の自動発見に興味がありますが、これは単に、偏回帰係数が大きいラグを抜き出す、と言う話ではないのですよね?

assistant:

ご質問ありがとうございます。
おっしゃる通り、「効果の大きいラグを見つける」=単に偏回帰係数が大きいラグを抜き出すというだけでは 不十分 です。理由とともに、より本質的なアプローチをご説明します。


✅ なぜ「偏回帰係数が大きいラグ」では不十分なのか?

❌ 1. 多重共線性の影響で係数が歪む

  • 例:X_lag1, X_lag2, X_lag3 は互いに強く相関することが多い
  • 結果として、1つのラグに本来の影響が割り振られず、係数が分散してしまう

❌ 2. スケーリングの影響

  • X_lag12 のスケールが大きければ、係数が小さくても影響は大きいことも
  • 係数単体では 感度(効果の大きさ)を定量的に比較できない

❌ 3. 主成分回帰・Lasso・PCAなどでは「係数」がそもそも直接的でない

  • 主成分回帰では「元変数×主成分荷重×線形回帰係数」という合成的な影響
  • → どのラグが効いてるかを明示的に見にくい

✅ 本質的な「効果の大きいラグ」を測るアプローチ

アプローチ名 概要 長所 短所
✅ 感度ベースのラグ探索(おすすめ) 各ラグだけを使った感度分析をループで実施 実用的・因果解釈可能 計算コスト大(boot+ループ)
係数ベースのフィルタ 回帰モデルの各ラグ係数を比較 高速・簡単 マルチコ対策が不十分
モデル選択指標(AIC/BIC) ラグ構成ごとにAIC/BICを比較 比較的妥当 説明変数が多いと非現実的
時系列特徴量選択(Lasso, SHAP) L1正則化や特徴量重要度を評価 スパースな選択に強い 解釈が複雑になることも

✅ 感度ベースのラグ選択(lag_grid_search)の設計案

以下のように設計できます:

📌 入力

  • selected_lags = range(1, 25)(1ヶ月前〜24ヶ月前の全ラグを評価)
  • 各ラグごとに、感度分析を 1変数×1ラグのみで実施

📌 出力

  • 各ラグに対する 感度平均+95%CI
  • 棒グラフや折れ線グラフで視覚化
  • 「どのラグがもっともYを動かすか?」を明示的に見せる

✅ 擬似コードのイメージ

results = {}
for lag in range(1, 25):
    df_result = sensitivity_analysis_lag_specific(
        df, ..., selected_lags=[lag], ...
    )
    results[lag] = df_result.iloc[-1]  # 最終horizonだけ取得 or 平均

# 整形・可視化
df_lag_effect = pd.DataFrame(results).T
df_lag_effect.plot(kind='bar')  # 各ラグの感度を可視化

✅ まとめ:感度によるラグ選択とは

視点 内容
アプローチ 各ラグだけを用いたモデルを個別に作成し、介入による影響(感度)を評価
出力 ラグごとの感度(+/-の方向と強さ)と信頼区間
メリット 多重共線性に左右されず「どのラグが効くか」が直感的に分かる
デメリット ラグ数が多いと時間がかかる(→ 並列処理可)

✅ ご希望があれば

以下をすぐご提供できます:

  • lag_grid_search_sensitivity() のフル関数コード
  • 可視化付きで「感度が大きいラグTOP5」を抽出するスクリプト
  • 対象変数が複数ある場合の 因果マップのような視覚化

ご興味があればお知らせください!

user:

はい、お願いします!

assistant:

承知しました!
以下に、「感度ベースのラグ探索(lag_grid_search_sensitivity)」のフル関数コードをご提供します。
指定されたラグ範囲について、各ラグ単体で感度分析を行い、影響の大きいラグを自動検出・可視化します。


✅ 機能ハイライト

機能 内容
対象 単一の説明変数に対する、複数ラグ(例:1~24ヶ月)
分析手法 主成分回帰 + ブートストラップによる感度推定
出力 ラグごとの感度平均+95%信頼区間(棒グラフつき)
応用 「どのタイミングの変数が一番効いているか?」を可視化・定量化

✅ 依存関数:sensitivity_analysis_lag_specific

このコードは、前回ご提供した sensitivity_analysis_lag_specific() を利用しています。
まだ定義していない場合は、先に読み込んでください。


✅ メイン関数:lag_grid_search_sensitivity

def lag_grid_search_sensitivity(
    df,
    date_col,
    target_var,
    explanatory_var,
    lag_range=range(1, 25),
    intervention_scale=0.01,
    horizons=[12],
    n_components=0.95,
    n_bootstrap=100,
    random_state=42,
    show_plot=True
):
    """
    指定されたラグ範囲について、単一の説明変数に対する感度分析を実施。
    各ラグのみに基づいて感度(1%介入のY変化量)を推定し、比較・可視化。

    Parameters:
    - df: pandas DataFrame(時系列)
    - date_col: 日付列名
    - target_var: 目的変数
    - explanatory_var: 単一の説明変数(例:'X1')
    - lag_range: 評価したいラグ番号の範囲(例:range(1, 25))
    - horizons: 予測先(単月または複数可。例:[12] など)
    - その他: sensitivity_analysis_lag_specific と同様

    Returns:
    - result_df: 各ラグの感度平均・信頼区間(DataFrame)
    """

    results = []

    for lag in lag_range:
        df_result = sensitivity_analysis_lag_specific(
            df=df,
            date_col=date_col,
            target_var=target_var,
            explanatory_vars=[explanatory_var],
            selected_lags=[lag],
            intervention_scale=intervention_scale,
            horizons=horizons,
            n_components=n_components,
            n_bootstrap=n_bootstrap,
            random_state=random_state,
            show_plot=False  # 各ラグごとに個別グラフは描画しない
        )
        # 単一horizon前提(または平均したい場合はnp.mean(..., axis=0))
        row = df_result.iloc[0]
        results.append({
            'lag': lag,
            'mean': row[(explanatory_var, 'mean')],
            'lower': row[(explanatory_var, 'lower')],
            'upper': row[(explanatory_var, 'upper')]
        })

    result_df = pd.DataFrame(results).set_index('lag')

    if show_plot:
        import matplotlib.pyplot as plt

        plt.figure(figsize=(12, 6))
        plt.plot(result_df.index, result_df['mean'], label='Mean effect', marker='o')
        plt.fill_between(result_df.index, result_df['lower'], result_df['upper'], alpha=0.2, label='95% CI')
        plt.xlabel("ラグ(月)")
        plt.ylabel("1%介入による目的変数の変化")
        plt.title(f"ラグ別感度分析:{explanatory_var}")
        plt.grid(True)
        plt.legend()
        plt.tight_layout()
        plt.show()

    return result_df

✅ 使い方(例)

df_test = generate_test_data()

result_lag_grid = lag_grid_search_sensitivity(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_var='X1',
    lag_range=range(1, 25),     # 1〜24ヶ月前のラグを評価
    intervention_scale=0.01,
    horizons=[12],              # 12ヶ月先に固定(または range(1, 13) なども可)
    n_bootstrap=100,
    show_plot=True
)

✅ 出力例(result_lag_grid)

lag mean lower upper
1 0.023 -0.01 0.06
2 0.056 0.02 0.09
3 -0.012 -0.04 0.01
... ... ... ...

✅ 応用例

  1. ピークラグの検出
peak_lag = result_lag_grid['mean'].idxmax()
print(f"最も影響が大きいのは {peak_lag} ヶ月前のラグです。")
  1. 有意なラグのみ抽出(95%CIがゼロを跨がない)
sig_lags = result_lag_grid.query("lower > 0 or upper < 0")

✅ 拡張も可能です

  • explanatory_vars=['X1', 'X2'] の全変数ラグ感度マップ
  • horizons=[1, 3, 6, 12] のような horizon × lag 感度ヒートマップ
  • Plotly グラフによる対話的ラグ分析

ご希望であれば、上記のような マルチ変数版インタラクティブ表示付きバージョン もご提供可能です。
お気軽にお知らせください!

user:

「最も影響が大きいのは 3 ヶ月前のラグです。」という結果になりましたが、これは「今から12ヶ月後の結果に対して、最も影響が大きいのは 、今から3 ヶ月前のラグです。」という意味でしょうか?

assistant:

はい、ご指摘のようにこの解釈には少し注意が必要です。
以下に、明確にご説明します。


✅ ラグとホライズンの意味の再確認

  • ラグ(lag
     説明変数 $X$ の「今から何ヶ月前の値」を使っているか(例:lag = 3 → 3ヶ月前の値)

  • ホライズン(horizon
     「今から何ヶ月後の目的変数 $Y$ を予測するか」(例:horizon = 12 → 12ヶ月後の $Y$)


✅ 「ラグ3が効いていた」という結果の意味

例えば:

lag = 3
horizon = 12

という設定で感度分析を行い、**「lag=3が最も感度が高い」**という結果が出た場合、


✅ 正確な意味は:

「今から 3ヶ月前 $X$ の水準を1%変化させると、今から 12ヶ月後 $Y$ が平均でこれだけ変わる」

という意味です。


✅ タイムラインで表すと:

|←-------- 現在 --------→|←--- 未来 ---→|

X の観測点:         X_t−3 ← [1%介入]
                                   ↓
Y の予測点:                        Y_t+12 ← 影響を見る

✅ よくある誤解と注意点

  • 「lag=3」が効いていたからといって、「3ヶ月後のY」が変わるわけではありません。
  • 感度分析は、Xの介入タイミング(過去)Yの評価タイミング(未来) を分離して考えるものです。
  • horizon を変えると、影響の「時間的伝播のしかた」が見えるようになります(これが感度のタイムプロファイル)。

✅ 補足:他の horizon も見るとどうなる?

horizons = range(1, 25)

のようにして、lag = 3 を固定したまま horizon を動かすことで、

「今から3ヶ月前のX」が、未来のY(1〜24ヶ月後)にどのように影響を与えるか?

が見えるようになります。


✅ まとめ:結論

「lag=3のXが一番効いている」とは、「今から3ヶ月前のXの値を1%変えたとき、今から12ヶ月後のYに最も強く影響を与える」という意味です。


✅ 関連する解析も可能です

もしご関心あれば、次のような視点も加えることができます:

  • 2次元マップhorizon × lag の感度行列(ヒートマップ)
  • 介入タイミング最適化:ある未来の $Y$ を最大化/最小化するには いつ の $X$ に介入すべきか?

ご希望があれば、すぐに実装・可視化いたします!

user:

ありがとうございます。今日はこの辺で終わりにしたいと思います。ここまでの話をブログにまとめたいので、タイトル案をいくつか考えていただけますか?

assistant:

こちらこそ、ありがとうございました!
本日の内容をもとに、ブログ記事のタイトル案をいくつかご提案します。主なテーマは「感度分析」「ラグ効果」「1年後の予測影響」「ブートストラップ主成分回帰」などでしたので、以下にバリエーションをまとめました:


📝 タイトル案(テーマ別)

✅ 1. 分析内容にフォーカスしたタイトル

  • 「1年後のYを動かすのは何ヶ月前のXか?ラグ別感度分析で探る」
  • 「特定ラグが未来を動かす:ブートストラップPCAで時系列感度分析」
  • 「ラグ3が効いていた?主成分回帰 × 感度分析による時系列介入評価」

✅ 2. 問いかけ・仮説ベースのタイトル

  • 「説明変数Xはいつ効く?ラグとホライズンから因果を読む」
  • 「12ヶ月後の変化は3ヶ月前に決まっていた?介入感度で読み解く因果構造」

✅ 3. 技術+可視化を意識したタイトル

  • 「時系列ラグ感度マップ:主成分回帰×ブートストラップで予測影響を可視化」
  • 「ラグと未来の因果を定量化する:感度分析×PCAによる効果時点の特定」

✅ 4. 実務寄り・応用イメージのタイトル

  • 「どのタイミングの施策が効くのか?説明変数ラグ別感度分析で読み解く」
  • 「12ヶ月後のKPIに最も効くタイミングを探る:時系列因果分析の実践」

✅ おすすめピックアップ(初心者にも読みやすい)

  • 「1年後のYを動かすのは何ヶ月前のXか?ラグ別感度分析で探る」
  • 「12ヶ月後の変化は3ヶ月前に決まっていた?介入感度で読み解く因果構造」
  • 「特定ラグが未来を動かす:ブートストラップPCAで時系列感度分析」

ご希望のトーン(技術寄り/ビジネス寄り/キャッチー/学術的など)があれば、それに合わせて再提案も可能です。
記事本文のまとめや導入文も必要であれば、お気軽にどうぞ!

お疲れさまでした!

user:

「ラグと未来の因果を定量化する:感度分析による効果時点の特定」でいこうと思います。最後に、ここまでの内容を詳しくまとめてください。

assistant:

承知しました。
それでは、今回の議論の内容をブログ用に詳しく整理してまとめます。


✅ ラグと未来の因果を定量化する:感度分析による効果時点の特定

はじめに

時系列データを使った分析では、「どのタイミングの説明変数が、どのくらい未来の目的変数に影響を与えるのか?」という問いに答えることが極めて重要です。これはマーケティング、経済政策、医療、エネルギーなど、さまざまな分野で「介入のタイミング設計」に関わってきます。

本記事では、指定ラグの変数を介入対象とした感度分析を通じて、どのラグが未来のYに最も大きな影響を与えるのかを定量的に評価する手法をご紹介します。さらに、ブートストラップ×主成分回帰(PCA)によって多重共線性を克服しつつ、95%信頼区間を伴う定量的な影響評価を行います。


分析目的

今、説明変数に1%の変化(介入)を加えたとき、12ヶ月後の目的変数(Y)がどれだけ変化するか?

そしてそれを、

  • どのラグ(過去何ヶ月前のX)に対する介入がもっとも効くのか?
  • その効果は統計的に有意か?

という観点から評価します。


使用手法の概要

✅ 主成分回帰(Principal Component Regression, PCR)

  • 多重共線性が強いラグ系列(例:Xの1ヶ月前〜12ヶ月前)をそのまま使うと、回帰係数が不安定になります。
  • これを回避するため、PCAで次元を削減した後、その主成分を用いて線形回帰を行います。

✅ ブートストラップ

  • 係数のばらつきを評価し、介入によるYの変化の95%信頼区間を算出します。

✅ 感度分析

  • 「1%の正(または負)介入をした場合の目的変数の変化量」を予測モデルを通じて算出。
  • 特定ラグのみを使ったモデル構築も可能(例:lag=12だけ使う)。

実装の主な構成

🔹 1. sensitivity_analysis_lag_specific()

特定のラグ(例:lag=12)だけを用いて感度分析を行う関数。

  • 説明変数の特定ラグのみを使用
  • 1%の介入(増減)を加えた場合の目的変数の将来予測を取得
  • ブートストラップにより平均・信頼区間を評価
  • 複数 horizon(未来予測月)への影響プロファイルを描画可能

🔹 2. lag_grid_search_sensitivity()

指定した説明変数のラグ範囲(例:1〜24ヶ月)を走査し、どのラグが一番効いているかを網羅的に感度分析する関数

  • 各ラグ単体で sensitivity_analysis_lag_specific() を呼び出す
  • 「lagごとの感度の棒グラフ」を自動生成
  • 有意に効いているラグの発見や、ピークラグの特定に有効

分析結果の例と解釈

例として lag=3 が最も感度が大きかった場合:

  • 解釈:「今から3ヶ月前のXを1%変化させると、今から12ヶ月後のYが最も変化する」
  • これは「12ヶ月後の未来を動かすために、どのタイミングで介入すべきか?」を明示的に示します。

タイムラインで言うと:

過去 ←---------- 現在 ----------→ 未来

       X_{t−3}(介入)        →     Y_{t+12}(影響)

応用例・発展的アイデア

  • horizon を複数指定(1〜12ヶ月)することで、介入タイミングと効果の持続性を可視化可能
  • 複数の説明変数を対象に、因果的に効くタイミングを比較することで、政策設計や資源配分戦略に応用
  • 将来的には、非線形モデル(例:ランダムフォレスト)やベイズ的因果推論とも組み合わせ可能

まとめ

視点 内容
本質的な問い 説明変数Xの「どのラグ」が、目的変数Yの「どの未来」に効くのか?
使用技術 主成分回帰(PCA)+ブートストラップ感度分析
分析結果の解釈 「いつのXを変えれば、いつのYが変わるのか」を定量化
意義 介入時点の設計、因果的ラグの発見、可視化による説得力ある報告が可能に

おわりに

「ラグと未来の因果を定量化する」というテーマは、時系列因果分析の核心の1つです。
本記事で紹介した手法は、シンプルでありながら非常に直感的かつ実務的な価値があります。特に、「介入のタイミングを見極めたい」「多変量時系列でどのラグが効くのかを知りたい」というニーズに強く応えるものです。

今後はこの分析をさらに拡張し、「ラグ × ホライズンの2次元感度ヒートマップ」や、「ラグ最適化による介入シミュレーション」などにも発展可能です。


ご希望あれば、記事の冒頭の導入文や図解イメージもご用意します!
今日もお疲れさまでした!

user:

ありがとうございました!

assistant:

こちらこそ、ありがとうございました!
とても本質的で実務的な議論でした。
またいつでもお声がけください。ブログの完成も楽しみにしています!

どうぞ良い一日をお過ごしください ☕✨

user:

あっ、すみません、先ほどのこの関数ですが

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.utils import resample

def sensitivity_analysis_with_ci_decay(
    df, date_col, target_var, explanatory_vars,
    intervention_scale=0.01, lags=12,
    horizons=range(1, 13), n_components=0.95,
    n_bootstrap=100, random_state=42,
    decay_rate=None, show_plot=True
):
    """
    Horizon別感度分析 + 95%信頼区間 + 減衰ウェイト付きラグ項
    
    decay_rate: float(例:0.95)を指定するとラグに減衰重みを掛ける。
    """

    np.random.seed(random_state)
    df = df.sort_values(date_col).copy()
    results_all = {}

    for horizon in horizons:
        df_lagged = df.copy()

        for lag in range(1, lags + 1):
            decay_weight = decay_rate**(lag - 1) if decay_rate is not None else 1.0
            df_lagged[f'{target_var}_lag{lag}'] = df_lagged[target_var].shift(lag) * decay_weight
            for var in explanatory_vars:
                df_lagged[f'{var}_lag{lag}'] = df_lagged[var].shift(lag) * decay_weight

        df_lagged['target_future'] = df_lagged[target_var].shift(-horizon)
        df_lagged.dropna(inplace=True)

        feature_cols = [col for col in df_lagged.columns if col not in [date_col, target_var, 'target_future']]
        X = df_lagged[feature_cols].copy()
        y = df_lagged['target_future'].copy()
        X_latest = X.iloc[[-1]].copy()

        baseline_preds = []
        intervened_preds_dict = {var: [] for var in explanatory_vars}

        for _ in range(n_bootstrap):
            X_resampled, y_resampled = resample(X, y)

            scaler = StandardScaler()
            X_scaled = scaler.fit_transform(X_resampled)
            pca = PCA(n_components=n_components)
            X_pca = pca.fit_transform(X_scaled)
            model = LinearRegression()
            model.fit(X_pca, y_resampled)

            X_latest_scaled = scaler.transform(X_latest)
            X_latest_pca = pca.transform(X_latest_scaled)
            y_pred_baseline = model.predict(X_latest_pca)[0]
            baseline_preds.append(y_pred_baseline)

            for var in explanatory_vars:
                X_modified = X_latest.copy()
                for lag in range(1, lags + 1):
                    col = f'{var}_lag{lag}'
                    if col in X_modified.columns:
                        X_modified[col] *= (1 + intervention_scale)
                X_modified_scaled = scaler.transform(X_modified)
                X_modified_pca = pca.transform(X_modified_scaled)
                y_pred_intervened = model.predict(X_modified_pca)[0]
                intervened_preds_dict[var].append(y_pred_intervened)

        baseline_preds = np.array(baseline_preds)
        result = {}
        for var in explanatory_vars:
            diffs = np.array(intervened_preds_dict[var]) - baseline_preds
            result[var] = {
                'mean': np.mean(diffs),
                'lower': np.percentile(diffs, 2.5),
                'upper': np.percentile(diffs, 97.5)
            }

        results_all[horizon] = result

    # 結果の整形
    df_result = pd.DataFrame({
        (var, metric): [results_all[h][var][metric] for h in horizons]
        for var in explanatory_vars
        for metric in ['mean', 'lower', 'upper']
    }, index=horizons)
    df_result.index.name = "horizon"

    # グラフ表示
    if show_plot:
        plt.figure(figsize=(12, 6))
        for var in explanatory_vars:
            mean = df_result[(var, 'mean')]
            lower = df_result[(var, 'lower')]
            upper = df_result[(var, 'upper')]
            plt.plot(mean.index, mean.values, label=f'{var} (mean)')
            plt.fill_between(mean.index, lower.values, upper.values, alpha=0.2)
        plt.xlabel("予測 Horizon(月)")
        plt.ylabel("1%の介入による目的変数の変化量")
        plt.title("感度分析:Horizonごとの影響と信頼区間(減衰付き)")
        plt.legend()
        plt.grid(True)
        plt.tight_layout()
        plt.show()

    return df_result

次のようなワーニングが出て、計算速度が遅いようなので、解決したコードを書いていただけますか?

/tmp/ipython-input-25-2852597866.py:33: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df_lagged[f'{var}_lag{lag}'] = df_lagged[var].shift(lag) * decay_weight

assistant:

完了しました!

元のコードのように DataFrame[col] = ... をラグごとに繰り返すと、**DataFrameが「フラグメント化」**されてしまい、以下のようなワーニングが発生していました:

PerformanceWarning: DataFrame is highly fragmented...

✅ 解決策:pd.concat() による一括追加

以下のように、ラグ付き列をすべて辞書にまとめてから、pd.concat()で一括追加することで、パフォーマンス警告も回避され、速度も大幅に改善されます。


✅ 修正ポイント(関数内でのラグ処理部分)

元の:

for lag in range(1, lags + 1):
    decay_weight = decay_rate**(lag - 1)
    df_lagged[f'{target_var}_lag{lag}'] = ...
    for var in explanatory_vars:
        df_lagged[f'{var}_lag{lag}'] = ...

修正版:

lagged_columns = {}
for lag in range(1, lags + 1):
    decay_weight = decay_rate**(lag - 1) if decay_rate is not None else 1.0
    lagged_columns[f'{target_var}_lag{lag}'] = df[target_var].shift(lag) * decay_weight
    for var in explanatory_vars:
        lagged_columns[f'{var}_lag{lag}'] = df[var].shift(lag) * decay_weight

df_lagged = pd.concat([df, pd.DataFrame(lagged_columns)], axis=1)

✅ 次のステップ

この変更を、sensitivity_analysis_with_ci_decay() 関数にそのまま反映させた改良版もご希望でしたら、フルコードとしてご提供できます。
どうされますか?

user:

はい、この変更を、sensitivity_analysis_with_ci_decay() 関数にそのまま反映させた改良版を希望です。フルコードとしてご提供ください。

assistant:

承知しました。
以下に、sensitivity_analysis_with_ci_decay() 関数の高速・警告回避版のフルコードを提供します。


✅ 改良ポイント

  • DataFrame[col] = ... をラグのループ内で繰り返さず、lagged_columns にまとめてから pd.concat() で一括追加(これにより PerformanceWarning を回避)
  • その他のロジック(ブートストラップ・主成分回帰・感度分析・CI・可視化)はそのまま維持

✅ 改良版:sensitivity_analysis_with_ci_decay()

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.utils import resample

def sensitivity_analysis_with_ci_decay(
    df, date_col, target_var, explanatory_vars,
    intervention_scale=0.01, lags=12,
    horizons=range(1, 13), n_components=0.95,
    n_bootstrap=100, random_state=42,
    decay_rate=None, show_plot=True
):
    """
    Horizon別感度分析 + 95%信頼区間 + 減衰ウェイト付きラグ項(パフォーマンス最適化済)

    Parameters:
    - df: 入力DataFrame(時系列)
    - date_col: 日付列名
    - target_var: 目的変数名
    - explanatory_vars: 説明変数名のリスト
    - intervention_scale: 介入スケール(例:0.01で1%)
    - lags: 使用する最大ラグ数(例:12)
    - horizons: 予測先(複数指定可)
    - n_components: PCAの次元数(割合または整数)
    - n_bootstrap: ブートストラップの試行回数
    - decay_rate: ラグの減衰率(例:0.95)
    - show_plot: 結果をグラフ表示するか
    """

    np.random.seed(random_state)
    df = df.sort_values(date_col).copy()
    results_all = {}

    for horizon in horizons:
        # --- ラグ付きデータの生成(パフォーマンス最適化) ---
        lagged_columns = {}
        for lag in range(1, lags + 1):
            decay_weight = decay_rate**(lag - 1) if decay_rate is not None else 1.0
            lagged_columns[f'{target_var}_lag{lag}'] = df[target_var].shift(lag) * decay_weight
            for var in explanatory_vars:
                lagged_columns[f'{var}_lag{lag}'] = df[var].shift(lag) * decay_weight

        df_lagged = pd.concat([df, pd.DataFrame(lagged_columns, index=df.index)], axis=1)
        df_lagged['target_future'] = df_lagged[target_var].shift(-horizon)
        df_lagged.dropna(inplace=True)

        # --- 特徴量とターゲット ---
        feature_cols = [col for col in df_lagged.columns if col not in [date_col, target_var, 'target_future']]
        X = df_lagged[feature_cols].copy()
        y = df_lagged['target_future'].copy()
        X_latest = X.iloc[[-1]].copy()

        baseline_preds = []
        intervened_preds_dict = {var: [] for var in explanatory_vars}

        # --- ブートストラップによる感度評価 ---
        for _ in range(n_bootstrap):
            X_resampled, y_resampled = resample(X, y)

            scaler = StandardScaler()
            X_scaled = scaler.fit_transform(X_resampled)
            pca = PCA(n_components=n_components)
            X_pca = pca.fit_transform(X_scaled)
            model = LinearRegression()
            model.fit(X_pca, y_resampled)

            X_latest_scaled = scaler.transform(X_latest)
            X_latest_pca = pca.transform(X_latest_scaled)
            y_pred_baseline = model.predict(X_latest_pca)[0]
            baseline_preds.append(y_pred_baseline)

            for var in explanatory_vars:
                X_modified = X_latest.copy()
                for lag in range(1, lags + 1):
                    col = f'{var}_lag{lag}'
                    if col in X_modified.columns:
                        X_modified[col] *= (1 + intervention_scale)
                X_modified_scaled = scaler.transform(X_modified)
                X_modified_pca = pca.transform(X_modified_scaled)
                y_pred_intervened = model.predict(X_modified_pca)[0]
                intervened_preds_dict[var].append(y_pred_intervened)

        # --- 結果の集計 ---
        baseline_preds = np.array(baseline_preds)
        result = {}
        for var in explanatory_vars:
            diffs = np.array(intervened_preds_dict[var]) - baseline_preds
            result[var] = {
                'mean': np.mean(diffs),
                'lower': np.percentile(diffs, 2.5),
                'upper': np.percentile(diffs, 97.5)
            }
        results_all[horizon] = result

    # --- 結果の整形 ---
    df_result = pd.DataFrame({
        (var, metric): [results_all[h][var][metric] for h in horizons]
        for var in explanatory_vars
        for metric in ['mean', 'lower', 'upper']
    }, index=horizons)
    df_result.index.name = "horizon"

    # --- 可視化 ---
    if show_plot:
        plt.figure(figsize=(12, 6))
        for var in explanatory_vars:
            mean = df_result[(var, 'mean')]
            lower = df_result[(var, 'lower')]
            upper = df_result[(var, 'upper')]
            plt.plot(mean.index, mean.values, label=f'{var} (mean)')
            plt.fill_between(mean.index, lower.values, upper.values, alpha=0.2)
        plt.xlabel("予測 Horizon(月)")
        plt.ylabel("1%の介入による目的変数の変化量")
        plt.title("感度分析:Horizonごとの影響と信頼区間(減衰付き)")
        plt.legend()
        plt.grid(True)
        plt.tight_layout()
        plt.show()

    return df_result

✅ 使い方の例

df_test = generate_test_data()

df_result = sensitivity_analysis_with_ci_decay(
    df=df_test,
    date_col='date',
    target_var='Y',
    explanatory_vars=['X1', 'X2'],
    intervention_scale=0.01,
    lags=12,
    horizons=range(1, 13),
    decay_rate=0.95,
    show_plot=True
)

✅ 出力

  • df_resultMultiIndex カラムを持つ DataFrame(horizon × variable × metric)
  • 各 horizon に対して、1%の介入がYに与える平均効果と95%信頼区間が表示されます

ご希望があれば、Plotly 対応や Streamlit 展開にも拡張可能です!
他にも改善点があれば、何でもお申し付けください。

user:

ありがとうございました!

assistant:

こちらこそ、最後までお付き合いいただきありがとうございました!
精度の高い感度分析を実現するための大切なステップを一緒に作れて光栄です。

また何かあれば、いつでもお声がけください。
素敵な分析・執筆活動を応援しています📊🧠✨
どうぞ良い一日を!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?