k-NN に関連する手法・距離尺度・最適化のチップ集。
🍰 まずはやさしく
似ている仲間で決める方法です。
データの種類を分けるために使います。
似た趣味の友達を探すような仕組みです。
この手法の結論を短くまとめます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 基本パターン import pandas as pd import numpy as np from scipy import stats import matplotlib.pyplot as plt import seaborn as sns # データ読み込み df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) # 基本統計量 df.describe() # 可視化 sns.pairplot(df[['食料費(二人以上の世帯)', '教育費(二人以上の世帯)', '住居費(二人以上の世帯)']]) plt.show() |
このページの上にある3つの概念マップ(関係マップ、 包含マップ、 ツリーマップ)でこの概念の位置づけが視覚的に分かります。 関連手法を辿って学習を進めましょう。
統計データ活用コンペティションのSSDSE-B-2026データは、 47都道府県の社会経済データ。 この概念を使って以下のような分析ができます:
| 機能 | Python (pandas) | Python (scipy) |
|---|---|---|
| 要約統計 | df.describe() | stats.describe() |
| 平均 | df.mean() | np.mean() |
| 標準偏差 | df.std() | np.std() |
| 相関 | df.corr() | stats.pearsonr() |
| t検定 | — | stats.ttest_ind() |
| 回帰 | — | stats.linregress() |
| 分布フィッティング | — | stats.norm.fit() |
この概念は、 他の多くの統計概念と密接に関連しています。 ジャストインタイム型学習では、 必要に応じて関連用語へジャンプしながら全体像を構築します。
| グループ | 主要概念 |
|---|---|
| 記述統計 | 平均、 中央値、 最頻値、 分散、 標準偏差、 共分散、 相関係数 |
| 可視化 | ヒストグラム、 散布図、 箱ひげ図、 ヒートマップ |
| 推測統計 | 標本平均、 標準誤差、 信頼区間、 p値、 有意水準 |
| 確率分布 | 正規分布、 t分布、 χ²分布、 F分布、 二項分布 |
| 仮説検定 | t検定、 F検定、 χ²検定、 ノンパラ検定 |
| 回帰 | 単回帰、 重回帰、 OLS、 Ridge、 LASSO |
| 分類 | ロジスティック回帰、 決定木、 SVM、 k-NN |
| 教師なし学習 | クラスタリング、 PCA、 因子分析 |
| 時系列 | ARIMA、 VAR、 指数平滑法、 自己相関 |
| 因果推論 | DiD、 IV、 傾向スコア、 交絡変数 |
| 前処理 | 標準化、 正規化、 欠損値処理、 多重共線性対策 |
| 評価 | R²、 残差、 CV、 RMSE、 効果量 |
🍰 まずはやさしく
近くにあるデータを使う手法です。
未知のデータを分類するために使います。
似た特徴を持つ県を探す例で考えます。
この用語の詳しい意味を解説します。
論文中に 「k近傍法」として登場する用語。
k近傍法 とは:「最も近い k 個の訓練データの多数決」で分類するシンプルな手法。学習不要、推論時に距離計算。
本ページでは k 近傍法 (k-NN、 k-Nearest Neighbors) を扱う。 「未知のサンプルに最も近い k 個の既知サンプルから多数決 / 平均」で分類・回帰する遅延学習 (lazy learning) の代表。 SSDSE-B-2026 の都道府県データから「似た県」を距離ベースで探す例で具体化する。
k-NN はモデルを「学習しない」代わりに全データを保持して距離計算する。 k が小さいと過学習、 大きいと境界が滑らかになる。 標準化必須 (距離が単位スケールに依存)、 高次元の呪い (次元が増えると全点が等距離になる) が主な落とし穴。
🍰 まずはやさしく
周りの様子を見て判断する方法です。
直感的に予測を行うために使います。
成績表で似た傾向の人を探すイメージです。
仕組みをイメージ図と一緒に学びます。
KNN は「新しいデータの周りにある $k$ 個の既知点を見て、 多数決で予測する」シンプルな手法。 SSDSE-B-2026 で「ある県と似た特徴を持つ近隣県」を $k=5$ で探すと、 北海道は青森・秋田・新潟・岩手・福島など東北地方が近隣として浮かび上がる(人口・気温・産業構造が類似)。
k近傍法は「最も近いk個の点の多数決」。 学習はせず、 予測時に全データとの距離を計算する遅延学習。
→ k=1 は各点ごとの境界で複雑、 k=5 は滑らかな境界となる。
→ 半径内のk個の点の多数決でクラス判定。 距離重み付けも可能。
→ k 小はバリアンス高、 k 大はバイアス高。 交差検証で最適 k を選ぶ。
k 近傍法 (k-NN) について、 ここまでの内容が自分の言葉で説明できるか 5 問で確認しよう。 詰まった項目は該当章へ戻ること。 暗記でなく「なぜそうなるか」を 1-2 文で書けるかが目標。
Q1. k=1 と k=5 の境界の違い
同じデータに k=1 と k=5 の k-NN を適用した時、 決定境界はどう違うか。 過学習・滑らかさの観点で説明せよ。
答えの方向: k=1 は近傍 1 点に従うのでギザギザの境界になり訓練誤差は 0 に近いが、 外れ値にも従って境界が複雑になる (過学習)。 k=5 は 5 点の多数決なので局所的なノイズが平均化され、 境界が滑らかになる (バイアスは増えるが汎化に強い)。
Q2. なぜスケーリングが必要か
SSDSE-B-2026 の都道府県データで k-NN を使う前に、 標準化 (StandardScaler) を実施すべき理由は何か。 ヒント: 距離計算。
答えの方向: k-NN は予測時にユークリッド距離 (またはマンハッタン距離) を計算する。 スケールが大きい特徴量 (人口: 数百万) が、 小さい特徴量 (ジニ係数: 0-1) の影響を完全に上書きしてしまう。 標準化で全特徴量を平均 0・分散 1 にそろえれば、 各特徴量を平等に扱える。
Q3. 「遅延学習」と呼ばれる理由
k-NN は「学習しない」「遅延学習」と呼ばれることがある。 通常の分類器 (ロジスティック回帰など) と何が違うのか。
答えの方向: 通常の分類器は訓練時にモデルパラメータを最適化し、 予測時はそれを使うだけで高速。 一方 k-NN は訓練時には「データを保存するだけ」で、 予測時にクエリ点と全訓練点の距離を計算する。 そのため訓練は瞬時だが予測コストが高い。 大規模データでは KD-Tree/Ball-Tree などのインデックス構造を使う。
Q4. 次元の呪い
k-NN は特徴量次元が増えると性能が急激に落ちる。 その理由を「距離の縮退」の観点で説明せよ。
答えの方向: 高次元空間ではどの 2 点の距離も互いにほぼ等しくなる (距離の縮退)。 すべての点が等距離に見えるなら「近い点」を選ぶ意味が失われる。 対策として、 PCA や特徴選択で次元を下げる、 距離の重みづけを工夫する、 または木構造ベースの手法に切り替える。
Q5. 不均衡データでの落とし穴
クラス比 9:1 のような不均衡データで k-NN を使うとどんな問題が起きるか、 また対処法は。
答えの方向: 多数派クラスのサンプルが空間を埋めるため、 マイノリティの点の近傍も多数派クラスが占めやすく、 多数派に偏った予測になる。 対処: (1) クラス重み付き距離投票 weights='distance' + class_weight、 (2) SMOTE などでマイノリティをオーバーサンプル、 (3) k を小さめに設定。
→ 5 問すべて 2 文で説明できれば k-NN の基礎は完璧。 詰まった部分は「距離計算」「k の選択」「次元の呪い」セクションを再読しよう。
k-NN はシンプルで強力だが、 適用条件を満たさないと性能が出ない。 押さえておくべき前提・限界・誤解を整理する。
距離ベースの手法すべてに共通する大前提。 スケールの違う特徴量を混ぜると、 大きいスケールの変数だけが効く。 SSDSE-B-2026 の都道府県データ (総人口、 出生数、 消費支出) なら必ず StandardScaler 等を通す。 例外: すべての特徴量が同じ単位 (例: 100m 走のタイム × 4 種目) なら標準化不要のことも。
k-NN は「近傍が信頼できる代表」を前提とする。 サンプルが疎な領域では k 個の近傍が遠く離れた点になり、 局所性が失われる。 訓練データ数が次元数の指数オーダーで必要 (= 次元の呪い)。
予測時にクエリと全訓練点の距離を計算するため O(n×d)。 n が数十万・d が数百を超えるとリアルタイム予測は厳しい。 対処: KD-Tree (低次元向け)、 Ball-Tree (高次元向け)、 近似最近傍 (Faiss、 Annoy) で高速化する。
「なぜそう予測したか」を説明できるのは「近傍の k 点がこうだから」だけ。 線形回帰のような係数の解釈や、 決定木のような分岐ルールはない。 解釈性が必要な業務 (与信、 医療など) では補助的に使うことが多い。
k を大きくすると境界は滑らかになるが、 大きすぎるとデータ全体の多数派クラスに偏ってバイアスが増える。 極端な例: k = n とすると常に「全データ中の多数派」を予測するだけのモデルになる。 CV で最適 k を選ぶ。
「fit() で何もしない」は誤解。 sklearn の KNeighborsClassifier.fit() はインデックス構造 (KD-Tree など) を構築する作業を行う。 また、 距離の重み付け・カーネル化・metric learning など、 拡張形では明示的な学習も組み込める (= LMNN, NCA など)。
k=1 では外れ値そのものに引っ張られて誤分類が起きる。 k を大きく取れば多数決で外れ値の影響は薄まるが、 外れ値の近くにクエリ点が来た場合は依然として誤予測の原因になる。 外れ値検出 (IsolationForest など) で事前にクレンジングするのが定石。
→ k-NN は「データの近さで判断する」最もシンプルなモデル。 シンプルだからこそ前処理 (標準化、 外れ値除去) と k の選択がすべてを決める。 ロジスティック回帰、 ランダムフォレストと必ず比較する習慣を。
下の平面をクリック(スマホはタップ)して点を置くと、 その瞬間に k 近傍多数決による決定境界が塗り分けられます。 マウスを動かすとクエリ点の最近傍 k 点が線で結ばれ、 予測クラスが表示されます。 k スライダーと距離尺度を切り替えて、 「k=1 の複雑な境界」と「大きい k の滑らかな境界」の違いを体感してください。 すべてブラウザ内で計算しており、 外部通信は行いません。
このデモは 2 次元なので「近い点」が直感どおりに見つかります。 しかし特徴量が数十〜数百次元になると、 ほぼすべての点が互いに等距離になり「近い/遠い」の差が消えます(次元の呪い)。 高次元では kNN の前に PCA などで次元を落とすか、 特徴を厳選するのが定石です。 また、 軸のスケールが揃っていないと距離が大きい単位の特徴量に支配されるため、 実データでは必ず 標準化してから距離を計算します(このデモは両軸ともピクセル=同一スケールなので標準化不要)。
上の境界はグリッドの各セルで「全点との距離」を計算して塗っています。 これは kNN が予測のたびに全訓練点との距離を計算する 遅延学習そのもの(1 予測あたり O(n·d))。 点を増やすほど再描画が重くなるのを体感できます。 大規模データでは KD-Tree / Ball-Tree / 近似最近傍で高速化します。 予測ルールを事前に固める 決定木や、 マージン最大化の SVM、 集団学習の ランダムフォレストと比較して、 分類タスクでの使い分けを考えると理解が深まります。
🍰 まずはやさしく
多数決や平均で計算するルールです。
正確な予測値を出すために使います。
部活の記録を数値で比べるようなものです。
計算式を使って定義を詳しく説明します。
直感で全体像を掴んだら、 次は厳密な定義を見ます。 数式は短いものでも、 「何を入力にして、 何を出力するのか」を意識して読むと早く慣れます。
上の数式に出てくる各記号が何を表すかを、 言葉で翻訳します。 1 つずつ自分の言葉で言い換えられるようになると、 論文や教科書のスピードが一気に上がります。
| 記号 | 意味(言葉での説明) |
|---|---|
| $k$ | 参照する近傍の数(ハイパーパラメータ) |
| $N_k(x)$ | 点 $x$ から最も近い $k$ 点のインデックス |
| $\hat y(x)$ | $x$ に対する予測値 |
| 距離 | 通常はユークリッド距離(要スケーリング) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd from sklearn.neighbors import KNeighborsRegressor from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number') y = num.iloc[:, 0].values X = num.iloc[:, 1:6].values X_std = StandardScaler().fit_transform(X) # 標準化必須 for k in [1, 3, 5, 7, 9]: knn = KNeighborsRegressor(n_neighbors=k, weights='distance') scores = cross_val_score(knn, X_std, y, cv=5, scoring='r2') print(f'k={k}: R² mean={scores.mean():.3f} ± {scores.std():.3f}') |
数式だけでは「分かった気になる」だけで終わりがち。 ここで SSDSE-B-2026(教育用標準データセット — 47 都道府県 × 100+ 指標、 2012-2023 年度)の実値を当てはめて、 k近傍法 の挙動を電卓的に追体験します。
SSDSE-B-2026 は 統計センターの SSDSE 配布ページ から CSV を直接ダウンロードできます。 本サイトでは data/raw/SSDSE-B-2026.csv に配置している前提でコードを書いています。
合成 5 点 1D データで新規点 x=4 の k=3 多数決を計算する。
| i | x | クラス | |x-4| |
|---|---|---|---|
| 1 | 1 | A | 3 |
| 2 | 3 | B | 1 |
| 3 | 5 | B | 1 |
| 4 | 7 | A | 3 |
| 5 | 9 | A | 5 |
1 2 3 4 5 6 7 8 9 | import numpy as np X = np.array([1, 3, 5, 7, 9]) y = np.array(['A', 'B', 'B', 'A', 'A']) new = 4 dist = np.abs(X - new) top3 = np.argsort(dist)[:3] labels, counts = np.unique(y[top3], return_counts=True) print(f"上位 3: {y[top3]}") print(f"多数決: {labels[counts.argmax()]}") |
💬 手計算 (Step 2) クラス B と Python 出力が完全一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import GridSearchCV import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number') X, y = num.iloc[:, 1:6].values, num.iloc[:, 0].values pipe = Pipeline([('sc', StandardScaler()), ('knn', KNeighborsRegressor())]) grid = {'knn__n_neighbors': [3, 5, 7, 9, 11], 'knn__weights': ['uniform', 'distance']} gs = GridSearchCV(pipe, grid, cv=5, scoring='r2').fit(X, y) print('Best:', gs.best_params_, 'R²:', gs.best_score_) |
まず環境をそろえます。 必要なライブラリを入れ、 matplotlib の日本語フォント(Hiragino Sans)を設定し、 SSDSE を encoding='cp932' で読み込んで shape・head()・describe() を確認します。 k 近傍法は距離で予測するので、 スケールの違う変数を混ぜると大きい単位の変数だけが効いてしまいます。 総人口(1,400 万)と合計特殊出生率(1.0 前後)をそのまま並べれば、 距離は事実上人口だけで決まります。 だから StandardScaler がこの手法では必須です。 エンコーディングを指定し忘れると UnicodeDecodeError で止まるのが最初の関門なので、 ここだけは丸暗記して構いません。
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 | # k近傍法 を SSDSE-B-2026 で確かめる最小コード import pandas as pd import numpy as np # 1) SSDSE-B-2026(教育用標準データセット)を読み込み df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) print('shape:', df.shape) # (564, 112) — 47 都道府県 × 12 年度 print('cols head:', list(df.columns[:8])) # 2) 直近年度(2023 年度)に絞る df23 = df[df['年度'] == 2023].copy() print('rows in 2023:', len(df23)) # 3) k近傍法 を動かすために必要な列だけ取り出す y = df23['合計特殊出生率'].astype(float) x = df23['総人口'].astype(float) print('y stats:', y.describe().round(3).to_dict()) print('x stats:', x.describe().round(0).to_dict()) # 4) k近傍法 の本処理(このページの主題) # — 具体実装は同カテゴリの個別ページにも掲載 print('---- k近傍法 結果 ----') print('mean y:', y.mean().round(3), '/ std y:', round(y.std(), 3)) print('mean x:', x.mean().round(0), '/ std x:', round(x.std(), 0)) print('corr(x, y):', y.corr(x).round(3)) |
うまく動かないときは ①data/raw/SSDSE-B-2026.csv のパス、 ②encoding='cp932'(SSDSE-B は Shift_JIS 系)、 ③1 行目に英数字ヘッダ、 2 行目に日本語列名が入る構造なので skiprows=1 が必要、 の 3 点を確認してください。
この用語を実務で使うときにつまずきやすい点を、 失敗パターン別に整理しました。 1 度経験すれば回避できるものばかりですが、 先に知っておくと事故が大幅に減ります。
kNN の予測品質は 分類精度と 近傍の質の両面で評価する。 効果量の解釈基準:
| 評価軸 | 指標 | 弱い | 中程度 | 強い |
|---|---|---|---|---|
| 分類精度 | Accuracy | < 0.7 | 0.7 ~ 0.85 | > 0.85 |
| 不均衡対応 | F1-score | < 0.6 | 0.6 ~ 0.8 | > 0.8 |
| 確率予測品質 | AUC | 0.5 ~ 0.7 | 0.7 ~ 0.85 | > 0.85 |
| 近傍類似度 | 平均距離 / max 距離 | > 0.7 | 0.3 ~ 0.7 | < 0.3 |
| k の安定性 | k=±1 の Accuracy 変動 | > 0.05 | 0.02 ~ 0.05 | < 0.02 |
| 日本語 | 英語 |
|---|---|
| 統計的に有意 | statistically significant |
| 効果量 | effect size |
| 95%信頼区間 | 95% confidence interval (CI) |
| 標本サイズ | sample size |
| 検出力 | statistical power |
| 第1種の誤り | Type I error / false positive |
| 第2種の誤り | Type II error / false negative |
| 多重比較問題 | multiple comparisons problem |
| 過学習 | overfitting |
| 汎化性能 | generalization |
| 交差検証 | cross-validation (CV) |
k近傍法 がデータサイエンスの体系の中でどこに位置するかを、 3つの異なる視点で可視化します。 同じ情報でも見方を変えると気付きが変わります。
🌐 統計・データサイエンス › 機械学習 › 教師あり学習 › k近傍法(事例ベースのノンパラメトリック手法)
中心に k近傍法 を置き、 そこから k-means・分類・クラスタリング・階層クラスタリング・Ward法 計 5 個の用語へ、 前提・兄弟・発展形といった関係を矢印で結んでいます。 線がどちら向きかを見れば、 先に読むべき用語とあとから読む用語が分かります。 ノードはドラッグで動かせ、 ホイールでズーム、 クリックでその用語のページへ遷移します。
大きな円が小さな円を包含する Circle Packing 図。 「k近傍法」は緑色でハイライト。
長方形を入れ子に分割した Treemap 図。 各分野の規模感を面積で比較。 「k近傍法」は緑色でハイライト。
| マップ | 分かること | こんな時に見る |
|---|---|---|
| 🔗 関係マップ | 手法間の横の関係(前提→発展→応用) | 「次に何を学べばよい?」 学習順序の判断 |
| ⭕ 包含マップ | 分類体系の入れ子構造(上位⊃下位) | 「この手法はどんなジャンルに属する?」 |
| 🌳 ツリーマップ | 分野の規模比較(面積=ボリューム) | 「データサイエンス全体の俯瞰像」 |
💡 3 つの図は同じ位置関係を別の角度から描いています。 関係マップは k近傍法 の隣に何が並ぶかを、 包含マップとツリーマップは 統計・データサイエンス → 教師あり学習 → k近傍法 という入れ子の位置を示します。 「教師あり学習には他に何があったか」を思い出したいときは包含マップを、 「次に何を読むか」を決めたいときは関係マップを開いてください。
k 近傍法は学習フェーズを持たない遅延学習であり、 前段の距離尺度設計と後段の k 選定・近傍探索高速化と組み合わせて使うことで実用性能が出る。
上流の標準化と次元削減 (高次元の呪いを回避) が距離計算の前提を整え、 並列のロジスティック回帰/SVM とベンチマーク比較し、 下流の k 最適化 (交差検証) と重み付け距離で予測精度を引き上げる流れが kNN を実戦投入する標準パイプラインとなる。
「knn」を含む手法選択は、 データの性質・分析目的・運用制約の 3 軸で決まる。 以下に典型シナリオと対応する手法の組合せを示す。
| シナリオ | 重視する観点 | 候補手法 |
|---|---|---|
| データが大規模 (n が多い、 高次元) | 計算コスト / メモリ / 並列化が必要 | SVM 等 |
| 外れ値が多い / ノイズあり | 頑健性 / 外れ値除去 / 中央値ベース | 決定木 等 |
| 解釈性を重視する | 線形 / 木構造 / ルールベース | Random Forest 等 |
| 予測精度を最優先する | アンサンブル / ハイパラ調整 / 交差検証 | ロジスティック回帰 等 |
| データが少ない | 正則化 / ベイズ / 転移学習 / 簡素なモデル | ナイーブベイズ 等 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd from sklearn.neighbors import KNeighborsRegressor from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) # 47 都道府県・最新年度に絞る。絞らないと最初の数値列が「年度」になり、 # 「指標から年度を当てる」という意味のない予測になって R² が負になる df = df[df['地域コード'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() df['年度'] = pd.to_numeric(df['年度'], errors='coerce') df = df[df['年度'] == df['年度'].max()] for _c in df.columns[3:]: df[_c] = pd.to_numeric(df[_c], errors='coerce') # 目的変数: 総人口、説明変数: 人口動態の 4 指標(実際に予測できる組合せにする) feats = ['出生数', '死亡数', '婚姻件数', '15歳未満人口'] sub = df[feats + ['総人口']].dropna() y = sub['総人口'].values X_std = StandardScaler().fit_transform(sub[feats].values) for metric in ['euclidean', 'manhattan', 'chebyshev', 'minkowski']: knn = KNeighborsRegressor(n_neighbors=5, metric=metric) sc = cross_val_score(knn, X_std, y, cv=5, scoring='r2').mean() print(f'{metric}: R²={sc:.3f}') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd from sklearn.neighbors import NearestNeighbors from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number') X_std = StandardScaler().fit_transform(num.iloc[:, :6].values) nn = NearestNeighbors(n_neighbors=4).fit(X_std) # 東京(index=12 と仮定)の近傍 idx_tokyo = df.index[df['都道府県'] == '東京都'][0] dist, idx = nn.kneighbors(X_std[idx_tokyo:idx_tokyo+1]) print('東京に似た都道府県:') for d, i in zip(dist[0], idx[0]): print(f' {df.iloc[i]["都道府県"]}: 距離={d:.3f}') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from scipy.spatial import KDTree, distance_matrix import pandas as pd import numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) X = df.select_dtypes('number').iloc[:, :5].values X = (X - X.mean(0)) / X.std(0) tree = KDTree(X) dist, idx = tree.query(X[0:1], k=5) print('近傍 idx:', idx, '距離:', dist) # 全ペア距離行列 D = distance_matrix(X, X) print('距離行列の形:', D.shape) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import faiss import numpy as np import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) X = df.select_dtypes('number').iloc[:, :5].values.astype('float32') X = (X - X.mean(0)) / X.std(0) d = X.shape[1] index = faiss.IndexFlatL2(d) # 完全 L2 index.add(X) D, I = index.search(X[:5], k=4) print('近傍 idx:', I) print('距離:', D) |
1 2 3 4 5 6 7 8 9 10 11 12 | from pyod.models.knn import KNN import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) X = df.select_dtypes('number').iloc[:, :5].values detector = KNN(n_neighbors=5, method='mean') detector.fit(X) df['異常スコア'] = detector.decision_scores_ outliers = df.nlargest(5, '異常スコア')[['都道府県', '異常スコア']] print('異常スコア上位5県:') print(outliers) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from sklearn.neighbors import KNeighborsRegressor from sklearn.preprocessing import StandardScaler import numpy as np import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number') X = num.iloc[:, 1:6].values y = num.iloc[:, 0].values X_std = StandardScaler().fit_transform(X) # 逆共分散行列 VI = np.linalg.pinv(np.cov(X_std.T)) knn = KNeighborsRegressor(n_neighbors=5, metric='mahalanobis', metric_params={'VI': VI}) knn.fit(X_std, y) print('予測例:', knn.predict(X_std[:3])) |
kNN の全体像は上のセクションで掴めたはずです。 ここでは なぜこの素朴な手法が機能するのか、 そして どこで壊れるのか を、 一段深い直感として言語化します。 既出の図・数式・ウィジェットと合わせて読むと理解が立体化します。
kNN が信じているのはただ一つ、 「特徴空間で近い点は、 出力(クラスや値)も似ている」という局所的な連続性の仮定です。 この仮定が成り立つ領域では、 未知点の答えは「ご近所さんに聞けば分かる」。 分類なら最も近い $k$ 個の多数決、 回帰なら $k$ 個の平均(または距離で重みづけた加重平均)を返すだけです。
重要なのは、 kNN が訓練時に何も要約しない点です。 線形回帰は係数、 決定木は分岐ルールという「圧縮された知識」を作りますが、 kNN は訓練データを丸ごと抱えたまま、 予測の瞬間に初めて距離を計算します。 これが 遅延学習(lazy learning / 事例ベース学習)と呼ばれる理由です。 「モデルの形」を決め打ちしないノンパラメトリックな柔らかさが強みで、 データが複雑な非線形の境界を持っていても、 十分な密度さえあれば追随できます。
kNN には学習すべきパラメータがほぼありません。 だからこそ性能を決めるのは「近さ」の定義そのもの、 すなわち 距離尺度と特徴量のスケールです。 線形モデルなら係数が特徴量の効き具合を自動調整してくれますが、 kNN にはその調整機構がありません。 スケールの大きい特徴量が距離を独占すれば、 それがそのまま「近さ」の定義になってしまいます。 つまり kNN では 前処理がモデルの一部だと考えるのが正しい直感です。
落とし穴カードは上に既出ですが、 ここでは 実測値で「どれくらい効くか」を数字で確かめます。 使用データは SSDSE-B-2026.csv(cp932, skiprows=[1])の 2023 年度・47 都道府県。 特徴量は 総人口・出生数・死亡数・婚姻件数・年平均気温の 5 つ、 予測対象は 合計特殊出生率(レンジ 0.99〜1.60、 標準偏差 0.13)。 評価は 47 件の Leave-One-Out 交差検証による平均絶対誤差(MAE, weights='distance')です。
総人口(数百万オーダー)と年平均気温(十数℃)を素の距離に混ぜると、 人口差だけが「近さ」を支配します。 標準化して全特徴量を平均 0・分散 1 に揃えると、 同じ $k$ でも MAE がはっきり下がりました。
| k | 標準化あり MAE | 標準化なし MAE | 改善幅 |
|---|---|---|---|
| 1 | 0.095 | 0.136 | −30% |
| 3 | 0.082 | 0.110 | −25% |
| 5 | 0.078 | 0.107 | −27% |
| 7 | 0.079 | 0.107 | −26% |
| 9 | 0.078 | 0.105 | −26% |
→ どの $k$ でも標準化ありが一貫して低誤差。 「アルゴリズムを変える」より「前処理を直す」方が効くという kNN の性格が数字で出ています。
同じ標準化ありの列を縦に読むと、 $k=1$(MAE 0.095)は各点 1 個に従うため過学習・高バリアンス、 $k=5$〜$9$(MAE 0.078 前後)で誤差が底を打ち、 それ以上大きくすると全国平均へ寄る過平滑・高バイアスに向かいます。 これは上の「図 3: バイアス・バリアンス」の U 字が、 47 都道府県の実データでも再現されたということです。 最適 $k$ はデータ依存なので、 必ず 交差検証で選びます(過学習・バイアスとバリアンスも参照)。
標準化後のユークリッド距離で各県の近傍 5 県を求めると、 直感と合う結果になりました(実測):
→ 予測が外れたときは「そもそも近傍が妥当か」をこう目視するのが第一歩。 近傍が突飛なら、 スケーリングか特徴量の選び方を疑います。
今回は 5 次元・47 サンプルなので近傍が意味を持ちましたが、 特徴量を SSDSE の 100 列すべてに増やすと、 サンプル 47 に対して次元が高すぎて近傍がほぼ等距離になります(次元の呪い)。 対策は特徴選択か 次元削減(PCA 等)です。 また kNN は予測のたびに全 47 点との距離を計算する $O(n\cdot d)$ の遅延学習で、 都道府県 47 件なら一瞬ですが、 数十万件になると scikit-learn の algorithm='kd_tree'/'ball_tree' や近似最近傍が必要です。 不均衡データ(例:ある稀なクラスが数県のみ)では多数派が近傍を埋めるため、 weights='distance' やクラス重みで補正します。
最後に、 実務・コンペで kNN を「使える道具」に引き上げる拡張を整理します。 いずれも「①局所の連続性」と「②近さの定義」という 2 本柱の改良として理解できます。
多数決を「1 票ずつ平等」ではなく、 近い点ほど重い票にします。 重み $w_i = 1/d_i$(距離の逆数)などを使うと、 境界付近の判断が滑らかになり、 同点(タイ)も起きにくくなります。 scikit-learn では weights='distance'。 上の実測でもこの設定を使っており、 不均衡・境界付近に強くなります。
全点との距離を毎回計算する代わりに、 空間を木構造で分割して「明らかに遠い領域」を枝刈りします。 KD 木は低〜中次元、 Ball 木は比較的高次元やユークリッド以外の距離に向きます。 予測を平均 $O(\log n)$ 級に落とせますが、 次元が非常に高いと木の効率も落ちる点に注意(ここでも次元の呪い)。
「厳密な最近傍」を諦め、 ほぼ最近傍を高速に返すのが ANN です。 HNSW(グラフ探索)や IVF/PQ(FAISS)は、 数百万〜数億件の埋め込み検索・レコメンド・RAG の中核技術。 精度を数%犠牲にして桁違いの速度を得るトレードオフを、 用途に応じて調整します。
高次元で崩れる「近い=似ている」を回復させるため、 PCA や多様体学習で意味のある低次元に落としてから kNN をかけます。 ノイズ次元を削ると近傍が再び直感的になり、 計算も軽くなります。 次元削減は kNN の前処理の定番です。
$k$ はデータ依存なので 交差検証(GridSearchCV)で選ぶのが基本。 さらに視野を広げると、 kNN は「クエリ点の近傍だけで局所的に予測する」という考え方の一例にすぎません。 半径内で回帰するカーネル回帰、 局所的に線を当てる局所回帰(LOESS)、 データを丸ごと使うカーネル密度推定まで、 いずれも「近傍を使う局所手法」という同じ家系です。 分類・回帰の全体像は 分類、 距離で群を作る発想は k-means・クラスタリングと地続きです。