03 SOIの移動平均と自己相関

前半では移動平均を用いたフィルタ処理を行い、後半では自己相関係数から SOI の周期性を観察する。

前半
121か月移動平均と 13か月移動平均
後半
自己相関係数による周期性の観察
入力データ
SOI_timeseries_updated.txt

1. この回でやること

オリジナルの SOI には、短周期・長周期の変動が混在している。ここでは、簡単なフィルタ処理として移動平均を用い、比較的見やすい周期成分を取り出す。

  1. 121か月移動平均を計算する
  2. 元の SOI から 121か月移動平均を引き、10年以上の変動を除く
  3. さらに 13か月移動平均をかけ、1年以内の変動を弱める
  4. その時系列に対して自己相関係数を求める
ここで使う移動平均は、考え方を学ぶための簡単な方法である。実際の研究では、フーリエ変換やデジタルフィルタを使うことも多い。

2. Pythonでよく出る機能の意味

この回では、計算式そのものよりも、配列を作る・欠損を除く・条件で切り出す・関数を定義する といった操作が多く出てくる。ここが読めると、自己相関のコードもかなり理解しやすくなる。

書き方意味
pd.read_csv(...)表形式データを読み込む
dropna(subset=[...])指定列に欠損がある行を落とす
reset_index(drop=True)行番号を振り直す
df[(条件1) & (条件2)]条件に合う行だけを取り出す
to_numpy()pandas の列や表を NumPy 配列に変える
def 関数名(...):自分で関数を定義する
pd.Series(x).rolling(...).mean()移動平均を計算する
np.isnan(x)欠損値 NaN かどうかを調べる
~np.isnan(x)NaN ではないデータだけを選ぶためのマスクを作る
np.arange(a, b)等間隔の整数列を作る
np.full(n, np.nan)長さ n の NaN 配列を作る
np.asarray(x)x を NumPy 配列として扱う
enumerate(...)番号と中身を同時に取り出す
大事な見方
長いコードは、「何を計算したいか」と「そのためにデータをどんな形にしているか」に分けて読むと理解しやすい。
STEP 1

3. 準備とデータ読み込み

前回作成した SOI_timeseries_updated.txt を読み込む。自己相関は教科書に合わせて 1951–1997 年に制限してよい。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# データ読み込み
df = pd.read_csv("SOI_timeseries_updated.txt", sep=r"\s+")
df = df.dropna(subset=["SOI"]).reset_index(drop=True)

# 教科書に合わせるなら期間を制限
df = df[(df["YEAR"] >= 1951) & (df["YEAR"] <= 1997)].reset_index(drop=True)

time = df["TIME"].to_numpy()
soi = df["SOI"].to_numpy()

print(df.head())
print(len(soi))

このブロックで新しく出るもの

  • pd.read_csv:テキストファイルを表として読む
  • sep=r"\s+":空白が1個以上あるところで区切る
  • dropna(subset=["SOI"]):SOI が欠損の行を落とす
  • reset_index(drop=True):削除後に行番号を 0,1,2,... と振り直す
  • df[(条件)]:条件に合う行だけ残す
  • to_numpy():列を NumPy 配列に変える

コードの意味

確認
  • TIME は何を表しているか
  • 1951–1997 の期間にすると、データ数はいくつになるか
  • len(soi) は何を数えているか
SOI bar plot
STEP 2

4. 121か月移動平均

まず、121か月(約10年)移動平均を計算する。中央配置の移動平均を使う。

def moving_average(x, window):
    return pd.Series(x).rolling(window=window, center=True).mean().to_numpy()

soi_ma121 = moving_average(soi, ____)

このブロックで新しく出るもの

  • def moving_average(...):自分で関数を作る
  • pd.Series(x):1次元データを pandas の Series にする
  • rolling(window=...):移動窓を作る
  • center=True:窓の中心に値を配置する
  • mean():その窓の平均をとる
  • to_numpy():最後に NumPy 配列へ戻す

コードの意味

考えてみよう
  • 121 という数字は何を意味するか
  • center=True を付ける理由は何か
  • なぜ端の数か月分では移動平均が計算できないのか
plt.figure(figsize=(12,4))
plt.plot(time, soi, label="Original SOI", alpha=0.5)
plt.plot(time, soi_ma121, label="121-month moving average", linewidth=2)
plt.axhline(0, color="k", linewidth=0.8)
plt.xlabel("Year")
plt.ylabel("SOI")
plt.title("SOI and 121-month moving average")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

描画でよく出るもの

  • plt.figure(figsize=(12,4)):図の大きさを決める
  • plt.plot(x, y):折れ線グラフを描く
  • label=...:凡例に表示する名前
  • alpha=0.5:少し透明にする
  • linewidth=2:線を太くする
  • plt.axhline(0):y=0 の横線を引く
  • plt.legend():凡例を表示する
  • plt.grid(True):グリッドを表示する
  • plt.tight_layout():文字や図がはみ出さないように整える
SOI and 121-month moving average
STEP 3

5. 長周期成分の除去

次に、元の SOI から 121か月移動平均を引く。これにより、10年以上の長周期変動を取り除く。

soi_hp = soi - soi_ma121
mask_hp = ~np.isnan(soi_hp)

time_hp = time[mask_hp]
soi_hp_valid = soi_hp[mask_hp]

このブロックで新しく出るもの

  • np.isnan(x):x が NaN かどうかを判定する
  • ~:True/False を反転する
  • x[mask]:mask が True の場所だけ取り出す

コードの意味

考えてみよう
  • ~np.isnan(...) は何をしているか
  • なぜ移動平均を引いた直後のデータをそのまま使えないのか
  • time にも同じ mask をかける理由は何か
plt.figure(figsize=(12,4))
plt.plot(time_hp, soi_hp_valid, linewidth=1.2)
plt.axhline(0, color="k", linewidth=0.8)
plt.xlabel("Year")
plt.ylabel("SOI")
plt.title("SOI after removing >10-year component")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
SOI after removing 10-year component
STEP 4

6. 13か月移動平均

さらに 13か月移動平均を行い、1年以内の短周期変動を弱める。こうして、年々変動を中心に見やすい時系列を作る。

soi_filt = moving_average(soi_hp_valid, ____)
mask_filt = ~np.isnan(soi_filt)

time_filt = time_hp[mask_filt]
soi_filt_valid = soi_filt[mask_filt]

コードの意味

考えてみよう
  • 13 という数字は何を落としたい処理か
  • 121か月移動平均のあとに、なぜさらに13か月移動平均を行うのか
  • soi_hp_validsoi_filt_valid はどう違うか
plt.figure(figsize=(12,4))
plt.plot(time_filt, soi_filt_valid, linewidth=2)
plt.axhline(0, color="k", linewidth=0.8)
plt.xlabel("Year")
plt.ylabel("SOI")
plt.title("Filtered SOI")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Filtered SOI
121か月移動平均で長周期を落とし、13か月移動平均で短周期を弱めるので、結果として数年スケールの変動が目立ちやすくなる。
STEP 5

7. 自己相関係数

自己相関とは、同じ時系列を前後にずらして比較したときの相関である。ここでは教科書に合わせて、内積型の定義で自己相関係数を求める。

def autocorrelation_fortran_style(x, max_lag):
    x = np.asarray(x)
    lags = np.arange(-max_lag, max_lag + 1)
    acf = np.full(len(lags), np.nan)
    n = len(x)

    for i, lag in enumerate(lags):
        if lag < 0:
            x1 = x[-lag:]
            x2 = x[:n + lag]
        elif lag > 0:
            x1 = x[:n - lag]
            x2 = x[lag:]
        else:
            x1 = x
            x2 = x

        num = np.sum(__________)
        den = np.sqrt(np.sum(x1**2) * np.sum(x2**2))
        if den > 0:
            acf[i] = num / den

    return lags, acf

lags, acf = autocorrelation_fortran_style(soi_filt_valid, ____)

このブロックで新しく出るもの

  • np.asarray(x):x を NumPy 配列として扱う
  • np.arange(-max_lag, max_lag + 1):lag の一覧を作る
  • np.full(len(lags), np.nan):結果を入れる NaN 配列を作る
  • for i, lag in enumerate(lags):番号 i と lag の値を同時に回す
  • x[a:b]:配列の一部を切り出す
  • np.sum(...):合計を計算する
  • np.sqrt(...):平方根をとる

コードの意味

特に大事なところ

考えてみよう
  • lag = 0 のとき、なぜ自己相関は 1 になるのか
  • なぜ左右ほぼ対称な形になるのか
  • num = np.sum(x1 * x2) は何を意味しているか
plt.figure(figsize=(10,5))
plt.plot(lags, acf, color="k", linewidth=1.5)
plt.axhline(0, color="k", linestyle="--", linewidth=0.8)
plt.axvline(0, color="k", linewidth=0.8)
plt.xlim(-120, 120)
plt.ylim(-1, 1)
plt.xlabel("Month")
plt.ylabel("Auto-Correlation")
plt.title("Autocorrelation of filtered SOI")
plt.tight_layout()
plt.show()

この描画で新しく出るもの

  • plt.axvline(0):x=0 の縦線を引く
  • plt.xlim(-120, 120):横軸の範囲を決める
  • plt.ylim(-1, 1):縦軸の範囲を決める
  • linestyle="--":破線にする
Autocorrelation of filtered SOI

8. 結果の見方

自己相関図では、数年おきに山や谷が現れるかを見る。SOI は完全な正弦波ではないので、3年・4年・5年で必ず明確な正のピークが出るとは限らない。

見るポイント
121か月移動平均10年以上のゆっくりした変動を表しているか
差し引き後の SOI長周期成分が弱まっているか
13か月移動平均後数年スケールの変動が見やすくなっているか
自己相関約 48 か月前後に山があるか、左右対称か
ここでは「きれいな単一周期」を探すというより、SOI が 2〜5 年程度の時間スケールで変動しているかを確認することが大事である。

8. 解答の表示

まずは上の穴埋めコードを自分で考えてみること。確認したい場合だけ、パスワードを入力して解答と完全スクリプトを表示する。