―固定効果モデルを用いたパネルデータ分析―
この教材は原論文と同じ種類のデータで計算し直しています。ただし工程の一部は、学習しやすさのために簡略化しています(下の「できないこと」を参照)。
| 原論文が使ったデータ | 総務省 ふるさと納税に関する現況調査・SSDSE(市区町村のすがた)・国勢調査・人口動態統計・住民基本台帳人口移動報告・市町村税課税状況等の調 分析単位:市区町村 中核手法:固定効果モデル・パネルデータ分析・回帰分析 |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | ふるさと納税は地方創生の切り札になりえるか ―固定効果モデルを用いたパネルデータ分析― 優秀賞/森 將暁(一橋大学 商学部 経営学科) |
▶ ここに書いてある分析は、ページ下部の「🐍 ブラウザで動かす」でそのまま実行できます(インストール不要)。動かしているのは code/2020_U2_yushu_furusato.py(264 行)そのものです。
ふるさと納税は、生まれ故郷や応援したい自治体に寄付をすると、返礼品がもらえて税金も 軽くなる制度です。制度が始まってから受入額はずっと増え続けています。
ただし、この制度が本来ねらっていたのは地方創生、つまり 「地域の経済が元気になること」と「人が地域に増えること」でした。 寄付が集まっているのはたしかですが、ねらいどおりの効果が出ているのかは きちんと検証されていませんでした。
原論文は、全国の市区町村を何年も追いかけたデータ(パネルデータ)を作り、 固定効果モデルという方法で次の 2 つの仮説を検証しました。
結論は、「経済効果はあるが、人口増加には効果がない」というものでした。
単純に「ふるさと納税を多く集めた自治体ほど人口が増えているか」を調べるだけでは、 答えを間違えます。
たとえば、もともと魅力のある自治体は、 良い返礼品を用意できるので寄付も集まりますし、そもそも住みたい人も多いはずです。 このとき「寄付が多い自治体は人口も多い」という関係が見えても、 それは寄付の効果ではなく、その自治体がもともと持っている魅力の効果です。 こういう見落とし変数のことを欠落変数といいます。
固定効果モデルは、この問題を「その自治体の中での動きだけを見る」ことで解決します。 自治体ごとの平均を引いてしまえば、時間で変わらない性質はすべて消えるからです。 下の Step1 では、それを実際の数字で見せます。
ここから先は、再現スクリプト code/2020_U2_yushu_furusato.py を
上から順に動かしたものです。表示している実行結果は、すべて実際に動かして出た値です。
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | # ================================================================ # ■ データ読み込み(市区町村パネル) # ================================================================ df = pd.read_csv(DATA) df = df.dropna(subset=['直近3年受入額_百万円', '人口増減率', '10万人当たり医師数']) print("=" * 74) print("■ 市区町村パネル(ふるさと納税 × 人口動態)") print(" ふるさと納税: 総務省『ふるさと納税に関する現況調査』団体別一覧(原論文と同じ出典)") print(" 人口動態 : SSDSE-A 2022〜2026 年版(版ごとの収録年次を重ねてパネル化)") print("=" * 74) print(f" 市区町村数: {df['地域コード'].nunique()} 団体") print(f" 年 : {sorted(df['年'].unique())}") print(f" 観測数 : {len(df)} 行") # 受入額は分布が大きく歪むので対数を取る(原論文も金額そのものではなく規模で効かせている) df['log受入額'] = np.log1p(df['直近3年受入額_百万円']) |
========================================================================== ■ 市区町村パネル(ふるさと納税 × 人口動態) ふるさと納税: 総務省『ふるさと納税に関する現況調査』団体別一覧(原論文と同じ出典) 人口動態 : SSDSE-A 2022〜2026 年版(版ごとの収録年次を重ねてパネル化) ========================================================================== 市区町村数: 1692 団体 年 : [np.int64(2020), np.int64(2022)] 観測数 : 3384 行
pd.read_csv で読んだあと dropna で欠測のある行を落としています。75 76 77 78 79 80 81 82 83 | # ================================================================ # ■ 記述統計(原論文 表3 に対応) # ================================================================ VARS = ['直近3年受入額_百万円', '自然増減率', '社会増減率', '人口増減率', '10万人当たり医師数'] print("\n【基本統計量】") desc = df[VARS].describe().T[['count', 'mean', 'std', 'min', '50%', 'max']] desc.columns = ['度数', '平均', '標準偏差', '最小', '中央', '最大'] print(desc.round(2).to_string()) |
【基本統計量】
度数 平均 標準偏差 最小 中央 最大
直近3年受入額_百万円 3384.0 1175.65 3030.73 0.04 345.60 70498.01
自然増減率 3384.0 -1.02 0.65 -5.92 -0.98 1.78
社会増減率 3384.0 -0.38 0.72 -12.52 -0.36 3.46
人口増減率 3384.0 -1.40 1.16 -17.07 -1.41 4.20
10万人当たり医師数 3384.0 171.55 176.41 0.00 138.53 2214.61df.describe().T で縦横を入れ替えると、変数が多いときに読みやすくなります。84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | # ================================================================ # ■ Step1. 固定効果モデルとは何をしているのか(within 変換を手で作る) # ================================================================ print("\n" + "=" * 74) print("■ Step1. 固定効果モデル=『各市区町村の平均からのズレ』で見る") print("=" * 74) print(" 同じ市区町村の中での動きだけを使うので、") print(" 「もともと魅力的な自治体だから寄付も移住も多い」といった、") print(" 時間で変わらない性質(=欠落変数)の影響を丸ごと消せる。") def within(frame, cols, group='地域コード'): """各グループ(市区町村)の平均を引く。これが固定効果モデルの中身。""" out = frame.copy() for c in cols: out[c + '_w'] = out[c] - out.groupby(group)[c].transform('mean') return out TARGETS = ['自然増減率', '社会増減率', '人口増減率'] XS = ['log受入額', '10万人当たり医師数'] dw = within(df, TARGETS + XS) sample = dw[dw['地域コード'] == dw['地域コード'].iloc[0]] print(f"\n 例:{sample['都道府県'].iloc[0]}{sample['市区町村'].iloc[0]} の人口増減率") for _, r in sample.iterrows(): print(f" {int(r['年'])}年 実測 {r['人口増減率']:+.3f} %" f" 平均からのズレ {r['人口増減率_w']:+.3f} %") |
==========================================================================
■ Step1. 固定効果モデル=『各市区町村の平均からのズレ』で見る
==========================================================================
同じ市区町村の中での動きだけを使うので、
「もともと魅力的な自治体だから寄付も移住も多い」といった、
時間で変わらない性質(=欠落変数)の影響を丸ごと消せる。
例:北海道函館市 の人口増減率
2020年 実測 -1.395 % 平均からのズレ +0.110 %
2022年 実測 -1.616 % 平均からのズレ -0.110 %groupby(...).transform('mean') はグループ平均を元の行数のまま返してくれるので、引き算にそのまま使えます。113 114 115 116 117 118 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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | # ================================================================ # ■ Step2. 固定効果回帰(原論文 表5 に対応する、人口側の 3 本) # ================================================================ print("\n" + "=" * 74) print("■ Step2. 固定効果モデルの推定(被説明変数を 3 通り)") print("=" * 74) def fe_ols(frame, y, xs, group='地域コード'): """within 変換したデータに最小二乗法をあてる(=固定効果推定)。 標準誤差は市区町村ごとにクラスターして、同じ自治体の観測が 似ていることを考慮する。 """ d = frame.dropna(subset=[y + '_w'] + [x + '_w' for x in xs]) Y = d[y + '_w'].to_numpy() X = np.column_stack([d[x + '_w'].to_numpy() for x in xs]) beta, *_ = np.linalg.lstsq(X, Y, rcond=None) resid = Y - X @ beta n, k = X.shape n_g = d[group].nunique() XtX_inv = np.linalg.inv(X.T @ X) meat = np.zeros((k, k)) for _g, idx in d.groupby(group).indices.items(): Xg, ug = X[idx], resid[idx] s = Xg.T @ ug meat += np.outer(s, s) dof = n - n_g - k scale = (n_g / max(n_g - 1, 1)) * ((n - 1) / max(dof, 1)) cov = XtX_inv @ meat @ XtX_inv * scale se = np.sqrt(np.diag(cov)) t = beta / se from scipy import stats as st p = 2 * (1 - st.t.cdf(np.abs(t), df=max(dof, 1))) ss_res = float(resid @ resid) ss_tot = float(((Y - Y.mean()) ** 2).sum()) return pd.DataFrame({'係数': beta, '標準誤差': se, 't値': t, 'p値': p}, index=xs), 1 - ss_res / ss_tot, n, n_g results = {} for y in TARGETS: tbl, r2, n, ng = fe_ols(dw, y, XS) results[y] = tbl print(f"\n【被説明変数: {y}】 観測 {n} / 市区町村 {ng} / within R² = {r2:.4f}") show = tbl.copy() show['判定'] = ['***' if p < .01 else '**' if p < .05 else '*' if p < .1 else 'n.s.' for p in show['p値']] print(show.round(4).to_string()) |
==========================================================================
■ Step2. 固定効果モデルの推定(被説明変数を 3 通り)
==========================================================================
【被説明変数: 自然増減率】 観測 3384 / 市区町村 1692 / within R² = 0.1336
係数 標準誤差 t値 p値 判定
log受入額 -0.1497 0.0164 -9.1504 0.0000 ***
10万人当たり医師数 -0.0001 0.0006 -0.2076 0.8356 n.s.
【被説明変数: 社会増減率】 観測 3384 / 市区町村 1692 / within R² = 0.0020
係数 標準誤差 t値 p値 判定
log受入額 0.0320 0.0238 1.3454 0.1787 n.s.
10万人当たり医師数 0.0005 0.0015 0.3112 0.7557 n.s.
【被説明変数: 人口増減率】 観測 3384 / 市区町村 1692 / within R² = 0.0195
係数 標準誤差 t値 p値 判定
log受入額 -0.1177 0.0266 -4.4285 0.0000 ***
10万人当たり医師数 0.0003 0.0014 0.2436 0.8076 n.s.np.linalg.lstsq は最小二乗法をそのまま解きます。外部ライブラリに頼らないので、ブラウザ上でもそのまま動きます。163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | # ================================================================ # ■ Step3. 原論文の結論と照らし合わせる # ================================================================ print("\n" + "=" * 74) print("■ Step3. 原論文の結論と、この再現の結果") print("=" * 74) b_soc = results['社会増減率'].loc['log受入額'] b_nat = results['自然増減率'].loc['log受入額'] b_pop = results['人口増減率'].loc['log受入額'] print(" 原論文(2012/2014/2016):") print(" ・社会増減率 … 関係が確認されなかった") print(" ・自然増減率 … 負の効果") print(" ・結論 … ふるさと納税は人口増加をもたらさない") print("\n この再現(2020〜2023):") for name, b in [('社会増減率', b_soc), ('自然増減率', b_nat), ('人口増減率', b_pop)]: verdict = ('有意な関係は見られない' if b['p値'] >= .05 else ('正の効果' if b['係数'] > 0 else '負の効果')) print(f" ・{name} … 係数 {b['係数']:+.4f} (p={b['p値']:.3f}) → {verdict}") print("\n → 『寄付が集まっても人口増加には結びつかない』という原論文の主旨が、") print(" 時点を変えても同じ向きで確認できるかどうかが読みどころ。") |
==========================================================================
■ Step3. 原論文の結論と、この再現の結果
==========================================================================
原論文(2012/2014/2016):
・社会増減率 … 関係が確認されなかった
・自然増減率 … 負の効果
・結論 … ふるさと納税は人口増加をもたらさない
この再現(2020〜2023):
・社会増減率 … 係数 +0.0320 (p=0.179) → 有意な関係は見られない
・自然増減率 … 係数 -0.1497 (p=0.000) → 負の効果
・人口増減率 … 係数 -0.1177 (p=0.000) → 負の効果
→ 『寄付が集まっても人口増加には結びつかない』という原論文の主旨が、
時点を変えても同じ向きで確認できるかどうかが読みどころ。{値:+.4f} は符号つきで小数 4 桁に揃えます。184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | # ================================================================ # ■ 図1: ふるさと納税受入額の推移(原論文 図1 に対応) # ================================================================ fig, ax = plt.subplots(figsize=(9, 5)) tot = (df.groupby('年')['ふるさと納税受入額_千円'].sum() / 1e6) ax.bar(tot.index.astype(int), tot.values, color='#1565C0') for x, v in zip(tot.index.astype(int), tot.values): ax.text(x, v, f'{v:,.0f}', ha='center', va='bottom', fontsize=11) ax.set_xlabel('年度') ax.set_ylabel('ふるさと納税受入額の合計(十億円)') ax.set_title('図1: ふるさと納税受入額の推移(分析対象の市区町村合計)\n' '出典: 総務省「ふるさと納税に関する現況調査」', fontsize=13) ax.grid(axis='y', alpha=.3) fig.savefig(os.path.join(FIG_DIR, '2020_U2_furusato_fig1_trend.png'), dpi=DPI, bbox_inches='tight') plt.close(fig) print("\n → html/figures/2020_U2_furusato_fig1_trend.png 保存完了") |
→ html/figures/2020_U2_furusato_fig1_trend.png 保存完了
ax.text(x, v, ...) で棒の上に数値を書き足せます。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 | # ================================================================ # ■ 図2: 固定効果の考え方(実測値と、平均からのズレ) # ================================================================ fig, axes = plt.subplots(1, 2, figsize=(13, 5)) top = (df.groupby('地域コード')['直近3年受入額_百万円'].mean() .sort_values(ascending=False).head(6).index) sub = df[df['地域コード'].isin(top)] subw = within(sub, ['人口増減率']) for code, g in subw.groupby('地域コード'): label = f"{g['都道府県'].iloc[0]}{g['市区町村'].iloc[0]}" axes[0].plot(g['年'], g['人口増減率'], marker='o', label=label) axes[1].plot(g['年'], g['人口増減率_w'], marker='o', label=label) axes[0].set_title('実測の人口増減率\n(自治体ごとの水準の違いが大きい)', fontsize=12) axes[1].set_title('自治体の平均を引いたあと\n(固定効果モデルが見ているのはこちら)', fontsize=12) for ax in axes: ax.set_xlabel('年') ax.set_ylabel('人口増減率(%)') ax.grid(alpha=.3) ax.legend(fontsize=9) axes[1].axhline(0, color='#C62828', lw=1.2, ls='--') fig.suptitle('図2: 固定効果モデルは「その自治体の中での動き」だけを使う', fontsize=14) fig.savefig(os.path.join(FIG_DIR, '2020_U2_furusato_fig2_within.png'), dpi=DPI, bbox_inches='tight') plt.close(fig) print(" → html/figures/2020_U2_furusato_fig2_within.png 保存完了") |
→ html/figures/2020_U2_furusato_fig2_within.png 保存完了
plt.subplots(1, 2) で 2 枚を横に並べます。228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | # ================================================================ # ■ 図3: 推定結果(係数と 95% 信頼区間) # ================================================================ fig, ax = plt.subplots(figsize=(9, 5)) ys = list(results.keys()) coef = [results[y].loc['log受入額', '係数'] for y in ys] se = [results[y].loc['log受入額', '標準誤差'] for y in ys] pos = np.arange(len(ys)) ax.errorbar(coef, pos, xerr=[1.96 * s for s in se], fmt='o', color='#1565C0', capsize=6, markersize=9, lw=2) ax.axvline(0, color='#C62828', ls='--', lw=1.4) ax.set_yticks(pos) ax.set_yticklabels(ys, fontsize=12) ax.set_xlabel('ふるさと納税受入額(対数)の係数と 95% 信頼区間') ax.set_title('図3: ふるさと納税は人口を増やしたか(固定効果モデル)\n' '横棒が 0 をまたいでいれば「効果があるとは言えない」', fontsize=13) ax.grid(axis='x', alpha=.3) fig.savefig(os.path.join(FIG_DIR, '2020_U2_furusato_fig3_coef.png'), dpi=DPI, bbox_inches='tight') plt.close(fig) print(" → html/figures/2020_U2_furusato_fig3_coef.png 保存完了") |
→ html/figures/2020_U2_furusato_fig3_coef.png 保存完了
ax.errorbar(..., xerr=...) で横向きの信頼区間を描けます。250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | # ================================================================ # ■ 再現できていない部分(隠さずに書く) # ================================================================ print("\n" + "=" * 74) print("■ この再現でできていないこと") print("=" * 74) print(" 1. 仮説1(経済効果)の被説明変数『納税義務者1人当たり課税対象所得』は") print(" SSDSE に収録が無く、この教材では検証できていない。") print(" 取得先: 総務省「市町村税課税状況等の調」") print(" 2. 時点が原論文(2012/2014/2016)ではなく 2020〜2023 年。") print(" SSDSE-A の版を重ねて作れるのがこの範囲のため。") print(" 3. 大学進学率・保育施設利用者比率・高齢者施設定員比率は、") print(" 市区町村パネルとして揃わないため統制変数から外している。") print("=" * 74) |
==========================================================================
■ この再現でできていないこと
==========================================================================
1. 仮説1(経済効果)の被説明変数『納税義務者1人当たり課税対象所得』は
SSDSE に収録が無く、この教材では検証できていない。
取得先: 総務省「市町村税課税状況等の調」
2. 時点が原論文(2012/2014/2016)ではなく 2020〜2023 年。
SSDSE-A の版を重ねて作れるのがこの範囲のため。
3. 大学進学率・保育施設利用者比率・高齢者施設定員比率は、
市区町村パネルとして揃わないため統制変数から外している。
==========================================================================スクリプトが作った図です。数字だけでは掴みにくい部分を目で確かめてください。
ページ下部の「🐍 ブラウザで動かす」を使えば、何も準備せずにそのまま実行できます。 手元の Python で動かしたい場合だけ、次を用意してください。
python3 code/2020_U2_furusato_data_prep.py でパネルを作ります
(作成済みのものは
2020_U2_panel.csv)。実行は python3 code/2020_U2_yushu_furusato.py です。ファイルの編集は要りません。
隠さずに書きます。
逆に、説明変数のふるさと納税受入額は原論文とまったく同じ出典で、 分析対象からの除外(東京 23 区・政令指定都市・福島県 5 町村)も原論文どおりです。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2023_U1_daijin.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。