線状降水帯予測プロトタイプを作ってみる【r001】Step 2 ー 1時間降水量から3時間積算雨量を作る
はじめに
前回のStep 1では、Open-MeteoとDIASという異なる取得元からJMA MSMを読み込み、ForecastRunという共通形式へそろえるところまでを作りました。
Step 1の最後に、DIASの実MSM地上面GPVを使って、次のところまで確認しています。
run_id run_20260612T2100Z
shape (39, 505, 481)
time_start 2026-06-12 22:00 UTC
time_end 2026-06-14 12:00 UTC
interval 1時間
nan_count 0
ここからStep 2へ進みます。
全体構成の記事では、Step 2の完了条件を次の3点にしていました。
- 指定した時間窓で積算雨量を作れる
- 欠測を検出できる
- PNGまたはGeoTIFFで確認できる
今回は、この条件に合わせて3時間移動積算雨量を実装します。
参考:
- 全体構成: https://qiita.com/rino_yume/items/171a7009633f8a11681c
- Step 1完了編: https://qiita.com/rino_yume/items/055986af6f1569b0f90c
Step 2でやること
処理の流れは次のようにしました。
Step 2では、Open-MeteoやDIAS固有の取得処理は書きません。
入力はStep 1で作った、次の共通データだけです。
ForecastRun
├─ time
├─ latitude
├─ longitude
└─ precipitation [mm]
取得元の違いはStep 1で吸収しているため、Step 2ではどちらも同じ1時間降水量Gridとして扱えます。
1. 3時間積算雨量をどう定義するか
Step 1のDIASデータでは、時刻は1時間ごとです。Open-MeteoのJMA APIでも、precipitationは表示時刻までの直前1時間の積算値として扱います。
例えば、
22:00 21:00~22:00の1時間降水量
23:00 22:00~23:00の1時間降水量
00:00 23:00~00:00の1時間降水量
と考えます。
この3つを足すと、
21:00~00:00 の3時間積算雨量
になります。
出力時刻には積算区間の終了時刻である00:00を持たせます。同時に、積算開始時刻として、
accumulation_start_time = 21:00
も保存します。
この2つを残しておけば、後から「どの3時間を積算した値なのか」を確認できます。
2. 「3サンプルを足す」とは決め打ちしない
今回のMSMは1時間間隔なので、3時間積算は3サンプルです。
ただし、将来扱うデータの時間間隔が変わる可能性があります。例えば30分間隔なら、
3時間 / 30分 = 6サンプル
です。
そのためStep 2では、サンプル数を固定せず、
時間窓 = 3時間
を指定し、入力時刻の間隔から必要なサンプル数を求めます。
window_samples = window_duration / input_interval
という考え方です。
これなら、解析処理の中に「3サンプル」という条件を直接埋め込まずに済みます。
3. 欠測は0 mmとして扱わない
ここはStep 2で特に注意したところです。
例えば、
22:00
23:00
01:00
となっていた場合、00:00がありません。
これを、
00:00 = 0 mm
として勝手に補うことはしません。
Step 2では、
22:00 → 23:00 = 1時間
23:00 → 01:00 = 2時間
となった時点で停止します。
降水量Gridの中にNaNやInfが含まれている場合も同様です。
欠測 ≠ 0 mm
として扱い、計算を続けません。
4. Step 2のDockerはネットワークを使わない
Step 2は、Step 1で保存したZarrを入力にしてローカル計算を行います。Open-MeteoやDIASへ通信する必要はありません。
そのため、Composeのstep2サービスは次の構成にしました。
services:
step2:
network_mode: none
当初は通常のCompose networkを使っていましたが、開発中にDockerのaddress poolが枯渇し、
all predefined address pools have been fully subnetted
というエラーが出ました。
Step 2自体にはネットワークが不要なので、ここはnetwork_mode: noneへ変更しました。これにより、Step 2を実行するたびにbridge networkを増やさずに済みます。
Step 1のOpen-Meteo取得やDIASダウンロードは外部通信が必要なので、Step 1側のネットワークは残しています。
5. 計算はまずNumPyで実装する
以前はGPUやCuPyも候補として考えましたが、r001のStep 2では使いません。
まずNumPyで実装します。
3時間積算では、格子点ごとにPythonループを回す必要はありません。時間軸方向の累積和を使います。
cumulative = np.cumsum(rainfall, axis=0)
累積和を、
0
1時間目
1+2時間目
1+2+3時間目
...
として持っておけば、3時間窓の和は、
累積値の後端 - 累積値の前端
で求められます。
実装では先頭に0を1枚追加して、
accumulated = cumulative[window:] - cumulative[:-window]
としています。
実際の実装では累積和をfloat64で計算し、長い時間列での丸め誤差を抑えています。出力は設定に応じてfloat32またはfloat64へ変換します。
6. なぜまだNumbaを使わないのか
Numbaを使わないと決めたわけではありません。
Step 2のソースには、将来追加できるようperformanceのoptional dependencyを残しています。
performance = [
"numba>=0.67,<0.68",
]
ただ、今回の主要処理は、
np.cumsum
配列差分
です。
すでにNumPy内部でまとめて処理されるため、先にNumbaへ移しても効果があるとは限りません。
そこで、
まずNumPy
↓
実MSMで計測
↓
遅い箇所を確認
↓
必要ならNumba
という順番にしました。
ベンチマーク用のCLIも用意しています。
docker compose run --rm step2 \
rainband-step2 benchmark \
--manifest \
/workspace/data/manifests/dias_msm_archive/run_20260612T2100Z.json \
--repeat 5
Zarrの読込は先に済ませ、NumPyの積算処理部分だけを計測します。
7. Step 1完成版からデータを引き継ぐ
今回のStep 2ソースには、Step 1のコードも同梱しています。
すでにfix012で作った実データがある場合は、再取得する必要はありません。
Step 2のZIPを置いてありますので、参考にしてください。
Step 1のデータをStep 2へコピーします。
cp -a \
../linear_rainband_r001_step1_fix012/data/. \
data/
これでStep 1で作成した、
Raw GRIB2
ForecastRun Zarr
manifest
をStep 2側でもそのまま使えます。
8. Dockerをビルドする
環境変数ファイルを作ります。
cp .env.example .env
sed -i "s/^LOCAL_UID=.*/LOCAL_UID=$(id -u)/" .env
sed -i "s/^LOCAL_GID=.*/LOCAL_GID=$(id -g)/" .env
ビルドします。
docker compose build --no-cache
Step 2でもUbuntu 24.04を基準にしています。
同じimageにStep 1とStep 2の両方を入れているため、必要ならStep 1のCLIも再実行できます。
rainband-step1
rainband-step2
9. ソースを検証する
Step 1と同じく、配布前検査をまとめて実行します。
docker compose run --rm step2 \
./scripts/validate_package.sh
検査内容は、
dependency consistency
Shell / Python syntax
Ruff format
Ruff check
mypy
pytest
Step 1 / Step 2 CLI
source contract
package source files
です。
今回、Step 2本体についてはUbuntu 24.04 + Docker実機で、
Ruff format PASS
Ruff check PASS
mypy PASS
pytest PASS
source contract PASS
package source check PASS
まで確認しています。
テストでは、1時間雨量を、
1 mm
2 mm
3 mm
4 mm
とした場合、3時間積算が、
1 + 2 + 3 = 6 mm
2 + 3 + 4 = 9 mm
になることを確認します。
さらに、
- 時刻が1時間飛んでいる
-
NaNが含まれる -
Infが含まれる - 積算に必要な時刻数が足りない
- 30分間隔なら6サンプルを使う
といったケースもテストしています。
10. DIAS MSMから3時間積算雨量を作る
Step 1で作成済みのmanifestを指定します。
docker compose run --rm step2 \
rainband-step2 accumulate \
--manifest \
/workspace/data/manifests/dias_msm_archive/run_20260612T2100Z.json \
--config /workspace/config/step2.yaml
Step 1の実データは39時刻あります。
3時間積算では最初の2時刻はまだ3時間分がそろわないため、
39 - 3 + 1 = 37時刻
になります。
実機での結果は次のとおりでした。
run_id run_20260612T2100Z
window_hours 3
window_samples 3
shape (37, 505, 481)
max_mm 219.719
期待していた(37, 505, 481)と一致しています。
最大3時間積算雨量は、この予報Runでは219.719 mmになりました。
11. 出力するデータ
Step 2では3種類の成果物を残します。
3時間積算雨量Zarr
data/processed/accumulation/3h/
└─ dias_msm_archive/
└─ run_20260612T2100Z.zarr
確認用PNG
data/preview/accumulation/3h/
└─ dias_msm_archive/
└─ run_20260612T2100Z_max.png
PNGは、全予報時刻について各格子の最大3時間積算雨量を取った確認用画像です。
これはStep 6のWebGISではなく、Step 2の計算結果を目視確認するための簡易出力です。
Step 2 manifest
data/manifests/step2_accumulation/3h/
└─ dias_msm_archive/
└─ run_20260612T2100Z.json
manifestには、
元のStep 1 manifest
元ZarrのSHA-256
積算時間
入力時間間隔
積算サンプル数
出力Zarr
確認用PNG
各SHA-256
を残します。
12. 保存後にもう一度読み直す
Step 1と同じ考え方で、
ファイルを書けた
だけでは完了にしません。
計算
↓
Zarr保存
↓
別Datasetとして再読込
↓
時刻一致
↓
積算雨量一致
まで確認します。
保存前後の積算雨量はnp.testing.assert_allclose()で比較しています。
13. manifestから再検証する
Step 2の成果物も後から検証できます。
docker compose run --rm step2 \
rainband-step2 verify \
--manifest \
/workspace/data/manifests/step2_accumulation/3h/dias_msm_archive/run_20260612T2100Z.json
実機では、
status PASS
まで確認できました。
このverifyでは、
Step 1 source manifest SHA-256
Step 1 ForecastRun verify
Step 2 Zarr SHA-256
Step 2 PNG SHA-256
Step 2 Zarr再読込
積算時間の整合
欠測の有無
を確認します。
元のStep 1データが途中で変わっていた場合も、そのまま処理を続けないようにしています。
14. NumPy版の実測結果
実DIAS MSMでNumPy版を5回計測しました。
input_shape (39, 505, 481)
output_shape (37, 505, 481)
repeat 5
median_ms 95.184
min_ms 88.170
max_ms 132.585
今回の規模では、3時間積算だけなら中央値で約95 msでした。
少なくともr001のMSM処理では、この部分を急いでNumba化する必要はなさそうです。
まずNumPy版を基準実装として使い、LFM 1 kmやLEPSへ進んでデータ量が増えた段階で改めて見直します。
15. Open-MeteoのForecastRunでも同じコードを使う
DIAS専用の積算処理にはしていません。
Step 1でOpen-Meteo側のmanifestが作られていれば、同じaccumulateをそのまま使えます。
<run_id>のような説明用文字列をそのままシェルへ渡すと、Bashでは<がリダイレクトとして扱われるため、ここでは最新manifestを自動で取得します。
OPEN_METEO_MANIFEST=$(
find data/manifests/openmeteo_jma_msm \
-maxdepth 1 \
-type f \
-name '*.json' \
-printf '%T@ %p\n' \
| sort -nr \
| head -1 \
| cut -d' ' -f2-
)
echo "$OPEN_METEO_MANIFEST"
今回の環境では、例えば、
data/manifests/openmeteo_jma_msm/latest_20260920T005852Z.json
が作成されています。
3時間積算は次のように実行します。
docker compose run --rm step2 \
rainband-step2 accumulate \
--manifest "/workspace/$OPEN_METEO_MANIFEST" \
--config /workspace/config/step2.yaml
Open-Meteo側のStep 1データが12時刻なら、期待する出力は、
input (12, 9, 9)
output (10, 9, 9)
です。
続いてStep 2 manifestを検証します。
RUN_ID=$(basename "$OPEN_METEO_MANIFEST" .json)
OPEN_METEO_STEP2_MANIFEST="/workspace/data/manifests/step2_accumulation/3h/openmeteo_jma_msm/${RUN_ID}.json"
docker compose run --rm step2 \
rainband-step2 verify \
--manifest "$OPEN_METEO_STEP2_MANIFEST"
最後にNumPyベンチマークも同じように実行できます。
docker compose run --rm step2 \
rainband-step2 benchmark \
--manifest "/workspace/$OPEN_METEO_MANIFEST" \
--repeat 5
Open-Meteo経路も実データで確認しました。結果は次のとおりです。
run_id latest_20260920T005852Z
window_hours 3
window_samples 3
shape (10, 9, 9)
max_mm 11.500
verify PASS
Step 1のOpen-Meteo入力は(12, 9, 9)だったので、3時間移動積算後の(10, 9, 9)は期待どおりです。
Zarr、PNG、Step 2 manifestも生成され、verifyまで正常に通りました。
これで、DIASとOpen-Meteoのどちらを入力にしても、同じStep 2処理を通せることを実データで確認できました。
性能計測については、実データサイズが大きいDIAS MSMを代表ケースとして採用しています。Open-Meteo側は今回9 × 9格子と小さいため、データ源ごとのbenchmarkをStep 2完了条件にはしていません。
16. DIASとOpen-Meteoの実機結果を並べる
最終的な実機確認結果をまとめると、次のようになりました。
| 入力 | 入力shape | 出力shape | 最大3時間雨量 | verify |
|---|---|---|---|---|
| DIAS MSM | (39, 505, 481) |
(37, 505, 481) |
219.719 mm |
PASS |
| Open-Meteo JMA MSM | (12, 9, 9) |
(10, 9, 9) |
11.500 mm |
PASS |
データの取得方法や格子サイズは違いますが、Step 2ではどちらも同じForecastRunを入力にして同じ処理を通しています。
ここで、Step 1でデータ取得部分を分離した効果を実データでも確認できました。
17. 実データサイズで考える
Step 1で取得したDIAS MSMは、
39 × 505 × 481
です。
float32の入力降水量だけなら約36 MBです。
今回の累積和は丸め誤差を抑えるためfloat64で計算するので、先頭の0面を含む累積配列は約74 MBになります。
3時間積算の出力は、
37 × 505 × 481
で、float32なら約34 MBです。
xarrayやZarrの管理情報を除いても、r001のMSM段階では通常のPCで十分扱える規模でした。
LFM 1 kmやLEPS 21メンバーへ進んだ段階で、必要なら計算方法やメモリ管理を見直します。
18. Step 2の完了条件
Step 2では、次をすべて通したら完了とします。
[✓] Docker build
[✓] Ruff format / check
[✓] mypy
[✓] pytest
[✓] Step 1 manifest verify
[✓] 時刻連続性検査
[✓] 欠測Grid検査
[✓] DIAS 3時間積算
[✓] DIAS 39時刻 → 37時刻
[✓] DIAS Zarr / PNG / manifest生成
[✓] DIAS Step 2 verify
[✓] NumPy benchmark(DIAS代表ケース)
[✓] Open-Meteo 3時間積算
[✓] Open-Meteo 12時刻 → 10時刻
[✓] Open-Meteo Zarr / PNG / manifest生成
[✓] Open-Meteo Step 2 verify
全体構成の記事で決めた、
指定した時間窓で積算雨量を作れる
欠測を検出できる
PNGまたはGeoTIFFで確認できる
という条件を、DIASとOpen-Meteoの両方で確認できました。
性能については、大きな実データであるDIAS MSMを代表ケースとして5回計測し、中央値95.184 msを記録しています。
Open-Meteo側は小さな9 × 9格子なので、個別benchmarkはStep 2完了条件から外しました。
以上をもって、r001 Step 2は正式にCOMPLETEとします。
19. 次は強雨域候補を抽出する
Step 2が完成すると、次はStep 3です。
Step 3では、今回作ったprecipitation_3hを入力にします。
ここから初めて、
強い雨がどこにまとまっているか
を調べる処理に入ります。
まとめ
Step 2では、Step 1で苦労した取得元の違いを意識する必要がなくなりました。
処理対象は、
ForecastRunの1時間降水量
だけです。
そのうえで、
時刻の穴を検出
欠測値を検出
3時間積算
Zarr保存
PNG確認
manifest
verify
までを一つの流れにしました。
実DIAS MSMでは、
39 × 505 × 481
↓
37 × 505 × 481
の3時間積算を正常に作成でき、最大3時間雨量は219.719 mmでした。
NumPy処理部分の中央値は95.184 msで、r001のMSM段階では十分軽いことも確認できました。
高速化を先に目的にせず、まずNumPy版を基準にした判断は、この段階では妥当だったと考えています。
Open-Meteo側も同じ共通処理で(12, 9, 9) → (10, 9, 9)、最大3時間雨量11.500 mm、verify PASSまで確認できました。
これでStep 2は正式に完了です。次はStep 3の強雨域候補抽出へ進みます。
参考資料
-
線状降水帯予測プロトタイプ r001 全体構成
https://qiita.com/rino_yume/items/171a7009633f8a11681c -
線状降水帯予測プロトタイプ r001 Step 1 完了編
https://qiita.com/rino_yume/items/055986af6f1569b0f90c -
NumPy
cumsum
https://numpy.org/doc/stable/reference/generated/numpy.cumsum.html -
xarray Zarr
https://docs.xarray.dev/en/stable/user-guide/io.html#zarr -
Open-Meteo JMA API
https://open-meteo.com/en/docs/jma-api

