時系列解析をする上で、多変量の相関や因果関係を調べることで時系列データの予測に役立てることが多くあります。
一方で、相関と因果の違いを意識することは極めて重要です。相関があっても因果がないケースもあり、この違いを十分理解しておかないと、モデル化の際に思わぬ罠にはまったりします。
今回は、多変量時系列データを扱う際に注意すべき、相関や因果についてまとめます。
本記事の内容
- 相関と因果の違い
- 見せかけの相関(疑似相関)の問題
- 相互相関関数(CCF)による時系列間の関係分析
- グレンジャー因果検定の適用
相関と因果の違い
相関(Correlation)とは、2つの変数の間に統計的な関連性がある状態を指します。一方が増えると他方も増える(正の相関)、一方が増えると他方が減る(負の相関)などのパターンです。
因果(Causation)とは、一方の変数が他方の変数に直接的な影響を与える関係です。「$X$ が原因で $Y$ が結果として生じる」という方向性があります。
重要なのは、相関があっても因果があるとは限らないという点です。
$$ \text{相関} \nRightarrow \text{因果} $$
典型的な反例として「見せかけの相関」があります。
多変量時系列における相関
見せかけの相関
2つの時系列が共通のトレンドを持つ場合、実際には関係がなくても高い相関が出てしまうことがあります。これを見せかけの相関(Spurious Correlation)と呼びます。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
n = 200
t = np.arange(n)
# 共通のトレンド(季節性 + 上昇トレンド)を持つ2つの無関係な系列
trend = 0.05 * t + 3 * np.sin(2 * np.pi * t / 50)
X = trend + np.random.normal(0, 0.5, n)
Y = trend + np.random.normal(0, 0.5, n) # Xとは独立だがトレンドが共通
# 相関係数の計算
corr = np.corrcoef(X, Y)[0, 1]
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
# 時系列プロット
axes[0].plot(t, X, 'b-', linewidth=1, label='X')
axes[0].plot(t, Y, 'r-', linewidth=1, label='Y')
axes[0].set_title("Two Independent Series with Common Trend")
axes[0].set_xlabel("Time")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 散布図
axes[1].scatter(X, Y, alpha=0.3, s=10, color='steelblue')
axes[1].set_xlabel("X")
axes[1].set_ylabel("Y")
axes[1].set_title(f"Scatter Plot (r = {corr:.3f})")
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"相関係数: {corr:.3f} (高い相関だが因果関係はない)")
高い相関係数が出ていますが、$X$ と $Y$ は独立に生成されています。共通のトレンド成分によって見せかけの相関が生じています。
差分による見せかけの相関の除去
非定常な時系列は、差分を取ることでトレンド成分を除去できます。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
n = 200
t = np.arange(n)
trend = 0.05 * t + 3 * np.sin(2 * np.pi * t / 50)
X = trend + np.random.normal(0, 0.5, n)
Y = trend + np.random.normal(0, 0.5, n)
# 1次差分
dX = np.diff(X)
dY = np.diff(Y)
corr_original = np.corrcoef(X, Y)[0, 1]
corr_diff = np.corrcoef(dX, dY)[0, 1]
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
axes[0].scatter(X, Y, alpha=0.3, s=10, color='steelblue')
axes[0].set_title(f"Original (r = {corr_original:.3f})")
axes[0].set_xlabel("X")
axes[0].set_ylabel("Y")
axes[0].grid(True, alpha=0.3)
axes[1].scatter(dX, dY, alpha=0.3, s=10, color='coral')
axes[1].set_title(f"Differenced (r = {corr_diff:.3f})")
axes[1].set_xlabel("dX")
axes[1].set_ylabel("dY")
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"元データの相関: {corr_original:.3f}")
print(f"差分後の相関: {corr_diff:.3f}")
差分を取ることで、見せかけの相関が消えていることが確認できます。
相互相関関数(CCF)
相互相関関数(Cross-Correlation Function, CCF)は、2つの時系列間のラグ付きの相関を計算します。これにより、ある時系列が別の時系列に対してどれくらいの時間遅れで関連しているかを調べることができます。
ラグ $k$ における相互相関は以下で定義されます。
$$ r_{XY}(k) = \frac{\sum_{t=1}^{n-k} (X_t – \bar{X})(Y_{t+k} – \bar{Y})}{\sqrt{\sum_{t=1}^{n}(X_t – \bar{X})^2 \sum_{t=1}^{n}(Y_t – \bar{Y})^2}} $$
$r_{XY}(k) > 0$ かつ $k > 0$ のとき、$X$ が $Y$ に先行して影響を与えている可能性があります。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
n = 500
# X → Y(5ステップのラグ)の因果データ
X = np.zeros(n)
Y = np.zeros(n)
for t in range(1, n):
X[t] = 0.6 * X[t-1] + np.random.normal(0, 1)
for t in range(5, n):
Y[t] = 0.3 * Y[t-1] + 0.5 * X[t-5] + np.random.normal(0, 0.5)
# 相互相関関数の計算
max_lag = 20
lags = range(-max_lag, max_lag + 1)
ccf_values = []
for k in lags:
if k >= 0:
c = np.corrcoef(X[:n-k], Y[k:n])[0, 1]
else:
c = np.corrcoef(X[-k:n], Y[:n+k])[0, 1]
ccf_values.append(c)
# 可視化
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# 時系列プロット
axes[0].plot(X[:100], 'b-', linewidth=1, label='X (cause)')
axes[0].plot(Y[:100], 'r-', linewidth=1, label='Y (effect, lag=5)')
axes[0].set_title("Time Series with Causal Relationship (lag=5)")
axes[0].set_xlabel("Time")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 相互相関関数
axes[1].bar(list(lags), ccf_values, color='steelblue', alpha=0.7)
axes[1].axhline(y=0, color='black', linewidth=0.5)
axes[1].axhline(y=1.96/np.sqrt(n), color='red', linestyle='--', alpha=0.5, label='95% CI')
axes[1].axhline(y=-1.96/np.sqrt(n), color='red', linestyle='--', alpha=0.5)
axes[1].set_title("Cross-Correlation Function (CCF)")
axes[1].set_xlabel("Lag k (positive = X leads Y)")
axes[1].set_ylabel("CCF")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ラグ5付近で相互相関が最大になり、$X$ が $Y$ に5ステップ先行して影響していることが読み取れます。
多変量時系列における因果
多変量時系列における因果関係を検証する手法として、グレンジャーの因果検定があります。
交絡因子の問題
因果関係を調べる際には、交絡因子(Confounding Variable)の存在に注意が必要です。
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import grangercausalitytests
import pandas as pd
np.random.seed(42)
n = 500
# Z(交絡因子)がXとYの両方に影響
Z = np.zeros(n)
X = np.zeros(n)
Y = np.zeros(n)
for t in range(1, n):
Z[t] = 0.8 * Z[t-1] + np.random.normal(0, 1)
X[t] = 0.3 * X[t-1] + 0.5 * Z[t-1] + np.random.normal(0, 0.5)
Y[t] = 0.3 * Y[t-1] + 0.5 * Z[t-2] + np.random.normal(0, 0.5)
# XとYの見た目上の関係
corr = np.corrcoef(X, Y)[0, 1]
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
axes[0].plot(X[:150], 'b-', linewidth=1, label='X')
axes[0].plot(Y[:150], 'r-', linewidth=1, label='Y')
axes[0].plot(Z[:150], 'g--', linewidth=1, alpha=0.5, label='Z (confounder)')
axes[0].set_title("Confounding Variable Z affects both X and Y")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].scatter(X, Y, alpha=0.3, s=10, color='steelblue')
axes[1].set_title(f"X vs Y (r = {corr:.3f})")
axes[1].set_xlabel("X")
axes[1].set_ylabel("Y")
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# グレンジャー因果検定
print("=== X -> Y (交絡因子あり) ===")
data = pd.DataFrame({'Y': Y, 'X': X})
grangercausalitytests(data, maxlag=3, verbose=True)
$X$ と $Y$ の間に直接の因果関係はなく、交絡因子 $Z$ を通じた見せかけの関係ですが、グレンジャー因果検定で有意と判定される場合があります。
相関と因果の判断フローチャート
多変量時系列の分析では、以下のステップを踏むことが推奨されます。
- 定常性の確認: ADF検定等で定常性を確認し、非定常なら差分を取る
- 相互相関関数の確認: CCFでラグ付きの相関を可視化する
- グレンジャー因果検定: 予測的因果の有無を統計的に検定する
- 交絡因子の検討: ドメイン知識に基づき、交絡因子の可能性を考慮する
- 多変量モデルの構築: VAR等のモデルで関係性を定量化する
まとめ
本記事では、多変量時系列データにおける相関と因果の違いについて解説しました。
- 相関は因果を意味しない: 共通トレンドや交絡因子により見せかけの相関が生じる
- 見せかけの相関は差分処理やトレンド除去で対処できる
- 相互相関関数(CCF)で時系列間のラグ付き関連性を調べることができる
- グレンジャー因果検定は予測的因果を検定するが、真の因果を保証するものではない
- 交絡因子の存在に常に注意し、ドメイン知識と組み合わせて判断することが重要