この教材は原論文と同じ種類のデータで計算し直しています。ただし工程の一部は、学習しやすさのために簡略化しています(下の「できないこと」を参照)。
| 原論文が使ったデータ | SSDSE-B・都道府県財政指数表・学校における教育の情報化の実態等に関する調査・全国学力・学習状況調査・人口推計・学校基本調査報告書・家計調査 分析単位:都道府県 中核手法:Random Forest・XGBoost・LightGBM・SHAP |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | デジタル教科書は学力にプラスかマイナスか?:機械学習×SHAP解析による検証 審査員奨励賞/立花 れい菜(上智大学大学院応用データサイエンス学位プログラム) |
▶ ここに書いてある分析は、ページ下部の「🐍 ブラウザで動かす」でそのまま実行できます(インストール不要)。動かしているのは code/2025_U5_3_shorei.py(606 行)そのものです。
このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
2019 年〜2022 年の GIGAスクール構想により、全国の小中学校に学習者用 PC が一人一台整備され、 デジタル教科書の導入が急速に進んでいる。しかし、デジタル教科書が学力に与える影響については 「プラスか?マイナスか?」をめぐって賛否が分かれており、実証的な分析が求められている。
この論文が挑んだ問いは「教育成果(大学進学率)の地域差を生む要因は何か」(テーマ:教育・学力・IT・デジタル)。 著者はSHAP・パネルデータ分析・ランダムフォレストを軸にこの問いへ定量的に答えを出した。 本ページではその分析の流れを実データで再現しながら、使われた統計手法を一つずつ学んでいく。
本再現コードでは、原論文が扱った学校単位のデジタル教科書・学力の詳細データが一般公開されていないため、 公開されている SSDSE-B-2026 / SSDSE-E-2026(47 都道府県・実公的統計、2022 年度)を用い、 教育成果の代理指標として 大学進学率(高卒者の進学者数 ÷ 卒業者数 × 100)を Random Forest・勾配ブースティング・Ridge回帰 の 3 モデルで予測し、 Permutation Importance(SHAPの代理指標)で各変数の効果を解釈している。
| カテゴリー | 変数名 | 説明(SSDSE から算出) |
|---|---|---|
| 人口・社会 | 高齢化率(%) | 65歳以上人口 ÷ 総人口 × 100 |
| 人口密度(人/km²) | 総人口 ÷ 総面積 | |
| 保育所数(千人対) | 保育所等数 ÷ 総人口 × 1000 | |
| 婚姻率(‰) | 婚姻件数 ÷ 15〜64歳人口 × 1000 | |
| 経済 | 1人当たり県民所得(万円) | 本分析で最も影響の大きい予測因子 |
| 教育費支出(円/月) | 二人以上世帯の教育費、家庭の教育投資 | |
| 医療・環境 | 医師数(10万人対) | 医師数 ÷ 総人口 × 100000、地域医療の充実度 |
| 年平均気温(℃) | 地域特性の制御変数 |
SSDSE-B-2026(2022 年度・47 都道府県)から算出した 大学進学率=高等学校卒業者のうち進学者数 ÷ 高等学校卒業者数 × 100(%)。 教育成果を示す公式統計量であり、平均 約56.6%(SD 約6.9、範囲 46.2〜73.0%)。
1 2 3 4 5 6 7 8 9 10 11 | df['大学進学率'] = (df['高等学校卒業者のうち進学者数'] / df['高等学校卒業者数']) * 100 # 特徴量 df['高齢化率'] = df['65歳以上人口'] / df['総人口'] * 100 df['保育所数千対'] = df['保育所等数'] / df['総人口'] * 1000 df['婚姻率'] = df['婚姻件数'] / df['15~64歳人口'] * 1000 df['医師数10万対'] = df['医師数'] / df['総人口'] * 100000 # 人口密度: 面積はha単位なので÷100でkm² df['人口密度'] = df['総人口'] / (df['総面積(北方地域及び竹島を除く)'] / 100) # 1人当たり県民所得 (万円) df['1人当たり県民所得'] = df['1人当たり県民所得(平成27年基準)'].astype(float) / 10 # 千円→万円 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | # 欠損値除去 feature_cols = [ '高齢化率', '教育費(二人以上の世帯)', '保育所数千対', '1人当たり県民所得', '婚姻率', '医師数10万対', '年平均気温', '人口密度', ] df_model = df[['都道府県', '大学進学率'] + feature_cols].dropna().reset_index(drop=True) FEATURE_LABELS = { '高齢化率': '高齢化率(%)', '教育費(二人以上の世帯)': '教育費支出(円/月)', '保育所数千対': '保育所数(千人対)', '1人当たり県民所得': '1人当たり県民所得(万円)', '婚姻率': '婚姻率(‰)', '医師数10万対': '医師数(10万人対)', '年平均気温': '年平均気温(℃)', '人口密度': '人口密度(人/km²)', } X = df_model[feature_cols].values y = df_model['大学進学率'].values print(f"\n 目的変数(大学進学率):") print(f" 平均: {y.mean():.1f}% SD: {y.std():.1f}% 範囲: {y.min():.1f}〜{y.max():.1f}%") print(f" 特徴量数: {len(feature_cols)}") print(f"\n 都道府県別大学進学率 上位5:") top5 = df_model.nlargest(5, '大学進学率')[['都道府県', '大学進学率']] for _, row in top5.iterrows(): print(f" {row['都道府県']}: {row['大学進学率']:.1f}%") print(f" 都道府県別大学進学率 下位5:") bot5 = df_model.nsmallest(5, '大学進学率')[['都道府県', '大学進学率']] for _, row in bot5.iterrows(): print(f" {row['都道府県']}: {row['大学進学率']:.1f}%") |
目的変数(大学進学率):
平均: 56.6% SD: 6.9% 範囲: 46.2〜73.0%
特徴量数: 8
都道府県別大学進学率 上位5:
京都府: 73.0%
東京都: 72.7%
神奈川県: 68.0%
大阪府: 67.5%
兵庫県: 67.0%
都道府県別大学進学率 下位5:
沖縄県: 46.2%
鹿児島県: 46.3%
山口県: 46.9%
秋田県: 47.5%
岩手県: 47.5%for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | print("\n" + "=" * 65) print("■ Step5. 交互作用分析(1人当たり県民所得 × 医師数10万対)") print("=" * 65) income_med = df_model['1人当たり県民所得'].median() doctor_med = df_model['医師数10万対'].median() df_model['所得区分'] = df_model['1人当たり県民所得'].apply( lambda x: '高所得(中央値以上)' if x >= income_med else '低所得(中央値未満)') df_model['医師区分'] = df_model['医師数10万対'].apply( lambda x: '医師多(中央値以上)' if x >= doctor_med else '医師少(中央値未満)') group_means = df_model.groupby(['所得区分', '医師区分'])['大学進学率'].mean().round(2) print(f"\n【所得区分 × 医師数区分 グループ別平均大学進学率】") print(group_means.unstack()) print(f"\n → 高所得×医師数多の組み合わせで進学率が最も高い傾向を確認") |
================================================================= ■ Step5. 交互作用分析(1人当たり県民所得 × 医師数10万対) ================================================================= 【所得区分 × 医師数区分 グループ別平均大学進学率】 医師区分 医師多(中央値以上) 医師少(中央値未満) 所得区分 低所得(中央値未満) 53.49 50.10 高所得(中央値以上) 62.59 59.72 → 高所得×医師数多の組み合わせで進学率が最も高い傾向を確認
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。まず非線形・交互作用効果を捉えられるモデルを採用することが有効だと考えられる。 その理由は社会経済指標は単純な線形効果でなく、要因の組み合わせで効きが変わるためOLSでは見落とすリスクがあるからである。 ここでは木ベースのアンサンブル学習を中心に、Random Forest・勾配ブースティング(XGBoost代替)・Ridge回帰(線形ベースライン)の3手法を比較する。 交差検証で安定して高い予測精度が得られる結果が期待される。
61 62 63 64 65 66 | print("\n図1: データ概要グラフを作成中...") fig1 = plt.figure(figsize=(14, 5)) gs1 = GridSpec(1, 3, figure=fig1, wspace=0.38) fig1.suptitle('大学進学率データの概要(実データ:SSDSE-B-2026, 2022年度)', fontsize=13, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。67 68 69 70 71 72 73 74 75 76 77 78 79 | # (a) 大学進学率の分布(ヒストグラム) ax1 = fig1.add_subplot(gs1[0]) ax1.hist(df_model['大学進学率'], bins=15, color='#1565C0', alpha=0.75, edgecolor='white', linewidth=0.7) ax1.axvline(df_model['大学進学率'].mean(), color='#E53935', linestyle='--', linewidth=2, label=f'平均 {df_model["大学進学率"].mean():.1f}%') ax1.axvline(df_model['大学進学率'].median(), color='#FB8C00', linestyle=':', linewidth=2, label=f'中央値 {df_model["大学進学率"].median():.1f}%') ax1.set_xlabel('大学進学率(%)', fontsize=10) ax1.set_ylabel('都道府県数', fontsize=10) ax1.set_title('大学進学率の分布\n(47都道府県、2022年度)', fontsize=11, fontweight='bold') ax1.legend(fontsize=9) ax1.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | # (b) 1人当たり県民所得 vs 大学進学率の散布図 ax2 = fig1.add_subplot(gs1[1]) sc = ax2.scatter(df_model['1人当たり県民所得'], df_model['大学進学率'], c=df_model['高齢化率'], cmap='RdYlGn_r', alpha=0.8, s=60, edgecolors='white', linewidth=0.5, zorder=3) cbar = plt.colorbar(sc, ax=ax2) cbar.set_label('高齢化率(%)', fontsize=8) coef2 = np.polyfit(df_model['1人当たり県民所得'], df_model['大学進学率'], 1) x_range = np.linspace(df_model['1人当たり県民所得'].min(), df_model['1人当たり県民所得'].max(), 100) ax2.plot(x_range, np.polyval(coef2, x_range), 'b-', linewidth=1.5, label='回帰直線') # ラベル(上位・下位都道府県) for _, row in df_model.iterrows(): if row['大学進学率'] > df_model['大学進学率'].quantile(0.9) or \ row['大学進学率'] < df_model['大学進学率'].quantile(0.1): ax2.annotate(row['都道府県'][:2], (row['1人当たり県民所得'], row['大学進学率']), fontsize=7, ha='left', va='bottom', color='#333') ax2.set_xlabel('1人当たり県民所得(万円)', fontsize=10) ax2.set_ylabel('大学進学率(%)', fontsize=10) ax2.set_title('県民所得 vs 大学進学率\n(色:高齢化率)', fontsize=11, fontweight='bold') ax2.legend(fontsize=8) ax2.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | # (c) 医師数10万対 vs 大学進学率の散布図 ax3 = fig1.add_subplot(gs1[2]) sc2 = ax3.scatter(df_model['医師数10万対'], df_model['大学進学率'], c=df_model['1人当たり県民所得'], cmap='RdYlGn', alpha=0.8, s=60, edgecolors='white', linewidth=0.5, zorder=3) cbar2 = plt.colorbar(sc2, ax=ax3) cbar2.set_label('1人当たり県民所得(万円)', fontsize=8) coef3 = np.polyfit(df_model['医師数10万対'], df_model['大学進学率'], 1) x3 = np.linspace(df_model['医師数10万対'].min(), df_model['医師数10万対'].max(), 100) ax3.plot(x3, np.polyval(coef3, x3), 'b-', linewidth=1.5, label='回帰直線') ax3.set_xlabel('医師数(10万人対)', fontsize=10) ax3.set_ylabel('大学進学率(%)', fontsize=10) ax3.set_title('医師数 vs 大学進学率\n(色:県民所得)', fontsize=11, fontweight='bold') ax3.legend(fontsize=8) ax3.grid(True, alpha=0.3) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2025_U5_3_fig1_overview.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" → 2025_U5_3_fig1_overview.png 保存完了") |
図1: データ概要グラフを作成中... → 2025_U5_3_fig1_overview.png 保存完了
np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。前節の3モデルとも高いCV精度を示した結果を踏まえると、 機械学習を政策提言に使うには、各変数の貢献を可視化する必要があると考えられる。 これを実現する手法として、ゲーム理論に基づく公平な貢献分解であるSHAP値に着目した。 1人当たり県民所得など社会経済指標が大学進学率に強く効くと示される結果が期待される。
SHAP(SHapley Additive exPlanations)は、 ゲーム理論の「Shapley値」を機械学習の解釈に応用した手法。 各特徴量が「個別の予測値」にどれだけ貢献しているかを計算する。
124 125 126 127 128 129 130 131 132 133 134 135 | print("図2: 特徴量重要度グラフを作成中...") fig2, axes2 = plt.subplots(1, 2, figsize=(14, 6)) fig2.suptitle('特徴量重要度の比較(3モデル)と Permutation Importance\n(実データ:SSDSE-B/E-2026)', fontsize=13, fontweight='bold') labels_ordered = [FEATURE_LABELS[feature_cols[i]] for i in order] imp_data = { 'Random Forest': rf_imp[order], 'Gradient Boosting': gbm_imp[order], 'Ridge(係数絶対値)': ridge_imp[order], } |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | # (a) 横並び棒グラフ ax_a = axes2[0] y_pos = np.arange(len(feature_cols)) width = 0.28 model_colors = ['#1565C0', '#E65100', '#2E7D32'] for i, (mname, imps) in enumerate(imp_data.items()): ax_a.barh(y_pos - width * (1 - i), imps, width, color=model_colors[i], alpha=0.8, label=mname, edgecolor='white') ax_a.set_yticks(y_pos) ax_a.set_yticklabels(labels_ordered, fontsize=9) ax_a.set_xlabel('特徴量重要度', fontsize=11) ax_a.set_title('3モデルの特徴量重要度\n(高いほど大学進学率との関連が強い)', fontsize=11, fontweight='bold') ax_a.legend(fontsize=9, loc='lower right') ax_a.grid(axis='x', alpha=0.3) ax_a.invert_yaxis() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。152 153 154 155 156 157 | # (b) Permutation Importance(SHAPの代理) ax_b = axes2[1] perm_order = np.argsort(perm_importances)[::-1] perm_labels = [FEATURE_LABELS[feature_cols[i]] for i in perm_order] perm_vals = perm_importances[perm_order] perm_std = perm_rf.importances_std[perm_order] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | # 経済・所得関連変数をハイライト econ_features = ['1人当たり県民所得', '医師数10万対', '教育費(二人以上の世帯)'] bar_colors_perm = ['#E53935' if feature_cols[i] in econ_features else '#1565C0' for i in perm_order] ax_b.barh(np.arange(len(perm_vals)), perm_vals, xerr=perm_std, capsize=3, color=bar_colors_perm, alpha=0.8, edgecolor='white', error_kw={'elinewidth': 1.2}) ax_b.set_yticks(np.arange(len(perm_vals))) ax_b.set_yticklabels(perm_labels, fontsize=9) ax_b.set_xlabel('Permutation Importance(±1SD)', fontsize=11) ax_b.set_title('Permutation Importance\n(SHAP値の代理指標, 赤=所得・医療関連変数)', fontsize=11, fontweight='bold') ax_b.axvline(0, color='gray', linewidth=0.8, linestyle='--') ax_b.grid(axis='x', alpha=0.3) ax_b.invert_yaxis() plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2025_U5_3_fig2_importance.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" → 2025_U5_3_fig2_importance.png 保存完了") |
図2: 特徴量重要度グラフを作成中... → 2025_U5_3_fig2_importance.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。前節のSHAP代理指標で1人当たり県民所得と医師数がともに正の効果を示した結果を踏まえると、 両者が単独でなく組み合わさることで効果が変わる交互作用が背景にあると考えられる。 これを検証する手法としてグループ別平均の交互作用プロットとRF予測面の部分依存プロットに着目した。 所得の高い自治体ほど医師数の多さが進学率をさらに押し上げるという結果が期待される。
交互作用効果とは、ある変数の効果が別の変数の値によって変わること。 実データでは「所得が高いほど大学進学率が高い」という関係に加え、 同じ所得水準でも医師数の多い都道府県ほど進学率が高い傾向が見られる (高所得×医師多で平均約62.6%、低所得×医師少で約50.1%)。
179 180 181 182 183 184 185 | print("図3: SHAPサマリープロット(代理)を作成中...") fig3, axes3 = plt.subplots(1, 2, figsize=(14, 6)) fig3.suptitle('SHAP値による特徴量効果の解釈(Random Forest)\n(実データ:SSDSE-B/E-2026)', fontsize=13, fontweight='bold') pred_vals = rf.predict(X) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | # パネル(a): 1人当たり県民所得 × RF予測値 Dependence Plot ax_a = axes3[0] income_idx = feature_cols.index('1人当たり県民所得') feat_income = df_model['1人当たり県民所得'].values color_arr = df_model['医師数10万対'].values sc = ax_a.scatter(feat_income, pred_vals, c=color_arr, cmap='RdYlGn', alpha=0.8, s=60, edgecolors='white', linewidth=0.5, zorder=3) cbar = plt.colorbar(sc, ax=ax_a, shrink=0.85) cbar.set_label('医師数(10万人対)', fontsize=9) # 都道府県ラベル for i, (xi, yi) in enumerate(zip(feat_income, pred_vals)): if yi > np.percentile(pred_vals, 85) or yi < np.percentile(pred_vals, 15): ax_a.annotate(df_model.loc[i, '都道府県'][:2], (xi, yi), fontsize=7, ha='left', va='bottom', color='#333') coef_inc = np.polyfit(feat_income, pred_vals, 2) x_inc = np.linspace(feat_income.min(), feat_income.max(), 200) ax_a.plot(x_inc, np.polyval(coef_inc, x_inc), 'b-', linewidth=2, label='二次フィット') ax_a.set_xlabel(FEATURE_LABELS['1人当たり県民所得'], fontsize=11) ax_a.set_ylabel('RF予測値(大学進学率 %)', fontsize=11) ax_a.set_title('Dependence Plot\n(県民所得 × RF予測値, 色:医師数)', fontsize=11, fontweight='bold') ax_a.legend(fontsize=9) ax_a.grid(True, alpha=0.2) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。210 211 212 213 214 215 216 217 218 | # パネル(b): 医師数10万対(高 vs 低)で予測値分布を比較 ax_b = axes3[1] doc_idx = feature_cols.index('医師数10万対') doc_med = np.median(X[:, doc_idx]) high_mask = X[:, doc_idx] >= doc_med low_mask = ~high_mask y_high = pred_vals[high_mask] y_low = pred_vals[low_mask] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | # バイオリンプロット風にKDE近似で表示(stripplot風) jitter_scale = 0.08 jp = np.full(high_mask.sum(), 1.0) jq = np.full(low_mask.sum(), 0.0) ax_b.scatter(jp + (np.arange(high_mask.sum()) % 5 - 2) * jitter_scale * 0.4, y_high, color='#E53935', alpha=0.6, s=40, label=f'医師数: 高(≥中央値 {doc_med:.0f}人)') ax_b.scatter(jq + (np.arange(low_mask.sum()) % 5 - 2) * jitter_scale * 0.4, y_low, color='#1565C0', alpha=0.6, s=40, label=f'医師数: 低(<中央値 {doc_med:.0f}人)') ax_b.axhline(y_high.mean(), color='#E53935', linestyle='--', linewidth=2, label=f'高群平均: {y_high.mean():.1f}%') ax_b.axhline(y_low.mean(), color='#1565C0', linestyle='--', linewidth=2, label=f'低群平均: {y_low.mean():.1f}%') ax_b.set_xticks([0, 1]) ax_b.set_xticklabels(['医師数: 低', '医師数: 高'], fontsize=11) ax_b.set_ylabel('RF予測値(大学進学率 %)', fontsize=11) ax_b.set_title('医師数(高 vs 低)の予測値分布\n→ 医師数が多い都道府県で進学率予測が高い', fontsize=11, fontweight='bold') ax_b.legend(fontsize=8.5, loc='upper left') ax_b.grid(axis='y', alpha=0.3) plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2025_U5_3_fig3_shap.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" → 2025_U5_3_fig3_shap.png 保存完了") |
図3: SHAPサマリープロット(代理)を作成中... → 2025_U5_3_fig3_shap.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。標本が47都道府県と少ないため、交差検証は Leave-One-Out(LOO-CV) で評価している(下表は再現コードの実測値)。
| モデル | LOO-CV R² | LOO-CV RMSE | 特徴 |
|---|---|---|---|
| Random Forest | 0.53 | 4.74pt | 安定した汎化性能・解釈しやすい |
| Gradient Boosting(XGB代替) | 0.55 | 4.66pt | やや高精度・ハイパーパラメータ調整が重要 |
| Ridge回帰(線形ベースライン) | 0.12 | 6.49pt | 線形モデル。非線形・交互作用を捉えられず精度は低め |
| 順位 | 変数名 | Perm. Imp. | 方向性 | 解釈 |
|---|---|---|---|---|
| 1 | 1人当たり県民所得 | 0.52 | 正 | 所得が高い都道府県ほど大学進学率が高い(最大の予測因子) |
| 2 | 高齢化率 | 0.12 | 負 | 高齢化が進んだ県ほど進学率が低い傾向 |
| 3 | 人口密度 | 0.11 | 正 | 人口が集積した都市部ほど進学率が高い |
| 4 | 保育所数(千人対) | 0.09 | 負 | 保育所が多い(≒地方的な)県ほど進学率が低い傾向 |
| 5 | 医師数(10万人対) | 0.04 | 正(弱) | 医療基盤が充実した県でやや進学率が高い |
246 247 248 249 250 251 252 253 254 255 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 | print("図4: 所得×医師数 交互作用図を作成中...") fig4, axes4 = plt.subplots(1, 2, figsize=(14, 6)) fig4.suptitle('交互作用分析:1人当たり県民所得 × 医師数(10万人対)\n(実データ:SSDSE-B/E-2026)', fontsize=13, fontweight='bold') # (a) 線形交互作用プロット(グループ別平均大学進学率) ax4a = axes4[0] income_labels3 = ['低所得\n(低1/3)', '中所得\n(中1/3)', '高所得\n(高1/3)'] income_q33 = df_model['1人当たり県民所得'].quantile(0.33) income_q67 = df_model['1人当たり県民所得'].quantile(0.67) def income_group(x): if x < income_q33: return '低所得\n(低1/3)' elif x < income_q67: return '中所得\n(中1/3)' else: return '高所得\n(高1/3)' df_plot = df_model.copy() df_plot['所得3区分'] = df_plot['1人当たり県民所得'].apply(income_group) df_plot['医師グループ'] = df_plot['医師数10万対'].apply( lambda x: '医師多(中央値以上)' if x >= doctor_med else '医師少(中央値未満)') for grp, col, ls in [('医師多(中央値以上)', '#E53935', '-'), ('医師少(中央値未満)', '#1565C0', '--')]: d = df_plot[df_plot['医師グループ'] == grp].groupby('所得3区分', observed=True)['大学進学率'].mean() vals = [d.get(lab, np.nan) for lab in income_labels3] ax4a.plot(income_labels3, vals, 'o-', color=col, linestyle=ls, linewidth=2.5, markersize=9, markerfacecolor='white', markeredgewidth=2.5, label=grp) ax4a.set_xlabel('1人当たり県民所得(区分)', fontsize=11) ax4a.set_ylabel('平均大学進学率(%)', fontsize=11) ax4a.set_title('交互作用プロット\n(医師多 vs 少)', fontsize=11, fontweight='bold') ax4a.legend(fontsize=10) ax4a.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。283 284 285 286 287 288 289 | # (b) 2D等高線プロット(RF予測面) ax4b = axes4[1] income_grid = np.linspace(df_model['1人当たり県民所得'].min(), df_model['1人当たり県民所得'].max(), 50) doctor_grid = np.linspace(df_model['医師数10万対'].min(), df_model['医師数10万対'].max(), 50) INC, DOC = np.meshgrid(income_grid, doctor_grid) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。290 291 292 293 294 295 296 297 298 299 300 301 | # 平均的な観測値を固定し、income と doctor だけ変化させる X_grid = np.tile(X.mean(axis=0), (50 * 50, 1)) inc_idx = feature_cols.index('1人当たり県民所得') doc_idx = feature_cols.index('医師数10万対') X_grid[:, inc_idx] = INC.ravel() X_grid[:, doc_idx] = DOC.ravel() pred_grid = rf.predict(X_grid).reshape(50, 50) cs = ax4b.contourf(INC, DOC, pred_grid, levels=20, cmap='RdYlGn') cbar4 = plt.colorbar(cs, ax=ax4b) cbar4.set_label('RF予測 大学進学率(%)', fontsize=9) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | # 実データ点をオーバーレイ ax4b.scatter(df_model['1人当たり県民所得'], df_model['医師数10万対'], c=df_model['大学進学率'], cmap='RdYlGn', s=40, alpha=0.7, edgecolors='black', linewidth=0.5, zorder=5) # 都道府県ラベル(上位・下位) for _, row in df_model.iterrows(): if row['大学進学率'] > df_model['大学進学率'].quantile(0.85) or \ row['大学進学率'] < df_model['大学進学率'].quantile(0.15): ax4b.annotate(row['都道府県'][:2], (row['1人当たり県民所得'], row['医師数10万対']), fontsize=7, color='black', ha='left', va='bottom') ax4b.set_xlabel('1人当たり県民所得(万円)', fontsize=11) ax4b.set_ylabel('医師数(10万人対)', fontsize=11) ax4b.set_title('RF予測面(等高線)\n県民所得 × 医師数の交互作用', fontsize=11, fontweight='bold') ax4b.text(0.05, 0.95, '→ 右上(高所得×医師数多)\nで大学進学率の予測が最大', transform=ax4b.transAxes, fontsize=9.5, va='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.85, edgecolor='#aaa')) plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2025_U5_3_fig4_interact.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" → 2025_U5_3_fig4_interact.png 保存完了") |
図4: 所得×医師数 交互作用図を作成中... → 2025_U5_3_fig4_interact.png 保存完了
for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。| 手法 | 目的 | Pythonでの実装 |
|---|---|---|
| Random Forest | バギングによるアンサンブル予測 | sklearn.ensemble.RandomForestRegressor |
| Gradient Boosting | ブースティングによる逐次改善 | sklearn.ensemble.GradientBoostingRegressor |
| Leave-One-Out 交差検証 | 少標本(47件)での汎化性能の公正な評価 | sklearn.model_selection.LeaveOneOut |
| Permutation Importance | SHAP値の代理・変数重要度 | sklearn.inspection.permutation_importance |
| Dependence Plot | 特徴量 × 予測値の非線形関係 | 散布図 + 二次回帰曲線で近似 |
| 交互作用プロット | 2変数の相乗・相殺効果の可視化 | plt.contourf()で予測面を等高線表示 |
| 実データの結合・前処理 | 複数の SSDSE(B・E)を都道府県名で結合 | pd.read_csv(...); df_b.merge(df_e, on='都道府県') |
| 観点 | 重回帰(統計) | 機械学習(RF等) |
|---|---|---|
| 目的 | 「なぜ」(因果推論・説明) | 「何が」(予測・変数重要度) |
| 仮定 | 線形性・正規性等の仮定が必要 | 仮定が少ない(ノンパラメトリック) |
| 交互作用 | 明示的に項を追加する必要 | 自動的に学習する |
| 解釈 | 係数・p値で直感的 | SHAP値・重要度で解釈 |
| 過学習 | 変数が少なければ起きにくい | 適切な正則化・CVが必要 |
分析スクリプトをダウンロードし、SSDSE-B-2026 / SSDSE-E-2026 の CSV を data/raw/ に配置して実行すると、実データから全図を出力します。
データの入手手順は上の「📥 データの準備」を参照してください。
numpy pandas matplotlib scikit-learnshap(SHAP値をライブラリで計算する場合)python3 code/2025_U5_3_shorei.py(実データ SSDSE-B/E-2026 を読み込み図を保存)
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
この論文を理解できたか、クリックで確認しましょう。間違えても解説が出ます。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2025_U5_3_shorei.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。