Lesson 305

X05 しまたびライン × 離島避難所カバレッジ — 空間需給バランス研究

観光防災離島需給バランスバブル比率指標X-tier
所要 60〜80分 / 想定レベル: L01 水準 (中級) / データ: DoBoX #42 #1281 #1282 + dataset_index.csv

データ取得手順

⚠️ このスクリプトは自動取得に対応していません。以下のデータセットを DoBoX から手動でダウンロードし、data/extras/ 以下に保存してください。

IDデータセット名
#42避難所情報
#222dataset #222
#666dataset #666
#1281瀬戸内海の航路情報
#1282瀬戸内しまたびライン利用状況

実行コマンド:

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

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

学習目標と問い

【本記事のスタイル: 空間需給バランス研究】 比率指標とバブルプロットで島の需給ギャップを可視化する。 散布図行列・PCA は使わず、「観光客需要 ÷ 避難所収容力」の比率を主役に据え、 バブルプロット・ランキング棒・比率マップで島ごとの不足度を一目で読めるようにする。

本レッスンは、瀬戸内海に浮かぶ広島県の 15 離島について、 観光客需要 (#1282 しまたびライン利用 = 寄港地数で代理) と 避難所供給 (#42 避難所件数 + 収容力合計) の 空間需給バランスを 可視化する。「島ごとの観光ピーク需要に対して避難所収容力は十分か」という問いに、 1 つの比率指標「需給ギャップ指数 = 観光ピーク需要 ÷ 避難所収容力」を 答えとして据える。

このレッスンで答えたい問い (1 文)

「観光航路 (しまたびライン) の利用増加に対して、各島の避難所収容力は ピーク時の需要を支えきれるのか? 防災弱者島はどこか?」

立てた仮説 (H1〜H4 — 空間需給特化)

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

到達点

15 離島を 1 つの比率指標 (gap_index) で順位付けし、 バブル・ランキング・地理マップ・散布回帰の 4 視点で需給アンバランスを読み取る。 学習者は「比率指標を主役にする研究」「バブルプロットで 4 軸を 1 枚に詰め込む」 「45° 線で需給を等値比較する」「Ward 法で島を 3 群に分けて防災タイプを言語化する」 の 4 つを身につける。

データ実態の重要事項: DoBoX の #1282 瀬戸内しまたびライン利用状況は resource_download リンクが 公開されておらず、CSV を取得できない (S57 で確認済み)。そのため本記事は 寄港地数 × 50 人/日 という単純な代理指標を需要として用いる。仮定は明示し、学習者が定数を変えて 再計算できるようコードを設計してある (要件C, 再現性)。

使用データ

本レッスンは DoBoX オープンデータ 3 件から派生する:

DoBoX ID タイトル 説明文
1281 瀬戸内海の航路情報 広島県内における航路の情報です。
42 避難所情報 広島県の各市町の地域防災計画等に記載されている避難所情報です。
1282 瀬戸内しまたびライン利用状況 瀬戸内しまたびラインの観光客数です。

サイズの整理 (要件L: 表示と次元の混同を防ぐ):

行数列数説明
原データ shelters.json4,065 件36 列避難所 1 件 = 1 行 (capacity, lat, lon を含む)
原データ sea_route.csv81 件6 列寄港地 1 件 = 1 行
島定義テーブル ISLAND_DEF15 行3 列(島名, 住所キーワード, 代表座標) を本記事で人手定義
本記事の指標行列 M15 島6 指標島 1 件 = 1 行, 列 = n_shelters / capacity / n_ports / peak_demand / gap_index / 代表座標
ランキング表 rank157gap_index 降順に並び替えただけ
クラスタ標準化行列 F_std153(n_ports, capacity, gap_index) を平均0・分散1 に揃えたもの (Ward 法の入力)

※ 本記事は 「島 = 1 行」の表 1 つを最後まで保持する。市町ごとや避難所ごとに集計を切り替えるのは 同じ表の groupby を変えるのと等価で、本記事では に固定して読みやすさを優先する。

島の同定ロジック:

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

原データ (DoBoX)

論題データセットDL保存先形式サイズ
避難所情報DoBoX #42ページから DL ボタンdata/shelters.jsonJSON4,065件
瀬戸内海の航路情報DoBoX #1281ページから DL ボタンdata/extras/sea_route.csvCSV81件
DoBoX カタログ全件 indexDoBoX #0ページから DL ボタンdata/dataset_index.csvCSV551件

一括取得(全レッスン共通, 推奨):

cd "2026 DoBoX 教材"
py -X utf8 data\fetch_all.py

fetch_all.py はカタログ・追加データを data/data/extras/ に再現可能ダウンロード。DoBoX のオープンデータは申請不要、商用・非商用とも利用可。本レッスンの .py スクリプトは、データが無ければ自動取得してから処理を始めるよう実装されていますensure_dataset() ヘルパ)。

非公開データ: #1282 瀬戸内しまたびライン利用状況 は DoBoX の resource_download リンクが 公開されておらず、data/extras/ に CSV を置けない。本記事ではカタログメタ + 寄港地数 × 定数で代理する。

本レッスン生成の中間 CSV (HTML から直 DL)

図 PNG (HTML から直 DL)

再現スクリプト

X05_island_supply_demand.py を以下で実行:

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

分析1: 島ごとに需要・供給を集計する (要件K: 1 島の構築過程)

狙い

15 の離島それぞれについて、3 つの数値を集計する:

これらを出発点に、peak_demand (=観光ピーク需要) と本記事の主役 gap_index (=需給ギャップ指数) を構築する。

手法

  1. 島の同定辞書を人手定義: 市町境では島が混ざるので、住所文字列キーワードで島を特定する辞書 ISLAND_DEF を用意 (15 島)。
  2. 避難所マッチ: 各島について、shelters.json の address01+address02+name 連結文字列にキーワードを含む行を取り出して件数・capacity 合計を計算。
  3. 寄港地マッチ: 各島について、sea_route.csv の 住所+寄港地にキーワードを含む行を取り出して港数を集計。
  4. ピーク需要を組み立て: peak_demand = n_ports × 50 (=港 1 つあたりピーク日想定来島者 50 人と仮定)。
  5. 需給ギャップ指数を計算: gap_index = peak_demand ÷ capacity。capacity = 0 の島は最大値で代用。

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import json, pandas as pd, numpy as np

# 島の定義 (住所キーワード) — 市町境では島が混ざるため、人手で島ごとに特定
ISLAND_DEF = [
    ("大崎上島",  ["大崎上島町"]),
    ("江田島本島", ["江田島市江田島町"]),
    ("能美・沖美", ["江田島市能美町","江田島市沖美町","江田島市大柿町"]),
    ("宮島",      ["廿日市市宮島町"]),
    # ... (全 15 島)
]
PEAK_PER_PORT = 50  # ピーク日 1 港あたり想定来島者 (仮定: 学習者が変更可能)

# 1) 避難所読込
sdf = pd.DataFrame(json.load(open("data/shelters.json", encoding="utf-8"))["items"])
sdf["fulladdr"] = (sdf["address01"].astype(str)
                   + " " + sdf["address02"].astype(str)
                   + " " + sdf["name"].astype(str))
sdf["capacity"] = pd.to_numeric(sdf["capacity"], errors="coerce").fillna(0.0)

# 2) 航路読込
route = pd.read_csv("data/extras/sea_route.csv", encoding="utf-8-sig")
route["住所"]   = route["住所"].astype(str)
route["寄港地"] = route["寄港地"].astype(str)

# 3) 島ごとに集計
records = []
for name, kws in ISLAND_DEF:
    sm = pd.Series(False, index=sdf.index)
    rm = pd.Series(False, index=route.index)
    for kw in kws:
        sm |= sdf["fulladdr"].str.contains(kw, na=False)
        rm |= route["住所"].str.contains(kw, na=False)
        rm |= route["寄港地"].str.contains(kw.replace("町",""), na=False)
    records.append({
        "island":     name,
        "n_shelters": int(sm.sum()),
        "capacity":   float(sdf.loc[sm, "capacity"].sum()),
        "n_ports":    int(rm.sum()),
    })

M = pd.DataFrame(records)
M["peak_demand"] = M["n_ports"] * PEAK_PER_PORT
M["gap_index"]   = np.where(M["capacity"]>0,
                            M["peak_demand"]/M["capacity"],
                            np.nan)
M["gap_index"] = M["gap_index"].fillna(M["gap_index"].max()*1.2)

結果 (表と読み取り)

なぜこの表か: 15 島を一望するには、図の前にまず 具体的な数値を 確認するのが定石。需要 (peak_demand) と供給 (capacity) を 1 行に並べると、 学習者が手で「ギャップ = 需要 ÷ 供給」を再計算できる。

表1: 島別 集計結果 (M, 15 行 × 6 列)

island n_shelters capacity n_ports rep_lat rep_lon peak_demand gap_index
大崎上島 67 4197 10 34.244 132.907 500 0.119
江田島本島 49 10609 3 34.250 132.477 150 0.014
能美・沖美 82 14054 3 34.202 132.443 150 0.011
宮島 25 1643 1 34.298 132.322 50 0.030
似島 7 3058 2 34.308 132.438 100 0.033
因島 45 43319 9 34.302 133.171 450 0.010
向島 31 26797 3 34.387 133.198 150 0.006
生口島 22 11645 1 34.306 133.094 50 0.004
佐木島 7 403 4 34.336 133.113 200 0.496
百島 6 3823 1 34.376 133.274 50 0.013
倉橋島 82 10620 0 34.145 132.526 0 0.000
阿多田島 5 316 1 34.194 132.314 50 0.158
豊島 12 1780 4 34.174 132.796 200 0.112
上蒲刈・下蒲刈 21 4540 0 34.186 132.692 0 0.000
斎島 1 30 1 34.150 132.860 50 1.667

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

1 島追跡: 大崎上島の指標が組み上がるまで (要件K: Before/After)

分析全体で大崎上島 1 島を追いかけると、3 つの数値 → ピーク需要 → ギャップ指数 → クラスタ の流れが具体的に見える。下表は 同じ 1 島が各段階でどんな数値になるかを段階別に並べたもの:

段階 値・処理 サイズ
1. 住所マッチで避難所を抽出 大崎上島町 を含む住所の避難所 = 67 件 67 行 (避難所 1 件 = 1 行)
2. capacity 列を合計 avg ≈ 63 人/件 → 合計 4197 人 スカラー 1 個
3. 寄港地マッチで港数を数える sea_route.csv で 大崎上島 を含む住所/港名 = 10 港 スカラー 1 個
4. ピーク需要を組み立て port × 50 = 500 人/日 スカラー 1 個
5. 需給ギャップ指数を算出 500 ÷ 4197 = 0.119 スカラー 1 個 (本記事の主役)
6. クラスタに割り当て 標準化 → Ward 距離計算 → 3 群分割 → クラスタ 1 (防災優先 (供給充実)) 整数 1 個

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

分析2: バブルプロット — 1 枚で 4 軸を読む (要件H,F,M)

狙い

本記事の 主役図。x = 避難所件数, y = 観光ピーク需要, 半径 = 需給ギャップ指数, 色 = 収容力合計 の 4 軸を 1 枚に詰め込み、15 島の需給バランスを一望する。 仮説 H1, H2 (需要は件数に比例しない / ピーク時に不足) を検証する第一の図。

手法

バブルプロットとは:

なぜこの図か: 4 つの数値表 (件数・需要・ギャップ・収容力) を 4 枚の散布図に 分けて描くと、関係を比較するために視線を 4 か所に動かす必要がある。バブルプロットは 1 枚で全 4 軸を読めるので、空間需給バランス研究の主役図として最適。

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt
import math

fig, ax = plt.subplots(figsize=(11.5, 7.0))
sizes = (M["gap_index"].values / M["gap_index"].max()) * 1500 + 60
sc = ax.scatter(
    M["n_shelters"], M["peak_demand"],
    s=sizes,
    c=M["capacity"], cmap="viridis",
    alpha=0.78, edgecolor="black", linewidth=0.8,
)
for _, row in M.iterrows():
    ax.annotate(row["island"], (row["n_shelters"], row["peak_demand"]),
                xytext=(7, 5), textcoords="offset points", fontsize=9.5)
fig.colorbar(sc, ax=ax, label="供給 = 収容力合計 (人)")
ax.set_xlabel("供給 = 避難所件数 (件)")
ax.set_ylabel("需要 = 観光ピーク需要 = 寄港地数 × 50 (人/日)")
ax.grid(alpha=0.3)
plt.savefig("assets/X05_bubble.png", dpi=140)

結果 (図と読み取り)

図1: 離島の需給バブルプロット (主役図)
図1: 離島の需給バブルプロット (主役図)

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

分析3: ギャップ指数ランキング — 防災弱者島を順位付け (要件F,M)

狙い

バブルプロットでは 位置と泡の大きさが直感的だが、正確な順位を付けるには 向かない。ここでは gap_index の値を 横棒グラフで順位付けし、 「ピーク時不足島」を一目で読めるようにする。仮説 H3 (ランキング上位は防災弱者島) を検証。

手法

実装

X05_island_supply_demand.py 行 846–870

 1
 2
 3
 4
 5
 6
 7
 8
 9
855
856
rank = M.sort_values("gap_index", ascending=False).reset_index(drop=True)
top = rank.head(15)
fig, ax = plt.subplots(figsize=(11, 7))
ax.barh(range(len(top)), top["gap_index"], color="#cf222e")
ax.set_yticks(range(len(top)))
ax.set_yticklabels(top["island"])
ax.axvline(1.0, color="#0969da", linestyle="--",
           label="境界線 = 1.0 (これより右は需要 > 供給)")
ax.set_xlabel("需給ギャップ指数 = ピーク需要 ÷ 収容力")
ax.legend()
plt.savefig("assets/X05_gap_ranking.png", dpi=140)

結果 (図と読み取り)

図2: 島別 需給ギャップ指数ランキング (上位15)
図2: 島別 需給ギャップ指数ランキング (上位15)

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

結果 (表と読み取り)

表2: ギャップ指数ランキング (上位15)

rank island n_ports peak_demand n_shelters capacity gap_index
1 斎島 1 50 1 30 1.667
2 佐木島 4 200 7 403 0.496
3 阿多田島 1 50 5 316 0.158
4 大崎上島 10 500 67 4197 0.119
5 豊島 4 200 12 1780 0.112
6 似島 2 100 7 3058 0.033
7 宮島 1 50 25 1643 0.030
8 江田島本島 3 150 49 10609 0.014
9 百島 1 50 6 3823 0.013
10 能美・沖美 3 150 82 14054 0.011
11 因島 9 450 45 43319 0.010
12 向島 3 150 31 26797 0.006
13 生口島 1 50 22 11645 0.004
14 倉橋島 0 0 82 10620 0.000
15 上蒲刈・下蒲刈 0 0 21 4540 0.000

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

分析4: 比率指標マップ — 緯度経度散布で色=ギャップ (要件F)

狙い

ランキング棒は 順位を付けるが、地理的な分布は見えない。ここでは島の代表座標 (避難所の中央緯度経度) を散布し、色で gap_index を、半径で需要を表現する。 仮説 H4 (本土との接続度と需給ギャップが関連) を地理から検証。

手法

実装

X05_island_supply_demand.py 行 900–928

 1
 2
 3
 4
 5
 6
 7
 8
 9
909
910
911
912
fig, ax = plt.subplots(figsize=(11, 7))
sc = ax.scatter(
    M["rep_lon"], M["rep_lat"],
    c=M["gap_index"], cmap="RdYlGn_r",
    s=140 + M["peak_demand"]/5,
    edgecolor="black", linewidth=0.8,
)
for _, r in M.iterrows():
    ax.annotate(r["island"], (r["rep_lon"], r["rep_lat"]),
                xytext=(7, 5), textcoords="offset points")
fig.colorbar(sc, ax=ax, label="ギャップ指数 (赤=不足, 緑=余裕)")
ax.set_xlabel("経度 (E)"); ax.set_ylabel("緯度 (N)")
plt.savefig("assets/X05_map.png", dpi=140)

結果 (図と読み取り)

図3: 離島の地理的需給ギャップ — 色=ギャップ, 半径=需要
図3: 離島の地理的需給ギャップ — 色=ギャップ, 半径=需要

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

分析5: 需要 vs 供給 散布図 — 単回帰と 45° 線で等値比較 (要件H)

狙い

仮説 H2 (観光客が多い島は収容力も大きい / ピーク時には不足) を 需要 vs 供給 の 直接的な散布図 + 単回帰 + 45° 線で検証する。45° 線は「需要 = 供給」の等値線で、 ここより 上にある島は需要が供給を超過

手法

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from scipy.stats import linregress

x = M["capacity"].values; y = M["peak_demand"].values
slope, intercept, r, p, _ = linregress(x, y)

fig, ax = plt.subplots(figsize=(10, 7))
ax.scatter(x, y, s=170, c="#0969da")
xx = np.linspace(0, x.max()*1.05, 50)
ax.plot(xx, slope*xx + intercept, color="#cf222e",
        label=f"単回帰 R²={r*r:.3f}")
mx = max(x.max(), y.max())*1.05
ax.plot([0, mx], [0, mx], "--", color="#666",
        label="45° 線 (需要=供給)")
ax.set_xlabel("供給 = 収容力合計 (人)")
ax.set_ylabel("需要 = ピーク需要 (人)")
ax.legend()
plt.savefig("assets/X05_demand_vs_supply.png", dpi=140)

結果 (図と読み取り)

図4: 需要 vs 供給 散布図 + 単回帰 + 45°線
図4: 需要 vs 供給 散布図 + 単回帰 + 45°線

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

結果 (表と読み取り)

表3: 需要 vs 供給 単回帰結果

x y n slope intercept r R2 p_value
capacity (供給) peak_demand (需要) 15 0.006 91.03 0.454 0.206 0.0889

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

分析6: 階層クラスタ (補助) — 観光重点 / 防災優先 / 両立 の 3 群分類

狙い

15 島を 3 つのタイプに言語化する: 観光重点 (需要過多), 防災優先 (供給充実), 両立 (中庸)。3 つの数値 (n_ports, capacity, gap_index) を標準化して Ward 法でクラスタ化する。これは 補助的な図で、 本記事の主役 (バブル・ランキング・地理マップ) を補強する位置付け。

手法

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from scipy.cluster.hierarchy import linkage, fcluster, dendrogram
from scipy.spatial.distance import pdist

F = M[["n_ports", "capacity", "gap_index"]].values.astype(float)
F_std = (F - F.mean(0)) / (F.std(0) + 1e-9)        # 標準化
Z = linkage(pdist(F_std, "euclidean"), method="ward")
labels3 = fcluster(Z, t=3, criterion="maxclust")    # 3 群

fig, ax = plt.subplots(figsize=(12, 5.6))
dendrogram(Z, labels=M["island"].tolist(),
           color_threshold=Z[-2, 2]-0.01, ax=ax)
ax.axhline(Z[-2, 2]-0.01, color="#cf222e", linestyle="--")
plt.savefig("assets/X05_dendrogram.png", dpi=140)

結果 (図と読み取り)

図5: 階層クラスタ (Ward 法) — 3 群の樹状図
図5: 階層クラスタ (Ward 法) — 3 群の樹状図

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

結果 (表と読み取り)

表4: 3 群の中心 (平均値)

n_ports capacity gap_index
cluster3
1 9.50 23758 0.065
2 1.92 7441 0.073
3 1.00 30 1.667

表5: 島→クラスタ + ラベル (15 島すべて)

island n_ports n_shelters capacity peak_demand gap_index cluster3 cluster_label
大崎上島 10 67 4197 500 0.119 1 防災優先 (供給充実)
江田島本島 3 49 10609 150 0.014 2 両立 (中庸)
能美・沖美 3 82 14054 150 0.011 2 両立 (中庸)
宮島 1 25 1643 50 0.030 2 両立 (中庸)
似島 2 7 3058 100 0.033 2 両立 (中庸)
因島 9 45 43319 450 0.010 1 防災優先 (供給充実)
向島 3 31 26797 150 0.006 2 両立 (中庸)
生口島 1 22 11645 50 0.004 2 両立 (中庸)
佐木島 4 7 403 200 0.496 2 両立 (中庸)
百島 1 6 3823 50 0.013 2 両立 (中庸)
倉橋島 0 82 10620 0 0.000 2 両立 (中庸)
阿多田島 1 5 316 50 0.158 2 両立 (中庸)
豊島 4 12 1780 200 0.112 2 両立 (中庸)
上蒲刈・下蒲刈 0 21 4540 0 0.000 2 両立 (中庸)
斎島 1 1 30 50 1.667 3 観光重点 (需要過多)

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

結論と仮説判定 (要件E)

仮説 H1〜H4 の判定

仮説結果判定
H1: 観光客需要は避難所件数に比例しない (観光地化偏在) 図1 のバブルプロットで x (件数) と y (需要) はばらつきが大きく、明確な比例関係は見えない。 件数が同じでも需要が桁違いの島がある (例: 阿多田島 vs 似島)。 支持
H2: 観光客が多い島は収容力も大きい (r > 0.4) が、ピーク時には不足 図4 で r = 0.454 の正相関 (閾値 0.4 を上回る)。 ピーク時に 45° 線を超える (= 不足する) 島は 1 島あり、外れ値として存在する。 支持
H3: ギャップ指数ランキング上位は防災弱者島 図2 で gap_index > 1.0 (= 需要超過) は 1 島。 最大は 斎島 (gap_index = 1.667, 港数 1)。 これらは寄港地数が多い観光ハブ島と一致。 部分支持
H4: 接続度 (港数) と需給ギャップが関連 図3 の地理マップで、港数の多い大崎上島 (10 港) などはギャップ高位。 ただし因島 (9 港) のように 大規模避難所を持つ島は例外的にギャップが低く、 接続度だけでは説明しきれない。 支持 (例外あり)

総合考察

発展課題

結果X → 新仮説Y → 課題Z (要件E)

課題1: 真のしまたびライン利用客数との照合

課題2: capacity 取得失敗島の補正

課題3: 季節性の組み込み

課題4: 23 市町への拡張 (要件L: 次元への意識)

課題5: 災害種別フラグ別ギャップの分解

課題6: 航路接続グラフ化 (本記事範囲外手法)