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)で数値化します。 - 1万水準のID列を持つ人工データでは、学習データ全体で平均を取る素朴なターゲットエンコーディングはテストAUCが0.59まで崩れ、CatBoostのネイティブ指定は0.72を保ちました。
- 弱点は学習時間です。後述の人工データを各ライブラリの既定設定で学習すると、LightGBMの0.14秒に対してCatBoostは23.8秒かかりました(fitのみの計測)。数値列だけのデータや、試行回数の多い探索では先にLightGBMを試す方が速く結論が出ます。
- CPUでは
boosting_typeの既定が常にPlainです。論文の「ordered boosting」が既定で動くのはGPUかつ5万件以下のときだけなので、小規模データで使うなら明示します。 - カテゴリ列の欠損(NaN)と浮動小数はエラーになります。pandas 3.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回の比較で葉が決まるため、推論が速く、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 |
| ライセンス | 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はその手間を内部で肩代わりします。
PythonでのCatBoost実装:インストールから予測まで
pip install catboost と対応Pythonバージョン
pip install catboost==1.2.10 scikit-learn optuna pandas
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)で実行を確かめています。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回目までの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を自前で作るときのように水準ごとのダミー列を推論側で作り直してそろえる処理は要りません。元の特徴量の列名と順序は学習時と同じにします。
カテゴリ列の前処理で止まる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に入れた場合も上と同じエラーです。整数に丸めるか、文字列に変換してから渡します。
2つめは、pandas 3.0で特に踏みやすくなりました。pandas 3.0では astype(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 はその平方根を使います。重みを付けても順位付けの指標である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から読み込んで推論に使えます。Rでは catboost.train などの関数でPythonと同じパラメータ名を使います。
CatBoostは無料で商用利用できますか?
できます。CatBoostはApache License 2.0のオープンソースで、1.2.10のPythonパッケージも同じライセンスです。商用製品への組み込みや改変も可能です。再配布するときは、ライセンス文の添付、変更したファイルへの変更した旨の表示、著作権表示の保持、NOTICEファイルがあればその内容の表示が条件です(ライセンス第4条)。学習や推論に料金はかかりませんが、GPU学習にはNVIDIA製GPUの環境を自分で用意する必要があります。