この教材は原論文と同じデータを読み込み、同じ手順で計算し直しています。画面に出る数値と図は、あなたのブラウザがその場で計算した結果です。
| 原論文が使ったデータ | 全国体力・運動能力、運動習慣等調査・SSDSE-B 分析単位:都道府県 中核手法:Pooled OLS・個体固定効果モデル・時間固定効果モデル・two-way 固定効果モデル・変量効果モデル |
|---|---|
| この教材が使うデータ |
|
| 原論文(PDF) | クラスタリングを用いた男子中学生の持久力低下の要因特定 統計数理賞/新井 陽登、陸 家傑、宮下 真翔(上智大学大学院理工学研究科)岡田 蒼未、柴田 悠生(上智大学理工学部情報理工学科) |
原論文と同じ粒度のデータは、この教材にも同梱しています。これを読み込めば、原論文と同じ細かさで分析をやり直せます(下の「🐍 ブラウザで動かす」でコードを書き換えて試せます)。ただし原論文が使った項目がすべて収録されているとは限りません。足りない項目は、上の「できないこと」に書いた出典から取ってくる必要があります。
🚫 この論文の再現コードは linearmodels を使うため、ブラウザの中では動かせません。お手元の Python で python3 code/2025_U3_suri.py を実行してください(637 行、編集不要)。必要なデータは上のリンクからダウンロードできます。
このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
スポーツ庁「全国体力・運動能力・運動習慣等調査」によると、男子中学生の20mシャトルラン平均は2008年の85.8回から2022年の77.8回へと低下し続けている。特に2019年以降の落ち込みは急激で、コロナ禍の影響も示唆されている。
原論文の概要(統計センター公式):「生徒の肥満率、運動部の加入率、朝食を毎日食べるという生活習慣が持久力に有意な影響を与える主要因であること、クラスタリングにより都道府県が「標準型」「生活習慣課題型」「食文化・環境要因型」に分類できることを明らかにしている。」
本ページではこの分析の流れを実データでたどりながら、使われた統計手法を一つずつ学んでいく。
この研究の問いは「どのような都道府県・時点の特性が持久力低下と関係しているか」を統計的に明らかにすることである。単純な時系列観察では因果が見えないため、47都道府県×3年のパネルデータと複数の統計手法を組み合わせて分析する。
| データ | 出典 | 取得変数 | 保存先 |
|---|---|---|---|
| 全国体力・運動能力等調査 中学校 実技・体格 (xlsx) |
スポーツ庁 | シャトルラン平均値、学校数、男子生徒数 | data/raw/{year}_jh_fitness.xlsx |
| 全国体力・運動能力等調査 生徒質問紙 (xlsx) |
スポーツ庁 | 運動部加入率(男子)、朝食を毎日食べる割合(男子) | data/raw/{year}_jh_student_q.xlsx |
| SSDSE-B 都道府県別時系列データ | 統計センター | 年平均気温、最高気温、降水日数、降水量 | data/raw/SSDSE-B-2026.csv |
| 肥満傾向児の出現率(都道府県別) | 愛媛県オープンデータ | 男子中学生(12〜14歳)肥満率 | data/raw/bmi_{year}.xlsx |
code/2025_U3_data_prep.py を実行すると data/2025_U3/2025_U3_panel.csv(141行×19列)が生成される。
愛媛県オープンデータのBMI Excelでは都道府県名が「青 森」(全角スペース入り)と記載されている。北海道は「道」が残り一致するが、「青森」は「青森県」と異なるため、接尾辞なし→標準名のマッピングが必要。
| 変数 | 平均 | 標準偏差 | 最小 | 最大 | 単位 |
|---|---|---|---|---|---|
| shuttle(20mシャトルラン) | 81.3 | 3.6 | 69.4 | 92.0 | 回 |
| bmi_rate(男子中学生肥満率) | 11.5 | 2.0 | 6.5 | 16.6 | % |
| stu_per_school(1校あたり生徒数) | 44.5 | 12.5 | 19.1 | 75.7 | 人/校 |
| sports_club(運動部加入率) | 76.2 | 5.1 | 64.7 | 87.8 | % |
| breakfast(朝食を毎日食べる割合) | 81.5 | 2.7 | 73.5 | 87.7 | % |
| avg_temp(年平均気温) | 16.1 | 2.3 | 9.8 | 23.9 | ℃ |
| max_temp(最高気温) | 31.9 | 1.4 | 26.5 | 34.3 | ℃ |
1 2 3 4 5 6 7 8 9 10 11 | data_file = 'data/2025_U3/2025_U3_panel.csv' df_raw = pd.read_csv(data_file) # linearmodels 用:都道府県名を数値IDに変換(MultiIndex の entity) df_raw['pref_id'] = pd.Categorical(df_raw['pref']).codes + 1 print("=" * 65) print("■ パネルデータ(実データ:N=47都道府県 × T=3年)") print(" 出典: スポーツ庁体力調査 / SSDSE-B / 愛媛県オープンデータ") print("=" * 65) print(df_raw[['shuttle', 'bmi_rate', 'stu_per_school', 'sports_club', 'breakfast', 'avg_temp', 'max_temp']].describe().round(3)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。.describe() — 件数・平均・標準偏差・四分位・最大/最小を一括計算。データの素性チェックに必須。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # Z-score は data_prep.py で既に計算済み({変数名}_z 列として保存) SCALE_VARS = ['avg_temp', 'max_temp', 'rain_days', 'rain_vol', 'bmi_rate', 'stu_per_school', 'sports_club', 'breakfast'] Z_VARS = [v + '_z' for v in SCALE_VARS] VAR_LABELS = { 'avg_temp_z': '年平均気温', 'max_temp_z': '最高気温', 'rain_days_z': '降水日数(年間)', 'rain_vol_z': '降水量(年間)', 'bmi_rate_z': '県肥満率', 'stu_per_school_z': '1学校当たり生徒数', 'sports_club_z': '運動部加入率', 'breakfast_z': '朝食を毎日食べる割合', } |
=================================================================
■ パネルデータ(実データ:N=47都道府県 × T=3年)
出典: スポーツ庁体力調査 / SSDSE-B / 愛媛県オープンデータ
=================================================================
shuttle bmi_rate stu_per_school ... breakfast avg_temp max_temp
count 141.000 141.000 141.000 ... 141.000 141.000 141.000
mean 81.303 11.459 44.456 ... 81.526 16.132 31.938
std 3.647 2.043 12.545 ... 2.665 2.273 1.372
min 69.350 6.527 19.105 ... 73.500 9.800 26.500
25% 78.920 10.013 34.188 ... 79.700 15.200 31.200
50% 81.320 11.280 44.670 ... 81.700 16.500 32.000
75% 83.850 12.930 51.697 ... 83.400 17.400 32.800
max 92.010 16.557 75.711 ... 87.700 23.900 34.300
[8 rows x 7 columns].map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。まずどの推定モデルが47都道府県×3年度のデータ構造に適合するかを統計的に検定することが有効だと考えられる。 その理由は地域固有の気候・運動文化など観測されない固定要因を無視すると、説明変数の効果が歪むからである。 ここでは個体・時間効果の取り扱いに着目し、F検定とHausman検定によるモデル選択を用いる。 個体固定効果モデルが選好される結果が期待される。
パネルデータ(複数の個体を複数時点で観察したデータ)には様々なモデルがある。どのモデルが適切かを、統計的検定で決定する。
| モデル | 説明 | 固定効果 | 変量効果 |
|---|---|---|---|
| Pooled OLS | 個体・時点の違いを完全に無視してプールして回帰 | なし | なし |
| 時間固定効果(Time FE) | 年度ダミーで全国共通の時間トレンドを除去 | 時間のみ | なし |
| 個体固定効果(Individual FE) | 都道府県ダミーで各県固有の不変特性を除去 | 個体のみ | なし |
| Two-way 固定効果 | 個体ダミー+時間ダミーを同時に投入 | 個体+時間 | なし |
| 変量効果(RE) | 個体固有効果を誤差の一部と仮定して推定(GLS) | なし | あり |
個体固定効果モデルの基本形:
固定効果を使うことで「都道府県が本来持つ特性」と「説明変数の効果」を分離できる。
「追加したダミー変数の係数がすべてゼロ」(固定効果不要)という帰無仮説をF検定で棄却できるか確かめる。
| 比較 | 追加ダミー数 (q) | F統計量 | p値 | 判定 |
|---|---|---|---|---|
| Pooled OLS vs 時間固定効果 | 2(年度ダミー2個) | 12.87 | < 0.001 | 有意 *** |
| Pooled OLS vs 個体固定効果 | 46(都道府県ダミー46個) | 7.62 | < 0.001 | 有意 *** |
| Pooled OLS vs Two-way FE | 48(個体46 + 時間2) | 11.50 | < 0.001 | 有意 *** |
statsmodels の OLS で制約モデル・非制約モデルを推定し、RSSの差からF統計量を計算する。
変量効果(RE)モデルは「個体固有効果 α_i が説明変数と無相関」という仮定のもとで一致推定量・有効推定量となる。この仮定が成立しない場合、REは偏ったバイアスを持ち、FEを使うべきである。
V_FE − V_RE は正定値でない場合があり、直接逆行列を取ると不安定。固有値分解で正の固有値のみを使った擬似逆行列(Moore-Penrose)を使う。
28 29 30 31 32 33 34 35 36 | print("図1: シャトルラン時系列推移グラフを作成中...") # スポーツ庁「全国体力・運動能力、運動習慣等調査」の公表集計値 # 出典: https://www.mext.go.jp/sports/b_menu/toukei/kodomo/zencyo/1368222_00002.htm # 2020年はコロナにより調査実施なし(None) hist_years = [2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022] shuttle_vals = [85.8, 85.4, 85.7, 84.4, 84.7, 84.7, 84.5, 85.0, 85.3, 85.5, 85.8, 83.7, None, 79.5, 77.8] |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。37 38 39 40 41 42 | # 都道府県別・年度別 シャトルラン平均(実パネルデータから計算) pref_year_avg = df_raw.groupby(['pref', 'year'])['shuttle'].mean().reset_index() fig1, axes1 = plt.subplots(1, 2, figsize=(13, 5)) fig1.suptitle('男子中学生の持久力テスト結果の推移\n(出典:スポーツ庁 全国体力・運動能力、運動習慣等調査)', fontsize=12, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | # (a) 全国平均の時系列(公表値) years_v = [y for y, v in zip(hist_years, shuttle_vals) if v is not None] vals_v = [v for v in shuttle_vals if v is not None] ax1a = axes1[0] ax1a.plot(years_v, vals_v, 'o-', color='#1565C0', linewidth=2.2, markersize=6, markerfacecolor='white', markeredgewidth=2, label='20mシャトルラン(全国平均)') ax1a.axvspan(2020, 2022, alpha=0.12, color='red', label='コロナ禍影響期') ax1a.axhline(vals_v[0], color='gray', linestyle='--', linewidth=0.8, alpha=0.6, label=f'2008年水準 ({vals_v[0]:.1f}回)') ax1a.set_xlabel('年度', fontsize=10) ax1a.set_ylabel('平均回数(回)', fontsize=10) ax1a.set_title('全国平均 時系列推移(公表値)', fontsize=11, fontweight='bold') ax1a.legend(fontsize=8.5) ax1a.grid(True, alpha=0.3) ax1a.set_ylim(74, 90) for y, v in zip(years_v[-3:], vals_v[-3:]): ax1a.annotate(f'{v:.1f}', (y, v), textcoords='offset points', xytext=(0, 8), fontsize=8.5, ha='center', color='#C62828', fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | # (b) 都道府県別の分布(実パネルデータ) ax1b = axes1[1] pref_colors = plt.cm.tab20(np.linspace(0, 1, N)) nat_mean = pref_year_avg.groupby('year')['shuttle'].mean() for i, pref in enumerate(df_raw['pref'].unique()): pref_d = pref_year_avg[pref_year_avg['pref'] == pref].sort_values('year') ax1b.plot(pref_d['year'], pref_d['shuttle'], color=pref_colors[i % 20], alpha=0.3, linewidth=0.9) ax1b.plot(nat_mean.index, nat_mean.values, color='black', linewidth=2.5, linestyle='--', label='47都道府県平均', zorder=5) ax1b.set_xlabel('年度', fontsize=10) ax1b.set_ylabel('シャトルラン平均回数(回)', fontsize=10) ax1b.set_title('都道府県別 推移(実データ)', fontsize=11, fontweight='bold') ax1b.legend(fontsize=9) ax1b.grid(True, alpha=0.3) ax1b.set_xticks(YEARS) plt.tight_layout() save_fig('fig1_trend') |
図1: シャトルラン時系列推移グラフを作成中... → html/figures/2025_U3_fig1_trend.png 保存完了
df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。前節の個体固定効果モデルが採用された結果を踏まえると、 残るのは「時間変動する変数のうち、シャトルラン記録に効くのはどれか」という問いであると考えられる。 これを検証する必要があるが、その手法としてZ-score標準化済みの説明変数による回帰係数比較に着目した。 肥満率・運動部加入率・朝食摂取率の3変数が両モデルで一貫して有意となる結果が期待される。
F検定・Hausman検定の結果に基づき、時間固定効果モデルと個体固定効果モデルの両方で回帰を行い、それぞれで有意な変数を特定する。全ての説明変数は事前にZ-score標準化されているため、回帰係数の絶対値が大きいほど目的変数への相対的影響が大きいことを意味する。
| 説明変数 | 時間FE: β | 時間FE: p | 個体FE: β | 個体FE: p | 解釈 |
|---|---|---|---|---|---|
| 年平均気温 | −0.46 | 0.218 | +4.55 | 0.230 | 非有意 |
| 最高気温 | +0.84 | 0.014 * | +0.15 | 0.631 | 時間FEのみ有意(正) |
| 降水日数 | +0.03 | 0.938 | +0.17 | 0.765 | 非有意 |
| 降水量 | +0.53 | 0.120 | +0.33 | 0.369 | 非有意 |
| 県肥満率 | −0.84 | 0.006 ** | −0.95 | 0.009 ** | 両モデルで有意(負) |
| 1校あたり生徒数 | −0.66 | 0.007 ** | −0.91 | 0.581 | 時間FEのみ有意(負) |
| 運動部加入率 | +0.83 | 0.001 *** | +3.24 | < 0.001 *** | 両モデルで有意(正) |
| 朝食を毎日食べる割合 | +0.65 | 0.020 * | +1.97 | 0.001 *** | 両モデルで有意(正) |
説明変数の単位がバラバラな場合(例:気温は℃、生徒数は人、加入率は%)、生の係数を比較しても意味がない。Z-score標準化後は「各変数が1標準偏差変化したときの目的変数の変化量(回数)」として解釈でき、変数間の相対的重要度を比較できる。
81 82 83 84 85 86 | print("図2: 固定効果モデル選択と回帰係数グラフを作成中...") fig2, axes2 = plt.subplots(1, 3, figsize=(16, 5)) fig2.suptitle('Step1〜2. パネルモデル選択(F検定・Hausman検定)と固定効果モデルの係数\n' '被説明変数:男子中学生 20mシャトルラン平均回数(実データ)', fontsize=11, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | # (a) F統計量の棒グラフ ax2a = axes2[0] f_labels = ['Pooled OLS\nvs 時間固定効果', 'Pooled OLS\nvs 個体固定効果', 'Pooled OLS\nvs two-way FE'] f_vals_ref = [f_time, f_ind, f_twoway] p_vals_ref = [p_time, p_ind, p_twoway] bar_colors = ['#E53935' if p < 0.05 else '#9E9E9E' for p in p_vals_ref] ax2a.barh(np.arange(3), f_vals_ref, color=bar_colors, alpha=0.85, edgecolor='white') crit_f = stats.f.ppf(0.95, 2, 130) ax2a.axvline(crit_f, color='orange', linestyle='--', linewidth=1.5, label=f'5%臨界値 ({crit_f:.2f})') ax2a.set_yticks(np.arange(3)) ax2a.set_yticklabels(f_labels, fontsize=9) ax2a.set_xlabel('F統計量', fontsize=10) ax2a.set_title('F検定:固定効果の有意性', fontsize=10, fontweight='bold') ax2a.legend(fontsize=8) ax2a.grid(axis='x', alpha=0.3) ax2a.invert_yaxis() for i, (fv, pv) in enumerate(zip(f_vals_ref, p_vals_ref)): sig = '***' if pv < 0.001 else '**' if pv < 0.01 else '*' if pv < 0.05 else 'n.s.' ax2a.text(fv + max(f_vals_ref) * 0.02, i, f'F={fv:.1f} {sig}', va='center', fontsize=8.5, fontweight='bold') red_patch = mpatches.Patch(color='#E53935', label='有意(p<0.05)') gray_patch = mpatches.Patch(color='#9E9E9E', label='非有意') ax2a.legend(handles=[red_patch, gray_patch], fontsize=8, loc='lower right') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | # (b) 決定係数の比較 + Hausman検定 ax2b = axes2[1] model_names = ['Pooled OLS', '時間FE\n(論文:0.39)', '個体FE\n(論文:0.75)', 'Two-way FE'] r2_vals = [ols_pooled.rsquared, ols_time_fe.rsquared, ols_ind_fe.rsquared, ols_twoway.rsquared] r2_colors = ['#9E9E9E', '#FF8F00', '#6A1B9A', '#757575'] ax2b.bar(np.arange(4), r2_vals, color=r2_colors, alpha=0.85, edgecolor='white') ax2b.set_xticks(np.arange(4)) ax2b.set_xticklabels(model_names, fontsize=8.5) ax2b.set_ylabel('決定係数 R²', fontsize=10) ax2b.set_title('各モデルの決定係数', fontsize=10, fontweight='bold') ax2b.grid(axis='y', alpha=0.3) ax2b.set_ylim(0, 1.0) for i, v in enumerate(r2_vals): ax2b.text(i, v + 0.02, f'{v:.3f}', ha='center', fontsize=9, fontweight='bold') hausman_note = (f"Hausman検定\n" f"χ²={H_stat:.2f}, p={p_H:.3f}\n" f"→ {'個体FE採用' if p_H < 0.05 else 'RE採用'}") ax2b.text(0.97, 0.04, hausman_note, transform=ax2b.transAxes, fontsize=8.5, va='bottom', ha='right', bbox=dict(boxstyle='round', facecolor='#FFF9C4', alpha=0.9, edgecolor='#F9A825')) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | # (c) 個体固定効果モデルの回帰係数(95% CI付き) ax2c = axes2[2] y_pos2 = np.arange(len(Z_VARS)) coef_i_list = [coef_ind[zv] for zv in Z_VARS] pval_i_list = [pval_ind[zv] for zv in Z_VARS] se_i_list = [ols_ind_fe.bse[zv] for zv in Z_VARS] ci_lo_list = [c - 1.96 * s for c, s in zip(coef_i_list, se_i_list)] ci_hi_list = [c + 1.96 * s for c, s in zip(coef_i_list, se_i_list)] bar_c2 = ['#6A1B9A' if p < 0.05 else '#BDBDBD' for p in pval_i_list] ax2c.barh(y_pos2, coef_i_list, color=bar_c2, alpha=0.8, height=0.6) ax2c.errorbar(coef_i_list, y_pos2, xerr=[np.array(coef_i_list) - np.array(ci_lo_list), np.array(ci_hi_list) - np.array(coef_i_list)], fmt='none', color='black', capsize=3, linewidth=1) ax2c.axvline(0, color='black', linewidth=0.8, linestyle='--') ax2c.set_yticks(y_pos2) ax2c.set_yticklabels([VAR_LABELS[zv] for zv in Z_VARS], fontsize=9) ax2c.set_xlabel('標準化係数(95% CI)', fontsize=9) ax2c.set_title(f'個体固定効果モデル\nR²={ols_ind_fe.rsquared:.3f}', fontsize=10, fontweight='bold') ax2c.grid(axis='x', alpha=0.3) ax2c.invert_yaxis() for i, (c, s, p) in enumerate(zip(coef_i_list, se_i_list, pval_i_list)): if p < 0.05: sig = '***' if p < 0.001 else '**' if p < 0.01 else '*' ax2c.text(c + 1.96 * s + 0.05, i, sig, va='center', fontsize=9, color='#4A148C') plt.tight_layout() save_fig('fig2_fe') |
図2: 固定効果モデル選択と回帰係数グラフを作成中... → html/figures/2025_U3_fig2_fe.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。前節の肥満率・運動部加入率・朝食摂取率がシャトルランに効くと判明した結果を踏まえると、 都道府県はこれら3軸の組み合わせで複数のタイプに分かれると考えられる。 これを検証して政策設計に活かす必要があるため、その手法としてK-meansクラスタリングに着目した。 エルボー法とシルエットスコアでk=3が最適となり、各群に異なる介入策が必要となる結果が期待される。
回帰分析で共通有意だった3変数(肥満率・運動部加入率・朝食摂取率)を用いて、2022年度の47都道府県をK-meansクラスタリングで類型化する。
| k | Inertia | Silhouette Score | 選択 |
|---|---|---|---|
| 2 | 99.4 | 0.256 | |
| 3 | 71.8 | 0.301 | ← 採用(エルボー+論文) |
| 4 | 55.7 | 0.321 | |
| 5 | 43.3 | 0.328 | |
| 6 | 36.5 | 0.341 |
sklearn の KMeans を使い、クラスタリング前に標準化(StandardScaler)を行う。論文の分析では2022年度データのみを使用。
| クラスター | 特徴 | 対策の方向性 |
|---|---|---|
| Cluster1 標準型 | 肥満率低、運動部参加率高、朝食摂取率高。バランスが良い。 | 現状維持・他クラスターへの横展開 |
| Cluster2 生活習慣課題型 | 都市部に多い。運動部離れ・朝食スキップが課題。肥満率も高め。 | 課外活動の多様化(部活動以外の運動機会)、食育の強化 |
| Cluster3 食文化・環境要因型 | 東北・北関東に集中。運動部参加は高いが肥満率が突出して高い。 | 地域の食文化(高カロリー食)の見直し、栄養指導の強化 |
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 186 187 188 189 190 191 192 193 194 195 | import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec import matplotlib.patches as mpatches import statsmodels.api as sm from linearmodels.panel import PooledOLS, PanelOLS, RandomEffects from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from sklearn.metrics import silhouette_score from scipy import stats import warnings warnings.filterwarnings('ignore') plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 import os FIGURE_DIR = 'html/figures' os.makedirs(FIGURE_DIR, exist_ok=True) def save_fig(name): path = os.path.join(FIGURE_DIR, f'2025_U3_{name}.png') plt.savefig(path, bbox_inches='tight', dpi=150) plt.close() print(f" → {path} 保存完了") C_CLUSTER = {1: '#E53935', 2: '#43A047', 3: '#1E88E5'} C_LABELS = {1: 'Cluster1: 標準型', 2: 'Cluster2: 生活習慣課題型', 3: 'Cluster3: 食文化・環境要因型'} YEARS = [2019, 2021, 2022] N = 47 T = len(YEARS) |
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)。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。196 197 198 199 200 201 202 203 | print("\n" + "=" * 65) print("■ Step1. モデル選択(F検定 + Hausman検定)") print("=" * 65) # ── Pooled OLS(statsmodels) ──────────────────────────────── df_flat = df_raw.copy() X_ols_df = sm.add_constant(df_flat[Z_VARS]) ols_pooled = sm.OLS(df_flat['shuttle'], X_ols_df).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()。204 205 206 207 208 209 | # ── 個体固定効果モデル(明示的ダミー変数)─────────────────── pref_dummies = pd.get_dummies(df_flat['pref_id'], prefix='pref', drop_first=True).astype(float) X_ind_fe_df = pd.concat([df_flat[Z_VARS].reset_index(drop=True), pref_dummies.reset_index(drop=True)], axis=1) X_ind_fe_df = sm.add_constant(X_ind_fe_df) ols_ind_fe = sm.OLS(df_flat['shuttle'], X_ind_fe_df).fit() |
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行でリストを作れます。210 211 212 213 214 215 | # ── 時間固定効果モデル(明示的ダミー変数)─────────────────── year_dummies = pd.get_dummies(df_flat['year'], prefix='yr', drop_first=True).astype(float) X_time_fe_df = pd.concat([df_flat[Z_VARS].reset_index(drop=True), year_dummies.reset_index(drop=True)], axis=1) X_time_fe_df = sm.add_constant(X_time_fe_df) ols_time_fe = sm.OLS(df_flat['shuttle'], X_time_fe_df).fit() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。216 217 218 219 220 221 | # ── Two-way 固定効果モデル ──────────────────────────────────── X_twoway_df = pd.concat([df_flat[Z_VARS].reset_index(drop=True), pref_dummies.reset_index(drop=True), year_dummies.reset_index(drop=True)], axis=1) X_twoway_df = sm.add_constant(X_twoway_df) ols_twoway = sm.OLS(df_flat['shuttle'], X_twoway_df).fit() |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。sm.OLS(y, X).fit() — 最小二乗法でモデルを推定。model.params, model.pvalues, model.conf_int() で結果取得。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | # ── F検定 ──────────────────────────────────────────────────── def panel_f_test(ols_restricted, ols_unrestricted, df_added): """ F検定: 固定効果ダミーの結合有意性 H₀: 追加ダミー変数の係数がすべてゼロ(固定効果不要) """ rss_r = ols_restricted.ssr rss_u = ols_unrestricted.ssr df_num = df_added df_den = ols_unrestricted.df_resid if rss_r <= rss_u or df_num <= 0: return 0.0, 1.0 f_stat = ((rss_r - rss_u) / df_num) / (rss_u / df_den) p_val = 1 - stats.f.cdf(f_stat, df_num, df_den) return float(f_stat), float(p_val) f_time, p_time = panel_f_test(ols_pooled, ols_time_fe, df_added=T-1) f_ind, p_ind = panel_f_test(ols_pooled, ols_ind_fe, df_added=N-1) f_twoway,p_twoway= panel_f_test(ols_pooled, ols_twoway, df_added=N+T-2) print(f"\n【F検定結果(Pooled OLS との比較)】") print(f" {'比較':<30} {'F統計量':>9} {'p値':>12} {'判定':>8}") print(" " + "-" * 65) print(f" {'Pooled OLS vs 時間固定効果':<30} {f_time:>9.3f} {p_time:>12.2e}" f" {'有意*' if p_time < 0.05 else '非有意':>8}") print(f" {'Pooled OLS vs 個体固定効果':<30} {f_ind:>9.3f} {p_ind:>12.2e}" f" {'有意*' if p_ind < 0.05 else '非有意':>8}") print(f" {'Pooled OLS vs two-way 固定効果':<30} {f_twoway:>9.3f} {p_twoway:>12.4f}" f" {'有意*' if p_twoway < 0.05 else '非有意':>8}") print(f"\n 論文の参考値: 時間FE F=10.269, 個体FE F=31.431, Two-way F=1.011") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。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 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | # ── Hausman検定(linearmodels の FE vs RE)─────────────────── df_panel = df_raw.set_index(['pref_id', 'year']) y_p = df_panel['shuttle'] X_p = sm.add_constant(df_panel[Z_VARS]) fe_res = PanelOLS(y_p, X_p, entity_effects=True).fit(cov_type='unadjusted') re_res = RandomEffects(y_p, X_p).fit(cov_type='unadjusted') def hausman_test(fe_result, re_result): """ Hausman検定: FE と RE の係数の差を利用した検定 H₀: 固有効果と説明変数が無相関(RE が一致・有効) H₁: 相関あり(RE はバイアスを持つ → FE を使うべき) """ fe_coef = fe_result.params re_coef = re_result.params shared = fe_coef.index.intersection(re_coef.index) diff = (fe_coef - re_coef)[shared].values V_fe = pd.DataFrame(fe_result.cov.values, index=fe_result.params.index, columns=fe_result.params.index).loc[shared, shared].values V_re = pd.DataFrame(re_result.cov.values, index=re_result.params.index, columns=re_result.params.index).loc[shared, shared].values V_diff = V_fe - V_re eigvals, eigvecs = np.linalg.eigh(V_diff) pos_mask = eigvals > 1e-10 if pos_mask.sum() == 0: V_inv = np.linalg.pinv(V_diff) else: V_inv = (eigvecs[:, pos_mask] @ np.diag(1.0 / eigvals[pos_mask]) @ eigvecs[:, pos_mask].T) H_stat = float(diff @ V_inv @ diff) df_H = int(pos_mask.sum()) p_H = 1 - stats.chi2.cdf(H_stat, df_H) return H_stat, df_H, p_H H_stat, df_H, p_H = hausman_test(fe_res, re_res) sig_h = '***' if p_H < 0.01 else '**' if p_H < 0.05 else '*' if p_H < 0.1 else 'n.s.' print(f"\n【Hausman検定(個体固定効果 vs 変量効果)】") print(f" χ²={H_stat:.3f}, df={df_H}, p={p_H:.4f} {sig_h}") if p_H < 0.05: print(" → 固定効果モデルを採用(個体効果と説明変数に相関がある)") else: print(" → 変量効果モデルが一致・有効(固有効果と説明変数に相関なし)") print(f" 論文の参考値: Hausman χ²≈17.1, p=0.047") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。sm.add_constant(X) — 切片項(定数1の列)を先頭に追加。statsmodelsで必須。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。301 302 303 304 305 306 | # ── モデル決定係数のまとめ ─────────────────────────────────── print(f"\n【決定係数のまとめ】") print(f" Pooled OLS : R²={ols_pooled.rsquared:.4f}") print(f" 時間固定効果 : R²={ols_time_fe.rsquared:.4f} (論文 0.3872)") print(f" 個体固定効果 : R²={ols_ind_fe.rsquared:.4f} (論文 0.7451)") print(f" Two-way FE : R²={ols_twoway.rsquared:.4f}") |
================================================================= ■ Step1. モデル選択(F検定 + Hausman検定) ================================================================= 【F検定結果(Pooled OLS との比較)】 比較 F統計量 p値 判定 ----------------------------------------------------------------- Pooled OLS vs 時間固定効果 12.872 7.94e-06 有意* Pooled OLS vs 個体固定効果 7.618 5.55e-16 有意* Pooled OLS vs two-way 固定効果 11.501 0.0000 有意* 論文の参考値: 時間FE F=10.269, 個体FE F=31.431, Two-way F=1.011 【Hausman検定(個体固定効果 vs 変量効果)】 χ²=26.233, df=7, p=0.0005 *** → 固定効果モデルを採用(個体効果と説明変数に相関がある) 論文の参考値: Hausman χ²≈17.1, p=0.047 【決定係数のまとめ】 Pooled OLS : R²=0.4982 時間固定効果 : R²=0.5812 (論文 0.3872) 個体固定効果 : R²=0.9011 (論文 0.7451) Two-way FE : R²=0.9337
np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | print("\n" + "=" * 65) print("■ Step2. 回帰分析結果(時間FE vs 個体FE)") print("=" * 65) print(f"\n {'説明変数':<22} {'時間FE 係数':>10} {'時間FE p':>9}" f" {'個体FE 係数':>12} {'個体FE p':>9} {'有意':>6}") print(" " + "-" * 75) coef_time = dict(zip(Z_VARS, ols_time_fe.params[Z_VARS])) pval_time = dict(zip(Z_VARS, ols_time_fe.pvalues[Z_VARS])) coef_ind = dict(zip(Z_VARS, ols_ind_fe.params[Z_VARS])) pval_ind = dict(zip(Z_VARS, ols_ind_fe.pvalues[Z_VARS])) for zv in Z_VARS: ct = coef_time[zv] pt = pval_time[zv] ci = coef_ind[zv] pi = pval_ind[zv] sig_t = '***' if pt < 0.01 else '**' if pt < 0.05 else '*' if pt < 0.1 else '' sig_i = '***' if pi < 0.01 else '**' if pi < 0.05 else '*' if pi < 0.1 else '' sig = sig_t or sig_i vn = VAR_LABELS[zv] print(f" {vn:<22} {ct:>10.4f} {pt:>9.4f}" f" {ci:>12.4f} {pi:>9.4f} {sig:>6}") |
================================================================= ■ Step2. 回帰分析結果(時間FE vs 個体FE) ================================================================= 説明変数 時間FE 係数 時間FE p 個体FE 係数 個体FE p 有意 --------------------------------------------------------------------------- 年平均気温 -0.4562 0.2179 4.5508 0.2296 最高気温 0.8388 0.0139 0.1485 0.6305 ** 降水日数(年間) 0.0253 0.9384 0.1678 0.7649 降水量(年間) 0.5340 0.1195 0.3340 0.3693 県肥満率 -0.8418 0.0058 -0.9532 0.0092 *** 1学校当たり生徒数 -0.6635 0.0067 -0.9076 0.5813 *** 運動部加入率 0.8286 0.0008 3.2417 0.0000 *** 朝食を毎日食べる割合 0.6525 0.0196 1.9731 0.0009 **
[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。331 332 333 334 335 336 | print("\n" + "=" * 65) print("■ Step3. K-meansクラスタリング(47都道府県を3類型に分類)") print("=" * 65) # 2022年データを使用(論文: 最新年度のデータでクラスタリング) df_2022 = df_raw[df_raw['year'] == 2022].copy().reset_index(drop=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。337 338 339 340 341 342 343 | # 有意変数(論文の分析で特定された変数)でクラスタリング CLUSTER_VARS = ['bmi_rate', 'sports_club', 'breakfast'] X_cluster = df_2022[CLUSTER_VARS].values # 標準化(KMeans 用) scaler = StandardScaler() X_scaled = scaler.fit_transform(X_cluster) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。StandardScaler().fit_transform(X) — 各列を「平均0・分散1」に標準化。単位が違う変数のβを比較可能に。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。344 345 346 347 348 349 350 351 352 353 354 355 | # ── エルボー法(k=2〜6)──────────────────────────────────── print(f"\n【エルボー法とシルエットスコア(k=2〜6)】") print(f" {'k':>4} {'Inertia':>12} {'Silhouette Score':>18}") print(" " + "-" * 36) inertias, silhouettes = [], [] for k in range(2, 7): km = KMeans(n_clusters=k, random_state=2025, n_init=20) km.fit(X_scaled) inertias.append(km.inertia_) sil = silhouette_score(X_scaled, km.labels_) silhouettes.append(sil) print(f" {k:>4} {km.inertia_:>12.2f} {sil:>18.4f}") |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。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 385 | # ── k=3でクラスタリング ───────────────────────────────────── kmeans = KMeans(n_clusters=3, random_state=2025, n_init=30) kmeans.fit(X_scaled) df_2022['cluster_raw'] = kmeans.labels_ # クラスター番号を論文と対応:シャトルラン平均で整列 shuttle_by_cluster = df_2022.groupby('cluster_raw')['shuttle'].mean().sort_values(ascending=False) # 最高=Cluster1、最低=Cluster2、中間=Cluster3 rank_map = {shuttle_by_cluster.index[0]: 1, shuttle_by_cluster.index[2]: 2, shuttle_by_cluster.index[1]: 3} df_2022['cluster'] = df_2022['cluster_raw'].map(rank_map) print(f"\n【クラスター別の基本統計(論文 Table 4 に対応)】") summary = df_2022.groupby('cluster').agg( 都道府県数=('pref', 'count'), シャトルラン平均=('shuttle', 'mean'), 肥満率平均=('bmi_rate', 'mean'), 運動部加入率平均=('sports_club', 'mean'), 朝食摂取率平均=('breakfast', 'mean'), ).round(2) print(summary) print(f"\n 論文の参考値: Cluster1=81.9, Cluster2=77.8, Cluster3=78.3") print(f"\n【クラスター1(標準型)の都道府県】") print(" " + ', '.join(df_2022[df_2022['cluster'] == 1]['pref'].tolist())) print(f"【クラスター2(生活習慣課題型)の都道府県】") print(" " + ', '.join(df_2022[df_2022['cluster'] == 2]['pref'].tolist())) print(f"【クラスター3(食文化・環境要因型)の都道府県】") print(" " + ', '.join(df_2022[df_2022['cluster'] == 3]['pref'].tolist())) |
=================================================================
■ Step3. K-meansクラスタリング(47都道府県を3類型に分類)
=================================================================
【エルボー法とシルエットスコア(k=2〜6)】
k Inertia Silhouette Score
------------------------------------
2 99.38 0.2562
3 71.77 0.3007
4 55.67 0.3208
5 43.33 0.3284
6 36.51 0.3410
【クラスター別の基本統計(論文 Table 4 に対応)】
都道府県数 シャトルラン平均 肥満率平均 運動部加入率平均 朝食摂取率平均
cluster
1 11 81.90 10.79 78.27 82.91
2 22 77.56 12.23 71.62 79.06
3 14 78.51 14.83 77.52 81.63
論文の参考値: Cluster1=81.9, Cluster2=77.8, Cluster3=78.3
【クラスター1(標準型)の都道府県】
埼玉県, 新潟県, 富山県, 石川県, 福井県, 三重県, 滋賀県, 鳥取県, 島根県, 山口県, 長崎県
【クラスター2(生活習慣課題型)の都道府県】
北海道, 千葉県, 東京都, 神奈川県, 長野県, 岐阜県, 静岡県, 愛知県, 京都府, 大阪府, 兵庫県, 奈良県, 和歌山県, 岡山県, 香川県, 愛媛県, 高知県, 福岡県, 佐賀県, 熊本県, 鹿児島県, 沖縄県
【クラスター3(食文化・環境要因型)の都道府県】
青森県, 岩手県, 宮城県, 秋田県, 山形県, 福島県, 茨城県, 栃木県, 群馬県, 山梨県, 広島県, 徳島県, 大分県, 宮崎県df.groupby('列').apply(関数) — グループごとに関数を適用。時系列や地域別の集計でよく使います。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。s[:-n]「末尾n文字を除く」/s[n:]「先頭n文字を除く」。スライス [start:stop:step] はリスト・タプル・文字列共通の基本ワザです。386 387 388 | print("\n\n" + "=" * 65) print("■ 図の生成(4枚)") print("=" * 65) |
================================================================= ■ 図の生成(4枚) =================================================================
x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。389 390 391 392 393 394 | print("図3: エルボー法とシルエットスコアグラフを作成中...") fig3, axes3 = plt.subplots(1, 2, figsize=(12, 5)) fig3.suptitle('Step3. K-meansクラスタリング: 最適クラスター数の選択(k=3)\n' '変数: 県肥満率・運動部加入率・朝食摂取率(2022年度、47都道府県)', fontsize=11, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。np.cumsum(arr) は累積和、np.linspace(a, b, n) は「aからbを等間隔でn個」。NumPyの定石です。395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | # (a) エルボー法 ks = range(2, 7) ax3a = axes3[0] ax3a.plot(list(ks), inertias, 'o-', color='#1565C0', linewidth=2.2, markersize=8, markerfacecolor='white', markeredgewidth=2.5) ax3a.axvline(3, color='red', linestyle='--', linewidth=1.8, alpha=0.8, label='k=3 選択') for k, ine in zip(ks, inertias): ax3a.annotate(f'{ine:.1f}', (k, ine), textcoords='offset points', xytext=(0, 8), fontsize=8, ha='center') ax3a.set_xlabel('クラスター数 k', fontsize=11) ax3a.set_ylabel('Inertia(Within-cluster 平方和)', fontsize=10) ax3a.set_title('エルボー法', fontsize=11, fontweight='bold') ax3a.legend(fontsize=10) ax3a.grid(True, alpha=0.3) ax3a.set_xticks(list(ks)) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | # (b) シルエットスコア ax3b = axes3[1] ax3b.plot(list(ks), silhouettes, 's-', color='#E65100', linewidth=2.2, markersize=8, markerfacecolor='white', markeredgewidth=2.5) ax3b.axvline(3, color='red', linestyle='--', linewidth=1.8, alpha=0.8, label='k=3 選択') best_k = list(ks)[silhouettes.index(max(silhouettes))] ax3b.axvline(best_k, color='green', linestyle=':', linewidth=1.5, alpha=0.7, label=f'Silhouette最大 k={best_k}') for k, sil in zip(ks, silhouettes): ax3b.annotate(f'{sil:.3f}', (k, sil), textcoords='offset points', xytext=(0, 8), fontsize=8, ha='center') ax3b.set_xlabel('クラスター数 k', fontsize=11) ax3b.set_ylabel('Silhouette Score', fontsize=10) ax3b.set_title('シルエットスコア\n(高いほどクラスターの分離が明確)', fontsize=10, fontweight='bold') ax3b.legend(fontsize=9) ax3b.grid(True, alpha=0.3) ax3b.set_xticks(list(ks)) plt.tight_layout() save_fig('fig3_elbow') |
図3: エルボー法とシルエットスコアグラフを作成中... → html/figures/2025_U3_fig3_elbow.png 保存完了
ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。430 431 432 433 434 435 | print("図4: クラスター散布図とボックスプロットを作成中...") fig4, axes4 = plt.subplots(1, 3, figsize=(15, 5)) fig4.suptitle('Step3. K-meansクラスタリング(k=3)の結果 ― 47都道府県の3類型\n' '出典: スポーツ庁体力調査・愛媛県オープンデータ(2022年度実データ)', fontsize=11, fontweight='bold') |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。{値:.2f}(小数2桁)、{値:,}(3桁区切り)、{値:>10}(右寄せ10桁)など、覚えると出力が一気に整います。436 437 438 439 440 441 442 443 444 445 446 447 | # (a) 散布図: 肥満率 vs 運動部加入率 ax4a = axes4[0] for cl in [1, 2, 3]: mask = df_2022['cluster'] == cl ax4a.scatter(df_2022.loc[mask, 'bmi_rate'], df_2022.loc[mask, 'sports_club'], color=C_CLUSTER[cl], s=70, alpha=0.85, edgecolors='white', linewidth=0.5, label=C_LABELS[cl], zorder=3) ax4a.set_xlabel('県肥満率(%)', fontsize=10) ax4a.set_ylabel('運動部加入率(%)', fontsize=10) ax4a.set_title('肥満率 vs 運動部加入率', fontsize=10, fontweight='bold') ax4a.legend(fontsize=8, loc='upper right') ax4a.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。plt.subplots(figsize=(W, H)) で図サイズ指定、fig.savefig(..., bbox_inches='tight') で余白を自動で詰めて保存。448 449 450 451 452 453 454 455 456 457 458 459 | # (b) 散布図: 肥満率 vs 朝食摂取率 ax4b = axes4[1] for cl in [1, 2, 3]: mask = df_2022['cluster'] == cl ax4b.scatter(df_2022.loc[mask, 'bmi_rate'], df_2022.loc[mask, 'breakfast'], color=C_CLUSTER[cl], s=70, alpha=0.85, edgecolors='white', linewidth=0.5, label=C_LABELS[cl], zorder=3) ax4b.set_xlabel('県肥満率(%)', fontsize=10) ax4b.set_ylabel('朝食を毎日食べる割合(%)', fontsize=10) ax4b.set_title('肥満率 vs 朝食摂取率', fontsize=10, fontweight='bold') ax4b.legend(fontsize=8, loc='upper right') ax4b.grid(True, alpha=0.3) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.dropna() は欠損行を除去、.copy() は独立したコピーを作る。pandasで警告を防ぐ定石。460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | # (c) クラスター別シャトルラン平均(棒グラフ) ax4c = axes4[2] cl_order = [1, 2, 3] sl_means = [df_2022[df_2022['cluster'] == cl]['shuttle'].mean() for cl in cl_order] sl_stds = [df_2022[df_2022['cluster'] == cl]['shuttle'].std() for cl in cl_order] cl_names = ['Cluster1\n標準型', 'Cluster2\n生活習慣\n課題型', 'Cluster3\n食文化・\n環境要因型'] ax4c.bar(np.arange(3), sl_means, yerr=sl_stds, capsize=5, color=[C_CLUSTER[c] for c in cl_order], alpha=0.85, edgecolor='white', error_kw={'elinewidth': 1.5}) ax4c.set_xticks(np.arange(3)) ax4c.set_xticklabels(cl_names, fontsize=9) ax4c.set_ylabel('シャトルラン平均回数(回)', fontsize=10) ax4c.set_title('クラスター別\nシャトルラン平均', fontsize=10, fontweight='bold') ax4c.grid(axis='y', alpha=0.3) y_min_c = min(sl_means) - max(sl_stds) * 1.2 y_max_c = max(sl_means) + max(sl_stds) * 1.8 ax4c.set_ylim(y_min_c, y_max_c) for i, (m, s) in enumerate(zip(sl_means, sl_stds)): ax4c.text(i, m + s + 0.3, f'{m:.1f}', ha='center', fontsize=10, fontweight='bold') ax4c.text(0.5, 0.03, '論文参考値: C1=81.9, C2=77.8, C3=78.3', transform=ax4c.transAxes, fontsize=8, ha='center', bbox=dict(boxstyle='round', facecolor='#FFF9C4', alpha=0.9, edgecolor='#F9A825')) plt.tight_layout() save_fig('fig4_cluster') print("\n" + "=" * 65) print("✓ 全図の生成完了(4枚)") print(" fig1_trend.png : シャトルラン時系列推移(公表値+都道府県別実データ)") print(" fig2_fe.png : パネルモデル選択(F検定・Hausman)と固定効果係数") print(" fig3_elbow.png : エルボー法とシルエットスコア") print(" fig4_cluster.png : クラスター散布図と類型別シャトルラン平均") print("=" * 65) print("\n【最終サマリー】") print(f"\n ■ モデル選択") print(f" 時間固定効果 F={f_time:.2f} ({'有意' if p_time < 0.05 else '非有意'})") print(f" 個体固定効果 F={f_ind:.2f} ({'有意' if p_ind < 0.05 else '非有意'})") print(f" Two-way FE F={f_twoway:.2f} ({'有意' if p_twoway < 0.05 else '非有意'})") print(f" Hausman χ²={H_stat:.2f} p={p_H:.4f} → {'個体FE採用' if p_H < 0.05 else 'RE採用'}") print(f"\n ■ 個体固定効果モデルで有意な変数") for zv in Z_VARS: pi = pval_ind[zv] if pi < 0.1: sig = '***' if pi < 0.001 else '**' if pi < 0.01 else '*' if pi < 0.05 else '†' print(f" {VAR_LABELS[zv]:<22} β={coef_ind[zv]:+.4f} p={pi:.4f} {sig}") print(f"\n ■ K-meansクラスタリング(k=3)") for cl in [1, 2, 3]: n_cl = (df_2022['cluster'] == cl).sum() m_cl = df_2022[df_2022['cluster'] == cl]['shuttle'].mean() print(f" Cluster{cl}: n={n_cl}都道府県, シャトルラン平均={m_cl:.1f}回") |
図4: クラスター散布図とボックスプロットを作成中...
→ html/figures/2025_U3_fig4_cluster.png 保存完了
=================================================================
✓ 全図の生成完了(4枚)
fig1_trend.png : シャトルラン時系列推移(公表値+都道府県別実データ)
fig2_fe.png : パネルモデル選択(F検定・Hausman)と固定効果係数
fig3_elbow.png : エルボー法とシルエットスコア
fig4_cluster.png : クラスター散布図と類型別シャトルラン平均
=================================================================
【最終サマリー】
■ モデル選択
時間固定効果 F=12.87 (有意)
個体固定効果 F=7.62 (有意)
Two-way FE F=11.50 (有意)
Hausman χ²=26.23 p=0.0005 → 個体FE採用
■ 個体固定効果モデルで有意な変数
県肥満率 β=-0.9532 p=0.0092 **
運動部加入率 β=+3.2417 p=0.0000 ***
朝食を毎日食べる割合 β=+1.9731 p=0.0009 ***
■ K-meansクラスタリング(k=3)
Cluster1: n=11都道府県, シャトルラン平均=81.9回
Cluster2: n=22都道府県, シャトルラン平均=77.6回
Cluster3: n=14都道府県, シャトルラン平均=78.5回f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。47都道府県×3年のパネルデータと複数の統計手法を用いた分析の結果:
この論文のアプローチの巧みさは「回帰で変数を特定 → クラスタリングで類型化」という2段階設計にある。回帰で特定した有意変数だけをクラスタリングの入力に使うことで、解釈可能で政策的含意のある類型化が実現できる。
以下のファイルをダウンロードして同じフォルダに置き、
python 2025_U3_suri.py を実行するだけで全図・全結果を再現できます。
スポーツ庁・SSDSE-B から収集・加工した実データです(47都道府県×3年)。
47都道府県 × 3年(2019・2021・2022年)の 141行。シャトルラン・持久走・スポーツ部活・朝食率・肥満率・気温・降水量など。
data_prep.py:スポーツ庁Excel + SSDSE-B → CSV の変換スクリプト。
suri.py:CSVを読み込んでパネル回帰・K-meansクラスタリング・全図を生成。必要ライブラリ:numpy, pandas, matplotlib, statsmodels, linearmodels, scikit-learn。
| データ | 出典 |
|---|---|
| 体力・運動能力調査(シャトルラン・持久走) | スポーツ庁 体力・運動能力調査 都道府県別集計 |
| 生活習慣アンケート(朝食・スポーツ部活) | スポーツ庁 体力・運動能力調査 質問紙調査 |
| 肥満傾向率 | 文科省 学校保健統計調査 |
| 気温・降水量 | SSDSE-B(統計でみる都道府県のすがた)2026年版 |
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。
この論文を理解できたか、クリックで確認しましょう。間違えても解説が出ます。
このページの分析は、この画面の中でそのまま実行できます。
Python をインストールする必要も、CSV をダウンロードする必要もありません。
下のセルの 「▶ ブラウザで実行」 を上から順に押すか、
「▶ 最初から全部実行」 で一気に流してください。
表示されるのは、本文の図表とまったく同じ計算の結果です
(動かしているのは再現スクリプト code/2025_U3_suri.py そのもの)。
コードは書き換えられます。「✏️ 書き換える」で数字や変数を変えて ▶ を押すと、 結果がどう変わるかをその場で確かめられます(元に戻すボタンつき)。 ページを開いた時点で裏で実行環境の準備が始まっているので、待ち時間はほとんどありません。 初回だけ実行環境の取得にインターネット接続が必要です。