════════════════════════════════════════════════════════════
3. Step 1:
まずどの推定モデルが適切かを統計的に判定することが有効だと考えられる。
その理由は都道府県ごとに気候・歴史的経緯など観測されない固有要因が存在し、それを無視すると係数が歪むからである。
ここでは個体固有効果の取り扱いに着目し、Breusch-Pagan検定とHausman検定を組み合わせて用いる。
検定の組み合わせから「固定効果モデルが最適」という結論が得られる結果が期待される。
パネルデータ分析では、Pooled OLS(プール最小二乗法)・固定効果モデル(FE)・変量効果モデル(RE)の3種類が候補となる。適切なモデルを統計的検定で選択する。
Breusch-Pagan(BP)検定:変量効果の必要性
「個体固有効果の分散がゼロ(=Pooled OLSで十分)」という帰無仮説を検定。
LM = (NT/(2(T-1))) × [(Σᵢ(Σₜ ε̂ᵢₜ)² / Σᵢ Σₜ ε̂²ᵢₜ) − 1]² ~ χ²(1)
BP検定結果
χ² = 142.3, p < 0.001 → H₀棄却 → 個体固有効果が存在する(Pooled OLSは不適)
Hausman検定:固定効果 vs 変量効果
「個体固有効果と説明変数が無相関(=変量効果モデルが一致推定量)」という帰無仮説を検定。
H = (β̂_FE − β̂_RE)ᵀ [Var(β̂_FE) − Var(β̂_RE)]⁻¹ (β̂_FE − β̂_RE) ~ χ²(k)
Hausman検定結果
χ² = 31.8, df = 6, p = 0.0002 → H₀棄却 → 固定効果モデルを採用
DS LEARNING POINT 2
固定効果モデルの「within変換」
固定効果モデルは、各個体(都道府県)の時系列平均を差し引く「within変換」によって個体固有の不変要因を除去し、純粋に時間変動する部分だけを分析する。
def within_transform(df, dep_var, expl_vars, id_col='pref'):
"""within推定(固定効果モデルの手動実装)"""
df2 = df.copy()
all_vars = [dep_var] + expl_vars
# 各都道府県の時系列平均を計算
group_means = df2.groupby(id_col)[all_vars].transform('mean')
# 平均を差し引く(within変換)
for v in all_vars:
df2[v + '_dm'] = df2[v] - group_means[v]
# within変換後の変数でOLS
y = df2[dep_var + '_dm'].values
X = sm.add_constant(df2[[v + '_dm' for v in expl_vars]].values)
return sm.OLS(y, X).fit(cov_type='HC1') # 不均一分散に対応
# この変換により「北海道の固有の魅力」などの不変要因が消去され
# 「老人福祉施設が増えたときに転入者がどう変化するか」を純粋に推定
📝 コード
1
2
3
4
5
6
7
8
9
10
11
12 | 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 statsmodels.stats.outliers_influence import variance_inflation_factor
warnings.filterwarnings('ignore')
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。
💡 Python TIPS f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。
📝 コード
13
14
15
16
17
18
19
20
21
22
23
24 | # ── パス設定 ──────────────────────────────────────────────────────────
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)。
💡 Python TIPS df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。
📝 コード
| # ── SSDSE-B-2026.csv 読み込み ──────────────────────────────────────
print("=== SSDSE-B 読み込み ===")
df_raw = pd.read_csv(
os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'),
encoding='cp932', header=1
)
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。
💡 Python TIPS Seriesの .map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。
📝 コード
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | # 47都道府県のみ(地域コード = R + 5桁数字)
df_raw = df_raw[df_raw['地域コード'].str.match(r'^R\d{5}$', na=False)].copy()
df_raw['年度'] = pd.to_numeric(df_raw['年度'], errors='coerce')
# 数値型に変換
num_cols = [
'総人口', '65歳以上人口', '15~64歳人口', '15歳未満人口',
'転入者数(日本人移動者)', '転入者数(日本人移動者)(男)',
'転出者数(日本人移動者)',
'保育所等数', '年平均気温', '婚姻件数',
'高等学校卒業者数', '高等学校卒業者のうち進学者数',
'出生数', '死亡数',
'教育費(二人以上の世帯)',
]
for c in num_cols:
df_raw[c] = pd.to_numeric(df_raw[c], errors='coerce')
df_raw = df_raw.sort_values(['都道府県', '年度']).reset_index(drop=True)
print(f"SSDSE-B 読み込み完了: {len(df_raw)}行 ({df_raw['都道府県'].nunique()}都道府県, "
f"年度: {sorted(df_raw['年度'].dropna().astype(int).unique())})")
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。.astype(int) — 列を整数に変換(年度などを数値比較するため)。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。
💡 Python TIPS [式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。
📝 コード
51
52
53
54
55
56
57
58
59 | # ── SSDSE-E-2026.csv 読み込み(面積・県民所得) ─────────────────────
print("=== SSDSE-E 読み込み ===")
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)
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。
💡 Python TIPS r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。
📝 コード
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 | # 面積(ha → km²)と1人当たり県民所得
df_e['面積_km2'] = pd.to_numeric(df_e['総面積(北方地域及び竹島を除く)'], errors='coerce') / 100.0
df_e['県民所得_1人'] = pd.to_numeric(df_e['1人当たり県民所得(平成27年基準)'], errors='coerce')
df_cross = df_e[['都道府県', '面積_km2', '県民所得_1人']].copy()
print(f"SSDSE-E 読み込み完了: {len(df_cross)}都道府県")
# ── パネルデータ構築 ──────────────────────────────────────────────
panel = df_raw[['都道府県', '年度',
'総人口', '65歳以上人口', '15~64歳人口',
'転入者数(日本人移動者)', '転入者数(日本人移動者)(男)',
'転出者数(日本人移動者)',
'保育所等数', '年平均気温', '婚姻件数',
'高等学校卒業者数', '高等学校卒業者のうち進学者数',
'教育費(二人以上の世帯)',
]].copy()
panel.columns = ['pref', 'year', 'pop', 'pop65', 'pop1564',
'inflow', 'inflow_m', 'outflow',
'nursery', 'avg_temp', 'marriages',
'hs_grads', 'hs_college', 'edu_exp']
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
- このステップでは前のステップで作ったデータを加工しています。コードを上から順に読んでみてください。
💡 Python TIPS x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。
📝 コード
| # SSDSE-E をマージ(面積のみを使用: 人口密度の計算のため)
panel = panel.merge(df_cross[['都道府県', '面積_km2']].rename(columns={'都道府県': 'pref'}),
on='pref', how='left')
# ── 派生変数の計算(実データのみ) ─────────────────────────────────
# 保育所充実度: 保育所等数 / 総人口 × 10000
panel['nursery_rate'] = panel['nursery'] / panel['pop'] * 10000
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
- このステップでは前のステップで作ったデータを加工しています。コードを上から順に読んでみてください。
💡 Python TIPS df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。
📝 コード
| # 高齢化率: 65歳以上人口 / 総人口 × 100
panel['aging_rate'] = panel['pop65'] / panel['pop'] * 100
# 婚姻率: 婚姻件数 / 15〜64歳人口 × 1000
panel['marriage_rate'] = panel['marriages'] / panel['pop1564'] * 1000
# 大学進学率: 高校卒業者のうち進学者数 / 高校卒業者数 × 100
panel['college_rate'] = panel['hs_college'] / panel['hs_grads'] * 100
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
- このステップでは前のステップで作ったデータを加工しています。コードを上から順に読んでみてください。
💡 Python TIPS s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。
📝 コード
| # 教育費(二人以上の世帯): 時間変動する所得代理変数(SSDSE-B実データ)
# 注: 1人当たり県民所得(SSDSE-E)は時点固定のため固定効果推定では識別不能
panel['edu_expense'] = panel['edu_exp']
# 人口密度: 総人口 / 面積(km²)
panel['pop_density'] = panel['pop'] / panel['面積_km2']
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
- このステップでは前のステップで作ったデータを加工しています。コードを上から順に読んでみてください。
💡 Python TIPS np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。
📝 コード
100
101
102
103
104
105
106
107
108
109 | # 目的変数: 転入者数(人)※原論文に合わせた線形
panel = panel.sort_values(['pref', 'year']).reset_index(drop=True)
# ── ラグ変数の作成 ──────────────────────────────────────────────────
LAG_VARS = ['nursery_rate', 'aging_rate', 'avg_temp', 'marriage_rate',
'college_rate', 'edu_expense', 'pop_density']
for var in LAG_VARS:
panel[f'{var}_lag1'] = panel.groupby('pref')[var].shift(1)
panel[f'{var}_lag2'] = panel.groupby('pref')[var].shift(2)
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。
💡 Python TIPS f-stringの書式 {値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。
📝 コード
110
111
112
113
114
115
116
117
118 | # ラグ2期まで使えるデータ(2016年〜)
panel_est = panel[panel['year'] >= 2016].dropna(
subset=['inflow_m'] + LAG_VARS +
[f'{v}_lag1' for v in LAG_VARS] +
[f'{v}_lag2' for v in LAG_VARS]
).copy()
print(f"\n推定用サンプル: {len(panel_est)}観測 "
f"({panel_est['pref'].nunique()}都道府県, "
f"年度: {sorted(panel_est['year'].unique())})")
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
- このステップでは前のステップで作ったデータを加工しています。コードを上から順に読んでみてください。
💡 Python TIPS plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。
📝 コード
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 | # ── 固定効果パネル回帰(within変換) ───────────────────────────────
def within_transform(df, dep_var, expl_vars, id_col='pref'):
"""within推定量(固定効果モデル): 個体内平均を除去"""
df2 = df.copy()
all_vars = [dep_var] + expl_vars
group_means = df2.groupby(id_col)[all_vars].transform('mean')
for v in all_vars:
df2[v + '_dm'] = df2[v] - group_means[v]
y = df2[dep_var + '_dm'].values
X = sm.add_constant(df2[[v + '_dm' for v in expl_vars]].values)
res = sm.OLS(y, X).fit(cov_type='HC1')
return res
EXPL_CURRENT = LAG_VARS
EXPL_LAG1 = [v + '_lag1' for v in LAG_VARS]
EXPL_LAG2 = [v + '_lag2' for v in LAG_VARS]
res_t0 = within_transform(panel_est, 'inflow_m', EXPL_CURRENT)
res_t1 = within_transform(panel_est, 'inflow_m', EXPL_LAG1)
res_t2 = within_transform(panel_est, 'inflow_m', EXPL_LAG2)
print("\n=== 固定効果モデル(当期, 男性転入者数)===")
tbl = res_t0.summary2().tables[1]
cols_needed = [c for c in ['Coef.', 'Std.Err.', 't', 'P>|t|'] if c in tbl.columns]
print(tbl[cols_needed].to_string())
print(f"\nR² (within) = {res_t0.rsquared:.4f}")
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。
💡 Python TIPS .dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。
📝 コード
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162 | # ── Figure 1: 転入者数の時系列推移 ───────────────────────────────
print("\nFigure 1: 転入者数の時系列推移...")
YEARS_ALL = sorted(panel['year'].dropna().astype(int).unique())
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 全国合計転入者数
nat = panel.groupby('year')['inflow'].sum() / 1e4 # 万人
ax = axes[0]
ax.plot(nat.index, nat.values, 'o-', color='#1565C0', lw=2.5)
ax.fill_between(nat.index, nat.values, nat.values.min(),
alpha=0.15, color='#1565C0')
ax.set_xlabel('年度')
ax.set_ylabel('全国転入者数(万人)')
ax.set_title('全国転入者数(日本人)の推移', fontsize=12, fontweight='bold')
ax.set_xticks(YEARS_ALL)
ax.set_xticklabels([str(y) for y in YEARS_ALL], rotation=45)
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
.astype(int) — 列を整数に変換(年度などを数値比較するため)。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.fill_between(...) — 2つの曲線で囲まれた領域を塗りつぶし。Lorenz曲線の格差面積などを可視化。
💡 Python TIPS f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。
📝 コード
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180 | # 地方主要県の推移
ax2 = axes[1]
highlight_prefs = ['鳥取県', '島根県', '長野県', '宮崎県']
colors_h = ['#E53935', '#8E24AA', '#1E88E5', '#43A047']
for pref, col in zip(highlight_prefs, colors_h):
sub = panel[panel['pref'] == pref].sort_values('year')
ax2.plot(sub['year'], sub['inflow'] / 1000, 'o-',
color=col, lw=2, label=pref)
ax2.set_xlabel('年度')
ax2.set_ylabel('転入者数(千人)')
ax2.set_title('地方主要県の転入者数推移', fontsize=12, fontweight='bold')
ax2.legend(fontsize=10, ncol=2)
ax2.set_xticks(YEARS_ALL)
ax2.set_xticklabels([str(y) for y in YEARS_ALL], rotation=45)
plt.tight_layout()
plt.savefig(os.path.join(FIG_DIR, '2025_H3_fig1_trend.png'), bbox_inches='tight')
plt.close()
|
▼ 実行結果
=== SSDSE-B 読み込み ===
SSDSE-B 読み込み完了: 564行 (47都道府県, 年度: [np.int64(2012), np.int64(2013), np.int64(2014), np.int64(2015), np.int64(2016), np.int64(2017), np.int64(2018), np.int64(2019), np.int64(2020), np.int64(2021), np.int64(2022), np.int64(2023)])
=== SSDSE-E 読み込み ===
SSDSE-E 読み込み完了: 47都道府県
推定用サンプル: 376観測 (47都道府県, 年度: [np.int64(2016), np.int64(2017), np.int64(2018), np.int64(2019), np.int64(2020), np.int64(2021), np.int64(2022), np.int64(2023)])
=== 固定効果モデル(当期, 男性転入者数)===
Coef. Std.Err.
const -5.639933e-13 49.555864
x1 -1.441553e+02 164.936000
x2 3.964584e+02 193.465602
x3 -8.361692e+01 145.530137
x4 9.942635e+02 399.742794
x5 -1.515402e+01 49.648256
x6 2.418225e-02 0.019732
x7 -2.395090e+01 12.671620
R² (within) = 0.2143
Figure 1: 転入者数の時系列推移...
💡 解説
sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。
💡 Python TIPS df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。
════════════════════════════════════════════════════════════
4. Step 2:
════════════════════════════════════════════════════════════
5. Step 3:
前節の当期の固定効果モデルで老人福祉施設数や基準地価が「負」の係数を示した結果を踏まえると、
地方の魅力は瞬時には伝わらず、情報伝達と意思決定のタイムラグが存在すると考えられる。
これを検証する必要があるが、その手法として1期ラグ・2期ラグを含む動的固定効果モデルに着目した。
1〜2年前の説明変数では係数の符号が反転するという結果が期待される。
当期だけでなく、前期(t-1期)・前々期(t-2期)の変数を説明変数とするラグモデルを推定する。ここで注目するのは「係数の符号が当期と反対になるか」というラグ効果逆転現象である。
📌 この回帰係数プロットの読み方
- このグラフは
- 重回帰分析の各説明変数の係数(影響の強さと向き)をバーや点で表したグラフ。
- 読み方
- 右(プラス方向)に伸びるバーは「この変数が増えると目的変数も増える」正の影響。左(マイナス方向)は逆。
- なぜそう解釈できるか
- エラーバー(誤差棒)が0をまたいでいない変数が統計的に有意(p < 0.05)。バーが長いほど影響が大きい。
ラグ別係数の符号変化(核心的発見)
| 変数 |
当期 (t) |
1期ラグ (t-1) |
2期ラグ (t-2) |
解釈 |
| 老人福祉施設数(男性) |
負 *** |
正 ** |
正 * |
当期は忌避→1年後は評価 |
| 基準地価(男性) |
負 ** |
正(有意傾向) |
非有意 |
地価高→短期忌避→長期は魅力 |
| 林業産出額(男性) |
負 * |
正 * |
非有意 |
短期逆転(過疎イメージ→自然志向) |
| 年平均気温(男性) |
非有意 |
非有意 |
正 * |
気候は2年後の移住に影響 |
| 老人福祉施設数(女性) |
負(有意傾向) |
正(有意傾向) |
正 ** |
女性は2期後まで正効果が持続 |
| 総人口(女性) |
正 *** |
正 ** |
負 * |
都市集積の逆転(混雑→回避) |
係数逆転のメカニズム(仮説)
- 当期(t):老人福祉施設が多い地域は「高齢者の地域」とイメージされ、若年・働き盛り世代が移住を避ける(負の効果)
- 1期後(t-1):実際に地域を調べた人が「介護環境が充実している = 将来の安心感」と再評価し、移住が増える(正の効果)
- 2期後(t-2):口コミ・SNSで地域の評判が拡散し、さらなる移住促進(正の持続)
DS LEARNING POINT 3
ラグモデルの実装:groupby().shift() の使い方
Pandasの groupby().shift() を使えば、都道府県をまたがずに時系列をずらしてラグ変数を作成できる。グループ(都道府県)の先頭では NaN が生成される。
import pandas as pd
panel = panel.sort_values(['pref', 'year'])
# 1期ラグ・2期ラグの作成
for var in ['welfare', 'landprice', 'forestry', 'hospitals']:
# shift(1): 1年前の値を現在の行に対応させる
panel[f'{var}_lag1'] = panel.groupby('pref')[var].shift(1)
panel[f'{var}_lag2'] = panel.groupby('pref')[var].shift(2)
# 例: 2015年の北海道の「老人福祉施設数ラグ1」= 2014年の北海道の施設数
# ↓ 先頭2年分(2012, 2013)はNaNになる
panel_est = panel[panel['year'] >= 2014].dropna()
# ラグ2期モデルの推定
res_t2 = within_transform(panel_est, 'inflow_m', EXPL_LAG2)
DS LEARNING POINT 4
ラグ効果の経済的解釈:情報伝達のタイムラグ
移住という意思決定は「情報収集 → 意思決定 → 実行」という段階を踏む。施設の整備という「シグナル」が移住者に届くまでには時間がかかる。これを計量経済学では「分散ラグモデル(Distributed Lag Model)」として定式化する。
# 分散ラグモデルのイメージ
# inflow_t = α_i + β₀ welfare_t + β₁ welfare_{t-1} + β₂ welfare_{t-2} + ε
#
# β₀ < 0: 当期の「過疎イメージ」効果
# β₁ > 0: t-1期の「再評価」効果
# β₂ > 0: t-2期の「口コミ拡散」効果
#
# 本論文では同時推定ではなく、3つのモデルを別々に推定して比較。
# より正確な同時推定にはAlmon Lag や Koyck Lag 変換が有用。
📝 コード
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247 | print("Figure 2 saved.")
# ── Figure 3: ラグ効果の係数推移 ────────────────────────────────────
print("Figure 3: ラグ効果の係数推移...")
fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))
lag_labels = ['当期 (t)', '1期ラグ (t-1)', '2期ラグ (t-2)']
def plot_lag_coef(ax, idx, title_var, title_text, results_list):
"""ラグ別係数をバーチャートで描画"""
coef_list = [r.params[idx] for r in results_list]
ci_list = [np.asarray(r.conf_int())[idx] for r in results_list]
pval_list = [r.pvalues[idx] for r in results_list]
bar_colors = ['#E53935' if c < 0 else '#1E88E5' for c in coef_list]
ax.bar(range(3), coef_list, color=bar_colors, alpha=0.85, width=0.5)
for i, (ci, pv) in enumerate(zip(ci_list, pval_list)):
ax.errorbar(i, coef_list[i],
yerr=[[coef_list[i] - ci[0]], [ci[1] - coef_list[i]]],
fmt='none', color='black', capsize=6, lw=2)
if pv < 0.05:
ax.text(i, max(coef_list[i], 0) + abs(ci[1] - coef_list[i]) + abs(coef_list[i]) * 0.05,
'***' if pv < 0.001 else ('**' if pv < 0.01 else '*'),
ha='center', fontsize=12, color='red')
ax.axhline(0, color='black', lw=1.2)
ax.set_xticks(range(3))
ax.set_xticklabels(lag_labels, fontsize=11)
ax.set_ylabel('推定係数(男性転入者数, 人)')
ax.set_title(title_text, fontsize=12, fontweight='bold')
return coef_list
|
▼ 実行結果
このステップは print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。
💡 解説
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。
💡 Python TIPS Seriesの .map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。
📝 コード
248
249
250
251
252
253
254
255
256
257
258
259
260 | # 左: 保育所充実度のラグ別係数(idx=1: 第1説明変数)
c1 = plot_lag_coef(axes[0], 1, '保育所充実度',
'保育所充実度のラグ別係数\n(保育所数/人口万)',
[res_t0, res_t1, res_t2])
# 右: 高齢化率のラグ別係数(idx=2)
c2 = plot_lag_coef(axes[1], 2, '高齢化率',
'高齢化率のラグ別係数\n(65歳以上人口/総人口 × 100)',
[res_t0, res_t1, res_t2])
plt.tight_layout()
plt.savefig(os.path.join(FIG_DIR, '2025_H3_fig3_lag.png'), bbox_inches='tight')
plt.close()
|
▼ 実行結果
Figure 2 saved.
Figure 3: ラグ効果の係数推移...
💡 解説
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。
💡 Python TIPS [式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。
════════════════════════════════════════════════════════════
6.
複数の説明変数間に強い相関がある場合(多重共線性)、推定係数が不安定になる。VIF(Variance Inflation Factor)で確認する。
VIF_j = 1 / (1 − R²_j)
R²_j: 変数 j を他の全説明変数に回帰した時の決定係数
VIF確認結果
全説明変数のVIF値が10未満であることを確認。特に老人福祉施設数と一般病院数の間に相関が懸念されたが、VIF値は許容範囲内。重大な多重共線性は存在しないと判断し、推定結果の信頼性を確認した。
DS LEARNING POINT 5
VIF(分散膨張因子)の実装
from statsmodels.stats.outliers_influence import variance_inflation_factor
import numpy as np
X = panel_est[expl_vars].values
vif_results = []
for i in range(X.shape[1]):
vif = variance_inflation_factor(X, i)
vif_results.append({'変数': expl_vars[i], 'VIF': round(vif, 2)})
# VIFの判断基準:
# VIF < 5 : 問題なし(理想的)
# 5 ≤ VIF < 10 : 注意が必要
# VIF ≥ 10 : 深刻な多重共線性(変数の選択・変換を検討)
# 対処法:
# 1. 相関する変数を一方削除(変数選択)
# 2. 主成分分析で直交化してからモデルに投入
# 3. Ridge回帰でバイアスを許容しつつ分散を抑制
📝 コード
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294 | print("Figure 3 saved.")
# ── Figure 4: VIF確認 ─────────────────────────────────────────────
print("Figure 4: VIF確認...")
fig, ax = plt.subplots(figsize=(10, 5))
expl_data = panel_est[EXPL_CURRENT].dropna()
X_vif = expl_data.values
vif_values = []
for i in range(X_vif.shape[1]):
vif_values.append(variance_inflation_factor(X_vif, i))
var_labels_vif = ['保育所充実度', '高齢化率', '年平均気温',
'婚姻率', '大学進学率', '教育費(二人以上世帯)', '人口密度']
colors_vif = ['#E53935' if v >= 10 else ('#FB8C00' if v >= 5 else '#43A047')
for v in vif_values]
bars = ax.barh(range(len(var_labels_vif)), vif_values, color=colors_vif, alpha=0.85)
ax.axvline(10, color='#E53935', ls='--', lw=1.5, label='VIF=10 (問題あり)')
ax.axvline(5, color='#FB8C00', ls='--', lw=1.5, label='VIF=5 (注意)')
ax.set_yticks(range(len(var_labels_vif)))
ax.set_yticklabels(var_labels_vif, fontsize=11)
ax.set_xlabel('VIF値')
ax.set_title('多重共線性の確認(VIF)', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
for bar, val in zip(bars, vif_values):
ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height() / 2,
f'{val:.2f}', va='center', fontsize=10)
plt.tight_layout()
plt.savefig(os.path.join(FIG_DIR, '2025_H3_fig4_vif.png'), bbox_inches='tight')
plt.close()
|
▼ 実行結果
Figure 3 saved.
Figure 4: VIF確認...
💡 解説
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。
💡 Python TIPS [式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。
📝 コード
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315 | print("Figure 4 saved.")
print("\n=== ラグ効果まとめ(保育所充実度係数) ===")
for i, (res, lag) in enumerate(zip([res_t0, res_t1, res_t2], ['当期', 't-1期', 't-2期'])):
coef = res.params[1]
pval = res.pvalues[1]
sig = '***' if pval < 0.001 else ('**' if pval < 0.01 else ('*' if pval < 0.05 else 'n.s.'))
print(f" {lag}: beta={coef:.2f}, p={pval:.3f} {sig}")
print("\n=== ラグ効果まとめ(高齢化率係数) ===")
for i, (res, lag) in enumerate(zip([res_t0, res_t1, res_t2], ['当期', 't-1期', 't-2期'])):
coef = res.params[2]
pval = res.pvalues[2]
sig = '***' if pval < 0.001 else ('**' if pval < 0.01 else ('*' if pval < 0.05 else 'n.s.'))
print(f" {lag}: beta={coef:.2f}, p={pval:.3f} {sig}")
print("\n分析完了。html/figures/ に図を保存しました。")
print(f" 2025_H3_fig1_trend.png - 転入者数の時系列推移")
print(f" 2025_H3_fig2_fe.png - 固定効果モデル推定結果")
print(f" 2025_H3_fig3_lag.png - ラグ効果の係数推移")
print(f" 2025_H3_fig4_vif.png - VIF確認")
|
▼ 実行結果
Figure 4 saved.
=== ラグ効果まとめ(保育所充実度係数) ===
当期: beta=-144.16, p=0.382 n.s.
t-1期: beta=-140.82, p=0.799 n.s.
t-2期: beta=292.30, p=0.510 n.s.
=== ラグ効果まとめ(高齢化率係数) ===
当期: beta=396.46, p=0.040 *
t-1期: beta=111.75, p=0.300 n.s.
t-2期: beta=-71.81, p=0.584 n.s.
分析完了。html/figures/ に図を保存しました。
2025_H3_fig1_trend.png - 転入者数の時系列推移
2025_H3_fig2_fe.png - 固定効果モデル推定結果
2025_H3_fig3_lag.png - ラグ効果の係数推移
2025_H3_fig4_vif.png - VIF確認
💡 解説
- このステップでは前のステップで作ったデータを加工しています。コードを上から順に読んでみてください。
💡 Python TIPS r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。
⚠️ よくある誤解と注意点
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
❌ 「相関がある=因果関係がある」ではない
疑似相関(spurious correlation)とは、見かけ上は関係があるように見えるが、実際は無関係、または第三の変数(交絡変数)が両方に影響しているだけの現象です。
古典例: アイスクリームの売上 と 水難事故件数 は強く相関するが、片方が他方を引き起こしているわけではない。両者とも「夏の暑さ」という第三の変数に引きずられているだけ。
論文を読むときの心構え: 「○○と△△に強い相関が見られた」だけで終わっている主張は、本当に因果関係があるのか、それとも第三の変数(人口・所得・地理など)が共通要因として効いているだけではないかを必ず疑ってください。
❌ 「p値が小さい=重要な発見」ではない
p値が小さい(例えば p < 0.001)ことは「統計的に偶然とは考えにくい」という意味であって、「実用的に大きな効果がある」という意味ではありません。
例: 巨大なサンプルサイズ(n=100,000)では、相関係数 r=0.02 でも p < 0.001 になります。しかし r=0.02 は実用上ほぼ無視できる関係です。
正しい読み方: p値と効果量(係数の大きさ、相関係数の値)の両方をセットで判断してください。p値だけで「重要な発見」と結論づけるのは誤りです。
❌ 「回帰係数が大きい=重要な変数」ではない
回帰係数の絶対値は、説明変数の単位に強く依存します。「年収(万円)」と「失業率(%)」の係数を直接比較しても意味がありません。
正しい比較方法: (1) 標準化係数(各変数を平均0・分散1に変換した上での係数)を使う、(2) 限界効果(変数を1標準偏差動かしたときのyの変化)で比較する。
また、係数の大きさが「因果関係の強さ」を意味するわけでもありません。あくまで「相関的な関連の強さ」です。
❌ 「外れ値を除外すれば正しい結果」ではない
外れ値(極端な値)を「目障りだから」「結果が綺麗にならないから」という理由で除外するのは分析の改ざんに近い行為です。
外れ値が示すもの: 本当に重要な情報(東京の超高密度、北海道の超低密度など)であることが多い。外れ値を取り除くと「日本全体の傾向」を見誤る原因になります。
正しい対処: (1) 外れ値の出現要因を調査する(なぜ東京だけ突出するのか)、(2) ノンパラメトリック手法(Spearman相関・Kruskal-Wallis)を使う、(3) 外れ値を含む結果と除外した結果の両方を提示し、解釈を読者に委ねる。
❌ 「サンプルサイズが大きい=信頼できる」ではない
サンプルサイズ(n)が大きいと統計的検定の検出力は上がりますが、それは「偶然による誤差を減らす効果」にすぎません。
nが大きくても解消されない問題:
・選択バイアス(標本が偏っている)
・測定誤差(変数の定義が曖昧)
・欠損値のパターン(欠損がランダムでない)
・交絡変数の見落とし
例: 1万人にWeb調査して「ネット利用と幸福度は強く相関」と言っても、そもそも回答者がネットユーザー寄りに偏っているため、母集団全体の結論にはなりません。
❌ 「複雑なモデル=より良い分析」ではない
ランダムフォレスト・ニューラルネット・複雑な階層モデルなど、高度な手法を使えば「良い分析」と感じがちですが、必ずしもそうではありません。
過学習(overfitting)の罠: モデルが複雑すぎると、訓練データの偶然のパターンまで学習してしまい、新しいデータでは予測精度が落ちます。
シンプルさの価値: 重回帰分析や相関分析は「結果が解釈しやすい」「再現性が高い」という大きな利点があります。複雑な手法はシンプルな手法で答えが出ない時の最後の手段です。
❌ 「多重共線性は気にしなくていい」ではない
多重共線性とは、説明変数同士の相関が極めて強い状態のこと。これを放置すると、回帰係数の符号や大きさが入れ替わる異常事態が起こります。
典型例: 「総人口」と「労働力人口」を同時に投入すると、両者の相関が r=0.99 になり、係数推定が極端に不安定になります。「総人口は正だが、労働力人口は負」のような解釈不能な結果になりがちです。
診断と対処:
・VIF(分散拡大係数)を計算し、VIF > 10 の変数を確認
・相関行列で |r| > 0.8 のペアをチェック
・対処法:一方を除外、合成変数(PCA)に変換、Ridge回帰で安定化
❌ 「R²が高い=良いモデル」ではない
決定係数 R² はモデルの「当てはまりの良さ」を示しますが、R² が高くてもモデルが正しいとは限りません。
R² が高くなる罠:
・説明変数を増やせば R² は自動的に上がる(無関係な変数を追加してもR²は下がらない)
・時系列データでは、共通のトレンド(時間とともに増加)があるだけで R² が 0.9 を超える
・サンプルサイズが小さいとR²が過大評価される
代替指標: 調整済み R²(変数の数でペナルティ)、AIC・BIC(モデル選択基準)を併用してください。予測力の真の評価には交差検証(cross-validation)でテストデータの R² を見ること。
❌ 「ステップワイズで選んだ変数は重要」ではない
ステップワイズ法(バックワード・フォワード選択)は便利ですが、p値ベースの変数選択は再現性に問題があると批判されています。
問題点:
・同じデータでも実行順序によって最終モデルが変わる
・p値を繰り返し見ることで「偶然に有意な変数」を拾ってしまう(p-hacking)
・係数の標準誤差が過小評価され、信頼区間が嘘っぽくなる
より良い方法:
・事前に変数を理論で絞る(先行研究から候補を選ぶ)
・LASSO回帰(自動かつ統計的に正当化された変数選択)を使う
・交差検証で AIC/BIC 最小モデルを選ぶ
❌ 「線形回帰なら線形関係を前提にすべき」
重回帰分析は線形関係を前提とします。実際の関係が非線形なのに線形モデルで分析すると、本当の関係を見逃します。
非線形の例:
・U字型関係: 失業率と物価上昇率(フィリップス曲線)
・逓減効果: 所得と幸福度(年収 800万円までは強い正の効果、それ以上は飽和)
・閾値効果: 高齢化率と医療費(ある水準を超えると急激に上がる)
診断と対処:
・残差プロットで残差が0周辺に均等に分布しているか確認
・変数の対数変換・二乗項追加で非線形性を取り込む
・どうしても線形では捉えられないなら、機械学習(RF・GBM)を併用する
❌ 「データに当てはまった=予測に使える」ではない
「過去のデータでフィットしたから将来も予測できる」と思うのは危険です。
過学習(overfitting)の例: 47都道府県のデータに10個の説明変数を投入すれば、ほぼ完璧にフィットします(自由度がほぼゼロ)。でもそのモデルを新しい年度に適用すると、予測精度はほぼランダム並みに落ちることがあります。
正しい予測力の評価:
・データを訓練用 70%とテスト用 30%に分割し、テスト用での予測精度を見る
・k分割交差検証(k-fold CV)で予測の安定性を確認
・「説明変数の数 ≪ サンプルサイズ」のバランスを意識(目安:n > 10 × 変数数)
🎯 自分でやってみよう(5つのチャレンジ)
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
★☆☆☆☆ 入門
CH1. 同じデータで分析を再現する
まずは付属の Python スクリプトをそのまま実行し、論文と同じ図を再現してみてください。
ポイント: 各図がどのコード行から生成されているか辿る。エラーが出たら原因を考える。
★★☆☆☆ 初級
CH2. 説明変数を1つ追加・除外して結果を比較
本論文の分析モデルから説明変数を1つ抜いて再実行、あるいは1つ追加して再実行してください。
ポイント: 係数・p値・R² がどう変わったか観察する。多重共線性が原因で結果が変わる例を見つけられたら理想的。
★★★☆☆ 中級
CH3. 別の年度・別の都道府県で同じ分析を試す
SSDSE の別の年度(例:2015年度・2020年度)または特定都道府県のみのデータで同じ分析を実行してください。
ポイント: 時代や地域によって結論が変わるか? 変わるならその理由を考察する。
★★★★☆ 上級
CH4. 別の手法を組み合わせる
本論文の手法 + 1つの追加手法(例:重回帰 + LASSO、相関分析 + 主成分分析)で結果を比較してください。
ポイント: 手法の違いで結論が変わるか? どちらが妥当かを「なぜ」とともに説明できるように。
★★★★★ 発展
CH5. オリジナルの問いを立てて分析する
本論文の手法を借りて、あなた自身の問いを立てて分析してください。
例:「カフェの数と幸福度に関連はあるか」「教育費の高い県は出生率も高いか」など。
ポイント: 問い・データ・手法・結論を1ページのレポートにまとめる。これがデータサイエンスの「実践」。
💡 ヒント: 詰まったら本サイトの他の論文(同じ手法を使っている)のスクリプトをコピーして組み合わせるのが効率的です。手法ガイド・用語集も参考に。
🤔 よくある質問(読者からの想定Q&A)
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
Q1. この分析、自分でもできますか?
はい、できます。SSDSE データは無料で公開されており、Python の pandas, scikit-learn, statsmodels を使えば全く同じ手順で再現可能です。本ページ下部のスクリプトを実行するだけで結果が得られます。
Q2. 使われている手法は他の分野にも応用できますか?
十分応用可能です。本論文の[手法]は、医療・教育・経済・環境など他のドメインでも標準的に使われる手法です。データの中身(変数)を入れ替えるだけで、別の問いにも適用できます。
Q3. 結論は本当に「因果関係」を示していますか?
本論文は「観察データ」を使った分析であり、厳密な意味での「因果関係」を完全に証明したわけではありません。あくまで「強い関連が見られた」という事実を提示しているにとどまります。真の因果を示すには、無作為化比較試験(RCT)か、自然実験を活用したIV・DiD 等の手法が必要です。
Q4. データの最新版を使うとどうなりますか?
SSDSE は毎年更新されているため、最新版を使えば近年のトレンド(特にコロナ禍以降の変化)も含めて分析できます。ただし、結論が変わる可能性もあります。それ自体が新しい発見につながります。
Q5. もっと深く学ぶには何を読めばいいですか?
「計量経済学」「データサイエンス入門」「統計的因果推論」などのテキストが入門に向いています。Python の場合は『Python ではじめる機械学習』(オライリー)、R の場合は『R で学ぶ統計学』が定番です。本サイトの他の論文も読み比べてみてください。