🐈

Kaggleメダル獲得への挑戦 Vol.4 ~Target Encoding

に公開

はじめに

この記事は30歳未経験からエンジニアに転身したド文系の僕が、機械学習エンジニアになるためにKaggleに挑戦しながらスキルを身につけていく過程をリアルタイムで発信していく連載記事となります。

毎週末、その週に僕自身が学んだ内容を発信していきます。
学びながら発信していきますので、場合によっては間違った情報や、誤って理解してしまっているものもあるかもしれません。
その際はぜひ温かくご指摘をいただけるとうれしいです!
ド文系でも本気で取り組めば結果を出せるんだという姿を見せられるように、日々頑張っていきます!

Vol.1はこちら

2025年5月19日〜5月25日までの学び

注)Kaggleは英語で構成されているため、その用語に慣れるためにも、主要な用語は英語のまま学ぶこととしています。
なので以下の記事でもところどころ英語のまま記載していますが、それは上記の意図があって敢えてそう表記しているものなので、その点だけご理解ください。

今週は「Target Encoding」中心に学んできたので、Target Encodingについて解説していきます。

Target Encoding(目的変数エンコーディング)とは何か?

カテゴリ変数(文字列やラベルなど)を目的変数の平均値で数値化する方法。
たとえば「職業」や「地域」などの文字情報をそれぞれに対応する「売上」「年収」などの平均値に変換する。

なぜTarget Encodingが必要か?

機械学習モデル(特に線形モデルや決定木系モデル)は文字データをそのまま扱えないため、何らかの数値化(エンコーディング)が必要となるため。

具体例として職業と年収に関する以下のようなデータがあったとします。

このデータでは職業欄が「教師」や「医者」といった文字列になっているため、このままでは機械学習モデルに使用できない。
そこで、この職業データをTarget Encodingの手法で以下のように数値に変換して、カテゴリごとのTarget Value(年収)の平均を計算する。

そして変換した値で元々の職業欄の値を置き換える。

このように、文字情報がモデルが扱える数値に変換され、しかも「職業」と「年収」の関係性をそのまま保持している。
→つまり機械学習に使用できる特徴量が増加したといえる(教師や医者などの情報のままでは機械学習に使用できなかったが、意味のある数値化をすることで学習に使えるデータとなった)。

どんな時に使用するか?

・カテゴリの種類が多いとき(High Cardinality*)
カテゴリを数値に変換する手法としてOneHotEncodingがあるが、カテゴリの種類が多いケースでOneHotEncodingを使用すると列数が増えすぎて高次元化してしまう。

・カテゴリとターゲットに意味のある関連があるとき
例えば今回の例の職業と年収や、地域と売上など、そのカテゴリと予測したいターゲット数値の間に一定の関連性がある際に使用する。

Data Leakage(データリーク)

このTarget Encodingを使用する際は、Data Leakageが発生しないように注意する必要がある。

Data Leakageとは、モデルの学習時に、本来は知っていてはいけない情報(未来の情報や目的変数の情報)を使ってしまうことをいう。
より詳しくいうと、Target Encodingは目的変数(正解データ)を使って数値を作るため、もし「学習データの目的変数そのもの」を使って平均を出してしまうとモデルが“未来の正解”をチラ見して学習することになる。

「学習データの目的変数そのものを使って平均を出してしまう」という点について以下の簡単なデータを使って解説をする。

Data Leakageあり(よくないEncoding方法)は以下の通り
データ1(地域A)をエンコードする際に、「地域Aの売上平均 = (100 + 150) / 2 = 125」とする。
→つまり、データ1自身の「売上100」も平均に含めている
これは自分(ID:1)の正解(売上:100)を使って自分を予測している
→これがData Leakageしている状態を指す

ではData Leakageしないやり方とはどういった手法か?
方法1:Leave-One-Out(1つ除く)方式
・データ1(地域A)のターゲットエンコーディング値に地域Aの他のデータだけを使う
→その結果、平均=150(データ2のみ使用)となる

方法2:K-Fold Target Encoding
・学習データを例えば5分割し「自分が含まれていないfold」のデータだけを使って平均を計算する

今回の例だとデータが少なすぎて具体的解説がここではできないが、Data Leakageを防止するためには、自らのデータ(目的変数の値)を学習に使用しないように設計することが重要。

なぜData Leakageが問題なのか?

本来モデルは、ある特徴量(地域Aなど)から目的変数(売上)を予測する力を学ばなければならない。
しかし自分の正解を使ってカテゴリの平均を出すと、そのデータが「正解を知ってる」状態で学習されてしまうため、学習データに対しては精度が異常に高くなる(ズルしてるから)。
しかしその一方で、未知のテストデータにはまったく使えなくなる。

つまりData Leakageが発生すると、学習時は精度が異常に良くなるが本番環境やテストでは全く当たらなくなる
→ この状態は「モデルが過学習して使い物にならなくなっている」と表現される

Data Leakage対策

・K-Fold Target Encoding
→学習データを分割して、他のFoldの平均でエンコードを実施することでdata Leakageを防止する

・Smoothing(平準化)
→カテゴリのデータ数が少ないときに全体平均とのバランスを取る

・ノイズを加える
→過学習を防ぐために少しランダム性を入れる=より本番のデータ・状況に近づける

Target Encodingまとめ

・目的
→カテゴリ変数をターゲットの平均で数値化し、意味を保持したままモデルに渡す

・メリット
→高精度・次元圧縮・関係性を活かした変換

・デメリット
→Data Leakage・過学習リスク

・主な対策
→K-Fold、平滑化、ノイズなど

具体的なPythonコード

Data Leakageあり・なし比較

from collections import defaultdict
import pandas as pd

# サンプルデータ
df = pd.DataFrame({
    'region': ['A', 'A', 'A', 'B', 'B', 'C'],
    'sales':  [100, 150, 130, 200, 220, 300]
})


# Leave-One-Out Target Encoding
def leave_one_out_target_encoding(X, y, col):
    # 出力用エンコーディング結果
    encoded_values = []
    # 全体のグループ平均用
    value_sums = defaultdict(float)
    value_counts = defaultdict(int)

    # まず、全体の合計と件数を集計
    for val, target in zip(X[col], y):
        value_sums[val] += target
        value_counts[val] += 1

    # 各行に対して「自分を除いた平均」を計算
    for val, target in zip(X[col], y):
        sum_ = value_sums[val] - target
        count = value_counts[val] - 1
        if count == 0:
            encoded = value_sums[val] / value_counts[val]  # fallback: 通常平均
        else:
            encoded = sum_ / count
        encoded_values.append(encoded)

    return encoded_values

# Data Leakageありのパターン
# 地域ごとの売上平均をそのまま使う(自分自身を含めている)
mean_sales_leak = df.groupby('region')['sales'].mean()

# region列を平均売上に置換
df['region_te_leak'] = df['region'].map(mean_sales_leak)


# Data Leakageなしのパターン
# エンコード実施
df['region_te_no_leak'] = leave_one_out_target_encoding(df, df['sales'], 'region')

# 両者比較
print(df[['region', 'sales', 'region_te_leak', 'region_te_no_leak']])

出力結果

  region  sales  region_te_leak  region_te_no_leak
0      A    100      126.666667              140.0
1      A    150      126.666667              115.0
2      A    130      126.666667              125.0
3      B    200      210.000000              220.0
4      B    220      210.000000              200.0
5      C    300      300.000000              300.0

👆Data Leakageの有無で値に差があることが確認できる

ちなみに実際の現場ではTargetEncoderなどを使用してData Leakage防止をしたTarget Encodingを行うのが一般的とのこと
具体的なコードは以下の通り。

import pandas as pd
import numpy as np
from sklearn.model_selection import KFold
from category_encoders import TargetEncoder
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score

# サンプルデータ(カテゴリ:region、ターゲット:sales)
df = pd.DataFrame({
    'region': ['A', 'A', 'A', 'B', 'B', 'C', 'C', 'C', 'C', 'D'],
    'sales':  [100, 150, 130, 200, 220, 300, 310, 290, 305, 180]
})

# 特徴量とターゲット
X = df[['region']]
y = df['sales']

# KFoldとTargetEncoderを組み合わせた処理
def kfold_target_encoding(X, y, col, n_splits=5, smoothing=5):

    # K-Fold交差検証の設定
    # n_splits: データを何分割するか(5なら5分割)
    # shuffle: データをシャッフルするか(Trueならデータをランダムにシャッフルしてから分割)
    # random_state: 乱数シード(再現性のために指定)
    kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)

    # 最終的なエンコード結果を格納する空のSeriesを作成
    # index=X.index: 元のデータフレームの行番号と一致させる(後でmergeしやすい)
    # dtype=float: エンコード値は少数(ターゲットの平均値)になるのでfloat型で設定
    # K-Foldを使ったTarget Encodingでは各foldごとにバラバラにエンコードされた値が返ってくる
    # このencoded_colにfoldごとに得られた結果を一括して順番通りに格納することで、最終的に「情報リークのない安全なカテゴリ列」を作成する
    encoded_col = pd.Series(index=X.index, dtype=float)
    # print(X)
    # print(y)

    # kf.split(X)で、全データをtrain_idx(学習用)とvalid_idx(検証用)に分割
    # これをfoldの数だけ繰り返す
    for train_idx, valid_idx in kf.split(X):
        print(f"Train indices: {train_idx}, Valid indices: {valid_idx}")
        
        # X_train:このfoldでの学習用カテゴリ変数(この場合はregion列)
        # y_train:このfoldでの学習用ターゲット変数(この場合はsales列)
        # X_valid:このfoldでの検証用カテゴリ変数(エンコード対象)
        X_train, y_train = X.iloc[train_idx], y.iloc[train_idx]
        X_valid = X.iloc[valid_idx]

        # TargetEncoderを初期化
        # cols: エンコード対象の列(この場合はregion列)
        # smoothing: スムージングのパラメータ(デフォルトは10)
        # →サンプル数が少ないカテゴリに対して「全体平均に近づける」平準化効果
        encoder = TargetEncoder(cols=[col], smoothing=smoothing)

        # 学習データで平均を計算
        # X_trainの各カテゴリに対して、y_trainの平均を計算
        # これにより、各カテゴリのターゲット変数の平均値が得られる
        encoder.fit(X_train, y_train)

        # 検証用データに変換を適用
        # X_validのカテゴリを、X_trainで学習したターゲットの変数で数値に変換
        # →この時、X_validに含まれるデータは自分自身の目的変数を使用していない(学習用データのみ使用している)
        encoded = encoder.transform(X_valid)

        # 結果をencoded_colに格納
        # valid_idxに対応数r行にエンコード済みの値を保存
        # これを各foldについて繰り返すことで全データの安全亜鉛コードが完成する
        encoded_col.iloc[valid_idx] = encoded[col].values

    return encoded_col

# 実行:情報リークのないエンコーディング
df['region_te'] = kfold_target_encoding(X, y, col='region', n_splits=5, smoothing=5)

# 確認
print(df[['region', 'sales', 'region_te']])

出力結果

Train indices: [0 2 3 4 5 6 7 9], Valid indices: [1 8]
Train indices: [1 2 3 4 6 7 8 9], Valid indices: [0 5]
Train indices: [0 1 3 4 5 6 8 9], Valid indices: [2 7]
Train indices: [0 1 2 3 5 6 7 8], Valid indices: [4 9]
Train indices: [0 1 2 4 5 7 8 9], Valid indices: [3 6]


  region  sales   region_te
0      A    100  220.914125
1      A    150  213.557054
2      A    130  218.081662
3      B    200  209.607489
4      B    220  222.618996
5      C    300  225.661540
6      C    310  212.247951
7      C    290  223.349930
8      C    305  218.954745
9      D    180  223.125000

smoothingの値の決め方smoothing は明示的なハイパーパラメータであり、モデルとデータの特性に応じてチューニングすべき項目
→smoothingの値には正解はなく、チューニング対象であると理解する

以下がsmoothingの値を決める際の参考データ
→あくまで目安あるという意識を忘れないこと!

smoothing を大きくすると、バリアンス(過学習)が減るがバイアスが増える。
smoothing を小さくすると、バイアスは減るがバリアンスが増える
バイアスとバリアンスはトレードオフの関係と言える

今週の学びは以上です。次週も頑張って学びを積み上げていきます!

未経験からエンジニア転職を目指すあなたへ

僕は今「未経験から本気でエンジニア転職を目指す人」のための新しいサポートサービスを準備中です。

もしあなたが…

  • 業務改善を通じて価値を出せるエンジニアを目指したい
  • 数字や仕組みで現場を変える力を、キャリアに活かしたい
  • これから学ぶべきステップを明確にしたい

そんな想いを少しでも持っているなら、ぜひ僕の公式LINEに登録しておいてください。
サービスに関する先行案内や最新情報を優先的にお届けします。
👉公式LINEはこちら

Discussion