ERSSTv5 の月平均海面水温データを用いて、各グリッドで時間方向の線形回帰を行い、全球のトレンドマップと p 値による有意性を調べる。
全球平均の海面水温は長期的には上昇傾向を示すが、海のどこでも同じ速さで上がっているわけではない。ある海域では強く上昇し、別の海域では変化が小さかったり、期間によっては低下して見えることもある。
そこで今回は、全球海面水温データを格子点ごとに時間方向へ線形回帰し、海面水温のトレンドを地図として可視化する。また、求めた傾きが統計的に 0 と区別できるかどうかを p 値で判定する。
| 項目 | 内容 |
|---|---|
| Full name | Global Ocean Sea Surface Temperature trend map from Observations Reprocessing |
| Product ID | GLOBAL_OMI_TEMPSAL_sst_trend |
| Source | Satellite observations |
| Spatial extent | Global Ocean, Latitude -89.97° to 89.98°, Longitude -179.98° to 179.98° |
| Spatial resolution | 0.05° × 0.05° |
| Temporal extent | Since 1 Jan 1982 |
| Variables | SST trends |
| Indicator family | Sea water temperature |
| Feature type | Grid |
| Format | NetCDF-4 |
| Originating centre | Met Office (UK) |
| Last metadata update | 25 November 2025 |
本演習では NOAA Extended Reconstructed SST version 5(ERSSTv5)の月平均海面水温データを使う。ファイルはこのページと同じディレクトリに置いてあるものを使う。
| 項目 | 内容 |
|---|---|
| Temporal Coverage | Monthly values for 1854/01 - 現在 |
| Long-term mean | 1981–2010 |
| Spatial Coverage | 全球 2.0° 緯度 × 2.0° 経度格子(89 × 180) |
| Latitude / Longitude | 88.0N - 88.0S, 0.0E - 358.0E |
| Level | Sea Level |
| Update Schedule | Monthly |
| Format | CF metadata standard, NetCDF4 |
| Missing data | -9.96921e+36 |
| 書き方 | 意味 |
|---|---|
| xr.open_dataset(...) | NetCDF などの多次元データを読む。 |
| ds["sst"] | 変数 sst を取り出す。 |
| pd.to_datetime(...) | 時間情報を日時型に変換する。 |
| np.isfinite(...) | 有限値かどうかを調べる。NaN を除くときに使う。 |
| np.polyfit(x, y, 1) | 1次式 y = ax + b の傾きと切片を求める。 |
| np.full(shape, np.nan) | 最初から NaN を入れた配列を作る。 |
| stats.t.cdf(...) | t 分布の累積分布関数。p 値の計算に使う。 |
| ax.pcolormesh(...) | 格子状データを色で塗って描く。 |
| ax.scatter(...) | 点を描く。有意な格子点の表示に使う。 |
import numpy as np
import xarray as xr
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import cartopy.crs as ccrs
import cartopy.feature as cfeature
ncfile = "sst.mnmean.nc"
ds = xr.open_dataset(______)
sst = ds["sst"].values.astype(float)
lat = ds["lat"].values
lon = ds["lon"].values
time = pd.to_datetime(ds["time"].values)
sst[sst < -100] = np.nan
start_year = ______
end_year = ______
mask_period = (time.year >= start_year) & (time.year <= end_year)
sst_sub = sst[mask_period, :, :]
time_sub = time[mask_period]
回帰では時間を数値として扱いたいので、年月日ではなく「年の小数」に変換する。
years = time_sub.year + (time_sub.month - ______) / 12.0
years = years.to_numpy(dtype=float)
print(years[:5])
print(years[-5:])
ここでは SOI のときと同じように np.polyfit(x, y, 1) を使い、傾き a をトレンドとみなす。
def calc_trend_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]
n = len(x)
if n < 3:
return np.nan, np.nan, np.nan, np.nan, np.nan
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 = np.nan if ss_tot == 0 else 1 - ss_res / ss_tot
r = np.corrcoef(x, y)[0, 1]
dof = n - 2
sxx = np.sum((x - np.mean(x)) ** 2)
mse = ss_res / dof
se_a = np.sqrt(mse / sxx)
tval = a / se_a
pval = 2 * (1 - stats.t.cdf(np.abs(tval), df=dof))
return r, a, b, r2, pval
nlat = len(lat)
nlon = len(lon)
trend = np.full((nlat, nlon), np.nan)
pval = np.full((nlat, nlon), np.nan)
rmap = np.full((nlat, nlon), np.nan)
r2map = np.full((nlat, nlon), np.nan)
min_count = ______
for i in range(nlat):
for j in range(nlon):
y = sst_sub[:, i, j]
x = years
mask = np.isfinite(y)
if np.sum(mask) < min_count:
continue
r, a, b, r2, p = calc_trend_stats(x[mask], y[mask])
trend[i, j] = ______
pval[i, j] = ______
rmap[i, j] = ______
r2map[i, j] = ______
alpha = 0.05
sig_mask = pval < alpha
fig = plt.figure(figsize=(14, 6))
ax = fig.add_subplot(1, 1, 1,
projection=ccrs.PlateCarree(central_longitude=______))
pcm = ax.pcolormesh(
lon, lat, trend,
transform=ccrs.PlateCarree(),
shading="auto",
cmap="coolwarm",
vmin=-0.10, vmax=0.10
)
ax.add_feature(cfeature.LAND, facecolor="0.85", edgecolor="none", zorder=3)
ax.coastlines(linewidth=0.8, zorder=4)
gl = ax.gridlines(draw_labels=True, linewidth=0.5, color="0.7", alpha=0.8)
gl.top_labels = False
gl.right_labels = False
cbar = fig.colorbar(pcm, ax=ax, pad=0.02, shrink=0.9)
cbar.set_label("Trend (°C/year)")
ax.set_title(f"Global SST Trend (ERSSTv5, {start_year}-{end_year})")
fig.tight_layout()
fig.canvas.draw()
fig.savefig("sst_trend.png", dpi=300, bbox_inches="tight", facecolor="white")
plt.show()
有意な格子点だけを黒点で重ねて表示する。
yy, xx = np.where(sig_mask)
ax.scatter(
lon[xx], lat[yy],
s=2, c="k", marker="o",
transform=ccrs.PlateCarree(),
zorder=5, alpha=_____
)
今回の検定では、各格子点について次の帰無仮説を考えている。
つまり、「その格子点では長期トレンドがない」と仮定する。このとき、実際に得られた傾き以上に大きな値が偶然に得られる確率が p 値である。
たとえば alpha = 0.05 としたとき、p < 0.05 なら「5%水準で有意」と呼ぶことが多い。
| 量 | 見ている内容 |
|---|---|
| 傾き a | 何 ℃/年で増減しているか |
| 決定係数 R² | 直線でどの程度説明できるか |
| p 値 | その傾きが 0 ではないといえるか |
ここまでで、各格子点における海面水温(SST)の長期トレンドを求め、 全球平均が上昇していても、その増減の大きさや有意性は空間的に一様ではないことを確認した。 では、この空間分布の違いは何によって説明できるのだろうか。
前回の ENSO コンポジット解析では、El Niño と La Niña のときに 海面水温の分布がどのように変わるかを調べた。 これは、SST の年々変動、つまり比較的短い時間スケールの変動を見ている。
たとえば赤道太平洋では ENSO の影響が強く、ある年には暖かく、 別の年には冷たくなる。このような場所では、SST の分布を理解するうえで ENSO が重要な説明要因になる。
一方、このページで見ているトレンドは、数十年スケールで SST がどの方向に変化してきたかを表している。 これは、温暖化や海洋循環の長期変化など、 長い時間スケールの変動を反映している可能性がある。
したがって、トレンドマップは「この海域では長い目で見ると暖まりやすいのか、 それともあまり変化していないのか」を示している。
ENSO コンポジットで大きな差が見られる海域では、 年々変動のかなりの部分を ENSO が説明していると考えられる。 しかし、ENSO の影響が弱い海域でも長期トレンドが強く現れることがある。 その場合、SST の変化は ENSO だけでは説明できない。
逆に、長期トレンドが見られても、年ごとのばらつきが非常に大きい海域では、 実際の SST 変動をトレンドだけで説明することは難しい。 たとえば ENSO のような大きな年々変動が重なると、 長期的には上昇傾向でも、ある年には一時的に低温になることがある。
このように、SST の変動を理解するには、 短期変動(ENSO)と長期変化(トレンド)を分けて考えることが重要である。 今後はさらに、海面高度や流れの情報も使いながら、 「なぜその場所で変化が起きるのか」を物理的に考えていく。
まずは上の穴埋めコードを自分で考えること。確認したい場合だけ、パスワードを入力して解答と完全スクリプトを表示する。
ncfile
1993
2021
0.5
30
a
p
r
r2
180
0.6
import numpy as np
import xarray as xr
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import cartopy.crs as ccrs
import cartopy.feature as cfeature
ncfile = "sst.mnmean.nc"
ds = xr.open_dataset(ncfile)
sst = ds["sst"].values.astype(float)
lat = ds["lat"].values
lon = ds["lon"].values
time = pd.to_datetime(ds["time"].values)
sst[sst < -100] = np.nan
start_year = 1993
end_year = 2021
mask_period = (time.year >= start_year) & (time.year <= end_year)
sst_sub = sst[mask_period, :, :]
time_sub = time[mask_period]
years = time_sub.year + (time_sub.month - 0.5) / 12.0
years = years.to_numpy(dtype=float)
def calc_trend_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]
n = len(x)
if n < 3:
return np.nan, np.nan, np.nan, np.nan, np.nan
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 = np.nan if ss_tot == 0 else 1 - ss_res / ss_tot
r = np.corrcoef(x, y)[0, 1]
dof = n - 2
sxx = np.sum((x - np.mean(x)) ** 2)
mse = ss_res / dof
se_a = np.sqrt(mse / sxx)
tval = a / se_a
pval = 2 * (1 - stats.t.cdf(np.abs(tval), df=dof))
return r, a, b, r2, pval
nlat = len(lat)
nlon = len(lon)
trend = np.full((nlat, nlon), np.nan)
pval = np.full((nlat, nlon), np.nan)
rmap = np.full((nlat, nlon), np.nan)
r2map = np.full((nlat, nlon), np.nan)
min_count = 30
for i in range(nlat):
for j in range(nlon):
y = sst_sub[:, i, j]
x = years
mask = np.isfinite(y)
if np.sum(mask) < min_count:
continue
r, a, b, r2, p = calc_trend_stats(x[mask], y[mask])
trend[i, j] = a
pval[i, j] = p
rmap[i, j] = r
r2map[i, j] = r2
alpha = 0.05
sig_mask = pval < alpha
fig = plt.figure(figsize=(14, 6))
ax = fig.add_subplot(1, 1, 1,
projection=ccrs.PlateCarree(central_longitude=180))
pcm = ax.pcolormesh(
lon, lat, trend,
transform=ccrs.PlateCarree(),
shading="auto",
cmap="coolwarm",
vmin=-0.10, vmax=0.10
)
ax.add_feature(cfeature.LAND, facecolor="0.85", edgecolor="none", zorder=3)
ax.coastlines(linewidth=0.8, zorder=4)
gl = ax.gridlines(draw_labels=True, linewidth=0.5, color="0.7", alpha=0.8)
gl.top_labels = False
gl.right_labels = False
cbar = fig.colorbar(pcm, ax=ax, pad=0.02, shrink=0.9)
cbar.set_label("Trend (°C/year)")
yy, xx = np.where(sig_mask)
ax.scatter(
lon[xx], lat[yy],
s=2, c="k", marker="o",
transform=ccrs.PlateCarree(),
zorder=5, alpha=0.6
)
ax.set_title(f"Global SST Trend (ERSSTv5, {start_year}-{end_year})\nblack dots: p < {alpha}")
fig.tight_layout()
fig.canvas.draw()
fig.savefig("sst_trend.png", dpi=300, bbox_inches="tight", facecolor="white")
plt.show()