このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
医療機関へのアクセスの良さ(診療所数・医師数など)は健康アウトカムを改善すると期待されるが、単純なOLS回帰では「内生性(endogeneity)」の問題が生じる。医療費が高い地域に診療所が多く立地する逆因果(reverse causality)や、観察されない地域特性(欠落変数)が両者に影響するため、OLS推定量は偏りを持つ可能性がある。本研究は操作変数法(IV:Instrumental Variable)、とりわけ2段階最小二乗法(2SLS)を用いてこの内生性バイアスを除去し、医療アクセスが健康コスト(保健医療費)に与える純粋な因果効果を推定する。
まず「医療アクセスと健康アウトカム:操作変数法によるパネル分析」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
保健医療費(二人以上の世帯)
健康コスト・医療利用の代理変数
一般診療所数
医療アクセスの指標(内生性の疑いあり)
降水量(年間)
気象条件が受診行動・施設立地に影響
高齢化率、消費支出(log)、総人口(log)
SSDSE-B 2SLS 操作変数法 パネルデータ linearmodels
SSDSE-B-2026(社会・人口統計体系)から47都道府県 × 12年(2012〜2023年度)= 564観測を使用する。各変数の定義と役割を以下に示す。
| 変数名 | 定義 | 単位 | 役割 |
|---|---|---|---|
| 保健医療費(log) | 保健医療費(二人以上世帯)の自然対数 | 円 | 目的変数 Y |
| 一般診療所数(log) | 一般診療所数の自然対数 | 件 | 内生変数(Endogenous) |
| 降水量・年間(log) | 年間降水量の自然対数 | mm | 操作変数(Instrument) |
| 高齢化率 | 65歳以上人口 / 総人口 × 100 | % | 外生的制御変数 |
| 消費支出(log) | 消費支出(二人以上世帯)の自然対数 | 円 | 外生的制御変数 |
| 総人口(log) | 総人口の自然対数 | 人 | 外生的制御変数 |
保健医療費・診療所数・降水量はいずれも右裾の長い分布を持つため、自然対数変換を施す。対数変換後の係数は「弾力性」として解釈でき、「診療所数が1%増加すると保健医療費は何%変化するか」という形で直感的な解釈が可能になる。
分析の第一歩として、目的変数である保健医療費の時系列推移を地域別に確認する。地域ごとのトレンドや変動パターンを把握することで、後続のパネル分析における「時間固定効果」の必要性が見えてくる。
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 | import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import warnings warnings.filterwarnings('ignore') import statsmodels.api as sm from linearmodels.iv import IV2SLS 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) 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 はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。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)。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) — 指定列で並べ替え(降順)。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | # 変数生成 df_b['保健医療費'] = df_b['保健医療費(二人以上の世帯)'] df_b['医療費_log'] = np.log(df_b['保健医療費'].clip(lower=1)) df_b['診療所数_log'] = np.log(df_b['一般診療所数'].clip(lower=1)) df_b['降水量_log'] = np.log(df_b['降水量(年間)'].clip(lower=1)) df_b['高齢化率'] = df_b['65歳以上人口'] / df_b['総人口'] * 100 df_b['消費支出_log'] = np.log(df_b['消費支出(二人以上の世帯)'].clip(lower=1)) df_b['人口_log'] = np.log(df_b['総人口'].clip(lower=1)) region_map = { '北海道': '北海道・東北', '青森県': '北海道・東北', '岩手県': '北海道・東北', '宮城県': '北海道・東北', '秋田県': '北海道・東北', '山形県': '北海道・東北', '福島県': '北海道・東北', '茨城県': '関東', '栃木県': '関東', '群馬県': '関東', '埼玉県': '関東', '千葉県': '関東', '東京都': '関東', '神奈川県': '関東', '新潟県': '中部', '富山県': '中部', '石川県': '中部', '福井県': '中部', '山梨県': '中部', '長野県': '中部', '岐阜県': '中部', '静岡県': '中部', '愛知県': '中部', '三重県': '近畿', '滋賀県': '近畿', '京都府': '近畿', '大阪府': '近畿', '兵庫県': '近畿', '奈良県': '近畿', '和歌山県': '近畿', '鳥取県': '中国・四国', '島根県': '中国・四国', '岡山県': '中国・四国', '広島県': '中国・四国', '山口県': '中国・四国', '徳島県': '中国・四国', '香川県': '中国・四国', '愛媛県': '中国・四国', '高知県': '中国・四国', '福岡県': '九州・沖縄', '佐賀県': '九州・沖縄', '長崎県': '九州・沖縄', '熊本県': '九州・沖縄', '大分県': '九州・沖縄', '宮崎県': '九州・沖縄', '鹿児島県': '九州・沖縄', '沖縄県': '九州・沖縄', } df_b['地域'] = df_b['都道府県'].map(region_map) region_colors = { '北海道・東北': '#4e9af1', '関東': '#e05c5c', '中部': '#f0a500', '近畿': '#5cb85c', '中国・四国': '#9b59b6', '九州・沖縄': '#f39c12', } fig, ax = plt.subplots(figsize=(10, 5)) yearly = df_b.groupby(['年度', '地域'])['保健医療費'].mean().reset_index() for reg, grp in yearly.groupby('地域'): ax.plot(grp['年度'], grp['保健医療費'] / 1000, marker='o', markersize=4, label=reg, color=region_colors.get(reg, 'gray')) ax.set_xlabel('年度', fontsize=12) ax.set_ylabel('保健医療費(千円)', fontsize=12) ax.set_title('地域別 保健医療費の推移(2012〜2023年)', fontsize=14, fontweight='bold') ax.legend(fontsize=9) ax.grid(alpha=0.3) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2022_U5_12_fig1_ts.png'), dpi=150, bbox_inches='tight') plt.close() print("Fig1 saved") |
Fig1 saved
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。2022年度の断面データを用いて、一般診療所数と保健医療費の単純な関係を散布図で確認する。この段階では OLS 回帰直線を引くことで、内生性バイアスが入り込んだ「生の相関」を可視化する。
OLS推定が一致推定量(consistent estimator)であるためには、説明変数と誤差項の間に相関がないこと(E[X'ε] = 0)が必要である。「内生性」はこの条件が満たされない状態を指す。
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | df_2022 = df_b[df_b['年度'] == 2022].copy() df_sc = df_2022[['都道府県', '一般診療所数', '保健医療費', '地域']].dropna() fig, ax = plt.subplots(figsize=(9, 6)) for reg, grp in df_sc.groupby('地域'): ax.scatter(grp['一般診療所数'], grp['保健医療費'] / 1000, color=region_colors.get(reg, 'gray'), label=reg, s=60, alpha=0.8) for _, row in df_sc.iterrows(): ax.annotate(row['都道府県'][:2], (row['一般診療所数'], row['保健医療費'] / 1000), fontsize=6, alpha=0.6, xytext=(2, 2), textcoords='offset points') slope, intercept, r, p, _ = stats.linregress(df_sc['一般診療所数'], df_sc['保健医療費'] / 1000) xr = np.linspace(df_sc['一般診療所数'].min(), df_sc['一般診療所数'].max(), 100) ax.plot(xr, slope * xr + intercept, 'k--', linewidth=1.5, label=f'回帰直線 (r={r:.2f}, p={p:.3f})') ax.set_xlabel('一般診療所数(件)', fontsize=12) ax.set_ylabel('保健医療費(千円)', fontsize=12) ax.set_title('一般診療所数 vs 保健医療費(2022年, N=47)', fontsize=13, fontweight='bold') ax.legend(fontsize=8, ncol=2) ax.grid(alpha=0.3) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2022_U5_12_fig2_scatter.png'), dpi=150, bbox_inches='tight') plt.close() print("Fig2 saved") |
Fig2 saved
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。stats.linregress(x, y) — 単回帰の傾き・切片・r値・p値・標準誤差を返します。使わない値は _ で受け取り。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。2段階最小二乗法(2SLS)の中核は2つのステップに分かれる。第1段階で内生変数(診療所数)を操作変数(降水量)と外生変数で回帰し、第2段階でその予測値(外生的変動のみを含む部分)を用いて目的変数(保健医療費)を推定する。
操作変数が有効(relevant)であるためには、操作変数と内生変数の間に十分に強い相関が必要である。以下の散布図で「降水量 vs 診療所数」の関係を確認する。
条件1(関連性)はデータから検証可能。条件2・3は経済理論・ドメイン知識による正当化が必要。第1段階のF統計量が10未満の場合は「弱操作変数」として推定が不安定になる。
Python の linearmodels パッケージは statsmodels と互換性のある形式でパネルデータの IV 推定をサポートする。第1段階・第2段階の結果を明示的に確認できる。
96 97 98 99 100 101 | iv_vars = ['高齢化率', '消費支出_log', '人口_log'] df_iv = df_b[['医療費_log', '診療所数_log', '降水量_log'] + iv_vars].dropna() # OLS X_ols = sm.add_constant(df_iv[['診療所数_log'] + iv_vars]) res_ols = sm.OLS(df_iv['医療費_log'], X_ols).fit() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。102 103 104 105 106 107 108 109 110 111 112 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 | # 2SLS using linearmodels IV2SLS try: exog = sm.add_constant(df_iv[iv_vars]) endog = df_iv[['診療所数_log']] instruments = df_iv[['降水量_log']] mod_iv = IV2SLS(df_iv['医療費_log'], exog, endog, instruments) res_iv = mod_iv.fit(cov_type='unadjusted') # Compare 診療所数_log coefficient ols_coef = res_ols.params['診療所数_log'] ols_se = res_ols.bse['診療所数_log'] iv_coef = res_iv.params['診療所数_log'] iv_se = res_iv.std_errors['診療所数_log'] fig, ax = plt.subplots(figsize=(8, 5)) methods = ['OLS(内生性無視)', '2SLS(IV推定)'] coefs_c = [ols_coef, iv_coef] ses_c = [ols_se, iv_se] colors_c = ['#888888', '#e05c5c'] ax.barh(methods, coefs_c, xerr=[1.96 * s for s in ses_c], color=colors_c, alpha=0.8, error_kw={'elinewidth': 2, 'capsize': 6}) ax.axvline(0, color='black', linewidth=0.8) ax.set_xlabel('診療所数(log)の係数', fontsize=12) ax.set_title('OLS vs 2SLS:診療所数が保健医療費に与える効果\n(内生性バイアスの補正比較)', fontsize=12, fontweight='bold') ax.grid(axis='x', alpha=0.3) for i, (c, s) in enumerate(zip(coefs_c, ses_c)): ax.text(c + 0.02, i, f'{c:.3f}±{1.96*s:.3f}', va='center', fontsize=9) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2022_U5_12_fig3_iv_compare.png'), dpi=150, bbox_inches='tight') plt.close() print(f"Fig3 saved: OLS={ols_coef:.3f}, 2SLS={iv_coef:.3f}") print(res_iv.summary) except Exception as e: print(f"2SLS error: {e}") # Fallback: just OLS coefficients coefs = res_ols.params.drop('const') ses = res_ols.bse.drop('const') pvals = res_ols.pvalues.drop('const') fig, ax = plt.subplots(figsize=(8, 5)) colors_c = ['#e05c5c' if p < 0.05 else '#888888' for p in pvals] ax.barh(range(len(coefs)), coefs, xerr=1.96 * ses, color=colors_c, alpha=0.8, error_kw={'elinewidth': 1.5, 'capsize': 4}) ax.set_yticks(range(len(coefs))) ax.set_yticklabels(coefs.index, fontsize=10) ax.axvline(0, color='black', linewidth=0.8) ax.set_xlabel('OLS係数', fontsize=12) ax.set_title('保健医療費の決定要因(OLS)', fontsize=12, fontweight='bold') ax.grid(axis='x', alpha=0.3) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2022_U5_12_fig3_iv_compare.png'), dpi=150, bbox_inches='tight') plt.close() |
Fig3 saved: OLS=0.116, 2SLS=-0.293
IV-2SLS Estimation Summary
==============================================================================
Dep. Variable: 医療費_log R-squared: 0.2345
Estimator: IV-2SLS Adj. R-squared: 0.2290
No. Observations: 564 F-statistic: 280.32
Date: Mon, May 18 2026 P-value (F-stat) 0.0000
Time: 11:24:18 Distribution: chi2(4)
Cov. Estimator: unadjusted
Parameter Estimates
==============================================================================
Parameter Std. Err. T-stat P-value Lower CI Upper CI
------------------------------------------------------------------------------
const -3.8039 5.0747 -0.7496 0.4535 -13.750 6.1424
高齢化率 0.0143 0.0107 1.3329 0.1826 -0.0067 0.0352
消費支出_log 0.7817 0.1015 7.7017 0.0000 0.5827 0.9806
人口_log 0.3583 0.8074 0.4438 0.6572 -1.2243 1.9409
診療所数_log -0.2927 0.8163 -0.3585 0.7200 -1.8926 1.3073
==============================================================================
Endogenous: 診療所数_log
Instruments: 降水量_log
Unadjusted Covariance (Homoskedastic)
Debiased: Falsefig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。OLS と 2SLS の推定係数を比較することで、内生性バイアスの方向と大きさを定量的に評価できる。OLS が内生性バイアスを含む一方、2SLS は操作変数による外生的変動のみを利用するため、理論上は不偏推定量(あるいは一致推定量)を与える。
| 推定法 | 診療所数(log)の係数 | 標準誤差 | 解釈 |
|---|---|---|---|
| OLS | +0.116 | 小さい | 内生性バイアス込みの正の係数(逆因果・欠落変数の可能性) |
| 2SLS(IV) | −0.293 | 大きい(SE拡大は正常) | 内生性バイアス除去後の推定。操作変数の外生的変動から識別 |
操作変数が内生変数を十分に説明できない(弱操作変数:weak instrument)場合、2SLS推定量は有限サンプルで大きなバイアスを持ち、信頼区間が著しく広がる。第1段階のF統計量による診断が必須。
OLS と 2SLS の係数が統計的に有意に異なるかどうかを検定することで、内生性の有無を統計的に評価できる。
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | df_iv_sc = df_b[df_b['年度'] == 2022][['都道府県', '降水量(年間)', '一般診療所数', '地域']].dropna() fig, ax = plt.subplots(figsize=(9, 6)) for reg, grp in df_iv_sc.groupby('地域'): ax.scatter(grp['降水量(年間)'], grp['一般診療所数'], color=region_colors.get(reg, 'gray'), label=reg, s=60, alpha=0.8) for _, row in df_iv_sc.iterrows(): ax.annotate(row['都道府県'][:2], (row['降水量(年間)'], row['一般診療所数']), fontsize=6, alpha=0.6, xytext=(2, 2), textcoords='offset points') x_v = df_iv_sc['降水量(年間)'] y_v = df_iv_sc['一般診療所数'] slope, intercept, r, p, _ = stats.linregress(x_v, y_v) xr = np.linspace(x_v.min(), x_v.max(), 100) ax.plot(xr, slope * xr + intercept, 'k--', linewidth=1.5, label=f'相関: r={r:.2f} (p={p:.3f})') ax.set_xlabel('降水量・年間(mm)', fontsize=12) ax.set_ylabel('一般診療所数(件)', fontsize=12) ax.set_title('操作変数の妥当性確認:降水量 vs 一般診療所数(2022年)\n(第1段階の関連性)', fontsize=12, fontweight='bold') ax.legend(fontsize=8, ncol=2) ax.grid(alpha=0.3) plt.tight_layout() plt.savefig(os.path.join(FIG_DIR, '2022_U5_12_fig4_iv_valid.png'), dpi=150, bbox_inches='tight') plt.close() print("Fig4 saved") print("All done!") |
Fig4 saved All done!
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。stats.linregress(x, y) — 単回帰の傾き・切片・r値・p値・標準誤差を返します。使わない値は _ で受け取り。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。SSDSE-B の47都道府県 × 12年(2012〜2023年度)のパネルデータを用い、降水量を操作変数として一般診療所数の内生性を処理した2SLS推定を行った結果:
| データ | 出典 |
|---|---|
| SSDSE-B-2026(都道府県別パネルデータ) | 統計数理研究所 SSDSE(社会・人口統計体系)、2012〜2023年度 |
| 保健医療費・消費支出(二人以上の世帯) | 家計調査(総務省)都道府県別結果 |
| 一般診療所数・総人口・65歳以上人口 | 医療施設調査(厚生労働省)・国勢調査 都道府県別集計 |
| 降水量(年間) | 気象庁 都道府県別年間降水量統計 |
本コードは SSDSE-B-2026.csv の実公的データのみ使用。合成データ・乱数生成は一切使用していない。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。