02 南方振動指数の計算

Python を用いて、タヒチとダーウィンの海面気圧データから Southern Oscillation Index(SOI)を計算する。

講義名
データサイエンス
使用言語
Python
到達目標
SOI の計算過程を段階的に理解し、時系列として描画できるようになる

1. 背景:ENSO と南方振動指数

エルニーニョとラニーニャは、海洋と大気が結びついた変動であり、まとめて ENSO(El Niño-Southern Oscillation)と呼ばれる。エルニーニョ時には赤道太平洋東部で暖水が広がり、大気循環も平年と異なる。

その大気側の変動を表す代表的な指標のひとつが 南方振動指数(SOI) である。SOI は、タヒチダーウィンの海面気圧の変動を用いて計算される。

El Niño と La Niña の概念図
エルニーニョ時(上)は赤道太平洋東部で暖水が広がり、ラニーニャ時(下)は逆に冷水が強まる。これに対応して大気循環も変化する。
参考
ENSO の概説:climate.gov/enso
SOI の計算方法:NOAA/CPC Readme.index

2. 使用データ

本演習では、タヒチおよびダーウィンの月平均海面気圧データを用いる。ファイルはこのページと同じディレクトリに置いてある。

2025年4月以降は -99.9 で欠損が入っている。Python ではこれを NaN として読ませる。

3. 計算の流れ

  1. タヒチ・ダーウィンの海面気圧データを読み込む
  2. 1981–2010 年を基準期間として climatology(各月平均)を求める
  3. anomaly(平均からのずれ)を計算する
  4. 標準偏差を用いて標準化する
  5. MSD(標準化した差のばらつき)を求める
  6. SOI を計算する
  7. 時系列として描画する
NOAA/CPC に従い、平均の基準期間は 1981–2010 とする。

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

SOI の計算では、計算そのものより前に、データの形を整える操作がたくさん出てくる。ここを飛ばすと、コードを読んでいて急に難しく感じる。

書き方意味
pd.read_csv(...)表形式データを読む。今回は空白区切りテキストを表として読む。
set_index("YEAR")YEAR 列を行ラベルにする。年でデータを選びやすくなる。
.loc[1981:2010, months]行と列をラベルで指定して切り出す。前は年、後ろは月名。
mean(axis=0)列ごとに平均する。各月の平均を求めるときに使う。
to_numpy()pandas の表を NumPy 配列に変える。
ravel()2次元配列を1次元に並べ直す。
np.isnan(...)欠損値 NaN かどうかを調べる。
enumerate(...)中身と番号を同時に取り出す。
pd.DataFrame(...)辞書や配列から表を作る。
大事な見方
Python の長いコードは、「計算式」と「データの形を整える部分」に分けて読むとわかりやすい。
STEP 1

5. データの読み込み

まずは Python でテキストファイルを読み込む。ここでは pandas を用いる。

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

dar = pd.read_csv("Darwin.txt", sep=______, engine="python", na_values=[-99.9])
tah = pd.read_csv("Tahiti.txt", sep=______, engine="python", na_values=[-99.9])

print(dar.head())
print(tah.head())
print(dar.columns)
print(dar.shape)
print(dar.tail())

ポイント

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

  • pd.read_csv:表データを読む関数
  • sep:何で区切られているかを指定する
  • engine="python":少し柔軟な読み込み方法を使う指定
  • na_values=[-99.9]:-99.9 を欠損値として扱う指定
  • head():先頭数行を見る
  • columns:列名一覧を見る
  • shape:行数と列数を見る
  • tail():末尾数行を見る
sep意味
","カンマ区切り
"\t"タブ区切り
" "スペース1個
r"\s+"空白が1個以上
確認
  • 列名はどのようになっているか
  • 何年分のデータが入っているか
  • 2025年4月以降の -99.9 はどのように読まれているか
STEP 2

6. climatology の計算

基準期間 1981–2010 年の各月平均を求める。これが climatology である。

months = ["JAN","FEB","MAR","APR","MAY","JUN",
          "JUL","AUG","SEP","OCT","NOV","DEC"]

dar = dar.set_index("YEAR")
tah = tah.set_index("YEAR")

dar_base = dar.loc[______:______, months]
tah_base = tah.loc[______:______, months]

mslpd = dar_base.mean(axis=______)
mslpt = tah_base.mean(axis=______)

print(mslpd)
print(mslpt)
print(len(mslpd))
print(mslpd["JAN"], mslpd["AUG"])

ポイント

確認
  • mslpd には何個の値が入るか
  • mslpd["JAN"] は何を意味するか
  • axis=1 にすると何が起きるか
STEP 3

7. anomaly の計算

観測値から climatology を引いて anomaly(平均からのずれ)を求める。

aDar = dar[months] - ______
aTah = tah[months] - ______

print(aDar.head())
print(aTah.head())
print(aDar.loc[1981:2010].mean(axis=0))
print(aTah.loc[1981:2010].mean(axis=0))

ポイント

STEP 4

8. 標準化の計算

anomaly を標準偏差で割り、スケールをそろえる。

aDar_base = aDar.loc[1981:2010, months].to_numpy().ravel()
aTah_base = aTah.loc[1981:2010, months].to_numpy().ravel()

aDar_base = aDar_base[~np.isnan(aDar_base)]
aTah_base = aTah_base[~np.isnan(aTah_base)]

sDar = np.sqrt(np.sum(aDar_base**2) / ______)
sTah = np.sqrt(np.sum(aTah_base**2) / ______)

nDar = aDar / ______
nTah = aTah / ______

print(sDar, sTah)
print(nDar.head())
print(np.nanmean(nDar.loc[1981:2010, months].to_numpy()))
print(np.nanmean(nTah.loc[1981:2010, months].to_numpy()))

ポイント

確認
  • aDar.loc[1981:2010, months] の shape はどうなるか
  • ravel() 後の shape はどうなるか
  • 標準化後の値は単位を持つか
STEP 5

9. MSD の計算

標準化したタヒチとダーウィンの差のばらつきを MSD として求める。

diff_base = (nTah.loc[1981:2010, months]
             - nDar.loc[1981:2010, months]).to_numpy().ravel()

diff_base = diff_base[~np.isnan(diff_base)]

MSD = np.sqrt(np.sum(diff_base**2) / diff_base.size)
print(MSD)

ポイント

STEP 6

10. SOI の計算

SOI = (______ - ______) / ______
print(SOI.head())

SOI は、標準化したタヒチとダーウィンの差を MSD で割って求める。

確認
  • 1951年1月の SOI の値はいくつか
  • SOI の符号は NOAA の図と一致しているか
STEP 7

11. 描画とデータ保存

records = []
for year in SOI.index:
    for imonth, month in enumerate(months, start=1):
        records.append({
            "YEAR": year,
            "MONTH": imonth,
            "SOI": SOI.loc[year, month]
        })

soi_ts = pd.DataFrame(records)
soi_ts["TIME"] = soi_ts["YEAR"] + (soi_ts["MONTH"] - 1) / 12.0
soi_ts = soi_ts.dropna(subset=["SOI"])

plt.figure(figsize=(12,4))
plt.plot(soi_ts["TIME"], soi_ts["SOI"])
plt.axhline(0, color="k")
plt.xlabel("Year")
plt.ylabel("SOI")
plt.title("Southern Oscillation Index")
plt.show()

# 月別テーブル型
SOI_out = SOI.copy()
SOI_out.insert(0, "YEAR", SOI_out.index)
SOI_out.to_csv("SOI_monthly_table_updated.txt",
               sep="\t", index=False, float_format="%.4f")

# 1次元時系列型
soi_ts.to_csv("SOI_timeseries_updated.txt",
              sep="\t", index=False, float_format="%.4f")

ポイント

確認
  • 強い正・負の期間はいつか
  • NOAA の SOI 図と大きく違わないか

12. 解答ページ

完全スクリプトは別ページにしてある。まず自分で考えてから必要なときだけ参照すること。

▶ 解答ページへ

13. 提出方法(重要)

本課題は、Jupyter Lab で作成した Notebook を HTML として保存し、Teams に提出すること。

提出手順

  1. Jupyter Lab 上で Notebook を開く
  2. 上部メニューから File → Save and Export Notebook As → HTML を選択する
  3. HTML ファイルを保存する
  4. 保存した HTML ファイルを Teams に提出する
Jupyter LabでHTML保存する方法
Jupyter Lab のメニューから HTML として保存する。
.ipynb のままでは提出しないこと。 必ず HTML に変換して提出すること。