医療AI研究では、陽性例と陰性例の数が大きく偏ることがあります。転倒・再入院・死亡・重度合併症・誤嚥性肺炎・歩行非自立・装具処方が必要になる症例などは、臨床的には重要でも、データ上は少数クラスになりやすいアウトカムです。
このとき、単純な accuracy だけを見ると、モデルが重要な少数クラスをまったく拾えていなくても、高性能に見えてしまいます。さらに、少数クラスを増やす目的で SMOTE などのリサンプリングを安易に使うと、データリーケージ・較正不良・臨床的に不自然な合成症例・外部検証での性能低下につながることがあります。
本稿は、第9部「医療AI研究の落とし穴と対策」の第4記事です。09·01 過学習 が訓練データへの過剰適合、09·02 データリーケージ が情報の漏れ、09·03 小規模データ問題 がイベント数の不足を扱ったのに対し、本稿では分類問題で頻出する クラス不均衡 を扱います。特に医療AI論文でよく見かける「accuracy が高い」「SMOTE でバランスを取った」という記載を、どのように読み、どのように設計すべきかを整理します。臨床的有用性の評価は 14·04 DCA デモ、評価設計の前段は 14·02 交差検証デモ も併せて読むと立体的に理解できます。
// 01 · LEARN OUTCOMESこの記事で学ぶこと
- 不均衡データで accuracy が危険になる理由を、混同行列から説明できる。
- 感度・特異度・PPV・NPV・F1・balanced accuracy・MCC・ROC-AUC・PR-AUC・Calibration・Decision Curve の使い分けを理解する。
- SMOTE の仕組みと、分割前 SMOTE がデータリーケージになる理由を説明できる。
- 少数例の医療データで SMOTE が臨床的に不自然な合成症例を作りうる理由を理解する。
- class_weight・閾値調整・層化分割・反復交差検証・確率較正などの代替策を選べる。
- Methods / Results / Discussion に、不均衡データへの対応をどう書くか整理できる。
// 02 · CONCLUSIONまず結論
// 03 · FIGURE図で理解する不均衡データ
不均衡データで最初に確認すべきなのは混同行列です。accuracy は全体の正解率なので、多数クラスが多いほど、多数クラスだけを当てるモデルでも高く見えます。
次に、混同行列を「臨床的コストの図」として読みます。偽陽性と偽陰性のどちらが重いかは、アウトカムと介入内容によって変わります。指標選択はこのコスト構造から決まります。
最後に、SMOTE の位置がなぜ重要かを図示します。分割前に SMOTE を行うと、評価データに近い合成サンプルが訓練側へ入る可能性があり、データリーケージにつながります。
// 04 · CLINICALリハ・神経・整形領域での使いどころ
回復期病棟のデータで歩行自立例が多い場合、非自立例は少数クラスになります。全例を自立と予測しても accuracy は高くなりますが、非自立例の見落としが退院支援・家屋調整・装具検討のタイミングを逃すことにつながります。感度を重視した閾値設計と、PPV を併記した混同行列で意思決定者に判断材料を提供します。
頻度が低いアウトカムでは、AUC が良好でも陽性と予測した患者の多くが偽陽性になることがあります。この場合、予測モデルは「診断」ではなく「追加評価が必要な群の抽出」と位置づける方が自然です。14·04 DCA デモ で臨床的有用性を視覚的に確認すると、しきい値選択の意味が掴めます。
PUL の各下位項目や代償動作の有無を細かく分類すると、項目ごとの陽性例・陰性例が少なくなります。少数クラスを合成するだけでは、代償動作の臨床的多様性(重度麻痺・失調・廃用・疼痛など)を十分に表せないことがあります。項目を絞る・ドメイン知識に基づき特徴量を制限する・被験者単位で分割する・外部または時期をずらした検証を計画する、という設計を併用します。
重症例が少ない画像データでオーバーサンプリングを行うと、同じ患者や似た撮影条件の情報が増え、モデルが病変ではなく撮影条件を学習する可能性があります。画像研究では、患者単位の分割・施設単位の外部検証・撮影条件の確認・Grad-CAM 等による説明確認が特に重要です。
// 05 · THEORY不均衡データの指標と SMOTE の仕組み
指標の使い分け
不均衡データでは単一指標でモデルを評価せず、研究目的に合わせて複数指標を組み合わせます。
Accuracy = (TP + TN) / N
→ 多数クラスに引っ張られる
感度 (Recall) = TP / (TP + FN)
→ 見逃しを避けたい場面
特異度 = TN / (TN + FP)
→ 過剰介入を避けたい場面
PPV (Precision) = TP / (TP + FP)
→ アウトカム頻度に強く依存
NPV = TN / (TN + FN)
→ 低リスク群を安全に除外できるか
F1 = 2 * P * R / (P + R)
Balanced Acc = (Sens + Spec) / 2
MCC = 混同行列全体のバランス
ROC-AUC = 閾値全体での順位付け
PR-AUC = ベースライン = 陽性率
陽性率の異なる研究間で単純比較しない
SMOTE の仕組み
SMOTE (Synthetic Minority Over-sampling Technique[1]) は、少数クラスの既存サンプルとその近傍サンプルの間に、新しい合成サンプルを作る手法です。単純な複製ではなく、特徴空間上で少数クラスを補間する点が特徴です。
SMOTE algorithm:
1. 少数クラスの点 A をランダムに選ぶ
2. A の k-近傍 (k_neighbors, 既定 5) から 1 点 B を選ぶ
3. 線分 A-B 上にランダム比率で合成サンプル C を作る:
C = A + λ * (B − A), λ ∈ [0, 1]
4. 必要数になるまで繰り返す
医療データでの SMOTE の限界
この発想は表形式データの分類で有用なことがあります。しかし医療データでは、特徴量が単なる連続値ではなく臨床的な意味を持つため、注意が必要です。
医療データでの注意点:
• 年齢 80 歳・FIM 30 と 50 歳・FIM 90 の中間として
65 歳・FIM 60 を作る → 臨床的に自然とは限らない
• カテゴリ変数(病型・性別・施設・装具有無)は
連続的に補間できない → SMOTE-NC など派生手法
• 少数クラスは均質ではない
→ 歩行非自立にも重度麻痺・認知障害・失調・廃用・疼痛など
• オーバーサンプリングで訓練データの陽性率が変わる
→ 予測確率が元の臨床集団のリスクを反映しなくなる
分割前 SMOTE がリーケージになる理由
ある陽性患者が test fold に入る予定だったとします。分割前に SMOTE を行うと、その患者に近い合成サンプルが作られ、後から train fold に入る可能性があります。モデルは test 患者に非常に近い情報を学習済みになり、性能が過大に見えます。09·02 データリーケージ の典型例です。
SMOTE を使わない代替策
1. アウトカム定義を見直す
→ 二値化のしきい値が臨床的に妥当か
2. 層化分割 (StratifiedKFold)
→ fold ごとの陽性率を保つ
3. class_weight="balanced"
→ 少数クラスの誤分類に大きなペナルティ
4. 閾値調整 (probability threshold tuning)
→ 機械的に 0.5 ではなく、臨床目的に合わせる
5. Calibration の確認
→ リスクを確率として提示するなら必須
6. Decision Curve Analysis (DCA)
→ 臨床的有用性で判断
// 06 · IMPLEMENTATION · PYTHONscikit-learn / imbalanced-learn 実装パターン
SMOTE を使う場合、最も重要なのは評価データを守ることです。下の実装では、(1) NG パターン、(2) imbalanced-learn の Pipeline を使った安全パターン、(3) SMOTE を使わない class_weight 比較を並べます。
# Educational example. Confirm IRB / facility rules before real use.
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
roc_auc_score, average_precision_score,
brier_score_loss, balanced_accuracy_score,
confusion_matrix, classification_report,
)
from sklearn.model_selection import (
StratifiedKFold, cross_validate, train_test_split,
)
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# imbalanced-learn の Pipeline を使うのが安全(fit_resample を sklearn と統合)
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
RANDOM_STATE = 42
df = pd.read_csv("example_rehab_dataset.csv")
target = "walking_independent_discharge"
numeric_features = [
"age", "days_from_onset", "fim_motor_admission",
"fim_cognition_admission", "sias_total", "gait_speed",
]
categorical_features = ["sex", "stroke_type"]
X = df[numeric_features + categorical_features]
y = df[target].astype(int)
print(f"陽性率: {y.mean():.3f}, 陽性 {y.sum()} / 陰性 {(1-y).sum()}")
preprocess = ColumnTransformer([
("num", Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
]), numeric_features),
("cat", Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore")),
]), categorical_features),
])
# ============================================
# ① BAD: do NOT apply SMOTE before split
# ============================================
# X_pre = preprocess.fit_transform(X)
# X_res, y_res = SMOTE(random_state=RANDOM_STATE).fit_resample(X_pre, y)
# X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, ...)
# → リーケージ: 合成サンプルが両 fold にまたがる
# ============================================
# ② GOOD: SMOTE inside each training fold
# ============================================
smote_pipe = Pipeline([
("preprocess", preprocess),
("smote", SMOTE(random_state=RANDOM_STATE, k_neighbors=3)),
("model", LogisticRegression(max_iter=2000, random_state=RANDOM_STATE)),
])
# ============================================
# ③ ALTERNATIVE: class_weight without SMOTE
# ============================================
weighted_pipe = Pipeline([
("preprocess", preprocess),
("model", LogisticRegression(
max_iter=2000, class_weight="balanced",
random_state=RANDOM_STATE,
)),
])
# ============================================
# 評価: 単一指標ではなく複数併用
# ============================================
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)
scoring = {
"roc_auc": "roc_auc",
"pr_auc": "average_precision",
"balanced_acc": "balanced_accuracy",
"brier": "neg_brier_score",
}
for name, pipe in [("SMOTE+Logit", smote_pipe),
("Weighted Logit", weighted_pipe)]:
result = cross_validate(pipe, X, y, cv=cv, scoring=scoring, n_jobs=-1)
print(f"\n{name}")
print(f" ROC-AUC : {result['test_roc_auc'].mean():.3f} ± {result['test_roc_auc'].std():.3f}")
print(f" PR-AUC : {result['test_pr_auc'].mean():.3f} ± {result['test_pr_auc'].std():.3f}")
print(f" Balanced Acc : {result['test_balanced_acc'].mean():.3f}")
print(f" Brier (lower better): {-result['test_brier'].mean():.3f}")
# ============================================
# 閾値調整: test ではなく validation で
# ============================================
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=RANDOM_STATE
)
weighted_pipe.fit(X_train, y_train)
proba_test = weighted_pipe.predict_proba(X_test)[:, 1]
# 例: 感度 ≥ 0.85 を満たす最大の特異度を持つ閾値(validation で決める想定)
from sklearn.metrics import roc_curve
fpr, tpr, thr = roc_curve(y_test, proba_test)
# 教育目的のみ。本番は validation セットで決めて test に適用すること
idx = np.argmax(tpr - fpr)
chosen_threshold = thr[idx]
pred = (proba_test >= chosen_threshold).astype(int)
print(f"\n選んだしきい値: {chosen_threshold:.3f}")
print(confusion_matrix(y_test, pred))
print(classification_report(y_test, pred, digits=3))
ポイントは次の通りです。(1) imblearn.pipeline.Pipeline を使い SMOTE を訓練 fold 内に閉じる、(2) ROC-AUC・PR-AUC・Brier score を併記、(3) 閾値は test データで選ばず、validation または事前ルールで決めて test に適用、(4) SMOTE を使わない class_weight="balanced" と必ず比較する。
// 07 · MYTHSよくある誤解
- 誤解: accuracy が高いから良いモデル
- 陽性率が低い研究で accuracy を主要指標にすると、少数クラスを見ていないと判断されやすいです。混同行列ベースでクラス別性能を必ず示します。
- 誤解: SMOTE でバランスを取れば不均衡は解決する
- SMOTE は合成手法であり、臨床的に不自然な症例を作る可能性があります。確率較正も歪みます。class_weight・閾値調整・Calibration・DCA など SMOTE 以外の選択肢を必ず検討します。
- 誤解: PR-AUC は ROC-AUC の上位互換
- PR-AUC は陽性率に強く依存するため、陽性率が異なる研究間で単純比較できません。ROC-AUC・PR-AUC・閾値別感度/特異度/PPV・Calibration をセットで示します。
- 誤解: しきい値は最良の test 性能を出すように選ぶ
- test データで最も良い閾値を選び、その性能を最終性能として出すと、過大評価になります。閾値は validation セットや事前ルール(感度 ≥ 0.85 など臨床基準)で決めます。
// 08 · WRITING論文 Methods / Results / Discussion に書くこと
不均衡データへの対応は、「SMOTE で補正した」と一文だけでは不十分です。いつ・どこで・何に対して・どの目的で実施したのかを書く必要があります。
- アウトカムの陽性例・陰性例の数と割合を明記。
- データ分割方法・層化の有無・患者単位 / 施設単位の分割。
- SMOTE・under-sampling・class_weight・閾値調整の有無。
- SMOTE を使った場合、訓練 fold 内だけで実施したこと。
- 主要評価指標と、それを選んだ臨床的理由。
- 閾値をどのデータで、どの基準により決めたか。
- Calibration と臨床的有用性(DCA)を評価したか。
Results では、accuracy だけでなく感度・特異度・PPV・NPV を示し、陽性率が低い場合は PR-AUC を併記、閾値別の性能を表で示し、交差検証なら平均値だけでなくばらつきも示します。Calibration plot・Brier score・外部検証データでの性能差も隠さず示します。
Discussion では、少数クラスの症例数による不確実性、SMOTE や class_weight が性能・較正に与える影響、陽性率が異なる集団へ適用する際の注意、閾値が施設のリソースや介入コストに依存すること、外部検証・前向き検証の必要性を整理します。TRIPOD+AI は予測モデル研究の透明な報告枠組み[4]、PROBAST+AI はバイアスと適用可能性の評価[5]として活用します。
不均衡データへの対応として、主要評価指標には ROC-AUC に加えて PR-AUC、感度、特異度、陽性的中率、陰性的中率を用いた。リサンプリングを行う場合は、データリーケージを避けるため、各交差検証の訓練 fold 内でのみ実施し、検証 fold および外部検証データには適用しなかった。閾値は事前に設定した臨床基準(感度 0.85 以上)に基づき訓練データから決定し、テストデータへ適用した。
// 09 · CHECKLIST解析前・投稿前チェックリスト
- 01陽性例・陰性例の件数と割合を本文または表に明記した
- 02accuracy を単独の主要指標にしていない
- 03感度・特異度・PPV・NPV などクラス別の性能を示した
- 04ROC-AUC だけでなく、必要に応じて PR-AUC も示した
- 05閾値の決定方法(train/validation/事前ルール)を Methods に明記した
- 06SMOTE 等のリサンプリングは訓練 fold 内だけで実施した
- 07test データ・外部検証データにはリサンプリングを適用していない
- 08class_weight や閾値調整など SMOTE 以外の選択肢も比較した
- 09Calibration を確認し、予測確率の解釈に注意した
- 10陽性率が異なる集団への適用可能性を Discussion で述べた
- 11外部検証または前向き検証の計画/必要性を明記した
- 12臨床的有用性(DCA / Net Benefit)の評価を検討した
// 10 · QUIZ理解度チェック
-
Q1陽性率 10% のデータで、全例を陰性と予測したモデルについて正しいものはどれですか。
- accuracy は 10% になる
- accuracy は 90% になり得る
- 感度は 100% になる
- PPV は必ず高い
SHOW ANSWER
B. 全例陰性と予測しても、陰性例 90% は当たるため accuracy は 90% になります。しかし陽性例を 1 例も拾えないため、感度は 0% です。 -
Q2SMOTE の最も危険な使い方はどれですか。
- 訓練 fold の内側だけで実施する
- 分割前に全データへ適用する
- class_weight と比較する
- 外部検証データには適用しない
SHOW ANSWER
B. 分割前 SMOTE は、評価データに近い合成サンプルが訓練データに入る可能性があり、データリーケージにつながります。 -
Q3不均衡データで PR-AUC を解釈する際の注意点はどれですか。
- 陽性率の影響を受けない
- 常に ROC-AUC より低いので不要
- ベースラインが陽性率に依存する
- Calibration を完全に代替できる
SHOW ANSWER
C. PR-AUC のベースラインは陽性率に依存します。陽性率が異なる研究間で単純比較しないよう注意が必要です。 -
Q4予測確率を臨床リスクとして使いたい場合、特に確認すべきものはどれですか。
- Calibration
- 学習時間
- 変数名の長さ
- 訓練 accuracy のみ
SHOW ANSWER
A. 識別能が良くても、予測確率が実際の発生率と一致しているとは限りません。リスク確率として使うなら Calibration を確認します。
// 11 · FAQよくある質問
- SMOTE は使うべきですか?
- 万能薬ではありません。少数クラスを合成する性質上、医療データでは臨床的に不自然な症例が増えたり、確率較正が歪んだりします。まず class_weight、閾値調整、Calibration、Decision Curve など SMOTE を使わない選択肢を検討し、使う場合は訓練 fold の内側だけで適用します。
- accuracy ではなくどの指標を見るべきですか?
- 研究目的によります。見逃しを避けたいなら感度、過剰介入を避けたいなら特異度・PPV、識別能の全体は ROC-AUC、まれな陽性を拾うなら PR-AUC、確率としての正しさは Calibration、臨床的有用性は Decision Curve(DCA) を組み合わせます。単一指標で判断せず、混同行列ベースで考えます。
- 分割前 SMOTE はなぜ危険ですか?
- テスト fold に入る予定だった症例に近い合成サンプルが訓練側に紛れ込み、評価が過大評価されるからです(09·02 データリーケージ 参照)。SMOTE は必ず train/test 分割や交差検証の後、訓練 fold の内側だけで実行します。imbalanced-learn の Pipeline を使うと自動的にこの構造を守れます。
// REF参考文献
- Chawla NV, Bowyer KW, Hall LO, Kegelmeyer WP. SMOTE: Synthetic Minority Over-sampling Technique. Journal of Artificial Intelligence Research 2002;16:321-357. — link
- Davis J, Goadrich M. The relationship between Precision-Recall and ROC curves. Proceedings of the 23rd International Conference on Machine Learning 2006:233-240.
- Saito T, Rehmsmeier M. The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets. PLOS ONE 2015;10(3):e0118432. — link
- Collins GS, Moons KGM, Dhiman P, Riley RD, Beam AL, Van Calster B, et al. TRIPOD+AI statement: updated guidance for reporting clinical prediction models that use regression or machine learning methods. BMJ 2024;385:e078378. — doi
- Moons KGM, Wolff RF, Riley RD, et al. PROBAST+AI: an updated quality, risk of bias, and applicability assessment tool for prediction models using regression or artificial intelligence methods. BMJ 2025;388:e082505. — doi
- Lemaître G, Nogueira F, Aridas CK. Imbalanced-learn: A Python toolbox to tackle the curse of imbalanced datasets in machine learning. JMLR 2017;18(17):1-5. — docs