SOI と El Niño 監視海域の SST anomaly を比較し、相関係数、回帰式、決定係数、ラグ相関を通して大気海洋相互作用を理解する。
前回までに SOI を計算し、移動平均や自己相関からその時間変動を見てきた。今回は、SOI を 大気側の指標、El Niño 監視海域の SST anomaly を 海洋側の指標 とみなし、両者の関係を調べる。
相関係数は、2つの変数がどれくらい一緒に増減するかを表す量である。値は -1 から 1 の範囲をとる。
ここで cov(X, Y) は共分散、σ は標準偏差である。共分散だけだと単位に依存するので、標準偏差で割って無次元化している。
SST anomaly を説明変数 X、SOI を目的変数 Y として、
の形の直線を求める。ここで a は傾き、b は切片である。最小二乗法では、観測値と回帰直線の差の二乗和が最小になるように a と b を決める。
決定係数 R² は、その回帰式がどれだけ当てはまっているかを表す指標である。
ここで SSR は残差平方和、TSS は全平方和である。単回帰では、相関係数の 2 乗にかなり近い意味を持つ。
| 量 | 意味 |
|---|---|
| 相関係数 r | 2つの変数の増減の向きと強さ |
| 回帰式 Y = aX + b | X から Y を近似する直線 |
| 決定係数 R² | その直線がどれだけ当てはまるか |
今回は 2 つのファイルを使う。
注意すべきことは、SOI のデータは 2025 年 3 月までであり、SST 側はそれ以降まで含む場合があることである。したがって、解析には必ず 共通期間だけ を使う。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
soi_df = pd.read_csv("SOI_timeseries_updated.txt", sep=r"\s+")
soi_df = soi_df.dropna(subset=["SOI"]).reset_index(drop=True)
soi_df["DATE"] = pd.to_datetime(dict(
year=soi_df["YEAR"].astype(int),
month=soi_df["MONTH"].astype(int),
day=1
))
sst_df = pd.read_csv("sstoi.indices", sep=______, engine="python")
print(sst_df.columns)
sst_df["DATE"] = pd.to_datetime(dict(
year=sst_df["YR"].astype(int),
month=sst_df["MON"].astype(int),
day=1
))
SST ファイルでは anomaly 列が ANOM, ANOM.1, ANOM.2, ANOM.3 のように並んでいる。これを分かりやすい列名に直してから、SOI と日付で結合する。
sst_use = sst_df[[
"DATE",
"ANOM",
"ANOM.1",
"ANOM.2",
"ANOM.3"
]].copy()
sst_use = sst_use.rename(columns={
"ANOM": "NINO1+2_ANOM",
"ANOM.1": "NINO3_ANOM",
"ANOM.2": "NINO4_ANOM",
"ANOM.3": "NINO3.4_ANOM"
})
df = pd.merge(soi_df, sst_use, on="DATE", how=______)
print(df["DATE"].min())
print(df["DATE"].max())
print(len(df))
まずは 1 つの関数にまとめて、どの海域が最も SOI と強く結びついているかを調べる。
def calc_stats(x, y):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
mask = np.isfinite(x) & np.isfinite(y)
x = x[mask]
y = y[mask]
r = np.corrcoef(x, y)[0, 1]
a, b = np.polyfit(x, y, 1)
y_pred = a * x + b
ss_res = np.sum((y - y_pred) ** 2)
ss_tot = np.sum((y - np.mean(y)) ** 2)
r2 = 1 - ss_res / ss_tot
return r, a, b, r2, x, y, y_pred
regions = ["NINO1+2_ANOM", "NINO3_ANOM", "NINO4_ANOM", "NINO3.4_ANOM"]
results = []
for region in regions:
r, a, b, r2, x_valid, y_valid, y_pred = calc_stats(df[region], df["SOI"])
results.append({
"Region": region,
"Correlation_r": r,
"Slope_a": a,
"Intercept_b": b,
"R2": r2
})
result_df = pd.DataFrame(results)
result_df["abs_r"] = result_df["Correlation_r"].abs()
result_df = result_df.sort_values("abs_r", ascending=______)
print(result_df)
最も相関の強い海域として NINO3.4 を選び、SOI との散布図を描く。回帰直線も一緒に描き、図中に相関係数と決定係数を入れる。
target = "NINO3.4_ANOM"
r, a, b, r2, x_valid, y_valid, y_pred = calc_stats(df[target], df["SOI"])
plt.figure(figsize=(6, 6))
plt.scatter(x_valid, y_valid, alpha=0.5)
xx = np.linspace(np.min(x_valid), np.max(x_valid), 100)
yy = a * xx + b
plt.plot(xx, yy, linewidth=2)
plt.xlabel("NINO3.4 SST anomaly")
plt.ylabel("SOI")
plt.title("NINO3.4 anomaly vs SOI")
plt.grid(True, alpha=0.3)
plt.text(
0.05, 0.95,
f"r = {r:.2f}\nR² = {r2:.2f}\nSOI = {a:.2f} × SST + {b:.2f}",
transform=plt.gca().transAxes,
verticalalignment="top"
)
plt.tight_layout()
plt.show()
実際の結果を以下に示す。
図:NINO3.4 海面水温偏差と SOI の散布図および回帰直線
今度は、SST と SOI を時間的にずらしながら相関を求める。これがラグ相関である。ここでは lag < 0 なら SOI が先行、lag > 0 なら SST が先行と読む。
def cross_correlation(x, y, max_lag):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
lags = np.arange(-max_lag, max_lag + 1)
cc = np.full(len(lags), np.nan)
for i, lag in enumerate(lags):
if lag < 0:
x1 = x[-lag:]
y1 = y[:len(y) + lag]
elif lag > 0:
x1 = x[:len(x) - lag]
y1 = y[lag:]
else:
x1 = x
y1 = y
mask = np.isfinite(x1) & np.isfinite(y1)
if np.sum(mask) > 2:
cc[i] = np.corrcoef(x1[mask], y1[mask])[0, 1]
return lags, cc
x_raw = df["NINO3.4_ANOM"].to_numpy()
y_raw = df["SOI"].to_numpy()
mask_raw = np.isfinite(x_raw) & np.isfinite(y_raw)
x_raw = x_raw[mask_raw]
y_raw = y_raw[mask_raw]
lags_raw, cc_raw = cross_correlation(x_raw, y_raw, max_lag=24)
imin_raw = np.nanargmin(cc_raw)
lag_min_raw = lags_raw[imin_raw]
corr_min_raw = cc_raw[imin_raw]
生データのままだと月ごとのゆらぎが大きいので、13か月移動平均をかけた後でもラグ相関を求める。ここでは 121か月移動平均は使わず、短周期のノイズを弱めるための 13か月平滑化だけを使う。
def moving_average(x, window):
return pd.Series(x).rolling(window=window, center=True).mean().to_numpy()
x_smooth = moving_average(df["NINO3.4_ANOM"].to_numpy(), 13)
y_smooth = moving_average(df["SOI"].to_numpy(), ____)
mask_smooth = np.isfinite(x_smooth) & np.isfinite(y_smooth)
x_smooth = x_smooth[mask_smooth]
y_smooth = y_smooth[mask_smooth]
lags_smooth, cc_smooth = cross_correlation(x_smooth, y_smooth, max_lag=24)
imin_smooth = np.nanargmin(cc_smooth)
lag_min_smooth = lags_smooth[imin_smooth]
corr_min_smooth = cc_smooth[imin_smooth]
最後に、フィルタなしと 13か月移動平均後のラグ相関を 1 枚に重ねて描く。
plt.figure(figsize=(8, 6))
plt.plot(lags_raw, cc_raw, linestyle="--", color="tab:blue", label="Raw")
plt.plot(lags_smooth, cc_smooth, color="tab:orange", linewidth=2, label="13-month smoothed")
plt.axhline(0, color="k", linewidth=0.8)
plt.axvline(0, color="k", linewidth=0.8)
plt.scatter(lag_min_raw, corr_min_raw, color="tab:blue", s=70, zorder=3)
plt.scatter(lag_min_smooth, corr_min_smooth, color="tab:orange", s=70, zorder=3)
plt.annotate(
f"raw\nlag={lag_min_raw}\nr={corr_min_raw:.2f}",
xy=(lag_min_raw, corr_min_raw),
xytext=(lag_min_raw - 8, corr_min_raw - 0.12),
arrowprops=dict(arrowstyle="->", color="tab:blue"),
color="tab:blue",
fontsize=12
)
plt.annotate(
f"smoothed\nlag={lag_min_smooth}\nr={corr_min_smooth:.2f}",
xy=(lag_min_smooth, corr_min_smooth),
xytext=(10, corr_min_smooth + 0.40),
arrowprops=dict(arrowstyle="->", color="tab:orange"),
color="tab:orange",
fontsize=12
)
plt.xlabel("Lag (month)")
plt.ylabel("Correlation")
plt.title("Cross-correlation (Raw vs Smoothed)")
plt.legend(loc="lower left")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
計算結果を図で確認する。
図:フィルタ前後のラグ相関比較。平滑化により関係が明瞭になる
まずは上の穴埋めコードを自分で考えること。確認したい場合だけ、パスワードを入力して解答と完全スクリプトを表示する。
sep=r"\s+"
how="inner"
ascending=False
13
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
soi_df = pd.read_csv("SOI_timeseries_updated.txt", sep=r"\s+")
soi_df = soi_df.dropna(subset=["SOI"]).reset_index(drop=True)
soi_df["DATE"] = pd.to_datetime(dict(
year=soi_df["YEAR"].astype(int),
month=soi_df["MONTH"].astype(int),
day=1
))
sst_df = pd.read_csv("sstoi.indices", sep=r"\s+", engine="python")
sst_df["DATE"] = pd.to_datetime(dict(
year=sst_df["YR"].astype(int),
month=sst_df["MON"].astype(int),
day=1
))
sst_use = sst_df[["DATE", "ANOM", "ANOM.1", "ANOM.2", "ANOM.3"]].copy()
sst_use = sst_use.rename(columns={
"ANOM": "NINO1+2_ANOM",
"ANOM.1": "NINO3_ANOM",
"ANOM.2": "NINO4_ANOM",
"ANOM.3": "NINO3.4_ANOM"
})
df = pd.merge(soi_df, sst_use, on="DATE", how="inner")
def calc_stats(x, y):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
mask = np.isfinite(x) & np.isfinite(y)
x = x[mask]
y = y[mask]
r = np.corrcoef(x, y)[0, 1]
a, b = np.polyfit(x, y, 1)
y_pred = a * x + b
ss_res = np.sum((y - y_pred) ** 2)
ss_tot = np.sum((y - np.mean(y)) ** 2)
r2 = 1 - ss_res / ss_tot
return r, a, b, r2, x, y, y_pred
regions = ["NINO1+2_ANOM", "NINO3_ANOM", "NINO4_ANOM", "NINO3.4_ANOM"]
results = []
for region in regions:
r, a, b, r2, x_valid, y_valid, y_pred = calc_stats(df[region], df["SOI"])
results.append({
"Region": region,
"Correlation_r": r,
"Slope_a": a,
"Intercept_b": b,
"R2": r2
})
result_df = pd.DataFrame(results)
result_df["abs_r"] = result_df["Correlation_r"].abs()
result_df = result_df.sort_values("abs_r", ascending=False).reset_index(drop=True)
print(result_df)
target = "NINO3.4_ANOM"
r, a, b, r2, x_valid, y_valid, y_pred = calc_stats(df[target], df["SOI"])
plt.figure(figsize=(6, 6))
plt.scatter(x_valid, y_valid, alpha=0.5)
xx = np.linspace(np.min(x_valid), np.max(x_valid), 100)
yy = a * xx + b
plt.plot(xx, yy, linewidth=2)
plt.xlabel("NINO3.4 SST anomaly")
plt.ylabel("SOI")
plt.title("NINO3.4 anomaly vs SOI")
plt.grid(True, alpha=0.3)
plt.text(0.05, 0.95,
f"r = {r:.2f}\nR² = {r2:.2f}\nSOI = {a:.2f} × SST + {b:.2f}",
transform=plt.gca().transAxes,
verticalalignment="top")
plt.tight_layout()
plt.show()
def moving_average(x, window):
return pd.Series(x).rolling(window=window, center=True).mean().to_numpy()
def cross_correlation(x, y, max_lag):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
lags = np.arange(-max_lag, max_lag + 1)
cc = np.full(len(lags), np.nan)
for i, lag in enumerate(lags):
if lag < 0:
x1 = x[-lag:]
y1 = y[:len(y) + lag]
elif lag > 0:
x1 = x[:len(x) - lag]
y1 = y[lag:]
else:
x1 = x
y1 = y
mask = np.isfinite(x1) & np.isfinite(y1)
if np.sum(mask) > 2:
cc[i] = np.corrcoef(x1[mask], y1[mask])[0, 1]
return lags, cc
x_raw = df["NINO3.4_ANOM"].to_numpy()
y_raw = df["SOI"].to_numpy()
mask_raw = np.isfinite(x_raw) & np.isfinite(y_raw)
x_raw = x_raw[mask_raw]
y_raw = y_raw[mask_raw]
lags_raw, cc_raw = cross_correlation(x_raw, y_raw, max_lag=24)
imin_raw = np.nanargmin(cc_raw)
lag_min_raw = lags_raw[imin_raw]
corr_min_raw = cc_raw[imin_raw]
x_smooth = moving_average(df["NINO3.4_ANOM"].to_numpy(), 13)
y_smooth = moving_average(df["SOI"].to_numpy(), 13)
mask_smooth = np.isfinite(x_smooth) & np.isfinite(y_smooth)
x_smooth = x_smooth[mask_smooth]
y_smooth = y_smooth[mask_smooth]
lags_smooth, cc_smooth = cross_correlation(x_smooth, y_smooth, max_lag=24)
imin_smooth = np.nanargmin(cc_smooth)
lag_min_smooth = lags_smooth[imin_smooth]
corr_min_smooth = cc_smooth[imin_smooth]
plt.figure(figsize=(8, 6))
plt.plot(lags_raw, cc_raw, linestyle="--", color="tab:blue", label="Raw")
plt.plot(lags_smooth, cc_smooth, color="tab:orange", linewidth=2, label="13-month smoothed")
plt.axhline(0, color="k", linewidth=0.8)
plt.axvline(0, color="k", linewidth=0.8)
plt.scatter(lag_min_raw, corr_min_raw, color="tab:blue", s=70, zorder=3)
plt.scatter(lag_min_smooth, corr_min_smooth, color="tab:orange", s=70, zorder=3)
plt.annotate(f"raw\nlag={lag_min_raw}\nr={corr_min_raw:.2f}",
xy=(lag_min_raw, corr_min_raw),
xytext=(lag_min_raw - 8, corr_min_raw - 0.12),
arrowprops=dict(arrowstyle="->", color="tab:blue"),
color="tab:blue", fontsize=12)
plt.annotate(f"smoothed\nlag={lag_min_smooth}\nr={corr_min_smooth:.2f}",
xy=(lag_min_smooth, corr_min_smooth),
xytext=(10, corr_min_smooth + 0.40),
arrowprops=dict(arrowstyle="->", color="tab:orange"),
color="tab:orange", fontsize=12)
plt.xlabel("Lag (month)")
plt.ylabel("Correlation")
plt.title("Cross-correlation (Raw vs Smoothed)")
plt.legend(loc="lower left")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()