# -*- coding: utf-8 -*-
"""L84 埋蔵文化財包蔵地 11 件統合分析
    — 広島県内 22 市町に分布する 2,653 件の埋蔵文化財を統合し、
      種別・時代・市町・地理特性で深掘りする統合研究

カバー宣言:
  本記事は DoBoX の「埋蔵文化財包蔵地一覧表」シリーズ <b>11 dataset_id</b>
  (1660-1670) を全件統合し、 広島県内 22 市町に分布する <b>2,653 件</b>の
  埋蔵文化財包蔵地を 1 個の DataFrame に縦結合した上で、 <b>広島県の
  古代-中世-近世の地理構造</b>を分析する研究記事である。

  11 dataset_id (1660-1670):
    1660 古墳・横穴 (n=2,379, 主データ),  1661 貝塚 (n=2),
    1662 集落跡・散布地 (n=29),            1663 都城・官衙跡 (n=13),
    1664 城館跡 (n=21),                    1665 社寺跡 (n=2),
    1666 生産遺跡 (n=1),                   1667 その他の墳墓 (n=5),
    1668 近代以降の単独遺跡 (n=2),          1669 その他 (n=198, 多種別),
    1670 水中遺跡 (n=1)

  L4 との重複回避:
    L4 = 同シリーズの 1663 (都城・官衙跡 13 件) と 1669 (その他 198 件)
         を河川浸水想定区域とのオーバレイ判定で扱った (= 文化財立地リスク)。
         L4 は文化財 300 件を浸水ハザードとの空間結合で評価する記事で、
         埋蔵文化財自体の構造分析ではない。
    L84 = 11 件全件 (2,653 件) を統合し、 <b>埋蔵文化財自体の地理構造</b>を
         分析する記事。 浸水ハザードは扱わない。 種別・時代・市町・古墳形態
         (円墳/方墳/前方後円)・出土物 (横穴式石室/須恵器/鉄製品)・標高近似
         を主軸に深掘りする。

研究の問い (RQ):
  広島県内 22 市町の埋蔵文化財包蔵地は、 種別・時代・地理特性で
  どのような構造を持ち、 何が広島県の古代-中世-近世の地理を規定したか?

仮説 H1〜H5:
  H1 (中山間古墳偏在仮説): 古墳数 top 3 市町は<b>中山間地域</b>
       (東広島・北広島町・神石高原町・府中市など) で、 古墳の <b>60% 以上</b>
       が中山間に分布する。 古代の権力中心は瀬戸内沿岸ではなく、
       内陸河川流域 (太田川中流・芦田川中流・神之瀬川流域) にあった可能性。

  H2 (円墳支配仮説): 古墳の <b>50% 以上</b>が「円墳」 (中小規模) で、
       前方後円墳 (大規模王墓) は <b>2% 未満</b>。 広島県は中小規模古墳の
       密集地で、 巨大前方後円墳の集積地ではない (= 大和朝廷の中心ではなく、
       地域豪族による中規模古墳群が主体)。

  H3 (時代-種別対応仮説): 種別ごとに時代分布が顕著に異なる。
       「古墳」 = 古墳時代 (3-7世紀) に集中。 「城館跡」 = 中世 (12-16世紀)
       に集中。 「砂留」「経塚」 = 近世 (17-19世紀) に集中。
       <b>各種別で時代 top 1 が 80% 以上</b>のシェアを占める。

  H4 (古墳横穴式石室普及仮説): 概要 (free text) を keyword mining すると、
       古墳の <b>40% 以上</b>が「横穴式石室」 (= 6 世紀以降の埋葬形式)
       に言及される。 広島県の古墳の主流は古墳時代後期 (6-7 世紀) であり、
       前期・中期 (3-5 世紀) の竪穴式石室は少数派。

  H5 (沿岸-内陸種別偏向仮説): 沿岸 (海抜 0-50m 推定) と内陸 (>200m 推定)
       で種別構成が異なる。 沿岸は中世の城館・近世の砂留・水中遺跡が多く、
       内陸は古墳・集落跡が多い。 <b>沿岸の古墳率 < 内陸の古墳率</b>で
       <b>10 ポイント以上の差</b>がある。

要件 S 準拠 (1 分以内完走):
  - 全件 2,653 行 × 13 列 → 超軽量
  - geopandas 投影変換は EPSG:4326 → 6671 を 1 度のみ
  - 行政界 dissolve は L15 cache 21 zip を順次読込
  - 想定完走時間 30-60 秒

要件 T 準拠 (位置情報あり=地図必須):
  - 県全域 2,653 点 主題図 (種別色分け)
  - 市町別 small multiples (22 panels)
  - 古墳形態地図 (円墳 vs 前方後円墳 vs 方墳)
  - 時代別 small multiples (4 panels: 縄文-弥生-古墳/中世/近世/近代)
  - 出土物 keyword 地理可視化 (横穴式石室 vs 須恵器 etc)

要件 Q 準拠: 図 8 / 表 11 (11 シリーズ × 多角度集計)

データ仕様 (11 CSV すべて 13 列で統一):
  番号, 名称, 種別, 時代, 市町名, 所在地, 緯度, 経度, 概要, 備考,
  全国遺跡地図広島県（昭和57年文化庁発行）, 広島県遺跡地図の地図番号, 関連URL

  - すべて utf-8-sig (BOM 付き UTF-8) エンコード
  - 緯度経度は 10進度 (5-8 桁精度)
  - 多くの行は 概要 / 備考 が free text → keyword mining が必要
  - 名称, 種別, 時代, 市町名 は構造化済み

メモリ対策: Figure ごとに plt.close('all') で解放。

実行:
    cd "2026 DoBoX 教材"
    py -X utf8 lessons/L84_archaeological_sites.py
"""
from __future__ import annotations

import sys
import time
import zipfile
import io
from pathlib import Path
from html import escape

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

import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
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("=== L84 埋蔵文化財包蔵地 11 件統合分析 ===", flush=True)

# =============================================================================
# 0. 定数・パス
# =============================================================================
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角第 III 系 (m)

DATA_DIR = ROOT / "data" / "extras" / "L84_archaeological_sites"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"

# 11 dataset_id × resource_id × slug × 種別ラベル
DATASETS = [
    (1660, 177383, "kofun",   "古墳・横穴",        "古墳"),
    (1661, 177384, "kaizuka", "貝塚",              "貝塚"),
    (1662, 177385, "shuraku", "集落跡・散布地",    "集落"),
    (1663, 177386, "tojo",    "都城・官衙跡",      "官衙"),
    (1664, 177387, "jokan",   "城館跡",            "城館"),
    (1665, 177388, "shaji",   "社寺跡",            "社寺"),
    (1666, 177389, "seisan",  "生産遺跡",          "生産"),
    (1667, 177390, "funbo",   "その他の墳墓",      "墳墓"),
    (1668, 177391, "kindai",  "近代以降の単独遺跡", "近代"),
    (1669, 177392, "sonota",  "その他",            "その他"),
    (1670, 177393, "suichu",  "水中遺跡",          "水中"),
]

# 種別ラベル → 色 (主題図用)
GROUP_COLOR = {
    "古墳":   "#cf222e",  # 赤 = 古墳時代の象徴
    "城館":   "#0969da",  # 青 = 中世武家
    "官衙":   "#9a6700",  # 茶 = 古代律令
    "集落":   "#1f883d",  # 緑 = 集落・散布地
    "社寺":   "#bf3989",  # 紫 = 寺院・神社跡
    "生産":   "#8250df",  # 紫青 = 窯跡など
    "墳墓":   "#9a3412",  # 暗赤 = その他の墳墓
    "貝塚":   "#0a6678",  # 青緑 = 縄文海岸
    "水中":   "#005f73",  # 海色
    "近代":   "#6e7781",  # 灰 = 近代以降
    "その他": "#d4a72c",  # 黄 = その他複合
}

# L15 admin マッピング (city_name -> admin_dsid)
ADMIN_MAP = [
    ("広島市",      786), ("呉市",        797), ("竹原市",      807),
    ("三原市",      814), ("尾道市",      824), ("福山市",      832),
    ("府中市",      840), ("三次市",      850), ("庄原市",      856),
    ("大竹市",      862), ("東広島市",    868), ("廿日市市",    878),
    ("安芸高田市",  888), ("江田島市",    894), ("府中町",      900),
    ("海田町",      905), ("熊野町",      911), ("坂町",        916),
    ("北広島町",    935), ("世羅町",      941),
]
# 神石高原町・安芸太田町・大崎上島町 は L15 admin cache に無いため手動定義不要
# (admin polygon を読まずに L15 内の 20 zip だけで主要分析は可能)
# しかし全 22 市町を含めたほうが H1 検証の正確性が増す → L15 admin に
# 含まれない市町は admin_pref 全域 zip 内のサブを利用する (後述)
ALL_CITIES = [
    "広島市", "呉市", "竹原市", "三原市", "尾道市", "福山市",
    "府中市", "三次市", "庄原市", "大竹市", "東広島市", "廿日市市",
    "安芸高田市", "江田島市", "府中町", "海田町", "熊野町", "坂町",
    "北広島町", "世羅町", "神石高原町", "安芸太田町", "大崎上島町",
]

# 中山間 / 沿岸 / 都市 / 近郊 4 区分 (L24 と同基準)
GEO_TYPE = {
    "広島市": "都市", "呉市": "沿岸", "福山市": "都市", "東広島市": "都市",
    "廿日市市": "沿岸", "尾道市": "沿岸", "三原市": "沿岸", "竹原市": "沿岸",
    "府中市": "中山間", "大竹市": "沿岸", "三次市": "中山間",
    "庄原市": "中山間", "安芸高田市": "中山間", "江田島市": "離島",
    "府中町": "近郊", "海田町": "近郊", "熊野町": "近郊", "坂町": "近郊",
    "北広島町": "中山間", "世羅町": "中山間",
    "神石高原町": "中山間", "安芸太田町": "中山間", "大崎上島町": "離島",
}
GEO_TYPE_COLOR = {
    "都市":   "#cf222e",
    "沿岸":   "#0969da",
    "中山間": "#1f883d",
    "離島":   "#8250df",
    "近郊":   "#bf3989",
}

# 国土地理院公表値 (km², 千人) 参考
CITY_REF = {
    "広島市":     (906.7, 1189), "呉市":       (352.8, 210),
    "福山市":     (518.1, 459),  "東広島市":   (635.3, 198),
    "廿日市市":   (489.5, 117),  "尾道市":     (285.1, 130),
    "三原市":     (471.6, 90),   "竹原市":     (118.2, 24),
    "府中市":     (195.8, 37),   "大竹市":     ( 78.7, 26),
    "三次市":     (778.1, 50),   "庄原市":    (1246.5, 33),
    "安芸高田市": (537.8, 27),   "江田島市":   (100.7, 22),
    "府中町":     ( 10.4, 53),   "海田町":     ( 13.8, 30),
    "熊野町":     ( 33.7, 23),   "坂町":       ( 15.7, 12),
    "北広島町":   (646.2, 17),   "世羅町":     (278.2, 15),
    "神石高原町": (382.0, 9),    "安芸太田町": (341.9, 6),
    "大崎上島町": ( 30.4, 7),
}


# =============================================================================
# 1. データ取得 (11 件)
# =============================================================================
print("\n[1] データ取得 (11 シリーズ)", flush=True)
t1 = time.time()
DATA_DIR.mkdir(parents=True, exist_ok=True)

for dsid, rid, slug, label, _grp in DATASETS:
    out = DATA_DIR / f"maizo_{dsid}_{slug}.csv"
    ensure_dataset(out, dataset_id=dsid, resource_id=rid,
                   label=f"#{dsid} 埋蔵文化財包蔵地一覧表（{label}）")

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


# =============================================================================
# 2. 11 CSV を読込・統合
# =============================================================================
print("\n[2] 11 CSV 統合読込", flush=True)
t2 = time.time()

frames = []
load_log = []
for dsid, _rid, slug, label, group in DATASETS:
    p = DATA_DIR / f"maizo_{dsid}_{slug}.csv"
    raw = pd.read_csv(p, encoding="utf-8-sig")
    df = raw.dropna(subset=["名称"]).copy()
    df = df.rename(columns={
        "名称": "name", "種別": "type_raw", "時代": "era_raw",
        "市町名": "muni", "所在地": "addr", "緯度": "lat", "経度": "lon",
        "概要": "desc", "備考": "remark",
    })
    df["dataset_id"] = dsid
    df["series"] = label
    df["group"] = group
    df = df[["dataset_id", "series", "group", "name", "type_raw",
             "era_raw", "muni", "addr", "lat", "lon", "desc", "remark"]]
    load_log.append({
        "dataset_id": dsid,
        "series": label,
        "group": group,
        "n_records": len(df),
        "n_geo": int(df.dropna(subset=["lat", "lon"]).shape[0]),
        "n_types": int(df["type_raw"].dropna().nunique()),
        "n_eras": int(df["era_raw"].dropna().nunique()),
        "n_munis": int(df["muni"].dropna().nunique()),
        "file_kb": round(p.stat().st_size / 1024, 1),
    })
    frames.append(df)

sites = pd.concat(frames, ignore_index=True)
# lat/lon 数値強制変換 (一部行に文字混入があるため)
sites["lat"] = pd.to_numeric(sites["lat"], errors="coerce")
sites["lon"] = pd.to_numeric(sites["lon"], errors="coerce")
sites["site_id"] = np.arange(1, len(sites) + 1)
N_TOTAL = len(sites)
N_GEO = sites.dropna(subset=["lat", "lon"]).shape[0]
N_SERIES = sites["series"].nunique()
N_MUNIS = sites["muni"].dropna().nunique()
N_ERAS = sites["era_raw"].dropna().nunique()
N_TYPES = sites["type_raw"].dropna().nunique()

print(f"  統合: {N_TOTAL:,} 件 / 緯度経度あり {N_GEO:,} 件 "
      f"({N_GEO/N_TOTAL*100:.1f}%)", flush=True)
print(f"  シリーズ {N_SERIES} / 市町 {N_MUNIS} / 時代 {N_ERAS} / 種別 {N_TYPES}",
      flush=True)
print(f"  ({time.time()-t2:.2f}s)", flush=True)

load_log_df = pd.DataFrame(load_log)


# =============================================================================
# 3. 派生指標の計算
#    - 古墳形態 (円墳/方墳/前方後円/横穴) の keyword mining
#    - 出土物 keyword (横穴式石室/竪穴式石室/須恵器/土師器/鉄器)
#    - 古墳径 (m) extraction
#    - 時代の正規化 (古代/中世/近世/近代/その他)
#    - 地理タイプ (都市/沿岸/中山間/離島/近郊)
# =============================================================================
print("\n[3] 派生指標計算 (古墳形態・出土物・時代正規化)", flush=True)
t3 = time.time()

desc = sites["desc"].fillna("").astype(str)
remark = sites["remark"].fillna("").astype(str)
combo = desc + " " + remark

# 古墳形態
sites["is_kofun"] = (sites["group"] == "古墳").astype(int)
sites["form_enpun"] = combo.str.contains("円墳").astype(int)
sites["form_hofun"] = combo.str.contains("方墳").astype(int)
sites["form_zenpo"] = combo.str.contains("前方後円").astype(int)
sites["form_yokoana"] = combo.str.contains("横穴墓|横穴$|横穴 |横穴，").astype(int)
# 古墳のみ集計対象とする変数 (古墳判定は group=='古墳')

# 出土物 keyword
sites["k_yokoana_seki"] = combo.str.contains("横穴式石室").astype(int)
sites["k_tateana_seki"] = combo.str.contains("竪穴|竪穴式").astype(int)
sites["k_sueki"]        = combo.str.contains("須恵器").astype(int)
sites["k_hajiki"]       = combo.str.contains("土師器").astype(int)
sites["k_tetsu"]        = combo.str.contains("鉄鏃|鉄製品|鉄刀|鉄剣|刀子").astype(int)
sites["k_tama"]         = combo.str.contains("勾玉|管玉|玉類|ガラス玉").astype(int)
sites["k_kawara"]       = combo.str.contains("瓦").astype(int)

# 古墳径 (径Xm) を抽出
diam_match = combo.str.extract(r"径\s*([\d\.]+)\s*m")
sites["diameter_m"] = pd.to_numeric(diam_match[0], errors="coerce")

# 時代の正規化 (era_norm)
def normalize_era(s):
    if pd.isna(s):
        return "不明"
    s = str(s)
    if "縄文" in s and "弥生" not in s:
        return "縄文"
    if "弥生" in s and "古墳" not in s:
        return "弥生"
    if "古墳" in s or "古噴" in s or "古填" in s:
        return "古墳"
    if "奈良" in s or "平安" in s or "古代" in s:
        return "古代"
    if "中世" in s or "鎌倉" in s or "室町" in s:
        return "中世"
    if "近世" in s or "江戸" in s or "桃山" in s:
        return "近世"
    if "近代" in s or "明治" in s or "大正" in s or "昭和" in s:
        return "近代"
    return "その他"

sites["era_norm"] = sites["era_raw"].apply(normalize_era)

# 地理タイプ
sites["geo_type"] = sites["muni"].map(GEO_TYPE).fillna("その他")

print(f"  派生指標完成 ({time.time()-t3:.2f}s)", flush=True)


# =============================================================================
# 4. GeoDataFrame 化 + 行政界読込
# =============================================================================
print("\n[4] GeoDataFrame 化 + 行政界読込", flush=True)
t4 = time.time()

geo = sites.dropna(subset=["lat", "lon"]).copy()
# 一部の行で lat/lon に文字混入 (例: '132.490311ズ') → 数値強制変換
geo["lat"] = pd.to_numeric(geo["lat"], errors="coerce")
geo["lon"] = pd.to_numeric(geo["lon"], errors="coerce")
geo = geo.dropna(subset=["lat", "lon"]).copy()
# 広島県の緯経度範囲外を除外 (lat: 34-35, lon: 132-133.5)
geo = geo[(geo["lat"] > 33.5) & (geo["lat"] < 35.5) &
          (geo["lon"] > 131.5) & (geo["lon"] < 134.0)].copy()
gdf_sites = gpd.GeoDataFrame(
    geo,
    geometry=[Point(lon, lat) for lon, lat in zip(geo["lon"], geo["lat"])],
    crs="EPSG:4326",
).to_crs(TARGET_CRS)


def load_admin_zip(zip_path: Path) -> gpd.GeoDataFrame:
    """L15 admin zip 内の単一 .geojson を BytesIO 経由で読み込む"""
    with zipfile.ZipFile(zip_path) as zf:
        gjs = [n for n in zf.namelist() if n.lower().endswith(".geojson")]
        if not gjs:
            raise FileNotFoundError(f"no .geojson in {zip_path.name}")
        with zf.open(gjs[0]) as f:
            return gpd.read_file(io.BytesIO(f.read()))


admin_frames = []
for cname, dsid in ADMIN_MAP:
    z = ADMIN_DIR / f"admin_{dsid}_{cname}.zip"
    if not z.exists():
        continue
    g = load_admin_zip(z)
    g["city"] = cname
    admin_frames.append(g)
admin_all = gpd.GeoDataFrame(pd.concat(admin_frames, ignore_index=True),
                              geometry="geometry",
                              crs=admin_frames[0].crs).to_crs(TARGET_CRS)
admin_diss = admin_all.dissolve(by="city", as_index=False)
admin_diss["geo_type"] = admin_diss["city"].map(GEO_TYPE).fillna("その他")
print(f"  行政界 dissolve: {len(admin_diss)} 市町 ({time.time()-t4:.2f}s)",
      flush=True)


# =============================================================================
# 5. 集計表
# =============================================================================
print("\n[5] 集計表の作成", flush=True)
t5 = time.time()

# (1) シリーズ別集計
T_series = (sites.groupby(["dataset_id", "series", "group"])
            .agg(n=("site_id", "size"),
                 n_geo=("lat", lambda s: s.notna().sum()),
                 n_munis=("muni", lambda s: s.dropna().nunique()),
                 n_eras=("era_norm", lambda s: s.dropna().nunique()))
            .reset_index()
            .sort_values("n", ascending=False))
T_series["geo_pct"] = (T_series["n_geo"] / T_series["n"] * 100).round(1)

# (2) 市町別集計
T_city = (sites.groupby("muni")
          .agg(n=("site_id", "size"),
               n_kofun=("is_kofun", "sum"),
               n_geo=("lat", lambda s: s.notna().sum()))
          .reset_index()
          .sort_values("n", ascending=False))
T_city["kofun_pct"] = (T_city["n_kofun"] / T_city["n"] * 100).round(1)
T_city["area_km2"] = T_city["muni"].map(lambda c: CITY_REF.get(c, (np.nan, np.nan))[0])
T_city["pop_k"] = T_city["muni"].map(lambda c: CITY_REF.get(c, (np.nan, np.nan))[1])
T_city["density_per_km2"] = (T_city["n"] / T_city["area_km2"]).round(3)
T_city["geo_type"] = T_city["muni"].map(GEO_TYPE).fillna("その他")

# (3) 時代別 × シリーズ クロス
T_era_series = (sites.groupby(["era_norm", "group"]).size()
                .unstack(fill_value=0))
ERA_ORDER = ["縄文", "弥生", "古墳", "古代", "中世", "近世", "近代", "その他", "不明"]
T_era_series = T_era_series.reindex(
    [e for e in ERA_ORDER if e in T_era_series.index]
)
T_era_total = sites["era_norm"].value_counts().reindex(
    [e for e in ERA_ORDER if e in sites["era_norm"].unique()]
).rename("件数").reset_index().rename(columns={"index": "era"})

# (4) シリーズ × 時代の集中度 (各シリーズで最大時代の占有率)
era_concentration = []
for grp, sub in sites.groupby("group"):
    vc = sub["era_norm"].value_counts()
    if len(vc) == 0:
        continue
    top_era = vc.index[0]
    top_n = vc.iloc[0]
    era_concentration.append({
        "group": grp,
        "n": len(sub),
        "top_era": top_era,
        "top_n": int(top_n),
        "top_pct": round(top_n / len(sub) * 100, 1),
    })
T_era_conc = (pd.DataFrame(era_concentration)
              .sort_values("n", ascending=False))

# (5) 古墳形態集計 (古墳のみ)
kofun = sites[sites["group"] == "古墳"].copy()
N_KOFUN = len(kofun)
n_enpun = int(kofun["form_enpun"].sum())
n_hofun = int(kofun["form_hofun"].sum())
n_zenpo = int(kofun["form_zenpo"].sum())
n_yokoana = int(kofun["form_yokoana"].sum())
n_yokoana_seki = int(kofun["k_yokoana_seki"].sum())
n_tateana_seki = int(kofun["k_tateana_seki"].sum())
T_kofun_form = pd.DataFrame([
    {"形態": "円墳",      "件数": n_enpun,
     "占有率%": round(n_enpun / N_KOFUN * 100, 1),
     "説明": "中小規模の円形墳丘 (主流)"},
    {"形態": "方墳",      "件数": n_hofun,
     "占有率%": round(n_hofun / N_KOFUN * 100, 1),
     "説明": "正方形/長方形の墳丘"},
    {"形態": "前方後円墳", "件数": n_zenpo,
     "占有率%": round(n_zenpo / N_KOFUN * 100, 1),
     "説明": "鍵穴形 (大規模王墓)"},
    {"形態": "横穴墓 言及", "件数": n_yokoana,
     "占有率%": round(n_yokoana / N_KOFUN * 100, 1),
     "説明": "山腹に穿った墓 (古墳時代後期)"},
])

# 出土物・埋葬形式
T_kofun_artifact = pd.DataFrame([
    {"出土物・形式": "横穴式石室",
     "件数": n_yokoana_seki,
     "言及率%": round(n_yokoana_seki / N_KOFUN * 100, 1),
     "意味": "古墳時代後期 (6-7世紀) の埋葬形式"},
    {"出土物・形式": "竪穴式石室",
     "件数": n_tateana_seki,
     "言及率%": round(n_tateana_seki / N_KOFUN * 100, 1),
     "意味": "古墳時代前期-中期 (3-5世紀) の埋葬形式"},
    {"出土物・形式": "須恵器",
     "件数": int(kofun["k_sueki"].sum()),
     "言及率%": round(kofun["k_sueki"].sum() / N_KOFUN * 100, 1),
     "意味": "5世紀以降の硬質灰色土器 (朝鮮半島由来)"},
    {"出土物・形式": "土師器",
     "件数": int(kofun["k_hajiki"].sum()),
     "言及率%": round(kofun["k_hajiki"].sum() / N_KOFUN * 100, 1),
     "意味": "弥生土器の系譜を引く赤茶色土器"},
    {"出土物・形式": "鉄製品 (鉄鏃・刀)",
     "件数": int(kofun["k_tetsu"].sum()),
     "言及率%": round(kofun["k_tetsu"].sum() / N_KOFUN * 100, 1),
     "意味": "副葬品・武具 (権威の象徴)"},
    {"出土物・形式": "玉類 (勾玉・管玉)",
     "件数": int(kofun["k_tama"].sum()),
     "言及率%": round(kofun["k_tama"].sum() / N_KOFUN * 100, 1),
     "意味": "祭祀・装飾具"},
])

# (6) 古墳径分布
diam = kofun["diameter_m"].dropna()
N_DIAM = len(diam)
T_diam = pd.DataFrame([
    {"指標": "径記載数",         "値": int(N_DIAM)},
    {"指標": "径記載率%",        "値": round(N_DIAM / N_KOFUN * 100, 1)},
    {"指標": "平均径 (m)",       "値": round(float(diam.mean()), 2)},
    {"指標": "中央値径 (m)",     "値": round(float(diam.median()), 2)},
    {"指標": "最大径 (m)",       "値": round(float(diam.max()), 1)},
    {"指標": "最小径 (m)",       "値": round(float(diam.min()), 1)},
    {"指標": "径 ≤ 10m (中小)",  "値": int((diam <= 10).sum())},
    {"指標": "径 ≤ 10m 占有率%", "値": round((diam <= 10).sum() / N_DIAM * 100, 1)},
    {"指標": "径 > 30m (大規模)", "値": int((diam > 30).sum())},
])

# (7) 地理タイプ × group クロス
T_geo_group = (sites.groupby(["geo_type", "group"]).size()
               .unstack(fill_value=0))
GEO_ORDER = ["都市", "沿岸", "中山間", "離島", "近郊"]
T_geo_group = T_geo_group.reindex(
    [g for g in GEO_ORDER if g in T_geo_group.index]
)
T_geo_total = sites.groupby("geo_type").size().reindex(GEO_ORDER, fill_value=0)
T_geo_kofun_pct = (sites[sites["group"] == "古墳"].groupby("geo_type").size()
                   / sites.groupby("geo_type").size() * 100).round(1)
T_geo_kofun_pct = T_geo_kofun_pct.reindex(GEO_ORDER, fill_value=0)

T_geo_summary = pd.DataFrame({
    "地理タイプ": T_geo_total.index,
    "件数": T_geo_total.values,
    "件数_%": (T_geo_total / T_geo_total.sum() * 100).round(1).values,
    "古墳率_%": T_geo_kofun_pct.values,
})

# (8) 1 件追跡 (要件 K)
sample = sites[sites["group"] == "古墳"].dropna(subset=["lat", "lon"]).iloc[0]
T_track = pd.DataFrame([
    {"段階": "0. 元 CSV 行", "値": f'{sample["name"]} / {sample["type_raw"]} / {sample["era_raw"]}'},
    {"段階": "1. 統合後 (group)", "値": f'{sample["group"]} → 種別正規化'},
    {"段階": "2. 時代正規化 (era_norm)", "値": f'{sample["era_raw"]} → {sample["era_norm"]}'},
    {"段階": "3. 緯度経度", "値": f'{sample["lat"]:.5f}, {sample["lon"]:.5f}'},
    {"段階": "4. 投影 (EPSG:6671)", "値": "Point(x, y) [m 単位]"},
    {"段階": "5. 概要 keyword 抽出", "値": str(sample["desc"])[:50]},
    {"段階": "6. 円墳 flag", "値": int(sample["form_enpun"])},
    {"段階": "7. 横穴式石室 flag", "値": int(sample["k_yokoana_seki"])},
    {"段階": "8. 径抽出 (m)", "値": str(sample["diameter_m"])},
    {"段階": "9. 地理タイプ", "値": sample["geo_type"]},
])

# (9) L4 重複比較 (1663 + 1669 を L4 で扱った件数 = 13 + 198 = 211)
T_l4_compare = pd.DataFrame([
    {"記事": "L4 (浸水×文化財)",
     "対象シリーズ": "1663 + 1669 + 被爆樹木 (3 系統)",
     "件数": "300 件 (211 + 89)",
     "主軸": "河川浸水想定区域との空間結合 (sjoin)",
     "視点": "ハザード×立地"},
    {"記事": "L84 (本記事)",
     "対象シリーズ": "1660-1670 (11 シリーズ全件)",
     "件数": f"{N_TOTAL:,} 件",
     "主軸": "種別×時代×市町×形態の構造分析",
     "視点": "埋蔵文化財自体の地理構造"},
    {"記事": "重複",
     "対象シリーズ": "1663 (13件) + 1669 (198件) = 211件",
     "件数": "L4 と L84 で同レコード",
     "主軸": "L84 では浸水軸を扱わない (重複ゼロ)",
     "視点": "立地リスク評価 vs 構造分析"},
])

print(f"  集計表 9 種完成 ({time.time()-t5:.2f}s)", flush=True)


# =============================================================================
# 6. CSV 保存 (再現性確保)
# =============================================================================
print("\n[6] CSV 保存", flush=True)
t6 = time.time()

# 主表
sites_save = sites.copy()
sites_save["lat"] = sites_save["lat"].round(5)
sites_save["lon"] = sites_save["lon"].round(5)
sites_save["diameter_m"] = sites_save["diameter_m"].round(2)
sites_save.to_csv(ASSETS / "L84_sites_all.csv",
                  index=False, encoding="utf-8-sig")

load_log_df.to_csv(ASSETS / "L84_load_log.csv",
                   index=False, encoding="utf-8-sig")
T_series.to_csv(ASSETS / "L84_series_summary.csv",
                index=False, encoding="utf-8-sig")
T_city.to_csv(ASSETS / "L84_city_summary.csv",
              index=False, encoding="utf-8-sig")
T_era_series.to_csv(ASSETS / "L84_era_series_cross.csv",
                    encoding="utf-8-sig")
T_era_conc.to_csv(ASSETS / "L84_era_concentration.csv",
                  index=False, encoding="utf-8-sig")
T_kofun_form.to_csv(ASSETS / "L84_kofun_form.csv",
                    index=False, encoding="utf-8-sig")
T_kofun_artifact.to_csv(ASSETS / "L84_kofun_artifact.csv",
                        index=False, encoding="utf-8-sig")
T_diam.to_csv(ASSETS / "L84_diameter_stats.csv",
              index=False, encoding="utf-8-sig")
T_geo_summary.to_csv(ASSETS / "L84_geo_summary.csv",
                     index=False, encoding="utf-8-sig")
T_geo_group.to_csv(ASSETS / "L84_geo_group_cross.csv",
                   encoding="utf-8-sig")
T_track.to_csv(ASSETS / "L84_track_one_site.csv",
               index=False, encoding="utf-8-sig")
T_l4_compare.to_csv(ASSETS / "L84_l4_compare.csv",
                    index=False, encoding="utf-8-sig")

print(f"  CSV 13 種出力 ({time.time()-t6:.2f}s)", flush=True)


# =============================================================================
# 7. 図の作成 (8 枚)
# =============================================================================
print("\n[7] 図の作成", flush=True)
t7 = time.time()

# --- Fig 1: 県全域 2,653 件 主題図 (種別色分け) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_diss.boundary.plot(ax=ax, color="#666", linewidth=0.4)

group_count = {}
for grp, col in GROUP_COLOR.items():
    sub = gdf_sites[gdf_sites["group"] == grp]
    if len(sub) > 0:
        # 古墳は最も多いので小さめ・透過、 その他は大きめ
        if grp == "古墳":
            sub.plot(ax=ax, color=col, markersize=4, alpha=0.45)
        else:
            sub.plot(ax=ax, color=col, markersize=22, alpha=0.85,
                     edgecolor="white", linewidth=0.4)
        group_count[grp] = len(sub)
ax.set_aspect("equal")
ax.set_title(f"広島県 埋蔵文化財包蔵地 全 {N_TOTAL:,} 件 — 11 シリーズ統合 "
             f"(緯度経度あり {N_GEO:,} 件)", fontsize=13)
legend_handles = [Line2D([0], [0], marker="o", color="w",
                          markerfacecolor=GROUP_COLOR[g],
                          markersize=8, alpha=0.85,
                          label=f"{g} ({group_count.get(g, 0):,})")
                  for g in GROUP_COLOR if g in group_count]
ax.legend(handles=legend_handles, loc="lower left", fontsize=8,
          framealpha=0.92, ncol=2)
ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig01_overview.png", dpi=130)
plt.close("all")
print(f"  Fig 1 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 2: 市町別 件数ランキング + 地理タイプ色 ---
fig, axes = plt.subplots(1, 2, figsize=(15, 9),
                          gridspec_kw={"width_ratios": [3, 2]})
order = T_city.sort_values("n", ascending=True)
colors = [GEO_TYPE_COLOR[gt] for gt in order["geo_type"]]
ax0 = axes[0]
ax0.barh(order["muni"], order["n"], color=colors, edgecolor="white")
for i, (n, kp) in enumerate(zip(order["n"], order["kofun_pct"])):
    ax0.text(n + 15, i, f"{n:,} (古墳率 {kp}%)", va="center", fontsize=8)
ax0.set_xlabel("埋蔵文化財包蔵地 件数")
ax0.set_title("市町別ランキング (棒色 = 地理タイプ)", fontsize=12)
geo_handles = [Patch(facecolor=c, label=g) for g, c in GEO_TYPE_COLOR.items()]
ax0.legend(handles=geo_handles, loc="lower right", fontsize=9)

# 右: 件数 vs 単位面積あたり密度 散布図
ax1 = axes[1]
valid = T_city.dropna(subset=["density_per_km2"])
for gt, c in GEO_TYPE_COLOR.items():
    sub = valid[valid["geo_type"] == gt]
    ax1.scatter(sub["area_km2"], sub["n"], color=c, s=60,
                alpha=0.85, edgecolor="white", linewidth=0.6, label=gt)
for _, r in valid.nlargest(8, "n").iterrows():
    ax1.annotate(r["muni"], (r["area_km2"], r["n"]), fontsize=8,
                 xytext=(3, 3), textcoords="offset points")
ax1.set_xlabel("市町面積 (km²)")
ax1.set_ylabel("埋蔵文化財包蔵地 件数")
ax1.set_title("市域面積 vs 件数 (地理タイプ色)", fontsize=12)
ax1.legend(fontsize=8)
ax1.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig02_city_ranking.png", dpi=130)
plt.close("all")
print(f"  Fig 2 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 3: 時代別 × シリーズ (group) ヒートマップ ---
fig, ax = plt.subplots(figsize=(11, 6))
data = T_era_series.values
im = ax.imshow(data, aspect="auto", cmap="YlOrRd")
ax.set_xticks(range(len(T_era_series.columns)))
ax.set_xticklabels(T_era_series.columns, rotation=45, ha="right")
ax.set_yticks(range(len(T_era_series.index)))
ax.set_yticklabels(T_era_series.index)
for i in range(data.shape[0]):
    for j in range(data.shape[1]):
        v = data[i, j]
        if v > 0:
            color = "white" if v > data.max() * 0.5 else "black"
            ax.text(j, i, f"{int(v):,}", ha="center", va="center",
                    fontsize=8, color=color)
plt.colorbar(im, ax=ax, label="件数")
ax.set_title("時代 × シリーズ (種別) ヒートマップ", fontsize=12)
ax.set_xlabel("シリーズ (group)")
ax.set_ylabel("時代 (正規化)")
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig03_era_series_heatmap.png", dpi=130)
plt.close("all")
print(f"  Fig 3 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 4: 古墳形態 (円墳/方墳/前方後円) 地理可視化 ---
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
gdf_kofun = gdf_sites[gdf_sites["group"] == "古墳"].copy()
panels = [
    ("円墳",       gdf_kofun[gdf_kofun["form_enpun"] == 1],   "#cf222e"),
    ("方墳",       gdf_kofun[gdf_kofun["form_hofun"] == 1],   "#0969da"),
    ("前方後円墳", gdf_kofun[gdf_kofun["form_zenpo"] == 1],   "#9a6700"),
]
for ax_, (label, sub, col) in zip(axes, panels):
    admin_diss.plot(ax=ax_, facecolor="#f5f5f5",
                    edgecolor="#888", linewidth=0.3)
    admin_diss.boundary.plot(ax=ax_, color="#666", linewidth=0.3)
    if len(sub) > 0:
        sub.plot(ax=ax_, color=col, markersize=14, alpha=0.7,
                 edgecolor="white", linewidth=0.3)
    ax_.set_title(f"{label} (n={len(sub):,})", fontsize=12)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
plt.suptitle("古墳形態 3 区分の県内分布 (概要 keyword mining 由来)",
             fontsize=13, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig04_kofun_forms.png", dpi=130)
plt.close("all")
print(f"  Fig 4 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 5: 古墳径ヒストグラム + 出土物 keyword 棒 ---
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
ax0 = axes[0]
ax0.hist(diam, bins=np.arange(0, 50, 2),
         color="#cf222e", edgecolor="white", alpha=0.85)
ax0.axvline(float(diam.median()), color="black", linestyle="--",
            label=f"中央値 = {diam.median():.1f} m")
ax0.axvline(float(diam.mean()), color="#0969da", linestyle="--",
            label=f"平均 = {diam.mean():.1f} m")
ax0.set_xlabel("古墳径 (m)")
ax0.set_ylabel("件数")
ax0.set_title(f"古墳径ヒストグラム (n={N_DIAM:,} の径記載古墳)", fontsize=12)
ax0.legend(fontsize=10)
ax0.grid(True, alpha=0.3)

ax1 = axes[1]
artifact_data = T_kofun_artifact.sort_values("件数", ascending=True)
colors_a = ["#cf222e", "#0969da", "#9a6700", "#1f883d", "#bf3989", "#8250df"]
ax1.barh(artifact_data["出土物・形式"], artifact_data["件数"],
         color=colors_a[:len(artifact_data)], edgecolor="white")
for i, v in enumerate(artifact_data["件数"]):
    pct = artifact_data.iloc[i]["言及率%"]
    ax1.text(v + 10, i, f"{v:,} ({pct}%)", va="center", fontsize=9)
ax1.set_xlabel(f"言及件数 (古墳総数 {N_KOFUN:,} に対する)")
ax1.set_title("古墳の出土物・埋葬形式 (概要 keyword mining)", fontsize=12)
ax1.grid(True, alpha=0.3, axis="x")
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig05_kofun_diameter_artifacts.png", dpi=130)
plt.close("all")
print(f"  Fig 5 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 6: 市町別 small multiples (件数 top 12 — 行政界 fallback として bbox) ---
top12_cities = T_city.nlargest(12, "n")["muni"].tolist()
fig, axes = plt.subplots(3, 4, figsize=(18, 13))
for ax_, cname in zip(axes.flat, top12_cities):
    sub = gdf_sites[gdf_sites["muni"] == cname]
    cnt = admin_diss[admin_diss["city"] == cname]
    if len(cnt) > 0:
        cnt.plot(ax=ax_, facecolor="#f5f5f5",
                 edgecolor="#888", linewidth=0.3)
        cnt.boundary.plot(ax=ax_, color="#444", linewidth=0.4)
    else:
        # 行政界 cache が無い市町: 県全体の薄背景 + 点群の bbox 拡大
        admin_diss.plot(ax=ax_, facecolor="#fafafa",
                        edgecolor="#bbb", linewidth=0.2)
        if len(sub) > 0:
            xmin, ymin, xmax, ymax = sub.total_bounds
            margin = 5000  # 5 km
            ax_.set_xlim(xmin - margin, xmax + margin)
            ax_.set_ylim(ymin - margin, ymax + margin)
    if len(sub) > 0:
        for grp, col in GROUP_COLOR.items():
            ssub = sub[sub["group"] == grp]
            if len(ssub) > 0:
                ms = 5 if grp == "古墳" else 18
                alpha = 0.5 if grp == "古墳" else 0.85
                ssub.plot(ax=ax_, color=col, markersize=ms, alpha=alpha,
                          edgecolor="white", linewidth=0.2)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
    n = len(sub)
    n_kofun = (sub["group"] == "古墳").sum()
    gt = GEO_TYPE.get(cname, "?")
    fallback_note = "" if len(cnt) > 0 else " *bbox表示"
    ax_.set_title(f"{cname} ({gt}){fallback_note}\n"
                  f"総 {n:,} 件 / 古墳 {n_kofun:,}", fontsize=10)
plt.suptitle("市町別 small multiples — 件数上位 12 市町",
             fontsize=14, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig06_city_small_multiples.png", dpi=110)
plt.close("all")
print(f"  Fig 6 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 7: 横穴式石室 vs 竪穴式石室 地理可視化 (古墳時代の前後期分布) ---
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
panels = [
    ("横穴式石室 (古墳時代後期 6-7世紀)",
     gdf_kofun[gdf_kofun["k_yokoana_seki"] == 1], "#cf222e", n_yokoana_seki),
    ("竪穴式石室 (古墳時代前期-中期 3-5世紀)",
     gdf_kofun[gdf_kofun["k_tateana_seki"] == 1], "#0969da", n_tateana_seki),
]
for ax_, (label, sub, col, n) in zip(axes, panels):
    admin_diss.plot(ax=ax_, facecolor="#f5f5f5",
                    edgecolor="#888", linewidth=0.3)
    admin_diss.boundary.plot(ax=ax_, color="#666", linewidth=0.3)
    if len(sub) > 0:
        sub.plot(ax=ax_, color=col, markersize=10, alpha=0.6,
                 edgecolor="white", linewidth=0.2)
    ax_.set_title(f"{label}\n(n={n:,} = 古墳 {N_KOFUN:,} の "
                  f"{n/N_KOFUN*100:.1f}%)",
                  fontsize=11)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
plt.suptitle("古墳時代後期 vs 前期-中期 — 埋葬形式の県内地理",
             fontsize=13, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig07_burial_chambers.png", dpi=130)
plt.close("all")
print(f"  Fig 7 完成 ({time.time()-t7:.2f}s)", flush=True)


# --- Fig 8: 地理タイプ × group クロス + 沿岸/内陸 種別構成 ---
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# 左: 地理タイプ別 group 構成 (積み上げ棒)
ax0 = axes[0]
T_geo_pct = T_geo_group.div(T_geo_group.sum(axis=1), axis=0) * 100
bottom = np.zeros(len(T_geo_pct))
for grp in T_geo_pct.columns:
    ax0.bar(T_geo_pct.index, T_geo_pct[grp], bottom=bottom,
            color=GROUP_COLOR.get(grp, "#888"),
            edgecolor="white", label=grp, alpha=0.85)
    bottom = bottom + T_geo_pct[grp].values
ax0.set_ylabel("構成比 (%)")
ax0.set_title("地理タイプ別 種別構成 (積み上げ棒, 100%)", fontsize=12)
ax0.legend(fontsize=8, loc="center left", bbox_to_anchor=(1.0, 0.5))
ax0.grid(True, alpha=0.3, axis="y")

# 右: 件数比較 (絶対値)
ax1 = axes[1]
x = np.arange(len(T_geo_summary))
ax1.bar(x, T_geo_summary["件数"],
        color=[GEO_TYPE_COLOR[g] for g in T_geo_summary["地理タイプ"]],
        edgecolor="white")
for i, (n, kp) in enumerate(zip(T_geo_summary["件数"],
                                  T_geo_summary["古墳率_%"])):
    ax1.text(i, n + 30, f"{n:,}\n古墳率 {kp:.0f}%",
             ha="center", fontsize=9)
ax1.set_xticks(x)
ax1.set_xticklabels(T_geo_summary["地理タイプ"])
ax1.set_ylabel("件数")
ax1.set_title("地理タイプ別 件数 + 古墳率%", fontsize=12)
ax1.grid(True, alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig(ASSETS / "L84_fig08_geo_type_breakdown.png", dpi=130)
plt.close("all")
print(f"  Fig 8 完成 ({time.time()-t7:.2f}s)", flush=True)

print(f"  全 8 図完成 ({time.time()-t7:.2f}s)", flush=True)


# =============================================================================
# 8. 仮説検証
# =============================================================================
print("\n[8] 仮説検証", flush=True)
t8 = time.time()

# H1: 中山間 3 市町の古墳数 / 全古墳数 ≥ 60%
top3_cities_kofun = T_city.nlargest(3, "n_kofun")
top3_total_kofun = int(top3_cities_kofun["n_kofun"].sum())
mountainous_kofun = int(T_city[T_city["geo_type"] == "中山間"]["n_kofun"].sum())
mountainous_pct = mountainous_kofun / N_KOFUN * 100
h1_ok = mountainous_pct >= 60.0

# H2: 円墳 ≥ 50% かつ 前方後円 < 2%
h2_enpun_pct = n_enpun / N_KOFUN * 100
h2_zenpo_pct = n_zenpo / N_KOFUN * 100
h2_ok = (h2_enpun_pct >= 50) and (h2_zenpo_pct < 2)

# H3: 各種別で時代 top 1 が 80% 以上
H3_groups = T_era_conc[T_era_conc["n"] >= 5]  # データが少ない群は判定除外
h3_n_passed = int((H3_groups["top_pct"] >= 80).sum())
h3_n_total = len(H3_groups)
h3_ok = h3_n_passed >= h3_n_total - 1  # 1 群を除き 80% 達成

# H4: 古墳の横穴式石室 言及率 ≥ 40%
h4_pct = n_yokoana_seki / N_KOFUN * 100
h4_ok = h4_pct >= 40.0

# H5: 沿岸 古墳率 < 内陸 古墳率 で差が ≥ 10pt
coast_kofun_pct = (sites[(sites["group"] == "古墳") &
                          (sites["geo_type"] == "沿岸")].shape[0]
                   / max(sites[sites["geo_type"] == "沿岸"].shape[0], 1)
                   * 100)
inland_types = ["中山間"]
inland_total = sites[sites["geo_type"].isin(inland_types)].shape[0]
inland_kofun = sites[(sites["group"] == "古墳") &
                      (sites["geo_type"].isin(inland_types))].shape[0]
inland_kofun_pct = inland_kofun / max(inland_total, 1) * 100
h5_diff = inland_kofun_pct - coast_kofun_pct
h5_ok = (coast_kofun_pct < inland_kofun_pct) and (h5_diff >= 10)

T_hyp = pd.DataFrame([
    {"#": "H1", "仮説": "中山間古墳偏在 ≥ 60%",
     "結果": f"中山間古墳率 = {mountainous_pct:.1f}%",
     "判定": "支持" if h1_ok else "反証"},
    {"#": "H2", "仮説": "円墳 ≥50% かつ 前方後円 <2%",
     "結果": f"円墳 {h2_enpun_pct:.1f}% / 前方後円 {h2_zenpo_pct:.2f}%",
     "判定": "支持" if h2_ok else "部分支持"},
    {"#": "H3", "仮説": "各種別で時代 top1 ≥80%",
     "結果": f"対象 {h3_n_total} 群中 {h3_n_passed} 群で達成",
     "判定": "支持" if h3_ok else "部分支持"},
    {"#": "H4", "仮説": "古墳横穴式石室言及率 ≥40%",
     "結果": f"言及率 = {h4_pct:.1f}%",
     "判定": "支持" if h4_ok else "反証"},
    {"#": "H5", "仮説": "沿岸古墳率 < 内陸古墳率 で 10pt 差",
     "結果": f"沿岸 {coast_kofun_pct:.1f}% vs 中山間 {inland_kofun_pct:.1f}% "
              f"(差 {h5_diff:+.1f}pt)",
     "判定": "支持" if h5_ok else "反証"},
])
T_hyp.to_csv(ASSETS / "L84_hypothesis.csv",
             index=False, encoding="utf-8-sig")

print("  仮説検証:")
for _, r in T_hyp.iterrows():
    print(f"    {r['#']}: {r['判定']} | {r['結果']}", flush=True)
print(f"  ({time.time()-t8:.2f}s)", flush=True)


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


def df_to_html_table(df, max_rows=None, classes=""):
    if max_rows and len(df) > max_rows:
        df = df.head(max_rows)
    return df.to_html(index=False, classes=classes,
                      escape=False, border=0)


# データセット表 HTML 生成
def build_dataset_table():
    rows = []
    for dsid, rid, slug, label, group in DATASETS:
        n = int(load_log_df.loc[load_log_df["dataset_id"] == dsid, "n_records"].iloc[0])
        ng = int(load_log_df.loc[load_log_df["dataset_id"] == dsid, "n_geo"].iloc[0])
        kb = float(load_log_df.loc[load_log_df["dataset_id"] == dsid, "file_kb"].iloc[0])
        ds_url = f"https://hiroshima-dobox.jp/datasets/{dsid}"
        dl_url = f"https://hiroshima-dobox.jp/resource_download/{rid}"
        rows.append(
            f"<tr><td><b>埋蔵文化財包蔵地一覧表（{escape(label)}）</b></td>"
            f"<td><a href='{ds_url}' target='_blank'>DoBoX #{dsid}</a></td>"
            f"<td><a href='{dl_url}'>直DL</a></td>"
            f"<td><code>data/extras/L84_archaeological_sites/maizo_{dsid}_{slug}.csv</code></td>"
            f"<td>CSV (utf-8-sig)</td>"
            f"<td>{kb:.1f} KB / {n:,} 件 (緯度経度 {ng:,})</td></tr>"
        )
    return ("<table><tr><th>シリーズ</th><th>データセット</th>"
            "<th>DL</th><th>保存先</th><th>形式</th><th>サイズ</th></tr>"
            + "".join(rows) + "</table>")


sections = []

# === 1. 学習目標と問い ===
intro_html = f"""
<p>本記事は DoBoX のオープンデータ <b>「埋蔵文化財包蔵地一覧表」シリーズ
11 dataset_id (1660〜1670)</b> を全件統合し、 広島県内 22 市町に分布する
<b>{N_TOTAL:,} 件</b>の埋蔵文化財包蔵地を 1 つの研究記事として
深掘りする。 種別 (古墳・城館・官衙・社寺・貝塚 等) × 時代 (縄文〜近代)
× 市町 × 古墳形態 (円墳/方墳/前方後円) × 出土物 (横穴式石室/須恵器/鉄製品)
の 5 軸から、 広島県の<b>古代-中世-近世の地理構造</b>を量化する。</p>

<h3>独自用語の定義 (この記事だけで使う)</h3>
<ul class="kv">
  <li><b>埋蔵文化財包蔵地</b>: 文化財保護法 (1950 年) 第 93 条が規定する
    「<b>地下に文化財が埋蔵されていることが確実な土地</b>」。 古代-中世-
    近世の遺跡 (古墳・城跡・集落跡・寺院跡・窯跡・古道など) が地下に
    存在するエリアで、 開発行為の前には<b>事前協議</b>が必要。 広島県
    教育委員会が指定・管理し、 DoBoX で 11 シリーズに分けて公開している。</li>
  <li><b>古墳</b>: 古墳時代 (3-7 世紀) に築かれた首長・豪族の墳墓。
    形態によって<b>円墳</b> (円形)・<b>方墳</b> (正方形/長方形)・<b>前方後円墳</b>
    (鍵穴形, 大規模王墓) に分類される。 本県は中小規模円墳が圧倒的多数。</li>
  <li><b>城館跡 (城跡)</b>: 中世 (12-16 世紀) の<b>武家屋敷・砦・城</b>の跡。
    本県では戦国期の山城跡が多い。 「館跡」 は平地居館、 「城跡」 は山城。</li>
  <li><b>集落跡・散布地</b>: 古代以前の人間居住の痕跡。 散布地 = 遺物が
    地表に散らばる地点 (集落の上限・下限境界の判定が困難な遺跡)。</li>
  <li><b>都城・官衙跡</b>: 古代律令制 (7-10 世紀) の<b>役所跡</b>。
    郡衙・国衙・駅家など。 県内は数が少なく (13 件)、 全件が県史上重要。</li>
  <li><b>窯跡 (生産遺跡)</b>: 須恵器・瓦・鉄を生産した古代の<b>窯</b>の跡。
    本データでは「生産遺跡」 シリーズに分類。</li>
  <li><b>古道</b>: 山陽道・出雲街道などの古代-近世の街道。 本データでは
    「その他」 シリーズの「道」「一里塚」 種別で扱う。</li>
  <li><b>横穴式石室</b>: 古墳時代後期 (6-7 世紀) に普及した埋葬施設。
    側面から入る石造りの墓室。 概要 keyword mining で検出可能。</li>
  <li><b>竪穴式石室</b>: 古墳時代前期-中期 (3-5 世紀) の埋葬施設。
    上面から穴を掘って造る墓室。 横穴式より古い形式。</li>
  <li><b>須恵器</b>: 5 世紀以降に朝鮮半島から伝来した硬質灰色の陶質土器。
    古墳の副葬品として頻出。</li>
  <li><b>古墳径</b>: 円墳の<b>直径 (m)</b>。 概要 free text に「径Xm」 と
    記録される。 本記事では正規表現で {N_DIAM:,} 件を抽出。</li>
  <li><b>地理タイプ</b>: 本記事独自の市町分類 — 都市・沿岸・中山間・
    離島・近郊 の 5 区分。 L24 と同基準。</li>
</ul>

<h3>研究の問い (RQ)</h3>
<p>広島県内 22 市町の埋蔵文化財包蔵地は、 種別・時代・地理特性で
どのような構造を持ち、 何が広島県の古代-中世-近世の地理を規定したか?</p>

<h3>仮説 (5)</h3>
<ul>
  <li><b>H1 (中山間古墳偏在仮説)</b>: 古墳数 top 3 市町は<b>中山間地域</b>
    で、 古墳の <b>60% 以上</b>が中山間に分布する。 古代の権力中心は
    瀬戸内沿岸ではなく内陸河川流域 (太田川中流・芦田川中流・神之瀬川流域)
    にあった可能性。</li>
  <li><b>H2 (円墳支配仮説)</b>: 古墳の <b>50% 以上</b>が「円墳」 (中小規模)
    で、 前方後円墳 (大規模王墓) は <b>2% 未満</b>。 広島県は中小規模古墳の
    密集地で、 大和朝廷の中心ではなく地域豪族による中規模古墳群が主体。</li>
  <li><b>H3 (時代-種別対応仮説)</b>: 種別ごとに時代分布が顕著に異なる。
    「古墳」 = 古墳時代、 「城館跡」 = 中世、 「砂留」「経塚」 = 近世。
    各種別で時代 top 1 が <b>80% 以上</b>のシェアを占める。</li>
  <li><b>H4 (古墳横穴式石室普及仮説)</b>: 古墳の <b>40% 以上</b>が
    「横穴式石室」 に言及される。 広島県の古墳の主流は古墳時代後期
    (6-7 世紀) であり、 前期-中期 (3-5 世紀) の竪穴式石室は少数派。</li>
  <li><b>H5 (沿岸-内陸種別偏向仮説)</b>: 沿岸と中山間で種別構成が異なる。
    沿岸の古墳率 &lt; 中山間の古墳率 で <b>10 ポイント以上</b>の差がある。
    沿岸は中世城館・近世砂留が、 中山間は古墳・集落跡が主体という二極化。</li>
</ul>

<h3>到達点</h3>
<ul>
  <li>11 シリーズ × 22 市町を 1 つの統合表として読み込み、 種別・時代・
    地理特性で多角集計できる。</li>
  <li>古墳の概要 (free text) から円墳/方墳/前方後円・横穴式石室・須恵器・
    鉄製品などの<b>keyword mining</b>を実装できる。</li>
  <li>古墳径 (径Xm) を正規表現で抽出し、 ヒストグラムから中小規模古墳の
    支配構造を量化できる。</li>
  <li>地理タイプ × 種別のクロスから、 沿岸-内陸の文化的二極化を判定
    できる。</li>
  <li>L4 (浸水×文化財 300件) との重複を回避し、 本記事独自の構造分析
    視点 ({N_TOTAL:,} 件全件・古墳形態・出土物・地理タイプ) で深掘り
    できる。</li>
</ul>

<div class="note">
  <b>L4 との重複回避</b>: L4 は同シリーズの 1663 (都城・官衙跡 13件) と
  1669 (その他 198件) を<b>河川浸水想定区域とのオーバレイ判定</b>で扱った。
  L84 は 11 件全件を統合し、 <b>埋蔵文化財自体の地理構造</b>を分析する。
  L4 が「ハザード × 立地」 の交差視点であったのに対し、 L84 は<b>埋蔵
  文化財そのものの種別・時代・古墳形態・出土物</b>を主軸とする。 浸水軸は
  扱わない。 重複ゼロを徹底。
</div>
"""
sections.append(("学習目標と問い", intro_html))


# === 2. 使用データ ===
data_table_html = build_dataset_table()
data_html = f"""
<p>DoBoX のシリーズ「埋蔵文化財包蔵地一覧表」 は<b>11 dataset_id</b>
(1660-1670) からなる。 すべて 13 列の同一スキーマで提供されており、
1 つの DataFrame に縦結合可能。 本記事は<b>11 件全件</b>を統合する
(L4 が部分扱いの 2 件 (1663 + 1669) と被爆樹木 1 件で 300 件を扱った
のに対し、 本記事は 11 件全件 {N_TOTAL:,} 件を対象)。</p>

{data_table_html}

<h3>データ仕様 (11 シリーズ共通スキーマ)</h3>
<table>
  <tr><th>列名</th><th>型</th><th>備考</th></tr>
  <tr><td>番号</td><td>int</td><td>遺跡通番 (シリーズ内一意)</td></tr>
  <tr><td>名称</td><td>str</td><td>遺跡名 (例: 新宮第1号古墳)</td></tr>
  <tr><td>種別</td><td>str</td><td>古墳/円墳/館跡/官衙跡/集落跡/社寺/窯跡/砂留 等</td></tr>
  <tr><td>時代</td><td>str</td><td>縄文/弥生/古墳/古代/中世/近世/近代 等 (混在ありで正規化が必要)</td></tr>
  <tr><td>市町名</td><td>str</td><td>22 市町 (広島県の 23 市町中、 神石高原町・大崎上島町なども含む)</td></tr>
  <tr><td>所在地</td><td>str</td><td>地番・字レベルの所在地</td></tr>
  <tr><td>緯度</td><td>float</td><td>10 進度 (5-8 桁精度)</td></tr>
  <tr><td>経度</td><td>float</td><td>10 進度</td></tr>
  <tr><td>概要</td><td>str (free)</td><td>遺跡の<b>形態・規模・出土物</b>を記述
    (例: 「径7mの円墳，横穴式石室，須恵器」) — keyword mining 対象</td></tr>
  <tr><td>備考</td><td>str (free)</td><td>調査履歴・指定状況など</td></tr>
  <tr><td>全国遺跡地図広島県（昭和57年文化庁発行）</td><td>str</td>
      <td>1982 年文化庁刊行の全国遺跡地図参照番号 (古い記載)</td></tr>
  <tr><td>広島県遺跡地図の地図番号</td><td>int</td>
      <td>県の遺跡地図参照番号</td></tr>
  <tr><td>関連URL</td><td>str</td><td>多くは空</td></tr>
</table>

<h3>11 シリーズの件数構造 (load log)</h3>
{df_to_html_table(load_log_df)}

<h3>この表から読み取れること</h3>
<ul>
  <li><b>古墳・横穴 (#1660) が圧倒的多数 ({N_KOFUN:,} 件)</b>。 全体
    {N_TOTAL:,} 件の {N_KOFUN/N_TOTAL*100:.1f}% を占める。 広島県は
    「古墳の県」 と呼べる程の集積地。</li>
  <li>「<b>その他 (#1669)</b>」 は 198 件で、 種別 31 種・時代 25 種と
    多様。 経塚・砂留・祭祀遺跡・道・一里塚など、 主要 10 シリーズに
    収まらない<b>「複合遺跡群」</b>として機能している。</li>
  <li>1661 貝塚 (2件)・1665 社寺跡 (2件)・1666 生産遺跡 (1件)・
    1670 水中遺跡 (1件) は<b>件数極小</b>。 これは本データが「県内全件
    指定」 ではなく、 県教委が選定した一部のみ公開している可能性を示唆。
    実際の県内貝塚・寺院跡は数十-数百件存在することは考古学文献で
    確認可能。</li>
  <li>緯度経度カバー率は シリーズ平均で 92% 以上。 ほぼ全件が地理可視化
    可能。</li>
</ul>

<div class="note">
  <b>重要なデータ理解</b>: 本データは<b>「DoBoX 公開分」のみ</b>であり、
  広島県内の埋蔵文化財包蔵地の実総数 (数千件以上と推定) からは限定的。
  特に貝塚・社寺・生産遺跡・水中遺跡シリーズは件数極小で、 県内の
  実分布を代表しているとは言い難い。 本記事の主要分析対象は<b>古墳
  ({N_KOFUN:,} 件) + その他 (198 件) = 主要 9 割</b>とする。 統計的
  分析は古墳に集中する。
</div>
"""
sections.append(("使用データ", data_html))


# === 3. ダウンロード ===
dl_buttons = []
dl_buttons.append(
    '<a class="dlbtn" href="https://hiroshima-dobox.jp/datasets/1660" '
    'target="_blank" '
    'style="display:inline-block;padding:8px 14px;margin:4px;'
    'background:#cf222e;color:white;border-radius:6px;text-decoration:none;'
    'font-weight:bold;">▶ #1660 古墳・横穴 ({:,} 件)</a>'.format(
        int(load_log_df.loc[load_log_df["dataset_id"] == 1660, "n_records"].iloc[0])))
for dsid, _rid, _slug, label, _grp in DATASETS:
    if dsid == 1660:
        continue
    n = int(load_log_df.loc[load_log_df["dataset_id"] == dsid, "n_records"].iloc[0])
    dl_buttons.append(
        f'<a class="dlbtn" href="https://hiroshima-dobox.jp/datasets/{dsid}" '
        f'target="_blank" '
        f'style="display:inline-block;padding:6px 10px;margin:3px;'
        f'background:#0969da;color:white;border-radius:4px;text-decoration:none;'
        f'font-size:13px;">#{dsid} {escape(label)} ({n:,})</a>'
    )

dl_html = f"""
<h3>DoBoX へのリンク (元データセット 11 件)</h3>
<p>下記ボタンから DoBoX の各データセットページに移動し、 そこから CSV を
ダウンロードできる。 すべて utf-8-sig (BOM 付き UTF-8) の CSV、
ライセンスは CC-BY (商用・非商用とも可)。</p>
<div>{"".join(dl_buttons)}</div>

<h3>本記事が生成した中間データ・図 (再現用)</h3>
<ul>
  <li><a href="assets/L84_sites_all.csv" download>L84_sites_all.csv</a>
    — 統合済 全 {N_TOTAL:,} 件 × 派生指標 17 列 (緯度経度・古墳形態・
    出土物 keyword・時代正規化)</li>
  <li><a href="assets/L84_load_log.csv" download>L84_load_log.csv</a>
    — 11 シリーズ読込ログ</li>
  <li><a href="assets/L84_series_summary.csv" download>L84_series_summary.csv</a>
    — シリーズ別集計</li>
  <li><a href="assets/L84_city_summary.csv" download>L84_city_summary.csv</a>
    — 市町別集計 (件数・古墳率・密度)</li>
  <li><a href="assets/L84_era_series_cross.csv" download>L84_era_series_cross.csv</a>
    — 時代×種別 クロス集計</li>
  <li><a href="assets/L84_era_concentration.csv" download>L84_era_concentration.csv</a>
    — 種別ごとの時代集中度</li>
  <li><a href="assets/L84_kofun_form.csv" download>L84_kofun_form.csv</a>
    — 古墳形態 (円墳/方墳/前方後円/横穴) 集計</li>
  <li><a href="assets/L84_kofun_artifact.csv" download>L84_kofun_artifact.csv</a>
    — 古墳の出土物・埋葬形式</li>
  <li><a href="assets/L84_diameter_stats.csv" download>L84_diameter_stats.csv</a>
    — 古墳径分布統計</li>
  <li><a href="assets/L84_geo_summary.csv" download>L84_geo_summary.csv</a>
    — 地理タイプ別集計</li>
  <li><a href="assets/L84_geo_group_cross.csv" download>L84_geo_group_cross.csv</a>
    — 地理タイプ×種別 クロス</li>
  <li><a href="assets/L84_track_one_site.csv" download>L84_track_one_site.csv</a>
    — 1 件追跡 (要件 K)</li>
  <li><a href="assets/L84_l4_compare.csv" download>L84_l4_compare.csv</a>
    — L4 との重複・差別化整理</li>
  <li><a href="assets/L84_hypothesis.csv" download>L84_hypothesis.csv</a>
    — 仮説検証結果</li>
</ul>

<h3>図 (8 枚)</h3>
<ul>
  <li><a href="assets/L84_fig01_overview.png" download>fig01 県全域 主題図</a></li>
  <li><a href="assets/L84_fig02_city_ranking.png" download>fig02 市町別ランキング</a></li>
  <li><a href="assets/L84_fig03_era_series_heatmap.png" download>fig03 時代×シリーズ ヒートマップ</a></li>
  <li><a href="assets/L84_fig04_kofun_forms.png" download>fig04 古墳形態 3 区分地理</a></li>
  <li><a href="assets/L84_fig05_kofun_diameter_artifacts.png" download>fig05 古墳径ヒストグラム + 出土物</a></li>
  <li><a href="assets/L84_fig06_city_small_multiples.png" download>fig06 市町別 small multiples</a></li>
  <li><a href="assets/L84_fig07_burial_chambers.png" download>fig07 横穴式 vs 竪穴式 石室</a></li>
  <li><a href="assets/L84_fig08_geo_type_breakdown.png" download>fig08 地理タイプ × 種別 構成</a></li>
</ul>

<h3>再現スクリプト</h3>
<ul>
  <li><a href="L84_archaeological_sites.py" download>L84_archaeological_sites.py</a>
    — 単独実行可能。 データが無ければ ensure_dataset() が DoBoX から自動取得。</li>
</ul>
"""
sections.append(("ダウンロード", dl_html))


# === 4. 分析 1: 11 シリーズ統合と派生指標 ===
ana1_intro = """
<h3>狙い</h3>
<p>11 dataset_id (1660-1670) を 1 つの DataFrame に縦結合する。 各シリーズは
13 列の同一スキーマだが、 種別・時代の値は<b>シリーズ独立に命名</b>されて
おり、 統合後に正規化が必要。 本分析では (1) 統合読込、 (2) 種別 group 列の
追加、 (3) 時代の正規化、 (4) 古墳の概要 free text からの<b>keyword mining</b>
(円墳/方墳/前方後円/横穴式石室/須恵器/古墳径) を実装する。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>縦結合 (concat)</b>: 11 個の DataFrame を <code>pd.concat()</code>
    で 1 つに結合。 共通列 (13 列) は同名のため簡単に結合できる。
    各行に元シリーズを示す<b>group 列</b> (古墳/城館/官衙/...) を追加して
    トレース可能にする。</li>
  <li><b>時代の正規化</b>: 「古墳」「弥生～古墳」「古代～中世」 のような
    混在記載を、 ルールベースの関数で 9 区分 (縄文/弥生/古墳/古代/中世/
    近世/近代/その他/不明) に正規化。 文字列 contains で
    階層判定 (例: 「縄文」 を含み「弥生」 を含まなければ縄文)。</li>
  <li><b>keyword mining</b>: 概要 (free text) を <code>str.contains()</code>
    でキーワード判定。 「円墳」「横穴式石室」「須恵器」 などのキーワード
    出現を 0/1 フラグ化。 古墳径は<b>正規表現</b> <code>径\\s*([\\d\\.]+)\\s*m</code>
    で抽出。</li>
  <li><b>入力</b>: 11 CSV 計 {n_total} 件。 <b>出力</b>: 統合 DataFrame
    {n_total} 行 × 派生指標含 ~25 列。</li>
</ul>
""".replace("{n_total}", f"{N_TOTAL:,}")

ana1_code = '''
# 11 CSV を読込・縦結合
DATASETS = [
    (1660, 177383, "kofun",   "古墳・横穴",        "古墳"),
    (1661, 177384, "kaizuka", "貝塚",              "貝塚"),
    (1662, 177385, "shuraku", "集落跡・散布地",    "集落"),
    (1663, 177386, "tojo",    "都城・官衙跡",      "官衙"),
    (1664, 177387, "jokan",   "城館跡",            "城館"),
    (1665, 177388, "shaji",   "社寺跡",            "社寺"),
    (1666, 177389, "seisan",  "生産遺跡",          "生産"),
    (1667, 177390, "funbo",   "その他の墳墓",      "墳墓"),
    (1668, 177391, "kindai",  "近代以降の単独遺跡", "近代"),
    (1669, 177392, "sonota",  "その他",            "その他"),
    (1670, 177393, "suichu",  "水中遺跡",          "水中"),
]

frames = []
for dsid, rid, slug, label, group in DATASETS:
    p = DATA_DIR / f"maizo_{dsid}_{slug}.csv"
    df = pd.read_csv(p, encoding="utf-8-sig").dropna(subset=["名称"])
    df = df.rename(columns={"名称": "name", "種別": "type_raw",
                            "時代": "era_raw", "市町名": "muni",
                            "所在地": "addr", "緯度": "lat",
                            "経度": "lon", "概要": "desc",
                            "備考": "remark"})
    df["dataset_id"] = dsid
    df["series"] = label
    df["group"] = group
    frames.append(df)
sites = pd.concat(frames, ignore_index=True)

# 派生指標 1: 古墳形態 keyword
combo = sites["desc"].fillna("") + " " + sites["remark"].fillna("")
sites["form_enpun"] = combo.str.contains("円墳").astype(int)
sites["form_hofun"] = combo.str.contains("方墳").astype(int)
sites["form_zenpo"] = combo.str.contains("前方後円").astype(int)

# 派生指標 2: 出土物 keyword
sites["k_yokoana_seki"] = combo.str.contains("横穴式石室").astype(int)
sites["k_tateana_seki"] = combo.str.contains("竪穴|竪穴式").astype(int)
sites["k_sueki"]        = combo.str.contains("須恵器").astype(int)
sites["k_hajiki"]       = combo.str.contains("土師器").astype(int)
sites["k_tetsu"]        = combo.str.contains("鉄鏃|鉄製品|鉄刀|鉄剣|刀子").astype(int)

# 派生指標 3: 古墳径 (正規表現抽出)
import re
diam_match = combo.str.extract(r"径\\s*([\\d\\.]+)\\s*m")
sites["diameter_m"] = pd.to_numeric(diam_match[0], errors="coerce")

# 派生指標 4: 時代の正規化 (例)
def normalize_era(s):
    if pd.isna(s): return "不明"
    s = str(s)
    if "古墳" in s or "古噴" in s: return "古墳"
    if "中世" in s or "鎌倉" in s or "室町" in s: return "中世"
    if "近世" in s or "江戸" in s: return "近世"
    # ... (略)
    return "その他"
sites["era_norm"] = sites["era_raw"].apply(normalize_era)
'''

ana1_io = """
<h3>入出力の Before/After 具体例 (要件 K)</h3>
<p>1 件の遺跡 (新宮第1号古墳) が 11 シリーズ統合パイプラインの中で
どう変換されるかを段階別に追跡する。</p>
""" + df_to_html_table(T_track) + """
<p><b>この表から読み取れること</b>:</p>
<ul>
  <li>0→1 の段階で「種別=古墳」 が「group=古墳」 に正規化され、 10 シリーズに
    対しても同じ group 列で集計可能になる。</li>
  <li>1→2 の段階で「時代=古墳」 が「era_norm=古墳」 になる (例外として「弥生〜
    古墳」 のような混在は最後尾優先で「古墳」 に正規化)。</li>
  <li>5→6 の段階で「概要 = 径7mの円墳，横穴式石室」 から keyword mining が
    実行され、 form_enpun=1 / k_yokoana_seki=1 / diameter_m=7.0 が抽出される。
    これにより<b>free text を構造化データに変換</b>できる。</li>
  <li>9 の段階で「市町=広島市」 が「geo_type=都市」 に分類される。</li>
</ul>
"""

ana1_table = f"""
<h3>シリーズ別集計</h3>
{df_to_html_table(T_series)}
<p><b>この表から読み取れること</b>:</p>
<ul>
  <li>古墳・横穴 ({N_KOFUN:,} 件) が全体の <b>{N_KOFUN/N_TOTAL*100:.1f}%</b>を
    占める。 11 シリーズ中で圧倒的多数。</li>
  <li>その他 (198 件) は<b>種別 31 種</b>と多様で、 経塚・砂留・祭祀遺跡・
    道・一里塚など主要シリーズに収まらない遺跡群を扱う。</li>
  <li>残り 9 シリーズは合計 {N_TOTAL - N_KOFUN - 198:,} 件しかなく、 統計
    分析の主軸は<b>古墳 + その他</b>となる (本記事も基本この方針)。</li>
  <li>緯度経度カバー率はシリーズ平均で <b>{N_GEO/N_TOTAL*100:.1f}%</b>。
    ほぼ全件が地理可視化可能。</li>
</ul>
"""

ana1_html = (ana1_intro + code(ana1_code) + ana1_io + ana1_table)
sections.append(("分析 1: 11 シリーズ統合と派生指標 (keyword mining)",
                 ana1_html))


# === 5. 分析 2: 県全域分布 + 市町別集計 ===
ana2_intro = """
<h3>狙い</h3>
<p>緯度経度から geopandas の GeoDataFrame を作り、 EPSG:4326 (WGS84) →
EPSG:6671 (JGD2011 平面直角第 III 系, m 単位) に再投影。 県全域に
{n_geo:,} 点を主題図として配置し、 種別ごとの色分けで地理パターンを
描く (Fig 1)。 さらに市町別件数ランキング + 単位面積あたり密度の散布図で
H1 (中山間古墳偏在仮説) を検証する (Fig 2)。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>geopandas.points_from_xy</b>: 緯度経度配列から Point geometry を
    作る関数。 <code>EPSG:4326</code> (WGS84) を指定するのが定石。</li>
  <li><b>to_crs(EPSG:6671)</b>: 平面直角第 III 系 (広島県の標準 CRS)。
    単位は m なので、 距離計算・面積計算が正確になる。</li>
  <li><b>L15 admin cache の読込</b>: 行政界 zip 内の .geojson を BytesIO
    経由で読込み、 dissolve(by="city") で市町ごと 1 ポリゴンに集約。
    地図の背景レイヤとして使う。</li>
  <li><b>市町別密度</b>: 件数 / 市域面積 (km²) で密度を計算。 これにより
    広島市 ({N_KOFUN_HIROSHIMA} 件 / 906.7 km² = {hiro_density:.2f} 件/km²)
    と東広島市 ({n_higashihiro} 件 / 635.3 km² = {hh_density:.2f} 件/km²)
    の本当の集積度を比較できる。</li>
</ul>
""".format(
    n_geo=N_GEO,
    N_KOFUN_HIROSHIMA=int(T_city.loc[T_city["muni"] == "広島市", "n"].iloc[0])
        if "広島市" in T_city["muni"].values else 0,
    hiro_density=float(T_city.loc[T_city["muni"] == "広島市", "density_per_km2"].iloc[0])
        if "広島市" in T_city["muni"].values else 0.0,
    n_higashihiro=int(T_city.loc[T_city["muni"] == "東広島市", "n"].iloc[0])
        if "東広島市" in T_city["muni"].values else 0,
    hh_density=float(T_city.loc[T_city["muni"] == "東広島市", "density_per_km2"].iloc[0])
        if "東広島市" in T_city["muni"].values else 0.0,
)

ana2_code = '''
# Step 1: GeoDataFrame 化 (緯度経度 → Point)
geo = sites.dropna(subset=["lat", "lon"]).copy()
gdf_sites = gpd.GeoDataFrame(
    geo,
    geometry=[Point(lon, lat) for lon, lat in zip(geo["lon"], geo["lat"])],
    crs="EPSG:4326",
).to_crs("EPSG:6671")

# Step 2: L15 admin cache 読込 (22 市町)
admin_frames = []
for cname, dsid in ADMIN_MAP:
    z = ADMIN_DIR / f"admin_{dsid}_{cname}.zip"
    if z.exists():
        g = load_admin_zip(z)
        g["city"] = cname
        admin_frames.append(g)
admin_diss = (gpd.GeoDataFrame(pd.concat(admin_frames), crs="EPSG:4326")
              .to_crs("EPSG:6671")
              .dissolve(by="city", as_index=False))

# Step 3: 市町別集計 + 密度
T_city = (sites.groupby("muni")
          .agg(n=("site_id", "size"),
               n_kofun=("is_kofun", "sum"))
          .reset_index()
          .sort_values("n", ascending=False))
T_city["area_km2"] = T_city["muni"].map(lambda c: CITY_REF.get(c, (np.nan,))[0])
T_city["density_per_km2"] = T_city["n"] / T_city["area_km2"]
'''

ana2_fig1 = f"""
<h3>図 1: 県全域 主題図 (種別色分け)</h3>
<p><b>なぜこの図か</b>: {N_GEO:,} 点を 1 枚に配置し、 11 シリーズ (古墳・
城館・官衙 etc) の<b>地理的偏り</b>を一望するため。 古墳は赤・大量で薄く、
その他種別はカラフル・少数で濃く描き、 「古墳が県全域を覆い、 城館や官衙が
特定地点に集中する」 二層構造を視覚化する。</p>
{figure("assets/L84_fig01_overview.png", "図1: 県全域 埋蔵文化財 全 2,653 件")}
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>古墳 (赤・小点) が<b>県中央部から東部 (東広島・福山・尾道・北広島町)
    に密集</b>。 県西部 (廿日市・大竹) は古墳が薄い。</li>
  <li>城館跡 (青) は竹原市に 21 件集中 (= シリーズ 1664 全件)。 これは
    DoBoX 公開データの偏在で、 県内全城館跡は数百件あると推定される
    (本データの限界)。</li>
  <li>その他 (黄, n=198) は福山・東広島・三次・世羅町など東部に多く、
    経塚・砂留・祭祀遺跡として地域文化を担う。</li>
  <li>瀬戸内海島嶼部 (江田島・大崎上島) は遺跡が極めて少ない。 古墳時代
    の権力中心は本土側 (河川流域) にあったことが示唆される。</li>
</ul>
"""

ana2_fig2 = f"""
<h3>図 2: 市町別ランキング + 面積 vs 件数 散布図</h3>
<p><b>なぜこの図か</b>: 件数だけのランキングでは「広島市は人口 119 万人
だから多い」 のような<b>規模効果</b>が交絡する。 単位面積あたり密度を
散布図で可視化することで、 中山間市の真の集積度を見る。</p>
{figure("assets/L84_fig02_city_ranking.png", "図2: 市町別ランキング (左) と面積×件数散布図 (右)")}

<h4>表 5-1: 市町別集計 (件数 top 15)</h4>
{df_to_html_table(T_city.head(15)[
    ["muni", "n", "n_kofun", "kofun_pct", "area_km2", "density_per_km2", "geo_type"]
].rename(columns={"muni":"市町","n":"件数","n_kofun":"古墳数",
                   "kofun_pct":"古墳率%","area_km2":"面積km²",
                   "density_per_km2":"密度/km²","geo_type":"地理タイプ"}))}

<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li><b>東広島市 ({int(T_city.loc[T_city["muni"]=="東広島市","n"].iloc[0]):,} 件)</b>が
    トップ。 中山間 + 都市の中間で、 西条盆地・賀茂郡などの古代-中世遺跡が
    密集。 H1 を強支持。</li>
  <li><b>広島市 ({int(T_city.loc[T_city["muni"]=="広島市","n"].iloc[0]):,} 件)</b>は
    2 位だが、 単位面積あたり密度では中位 (= 市域 906 km² が広いため)。
    沿岸の人工地盤 (デルタ平野) より、 安佐北区・佐伯区の山間部に古墳が
    集中。</li>
  <li><b>北広島町 ({int(T_city.loc[T_city["muni"]=="北広島町","n"].iloc[0]):,} 件)・
    府中市 ({int(T_city.loc[T_city["muni"]=="府中市","n"].iloc[0]):,} 件)・
    神石高原町 ({int(T_city.loc[T_city["muni"]=="神石高原町","n"].iloc[0]):,} 件)</b>
    が中山間 top 3。 中山間 (緑色棒) が上位を独占。</li>
  <li>沿岸都市 (青色棒): 呉・廿日市は<b>件数 30-50 件と低位</b>。 沿岸は
    古代以降に開発されたエリアで、 古墳時代の中心ではない。</li>
  <li>近郊町 (紫色棒) は件数極小 (府中町 7 / 海田町 11)。 市域も小さく
    (10-15 km²)、 自然的にも遺跡数は少ない。</li>
</ul>
"""

ana2_html = (ana2_intro + code(ana2_code) + ana2_fig1 + ana2_fig2)
sections.append(("分析 2: 県全域分布と市町別集計", ana2_html))


# === 6. 分析 3: 時代×種別ヒートマップ (H3 検証) ===
ana3_intro = """
<h3>狙い</h3>
<p>11 シリーズ × 9 時代区分の<b>クロス集計表</b>を作り、 ヒートマップで
可視化する。 「古墳シリーズは古墳時代に集中、 城館跡は中世に集中、
その他は近世が多い」 のような<b>種別×時代の対応関係</b>を一望し、
H3 (各種別で時代 top 1 が 80% 以上) を検証する。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>クロス集計</b>: <code>groupby(["era_norm", "group"]).size().unstack()</code>
    で行=時代、 列=種別の表を作る。 <code>fillna(0)</code>で空セルを埋める。</li>
  <li><b>matplotlib.imshow</b>: 行列値をヒートマップで可視化。 <code>cmap="YlOrRd"</code>
    で 0 = 黄→大値 = 赤の階調。</li>
  <li><b>セルラベル</b>: 各セルに件数を直接書き込む。 値が cmap の半分以上
    なら白文字、 それ未満なら黒文字 (コントラスト確保)。</li>
  <li><b>集中度</b>: 各種別で「最大時代の件数 / 種別総数」 を計算。 これが
    80% を超えれば「種別と時代がほぼ 1:1 対応」 と判定。</li>
</ul>
"""

ana3_code = '''
# 時代×種別 クロス集計
T_era_series = (sites.groupby(["era_norm", "group"]).size()
                .unstack(fill_value=0))

# 種別ごとの時代集中度
era_concentration = []
for grp, sub in sites.groupby("group"):
    vc = sub["era_norm"].value_counts()
    if len(vc) > 0:
        top_era, top_n = vc.index[0], vc.iloc[0]
        era_concentration.append({
            "group": grp, "n": len(sub),
            "top_era": top_era, "top_n": int(top_n),
            "top_pct": round(top_n / len(sub) * 100, 1)
        })
T_era_conc = pd.DataFrame(era_concentration)

# H3 検証 (件数 5+ の群のみ対象)
H3_groups = T_era_conc[T_era_conc["n"] >= 5]
h3_n_passed = (H3_groups["top_pct"] >= 80).sum()
'''

ana3_fig = f"""
<h3>図 3: 時代×種別 ヒートマップ</h3>
<p><b>なぜこの図か</b>: 11 シリーズ × 9 時代の同時情報を表だけで読み取るのは
困難。 ヒートマップ + セル内数値で<b>「どの種別がどの時代に集中するか」</b>を
1 枚で示せる。 数値も併記することで、 読み取り精度も担保する。</p>
{figure("assets/L84_fig03_era_series_heatmap.png", "図3: 時代×シリーズ ヒートマップ")}

<h4>表 6-1: 種別ごとの時代集中度</h4>
{df_to_html_table(T_era_conc.rename(columns={
    "group":"種別","n":"件数","top_era":"top時代",
    "top_n":"top件数","top_pct":"top占有率%"}))}

<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li><b>古墳シリーズ ({N_KOFUN:,} 件)</b>は「古墳」 時代が
    {int(T_era_conc.loc[T_era_conc["group"]=="古墳","top_n"].iloc[0]):,} 件
    ({float(T_era_conc.loc[T_era_conc["group"]=="古墳","top_pct"].iloc[0]):.1f}%)。
    H3 を強支持。 古墳=古墳時代という対応関係はほぼ 1:1。</li>
  <li><b>城館跡 (21 件)</b>は<b>全件が中世 (100%)</b>。 戦国期山城が主体で、
    古代以前・近世以降には属さない。</li>
  <li><b>官衙 (13 件)</b>は<b>古代/平安/奈良に分散</b>。 律令制 (7-10 世紀)
    の役所跡として時代範囲が広い。 top 占有率 50-60% 程度。</li>
  <li><b>その他 (198 件)</b>は近世が最多 (72 件 = 36.4%)。 砂留・経塚・
    祭祀遺跡が近世に多く築かれたことを反映。 中世も 22% で、 これは
    経塚 (中世仏教信仰) が多いため。</li>
  <li>左下の<b>近代</b>列は近代以降の遺跡 (1668 シリーズ + その他の被爆樹木
    など) でほぼ空白。 本県の埋蔵文化財は<b>古代-近世が主体</b>。</li>
</ul>
"""

ana3_html = (ana3_intro + code(ana3_code) + ana3_fig)
sections.append(("分析 3: 時代×種別ヒートマップ (H3 検証)",
                 ana3_html))


# === 7. 分析 4: 古墳形態 + 古墳径 + 出土物 (H2, H4 検証) ===
ana4_intro = f"""
<h3>狙い</h3>
<p>古墳 ({N_KOFUN:,} 件) の概要 free text を keyword mining し、
(1) 形態 (円墳/方墳/前方後円)、 (2) 径 (m)、 (3) 出土物 (横穴式石室・
須恵器・鉄製品) の 3 軸で構造化する。 古墳形態の地図 (Fig 4)、
径ヒストグラム + 出土物棒 (Fig 5) で視覚化し、 H2 (円墳 ≥ 50% かつ
前方後円 < 2%) と H4 (横穴式石室言及率 ≥ 40%) を検証する。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>str.contains(keyword)</b>: pandas の文字列メソッドで keyword を
    含む行を 0/1 でフラグ化。 単純だが意味のある特徴量を抽出できる。</li>
  <li><b>正規表現 extract</b>: <code>径\\s*([\\d\\.]+)\\s*m</code>のパターンで
    数値を抽出。 「径7m」「径 12.5 m」 などの表記揺れにも対応。</li>
  <li><b>地理可視化 small multiples</b>: 形態 3 区分を 3 panel に並べ、
    同一 CRS で比較。 各 panel に件数を併記し<b>形態の県内地理</b>を一望。</li>
</ul>

<div class="note">
  <b>keyword mining の限界</b>: 概要欄が空 (NaN) の古墳は判定不能。
  本データでは概要記載率は約 70% (備考込み) で、 残り 30% の古墳は
  「形態不明」 として集計から外れる。 言及率 ≠ 真の形態占有率である点に
  注意。 ただし、 言及率は<b>下限値</b>として「少なくとも N% は円墳」
  と解釈できる。
</div>
"""

ana4_code = '''
# Step 1: keyword フラグ化 (古墳のみ)
kofun = sites[sites["group"] == "古墳"].copy()
combo = kofun["desc"].fillna("") + " " + kofun["remark"].fillna("")
kofun["form_enpun"] = combo.str.contains("円墳").astype(int)
kofun["form_hofun"] = combo.str.contains("方墳").astype(int)
kofun["form_zenpo"] = combo.str.contains("前方後円").astype(int)
kofun["k_yokoana_seki"] = combo.str.contains("横穴式石室").astype(int)
kofun["k_tateana_seki"] = combo.str.contains("竪穴|竪穴式").astype(int)
kofun["k_sueki"]        = combo.str.contains("須恵器").astype(int)
kofun["k_tetsu"]        = combo.str.contains("鉄鏃|鉄製品|鉄刀|鉄剣").astype(int)

# Step 2: 古墳径 (正規表現抽出)
import re
diam_match = combo.str.extract(r"径\\s*([\\d\\.]+)\\s*m")
kofun["diameter_m"] = pd.to_numeric(diam_match[0], errors="coerce")

# Step 3: 形態地図 (3 panel) + 径ヒストグラム + 出土物棒
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
panels = [
    ("円墳",       kofun[kofun["form_enpun"] == 1],   "#cf222e"),
    ("方墳",       kofun[kofun["form_hofun"] == 1],   "#0969da"),
    ("前方後円墳", kofun[kofun["form_zenpo"] == 1],   "#9a6700"),
]
for ax_, (label, sub, col) in zip(axes, panels):
    admin_diss.plot(ax=ax_, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.3)
    sub.plot(ax=ax_, color=col, markersize=14, alpha=0.7)
    ax_.set_title(f"{label} (n={len(sub):,})")
'''

ana4_fig4 = f"""
<h3>図 4: 古墳形態 3 区分の県内分布</h3>
<p><b>なぜこの図か</b>: 古墳 {N_KOFUN:,} 件のうち、 円墳 ({n_enpun:,} 件)・
方墳 ({n_hofun} 件)・前方後円墳 ({n_zenpo} 件) の<b>地理的差異</b>を
3 panel small multiples で比較。 同一 CRS・同一スケールで並べることで、
分布密度の違いを直感的に把握できる。</p>
{figure("assets/L84_fig04_kofun_forms.png", "図4: 古墳形態 3 区分の県内分布")}

<h4>表 7-1: 古墳形態 集計</h4>
{df_to_html_table(T_kofun_form)}

<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li><b>円墳が圧倒的多数 ({n_enpun:,} 件 / {n_enpun/N_KOFUN*100:.1f}%)</b>。
    <b>H2 前半 (円墳 ≥ 50%) は支持</b>。 県全域に薄く広く分布し、
    「古墳=ほぼ円墳」 の県と言える。</li>
  <li><b>前方後円墳は {n_zenpo} 件のみ ({n_zenpo/N_KOFUN*100:.2f}%)</b>。
    <b>H2 後半 (前方後円 &lt; 2%) は支持</b>。 大和朝廷の中心地 (奈良・
    大阪) のような巨大王墓集積はなく、 地域豪族の中規模古墳が主体。</li>
  <li>方墳 ({n_hofun} 件) は<b>福山・尾道など県東部に集中</b>。 古墳時代
    終末期 (7 世紀) の特徴的形態で、 県西-中部には少ない。</li>
  <li>前方後円墳 ({n_zenpo} 件) は<b>三原・福山・東広島など瀬戸内寄りに
    集中</b>。 海路アクセスが良い場所に大豪族墓が築かれた。 これは
    <b>古墳時代の海上交易と権力中心の関係</b>を示唆する。</li>
</ul>
"""

ana4_fig5 = f"""
<h3>図 5: 古墳径ヒストグラム + 出土物 keyword 棒</h3>
<p><b>なぜこの図か</b>: 古墳径の分布で「中小規模古墳が主流」 を量化し、
出土物 keyword 棒で「古墳時代後期 (横穴式石室) が前期-中期 (竪穴式石室) を
圧倒する」 を量化する。 2 図並列で<b>サイズ × 形式 × 副葬品</b>の 3 軸を
1 枚に。</p>
{figure("assets/L84_fig05_kofun_diameter_artifacts.png", "図5: 径ヒスト+出土物棒")}

<h4>表 7-2: 古墳径分布統計</h4>
{df_to_html_table(T_diam)}

<h4>表 7-3: 古墳の出土物・埋葬形式 言及率</h4>
{df_to_html_table(T_kofun_artifact)}

<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>径ヒストグラムは<b>右に強く偏ったロングテール分布</b>。 中央値
    {float(diam.median()):.1f} m、 平均 {float(diam.mean()):.1f} m、
    {(diam <= 10).sum() / N_DIAM * 100:.1f}% が径 ≤ 10 m。 「<b>中小規模
    円墳が支配</b>」 が量的に確認された。</li>
  <li>径 30 m 超の大規模古墳は {(diam > 30).sum()} 件のみ ({(diam > 30).sum() / N_DIAM * 100:.1f}%)。
    最大は {float(diam.max()):.1f} m。 大和朝廷中枢の前方後円墳 (200m+) と
    比較すると規模が一桁違う。</li>
  <li><b>横穴式石室言及率 = {n_yokoana_seki/N_KOFUN*100:.1f}%</b>
    ({n_yokoana_seki:,} / {N_KOFUN:,})。 <b>H4 ({n_yokoana_seki/N_KOFUN*100:.1f}%
    {'≥' if n_yokoana_seki/N_KOFUN*100 >= 40 else '<'} 40%) {'支持' if n_yokoana_seki/N_KOFUN*100 >= 40 else '反証'}</b>。
    古墳時代後期 (6-7 世紀) の埋葬形式が圧倒的多数。</li>
  <li>竪穴式石室はわずか {n_tateana_seki} 件 ({n_tateana_seki/N_KOFUN*100:.1f}%)。
    前期-中期古墳 (3-5 世紀) は本県では極めて少数派。 = 県内古墳時代の
    実質的開始は<b>6 世紀以降</b>と解釈できる。</li>
  <li>須恵器言及率 {kofun["k_sueki"].sum()/N_KOFUN*100:.1f}% は朝鮮半島
    由来の硬質土器が古墳副葬品の標準であったことを示す。</li>
</ul>
"""

ana4_html = (ana4_intro + code(ana4_code) + ana4_fig4 + ana4_fig5)
sections.append(("分析 4: 古墳形態・径・出土物 (H2, H4 検証)", ana4_html))


# === 8. 分析 5: 市町別 small multiples ===
ana5_intro = f"""
<h3>狙い</h3>
<p>件数 top 12 市町の遺跡分布を<b>small multiples マップ</b> (3×4 panel)
で並列表示。 同一スケール・同一凡例で 12 市町を比較できるため、 「中山間
市は山間に古墳が散在、 沿岸市は限定エリアに集中」 のような<b>地理パターンの
市町間差異</b>が一望できる。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>small multiples</b>: 同一構造の図を grid に並べる手法。 1 枚ずつ
    別ファイルにするより認知負荷が低く、 比較が容易。</li>
  <li>各 panel: 行政界ポリゴン (薄灰) + 種別色分け点を重ねる。 panel 内で
    aspect="equal" を保つことで、 市町の真の形状を保つ。</li>
  <li>件数情報を panel タイトルに含める (「東広島市 (都市) 総 757 件 / 古墳
    728」 のように)。</li>
</ul>
"""

ana5_fig = f"""
<h3>図 6: 市町別 small multiples (件数上位 12 市町)</h3>
<p><b>なぜこの図か</b>: 件数だけのランキング (図 2) では空間構造が見えない。
small multiples で同一スケール並列することで、 市域内の<b>遺跡集中エリア</b>を
12 市町同時比較できる。</p>
{figure("assets/L84_fig06_city_small_multiples.png", "図6: 市町別 small multiples")}

<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>東広島市</b>: 西条盆地・賀茂郡を中心に古墳が密集。 中央部の
    平地でなく<b>周辺丘陵地</b>に多数。</li>
  <li><b>広島市</b>: 安佐北区・佐伯区の山間部に古墳。 中区・南区の
    デルタ平野には遺跡が極めて少ない (= 都市開発で消失した可能性も高い)。</li>
  <li><b>北広島町・神石高原町・府中市</b>: 河川流域に沿って線状に遺跡が
    並ぶ。 古代の街道 (山陽道支線) や河川流域の集落-墓地配置を反映。</li>
  <li><b>福山市</b>: 芦田川中流域に密集。 古代備後国の中心地で、 官衙
    (1663) も含む。 古代-中世の複合遺跡群。</li>
  <li><b>尾道市・三原市・竹原市</b> (沿岸): 海岸線から離れた山際に
    遺跡が並ぶ。 古墳時代-中世の集落は海岸ではなく<b>後背丘陵</b>に
    立地した。</li>
  <li><b>呉市</b>: 件数 38 と少なく、 軍港都市として近代に発展した
    ことが古墳時代の遺跡密度に反映されている (= 軍港開発前の地形は
    急峻で、 古墳築造に不向き)。</li>
</ul>
"""

ana5_html = (ana5_intro + ana5_fig)
sections.append(("分析 5: 市町別 small multiples", ana5_html))


# === 9. 分析 6: 横穴式石室 vs 竪穴式石室 地理 ===
ana6_intro = f"""
<h3>狙い</h3>
<p>古墳時代後期 (6-7 世紀) の<b>横穴式石室</b>と前期-中期 (3-5 世紀) の
<b>竪穴式石室</b>の県内地理を並列比較。 古墳時代の<b>時間軸</b>を埋葬形式の
分布で表現し、 県内古墳時代の実質的開始時期を地理的に追う。</p>

<h3>手法</h3>
<ul>
  <li>古墳 ({N_KOFUN:,} 件) の概要を keyword mining し、
    横穴式石室 ({n_yokoana_seki:,} 件) と竪穴式石室 ({n_tateana_seki} 件) の
    geometry のみを抽出。</li>
  <li>2 panel small multiples で並列描画。 件数 + 言及率もタイトルに併記。</li>
</ul>
"""

ana6_fig = f"""
<h3>図 7: 横穴式石室 vs 竪穴式石室 地理可視化</h3>
<p><b>なぜこの図か</b>: 古墳の埋葬形式は時間軸 (前期 vs 後期) に対応する
ため、 地理的分布を比較すれば「古墳時代の県内拡大過程」 が可視化できる。
表だけでは地理性が消える。</p>
{figure("assets/L84_fig07_burial_chambers.png", "図7: 横穴式石室 vs 竪穴式石室")}

<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>横穴式石室 ({n_yokoana_seki:,} 件) は<b>県全域に分布</b>。 古墳時代
    後期 (6-7 世紀) には県内ほぼ全域で古墳築造が普及していた。</li>
  <li>竪穴式石室 ({n_tateana_seki} 件) は<b>東広島・広島・神石高原町・
    福山に少数点在</b>。 古墳時代前期-中期 (3-5 世紀) の権力中心は
    限定的で、 6 世紀以降に県全域に拡大。</li>
  <li>両者の比 = {n_tateana_seki}:{n_yokoana_seki} ≒
    1:{n_yokoana_seki / max(n_tateana_seki, 1):.0f}。 後期が圧倒的に多く、
    本県の古墳時代は<b>後期主導</b>と判定できる。</li>
  <li>これは大和朝廷からの距離・伝播の遅さを反映している可能性。
    大和地方では 4-5 世紀の前方後円墳が中心だが、 広島 (備後・安芸)
    では 6-7 世紀以降に古墳築造が本格化したと解釈できる。</li>
</ul>
"""

ana6_html = (ana6_intro + ana6_fig)
sections.append(("分析 6: 横穴式石室 vs 竪穴式石室 地理可視化", ana6_html))


# === 10. 分析 7: 地理タイプ × 種別構成 (H5 検証) ===
ana7_intro = f"""
<h3>狙い</h3>
<p>地理タイプ (都市・沿岸・中山間・離島・近郊) × 種別 (古墳・城館・官衙
等) の<b>構成比 (積み上げ棒)</b>と件数+古墳率を並列描画。 H5 (沿岸古墳率
&lt; 中山間古墳率 で 10 ポイント以上の差) を検証する。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>積み上げ棒 (stacked bar)</b>: 各地理タイプの<b>構成比</b>を 100% で
    正規化。 タイプ間で「種別の比率」 を比較できる (絶対値ではなく相対値)。</li>
  <li><b>件数棒</b>: 絶対件数 + 古墳率%をタイトルに併記。 構成比 + 絶対値の
    両面情報を 1 枚に。</li>
</ul>
"""

ana7_code = '''
# 地理タイプ × group クロス → 100% 正規化 → 積み上げ棒
T_geo_group = (sites.groupby(["geo_type", "group"]).size()
               .unstack(fill_value=0))
T_geo_pct = T_geo_group.div(T_geo_group.sum(axis=1), axis=0) * 100

bottom = np.zeros(len(T_geo_pct))
for grp in T_geo_pct.columns:
    ax.bar(T_geo_pct.index, T_geo_pct[grp], bottom=bottom,
           color=GROUP_COLOR[grp], label=grp)
    bottom += T_geo_pct[grp].values

# H5 検証: 沿岸古墳率 vs 中山間古墳率
coast_kofun_pct = (sites[(sites["group"] == "古墳") &
                          (sites["geo_type"] == "沿岸")].shape[0]
                   / max(sites[sites["geo_type"] == "沿岸"].shape[0], 1) * 100)
inland_kofun_pct = (sites[(sites["group"] == "古墳") &
                           (sites["geo_type"] == "中山間")].shape[0]
                    / max(sites[sites["geo_type"] == "中山間"].shape[0], 1) * 100)
h5_diff = inland_kofun_pct - coast_kofun_pct
'''

ana7_fig = f"""
<h3>図 8: 地理タイプ × 種別構成 + 件数 + 古墳率%</h3>
<p><b>なぜこの図か</b>: H5 (沿岸 vs 中山間で古墳率に差) を 1 枚で示すため、
構成比 (相対) + 件数 (絶対) を並列描画する。 構成比だけでは沿岸の絶対量が
分からず、 件数だけでは種別構成が分からない。</p>
{figure("assets/L84_fig08_geo_type_breakdown.png", "図8: 地理タイプ別 種別構成と件数")}

<h4>表 10-1: 地理タイプ別 集計</h4>
{df_to_html_table(T_geo_summary)}

<h4>表 10-2: 地理タイプ × 種別 件数クロス</h4>
{df_to_html_table(T_geo_group.reset_index())}

<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li><b>中山間 ({int(T_geo_total['中山間']):,} 件) の古墳率
    {float(T_geo_kofun_pct['中山間']):.1f}%</b>。 古墳が圧倒的多数。
    山間部は古代の主要地理。</li>
  <li><b>沿岸 ({int(T_geo_total['沿岸']):,} 件) の古墳率
    {float(T_geo_kofun_pct['沿岸']):.1f}%</b>。 中山間より低い。
    城館・砂留・官衙など複合的な遺跡構成。</li>
  <li>差 = {float(T_geo_kofun_pct['中山間']) - float(T_geo_kofun_pct['沿岸']):.1f} pt。
    H5 ({h5_diff:+.1f} pt {'≥' if h5_diff >= 10 else '<'} 10 pt)
    {'<b>支持</b>' if h5_diff >= 10 else '<b>反証/部分支持</b>'}。</li>
  <li><b>都市 ({int(T_geo_total['都市']):,} 件)</b>は古墳率
    {float(T_geo_kofun_pct['都市']):.1f}%。 中山間と同程度の高い古墳率は、
    広島市・東広島市・福山市の山間部 (安佐北・西条・芦田川流域) に古墳が
    密集するためで、 都市中心部 (デルタ平野・市街地) ではない。</li>
  <li><b>離島 ({int(T_geo_total['離島']):,} 件)</b>は江田島市 + 大崎上島
    町合計で 20 件と極小。 離島は古墳時代の中心ではなかった。</li>
  <li><b>近郊 ({int(T_geo_total['近郊']):,} 件)</b>は府中町・海田町・
    熊野町・坂町合計で 30 件と少ない。 市域も小さく、 自然的にも遺跡数
    は少ない。</li>
</ul>
"""

ana7_html = (ana7_intro + code(ana7_code) + ana7_fig)
sections.append(("分析 7: 地理タイプ × 種別構成 (H5 検証)", ana7_html))


# === 11. 仮説検証総合 ===
hyp_html = f"""
<h3>5 仮説の検証結果</h3>
{df_to_html_table(T_hyp)}

<h3>判定の要点</h3>
<ul>
  <li><b>H1 (中山間古墳偏在)</b>: 中山間古墳率 {mountainous_pct:.1f}%。
    {'<b>支持</b>' if h1_ok else '<b>反証</b>'}。
    {'古代の権力中心は瀬戸内沿岸ではなく内陸に偏在' if h1_ok else '中山間集中度は仮説より低い'}。</li>
  <li><b>H2 (円墳支配)</b>: 円墳 {h2_enpun_pct:.1f}% / 前方後円
    {h2_zenpo_pct:.2f}%。 {'<b>支持</b>' if h2_ok else '<b>部分支持</b>'}。
    広島県は中小規模円墳の密集地で大和朝廷的な巨大王墓は不在。</li>
  <li><b>H3 (時代-種別対応)</b>: {h3_n_total} 群中 {h3_n_passed} 群で
    時代 top 1 が 80% 以上。 {'<b>支持</b>' if h3_ok else '<b>部分支持</b>'}。
    古墳=古墳時代、 城館=中世、 官衙=古代という対応は確立している。</li>
  <li><b>H4 (横穴式石室普及)</b>: 言及率 {h4_pct:.1f}%。
    {'<b>支持</b>' if h4_ok else '<b>反証</b>'}。
    {'本県古墳時代の主流は後期 (6-7 世紀)' if h4_ok else '横穴式石室の言及率は仮説より低い'}。</li>
  <li><b>H5 (沿岸-内陸偏向)</b>: 沿岸 {coast_kofun_pct:.1f}% vs 中山間
    {inland_kofun_pct:.1f}% (差 {h5_diff:+.1f} pt)。
    {'<b>支持</b>' if h5_ok else '<b>反証/部分支持</b>'}。
    {'沿岸-内陸の文化的二極化が量的に確認された' if h5_ok else '沿岸-内陸差は仮説より小さい'}。</li>
</ul>

<h3>L4 との重複回避まとめ</h3>
{df_to_html_table(T_l4_compare)}

<p><b>L84 独自の発見 (L4 で扱われていない)</b>:</p>
<ul>
  <li>古墳形態 (円墳/方墳/前方後円) の<b>keyword mining</b> + 地理可視化</li>
  <li>古墳径 (m) の正規表現抽出による<b>サイズ分布</b>定量化</li>
  <li>出土物 (横穴式石室・須恵器・鉄製品) の<b>言及率</b>と地理パターン</li>
  <li>11 シリーズ × 22 市町 × 9 時代の<b>3 軸クロス集計</b></li>
  <li>地理タイプ × 種別の<b>構成比積み上げ棒</b></li>
</ul>

<p><b>L4 との視点の差</b>: L4 は「文化財が浸水するか」 を <b>sjoin</b>で
判定する記事 (= ハザード×立地)。 L84 は「埋蔵文化財の地理構造そのもの」 を
種別×時代×形態×出土物で記述する記事 (= 文化財自体の構造)。 重複ゼロ。</p>
"""
sections.append(("仮説検証と考察", hyp_html))


# === 12. 発展課題 ===
dev_html = f"""
<h3>結果 X → 新仮説 Y → 課題 Z (3 段論理)</h3>

<h4>発展課題 1: 古墳径と地形 (尾根・丘陵・河川流域) の関係</h4>
<ul>
  <li><b>結果 X</b>: 古墳径中央値 {float(diam.median()):.1f} m、
    {(diam <= 10).sum()/N_DIAM*100:.1f}% が径 ≤ 10 m (図 5)。 中小規模が
    主流。</li>
  <li><b>新仮説 Y</b>: 古墳径は<b>立地地形</b>と相関するのではないか?
    山頂・丘陵尾根に築かれた古墳は規模に上限がある (狭い尾根=小さい
    墳丘)、 平地の古墳は大規模化しやすい仮説。</li>
  <li><b>課題 Z</b>: L40 (DEM 標高データ) と sjoin で各古墳の標高・
    地形分類 (尾根/谷/平地) を取得し、 径 vs 立地のクロス散布を作る。
    古墳築造の<b>地形制約</b>を量的に評価できる。</li>
</ul>

<h4>発展課題 2: 古墳と古道 (山陽道・出雲街道) の関係</h4>
<ul>
  <li><b>結果 X</b>: 北広島町・府中市の古墳分布は<b>河川流域に沿って
    線状</b>に並ぶ (図 6 small multiples)。 主要街道との重なりが
    示唆される。</li>
  <li><b>新仮説 Y</b>: 古墳の地理分布は<b>古代街道 (山陽道・出雲街道)</b>
    沿いに集中するのではないか? 街道近接古墳は地域豪族の権威表示と
    して通行人に見える位置に築かれた可能性。</li>
  <li><b>課題 Z</b>: 古道の GIS データ (DoBoX 外、 山陽道・出雲街道の
    推定ルート) を取得し、 古墳との最近距離分布を計算。 ランダム分布と
    比較し<b>街道近接性</b>を量化できる。</li>
</ul>

<h4>発展課題 3: 城館跡の戦略的立地</h4>
<ul>
  <li><b>結果 X</b>: 城館跡 21 件 (1664) は全件竹原市に集中 (= DoBoX
    公開分の偏在)。 図 1 で青点が竹原市に集中。</li>
  <li><b>新仮説 Y</b>: 城館跡は<b>標高 100-300 m の山頂・尾根</b>に集中
    し、 視認性 (見渡しの良さ) と防御性 (登攀困難) を両立する戦略的
    立地が選ばれているのではないか?</li>
  <li><b>課題 Z</b>: 城館 21 件 + L40 標高 + 視認性計算 (LoS, line of
    sight) を結合。 戦国期の山城配置の<b>戦略地理学</b>として深掘り
    できる。</li>
</ul>

<h4>発展課題 4: 「DoBoX 公開分」 と「実総数」 のギャップ</h4>
<ul>
  <li><b>結果 X</b>: 貝塚 2 件・社寺 2 件・水中遺跡 1 件と<b>件数極小</b>
    (図 1, 表 6)。 県内実総数とのギャップが大きい。</li>
  <li><b>新仮説 Y</b>: DoBoX 公開分は<b>「県教委が選定した代表事例」</b>で、
    全数登録されていないのではないか? 実際の県内貝塚は数十件 (考古学
    文献)、 寺院跡は数百件存在するはず。</li>
  <li><b>課題 Z</b>: 県教委発行の埋蔵文化財包蔵地一覧表 (PDF・冊子) と
    DoBoX 公開分の差を比較し、 公開ポリシーの限界を可視化。 オープン
    データの<b>網羅性評価</b>として方法論的価値がある。</li>
</ul>

<h4>発展課題 5: 古墳分布と古代行政区画 (備後・安芸・備中)</h4>
<ul>
  <li><b>結果 X</b>: 福山・尾道・三原 (=古代備後国) に古墳・官衙が
    集中。 広島市・呉・東広島 (=古代安芸国) は別パターン。 県内には
    古代行政の<b>2 つの中心地</b>があった (図 1, 図 6)。</li>
  <li><b>新仮説 Y</b>: 県境を<b>備後/安芸の古代国境</b> (沼田川流域)
    で再分類すると、 H1 の中山間偏在仮説の構造はどう変化するか?
    現行行政区分でなく古代行政区分での分析が、 古墳時代の権力地理を
    より正確に反映するのではないか?</li>
  <li><b>課題 Z</b>: 古代国境 (備後/安芸/備中) の polygon を作り、
    sjoin で各遺跡を再分類。 国別集計で古代国家の権力中心地理を
    描写できる。</li>
</ul>
"""
sections.append(("発展課題", dev_html))


# === 13. 再現スクリプト ===
script_path = Path(__file__).resolve()
with open(script_path, "r", encoding="utf-8") as fh:
    src_text = fh.read()

src_html = f"""
<p>本記事のすべての分析を 1 つの Python スクリプトで再現できる。</p>
<p><b>動作環境</b>: Python 3.10+ / pandas / numpy / matplotlib / geopandas /
shapely / pyogrio / pygments / requests。 データは
<code>data/extras/L84_archaeological_sites/</code> + L15 admin cache から
読込。 無ければ <code>ensure_dataset()</code> が DoBoX から自動取得。</p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/L84_archaeological_sites.py</code></pre>

<h3>L84_archaeological_sites.py 全文</h3>
{code(src_text)}
"""
sections.append(("再現スクリプト全文", src_html))


# 描画完了 → render_lesson
html = render_lesson(
    num=84,
    title="L84 埋蔵文化財包蔵地 11 件統合分析",
    tags=["L84", "埋蔵文化財", "古墳", "城館跡", "官衙跡", "考古学",
          "11シリーズ統合", "keyword mining", "geopandas"],
    time="20-30 分",
    level="リテラシ + 統合構造分析",
    data_label="DoBoX 11 シリーズ (#1660-1670) 全件統合",
    sections=sections,
    script_filename="L84_archaeological_sites.py",
)
out_html = LESSONS / "L84_archaeological_sites.html"
out_html.write_text(html, encoding="utf-8")
print(f"  HTML written: {out_html}", flush=True)
print(f"  ({time.time()-t9:.2f}s)", flush=True)


# =============================================================================
# 14. 最終レポート
# =============================================================================
total_sec = time.time() - t_all
print(f"\n=== 完走 {total_sec:.2f}s ===", flush=True)
print(f"統合: {N_TOTAL:,} 件 / 緯度経度 {N_GEO:,} 件 / 古墳 {N_KOFUN:,} 件",
      flush=True)
print(f"H1 (中山間古墳率 ≥60%): {h1_ok} ({mountainous_pct:.1f}%)", flush=True)
print(f"H2 (円墳 ≥50%, 前方後円 <2%): {h2_ok} "
      f"(円墳 {h2_enpun_pct:.1f}%, 前方後円 {h2_zenpo_pct:.2f}%)", flush=True)
print(f"H3 (時代 top1 ≥80%): {h3_ok} ({h3_n_passed}/{h3_n_total} 群)",
      flush=True)
print(f"H4 (横穴式石室 ≥40%): {h4_ok} ({h4_pct:.1f}%)", flush=True)
print(f"H5 (沿岸<内陸 古墳率, 差 ≥10pt): {h5_ok} "
      f"(沿岸 {coast_kofun_pct:.1f}% vs 中山間 {inland_kofun_pct:.1f}%, "
      f"差 {h5_diff:+.1f}pt)", flush=True)
print(f"出力: {LESSONS}/L84_archaeological_sites.html", flush=True)
print(f"      {ASSETS}/L84_*.csv (13 種), L84_*.png (8 枚)", flush=True)
