ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机器学习回归实战:从数据清洗到特征工程的完整复盘

机器学习回归实战:从数据清洗到特征工程的完整复盘 提交完第二次作业的那天晚上我把代码仓库从头到尾又翻了一遍。这次题目是训练营里经典的二手房价预测回归任务给了一份带缺失、带脏数据、字段含义也不够清晰的表格要求在限定时间内完成从数据清洗、特征工程到建模提交的完整流程。相比第一次作业只是套模板跑通一个baseline这次我明显感觉踏实了很多——不是因为分数多高而是每一步操作背后的为什么终于能说清楚了。这篇复盘主要写给正在上数据分析或机器学习课程的朋友尤其是那种第一次做完整回归练手项目、作业连续卡壳、调参调到怀疑人生的人。我会把破题思路、清洗策略、特征工程、建模调参以及这次踩过的坑全部揉开来讲。1. 拿到题目后的破题思路评分规则才是第一份需求文档第一次作业的时候我拿到数据就开始跑模型结果发现跑完也不知道自己在干什么。这次我学乖了先花半小时把作业要求、评分规则和数据字典从头读了一遍然后才动手敲代码。1.1 名义上是回归题真正的考点却不在模型这个作业表面上是回归预测训练集大约两万条测试集五千多条四十来个字段评估指标是RMSE均方根误差。很多同学看到回归两个字就开始调模型参数默认这是一道模型题。但我把数据打开看了一圈之后立刻意识到这题真正的难度在数据本身字段缺失率高、类型混乱、还有大量需要结合业务常识才能理解的值。举个例子数据里有一个总建筑面积字段按常理应该是连续正数但我扫了一遍发现里面有几百条记录是0。如果直接把这个当成缺失值删掉或者填中位数模型学到的规律就会跑偏。后来我结合房屋类型地下室面积这些字段去看才发现部分0值其实代表没有地上建筑面积或房屋类型不是普通住宅这种情况下直接删除反而会丢掉一个重要信息。这个发现让我确定了一件事这作业的差距不在模型选型而在特征层面对信息利用的细致程度。所以我把时间分配改成了七成做清洗和特征三成做建模调参。1.2 先打通数据流再考虑建模精度我第一篇博文里提到过数据科学项目最容易翻车的不是模型精度低而是实验之间标准不统一。这次作业我吸取教训一开始就把整个流程整理成了一个固定的数据流读取原始数据、基础清洗、特征工程、建模、评估每一步都封装成函数。这样做的好处非常明显后面迭代特征的时候我只需要修改特征工程那一段然后一键重跑后面的流程所有实验的结果都可以横向对比。同时我固定了随机种子并且写了一个统一的评估函数。这个习惯在第二次作业里帮我省了非常多事因为不用每次跑完都去猜这次分数变好到底是特征起作用了还是只是随机波动。如果读者你现在刚开始做机器学习作业我建议也先花半天时间把这套骨架搭好不要急着调参。1.3 合理的任务拆解比闷头干活更重要我把这次作业按一周时间做了拆解实际执行之后有一些偏差但大体是准的。我原计划是这样第一天EDA探索性数据分析把每个字段的分布、缺失率、与目标变量的关系摸一遍第二天数据清洗处理缺失值和异常值第三天到第四天特征工程包括连续变量变换、类别变量编码、交叉特征第五天到第六天建模和调参跑基线、交叉验证、参数搜索第七天提交、复盘检查代码可复现性。实际执行下来特征工程阶段比计划多花了半天时间因为交叉特征的构造需要反复验证有效性。好在我在前面清洗和建模部分都留了缓冲时间最后整体节奏没有崩。这个经历让我意识到作业级项目里时间盒的设定非常关键——固定每个阶段的最长投入时间一旦超时就必须砍掉收益不高的探索方向先保证提交一个完整且合理的结果。2. 数据清洗第一个真正卡住的环节这次作业里我磨得最久的就是数据清洗。不是说模型难调而是脏数据的处理方式对后面所有步骤的影响太大了。清洗做对了后面特征和模型都能顺利清洗做错了后面再努力也是在错误的基准上打转。2.1 缺失值不能一刀切要分列处理很多人在第一次作业里养成了习惯删掉缺失值多的列或者全部用中位数填充。这种一刀切在第二次作业里一定会吃亏。我专门做了一张表来记录不同列的缺失情况和处理方案这里给大家看一下我当时的部分决策字段缺失率处理方式原因朝向1.2%众数填充缺失率低对模型影响小众数是该类型下最常见取值建造年份35%结合周边字段估计后填充缺失样本大多年代较久若直接删会造成系统偏差装修情况67%删除原列保留是否缺失标志缺失率过高且与另一字段高度相关总楼层22%按小区分组取中位数同一小区楼栋总层数通常一致组内填充更合理这里我想重点说说建造年份这种高缺失率字段。一开始我想直接删掉这一列但我用探索性分析看了一眼发现缺失建造年份的样本在价格分布上和低房龄的房子更接近这说明缺失并不是完全随机的。后来我把小区名称、所在区域、周边配套这些字段结合起来推测了一个大概年份范围填进去之后模型的效果立刻提升了一些。所以处理缺失值的核心原则是先去判断缺失机制再去选填充策略。是随机缺失还是与某些业务因素相关如果与业务因素相关直接用全局统计量填充就是在往数据里塞噪声。2.2 异常值处理前先理解字段的业务含义这次作业里0值问题是我印象最深的一个坑。前面提到总建筑面积出现0值有些同学直接删了但我查了原始记录后发现这些样本的房屋类型字段基本都对应非普通住宅比如别墅或商办类。这个信息本身对价格预测就很有区分度删掉等于把最有价值的标签丢了。我当时的处理方式分两步。第一步把面积为0且类型为非普通住宅的样本单独打上一个特征标记是否为非普通住宅第二步如果面积为0且其他字段也没有任何参考信息才考虑删除。这样做的好处是模型既能看到这类房子的特殊性又不至于被0值带偏。关于异常值我还用IQR四分位距做了辅助判断但我没有完全依赖统计方法。因为统计方法只能告诉你这个值偏离了分布不能告诉你为什么偏离。这时候业务逻辑反而更重要。比如房价极低的情况有可能是点数错误也可能是某种特殊产权类型这两个的处理方向完全不同。2.3 字符串和类型混乱最容易被忽略的耗时间题除了缺失和异常这次数据里还有大量字段类型混乱的问题。比如年份字段里混着1990年这种带单位的字符串单价字段里出现了12,500元/平方米这种带逗号和单位的文本还有日期字段居然是字符串格式存进去的。这些看起来是小问题但如果你不处理模型根本读不进去。我的做法是写了一个标准化函数把所有数值类字符串里的逗号、单位、空格统一去掉并转成float日期字段用专门的日期解析库转成标准格式然后提取出距今的天数作为一个新特征。这一步做完后面建模就顺畅很多了。提示清洗数据的时候每一步操作都要有记录。我习惯在代码注释里写下这一行的处理原因比如townhouse总面积众数填充原因是同小区类似房型总层数基本一致。这个习惯在复盘的时候帮助很大因为你能清楚知道每一次数据改动对模型的影响来自哪里。3. 特征工程分数拉开差距的地方如果说清洗是地基那一百个人跑出来的特征工程就是完全不同的房子。第二次作业里我尝到了特征工程的甜头也理解了为什么这类比赛/作业最终排名前列的人模型可能都很普通但特征都做得非常细致。3.1 连续特征的分箱与变换对于连续数值特征我做了两件事变换和分箱。首先要说的是对数变换。像面积、总价这种分布严重右偏的字段我直接用np.log1p处理过一遍让分布更接近正态。这一步对线性模型帮助非常大对树模型帮助不大但因为后面我还准备跑线性基线所以统一处理了也不亏。然后是分箱。这里我以房龄为例。原始字段是建造年份我把它转换成了房龄然后按照业务含义分成几段5年以内是新房5到10年是次新房10到20年是中龄房20年以上是老房。树模型虽然理论上能自己找切分点但我事先根据业务理解把类别划分好相当于给模型注入了先验知识在训练数据量有限时能明显降低过拟合风险。类似的分箱我还在面积字段上做了一次不过不是简单切年龄区间而是结合总楼层算了一个楼层位置比例的新特征。这样比单纯用绝对楼层要更合理因为同样是5楼在一栋8层楼和一栋30层楼里意义完全不同。3.2 类别变量编码目标编码的正确打开方式这次数据里的类别型字段挺多比如朝向、装修情况、房屋类型。最常见的做法是One-Hot编码类别少的时候没问题但有些字段类别特别多One-Hot之后特征维度爆炸模型性能反而下降。我采用了两套方案。第一套是频次编码也就是用每个类别出现的次数作为特征值。这个方案对出现次数少的小类别非常友好因为模型能感知到这是个稀有类别。第二套是目标编码用目标变量的均值来编码类别。目标编码是刷分利器但也最容易出问题——如果直接在全部数据上计算均值就会发生目标泄露我在后面踩坑章节还会单独讲。这里简单提一下正确的姿势把目标编码放到交叉验证的每一折内部去计算只基于训练折的数据估计均值然后应用到验证折。也就是说编码必须在fold内部做不能整个数据集一起算完再划分。这个细节直接决定了你的本地验证分数是真实可信还是自欺欺人。3.3 交叉特征信息量大于原始特征之和我这次得分提升最明显的一步来自交叉特征。一个例子是区域×房屋类型同样的面积在核心地段的老公寓和在新城区的别墅价格逻辑完全不同。另一个例子是朝向×总楼层朝南的低楼层和朝南的高楼层在采光和通风上的价值也不同。构造交叉特征时要注意控制数量。穷举所有组合会让特征空间爆炸而且很多交叉没有意义。我一般会先用EDA判断哪些字段对目标变量的分组差异最明显只挑那些有业务逻辑支撑的组合来做。比如我看了数据后发现所在区域对单价的方差影响最大那么优先和区域做交叉就基本不会错。4. 建模与调参从基线模型到最终提交特征工程做得差不多了我开始进入建模环节。这里要坦白说一句在表格数据上模型的选择和调参对最终结果的影响远没有特征工程大。但建模环节的工程细节决定了你的分数能不能稳定复现。4.1 先用线性回归做基线不是为了精度我第一轮跑的不是XGBoost也不是LightGBM而是最普通的线性回归。这么做不是指望线性回归拿高分而是用它当哨兵如果线性模型在训练集上完全学不进去或者损失爆炸那八成是特征工程里有编码错误、无穷值或者单位未统一的问题。线性模型对这些问题非常敏感树模型反而会容忍甚至隐藏掉。等线性回归能稳定跑出合理分数之后我再换上LightGBM作为主力模型。选LightGBM主要是因为它在表格数据上训练速度快内存占用小而且自带对缺失值的处理对这次作业这种数据情况比较合适。XGBoost当然也能用但调参成本略高一点CatBoost对类别特征很友好只是类别字段我用目标编码处理过了优势不那么明显。4.2 交叉验证划分方式比模型参数更重要说到这里忍不住想强调一个容易被忽略的点交叉验证的划分方式直接决定了你后续所有调参决策是否可信。我这次用的是5折KFold然后固定了随机种子保证每次实验的划分完全一致。但我同时也思考了一个问题这份数据有日期字段存在时间先后的可能性。如果是时间序列场景随机切分会导致训练集里出现未来信息本地验证分数虚高线上分数却崩掉。这次作业的数据来源官方没有明确说明所以我先用KFold跑然后专门对比了一次TimeSeriesSplit的结果发现两者结论基本一致这才放心继续用KFold。建议大家在拿到任何带时间字段的数据时都做一次这个对比不要想当然。4.3 参数调优的具体顺序和节奏LightGBM调参我有一套固定的顺序供参考先固定learning_rate0.05用早停法确定一个合理的树数量n_estimators再调num_leaves和max_depth控制模型复杂度然后调min_child_samples和min_child_weight继续抑制过拟合最后微调feature_fraction和bagging_fraction引入随机性提升泛化能力。我这里有一份我当时用的参数搜索范围表可以给大家参考参数搜索范围说明num_leaves16 ~ 255数值越大模型越复杂容易过拟合max_depth5 ~ 15限制树深度和num_leaves配合使用min_child_samples20 ~ 100叶子节点最小样本数越大越保守feature_fraction0.6 ~ 0.9每棵树随机取特征比例bagging_fraction0.7 ~ 1.0每棵树随机取样本比例调参的关键原则是每次只动一个变量。比如我先固定其余参数单独扫描num_leaves从16到255记录每一档的交叉验证分数然后再动下一个参数。切忌同时改好几个参数因为一旦分数变化你根本不知道是哪个改动起的作用。5. 这次作业里的真实踩坑记录第三次提交之前我的本地验证分数已经挺漂亮了但线上分数一度很难看。那两天我一直在排查最后发现了几类非常典型的坑全是数据科学新手很容易踩的。5.1 特征泄漏本地分数虚高的头号元凶特征泄漏这个问题说多少次都不为过。我在处理目标编码的时候一开始图省事直接用整份训练集计算了每个类别的价格均值然后把这个均值作为特征填进去。训练集会话里看交叉验证分数特别好因为编码已经见过了验证集的目标值。可一旦换到线上测试集测试集的目标值我根本看不到这个特征就失效了分数直接崩掉。正确的做法是保证目标编码的计算只使用训练折的数据。写出来的代码逻辑就是在交叉验证循环里对于每一折从训练部分算均值然后同时应用到这一折的训练和验证部分。我后来把整个编码过程封装成了自定义的转换器确保它只会基于传入的数据来拟合彻底堵死了泄漏的路子。5.2 验证集划分不合理导致调参方向错误另一个让我头疼的问题是本地验证和线上分数趋势不一致。明明本地验证RMSE在降线上分数却在升。检查后发现问题是验证集划分太粗糙。我一开始直接用train_test_split切了20%出来做验证但这个数据里不同区域的房价分布差异很大如果验证集里恰好高价位区域占比偏高那验证分数就代表不了整体。解决方案是改用分层抽样让训练集和验证集里各个房价区间的比例保持一致。我后来按价格分桶之后做StratifiedKFold本地验证分数和线上分数的相关性立刻好了很多。对这类回归任务这不是标准解法但实际操作中非常有效推荐遇到相同问题的人试试。5.3 随机种子不固定实验对比失真我有个坏习惯初始代码里忘记固定随机种子结果跑了两次清洗之后交叉验证分数忽高忽低我一度以为是特征工程搞错了。排查了很久才发现只是随机划分变了。这就是典型的浪费生命场景。现在我的所有项目中读取数据之后第一件事就是固定随机种子包括所有可能引入随机性的库。同时在代码仓库里记录了跑实验时使用的关键库版本。这样即使一个月后回头看也能准确复现当时的实验结果。6. 提交与复盘分数以外的收获最后一天我没有急着反复提交而是专门留出时间做了一次完整的复盘。复盘的过程比我想象中更有收获因为很多问题在往前赶的时候是不会浮现的。6.1 这次复盘我重点检查了三件事第一验证策略和提交目标是否一致。我会问自己我本地验证时假设的数据分布和评测时用的数据分布是否一致如果不清楚就做一个简单的敏感性分析比如换几种划分方式看分数波动波动太大的话说明验证策略不可靠。第二每个特征是否真的经得起交叉验证的检验。我这次构造了不少特征复盘时我用单特征替换的方式逐个实验发现大概有三分之一的特征贡献度很低。在最终模型里保留这些特征不仅没帮助还拖慢了训练速度。于是我做了一次特征筛选把贡献度低的都去掉模型训练时间缩短了30%分数还略微升了一点。第三整个流程是否可复现。我重新在一个干净的Python环境里按顺序跑了一遍所有脚本确保从原始数据到最终提交文件之间不需要任何手动操作。因为如果哪一步是手动在Jupyter Notebook里操作完成的过两天可能连你自己都记不清当时怎么处理的。6.2 代码组织的一点体会这次作业我全程用notebook写原型但对最终可复现的流程做了模块化整理。我按照数据读取、清洗、特征、模型、提交分成几个脚本中间用统一的配置文件传递参数。虽然训练营作业对代码规范的要求不高但这样做之后我每次想尝试一个新特征或者一个新参数都只需要改很小一块地方实验效率提升明显。6.3 下一次作业我会怎么做如果后面还有更复杂的任务我的优先级会是这样先把数据流骨架和验证方案做到位再考虑特征和模型。在特征方面我想尝试做一点模型融合比如把LightGBM和CatBoost的结果做一个加权平均或者尝试简单的Stacking。不是为了刷分而刷分而是想理解多模型之间的差异性在什么条件下才能真正带来提升。这属于进阶方向但基础打牢之后再往上走会顺很多。最后分享一个小技巧提交最终结果之前一定要在干净环境里重新完整跑一遍流程。我这次就差点因为notebook里残留了一个旧变量导致提交文件数据错位那种错误在评分阶段是完全看不出来的一旦发生之前的努力就全白费了。检查完这个问题你才算真正对这次作业负责了。
返回列表