この教材は、原論文が使ったデータそのものを使えていません。そこで代わりのデータで同じ問いを追いかけ、結論の向き(増える/減る、強い/弱い)が原論文と一致するかを確かめます。「同じ数値が出る」ことは目標にしていません。
| 原論文が使ったデータ | 統計でみる市区町村のすがた・地方公共団体の主要財政指標一覧・都道府県地価調査・国勢調査・SSDSE-A・学校基本調査 分析単位:市区町村 中核手法:相関分析 |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | 地価に関する最適モデルの構築と手法提案 統計数理賞/柏原 昊隼、田原 睦己、大西 裕貴(雲雀丘学園高等学校) |
原論文と同じ粒度のデータは、この教材にも同梱しています。これを読み込めば、原論文と同じ細かさで分析をやり直せます(下の「🐍 ブラウザで動かす」でコードを書き換えて試せます)。ただし原論文が使った項目がすべて収録されているとは限りません。足りない項目は、上の「できないこと」に書いた出典から取ってくる必要があります。
▶ ここに書いてある分析は、ページ下部の「🐍 ブラウザで動かす」でそのまま実行できます(インストール不要)。動かしているのは code/2023_H3_suri.py(419 行)そのものです。
このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
地価(土地の価格)は、人口、雇用、観光、教育、医療、気候など多様な社会経済要因によって決定される。本研究では、47都道府県の統計データ(SSDSE-B)を用いて、住宅地標準価格を目的変数とする複数の回帰モデルを構築し、モデル選択基準(AIC・BIC・交差検証)を用いて最適モデルを提案する。
原論文の概要(統計センター公式):「都市部への人口集中や地方の過疎化の問題に着目し、地価データを用いて因子分析及び重回帰分析を行い、地価の高い地域に共通する因子の特徴を推定することで、地方の人口増加に貢献する要因を見出した。」
本ページではこの分析の流れを実データでたどりながら、使われた統計手法を一つずつ学んでいく。
SSDSE-B OLS重回帰 Ridge回帰 Lasso回帰 AIC・BIC 交差検証
SSDSE-B(都道府県別統計データセット)の2022年度データを使用。住宅地価格に影響すると考えられる9つの説明変数を理論的根拠とともに選定した。
| 種別 | 変数(コード) | 説明 | 想定効果 |
|---|---|---|---|
| 目的変数 | C5401 住宅地標準価格 | 都道府県の住宅地平均標準価格(千円/m²) | ― |
| 需要 | A1101 総人口 | 人口が多いほど土地需要が高い | 正 |
| A5101/A1101 転入率 | 人口流入が活発なほど需要増 | 正 | |
| 労働市場 | A1302/A1101 生産年齢人口割合 F3103/F3102 有効求人倍率 | 労働力・雇用の豊富さ | 正 |
| 産業・観光 | G7101/A1101 宿泊者数per capita | 商業・観光の活発さ | 正 |
| 教育 | E6302/A1101 大学学生率 | 教育機関が集積するほど都市的価値が上昇 | 正 |
| 医療 | I510120/A1101 病院密度 | 医療環境の充実(アメニティ効果) | 正? |
| 気候 | B4101 年平均気温 | 温暖な地域は居住選好が高い | 正 |
| 少子化 | A4103 合計特殊出生率 | 若年世帯が多い地域の居住価値 | ? |
まず総人口と住宅地標準価格の散布図を描き、外れ値の有無を確認する。東京都は人口・地価ともに他都道府県から大きく乖離した「超高価格・超大人口」の外れ値であり、除外して分析する。
地価・人口のような右裾が長い変数は対数変換することで、外れ値の影響を抑えつつ全体の傾向を把握しやすくなる。散布図で外れ値を確認し、レバレッジ点(推定値に大きな影響を与える観測値)かどうかを判断するのは回帰分析の基本ステップ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 import statsmodels.api as sm from sklearn.linear_model import Ridge, Lasso, LinearRegression from sklearn.model_selection import cross_val_score from sklearn.preprocessing import StandardScaler 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} のように書式も指定できます。15 16 17 18 19 20 21 22 23 24 25 26 | # ── パス設定 ────────────────────────────────────────────────────────── 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, }) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。27 28 29 30 31 32 33 34 | # ── SSDSE-B-2026.csv 読み込み ────────────────────────────────────── print("=== SSDSE-B 読み込み ===") df_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'), encoding='cp932', header=0, skiprows=[1], # 日本語ラベル行をスキップ(先頭行=コード名を使用) ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。35 36 37 38 39 40 | # 都道府県レベルのみ(地域コード = R + 5桁数字) df_raw = df_raw[df_raw['Code'].str.match(r'^R\d{5}$', na=False)].copy() # 年度列の名前統一(先頭列が年度) year_col = df_raw.columns[0] # 'SSDSE-B-2026' df_raw[year_col] = pd.to_numeric(df_raw[year_col], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | # 2022年度のみ使用 df = df_raw[df_raw[year_col] == 2022].copy() print(f"2022年度データ: {len(df)}都道府県") # ── 数値変換 ──────────────────────────────────────────────────────── NUM_COLS = [ 'C5401', # 標準価格(住宅地, 千円/m²) 'C5403', # 標準価格(商業地, 千円/m²) 'A1101', # 総人口 'A1302', # 15〜64歳人口(生産年齢人口) 'F3102', # 有効求人数(新規) 'F3103', # 有効求職者数(新規) 'G7101', # 宿泊者数(延べ) 'E6302', # 大学学生数 'I510120', # 病院数 'B4101', # 年平均気温 'A4103', # 合計特殊出生率 'A5101', # 転入者数 ] for c in NUM_COLS: df[c] = pd.to_numeric(df[c], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。62 63 64 65 66 67 | # ── 派生変数の計算(実データのみ) ───────────────────────────────── # 生産年齢人口割合 (%) df['working_age_pct'] = df['A1302'] / df['A1101'] * 100 # 有効求人倍率 = 有効求人数 / 有効求職者数 df['job_ratio'] = df['F3103'] / df['F3102'] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。68 69 70 71 72 73 74 75 | # 宿泊者数 per capita df['tourism_pc'] = df['G7101'] / df['A1101'] # 大学学生率 (%) df['univ_rate'] = df['E6302'] / df['A1101'] * 100 # 病院密度(人口1万対) df['hospital_density'] = df['I510120'] / df['A1101'] * 10000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。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 102 | # 転入率 (%) df['inflow_rate'] = df['A5101'] / df['A1101'] * 100 # 目的変数・説明変数の列名 TARGET = 'C5401' # 住宅地標準価格(千円/m²) PRED_COLS = [ 'A1101', # 総人口(万人換算しない) 'working_age_pct', # 生産年齢人口割合 'job_ratio', # 有効求人倍率 'tourism_pc', # 宿泊者数per capita 'univ_rate', # 大学学生率 'hospital_density',# 病院密度 'B4101', # 年平均気温 'A4103', # 合計特殊出生率 'inflow_rate', # 転入率 ] PRED_LABELS = [ '総人口', '生産年齢人口割合', '有効求人倍率', '宿泊者数per capita', '大学学生率', '病院密度', '年平均気温', '合計特殊出生率', '転入率', ] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。103 104 105 106 107 108 109 110 111 | # ── 分析用データ整理 ──────────────────────────────────────────────── df_model = df[['Prefecture', TARGET] + PRED_COLS].dropna().copy() df_model = df_model.reset_index(drop=True) print(f"欠損除去後サンプル数: {len(df_model)}") # 東京都フラグ tokyo_mask = df_model['Prefecture'].str.contains('東京', na=False) df_no_tokyo = df_model[~tokyo_mask].copy().reset_index(drop=True) print(f"東京除外後サンプル数: {len(df_no_tokyo)}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。112 113 114 115 116 117 118 119 120 121 | # ── OLS 重回帰(statsmodels, 東京除外)──────────────────────────── print("\n=== OLS 重回帰(statsmodels, 東京除外)===") X_ols = sm.add_constant(df_no_tokyo[PRED_COLS].astype(float)) y_ols = df_no_tokyo[TARGET].astype(float) res_ols = sm.OLS(y_ols, X_ols).fit() print(res_ols.summary()) print(f"\nAIC = {res_ols.aic:.2f}") print(f"BIC = {res_ols.bic:.2f}") print(f"R² = {res_ols.rsquared:.4f}") print(f"Adj.R² = {res_ols.rsquared_adj:.4f}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。122 123 124 125 126 127 128 129 | # ── 標準化係数の計算 ─────────────────────────────────────────────── scaler = StandardScaler() X_scaled_nt = scaler.fit_transform(df_no_tokyo[PRED_COLS].astype(float)) y_nt = df_no_tokyo[TARGET].astype(float).values # 標準化 OLS lr = LinearRegression().fit(X_scaled_nt, y_nt) std_coefs = lr.coef_ |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。130 131 132 133 134 135 136 137 138 139 140 141 142 143 | # ── sklearn モデル比較(5分割 CV R²)── 東京除外 ────────────────── print("\n=== モデル比較(5分割 CV R²)===") cv = 5 # OLS(LinearRegression) cv_ols = cross_val_score(LinearRegression(), X_scaled_nt, y_nt, cv=cv, scoring='r2') # Ridge(α=1.0) cv_ridge = cross_val_score(Ridge(alpha=1.0), X_scaled_nt, y_nt, cv=cv, scoring='r2') # Lasso(α=0.1) cv_lasso = cross_val_score(Lasso(alpha=0.1, max_iter=10000), X_scaled_nt, y_nt, cv=cv, scoring='r2') print(f"OLS CV-R²: {cv_ols.mean():.4f} ± {cv_ols.std():.4f}") print(f"Ridge CV-R²: {cv_ridge.mean():.4f} ± {cv_ridge.std():.4f}") print(f"Lasso CV-R²: {cv_lasso.mean():.4f} ± {cv_lasso.std():.4f}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。144 145 146 147 148 149 | # Lasso 係数(変数選択確認) lasso_model = Lasso(alpha=0.1, max_iter=10000).fit(X_scaled_nt, y_nt) print("\nLasso 係数(ゼロ = 変数選択で除外):") for label, coef in zip(PRED_LABELS, lasso_model.coef_): mark = " ← 除外" if abs(coef) < 1e-6 else "" print(f" {label}: {coef:.4f}{mark}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。150 151 152 153 154 155 156 | # ── Figure 1: 人口 vs 住宅地価格(東京外れ値)──────────────────── print("\nFigure 1: 人口 vs 住宅地価格(対数軸)...") fig, ax = plt.subplots(figsize=(9, 6)) ax.scatter(df_no_tokyo['A1101'] / 1e6, df_no_tokyo[TARGET], color='#1565C0', alpha=0.75, s=60, zorder=3, label='各都道府県(東京除外)') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。157 158 159 160 161 162 163 164 165 166 167 168 | # 東京を別途プロット if tokyo_mask.any(): df_tokyo = df_model[tokyo_mask] ax.scatter(df_tokyo['A1101'] / 1e6, df_tokyo[TARGET], color='#C62828', s=120, zorder=4, marker='*', label='東京都(外れ値)') for _, row in df_tokyo.iterrows(): ax.annotate('東京都', xy=(row['A1101'] / 1e6, row[TARGET]), xytext=(8, -14), textcoords='offset points', fontsize=10, color='#C62828', fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | # 主要都市をラベル for _, row in df_no_tokyo.iterrows(): if row['A1101'] > 5e6 or row[TARGET] > 90000: ax.annotate(row['Prefecture'], xy=(row['A1101'] / 1e6, row[TARGET]), xytext=(4, 4), textcoords='offset points', fontsize=8, color='#333333') ax.set_xscale('log') ax.set_yscale('log') ax.set_xlabel('総人口(百万人, 対数軸)', fontsize=12) ax.set_ylabel('住宅地標準価格(千円/m², 対数軸)', fontsize=12) ax.set_title('総人口と住宅地標準価格の関係\n(2022年度、47都道府県)', fontsize=13, fontweight='bold') ax.legend(fontsize=10) ax.grid(True, alpha=0.3, which='both') plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2023_H3_fig1_scatter.png'), bbox_inches='tight') plt.close() |
=== SSDSE-B 読み込み ===
2022年度データ: 47都道府県
欠損除去後サンプル数: 47
東京除外後サンプル数: 46
=== OLS 重回帰(statsmodels, 東京除外)===
OLS Regression Results
==============================================================================
Dep. Variable: C5401 R-squared: 0.888
Model: OLS Adj. R-squared: 0.860
Method: Least Squares F-statistic: 31.59
Date: Mon, 18 May 2026 Prob (F-statistic): 1.77e-14
Time: 11:24:27 Log-Likelihood: -498.36
No. Observations: 46 AIC: 1017.
Df Residuals: 36 BIC: 1035.
Df Model: 9
Covariance Type: nonrobust
====================================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------------
const 1.718e+04 1.04e+05 0.166 0.869 -1.93e+05 2.27e+05
A1101 0.0108 0.002 6.652 0.000 0.007 0.014
working_age_pct -771.5408 1797.153 -0.429 0.670 -4416.336 2873.255
job_ratio -1.266e+04 1.25e+04 -1.010 0.319 -3.81e+04 1.28e+04
tourism_pc -1066.4449 1616.124 -0.660 0.514 -4344.096 2211.206
univ_rate 1.219e+04 3407.884 3.577 0.001 5278.430 1.91e+04
hospital_density -2.24e+04 1.2e+04 -1.864 0.070 -4.68e+04 1971.554
B4101 4248.2406 1586.709 2.677 0.011 1030.245 7466.236
A4103 -6405.8945 2.93e+04 -0.218 0.828 -6.59e+04 5.31e+04
inflow_rate 3475.0966 9535.154 0.364 0.718 -1.59e+04 2.28e+04
==============================================================================
Omnibus: 6.053 Durbin-Watson: 2.323
Prob(Omnibus): 0.048 Jarque-Bera (JB): 8.678
Skew: 0.121 Prob(JB): 0.0130
Kurtosis: 5.114 Cond. No. 1.67e+08
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.67e+08. This might indicate that there are
strong multicollinearity or other numerical problems.
AIC = 1016.71
BIC = 1035.00
R² = 0.8876
Adj.R² = 0.8595
=== モデル比較(5分割 CV R²)===
OLS CV-R²: -0.3679 ± 1.6510
Ridge CV-R²: -0.3113 ± 1.5894
Lasso CV-R²: -0.3679 ± 1.6510
Lasso 係数(ゼロ = 変数選択で除外):
総人口: 23888.5067
生産年齢人口割合: -1969.9524
有効求人倍率: -3127.5651
宿泊者数per capita: -1655.1395
大学学生率: 9642.8188
病院密度: -6092.5374
年平均気温: 9721.9320
合計特殊出生率: -906.9724
転入率: 1115.4108
Figure 1: 人口 vs 住宅地価格(対数軸)...for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。東京都を除外した46都道府県でOLS(最小二乗法)重回帰を実施。説明変数を標準化(平均0・分散1)することで、係数の大きさが「地価への影響度」の直接比較を可能にする(標準化回帰係数)。
| 指標 | 値 | 解釈 |
|---|---|---|
| R²(決定係数) | 0.888 | 住宅地価格の変動の88.8%を説明 |
| Adj.R²(自由度修正済) | 0.860 | 変数数を補正したモデル当てはまり |
| AIC | 1016.7 | モデル情報量基準(小さいほど良い) |
| BIC | 1035.0 | ベイズ情報量基準(変数数にペナルティ) |
| 変数 | 係数方向 | p値 | 解釈 |
|---|---|---|---|
| 総人口 | 正 (+) | p<0.001 *** | 人口集積が地価を強く押し上げる |
| 大学学生率 | 正 (+) | p<0.01 ** | 高等教育機関の集積が都市価値を上昇させる |
| 年平均気温 | 正 (+) | p<0.05 * | 温暖な気候はアメニティ価値として地価に反映 |
AIC(赤池情報量基準)とBIC(ベイズ情報量基準)はモデルの当てはまりの良さと複雑さのトレードオフを定量化する。変数を増やすと当てはまりは上がるが、AIC/BICのペナルティ項が大きくなる。BICはAICよりもペナルティが大きく、よりシンプルなモデルを選ぶ傾向がある。
190 191 192 193 194 195 196 197 198 | print("Figure 1 saved.") # ── Figure 2: OLS 標準化係数棒グラフ ───────────────────────────── print("Figure 2: OLS 標準化係数棒グラフ...") # |β| でソート sort_idx = np.argsort(np.abs(std_coefs))[::-1] sorted_labels = [PRED_LABELS[i] for i in sort_idx] sorted_coefs = std_coefs[sort_idx] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | # p値(元のOLSから対応する順番) pvals_ols = res_ols.pvalues[1:].values # 定数項を除く sorted_pvals = pvals_ols[sort_idx] colors_bar = ['#C62828' if c < 0 else '#1565C0' for c in sorted_coefs] sig_marks = ['***' if p < 0.001 else ('**' if p < 0.01 else ('*' if p < 0.05 else '')) for p in sorted_pvals] fig, ax = plt.subplots(figsize=(10, 6)) y_pos = np.arange(len(sorted_labels)) bars = ax.barh(y_pos, sorted_coefs, color=colors_bar, alpha=0.85, height=0.65) for bar, mark in zip(bars, sig_marks): x = bar.get_width() offset = 0.012 if x >= 0 else -0.012 ax.text(x + offset, bar.get_y() + bar.get_height() / 2, mark, va='center', ha='left' if x >= 0 else 'right', fontsize=11, color='#333333') ax.axvline(0, color='black', lw=1) ax.set_yticks(y_pos) ax.set_yticklabels([f'{l}' for l in sorted_labels], fontsize=11) ax.set_xlabel('標準化回帰係数(β)', fontsize=12) ax.set_title('住宅地価格モデル:OLS 標準化係数\n(東京除外、|β|降順、*** p<0.001, ** p<0.01, * p<0.05)', fontsize=12, fontweight='bold') neg_p = mpatches.Patch(color='#C62828', alpha=0.85, label='負の効果') pos_p = mpatches.Patch(color='#1565C0', alpha=0.85, label='正の効果') ax.legend(handles=[pos_p, neg_p], fontsize=10, loc='lower right') ax.grid(axis='x', alpha=0.3) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2023_H3_fig2_coef.png'), bbox_inches='tight') plt.close() |
Figure 1 saved. Figure 2: OLS 標準化係数棒グラフ...
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。OLS はサンプル数が小さい場合(本研究では n=46 に対して p=9 変数)に過学習しやすい。Ridge回帰とLasso回帰は、係数の大きさに罰則(正則化項)を加えることで過学習を抑制する。
Ridge(L2)は全係数を「均等に縮小」するが、ゼロにはしない。Lasso(L1)は一部の係数を完全にゼロにする「変数選択」機能を持つ。多重共線性が強い場合はRidge、変数選択が必要な場合はLassoが有利。Elastic NetはL1とL2の両方を組み合わせる。
234 235 236 237 238 239 240 241 242 243 244 | print("Figure 2 saved.") # ── Figure 3: OLS vs Ridge vs Lasso CV-R² 比較 ─────────────────── print("Figure 3: モデル比較(CV-R²)...") model_names = ['OLS\n(最小二乗法)', 'Ridge\n(α=1.0)', 'Lasso\n(α=0.1)'] cv_means = [cv_ols.mean(), cv_ridge.mean(), cv_lasso.mean()] cv_stds = [cv_ols.std(), cv_ridge.std(), cv_lasso.std()] bar_colors_m = ['#1565C0', '#2E7D32', '#E65100'] fig, ax = plt.subplots(figsize=(9, 6)) x_pos = np.arange(len(model_names)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | # プロット:バーは 0 基準、正・負どちらも表示 bars_m = ax.bar(x_pos, cv_means, yerr=cv_stds, capsize=8, color=bar_colors_m, alpha=0.85, width=0.5, error_kw={'elinewidth': 2, 'ecolor': '#333333'}) for i, (mean_val, std_val) in enumerate(zip(cv_means, cv_stds)): # ラベルを棒の外側に配置 y_label = mean_val + std_val + 0.05 if mean_val >= 0 else mean_val - std_val - 0.12 va = 'bottom' if mean_val >= 0 else 'top' ax.text(i, y_label, f'R²={mean_val:.3f}', ha='center', va=va, fontsize=11, fontweight='bold') ax.set_xticks(x_pos) ax.set_xticklabels(model_names, fontsize=12) ax.set_ylabel('5分割交差検証 R²(平均 ± 標準偏差)', fontsize=11) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。261 262 263 264 265 266 267 268 269 270 271 272 | # y軸範囲:負のR²も見えるように y_min = min(cv_means) - max(cv_stds) - 0.3 y_max = max(cv_means) + max(cv_stds) + 0.3 ax.set_ylim(max(-2.5, y_min), min(1.15, y_max)) ax.set_title('住宅地価格モデル:OLS vs Ridge vs Lasso\n(5分割 CV R²、東京除外、n=46)', fontsize=12, fontweight='bold') ax.axhline(0, color='black', lw=1.5, ls='--', label='R²=0(ベースライン)') ax.axhline(res_ols.rsquared, color='#888888', lw=1.2, ls=':', label=f'訓練データ R²={res_ols.rsquared:.3f}') ax.legend(fontsize=10) ax.grid(axis='y', alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。273 274 275 276 277 278 279 280 281 282 283 | # 注釈:過学習の説明 note = ('負のCV-R²はモデルの過学習を示す。\n' 'n=46に対して9変数は多すぎる。\n' 'Ridge/Lassoは正則化でやや改善。') ax.text(0.97, 0.03, note, transform=ax.transAxes, fontsize=9, color='#555555', ha='right', va='bottom', bbox=dict(boxstyle='round', facecolor='#FFF9C4', alpha=0.85)) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2023_H3_fig3_cv.png'), bbox_inches='tight') plt.close() |
Figure 2 saved. Figure 3: モデル比較(CV-R²)...
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。3つのモデルの汎化性能(未知のデータへの予測精度)を5分割交差検証(5-fold cross-validation)によって比較する。
| モデル | 訓練R² | CV-R²(平均) | AIC | 評価 |
|---|---|---|---|---|
| OLS(全変数) | 0.888 | 負(過学習) | 1016.7 | 過学習:訓練データのみ高精度 |
| Ridge(α=1.0) | ― | 負(わずかに改善) | ― | 正則化の効果はあるが限定的 |
| Lasso(α=0.1) | ― | 負(OLSと同程度) | ― | α=0.1では変数選択が不十分 |
| 推奨:OLS(有意変数3つ) | ― | 要検証 | ― | p値有意な3変数のみ使用が妥当 |
k分割交差検証はデータをk個のブロック(Fold)に分割し、1つをテスト用・残りを訓練用として順番に評価する。k回の評価の平均がモデルの汎化性能の推定値となる。n=46のような小標本では k=5 または k=10 が一般的。
285 286 287 288 289 290 291 292 293 294 | print("Figure 3 saved.") # ── Figure 4: 予測値 vs 実測値(OLS, 東京除外)───────────────── print("Figure 4: 予測値 vs 実測値...") y_pred_ols = res_ols.fittedvalues.values y_actual = y_ols.values fig, ax = plt.subplots(figsize=(8, 7)) ax.scatter(y_actual, y_pred_ols, color='#1565C0', alpha=0.75, s=70, zorder=3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。295 296 297 298 299 300 | # 45度線 lims = [min(y_actual.min(), y_pred_ols.min()) * 0.9, max(y_actual.max(), y_pred_ols.max()) * 1.1] ax.plot(lims, lims, 'k--', lw=1.5, label='完全予測(y=ŷ)', alpha=0.6) ax.set_xlim(lims) ax.set_ylim(lims) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。301 302 303 304 305 306 307 308 | # 主要都市ラベル for i, row in df_no_tokyo.iterrows(): if row[TARGET] > 80000 or y_pred_ols[i] > 80000 or \ abs(y_actual[i] - y_pred_ols[i]) > 20000: ax.annotate(row['Prefecture'], xy=(y_actual[i], y_pred_ols[i]), xytext=(5, 5), textcoords='offset points', fontsize=8, color='#555555') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | # RMSE / R² rmse = np.sqrt(np.mean((y_actual - y_pred_ols) ** 2)) r2 = res_ols.rsquared ax.text(0.05, 0.92, f'R² = {r2:.4f}\nRMSE = {rmse:,.0f} 千円/m²\nAIC = {res_ols.aic:.1f}', transform=ax.transAxes, fontsize=11, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='#EFF3FF', alpha=0.85)) ax.set_xlabel('実測値(住宅地標準価格, 千円/m²)', fontsize=12) ax.set_ylabel('OLS 予測値(千円/m²)', fontsize=12) ax.set_title('OLS モデルの予測精度\n(東京除外、2022年度 46都道府県)', fontsize=12, fontweight='bold') ax.legend(fontsize=10) ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2023_H3_fig4_pred.png'), bbox_inches='tight') plt.close() |
Figure 3 saved. Figure 4: 予測値 vs 実測値...
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | print("Figure 4 saved.") # ── 結果サマリー出力 ────────────────────────────────────────────── print("\n" + "=" * 60) print("分析完了:地価最適モデルの構築と手法提案") print("=" * 60) print(f" サンプル数: {len(df_no_tokyo)}都道府県(東京除外)") print(f" 目的変数: 住宅地標準価格(C5401, 千円/m²)") print() print(f"【OLS モデル性能】") print(f" R² = {res_ols.rsquared:.4f}, Adj.R² = {res_ols.rsquared_adj:.4f}") print(f" AIC = {res_ols.aic:.2f}, BIC = {res_ols.bic:.2f}") print() print(f"【5分割 CV R²】") print(f" OLS: {cv_ols.mean():.4f} ± {cv_ols.std():.4f}") print(f" Ridge: {cv_ridge.mean():.4f} ± {cv_ridge.std():.4f}") print(f" Lasso: {cv_lasso.mean():.4f} ± {cv_lasso.std():.4f}") print() print("【出力ファイル】") print(" html/figures/2023_H3_fig1_scatter.png - 人口 vs 住宅地価格") print(" html/figures/2023_H3_fig2_coef.png - OLS 標準化係数") print(" html/figures/2023_H3_fig3_cv.png - モデル比較(CV-R²)") print(" html/figures/2023_H3_fig4_pred.png - 予測値 vs 実測値") |
Figure 4 saved. ============================================================ 分析完了:地価最適モデルの構築と手法提案 ============================================================ サンプル数: 46都道府県(東京除外) 目的変数: 住宅地標準価格(C5401, 千円/m²) 【OLS モデル性能】 R² = 0.8876, Adj.R² = 0.8595 AIC = 1016.71, BIC = 1035.00 【5分割 CV R²】 OLS: -0.3679 ± 1.6510 Ridge: -0.3113 ± 1.5894 Lasso: -0.3679 ± 1.6510 【出力ファイル】 html/figures/2023_H3_fig1_scatter.png - 人口 vs 住宅地価格 html/figures/2023_H3_fig2_coef.png - OLS 標準化係数 html/figures/2023_H3_fig3_cv.png - モデル比較(CV-R²) html/figures/2023_H3_fig4_pred.png - 予測値 vs 実測値
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。原論文の最大の特徴は、地価をそのまま多数の変数で回帰するのではなく、まず因子分析で「地価の高い地域に共通する因子」を抽出し、その因子で地価を説明するという 2 段構えのフローです。多数の相関し合う変数を少数の「潜在因子」に縮約することで、多重共線性を避けつつ「何が地価を決めるのか」を解釈しやすくしています。
SSDSE-B-2026(2022年度・47都道府県)の住宅地標準価格を目的変数に、都市の性格を表す 8 変数を因子分析(2因子・バリマックス回転)で縮約し、得られた因子得点で log(地価) を回帰します。
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 | import numpy as np import pandas as pd import statsmodels.api as sm from sklearn.decomposition import FactorAnalysis 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('都道府県') e_raw = pd.read_csv('data/raw/SSDSE-E-2026.csv', encoding='cp932', header=2) e = e_raw[e_raw['地域コード'].str.match(r'^R\d{5}$', na=False)].set_index('都道府県') # 目的変数: 住宅地の標準価格(円/m2) price = pd.to_numeric(b['標準価格(平均価格)(住宅地)'], errors='coerce') # 因子分析にかける観測変数(都市の性格を表す 8 変数) X = pd.DataFrame(index=b.index) X['人口密度'] = b['総人口'] / pd.to_numeric(e['可住地面積'], errors='coerce') X['転入率'] = b['転入者数(日本人移動者)'] / b['総人口'] * 100 X['大学学生数割合'] = b['大学学生数'] / b['総人口'] * 100 X['求人倍率'] = pd.to_numeric(b['月間有効求人数(一般)'], errors='coerce') / pd.to_numeric(b['月間有効求職者数(一般)'], errors='coerce') X['宿泊者数per千人'] = pd.to_numeric(b['延べ宿泊者数'], errors='coerce') / b['総人口'] * 1000 X['高齢化率'] = b['65歳以上人口'] / b['総人口'] * 100 X['出生率'] = pd.to_numeric(b['合計特殊出生率'], errors='coerce') X['診療所密度'] = b['一般診療所数'] / b['総人口'] * 10000 Xz = (X - X.mean()) / X.std() # 標準化(因子分析の前提) # 因子分析(2因子・バリマックス回転) fa = FactorAnalysis(n_components=2, rotation='varimax', random_state=0) scores = fa.fit_transform(Xz.values) loadings = pd.DataFrame(fa.components_.T, index=X.columns, columns=['因子1', '因子2']) print('=== 因子負荷量(バリマックス回転後) ===') print(loadings.round(3)) # 因子得点で log(地価) を回帰(原論文のフロー: 因子分析 → 重回帰) sc = pd.DataFrame(scores, index=X.index, columns=['因子1', '因子2']) model = sm.OLS(np.log(price), sm.add_constant(sc)).fit() print() print('=== log(住宅地価格) を因子得点で回帰 ===') print(f'R2 = {model.rsquared:.3f}') for c in ['因子1', '因子2']: print(f' {c}: 係数 {model.params[c]:+.3f} (p={model.pvalues[c]:.2e})') print() print('因子1の得点 上位5県:', '、'.join(sc['因子1'].nlargest(5).index)) print('因子2の得点 上位5県:', '、'.join(sc['因子2'].nlargest(5).index)) |
FactorAnalysis(n_components=2, rotation='varimax') で回転付き因子分析ができます。負荷量は fa.components_.T、因子得点は fit_transform の戻り値。SSDSE-B(47都道府県、2022年度)の実データを用いた地価モデル分析の結果:
| データ・コード | 出典・説明 |
|---|---|
| SSDSE-B-2026.csv(都道府県別統計) | 統計センター SSDSE(社会・人口統計体系データセット) |
| 目的変数: C5401(住宅地標準価格) | 国土交通省 地価公示データ(SSDSE-B 収録) |
| 分析手法: OLS, Ridge, Lasso, CV | statsmodels / scikit-learn を使用 |
実公的データ(SSDSE-B-2026.csv)のみ使用。合成データ・乱数(np.random)は一切使用しない。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
この論文を理解できたか、クリックで確認しましょう。間違えても解説が出ます。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2023_H3_suri.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。