Skip to content

5. 前処理と特徴量選択

本章では、機械学習モデルの性能を大きく左右する 前処理 と 特徴量選択 について学びます。実務では、モデルの選択よりもデータの前処理に多くの時間を費やすことが一般的です。

前処理の重要性

なぜ前処理が必要か

実世界のデータは、そのままでは機械学習モデルに適用できないことがほとんどです。

graph LR
    A("生データ") --> B("前処理")
    B --> C("学習可能なデータ")
    C --> D("機械学習モデル")

    B --> B1("欠損値処理")
    B --> B2("スケーリング")
    B --> B3("エンコーディング")
    B --> B4("特徴量選択")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef model fill:#F7EBF2,stroke:#CC79A7,stroke-width:2px,color:#000
    classDef out fill:#F5ECE2,stroke:#BF8040,stroke-width:2px,color:#000
    class A,C,B1 data
    class B,B2 proc
    class D,B3 model
    class B4 out

前処理が必要な理由

  • 欠損値:データに空白や NaN が含まれていると、多くのモデルはエラーになる
  • スケールの違い:特徴量ごとに値の範囲が大きく異なると、学習が不安定になる
  • カテゴリ変数:文字列データは数値に変換しないとモデルが扱えない
  • 外れ値:極端な値がモデルの学習を歪める可能性がある

前処理にかかる時間

一般的に、機械学習プロジェクトの 60〜80% の時間はデータの収集と前処理に費やされると言われています。

5.1 欠損値処理

5.1.1 欠損値の確認

欠損値を確認するためには、pandas の pd.DataFrame.isnull() メソッドを使用します。

import pandas as pd
import numpy as np

# 欠損値を含むサンプルデータ
df = pd.DataFrame({
    'age': [25, 30, np.nan, 35, 40],
    'salary': [300, np.nan, 400, 450, np.nan],
    'city': ['Tokyo', 'Osaka', None, 'Tokyo', 'Fukuoka']
})

# 欠損値の確認
print(df.isnull().sum())
print(df.isnull().mean() * 100)

5.1.2 欠損値の削除

欠損値を含む行を削除するには pd.DataFrame.dropna() メソッドを使用します。

# 欠損値を含む行を削除
df_dropped = df.dropna()
print(df_dropped)

削除の注意点

欠損値の削除はデータ量を減らすため、データが少ない場合は慎重に判断が必要です。

5.1.3 欠損値の補完

欠損値を特定の値で埋めるには pd.DataFrame.fillna() メソッドを使用します。

# 数値列を平均値で補完
df['age'] = df['age'].fillna(df['age'].mean())

# 数値列を中央値で補完
df['salary'] = df['salary'].fillna(df['salary'].median())

# カテゴリ列を最頻値で補完
df['city'] = df['city'].fillna(df['city'].mode()[0])

補完方法の選び方を以下の表にまとめます。

補完方法 適した場面 注意点
平均値 数値データ、正規分布に近い 外れ値の影響を受けやすい
中央値 数値データ、外れ値がある 平均値より頑健
最頻値 カテゴリデータ 偏りが生じる可能性がある
定数値 欠損に意味がある場合 ドメイン知識が必要

5.1.4 scikit-learn による欠損値処理

scikit-learn の sklearn.impute.SimpleImputer クラスを使うと、より体系的に欠損値を処理できます。

from sklearn.impute import SimpleImputer

# 平均値で補完
imputer = SimpleImputer(strategy='mean')
df_imputed = imputer.fit_transform(df[['age', 'salary']])

SimpleImputer クラスの strategy パラメータには、'mean'(平均値)、'median'(中央値)、'most_frequent'(最頻値)、'constant'(定数値)を指定できます。fit_transform() は、データから統計量を計算(fit() メソッド)し、それを使って変換(transform() メソッド)を行うメソッドです。

Playground:欠損値処理

欠損値処理 の Playground では、空欄を含むデータ表に対して「行を削除」「平均値で補完」「中央値で補完」「定数値で補完」を切り替えられます。補完されたセルがハイライトされ、行数や平均値がその場で変わるので、選ぶ手法によって手元に残るデータが変わることを確かめてみましょう。

5.2 スケーリング

5.2.1 スケーリングの必要性

特徴量のスケール(値の範囲)が大きく異なると、機械学習モデルの学習に悪影響を与えることがあります。

graph TB
    A("スケーリング") --> B("標準化<br/>StandardScaler")
    A --> C("正規化<br/>MinMaxScaler")

    B --> B1("平均=0, 標準偏差=1<br/>に変換")
    C --> C1("0〜1 の範囲<br/>に変換")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef neu fill:#F2F2F2,stroke:#8C8C8C,stroke-width:2px,color:#000
    class A neu
    class B,B1 data
    class C,C1 proc

5.2.2 標準化(StandardScaler)

標準化は、データを平均 0、標準偏差 1 に変換します。

\[ z = \frac{x - \mu}{\sigma} \]

scikit-learn では sklearn.preprocessing.StandardScaler クラスを使います。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

5.2.3 正規化(MinMaxScaler)

正規化は、訓練データの最小値を 0、最大値を 1 に変換します。未知のデータが訓練時の範囲を外れると、変換後の値も 0〜1 の範囲を外れる場合があります。

\[ x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}} \]

scikit-learn では sklearn.preprocessing.MinMaxScaler クラスを使います。

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)

スケーリングの選び方

  • StandardScaler:外れ値が少ない場合、多くの機械学習アルゴリズムで有効
  • MinMaxScaler:外れ値がない場合、ニューラルネットワークでよく使用

迷った場合は 標準化 を選ぶのが無難です。なお、決定木やランダムフォレストなどの木系のアルゴリズム(第8章で学びます)は、特徴量のスケールに影響を受けないため、スケーリングは不要です。

5.2.4 スケーリングがモデルに与える影響

スケーリングの有無でモデルの性能がどう変わるか、実際に確認してみましょう。

import seaborn as sns
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier

# ペンギンデータの読み込みと前処理
penguins = sns.load_dataset('penguins').dropna()
X = penguins[['bill_length_mm', 'bill_depth_mm',
              'flipper_length_mm', 'body_mass_g']]
y = penguins['species']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# スケーリングなし
knn = KNeighborsClassifier()
knn.fit(X_train, y_train)
print(f"スケーリングなし: {knn.score(X_test, y_test):.4f}")

# スケーリングあり
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

knn_scaled = KNeighborsClassifier()
knn_scaled.fit(X_train_scaled, y_train)
print(f"スケーリングあり: {knn_scaled.score(X_test_scaled, y_test):.4f}")
スケーリングなし: 0.8209
スケーリングあり: 1.0000

k-NN(k近傍法。第7章で学びます)はデータ間の距離に基づいて予測を行うため、スケーリングの有無で精度が大きく変わることがわかります。

fit_transform と transform の使い分け

上のコードでは、訓練データには fit_transform() メソッドを使い、テストデータには transform() メソッドのみを使っている点に注意してください。これは、テストデータに含まれる情報を学習に漏らさないための原則です。テストデータに対しても fit_transform() メソッドを使ってしまうと、テストデータの統計量(平均や標準偏差)がスケーリングに反映されてしまい、適切な評価ができなくなります。

Playground:スケーリング

スケーリング の Playground では、年齢[歳]と年収[万円]のように桁が違う 2 つの特徴量を使い、「なし」「標準化」「正規化」を切り替えられます。スケーリングなしでは k-NN の決定領域が年収だけで決まる横しま模様になり、尺度をそろえると年齢も使われるようになる様子を観察してみましょう。

5.3 エンコーディング

5.3.1 カテゴリ変数のエンコーディング

機械学習モデルは数値データしか扱えないため、カテゴリ変数を数値に変換する必要があります。

graph TB
    A("エンコーディング") --> B("ラベルエンコーディング")
    A --> C("One-Hot エンコーディング")

    B --> B1("各カテゴリに整数を割り当て<br/>例: A→0, B→1, C→2")
    C --> C1("各カテゴリをバイナリ列に展開<br/>例: A→[1,0,0], B→[0,1,0]")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef neu fill:#F2F2F2,stroke:#8C8C8C,stroke-width:2px,color:#000
    class A neu
    class B,B1 data
    class C,C1 proc

5.3.2 ラベルエンコーディング

scikit-learn では sklearn.preprocessing.LabelEncoder クラスを使います。

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
df['city_encoded'] = le.fit_transform(df['city'])

ラベルエンコーディングは便利ですが、注意点があります。カテゴリに数値が割り当てられるため、本来順序のないカテゴリ変数に適用すると、モデルが誤った大小関係を学習してしまう可能性があります。また、LabelEncoder クラスはカテゴリを文字コードの順(英字ならアルファベット順)に並べて番号を割り当てるため、サイズの S, M, L のように自然な順序のあるカテゴリでも「L=0、M=1、S=2」となり、本来の順序は反映されません。カテゴリの順序を数値に反映したい場合は、後述する 順序エンコーディング を使います。

5.3.3 One-Hot エンコーディング

pandas の pd.get_dummies() 関数または scikit-learn の sklearn.preprocessing.OneHotEncoder クラスを使います。

import pandas as pd

# pandas の get_dummies を使用
df_encoded = pd.get_dummies(df, columns=['city'], drop_first=True)
from sklearn.preprocessing import OneHotEncoder

# scikit-learn の OneHotEncoder を使用
encoder = OneHotEncoder(sparse_output=False, drop='first')
encoded = encoder.fit_transform(df[['city']])

どちらを使うべきか

  • 順序エンコーディング:順序があるカテゴリ(小・中・大など)。OrdinalEncoder クラスで順序を指定する
  • One-Hot エンコーディング:順序がないカテゴリ(色、地域など)

5.3.4 順序エンコーディング

順序エンコーディング(Ordinal Encoding) は、カテゴリに意味のある順序がある場合に使用します。scikit-learn の sklearn.preprocessing.OrdinalEncoder クラスを使い、カテゴリの順序を明示的に指定できます。

import pandas as pd
from sklearn.preprocessing import OrdinalEncoder

df = pd.DataFrame({
    'education': ['高校', '大学', '大学院', '高校', '大学']
})

# 順序を指定してエンコーディング
encoder = OrdinalEncoder(
    categories=[['高校', '大学', '大学院']]
)
df['education_encoded'] = encoder.fit_transform(
    df[['education']]
)
print(df)
  education  education_encoded
0        高校                0.0
1        大学                1.0
2       大学院                2.0
3        高校                0.0
4        大学                1.0

各エンコーディング手法の特徴と使い分けを以下の表にまとめます。

手法 特徴 適した場面 注意点
ラベル 整数に変換 決定木系モデル 順序を暗示する
ワンホット 0/1 の列を作成 線形モデル全般 列数が増える
順序 順序付き整数に変換 順序のあるカテゴリ 順序の指定が必要

Playground:カテゴリ変数のエンコーディング

カテゴリ変数のエンコーディング の Playground では、「色」のような文字カテゴリに対してラベルエンコーディングと One-Hot エンコーディングを切り替え、変換後の表を見比べられます。ラベルが 0・1・2 という順序を暗示してしまうこと、One-Hot ではカテゴリの数だけ列が増えることを実際の表で確認してみましょう。

5.4 特徴量選択

5.4.1 特徴量選択とは

すべての特徴量が予測に役立つわけではありません。不要な特徴量を削除することで、モデルの性能向上や計算時間の短縮が期待できます。

不要な特徴量を含めたままモデルを学習すると、以下のような問題が発生します。

  • 過学習:ノイズを含む特徴量がモデルの汎化性能を低下させる
  • 計算コストの増大:特徴量が多いほど学習に時間がかかる
  • 解釈性の低下:どの特徴量が重要かわかりにくくなる

ここでの例は特徴量選択の手順を説明するものです。予測性能を評価するときは、選択器もパイプラインに含め、各分割の訓練データだけで特徴量を選びます。

5.4.2 分散による特徴量選択

分散が小さい(ほとんど同じ値)特徴量は予測に貢献しにくいため、削除候補となります。scikit-learn では sklearn.feature_selection.VarianceThreshold クラスを使います。

from sklearn.feature_selection import VarianceThreshold

selector = VarianceThreshold(threshold=0.1)
X_selected = selector.fit_transform(X)

5.4.3 相関による特徴量選択

目的変数との相関が高い特徴量を選択する方法です。

import pandas as pd

# 相関行列の計算
correlation = df.corr()

# 目的変数との相関を確認
print(correlation['target'].sort_values(ascending=False))

5.4.4 モデルベースの特徴量重要度

ランダムフォレストなどの決定木系の機械学習モデルでは、各特徴量が予測にどれくらい役立ったかを、特徴量重要度(feature_importances_ 属性)として確認できます。これにより、モデルベースの特徴量選択を行えます。決定木については第8章で詳しく学びますが、ここでは結果のイメージだけを押さえてください。

import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder

# ペンギンデータの準備
penguins = sns.load_dataset('penguins').dropna()
X = penguins[['bill_length_mm', 'bill_depth_mm',
              'flipper_length_mm', 'body_mass_g']]
le = LabelEncoder()
y = le.fit_transform(penguins['species'])

# ランダムフォレストで特徴量重要度を計算
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

# 特徴量重要度の表示
importance = pd.Series(
    rf.feature_importances_, index=X.columns
).sort_values(ascending=False)
print(importance.round(4))

# 棒グラフで可視化
importance.plot(kind='barh')
plt.xlabel('特徴量重要度')
plt.title('ランダムフォレストの特徴量重要度')
plt.tight_layout()
plt.show()
bill_length_mm       0.4076
flipper_length_mm    0.3240
bill_depth_mm        0.1926
body_mass_g          0.0758
dtype: float64

bill_length_mm(くちばしの長さ)が最も重要な特徴量であることがわかります。

5.4.5 SelectKBest による特徴量選択

sklearn.feature_selection.SelectKBest クラスは、統計的検定に基づいて上位 \(k\) 個の特徴量を選択します。

import seaborn as sns
import pandas as pd
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.preprocessing import LabelEncoder

# データの準備
penguins = sns.load_dataset('penguins').dropna()
X = penguins[['bill_length_mm', 'bill_depth_mm',
              'flipper_length_mm', 'body_mass_g']]
le = LabelEncoder()
y = le.fit_transform(penguins['species'])

# 上位2つの特徴量を選択
selector = SelectKBest(score_func=f_classif, k=2)
X_selected = selector.fit_transform(X, y)

# 選択された特徴量を確認
selected = X.columns[selector.get_support()]
print(f"選択された特徴量: {list(selected)}")

# 各特徴量のスコア
scores = pd.Series(
    selector.scores_, index=X.columns
).sort_values(ascending=False)
print(f"\n特徴量スコア:")
print(scores.round(2))
選択された特徴量: ['bill_length_mm', 'flipper_length_mm']

特徴量スコア:
flipper_length_mm    567.41
bill_length_mm       397.30
bill_depth_mm        344.83
body_mass_g          341.89
dtype: float64

f_classif() 関数は分類問題用の統計検定(F検定)です。回帰問題の場合は f_regression() 関数を使用します。

F検定とは

F検定は、群間のばらつきと群内のばらつきを比べ、ある特徴量がクラスの違いをどれだけ説明できるかを評価する統計的手法です。値が大きいほど、クラス分けに有効な特徴量といえます。

Playground:特徴量選択

特徴量選択 の Playground では、各特徴量と目的変数(家賃)との相関の強さを棒グラフで見ながら、しきい値スライダーで使う特徴量を絞り込めます。物件 ID のような無意味な特徴量が相関ほぼ 0 で真っ先に落ちること、しきい値を上げすぎると役立つ特徴量まで失われることを試してみましょう。

5.5 機械学習パイプライン

5.5.1 パイプラインとは

複数の処理を順番につなげて1つにまとめたものを パイプライン と呼びます。scikit-learn では、Pipeline クラスを使って前処理や特徴量選択、モデルの学習を一連の流れとして管理できます。

graph LR
    A("生データ") --> B("欠損値処理")
    B --> C("スケーリング")
    C --> D("モデル学習")
    D --> E("予測")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef model fill:#F7EBF2,stroke:#CC79A7,stroke-width:2px,color:#000
    classDef out fill:#F5ECE2,stroke:#BF8040,stroke-width:2px,color:#000
    class A data
    class B,C proc
    class D model
    class E out

5.5.2 scikit-learn の Pipeline

scikit-learn の sklearn.pipeline.Pipeline クラスを使うと、複数の処理ステップを 1 つのオブジェクトにまとめられます。

import seaborn as sns
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split

# データの準備(欠損値はパイプラインの中で補完する)
penguins = sns.load_dataset("penguins")
X = penguins[["bill_length_mm", "bill_depth_mm",
              "flipper_length_mm", "body_mass_g"]]
y = penguins["species"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# パイプラインの構築
pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="mean")),  # 1. 欠損値を平均値で補完
    ("scaler", StandardScaler()),                 # 2. 標準化
    ("classifier", KNeighborsClassifier())        # 3. k近傍法で分類
])

# 学習と予測(前処理とモデルが一括で実行される)
pipeline.fit(X_train, y_train)
score = pipeline.score(X_test, y_test)
print(f"パイプラインの精度: {score:.4f}")
パイプラインの精度: 0.9855

パイプラインの利点

  • コードがすっきりする
  • データ漏洩(data leakage)を防げる
  • 交差検証との相性が良い

Pipeline クラスにはステップを (名前, 変換器) のタプルのリストとして渡します。ここで 変換器 とは、標準化や欠損値補完のようにデータを変換する処理のことです。パイプラインの fit() メソッドを呼ぶと、前処理の各ステップでは fit_transform() メソッドが順に実行され、変換後のデータが次のステップへ渡されます。最後のステップでは、そのデータを使ってモデルの fit() メソッドが実行されます。predict() メソッドや score() メソッドを呼ぶと、学習時と同じ前処理(transform() メソッド)が適用されたあとで、予測や評価が行われます。

5.5.3 ColumnTransformer で異なる列に異なる処理を適用

実際のデータセットには数値列とカテゴリ列が混在しています。sklearn.compose.ColumnTransformer クラスを使うと、列の種類ごとに異なる前処理を適用できます。

import seaborn as sns
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split

# Titanicデータの準備
titanic = sns.load_dataset('titanic')
features = ['age', 'fare', 'pclass', 'sex', 'embarked']
X = titanic[features]
y = titanic['survived']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 数値列とカテゴリ列を定義
numeric_features = ['age', 'fare']
categorical_features = ['pclass', 'sex', 'embarked']

# 数値列の前処理パイプライン
numeric_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# カテゴリ列の前処理パイプライン
categorical_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore'))
])

# ColumnTransformer で統合
preprocessor = ColumnTransformer([
    ('num', numeric_transformer, numeric_features),
    ('cat', categorical_transformer, categorical_features)
])

# 前処理 + モデルのパイプライン
full_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', KNeighborsClassifier())
])

# 学習と評価
full_pipeline.fit(X_train, y_train)
score = full_pipeline.score(X_test, y_test)
print(f"Titanic 生存予測の精度: {score:.4f}")
Titanic 生存予測の精度: 0.7933

ColumnTransformer クラスには (名前, 変換器, 対象列) という形のタプルを並べて渡します。上の例では、数値列 numeric_features に numeric_transformer(中央値補完→標準化)を、カテゴリ列 categorical_features に categorical_transformer(最頻値補完→ワンホット)を適用し、これを Pipeline クラスで分類器とつなぐことで、前処理から学習までを一括で実行しています。各列に適用される処理を以下の表にまとめます。

ステップ 数値列の処理 カテゴリ列の処理
1. 欠損値補完 中央値(SimpleImputer) 最頻値(SimpleImputer)
2. 変換 標準化(StandardScaler) ワンホット(OneHotEncoder)
3. モデル KNeighborsClassifier KNeighborsClassifier

5.6 汎化性能の評価とモデル選択

5.6.1 交差検証

データを複数に分割し、それぞれで学習・評価を行うことで、モデルの汎化性能をより正確に評価できます。データを \(k\) 個のグループ(フォールド)に分割し、そのうち 1 つを検証用、残りを訓練用として \(k\) 回繰り返し評価する手法を k分割交差検証(k-Fold Cross-Validation) と呼びます。

例えば 5 分割交差検証(\(k=5\))では、次のように 5 回の学習と評価を行います。

回 Fold 1 Fold 2 Fold 3 Fold 4 Fold 5
1 検証 訓練 訓練 訓練 訓練
2 訓練 検証 訓練 訓練 訓練
3 訓練 訓練 検証 訓練 訓練
4 訓練 訓練 訓練 検証 訓練
5 訓練 訓練 訓練 訓練 検証

5 回の評価スコアの平均を最終的な評価指標とすることで、より信頼性の高い評価が得られます。scikit-learn では sklearn.model_selection.cross_val_score() 関数を使います。

import seaborn as sns
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

# データの準備
penguins = sns.load_dataset("penguins").dropna()
X = penguins[["bill_length_mm", "bill_depth_mm",
              "flipper_length_mm", "body_mass_g"]]
y = penguins["species"]

# パイプラインの構築
pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", KNeighborsClassifier())
])

# 5分割交差検証
scores = cross_val_score(pipeline, X, y, cv=5)
print(f"各フォールドのスコア: {scores.round(4)}")
print(f"平均スコア: {scores.mean():.4f}")
print(f"標準偏差:   {scores.std():.4f}")
各フォールドのスコア: [0.9851 0.9851 0.9851 0.9848 1.    ]
平均スコア: 0.9880
標準偏差:   0.0060

cv パラメータを 5 に指定すると、データを 5 分割し、学習と評価を 5 回繰り返します。平均スコアが高く、標準偏差が小さいほど、安定して高い精度を出せるモデルであることを意味します。

Playground:交差検証

交差検証 の Playground では、分割数 k を変えながら各フォールドの訓練用・検証用の割り当てと、そのときのスコアを 1 回ずつ追えます。同じデータでも分け方によってスコアがばらつくこと、平均を取ると安定した評価になることを、標準偏差の値とあわせて確かめてみましょう。

5.6.2 訓練・検証・テスト分割の考え方

以降で説明するハイパーパラメータの調整や複数モデルの比較を行う場合は、データを訓練・検証・テストの 3 つに分割することが推奨されます。交差検証を使う場合は、訓練データと検証データの役割を交差検証が自動的に担うため、全体を訓練用(交差検証用)とテスト用の 2 つに分割すれば十分です。

from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score

# まずテストデータを分離(最終評価用)
X_trainval, X_test, y_trainval, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 訓練+検証データで交差検証
scores = cross_val_score(pipeline, X_trainval, y_trainval, cv=5)
print(f"交差検証スコア: {scores.mean():.4f}")

# 最終評価はテストデータで 1 回だけ行う
pipeline.fit(X_trainval, y_trainval)
test_score = pipeline.score(X_test, y_test)
print(f"テストスコア:   {test_score:.4f}")
交差検証スコア: 0.9813
テストスコア:   1.0000

5.6.3 GridSearchCV によるハイパーパラメータ探索

ハイパーパラメータの最適な組み合わせを探索する方法です。scikit-learn では sklearn.model_selection.GridSearchCV クラスを使います。

import seaborn as sns
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split

# データの準備
penguins = sns.load_dataset("penguins").dropna()
X = penguins[["bill_length_mm", "bill_depth_mm",
              "flipper_length_mm", "body_mass_g"]]
y = penguins["species"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# パイプラインの構築
pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", KNeighborsClassifier())
])

# 探索するパラメータの候補(「ステップ名__パラメータ名」で指定)
param_grid = {
    "classifier__n_neighbors": [3, 5, 7, 9, 11],       # k の候補
    "classifier__weights": ["uniform", "distance"]     # 近傍の重み付け方法
}

# GridSearchCV の実行
grid_search = GridSearchCV(
    pipeline,
    param_grid,
    cv=5,
    scoring="accuracy",
    n_jobs=-1
)
grid_search.fit(X_train, y_train)

# 結果の表示
print(f"最適なパラメータ: {grid_search.best_params_}")
print(f"最高スコア(交差検証): {grid_search.best_score_:.4f}")
print(f"テストスコア: {grid_search.score(X_test, y_test):.4f}")
最適なパラメータ: {'classifier__n_neighbors': 7, 'classifier__weights': 'distance'}
最高スコア(交差検証): 0.9850
テストスコア: 1.0000

パイプラインと組み合わせる場合、param_grid ではパイプライン内のパラメータを ステップ名__パラメータ名 の形式で指定します(アンダースコア 2 つでステップ名とパラメータ名をつなぎます)。また、n_jobs=-1 を指定すると、すべての CPU コアを使って並列処理されるため、計算が高速化されます。

5.6.4 複数モデルの比較

パイプラインと交差検証を組み合わせることで、複数のモデルを公平に比較できます。

import seaborn as sns
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier

# データの準備
penguins = sns.load_dataset('penguins').dropna()
X = penguins[['bill_length_mm', 'bill_depth_mm',
              'flipper_length_mm', 'body_mass_g']]
y = penguins['species']

# 比較するモデルの定義
models = {
    'k-NN': KNeighborsClassifier(),
    'Logistic': LogisticRegression(max_iter=1000),
    'Decision Tree': DecisionTreeClassifier(random_state=42),
    'Random Forest': RandomForestClassifier(random_state=42)
}

# 各モデルの交差検証スコアを計算
results = []
for name, model in models.items():
    pipeline = Pipeline([
        ('scaler', StandardScaler()),
        ('classifier', model)
    ])
    scores = cross_val_score(pipeline, X, y, cv=5)
    results.append({
        'Model': name,
        'Mean Score': scores.mean(),
        'Std': scores.std()
    })

# 結果を DataFrame で表示
df_results = pd.DataFrame(results)
df_results = df_results.sort_values(
    'Mean Score', ascending=False
)
print(df_results.round(4).to_string(index=False))
        Model  Mean Score    Std
     Logistic      0.9910 0.0119
         k-NN      0.9880 0.0060
Random Forest      0.9790 0.0154
Decision Tree      0.9669 0.0200

交差検証のスコアと標準偏差の両方を見ることで、精度が高いだけでなく安定しているモデルを選ぶことができます。

5.7 演習問題

演習 5-1: 欠損値処理の実践

演習 5-1

titanic データセットを使って、以下のタスクを実行してください。

タスク:

  1. データを読み込み、各列の欠損値の数と割合を表示する
  2. age 列の欠損値を中央値で補完する
  3. embarked 列と embark_town 列の欠損値を、それぞれの最頻値で補完する
  4. 欠損値の多い deck 列を削除する
  5. 処理後のデータに欠損値がないことを確認する
解答例
import seaborn as sns

# 1. データの読み込みと欠損値の確認
titanic = sns.load_dataset("titanic")
missing = titanic.isnull().sum()
ratio = (missing / len(titanic) * 100).round(1)
print("欠損値の数と割合(欠損のある列のみ):")
print(missing[missing > 0])
print(ratio[missing > 0])

# 2. age 列を中央値で補完
titanic["age"] = titanic["age"].fillna(titanic["age"].median())

# 3. embarked 列と embark_town 列を、それぞれの最頻値で補完
titanic["embarked"] = titanic["embarked"].fillna(
    titanic["embarked"].mode()[0]
)
titanic["embark_town"] = titanic["embark_town"].fillna(
    titanic["embark_town"].mode()[0]
)

# 4. deck 列を削除
titanic = titanic.drop(columns=["deck"])

# 5. 欠損値がないことを確認
print(f"\n処理後の欠損値の合計: {titanic.isnull().sum().sum()}")
欠損値の数と割合(欠損のある列のみ):
age            177
embarked         2
deck           688
embark_town      2
dtype: int64
age            19.9
embarked        0.2
deck           77.2
embark_town     0.2
dtype: float64

処理後の欠損値の合計: 0

考察:age は数値で分布に偏りがあるので、外れ値の影響を受けにくい中央値で補完しました。embarked はカテゴリ変数なので最頻値です。deck は77.2%が欠損していて、補完しても大半が推測の値になるので削除しました。embark_town は embarked と同じ内容で、欠損も同じ2行です。

演習 5-2: スケーリングの効果を比較

演習 5-2

penguins データセットを使って、以下のタスクを実行してください。

タスク:

  1. 入力5.10と同様に dropna() メソッドで欠損行を除き、数値特徴量4列を X、species 列を y とする
  2. 訓練・テストに分割する(test_size=0.2, random_state=42)。以降の3パターンで同じ分割を使う
  3. スケーリングなし、StandardScaler クラス、MinMaxScaler クラスの3パターンで k-NN モデルを学習する。スケーラーは訓練データで fit_transform() メソッドで学習し、テストデータには transform() メソッドのみを適用する
  4. それぞれのテストデータに対する精度を比較する
  5. 結果から、スケーリングの重要性について考察する
解答例
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

# 1. データの準備(数値特徴量4列)
penguins = sns.load_dataset("penguins").dropna()
feature_cols = ["bill_length_mm", "bill_depth_mm",
                "flipper_length_mm", "body_mass_g"]
X = penguins[feature_cols].values
y = penguins["species"].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 2. 3パターンで k-NN を学習し、3. 精度を比較
patterns = {
    "スケーリングなし": None,
    "StandardScaler": StandardScaler(),
    "MinMaxScaler": MinMaxScaler(),
}
for name, scaler in patterns.items():
    if scaler is None:
        X_train_s, X_test_s = X_train, X_test
    else:
        X_train_s = scaler.fit_transform(X_train)
        X_test_s = scaler.transform(X_test)
    model = KNeighborsClassifier(n_neighbors=5)
    model.fit(X_train_s, y_train)
    acc = accuracy_score(y_test, model.predict(X_test_s))
    print(f"{name}: {acc:.4f}")
スケーリングなし: 0.8209
StandardScaler: 1.0000
MinMaxScaler: 1.0000

考察:精度はスケーリングなしで0.8209、StandardScaler クラスと MinMaxScaler クラスではどちらも1.0000でした。body_mass_g は数千グラム、bill_depth_mm は十数ミリと値の範囲が大きく違います。スケーリングしないと、距離がほぼ体重だけで決まってしまいます。k-NN のように距離を使うモデルでは、スケーリングが必要です。

演習 5-3: パイプラインの構築

演習 5-3

titanic データセットを使って、以下の仕様のパイプラインを構築してください。目的変数は survived 列(生存の有無)です。本章の演習では、本文と同じく k-NN を分類モデルとして使います。モデルのしくみは第7章で学びます。

タスク:

  1. 使用する特徴量:age, fare(数値列)、sex, embarked, pclass(カテゴリ列)
  2. 数値列:中央値で欠損値補完 → 標準化
  3. カテゴリ列:最頻値で欠損値補完 → ワンホットエンコーディング
  4. モデル:k-NN(KNeighborsClassifier クラス。入力5.22と同じ)
  5. 5 分割交差検証でモデルの精度を評価する
解答例
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

# データの準備
titanic = sns.load_dataset("titanic")
num_cols = ["age", "fare"]
cat_cols = ["sex", "embarked", "pclass"]
X = titanic[num_cols + cat_cols]
y = titanic["survived"]

# 数値列: 中央値補完 → 標準化
num_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

# カテゴリ列: 最頻値補完 → ワンホットエンコーディング
cat_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore"))
])

# 列ごとに前処理を振り分けるColumnTransformer
preprocessor = ColumnTransformer([
    ("num", num_pipeline, num_cols),
    ("cat", cat_pipeline, cat_cols)
])

# 前処理 + k-NNのパイプライン
pipeline = Pipeline([
    ("preprocessor", preprocessor),
    ("classifier", KNeighborsClassifier())
])

# 5分割交差検証で評価
scores = cross_val_score(pipeline, X, y, cv=5)
print(f"各分割のスコア: {scores.round(4)}")
print(f"平均スコア: {scores.mean():.4f}")
各分割のスコア: [0.7877 0.8034 0.809  0.8146 0.809 ]
平均スコア: 0.8047

考察:前処理をパイプラインに入れると、交差検証の各分割で、訓練側だけで fit して検証側には transform だけを使う手順が自動で守られます。そのためデータ漏洩が起きません。平均スコアは0.8047(約80%)でした。どの特徴量が効いているかは、同じ分割のまま特徴量の組み合わせを変えて比べると調べられます。

演習 5-4: GridSearchCV によるモデル最適化

演習 5-4

演習 5-3 で構築したパイプラインを拡張して、以下のタスクを実行してください。

タスク:

  1. 先に訓練用と最終テスト用に分割する(test_size=0.2, random_state=42)。GridSearchCV クラスには訓練用だけを渡す
  2. k-NN の n_neighbors を [3, 5, 7, 9, 11]、weights を ["uniform", "distance"] の中から探索する
  3. GridSearchCV クラスを使い、訓練データ内の5分割交差検証で最適なパラメータを見つける
  4. 最適なパラメータと交差検証スコアを表示する
  5. 選んだモデルをテストデータで一度だけ評価する
解答例
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split, GridSearchCV

# データの準備(演習5-3と同じ)
titanic = sns.load_dataset("titanic")
num_cols = ["age", "fare"]
cat_cols = ["sex", "embarked", "pclass"]
X = titanic[num_cols + cat_cols]
y = titanic["survived"]

# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 演習5-3と同じパイプライン
num_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])
cat_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore"))
])
preprocessor = ColumnTransformer([
    ("num", num_pipeline, num_cols),
    ("cat", cat_pipeline, cat_cols)
])
pipeline = Pipeline([
    ("preprocessor", preprocessor),
    ("classifier", KNeighborsClassifier())
])

# 2-3. k-NN の近傍数と重み付け方法を GridSearchCV で探索
param_grid = {
    "classifier__n_neighbors": [3, 5, 7, 9, 11],
    "classifier__weights": ["uniform", "distance"],
}
grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)

# 4. 最適なパラメータと交差検証スコア
print(f"最適なパラメータ: {grid_search.best_params_}")
print(f"交差検証スコア: {grid_search.best_score_:.4f}")

# 5. テストデータでの最終評価
test_acc = grid_search.score(X_test, y_test)
print(f"テストデータでの精度: {test_acc:.4f}")
最適なパラメータ: {'classifier__n_neighbors': 3, 'classifier__weights': 'uniform'}
交差検証スコア: 0.8230
テストデータでの精度: 0.7877

考察:パイプライン内のパラメータは ステップ名__パラメータ名(アンダースコア2つ)で指定します。選ばれたのは n_neighbors=3、weights="uniform" で、交差検証スコアは0.8230、テストデータでの精度は0.7877でした。1回の分割だけでは、過学習の有無は判断できません。パラメータの探索は訓練データ内の交差検証で行い、テストデータは最後に一度だけ使います。

まとめ

本章では、機械学習における前処理と特徴量選択について学びました。

第 5 章のまとめ

欠損値処理

スケーリング

エンコーディング

  • ラベルエンコーディング:ラベルを整数に変換(整数の順番が意味の順序と一致するとは限らない)
  • One-Hot エンコーディング:順序がないカテゴリ向け
  • 順序エンコーディング:カテゴリの順序を明示的に指定

特徴量選択

パイプライン

  • 複数の前処理とモデルを一連の流れで管理
  • データ漏洩を防ぎ、コードを整理できる
  • ColumnTransformer クラスで列の種類ごとに異なる前処理を適用

モデル選択

  • 交差検証で汎化性能を評価
  • GridSearchCV クラスでハイパーパラメータを探索