4日目:カテゴリ変数と数値変数!データ型に応じた前処理テクニック
皆さん、こんにちは!AI学習ロードマップ4日目です。昨日までに、Pandasでのデータ読み込みと整形、そしてMatplotlib/Seabornでの可視化を通じて、データの本質を理解する重要性を学びました。今日は、AIモデルにデータを投入する前の 「前処理」 の中でも特に重要な、 「カテゴリ変数と数値変数」のデータ型に応じたテクニック に焦点を当てていきます。
AIモデルは、生のデータをそのまま学習できるわけではありません。特に、私たちが日常で使う「色」や「性別」のような カテゴリ変数 は、モデルが理解できる 数値 に変換する必要があります。また、 数値変数 も、モデルの性能を最大化するために適切なスケーリングや変換が必要になることがあります。
AIエンジニアとして、これらの前処理テクニックを習得することは、モデルの精度を向上させ、頑健性を高めるための必須スキルです。それぞれのテクニックの理論と実践を、豊富なコード例とともに解説していきます。
1. AIモデルとデータ型の関係性
ほとんどの機械学習モデルは、入力として数値データを期待します。これは、モデルが内部で数学的な計算(例:重み付け、距離計算など)を行うためです。
-
数値変数 (Numerical Variables):
- 連続変数 (Continuous): 身長、体重、気温、売上高など、無限の値を取りうる変数。
- 離散変数 (Discrete): 家族の人数、商品の個数、テストの点数など、整数値を取りうる変数。
- これらの数値変数は、そのままモデルに入力できることが多いですが、スケールの違いや分布の偏りがモデルの学習に悪影響を与えることがあります。
-
カテゴリ変数 (Categorical Variables):
- 順序なしカテゴリ変数 (Nominal): 性別(男性、女性)、色(赤、青、緑)、都市(東京、大阪)など、順序に意味がない変数。
- 順序ありカテゴリ変数 (Ordinal): 学歴(小学、中学、高校、大学)、評価(低、中、高)、満足度(不満、普通、満足)など、順序に意味がある変数。
- これらのカテゴリ変数は、直接モデルに入力することができません。何らかの数値表現に変換する必要があります。
このセクションでは、それぞれのデータ型がモデルに与える影響を理解し、適切な前処理手法を選択することの重要性を解説します。
2. カテゴリ変数の前処理テクニック
カテゴリ変数を数値に変換する主要なテクニックを見ていきましょう。
2.1. ラベルエンコーディング (Label Encoding)
カテゴリを単純な整数値にマッピングする手法です。
- 適用例: 順序に意味のあるカテゴリ変数(例:学歴、評価)
- メリット: シンプルでメモリ効率が良い。
- デメリット: 順序に意味のないカテゴリ変数に適用すると、モデルが誤った順序関係(例:「赤」が「青」より大きいなど)を学習してしまうリスクがある。
from sklearn.preprocessing import LabelEncoder
import pandas as pd
import numpy as np
# サンプルデータフレーム
df_cat = pd.DataFrame({
'Color': ['Red', 'Green', 'Blue', 'Red', 'Green'],
'Size': ['Small', 'Medium', 'Large', 'Small', 'Medium'],
'Rating': ['Good', 'Excellent', 'Poor', 'Good', 'Excellent'] # 順序あり
})
print("--- 元のデータフレーム (カテゴリ変数) ---")
print(df_cat)
# LabelEncoderの初期化
le = LabelEncoder()
# 'Rating'列に適用 (順序ありと仮定)
df_cat['Rating_Encoded'] = le.fit_transform(df_cat['Rating'])
print("\n--- 'Rating'列をラベルエンコーディング ---")
print(df_cat[['Rating', 'Rating_Encoded']])
print("対応関係:", list(le.classes_)) # エンコードされた値と元のカテゴリの対応関係
# 注意: 'Color'のような順序なし変数に適用すると誤解を招く可能性
df_cat['Color_Encoded_Bad'] = le.fit_transform(df_cat['Color'])
print("\n--- 'Color'列にラベルエンコーディング (非推奨) ---")
print(df_cat[['Color', 'Color_Encoded_Bad']])
print("対応関係:", list(le.classes_)) # 文字列のアルファベット順になる
LabelEncoder
は、fit_transform
で学習と変換を一度に行います。変換後の値と元のカテゴリの対応関係はle.classes_
で確認できます。Poor: 0, Good: 1, Excellent: 2
のように順序付けられれば問題ありませんが、Blue: 0, Green: 1, Red: 2
のようにアルファベット順になるため、順序に意味のないデータでは避けるべきです。
2.2. ワンホットエンコーディング (One-Hot Encoding)
カテゴリ変数を、各カテゴリに対応するバイナリ(0または1)の新しい列に変換する手法です。
- 適用例: 順序に意味のないカテゴリ変数(例:性別、色、都市)
- メリット: モデルがカテゴリ間に誤った順序関係を学習するのを防ぐ。
- デメリット: カテゴリの数が非常に多い場合、生成される列の数も多くなり、メモリ使用量の増加や次元の呪い(モデルの複雑化、過学習のリスク)につながる。
# 'Color'列にワンホットエンコーディングを適用
# pandasのget_dummiesが便利
df_onehot = pd.get_dummies(df_cat, columns=['Color'], prefix='Color')
print("\n--- 'Color'列をワンホットエンコーディング ---")
print(df_onehot[['Color', 'Color_Blue', 'Color_Green', 'Color_Red']])
# prefixを指定しない場合
# df_onehot_no_prefix = pd.get_dummies(df_cat, columns=['Color'])
# print("\n--- Prefixなしの場合 ---")
# print(df_onehot_no_prefix[['Color', 'Blue', 'Green', 'Red']])
# ドロップファースト (Drop First)
# 複数カテゴリの場合、N-1個のダミー変数で表現可能。
# これにより多重共線性を防ぐことができるが、一部のモデルでは不要な場合もある。
df_onehot_dropfirst = pd.get_dummies(df_cat, columns=['Color'], drop_first=True)
print("\n--- 'Color'列をワンホットエンコーディング (drop_first=True) ---")
print(df_onehot_dropfirst[['Color', 'Color_Green', 'Color_Red']]) # Color_Blue列が削除される
drop_first=True
は、N個のカテゴリがある場合、N-1個のダミー変数を作成します。これにより、多重共線性(特徴量間の高い相関)を防ぐ効果がありますが、モデルの種類によっては必須ではありません。
2.3. その他のエンコーディング手法(応用)
カテゴリ変数の数が多い場合や、特定のモデルでより良い結果を得たい場合に使われます。
-
頻度エンコーディング (Frequency Encoding) / カウントエンコーディング (Count Encoding):
- カテゴリの出現頻度やカウント数を数値として割り当てる。
- メリット: シンプル、次元削減、モデルが頻度情報を利用できる。
- デメリット: 異なるカテゴリでも同じ頻度を持つ場合、区別できなくなる。
-
ターゲットエンコーディング (Target Encoding) / 平均エンコーディング (Mean Encoding):
- 各カテゴリの目的変数(予測したい値)の平均値(分類問題では確率)を数値として割り当てる。
- メリット: モデルがカテゴリと目的変数の関係性を直接学習できる。次元削減。
- デメリット: データリーク(学習データで計算した情報がテストデータに漏れること)のリスクがあるため、交差検証と組み合わせるなど慎重な実装が必要。過学習しやすい。
# ターゲットエンコーディングの例 (簡易版、交差検証は考慮せず)
# 実際のプロジェクトでは、データリーク対策のためCVFoldやSmoothingを考慮する
df_target_enc = pd.DataFrame({
'City': ['Tokyo', 'Osaka', 'Tokyo', 'Kyoto', 'Osaka', 'Tokyo'],
'Price': [100, 150, 110, 200, 140, 105] # 目的変数 (数値)
})
# 各都市の平均価格を計算
city_avg_price = df_target_enc.groupby('City')['Price'].mean().to_dict()
print("\n--- 都市ごとの平均価格 ---")
print(city_avg_price)
# ターゲットエンコーディングを適用
df_target_enc['City_Encoded_Price'] = df_target_enc['City'].map(city_avg_price)
print("\n--- ターゲットエンコーディング適用後 ---")
print(df_target_enc)
ターゲットエンコーディングは非常に強力ですが、データリーク対策が甘いと過学習の原因になりやすいので注意が必要です。実運用では、カテゴリカル変数を扱う専門ライブラリ(例: category_encoders
)の利用も検討すると良いでしょう。
3. 数値変数の前処理テクニック
数値変数はそのまま使えることが多いですが、モデルの性能を向上させるために以下の前処理が有効です。
3.1. スケーリング (Scaling)
特徴量のスケール(範囲)が大きく異なる場合、一部の機械学習モデル(特に距離ベースのモデル:K近傍法、SVM、主成分分析など、および勾配降下法を用いるモデル:線形回帰、ロジスティック回帰、ニューラルネットワークなど)の学習効率や精度に悪影響を与えることがあります。
-
Min-Maxスケーリング (Normalization): データを特定の範囲(通常は0から1)にスケーリングします。
$X_{normalized} = \frac{X - X_{min}}{X_{max} - X_{min}}$
- メリット: データ範囲が固定されるため、ニューラルネットワークの活性化関数などで効果的。
- デメリット: 外れ値に非常に敏感で、外れ値があると他のデータが狭い範囲に圧縮されてしまう。
-
標準化 (Standardization): データを平均0、標準偏差1になるように変換します。
$X_{standardized} = \frac{X - \mu}{\sigma}$
- メリット: 外れ値の影響を受けにくい。正規分布に従うデータに有効。
- デメリット: データ範囲が固定されない。
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns
# サンプルデータ (年齢と給与など、スケールが異なる変数)
np.random.seed(42)
df_num = pd.DataFrame({
'Age': np.random.randint(18, 60, 100),
'Salary': np.random.randint(30000, 150000, 100),
'Experience': np.random.randint(0, 30, 100)
})
print("--- スケーリング前のデータ概要 ---")
print(df_num.describe())
# データの分布を可視化 (スケーリング前)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
sns.histplot(df_num['Age'], kde=True, ax=axes[0]).set_title('Original Age')
sns.histplot(df_num['Salary'], kde=True, ax=axes[1]).set_title('Original Salary')
sns.histplot(df_num['Experience'], kde=True, ax=axes[2]).set_title('Original Experience')
plt.tight_layout()
plt.suptitle("Original Numerical Feature Distributions", y=1.02, fontsize=16)
plt.show()
# Min-Maxスケーリングの適用
scaler_minmax = MinMaxScaler()
df_minmax_scaled = pd.DataFrame(scaler_minmax.fit_transform(df_num), columns=df_num.columns)
print("\n--- Min-Maxスケーリング後のデータ概要 ---")
print(df_minmax_scaled.describe())
# 標準化の適用
scaler_standard = StandardScaler()
df_standard_scaled = pd.DataFrame(scaler_standard.fit_transform(df_num), columns=df_num.columns)
print("\n--- 標準化後のデータ概要 ---")
print(df_standard_scaled.describe())
# スケーリング後のデータの分布を可視化
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
sns.histplot(df_minmax_scaled['Age'], kde=True, ax=axes[0, 0]).set_title('Min-Max Scaled Age (0-1)')
sns.histplot(df_minmax_scaled['Salary'], kde=True, ax=axes[0, 1]).set_title('Min-Max Scaled Salary (0-1)')
sns.histplot(df_minmax_scaled['Experience'], kde=True, ax=axes[0, 2]).set_title('Min-Max Scaled Exp (0-1)')
sns.histplot(df_standard_scaled['Age'], kde=True, ax=axes[1, 0]).set_title('Standard Scaled Age (Mean 0, Std 1)')
sns.histplot(df_standard_scaled['Salary'], kde=True, ax=axes[1, 1]).set_title('Standard Scaled Salary (Mean 0, Std 1)')
sns.histplot(df_standard_scaled['Experience'], kde=True, ax=axes[1, 2]).set_title('Standard Scaled Exp (Mean 0, Std 1)')
plt.tight_layout()
plt.suptitle("Scaled Numerical Feature Distributions", y=1.02, fontsize=16)
plt.show()
グラフを見ると、Min-Maxスケーリングはデータを0〜1の範囲に収めていますが、元の分布の形状は維持されています。標準化は、データの中心を0に、スケールを1に揃えていますが、これも元の分布の形状を維持しています。どちらのスケーリングを選ぶかは、利用するAIモデルやデータの特性によって異なります。
3.2. 対数変換 (Log Transformation)
データが右に大きく偏った(右に長い裾を持つ)分布を持つ場合、対数変換を行うことで分布を正規分布に近づけ、モデルの性能を改善できることがあります。
- 適用例: 売上、人口、価格など、スケールが大きく、非対称な分布を持つデータ。
-
注意: 値が0や負の数を含む場合は、
log1p
(log(1+x)) を使用するか、適切なオフセットを加える必要があります。
# 右に偏った分布を持つデータ (例: 収入)
income_data = np.random.exponential(scale=50000, size=1000)
df_income = pd.DataFrame({'Income': income_data})
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
sns.histplot(df_income['Income'], kde=True, bins=50)
plt.title("Original Income Distribution (Right Skewed)")
plt.xlabel("Income")
plt.ylabel("Frequency")
# 対数変換
df_income['Log_Income'] = np.log1p(df_income['Income']) # log(1+x)を使用
plt.subplot(1, 2, 2)
sns.histplot(df_income['Log_Income'], kde=True, bins=50)
plt.title("Log-transformed Income Distribution")
plt.xlabel("Log(1+Income)")
plt.ylabel("Frequency")
plt.tight_layout()
plt.show()
対数変換によって、右に偏っていた分布がより正規分布に近づき、モデルが学習しやすくなります。
3.3. ビニング (Binning) / 離散化 (Discretization)
連続する数値変数を、いくつかの区間(ビン)に分割し、カテゴリ変数として扱う手法です。
- 適用例: 年齢を「若年層」「中年層」「高齢層」などに分ける、価格帯を「低価格」「中価格」「高価格」に分けるなど。
- メリット: ノイズの低減、非線形な関係を線形モデルで扱えるようになる、外れ値の影響を軽減。
- デメリット: 情報の一部が失われる可能性。ビンの数が多すぎると過学習のリスク、少なすぎると情報不足。
# 年齢データをビニング
bins = [0, 20, 40, 60, 80, 100] # 年齢の区間を定義
labels = ['Youth', 'Young Adult', 'Adult', 'Senior', 'Elderly'] # 各区間のラベル
df_num['Age_Group'] = pd.cut(df_num['Age'], bins=bins, labels=labels, right=False) # right=Falseで左閉区間
print("\n--- 年齢のビニング ---")
print(df_num[['Age', 'Age_Group']].head())
# 可視化で確認
plt.figure(figsize=(7, 5))
sns.countplot(data=df_num, x='Age_Group', order=labels)
plt.title("Age Group Distribution")
plt.xlabel("Age Group")
plt.ylabel("Count")
plt.show()
pd.cut()
は、指定した区間でデータをビンに分割します。pd.qcut()
を使うと、データの度数に基づいて等しい数の要素が各ビンに含まれるように分割することもできます。
4. 前処理パイプラインの構築 (sklearn.pipeline
)
複数の前処理ステップを順番に適用する場合、scikit-learn
のPipeline
を使用すると、コードを整理し、保守性を高めることができます。これは、特に交差検証を行う際に、データリークを防ぐためにも重要です。
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# 総合的なサンプルデータ
data_full = {
'Gender': ['Male', 'Female', 'Male', 'Female', 'Male', 'Female', 'Male', 'Female'],
'Age': [25, 30, 22, 35, 40, 28, 45, 32],
'City': ['Tokyo', 'Osaka', 'Tokyo', 'Kyoto', 'Osaka', 'Tokyo', 'Kyoto', 'Osaka'],
'Income': [50000, 70000, 45000, 80000, 90000, 60000, 100000, 75000],
'Purchased': [0, 1, 0, 1, 1, 0, 1, 0] # 目的変数 (購入したか否か)
}
df_full = pd.DataFrame(data_full)
# 特徴量と目的変数の分離
X = df_full.drop('Purchased', axis=1)
y = df_full['Purchased']
# 訓練データとテストデータに分割 (データリークを防ぐため前処理前に分割)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 前処理ステップの定義
# 数値特徴量に対する変換 (StandardScaler)
numeric_features = ['Age', 'Income']
numeric_transformer = StandardScaler()
# カテゴリ特徴量に対する変換 (OneHotEncoder)
categorical_features = ['Gender', 'City']
categorical_transformer = OneHotEncoder(handle_unknown='ignore') # 未知のカテゴリは無視
# ColumnTransformerを使って、異なる列に異なる変換を適用
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# パイプラインの構築: 前処理 -> モデル学習
model_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', LogisticRegression(random_state=42)) # ロジスティック回帰モデル
])
# パイプラインで学習
model_pipeline.fit(X_train, y_train)
# テストデータで予測
y_pred = model_pipeline.predict(X_test)
print("\n--- パイプラインを使った予測結果 (テストデータ) ---")
print("予測値:", y_pred)
print("実測値:", y_test.values)
# 各変換器が学習した内容を確認 (例: OneHotEncoderのカテゴリ)
# print("\nOneHotEncoderの学習したカテゴリ:", model_pipeline.named_steps['preprocessor'].named_transformers_['cat'].categories_)
ColumnTransformer
は、複数の変換器を並行して実行し、それぞれの変換結果を結合します。これにより、数値列とカテゴリ列に異なる前処理を適用できます。Pipeline
に含めることで、訓練データに対してfit_transform
、テストデータに対してtransform
を自動的に適用し、データリークを防ぎながら堅牢なモデル構築プロセスを実現します。
5. まとめと次へのステップ
本日は、AI学習ロードマップの4日目として、カテゴリ変数と数値変数のそれぞれの特性に応じた前処理テクニックを学びました。
- AIモデルはほとんどが数値データを期待するため、カテゴリ変数は何らかの形で数値に変換する必要があります。
- ラベルエンコーディングは順序ありカテゴリ向けですが、安易な使用は避けるべきです。
- ワンホットエンコーディングは順序なしカテゴリの標準的な手法で、多重共線性のリスクに注意しつつ利用します。
- ターゲットエンコーディングのような応用的な手法は強力ですが、データリーク対策が重要です。
- 数値変数は直接利用できますが、スケーリング(Min-Maxスケーリング、標準化)、対数変換、ビニングといった前処理によって、モデルの学習効率や精度を向上させることができます。
- 複数の前処理ステップを効率的かつ安全に管理するために、
scikit-learn
のPipeline
とColumnTransformer
を活用することが推奨されます。これにより、 データリーク を防ぎ、コードの保守性を高めることができます。
これらの前処理テクニックは、AIモデルの性能を最大化するための基盤となるスキルです。実際のプロジェクトでは、データの性質、使用するモデル、目的変数の種類によって最適な前処理手法は異なります。試行錯誤を通じて、最も適切な方法を見つけ出すことが重要です。
明日は、これらの前処理済みデータを使って、いよいよ機械学習の基本的なモデルである 「教師あり学習」と「教師なし学習」 の概念に踏み込んでいきます。今日までの知識が、AIの「脳」がどのように機能するのかを理解する上で役立つでしょう。
それでは、また明日!