PythonでScikit-learnを使い、欠損値を回帰補完やKNN補完で補完する方法

Pythonで欠損値を補完する方法まとめ|KNN補完と回帰補完(線形・Ridge・Lasso)

データ解析や機械学習の前処理では、欠損値(missing values) の扱いが必須のテーマです。
欠損値を適切に処理しないと、統計量がゆがんだり、学習済みモデルの性能が大きく低下したりします。

本記事では、Python の Scikit-learn を使って、KNN補完(K近傍法による補完)と、
線形回帰・Ridge回帰・Lasso回帰を用いた回帰補完で欠損値を埋める具体的な方法を解説します。
単にコードを紹介するだけでなく、各手法の特徴や注意点にも触れます。

この記事でわかること

  • 欠損値とは何か、なぜそのままにしてはいけないのか
  • KNNImputer を用いた KNN 補完の基本的な使い方と注意点
  • Linear Regression / Ridge / Lasso による回帰補完の実装方法
  • 各手法のメリット・デメリットと、どのような状況で使うとよいか

こんな人におすすめ

  • 平均値・中央値以外の、もう一歩踏み込んだ欠損値補完方法を試したい人
  • Scikit-learn を使って KNN 補完や回帰補完を実装してみたい人
  • 線形回帰/Ridge/Lasso の違いを、欠損値補完の観点から理解したい人

主なライブラリと動作環境

記事内のコードでは、主に以下のライブラリを使用します。

  • numpy
  • pandas
  • scikit-learn(sklearn)

バージョンによって細かい挙動が異なる場合がありますが、
一般的な 1.x 系の Scikit-learn を想定しています。

まずは必要なライブラリをインポートします。

import time
import numpy as np
import pandas as pd

from sklearn.impute import KNNImputer
from sklearn.linear_model import LinearRegression, Ridge, Lasso
  

欠損値とは?

欠損値とは、本来観測されるはずだった値が何らかの理由で
記録されていない(NaN などになっている)状態 を指します。
データに欠損が含まれたまま平均や相関を計算したり、機械学習モデルにそのまま入力したりすると、
分布がゆがんだり、エラーが発生したりすることがあります。

欠損値が発生するパターンにはいくつかの種類がありますが、
実務では「なぜ欠損しているのか」を理解したうえで、
削除するのか、単純な値で埋めるのか、予測モデルで補完するのか を選ぶことが重要です。

欠損値処理方法の概要

代表的な欠損値処理の方法をざっくりまとめると、次のようになります。

  1. 欠損値を含む行や列を除外する
  2. 0 や固定値など、定数で置換する
  3. 平均値・中央値・最頻値で置換する
  4. 回帰モデルや KNN などの予測モデルで補完する

本記事では、より情報を活用できる手法である
(4) 回帰モデルや KNN を用いた補完 にフォーカスして解説します。

KNN補完(K近傍法)とは

KNN補完 は、類似したサンプル(レコード)を近傍として探し、
その値から欠損値を推定する方法です。
特徴量空間での距離が近い K 個のサンプルを見つけ、その値の平均(または距離で重み付けした平均)で欠損を埋めます。

Scikit-learn では KNNImputer クラスを使うことで、
数行のコードで KNN 補完を実行できます。

KNNImputer の基本的な使い方

from sklearn.impute import KNNImputer

# KNN補完用のインスタンスを生成
# n_neighbors: 近傍点の数(K)
# weights: 'uniform' なら等重み、'distance' なら距離の逆数で重み付け
# metric: 距離の計算方法(デフォルトは欠損に対応した nan_euclidean)
imputer = KNNImputer(
    n_neighbors=5,
    weights="uniform",
    metric="nan_euclidean",
)

# 欠損を含む例示用データ
X = [
    [1, 2, np.nan],
    [3, 4, 5],
    [np.nan, 6, 7],
]

# モデルを学習し、欠損値を補完
imputer.fit(X)
X_imputed = imputer.transform(X)

print(X_imputed)
  

KNN 補完は、データのスケールの影響を受けやすい という特徴があります。
特徴量ごとのスケール差が大きい場合は、StandardScaler などで前処理してから
KNNImputer を適用するほうが、距離の計算が安定しやすくなります。

また、K(n_neighbors)の値の選び方 によって結果が変わるため、
検証データを使って適切な K をチューニングすることが大切です。

回帰補完とは

回帰補完 は、「欠損を含む列(特徴量)を目的変数」、
「その他の欠損を含まない列を説明変数」とみなして回帰モデルを学習し、
その予測値で欠損を埋める方法です。

Scikit-learn では、例えば次のような回帰モデルがよく使われます。

  • LinearRegression:通常の線形回帰
  • Ridge:L2 正則化付きの線形回帰
  • Lasso:L1 正則化付きの線形回帰(係数が 0 になりやすい)

以下では共通のサンプルデータに対して、各回帰モデルで欠損値補完を行う例を順番に紹介します。

LinearRegression による回帰補完

まずは最もシンプルな 線形回帰 を使った例です。
欠損を含む列を順番に目的変数として学習し、その列の欠損値を予測値で埋めていきます。

# サンプルデータの準備
data = pd.DataFrame(
    {
        "a": [1, 2, np.nan, 4, np.nan, 6, 7, np.nan, 9, 10],
        "b": [1, 8, 5, 9, 10, 2, 7, 3, 4, 6],
        "c": [1, 2, 7, 8, 9, 10, 4, 3, 5, 6],
    }
)

print(data)

# 欠損値を持つ列名のリストを取得
features_with_nan = [
    feature
    for feature in data.columns
    if data[feature].isnull().sum() > 0
]

# 線形回帰モデルを作成
linreg = LinearRegression()

# 列ごとに回帰モデルを学習し、欠損値を補完
for feature in features_with_nan:
    # 欠損がない行のみを使って学習用データを作成
    X_train = data.loc[data[feature].notnull()].drop(columns=[feature])
    y_train = data.loc[data[feature].notnull(), feature]

    linreg.fit(X_train, y_train)

    # 欠損がある行を抽出し、予測して補完
    X_test = data.loc[data[feature].isnull()].drop(columns=[feature])
    data.loc[data[feature].isnull(), feature] = linreg.predict(X_test)

print(data)
  

線形回帰はシンプルで計算も軽い反面、
説明変数と目的変数の関係が線形に近い 場合に特に有効です。
非線形な関係が強い場合は、別のモデル(ランダムフォレストなど)を検討する余地があります。

Ridge 回帰による回帰補完

Ridge回帰 は、線形回帰に L2 正則化 を加えた手法です。
モデルの係数が大きくなりすぎることを抑制できるため、
説明変数同士にある程度相関がある場合でも、過学習をある程度防ぎながら安定した予測を行いやすくなります。

# 同じサンプルデータを再利用
data = pd.DataFrame(
    {
        "a": [1, 2, np.nan, 4, np.nan, 6, 7, np.nan, 9, 10],
        "b": [1, 8, 5, 9, 10, 2, 7, 3, 4, 6],
        "c": [1, 2, 7, 8, 9, 10, 4, 3, 5, 6],
    }
)

print(data)

features_with_nan = [
    feature
    for feature in data.columns
    if data[feature].isnull().sum() > 0
]

# Ridge 回帰モデルを作成
ridge = Ridge()

for feature in features_with_nan:
    X_train = data.loc[data[feature].notnull()].drop(columns=[feature])
    y_train = data.loc[data[feature].notnull(), feature]

    ridge.fit(X_train, y_train)

    X_test = data.loc[data[feature].isnull()].drop(columns=[feature])
    data.loc[data[feature].isnull(), feature] = ridge.predict(X_test)

print(data)
  

α(正則化の強さ)を調整することで、汎化性能とバイアスのバランス をコントロールできます。
特徴量が多く相関も強いデータでは、単純な線形回帰より Ridge 回帰を選んだほうが安定するケースが多いです。

Lasso 回帰による回帰補完

Lasso回帰 は、L1 正則化付きの線形回帰です。
係数を 0 に押し込む働きがあるため、不要な特徴量の係数が 0 になりやすく、
特徴量選択の効果 が得られます。

ただし、Lasso は正則化パラメータの値によっては多くの係数を 0 にしてしまうため、
欠損値補完の用途では、適切なパラメータ設定 が特に重要です。

# 同じサンプルデータを再利用
data = pd.DataFrame(
    {
        "a": [1, 2, np.nan, 4, np.nan, 6, 7, np.nan, 9, 10],
        "b": [1, 8, 5, 9, 10, 2, 7, 3, 4, 6],
        "c": [1, 2, 7, 8, 9, 10, 4, 3, 5, 6],
    }
)

print(data)

features_with_nan = [
    feature
    for feature in data.columns
    if data[feature].isnull().sum() > 0
]

# Lasso 回帰モデルを作成
lasso = Lasso()

for feature in features_with_nan:
    X_train = data.loc[data[feature].notnull()].drop(columns=[feature])
    y_train = data.loc[data[feature].notnull(), feature]

    lasso.fit(X_train, y_train)

    X_test = data.loc[data[feature].isnull()].drop(columns=[feature])
    data.loc[data[feature].isnull(), feature] = lasso.predict(X_test)

print(data)
  

手法ごとの比較と選び方の目安

ここまで紹介した KNN 補完と 3 種類の回帰補完を、ざっくり比較すると次のようなイメージです。

  • KNN補完
    類似サンプルから値を埋めるため、直感的で扱いやすい一方、
    データ数が多いと計算コストが増えます。特徴量のスケーリングが重要です。
  • LinearRegression
    線形な関係が強いときにシンプルで高速。正則化がないため、外れ値や多重共線性には弱い面があります。
  • Ridge
    L2 正則化により係数がなだらかになり、多重共線性がある場合にも比較的安定します。
    「とりあえず線形モデルで補完したい」場合の第一候補になりやすいです。
  • Lasso
    特徴量選択の効果があるため、不要な特徴量が多い場合に有効ですが、
    正則化が強すぎると補完精度が落ちる可能性があります。

実務では、検証用データを用意して複数手法を比較し、
最も誤差が小さい方法を採用する
のが基本です。

まとめ

Python の Scikit-learn を使うことで、KNN 補完や回帰補完(Linear / Ridge / Lasso)を簡単に実装できます。
単純な平均値補完に比べて、他の特徴量との関係性を利用できるため、
条件が合えばモデルの精度向上が期待できます。

一方で、欠損が大量にある場合や、欠損の発生メカニズムが偏っている場合 には、
どの手法でも補完の不確実性が高くなります。
そのようなときは、欠損の原因調査や、欠損パターンごとの別扱いなど、
モデリング以前の段階での工夫もあわせて検討してください。

本記事のコード例をベースに、自分のデータセットに合わせて
モデルやハイパーパラメータを調整しながら、最適な欠損値処理の方法を探してみてください。