3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

無償データで72時間降雨予測を組み立てる Step 3:GFS・IFS・AIFSを区間雨量へ揃え、安倍川流域平均雨量を計算する

3
Posted at

無償データで72時間降雨予測を組み立てる Step 3:GFS・IFS・AIFSを区間雨量へ揃え、安倍川流域平均雨量を計算する

はじめに

前回のStep 2では、NOAA GFS、ECMWF IFS、ECMWF AIFS Singleから、同じ予報初期時刻の72時間予報を取得しました。

今回使うrunは、次の時刻です。

2026081418

これはUTCで2026年8月14日18時、日本時間では2026年8月15日3時です。取得できたファイル数は次のとおりでした。

データセット native予報時間 ファイル数
NOAA GFS 0.25度 1時間間隔、f001~f072 72
ECMWF IFS Open Data 3時間間隔、f000~f072 25
ECMWF AIFS Single 6時間間隔、f000~f072 13
合計 110

ここまでで、予報データを取得して保存するところまではできました。ただし、3つのデータセットをそのまま横に並べても、雨量として同じ意味にはなりません。

GFSとECMWFでは配布単位が異なり、予報時間の刻みも違います。GRIB2に入っている雨量が、直前からの区間積算なのか、ある時点からの累積なのかも確認する必要があります。さらに、安倍川流域の雨量として使うには、予報格子と流域界を重ね、面積を考慮して集計しなければなりません。

Step 3では、予測精度を上げる機械学習にはまだ進みません。まず、流出解析や観測検証へ渡せるように、時刻、単位、積算区間、格子、流域平均の意味を固定するところまで進めます。

今回作るもの

プロジェクト名は次のようにしました。

RainfallForecast72h_FreeData_Step3_r001

参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。

処理の流れは次のとおりです。

ChatGPT Image 2026年8月15日 17_03_49.png

成果物は、主に次の4種類です。

  • データセット別のnative区間雨量
  • 予報格子と流域界の交差面積・格子重み
  • 安倍川流域のnative区間平均雨量と比較用1時間系列
  • Parquet、CSV、DuckDB、検証レポート、SHA-256 manifest

河川流量、水位、洪水リスク評価はまだ行いません。そこへ進む前に、降雨入力そのものを
再現できる状態にします。

なぜGRIB2をそのまま比較できないのか

予報時間の刻みが違う

今回取得したデータは、次の時間間隔です。

GFS    1時間
IFS    3時間
AIFS   6時間

例えば、IFSのf006に入っている値を「6時の1時間雨量」と読んでしまうと、意味が変わって
しまいます。3時間積算なら、対象は3~6時間の区間です。AIFSも同じで、6時間値を1時間値
として扱うことはできません。

そこで、Step 3では、まず配布元の時間間隔を保ったnative区間雨量を作ります。

GFS    0~1、1~2、…、71~72時間
IFS    0~3、3~6、…、69~72時間
AIFS   0~6、6~12、…、66~72時間

このnative系列を正本とします。

単位が違う

GFSの地上積算降水量は、GRIB2ではkg m-2またはmmとして読まれることがあります。
水1kg/m²は水深1mmに相当するため、係数は1です。

一方、ECMWFのtpはメートル単位です。mmへ変換するときは1000倍します。

GFS    kg m-2 / mm → mm    × 1
IFS    m          → mm    × 1000
AIFS   m          → mm    × 1000

ただし、ソースではモデル名だけを見て1000倍しません。実GRIB2のunitsが設定に登録された
値と一致することを確認し、未知の単位なら停止します。単位を推測して処理を続けると、
1000倍の誤差を作る可能性があるためです。

累積値と区間値を見分ける必要がある

Step 3では、ファイル名のf003やモデル名だけを根拠にしません。GRIB2から次を読みます。

shortName
units
stepType
stepRange
startStep
endStep
validityDate
validityTime

同じstartStepを持つ直前の場がある場合は、その値との差を取ります。新しいstartStepの
最初の場は、GRIB2が示す区間量として使います。

同じstartStepの前値なし
    → startStep~endStepの区間値として使用

同じstartStepの前値あり
    → 現在値-前値で区間雨量を作成

この方法にしたのは、run開始から累積する場合と、途中で積算開始が切り替わる場合を、
同じ処理で扱えるようにするためです。

Step 2のデータセット原本を引き継ぐ

Step 3を展開し、Step 2 r005で取得したデータセットの原本をコピーします。

unzip RainfallForecast72h_FreeData_Step3_r001.zip
cd RainfallForecast72h_FreeData_Step3_r001

cp .env.example .env

rsync -a \
  ../RainfallForecast72h_FreeData_Step2_r005/data/raw/ \
  data/raw/

.envを確認します。

STEP3_IMAGE_NAME=rainfallforecast72h-freedata-step3
STEP3_IMAGE_TAG=r001
FORECAST_RUN=2026081418
ACQUISITION_PROFILE=rainfall_minimal

Step 3ではlatestを使いません。Step 2で3データセットをまとめて検証した共通runを固定します。
処理中に新しい予報が公開されても、別runが混ざらないようにするためです。

また、コピーした原本をファイル名だけで信用しません。Step 2のsidecarにある
size_bytesとsha256を、Step 3開始時に全ファイルへ再照合します。同名のGRIB2へ
別内容が上書きされた場合や、コピー途中で欠けた場合は、ecCodesで読む前に停止します。
さらに、sidecarとGRIB2の初期時刻・有効時刻を固定runと照合し、別runの混入も拒否します。

既に取得した原本を処理するだけなら、外部接続は行いません。Step 2の不足ファイルを再取得する
場合だけ、提供元の利用条件を再確認してACCEPT_DATA_TERMS=YESを使います。

安倍川の参照流域界を準備する

今回は国土数値情報W07を使う

無償データだけで処理を再現できるよう、既定構成では国土数値情報の「流域メッシュ
W07」を使います。

W07は100mメッシュごとに水系名・河川名などを持つ平成21年度のデータです。安倍川は
一次メッシュ5238と5338にまたがるため、次の2ファイルを使います。

W07-09_5238-jgd_GML.zip
W07-09_5338-jgd_GML.zip

国土数値情報の公式ページで利用条件を確認し、次へ手動で保存します。

data/raw/mlit/ksj_w07/
├── W07-09_5238-jgd_GML.zip
└── W07-09_5338-jgd_GML.zip

原本はGitや配布ZIPへ入れません。W07は非商用の利用条件が示されているため、実装側で
自動ダウンロードする構成にもしていません。

保存後、次を実行します。

./scripts/prepare_w07_reference.sh

Docker Composeを直接実行する場合です。

docker compose run --rm prepare-basin-w07

処理では、W07の水系名に相当するW07_004が安倍川のメッシュだけを抽出し、2ファイルを
結合します。

出力は次です。

data/processed/basin/
├── abe_river_basin_reference.gpkg
├── abe_river_basin_reference.geojson
└── abe_river_basin_reference.meta.json

正式流域界とは呼ばない

W07は平成21年度の参照データです。現在の河川管理者による正式な流域界GISとは位置付けが
異なるため、メタデータには次の状態を残します。

geometry_status = ksj_w07_reference

official_polygonとは記録しません。

国土交通省資料にある安倍川の流域面積567km²、幹川流路延長51kmを参照値として記録し、
抽出したポリゴンの面積が大きく外れていないかを確認します。ただし、面積が近いことだけで
正式境界と判断することはありません。

独自の流域界へ差し替える

河川管理者から入手したGISや、DEMから作成した流域界を使うこともできます。

docker compose run --rm -T check-tools \
  python -m rainfall_processing prepare-basin-custom \
    --root /workspace \
    --path /workspace/data/import/abe_basin.gpkg \
    --status derived_dem

--statusは次から選びます。

official          正式GISと基準日・利用条件を確認した場合
derived_dem       DEMから作成した場合
reference_custom  その他の参照境界

処理確認だけなら暫定矩形も作れます。

docker compose run --rm prepare-basin-preview

ただし、暫定矩形にはprovisional_bboxが記録され、通常のstep3-runでは拒否されます。
矩形の平均雨量を「安倍川流域平均」と誤って扱わないためです。

フォルダー構成

主な追加ファイルは次のとおりです。

RainfallForecast72h_FreeData_Step3_r001/
├── configs/
│   ├── processing.yaml
│   ├── basin.yaml
│   └── stations.yaml
├── src/rainfall_processing/
│   ├── basin.py
│   ├── grib_reader.py
│   ├── normalizer.py
│   ├── units.py
│   ├── weights.py
│   ├── aggregate.py
│   ├── stations.py
│   ├── validation.py
│   ├── ledger.py
│   ├── manifest.py
│   ├── pipeline.py
│   └── cli.py
├── scripts/
│   ├── prepare_w07_reference.sh
│   └── run_step3_processing.sh
├── docs/
│   ├── step3_design.md
│   ├── step3_runbook.md
│   ├── step3_output_schema.md
│   └── maintenance_guide.md
├── tests/
│   └── test_step3_processing.py
├── Dockerfile
├── compose.yaml
└── README.md

Step 1・2の取得処理も残してあります。Step 3で原本不足が見つかったときに、同じ取得契約で
再検証・再取得できるようにするためです。

設定ファイル

processing.yaml

単位、native時間間隔、負値許容値、保存先、検証条件をまとめています。

project:
  step: 3
  forecast_horizon_hours: 72

time_axis:
  canonical_timezone: "UTC"
  display_timezone: "Asia/Tokyo"
  keep_native_interval: true
  create_hourly_comparison: true

rainfall:
  normalized_amount_unit: "mm"
  negative_rounding_tolerance_mm: 0.05
  missing_value_policy: "欠測と0mmを区別し、欠測を0へ置換しない"

sources:
  noaa_gfs_0p25:
    accepted_units: ["kg m**-2", "kg m-2", "kg/m^2", "mm"]
    unit_to_mm_factor: 1.0
    accumulation_mode: "auto_from_grib"
    expected_interval_hours: 1

  ecmwf_ifs_open:
    accepted_units: ["m"]
    unit_to_mm_factor: 1000.0
    accumulation_mode: "auto_from_grib"
    expected_interval_hours: 3

  ecmwf_aifs_open:
    accepted_units: ["m"]
    unit_to_mm_factor: 1000.0
    accumulation_mode: "auto_from_grib"
    expected_interval_hours: 6

accumulation_modeはauto_from_gribにしています。設定名にモデル固有の決め打ちを
入れず、実GRIB2のstartStep/endStepを見て処理します。

basin.yaml

basin:
  id: "abe_river_basin"
  name_ja: "安倍川流域"
  source_files:
    - "data/raw/mlit/ksj_w07/W07-09_5238-jgd_GML.zip"
    - "data/raw/mlit/ksj_w07/W07-09_5338-jgd_GML.zip"

  filter:
    column_candidates: ["W07_004", "水系名", "river_system_name"]
    value: "安倍川"

  area_crs_epsg: 6676
  official_reference_area_km2: 567.0
  allowed_relative_difference: 0.20
  minimum_grid_coverage_ratio: 0.98

面積計算にはJGD2011平面直角座標系VIII系を使います。WGS84緯度経度のまま面積を計算しません。

実装で大事にした点

原本を上書きせず、処理前に同一性を確かめる

Step 2で取得したdata/rawは取得証跡です。Step 3は読み取るだけで、正規化結果を別の
ディレクトリへ作ります。さらに、各sidecarのファイルサイズとSHA-256を実GRIB2へ照合し、
一致した原本だけを正規化します。更新日時やファイル名だけでは再利用可否を決めません。

data/raw       外部原本・sidecar
data/interim   GRIB2メタデータの点検結果
data/processed 正規化雨量・格子重み・流域平均

積算解釈や単位変換を後で見直す場合も、原本から再処理できます。

微小負値だけを0mmへ補正する

累積値の差分では、浮動小数点やエンコードの丸めにより、ごく小さな負値が出ることが
あります。設定値以内だけ0mmへ補正します。

negative_rounding_tolerance_mm: 0.05

補正した行にはQCフラグを残します。これを超える負値は、run混在、積算開始の誤認、
格子変化などの可能性があるため停止します。

value_mm, qc_flag = sanitize_interval_rainfall(
    interval_value_mm,
    tolerance_mm=negative_tolerance_mm,
)

絶対値へ変換したり、すべて0mmへ丸めたりはしません。

格子中心だけで流域内外を決めない

GFS、IFS、AIFSは0.25度格子です。安倍川流域に対してはかなり粗いため、格子中心が流域内か
どうかだけで選ぶと、境界付近の格子を丸ごと入れるか、丸ごと捨てることになります。

そこで、格子中心と格子増分からセルポリゴンを復元し、安倍川流域との交差面積を求めます。

格子重み = 流域との交差面積 / 全交差面積

流域平均雨量 = Σ(格子雨量 × 格子重み)

取得範囲が流域全体を覆っているかも別に計算します。

流域被覆率 = 取得格子と流域の交差面積 / 流域面積

重み合計が1でも、取得格子が流域の一部しか覆っていない場合があります。そのため、
重み合計と被覆率は別々に検査します。既定では被覆率98%未満をエラーにします。

欠測を0mmとして集計しない

流域と交差する格子の値が欠測だった場合、0mmとして計算を続けません。

欠測 = 観測・予報値がない
0mm  = 雨が降らないと予報された

意味が違うためです。欠測格子がある区間は停止し、原本、GRIB inventory、取得範囲を確認します。

Docker Composeで実行する

ビルド

docker compose build

ツールを確認します。

docker compose run --rm check-tools

主な確認対象です。

Python
CDO
ecCodes / grib_ls / grib_get_data
GDAL / ogr2ogr
DuckDB
PyArrow
GeoPandas
Pyogrio
Shapely
PyProj
Git

docstringを確認する

docker compose run --rm check-docstrings

正常時は次です。

DOCSTRING_CHECK=PASS scope=src+tests

Step 3設定を検査する

docker compose run --rm validate-step3

ここでは、Step 2の取得間隔とStep 3の処理間隔が一致するか、72時間で終わっているか、
単位変換係数や流域被覆率の閾値が正しいかを確認します。

Step 2の原本を再検証する

VERIFY_TARGETS=noaa_gfs_0p25:2026081418,ecmwf_ifs_open:2026081418,ecmwf_aifs_open:2026081418 \
ALLOW_PARTIAL=NO \
docker compose run --rm verify-step2

110ファイル、110 sidecar、3つのrun manifestがそろい、SHA-256とGRIB2が正常であることを
確認してから処理します。

Step 3を一括実行する

流域界の準備後、次を実行します。

./scripts/run_step3_processing.sh 2026081418

スクリプトでは、次の順に処理します。

ツール確認
  ↓
docstring確認
  ↓
Step 3設定確認
  ↓
準備済み流域界を確認
  ↓
GFS・IFS・AIFSをnative区間雨量へ正規化
  ↓
観測所候補台帳を作成
  ↓
格子と流域界の交差面積・重みを作成
  ↓
native区間の流域平均雨量を作成
  ↓
比較用1時間系列を作成
  ↓
件数・欠測・負値・被覆率・質量収支を検証
  ↓
検証を通過したrunだけDuckDBへ登録
  ↓
manifestとrunサマリーを作成
  ↓
テスト・Git混入検査

正常終了時は次が表示されます。

STEP3_RAINFALL_PROCESSING=PASS

Step 3本体だけを直接実行する場合です。

FORECAST_RUN=2026081418 \
docker compose run --rm step3-run

サマリーを再表示します。

FORECAST_RUN=2026081418 \
docker compose run --rm step3-status

native区間と比較用1時間系列

正規化後の区間数は次になります。

データセット 入力ファイル native区間 1時間比較系列
GFS 72 72 72
IFS 25(f000を含む) 24 72
AIFS 13(f000を含む) 12 72

f000は差分の基準であり、長さ0時間の雨量区間としては出力しません。

GFSはnative 1時間値をそのまま使います。IFSとAIFSは、モデル間の時刻をそろえて表示・比較
できるよう、3時間または6時間の値を均等配分します。

IFS 3時間雨量 12mm
    → 4mm, 4mm, 4mm

AIFS 6時間雨量 18mm
    → 3mm × 6時間

ただし、これは時間内の降雨ピークを復元したものではありません。出力には次を残します。

is_disaggregated = true
hourly_method     = estimated_uniform
usage_note        = 比較用。native区間値を正本とする

後の流出解析へ1時間値を渡す方法は、観測雨量との検証を行ってから決めます。

生成されるファイル

data/interim/step3/grib_inventory/
  GRIB2ごとの変数、単位、積算区間、格子情報

data/processed/rainfall/native_interval/
  データセット別・run別のnative区間雨量Parquet

data/processed/rainfall/native_interval_csv/
  人が確認しやすいCSV

data/processed/basin_weights/
  格子セル面積、流域交差面積、重み、被覆率

data/processed/rainfall/basin_mean/
  native流域平均と比較用1時間系列

catalog/rainfall_stations.csv
catalog/rainfall_stations.parquet
catalog/rainfall_stations.geojson
catalog/rainfall_step3.duckdb
catalog/rainfall_step3_schema.sql
catalog/step3_manifest.json

reports/step3/
  正規化サマリー、流域界検査、流域集計、検証、runサマリー

DuckDBへ保存する内容

DuckDBには次のテーブルを作ります。

processing_run
rainfall_native_interval
basin_grid_weights
basin_mean_native
basin_mean_hourly
rainfall_stations

外部GRIB2やW07原本は取り込みません。Parquetから再構築できるSQLも保存します。

catalog/rainfall_step3_schema.sql

DB作成後は、読み取り専用接続で再オープンできることまで確認します。

検証内容

Step 3では、処理が最後まで動いたかだけでなく、次を確認します。

  • GFS 72区間、IFS 24区間、AIFS 12区間
  • 72時間の終端がそろっている
  • 区間雨量に欠測がない
  • 許容外の負値がない
  • run内で格子数・格子位置が変わっていない
  • 格子重み合計が1
  • 流域被覆率が98%以上
  • 比較用1時間系列が1~72時間で連続している
  • native合計と1時間配分後の合計が一致する
  • sidecarのサイズ・SHA-256と原本GRIB2が一致する
  • DuckDBは上記検証の合格後に作成され、read-onlyで開ける
  • 原本がGitへ混入していない

検証レポートは次です。

cat reports/step3/step3_validation_2026081418.md

期待する区間数は次です。

GFS native intervals   72
IFS native intervals   24
AIFS native intervals  12
hourly comparison      72 rows per dataset

観測所候補も台帳化する

Step 3では、観測雨量そのものはまだ読み込みませんが、次の候補地点を台帳へ登録します。

気象庁AMeDAS

  • 井川
  • 有東木
  • 鍵穴
  • 静岡

国土交通省 水文水質データベース

  • 湯の島
  • 大谷
  • 戸持

流域界を変更した場合に、観測所が流域内か、流域境界からどの程度離れているかを再計算
できるよう、提供元ID、地点ID、緯度経度、標高、取得先URLをYAMLへ残します。

次のStep 4で、これらの観測所から過去雨量を取得し、予報雨量との比較へ進みます。

ソースのコメントとdocstring

Step 3は、計算式よりも「どの雨量を、どの期間の値として読んだか」が後から分かることが
重要です。そのため、ソースには次の理由をコメントとdocstringへ残しています。

  • なぜモデル名ではなくstartStep/endStepを使うのか
  • なぜ未知単位で停止するのか
  • なぜ微小負値だけを0mmへ補正するのか
  • なぜ欠測を0mmとして扱わないのか
  • なぜ格子中心判定ではなく交差面積を使うのか
  • なぜ流域被覆率を別に計算するのか
  • なぜIFS・AIFSの1時間値に推定フラグを残すのか
  • なぜW07を正式流域界と呼ばないのか
  • なぜDuckDBへ原本を入れないのか

Pythonのモジュール、クラス、関数、メソッド、入れ子関数、テスト用Fakeを自動検査します。

docker compose run --rm check-docstrings

後から単位や積算期間を変更するときのメンテナンス手順と、再生成する範囲は、次にまとめています。

docs/maintenance_guide.md

コードだけでなく、記事側にも「なぜその処理にしたか」と、変更時に確認する場所を残します。

テスト

docker compose run --rm test

Step 3で追加した主なテストです。

  • 同じstartStepの値を累積差分へ変換できる
  • 新しいstartStepの最初の値を区間値として扱える
  • ECMWFのmをmmへ変換できる
  • 微小負値だけを0mmへ補正する
  • 許容外の負値で停止する
  • 面積重み付き流域平均を計算できる
  • 欠測格子を0mmへ置換せず停止する
  • 3時間・6時間の均等配分で72時間合計を保存する
  • sidecarと原本のサイズ・SHA-256不一致を検出できる
  • Compose、Dockerfile、記事、メンテナンス資料の説明が残っている
  • すべてのPython定義にdocstringがある

配布前のローカル検査では、Pythonのコンパイル、docstring検査、YAMLとシェルの構文検査に
加え、76件のpytestが通ることを確認しています。実GRIB2、W07、DuckDBを使う一括処理は、
Docker Compose環境で実行します。

Step 3で残る課題

今回の流域平均雨量は、GFS・IFS・AIFSの粗い格子を安倍川流域へ面積配分した値です。
0.25度格子を細かく補間しても、局地的な地形性降雨を新しく予測できるわけではありません。

また、W07は現在の正式流域界ではなく、IFS・AIFSの比較用1時間系列も均等配分です。この2点は、
出力メタデータに明記しています。

Step 3の目的は、精度を良く見せることではなく、どの原本から、どの時間区間と単位を使い、
どの流域界で集計したかを再現できる状態にすること
です。

次のステップ

Step 4では、安倍川流域周辺の観測雨量を整理します。

予定している内容は次のとおりです。

  1. AMeDASの過去1時間雨量をCSVから正規化する
  2. 国土交通省の流域内雨量観測所を追加する
  3. JSTとUTCを明示して時刻をそろえる
  4. 欠測・品質情報・観測所移設情報を残す
  5. GFS・IFS・AIFSの流域平均予報雨量と比較する
  6. MAE、RMSE、Bias、降雨有無、強雨検出を予報時間別に集計する
  7. 2022年9月の豪雨事例を最初の検証期間にする

ここで初めて、どの予報モデルが安倍川流域でどのような癖を持つかを確認できます。
統計補正や確率予測は、その検証結果を見てから進めます。

参考リンク

NOAA

ECMWF

安倍川流域界・河川情報

観測雨量

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?