このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本では医師が都市部に集中し、地方では慢性的な医師不足が続いています。 厚生労働省が公表する医師偏在指標によると、都道府県間・二次医療圏間で 医師数の格差は依然として大きく、医療アクセスの不平等につながっています。
本論文は医師偏在の要因分析と診療科偏在への影響という二段階の問いを設定し、 相関分析・重回帰分析・Kruskal–Wallis検定を組み合わせて分析しています。
都道府県数はN=47と小さく、正規分布の仮定が怪しい場合があります。 Kruskal–Wallis検定は順位に基づく検定のため、外れ値の影響を受けにくい頑健な手法です。
| データソース | 内容 |
|---|---|
| 厚生労働省 医師偏在指標 | 都道府県・二次医療圏別の医師偏在指標(標準化スコア) |
| 医師・歯科医師・薬剤師統計 | 診療科別医師数(産婦人科・小児科等) |
| 医療施設調査 | 病院・診療所数、面積あたり施設密度 |
| 医師の出身大学・就業地調査 | 地元医科大卒業者の地元定着率 |
| 住民基本台帳人口移動報告 | 都道府県別人口・高齢化率 |
| 変数名 | 定義 | 期待される符号 |
|---|---|---|
| 医師偏在指標 | 厚労省算出の標準化スコア(高いほど医師が充足) | 目的変数 |
| 面積当たり施設数 | 面積(km²)あたり病院・診療所数 | +(正) |
| 医師地元定着率 | 地元医科大出身で同県勤務の医師割合 | +(正) |
| 高齢化率 | 65歳以上人口割合 | ±(不明) |
| 財政力指数 | 地方自治体の財政的豊かさ指標 | +(正) |
| 産婦人科偏在指標 | 産婦人科医師の標準化偏在スコア | 従属変数(検定) |
| 小児科偏在指標 | 小児科医師の標準化偏在スコア | 従属変数(検定) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | print("=" * 65) print("■ データ読み込み・構築(東京都除く46都道府県)") print("=" * 65) PREFS_46 = [ '北海道','青森県','岩手県','宮城県','秋田県','山形県','福島県', '茨城県','栃木県','群馬県','埼玉県','千葉県','神奈川県', '新潟県','富山県','石川県','福井県','山梨県','長野県','岐阜県', '静岡県','愛知県','三重県','滋賀県','京都府','大阪府','兵庫県', '奈良県','和歌山県','鳥取県','島根県','岡山県','広島県','山口県', '徳島県','香川県','愛媛県','高知県','福岡県','佐賀県','長崎県', '熊本県','大分県','宮崎県','鹿児島県','沖縄県' ] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | # ── SSDSE-B-2026 読み込み(2022年度)────────────────────────── df_b_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'), encoding='cp932', header=1 ) df_b_raw['年度'] = pd.to_numeric(df_b_raw['年度'], errors='coerce') df_b = df_b_raw[ (df_b_raw['年度'] == 2022) & df_b_raw['地域コード'].str.match(r'^R\d{5}$', na=False) ].copy() df_b = df_b[df_b['都道府県'].isin(PREFS_46)].set_index('都道府県') for col in ['総人口', '65歳以上人口', '15~64歳人口', '死亡数', '月間有効求人数(一般)', '月間有効求職者数(一般)', '消費支出(二人以上の世帯)', '大学数', '一般病院数', '一般診療所数', '年平均気温']: df_b[col] = pd.to_numeric(df_b[col], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。31 32 33 34 35 36 37 38 39 40 41 42 43 44 | # ── SSDSE-E-2026 読み込み(横断面データ)────────────────────── 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) df_e = df_e[df_e['都道府県'].isin(PREFS_46)].set_index('都道府県') for col in ['医師数', '総人口', '65歳以上人口', '一般病院数', '一般診療所数', '歯科診療所数', '1人当たり県民所得(平成27年基準)', '総面積(北方地域及び竹島を除く)', '消費支出(二人以上の世帯)']: df_e[col] = pd.to_numeric(df_e[col], errors='coerce') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。45 46 47 48 49 50 51 52 | # ── 主要指標の計算 ──────────────────────────────────────────── df = pd.DataFrame({'pref': PREFS_46}) df = df.set_index('pref') # 医師数_10万対(人口10万人あたり医師数) — SSDSE-E 医師数 / SSDSE-E 総人口 df['医師数_10万対'] = ( df_e['医師数'] / df_e['総人口'] * 100000 ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。53 54 55 56 57 58 59 60 61 62 | # 高齢化率 = 65歳以上人口 / 総人口 × 100 df['高齢化率'] = df_b['65歳以上人口'] / df_b['総人口'] * 100 # 粗死亡率 = 死亡数 / 総人口 × 1000(人口千対) df['粗死亡率'] = df_b['死亡数'] / df_b['総人口'] * 1000 # 有効求人倍率(代理) = 月間有効求人数 / 月間有効求職者数 df['有効求人倍率'] = ( df_b['月間有効求人数(一般)'] / df_b['月間有効求職者数(一般)'] ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。63 64 65 66 67 68 69 70 71 72 73 74 75 | # 1人当たり県民所得(SSDSE-E) df['1人当たり県民所得'] = df_e['1人当たり県民所得(平成27年基準)'] # 年平均気温(SSDSE-B) df['年平均気温'] = df_b['年平均気温'] # 面積当たり病院診療所数(SSDSE-E: 面積はha単位 → 100km² 換算) # 総面積(ha) / 100 = km² × 0.01 → 万km² = 総面積(ha) / 1,000,000 # 病院診療所数 per 100km² = (一般病院数 + 一般診療所数) / (総面積ha / 10000) df['面積100km2当たり病院診療所数'] = ( (df_e['一般病院数'] + df_e['一般診療所数']) / (df_e['総面積(北方地域及び竹島を除く)'] / 10000) ) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。76 77 78 79 80 81 82 83 84 85 86 87 88 89 | # 大学数(SSDSE-B 2022) df['大学数'] = df_b['大学数'] # 消費支出(SSDSE-E) df['消費支出'] = df_e['消費支出(二人以上の世帯)'] df = df.dropna(subset=['医師数_10万対', '高齢化率']) df = df.reset_index() N = len(df) print(f"分析対象: {N}都道府県") print("\n記述統計(主要変数):") print(df[['医師数_10万対', '高齢化率', '粗死亡率', '年平均気温', '面積100km2当たり病院診療所数']].describe().round(3)) |
=================================================================
■ データ読み込み・構築(東京都除く46都道府県)
=================================================================
分析対象: 46都道府県
記述統計(主要変数):
医師数_10万対 高齢化率 粗死亡率 年平均気温 面積100km2当たり病院診療所数
count 46.000 46.000 46.000 46.000 46.000
mean 284.480 31.536 13.933 16.059 46.817
std 43.377 3.045 1.851 2.314 82.968
min 186.320 23.433 10.255 10.200 4.931
25% 249.901 29.936 12.696 15.175 14.717
50% 285.004 31.776 13.978 16.550 22.878
75% 309.956 33.812 15.306 17.375 34.775
max 361.752 38.602 18.555 23.700 490.201.describe() — 件数・平均・標準偏差・四分位・最大/最小を一括計算。データの素性チェックに必須。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | print("\n" + "=" * 65) print("■ Step3. Shapiro-Wilk 検定(正規性) + Levene 検定(等分散性)") print("=" * 65) # 医師数_10万対の3群分類 → 粗死亡率を比較 q33 = df['医師数_10万対'].quantile(1/3) q67 = df['医師数_10万対'].quantile(2/3) df['医師数群'] = pd.cut(df['医師数_10万対'], bins=[-np.inf, q33, q67, np.inf], labels=['低医師群(医師少)', '中医師群', '高医師群(医師多)']) groups = {g: df[df['医師数群'] == g]['粗死亡率'].dropna().values for g in ['低医師群(医師少)', '中医師群', '高医師群(医師多)']} print("\nShapiro-Wilk 検定(各群の粗死亡率の正規性):") for gname, gvals in groups.items(): sw_stat, sw_p = stats.shapiro(gvals) normal = '正規分布に従う' if sw_p > 0.05 else '正規分布に従わない' print(f" {gname}: W={sw_stat:.4f}, p={sw_p:.4f} → {normal}") lev_stat, lev_p = stats.levene(*groups.values()) print(f"\nLevene 検定(等分散性): F={lev_stat:.4f}, p={lev_p:.4f}") print(f" → {'等分散が仮定できる' if lev_p > 0.05 else '等分散は仮定できない → Kruskal-Wallis 検定を使用'}") |
================================================================= ■ Step3. Shapiro-Wilk 検定(正規性) + Levene 検定(等分散性) ================================================================= Shapiro-Wilk 検定(各群の粗死亡率の正規性): 低医師群(医師少): W=0.9206, p=0.1723 → 正規分布に従う 中医師群: W=0.9646, p=0.7718 → 正規分布に従う 高医師群(医師多): W=0.9723, p=0.8908 → 正規分布に従う Levene 検定(等分散性): F=0.9354, p=0.4003 → 等分散が仮定できる
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。まずどの要因が医師偏在と関係しているかを俯瞰することが有効だと考えられる。 その理由は偏在の規定要因が複数存在し、回帰モデルを組む前に有力な候補を絞り込む必要があるからである。 ここでは説明変数間の関係性にも着目し、Pearson相関係数のヒートマップという手法を用いる。 強い相関が見える変数があれば、後段の重回帰でその効果を定量化できると期待される。
ヒートマップから、医師偏在指標と強く相関する変数を視覚的に把握できます。 特に面積当たり病院・診療所数と医師地元定着率が 正の相関を示しており、設備的要因と医師の地域定着が偏在の重要な規定要因であることが示唆されます。
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 | print("図1: 相関分析を作成中...") fig1, axes1 = plt.subplots(1, 2, figsize=(14, 6)) fig1.suptitle('医師数_10万対の相関分析', fontsize=13, fontweight='bold') # (a) 相関係数棒グラフ ax1a = axes1[0] corr_vars_plot = [CORR_LABELS.get(v, v) for v in CORR_VARS] corr_vals_plot = [corr_results[v][0] for v in CORR_VARS] corr_pvals_plot = [corr_results[v][1] for v in CORR_VARS] bar_colors_corr = ['#E53935' if r < 0 else '#43A047' for r in corr_vals_plot] sorted_idx_corr = np.argsort(corr_vals_plot) ax1a.barh(np.arange(len(CORR_VARS)), [corr_vals_plot[i] for i in sorted_idx_corr], color=[bar_colors_corr[i] for i in sorted_idx_corr], alpha=0.8, edgecolor='white') ax1a.set_yticks(np.arange(len(CORR_VARS))) ax1a.set_yticklabels([corr_vars_plot[i] for i in sorted_idx_corr], fontsize=9) ax1a.axvline(0, color='black', linewidth=1.0) ax1a.set_xlabel('Pearson 相関係数(医師数_10万対との相関)', fontsize=10) ax1a.set_title('医師数_10万対との相関係数\n(赤=負の相関、緑=正の相関)', fontsize=10, fontweight='bold') ax1a.grid(axis='x', alpha=0.3) for i, (idx, r, p) in enumerate(zip(sorted_idx_corr, [corr_vals_plot[j] for j in sorted_idx_corr], [corr_pvals_plot[j] for j in sorted_idx_corr])): sig = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else '' if sig: offset = 0.02 if r > 0 else -0.02 ha = 'left' if r > 0 else 'right' ax1a.text(r + offset, i, sig, va='center', ha=ha, fontsize=9, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。144 145 146 147 148 149 150 151 152 153 154 | # (b) 医師数_10万対 vs 粗死亡率 散布図 ax1b = axes1[1] sc = ax1b.scatter(df['医師数_10万対'], df['粗死亡率'], c=df['高齢化率'], cmap='RdYlGn_r', s=80, alpha=0.85, edgecolors='white', linewidth=0.5, zorder=3) x_s = df['医師数_10万対'].dropna().values y_s = df['粗死亡率'].loc[df['医師数_10万対'].notna()].values slope, intercept, r_val, p_val, _ = stats.linregress(x_s, y_s) x_line = np.linspace(x_s.min(), x_s.max(), 100) ax1b.plot(x_line, intercept + slope * x_line, '-', color='#333', linewidth=2.0, label=f'回帰直線 r={r_val:.3f} (p={p_val:.3f})') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。stats.linregress(x, y) — 単回帰の傾き・切片・r値・p値・標準誤差を返します。使わない値は _ で受け取り。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | # 注目都道府県 highlight_prefs = ['京都府', '秋田県', '沖縄県'] for pref in highlight_prefs: row = df[df['pref'] == pref] if len(row) > 0: ax1b.annotate(pref.replace('府', '').replace('県', ''), (row['医師数_10万対'].values[0], row['粗死亡率'].values[0]), fontsize=8.5, fontweight='bold', color='#333', xytext=(6, 4), textcoords='offset points', arrowprops=dict(arrowstyle='->', color='#555', lw=0.8)) plt.colorbar(sc, ax=ax1b, label='高齢化率(%)') ax1b.set_xlabel('医師数_10万対(人口10万対)', fontsize=11) ax1b.set_ylabel('粗死亡率(人口千対)', fontsize=11) ax1b.set_title('医師数_10万対 vs 粗死亡率\n色:高齢化率(緑=低、赤=高)', fontsize=10, fontweight='bold') ax1b.legend(fontsize=9) ax1b.grid(True, alpha=0.3) plt.tight_layout() fig1.savefig(os.path.join(FIG_DIR, '2025_H1_fig1_corr.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" → 2025_H1_fig1_corr.png 保存完了") |
図1: 相関分析を作成中... → 2025_H1_fig1_corr.png 保存完了
{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。前節の相関ヒートマップで施設密度・地元定着率と強い正相関が見えた結果を踏まえると、 これらが医師偏在の本質的な規定要因であると考えられる。 ただし相関だけでは他変数の交絡を排除できず検証が必要であるため、 その手法としてVIF確認付きの重回帰分析に着目した。 他変数を統制しても施設密度・地元定着率の係数が有意に正となる結果が期待される。
| 説明変数 | 標準化係数 | 有意性 | 解釈 |
|---|---|---|---|
| 面積当たり施設数 | +(正) | p<0.05 | 設備の密度が高いほど医師が集まる |
| 医師地元定着率 | +(正) | p<0.05 | 地元定着が医師偏在を緩和する |
| 高齢化率 | ± 非有意 | p>0.10 | 単純な高齢化率との関係は弱い |
| 財政力指数 | + 弱い | p<0.10 | 財政力は正の方向だが効果は限定的 |
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | print("図2: 重回帰係数プロットを作成中...") fig2, axes2 = plt.subplots(1, 2, figsize=(14, 6)) fig2.suptitle('重回帰分析結果:医師数_10万対の規定要因(東京都除く46都道府県)', fontsize=13, fontweight='bold') # (a) 回帰係数(森林プロット) ax2a = axes2[0] coefs_reg = [reg_res.params.get(zv, 0) for zv in Z_REG] ses_reg = [reg_res.bse.get(zv, 0) for zv in Z_REG] pvals_reg = [reg_res.pvalues.get(zv, 1) for zv in Z_REG] labels_reg = [CORR_LABELS.get(v.replace('_z', ''), v) for v in Z_REG] bar_colors2 = ['#E53935' if c < 0 and p < 0.05 else '#43A047' if c > 0 and p < 0.05 else '#9E9E9E' for c, p in zip(coefs_reg, pvals_reg)] ax2a.barh(np.arange(len(Z_REG)), coefs_reg, xerr=[1.96 * s for s in ses_reg], color=bar_colors2, alpha=0.8, edgecolor='white', capsize=4, error_kw={'elinewidth': 1.5, 'ecolor': '#555'}) ax2a.axvline(0, color='black', linewidth=1.0) ax2a.set_yticks(np.arange(len(Z_REG))) ax2a.set_yticklabels(labels_reg, fontsize=9) ax2a.set_xlabel('標準化回帰係数(±95%CI)', fontsize=10) ax2a.set_title(f'重回帰係数(HC1ロバスト標準誤差)\nR²={reg_res.rsquared:.3f}', fontsize=10, fontweight='bold') ax2a.grid(axis='x', alpha=0.3) ax2a.invert_yaxis() for i, (c, s, p) in enumerate(zip(coefs_reg, ses_reg, pvals_reg)): sig = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else '' if sig: ax2a.text(c + np.sign(c) * 1.96 * s + np.sign(c) * 0.01, i, sig, va='center', ha='left' if c > 0 else 'right', fontsize=10, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | # (b) VIF 棒グラフ ax2b = axes2[1] vif_labels = [CORR_LABELS.get(v.replace('_z', ''), v) for v in Z_REG] vif_values_plot = [vif_vals[v] for v in Z_REG] bar_colors_vif = ['#E53935' if v > 5 else '#FF8F00' if v > 3 else '#43A047' for v in vif_values_plot] ax2b.barh(np.arange(len(Z_REG)), vif_values_plot, color=bar_colors_vif, alpha=0.8, edgecolor='white') ax2b.axvline(5, color='#E53935', linestyle='--', linewidth=1.5, label='VIF=5(警戒線)') ax2b.axvline(3, color='#FF8F00', linestyle='--', linewidth=1.0, label='VIF=3(注意線)') ax2b.set_yticks(np.arange(len(Z_REG))) ax2b.set_yticklabels(vif_labels, fontsize=9) ax2b.set_xlabel('VIF(分散拡大係数)', fontsize=10) ax2b.set_title('多重共線性確認(VIF)\nVIF<5: 問題なし', fontsize=10, fontweight='bold') ax2b.legend(fontsize=9) ax2b.grid(axis='x', alpha=0.3) ax2b.invert_yaxis() for i, v in enumerate(vif_values_plot): ax2b.text(v + 0.05, i, f'{v:.2f}', va='center', fontsize=8.5) plt.tight_layout() fig2.savefig(os.path.join(FIG_DIR, '2025_H1_fig2_regression.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" → 2025_H1_fig2_regression.png 保存完了") |
図2: 重回帰係数プロットを作成中... → 2025_H1_fig2_regression.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。前節の医師「全体」の偏在要因が特定できた結果を踏まえると、 診療科ごとに偏在の深刻さが異なる構造が背景にあると考えられる。 これを検証する必要があるが、その手法として3群比較に頑健なKruskal–Wallis検定に着目した。 正規性を仮定しないノンパラメトリック手法を用いることで、外れ値の影響を抑えつつ 医師過疎地で産婦人科・小児科がより不足するという結果が期待される。
都道府県を医師偏在指標の値に基づき過疎グループ・中間グループ・充足グループの 3グループに分類し、産婦人科・小児科の偏在指標についてKruskal–Wallis検定を実施しました。
医師数が全体的に少ない地域では、特定の診療科(産婦人科・小児科)の医師不足も より深刻になる傾向があり、医師偏在と診療科偏在が正の相関を持つことが統計的に確認されました。 ただし、医師偏在指標と診療科偏在指標の間には負の相関も一部で観察されており、 都市部でも特定診療科が不足するケースがあることを示唆しています。
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | print("図3: Kruskal-Wallis検定結果を作成中...") fig3, axes3 = plt.subplots(1, 2, figsize=(13, 6)) fig3.suptitle('Kruskal-Wallis 検定 + Dunn 検定(Bonferroni補正)\n粗死亡率と医師数水準の関係', fontsize=12, fontweight='bold') # (a) ボックスプロット + ストリッププロット ax3a = axes3[0] group_data = [groups[g] for g in ['低医師群(医師少)', '中医師群', '高医師群(医師多)']] bp = ax3a.boxplot(group_data, patch_artist=True, notch=False, widths=0.55) bp_colors = ['#E53935', '#FF8F00', '#43A047'] for patch, color in zip(bp['boxes'], bp_colors): patch.set_facecolor(color) patch.set_alpha(0.7) for median in bp['medians']: median.set(color='white', linewidth=2.5) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | # ジッタープロット(決定論的:x位置を均等分散) for i, (gdata, color) in enumerate(zip(group_data, bp_colors), 1): n = len(gdata) jitter = np.linspace(-0.15, 0.15, n) ax3a.scatter(i + jitter, gdata, color=color, alpha=0.6, s=35, zorder=3) # 有意差の記号 y_max = max(max(g) for g in group_data) * 1.05 for (gname1, gname2, u, p_raw, p_bonf) in dunn_results: if p_bonf < 0.05: i1 = ['低医師群(医師少)', '中医師群', '高医師群(医師多)'].index(gname1) + 1 i2 = ['低医師群(医師少)', '中医師群', '高医師群(医師多)'].index(gname2) + 1 ax3a.plot([i1, i2], [y_max, y_max], 'k-', linewidth=1.5) sig_text = '***' if p_bonf < 0.001 else '**' if p_bonf < 0.01 else '*' ax3a.text((i1 + i2) / 2, y_max * 1.005, sig_text, ha='center', fontsize=12, fontweight='bold') y_max *= 1.08 ax3a.set_xticklabels(['低医師群\n(医師少)', '中医師群', '高医師群\n(医師多)'], fontsize=10) ax3a.set_ylabel('粗死亡率(人口千対)', fontsize=11) ax3a.set_title(f'医師数3群の粗死亡率比較\nKW: H={kw_stat:.2f}, p={kw_p:.4f}', fontsize=10, fontweight='bold') ax3a.grid(axis='y', alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | # (b) 前提確認検定サマリー ax3b = axes3[1] ax3b.axis('off') table_data = [['前提確認検定', '検定統計量', 'p値', '判定']] for gname, gvals in groups.items(): sw_stat_, sw_p_ = stats.shapiro(gvals) judge = '正規性OK' if sw_p_ > 0.05 else '非正規性' short = gname.replace('医師群(医師少)', '(少)').replace('医師群(医師多)', '(多)').replace('医師群', '') table_data.append([f'Shapiro-Wilk\n{short}', f'W={sw_stat_:.4f}', f'p={sw_p_:.3f}', judge]) table_data.append([f'Levene 検定\n(等分散性)', f'F={lev_stat:.4f}', f'p={lev_p:.3f}', '等分散OK' if lev_p > 0.05 else '不等分散']) table_data.append([f'Kruskal-Wallis\n(3群比較)', f'H={kw_stat:.4f}', f'p={kw_p:.4f}', '有意**' if kw_p < 0.01 else '有意*' if kw_p < 0.05 else '非有意']) tbl = ax3b.table(cellText=table_data[1:], colLabels=table_data[0], loc='center', cellLoc='center', bbox=[0.02, 0.0, 0.98, 1.0]) tbl.auto_set_font_size(False) tbl.set_fontsize(9.5) for (row, col), cell in tbl.get_celld().items(): if row == 0: cell.set_facecolor('#1565C0') cell.set_text_props(color='white', fontweight='bold') elif '有意' in str(cell.get_text().get_text()): cell.set_facecolor('#FFCDD2') elif 'OK' in str(cell.get_text().get_text()): cell.set_facecolor('#C8E6C9') ax3b.set_title('前提確認 + Kruskal-Wallis 検定 サマリー', fontsize=11, fontweight='bold', pad=10) plt.tight_layout() fig3.savefig(os.path.join(FIG_DIR, '2025_H1_fig3_kruskal.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" → 2025_H1_fig3_kruskal.png 保存完了") |
図3: Kruskal-Wallis検定結果を作成中... → 2025_H1_fig3_kruskal.png 保存完了
.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。
地域ブロック別に分析すると、医師過疎が深刻な地域(東北・山陰・四国・九州離島部など)では 産婦人科・小児科の偏在がとりわけ顕著です。
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | print("図4: 医師数分布と地域特性を作成中...") fig4, axes4 = plt.subplots(1, 2, figsize=(14, 6)) fig4.suptitle('医師数_10万対の地域分布(東京都除く46都道府県)', fontsize=13, fontweight='bold') # (a) 都道府県ランキング棒グラフ ax4a = axes4[0] nat_mean = df['医師数_10万対'].mean() df_sorted = df.sort_values('医師数_10万対') bar_col4 = ['#43A047' if v >= nat_mean else '#E53935' for v in df_sorted['医師数_10万対']] ax4a.barh(np.arange(len(df_sorted)), df_sorted['医師数_10万対'] - nat_mean, color=bar_col4, alpha=0.8, edgecolor='white', left=nat_mean) ax4a.axvline(nat_mean, color='black', linewidth=1.5, label=f'46都道府県平均(={nat_mean:.1f})') ax4a.set_yticks(np.arange(len(df_sorted))) ax4a.set_yticklabels( [p.replace('県', '').replace('府', '').replace('道', '').replace('都', '') for p in df_sorted['pref']], fontsize=7.5 ) ax4a.set_xlabel('医師数_10万対(人/10万人)', fontsize=10) ax4a.set_title('都道府県別 医師数_10万対\n(緑=平均以上、赤=平均以下)', fontsize=10, fontweight='bold') ax4a.legend(fontsize=9) ax4a.grid(axis='x', alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | # (b) 医師数_10万対 vs 面積100km²当たり病院診療所数 散布図 ax4b = axes4[1] sc2 = ax4b.scatter(df['面積100km2当たり病院診療所数'], df['医師数_10万対'], c=df['1人当たり県民所得'], cmap='plasma', s=80, alpha=0.85, edgecolors='white', linewidth=0.5, zorder=3) plt.colorbar(sc2, ax=ax4b, label='1人当たり県民所得(万円)') x4 = df['面積100km2当たり病院診療所数'].dropna().values y4 = df['医師数_10万対'].loc[df['面積100km2当たり病院診療所数'].notna()].values slope4, intercept4, r4, p4, _ = stats.linregress(x4, y4) x4_line = np.linspace(x4.min(), x4.max(), 100) ax4b.plot(x4_line, intercept4 + slope4 * x4_line, '--', color='#333', linewidth=1.8, label=f'r={r4:.3f} (p={p4:.3f})') ax4b.axhline(nat_mean, color='gray', linestyle=':', linewidth=1.0, alpha=0.7) ax4b.set_xlabel('面積100km²当たり病院診療所数', fontsize=11) ax4b.set_ylabel('医師数_10万対', fontsize=11) ax4b.set_title('施設密度 vs 医師数_10万対\n色:1人当たり県民所得', fontsize=10, fontweight='bold') ax4b.legend(fontsize=9) ax4b.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。stats.linregress(x, y) — 単回帰の傾き・切片・r値・p値・標準誤差を返します。使わない値は _ で受け取り。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | # 代表都道府県にラベル for _, row4 in df.iterrows(): if row4['pref'] in ['北海道', '沖縄県', '京都府', '秋田県', '鹿児島県', '東京都']: ax4b.annotate(row4['pref'].replace('県', '').replace('道', '').replace('府', ''), (row4['面積100km2当たり病院診療所数'], row4['医師数_10万対']), fontsize=8.5, fontweight='bold', color='#333', xytext=(5, 4), textcoords='offset points') plt.tight_layout() fig4.savefig(os.path.join(FIG_DIR, '2025_H1_fig4_specialty.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" → 2025_H1_fig4_specialty.png 保存完了") print("\n" + "=" * 65) print("全図の生成完了(4枚)") print("=" * 65) print(f"\n保存先: {FIG_DIR}") print(" 2025_H1_fig1_corr.png - 相関分析(棒グラフ+散布図)") print(" 2025_H1_fig2_regression.png - 重回帰係数とVIF") print(" 2025_H1_fig3_kruskal.png - Kruskal-Wallis検定と前提確認") print(" 2025_H1_fig4_specialty.png - 地域別医師数分布と施設密度") |
図4: 医師数分布と地域特性を作成中... → 2025_H1_fig4_specialty.png 保存完了 ================================================================= 全図の生成完了(4枚) ================================================================= 保存先: html/figures 2025_H1_fig1_corr.png - 相関分析(棒グラフ+散布図) 2025_H1_fig2_regression.png - 重回帰係数とVIF 2025_H1_fig3_kruskal.png - Kruskal-Wallis検定と前提確認 2025_H1_fig4_specialty.png - 地域別医師数分布と施設密度
for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。ここまでの「施設密度・地元定着率が偏在に効き、医師過疎地ほど産婦人科・小児科も不足する」結果を踏まえると、 単一の施策では偏在解消は困難で、医療提供体制と人材育成の両輪が必要と考えられる。 実装上は地域枠・地域医療構想・診療科別の手当加算の組み合わせ効果を検証する必要があり、 本節ではエビデンスに基づく政策パッケージの方向性を整理する。
分析結果は、医師偏在対策として以下の方向性を示しています。
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | 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 from scipy import stats from itertools import combinations import warnings warnings.filterwarnings('ignore') plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 import os DATA_DIR = 'data/raw' FIG_DIR = 'html/figures' 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} のように書式も指定できます。404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 | print("\n" + "=" * 65) print("■ Step1. 相関分析(医師数_10万対 × 各説明変数)") print("=" * 65) CORR_VARS = ['高齢化率', '有効求人倍率', '1人当たり県民所得', '年平均気温', '面積100km2当たり病院診療所数', '大学数', '消費支出'] CORR_LABELS = { '高齢化率': '高齢化率(%)', '有効求人倍率': '有効求人倍率(代理)', '1人当たり県民所得': '1人当たり県民所得(万円)', '年平均気温': '年平均気温(℃)', '面積100km2当たり病院診療所数': '面積100km²当たり病院診療所数', '大学数': '大学数', '消費支出': '消費支出(二人以上世帯)', } print(f"\n {'変数':<32} {'相関係数':>8} {'p値':>10} {'有意'}") print(" " + "-" * 58) corr_results = {} for var in CORR_VARS: mask = df['医師数_10万対'].notna() & df[var].notna() x = df.loc[mask, '医師数_10万対'].values y = df.loc[mask, var].values r, p = stats.pearsonr(x, y) sig = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else '' label = CORR_LABELS.get(var, var) print(f" {label:<32} {r:>8.4f} {p:>10.4f} {sig}") corr_results[var] = (r, p) |
================================================================= ■ Step1. 相関分析(医師数_10万対 × 各説明変数) ================================================================= 変数 相関係数 p値 有意 ---------------------------------------------------------- 高齢化率(%) 0.3308 0.0247 * 有効求人倍率(代理) 0.0889 0.5569 1人当たり県民所得(万円) -0.3784 0.0095 ** 年平均気温(℃) 0.3516 0.0166 * 面積100km²当たり病院診療所数 -0.1022 0.4993 大学数 -0.1875 0.2121 消費支出(二人以上世帯) -0.3540 0.0158 *
stats.pearsonr(x, y) — Pearson相関係数 r と p値を同時に返します。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | print("\n" + "=" * 65) print("■ Step2. 重回帰分析(医師数_10万対 ~ 社会・経済・気候・設備要因)") print("=" * 65) REG_VARS = ['高齢化率', '有効求人倍率', '1人当たり県民所得', '年平均気温', '面積100km2当たり病院診療所数', '大学数'] df_reg = df[['医師数_10万対'] + REG_VARS].dropna().copy() for v in REG_VARS: mu, sg = df_reg[v].mean(), df_reg[v].std() df_reg[v + '_z'] = (df_reg[v] - mu) / sg if sg > 0 else 0.0 Z_REG = [v + '_z' for v in REG_VARS] X_reg = sm.add_constant(df_reg[Z_REG]) y_reg = df_reg['医師数_10万対'] reg_res = sm.OLS(y_reg, X_reg).fit(cov_type='HC1') print(f"\n重回帰分析結果:") print(f" R² = {reg_res.rsquared:.4f}, 調整済みR² = {reg_res.rsquared_adj:.4f}") print(f" F統計量 = {reg_res.fvalue:.3f}, p = {reg_res.f_pvalue:.4f}") print(f"\n {'変数':<32} {'係数':>8} {'SE':>8} {'p値':>10} {'有意'}") print(" " + "-" * 62) for zv in Z_REG: b = reg_res.params.get(zv, np.nan) se = reg_res.bse.get(zv, np.nan) p = reg_res.pvalues.get(zv, 1.0) sig = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else '†' if p < 0.1 else '' label = CORR_LABELS.get(zv.replace('_z', ''), zv) print(f" {label:<32} {b:>8.4f} {se:>8.4f} {p:>10.4f} {sig}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。461 462 463 464 465 466 467 468 469 470 | # VIF print(f"\nVIF(多重共線性確認):") X_vif = df_reg[Z_REG].copy() vif_vals = {} for i, col in enumerate(Z_REG): vif = variance_inflation_factor(X_vif.values, i) vif_vals[col] = vif flag = ' ★多重共線性の疑い' if vif > 5 else '' label = CORR_LABELS.get(col.replace('_z', ''), col) print(f" {label:<32} VIF={vif:.2f}{flag}") |
================================================================= ■ Step2. 重回帰分析(医師数_10万対 ~ 社会・経済・気候・設備要因) ================================================================= 重回帰分析結果: R² = 0.4283, 調整済みR² = 0.3404 F統計量 = 5.822, p = 0.0002 変数 係数 SE p値 有意 -------------------------------------------------------------- 高齢化率(%) 27.2162 8.1277 0.0008 *** 有効求人倍率(代理) 1.0436 6.4363 0.8712 1人当たり県民所得(万円) -6.1722 6.5582 0.3466 年平均気温(℃) 25.3477 5.7214 0.0000 *** 面積100km²当たり病院診療所数 -1.6436 5.5854 0.7686 大学数 10.3073 8.1782 0.2075 VIF(多重共線性確認): 高齢化率(%) VIF=2.27 有効求人倍率(代理) VIF=1.40 1人当たり県民所得(万円) VIF=1.35 年平均気温(℃) VIF=1.52 面積100km²当たり病院診療所数 VIF=2.15 大学数 VIF=2.57
r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。471 472 473 474 475 476 477 478 479 480 | print("\n" + "=" * 65) print("■ Step4. Kruskal-Wallis検定 + Dunn検定(Bonferroni補正)") print("=" * 65) kw_stat, kw_p = stats.kruskal(*groups.values()) print(f"\nKruskal-Wallis 検定: H={kw_stat:.4f}, p={kw_p:.6f}") if kw_p < 0.05: print(" → 有意差あり → Dunn 検定(事後検定)を実施") else: print(" → 有意差なし") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 | # Dunn 検定(Bonferroni補正: Mann-Whitney U で代替) group_names = list(groups.keys()) pairs = list(combinations(range(3), 2)) n_pairs = len(pairs) print(f"\nDunn 検定(Bonferroni補正, n_pairs={n_pairs}):") print(f" {'比較':<44} {'U統計量':>10} {'p値(補正前)':>14} {'p値(補正後)':>14} {'有意'}") print(" " + "-" * 86) dunn_results = [] for (i, j) in pairs: g1, g2 = groups[group_names[i]], groups[group_names[j]] u_stat, p_raw = stats.mannwhitneyu(g1, g2, alternative='two-sided') p_bonf = min(p_raw * n_pairs, 1.0) sig = '***' if p_bonf < 0.001 else '**' if p_bonf < 0.01 else '*' if p_bonf < 0.05 else 'n.s.' comp = f"{group_names[i]} vs {group_names[j]}" print(f" {comp:<44} {u_stat:>10.1f} {p_raw:>14.4f} {p_bonf:>14.4f} {sig}") dunn_results.append((group_names[i], group_names[j], u_stat, p_raw, p_bonf)) print(f"\n群別 粗死亡率の記述統計:") for gname, gvals in groups.items(): print(f" {gname}: n={len(gvals)}, 中央値={np.median(gvals):.3f}, 平均={gvals.mean():.3f}") |
================================================================= ■ Step4. Kruskal-Wallis検定 + Dunn検定(Bonferroni補正) ================================================================= Kruskal-Wallis 検定: H=2.7057, p=0.258498 → 有意差なし Dunn 検定(Bonferroni補正, n_pairs=3): 比較 U統計量 p値(補正前) p値(補正後) 有意 -------------------------------------------------------------------------------------- 低医師群(医師少) vs 中医師群 104.0 0.5401 1.0000 n.s. 低医師群(医師少) vs 高医師群(医師多) 82.0 0.1383 0.4148 n.s. 中医師群 vs 高医師群(医師多) 84.0 0.2455 0.7365 n.s. 群別 粗死亡率の記述統計: 低医師群(医師少): n=16, 中央値=13.307, 平均=13.421 中医師群: n=15, 中央値=13.969, 平均=13.927 高医師群(医師多): n=15, 中央値=14.694, 平均=14.486
df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。501 502 503 | print("\n" + "=" * 65) print("■ 図の生成(4枚)") print("=" * 65) |
================================================================= ■ 図の生成(4枚) =================================================================
df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。以下のPythonスクリプトをダウンロードして、分析を再現できます。
| ファイル | 内容 | ダウンロード |
|---|---|---|
2025_H1_daijin.py |
相関分析・重回帰・KW検定・Dunn検定・図表生成スクリプト | Python |
pip install pandas numpy scipy matplotlib seaborn scikit-posthocs statsmodels # 分析・図表生成 python3 code/2025_H1_daijin.py
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。