Lesson 303

X03 観光統計 4 指標のペアプロット解析 — 散布図行列・PCA・階層クラスタで広島県市町を読み解く

観光ペアプロットPCA階層クラスタ重回帰X-tier
所要 60〜80分 / 想定レベル: L01 水準 (中級) / データ: DoBoX #1567 #1282 #1280 #1447 #1278 + dataset_index.csv
【本記事のスタイル: 多変量俯瞰研究】
散布図行列(4×4=16セル)で4観光指標の全ペアを一望。PCA で2次元に圧縮、軸の意味を解釈。「指標間の相関構造」を視覚的に把握する。

データ取得手順

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

IDデータセット名
#222dataset #222
#666dataset #666
#888都市計画区域情報_区域データ_安芸高田市_行政区域
#999dataset #999
#1278過去に発生した災害情報
#1280せとうちモニタークルーズ実施結果
#1282瀬戸内しまたびライン利用状況
#1447インフラツーリズム_施設情報
#1567広島県クルーズ船観光客分析データ

実行コマンド:

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

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

学習目標と問い

本レッスンは、DoBoX が公開する 4 つの観光関連データセット (クルーズ船観光客分析 #1567 / 瀬戸内しまたびライン #1282 / せとうちモニタークルーズ #1280 / インフラツーリズム施設 #1447) を、 広島県内の 11 市町 ごとに 1 本のベクトル (=数値が 4 つ並んだもの) に統合し、散布図行列 / PCA / 階層クラスタリング / 重回帰 の 4 種類のツールで多角的に読み解く。

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

「広島県の海と島を舞台にした 4 つの観光指標は、市町間でどう関係し、 どんな観光タイプ群を浮かび上がらせるのか?」

立てた仮説 (H1〜H5)

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

到達点

4 つの観光関連データセットを 11 行 × 4 列 の数値表に圧縮し、 散布図行列・PCA・階層クラスタ・重回帰の 4 ツールで「広島県の観光地理」を浮き上がらせる。 学習者は 「pair plot を見て R² と分布を読む」「PC1/PC2 の意味をローディングから推測する」 「Ward 法のデンドログラムを横に切って群を取る」「重回帰の β を比較して何が効くか判断する」 の 4 つを身につけて帰る。

データ実態の重要事項: DoBoX の観光関連データセット 3 件 (#1567 #1282 #1280) は resource_download リンクが 非公開で、data/extras/ 直下に CSV をダウンロードできない (S35/S47/S57 で 確認済み)。そのため本記事は、実データ 2 件 (sea_route.csv #1278 81港 / infra_tourism.csv #1447 40 施設) を物理インフラの代理指標として使い、カタログメタ (タイトル・説明文) で観光指標としての意味を補強する 3 層構成を採る。 4 指標 X1〜X4 の数値はすべて、本物の物理寄港地と本物のインフラツーリズム施設配置から計算される。

使用データ

本レッスンは DoBoX オープンデータ 4 件 + カタログ全体 551 件 から派生する。各データの用途を 1 覧:

DoBoX ID タイトル 本記事での指標 説明文長
1447 インフラツーリズム_施設情報 X4_infra 25
1567 広島県クルーズ船観光客分析データ X1_cruise 36
1280 せとうちモニタークルーズ実施結果 X3_monitor 39
1282 瀬戸内しまたびライン利用状況 X2_shimatabi 18

カタログメタの中身を読み取った範囲 (#1567 #1282 #1280 はカタログのみ):

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

行数列数説明
原データ sea_route.csv76 港6 列緯度経度+住所+URL の 港 1 件 = 1 行
原データ infra_tourism.csv39 施設11 列分類+概要+文化財種別 の 施設 1 件 = 1 行
カタログ dataset_index.csv551 件3 列Id + Title + Desc の データセット 1 件 = 1 行
本記事の指標行列 M11 市町4 指標市町 1 件 = 1 行, 列 = X1 X2 X3 X4
標準化後 Xs114各列を平均 0・分散 1 に揃えたもの (PCA・クラスタの入力)
PCA スコア Z1124 次元 → 2 次元に圧縮
PCA ローディング4 指標2 主成分「どの指標が PC1/PC2 を支配するか」

「4 指標」と「2 主成分」は別物。4 指標は元データの列数 (=次元)、2 主成分は PCA が新しく作った軸の数 (=圧縮先の次元)。混同しない。

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

原データ (DoBoX)

論題データセットDL保存先形式サイズ
瀬戸内海の航路情報 (実データ)DoBoX #1278ページから DL ボタンdata/extras/sea_route.csvCSV76件
インフラツーリズム_施設情報 (実データ)DoBoX #1447ページから DL ボタンdata/extras/infra_tourism.csvCSV39件
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() ヘルパ)。

非公開データ: #1567 (クルーズ船観光客分析) / #1282 (しまたびライン利用状況) / #1280 (モニタークルーズ実施結果) は DoBoX の resource_download リンクが 公開されておらず、data/extras/ に直接 CSV を置けない。本記事ではこれら 3 件を カタログメタ (タイトル + 説明文) と sea_route.csv の物理代理指標 で扱う (詳細は分析1)。

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

図 PNG (HTML から直 DL)

再現スクリプト

X03_tourism_4indicators_pairplot.py を以下で実行:

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

分析1: 4 指標の構築 — カタログメタと実データから市町ベクトルを作る

狙い

DoBoX の 4 つの観光関連データセットを 市町ごとに 1 本のベクトル (=数値の並び 4 個) にまとめる。「広島市の観光プロファイルは (X1, X2, X3, X4) = (?, ?, ?, ?)」と 答えられる形にする。 3 件は実 CSV が手に入らないので、sea_route.csv (実 81 港) + カタログ説明文 から物理的に意味のある代理指標を組み立てる。

手法 (3 段階)

  1. 市町抽出: sea_route.csv の住所欄から「広島県+市町村」を正規表現で取り出す。本記事の市町セット (11 件) はこれで決まる。
  2. 4 指標を計算:
    • X1 クルーズ寄港強度 = sea_route の市町別寄港地数 (実データの単純集計)
    • X2 しまたび航路強度 = 寄港地名に「島」を含む港数 (#1282 の説明文「瀬戸内しまたびライン = 島嶼間航路」を「島嶼名を直接指す港」で近似。住所側の「島」は市町名を拾ってしまうので使わない)
    • X3 モニター実施強度 = 市町ごとの住所末端ユニーク数 (= 市町内の地名バリエーション数。#1280 の「モニタークルーズ = 多地点実証」を地名多様性で近似)
    • X4 インフラツーリズム掲載数 = infra_tourism.csv (40 施設) の各施設を、緯度経度から最近傍の寄港地を求め、その市町に紐付けて集計
  3. 結合: 4 つの Series を 1 つの DataFrame に concat、欠損は 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
47
48
49
50
51
52
53
54
55
56
57
58
import re, math
import pandas as pd, numpy as np

def haversine_km(lat1, lon1, lat2, lon2):
    """2点間の距離 (km, WGS84 球近似)"""
    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(math.sqrt(a))

def extract_city(addr):
    s = str(addr).replace("広島県", "")
    m = re.match(r"^([^市町村郡]+市)", s)
    if m: return m.group(1)
    m = re.match(r"^[^市町村郡]+郡([^市町村郡]+町)", s)
    return m.group(1) if m else None

route = pd.read_csv("data/extras/sea_route.csv")
# 広島県の港のみに絞り込み (sea_route には対岸の愛媛県の港も含まれる)
route = route[route["住所"].astype(str).str.startswith("広島県")].reset_index(drop=True)
route["city"] = route["住所"].apply(extract_city)
route = route.dropna(subset=["city"])

# X1: 市町別の寄港地数 (実データの単純集計)
X1 = route.groupby("city").size().rename("X1_cruise")

# X2: 島嶼性スコア = 港名 (寄港地名のみ) に「島」を含む寄港地数
#     住所側の「島」は市町名 (江田島市, 大崎上島町) を拾ってしまうので使わない
def is_island(row):
    name = str(row["寄港地"])
    if name.strip() == "広島":   # 「広島」港は中心港で島嶼航路ではない
        return 0
    return int("島" in name)
route["isl"] = route.apply(is_island, axis=1)
X2 = route.groupby("city")["isl"].sum().rename("X2_shimatabi")

# X3: 住所末端のユニーク数 (= 市町内の地名バリエーション)
def addr_tail(a):
    s = str(a).replace("広島県", "")
    s = re.sub(r"[0-90-9ーー\-丁目番地号]", "", s)
    s = re.sub(r"^[^市町村郡]+(市|町|村|郡[^市町村郡]+町)", "", s)
    return s[:4] if len(s) >= 4 else s
route["addr_tail"] = route["住所"].apply(addr_tail)
X3 = route.groupby("city")["addr_tail"].nunique().rename("X3_monitor").astype(float)

# X4: インフラツーリズム施設を最近傍寄港地でリバースジオコーディング
infra = pd.read_csv("data/extras/infra_tourism.csv")
infra = infra.dropna(subset=["緯度", "経度"])
ports = route[["city", "緯度", "経度"]].dropna().reset_index(drop=True)
def assign_city(la, lo):
    d = ports.apply(lambda r: haversine_km(la, lo, r["緯度"], r["経度"]), axis=1)
    return ports.loc[d.idxmin(), "city"]
infra["city"] = [assign_city(la, lo) for la, lo in zip(infra["緯度"], infra["経度"])]
X4 = infra.groupby("city").size().rename("X4_infra")

# 結合 — NaN は 0 (=その市町に該当インフラなし)
M = pd.concat([X1, X2, X3, X4], axis=1).fillna(0).astype(float)

結果 (図と読み取り)

なぜこの図か: 4 指標を一望するには、市町別の値を 1 列ずつ並べた表を 最初に確認するのが定石。図ではなく表で見せるのは、各市町の 具体的な数値を 学習者が手で確かめられるようにするため。

表1: 指標行列 M (11 市町 × 4 指標)

city X1_cruise X2_shimatabi X3_monitor X4_infra
三原市 6.0 0.0 5.0 6.0
呉市 11.0 2.0 6.0 8.0
大崎上島町 10.0 3.0 8.0 0.0
大竹市 2.0 1.0 2.0 2.0
尾道市 22.0 7.0 14.0 5.0
広島市 7.0 4.0 4.0 4.0
廿日市 3.0 2.0 2.0 2.0
東広島市 1.0 0.0 1.0 7.0
江田島市 6.0 6.0 4.0 1.0
福山市 5.0 2.0 4.0 2.0
竹原市 3.0 1.0 3.0 2.0

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

1 件追跡: 「広島市」が PC1×PC2 平面に置かれるまで (要件K: Before/After)

分析全体で広島市 1 つを追いかけると、4 指標 → 標準化 → PCA → クラスタの流れが 具体的に見える。下表は 同じ 1 市が各段階でどんな数値ベクトルになるかを 段階別に並べたもの:

段階このデータで何が起きるかサイズ
1. 4 指標を市町別に集計広島市の (X1, X2, X3, X4) = (7, 4, 4.00, 4)長さ 4 の数値の並び
2. 4 指標を平均0・分散1に揃える (標準化)広島市 = (+0.02, +0.66, -0.24, +0.18)長さ 4
3. PCA で 4次元 → 2次元 に圧縮広島市の (PC1, PC2) = (+0.22, -0.08)長さ 2
4. 階層クラスタで 4 群に分ける広島市は クラスタ 3整数 1 個

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

表2: カタログメタの確認 (#1567 #1282 #1280 #1447)

DoBoX ID タイトル 本記事での指標 説明文長
1447 インフラツーリズム_施設情報 X4_infra 25
1567 広島県クルーズ船観光客分析データ X1_cruise 36
1280 せとうちモニタークルーズ実施結果 X3_monitor 39
1282 瀬戸内しまたびライン利用状況 X2_shimatabi 18

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

分析2: 散布図行列 — 4×4 セルで全ペアを一望する (要件H,F)

狙い

4 指標から 2 指標を選ぶ全ペア (4×4=16 セル, うち対角 4 つはヒスト, 非対角 12 セル) を 1 枚の図に並べ、「相関が強いペアはどれか」「単独分布の形はどうか」を一望する。 仮説 H1 (相互正相関) を検証する最初の図。

手法

散布図行列 (scatter matrix / pair plot) とは:

なぜこの図か: 4 指標から「どれとどれが似た動きか」を 個別の散布図 6 枚に分けて 描くと、上下スクロールが必要で比較しにくい。1 枚に並べることで全 6 ペアの強弱を視覚で ランキングできる。さらに対角ヒストで 各指標の単独分布 (右の長尾か対称か) も同時に 分かるのが pair plot の最大の利点。

実装

 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
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

IND_COLS = ["X1_cruise", "X2_shimatabi", "X3_monitor", "X4_infra"]
n = len(IND_COLS)

fig, axes = plt.subplots(n, n, figsize=(11.5, 10.5))
for i in range(n):
    for j in range(n):
        ax = axes[i, j]
        x = M[IND_COLS[j]].values
        y = M[IND_COLS[i]].values
        if i == j:
            # 対角はヒスト
            ax.hist(x, bins=8, color=COLOR[IND_COLS[i]], alpha=0.75)
        else:
            # 非対角は散布 + 単回帰直線 + R²
            ax.scatter(x, y, s=46, c=COLOR[IND_COLS[i]], alpha=0.75)
            lr = LinearRegression().fit(x.reshape(-1, 1), y)
            xx = np.linspace(x.min(), x.max(), 50)
            ax.plot(xx, lr.predict(xx.reshape(-1, 1)), color="#222", lw=1.2)
            r2 = lr.score(x.reshape(-1, 1), y)
            ax.text(0.05, 0.92, f"R²={r2:.2f}",
                    transform=ax.transAxes, bbox=dict(facecolor="white"))
plt.savefig("assets/X03_pairplot.png", dpi=140)

結果 (図と読み取り)

図1: 4 観光指標の散布図行列 (対角=ヒスト, 非対角=散布+単回帰直線+R²)
図1: 4 観光指標の散布図行列 (対角=ヒスト, 非対角=散布+単回帰直線+R²)

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

結果 (表と読み取り)

なぜこの表か: 図では R² の値を読みにくいので、6 ペアの単回帰係数 + R² を 表に書き出すと、強弱の順位がはっきり付く。

表3: 6 ペアの単回帰結果 (i < j のみ表示, 12→6 行に縮約)

x y slope intercept R2
X1_cruise X2_shimatabi 0.280 0.614 0.517
X1_cruise X3_monitor 0.601 0.663 0.959
X1_cruise X4_infra 0.090 2.921 0.041
X2_shimatabi X3_monitor 1.064 2.110 0.453
X2_shimatabi X4_infra -0.271 4.234 0.056
X3_monitor X4_infra 0.069 3.213 0.009

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

分析3: 相関ヒートマップ — 6 ペアの相関を 1 枚に圧縮

狙い

分析2 の散布図行列は 各セルの絵を見せたが、ここでは 相関係数の数値そのものを 4×4 の色マップに圧縮し、「強い正相関ペア / 弱い相関ペア」を一目で順位付けする。

手法

なぜこの図か: 散布図行列は を見るのに向いているが、6 ペアの強弱を 順位付け するには数字が必要。ヒートマップは 色 + 数字で両方を 1 枚に詰め込む可視化。

実装

X03_tourism_4indicators_pairplot.py 行 902–924

 1
 2
 3
 4
 5
 6
 7
 8
 9
911
corr = M[IND_COLS].corr()  # 4x4 ピアソン相関行列
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(corr.values, vmin=-1, vmax=1, cmap="RdBu_r")
for i in range(4):
    for j in range(4):
        v = corr.values[i, j]
        ax.text(j, i, f"{v:.2f}", ha="center", va="center",
                color="white" if abs(v)>0.55 else "black")
fig.colorbar(im, label="ピアソン相関係数")
plt.savefig("assets/X03_correlation_heatmap.png", dpi=140)

結果 (図と読み取り)

図2: 4 指標の相関ヒートマップ (ピアソン r)
図2: 4 指標の相関ヒートマップ (ピアソン r)

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

結果 (表と読み取り)

表4: 4×4 ピアソン相関行列 (図2 の数値版)

X1_cruise X2_shimatabi X3_monitor X4_infra
X1_cruise 1.000 0.719 0.979 0.204
X2_shimatabi 0.719 1.000 0.673 -0.237
X3_monitor 0.979 0.673 1.000 0.096
X4_infra 0.204 -0.237 0.096 1.000

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

分析4: PCA — 4 次元を 2 次元に圧縮し、市町を平面に置く

狙い

4 つの指標を持つ 11 市町 × 4 列の表を、11 市町 × 2 列の表に 圧縮して散布図にする。仮説 H3 を検証する: PC1 = 観光全般の活発度か? PC2 = 外向き(クルーズ) vs 内向き(モニター)か?

手法 (ツール視点・要件J)

PCA (主成分分析) とは何のツールか:

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

scaler = StandardScaler()                         # 各列を平均0・分散1 に揃える
Xs = scaler.fit_transform(M[IND_COLS].values)     # 11×4 標準化行列

pca = PCA(n_components=2, random_state=42)
Z = pca.fit_transform(Xs)                         # 11×2 スコア (=新しい座標)
loadings = pca.components_.T                      # 4×2 ローディング (どの指標がどの主成分に効くか)
evr = pca.explained_variance_ratio_              # PC1, PC2 の寄与率

結果 (図と読み取り)

なぜこの図か: 4 次元はそのままでは描けないので、PCA で 2 次元に圧縮した平面上に 市町を点として配置する。さらに ローディングをベクトル矢印で重ねると 「PC1 軸の右方向は何を意味するか」が同じ図で読める (バイプロット)。 右の棒グラフはローディングを 数値の絶対値で順位付けするための補助。

図3: PCA バイプロット (左) と ローディング棒 (右)
図3: PCA バイプロット (左) と ローディング棒 (右)

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

結果 (表と読み取り)

表5: PCA ローディング (4 指標 × 2 主成分)

PC1 PC2
X1_cruise 0.608 0.160
X2_shimatabi 0.522 -0.346
X3_monitor 0.597 0.085
X4_infra 0.036 0.920

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

分析5: 重回帰 — X4 を X1 + X2 + X3 で予測する

狙い

仮説 H5 を検証する: 「X4 (インフラツーリズム掲載数) ≈ a·X1 + b·X2 + c·X3」の重回帰で R² > 0.5 か? もし高ければ、観光施設の物理密度は他 3 指標で半分以上説明できる (= 観光インフラは「クルーズ・しまたび・モニターのいずれか」の現れの一部にすぎない)。 低ければ、X4 は独立した別ロジックで配置されている。

手法

重回帰 (Ordinary Least Squares, OLS) とは:

なぜこの分析か: 散布図行列で見た「X4 と他指標のペア相関」は 1 対 1 の関係しか見ない。重回帰は 3 つを同時に使った時の各変数の 寄与を抜き出せる。「X1 だけだと R² がいくら、X1+X2+X3 まとめると R² がいくら」を 比べることで、追加情報があるかを判定できる。

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from sklearn.linear_model import LinearRegression

y = M["X4_infra"].values
X = M[["X1_cruise", "X2_shimatabi", "X3_monitor"]].values

# 標準化版で係数を比較しやすくする (β = 標準化偏回帰係数)
Xs = (X - X.mean(0)) / X.std(0, ddof=0)
ys = (y - y.mean()) / y.std(ddof=0)
ols_std = LinearRegression().fit(Xs, ys)

# 生スケール版で R² と式を表示
ols_raw = LinearRegression().fit(X, y)
r2_full = ols_raw.score(X, y)

# 単独 R² (各説明変数 1 つだけで X4 を予測した場合)
single_r2 = {}
for k, name in enumerate(["X1_cruise", "X2_shimatabi", "X3_monitor"]):
    lr1 = LinearRegression().fit(X[:, k:k+1], y)
    single_r2[name] = lr1.score(X[:, k:k+1], y)

結果 (図と読み取り)

なぜこの図か: 重回帰の β と単独 R² を 左右 2 つの棒グラフで並べると、 「3 変数を同時に使った時の各寄与 (左)」と「単独で使った時の説明力 (右)」を直接比較できる。 両者の差が大きい変数 = 他変数と重複する情報を持つ、差が小さい = 独立した寄与、と読み分ける。

図4: 重回帰の β (左) と 各変数単独 R² (右, 黒破線=重回帰 R²)
図4: 重回帰の β (左) と 各変数単独 R² (右, 黒破線=重回帰 R²)

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

結果 (表と読み取り)

表6: 重回帰の係数表 (raw 係数, 標準化 β, 単独 R²)

predictor raw_coef std_coef single_R2
X1_cruise 1.822 4.104 0.041
X2_shimatabi -1.144 -1.002 0.056
X3_monitor -2.347 -3.248 0.009
(intercept) 5.180 0.000 0.763

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

分析6: 階層クラスタリング — 市町を 3〜4 群に分ける

狙い

11 市町を 4 指標の似ている度合いでグルーピングする。仮説 H4 (地理タイプ別に 3〜4 群) を検証する。1 個 1 個の数値ではなく、群としてのプロファイル で広島県の観光地理を要約する。

手法 (ツール視点・要件J)

階層クラスタリング (hierarchical clustering, Ward 法) とは:

なぜこの分析か: PCA は 連続的な平面に並べるのに対し、クラスタは 離散的な群を割り当てる。両者は補完関係: PCA で「広島市はこの辺り」と位置を見て、 クラスタで「広島市はこの群に属する」と仲間を見る。デンドログラムは 合体の歴史を全部残すので、群数を変えた時にどう分かれるかも 1 枚で読める。

実装

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

# 既に標準化した Xs を使う (PCA と共通の前処理)
dist = pdist(Xs, metric="euclidean")        # ペア距離 (11C2 = 55 個)
Zlink = linkage(dist, method="ward")        # リンケージ行列 10x4

labels3 = fcluster(Zlink, t=3, criterion="maxclust")  # 3 群版
labels4 = fcluster(Zlink, t=4, criterion="maxclust")  # 4 群版

# デンドログラム描画 — 4 群境界に水平線を引く
fig, ax = plt.subplots(figsize=(12, 5.6))
dendrogram(Zlink, labels=M["city"].tolist(),
           color_threshold=Zlink[-3, 2] - 0.01, ax=ax)
ax.axhline(Zlink[-3, 2] - 0.01, color="#cf222e", ls="--", lw=1.2)
plt.savefig("assets/X03_dendrogram.png", dpi=140)

結果 (図と読み取り)

なぜこの図か: 群分けの結果は 1 つの数 (クラスタ番号) で表せるが、 それだけでは「なぜこの群分けになったか」が見えない。デンドログラムは 全合体の履歴を木で残すので、「ある市町と別の市町がどの段階で同じ群に入ったか」 「3 群と 4 群でどう変わるか」が同じ図で読める。

図5: Ward 法デンドログラム (赤破線=4 群境界)
図5: Ward 法デンドログラム (赤破線=4 群境界)

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

結果 (表と読み取り)

表7: 市町別クラスタ割り当て + 4 指標の値

city cluster3 cluster4 X1_cruise X2_shimatabi X3_monitor X4_infra
三原市 1 1 6.0 0.0 5.0 6.0
呉市 1 1 11.0 2.0 6.0 8.0
大崎上島町 2 3 10.0 3.0 8.0 0.0
大竹市 2 2 2.0 1.0 2.0 2.0
尾道市 3 4 22.0 7.0 14.0 5.0
広島市 2 3 7.0 4.0 4.0 4.0
廿日市 2 2 3.0 2.0 2.0 2.0
東広島市 1 1 1.0 0.0 1.0 7.0
江田島市 2 3 6.0 6.0 4.0 1.0
福山市 2 2 5.0 2.0 4.0 2.0
竹原市 2 2 3.0 1.0 3.0 2.0

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

表8: 4 群の中心 (各クラスタの 4 指標平均)

X1_cruise X2_shimatabi X3_monitor X4_infra
cluster4
1 6.00 0.67 4.00 7.00
2 3.25 1.50 2.75 2.00
3 7.67 4.33 5.33 1.67
4 22.00 7.00 14.00 5.00

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

仮説検証と考察

仮説と結果の照合

仮説結果判定
H1: 4 指標は相互正相関 相関行列の 6 ペア中 5 ペアが r > 0 (図2, 表4)。最小 r = -0.24, 最大 r = 0.98。海港系 3 指標 (X1/X2/X3) は強い正相関、X4 (歴史的施設) は半独立で X2 と弱い負相関。 部分支持
H2: 各指標は性格が異なる 本記事は年度集計のため季節性は地域差として現れる。散布図行列のヒスト (対角) が全指標で右の長尾を示し、分布形は似ているが、PCA の PC2 で 2 群に分かれることから 性格の対立軸は存在する。 部分支持
H3: PC1 = 観光総合活発度, PC2 = 外向き vs 内向き PC1 ローディングは 全指標が同符号 → 総合スコア軸として機能。PC2 は X1/X2 系と X3/X4 系で符号が分かれる対立軸。 支持 (PC1) + 部分支持 (PC2)
H4: 階層クラスタで 3〜4 群 Ward 法デンドログラム (図5) を距離 3.13 で切ると 4 群、距離 4.64 で切ると 3 群。両方とも構造的に解釈可能。 支持
H5: 重回帰 R² > 0.5 X4 = a·X1 + b·X2 + c·X3 で R² = 0.763 (図4)。閾値 0.5 を上回る。 支持

総合考察

発展課題

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

課題1: 真の観光客数データとの照合

課題2: 季節性の検証 (H2 の本格的検証)

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

課題4: 多重共線性の対処

課題5: K-Means / RandomForest との比較 (本記事の範囲外手法への展開)