このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の合計特殊出生率(TFR)は長期低下傾向にあり、2023年の全国平均は 1.20 と過去最低を更新した。しかし都道府県間の格差は大きく、最高(沖縄県)と最低(東京都)の差は0.6以上に及ぶ。この地域差を生む要因を統計的に明らかにすることは、少子化対策の効果的立案に不可欠である。
まず「都道府県別出生率の予測モデル構築Ridge回帰・Lasso回帰による変数選択と正則化」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
Ridge回帰 Lasso回帰 交差検証 係数パス図 SSDSE-B
SSDSE(社会・人口統計体系データセット)-B の2023年断面データを使用。全47都道府県、欠損値なし。
| 項目 | 内容 |
|---|---|
| データソース | SSDSE-B-2026.csv(都道府県統計) |
| 使用年度 | 2023年(最新年) |
| サンプル数 | 47都道府県 |
| 目的変数 | 合計特殊出生率(TFR) |
| 説明変数数 | 8変数 |
| 変数名 | 計算方法 | 仮説の方向 | 根拠 |
|---|---|---|---|
| 婚姻率(‰) | 婚姻件数 ÷ 総人口 × 1000 | 正(+) | 婚姻が出産の前提条件 |
| 保育所定員率 | 保育所等定員数 ÷ 総人口 × 10000 | 正(+) | 保育環境の整備が出産促進 |
| 保育待機児童率(%) | 待機児童数 ÷ 在所児数 × 100 | 負(−) | 待機児童多い = 保育不足 |
| 高齢化率(%) | 65歳以上人口 ÷ 総人口 × 100 | 正?/負? | 若年人口比の逆代理 |
| 年少人口率(%) | 15歳未満人口 ÷ 総人口 × 100 | 正(+) | 子育て環境の豊かさ |
| 総人口(万人) | 総人口 ÷ 10000 | 負(−) | 都市化の代理指標 |
| 消費支出(円) | 消費支出(二人以上の世帯) | 正? | 経済水準・生活コスト |
| 保健医療費(円) | 保健医療費(二人以上の世帯) | 正? | 医療へのアクセス・高齢化 |
1 2 3 | YEAR = 2023 df = df_b[df_b['年度'] == YEAR].copy().reset_index(drop=True) print(f"\n使用年度: {YEAR}年, 都道府県数: {len(df)}") |
使用年度: 2023年, 都道府県数: 47
df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。4 5 6 7 8 9 10 11 12 | TARGET = '合計特殊出生率' # 人口ベースの比率変数を計算 df['高齢化率'] = df['65歳以上人口'] / df['総人口'] * 100 df['年少人口率'] = df['15歳未満人口'] / df['総人口'] * 100 df['婚姻率'] = df['婚姻件数'] / df['総人口'] * 1000 df['保育所定員率'] = df['保育所等定員数'] / df['総人口'] * 10000 df['保育待機児童率'] = df['保育所等利用待機児童数'] / df['保育所等在所児数'] * 100 df['人口密度代理'] = df['総人口'] / 10000 # 万人単位(都市化の代理) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # 説明変数リスト(8変数) FEAT_COLS = [ '婚姻率', # 婚姻率(人口千人当たり) '保育所定員率', # 保育所定員数(人口万人当たり) '保育待機児童率', # 保育待機児童率(在所児対比%) '高齢化率', # 65歳以上人口比率 '年少人口率', # 15歳未満人口比率 '人口密度代理', # 総人口(万人):都市化・人口規模の代理 '消費支出(二人以上の世帯)', # 消費支出 '保健医療費(二人以上の世帯)', # 保健医療費 ] FEAT_LABELS = { '婚姻率': '婚姻率\n(‰)', '保育所定員率': '保育所定員率\n(万人対)', '保育待機児童率': '保育待機\n児童率(%)', '高齢化率': '高齢化率\n(%)', '年少人口率': '年少人口率\n(%)', '人口密度代理': '総人口\n(万人)', '消費支出(二人以上の世帯)': '消費支出\n(円)', '保健医療費(二人以上の世帯)': '保健医療費\n(円)', } |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。35 36 37 38 39 40 41 42 43 44 45 | # 欠損値処理 cols_needed = [TARGET] + FEAT_COLS + ['都道府県'] df_clean = df[cols_needed].dropna().copy() print(f"\n欠損除外後: {len(df_clean)} 都道府県") X = df_clean[FEAT_COLS].values y = df_clean[TARGET].values.astype(float) prefs = df_clean['都道府県'].values print(f"\n目的変数(合計特殊出生率)の統計:") print(f" 平均={y.mean():.3f}, 標準偏差={y.std():.3f}, 最小={y.min():.2f}, 最大={y.max():.2f}") |
欠損除外後: 47 都道府県 目的変数(合計特殊出生率)の統計: 平均=1.293, 標準偏差=0.132, 最小=0.99, 最大=1.60
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。46 47 48 49 50 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 | region_map = { '北海道': '北海道・東北', '青森県': '北海道・東北', '岩手県': '北海道・東北', '宮城県': '北海道・東北', '秋田県': '北海道・東北', '山形県': '北海道・東北', '福島県': '北海道・東北', '茨城県': '関東', '栃木県': '関東', '群馬県': '関東', '埼玉県': '関東', '千葉県': '関東', '東京都': '関東', '神奈川県': '関東', '新潟県': '中部', '富山県': '中部', '石川県': '中部', '福井県': '中部', '山梨県': '中部', '長野県': '中部', '岐阜県': '中部', '静岡県': '中部', '愛知県': '中部', '三重県': '近畿', '滋賀県': '近畿', '京都府': '近畿', '大阪府': '近畿', '兵庫県': '近畿', '奈良県': '近畿', '和歌山県': '近畿', '鳥取県': '中国・四国', '島根県': '中国・四国', '岡山県': '中国・四国', '広島県': '中国・四国', '山口県': '中国・四国', '徳島県': '中国・四国', '香川県': '中国・四国', '愛媛県': '中国・四国', '高知県': '中国・四国', '福岡県': '九州・沖縄', '佐賀県': '九州・沖縄', '長崎県': '九州・沖縄', '熊本県': '九州・沖縄', '大分県': '九州・沖縄', '宮崎県': '九州・沖縄', '鹿児島県': '九州・沖縄', '沖縄県': '九州・沖縄', } region_colors = { '北海道・東北': '#4472C4', '関東': '#ED7D31', '中部': '#A9D18E', '近畿': '#FFC000', '中国・四国': '#9DC3E6', '九州・沖縄': '#FF7F7F', } df_clean['地域'] = df_clean['都道府県'].map(region_map) df_clean_sorted = df_clean.sort_values(TARGET, ascending=True).reset_index(drop=True) national_avg = y.mean() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。2023年の都道府県別合計特殊出生率を低い順に示す。地域差の大きさと地域パターンを確認することが、モデル構築の出発点となる。
OLS(通常の最小二乗法)は観測数 N に対して説明変数 p が少ない場合に最良線形不偏推定量(BLUE)となる。しかし N=47、p=8 という小さいデータでは、変数を多く入れると過学習が起きやすい。
正則化回帰はバイアス(偏り)を少し増やす代わりに、分散(ばらつき)を大きく減らす。これが「バイアス−バリアンス トレードオフ」であり、汎化性能の改善につながる。
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | fig1, ax1 = plt.subplots(figsize=(10, 12)) bar_colors = [region_colors.get(r, '#999') for r in df_clean_sorted['地域']] bars = ax1.barh( range(len(df_clean_sorted)), df_clean_sorted[TARGET], color=bar_colors, edgecolor='white', linewidth=0.5, height=0.75 ) ax1.axvline(national_avg, color='darkred', linestyle='--', linewidth=1.5, label=f'全国平均: {national_avg:.2f}') ax1.set_yticks(range(len(df_clean_sorted))) ax1.set_yticklabels(df_clean_sorted['都道府県'], fontsize=9) ax1.set_xlabel('合計特殊出生率', fontsize=12) ax1.set_title(f'合計特殊出生率 都道府県ランキング({YEAR}年)', fontsize=14, fontweight='bold') ax1.set_xlim(0.8, 2.2) |
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の定石です。98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | # 凡例 from matplotlib.patches import Patch legend_patches = [Patch(color=c, label=r) for r, c in region_colors.items()] legend_patches.append(plt.Line2D([0], [0], color='darkred', linestyle='--', linewidth=1.5, label=f'全国平均: {national_avg:.2f}')) ax1.legend(handles=legend_patches, loc='lower right', fontsize=9, framealpha=0.9) ax1.grid(axis='x', alpha=0.3, linestyle=':') ax1.spines['top'].set_visible(False) ax1.spines['right'].set_visible(False) plt.tight_layout() fig1_path = os.path.join(FIG_DIR, '2019_H3_fig1.png') plt.savefig(fig1_path, bbox_inches='tight') plt.close() print(f"\n[保存] {fig1_path}") |
[保存] html/figures/2019_H3_fig1.png
import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。Ridge回帰・Lasso回帰はOLSの損失関数にペナルティ項を加えた手法である。ペナルティの強さを制御するパラメータ λ(正則化パラメータ)が大きいほど係数は0に近づく。
ペナルティ: 係数の二乗和 Σβⱼ²
効果: 全係数を均等に0方向へ縮小(shrinkage)
特徴: 係数は0に近づくが、完全には0にならない
適する場面: 多くの変数が少しずつ影響する場合
本分析の最適λ: 0.121
ペナルティ: 係数の絶対値和 Σ|βⱼ|
効果: 一部の係数をちょうど0にする(スパース解)
特徴: 自動的に変数選択を行う
適する場面: 少数の変数のみが重要な場合
本分析の最適λ: 0.0010
制約付き最適化の観点から見ると、Ridge はβ空間の 「球形」(円形)の制約集合を持ち、OLS の等高線と接する点は通常、軸上ではない(係数が正確に0にならない)。
一方 Lasso の制約集合は 「ひし形」(L1球)であり、角(頂点)で接することが多い。頂点では複数の係数が0になるため、自然にスパース(疎)な解が生まれる。
予測誤差の期待値は「バイアス²」+「バリアンス」+「避けられないノイズ」に分解できる。
λ → 0(正則化なし): バイアス小・バリアンス大(過学習)
λ → ∞(強い正則化): バイアス大・バリアンス小(未学習)
最適λ: バイアス²+バリアンスが最小になる均衡点
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | alphas = np.logspace(-3, 1, 100) coefs = [] for a in alphas: lasso = Lasso(alpha=a, max_iter=10000) lasso.fit(X_scaled, y) coefs.append(lasso.coef_) coefs = np.array(coefs) fig2, ax2 = plt.subplots(figsize=(10, 6)) colors_path = plt.cm.tab10(np.linspace(0, 1, len(FEAT_COLS))) for i, (col, color) in enumerate(zip(FEAT_COLS, colors_path)): ax2.semilogx(alphas, coefs[:, i], color=color, label=FEAT_LABELS.get(col, col), linewidth=2) ax2.axvline(best_alpha_lasso, color='red', linestyle='--', linewidth=2, label=f'最適λ = {best_alpha_lasso:.4f}') ax2.axhline(0, color='black', linewidth=0.5, linestyle='-') ax2.set_xlabel('正則化パラメータ λ(対数スケール)', fontsize=12) ax2.set_ylabel('標準化係数', fontsize=12) ax2.set_title('Lasso 係数パス図:λ増大に伴う係数の収縮', fontsize=14, fontweight='bold') ax2.legend(loc='upper right', fontsize=9, ncol=2, framealpha=0.9) ax2.grid(True, alpha=0.3, linestyle=':') ax2.spines['top'].set_visible(False) ax2.spines['right'].set_visible(False) plt.tight_layout() fig2_path = os.path.join(FIG_DIR, '2019_H3_fig2.png') plt.savefig(fig2_path, bbox_inches='tight') plt.close() print(f"[保存] {fig2_path}") |
[保存] html/figures/2019_H3_fig2.png
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。正則化パラメータ λ を変化させたとき、各変数の係数がどのように変化するかを示す「係数パス図」(正則化パス)は、変数選択の過程を可視化する重要なツールである。
Lassoはλが大きくなるにつれ、係数を正確に0にする(スパース性)。これはL1ペナルティの微分不可能性(β=0での「折れ」)に起因する。変数が多くサンプルが少ない高次元問題で特に威力を発揮する。
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | short_labels = [FEAT_LABELS.get(c, c) for c in FEAT_COLS] n_vars = len(FEAT_COLS) x = np.arange(n_vars) width = 0.25 fig3, ax3 = plt.subplots(figsize=(13, 6)) bars_ols = ax3.bar(x - width, ols_coefs, width, label='OLS', color='#4472C4', alpha=0.85) bars_ridge = ax3.bar(x, ridge_coefs, width, label='Ridge', color='#ED7D31', alpha=0.85) bars_lasso = ax3.bar(x + width, lasso_coefs, width, label='Lasso', color='#70AD47', alpha=0.85) ax3.axhline(0, color='black', linewidth=0.8) ax3.set_xticks(x) ax3.set_xticklabels(short_labels, fontsize=9, rotation=0) ax3.set_ylabel('標準化係数', fontsize=12) ax3.set_title('OLS・Ridge・Lasso の係数比較(標準化後)', fontsize=14, fontweight='bold') ax3.legend(fontsize=11, framealpha=0.9) ax3.grid(axis='y', alpha=0.3, linestyle=':') ax3.spines['top'].set_visible(False) ax3.spines['right'].set_visible(False) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。168 169 170 171 172 173 174 175 176 177 | # 最適λをテキストで注釈 ax3.text(0.02, 0.98, f'Ridge 最適λ={best_alpha_ridge:.3f}\nLasso 最適λ={best_alpha_lasso:.4f}', transform=ax3.transAxes, fontsize=10, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8)) plt.tight_layout() fig3_path = os.path.join(FIG_DIR, '2019_H3_fig3.png') plt.savefig(fig3_path, bbox_inches='tight') plt.close() print(f"[保存] {fig3_path}") |
[保存] html/figures/2019_H3_fig3.png
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。3手法の標準化後係数を並べて比較することで、正則化がどの変数の係数をどの程度縮小させるかを確認できる。
| 変数 | OLS係数 | Ridge係数 (λ=0.121) |
Lasso係数 (λ=0.001) |
Lasso選択 |
|---|---|---|---|---|
| 婚姻率(‰) | 0.0618 | 0.0594 | 0.0476 | 選択 |
| 保育所定員率 | −0.0007 | −0.0002 | 0.0000 | 除外 |
| 保育待機児童率(%) | −0.0160 | −0.0161 | −0.0150 | 選択 |
| 高齢化率(%) | 0.0950 | 0.0919 | 0.0851 | 選択 |
| 年少人口率(%) | 0.1136 | 0.1127 | 0.1140 | 選択 |
| 総人口(万人) | −0.0475 | −0.0474 | −0.0409 | 選択 |
| 消費支出(円) | −0.0026 | −0.0034 | −0.0023 | 選択 |
| 保健医療費(円) | 0.0032 | 0.0032 | 0.0000 | 除外 |
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | alphas_ridge_grid = np.logspace(-3, 3, 60) alphas_lasso_grid = np.logspace(-3, 1, 60) ridge_cv_mses = [] for a in alphas_ridge_grid: r = Ridge(alpha=a) scores = cross_val_score(r, X_scaled, y, cv=5, scoring='neg_mean_squared_error') ridge_cv_mses.append(-scores.mean()) lasso_cv_mses = [] for a in alphas_lasso_grid: l = Lasso(alpha=a, max_iter=10000) scores = cross_val_score(l, X_scaled, y, cv=5, scoring='neg_mean_squared_error') lasso_cv_mses.append(-scores.mean()) fig4, ax4 = plt.subplots(figsize=(10, 6)) ax4.loglog(alphas_ridge_grid, ridge_cv_mses, color='#ED7D31', linewidth=2.5, label='Ridge CV-MSE') ax4.loglog(alphas_lasso_grid, lasso_cv_mses, color='#70AD47', linewidth=2.5, label='Lasso CV-MSE') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | # 最適λの縦線 ax4.axvline(best_alpha_ridge, color='#ED7D31', linestyle='--', linewidth=1.8, label=f'Ridge 最適λ = {best_alpha_ridge:.3f}') ax4.axvline(best_alpha_lasso, color='#70AD47', linestyle='--', linewidth=1.8, label=f'Lasso 最適λ = {best_alpha_lasso:.4f}') ax4.set_xlabel('正則化パラメータ λ(対数スケール)', fontsize=12) ax4.set_ylabel('交差検証 MSE(対数スケール)', fontsize=12) ax4.set_title('5分割交差検証による MSE vs λ(Ridge・Lasso)', fontsize=14, fontweight='bold') ax4.legend(fontsize=10, framealpha=0.9) ax4.grid(True, which='both', alpha=0.3, linestyle=':') ax4.spines['top'].set_visible(False) ax4.spines['right'].set_visible(False) plt.tight_layout() fig4_path = os.path.join(FIG_DIR, '2019_H3_fig4.png') plt.savefig(fig4_path, bbox_inches='tight') plt.close() print(f"[保存] {fig4_path}") |
[保存] html/figures/2019_H3_fig4.png
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。交差検証(Cross-Validation、CV)はデータを複数に分割してモデルの汎化性能を評価する手法であり、過学習を防ぎながら最適な正則化強度λを選択するために使う。
| 手法 | 最適λ | CV-MSE | 係数がゼロの変数数 |
|---|---|---|---|
| Ridge | 0.1207 | 0.004760 | 0 |
| Lasso | 0.0010 | 0.004803 | 2 |
なぜ訓練データだけでλを選んではいけないか?
λを小さくすればするほど訓練データへの当てはまりは改善する(過学習方向)。訓練データのMSEだけを見ると、常にλ→0(OLS)が最良に見えてしまう。テストデータ相当の「見ていないデータ」でのMSEを評価するCVが必要。
注意:N=47の小さいデータでは5分割CVでも各フォールドが約9件しかなく、CV-MSEの推定精度は低い。実務では「1標準誤差ルール(1SE rule)」を用いて、最適λより少し大きいλを採用することもある。
・全変数に何らかの効果がありそうなとき
・変数間の相関が高いとき(多重共線性への耐性)
・予測精度を最優先するとき
・係数の安定性が重要なとき
・重要な変数が少数と考えられるとき
・変数の解釈・説明が重要なとき
・変数が多くサンプルが少ないとき(p≥N)
・シンプルなモデルが望ましいとき
使用データ:SSDSE-B-2026.csv(都道府県統計、2023年断面、47都道府県)。合成データは一切使用していない。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。