# -*- coding: utf-8 -*-
"""L62 避難情報 単独 3 研究例分析
       — 広島県内の避難指示・避難勧告等発令履歴を
         3 つの独立した研究角度で読み解く

カバー宣言:
  本記事は DoBoX のシリーズ「避難情報」 1 件
  (dataset_id = 41) を <b>単独</b>で取り上げ、
  広島県内で発令される避難情報 (避難指示・避難勧告・高齢者等避難等) を
  3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
  Phase 5 災害対応系の起点 = 「避難発令」 という防災の意思決定そのものを
  正面から扱う。

  「避難情報」 とは:
    広島県危機管理課が運営するオープンデータ API の
    <b>現時点で発令中の避難情報スナップショット</b>。
    平常時は空 (items=[], total=0)、災害発生時に該当市町・地区別に
    警戒レベル付き (L1〜L5) の避難情報が JSON 配列で出現する
    リアルタイムデータセット。

    本リソース (resource_id=110, JSON, 34 bytes 平常時) のスキーマは:
      - items: 発令中の避難情報の配列
        - 発令日時 / 市町 / 警戒レベル / 避難種別 / 対象地区 / 解除日時
      - total: 発令件数

  既存防災レイヤー (L03 / L61) との関係:
    L03 (避難所情報) は<b>場所 (4,065 件の固定施設位置)</b> を扱った。
    L61 (過去災害) は<b>事実 (518 件の発生地点記録)</b> を扱った。
    本記事 L62 は<b>意思決定の発令 (動的タイムライン)</b> を扱う。
    防災 3 系統 = <b>場所 (L03) + 事実 (L61) + 発令 (L62)</b> という
    災害対応のライフサイクル全体が初めて教材化される。

  本データ #41 の独自性と限界:
    - <b>リアルタイム API (= 平常時は空)</b>: dataset 41 の JSON は
      災害発令中のみ items に内容が入る。本日 (2026-05-10) の取得結果は
      total=0 件 (= 県内に発令中の避難情報は無い平常状態)。
      これは「災害が起きていない = 良い状態」 を示す貴重な負例 (= 平常基準) でもある。
    - <b>履歴アーカイブ非保持</b>: 過去発令はこの API 経由では取得できない
      (発令解除と同時に items から消える)。歴史的研究には<b>別途公的資料に基づく
      再構成データセット</b>が必要。
    - <b>対策</b>: 本研究では (a) DoBoX 本体の API スキーマを正確に文書化、
      (b) 広島県危機管理課・各市町災害対策本部報告書・気象庁災害時報告書等の
      <b>公的に発表された大規模災害時の発令履歴</b>を再構成した
      「<b>避難発令履歴アーカイブ</b>」 (research reconstruction) を
      L62_evacuation_history_reconstructed.csv として収録し、
      これを基に研究を進める。再構成データには出典 (URL) と再構成手順を明示。

研究の問い (3 RQ):
  RQ1 (主研究): 広島県の<b>避難情報発令の頻度・時系列構造</b>はどう描けるか?
       2014-2021 の主要災害イベント期間における県内避難情報発令件数を
       <b>年別・月別・警戒レベル別・市町別</b>で多角度集計。
       特に 2021 年 5 月の<b>災害対策基本法改正 (警戒レベル運用開始)</b> 前後で
       発令の様態がどう変わったかを比較。

  RQ2 (副研究 1): <b>2018 西日本豪雨の発令タイミング</b> — 災害発生との時間関係
       2018 年 7 月 5-8 日豪雨において、各市町が
       <b>いつ・どの警戒レベルで・どの地区に</b> 避難情報を発令したか。
       気象庁の大雨特別警報発表時刻と発令時刻の<b>タイムラグ</b>を計算し、
       「警報発表 → 避難発令」 の意思決定速度を市町間で比較。
       2018 では<b>発令していても被害が出た</b>市町と<b>発令前に被害が起きた</b>
       市町の存在が国会答弁・有識者会議資料で議論されたが、本研究で空間的に再現する。

  RQ3 (副研究 2): <b>人口対比の発令頻度</b> — 市町別の発令傾向差
       L22 既扱の市町別人口 (R2 国勢調査) を母数とし、
       単位人口当たりの発令件数 (per 10,000) を市町別に算出。
       <b>沿岸 vs 内陸 vs 都市部</b>で発令傾向に差があるか。
       「過剰発令」 (= 人口に対して発令件数が極端に多い)
       「過小発令」 (= 災害履歴は多いが発令件数が極端に少ない) の
       地理パターンを抽出し、各市町の<b>警戒姿勢</b> (cautious vs aggressive)
       を空間統計で可視化。

仮説 (5):
  H1 (二大災害集中, RQ1): 再構成された 2014-2021 の発令件数のうち
       <b>2018 西日本豪雨 + 2014 広島市土砂 ≥ 50%</b> を予想。
       近年の 2 大豪雨が発令量も支配する偏在型分布。

  H2 (法改正後の頻度増, RQ1): 2021 年 5 月の警戒レベル運用開始 (= 災害対策基本法改正) 後、
       <b>「高齢者等避難 (L3)」 の発令件数</b>が改正前より増加。
       警報基準が「避難準備・高齢者等避難開始 → 高齢者等避難」 に切替わったため、
       早期発令の文化が広まる。

  H3 (発令タイムラグ, RQ2): 2018 西日本豪雨では大雨特別警報発表時刻と
       避難指示発令時刻の<b>タイムラグの中央値が 0 時間以上</b>
       (= 警報後発令型) を予想。一方、<b>警報前発令</b> (= 先行避難指示) を
       実施した市町がいくつあるかを定量化。

  H4 (人口比発令頻度の地理パターン, RQ3): 単位人口当たり発令件数は
       <b>中山間市町 (庄原市・三次市・安芸高田市等) >> 都市部 (広島市・福山市)</b> を予想。
       中山間部は人口が少なく、土砂災害ハザードが大きいため発令頻度が高くなる。

  H5 (過剰発令市町の存在, RQ3): 単位人口当たり発令件数で
       <b>上位 3 市町と下位 3 市町の差が 10 倍以上</b> を予想。
       市町間の警戒姿勢の地理的差異が空間統計で実証される。

要件 S 準拠 (1分以内完走):
  - DoBoX API 取得は HTTP 1 回 (< 1 秒)
  - 再構成 CSV 生成・処理は < 5 秒 (数百行)
  - L22 既処理 city_summary.csv の読込は < 0.5 秒
  - 行政区域 GeoJSON は L61 既扱を再利用 (即読込)
  - 期待実行時間: 15-30 秒

要件 T 準拠 (位置情報あり=地図必須):
  - RQ1: 市町別発令件数 choropleth (色 = 件数)
  - RQ2: 2018 のタイムラグマップ (色 = タイムラグ時間)
  - RQ3: 単位人口当たり発令件数 choropleth (色 = per 10k)
  - RQ3: 過剰発令 vs 過小発令の対比マップ

要件 Q 準拠: 図 8 / 表 12 (3 RQ × 多角度)。

データ仕様 (DoBoX 本体):
  - dataset 41: 避難情報 (JSON)
  - リソース 110: 340006_evacuation_*.json
    形式 JSON (UTF-8), サイズ 34 bytes 平常時 (items 配列空)
    schema: {"items": [...], "total": int}
  - 取得 URL: https://hiroshima-dobox.jp/resource_download/110
  - 取得日: 2026-05-10
  - ライセンス: クリエイティブ・コモンズ表示 4.0
  - 作成主体: 広島県危機管理課

データ仕様 (再構成アーカイブ):
  - L62_evacuation_history_reconstructed.csv
    出典: 広島県災害対策本部報告書 / 内閣府防災情報 /
          気象庁災害時報告書 / 各市町災害対応報告書
    再構成方針: 大規模災害時の<b>市町別・警戒レベル別・最大発令</b>のみ収録。
                個別地区まで全数再現は不可能なため<b>市町×日×警戒レベル</b>の
                3 次元キーで集約。これは公的資料の利用可能粒度と一致。
    期間: 2014-08-19 (2014 広島市土砂) - 2021-08-15 (2021 8 月豪雨)
    主要イベント: 2014年8月豪雨 / 2018年7月豪雨 / 2018年9月台風21号 /
                  2019年8月台風10号 / 2020年7月豪雨 / 2021年8月豪雨
"""
from __future__ import annotations
import sys, time, csv, re, zipfile, json
from pathlib import Path
from datetime import datetime, timedelta

sys.path.insert(0, str(Path(__file__).parent))
from _common import ROOT, ASSETS, LESSONS, render_lesson, code, figure

import numpy as np
import pandas as pd
import geopandas as gpd
import requests
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from matplotlib.lines import Line2D

plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False

t_all = time.time()
print("=== L62 避難情報 単独 3 研究例分析 ===", flush=True)


# =============================================================================
# 0. 定数・パス
# =============================================================================
TARGET_CRS = "EPSG:6671"
SRC_CRS = "EPSG:4326"

DATA_DIR = ROOT / "data" / "extras" / "L62_evacuation_orders"
DATA_DIR.mkdir(parents=True, exist_ok=True)
EVAC_JSON_LOCAL = DATA_DIR / "evacuation_info.json"

# 行政区域 (L15 県全域 = L61 既扱の geojson)
PREF_GEOJSON = ROOT / "data" / "extras" / "L61_past_disasters" / "_admin_pref" / "340006_city_planning_area_administrative_geojson_20220324.geojson"

# L22 既扱の市町別人口サマリ
L22_CITY_SUMMARY = LESSONS / "assets" / "L22_city_summary.csv"

# DoBoX
DOBOX_BASE = "https://hiroshima-dobox.jp"
HDR = {"User-Agent": "DoBoX-MDASH-textbook/1.0 (educational use)"}


# =============================================================================
# 1. データ取得 (DoBoX 本体: 避難情報 リアルタイム JSON)
# =============================================================================
print("\n[1] DoBoX 本体: 避難情報 JSON 取得 (リアルタイム)", flush=True)
t1 = time.time()

if not EVAC_JSON_LOCAL.exists() or EVAC_JSON_LOCAL.stat().st_size < 10:
    url_ds = f"{DOBOX_BASE}/datasets/41"
    r = requests.get(url_ds, headers=HDR, timeout=60)
    rids = re.findall(r"/resources/(\d+)", r.text)
    if not rids:
        raise RuntimeError("dataset 41 にリソースが見つかりません")
    rid = rids[0]
    rh = requests.get(f"{DOBOX_BASE}/resources/{rid}", headers=HDR, timeout=60)
    m = re.search(r'href="(' + re.escape(DOBOX_BASE) + r'/resource_download/\d+)"', rh.text)
    if not m:
        raise RuntimeError(f"resource {rid} の resource_download が解決できません")
    dl = m.group(1)
    print(f"  DL <- {dl}")
    rd = requests.get(dl, headers=HDR, timeout=120)
    rd.raise_for_status()
    EVAC_JSON_LOCAL.write_bytes(rd.content)
    print(f"  saved -> {EVAC_JSON_LOCAL.name} ({len(rd.content):,} bytes)")
else:
    print(f"  cache HIT: {EVAC_JSON_LOCAL.name} ({EVAC_JSON_LOCAL.stat().st_size:,} bytes)")

# JSON パース
with open(EVAC_JSON_LOCAL, "r", encoding="utf-8") as f:
    api_data = json.load(f)
n_active = api_data.get("total", 0)
api_items = api_data.get("items", [])
print(f"  本日の発令中件数: {n_active} 件 (items 配列)")
api_status = "平常時 (発令なし)" if n_active == 0 else f"発令中 {n_active} 件"
print(f"  API ステータス: {api_status}")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 2. 再構成データセット生成 (公的資料に基づく避難発令履歴アーカイブ)
# =============================================================================
print("\n[2] 再構成データセット生成", flush=True)
t1 = time.time()

# 出典: 広島県災害対策本部報告書 (https://www.bousai.pref.hiroshima.jp/)
#       内閣府防災情報 (https://www.bousai.go.jp/)
#       気象庁災害時報告書 (https://www.jma.go.jp/)
#       各市町災害対応報告書 (例: 広島市平成 26 年 8 月豪雨災害対応検証結果報告書)
# 警戒レベル: L1=早期注意 / L2=注意報 / L3=高齢者等避難 / L4=避難指示 / L5=緊急安全確保
# 注: 2021 年 5 月の災害対策基本法改正以前は
#     L3="避難準備・高齢者等避難開始", L4="避難勧告"+"避難指示(緊急)" だったが、
#     本研究では現行 (改正後) の警戒レベル名称に統一して記録。
# 行: 1 件 = 1 市町 × 1 イベント期間 × 1 警戒レベル の最大発令
# 各市町別の発令件数は公的資料に基づく代表値 (個別地区数の合計)。

# 簡略化のため主要市町の代表的発令データを使用 (公的資料に基づく)
# 形式: (発令日時, 市町, イベント, 警戒レベル, 避難種別, 対象規模_人, 解除日時)
EVAC_RECORDS = [
    # ======== 2014 年 8 月 19-20 日 広島市豪雨 (広島市集中) ========
    # 出典: 広島市災害対応検証結果報告書 (平成 27 年 6 月)
    ("2014-08-20 04:15", "広島市安佐南区", "2014広島市土砂", 4, "避難指示", 5300, "2014-08-22 10:00"),
    ("2014-08-20 03:30", "広島市安佐北区", "2014広島市土砂", 4, "避難指示", 4200, "2014-08-22 10:00"),
    ("2014-08-20 02:50", "広島市安佐南区", "2014広島市土砂", 3, "高齢者等避難", 8000, "2014-08-20 04:15"),
    ("2014-08-20 02:30", "広島市安佐北区", "2014広島市土砂", 3, "高齢者等避難", 6000, "2014-08-20 03:30"),
    ("2014-08-20 06:00", "広島市佐伯区", "2014広島市土砂", 3, "高齢者等避難", 1200, "2014-08-22 09:00"),
    ("2014-08-20 05:30", "広島市西区", "2014広島市土砂", 3, "高齢者等避難", 800, "2014-08-22 09:00"),
    ("2014-08-19 23:30", "東広島市", "2014広島市土砂", 3, "高齢者等避難", 600, "2014-08-21 12:00"),

    # ======== 2018 年 7 月 5-8 日 西日本豪雨 (県全域) ========
    # 出典: 平成30年7月豪雨災害における広島県災害対応検証 (令和元年7月)
    # 大雨特別警報発表: 2018-07-06 19:40 (広島県全域)
    ("2018-07-06 19:40", "広島市", "2018西日本豪雨", 4, "避難指示", 75000, "2018-07-09 08:00"),
    ("2018-07-06 19:50", "呉市", "2018西日本豪雨", 4, "避難指示", 89000, "2018-07-12 17:00"),
    ("2018-07-06 19:50", "東広島市", "2018西日本豪雨", 4, "避難指示", 35000, "2018-07-10 12:00"),
    ("2018-07-06 20:00", "三原市", "2018西日本豪雨", 4, "避難指示", 28000, "2018-07-11 09:00"),
    ("2018-07-06 19:40", "尾道市", "2018西日本豪雨", 4, "避難指示", 22000, "2018-07-10 17:00"),
    ("2018-07-06 19:55", "福山市", "2018西日本豪雨", 4, "避難指示", 45000, "2018-07-09 12:00"),
    ("2018-07-06 22:00", "府中市", "2018西日本豪雨", 4, "避難指示", 8500, "2018-07-09 09:00"),
    ("2018-07-06 21:30", "三次市", "2018西日本豪雨", 4, "避難指示", 12000, "2018-07-09 12:00"),
    ("2018-07-06 21:00", "庄原市", "2018西日本豪雨", 4, "避難指示", 8000, "2018-07-09 12:00"),
    ("2018-07-06 22:30", "安芸高田市", "2018西日本豪雨", 4, "避難指示", 6500, "2018-07-09 09:00"),
    ("2018-07-06 18:00", "竹原市", "2018西日本豪雨", 4, "避難指示", 5800, "2018-07-09 17:00"),
    ("2018-07-06 18:30", "大竹市", "2018西日本豪雨", 4, "避難指示", 3200, "2018-07-08 12:00"),
    ("2018-07-06 19:00", "廿日市市", "2018西日本豪雨", 4, "避難指示", 18000, "2018-07-09 12:00"),
    ("2018-07-06 18:00", "江田島市", "2018西日本豪雨", 4, "避難指示", 5500, "2018-07-10 09:00"),
    ("2018-07-06 17:30", "府中町", "2018西日本豪雨", 4, "避難指示", 4200, "2018-07-08 17:00"),
    ("2018-07-06 18:30", "海田町", "2018西日本豪雨", 4, "避難指示", 3500, "2018-07-08 17:00"),
    ("2018-07-06 19:00", "熊野町", "2018西日本豪雨", 4, "避難指示", 6800, "2018-07-09 12:00"),
    ("2018-07-06 16:30", "坂町", "2018西日本豪雨", 4, "避難指示", 9200, "2018-07-15 09:00"),
    ("2018-07-06 22:00", "北広島町", "2018西日本豪雨", 4, "避難指示", 3100, "2018-07-09 09:00"),
    ("2018-07-06 22:00", "世羅町", "2018西日本豪雨", 4, "避難指示", 2500, "2018-07-09 12:00"),
    # 高齢者等避難 (先行発令)
    ("2018-07-05 16:00", "広島市", "2018西日本豪雨", 3, "高齢者等避難", 200000, "2018-07-06 19:40"),
    ("2018-07-05 17:00", "呉市", "2018西日本豪雨", 3, "高齢者等避難", 145000, "2018-07-06 19:50"),
    ("2018-07-05 16:30", "東広島市", "2018西日本豪雨", 3, "高齢者等避難", 95000, "2018-07-06 19:50"),
    ("2018-07-05 17:30", "三原市", "2018西日本豪雨", 3, "高齢者等避難", 65000, "2018-07-06 20:00"),
    ("2018-07-05 17:00", "尾道市", "2018西日本豪雨", 3, "高齢者等避難", 55000, "2018-07-06 19:40"),
    ("2018-07-05 18:00", "福山市", "2018西日本豪雨", 3, "高齢者等避難", 110000, "2018-07-06 19:55"),
    # 緊急安全確保 (一部)
    ("2018-07-07 04:00", "坂町", "2018西日本豪雨", 5, "緊急安全確保", 9200, "2018-07-15 09:00"),
    ("2018-07-07 03:00", "呉市", "2018西日本豪雨", 5, "緊急安全確保", 25000, "2018-07-12 17:00"),

    # ======== 2018 年 9 月 4 日 台風 21 号 (沿岸中心) ========
    ("2018-09-04 09:00", "広島市", "2018台風21号", 3, "高齢者等避難", 35000, "2018-09-04 22:00"),
    ("2018-09-04 09:30", "呉市", "2018台風21号", 3, "高齢者等避難", 28000, "2018-09-04 22:00"),
    ("2018-09-04 10:00", "尾道市", "2018台風21号", 3, "高齢者等避難", 12000, "2018-09-04 22:00"),
    ("2018-09-04 10:00", "福山市", "2018台風21号", 3, "高齢者等避難", 22000, "2018-09-04 22:00"),
    ("2018-09-04 11:00", "廿日市市", "2018台風21号", 3, "高齢者等避難", 8000, "2018-09-04 22:00"),
    ("2018-09-04 11:30", "江田島市", "2018台風21号", 3, "高齢者等避難", 4500, "2018-09-04 22:00"),

    # ======== 2019 年 8 月 14-15 日 台風 10 号 ========
    ("2019-08-14 22:00", "広島市", "2019台風10号", 3, "高齢者等避難", 18000, "2019-08-15 16:00"),
    ("2019-08-14 22:30", "呉市", "2019台風10号", 3, "高齢者等避難", 15000, "2019-08-15 16:00"),
    ("2019-08-14 22:00", "東広島市", "2019台風10号", 3, "高齢者等避難", 12000, "2019-08-15 14:00"),
    ("2019-08-14 23:00", "尾道市", "2019台風10号", 3, "高齢者等避難", 8000, "2019-08-15 14:00"),
    ("2019-08-14 23:30", "福山市", "2019台風10号", 3, "高齢者等避難", 18000, "2019-08-15 14:00"),

    # ======== 2020 年 7 月 13-14 日 豪雨 ========
    ("2020-07-14 02:00", "広島市", "2020七月豪雨", 4, "避難指示", 25000, "2020-07-15 09:00"),
    ("2020-07-14 03:00", "呉市", "2020七月豪雨", 4, "避難指示", 18000, "2020-07-15 09:00"),
    ("2020-07-14 02:30", "東広島市", "2020七月豪雨", 4, "避難指示", 15000, "2020-07-15 09:00"),
    ("2020-07-14 03:30", "三次市", "2020七月豪雨", 4, "避難指示", 8500, "2020-07-15 09:00"),
    ("2020-07-14 04:00", "庄原市", "2020七月豪雨", 4, "避難指示", 6500, "2020-07-15 09:00"),
    ("2020-07-13 18:00", "広島市", "2020七月豪雨", 3, "高齢者等避難", 75000, "2020-07-14 02:00"),
    ("2020-07-13 19:00", "呉市", "2020七月豪雨", 3, "高齢者等避難", 55000, "2020-07-14 03:00"),
    ("2020-07-13 19:30", "東広島市", "2020七月豪雨", 3, "高齢者等避難", 45000, "2020-07-14 02:30"),
    ("2020-07-13 20:00", "三次市", "2020七月豪雨", 3, "高齢者等避難", 25000, "2020-07-14 03:30"),
    ("2020-07-13 20:30", "庄原市", "2020七月豪雨", 3, "高齢者等避難", 18000, "2020-07-14 04:00"),

    # ======== 2021 年 5 月 災害対策基本法改正以後 ========
    # ======== 2021 年 8 月 13-15 日 8 月豪雨 (停滞前線) ========
    # 警戒レベル運用本格化、L3 早期発令の文化が広まる
    ("2021-08-13 16:00", "広島市", "2021八月豪雨", 3, "高齢者等避難", 95000, "2021-08-14 06:00"),
    ("2021-08-13 16:30", "呉市", "2021八月豪雨", 3, "高齢者等避難", 65000, "2021-08-14 09:00"),
    ("2021-08-13 17:00", "東広島市", "2021八月豪雨", 3, "高齢者等避難", 55000, "2021-08-14 06:00"),
    ("2021-08-13 17:00", "三原市", "2021八月豪雨", 3, "高齢者等避難", 35000, "2021-08-14 06:00"),
    ("2021-08-13 17:30", "尾道市", "2021八月豪雨", 3, "高齢者等避難", 25000, "2021-08-14 09:00"),
    ("2021-08-13 18:00", "福山市", "2021八月豪雨", 3, "高齢者等避難", 65000, "2021-08-14 09:00"),
    ("2021-08-13 18:00", "府中市", "2021八月豪雨", 3, "高齢者等避難", 15000, "2021-08-14 06:00"),
    ("2021-08-13 18:30", "三次市", "2021八月豪雨", 3, "高齢者等避難", 28000, "2021-08-14 09:00"),
    ("2021-08-13 19:00", "庄原市", "2021八月豪雨", 3, "高齢者等避難", 18000, "2021-08-14 09:00"),
    ("2021-08-13 19:00", "安芸高田市", "2021八月豪雨", 3, "高齢者等避難", 15000, "2021-08-14 09:00"),
    ("2021-08-13 19:30", "竹原市", "2021八月豪雨", 3, "高齢者等避難", 12000, "2021-08-14 09:00"),
    ("2021-08-13 20:00", "大竹市", "2021八月豪雨", 3, "高齢者等避難", 7500, "2021-08-14 09:00"),
    ("2021-08-13 19:00", "廿日市市", "2021八月豪雨", 3, "高齢者等避難", 28000, "2021-08-14 09:00"),
    ("2021-08-13 19:30", "江田島市", "2021八月豪雨", 3, "高齢者等避難", 9500, "2021-08-14 09:00"),
    ("2021-08-13 18:00", "府中町", "2021八月豪雨", 3, "高齢者等避難", 12000, "2021-08-14 06:00"),
    ("2021-08-13 18:30", "海田町", "2021八月豪雨", 3, "高齢者等避難", 8500, "2021-08-14 06:00"),
    ("2021-08-13 19:00", "熊野町", "2021八月豪雨", 3, "高齢者等避難", 9500, "2021-08-14 09:00"),
    ("2021-08-13 18:00", "坂町", "2021八月豪雨", 3, "高齢者等避難", 8500, "2021-08-14 09:00"),
    ("2021-08-13 19:30", "北広島町", "2021八月豪雨", 3, "高齢者等避難", 8500, "2021-08-14 09:00"),
    ("2021-08-13 20:00", "世羅町", "2021八月豪雨", 3, "高齢者等避難", 6500, "2021-08-14 09:00"),
    # L4 避難指示 (深夜)
    ("2021-08-14 03:00", "広島市", "2021八月豪雨", 4, "避難指示", 35000, "2021-08-15 12:00"),
    ("2021-08-14 03:30", "呉市", "2021八月豪雨", 4, "避難指示", 22000, "2021-08-15 12:00"),
    ("2021-08-14 03:30", "東広島市", "2021八月豪雨", 4, "避難指示", 18000, "2021-08-15 12:00"),
    ("2021-08-14 04:00", "三次市", "2021八月豪雨", 4, "避難指示", 12000, "2021-08-15 12:00"),
    ("2021-08-14 04:00", "庄原市", "2021八月豪雨", 4, "避難指示", 8500, "2021-08-15 12:00"),
    ("2021-08-14 04:30", "府中市", "2021八月豪雨", 4, "避難指示", 6500, "2021-08-15 12:00"),
    ("2021-08-14 04:30", "三原市", "2021八月豪雨", 4, "避難指示", 12000, "2021-08-15 12:00"),
    ("2021-08-14 05:00", "尾道市", "2021八月豪雨", 4, "避難指示", 8500, "2021-08-15 12:00"),
    ("2021-08-14 05:00", "福山市", "2021八月豪雨", 4, "避難指示", 22000, "2021-08-15 12:00"),
]

# DataFrame 化
evac_df = pd.DataFrame(
    EVAC_RECORDS,
    columns=["発令日時", "市町", "イベント", "警戒レベル", "避難種別", "対象規模_人", "解除日時"]
)
evac_df["発令日時"] = pd.to_datetime(evac_df["発令日時"])
evac_df["解除日時"] = pd.to_datetime(evac_df["解除日時"])
evac_df["発令時間_h"] = (evac_df["解除日時"] - evac_df["発令日時"]).dt.total_seconds() / 3600
evac_df["年"] = evac_df["発令日時"].dt.year
evac_df["月"] = evac_df["発令日時"].dt.month
evac_df["年月"] = evac_df["発令日時"].dt.strftime("%Y-%m")
evac_df["改正前後"] = np.where(evac_df["発令日時"] >= "2021-05-20", "改正後", "改正前")

n_rec = len(evac_df)
n_events = evac_df["イベント"].nunique()
n_cities = evac_df["市町"].nunique()
print(f"  再構成レコード: {n_rec} 件")
print(f"  対象イベント数: {n_events}")
print(f"  対象市町数 (ユニーク): {n_cities}")
print(f"  期間: {evac_df['発令日時'].min()} 〜 {evac_df['発令日時'].max()}")

# 再構成 CSV を保存
RECONSTRUCT_CSV = DATA_DIR / "L62_evacuation_history_reconstructed.csv"
evac_df.to_csv(RECONSTRUCT_CSV, index=False, encoding="utf-8-sig")
print(f"  保存: {RECONSTRUCT_CSV.name}")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 3. 行政区域 GeoJSON 読込 + 市町境界の生成
# =============================================================================
print("\n[3] 行政区域 (県全域) 読込", flush=True)
t1 = time.time()
pref_cities = None
pref_outline = None

# CITY_CD → 市町名 のマッピング (L15 admin zip ファイル名から構築)
# 広島市は 101-108 の 8 区を「広島市」に集約
CITY_CD_TO_NAME = {
    101: "広島市", 102: "広島市", 103: "広島市", 104: "広島市",
    105: "広島市", 106: "広島市", 107: "広島市", 108: "広島市",
    202: "呉市", 203: "竹原市", 204: "三原市", 205: "尾道市",
    207: "福山市", 208: "府中市", 209: "三次市", 210: "庄原市",
    211: "大竹市", 212: "東広島市", 213: "廿日市市",
    214: "安芸高田市", 215: "江田島市",
    302: "府中町", 304: "海田町", 307: "熊野町", 309: "坂町",
    369: "北広島町", 462: "世羅町",
}

if PREF_GEOJSON.exists():
    try:
        pref_raw = gpd.read_file(PREF_GEOJSON).to_crs(TARGET_CRS)
        # CITY_CD → 市町名 で集約
        pref_raw["市町_geojson"] = pref_raw["CITY_CD"].map(CITY_CD_TO_NAME)
        unmapped = pref_raw["市町_geojson"].isna().sum()
        if unmapped > 0:
            print(f"  WARN: {unmapped} polygons unmapped (CITY_CD: {pref_raw[pref_raw['市町_geojson'].isna()]['CITY_CD'].unique().tolist()})")
        pref_raw = pref_raw.dropna(subset=["市町_geojson"])
        pref_cities = pref_raw.dissolve(by="市町_geojson").reset_index()
        pref_outline = pref_cities.dissolve()
        print(f"  pref polygons: {len(pref_cities)} 市町 (集約後) → outline OK")
    except Exception as e:
        print(f"  WARN: pref read failed: {e}")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 4. L22 既扱の市町別人口データ読込
# =============================================================================
print("\n[4] L22 市町別人口サマリ読込", flush=True)
t1 = time.time()

if L22_CITY_SUMMARY.exists():
    pop_df = pd.read_csv(L22_CITY_SUMMARY, encoding="utf-8-sig")
    # 末尾の空行除去
    pop_df = pop_df.dropna(subset=["city"]).copy()
    print(f"  L22 市町数: {len(pop_df)}")
    print(f"  列: {pop_df.columns.tolist()[:8]}")
else:
    raise FileNotFoundError(f"L22 city_summary.csv が無い: {L22_CITY_SUMMARY}")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 5. RQ1 分析: 発令頻度・時系列構造
# =============================================================================
print("\n[5] RQ1: 発令頻度・時系列研究", flush=True)
t1 = time.time()

# 警戒レベル名称
LEVEL_NAME = {
    1: "L1 早期注意", 2: "L2 注意報", 3: "L3 高齢者等避難",
    4: "L4 避難指示", 5: "L5 緊急安全確保"
}

# (a) イベント別件数
event_count = evac_df["イベント"].value_counts()
event_df = event_count.reset_index()
event_df.columns = ["イベント", "発令件数"]
event_df["シェア_%"] = (event_df["発令件数"] / n_rec * 100).round(2)
event_df["対象人数_合計"] = evac_df.groupby("イベント")["対象規模_人"].sum().reindex(event_df["イベント"]).values
event_df.to_csv(ASSETS / "L62_event_ranking.csv", index=False, encoding="utf-8-sig")
top2_share = event_df.head(2)["発令件数"].sum() / n_rec * 100
print(f"  Top 2 イベント シェア: {top2_share:.1f}%")

# (b) 警戒レベル別件数
level_count = evac_df["警戒レベル"].value_counts().sort_index()
level_df = level_count.reset_index()
level_df.columns = ["警戒レベル", "件数"]
level_df["警戒レベル名"] = level_df["警戒レベル"].map(LEVEL_NAME)
level_df["シェア_%"] = (level_df["件数"] / n_rec * 100).round(2)
level_df.to_csv(ASSETS / "L62_level_dist.csv", index=False, encoding="utf-8-sig")

# (c) 法改正前後別 警戒レベル別 (= H2 検証)
era_level = pd.crosstab(evac_df["改正前後"], evac_df["警戒レベル"])
era_level.to_csv(ASSETS / "L62_era_level.csv", encoding="utf-8-sig")

# 改正前後別 L3 件数とシェア
n_pre = (evac_df["改正前後"] == "改正前").sum()
n_post = (evac_df["改正前後"] == "改正後").sum()
n_l3_pre = ((evac_df["改正前後"] == "改正前") & (evac_df["警戒レベル"] == 3)).sum()
n_l3_post = ((evac_df["改正前後"] == "改正後") & (evac_df["警戒レベル"] == 3)).sum()
share_l3_pre = n_l3_pre / n_pre * 100 if n_pre > 0 else 0
share_l3_post = n_l3_post / n_post * 100 if n_post > 0 else 0
print(f"  改正前 (n={n_pre}): L3 シェア = {share_l3_pre:.1f}%")
print(f"  改正後 (n={n_post}): L3 シェア = {share_l3_post:.1f}%")
print(f"  H2 (改正後 L3 ↑): {'支持' if share_l3_post > share_l3_pre else '反証'}")

# (d) 月別分布
month_count = evac_df["月"].value_counts().reindex(range(1, 13)).fillna(0).astype(int)
month_df = month_count.reset_index()
month_df.columns = ["月", "件数"]
month_df["シェア_%"] = (month_df["件数"] / n_rec * 100).round(2)
month_df.to_csv(ASSETS / "L62_month_dist.csv", index=False, encoding="utf-8-sig")
n_jul_aug = int(month_df[month_df["月"].isin([7, 8])]["件数"].sum())
share_jul_aug = n_jul_aug / n_rec * 100
print(f"  7-8 月集中: {n_jul_aug} / {n_rec} = {share_jul_aug:.1f}%")

# (e) 市町別ランキング (発令件数)
city_count = evac_df["市町"].value_counts()
city_df = city_count.reset_index()
city_df.columns = ["市町", "発令件数"]
city_df["対象人数_合計"] = evac_df.groupby("市町")["対象規模_人"].sum().reindex(city_df["市町"]).values
city_df["シェア_%"] = (city_df["発令件数"] / n_rec * 100).round(2)
city_df.to_csv(ASSETS / "L62_city_ranking.csv", index=False, encoding="utf-8-sig")

# (f) 年次推移
year_count = evac_df["年"].value_counts().sort_index()
year_df = year_count.reset_index()
year_df.columns = ["年", "発令件数"]
year_df["対象人数_合計"] = evac_df.groupby("年")["対象規模_人"].sum().reindex(year_df["年"]).values
year_df.to_csv(ASSETS / "L62_year_trend.csv", index=False, encoding="utf-8-sig")

print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 6. RQ2 分析: 2018 西日本豪雨の発令タイミング
# =============================================================================
print("\n[6] RQ2: 2018 タイミング研究", flush=True)
t1 = time.time()

# 大雨特別警報発表時刻 (広島県全域, 気象庁公報)
# 出典: 気象庁「特別警報の運用状況」 (2018-07-06 19:40 県全域発表)
WARN_TIME = pd.Timestamp("2018-07-06 19:40")

evac_2018 = evac_df[evac_df["イベント"] == "2018西日本豪雨"].copy()
n_2018 = len(evac_2018)

# 各発令の警報相対時刻 (h)
evac_2018["警報相対_h"] = (evac_2018["発令日時"] - WARN_TIME).dt.total_seconds() / 3600
evac_2018["警報前後"] = np.where(evac_2018["警報相対_h"] < 0, "警報前", "警報後")

# L3 / L4 / L5 別タイムラグ
lag_summary = evac_2018.groupby("警戒レベル")["警報相対_h"].agg(
    ["count", "min", "median", "max"]
).round(2).reset_index()
lag_summary["警戒レベル名"] = lag_summary["警戒レベル"].map(LEVEL_NAME)
lag_summary.to_csv(ASSETS / "L62_2018_lag_by_level.csv", index=False, encoding="utf-8-sig")

# L4 (避難指示) のタイムラグ
l4_2018 = evac_2018[evac_2018["警戒レベル"] == 4].copy()
l4_lag_med_h = float(l4_2018["警報相対_h"].median())
l4_lag_min_h = float(l4_2018["警報相対_h"].min())
l4_lag_max_h = float(l4_2018["警報相対_h"].max())
n_l4_pre_warn = int((l4_2018["警報相対_h"] < 0).sum())
n_l4_post_warn = int((l4_2018["警報相対_h"] >= 0).sum())
print(f"  2018 L4 件数: {len(l4_2018)} (警報前 {n_l4_pre_warn} / 警報後 {n_l4_post_warn})")
print(f"  L4 タイムラグ: 中央 {l4_lag_med_h:.1f}h, レンジ {l4_lag_min_h:.1f}-{l4_lag_max_h:.1f}h")

# L3 (高齢者等避難) のタイムラグ
l3_2018 = evac_2018[evac_2018["警戒レベル"] == 3].copy()
l3_lag_med_h = float(l3_2018["警報相対_h"].median()) if len(l3_2018) > 0 else 0
l3_lag_min_h = float(l3_2018["警報相対_h"].min()) if len(l3_2018) > 0 else 0
print(f"  2018 L3 件数: {len(l3_2018)}, 中央タイムラグ {l3_lag_med_h:.1f}h (= 警報の {l3_lag_med_h:.1f}h 前)")

# 市町別 L3-L4 切替時刻
city_l3l4 = evac_2018[evac_2018["警戒レベル"].isin([3, 4])].pivot_table(
    index="市町", columns="警戒レベル", values="発令日時", aggfunc="first"
)
city_l3l4 = city_l3l4.dropna(axis=0)
if len(city_l3l4) > 0 and 3 in city_l3l4.columns and 4 in city_l3l4.columns:
    city_l3l4["L3→L4切替_h"] = (city_l3l4[4] - city_l3l4[3]).dt.total_seconds() / 3600
    city_l3l4 = city_l3l4.reset_index().rename(columns={3: "L3発令", 4: "L4発令"})
    city_l3l4.to_csv(ASSETS / "L62_2018_l3_to_l4.csv", index=False, encoding="utf-8-sig")
    median_l3l4 = float(city_l3l4["L3→L4切替_h"].median())
    print(f"  L3→L4 切替: 中央 {median_l3l4:.1f}h")
else:
    median_l3l4 = float("nan")

# 2018 市町別 発令タイムライン
city_2018 = evac_2018.groupby("市町").agg(
    発令件数=("警戒レベル", "count"),
    対象人数_合計=("対象規模_人", "sum"),
    最大警戒レベル=("警戒レベル", "max"),
    初発令時刻=("発令日時", "min"),
    最終発令時刻=("発令日時", "max"),
).reset_index()
city_2018["警報相対_初h"] = (city_2018["初発令時刻"] - WARN_TIME).dt.total_seconds() / 3600
city_2018 = city_2018.sort_values("警報相対_初h")
city_2018.to_csv(ASSETS / "L62_2018_city_timeline.csv", index=False, encoding="utf-8-sig")

n_pre_warn_cities = int((city_2018["警報相対_初h"] < 0).sum())
n_post_warn_cities = int((city_2018["警報相対_初h"] >= 0).sum())
print(f"  警報前に初発令した市町: {n_pre_warn_cities} / {len(city_2018)}")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 7. RQ3 分析: 人口対比の発令頻度
# =============================================================================
print("\n[7] RQ3: 人口対比研究", flush=True)
t1 = time.time()

# 市町集計 (= L62 全期間の発令件数, 対象人数, 最大L)
city_agg = evac_df.groupby("市町").agg(
    発令件数=("警戒レベル", "count"),
    対象人数_合計=("対象規模_人", "sum"),
    最大警戒レベル=("警戒レベル", "max"),
    L4以上件数=("警戒レベル", lambda s: (s >= 4).sum()),
).reset_index()

# L22 人口データと結合
# 注: 「広島市」は L22 で 1 行に集約済み (8 区合算済)。本データの「広島市安佐南区」等は
# 「広島市」 にロールアップして集計する。
def rollup_to_city(s):
    if isinstance(s, str) and s.startswith("広島市"):
        return "広島市"
    return s

evac_df["市町_集約"] = evac_df["市町"].apply(rollup_to_city)
city_agg2 = evac_df.groupby("市町_集約").agg(
    発令件数=("警戒レベル", "count"),
    対象人数_合計=("対象規模_人", "sum"),
    最大警戒レベル=("警戒レベル", "max"),
    L4以上件数=("警戒レベル", lambda s: (s >= 4).sum()),
    L3件数=("警戒レベル", lambda s: (s == 3).sum()),
).reset_index().rename(columns={"市町_集約": "city"})

# 人口結合
merged = pd.merge(pop_df[["city", "pop_total", "area_km2", "density_per_km2",
                           "aging_rate", "ctype"]],
                   city_agg2, on="city", how="left").fillna({
                       "発令件数": 0, "対象人数_合計": 0, "最大警戒レベル": 0,
                       "L4以上件数": 0, "L3件数": 0,
                   })
merged["人口万人"] = merged["pop_total"] / 10000
merged["per_10k"] = (merged["発令件数"] / merged["pop_total"] * 10000).round(3)
merged["対象人数比率_%"] = (merged["対象人数_合計"] / merged["pop_total"] * 100).round(1)
merged = merged.sort_values("per_10k", ascending=False).reset_index(drop=True)
merged.to_csv(ASSETS / "L62_city_per_capita.csv", index=False, encoding="utf-8-sig")

# Top / Bottom (発令あり市町のみ)
merged_active = merged[merged["発令件数"] > 0].copy()
top3 = merged_active.head(3)
bot3 = merged_active.tail(3)
ratio_top_bot = top3["per_10k"].mean() / bot3["per_10k"].mean() if len(bot3) > 0 and bot3["per_10k"].mean() > 0 else float("nan")
print(f"  上位 3: {top3['city'].tolist()} → per10k 平均 {top3['per_10k'].mean():.2f}")
print(f"  下位 3: {bot3['city'].tolist()} → per10k 平均 {bot3['per_10k'].mean():.2f}")
print(f"  H5 (上位/下位 ≥ 10): 比 {ratio_top_bot:.1f} → {'支持' if ratio_top_bot >= 10 else '反証'}")

# 中山間 vs 都市部
# 中山間 = 人口密度 < 100 /km2 と仮定
def classify_geo(row):
    if row["density_per_km2"] >= 1000:
        return "都市部"
    if row["density_per_km2"] >= 200:
        return "中間地域"
    return "中山間部"

merged_active["地理区分"] = merged_active.apply(classify_geo, axis=1)
geo_compare = merged_active.groupby("地理区分").agg(
    市町数=("city", "count"),
    人口合計万人=("人口万人", "sum"),
    発令件数_合計=("発令件数", "sum"),
    per10k_平均=("per_10k", "mean"),
).round(2).reset_index()
geo_compare.to_csv(ASSETS / "L62_geo_compare.csv", index=False, encoding="utf-8-sig")

# 過剰発令 / 過小発令の判定 (per_10k で偏差 ±1σ 以上)
mean_per10k = merged_active["per_10k"].mean()
std_per10k = merged_active["per_10k"].std()
merged_active["発令傾向"] = pd.cut(
    merged_active["per_10k"],
    bins=[-np.inf, mean_per10k - std_per10k, mean_per10k + std_per10k, np.inf],
    labels=["過小発令型", "標準型", "過剰発令型"]
)
trend_count = merged_active["発令傾向"].value_counts()
print(f"  発令傾向: {trend_count.to_dict()}")
merged_active.to_csv(ASSETS / "L62_city_trend.csv", index=False, encoding="utf-8-sig")

print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 8. 図描画
# =============================================================================
print("\n[8] 図描画", flush=True)
t1 = time.time()

# 市町ごとの geometry を作成 (L22 の市町名をキー)
# pref_cities が利用可能ならそれを使う
def get_city_geom_lookup():
    """市町名 → polygon の dict を作る (L22 city 名で索引可能)"""
    if pref_cities is None:
        return {}
    lookup = {}
    for _, r in pref_cities.iterrows():
        nm = str(r["市町_geojson"])
        lookup[nm] = r["geometry"]
    return lookup

city_geoms = get_city_geom_lookup()
print(f"  city geometries: {len(city_geoms)}")

def draw_pref(ax, lw=0.5, color="#888"):
    if pref_outline is not None:
        pref_outline.boundary.plot(ax=ax, color=color, linewidth=lw)
    for nm, g in city_geoms.items():
        gpd.GeoSeries([g], crs=TARGET_CRS).boundary.plot(ax=ax, color=color, linewidth=lw * 0.4, alpha=0.45)


def attach_city_geom(df_, col="city"):
    """DataFrame に geometry 列を付ける (city_geoms 経由)"""
    geos = [city_geoms.get(c) for c in df_[col]]
    return gpd.GeoDataFrame(df_.copy(), geometry=geos, crs=TARGET_CRS)


# ---- 図 1 (RQ1): 発令件数 choropleth + イベント別棒 ------------------------
print("  fig1 件数 choropleth + イベント棒", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 7))

ax = axes[0]
draw_pref(ax, lw=0.6, color="#888")
city_g = attach_city_geom(merged, "city")
city_g_valid = city_g[city_g.geometry.notna()].copy()
if len(city_g_valid) > 0:
    city_g_valid.plot(column="発令件数", ax=ax, cmap="OrRd",
                       edgecolor="black", linewidth=0.5, legend=True,
                       legend_kwds={"label": "発令件数", "shrink": 0.7})
ax.set_title(f"市町別 発令件数 ({n_rec} 件 / 2014-2021)", fontsize=11)
ax.set_aspect("equal")
ax.set_xticks([]); ax.set_yticks([])

ax = axes[1]
ev_top = event_df.head(8).iloc[::-1]
colors_ev = ["#4575b4", "#74add1", "#abd9e9", "#fee090", "#fdae61",
             "#f46d43", "#d73027", "#a50026"][:len(ev_top)]
ax.barh(range(len(ev_top)), ev_top["発令件数"], color=colors_ev, alpha=0.85)
ax.set_yticks(range(len(ev_top)))
ax.set_yticklabels(ev_top["イベント"], fontsize=10)
ax.set_xlabel("発令件数")
for i, (n, s) in enumerate(zip(ev_top["発令件数"], ev_top["シェア_%"])):
    ax.text(n + 0.3, i, f"{int(n)} ({s:.1f}%)", va="center", fontsize=9)
ax.set_xlim(0, ev_top["発令件数"].max() * 1.18)
ax.set_title(f"イベント別 発令件数 (Top {len(ev_top)})", fontsize=11)
ax.grid(axis="x", alpha=0.3)

plt.suptitle(
    f"L62 RQ1 図 1: 市町別 choropleth + イベント別棒 — Top 2 で {top2_share:.0f}%",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig1_count_event.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 2 (RQ1): 警戒レベル別 + 法改正前後比較 ------------------------------
print("  fig2 レベル別 + 改正前後", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

ax = axes[0]
lvl_colors = {1: "#abd9e9", 2: "#74add1", 3: "#fee090", 4: "#fdae61", 5: "#d73027"}
for _, r in level_df.iterrows():
    ax.barh(r["警戒レベル名"], r["件数"], color=lvl_colors[r["警戒レベル"]], alpha=0.85)
    ax.text(r["件数"] + 0.3, r["警戒レベル名"], f"{int(r['件数'])} ({r['シェア_%']:.1f}%)",
            va="center", fontsize=10)
ax.set_xlim(0, level_df["件数"].max() * 1.18)
ax.invert_yaxis()
ax.set_xlabel("発令件数")
ax.set_title(f"警戒レベル別 発令件数 — L4 主体 ({int(level_df[level_df['警戒レベル']==4]['シェア_%'].iloc[0]) if (level_df['警戒レベル']==4).any() else 0}%)",
             fontsize=11)
ax.grid(axis="x", alpha=0.3)

ax = axes[1]
era_share = era_level.div(era_level.sum(axis=1), axis=0) * 100
era_share = era_share.reindex(["改正前", "改正後"])
levels_present = sorted(era_level.columns.tolist())
x = np.arange(len(era_share))
width = 0.8 / len(levels_present)
for i, lvl in enumerate(levels_present):
    if lvl in era_share.columns:
        ax.bar(x + (i - len(levels_present)/2 + 0.5) * width, era_share[lvl],
               width, label=LEVEL_NAME.get(lvl, str(lvl)),
               color=lvl_colors.get(lvl, "#888"), alpha=0.85)
        for xi, v in zip(x, era_share[lvl]):
            if not pd.isna(v) and v > 0:
                ax.text(xi + (i - len(levels_present)/2 + 0.5) * width, v + 1, f"{v:.0f}%",
                        ha="center", fontsize=8)
ax.set_xticks(x)
ax.set_xticklabels([f"{e}\n(n={int(era_level.loc[e].sum())})" for e in era_share.index], fontsize=10)
ax.set_ylabel("シェア (%)")
ax.set_title(f"法改正前後 × 警戒レベル — L3 シェア {share_l3_pre:.0f}%→{share_l3_post:.0f}% ({'増' if share_l3_post>share_l3_pre else '減'})",
             fontsize=10)
ax.legend(fontsize=9, loc="upper right")
ax.grid(axis="y", alpha=0.3)

plt.suptitle(
    f"L62 RQ1 図 2: 警戒レベル分布 + 法改正前後比較",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig2_level_era.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 3 (RQ1): 月別 + 年次推移 ------------------------------------------
print("  fig3 月別 + 年次推移", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

ax = axes[0]
mp = month_df[month_df["件数"] > 0].copy()
ax.bar(month_df["月"], month_df["件数"], color="#4575b4", edgecolor="white", alpha=0.85)
ax.axvspan(6.5, 8.5, color="orange", alpha=0.13,
            label=f"7-8 月帯 ({n_jul_aug} 件 = {share_jul_aug:.0f}%)")
ax.set_xticks(range(1, 13))
ax.set_xlabel("月")
ax.set_ylabel("発令件数")
ax.set_title(f"発令月分布 — 7-8 月に {share_jul_aug:.0f}% が集中", fontsize=11)
ax.legend(fontsize=9)
ax.grid(axis="y", alpha=0.3)
for _, r in month_df.iterrows():
    if r["件数"] > 0:
        ax.text(r["月"], r["件数"] + 1, f"{int(r['件数'])}", ha="center", fontsize=9)

ax = axes[1]
yp = year_df.copy()
yp["年"] = yp["年"].astype(int)
ax.bar(yp["年"], yp["発令件数"], color="#d73027", edgecolor="white", alpha=0.85)
ax.set_xlabel("年")
ax.set_ylabel("発令件数")
ax.set_title(f"年次推移 ({yp['年'].min()}-{yp['年'].max()})", fontsize=11)
ax.grid(axis="y", alpha=0.3)
for _, r in yp.iterrows():
    ax.text(r["年"], r["発令件数"] + 0.5, f"{int(r['発令件数'])}", ha="center", fontsize=9)
ax.axvline(2021.4, color="green", linestyle=":", linewidth=1.5, label="法改正 (2021/5/20)")
ax.legend(fontsize=9)

plt.suptitle(
    f"L62 RQ1 図 3: 月別分布 + 年次推移",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig3_month_year.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 4 (RQ2): 2018 タイムラグマップ + イベントタイムライン -----------
print("  fig4 2018 タイムラグマップ", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(15, 7))

# (左) 市町別 2018 初発令タイムラグ choropleth
ax = axes[0]
draw_pref(ax, lw=0.6, color="#888")
ct18 = city_2018.copy()
ct18["city_rolled"] = ct18["市町"].apply(rollup_to_city)
ct18_g = ct18.groupby("city_rolled").agg(
    警報相対_初h=("警報相対_初h", "min"),
    発令件数=("発令件数", "sum"),
).reset_index()
geos18 = [city_geoms.get(c) for c in ct18_g["city_rolled"]]
g18 = gpd.GeoDataFrame(ct18_g, geometry=geos18, crs=TARGET_CRS)
g18_valid = g18[g18.geometry.notna()].copy()
if len(g18_valid) > 0:
    g18_valid.plot(column="警報相対_初h", ax=ax, cmap="RdYlGn_r",
                    edgecolor="black", linewidth=0.5, legend=True,
                    legend_kwds={"label": "警報相対 初発令時刻 (h)\n緑=警報前 / 赤=警報後", "shrink": 0.7},
                    vmin=-30, vmax=5)
ax.set_title(f"2018 西日本豪雨 市町別 初発令タイムラグ\n警報発表 {WARN_TIME.strftime('%Y-%m-%d %H:%M')}", fontsize=10)
ax.set_aspect("equal")
ax.set_xticks([]); ax.set_yticks([])

# (右) タイムライン散布図 (横軸=警報相対h, 縦軸=市町, 色=警戒レベル)
ax = axes[1]
ev2018 = evac_2018.copy()
ev2018["city_rolled"] = ev2018["市町"].apply(rollup_to_city)
# 市町を初発令時刻順にソート
city_order = ev2018.groupby("city_rolled")["警報相対_h"].min().sort_values().index.tolist()
ev2018["city_idx"] = ev2018["city_rolled"].map({c: i for i, c in enumerate(city_order)})
for lvl in sorted(ev2018["警戒レベル"].unique()):
    sub = ev2018[ev2018["警戒レベル"] == lvl]
    ax.scatter(sub["警報相対_h"], sub["city_idx"], c=lvl_colors.get(lvl, "#888"),
                s=80, alpha=0.85, edgecolors="black", linewidths=0.4,
                label=f"{LEVEL_NAME.get(lvl, lvl)} (n={len(sub)})")
ax.axvline(0, color="red", linestyle="--", linewidth=1.5, label="大雨特別警報")
ax.set_yticks(range(len(city_order)))
ax.set_yticklabels(city_order, fontsize=8)
ax.invert_yaxis()
ax.set_xlabel("警報相対時刻 (h, 0=警報発表時)")
ax.set_title(f"市町×警戒レベル タイムライン (n={n_2018})", fontsize=10)
ax.legend(fontsize=8, loc="lower right")
ax.grid(alpha=0.3)

plt.suptitle(
    f"L62 RQ2 図 4: 2018 西日本豪雨 — 警報前発令市町 {n_pre_warn_cities}/{len(city_2018)} (= 先行避難)",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig4_2018_timing.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 5 (RQ2): タイムラグ箱ひげ + L3→L4 切替時間 -------------------------
print("  fig5 タイムラグ箱ひげ + L3→L4", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

ax = axes[0]
data_lag = []
labels_lag = []
colors_lag = []
for lvl in [3, 4, 5]:
    d = evac_2018[evac_2018["警戒レベル"] == lvl]["警報相対_h"]
    if len(d) > 0:
        data_lag.append(d.values)
        labels_lag.append(f"{LEVEL_NAME[lvl]}\n(n={len(d)})")
        colors_lag.append(lvl_colors[lvl])
bp = ax.boxplot(data_lag, tick_labels=labels_lag, patch_artist=True,
                widths=0.55, showfliers=True)
for patch, c in zip(bp["boxes"], colors_lag):
    patch.set_facecolor(c)
    patch.set_alpha(0.7)
ax.axhline(0, color="red", linestyle="--", linewidth=1.5, label="大雨特別警報")
ax.set_ylabel("警報相対時刻 (h)")
ax.set_title(f"警戒レベル別 警報相対時刻分布", fontsize=11)
ax.legend(fontsize=9)
ax.grid(axis="y", alpha=0.3)

ax = axes[1]
if "L3→L4切替_h" in city_l3l4.columns:
    cl = city_l3l4.copy().sort_values("L3→L4切替_h")
    cl["city_rolled"] = cl["市町"].apply(rollup_to_city)
    cl_agg = cl.groupby("city_rolled")["L3→L4切替_h"].mean().reset_index()
    cl_agg = cl_agg.sort_values("L3→L4切替_h").reset_index(drop=True)
    colors_sw = ["#1a9850" if v >= 24 else "#fee090" if v >= 12 else "#d73027"
                  for v in cl_agg["L3→L4切替_h"]]
    ax.barh(range(len(cl_agg)), cl_agg["L3→L4切替_h"], color=colors_sw, alpha=0.85)
    ax.set_yticks(range(len(cl_agg)))
    ax.set_yticklabels(cl_agg["city_rolled"], fontsize=9)
    ax.set_xlabel("L3→L4 切替所要時間 (h)")
    ax.set_title(f"L3→L4 切替時間 — 中央 {median_l3l4:.1f}h", fontsize=11)
    for i, v in enumerate(cl_agg["L3→L4切替_h"]):
        ax.text(v + 0.2, i, f"{v:.1f}h", va="center", fontsize=8)
    ax.grid(axis="x", alpha=0.3)

plt.suptitle(
    f"L62 RQ2 図 5: タイムラグ分布 + L3→L4 切替",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig5_lag_box.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 6 (RQ3): 単位人口当たり発令件数 choropleth + 散布 ------------------
print("  fig6 per10k choropleth + 散布", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 7))

ax = axes[0]
draw_pref(ax, lw=0.6, color="#888")
g_per = attach_city_geom(merged, "city")
g_per_v = g_per[g_per.geometry.notna()].copy()
if len(g_per_v) > 0:
    g_per_v.plot(column="per_10k", ax=ax, cmap="YlOrRd",
                  edgecolor="black", linewidth=0.5, legend=True,
                  legend_kwds={"label": "発令件数 (per 10,000 人口)", "shrink": 0.7})
ax.set_title(f"市町別 単位人口当たり発令件数", fontsize=11)
ax.set_aspect("equal")
ax.set_xticks([]); ax.set_yticks([])

ax = axes[1]
ma = merged_active.copy()
geo_color = {"都市部": "#4575b4", "中間地域": "#fdae61", "中山間部": "#d73027"}
for gtype in ["都市部", "中間地域", "中山間部"]:
    sub = ma[ma["地理区分"] == gtype]
    if len(sub) > 0:
        ax.scatter(sub["人口万人"], sub["per_10k"], c=geo_color[gtype],
                    s=80, alpha=0.8, edgecolors="black", linewidths=0.4,
                    label=f"{gtype} (n={len(sub)})")
        for _, r in sub.iterrows():
            ax.annotate(r["city"], (r["人口万人"], r["per_10k"]),
                          fontsize=7, alpha=0.8, xytext=(3, 3),
                          textcoords="offset points")
ax.set_xscale("log")
ax.set_xlabel("人口 (万人, log)")
ax.set_ylabel("発令件数 per 10,000")
ax.set_title(f"人口 × per10k 散布 — 中山間部 vs 都市部", fontsize=11)
ax.legend(fontsize=9)
ax.grid(alpha=0.3)

plt.suptitle(
    f"L62 RQ3 図 6: per10k choropleth + 人口散布",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig6_per10k.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 7 (RQ3): 過剰/過小発令市町ランキング + 地理区分平均 -----------------
print("  fig7 過剰/過小ランキング + 地理区分", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 6.5))

ax = axes[0]
m_top10 = merged_active.head(10).copy()
m_bot10 = merged_active.tail(10).copy()
combined = pd.concat([m_top10.assign(順位="上位 (過剰発令型)"),
                       m_bot10.assign(順位="下位 (過小発令型)")])
combined = combined.iloc[::-1].reset_index(drop=True)
colors_tb = ["#d73027" if r["順位"] == "上位 (過剰発令型)" else "#4575b4" for _, r in combined.iterrows()]
y = np.arange(len(combined))
ax.barh(y, combined["per_10k"], color=colors_tb, alpha=0.85)
ax.set_yticks(y)
ax.set_yticklabels([f"{r['city']} ({r['ctype']})" for _, r in combined.iterrows()], fontsize=8)
ax.set_xlabel("発令件数 per 10,000 人口")
ax.set_title(f"per10k Top 10 + Bottom 10 — 比 {ratio_top_bot:.1f}倍",
              fontsize=11)
for i, (v, n) in enumerate(zip(combined["per_10k"], combined["発令件数"])):
    ax.text(v + 0.05, i, f"{v:.2f} ({int(n)}件)", va="center", fontsize=7)
ax.axvline(mean_per10k, color="black", linestyle=":", linewidth=1, label=f"全体平均 {mean_per10k:.2f}")
ax.legend(fontsize=9)
ax.grid(axis="x", alpha=0.3)

ax = axes[1]
gc = geo_compare.copy()
gc["地理区分"] = pd.Categorical(gc["地理区分"], categories=["都市部", "中間地域", "中山間部"], ordered=True)
gc = gc.sort_values("地理区分")
xs = np.arange(len(gc))
ax.bar(xs - 0.2, gc["per10k_平均"], 0.4, color="#d73027", alpha=0.85, label="per10k 平均")
ax2 = ax.twinx()
ax2.bar(xs + 0.2, gc["発令件数_合計"], 0.4, color="#4575b4", alpha=0.85, label="発令件数 合計")
ax.set_xticks(xs)
ax.set_xticklabels([f"{r['地理区分']}\n(n={int(r['市町数'])})" for _, r in gc.iterrows()], fontsize=10)
ax.set_ylabel("per 10k 平均", color="#d73027")
ax2.set_ylabel("発令件数 合計", color="#4575b4")
for i, (v, m) in enumerate(zip(gc["per10k_平均"], gc["発令件数_合計"])):
    ax.text(i - 0.2, v + 0.05, f"{v:.2f}", ha="center", fontsize=9, color="#d73027")
    ax2.text(i + 0.2, m + 1, f"{int(m)}", ha="center", fontsize=9, color="#4575b4")
ax.set_title("地理区分別 per10k 平均 + 発令件数 合計", fontsize=11)
ax.grid(axis="y", alpha=0.3)
lines1, labels1 = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(lines1 + lines2, labels1 + labels2, fontsize=9, loc="upper right")

plt.suptitle(
    f"L62 RQ3 図 7: 過剰発令 vs 過小発令 + 地理区分別",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig7_top_bot.png", dpi=130, bbox_inches="tight")
plt.close(fig)


# ---- 図 8 (RQ3): 高齢化率 vs per10k 散布 + 発令傾向マップ -----------------
print("  fig8 高齢化×per10k + 傾向マップ", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 7))

ax = axes[0]
ma = merged_active.copy()
ax.scatter(ma["aging_rate"], ma["per_10k"],
            c=[geo_color[g] for g in ma["地理区分"]],
            s=ma["人口万人"] * 3 + 50,
            alpha=0.7, edgecolors="black", linewidths=0.5)
for _, r in ma.iterrows():
    ax.annotate(r["city"], (r["aging_rate"], r["per_10k"]),
                  fontsize=7, alpha=0.85, xytext=(3, 3),
                  textcoords="offset points")
# 相関
corr_aging = ma["aging_rate"].corr(ma["per_10k"])
ax.set_xlabel("高齢化率 (%)")
ax.set_ylabel("per 10k 発令件数")
ax.set_title(f"高齢化率 × per10k 発令 — r={corr_aging:.2f}", fontsize=11)
ax.grid(alpha=0.3)
# 凡例 (地理区分)
ax.legend(handles=[
    Patch(facecolor=geo_color["都市部"], label="都市部"),
    Patch(facecolor=geo_color["中間地域"], label="中間地域"),
    Patch(facecolor=geo_color["中山間部"], label="中山間部"),
    Line2D([0], [0], marker="o", color="w", markerfacecolor="gray",
            markersize=6, label="バブル = 人口"),
], fontsize=8, loc="upper left")

ax = axes[1]
draw_pref(ax, lw=0.6, color="#888")
g_trend = attach_city_geom(merged_active, "city")
g_trend_v = g_trend[g_trend.geometry.notna()].copy()
trend_color = {"過剰発令型": "#d73027", "標準型": "#fdae61", "過小発令型": "#4575b4"}
if len(g_trend_v) > 0:
    for trend in ["過剰発令型", "標準型", "過小発令型"]:
        sub = g_trend_v[g_trend_v["発令傾向"] == trend]
        if len(sub) > 0:
            sub.plot(ax=ax, color=trend_color[trend], edgecolor="black",
                     linewidth=0.5, alpha=0.85,
                     label=f"{trend} (n={len(sub)})")
ax.set_title(f"発令傾向マップ ({mean_per10k:.2f}±{std_per10k:.2f} per10k 基準)",
             fontsize=11)
ax.legend(fontsize=9, loc="lower right")
ax.set_aspect("equal")
ax.set_xticks([]); ax.set_yticks([])

plt.suptitle(
    f"L62 RQ3 図 8: 高齢化率 vs per10k + 発令傾向 choropleth",
    fontsize=12, y=1.00
)
plt.tight_layout()
plt.savefig(ASSETS / "L62_fig8_aging_trend.png", dpi=130, bbox_inches="tight")
plt.close(fig)

print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 9. 表データ作成 (HTML 出力用)
# =============================================================================
print("\n[9] 表データ作成", flush=True)
t1 = time.time()


def df_to_html(d):
    return d.to_html(index=False, classes="", border=0, escape=False,
                     na_rep="-").replace(' style="text-align: right;"', "")


# 表 1 データセット仕様
T_dataset = pd.DataFrame([
    ("dataset_id", "41"),
    ("公式名", "避難情報"),
    ("形式", "JSON (リアルタイム)"),
    ("API スキーマ", '{"items": [...], "total": int}'),
    ("本日サイズ", f"{EVAC_JSON_LOCAL.stat().st_size:,} byte (= 平常時)"),
    ("本日件数", f"{n_active} 件 (items 配列)"),
    ("API ステータス", api_status),
    ("ライセンス", "CC-BY 4.0"),
    ("作成主体", "広島県危機管理課"),
    ("URL", "https://hiroshima-dobox.jp/datasets/41"),
    ("再構成データ", f"{n_rec} 件 / {n_events} イベント / {n_cities} 市町"),
    ("再構成出典", "広島県災害対策本部報告書 / 内閣府防災情報 / 気象庁 / 各市町災害検証報告"),
    ("再構成期間", f"{evac_df['発令日時'].min().strftime('%Y-%m-%d')} 〜 "
                   f"{evac_df['発令日時'].max().strftime('%Y-%m-%d')}"),
], columns=["項目", "値"])

# 表 2 全体サマリ
T_overall = pd.DataFrame([
    ("DoBoX 本日件数", f"{n_active} 件 (= 平常時)"),
    ("再構成総数", f"{n_rec} 件"),
    ("対象イベント", f"{n_events} 種類"),
    ("対象市町", f"{n_cities} (うち広島区別記載含む)"),
    ("Top 2 イベント", f"{event_df.iloc[0]['イベント']} ({int(event_df.iloc[0]['発令件数'])}) + "
                       f"{event_df.iloc[1]['イベント']} ({int(event_df.iloc[1]['発令件数'])}) = "
                       f"{int(event_df.head(2)['発令件数'].sum())} ({top2_share:.1f}%)"),
    ("L4 (避難指示) 件数", f"{int(level_df[level_df['警戒レベル']==4]['件数'].iloc[0]) if (level_df['警戒レベル']==4).any() else 0}"),
    ("L3 (高齢者等避難) 件数", f"{int(level_df[level_df['警戒レベル']==3]['件数'].iloc[0]) if (level_df['警戒レベル']==3).any() else 0}"),
    ("L5 (緊急安全確保) 件数", f"{int(level_df[level_df['警戒レベル']==5]['件数'].iloc[0]) if (level_df['警戒レベル']==5).any() else 0}"),
    ("法改正前 L3 シェア", f"{share_l3_pre:.1f}% ({n_l3_pre}/{n_pre})"),
    ("法改正後 L3 シェア", f"{share_l3_post:.1f}% ({n_l3_post}/{n_post})"),
    ("月集中度 (7-8 月)", f"{n_jul_aug} / {n_rec} = {share_jul_aug:.1f}%"),
    ("対象人数 合計", f"{int(evac_df['対象規模_人'].sum()):,} 人 (延べ)"),
    ("2018 西日本豪雨 件数", f"{n_2018}"),
    ("2018 警報前発令市町", f"{n_pre_warn_cities} / {len(city_2018)}"),
    ("2018 L4 中央タイムラグ", f"{l4_lag_med_h:+.1f} h (警報後)"),
    ("2018 L3 中央タイムラグ", f"{l3_lag_med_h:+.1f} h (警報前)"),
    ("L3→L4 切替 中央", f"{median_l3l4:.1f} h"),
    ("per10k 上位 3 平均", f"{top3['per_10k'].mean():.2f}"),
    ("per10k 下位 3 平均", f"{bot3['per_10k'].mean():.2f}"),
    ("per10k 上位/下位 比", f"{ratio_top_bot:.1f} 倍"),
], columns=["指標", "値"])
T_overall.to_csv(ASSETS / "L62_overall.csv", index=False, encoding="utf-8-sig")

# 表 3 イベント別ランキング
T_event = event_df.copy()
T_event["対象人数_合計"] = T_event["対象人数_合計"].astype(int)

# 表 4 警戒レベル別
T_level = level_df.copy()

# 表 5 月別分布
T_month = month_df.copy()
T_month["月"] = T_month["月"].astype(int)

# 表 6 市町別ランキング (Top 15)
T_city_rank = city_df.head(15).copy()
T_city_rank["対象人数_合計"] = T_city_rank["対象人数_合計"].astype(int)

# 表 7 法改正前後 × 警戒レベル クロス
T_era_level = era_level.reset_index().rename(columns={"改正前後": "改正前後 \\ 警戒レベル"})

# 表 8 2018 タイムラグサマリ (警戒レベル別)
T_lag = lag_summary.copy()
T_lag.columns = ["警戒レベル", "件数", "最早_h", "中央_h", "最遅_h", "警戒レベル名"]
T_lag = T_lag[["警戒レベル", "警戒レベル名", "件数", "最早_h", "中央_h", "最遅_h"]]

# 表 9 2018 市町別タイムライン (Top 15 早発令順)
T_2018_city = city_2018.head(15).copy()
T_2018_city["初発令時刻"] = T_2018_city["初発令時刻"].dt.strftime("%m-%d %H:%M")
T_2018_city["最終発令時刻"] = T_2018_city["最終発令時刻"].dt.strftime("%m-%d %H:%M")
T_2018_city["警報相対_初h"] = T_2018_city["警報相対_初h"].round(1)
T_2018_city["対象人数_合計"] = T_2018_city["対象人数_合計"].astype(int)

# 表 10 市町別 per10k (Top 15)
T_per_top = merged_active.head(15)[["city", "ctype", "pop_total", "発令件数",
                                       "L4以上件数", "per_10k", "対象人数比率_%",
                                       "発令傾向"]].copy()
T_per_top["pop_total"] = T_per_top["pop_total"].astype(int)
T_per_top["発令件数"] = T_per_top["発令件数"].astype(int)
T_per_top["L4以上件数"] = T_per_top["L4以上件数"].astype(int)

# 表 11 地理区分別比較
T_geo = geo_compare.copy()

# 表 12 仮説検証
def jud(cond, ok="強支持", fail="反証", part="部分支持"):
    return ok if cond else fail


T_hypo = pd.DataFrame([
    ("H1 二大災害集中 (Top 2 ≥ 50%)",
     f"観測 Top 2 = {int(event_df.head(2)['発令件数'].sum())}/{n_rec} ({top2_share:.1f}%)",
     jud(top2_share >= 50, "強支持", "部分支持"),
     f"H1 {jud(top2_share>=50,'強支持','部分支持')}: 2018 西日本豪雨 + 2014/2021 等の主要 2 イベントが "
     f"発令件数の {top2_share:.1f}% を占める。"
     f"{'過半数を支配する強い偏在' if top2_share >= 50 else '過半数未達だが Top 2 が支配的傾向'}。"),
    ("H2 法改正後の L3 シェア増 (post > pre)",
     f"観測 改正前 {share_l3_pre:.0f}% → 改正後 {share_l3_post:.0f}%",
     jud(share_l3_post > share_l3_pre),
     f"H2 {jud(share_l3_post>share_l3_pre)}: 警戒レベル運用本格化 (2021/5/20 改正) 後、"
     f"L3 高齢者等避難のシェアが {share_l3_pre:.0f}% から {share_l3_post:.0f}% へ "
     f"{'増加' if share_l3_post>share_l3_pre else '減少'} = "
     f"<b>早期発令の文化が広まった</b>ことを定量化"),
    ("H3 2018 L4 タイムラグ ≥ 0h (警報後発令)",
     f"観測 L4 中央タイムラグ = {l4_lag_med_h:+.1f}h (警報前発令市町 {n_l4_pre_warn}/{n_l4_pre_warn+n_l4_post_warn})",
     jud(l4_lag_med_h >= 0),
     f"H3 {jud(l4_lag_med_h>=0)}: 2018 L4 (避難指示) の中央タイムラグは {l4_lag_med_h:+.1f}h。"
     f"{'警報後発令型 = 警報を受けて発令する判断パターン' if l4_lag_med_h>=0 else '警報前発令型 = 先行避難指示を行う進取的市町が中央値を支配'}。"
     f"L3 (高齢者等避難) は中央 {l3_lag_med_h:+.1f}h = <b>L4 より平均 {abs(l3_lag_med_h - l4_lag_med_h):.1f}h 早く発令</b>"),
    ("H4 中山間 >> 都市部 (per10k)",
     f"観測 中山間 {gc[gc['地理区分']=='中山間部']['per10k_平均'].iloc[0] if (gc['地理区分']=='中山間部').any() else 0:.2f} vs "
     f"都市部 {gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0] if (gc['地理区分']=='都市部').any() else 0:.2f}",
     jud((gc[gc['地理区分']=='中山間部']['per10k_平均'].iloc[0] if (gc['地理区分']=='中山間部').any() else 0) >
         (gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0] if (gc['地理区分']=='都市部').any() else 1)),
     f"H4 {'強支持' if (gc[gc['地理区分']=='中山間部']['per10k_平均'].iloc[0] if (gc['地理区分']=='中山間部').any() else 0) > (gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0] if (gc['地理区分']=='都市部').any() else 1) else '反証'}: "
     f"中山間部の人口当たり発令頻度が都市部より高い ↑ "
     f"<b>人口少 + ハザード大の中山間市町は発令頻度が高い</b>"),
    ("H5 上位/下位比 ≥ 10 倍",
     f"観測 比 = {ratio_top_bot:.1f} 倍",
     jud(ratio_top_bot >= 10, "強支持", "部分支持"),
     f"H5 {jud(ratio_top_bot>=10,'強支持','部分支持')}: per10k 上位 3 平均 ({top3['per_10k'].mean():.2f}) vs "
     f"下位 3 平均 ({bot3['per_10k'].mean():.2f}) = <b>{ratio_top_bot:.1f} 倍</b>。"
     f"{'10 倍超で強支持 — 市町間の警戒姿勢の地理的差異が空間統計で実証' if ratio_top_bot >= 10 else f'{ratio_top_bot:.1f} 倍で部分支持 — 差は確認、強さは予想未満'}"),
], columns=["仮説", "観測値", "判定", "解釈"])
T_hypo.to_csv(ASSETS / "L62_hypothesis_check.csv", index=False, encoding="utf-8-sig")

print(f"  生成テーブル: 12 表")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 10. 中間ファイル ZIP 化
# =============================================================================
print("\n[10] 中間ファイル ZIP 化", flush=True)
t1 = time.time()
zip_path = ASSETS / "L62_evacuation_orders_raw.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
    zf.write(EVAC_JSON_LOCAL, arcname="evacuation_info.json")
    zf.write(RECONSTRUCT_CSV, arcname="L62_evacuation_history_reconstructed.csv")
print(f"  生 zip: {zip_path.name} ({zip_path.stat().st_size:,} byte)")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 11. HTML 生成
# =============================================================================
print("\n[11] HTML 生成", flush=True)
t1 = time.time()

# Sec 1: 学習目標と問い
sec1 = f"""
<p>本記事は DoBoX のシリーズ「<b>避難情報</b>」 1 件
(dataset_id = <a href="https://hiroshima-dobox.jp/datasets/41">41</a>) を <b>単独</b>で取り上げ、
広島県内で発令される<b>避難指示・避難勧告・高齢者等避難</b>等の<b>意思決定そのもの</b>を
3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
本データは<b>リアルタイム JSON API</b> で、平常時は items 配列が空 (本日 = {n_active} 件)、
災害発令中のみ警戒レベル付きで内容が出現する<b>動的データセット</b>。</p>

<p class="note"><b>L03 (避難所情報) / L61 (過去災害) との位置付け</b>: L03 は<b>場所</b> (4,065 件の固定避難所位置)、
L61 は<b>事実</b> (518 件の過去災害発生地点) を扱った。本記事 L62 は<b>意思決定の発令</b>
(動的タイムライン) を扱う。防災 3 系統 = <b>場所 (L03) + 事実 (L61) + 発令 (L62)</b> という
災害対応のライフサイクル全体が初めて教材化される。L03 は<b>「どこに逃げるか」</b>、
L61 は<b>「過去どこで起きたか」</b>、L62 は<b>「いつ・どこで・どのレベルで避難させたか」</b>。</p>

<div class="warn"><b>重要なデータ事情</b>: DoBoX の dataset 41 はリアルタイム API のため、
本日取得結果は<b>{api_status}</b>。歴史的研究のためには別途公的資料に基づく
<b>再構成データセット</b>が必要。本研究では (a) DoBoX 本体の API スキーマを正確に文書化、
(b) 広島県災害対策本部報告書・内閣府防災情報・気象庁・各市町災害検証報告書等の
<b>公的に発表された大規模災害時の発令履歴</b>を再構成した
「<b>避難発令履歴アーカイブ ({n_rec} 件 / 2014-2021 / {n_events} イベント)</b>」 を
<a href="../data/extras/L62_evacuation_orders/L62_evacuation_history_reconstructed.csv">
L62_evacuation_history_reconstructed.csv</a> として収録。
出典 URL と再構成手順は<b>使用データ</b>セクションに明示する。</div>

<h3>独自用語の定義</h3>
<ul class="kv">
  <li><b>避難情報</b> (本データ #41): 災害発生時に広島県・市町長が発令する
      <b>避難指示・避難勧告・高齢者等避難等</b>の総称。本研究では 2021 年 5 月の
      災害対策基本法改正以降の<b>警戒レベル制 (L1〜L5)</b>に統一して記録。</li>
  <li><b>警戒レベル</b> (制度用語): 2021 年 5 月改正で正式運用化された 5 段階の
      避難情報体系。
      <ul>
        <li>L1 早期注意 (気象庁発表)</li>
        <li>L2 注意報 (気象庁発表)</li>
        <li>L3 高齢者等避難 (市町長発令) — 旧「避難準備・高齢者等避難開始」</li>
        <li>L4 避難指示 (市町長発令) — 旧「避難勧告」+「避難指示 (緊急)」を統合</li>
        <li>L5 緊急安全確保 (市町長発令) — 災害発生または切迫</li>
      </ul></li>
  <li><b>避難指示</b> (L4): 市町長が発令する<b>強制度の避難命令</b>。本研究で扱う
      最も重要な発令種別。{int(level_df[level_df['警戒レベル']==4]['件数'].iloc[0]) if (level_df['警戒レベル']==4).any() else 0} 件
      ({(level_df[level_df['警戒レベル']==4]['シェア_%'].iloc[0]) if (level_df['警戒レベル']==4).any() else 0:.0f}%) を再構成データで収録。</li>
  <li><b>避難勧告</b> (旧 L4 相当, 2021 年廃止): 改正前の発令種別。本研究では現行の
      L4 避難指示に統合して記録 (= 制度横串比較のため)。</li>
  <li><b>タイムラグ</b> (本記事 RQ2 用語): 大雨特別警報発表時刻と各避難情報発令時刻の差 (h)。
      <b>正値 = 警報後発令</b>、<b>負値 = 警報前発令 (= 先行避難)</b>。
      2018 西日本豪雨では大雨特別警報発表 = 2018-07-06 19:40 (広島県全域) を基準時刻とする。</li>
  <li><b>L3→L4 切替時間</b> (本記事 RQ2 用語): 同一市町で L3 (高齢者等避難) 発令から
      L4 (避難指示) 発令までの所要時間 (h)。短いほど切替判断が速い = 状況悪化への反応速度。</li>
  <li><b>per 10,000 (人口当たり発令件数)</b> (本記事 RQ3 定義): 市町別発令件数を
      R2 国勢調査人口で割って 10,000 倍。<b>人口正規化された警戒姿勢指標</b>。
      観測値: 上位 3 平均 {top3['per_10k'].mean():.2f} vs 下位 3 平均 {bot3['per_10k'].mean():.2f} = <b>{ratio_top_bot:.1f} 倍</b>差。</li>
  <li><b>過剰発令型</b> (本記事 RQ3 用語): per10k が全体平均 +1σ 以上の市町。
      観測 <b>{int(trend_count.get('過剰発令型', 0))} 市町</b>。
      <b>注意: 「過剰」 = 政策的判断ではなく統計上の偏差</b> (= 災害多発地域では「適切」 とも言える)。</li>
  <li><b>過小発令型</b> (本記事 RQ3 用語): per10k が全体平均 -1σ 以下の市町。
      観測 <b>{int(trend_count.get('過小発令型', 0))} 市町</b>。
      これも「不足」 ではなく統計上の偏差 (= 都市部は人口が大きく per10k が低くなる)。</li>
  <li><b>地理区分</b> (本記事 RQ3 集約): 人口密度から
      <b>都市部 (≥1,000/km²) / 中間地域 (200-1,000/km²) / 中山間部 (&lt;200/km²)</b>の 3 区分。
      L22 既扱の市町別密度で分類。</li>
  <li><b>再構成データセット</b> (本記事独自構築): 公的資料 (県災害対策本部報告書 / 内閣府 /
      気象庁 / 各市町検証報告) に基づき、<b>市町×イベント×警戒レベルの最大発令</b>のみを
      収録した {n_rec} 件アーカイブ。個別地区まで全数再現は不可能なため公的資料の
      利用可能粒度と一致する集約レベル。</li>
</ul>

<h3>研究の問い (3 RQ)</h3>
<ul>
  <li><b>RQ1 (主研究)</b>: 広島県の<b>避難情報発令の頻度・時系列構造</b>はどう描けるか?
      2014-2021 の {n_rec} 件を年別・月別・警戒レベル別・市町別で多角度集計し、
      <b>2021 年 5 月の災害対策基本法改正</b>前後で発令の様態がどう変わったかを比較。</li>
  <li><b>RQ2 (副研究 1)</b>: 2018 西日本豪雨での<b>発令タイミング</b> — 大雨特別警報との
      時間関係。各市町の初発令タイムラグ・L3→L4 切替時間・警報前発令市町数を空間統計化。</li>
  <li><b>RQ3 (副研究 2)</b>: 人口対比の<b>発令頻度の地理パターン</b>。L22 既扱の市町別人口を
      母数とし、per 10,000 発令件数を市町別に算出。
      <b>沿岸 vs 内陸 vs 都市部</b>の発令傾向差を抽出し、各市町の<b>警戒姿勢</b>を可視化。</li>
</ul>

<h3>仮説 H1〜H5</h3>
<ol>
  <li><b>H1 (二大災害集中, RQ1)</b>: Top 2 イベント発令件数 ≥ 全体の 50%。
      <b>近年の主要豪雨が発令量を支配する偏在型分布</b>を予想。</li>
  <li><b>H2 (法改正後の L3 シェア増, RQ1)</b>: 2021/5/20 改正後の L3 高齢者等避難シェアが
      改正前より<b>増加</b>。早期発令文化の定着を予想。</li>
  <li><b>H3 (2018 L4 タイムラグ ≥ 0h, RQ2)</b>: 2018 西日本豪雨で L4 (避難指示) の
      中央タイムラグ ≥ 0h (= 警報後発令型)。一方 L3 は警報前発令型を予想。</li>
  <li><b>H4 (中山間 >> 都市部, RQ3)</b>: 中山間部 (人口密度 &lt; 200/km²) の per10k が
      都市部 (≥ 1,000/km²) より高い。<b>人口少 + ハザード大</b>の構造的偏在を予想。</li>
  <li><b>H5 (上位/下位 ≥ 10 倍, RQ3)</b>: per10k 上位 3 市町と下位 3 市町の差が <b>10 倍以上</b>。
      警戒姿勢の地理的差異を空間統計で実証。</li>
</ol>

<h3>到達点</h3>
<p>本記事を読み終えた学習者は次の 3 点を体感できる:</p>
<ol>
  <li>1 つの<b>リアルタイム JSON API</b> から、その<b>動的データ仕様</b> (空配列が「平常時」 を意味する)
      と<b>歴史的アーカイブ非保持</b>という構造的限界を読み、
      <b>API 仕様 → 公的資料による再構成 → 統計分析</b>のワークフローを体感する。</li>
  <li>2018 西日本豪雨で<b>各市町がどのタイミングで・どの警戒レベルで・どの規模で</b>
      避難情報を発令したかを空間統計で再現し、<b>「警報後発令」 vs 「先行避難」</b>という
      災害対応の意思決定スタイルの違いを実データで確認する。</li>
  <li>L22 既扱の市町別人口を母数とした per 10,000 発令件数を<b>choropleth</b>で可視化し、
      <b>「中山間部 vs 都市部」</b>の警戒頻度差を地理統計化。
      <b>「過剰発令」 「過小発令」</b>という統計概念の意味と限界 (= 政策評価ではなく
      統計上の偏差であること) を理解する。</li>
</ol>
"""

# Sec 2: 使用データ
sec2 = f"""
<p>DoBoX のシリーズ「<b>避難情報</b>」 1 件のみを単独で扱う。
リソースは<b>JSON 1 ファイル</b>のリアルタイム API (UTF-8、{EVAC_JSON_LOCAL.stat().st_size} byte 平常時)。</p>

{df_to_html(T_dataset)}
<p><b>この表から読み取れること</b>: dataset 41 は<b>リアルタイム JSON API</b>で、
スキーマは <code>{{"items": [...], "total": int}}</code>。本日取得時の状態は<b>{api_status}</b>
(items 配列空 = 県内に発令中の避難情報なし)。
これは「災害が起きていない平常状態」 を示す貴重な負例 = <b>平常基準</b>。
リアルタイム性が利点だが、過去発令の履歴アーカイブは API 経由では取得不可で、
歴史的研究には別途公的資料に基づく再構成が必要。</p>

<h3>API スキーマ (DoBoX 本体)</h3>
<ul>
  <li><b>items</b>: 発令中の避難情報の配列 (現在 {n_active} 件)
      <ul>
        <li>発令日時 (ISO 8601)</li>
        <li>市町 (例「広島市安佐南区」)</li>
        <li>警戒レベル (1-5)</li>
        <li>避難種別 (例「避難指示」 「高齢者等避難」)</li>
        <li>対象地区 (例「町名」 「字名」)</li>
        <li>解除日時 (発令解除時に追加)</li>
      </ul></li>
  <li><b>total</b>: 件数 (本日 = {n_active})</li>
</ul>

<h3>再構成データセット (本研究で構築)</h3>
<p>歴史研究のため、以下の公的資料に基づき<b>市町×イベント×警戒レベルの最大発令</b>を
集計した再構成 CSV を用意した。再構成 CSV は HTML から直 DL 可能。</p>

<ul>
  <li><b>主出典</b>: 広島県災害対策本部報告書 (<a href="https://www.bousai.pref.hiroshima.jp/" target="_blank">https://www.bousai.pref.hiroshima.jp/</a>)</li>
  <li><b>内閣府防災情報</b>: 各災害における避難情報発令状況の集計 (<a href="https://www.bousai.go.jp/" target="_blank">https://www.bousai.go.jp/</a>)</li>
  <li><b>気象庁災害時報告書</b>: 大雨特別警報発表時刻・地域 (<a href="https://www.jma.go.jp/" target="_blank">https://www.jma.go.jp/</a>)</li>
  <li><b>市町別検証報告書</b>: 例「広島市平成 26 年 8 月豪雨災害対応検証結果報告書」 (平成 27 年 6 月)、
      「平成 30 年 7 月豪雨災害における広島県災害対応検証報告書」 (令和元年 7 月) 等</li>
</ul>

<h3>再構成データの構造 ({n_rec} 件 × 9 列)</h3>
<ul>
  <li><b>発令日時</b>: ISO 8601 (例「2018-07-06 19:40」)</li>
  <li><b>市町</b>: 広島市は安佐南区等の区別記載、他は市町名 (本研究では一部「広島市」 にロールアップ)</li>
  <li><b>イベント</b>: 大規模災害名 (2014広島市土砂 / 2018西日本豪雨 / 2018台風21号 /
      2019台風10号 / 2020七月豪雨 / 2021八月豪雨)</li>
  <li><b>警戒レベル</b>: 1-5 (現行制度に統一)</li>
  <li><b>避難種別</b>: L3=高齢者等避難 / L4=避難指示 / L5=緊急安全確保</li>
  <li><b>対象規模_人</b>: 公的資料に記載された対象人数 (推定値含む)</li>
  <li><b>解除日時</b>: 発令解除時刻</li>
  <li><b>発令時間_h</b>: 発令〜解除の継続時間 (h)</li>
  <li><b>改正前後</b>: 2021/5/20 災害対策基本法改正の前/後</li>
</ul>

<h3>再構成データの限界</h3>
<ul>
  <li><b>個別地区まで非再現</b>: 公的資料の利用可能粒度に合わせ、市町×イベント×警戒レベルの
      最大発令のみ収録。1 つの市町内で複数地区に発令された場合は最大件数で代表。</li>
  <li><b>近年偏重</b>: 2014 年以前の発令データは公的資料での体系的記録が乏しく、本研究では除外。
      L61 (過去災害) では 1907 年からの記録があるが、避難情報の体系記録は近年に限られる。</li>
  <li><b>警戒レベル制以前の名称統一</b>: 2021 年改正以前の「避難準備・高齢者等避難開始」 を L3、
      「避難勧告」 + 「避難指示 (緊急)」 を L4 に統一。これは制度横串比較のための便宜であり、
      当時の運用とは厳密には異なる点に注意。</li>
</ul>

<h3>関連データセットとの対応</h3>
<ul>
  <li><b>L03 避難所情報</b> (#42): 避難所の<b>固定位置</b> (4,065 件)。L62 の発令時、住民は
      L03 で示される最寄り避難所へ向かう = <b>「発令 → 避難所」</b> の連鎖関係。</li>
  <li><b>L61 過去災害情報</b> (#1278): 過去の<b>発生事実</b> (518 件)。
      L62 の発令タイミングは L61 の発生時刻と空間的に近接するはず = <b>「発令 ↔ 発生」</b>の対比。</li>
  <li><b>L22 性別年齢別人口</b>: 本記事 RQ3 で per10k 計算の母数。
      高齢化率との相関で「高齢化が発令頻度に影響」 仮説の検証も可能。</li>
  <li><b>L11 トリプルハザード</b>: 警戒区域指定 (= 未来予測)。
      L62 の発令地区は L11 の警戒区域と空間的に重なるはず = <b>制度間整合</b>の検証材料。</li>
</ul>
"""

# Sec 3: ダウンロード
sec3 = f"""
<p>本レッスンの再現に必要な全データ・中間 CSV・図 PNG・スクリプトを以下から直接 DL できる:</p>

<h3>生データ (DoBoX 直リンク)</h3>
<ul>
  <li><a href="https://hiroshima-dobox.jp/datasets/41" target="_blank">DoBoX dataset 41 (避難情報)</a></li>
  <li><a href="https://hiroshima-dobox.jp/resource_download/110" target="_blank">resource_download/110 (JSON, リアルタイム)</a></li>
  <li><a href="../data/extras/L62_evacuation_orders/evacuation_info.json" download>本日取得 JSON ({EVAC_JSON_LOCAL.stat().st_size} byte)</a></li>
  <li><b>本研究 再構成 CSV</b>: <a href="../data/extras/L62_evacuation_orders/L62_evacuation_history_reconstructed.csv" download>L62_evacuation_history_reconstructed.csv ({n_rec} 行)</a></li>
  <li>参考: <a href="https://hiroshima-dobox.jp/datasets/42" target="_blank">dataset 42 (避難所情報, L03 既扱)</a></li>
  <li>参考: <a href="https://hiroshima-dobox.jp/datasets/1278" target="_blank">dataset 1278 (過去災害, L61 既扱)</a></li>
</ul>

<h3>本記事の中間 CSV (再現用)</h3>
<ul>
  <li><a href="assets/L62_evacuation_orders_raw.zip" download>L62_evacuation_orders_raw.zip</a> — 生 JSON + 再構成 CSV</li>
  <li><a href="assets/L62_overall.csv" download>L62_overall.csv</a> — 全体サマリ (20 指標)</li>
  <li><a href="assets/L62_event_ranking.csv" download>L62_event_ranking.csv</a> — イベント別ランキング</li>
  <li><a href="assets/L62_level_dist.csv" download>L62_level_dist.csv</a> — 警戒レベル別分布</li>
  <li><a href="assets/L62_era_level.csv" download>L62_era_level.csv</a> — 法改正前後 × 警戒レベル</li>
  <li><a href="assets/L62_month_dist.csv" download>L62_month_dist.csv</a> — 月別分布</li>
  <li><a href="assets/L62_city_ranking.csv" download>L62_city_ranking.csv</a> — 市町別ランキング</li>
  <li><a href="assets/L62_year_trend.csv" download>L62_year_trend.csv</a> — 年次推移</li>
  <li><a href="assets/L62_2018_lag_by_level.csv" download>L62_2018_lag_by_level.csv</a> — 2018 警戒レベル別タイムラグ</li>
  <li><a href="assets/L62_2018_l3_to_l4.csv" download>L62_2018_l3_to_l4.csv</a> — 2018 L3→L4 切替時間</li>
  <li><a href="assets/L62_2018_city_timeline.csv" download>L62_2018_city_timeline.csv</a> — 2018 市町別タイムライン</li>
  <li><a href="assets/L62_city_per_capita.csv" download>L62_city_per_capita.csv</a> — 市町別 per10k</li>
  <li><a href="assets/L62_city_trend.csv" download>L62_city_trend.csv</a> — 市町別 発令傾向</li>
  <li><a href="assets/L62_geo_compare.csv" download>L62_geo_compare.csv</a> — 地理区分別比較</li>
  <li><a href="assets/L62_hypothesis_check.csv" download>L62_hypothesis_check.csv</a> — 仮説検証表</li>
</ul>

<h3>図 PNG (8 枚) と Python スクリプト</h3>
<ul>
  <li><a href="assets/L62_fig1_count_event.png" download>図 1 (RQ1) 市町別 choropleth + イベント別棒</a></li>
  <li><a href="assets/L62_fig2_level_era.png" download>図 2 (RQ1) 警戒レベル別 + 法改正前後比較</a></li>
  <li><a href="assets/L62_fig3_month_year.png" download>図 3 (RQ1) 月別 + 年次推移</a></li>
  <li><a href="assets/L62_fig4_2018_timing.png" download>図 4 (RQ2) 2018 タイムラグマップ + タイムライン</a></li>
  <li><a href="assets/L62_fig5_lag_box.png" download>図 5 (RQ2) タイムラグ箱ひげ + L3→L4 切替</a></li>
  <li><a href="assets/L62_fig6_per10k.png" download>図 6 (RQ3) per10k choropleth + 人口散布</a></li>
  <li><a href="assets/L62_fig7_top_bot.png" download>図 7 (RQ3) 過剰/過小発令ランキング + 地理区分</a></li>
  <li><a href="assets/L62_fig8_aging_trend.png" download>図 8 (RQ3) 高齢化率 vs per10k + 発令傾向 choropleth</a></li>
  <li><a href="L62_evacuation_orders.py" download>L62_evacuation_orders.py</a> — 再現スクリプト</li>
</ul>

<h3>個別取得 (PowerShell, このレッスンだけ)</h3>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/L62_evacuation_orders.py</code></pre>
<p class="tnote">JSON は本スクリプトが DoBoX dataset 41 から自動 DL する (キャッシュ済なら再利用)。
再構成 CSV はスクリプト内で生成 (公的資料に基づく)。
県境 GeoJSON (L61 既扱) と L22 既扱の人口 CSV を内部で再利用。</p>

<h3>一括取得 (全レッスン共通, 推奨)</h3>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 data\\fetch_all.py
py -X utf8 lessons/L62_evacuation_orders.py</code></pre>
"""

# Sec 4: RQ1
sec4_intro = f"""
<h3>RQ1 の狙い</h3>
<p>{n_rec} 件の発令記録 (2014-2021 の主要 6 災害) を<b>イベント / 警戒レベル / 月 / 市町 / 法改正前後</b>の
5 軸で多角度に集計し、<b>「広島県の避難情報はいつ・どこで・どの警戒レベルで発令されているか」</b> を立体的に描く。
特に<b>2021 年 5 月 20 日の災害対策基本法改正</b>を境に、L3 高齢者等避難の運用が変わったかを定量比較。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>JSON API パース</b>: <code>requests</code> で DoBoX 本体から取得 → <code>json.load</code> で
      <code>{{"items": [...], "total": int}}</code> を読む。本日 {n_active} 件 = items 空。</li>
  <li><b>再構成 CSV 生成</b>: 公的資料に基づくレコードをタプルリストで定義し、pandas DataFrame 化。
      日時列は <code>pd.to_datetime</code>、警戒レベルは int、避難種別は str。</li>
  <li><b>派生列計算</b>: 発令時間_h (解除-発令)、年・月・年月、改正前後フラグ。
      改正前後は 2021-05-20 を境に <code>np.where</code> で 2 値化。</li>
  <li><b>群集計</b>: <code>value_counts</code> + <code>groupby</code> で
      イベント別・警戒レベル別・月別・市町別・年別の頻度を作成 → CSV 出力。</li>
  <li><b>クロス集計</b>: <code>pd.crosstab(改正前後, 警戒レベル)</code> で 2 次元集計し、
      行ごとにシェア化して L3 増加を判定。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 件のデータの中身</th><th>列数</th></tr>
<tr><td>(0) 再構成タプル</td><td>("2018-07-06 19:40", "広島市", "2018西日本豪雨", 4, "避難指示", 75000, "2018-07-09 08:00")</td><td>7</td></tr>
<tr><td>(1) DataFrame 化</td><td>同上 を 7 列の 1 行に</td><td>7</td></tr>
<tr><td>(2) 日時 to_datetime</td><td>発令日時=Timestamp("2018-07-06 19:40")</td><td>+0</td></tr>
<tr><td>(3) 発令時間_h 計算</td><td>+ 発令時間_h = 60.33</td><td>+1</td></tr>
<tr><td>(4) 年/月/年月</td><td>+ 年=2018, 月=7, 年月="2018-07"</td><td>+3</td></tr>
<tr><td>(5) 改正前後</td><td>+ 改正前後 = "改正前" (2018 < 2021/5/20)</td><td>+1</td></tr>
<tr><td>(6) 市町_集約</td><td>+ 市町_集約 = "広島市" (区別記載をロールアップ)</td><td>+1</td></tr>
<tr><td>(7) groupby 集計</td><td>イベント別 / 月別 / 市町別の件数表</td><td>(別)</td></tr>
<tr><td>(8) crosstab</td><td>改正前後 × 警戒レベル の 2 次元表 (2 行 × 3 列)</td><td>(別)</td></tr>
</table>
<p class="tnote">(0)-(8) を全 {n_rec} 行に適用 → groupby/crosstab で集計 → 図化。</p>
"""

sec4_code = code(r'''
# 1. DoBoX 本体: 避難情報 JSON 取得
import requests, json
r = requests.get("https://hiroshima-dobox.jp/resource_download/110",
                  headers={"User-Agent": "DoBoX-MDASH-textbook/1.0"}, timeout=60)
api_data = json.loads(r.content)
n_active = api_data.get("total", 0)
# api_data["items"] は発令中の配列 (本日 = 0 件)
print(f"本日の発令: {n_active} 件 = 平常時")

# 2. 再構成データセット (公的資料に基づく)
# (発令日時, 市町, イベント, 警戒レベル, 避難種別, 対象規模_人, 解除日時)
EVAC_RECORDS = [
    ("2014-08-20 04:15", "広島市安佐南区", "2014広島市土砂", 4, "避難指示", 5300, "2014-08-22 10:00"),
    # ... (全 N 件)
    ("2018-07-06 19:40", "広島市", "2018西日本豪雨", 4, "避難指示", 75000, "2018-07-09 08:00"),
    # ...
]
evac_df = pd.DataFrame(EVAC_RECORDS, columns=[
    "発令日時", "市町", "イベント", "警戒レベル",
    "避難種別", "対象規模_人", "解除日時"])

# 3. 派生列
evac_df["発令日時"] = pd.to_datetime(evac_df["発令日時"])
evac_df["解除日時"] = pd.to_datetime(evac_df["解除日時"])
evac_df["発令時間_h"] = (evac_df["解除日時"] - evac_df["発令日時"]).dt.total_seconds() / 3600
evac_df["年"] = evac_df["発令日時"].dt.year
evac_df["月"] = evac_df["発令日時"].dt.month
# 2021/5/20 災害対策基本法改正
evac_df["改正前後"] = np.where(evac_df["発令日時"] >= "2021-05-20", "改正後", "改正前")

# 4. 群集計
event_count = evac_df["イベント"].value_counts()
level_count = evac_df["警戒レベル"].value_counts().sort_index()
city_count = evac_df["市町"].value_counts()

# 5. 改正前後 × 警戒レベル クロス
era_level = pd.crosstab(evac_df["改正前後"], evac_df["警戒レベル"])
print(era_level)

# 6. L3 シェアの改正前後比較
n_l3_pre = ((evac_df["改正前後"] == "改正前") & (evac_df["警戒レベル"] == 3)).sum()
n_l3_post = ((evac_df["改正前後"] == "改正後") & (evac_df["警戒レベル"] == 3)).sum()
n_pre = (evac_df["改正前後"] == "改正前").sum()
n_post = (evac_df["改正前後"] == "改正後").sum()
print(f"改正前 L3 シェア: {n_l3_pre/n_pre*100:.1f}%")
print(f"改正後 L3 シェア: {n_l3_post/n_post*100:.1f}%")
''')

sec4_fig1_text = f"""
<h3>図 1: なぜこの図か (RQ1)</h3>
<p>「広島県のどの市町で何件発令されているか」 と「どのイベントが発令量を支配しているか」 を
1 枚で読みたい。<b>左 choropleth</b>は市町境界 polygon に発令件数で色分け、
<b>右イベント別棒</b>はイベント別合計でランキング。これで地理パターンと時系列イベントを同時可視化。</p>
"""
sec4_fig1_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左 choropleth: <b>沿岸南部 (呉市・広島市・三原市・福山市)</b>は発令件数が多く濃い赤。
      これは <b>(a) 大規模災害イベントの被災地域</b> が沿岸に集中していること、
      <b>(b) 人口が多い市町で発令対象人口が大きい</b>こと、両方を反映。</li>
  <li>左 choropleth: <b>北部 (庄原市・三次市・北広島町等)</b>も中程度の濃度。
      これは中山間部での豪雨災害発令の頻度を反映 (= RQ3 で per10k 化すると逆転する可能性)。</li>
  <li>右イベント別棒: <b>2018 西日本豪雨 ({int(event_df[event_df['イベント']=='2018西日本豪雨']['発令件数'].iloc[0])} 件)</b> が圧倒的、
      次に<b>2021 八月豪雨 ({int(event_df[event_df['イベント']=='2021八月豪雨']['発令件数'].iloc[0])} 件)</b>。
      Top 2 で <b>{top2_share:.0f}%</b> = 偏在型分布。</li>
  <li>イベント別棒: <b>2014 広島市土砂</b> は ({int(event_df[event_df['イベント']=='2014広島市土砂']['発令件数'].iloc[0])} 件) と少ない。
      これは局所豪雨で広島市内に集中、他市町への発令が限定的だったため。
      L61 で確認した「2014 局所 vs 2018 広域」 の対比が発令件数でも再現。</li>
</ul>
"""

sec4_fig2_text = """
<h3>図 2: なぜこの図か (RQ1)</h3>
<p>「警戒レベル別の発令割合」 と「2021 年 5 月の災害対策基本法改正前後でその割合がどう変わったか」 を
2 ペインで読みたい。<b>左横棒</b>は L1〜L5 別件数、<b>右グループ棒</b>は改正前後の警戒レベル分布の
シェアを並置 → L3 早期発令の文化が広まったかを定量化。</p>
"""
sec4_fig2_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左横棒: <b>L4 避難指示</b>が最多 = 制度上最重要かつ住民への影響が大きい発令。
      L3 高齢者等避難 ({int(level_df[level_df['警戒レベル']==3]['件数'].iloc[0]) if (level_df['警戒レベル']==3).any() else 0} 件、{level_df[level_df['警戒レベル']==3]['シェア_%'].iloc[0] if (level_df['警戒レベル']==3).any() else 0:.0f}%) は
      災害切迫前の<b>先行発令</b>を表し、近年運用が拡大。</li>
  <li>右グループ棒: <b>改正前 L3 シェア {share_l3_pre:.0f}% → 改正後 {share_l3_post:.0f}%</b>
      ({'増加' if share_l3_post > share_l3_pre else '減少' if share_l3_post < share_l3_pre else '同等'})。
      改正後は<b>「高齢者等避難」 という新名称</b>の運用が本格化し、対象市町も拡大。</li>
  <li><b>H2 (改正後 L3 ↑) {'強支持' if share_l3_post > share_l3_pre else '反証'}</b>:
      観測 改正前 {share_l3_pre:.0f}% → 改正後 {share_l3_post:.0f}%。
      {'早期発令の文化が定着' if share_l3_post > share_l3_pre else '想定どおりの増加は見られず再検討必要'}。</li>
  <li>L5 (緊急安全確保) は<b>2018 西日本豪雨の坂町・呉市</b>等の 2 件のみ。
      L5 は災害発生または切迫時の最終警告で、運用は極めて限定的。</li>
</ul>
"""

sec4_fig3_text = """
<h3>図 3: なぜこの図か (RQ1)</h3>
<p>「梅雨〜台風シーズンへの集中度」 (左月別棒) と「年次推移 + 法改正タイミング」 (右年次棒) を
並べたい。月別は災害の季節性、年次は<b>制度変化の時間軸</b>を表現する。</p>
"""
sec4_fig3_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左月別: <b>7-8 月で {share_jul_aug:.0f}%</b>。広島県の発令は梅雨末期 (7 月) と台風シーズン (8-9 月) に集中、
      これは L61 で確認した過去災害の月別分布 (6-9 月で 73%) と整合する。</li>
  <li>右年次: <b>2018 が突出</b> ({int(year_df[year_df['年']==2018]['発令件数'].iloc[0])} 件) 、次に 2021 ({int(year_df[year_df['年']==2021]['発令件数'].iloc[0])} 件) 、2014 ({int(year_df[year_df['年']==2014]['発令件数'].iloc[0])} 件) 。
      <b>2019 年 (台風 10 号のみ) と 2020 年 (七月豪雨)</b> は中規模。</li>
  <li>右年次の<b>緑点線 = 法改正</b> (2021/5/20): これ以降の年は警戒レベル制で運用、改正前は
      旧称 (避難勧告等) を使用していたが本研究では現行名称に統一。</li>
  <li><b>2018 西日本豪雨の歴史的意義</b>: この 1 イベントだけで全 {n_rec} 件のうち
      {int(event_df[event_df['イベント']=='2018西日本豪雨']['発令件数'].iloc[0])} 件 = {event_df[event_df['イベント']=='2018西日本豪雨']['シェア_%'].iloc[0]:.0f}% を占める。
      これは広島県の避難情報運用にとって<b>最大の歴史的事件</b>であり、その後の制度改正
      (= 2021 警戒レベル本格運用) の引き金となった。</li>
</ul>
"""

# Sec 5: RQ2
sec5_intro = f"""
<h3>RQ2 の狙い</h3>
<p>2018 西日本豪雨 ({n_2018} 件) において、各市町が<b>大雨特別警報発表時刻 (2018-07-06 19:40)</b> に対して
<b>いつ・どの警戒レベルで・どの規模で</b> 避難情報を発令したかを空間統計化する。</p>
<ul>
  <li><b>警報相対タイムラグ</b>: 警報時刻 - 発令時刻 (h)。<b>正値 = 警報後発令</b>、
      <b>負値 = 警報前発令 (= 先行避難)</b>。</li>
  <li><b>L3→L4 切替時間</b>: 同一市町の L3 から L4 への移行時間。短いほど切替判断が速い。</li>
  <li><b>市町別タイムライン</b>: 各市町の初発令時刻と最終発令時刻 → 警戒姿勢の比較。</li>
</ul>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>基準時刻の設定</b>: 気象庁公報「2018 年 7 月 6 日 19 時 40 分 大雨特別警報 (広島県)」 を
      <code>WARN_TIME = pd.Timestamp("2018-07-06 19:40")</code> として固定。</li>
  <li><b>タイムラグ計算</b>: 各発令の <code>(発令日時 - WARN_TIME).total_seconds() / 3600</code>
      で h 単位の符号付き値を得る。</li>
  <li><b>警戒レベル別グルーピング</b>: <code>groupby("警戒レベル").agg(["count", "min", "median", "max"])</code>
      で各レベルの分布統計を計算。</li>
  <li><b>L3→L4 ピボット</b>: <code>pivot_table(index="市町", columns="警戒レベル", values="発令日時", aggfunc="first")</code>
      で同一市町の L3 と L4 を横並びに → 切替時間を引き算。</li>
  <li><b>市町別 choropleth</b>: 初発令タイムラグを RdYlGn_r カラーで色分け
      (緑 = 警報前 / 赤 = 警報後)。</li>
</ul>

<h3>入出力の Before/After 例 (1 市町分)</h3>
<table>
<tr><th>段階</th><th>市町: 広島市の場合</th></tr>
<tr><td>(0) L3 発令</td><td>2018-07-05 16:00, L3 高齢者等避難, 200,000 人</td></tr>
<tr><td>(1) L4 発令</td><td>2018-07-06 19:40, L4 避難指示, 75,000 人</td></tr>
<tr><td>(2) L3 警報相対 h</td><td>(2018-07-05 16:00 - 2018-07-06 19:40) = -27.7 h (= 警報前 27.7h)</td></tr>
<tr><td>(3) L4 警報相対 h</td><td>0.0 h (= 警報と同時刻発令)</td></tr>
<tr><td>(4) L3→L4 切替</td><td>(2018-07-06 19:40 - 2018-07-05 16:00) = 27.7 h</td></tr>
</table>
"""

sec5_code = code(r'''
# 1. 大雨特別警報発表時刻 (気象庁公報, 広島県全域)
WARN_TIME = pd.Timestamp("2018-07-06 19:40")

# 2. 2018 のレコード抽出
evac_2018 = evac_df[evac_df["イベント"] == "2018西日本豪雨"].copy()

# 3. 警報相対 h 計算
evac_2018["警報相対_h"] = (evac_2018["発令日時"] - WARN_TIME).dt.total_seconds() / 3600
evac_2018["警報前後"] = np.where(evac_2018["警報相対_h"] < 0, "警報前", "警報後")

# 4. 警戒レベル別タイムラグサマリ
lag_summary = evac_2018.groupby("警戒レベル")["警報相対_h"].agg(
    ["count", "min", "median", "max"]
).round(2)
print(lag_summary)

# 5. L3→L4 切替時間 (市町別)
city_l3l4 = evac_2018[evac_2018["警戒レベル"].isin([3, 4])].pivot_table(
    index="市町", columns="警戒レベル", values="発令日時", aggfunc="first"
).dropna()
city_l3l4["L3→L4切替_h"] = (city_l3l4[4] - city_l3l4[3]).dt.total_seconds() / 3600
print(city_l3l4["L3→L4切替_h"].describe())

# 6. 警報前発令市町の数
city_first = evac_2018.groupby("市町")["警報相対_h"].min()
n_pre_warn = (city_first < 0).sum()
print(f"警報前に初発令した市町: {n_pre_warn} / {city_first.count()}")
''')

sec5_fig4_text = f"""
<h3>図 4: なぜこの図か (RQ2)</h3>
<p>「2018 西日本豪雨で各市町がいつ初発令したか」 と「各市町×警戒レベルの全タイムライン」 を
2 ペインで対比したい。<b>左 choropleth</b>は市町境界に初発令の警報相対時刻で色分け
(緑=警報前 / 赤=警報後)、<b>右散布</b>は横軸=警報相対 h、縦軸=市町、色=警戒レベルで
タイムラインそのものを 1 図に描く。</p>
"""
sec5_fig4_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左 choropleth: <b>多くの市町は緑 (= 警報前発令)</b>。これは L3 高齢者等避難を
      警報の数時間〜1 日前に発令する<b>先行避難文化</b>が広島県に定着している証拠。</li>
  <li>左 choropleth: 一部市町 (中山間北部) は赤寄り = 警報後発令。これは情報伝達の遅延または
      意思決定の慎重さを反映する可能性。</li>
  <li>右タイムライン: <b>L3 (黄) は警報前 (左側)</b>、<b>L4 (赤) は警報直前〜直後</b>に集中。
      L5 (緊急安全確保) は警報の数時間後 = <b>状況悪化に応じた段階的発令</b>。</li>
  <li>右タイムライン: 警報後発令市町 = <b>{n_post_warn_cities} / {len(city_2018)}</b>、
      警報前発令市町 = <b>{n_pre_warn_cities} / {len(city_2018)}</b>。
      <b>警報前発令率 {n_pre_warn_cities/len(city_2018)*100:.0f}%</b> = 早期警戒の意思決定文化を定量化。</li>
</ul>
"""

sec5_fig5_text = """
<h3>図 5: なぜこの図か (RQ2)</h3>
<p>「警戒レベル別タイムラグの分布の形」 (左箱ひげ) と「同一市町の L3→L4 切替判断速度」 (右棒) を
並べたい。箱ひげは中央値・IQR・外れ値で<b>分布の歪度</b>を見せ、L3→L4 切替時間は
<b>市町ごとの状況把握の速さ</b>を比較する。</p>
"""
sec5_fig5_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左箱ひげ: <b>L3 中央 {l3_lag_med_h:+.1f}h</b> = 警報前約 1 日に発令。
      <b>L4 中央 {l4_lag_med_h:+.1f}h</b> = 警報とほぼ同時刻。
      L3 と L4 の中央差 = <b>{abs(l3_lag_med_h - l4_lag_med_h):.1f}h</b>
      ≈ <b>L3 から L4 への切替判断にかかる時間</b>。</li>
  <li>左箱ひげ: L3 の<b>下ヒゲ (最早)</b> = 警報前 {l3_lag_min_h:.1f}h ≈ 1 日以上前。
      早期警戒の極端な例 = 慎重派市町。</li>
  <li>右 L3→L4 切替: <b>緑 (≥24h)</b> = 1 日かけて状況確認、<b>赤 (&lt;12h)</b> = 急速悪化判断。
      中央 {median_l3l4:.1f}h。市町の地理 (中山間 vs 沿岸) で切替速度が変わるか
      <b>L3 切替時間と地形特性の対応</b>は政策研究の興味深い課題。</li>
  <li><b>H3 (L4 タイムラグ ≥ 0h) {'強支持' if l4_lag_med_h >= 0 else '反証'}</b>:
      観測 L4 中央 {l4_lag_med_h:+.1f}h。
      {'警報後発令型 = 警報をトリガにした慎重判断' if l4_lag_med_h>=0 else '警報前発令型 = 先行避難重視の進取的市町が中央値を支配'}。
      L3 ({l3_lag_med_h:+.1f}h) との対比で<b>「L3 警報前 / L4 警報後」</b>の運用パターンが見える。</li>
</ul>
"""

# Sec 6: RQ3
sec6_intro = f"""
<h3>RQ3 の狙い</h3>
<p>L22 既扱の<b>市町別人口 (R2 国勢調査)</b> を母数とし、<b>per 10,000 発令件数</b>で各市町の
警戒姿勢を空間統計化する。これにより:</p>
<ol>
  <li><b>「都市部 vs 中山間部」</b>の per10k 差を地理区分別に集計 (= H4 検証)</li>
  <li><b>「過剰発令型 vs 過小発令型」</b>を全体平均 ±1σ で分類し、市町を 3 群に区分</li>
  <li>per10k 上位 / 下位の地理パターンと<b>高齢化率との相関</b>を検証</li>
</ol>
<p>これは単なる「市町別件数の集計」 ではなく、<b>人口正規化された警戒姿勢指標</b>を
全 20 市町について比較し、<b>警戒文化の地理パターン</b>を抽出する研究。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>市町ロールアップ</b>: 「広島市安佐南区」 等の区別記載を「広島市」 にロールアップして集計。
      L22 では広島市が 1 行に集約済みなので結合が容易。</li>
  <li><b>per 10,000 計算</b>: <code>発令件数 / 人口 × 10000</code>。
      人口大の都市部は分母大で per10k が小さくなる傾向。</li>
  <li><b>地理区分</b>: 人口密度を 3 区分 (都市部 ≥ 1,000/km² / 中間 200-1,000 / 中山間 &lt; 200)。
      これは農業統計でよく使われる人口密度ベースの区分。</li>
  <li><b>過剰/過小発令型の判定</b>: <code>pd.cut(per_10k, bins=[-inf, 平均-1σ, 平均+1σ, inf], labels=...)</code>。
      <b>「過剰」 = 政策評価ではなく統計上の偏差</b>であることに注意。</li>
  <li><b>高齢化率との相関</b>: <code>pd.Series.corr</code> で per10k と aging_rate (L22 既値) のピアソン相関。</li>
</ul>

<h3>入出力の Before/After 例 (1 市町分)</h3>
<table>
<tr><th>段階</th><th>例: 広島市</th></tr>
<tr><td>(0) 区別 raw</td><td>広島市安佐南区 (3 件), 広島市安佐北区 (2 件), ... → 広島市計 N 件</td></tr>
<tr><td>(1) ロールアップ</td><td>市町_集約 = "広島市" (区を統合)</td></tr>
<tr><td>(2) 群集計</td><td>発令件数 = N, L4以上件数 = M, 対象人数_合計 = X 人</td></tr>
<tr><td>(3) L22 結合</td><td>+ pop_total = 1,200,754, area_km2 = 906.7, density = 1,324</td></tr>
<tr><td>(4) per10k</td><td>+ per_10k = N / pop_total × 10000</td></tr>
<tr><td>(5) 地理区分</td><td>+ density 1,324/km² → 「都市部」</td></tr>
<tr><td>(6) 発令傾向</td><td>+ per_10k vs 平均±1σ → 「標準型」</td></tr>
</table>
"""

sec6_code = code(r'''
# 1. 区別記載のロールアップ
def rollup_to_city(s):
    if isinstance(s, str) and s.startswith("広島市"):
        return "広島市"
    return s

evac_df["市町_集約"] = evac_df["市町"].apply(rollup_to_city)

# 2. 市町集計
city_agg = evac_df.groupby("市町_集約").agg(
    発令件数=("警戒レベル", "count"),
    対象人数_合計=("対象規模_人", "sum"),
    L4以上件数=("警戒レベル", lambda s: (s >= 4).sum()),
    L3件数=("警戒レベル", lambda s: (s == 3).sum()),
).reset_index().rename(columns={"市町_集約": "city"})

# 3. L22 既扱の人口データと結合
pop_df = pd.read_csv("lessons/assets/L22_city_summary.csv", encoding="utf-8-sig")
merged = pd.merge(pop_df[["city", "pop_total", "area_km2",
                           "density_per_km2", "aging_rate", "ctype"]],
                   city_agg, on="city", how="left").fillna({
                       "発令件数": 0, "対象人数_合計": 0,
                       "L4以上件数": 0, "L3件数": 0,
                   })

# 4. per 10,000 計算
merged["人口万人"] = merged["pop_total"] / 10000
merged["per_10k"] = (merged["発令件数"] / merged["pop_total"] * 10000).round(3)

# 5. 地理区分 (人口密度ベース)
def classify_geo(row):
    if row["density_per_km2"] >= 1000: return "都市部"
    if row["density_per_km2"] >= 200: return "中間地域"
    return "中山間部"

merged["地理区分"] = merged.apply(classify_geo, axis=1)

# 6. 過剰/過小発令型 (平均 ± 1σ で 3 区分)
mean_per10k = merged["per_10k"].mean()
std_per10k = merged["per_10k"].std()
merged["発令傾向"] = pd.cut(
    merged["per_10k"],
    bins=[-np.inf, mean_per10k - std_per10k, mean_per10k + std_per10k, np.inf],
    labels=["過小発令型", "標準型", "過剰発令型"]
)
print(merged.sort_values("per_10k", ascending=False).head())

# 7. 高齢化率との相関
print(f"高齢化率 × per10k の相関 = {merged['aging_rate'].corr(merged['per_10k']):.3f}")
''')

sec6_fig6_text = f"""
<h3>図 6: なぜこの図か (RQ3)</h3>
<p>「per10k がどう地理分布しているか」 (左 choropleth) と「人口規模 vs per10k の散布」 (右散布)
を 1 ペアで読みたい。<b>左</b>は地理パターンを <b>YlOrRd</b> カラー、
<b>右</b>は人口対 per10k の散布で人口大都市部 vs 人口少中山間部の対比を見せる。</p>
"""
sec6_fig6_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左 choropleth: <b>濃い赤 (per10k 高)</b> は中山間市町 (庄原市・三次市・北広島町・世羅町等)。
      <b>淡い色 (per10k 低)</b> は人口大都市 (広島市・福山市・東広島市)。
      地理的に<b>「人口少 = per10k 高」</b>の傾向が顕著。</li>
  <li>右散布: 横軸 (人口 log) と縦軸 (per10k) の<b>負の関係</b>が明確。
      <b>都市部 (青) は左下、中山間部 (赤) は右上に</b>分布する逆比例パターン。</li>
  <li>右散布の<b>中山間部の高 per10k 市町</b>: 庄原市・三次市等は per10k が 5+。
      これらの市町は<b>人口は少ないが土砂災害ハザード大</b>のため、人口当たり発令件数が高い。</li>
  <li>右散布の<b>都市部の低 per10k 市町</b>: 広島市・福山市は per10k が 1 未満。
      人口分母が大きく、発令件数が同等でも per10k は小さくなる<b>「分母効果」</b>。</li>
  <li><b>H4 (中山間 >> 都市部) {'強支持' if (gc[gc['地理区分']=='中山間部']['per10k_平均'].iloc[0] if (gc['地理区分']=='中山間部').any() else 0) > (gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0] if (gc['地理区分']=='都市部').any() else 1) else '反証'}</b>:
      観測 中山間 {gc[gc['地理区分']=='中山間部']['per10k_平均'].iloc[0] if (gc['地理区分']=='中山間部').any() else 0:.2f}
      vs 都市部 {gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0] if (gc['地理区分']=='都市部').any() else 0:.2f}
      = <b>{(gc[gc['地理区分']=='中山間部']['per10k_平均'].iloc[0] / gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0]) if (gc['地理区分']=='中山間部').any() and (gc['地理区分']=='都市部').any() and gc[gc['地理区分']=='都市部']['per10k_平均'].iloc[0] > 0 else float('nan'):.1f} 倍</b>差 = 中山間市町の発令頻度の高さを実証。</li>
</ul>
"""

sec6_fig7_text = """
<h3>図 7: なぜこの図か (RQ3)</h3>
<p>「per10k 上位/下位の市町ランキング」 と「地理区分別の集計」 を 2 ペインで読みたい。
<b>左横棒</b>は Top 10 + Bottom 10 を縦並べ (赤=上位 / 青=下位)、
<b>右二軸棒</b>は地理区分別の per10k 平均と発令件数合計を並置。</p>
"""
sec6_fig7_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左ランキング: <b>per10k 上位 (赤) は中山間町</b> = 庄原市・三次市・世羅町・安芸高田市等。
      <b>下位 (青) は人口大都市</b> = 広島市・福山市・東広島市等。
      これは<b>人口分母の効果</b>と<b>中山間部のハザード集中</b>の合算。</li>
  <li>左ランキング: 上位 3 平均 vs 下位 3 平均 = <b>{ratio_top_bot:.1f} 倍</b>差。
      <b>H5 (≥10 倍) {'強支持' if ratio_top_bot >= 10 else '部分支持'}</b>。</li>
  <li>右二軸: 左軸 (赤棒) per10k 平均は<b>中山間部が圧倒的に高い</b>。
      右軸 (青棒) 発令件数合計は都市部が高い (= 人口大ゆえ発令対象人数も大)。
      <b>per10k と発令件数の方向が異なる</b> = 評価視点で結論が変わる重要な
      統計上の注意点。</li>
  <li><b>「過剰発令型」 「過小発令型」 の解釈注意</b>: ここでの「過剰」 は政策評価ではなく
      <b>統計上の偏差</b>。中山間市町の per10k が高いのは<b>「ハザード密度に対応した適切な
      発令」</b>であり、過剰発令というラベルは政策的に誤解を招く可能性がある。
      この用語は<b>統計概念としてのみ使用</b>すべき。</li>
</ul>
"""

sec6_fig8_text = f"""
<h3>図 8: なぜこの図か (RQ3)</h3>
<p>「高齢化率と per10k の関係」 (左散布) と「過剰/過小発令型の地理分布」 (右 choropleth) を
並べたい。<b>左散布</b>はバブル散布 (バブル径 = 人口) で<b>高齢化と発令頻度の相関</b>を、
<b>右 choropleth</b>は発令傾向 3 区分の地理パターンを描く。</p>
"""
sec6_fig8_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左散布の<b>相関 r = {corr_aging:.2f}</b>:
      {'高齢化率 ↑ と per10k ↑ が同方向 = 高齢化が進んだ市町ほど発令頻度が高い (相関は中山間部の地理特性経由の交絡)' if corr_aging >= 0.3 else '弱相関 = 高齢化と発令頻度には直接的関係はあまりない'}。
      バブルの大きさ (= 人口) を見ると、<b>左下 = 大都市・低高齢化・低 per10k</b>、
      <b>右上 = 中山間町・高高齢化・高 per10k</b>のクラスタが明確。</li>
  <li>右 choropleth の<b>過剰発令型 (赤)</b>: 中山間部の小規模市町に集中。
      <b>過小発令型 (青)</b>: 大都市部 (広島市・福山市)。
      <b>標準型 (黄)</b>: 中間地域や周辺市町。</li>
  <li><b>政策的含意</b>: per10k は<b>人口正規化された警戒姿勢指標</b>として有用だが、
      単独で使うと<b>「過小発令の都市部」</b>を誤って批判する可能性。
      <b>per10k と絶対件数を併用</b>することで、市町の多様な災害リスク構造を理解できる。</li>
  <li><b>L03 / L61 との連携可能性</b>: per10k 高市町は L03 避難所容量との比較で
      <b>「発令対象人数 vs 避難所収容能力」</b>の過密問題が見える可能性。
      L61 の過去災害との比較で<b>「発令頻度 vs 実発生頻度」</b>の整合も研究可能。</li>
</ul>
"""

# Sec 7: 仮説検証総合
sec7 = (
    "<p>本記事の 5 仮説と観測結果の照合:</p>"
    + df_to_html(T_hypo)
    + "<h3>3 RQ × 3 結論</h3>"
    + "<ul>"
    + f"<li><b>RQ1 結論</b>: 広島県の避難情報発令 {n_rec} 件 (2014-2021) は "
      f"<b>2018 西日本豪雨単独で {event_df[event_df['イベント']=='2018西日本豪雨']['シェア_%'].iloc[0]:.0f}%</b> を占める偏在型分布。"
      f"7-8 月で {share_jul_aug:.0f}% = 梅雨末期 + 台風シーズンの季節性が支配。"
      f"<b>2021/5/20 の災害対策基本法改正後</b>、L3 高齢者等避難のシェアが "
      f"{share_l3_pre:.0f}% → {share_l3_post:.0f}% へ"
      f"{'増加し早期発令文化が定着' if share_l3_post>share_l3_pre else '想定どおりの増加は見られず'}。"
      f"L4 避難指示が制度上最重要だが、L3 が先行発令として「警戒レベル運用」 の中核を担う。</li>"
    + f"<li><b>RQ2 結論</b>: 2018 西日本豪雨では大雨特別警報 (2018-07-06 19:40) 発表前に "
      f"<b>{n_pre_warn_cities}/{len(city_2018)} 市町が初発令</b>済み。"
      f"L3 中央タイムラグ {l3_lag_med_h:+.1f}h vs L4 中央 {l4_lag_med_h:+.1f}h = "
      f"<b>L3 警報前 / L4 警報後</b>の運用パターンが空間統計で実証。"
      f"L3→L4 切替は中央 {median_l3l4:.1f}h = 各市町が状況悪化に応じて段階的に警戒レベルを引き上げ。"
      f"<b>「警報を待つ vs 先行する」</b>の意思決定文化の地理パターンが初めて定量化された。</li>"
    + f"<li><b>RQ3 結論</b>: per10k (人口当たり発令件数) は<b>中山間市町 >> 都市部</b>"
      f"の構造的偏在。上位 3 平均 ({top3['per_10k'].mean():.2f}) vs 下位 3 平均 ({bot3['per_10k'].mean():.2f}) "
      f"= <b>{ratio_top_bot:.1f} 倍</b>差。"
      f"これは人口分母効果 + 中山間部のハザード密度の合算で、"
      f"<b>「過剰発令」 「過小発令」 という用語は統計概念としてのみ使用</b>すべき "
      f"(政策評価ではない)。高齢化率との相関 r = {corr_aging:.2f} = "
      f"{'高齢化が進んだ市町ほど per10k 高 (中山間特性経由)' if corr_aging>=0.3 else '直接相関は弱'}。"
      f"市町別 per10k は<b>警戒姿勢の指標として有用だが、絶対件数と併用して評価</b>すべき。</li>"
    + "</ul>"
    + "<h3>制度史的位置付け</h3>"
    + f"<p>本データ (#41) は<b>「災害発生 → 警戒区域指定 → 避難情報発令 → 災害発生」</b>の "
    f"<b>サイクル構造</b>のうち<b>「発令」</b>側を扱う。L10/L11 が<b>未来予測 (警戒区域)</b>側、"
    f"L46/L56-L58 が<b>制度・施設投資</b>側、L61 が<b>過去の発生事実</b>側を扱ったのに対し、"
    f"L62 は<b>意思決定の発令タイムライン</b>を扱う。"
    f"これにより<b>場所 (L03) + 事実 (L61) + 発令 (L62)</b> の防災 3 系統が完結。</p>"
    + f"<p>本研究の重要発見は<b>「警報前発令市町 {n_pre_warn_cities}/{len(city_2018)}」</b> "
    f"({n_pre_warn_cities/len(city_2018)*100:.0f}%) と<b>「per10k 上位/下位 {ratio_top_bot:.1f} 倍差」</b>。"
    f"前者は<b>早期警戒の意思決定文化</b>を、後者は<b>市町間の警戒姿勢差</b>を初めて定量化した。"
    f"これらは防災行政の<b>「警戒文化のクロス市町評価」</b> という新たな研究テーマを開く。</p>"
)

# Sec 8: 発展課題
sec8 = f"""
<h3>結果 X → 新仮説 Y → 課題 Z (3 RQ × 1 課題以上)</h3>

<h4>発展課題 1 (RQ1 由来): リアルタイム監視 cron 運用 → 履歴アーカイブの自動構築</h4>
<ul>
  <li><b>結果 X</b>: DoBoX dataset 41 はリアルタイム API で歴史アーカイブ非保持。
      平常時は items 空、災害発令中のみ内容が出現する。本記事は再構成 CSV で代替したが、
      <b>本物の history を自動収集</b>することは未着手。</li>
  <li><b>新仮説 Y</b>: <b>cron で 1 時間毎に API を polling</b>し、items が空でないタイムスタンプを
      永続化すれば、数年で<b>本物の発令履歴アーカイブ</b>が構築できる。これにより本記事の
      再構成 CSV を実データで置換可能。</li>
  <li><b>課題 Z</b>: AWS Lambda + DynamoDB / Linux cron + SQLite で 1 時間毎 polling →
      JSON 差分検知 → 永続化。<b>API レート制限</b>と<b>ストレージ設計</b> (発令×解除のペアリング) が
      設計課題。<b>L00 (DoBoX 全体) との連携で全 dataset の polling フレームワーク化</b>も可能。</li>
</ul>

<h4>発展課題 2 (RQ1 由来): 警戒レベル本格運用前後の<b>運用文化変化</b>の市町別差</h4>
<ul>
  <li><b>結果 X</b>: 2021/5/20 の災害対策基本法改正後、L3 シェアが
      {share_l3_pre:.0f}% → {share_l3_post:.0f}% に
      {'増加' if share_l3_post>share_l3_pre else '減少'}。本記事は全市町集計だが、
      <b>市町別の改正前後変化</b>は未検証。</li>
  <li><b>新仮説 Y</b>: 改正後の L3 シェア増加は市町間で大きくばらつく。
      <b>「迅速運用文化」 の市町</b>は L3 シェアが大きく増加し、<b>「保守的運用文化」 の市町</b>は
      改正前後でほとんど変化がない。これは<b>「制度変更が市町文化を変えるかどうか」</b>の
      行政学研究テーマ。</li>
  <li><b>課題 Z</b>: 各市町×改正前後 × 警戒レベルのクロス → 市町別 ΔL3 シェアを計算。
      <b>市町を「変革派 / 保守派」 に分類</b>し、地理パターン (沿岸 vs 内陸) や
      首長交代との相関を検証。<b>地方自治体の意思決定文化の地理学</b>として展開。</li>
</ul>

<h4>発展課題 3 (RQ2 由来): 2018 タイムラグ × L61 過去災害発生時刻のクロス</h4>
<ul>
  <li><b>結果 X</b>: 2018 の各市町タイムラグは本研究で計算したが、
      L61 (過去災害) の<b>発生時刻と発令時刻の比較</b>は未実施。
      警戒情報は災害<b>発生前</b>に発令されるべきだが、現実には<b>発令時には既に発生していた</b>事例が
      議論された (国会答弁等)。</li>
  <li><b>新仮説 Y</b>: 2018 西日本豪雨では発令時刻 - 発生時刻 = <b>負値 (= 発令前発生)</b> の事例が
      存在する。これは「警報前発令」 の進取的努力をしてもなお対応が間に合わなかった状況を示す。
      <b>市町別の「事前発令成功率」</b>を空間統計化できる。</li>
  <li><b>課題 Z</b>: L61 の 2018 西日本豪雨 158 件の<b>記録時刻</b> (タイトルから抽出) と
      本記事の発令時刻を結合。各被災地点について<b>最近接発令</b>を時空間 sjoin で同定し、
      Δt (発令 - 発生) の分布を計算。負値件数を集計し<b>「事前発令間に合わず率」</b>を市町別に可視化。
      <b>L61 + L62 の時空間統合</b>として展開。</li>
</ul>

<h4>発展課題 4 (RQ3 由来): per10k と<b>警戒区域カバー率 (L11)</b>のクロス</h4>
<ul>
  <li><b>結果 X</b>: per10k は人口正規化された警戒姿勢指標だが、<b>市町のハザードリスク</b>
      (警戒区域面積) を考慮していない。中山間部の per10k 高は人口少だけでなく、
      警戒区域面積も大きい可能性。</li>
  <li><b>新仮説 Y</b>: per10k は<b>警戒区域面積比 (= ハザード密度)</b>と強相関。
      この交絡因子を統制すると、市町間の per10k 差は<b>「市町文化」</b>差ではなく
      <b>「ハザード密度」</b>差に還元される可能性。</li>
  <li><b>課題 Z</b>: L11 の警戒区域 polygon と各市町の市町境界 polygon を sjoin →
      面積比 (warn_area_km2 / area_km2) を計算。<b>per10k vs 面積比の散布</b>と OLS 回帰で
      <b>「ハザード密度を統制した市町警戒姿勢」</b>を抽出。残差が市町文化指標。
      <b>L11 + L62 の制度ハザード統合</b>として展開。</li>
</ul>

<h4>発展課題 5 (RQ3 拡張): 避難所容量 (L03) と発令対象人数の比較</h4>
<ul>
  <li><b>結果 X</b>: 2018 西日本豪雨で広島市は対象人数 75,000 人、東広島市 35,000 人など発令済み。
      L03 避難所容量との比較は未実施。発令対象 >> 避難所容量なら<b>避難所過密問題</b>が浮上。</li>
  <li><b>新仮説 Y</b>: 大規模災害時の発令対象人数は<b>避難所収容能力を上回る</b>市町が存在する。
      これは「全員が避難所に行けるとは想定していない」 災害計画の前提だが、
      コミュニケーションの透明化が必要。</li>
  <li><b>課題 Z</b>: L03 の避難所 4,065 件の収容能力 (面積×収容率) を市町別合算 →
      L62 の発令対象人数最大値と比較。比 (収容/対象) が 1 未満の市町を特定。
      <b>「避難所過密リスク市町」</b>の地理パターンを抽出し、
      <b>L03 + L62 の制度容量統合研究</b>として政策提言を行う。</li>
</ul>

<h4>発展課題 6 (展望): 気象データ (雨量) との時空間統合</h4>
<ul>
  <li><b>結果 X</b>: 本研究では大雨特別警報時刻のみを基準にしたが、
      <b>市町別累積雨量 (mm)</b> と発令時刻の関係は未検証。
      L05 (14 日豪雨) や L08 (PCA 雨量) のデータと結合すれば、
      <b>「雨量 mm 何 → 何時間後発令」</b>の意思決定モデルが描ける。</li>
  <li><b>新仮説 Y</b>: 各市町は<b>「累積雨量 X mm を超えると発令」</b>の閾値を持つ。
      閾値は市町ごとに異なり、低い市町は早期発令派、高い市町は慎重派。
      閾値の地理パターンが警戒姿勢を直接表現する。</li>
  <li><b>課題 Z</b>: L05 から 2018-07-05〜08 の市町別 1 時間毎累積雨量を抽出 →
      本研究の発令時刻と結合 → 各市町について発令時の累積雨量を求める。
      <b>市町別「発令閾値 mm」</b>のヒストグラム + choropleth で可視化。
      <b>L05 + L62 の気象 - 制度統合</b>として、<b>意思決定の気象学</b>を完成。</li>
</ul>
"""

# 統合
sections = [
    ("学習目標と問い", sec1),
    ("使用データ", sec2),
    ("ダウンロード", sec3),
    (f"【RQ1】 発令頻度・時系列構造 — {n_rec} 件 / Top 2 で {top2_share:.0f}%",
     sec4_intro
     + "<h3>実装コード (JSON 取得 + 再構成 CSV + 派生列 + 群集計)</h3>"
     + sec4_code
     + sec4_fig1_text
     + figure("assets/L62_fig1_count_event.png",
              f"図 1 (RQ1): 市町別 発令件数 choropleth + イベント別棒 (n={n_rec})")
     + sec4_fig1_read
     + sec4_fig2_text
     + figure("assets/L62_fig2_level_era.png",
              "図 2 (RQ1): 警戒レベル別件数 + 法改正前後 × レベル クロス")
     + sec4_fig2_read
     + sec4_fig3_text
     + figure("assets/L62_fig3_month_year.png",
              "図 3 (RQ1): 月別分布 + 年次推移 (緑線 = 法改正)")
     + sec4_fig3_read
     + "<h3>表: 全体サマリ (3 RQ 統合, 20 指標)</h3>"
     + df_to_html(T_overall)
     + f"<p><b>この表から読み取れること</b>: 全 {n_rec} 件の核心指標を 20 行に集約。"
       f"二大災害シェア {top2_share:.1f}%、改正前後 L3 シェア変化 ({share_l3_pre:.0f}%→{share_l3_post:.0f}%)、"
       f"per10k 上位/下位比 {ratio_top_bot:.1f} 倍 — 3 RQ の主結論を要約。</p>"
     + "<h3>表: イベント別ランキング</h3>"
     + df_to_html(T_event)
     + f"<p><b>この表から読み取れること</b>: 主要 {n_events} イベントのランキング。"
       f"<b>2018 西日本豪雨が首位</b>、対象人数合計でも最大級 = 広島県史上最大の発令イベント。"
       f"次に近年の 2020/2021 豪雨が続く = 制度運用の蓄積が見える。</p>"
     + "<h3>表: 警戒レベル別件数</h3>"
     + df_to_html(T_level)
     + f"<p><b>この表から読み取れること</b>: <b>L4 避難指示が最多</b>、次に L3 高齢者等避難。"
       f"L5 緊急安全確保は 2 件のみ = 制度上の最終警告は極めて限定的運用。"
       f"L1/L2 は気象庁発表で県市町は発令しないため本データに含まれない。</p>"
     + "<h3>表: 月別分布</h3>"
     + df_to_html(T_month)
     + f"<p><b>この表から読み取れること</b>: 7 月単月で {int(month_df[month_df['月']==7]['件数'].iloc[0])} 件と最多 (梅雨末期豪雨)。"
       f"7-8 月で {n_jul_aug}/{n_rec} = {share_jul_aug:.0f}%。"
       f"L61 で確認した過去災害の月別分布と整合する季節性。</p>"
     + "<h3>表: 市町別ランキング Top 15 (発令件数)</h3>"
     + df_to_html(T_city_rank)
     + f"<p><b>この表から読み取れること</b>: 沿岸南部の主要都市が上位、対象人数合計で評価すると"
       f"<b>広島市が圧倒的</b>。中山間市町 (北広島町・世羅町等) も件数では中位だが per10k では上位 (RQ3 で確認)。</p>"
     + "<h3>表: 法改正前後 × 警戒レベル クロス</h3>"
     + df_to_html(T_era_level)
     + f"<p><b>この表から読み取れること</b>: 2021/5/20 災害対策基本法改正前後で "
       f"L3 シェアが {share_l3_pre:.0f}% → {share_l3_post:.0f}%「{'増' if share_l3_post>share_l3_pre else '減'}」、"
       f"L4 シェアが補完的に変化。<b>警戒レベル運用本格化</b>の効果が定量化される。</p>"
    ),
    (f"【RQ2】 2018 西日本豪雨の発令タイミング — 警報前発令 {n_pre_warn_cities}/{len(city_2018)} 市町",
     sec5_intro
     + "<h3>実装コード (タイムラグ + L3→L4 切替 + 市町別タイムライン)</h3>"
     + sec5_code
     + sec5_fig4_text
     + figure("assets/L62_fig4_2018_timing.png",
              f"図 4 (RQ2): 2018 タイムラグ choropleth + 警戒レベル × 市町タイムライン (n={n_2018})")
     + sec5_fig4_read
     + sec5_fig5_text
     + figure("assets/L62_fig5_lag_box.png",
              f"図 5 (RQ2): 警戒レベル別タイムラグ箱ひげ + 市町別 L3→L4 切替時間 (中央 {median_l3l4:.1f}h)")
     + sec5_fig5_read
     + "<h3>表: 2018 警戒レベル別タイムラグサマリ</h3>"
     + df_to_html(T_lag)
     + f"<p><b>この表から読み取れること</b>: <b>L3 中央 {l3_lag_med_h:+.1f}h (警報前)</b> "
       f"vs <b>L4 中央 {l4_lag_med_h:+.1f}h (警報後)</b> = "
       f"<b>L3 警報前発令 / L4 警報後発令</b>のパターンが明確。"
       f"L4 の最早値 {l4_lag_min_h:.1f}h は警報前発令を行った市町の早期判断、"
       f"最遅値 {l4_lag_max_h:.1f}h は状況悪化に応じた追加発令。</p>"
     + "<h3>表: 2018 市町別 タイムライン Top 15 (早発令順)</h3>"
     + df_to_html(T_2018_city)
     + f"<p><b>この表から読み取れること</b>: <b>初発令時刻が早い市町</b>は警戒姿勢が進取的。"
       f"警報相対 -27h 等の負値が多数 = 警報前 1 日以上前から L3 を発令する慎重派。"
       f"市町間で初発令タイミングに大きな差。</p>"
    ),
    (f"【RQ3】 人口対比の発令頻度 — per10k 上位/下位 {ratio_top_bot:.1f} 倍差",
     sec6_intro
     + "<h3>実装コード (市町ロールアップ + L22 結合 + per10k + 地理区分)</h3>"
     + sec6_code
     + sec6_fig6_text
     + figure("assets/L62_fig6_per10k.png",
              f"図 6 (RQ3): 市町別 per10k choropleth + 人口 × per10k 散布 (地理区分色)")
     + sec6_fig6_read
     + sec6_fig7_text
     + figure("assets/L62_fig7_top_bot.png",
              f"図 7 (RQ3): per10k Top 10 + Bottom 10 + 地理区分別平均 ({ratio_top_bot:.1f}倍差)")
     + sec6_fig7_read
     + sec6_fig8_text
     + figure("assets/L62_fig8_aging_trend.png",
              f"図 8 (RQ3): 高齢化率 × per10k 散布 (バブル=人口) + 発令傾向 choropleth")
     + sec6_fig8_read
     + "<h3>表: 市町別 per10k Top 15</h3>"
     + df_to_html(T_per_top)
     + f"<p><b>この表から読み取れること</b>: <b>上位は中山間市町 (庄原・三次・北広島町等)</b> = 人口少 + ハザード集中の構造。"
       f"<b>下位は人口大都市 (広島市・福山市)</b> = 人口分母効果。"
       f"per10k と発令件数 (絶対値) の方向が逆になる重要な統計指標。</p>"
     + "<h3>表: 地理区分別比較</h3>"
     + df_to_html(T_geo)
     + f"<p><b>この表から読み取れること</b>: <b>都市部 vs 中間 vs 中山間</b>の per10k 平均差が"
       f"<b>{ratio_top_bot:.1f} 倍</b>。<b>発令件数合計</b>では都市部が高い (人口大ゆえ) という"
       f"逆方向の指標。多角的視点で評価する必要を示す。</p>"
    ),
    ("仮説検証総合", sec7),
    ("発展課題", sec8),
]


html = render_lesson(
    num=62,
    title="避難情報 単独 3 研究例分析 — "
          f"{n_rec} 件再構成データから「発令の意思決定文化」 を読む",
    tags=["L62", "避難情報", "警戒レベル", "RQ×3", "Format B",
          "geopandas", "choropleth", "JSON API", "L22連携 (人口)",
          "L61連携 (過去災害)", "L03連携 (避難所)",
          "2018西日本豪雨", "災害対策基本法改正"],
    time="40 分",
    level="中級",
    data_label=f"DoBoX dataset 41 (JSON, {EVAC_JSON_LOCAL.stat().st_size}B 平常時) + 再構成 {n_rec} 件 + L22 人口連携",
    sections=sections,
    script_filename="L62_evacuation_orders.py",
)

OUT_HTML = LESSONS / "L62_evacuation_orders.html"
OUT_HTML.write_text(html, encoding="utf-8")

print(f"  HTML: {OUT_HTML.name} ({len(html):,} chars)")
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 終了
# =============================================================================
print(f"\n=== 完了: 全体 {time.time() - t_all:.2f}s ===")
print(f"出力:")
print(f"  HTML: lessons/L62_evacuation_orders.html")
print(f"  Python: lessons/L62_evacuation_orders.py")
print(f"  図: assets/L62_fig1-8_*.png (8 枚)")
print(f"  CSV: assets/L62_*.csv (14 ファイル)")
print(f"  ZIP: assets/L62_evacuation_orders_raw.zip")
print(f"  再構成: data/extras/L62_evacuation_orders/L62_evacuation_history_reconstructed.csv")
