このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
日本の公立学校教員による精神疾患を理由とした休職は近年増加傾向にあり、 教育現場における深刻な人材確保問題となっている。文部科学省の調査によれば、 精神疾患による休職者数は2000年代以降、一貫して高い水準で推移している。
まず「教員の精神疾患休職の要因と教育政策への示唆」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
2011-2023年の13時点を対象にVARモデルで各指標間の 時系列的な因果関係を探る。「不登校率の増加が翌年の教員休職率を上昇させるのか」を Granger因果性検定で検証。
47都道府県のクロスセクションデータを使い、対数変換OLS で精神疾患休職率の地域差を説明する要因を特定。 不均一分散検定・多重共線性診断を実施。
| 変数 | 内容 | 変換 | 出典 |
|---|---|---|---|
| 休職率変化率 | 精神疾患による教員休職率(千人当たり) | 対前年変化率 | 文部科学省・SSDSE |
| 不登校率変化率 | 小中学校の不登校児童生徒数(千人当たり) | 対前年変化率 | 文部科学省 |
| 暴力発生率変化率 | 学校内暴力行為の発生件数(千人当たり) | 対前年変化率 | 文部科学省 |
| いじめ発生率変化率 | いじめ認知件数(千人当たり) | 対前年変化率 | 文部科学省 |
| 採用試験倍率差分 | 公立学校教員採用試験の競争倍率 | 前年差分 | 文部科学省 |
| 記号 | 変数名 | 変換 | 役割 |
|---|---|---|---|
| Y | 精神疾患による教員休職率 | ln(Y)【被説明変数】 | 目的変数 |
| A | 不登校率 | ln(A) | 子ども側変数 |
| B | 暴力発生率 | ln(B) | 子ども側変数 |
| C | いじめ発生率 | ln(C) | 子ども側変数 |
| D | 学力偏差値(全国学力調査) | そのまま | 地域の学習環境 |
| E | 母子・父子世帯率 | ln(E) | 家庭の困難さ |
| F | 児童対受験者率(採用競争率) | ln(F) | 採用競争の激しさ |
| G | 教員一人当たり児童数 | そのまま | 業務負荷の代理 |
| H | 人口密度(中心化後) | ln(H)-平均【中心化】 | 都市度 |
| H² | 人口密度の二乗項 | (中心化ln(H))² | U字型関係を検出 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | print("=" * 65) print("■ データ読み込み: SSDSE-B-2026.csv(47都道府県、2012-2023年)") print("=" * 65) df_raw = pd.read_csv( os.path.join(DATA_DIR, 'SSDSE-B-2026.csv'), encoding='cp932', header=1 ) # 47都道府県のみ抽出(R + 5桁数字) df_pref = df_raw[df_raw['地域コード'].str.match(r'^R\d{5}$', na=False)].copy() # 数値変換 for col in ['合計特殊出生率', '婚姻件数', '15~64歳人口', '総人口', '65歳以上人口', '15歳未満人口']: df_pref[col] = pd.to_numeric(df_pref[col], errors='coerce') # 婚姻率 = 婚姻件数 / 15~64歳人口 × 1000(労働年齢人口千人あたり婚姻件数) df_pref['婚姻率'] = df_pref['婚姻件数'] / df_pref['15~64歳人口'] * 1000 print(f"データ件数: {len(df_pref)}({df_pref['年度'].nunique()}年 × {df_pref['都道府県'].nunique()}都道府県)") print(f"年度: {sorted(df_pref['年度'].unique())}") |
================================================================= ■ データ読み込み: SSDSE-B-2026.csv(47都道府県、2012-2023年) ================================================================= データ件数: 564(12年 × 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)]
pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。まず時間的先行性を確認することが有効だと考えられる。 その理由は「不登校が増えたから教員が休職した」のか「休職した教員が増えたから対応できず不登校が増えた」のかを区別する必要があるからである。 ここでは複数時系列の相互依存に着目し、VARモデルとGranger因果性検定を用いる。 不登校率の変化が翌期の休職率を予測するという時間的先行性が確認される結果が期待される。
VAR(Vector Autoregression:ベクトル自己回帰)モデルは、 複数の時系列変数が互いに影響し合う関係を同時に推定するモデルである。 単変量の AR モデルを多変量に拡張したもので、どの変数がどの変数を予測するかを データから学習する。
# statsmodels による VAR(1) 推定 from statsmodels.tsa.vector_ar.var_model import VAR model = VAR(df_ts) # df_ts: 5変数の時系列 DataFrame result = model.fit(maxlags=1) # ラグ1期のVARを推定 print(result.summary())
Granger因果性(グレンジャー因果性)とは、「変数Xの過去の値が 変数Yの将来の予測に役立つとき、XはYをGranger的に引き起こす」という概念である。 これは因果関係そのものではなく予測力の有無を検定する。
# Granger因果性検定(statsmodels) gc = result.test_causality( '休職率変化率', causing='不登校率変化率', kind='wald' # Wald検定(chi-squared統計量) ) print(f"Wald統計量: {gc.test_statistic:.3f}") print(f"p値: {gc.pvalue:.4f}") print(f"自由度: {gc.df}")
インパルス応答関数(IRF: Impulse Response Function)は、
あるショック(例:不登校率が1単位突然増加)が将来の他変数に与える動態的な影響を
視覚化したものである。
Bootstrap信頼区間は、パラメータ推定の不確実性を考慮した区間であり、
1000回の再サンプリング(推定→シミュレーション→IRF計算)で構築する。
# IRF計算とBootstrap信頼区間 irf = result.irf(periods=10) # irfs の形状: (periods+1, n_vars, n_vars) # irfs[h, i, j] = 変数iのh期後の応答 ← 変数jへの1単位ショック irf_vals = irf.irfs[:, 0, 1] # 変数0(休職率) ← 変数1(不登校率) # パラメトリックBootstrapでCI構築 def parametric_irf_bootstrap(var_res, n_periods=10, n_boot=1000): boot_irfs = [] for _ in range(n_boot): # 推定済みモデルからシミュレーション sim = simulate_var(var_res) r_b = VAR(sim).fit(1) boot_irfs.append(r_b.irf(n_periods).irfs[:, 0, 1]) lower = np.percentile(boot_irfs, 2.5, axis=0) upper = np.percentile(boot_irfs, 97.5, axis=0) return lower, upper
25 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 | print("\n図1: 時系列推移プロットを作成中...") fig1, ax1a = plt.subplots(figsize=(10, 5)) ax1b = ax1a.twinx() years_ts = annual.index.values l1 = ax1a.plot(years_ts, annual['TFR'], color=COLORS['primary'], linewidth=2.5, marker='o', markersize=7, markerfacecolor='white', markeredgewidth=2, label='合計特殊出生率(TFR)') l2 = ax1b.plot(years_ts, annual['婚姻率'], color=COLORS['secondary'], linewidth=2.5, marker='s', markersize=7, markerfacecolor='white', markeredgewidth=2, linestyle='--', label='婚姻率(千人あたり)') ax1a.set_xlabel('年度', fontsize=12) ax1a.set_ylabel('合計特殊出生率(TFR)', fontsize=12, color=COLORS['primary']) ax1b.set_ylabel('婚姻率(15-64歳人口千人あたり)', fontsize=12, color=COLORS['secondary']) ax1a.tick_params(axis='y', labelcolor=COLORS['primary']) ax1b.tick_params(axis='y', labelcolor=COLORS['secondary']) ax1a.set_title('全国平均 合計特殊出生率(TFR)と婚姻率の推移(2012-2023年)\n' 'データ出典: SSDSE-B-2026(e-Stat)', fontsize=12, fontweight='bold') ax1a.set_xlim(2011.5, 2023.5) ax1a.grid(True, alpha=0.3) lines = l1 + l2 labels = [l.get_label() for l in lines] ax1a.legend(lines, labels, loc='upper right', fontsize=10) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。55 56 57 58 59 60 61 62 63 64 | # 注釈 ax1a.annotate('COVID-19\n(2020年)', xy=(2020, annual.loc[2020, 'TFR']), xytext=(2020.5, annual.loc[2020, 'TFR'] + 0.04), arrowprops=dict(arrowstyle='->', color='gray', lw=1.2), fontsize=8, color='gray') plt.tight_layout() fig1.savefig(os.path.join(FIGURE_DIR, '2025_U2_fig1_trend.png'), bbox_inches='tight', dpi=150) plt.close(fig1) print(" → 2025_U2_fig1_trend.png 保存完了") |
図1: 時系列推移プロットを作成中... → 2025_U2_fig1_trend.png 保存完了
x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。前節のVARで「不登校→休職」の時間的先行性が確認された結果を踏まえると、 地域ごとの教育環境・社会的困難の違いも休職率に効いている可能性が背景にあると考えられる。 これを検証する必要があるが、その手法として対数変換と中心化二次項を含む横断面OLSに着目した。 都市部・農村部の両端で休職率が高まるU字型関係が観察される結果が期待される。
β₂ = ∂ln(Y)/∂ln(E) ≈ % change in Y per 1% change in E論文は人口密度について線形項(H)と二次項(H²)を同時投入している。 これは「都市でも農村でも休職率が高い」というU字型関係を想定した設定である。 二次項を含む回帰では多重共線性(H と H² の相関が高くなる)が問題になるため、 変数を平均で中心化(センタリング)してから二乗する。
# 人口密度の中心化と二次項の作成 ln_pop = np.log(population_density) ln_pop_c = ln_pop - ln_pop.mean() # 中心化 ln_pop_c2 = ln_pop_c ** 2 # 二次項 # 線形項H: 中心化の値 → H=0のとき平均的な人口密度 # 二次項H²: U字型の検出。係数が正→最小値を持つU字型
OLS推定では均一分散(homoskedasticity)の仮定が必要であるが、 都道府県データでは都市部と地方で分散が異なることが多い(不均一分散)。 Breusch-Pagan検定は「残差の二乗が説明変数で説明できるか」を検定し、 不均一分散の有無を診断する。
from statsmodels.stats.diagnostic import het_breuschpagan # 残差に対して実行 ols_result = sm.OLS(y, X).fit() bp_stat, bp_pval, f_stat, f_pval = het_breuschpagan( ols_result.resid, # OLS残差 X # 説明変数行列(定数項含む) ) # p値が有意 → 不均一分散の証拠 → 頑健標準誤差を使う # 頑健標準誤差(HC3法) ols_robust = ols_result.get_robustcov_results(cov_type='HC3')
VIF(Variance Inflation Factor)は各説明変数がどの程度 他の説明変数と相関しているかを示す指標である。 一般に VIF > 10 で多重共線性が疑われ、推定が不安定になる。
from statsmodels.stats.outliers_influence import variance_inflation_factor X_arr = X.values # 定数項を除いた説明変数の配列 vif_df = pd.DataFrame({ 'variable': X.columns, 'VIF': [variance_inflation_factor(X_arr, i) for i in range(X_arr.shape[1])] }) print(vif_df.sort_values('VIF', ascending=False))
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | print("図2: インパルス応答関数(IRF)を作成中...") fig2, axes2 = plt.subplots(1, 2, figsize=(13, 5)) h_vals = np.arange(n_periods + 1) # 左パネル: 婚姻率ショック → TFR ax2a = axes2[0] ax2a.plot(h_vals, irf_marr_to_tfr, color=COLORS['primary'], linewidth=2.5, marker='o', markersize=5, label='IRF 点推定値') ax2a.fill_between(h_vals, irf_mt_lo, irf_mt_hi, color=COLORS['primary'], alpha=0.2, label='漸近95%CI(デルタ法)') ax2a.axhline(0, color='black', linewidth=0.8, linestyle='--') ax2a.set_xlabel('ホライゾン(年)', fontsize=11) ax2a.set_ylabel('TFR の応答', fontsize=11) ax2a.set_title('IRF:婚姻率への1単位ショック → TFR の応答\n(漸近デルタ法 95%CI)', fontsize=11, fontweight='bold') ax2a.legend(fontsize=9) ax2a.grid(True, alpha=0.3) ax2a.set_xlim(-0.3, n_periods + 0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。ax.fill_between(...) — 2つの曲線で囲まれた領域を塗りつぶし。Lorenz曲線の格差面積などを可視化。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。85 86 87 88 89 90 91 92 93 94 95 96 97 98 | # 右パネル: TFR ショック → 婚姻率 ax2b = axes2[1] ax2b.plot(h_vals, irf_tfr_to_marr, color=COLORS['secondary'], linewidth=2.5, marker='s', markersize=5, label='IRF 点推定値') ax2b.fill_between(h_vals, irf_tm_lo, irf_tm_hi, color=COLORS['secondary'], alpha=0.2, label='漸近95%CI(デルタ法)') ax2b.axhline(0, color='black', linewidth=0.8, linestyle='--') ax2b.set_xlabel('ホライゾン(年)', fontsize=11) ax2b.set_ylabel('婚姻率の応答', fontsize=11) ax2b.set_title('IRF:TFR への1単位ショック → 婚姻率の応答\n(漸近デルタ法 95%CI)', fontsize=11, fontweight='bold') ax2b.legend(fontsize=9) ax2b.grid(True, alpha=0.3) ax2b.set_xlim(-0.3, n_periods + 0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。ax.fill_between(...) — 2つの曲線で囲まれた領域を塗りつぶし。Lorenz曲線の格差面積などを可視化。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。99 100 101 102 103 104 105 106 107 108 109 | # 学習ポイントの注釈 fig2.text(0.5, -0.04, '【学習ポイント】IRFは「1変数へのショックが他変数に与える動学的影響」を可視化する。\n' '95%CI(薄色帯)がゼロを含む場合、その応答は統計的に有意でない。', ha='center', fontsize=9, style='italic', color='gray', bbox=dict(boxstyle='round', facecolor='#FFF9C4', alpha=0.7)) plt.tight_layout() fig2.savefig(os.path.join(FIGURE_DIR, '2025_U2_fig2_irf.png'), bbox_inches='tight', dpi=150) plt.close(fig2) print(" → 2025_U2_fig2_irf.png 保存完了") |
図2: インパルス応答関数(IRF)を作成中... → 2025_U2_fig2_irf.png 保存完了
s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。
| 変数 | モデル1-2(不登校率A) | モデル3-4(暴力発生率B) | モデル5-6(いじめ発生率C) | |||
|---|---|---|---|---|---|---|
| OLS | 頑健SE | OLS | 頑健SE | OLS | 頑健SE | |
| 学力偏差値 D | −0.012 | −0.012 | −0.006 | −0.006 | −0.005 | −0.005 |
| ln(母子父子世帯率) E | 0.365*** | 0.365*** | 0.364*** | 0.364*** | 0.380*** | 0.380*** |
| ln(児童対受験者率) F | 0.341*** | 0.341*** | 0.334*** | 0.334*** | 0.310*** | 0.310*** |
| 教員一人当たり児童数 G | 0.078*** | 0.078*** | 0.079*** | 0.079*** | 0.083*** | 0.083*** |
| 中心化ln(人口密度) H | −0.023 | −0.023 | −0.020 | −0.020 | −0.008 | −0.008 |
| 中心化ln(人口密度)² H² | 0.080*** | 0.080*** | 0.084*** | 0.084*** | 0.086*** | 0.086*** |
| 子ども側変数 Xᵢ | 0.202* | 0.202* | −0.003 | −0.003 | 0.093 | 0.093 |
| 自由度修正済R² | 0.688 | 0.659 | 0.666 | |||
| Breusch-Pagan p値 | 0.939 n.s. | 0.856 n.s. | 0.767 n.s. | |||
※実データ(文科省統計・SSDSE-B)による再現。論文の結果との差異は採用試験倍率等の近似変数によるもの。***p<0.01, **p<0.05, *p<0.1
単変量ARでは1変数しか見られないが、VARは n 変数の相互影響を n 本の連立方程式で同時推定する。 「教育指標が相互に影響し合う」という実態を表現できる。
Granger因果性 ≠ 真の因果関係。「変数Xの過去が変数Yの将来を予測するか」という 統計的命題である。内生性・交絡変数の問題は別途対処が必要。
IRFはVARの係数行列 A の累乗から計算される。Bootstrap CIにより推定誤差込みの 不確実性を表示でき、「効果が統計的にゼロと異ならないか」を判断できる。
ln(Y)=α+β·ln(X)+ε のとき、β≈ΔlnY/ΔlnX ≈ (XのΔ%) に対する (YのΔ%)。 変数間の比率関係や歪んだ分布を扱うのに有効で、経済学・社会科学で広く使われる。
不均一分散があるとOLSのSEは過小/過大評価され、t検定が信頼できない。 BP検定で診断し、有意なら頑健SE(HC3)を使う。係数は変わらずSEのみ修正される。
VIF > 10 は問題の目安。対処法は①変数削除②主成分分析③Ridge回帰④中心化。 本論文の二次項(H²)は中心化で多重共線性を軽減している。
X と X² を同時投入すると X と X² の相関が極めて高くなる(多重共線性)。
X_c = X - mean(X) とすることで Cov(X_c, X_c²)≈0 に近づき安定した推定が可能になる。
係数の解釈:H の係数が0・H² の係数が正 → 平均人口密度でミニマムを持つU字型。
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 | import os import warnings import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from statsmodels.tsa.vector_ar.var_model import VAR import statsmodels.api as sm warnings.filterwarnings('ignore') plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 FIGURE_DIR = os.path.normpath('html/figures') DATA_DIR = os.path.normpath('data/raw') os.makedirs(FIGURE_DIR, exist_ok=True) COLORS = { 'primary': '#1565C0', 'secondary': '#E65100', 'success': '#2E7D32', 'danger': '#C62828', 'purple': '#6A1B9A', 'teal': '#00695C', 'gray': '#616161', } |
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} のように書式も指定できます。140 141 142 143 144 145 146 147 148 149 150 | print("\n" + "=" * 65) print("■ 分析1:VARモデル + Granger因果性検定(全国年度平均)") print("=" * 65) # 全国年度平均を計算 annual = df_pref.groupby('年度')[['合計特殊出生率', '婚姻率']].mean().sort_index() annual.columns = ['TFR', '婚姻率'] annual = annual.dropna() print(f"\n【全国年度平均({annual.index[0]}-{annual.index[-1]}年)】") print(annual.round(4)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。151 152 153 154 155 156 157 158 159 | # VAR モデル推定(ラグ次数を AIC で選択、最大2) var_model = VAR(annual) var_result = var_model.fit(maxlags=2, ic='aic') lag_order = var_result.k_ar print(f"\n【VAR モデル情報】") print(f" 選択ラグ次数 : {lag_order}") print(f" 標本数 : T={len(annual)}") print(f" AIC : {var_result.aic:.4f}") print(f" BIC : {var_result.bic:.4f}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | # Granger 因果性検定 print(f"\n【Granger因果性検定(Wald検定)】") print(f" 帰無仮説:原因変数の係数がすべてゼロ(Granger因果なし)\n") granger_results = {} test_pairs = [ ('TFR', '婚姻率', '婚姻率 → TFR(婚姻率が TFR を予測するか)'), ('婚姻率', 'TFR', 'TFR → 婚姻率(TFR が婚姻率を予測するか)'), ] for response, causing, desc in test_pairs: try: gc = var_result.test_causality(response, causing=causing, kind='wald') stat = float(gc.test_statistic) pval = float(gc.pvalue) df_gc = int(gc.df) except Exception: gc = var_result.test_causality(response, causing=causing, kind='f') stat = float(gc.test_statistic) pval = float(gc.pvalue) df_gc = lag_order sig = '***' if pval < 0.01 else '**' if pval < 0.05 else '*' if pval < 0.1 else 'n.s.' granger_results[desc] = {'stat': stat, 'pvalue': pval, 'df': df_gc, 'sig': sig, 'response': response, 'causing': causing} print(f" {desc}") print(f" Wald統計量={stat:.3f} df={df_gc} p値={pval:.4f} {sig}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。186 187 188 189 190 191 192 193 | # インパルス応答関数(IRF) n_periods = 8 irf_obj = var_result.irf(n_periods) # TFR の変数インデックス var_names = list(annual.columns) idx_tfr = var_names.index('TFR') idx_marr = var_names.index('婚姻率') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。194 195 196 197 198 199 200 201 202 203 204 205 206 | # 婚姻率 → TFR の IRF irf_marr_to_tfr = irf_obj.irfs[:, idx_tfr, idx_marr] # TFR → 婚姻率 の IRF irf_tfr_to_marr = irf_obj.irfs[:, idx_marr, idx_tfr] # 漸近デルタ法による IRF 信頼区間(statsmodels 組み込み、ランダム数不使用) print("\nIRF 信頼区間計算中(漸近デルタ法)...") irf_se = irf_obj.stderr() # shape: (n_periods+1, n_vars, n_vars) — Lütkepohl delta method irf_mt_lo = irf_marr_to_tfr - 1.96 * irf_se[:, idx_tfr, idx_marr] irf_mt_hi = irf_marr_to_tfr + 1.96 * irf_se[:, idx_tfr, idx_marr] irf_tm_lo = irf_tfr_to_marr - 1.96 * irf_se[:, idx_marr, idx_tfr] irf_tm_hi = irf_tfr_to_marr + 1.96 * irf_se[:, idx_marr, idx_tfr] print("完了(漸近95%CI)") |
=================================================================
■ 分析1:VARモデル + Granger因果性検定(全国年度平均)
=================================================================
【全国年度平均(2012-2023年)】
TFR 婚姻率
年度
2012 1.4604 7.9540
2013 1.4789 8.0104
2014 1.4719 7.9186
2015 1.5296 7.9540
2016 1.5206 7.7529
2017 1.5126 7.6353
2018 1.5034 7.4159
2019 1.4549 7.5925
2020 1.4213 6.9224
2021 1.3998 6.4994
2022 1.3583 6.4363
2023 1.2928 6.0528
【VAR モデル情報】
選択ラグ次数 : 1
標本数 : T=12
AIC : -9.8284
BIC : -9.6113
【Granger因果性検定(Wald検定)】
帰無仮説:原因変数の係数がすべてゼロ(Granger因果なし)
婚姻率 → TFR(婚姻率が TFR を予測するか)
Wald統計量=7.551 df=1 p値=0.0060 ***
TFR → 婚姻率(TFR が婚姻率を予測するか)
Wald統計量=0.323 df=1 p値=0.5701 n.s.
IRF 信頼区間計算中(漸近デルタ法)...
完了(漸近95%CI)df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | print("\n" + "=" * 65) print("■ 分析2:都道府県別 TFR × 婚姻率(地域間比較)") print("=" * 65) # 3つの代表年度を使用 years_scatter = [2014, 2018, 2022] df_scatter = df_pref[df_pref['年度'].isin(years_scatter)].dropna( subset=['合計特殊出生率', '婚姻率'] ).copy() print(f"\n散布図データ: {len(df_scatter)}件({years_scatter}年 × 47都道府県)") # 都道府県別回帰係数(2022年の例) df22 = df_scatter[df_scatter['年度'] == 2022].copy() X22 = sm.add_constant(df22['婚姻率'].values) fit22 = sm.OLS(df22['合計特殊出生率'].values, X22).fit() print(f"\n2022年 都道府県別 OLS: 婚姻率 → TFR") print(f" 係数={fit22.params[1]:.4f}, p={fit22.pvalues[1]:.4f}, R²={fit22.rsquared:.3f}") |
================================================================= ■ 分析2:都道府県別 TFR × 婚姻率(地域間比較) ================================================================= 散布図データ: 141件([2014, 2018, 2022]年 × 47都道府県) 2022年 都道府県別 OLS: 婚姻率 → TFR 係数=0.0460, p=0.2521, R²=0.029
sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | print("図3: Granger因果性検定結果を作成中...") fig3, axes3 = plt.subplots(1, 2, figsize=(13, 5)) # 左パネル: Wald 統計量の棒グラフ ax3a = axes3[0] labels_gc = list(granger_results.keys()) wald_vals = [granger_results[k]['stat'] for k in labels_gc] pvals_gc = [granger_results[k]['pvalue'] for k in labels_gc] sigs_gc = [granger_results[k]['sig'] for k in labels_gc] df_gc_vals = [granger_results[k]['df'] for k in labels_gc] bar_colors_gc = [] for p in pvals_gc: if p < 0.01: bar_colors_gc.append(COLORS['danger']) elif p < 0.05: bar_colors_gc.append(COLORS['secondary']) elif p < 0.1: bar_colors_gc.append(COLORS['success']) else: bar_colors_gc.append(COLORS['gray']) y_pos = np.arange(len(labels_gc)) ax3a.barh(y_pos, wald_vals, color=bar_colors_gc, alpha=0.85, edgecolor='white', linewidth=0.8, height=0.5) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | # 臨界値 chi2_05 = 3.841 * lag_order chi2_01 = 6.635 * lag_order ax3a.axvline(chi2_05, color='orange', linestyle='--', linewidth=1.5, label=f'χ²₅%(df={lag_order})={chi2_05:.2f}') ax3a.axvline(chi2_01, color='red', linestyle=':', linewidth=1.5, label=f'χ²₁%(df={lag_order})={chi2_01:.2f}') ax3a.set_yticks(y_pos) # 短縮ラベル short_labels = ['婚姻率 → TFR', 'TFR → 婚姻率'] ax3a.set_yticklabels(short_labels, fontsize=11) ax3a.set_xlabel('Wald統計量(χ²)', fontsize=11) ax3a.set_title(f'Granger因果性検定(VAR({lag_order})、Wald検定)', fontsize=12, fontweight='bold') for i, (v, sig, pv) in enumerate(zip(wald_vals, sigs_gc, pvals_gc)): ax3a.text(v + 0.05, i, f'{v:.2f} {sig}\n(p={pv:.3f})', va='center', fontsize=9, fontweight='bold') legend_patches = [ plt.Rectangle((0, 0), 1, 1, color=COLORS['danger'], label='p < 0.01 ***'), plt.Rectangle((0, 0), 1, 1, color=COLORS['secondary'], label='p < 0.05 **'), plt.Rectangle((0, 0), 1, 1, color=COLORS['success'], label='p < 0.10 *'), plt.Rectangle((0, 0), 1, 1, color=COLORS['gray'], label='n.s.'), ] ax3a.legend(handles=legend_patches + [ plt.Line2D([0], [0], color='orange', linestyle='--', label=f'χ²₅%={chi2_05:.2f}'), plt.Line2D([0], [0], color='red', linestyle=':', label=f'χ²₁%={chi2_01:.2f}'), ], loc='lower right', fontsize=8) ax3a.set_xlim(0, max(wald_vals) * 1.6 + chi2_01 * 0.2) ax3a.grid(axis='x', alpha=0.3) ax3a.invert_yaxis() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。279 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 313 314 315 316 317 318 319 320 | # 右パネル: p値の比較とVAR係数サマリー ax3b = axes3[1] # VAR 係数行列を棒グラフで可視化(ラグ1のみ) coef_mat = var_result.coefs[0] # shape: (n_vars, n_vars) [i, j]: i が被説明変数, j が説明変数 bse_mat = np.sqrt(np.diag(var_result.cov_params()[:var_result.neqs**2, :var_result.neqs**2]).reshape(var_result.neqs, var_result.neqs)) # 4つのクロス係数 coef_labels = [ f'TFR(t-1)→TFR', f'婚姻率(t-1)→TFR', f'TFR(t-1)→婚姻率', f'婚姻率(t-1)→婚姻率', ] coef_vals = [coef_mat[idx_tfr, idx_tfr], coef_mat[idx_tfr, idx_marr], coef_mat[idx_marr, idx_tfr], coef_mat[idx_marr, idx_marr]] if lag_order >= 1: coef_vals_disp = coef_vals else: coef_vals_disp = coef_vals x_pos_b = np.arange(len(coef_labels)) bar_col_b = [COLORS['primary'] if c >= 0 else COLORS['danger'] for c in coef_vals_disp] ax3b.bar(x_pos_b, coef_vals_disp, color=bar_col_b, alpha=0.85, edgecolor='white', linewidth=0.8) ax3b.axhline(0, color='black', linewidth=0.8, linestyle='--') ax3b.set_xticks(x_pos_b) ax3b.set_xticklabels(coef_labels, fontsize=9, rotation=15, ha='right') ax3b.set_ylabel('VAR係数値', fontsize=11) ax3b.set_title(f'VAR({lag_order}) ラグ1係数(ラグ1の係数行列)', fontsize=11, fontweight='bold') ax3b.grid(axis='y', alpha=0.3) for i, v in enumerate(coef_vals_disp): ax3b.text(i, v + (0.01 if v >= 0 else -0.02), f'{v:.3f}', ha='center', fontsize=9, fontweight='bold') plt.tight_layout() fig3.savefig(os.path.join(FIGURE_DIR, '2025_U2_fig3_granger.png'), bbox_inches='tight', dpi=150) plt.close(fig3) print(" → 2025_U2_fig3_granger.png 保存完了") |
図3: Granger因果性検定結果を作成中... # 実行時エラーで途中まで
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | print("図4: 都道府県別散布図(複数年)を作成中...") fig4, axes4 = plt.subplots(1, 3, figsize=(15, 5)) fig4.suptitle('都道府県別 合計特殊出生率(TFR)× 婚姻率(複数年比較)\n' 'データ出典: SSDSE-B-2026(e-Stat)', fontsize=12, fontweight='bold') scatter_colors = [COLORS['primary'], COLORS['secondary'], COLORS['success']] for ax, yr, col in zip(axes4, years_scatter, scatter_colors): df_yr = df_scatter[df_scatter['年度'] == yr].dropna( subset=['合計特殊出生率', '婚姻率'] ) x = df_yr['婚姻率'].values y = df_yr['合計特殊出生率'].values ax.scatter(x, y, color=col, alpha=0.7, s=55, edgecolors='white', linewidth=0.7, zorder=3) # 回帰直線 X_fit = sm.add_constant(x) fit_yr = sm.OLS(y, X_fit).fit() x_line = np.linspace(x.min(), x.max(), 100) y_line = fit_yr.params[0] + fit_yr.params[1] * x_line ax.plot(x_line, y_line, color=col, linewidth=2.0, linestyle='-', alpha=0.9, zorder=2) r2 = fit_yr.rsquared pv = fit_yr.pvalues[1] sig = '***' if pv < 0.01 else '**' if pv < 0.05 else '*' if pv < 0.1 else 'n.s.' ax.set_xlabel('婚姻率(15-64歳人口千人あたり)', fontsize=10) ax.set_ylabel('合計特殊出生率(TFR)', fontsize=10) ax.set_title(f'{yr}年度\nβ={fit_yr.params[1]:.3f}{sig} R²={r2:.3f}', fontsize=11, fontweight='bold') ax.grid(True, alpha=0.3) # 上位・下位都道府県にラベル df_yr_sorted = df_yr.sort_values('合計特殊出生率') for _, row in df_yr_sorted.tail(3).iterrows(): ax.annotate(row['都道府県'], (row['婚姻率'], row['合計特殊出生率']), fontsize=7, alpha=0.8, xytext=(3, 2), textcoords='offset points') for _, row in df_yr_sorted.head(2).iterrows(): ax.annotate(row['都道府県'], (row['婚姻率'], row['合計特殊出生率']), fontsize=7, alpha=0.8, xytext=(3, -8), textcoords='offset points') plt.tight_layout() fig4.savefig(os.path.join(FIGURE_DIR, '2025_U2_fig4_scatter.png'), bbox_inches='tight', dpi=150) plt.close(fig4) print(" → 2025_U2_fig4_scatter.png 保存完了") print("\n" + "=" * 65) print("✓ 全図の生成完了") print("=" * 65) print("\n【主要結果サマリー】") for desc, res in granger_results.items(): print(f" {desc}: Wald={res['stat']:.3f}, p={res['pvalue']:.4f} {res['sig']}") print(f"\n IRF(婚姻率→TFR): h=1期後の応答={irf_marr_to_tfr[1]:.4f}") print(f" IRF(TFR→婚姻率): h=1期後の応答={irf_tfr_to_marr[1]:.4f}") print(f"\n 2022年 都道府県別 OLS: 婚姻率β={fit22.params[1]:.4f}, R²={fit22.rsquared:.3f}") |
図4: 都道府県別散布図(複数年)を作成中... → 2025_U2_fig4_scatter.png 保存完了 ================================================================= ✓ 全図の生成完了 ================================================================= 【主要結果サマリー】 婚姻率 → TFR(婚姻率が TFR を予測するか): Wald=7.551, p=0.0060 *** TFR → 婚姻率(TFR が婚姻率を予測するか): Wald=0.323, p=0.5701 n.s. IRF(婚姻率→TFR): h=1期後の応答=0.0637 IRF(TFR→婚姻率): h=1期後の応答=1.5020 2022年 都道府県別 OLS: 婚姻率β=0.0460, R²=0.029
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。for _, row in df.iterrows() — DataFrameを1行ずつ取り出すループ。1点ずつ描画したいときに使用。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。ここまでの時系列で不登校→休職、横断面で母子父子世帯率・採用倍率・人口密度のU字が効くという結果を踏まえると、 教員の精神疾患休職は「子どもの困難」「家庭の困難」「労働市場の構造」の三層で生じると考えられる。 実務的にはSC増配・スクールカウンセラー配置・採用倍率の安定化を組み合わせる政策が求められ、 本節ではエビデンスに基づく対策の方向性を整理する。
本研究は時系列(VAR)と横断面(OLS)の2種類の分析を組み合わせることで、 教員の精神疾患休職の要因を多角的に検証した。
政策的示唆として、(1)不登校支援体制の充実によるカスケード効果の抑制、 (2)母子父子世帯の多い地域への重点的教員支援、 (3)大都市・農村部それぞれに対応した精神健康支援プログラムが提言されている。
以下のファイルをダウンロードして同じフォルダに置き、
python 2025_U2_yushu.py を実行するだけで全図・全結果を再現できます。
文科省 人事行政状況調査・問題行動調査・SSDSE-B から収集・加工した実データです。
ts.csv:精神疾患休職者数・不登校率・暴力件数・いじめ件数・採用試験倍率(FY2011-2023)
pref.csv:47都道府県の精神疾患休職率・不登校率・暴力率・いじめ率・人口密度など(FY2022)
data_prep.py:生データ(PDF・Excel)→ CSV の変換スクリプト。
yushu.py:CSVを読み込んでVAR・OLS・全図を生成。必要ライブラリ:numpy, pandas, matplotlib, statsmodels, pdfplumber。
| データ | 出典 |
|---|---|
| 精神疾患による教員休職者数・率 | 文科省 教育職員に係る懲戒処分等の状況(人事行政状況調査) |
| 不登校・暴力行為・いじめ件数 | 文科省 問題行動・不登校等生徒指導上の諸課題に関する調査 |
| 教員数・児童生徒数・人口 | SSDSE-B(統計でみる都道府県のすがた)2026年版 |
| 学力偏差値・ひとり親世帯率・採用試験倍率 | 全国学力調査(文科省)・国勢調査(総務省)・教員採用等状況(文科省)の近似値 |
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。