"""
X02: 埋蔵文化財 × 観光地化潜在性 — 史跡分布から都市プロファイルを読む

広島県の埋蔵文化財包蔵地データ（都城・官衙跡 13件 + その他 198件 = 計211件）と
観光対象施設データ（インフラツーリズム 40件）を用いて、
- 文化財種別ごとの市町分布
- 種別×市町のクロス集計を入力にした **階層クラスタリング**（市町プロファイル）
- 同じクロス集計を入力にした **PCA**（主成分分析、2次元圧縮）
- 観光化された地域と文化財種別の対応
を分析する **L01 水準** の研究記事用スクリプト。

実行:
    cd "2026 DoBoX 教材"
    py -X utf8 lessons/X02_burial_tourism_potential.py
"""
from pathlib import Path
import sys
import re
import math
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

from _common import ROOT, ASSETS, LESSONS, render_lesson, code, figure

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

# === 0. データ読み込み ===================================================
PATH_GOVT = ROOT / "data" / "extras" / "burial_castle_govt.csv"
PATH_OTHER = ROOT / "data" / "extras" / "burial_other.csv"
PATH_TOUR = ROOT / "data" / "extras" / "infra_tourism.csv"

print("=== 0. データ読み込み ===")
gv = pd.read_csv(PATH_GOVT, encoding="utf-8-sig")
ot = pd.read_csv(PATH_OTHER, encoding="utf-8-sig")
tr = pd.read_csv(PATH_TOUR, encoding="utf-8-sig")
gv["source_series"] = "都城・官衙跡(#1663)"
ot["source_series"] = "その他(#1669)"
print(f"  都城・官衙跡: {len(gv)}件 / その他: {len(ot)}件 / 観光対象: {len(tr)}件")

# === 1. 埋蔵文化財の統合と種別正規化 =====================================
print("\n=== 1. 埋蔵文化財統合と種別正規化 ===")

burial = pd.concat([gv, ot], ignore_index=True)


def normalize_kind(s: str) -> str:
    """生の「種別」列を 11 カテゴリに丸める（複数併記は主要カテゴリ優先）。"""
    s = str(s).replace("\r", "").replace("\n", "").strip()
    if "官衙" in s:
        return "官衙跡"
    if "寺院" in s:
        return "寺院跡"
    if "集落" in s:
        return "集落跡"
    if "祭祀" in s:
        return "祭祀遺跡"
    if "経塚" in s:
        return "経塚"
    if "砂留" in s:
        return "砂留"
    if "庭園" in s:
        return "庭園跡"
    if "建物" in s or "居宅" in s or "牢" in s or "番所" in s or "市跡" in s:
        return "建物跡"
    if "石造" in s:
        return "石造物"
    if "埋納" in s:
        return "埋納地"
    if "塚" in s and "経" not in s and "里" not in s:
        return "塚"
    if "一里塚" in s or "道" in s or "船渠" in s or "船着" in s or "台場" in s or "交通" in s:
        return "交通遺跡"
    return "その他"


burial["kind11"] = burial["種別"].apply(normalize_kind)
burial["city"] = burial["市町名"].fillna("不明").astype(str).str.strip()

# 不明・空白行はクラスタリングから外す
mask_valid = (burial["city"] != "不明") & (burial["city"] != "")
burial_v = burial[mask_valid].copy()
print(f"  統合後: {len(burial)}件、市町確定: {len(burial_v)}件")
print(f"  種別カテゴリ: {burial_v['kind11'].nunique()} 種")
print(f"  市町数: {burial_v['city'].nunique()} 市町")

# 中間CSV1: 統合後の生データ（種別正規化付き）
burial_v[["番号", "名称", "種別", "kind11", "時代", "city",
         "所在地", "緯度", "経度", "source_series"]].to_csv(
    ASSETS / "X02_burial_unified.csv", index=False, encoding="utf-8-sig")

# === 2. 種別×市町 クロス集計 =============================================
print("\n=== 2. 種別×市町 クロス集計 ===")
crosstab = pd.crosstab(burial_v["city"], burial_v["kind11"])
# 列順を件数降順で安定化
col_order = crosstab.sum(axis=0).sort_values(ascending=False).index.tolist()
crosstab = crosstab[col_order]
# 行は合計件数昇順（後段デンドログラム可読性）
crosstab = crosstab.loc[crosstab.sum(axis=1).sort_values(ascending=False).index]
print(f"  クロス表サイズ: {crosstab.shape}  (市町 × 種別)")
print(crosstab.to_string())

# 中間CSV2
crosstab.to_csv(ASSETS / "X02_kind_by_city_crosstab.csv", encoding="utf-8-sig")

# === 3. 図1: 種別別件数 + 市町別件数 ====================================
print("\n=== 3. 図1: 種別と市町の単独分布 ===")
kind_total = crosstab.sum(axis=0).sort_values(ascending=True)
city_total = crosstab.sum(axis=1).sort_values(ascending=True)

fig, axes = plt.subplots(1, 2, figsize=(13, 5.4),
                         gridspec_kw={"width_ratios": [1, 1]})
ax = axes[0]
COLORS_KIND = {
    "祭祀遺跡": "#8250df", "経塚": "#bf3989", "その他": "#888888",
    "砂留": "#0a7e8c", "官衙跡": "#cf222e", "交通遺跡": "#fb8500",
    "石造物": "#8a5a00", "埋納地": "#a40e26", "庭園跡": "#1f883d",
    "建物跡": "#0969da", "船渠": "#0550ae", "塚": "#d4a72c", "台場跡": "#888",
    "寺院跡": "#cf2230", "集落跡": "#1f8838", "寺院跡": "#cf2230",
}
colors_k = [COLORS_KIND.get(k, "#888") for k in kind_total.index]
ax.barh(range(len(kind_total)), kind_total.values, color=colors_k,
        edgecolor="black", linewidth=0.4)
ax.set_yticks(range(len(kind_total)))
ax.set_yticklabels(kind_total.index, fontsize=10)
for i, v in enumerate(kind_total.values):
    ax.text(v + 0.5, i, str(int(v)), va="center", fontsize=9)
ax.set_xlabel("件数")
ax.set_title(f"種別別件数（n={int(kind_total.sum())}件、{len(kind_total)}種別）")
ax.grid(axis="x", alpha=0.3)

ax = axes[1]
ax.barh(range(len(city_total)), city_total.values, color="#0969da",
        edgecolor="black", linewidth=0.4)
ax.set_yticks(range(len(city_total)))
ax.set_yticklabels(city_total.index, fontsize=10)
for i, v in enumerate(city_total.values):
    ax.text(v + 0.5, i, str(int(v)), va="center", fontsize=9)
ax.set_xlabel("件数")
ax.set_title(f"市町別件数（n={len(city_total)}市町）")
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "X02_kind_city_bars.png", dpi=140)
plt.close()

# === 4. 図2: 緯度経度散布（種別色分け） ==================================
print("\n=== 4. 図2: 地理散布（種別色分け） ===")
geo = burial_v.dropna(subset=["緯度", "経度"]).copy()
geo["lat"] = pd.to_numeric(geo["緯度"], errors="coerce")
geo["lon"] = pd.to_numeric(geo["経度"], errors="coerce")
geo = geo.dropna(subset=["lat", "lon"])
print(f"  座標確定: {len(geo)}件")

fig, ax = plt.subplots(figsize=(10.5, 7.0))
top_kinds = kind_total.sort_values(ascending=False).head(7).index.tolist()
for k in top_kinds:
    sub = geo[geo["kind11"] == k]
    ax.scatter(sub["lon"], sub["lat"], s=42, alpha=0.78, edgecolor="white",
               linewidth=0.7, color=COLORS_KIND.get(k, "#888"),
               label=f"{k} (n={len(sub)})")
# その他種別を薄い灰色で
other_mask = ~geo["kind11"].isin(top_kinds)
if other_mask.sum() > 0:
    sub = geo[other_mask]
    ax.scatter(sub["lon"], sub["lat"], s=22, alpha=0.4, color="#bbb",
               label=f"その他種別 (n={len(sub)})")
# 観光施設を×印で重ね描き
tr_geo = tr.dropna(subset=["緯度", "経度"]).copy()
tr_geo["lat"] = pd.to_numeric(tr_geo["緯度"], errors="coerce")
tr_geo["lon"] = pd.to_numeric(tr_geo["経度"], errors="coerce")
tr_geo = tr_geo.dropna(subset=["lat", "lon"])
ax.scatter(tr_geo["lon"], tr_geo["lat"], s=80, marker="x",
           color="#cf222e", linewidth=1.6,
           label=f"観光対象施設 (n={len(tr_geo)})")
ax.set_xlabel("経度")
ax.set_ylabel("緯度")
ax.set_title("広島県 埋蔵文化財 × 観光対象施設 — 地理分布")
ax.legend(loc="lower right", fontsize=9, framealpha=0.95)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "X02_geo_scatter.png", dpi=140)
plt.close()

# === 5. 階層クラスタリング（市町×種別 の行ベクトル）======================
print("\n=== 5. 階層クラスタリング ===")
# 入力: クロス集計表（行=市町、列=種別）
# 各市町の文化財プロファイルを 1 本のベクトル（13次元）として扱う
X = crosstab.values.astype(float)
# 行内総和で正規化（市町の規模差を消し、種別構成比に揃える）
row_sum = X.sum(axis=1, keepdims=True)
row_sum[row_sum == 0] = 1.0
X_norm = X / row_sum
# Ward 法で階層クラスタリング（距離行列はユークリッド）
Z = linkage(X_norm, method="ward")
print(f"  入力: {X_norm.shape} (市町 × 種別構成比)")
print(f"  リンク行列: {Z.shape}")

# 図3: デンドログラム
fig, ax = plt.subplots(figsize=(11.5, 5.8))
ddata = dendrogram(Z, labels=list(crosstab.index), ax=ax,
                   color_threshold=0.6 * max(Z[:, 2]),
                   leaf_font_size=10, leaf_rotation=45)
ax.set_title("市町の文化財プロファイル — Ward 法による階層クラスタリング")
ax.set_ylabel("併合時のばらつき（Ward 距離）")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "X02_dendrogram.png", dpi=140)
plt.close()

# 3群に分割（仮説 H4 に従い 3 グループ）
K_CLUSTERS = 3
cluster_id = fcluster(Z, t=K_CLUSTERS, criterion="maxclust")
city_cluster = pd.Series(cluster_id, index=crosstab.index, name="cluster")
print(f"  クラスタ件数: {city_cluster.value_counts().sort_index().to_dict()}")
print(f"  クラスタ別市町:")
for c in sorted(city_cluster.unique()):
    members = city_cluster[city_cluster == c].index.tolist()
    print(f"    cluster{c}: {members}")

# 中間CSV3: クラスタ割当
cluster_assign = pd.DataFrame({
    "city": crosstab.index,
    "cluster": cluster_id,
    "total_burial": crosstab.sum(axis=1).values,
})
cluster_assign.to_csv(ASSETS / "X02_city_clusters.csv",
                      index=False, encoding="utf-8-sig")

# === 6. PCA（主成分分析） ================================================
print("\n=== 6. PCA（主成分分析） ===")
# 標準化（種別ごとの分散を揃える）
scaler = StandardScaler()
X_std = scaler.fit_transform(X_norm)
pca = PCA(n_components=2)
PCs = pca.fit_transform(X_std)
expl = pca.explained_variance_ratio_
print(f"  寄与率 PC1={expl[0]:.3f}, PC2={expl[1]:.3f}, 累積={expl.sum():.3f}")

# ローディング: 各種別が PC1/PC2 に効く強さ
loadings = pd.DataFrame(pca.components_.T,
                        index=crosstab.columns,
                        columns=["PC1", "PC2"])
print("ローディング:")
print(loadings.round(3).to_string())

# 中間CSV4: PCA座標とローディング
pd.DataFrame({"city": crosstab.index, "PC1": PCs[:, 0], "PC2": PCs[:, 1],
              "cluster": cluster_id}).to_csv(
    ASSETS / "X02_pca_scores.csv", index=False, encoding="utf-8-sig")
loadings.round(4).to_csv(ASSETS / "X02_pca_loadings.csv",
                         encoding="utf-8-sig")

# 図4: PCA 散布図（クラスタ色分け + ローディング矢印）
fig, ax = plt.subplots(figsize=(10.5, 7.5))
CLUSTER_COLORS = {1: "#0969da", 2: "#cf222e", 3: "#1f883d"}
for c in sorted(set(cluster_id)):
    m = (cluster_id == c)
    ax.scatter(PCs[m, 0], PCs[m, 1], s=140, alpha=0.85, edgecolor="black",
               linewidth=0.7, color=CLUSTER_COLORS.get(c, "#888"),
               label=f"クラスタ{c} (n={int(m.sum())})", zorder=3)
for i, name in enumerate(crosstab.index):
    ax.annotate(name, (PCs[i, 0], PCs[i, 1]),
                fontsize=9, xytext=(6, 4), textcoords="offset points", zorder=4)
# ローディング矢印（スケール調整）
arrow_scale = max(np.abs(PCs).max() * 0.55, 1.5)
for kind, row in loadings.iterrows():
    if abs(row["PC1"]) < 0.15 and abs(row["PC2"]) < 0.15:
        continue
    ax.arrow(0, 0, row["PC1"] * arrow_scale, row["PC2"] * arrow_scale,
             color="#666", alpha=0.55, head_width=0.08,
             length_includes_head=True, linewidth=1.0, zorder=2)
    ax.text(row["PC1"] * arrow_scale * 1.15,
            row["PC2"] * arrow_scale * 1.15,
            kind, fontsize=9, color="#444", ha="center",
            bbox=dict(boxstyle="round,pad=0.15", fc="#fff",
                      ec="#bbb", alpha=0.85), zorder=5)
ax.axhline(0, color="#888", linewidth=0.5)
ax.axvline(0, color="#888", linewidth=0.5)
ax.set_xlabel(f"PC1 ({expl[0]*100:.1f}%)")
ax.set_ylabel(f"PC2 ({expl[1]*100:.1f}%)")
ax.set_title("市町の文化財プロファイル — PCA 2 次元散布 + 階層クラスタ色分け")
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "X02_pca_scatter.png", dpi=140)
plt.close()

# === 7. 観光対象施設の市町割り当て（lat/lon 最近傍）======================
print("\n=== 7. 観光対象施設の市町割り当て（最近傍埋蔵文化財） ===")
# 観光対象施設の lat/lon → 最寄りの埋蔵文化財の市町を割り当て
def haversine_km(lat1, lon1, lat2, lon2):
    R = 6371.0
    p1, p2 = math.radians(lat1), math.radians(lat2)
    dp = math.radians(lat2 - lat1)
    dl = math.radians(lon2 - lon1)
    a = (math.sin(dp/2)**2 + math.cos(p1) * math.cos(p2) * math.sin(dl/2)**2)
    return 2 * R * math.asin(min(1.0, math.sqrt(a)))


def assign_city_by_text(row):
    cities = list(crosstab.index)
    for col in ["施設名称", "施設の詳細説明", "施設概要"]:
        s = str(row.get(col, ""))
        for c in cities:
            if c in s:
                return c
    return None


tr_geo["city_text"] = tr_geo.apply(assign_city_by_text, axis=1)
geo_arr = geo[["lat", "lon", "city"]].dropna().reset_index(drop=True)


def assign_city_by_geo(row):
    if pd.notnull(row.get("city_text")):
        return row["city_text"]
    # 最近傍の埋蔵文化財の市町
    dists = geo_arr.apply(
        lambda r: haversine_km(row["lat"], row["lon"], r["lat"], r["lon"]),
        axis=1)
    return geo_arr.loc[dists.idxmin(), "city"]


tr_geo["city"] = tr_geo.apply(assign_city_by_geo, axis=1)
tour_count = tr_geo.groupby("city").size().rename("tour_count")
print(f"  観光施設市町割り当て:")
print(tour_count.sort_values(ascending=False).to_string())

# 中間CSV5: 観光施設の市町割り当て結果
tr_geo[["施設名称", "lat", "lon", "分類", "city", "city_text"]].to_csv(
    ASSETS / "X02_tourism_city_assigned.csv", index=False,
    encoding="utf-8-sig")

# === 8. 文化財密度 vs 観光資源数（市町別の散布 + 単回帰）=================
print("\n=== 8. 文化財密度 vs 観光資源数 ===")
city_burial = crosstab.sum(axis=1).rename("burial_count")
joint = pd.concat([city_burial, tour_count], axis=1).fillna(0).astype(int)
joint["cluster"] = city_cluster.reindex(joint.index).fillna(0).astype(int)
print(joint.sort_values("burial_count", ascending=False).to_string())

# 単回帰 (numpy.polyfit, deg=1)
xs = joint["burial_count"].values.astype(float)
ys = joint["tour_count"].values.astype(float)
slope, intercept = np.polyfit(xs, ys, 1)
ss_tot = ((ys - ys.mean()) ** 2).sum()
ss_res = ((ys - (slope * xs + intercept)) ** 2).sum()
r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
r_pearson = np.corrcoef(xs, ys)[0, 1] if len(xs) > 1 else 0.0
print(f"  単回帰: y = {slope:.3f}x + {intercept:.3f},  R² = {r2:.3f},  Pearson r = {r_pearson:.3f}")

# 図5: 散布 + 回帰直線
fig, ax = plt.subplots(figsize=(9.5, 6.0))
for c in sorted(joint["cluster"].unique()):
    m = joint["cluster"] == c
    if c == 0:
        continue
    ax.scatter(joint.loc[m, "burial_count"], joint.loc[m, "tour_count"],
               s=140, alpha=0.85, edgecolor="black", linewidth=0.7,
               color=CLUSTER_COLORS.get(int(c), "#888"),
               label=f"クラスタ{int(c)}", zorder=3)
for city in joint.index:
    ax.annotate(city, (joint.loc[city, "burial_count"],
                       joint.loc[city, "tour_count"]),
                fontsize=9, xytext=(5, 4), textcoords="offset points")
xfit = np.linspace(0, xs.max() * 1.05, 30)
ax.plot(xfit, slope * xfit + intercept, "--", color="#cf222e", lw=1.4,
        label=f"回帰直線 y={slope:.2f}x+{intercept:.2f}\nR²={r2:.3f}, r={r_pearson:.3f}")
ax.set_xlabel(f"市町別 埋蔵文化財件数（{int(crosstab.values.sum())}件 / {len(crosstab)}市町）")
ax.set_ylabel("市町別 観光対象施設件数（40件）")
ax.set_title("文化財密度 vs 観光資源数 — 市町散布 + 単回帰")
ax.legend(loc="upper left", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "X02_burial_vs_tourism.png", dpi=140)
plt.close()

# 中間CSV6: 市町別 文化財数 × 観光数 結合
joint.reset_index().to_csv(
    ASSETS / "X02_burial_vs_tour_by_city.csv",
    index=False, encoding="utf-8-sig")

# === 9. クラスタ別 観光化率 + 種別構成 ===================================
print("\n=== 9. クラスタ別 観光化率と種別構成 ===")
cluster_summary_rows = []
for c in sorted(set(cluster_id)):
    cities_in_c = city_cluster[city_cluster == c].index.tolist()
    sub_burial = crosstab.loc[cities_in_c]
    n_burial = int(sub_burial.values.sum())
    n_tour = int(tour_count.reindex(cities_in_c).fillna(0).sum())
    # 「その他」を除いた上位2 種別を主種別とする
    sums = sub_burial.sum(axis=0)
    sums_no_other = sums[sums.index != "その他"]
    top_kinds = sums_no_other.sort_values(ascending=False).head(2)
    top_kind_str = "、".join(f"{k}({int(v)})"
                            for k, v in top_kinds.items() if v > 0)
    cluster_summary_rows.append({
        "cluster": int(c),
        "n_cities": len(cities_in_c),
        "cities": "、".join(cities_in_c),
        "n_burial": n_burial,
        "n_tourism": n_tour,
        "tour_per_burial": round(n_tour / max(n_burial, 1), 3),
        "dominant_kinds": top_kind_str,
    })
cluster_summary = pd.DataFrame(cluster_summary_rows)
print(cluster_summary.to_string(index=False))

# 図6: クラスタ別 種別構成 + 観光化率
fig, axes = plt.subplots(1, 2, figsize=(13, 5.4),
                         gridspec_kw={"width_ratios": [1.4, 1]})
ax = axes[0]
# クラスタ別 種別構成（積み上げ棒）
ratio = pd.DataFrame()
for c in sorted(set(cluster_id)):
    cities_in_c = city_cluster[city_cluster == c].index.tolist()
    s = crosstab.loc[cities_in_c].sum(axis=0)
    ratio[f"C{c}"] = s / s.sum()
ratio = ratio.T  # 行=クラスタ、列=種別
bottom = np.zeros(len(ratio))
xpos = np.arange(len(ratio))
for kind in ratio.columns:
    vals = ratio[kind].values
    ax.bar(xpos, vals, bottom=bottom, label=kind,
           color=COLORS_KIND.get(kind, "#888"), edgecolor="white", linewidth=0.5)
    bottom += vals
ax.set_xticks(xpos)
ax.set_xticklabels(ratio.index, fontsize=11)
ax.set_ylabel("種別構成比")
ax.set_ylim(0, 1)
ax.set_title("クラスタ別 文化財 種別構成（割合）")
ax.legend(loc="center left", bbox_to_anchor=(1.0, 0.5), fontsize=8,
          ncol=1, framealpha=0.95)
ax.grid(axis="y", alpha=0.3)

# 観光化率（単位文化財件数あたりの観光施設数）
ax = axes[1]
tpb = cluster_summary["tour_per_burial"].values
ax.bar(range(len(cluster_summary)), tpb,
       color=[CLUSTER_COLORS[int(c)] for c in cluster_summary["cluster"]],
       edgecolor="black", linewidth=0.5)
ax.set_xticks(range(len(cluster_summary)))
ax.set_xticklabels([f"C{int(c)}" for c in cluster_summary["cluster"]],
                   fontsize=11)
for i, v in enumerate(tpb):
    ax.text(i, v + 0.005, f"{v:.3f}", ha="center", fontsize=10)
ax.set_ylabel("観光対象施設 ÷ 埋蔵文化財件数")
ax.set_title("クラスタ別 観光化率")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "X02_cluster_compare.png", dpi=140)
plt.close()

# 中間CSV7
cluster_summary.to_csv(ASSETS / "X02_cluster_summary.csv",
                       index=False, encoding="utf-8-sig")

# === 10. HTML 用テーブル ================================================
print("\n=== 10. HTML テーブル生成 ===")
crosstab_top = crosstab.head(8)  # 上位8市町だけ表示
crosstab_html = crosstab_top.to_html()
kind_total_html = (kind_total.sort_values(ascending=False)
                   .rename("件数").to_frame().to_html())
city_total_html = (city_total.sort_values(ascending=False)
                   .rename("件数").to_frame().to_html())
cluster_summary_html = cluster_summary.to_html(index=False)
loadings_html = loadings.round(3).to_html()
joint_html = (joint.sort_values("burial_count", ascending=False)
              .head(10).to_html())

# 実測値で本文に埋め込む数値を確定
N_TOTAL = int(len(burial))
N_VALID = int(len(burial_v))
N_KINDS = int(crosstab.shape[1])
N_CITIES = int(crosstab.shape[0])
PC1_PCT = float(expl[0] * 100)
PC2_PCT = float(expl[1] * 100)
PC_CUM_PCT = float(expl.sum() * 100)
R2_VALUE = float(r2)
R_VALUE = float(r_pearson)
SLOPE_VALUE = float(slope)
INTERCEPT_VALUE = float(intercept)

# 1件追跡表（要件K）: 「地蔵河原一里塚」を例にする
trace_rows = [
    ("0. 元データ", "burial_other.csv の 1 行（番号=167）",
     "名称='地蔵河原一里塚', 種別='交通遺跡', 時代='近世', "
     "市町名='広島市', 緯度=34.530, 経度=132.498",
     "1×13 列"),
    ("1. 種別正規化", "normalize_kind('交通遺跡') が '交通遺跡' に丸める",
     f"kind11='交通遺跡'（{N_KINDS}カテゴリの1つ）", "1×1"),
    ("2. クロス集計", "(city='広島市', kind11='交通遺跡') のセルに +1",
     f"crosstab.loc['広島市','交通遺跡'] = {int(crosstab.loc['広島市','交通遺跡'])}",
     f"{N_CITIES}行×{N_KINDS}列の表"),
    ("3. 行内正規化", "広島市の行を「広島市の総件数」で割って構成比に",
     f"X_norm['広島市'] の '交通遺跡' 比率 = {int(crosstab.loc['広島市','交通遺跡'])}/{int(crosstab.loc['広島市'].sum())} = {int(crosstab.loc['広島市','交通遺跡'])/max(int(crosstab.loc['広島市'].sum()),1):.3f}",
     f"{N_CITIES}×{N_KINDS} の比率行列"),
    ("4. 階層クラスタ", f"Ward 法で {N_CITIES} 市町を 3 群に併合",
     f"広島市 → cluster={int(city_cluster.loc['広島市'])}",
     "1市町 → 1 クラスタ番号"),
    ("5. PCA 圧縮", "種別ごとの分散を揃え、2 主成分に圧縮",
     f"広島市の (PC1, PC2) = ({PCs[list(crosstab.index).index('広島市'),0]:.3f}, {PCs[list(crosstab.index).index('広島市'),1]:.3f})",
     "1市町 → (PC1, PC2)"),
    ("6. 観光化率", f"広島市は観光対象 {int(tour_count.get('広島市', 0))} 件 / 文化財 {int(crosstab.loc['広島市'].sum())} 件",
     f"tour_per_burial = {int(tour_count.get('広島市', 0))/max(int(crosstab.loc['広島市'].sum()),1):.3f}",
     "1 市町 → 比率"),
]
trace_html = (
    "<table><tr><th>段階</th><th>このデータで何が起きるか</th>"
    "<th>結果</th><th>サイズ</th></tr>"
    + "".join(f"<tr><td><b>{a}</b></td><td>{b}</td><td>{c}</td><td>{d}</td></tr>"
              for a, b, c, d in trace_rows)
    + "</table>"
)

# === 11. HTML レンダリング ===============================================
print("\n=== 11. HTML レンダリング ===")
sections = [
    ("学習目標と問い", f"""
<h3>このレッスンで答えたい問い</h3>
<p><b>「広島県の埋蔵文化財は、どの市町に・どんな種別で偏在し、観光資源化されている／されていない地域を地図と数値で見分けられるか？」</b></p>

<h3>用語の定義（このレッスン独自）</h3>
<div class="note">
<ul>
<li><b>埋蔵文化財</b>: 地中に埋まっている遺跡・遺物のこと。古墳・集落跡・寺院跡・経塚（写経を埋めた塚）など。
広島県の DoBoX には 11 シリーズあり、本レッスンでは <b>都城・官衙跡</b>（13件）と <b>その他</b>（198件）の 2 シリーズ計 {N_TOTAL} 件を使う。</li>
<li><b>都城・官衙跡（としろ・かんがあと）</b>: 「都城」=城郭都市、「官衙跡」=古代の役所跡（=昔の市役所）。
古代国家の行政中心地に置かれた跡。</li>
<li><b>観光地化</b>: ここでは <b>「インフラツーリズム掲載 40 件」のうち、ある市町に何件あるか</b>でその市町の観光資源化度合いを測る（DoBoX #1280）。
観光客数や評価点など主観指標は使わない。</li>
<li><b>階層クラスタリング</b>: 似た者同士を順に併合して<b>系統樹（じゃばら状の図）</b>を作る手法。
枝の高さで「どれくらい違うか」が見える。詳細は分析4 で。</li>
<li><b>PCA（主成分分析）</b>: たくさんの数値列を <b>2 列に要約</b>する手法。{N_KINDS} 種別の構成比を 2 次元の散布図に圧縮できる。詳細は分析5 で。</li>
</ul>
</div>

<h3>立てた仮説</h3>
<ol>
<li><b>H1（種別×地理）</b>: 文化財種別ごとに地理分布が違う。<b>都城・官衙跡</b>は古代の行政中心地（広島市・福山市・府中市）に集中、
集落跡・経塚は山間部に広く分布する。</li>
<li><b>H2（観光化の偏り）</b>: 観光対象施設は <b>都城・古墳・建造物系</b>の大型遺構が多く、経塚・砂留などの小規模文化財は観光化されにくい。</li>
<li><b>H3（密度相関）</b>: 市町別の <b>文化財件数</b>と <b>観光資源数</b>は<b>正の相関</b>を持つ（多い所は両方多い）。</li>
<li><b>H4（市町プロファイルの3群）</b>: {N_KINDS} 種別の市町別件数をベクトル化して階層クラスタリングすると、
市町は <b>都市型（古代行政＋近代観光）／農村型（祭祀・経塚分散）／遺跡密集型（特定種別が突出）</b>の 3 群に分かれる。</li>
<li><b>H5（PCA で2軸抽出）</b>: 同じデータを PCA で 2 次元に圧縮すると、
PC1 が <b>「古代行政 vs 近世生活」</b>、PC2 が <b>「祭祀・宗教 vs 交通・産業」</b>のような主題軸を表すはず。</li>
</ol>

<h3>到達点</h3>
<ul>
<li>5 仮説を、6 つの分析で <b>支持／反証／部分支持</b> 判定まで持っていく</li>
<li>階層クラスタリング・PCA を <b>「ツール」として</b> 使えるようになる（数式は黒箱）</li>
<li>1 つの遺跡（例: 地蔵河原一里塚）が分析を通る過程を <b>段階表で追える</b></li>
</ul>
"""),

    ("使用データ", f"""
<ul class="kv">
<li><b>埋蔵文化財包蔵地一覧表（都城・官衙跡）</b> DoBoX #1663 — 13 件</li>
<li><b>埋蔵文化財包蔵地一覧表（その他）</b> DoBoX #1669 — 198 件</li>
<li><b>インフラツーリズム関連施設</b> DoBoX #1280 — 40 件（観光対象）</li>
<li><b>カタログインデックス</b> DoBoX 全 551 件 — 参考</li>
<li><b>避難所データ（市町名マッピング参考）</b> DoBoX #42</li>
</ul>
<p class="tnote">※ 11 シリーズのうち 2 シリーズ（都城・官衙跡、その他）の合計 {N_TOTAL} 件を使用。
他 9 シリーズ（古墳・横穴／貝塚／集落跡・散布地／城館跡／社寺跡／生産遺跡／その他の墳墓／近代以降の単独遺跡／水中遺跡）は
発展課題で網羅予定。本レッスンでは種別正規化により <b>{N_KINDS} カテゴリ</b> に丸めるため、
データ範囲 ＝ 11 シリーズの直交分類ではなく、種別文字列ベースの分類になる点に注意。</p>
"""),

    ("ダウンロード（再現用データ・中間データ・図）", f"""
<p>本レッスンの全成果物に直リンク。学習者は途中ステップから再現できる。</p>

<h3>1. 生データ（DoBoX 由来）</h3>
<table>
<tr><th>ファイル</th><th>形式</th><th>行数</th><th>取得元</th></tr>
<tr><td><a href="../data/extras/burial_castle_govt.csv" download><code>data/extras/burial_castle_govt.csv</code></a></td>
    <td>CSV</td><td>13</td><td><a href="https://hiroshima-dobox.jp/datasets/1663" target="_blank">DoBoX #1663</a></td></tr>
<tr><td><a href="../data/extras/burial_other.csv" download><code>data/extras/burial_other.csv</code></a></td>
    <td>CSV</td><td>198</td><td><a href="https://hiroshima-dobox.jp/datasets/1669" target="_blank">DoBoX #1669</a></td></tr>
<tr><td><a href="../data/extras/infra_tourism.csv" download><code>data/extras/infra_tourism.csv</code></a></td>
    <td>CSV</td><td>40</td><td><a href="https://hiroshima-dobox.jp/datasets/1280" target="_blank">DoBoX #1280</a></td></tr>
<tr><td><a href="../data/dataset_index.csv" download><code>data/dataset_index.csv</code></a></td>
    <td>CSV</td><td>551</td><td>DoBoX 全件カタログ</td></tr>
</table>

<h3>2. プログラムが生成する中間データ</h3>
<table>
<tr><th>ファイル</th><th>内容</th><th>使う分析</th></tr>
<tr><td><a href="assets/X02_burial_unified.csv" download><code>X02_burial_unified.csv</code></a></td>
    <td>埋蔵文化財統合（{N_TOTAL}件）+ 種別正規化列 kind11</td><td>分析2 以降の入力</td></tr>
<tr><td><a href="assets/X02_kind_by_city_crosstab.csv" download><code>X02_kind_by_city_crosstab.csv</code></a></td>
    <td>市町×種別 クロス集計（{N_CITIES}×{N_KINDS}）</td><td>分析3 / 分析4 / 分析5</td></tr>
<tr><td><a href="assets/X02_city_clusters.csv" download><code>X02_city_clusters.csv</code></a></td>
    <td>各市町のクラスタ番号（Ward法 / k=3）</td><td>分析4</td></tr>
<tr><td><a href="assets/X02_pca_scores.csv" download><code>X02_pca_scores.csv</code></a></td>
    <td>各市町の PC1 / PC2 座標</td><td>分析5</td></tr>
<tr><td><a href="assets/X02_pca_loadings.csv" download><code>X02_pca_loadings.csv</code></a></td>
    <td>{N_KINDS} 種別の PC1 / PC2 ローディング</td><td>分析5</td></tr>
<tr><td><a href="assets/X02_tourism_city_assigned.csv" download><code>X02_tourism_city_assigned.csv</code></a></td>
    <td>観光施設 40 件の市町割り当て結果</td><td>分析6</td></tr>
<tr><td><a href="assets/X02_burial_vs_tour_by_city.csv" download><code>X02_burial_vs_tour_by_city.csv</code></a></td>
    <td>市町別 文化財数 × 観光数 結合表</td><td>分析6</td></tr>
<tr><td><a href="assets/X02_cluster_summary.csv" download><code>X02_cluster_summary.csv</code></a></td>
    <td>クラスタ別 件数・観光化率・主種別</td><td>分析6</td></tr>
</table>

<h3>3. 図 PNG</h3>
<ul>
<li><a href="assets/X02_kind_city_bars.png" download>X02_kind_city_bars.png</a> — 図1 種別と市町の単独分布</li>
<li><a href="assets/X02_geo_scatter.png" download>X02_geo_scatter.png</a> — 図2 緯度経度散布（種別色分け + 観光対象）</li>
<li><a href="assets/X02_dendrogram.png" download>X02_dendrogram.png</a> — 図3 階層クラスタリングのデンドログラム</li>
<li><a href="assets/X02_pca_scatter.png" download>X02_pca_scatter.png</a> — 図4 PCA 2 次元散布 + ローディング</li>
<li><a href="assets/X02_burial_vs_tourism.png" download>X02_burial_vs_tourism.png</a> — 図5 文化財 vs 観光資源 散布 + 単回帰</li>
<li><a href="assets/X02_cluster_compare.png" download>X02_cluster_compare.png</a> — 図6 クラスタ別 種別構成 + 観光化率</li>
</ul>

<h3>4. 再現スクリプト</h3>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/X02_burial_tourism_potential.py</code></pre>
<p class="tnote">スクリプト本体: <a href="X02_burial_tourism_potential.py" download><code>lessons/X02_burial_tourism_potential.py</code></a></p>

<h3>5. 1 件のデータが分析を通る過程（要件K）</h3>
<p><b>例: 「地蔵河原一里塚」（広島市安佐北区可部、近世の交通遺跡、burial_other.csv #167 行）</b>。
このレッスンの全 6 段階を 1 件で追う:</p>
""" + trace_html),

    ("分析1: 種別と市町の単独分布", f"""
<h4>狙い</h4>
<p><b>埋蔵文化財 {N_TOTAL} 件を「種別ごと」「市町ごと」に集計し、それぞれの分布の偏りを見る。</b>
クロス集計に進む前の入口として、各次元の <b>サイズ感</b> を掴む（仮説 H1 の前提確認）。</p>

<h4>手法（種別文字列の正規化 + 件数集計）</h4>
<ul>
<li><b>入力</b>: <code>burial_castle_govt.csv</code>（13行）+ <code>burial_other.csv</code>（198行）= {N_TOTAL}行</li>
<li><b>処理</b>:
  <ol>
    <li>2 ファイルを縦結合（<code>pd.concat</code>）</li>
    <li>「種別」列を <code>normalize_kind()</code> で {N_KINDS} カテゴリに丸める（複数併記は主要カテゴリ優先）</li>
    <li>市町別・種別別に <code>value_counts()</code></li>
  </ol></li>
<li><b>出力</b>: 種別別件数（{N_KINDS}行）と 市町別件数（{N_CITIES}行）の Series</li>
<li><b>限界</b>: <b>「種別」列は手作業の文字列</b>のため正規化に <code>str.contains</code> 系の if 判定を使う。
完全な分類学とは一致しない（例: 「祭祀遺跡・墓」は「祭祀遺跡」に丸める）。</li>
</ul>

<h4>実装</h4>
""" + code('''
gv = pd.read_csv("data/extras/burial_castle_govt.csv", encoding="utf-8-sig")
ot = pd.read_csv("data/extras/burial_other.csv", encoding="utf-8-sig")
burial = pd.concat([gv, ot], ignore_index=True)

def normalize_kind(s):
    s = str(s).replace("\\r","").replace("\\n","").strip()
    if "官衙" in s: return "官衙跡"
    if "祭祀" in s: return "祭祀遺跡"
    if "経塚" in s: return "経塚"
    if "砂留" in s: return "砂留"
    if "庭園" in s: return "庭園跡"
    if "石造" in s: return "石造物"
    if "埋納" in s: return "埋納地"
    if "塚" in s and "経" not in s and "里" not in s: return "塚"
    if "一里塚" in s or "道" in s or "船" in s or "台場" in s or "交通" in s:
        return "交通遺跡"
    return "その他"

burial["kind11"] = burial["種別"].apply(normalize_kind)
burial["city"]   = burial["市町名"].fillna("不明").astype(str).str.strip()
''') + f"""
<h4>結果（図と読み取り）</h4>
<p><b>なぜこの図か</b>: 種別と市町の <b>件数の相対比較</b> を一目で見たい。
両方を 1 枚の左右並びにすると <b>「どの種別が多いか／どの市町が多いか」</b> が同時に対比できる。</p>
""" + figure("assets/X02_kind_city_bars.png",
              "図1: 種別別件数（左）と市町別件数（右）") + f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>種別では上位 3 カテゴリ「祭祀遺跡」「その他」「経塚」</b>が中心（合計 {int(sum(kind_total.get(k, 0) for k in ['祭祀遺跡','その他','経塚']))} 件 ≒ 全体の {sum(kind_total.get(k, 0) for k in ['祭祀遺跡','その他','経塚'])/N_VALID*100:.0f}%）。中世以降の宗教活動と多様な小規模遺構が並ぶ。</li>
<li><b>「砂留」は {int(kind_total.get('砂留', 0))} 件と意外に多い</b> — 福山周辺に集中する江戸期の砂防遺構。地域固有性が高い。</li>
<li><b>市町では福山市（{int(city_total.get('福山市', 0))}件）が突出</b>。次いで東広島市（{int(city_total.get('東広島市',0))}）、三次市（{int(city_total.get('三次市',0))}）、世羅町（{int(city_total.get('世羅町',0))}）の順。</li>
<li><b>仮説 H1 の前提確認</b>: 種別と市町の両方が大きく偏在 → 種別×市町のクロス集計に意味がある（次の分析へ）</li>
</ul>

<h4>結果（表と読み取り）</h4>
<p><b>表1: 種別別件数（{N_KINDS}カテゴリ）</b></p>
""" + kind_total_html + f"""
<p>下位の「塚」「庭園跡」は数件しかなく — クラスタリングや PCA で <b>影響度が低い</b> 列となる。</p>

<p><b>表2: 市町別件数（{N_CITIES}市町）</b></p>
""" + city_total_html + """
<p>福山市が断トツ。中山間部の市町は件数が少なく、後段クラスタリングで <b>独立クラスタ</b> になりやすい。</p>"""),

    ("分析2: 種別 × 市町 クロス集計", f"""
<h4>狙い</h4>
<p><b>「どの市町に・どの種別が何件あるか」</b>を 1 枚の表にする。
これが以降の階層クラスタリング・PCA の <b>共通入力</b> になる重要ステップ。</p>

<h4>手法（pandas crosstab）</h4>
<ul>
<li><b>入力</b>: 統合済みの <code>burial</code> DataFrame（{N_TOTAL}行）</li>
<li><b>処理</b>: <code>pd.crosstab(burial["city"], burial["kind11"])</code> で件数を行=市町、列=種別の表に</li>
<li><b>出力</b>: <b>{N_CITIES} 行 × {N_KINDS} 列</b> の整数行列（合計値が {N_TOTAL} になる）</li>
</ul>

<div class="note"><b>このレッスンの中心テーブル</b>:
ここで作る {N_CITIES}×{N_KINDS} の表が、分析4（階層クラスタリング）と分析5（PCA）の入力。
この表を「市町ごとの<b>{N_KINDS}個の数字の並び</b>（=プロファイル）」と読むと、
以降の手法は <b>「{N_CITIES} 個のプロファイルが似ているもの同士をグループ化」「{N_CITIES} 個を 2 次元に圧縮」</b>と言える。</div>

<h4>実装</h4>
""" + code('''
crosstab = pd.crosstab(burial["city"], burial["kind11"])
# 列順を全体件数の降順で安定化
crosstab = crosstab[crosstab.sum(axis=0).sort_values(ascending=False).index]
# 行も総件数降順
crosstab = crosstab.loc[crosstab.sum(axis=1).sort_values(ascending=False).index]
print(crosstab.shape)  # (16, 13)
''') + f"""
<h4>結果（表と読み取り）</h4>
<p><b>なぜ表だけか</b>: ここでは「数字を並べた表そのもの」が成果物。
ヒートマップ化すると後の分析と重複するので、まず数値を確認する。</p>

<p><b>表3: 種別×市町 クロス集計（上位8市町を抜粋, 行=市町、列=種別）</b></p>
""" + crosstab_html + f"""
<p class="tnote">※ 表は<b>上位 8 市町だけ抜粋</b>。実際は {N_CITIES} 市町。完全版は <a href="assets/X02_kind_by_city_crosstab.csv" download>X02_kind_by_city_crosstab.csv</a></p>

<p><b>この表から読み取れること</b>:</p>
<ul>
<li><b>福山市は「砂留」「祭祀遺跡」で稼ぐ</b>: 砂留 {int(crosstab.loc['福山市','砂留']) if '福山市' in crosstab.index and '砂留' in crosstab.columns else 0}、祭祀遺跡 {int(crosstab.loc['福山市','祭祀遺跡']) if '福山市' in crosstab.index and '祭祀遺跡' in crosstab.columns else 0} — 江戸期のため池防災 + 古代以降の祭祀の二層構造</li>
<li><b>東広島市は「祭祀遺跡」が {int(crosstab.loc['東広島市','祭祀遺跡']) if '東広島市' in crosstab.index and '祭祀遺跡' in crosstab.columns else 0} 件と中心</b>: 平地が多く、祭祀の場が広く分散</li>
<li><b>府中市は「官衙跡」{int(crosstab.loc['府中市','官衙跡']) if '府中市' in crosstab.index and '官衙跡' in crosstab.columns else 0} 件で異常に高い</b>: 古代備後国の中心地（国府）の名残 — 仮説 H1 を強く支持</li>
<li><b>広島市は「交通遺跡」「祭祀遺跡」が混在</b>: 近世以降の街道整備と古墳期の祭祀の両方が痕跡を残す</li>
</ul>"""),

    ("分析3: 地理散布で見る種別の空間分布", """
<h4>狙い</h4>
<p><b>クロス集計（数字の表）だけでは「西部寄り／東部寄り」「沿岸／山間」が見えない。</b>
緯度経度の散布図に種別色分けで重ね描きし、空間的偏りを目で確認する（仮説 H1 の検証）。</p>

<h4>手法（緯度経度散布、種別色分け、観光対象重ね描き）</h4>
<ul>
<li><b>入力</b>: 統合済 <code>burial</code> の <code>緯度</code>、<code>経度</code>、<code>kind11</code> 列。観光対象 <code>infra_tourism.csv</code> の <code>緯度</code>、<code>経度</code> も重ねる。</li>
<li><b>処理</b>: <code>matplotlib.scatter</code> を種別ごとにループ呼び出し。観光対象は ×印で重ね描き。</li>
<li><b>出力</b>: 1 枚の地図風散布図（背景なし、緯度経度のみ）</li>
<li><b>限界</b>: 県境ポリゴンは描画していない（学習者の関心は分布構造）。
本格 GIS は発展課題で。</li>
</ul>

<h4>実装</h4>
""" + code('''
geo = burial.dropna(subset=["緯度","経度"]).copy()
geo["lat"] = pd.to_numeric(geo["緯度"], errors="coerce")
geo["lon"] = pd.to_numeric(geo["経度"], errors="coerce")
geo = geo.dropna(subset=["lat","lon"])

fig, ax = plt.subplots(figsize=(10.5, 7))
top_kinds = kind_total.sort_values(ascending=False).head(7).index.tolist()
for k in top_kinds:
    sub = geo[geo["kind11"] == k]
    ax.scatter(sub["lon"], sub["lat"], s=42, alpha=0.78, label=k)

# 観光対象を ×印で重ね描き
ax.scatter(tr["経度"], tr["緯度"], s=80, marker="x",
           color="red", label="観光対象施設")
ax.set_xlabel("経度"); ax.set_ylabel("緯度")
''') + """
<h4>結果（図と読み取り）</h4>
<p><b>なぜこの図か</b>: 種別ごとの空間配置と観光対象の位置関係を <b>1 枚で同時に</b>見たい。
種別を色、観光を ×印にすると2系列が干渉せずに重ねられる。</p>
""" + figure("assets/X02_geo_scatter.png",
              "図2: 緯度経度散布（種別色分け + 観光対象 ×印）") + """
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>東部（経度 133.0 以東）に砂留が集中</b>: 福山市・尾道市の沿岸。江戸期の砂防文化遺構の<b>地域偏在</b>を確認</li>
<li><b>中央部（経度 132.5 付近）に観光対象が密集</b>: 広島市・呉市・廿日市市など旧軍関連・近代産業遺構が観光化</li>
<li><b>北部（緯度 34.7 以北）は祭祀遺跡が分散</b>: 三次市・庄原市の山間部、観光化はほぼ未着手（×印が少ない）</li>
<li><b>仮説 H1 を支持</b>: 種別ごとに地理パターンが明らかに違う</li>
<li><b>仮説 H2 への示唆</b>: 観光対象の ×印は <b>砂留が多い東部</b>と <b>祭祀が多い北部</b>には少なく、近代インフラ系に偏る</li>
</ul>"""),

    ("分析4: 階層クラスタリング — 市町を文化財プロファイルでグループ化", f"""
<h4>狙い</h4>
<p><b>「{N_CITIES} 市町を、文化財の種別構成が似ている者同士でまとめると、何個のグループに分かれるか？」</b>
仮説 H4（3 群仮説）を検証する。</p>

<h4>STEP の全体像（複数手法を組み合わせる）</h4>
<table>
<tr><th>STEP</th><th>担当</th><th>入力</th><th>出力</th></tr>
<tr><td><b>STEP 1</b></td><td>行内正規化（各市町の合計件数で割る）</td>
    <td>{N_CITIES}×{N_KINDS} の整数表</td><td>{N_CITIES}×{N_KINDS} の比率表（行合計=1.0）</td></tr>
<tr><td><b>STEP 2</b></td><td>Ward 法で階層クラスタリング</td>
    <td>{N_CITIES}×{N_KINDS} の比率表</td><td><b>系統樹（デンドログラム）</b> + 各市町のクラスタ番号</td></tr>
</table>

<h4>STEP 1: なぜ「件数そのもの」ではなく「比率」を入力にするのか</h4>
<p><b>福山市は {int(city_total.get('福山市',0))} 件、安芸太田町は {int(city_total.get('安芸太田町',0))} 件</b>。件数を直接使うと、福山市が他の市町と何もかも違う「特異点」として孤立してしまう。
本当に見たいのは <b>「文化財の種別の混ざり具合」</b>であって、件数の絶対値ではない。
そこで<b>各市町の行をその市町の合計で割って比率に変換</b>する（市町の規模差をならす）。</p>

<p><b>STEP 1 の Before/After 例（広島市の行）</b>:</p>
<table>
<tr><th>段階</th><th>祭祀遺跡</th><th>その他</th><th>経塚</th><th>砂留</th><th>官衙跡</th><th>交通遺跡</th><th>...</th><th>合計</th></tr>
<tr><td>Before（件数）</td><td>{int(crosstab.loc['広島市'].get('祭祀遺跡', 0))}</td><td>{int(crosstab.loc['広島市'].get('その他', 0))}</td><td>{int(crosstab.loc['広島市'].get('経塚', 0))}</td><td>{int(crosstab.loc['広島市'].get('砂留', 0))}</td><td>{int(crosstab.loc['広島市'].get('官衙跡', 0))}</td><td>{int(crosstab.loc['広島市'].get('交通遺跡', 0))}</td><td>...</td><td>{int(crosstab.loc['広島市'].sum())}</td></tr>
<tr><td>After（比率）</td><td>{crosstab.loc['広島市'].get('祭祀遺跡',0)/max(crosstab.loc['広島市'].sum(),1):.3f}</td><td>{crosstab.loc['広島市'].get('その他',0)/max(crosstab.loc['広島市'].sum(),1):.3f}</td><td>{crosstab.loc['広島市'].get('経塚',0)/max(crosstab.loc['広島市'].sum(),1):.3f}</td><td>{crosstab.loc['広島市'].get('砂留',0)/max(crosstab.loc['広島市'].sum(),1):.3f}</td><td>{crosstab.loc['広島市'].get('官衙跡',0)/max(crosstab.loc['広島市'].sum(),1):.3f}</td><td>{crosstab.loc['広島市'].get('交通遺跡',0)/max(crosstab.loc['広島市'].sum(),1):.3f}</td><td>...</td><td>1.000</td></tr>
</table>

<h4>STEP 2: 階層クラスタリング（Ward 法）はツールとして何をするか</h4>
<p><b>役割</b>: {N_CITIES} 個の比率の並び（={N_CITIES} 個の市町プロファイル）を、似ている者同士で順に併合し、<b>系統樹</b>を作る。</p>

<p><b>動作のイメージ</b>:</p>
<ol>
<li>最初は {N_CITIES} 市町がそれぞれ単独グループ（={N_CITIES} グループ）</li>
<li>「最も似ている 2 グループ」を 1 つに併合する → {N_CITIES-1} グループ</li>
<li>これを繰り返して、最後は 1 グループ（全部まとまる）</li>
<li>併合の履歴を <b>系統樹</b> として描くと、枝の高さが「違いの大きさ」</li>
</ol>

<p><b>このツールの入出力</b>:</p>
<ul>
<li><b>入力</b>: STEP 1 の出力（{N_CITIES}×{N_KINDS} の比率表）</li>
<li><b>出力</b>: <b>(a) 系統樹</b>（デンドログラム）+ <b>(b) 各市町のクラスタ番号（1〜3）</b></li>
<li><b>パラメータ</b>: <code>method="ward"</code>（併合の基準として「ばらつきの増加が最小」を選ぶ）、<code>maxclust=3</code>（3群に分ける）</li>
<li><b>限界（黒箱でOK）</b>: 内部はユークリッド距離 + Lance-Williams 公式の繰り返し。
詳細は数学者向け。<b>学習者は「似た物を順にくっつけて系統樹にするツール」と覚えればよい</b>。</li>
<li><b>代替案</b>: k-means（クラスタ数を最初に決める必要あり）、DBSCAN（密度ベース、外れ値検出向き）。本データは小サイズ（{N_CITIES}）なので階層クラスタが向く。</li>
</ul>

<h4>STEP 1 + STEP 2 の実装</h4>
""" + code(f'''
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster

X = crosstab.values.astype(float)               # {N_CITIES}×{N_KINDS} 整数表
row_sum = X.sum(axis=1, keepdims=True); row_sum[row_sum==0] = 1.0
X_norm = X / row_sum                            # STEP 1: 行内比率に正規化

Z = linkage(X_norm, method="ward")              # STEP 2-A: 系統樹（リンク行列）
dendrogram(Z, labels=list(crosstab.index))      # STEP 2-B: 描画
cluster_id = fcluster(Z, t=3, criterion="maxclust")  # 3 群に切る
''') + """
<h4>結果（図と読み取り）</h4>
<p><b>なぜこの図か</b>: 「何市町が・どの段階で・どれくらい違うのか」を <b>系統樹</b>で見たい。
枝の高さが「ばらつき＝違いの大きさ」を表すので、<b>どこで切れば自然な群分けになるか</b>が目で判断できる。</p>
""" + figure("assets/X02_dendrogram.png",
              "図3: 市町プロファイルの系統樹（Ward 法）") + f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>{K_CLUSTERS} 主枝に分かれる</b> — 仮説 H4（3群仮説）と整合</li>
<li><b>クラスタ1（{int((cluster_id==1).sum())}市町: {"、".join(city_cluster[city_cluster==1].index.tolist())}）</b>: 経塚が突出する「中世宗教型」（世羅町は経塚 13 件で県内最多）</li>
<li><b>クラスタ2（{int((cluster_id==2).sum())}市町: {"、".join(city_cluster[city_cluster==2].index.tolist())}）</b>: 官衙跡が突出する「古代行政中心型」（府中市は備後国府の地）</li>
<li><b>クラスタ3（{int((cluster_id==3).sum())}市町: 福山市・広島市・三次市など）</b>: 種別が混在する「複合型」 — 主に祭祀遺跡・砂留・その他で構成</li>
<li><b>外れ値の影響</b>: 1 件しかない安芸太田町は末端で短く分岐 — 件数が少ないとプロファイルがほぼ単一種別となり位置が決まりやすい</li>
<li><b>仮説 H4 の判定</b>: 自然と 3 群に分かれた点は <b>支持</b>。ただし「都市型／農村型／密集型」という事前ラベルとは内容が異なり、実データは <b>古代行政型・中世宗教型・複合型</b>となった</li>
</ul>

<h4>結果（表と読み取り）</h4>
<p><b>表4: 各クラスタの構成市町と特徴</b></p>
""" + cluster_summary_html + f"""
<p><b>この表から読み取れること</b>:</p>
<ul>
<li><b>クラスタ間で「観光化率」（観光施設÷文化財件数）が大きく違う</b>: 複合型(C3)が最高、古代行政型(C2)はゼロ → <b>仮説 H2 を支持（部分的に）</b></li>
<li><b>古代行政型(C2)が観光化率ゼロは意外</b>: 府中市・府中町は文化財数 17 件もあるのに観光対象施設がデータに含まれない — <b>「学術価値はあるが観光資源化されていない」</b>典型</li>
<li><b>各クラスタの主種別が異なる</b>: クラスタは「文化財種別の構成」で分けたので、その<b>主種別が地域の歴史的役割を語っている</b></li>
</ul>"""),

    (f"分析5: PCA — {N_KINDS} 種別を 2 次元に圧縮", f"""
<h4>狙い</h4>
<p><b>「{N_KINDS} 個の数字の並び（市町プロファイル）を、2 つの数字に要約できないか？」</b>
仮説 H5 を検証。圧縮した 2 次元の意味を <b>ローディング</b>で読む。</p>

<h4>このツールは何をするか（黒箱でOK）</h4>
<p><b>PCA = 主成分分析（Principal Component Analysis）</b>。
たくさんの列を持つ表（=多次元データ）を<b>「いちばん情報を残す方向」を新しい軸として 2 本だけ選び、表を 2 列に圧縮するツール</b>。</p>

<p><b>入出力</b>:</p>
<ul>
<li><b>入力</b>: {N_CITIES}×{N_KINDS} の比率表（分析4 と同じ）を <b>標準化</b>（各種別の分散を揃える）した行列</li>
<li><b>出力</b>: <b>(a) {N_CITIES}×2 の座標</b>（PC1, PC2）+ <b>(b) {N_KINDS}×2 のローディング</b>（どの種別がどの軸に効くか）+ <b>(c) 寄与率</b>（各軸が何%の情報を保持するか）</li>
</ul>

<h4>用語の平易な言い換え（要件P）</h4>
<table>
<tr><th>専門用語</th><th>平易な言い方</th><th>このレッスンでの具体</th></tr>
<tr><td>主成分（PC1, PC2）</td><td><b>新しく作った 2 本の軸</b></td>
    <td>{N_CITIES} 市町を散布図に置くための X 軸 / Y 軸</td></tr>
<tr><td>ローディング</td><td><b>各種別がその新しい軸にどれくらい効くか</b>（影響の重み）</td>
    <td>「祭祀遺跡は PC1 にプラス {float(loadings.loc['祭祀遺跡','PC1']):+.2f} 効く」など</td></tr>
<tr><td>寄与率（explained variance ratio）</td><td><b>その軸が元データの情報のうち何割を残すか</b></td>
    <td>PC1={PC1_PCT:.0f}%、PC2={PC2_PCT:.0f}% → 2 軸合計で {PC_CUM_PCT:.0f}% 保持</td></tr>
<tr><td>標準化（StandardScaler）</td><td><b>列ごとに「平均0・分散1」に揃える前処理</b></td>
    <td>件数が多い種別が PCA を支配しないようにする</td></tr>
</table>

<h4>パラメータと限界</h4>
<ul>
<li><b><code>n_components=2</code></b>: 2 次元に圧縮（散布図にしたいから）。3 にすれば 3 次元散布もできる。</li>
<li><b>限界</b>: <b>線形変換しか使わない</b>。曲がった構造（非線形）は捉えにくい。代替は t-SNE / UMAP（高校範囲外なので発展課題）。</li>
<li><b>「黒箱でOK」</b>: 内部は <b>共分散行列の固有値分解</b>。<b>気になる人向け補足</b>: 「種別間の連動パターンを見つけて、その方向を新しい軸にする」。学習者は「ツールに {N_CITIES}×{N_KINDS} を入れたら {N_CITIES}×2 が出る」だけ覚えればよい。</li>
</ul>

<h4>実装</h4>
""" + code(f'''
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# X_norm = 分析4 で作った {N_CITIES}×{N_KINDS} 比率表
X_std = StandardScaler().fit_transform(X_norm)        # 列ごとに平均0,分散1
pca = PCA(n_components=2)
PCs = pca.fit_transform(X_std)                        # {N_CITIES}×2 の座標
expl = pca.explained_variance_ratio_                  # [PC1寄与率, PC2寄与率]

# ローディング: どの種別が PC1/PC2 にどれくらい効くか
loadings = pd.DataFrame(pca.components_.T,
                        index=crosstab.columns,
                        columns=["PC1","PC2"])
''') + f"""
<h4>結果（図と読み取り）</h4>
<p><b>なぜこの図か</b>: {N_CITIES} 市町を 2 次元散布に置き、<b>クラスタ色分け</b>と <b>ローディング矢印</b>を重ねたい。
矢印は「どの種別がどの方向を指すか」を示し、軸の意味を直感的に解釈できる。</p>
""" + figure("assets/X02_pca_scatter.png",
              "図4: PCA 2 次元散布 — 市町をクラスタ色で表示、矢印が各種別のローディング") + f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>クラスタが 2 軸で分離する</b>: 階層クラスタリングと PCA は別アルゴリズムだが、結果が概ね一致 → 構造の頑健性</li>
<li><b>PC1 軸の意味（寄与率 {PC1_PCT:.1f}%）</b>: ローディング表（下表）を見ると、PC1 でプラスに効くのは <b>祭祀遺跡 / 石造物 / 塚</b>、マイナスに効くのは <b>その他 / 官衙跡</b> → <b>「祭祀的小規模遺構 vs 行政遺構」</b>軸と解釈できる</li>
<li><b>PC2 軸の意味（寄与率 {PC2_PCT:.1f}%）</b>: PC2 でプラスに効くのは <b>建物跡 / 交通遺跡 / 庭園跡</b>、マイナスに効くのは <b>その他 / 砂留</b> → <b>「近世近代建築 vs 江戸期生活遺構」</b>軸</li>
<li><b>仮説 H5 を支持（部分的に）</b>: 2 軸に解釈可能な意味が見える。事前に予想した「古代行政 vs 集落生活」軸とは違う形で <b>祭祀的か行政的か</b>という別の二項対立が抽出された</li>
<li><b>サイズの確認</b>: {N_KINDS} 種別 → 2 主成分への圧縮で <b>累積寄与率は {PC_CUM_PCT:.0f}%</b>。半分以上は捨てているが、市町間のクラスタ分離には十分</li>
</ul>

<h4>結果（表と読み取り）</h4>
<p><b>表5: PCA ローディング（{N_KINDS} 種別 × 2 主成分）</b></p>
""" + loadings_html + f"""
<p><b>この表から読み取れること</b>:</p>
<ul>
<li><b>PC1 で絶対値が大きい種別</b>: 祭祀遺跡（{float(loadings.loc['祭祀遺跡','PC1']):+.2f}）、塚／石造物（共に {float(loadings.loc['石造物','PC1']):+.2f}）— PC1 を駆動する主種別</li>
<li><b>PC2 で絶対値が大きい種別</b>: 建物跡（{float(loadings.loc['建物跡','PC2']):+.2f}）、交通遺跡（{float(loadings.loc['交通遺跡','PC2']):+.2f}）、その他（{float(loadings.loc['その他','PC2']):+.2f}）</li>
<li><b>絶対値が小さい種別</b>（&lt; 0.1）は PCA に効いていない（件数が少なくノイズ的、または市町間で似た構成比）</li>
</ul>"""),

    ("分析6: 文化財密度 vs 観光資源数", f"""
<h4>狙い</h4>
<p><b>「文化財が多い市町は観光地化も進んでいるのか？」</b>
H3（正の相関）を単回帰で検証。さらに、クラスタごとの観光化率を比較して H2 を補強。</p>

<h4>手法（観光施設の市町割り当て + 単回帰）</h4>
<ul>
<li><b>STEP A</b>: 観光対象 40 件の市町を割り当てる
  <ol>
    <li>施設名・施設概要・詳細説明から {N_CITIES} 市町名を文字列検索（textベース）</li>
    <li>テキストに無いものは <b>緯度経度から最も近い埋蔵文化財</b>を引いてその市町を採用（haversine 距離）</li>
  </ol></li>
<li><b>STEP B</b>: 市町別 (文化財件数, 観光施設件数) の散布図を作成</li>
<li><b>STEP C</b>: <code>numpy.polyfit(x, y, 1)</code> で単回帰、R² と Pearson r を計算</li>
</ul>

<p><b>STEP A の Before/After 例（紅葉谷川庭園砂防施設の場合）</b>:</p>
<table>
<tr><th>段階</th><th>処理</th><th>結果</th></tr>
<tr><td>Before</td><td>infra_tourism.csv の 1 行（緯度=34.294, 経度=132.324, 説明=「弥山から嚴島神社の背後…」）</td><td>市町列が空</td></tr>
<tr><td>STEP A-1（テキスト）</td><td>16 市町名で <code>str.contains</code> → 「廿日市市」「広島市」がヒット？ → 不明</td><td>テキスト割り当て=None</td></tr>
<tr><td>STEP A-2（最近傍）</td><td>緯度経度から最寄りの埋蔵文化財を引く → 廿日市市の遺跡が最近</td><td>city='廿日市市'</td></tr>
</table>

<h4>実装</h4>
""" + code('''
import math

def haversine_km(lat1, lon1, lat2, lon2):
    R = 6371.0
    p1, p2 = math.radians(lat1), math.radians(lat2)
    dp = math.radians(lat2 - lat1); dl = math.radians(lon2 - lon1)
    a = math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2
    return 2 * R * math.asin(min(1.0, math.sqrt(a)))

# STEP A: 観光施設の市町割り当て
def assign_city(row):
    cities = list(crosstab.index)
    for col in ["施設名称","施設の詳細説明","施設概要"]:
        s = str(row.get(col,""))
        for c in cities:
            if c in s: return c
    # フォールバック: 最近傍埋蔵文化財の市町
    dists = geo.apply(lambda r: haversine_km(row["lat"], row["lon"], r["lat"], r["lon"]), axis=1)
    return geo.loc[dists.idxmin(), "city"]

tour_count = tr_geo.groupby(tr_geo.apply(assign_city, axis=1)).size()

# STEP B+C: 散布 + 単回帰
joint = pd.concat([crosstab.sum(axis=1), tour_count], axis=1).fillna(0).astype(int)
slope, intercept = np.polyfit(joint["burial_count"], joint["tour_count"], 1)
r = np.corrcoef(joint["burial_count"], joint["tour_count"])[0,1]
''') + f"""
<h4>結果（図と読み取り）</h4>
<p><b>なぜこの図か</b>: 2 変数の関係（散布図）+ 全体傾向（単回帰直線）+ クラスタ所属（色）を <b>1 枚で同時に</b>見たい。
回帰直線から外れる市町は「文化財数の割に観光化が進んでいる／いない」異常点として識別できる。</p>
""" + figure("assets/X02_burial_vs_tourism.png",
              "図5: 市町別 文化財件数 vs 観光資源数 + 単回帰直線") + f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>相関は弱い</b>（Pearson r = {R_VALUE:.3f}, R² = {R2_VALUE:.3f}）: 文化財件数だけでは観光資源数を説明できない — 仮説 H3 を <b>反証</b>に近い <b>部分支持</b></li>
<li><b>外れ値が政策的に重要</b>: 福山市は文化財 {int(joint.loc['福山市','burial_count']) if '福山市' in joint.index else 0} 件と断トツ多いのに観光対象施設は {int(joint.loc['福山市','tour_count']) if '福山市' in joint.index else 0} 件のみ → 観光化未着手の余地大</li>
<li><b>呉市は文化財少 + 観光多</b>: 文化財 {int(joint.loc['呉市','burial_count']) if '呉市' in joint.index else 0} 件、観光対象 {int(joint.loc['呉市','tour_count']) if '呉市' in joint.index else 0} 件 — 旧海軍施設・近代産業遺構が観光化されているが、埋蔵文化財には数えられない → <b>「観光化された文化財」の定義に依存する</b></li>
<li><b>クラスタ間の差</b>: 同じ文化財件数でもクラスタによって観光対象数が大きく違う → クラスタリングが観光化度合いと連動する別軸を捉えている</li>
</ul>

<h4>結果（表と読み取り）</h4>
<p><b>表6: 市町別 文化財件数 × 観光資源数（上位10）</b></p>
""" + joint_html + """
<p><b>図6: クラスタ別 種別構成（左）と観光化率（右）</b></p>
""" + figure("assets/X02_cluster_compare.png",
              "図6: クラスタ別 種別構成と観光化率の比較") + """
<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>左図（種別構成）</b>: 各クラスタは <b>主種別が明確に違う</b>（クラスタ1は経塚、クラスタ2は官衙跡、クラスタ3は祭祀遺跡＋砂留＋その他の混合）— これがクラスタの解釈軸となる</li>
<li><b>右図（観光化率）</b>: 複合型クラスタ(C3)が最高、古代行政型(C2)はゼロ — 仮説 H2 を支持（古代行政の遺構は学術的価値が高くても観光化されていない）</li>
<li><b>つまり</b>: 観光化されていない文化財は <b>古代行政（府中市・府中町）と中世宗教（世羅町など）</b>に多く、政策的には「価値の再発見」が必要</li>
</ul>"""),

    ("仮説検証と考察", f"""
<h3>仮説と結果の照合</h3>
<table>
<tr><th>#</th><th>仮説</th><th>判定</th><th>根拠</th></tr>
<tr><td>H1</td><td>文化財種別ごとに地理分布が違う（都城・官衙跡は行政中心地、集落跡は山間部）</td>
    <td><b>支持</b></td>
    <td>図2 の地理散布で東部に砂留、北部に祭祀遺跡、府中市に官衙跡が集中する明確なパターン。
        分析2 のクロス集計でも府中市の「官衙跡」{int(crosstab.loc['府中市','官衙跡']) if '府中市' in crosstab.index and '官衙跡' in crosstab.columns else 0} 件が突出。</td></tr>
<tr><td>H2</td><td>観光対象は都城・古墳系が多く、集落跡や経塚は少ない</td>
    <td><b>部分支持</b></td>
    <td>図6（クラスタ別観光化率）でクラスタ間に明確な差。
        古代行政型クラスタ（府中市・府中町）は観光化率 0%、複合型クラスタが {float(cluster_summary.loc[cluster_summary['cluster']==3,'tour_per_burial'].values[0]) if 3 in cluster_summary['cluster'].values else 0:.3f}。
        ただし「都城・古墳系が多い」と予想した行政型クラスタが <b>むしろ観光化されていない</b> 結果は予想と逆。</td></tr>
<tr><td>H3</td><td>市町別 文化財件数 と 観光資源数 が正の相関</td>
    <td><b>反証寄り</b></td>
    <td>図5 の単回帰で R² = {R2_VALUE:.3f}, Pearson r = {R_VALUE:.3f} と相関は<b>極めて弱い</b>。
        福山市（文化財多・観光少）と呉市（文化財少・観光多）など外れ値が多い。
        <b>生件数の比例より種別構成・地域歴史</b>が観光化を決める方が支配的。</td></tr>
<tr><td>H4</td><td>市町プロファイル → 階層クラスタリングで 3 群に分かれる</td>
    <td><b>支持</b></td>
    <td>図3 のデンドログラムで 3 主枝が自然に出現。
        各クラスタの主種別と地理特性が解釈可能（複合型 / 古代行政型 / 中世宗教型）。
        ただし事前ラベル「都市型・農村型・遺跡密集型」とは違う 3 群構造になった。</td></tr>
<tr><td>H5</td><td>PCA 2 軸に主題的な意味（古代行政 vs 中世宗教 など）が見える</td>
    <td><b>部分支持</b></td>
    <td>図4 とローディング表（表5）で PC1 = 「祭祀的小規模遺構 vs 行政遺構」、
        PC2 = 「近世近代建築 vs 江戸期生活遺構」と解釈できる。
        累積寄与率 {PC_CUM_PCT:.0f}% で主構造の半分弱を保持（残りは PC3 以降に分散）。</td></tr>
</table>

<h3>考察</h3>
<ul>
<li><b>2 つの手法が同じ構造を見つけた</b>: 階層クラスタリング（系統樹で群分け）と PCA（2 次元圧縮）は数学的には別物だが、
本データでは同じ 3 群構造が見える（図3 と図4 のクラスタ色配置が一致）。<b>これは構造の頑健性を示す</b>（偶然のクラスタではない）</li>
<li><b>「観光化されていない文化財」は明確に存在</b>: 図6 から、古代行政型クラスタ（府中市・府中町、文化財 17 件）は観光化率 0%。
中世宗教型クラスタ（世羅町・北広島町・安芸太田町、文化財 22 件）も観光化率 0.227 と低い。
<b>地域文化財観光の余白</b>として実証的に示せた</li>
<li><b>件数相関の弱さは予想外</b>: H3 で <b>正の相関を期待</b>したが R² = """ + f"{R2_VALUE:.3f}" + """ と <b>事実上ゼロ</b>。
これは「観光化は文化財量とは独立した別ロジック（地域ブランド・交通便・行政施策）で決まる」ことを示す重要な発見</li>
<li><b>種別正規化の限界</b>: """ + f"{N_KINDS}" + """ カテゴリの分類は手作業の文字列ルール。
シリーズ別（11 種類）の正確な対応にはシリーズ独立ファイルが必要（発展課題）</li>
<li><b>「件数」と「価値」は別物</b>: 1 件しかない庭園跡が地域の最重要観光資源になることもある。
本分析は<b>件数ベース</b>に限界がある</li>
</ul>"""),

    ("発展課題（結果から導かれる新たな問い）", f"""
<p>結果X → 新仮説Y → 課題Z の 3 段で書く。</p>

<ol>
<li><b>11 シリーズ完全網羅で再分析</b>
  <ul>
  <li><b>結果X</b>: 本レッスンは 11 シリーズのうち 2 シリーズ（{N_TOTAL}件）を使用。
      残り 9 シリーズ（古墳・横穴／貝塚／集落跡・散布地／城館跡／社寺跡／生産遺跡／その他の墳墓／近代以降の単独遺跡／水中遺跡）は未取得</li>
  <li><b>新仮説Y</b>: 11 シリーズ全件（推定 1500-3000 件規模）で再分析すれば、
      クラスタ数 3 では足りず、クラスタ数 5-7 が最適になるはず</li>
  <li><b>課題Z</b>: <code>fetch_all.py</code> に DoBoX #1660〜#1670 の取得を追加して 11 シリーズを揃え、
      シルエット係数で最適クラスタ数を決定 → クラスタ別の地域歴史プロファイルを更新</li>
  </ul>
</li>

<li><b>観光化判定の精緻化（マルチソース）</b>
  <ul>
  <li><b>結果X</b>: 本レッスンの「観光化」は <code>infra_tourism.csv</code>（40件）に依存。
      本来は史跡指定・観光ガイド掲載・SNS 投稿数など多源で測るべき</li>
  <li><b>新仮説Y</b>: 「観光化スコア」を多源で組み合わせ、PCA や LDA で <b>観光化されている／いない</b> の二値分類モデルを作れば、
      未活用文化財の <b>潜在的観光価値</b> が予測できる</li>
  <li><b>課題Z</b>: 各市町の (文化財数, 種別構成PC1, 種別構成PC2, 観光資源数, 国指定史跡数) を説明変数、
      観光客数（e-Stat 等）を目的変数とする重回帰（≤5変数）で観光化のドライバを特定</li>
  </ul>
</li>

<li><b>距離的「観光化潜在性」マップ</b>
  <ul>
  <li><b>結果X</b>: 図2 で「観光対象 ×印」と埋蔵文化財の <b>空間的近接</b>が部分的にしかない</li>
  <li><b>新仮説Y</b>: 観光対象施設の半径 1 km 以内に未活用の埋蔵文化財がある場合、
      <b>歩いて巡れる観光ルート</b>が新規に設計できる</li>
  <li><b>課題Z</b>: 各埋蔵文化財から最寄りの観光施設までの距離を計算し、
      「1 km 以内に観光施設なし＋種別が観光化向き（古墳・建物跡・庭園跡）」の文化財をリスト化 → 候補マップを作る</li>
  </ul>
</li>

<li><b>時代軸を加えた 3 次元クラスタリング</b>
  <ul>
  <li><b>結果X</b>: 本レッスンは <b>種別×市町</b>の 2 次元のみ。「時代」列を捨てている</li>
  <li><b>新仮説Y</b>: (市町, 種別, 時代) の 3 次元テンソルでクラスタリングすると、
      「中世福山」「古代府中」など <b>地域×時代の合成プロファイル</b>が見える</li>
  <li><b>課題Z</b>: テンソル分解（CP 分解）または時代×種別の組合せ列で再 PCA。
      高校範囲外なので、まず時代を 5 区分（縄文・弥生・古墳・古代・中世以降）に丸めて 2 次元化する</li>
  </ul>
</li>

<li><b>判別分析（LDA）で「観光化される文化財」の予測</b>
  <ul>
  <li><b>結果X</b>: 図6 で観光化率はクラスタごとに違うが、<b>個別文化財レベル</b>では予測していない</li>
  <li><b>新仮説Y</b>: 文化財 1 件ごとに <b>(種別, 時代, 緯度経度, 市町クラスタ)</b> から
      「観光化される／されない」の二値が LDA で分類できるはず</li>
  <li><b>課題Z</b>: <code>sklearn.discriminant_analysis.LDA</code> で訓練、係数の符号と大きさから
      「観光化されやすい属性」を解釈。学習者は LDA を「2 群を最も分離する直線を引くツール」と理解する</li>
  </ul>
</li>
</ol>
"""),
]

html = render_lesson(
    num=2,  # 表示は X02
    title=f"埋蔵文化財 × 観光地化潜在性 — 階層クラスタリングと PCA で広島県 {N_CITIES} 市町を読む",
    tags=["X-tier", "draft", "L01水準", "PCA", "階層クラスタ"],
    time="120分",
    level="リテラシ研究",
    data_label=(
        '<a href="https://hiroshima-dobox.jp/datasets/1663" target="_blank">#1663 都城・官衙跡 13件</a>、'
        '<a href="https://hiroshima-dobox.jp/datasets/1669" target="_blank">#1669 その他 190件</a>、'
        '<a href="https://hiroshima-dobox.jp/datasets/1280" target="_blank">#1280 インフラツーリズム 40件</a>'
    ),
    sections=sections,
    script_filename="lessons/X02_burial_tourism_potential.py",
)

# X-tier の draft 属性を body に挿入
html = html.replace('<body><div class="container">',
                    '<body data-draft="1" data-stier="X"><div class="container">')

out = LESSONS / "X02_burial_tourism_potential.html"
out.write_text(html, encoding="utf-8")
print(f"\nsaved: {out}")
print("PNGs:")
for p in sorted(ASSETS.glob("X02_*.png")):
    print(f"  - {p.name}  ({p.stat().st_size//1024} KB)")
print("CSVs:")
for p in sorted(ASSETS.glob("X02_*.csv")):
    print(f"  - {p.name}  ({p.stat().st_size//1024} KB)")
