このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の人口動態は都道府県間で大きな差異が存在する。「全国的な少子高齢化」という一言では捉えられない地域ごとの多様なパターンがある。本研究は「なぜ都道府県によって人口動態がこれほど異なるのか」を、外れ値の識別・クラスタリング・PLS回帰という統計手法を組み合わせて分析した。
まず「人口動態の地域差に着目した基礎的分析」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
特に、沖縄県・秋田県・東京都が「外れ値」として通常の分析の枠外に位置することを可視化し、それを統計的に処理した上で残りの44都道府県をクラスタリングするという分析設計が本論文の特徴である。
| データ | 出典 | 使用変数 |
|---|---|---|
| SSDSE-B 都道府県別統計 | 統計数理研究所(2022〜2023年) | 保育所定員数・病院数・有効求人数・消費支出・婚姻件数・住宅地価格など |
| 人口推計・人口動態統計 | e-Stat(総務省・厚生労働省) | 自然増減率・社会増減率(‰) |
| 医師・介護施設等統計 | e-Stat(厚生労働省) | 医師数・介護施設整備率 |
| 目的変数 | 説明変数の種類 | 変数例 |
|---|---|---|
| 自然増減率(‰) 出生数 − 死亡数 ÷ 人口×1000 |
経済 | 1人当たり県民所得、出産費用 |
| 子育て | 保育所定員率(保育所定員/人口) | |
| 医療 | 医師数(人口当たり)、病院数 | |
| 社会 | 婚姻件数、自殺率 | |
| 社会増減率(‰) 転入数 − 転出数 ÷ 人口×1000 |
経済 | 県民所得、有効求人数 |
| 労働 | 若年労働力率 | |
| 住宅 | 持ち家比率、公営借家数、住宅地価格 |
1 2 3 4 5 6 7 8 9 | print("=" * 65) print("■ データ読み込み・構築(SSDSE-B & SSDSE-E 実データのみ)") print("=" * 65) # ── SSDSE-B-2026 読み込み ──────────────────────────────────── df_b_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'), encoding='cp932', header=1 ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。10 11 12 13 14 15 16 | # 47都道府県のみ df_b_raw = df_b_raw[df_b_raw['地域コード'].str.match(r'^R\d{5}$', na=False)].copy() df_b_raw['年度'] = pd.to_numeric(df_b_raw['年度'], errors='coerce') # 2022年データを使用 df_b = df_b_raw[df_b_raw['年度'] == 2022].copy().reset_index(drop=True) print(f"SSDSE-B 2022年: {len(df_b)}都道府県") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | # 必要列を数値変換 num_cols_b = [ '総人口', '65歳以上人口', '15~64歳人口', '15歳未満人口', '出生数', '死亡数', '転入者数(日本人移動者)', '転出者数(日本人移動者)', '保育所等数', '保育所等定員数', '婚姻件数', '年平均気温', '高等学校卒業者数', '高等学校卒業者のうち進学者数', '合計特殊出生率', '一般病院数', '月間有効求人数(一般)', '標準価格(平均価格)(住宅地)', '消費支出(二人以上の世帯)', '着工新設持家数', '教育費(二人以上の世帯)', ] for c in num_cols_b: df_b[c] = pd.to_numeric(df_b[c], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。34 35 36 37 38 39 40 41 42 43 44 45 | # ── SSDSE-E-2026 読み込み ──────────────────────────────────── df_e_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-E-2026.csv'), encoding='cp932', header=1 ) df_e = df_e_raw.iloc[1:].copy() df_e.columns = df_e_raw.iloc[0].values df_e = df_e[df_e['都道府県'] != '全国'].reset_index(drop=True) df_e['県民所得_1人'] = pd.to_numeric(df_e['1人当たり県民所得(平成27年基準)'], errors='coerce') df_e['医師数'] = pd.to_numeric(df_e['医師数'], errors='coerce') df_e['面積_km2'] = pd.to_numeric(df_e['総面積(北方地域及び竹島を除く)'], errors='coerce') / 100.0 print(f"SSDSE-E: {len(df_e)}都道府県") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | # ── 2つのデータを結合 ───────────────────────────────────────── df_pref = df_b[['都道府県', '総人口', '65歳以上人口', '15~64歳人口', '15歳未満人口', '出生数', '死亡数', '転入者数(日本人移動者)', '転出者数(日本人移動者)', '保育所等数', '保育所等定員数', '婚姻件数', '年平均気温', '高等学校卒業者数', '高等学校卒業者のうち進学者数', '合計特殊出生率', '一般病院数', '月間有効求人数(一般)', '標準価格(平均価格)(住宅地)', '消費支出(二人以上の世帯)', '着工新設持家数', '教育費(二人以上の世帯)', ]].copy() df_pref = df_pref.rename(columns={'都道府県': 'pref'}) df_pref = df_pref.merge( df_e[['都道府県', '県民所得_1人', '医師数', '面積_km2']].rename(columns={'都道府県': 'pref'}), on='pref', how='left' ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。61 62 63 64 65 66 | # ── 主要指標の計算(実データのみ) ─────────────────────────── # 自然増減率: (出生数 - 死亡数) / 総人口 × 1000 (‰) df_pref['自然増減率'] = (df_pref['出生数'] - df_pref['死亡数']) / df_pref['総人口'] * 1000 # 社会増減率: (転入者数 - 転出者数) / 総人口 × 1000 (‰) df_pref['社会増減率'] = (df_pref['転入者数(日本人移動者)'] - df_pref['転出者数(日本人移動者)']) / df_pref['総人口'] * 1000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。67 68 69 70 71 72 73 74 | # 高齢化率: 65歳以上人口 / 総人口 × 100 df_pref['高齢化率'] = df_pref['65歳以上人口'] / df_pref['総人口'] * 100 # 保育所充実度: 保育所等数 / 総人口 × 10000 df_pref['保育所充実度'] = df_pref['保育所等数'] / df_pref['総人口'] * 10000 # 保育所定員率: 保育所等定員数 / 総人口 × 1000 df_pref['保育所定員率'] = df_pref['保育所等定員数'] / df_pref['総人口'] * 1000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。75 76 77 78 79 80 81 82 | # 大学進学率: 高校卒業者うち進学者 / 高校卒業者 × 100 df_pref['大学進学率'] = df_pref['高等学校卒業者のうち進学者数'] / df_pref['高等学校卒業者数'] * 100 # 婚姻率: 婚姻件数 / 15〜64歳人口 × 1000 df_pref['婚姻率'] = df_pref['婚姻件数'] / df_pref['15~64歳人口'] * 1000 # 医師密度: 医師数 / 総人口 × 10000 df_pref['医師密度'] = df_pref['医師数'] / df_pref['総人口'] * 10000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。83 84 85 86 87 88 89 | # 人口密度: 総人口 / 面積km² df_pref['人口密度'] = df_pref['総人口'] / df_pref['面積_km2'] print(f"\n構築データ: {len(df_pref)}都道府県") print(f"変数数: {len(df_pref.columns)}列") print("\n記述統計(主要変数):") print(df_pref[['自然増減率','社会増減率','高齢化率','婚姻率']].describe().round(3)) |
=================================================================
■ データ読み込み・構築(SSDSE-B & SSDSE-E 実データのみ)
=================================================================
SSDSE-B 2022年: 47都道府県
SSDSE-E: 47都道府県
構築データ: 47都道府県
変数数: 34列
記述統計(主要変数):
自然増減率 社会増減率 高齢化率 婚姻率
count 47.000 47.000 47.000 47.000
mean -7.756 -1.305 31.350 6.436
std 2.418 1.764 3.269 0.554
min -14.262 -4.032 22.810 5.056
25% -9.428 -2.403 29.847 6.163
50% -7.734 -1.626 31.421 6.429
75% -6.459 -0.536 33.720 6.691
max -0.995 2.991 38.602 8.083.describe() — 件数・平均・標準偏差・四分位・最大/最小を一括計算。データの素性チェックに必須。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | print("\n" + "=" * 65) print("■ Step1. 外れ値の特定") print("=" * 65) df_pref = df_pref.dropna(subset=['自然増減率', '社会増減率']).reset_index(drop=True) def iqr_outliers(series, factor=1.5): q1 = series.quantile(0.25) q3 = series.quantile(0.75) iqr = q3 - q1 lower = q1 - factor * iqr upper = q3 + factor * iqr return series[(series < lower) | (series > upper)].index.tolist() nat_outlier_idx = iqr_outliers(df_pref['自然増減率']) soc_outlier_idx = iqr_outliers(df_pref['社会増減率']) nat_outlier_prefs = df_pref.loc[nat_outlier_idx, 'pref'].tolist() soc_outlier_prefs = df_pref.loc[soc_outlier_idx, 'pref'].tolist() print(f"自然増減率の外れ値: {nat_outlier_prefs}") print(f"社会増減率の外れ値: {soc_outlier_prefs}") OUTLIERS = list(set(nat_outlier_prefs + soc_outlier_prefs)) print(f"\n外れ値として独立クラスタ扱い: {OUTLIERS}") |
================================================================= ■ Step1. 外れ値の特定 ================================================================= 自然増減率の外れ値: ['秋田県', '沖縄県'] 社会増減率の外れ値: ['埼玉県', '千葉県', '東京都', '神奈川県'] 外れ値として独立クラスタ扱い: ['東京都', '秋田県', '千葉県', '埼玉県', '沖縄県', '神奈川県']
.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | print("図2: GMMクラスタリング結果を作成中...") fig2, axes2 = plt.subplots(1, 2, figsize=(13, 5.5)) fig2.suptitle('ガウス混合モデル(GMM)クラスタリング(外れ値除外後の都道府県)', fontsize=13, fontweight='bold') # (a) BIC曲線 ax2a = axes2[0] ks_plot = range(1, 6) ax2a.plot(list(ks_plot), bics, 'o-', color='#1565C0', linewidth=2.2, markersize=8, markerfacecolor='white', markeredgewidth=2) ax2a.axvline(3, color='#E53935', linestyle='--', linewidth=1.5, label='k=3 選択') ax2a.set_xlabel('コンポーネント数 k', fontsize=11) ax2a.set_ylabel('BIC(情報量基準)', fontsize=11) ax2a.set_title('BIC によるコンポーネント数の選択', fontsize=11, fontweight='bold') ax2a.legend(fontsize=10) ax2a.grid(True, alpha=0.3) for k, bic in zip(ks_plot, bics): ax2a.annotate(f'{bic:.0f}', (k, bic), textcoords='offset points', xytext=(0, 8), ha='center', fontsize=8.5) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。134 135 136 137 138 139 140 141 142 | # (b) 散布図(自然増減率 vs 社会増減率) ax2b = axes2[1] for cl in [1, 2, 3]: mask = df_main['cluster'] == cl ax2b.scatter(df_main.loc[mask, '自然増減率'], df_main.loc[mask, '社会増減率'], color=C_GMM[cl], s=80, alpha=0.85, edgecolors='white', linewidth=0.5, label=f'クラスタ{cl}', zorder=3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | # 外れ値を表示 for pref in OUTLIERS: row = df_pref[df_pref['pref'] == pref] if len(row) == 0: continue ax2b.scatter(row['自然増減率'].values, row['社会増減率'].values, color=C_GMM['outlier'], s=120, marker='*', edgecolors='white', linewidth=0.5, zorder=4) ax2b.annotate(pref.replace('県','').replace('都','').replace('道',''), (row['自然増減率'].values[0], row['社会増減率'].values[0]), fontsize=9, fontweight='bold', color=C_GMM['outlier'], xytext=(6, 4), textcoords='offset points') ax2b.axhline(0, color='gray', linewidth=0.8, alpha=0.5) ax2b.axvline(0, color='gray', linewidth=0.8, alpha=0.5) ax2b.set_xlabel('自然増減率(‰)', fontsize=11) ax2b.set_ylabel('社会増減率(‰)', fontsize=11) ax2b.set_title('GMM クラスタリング結果\n(★:外れ値として除外)', fontsize=11, fontweight='bold') outlier_names = [p.replace('県','').replace('都','') for p in OUTLIERS] outlier_p = mpatches.Patch(color=C_GMM['outlier'], label=f'外れ値({", ".join(outlier_names)})') handles, labels_leg = ax2b.get_legend_handles_labels() ax2b.legend(handles=handles+[outlier_p], fontsize=9) ax2b.grid(True, alpha=0.3) plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2025_U5_5_gmm.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" -> 2025_U5_5_gmm.png 保存完了") |
図2: GMMクラスタリング結果を作成中... -> 2025_U5_5_gmm.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | print("図3: PLS係数とバイオリンプロットを作成中...") fig3, axes3 = plt.subplots(1, 2, figsize=(14, 6)) fig3.suptitle('PLS 回帰係数の比較とクラスタ別分布(バイオリンプロット)', fontsize=13, fontweight='bold') # (a) PLS係数(全体、自然増減率) ax3a = axes3[0] feat_labels_nat = ['高齢化率', '保育所定員率', '医師密度\n(医師数/人口万)', '婚姻率', '合計特殊出生率'] coef_vals = coef_nat_all[:len(feat_labels_nat)] bar_colors_nat = ['#E53935' if c < 0 else '#43A047' for c in coef_vals] ax3a.barh(np.arange(len(feat_labels_nat)), coef_vals, color=bar_colors_nat, alpha=0.8, edgecolor='white') ax3a.axvline(0, color='black', linewidth=1.0) ax3a.set_yticks(np.arange(len(feat_labels_nat))) ax3a.set_yticklabels(feat_labels_nat, fontsize=10) ax3a.set_xlabel('PLS 回帰係数', fontsize=11) ax3a.set_title(f'PLS 回帰係数(自然増減率)\n全体 R²={r2_nat_all:.3f}', fontsize=11, fontweight='bold') ax3a.grid(axis='x', alpha=0.3) ax3a.invert_yaxis() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | # (b) バイオリンプロット(クラスタ別 自然増減率分布) ax3b = axes3[1] violin_data = [df_main[df_main['cluster'] == cl]['自然増減率'].dropna().values for cl in [1, 2, 3]] violin_data = [d for d in violin_data if len(d) > 1] if len(violin_data) >= 2: vp = ax3b.violinplot(violin_data, positions=range(1, len(violin_data)+1), showmedians=True, showextrema=True) for i, (body, cl) in enumerate(zip(vp['bodies'], [1, 2, 3])): body.set_facecolor(C_GMM[cl]) body.set_alpha(0.7) vp['cmedians'].set_color('white') vp['cmedians'].set_linewidth(2.5) ax3b.set_xticks([1, 2, 3]) ax3b.set_xticklabels([f'クラスタ{cl}\n(n={len(df_main[df_main["cluster"]==cl])})' for cl in [1,2,3]], fontsize=10) ax3b.set_ylabel('自然増減率(‰)', fontsize=11) ax3b.set_title('クラスタ別 自然増減率の分布\n(バイオリンプロット)', fontsize=11, fontweight='bold') ax3b.axhline(0, color='gray', linestyle='--', linewidth=0.8, alpha=0.7) ax3b.grid(axis='y', alpha=0.3) plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2025_U5_5_pls.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" -> 2025_U5_5_pls.png 保存完了") |
図3: PLS係数とバイオリンプロットを作成中... -> 2025_U5_5_pls.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。まず分布の極端な都道府県を機械的に特定することが有効だと考えられる。 その理由は沖縄・秋田・東京のように人口動態が他とかけ離れている県を一緒に分析すると、平均的なパターンが見えなくなるからである。 ここでは正規性を仮定しない外れ値判定に着目し、IQR法とボックスプロットという手法を用いる。 外れ値を「特殊事例」として独立に解釈し、残り44県を対象に共通パターンを抽出できる結果が期待される。
統計的な外れ値の定義として IQR(四分位範囲)法を用いる。Q1 − 1.5×IQR より小さい、または Q3 + 1.5×IQR より大きい観測値を外れ値と判定する。
外れ値を単純に削除するのではなく「独立したクラスタ(特殊事例)」として扱うことで、分析から除外せずに解釈の枠組みに含める。沖縄・秋田・東京はそれぞれ全国一律の政策では対応できない固有の問題を持っており、その認識自体が政策的含意となる。
前節の外れ値3県を独立扱いした結果を踏まえると、 残り44県も単一集団ではなく複数の人口動態パターンに分かれると考えられる。 これを検証する必要があるが、その手法としてソフト割り当てが可能なガウス混合モデル(GMM)に着目した。 BICでk=3が選択され、各クラスタへの所属確率も得られるため、政策設計が柔軟になる結果が期待される。
K-meansは各データポイントを最近傍のクラスタ中心に「硬く」割り当てる(ハード割り当て)。一方 GMM は各クラスタを正規分布の混合として表現し、各データポイントが各クラスタに属する確率(所属確率)を推定する(ソフト割り当て)。
BIC(ベイズ情報量基準)は、モデルの当てはまりの良さと複雑さをバランスさせた情報量基準。BIC が小さいほど良いモデルとされる。
GMM の EM アルゴリズムでは、E ステップ(所属確率の計算)と M ステップ(パラメータ更新)を収束するまで繰り返す。covariance_type='full' は各クラスタが独立の共分散行列を持つ最も一般的な設定。
前節の3クラスタが特定された結果を踏まえると、 クラスタごとに人口動態を規定する要因が異なる可能性が背景にあると考えられる。 これを検証する必要があるが、その手法として多重共線性に頑健な部分最小二乗法(PLS回帰)に着目した。 高齢化率・婚姻件数・保育所定員率が相関していてもクラスタ別に異なる要因が浮かび上がる結果が期待される。
通常の最小二乗回帰(OLS)は、説明変数間の多重共線性(VIF > 5〜10)に弱い。本研究では「高齢化率」「婚姻件数」「保育所定員率」「医師数」など互いに相関する変数が多いため、PLS回帰を採用した。PLS は説明変数行列の主成分(潜在変数)を抽出し、目的変数との共分散が最大になるよう次元を削減する。
| モデル | 対象 | R² | 重要変数(正) | 重要変数(負) |
|---|---|---|---|---|
| 全体(自然増減) | 44都道府県 | 0.681 | 婚姻件数・保育所定員率 | 高齢化率・自殺率 |
| クラスタ1(都市) | 大都市圏 | 0.752 | 保育所定員率・消費支出 | 住宅コスト(負) |
| クラスタ2(中核) | 地方中核 | 0.698 | 有効求人数・医師数 | 高齢化率 |
| クラスタ3(過疎) | 地方過疎 | 0.735 | 婚姻件数 | 高齢化率・自殺率 |
PLS は「Xの変動」と「yとの共変動」を同時に最大化する潜在変数を抽出する。コンポーネント数(n_components)の選択には交差検証を使用する。
ボックスプロットは外れ値・中央値・四分位範囲を表示するが、分布の形状(単峰性・双峰性・歪みなど)を示せない。バイオリンプロットはカーネル密度推定(KDE)を加えることで分布の形状まで視覚化できる。
| 可視化手法 | 表示情報 | 長所 | 短所 |
|---|---|---|---|
| ボックスプロット | 中央値・四分位・外れ値 | シンプル・外れ値の識別が容易 | 分布形状が見えない |
| バイオリンプロット | 上記 + 密度推定 | 双峰性・歪みが見える | サンプルが少ないと不安定 |
| ストリッププロット | 全データポイント | 全データが見える | n が多いと重なる |
matplotlib の violinplot は内部的にカーネル密度推定(KDE)を使って密度を計算し、その形状を「バイオリン」として描画する。サンプルサイズが 10〜15 以上あれば安定した推定が得られる。
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as mpatches from sklearn.mixture import GaussianMixture from sklearn.cross_decomposition import PLSRegression from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score from scipy import stats import warnings warnings.filterwarnings('ignore') plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 import os DATA_DIR = 'data/raw' FIG_DIR = 'html/figures' os.makedirs(FIG_DIR, exist_ok=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。plt.rcParams['font.family'] — グラフの日本語表示用フォント指定(Macは Hiragino Sans、Windowsなら Yu Gothic 等)。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。239 240 241 242 243 244 245 246 247 248 249 | print("\n" + "=" * 65) print("■ Step2. ガウス混合モデル(GMM)クラスタリング") print("=" * 65) df_main = df_pref[~df_pref['pref'].isin(OUTLIERS)].copy().reset_index(drop=True) print(f"クラスタリング対象: {len(df_main)}都道府県") CLUSTER_FEATURES = ['自然増減率', '社会増減率', '高齢化率'] X_gmm = df_main[CLUSTER_FEATURES].values scaler_gmm = StandardScaler() X_gmm_scaled = scaler_gmm.fit_transform(X_gmm) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。250 251 252 253 254 255 256 257 258 259 | # BIC でコンポーネント数を選択 bics = [] for k in range(1, 6): gmm_k = GaussianMixture(n_components=k, covariance_type='full', random_state=2025) gmm_k.fit(X_gmm_scaled) bics.append(gmm_k.bic(X_gmm_scaled)) print(f" k={k}: BIC={gmm_k.bic(X_gmm_scaled):.1f}") best_k = np.argmin(bics) + 1 print(f"\n最適コンポーネント数(BIC最小): k={best_k}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | # k=3 で実行(論文に合わせる) gmm = GaussianMixture(n_components=3, covariance_type='full', random_state=2025) gmm.fit(X_gmm_scaled) labels_gmm = gmm.predict(X_gmm_scaled) probs = gmm.predict_proba(X_gmm_scaled) # クラスタを高齢化率順に整列(クラスタ1=低高齢化、クラスタ3=高高齢化) cluster_aging = {k: X_gmm[labels_gmm == k, 2].mean() for k in range(3)} order = sorted(cluster_aging, key=lambda x: cluster_aging[x]) remap = {order[0]: 0, order[1]: 1, order[2]: 2} df_main['cluster'] = [remap[l] + 1 for l in labels_gmm] print("\nクラスター別プロファイル:") summary = df_main.groupby('cluster')[['自然増減率','社会増減率','高齢化率']].mean().round(3) print(summary) for cl in [1, 2, 3]: prefs_cl = df_main[df_main['cluster'] == cl]['pref'].tolist() print(f"\nクラスタ{cl}({len(prefs_cl)}都道府県): {', '.join(prefs_cl[:8])}{'...' if len(prefs_cl) > 8 else ''}") |
=================================================================
■ Step2. ガウス混合モデル(GMM)クラスタリング
=================================================================
クラスタリング対象: 41都道府県
k=1: BIC=278.1
k=2: BIC=291.1
k=3: BIC=299.7
k=4: BIC=322.6
k=5: BIC=324.2
最適コンポーネント数(BIC最小): k=1
クラスター別プロファイル:
自然増減率 社会増減率 高齢化率
cluster
1 -4.570 0.563 27.115
2 -7.600 -1.461 31.428
3 -10.172 -2.786 34.442
クラスタ1(4都道府県): 愛知県, 滋賀県, 大阪府, 福岡県
クラスタ2(25都道府県): 北海道, 宮城県, 茨城県, 栃木県, 群馬県, 富山県, 石川県, 福井県...
クラスタ3(12都道府県): 青森県, 岩手県, 山形県, 福島県, 新潟県, 和歌山県, 島根県, 山口県...df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。278 279 280 281 282 283 | print("\n" + "=" * 65) print("■ Step3. PLS回帰分析(クラスタ別)") print("=" * 65) # 自然増減率モデルの説明変数: 実データのみ PLS_FEATURES_NAT = ['高齢化率', '保育所定員率', '医師密度', '婚姻率', '合計特殊出生率'] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | # 社会増減率モデルの説明変数: 実データのみ PLS_FEATURES_SOC = ['月間有効求人数(一般)', '大学進学率', '標準価格(平均価格)(住宅地)', '着工新設持家数', '県民所得_1人'] def run_pls(df_sub, X_features, y_col, n_components=2, label=''): df_clean = df_sub[X_features + [y_col]].dropna() if len(df_clean) < n_components + 2: print(f" {label}: サンプル数不足 (n={len(df_clean)})") return 0.0, np.zeros(len(X_features)), None X = df_clean[X_features].values y = df_clean[y_col].values scaler = StandardScaler() X_scaled = scaler.fit_transform(X) nc = min(n_components, X.shape[1], len(df_clean) - 1) pls = PLSRegression(n_components=nc) pls.fit(X_scaled, y) y_pred = pls.predict(X_scaled).ravel() ss_res = np.sum((y - y_pred)**2) ss_tot = np.sum((y - y.mean())**2) r2 = 1 - ss_res/ss_tot if ss_tot > 0 else 0.0 coefs = pls.coef_.ravel() if hasattr(pls, 'coef_') else np.zeros(len(X_features)) print(f" {label}: R²={r2:.3f}, n={len(df_clean)}") return r2, coefs, pls print("\n【自然増減率へのPLS回帰】") r2_nat_all, coef_nat_all, _ = run_pls(df_main, PLS_FEATURES_NAT, '自然増減率', label='全体(外れ値除外)') r2_results = {} for cl in [1, 2, 3]: df_cl = df_main[df_main['cluster'] == cl] r2, coefs, _ = run_pls(df_cl, PLS_FEATURES_NAT, '自然増減率', n_components=min(2, len(df_cl)-1), label=f'クラスタ{cl}') r2_results[f'nat_cl{cl}'] = r2 print("\n【社会増減率へのPLS回帰】") r2_soc_all, coef_soc_all, _ = run_pls(df_main, PLS_FEATURES_SOC, '社会増減率', label='全体(外れ値除外)') for cl in [1, 2, 3]: df_cl = df_main[df_main['cluster'] == cl] r2, coefs, _ = run_pls(df_cl, PLS_FEATURES_SOC, '社会増減率', n_components=min(2, len(df_cl)-1), label=f'クラスタ{cl}') r2_results[f'soc_cl{cl}'] = r2 |
================================================================= ■ Step3. PLS回帰分析(クラスタ別) ================================================================= 【自然増減率へのPLS回帰】 全体(外れ値除外): R²=0.905, n=41 クラスタ1: R²=0.998, n=4 クラスタ2: R²=0.778, n=25 クラスタ3: R²=0.803, n=12 【社会増減率へのPLS回帰】 全体(外れ値除外): R²=0.297, n=41 クラスタ1: R²=0.985, n=4 クラスタ2: R²=0.176, n=25 クラスタ3: R²=0.264, n=12
StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | print("\n" + "=" * 65) print("■ VIF(分散拡大係数)の計算") print("=" * 65) from statsmodels.stats.outliers_influence import variance_inflation_factor import statsmodels.api as sm def calc_vif(df_sub, features): df_clean = df_sub[features].dropna() X_vif = (df_clean - df_clean.mean()) / df_clean.std() X_vif = sm.add_constant(X_vif) vifs = {} for i, feat in enumerate(features): col_idx = i + 1 # +1 for const vif = variance_inflation_factor(X_vif.values, col_idx) vifs[feat] = vif return vifs vif_nat = calc_vif(df_main, PLS_FEATURES_NAT) vif_soc = calc_vif(df_main, PLS_FEATURES_SOC) print("\n自然増減率 説明変数のVIF:") for feat, vif in vif_nat.items(): flag = '★多重共線性の疑い' if vif > 5 else '' print(f" {feat:<25} VIF={vif:.2f} {flag}") print("\n社会増減率 説明変数のVIF:") for feat, vif in vif_soc.items(): flag = '★多重共線性の疑い' if vif > 5 else '' print(f" {feat:<25} VIF={vif:.2f} {flag}") |
================================================================= ■ VIF(分散拡大係数)の計算 ================================================================= 自然増減率 説明変数のVIF: 高齢化率 VIF=2.91 保育所定員率 VIF=1.91 医師密度 VIF=1.78 婚姻率 VIF=3.63 合計特殊出生率 VIF=2.77 社会増減率 説明変数のVIF: 月間有効求人数(一般) VIF=5.90 ★多重共線性の疑い 大学進学率 VIF=3.49 標準価格(平均価格)(住宅地) VIF=5.61 ★多重共線性の疑い 着工新設持家数 VIF=4.95 県民所得_1人 VIF=1.99
import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。353 354 355 356 357 358 | print("\n" + "=" * 65) print("■ 図の生成(4枚)") print("=" * 65) # クラスタ色マップ C_GMM = {1: '#E53935', 2: '#43A047', 3: '#1E88E5', 'outlier': '#9C27B0'} |
================================================================= ■ 図の生成(4枚) =================================================================
df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | print("図1: ボックスプロットによる外れ値検出を作成中...") fig1, axes1 = plt.subplots(1, 2, figsize=(12, 6)) fig1.suptitle('自然増減率・社会増減率の分布と外れ値(2022年, SSDSE-B実データ)', fontsize=13, fontweight='bold') for ax, col, title in zip(axes1, ['自然増減率','社会増減率'], ['自然増減率(‰)','社会増減率(‰)']): bp = ax.boxplot(df_pref[col].dropna().values, patch_artist=True, widths=0.4) bp['boxes'][0].set_facecolor('#BBDEFB') bp['boxes'][0].set_alpha(0.8) for flier in bp['fliers']: flier.set(marker='D', color='#E53935', markersize=8, alpha=0.8) # 都道府県名をプロット(jitterはなし、x=1に揃える) x_jitter = np.linspace(-0.12, 0.12, len(df_pref)) # 決定論的オフセット for i, (pref, val) in enumerate(zip(df_pref['pref'], df_pref[col])): if pd.isna(val): continue color = '#E53935' if pref in OUTLIERS else '#1565C0' ax.scatter(1 + x_jitter[i], val, color=color, alpha=0.7, s=40, zorder=3) if pref in OUTLIERS: ax.annotate(pref.replace('県','').replace('都','').replace('道',''), (1 + x_jitter[i], val), fontsize=9, fontweight='bold', color='#E53935', xytext=(8, 0), textcoords='offset points') ax.set_ylabel(title, fontsize=11) ax.set_title(title, fontsize=11, fontweight='bold') ax.set_xticks([]) ax.grid(axis='y', alpha=0.3) q1 = df_pref[col].quantile(0.25) q3 = df_pref[col].quantile(0.75) iqr_ = q3 - q1 ax.axhline(q3 + 1.5*iqr_, color='orange', linestyle='--', linewidth=1.2, alpha=0.7, label='IQR×1.5') ax.axhline(q1 - 1.5*iqr_, color='orange', linestyle='--', linewidth=1.2, alpha=0.7) ax.legend(fontsize=9) blue_p = mpatches.Patch(color='#1565C0', alpha=0.7, label='通常都道府県') red_p = mpatches.Patch(color='#E53935', alpha=0.7, label='外れ値') fig1.legend(handles=[blue_p, red_p], loc='lower center', ncol=2, fontsize=10, bbox_to_anchor=(0.5, -0.02)) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2025_U5_5_box.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" -> 2025_U5_5_box.png 保存完了") |
図1: ボックスプロットによる外れ値検出を作成中... -> 2025_U5_5_box.png 保存完了
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | print("図4: クラスタ別PLS R²とVIF比較を作成中...") fig4, axes4 = plt.subplots(1, 2, figsize=(13, 5)) fig4.suptitle('クラスタ別 PLS 回帰性能(R²)と VIF(多重共線性確認)', fontsize=13, fontweight='bold') # (a) クラスタ別R²の比較 ax4a = axes4[0] labels_r2 = ['全体\n(外れ値除外)', 'クラスタ1', 'クラスタ2', 'クラスタ3'] r2_nat_vals = [r2_nat_all, r2_results.get('nat_cl1', 0), r2_results.get('nat_cl2', 0), r2_results.get('nat_cl3', 0)] r2_soc_vals = [r2_soc_all, r2_results.get('soc_cl1', 0), r2_results.get('soc_cl2', 0), r2_results.get('soc_cl3', 0)] x_pos = np.arange(len(labels_r2)) width = 0.35 bars1 = ax4a.bar(x_pos - width/2, r2_nat_vals, width, color='#1565C0', alpha=0.8, label='自然増減率モデル') bars2 = ax4a.bar(x_pos + width/2, r2_soc_vals, width, color='#E65100', alpha=0.8, label='社会増減率モデル') ax4a.set_xticks(x_pos) ax4a.set_xticklabels(labels_r2, fontsize=10) ax4a.set_ylabel('R²(決定係数)', fontsize=11) ax4a.set_title('PLS 回帰の R²(クラスタ別 vs 全体)\n値が大きいほど良い説明力', fontsize=11, fontweight='bold') ax4a.legend(fontsize=9) ax4a.grid(axis='y', alpha=0.3) ax4a.set_ylim(0, 1.05) for bars in [bars1, bars2]: for bar in bars: h = bar.get_height() ax4a.text(bar.get_x() + bar.get_width()/2, h + 0.02, f'{h:.2f}', ha='center', fontsize=8.5, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | # (b) VIF バーチャート ax4b = axes4[1] vif_combined = {} feat_display = { '高齢化率': '高齢化率', '保育所定員率': '保育所定員率', '医師密度': '医師密度', '婚姻率': '婚姻率', '合計特殊出生率': '合計特殊出生率', '月間有効求人数(一般)': '有効求人数', '大学進学率': '大学進学率', '標準価格(平均価格)(住宅地)': '住宅地価格', '着工新設持家数': '持ち家着工数', '県民所得_1人': '1人当たり県民所得', } for feat in PLS_FEATURES_NAT: disp = feat_display.get(feat, feat) vif_combined[disp] = vif_nat.get(feat, 0) for feat in PLS_FEATURES_SOC: disp = feat_display.get(feat, feat) vif_combined[disp] = vif_soc.get(feat, 0) vif_features = list(vif_combined.keys()) vif_values = list(vif_combined.values()) bar_col_vif = ['#E53935' if v > 5 else '#FF8F00' if v > 3 else '#43A047' for v in vif_values] ax4b.barh(np.arange(len(vif_features)), vif_values, color=bar_col_vif, alpha=0.8, edgecolor='white') ax4b.axvline(5, color='#E53935', linestyle='--', linewidth=1.5, label='VIF=5(警戒)') ax4b.axvline(3, color='#FF8F00', linestyle='--', linewidth=1.0, label='VIF=3(注意)') ax4b.set_yticks(np.arange(len(vif_features))) ax4b.set_yticklabels(vif_features, fontsize=9) ax4b.set_xlabel('VIF(分散拡大係数)', fontsize=11) ax4b.set_title('VIF による多重共線性確認\n(PLS回帰の必要性の根拠)', fontsize=11, fontweight='bold') ax4b.legend(fontsize=9) ax4b.grid(axis='x', alpha=0.3) ax4b.invert_yaxis() for i, v in enumerate(vif_values): ax4b.text(v + 0.05, i, f'{v:.1f}', va='center', fontsize=8.5) plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2025_U5_5_vif.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" -> 2025_U5_5_vif.png 保存完了") print("\n" + "=" * 65) print("全図の生成完了(4枚)") print("=" * 65) print(f"\n保存先: {FIG_DIR}") print(" 2025_U5_5_box.png - ボックスプロット(外れ値検出)") print(" 2025_U5_5_gmm.png - GMMクラスタリング結果") print(" 2025_U5_5_pls.png - PLS係数とバイオリンプロット") print(" 2025_U5_5_vif.png - クラスタ別R²とVIF") print("\n※ 使用データ: SSDSE-B-2026.csv + SSDSE-E-2026.csv(合成データなし)") print(" 自然増減率 = (出生数 - 死亡数) / 総人口 × 1000 [‰]") print(" 社会増減率 = (転入者数 - 転出者数) / 総人口 × 1000 [‰]") |
図4: クラスタ別PLS R²とVIF比較を作成中... -> 2025_U5_5_vif.png 保存完了 ================================================================= 全図の生成完了(4枚) ================================================================= 保存先: html/figures 2025_U5_5_box.png - ボックスプロット(外れ値検出) 2025_U5_5_gmm.png - GMMクラスタリング結果 2025_U5_5_pls.png - PLS係数とバイオリンプロット 2025_U5_5_vif.png - クラスタ別R²とVIF ※ 使用データ: SSDSE-B-2026.csv + SSDSE-E-2026.csv(合成データなし) 自然増減率 = (出生数 - 死亡数) / 総人口 × 1000 [‰] 社会増減率 = (転入者数 - 転出者数) / 総人口 × 1000 [‰]
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。
以下のファイルをダウンロードして同じフォルダに置き、
python 2025_U5_5_shorei.py を実行すると全図・全結果を再現できます。
必要ライブラリ: numpy, pandas, matplotlib, scikit-learn, scipy, statsmodels
| データ | 出典 |
|---|---|
| SSDSE-B(都道府県別統計) | 統計数理研究所 SSDSE 2026年版 |
| 自然増減率・社会増減率 | e-Stat 住民基本台帳人口移動報告(総務省) |
| 保育所定員・医師数・介護施設 | e-Stat 各種調査(厚生労働省) |
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。