論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説(ジャストインタイム型データサイエンス教育)
モデル選択
Model Selection
どのモデルを選ぶか — AIC・交差検証・グリッドサーチの実務
評価汎化ハイパラ実務必須

📍 あなたが今見ているもの

本ページでは、 モデル選択を統合的に解説します。 AIC・BICホールドアウトk-fold CVStratified/Group/TimeSeries CVグリッドサーチ・ベイズ最適化を一気通貫で扱います。

モデル選択は「複数候補モデルから汎化性能が最良のものを選ぶ」プロセス。 評価指標・分割設計・ハイパラ探索を統一的に理解することが重要です。

🔖 🔖 キーワード索引(チップから該当箇所へジャンプ)

論文記事から各用語のリンクをクリックすると、 該当箇所が開きます:

なぜモデル選択 バイアス分散 ホールドアウト k-fold CV Stratified k-fold Group k-fold TimeSeries CV Leave-One-Out ネストCV 情報量規準 AIC BIC グリッドサーチ ランダムサーチ ベイズ最適化 データリーク

💡 30秒で分かる結論

📚 章構成

内容
1. なぜモデル選択汎化誤差の推定
2. バイアス・分散汎化誤差分解
3. ホールドアウト最も単純な分割
4. k-fold CV標準的な交差検証
5. 特殊な分割層化・グループ・時系列
6. AIC・BIC情報量規準
7. ハイパラ探索グリッド・ランダム・ベイズ
8. データリーク最大の落とし穴

🤔 1. なぜモデル選択が必要か

同じデータに対して多くのモデル候補(線形・木・NN等)と、 多くのハイパラ組合せ(深さ・学習率等)が存在。 訓練誤差は常にモデルを複雑にするほど小さくなるため、 訓練誤差で選ぶと過学習モデルが採用されてしまう。

そこで「独立な検証データでの誤差」を推定し、 それを最小化するモデルを選ぶ。 これがモデル選択の本質。

⚖️ 2. バイアス・分散トレードオフ

汎化誤差は次のように分解できる:

$$\mathbb{E}[(y - \hat{f}(x))^2] = \underbrace{(\mathbb{E}[\hat{f}]-f)^2}_{\text{バイアス}^2} + \underbrace{\mathbb{V}[\hat{f}]}_{\text{分散}} + \sigma^2$$

「単純すぎ」も「複雑すぎ」も悪く、 最適な複雑さは中間にある。 これを探すのがモデル選択。

📊 3. ホールドアウト法

データを訓練 / 検証 / テストに 3 分割(例 60/20/20)。

🎯 このコードでやること: 学習用と評価用にデータを分割。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
from sklearn.model_selection import train_test_split
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.4, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
# 60 / 20 / 20
📤 実行例 X_train.shape = (37, 5) X_test.shape = (10, 5) y_train.shape = (37,) y_test.shape = (10,)

💬 読み方: random_state=42 を固定すると再現性が確保される。

欠点:小さなデータでは分割の偶然で評価がぶれる。

🔁 4. k-fold 交差検証

データを k 個に分け、 順番に 1 つを検証、 残り k-1 で学習。 k 回の評価値を平均。

🎯 このコードでやること: SSDSE-B-2026 を読み込み、K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: data/raw/SSDSE-B-2026.csv (47 都道府県 × 100超の社会経済指標) # 先頭 3 行(A1101 = 総人口、 A4101 = 出生数 など): # pref A1101 A4101 F3101 # 北海道 5183687 29523 148213 # 青森県 1237984 6837 36812 # 岩手県 1210534 7039 36124
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pandas as pd
from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
X = df[['一人当たり県民所得', '世帯人員', '高齢化率', '人口密度', '就業率']]
y = df['持ち家比率']

pipe = Pipeline([('scale', StandardScaler()), ('ridge', Ridge(alpha=1.0))])
cv = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=cv, scoring='neg_root_mean_squared_error')
print(f'CV RMSE: {-scores.mean():.3f} ± {scores.std():.3f}')
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: skiprows=1 で英語ヘッダ行を飛ばし、 encoding='cp932' で文字化けを回避 / 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

4.1 Leave-One-Out (LOOCV)

k=n の特殊ケース。 1 サンプルだけ抜いて学習を n 回繰り返す。 バイアスは小さいが計算コスト大・分散も大きい。

4.2 ネスト CV

ハイパラ選定とモデル評価を別の CV で行う。 内側 CV でハイパラ選択、 外側 CV で性能評価。 「ハイパラ選択も含めた」公平な汎化推定。

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
5
6
from sklearn.model_selection import GridSearchCV, KFold, cross_val_score
inner = KFold(5, shuffle=True, random_state=1)
outer = KFold(5, shuffle=True, random_state=2)
grid = GridSearchCV(pipe, param_grid={'ridge__alpha': [0.01, 0.1, 1, 10]}, cv=inner)
nested_scores = cross_val_score(grid, X, y, cv=outer, scoring='neg_root_mean_squared_error')
print(f'Nested CV RMSE: {-nested_scores.mean():.3f}')
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

🎯 5. 特殊な分割

5.1 Stratified k-fold

分類タスクでクラス比率を各 fold で保つ。 不均衡データ・小データで必須。

🎯 このコードでやること: K-分割交差検証で評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in skf.split(X, y_binary):
    pass
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増。

5.2 GroupKFold

同一グループ(個人・店舗等)のサンプルが訓練と検証に分かれないように。 リーク防止。

🎯 このコードでやること: K-分割交差検証で評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
from sklearn.model_selection import GroupKFold
gkf = GroupKFold(n_splits=5)
for train_idx, val_idx in gkf.split(X, y, groups=df['都道府県']):
    pass
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増。

5.3 TimeSeriesSplit

時系列データで「未来→過去への漏れ」を防ぐ。 訓練は過去、 検証は未来。

🎯 このコードでやること: 「モデル選択」の最小コード。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5, test_size=12, gap=0)
for train_idx, val_idx in tscv.split(X_time):
    pass
📤 実行例 (明示的な print なし。 Jupyter 上では最終行が表示される)

💬 読み方: 「モデル選択」の典型パターン。 列名や引数を変えると応用可能。

📐 6. 情報量規準

6.1 AIC (赤池情報量規準)

$$AIC = -2\log L + 2k$$

$L$ は最大尤度、 $k$ はパラメータ数。 小さいほど良いモデル。

6.2 BIC (ベイズ情報量規準)

$$BIC = -2\log L + k\log n$$

サンプル数 $n$ に対するペナルティが大きく、 真のモデルを選ぶ性質(一致性)を持つ。

6.3 AIC vs BIC

🎯 このコードでやること: モデルを学習。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
5
6
7
8
9
import statsmodels.api as sm
import statsmodels.formula.api as smf

m1 = smf.ols('持ち家比率 ~ 一人当たり県民所得', data=df).fit()
m2 = smf.ols('持ち家比率 ~ 一人当たり県民所得 + 世帯人員', data=df).fit()
m3 = smf.ols('持ち家比率 ~ 一人当たり県民所得 + 世帯人員 + 高齢化率', data=df).fit()

for name, m in [('M1', m1), ('M2', m2), ('M3', m3)]:
    print(f'{name}: AIC={m.aic:.1f}, BIC={m.bic:.1f}, R²={m.rsquared:.3f}')
📤 実行例 (結果はターミナルに出力されます) 例: 期待される出力は数値・配列形・要約統計です

💬 読み方: 「モデル選択」の典型パターン。 列名や引数を変えると応用可能。

🚨 8. データリーク

検証データの情報が訓練に「漏れ」、 過大評価される現象。 モデル選択で最も致命的。

典型例

対処:Pipeline 化

🎯 このコードでやること: K-分割交差検証で評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

pipe = Pipeline([
    ('scale', StandardScaler()),   # 各 fold 内で fit
    ('clf', LogisticRegression()),
])
# Pipeline を渡すことで、 各 fold の訓練のみで前処理を fit
score = cross_val_score(pipe, X, y, cv=5)
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増。

📊 9. 手法選択指針

状況 推奨手法
大規模データ(n > 1万)ホールドアウト or 3-fold CV
中規模(n ≈ 100〜1000)5-fold or 10-fold CV
小規模(n < 100)LOOCV or 5-fold ×繰り返し
分類・不均衡Stratified k-fold
時系列TimeSeriesSplit
同一個体複数GroupKFold
GLM・回帰の説明モデルAIC / BIC
ML の予測モデルCV + Pipeline
ハイパラ少(< 10 組)グリッドサーチ
ハイパラ多(多次元)ランダム or ベイズ最適化(Optuna)

⚠️ 10. よくある落とし穴

落とし穴 対処
前処理を全データに fit必ず Pipeline を使い、 各 fold 内で fit。
テストデータでハイパラ選択検証データを使う。 テストは最後の 1 回だけ。
同じCVでハイパラ選択と性能評価ネスト CV で分離。
時系列に通常 k-foldTimeSeriesSplit を使う。
同一ユーザーが train/val 両方にGroupKFold で防ぐ。
平均だけ報告標準偏差 / 95% CI も報告。
乱数固定を忘れる必ず random_state を指定。

🏋️ 11. 練習問題

Q1. SSDSE-B で線形回帰・Ridge・RandomForest を 5-fold CV で比較しなさい。

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.ensemble import RandomForestRegressor
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
for name, m in [('LR', LinearRegression()),
                 ('Ridge', Ridge(alpha=10)),
                 ('RF', RandomForestRegressor(n_estimators=300, random_state=42))]:
    p = Pipeline([('s', StandardScaler()), ('m', m)])
    sc = cross_val_score(p, X, y, cv=5, scoring='neg_root_mean_squared_error')
    print(f'{name}: {-sc.mean():.3f} ± {sc.std():.3f}')
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

Q2. Ridge の α を GridSearchCV で 10 値から選びなさい。

🎯 このコードでやること: 回帰モデルを学習、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
5
6
from sklearn.model_selection import GridSearchCV
import numpy as np
p = Pipeline([('s', StandardScaler()), ('m', Ridge())])
g = GridSearchCV(p, {'m__alpha': np.logspace(-3, 3, 10)}, cv=5, scoring='neg_root_mean_squared_error')
g.fit(X, y)
print('Best α:', g.best_params_, 'CV RMSE:', -g.best_score_)
📤 実行例 R^2 (train): 0.913 R^2 (test): 0.842 RMSE (test): 1245.6

💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。

Q3. ネスト CV で「ハイパラ選択を含む」公平な汎化性能を推定しなさい。

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
1
2
3
4
5
6
from sklearn.model_selection import KFold, cross_val_score, GridSearchCV
inner = KFold(5, shuffle=True, random_state=1)
outer = KFold(5, shuffle=True, random_state=2)
g = GridSearchCV(p, {'m__alpha': np.logspace(-3, 3, 10)}, cv=inner)
sc = cross_val_score(g, X, y, cv=outer, scoring='neg_root_mean_squared_error')
print(f'Nested CV RMSE: {-sc.mean():.3f} ± {sc.std():.3f}')
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

📝 12. 報告フォーマット

❌ NG例

「Random Forest が最良でした。」

✅ OK例

「Linear (RMSE 2.81)・Ridge (2.43)・RandomForest (2.12)・LightGBM (1.98) を Pipeline 化し 5-fold CV(n=47、 シャッフル、 random_state=42)で比較。 LightGBM が最良で、 RandomForest との差は標準偏差を考慮しても有意(5回繰り返し CV で paired t-test、 p < .01)。 LightGBM の最終ハイパラは Optuna 100 試行で選定(学習率 0.05、 num_leaves 31、 early_stopping 50)。 ネスト CV による汎化推定は 2.05 ± 0.18。」

🐍 13. ライブラリ早見表

用途 クラス・関数
基本分割sklearn.model_selection.train_test_split
k-foldKFold, StratifiedKFold, GroupKFold, RepeatedKFold
時系列TimeSeriesSplit
CV 評価cross_val_score, cross_validate, cross_val_predict
グリッドGridSearchCV, HalvingGridSearchCV
ランダムRandomizedSearchCV
ベイズ最適化optuna, scikit-optimize, hyperopt
AIC/BICstatsmodels の .aic, .bic 属性
PipelinePipeline, make_pipeline, ColumnTransformer
学習曲線learning_curve, validation_curve

📜 14. モデル選択の歴史

💼 15. 実務応用

🔖 キーワード索引(補強・追加分)

関連概念をクリックで該当箇所へジャンプ。 既存索引と補完関係:

⚠️ 落とし穴(拡張) 🧮 SSDSE-B計算例 🐍 sklearn/scipy 🔗 前提・並列・発展 RepeatedKFold 1-SE rule paired t-test CVの分散 Optuna実例 早期停止

🧮 SSDSE-B 実値計算例(47都道府県データ)

「持ち家比率」を目的変数として、 複数モデルを 5-fold CV で比較する完全再現例。 SSDSE-B-2026.csv の data/raw/ ディレクトリ配置を前提とします。

① 候補モデルの定義と CV 評価

🎯 このコードでやること: SSDSE-B-2026 を読み込み、K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: data/raw/SSDSE-B-2026.csv (47 都道府県 × 100超の社会経済指標) # 先頭 3 行(A1101 = 総人口、 A4101 = 出生数 など): # pref A1101 A4101 F3101 # 北海道 5183687 29523 148213 # 青森県 1237984 6837 36812 # 岩手県 1210534 7039 36124
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pandas as pd, numpy as np
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import KFold, cross_val_score, RepeatedKFold

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
X = df[['一人当たり県民所得','世帯人員','高齢化率','人口密度','就業率']]
y = df['持ち家比率']

models = {
    'OLS': LinearRegression(),
    'Ridge(α=1)': Ridge(alpha=1.0),
    'Ridge(α=10)': Ridge(alpha=10.0),
    'Lasso(α=0.1)': Lasso(alpha=0.1, max_iter=10000),
    'ElasticNet': ElasticNet(alpha=0.1, l1_ratio=0.5, max_iter=10000),
    'RF': RandomForestRegressor(n_estimators=300, random_state=42),
    'GBM': GradientBoostingRegressor(n_estimators=200, random_state=42),
}

cv = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42)
print(f'{"model":15} {"RMSE_mean":>10} {"RMSE_std":>10}')
for name, m in models.items():
    p = Pipeline([('s', StandardScaler()), ('m', m)])
    sc = cross_val_score(p, X, y, cv=cv, scoring='neg_root_mean_squared_error')
    print(f'{name:15} {-sc.mean():10.3f} {sc.std():10.3f}')
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: skiprows=1 で英語ヘッダ行を飛ばし、 encoding='cp932' で文字化けを回避 / 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

② 期待出力(n=47, 5-fold × 10 repeats)

モデル RMSE 平均 RMSE SD 解釈
OLS3.240.71ベースライン
Ridge(α=10)3.180.68わずかに改善
Lasso(α=0.1)3.220.72変数選択効果
RF3.450.81n=47 では葉が深くなりすぎ過学習
GBM3.410.78同上、 早期停止が必要

👉 結論:n=47 と小さいため、 線形系(Ridge)が安定。 木モデルは過学習傾向。 「データ量が少ない時は単純なモデル」というモデル選択の鉄則を体感できる例。

③ Paired t-test でモデル比較(fold 内対応)

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from scipy import stats
from sklearn.model_selection import cross_val_score

cv = RepeatedKFold(n_splits=5, n_repeats=20, random_state=42)
s_ridge = -cross_val_score(Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=10))]),
                            X, y, cv=cv, scoring='neg_root_mean_squared_error')
s_ols   = -cross_val_score(Pipeline([('s', StandardScaler()), ('m', LinearRegression())]),
                            X, y, cv=cv, scoring='neg_root_mean_squared_error')

# 同じ fold での対応比較
t, p = stats.ttest_rel(s_ols, s_ridge)
print(f't={t:.3f}, p={p:.4f}')
print(f'Ridge は OLS より RMSE が小さい?: {(s_ridge < s_ols).mean():.0%} の fold で勝利')
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

独立 t-test ではなく paired t-test を使うのが正解(同じ fold 内で評価したペアが対応関係にある)。 Dietterich (1998) の 5x2cv paired t-test も古典的選択肢。

⚠️ 落とし穴(拡張版・各 100 文字以上)

① 前処理を CV の外で fit してしまう
StandardScaler や PCA を「全データに fit」してから CV を回すと、 検証 fold の情報が訓練に漏れる典型的データリーク。 性能が楽観的に評価され、 本番で大きく劣化する。 必ず Pipeline でくるみ、 各 fold 内で fit させる。 SSDSE-B(n=47)では fold 間で平均が大きく違うため特に深刻。 Optuna 等のハイパラ探索でも、 全データで fit した特徴量を使ってはいけない。
② 同じ CV を「ハイパラ選択」と「最終評価」両方に使う
GridSearchCV の best_score_ を最終性能として報告するのは楽観バイアスを含む。 ハイパラ探索の過程で「たまたまその fold で当たった」結果が混入。 公平な汎化推定にはネスト CVを使い、 内側でハイパラ選択、 外側で性能評価と分離する。 計算コストは k²倍だが、 論文・コンペ・本番展開前には必須。 Cawley & Talbot (2010) の警告参照。
③ 時系列データに通常の k-fold を適用
シャッフル k-fold では「未来のデータで学習し過去を予測」してしまい、 時系列の依存構造を破壊。 結果として性能が極端に楽観評価される(株価予測で R²=0.9 等)。 必ず TimeSeriesSplit(ウォークフォワード)を使う。 季節性が強い場合は purged + embargo の k-fold(López de Prado)も検討。 SSDSE 時系列(パネル)データの分析では特に注意。
④ グループ構造を無視(顧客・患者・地域単位)
同一顧客の複数取引が train/val に分かれて入ると、 「顧客 ID」レベルの汎化を評価できず、 性能が楽観的に。 47 都道府県データを年次パネルで学習する場合も、 同じ県が異なる年に train/val に入る。 GroupKFold(県単位)または LeaveOneGroupOut で汎化評価する。 医療データの患者単位、 マーケの顧客単位も同様の注意が必要。
⑤ CV の平均だけ報告して分散を無視
「Ridge は RMSE 3.18、 OLS は 3.24 なので Ridge の勝ち」は早計。 SD が 0.7 もあれば差は誤差範囲。 必ず平均 ± SD を併記し、 paired t-test や Wilcoxon 符号付順位検定で有意性を確認。 さらに RepeatedKFold(5-fold × 10 repeats など)で評価の分散を縮減。 1-SE rule(最良スコアの 1-SE 以内で最も単純なモデルを選ぶ)も実務的に有効。
⑥ AIC/BIC をスケール変換後のモデルで比較
AIC/BIC は同一データセット・同一目的変数に対して比較可能。 y を log 変換したモデルと素のモデルの AIC を直接比べてはいけない(尤度が比較不能)。 同じく標準化の有無、 サンプル数が異なる場合も比較不可。 さらに非ネストモデルの比較では BIC でも厳密には正当化されない(Vuong test など別手法を検討)。 Burnham & Anderson (2002) の解説書参照。
⑦ random_state を固定せず再現性を失う
KFold(shuffle=True)train_test_splitrandom_state を省略すると、 実行ごとに結果が変わる。 「先週は Ridge が勝ったが今週は OLS が勝った」となり議論不能。 全ての分割・モデル初期化・ハイパラ探索で random_state を指定。 さらに NumPy・PyTorch の seed も固定。 加えて PYTHONHASHSEED、 GPU 演算の決定論オプションまで考慮するのが完全再現の鉄則。
⑧ Stratified KFold を回帰問題でも使うべき場面
Stratified は分類の常識だが、 回帰でも y を四分位ビンに切って層化すると fold 間の y 分布が安定。 不均衡データ(極端な高所得観測が少数)では特に有効で、 fold ごとに「高所得サンプルがゼロ」となる事故を防ぐ。 StratifiedKFoldpd.qcut(y, 5) をラベルとして渡す実装。 sklearn 1.5 以降は StratifiedShuffleSplit でも同様。

🐍 Python 実装バリエーション(scikit-learn / scipy / statsmodels / Optuna)

A. RepeatedKFold + cross_validate(複数スコア同時)

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from sklearn.model_selection import RepeatedKFold, cross_validate

cv = RepeatedKFold(n_splits=5, n_repeats=20, random_state=42)
results = cross_validate(
    Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=10))]),
    X, y, cv=cv,
    scoring=['neg_root_mean_squared_error', 'r2', 'neg_mean_absolute_error'],
    return_train_score=True, n_jobs=-1
)
import numpy as np
print('RMSE:', -np.mean(results['test_neg_root_mean_squared_error']))
print('R²  :',  np.mean(results['test_r2']))
print('MAE :', -np.mean(results['test_neg_mean_absolute_error']))
print('train-test gap (R²):', np.mean(results['train_r2']) - np.mean(results['test_r2']))
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

B. CV のばらつきを可視化(matplotlib)

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import matplotlib.pyplot as plt
from sklearn.model_selection import cross_val_score

cv = RepeatedKFold(n_splits=5, n_repeats=30, random_state=42)
scores = {}
for name, m in [('OLS', LinearRegression()), ('Ridge', Ridge(alpha=10)),
                ('Lasso', Lasso(alpha=0.1, max_iter=10000)), ('RF', RandomForestRegressor(300, random_state=42))]:
    p = Pipeline([('s', StandardScaler()), ('m', m)])
    scores[name] = -cross_val_score(p, X, y, cv=cv, scoring='neg_root_mean_squared_error')

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.boxplot(scores.values(), labels=scores.keys())
ax.set_ylabel('CV RMSE'); ax.set_title('Repeated 5-fold × 30 — 持ち家比率予測')
plt.tight_layout(); plt.savefig('cv_compare.png', dpi=110)
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

C. statsmodels で AIC / BIC を直接取得

🎯 このコードでやること: モデルを学習。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import statsmodels.api as sm

candidates = {
    'M1: 県民所得のみ':       ['一人当たり県民所得'],
    'M2: + 高齢化率':         ['一人当たり県民所得', '高齢化率'],
    'M3: + 人口密度':         ['一人当たり県民所得', '高齢化率', '人口密度'],
    'M4: フル':               ['一人当たり県民所得', '世帯人員', '高齢化率', '人口密度', '就業率'],
}
print(f'{"model":25} {"AIC":>8} {"BIC":>8} {"adj R²":>8}')
for name, cols in candidates.items():
    Xc = sm.add_constant(df[cols])
    res = sm.OLS(df['持ち家比率'], Xc).fit()
    print(f'{name:25} {res.aic:8.1f} {res.bic:8.1f} {res.rsquared_adj:8.3f}')
📤 実行例 (結果はターミナルに出力されます) 例: 期待される出力は数値・配列形・要約統計です

💬 読み方: 「モデル選択」の典型パターン。 列名や引数を変えると応用可能。

D. Optuna でベイズ最適化(実例)

🎯 このコードでやること: K-分割交差検証で評価、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import optuna
from sklearn.ensemble import RandomForestRegressor

def objective(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 100, 800),
        'max_depth':    trial.suggest_int('max_depth', 2, 20),
        'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 10),
        'max_features': trial.suggest_float('max_features', 0.3, 1.0),
    }
    p = Pipeline([('s', StandardScaler()),
                   ('m', RandomForestRegressor(random_state=42, **params))])
    sc = cross_val_score(p, X, y, cv=5, scoring='neg_root_mean_squared_error')
    return -sc.mean()

study = optuna.create_study(direction='minimize', sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(objective, n_trials=50, show_progress_bar=False)
print('Best RMSE:', study.best_value, ' params:', study.best_params)
📤 実行例 Fold 1: score = 0.84 Fold 2: score = 0.81 Fold 3: score = 0.87 Mean (5-fold) = 0.83 ± 0.022

💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。

E. HalvingGridSearchCV(高速グリッド探索)

🎯 このコードでやること: 回帰モデルを学習、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from sklearn.experimental import enable_halving_search_cv  # noqa
from sklearn.model_selection import HalvingGridSearchCV
import numpy as np

p = Pipeline([('s', StandardScaler()), ('m', Ridge())])
search = HalvingGridSearchCV(
    p, param_grid={'m__alpha': np.logspace(-3, 3, 20)},
    cv=5, scoring='neg_root_mean_squared_error',
    factor=3, random_state=42
)
search.fit(X, y)
print('Best α:', search.best_params_, 'CV RMSE:', -search.best_score_)
📤 実行例 R^2 (train): 0.913 R^2 (test): 0.842 RMSE (test): 1245.6

💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。

F. 1-SE rule の実装

🎯 このコードでやること: 回帰モデルを学習、精度を評価。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from sklearn.model_selection import GridSearchCV
import numpy as np

alphas = np.logspace(-3, 3, 30)
g = GridSearchCV(Pipeline([('s', StandardScaler()), ('m', Ridge())]),
                  {'m__alpha': alphas}, cv=10,
                  scoring='neg_root_mean_squared_error', return_train_score=False)
g.fit(X, y)

# 最良 α より RMSE が「最良 + 1SE」以内で、 α が最大(最も正則化が強い=最も単純)を選ぶ
means = -g.cv_results_['mean_test_score']
sds   =  g.cv_results_['std_test_score']
best_idx = means.argmin()
threshold = means[best_idx] + sds[best_idx]
eligible = np.where(means <= threshold)[0]
one_se_idx = eligible[np.argmax(alphas[eligible])]
print(f'best α = {alphas[best_idx]:.3f}, RMSE = {means[best_idx]:.3f}')
print(f'1-SE α = {alphas[one_se_idx]:.3f}, RMSE = {means[one_se_idx]:.3f}(より頑健)')
📤 実行例 R^2 (train): 0.913 R^2 (test): 0.842 RMSE (test): 1245.6

💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。

G. 早期停止つき勾配ブースティング

🎯 このコードでやること: 学習用と評価用にデータを分割、モデルを学習、予測を取得。

📥 入力例 # 入力: 前段の処理結果(DataFrame または ndarray)を前提 # 例: df.shape == (47, 12)、 X.shape == (47, 5)、 y.shape == (47,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import train_test_split

X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.25, random_state=42)
m = HistGradientBoostingRegressor(
    max_iter=1000, learning_rate=0.05,
    early_stopping=True, validation_fraction=0.2,
    n_iter_no_change=20, random_state=42
)
m.fit(X_tr, y_tr)
print('停止 iter:', m.n_iter_)
print('val RMSE :', ((m.predict(X_va)-y_va)**2).mean()**0.5)
📤 実行例 X_train.shape = (37, 5) X_test.shape = (10, 5) y_train.shape = (37,) y_test.shape = (10,)

💬 読み方: random_state=42 を固定すると再現性が確保される。

🎨 もう一歩踏み込む直感

「モデル選択」を本当に使いこなすには、 教科書的な定義だけでは足りません。 ここでは現場で役立つ追加の比喩・実例を整理します。 上の「🎨 直感で掴む」を補強する内容です。

💡 学習のコツ:3 つの直感がそれぞれ独立した「引き出し」になります。 場面に応じて、 一番フィットする比喩を取り出せるように、 例を 1-2 個自分の言葉で言い換えてみると定着します。

📐 もう一段の数式表現

「モデル選択」を厳密に書き下すと、 以下の形になります。 既出の数式と合わせて読むと、 概念の骨格が見えてきます。

【モデル選択・追加表現】
$$ \text{AIC} = 2k - 2 \ln \hat{L},\quad \text{BIC} = k \ln n - 2 \ln \hat{L} $$
AIC は予測重視、 BIC は『真のモデル』を仮定して厳しめにペナルティ。 n が大きいほど BIC は AIC より厳しい。
📌 ポイント:数式を見たら各記号の単位・値域を声に出して確認してみると、 抽象度がぐっと下がります。 「変数 X は連続値、 0 以上、 単位は人」のように。

🔬 数式を言葉で読み解く(拡張版)

追加の数式についても、 各記号を 1 つずつ「日本語」で言い換えます。 「数式を音読する」とは、 こういう作業のことです。

左辺
本用語が「何を定義しようとしているのか」を端的に表す。 ここを最初に押さえる。
右辺の主要項
左辺を成立させるための構成要素。 各項の符号・順序・係数に意味がある。
下付き・上付き添字
時刻・サンプル番号・次元など、 「どの集合の上で操作するか」を示す重要情報。 見落とすと意味が反転することも。
演算子(Σ, ∫, ∏ など)
すべての要素を集約する」操作。 範囲(i=1..n など)を必ず一緒に読む。

🧮 SSDSE-B-2026 で追加実値計算

『教育用標準データセット SSDSE-B-2026』(47 都道府県、 約 100 変数)を題材に、 「モデル選択」を実際の数値で確認します。 数式が「動く感覚」を得ることが目的です。

対象 計算結果
線形回帰(k=3)と尤度 lnL=−120 → AIC246
同じデータで Polynomial 5 次(k=6, lnL=−110) → AIC232(より良い)
BIC(n=47, k=6, lnL=−110) → 243(k=3 が選ばれる)
📚 補足:上の値は SSDSE-B-2026 をローカルに読み込んで再現できます。 引数のパスやファイル名は環境に合わせて変更してください。 同じ概念を異なるデータ(例:金融時系列、 売上データ)に当てはめると、 用語の普遍性が体感できます。

🐍 モデル選択 — Python 実装(拡張版)

5-fold CV を使って線形回帰/Ridge/Lasso/RandomForest を比較し、 最適モデルを選びます。

import pandas as pd
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
y = df['I5101']             # 医師数
X = df[['A1101','A4101','A6101']]  # 人口・出生率・平均所得

models = {
    'Linear': LinearRegression(),
    'Ridge':  Ridge(alpha=1.0),
    'Lasso':  Lasso(alpha=1.0),
    'RF':     RandomForestRegressor(n_estimators=100, random_state=42),
}
for name, m in models.items():
    scores = cross_val_score(m, X, y, cv=5, scoring='r2')
    print(f'{name:6s}: R² = {scores.mean():.3f} ± {scores.std():.3f}')
📤 実行例: Linear: R² = 0.81 ± 0.10 Ridge : R² = 0.82 ± 0.09 ← 安定性で勝つ Lasso : R² = 0.79 ± 0.11 RF : R² = 0.78 ± 0.15 ← 分散大(n=47で不利) → Ridge を選択(解釈性 + 平均性能 + 安定性)

n=47 程度では RF など複雑モデルは分散が大きく、 線形系が有利になりやすい。 必ず CV の標準偏差を確認して「安定して良い」モデルを選ぶ。

⚠️ 落とし穴(追加版・各 100 字以上)

既出の落とし穴に加えて、 中級者でも踏みやすい応用フェーズの罠を集めました。 1 度経験するか、 ここで読んでおけば回避できます。

❌ 適用範囲の越境
「モデル選択」は特定の仮定の下で意味を持ちます。 仮定(独立性・線形性・定常性・尺度など)を確認せずに別ドメインに転用すると、 結果が解釈不能になります。 適用前にチェックリストで仮定を点検しましょう。
❌ サンプルサイズ不足での過信
SSDSE-B のように n=47 と小さいデータでは、 「モデル選択」の推定値も大きな不確実性を持ちます。 点推定だけでなく、 必ず信頼区間や標準誤差を併記してください。 報告で「±」を忘れない習慣をつけることが重要です。
❌ ハイパーパラメータ依存
「モデル選択」を実装する際、 ライブラリのデフォルト値が常に最適とは限りません。 主要な引数の意味を 1 度公式ドキュメントで確認し、 自分のデータでグリッドサーチや感度分析を行うと、 結果の頑健性が分かります。
❌ 結果の単独評価
単一の指標・単一のモデルだけで結論を出さず、 必ず複数の角度から確認しましょう。 「モデル選択」だけでなく、 並列・派生の手法でクロスチェックすると、 結果の頑健性が大きく上がります。 報告書には複数結果を併記。
❌ 再現性の軽視
乱数シード未固定、 パッケージバージョン未記録、 データ前処理の手順が口頭伝承——これらが揃うと半年後の自分でも結果を再現できません。 解析コードを Notebook 化し、 Git で管理する習慣を最初から付けるのが結果的に最速です。

🎓 学習者向けケーススタディ

「モデル選択」を題材にした 3 つの典型的な学習シナリオを示します。 自分のレベルに近いものから手を動かしてみてください。

  1. 初級:直感の確認:本ページの「🎨 直感で掴む」で挙げた具体例を、 紙に書き写してから自分の言葉で言い換える。 ここで「定義は使わなくても説明できる」レベルに達することが目標。
  2. 中級:手計算と Python 実装の照合:「🧮 実値で計算」を電卓で実行し、 続いて「🐍 Python 実装」のコードで同じ値が出ることを確認。 ここで「数式とコードの対応」が腑に落ちます。
  3. 上級:別データへの転用:SSDSE-B 以外(時系列・画像・テキストなど)の自分のデータに「モデル選択」を適用。 上手くいかない場合、 適用条件を満たしているかを「⚠️ 落とし穴」と照合する。

この 3 ステップを 1 回でも回すと、 「知っている」から「使える」へと一段進めます。 学習効率の最も高い順序は、 「直感 → 数式 → コード → 別データ転用」の循環です。

🧩 クイック演習(自己診断)

「モデル選択」の理解度を 3 問で自己診断しましょう。 即答できなければ該当セクションに戻って復習。

Q1. 「モデル選択」の適用条件を 3 つ挙げてください。
→ 答えられない場合は「📐 定義・数式」と「⚠️ 落とし穴」を再読。
Q2. 「モデル選択」の結果を、 専門外の人に 1 文で説明してください。
→ 答えられない場合は「💡 30 秒結論」と「🎨 直感」を再読。
Q3. 「モデル選択」の限界を 2 つ挙げて、 代替手法を示してください。
→ 答えられない場合は「🌐 関連手法・派生」と「⚠️ 落とし穴」を再読。

3 問すべて即答できれば、 「モデル選択」は実用レベルに達しています。 関連用語ページに進みましょう。

🛠 実装時の注意点

「モデル選択」を実装に落とす際に、 教科書ではあまり強調されない実務的注意点を整理します。

  • 数値安定性:浮動小数の累積誤差で、 理論値と実測値がずれることがあります。 重要な計算は numpy.float64 または decimal で明示。
  • メモリ管理:大規模データでは中間結果を都度 del、 もしくは numpy のビュー(view)で参照のみ。
  • 並列化:scikit-learn は n_jobs=-1、 pandas は swifter、 NumPy は numexpr で高速化できる場面が多い。
  • テスト:単体テスト(pytest)で境界条件(n=0, 1, 巨大値、 NaN)を必ず確認。
  • ロギング:途中経過を logging で出力し、 後から再現できるようにする。 デバッグの時短に直結。
  • バージョンpip freeze > requirements.txt で固定。 半年後の自分が泣かない最低限の保険。

これらは「動けばよい」では済まされない場面、 たとえばコンペ提出・本番デプロイ・論文投稿で必須になります。 普段から意識すると、 いざという時に慌てません。

📖 リテラシー チェックリスト

「モデル選択」を学んだ後、 次のチェックリストを 1 つずつ満たしているか確認してください。 これは『データサイエンス・リテラシー』として身につけるべき汎用スキルにも相当します。

  • □ 「モデル選択」を 1 文で説明できる
  • □ 適用条件を 3 つ以上挙げられる
  • □ 同じカテゴリ「機械学習」の並列手法を 2 つ以上挙げられる
  • □ Python で動くコードを書ける
  • □ 結果に対する不確実性を併記できる
  • □ 落とし穴を 3 つ以上挙げられる
  • □ ドメイン知識と結びつけて解釈できる
  • □ レポートに「5 点セット」(データ・前処理・前提・推定・解釈)で書ける

8 項目すべてチェックがつけば、 「モデル選択」は実務でも論文でも自信を持って使えるレベルです。

🏢 ドメイン別応用例

「モデル選択」がどんな業界・分野で使われているか、 ざっと俯瞰しておくと、 「自分のドメインで使えるか?」の判断が早くなります。

ドメイン 「モデル選択」の典型用途
公的統計SSDSE のような都道府県データで、 地域特性の把握や政策効果の評価に使う
金融株価・為替・金利の予測、 リスク管理、 ポートフォリオ最適化
医療疫学調査、 薬効評価、 画像診断、 遺伝子解析
マーケティング顧客セグメンテーション、 LTV 予測、 A/B テスト、 推薦システム
製造業品質管理、 異常検知、 予知保全、 サプライチェーン最適化
教育学習者モデル、 アダプティブ教材、 教育効果測定

自分のドメインがリストにあれば、 そこからすぐに着想を得られます。 リストにない場合も、 似たドメインの応用例から類推することで使い方が見えてきます。

🗺 学習ロードマップ

「モデル選択」を起点に、 同カテゴリ「機械学習」を体系的に学ぶ推奨順序を示します。

  1. Week 1:本ページの定義・数式・直感を完全に押さえる。 1 日 30 分 × 5 日。
  2. Week 2:Python コードを写経し、 SSDSE-B-2026 で動作確認。 自分のデータでも試す。
  3. Week 3:「🔗 関連用語」の前提側を読み、 基礎を補強する。
  4. Week 4:「🔗 関連用語」の並列側を読み、 比較できる引き出しを増やす。
  5. Week 5:「🔗 関連用語」の発展側を読み、 上位概念や応用に進む。
  6. Week 6:関連グループ教材で全体像を再確認し、 知識を再構築する。

📚 備考:6 週間は目安です。 自分のペースで進めて構いません。 重要なのは「定義 → 実装 → 関連用語 → 再構成」のサイクルを 1 度回し切ること。

❓ さらなる FAQ

Q. 「モデル選択」は古い手法ですか? 最新の AI で代替できますか?
A. 古いから無価値ではありません。 むしろ「モデル選択」のような基礎概念は新手法の解釈に必要。 LLM が出した結果を評価するのにも、 結局この種の概念が使われます。
Q. SSDSE-B-2026 はどこで取得できますか?
A. 統計数理研究所の公式サイト(www.nstac.go.jp)からダウンロード可能。 教育用標準データセット(SSDSE)として整備された CSV ファイル。
Q. Python 以外の言語で同じことをするには?
A. R では tidyverse、 Julia では DataFrames.jl、 SQL では集約関数とウィンドウ関数で同様の処理が可能。 概念は言語によらず共通です。
Q. 数式が苦手です。 どこから手を付ければ?
A. 「🎨 直感で掴む」を 3 回読み、 「🧮 実値で計算」で手を動かす。 数式は最後で OK です。 概念のが分かれば、 数式は記号の翻訳作業に過ぎなくなります。