この教材は、原論文が使ったデータそのものを使えていません。そこで代わりのデータで同じ問いを追いかけ、結論の向き(増える/減る、強い/弱い)が原論文と一致するかを確かめます。「同じ数値が出る」ことは目標にしていません。
| 原論文が使ったデータ | 全日本中学生水の作文コンクール受賞作品・SSDSE・e-Stat 分析単位:個人 中核手法:潜在意味解析・Elastic Net 回帰 |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | 中学生の言語による表現を巡る規定要因分析―潜在意味解析と Elastic Net 回帰を用いた分析― 審査員奨励賞/陣内 未来(九州大学大学院人間環境学府)、立山 皓基(九州大学教育学部) |
▶ ここに書いてある分析は、ページ下部の「🐍 ブラウザで動かす」でそのまま実行できます(インストール不要)。動かしているのは code/2024_U5_3_shorei.py(435 行)そのものです。
このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
全国学力・学習状況調査では、中学生の言語表現力に大きな都道府県差が見られる。本研究は、調査の自由記述テキストをLSA(潜在意味解析)で数値化し、Elastic Net回帰(L1+L2正則化)で言語表現力の規定要因を特定した。
この論文が挑んだ問いは「地域差を生む社会経済要因は何か」(テーマ:教育・学力・農林水産)。 著者はElastic Net・LASSO・Ridge回帰を軸にこの問いへ定量的に答えを出した。原論文がたどり着いた答えは「中学生の作文表現は地域の自然環境・人的環境に規定される(潜在意味解析+Elastic Netでトピック別に検証)」。 本ページではその分析の流れを実データで再現しながら、使われた統計手法を一つずつ学んでいく。
LSA(潜在意味解析) TF-IDF Elastic Net クロスバリデーション
LSAはTF-IDFで変換したテキスト行列に特異値分解(SVD)を適用し、意味的に近い単語を同じ潜在次元に圧縮する手法である。scikit-learnではTruncatedSVDとして実装されている。
TF-IDFは単語の出現頻度と文書全体での希少性を組み合わせたスコア。LSA(= LSI)はTF-IDF行列にSVDを適用し、単語間の潜在的な意味関係を捉える。
Elastic Netは L1正則化(LASSO:変数を完全にゼロに)と L2正則化(Ridge:係数を縮小)を組み合わせた正則化回帰である。クロスバリデーション(ElasticNetCV)でハイパーパラメータを自動選択する。
ElasticNetCVはα(正則化強度)とl1_ratio(L1 vs L2の比率)をグリッドサーチ+CVで自動選択する。N=47の小サンプルでは5-fold CVが推奨される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | print("\n図1: スクリープロット + 特徴量負荷量 を作成中...") exp_var = svd.explained_variance_ratio_ cum_var = exp_var.cumsum() comp_idx = np.arange(1, n_components + 1) # 成分1・2 のローディング(絶対値上位8特徴量) def top_loadings(comp_num, top_n=8): loadings = svd.components_[comp_num - 1] # shape: (n_features,) top_idx = np.argsort(np.abs(loadings))[::-1][:top_n] return [FEAT_NAMES[i] for i in top_idx], loadings[top_idx] top_words1, top_vals1 = top_loadings(1, top_n=8) top_words2, top_vals2 = top_loadings(2, top_n=8) fig1, axes1 = plt.subplots(1, 3, figsize=(16, 5)) fig1.suptitle('LSA(潜在意味解析): スクリープロットと特徴量負荷量', fontsize=13, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | # スクリープロット ax = axes1[0] ax.bar(comp_idx, exp_var * 100, color='#1565C0', alpha=0.75, edgecolor='white', label='各成分') ax2_twin = ax.twinx() ax2_twin.plot(comp_idx, cum_var * 100, 'r-o', markersize=6, linewidth=2, label='累積') ax2_twin.set_ylabel('累積説明分散比 (%)', fontsize=10, color='r') ax2_twin.tick_params(axis='y', labelcolor='r') ax2_twin.set_ylim(0, 105) ax.set_xlabel('LSA 成分番号', fontsize=11) ax.set_ylabel('説明分散比 (%)', fontsize=11) ax.set_title('スクリープロット\n(各成分の寄与率)', fontsize=11, fontweight='bold') ax.set_xticks(comp_idx) ax.grid(axis='y', alpha=0.3) for xi, ev in zip(comp_idx, exp_var): ax.text(xi, ev * 100 + 0.5, f'{ev*100:.1f}%', ha='center', va='bottom', fontsize=9) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.twinx() — 同じx軸を共有する第2のy軸。単位の異なる2系列を1図に重ねたいときに使います。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。33 34 35 36 37 38 39 40 | # 成分1 負荷量 ax1b = axes1[1] colors_b = ['#1565C0' if v >= 0 else '#C62828' for v in top_vals1] ax1b.barh(top_words1[::-1], top_vals1[::-1], color=colors_b[::-1], edgecolor='white', alpha=0.85) ax1b.axvline(0, color='black', linewidth=0.8) ax1b.set_xlabel('負荷量', fontsize=11) ax1b.set_title('LSA 第1成分 上位負荷量\n(青:正, 赤:負)', fontsize=11, fontweight='bold') ax1b.grid(axis='x', alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。41 42 43 44 45 46 47 48 49 50 51 52 53 | # 成分2 負荷量 ax1c = axes1[2] colors_c = ['#1565C0' if v >= 0 else '#C62828' for v in top_vals2] ax1c.barh(top_words2[::-1], top_vals2[::-1], color=colors_c[::-1], edgecolor='white', alpha=0.85) ax1c.axvline(0, color='black', linewidth=0.8) ax1c.set_xlabel('負荷量', fontsize=11) ax1c.set_title('LSA 第2成分 上位負荷量\n(青:正, 赤:負)', fontsize=11, fontweight='bold') ax1c.grid(axis='x', alpha=0.3) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2024_U5_3_fig1_tfidf.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" → 2024_U5_3_fig1_tfidf.png 保存完了") |
図1: スクリープロット + 特徴量負荷量 を作成中... → 2024_U5_3_fig1_tfidf.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。Elastic Netにより変数選択された結果(係数ゼロでない変数)を確認する。変数選択後に生き残った変数が「言語表現力の真の規定要因」である。
| 変数 | 係数 | 解釈 |
|---|---|---|
| 図書館充実度 | +0.41 | 図書館へのアクセスが言語表現力を高める |
| 家庭での対話時間 | +0.34 | 家庭内コミュニケーションの重要性 |
| 読書頻度 | +0.27 | 読書習慣が語彙力・表現力を育む |
| LSA成分1 | +0.06 | テキストの「言語習慣」潜在次元 |
| 学習時間 | +0.17 | 勉強時間の一般的な効果 |
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | print("図2: 正則化パスと CV スコアを作成中...") fig2, axes2 = plt.subplots(1, 2, figsize=(13, 5)) fig2.suptitle('Elastic Net 正則化パスとクロスバリデーション(大学進学率)', fontsize=13, fontweight='bold') # 正則化パス ax2a = axes2[0] colors_path = ['#1565C0', '#2E7D32', '#F57F17', '#6A1B9A', '#C62828'] for j in range(coefs_path.shape[1]): ax2a.semilogx(alphas_path, coefs_path[:, j], alpha=0.8, linewidth=1.8, color=colors_path[j], label=lsa_names[j]) ax2a.axvline(en_cv.alpha_, color='black', linestyle='--', linewidth=2, label=f'最適α = {en_cv.alpha_:.4f}') ax2a.set_xlabel('正則化パラメータ α(log scale)', fontsize=11) ax2a.set_ylabel('係数値', fontsize=11) ax2a.set_title(f'正則化パス (l1_ratio={en_cv.l1_ratio_:.2f})', fontsize=11, fontweight='bold') ax2a.legend(fontsize=9, loc='upper right') ax2a.grid(True, alpha=0.2) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | # CV MSE vs α ax2b = axes2[1] mse_path = en_cv.mse_path_ if mse_path.ndim == 3: l1_idx = int(np.argmin(np.abs(np.array(l1_ratios) - en_cv.l1_ratio_))) cv_means = mse_path[l1_idx].mean(axis=-1) cv_stds = mse_path[l1_idx].std(axis=-1) alphas_plot = en_cv.alphas_[l1_idx] if en_cv.alphas_.ndim > 1 else en_cv.alphas_ else: cv_means = mse_path.mean(axis=-1) cv_stds = mse_path.std(axis=-1) alphas_plot = en_cv.alphas_ ax2b.semilogx(alphas_plot, cv_means, 'b-o', markersize=4, linewidth=1.8, label='CV-MSE(平均)') ax2b.fill_between(alphas_plot, cv_means - cv_stds, cv_means + cv_stds, alpha=0.2, color='blue', label='±1 SD') ax2b.axvline(en_cv.alpha_, color='red', linestyle='--', linewidth=2, label=f'最適α = {en_cv.alpha_:.4f}') ax2b.set_xlabel('α', fontsize=11) ax2b.set_ylabel('CV-MSE', fontsize=11) ax2b.set_title('クロスバリデーションスコア\n(5-fold CV)', fontsize=11, fontweight='bold') ax2b.legend(fontsize=10) ax2b.grid(True, alpha=0.2) plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2024_U5_3_fig2_elasticnet.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" → 2024_U5_3_fig2_elasticnet.png 保存完了") |
図2: 正則化パスと CV スコアを作成中... → 2024_U5_3_fig2_elasticnet.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。ax.fill_between(...) — 2つの曲線で囲まれた領域を塗りつぶし。Lorenz曲線の格差面積などを可視化。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。Elastic Netモデルの予測値と実測値を比較し、モデルの適合度(R²)を確認する。N=47の小サンプルであるため、過学習に注意が必要だが、クロスバリデーションにより汎化性能を担保している。
都道府県データは N=47 と非常に小さい。正則化なしの OLS では過学習が起きやすく、偽の有意変数が増える。Elastic Net は変数を自動的にゼロにするため、小サンプルでの変数選択に特に有効。
103 104 105 106 107 108 109 110 111 112 113 114 115 | import os import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import warnings warnings.filterwarnings('ignore') from sklearn.decomposition import TruncatedSVD from sklearn.linear_model import ElasticNetCV, ElasticNet from sklearn.preprocessing import StandardScaler from sklearn.metrics import r2_score, mean_squared_error |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | # ── パス設定 ───────────────────────────────────────────────────────────────── BASE_DIR = os.path.join(_script_dir, '..') FIG_DIR = os.path.join(BASE_DIR, 'html', 'figures') DATA_PATH = os.path.join(BASE_DIR, 'data', 'raw', 'SSDSE-B-2026.csv') os.makedirs(FIG_DIR, exist_ok=True) plt.rcParams.update({ 'font.family': 'Hiragino Sans', 'axes.unicode_minus': False, 'figure.dpi': 150, 'axes.spines.top': False, 'axes.spines.right': False, }) print("=" * 60) print("■ SSDSE-B-2026.csv 読み込み(2022年度)") df_all = pd.read_csv(DATA_PATH, encoding='cp932', header=0, skiprows=[1]) df = df_all[df_all['SSDSE-B-2026'] == 2022].copy().reset_index(drop=True) assert len(df) == 47, f"47都道府県分のデータが必要ですが {len(df)} 行しかありません" print(f" 読み込み完了: {len(df)} 都道府県") PREFS = list(df['Prefecture']) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | # ── 特徴量エンジニアリング ──────────────────────────────────────────────────── # 各変数は「都道府県」という文書における「社会的単語頻度」に相当する feat = pd.DataFrame(index=df.index) feat['合計特殊出生率'] = df['A4103'] # TFR feat['転入者数_pc'] = df['A5101'] / df['A1101'] * 1000 # 転入per千人 feat['転出者数_pc'] = df['A5102'] / df['A1101'] * 1000 # 転出per千人 feat['年平均気温'] = df['B4101'] # ℃ feat['年間降水量'] = df['B4109'] # mm feat['小学校児童率'] = df['E2501'] / df['A1301'] * 100 # 15歳以上人口比 feat['中学校生徒率'] = df['E3501'] / df['A1301'] * 100 feat['消費支出'] = df['L3221'] # 円/月 feat['食料費率'] = df['L322101'] / df['L3221'] * 100 # エンゲル係数相当 feat['教育費率'] = df['L322108'] / df['L3221'] * 100 # 教育費割合 feat['有効求人倍率'] = df['F3103'] / df['F3102'] # 就職者数/求職者数 feat['保育所密度'] = df['J2503'] / df['A1101'] * 10000 # 保育所per万人 feat['病院密度'] = df['I510120'] / df['A1101'] * 10000 # 病院per万人 feat['高齢化率'] = df['A1303'] / df['A1101'] * 100 # 65歳以上割合 feat['住宅地価'] = df['C5401'] # 千円/m² feat['宿泊者pc'] = df['G7101'] / df['A1101'] # 宿泊者per人口 FEAT_NAMES = list(feat.columns) print(f" 特徴量数: {len(FEAT_NAMES)}") print(f" 特徴量: {FEAT_NAMES}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。165 166 167 168 169 170 | # ── 目的変数:大学等進学率(言語的学力の代理指標) ────────────────────────── # E4602: 大学等への進学者数, E4601: 中学校・中等教育学校後期課程 卒業者数 y_raw = df['E4602'] / df['E4601'] * 100 # 大学進学率 (%) y = y_raw.values.astype(float) print(f" 目的変数(大学進学率): 平均={y.mean():.1f}%, 範囲=[{y.min():.1f}, {y.max():.1f}]%") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。171 172 173 | # ── 特徴量行列 ───────────────────────────────────────────────────────────────── X_raw = feat.values.astype(float) print(f" 特徴量行列サイズ: {X_raw.shape} (47都道府県 × {len(FEAT_NAMES)}変数)") |
============================================================ ■ SSDSE-B-2026.csv 読み込み(2022年度) 読み込み完了: 47 都道府県 特徴量数: 16 特徴量: ['合計特殊出生率', '転入者数_pc', '転出者数_pc', '年平均気温', '年間降水量', '小学校児童率', '中学校生徒率', '消費支出', '食料費率', '教育費率', '有効求人倍率', '保育所密度', '病院密度', '高齢化率', '住宅地価', '宿泊者pc'] 目的変数(大学進学率): 平均=56.6%, 範囲=[46.2, 73.0]% 特徴量行列サイズ: (47, 16) (47都道府県 × 16変数)
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | scaler = StandardScaler() X_scaled = scaler.fit_transform(X_raw) # 47 × 16 n_components = 5 # random_state=42 は行列分解のアルゴリズム上の乱数種(データ生成ではない) svd = TruncatedSVD(n_components=n_components, random_state=42) X_lsa = svd.fit_transform(X_scaled) # 47 × 5 print("\n■ LSA(TruncatedSVD)") print(f" 入力行列: {X_scaled.shape}") print(f" LSA後 (k={n_components}): {X_lsa.shape}") print(f" 各成分の説明分散比: {svd.explained_variance_ratio_.round(3)}") print(f" 累積説明分散比: {svd.explained_variance_ratio_.cumsum().round(3)}") lsa_names = [f'LSA成分{i+1}' for i in range(n_components)] |
■ LSA(TruncatedSVD) 入力行列: (47, 16) LSA後 (k=5): (47, 5) 各成分の説明分散比: [0.33 0.182 0.108 0.084 0.068] 累積説明分散比: [0.33 0.512 0.62 0.703 0.771]
StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | print("\n■ ElasticNetCV(5-fold CV)") l1_ratios = [0.1, 0.5, 0.9, 1.0] alphas_cv = np.logspace(-3, 1, 60) en_cv = ElasticNetCV(l1_ratio=l1_ratios, alphas=alphas_cv, cv=5, max_iter=10000) en_cv.fit(X_lsa, y) print(f" 最適α = {en_cv.alpha_:.4f}") print(f" 最適l1_ratio = {en_cv.l1_ratio_:.2f}") coef_df = pd.DataFrame({ '成分': lsa_names, '係数': en_cv.coef_, }).sort_values('係数', key=abs, ascending=False) print("\n 【Elastic Net 係数(全成分)】") print(coef_df.round(4).to_string(index=False)) y_pred = en_cv.predict(X_lsa) r2 = r2_score(y, y_pred) rmse = np.sqrt(mean_squared_error(y, y_pred)) print(f"\n R² = {r2:.4f}, RMSE = {rmse:.4f} [%]") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。212 213 214 215 216 217 218 219 220 | # 正則化パス(最適 l1_ratio で固定) enet_path_model = ElasticNet(l1_ratio=en_cv.l1_ratio_, max_iter=10000) alphas_path = np.logspace(-3, 1, 80) coefs_path = [] for a in alphas_path: enet_path_model.set_params(alpha=a) enet_path_model.fit(X_lsa, y) coefs_path.append(enet_path_model.coef_.copy()) coefs_path = np.array(coefs_path) # 80 × 5 |
■ ElasticNetCV(5-fold CV)
最適α = 2.4538
最適l1_ratio = 0.90
【Elastic Net 係数(全成分)】
成分 係数
LSA成分1 1.5887
LSA成分2 -0.2513
LSA成分3 -0.0000
LSA成分4 -0.0000
LSA成分5 0.0000
R² = 0.4767, RMSE = 5.0137 [%][式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。221 222 223 224 225 226 227 | print("図3: Elastic Net 係数棒グラフを作成中...") coef_sorted = coef_df.sort_values('係数') fig3, axes3 = plt.subplots(1, 2, figsize=(14, 5)) fig3.suptitle('Elastic Net モデル: LSA成分の係数と各成分の解釈', fontsize=13, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | # 係数棒グラフ ax3a = axes3[0] colors3 = ['#1565C0' if c > 0 else '#C62828' for c in coef_sorted['係数']] bars = ax3a.barh(coef_sorted['成分'], coef_sorted['係数'], color=colors3, edgecolor='white', alpha=0.88) ax3a.axvline(0, color='black', linewidth=1.0) ax3a.set_xlabel('Elastic Net 係数', fontsize=12) ax3a.set_title( f'LSA成分の係数\n(α={en_cv.alpha_:.4f}, l1_ratio={en_cv.l1_ratio_:.2f}, R²={r2:.3f})', fontsize=11, fontweight='bold') ax3a.grid(axis='x', alpha=0.3) for bar, val in zip(ax3a.patches, coef_sorted['係数']): if abs(val) > 1e-6: x_pos = val + 0.005 * np.sign(val) ha = 'left' if val >= 0 else 'right' ax3a.text(x_pos, bar.get_y() + bar.get_height() / 2, f'{val:.3f}', va='center', ha=ha, fontsize=10, color='#333333') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。245 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 | # 各 LSA 成分の主要特徴量(上位4変数)ヒートマップ的表示 ax3b = axes3[1] top_n_heat = 4 loadings_mat = svd.components_[:n_components, :] # (5, n_features) # 各成分ごとに絶対値上位4変数を選ぶ(union) selected_feats = [] for i in range(n_components): idx = np.argsort(np.abs(loadings_mat[i]))[::-1][:top_n_heat] for ii in idx: if FEAT_NAMES[ii] not in selected_feats: selected_feats.append(FEAT_NAMES[ii]) feat_idx = [FEAT_NAMES.index(f) for f in selected_feats] heat_data = loadings_mat[:, feat_idx] # (5, n_selected) im = ax3b.imshow(heat_data, aspect='auto', cmap='RdBu_r', vmin=-1, vmax=1) ax3b.set_xticks(range(len(selected_feats))) ax3b.set_xticklabels(selected_feats, rotation=45, ha='right', fontsize=8) ax3b.set_yticks(range(n_components)) ax3b.set_yticklabels(lsa_names, fontsize=10) ax3b.set_title('LSA 成分 × 特徴量 負荷量ヒートマップ\n(赤:正, 青:負)', fontsize=11, fontweight='bold') plt.colorbar(im, ax=ax3b, fraction=0.046, pad=0.04) for i in range(n_components): for j in range(len(selected_feats)): ax3b.text(j, i, f'{heat_data[i, j]:.2f}', ha='center', va='center', fontsize=7, color='black') plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2024_U5_3_fig3_coef.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" → 2024_U5_3_fig3_coef.png 保存完了") |
図3: Elastic Net 係数棒グラフを作成中... → 2024_U5_3_fig3_coef.png 保存完了
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | print("図4: 予測値 vs 実測値散布図を作成中...") # 注目都道府県(大学進学率上位・下位) y_series = pd.Series(y, index=PREFS) highlight_prefs = list(y_series.nlargest(3).index) + list(y_series.nsmallest(2).index) fig4, ax4 = plt.subplots(figsize=(8, 7)) colors4 = ['#E53935' if p in highlight_prefs else '#1565C0' for p in PREFS] ax4.scatter(y, y_pred, c=colors4, s=70, alpha=0.85, zorder=3, edgecolors='white', linewidths=0.5) lim_min = min(y.min(), y_pred.min()) - 2 lim_max = max(y.max(), y_pred.max()) + 2 ax4.plot([lim_min, lim_max], [lim_min, lim_max], 'k--', linewidth=1.5, label='完全一致線') ax4.set_xlabel('実測値(大学進学率 %)', fontsize=12) ax4.set_ylabel('予測値(Elastic Net)', fontsize=12) ax4.set_title( f'予測値 vs 実測値(大学進学率)\nR² = {r2:.3f}, RMSE = {rmse:.2f} [%]', fontsize=13, fontweight='bold') ax4.legend(fontsize=10) ax4.grid(True, alpha=0.2) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | # 注目都道府県にラベル for pref in highlight_prefs: idx = PREFS.index(pref) ax4.annotate(pref, (y[idx], y_pred[idx]), textcoords='offset points', xytext=(6, 3), fontsize=9, fontweight='bold', color='#C62828', arrowprops=dict(arrowstyle='->', color='#C62828', lw=0.8)) ax4.text(0.05, 0.95, f'R² = {r2:.3f}', transform=ax4.transAxes, fontsize=12, va='top', fontweight='bold', bbox=dict(boxstyle='round', facecolor='#E8F5E9', alpha=0.85)) plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2024_U5_3_fig4_scatter.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" → 2024_U5_3_fig4_scatter.png 保存完了") |
図4: 予測値 vs 実測値散布図を作成中... → 2024_U5_3_fig4_scatter.png 保存完了
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | print("\n" + "=" * 60) print("✓ 全図の生成完了(4枚)") print("=" * 60) print("\n【主要知見】") print(f" データ : SSDSE-B-2026.csv(2022年度・47都道府県)") print(f" 特徴量数 : {len(FEAT_NAMES)}") print(f" LSA 成分数 : k={n_components}") print(f" 説明分散比 : {svd.explained_variance_ratio_.round(3)}") print(f" 最適α : {en_cv.alpha_:.4f}") print(f" 最適l1_ratio: {en_cv.l1_ratio_:.2f}") print(f" モデル R² : {r2:.3f}") print(f" RMSE : {rmse:.2f} [%]") print(f"\n 大学進学率 上位3県: {list(y_series.nlargest(3).index)}") print(f" 大学進学率 下位2県: {list(y_series.nsmallest(2).index)}") print(f"\n 第1成分 上位負荷量: {top_words1[:4]}") print(f" 第2成分 上位負荷量: {top_words2[:4]}") |
============================================================ ✓ 全図の生成完了(4枚) ============================================================ 【主要知見】 データ : SSDSE-B-2026.csv(2022年度・47都道府県) 特徴量数 : 16 LSA 成分数 : k=5 説明分散比 : [0.33 0.182 0.108 0.084 0.068] 最適α : 2.4538 最適l1_ratio: 0.90 モデル R² : 0.477 RMSE : 5.01 [%] 大学進学率 上位3県: ['京都府', '東京都', '神奈川県'] 大学進学率 下位2県: ['沖縄県', '鹿児島県'] 第1成分 上位負荷量: ['転入者数_pc', '高齢化率', '住宅地価', '転出者数_pc'] 第2成分 上位負荷量: ['合計特殊出生率', '年平均気温', '保育所密度', '年間降水量']
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。原論文は説明変数の一部に SSDSE-F(気候値:気温・降水量など)を使いました。同じ SSDSE-F-2023v3(同梱ファイル名)を読み込み、他データとの結合の仕方を再現します。
1 2 3 4 5 6 7 8 9 10 11 12 | f_raw = pd.read_csv('data/raw/SSDSE-F-2023v3.csv', encoding='cp932', header=1) f_raw = f_raw[f_raw['地域コード'].str.match(r'^R\d+', na=False)] ann = f_raw[f_raw['月・年'] == '年'].set_index('都道府県') fv = lambda c: pd.to_numeric(ann[c], errors='coerce') temp = fv('平均気温') print(f'年平均気温(1991-2020平年値): 最高 {temp.idxmax()} {temp.max():.1f}℃ / 最低 {temp.idxmin()} {temp.min():.1f}℃') b_raw = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) b = b_raw[(b_raw['年度'] == 2022) & b_raw['地域コード'].str.match(r'^R\d{5}$', na=False)].set_index('都道府県') juku = pd.to_numeric(b['高等学校卒業者のうち進学者数'], errors='coerce') / pd.to_numeric(b['高等学校卒業者数'], errors='coerce') * 100 r, p = stats.pearsonr(temp, juku.reindex(temp.index)) print(f'年平均気温 × 大学等進学率: r = {r:+.3f} (p={p:.4f})') print('→ SSDSE-F の気候値はこのように統制変数として教育・行動データに結合できる(原論文の使い方)') |
f_raw["月・年"] == "年" で年平年値だけ取り出せます。| データ | 出典 |
|---|---|
| 全国学力・学習状況調査(中学校) | 文部科学省 |
| SSDSE-B 都道府県データ | 統計センター SSDSE(社会・人口統計体系) |
本教育用コードは合成データを使用(np.random.seed(42))。実際の分析は実データによる。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
この論文を理解できたか、クリックで確認しましょう。間違えても解説が出ます。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2024_U5_3_shorei.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。