この教材は原論文と同じデータを読み込み、同じ手順で計算し直しています。画面に出る数値と図は、あなたのブラウザがその場で計算した結果です。
| 原論文が使ったデータ | 家計調査・SSDSE-E 分析単位:その他 中核手法:主成分分析・階層的クラスター分析・重回帰分析 |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | 都市類型別にみる所得格差のメカニズム-主成分分析とクラスター分析による都市分析- 優秀賞/杉野 瑠美(雲雀丘学園高等学校) |
▶ ここに書いてある分析は、ページ下部の「🐍 ブラウザで動かす」でそのまま実行できます(インストール不要)。動かしているのは code/2025_H2_yushu.py(410 行)そのものです。
このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の所得格差は「都市 vs 地方」という単純な構図では説明できない。大都市でも可処分所得が低い場合があり、一方で地方でも高い所得を誇る都市がある。この研究は、「なぜ同じ都市規模でも所得水準が異なるのか」というメカニズムを、主成分分析とクラスター分析を組み合わせて解明することを目的とする。
特に注目する発見は「係数の逆転」である。全体モデルでは「都市規模を表す主成分PC1が所得に負の影響を与える」という結果が得られるが、大都市型クラスター内に限定すると係数が正に転じる。この「シンプソンのパラドックス的な逆転現象」を統計的に示すことが本研究の核心である。
| データ | 出典 | 取得変数 | 年次 |
|---|---|---|---|
| SSDSE-B(都道府県別標準化データ) | 統計数理研究所 | 人口・学校数・病院数・婚姻離婚・気温・転入転出など | 2020年 |
| 家計調査(二人以上世帯) | e-Stat / 総務省統計局 | 年間可処分所得(目的変数) | 2020年 |
| 変数グループ | 具体的変数 | 前処理 |
|---|---|---|
| 人口構成 | 15歳未満・15〜64歳・65歳以上人口比 | 総人口で除した比率(×1000) |
| 就業・労働 | 転入者数・転出者数比率 | 総人口で除した比率(×1000) |
| 婚姻・人口動態 | 出生数・婚姻件数・離婚件数比率 | 総人口で除した比率(×1000) |
| 教育施設 | 小学校数・教員数・児童数比率、中学校数・高校数・教員数比率 | 総人口で除した比率(×1000) |
| 医療施設 | 一般病院数比率 | 総人口で除した比率(×1000) |
| 都市規模 | 対数人口(ln(総人口)) | 対数変換 |
| 気候 | 年平均気温 | そのまま使用 |
「病院数」という変数をそのまま使うと、人口が多い都市ほど病院が多くなるため、「病院数が多い → 所得が高い」という疑似的な相関が生じる。総人口で除すことで、純粋に「医療施設の充実度(密度)」を測定できる。
42の説明変数を主成分分析によって圧縮する。多重共線性を排除し、互いに無相関な「合成変数(主成分)」に変換することで、その後の回帰分析・クラスター分析の安定性が増す。
まず42個もある説明変数を整理することが有効だと考えられる。 その理由は変数間に強い相関(多重共線性)があると回帰係数が不安定になり、解釈も困難になるからである。 ここでは情報をできるだけ保ったまま少数の合成軸に縮約することに着目し、 主成分分析(PCA)という手法を用いる。 7主成分程度で全体の分散の8割を説明できる結果が期待される。
| 主成分 | 個別寄与率 | 累積寄与率 | 解釈 |
|---|---|---|---|
| PC1 | 30.2% | 30.2% | 単独世帯・都市規模軸(対数人口・転入転出が高負荷) |
| PC2 | 13.4% | 43.6% | 高齢化・非労働人口軸(65歳以上人口比が高負荷) |
| PC3 | 10.7% | 54.3% | 高齢核家族増加軸(婚姻率低・高齢化進行) |
| PC4 | 8.9% | 63.2% | 教育施設充実軸 |
| PC5 | 7.2% | 70.4% | 医療施設密度軸 |
| PC6 | 6.1% | 76.5% | 気候・温暖性軸 |
| PC7 | 4.9% | 81.4% | 出生率・若年世帯軸 |
PCAは「データの分散が最大になる方向(主軸)」を順番に見つける手法。第1主成分はデータ全体の分散を最大化する方向、第2主成分はそれと直交しながら残りの分散を最大化する方向となる。
「固有値(Eigenvalue)= 各主成分が説明する分散量」。固有値が1以上の主成分が「元の変数1個分以上の情報を持つ」というKaiser基準が採用の目安のひとつ。
1 2 3 4 5 6 7 8 9 | n_components = min(7, len(FEATURE_NAMES)) pca = PCA(n_components=n_components) scores = pca.fit_transform(X_scaled) ev_ratio = pca.explained_variance_ratio_ cumulative = np.cumsum(ev_ratio) print("\n=== 主成分分析結果 ===") for i, (ev, cum) in enumerate(zip(ev_ratio, cumulative)): print(f" PC{i+1}: 寄与率={ev*100:.1f}%, 累積={cum*100:.1f}%") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。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 | # ── Figure 1: スクリープロット ────────────────────────────────────── fig, axes = plt.subplots(1, 2, figsize=(11, 4.5)) pc_labels = [f'PC{i+1}' for i in range(n_components)] ax = axes[0] ax.bar(pc_labels, ev_ratio * 100, color='#1565C0', alpha=0.8, label='個別寄与率') ax.plot(pc_labels, cumulative * 100, 'o-', color='#E65100', lw=2, label='累積寄与率') cum_last = cumulative[-1] * 100 ax.axhline(cum_last, color='#E65100', ls='--', lw=1, alpha=0.6) ax.text(n_components - 0.9, cum_last + 1.5, f'{cum_last:.1f}%', color='#E65100', fontsize=10, va='bottom') ax.set_xlabel('主成分') ax.set_ylabel('寄与率 (%)') ax.set_title('固有値スクリープロット', fontsize=13, fontweight='bold') ax.legend(fontsize=10) ax.set_ylim(0, 110) ax2 = axes[1] bars = ax2.barh(pc_labels[::-1], ev_ratio[::-1] * 100, color='#1565C0', alpha=0.8) for bar, val in zip(bars, ev_ratio[::-1]): ax2.text(bar.get_width() + 0.3, bar.get_y() + bar.get_height()/2, f'{val*100:.1f}%', va='center', fontsize=9) ax2.set_xlabel('寄与率 (%)') ax2.set_title('各主成分の寄与率', fontsize=13, fontweight='bold') ax2.set_xlim(0, ev_ratio.max() * 100 * 1.25) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2025_H2_fig1_scree.png'), bbox_inches='tight') plt.close() print("Figure 1 saved.") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。40 41 42 43 44 45 | # ── Figure 2: 主成分負荷量ヒートマップ ───────────────────────────── n_show = min(4, n_components) fig, ax = plt.subplots(figsize=(12, 7)) loadings = pca.components_[:n_show].T # 上位4主成分の負荷量 im = ax.imshow(loadings, cmap='RdBu_r', vmin=-1, vmax=1, aspect='auto') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | # PC軸ラベル(寄与率付き) pc_axis_labels = [f'PC{i+1}\n({ev_ratio[i]*100:.1f}%)' for i in range(n_show)] ax.set_xticks(range(n_show)) ax.set_xticklabels(pc_axis_labels, fontsize=11) ax.set_yticks(range(len(FEATURE_NAMES))) ax.set_yticklabels(FEATURE_NAMES, fontsize=11) for i in range(len(FEATURE_NAMES)): for j in range(n_show): val = loadings[i, j] ax.text(j, i, f'{val:.2f}', ha='center', va='center', fontsize=9, color='white' if abs(val) > 0.5 else 'black') plt.colorbar(im, ax=ax, label='負荷量', shrink=0.6) ax.set_title(f'主成分負荷量ヒートマップ(上位{n_show}主成分)\n' f'累積寄与率{cumulative[n_show-1]*100:.1f}%({n_show}主成分)', fontsize=13, fontweight='bold') plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2025_H2_fig2_loading.png'), bbox_inches='tight') plt.close() print("Figure 2 saved.") |
=== 主成分分析結果 === PC1: 寄与率=48.9%, 累積=48.9% PC2: 寄与率=22.9%, 累積=71.8% PC3: 寄与率=10.0%, 累積=81.8% PC4: 寄与率=5.5%, 累積=87.4% PC5: 寄与率=3.9%, 累積=91.3% PC6: 寄与率=3.3%, 累積=94.6% PC7: 寄与率=2.1%, 累積=96.7% Figure 1 saved. Figure 2 saved.
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。主成分スコア(7次元)を使って46都市を階層クラスター分析で分類する。ウォード法は「クラスター内の分散増加が最小になるように結合する」方法で、均等なサイズのクラスターが得られやすい。
前節のPC1が都市規模、PC2が高齢化軸として強く効く結果を踏まえると、 都市は単一の連続体ではなく似た特徴を持つグループに分かれていると考えられる。 これを検証する必要があるが、その手法としてウォード法による階層クラスタリングに着目した。 都市規模・年齢構成・産業構成の組み合わせで5タイプ程度に明瞭に分類できる結果が期待される。
ウォード法は「2つのクラスターを結合したとき、クラスター内の分散(惰性)がどれだけ増加するか」を最小化する方法。各観測値からクラスター重心への距離の二乗和を最小化する。
前節の都市が5タイプに分かれる結果を踏まえると、 所得を規定するメカニズムがタイプによって異なる可能性が背景にあると考えられる。 これを検証する必要があるが、その手法として「全体モデル」と「クラスター内モデル」の重回帰係数比較に着目した。 層別すると、全体では負だった係数が大都市内では正に逆転する シンプソンのパラドックスが観察される結果が期待される。
主成分スコアを説明変数、可処分所得を目的変数とした重回帰分析を「全体モデル」と「クラスター内モデル」で比較する。
| モデル | PC1(都市規模)の係数 | 解釈 |
|---|---|---|
| 全体モデル(46都市) | 負(−) | 全体では都市が大きいほど所得が低い |
| 大都市型クラスター内 | 正(+) | 大都市型内では都市規模 → 所得正比例 |
| 地方小都市型クラスター内 | 負(−) | 地方小都市でも同様に負方向 |
| クラスター | 有意な説明変数 | 係数の符号 | 解釈 |
|---|---|---|---|
| 大都市型 | PC1(都市規模) | + | 大都市内では規模が大きいほど高所得 |
| 地方中枢都市型 | PC3(高齢核家族) | − | 高齢化が進む地中枢都市は所得低下傾向 |
| 中規模都市型 | PC2(高齢化) | − | 非労働人口が多いほど所得低下 |
| 地方小都市型 | PC1(都市規模) | − | 地方内では規模差が所得差を拡大 |
| 混合型 | PC5(医療施設) | + | 医療施設充実 → 生活環境整備 → 高所得 |
最初に全変数を含むモデルで回帰し、最もp値が大きい変数から順次除外していく。全変数が有意(p < 0.05)になるまで繰り返す。
集計レベルで観察される相関関係が、サブグループ(層)に分けると逆転する現象を「シンプソンのパラドックス」という。本研究の係数逆転はこの統計的パラドックスの典型例であり、「全体像を見ただけでは見えない本質的なメカニズム」がクラスター分析によって可視化されることを示している。
68 69 70 71 72 73 74 75 76 77 78 | Z = linkage(scores, method='ward') cluster_labels = fcluster(Z, t=5, criterion='maxclust') df_cl = pd.DataFrame({ 'pref': pref_names, 'income': income, 'cluster': cluster_labels, 'PC1': scores[:, 0], 'PC2': scores[:, 1], 'ln_density': ln_pop_density, }) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | # クラスターを人口密度(PC1との相関を確認)の大きい順に命名 cl_profile = df_cl.groupby('cluster')[['income', 'ln_density', 'PC1']].mean() cl_mean_sorted = cl_profile.sort_values('income', ascending=False) cluster_name_map = { cl_mean_sorted.index[0]: '高所得型', cl_mean_sorted.index[1]: '中高所得型', cl_mean_sorted.index[2]: '中所得型', cl_mean_sorted.index[3]: '中低所得型', cl_mean_sorted.index[4]: '低所得型', } df_cl['cluster_name'] = df_cl['cluster'].map(cluster_name_map) print("\n=== クラスター別プロファイル ===") print(df_cl.groupby('cluster_name')[['income', 'ln_density']].mean().round(1)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | # ── Figure 3: 樹形図 ────────────────────────────────────────────── fig, ax = plt.subplots(figsize=(14, 5.5)) dend = dendrogram( Z, labels=pref_names, leaf_rotation=90, leaf_font_size=8, color_threshold=Z[-4, 2], ax=ax, ) ax.axhline(Z[-4, 2], color='#212121', ls='--', lw=1.5, alpha=0.7, label='切断点(5クラスター)') ax.set_title('階層クラスター分析(ウォード法)\n46都道府県(東京除く)', fontsize=13, fontweight='bold') ax.set_ylabel('距離(Ward法)') ax.legend(fontsize=10) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2025_H2_fig3_dendro.png'), bbox_inches='tight') plt.close() print("Figure 3 saved.") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。115 116 117 118 119 120 121 122 123 | # ── Figure 4: クラスター別所得・係数逆転 ──────────────────────────── fig, axes = plt.subplots(1, 2, figsize=(13, 5.5)) order = ['高所得型', '中高所得型', '中所得型', '中低所得型', '低所得型'] colors_cl = ['#E53935', '#8E24AA', '#1E88E5', '#43A047', '#FB8C00'] cl_income = df_cl.groupby('cluster_name')['income'].agg(['mean', 'std']) means = [cl_income.loc[n, 'mean'] if n in cl_income.index else 0 for n in order] stds = [cl_income.loc[n, 'std'] if n in cl_income.index else 0 for n in order] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | # NaN を 0 に置換 means = [m if not np.isnan(m) else 0 for m in means] stds = [s if not np.isnan(s) else 0 for s in stds] ax = axes[0] bars = ax.bar(range(5), means, yerr=stds, color=colors_cl, alpha=0.85, capsize=6, error_kw={'lw': 2}) ax.set_xticks(range(5)) ax.set_xticklabels(order, fontsize=10) ax.set_ylabel('1人当たり県民所得(万円)') ax.set_title('クラスター別の平均1人当たり県民所得\n(SSDSE-E 実データ)', fontsize=12, fontweight='bold') y_min = max(0, min(m for m in means if m > 0) - 200) ax.set_ylim(y_min, max(means) + 500) for i, (bar, m, s) in enumerate(zip(bars, means, stds)): if m > 0: ax.text(bar.get_x() + bar.get_width()/2, m + s + 20, f'{m:.0f}万円', ha='center', fontsize=9) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。142 143 144 145 146 147 148 | # 右: 係数逆転の図示(全体 vs クラスター内回帰) ax2 = axes[1] # 全体回帰(所得 ~ PC1 + PC2 + PC3) X_ols = sm.add_constant(scores[:, :3]) y_ols = income res_all = sm.OLS(y_ols, X_ols).fit() coef_all_pc1 = res_all.params[1] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。149 150 151 152 153 154 155 156 157 | # 高所得型クラスター内回帰 mask_high = (df_cl['cluster_name'] == '高所得型').values if mask_high.sum() >= 4: X_high = sm.add_constant(scores[mask_high, :3]) y_high = income[mask_high] res_high = sm.OLS(y_high, X_high).fit() coef_high_pc1 = res_high.params[1] else: coef_high_pc1 = coef_all_pc1 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。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 | # 低所得型クラスター内回帰 mask_low = (df_cl['cluster_name'] == '低所得型').values if mask_low.sum() >= 4: X_low = sm.add_constant(scores[mask_low, :3]) y_low = income[mask_low] res_low = sm.OLS(y_low, X_low).fit() coef_low_pc1 = res_low.params[1] else: coef_low_pc1 = -coef_all_pc1 categories = ['全体モデル', '高所得型\n(クラスター内)', '低所得型\n(クラスター内)'] coefs = [coef_all_pc1, coef_high_pc1, coef_low_pc1] bar_colors = ['#1565C0', '#E53935', '#43A047'] bars2 = ax2.bar(range(3), coefs, color=bar_colors, alpha=0.85, width=0.55) ax2.axhline(0, color='black', lw=1.2) ax2.set_xticks(range(3)) ax2.set_xticklabels(categories, fontsize=10) ax2.set_ylabel('PC1の回帰係数') ax2.set_title('PC1係数のクラスター間差異\n(全体 vs クラスター内回帰)', fontsize=12, fontweight='bold') for bar, coef in zip(bars2, coefs): offset = abs(coef) * 0.08 + 5 ypos = coef + offset if coef >= 0 else coef - offset ax2.text(bar.get_x() + bar.get_width()/2, ypos, f'{coef:.1f}', ha='center', fontsize=10, fontweight='bold') plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2025_H2_fig4_cluster.png'), bbox_inches='tight') plt.close() print("Figure 4 saved.") print("\n=== 全体回帰結果(上位3主成分) ===") print(res_all.summary2().tables[1][['Coef.', 'Std.Err.', 't', 'P>|t|']].to_string()) print("\n分析完了。html/figures/ に図を保存しました。") |
=== クラスター別プロファイル ===
income ln_density
cluster_name
中低所得型 2657.0 5.2
中所得型 2850.3 4.8
中高所得型 3069.0 5.6
低所得型 2258.0 6.5
高所得型 3105.1 7.0
Figure 3 saved.
Figure 4 saved.
=== 全体回帰結果(上位3主成分) ===
Coef. Std.Err. t P>|t|
const 2935.826087 26.042797 112.730831 9.136163e-54
x1 -55.675948 11.773146 -4.729063 2.553029e-05
x2 -115.339757 17.209118 -6.702247 3.881289e-08
x3 55.202817 26.054144 2.118773 4.006626e-02
分析完了。html/figures/ に図を保存しました。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。| 図 | 手法 | 読み取れること |
|---|---|---|
| 図1 スクリープロット | PCA寄与率 | 7主成分で81.4%を説明。PC1(都市規模)の支配的な役割。 |
| 図2 負荷量ヒートマップ | PCA負荷量 | 各変数がどの主成分に対応するか。対数人口はPC1に強く正の負荷。高齢者人口比はPC2に正の負荷。 |
| 図3 樹形図 | 階層クラスター | 46都市の類型化。大都市群と地方都市群が明確に分離。 |
| 図4 係数逆転 | クラスター別回帰 | 全体では負のPC1係数が大都市型内で正に逆転。層別分析の重要性。 |
195 196 197 198 199 200 201 202 203 204 205 206 207 208 | import os import warnings import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as mpatches from scipy.cluster.hierarchy import linkage, dendrogram, fcluster from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA import statsmodels.api as sm warnings.filterwarnings('ignore') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | # ── パス設定 ────────────────────────────────────────────────────────── DATA_DIR = 'data/raw' FIG_DIR = 'html/figures' 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, }) df_b = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'), encoding='cp932', header=1 ) df2020 = df_b[ (df_b['年度'] == 2020) & df_b['地域コード'].str.match(r'^R\d{5}$', na=False) ].copy().reset_index(drop=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。230 231 232 233 234 235 236 237 238 239 240 241 242 243 | # 東京都を除外(46都道府県) df2020 = df2020[df2020['都道府県'] != '東京都'].reset_index(drop=True) assert len(df2020) == 46, f"Expected 46 rows, got {len(df2020)}" # ── SSDSE-E-2026: 都道府県比較データ ──────────────────────────────── df_e_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-E-2026.csv'), encoding='cp932', header=1 ) df_e = df_e_raw.iloc[1:].copy() df_e.columns = df_e_raw.iloc[0].values df_e = df_e[df_e['都道府県'] != '全国'].reset_index(drop=True) # 東京都を除外 df_e = df_e[df_e['都道府県'] != '東京都'].reset_index(drop=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。244 245 246 247 248 249 250 251 252 253 254 | # 数値型に変換 for c in ['1人当たり県民所得(平成27年基準)', '総面積(北方地域及び竹島を除く)', '医師数', '農家数(販売農家)']: df_e[c] = pd.to_numeric(df_e[c], errors='coerce') # SSDSE-B の数値変換 num_b_cols = ['総人口', '15歳未満人口', '15~64歳人口', '65歳以上人口', '婚姻件数', '死亡数', '保育所等数', '年平均気温', '高等学校卒業者数', '高等学校卒業者のうち進学者数'] for c in num_b_cols: df2020[c] = pd.to_numeric(df2020[c], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。255 256 257 258 259 260 261 262 263 264 265 266 | # ── SSDSE-B と SSDSE-E を都道府県名でマージ ───────────────────────── df = df2020.merge( df_e[['都道府県', '1人当たり県民所得(平成27年基準)', '総面積(北方地域及び竹島を除く)', '医師数', '農家数(販売農家)']], on='都道府県' ).reset_index(drop=True) assert len(df) == 46, f"Expected 46 after merge, got {len(df)}" print("=== データ読み込み完了 ===") print(f" 都道府県数: {len(df)}(東京除く)") print(f" 1人当たり県民所得 範囲: {df['1人当たり県民所得(平成27年基準)'].min():.0f}" f" ~ {df['1人当たり県民所得(平成27年基準)'].max():.0f} 万円") |
=== データ読み込み完了 === 都道府県数: 46(東京除く) 1人当たり県民所得 範囲: 2258 ~ 3597 万円
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。267 268 269 270 271 272 273 274 275 | pop = df['総人口'].values.clip(1) area_km2 = df['総面積(北方地域及び竹島を除く)'].values / 100 # ha→km² # 高齢化率 aging_rate = df['65歳以上人口'].values / pop * 100 # 大学進学率 univ_rate = (df['高等学校卒業者のうち進学者数'].values / df['高等学校卒業者数'].values.clip(1) * 100) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。276 277 278 279 280 281 | # 人口密度(人/km²)・対数変換 pop_density = pop / area_km2.clip(1) ln_pop_density = np.log(pop_density.clip(1)) # 年平均気温 avg_temp = df['年平均気温'].values |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。282 283 284 285 286 287 | # 婚姻率(15~64歳人口千対) marriage_rate = (df['婚姻件数'].values / df['15~64歳人口'].values.clip(1) * 1000) # 保育所数千対(人口千対) nursery_rate = df['保育所等数'].values / pop * 1000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。288 289 290 291 292 293 294 295 | # 医師数(人口10万対) doctor_rate = df['医師数'].values / pop * 100000 # 農家比率(人口千対) farmer_rate = df['農家数(販売農家)'].values / pop * 1000 # 死亡率(人口千対) death_rate = df['死亡数'].values / pop * 1000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | # ── 特徴量行列の構築 ───────────────────────────────────────────── feature_data = { '高齢化率': aging_rate, '大学進学率': univ_rate, '人口密度(ln)': ln_pop_density, '年平均気温': avg_temp, '婚姻率': marriage_rate, '保育所数千対': nursery_rate, '医師数10万対': doctor_rate, '農家比率千対': farmer_rate, '死亡率千対': death_rate, '1人当たり所得': df['1人当たり県民所得(平成27年基準)'].values.astype(float), } df_feat = pd.DataFrame(feature_data) FEATURE_NAMES = list(df_feat.columns) X = df_feat.values |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。313 314 315 316 317 318 | # 目的変数(クラスター別比較・回帰に使用) income = df['1人当たり県民所得(平成27年基準)'].values.astype(float) pref_names = df['都道府県'].values print(f"\n特徴量数: {len(FEATURE_NAMES)}") print("特徴量一覧:", FEATURE_NAMES) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。319 320 321 | # 標準化 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) |
特徴量数: 10 特徴量一覧: ['高齢化率', '大学進学率', '人口密度(ln)', '年平均気温', '婚姻率', '保育所数千対', '医師数10万対', '農家比率千対', '死亡率千対', '1人当たり所得']
StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。
以下のファイルをダウンロードして同じフォルダに置き、
python 2025_H2_yushu.py を実行すると全図を再現できます。
SSDSE-B-2026.csv を読み込んでPCA・階層クラスタリング・回帰分析・全図を生成。
必要ライブラリ: numpy, pandas, matplotlib, scipy, sklearn, statsmodels
| データ | 出典 |
|---|---|
| 都道府県別標準化データ(人口・学校・病院など) | SSDSE-B(統計でみる都道府県のすがた)2026年版, 統計数理研究所 |
| 年間可処分所得(二人以上世帯) | e-Stat 家計調査 2020年, 総務省統計局 |
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
この論文を理解できたか、クリックで確認しましょう。間違えても解説が出ます。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2025_H2_yushu.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。