Lesson 01

DoBoX カタログを多角的に俯瞰する — 551件のメタデータから主題を抽出

v2-rewrite導入基礎リテラシ
所要 90分 / 想定レベル: リテラシ導入 / データ: DoBoX データカタログ 全 551 件 (Id, Title, Desc)

データ取得手順

このスクリプトは初回実行時にデータを自動取得します(DoBoX からの直接ダウンロード)。

IDデータセット名
#42避難所情報
#794都市計画区域情報_区域データ_広島市_各種用途地域
#888都市計画区域情報_区域データ_安芸高田市_行政区域
#922都市計画区域情報_区域データ_広島県_行政区域
#1275観測情報_雨量日集計
#1279県内のカメラ情報
#1437観測情報_水位日集計
#1670埋蔵文化財包蔵地一覧表(水中遺跡)

実行コマンド:

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

DoBoX のオープンデータは申請不要・商用/非商用とも利用可。 data/extras/.gitignore 対象(約 57 GB のキャッシュ)。 スクリプト実行で自動再生成されます。

学習目標と問い

このレッスンで答えたい問い

「広島県は何を観測・整備し、どこに偏在しているか」を、カタログのメタデータと2つの実データから読めるか?

用語の定義(このレッスン独自)

  • 「ドメイン」: 本レッスン用に手動で定義した5主題辞書(防災・観測・地形・施設・文化財)。 DoBoX の公式分類ではない。各ドメインに5つの代表キーワードを設定し、タイトルor説明文に含むかで集計する。
  • 「シリーズ」: タイトル先頭が同じ命名規則を持つデータセット群(例: 「都市計画区域情報_*」が309件で1シリーズ)
  • 「クラスタ」: TF-IDF + k-means が機械的に分けたグループ(手動辞書とは別)

立てた仮説

  1. H1(辞書ドメイン偏在): 5ドメインの件数は 観測と防災に偏るはず(2018年西日本豪雨以降の防災DX投資の反映)
  2. H2(市町別の整備対称性): 各市町の カメラ数(災害監視整備)避難所数(受入整備)はある程度比例するはず(人口比に従う)。 外れ値の市町があれば、整備偏りの政策的意義が読める
  3. H3(雨量分布の長尾性): 2024-07-01 豪雨日の観測所別 日合計雨量は 右に長い裾を持つ分布のはず(大半は数十mm、少数の観測所が200mm超を記録)
  4. H4(手動辞書 ≒ 自動クラスタ?): 手動の5ドメイン辞書と TF-IDF+k-means の自動クラスタが 似た構造を捉えるかを検証。 ずれていれば、辞書が見落とした主題を機械が発見している証拠

到達点

使用データ

ダウンロード(再現用データ・中間データ・図)

本レッスンの全成果物に直リンクを置いた。途中ステップから再現したい学習者向け。

1. 生データ(DoBoX 由来)

ファイル形式サイズ取得元
data/dataset_index.csv CSV (3列: Id/Title/Desc)約 280 KB / 551 行 DoBoX /datasets ページ群
data/camera_list.csv CSV (緯度・経度・住所・路河川名)約 70 KB / 351 行 DoBoX #1279
data/shelters.json JSON (4,065件、災害種別フラグ等)約 4 MB DoBoX #42
data/rain_2024/rain_2024-07-01.csv CSV (10分値, 5段ヘッダ)約 1.2 MB DoBoX #1275 res 94500

2. プログラムで生成される中間データ

ファイル内容使う分析
L01_keyword_counts.csv 5ドメイン×5キーワード=25行の出現件数図1 辞書ドメイン頻度
L01_city_camera_shelter.csv 市町別 カメラ数・避難所数図2 散布図
L01_rain_daily_totals.csv 2024-07-01 観測所別 日合計雨量図3 分布
L01_cluster_assignments.csv 551件 × k-meansクラスタ番号 (k=6)図4 クラスタ
L01_cluster_summary.csv 6クラスタの代表トークン上位8図4 表3
L01_domain_cluster_cross.csv 手動ドメイン × 自動クラスタのクロス集計図5 一致度

3. 図 PNG

4. 再現スクリプト

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

スクリプト本体: lessons/L01_catalog_overview.py

分析1: ドメイン辞書のキーワード頻度

狙い

5主題(ドメイン)の辞書を自分で作って、カタログ551件にどの主題が多いかを集計する。 辞書設計の正当性を確認するための入口分析。

用語: 「ドメイン」とは本レッスンで手動定義した5主題辞書(防災・観測・地形・施設・文化財)。 DoBoX の公式分類ではない。各ドメインに5つの代表キーワードを設定し、タイトルor説明文に含むかを集計する。

手法(str.contains 集計)

実装

L01_catalog_overview.py 行 546–577

 1
 2
 3
 4
 5
 6
 7
 8
 9
555
556
557
558
559
DOMAIN_KEYWORDS = {
    "防災・ハザード": ["浸水","土砂","津波","地震","避難"],
    "観測・センサ":   ["雨量","水位","潮位","カメラ","観測情報"],
    "地形・地盤":     ["標高","傾斜","点群","ボーリング","CS立体"],
    "インフラ・施設": ["道路","橋","ダム","ため池","盛土"],
    "文化財・歴史":   ["埋蔵文化財","被爆","城","遺跡","観光"],
}
records = []
for domain, kws in DOMAIN_KEYWORDS.items():
    for kw in kws:
        # OR集計: タイトルor説明文のどちらかにヒットすれば1
        hit = idx["Title"].str.contains(kw, na=False) | idx["Desc"].str.contains(kw, na=False)
        records.append({"domain": domain, "keyword": kw, "count": int(hit.sum())})
kw_df = pd.DataFrame(records)

結果(図と読み取り)

なぜこの図か: ドメイン間の件数の相対比較を一目で見たい。 横棒は数値の順序を保ちながらラベル名も読める。色分けでドメイン内ばらつきと間の差を同時に表示。

図1: 5ドメイン × 5キーワードの出現件数(タイトル or 説明文一致)
図1: 5ドメイン × 5キーワードの出現件数(タイトル or 説明文一致)

この図から読み取れること:

結果(表と読み取り)

なぜこの表か: 図1 の数値ばらつきをドメイン単位で集約し、5ドメインの相対サイズを1行ずつ確認。

表1: ドメイン別 延べ件数(5キーワード合計)
延べ該当件数
domain
防災・ハザード 106
観測・センサ 18
地形・地盤 24
インフラ・施設 22
文化財・歴史 21

観測・防災が最厚、文化財が最薄。延べカウントなので絶対数より順序を読む。

表2: キーワード頻度 上位10
domain keyword count
防災・ハザード 浸水 67
防災・ハザード 土砂 35
文化財・歴史 埋蔵文化財 11
地形・地盤 標高 10
インフラ・施設 道路 10
観測・センサ 観測情報 9
地形・地盤 点群 5
地形・地盤 傾斜 5
インフラ・施設 ため池 4
文化財・歴史 遺跡 4

上位は防災・観測・施設の混合。「ボーリング」「CS立体」のような専門語は下位 — 件数が少ない=重要でない、ではない。

分析2: 市町別 カメラ数 × 避難所数

狙い

「災害監視整備(カメラ)が多い市町は受入整備(避難所)も多いか?」 人口比例の対称性が成立するか、外れる市町はどこかを見る(仮説H2)。

手法(市町抽出 + Joint plot)

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import json

cam = pd.read_csv("data/camera_list.csv", encoding="utf-8-sig")
with open("data/shelters.json", encoding="utf-8") as f:
    sh = pd.DataFrame(json.load(f)["items"])

def extract_city(addr):
    s = str(addr).replace("広島県", "")
    m = re.match(r"^([^市区町村]+[市町村])", s)
    return m.group(1) if m else None

cam["city"] = cam["住所"].apply(extract_city)
sh["city"] = (sh["municipalityName"].astype(str)
              .str.replace("広島県", "", regex=False)
              .apply(lambda x: re.match(r"^([^市区町村]+市)", x).group(1)
                     if re.match(r"^([^市区町村]+市)", x) else x))

cam_count = cam.groupby("city").size().rename("camera_count")
sh_count  = sh.groupby("city").size().rename("shelter_count")
counts = pd.concat([cam_count, sh_count], axis=1).fillna(0).astype(int)

# Joint plot (中央=散布, 上=カメラ数ヒスト, 右=避難所数ヒスト) を GridSpec で組む

結果(図と読み取り)

なぜこの図か: 2変数の関係(散布図)と各変数単独の分布(周辺ヒスト)を1枚で見たい。 期待比例線を引くことで「どの市町がバランスから外れるか」が一目で分かる。

図2: 市町別 カメラ数 × 避難所数 散布(DoBoX #1279, #42 の結合)
図2: 市町別 カメラ数 × 避難所数 散布(DoBoX #1279, #42 の結合)

この図から読み取れること:

結果(表と読み取り)

表3: 市町別 件数 上位10(避難所数降順)
camera_count shelter_count
city
広島市 79 939
呉市 21 518
福山市 39 474
東広島市 17 279
尾道市 3 273
庄原市 20 247
江田島市 2 131
三原市 5 128
府中市 17 120
廿日市 14 120

広島市が他市町を圧倒的に上回る。カメラ数では呉市・福山市・三次市など複数市が拮抗。

分析3: 観測所の日合計雨量分布(2024-07-01)

狙い

「ある豪雨日に観測所間でどれくらい雨量がばらつくか」を分布の形で見る。 L05/L06 の時空間分析への伏線。仮説H3(右の長尾)を検証。

手法(雨量CSV → 日合計 → ヒスト)

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from _common import parse_rain_csv

tidy = parse_rain_csv("data/rain_2024/rain_2024-07-01.csv")  # (144, 約400)
day_total = tidy.sum(axis=0)
day_total = day_total[day_total > 0].sort_values(ascending=False)

fig, axes = plt.subplots(1, 2, figsize=(13, 4.6))
axes[0].hist(day_total, bins=40)
axes[0].axvline(day_total.median(), ls="--", label=f"中央値 {day_total.median():.0f}mm")
axes[0].axvline(day_total.quantile(0.95), ls="--", label=f"P95 {day_total.quantile(0.95):.0f}mm")
axes[1].barh(range(15), day_total.head(15).values)

結果(図と読み取り)

なぜこの図か: 連続値の偏在を見るにはヒストグラムが定石。 中央値・P95 を縦線で示すと「典型観測所」と「特異な多雨観測所」の境界が見える。 右の上位15バーは「ヒスト右端に何があるか」の正体を示す補助。

図3: 2024-07-01 観測所別 日合計雨量の分布と上位15観測所
図3: 2024-07-01 観測所別 日合計雨量の分布と上位15観測所

この図から読み取れること:

結果(表と読み取り)

表4: 上位15観測所 日合計雨量
日合計mm
矢草(砂防) 173.0
馬の口 168.0
小瀬川ダム 167.0
矢草北 159.0
景浦 147.0
阿戸 145.0
矢草南 145.0
野貝原(砂防) 141.0
津田(国) 141.0
地御前(砂防) 138.0
楠那 138.0
中道(国) 135.0
135.0
五日市観音 135.0
安芸 133.0

上位観測所の日合計は中央値の数倍に達する。これらが豪雨災害の引き金となる地点。

分析4: テキスト自動グルーピング(TF-IDF + k-means)

狙い

「タイトル+説明文が似ているデータセット同士を、機械が自動で同じグループにまとめる」。 551件を人手で読まずにカタログの主題構造を掴む(仮説H4)。

準備: なぜテキストを「数値の表」に変えるのか

機械は文字列のまま「似ている/似ていない」を比較できない。 そこで各データセットを 「数値の並び(=表の1行)」に変換する。 各列は1単語、各セルは「その単語がそのデータセットでどれくらい特徴的か」の数値。 こうすれば行同士の距離を計算でき、近い行を機械的にまとめられる。

本ツールは2ステップを続けて通す:

ステップ担当入力出力
STEP 1TF-IDFテキスト 551件551行 × N列の数値の表(N=語彙数, 数千)
STEP 2k-meansSTEP 1 が作った表551個のグループ番号(0〜5)

STEP 1: TF-IDF(読み: ティー・エフ・アイ・ディー・エフ)

役割: テキストを「特徴的な単語の数値表」に変換する。

列数(語彙数 N)はいくつ? 本レッスンでは min_df=3, max_df=0.6 でフィルタした結果、 N ≒ 数千。具体的な値は実行時に決まる(コーパス依存)。 8 ではない。下の表で見せる「上位8語」は、数千列の中から最も値が大きい 8 列だけ取り出したもの。 残りの数千列はほとんど 0(ゼロ)になる「疎な表」。

STEP 1 の出力イメージ(実際は 551行 × 数千列、ここでは見やすいように上位語の列だけ抜粋):

データセット ↓ \ 単語 →雨量観測集計河川浸水都市計画埋蔵...(残り数千列)
#1275 観測情報_雨量日集計0.520.450.380.000.000.000.00...
#1437 観測情報_水位日集計0.100.420.350.000.000.000.00...
#794 河川浸水想定_太田川0.000.000.000.410.480.000.00...
#922 都市計画区域_広島市0.000.000.000.000.000.550.00...
#1670 埋蔵文化財_その他0.000.000.000.000.000.000.62...
...(残り 546 行分)...

※ 値は概念例。実際は scipy.sparse の疎行列で持ち、ほぼゼロのセルは保存しない。

表の見方:

STEP 1 の具体例(1行だけクローズアップ): ID=1275「観測情報_雨量日集計」の行で、値が大きい上位8列だけ取り出した:

単語TF-IDF 値解釈
雨量0.52このデータで頻出 + 他では珍しい → 高得点
観測情報0.45同上
集計0.38同上
観測所0.31同上
10分0.2710分値という単位の語
日合計0.21集計単位の語
広島県0.04ほぼ全データに出る → 低得点
データ0.02同上、ほぼ無視されるレベル
...残り数千列ほぼ 0このデータに関係ない語

※ 値は概念例。表示は上位8語だけだが、列数(次元数)は数千。

STEP 1 はここで終わり。551件すべてに適用すると、上の「551行×数千列の表」が完成する。

STEP 1 の実装

L01_catalog_overview.py 行 752–839

 1
 2
 3
 4
 5
 6
 7
 8
 9
761
762
763
764
765
766
767
768
769
def jp_tokens(s):
    """漢字塊そのもの+bi-gram 展開で形態素解析なしの簡易トークン化"""
    s = str(s)
    toks = re.findall(r"[゠-ヿーー]{2,}", s)              # カタカナ語
    for chunk in re.findall(r"[一-鿿]{2,}", s):          # 漢字塊
        if len(chunk) <= 6:
            toks.append(chunk)                          # 塊全体
        for i in range(len(chunk) - 1):
            toks.append(chunk[i:i + 2])                 # bi-gram
    toks += re.findall(r"[A-Za-z]{2,}", s)              # 英字
    return toks

texts = (idx["Title"] + " " + idx["Desc"]).tolist()
joined = [" ".join(jp_tokens(s)) for s in texts]

# TF-IDF: ここまでが STEP 1
vec = TfidfVectorizer(token_pattern=r"\S+", min_df=3, max_df=0.6)
X = vec.fit_transform(joined)               # 551 × 語彙数 の疎行列 (= 551本のベクトル)

STEP 2: k-means(読み: ケー・ミーンズ)

役割: STEP 1 で得た 551本のベクトルを k 個のグループに分ける。 本レッスンでは k=6(人が指定)。

動作のイメージ:

  1. k 個の「中心」をランダムに置く
  2. 551本のベクトル各々を「最も近い中心」に割り当てる → k グループに分かれる
  3. 各グループ内ベクトルの平均を取って中心を更新する
  4. 2-3 を中心が動かなくなるまで繰り返す

STEP 2 の具体例: STEP 1 で作った 551本のベクトルがどのグループに入ったか:

データセットSTEP 1 の特徴語(高得点)STEP 2 のグループ
#1275 観測情報_雨量日集計雨量、観測、集計C0(観測情報族)
#1437 観測情報_水位日集計水位、観測、集計C0(同じグループ)
#794 河川浸水想定区域情報_計画規模_太田川河川、浸水、想定、太田川C1(河川浸水想定)
#922 都市計画区域情報_区域データ_広島市都市計画、区域、広島C2(都市計画区域)
#1670 埋蔵文化財包蔵地一覧表_その他埋蔵文化財、包蔵、一覧C5など(小グループ)

STEP 1 で似た特徴語を持つデータセットが、STEP 2 で同じグループ番号に集まる。これがツールの骨格。

STEP 2 の実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
779
780
from sklearn.cluster import KMeans

# X = STEP 1 の出力 (551 × 語彙数)
km = KMeans(n_clusters=6, n_init=10, random_state=42)
labels = km.fit_predict(X)                  # 551個の整数 (0..5)

# 各クラスタの代表トークン (中心ベクトルで値が大きい語)
terms = np.array(vec.get_feature_names_out())
for c in range(6):
    top = terms[km.cluster_centers_[c].argsort()[::-1][:8]]
    print(f"cluster {c}: {top}")

結果(図と読み取り)

なぜこの図か: クラスタ別のサイズ(左バー)代表語(右テーブル)を並列で見たい。 代表語を見ることで「このグループは何の集まりか」を人間が解釈できる。

図4: 自動グルーピング結果 — クラスタサイズ(左)と代表語上位8(右)
図4: 自動グルーピング結果 — クラスタサイズ(左)と代表語上位8(右)

結果(表と読み取り)

表5: 各グループの代表語と件数(解釈付き)
クラスタ 件数 代表トークン (上位8)
C0 32 警戒 戒区 災害 別警 特別 砂災 土砂 害警
C1 90 河川 管理 想定 浸水 場合 氾濫 規模 水想
C2 268 広島 都市計画区域 市街 街化 規制 域外 島市 行政区域
C3 40 用現 利用 現況 物利 建物利用現況 土地利用現況 土地 地利
C4 53 用途 途地 地域 線引 非線 非線引 種用 各種用途地域
C5 68 雪崩 状況 険箇 危険 所情 箇所 地開 発状

この結果から読み取れること:

このツールの限界

分析5: ドメイン辞書 × k-meansクラスタ クロス集計

狙い

「手で作った辞書(分析1のドメイン)と、機械が見つけたグループ(分析4のクラスタ)はどれくらい一致するか?」 完全一致なら辞書が冗長、ずれていれば機械が辞書外の構造を発見した証拠。仮説H4の補完検証。

手法(クロス集計 + ヒートマップ)

実装

L01_catalog_overview.py 行 847–889

 1
 2
 3
 4
 5
 6
 7
 8
 9
856
857
858
859
860
861
862
863
864
865
# 各ドメインに「いずれかのキーワード」がヒットする文書集合
dom_hits = {}
for domain, kws in DOMAIN_KEYWORDS.items():
    mask = pd.Series(False, index=idx.index)
    for kw in kws:
        mask = mask | idx["Title"].str.contains(kw, na=False) | idx["Desc"].str.contains(kw, na=False)
    dom_hits[domain] = mask.values
# どの辞書にもヒットしない = 辞書外
no_dom = ~np.any(np.column_stack(list(dom_hits.values())), axis=1)
dom_hits["(辞書外)"] = no_dom

# 6行 × 6列 のクロス集計
domains_order = list(DOMAIN_KEYWORDS.keys()) + ["(辞書外)"]
cross_mat = np.zeros((len(domains_order), 6), dtype=int)
for i, d in enumerate(domains_order):
    for c in range(6):
        cross_mat[i, c] = int(((labels == c) & dom_hits[d]).sum())

ax.imshow(cross_mat, cmap="YlOrRd")  # 濃いセル = 辞書ドメインがクラスタに集中

結果(図と読み取り)

なぜこの図か: 手動辞書と機械分類を比較するとき、クロス集計のヒートマップが直感的。 行×列のセル濃度で「どこに何件集まるか」が一目で分かる。 セルが対角的に集中するほど辞書とクラスタの一致度が高い。

図5: 手動ドメイン辞書 × 教師なしk-meansクラスタの一致度
図5: 手動ドメイン辞書 × 教師なしk-meansクラスタの一致度

結果(表と読み取り)

表6: ドメイン × クラスタ クロス集計(数値)
C0 C1 C2 C3 C4 C5
防災・ハザード 32 66 7 0 0 0
観測・センサ 0 0 12 0 0 0
地形・地盤 0 0 18 0 0 0
インフラ・施設 0 5 17 0 0 0
文化財・歴史 0 0 16 0 0 0
(辞書外) 0 21 205 40 53 68

この結果から読み取れること:

仮説検証と考察

仮説と結果の照合

#仮説判定根拠
H1辞書ドメインの件数は観測・防災に偏る支持 図1 と表1 で観測・防災が高位、文化財が最薄。 2018年西日本豪雨以降の防災DX投資が反映していると解釈できる。
H2市町別 カメラ数 × 避難所数 が比例する部分支持 図2 で正の相関は見えるが線形比例ではない。 広島市が避難所数で突出する一方、中山間市町ではカメラだけが多い等の偏りが観察される。 市町の地理特性と整備履歴によって整備パターンが分岐する。
H32024-07-01 の日合計雨量分布は右の長尾を持つ支持 図3 のヒストグラムは強い右非対称。中央値と P95 の比が大きく、 ごく少数の観測所が極端な雨量を記録している(豪雨の局所集中)。
H4手動ドメイン辞書と教師なしクラスタは似た構造を捉える部分支持 図5 で観測ドメインはクラスタに集中する一方、 都市計画区域系のような最大シリーズは「辞書外」に大量に落ち、機械学習が独立クラスタを発見した。 辞書設計の見直しが必要というメタな学び。

考察

発展課題(結果から導かれる新たな問い)

各課題は、上の結果新たな仮説に裏打ちされている。 「なぜこの課題か」を結果と接続して提示する。

  1. 市町整備の人口正規化
    • 結果X: 図2 で広島市が避難所数で突出。生件数比較は人口規模に大きく影響される
    • 新仮説Y: 人口10万人あたりに正規化すれば、中山間市町の方が 1人あたり整備量 が高いかもしれない(過疎地ほど避難所が必要)
    • 課題Z: e-Stat や RESAS から市町別人口を取得し、人口正規化した「カメラ密度」「避難所密度」で再描画
  2. 豪雨日の空間配置(L06 への接続)
    • 結果X: 図3 で日合計雨量上位15観測所が長尾を作るが、観測所名だけでは地形配置が読めない
    • 新仮説Y: 上位観測所は山間部の谷筋に局在する(地形性降水)。空間配置を見れば梅雨前線の通過パターンも見えるはず
    • 課題Z: L06 (2024-07-01 KDE small multiples) で時空間進行を可視化。本レッスンの上位15観測所が時間軸でいつ降ったかを照合
  3. クラスタ数 k の最適化
    • 結果X: 図5 で都市計画区域系が「辞書外」に大量に落ち、図4 では k=6 で 1クラスタに集約される
    • 新仮説Y: k を上げれば(例 k=10〜15)、都市計画内部の建物利用/土地利用/人口 等のサブ主題が分離するはず
    • 課題Z: シルエット係数や Davies-Bouldin 指数で k=2..15 を比較。最適 k で再クラスタ → 辞書外の中身を再分類
  4. 辞書設計の改善
    • 結果X: 図5 で「辞書外」が大きい — 5主題辞書ではカタログの大半をカバーしていない
    • 新仮説Y: 「都市計画」「規制」「環境」を6主題目として加えれば、辞書外が大幅に減るはず
    • 課題Z: クラスタ代表トークンを参考に辞書を拡張し、図5 を再描画。辞書外が10%未満になるかを検証
  5. 意味埋め込みベースのクラスタ
    • 結果X: TF-IDF は文字一致なので「治水」と「水害」のような同義語を別クラスタに分けてしまう
    • 新仮説Y: 多言語SBERT(paraphrase-multilingual-MiniLM)で埋め込めば、同義語が同じベクトル空間で近づき、主題的純度が上がるはず
    • 課題Z: 説明文を SBERT 埋め込み → UMAP で2次元化 → 本レッスンの TF-IDF クラスタと色分け重ね描画して比較