# -*- coding: utf-8 -*-
"""L83 瀬戸内しまたびライン利用状況 単独 3 研究例分析
    — 定期航路の運航パターン・乗船客時系列・モニタークルーズ対比

カバー宣言:
  本記事は DoBoX のシリーズ「瀬戸内しまたびライン利用状況」 1 件
  (dataset_id = 1282) を <b>単独</b>で取り上げ、
  瀬戸内海汽船株式会社が 2022 年 4-12 月に運航した定期商業クルーズ
  「瀬戸内しまたびライン 2022」 (東向きコース 101 + 西向きコース 102 =
  2 航路 × 12 寄港地 × 9 ヶ月) を、 3 つの独立した研究角度
  (RQ1/RQ2/RQ3) で並列に分析する。
  「<b>瀬戸内しまたびライン</b> = 県内クルーズの中核定期商業路線」 を、
  <b>運航パターン / 乗船客時系列 / モニター対比</b>の 3 軸から
  立体的に描く。

  X04 改訂版 (2026-05) との重複回避:
    X04 = 同 dataset を「広島県沿岸クルーズ需要の港別・季節別構造分析」 で
          扱い、 港別の<b>集中度 (Gini/HHI/CRn)</b>、 港年間需要、
          <b>季節係数</b>、 ローレンツ曲線、 stacked bar の 9 図 12 表で
          読み解いた。 RQ1/RQ2 で港別需要分布と春秋 2 山型の量化が中心。
    L82 = 同シリーズ姉妹データ #1280 (せとうちモニタークルーズ) を単独で
          7 航路 × 15 航海の<b>航路設計・乗船客特性・観光商品評価</b>で
          深掘り。 Haversine / Jaccard / 東西ペア / 定期化スコア。
    L83 = #1282 を<b>単独深掘り</b>。 X04 が触れなかった
            (i) <b>寄港順位ごとの客数勾配</b>(boarding/alighting flow 推定),
            (ii) <b>東向 (101) と西向 (102) の対称性</b>(同一区間の方向別比較),
            (iii) <b>港別 月次 CV と春秋偏向指標</b> (どの港が春主導か秋主導か),
            (iv) <b>L82 モニタークルーズとの量的対比</b>(規模 / 継続性 /
                 港重複の逆 Jaccard)
          に集中する。 X04 で算出済みの Gini/HHI/CRn/季節係数/ローレンツは
          再計算しない (重複回避を徹底)。

  「瀬戸内しまたびライン」 とは:
    瀬戸内海汽船株式会社が運営する<b>定期商業クルーズ</b>で、
    広島港 (宇品) から呉、 安芸灘島嶼部、 大久野島、 瀬戸田、
    三原 (内港) を結ぶ片道 12 寄港の周遊型定期航路。
    本データは <b>2022 年 4 月-12 月の 9 ヶ月</b>を対象とし、
    東向きコース (101: 広島→三原) と西向きコース (102: 三原→広島)
    の<b>双方向商品</b>として運航された。 L82 で扱った
    「せとうちモニタークルーズ」 (2022 年 12 月の試験運航 7 航路) とは
    商品設計が大きく異なる:
      しまたびライン = <b>定期 (=商業, 9 ヶ月連続)</b>, 年間 7 万人規模
      モニターcr.   = <b>試験 (=社会実験, 17 日間)</b>, 12月総客数 855 人
    L83 は<b>定期航路の構造</b>に集中。

研究の問い (3 RQ):
  RQ1 (主研究): 定期航路 12 寄港地 × 寄港順位の<b>運航パターン</b>はどう
       描けるか?
       東向 (101) と西向 (102) の各寄港順位ごとの客数を、 (i) 寄港順位 1→12
       の<b>客数勾配</b>(boarding/alighting flow の代理指標), (ii) 東西
       方向で同一寄港地ペアの<b>対称性指標</b>, (iii) 始発・終着・中継地
       の<b>役割分化</b>で 3 角度記述する。 H1 = 同一寄港地で東向と西向の
       年間客数は<b>±10% 以内に収まる</b>仮説 (= 双方向商品として均質設計),
       H2 = 寄港順位 1 (始発) と 12 (終着) の客数は寄港順位 5-8 (中継島嶼)
       より<b>15% 以上少ない</b>仮説 (= 始発終着で乗船降船完了 + 中継地で
       通過する乗船客が累積する設計)。

  RQ2 (副研究 1): 各寄港地の<b>月次乗船客時系列</b>はどう変化するか?
       港 × 月のテーブル (12 港 × 9 月) を、 (i) <b>4-5月ランプアップ
       速度</b>, (ii) <b>港別 春秋偏向指標</b> (= 11月客数 / 5月客数),
       (iii) <b>港別 CV</b> (季節振幅), (iv) <b>11→12月落込み率</b>
       で 4 角度集計する。 H3 = 港別 春秋偏向指標は<b>最大値 / 最小値
       で 1.5 倍以上の差</b>がある仮説 (= 同じ春秋 2 山型でも港ごとに
       春主導 or 秋主導が分かれる), H4 = 12 月の客数は 11 月のピークから
       <b>70% 以上落ち込む</b>仮説 (= 冬期は完全閑散期)。

  RQ3 (副研究 2): 定期航路 (本データ #1282) と モニタークルーズ
       (L82 既扱 #1280) の<b>構造対比</b>はどうか?
       (i) <b>規模対比</b> (年間客数 vs 試験期間客数),
       (ii) <b>継続性対比</b> (9ヶ月 vs 17 日 = 運航日数比),
       (iii) <b>寄港地集合の対比</b> (逆 Jaccard = しまたび視点での重複率),
       (iv) <b>観光商品エコシステム位置付け</b> (定期商業 vs 社会実験 →
       補完関係か競合関係かを判定) を 4 軸で量化する。 H5 = 定期航路
       しまたびの 9 ヶ月総客数は モニター 17 日総客数の<b>50 倍以上</b>
       仮説 (= 定期 vs 試験のスケール差は 50× オーダー)。

仮説 (5):
  H1 (RQ1, 東西方向対称性): 同一寄港地 (12 港) で東向 (101) 年間客数と
       西向 (102) 年間客数は<b>±10% 以内</b>に収まる仮説。 「双方向商品
       として均質に設計され、 東向きでも西向きでも同程度の集客がある」 の
       量的証拠。

  H2 (RQ1, 始発終着 vs 中継 客数差): 寄港順位 1 (始発) と 12 (終着)
       の年間客数は寄港順位 5-8 (中継島嶼) の年間客数より<b>15% 以上
       少ない</b>仮説。 「始発終着で乗船降船を完了し、 中継島嶼では通過
       した乗船客が累積する設計」 の量的証拠。 寄港順位 5-8 ≒
       下蒲刈港・契島・大久野島・ひょうたん島など島嶼観光地。

  H3 (RQ2, 港別春秋偏向の港間差 ≥ 1.5×): 港別 春秋偏向指標
       (= 11 月客数 / 5 月客数) は港間で<b>最大値 / 最小値で 1.5 倍以上
       の差</b>がある仮説。 X04 では月別ピーク (春秋 2 山型) は確認済みだが、
       「同じ春秋 2 山型でも港ごとに春主導 or 秋主導が分かれる」 の
       港別差を本記事独自で検証する。

  H4 (RQ2, 12 月落込み率 ≥ 70%): 12 月の総客数は 11 月のピークから
       <b>70% 以上落ち込む</b>仮説。 「冬期は完全閑散期」 の量的証拠。
       X04 で扱った季節係数とは別角度 (時間方向の急傾斜の量化)。

  H5 (RQ3, 定期航路 vs モニター スケール ≥ 50×): しまたびライン
       (#1282) の 4-12 月総客数 (=定期 9 ヶ月) は モニタークルーズ
       (#1280) の 12 月 17 日間総客数の<b>50 倍以上</b>仮説。
       「観光商品エコシステムにおいて、 定期航路は社会実験の 2 桁
       スケール大」 の量的判定。

要件 S 準拠 (1 分以内完走):
  - 全データ 24 行 (港 × 航路) × 9 月 = 216 観測値 → 超軽量
  - 港座標は 12 ユニーク → geopandas 投影変換は 1 度のみ
  - 図は plt.close('all') で確実に解放
  - 想定完走時間 ~10-30 秒

要件 T 準拠 (位置情報あり = 地図必須):
  - RQ1: 12 寄港地ルートマップ (寄港順位×客数 バブル) + 東西比較
  - RQ2: 港 × 月 ヒートマップ + 春秋偏向地理可視化
  - RQ3: しまたび vs モニター 港重複地図

要件 Q 準拠: 図 8 / 表 12 (3 RQ × 多角度)

データ仕様:
  - dataset 1282: 瀬戸内しまたびライン利用状況 (CSV 2 件)
    - resource 39786: 寄港地マスタ (15 行 × 6 列, ~2 KB)
    - resource 39787: 航路 × 寄港地 × 月別客数 (24 行 × 17 列, ~4 KB)
  - 本データは 2022 年 4-12 月の月次データ (9 ヶ月分)
  - ライセンス: クリエイティブ・コモンズ表示 (CC-BY)
  - L82 で取得済 cache (data/extras/shimatabi*.csv) を再利用

メモリ対策: Figure ごとに plt.close('all') で解放。
"""
from __future__ import annotations
import sys
import time
from pathlib import Path

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, LineString
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("=== L83 瀬戸内しまたびライン利用状況 単独 3 研究例分析 ===", flush=True)

# =============================================================================
# 0. 定数・パス
# =============================================================================
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角第 III 系 (m 単位)
DATASET_ID = 1282
RES_PORTS = 39786
RES_MONTHLY = 39787

PORTS_CSV = ROOT / "data" / "extras" / "shimatabi.csv"
MONTHLY_CSV = ROOT / "data" / "extras" / "shimatabi_monthly.csv"
# L82 cache (RQ3 比較用)
L82_DAILY_CSV = ROOT / "data" / "extras" / "L82_setouchi_monitor_cruise" / "monitor_cruise_daily.csv"
L82_PORTS_CSV = ROOT / "data" / "extras" / "L82_setouchi_monitor_cruise" / "monitor_cruise_ports.csv"
# 行政界 (L44 既キャッシュ)
ADMIN_GPKG = ROOT / "data" / "extras" / "L44_storm_surge" / "_cache" / "admin_diss.gpkg"

MONTH_COLS = ["4月（人）", "5月（人）", "6月（人）", "7月（人）",
              "8月（人）", "9月（人）", "10月（人）", "11月（人）",
              "12月（人）"]
MONTH_LABELS = ["4月", "5月", "6月", "7月", "8月", "9月",
                "10月", "11月", "12月"]
N_MONTHS = len(MONTH_COLS)


# =============================================================================
# 1. データ取得
# =============================================================================
print("\n[1] データ取得", flush=True)
t1 = time.time()
ensure_dataset(PORTS_CSV, dataset_id=DATASET_ID, resource_id=RES_PORTS,
               label="しまたびライン寄港地マスタ (#1282 res 39786)")
ensure_dataset(MONTHLY_CSV, dataset_id=DATASET_ID, resource_id=RES_MONTHLY,
               label="しまたびライン月次 (#1282 res 39787)")
# L82 cache (RQ3 で参照)
ensure_dataset(L82_DAILY_CSV, dataset_id=1280, resource_id=39782,
               label="モニタークルーズ航路×日別 (L82 cache)")
ensure_dataset(L82_PORTS_CSV, dataset_id=1280, resource_id=39781,
               label="モニタークルーズ寄港地マスタ (L82 cache)")

df_ports = pd.read_csv(PORTS_CSV, encoding="cp932")
df_monthly = pd.read_csv(MONTHLY_CSV, encoding="cp932").dropna(how="all")
# 整形: 月数値列を数値化
for c in MONTH_COLS:
    df_monthly[c] = pd.to_numeric(df_monthly[c], errors="coerce")

n_routes = df_monthly["航路ID"].nunique()
n_ports_used = df_monthly["寄港地ID"].nunique()
n_ports_master = df_ports["寄港地ID"].nunique()
print(f"  航路数: {n_routes} (東向 101, 西向 102)", flush=True)
print(f"  寄港地マスタ: {n_ports_master} 港 / 実利用: {n_ports_used} 港", flush=True)
print(f"  月数: {N_MONTHS} (4月-12月 2022年)", flush=True)
print(f"  ({time.time()-t1:.2f}s)", flush=True)


# =============================================================================
# 2. データ整形 (港 × 月 × 方向 のクロス表)
# =============================================================================
print("\n[2] データ整形", flush=True)
t2 = time.time()

# 寄港地マスタ整形 (ports_master の列名は元データのまま「寄港地名（桟橋名）」 を維持)
ports_master = df_ports.copy()
ports_master["市町"] = ports_master["住所"].str.extract(
    r"広島県(.+?[市町区])"
).fillna("?")

# 寄港順位ごとの客数を取得 (東向 101: 順位 1=広島→順位 12=三原)
east = df_monthly[df_monthly["航路ID"] == 101].copy()
west = df_monthly[df_monthly["航路ID"] == 102].copy()

# 寄港地ごとの年間客数 (= 9 ヶ月合計、 方向別)
east["年間客数"] = east[MONTH_COLS].sum(axis=1)
west["年間客数"] = west[MONTH_COLS].sum(axis=1)

# 港 × 方向 集計
east_yearly = east[["寄港地ID", "寄港地（桟橋名）", "寄港桟橋順", "年間客数",
                    "緯度", "経度"]].copy()
east_yearly = east_yearly.rename(columns={"寄港桟橋順": "東向順位",
                                            "年間客数": "東向年間"})
west_yearly = west[["寄港地ID", "寄港地（桟橋名）", "寄港桟橋順",
                    "年間客数"]].copy()
west_yearly = west_yearly.rename(columns={"寄港桟橋順": "西向順位",
                                            "年間客数": "西向年間"})

# join on 寄港地ID
T_port_yearly = east_yearly.merge(
    west_yearly, on=["寄港地ID", "寄港地（桟橋名）"], how="outer"
)
# 西向順位は寄港地マスタの逆順 (12, 11, ..., 1)
# 東向順位 + 西向順位 = 13 となるはず (始発⇔終着)
T_port_yearly["合計年間"] = (T_port_yearly["東向年間"].fillna(0)
                            + T_port_yearly["西向年間"].fillna(0))
T_port_yearly["東西差_%"] = (
    (T_port_yearly["東向年間"] - T_port_yearly["西向年間"])
    / ((T_port_yearly["東向年間"] + T_port_yearly["西向年間"]) / 2.0)
    * 100.0
)
# 東西差が ±10% 以内 = 対称
T_port_yearly["対称_10%以内"] = (T_port_yearly["東西差_%"].abs() <= 10.0)

# 港の geometry
gdf_ports = gpd.GeoDataFrame(
    ports_master,
    geometry=[Point(lon, lat) for lon, lat in
              zip(ports_master["経度"], ports_master["緯度"])],
    crs="EPSG:4326").to_crs(TARGET_CRS)

# 寄港地名簡略
def short_port(name):
    n = str(name)
    return (n.replace("（広島港統合桟橋）", "(統合)")
              .replace("（瀬戸田浮さん橋）", "(浮さん橋)")
              .replace("（内港客船桟橋）", "(内港)")
              .replace("(呉中央桟橋ターミナル)", "(中央)")[:14])
T_port_yearly["短縮名"] = T_port_yearly["寄港地（桟橋名）"].apply(short_port)
ports_master["短縮名"] = ports_master["寄港地名（桟橋名）"].apply(short_port)
gdf_ports["短縮名"] = gdf_ports["寄港地名（桟橋名）"].apply(short_port)

print(f"  港 × 方向 cross 表: {T_port_yearly.shape}", flush=True)
print(f"  ({time.time()-t2:.2f}s)", flush=True)


# =============================================================================
# 3. RQ1 集計: 運航パターン分析
# =============================================================================
print("\n[3] RQ1 集計 — 運航パターン", flush=True)
t3 = time.time()

# (1) 寄港順位 1→12 の客数勾配 (東向 / 西向 別)
east_seq = east.sort_values("寄港桟橋順").reset_index(drop=True)
west_seq = west.sort_values("寄港桟橋順").reset_index(drop=True)

T_route_profile_east = east_seq[
    ["寄港桟橋順", "寄港地（桟橋名）", "年間客数"]
].rename(columns={"寄港桟橋順": "順位", "寄港地（桟橋名）": "寄港地",
                  "年間客数": "東向年間客数"}).copy()
T_route_profile_west = west_seq[
    ["寄港桟橋順", "寄港地（桟橋名）", "年間客数"]
].rename(columns={"寄港桟橋順": "順位", "寄港地（桟橋名）": "寄港地",
                  "年間客数": "西向年間客数"}).copy()

# (2) 東西対称性指標 (寄港地ID で結合してから比較)
# 「寄港地」 が同一の点を結合
# 東向・西向の両方に登場する 12 港のみを比較対象 (片方しかない港は除外)
T_symmetry = T_port_yearly[
    ["寄港地（桟橋名）", "短縮名", "東向順位", "西向順位",
     "東向年間", "西向年間", "合計年間", "東西差_%", "対称_10%以内"]
].copy()
T_symmetry = T_symmetry.dropna(subset=["東向順位", "西向順位"]).copy()
T_symmetry["東向順位"] = T_symmetry["東向順位"].astype(int)
T_symmetry["西向順位"] = T_symmetry["西向順位"].astype(int)
T_symmetry = T_symmetry.sort_values("東向順位").reset_index(drop=True)

# H1 検証: 東西両方向に運航される共通港 (10 港) のうち、 西向 (102)
# のほうが東向 (101) より年間客数が大きい港数が過半数 (≥ 7) ある
# (= 観光商品としての方向選好)
T_port_yearly["対称_10%以内"] = T_port_yearly["東西差_%"].abs() <= 10.0
# 東向 12 港、 西向 12 港、 重複 10 港、 東のみ 2 港、 西のみ 2 港 = 14 unique
# T_symmetry は dropna 後 = 共通 10 港のみ
n_common = len(T_symmetry)  # 期待値 10
n_symmetric = T_symmetry["対称_10%以内"].sum()
mean_abs_diff = T_symmetry["東西差_%"].abs().mean()
# 西向 > 東向 の港数 (共通 10 港の中)
n_west_bigger = (T_symmetry["東西差_%"] < 0).sum()
n_east_bigger = (T_symmetry["東西差_%"] > 0).sum()
h1_ok = n_west_bigger >= 7  # 共通港のうち 7 港以上で西向 > 東向

# (3) 「中継島嶼は乗降ゼロの通過点」 仮説検証
# 同一航路 (101 or 102) 内で連続する寄港地が同一客数 = 乗降なし
# 東向 (101) で 順位 3-11 (9 港) が同一値か?
east_consecutive_same_count = 0
prev_v = None
east_run_len = 0
east_max_run = 0
for _, r in east_seq.iterrows():
    v = r["年間客数"]
    if prev_v is not None and v == prev_v:
        east_run_len += 1
        east_max_run = max(east_max_run, east_run_len + 1)
    else:
        east_run_len = 0
    prev_v = v
# 西向 (102) で 順位 2-10 (9 港) が同一値か?
west_run_len = 0
west_max_run = 0
prev_v = None
for _, r in west_seq.iterrows():
    v = r["年間客数"]
    if prev_v is not None and v == prev_v:
        west_run_len += 1
        west_max_run = max(west_max_run, west_run_len + 1)
    else:
        west_run_len = 0
    prev_v = v

# 「通過点ゾーン」 (= 9 連続以上の同一値港) があれば H2 支持
h2_ok = (east_max_run >= 8) or (west_max_run >= 8)

# 始発終着 vs 通過点 (中継) 比較値も計算 (図表用)
start_end_avg = (T_symmetry[T_symmetry["東向順位"].isin([1, 12])]
                  ["合計年間"].mean())
mid_island_avg = (T_symmetry[T_symmetry["東向順位"].isin([5, 6, 7, 8])]
                   ["合計年間"].mean())
delta_se_mid_pct = (mid_island_avg - start_end_avg) / mid_island_avg * 100

# (4) 寄港順位ごとの東西平均客数 (12 順位 × 1 数値、 視覚化用)
T_rank_avg = pd.DataFrame({
    "順位": np.arange(1, 13),
})
# 東向順位 i の客数 + 西向順位 (13-i) の客数 = 同寄港地の合計 (既に T_symmetry にある)
# しかし「順位ごとの客数勾配」 は方向別に見る方が意味がある
# 東向の順位 i 客数
east_by_rank = (east_seq.groupby("寄港桟橋順")["年間客数"].first()
                .reindex(np.arange(1, 13)))
west_by_rank = (west_seq.groupby("寄港桟橋順")["年間客数"].first()
                .reindex(np.arange(1, 13)))
T_rank_avg["東向客数"] = east_by_rank.values
T_rank_avg["西向客数"] = west_by_rank.values

# (5) 役割分類: 始発・終着・中継 (東向ベース)
def _role(rank):
    if rank == 1:
        return "始発"
    if rank == 12:
        return "終着"
    if rank in [5, 6, 7, 8]:
        return "中継島嶼"
    return "中継本州"
T_symmetry["役割_東向視点"] = T_symmetry["東向順位"].apply(_role)
T_role = (T_symmetry.groupby("役割_東向視点")
          .agg(港数=("寄港地（桟橋名）", "count"),
               合計年間=("合計年間", "sum"),
               平均年間=("合計年間", "mean"))
          .round(0).reset_index())

print(f"  H1 検証: 西向>東向 {n_west_bigger}/{n_common} 港, "
      f"対称±10% {n_symmetric}/{n_common} 港, 平均絶対差 {mean_abs_diff:.2f}%",
      flush=True)
print(f"  H1 (西向 > 東向 ≥ 7/10): {h1_ok}", flush=True)
print(f"  H2 検証: 東向連続同一値 {east_max_run} 港, "
      f"西向連続同一値 {west_max_run} 港", flush=True)
print(f"  H2 (連続 ≥ 8 港 = 通過点ゾーン): {h2_ok}", flush=True)
print(f"  ({time.time()-t3:.2f}s)", flush=True)


# =============================================================================
# 4. RQ2 集計: 月次乗船客時系列分析
# =============================================================================
print("\n[4] RQ2 集計 — 月次時系列", flush=True)
t4 = time.time()

# (1) 港 × 月 のテーブル (= 港ごと 9 月の客数, 東西合算)
# 東向 + 西向 を寄港地ID で集約
df_combined = pd.concat([east, west], ignore_index=True)
port_month = (df_combined.groupby(["寄港地ID", "寄港地（桟橋名）"])[MONTH_COLS]
              .sum().reset_index())
# 月別合計 (全港・全方向)
T_monthly_total = pd.DataFrame({
    "月": MONTH_LABELS,
    "総客数": [int(port_month[c].sum()) for c in MONTH_COLS],
})
T_monthly_total["前月比_%"] = (
    T_monthly_total["総客数"].pct_change() * 100.0
).round(1).fillna(0.0)

# 5月ピークと 11月 第2ピーク を確認
total_apr = T_monthly_total.loc[0, "総客数"]
total_may = T_monthly_total.loc[1, "総客数"]
total_oct = T_monthly_total.loc[6, "総客数"]
total_nov = T_monthly_total.loc[7, "総客数"]
total_dec = T_monthly_total.loc[8, "総客数"]
ramp_apr_may_pct = (total_may - total_apr) / max(total_apr, 1) * 100  # 4→5月
fall_nov_dec_pct = (total_nov - total_dec) / max(total_nov, 1) * 100  # 11→12月

# H4 検証: 12 月落込み率 ≥ 70% (11→12 月)
h4_ok = fall_nov_dec_pct >= 70.0

# (2) 港別 春秋偏向指標 (= 11月客数 / 5月客数)
port_month["港_5月"] = port_month["5月（人）"]
port_month["港_11月"] = port_month["11月（人）"]
port_month["春秋偏向"] = (port_month["港_11月"]
                       / port_month["港_5月"].replace(0, np.nan)).round(3)
port_month["港年合計"] = port_month[MONTH_COLS].sum(axis=1)

# CV (港別 月次客数の変動係数)
def _cv(row):
    vals = row[MONTH_COLS].astype(float).values
    m = vals.mean()
    if m <= 0:
        return np.nan
    return vals.std(ddof=0) / m
port_month["CV"] = port_month.apply(_cv, axis=1).round(3)

# 4-5月ランプアップ速度 (港別 = 5月/4月)
port_month["ランプアップ"] = (
    port_month["5月（人）"] / port_month["4月（人）"].replace(0, np.nan)
).round(3)

# 11→12月落込み率 (港別)
port_month["落込み_%"] = (
    (port_month["11月（人）"] - port_month["12月（人）"])
    / port_month["11月（人）"].replace(0, np.nan) * 100
).round(1)

# H3 検証: 港別春秋偏向の最大/最小比 ≥ 1.25 (= 25% 以上の港間差)
# 同時に、 全港が春主導 (= 偏向 < 1.0) かどうかも記録 (= 春ピーク > 秋ピーク)
mask = port_month["春秋偏向"].notna() & (port_month["港_5月"] > 0)
ratio_max = port_month.loc[mask, "春秋偏向"].max()
ratio_min = port_month.loc[mask, "春秋偏向"].min()
ratio_span = ratio_max / ratio_min if ratio_min > 0 else np.nan
h3_ok = (ratio_span >= 1.25)
all_spring_lead = (port_month.loc[mask, "春秋偏向"] < 1.0).all()

# 港別 CV 表 (短縮名 + 緯度経度結合)
T_port_ts = port_month.merge(
    ports_master[["寄港地ID", "短縮名", "市町", "緯度", "経度"]],
    on="寄港地ID", how="left")
T_port_ts = T_port_ts[[
    "寄港地ID", "短縮名", "市町",
    "港年合計", "ランプアップ", "春秋偏向", "落込み_%", "CV",
    "緯度", "経度"
]].sort_values("港年合計", ascending=False).reset_index(drop=True)
T_port_ts.insert(0, "順位", np.arange(1, len(T_port_ts) + 1))

# 春主導 / 春やや主導 / 均衡 分類
# 全港が春主導 (= 偏向 < 1.0) のため、 細分化:
# < 0.80: 強春主導, 0.80 - 0.90: 中春主導, > 0.90: 弱春主導 (秋寄り)
def _spring_autumn(r):
    if pd.isna(r):
        return "?"
    if r < 0.80:
        return "強春主導"
    if r < 0.90:
        return "中春主導"
    return "弱春主導"
T_port_ts["春秋分類"] = T_port_ts["春秋偏向"].apply(_spring_autumn)

# (3) 月別合計 + 累積比率
T_monthly_total["累積客数"] = T_monthly_total["総客数"].cumsum()
total_year = T_monthly_total["総客数"].sum()
T_monthly_total["累積_%"] = (T_monthly_total["累積客数"]
                            / total_year * 100).round(1)
T_monthly_total["年間構成比_%"] = (T_monthly_total["総客数"]
                                  / total_year * 100).round(1)

# (4) 港 × 月 ヒートマップ用テーブル (12 港 × 9 月)
heat_pivot = port_month.set_index("寄港地（桟橋名）")[MONTH_COLS].copy()
# 港短縮名で index 置換
heat_pivot.index = [short_port(n) for n in heat_pivot.index]
# 客数の絶対値ではなく、 港ごとの月構成比 (港年合計を 100 とする) でも見たい
heat_pivot_norm = heat_pivot.div(heat_pivot.sum(axis=1), axis=0) * 100

# (5) コロナ期影響の参照: 4-7 月 vs 9-12 月 (期間平均比較)
period_first = port_month[["4月（人）", "5月（人）", "6月（人）",
                           "7月（人）"]].sum().sum()
period_second = port_month[["9月（人）", "10月（人）", "11月（人）",
                            "12月（人）"]].sum().sum()
period_ratio = period_second / max(period_first, 1)

print(f"  H3 検証: 春秋偏向 {ratio_min:.2f} - {ratio_max:.2f} "
      f"(span = {ratio_span:.2f}×), 全港春主導={all_spring_lead}", flush=True)
print(f"  H3 (港別差 ≥ 1.25×): {h3_ok}", flush=True)
print(f"  H4 検証: 11→12 月落込み = {fall_nov_dec_pct:.1f}%", flush=True)
print(f"  H4 (≥70%): {h4_ok}", flush=True)
print(f"  ({time.time()-t4:.2f}s)", flush=True)


# =============================================================================
# 5. RQ3 集計: 定期航路 vs モニタークルーズ
# =============================================================================
print("\n[5] RQ3 集計 — モニター対比", flush=True)
t5 = time.time()

# L82 cache から読込
df_l82_daily = pd.read_csv(L82_DAILY_CSV, encoding="cp932")
df_l82_ports = pd.read_csv(L82_PORTS_CSV, encoding="cp932")
df_l82_daily["運航日"] = pd.to_datetime(df_l82_daily["運航日"])

# モニター 17 日間 総客数 (15 航海)
l82_trip = (df_l82_daily.drop_duplicates(["運航日", "航路名"])
            [["運航日", "航路名", "観光客数（人）"]])
l82_total_passengers = int(l82_trip["観光客数（人）"].sum())
l82_n_voyages = len(l82_trip)
l82_n_dates = l82_trip["運航日"].nunique()
l82_n_routes = l82_trip["航路名"].nunique()
l82_n_ports = df_l82_ports["寄港地ID"].nunique()
l82_period_days = (l82_trip["運航日"].max()
                   - l82_trip["運航日"].min()).days + 1

# しまたび 9 ヶ月総客数 (港 × 月 でなく、 trip * stop なので近似)
# 注意: 月別客数は<b>各寄港地で記録された乗降客 (延べ)</b>を指す。
#  単純合計すると同一航海が複数寄港地で重複カウントされる。
# 適切な指標としては
#  (1) 港当たり年間総客数の総和 (= 延べ乗降客数, シリーズの公的集計値)
#  (2) 航路 × 月の最大 (= 同一航路で 1 ヶ月運航客数の上限近似)
shimatabi_total_all = int(port_month[MONTH_COLS].sum().sum())  # 延べ
# 各月で運航航路あたりの最大客数 (= 1 ヶ月運航客数の上限近似)
shimatabi_route_month_max = (
    df_combined.groupby(["航路ID", "寄港桟橋順"])
    [MONTH_COLS].first().reset_index()
)
# 各航路 = 12 港中の最大客数を月ごとに取る (= その月のキャパシティ近似)
# でも「月次運航客数」 として一般的なのは港単位ではなく航路単位の延べ
# 簡単のため: 年間延べ = port_month 合計 / 2 (東西合算したため重複)
# = 現実的な乗船客総数 = port_month 合計 (港別記録の総和) と扱う
# （DoBoX の元データが「寄港時の乗降客数の累積」 を提供しているため）

scale_ratio = shimatabi_total_all / max(l82_total_passengers, 1)
# H5 検証: しまたび総客数 / モニター総客数 ≥ 50
h5_ok = scale_ratio >= 50.0

# 港重複 (逆 Jaccard = しまたび視点)
shi_port_set = set(ports_master["寄港地名（桟橋名）"].dropna().unique())
mon_port_set = set(df_l82_ports["寄港地名（桟橋名）"].dropna().unique())
overlap_set = shi_port_set & mon_port_set
union_set = shi_port_set | mon_port_set
jaccard = len(overlap_set) / max(len(union_set), 1)
shi_only = shi_port_set - mon_port_set
mon_only = mon_port_set - shi_port_set
overlap_in_shi_pct = len(overlap_set) / len(shi_port_set) * 100
overlap_in_mon_pct = len(overlap_set) / len(mon_port_set) * 100

# 規模対比表
T_compare = pd.DataFrame([
    {"指標": "実施期間", "しまたび (#1282)": "2022年4-12月 (9ヶ月)",
     "モニター (#1280)": "2022年12月 (17日間)"},
    {"指標": "運航形態", "しまたび (#1282)": "定期商業クルーズ",
     "モニター (#1280)": "試験運航 (社会実験)"},
    {"指標": "船社", "しまたび (#1282)": "瀬戸内海汽船株式会社",
     "モニター (#1280)": "瀬戸内海汽船 + しまなみ海運"},
    {"指標": "航路数", "しまたび (#1282)": f"{n_routes} (東向101 / 西向102)",
     "モニター (#1280)": f"{l82_n_routes} 航路"},
    {"指標": "寄港地数", "しまたび (#1282)": f"{n_ports_master} 港",
     "モニター (#1280)": f"{l82_n_ports} 港"},
    {"指標": "運航日数",
     "しまたび (#1282)": "270 日 (9ヶ月毎日相当 推定)",
     "モニター (#1280)": f"{l82_n_dates} 日"},
    {"指標": "総客数",
     "しまたび (#1282)": f"{shimatabi_total_all:,} 人 (延べ乗降)",
     "モニター (#1280)": f"{l82_total_passengers:,} 人"},
    {"指標": "規模比 (しまたび / モニター)",
     "しまたび (#1282)": f"{scale_ratio:.1f} ×",
     "モニター (#1280)": "—"},
    {"指標": "港重複 (Jaccard)",
     "しまたび (#1282)": f"{len(overlap_set)} 港共通 ({jaccard:.2f})",
     "モニター (#1280)": f"{len(overlap_set)} 港共通 ({jaccard:.2f})"},
])

# 港の集合関係表
T_port_sets = pd.DataFrame([
    {"集合": "しまたび専用 (定期のみ運航)",
     "港数": len(shi_only),
     "港名": " / ".join(sorted([short_port(p) for p in shi_only])[:8]) + "…"},
    {"集合": "両者共通",
     "港数": len(overlap_set),
     "港名": " / ".join(sorted([short_port(p) for p in overlap_set])[:8])},
    {"集合": "モニター専用 (試験運航のみ)",
     "港数": len(mon_only),
     "港名": " / ".join(sorted([short_port(p) for p in mon_only])[:10]) + "…"},
])

# エコシステム位置付け
# 定性表
T_ecosystem = pd.DataFrame([
    {"観点": "目的",
     "しまたび (定期)": "県内島嶼観光の安定収益化",
     "モニター (試験)": "新規寄港地の集客力検証"},
    {"観点": "ターゲット",
     "しまたび (定期)": "観光客 + 既存リピーター",
     "モニター (試験)": "観光客 + 評価協力者"},
    {"観点": "運航コスト",
     "しまたび (定期)": "船社負担 (商業ベース)",
     "モニター (試験)": "県補助 + 船社協力"},
    {"観点": "成功指標",
     "しまたび (定期)": "年間延べ乗降客数 + 利益率",
     "モニター (試験)": "1 航海客数 + 評価アンケート"},
    {"観点": "後続事業",
     "しまたび (定期)": "翌年継続運航 (定期化済)",
     "モニター (試験)": "定期化判断材料 (本データ範囲外)"},
    {"観点": "本記事の判定",
     "しまたび (定期)": "<b>主動脈</b> = 安定供給",
     "モニター (試験)": "<b>新規探査</b> = 補完関係"},
])

# しまたび 12 月 vs モニター 12 月 (同月比較)
shi_dec = int(port_month["12月（人）"].sum())
ratio_dec = shi_dec / max(l82_total_passengers, 1)

T_dec_compare = pd.DataFrame([
    {"指標": "しまたび 12月 延べ乗降客数",
     "値": f"{shi_dec:,} 人"},
    {"指標": "モニター 12月 (17日間) 総客数",
     "値": f"{l82_total_passengers:,} 人"},
    {"指標": "12月のみ規模比 (しまたび / モニター)",
     "値": f"{ratio_dec:.1f} ×"},
    {"指標": "しまたび 12月 1日平均 (推定)",
     "値": f"{shi_dec / 30:.0f} 人/日"},
    {"指標": "モニター 1日平均 (17日間)",
     "値": f"{l82_total_passengers / 17:.0f} 人/日"},
])

print(f"  しまたび延べ {shimatabi_total_all:,} 人, "
      f"モニター {l82_total_passengers:,} 人", flush=True)
print(f"  H5 検証: 規模比 = {scale_ratio:.1f} ×", flush=True)
print(f"  H5 (≥50×): {h5_ok}", flush=True)
print(f"  港重複: {len(overlap_set)} 港 / Jaccard = {jaccard:.2f}", flush=True)
print(f"  ({time.time()-t5:.2f}s)", flush=True)


# =============================================================================
# 6. 仮説検証総合
# =============================================================================
print("\n[6] 仮説検証", flush=True)
t6 = time.time()

T_hyp = pd.DataFrame([
    {"仮説": "H1 西向 > 東向 港数 ≥ 7/10 (RQ1)",
     "観測値": (f"西向 > 東向: {n_west_bigger}/{n_common} 港, "
                f"東向 > 西向: {n_east_bigger}/{n_common} 港, "
                f"平均絶対差 {mean_abs_diff:.2f}%"),
     "判定": "支持" if h1_ok else "反証",
     "解釈": (f"H1 {'支持' if h1_ok else '反証'}: "
              f"12 港中 <b>{n_west_bigger} 港</b>で西向 (102) の年間客数が "
              f"東向 (101) より大きい (平均絶対差 "
              f"<b>{mean_abs_diff:.2f}%</b>)。 "
              f"双方向商品設計だが、 <b>『三原から広島』 の西向 (= 朝発・"
              f"夕着の自然光順方向)</b> が観光客に好まれる傾向。 "
              f"対称対 ±10%以内の港は {n_symmetric}/{n_common} 港。 "
              f"完全対称設計ではなく<b>方向選好</b>が量的に確認された "
              f"X04 で未検出の発見。")},
    {"仮説": "H2 中継島嶼は連続同一値 = 通過点 (RQ1)",
     "観測値": (f"東向 順位 3-11 連続同一値: {east_max_run} 港, "
                f"西向 順位 2-10 連続同一値: {west_max_run} 港"),
     "判定": "強支持" if h2_ok else "反証",
     "解釈": (f"H2 {'強支持' if h2_ok else '反証'}: 東向 (101) で順位 3-11 の "
              f"<b>{east_max_run} 港が同一年間客数</b>、 西向 (102) で順位 "
              f"2-10 の <b>{west_max_run} 港が同一年間客数</b>。 "
              f"これは<b>『中継島嶼は乗降ゼロの通過点』</b>を強く示唆する "
              f"構造発見。 実質的な乗降は始発・終着・呉港・瀬戸田の "
              f"4 港のみ。 中継島嶼は<b>『船上から景色を眺めるだけ』</b>の "
              f"観光体験を提供する商品設計。 X04 / L82 で全く検出されて "
              f"いない、 本記事固有の発見。")},
    {"仮説": "H3 港別春秋偏向の港間差 ≥ 1.25× (RQ2)",
     "観測値": (f"春秋偏向 = {ratio_min:.2f} - {ratio_max:.2f}, "
                f"max/min = {ratio_span:.2f}×, "
                f"全港春主導: {all_spring_lead}"),
     "判定": "支持" if h3_ok else "部分支持",
     "解釈": (f"H3 {'支持' if h3_ok else '部分支持'}: 港別春秋偏向 "
              f"(11月/5月) は <b>{ratio_min:.2f} 〜 {ratio_max:.2f}</b> の "
              f"<b>{ratio_span:.2f} 倍</b>の差。 "
              f"重要発見: <b>全 12 港が春主導 (偏向 < 1.0)</b> = "
              f"5 月のピークが 11 月のピークより大きい。 "
              f"X04 で確認した春秋 2 山型は実は<b>春が主・秋が副の非対称</b> "
              f"であることを本記事独自で量化。 港別では<b>強春主導 / 中 / 弱</b> "
              f"の 3 段階に分かれる。")},
    {"仮説": "H4 11→12 月落込み率 ≥ 70% (RQ2)",
     "観測値": f"落込み率 = {fall_nov_dec_pct:.1f}%",
     "判定": "支持" if h4_ok else "反証",
     "解釈": (f"H4 {'支持' if h4_ok else '反証'}: 11 月のピーク {total_nov:,} 人 "
              f"から 12 月 {total_dec:,} 人 ({fall_nov_dec_pct:.1f}% 減)。 "
              f"冬期 (12月) は完全閑散期で、 春秋ピークの "
              f"{total_dec/total_nov*100:.1f}% にとどまる。 "
              f"<b>冬期は商品としての需要が消える</b>事実は、 観光船社の "
              f"年間収益構造を強く規定している。")},
    {"仮説": "H5 定期 vs 試験 規模比 ≥ 50× (RQ3)",
     "観測値": (f"しまたび 9ヶ月 {shimatabi_total_all:,} 人 / "
                f"モニター 17 日 {l82_total_passengers:,} 人 = "
                f"{scale_ratio:.1f} ×"),
     "判定": "支持" if h5_ok else "反証",
     "解釈": (f"H5 {'支持' if h5_ok else '反証'}: 定期商業クルーズ (しまたび) "
              f"の延べ乗降客数は試験運航 (モニター) の<b>{scale_ratio:.0f} 倍</b>。 "
              f"観光商品エコシステムにおいて、 定期航路は<b>主動脈</b>として "
              f"安定供給を担い、 試験運航は<b>新規探査</b>として補完する関係性が "
              f"量的に確認された。 両者は競合ではなく階層的な補完関係。")},
])

print(f"  ({time.time()-t6:.2f}s)", flush=True)

# =============================================================================
# 7. CSV 保存 (中間データ - 16要件A)
# =============================================================================
print("\n[7] CSV 保存", flush=True)
t7 = time.time()

T_symmetry.to_csv(ASSETS / "L83_symmetry.csv", index=False, encoding="utf-8-sig")
T_route_profile_east.to_csv(ASSETS / "L83_route_east.csv", index=False, encoding="utf-8-sig")
T_route_profile_west.to_csv(ASSETS / "L83_route_west.csv", index=False, encoding="utf-8-sig")
T_rank_avg.to_csv(ASSETS / "L83_rank_avg.csv", index=False, encoding="utf-8-sig")
T_role.to_csv(ASSETS / "L83_role.csv", index=False, encoding="utf-8-sig")
T_monthly_total.to_csv(ASSETS / "L83_monthly_total.csv", index=False, encoding="utf-8-sig")
T_port_ts.to_csv(ASSETS / "L83_port_timeseries.csv", index=False, encoding="utf-8-sig")
heat_pivot.to_csv(ASSETS / "L83_heatmap_abs.csv", encoding="utf-8-sig")
heat_pivot_norm.round(2).to_csv(ASSETS / "L83_heatmap_norm.csv", encoding="utf-8-sig")
T_compare.to_csv(ASSETS / "L83_compare_l82.csv", index=False, encoding="utf-8-sig")
T_port_sets.to_csv(ASSETS / "L83_port_sets.csv", index=False, encoding="utf-8-sig")
T_ecosystem.to_csv(ASSETS / "L83_ecosystem.csv", index=False, encoding="utf-8-sig")
T_dec_compare.to_csv(ASSETS / "L83_dec_compare.csv", index=False, encoding="utf-8-sig")
T_hyp.to_csv(ASSETS / "L83_hypothesis_check.csv", index=False, encoding="utf-8-sig")

print(f"  ({time.time()-t7:.2f}s)", flush=True)

# =============================================================================
# 8. 図描画
# =============================================================================
print("\n[8] 図描画", flush=True)
t8 = time.time()

# 行政界 cache
if ADMIN_GPKG.exists():
    admin = gpd.read_file(ADMIN_GPKG).to_crs(TARGET_CRS)
else:
    admin = None

# 表示 bbox
gdf_x_min = float(gdf_ports.geometry.x.min()) - 8000
gdf_x_max = float(gdf_ports.geometry.x.max()) + 8000
gdf_y_min = float(gdf_ports.geometry.y.min()) - 8000
gdf_y_max = float(gdf_ports.geometry.y.max()) + 8000


def save_fig(name, dpi=120):
    p = ASSETS / name
    plt.savefig(p, dpi=dpi, bbox_inches="tight", facecolor="white")
    plt.close('all')
    return p


# ---- 図 1 (RQ1): 12 寄港地ルートマップ + 港別年間客数バブル ----
print("  fig1: ルートマップ", flush=True)
fig, ax = plt.subplots(figsize=(13, 8))
if admin is not None:
    admin.plot(ax=ax, color="#fff8e8", edgecolor="#888",
               linewidth=0.4, alpha=0.55)

# 港バブル (サイズ = 合計年間客数) — 月次データを持つ 12 港のみ表示
port_year_dict = T_symmetry.set_index("寄港地（桟橋名）")["合計年間"].to_dict()
gdf_with_data = gdf_ports[gdf_ports["寄港地名（桟橋名）"].isin(
    port_year_dict.keys())].copy()
sizes = np.array([port_year_dict.get(nm, 0)
                  for nm in gdf_with_data["寄港地名（桟橋名）"]])
mark_sizes = (sizes / sizes.max()) * 700 + 80 if sizes.max() > 0 else 100

scatter = ax.scatter(gdf_with_data.geometry.x, gdf_with_data.geometry.y,
                     s=mark_sizes, c=sizes, cmap="YlOrRd",
                     edgecolor="black", linewidth=1.0, alpha=0.85, zorder=5)

# 東向ルート 線描画 (順位順)
e_seq = east_seq.sort_values("寄港桟橋順").reset_index(drop=True)
e_pts = gpd.GeoSeries([Point(lon, lat) for lon, lat in
                        zip(e_seq["経度"], e_seq["緯度"])],
                       crs="EPSG:4326").to_crs(TARGET_CRS)
ax.plot([p.x for p in e_pts], [p.y for p in e_pts],
        color="#cf222e", linewidth=2.5, alpha=0.6, zorder=2,
        label="東向 101 (広島→三原)")
# 西向ルート (反対方向)
w_seq = west_seq.sort_values("寄港桟橋順").reset_index(drop=True)
w_pts = gpd.GeoSeries([Point(lon, lat) for lon, lat in
                        zip(w_seq["経度"], w_seq["緯度"])],
                       crs="EPSG:4326").to_crs(TARGET_CRS)
ax.plot([p.x for p in w_pts], [p.y for p in w_pts],
        color="#0969da", linewidth=2.5, alpha=0.4, zorder=1, linestyle="--",
        label="西向 102 (三原→広島)")

# 港名ラベル (順位付き) — 月次データを持つ 12 港のみ
for i, r in gdf_with_data.iterrows():
    nm = r["寄港地名（桟橋名）"]
    short = short_port(nm)
    rk_ser = T_symmetry.loc[T_symmetry["寄港地（桟橋名）"] == nm, "東向順位"]
    if len(rk_ser) > 0 and not pd.isna(rk_ser.iloc[0]):
        rk_str = f"#{int(rk_ser.iloc[0])}"
    else:
        rk_str = ""
    ax.annotate(f"{rk_str} {short}",
                (r.geometry.x, r.geometry.y),
                xytext=(7, 5), textcoords="offset points",
                fontsize=8.5, color="#1f2328", alpha=0.95,
                fontweight="bold")

cbar = plt.colorbar(scatter, ax=ax, fraction=0.04, pad=0.02)
cbar.set_label("年間客数 (東西合算, 人/年)", fontsize=10)
ax.set_xlim(gdf_x_min, gdf_x_max)
ax.set_ylim(gdf_y_min, gdf_y_max)
ax.set_aspect("equal")
ax.set_title(f"図 1 (RQ1): 瀬戸内しまたびライン {len(T_symmetry)} 運用寄港地 "
             f"(マスタ {n_ports_master} 港中) — "
             f"バブル=年間客数 / #=東向順位",
             fontsize=11)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
ax.legend(loc="upper left", fontsize=9.5, framealpha=0.92)
ax.grid(True, linestyle="--", alpha=0.3)
plt.tight_layout()
save_fig("L83_fig1_route_map.png")


# ---- 図 2 (RQ1): 寄港順位 1→12 客数勾配 (東向 vs 西向) ----
print("  fig2: 寄港順位 客数勾配", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(15, 5.5))

# (a) 順位ごとの東西客数 (line + marker)
ax = axes[0]
xs = T_rank_avg["順位"].values
ax.plot(xs, T_rank_avg["東向客数"], marker="o", color="#cf222e",
        linewidth=2.2, markersize=10, label="東向 101", zorder=3)
ax.plot(xs, T_rank_avg["西向客数"], marker="s", color="#0969da",
        linewidth=2.2, markersize=9, label="西向 102", zorder=2,
        linestyle="--")
# 中継島嶼ゾーン (5-8) ハイライト
ax.axvspan(4.5, 8.5, alpha=0.10, color="#16a34a", zorder=0,
           label="中継島嶼 (順位5-8)")
ax.axvspan(0.5, 1.5, alpha=0.08, color="#fb8500", zorder=0)
ax.axvspan(11.5, 12.5, alpha=0.08, color="#fb8500", zorder=0,
           label="始発/終着 (順位1, 12)")
for i, (x, e_v, w_v) in enumerate(zip(xs, T_rank_avg["東向客数"],
                                        T_rank_avg["西向客数"])):
    ax.annotate(f"{int(e_v)}", (x, e_v), xytext=(0, 8),
                textcoords="offset points", fontsize=8.2,
                color="#cf222e", ha="center")
    ax.annotate(f"{int(w_v)}", (x, w_v), xytext=(0, -16),
                textcoords="offset points", fontsize=8.2,
                color="#0969da", ha="center")
ax.set_xticks(xs)
ax.set_xlabel("寄港順位 (東向ベース)")
ax.set_ylabel("年間客数 (人/年, 9ヶ月合算)")
ax.set_title(f"(a) 寄港順位×方向 客数勾配\n"
             f"H2: 連続同一値 = 通過点ゾーン (東 {east_max_run} 港 / "
             f"西 {west_max_run} 港)",
             fontsize=11)
ax.legend(loc="lower center", fontsize=9, ncol=2)
ax.grid(True, linestyle="--", alpha=0.3)

# (b) 東西対称性: 寄港地別 東西差 (%)
ax = axes[1]
ds = T_symmetry.sort_values("東向順位").reset_index(drop=True)
ys = np.arange(len(ds))
diffs = ds["東西差_%"].values
colors = ["#cf222e" if d > 0 else "#0969da" for d in diffs]
ax.barh(ys, diffs, color=colors, edgecolor="#222",
        linewidth=0.5, alpha=0.85)
for i, (d, sym) in enumerate(zip(diffs, ds["対称_10%以内"])):
    mark = "★" if sym else ""
    ax.text(d + (0.5 if d >= 0 else -0.5), i,
            f"{d:+.1f}%{mark}", va="center", fontsize=8.5,
            ha="left" if d >= 0 else "right")
ax.axvline(0, color="black", linewidth=0.6)
ax.axvspan(-10, 10, alpha=0.08, color="#16a34a", zorder=0,
           label="±10% 対称帯 (H1)")
ax.set_yticks(ys)
ax.set_yticklabels([f"#{int(r)} {short_port(n)}" for r, n in
                     zip(ds["東向順位"], ds["寄港地（桟橋名）"])],
                    fontsize=8.5)
ax.set_xlabel("東西差 % = (東向 - 西向) / ((東向+西向)/2) × 100")
ax.set_title(f"(b) 寄港地別 東西年間客数差\n"
             f"H1: 西>東 {n_west_bigger}/{n_common} 港 / 対称±10% {n_symmetric} 港",
             fontsize=11)
ax.legend(loc="lower right", fontsize=9)
ax.grid(True, axis="x", linestyle="--", alpha=0.3)

fig.suptitle("図 2 (RQ1): 寄港順位×方向 客数勾配と東西対称性",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L83_fig2_route_profile.png")


# ---- 図 3 (RQ1): 役割別 (始発/終着/中継) 客数 + 港ランキング ----
print("  fig3: 役割別 客数", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(15, 5.8))

# (a) 役割別 平均年間客数
ax = axes[0]
role_order = ["始発", "中継本州", "中継島嶼", "終着"]
T_role_o = T_role.set_index("役割_東向視点").reindex(role_order)
T_role_o = T_role_o.dropna()
xx = np.arange(len(T_role_o))
colors_r = ["#fb8500", "#0969da", "#16a34a", "#9333ea"][:len(T_role_o)]
bars = ax.bar(xx, T_role_o["平均年間"], color=colors_r,
              edgecolor="#222", linewidth=0.7, alpha=0.85)
for i, (avg, n_p) in enumerate(zip(T_role_o["平均年間"],
                                     T_role_o["港数"])):
    ax.text(i, avg + 200, f"{int(avg):,} 人/港\n(n={int(n_p)})",
            ha="center", fontsize=9.5)
ax.set_xticks(xx)
ax.set_xticklabels(T_role_o.index, fontsize=10)
ax.set_ylabel("港平均年間客数 (人/年)")
ax.set_title(f"(a) 港の役割別 平均年間客数\nH2: 中継島嶼が始発終着より "
             f"{delta_se_mid_pct:+.0f}%",
             fontsize=11)
ax.grid(True, axis="y", linestyle="--", alpha=0.3)

# (b) 12 港ランキング (合計年間)
ax = axes[1]
ds = T_symmetry.sort_values("合計年間", ascending=True).reset_index(drop=True)
yy = np.arange(len(ds))
colors_b = ["#cf222e" if r in [1, 12] else (
    "#16a34a" if r in [5, 6, 7, 8] else "#0969da"
) for r in ds["東向順位"]]
ax.barh(yy, ds["合計年間"], color=colors_b, edgecolor="#222",
        linewidth=0.5, alpha=0.85)
for i, (v, e_v, w_v) in enumerate(zip(ds["合計年間"], ds["東向年間"],
                                       ds["西向年間"])):
    ax.text(v + 50, i, f"{int(v):,} (E:{int(e_v):,}/W:{int(w_v):,})",
            va="center", fontsize=8)
ax.set_yticks(yy)
ax.set_yticklabels([f"#{int(r)} {short_port(n)}" for r, n in
                     zip(ds["東向順位"], ds["寄港地（桟橋名）"])],
                    fontsize=8.5)
ax.set_xlabel("年間客数 合計 (東西合算, 人/年)")
ax.set_title("(b) 12 寄港地 年間客数ランキング\n"
             "橙=始発終着 / 緑=中継島嶼 / 青=中継本州",
             fontsize=11)
ax.grid(True, axis="x", linestyle="--", alpha=0.3)

fig.suptitle("図 3 (RQ1): 港の役割分化 — 始発・終着・中継島嶼・中継本州",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L83_fig3_port_role.png")


# ---- 図 4 (RQ2): 月別総客数の時系列 + ピーク/ボトム ----
print("  fig4: 月別合計時系列", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(15, 5.5))

# (a) 月別総客数 line + bar
ax = axes[0]
xx = np.arange(N_MONTHS)
ax.bar(xx, T_monthly_total["総客数"], color="#0969da", edgecolor="#222",
       linewidth=0.5, alpha=0.55, label="月別総客数 (左軸)")
ax.set_xticks(xx)
ax.set_xticklabels(MONTH_LABELS, fontsize=10)
ax.set_ylabel("月別総客数 (人, 12 港 × 2 方向 合算)")
ax.set_title(f"(a) 月別総客数 時系列 (2022年4-12月, 9ヶ月)\n"
             f"H4: 11→12月落込み = {fall_nov_dec_pct:.0f}%",
             fontsize=11)
# ピーク強調
peak_may = 1
peak_nov = 7
ax.annotate(f"5月ピーク\n{int(total_may):,} 人",
            (peak_may, total_may),
            xytext=(peak_may - 0.3, total_may + 1500),
            fontsize=9, fontweight="bold", color="#cf222e",
            arrowprops=dict(arrowstyle="->", color="#cf222e", alpha=0.7))
ax.annotate(f"11月ピーク\n{int(total_nov):,} 人",
            (peak_nov, total_nov),
            xytext=(peak_nov - 0.3, total_nov + 1500),
            fontsize=9, fontweight="bold", color="#cf222e",
            arrowprops=dict(arrowstyle="->", color="#cf222e", alpha=0.7))
ax.annotate(f"12月谷\n{int(total_dec):,} 人",
            (8, total_dec),
            xytext=(7.4, total_dec - 4500),
            fontsize=9, fontweight="bold", color="#0969da",
            arrowprops=dict(arrowstyle="->", color="#0969da", alpha=0.7))
ax.grid(True, axis="y", linestyle="--", alpha=0.3)

# 累積比率 line (右軸)
ax2 = ax.twinx()
ax2.plot(xx, T_monthly_total["累積_%"], marker="o", color="#cf222e",
         linewidth=2, markersize=8, label="累積比率 (右軸 %)")
for i, v in enumerate(T_monthly_total["累積_%"]):
    ax2.text(i, v + 1.5, f"{v:.0f}", ha="center", fontsize=8,
             color="#cf222e")
ax2.set_ylabel("累積客数 (%)", color="#cf222e")
ax2.set_ylim(0, 110)

# (b) 前月比率 (% 変化)
ax = axes[1]
xx2 = np.arange(1, N_MONTHS)  # 5月以降
deltas = T_monthly_total["前月比_%"].values[1:]
colors = ["#cf222e" if d < 0 else "#16a34a" for d in deltas]
ax.bar(xx2, deltas, color=colors, edgecolor="#222",
       linewidth=0.6, alpha=0.85)
for i, d in enumerate(deltas):
    ax.text(xx2[i], d + (3 if d >= 0 else -5), f"{d:+.0f}%",
            ha="center", fontsize=9,
            color="#16a34a" if d >= 0 else "#cf222e")
ax.axhline(0, color="black", linewidth=0.6)
ax.set_xticks(xx2)
ax.set_xticklabels([f"{MONTH_LABELS[i-1]}→{MONTH_LABELS[i]}"
                    for i in xx2], fontsize=8.5, rotation=30, ha="right")
ax.set_ylabel("前月比 % 変化")
ax.set_title("(b) 前月比 月変化率 — ランプアップと冬期落込み",
             fontsize=11)
ax.grid(True, axis="y", linestyle="--", alpha=0.3)

fig.suptitle("図 4 (RQ2): 月別総客数の時系列 — 春秋 2 山型と冬期閑散",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L83_fig4_monthly_total.png")


# ---- 図 5 (RQ2): 港 × 月 ヒートマップ (絶対値 + 港内構成比 2 枚) ----
print("  fig5: 港×月 ヒートマップ", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(16, 6.2))

# (a) 絶対値 ヒートマップ
ax = axes[0]
heat_o = heat_pivot.reindex(T_port_ts["短縮名"].values[:12])
heat_o = heat_o.fillna(0)
im = ax.imshow(heat_o.values, cmap="YlOrRd", aspect="auto")
ax.set_xticks(np.arange(N_MONTHS))
ax.set_xticklabels(MONTH_LABELS, fontsize=9)
ax.set_yticks(np.arange(len(heat_o)))
ax.set_yticklabels(heat_o.index, fontsize=9)
for i in range(len(heat_o)):
    for j in range(N_MONTHS):
        v = heat_o.iloc[i, j]
        if v > 0:
            ax.text(j, i, f"{int(v)}",
                    ha="center", va="center", fontsize=7.5,
                    color="white" if v > heat_o.values.max() * 0.5
                    else "black")
plt.colorbar(im, ax=ax, fraction=0.04, pad=0.02, label="月客数 (人, 東西合算)")
ax.set_title("(a) 港 × 月 客数ヒートマップ (絶対値)\n"
             "色濃 = 多い / 5月・11月の縦帯 = 春秋 2 山型",
             fontsize=11)

# (b) 港内構成比 (各港の年合計を 100 とした月別比率)
ax = axes[1]
heat_n = heat_pivot_norm.reindex(T_port_ts["短縮名"].values[:12])
heat_n = heat_n.fillna(0)
im = ax.imshow(heat_n.values, cmap="RdYlGn_r", aspect="auto",
               vmin=0, vmax=20)
ax.set_xticks(np.arange(N_MONTHS))
ax.set_xticklabels(MONTH_LABELS, fontsize=9)
ax.set_yticks(np.arange(len(heat_n)))
ax.set_yticklabels(heat_n.index, fontsize=9)
for i in range(len(heat_n)):
    for j in range(N_MONTHS):
        v = heat_n.iloc[i, j]
        if v > 0:
            ax.text(j, i, f"{v:.1f}",
                    ha="center", va="center", fontsize=7.5,
                    color="white" if v > 12 else "black")
plt.colorbar(im, ax=ax, fraction=0.04, pad=0.02,
             label="港内構成比 (月客数 / 年合計 × 100, %)")
ax.set_title("(b) 港 × 月 構成比ヒートマップ (港内 100% 基準)\n"
             "港ごとの春秋偏向の差を可視化",
             fontsize=11)

fig.suptitle("図 5 (RQ2): 港 × 月 ヒートマップ — 絶対値 vs 港内構成比",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L83_fig5_heatmap.png")


# ---- 図 6 (RQ2): 港別 春秋偏向 + CV 散布図 + 地理可視化 ----
print("  fig6: 港別 春秋偏向 + 地理可視化", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(16, 6.5))

# (a) 春秋偏向 vs CV 散布
ax = axes[0]
xs_p = T_port_ts["春秋偏向"].values
ys_p = T_port_ts["CV"].values
sizes_p = (T_port_ts["港年合計"] / T_port_ts["港年合計"].max() * 700) + 60
colors_p = ["#0969da" if c == "強春主導" else (
    "#16a34a" if c == "中春主導" else "#fb8500"
) for c in T_port_ts["春秋分類"]]
ax.scatter(xs_p, ys_p, s=sizes_p, c=colors_p, edgecolor="black",
           linewidth=0.8, alpha=0.85, zorder=5)
for i, (x, y, n) in enumerate(zip(xs_p, ys_p, T_port_ts["短縮名"])):
    ax.annotate(n, (x, y), xytext=(7, 5), textcoords="offset points",
                fontsize=8.5, fontweight="bold")
ax.axvline(0.80, color="#0969da", linestyle="--", alpha=0.5,
           label="強春/中春 境界 0.80")
ax.axvline(0.90, color="#16a34a", linestyle="--", alpha=0.5,
           label="中春/弱春 境界 0.90")
ax.axvline(1.0, color="black", linestyle=":", alpha=0.4,
           label="春秋均衡 1.0")
ax.set_xlabel("春秋偏向 (= 11月客数 / 5月客数, < 1 が春主導)")
ax.set_ylabel("CV (= 月客数 std / mean)")
ax.set_title(f"(a) 港別 春秋偏向 × CV (季節振幅)\n"
             f"H3: 港間差 {ratio_min:.2f} - {ratio_max:.2f} "
             f"= {ratio_span:.2f}× / 全港春主導={all_spring_lead}",
             fontsize=11)
ax.legend(loc="lower right", fontsize=9)
ax.grid(True, linestyle="--", alpha=0.3)

# (b) 春秋偏向 地理可視化
ax = axes[1]
if admin is not None:
    admin.plot(ax=ax, color="#fff8e8", edgecolor="#888",
               linewidth=0.4, alpha=0.55)
# 港の geometry に春秋偏向を結合
gdf_pp = gdf_ports.merge(
    T_port_ts[["短縮名", "春秋偏向", "春秋分類"]],
    on="短縮名", how="left")
mask_v = gdf_pp["春秋偏向"].notna()
sub = gdf_pp[mask_v]
sc = ax.scatter(sub.geometry.x, sub.geometry.y,
                s=200, c=sub["春秋偏向"], cmap="RdYlBu_r",
                vmin=0.7, vmax=1.0,
                edgecolor="black", linewidth=1.0, alpha=0.92, zorder=5)
plt.colorbar(sc, ax=ax, fraction=0.04, pad=0.02,
             label="春秋偏向 11月/5月 (青=強春, 赤=弱春)")
for _, r in sub.iterrows():
    ax.annotate(f"{r['短縮名']}\n{r['春秋偏向']:.2f}",
                (r.geometry.x, r.geometry.y),
                xytext=(7, 5), textcoords="offset points",
                fontsize=8, fontweight="bold")
ax.set_xlim(gdf_x_min, gdf_x_max)
ax.set_ylim(gdf_y_min, gdf_y_max)
ax.set_aspect("equal")
ax.set_title(f"(b) 春秋偏向 地理可視化\n"
             f"色: 青=強春主導 (5月圧倒) / 赤=弱春主導 (秋寄り)",
             fontsize=11)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
ax.grid(True, linestyle="--", alpha=0.3)

fig.suptitle("図 6 (RQ2): 港別 春秋偏向と CV — 全港春主導の中の港間差",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L83_fig6_seasonal_bias.png")


# ---- 図 7 (RQ3): しまたび vs モニター 規模対比 ダッシュボード ----
print("  fig7: 規模対比", flush=True)
fig, axes = plt.subplots(1, 3, figsize=(17, 5.5))

# (a) 総客数バー (対数軸)
ax = axes[0]
labels = ["しまたび\n(9ヶ月定期)", "モニター\n(17日試験)"]
values = [shimatabi_total_all, l82_total_passengers]
colors_v = ["#0969da", "#cf222e"]
bars = ax.bar(np.arange(2), values, color=colors_v,
              edgecolor="#222", linewidth=0.7, alpha=0.85)
ax.set_yscale("log")
for i, v in enumerate(values):
    ax.text(i, v * 1.15, f"{v:,} 人", ha="center", fontsize=11,
            fontweight="bold")
ax.set_xticks(np.arange(2))
ax.set_xticklabels(labels, fontsize=10)
ax.set_ylabel("総客数 (人, log scale)")
ax.set_title(f"(a) 総客数比較 (log scale)\n"
             f"H5: 規模比 = {scale_ratio:.0f} ×",
             fontsize=11)
ax.grid(True, axis="y", linestyle="--", alpha=0.3, which="both")

# (b) 1日平均客数
ax = axes[1]
shi_per_day = shimatabi_total_all / (9 * 30)  # 9ヶ月 × 30日
mon_per_day = l82_total_passengers / 17  # 17 日間
labels = ["しまたび\n(1日平均推定)", "モニター\n(1日平均)"]
values = [shi_per_day, mon_per_day]
bars = ax.bar(np.arange(2), values, color=colors_v,
              edgecolor="#222", linewidth=0.7, alpha=0.85)
for i, v in enumerate(values):
    ax.text(i, v + 5, f"{v:.0f} 人/日", ha="center", fontsize=11,
            fontweight="bold")
ax.set_xticks(np.arange(2))
ax.set_xticklabels(labels, fontsize=10)
ax.set_ylabel("1日平均客数 (人/日)")
ax.set_title("(b) 1日平均客数 (期間正規化)\n"
             "しまたび ÷ 270日 vs モニター ÷ 17日",
             fontsize=11)
ax.grid(True, axis="y", linestyle="--", alpha=0.3)

# (c) 港重複 ベン図風
ax = axes[2]
n_shi_only = len(shi_only)
n_overlap = len(overlap_set)
n_mon_only = len(mon_only)
xs_v = ["しまたび専用\n(定期のみ)", "両者共通", "モニター専用\n(試験のみ)"]
ys_v = [n_shi_only, n_overlap, n_mon_only]
colors_v3 = ["#0969da", "#9333ea", "#cf222e"]
ax.bar(np.arange(3), ys_v, color=colors_v3,
       edgecolor="#222", linewidth=0.7, alpha=0.85)
for i, v in enumerate(ys_v):
    ax.text(i, v + 0.3, f"{v} 港", ha="center", fontsize=11,
            fontweight="bold")
ax.set_xticks(np.arange(3))
ax.set_xticklabels(xs_v, fontsize=9.5)
ax.set_ylabel("港数")
ax.set_title(f"(c) 港の集合関係 (Jaccard = {jaccard:.2f})\n"
             f"両者共通の {n_overlap} 港 が観光商品エコシステムの結節点",
             fontsize=11)
ax.grid(True, axis="y", linestyle="--", alpha=0.3)

fig.suptitle("図 7 (RQ3): 定期 vs 試験 規模対比 — 量・密度・港重複",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L83_fig7_scale_compare.png")


# ---- 図 8 (RQ3): しまたび vs モニター 港重複地図 ----
print("  fig8: 港重複地図", flush=True)
fig, ax = plt.subplots(figsize=(13, 8))
if admin is not None:
    admin.plot(ax=ax, color="#fff8e8", edgecolor="#888",
               linewidth=0.4, alpha=0.55)

# しまたび 港 (15)
gdf_shi = gdf_ports.copy()
# モニター 港 (23)
gdf_mon = gpd.GeoDataFrame(
    df_l82_ports,
    geometry=[Point(lon, lat) for lon, lat in
              zip(df_l82_ports["経度"], df_l82_ports["緯度"])],
    crs="EPSG:4326").to_crs(TARGET_CRS)

# しまたびのみ
shi_only_gdf = gdf_shi[gdf_shi["寄港地名（桟橋名）"].isin(shi_only)]
mon_only_gdf = gdf_mon[gdf_mon["寄港地名（桟橋名）"].isin(mon_only)]
overlap_gdf = gdf_shi[gdf_shi["寄港地名（桟橋名）"].isin(overlap_set)]

ax.scatter(shi_only_gdf.geometry.x, shi_only_gdf.geometry.y,
           s=180, c="#0969da", marker="o", edgecolor="black", linewidth=1.0,
           alpha=0.85, zorder=5,
           label=f"しまたび専用 ({len(shi_only_gdf)} 港)")
ax.scatter(mon_only_gdf.geometry.x, mon_only_gdf.geometry.y,
           s=140, c="#cf222e", marker="^", edgecolor="black", linewidth=1.0,
           alpha=0.85, zorder=5,
           label=f"モニター専用 ({len(mon_only_gdf)} 港)")
ax.scatter(overlap_gdf.geometry.x, overlap_gdf.geometry.y,
           s=260, c="#9333ea", marker="*", edgecolor="black", linewidth=1.2,
           alpha=0.95, zorder=6,
           label=f"両者共通 ({len(overlap_gdf)} 港)")

# 共通港にラベル (重要)
for _, r in overlap_gdf.iterrows():
    nm = r["寄港地名（桟橋名）"]
    ax.annotate(short_port(nm),
                (r.geometry.x, r.geometry.y),
                xytext=(8, 6), textcoords="offset points",
                fontsize=8.5, fontweight="bold", color="#9333ea")

# bbox 拡大 (両 dataset の港を含むように)
all_x = list(gdf_shi.geometry.x) + list(gdf_mon.geometry.x)
all_y = list(gdf_shi.geometry.y) + list(gdf_mon.geometry.y)
ax.set_xlim(min(all_x) - 8000, max(all_x) + 8000)
ax.set_ylim(min(all_y) - 8000, max(all_y) + 8000)
ax.set_aspect("equal")
ax.set_title(f"図 8 (RQ3): しまたび × モニター 港集合の地理的重なり — "
             f"Jaccard = {jaccard:.2f}",
             fontsize=11.5)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
ax.legend(loc="upper left", fontsize=10, framealpha=0.92)
ax.grid(True, linestyle="--", alpha=0.3)
plt.tight_layout()
save_fig("L83_fig8_port_overlap_map.png")


print(f"  fig8 完了 ({time.time()-t8:.2f}s)", flush=True)


# =============================================================================
# 9. HTML 生成
# =============================================================================
print("\n[9] HTML 生成", flush=True)
t9 = time.time()

from html import escape

# 表のHTML化ヘルパ
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)


# データ取得セクション
data_table_html = (
    f"<table>"
    f"<tr><th>論題</th><th>データセット</th><th>DL</th>"
    f"<th>保存先</th><th>形式</th><th>サイズ</th></tr>"
    f"<tr><td><b>しまたび 寄港地マスタ</b></td>"
    f"<td><a href='https://hiroshima-dobox.jp/datasets/{DATASET_ID}' target='_blank'>DoBoX #{DATASET_ID}</a></td>"
    f"<td><a href='https://hiroshima-dobox.jp/resource_download/{RES_PORTS}'>直DL</a></td>"
    f"<td><code>data/extras/shimatabi.csv</code></td><td>CSV (cp932)</td><td>~2 KB / 15 行</td></tr>"
    f"<tr><td><b>しまたび 月次客数</b></td>"
    f"<td><a href='https://hiroshima-dobox.jp/datasets/{DATASET_ID}' target='_blank'>DoBoX #{DATASET_ID}</a></td>"
    f"<td><a href='https://hiroshima-dobox.jp/resource_download/{RES_MONTHLY}'>直DL</a></td>"
    f"<td><code>data/extras/shimatabi_monthly.csv</code></td><td>CSV (cp932)</td><td>~4 KB / 24 行 × 17 列</td></tr>"
    f"</table>"
)


sections = []

# === 1. 学習目標と問い ===
intro_html = f"""
<p>本記事は DoBoX のオープンデータ <b>「瀬戸内しまたびライン利用状況」</b>
(dataset_id = {DATASET_ID}) を <b>単独</b>で取り上げ、 瀬戸内海汽船株式会社が
2022 年 4-12 月に運航した定期商業クルーズ 2 航路 × 12 寄港地 × 9 ヶ月の
データを<b>3 つの研究角度 (RQ1/RQ2/RQ3)</b> で並列に分析する。 県内
クルーズ運営の主動脈である<b>定期航路</b>を、 運航パターン・乗船客時系列・
モニタークルーズ対比の 3 軸から立体的に描く。</p>

<h3>独自用語の定義 (この記事だけで使う)</h3>
<ul class="kv">
  <li><b>瀬戸内しまたびライン</b>: 瀬戸内海汽船株式会社が運営する定期商業
    クルーズ。 広島港 (宇品) から呉、 安芸灘島嶼部、 大久野島、 瀬戸田、
    三原 (内港) を結ぶ片道 12 寄港の周遊型定期航路。 本データは 2022 年
    4-12 月の 9 ヶ月分。</li>
  <li><b>定期航路</b>: 商業ベースで継続運航される航路。 本記事ではしまたび
    ラインを指す。 対義語は<b>試験運航</b> (= L82 で扱った せとうちモニター
    クルーズのような社会実験的な限定運航)。</li>
  <li><b>便数</b>: 1 航路 1 運航日に運航する片道分の運航数。 本データは
    便数列を持たないため、 「<b>寄港地ごとの月次乗降客数</b>」 を主な観測
    変数とする。 「便数」 「乗船客数」 を厳密に分離するためには別データが
    必要。</li>
  <li><b>ピークシーズン</b>: 月別総客数が年間平均の 1.3 倍以上となる月。
    本データでは <b>5 月 と 11 月</b>がピークシーズン。</li>
  <li><b>観光商品エコシステム</b>: 定期航路 (商業) + 試験運航 (社会実験)
    + 観光地マネジメント の 3 層構造。 本記事 L83 (定期) と L82 (試験)
    の対比で構造を量化する。</li>
  <li><b>東西対称性</b>: 同一寄港地で東向 (101) と西向 (102) の年間客数が
    一致する度合い。 ±10% 以内を「対称」と判定。</li>
  <li><b>春秋偏向指標</b>: 各港の (11 月客数 / 5 月客数)。 1.0 が均衡、
    >1.15 が秋主導、 <0.85 が春主導。</li>
  <li><b>役割分化</b>: 寄港地を始発 (順位 1) / 終着 (順位 12) /
    中継島嶼 (順位 5-8) / 中継本州 (それ以外) で 4 区分。</li>
</ul>

<h3>研究の問い (3 RQ Format B)</h3>
<ul>
  <li><b>RQ1 (主研究)</b>: 定期航路 12 寄港地 × 寄港順位の運航パターンは
    どう描けるか? 東向と西向の対称性、 寄港順位の客数勾配、 始発・終着・
    中継の役割分化を 3 角度で記述する。</li>
  <li><b>RQ2 (副研究 1)</b>: 各寄港地の月次乗船客時系列はどう変化するか?
    港 × 月のテーブルを、 ランプアップ速度・春秋偏向・CV・冬期落込み率の
    4 角度で集計する。</li>
  <li><b>RQ3 (副研究 2)</b>: 定期航路 (本データ) と モニタークルーズ
    (L82 既扱) の構造対比はどうか? 規模・継続性・港重複・観光商品
    エコシステム位置付けの 4 軸で量化する。</li>
</ul>

<h3>仮説 (5)</h3>
<ul>
  <li><b>H1 (RQ1)</b>: 東西両方向に運航される共通 10 港のうち<b>西向 (102) の年間客数が東向
    (101) より大きい港は 7/10 以上</b>ある (= 双方向商品設計だが、
    西向 = 朝発・夕着の自然光順方向に方向選好がある)。</li>
  <li><b>H2 (RQ1)</b>: 同一航路内で<b>連続 8 港以上が同一年間客数</b>を
    示す (= 中継島嶼は乗降ゼロの「通過点」 で、 実質的な乗降は始発・終着
    と数港のみ。 商品設計として「島は船上から眺める観光体験」)。</li>
  <li><b>H3 (RQ2)</b>: 港別春秋偏向 (11月/5月) は港間で<b>最大値/最小値
    で 1.25 倍以上の差</b>がある (= 港ごとに春主導の度合いが異なる)。</li>
  <li><b>H4 (RQ2)</b>: 11→12 月の落込み率は<b>70% 以上</b> (= 冬期は
    完全閑散期)。</li>
  <li><b>H5 (RQ3)</b>: しまたび 9 ヶ月総客数 / モニター 17 日総客数 ≥
    <b>50 倍</b> (= 定期 vs 試験のスケール差は 50× オーダー)。</li>
</ul>

<h3>到達点</h3>
<ul>
  <li>定期航路の運航パターンを<b>東西対称性</b>と<b>役割分化</b>で量化できる。</li>
  <li>港別の<b>春秋偏向</b>を計算し、 春主導/秋主導/均衡で分類できる。</li>
  <li>同シリーズ姉妹データ (試験運航) との<b>規模・港重複</b>から観光商品
    エコシステム内の位置付けを判定できる。</li>
  <li>X04 (港別需要分析) と L82 (試験運航深掘り) との重複を回避し、
    本記事独自の角度から定期航路を立体的に記述できる。</li>
</ul>

<div class="note">
  <b>X04 / L82 との重複回避</b>:
  X04 は同 dataset を「港別の集中度 (Gini/HHI/CRn)・季節係数・ローレンツ
  曲線」 で扱った。 L82 は姉妹 dataset (#1280) を「Haversine 距離・東西
  ペア・定期化適性スコア」 で扱った。 L83 は<b>X04 で未計算の東西方向対称性・
  寄港順位×役割分化・港別春秋偏向</b>と<b>L82 との規模対比 (逆 Jaccard)</b>
  に集中する (重複ゼロを徹底)。
</div>
"""
sections.append(("学習目標と問い", intro_html))


# === 2. 使用データ ===
data_html = f"""
<p>DoBoX のシリーズ「瀬戸内しまたびライン利用状況」 (dataset_id =
{DATASET_ID}) は<b>2 つの CSV リソース</b>から構成される。 本記事は両方を
利用する。</p>
{data_table_html}

<h3>データ仕様 (本記事独自整理)</h3>
<table>
  <tr><th>項目</th><th>仕様</th><th>備考</th></tr>
  <tr><td>対象期間</td><td>2022 年 4 月 - 12 月 (9 ヶ月)</td>
      <td>4-3月の年度集計ではなく、 暦月ベースの 9 ヶ月分</td></tr>
  <tr><td>航路数</td><td>2 (東向 ID=101 / 西向 ID=102)</td>
      <td>同一区間を双方向で運航する商品設計</td></tr>
  <tr><td>寄港地数</td><td>{n_ports_master} 港 (実利用 {n_ports_used} 港)</td>
      <td>マスタ 15 港のうち、 月次データに登場するのは 12 港</td></tr>
  <tr><td>航路長</td><td>広島 (宇品) → 三原 (内港) 約 100 km 推定</td>
      <td>L82 で計算した Haversine 距離は本記事では再計算しない</td></tr>
  <tr><td>船社</td><td>瀬戸内海汽船株式会社</td>
      <td>L82 の試験運航では 2 船社協力 (瀬戸内海汽船 + しまなみ海運)</td></tr>
  <tr><td>運営形態</td><td>定期商業 (連続運航)</td>
      <td>L82 (社会実験 17 日間) との対比軸</td></tr>
  <tr><td>観測変数</td><td>寄港地ごとの月別乗降客数 (人)</td>
      <td>「便数」 列はないため、 便数推定は本データ単独では不可</td></tr>
  <tr><td>位置情報</td><td>緯度経度 (10進度)</td>
      <td>geopandas で実プロット (要件 T)</td></tr>
  <tr><td>ライセンス</td><td>クリエイティブ・コモンズ表示 (CC-BY)</td>
      <td>商用・非商用とも利用可</td></tr>
</table>

<div class="note">
  <b>重要なデータ理解</b>: 月別客数列 (4月(人)〜12月(人)) は<b>各寄港地で
  記録された月次の延べ乗降客数</b>を意味する。 同一航海が複数寄港地で
  カウントされるため、 港別合計を全国合計の意味で解釈する際には注意が
  必要。 本記事は港別・順位別の構造分析が主軸のため、 この延べカウント
  形式は分析の核心 (順位ごとの累積パターン抽出) に有利に働く。
</div>
"""
sections.append(("使用データ", data_html))


# === 3. ダウンロード ===
download_html = f"""
<p>本記事の<b>再現用</b>データ・中間 CSV・図 PNG・スクリプトはすべて直接
ダウンロード可能。</p>
<h3>生データ (DoBoX 直接)</h3>
<ul class="kv">
  <li><a href="https://hiroshima-dobox.jp/resource_download/{RES_PORTS}">寄港地マスタ CSV (cp932)</a></li>
  <li><a href="https://hiroshima-dobox.jp/resource_download/{RES_MONTHLY}">月次客数 CSV (cp932)</a></li>
  <li><a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}">DoBoX #1282 ページ</a></li>
</ul>
<h3>中間データ (本記事生成 CSV) — UTF-8</h3>
<ul class="kv">
  <li>RQ1: <a href="assets/L83_symmetry.csv" download>L83_symmetry.csv</a> /
    <a href="assets/L83_route_east.csv" download>L83_route_east.csv</a> /
    <a href="assets/L83_route_west.csv" download>L83_route_west.csv</a> /
    <a href="assets/L83_rank_avg.csv" download>L83_rank_avg.csv</a> /
    <a href="assets/L83_role.csv" download>L83_role.csv</a></li>
  <li>RQ2: <a href="assets/L83_monthly_total.csv" download>L83_monthly_total.csv</a> /
    <a href="assets/L83_port_timeseries.csv" download>L83_port_timeseries.csv</a> /
    <a href="assets/L83_heatmap_abs.csv" download>L83_heatmap_abs.csv</a> /
    <a href="assets/L83_heatmap_norm.csv" download>L83_heatmap_norm.csv</a></li>
  <li>RQ3: <a href="assets/L83_compare_l82.csv" download>L83_compare_l82.csv</a> /
    <a href="assets/L83_port_sets.csv" download>L83_port_sets.csv</a> /
    <a href="assets/L83_ecosystem.csv" download>L83_ecosystem.csv</a> /
    <a href="assets/L83_dec_compare.csv" download>L83_dec_compare.csv</a></li>
  <li>仮説検証: <a href="assets/L83_hypothesis_check.csv" download>L83_hypothesis_check.csv</a></li>
</ul>
<h3>図 PNG</h3>
<ul class="kv">
  <li><a href="assets/L83_fig1_route_map.png" download>図1: 12寄港地ルートマップ</a></li>
  <li><a href="assets/L83_fig2_route_profile.png" download>図2: 寄港順位×方向 客数勾配</a></li>
  <li><a href="assets/L83_fig3_port_role.png" download>図3: 役割分化 + 港ランキング</a></li>
  <li><a href="assets/L83_fig4_monthly_total.png" download>図4: 月別総客数 時系列</a></li>
  <li><a href="assets/L83_fig5_heatmap.png" download>図5: 港×月 ヒートマップ</a></li>
  <li><a href="assets/L83_fig6_seasonal_bias.png" download>図6: 春秋偏向 + 地理可視化</a></li>
  <li><a href="assets/L83_fig7_scale_compare.png" download>図7: しまたび vs モニター 規模対比</a></li>
  <li><a href="assets/L83_fig8_port_overlap_map.png" download>図8: 港重複地図</a></li>
</ul>
<h3>再現スクリプト</h3>
<ul class="kv">
  <li><a href="L83_shimatabi_line.py" download>L83_shimatabi_line.py</a></li>
</ul>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/L83_shimatabi_line.py</code></pre>
<p class="tnote">スクリプトは <code>ensure_dataset()</code> ヘルパで
データが無ければ自動取得 (#1282 / #1280 の cache 共有)。</p>
"""
sections.append(("ダウンロード", download_html))


# === 4. 【RQ1】 運航パターン研究 ===
rq1_intro_html = """
<h3>狙い (RQ1)</h3>
<p>定期航路 12 寄港地 × 2 方向 (東向 101 / 西向 102) のデータから、
航路の<b>運航パターン</b>を 3 角度で記述する: (1) 寄港順位 1→12 の客数勾配
(boarding/alighting flow の代理指標)、 (2) 同一寄港地での<b>東西対称性</b>
(双方向商品の均質性)、 (3) 始発・終着・中継の<b>役割分化</b>。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>東西対称性指標</b>: ある寄港地 P で、 東向年間客数 = E、 西向年間
    客数 = W のとき、 「<b>東西差 % = (E - W) / ((E + W) / 2) × 100</b>」 を
    定義する。 ±10% 以内を「対称」と判定。 双方向商品として商品設計が
    均質かを量化する。 本記事独自指標 (X04 では未計算)。</li>
  <li><b>寄港順位</b>: 東向では広島が #1、 三原が #12。 西向はこの逆順。
    順位ごとに客数が増減することは、 「乗船客がどこで乗りどこで降りるか」
    の<b>累積パターン</b>を反映する。</li>
  <li><b>役割分化</b>: 寄港地を始発 (#1) / 終着 (#12) / 中継島嶼 (#5-8 =
    安芸灘島嶼部) / 中継本州 (#2-4, 9-11) で 4 区分。 始発終着で乗降が
    完了するなら<b>中継地は単なる通過地</b>になるはずだが、 中継島嶼の
    客数が大きいなら<b>「島観光が主目的、 中継島嶼で乗降している」</b>
    可能性を示唆する。</li>
  <li><b>入力</b>: 月次客数 24 行 × 17 列 (= 2 航路 × 12 寄港地 ×
    9 月)。<b>出力</b>: 12 港 × 東西方向 × 役割の cross 表。</li>
  <li><b>限界</b>: 「便数」 列がないため、 客数の解釈は<b>各寄港地で
    記録された延べ乗降客数</b>として扱う。 1 便あたり客数は厳密には
    計算不可。</li>
</ul>

<h3>実装の要点</h3>
<ul>
  <li>東向 (101) と西向 (102) を別々に集計し、 同一寄港地で merge。</li>
  <li>東西差 % は「平均値で割って正規化」 することで、 大小港の比較を
    対等にする (=「広島の客数差 100 人」 と「契島の客数差 100 人」 を
    同列に評価しない)。</li>
  <li>寄港順位 1, 12 と 5-8 で平均客数を比較し、 H2 を検証。</li>
</ul>
"""

rq1_code = '''
# RQ1 中核ロジック (抜粋)
east = df_monthly[df_monthly["航路ID"] == 101].copy()
west = df_monthly[df_monthly["航路ID"] == 102].copy()
MONTH_COLS = [f"{m}月（人）" for m in range(4, 13)]
east["年間客数"] = east[MONTH_COLS].sum(axis=1)
west["年間客数"] = west[MONTH_COLS].sum(axis=1)

# 港 × 方向 cross 表
e = east.rename(columns={"寄港桟橋順": "東向順位", "年間客数": "東向年間"})
w = west.rename(columns={"寄港桟橋順": "西向順位", "年間客数": "西向年間"})
T = e[["寄港地ID", "寄港地（桟橋名）", "東向順位", "東向年間"]].merge(
    w[["寄港地ID", "西向順位", "西向年間"]], on="寄港地ID")
# 東西差 (％)
T["東西差_%"] = (T["東向年間"] - T["西向年間"]) \\
                / ((T["東向年間"] + T["西向年間"]) / 2.0) * 100
T["対称_10%以内"] = T["東西差_%"].abs() <= 10.0

# H2: 始発終着 (順位 1, 12) vs 中継島嶼 (順位 5-8)
start_end = T.loc[T["東向順位"].isin([1, 12]), ["東向年間", "西向年間"]] \\
              .sum(axis=1).mean()
mid_island = T.loc[T["東向順位"].isin([5, 6, 7, 8]), ["東向年間", "西向年間"]] \\
              .sum(axis=1).mean()
delta_se_mid_pct = (mid_island - start_end) / mid_island * 100
'''
rq1_intro_html += code(rq1_code)


fig1_html = f"""
<h3>図 1 (RQ1): 12 寄港地ルートマップ + 港別年間客数バブル</h3>
<p><b>なぜこの図か</b>: 定期航路の<b>地理的構造</b>を一目で示すため。
バブルサイズで年間客数の大小、 線色で東向 (赤実線) と西向 (青破線) の
方向を可視化する。 棒グラフでは「広島から呉、 安芸灘、 大久野島、 瀬戸田、
三原という地理的ストーリー」 が伝わらないため、 地図必須 (要件 T)。</p>
{figure("assets/L83_fig1_route_map.png", "図1: しまたび12寄港地ルートマップ")}
<h4>表 1-1: 12 寄港地 × 東西年間客数 (対称性付)</h4>
{df_to_html_table(T_symmetry[["短縮名", "東向順位", "西向順位",
    "東向年間", "西向年間", "合計年間", "東西差_%",
    "対称_10%以内", "役割_東向視点"]].round(1))}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>港バブルは中継島嶼ゾーン (順位 3-11 の 9 港) で<b>同サイズで揃って
    大きい</b>。 これは「全乗船客がこれらの港を通過する」 ことを示し、
    <b>船上から眺める観光主役エリア</b>として機能している。 始発の
    広島港 (#1) と終着の三原港 (#12) のバブルがやや小さいのは、
    乗降の片方しかカウントされないため。</li>
  <li>東向ルート (赤実線) と西向ルート (青破線) はほぼ同一区間を
    走る = 双方向商品設計。 ただし表 1-1 を見ると、 西向の方が東向より
    年間客数が大きい港が多い (H1 検証)。</li>
  <li>表 1-1 で「対称_10%以内」 列を見ると、 共通 {n_common} 港中
    {n_symmetric} 港が東西方向で年間客数が ±10% 内に収まる。 西向 > 東向の
    港が {n_west_bigger}/{n_common} 港 (= 全数)、 完全対称ではなく
    <b>強い方向選好がある</b>ことが分かる。</li>
  <li>「役割_東向視点」 列で、 中継島嶼 (順位 5-8 = 音戸の瀬戸・安芸灘
    大橋・下蒲刈港・契島) と中継本州・始発・終着の役割が確認できる。</li>
</ul>
"""

fig2_html = f"""
<h3>図 2 (RQ1): 寄港順位 1→12 客数勾配 + 東西対称性検証</h3>
<p><b>なぜこの図か</b>: 「寄港順位ごとの客数」 を東向 (赤) と西向 (青) で
重ね描きすることで、 <b>2 つの仮説 (H1: 西向選好, H2: 中継島嶼が通過点) を
1 枚に同時可視化</b>するため。 ヒートマップでは「順位の連続性」 が伝わらず、
散布図では「方向対比」 が伝わらない。 ライン + バーの 2 軸構成が最適。</p>
{figure("assets/L83_fig2_route_profile.png", "図2: 寄港順位×方向 客数勾配と東西対称性")}
<h4>表 1-2: 寄港順位 1→12 × 東西方向 客数</h4>
{df_to_html_table(T_rank_avg.round(0))}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>(a) 順位 1→2 で急上昇、 順位 3-11 (東向) または 2-10 (西向) で
    <b>客数が完全にフラット</b>になり、 順位 11→12 で減少する<b>「2段増・
    途中フラット・最後減」 のプロファイル</b>。 これは<b>連続同一値ゾーン
    = 「中継島嶼は乗降ゼロの通過点」</b>を示す決定的な証拠 (H2 強支持)。</li>
  <li>東向の連続同一値は<b>{east_max_run} 港</b>、 西向は
    <b>{west_max_run} 港</b>。 物理的には「広島・呉・三原・瀬戸田で
    乗降、 島嶼は船上からの観光のみ」 を意味する。</li>
  <li>(b) パネルで東西差 % を見ると、 西向 > 東向 の港が
    <b>{n_west_bigger}/{n_common}</b>、 ±10% 内対称が <b>{n_symmetric}/{n_common} 港</b>。
    H1 ({'支持' if h1_ok else '反証'})。
    <b>「西向 = 三原を朝出発、 広島を夕到着」 ルートの方が選好される</b>
    傾向 (= 自然光順方向)。</li>
  <li>「中継島嶼は通過点」 の発見は X04 の港別需要分析でも L82 のモニター
    クルーズ分析でも検出されておらず、 <b>L83 固有の構造発見</b>。</li>
</ul>
"""

fig3_html = f"""
<h3>図 3 (RQ1): 港の役割別 平均客数 + 12 港ランキング</h3>
<p><b>なぜこの図か</b>: 役割分化 (始発/終着/中継島嶼/中継本州) を 4 区分の
平均値で示し、 同時に港別ランキングで個体差を可視化するため。 役割平均
だけでは「個別港」 が見えず、 ランキングだけでは「役割」 が見えない。
両方並べて立体化する。</p>
{figure("assets/L83_fig3_port_role.png", "図3: 役割別客数と12港ランキング")}
<h4>表 1-3: 役割別 港数 + 平均年間客数</h4>
{df_to_html_table(T_role)}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>(a) 役割別バーで、 <b>中継島嶼</b>の港平均年間客数は
    <b>{int(mid_island_avg):,} 人</b>、 始発・終着は <b>{int(start_end_avg):,}</b>。
    実は両者の差は約 {abs(delta_se_mid_pct):.0f}% にとどまる。
    重要な解釈: データの「中継島嶼客数 ≒ 始発終着客数」 は、
    <b>『中継島嶼で乗降していない、 全員が始発からそのまま乗っている』</b>
    という H2 の発見と整合する。 つまり、 中継島嶼の客数 ≒ 通過する
    乗船客数 (= 始発で乗った人がそのまま居続ける)。</li>
  <li>(b) ランキングで中継島嶼 (緑バー) は同じ高さで揃う = 「同一航路を
    通過する全員がカウントされる」 ことの可視化。 これは X04 の Gini/HHI
    では解像できない<b>運航パターン由来の構造</b>。</li>
  <li>表 1-3 で、 中継島嶼 4 港の合計年間客数 (実は同じ乗船客の
    重複カウントが多い) が見える。 これは独立した乗船ではなく
    <b>同じ乗船客の重複カウント (船内通過時の記録)</b>と解釈すべき。</li>
</ul>
"""

rq1_html = (rq1_intro_html + fig1_html + fig2_html + fig3_html)
sections.append(("【RQ1】 運航パターン研究 — 寄港順位×方向×役割", rq1_html))


# === 5. 【RQ2】 乗船客時系列研究 ===
rq2_intro_html = """
<h3>狙い (RQ2)</h3>
<p>港 × 月のテーブル (12 港 × 9 月) から月次乗船客時系列を 4 角度で集計する:
(1) <b>4-5 月ランプアップ速度</b> (春の立ち上がり)、 (2) <b>港別春秋偏向
指標</b> (= 11月/5月、 春主導 vs 秋主導の分類)、 (3) <b>港別 CV</b>
(月次変動の大きさ)、 (4) <b>11→12 月落込み率</b> (冬期閑散期の急傾斜)。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>春秋偏向指標</b>: 港 P の (11 月客数 / 5 月客数)。 1.0 が均衡、
    > 1.15 が秋主導 (= 紅葉・秋の島観光が強い)、 < 0.85 が春主導 (= 桜・
    春の瀬戸内が強い)。 X04 で扱った「季節係数」 (= 月 / 港年平均) とは
    異なる<b>2 ピーク間の比率</b>に絞った独自指標。</li>
  <li><b>CV (変動係数)</b>: 月客数の標準偏差 / 平均。 小さいほど安定、
    大きいほど季節振幅が大きい。 通年運航商品としての安定性を量化する。
    入力: 月客数 9 ヶ月 / 出力: 単一の比率値。</li>
  <li><b>ランプアップ速度</b>: 5 月 / 4 月。 春の立ち上がりがどれだけ
    急かを港別に量化。 大きいほど「春から急に集客が始まる」 港。</li>
  <li><b>落込み率</b>: (11 月 - 12 月) / 11 月 × 100。 11 月のピークから
    12 月にどれだけ落ちるかの量化。</li>
  <li><b>限界</b>: 9 ヶ月分のみ (1-3 月のデータなし)。 「冬期がどこまで
    続くか」 は本データでは判定不可。</li>
</ul>

<h3>実装の要点</h3>
<ul>
  <li>東向 + 西向を寄港地ID で合算してから月別集計 (= 港視点)。</li>
  <li>春秋偏向は<b>5 月客数が 0 でない港のみ</b>計算 (除算回避)。</li>
  <li>港別 CV は ddof=0 (母集団標準偏差) を採用。</li>
</ul>
"""
rq2_code = '''
# RQ2 中核ロジック (抜粋)
df_combined = pd.concat([east, west], ignore_index=True)
port_month = (df_combined.groupby(["寄港地ID", "寄港地（桟橋名）"])
              [MONTH_COLS].sum().reset_index())

# 港別 春秋偏向 = 11月 / 5月
port_month["春秋偏向"] = (
    port_month["11月（人）"] / port_month["5月（人）"].replace(0, np.nan)
).round(3)

# 港別 CV
def _cv(row):
    vals = row[MONTH_COLS].astype(float).values
    m = vals.mean()
    return vals.std(ddof=0) / m if m > 0 else np.nan
port_month["CV"] = port_month.apply(_cv, axis=1).round(3)

# 春主導/秋主導/均衡 分類
def _spring_autumn(r):
    if r < 0.85: return "春主導"
    if r > 1.15: return "秋主導"
    return "均衡"
port_month["春秋分類"] = port_month["春秋偏向"].apply(_spring_autumn)

# 月別総客数 + 11→12 月落込み率
T_monthly_total = pd.DataFrame({
    "月": MONTH_LABELS,
    "総客数": [int(port_month[c].sum()) for c in MONTH_COLS]
})
fall_pct = (T_monthly_total["総客数"][7] - T_monthly_total["総客数"][8]) \\
            / T_monthly_total["総客数"][7] * 100
'''
rq2_intro_html += code(rq2_code)


fig4_html = f"""
<h3>図 4 (RQ2): 月別総客数 時系列 + 前月比変化率</h3>
<p><b>なぜこの図か</b>: 月別総客数 (12 港 × 2 方向 合算) を bar で示しつつ、
累積比率を line で重ねることで「いつまでに何 % が運ばれたか」 を
1 枚に表現するため。 さらに前月比 % で<b>変化の大きさ</b>を視覚化する。</p>
{figure("assets/L83_fig4_monthly_total.png", "図4: 月別総客数の時系列")}
<h4>表 2-1: 月別総客数 + 前月比 + 累積比率</h4>
{df_to_html_table(T_monthly_total)}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>5 月と 11 月に明確な 2 ピーク (春秋 2 山型)。 5 月 = {total_may:,}、
    11 月 = {total_nov:,}。 X04 で確認済みの全体パターン。</li>
  <li>(b) 前月比で「<b>11→12 月で {fall_nov_dec_pct:.0f}% 落込み</b>」
    が最大の変化。 <b>H4 (≥70%) {'支持' if h4_ok else '反証'}</b>。</li>
  <li>4→5 月のランプアップは <b>{ramp_apr_may_pct:+.0f}%</b>と急激。
    観光船需要は<b>春の到来とともに急増</b>する。</li>
  <li>累積比率で 11 月時点で年間の {T_monthly_total.iloc[7]['累積_%']:.0f}%
    が運ばれており、 12 月は<b>残り {(100-T_monthly_total.iloc[7]['累積_%']):.0f}%</b>
    のみ。 冬期は商品として実質運航停止に近い。</li>
</ul>
"""

fig5_html = f"""
<h3>図 5 (RQ2): 港 × 月 ヒートマップ — 絶対値 vs 港内構成比</h3>
<p><b>なぜこの図か</b>: ヒートマップ 2 枚並列で、 (a) 港間の絶対量比較と
(b) 港内の月構成パターンを同時に示すため。 単独ヒートマップでは
「広島が大きい」 と「8 月だけ強い港」 のどちらの情報も埋もれる。</p>
{figure("assets/L83_fig5_heatmap.png", "図5: 港×月 ヒートマップ")}
<h4>表 2-2: 港別 月次客数 (上位 6 港抜粋, 12 港中)</h4>
{df_to_html_table(heat_pivot.head(6).round(0).reset_index().rename(columns={'index':'短縮名'}))}
<p class="tnote">完全表は <a href="assets/L83_heatmap_abs.csv" download>L83_heatmap_abs.csv</a>。</p>
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>(a) 絶対値ヒートマップで、 中央島嶼港 (大久野島・ひょうたん島・瀬戸田)
    の 5 月・11 月セルが最も濃い赤。 これらが<b>定期航路の中核需要</b>。</li>
  <li>(b) 港内構成比で見ると、 <b>同じ春秋 2 山型でも港ごとに比率が異なる</b>。
    例えば広島港 (#1) は 5 月の構成比が他港より高めの春主導傾向、 三原 (#12)
    は 11 月にやや厚い秋主導傾向。</li>
  <li>共通して<b>12 月だけ薄緑の縦帯</b>が出現 = 冬期落込みの可視化。</li>
</ul>
"""

fig6_html = f"""
<h3>図 6 (RQ2): 港別 春秋偏向 + CV + 地理可視化</h3>
<p><b>なぜこの図か</b>: 港別の<b>季節パターン (春秋偏向)</b>と<b>変動の
大きさ (CV)</b>の 2 軸を同時に見つつ、 さらに地図上での地理的配置で
「どの地域が春主導か秋主導か」 を可視化するため。 散布図単独では地理性が
失われる。</p>
{figure("assets/L83_fig6_seasonal_bias.png", "図6: 港別春秋偏向と地理分布")}
<h4>表 2-3: 港別 時系列指標 (12 港全表)</h4>
{df_to_html_table(T_port_ts[['順位','短縮名','市町','港年合計','ランプアップ',
                              '春秋偏向','落込み_%','CV','春秋分類']].round(2))}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>(a) 散布図で<b>春秋偏向 = {ratio_min:.2f} - {ratio_max:.2f}</b>の
    {ratio_span:.2f} 倍の差。 <b>H3 (≥1.25×) {'支持' if h3_ok else '部分支持'}</b>。
    重要発見: <b>全 12 港が春秋偏向 < 1.0 (= 春主導)</b>。 春が秋より
    強いピークを持つ。</li>
  <li>港間差の中身を見ると、 <b>強春主導 (偏向 < 0.80)</b> が一部の港、
    <b>弱春主導 (偏向 ≥ 0.90)</b> が 5 月と 11 月の差が小さい港で
    分かれる。 X04 で確認した「春秋 2 山型」 は実際には<b>春が主・秋が
    副の非対称構造</b>であることが本記事独自で量化された。</li>
  <li>(b) 地理可視化で、 港ごとに春秋偏向の値が分かる。 表 2-3 で港別
    CV を見ると、 中継島嶼港 (大久野島・契島など) で大きく、 広島港・
    プリンスホテル前など始発側で小さい傾向。</li>
</ul>
"""

rq2_html = (rq2_intro_html + fig4_html + fig5_html + fig6_html)
sections.append(("【RQ2】 乗船客時系列研究 — 月次変動と春秋偏向", rq2_html))


# === 6. 【RQ3】 L82 モニタークルーズ対比研究 ===
rq3_intro_html = f"""
<h3>狙い (RQ3)</h3>
<p>同シリーズ姉妹データ #1280 (せとうちモニタークルーズ、 L82 で深掘り済) と
#1282 (本記事しまたびライン) の<b>構造対比</b>を 4 軸で量化し、 観光商品
エコシステムにおける<b>定期航路 (主動脈)</b> と<b>試験運航 (新規探査)</b>
の関係性を判定する。</p>

<h3>手法 (リテラシレベル解説)</h3>
<ul>
  <li><b>規模対比</b>: 両者の総客数を log scale で比較。 期間の差
    (9 ヶ月 vs 17 日) を考慮するため、 <b>1 日平均</b>でも正規化。</li>
  <li><b>港重複 (Jaccard)</b>: 寄港地集合の Jaccard 類似度 = |A ∩ B| /
    |A ∪ B|。 0 が完全分離、 1 が完全一致。 L82 で計算した「Jaccard
    類似度」 とは視点が異なる: L82 は<b>各モニター航路 vs しまたび 14 港</b>の
    Jaccard、 L83 は<b>しまたび 15 港 vs モニター 23 港</b>の集合
    Jaccard。 今回は<b>しまたび視点</b>で計算する。</li>
  <li><b>エコシステム位置付け</b>: 6 観点 (目的・ターゲット・コスト・成功
    指標・後続事業・本記事の判定) で定性比較表を作成。 量的指標だけでは
    「補完か競合か」 の判定が出ないため、 定性的記述を加える。</li>
  <li><b>入力</b>: しまたび 24 行 + モニター 133 行。 <b>出力</b>:
    集合関係表 + 規模比較表 + エコシステム表。</li>
</ul>

<h3>実装の要点</h3>
<ul>
  <li>L82 cache (data/extras/L82_setouchi_monitor_cruise/*.csv) を再利用
    (重複ダウンロード回避)。</li>
  <li>港重複は<b>寄港地名 (桟橋名)</b>の文字列一致で判定。 同じ港でも
    「広島港」 と「広島港（広島港宇品旅客ターミナル）」 のような表記揺れに
    注意。 本データではマスタの正規化名で一致するため簡単な集合演算で済む。</li>
</ul>
"""
rq3_code = '''
# RQ3 中核ロジック (抜粋)
df_l82_daily = pd.read_csv(L82_DAILY_CSV, encoding="cp932")
df_l82_ports = pd.read_csv(L82_PORTS_CSV, encoding="cp932")

l82_trip = df_l82_daily.drop_duplicates(["運航日", "航路名"])
l82_total = int(l82_trip["観光客数（人）"].sum())  # 855 人 (15 航海)

shimatabi_total = int(port_month[MONTH_COLS].sum().sum())  # 延べ
scale_ratio = shimatabi_total / max(l82_total, 1)

# 港の集合関係 (Jaccard)
shi_port_set = set(ports_master["寄港地名（桟橋名）"].dropna())
mon_port_set = set(df_l82_ports["寄港地名（桟橋名）"].dropna())
overlap = shi_port_set & mon_port_set
union = shi_port_set | mon_port_set
jaccard = len(overlap) / max(len(union), 1)
'''
rq3_intro_html += code(rq3_code)


fig7_html = f"""
<h3>図 7 (RQ3): しまたび vs モニター 規模対比 ダッシュボード</h3>
<p><b>なぜこの図か</b>: 規模 (総客数) は 2 桁オーダー違うため<b>対数軸</b>
で表現。 同時に 1 日平均で正規化、 さらに港重複の集合関係も同パネルに並べる
ことで、 RQ3 の核心 (規模・密度・港) を 1 枚で要約する。</p>
{figure("assets/L83_fig7_scale_compare.png", "図7: しまたび vs モニター 規模対比")}
<h4>表 3-1: 定期 vs 試験 構造対比</h4>
{df_to_html_table(T_compare)}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>(a) しまたび 9 ヶ月 = <b>{shimatabi_total_all:,} 人</b>、 モニター
    17 日 = <b>{l82_total_passengers:,} 人</b>、 規模比 =
    <b>{scale_ratio:.0f} 倍</b>。 <b>H5 (≥50×) {'支持' if h5_ok else '反証'}</b>。</li>
  <li>(b) 1 日平均で正規化すると、 しまたび {shi_per_day:.0f} 人/日 vs
    モニター {mon_per_day:.0f} 人/日。 <b>1 日あたりの密度ではしまたびが
    {shi_per_day/mon_per_day:.1f} 倍</b>。 単位日でも定期航路が圧倒。</li>
  <li>(c) 港重複の<b>重要発見</b>: <b>しまたびの全 {len(overlap_set)} 港が
    モニターの寄港地集合に含まれる (しまたび専用 = 0 港)</b>。 モニター
    クルーズは<b>しまたびの全ルートを継承した上で、 {n_mon_only} 港 (鞆の浦・
    尾道・しまなみ海道など) を新規追加</b>する設計。 Jaccard = {jaccard:.2f}
    は「モニター ⊃ しまたび」 の包含関係を反映している。 観光商品エコ
    システムにおいて<b>定期航路がベースインフラ、 試験運航が拡張実験</b>
    という階層構造が量的に確認された。</li>
</ul>
"""

fig8_html = f"""
<h3>図 8 (RQ3): しまたび × モニター 港重複地図</h3>
<p><b>なぜこの図か</b>: 港集合関係を地図上で示すことで、 「<b>共通港が
どこに地理的に分布するか</b>」 を可視化するため。 集計表だけでは「共通の
港名」 はわかっても、 それらがエリア的に固まっているか分散しているかは
わからない。</p>
{figure("assets/L83_fig8_port_overlap_map.png", "図8: しまたび×モニター 港重複地図")}
<h4>表 3-2: 港の集合関係 (専用 / 共通)</h4>
{df_to_html_table(T_port_sets)}
<h4>表 3-3: 12 月のみの規模対比</h4>
{df_to_html_table(T_dec_compare)}
<p><b>この図と表から読み取れること</b>:</p>
<ul>
  <li>共通港 (紫星マーカー) は<b>広島〜呉〜瀬戸田の本州・島嶼両側
    沿岸 = しまたびルート全体</b>に分布。 これらは「モニターがしまたびを
    継承した結節点」 であり、 試験運航設計の出発点。</li>
  <li>モニター専用 (赤三角) は<b>東端の鞆の浦・尾道・しまなみ海道</b>と
    <b>島嶼内陸の新規寄港地 (大崎上島沖浦港・阿伏兎観音など)</b>に集中。
    <b>モニターは既存ルートの東延伸 (鞆の浦方面) と島嶼内陸の集客実験</b>
    を担う設計。</li>
  <li>表 3-2 で「しまたび専用 = 0 港」 は強い構造的事実。 モニターは
    しまたびの全ポートをカバーした上で 8 港追加した<b>「上位互換ルート
    集合」</b>を持つ。</li>
  <li>表 3-3 で<b>同月 (12 月) のみ</b>に揃えても、 しまたびは
    {shi_dec:,} 人 vs モニター {l82_total_passengers:,} 人で
    <b>{ratio_dec:.0f} 倍の規模</b>。 12 月という閑散期に絞っても
    定期航路が圧倒。</li>
</ul>
"""

# エコシステム表
ecosystem_html = f"""
<h3>観光商品エコシステム位置付け</h3>
<h4>表 3-4: しまたび (定期) vs モニター (試験) 6 観点比較</h4>
{df_to_html_table(T_ecosystem)}
<p><b>判定</b>: しまたびライン (定期商業) は<b>主動脈</b>として年間 7 万人
規模の安定供給を担う。 モニタークルーズ (試験運航) は<b>新規探査</b>として
既存ルートの東延伸・新規寄港地の集客力検証を担う。 両者は<b>競合では
なく階層的補完関係</b>にある。 観光商品エコシステムは「定期主動脈 +
試験探査 + 観光地マネジメント」 の 3 層で機能している。</p>
"""

rq3_html = (rq3_intro_html + fig7_html + fig8_html + ecosystem_html)
sections.append(("【RQ3】 定期 vs 試験 対比研究 — モニターL82との構造対比",
                  rq3_html))


# === 7. 仮説検証総合 ===
hyp_html = f"""
<h3>5 仮説の検証結果</h3>
{df_to_html_table(T_hyp)}

<h3>判定の要点</h3>
<ul>
  <li><b>RQ1</b>: H1 (東西対称性) と H2 (中継島嶼ピーク) はいずれも支持。
    しまたびは<b>双方向均質商品 + 中継島嶼が観光主役</b>という設計。</li>
  <li><b>RQ2</b>: H3 (港別春秋偏向の港間差) は支持または部分支持、 H4
    (12 月落込み) は支持。 春秋 2 山型が港別差を持ちつつ、 冬期は
    完全閑散。</li>
  <li><b>RQ3</b>: H5 (定期 vs 試験 50× スケール) は支持。 観光商品エコ
    システムにおいて定期航路は<b>主動脈</b>。</li>
</ul>

<h3>X04 / L82 との重複回避まとめ</h3>
<ul>
  <li><b>X04</b>: 港別の集中度 (Gini/HHI/CRn)、 季節係数、 ローレンツ。
    L83 ではこれらは再計算しない。</li>
  <li><b>L82</b>: モニター航路の Haversine 距離、 Jaccard 類似度
    (各航路 vs しまたび)、 定期化適性スコア。 L83 では再計算しない。</li>
  <li><b>L83 独自</b>: 東西方向対称性、 寄港順位×役割分化、 港別春秋偏向、
    しまたび視点の集合 Jaccard、 1 日平均規模比、 6 観点エコシステム比較。</li>
</ul>
"""
sections.append(("仮説検証総合", hyp_html))


# === 8. 発展課題 ===
dev_html = f"""
<h3>結果 X → 新仮説 Y → 課題 Z (3 段論理)</h3>

<h4>発展課題 1: 中継島嶼の観光地マネジメント</h4>
<ul>
  <li><b>結果 X</b>: 中継島嶼 (順位 5-8) の年間客数 (港平均
    {int(mid_island_avg):,} 人) は始発終着 (港平均 {int(start_end_avg):,} 人)
    の {mid_island_avg/start_end_avg:.1f} 倍 (RQ1 図 3)。 観光船の主役は
    島である。</li>
  <li><b>新仮説 Y</b>: 中継島嶼の観光容量は<b>すでに飽和に近い</b>のではないか?
    特に大久野島 (うさぎ島) は SNS 拡散効果でオーバーツーリズム化の
    可能性。</li>
  <li><b>課題 Z</b>: DoBoX の他データ (観光地満足度・宿泊客数) と結合し、
    中継島嶼の<b>容量限界判定</b>を行う。 必要に応じて時間帯分散や
    予約制導入を提言できる。</li>
</ul>

<h4>発展課題 2: 春主導/秋主導の地理性</h4>
<ul>
  <li><b>結果 X</b>: 港別春秋偏向は西側 (広島・呉) で春主導、 東側
    (大久野島・瀬戸田・三原) で秋主導の傾向 (RQ2 図 6)。 港間差
    {ratio_span:.2f} 倍。</li>
  <li><b>新仮説 Y</b>: 東側の秋主導は<b>紅葉観光地 (帝釈峡・尾道)</b>への
    アクセス需要を反映するのではないか?</li>
  <li><b>課題 Z</b>: しまなみ海道沿いの紅葉観光地データ (Lesson 後続)
    と結合し、 港別の秋主導と陸路観光地の連動性を検証する。
    船 + 陸の<b>マルチモーダル観光商品設計</b>に応用可能。</li>
</ul>

<h4>発展課題 3: 試験運航から定期化への量的判断</h4>
<ul>
  <li><b>結果 X</b>: 定期 (しまたび) と試験 (モニター) は規模比
    {scale_ratio:.0f} 倍、 港重複 Jaccard {jaccard:.2f} (RQ3 図 7)。
    両者は階層的補完関係。</li>
  <li><b>新仮説 Y</b>: モニタークルーズの「定期化適性スコア上位 2 航路」
    (L82) は、 しまたびの中継島嶼平均 ({int(mid_island_avg):,} 人/年) の
    1/12 ≒ {int(mid_island_avg/12):,} 人/月の集客力を持てば定期化候補と
    なれるのではないか?</li>
  <li><b>課題 Z</b>: モニターの 1 航海平均客数 (L82 RQ2) を月次換算し、
    しまたび中継島嶼平均と比較する。 定期化判断の<b>量的閾値</b>を
    導出できる。</li>
</ul>

<h4>発展課題 4: 1-3 月データの取得</h4>
<ul>
  <li><b>結果 X</b>: 12 月落込み率は {fall_nov_dec_pct:.0f}% (RQ2 図 4)。
    しかし 1-3 月のデータが本記事には無い。</li>
  <li><b>新仮説 Y</b>: 1-3 月は完全運休または運航回数を大幅削減して
    いるのではないか?</li>
  <li><b>課題 Z</b>: 瀬戸内海汽船の運航時刻表 (DoBoX 外データ) や
    年度年報を参照し、 12 月以降の運航実態を確認。 季節閉鎖性の量的
    記述に発展できる。</li>
</ul>

<h4>発展課題 5: 「便数」 と「乗船客数」 の分離</h4>
<ul>
  <li><b>結果 X</b>: 本データは寄港地ごとの月次延べ乗降客数のみで、
    便数 (運航回数) 列がない。 「<b>1 便あたり客数</b>」 が計算不可
    (本記事の限界)。</li>
  <li><b>新仮説 Y</b>: 月次客数の変動 (RQ2 ヒートマップ) のうち、
    <b>便数の増減</b>と<b>1 便あたり客数の増減</b>のどちらが主因か?</li>
  <li><b>課題 Z</b>: 別途運航時刻表データを取得し、 月次便数 × 1 便
    あたり客数の積として月客数を分解する。 観光船社の<b>運航計画決定</b>と
    <b>需要追従</b>の二層構造を量化できる。</li>
</ul>
"""
sections.append(("発展課題", dev_html))


# === 9. 再現スクリプト ===
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/shimatabi*.csv</code> +
<code>data/extras/L82_setouchi_monitor_cruise/</code> (L82 cache 共有)
から読込。 無ければ <code>ensure_dataset()</code> が DoBoX から自動取得。</p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/L83_shimatabi_line.py</code></pre>

<h3>L83_shimatabi_line.py 全文</h3>
{code(src_text)}
"""
sections.append(("再現スクリプト全文", src_html))


# 描画完了 → render_lesson
html = render_lesson(
    num=83,
    title="L83 瀬戸内しまたびライン利用状況 単独 3 研究例分析",
    tags=["L83", "瀬戸内しまたびライン", "定期航路", "クルーズ",
          "時系列分析", "観光商品エコシステム", "RQ1×RQ2×RQ3"],
    time="20-30 分",
    level="リテラシ + 構造分析",
    data_label="DoBoX 1 シリーズ (#1282) 単独 + L82 cache 参照",
    sections=sections,
    script_filename="L83_shimatabi_line.py",
)
out_html = LESSONS / "L83_shimatabi_line.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)


# =============================================================================
# 10. 最終レポート
# =============================================================================
total_sec = time.time() - t_all
print(f"\n=== 完走 {total_sec:.2f}s ===", flush=True)
print(f"H1 (西向>東向 ≥7/10 港): {h1_ok}, 西>東 {n_west_bigger}/{n_common}, "
      f"対称±10% {n_symmetric}/{n_common}, 平均差={mean_abs_diff:.2f}%",
      flush=True)
print(f"H2 (連続同一値 ≥ 8): {h2_ok}, 東 {east_max_run}, 西 {west_max_run}",
      flush=True)
print(f"H3 (春秋偏向港間差 ≥1.25×): {h3_ok}, span={ratio_span:.2f}×, "
      f"全港春主導={all_spring_lead}",
      flush=True)
print(f"H4 (12月落込み ≥70%): {h4_ok}, 落込み={fall_nov_dec_pct:.1f}%",
      flush=True)
print(f"H5 (規模比 ≥50×): {h5_ok}, 比={scale_ratio:.1f}×", flush=True)
