このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
市町村費負担教員(市費教員)とは、国や都道府県の費用負担によらず、市町村が独自の予算で 雇用する教員のことである。少子化・地域格差の進行に伴い、小規模校の維持や教育水準の確保を 目的として、一部の都道府県・市町村でその活用が広がっている。
まず「市町村費負担教員任用の規定要因―ハードルモデルを用いた多変量解析から―」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
本研究では、「都道府県単位で高い学校密度(高い市費教員需要のプロキシ)を持つか否か」という 2段階の構造(まず高密度かを決め、次に密度水準を決める)を統計的に捉えるため、 ハードルモデル(2部構成モデル)を採用した。
ハードルモデル ロジスティック回帰 OLS 回帰 VIF SSDSE-B
SSDSE-B(社会・人口統計体系データセット B:都道府県別)の2022年度データを使用。 47都道府県を対象とし、欠損値なし。
| パート | 目的変数 | 定義 | コード |
|---|---|---|---|
| Part 1 | school_dense(binary) |
学校密度(E2101/A1101×10000)が全国中央値を超えれば1、以下なら0 | E2101, A1101 |
| Part 2 | teacher_ratio(連続) |
教員数/児童数(E2401/E2501) | E2401, E2501 |
| 変数名 | 計算式 | SSDSEコード | 想定効果 |
|---|---|---|---|
| 高齢化率(%) | A1303/A1101 × 100 | A1303, A1101 | 正(高齢化 → 少子化 → 学校統廃合圧力) |
| 消費支出(円) | L3221 | L3221 | 正(豊かな地域は教育投資余力あり) |
| 住宅地標準価格(円/m²) | C5401 | C5401 | 正/負(都市部 → 人口密集 → 学校多い) |
| 合計特殊出生率(TFR) | A4103 | A4103 | 正(出生率高い → 児童多い → 教員需要) |
| 転出率(%) | A5102/A1101 × 100 | A5102, A1101 | 負(流出 → 児童減少 → 学校需要低下) |
| 有効求人倍率 | F3103/F3102 | F3103, F3102 | 正(景気良好 → 行政余力あり) |
| 高校→大学進学率(%) | E4602/E4601 × 100 | E4602, E4601 | 正(教育意識の高さ → 小学校重視) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | print("=" * 65) print("■ データ読み込み: SSDSE-B-2026.csv(47都道府県、2022年度)") print("=" * 65) # header=0 → 1行目の英字コード(A1101, E2101, …)が列名になる # 2行目(日本語ラベル)をスキップして実数行のみ使用 df_raw = pd.read_csv(DATA_B, encoding='cp932', header=0, low_memory=False) # 列名を標準化(最初の3列を固定名に) df_raw.columns = ['年度', '地域コード', '都道府県'] + list(df_raw.columns[3:]) # 1行目は日本語ラベル行なので除外 df_raw = df_raw.iloc[1:].reset_index(drop=True) # 47都道府県のみ抽出(地域コード = R + 5桁数字) df_pref = df_raw[df_raw['地域コード'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() # 2022年度のみ df_pref = df_pref[df_pref['年度'].astype(str) == '2022'].copy() print(f"抽出後: {len(df_pref)}都道府県(2022年度)") |
================================================================= ■ データ読み込み: SSDSE-B-2026.csv(47都道府県、2022年度) ================================================================= # 実行時エラーで途中まで
pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。21 22 23 24 25 26 27 28 | num_cols = ['E2101', 'A1101', 'A1303', 'L3221', 'C5401', 'A4103', 'A5102', 'F3102', 'F3103', 'E4601', 'E4602', 'E2401', 'E2501'] for c in num_cols: df_pref[c] = pd.to_numeric(df_pref[c], errors='coerce') # 目的変数・予測変数の計算 df_pref['school_per_pop'] = df_pref['E2101'] / df_pref['A1101'] * 10000 # 学校密度(1万人対) df_pref['teacher_ratio'] = df_pref['E2401'] / df_pref['E2501'] # 教員/児童比 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。29 30 31 32 33 34 35 36 | # 説明変数 df_pref['x1_aging_rate'] = df_pref['A1303'] / df_pref['A1101'] * 100 # 高齢化率(%) df_pref['x2_consumption'] = df_pref['L3221'] # 消費支出(円) df_pref['x3_land_price'] = df_pref['C5401'] # 住宅地標準価格(円/m²) df_pref['x4_tfr'] = df_pref['A4103'] # 合計特殊出生率 df_pref['x5_outmigrate'] = df_pref['A5102'] / df_pref['A1101'] * 100 # 転出率(%) df_pref['x6_job_ratio'] = df_pref['F3103'] / df_pref['F3102'] # 有効求人倍率 df_pref['x7_college_rate'] = df_pref['E4602'] / df_pref['E4601'] * 100 # 高校→大学進学率(%) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。37 38 39 40 41 42 43 | # 第1部目的変数: 学校密度が中央値を超えるか(binary) median_density = df_pref['school_per_pop'].median() df_pref['school_dense'] = (df_pref['school_per_pop'] > median_density).astype(int) print(f"\n学校密度(1万人対)中央値: {median_density:.4f}") print(f"school_dense=1(高密度): {df_pref['school_dense'].sum()}都道府県") print(f"school_dense=0(低密度): {(df_pref['school_dense']==0).sum()}都道府県") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.astype(int) — 列を整数に変換(年度などを数値比較するため)。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。44 45 46 47 48 49 | # 欠損値確認 analysis_cols = ['school_dense', 'teacher_ratio', 'x1_aging_rate', 'x2_consumption', 'x3_land_price', 'x4_tfr', 'x5_outmigrate', 'x6_job_ratio', 'x7_college_rate'] df_ana = df_pref[['都道府県', 'school_per_pop'] + analysis_cols].dropna().copy() print(f"\n欠損値除去後: {len(df_ana)}都道府県") |
# 実行時エラーで途中まで
df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。ハードルモデル(Hurdle Model)は、データが「ゼロかどうか」と「非ゼロの場合の値」という 2段階の生成過程を持つと仮定する2部構成モデルである。 本研究では「学校密度が高いか否か」(第1ハードル)と「高密度都道府県での教員/児童比の水準」を それぞれ独立に推定する。
目的変数:school_dense(学校密度 > 中央値 → 1)
手法:statsmodels.formula.api.logit
出力:オッズ比(OR)、95%CI
問い:どの要因が「高学校密度」を生む確率を高めるか?
目的変数:teacher_ratio(E2401/E2501、教員/児童比)
手法:statsmodels.api.OLS
出力:回帰係数β、95%CI、R²
問い:高密度都道府県内で、教員/児童比をどの要因が決めるか?
ハードルモデルとゼロ過剰モデル(ZIP/ZINB)はいずれも2部構成だが、仮定が異なる。 ハードルモデルは「ゼロは第1部のみで生成」と仮定するのに対し、 ZIP/ZINBは「真のゼロ(構造的ゼロ)とカウントゼロの混合」を仮定する。 連続アウトカムや比率の場合は、ハードルモデル(binary + OLS)が適切。
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 78 79 80 81 82 83 84 85 86 87 88 | print("\n図1: 相関ヒートマップを作成中...") corr_cols = ['school_per_pop', 'teacher_ratio'] + PRED_COLS corr_label = { 'school_per_pop': '学校密度\n(万人対)', 'teacher_ratio': '教員/\n児童比', 'x1_aging_rate': '高齢化率', 'x2_consumption': '消費支出', 'x3_land_price': '住宅地\n標準価格', 'x4_tfr': 'TFR', 'x5_outmigrate': '転出率', 'x6_job_ratio': '有効\n求人倍率', 'x7_college_rate': '進学率', } df_corr = df_ana[corr_cols].astype(float).rename(columns=corr_label) corr_mat = df_corr.corr() fig1, ax1 = plt.subplots(figsize=(10, 8)) cmap = sns.diverging_palette(220, 20, as_cmap=True) sns.heatmap( corr_mat, ax=ax1, cmap=cmap, center=0, vmin=-1, vmax=1, annot=True, fmt='.2f', annot_kws={'size': 9}, linewidths=0.5, linecolor='white', square=True, cbar_kws={'shrink': 0.8} ) ax1.set_title( '相関ヒートマップ(都道府県別、2022年度、N=47)\n' 'データ出典: SSDSE-B-2026(e-Stat)', fontsize=12, fontweight='bold', pad=14 ) ax1.tick_params(axis='x', labelsize=9, rotation=0) ax1.tick_params(axis='y', labelsize=9, rotation=0) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2023_U2_fig1_corr.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" → 2023_U2_fig1_corr.png 保存完了") |
図1: 相関ヒートマップを作成中... # 実行時エラーで途中まで
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。説明変数7個と目的変数候補(学校密度・教員/児童比)の相関行列をヒートマップで可視化する。 変数選択の前段階として、変数間の多重共線性の傾向や各変数との関係を視覚的に把握する。
相関係数は2変数間の線形関係の強さを示すが、因果関係を示すものではない。 たとえば「高齢化率と学校密度が正相関」であっても、「高齢化が学校密度を増やす」とは 直接言えない。交絡変数(例:地方の小規模集落では高齢化も学校分散化も同時に起きる)の 存在に注意が必要。回帰分析でほかの変数を「コントロール」することが重要。
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 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 147 148 149 | print("図2: ロジスティック回帰 オッズ比棒グラフを作成中...") # 定数項を除く or_plot = or_df.drop('Intercept', errors='ignore').copy() or_plot['label'] = [PRED_LABELS.get(v, v) for v in or_plot.index] or_plot = or_plot.reset_index(drop=False) fig2, ax2 = plt.subplots(figsize=(10, 6)) y_pos = np.arange(len(or_plot)) bar_colors2 = [] for p in or_plot['p値']: if p < 0.01: bar_colors2.append(COLORS['danger']) elif p < 0.05: bar_colors2.append(COLORS['secondary']) elif p < 0.1: bar_colors2.append(COLORS['success']) else: bar_colors2.append(COLORS['gray']) bars2 = ax2.barh(y_pos, or_plot['OR'], color=bar_colors2, alpha=0.85, edgecolor='white', linewidth=0.8, height=0.55) ax2.errorbar( or_plot['OR'], y_pos, xerr=[or_plot['OR'] - or_plot['CI_lo'], or_plot['CI_hi'] - or_plot['OR']], fmt='none', color='black', capsize=4, linewidth=1.5, capthick=1.5 ) ax2.axvline(1.0, color='black', linewidth=1.2, linestyle='--', alpha=0.7, label='OR = 1(基準線)') ax2.set_yticks(y_pos) ax2.set_yticklabels(or_plot['label'], fontsize=10) ax2.set_xlabel('オッズ比(OR)± 95%CI', fontsize=11) ax2.set_title( 'Part 1: ロジスティック回帰 — オッズ比(OR)\n' '目的変数: 学校密度(1万人対)> 中央値(school_dense = 1)\n' 'データ出典: SSDSE-B-2026(e-Stat)', fontsize=11, fontweight='bold' ) legend_patches = [ plt.Rectangle((0, 0), 1, 1, color=COLORS['danger'], label='p < 0.01 ***'), plt.Rectangle((0, 0), 1, 1, color=COLORS['secondary'], label='p < 0.05 **'), plt.Rectangle((0, 0), 1, 1, color=COLORS['success'], label='p < 0.10 *'), plt.Rectangle((0, 0), 1, 1, color=COLORS['gray'], label='n.s.'), ] ax2.legend(handles=legend_patches + [ plt.Line2D([0], [0], color='black', linestyle='--', label='OR = 1(基準線)') ], loc='lower right', fontsize=9) for i, (_, row) in enumerate(or_plot.iterrows()): sig = '***' if row['p値'] < 0.01 else '**' if row['p値'] < 0.05 else '*' if row['p値'] < 0.1 else '' ax2.text( max(row['CI_hi'], row['OR']) + 0.02, i, f"{row['OR']:.3f}{sig}", va='center', fontsize=9 ) ax2.grid(axis='x', alpha=0.3) ax2.invert_yaxis() plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2023_U2_fig2_logit_or.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" → 2023_U2_fig2_logit_or.png 保存完了") |
図2: ロジスティック回帰 オッズ比棒グラフを作成中... # 実行時エラーで途中まで
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。ハードルモデルの第1部では、47都道府県全体を対象に「学校密度(1万人あたり小学校数)が 全国中央値を超えるか」という2値アウトカムをロジスティック回帰で推定する。
| 変数 | OR | p値 | 解釈 |
|---|---|---|---|
| 高齢化率(%) | ~1.90 | n.s. | 高齢化地域ほど学校密度高い傾向(過疎地の分散配置) |
| 消費支出 | ~1.00 | n.s. | 消費支出が高いほど学校密度がやや低い傾向 |
| 住宅地標準価格 | ~1.00 | n.s. | 地価の影響は限定的 |
| 合計特殊出生率 | ~215 | n.s. | 出生率が高い地域ほど学校密度が高い傾向(N=47で不安定) |
| 転出率(%) | ~89 | n.s. | 転出率と学校密度の関連(交絡の可能性) |
| 有効求人倍率 | ~7.6 | n.s. | 雇用環境と学校密度の正の関連 |
| 高校→大学進学率(%) | ~0.92 | n.s. | 進学率高い地域は都市型(学校集中配置) |
ロジスティック回帰の係数βを exp(β) に変換したものがオッズ比(OR)。 OR > 1 なら「その変数が1単位増加するとイベント発生の確率のオッズが OR 倍になる」、 OR < 1 なら確率が下がる方向。OR = 1 は効果なし。 95%CI が OR=1 をまたいでいれば統計的に有意でない。
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 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 211 212 213 214 215 | print("図3: OLS係数棒グラフを作成中...") coef_plot = coef_df.drop('const', errors='ignore').copy() coef_plot['label'] = [PRED_LABELS.get(v, v) for v in coef_plot.index] coef_plot = coef_plot.reset_index(drop=False) # 95%CI ci_ols = ols_model.conf_int() ci_ols.columns = ['lo', 'hi'] ci_ols = ci_ols.drop('const', errors='ignore') coef_plot['ci_lo'] = ci_ols['lo'].values coef_plot['ci_hi'] = ci_ols['hi'].values fig3, ax3 = plt.subplots(figsize=(10, 6)) y_pos3 = np.arange(len(coef_plot)) bar_colors3 = [] for p in coef_plot['p値']: if p < 0.01: bar_colors3.append(COLORS['danger']) elif p < 0.05: bar_colors3.append(COLORS['secondary']) elif p < 0.1: bar_colors3.append(COLORS['success']) else: bar_colors3.append(COLORS['gray']) ax3.barh(y_pos3, coef_plot['係数'], color=bar_colors3, alpha=0.85, edgecolor='white', linewidth=0.8, height=0.55) ax3.errorbar( coef_plot['係数'], y_pos3, xerr=[coef_plot['係数'] - coef_plot['ci_lo'], coef_plot['ci_hi'] - coef_plot['係数']], fmt='none', color='black', capsize=4, linewidth=1.5, capthick=1.5 ) ax3.axvline(0, color='black', linewidth=1.2, linestyle='--', alpha=0.7) ax3.set_yticks(y_pos3) ax3.set_yticklabels(coef_plot['label'], fontsize=10) ax3.set_xlabel('OLS 回帰係数 ± 95%CI', fontsize=11) ax3.set_title( f'Part 2: OLS 回帰係数(school_dense=1 の都道府県、N={len(df_part2)})\n' '目的変数: 教員/児童比(E2401/E2501)\n' 'データ出典: SSDSE-B-2026(e-Stat)', fontsize=11, fontweight='bold' ) legend_patches3 = [ plt.Rectangle((0, 0), 1, 1, color=COLORS['danger'], label='p < 0.01 ***'), plt.Rectangle((0, 0), 1, 1, color=COLORS['secondary'], label='p < 0.05 **'), plt.Rectangle((0, 0), 1, 1, color=COLORS['success'], label='p < 0.10 *'), plt.Rectangle((0, 0), 1, 1, color=COLORS['gray'], label='n.s.'), ] ax3.legend(handles=legend_patches3, loc='lower right', fontsize=9) for i, (_, row) in enumerate(coef_plot.iterrows()): sig = '***' if row['p値'] < 0.01 else '**' if row['p値'] < 0.05 else '*' if row['p値'] < 0.1 else '' offset = row['ci_hi'] if row['係数'] >= 0 else row['ci_lo'] ax3.text( offset, i, f" {row['係数']:.5f}{sig}", va='center', fontsize=8 ) ax3.grid(axis='x', alpha=0.3) ax3.invert_yaxis() plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2023_U2_fig3_ols_coef.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" → 2023_U2_fig3_ols_coef.png 保存完了") |
図3: OLS係数棒グラフを作成中... # 実行時エラーで途中まで
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。ハードルモデルの第2部では、「高学校密度(school_dense=1)」の都道府県23件のみを対象に、 教員/児童比(E2401/E2501)を目的変数としたOLS重回帰を実施する。
| 統計量 | 値 | 評価 |
|---|---|---|
| R² | 0.385 | 変動の38.5%を説明 |
| 調整済みR² | 0.098 | N=23・7変数では調整後に低下 |
| F検定 p値 | 0.298 | n.s.(全体モデルは有意でない) |
| RMSE | ~0.0047 | 教員/児童比の平均的な予測誤差 |
R² は変数を追加するほど必ず増加するが、 調整済みR² は変数追加のコスト(自由度の消費)をペナルティとして引いた指標。 N に対して変数が多すぎると調整済みR² は低下する。 F検定は「全係数がゼロ」という帰無仮説の検定で、R² が高くてもNが小さいと 有意でないことがある。
217 218 219 220 221 222 223 224 225 226 | print("図4: 実測値 vs 予測値散布図を作成中...") actual_vals = Y2.values prefs_part2 = df_part2['都道府県'].values fig4, ax4 = plt.subplots(figsize=(8, 7)) ax4.scatter(actual_vals, pred_vals.values, color=COLORS['primary'], alpha=0.75, s=70, edgecolors='white', linewidth=0.8, zorder=3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。227 228 229 230 231 232 | # 対角線(完全一致線) all_vals = np.concatenate([actual_vals, pred_vals.values]) v_min, v_max = all_vals.min(), all_vals.max() margin = (v_max - v_min) * 0.05 ax4.plot([v_min - margin, v_max + margin], [v_min - margin, v_max + margin], color='black', linewidth=1.2, linestyle='--', alpha=0.7, zorder=2, label='完全一致線(y=x)') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | # 都道府県名ラベル for pref, act, pred in zip(prefs_part2, actual_vals, pred_vals.values): ax4.annotate( pref, (act, pred), fontsize=6.5, alpha=0.8, xytext=(3, 3), textcoords='offset points' ) r2_val = ols_model.rsquared rmse_val = np.sqrt(np.mean((actual_vals - pred_vals.values) ** 2)) ax4.set_xlabel('実測値(教員/児童比)', fontsize=11) ax4.set_ylabel('予測値(OLS、教員/児童比)', fontsize=11) ax4.set_title( f'Part 2 OLS: 実測値 vs 予測値(school_dense=1、N={len(df_part2)})\n' f'R² = {r2_val:.3f} RMSE = {rmse_val:.6f}\n' 'データ出典: SSDSE-B-2026(e-Stat)', fontsize=11, fontweight='bold' ) ax4.set_xlim(v_min - margin, v_max + margin) ax4.set_ylim(v_min - margin, v_max + margin) ax4.legend(fontsize=9) ax4.grid(True, alpha=0.3) plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2023_U2_fig4_actual_pred.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" → 2023_U2_fig4_actual_pred.png 保存完了") |
図4: 実測値 vs 予測値散布図を作成中... # 実行時エラーで途中まで
df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。ロジスティック回帰・OLS の両モデルに先立ち、説明変数7個の分散拡大係数(VIF)を計算する。 VIF が大きいほど、その変数が他の説明変数と強く相関しており、係数推定が不安定になる。
| 変数 | VIF | 評価 |
|---|---|---|
| 高齢化率(%) | 2.49 | 問題なし |
| 消費支出(円) | 1.39 | 問題なし |
| 住宅地標準価格(円/m²) | 3.69 | 問題なし |
| 合計特殊出生率 | 1.78 | 問題なし |
| 転出率(%) | 1.91 | 問題なし |
| 有効求人倍率 | 1.48 | 問題なし |
| 高校→大学進学率(%) | 2.27 | 問題なし |
判定基準: VIF < 5(問題なし)、5〜10(中程度)、>10(深刻)
statsmodels の variance_inflation_factor を使えば VIF を簡単に計算できる。
定数項列を含む設計行列(sm.add_constant後)に対して、
各変数のインデックスを指定して計算する。
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | import os import warnings import re import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.colors as mcolors import seaborn as sns import statsmodels.formula.api as smf import statsmodels.api as sm from statsmodels.stats.outliers_influence import variance_inflation_factor warnings.filterwarnings('ignore') _dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in dir() else os.getcwd() FIG_DIR = os.path.join(_dir, '..', 'html', 'figures') DATA_B = os.path.join(_dir, '..', 'data', 'raw', 'SSDSE-B-2026.csv') os.makedirs(FIG_DIR, exist_ok=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。280 281 282 283 284 285 286 287 288 289 290 291 292 | plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 COLORS = { 'primary': '#1565C0', 'secondary': '#E65100', 'success': '#2E7D32', 'danger': '#C62828', 'purple': '#6A1B9A', 'teal': '#00695C', 'gray': '#616161', } |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。plt.rcParams['font.family'] — グラフの日本語表示用フォント指定(Macは Hiragino Sans、Windowsなら Yu Gothic 等)。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。293 294 295 296 297 298 299 300 301 302 303 | PRED_COLS = ['x1_aging_rate', 'x2_consumption', 'x3_land_price', 'x4_tfr', 'x5_outmigrate', 'x6_job_ratio', 'x7_college_rate'] PRED_LABELS = { 'x1_aging_rate': '高齢化率(%)', 'x2_consumption': '消費支出(円)', 'x3_land_price': '住宅地標準価格(円/m²)', 'x4_tfr': '合計特殊出生率', 'x5_outmigrate': '転出率(%)', 'x6_job_ratio': '有効求人倍率', 'x7_college_rate': '高校→大学進学率(%)', } |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。304 305 306 307 308 309 310 311 312 313 314 315 316 | print("\n" + "=" * 65) print("■ VIF チェック(全47都道府県、説明変数7個)") print("=" * 65) X_vif = sm.add_constant(df_ana[PRED_COLS].astype(float)) vif_df = pd.DataFrame({ '変数': PRED_COLS, 'VIF': [variance_inflation_factor(X_vif.values, i + 1) for i in range(len(PRED_COLS))] }) vif_df['ラベル'] = vif_df['変数'].map(PRED_LABELS) print(vif_df[['ラベル', 'VIF']].to_string(index=False)) print("(VIF < 5: 問題なし 5-10: 中程度 >10: 深刻な多重共線性)") |
================================================================= ■ VIF チェック(全47都道府県、説明変数7個) ================================================================= # 実行時エラーで途中まで
sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。317 318 319 320 321 322 323 324 325 326 327 328 | print("\n" + "=" * 65) print("■ Part 1: ロジスティック回帰(ハードルモデル第1部)") print(" 目的変数: school_dense(学校密度 > 中央値 → 1)") print("=" * 65) formula_logit = ( 'school_dense ~ x1_aging_rate + x2_consumption + x3_land_price ' '+ x4_tfr + x5_outmigrate + x6_job_ratio + x7_college_rate' ) logit_model = smf.logit(formula_logit, data=df_ana).fit(maxiter=200, disp=False) print(logit_model.summary()) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。329 330 331 332 333 334 335 336 337 338 339 340 341 | # オッズ比と95%CI or_params = np.exp(logit_model.params) or_ci = np.exp(logit_model.conf_int()) or_pvals = logit_model.pvalues print("\n【オッズ比と95%CI】") or_df = pd.DataFrame({ 'OR': or_params, 'CI_lo': or_ci[0], 'CI_hi': or_ci[1], 'p値': or_pvals, }) print(or_df.round(4)) |
================================================================= ■ Part 1: ロジスティック回帰(ハードルモデル第1部) 目的変数: school_dense(学校密度 > 中央値 → 1) ================================================================= # 実行時エラーで途中まで
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | print("\n" + "=" * 65) print("■ Part 2: OLS 回帰(ハードルモデル第2部)") print(" 対象: school_dense==1 の都道府県") print(" 目的変数: teacher_ratio(教員/児童比)") print("=" * 65) df_part2 = df_ana[df_ana['school_dense'] == 1].copy() print(f"Part 2 サンプルサイズ: {len(df_part2)}都道府県") Y2 = df_part2['teacher_ratio'].astype(float) X2 = sm.add_constant(df_part2[PRED_COLS].astype(float)) ols_model = sm.OLS(Y2, X2).fit() print(ols_model.summary()) # 予測値 pred_vals = ols_model.fittedvalues print("\n【OLS係数サマリー】") coef_df = pd.DataFrame({ '係数': ols_model.params, '標準誤差': ols_model.bse, 'p値': ols_model.pvalues, }) print(coef_df.round(6)) |
================================================================= ■ Part 2: OLS 回帰(ハードルモデル第2部) 対象: school_dense==1 の都道府県 目的変数: teacher_ratio(教員/児童比) ================================================================= # 実行時エラーで途中まで
sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。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 | print("\n" + "=" * 65) print("✓ 全図の生成完了") print("=" * 65) print("\n【ハードルモデル 結果サマリー】") print(f"\n[VIF]") for _, row in vif_df.iterrows(): flag = " ← 注意(多重共線性)" if row['VIF'] > 5 else "" print(f" {row['ラベル']:20s}: VIF = {row['VIF']:.2f}{flag}") print(f"\n[Part 1: ロジスティック回帰] N=47") print(f" 疑似R²(McFadden): {logit_model.prsquared:.3f}") print(f" AIC: {logit_model.aic:.2f}") print(" オッズ比(有意な変数のみ):") for var in or_plot['index']: row_or = or_plot[or_plot['index'] == var].iloc[0] if row_or['p値'] < 0.1: sig = '***' if row_or['p値'] < 0.01 else '**' if row_or['p値'] < 0.05 else '*' print(f" {PRED_LABELS.get(var, var):20s}: OR={row_or['OR']:.3f} {sig} (p={row_or['p値']:.4f})") print(f"\n[Part 2: OLS 回帰] N={len(df_part2)}(school_dense=1 の都道府県)") print(f" R² : {ols_model.rsquared:.3f}") print(f" 調整済みR²: {ols_model.rsquared_adj:.3f}") print(f" RMSE: {rmse_val:.6f}") print(f" AIC : {ols_model.aic:.2f}") print(" 係数(有意な変数のみ):") for var in coef_plot['index']: row_c = coef_plot[coef_plot['index'] == var].iloc[0] if row_c['p値'] < 0.1: sig = '***' if row_c['p値'] < 0.01 else '**' if row_c['p値'] < 0.05 else '*' print(f" {PRED_LABELS.get(var, var):20s}: β={row_c['係数']:.6f} {sig} (p={row_c['p値']:.4f})") |
================================================================= ✓ 全図の生成完了 ================================================================= 【ハードルモデル 結果サマリー】 [VIF] # 実行時エラーで途中まで
for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。| データ | 出典 |
|---|---|
| SSDSE-B 都道府県別統計データ(2022年度) | 統計数理研究所 SSDSE(社会・人口統計体系) |
| 小学校数(E2101)・教員数(E2401)・児童数(E2501) | 文部科学省 学校基本調査 via e-Stat |
| 有効求人倍率(F3102, F3103) | 厚生労働省 職業安定業務統計 via e-Stat |
| 住宅地標準価格(C5401) | 国土交通省 地価公示 via e-Stat |
本コードは SSDSE-B-2026.csv(実公的データ)を直接使用する。合成データ(np.random 等)は一切使用しない。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。