はじめに
前回のpandas入門では、データの読み込み・加工・集計の基本操作を整理した。
今回はその続きとして、整形したデータを予測モデルに渡せる形に仕上げる工程、いわゆる「特徴量エンジニアリング」を扱う。
モデルにそのままデータを投げ込めばいい、と思いがちだが実際はそうでもない。
モデルの精度の大半は、特徴量の質で決まると言われており、ここへの投資が最も効果が出やすい工程でもある。
これまでのシリーズで扱ってきた小売店の売上データを引き続き例に使って整理していく。
特徴量エンジニアリングとは
「特徴量(Feature)」とは、モデルへの入力として使う変数のこと。
生データのカラムをそのまま使う場合もあるが、多くの場合は加工・変換・組み合わせによってモデルが学習しやすい形に整える必要がある。
生データ(pandas DataFrame)
↓ 特徴量エンジニアリング
モデルに渡せる特徴量セット
↓
予測モデル(LightGBM など)
↓
予測結果
この記事では以下の流れで整理する。
- データの準備(前回の復習)
- 数値特徴量の変換
- カテゴリ特徴量のエンコーディング
- 日付特徴量の展開
- 特徴量の組み合わせ(交互作用特徴量)
- 特徴量の選択
- モデルへの投入とベースライン確認
使用するサンプルデータ
前回と同じ小売店の売上データを使う。まずデータを作成して全体像を確認しておく。
import pandas as pd
import numpy as np
np.random.seed(42)
n = 500
df = pd.DataFrame({
"日付": pd.date_range("2023-01-01", periods=n, freq="D"),
"カテゴリ": np.random.choice(["食品", "飲料", "日用品", "菓子"], n),
"地域": np.random.choice(["東京", "大阪", "名古屋"], n),
"来客数": np.random.randint(80, 300, n),
"気温": np.random.uniform(0, 35, n).round(1),
"特売フラグ": np.random.choice([0, 1], n, p=[0.8, 0.2]),
"売上": np.random.randint(80, 300, n) * 1000,
})
df.head()
| 日付 | カテゴリ | 地域 | 来客数 | 気温 | 特売フラグ | 売上 |
|---|---|---|---|---|---|---|
| 2023-01-01 | 食品 | 東京 | 142 | 8.3 | 0 | 196000 |
| 2023-01-02 | 飲料 | 大阪 | 211 | 12.1 | 1 | 254000 |
| ... | ... | ... | ... | ... | ... | ... |
目標:来客数・気温・特売フラグ・カテゴリ・地域・日付情報から「売上」を予測する。
1. 数値特徴量の変換
対数変換:分布が偏っているとき
統計学入門②で整理したとおり、売上や収入のような右裾の長いデータは対数変換で正規分布に近づけることができる。
# 分布の確認
print(f"歪度(変換前): {df['売上'].skew():.3f}")
# 対数変換(log1p は log(x+1) でゼロ値も扱える)
df["売上_log"] = np.log1p(df["売上"])
print(f"歪度(変換後): {df['売上_log'].skew():.3f}")
目的変数(今回は「売上」)に対数変換を施した場合、予測結果を元のスケールに戻すには np.expm1()(log1p の逆関数)を使う。
標準化・正規化:スケールを揃える
「来客数(80〜300)」と「気温(0〜35)」のようにスケールが大きく異なる変数が混在すると、距離ベースのモデル(KNNなど)や線形モデルで影響度の大小が変数のスケールに引っ張られてしまう。
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 標準化(平均0・標準偏差1に変換)→ 線形モデル・SVMに向く
scaler_std = StandardScaler()
df["来客数_標準化"] = scaler_std.fit_transform(df[["来客数"]])
# 正規化(0〜1の範囲に変換)→ ニューラルネットワークに向く
scaler_mm = MinMaxScaler()
df["気温_正規化"] = scaler_mm.fit_transform(df[["気温"]])
LightGBMやXGBoostなどの勾配ブースティング系はスケールに影響を受けないため、標準化・正規化は不要。
線形回帰・ロジスティック回帰・KNN・SVMなどを使う場合に適用する。
ビニング(区間分割):連続値をカテゴリに変換する
気温を「低温・中温・高温」のように区間ごとに分けることで、非線形な関係をモデルが捉えやすくなることがある。
# 気温を3区分に分割
df["気温区分"] = pd.cut(
df["気温"],
bins=[0, 10, 25, 35],
labels=["低温", "中温", "高温"]
)
# 等頻度で分割(各区間のデータ件数が均等になる)
df["来客数_区分"] = pd.qcut(df["来客数"], q=4, labels=["少", "やや少", "やや多", "多"])
cut(等幅)と qcut(等頻度)の使い分けは、データの分布によって判断する。外れ値が多いデータには qcut のほうが安定しやすい。
2. カテゴリ特徴量のエンコーディング
機械学習モデルは基本的に数値しか受け取れない。「食品」「飲料」などの文字列データは数値に変換する必要がある。
ラベルエンコーディング:順序があるカテゴリに
カテゴリ間に順序関係がある(例:「低・中・高」「S・A・B・C」)場合に使う。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df["気温区分_encoded"] = le.fit_transform(df["気温区分"].astype(str))
ワンホットエンコーディング:順序がないカテゴリに
「食品」「飲料」「日用品」「菓子」のように順序のないカテゴリには、各カテゴリを0/1のフラグ列に展開するワンホットエンコーディングを使う。
# pandasのget_dummies で実装
df_encoded = pd.get_dummies(df, columns=["カテゴリ", "地域"], drop_first=True)
# drop_first=True:多重共線性を避けるために1列を削除する
print(df_encoded.filter(like="カテゴリ").head())
カテゴリ_日用品 カテゴリ_菓子 カテゴリ_飲料
0 0 0 0
1 0 0 1
2 1 0 0
カテゴリの種類数(カーディナリティ)が多い場合(例:商品ID、郵便番号など)にワンホットエンコーディングを使うと列数が爆発する。
その場合は後述のターゲットエンコーディングを検討する。
ターゲットエンコーディング:カーディナリティが高いカテゴリに
各カテゴリを「そのカテゴリに属するデータの目的変数の平均値」で置き換える手法。
# カテゴリ別の平均売上でエンコード
target_mean = df.groupby("カテゴリ")["売上"].mean()
df["カテゴリ_target_enc"] = df["カテゴリ"].map(target_mean)
print(target_mean)
ターゲットエンコーディングはリーク(データの漏洩)が起きやすい。
実務ではクロスバリデーションのfold内で計算するなど工夫が必要。今回は概念の理解に留める。
3. 日付特徴量の展開
日付列は「2023-01-15」という値のままではモデルに渡せない。含まれる時間的な情報を個別の特徴量として展開する。
df["日付"] = pd.to_datetime(df["日付"])
# 基本の展開
df["年"] = df["日付"].dt.year
df["月"] = df["日付"].dt.month
df["日"] = df["日付"].dt.day
df["曜日"] = df["日付"].dt.dayofweek # 0=月曜 〜 6=日曜
df["週番号"] = df["日付"].dt.isocalendar().week.astype(int)
# 派生フラグ
df["週末フラグ"] = (df["曜日"] >= 5).astype(int) # 土日を1に
df["月初フラグ"] = (df["日"] <= 3).astype(int)
df["月末フラグ"] = (df["日"] >= 28).astype(int)
# 季節(四半期)
df["四半期"] = df["日付"].dt.quarter
週末フラグや月末フラグのような「人の行動パターンを反映した特徴量」は、売上予測のような実務タスクで効きやすい。
4. 特徴量の組み合わせ(交互作用特徴量)
2つの特徴量を組み合わせることで、単独では表現できなかったパターンをモデルに伝えられることがある。
# 来客数 × 特売フラグ:特売時の来客増加の相乗効果を表す
df["来客数×特売"] = df["来客数"] * df["特売フラグ"]
# 来客数 ÷ 気温:気温あたりの来客効率
df["来客数/気温"] = df["来客数"] / (df["気温"] + 1) # ゼロ除算を避けるため+1
# 移動平均:直近7日間の売上トレンドを特徴量にする(時系列データに有効)
df = df.sort_values("日付").reset_index(drop=True)
df["売上_7日移動平均"] = df["売上"].rolling(window=7, min_periods=1).mean()
交互作用特徴量はEDAで見つけた「この2変数は一緒に動いていそう」という仮説を形にするもの。
闇雲に作るより、ドメイン知識・EDAの気づきをもとに作るほうが精度が上がりやすい。
5. 特徴量の選択
特徴量が増えすぎると、モデルが過学習しやすくなったり、学習が遅くなったりする。
不要な特徴量を絞り込む方法を整理する。
相関係数による確認
import matplotlib.pyplot as plt
import seaborn as sns
# 数値列の相関行列
num_cols = ["来客数", "気温", "特売フラグ", "売上", "週末フラグ", "来客数×特売"]
corr = df[num_cols].corr()
plt.figure(figsize=(8, 6))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", center=0)
plt.title("特徴量の相関行列")
plt.tight_layout()
plt.show()
目的変数(売上)との相関が低い特徴量は予測に貢献しにくい。
また特徴量どうしの相関が0.9以上の場合は多重共線性を招くため、片方を除外する。
分散が低い特徴量の除外
ほぼ同じ値しか取らない特徴量(例:99%が0のフラグ列)はモデルへの貢献が小さい。
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.01)
# 分散が0.01未満の列を自動で除外(数値列のみ対象)
6. モデルへの投入とベースライン確認
特徴量の準備が整ったら、実際にモデルに投入して精度を確認する。
今回はシリーズ通じて「とりあえずこれ」と言われてきたLightGBMを使う。
from lightgbm import LGBMRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
# 特徴量と目的変数を定義
feature_cols = [
"来客数", "気温", "特売フラグ",
"月", "曜日", "週末フラグ", "四半期",
"来客数×特売", "売上_7日移動平均",
"カテゴリ_日用品", "カテゴリ_菓子", "カテゴリ_飲料",
"地域_大阪", "地域_名古屋",
]
X = df_encoded[feature_cols]
y = df_encoded["売上"]
# 学習データとテストデータに分割(時系列なので末尾20%をテストに)
split_idx = int(len(df) * 0.8)
X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:]
y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]
# モデルの学習
model = LGBMRegressor(n_estimators=300, learning_rate=0.05, random_state=42)
model.fit(X_train, y_train)
# 予測と評価
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"MAE(平均絶対誤差): {mae:,.0f} 円")
print(f"R²スコア: {r2:.3f}")
特徴量重要度の確認
モデルがどの特徴量を重視したかを確認することで、次の特徴量改善のヒントが得られる。
import pandas as pd
import matplotlib.pyplot as plt
importance = pd.Series(
model.feature_importances_,
index=feature_cols
).sort_values(ascending=False)
plt.figure(figsize=(8, 5))
importance.plot(kind="barh")
plt.title("特徴量重要度")
plt.xlabel("重要度スコア")
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
重要度が高い特徴量に対してさらに細かい加工を試したり、重要度が低い特徴量を削除して再学習するといった試行錯誤がここから始まる。
特徴量重要度はLightGBMが各特徴量を何回分岐に使ったかを集計したもの(デフォルトは split)。
importance_type="gain" にすると情報利得ベースになり、より信頼性が高いとされる。
特徴量エンジニアリングの全体まとめ
| 種類 | 手法 | 主なユースケース |
|---|---|---|
| 数値変換 | 対数変換 np.log1p()
|
右裾が長い分布を補正 |
| 数値変換 | 標準化 StandardScaler
|
線形モデル・SVMのスケール統一 |
| 数値変換 | 正規化 MinMaxScaler
|
ニューラルネット向け |
| 数値変換 | ビニング pd.cut / qcut
|
非線形パターンの補捉 |
| カテゴリ | ワンホット get_dummies
|
順序なしカテゴリ(種類数が少ない) |
| カテゴリ | ラベルエンコード | 順序ありカテゴリ |
| カテゴリ | ターゲットエンコード | 種類数が多いカテゴリ |
| 日付 | 年・月・曜日・フラグ展開 | 時系列・季節性の反映 |
| 組み合わせ | 交互作用特徴量・比率 | ドメイン知識ベースの仮説 |
| 組み合わせ | 移動平均・ラグ特徴量 | 時系列トレンドの反映 |
| 選択 | 相関行列・分散フィルタ | 不要特徴量の除外 |
まとめ
特徴量エンジニアリングは、EDAで得た「気づき」と統計学で整理した「分布の知識」を組み合わせて、モデルが学習しやすい形にデータを仕上げる工程である。
ここまでのシリーズの流れを振り返ると、以下のように繋がっている。
問題設定(第1回)
→ 可視化・EDA(第2〜3回)でデータの傾向を把握
→ 統計学①②で数値的な根拠を整理
→ pandas入門でデータを加工・集計
→ 特徴量エンジニアリング(今回)でモデルに渡す形に仕上げる
→ 次回:予測モデルの構築・チューニング
次回はLightGBMを使った予測モデルの構築と、交差検証・ハイパーパラメータチューニングまでを整理していく予定。