このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の空き家問題は少子高齢化・人口減少に伴い深刻化している。しかし空き家数そのものを都道府県・年度のパネルデータで継続的に観測することは難しい。本研究では転出率(人口流出)と着工新設住宅戸数(新規供給)を空き家の代理変数(プロキシ変数)として用い、固定効果モデルにより要因を特定した。
まず「パネルデータを用いた空き家に関する要因分析」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
SSDSE-B PanelOLS 固定効果モデル Hausman検定 プロキシ変数
SSDSE(社会・人口統計体系データセット)-B は全47都道府県の年次統計を収録する。本分析では2012〜2023年の12年分を使用し、都道府県 × 年度の二次元構造(バランスパネル)を構築した。
| 変数カテゴリ | 変数名(生成) | 元データ / 計算方法 | 役割 |
|---|---|---|---|
| 目的変数 (空き家の代理) | 転出率 | 転出者数 / 総人口 × 1,000 | 人口流出 → 空き家増加 |
| 着工率 | 着工新設住宅戸数 / 総人口 × 10,000 | 新規供給過剰 → 空き家リスク | |
| 説明変数 | 高齢化率 | 65歳以上人口 / 総人口 × 100(%) | 高齢化 → 空き家増加 |
| 地価_log | log(住宅地平均地価) | 地価上昇 → 転出抑制? | |
| 求人倍率 | 月間有効求人数 / 月間有効求職者数 | 雇用好況 → 転出抑制 | |
| 消費支出_log | log(消費支出:二人以上世帯) | 生活水準の代理 |
固定効果モデルは「within変換」(個体平均からの偏差)によって観察されない個体固有効果αᵢを消去する。各都道府県の時点間変動のみを使って係数を推定するため、地域固有の産業構造や文化的差異を自動的に除去できる。
1 2 3 4 5 6 7 8 | df_b = pd.read_csv(DATA_B, encoding='cp932', header=1) df_b = df_b[df_b['地域コード'].str.match(r'^R\d{5}$', na=False)].copy() # 年度を整数に df_b['年度'] = df_b['年度'].astype(int) df_b = df_b.sort_values(['都道府県', '年度']).reset_index(drop=True) print(f"パネルデータ形状: {df_b.shape} ({df_b['都道府県'].nunique()} 都道府県 × {df_b['年度'].nunique()} 年度)") print(f"年度範囲: {df_b['年度'].min()} 〜 {df_b['年度'].max()}") |
パネルデータ形状: (564, 112) (47 都道府県 × 12 年度) 年度範囲: 2012 〜 2023
pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。.astype(int) — 列を整数に変換(年度などを数値比較するため)。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。9 10 11 12 13 14 15 16 | df_b['転出率'] = df_b['転出者数(日本人移動者)'] / df_b['総人口'] * 1000 # 人口千対 df_b['転入率'] = df_b['転入者数(日本人移動者)'] / df_b['総人口'] * 1000 df_b['着工率'] = df_b['着工新設住宅戸数'] / df_b['総人口'] * 10000 # 人口1万対 df_b['高齢化率'] = df_b['65歳以上人口'] / df_b['総人口'] * 100 # % df_b['地価_log'] = np.log(df_b['標準価格(平均価格)(住宅地)'].clip(lower=1)) df_b['求人倍率'] = df_b['月間有効求人数(一般)'] / df_b['月間有効求職者数(一般)'].replace(0, np.nan) df_b['消費支出_log']= np.log(df_b['消費支出(二人以上の世帯)'].clip(lower=1)) df_b['純移動率'] = (df_b['転入者数(日本人移動者)'] - df_b['転出者数(日本人移動者)']) / df_b['総人口'] * 1000 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | region_map = { '北海道': '北海道・東北', '青森県': '北海道・東北', '岩手県': '北海道・東北', '宮城県': '北海道・東北', '秋田県': '北海道・東北', '山形県': '北海道・東北', '福島県': '北海道・東北', '茨城県': '関東', '栃木県': '関東', '群馬県': '関東', '埼玉県': '関東', '千葉県': '関東', '東京都': '関東', '神奈川県': '関東', '新潟県': '中部', '富山県': '中部', '石川県': '中部', '福井県': '中部', '山梨県': '中部', '長野県': '中部', '岐阜県': '中部', '静岡県': '中部', '愛知県': '中部', '三重県': '近畿', '滋賀県': '近畿', '京都府': '近畿', '大阪府': '近畿', '兵庫県': '近畿', '奈良県': '近畿', '和歌山県': '近畿', '鳥取県': '中国・四国', '島根県': '中国・四国', '岡山県': '中国・四国', '広島県': '中国・四国', '山口県': '中国・四国', '徳島県': '中国・四国', '香川県': '中国・四国', '愛媛県': '中国・四国', '高知県': '中国・四国', '福岡県': '九州・沖縄', '佐賀県': '九州・沖縄', '長崎県': '九州・沖縄', '熊本県': '九州・沖縄', '大分県': '九州・沖縄', '宮崎県': '九州・沖縄', '鹿児島県': '九州・沖縄', '沖縄県': '九州・沖縄', } df_b['地域'] = df_b['都道府県'].map(region_map) region_order = ['北海道・東北', '関東', '中部', '近畿', '中国・四国', '九州・沖縄'] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。まず47都道府県の全国平均として、転出率・転入率・着工新設住宅戸数の2012〜2023年の推移を確認する。転出超過の時期や景気変動との対応関係から、空き家問題の背景を読み取る。
38 39 40 41 42 43 44 45 46 | print("\n[図1] 時系列プロット作成中...") ts = df_b.groupby('年度').agg( 転出率_mean=('転出率', 'mean'), 着工率_mean=('着工率', 'mean'), 転入率_mean=('転入率', 'mean'), ).reset_index() fig, axes = plt.subplots(1, 2, figsize=(12, 4.5)) fig.suptitle('転出率・着工新設住宅の全国平均推移(2012〜2023年)', fontsize=14, fontweight='bold', y=1.01) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | # 左:転出率 ax = axes[0] ax.plot(ts['年度'], ts['転出率_mean'], 'o-', color='#C62828', linewidth=2.2, markersize=6, label='転出率(千人対)') ax.plot(ts['年度'], ts['転入率_mean'], 's--', color='#1565C0', linewidth=2, markersize=5, label='転入率(千人対)', alpha=0.8) ax.fill_between(ts['年度'], ts['転出率_mean'], ts['転入率_mean'], where=ts['転出率_mean'] > ts['転入率_mean'], alpha=0.15, color='#C62828', label='転出超過') ax.fill_between(ts['年度'], ts['転出率_mean'], ts['転入率_mean'], where=ts['転出率_mean'] <= ts['転入率_mean'], alpha=0.15, color='#1565C0') ax.set_xlabel('年度', fontsize=11) ax.set_ylabel('率(人口千対)', fontsize=11) ax.set_title('転出率・転入率', fontsize=12) ax.legend(fontsize=10) ax.grid(axis='y', alpha=0.4) ax.xaxis.set_major_locator(mticker.MultipleLocator(2)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.fill_between(...) — 2つの曲線で囲まれた領域を塗りつぶし。Lorenz曲線の格差面積などを可視化。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。65 66 67 68 69 70 71 72 73 74 | # 右:着工率 ax2 = axes[1] ax2.plot(ts['年度'], ts['着工率_mean'], 'D-', color='#2E7D32', linewidth=2.2, markersize=6, label='着工新設住宅戸数(人口1万対)') ax2.set_xlabel('年度', fontsize=11) ax2.set_ylabel('戸数(人口1万対)', fontsize=11) ax2.set_title('着工新設住宅戸数', fontsize=12) ax2.legend(fontsize=10) ax2.grid(axis='y', alpha=0.4) ax2.xaxis.set_major_locator(mticker.MultipleLocator(2)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。75 76 77 78 79 80 81 82 83 84 | # 補足テキスト axes[0].annotate('コロナ禍\n2020年', xy=(2020, ts.loc[ts['年度']==2020,'転出率_mean'].values[0]), xytext=(2021, ts.loc[ts['年度']==2020,'転出率_mean'].values[0]+0.5), fontsize=9, color='gray', arrowprops=dict(arrowstyle='->', color='gray', lw=1)) plt.tight_layout() fig.savefig(os.path.join(FIG_DIR, '2022_U5_1_fig1_ts.png'), dpi=150, bbox_inches='tight') plt.close(fig) print(" -> 保存: 2022_U5_1_fig1_ts.png") |
[図1] 時系列プロット作成中... -> 保存: 2022_U5_1_fig1_ts.png
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。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 113 114 | print("\n" + "="*60) print("[固定効果モデル 主要指標]") print(f" R² (within) : {fe_res.rsquared:.4f}") print(f" R² (between) : {fe_res.rsquared_between:.4f}") print(f" 観測数 : {fe_res.nobs}") print(f" エンティティ : {fe_res.entity_info['total']}") print() print("[係数(転出率への影響)]") for var in EXOG_VARS: if var in fe_res.params: c = fe_res.params[var] p = fe_res.pvalues[var] sig = '***' if p < 0.001 else '** ' if p < 0.01 else '* ' if p < 0.05 else ' ' print(f" {var:18s}: {c:+.4f} {sig} (p={p:.4f})") print() print("[Hausman 検定]") print(f" H = {H_stat:.4f}, df = {H_df}, p = {H_pval:.4f}") if H_pval < 0.05: print(" → 固定効果モデルを採用(ランダム効果と有意差あり)") else: print(" → p≥0.05(ランダム効果モデルも統計的に許容)") print() print("[生成図一覧]") for f in ['2022_U5_1_fig1_ts.png', '2022_U5_1_fig2_fe_coef.png', '2022_U5_1_fig3_scatter.png', '2022_U5_1_fig4_region.png']: path = os.path.join(FIG_DIR, f) size = os.path.getsize(path) // 1024 if os.path.exists(path) else -1 print(f" {f} ({size} KB)") print("="*60) print("\n完了: 全4図を html/figures/ に保存しました。") |
============================================================ [固定効果モデル 主要指標] R² (within) : 0.0603 R² (between) : -0.0388 観測数 : 564 エンティティ : 47.0 [係数(転出率への影響)] 高齢化率 : -0.0953 ** (p=0.0046) 地価_log : -0.4199 (p=0.5878) 求人倍率 : +0.5530 * (p=0.0107) 消費支出_log : +0.3599 (p=0.4900) [Hausman 検定] H = 1.7123, df = 4, p = 0.7885 → p≥0.05(ランダム効果モデルも統計的に許容) [生成図一覧] 2022_U5_1_fig1_ts.png (134 KB) 2022_U5_1_fig2_fe_coef.png (88 KB) 2022_U5_1_fig3_scatter.png (148 KB) 2022_U5_1_fig4_region.png (155 KB) ============================================================ 完了: 全4図を html/figures/ に保存しました。
plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。パネルOLS固定効果モデルを用いて転出率への影響要因を推定する。標準誤差は都道府県単位のクラスター標準誤差を採用し、同一都道府県内の時系列相関を考慮する。
| 変数 | 係数の予想符号 | 解釈 |
|---|---|---|
| 高齢化率(%) | 正 | 高齢化が進むと若年層の転出が増え、空き家増加に寄与 |
| 住宅地地価(log) | 負 | 地価が高い地域は経済的活力があり、転出が抑制される |
| 有効求人倍率 | 負 | 雇用が好調であれば転出が抑制される |
| 消費支出(log) | ? | 生活水準の高さを反映するが、転出との関係は複合的 |
FEとREの係数差の共分散行列を用いたWald統計量。差の共分散行列が半正定値でない場合はpinv(擬似逆行列)で対処する。自由度は説明変数の数(ここでは4)。
直接観測できない概念(ここでは年次の空き家状況)を分析するために、理論的に関連する観測可能な変数(転出率・着工率)を代理変数として用いる手法。プロキシ変数を使う際は、どのメカニズムで元の概念と対応するかを理論的に説明できることが重要。
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 | print("\n[図2] 固定効果係数プロット作成中...") coef_df = pd.DataFrame({ 'coef': fe_res.params.drop('const', errors='ignore'), 'se': fe_res.std_errors.drop('const', errors='ignore'), 'pval': fe_res.pvalues.drop('const', errors='ignore'), }) coef_df['ci_lo'] = coef_df['coef'] - 1.96 * coef_df['se'] coef_df['ci_hi'] = coef_df['coef'] + 1.96 * coef_df['se'] coef_df = coef_df.reindex(EXOG_VARS) var_labels = { '高齢化率': '高齢化率(%)', '地価_log': '住宅地地価\n(log, 円/㎡)', '求人倍率': '有効求人倍率', '消費支出_log':'消費支出\n(log, 円)', } labels = [var_labels.get(v, v) for v in coef_df.index] colors = ['#C62828' if p < 0.05 else '#90A4AE' for p in coef_df['pval']] sig_markers = ['★' if p < 0.01 else '✓' if p < 0.05 else '' for p in coef_df['pval']] fig, ax = plt.subplots(figsize=(8, 5)) y_pos = np.arange(len(coef_df)) ax.barh(y_pos, coef_df['coef'], color=colors, height=0.55, alpha=0.85) ax.errorbar(coef_df['coef'], y_pos, xerr=1.96 * coef_df['se'], fmt='none', color='#333', elinewidth=1.5, capsize=5) ax.axvline(0, color='black', linewidth=0.9, linestyle='-') for i, (coef, sig, pval) in enumerate(zip(coef_df['coef'], sig_markers, coef_df['pval'])): xoff = coef_df['ci_hi'].iloc[i] + abs(coef_df['ci_hi'].max()) * 0.03 pstr = f'p={pval:.3f}' if pval >= 0.001 else 'p<0.001' ax.text(xoff, i, f'{sig} {pstr}', va='center', fontsize=9, color='#C62828' if pval < 0.05 else '#666') ax.set_yticks(y_pos) ax.set_yticklabels(labels, fontsize=11) ax.set_xlabel('推定係数(従属変数:転出率 人口千対)', fontsize=11) ax.set_title('固定効果モデル(FE)の推定係数\n(エラーバー:95%信頼区間、標準誤差はクラスタリング)', fontsize=12, fontweight='bold') ax.grid(axis='x', alpha=0.4, linestyle='--') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。157 158 159 160 161 162 163 | # 凡例 from matplotlib.patches import Patch legend_els = [ Patch(facecolor='#C62828', label='p < 0.05(統計的有意)'), Patch(facecolor='#90A4AE', label='p ≥ 0.05(非有意)'), ] ax.legend(handles=legend_els, fontsize=10, loc='lower right') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。164 165 166 167 168 169 170 171 172 173 174 | # Hausman 結果をテキスト注釈 h_text = (f'Hausman検定: H={H_stat:.2f}, p={H_pval:.3f}\n' f'→ 固定効果モデル{"適切" if H_pval < 0.05 else "(RE許容)"}') ax.text(0.02, 0.98, h_text, transform=ax.transAxes, fontsize=9, va='top', color='#333', bbox=dict(boxstyle='round,pad=0.4', facecolor='#EFF3FF', alpha=0.8)) plt.tight_layout() fig.savefig(os.path.join(FIG_DIR, '2022_U5_1_fig2_fe_coef.png'), dpi=150, bbox_inches='tight') plt.close(fig) print(" -> 保存: 2022_U5_1_fig2_fe_coef.png") |
[図2] 固定効果係数プロット作成中... -> 保存: 2022_U5_1_fig2_fe_coef.png
fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。47都道府県の2012〜2023年平均値を用い、住宅地地価と転出率の関係を地域別の色分けで可視化する。OLS回帰線も重ねて、全体的な傾向を確認する。
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | print("\n[図3] 散布図(転出率 vs 住宅地地価)作成中...") # 2012〜2023 年の平均で集計 df_avg = df_b.groupby('都道府県').agg( 転出率_avg=('転出率', 'mean'), 地価_avg=('標準価格(平均価格)(住宅地)', 'mean'), 地域=('地域', 'first'), ).reset_index() region_colors = { '北海道・東北': '#1565C0', '関東': '#C62828', '中部': '#2E7D32', '近畿': '#E65100', '中国・四国': '#6A1B9A', '九州・沖縄': '#00838F', } fig, ax = plt.subplots(figsize=(10, 7)) for region, grp in df_avg.groupby('地域'): ax.scatter(grp['地価_avg'], grp['転出率_avg'], color=region_colors.get(region, 'gray'), s=70, alpha=0.85, label=region, zorder=3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。199 200 201 202 203 204 | # 都道府県ラベル(一部省略せず全て) for _, row in df_avg.iterrows(): pref_short = row['都道府県'].replace('県','').replace('府','').replace('都','').replace('道','') ax.annotate(pref_short, (row['地価_avg'], row['転出率_avg']), textcoords='offset points', xytext=(4, 2), fontsize=7, alpha=0.85, color='#333') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | # OLS 回帰線 x_all = df_avg['地価_avg'].values y_all = df_avg['転出率_avg'].values slope, intercept, r_val, p_val, _ = stats.linregress(x_all, y_all) xline = np.linspace(x_all.min(), x_all.max(), 100) ax.plot(xline, slope * xline + intercept, '--', color='gray', linewidth=1.5, label=f'回帰線 (r={r_val:.2f}, p={p_val:.3f})') ax.set_xlabel('住宅地平均地価(円/㎡)\n2012〜2023年平均', fontsize=12) ax.set_ylabel('転出率(人口千対)\n2012〜2023年平均', fontsize=12) ax.set_title('転出率 vs 住宅地地価(都道府県別)', fontsize=13, fontweight='bold') ax.legend(fontsize=10, ncol=2, loc='upper right') ax.grid(alpha=0.3, linestyle='--') ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'{int(x):,}')) plt.tight_layout() fig.savefig(os.path.join(FIG_DIR, '2022_U5_1_fig3_scatter.png'), dpi=150, bbox_inches='tight') plt.close(fig) print(" -> 保存: 2022_U5_1_fig3_scatter.png") |
[図3] 散布図(転出率 vs 住宅地地価)作成中... -> 保存: 2022_U5_1_fig3_scatter.png
stats.linregress(x, y) — 単回帰の傾き・切片・r値・p値・標準誤差を返します。使わない値は _ で受け取り。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。47都道府県を6地域(北海道・東北 / 関東 / 中部 / 近畿 / 中国・四国 / 九州・沖縄)に区分し、転出率の分布と年次推移を比較する。地域間の構造的差異を記述統計的に把握する。
同じ都道府県のデータは時系列的に相関している(系列相関)。これを無視した通常のOLS標準誤差は過小推定になり、t値が実際より大きく見えて有意と誤判定しやすい。クラスター標準誤差は「都道府県単位で誤差項の相関を許容」することでこの問題を修正する。
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.ticker as mticker import warnings warnings.filterwarnings('ignore') from linearmodels.panel import PanelOLS, RandomEffects import statsmodels.api as sm from scipy import stats plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 import os FIG_DIR = 'html/figures' DATA_B = 'data/raw/SSDSE-B-2026.csv' os.makedirs(FIG_DIR, exist_ok=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。plt.rcParams['font.family'] — グラフの日本語表示用フォント指定(Macは Hiragino Sans、Windowsなら Yu Gothic 等)。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。246 247 248 249 250 251 252 | print("\n[パネル分析] 固定効果モデル推定中...") EXOG_VARS = ['高齢化率', '地価_log', '求人倍率', '消費支出_log'] ENDOG_VAR = '転出率' df_panel = df_b[['都道府県', '年度', ENDOG_VAR] + EXOG_VARS].dropna().copy() print(f" 分析対象: {len(df_panel)} 観測 ({df_panel['都道府県'].nunique()} 都道府県)") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。253 254 255 256 257 258 259 260 261 262 263 264 265 | # MultiIndex 設定 df_panel = df_panel.set_index(['都道府県', '年度']) # ── 固定効果モデル(FE) fe_model = PanelOLS( df_panel[ENDOG_VAR], sm.add_constant(df_panel[EXOG_VARS]), entity_effects=True, time_effects=False, ) fe_res = fe_model.fit(cov_type='clustered', cluster_entity=True) print("\n[固定効果モデル推定結果]") print(fe_res.summary.tables[1]) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。266 267 268 269 270 271 | # ── ランダム効果モデル(RE)— Hausman 検定用 re_model = RandomEffects( df_panel[ENDOG_VAR], sm.add_constant(df_panel[EXOG_VARS]), ) re_res = re_model.fit(cov_type='robust') |
[パネル分析] 固定効果モデル推定中...
分析対象: 564 観測 (47 都道府県)
[固定効果モデル推定結果]
Parameter Estimates
==============================================================================
Parameter Std. Err. T-stat P-value Lower CI Upper CI
------------------------------------------------------------------------------
const 19.069 10.680 1.7855 0.0748 -1.9134 40.052
高齢化率 -0.0953 0.0335 -2.8438 0.0046 -0.1611 -0.0295
地価_log -0.4199 0.7742 -0.5424 0.5878 -1.9409 1.1011
求人倍率 0.5530 0.2160 2.5605 0.0107 0.1287 0.9773
消費支出_log 0.3599 0.5210 0.6907 0.4900 -0.6637 1.3836
==============================================================================sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。272 273 274 275 276 277 | print("\n[Hausman検定] FE vs RE 係数差の検定...") fe_params = fe_res.params.drop('const', errors='ignore') re_params = re_res.params.drop('const', errors='ignore') common_vars = fe_params.index.intersection(re_params.index) b_diff = fe_params[common_vars].values - re_params[common_vars].values |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | # 共分散行列差 fe_cov = fe_res.cov.loc[common_vars, common_vars].values re_cov = re_res.cov.loc[common_vars, common_vars].values cov_diff = fe_cov - re_cov try: cov_inv = np.linalg.pinv(cov_diff) H_stat = float(b_diff @ cov_inv @ b_diff) H_df = len(common_vars) H_pval = 1 - stats.chi2.cdf(H_stat, H_df) hausman_ok = True except Exception: H_stat, H_df, H_pval = np.nan, len(common_vars), np.nan hausman_ok = False print(f" Hausman 統計量 H = {H_stat:.4f}") print(f" 自由度 = {H_df}") print(f" p値 = {H_pval:.4f}") if hausman_ok: if H_pval < 0.05: print(" → p<0.05: 固定効果モデルが適切(個体固有の偏りを制御)") else: print(" → p≥0.05: ランダム効果モデルも許容") |
[Hausman検定] FE vs RE 係数差の検定... Hausman 統計量 H = 1.7123 自由度 = 4 p値 = 0.7885 → p≥0.05: ランダム効果モデルも許容
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。301 302 303 304 305 306 307 308 309 310 | print("\n[図4] 地域別転出率比較作成中...") # 2022年(最新の完全年)で比較(全期間の平均も補助で) df_region_data = [] for region in region_order: vals = df_b[df_b['地域'] == region]['転出率'].dropna().values df_region_data.append(vals) fig, axes = plt.subplots(1, 2, figsize=(13, 5.5)) fig.suptitle('地域別 転出率の分布(2012〜2023年)', fontsize=14, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。311 312 313 314 315 316 317 318 319 320 321 322 323 324 | # 左:箱ひげ図 ax = axes[0] bp = ax.boxplot(df_region_data, patch_artist=True, notch=False, medianprops=dict(color='white', linewidth=2.5), whiskerprops=dict(linewidth=1.5), flierprops=dict(marker='o', markersize=4, alpha=0.5)) for patch, region in zip(bp['boxes'], region_order): patch.set_facecolor(region_colors[region]) patch.set_alpha(0.8) ax.set_xticks(range(1, len(region_order)+1)) ax.set_xticklabels([r.replace('・', '\n') for r in region_order], fontsize=9) ax.set_ylabel('転出率(人口千対)', fontsize=11) ax.set_title('地域別転出率(箱ひげ図)', fontsize=12) ax.grid(axis='y', alpha=0.4) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | # 右:年別推移(地域別折れ線) ax2 = axes[1] for region in region_order: ts_r = df_b[df_b['地域'] == region].groupby('年度')['転出率'].mean() ax2.plot(ts_r.index, ts_r.values, 'o-', color=region_colors[region], linewidth=1.8, markersize=4, label=region, alpha=0.9) ax2.set_xlabel('年度', fontsize=11) ax2.set_ylabel('転出率(人口千対)', fontsize=11) ax2.set_title('地域別 転出率の年次推移', fontsize=12) ax2.legend(fontsize=9, loc='upper right', ncol=2) ax2.grid(alpha=0.4) ax2.xaxis.set_major_locator(mticker.MultipleLocator(2)) plt.tight_layout() fig.savefig(os.path.join(FIG_DIR, '2022_U5_1_fig4_region.png'), dpi=150, bbox_inches='tight') plt.close(fig) print(" -> 保存: 2022_U5_1_fig4_region.png") |
[図4] 地域別転出率比較作成中... -> 保存: 2022_U5_1_fig4_region.png
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。SSDSE-B(47都道府県×12年)のパネルデータと固定効果モデルを用いた分析から、空き家増加の代理指標(転出率)に関して以下が明らかになった:
| データ | 出典 |
|---|---|
| SSDSE-B-2026.csv(都道府県パネルデータ) | 統計数理研究所 SSDSE(社会・人口統計体系)B表 |
| 転出者数・転入者数 | 総務省 住民基本台帳人口移動報告(SSDSE-B収録) |
| 着工新設住宅戸数 | 国土交通省 建築着工統計調査(SSDSE-B収録) |
| 住宅地標準価格 | 国土交通省 地価公示(SSDSE-B収録) |
| 有効求人倍率 | 厚生労働省 職業安定業務統計(SSDSE-B収録) |
本スクリプトはSSDSE-B-2026.csvの実データを使用。実行にはlinearmodels(pip install linearmodels)が必要。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。