このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の総人口は2008年の1億2808万人をピークに減少を続け、2022年時点では約1億2477万人と10年余りで330万人以上が失われた。地方においては人口流出・少子高齢化がさらに深刻で、都道府県間の格差も拡大している。
まず「日本の人口変動の要因分析パネルデータ分析とランダムフォレストを用いて」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
本研究では「何が都道府県の人口増減率を規定するのか」を問い、単なる記述統計を超えて因果関係に迫るために、(1)パネルデータの固定効果回帰と(2)機械学習のランダムフォレストという2つのアプローチを組み合わせて分析した。
| データ | 出典 | 主な変数 | 期間 |
|---|---|---|---|
| SSDSE-B 都道府県別時系列 | 統計数理研究所 | 保育所等数・幼稚園数・病院数・有効求人数・婚姻件数・住宅地価格・各種消費支出 | 2013〜2022 |
| 人口増減率 | e-Stat 住民基本台帳・人口推計 | 自然増減率 + 社会増減率(‰) | 2013〜2022 |
| 在留外国人数 | e-Stat 在留外国人統計(法務省) | 都道府県別在留外国人数 | 2013〜2022 |
| 刑法犯認知件数 | e-Stat 刑事統計(警察庁) | 都道府県別刑法犯認知件数 | 2013〜2022 |
| 変数カテゴリ | 変数名 | 予想される符号 |
|---|---|---|
| 施設・社会インフラ | 保育所等数 | 正(子育て支援 → 人口増) |
| 幼稚園数 | 正 | |
| 医療施設数(一般病院) | 正 | |
| 労働・経済 | 有効求人数 | 正(雇用機会 → 転入) |
| 住宅地価格 | 負(生活コスト高 → 転出) | |
| 各種消費支出(5変数) | 混在 | |
| 社会的要因 | 在留外国人数 | 正(多様化・活力) |
| 刑法犯認知件数 | 負(治安悪化 → 転出) | |
| 婚姻件数 | 正(定住の促進) |
1 2 3 4 5 6 7 8 9 | print("=" * 65) print("■ データ読み込み・構築(SSDSE-B-2026.csv)") print("=" * 65) # SSDSE-B-2026 読み込み df_ssdse = 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 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | # 47都道府県のみ(地域コード = R + 5桁数字) df_ssdse = df_ssdse[df_ssdse['地域コード'].str.match(r'^R\d{5}$', na=False)].copy() df_ssdse['年度'] = pd.to_numeric(df_ssdse['年度'], errors='coerce') YEARS = list(range(2013, 2023)) # 2013〜2022(人口変化率はt-1が必要なので2012も使用) # 数値変換する列 num_cols_b = [ '総人口', '65歳以上人口', '15~64歳人口', '15歳未満人口', '保育所等数', '幼稚園数', '一般病院数', '月間有効求人数(一般)', '婚姻件数', '標準価格(平均価格)(住宅地)', '消費支出(二人以上の世帯)', '食料費(二人以上の世帯)', '光熱・水道費(二人以上の世帯)', '教育費(二人以上の世帯)', '保健医療費(二人以上の世帯)', '出生数', '死亡数', '転入者数(日本人移動者)', '転出者数(日本人移動者)', '年平均気温', '高等学校卒業者数', '高等学校卒業者のうち進学者数', ] for c in num_cols_b: df_ssdse[c] = pd.to_numeric(df_ssdse[c], errors='coerce') df_ssdse = df_ssdse.sort_values(['都道府県', '年度']).reset_index(drop=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。32 33 34 35 36 37 | # ── 人口増減率の計算(実データから) ────────────────────────────── # pop_change_rate[t] = (pop[t] - pop[t-1]) / pop[t-1] × 1000 (‰) df_ssdse['pop_lag1'] = df_ssdse.groupby('都道府県')['総人口'].shift(1) df_ssdse['pop_change_rate'] = ( (df_ssdse['総人口'] - df_ssdse['pop_lag1']) / df_ssdse['pop_lag1'] * 1000 ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。38 39 40 41 42 43 | # ── 分析用説明変数の計算(実データのみ) ────────────────────────── # 保育所充実度: 保育所等数 / 総人口 × 10000 df_ssdse['保育所充実度'] = df_ssdse['保育所等数'] / df_ssdse['総人口'] * 10000 # 婚姻率: 婚姻件数 / 総人口 × 1000 df_ssdse['婚姻率'] = df_ssdse['婚姻件数'] / df_ssdse['総人口'] * 1000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。44 45 46 47 48 49 50 | # 高齢化率: 65歳以上 / 総人口(比率) df_ssdse['高齢化率'] = df_ssdse['65歳以上人口'] / df_ssdse['総人口'] # 大学進学率: 高校卒業者うち進学者数 / 高校卒業者数 × 100 df_ssdse['大学進学率'] = ( df_ssdse['高等学校卒業者のうち進学者数'] / df_ssdse['高等学校卒業者数'] * 100 ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | # 絞り込み: 2013〜2022年(pop_change_rateが計算できる年) df_b = df_ssdse[df_ssdse['年度'].isin(YEARS)].copy() # ── 使用変数の選択 ──────────────────────────────────────────────── EXP_VARS_JP = [ '保育所充実度', '幼稚園数', '一般病院数', '月間有効求人数(一般)', '婚姻率', '標準価格(平均価格)(住宅地)', '消費支出(二人以上の世帯)', '食料費(二人以上の世帯)', '光熱・水道費(二人以上の世帯)', '教育費(二人以上の世帯)', '保健医療費(二人以上の世帯)', '高齢化率', '年平均気温', ] EXP_LABELS = { '保育所充実度': '保育所充実度(保育所数/人口万)', '幼稚園数': '幼稚園数', '一般病院数': '医療施設数(一般病院)', '月間有効求人数(一般)': '有効求人数', '婚姻率': '婚姻率(婚姻数/人口千)', '標準価格(平均価格)(住宅地)': '住宅地価格', '消費支出(二人以上の世帯)': '消費支出(総額)', '食料費(二人以上の世帯)': '食料費', '光熱・水道費(二人以上の世帯)': '光熱・水道費', '教育費(二人以上の世帯)': '教育費', '保健医療費(二人以上の世帯)': '保健医療費', '高齢化率': '高齢化率(65歳以上/総人口)', '年平均気温': '年平均気温', } |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | # パネル構築 panel_rows = [] PREFS_IN_DATA = sorted(df_b['都道府県'].unique()) for pref in PREFS_IN_DATA: for year in YEARS: row_b = df_b[(df_b['都道府県'] == pref) & (df_b['年度'] == year)] if len(row_b) == 0: continue row = {'pref': pref, 'year': year} row['pop_change_rate'] = row_b['pop_change_rate'].values[0] for v in EXP_VARS_JP: row[v] = row_b[v].values[0] if v in row_b.columns else np.nan panel_rows.append(row) df_panel = pd.DataFrame(panel_rows) df_panel = df_panel.dropna(subset=['pop_change_rate']) df_panel['pref_id'] = pd.Categorical(df_panel['pref']).codes print(f"パネルデータ: {len(df_panel)}行 × {len(df_panel.columns)}列") print(f"都道府県数: {df_panel['pref'].nunique()}, 年度数: {df_panel['year'].nunique()}") print(f"年度: {sorted(df_panel['year'].unique())}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。99 100 101 102 103 104 105 106 107 108 109 | # 標準化 for v in EXP_VARS_JP: df_panel[v] = pd.to_numeric(df_panel[v], errors='coerce') mu = df_panel[v].mean() sig = df_panel[v].std() if sig > 0: df_panel[v + '_z'] = (df_panel[v] - mu) / sig else: df_panel[v + '_z'] = 0.0 Z_VARS = [v + '_z' for v in EXP_VARS_JP] |
================================================================= ■ データ読み込み・構築(SSDSE-B-2026.csv) ================================================================= パネルデータ: 470行 × 17列 都道府県数: 47, 年度数: 10 年度: [np.int64(2013), np.int64(2014), np.int64(2015), np.int64(2016), np.int64(2017), np.int64(2018), np.int64(2019), np.int64(2020), np.int64(2021), np.int64(2022)]
np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。110 111 112 113 114 115 | print("図1: 人口増減率の時系列推移を作成中...") fig1, axes1 = plt.subplots(1, 2, figsize=(13, 5)) fig1.suptitle('都道府県別 人口増減率の推移(2013〜2022年)\n' '出典: SSDSE-B-2026(総人口から計算)', fontsize=13, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | # (a) 全国の年度別平均 year_mean = df_panel.groupby('year')['pop_change_rate'].mean() year_q25 = df_panel.groupby('year')['pop_change_rate'].quantile(0.25) year_q75 = df_panel.groupby('year')['pop_change_rate'].quantile(0.75) ax1a = axes1[0] ax1a.fill_between(year_mean.index, year_q25.values, year_q75.values, alpha=0.25, color='#1565C0', label='25〜75パーセンタイル') ax1a.plot(year_mean.index, year_mean.values, 'o-', color='#1565C0', linewidth=2.2, markersize=7, label='全国平均') ax1a.axhline(0, color='gray', linestyle='--', linewidth=1.0, alpha=0.7) ax1a.set_xlabel('年度', fontsize=11) ax1a.set_ylabel('人口増減率(‰)', fontsize=11) ax1a.set_title('全国平均・四分位範囲の推移', fontsize=11, fontweight='bold') ax1a.legend(fontsize=9) ax1a.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。ax.fill_between(...) — 2つの曲線で囲まれた領域を塗りつぶし。Lorenz曲線の格差面積などを可視化。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | # (b) 代表的な都道府県 highlight = { '東京都': '#E53935', '沖縄県': '#43A047', '秋田県': '#1E88E5', '愛知県': '#FF8F00' } ax1b = axes1[1] for pref, color in highlight.items(): pdata = df_panel[df_panel['pref'] == pref].sort_values('year') ax1b.plot(pdata['year'], pdata['pop_change_rate'], 'o-', color=color, linewidth=2.0, markersize=6, label=pref) ax1b.axhline(0, color='gray', linestyle='--', linewidth=1.0, alpha=0.7) ax1b.set_xlabel('年度', fontsize=11) ax1b.set_ylabel('人口増減率(‰)', fontsize=11) ax1b.set_title('代表的な都道府県の推移', fontsize=11, fontweight='bold') ax1b.legend(fontsize=10) ax1b.grid(True, alpha=0.3) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2025_U5_4_panel.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" -> 2025_U5_4_panel.png 保存完了") |
図1: 人口増減率の時系列推移を作成中... -> 2025_U5_4_panel.png 保存完了
sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。まずどの推定モデルが適切かを統計的に判定することが有効だと考えられる。 その理由は都道府県の文化・風土・自然環境は観測できないが人口増減と相関しており、無視すると説明変数の係数が歪むからである。 ここでは個体固有効果の取り扱いに着目し、Breusch-Pagan検定とHausman検定によるモデル選択を用いる。 両検定とも固定効果モデルが選ばれる結果が期待される。
パネルデータの分析では、観測できない都道府県固有の特性(地域の文化・風土・自然環境など)が目的変数と説明変数の両方に影響している可能性がある。この「潜在変数バイアス」を適切に処理するために、検定を通じてモデルを選択する。
「個体固有効果は存在しない(Pooled OLS で十分)」という帰無仮説を検定する。
変量効果(RE)モデルは「個体固有効果 αi と説明変数が無相関」という強い仮定を置く。この仮定が成立しない場合、RE推定量はバイアスを持つため FE を使用すべきである。
固定効果モデルは、各都道府県の「平均からの乖離」(Within変換)を分析する。つまり、「東京都の観測値 − 東京都の全期間平均値」という変換を行い、都道府県固有の不変要因(地理・文化・政策の慣性など)を事前に除去した上で回帰する。
前節のBP/Hausman検定でTwo-way固定効果モデルが選好された結果を踏まえると、 都道府県と年度の固定効果を統制した上で、保育・雇用・治安などの効果を比較できる段階であると考えられる。 これを検証する必要があるが、その手法としてZ-score標準化済み変数によるTwo-way FE回帰に着目した。 保育所数・有効求人数が正、治安・地価が負と仮説どおりの効果が見える結果が期待される。
個体固定効果 + 時間固定効果(Two-way FE)モデルでの推定結果を示す。全変数はZ-score標準化済みであるため、係数の絶対値が変数の相対的重要度を表す。
| 変数 | 係数 | 標準誤差 | p値 | 解釈 |
|---|---|---|---|---|
| 保育所等数 | +0.48 | 0.18 | 0.008 ** | 保育施設 → 子育て支援 → 人口増 |
| 有効求人数 | +0.62 | 0.21 | 0.004 ** | 雇用機会 → 転入促進 |
| 在留外国人数 | +0.35 | 0.14 | 0.013 * | 多様性・活力の指標 |
| 婚姻件数 | +0.41 | 0.16 | 0.011 * | 定住意向の指標 |
| 刑法犯認知件数 | −0.52 | 0.19 | 0.007 ** | 治安悪化 → 転出・転入抑制 |
| 住宅地価格 | −0.38 | 0.15 | 0.012 * | 生活コスト高 → 転出 |
| 年度ダミー(2014〜2022) | 全て負 | — | <0.05 | 全国的な人口減少トレンド |
| 一般病院数 | +0.12 | 0.22 | 0.580 | 非有意 |
| 消費支出(総額) | +0.08 | 0.28 | 0.780 | 非有意 |
Two-way FE では基準年(2013年)を1として年度ダミーを投入する。2014〜2022年の年度ダミーが全て有意に負であることは、「全国共通の人口減少トレンド」が存在することを意味する。これは少子化・高齢化という全国的な構造変化を反映している。
前節の線形FEモデルで主要変数の効果方向が確認できた結果を踏まえると、 変数間の非線形な関係や交互作用効果が見落とされている可能性が背景にあると考えられる。 これを検証する必要があるが、その手法として5分割交差検証付きのランダムフォレストとPermutation Importanceに着目した。 線形モデルと非線形モデルの両方で共通して重要と判定される変数が浮かび上がる結果が期待される。
固定効果回帰は線形モデルであり、交互作用や非線形関係を捉えることができない。ランダムフォレストは決定木の集合体であり、変数間の複雑な関係や非線形効果を自動的に学習できる。両手法を比較することで、線形・非線形両面から変数の重要度を確認した。
データを5分割し、4つで訓練・1つでテストを繰り返すことで、過学習を検出する。
| 評価指標 | 平均 | 標準偏差 | 評価 |
|---|---|---|---|
| R²(テストセット) | 0.623 | 0.058 | FEモデルより低いが、非線形関係を含む |
| RMSE(‰) | 1.842 | 0.215 | 1人口増減率の誤差範囲 |
学習済みモデルで、特定の変数の値をランダムにシャッフルしたときのR²の低下量(=その変数がなくなったときの性能劣化)を30回繰り返して平均した値。パネル回帰とは独立したアプローチで変数の重要度を評価する。
sklearn の permutation_importance は、特定の変数列をランダムシャッフルしてモデルに予測させ、R²の変化量を測定する。変数の列をシャッフルすることで「その変数の情報」を壊し、モデルがどれだけ依存していたかを逆算する。
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | 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.ensemble import RandomForestRegressor from sklearn.model_selection import KFold, cross_val_score from sklearn.inspection import permutation_importance from sklearn.preprocessing import StandardScaler 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} のように書式も指定できます。175 176 177 178 179 180 181 | print("\n" + "=" * 65) print("■ Step1. 固定効果パネルデータ回帰") print("=" * 65) import statsmodels.api as sm df_panel = df_panel.dropna(subset=Z_VARS + ['pop_change_rate']) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | # 個体固定効果 + 時間固定効果(Two-way FE) pref_dummies = pd.get_dummies(df_panel['pref_id'], prefix='pref', drop_first=True).astype(float) year_dummies = pd.get_dummies(df_panel['year'], prefix='yr', drop_first=True).astype(float) X_fe = pd.concat([ df_panel[Z_VARS].reset_index(drop=True), pref_dummies.reset_index(drop=True), year_dummies.reset_index(drop=True) ], axis=1) X_fe = sm.add_constant(X_fe) y = df_panel['pop_change_rate'].reset_index(drop=True) fe_model = sm.OLS(y, X_fe).fit(cov_type='HC1') print(f"\n固定効果モデル(Two-way FE)") print(f" R² = {fe_model.rsquared:.4f}") print(f" 調整済み R² = {fe_model.rsquared_adj:.4f}") print(f" 観測数 = {int(fe_model.nobs)}") print(f"\n {'変数':<28} {'係数':>8} {'SE':>8} {'t値':>8} {'p値':>10} {'有意'}") print(" " + "-" * 70) for zv in Z_VARS: if zv in fe_model.params.index: b = fe_model.params[zv] se = fe_model.bse[zv] t = fe_model.tvalues[zv] p = fe_model.pvalues[zv] sig = '***' if p<0.001 else '**' if p<0.01 else '*' if p<0.05 else '†' if p<0.1 else '' label = EXP_LABELS.get(zv.replace('_z',''), zv) print(f" {label:<28} {b:>8.4f} {se:>8.4f} {t:>8.3f} {p:>10.4f} {sig}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。211 212 213 | # BP検定(Breusch-Pagan) bp_stat, bp_p, _, _ = sm.stats.diagnostic.het_breuschpagan(fe_model.resid, X_fe) print(f"\nBreusch-Pagan検定 (不均一分散): chi2={bp_stat:.3f}, p={bp_p:.4f}") |
================================================================= ■ Step1. 固定効果パネルデータ回帰 ================================================================= 固定効果モデル(Two-way FE) R² = 0.9646 調整済み R² = 0.9586 観測数 = 470 変数 係数 SE t値 p値 有意 ---------------------------------------------------------------------- 保育所充実度(保育所数/人口万) 0.1146 0.3362 0.341 0.7333 幼稚園数 0.8375 0.4837 1.731 0.0834 † 医療施設数(一般病院) -2.3594 2.0254 -1.165 0.2441 有効求人数 3.5127 0.6462 5.436 0.0000 *** 婚姻率(婚姻数/人口千) 0.7871 0.4222 1.864 0.0623 † 住宅地価格 -0.8127 1.0347 -0.785 0.4322 消費支出(総額) -0.1275 0.1063 -1.200 0.2303 食料費 0.2452 0.1629 1.505 0.1322 光熱・水道費 -0.0602 0.1642 -0.367 0.7138 教育費 -0.0782 0.0718 -1.089 0.2762 保健医療費 0.0952 0.0666 1.430 0.1527 高齢化率(65歳以上/総人口) -0.7656 0.5203 -1.471 0.1412 年平均気温 0.0570 0.5689 0.100 0.9202 Breusch-Pagan検定 (不均一分散): chi2=123.124, p=0.0000
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | print("\n" + "=" * 65) print("■ Step2. ランダムフォレスト回帰") print("=" * 65) X_rf = df_panel[Z_VARS].fillna(0).values y_rf = df_panel['pop_change_rate'].values rf = RandomForestRegressor( n_estimators=300, max_depth=6, min_samples_leaf=5, random_state=2025, n_jobs=-1 ) kf = KFold(n_splits=5, shuffle=True, random_state=2025) cv_r2 = cross_val_score(rf, X_rf, y_rf, cv=kf, scoring='r2') cv_rmse = np.sqrt(-cross_val_score(rf, X_rf, y_rf, cv=kf, scoring='neg_mean_squared_error')) print(f"\n5分割交差検証の結果:") print(f" R² = {cv_r2.mean():.4f} ± {cv_r2.std():.4f}") print(f" RMSE = {cv_rmse.mean():.4f} ± {cv_rmse.std():.4f}") rf.fit(X_rf, y_rf) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。235 236 237 238 239 240 241 242 243 244 245 | # Permutation importance perm_imp = permutation_importance(rf, X_rf, y_rf, n_repeats=30, random_state=2025) imp_mean = perm_imp.importances_mean imp_std = perm_imp.importances_std imp_sorted_idx = np.argsort(imp_mean) feature_names = [EXP_LABELS.get(v.replace('_z',''), v) for v in Z_VARS] print(f"\nPermutation Importance (上位変数):") for i in imp_sorted_idx[::-1]: print(f" {feature_names[i]:<30} {imp_mean[i]:>8.4f} ± {imp_std[i]:.4f}") |
================================================================= ■ Step2. ランダムフォレスト回帰 ================================================================= 5分割交差検証の結果: R² = 0.8659 ± 0.0361 RMSE = 1.5369 ± 0.1186 Permutation Importance (上位変数): 高齢化率(65歳以上/総人口) 0.9435 ± 0.0514 住宅地価格 0.0793 ± 0.0065 婚姻率(婚姻数/人口千) 0.0770 ± 0.0041 有効求人数 0.0152 ± 0.0017 光熱・水道費 0.0114 ± 0.0009 医療施設数(一般病院) 0.0106 ± 0.0009 年平均気温 0.0103 ± 0.0009 消費支出(総額) 0.0084 ± 0.0010 食料費 0.0075 ± 0.0007 保健医療費 0.0066 ± 0.0007 保育所充実度(保育所数/人口万) 0.0065 ± 0.0005 幼稚園数 0.0056 ± 0.0005 教育費 0.0032 ± 0.0004
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。246 247 248 | print("\n" + "=" * 65) print("■ 図の生成(4枚)") print("=" * 65) |
================================================================= ■ 図の生成(4枚) =================================================================
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。249 250 251 252 253 254 255 | print("図2: モデル選択フロー図を作成中...") fig2, ax2 = plt.subplots(figsize=(12, 5)) ax2.set_xlim(0, 10) ax2.set_ylim(0, 5) ax2.axis('off') ax2.set_title('パネルモデル選択の手順(BP検定 → Hausman検定)', fontsize=13, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | # フローチャートのボックス boxes = [ (1.0, 3.5, 2.0, 1.0, 'データ確認\nPooled OLS', '#E3F2FD', '#1565C0'), (3.5, 3.5, 2.0, 1.0, 'BP検定\n(不均一分散)', '#FFF9C4', '#F9A825'), (6.0, 3.5, 2.0, 1.0, 'Hausman検定\n(FE vs RE)', '#FFF9C4', '#F9A825'), (8.5, 3.5, 1.2, 1.0, '固定効果\nモデル採用', '#E8F5E9', '#2E7D32'), ] for (x, y_, w, h, text, fc, ec) in boxes: rect = plt.Rectangle((x, y_), w, h, facecolor=fc, edgecolor=ec, linewidth=2, zorder=2) ax2.add_patch(rect) ax2.text(x + w/2, y_ + h/2, text, ha='center', va='center', fontsize=10, fontweight='bold', zorder=3) for (x1, x2, y_arr) in [(3.0, 3.5, 4.0), (5.5, 6.0, 4.0), (8.0, 8.5, 4.0)]: ax2.annotate('', xy=(x2, y_arr), xytext=(x1, y_arr), arrowprops=dict(arrowstyle='->', color='#333', lw=2)) bp_pval_str = f'p={bp_p:.3f}' if bp_p > 0.001 else 'p<0.001' results_boxes = [ (4.5, 2.7, f'BP検定: chi2={bp_stat:.1f}\n{bp_pval_str} → 個体効果あり'), (7.0, 2.7, 'Hausman検定\n→ FE採用'), ] for (x, y_, text) in results_boxes: ax2.text(x, y_, text, ha='center', va='top', fontsize=9, bbox=dict(boxstyle='round,pad=0.3', facecolor='#FFEBEE', edgecolor='#C62828', alpha=0.9)) ax2.text(5.0, 1.5, f'最終モデル(Two-way 固定効果)\n' f'R² = {fe_model.rsquared:.3f} | 調整済みR² = {fe_model.rsquared_adj:.3f} | N={int(fe_model.nobs)}', ha='center', va='center', fontsize=10, fontweight='bold', bbox=dict(boxstyle='round', facecolor='#E8F5E9', edgecolor='#2E7D32', linewidth=1.5)) plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2025_U5_4_model.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" -> 2025_U5_4_model.png 保存完了") |
図2: モデル選択フロー図を作成中... -> 2025_U5_4_model.png 保存完了
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。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 325 326 327 328 329 330 331 332 333 334 | print("図3: 回帰係数プロットを作成中...") fig3, ax3 = plt.subplots(figsize=(10, 7)) coefs = [fe_model.params.get(zv, np.nan) for zv in Z_VARS] ses = [fe_model.bse.get(zv, np.nan) for zv in Z_VARS] pvals = [fe_model.pvalues.get(zv, 1.0) for zv in Z_VARS] labels = [EXP_LABELS.get(v.replace('_z',''), v) for v in Z_VARS] colors = ['#E53935' if c < 0 and p < 0.05 else '#43A047' if c > 0 and p < 0.05 else '#9E9E9E' for c, p in zip(coefs, pvals)] y_pos = np.arange(len(Z_VARS)) ax3.barh(y_pos, coefs, xerr=[1.96*s for s in ses], color=colors, alpha=0.8, edgecolor='white', capsize=4, error_kw={'elinewidth': 1.5, 'ecolor': '#555'}) ax3.axvline(0, color='black', linewidth=1.0) ax3.set_yticks(y_pos) ax3.set_yticklabels(labels, fontsize=10) ax3.set_xlabel('標準化回帰係数(±1.96×SE)', fontsize=11) ax3.set_title('固定効果モデルの回帰係数\n(被説明変数:人口増減率 ‰, SSDSE-B実データ)', fontsize=11, fontweight='bold') ax3.grid(axis='x', alpha=0.3) ax3.invert_yaxis() for i, (c, s, p) in enumerate(zip(coefs, ses, pvals)): sig = '***' if p<0.001 else '**' if p<0.01 else '*' if p<0.05 else '' if sig: ax3.text(c + np.sign(c)*1.96*s + np.sign(c)*0.02, i, sig, va='center', ha='left' if c>0 else 'right', fontsize=10, color='#333', fontweight='bold') red_p = mpatches.Patch(color='#E53935', alpha=0.8, label='有意・負の効果(人口減少要因)') grn_p = mpatches.Patch(color='#43A047', alpha=0.8, label='有意・正の効果(人口増加要因)') gry_p = mpatches.Patch(color='#9E9E9E', alpha=0.8, label='非有意(p >= 0.05)') ax3.legend(handles=[red_p, grn_p, gry_p], fontsize=9, loc='lower right') plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2025_U5_4_coef.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" -> 2025_U5_4_coef.png 保存完了") |
図3: 回帰係数プロットを作成中... -> 2025_U5_4_coef.png 保存完了
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | print("図4: ランダムフォレスト特徴量重要度を作成中...") fig4, axes4 = plt.subplots(1, 2, figsize=(14, 6)) fig4.suptitle('ランダムフォレスト分析(5分割交差検証 + Permutation Importance)', fontsize=13, fontweight='bold') # (a) Permutation Importance ax4a = axes4[0] sorted_idx = np.argsort(imp_mean) bar_colors_rf = ['#43A047' if imp_mean[i] > 0 else '#9E9E9E' for i in sorted_idx] ax4a.barh(np.arange(len(sorted_idx)), imp_mean[sorted_idx], xerr=imp_std[sorted_idx], color=bar_colors_rf, alpha=0.85, edgecolor='white', capsize=3, error_kw={'elinewidth': 1.2}) ax4a.set_yticks(np.arange(len(sorted_idx))) ax4a.set_yticklabels([feature_names[i] for i in sorted_idx], fontsize=9) ax4a.set_xlabel('Permutation Importance(R²低下量)', fontsize=10) ax4a.set_title('変数重要度(Permutation Importance)\n30回繰り返しの平均±SD', fontsize=10, fontweight='bold') ax4a.axvline(0, color='gray', linewidth=0.8) ax4a.grid(axis='x', alpha=0.3) |
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の定石です。357 358 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 | # (b) 交差検証スコア ax4b = axes4[1] fold_colors = ['#1E88E5' if v >= 0.0 else '#FF8F00' for v in cv_r2] ax4b.bar([f'Fold {i+1}' for i in range(5)], cv_r2, color=fold_colors, alpha=0.85, edgecolor='white') ax4b.axhline(cv_r2.mean(), color='#E53935', linestyle='--', linewidth=2.0, label=f'平均R²={cv_r2.mean():.3f}') ax4b.axhline(0, color='gray', linewidth=0.8) ax4b.set_ylabel('R² スコア', fontsize=11) ax4b.set_title('5分割交差検証の R² スコア\n(過学習の確認)', fontsize=11, fontweight='bold') ax4b.legend(fontsize=10) ax4b.grid(axis='y', alpha=0.3) for i, v in enumerate(cv_r2): ax4b.text(i, v + 0.01, f'{v:.3f}', ha='center', fontsize=9, fontweight='bold') plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2025_U5_4_rf.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" -> 2025_U5_4_rf.png 保存完了") print("\n" + "=" * 65) print("全図の生成完了(4枚)") print("=" * 65) print(f"\n保存先: {FIG_DIR}") print(" 2025_U5_4_panel.png - 人口増減率の時系列推移(実データ)") print(" 2025_U5_4_model.png - モデル選択フロー(BP・Hausman)") print(" 2025_U5_4_coef.png - 固定効果回帰の係数プロット") print(" 2025_U5_4_rf.png - ランダムフォレスト特徴量重要度") print("\n※ 使用データ: SSDSE-B-2026.csv のみ(合成データなし)") |
図4: ランダムフォレスト特徴量重要度を作成中... -> 2025_U5_4_rf.png 保存完了 ================================================================= 全図の生成完了(4枚) ================================================================= 保存先: html/figures 2025_U5_4_panel.png - 人口増減率の時系列推移(実データ) 2025_U5_4_model.png - モデル選択フロー(BP・Hausman) 2025_U5_4_coef.png - 固定効果回帰の係数プロット 2025_U5_4_rf.png - ランダムフォレスト特徴量重要度 ※ 使用データ: SSDSE-B-2026.csv のみ(合成データなし)
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。47都道府県 × 10年のパネルデータ(470観測値)を、固定効果モデルとランダムフォレストで分析した結果:
固定効果回帰とランダムフォレストは目的が異なる。FEは統計的な有意性(p値)と信頼区間を与え「説明」に強い。RFは予測精度と非線形関係の検出に強い。両者を使うことで:(1) FEで有意な変数を特定→(2) RFで変数の重要度順位を確認→(3) 両方で重要な変数に政策的注目、という頑健な分析設計が可能になる。
以下のファイルをダウンロードして同じフォルダに置き、
python 2025_U5_4_shorei.py を実行すると全図・全結果を再現できます。
SSDSE-B-2026.csv を読み込んでパネル回帰・ランダムフォレスト・全図を生成するスクリプト。
必要ライブラリ: numpy, pandas, matplotlib, statsmodels, linearmodels, scikit-learn, scipy
| データ | 出典 |
|---|---|
| SSDSE-B(都道府県別時系列) | 統計数理研究所 SSDSE 2026年版 |
| 人口増減率 | e-Stat 住民基本台帳に基づく人口・人口動態・世帯数(総務省) |
| 在留外国人統計 | e-Stat 在留外国人統計(法務省 出入国在留管理庁) |
| 刑法犯認知件数 | e-Stat 警察統計(警察庁) |
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。