データリーケージは、予測モデル研究で見落とされやすい落とし穴です。 特に、標準化、欠測補完、カテゴリ変数処理、特徴量選択は、 何気なく全データで実行すると、評価データの情報が学習側に混ざります。 本記事では、前処理を Pipeline と ColumnTransformer に入れ、 train で fit し、test には transform だけを適用する考え方を整理します。
前処理は、モデルに入る前の準備作業です。 しかし、その準備段階で評価データを見てしまうと、 モデル性能が過大評価されます。 これは、リハ研究の表形式データでも頻繁に起こります。
// 01 · LEARN OUTCOMESこの記事で学ぶこと
- 前処理がデータリーケージの発生源になりやすい理由を説明できます。
- fit と transform の違いを、train/test 分割と関連づけて理解できます。
- Pipeline と ColumnTransformer による安全な前処理実装を確認できます。
- 同一患者の複数評価や多施設データで、GroupKFold を使う意味を整理できます。
// 02 · CONCLUSION結論
リーケージは、悪意がなくても起こります。 たとえば、全データの平均と標準偏差で StandardScaler を fit してから、 train/test に分けるだけでも問題になります。 test の分布を使って train を整えたことになるためです。
重要なのは、前処理を「解析前の一括作業」と考えないことです。 前処理も、モデル学習の一部です。 そのため、学習データだけで規則を作り、 評価データにはその規則を適用するだけにします。
// 03 · FIGURE図で見る NG と OK
図1では、前処理の順番を比較します。 NG パターンでは、最初に全データで欠測補完や標準化を行います。 その後に train/test に分割すると、test 側の情報が前処理器に入ります。 これが、典型的な前処理リーケージです。
OK パターンでは、最初にデータを分割します。 さらに CV では、各 fold の train 部分だけで前処理器を fit します。 validation fold や test set には、その前処理器を transform として使います。 この順番にすると、評価データの情報を学習側に入れにくくなります。
図2では、ColumnTransformer の役割を示します。 数値列には欠測補完と標準化を行います。 カテゴリ列には欠測補完と One-Hot Encoding を行います。 テキスト列がある場合は、別の前処理に分けられます。 それらを Pipeline に入れることで、CV の各 fold 内で処理できます。
// 04 · CLINICALリハ研究で起こる場面
退院時歩行自立を予測する研究を考えます。 入院時 FIM、SIAS、年齢、発症から入院までの日数を使います。 全データで StandardScaler を fit してから CV すると、 validation 側の平均と分散が train 側に混ざります。 その結果、AUC が高く見える場合があります。
施設 ID をそのまま One-Hot にすると、列数が増えます。 Target Encoding を使う選択肢もあります。 ただし、施設ごとの転帰率を全データで計算すると、 validation fold のアウトカム情報を使うことになります。 out-of-fold の形で、fold 内だけで計算します。
回復期病棟のデータでは、握力や SIAS に欠測が混ざります。 性別、病型、退院前生活場所などのカテゴリ列もあります。 これらを pandas で先に一括処理すると、 評価データの情報を使いやすくなります。 ColumnTransformer に入れると、列ごとに安全に分岐できます。
同一患者から複数日の加速度データを得る研究では、 患者単位の平均、最大値、変動係数などを作ります。 test 患者の値を train 側の集約に含めると、 患者情報の混入になります。 同一患者の複数評価がある場合は、GroupKFold を使います。
// 05 · THEORYfit と transform の考え方
リーケージは、予測時点で使えない情報が、 説明変数や前処理の規則に混ざることです。 たとえば、入院時に退院時 FIM や入院期間は分かりません。 それらを説明変数に入れると、時点のリーケージになります。
前処理でも同じことが起こります。 標準化では、平均と標準偏差を推定します。 欠測補完では、中央値や最頻値を推定します。 これらの値を test set も含めて推定すると、 test set の情報で train set を整えることになります。
観測時点 t で使える情報: X_t
観測時点 t より後の情報: X_future, Y_future
安全な予測モデル:
model uses X_t only
リーケージがある予測モデル:
model or preprocessing uses X_future or Y_future
scikit-learn では、fit と transform を分けて考えます。 fit は、前処理器が規則を学習する段階です。 transform は、その規則をデータに適用する段階です。
fit(X_train):
parameters = phi(X_train)
transform(X_new):
X_new_transformed = apply(parameters, X_new)
fit_transform(X_train):
parameters = phi(X_train)
X_train_transformed = apply(parameters, X_train)
CV では、この処理を fold ごとに繰り返します。
cross_val_score(pipe, X, y, cv=k) は、
内部で pipe.fit(X_train_fold) を fold ごとに呼びます。
Pipeline に前処理を入れておけば、各 fold の train 部分だけで fit されます。
for each fold:
X_train_fold, X_valid_fold = split(X)
preprocessing.fit(X_train_fold)
X_train_ready = preprocessing.transform(X_train_fold)
X_valid_ready = preprocessing.transform(X_valid_fold)
model.fit(X_train_ready, y_train_fold)
evaluate(model, X_valid_ready, y_valid_fold)
ここで重要なのは、validation fold に対して fit しないことです。 validation fold は、未知データの代わりです。 未知データの分布を使って前処理を作ると、 本来より評価が甘くなります。
// 06 · IMPLEMENTATIONPython 実装例
以下は、教育用の仮想データ example_rehab_dataset.csv を想定した例です。
数値列には中央値補完と標準化を行います。
カテゴリ列には最頻値補完と One-Hot Encoding を行います。
それらを ColumnTransformer で結合し、LogisticRegression へ渡します。
# 教育用の仮想データを想定します。
# 実データでは、個人情報保護、倫理審査、施設ルール、
# 共同研究契約、データ利用規約を確認してから解析します。
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import (
StratifiedKFold,
GroupKFold,
cross_val_score,
train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
RANDOM_STATE = 42
# 例: 入院時情報から退院時歩行自立を予測する仮想データ
df = pd.read_csv("example_rehab_dataset.csv")
# 予測時点で取得できない列は、説明変数から除外します。
# discharge_fim_total, length_of_stay, followup_6m_fim などは、
# 入院時予測モデルでは未来情報になる可能性があります。
target = "walking_independent_discharge"
leaky_columns = [
"discharge_fim_total",
"discharge_fim_motor",
"length_of_stay",
"followup_6m_fim",
]
X = df.drop(columns=[target] + leaky_columns, errors="ignore")
y = df[target]
numeric_features = [
"age",
"days_from_onset",
"admission_fim_total",
"admission_fim_motor",
"admission_fim_cognition",
"sias_lower_limb",
"grip_strength",
]
categorical_features = [
"sex",
"stroke_type",
"facility_id",
"prehospital_living_place",
]
numeric_features = [c for c in numeric_features if c in X.columns]
categorical_features = [c for c in categorical_features if c in X.columns]
numeric_pipe = Pipeline(
steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
]
)
categorical_pipe = Pipeline(
steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore")),
]
)
preprocess = ColumnTransformer(
transformers=[
("num", numeric_pipe, numeric_features),
("cat", categorical_pipe, categorical_features),
],
remainder="drop",
)
pipe = Pipeline(
steps=[
("preprocess", preprocess),
("model", LogisticRegression(max_iter=2000, class_weight="balanced")),
]
)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)
auc_scores = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc")
print("CV AUC:", np.round(auc_scores, 3))
print("Mean AUC:", round(auc_scores.mean(), 3))
# 最終評価用の hold-out test を使う場合も、fit は train のみです。
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
stratify=y,
random_state=RANDOM_STATE,
)
pipe.fit(X_train, y_train)
y_pred = pipe.predict_proba(X_test)[:, 1]
print("Hold-out AUC:", round(roc_auc_score(y_test, y_pred), 3))
# 同一患者の複数評価がある場合は、患者単位で分割します。
if "patient_id" in df.columns:
groups = df["patient_id"]
group_cv = GroupKFold(n_splits=5)
group_scores = cross_val_score(
pipe,
X,
y,
cv=group_cv,
groups=groups,
scoring="roc_auc",
)
print("GroupKFold AUC:", np.round(group_scores, 3))
# NG 例: 以下は全データで前処理を fit してから分割しています。
# from sklearn.preprocessing import StandardScaler
# from sklearn.impute import SimpleImputer
# imputer = SimpleImputer(strategy="median")
# scaler = StandardScaler()
# X_num = imputer.fit_transform(X[numeric_features])
# X_num = scaler.fit_transform(X_num)
# X_train, X_test, y_train, y_test = train_test_split(X_num, y)
# この書き方では test 側の分布が補完器と scaler に混入します。
この実装では、前処理器とモデルが 1 つの Pipeline になっています。
そのため、cross_val_score の各 fold で、
補完器、標準化器、One-Hot Encoder が train fold のみで fit されます。
validation fold には transform のみが適用されます。
実務では、Target Encoding、特徴量選択、PCA、欠測フラグ作成なども、 同じ考え方で Pipeline 内に入れます。 pandas で一括処理したい場面もありますが、 評価データを含む処理になっていないかを確認します。
// 07 · MYTHSよくある誤解
- 誤解 1:Pipeline は便利だが、なくても同じ
- 手動で fit と transform を管理すると、どこかで評価データを混ぜやすくなります。 Pipeline は、前処理とモデルを 1 つの学習手順として扱うための仕組みです。
- 誤解 2:train で fit すれば、CV では再 fit しなくてよい
- CV では、各 fold の train 部分だけで fit します。 外側の train 全体で fit した前処理器を、内側の validation に使うと、 validation の独立性が弱くなります。
- 誤解 3:pandas で前処理してから sklearn に渡すのが自然
- pandas は探索や集計には便利です。 しかし、全データに対する一括処理になりやすい面があります。 モデル評価に関わる前処理は、Pipeline 化を検討します。
- 誤解 4:Target Encoding は少しのリーケージなら問題になりにくい
- 小規模な医療データでは、少しのリーケージでも評価が変わります。 施設 ID、薬剤名、疾患サブタイプなどで Target Encoding を使う場合は、 out-of-fold の計算にします。
// 08 · WRITING論文 Methods での書き方
TRIPOD+AI では、予測モデルのデータ処理、分割、検証、 欠測処理、モデル構築手順を明確に記載することが重要です。 前処理がリーケージを起こしていないかは、査読でも確認されやすい点です。 報告では、何を、どのデータで fit したかを明記します。 [5]
- train/test 分割の時点、割合、層化の有無。
- CV の種類、fold 数、乱数 seed。
- 同一患者の複数行がある場合の GroupKFold。
- 数値列、カテゴリ列、テキスト列の前処理内容。
- 欠測補完、標準化、エンコードの fit 対象。
- 特徴量選択を CV 内で行ったか。
- 予測時点で取得できない列の除外基準。
たとえば、次のように書くと、前処理の流れが伝わりやすくなります。
All preprocessing steps, including missing-value imputation,
standardization, and one-hot encoding, were performed within
scikit-learn Pipeline and ColumnTransformer objects.
During cross-validation, preprocessing parameters were estimated
using the training fold only and then applied to the validation fold.
査読者が気にしやすいのは、全データで前処理していないかです。 もう 1 つは、予測時点の整合性です。 入院時予測モデルに、退院時評価、入院期間、退院先、 退院後追跡値が入っていないかを確認します。
また、多施設データでは施設 ID の扱いも重要です。 施設差を変数として使う場合もあります。 一方で、施設ごとの転帰率を全データで計算すると、 Target Encoding のリーケージになります。 施設別分布の違いは、外部検証や施設単位分割とも関係します。
// 09 · CHECKLIST前処理リーケージ確認リスト
- 01欠測補完器を全データで fit していない。
- 02Scaler を train fold 内だけで fit している。
- 03One-Hot Encoder は Pipeline 内で処理している。
- 04特徴量選択を CV の外で行っていない。
- 05Target Encoding は out-of-fold で計算している。
- 06同一患者の複数評価を別 fold に分けていない。
- 07予測時点で取得できない列を説明変数から外している。
- 08Methods に fit 対象と検証手順を書ける状態になっている。
// 10 · QUIZ理解度チェック
-
Q1欠測補完でリーケージを起こしやすい処理はどれですか。
- train fold の中央値で補完器を fit する
- validation fold には transform のみ行う
- 全データで中央値を計算してから CV する
- Pipeline に補完器を入れる
SHOW ANSWER
C. validation/test の情報が補完器に入ります。 -
Q2scikit-learn Pipeline の利点として適切なものはどれですか。
- モデル性能を必ず改善する
- CV 内で前処理とモデル学習を一体化できる
- 欠測があるデータを全て削除できる
- 外部検証を不要にできる
SHOW ANSWER
B. 性能改善そのものを保証する仕組みではありません。 -
Q3同一患者から複数行の評価データがある場合、何を検討しますか。
- GroupKFold
- MinMaxScaler の全データ fit
- test set のアウトカム確認
- 退院時情報の追加
SHOW ANSWER
A. 同一患者のデータが train と test に分かれないようにします。 -
Q4入院時予測モデルで注意する時点のリーケージはどれですか。
- 年齢
- 入院時 FIM
- 発症から入院までの日数
- 入院期間
SHOW ANSWER
D. 入院期間は退院後に確定する情報です。
// REF参考文献
- Kaufman S, Rosset S, Perlich C. Leakage in data mining: Formulation, detection, and avoidance. ACM Transactions on Knowledge Discovery from Data 2012;6(4):15:1-15:21.
- Pedregosa F, Varoquaux G, Gramfort A, et al. Scikit-learn: Machine learning in Python. Journal of Machine Learning Research 2011;12:2825-2830.
- Kuhn M, Johnson K. Feature Engineering and Selection. Boca Raton: CRC Press; 2019.
- Steyerberg EW. Clinical Prediction Models. 2nd ed. Cham: Springer; 2019.
- Collins GS, Moons KGM, Dhiman P, et al. TRIPOD+AI statement: updated guidance for reporting clinical prediction model studies that use regression or machine learning methods. BMJ 2024;385:e078378.