CatBoost 是一种基于梯度提升决策树的机器学习算法和开源工具库。它尤其擅长处理由一行行、一列列组成的结构化表格数据,并且能够原生接收城市、产品类型、会员等级、设备型号等类别特征。官方实现支持分类、回归、排序等任务,也支持数值、类别、文本和嵌入特征,以及 CPU、GPU、Python、R、命令行和 Apache Spark 等使用方式。[^1]
很多介绍会把 CatBoost 简化成“一个不用手工 One-Hot 的树模型”。这句话不算错误,但远远不够。CatBoost 真正值得理解的地方包括:
- 它为什么使用许多棵树,而不是一棵很大的树;
- 它怎样让后面的树不断纠正前面的错误;
- 类别特征为什么比数值特征更难处理;
- 普通目标编码为什么容易泄漏标签;
- CatBoost 的有序类别统计如何降低泄漏风险;
- Ordered Boosting 与有序类别统计有什么区别;
- 对称树为什么能带来规整结构和较快推理;
- 怎样正确划分训练集、验证集和测试集;
- 怎样选择损失函数、指标和主要超参数;
- 怎样判断模型是真的有泛化能力,而不是记住了历史数据。
本文从这些问题出发,逐步建立一套完整的 CatBoost 认知框架。
目录
- 一、先用一句话理解 CatBoost
- 二、CatBoost 适合解决什么问题
- 三、训练模型前必须理解的基础概念
- 四、先理解决策树
- 五、为什么一棵决策树通常不够
- 六、什么是 Boosting 和梯度提升
- 七、损失函数:模型如何知道自己错了
- 八、类别特征为什么难处理
- 九、CatBoost 如何处理类别特征
- 十、Ordered Boosting 到底是什么
- 十一、CatBoost 的对称树
- 十二、数值量化与缺失值处理
- 十三、完整而可靠的模型训练流程
- 十四、第一次 CatBoost 分类训练:完整 Python 示例
- 十五、CatBoost 回归模型示例
- 十六、CatBoost 命令行训练示例
- 十七、最重要的 CatBoost 参数
- 十八、推荐的调参顺序
- 十九、分类模型应该看哪些指标
- 二十、回归和排序模型的评估指标
- 二十一、类别不平衡与概率阈值
- 二十二、概率校准
- 二十三、特征重要性、SHAP 与模型解释
- 二十四、CatBoost、LightGBM、XGBoost 与随机森林的区别
- 二十五、什么时候适合使用 CatBoost
- 二十六、什么时候 CatBoost 不一定合适
- 二十七、CPU、GPU 与可复现性
- 二十八、模型保存、部署与版本管理
- 二十九、初学者最常见的错误
- 三十、建立第一个可靠 CatBoost 项目的检查清单
- 结语
一、先用一句话理解 CatBoost
可以先把 CatBoost 理解为:
CatBoost 会建立许多棵相对较小的决策树,让后面的树不断修正前面树留下的错误;同时,它使用一套专门设计的有序统计方法处理类别特征,尽量降低普通目标编码和传统 Boosting 中的预测偏移。
从算法家族上看:
机器学习
└── 监督学习
└── 集成学习
└── Boosting
└── 梯度提升决策树 GBDT
├── XGBoost
├── LightGBM
└── CatBoost
CatBoost 不是神经网络,也不是大语言模型。它主要面对的是这样的数据:
| 城市 | 会员等级 | 设备类型 | 注册天数 | 近90天订单数 | 最近登录间隔 | 是否流失 |
|---|---|---|---|---|---|---|
| 上海 | 黄金会员 | Android | 520 | 23 | 2 | 0 |
| 北京 | 普通会员 | iPhone | 83 | 1 | 46 | 1 |
| 广州 | 白金会员 | Windows | 910 | 65 | 1 | 0 |
其中:
- 每一行是一名客户;
- 每一列是一个属性;
城市、会员等级、设备类型是类别特征;注册天数、订单数、登录间隔是数值特征;是否流失是模型需要学习的答案。
CatBoost 的任务,就是从大量历史记录中寻找规律,然后对尚未发生结果的新记录进行预测。
二、CatBoost 适合解决什么问题
CatBoost 支持的核心任务包括分类、回归、多分类、多标签分类和排序。[^6]
2.1 二分类
答案只有两种。
例如:
客户是否会流失
订单是否存在欺诈风险
设备是否会在未来7天发生故障
用户是否会点击某个商品
贷款是否会发生逾期
模型通常输出一个概率:
客户未来30天流失概率:0.783
随后再根据业务阈值决定是否判定为流失。
2.2 多分类
答案有三个或更多互斥类别。
例如:
商品类别:服装、食品、家电、图书
故障类型:电源故障、传感器故障、通信故障、机械故障
客户等级:普通、成长、高价值、沉睡
模型可能输出:
普通客户:0.12
成长客户:0.28
高价值客户:0.54
沉睡客户:0.06
这些概率之和通常为 1。
2.3 回归
回归模型预测连续数值。
例如:
房屋成交价格
未来30天销售额
订单配送时间
设备剩余寿命
客户未来一年消费金额
输出可能是:
预计成交价格:3,218,600 元
2.4 排序
排序模型关注的是对象之间的相对顺序。
例如:
给同一用户展示哪些商品,以及展示顺序
搜索结果中哪些页面排在前面
风险工单应该先处理哪一条
候选广告应该怎样排序
排序任务通常会把同一次搜索、同一个用户请求或同一个查询下的候选对象放在一个组内进行比较。
2.5 CatBoost 最典型的数据形态
CatBoost 最常见的应用对象是结构化表格数据:
- 行数可能从几千到数百万甚至更多;
- 列中既有数值,也有类别;
- 各列具有明确业务含义;
- 目标是分类、回归或排序;
- 数据通常来自数据库、CSV、Parquet、数据仓库或特征平台。
虽然 CatBoost 也支持文本和嵌入特征,但本文重点解释它最成熟、最有代表性的场景:数值特征与类别特征混合的表格监督学习。[^1]
三、训练模型前必须理解的基础概念
3.1 样本 Sample
样本是模型学习的一条历史记录。
在客户流失任务中,一名客户在某个观察时点的一条记录,就是一个样本:
客户ID:C100023
城市:上海
会员等级:黄金会员
注册天数:520
近90天订单数:23
最近登录间隔:2天
未来30天是否流失:否
如果数据集中有 50 万名客户,就可能有 50 万条样本。
需要注意:
一行不一定天然等于一个独立样本。
如果同一名客户每天生成一条记录,那么同一客户可能出现几十次。划分训练集和测试集时,如果同一客户同时进入两个集合,就可能产生实体泄漏。
3.2 特征 Feature
特征是模型在做出预测之前允许看到的信息。
常见类型如下:
| 特征类型 | 示例 |
|---|---|
| 数值特征 | 年龄、价格、订单数、距离、时长 |
| 类别特征 | 城市、产品类型、会员等级、设备型号 |
| 二值特征 | 是否自动续费、是否实名认证 |
| 时间特征 | 注册日期、观察日期、星期、月份 |
| 文本特征 | 商品标题、评价内容、工单描述 |
| 嵌入特征 | 用户向量、商品向量、文本向量 |
模型并不知道“城市”“价格”“订单数”在人类世界中的含义。它只能通过历史数据学习:
某种特征组合出现时,某个结果是否更常见。
3.3 标签 Label 或 Target
标签是模型需要学习的正确答案。
例如:
是否流失:0 或 1
房价:3218600
故障类型:通信故障
监督学习的核心形式可以写成:
[
X \longrightarrow y
]
其中:
- (X) 表示输入特征;
- (y) 表示标签;
- 模型试图学习一个函数 (f),使得 (f(X)) 尽量接近 (y)。
3.4 模型 Model
模型是从训练数据中学到的一组规则或参数。
CatBoost 模型不是一条简单公式,而是许多棵树的组合。预测时,一条新记录会依次经过这些树,每棵树贡献一个分数,所有分数汇总后形成最终结果。
3.5 模型参数与超参数
这两个词容易混淆。
模型参数
由训练算法从数据中自动学到,例如:
- 每棵树选择了哪些分裂条件;
- 每个叶子节点的数值;
- 类别统计使用了哪些组合。
超参数
由训练者在训练前设置,例如:
树的数量 iterations
学习率 learning_rate
树深度 depth
正则化强度 l2_leaf_reg
模型参数是“训练出来的”,超参数是“训练配置”。
3.6 训练、验证和测试
训练集 Training Set
模型直接用它建立树、计算梯度和更新参数。
验证集 Validation Set
模型不使用它建立树,但会在训练过程中观察其指标,用于:
- 判断是否过拟合;
- 选择最佳迭代轮次;
- 比较超参数;
- 执行早停。
测试集 Test Set
测试集应该在模型方案基本确定后使用,用于评估最终泛化能力。
可以把三者比喻为:
训练集:课本和练习题
验证集:模拟考试
测试集:最终考试
如果根据测试集表现反复修改特征和参数,测试集就已经被间接用于调参,不再是真正独立的最终考试。
3.7 训练与推理
训练 Training
从历史数据中学习模型,通常比较耗时。
推理 Inference / Prediction
把训练好的模型应用于新数据,通常比训练快得多。
生产系统中经常是:
每周或每天训练一次
每分钟、每秒甚至每个请求执行预测
3.8 泛化能力
训练模型的目标不是把历史数据背下来,而是对未见数据做出有效预测。
一个模型可能:
训练集准确率:99%
测试集准确率:61%
这通常意味着模型记住了训练数据中的细节,却没有学到稳定规律。这种现象叫作过拟合。
四、先理解决策树
CatBoost 由许多棵决策树组成,因此必须先理解一棵树怎样工作。
假设需要判断客户是否可能流失,一棵简单决策树可能是:
最近登录间隔是否大于30天?
├── 是
│ └── 近90天订单数是否小于2?
│ ├── 是:高流失风险
│ └── 否:中等流失风险
└── 否
└── 是否关闭自动续费?
├── 是:中等流失风险
└── 否:低流失风险
4.1 节点、分裂与叶子
| 名称 | 含义 |
|---|---|
| 根节点 | 树的第一个判断条件 |
| 节点 | 中间判断位置 |
| 分裂 Split | 用一个条件把样本分到不同分支 |
| 分支 | 条件成立或不成立后进入的路径 |
| 叶子 Leaf | 路径终点,保存预测值或修正值 |
| 深度 Depth | 从根节点到叶子需要经过的层数 |
数值特征的分裂通常类似:
DaysSinceLastLogin > 30
类别特征经过 CatBoost 内部转换后,也会形成可用于树分裂的数值统计条件。
4.2 树怎样选择分裂条件
假设当前有 10 万条训练样本和 50 个特征。模型会考察大量候选条件,例如:
年龄 < 25
年龄 < 35
订单数 < 2
订单数 < 5
最近登录间隔 > 14
最近登录间隔 > 30
对每个候选条件,模型都会估计:
使用这个条件分开样本后,损失函数能改善多少?
改善最大的候选条件通常更有可能被选中。
因此,决策树不是人工写出来的 if...else,而是算法从数据中自动搜索出来的规则结构。
4.3 决策树为什么适合表格数据
树模型有几个天然优势:
- 能表示非线性关系;
- 能自动发现特征之间的交互;
- 通常不需要标准化数值特征;
- 对异常值和不同量纲相对不敏感;
- 能处理“只有超过某个阈值才产生明显影响”的规则。
例如,客户流失风险可能不是随登录间隔线性增加,而是:
0~7天:风险很低
8~30天:风险缓慢上升
超过30天:风险突然增大
树模型可以自然地通过多个阈值表达这种关系。
五、为什么一棵决策树通常不够
5.1 树太浅:欠拟合
一棵很浅的树可能只学会:
最近登录间隔大于30天 → 容易流失
但真实情况还受到会员等级、历史订单、客服投诉、价格敏感度、使用设备等因素影响。
树太浅时,模型过于简单,无法捕捉复杂规律,这叫欠拟合。
5.2 树太深:过拟合
如果把一棵树无限加深,它可能学到非常狭窄的规则:
城市=某城市
且设备型号=某型号
且注册天数在417~419之间
且近90天订单数=3
且某字段缺失
→ 判定为流失
这条规则可能刚好解释训练集中的几个样本,却无法推广到未来。
树越深,通常表达能力越强,但记忆噪声的能力也越强。
5.3 集成学习的基本想法
与其依赖一棵巨大的树,不如组合许多棵相对受约束的树。
这就是集成学习:
让多个弱模型共同组成一个更强的模型。
随机森林和梯度提升都属于树集成,但组织方式不同。
随机森林
- 多棵树相对独立训练;
- 每棵树看到不同的样本或特征子集;
- 最后通过平均或投票得到结果。
Boosting
- 树是按顺序训练的;
- 后一棵树专门修正前面模型仍然存在的错误;
- 最终把所有树的贡献累加起来。
CatBoost 属于第二种。
六、什么是 Boosting 和梯度提升
6.1 先理解“不断订正错题”
可以把 Boosting 想象成一个学生连续做模拟考试:
第1次:先做一个粗略判断
第2次:重点订正第1次错得最明显的部分
第3次:继续订正前两次仍未解决的部分
第4次:进一步修正
……
对应到模型:
第1棵树:给出初步预测
第2棵树:修正第1棵树的残余错误
第3棵树:修正前两棵树的残余错误
……
最终模型由许多小修正累积而成。
6.2 梯度提升的简化公式
设前 (m-1) 棵树构成的模型为:
[
F_(x)
]
第 (m) 棵树为:
[
h_m(x)
]
更新后的模型为:
[
F_m(x)=F_(x)+\eta h_m(x)
]
其中:
- (x) 是一条样本;
- (F_(x)) 是当前模型的预测;
- (h_m(x)) 是新树提供的修正;
- (\eta) 是学习率
learning_rate。
换成自然语言:
新模型 = 旧模型 + 学习率 × 新树的修正结果
6.3 为什么叫“梯度”提升
模型需要知道:
当前预测应该往哪个方向调整?
应该调整多少?
损失函数对当前预测的梯度,提供了这种方向信息。
在平方误差回归中,负梯度与普通残差很接近,所以很多入门材料会说:
新树在拟合残差。
这个说法便于理解,但更一般的表述是:
新树尝试拟合损失函数相对于当前预测值的负梯度。
不同损失函数会产生不同梯度,因此同一批数据使用不同目标函数,最终学到的树也可能不同。
6.4 学习率有什么作用
假设新树认为某条样本的分数应该增加 0.20。
当学习率为 1.0:
实际增加 0.20
当学习率为 0.05:
实际只增加 0.01
因此通常存在这样的权衡:
较小学习率 + 较多树
较大学习率 + 较少树
较小学习率通常让每一步更谨慎,但需要更多迭代和更长训练时间。较大学习率训练更快,但可能较早过拟合,或者跨过较好的解。
6.5 Boosting 与随机森林的根本区别
| 对比项 | 随机森林 | 梯度提升树 |
|---|---|---|
| 树之间关系 | 相对独立 | 按顺序依赖 |
| 核心思想 | 多棵树平均,降低方差 | 后一棵树纠正前面误差 |
| 训练方式 | 更容易并行 | 迭代式训练 |
| 参数敏感度 | 相对较低 | 通常更需要调参 |
| 表格预测上限 | 常作为稳健基线 | 经常能取得更高精度 |
随机森林并不是“低级版本”,它在噪声较大、小数据和需要快速稳健基线时仍然很有价值。官方定义中,随机森林通过在不同子样本上训练多棵决策树并进行平均,提高预测精度并控制过拟合。[^12]
七、损失函数:模型如何知道自己错了
损失函数是训练的“计分规则”。没有损失函数,模型就不知道什么叫预测得更好。
7.1 二分类中的 Logloss
假设真实标签是 1。
模型 A 预测:
正类概率:0.90
模型 B 预测:
正类概率:0.55
两者按照 0.5 阈值都判为正类,但模型 A 给真实答案更高概率,因此 Logloss 更低。
二分类 Logloss 可以写成:
[
L=-\left[y\log(p)+(1-y)\log(1-p)\right]
]
其中:
- (y) 是真实标签,取 0 或 1;
- (p) 是模型预测为 1 的概率。
Logloss 的重要特点是:
对“非常自信但完全错误”的预测惩罚很重。
例如真实标签是 1:
预测 0.90:损失较小
预测 0.55:损失中等
预测 0.01:损失非常大
7.2 多分类中的 MultiClass
多分类模型通常为每个类别产生一个原始分数,再通过 Softmax 转换成概率:
[
p_k=\frac{e^}{\sum_j e^}
]
其中:
- (s_k) 是第 (k) 类的原始分数;
- (p_k) 是第 (k) 类的概率。
所有类别概率之和为 1。
7.3 回归中的 RMSE 与 MAE
RMSE
[
RMSE=\sqrt{\frac{1}\sum_^(\hat-y_i)^2}
]
因为误差被平方,大误差受到更重惩罚。
MAE
[
MAE=\frac{1}\sum_^|\hat-y_i|
]
MAE 更直观,也比 RMSE 对极端误差不那么敏感。
使用哪个目标,取决于业务对大误差的容忍程度。
7.4 loss_function 与 eval_metric 不完全相同
CatBoost 中常见两个参数:
loss_function
eval_metric
loss_function:训练时真正优化的目标;eval_metric:验证集上用于早停和最佳模型选择的指标。
此外还有:
custom_metric
它只负责在训练过程中额外显示指标,不一定参与优化。CatBoost 官方文档明确区分了训练目标、验证指标和仅用于监控的自定义指标。[^6]
初学阶段,建议先让 loss_function 和 eval_metric 保持一致,减少不必要的复杂性。
八、类别特征为什么难处理
类别特征是 CatBoost 最有代表性的优势来源。
8.1 什么是类别特征
类别特征表示不同种类,而不是连续大小。
例如:
城市:北京、上海、广州
会员等级:普通、白银、黄金、白金
设备:Android、iPhone、Windows、Mac
获客渠道:搜索、广告、推荐、线下
城市可以用数字 ID 存储:
北京 = 101
上海 = 205
广州 = 309
但这不意味着:
广州 > 上海 > 北京
这些数字只是标识符,不是具有大小关系的测量值。
8.2 错误方式一:直接把类别 ID 当数值
假设:
北京 = 1
上海 = 2
广州 = 3
深圳 = 4
树模型可能建立:
CityId < 2.5
这会把北京、上海分到一边,把广州、深圳分到另一边。
问题是,这种分组只由任意编号决定。换一套编号后,模型结构也会变化。
因此:
纯标识符即使看起来是整数,也通常应该作为类别,而不是连续数值。
8.3 传统方式二:One-Hot Encoding
One-Hot 会把一个类别列拆成多个 0/1 列。
原始数据:
| City |
|---|
| 北京 |
| 上海 |
| 广州 |
编码后:
| City_北京 | City_上海 | City_广州 |
|---|---|---|
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 1 |
当类别很少时,One-Hot 简单有效。
但如果存在:
5万个商品
20万个用户群组
1万个设备型号
数千个地区
One-Hot 会制造大量稀疏列,增加内存、训练时间和工程复杂度。
更重要的是,One-Hot 只能直接表达“是不是这个类别”,很难自动概括:
这个类别在历史上更接近什么行为模式?
这个类别与另一个类别组合时有什么规律?
CatBoost 官方建议在使用原生类别特征时,不要在预处理阶段手工完成 One-Hot;低基数类别是否采用 One-Hot,可以由 CatBoost 内部参数控制。[^3]
8.4 传统方式三:频次编码
频次编码把类别替换为其出现频率。
例如:
北京出现 20,000 次 → 0.20
上海出现 18,000 次 → 0.18
某小城市出现 100 次 → 0.001
它能表达类别是否常见,但无法直接表达该类别与标签的关系。
两个城市可能出现频率相同,流失率却完全不同。
8.5 传统方式四:目标编码
目标编码会使用标签统计类别。
例如:
| 城市 | 客户数 | 流失客户数 | 流失率 |
|---|---|---|---|
| 北京 | 10000 | 1800 | 18% |
| 上海 | 9000 | 1260 | 14% |
| 某小城市 | 2 | 2 | 100% |
然后用城市流失率替换城市名称。
这比简单编号包含更多信息,但有两个严重问题。
问题一:目标泄漏
如果计算某一行的城市流失率时,使用了这一行自己的标签,就相当于让答案参与构造输入。
问题二:低频类别极端化
某个类别只有一条样本,而且恰好流失:
流失率 = 1 / 1 = 100%
模型可能把这个偶然结果当成强规律。
因此,普通全量目标编码很容易让训练指标虚高、测试指标下降。
九、CatBoost 如何处理类别特征
CatBoost 不要求把类别简单编号成连续数值。它会在选择树分裂之前,将类别特征及其组合转换成多种数值统计特征。官方文档把这类统计通常称为 Ctr。[^3]
需要注意:
这里的 Ctr 是 CatBoost 内部类别统计的统称,不应机械地理解为广告业务中的点击率。
9.1 有序统计的直观思想
假设训练数据被随机排列为:
| 排列位置 | 城市 | 是否流失 |
|---|---|---|
| 1 | 北京 | 0 |
| 2 | 上海 | 1 |
| 3 | 北京 | 1 |
| 4 | 北京 | 0 |
| 5 | 上海 | 0 |
为第 4 条“北京”样本构造类别统计时,只使用它前面已经出现的北京样本:
位置1:北京,标签0
位置3:北京,标签1
不会使用第 4 条自己的标签,也不会使用后面位置 5 的数据。
于是,第 4 条样本看到的是一种“过去统计”,而不是包含自己答案的全量统计。
9.2 一个便于理解的示意公式
有序目标统计可以用下面的平滑形式帮助理解:
[
CTR_i=
\frac{
\sum_{j<i}\mathbf{1}(x_j=x_i)y_j + \alpha P
}{
\sum_{j<i}\mathbf{1}(x_j=x_i)+\alpha
}
]
其中:
- (x_i) 是当前样本的类别;
- (j<i) 表示只使用排列中当前样本之前的记录;
- (y_j) 是历史标签;
- (P) 是先验值;
- (\alpha) 控制先验强度。
这是便于理解的通用示意,并非对 CatBoost 所有 Ctr 类型和工程实现的完整复刻。CatBoost 会根据任务、标签类型、CPU/GPU 和 Ctr 配置生成不同统计。[^3]
9.3 为什么要加入先验和平滑
假设整体流失率为 20%。
某个新城市第一次出现,历史样本为 0 条。此时模型不能直接计算:
流失数 / 总数
即使这个城市只出现一次并且流失,也不应该立刻得到 100% 的极端估计。
先验和平滑表达了一个合理原则:
样本很少时,多相信整体平均;
样本逐渐增多后,再更多相信该类别自己的历史。
例如,在简化示意中:
整体先验:20%
该城市:1条样本,且流失
平滑后可能得到远低于100%的统计值
这能降低稀有类别带来的高方差。
9.4 为什么使用多个排列
单一排列会让结果受到样本顺序影响。
例如某个类别的第一条记录没有历史,第二条只有一条历史,越靠后可用历史越多。
CatBoost 会使用排列机制,并在内部训练过程中构造相应统计,从而降低对某一个固定顺序的依赖。官方算法说明中明确包含对训练对象进行排列,并基于当前对象之前的历史构造类别数值特征。[^3]
9.5 类别组合
CatBoost 不只可以使用单个类别:
城市
设备类型
会员等级
还可以形成组合:
城市 + 设备类型
会员等级 + 获客渠道
城市 + 会员等级 + 设备类型
例如:
“上海”本身是一种信息;
“上海 + iPhone”是更具体的信息;
“上海 + iPhone + 黄金会员”又是更细的组合。
组合类别可以发现单个特征无法表达的交互,但组合阶数越高,也会带来:
- 类别基数快速增长;
- 稀有组合增多;
- 内存和模型体积增加;
- 训练速度下降;
- 过拟合风险上升。
max_ctr_complexity 用来限制最多组合多少个类别特征。官方参数说明将其定义为可组合特征的最大数量。[^4]
9.6 低基数类别与 One-Hot
并不是所有类别都必须构造复杂统计。
例如:
是否自动续费:是、否
设备大类:移动端、桌面端
用户状态:新用户、老用户
这类低基数特征可能适合 One-Hot。
CatBoost 可以通过 one_hot_max_size 控制:当类别唯一值数量不超过阈值时,使用内部 One-Hot;超过阈值时,使用类别统计。不同任务和设备的默认行为可能不同,因此不要死记默认值,应查看实际参数。[^4]
9.7 训练时应该怎样提供类别列
以 Pandas 为例,建议:
categorical_features = [
"City",
"MembershipLevel",
"DeviceType",
]
for column in categorical_features:
df[column] = (
df[column]
.astype("string")
.fillna("__MISSING__")
.astype(str)
)
关键原则:
- 类别值使用稳定字符串或整数标识;
- 训练与预测使用完全一致的表示;
- 不要让同一个类别有时写成
1,有时写成1.0; - 不要让缺失值随机出现
NULL、None、N/A、空字符串等多种形式; - 不要在 CatBoost 之前做包含全量标签的普通目标编码。
十、Ordered Boosting 到底是什么
这是 CatBoost 最容易被误解的概念之一。
需要先明确:
有序类别统计 ≠ Ordered Boosting
它们都使用“有序”思想,也都服务于降低泄漏和预测偏移,但解决的是两个不同环节的问题。CatBoost 原始论文把有序类别处理和 Ordered Boosting 作为两项核心技术。[^2]
10.1 有序类别统计解决什么问题
它解决的是:
构造类别目标统计时,怎样避免当前样本的标签直接参与当前样本的特征计算?
也就是前一章讨论的问题。
10.2 Ordered Boosting 解决什么问题
传统梯度提升中,模型在训练样本上计算预测和梯度,然后继续使用同一批样本训练后续树。
因为当前模型本身已经用过这些训练样本,所以训练样本上的梯度估计与真正未见样本上的梯度分布可能存在系统差异。CatBoost 论文把这种现象讨论为一种 prediction shift。
Ordered Boosting 的概念目标是:
为某个训练样本计算当前预测和梯度时,尽量使用一个没有训练过该样本本身的历史模型。
10.3 直观例子
假设样本排列为:
样本1、样本2、样本3、样本4、样本5
在概念上:
预测样本1:没有更早样本
预测样本2:使用样本1形成的历史模型
预测样本3:使用样本1、2形成的历史模型
预测样本4:使用样本1、2、3形成的历史模型
这样,样本4在计算梯度时,尽量模拟“它是尚未见过的新数据”。
真实 CatBoost 实现为了效率使用了更复杂的工程优化,不能把它简单理解为真的为每条样本独立完整训练一套模型,但上述比喻能够表达它的统计动机。
10.4 Plain 与 Ordered
CatBoost 支持不同 Boosting 方案,其中常见的是:
Plain
Ordered
Plain更接近常规梯度提升流程;Ordered使用有序思想降低预测偏移,但通常计算开销更大。
具体可用性、默认值和行为会受任务、样本规模、CPU/GPU 等条件影响。不要认为“只要模型叫 CatBoost,就一定启用了 Ordered Boosting”。应明确检查训练参数,必要时通过 get_all_params() 查看实际生效配置。[^4]
10.5 has_time 不是时间验证的替代品
CatBoost 的 has_time=True 表示使用输入数据已有顺序,在类别数值转换和树结构选择阶段不再执行随机排列;如果提供了 Timestamp 列,也可由时间戳确定顺序。[^4]
但它不能替代:
按时间构造特征
按时间划分训练集和测试集
防止未来数据进入历史样本
正确顺序应该是:
先保证数据无未来泄漏
→ 再进行时间切分
→ 最后决定是否使用 has_time
仅仅设置 has_time=True,不会自动修复使用未来统计、未来状态或未来标签构造的错误特征。
十一、CatBoost 的对称树
CatBoost 默认常用的树生长策略是 SymmetricTree,也称对称树或 Oblivious Tree。官方文档说明,对称树逐层建立,同一层的所有叶子使用相同分裂条件。[^4]
11.1 普通非对称树
普通树的不同节点可以使用不同条件:
第1层:最近登录间隔 > 30?
左侧第2层:订单数 < 2?
右侧第2层:会员等级 = 普通?
不同路径可以完全不同,因此结构灵活。
11.2 对称树
对称树在同一层使用同一个条件:
第1层所有样本:最近登录间隔 > 30?
第2层所有分支:近90天订单数 < 2?
第3层所有分支:是否自动续费?
深度为 3 时,每条样本经过同样三项判断,根据三个判断的真假组合落入一个叶子。
11.3 深度与叶子数量
完整对称树深度为 (d) 时,叶子数通常是:
[
2^d
]
| 深度 | 叶子数 |
|---|---|
| 4 | 16 |
| 6 | 64 |
| 8 | 256 |
| 10 | 1024 |
| 12 | 4096 |
因此,深度从 6 增加到 10,不是只多 4 个判断,而是把单棵完整树的叶子数从 64 提高到 1024。
这就是 depth 对复杂度影响非常大的原因。
11.4 对称树的优点
结构规整
同一层使用同一条件,模型结构容易高效存储和执行。
推理速度快
预测时只需按照每层条件构造一个叶子索引。官方调参文档指出,对称树的应用速度可显著快于非对称树,在部分比较中大约可达到一个数量级的差异。[^4]
带来一定结构约束
对称结构限制了树在某个局部区域无限自由生长,有时有助于减少过拟合。
11.5 对称树的局限
真实数据的不同区域可能需要完全不同的规则。
例如:
新客户主要受获客渠道影响;
老客户主要受活跃度和消费变化影响;
企业客户主要受合同周期影响。
对称树要求同一层共享条件,有时不如非对称树灵活。
CatBoost 还支持:
Depthwise:逐层生长,但不同叶子可选择不同最佳条件;Lossguide:每次优先分裂收益最大的叶子。
初学者建议先从默认的 SymmetricTree 建立可靠基线,再用同一验证方案比较其他策略,而不是预先断定某一种结构必然最好。[^4]
十二、数值量化与缺失值处理
12.1 什么是数值量化
树模型需要为数值特征寻找分裂阈值。
假设特征是订单金额:
21.3、32.8、45.0、47.6、63.2、88.9、102.5……
CatBoost 会先为连续数值构造若干边界:
30、50、80、100……
然后树可以使用:
订单金额 < 50
订单金额 < 80
这个将连续数值映射到离散区间的过程叫作量化 Quantization。
border_count 控制数值特征可使用的分裂边界数量。官方文档说明其默认值会因 CPU、GPU 和任务而变化,因此不应脱离运行环境死记一个默认数字。[^4]
12.2 为什么通常不需要标准化
线性模型、KNN 和神经网络经常需要 StandardScaler 或 MinMaxScaler,因为数值尺度会影响距离、梯度或优化。
树模型主要判断:
特征是否大于某个阈值
因此,下面两种表示通常产生相同的排序关系:
收入:10000、20000、30000
收入(万元):1、2、3
所以 CatBoost 通常不要求对普通数值特征做标准化。
但这不代表数值预处理完全没有价值。下面这些处理仍可能有意义:
- 修正单位错误;
- 截断明显错误值;
- 对长尾目标选择对数变换;
- 构造有业务含义的比例和变化率;
- 保持训练与推理数值口径一致。
12.3 数值缺失值
CatBoost 可以直接识别数值缺失。官方支持:
Forbidden:不允许缺失
Min:把缺失视为小于所有正常值
Max:把缺失视为大于所有正常值
默认模式通常为 Min,并会考虑把缺失值与其他值分开的候选分裂。[^5]
因此,不要没有依据地把所有数值缺失填成 0。
例如:
收入 = 0
可能表示真实收入为 0,也可能表示没有采集到收入。如果把缺失填成 0,模型无法区分这两种情况。
常见处理方式是:
保留 NaN
必要时再增加一个缺失标记:
Income
HasIncome
这样模型既能看到收入值,也能看到数据是否存在。
12.4 类别缺失值
官方文档指出,类别特征不会像数值特征那样使用专门的 Min/Max 缺失处理。[^5]
工程上建议把类别缺失统一成稳定字符串:
__MISSING__
例如:
df["DeviceType"] = (
df["DeviceType"]
.astype("string")
.fillna("__MISSING__")
.astype(str)
)
最重要的是训练和预测完全一致。
下面这些值会被视为不同类别:
NULL
None
N/A
空字符串
__MISSING__
如果训练时用 __MISSING__,线上预测却传空字符串,模型看到的就是另一个类别。
十三、完整而可靠的模型训练流程
真正的模型项目不是“读取 CSV 后执行 fit()”。一套可靠流程至少包括以下阶段。
13.1 第一步:明确业务问题
必须先写清楚:
预测对象是谁?
预测时点是什么?
预测未来多长时间?
输出是类别、概率还是连续数值?
模型结果将被怎样使用?
例如:
在每周一凌晨,预测当前活跃客户未来30天是否会流失。
这句话决定:
- 一条样本是什么;
- 标签怎样定义;
- 哪些特征在预测时可用;
- 数据怎样按时间切分;
- 模型输出何时失效。
13.2 第二步:定义标签
“流失”必须有明确口径:
未来30天没有登录?
未来30天没有下单?
主动注销?
订阅到期后未续费?
标签定义不清,模型再复杂也没有意义。
标签还需要避免使用不可稳定获得的信息。例如人工运营人员事后标注的模糊状态,可能难以长期复现。
13.3 第三步:确定预测时点可用特征
对于每一个特征都要问:
在真正执行预测的那个时刻,这个值是否已经存在?
常见泄漏包括:
使用未来信息
预测 1 月 1 日客户是否会在未来 30 天流失,却使用了 1 月 15 日产生的客服记录。
使用事后更新字段
数据库中的客户状态被后续流程覆盖,回看历史时看到的是最新状态,而不是当时状态。
使用全周期聚合
预测某个历史日期,却使用了截至今天计算的累计订单数。
使用标签衍生字段
某个字段本质上由流失结果生成,例如“流失原因”。
13.4 第四步:建立数据快照
可靠训练最好使用不可变的数据快照:
dataset_2026_09_02.parquet
同时记录:
- SQL 或数据生成代码版本;
- 数据行数;
- 时间范围;
- 标签分布;
- 特征列表;
- 缺失率;
- 文件哈希;
- 数据生成时间。
否则以后很难解释:
为什么同一套代码重新训练后结果变了?
13.5 第五步:检查数据质量
至少检查:
主键是否重复
标签是否缺失
类别是否异常膨胀
数值是否超出合理范围
训练与测试字段类型是否一致
是否存在常量列
是否存在几乎唯一的ID列
是否存在异常高缺失率
是否存在同一实体跨集合重复
不要认为树模型“什么脏数据都能吃”。
13.6 第六步:正确划分数据
独立同分布数据
如果样本相互独立、没有明显时间变化,可以使用随机分层切分:
训练集 70%
验证集 15%
测试集 15%
时间数据
如果目标用于预测未来,应按时间切分:
较早数据 → 训练
较新数据 → 验证
最新数据 → 测试
重复实体数据
如果同一客户、设备、商户或患者会出现多次,应考虑按实体分组切分,避免同一实体同时进入训练和测试。
时间与实体同时存在
有时需要同时满足:
测试时间晚于训练时间
同一实体不会以高度重复形式泄漏
这通常比随机切分复杂,但更接近真实上线环境。
13.7 第七步:建立简单基线
在训练 CatBoost 之前,先建立至少一个简单基线:
永远预测多数类
逻辑回归
简单规则
随机森林
历史平均值
如果复杂模型不能稳定超过简单基线,就需要检查:
- 标签是否可预测;
- 特征是否有信息;
- 数据是否泄漏;
- 指标是否选错;
- 测试集是否过小。
13.8 第八步:训练第一版 CatBoost
第一版的目标不是追求极限,而是建立可复现基线:
固定数据快照
固定特征列表
固定随机种子
固定切分方式
使用验证集
启用早停
保存参数和日志
13.9 第九步:分析错误
不能只看一个总分。
分类模型至少分析:
- 哪一类最容易错;
- 哪些人群召回率低;
- 哪些概率区间不准;
- 哪些月份或渠道退化;
- 错误是否集中在缺失数据较多的样本。
回归模型至少分析:
- 大值是否系统低估;
- 小值是否系统高估;
- 哪些地区误差最大;
- 误差是否随时间扩大。
13.10 第十步:最终测试与上线监控
参数和特征基本确定后,再使用测试集进行最终评估。
上线后还要监控:
输入特征分布漂移
类别新值比例
缺失率变化
预测概率分布
实际指标变化
模型延迟
错误率
离线测试合格不等于模型可以永久有效。业务环境、用户结构和数据采集方式都会变化。
十四、第一次 CatBoost 分类训练:完整 Python 示例
下面使用虚构的客户流失数据演示二分类训练。
14.1 安装依赖
python -m pip install -U catboost pandas scikit-learn
CatBoost 官方 Python 包可以通过 pip install catboost 安装。[^1]
14.2 假设数据字段
customer_churn.csv 包含:
| 字段 | 类型 | 含义 |
|---|---|---|
| CustomerId | 标识 | 客户ID,不参与训练 |
| City | 类别 | 城市 |
| MembershipLevel | 类别 | 会员等级 |
| DeviceType | 类别 | 常用设备 |
| AcquisitionChannel | 类别 | 获客渠道 |
| Age | 数值 | 年龄 |
| TenureDays | 数值 | 注册天数 |
| OrderCount90D | 数值 | 近90天订单数 |
| AvgOrderValue90D | 数值 | 近90天平均订单金额 |
| DaysSinceLastLogin | 数值 | 距最近登录天数 |
| SupportTickets30D | 数值 | 近30天客服工单数 |
| IsAutoRenew | 数值 | 是否自动续费,0/1 |
| IsChurned | 标签 | 是否流失,0/1 |
14.3 完整代码
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
from catboost import CatBoostClassifier, Pool
from sklearn.metrics import (
accuracy_score,
brier_score_loss,
confusion_matrix,
f1_score,
log_loss,
precision_score,
recall_score,
roc_auc_score,
)
from sklearn.model_selection import train_test_split
DATA_PATH = Path("customer_churn.csv")
OUTPUT_DIR = Path("artifacts")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
TARGET_COLUMN = "IsChurned"
ID_COLUMN = "CustomerId"
CATEGORICAL_FEATURES = [
"City",
"MembershipLevel",
"DeviceType",
"AcquisitionChannel",
]
NUMERICAL_FEATURES = [
"Age",
"TenureDays",
"OrderCount90D",
"AvgOrderValue90D",
"DaysSinceLastLogin",
"SupportTickets30D",
"IsAutoRenew",
]
FEATURE_COLUMNS = CATEGORICAL_FEATURES + NUMERICAL_FEATURES
# 1. 读取数据
if not DATA_PATH.exists():
raise FileNotFoundError(f"找不到数据文件:{DATA_PATH.resolve()}")
df = pd.read_csv(DATA_PATH)
# 2. 检查必要字段
required_columns = {
TARGET_COLUMN,
ID_COLUMN,
*FEATURE_COLUMNS,
}
missing_columns = sorted(required_columns.difference(df.columns))
if missing_columns:
raise ValueError(f"数据缺少必要字段:{missing_columns}")
# 3. 检查标签
if df[TARGET_COLUMN].isna().any():
raise ValueError("标签列存在缺失值。")
invalid_labels = sorted(
set(df[TARGET_COLUMN].unique()).difference({0, 1})
)
if invalid_labels:
raise ValueError(f"标签必须为0或1,发现异常值:{invalid_labels}")
df[TARGET_COLUMN] = df[TARGET_COLUMN].astype(int)
# 4. 统一类别特征表示
for column in CATEGORICAL_FEATURES:
df[column] = (
df[column]
.astype("string")
.fillna("__MISSING__")
.astype(str)
)
# 5. 数值列转为数值类型;无法转换的值变为NaN
for column in NUMERICAL_FEATURES:
df[column] = pd.to_numeric(df[column], errors="coerce")
# 6. 划分训练集、验证集、测试集
# 这个示例假设每名客户只有一条独立记录,因此使用分层随机切分。
# 对时间预测、重复客户、设备记录等数据,应改用时间切分或分组切分。
train_valid_df, test_df = train_test_split(
df,
test_size=0.15,
random_state=42,
stratify=df[TARGET_COLUMN],
)
train_df, valid_df = train_test_split(
train_valid_df,
test_size=0.1764705882, # 约等于总数据的15%
random_state=42,
stratify=train_valid_df[TARGET_COLUMN],
)
# 7. 构造Pool
train_pool = Pool(
data=train_df[FEATURE_COLUMNS],
label=train_df[TARGET_COLUMN],
cat_features=CATEGORICAL_FEATURES,
feature_names=FEATURE_COLUMNS,
)
valid_pool = Pool(
data=valid_df[FEATURE_COLUMNS],
label=valid_df[TARGET_COLUMN],
cat_features=CATEGORICAL_FEATURES,
feature_names=FEATURE_COLUMNS,
)
test_pool = Pool(
data=test_df[FEATURE_COLUMNS],
label=test_df[TARGET_COLUMN],
cat_features=CATEGORICAL_FEATURES,
feature_names=FEATURE_COLUMNS,
)
# 8. 定义模型
model = CatBoostClassifier(
loss_function="Logloss",
eval_metric="Logloss",
custom_metric=["AUC", "Accuracy", "F1", "BrierScore"],
iterations=3000,
learning_rate=0.03,
depth=6,
l2_leaf_reg=5,
random_strength=1,
od_type="Iter",
od_wait=150,
use_best_model=True,
random_seed=42,
task_type="CPU",
thread_count=-1,
verbose=100,
allow_writing_files=False,
)
# 9. 训练
model.fit(
train_pool,
eval_set=valid_pool,
)
# 10. 测试集预测
positive_probabilities = model.predict_proba(test_pool)[:, 1]
predicted_labels = (positive_probabilities >= 0.5).astype(int)
actual_labels = test_df[TARGET_COLUMN].to_numpy()
# 11. 计算指标
metrics = {
"best_iteration": int(model.get_best_iteration()),
"tree_count": int(model.tree_count_),
"accuracy": float(
accuracy_score(actual_labels, predicted_labels)
),
"precision": float(
precision_score(
actual_labels,
predicted_labels,
zero_division=0,
)
),
"recall": float(
recall_score(
actual_labels,
predicted_labels,
zero_division=0,
)
),
"f1": float(
f1_score(
actual_labels,
predicted_labels,
zero_division=0,
)
),
"roc_auc": float(
roc_auc_score(actual_labels, positive_probabilities)
),
"logloss": float(
log_loss(actual_labels, positive_probabilities)
),
"brier_score": float(
brier_score_loss(actual_labels, positive_probabilities)
),
"confusion_matrix": confusion_matrix(
actual_labels,
predicted_labels,
).tolist(),
}
print(json.dumps(metrics, ensure_ascii=False, indent=2))
# 12. 保存逐条预测
prediction_df = pd.DataFrame({
ID_COLUMN: test_df[ID_COLUMN].to_numpy(),
"Actual": actual_labels,
"Predicted": predicted_labels,
"Probability": positive_probabilities,
})
prediction_df.to_csv(
OUTPUT_DIR / "test_predictions.csv",
index=False,
encoding="utf-8-sig",
)
# 13. 保存特征重要性
importance_df = pd.DataFrame({
"Feature": FEATURE_COLUMNS,
"Importance": model.get_feature_importance(
type="FeatureImportance"
),
}).sort_values("Importance", ascending=False)
importance_df.to_csv(
OUTPUT_DIR / "feature_importance.csv",
index=False,
encoding="utf-8-sig",
)
# 14. 保存原生模型
model_path = OUTPUT_DIR / "customer_churn_catboost.cbm"
model.save_model(model_path)
# 15. 保存模型元数据
metadata = {
"model_type": "CatBoostClassifier",
"target": TARGET_COLUMN,
"feature_columns": FEATURE_COLUMNS,
"categorical_features": CATEGORICAL_FEATURES,
"numerical_features": NUMERICAL_FEATURES,
"missing_category_token": "__MISSING__",
"classification_threshold": 0.5,
"metrics": metrics,
"parameters": model.get_all_params(),
}
with open(
OUTPUT_DIR / "model_metadata.json",
"w",
encoding="utf-8",
) as file:
json.dump(
metadata,
file,
ensure_ascii=False,
indent=2,
)
print(f"模型已保存到:{model_path.resolve()}")
14.4 这段代码最重要的不是参数
真正值得学习的是流程:
检查字段
→ 检查标签
→ 统一类别字符串
→ 保留数值NaN
→ 独立切分训练、验证、测试
→ 使用Pool声明类别列
→ 验证集早停
→ 测试集一次性评估
→ 保存概率、模型、参数和元数据
这套流程比盲目寻找所谓“最佳参数”更重要。
14.5 对新样本执行预测
import pandas as pd
from catboost import CatBoostClassifier, Pool
model = CatBoostClassifier()
model.load_model("artifacts/customer_churn_catboost.cbm")
categorical_features = [
"City",
"MembershipLevel",
"DeviceType",
"AcquisitionChannel",
]
feature_columns = [
"City",
"MembershipLevel",
"DeviceType",
"AcquisitionChannel",
"Age",
"TenureDays",
"OrderCount90D",
"AvgOrderValue90D",
"DaysSinceLastLogin",
"SupportTickets30D",
"IsAutoRenew",
]
new_customer = pd.DataFrame([{
"City": "上海",
"MembershipLevel": "普通会员",
"DeviceType": "Android",
"AcquisitionChannel": "搜索",
"Age": 31,
"TenureDays": 120,
"OrderCount90D": 1,
"AvgOrderValue90D": 96.5,
"DaysSinceLastLogin": 38,
"SupportTickets30D": 2,
"IsAutoRenew": 0,
}])
for column in categorical_features:
new_customer[column] = (
new_customer[column]
.astype("string")
.fillna("__MISSING__")
.astype(str)
)
prediction_pool = Pool(
data=new_customer[feature_columns],
cat_features=categorical_features,
feature_names=feature_columns,
)
churn_probability = model.predict_proba(prediction_pool)[0, 1]
print(f"流失概率:{churn_probability:.4f}")
线上预测必须保证:
- 特征名称一致;
- 特征顺序一致;
- 类别列类型一致;
- 缺失值口径一致;
- 数值单位一致;
- 生成时点一致。
十五、CatBoost 回归模型示例
假设要预测房屋成交价格,类别特征包括:
City
District
PropertyType
DecorationLevel
数值特征包括:
Area
Floor
BuildingAge
DistanceToSubway
RoomCount
示例:
import pandas as pd
from catboost import CatBoostRegressor, Pool
from sklearn.metrics import mean_absolute_error, mean_squared_error
train_df = pd.read_csv("house_train.csv")
valid_df = pd.read_csv("house_valid.csv")
test_df = pd.read_csv("house_test.csv")
categorical_features = [
"City",
"District",
"PropertyType",
"DecorationLevel",
]
feature_columns = [
"City",
"District",
"PropertyType",
"DecorationLevel",
"Area",
"Floor",
"BuildingAge",
"DistanceToSubway",
"RoomCount",
]
target = "SalePrice"
for frame in (train_df, valid_df, test_df):
for column in categorical_features:
frame[column] = (
frame[column]
.astype("string")
.fillna("__MISSING__")
.astype(str)
)
train_pool = Pool(
train_df[feature_columns],
train_df[target],
cat_features=categorical_features,
)
valid_pool = Pool(
valid_df[feature_columns],
valid_df[target],
cat_features=categorical_features,
)
test_pool = Pool(
test_df[feature_columns],
test_df[target],
cat_features=categorical_features,
)
model = CatBoostRegressor(
loss_function="RMSE",
eval_metric="RMSE",
custom_metric=["MAE", "R2"],
iterations=4000,
learning_rate=0.03,
depth=7,
l2_leaf_reg=8,
random_seed=42,
od_type="Iter",
od_wait=200,
use_best_model=True,
verbose=100,
allow_writing_files=False,
)
model.fit(train_pool, eval_set=valid_pool)
predictions = model.predict(test_pool)
mae = mean_absolute_error(test_df[target], predictions)
rmse = mean_squared_error(
test_df[target],
predictions,
) ** 0.5
print(f"MAE: {mae:.2f}")
print(f"RMSE: {rmse:.2f}")
model.save_model("house_price_catboost.cbm")
如果房价分布极度长尾,也可以研究:
对目标做 log1p 变换
使用 MAE、Quantile 等目标
按城市或价格区间分层评估
但任何目标变换都要在预测后正确反变换,并在原始业务尺度上评估。
十六、CatBoost 命令行训练示例
CatBoost 提供 CLI,可以直接通过命令行训练和预测,适合批处理、定时任务和非 Python 主业务系统。[^1]
16.1 数据列示例
假设 train.tsv 的列顺序为:
IsChurned
CustomerId
City
MembershipLevel
DeviceType
AcquisitionChannel
Age
TenureDays
OrderCount90D
AvgOrderValue90D
DaysSinceLastLogin
SupportTickets30D
IsAutoRenew
16.2 Column Description 文件
建立 customer_churn.cd:
0 Label IsChurned
1 SampleId CustomerId
2 Categ City
3 Categ MembershipLevel
4 Categ DeviceType
5 Categ AcquisitionCha
