Lesson 07

k-means による雨量観測所のクラスタリング — 時間プロファイル × 地理分布

v2-rewrite選択応用基礎教師なし学習ML入門
所要 100分 / 想定レベル: 応用基礎 / データ: 2データ横断: #1275 (雨量10分値 14日) + #1274 (観測所一覧 lat/lon)

学習目標

使用データ (2 データセット横断)

本レッスンは「観測所 = 特徴量ベクトル」と捉え、 14日間の時間プロファイル + 緯度経度 という異なる性格の特徴量を 1 つのクラスタリング問題として扱う。

データ取得手順

論題データセットDL保存先形式サイズ
雨量10分値 14日分 (各日別 CSV)DoBoX #1275ページから DL ボタンdata/rain_2024/rain_2024-MM-DD.csv (×14)CSV (5段ヘッダ, 10分値, UTF-8 BOM)1.0〜1.4 MB / 日
観測所一覧 (緯度経度)DoBoX #1274直DLdata/extras/stations_master.csvCSV (3行ヘッダ, 緯度経度・観測所属性)約 160 KB

個別取得(PowerShell, このレッスンだけ):

cd "2026 DoBoX 教材"
iwr "https://hiroshima-dobox.jp/resource_download/39775" -OutFile "data/extras/stations_master.csv"

一括取得(全レッスン共通, 推奨):

cd "2026 DoBoX 教材"
py -X utf8 data\fetch_all.py

fetch_all.py はカタログ・追加データを data/data/extras/ に再現可能ダウンロード。DoBoX のオープンデータは申請不要、商用・非商用とも利用可。本レッスンの .py スクリプトは、データが無ければ自動取得してから処理を始めるよう実装されていますensure_dataset() ヘルパ)。

スクリプト(全体ソースコード)

⬇ L07_kmeans_stations.py

cd "2026 DoBoX 教材"
python -X utf8 lessons/L07_kmeans_stations.py

方法

  1. 10分値 → 日合計 に集約 (resample("D").sum)。 さらに 14日合計が 1mm 未満の観測所は無降雨/欠測として除外。
  2. 7 次元 特徴量ベクトル を観測所ごとに構築: [sum_mm, max_day, std_day, peak_hour, wet_days, lat, lon]。 lat/lon は別途 #1274 から結合する。
  3. エルボー法シルエットスコア を k=2..10 で計算。 3 つの特徴量ビュー (雨量のみ / 空間のみ / 両方統合) で並べて、 最もシルエットが高い k を採用。
  4. 採用 k で k-means を 雨量特徴のみ に適用 → クラスタごとの 14 日プロファイル (small multiples) と 地理分布マップを描き、時間 ↔ 空間の対応を読む。
  5. 3 パターン (A) 雨量のみ / (B) 空間のみ / (C) 統合 で クラスタリングし、地図上に同時並列表示。 ARI (Adjusted Rand Index) で 3 結果の一致度を定量化。
  6. クラスタ × 特徴量 の z-score ヒートマップ で各クラスタの 「個性」 (どの特徴が高い/低い) を 1 枚で読めるようにする。

コード解説

L07_kmeans_stations.py 行 418–562

 1
 2
 3
 4
 5
 6
 7
 8
 9
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
import pandas as pd, numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, adjusted_rand_score
from _common import parse_rain_csv, ensure_dataset

# === (0) データ自動取得 ===
RAIN_RID = {"2024-06-29": 94490, ..., "2024-07-12": 94556}
for date, rid in RAIN_RID.items():
    ensure_dataset(f"data/rain_2024/rain_{date}.csv", resource_id=rid,
                   min_bytes=500_000)
ensure_dataset("data/extras/stations_master.csv",
               dataset_id=1274, resource_id=39775)

# === (1) 14日分の10分値を日合計に ===
rain_dfs   = [parse_rain_csv(f) for f in sorted(Path("data/rain_2024").glob("rain_2024-*.csv"))]
rain_full  = pd.concat(rain_dfs).sort_index()
rain_daily = rain_full.resample("D").sum(min_count=1)

# === (2) 7次元 特徴量ベクトル ===
feats = pd.DataFrame(index=rain_daily.columns)
feats["sum_mm"]    = rain_daily.sum(axis=0)
feats["max_day"]   = rain_daily.max(axis=0)
feats["std_day"]   = rain_daily.std(axis=0).fillna(0)
feats["peak_hour"] = rain_full.idxmax(axis=0).dt.hour.astype(float)
feats["wet_days"]  = (rain_daily >= 1.0).sum(axis=0)

# 観測所マスタ #1274 から lat/lon を結合
sm_raw = pd.read_csv("data/extras/stations_master.csv",
                     header=None, encoding="utf-8-sig")
sm = sm_raw.iloc[3:].copy()
sm.columns = sm_raw.iloc[2, :].tolist()
sm = sm[sm["データ種別"].astype(str)=="1"]   # 雨量
ll = sm.set_index("観測所名")[["緯度","経度"]].astype(float)
ll.columns = ["lat","lon"]
feats = feats.join(ll, how="inner")

# === (3) 3つの特徴量ビューを用意 ===
RAIN_F = ["sum_mm","max_day","std_day","peak_hour","wet_days"]
GEO_F  = ["lat","lon"]
X_rain = StandardScaler().fit_transform(feats[RAIN_F].values)
X_geo  = StandardScaler().fit_transform(feats[GEO_F].values)
# 統合は雨量を1.0、地理を0.7倍 → 雨量側を主役に保ちつつ地理も効かせる
X_both = np.hstack([X_rain,
                    StandardScaler().fit_transform(feats[GEO_F].values) * 0.7])

# === (4) エルボー + シルエットで k を選ぶ ===
KS = range(2, 11)
def scan(X):
    out = []
    for k in KS:
        km = KMeans(n_clusters=k, n_init=10, random_state=42).fit(X)
        out.append((k, km.inertia_, silhouette_score(X, km.labels_)))
    return pd.DataFrame(out, columns=["k","inertia","silhouette"])
sc_rain = scan(X_rain)
k_opt   = int(sc_rain.loc[sc_rain["silhouette"].idxmax(), "k"])

# === (5) k-means 本番 (雨量特徴ベース) ===
km = KMeans(n_clusters=k_opt, n_init=20, random_state=42).fit(X_rain)
feats["cluster"] = km.labels_

# === (6) クラスタ平均の時間プロファイル ===
for c in range(k_opt):
    members = feats[feats.cluster==c].index
    rain_daily[members].mean(axis=1).plot(label=f"c{c}", linewidth=2)

# === (7) 3パターンを ARI で比較 ===
lab_rain = KMeans(k_opt, random_state=42, n_init=20).fit_predict(X_rain)
lab_geo  = KMeans(k_opt, random_state=42, n_init=20).fit_predict(X_geo)
lab_both = KMeans(k_opt, random_state=42, n_init=20).fit_predict(X_both)
print("ARI rain↔geo  =", adjusted_rand_score(lab_rain, lab_geo))
print("ARI rain↔both =", adjusted_rand_score(lab_rain, lab_both))

結果

(左) エルボー法。3 ビューとも k を増やすと inertia は単調減少。(右) シルエットスコア。雨量特徴ビュー (青) で k* が選ばれる。
(左) エルボー法。3 ビューとも k を増やすと inertia は単調減少。(右) シルエットスコア。雨量特徴ビュー (青) で k* が選ばれる。
k-means (k=k*) クラスタ別 14 日プロファイル。薄線が個別観測所、太線がクラスタ平均。「いつ降ったか」のピーク日が違うことが分離の本質。
k-means (k=k*) クラスタ別 14 日プロファイル。薄線が個別観測所、太線がクラスタ平均。「いつ降ったか」のピーク日が違うことが分離の本質。
クラスタの地理分布。バブル径は 14 日合計 (mm)。時間プロファイルだけで分けたのに、地図上にも空間構造が現れる。
クラスタの地理分布。バブル径は 14 日合計 (mm)。時間プロファイルだけで分けたのに、地図上にも空間構造が現れる。
(A) 雨量のみ (B) 空間のみ (C) 統合 の 3 パターン比較。(B) は地理ブロック、(A) は時間パターン、(C) はその折衷。
(A) 雨量のみ (B) 空間のみ (C) 統合 の 3 パターン比較。(B) は地理ブロック、(A) は時間パターン、(C) はその折衷。
クラスタ × 特徴量 の z-score ヒートマップ。赤=平均より高い、青=低い。各クラスタの「個性」を 1 枚で把握。
クラスタ × 特徴量 の z-score ヒートマップ。赤=平均より高い、青=低い。各クラスタの「個性」を 1 枚で把握。

クラスタサマリー (k = k*)

クラスタ 観測所数 14日合計 平均(mm) 最大日 平均(mm) 雨日数 平均 ピーク時刻 平均(時) 緯度 平均 経度 平均
0 22 201.0 75.3 5.68 20.4 34.619 133.038
1 52 199.0 66.6 6.69 3.4 34.346 132.851
2 31 281.6 91.5 7.23 20.2 34.565 132.449
3 100 219.7 74.3 7.00 20.3 34.703 132.804
4 36 174.6 53.8 7.06 20.4 34.673 132.691
5 88 253.6 95.8 7.07 3.2 34.416 132.607
6 36 216.9 71.4 8.39 19.2 34.825 133.066
7 27 323.0 136.2 7.04 5.6 34.358 132.344

3 パターンの一致度 (ARI)

比較Adjusted Rand Index
(A) 雨量のみ vs (B) 空間のみ0.130
(A) 雨量のみ vs (C) 統合0.679
(B) 空間のみ vs (C) 統合0.211

ARI は 2 つのクラスタリング結果の一致度 (1.0 で完全一致, 0 でランダム同等, 負で逆相関)。雨量と空間が低 ARI なら「時間パターンと地理は別軸の情報」、高 ARI なら「地理が時間パターンを支配」と解釈できる。

考察

発展課題

  1. k-means → DBSCAN / HDBSCAN: 球状クラスタを仮定しない 密度ベース手法に置き換えると、外れ値観測所 (山頂・離島) が ノイズとして抽出される。
  2. 階層クラスタリング + デンドログラム: scipy.cluster.hierarchy.linkage で観測所間の 類似度ツリーを作り、k を「ツリーをどこで切るか」で選ぶ。
  3. 動的時間伸縮 (DTW) で時系列クラスタリング: ピーク日が 1 日ずれるだけで Euclid 距離は急増するため、 DTW で「形が似ているか」を見ると別の知見が出る。
  4. クラスタの時間安定性: 同じ手続きを 7 月・8 月・9 月で それぞれ実行し、観測所のクラスタ所属が 季節間でどれだけ 安定か を Adjusted Rand Index で評価する。
  5. folium で対話マップ: lat/lon があるので folium.CircleMarkerDoBoX のオープンデータを可視化し、クラスタ凡例付きで HTML 埋め込みする (L076-L079 参照)。
  6. クラスタ → 流域連携: L080 (水系単位の雨量×水位ラグ) と接続。クラスタを水系メタ (#1276) で塗り分ければ、 「気象学的クラスタ」と「流域単位」のズレが見える。