4.2 数据准备与预处理 本节摘要:数据准备与预处理决定模型性能的上限,而 LightGBM 对数据有一套自己的脾气:它原生支持类别特征、能自行处理缺失值,不需要照搬"先独热编码、先均值填充"的通用套路。本节对比"通用预处理"和"LightGBM 专属做法"的差别,讲清 Dataset 对象怎么建、类别特征怎么声明、缺失值什么时候该放手、数据集怎么切分,以及特征工程在树模型里的真实价值。
本节摘要:数据准备与预处理决定模型性能的上限,而 LightGBM 对数据有一套自己的脾气:它原生支持类别特征、能自行处理缺失值,不需要照搬"先独热编码、先均值填充"的通用套路。本节对比"通用预处理"和"LightGBM 专属做法"的差别,讲清 Dataset 对象怎么建、类别特征怎么声明、缺失值什么时候该放手、数据集怎么切分,以及特征工程在树模型里的真实价值。
阅读完本节,你应当能够:
有一句话在数据建模圈流传很广:数据决定了模型性能的上限,算法只是逼近这个上限。这话对 LightGBM 尤其成立。LightGBM 训练再快、精度再高,喂进去的是一堆脏数据,出来的结果也不会好到哪去。
但"数据准备"这四个字,很容易被理解成一套放之四海皆准的流程:先删缺失、再独热编码、再归一化、再切分。这套流程在逻辑回归、支持向量机这些模型上是成立的,因为它们对数值尺度敏感、要求特征正交。可 LightGBM 是树模型,它按特征值的大小关系找分裂点,不在乎量纲,也能原生处理类别和缺失。照搬通用流程,轻则多做无用功,重则把信息弄丢。
所以这一节的核心主张是:先理解 LightGBM 怎么吃数据,再决定怎么准备数据。别用线性模型的习惯去伺候一棵树。
LightGBM 训练时,最好把数据包成它自己的 Dataset 对象,而不是直接传一个普通矩阵。这个对象会在构建时对数据做一次预处理,把连续特征离散成直方图用的桶,把类别特征单独标记,把缺失值单独归位。这些事如果交给通用流程手工做,又慢又容易错。
其中最关键的是类别特征。很多人的第一反应是独热编码——每个类别展开成一列零一。这在类别数量少的时候没问题,但遇到高基数类别,比如用户编号、商品编号、城市名,独热编码会让特征维度爆炸,内存和训练时间都跟着涨。更糟的是,树模型其实并不需要独热编码,它只需要知道"这个特征是一个整体,按类别分组"就够了。
LightGBM 的原生做法是:你把类别特征声明出来,它在训练时用"按类别统计梯度、找最优类别组合"的方式自动处理。省了维度暴涨,还常常比独热编码效果更好。
import lightgbm as lgb X = [[1, "北京"], [2, "上海"], [3, "广州"], [4, "北京"]] y = [0, 1, 0, 1] # 第 2 列是类别特征,用 categorical_feature 声明,而不是先独热编码 train_data = lgb.Dataset(X, label=y, categorical_feature=[1])
Dataset 对象还有两个容易被忽视的细节。一个是验证集可以引用训练集来构建——通过 reference 参数让验证集复用训练集的分桶方式,既保证两边特征对齐,又能省内存、加速构建。另一个是它把连续特征离散成直方图的过程,是在构建时一次性完成的,训练时反复复用,这正是 LightGBM 快的重要来源。理解了这一点,你就明白为什么"先把数据喂成 Dataset、再训练"比"边训边转换"高效得多。
类别特征也不是无脑声明就好。低基数类别(比如性别、是否)声明不声明差别不大;真正受益的是高基数类别,独热编码会让它们炸掉。但要注意:类别取值过于稀疏、某些类别在训练集里只出现一两次时,原生处理也难学到稳定规律,这时反而要考虑合并低频类别,或者做目标编码之类的进一步处理。
💡 关键直觉:类别特征的声明要趁早。一旦你在切分前把它独热编码成了几十列,模型就再也看不出它们原本是一个整体了,原生处理的好处也一并丢掉。
缺失值怎么处理,通用教程会给你一排选项:删样本、删特征、均值填充、中位数填充、众数填充、模型预测填充。这些选项本身没错,但很多人漏了一个对树模型最省事的选项——不处理。
LightGBM 在构建直方图时,会把缺失值单独归到一类,训练时自动学习"缺失值往左分还是往右分"。换句话说,缺失本身可能携带信息。一个客户"没填收入"和"收入为零"是两回事,硬把缺失填成零,等于把这两种情况混为一谈,反而丢了信号。
那什么时候该填、该删?看缺失比例和缺失是否随机。缺失比例很低、且能确定是录入错误,删掉省事;缺失比例很高、缺失本身有意义,交给模型更稳。只有当缺失是"这个值确实未知、但你知道它服从某种分布"时,用统计量填充才合理。
| 缺失值处理策略 | 适用场景 | 风险 |
|---|---|---|
| 直接删除样本 | 缺失比例低、缺失随机 | 可能丢掉少数但重要的样本 |
| 统计量填充 | 缺失服从已知分布 | 会掩盖缺失本身的信息 |
| 特定值填充 | 缺失有明确业务含义 | 选错填充值等于制造噪声 |
| 交给模型处理 | 树模型、缺失比例中等 | 需确认模型支持原生缺失 |
切分是数据准备里最容易被忽视、却又最容易出事的环节。核心原则只有一条:测试集从头到尾不能碰。验证集用来调参和早停,测试集只做最终评估。一旦你在切分前用全体数据的统计量去填充缺失、去做归一化,就已经把测试集的信息泄露进了训练,评估结果会虚高。
切分方式也要看数据。类别不均衡时用分层抽样,保证每个切分里正负比例一致;时间序列数据要按时间顺序切,不能随机打乱,否则就是拿未来预测过去,自欺欺人。
特征工程对树模型的价值,和线性模型不一样。线性模型靠特征缩放、靠特征组合去逼近非线性关系;树模型本身就能通过分裂捕捉非线性,所以归一化对它几乎没用。真正有用的特征工程,是提供树模型"不好自己长出来"的信息:从时间戳里拆出星期几、从经纬度里算出距离、用业务知识构造比值。这些是人给模型开的"捷径"。
举两个具体例子。做外卖配送时长预测,原始特征只有下单时间、商家位置、顾客位置。直接喂进去,模型得自己去猜"凌晨三点和下午三点不一样",还得自己算距离。如果你提前拆出"小时"这个特征、算好"商家到顾客的直线距离",模型的学习负担就轻了一大截,同等数据量下效果往往更好。做信贷风控,原始特征有月收入、月支出,直接喂,模型要自己发现"入不敷出"这个信号;你提前算一个"支出收入比",等于把这个信号直接递到它嘴边。
这就是特征工程对树模型的真实价值:不是把数据变换得更"规范",而是把领域知识翻译成模型好用的形式。判断一个特征该不该做,标准很简单——这个信息,树靠纯分裂能不能自己长出来?长不出来、又确实重要,就值得手动构造。

⚠️ 常见坑:在切分之前用全体数据做归一化或缺失填充,等于把测试集信息泄露进训练,评估虚高、上线翻车。所有"fit"动作只能在训练集上做。
特征工程做到后面,往往会遇到特征太多、噪声太多的问题。不是每个特征都有用,冗余和无关特征多了,训练变慢,还更容易过拟合。特征选择就是给特征瘦身,留下真正有信号的,去掉拖后腿的。
有三种思路。过滤式:用一个和模型无关的统计指标——比如方差、和目标变量的相关系数——先筛掉明显没用的特征,快但粗,可能误删有交互价值的特征。包裹式:把特征子集当成一个搜索问题,用模型的实际成绩做评价,准但贵,特征多时算不动。嵌入式:把选择融进训练本身,树模型的特征重要性就属于这一类,训练完顺手就能拿到排序。
对 LightGBM,最省事的路径是嵌入式。先训一版模型,按增益口径给特征重要性排序,把长期垫底、删掉也不影响验证集成绩的特征移除或合并,再训一版对比。反复几轮,通常能用更少的特征换来更稳、更快的模型。别一上来就靠包裹式硬搜,预算和耐心都耗不起。
不用。直接声明类别特征,让 LightGBM 原生处理,通常比独热编码、标签编码效果更好也更省内存。高基数类别尤其如此。
先问一句:这个缺失有没有含义。如果"没填"本身就是一种信号,比如客户拒绝提供信息,就交给模型,它会学会怎么分。如果缺失是随机的、比例又低,删除或填充都可以。别无脑均值填充。
树模型不做。LightGBM 按特征值的大小关系找分裂点,不在乎量纲。做了既费事,还可能在切分前引入数据泄露的风险。
要。不固定,每次切分结果都不同,实验不可复现,调参结论也没法横向比较。固定种子是低成本、高回报的习惯。
数据备好了,接下来就是让它跑起来。下一节我们讲 LightGBM 的模型训练与参数调优——早停怎么用、先粗后细为什么比一把梭更靠谱。