「似た過去の症例ではどうだったか」── 臨床でよく使う考え方に近い発想を、距離計算で表したものが k近傍法(k-Nearest Neighbors; kNN) です。新しい患者データが来たとき、特徴量空間で近い過去症例を k 人探し、その多数決(分類)または平均(回帰)で予測します。ロジスティック回帰のように係数を推定するのではなく、訓練データを保持して予測時に近傍を探すため、lazy learning と呼ばれることもあります。仕組みは単純ですが、標準化、k の選び方、変数の数に強く影響されるため、使いどころを理解しておくことが重要です[1]。
// 01 · LEARN OUTCOMESこの記事でわかること
この記事では、kNNを実際のリハビリテーション研究で使う前に押さえておきたい点を整理します。
- kNN が「似た k 人の多数決(または平均)で予測する」と1文で説明できる
- k の選び方が「過学習 vs 未学習」のトレードオフだと理解できる
- 「次元の呪い」が距離計算をどう壊すかを具体例で説明できる
- 標準化が重要な理由と、リハ研究での適用範囲・限界を判断できる
// 02 · CONCLUSIONまず結論
// 03 · FIGURE直感的な図解
まず、kNN の予測プロセスを2次元で可視化します。「新しい点の周りで k 個の最近傍を探す → 多数決」が本質です。
続いて、k の選択が予測性能をどう左右するかを見てみます。k が小さすぎても大きすぎても性能が落ちることがあり、交差検証での調整が必要です。
3つ目は、kNN で特に注意したい「次元の呪い(curse of dimensionality)」です。変数が増えると空間の広がりが大きくなり、同じ半径で近傍を考えても、2次元では複数の点が入る一方で、3次元では同じ数の点が集まりにくくなります。下の図は、その違いを直感的に示した2次元と3次元の比較図です。
// 04 · CLINICAL医療・リハビリでの具体例
kNN は、リハビリ領域などの医学研究では「似た症例を探す」考え方に相性が良いです。ただし、必ずしも予測精度が良いわけではないので、最善の予測モデルにならないことも多いため、目的に応じて使い分けることが大切です。
「この患者と似た過去症例では、退院時FIMや転帰はどうなったか?」を検討するツールとして使えます。年齢、入院時FIM、発症から入院までの日数などを使って距離を計算し、予測したい症例に近い症例を 5〜10 例ほど出します。単独で治療方針を決めるものではありませんが、リハ医、療法士、MSW が予後を話し合うときの参考情報として使える可能性があります。平均値だけでなく、分布や個々の症例も見せると、臨床的には使いやすくなります。
希少疾患など、サンプル数が少ない臨床研究では、複雑なモデルを使うほど予測モデルが不安定になりやすくなります。kNN は他の機械学習モデルよりもシンプルであるため、比較的少ないサンプル数でも扱いやすいモデルです。ただし、「パラメータが少ないからサンプル数が少なくても安定する」と単純には言えません。k の選び方や変数の数に影響されるため、交差検証で性能を確認し、ロジスティック回帰などの基本モデルと比較して解釈する必要もあります。
kNN は欠損値の代入(KNNImputer)にも使われます。たとえば FIM 認知が欠損している場合、他の特徴量が似ている k 人の値を使って補完します。MICE(02·02 欠損値処理/公開予定)より実装は単純ですが、距離計算に依存するため標準化の影響を受けます。また、補完器は必ず訓練データだけで fit し、検証データやテストデータの情報が混ざらないようにします。この問題は データリーケージとは何か で詳しく整理しています。
ウェアラブルから 100 種類の歩行特徴量を抽出したようなデータでは、そのまま kNN を使うと距離の差が小さくなり、近傍の解釈が難しくなることがあります(Fig.3)。この場合は、PCA や特徴量選択で変数を減らしてから kNN を使う、あるいは正則化モデル、SVM、勾配ブースティングなど別の手法と比較するのが安全です。
// 05 · THEORY数式・理論
kNN の動作を、距離関数、予測ルール、k の選び方、計算量に分けて整理します。
距離関数の定義
# ユークリッド距離(L2、よく使われる距離)
d(x, x') = √[ Σⱼ (xⱼ − x'ⱼ)² ]
# マンハッタン距離(L1)
d(x, x') = Σⱼ |xⱼ − x'ⱼ|
# ミンコフスキー距離(L1, L2 を含む一般化)
d(x, x') = ( Σⱼ |xⱼ − x'ⱼ|^p )^(1/p)
p=1 → マンハッタン距離
p=2 → ユークリッド距離
# コサイン距離(向きの近さを見る距離)
d(x, x') = 1 − (x · x') / (‖x‖ · ‖x'‖)
医療データでは、年齢、FIM、発症からの日数、検査値など、変数ごとに単位も範囲も違います。そのまま距離を計算すると、値の範囲が大きい変数が距離を支配します。したがって、kNN を使う場合は、通常は標準化を行ってから距離を計算します。
予測ルール
# 入力: テスト点 x_test, 訓練データ {(x_i, y_i)}
# Step 1: 全訓練点との距離を計算
distances = [d(x_test, x_i) for i in 1..N]
# Step 2: 距離が小さい順に k 個を選ぶ
k_nearest = argsort(distances)[:k]
# Step 3a: 分類タスク
y_pred = mode({y_i for i in k_nearest}) # 多数決
# Step 3b: 回帰タスク
y_pred = mean({y_i for i in k_nearest}) # 平均
# Step 3c: 距離で重み付けする場合
weights_i = 1 / (d(x_test, x_i) + ε)
y_pred = Σ weights_i · y_i / Σ weights_i
分類では、多数決だけでなく「近い症例ほど重く見る」distance-weighted voting もよく使われます。scikit-learn では weights="distance" で指定できます。
k の選択とバイアス・バリアンス
# k = 1
- 最も近い1例だけで予測する
- 局所的な違いを拾える一方、ノイズにも敏感
- 訓練データでは良く見えやすいが、検証データでは不安定になりやすい
# k が大きい
- 多くの症例の多数決や平均に近づく
- ノイズの影響は減るが、個別性や局所構造は薄くなる
# k = √N
- 候補値を決めるための目安
- N = 100 → k ≈ 10
- N = 200 → k ≈ 15
- ただし最終的には CV で決める
分類タスクでは、同票を避けるために奇数の k を候補に入れることがあります。ただし、クラス数や重み付けの有無によっては、奇数にしても完全に同票を避けられるとは限りません。
次元の呪い
高次元空間では、点同士の距離の差が小さくなりやすく、「最近傍」が直感ほど近いとは限らなくなります。Beyer らは、一定の条件下で高次元になるほど最近傍距離と最遠傍距離の差が相対的に小さくなることを示しています[2]。
# 直感的な理解
変数が少ない:
症例同士の「近い」「遠い」が比較的わかりやすい
変数が多い:
空間が急激に広がる
同じ症例数ではデータがまばらになる
距離の差が小さくなりやすい
実務上の対応:
- 使う変数を臨床的に絞る
- PCA などで次元削減する
- kNN 以外のモデルとも比較する
計算量
# 単純な実装
- 予測のたびに全訓練データとの距離を計算する
- サンプル数 N と変数数 d が大きいほど遅くなる
# KD-tree / Ball-tree
- 低次元では検索を速くできることがある
- 高次元では効果が小さくなることが多い
# Approximate nearest neighbor
- FAISS や Annoy など
- 大規模データで近似的に近傍を探す場合に使う
// 06 · IMPLEMENTATION · PYTHON実装
scikit-learn での基本実装です。kNN は距離計算に基づくため、標準化を Pipeline に入れて、交差検証の各 fold 内で fit することが重要です。
import numpy as np
import pandas as pd
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.neighbors import KNeighborsClassifier, NearestNeighbors
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score
df = pd.read_csv("rehab_cohort.csv")
features = [
"age",
"fim_motor_admission",
"fim_cog_admission",
"onset_days",
"nihss",
]
X = df[features]
y = df["discharge_home"] # 例: 自宅退院あり=1, なし=0
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=y,
random_state=42
)
# ============================================
# ① 基本実装: kNN 分類
# ============================================
knn_pipe = Pipeline([
("scaler", StandardScaler()),
("knn", KNeighborsClassifier(
n_neighbors=5,
weights="distance",
metric="euclidean"
)),
])
knn_pipe.fit(X_train, y_train)
y_proba = knn_pipe.predict_proba(X_test)[:, 1]
print(f"Test AUC: {roc_auc_score(y_test, y_proba):.3f}")
# ============================================
# ② k・距離・重み付けを交差検証で調整
# ============================================
param_grid = {
"knn__n_neighbors": [3, 5, 7, 11, 15, 21, 31],
"knn__weights": ["uniform", "distance"],
"knn__metric": ["euclidean", "manhattan"],
}
grid = GridSearchCV(
estimator=knn_pipe,
param_grid=param_grid,
cv=5,
scoring="roc_auc",
n_jobs=-1
)
grid.fit(X_train, y_train)
print(f"Best params: {grid.best_params_}")
print(f"Best CV AUC: {grid.best_score_:.3f}")
# ============================================
# ③ 類似症例検索: 診療支援・説明用
# ============================================
# StandardScaler は訓練データだけで fit する
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
nn = NearestNeighbors(
n_neighbors=10,
metric="euclidean"
)
nn.fit(X_train_scaled)
new_case = pd.DataFrame([{
"age": 72,
"fim_motor_admission": 35,
"fim_cog_admission": 25,
"onset_days": 14,
"nihss": 8,
}])
new_case_scaled = scaler.transform(new_case)
distances, positions = nn.kneighbors(new_case_scaled)
# positions は X_train 内での位置なので、元の index に戻す
similar_index = X_train.iloc[positions[0]].index
similar_cases = df.loc[similar_index]
print("最も似た10症例:")
print(similar_cases[
["age", "fim_motor_admission", "fim_motor_discharge", "discharge_home"]
])
# ============================================
# ④ 欠損値補完: KNNImputer
# ============================================
from sklearn.impute import KNNImputer
X_missing = X.copy()
X_missing.loc[10:30, "fim_cog_admission"] = np.nan
X_train_m, X_test_m = train_test_split(
X_missing,
test_size=0.2,
random_state=42
)
# KNNImputer も訓練データだけで fit する
# 距離計算に使うため、標準化も Pipeline に含める
impute_pipe = Pipeline([
("scaler", StandardScaler()),
("imputer", KNNImputer(n_neighbors=5, weights="distance")),
])
X_train_imputed_scaled = impute_pipe.fit_transform(X_train_m)
X_test_imputed_scaled = impute_pipe.transform(X_test_m)
# 元の単位に戻して確認したい場合
X_train_imputed = pd.DataFrame(
impute_pipe["scaler"].inverse_transform(X_train_imputed_scaled),
columns=features,
index=X_train_m.index
)
# ============================================
# ⑤ 高次元データへの対応: PCA → kNN
# ============================================
from sklearn.decomposition import PCA
# 例: gait_feature_ で始まる歩行特徴量が多数ある場合
gait_features = [c for c in df.columns if c.startswith("gait_feature_")]
if len(gait_features) > 0:
X_gait = df[gait_features]
X_gait_train, X_gait_test, y_train_g, y_test_g = train_test_split(
X_gait, y,
test_size=0.2,
stratify=y,
random_state=42
)
high_dim_pipe = Pipeline([
("scaler", StandardScaler()),
("pca", PCA(n_components=10)),
("knn", KNeighborsClassifier(n_neighbors=15)),
])
high_dim_pipe.fit(X_gait_train, y_train_g)
重要な注意点: kNN は、予測時にも訓練データを参照します。そのため、モデルの「サイズ」は訓練データに依存し、デプロイ時にはメモリと計算時間を考える必要があります。症例数が非常に多い場合は、BallTree、KDTree、FAISS などの近似最近傍検索も選択肢になります[3]。
// 07 · MYTHSよくある誤解
- kNN は「シンプルだから」いつも安定している
- 誤りです。仕組みはシンプルですが、標準化忘れ・k の選択ミス・次元の呪いの影響を強く受けます。kNN は前処理と特徴量設計にかなり敏感な手法です。
- k は 5 か 7 にしておけば大丈夫
- 不正確です。最適な k は、症例数、クラス比、ノイズの大きさ、特徴量の数によって変わります。√N は候補値を決める目安にはなりますが、最終的には CV で調整します。
- kNN は「係数を推定しない」ので過学習しない
- 誤りです。k=1 では、訓練データの個々のノイズまで拾いやすくなります。「学習しない」とは、係数のようなパラメータを推定しないという意味であり、過学習しないという意味ではありません。
- 変数を増やすほど類似症例が見つけやすくなる
- 誤りです。変数を増やすと情報が増える一方で、症例数が十分でない場合は高次元空間でデータがまばらになります。その結果、距離の差が小さくなり、近傍の意味が弱くなることがあります。kNN では、学習に用いる変数を絞ることも重要です。
// 08 · WRITING論文での書き方
Methods に記述すべき項目
- kNN を使う理由(類似症例検索 / ベースライン比較 / KNNImputer)
- 距離関数(ユークリッド / マンハッタン / コサイン)とその選択理由
- 標準化を Pipeline で行ったこと
- k の値とその決定方法(CV、grid search)
- 重み付け(uniform / distance-weighted)
- 変数数が多い場合は次元削減手法(PCA等)の併用
査読者が指摘しやすい点
- 「標準化を行ったか明記されていない」
- 「k の選択方法が示されていない(arbitrary に決めた可能性)」
- 「変数数が多いのに次元削減を併用していない」
- 「kNN を選んだ理由が不明確(なぜロジスティック回帰ではないか)」
レポーティング規範は TRIPOD+AI Item 11(モデル開発)を参照します[4]。kNN を使う場合も、他のモデルと同じように、前処理、ハイパーパラメータ、評価方法を明記する必要があります。特に、標準化と k の決定方法は Methods に必ず書いておきたい項目です。
// 09 · CHECKLISTチェックリスト
kNN の使い方を点検する6項目。
- 01kNN を使う目的(類似症例検索 / ベースライン / 補完)が明確
- 02標準化を Pipeline 内で行っている(訓練データのみで fit)
- 03k を CV で最適化した(候補は √N の周辺)
- 04距離関数(L1/L2/コサイン)を選択した理由がある
- 05変数数が多い場合、特徴量選択や PCA 等を検討した
- 06distance-weighted vs uniform を比較した
// 10 · QUIZミニクイズ
-
Q1kNN を使う前に特に重要な前処理は?
- 欠損値の処理
- 特徴量の標準化(StandardScaler等)
- 外れ値の除去
- 対数変換
SHOW ANSWER
B. kNN は距離計算に基づきます。年齢、FIM、発症からの日数のようにスケールが異なる変数をそのまま入れると、値の範囲が大きい変数が距離を支配します。欠損値処理も重要ですが、kNN の前提として標準化は特に重要です。 -
Q2k=1 を選んだ場合、最も起こりやすい問題は?
- 未学習
- 過学習(訓練データへの過剰適合)
- 標準化ができない
- 距離計算が遅くなる
SHOW ANSWER
B. k=1 では「最も近い1点をそのまま予測」するため、訓練データのノイズに影響されやすくなります。訓練データで良く見えても、検証データでは性能が下がることがあります。k は √N 程度を出発点にしつつ、CV で探索します。 -
Q3変数数 100 のデータに kNN を直接適用すると性能が下がりやすい主因は?
- 標準化が難しくなる
- 次元の呪いで「近い」の意味が弱くなる
- サンプル数が足りなくなる
- k の選択ができない
SHOW ANSWER
B. 高次元では、点同士の距離の差が小さくなり、「近い症例」を選ぶ意味が弱くなります。これが「次元の呪い」です。対策として、PCA での次元削減、特徴量選択、他のアルゴリズムとの比較を検討します。
// REF参考文献
- Cover T, Hart P. Nearest neighbor pattern classification. IEEE Trans Inf Theory 1967;13(1):21-27. — link
- Beyer K, Goldstein J, Ramakrishnan R, Shaft U. When is "nearest neighbor" meaningful? ICDT 1999:217-235.
- Pedregosa F, Varoquaux G, Gramfort A, et al. Scikit-learn: Machine learning in Python. JMLR 2011;12:2825-2830.
- Collins GS, Moons KGM, et al. TRIPOD+AI statement. BMJ 2024;385:e078378.