工具实战
聚焦量化样本表的输入规范,说明 CatBoost 在类别特征、缺失值和时间切分上的正确处理方式,避免随机切分与错误编码造成验证失真。
第 1 讲已经说明了 CatBoost 为什么适合量化建模:它对类别特征友好、对缺失值容忍度高、在中小样本里通常比一味堆深网络更稳。但真正落地时,模型是否好用,往往不是先败在参数,而是先败在输入。很多团队把日频因子表、行业标签、停牌空值、未来收益标签直接拼进一个 DataFrame,就开始训练,结果不是线下分数虚高,就是上线后稳定性明显下滑。
本讲只解决一个问题:在量化场景里,怎样把 CatBoost 的输入表整理成一份可以训练、可以复现、也能避免未来函数的样本表。
你需要完成三件事:
如果这三件事没有先做对,后面的参数调优和早停策略基本都会建立在偏掉的样本上。
CatBoost 的一个常见误解是:它既然“原生支持类别特征”和“自动处理缺失值”,那输入就可以很随意。这个理解只对了一半。CatBoost 确实能减少大量手工预处理工作,但它并不会替你判断列的业务含义,也不会替你识别时间穿越。
在量化任务里,最常见的三个输入错误是:
这三个问题里,前两个会让模型学到错误结构,第三个会直接污染验证结论。对 CatBoost 来说,输入规范不是“训练前整理一下表格”,而是模型可信度的第一道门槛。
先把目标压缩到最小。假设我们做的是日频横截面收益预测,每一行代表某只股票在某个交易日可见的一组特征,对应未来 5 个交易日收益标签。那一张最小可训练样本表通常至少包含以下字段:
trade_date:样本所属交易日。instrument:证券代码或内部资产 ID。industry_lv1:一级行业标签。is_st、is_suspend:交易状态类标签。turnover_20d、volatility_20d、momentum_60d:连续数值特征。earnings_flag、report_quarter:事件或财报类离散特征。label_ret_5d:未来 5 日收益标签。这里有一个关键约束:特征列必须全部来自 trade_date 当天收盘前或当日可确认的数据,标签列必须全部来自未来窗口。 任何把未来回看值混进特征的做法,都会让后面所有验证失去意义。
如果你的表里现在还混着“未来收益滚动均值”“未来成交量变化率”这类字段,先不要训练,先删掉。CatBoost 再稳,也救不了标签泄露。
在量化数据里,不是所有“看起来不是小数”的列都该进 cat_features。一个更稳的判断方式是:这列是否代表离散身份,而不是数值大小。
通常适合交给 CatBoost 作为类别特征的列包括:
bull、bear、sideways。通常不应该当类别特征处理的列包括:
一个常见坑是把证券代码直接转成整数,例如把 600519.SH 编成 1057,然后当数值列输入。这会让模型错误地把“1057 比 18 大”理解成有意义的连续关系。对 CatBoost 来说,代码、行业、交易状态这类列最适合保留成字符串或显式声明为类别列。
很多人在使用树模型时习惯先做 one-hot 编码,但在 CatBoost 里,这往往不是最佳起点。CatBoost 的优势之一,就是它能直接接收类别列并在内部做有序统计变换。对量化任务来说,这有两个现实好处:
真正该做的不是“先独热”,而是“先保真”:
如果你确实要做分桶,比如把市值分成 small、mid、large 三类,也应该在训练规则里固定分桶边界,再把同样规则应用到验证集和测试集,而不是每一段数据各自按分位数重切一次。否则模型学到的就不是稳定语义,而是每一段样本内部的相对排名。
CatBoost 能处理缺失值,不等于所有空值都应该原封不动交给它。量化数据里的缺失通常至少分成三类:
前两类可以保留,但你最好知道它们为什么空。第三类如果不先排查,就会把数据问题伪装成模型鲁棒性。
一个稳妥做法是:
listed_days。xxx_missing_flag。如果某列缺失率在少数日期突然飙升,例如平时只有 2%,某几天突然变成 70%,这通常不是“CatBoost 会自动处理”的问题,而是你该回头检查 ETL 或数据源的信号。
量化任务和普通表格任务最大的区别之一,就是样本顺序不能随便打散。随机切分在用户画像、风控审批之类任务里可能还成立,但在收益预测、因子建模、事件驱动策略里,随机切分几乎一定会高估效果。
正确原则很简单:
例如:
2021-01-01 到 2023-12-312024-01-01 到 2024-06-302024-07-01 到 2024-12-31如果你把 2024 年的数据随机抽一部分回到训练集中,再拿剩余 2024 年样本做验证,那么行业结构、市场状态、事件分布都已经提前泄露给模型了。验证分数会很好看,但上线以后很容易出现“训练时什么都对,实盘时显著降级”的情况。
下面给一个足够小、但方向正确的示例。重点不是参数,而是输入约定。
from catboost import CatBoostRegressor, Pool
feature_cols = [
"industry_lv1",
"instrument",
"is_st",
"turnover_20d",
"volatility_20d",
"momentum_60d",
"earnings_flag",
]
cat_cols = ["industry_lv1", "instrument", "is_st", "earnings_flag"]
target_col = "label_ret_5d"
train_df = full_df[(full_df["trade_date"] >= "2021-01-01") & (full_df["trade_date"] <= "2023-12-31")].copy()
valid_df = full_df[(full_df["trade_date"] >= "2024-01-01") & (full_df["trade_date"] <= "2024-06-30")].copy()
train_pool = Pool(
data=train_df[feature_cols],
label=train_df[target_col],
cat_features=cat_cols,
)
valid_pool = Pool(
data=valid_df[feature_cols],
label=valid_df[target_col],
cat_features=cat_cols,
)
model = CatBoostRegressor(
loss_function="RMSE",
eval_metric="RMSE",
iterations=800,
depth=6,
learning_rate=0.03,
random_seed=42,
verbose=100,
)
model.fit(train_pool, eval_set=valid_pool, use_best_model=True)
这个示例里最重要的不是 depth=6,而是以下三点:
在训练能跑通之后,不要立刻进入调参。先做一轮输入一致性检查,至少回答下面五个问题:
这里尤其要注意“新类别涌入”。例如行业分类规则升级、证券简称变更、交易状态编码变化,都可能让验证集和测试集出现训练集没见过的类别。CatBoost 能处理一部分未见类别,但如果变化是系统性的,你更应该先统一口径,而不是让模型被动承受脏输入。
实际项目里,以下几种情况最容易被误判成“模型问题”:
这些问题如果不在输入层解决,后面调 depth、l2_leaf_reg、bagging_temperature 只是表面功夫。CatBoost 的强项是把一份结构清楚的表学得更稳,而不是替你把脏样本自动修正成干净样本。
如果你已经做到下面几点,这一讲就算真正完成:
Pool,让 CatBoost 正常读取类别列。做到这一步,你才真正有资格进入下一讲的参数调优和早停策略。否则调参调出来的只是偏样本上的最优解。
本讲是《CatBoost量化建模完整学习计划》的第 2/9 讲,当前主题是《CatBoost特征输入规范:类别编码、缺失值与时间切分》。
上一讲:第 1 讲《CatBoost在量化任务中的优势:有序提升与抗过拟合机制》。
下一讲:第 3 讲《CatBoost参数调参与早停策略:稳定收益优先配置法》。
后续安排:第 4 讲《CatBoost在事件驱动策略中的应用:财报与公告特征融合》;第 5 讲《CatBoost模型可解释性:特征贡献、样本归因与异常诊断》。
风险揭示与免责声明
本页面内容仅用于量化研究与技术交流,旨在展示研究方法与流程,不构成对任何金融产品、证券或衍生品的要约、招揽、推荐或保证。
本文所涉历史数据、回测结果与示例参数不代表未来表现,也不应作为投资决策依据。
市场存在波动、流动性与执行偏差等不确定性,任何策略均可能出现收益波动或阶段性失效。
读者应结合自身风险承受能力进行独立判断,并在必要时咨询持牌专业机构意见。