はじめに
カテゴリ変数の定番エンコードに target encoding(平均エンコード) があります。カテゴリを「そのカテゴリでの目的変数の平均」で置き換える方法です。ただ、素朴にやると目的変数のリークを起こし、特に高カーディナリティ(カテゴリの種類が多い)ほど深刻になります。
CatBoostはカテゴリ列をcat_featuresに渡すだけで扱えますが、その内部で使われているのが Ordered Target Statistics(順序付きターゲット統計、以下 ordered TS) で、これがリークを自動的に防いでいます。この記事では、素朴なtarget encodingがどうリークするかを実測し、ordered TSがそれを防ぐことを確かめます。catboost==1.2.10を使います。
target encodingとそのリーク
素朴なtarget encodingは、カテゴリvを「訓練データでvだった行の目的変数の平均」に置き換えます。小さなデータで見てみます。目的変数yはカテゴリと無関係なノイズにしてあり(本来カテゴリからは当てられないはず)、各カテゴリは3行ずつです。比較のため、自分の行を除いた平均(leave-one-out、正直版)も並べます。
import pandas as pd
from sklearn.metrics import roc_auc_score
df = pd.DataFrame({
"cat": ["A","A","A","B","B","B","C","C","C","D","D","D"],
"y": [1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1],
})
g = df.groupby("cat")["y"]
csum, ccnt = g.transform("sum"), g.transform("count")
df["naive_te"] = csum / ccnt # 自分の行を含む平均
df["loo_te"] = (csum - df["y"]) / (ccnt - 1) # 自分の行を除いた平均(正直版)
print(df)
cat y naive_te loo_te
0 A 1 0.667 0.5
1 A 1 0.667 0.5
2 A 0 0.667 1.0
3 B 0 0.333 0.5
4 B 1 0.333 0.0
5 B 0 0.333 0.5
6 C 1 0.333 0.0
7 C 0 0.333 0.5
8 C 0 0.333 0.5
9 D 1 1.000 1.0
10 D 1 1.000 1.0
11 D 1 1.000 1.0
naive_teはカテゴリ平均に自分の行の答えを含んでいます。1件だけのカテゴリなら平均=自分の答えで完全に漏れますが、この例のように複数件あっても漏れます。符号化した値をそのまま予測値として訓練データでAUCを測ると、yはノイズなのに0.8まで出ます。
roc_auc_score(df["y"], df["naive_te"]) # 0.80 ← ノイズなのに当たって見える
roc_auc_score(df["y"], df["loo_te"]) # 0.50 ← 自分の行を除くと消える
自分の行を除いたloo_teでは0.5(=当てずっぽう)に戻ります。この差の0.3が、符号化に混入した自分の答え=リークです。カーディナリティが高いほど1カテゴリあたりの件数が減り、この漏れは強くなります。
リークを実測する
リークだけを取り出すため、**目的変数を純粋なノイズ(カテゴリと無関係な0/1)**にします。正しく符号化できていれば、検証AUCはどう頑張っても0.5のはずです。カーディナリティを変えながら、素朴なtarget encodingとCatBoost(ordered TS)の訓練AUC・検証AUCを測ります(8シード平均、N=6000)。

naive TE CatBoost (ordered TS)
cardinality train valid train valid
10 0.530 0.502 0.515 0.509
100 0.603 0.500 0.524 0.501
1000 0.819 0.499 0.536 0.498
3000 0.945 0.504 0.588 0.493
素朴なtarget encodingは、カーディナリティが上がると訓練AUCが0.945まで膨れ上がります。目的変数はただのノイズなのに、訓練上は「よく当たっている」ように見えるわけです。しかし検証AUCは一貫して0.5で、この差はすべてリークです。一方CatBoostは訓練AUCの膨張が小さく(最大でも0.588)、検証AUCは正しく0.5付近にとどまります。
Ordered Target Statistics の仕組み
CatBoostはリークを防ぐために、データをランダムな順序に並べ替え、各行を「その行より前に来た行だけ」で符号化します。自分自身とそれ以降の行を使わないので、答えが漏れません。
TS(row_i) = (前の行のうち同カテゴリの目的変数の和 + a * prior) / (前の行のうち同カテゴリの件数 + a)
aは平滑化の強さ、priorは全体平均です。時系列の「過去だけで集計する」やり方をランダム順序で行うイメージで、K-fold target encoding(out-of-foldで符号化する手動テク)を自動化したものと言えます。cat_featuresに列を渡すと、この処理が内部で走ります。
from catboost import CatBoostClassifier
model = CatBoostClassifier(iterations=300, verbose=0)
model.fit(X_train, y_train, cat_features=["cat"]) # ← ordered TS が内部で使われる
リークの実害
純ノイズの例では検証AUCが0.5のままなので「訓練で騙されるだけ」に見えますが、本物のシグナルがある場面ではリークが検証性能を実際に下げます。数値特徴(本物のシグナル)に、高カーディナリティのノイズカテゴリを1本足したデータで比べます。

手法 検証AUC
数値特徴のみ(天井) 0.793
素朴なTE + 数値 0.597 (訓練AUCは 0.990)
K-fold TE + 数値 0.782
CatBoost native(ordered TS) 0.807
素朴なtarget encodingは訓練AUC 0.990と絶好調に見えるのに、検証AUCは0.597へ崩壊しました。リークしたノイズカテゴリにモデルが飛びついて過学習し、本来0.793あった性能を大きく損ないます。K-fold TEを正しく組めば0.782と天井近くを保てますが、手間がかかります。CatBoostはcat_featuresに渡すだけで0.807と天井を維持し、手動のK-foldと同等以上の結果を無調整で得られました。
注意点
- ordered TSでもリークが完全にゼロになるわけではありません。純ノイズ・高カーディナリティでは訓練AUCが0.588まで上がりました(素朴版の0.945よりはるかに小さい)。検証性能は保たれます。
- 素朴なtarget encodingが必ず悪いわけではなく、K-fold(out-of-fold)で正しく組めばリークは防げます。CatBoostの利点は、それを
cat_featuresに渡すだけで自動でやってくれることです。 - 平滑化の強さは基本的に自動ですが、必要なら関連パラメータ(例:
nan_modeやCTR系の設定)で調整できます。 - ここでの実測はカテゴリが目的変数と無関係なノイズの場合です。カテゴリに本物の情報があるときは、ordered TSはリークを抑えつつその情報を活かします。
まとめ
- target encodingは高カーディナリティで目的変数がリークしやすく、訓練AUCだけが膨れ上がる(純ノイズで最大0.945、検証は0.5)
- CatBoostのOrdered Target Statisticsは、ランダム順序で「自分より前の行だけ」を使って符号化し、リークを防ぐ
- 本物のシグナル+ノイズカテゴリでは、素朴なTEが検証AUCを0.793→0.597に落とす一方、CatBoostは0.807と天井を維持した
- 正しいK-fold target encodingと同等の効果を、
cat_featuresに渡すだけで自動的に得られる