13 SGLIクロロフィルマッピング

GCOM-C/SGLI の 8日平均クロロフィルa濃度を読み込み、対数スケールで黒潮・伊豆諸島周辺の分布を可視化する。

講義名
データサイエンス
使用データ
GCOM-C/SGLI 8-day chlorophyll-a
到達目標
海色衛星データをNetCDFから読み込み、logスケールで適切に地図表示できるようになる

1. 背景:クロロフィルをなぜ見るのか

クロロフィルa濃度は、海洋表層に存在する植物プランクトン量の指標としてよく使われる。衛星海色センサーは、海面から戻ってくる可視光の色を観測し、経験式・半解析式などを通じてクロロフィルa濃度を推定する。

黒潮周辺では、黒潮本流、沿岸水、前線、渦、島の周辺などでクロロフィル分布が大きく変わる。海洋学実習で観測する海域がどのような海色場の中にあるのかを、事前に衛星データで確認することが目的である。

注意:クロロフィルが高い場所が、必ずその場で生産が高い場所とは限らない。移流、混合、雲による欠損、沿岸水の濁り、CDOM、懸濁物の影響も考える必要がある。

2. 使用データ

この演習では、元のSGLIファイルを学生用に軽量化した NetCDF ファイルを使う。元データは 0.0025° 格子で非常に高解像度であり、変数数も多いため、そのまま学生PCで扱うには重い。そこで CHLA_AVE だけを残し、空間的に粗くした軽量版を使う。

項目内容
ファイル名kuroshio_sgli_chl_20260501_8day_005deg.nc
変数名chl(time, latitude, longitude)
単位mg m-3
期間2026-05-01 から 2026-05-08 の8日平均
代表日2026-05-04
主な表示範囲137–142°E, 32–36°N
雲と欠損:SGLIのような可視・近赤外の海色センサーは、雲があると海面を観測できない。そのため、8日平均でも欠損が残ることがある。

3. 計算の流れ

  1. NetCDF ファイルを xarray で開く
  2. chl 変数を取り出す
  3. 表示したい緯度経度範囲を指定する
  4. その範囲だけを .sel() で切り出す
  5. 対数表示のため、0以下の値をマスクする
  6. LogNorm を使って 0.1〜30 mg m-3 の範囲で描く
  7. 陸域・海岸線・島名を重ねる

4. 作成される図の例

下の図は、このページの完全スクリプトを Jupyter Lab で実行して保存した PNG である。HTML と同じディレクトリに画像ファイルを置けば、このようにページ上に掲載できる。

GCOM-C/SGLI chlorophyll-a map around Izu Islands
図1. GCOM-C/SGLI 8日平均クロロフィルa濃度。表示範囲は 137–142°E, 32–36°N。色は対数スケールで 0.1〜30 mg m-3
教員用メモ:Jupyter Lab 上で plt.savefig(...) を実行すると、同じ作業ディレクトリにPNGが保存される。そのPNGをこのHTMLと同じ場所へ置けば、Webページに図として表示できる。

5. Pythonで使う主な機能

書き方意味
xr.open_dataset(...)NetCDFファイルを開く。
ds["chl"]クロロフィル変数を取り出す。
isel(time=0)time次元の最初の時刻を取り出す。
sel(longitude=slice(...), latitude=slice(...))緯度経度の範囲で切り出す。
where(chl_sub > 0)0以下の値をNaNにする。
LogNorm(vmin=0.1, vmax=30)色を対数スケールで表示する。
ax.text(...)地図上に島名などの文字を書く。
plt.savefig(...)Jupyter Lab の作業ディレクトリに図をPNGとして保存する。
STEP 1

6. データを読む

import numpy as np
import xarray as xr
import matplotlib.pyplot as plt

import cartopy.crs as ccrs
import cartopy.feature as cfeature

from matplotlib.colors import LogNorm

filename = "kuroshio_sgli_chl_20260501_8day_005deg.nc"

ds = xr.open_dataset(filename)
print(ds)

chl = ds["chl"].isel(time=0)
date_str = str(ds["time"].values[0])[:10]
print("Representative date:", date_str)

time=1 のファイルなので、isel(time=0) で1枚だけ取り出す。

STEP 2

7. 表示範囲を指定する

lon_min, lon_max = 137, 142
lat_min, lat_max = 32, 36

ここを変更すると、表示する範囲を自由に変えられる。

範囲の例

  • 広域:123–150E, 24–50N
  • 黒潮・黒潮続流:130–150E, 28–40N
  • 観測実習海域付近:137–142E, 32–36N
STEP 3

8. 指定範囲を切り出す

lat = ds["latitude"]
lon = ds["longitude"]

if lat[0] < lat[-1]:
    lat_slice = slice(lat_min, lat_max)
else:
    lat_slice = slice(lat_max, lat_min)

if lon[0] < lon[-1]:
    lon_slice = slice(lon_min, lon_max)
else:
    lon_slice = slice(lon_max, lon_min)

chl_sub = chl.sel(
    longitude=lon_slice,
    latitude=lat_slice
)

lon_sub = chl_sub["longitude"].values
lat_sub = chl_sub["latitude"].values

print("Subset shape:", chl_sub.shape)

緯度座標はデータによって昇順・降順が異なることがある。そのため、lat[0] < lat[-1] で向きを判定してから slice を作っている。

STEP 4

9. LogNormで表示する

chl_plot = chl_sub.where(chl_sub > 0)

chl_vmin = 0.1
chl_vmax = 30.0

fig = plt.figure(figsize=(9, 7))
ax = plt.axes(projection=ccrs.PlateCarree())

ax.set_extent(
    [lon_min, lon_max, lat_min, lat_max],
    crs=ccrs.PlateCarree()
)

p = ax.pcolormesh(
    lon_sub,
    lat_sub,
    chl_plot,
    transform=ccrs.PlateCarree(),
    shading="auto",
    cmap="jet",
    norm=LogNorm(vmin=chl_vmin, vmax=chl_vmax)
)

クロロフィルは 0.1, 1, 10 のように桁で変化するため、線形スケールではなく対数スケールで見る。

重要:LogNorm は0以下の値を扱えないため、先に chl_sub.where(chl_sub > 0) で0以下をマスクする。
STEP 5

10. 島名を書き込む

ax.add_feature(cfeature.LAND, facecolor="0.75", zorder=3)
ax.coastlines(resolution="10m", linewidth=0.8, zorder=4)

islands = {
    "Oshima":   (139.38, 34.75),
    "Miyake":   (139.53, 34.08),
    "Mikura":   (139.60, 33.88),
    "Hachijo":  (139.78, 33.10),
}

for name, (x, y) in islands.items():
    ax.plot(
        x, y,
        marker="o",
        markersize=4,
        color="black",
        transform=ccrs.PlateCarree(),
        zorder=10
    )

    ax.text(
        x + 0.05, y + 0.05,
        name,
        transform=ccrs.PlateCarree(),
        fontsize=10,
        color="black",
        weight="bold",
        zorder=11,
        bbox=dict(
            facecolor="white",
            edgecolor="none",
            alpha=0.75,
            boxstyle="round,pad=0.2"
        )
    )

日本語の島名は、学生PCのMatplotlib環境によって文字化けすることがある。そのため、ここでは英語表記にしている。

STEP 6

11. 図をPNGとして保存する

Jupyter Lab で作成した図をWebページに掲載するには、plt.show() の前に plt.savefig() を入れる。保存先は、Notebookを実行している作業ディレクトリである。

# 図を保存するファイル名
output_png = f"fig13_sgli_chl_{date_str}_{lon_min}_{lon_max}E_{lat_min}_{lat_max}N.png"

# Web掲載用に高解像度で保存
plt.savefig(output_png, dpi=300, bbox_inches="tight")
print("Saved figure:", output_png)

plt.show()
順番が重要:保存は plt.show() の前に行う。環境によっては、plt.show() の後に保存すると白紙画像になることがある。

12. 完全スクリプト

以下をJupyter Labのセルにそのまま貼り付けて実行する。地図を表示すると同時に、Web掲載用のPNGも作業ディレクトリへ保存する。

# ============================================================
# 13. GCOM-C/SGLIクロロフィルの読み込みとログスケール表示
# ============================================================
#
# このスクリプトで行うこと
# 1. 軽量化した SGLI クロロフィル NetCDF を読む
# 2. 表示したい緯度経度範囲を指定する
# 3. chl 変数を切り出す
# 4. 対数スケールでクロロフィルを地図表示する
# 5. 伊豆諸島の代表的な島名を英語で書き込む
# 6. 作成した図をPNGとして保存する
#
# ファイル:
#   kuroshio_sgli_chl_20260501_8day_005deg.nc
#
# 出力図:
#   fig13_sgli_chl_2026-05-04_137_142E_32_36N.png
# ============================================================


# ============================================================
# 0. ライブラリ
# ============================================================

import numpy as np
import xarray as xr
import matplotlib.pyplot as plt

import cartopy.crs as ccrs
import cartopy.feature as cfeature

from matplotlib.colors import LogNorm


# ============================================================
# 1. ファイルを開く
# ============================================================

filename = "kuroshio_sgli_chl_20260501_8day_005deg.nc"

ds = xr.open_dataset(filename)
print(ds)


# ============================================================
# 2. 表示範囲の設定
# ============================================================
#
# ここを変えれば、表示する緯度経度範囲を変更できる。
#
# 例1:SGLIデータ全体に近い範囲
# lon_min, lon_max = 123, 150
# lat_min, lat_max = 24, 50
#
# 例2:黒潮・黒潮続流を中心に見る
# lon_min, lon_max = 130, 150
# lat_min, lat_max = 28, 40
#
# 例3:観測実習海域付近を見る
# lon_min, lon_max = 137, 142
# lat_min, lat_max = 32, 36
# ============================================================

lon_min, lon_max = 137, 142
lat_min, lat_max = 32, 36


# ============================================================
# 3. データを取り出す
# ============================================================
#
# 今回のファイルは time=1 なので isel(time=0) でよい。
# chl の単位は mg m^-3。
# ============================================================

chl = ds["chl"].isel(time=0)

date_str = str(ds["time"].values[0])[:10]
print("Representative date:", date_str)


# ============================================================
# 4. 指定範囲だけ切り出す
# ============================================================
#
# latitude / longitude が昇順か降順かに対応できるようにする。
# 今回の軽量版ファイルでは、latitude が降順の場合があるので、
# slice の向きを自動判定する。
# ============================================================

lat = ds["latitude"]
lon = ds["longitude"]

if lat[0] < lat[-1]:
    lat_slice = slice(lat_min, lat_max)
else:
    lat_slice = slice(lat_max, lat_min)

if lon[0] < lon[-1]:
    lon_slice = slice(lon_min, lon_max)
else:
    lon_slice = slice(lon_max, lon_min)

chl_sub = chl.sel(
    longitude=lon_slice,
    latitude=lat_slice
)

lon_sub = chl_sub["longitude"].values
lat_sub = chl_sub["latitude"].values

print("Subset shape:", chl_sub.shape)
print("Subset lon:", float(np.nanmin(lon_sub)), "to", float(np.nanmax(lon_sub)))
print("Subset lat:", float(np.nanmin(lat_sub)), "to", float(np.nanmax(lat_sub)))


# ============================================================
# 5. 0以下をマスク
# ============================================================
#
# LogNorm を使うときは 0 以下の値を扱えない。
# そのため、0 以下は NaN にする。
# ============================================================

chl_plot = chl_sub.where(chl_sub > 0)


# ============================================================
# 6. カラースケールの設定
# ============================================================
#
# クロロフィルは桁で変わるのでログスケールで描く。
# ここでは 0.1〜30 mg m^-3 とする。
#
# 低濃度域をもっと見たい場合は chl_vmin = 0.01 にしてもよい。
# ============================================================

chl_vmin = 0.1
chl_vmax = 30.0


# ============================================================
# 7. 地図を描く
# ============================================================

fig = plt.figure(figsize=(9, 7))
ax = plt.axes(projection=ccrs.PlateCarree())

# 表示範囲
ax.set_extent(
    [lon_min, lon_max, lat_min, lat_max],
    crs=ccrs.PlateCarree()
)

# クロロフィルを描画
p = ax.pcolormesh(
    lon_sub,
    lat_sub,
    chl_plot,
    transform=ccrs.PlateCarree(),
    shading="auto",
    cmap="jet",
    norm=LogNorm(vmin=chl_vmin, vmax=chl_vmax)
)

# 陸域・海岸線
ax.add_feature(cfeature.LAND, facecolor="0.75", zorder=3)
ax.coastlines(resolution="10m", linewidth=0.8, zorder=4)


# ============================================================
# 8. 島名を地図に書き込む
# ============================================================
#
# 日本語は学生PCのMatplotlib環境で文字化けしやすい。
# そのため、ここでは英語表記にする。
# ============================================================

islands = {
    "Oshima":   (139.38, 34.75),
    "Miyake":   (139.53, 34.08),
    "Mikura":   (139.60, 33.88),
    "Hachijo":  (139.78, 33.10),
}

# 必要なら以下も追加できるが、伊豆諸島北部ではラベルが混みやすい。
# islands["Toshima"] = (139.28, 34.52)
# islands["Niijima"] = (139.25, 34.37)
# islands["Kozushima"] = (139.15, 34.22)

for name, (x, y) in islands.items():

    # 島の位置に点を打つ
    ax.plot(
        x, y,
        marker="o",
        markersize=4,
        color="black",
        transform=ccrs.PlateCarree(),
        zorder=10
    )

    # 島名を書く
    ax.text(
        x + 0.05, y + 0.05,
        name,
        transform=ccrs.PlateCarree(),
        fontsize=10,
        color="black",
        weight="bold",
        zorder=11,
        bbox=dict(
            facecolor="white",
            edgecolor="none",
            alpha=0.75,
            boxstyle="round,pad=0.2"
        )
    )


# ============================================================
# 9. 緯度経度線・カラーバー・タイトル
# ============================================================

gl = ax.gridlines(
    draw_labels=True,
    linewidth=0.5,
    color="gray",
    alpha=0.5,
    linestyle="--"
)
gl.top_labels = False
gl.right_labels = False

cb = plt.colorbar(
    p,
    ax=ax,
    shrink=0.85,
    pad=0.04,
    extend="both"
)
cb.set_label("Chlorophyll-a concentration (mg m$^{-3}$)")

plt.title(
    f"GCOM-C/SGLI 8-day mean chlorophyll-a concentration\n"
    f"{date_str}  |  {lon_min}–{lon_max}°E, {lat_min}–{lat_max}°N"
)

plt.tight_layout()


# ============================================================
# 10. 図をPNGとして保存する
# ============================================================
#
# Jupyter Lab の作業ディレクトリに保存される。
# Webページに掲載するときは、HTMLと同じディレクトリに置く。
#
# 注意:環境によっては plt.show() の後に保存すると
# 白紙になることがあるので、plt.show() の前に保存する。
# ============================================================

output_png = f"fig13_sgli_chl_{date_str}_{lon_min}_{lon_max}E_{lat_min}_{lat_max}N.png"

plt.savefig(output_png, dpi=300, bbox_inches="tight")
print("Saved figure:", output_png)

plt.show()


# ============================================================
# 11. 後片付け
# ============================================================

ds.close()
print("Finished.")

13. 結果の見方

考えてみよう
  • 観測実習海域付近のクロロフィルは高いか低いか。
  • 高クロロフィル域は沿岸に多いか、沖合にも見られるか。
  • 伊豆諸島の周辺でクロロフィル分布が変化している場所はあるか。
  • 欠損域を見たとき、日別データではなく8日平均を使う理由を説明できるか。

14. まとめ

次は、このクロロフィル分布に ADT 等値線や地衡流ベクトルを重ねることで、物理場と生物・海色場の対応を調べる。