この教材は、原論文が使ったデータそのものを使えていません。そこで代わりのデータで同じ問いを追いかけ、結論の向き(増える/減る、強い/弱い)が原論文と一致するかを確かめます。「同じ数値が出る」ことは目標にしていません。
| 原論文が使ったデータ | SSDSE-A・地域別最低賃金の全国一覧 分析単位:市区町村 中核手法:重回帰分析・Elastic Net 回帰・ランダムフォレスト回帰・主成分分析・k-平均法 |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | 市区町村ごとの失業率の要因分析 審査員奨励賞/富張 聡祥(東京大学理学部情報科学科) |
原論文と同じ粒度のデータは、この教材にも同梱しています。これを読み込めば、原論文と同じ細かさで分析をやり直せます(下の「🐍 ブラウザで動かす」でコードを書き換えて試せます)。ただし原論文が使った項目がすべて収録されているとは限りません。足りない項目は、上の「できないこと」に書いた出典から取ってくる必要があります。
▶ ここに書いてある分析は、ページ下部の「🐍 ブラウザで動かす」でそのまま実行できます(インストール不要)。動かしているのは code/2023_U5_5_shorei.py(491 行)そのものです。
このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の労働市場は地域によって大きく異なる。大都市圏では求人が求職者を上回る「売り手市場」が続く一方、地方では依然として職が見つかりにくい状況が続いている。本論文は「なぜ失業率(求職圧力)に地域差が生まれるのか」を、都道府県別の社会・経済統計を用いた重回帰分析によって明らかにする。
この論文が挑んだ問いは「雇用・労働環境の地域差を生む要因は何か」(テーマ:労働・雇用)。 著者はElastic Net・Gini係数・SHAPを軸にこの問いへ定量的に答えを出した。原論文がたどり着いた答えは「労働力人口と第一次産業の活動が失業率に一貫して影響。市区町村の特性は「都市部かどうか」の軸に集約される(回帰+RF+SHAP+PCA+k平均)」。 本ページではその分析の流れを実データで再現しながら、使われた統計手法を一つずつ学んでいく。
市区町村レベルの失業率データはSSDSE-Bに含まれないため、本分析では「求職圧力指数」を構築する。これは月間有効求職者数を(求職者数+求人数)で割った値であり、値が高いほど「求職者が求人を上回る就職難状態」を示す失業圧力のプロキシ変数として機能する。
SSDSE-B 2022年 47都道府県 重回帰分析 VIF・標準化係数
使用データはSSDSE-B-2026(都道府県別)の2022年データ(47都道府県)。市区町村レベルのSSDSE-Aには失業率の直接指標が含まれないため、都道府県レベルで分析する。
| 変数名 | SSDSE-B列名 | 想定される効果 | 理由 |
|---|---|---|---|
| 高齢化率(%) | 65歳以上人口 / 総人口 × 100 | 負(求職圧力↓) | 高齢化が進む地域は生産年齢人口が少なく、競争が緩和される |
| 男性率(生産年齢,%) | 15〜64歳人口(男)/ 15〜64歳人口 × 100 | ? | 労働参加の性別構成が求職圧力に影響 |
| 消費支出(円/月) | 消費支出(二人以上の世帯) | 負 | 経済活力が高い地域では消費が旺盛で雇用が生まれやすい |
| 住宅地価格(円/m²) | 標準価格(平均価格)(住宅地) | 負 | 地価が高い地域は経済規模が大きく求人が多い |
| 合計特殊出生率(TFR) | 合計特殊出生率 | 負 | 活力ある地域は出生率も高く、労働需要も旺盛 |
| 大学進学率(%) | 高校卒業者うち進学者 / 高校卒業者 × 100 | 正/負 | 高学歴化が求職者の質向上or労働市場ミスマッチを生む可能性 |
| 年平均気温(℃) | 年平均気温 | 正 | 温暖な南方地域は産業構造が異なる |
| 宿泊者 per capita | 延べ宿泊者数 / 総人口 | 負 | 観光業が盛んな地域は雇用機会が豊富 |
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 scipy import stats as scipy_stats from scipy.stats import gaussian_kde import statsmodels.api as sm from statsmodels.stats.outliers_influence import variance_inflation_factor |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # ── パス設定 ───────────────────────────────────────────────────────────────── FIG_DIR = 'html/figures' DATA_DIR = 'data/raw' 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("=" * 65) print("■ データ読み込み(SSDSE-B-2026 実データのみ)") print("=" * 65) df_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'), encoding='cp932', header=1 ) |
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ループ不要なのが強み。35 36 37 38 39 40 41 42 | # 47都道府県のみ(地域コード R + 5桁数字、かつ全国合計 R00000 を除く) df_raw = df_raw[df_raw['地域コード'].str.match(r'^R\d{5}$', na=False)].copy() df_raw['年度'] = pd.to_numeric(df_raw['年度'], errors='coerce') # 2022年データを使用(最新の完全データ年) YEAR = 2022 df = df_raw[df_raw['年度'] == YEAR].copy().reset_index(drop=True) print(f"SSDSE-B {YEAR}年: {len(df)}都道府県") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。43 44 45 46 47 48 49 50 51 52 53 54 55 | # 数値変換 NUM_COLS = [ '月間有効求職者数(一般)', '月間有効求人数(一般)', '総人口', '15~64歳人口', '15~64歳人口(男)', '65歳以上人口', '消費支出(二人以上の世帯)', '標準価格(平均価格)(住宅地)', '合計特殊出生率', '高等学校卒業者数', '高等学校卒業者のうち進学者数', '年平均気温', '延べ宿泊者数', ] for c in NUM_COLS: df[c] = pd.to_numeric(df[c], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。56 57 58 59 60 61 62 | # ── 変数の構築 ──────────────────────────────────────────────────────────────── # 目的変数:求職圧力指数(Jobseeker Pressure Index) df['求職圧力指数'] = ( df['月間有効求職者数(一般)'] / (df['月間有効求職者数(一般)'] + df['月間有効求人数(一般)']) * 100 ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。63 64 65 66 67 68 69 70 71 | # 説明変数 df['高齢化率'] = df['65歳以上人口'] / df['総人口'] * 100 df['男性率'] = df['15~64歳人口(男)'] / df['15~64歳人口'] * 100 df['消費支出'] = df['消費支出(二人以上の世帯)'] # 円/月 df['住宅地価格'] = df['標準価格(平均価格)(住宅地)'] # 円/m² df['TFR'] = df['合計特殊出生率'] df['大学進学率'] = df['高等学校卒業者のうち進学者数'] / df['高等学校卒業者数'] * 100 df['年平均気温'] = df['年平均気温'] df['宿泊者per_capita'] = df['延べ宿泊者数'] / df['総人口'] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | # 分析変数名 Y_NAME = '求職圧力指数' X_NAMES = ['高齢化率', '男性率', '消費支出', '住宅地価格', 'TFR', '大学進学率', '年平均気温', '宿泊者per_capita'] # 表示用変数名(短縮) X_LABELS = { '高齢化率': '高齢化率\n(%)', '男性率': '男性率\n(生産年齢,%)', '消費支出': '消費支出\n(円/月)', '住宅地価格': '住宅地価格\n(円/m²)', 'TFR': '合計特殊\n出生率', '大学進学率': '大学進学率\n(%)', '年平均気温': '年平均気温\n(℃)', '宿泊者per_capita':'宿泊者\nper capita', } |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | # 欠損除外 VARS_ALL = [Y_NAME] + X_NAMES + ['都道府県'] df_ana = df[VARS_ALL].dropna().reset_index(drop=True) N = len(df_ana) prefs = df_ana['都道府県'].tolist() y = df_ana[Y_NAME].values X = df_ana[X_NAMES].values print(f"\n分析対象: {N}都道府県") print(f"\n{Y_NAME} 記述統計:") print(f" 平均 = {y.mean():.2f}%") print(f" 標準偏差 = {y.std():.2f}%") print(f" 最小 = {y.min():.2f}% ({prefs[y.argmin()]})") print(f" 最大 = {y.max():.2f}% ({prefs[y.argmax()]})") |
================================================================= ■ データ読み込み(SSDSE-B-2026 実データのみ) ================================================================= SSDSE-B 2022年: 47都道府県 分析対象: 47都道府県 求職圧力指数 記述統計: 平均 = 42.22% 標準偏差 = 4.44% 最小 = 34.01% (福井県) 最大 = 53.18% (神奈川県)
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。重回帰分析を行う前に、目的変数の分布を確認する。分布の形状・外れ値の有無・正規性を視覚的に把握することは、分析結果の信頼性を高める重要な前処理ステップである。
ヒストグラムはビン幅の選択によって形状が変わるという欠点がある。KDE(カーネル密度推定)は連続的な密度関数を推定するため、より滑らかで安定した分布の可視化が可能。scipy.stats.gaussian_kde はスコットの帯域幅を自動選択する。
103 104 105 106 107 108 109 110 111 112 | print("\n図1: 分布図(ヒストグラム + KDE)を作成中...") fig1, ax1 = plt.subplots(figsize=(9, 5)) # ヒストグラム n_bins = 10 counts, bin_edges, _ = ax1.hist( y, bins=n_bins, color='#1565C0', alpha=0.65, edgecolor='white', label='度数', density=True ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | # KDE(カーネル密度推定) kde = gaussian_kde(y, bw_method='scott') x_kde = np.linspace(y.min() - 1, y.max() + 1, 300) ax1.plot(x_kde, kde(x_kde), color='#E65100', linewidth=2.5, label='KDE(カーネル密度)') # 平均・中央値の縦線 ax1.axvline(y.mean(), color='#1565C0', linestyle='--', linewidth=1.8, label=f'平均 = {y.mean():.2f}%') ax1.axvline(np.median(y), color='#2E7D32', linestyle=':', linewidth=1.8, label=f'中央値 = {np.median(y):.2f}%') ax1.set_xlabel('求職圧力指数(%)', fontsize=12) ax1.set_ylabel('密度', fontsize=12) ax1.set_title( f'求職圧力指数の分布 — 47都道府県({YEAR}年)\n' f'(月間有効求職者数 / (求職者数 + 求人数) × 100)\nデータ:SSDSE-B-2026 実データ', fontsize=12, fontweight='bold' ) ax1.legend(fontsize=10) ax1.grid(axis='y', alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | # 上位・下位都道府県ラベル highlight = ['北海道', '沖縄県', '東京都', '愛知県', '秋田県'] for pref in highlight: if pref in prefs: idx = prefs.index(pref) kde_val = float(kde(np.array([y[idx]]))[0]) ax1.annotate( pref.replace('道','').replace('府','').replace('都','').replace('県',''), xy=(y[idx], 0.005), xytext=(y[idx], kde_val * 0.55), fontsize=8, color='#C62828', arrowprops=dict(arrowstyle='->', color='#C62828', lw=1.0), ha='center' ) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2023_U5_5_fig1_dist.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" -> 2023_U5_5_fig1_dist.png 保存完了") |
図1: 分布図(ヒストグラム + KDE)を作成中... -> 2023_U5_5_fig1_dist.png 保存完了
df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。目的変数と各説明変数の Pearson 相関係数を算出し、説明変数間の多重共線性の候補も確認する。相関係数ヒートマップは全変数ペアの相関を一覧できる強力な視覚化ツールである。
| 説明変数 | 相関係数 r | p値 | 有意性 | 解釈 |
|---|---|---|---|---|
| 高齢化率 | −0.412 | 0.004 | ** | 高齢化が進むほど求職圧力が低い(生産年齢人口が少なく競争が緩和) |
| 住宅地価格 | +0.304 | 0.038 | * | 地価が高い大都市圏ほど求職者数が多い |
| 合計特殊出生率(TFR) | −0.319 | 0.029 | * | 出生率が高い地域は経済・雇用環境がよい傾向 |
| 大学進学率 | +0.330 | 0.024 | * | 高学歴化が求職者の増加(またはミスマッチ)につながる可能性 |
| 男性率(生産年齢) | −0.219 | 0.140 | ns | 有意な関連なし |
| 消費支出 | +0.021 | 0.887 | ns | 単純相関では有意な関連なし |
| 年平均気温 | +0.229 | 0.121 | ns | 有意な関連なし(p=0.12) |
| 宿泊者 per capita | +0.078 | 0.601 | ns | 有意な関連なし |
相関行列のヒートマップは、多変数データの構造を素早く把握する標準的な手法。imshow と RdBu_r カラーマップを組み合わせ、正負の相関を直感的に示す。
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 | print("図2: 相関係数ヒートマップを作成中...") fig2, ax2 = plt.subplots(figsize=(10, 9)) corr_vals = corr_matrix.values n_all = len(corr_matrix.columns) col_labels_short = [X_LABELS.get(c, c) if c != Y_NAME else '求職圧力\n指数' for c in corr_matrix.columns] im = ax2.imshow(corr_vals, cmap='RdBu_r', vmin=-1, vmax=1, aspect='auto') cbar = plt.colorbar(im, ax=ax2, fraction=0.046, pad=0.04) cbar.set_label('Pearson相関係数', fontsize=10) ax2.set_xticks(range(n_all)) ax2.set_yticks(range(n_all)) ax2.set_xticklabels(col_labels_short, fontsize=8, rotation=35, ha='right') ax2.set_yticklabels(col_labels_short, fontsize=8) ax2.set_title( f'Pearson相関係数行列(n={N}都道府県)\n* p<0.05 ** p<0.01 *** p<0.001\nデータ:SSDSE-B-2026', fontsize=12, fontweight='bold' ) for i in range(n_all): for j in range(n_all): val = corr_vals[i, j] sig_str = '' if i != j: try: _, pv = scipy_stats.pearsonr( df_corr.iloc[:, i].dropna(), df_corr.iloc[:, j].dropna() ) sig_str = '***' if pv < 0.001 else '**' if pv < 0.01 else '*' if pv < 0.05 else '' except Exception: pass text_color = 'white' if abs(val) > 0.6 else 'black' ax2.text(j, i, f'{val:.2f}{sig_str}', ha='center', va='center', fontsize=7.5, fontweight='bold', color=text_color) plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2023_U5_5_fig2_corr.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" -> 2023_U5_5_fig2_corr.png 保存完了") |
図2: 相関係数ヒートマップを作成中... -> 2023_U5_5_fig2_corr.png 保存完了
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。stats.pearsonr(x, y) — Pearson相関係数 r と p値を同時に返します。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。複数の説明変数を同時に投入する重回帰分析では、変数間の多重共線性(multicollinearity)が問題になる。VIF(分散拡大係数, Variance Inflation Factor)は各説明変数の多重共線性の程度を数値化する指標である。
| 説明変数 | VIF | 判定 |
|---|---|---|
| 合計特殊出生率(TFR) | 3.18 | 問題なし |
| 年平均気温 | 3.07 | 問題なし |
| 高齢化率 | 3.03 | 問題なし |
| 住宅地価格 | 2.91 | 問題なし |
| 大学進学率 | 2.40 | 問題なし |
| 消費支出 | 1.70 | 問題なし |
| 男性率(生産年齢) | 1.60 | 問題なし |
| 宿泊者 per capita | 1.28 | 問題なし |
VIF は statsmodels の variance_inflation_factor で計算できる。定数項を含めた設計行列を入力とし、各列(変数)のVIF値を返す。
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | print("図3: 標準化係数棒グラフを作成中...") # 95% CI(±1.96×SE) ci95 = 1.96 * std_bse # 有意性フラグで色分け bar_colors = [] for pv in std_pvals: if pv < 0.01: bar_colors.append('#C62828') # 深紅:高有意 elif pv < 0.05: bar_colors.append('#E65100') # オレンジ:有意 else: bar_colors.append('#90CAF9') # 薄青:非有意 short_labels = [X_LABELS.get(xn, xn) for xn in X_NAMES] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | # 標準化係数の降順ソート sort_idx = np.argsort(std_coefs) sorted_labels = [short_labels[i] for i in sort_idx] sorted_coefs = std_coefs[sort_idx] sorted_ci = ci95[sort_idx] sorted_pvals = std_pvals[sort_idx] sorted_colors = [bar_colors[i] for i in sort_idx] fig3, ax3 = plt.subplots(figsize=(10, 6)) bars3 = ax3.barh( np.arange(len(X_NAMES)), sorted_coefs, color=sorted_colors, alpha=0.85, edgecolor='white', xerr=sorted_ci, error_kw=dict(ecolor='#333', capsize=4, linewidth=1.2) ) ax3.axvline(0, color='black', linewidth=1.0) ax3.set_yticks(np.arange(len(X_NAMES))) ax3.set_yticklabels(sorted_labels, fontsize=10) ax3.set_xlabel('標準化回帰係数(β)', fontsize=12) ax3.set_title( f'OLS 重回帰:標準化係数(β)と95%信頼区間\n' f'目的変数:求職圧力指数 R²={ols_result.rsquared:.3f} AdjR²={ols_result.rsquared_adj:.3f} n={N}', fontsize=12, fontweight='bold' ) ax3.grid(axis='x', alpha=0.3) ax3.invert_yaxis() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。237 238 239 240 241 242 243 244 245 | # 有意性ラベル for i, (bar, pv) in enumerate(zip(bars3, sorted_pvals)): w = bar.get_width() sig_str = '***' if pv < 0.001 else '**' if pv < 0.01 else '*' if pv < 0.05 else 'ns' offset = sorted_ci[i] + 0.02 ax3.text(w + offset if w >= 0 else w - offset - 0.05, bar.get_y() + bar.get_height() / 2, sig_str, va='center', fontsize=10, color='#C62828' if sig_str != 'ns' else '#999') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。246 247 248 249 250 251 252 253 254 255 256 257 258 | # 凡例 from matplotlib.patches import Patch legend_elements = [ Patch(facecolor='#C62828', alpha=0.85, label='p < 0.01(高度有意)'), Patch(facecolor='#E65100', alpha=0.85, label='p < 0.05(有意)'), Patch(facecolor='#90CAF9', alpha=0.85, label='p ≥ 0.05(非有意)'), ] ax3.legend(handles=legend_elements, fontsize=9, loc='lower right') plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2023_U5_5_fig3_coef.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" -> 2023_U5_5_fig3_coef.png 保存完了") |
図3: 標準化係数棒グラフを作成中... -> 2023_U5_5_fig3_coef.png 保存完了
import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。VIF で多重共線性の問題がないことを確認した上で、全8変数を投入したOLS重回帰を実行する。標準化回帰係数(β係数)を用いることで、単位の異なる変数間で「どの変数が目的変数に最も大きな影響を持つか」を公平に比較できる。
| 指標 | 値 | 解釈 |
|---|---|---|
| 決定係数 R² | 0.452 | 分散の45.2%を8変数で説明 |
| 自由度調整済み R² | 0.336 | 変数数の影響を調整後の説明力 |
| F統計量 | 3.91 | モデル全体の有意性検定 |
| F p値 | 0.0019 | モデル全体は高度に有意(p<0.01) |
| サンプルサイズ | 47 | 47都道府県 |
| 説明変数 | β(標準化) | p値 | 有意性 | 解釈 |
|---|---|---|---|---|
| 合計特殊出生率(TFR) | −0.731 | 0.0015 | ** | 最も強い負の効果。出生率が高いほど求職圧力が低い(活力ある地域) |
| 年平均気温 | +0.421 | 0.0524 | ns | 温暖な地域ほど求職圧力が高い傾向(辺縁有意) |
| 高齢化率 | −0.416 | 0.0536 | ns | 高齢化が進むほど求職圧力が低い傾向(辺縁有意) |
| 住宅地価格 | −0.366 | 0.0824 | ns | 地価が高い地域ほど求職圧力が低い(大都市圏の求人充実) |
| 男性率(生産年齢) | −0.283 | 0.0702 | ns | 辺縁有意。男性比率が高いほど求職圧力が低い |
| 宿泊者 per capita | +0.054 | 0.692 | ns | 効果は小さく非有意 |
| 消費支出 | −0.070 | 0.656 | ns | 多変数調整後は効果が消える(交絡が解消) |
| 大学進学率 | −0.010 | 0.957 | ns | 回帰モデルでは無効果 |
標準化係数は「説明変数が1標準偏差変化したとき、目的変数が何標準偏差変化するか」を表す。β の絶対値が大きいほど、その変数の相対的な影響力が大きい。実装には全変数を標準化してからOLSを実行する方法が最も直感的。
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | print("図4: 散布図(消費支出 vs 求職圧力指数)を作成中...") x_scatter = df_ana['消費支出'].values y_scatter = y r_s, p_s = scipy_stats.pearsonr(x_scatter, y_scatter) coef_fit = np.polyfit(x_scatter, y_scatter, 1) x_fit = np.linspace(x_scatter.min(), x_scatter.max(), 200) y_fit = np.polyval(coef_fit, x_fit) fig4, ax4 = plt.subplots(figsize=(10, 7)) scatter = ax4.scatter( x_scatter / 1000, y_scatter, # 千円単位に変換 c='#1565C0', alpha=0.75, s=70, edgecolors='white', linewidth=0.6, zorder=3 ) ax4.plot(x_fit / 1000, y_fit, color='#E65100', linewidth=2.2, linestyle='--', label=f'回帰直線 (r={r_s:.3f})', zorder=2) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。stats.pearsonr(x, y) — Pearson相関係数 r と p値を同時に返します。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | # 都道府県ラベル(代表的な都道府県) LABEL_PREFS = ['北海道', '東京都', '大阪府', '愛知県', '沖縄県', '秋田県', '青森県', '鹿児島県', '福岡県', '神奈川県'] for i, pref in enumerate(prefs): if pref in LABEL_PREFS: short = pref.replace('都','').replace('道','').replace('府','').replace('県','') ax4.annotate( short, (x_scatter[i] / 1000, y_scatter[i]), textcoords='offset points', xytext=(5, 3), fontsize=8.5, color='#333', bbox=dict(boxstyle='round,pad=0.2', facecolor='white', alpha=0.7, edgecolor='none') ) ax4.set_xlabel('消費支出(二人以上の世帯)[千円/月]', fontsize=12) ax4.set_ylabel('求職圧力指数(%)', fontsize=12) ax4.set_title( f'消費支出と求職圧力指数の関係(都道府県別, {YEAR}年)\n' f'r = {r_s:.3f} {"p < 0.05" if p_s < 0.05 else f"p = {p_s:.3f}"} ' f'回帰係数 = {coef_fit[0]*1000:.4f} (%/千円)', fontsize=12, fontweight='bold' ) ax4.legend(fontsize=11) ax4.grid(True, alpha=0.25) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。302 303 304 305 306 307 308 309 310 311 312 313 314 | # テキストボックス:解釈 ax4.text( 0.03, 0.95, f'r = {r_s:.3f}\n{"p < 0.05" if p_s < 0.05 else "p = {:.3f}".format(p_s)}\n' f'{"消費支出が高いほど\n求職圧力が低い傾向" if r_s < 0 else "消費支出が高いほど\n求職圧力が高い傾向"}', transform=ax4.transAxes, fontsize=10, va='top', bbox=dict(boxstyle='round', facecolor='#E3F2FD', alpha=0.85) ) plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2023_U5_5_fig4_scatter.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" -> 2023_U5_5_fig4_scatter.png 保存完了") |
図4: 散布図(消費支出 vs 求職圧力指数)を作成中... -> 2023_U5_5_fig4_scatter.png 保存完了
{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。個々の変数と目的変数の関係を散布図で確認する。「経済力(消費支出)が高い地域ほど失業圧力が低い」という仮説を検討する。
316 317 318 319 320 321 322 323 | df_corr = df_ana[X_NAMES + [Y_NAME]].copy() corr_matrix = df_corr.corr() print("\n【説明変数と求職圧力指数の相関】") for xn in X_NAMES: r, p = scipy_stats.pearsonr(df_ana[xn], y) sig = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else 'ns' print(f" {xn:<16} r = {r:+.3f} p = {p:.4f} {sig}") |
【説明変数と求職圧力指数の相関】 高齢化率 r = -0.412 p = 0.0040 ** 男性率 r = -0.219 p = 0.1399 ns 消費支出 r = +0.021 p = 0.8870 ns 住宅地価格 r = +0.304 p = 0.0375 * TFR r = -0.319 p = 0.0288 * 大学進学率 r = +0.330 p = 0.0236 * 年平均気温 r = +0.229 p = 0.1209 ns 宿泊者per_capita r = +0.078 p = 0.6012 ns
stats.pearsonr(x, y) — Pearson相関係数 r と p値を同時に返します。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。324 325 326 327 328 329 330 331 | X_reg = sm.add_constant(X) vif_vals = [variance_inflation_factor(X_reg, i + 1) for i in range(len(X_NAMES))] vif_df = pd.DataFrame({'変数': X_NAMES, 'VIF': vif_vals}) print("\n【VIF(分散拡大係数)】") for i, (xn, v) in enumerate(zip(X_NAMES, vif_vals)): flag = ' ★多重共線性の疑い' if v > 5 else '' print(f" {xn:<16} VIF = {v:.2f}{flag}") |
【VIF(分散拡大係数)】 高齢化率 VIF = 3.03 男性率 VIF = 1.60 消費支出 VIF = 1.70 住宅地価格 VIF = 2.91 TFR VIF = 3.18 大学進学率 VIF = 2.40 年平均気温 VIF = 3.07 宿泊者per_capita VIF = 1.28
sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | ols_result = sm.OLS(y, X_reg).fit() print("\n【OLS 重回帰結果(非標準化)】") print(f" R² = {ols_result.rsquared:.3f} AdjR² = {ols_result.rsquared_adj:.3f}") print(f" F統計量 = {ols_result.fvalue:.2f} p = {ols_result.f_pvalue:.4f}") print(ols_result.summary().tables[1]) # 標準化 OLS(Zスコア変換) y_z = (y - y.mean()) / y.std() X_z = (X - X.mean(axis=0)) / X.std(axis=0) X_z_const = sm.add_constant(X_z) ols_std = sm.OLS(y_z, X_z_const).fit() std_coefs = ols_std.params[1:] # 定数項を除く std_pvals = ols_std.pvalues[1:] std_bse = ols_std.bse[1:] # 標準誤差(95%CI用) print("\n【標準化回帰係数】") for xn, sc, pv in zip(X_NAMES, std_coefs, std_pvals): sig = '***' if pv < 0.001 else '**' if pv < 0.01 else '*' if pv < 0.05 else 'ns' print(f" {xn:<16} β = {sc:+.3f} p = {pv:.4f} {sig}") |
【OLS 重回帰結果(非標準化)】
R² = 0.452 AdjR² = 0.336
F統計量 = 3.91 p = 0.0019
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
const 148.0572 41.149 3.598 0.001 64.755 231.360
x1 -0.5714 0.287 -1.992 0.054 -1.152 0.009
x2 -1.2922 0.694 -1.863 0.070 -2.697 0.112
x3 -1.644e-05 3.66e-05 -0.449 0.656 -9.05e-05 5.77e-05
x4 -2.647e-05 1.48e-05 -1.784 0.082 -5.65e-05 3.57e-06
x5 -21.9614 6.434 -3.414 0.002 -34.985 -8.937
x6 -0.0064 0.119 -0.054 0.957 -0.248 0.235
x7 0.8260 0.412 2.002 0.052 -0.009 1.661
x8 0.1566 0.392 0.399 0.692 -0.637 0.950
==============================================================================
【標準化回帰係数】
高齢化率 β = -0.416 p = 0.0536 ns
男性率 β = -0.283 p = 0.0702 ns
消費支出 β = -0.070 p = 0.6560 ns
住宅地価格 β = -0.366 p = 0.0824 ns
TFR β = -0.731 p = 0.0015 **
大学進学率 β = -0.010 p = 0.9574 ns
年平均気温 β = +0.421 p = 0.0524 ns
宿泊者per_capita β = +0.054 p = 0.6920 nssm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | print("\n" + "=" * 65) print("✓ 全図の生成完了(4枚)") print("=" * 65) print(f"\n保存先: {os.path.abspath(FIG_DIR)}") print(" 2023_U5_5_fig1_dist.png - 求職圧力指数の分布(ヒストグラム+KDE)") print(" 2023_U5_5_fig2_corr.png - 相関係数ヒートマップ") print(" 2023_U5_5_fig3_coef.png - OLS標準化係数棒グラフ(エラーバー付き)") print(" 2023_U5_5_fig4_scatter.png - 消費支出 vs 求職圧力指数散布図") print() print("【主要知見】") print(f" 分析対象: {N}都道府県(SSDSE-B {YEAR}年, 実データ)") print(f" 求職圧力指数: 平均 {y.mean():.2f}% SD={y.std():.2f}%") print(f" OLS R² = {ols_result.rsquared:.3f} AdjR² = {ols_result.rsquared_adj:.3f}") print(f" VIF 最大値: {max(vif_vals):.2f} 最小値: {min(vif_vals):.2f}") sig_vars = [X_NAMES[i] for i, p in enumerate(std_pvals) if p < 0.05] print(f" 有意な説明変数(p<0.05): {sig_vars if sig_vars else '—(なし)'}") print(f" r(消費支出 vs 求職圧力): {r_s:.3f} p={p_s:.4f}") print() print(" ※ 合成データ・乱数は一切使用していません") print(" ※ データ出典: 統計センター SSDSE-B-2026") |
================================================================= ✓ 全図の生成完了(4枚) ================================================================= 保存先: /Users/shimpei/Dropbox/Works_Researches/2026 統計・データ解析コンペ/html/figures 2023_U5_5_fig1_dist.png - 求職圧力指数の分布(ヒストグラム+KDE) 2023_U5_5_fig2_corr.png - 相関係数ヒートマップ 2023_U5_5_fig3_coef.png - OLS標準化係数棒グラフ(エラーバー付き) 2023_U5_5_fig4_scatter.png - 消費支出 vs 求職圧力指数散布図 【主要知見】 分析対象: 47都道府県(SSDSE-B 2022年, 実データ) 求職圧力指数: 平均 42.22% SD=4.44% OLS R² = 0.452 AdjR² = 0.336 VIF 最大値: 3.18 最小値: 1.28 有意な説明変数(p<0.05): ['TFR'] r(消費支出 vs 求職圧力): 0.021 p=0.8870 ※ 合成データ・乱数は一切使用していません ※ データ出典: 統計センター SSDSE-B-2026
np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。原論文は市区町村レベルのデータで、①重回帰 → ②Elastic Net(過学習抑制) → ③ランダムフォレスト回帰+SHAP値(非線形関係と重要度) → ④主成分分析+k平均法(次元削減と類型化)という 4 段のパイプラインを組んでいます。ここでは原論文と同じ市区町村データ(SSDSE-A-2025、N=1,740)で、失業率 = 完全失業者数 ÷(就業者数+完全失業者数)を計算し、4 段すべてを一気に再現します。
1 2 3 4 5 6 7 8 9 10 11 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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | import numpy as np import pandas as pd import statsmodels.api as sm from sklearn.linear_model import ElasticNetCV from sklearn.ensemble import RandomForestRegressor from sklearn.decomposition import PCA from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score import shap # SSDSE-A(市区町村データ、原論文と同じデータ種別) a = pd.read_csv('data/raw/SSDSE-A-2025.csv', encoding='cp932', header=2) a = a[a['地域コード'].str.match(r'^R\d+', na=False)].copy() num = lambda c: pd.to_numeric(a[c], errors='coerce') # 目的変数: 失業率 = 完全失業者数 ÷ (就業者数 + 完全失業者数) × 100 labor = num('就業者数') + num('完全失業者数') y_all = num('完全失業者数') / labor * 100 X_all = pd.DataFrame({ '高齢化率': num('65歳以上人口') / num('総人口') * 100, '人口密度': num('総人口') / num('可住地面積'), '転入率': num('転入者数(日本人移動者)') / num('総人口') * 100, '単独世帯割合': num('単独世帯数') / num('一般世帯数') * 100, '第1次産業割合': num('第1次産業就業者数') / num('就業者数') * 100, '第2次産業割合': num('第2次産業就業者数') / num('就業者数') * 100, '第3次産業割合': num('第3次産業就業者数') / num('就業者数') * 100, '経常収支比率': num('経常収支比率(市町村財政)'), '小売店密度': num('小売店数') / num('総人口') * 10000, '医療福祉従業者割合': num('従業者数(民営)(医療、福祉)') / num('従業者数(民営)') * 100, }) mask = ~(y_all.isna() | X_all.isna().any(axis=1)) & (labor > 0) X_all, y_all = X_all[mask].reset_index(drop=True), y_all[mask].reset_index(drop=True) names = a.loc[mask.values, ['都道府県', '市区町村']].reset_index(drop=True) print(f'市区町村数: {len(X_all)}(欠損除外後) 失業率 平均 {y_all.mean():.2f}% / 最大 {y_all.max():.2f}%') # ── (1) 重回帰(OLS) ── Xz = pd.DataFrame(StandardScaler().fit_transform(X_all), columns=X_all.columns) ols = sm.OLS(y_all, sm.add_constant(Xz)).fit() print('\n=== (1) 重回帰(標準化係数、上位5) ===') top = ols.params.drop('const').abs().sort_values(ascending=False).head(5) for n in top.index: print(f' {n:12s} {ols.params[n]:+.3f} (p={ols.pvalues[n]:.1e})') # ── (2) Elastic Net(CV で正則化を選択) ── en = ElasticNetCV(cv=5, l1_ratio=[.1, .5, .9], random_state=0).fit(Xz, y_all) kept = [(n, c) for n, c in zip(X_all.columns, en.coef_) if abs(c) > 1e-10] print(f'=== (2) Elastic Net === alpha={en.alpha_:.4f}, l1_ratio={en.l1_ratio_}, 残った変数 {len(kept)}/10') # ── (3) RandomForest + SHAP ── X_tr, X_te, y_tr, y_te = train_test_split(X_all, y_all, test_size=0.2, random_state=42) rf = RandomForestRegressor(n_estimators=300, random_state=42).fit(X_tr, y_tr) print(f'=== (3) RandomForest === テストR2 = {r2_score(y_te, rf.predict(X_te)):.3f}') sv = shap.TreeExplainer(rf).shap_values(X_te) imp = pd.Series(np.abs(sv).mean(axis=0), index=X_all.columns).sort_values(ascending=False) print(' SHAP 上位3:', '、'.join(f'{n}({v:.3f})' for n, v in imp.head(3).items())) # ── (4) PCA + k平均法 ── pca = PCA(n_components=2) pc = pca.fit_transform(Xz) km = KMeans(n_clusters=4, random_state=0, n_init=10).fit(pc) print(f'=== (4) PCA + k-means === PC1+PC2 寄与率 = {pca.explained_variance_ratio_.sum()*100:.1f}%') for k in range(4): m = km.labels_ == k print(f' クラスタ{k}: {m.sum():4d} 市区町村 平均失業率 {y_all[m].mean():.2f}% 例: ' + '、'.join((names['市区町村'][m]).head(3))) |
X_all)に統一しておくと、4 つの手法に同じデータを流して結果を比較できます。以下のファイルをダウンロードして同じフォルダに置き、python 2023_U5_5_shorei.py を実行すると全図・全結果を再現できます。
必要ライブラリ: numpy, pandas, matplotlib, scipy, statsmodels
| データ | 出典 |
|---|---|
| SSDSE-B(都道府県別)2022年データ | 統計センター SSDSE 2026年版 |
| 月間有効求職者数・求人数(労働市場統計) | 厚生労働省 職業安定業務統計(SSDSE-B収録) |
| 合計特殊出生率・消費支出・住宅地価格 | 各府省統計(SSDSE-B収録) |
本コードは SSDSE-B-2026 の実データのみ使用。合成データ・乱数(np.random 等)は一切使用していません。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
この論文を理解できたか、クリックで確認しましょう。間違えても解説が出ます。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2023_U5_5_shorei.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。