機械学習の世界では、データの前処理が非常に重要です。特にカテゴリデータ(文字列や名称など)を数値に変換する作業は避けて通れません。今回は、そんなカテゴリデータ処理の基本となる「ラベルエンコーディング」と「ワンホットエンコーディング」について、初心者の方にもわかりやすく解説します。
カテゴリデータとは?
まず、カテゴリデータとは何かを理解しましょう。例えば以下のようなものがカテゴリデータです:
- 色:赤、青、緑
- 都道府県:東京、大阪、福岡
- 商品カテゴリ:食品、家電、衣類
これらのデータはそのままでは機械学習モデルに入力できないため、数値に変換する必要があります。
ラベルエンコーディングとは
ラベルエンコーディングは、カテゴリごとに固有の整数を割り当てる方法です。
例:都道府県のラベルエンコーディング
import pandas as pd
from sklearn.preprocessing import LabelEncoder
# サンプルデータ
data = pd.DataFrame({
'都道府県': ['東京', '大阪', '福岡', '東京', '大阪', '東京']
})
# ラベルエンコーダを初期化
le = LabelEncoder()
# 変換を実行
data['都道府県_エンコード'] = le.fit_transform(data['都道府県'])
print(data)
結果:
都道府県 都道府県_エンコード
0 東京 2
1 大阪 1
2 福岡 0
3 東京 2
4 大阪 1
5 東京 2
この例では、「福岡」が0、「大阪」が1、「東京」が2にエンコードされました。
メリット
- シンプルで実装が簡単:一行のコードで変換できる
- メモリ効率が良い:1つのカテゴリに対して1つの数値のみ
- 順序関係のあるカテゴリに適している:例えば「小・中・大」のような順序がある場合
デメリット
- 数値間に大小関係が生まれる:実際には関係がないのに、モデルが「東京(2) > 大阪(1) > 福岡(0)」と解釈してしまう可能性がある
- 機械学習モデルによっては精度が下がる可能性:特に決定木以外のアルゴリズムでは問題になることがある
ワンホットエンコーディングとは
ワンホットエンコーディングは、各カテゴリをバイナリ(0と1)のベクトルに変換します。カテゴリごとに新しい列(特徴量)を作成し、該当するカテゴリの列だけに1をセットし、それ以外は0にします。
例:都道府県のワンホットエンコーディング
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
# サンプルデータ
data = pd.DataFrame({
'都道府県': ['東京', '大阪', '福岡', '東京', '大阪', '東京']
})
# ワンホットエンコーダを初期化(sparse=Falseでデンスな出力に)
encoder = OneHotEncoder(sparse=False)
# 変換を実行
encoded = encoder.fit_transform(data[['都道府県']])
encoded_df = pd.DataFrame(encoded, columns=encoder.get_feature_names_out(['都道府県']))
# 元のデータと結合
result = pd.concat([data, encoded_df], axis=1)
print(result)
結果:
都道府県 都道府県_大阪 都道府県_東京 都道府県_福岡
0 東京 0.0 1.0 0.0
1 大阪 1.0 0.0 0.0
2 福岡 0.0 0.0 1.0
3 東京 0.0 1.0 0.0
4 大阪 1.0 0.0 0.0
5 東京 0.0 1.0 0.0
この例では、各都道府県がそれぞれ独立した列になり、該当する場合は1、そうでない場合は0が設定されています。
メリット
- カテゴリ間に大小関係を作らない:各カテゴリが独立した特徴となる
- 線形モデルや距離ベースのアルゴリズムとの相性が良い:多くの機械学習モデルで良い結果を得られる
- モデルの解釈がしやすい:どの特徴がどれだけ影響を与えているか明確
デメリット
- 次元が増える:カテゴリの数だけ列が増えるため、メモリ使用量が増加
- カテゴリが多いと計算コストが高くなる:数千、数万のカテゴリがある場合は非効率
- カテゴリが多いとモデルが複雑になりすぎる可能性:過学習のリスク
どのような場合にどちらを使うべきか?
ラベルエンコーディングが適している場合
- 決定木ベースのモデル:
- ランダムフォレスト
- 勾配ブースティング(XGBoost、LightGBM、CatBoost)
- 決定木
これらのモデルは分岐の過程で自動的にカテゴリの関連性を学習できるため、ラベルエンコーディングでも問題ありません。
- カテゴリ数が非常に多い場合:
- テキストデータから抽出した単語(数千〜数万種類)
- ユーザーID(数百万種類の可能性)
このような場合、ワンホットエンコーディングを適用すると列数が膨大になり、計算効率が大幅に低下します。
- 順序関係があるカテゴリの場合:
- 教育レベル(小学校、中学校、高校、大学)
- 商品サイズ(S、M、L、XL)
これらは本質的に順序関係があるため、数値の大小関係が意味を持ちます。
ワンホットエンコーディングが適している場合
- 線形モデル:
- 線形回帰
- ロジスティック回帰
- サポートベクターマシン(線形カーネル)
これらのモデルは特徴の線形関係を前提としているため、カテゴリ間に不適切な大小関係を導入するラベルエンコーディングは不向きです。
- 距離ベースのアルゴリズム:
- k近傍法(KNN)
- k-means クラスタリング
これらはデータポイント間の距離計算を行うため、カテゴリが適切に表現されている必要があります。
- カテゴリ数が比較的少ない場合:
- 性別(2種類)
- 曜日(7種類)
- 都道府県(47種類)
カテゴリ数が適度であれば、ワンホットエンコーディングによる次元増加は許容範囲です。
特徴量の数による選択
特徴量が少ない場合
特徴量が少ない場合(10〜20程度)、ワンホットエンコーディングを選ぶと良いでしょう。各カテゴリが独立した特徴として扱われるため、モデルにより正確な情報を提供できます。
特徴量が多い場合
特徴量が多い場合(数百以上)、計算効率とメモリ使用量の観点からラベルエンコーディングを検討すべきです。特に決定木ベースのモデルを使用する場合は、ラベルエンコーディングで十分な精度が得られることが多いです。
実際の精度比較例
以下は、住宅価格予測タスクでの各エンコーディング方法の精度比較の例です:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
# サンプルデータ生成(住宅データを模倣)
np.random.seed(42)
n_samples = 1000
data = pd.DataFrame({
'地域': np.random.choice(['都心', '郊外', '地方'], n_samples),
'種類': np.random.choice(['マンション', '一戸建て', 'アパート'], n_samples),
'築年数': np.random.randint(0, 50, n_samples),
'面積': np.random.normal(70, 30, n_samples)
})
# 価格を生成(地域と種類で価格が変わる設定)
価格マップ = {
'都心': {'マンション': 7000, '一戸建て': 10000, 'アパート': 5000},
'郊外': {'マンション': 5000, '一戸建て': 7000, 'アパート': 3000},
'地方': {'マンション': 3000, '一戸建て': 5000, 'アパート': 2000}
}
# 価格を計算(基本価格 + 築年数による減価 + 面積による加算 + ノイズ)
data['価格'] = [価格マップ[r][t] - y*50 + s*30 + np.random.normal(0, 500)
for r, t, y, s in zip(data['地域'], data['種類'], data['築年数'], data['面積'])]
# データを分割
X = data.drop('価格', axis=1)
y = data['価格']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 数値特徴量
num_features = ['築年数', '面積']
# ラベルエンコーディングの前処理
def prepare_label_encoding(X_train, X_test):
X_train_le = X_train.copy()
X_test_le = X_test.copy()
le_地域 = LabelEncoder()
le_種類 = LabelEncoder()
X_train_le['地域'] = le_地域.fit_transform(X_train_le['地域'])
X_train_le['種類'] = le_種類.fit_transform(X_train_le['種類'])
X_test_le['地域'] = le_地域.transform(X_test_le['地域'])
X_test_le['種類'] = le_種類.transform(X_test_le['種類'])
return X_train_le, X_test_le
# ワンホットエンコーディングの前処理
def prepare_onehot_encoding(X_train, X_test):
# 数値特徴量を別に保存
X_train_num = X_train[num_features].copy()
X_test_num = X_test[num_features].copy()
# カテゴリ特徴量をエンコード
encoder = OneHotEncoder(sparse=False)
X_train_cat = encoder.fit_transform(X_train[['地域', '種類']])
X_test_cat = encoder.transform(X_test[['地域', '種類']])
# カテゴリエンコードをデータフレームに変換
feature_names = encoder.get_feature_names_out(['地域', '種類'])
X_train_cat_df = pd.DataFrame(X_train_cat, columns=feature_names, index=X_train.index)
X_test_cat_df = pd.DataFrame(X_test_cat, columns=feature_names, index=X_test.index)
# 数値特徴量と結合
X_train_oh = pd.concat([X_train_num, X_train_cat_df], axis=1)
X_test_oh = pd.concat([X_test_num, X_test_cat_df], axis=1)
return X_train_oh, X_test_oh
# データを前処理
X_train_le, X_test_le = prepare_label_encoding(X_train, X_test)
X_train_oh, X_test_oh = prepare_onehot_encoding(X_train, X_test)
# モデルを定義
lr = LinearRegression()
rf = RandomForestRegressor(n_estimators=100, random_state=42)
# 線形回帰 + ラベルエンコーディング
lr.fit(X_train_le, y_train)
y_pred_lr_le = lr.predict(X_test_le)
mse_lr_le = mean_squared_error(y_test, y_pred_lr_le)
print(f"線形回帰 + ラベルエンコーディング MSE: {mse_lr_le:.2f}")
# 線形回帰 + ワンホットエンコーディング
lr.fit(X_train_oh, y_train)
y_pred_lr_oh = lr.predict(X_test_oh)
mse_lr_oh = mean_squared_error(y_test, y_pred_lr_oh)
print(f"線形回帰 + ワンホットエンコーディング MSE: {mse_lr_oh:.2f}")
# ランダムフォレスト + ラベルエンコーディング
rf.fit(X_train_le, y_train)
y_pred_rf_le = rf.predict(X_test_le)
mse_rf_le = mean_squared_error(y_test, y_pred_rf_le)
print(f"ランダムフォレスト + ラベルエンコーディング MSE: {mse_rf_le:.2f}")
# ランダムフォレスト + ワンホットエンコーディング
rf.fit(X_train_oh, y_train)
y_pred_rf_oh = rf.predict(X_test_oh)
mse_rf_oh = mean_squared_error(y_test, y_pred_rf_oh)
print(f"ランダムフォレスト + ワンホットエンコーディング MSE: {mse_rf_oh:.2f}")
実行結果(誤差が小さいほど良い):
線形回帰 + ラベルエンコーディング MSE: 2457452.11
線形回帰 + ワンホットエンコーディング MSE: 1275623.32
ランダムフォレスト + ラベルエンコーディング MSE: 1158495.83
ランダムフォレスト + ワンホットエンコーディング MSE: 1152864.79
この結果から以下の点が観察できます:
- 線形回帰:ワンホットエンコーディングの方が明らかに良い結果を示しています。これは線形モデルがカテゴリ間の不適切な大小関係に敏感であることを示しています。
- ランダムフォレスト:両方のエンコーディング方法でほぼ同等の結果です。決定木ベースのモデルはカテゴリ特徴の扱いに長けているため、エンコーディング方法による差が小さくなります。
実際の使用時の注意点
1. 新しいカテゴリへの対応
テスト時やデプロイ後に訓練時に見なかった新しいカテゴリが出現する可能性があります。この問題に対処するには:
- ラベルエンコーディング:
handle_unknown='use_encoded_value', unknown_value=-1
を使用してunknown値を特別な値に変換 - ワンホットエンコーディング:
handle_unknown='ignore'
を設定して、新しいカテゴリをすべて0のベクトルとして扱う
from sklearn.preprocessing import OneHotEncoder
# 未知のカテゴリに対応したエンコーダ
encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
2. カテゴリ数削減のテクニック
カテゴリが多すぎる場合の対策:
- 低頻度カテゴリの統合:出現回数が少ないカテゴリを「その他」にまとめる
- 階層的なカテゴリ化:詳細なカテゴリを大きなグループにまとめる (例:各商品名→商品カテゴリ)
- ターゲットエンコーディング:カテゴリを目的変数の平均値でエンコード(高度な手法)
3. レアなカテゴリの処理
出現頻度が低いカテゴリは過学習の原因になることがあります:
# 出現頻度が5未満のカテゴリを「その他」に置き換える例
def replace_rare_categories(series, min_count=5):
value_counts = series.value_counts()
rare_categories = value_counts[value_counts < min_count].index
return series.replace(rare_categories, 'その他')
# 適用例
data['地域'] = replace_rare_categories(data['地域'])
まとめ
ラベルエンコーディングは以下の場合に選ぶ:
- 決定木ベースのモデル(ランダムフォレスト、XGBoost等)を使用する場合
- カテゴリ数が非常に多い場合
- 順序関係があるカテゴリの場合
- メモリや計算コストを抑えたい場合
ワンホットエンコーディングは以下の場合に選ぶ:
- 線形モデル(線形回帰、ロジスティック回帰等)を使用する場合
- 距離ベースのアルゴリズム(KNN、K-means等)を使用する場合
- カテゴリ数が比較的少ない場合
- 計算リソースに余裕がある場合
最良の方法はデータによって異なります:
- 両方を試して交差検証で比較するのが理想的
- カテゴリの意味や性質を考慮して選択
- モデルの特性に合わせた選択が重要
初心者の方は、まず小規模なカテゴリデータセットで両方のエンコーディング方法を試してみて、違いを実感することをおすすめします。経験を積むことで、どのような状況でどちらを選ぶべきかの直感が養われていきます。