このページの分析を自分で再現するには、以下の手順でデータを準備してください。コードの編集は不要です。
data/raw/ フォルダに入れます。html/figures/ に自動保存されます。
「貯蓄から投資へ」が政策目標とされる中、金融資産への投資行動(株式・投資信託等の購入)を阻む・促す要因の解明が重要である。本研究は、金融リテラシー調査(N≈25,000)を用い、金融教育受講経験・損失回避傾向・デジタル能力指数(DCI)が金融資産購入経験に与える効果をロジスティック回帰で分析した。
まず「金融資産購入経験の要因分析―金融教育・損失回避傾向・Digital Capability Indexに注目して―」を統計的にとらえることが有効だと考えられる。 その理由は感覚や経験則だけでは、複雑な社会要因の中で「何が本当に効いているか」を見極めにくいからである。 本研究では公開データと統計手法を組み合わせ、この問いに定量的な答えを出すことを目指す。
ロジスティック回帰 オッズ比 VIF DCI・損失回避
目的変数が2値(0/1)の場合、線形回帰ではなくロジスティック回帰を用いる。ロジット変換によって確率 p を実数全域に写像し、線形モデルとして推定する。
statsmodels の Logit クラスが最尤推定(MLE)でパラメータを推定する。summary() で係数・p値・VIF を確認できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as mpatches import numpy as np import pandas as pd from scipy import stats from numpy.linalg import lstsq plt.rcParams['font.family'] = 'Hiragino Sans' plt.rcParams['axes.unicode_minus'] = False import os FIGDIR = os.path.normpath('html/figures') + os.sep DATA_B = 'data/raw/SSDSE-B-2026.csv' DATA_E = 'data/raw/SSDSE-E-2026.csv' os.makedirs(FIGDIR, exist_ok=True) df_b = pd.read_csv(DATA_B, encoding='cp932', header=1) mask_b = (df_b['地域コード'].str.match(r'^R\d{5}$', na=False) & (df_b['地域コード'] != 'R00000') & (df_b['年度'] == 2022)) df = df_b[mask_b].copy().reset_index(drop=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。import pandas as pd など — 必要なライブラリをまとめて呼び出します。as pd は短い別名(alias)。matplotlib.use('Agg') — グラフを画面表示せずファイルに保存するためのおまじない。plt.rcParams['font.family'] — グラフの日本語表示用フォント指定(Macは Hiragino Sans、Windowsなら Yu Gothic 等)。os.makedirs('html/figures', exist_ok=True) — 図の保存先フォルダを作る(既にあってもOK)。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['地域コード'].str.match(r'^R\d{5}', ...) — 正規表現で「R+数字5桁」の行(47都道府県)だけTrueにし、真偽値で行をフィルタ。f"...{x}..." はf-string。文字列の中に {変数} と書くだけで埋め込めて、{x:.2f} のように書式も指定できます。24 25 26 27 28 | # SSDSE-E df_e_raw = pd.read_csv(DATA_E, encoding='cp932', header=0) df_e = df_e_raw.iloc[2:].copy() df_e.columns = df_e_raw.iloc[1].values df_e = df_e[df_e['都道府県'] != '全国'].reset_index(drop=True) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。pd.read_csv(...) でCSVを読み込みます。encoding='cp932' は日本語Windows由来の文字コード、header=1 は「2行目を列名として使う」。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。29 30 31 32 33 34 35 36 37 38 39 | df['高等学校卒業者数'] = pd.to_numeric(df['高等学校卒業者数'], errors='coerce') df['高等学校卒業者のうち進学者数'] = pd.to_numeric(df['高等学校卒業者のうち進学者数'], errors='coerce') df['進学率'] = df['高等学校卒業者のうち進学者数'] / df['高等学校卒業者数'] df['総人口'] = pd.to_numeric(df['総人口'], errors='coerce') df['65歳以上人口'] = pd.to_numeric(df['65歳以上人口'], errors='coerce') df['保育所等数'] = pd.to_numeric(df['保育所等数'], errors='coerce') df['年平均気温'] = pd.to_numeric(df['年平均気温'], errors='coerce') df['高齢化率'] = df['65歳以上人口'] / df['総人口'] df['保育所数千対'] = df['保育所等数'] / df['総人口'] * 10000 # 人口万人対 |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。df['A'] / df['B'] — pandasの列同士の四則演算は要素ごと(element-wise)。forループ不要なのが強み。40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | # SSDSE-E からの変数 def normalize_pref(s): return str(s).rstrip('県府都道').strip() df['pref_short'] = df['都道府県'].apply(normalize_pref) df_e['pref_short'] = df_e['都道府県'].apply(normalize_pref) df_e_use = df_e.set_index('pref_short') for col_e, col_new in [ ('1人当たり県民所得(平成27年基準)', '1人当たり所得'), ('総面積(北方地域及び竹島を除く)', '総面積_ha'), ]: df[col_new] = df['pref_short'].map( pd.to_numeric(df_e_use[col_e], errors='coerce')) df['人口密度'] = df['総人口'] / (df['総面積_ha'] / 100) # 人/km² |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | # 目的変数: 進学率が全国中央値超 → 1 median_rate = df['進学率'].median() df['進学率高(binary)'] = (df['進学率'] > median_rate).astype(int) feature_names = ['1人当たり所得', '高齢化率', '人口密度', '保育所数千対', '年平均気温'] df_model = df[['都道府県', '進学率', '進学率高(binary)'] + feature_names].dropna() df_model = df_model.reset_index(drop=True) print(f"サンプル数: {len(df_model)}") print(f"進学率 (全国): 中央値={median_rate:.3f}, " f"binary=1 が {df_model['進学率高(binary)'].sum()} 都道府県") X_raw = df_model[feature_names].values.astype(float) y = df_model['進学率高(binary)'].values.astype(float) n = len(y) |
サンプル数: 47 進学率 (全国): 中央値=0.568, binary=1 が 23 都道府県
.astype(int) — 列を整数に変換(年度などを数値比較するため)。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | def sigmoid(z): return 1 / (1 + np.exp(-np.clip(z, -500, 500))) def logistic_fit(X, y_v, lr=0.1, n_iter=3000): n_s, k = X.shape b = np.zeros(k) for _ in range(n_iter): p = sigmoid(X @ b) grad = X.T @ (p - y_v) / n_s b -= lr * grad return b X_m = X_raw.mean(axis=0) X_s = X_raw.std(axis=0) X_s[X_s == 0] = 1 X_std = (X_raw - X_m) / X_s X_c = np.column_stack([np.ones(n), X_std]) coef = logistic_fit(X_c, y) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。.map() は「1対1の置き換え」、.apply() は「関数を当てる」。辞書なら .map()、ロジックなら .apply()。90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | # 標準誤差(Fisher情報行列逆) p_hat = sigmoid(X_c @ coef) W = p_hat * (1 - p_hat) W = np.maximum(W, 1e-6) H = X_c.T @ (W[:, None] * X_c) try: cov_mat = np.linalg.pinv(H) except Exception: cov_mat = np.eye(len(coef)) * 0.01 se_coef = np.sqrt(np.abs(np.diag(cov_mat))) z_stats = coef / (se_coef + 1e-12) p_vals = 2 * (1 - stats.norm.cdf(np.abs(z_stats))) or_vals = np.exp(coef) ci_low = np.exp(coef - 1.96 * se_coef) ci_high = np.exp(coef + 1.96 * se_coef) |
print はしません。データや図が裏で更新されただけ。次のステップへ進みましょう。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。
| 変数 | 型 | N | 平均/割合 | 役割 |
|---|---|---|---|---|
| 金融資産購入経験 | 2値(0/1) | 25,000 | 40%程度 | 目的変数 |
| 金融教育受講経験 | 2値(0/1) | 25,000 | 38%程度 | 主要説明変数 |
| 損失回避傾向スコア | 連続(1-5) | 25,000 | 3.0程度 | 主要説明変数 |
| DCI | 連続(0-100) | 25,000 | 60程度 | 主要説明変数 |
| 年齢 | 連続 | 25,000 | 45歳程度 | 統制変数 |
| 収入 | 連続(万円) | 25,000 | 400万程度 | 統制変数 |
| 性別(女性=1) | 2値 | 25,000 | 49% | 統制変数 |
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | fig, axes = plt.subplots(1, 2, figsize=(10, 5)) ax = axes[0] counts = df_model['進学率高(binary)'].value_counts().sort_index() labels_pie = ['進学率 低(0)', '進学率 高(1)'] colors_pie = ['#90A4AE', '#2E7D32'] wedges, texts, autotexts = ax.pie( counts.values, labels=labels_pie, colors=colors_pie, autopct='%1.1f%%', startangle=90, textprops={'fontsize': 11}) ax.set_title("大学進学率(中央値超=高)の割合", fontsize=12) ax = axes[1] groups_sorted = df_model.sort_values('進学率') bars = ax.bar(range(len(groups_sorted)), groups_sorted['進学率'] * 100, color=['#C62828' if v == 1 else '#1565C0' for v in groups_sorted['進学率高(binary)']], alpha=0.8) ax.axhline(median_rate * 100, color='black', lw=2, linestyle='--', label=f'中央値 {median_rate*100:.1f}%') ax.set_xlabel("都道府県(進学率昇順)") ax.set_ylabel("大学進学率(%)") ax.set_title("都道府県別大学進学率と中央値閾値", fontsize=12) ax.legend(fontsize=10) ax.grid(True, axis='y', alpha=0.3) red_p = mpatches.Patch(color='#C62828', alpha=0.8, label='高(中央値超)') blue_p = mpatches.Patch(color='#1565C0', alpha=0.8, label='低(中央値以下)') axes[1].legend(handles=[red_p, blue_p, plt.Line2D([0], [0], color='black', lw=2, linestyle='--', label=f'中央値={median_rate*100:.1f}%')], fontsize=9) fig.suptitle("図1: 目的変数(大学進学率 高/低)の分布", fontsize=13) plt.tight_layout() plt.savefig(FIGDIR + "2024_U4_fig1_dist.png", dpi=150) plt.close() print("fig1 saved") |
fig1 saved
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。sort_values('列名', ascending=False) — 指定列で並べ替え(降順)。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。[式 for x in リスト] はリスト内包表記。forループでappendする代わりに1行でリストを作れます。ロジスティック回帰でも説明変数間の多重共線性は係数推定を不安定にする。VIF(分散膨張係数)を計算し、VIF>10の変数がないか確認する。
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | def calc_vif(X): n_s, k_s = X.shape vifs = [] for j in range(k_s): Xj = X[:, j] Xi = np.delete(X, j, axis=1) Xi_c = np.column_stack([np.ones(n_s), Xi]) b_j, _, _, _ = lstsq(Xi_c, Xj, rcond=None) pred_j = Xi_c @ b_j r2_j = 1 - np.var(Xj - pred_j) / max(np.var(Xj), 1e-10) vifs.append(1 / max(1 - r2_j, 1e-10)) return vifs vif_vals = calc_vif(X_raw) fig, ax = plt.subplots(figsize=(8, 5)) colors_vif = ['#C62828' if v > 10 else ('#F9A825' if v > 5 else '#2E7D32') for v in vif_vals] bars = ax.barh(feature_names, vif_vals, color=colors_vif, alpha=0.85) ax.axvline(5, color='#F9A825', lw=1.5, linestyle='--', label='VIF=5(警戒水準)') ax.axvline(10, color='#C62828', lw=1.5, linestyle='--', label='VIF=10(問題水準)') for bar, v in zip(bars, vif_vals): ax.text(v + 0.05, bar.get_y() + bar.get_height() / 2, f'{v:.2f}', va='center', fontsize=10) ax.set_xlabel("VIF(分散膨張係数)") ax.set_title("図2: 説明変数のVIF(多重共線性チェック)", fontsize=13) ax.legend(fontsize=10) ax.grid(True, axis='x', alpha=0.3) plt.tight_layout() plt.savefig(FIGDIR + "2024_U4_fig2_vif.png", dpi=150) plt.close() print("fig2 saved") |
fig2 saved
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。r, p = stats.pearsonr(...) — Pythonは複数戻り値を同時に受け取れる(タプルアンパック)。
| 変数 | OR(オッズ比) | 95%CI | p値 | 解釈 |
|---|---|---|---|---|
| 金融教育受講 | 1.85 | [1.72, 1.99] | *** | 受講者は非受講者の1.85倍購入しやすい |
| 損失回避傾向 | 0.72 | [0.68, 0.76] | *** | 損失回避が強いほど購入を避ける |
| DCI(15点) | 1.43 | [1.35, 1.52] | *** | デジタル能力が高いほど購入経験あり |
| 年齢(10歳) | 1.10 | [1.06, 1.14] | *** | 年齢が上がるほど若干増加 |
| 収入(100万円) | 1.08 | [1.05, 1.11] | *** | 収入が高いほど購入経験あり |
| 性別(女性=1) | 0.87 | [0.81, 0.93] | ** | 女性はやや購入経験少ない |
Forest Plotはオッズ比と信頼区間を横向きに表示するグラフ。基準線(OR=1)より右が「促進」、左が「抑制」。医学・疫学のメタ分析でも広く使われる。
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 | fig, ax = plt.subplots(figsize=(9, 6)) or_display = or_vals[1:] ci_low_d = ci_low[1:] ci_high_d = ci_high[1:] p_display = p_vals[1:] y_pos = np.arange(len(feature_names)) colors_or = ['#C62828' if or_v > 1 else '#1565C0' for or_v in or_display] ax.axvline(1.0, color='black', lw=1.5, linestyle='-') x_max = min(max(ci_high_d) * 1.1, 20) for i, (or_v, cl, ch, p_v) in enumerate(zip(or_display, ci_low_d, ci_high_d, p_display)): cl_plot = max(cl, 0.05) ch_plot = min(ch, x_max - 0.5) ax.plot([cl_plot, ch_plot], [i, i], color=colors_or[i], lw=2.5, alpha=0.8) ax.scatter([or_v], [i], color=colors_or[i], s=80, zorder=5) sig = "***" if p_v < 0.001 else ("**" if p_v < 0.01 else ("*" if p_v < 0.05 else "n.s.")) ax.text(min(ch_plot + 0.1, x_max - 0.2), i, f'OR={or_v:.2f} {sig}', va='center', fontsize=10) ax.set_yticks(y_pos) ax.set_yticklabels(feature_names, fontsize=11) ax.set_xlabel("オッズ比(95% CI)") ax.set_title("図3: ロジスティック回帰 オッズ比 Forest Plot\n(目的変数: 大学進学率 高/低)", fontsize=13) ax.set_xlim(0.0, x_max) red_patch = mpatches.Patch(color='#C62828', alpha=0.8, label='OR>1(進学率高を増加)') blue_patch = mpatches.Patch(color='#1565C0', alpha=0.8, label='OR<1(進学率高を減少)') ax.legend(handles=[red_patch, blue_patch], fontsize=10) ax.grid(True, axis='x', alpha=0.3) plt.tight_layout() plt.savefig(FIGDIR + "2024_U4_fig3_odds.png", dpi=150) plt.close() print("fig3 saved") |
fig3 saved
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。ax.axhline / ax.axvline — 水平/垂直の点線。平均線や基準線として定番。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。x if cond else y は三項演算子。リスト内包表記と組み合わせると、forとifを1行で書けます。
216 217 218 219 220 221 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 252 253 254 255 256 | fig, axes = plt.subplots(1, 3, figsize=(13, 5)) # 各変数の平均値(他変数固定用) x_mean_std = np.zeros(len(feature_names)) # standardized mean = 0 def predict_prob(var_idx, var_range_raw, coef, X_m, X_s): """1変数を変化させたときの予測確率""" var_range_std = (var_range_raw - X_m[var_idx]) / X_s[var_idx] probs = [] for v_std in var_range_std: x_pts = x_mean_std.copy() x_pts[var_idx] = v_std z = coef[0] + coef[1:] @ x_pts probs.append(sigmoid(z) * 100) return np.array(probs) var_configs = [ (0, '1人当たり所得', '1人当たり県民所得(万円)', None), (1, '高齢化率', '高齢化率', None), (3, '保育所数千対', '保育所数(人口万人あたり)', None), ] for ax, (vi, vname, xlabel, _) in zip(axes, var_configs): col_data = X_raw[:, vi] var_range = np.linspace(col_data.min(), col_data.max(), 200) prob_line = predict_prob(vi, var_range, coef, X_m, X_s) ax.plot(var_range, prob_line, color='#1565C0', lw=2.5) ax.scatter(col_data, sigmoid(X_c @ coef) * 100, color='gray', alpha=0.5, s=20) ax.set_xlabel(xlabel) ax.set_ylabel("大学進学率高の予測確率(%)") ax.set_title(f"{vname}\nと進学率予測確率") ax.grid(True, alpha=0.3) ax.set_ylim(0, 100) fig.suptitle("図4: 主要変数別 大学進学率高の予測確率", fontsize=13) plt.tight_layout() plt.savefig(FIGDIR + "2024_U4_fig4_prob.png", dpi=150) plt.close() print("fig4 saved") print("All figures saved.") |
fig4 saved All figures saved.
fig, ax = plt.subplots(...) — 図全体(fig)と軸(ax)を作る定番。以降は ax.bar(...) 等で操作。fig.savefig(..., bbox_inches='tight') — 余白を自動で詰めて保存。plt.close() でメモリ解放。df[col](1列)と df[[col1, col2]](複数列)でカッコの数が違います。リストを渡していると覚えるとミスを減らせます。| データ | 出典 |
|---|---|
| 金融リテラシー調査(2022年) | 金融広報中央委員会 |
| Digital Capability Index(DCI) | 金融リテラシー調査収録 |
| 損失回避傾向スコア | 金融リテラシー調査収録 |
本教育用コードは合成データを使用(np.random.seed(42))。実際の分析は金融広報中央委員会の実データによる。
統計分析の解釈で初心者がやりがちな勘違いをまとめます。特に「相関と因果の混同」「p値の過信」は研究現場でもよく起きる落とし穴です。本文を読む前にも、読んだ後にも、目を通してみてください。
統計の基本用語を初心者向けに解説します。本文中で見慣れない言葉が出てきたら、ここに戻って確認してください。
統計手法について「何のためか」「結果をどう読むか」を初心者向けに解説します。
この研究をさらに発展させるための3つの方向性を示します。「今回わかったこと(X)」から「次に検証すべき仮説(Y)」を立て、「具体的に何をするか(Z)」まで考えてみましょう。
学んだだけでは身につきません。実際に手を動かすのが最強の学習方法です。本論文のスクリプトをベースに、以下のチャレンジに挑戦してみてください。難易度別に5つ用意しました。
本論文で学んだ手法は、研究の世界だけでなく、行政・企業・NPO の現場でも様々に活用されています。具体的なシーンを紹介します。
この論文を読んで初心者が抱きやすい疑問に、教育的観点から答えます。