Claude CodeでxarrayのNetCDFを扱う — 次元・座標・単位を検算する
気候・海洋のNetCDFをxarrayで読むコードをClaude Codeに書かせ、次元・座標・単位・時間軸をassertで検算する手順です。ncdump -hの渡し方とdaskのchunks指定も扱います。
NetCDFの気候・海洋データをClaude Codeに読ませるときは、コードを書かせる前にncdump -hのヘッダ出力を渡し、書かせたあとに次元・座標・単位・時間軸をassertで検算します。この順番にすると、変数名の取り違えや「ケルビンを摂氏として平均した」といった静かな誤りを、実行時に止められます。
ここでは、xarrayで読むコードの書かせ方と、検算用のassertの置き場所、daskの遅延評価に載せるchunks指定までを順に示します。コード例は手順確認用の仮のもので、変数名やファイル名は実データに合わせて読み替えてください。
前提 — 必要なライブラリとncdump
xarrayでNetCDFを読み書きするには、scipy、h5netcdf、netCDF4-Pythonのいずれかが入っている必要があります。ただしscipyが扱えるのはNetCDF3形式だけです。気候モデルの出力はNetCDF4(HDF5ベース)が多いので、netCDF4かh5netcdfを入れておくのが無難です。
本文のコードは、さらに次のライブラリに依存します。
chunksの指定やopen_mfdataset(parallel=True)はdaskの配列として読むため、daskが必要ですnoleapのような標準外のcalendarを扱うときは、cftimeが使われますncdumpはxarrayのPythonパッケージとは別のコマンドです。手元で動くかはncdump -hで確かめ、無ければnetCDFのコマンドラインツールを別に入れます
pip install xarray netCDF4 h5netcdf dask cftime
ncdump -h tas_day_model_historical.nc | head最初に渡すのは、データ本体ではなくncdump -hの出力
NetCDFはバイナリ形式で、中身の構造(次元・変数・属性)が自己記述されています。Claude Codeに構造を教える最短の方法は、ncdump -hでヘッダだけをテキストにして渡すことです。値を何万行も読ませる必要はありません。
ncdump -h tas_day_model_historical.nc > tas_day.cdl@でファイルを参照すると、テキストファイルの内容が会話に入ります。ただし256KBを超えるテキストファイルは含まれず、読み込みの上限は既定で25,000トークンです。ヘッダだけのテキストにしておけば、この上限に引っかかりません。
@tas_day.cdl が対象ファイルのヘッダです。
xarrayで tas を読み、全期間の領域平均の年平均気温を返す関数を書いてください。
次元名・座標名・units は、必ずこのヘッダの記載に合わせてください。ターミナルから一度きりで頼むなら、標準入力に流す形もあります。
ncdump -h tas_day_model_historical.nc | claude -p \
"このヘッダから、次元・座標変数・units・calendar を表にして"git log --oneline -20 | claude -p "..."のように、コマンドの出力をパイプで渡す書き方があります。ncdump -hの出力でも同じ使い方ができます。
最初に表を出させるのは、コードを書かせる前に、Claude側の理解をあなたが確かめるためです。latが昇順か降順か、timeのcalendarが何か、tasのunitsが何かを目で押さえておくと、後の検算で何を固定すればよいかが決まります。
CLAUDE.mdに検算の約束を書いておく
毎回プロンプトで念押しするより、プロジェクトのCLAUDE.mdに書いておくほうが漏れません。プロジェクトのCLAUDE.mdは、ビルドやテストのコマンド、コーディング規約、よくある作業の手順といった、チーム共通の指示を置く場所です。読み込まれたかどうかは、セッション内で/contextを実行すると確かめられます。
例えば次のような約束です。
## NetCDF / xarray の扱い
- 読み込みは xr.open_dataset(path, chunks={}) を使い、全体を .load() しない
- 変数を使う前に、dims・units・時間軸の calendar を assert で確認する
- 気温は units が K か degC かを attrs["units"] から読み、
値の範囲(K なら 150〜350)も assert で確認する
- 結果を出す前に、小さな期間を .compute() して値を目で確かめる
- 入力ファイルのパスには書き込まない(出力は outputs/ に出す)このルールは、書かせたコードのレビュー基準にもなります。assertの無いコードが返ってきたら、約束と食い違っていると指摘できます。
何を検算するか — 次元・座標・単位・時間軸
検算は、データの「形」「ものさし」「時間」の三つに分けると整理できます。
| 観点 | 確かめること | assertの例 |
|---|---|---|
| 次元 | 確かめること変数のdimsが想定どおりか | assertの例set(da.dims) == {"time","lat","lon"} |
| 座標 | 確かめること緯度経度の範囲、昇順・降順、経度の表記(0〜360か−180〜180か) | assertの例latが−90〜90、timeが単調増加 |
| 単位 | 確かめることattrs["units"]と値の桁が整合しているか | assertの例Kなら150〜350に収まる |
| 時間軸 | 確かめることcalendarとデコードの結果、欠けた日がないか | assertの例日数が期待値と一致 |
例えば次のような形のチェック関数を、Claude Codeに書かせます。
import xarray as xr
def check_tas(path: str) -> None:
with xr.open_dataset(path, chunks={}) as ds:
da = ds["tas"]
# 次元
assert set(da.dims) == {"time", "lat", "lon"}, da.dims
# 座標
assert float(ds["lat"].min()) >= -90 and float(ds["lat"].max()) <= 90
assert ds["time"].to_index().is_monotonic_increasing
# 単位と値の桁
units = da.attrs.get("units")
assert units in {"K", "degK"}, f"想定外の units: {units}"
first = da.isel(time=0).compute()
assert 150 < float(first.min()) and float(first.max()) < 350with文で開くと、読み終えたあとにファイルが自動で閉じられます。xarrayの公式ガイドも、Dataset.close()を呼ぶよりwithのほうが見通しがよいと書いています。
座標は、範囲だけでなく並びと重なりも見る
座標の検算は、範囲の確認だけでは足りません。複数のファイルをopen_mfdatasetでつないだときは、境目で同じ日付が二重に入ったり、日付が抜けたりすることがあります。経度も、0〜360度で保存されたデータと、−180〜180度で保存されたデータが混ざると、領域の切り出しが空になります。
import numpy as np
def check_coords(ds, expected_days: int) -> None:
idx = ds["time"].to_index()
assert idx.is_unique, "time に重複があります"
assert len(idx) == expected_days, f"日数 {len(idx)} != {expected_days}"
# ncdump -h で確かめた表記(ここでは 0〜360)だけを許す
lon = ds["lon"]
assert float(lon.min()) >= 0 and float(lon.max()) <= 360, "lon が 0〜360 ではありません"
assert np.all(np.diff(ds["lat"].values) != 0), "lat に重複があります"expected_daysは、対象期間の日数をプロンプトで明示して渡します。historicalの1950〜2014年を日別に持つ標準calendarのデータなら、うるう年を含めた日数が決まります。noleapのcalendarなら、1年は365日で数えます。ここを手計算でなくコードで出させ、calendarごとの数え方をClaude Codeに書かせて、結果をヘッダの記載と見比べます。
単位の取り違えは、属性と値の両方で捕まえる
単位の誤りは、属性だけを見ても、値だけを見ても取りこぼします。属性が正しいのに値がおかしいデータもあるからです。
xarrayの公式ガイドのOPeNDAPの節には、実在の再解析データを開いた例があります。TairのunitsはdegKで、actual_rangeは[185.16 322.1]です。同じデータセットのdTdxとdTdyは°C/mで、単位の書き方が混在しています。PRISMの月別データでは、tmaxのunitsがCelsius_scaleと書かれています。
ここから分かるのは、unitsの書き方がデータセットごとに違うということです。"K"だけを許可するassertは、degKのデータで落ちます。どの表記を許すかは、ncdump -hの出力を見たうえで、許可リストとして明示するのが現実的です。
属性の文字列だけで判定しないために、値の範囲も合わせて見ます。
UNIT_RANGES = {
"K": (150.0, 350.0),
"degK": (150.0, 350.0),
"degC": (-90.0, 60.0),
"Celsius_scale": (-90.0, 60.0),
}
def assert_temperature_units(da) -> None:
units = da.attrs.get("units")
assert units in UNIT_RANGES, f"未登録の units: {units}"
lo, hi = UNIT_RANGES[units]
sample = da.isel(time=0).compute()
assert lo < float(sample.min()) and float(sample.max()) < hi, (
f"units={units} だが値の範囲が合わない: "
f"{float(sample.min())}〜{float(sample.max())}"
)この範囲は気温を前提にした目安で、降水や塩分には使えません。変数ごとに許容範囲を表にして、CLAUDE.mdか別ファイルに置いておくと、Claudeが新しい変数を扱うときも同じ基準で検算できます。
time座標のデコードは、失敗する前提で確かめる
NetCDFの時間は、days since 1850-01-01のような「基準日からの経過」で保存されることが多く、xarrayは既定でこれを日時に変換(デコード)します。この変換が崩れるデータが、実在します。
公式ガイドのOPeNDAPの節では、このデータセットがCFの規約に完全には従っていないと説明されています。calendarの属性が'360_day'のような文字列ではなく、整数の360で書かれていて、自動デコードが失敗します。回避策としてdecode_times=Falseで開く例が載っています。
気候モデルの出力には、うるう年のないnoleapのような暦もあります。標準外のcalendarは、cftimeライブラリを使ったCFTimeIndexで扱われます。xarray 2025.01.2からは、ナノ秒の範囲を超える日付も扱えるようになっています。
Claude Codeには、時間軸について次の確認を書かせます。
def check_time(ds) -> None:
time = ds["time"]
print("dtype:", time.dtype)
print("calendar:", time.attrs.get("calendar"), time.encoding.get("calendar"))
print("first/last:", time.values[0], time.values[-1])
assert time.size > 1
assert time.to_index().is_monotonic_increasingdtypeがobjectなら、cftimeで表現されているサインです。calendarの種類を最初に表にして確かめるのは、日付の扱いがここで分かれるためです。
デコードに失敗した場合は、decode_times=Falseで開いて生の数値を見て、基準日と単位をヘッダと突き合わせます。そのうえで、必要ならunitsとcalendarを補ったデコードを手で書かせます。失敗を握りつぶして先へ進まないよう、assertで止めるのが原則です。
chunksを指定して、大きなファイルをdaskの遅延評価に載せる
xarrayは、netCDFのデータを常に遅延読み込みします。スライスや部分選択だけなら、値はメモリに載りません。さらにchunksを指定すると、daskの配列になり、並列に処理できます。
# 単一ファイルを、時間方向10個ずつのチャンクで開く
ds = xr.open_dataset("tas_day.nc", chunks={"time": 10})
# ファイルのチャンクに合わせる(1チャンク=1ブロック)
ds = xr.open_dataset("tas_day.nc", chunks={})
# 複数ファイルをまとめて並列に開く
ds = xr.open_mfdataset("tas_day_*.nc", parallel=True)chunks={}は、daskの1チャンクをディスク上の1チャンクに対応させる指定です。chunks="auto"は、ディスク上のチャンクの倍数に自動で決めます。多数のファイルをopen_mfdatasetで開くときは、engine="h5netcdf"が速くなる場合があります。
チャンクの大きさの目安は、1チャンクあたり100万要素以上です。小さすぎるとdaskのタスク数が増えてオーバーヘッドが大きくなり、大きすぎると同時に載せたときにメモリを圧迫します。時間方向の処理が中心なら時間方向を細かく、空間の部分抽出が中心なら緯度経度を細かく、というように、あとの解析に合わせて決めます。
遅延評価の落とし穴は、検算のassertが評価を引き起こすことです。float(da.min())のような式は、全体を走査します。サイズが大きいデータでは、次のように範囲を絞ってから確かめます。
sample = da.isel(time=0).compute() # 先頭1時刻だけを実計算.compute()と.load()は似ていますが、compute()は新しいオブジェクトを返し、load()は元のオブジェクトを書き換えます。公式ガイドはcompute()のほうを勧めています。
Claude Codeに検算まで回させる
最後に、書かせたコードを実行させ、出力を読ませます。「動きました」で終わらせず、検算の結果を数字で報告させるのがコツです。
tas_day.nc に対して check_tas() を実行してください。
失敗したら、どの assert で落ちたかと、そのときの値を示してください。
通ったら、次元・units・期間(最初と最後の日付)を1行で報告してください。PostToolUseフックを使うと、Edit|Writeでコードが書き換わるたびにチェックを走らせる構成にもできます。フックの設定では、"matcher": "Edit|Write"でlintなどのコマンドを呼べます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "python scripts/check_units.py" }
]
}
]
}
}scripts/check_units.pyの中身は、上で書いたcheck_tas()を、手元の小さなサンプルファイルに当てるだけのもので構いません。大きな実データを毎回走査する設計は避けます。
落とし穴
欠損値の印がそのまま平均に混ざる
netCDFの欠損値は_FillValueで表されます。xarrayは読み込み時にmask_and_scaleの設定でこれをマスクするかどうかを制御します。マスクされない設定で開いたり、独自の印が使われていたりすると、極端な値が数値のまま残り、平均や最小値を大きく狂わせます。検算の最初にda.isnull().sum()で欠損の数を確かめ、値の範囲のassertと合わせて見ると、混入に気づけます。
元ファイルは書き換わらない、という前提
公式ガイドには、データセットの値を変更しても、メモリ上のコピーだけが変わり、ディスク上の元ファイルには触れないと書かれています。書かせたコードが元ファイルを壊す心配は、to_netcdfで書き出さない限りありません。
とはいえ、to_netcdfの出力先をうっかり元ファイルと同じパスにすると、上書きの危険があります。出力先はoutputs/のような別のディレクトリに固定し、CLAUDE.mdに「入力ファイルのパスには書き込まない」と書いておくと安全です。
まとめ
xarrayで気候・海洋データを扱うコードをClaude Codeに書かせるときの要点は、構造を先に渡し、約束をCLAUDE.mdに置き、assertで形・ものさし・時間を止めることです。ncdump -hの表を出させて理解を確かめ、unitsは属性と値の範囲の両方で、timeはdtypeとcalendarで確かめます。大きなファイルはchunksで遅延評価に載せ、検算は先頭の1時刻など小さな範囲に絞ります。
検算の考え方は、他のデータでも使えます。JANコードのチェックデジットを検算する手順や、データ移行の項目マッピングと件数照合も、「機械的に確かめられる性質を先にassertへ落とす」という同じ型です。機械学習の前処理でデータの扱いを点検したいときは、データリークの点検も参考になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。