CatBoost(キャットブースト)は、ロシアのYandexが開発し2017年7月18日にApache License 2.0でオープンソース化した勾配ブースティング決定木(GBDT)のライブラリです。同社が以前開発したMatrixNetアルゴリズムの後継にあたり、文字列のカテゴリ列をエンコードせずに渡せることが最大の特徴です。この記事では、カテゴリ列を安全に数値化する仕組みと、推論が速い理由である対称木の葉の決まり方を、NeurIPS 2018の原論文と実際のモデルの分割値で図解します。そのうえで、最新版1.2.10(2026年2月18日公開)で実行を確かめたPythonコードと実測値で、学習・推論・書き出し・評価・チューニングまでを通します。
まとめ:CatBoostの要点と選びどころ
- カテゴリ列は
cat_featuresに列名を渡すだけで学習できます。代表的な処理は、並べ替えた順序で自分より前にある同じカテゴリの行から計算する順序付きターゲット統計(ordered TS)です。水準数の少ない列にはone-hotを使い、目的変数を使わない頻度統計も利用します。 - 1万水準のID列を持つ人工データでは、学習データ全体で平均を取る素朴なターゲットエンコーディングはテストAUCが0.59まで崩れ、CatBoostのネイティブ指定は0.72を保ちました。
- 弱点は学習時間です。後述の人工データを各ライブラリの既定設定で学習すると、LightGBMの0.14秒に対してCatBoostは23.8秒かかりました(fitのみの計測)。数値列だけのデータや、試行回数の多い探索では先にLightGBMを試す方が速く結論が出ます。
- 推論は逆に速く、木の本数(1000本)と深さ(6)をそろえると、10万行の一括予測はCatBoostが0.086秒、XGBoostが0.600秒、LightGBMが3.551秒でした。同じ深さの分割条件が共通の対称木なので、葉の番号が比較結果を並べた2進数で決まります。
- ONNXへの書き出しは数値列だけのモデルに限られます。Core MLは数値列に加え、one-hotで扱われたカテゴリ列も書き出せます。カテゴリ列を使ったモデルは、保存したcbmを、公式のC・C++・JVM・Rust・.NET・Node.js向け推論専用ライブラリで読み込めます。独立したコードが必要なら、学習データの
Poolを渡してC++かPython形式で書き出します。 - CPUでは
boosting_typeの既定が常にPlainです。論文の「ordered boosting」が既定で動くのはGPUかつ5万件以下のときだけなので、小規模データで使うなら明示します。 - カテゴリ列の欠損(NaN)と浮動小数はエラーになります。pandas 3.0以降(2026年10月22日公開の3.1.0でも同じ)では
astype(str)でも欠損がNaNのまま残るので、fillnaで文字列に置き換えます。
以下、仕組み、ほかのGBDTとの違い、推論の速さ、Pythonでの実装、チューニングと評価の順に説明します。
CatBoostの仕組み:順序付きターゲット統計と対称木
カテゴリ列を目的変数の平均で数値化するときのリーク
決定木はカテゴリの文字列をそのまま分割に使えないため、何らかの方法で数値に置き換えます。水準数の多い列(ユーザーID、郵便番号、商品コードなど)でone-hotにすると列が水準の数だけ増えるので、「そのカテゴリの行の目的変数の平均」で置き換えるターゲットエンコーディングがよく使われます。
問題は、学習データ全体で平均を取ると、各行のエンコード値にその行自身の正解が混ざることです。1水準に数行しかない列では、平均値がほぼ正解そのものになり、モデルは学習データ上でだけ当たるルールを覚えます。ProkhorenkovaらのNeurIPS 2018論文「CatBoost: unbiased boosting with categorical features」はこれを「target leakage」による「prediction shift」と呼び、CatBoostの2つの技術はどちらもこの対策だと述べています。
ordered TS:ランダム順列で自分より前に並んだ行だけから計算する統計量
CatBoostは学習データの行をランダムに並べ替え、その順番を人工的な時間として扱います。各行のエンコード値は、並びで自分より前にある同じカテゴリの行だけから計算します。自分自身と後ろの行の正解は使わないので、推論時(正解が未知の行を変換するとき)と同じ条件で学習できます。先頭付近の行は履歴が少ないため、事前分布(prior)を足して値を安定させます。
1.2.10をCPUで分類・回帰に使う場合の既定では、水準が2以下の列(性別など)はone-hotで扱い(one_hot_max_size=2)、3水準以上をordered TSで変換します。ランキング学習では既定が10になるなど、学習モードによって値が変わります。さらに、カテゴリ同士の組み合わせ(例:職業×学歴)も自動で作り、組み合わせる列数の上限は max_ctr_complexity=4 です(GPUの多クラス分類では1)。
ordered boostingとboosting_typeの既定
同じリークは勾配の計算でも起きます。前の木が学習に使った行で次の木の勾配を計算すると、残差が実際より小さく見えるためです。論文のordered boostingは、行ごとに「自分より前の行だけで学習したモデル」の予測から残差を出してこれを避けます。
ただし、この方式は計算が重く、ライブラリの既定では多くの場合オフです。公式ドキュメントの学習パラメータ一覧によると、boosting_type の既定はCPUでは Plain、GPUでは5万件以下かつMultiClass系以外のときだけ Ordered です。手元のCPUでも1,000行・2万行・6万行のいずれも Plain になりました。学習2,000行・300水準のID列を持つ人工データ(3シード平均・fitのみ計測)で比べると、Ordered はAUC 0.7228・11.3秒、Plain は0.7154・6.3秒でした。この人工データでは学習時間が約1.8倍になる代わりにAUCが0.007上がっています。数千行規模で精度を詰める場面では、boosting_type="Ordered" を明示し、検証データでPlainと比べてから採否を決めます。
対称木:同じ深さのノードは同じ分割条件
木の形も他のGBDTと違います。既定の grow_policy="SymmetricTree" では、同じ深さのノードがすべて同じ特徴量・同じしきい値で分割されます。深さ6(既定値)の木は6回の比較で64個の葉のどれかに決まり、1本ごとの自由度が低いぶん過学習しにくい形です。この形が推論を速くする仕組みは、後の「推論の速さ」の章で実際の分割値を使って図解します。LightGBMのように不均一な形で木を伸ばしたい場合は grow_policy="Lossguide" に切り替えられ、その場合の depth 既定値は16になります。
XGBoost・LightGBMとの違い:カテゴリ処理・木の形・学習時間
| 項目 | CatBoost 1.2.10 | LightGBM 4.7.0 | XGBoost 3.4.1 |
|---|---|---|---|
| 開発元 | Yandex | Microsoft | DMLC |
| 文字列カテゴリの直接入力 | 可(cat_features) | 不可(category型か整数コード) | 不可(category型へ変換) |
| カテゴリの変換方式 | ordered TS+組み合わせ | 勾配統計で水準を分割 | 分割(enable_categorical) |
| 木の伸ばし方(既定) | 対称木 | leaf-wise | depth-wise |
| 学習時間 5万行×50列 | 19.73秒 | 1.17秒 | 1.56秒 |
| AUC 5万行×50列 | 0.9921 | 0.9886 | 0.9902 |
| 推論 10万行(1000本・深さ6) | 0.086秒 | 3.551秒 | 0.600秒 |
| ライセンス | Apache 2.0 | MIT | Apache 2.0 |
学習時間とAUCは、数値列だけの5万行×50列の人工データを各ライブラリの既定値で学習した値です(GBDTの仕組みと主要4実装の実測比較と同じ環境・同じデータ)。既定値のままではCatBoostがAUCで最も高く、学習時間はLightGBMの約17倍でした。推論の行は条件をそろえて別に測った値で、測り方は後の「推論の速さ」の章に書いています。
実務での選び方は明快です。文字列のカテゴリ列が多く、前処理の手間とリークの危険を減らしたいならCatBoostを選びます。数値列が中心で、学習を何百回も回す探索が前提ならLightGBMから始めます。LightGBM側の詳しい使い方はLightGBMの仕組みと実装例で扱っています。
高カーディナリティのID列:素朴なターゲットエンコーディングとの実測比較
ordered TSの効果を、リークが最も起きやすい条件で確かめました。30,000行のうち7割で学習し、ユーザーIDに相当する1万水準の列(1水準あたり学習行は約2.1行)に、目的変数へ弱い影響を持たせています。数値列2本と合わせた3列のデータで、3シードの平均を取りました。
実験条件は次のとおりです。数値列x1・x2は標準正規乱数、IDごとの効果は標準偏差0.5の正規乱数で、0.8×x1から0.5×x2を引いてIDの効果を足した値をロジットとし、そのシグモイド確率で目的変数を0か1に決めています。シードは0・1・2で、train_test_split(test_size=0.3) で分割し、モデルはすべて各ライブラリの既定値(スレッド数も既定)です。環境はIntel Core i9-9880H(16スレッド)のmacOS、Python 3.13.14・catboost 1.2.10・lightgbm 4.7.0です。
| 方式 | 学習AUC | テストAUC | 学習時間(fitのみ) |
|---|---|---|---|
| 学習データ全体で平均+LightGBM | 0.9331 | 0.5855 | 0.15秒 |
| 学習データ全体で平均+CatBoost | 0.9345 | 0.5953 | 7.77秒 |
| LightGBMのネイティブカテゴリ | 0.7584 | 0.7184 | 0.14秒 |
| CatBoostのcat_features指定 | 0.7984 | 0.7192 | 23.76秒 |
| (参考)ID列を使わない | 0.7584 | 0.7184 | - |
学習データ全体の平均でエンコードすると、学習AUCが0.93まで上がる一方でテストAUCは0.59前後まで落ちます。CatBoostに渡しても結果は同じで、崩れの原因はライブラリではなくエンコードの手順にあります。CatBoostに文字列のまま cat_features で渡した場合だけ、ID列を使いながらテストAUCを保てました。
LightGBMのネイティブ指定は、ID列を除いたモデルと小数4桁まで同じ値でした。分割回数(split重要度)を見ると、ID列は0回です。この実験の条件(既定パラメータ・1水準あたり約2.1行)では、LightGBMはID列から分割を1つも作らず、リークしない代わりに情報も取り出していません。どのパラメータが効いているかまでは切り分けていないため、LightGBMが低頻度のカテゴリを常に無視するという意味ではありません。この種の列を自前でターゲットエンコードするなら、各行が属する折を除いたデータで平均を計算するout-of-fold方式など、自身の正解を変換値に混ぜない設計を使います(特徴量エンジニアリングの工程と実装判断で扱っています)。CatBoostはその手間を内部で肩代わりします。
推論の速さ:対称木の葉番号の図解と実測
対称木の葉番号:分割結果の2進数による表現
対称木では、1本の木の中で使う分割条件が深さの数だけしかありません。深さ3なら条件は3つで、各条件を満たすかどうか(1か0か)を並べた3桁の2進数が、そのまま8個の葉の番号になります。次の図は、3列の人工データ(make_classification で2,000行・random_state=0)で深さ3の木を3本学習し、1本目の木を書き出した実際の値です。
1本目の木(深さ3・葉8個)に、15行目 x0=1.7196, x1=0.5251, x2=-2.3074 を通す
条件0: x2 > 0.5602 → いいえ → 0 (1の位)
条件1: x1 > -0.5158 → はい → 1 (2の位)
条件2: x0 > 0.2742 → はい → 1 (4の位)
葉番号 = 0×1 + 1×2 + 1×4 = 6 (2進数 110)
葉6の値 = -0.17168
葉番号 0 1 2 3 4 5 6 7
行数 186 211 421 345 96 249 101 391 (学習データ2,000行の内訳)
条件の順番と位の対応は、save_model(format="json") で書き出したJSONの oblivious_trees にある splits 配列のi番目の条件が、2進数の2のi乗の位に当たります。この規則で2,000行すべての葉番号を手計算し、calc_leaf_indexes が返す値と照合すると全行で一致しました。
通常の決定木は、ノードごとに次に見る条件が変わるため、枝をたどる分岐の連続になります。対称木は条件が深さごとに1つなので、深さの数だけ比較してその結果をビットとして足し合わせれば葉が決まり、行ごとに違う枝をたどる処理が要りません。CatBoostのGitHubリポジトリのREADMEは特長に「Best-in-class prediction speed」を挙げており、その土台がこの構造です。
10万行の一括推論と1行推論の実測
数値列50本の人工データ(make_classification・n_informative=20・15万行を random_state=0 で生成)の先頭5万行で学習し、残りの10万行を predict_proba に渡した時間を5回測った中央値です。計時の前に1,000行の予測を1回流してウォームアップしています。1行推論は1行のデータで1,000回呼んだ平均で、Pythonの呼び出しの手間も含みます。予測の時間だけを計時し、学習時間は含めていません。
| 条件 | ライブラリ | 木の本数 | 10万行の一括推論 | 1行推論 |
|---|---|---|---|---|
| 既定値 | CatBoost | 1000 | 0.086秒 | 1.060ミリ秒 |
| 既定値 | LightGBM | 100 | 0.325秒 | 2.420ミリ秒 |
| 既定値 | XGBoost | 100 | 0.060秒 | 1.281ミリ秒 |
| 1000本・深さ6 | CatBoost | 1000 | 0.086秒 | 0.893ミリ秒 |
| 1000本・深さ6 | LightGBM | 1000 | 3.551秒 | 3.606ミリ秒 |
| 1000本・深さ6 | XGBoost | 1000 | 0.600秒 | 2.555ミリ秒 |
既定値ではLightGBMとXGBoostの木が100本、CatBoostが1000本なので、そのまま比べると本数の差を測ることになります。そこで本数を1000本、最大深さを6(LightGBMは max_depth=6・num_leaves=64、XGBoostは max_depth=6)、学習率を0.05にそろえると、CatBoostの一括推論はXGBoostの約7分の1、LightGBMの約41分の1の時間でした。既定値どうしの比較でも、CatBoostは10倍の本数の木を持ちながら、XGBoostに近い時間(0.086秒と0.060秒)で終わっています。環境はIntel Core i9-9880H(16スレッド)、catboost 1.2.10・lightgbm 4.7.0・xgboost 3.4.1で、スレッド数は各ライブラリの既定です。
今回の数値列データでは、同じ本数の木を使う推論でCatBoostが速い結果でした。オンライン推論や夜間バッチへの採用時は、実際のデータと必要な予測精度で応答時間を比較します。学習を何度も回す探索ではLightGBM、作ったモデルを大量の行に当て続ける本番運用ではCatBoostを先に試す、という分担が目安になります。推論の応答時間や再学習の周期まで含めてモデルを設計する場合は、一創の機械学習モデル開発でも相談を受けています。
PythonでのCatBoost実装:インストールから予測まで
pip install catboost と対応Pythonバージョン
python -m pip install catboost==1.2.10 scikit-learn==1.9.1 optuna==5.0.0 pandas==3.1.0
python -c "import catboost; print(catboost.__version__)" # 1.2.10
この記事のコードは catboost 1.2.10・scikit-learn 1.9.1・optuna 5.0.0・pandas 3.0.6(Python 3.13.14)で実行を確かめ、推論時間・書き出し・欠損の扱いは2026年10月23日にpandas 3.1.0・lightgbm 4.7.0・xgboost 3.4.1・onnxruntime 1.23.2で測り直しています(ONNXの例を動かすには python -m pip install onnxruntime==1.23.2 も必要です)。PyPIの1.2.10には、CPython 3.8〜3.14向けのビルド済みwheelがあります。LinuxとWindowsの配布版はCUDAライブラリを静的リンクしているので、NVIDIA Driver 450.80.02以上とCompute Capability 3.5以上のGPUがあれば、追加のCUDA導入なしでGPU学習できます。公式の導入先はPythonのほか、Rパッケージ、コマンドライン版、Apache Spark用パッケージがあります。
CatBoostClassifierの学習:cat_featuresと検証データでの早期終了
OpenMLの成人所得データ(adult・48,842行・カテゴリ8列)で、年収が5万ドルを超えるかを分類します。カテゴリ列の欠損を文字列に置き換える1行を除けば、エンコードの前処理はありません。
import pandas as pd
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from catboost import CatBoostClassifier
X, y = fetch_openml("adult", version=2, as_frame=True, return_X_y=True)
y = (y == ">50K").astype(int)
cat_cols = X.select_dtypes(exclude="number").columns.tolist()
X[cat_cols] = X[cat_cols].astype("object").fillna("missing") # カテゴリ列のNaNは文字列へ
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y)
X_fit, X_va, y_fit, y_va = train_test_split(X_tr, y_tr, test_size=0.2, random_state=0, stratify=y_tr)
model = CatBoostClassifier(
iterations=3000,
learning_rate=0.1,
eval_metric="AUC",
early_stopping_rounds=100,
random_seed=0,
verbose=0,
)
model.fit(X_fit, y_fit, cat_features=cat_cols, eval_set=(X_va, y_va))
print(model.get_best_iteration(), model.tree_count_) # 563 564
print(roc_auc_score(y_te, model.predict_proba(X_te)[:, 1])) # 0.9283
eval_set を渡し、最後の検証データセットのラベルが単一値でない場合、use_best_model が既定でTrueになり、検証AUCが最良だった反復番号563(0始まりのため564回目)までの564本に木が切り詰められます。use_best_model=False にすると、打ち切り時点の664本が残ります。eval_metric は早期終了と最良反復の判定に使う指標で、未指定なら学習の損失関数(二値分類ではLogloss)と同じになります。
既定値のまま(1000本・学習率は自動)でも、同じデータでテストAUCは0.929でした。ただし学習には46.7秒かかったので、検証データを分けて早期終了を効かせる形を標準にします。
CatBoostRegressorでの回帰:損失関数の既定はRMSE
回帰は CatBoostRegressor に替えるだけで、fit の引数(cat_features・eval_set・early_stopping_rounds)は分類と同じです。loss_function と eval_metric の既定はどちらもRMSEで、外れ値に強くしたい場合は loss_function="MAE" や "Huber:delta=1.0" を指定します。
モデルの保存と、学習時に無かったカテゴリの予測
model.save_model("adult.cbm")
loaded = CatBoostClassifier().load_model("adult.cbm")
new = X_te.head(3).copy()
new["occupation"] = "Astronaut" # 学習データに無い水準
print(loaded.predict_proba(new)[:, 1])
学習データに無い水準を渡してもエラーになりません。その水準は学習データでの出現がゼロ件として数値化され、ほかの列の値と合わせて予測が出ます。読み込んだモデルの予測は保存前と一致しました。DataFrameで渡す場合、one-hotを自前で作るときのように水準ごとのダミー列を推論側で作り直してそろえる処理は要りません。元の特徴量の列名と順序は学習時と同じにします。
ONNX・Core ML・C++への書き出しとカテゴリ列の制約
import numpy as np, pandas as pd
import onnxruntime as ort
from catboost import CatBoostClassifier, Pool
rng = np.random.default_rng(0)
X = pd.DataFrame({"x0": rng.normal(size=3000), "x1": rng.normal(size=3000),
"c": rng.choice(list("abcde"), size=3000)})
y = (X["x0"] + rng.normal(scale=0.5, size=3000) > 0).astype(int)
# 数値列だけのモデルはONNXに書き出せる
model_num = CatBoostClassifier(iterations=100, random_seed=0, verbose=0).fit(X[["x0", "x1"]], y)
model_num.save_model("model.onnx", format="onnx")
sess = ort.InferenceSession("model.onnx")
label, proba = sess.run(None, {"features": X[["x0", "x1"]].to_numpy(np.float32)})
onnx_p1 = np.array([p[1] for p in proba])
print(np.abs(onnx_p1 - model_num.predict_proba(X[["x0", "x1"]])[:, 1]).max()) # 1.08e-07
# カテゴリ列を使うモデルは、学習データのPoolを渡してC++かPythonのコードにする
model_cat = CatBoostClassifier(iterations=100, random_seed=0, verbose=0).fit(X, y, cat_features=["c"])
model_cat.save_model("model.py", format="python", pool=Pool(X, y, cat_features=["c"]))
save_model の format は、cbm(既定のバイナリ)・json・onnx・coreml・cpp・python・pmml の7種類です。公式のsave_modelリファレンスに記載された形式を、カテゴリ列を持つモデルで試すと次の結果になりました(数値2列+5水準のカテゴリ1列・100本のモデル)。
| format | 数値列だけ | カテゴリ列あり |
|---|---|---|
| cbm | 可 | 可 |
| onnx | 可 | 不可(export does yet not support categorical features) |
| coreml | 可 | 既定では不可/one-hotで扱われた列だけ可 |
| pmml | 可 | one-hotで扱われた列だけ可 |
| cpp・python | 可 | 学習データのPoolを渡せば可 |
| json | 可 | 可(PoolなしでもCatBoostで再読込・推論可) |
同じ5水準の列でも、one_hot_max_size=5 を指定してone-hotで扱わせると、Core MLとPMMLには書き出せました。ONNXはこの場合も同じエラーのままです。
ONNXに書き出したモデルをonnxruntime 1.23.2で読み込むと、入力名は features、出力は label と probabilities の2つです。3,000行で正例の確率を比べると、CatBoostの predict_proba との差は最大1.08×10-7でした。差の原因(入力のfloat32変換か、書き出したモデル側の数値表現か)までは切り分けていません。Python形式で書き出したカテゴリ列ありのモデルも、生成された apply_catboost_model(数値列のリスト, カテゴリ列のリスト) の出力が、3,000行すべてでCatBoostの生の予測値(RawFormulaVal)と差1.8×10-15以内で一致しています。順序付きターゲット統計を使ったモデルをONNXで動かしたい場合は、この制約が設計を決めます。ONNX形式とONNX Runtimeの使い方はONNXとは?.onnxファイルの中身とONNX Runtimeでの推論で扱っています。
カテゴリ列の前処理で止まる3つのエラー
CatBoostが受け付けるカテゴリ値は整数か文字列だけです。次の3つは、1.2.10で実際に出たメッセージです。
- cat_featuresの指定漏れ:
Cannot convert 'Private' to float。文字列の列を数値として読もうとして失敗しています。cat_featuresに列名を足します。 - カテゴリ列の欠損:
cat_features must be integer or string, real number values and NaN values should be converted to string.。公式FAQは、浮動小数とNaNには一意の文字列表現が無いため禁止していると説明しています。 - 数値列をカテゴリ扱いにした:float型の列を
cat_featuresに入れた場合も上と同じエラーです。数値特徴ならcat_featuresから外します。カテゴリとして扱う設計なら、値を不用意に丸めず、一貫した文字列に変換してから渡します。
2つめは、pandas 3.0で特に踏みやすくなりました。pandas 3.0以降では astype(str) の結果が新しい文字列型(dtypeは str)になり、欠損は "nan" という文字列にならずNaNのまま残ります。古い記事の X[cat_cols].astype(str) をそのまま使うと上のエラーで止まるので、astype("object").fillna("missing") のように欠損を明示的に文字列へ置き換えます。数値列の欠損はそのままで構いません。既定の nan_mode="Min" で、欠損を最小値より小さい値として扱い、欠損とそれ以外を分ける分割が必ず候補に入ります。
既定パラメータとチューニング:depth・learning_rate・Optuna
1.2.10の既定値と調整の優先順位
| パラメータ | 既定値 | 調整の目安 |
|---|---|---|
| iterations | 1000 | 早期終了に任せて大きめ |
| learning_rate | 自動(adultで0.0493) | 0.02〜0.3 |
| depth | 6 | 4〜10 |
| l2_leaf_reg | 3 | 1〜30 |
| random_strength | 1 | 0.1〜10 |
| border_count | 254(CPU) | 通常は変えない |
| bootstrap_type | MVS(subsample 0.8) | 通常は変えない |
既定値は、adultで学習したモデルの get_all_params() で確認した値です。learning_rate は、LoglossやRMSEなどでは反復回数とデータから自動で決まります(adultの1000本で0.0493、5本だけなら0.5)。l2_leaf_reg などを自分で設定すると自動決定が外れて0.03になるので、学習率も一緒に指定します。調整は learning_rate と depth を先に決め、次に l2_leaf_reg、最後に random_strength の順で十分です。border_count(数値列の分割候補数)はGPUでは既定が128(PairLogitPairwise・YetiRankPairwiseでは32)に下がる点だけ覚えておきます。
Optunaでの探索:検証AUCを目的関数とする設定
import optuna
from catboost import Pool
train_pool = Pool(X_fit, y_fit, cat_features=cat_cols)
valid_pool = Pool(X_va, y_va, cat_features=cat_cols)
def objective(trial):
params = {
"iterations": 2000,
"learning_rate": trial.suggest_float("learning_rate", 0.02, 0.3, log=True),
"depth": trial.suggest_int("depth", 4, 10),
"l2_leaf_reg": trial.suggest_float("l2_leaf_reg", 1.0, 30.0, log=True),
"random_strength": trial.suggest_float("random_strength", 0.1, 10.0, log=True),
"eval_metric": "AUC",
"early_stopping_rounds": 100,
"random_seed": 0,
"verbose": False,
}
model = CatBoostClassifier(**params)
model.fit(train_pool, eval_set=valid_pool)
trial.set_user_attr("best_iteration", model.get_best_iteration())
return model.get_best_score()["validation"]["AUC"]
study = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=0))
study.optimize(objective, n_trials=20)
print(study.best_value, study.best_params)
adultで20試行を回すと1,004.5秒(1試行あたり約50秒)かかり、検証AUCの最良値は0.9306でした(depth=4・learning_rate=0.0624・l2_leaf_reg=2.71・random_strength=0.322、最良反復1509)。この設定で反復数を1510に固定し、検証用に分けた行も含めた学習データ全体で学び直すと、テストAUCは0.9293です。既定値のまま学習した0.929との差は0.0003しかありません。adultのこの分割・20試行の範囲では、探索に17分かけても既定値からの改善はわずかでした。探索は、既定値と早期終了で作った基準のAUCを記録してから回し、上積みが計算時間に見合うかで打ち切りを判断します。
Pool はデータとカテゴリ列の指定をまとめたオブジェクトで、試行ごとに文字列の変換をやり直さずに済みます。探索手法の選び方や打ち切りの設計はハイパーパラメータ探索の手法と打ち切り設計で詳しく扱っています。
評価と解釈:cv・特徴量重要度・不均衡データ
catboost.cvでの交差検証と出力列
from catboost import Pool, cv
params = {"loss_function": "Logloss", "eval_metric": "AUC",
"iterations": 500, "learning_rate": 0.1, "random_seed": 0, "verbose": False}
res = cv(Pool(X_tr, y_tr, cat_features=cat_cols), params,
fold_count=5, stratified=True, early_stopping_rounds=50, verbose=False)
print(res.columns.tolist())
print(res.tail(1))
cv は反復ごとに、各折の指標の平均と標準偏差をDataFrameで返します。出力列は test-AUC-mean・test-AUC-std・test-Logloss-mean・train-Logloss-mean などで、adultの学習データ(39,073行)では出力の最終行が反復番号481(0始まりなので482反復目)で、test-AUC-mean 0.9295(標準偏差0.0022)でした。5折の計算に116秒かかります。fold_count の既定は3です。時系列データや同じ顧客の行が複数あるデータでは、ランダムな折分けがリークの原因になるので、分割方法は交差検証の分割手法とデータリーク対策を参考に選んでください。
get_feature_importanceの3種類:順位が入れ替わる例
from catboost import Pool
print(model.get_feature_importance(prettified=True).head(4))
test_pool = Pool(X_te, y_te, cat_features=cat_cols)
print(model.get_feature_importance(test_pool, type="LossFunctionChange", prettified=True).head(4))
shap_values = model.get_feature_importance(test_pool, type="ShapValues")
print(shap_values.shape) # (9769, 15)=14特徴量+期待値
早期終了で学習した上のモデルでは、既定の PredictionValuesChange(予測値がどれだけ動くか)の上位が capital-gain 20.1・relationship 15.7・age 12.1・capital-loss 8.3 でした。テストデータで損失の悪化量を測る LossFunctionChange では、capital-lossが上位4つから外れ、hours-per-weekが入ります。ShapValues は行ごと・特徴量ごとの寄与で、最後の列に期待値が付くため列数が特徴量より1つ多くなります。個別の予測を説明したいならSHAPを使います。LossFunctionChangeで削る列を決めるなら、最終評価用のテストデータではなく検証データで計算し、テストデータは列を削った後のモデルの評価だけに使います。SHAPとLIMEの違いはモデル解釈性の手法の選び方にまとめています。
不均衡データ:auto_class_weightsでの再現率の変化
from sklearn.metrics import recall_score, precision_score, roc_auc_score
for w in [None, "Balanced", "SqrtBalanced"]:
m = CatBoostClassifier(iterations=500, learning_rate=0.1, auto_class_weights=w,
random_seed=0, verbose=0)
m.fit(X_fit, y_fit, cat_features=cat_cols)
pred = m.predict(X_te).astype(int)
print(w, recall_score(y_te, pred), precision_score(y_te, pred),
roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]))
| auto_class_weights | 再現率 | 適合率 | AUC |
|---|---|---|---|
| None(既定) | 0.6501 | 0.7823 | 0.9280 |
| SqrtBalanced | 0.7682 | 0.6994 | 0.9291 |
| Balanced | 0.8554 | 0.6139 | 0.9287 |
adultの正例(年収5万ドル超)は23.9%です。Balanced はクラスの出現頻度に反比例する重み、SqrtBalanced はその平方根を使います。今回のadultの比較では、重みを付けても順位付けの指標であるAUCはほぼ変わらず、動くのは predict が使う確率0.5のしきい値での再現率と適合率です。取りこぼしを減らしたい用途なら Balanced、確率そのものを使う用途(スコアリングや期待値計算)なら重みを付けずに、しきい値の方を調整します。再現率と適合率の読み方は混同行列と適合率・再現率・F値の見方で解説しています。
CatBoostを使わない方がよい場面
学習時間を優先する場面では、CatBoostを第一候補にしません。カテゴリ列が無い、または水準の少ない列しか無いデータではカテゴリ処理の利点が出る余地が小さく、数値列だけのベンチマークではCatBoostのAUCの上積みが0.002〜0.004だったのに対し、学習時間はLightGBMの約17倍でした。試行を数百回回す探索や、毎日再学習するバッチでは、この差がそのまま計算コストになります。この条件ならLightGBMかXGBoostを選びます。
GPU学習を再現性が必要な場面に使うのも避けます。公式ドキュメントは、GPU学習は浮動小数の加算順序が決まらないため、同じ乱数シードでもCPUのような決定性は保証されないと明記しています。監査や比較実験で結果を再現する必要があるなら、CPUで学習します。
1万行以下の小さな表データでは、GBDT自体が最善とは限りません。2025年1月のNature論文では、1万行・500特徴量までのデータでTabPFNがGBDTを上回ったと報告されており、その整理はGBDTの記事の「採用しない方がよい場面」に書いています。
よくある質問
CatBoostとLightGBMの違いは何ですか?
大きな違いはカテゴリ列の扱いと学習速度です。CatBoostは文字列のカテゴリ列をそのまま受け取り、順序付きターゲット統計で数値化するので、水準数の多い列でもリークしにくい設計です。LightGBMは文字列を直接受け取らず、pandasのcategory型にするか、整数コードに変換して categorical_feature で指定します。この記事の1万水準のID列(1水準約2行)では、LightGBMはこの列を分割に1回も使いませんでした。速度はLightGBMが大きく上回り、数値列だけの5万行では既定値でCatBoostの約17分の1の時間で学習しました。
CatBoostではカテゴリ変数のエンコーディングは不要ですか?
one-hotやラベルエンコーディングは不要で、列名を cat_features に渡せば内部で変換されます。自前でターゲットエンコーディングをしてから渡すと、学習データ全体の平均を使った場合にリークが起き、この記事の実測ではテストAUCが0.72から0.59前後へ落ちました。必要な前処理は、カテゴリ列の欠損や浮動小数を文字列か整数に変換することだけです。
CatBoostの学習をGPUで速くできますか?
task_type="GPU" と devices="0" を指定するとGPUで学習します。LinuxとWindowsの配布版はCUDAを同梱しているため、NVIDIA Driver 450.80.02以上とCompute Capability 3.5以上のGPUがあれば動きます。GPUでは border_count の既定が128(PairLogitPairwise・YetiRankPairwiseでは32)に下がり、5万件以下では多クラス分類を除いて boosting_type の既定がOrderedになるなど、CPUと既定値が変わる点と、結果が完全には再現しない点に注意します。
CatBoostはPython以外から使えますか?
公式の導入ガイドはPythonパッケージのほか、Rパッケージ、コマンドライン版、Apache Spark用パッケージを用意しています。Pythonで学習して保存した .cbm 形式のモデルは、コマンドライン版やC++のモデル適用APIから読み込んで推論に使えます。C・JVM・Rust・.NET・Node.js向けの推論専用ライブラリもあり(公式のモデル適用APIの一覧)、数値列だけのモデルならONNXに書き出してONNX Runtimeでも動かせます。Rでは catboost.train などの関数でPythonと同じパラメータ名を使います。
CatBoostは無料で商用利用できますか?
できます。CatBoostはApache License 2.0のオープンソースで、1.2.10のPythonパッケージも同じライセンスです。商用製品への組み込みや改変も可能です。再配布するときは、ライセンス文の添付、変更したファイルへの変更した旨の表示、著作権表示の保持、NOTICEファイルがあればその内容の表示が条件です(ライセンス第4条)。学習や推論に料金はかかりませんが、GPU学習にはNVIDIA製GPUの環境を自分で用意する必要があります。