無償データで72時間降雨予測を組み立てる:Step 1 取得条件とデータカタログを先に作る
はじめに
72時間先までの降雨予測を試すにあたり、最初はデータ取得プログラムから書こうと考えていました。ところが、配布元を調べていくと、同じ「雨量データ」でも時刻、格子、単位、ファイル名、利用条件がかなり違います。
とりあえずダウンロードしてから考える進め方では、次のような問題が後から出ます。
- 予報初期時刻と予報対象時刻を取り違える
- 積算降水量を別の予報ランと差分してしまう
- 同じファイル名でも製品世代によって格子が違う
- 無償データを、そのまま再配布できると思い込む
- 数GBのGRIB2やNetCDFをGitへ入れてしまう
- 学習を始めた後で、商用利用できないデータだと分かる
そこで今回は、外部データを1件も取得せず、取得前の台帳を先に作ることにしました。対象範囲、取得先、利用条件、ファイル名、時刻、格子、変数、保存先をYAMLへ記録し、JSON、CSV、GeoJSON、DuckDBへ展開します。
この記事ではStep 1までを扱います。実際のAMeDAS、MSM、GFS、ECMWF、DEMのダウンロードは次のStep 2で実装します。
本文中の利用条件と製品仕様は2026年8月8日に確認した内容です。実際に利用する
ときは、必ず配布元の最新ページを確認してください。
今回作るもの
プロジェクト名は次のようにしました。
RainfallForecast72h_FreeData_Step1_r001
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
Step 1の役割は、次の6点です。
- 対象流域または対象県を決める
- RISH、気象庁、NOAA、ECMWF、国土地理院の取得先を整理する
- 各データの利用条件と確認日を記録する
- ファイル名、時刻、格子、変数を確認する
- YAMLをカタログの原本にする
- 気象・地形データ本体をGitへ入れない仕組みを作る
処理の流れは次のとおりです。
対象地域は安倍川流域周辺とした
今回は、静岡県中部の安倍川流域周辺を初期対象にしました。安倍川は流路延長51km、流域面積567km²の河川です。山地から静岡市街地、駿河湾まで地形条件が連続しており、将来、地形性降雨の補正や流域平均雨量へ進めやすいと考えました。
ただし、Step 1では公式流域界をまだ取得していません。ここで設定した範囲は、気象格子やDEMの仕様確認に使う暫定矩形です。公式な安倍川流域界としては使いません。
# configs/target_area.yaml
target_area:
id: "abe_river_basin_shizuoka"
name_ja: "安倍川流域周辺"
# Step 1の確認用。公式流域界ではないことを明示しておく。
geometry_status: "provisional_bbox"
geometry_note: >-
Step 1のデータカタログ確認用に設定した暫定矩形。
公式な安倍川流域界ではない。
# 主に評価と表示に使う範囲。
analysis_bbox_wgs84:
west: 138.02
south: 34.83
east: 138.57
north: 35.56
# 雨域の移動と周辺地形を含めるため、取得時は少し広くする。
acquisition_bbox_wgs84:
west: 137.75
south: 34.55
east: 138.90
north: 35.85
検査プログラムでは、取得矩形が解析矩形を完全に含むか確認します。
また、geometry_status が official_polygon でない間は警告を残します。警告を消すことより、暫定範囲であることを見失わない方を優先しました。
登録したデータソース
今回のカタログには9製品を登録しました。
| 製品ID | 配布元・製品 | 主な用途 | Step 1の状態 |
|---|---|---|---|
rish_jma_msm_gpv |
RISH 過去MSM GPV | 過去予報と実況の対応作成 | 有効・未取得 |
rish_jma_gsm_gpv |
RISH 過去GSM GPV | 72時間側と広域場の検証 | 有効・未取得 |
rish_jma_radar_gpv |
RISH 全国合成レーダーGPV | 過去の雨域形状と移動 | 有効・未取得 |
jma_amedas_history |
気象庁 過去AMeDAS | 地点雨量による検証 | 有効・未取得 |
jma_high_resolution_gsm |
JMA High-Resolution GSM | 現在の72時間予報候補 | 既定で無効 |
noaa_gfs_0p25 |
NOAA GFS 0.25度 | 無償構成の基準予報 | 有効・未取得 |
ecmwf_ifs_open |
ECMWF Open Data IFS | GFSとの比較 | 有効・未取得 |
ecmwf_aifs_open |
ECMWF Open Data AIFS | AI予報との比較 | 有効・未取得 |
gsi_fundamental_geospatial_dem |
国土地理院 DEM | 標高・傾斜などの固定特徴量 | 有効・未取得 |
JMA High-Resolution GSM は登録制で、用途や再配布条件を確認してから使う必要が
あるため、YAMLには載せつつ enabled: false としました。候補から削除するのでは
なく、「なぜ今は使わないのか」をカタログへ残しています。
無償でも利用条件は同じではない
今回、いちばん先に整理したかったのがここです。「無償で取得できる」と「自由に再配布できる」は別です。
| 配布元 | カタログへ記録した扱い | 実装上の方針 |
|---|---|---|
| RISH | 非商用の学術研究・教育、第三者譲渡不可 | 原本をGitや配布ZIPへ入れない |
| 気象庁Web | 原則として公共データ利用規約第1.0版 | 出典と加工した旨を記録する |
| JMA High-Resolution GSM | 登録制、個別条件あり | 条件確認まで無効にする |
| NOAA | 米国政府作成情報は原則パブリックドメイン | NOAAの承認を受けたように見せない |
| ECMWF Open Data | CC BY 4.0とECMWF Terms of Use | 出典、著作権表示、変更内容を残す |
| 国土地理院 | 利用規約に加え、利用方法によって手続きが必要 | 公開方法を決める前に再確認する |
RISH
京都大学生存圏研究所の生存圏データベースは、過去のMSM、GSM、全国合成レーダーGPVを調べるうえで非常に便利です。一方、利用内規では用途が非商用の学術研究・教育に限られ、取得データの第三者への譲渡も行わないことになっています。
そのため、RISH由来の原本はローカルへ保存し、GitHubや記事の配布ZIPには含めない方針としました。将来、業務利用や商用サービスへ進む場合は、正規配信元へ切り替える前提です。
気象庁
気象庁ホームページのコンテンツは、個別の権利表記がない限り、公共データ利用規約第1.0版に準拠します。ただし、High-Resolution GSMのように別の利用条件を持つサービスは、一般のWebコンテンツと分けて記録します。
- 気象庁 過去の気象データ・ダウンロード
- 気象庁ホームページの利用について
- JMA High-Resolution GSM Data Service
- GISC Tokyo Terms and Conditions
NOAA
GFSは0.25度格子を含むGRIB2として公開され、ファイル名はgfs.tCCz.pgrb2.0p25.fFFF という形です。72時間先を十分に含むため、最初のリアルタイム基準予報に使いやすいデータです。
米国政府の情報は、個別の注記がない限りパブリックドメインとして扱われますが、出典は記載します。また、NOAAが本プロジェクトを推奨・承認しているような表現は使いません。
ECMWF
ECMWF Open DataのIFSとAIFSは、0.25度格子のGRIB2として公開されています。
Open Dataポータルは直近12ラン程度のローリング保存なので、リアルタイム取得を始めた後は自分の環境へ保存しないと過去予報が残りません。
国土地理院
基盤地図情報のDEMは、降雨の地形補正に使います。ダウンロードサービスは利用者登録が必要です。また、データを使った成果の公開方法によっては、測量法上の手続きを確認する必要があります。
利用条件の要約は法的な判断を自動化するものではありません。カタログにはreviewed_atと公式URLを持たせ、取得や公開の前に再確認できるようにしています。
ファイル名、時刻、格子、変数をカタログ化する
配布元ごとの説明を文章だけで残すと、取得コードを書くときに再調査が必要になります。そこで、製品YAMLに次の情報をまとめました。
# configs/sources/noaa.yaml の一部
- id: "noaa_gfs_0p25"
name_ja: "NOAA GFS 0.25度"
enabled: true
download_in_step1: false
files:
format: "GRIB2"
filename_patterns:
- id: "gfs_pgrb2_0p25"
description: "GFS 0.25度の標準ファイル名"
regex: '^gfs\.t(?P<cycle>\d{2})z\.pgrb2\.0p25\.f(?P<lead>\d{3})$'
example: "gfs.t00z.pgrb2.0p25.f072"
time:
run_cycles_utc: [0, 6, 12, 18]
maximum_forecast_hour: 384
target_forecast_hour: 72
run_time_source: "gfs.YYYYMMDD directory + filename cycle"
valid_time_rule: "run_time_utc + lead_hour"
grid:
crs: "latitude_longitude"
domain: "global"
nominal_resolution: "0.25 degree"
subset_bbox_key: "acquisition_bbox_wgs84"
variables:
- id: "apcp"
source_name: "APCP"
standard_name: "precipitation_amount"
unit: "kg/m2"
normalized_unit: "mm"
role: "target"
required: true
storage:
raw_subdir: "data/raw/noaa/gfs"
include_in_git: false
checksum: "sha256"
時刻は4項目に分ける
予報データでは、少なくとも次の4項目を分けます。
| 項目 | 内容 |
|---|---|
run_time_utc |
予報計算の初期時刻 |
valid_time_utc |
その値が対象とする時刻 |
lead_hour |
初期時刻から何時間先か |
received_at_utc |
自分のシステムが受信した時刻 |
保存はUTC、画面表示はAsia/Tokyoとしました。将来の学習データ作成では、received_at_utcより後に知り得た情報を入力へ混ぜないようにします。
JMAのFD表現も時間へ直す
GSMのファイル名には FD0201-0300 のような表現があります。これは2日1時間先から3日0時間先、つまり49~72時間先です。
# src/rainfall_catalog/filename_parser.py の一部
def fd_code_to_hour(value: str) -> int:
"""JMAの``FDddhh``表現を初期時刻からの時間へ変換する。
例として ``0300`` は3日0時間、すなわち72時間先を表す。
"""
if len(value) != 4 or not value.isdigit():
raise ValueError(f"FDコードは4桁の数字である必要があります: {value}")
day = int(value[:2])
hour = int(value[2:])
if hour >= 24:
raise ValueError(f"FDコードの時部分が不正です: {value}")
return day * 24 + hour
CLIから実際に確認できます。
PYTHONPATH=src python -m rainfall_catalog inspect-filename \
--root . \
--source-id rish_jma_gsm_gpv \
--filename Z__C_RJTD_20260808000000_GSM_GPV_Rjp_Gll0p1deg_Lsurf_FD0201-0300_grib2.bin
出力の一部は次のようになります。
{
"normalized": {
"run_time_utc": "2026-08-08T00:00:00Z",
"fd_start_hour": 49,
"fd_end_hour": 72
}
}
フォルダー構成
後続ステップで取得、切り出し、学習、API配信を追加しやすいように、設定とデータを分けています。
RainfallForecast72h_FreeData_Step1_r001/
├── configs/
│ ├── project_policy.yaml
│ ├── target_area.yaml
│ └── sources/
│ ├── rish.yaml
│ ├── jma.yaml
│ ├── noaa.yaml
│ ├── ecmwf.yaml
│ └── gsi.yaml
├── src/rainfall_catalog/
│ ├── models.py
│ ├── loaders.py
│ ├── validators.py
│ ├── filename_parser.py
│ ├── catalog_builder.py
│ ├── exporters.py
│ ├── online_check.py
│ ├── git_guard.py
│ └── cli.py
├── tests/
├── scripts/
├── docs/
├── data/
│ ├── raw/
│ ├── cache/
│ ├── interim/
│ └── processed/
├── catalog/
├── reports/
├── qiita/
├── Dockerfile
├── compose.yaml
└── pyproject.toml
YAMLを正とし、catalog/ と reports/ は再生成可能な成果物にしています。
原本をGitへ入れない
.gitignore だけに頼ると、別のディレクトリへ置いたファイルを見落とします。
今回は次の3段階で除外しました。
-
data/raw、cache、interim、processedを.gitignoreで除外 - 同じディレクトリを
.dockerignoreでも除外 -
guard-gitでGRIB2、NetCDF、DEM、圧縮ファイルの混入を検査
/data/raw/**
!/data/raw/.gitkeep
!/data/raw/README.md
/data/cache/**
/data/interim/**
/data/processed/**
*.grib2
*.grb2
*.nc
*.nc4
*.zarr/
*.tif
*.tiff
*.gml
*.zip
検査は次のコマンドで実行します。
PYTHONPATH=src python -m rainfall_catalog guard-git --root .
Git初期化済みなら git ls-files を確認し、未初期化の配布フォルダーではファイルシステムを走査します。.gitkeepと各ディレクトリのREADMEだけは許可します。
Docker Composeで実行する
Python 3.13のコンテナを使います。
docker compose build
まず設定を検査します。
docker compose run --rm validate
カタログを生成します。
docker compose run --rm catalog
テストとGit保護も実行します。
docker compose run --rm test
docker compose run --rm guard
今回の実装では16件のテストを用意し、次を確認しました。
- 9製品を重複なく読み込める
- Step 1で外部取得が有効になっていない
- 制限付きのJMA製品が既定で無効になっている
- 17種類のファイル名例が正規表現へ一致する
-
FD0300を72時間へ変換できる - 取得矩形が解析矩形を含む
- 原本ファイルの混入を検出できる
- DuckDBなしでもJSON、CSV、GeoJSON、SQLを生成できる
- URL確認が本文を全量取得しない
URLの疎通確認は任意にした
通常の validate と catalog はネットワークへ接続しません。公式サイトが一時的に停止していても、ローカルの設定検査は続けられます。
URLの誤記を確認したい場合だけ、次を実行します。
docker compose --profile manual run --rm check-urls
この処理はHEADを優先します。HEAD非対応のURLに限り、Rangeヘッダーを付けて先頭1byteだけを要求します。大容量GRIB2の取得処理としては使いません。
HTTP 200が返っても、利用許諾や製品仕様が正しいことまでは分かりません。疎通確認と利用条件の確認を分けたのは、そのためです。
生成されるカタログ
Dockerで catalog を実行すると、次のファイルを生成します。
| ファイル | 内容 |
|---|---|
catalog/data_catalog.json |
YAMLの情報をまとめた全体JSON |
catalog/data_sources.csv |
9製品の一覧 |
catalog/use_conditions.csv |
製品ごとの利用条件 |
catalog/endpoints.csv |
29件の取得・説明・規約URL |
catalog/file_patterns.csv |
17件のファイル名パターン |
catalog/variables.csv |
47件の取得・派生変数 |
catalog/target_area.geojson |
暫定解析矩形と取得矩形 |
catalog/catalog_schema.sql |
DuckDBを再構築できるSQL |
catalog/data_catalog.duckdb |
検索用のメタデータDB |
catalog/manifest.json |
設定と生成物のSHA-256 |
reports/catalog_validation.md |
人が読む検査結果 |
reports/catalog_validation.json |
CIなどで読む検査結果 |
DuckDBには、data_sources、use_conditions、endpoints、file_patterns、
variables、target_area を登録します。データ本体ではなく、取得前のメタデータだけです。
例えば、再配布不可または登録が必要な製品は次のSQLで確認できます。
SELECT
d.source_id,
d.name_ja,
u.commercial_use,
u.redistribution,
u.registration_required,
u.reviewed_at
FROM data_sources AS d
JOIN use_conditions AS u USING (source_id)
WHERE u.redistribution = 'prohibited'
OR u.registration_required = true
ORDER BY d.source_id;
ローカルPythonで確認する場合
Dockerを使わずに確認する場合は、次のように実行します。
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
python -m rainfall_catalog validate --root .
python -m rainfall_catalog build --root .
pytest -q
DuckDBを入れない軽い環境では、バイナリDBだけ省略できます。
PYTHONPATH=src python -m rainfall_catalog build \
--root . \
--skip-duckdb
この場合もJSON、CSV、GeoJSON、SQL、検証レポート、manifestは生成されます。
Step 1で確認できたこと
Step 1を先に作ったことで、次の点が明確になりました。
- 過去予報はRISHのMSM・GSMを中心に整理する
- 過去の雨域は全国合成レーダーGPVで確認する
- 地点での検証はAMeDASを使う
- リアルタイムの基準予報はGFSから始める
- ECMWF IFS・AIFSを比較対象にする
- JMA High-Resolution GSMは条件確認後に有効化する
- DEMは国土地理院の基盤地図情報を使う
- 原本は配布物へ含めない
- 時刻はUTCで統一し、4種類の時刻項目を分ける
- 暫定矩形は公式流域界として扱わない
データを1件も取得していませんが、次の実装で迷いやすい部分はかなり減りました。
次のステップ
次回のStep 2では、カタログを読み込んで実際にデータを取得します。最初から全期間、全国分を集めるのではなく、安倍川流域周辺と短い検証期間に限定します。
予定している内容は次のとおりです。
- NOAA GFSの最新ランを取得する
- ECMWF Open DataのIFS・AIFSを取得する
- 対象矩形と必要変数だけを切り出す
- 原本ごとに取得URL、時刻、サイズ、SHA-256をsidecar JSONへ残す
- 再試行、タイムアウト、途中ファイル削除を実装する
- GRIB2のメッセージ一覧を検査する
- 72時間分が欠けていないか確認する
- AMeDASと過去GPVの取得方法を段階的に追加する
Step 2でも、いきなり予測モデルは作りません。まず「同じ条件で取り直せること」と
「予報初期時刻から72時間先までを正しく並べられること」を確認します。

