第5〜7回の単変量補完では「その列の情報だけ」を使い、第8回では時系列特有の前後関係を使いました。
第5回:単変量補完① — 定数・任意値での補完(pandas と SimpleImputer)
https://www.salesanalytics.co.jp/datascience/datascience339/第6回:単変量補完② — 平均・中央値・最頻値での補完
https://www.salesanalytics.co.jp/datascience/datascience340/第7回:単変量補完③ — ランダムサンプル補完で分布を保つ
https://www.salesanalytics.co.jp/datascience/datascience341/第8回:時系列データの欠損値補完 — LOCF / NOCB / 線形補間
https://www.salesanalytics.co.jp/datascience/datascience342/
しかし、これらには共通の弱点があります。
それは 変数同士の関係(相関)を補完値に反映できない ことです。
たとえば「客室クラスが高い乗客は運賃も高い」「年齢が高い人は同伴家族が少ない」といった関係は、現実のデータに数多く存在します。
こうした 他の変数との関係を活用して補完する のが、今回扱う 多変量補完(multivariate imputation) です。
その第一歩として、直感的でわかりやすい KNN Imputer(k近傍法による補完) を紹介します。
多変量補完とは?
ここで、単変量補完と多変量補完の違いをもう一度整理しておきます。
| 補完に使う情報 | 代表的な手法 | |
|---|---|---|
| 単変量補完 | その列の値だけ | 平均・中央値・最頻値・定数・ランダム |
| 多変量補完 | 他の列の情報も含めて |
第1回でお話しした欠損メカニズムを思い出してください。
MAR(Missing At Random) は「欠損が他の観測変数で説明できる」状態でした。多変量補完は、まさにこの MAR に対して効果を発揮 します。
他の変数の情報を使うことで、欠損値をより正確に、バイアスを抑えて推定できるのです。
KNN Imputer とは?
KNN Imputer は、k近傍法(k-Nearest Neighbors) の考え方を補完に応用した手法です。
基本的なアイデアはとてもシンプルです。
欠損のある行に「似ている」行を k 個探し、 その k 個の値の平均(または加重平均)で欠損を埋める
「似ている」とは、欠損していない他の特徴量の値が近い、という意味です。
たとえば年齢が欠損している乗客がいたら、客室クラス・運賃・同伴家族数などが近い乗客を k 人探し、その人たちの年齢の平均で埋める、というイメージです。
アルゴリズムの流れ
1. 欠損のある行について、他の特徴量を使って各行との「距離」を計算 する
2. 距離が近い順に k 個の行(近傍) を選ぶ
3. その k 個の行の、補完したい列の値を 平均(または距離で重み付けした加重平均) して欠損を埋める
サンプルデータの準備
これまで通り、seaborn の Titanic データセットを使います。
KNN Imputer は 数値データ を対象とするため、数値列だけを抽出します。
以下、コードです。
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
# Titanicデータセットの読み込み
df = sns.load_dataset('titanic')
# 数値列だけを使う
numeric_cols = ['age', 'fare', 'pclass', 'sibsp', 'parch']
df_num = df[numeric_cols].copy()
# 学習用とテスト用に分割
df_train, df_test = train_test_split(
df_num,
test_size=0.2,
random_state=42
)
print("欠損数(学習データ):")
print(df_train.isnull().sum())
numeric_cols:KNN の距離計算に使える数値列を選択ageには欠損があり、これを他の列(fare,pclass,sibsp,parch)の情報で補完します
以下、実行結果です。
欠損数(学習データ): age 140 fare 0 pclass 0 sibsp 0 parch 0 dtype: int64
学習データの age 列に欠損があることが確認できます。
基本的な KNN Imputer の使い方
scikit-learn の KNNImputer クラスを使います。
第5〜6回で紹介した SimpleImputer と同じく、fit_transform(学習データ)/ transform(テストデータ)の流れでデータ漏洩を防げます。
以下、コードです。
from sklearn.impute import KNNImputer
# KNN Imputer を作成(近傍数 k=5)
knn_imputer = KNNImputer(n_neighbors=5, weights='uniform')
# 学習データで fit_transform
train_imputed = knn_imputer.fit_transform(df_train)
# テストデータには transform のみ
test_imputed = knn_imputer.transform(df_test)
# 配列をデータフレームに戻す
df_train_knn = pd.DataFrame(
train_imputed,
columns=numeric_cols,
index=df_train.index
)
df_test_knn = pd.DataFrame(
test_imputed,
columns=numeric_cols,
index=df_test.index
)
print(f"補完前の欠損(学習): {df_train['age'].isnull().sum()}")
print(f"補完後の欠損(学習): {df_train_knn['age'].isnull().sum()}")
print(f"補完後の欠損(テスト): {df_test_knn['age'].isnull().sum()}")
KNNImputer(n_neighbors=5, weights='uniform'):5つの近傍の 単純平均 で補完fit_transform:学習データの行から近傍関係を学び、変換するtransform:テストデータを、学習データの行を参照して補完する
以下、実行結果です。
補完前の欠損(学習): 140 補完後の欠損(学習): 0 補完後の欠損(テスト): 0
学習データ・テストデータの両方で age の欠損がゼロになっています。
最重要ポイント:標準化(スケーリング)
KNN Imputer を使ううえで、絶対に押さえておくべき注意点 があります。
それは 特徴量のスケール(数値の大きさ)を揃える こと、すなわち 標準化(standardization) です。
なぜ標準化が必要なのか
KNN は「距離」で近傍を判断します。
ところが、Titanic データの特徴量はスケールがバラバラです。
以下、コードです。
print(df_train.describe().round(2))
以下、実行結果です。
age fare pclass sibsp parch count 572.00 712.00 712.00 712.00 712.00 mean 29.50 32.59 2.33 0.55 0.38 std 14.50 51.97 0.82 1.18 0.79 min 0.42 0.00 1.00 0.00 0.00 25% 21.00 7.92 2.00 0.00 0.00 50% 28.00 14.45 3.00 0.00 0.00 75% 38.00 30.50 3.00 1.00 0.00 max 80.00 512.33 3.00 8.00 6.00
fare は0〜500超の範囲、pclass は1〜3、sibsp や parch は0〜8程度、と スケールがまったく違う ことがわかります。
このまま距離を計算すると、値が大きい fare の差ばかりが距離を支配 してしまい、pclass や sibsp の違いがほとんど無視されます。
これでは「本当に似ている行」を正しく見つけられません。
標準化を組み込んだ正しい実装
StandardScaler で標準化してから KNN 補完し、最後に元のスケールへ戻す、という流れを実装します。
以下、コードです。
from sklearn.preprocessing import StandardScaler
# ステップ1:標準化(学習データで fit)
scaler = StandardScaler()
train_scaled = scaler.fit_transform(df_train)
test_scaled = scaler.transform(df_test)
# ステップ2:標準化した状態で KNN 補完
knn = KNNImputer(
n_neighbors=5,
weights='uniform'
)
train_scaled_imp = knn.fit_transform(train_scaled)
test_scaled_imp = knn.transform(test_scaled)
# ステップ3:元のスケールに戻す(inverse_transform)
train_final = scaler.inverse_transform(train_scaled_imp)
test_final = scaler.inverse_transform(test_scaled_imp)
df_train_scaled_knn = pd.DataFrame(
train_final,
columns=numeric_cols,
index=df_train.index
)
print("標準化あり KNN 補完後の age(先頭5件):")
print(df_train_scaled_knn['age'].head())
print(f"補完後の欠損: {df_train_scaled_knn['age'].isnull().sum()}")
StandardScaler:各特徴量を平均0・標準偏差1に変換(スケールを揃える)scaler.fit_transform(df_train):学習データで スケーリングのパラメータを学ぶ(データ漏洩防止)scaler.inverse_transform(...):補完後、元の単位(歳・ドルなど)に戻す
以下、実行結果です。
標準化あり KNN 補完後の age(先頭5件): 331 45.5 733 23.0 382 32.0 704 26.0 813 6.0 Name: age, dtype: float64 補完後の欠損: 0
この「標準化 → 補完 → 逆変換」の流れが、KNN Imputer を正しく使う鉄則です。
パラメータ n_neighbors の影響を見る
KNN Imputer の最も重要なパラメータは n_neighbors(近傍数 k) です。
k をいくつにするかで補完結果が変わります。
- k が小さい(例:1〜2):少数の近い行だけを見る。局所的だが、外れ値の影響を受けやすい
- k が大きい(例:20〜):多くの行を平均する。安定するが、全体平均に近づき、平滑化されすぎる
実際に k を変えて、補完後の age 分布がどう変わるかを可視化してみましょう。
以下、コードです。
fig, ax = plt.subplots(figsize=(10, 5))
# 元データ(欠損を除いた age)の分布
original_age = df_train['age'].dropna()
ax.hist(original_age, bins=30, density=True, alpha=0.4,
color='gray', label='Original (NaN dropped)')
# k を変えて補完した結果を重ねる
for k in [1, 5, 20]:
scaler_k = StandardScaler()
tr_scaled = scaler_k.fit_transform(df_train)
knn_k = KNNImputer(n_neighbors=k)
tr_imp = scaler_k.inverse_transform(knn_k.fit_transform(tr_scaled))
age_imp = pd.DataFrame(tr_imp, columns=numeric_cols)['age']
# 密度の折れ線として描く(ヒストグラムの山の頂点を結ぶ)
counts, bin_edges = np.histogram(
age_imp,
bins=30,
density=True
)
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
ax.plot(
bin_centers,
counts,
linewidth=2,
label=f'KNN (k={k})'
)
ax.set_title('Effect of n_neighbors on Imputed Age Distribution')
ax.set_xlabel('Age')
ax.set_ylabel('Density')
ax.legend()
plt.tight_layout()
plt.show()
np.histogram(..., density=True):ヒストグラムの密度を計算(描画はせず数値だけ取得)- 各 k の補完結果を密度の折れ線として重ね描き
以下、実行結果です。

k=1 は元データに近いギザギザした分布、k=20 は滑らかだがピークが過度に集中した分布、k=5 はその中間、というように k によって分布の性質が変わる ことが視覚的にわかります。
一般的には k=5 前後が出発点としてよく使われますが、データに応じて調整します。
weights パラメータ:単純平均か加重平均か
もう一つの重要なパラメータが weights です。近傍 k 個の値をどう集約するかを決めます。
weights の値 |
動作 |
|---|---|
'uniform'(デフォルト) |
k 個すべてを 均等に平均 |
'distance' |
距離が近い行ほど大きく重み付け |
'distance' は「より似ている行の値を、より強く反映する」という、より洗練された集約方法です。
以下、コードです。
# uniform と distance を比較
for w in ['uniform', 'distance']:
sc = StandardScaler()
tr_s = sc.fit_transform(df_train)
knn_w = KNNImputer(n_neighbors=5, weights=w)
tr_i = sc.inverse_transform(knn_w.fit_transform(tr_s))
age_w = pd.DataFrame(tr_i, columns=numeric_cols)['age']
print(
f"weights='{w}': 補完後 age の平均={age_w.mean():.2f}, "
f"標準偏差={age_w.std():.2f}"
)
weights='uniform':5つの近傍を均等に平均weights='distance':近い行ほど強く反映した加重平均
以下、実行結果です。
weights='uniform': 補完後 age の平均=29.18, 標準偏差=13.63 weights='distance': 補完後 age の平均=29.19, 標準偏差=13.86
uniform と distance で補完後の平均・標準偏差がわずかに異なることが確認できます。
'distance' のほうが「より似た行」を重視するため、データによってはより妥当な補完になることがあります。
単変量補完との分布比較
KNN 補完が、第6回の中央値補完と比べてどれだけ自然な分布を保つかを確認しましょう。
以下、コードです。
# 中央値補完
df_median = df_train.copy()
df_median['age'] = df_median['age'].fillna(df_train['age'].median())
# KNN 補完(標準化あり)
sc = StandardScaler()
tr_s = sc.fit_transform(df_train)
knn_f = KNNImputer(n_neighbors=5)
tr_i = sc.inverse_transform(knn_f.fit_transform(tr_s))
df_knn = pd.DataFrame(
tr_i,
columns=numeric_cols,
index=df_train.index
)
# グラフ化
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
# 元データ(NaN を除く)
axes[0].hist(
df_train['age'].dropna(),
bins=30,
color='gray',
edgecolor='black'
)
axes[0].set_title('Original (NaN dropped)')
axes[0].set_xlabel('Age')
axes[0].set_ylabel('Frequency')
# 中央値補完
axes[1].hist(
df_median['age'],
bins=30,
color='steelblue',
edgecolor='black'
)
axes[1].set_title('Median Imputation')
axes[1].set_xlabel('Age')
# KNN 補完
axes[2].hist(
df_knn['age'],
bins=30,
color='seagreen',
edgecolor='black'
)
axes[2].set_title('KNN Imputation (k=5)')
axes[2].set_xlabel('Age')
plt.tight_layout()
plt.show()
以下、実行結果です。

中央値補完は中央値付近に不自然な突出が出るのに対し、KNN 補完は元の分布の形をより自然に保つ ことがわかります。
これは、KNN が「似た行の実際の値」を使って補完しているためです。
まとめ
今回のポイントを振り返りましょう。
- 多変量補完 は他の変数の情報を使う補完で、特に MAR に対して効果的
- KNN Imputer は「似た行を k 個探し、その値の平均で埋める」手法
- scikit-learn の
KNNImputerで実装でき、fit_transform/transformでデータ漏洩を防げる
- 標準化(
StandardScaler)が必須:スケールを揃えないと、値の大きい特徴量に距離が支配される - 正しい流れは「標準化 → KNN補完 → 逆変換」
n_neighbors(k) は補完結果に影響:小さいと局所的、大きいと平滑化される
weightsで単純平均('uniform')か加重平均('distance')を選べる- 中央値補完より 自然な分布を保てる が、計算コストが高く数値データに限られる

