news 2026/10/22 15:02:12

Sklearn数据预处理详解:从缺失值清洗到特征编码与Pipeline实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sklearn数据预处理详解:从缺失值清洗到特征编码与Pipeline实战

做机器学习时间长了,你会发现一个很朴素的道理:模型上限其实在数据里,预处理决定了下限。特征量纲不一致、缺失值乱填、类别列没编码、训练集和测试集各自“美丽”,这些问题不解决,后面的模型调参再用心,效果也会被拖垮。sklearn里的数据预处理(Preprocessing)就是这么个不起眼却绕不过去的模块。这篇是sklearn专题的第三篇,我准备把从缺失值清洗、数值缩放、类别编码到流水线组合这套东西完整拆一遍,给出的代码和思路都是自己实际跑过、踩过坑才沉淀下来的,适合刚在sklearn里完成建模、开始关心“为什么同样数据别人分数高那么多”的同学,也适合那些想让预处理代码不再手忙脚乱的开发者。

整个专题我习惯把它划分成四个层次:先想清楚模型要什么数据,再逐列把脏数据洗干净,然后把特征变成模型“吃得懂”的数值形态,最后把一串操作固化成可复用的流水线。这篇文章就按这个脉络走,用一个虚构的房价预测场景贯穿始终,每段代码都会解释“为什么这么写”,而不是单纯贴一份作业。如果你已经入了sklearn的门,但每次预处理都要临时翻文档、调参数,这篇应该能帮你把思路彻底捋顺。

1. 先把思路理顺:数据预处理要解决什么问题

1.1 模型为什么对“脏数据”这么敏感

很多人第一次接触数据预处理,是被报错逼的:字符串列丢进模型直接报错,或者全DataFrame转成数值后效果莫名其妙变差。这些现象背后其实是同一个逻辑:模型本质是数学函数,它对输入的数据形式是有假设的。

拿线性回归和SVM这类模型来说,它们依赖距离计算或者梯度下降。假如面积这个特征的取值范围是30到300,房龄是0到40,两者直接放进模型,面积因为数值大,对距离的贡献天然就压过房龄,量纲在这里直接扭曲了权重。树模型对数值量纲不太敏感,但它也有自己的痛点:类别特征如果直接编码成[0,1,2],模型会把这个数值当成连续变量对待,认为2是1的两倍、2比1更“大”,这就凭空捏造了一种排序关系。

还有一类更隐蔽的问题,就是数据分布。线性模型通常默认特征和标签之间的关系近似线性,如果某个特征本身是对数分布或严重偏态,直接丢进模型,哪怕量纲对了,模型也很难拟合出正确关系。数据预处理的本质,就是不断消除这些“模型假设”和“现实数据”之间的错位。

1.2 预处理模块的“家族图谱”

sklearn里的数据预处理能力并不是全集中在一个类里,而是散布在好几个子模块中。我一开始就是没摸清这张地图,经常一个功能找半天。按用途拆开来看,这几个模块是主力:

模块典型类/函数解决的问题
sklearn.imputeSimpleImputer, MissingIndicator缺失值填充与缺失标志
sklearn.preprocessingStandardScaler, MinMaxScaler, OneHotEncoder, OrdinalEncoder, FunctionTransformer数值缩放、编码、自定义变换
sklearn.composeColumnTransformer对不同列分别做不同预处理
sklearn.pipelinePipeline, make_pipeline把多个步骤串成流水线
sklearn.model_selectiontrain_test_split划分训练集与测试集
sklearn.feature_selectionSelectKBest等预处理后的特征筛选

这个家族视图很重要,因为预处理不是一步操作,而是一条链:缺失值清洗应该在缩放之前,类别编码通常和数值缩放并行,最后统一交给下游模型。不同模块各管一段,Pipeline负责把它们粘起来。

还有两个经常和预处理混在一起的动作,需要单独拎出来认清:特征选择(sklearn.feature_selection)是减少特征数量,属于“筛”;特征提取(如PCA)是做降维压缩,属于“并”。筛和并可能在预处理前后反复迭代,但和“清洗+编码+缩放”不是一个环节,别混在一个步骤里写,不然调试的时候分不清问题出在哪。

1.3 一个贯穿全篇的实操数据

为了不让你看代码看得很飘,我这一整篇统一用一个虚构的房屋成交记录数据集来演示,数据规模不大但品种齐全:

  • 面积:连续数值,少量缺失
  • 房龄:连续数值,极个别年份异常
  • 装修等级:字符串类别,取值是“毛坯、简装、精装”,存在自然顺序
  • 朝向:字符串类别,取值是“朝南、朝北、东西”,无内在顺序
  • 所在楼层:连续数值,范围1到50
  • 成交价格:标签列,用来做纵向验证

这个数据集很典型:有数值、有有序类别、有无序类别,又有缺失和噪声,把预处理的常见场景全部覆盖了。后面每个环节我都会用这几列来讲,你完全可以照着代码在自己机器上复现。下面进入正题,先处理最基础的缺失值和脏数值。

2. 数据清洗:缺失值怎么填,异常值怎么办

2.1 缺失值第一课:先弄清楚数据是怎么丢的

很多人拿到NaN就机械地填中位数,这种做法不是不对,而是没有问最关键的“为什么缺失”。统计上会把缺失机制分成几类:完全随机缺失指缺失和任何变量都无关,比如录入员手滑漏填了一个面积;随机缺失指缺失跟其他已观测变量有关,比如高楼层数据更容易缺失;非随机缺失则指缺失本身跟缺失值有关,比如贵的房子面积故意不填全。

三种场景下,填充策略的含义完全不同。完全随机缺失用均值、中位数这类“无信息填充”是安全的;随机缺失如果只用列内统计忽略其他列,会丢失一些可推测的模式;非随机缺失则更麻烦,填一个普通值进去反而可能系统性地扭曲分布。

实操上我不会为了判断缺失机制做一堆统计检验,但通常做两件很朴素的事:先按列统计缺失率,缺失超过30%的特征要怀疑其保留价值;再对比“缺失样本”和“非缺失样本”在标签上的分布,如果差异明显,说明缺失本身可能带着信息。这个信息很多时候比猜测填充还要有价值。

2.2 SimpleImputer用起来,以及四种策略怎么选

sklearn里面对缺失值,主力工具是SimpleImputer。它默认对每列单独做统计填充,核心就一个strategy参数:

from sklearn.impute import SimpleImputer # 中位数填充,适合面积这类可能有偏的数值特征 imp_median = SimpleImputer(strategy='median') X_area_filled = imp_median.fit_transform(X_train[['面积']]) # 若是类别列,'most_frequent'是最常用的 imp_mode = SimpleImputer(strategy='most_frequent') X_decor_filled = imp_mode.fit_transform(X_train[['装修等级']])

四种strategy的使用场景,我在实战里总结成一个口诀:

  • mean:数据近似正态、无明显异常值的时候用。均值受极端值影响大,一个离谱的500平别墅能直接把平均值拉高。
  • median:数据偏态或存在异常值时优先选。价格、面积这类取值永远为正、大值拖尾明显,中位数远比均值稳健。
  • most_frequent:类别型特征专用,填众数。注意它不分顺序,对有序类别也只是填个最多出现的值。
  • constant:有领域知识时使用。比如“收入未填写”可以填-1然后配合缺失指示器,把“故意不填”作为一个有业务含义的信号。

还有一个很容易被忽略的参数add_indicator。设成True时,SimpleImputer会额外生成一列“是否缺失”的0/1标志。这个参数在风控、营销场景非常有价值:用户没填收入这件事,本身可能就是风险特征。缺失率不高但业务上敏感时,宁可保留标志也不让缺失信息悄悄消失。

2.3 缺失值填充的“先后顺序”陷阱

SimpleImputer虽然好用,但它有个很实际的问题:它对每列单独做统计,完全不看其他列。假如面积和楼层高度相关,面积缺失但楼层没缺失,直接用列中位数填充,就会丢掉这种列间关系。

这时候更工程化的做法是先用规则把可推断的缺失补上,再交给SimpleImputer兜底。我处理表格数据时,通常先在数据层做一轮业务规则填充:比如“装修等级为毛坯但成交价填写异常”这类多列联动业务校验,先把能确定的补掉,剩下的真正的“随机洞”才交给统计填充。

还有两个细节经常让人翻车。第一,缺失值不一定叫NaN。很多数据导出工具会把缺失写成空字符串、-999、0或者“Unknown”。SimpleImputer默认只认np.nan,如果是空字符串,你需要先把数据统一处理成NaN,否则字符串列会直接把类型搞乱。第二,任何填充都必须在训练集上先fit。测试集的缺失值要用训练集学到的统计量去填,绝对不能拿测试集自己的均值、中位数来填,不然就是数据泄露,后面会专门展开。

2.4 异常值:要不要删,怎么处理更稳妥

异常值这个问题比缺失值还让人纠结,因为“异常”的定义本身有主观性。我见过太多人在建模前用3倍标准差框掉一堆点,结果模型上线后泛化很差——因为现实世界本来就存在合理的极端值,别墅不是异常,它是样本的真实分布。

我现在的处理习惯是分场景看模型。树模型对单个异常点基本免疫,阈值切分决定了它不会被一个500平的点拉着走,这种情况异常值可以先不管。线性模型、KNN、神经网络这类受距离或梯度影响的模型,异常值会显著拉偏结果。这时候先不急着删,几个更稳的路径是:用RobustScaler这类对异常不敏感的缩放器、对偏态特征做log变换压缩长尾、用分位数截断而不是硬性删除。

需要快速看异常情况时,我经常会用IQR规则做一次扫描,但不把它当自动删除依据:

import numpy as np def outlier_flags(col): q1, q3 = np.percentile(col, [25, 75]) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr return (col < lower) | (col > upper) # 房龄列里出现负数,大概率是录入错误 age_flags = outlier_flags(df['房龄'])

跑完这个函数,重点看两类结果:一类是“明显不可能”的值,比如房龄-5,这类属于错误数据,直接修正或剔除;另一类是“数值很大但业务上可能”的值,比如500平豪宅,这类属于分布长尾,保留并在缩放环节处理。把“错误”和“极端”分开对待,比一刀切删点安全得多。

3. 数值特征:缩放、变换与空间扩展

3.1 四款缩放器怎么选,对比讲透

清洗完缺失值和脏数值,下一步就是统一量纲。sklearn里最常用的四款缩放器,很多人只知道用StandardScaler,但选型差了会直接影响模型表现。我把它们的原理和适用场景对列放在一起:

缩放器数学逻辑输出范围适用场景对异常值敏感度
StandardScalerz=(x-μ)/σ大致[-3,3],但不限线性模型、逻辑回归、PCA,特征近似正态时敏感
MinMaxScaler(x-min)/(max-min)[0,1](可自定义范围)卷积输入、有明确边界的特征,如像素敏感
RobustScaler(x-median)/IQR大致[-1.5,1.5]之外仍有值数据含较多异常值、分布偏态不敏感
MaxAbsScalerx / max(x)[-1,1]

StandardScaler是默认好马,但只有在特征分布不是极端偏态的时候才适合。它做的事情是把数据变成均值为0、方差为1的分布,公式就是z = (x - μ) / σ。凡是用距离、梯度、正则化惩罚的模型,标准化之后各特征对目标的影响才能在同一个尺度上比较。

MinMaxScaler则适合你知道特征有天然边界的场景。比如图像像素本来就是0到255,缩放到0到1很自然。但要注意它会被一个极端值支配:某一列一个超大值,整列都会被压成紧贴0到1的小片区域。而且训练集里的max在测试集不一定同样是最大值,新数据进来完全可能超出边界,这在线上预测时需要小心。

RobustScaler是我处理含噪声数据时的首选。它用中位数和四分位距代替均值和标准差,极端值对它的影响非常有限。房价Demo里如果面积存在别墅这种长尾,我一般直接用RobustScaler,比先删点再标准化省事且安全。

3.2 训练集与测试集:fit和transform必须分开

缩放器使用中最大的坑,我看大多数人第一年都会踩:对测试集也调用了fit_transform,导致训练和测试各自被标准化到不同的空间。正确写法是训练集上fit,然后只transform训练集和测试集:

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train[['面积', '房龄', '所在楼层']]) X_test_scaled = scaler.transform(X_test[['面积', '房龄', '所在楼层']])

为什么要这样?标准化用的μ和σ应该是训练集样本的统计量,代表模型训练时看到的数据分布。如果测试集也单独fit,测试集的μ和σ会偷偷掺进来。更直白一点,模型在训练时根本不该“听说”测试集长什么样,哪怕只是均值。对树模型、朴素贝叶斯这类不做缩放的模型,这个坑暂时无关痛痒,但对线性模型、SVM、神经网络,不正确的fit/transform分配会在线下评估时给你一个虚高的分数。

顺带提一个自查方法:训练时保存的scaler对象,后续预测时一定原样拿过来transform。如果哪天发现上线代码里又重新用数据fit了一下scaler,先停下来检查数据泄露。

3.3 模型学不会非线性?多项式特征和分箱来帮忙

缩放解决统一量纲,但很多场景下模型还缺少“表达非线性”的能力。线性回归天然只能学一条直线,可面积和价格的关系哪儿是直线,往往是边际递减。两种情况可以快速给线性模型“加戏”:PolynomialFeatures和分箱。

PolynomialFeatures会生成原始特征的高次项和交叉项,比如面积、房龄两列在degree=2时,会额外生成面积^2、房龄^2、面积×房龄:

from sklearn.preprocessing import PolynomialFeatures poly = PolynomialFeatures(degree=2, include_bias=False) X_train_poly = poly.fit_transform(X_train[['面积', '房龄']])

加了高次项后,线性模型就能拟合出弯曲的关系。但这里面有个非常实在的提醒:degree不要无脑调高。3次、4次之后特征数量爆炸式增长,很容易过拟合,而且交叉项非常难解释。我的习惯是degree最多2,且只在特征数量少的时候用。

分箱则是另一种思路,用KBinsDiscretizer把连续变量切成几段,每段作为一个类别输入。这种操作对逻辑回归这类模型特别友好,本质上是让模型在段内学一个常数效应。比如把面积分成80-100、100-120、120-150几档,业务解释也顺了:“120平到150平的房子,每平单价溢价明显”。分箱后输出可以是数值编码也可以继续OneHot,配合后续Pipeline用起来很顺手。

4. 类别特征:从序号编码到一键编码

4.1 第一个大坑:别拿LabelEncoder编码特征

这里必须讲一个很多人都会踩的经典坑。网上很多老教程教人处理字符串列时,随手写一个循环:

from sklearn.preprocessing import LabelEncoder # 危险示例:用LabelEncoder处理特征列 for col in ['装修等级', '朝向']: le = LabelEncoder() df[col] = le.fit_transform(df[col])

这段代码能跑通,但隐患很大。LabelEncoder的设计初衷是编码标签y的,它生成的整数编码和模型无关,完全按字符串的排序规则给编号。比如“毛坯、精装、简装”三个词,如果按字母序排,顺序可能变成“简装(0)、毛坯(1)、精装(2)”,这跟业务上的“低中高”根本不匹配。大多数模型(尤其树模型和线性模型)会把0、1、2当成有大小关系的连续值,于是模型学到了一条完全虚假的排序关系。

很多人跑完发现特征重要性里“装修等级”异常重要,结果却是这个虚假顺序在捣鬼。不要问我是怎么知道的。类别特征的标准做法是两个正规军:有序类别用OrdinalEncoder,无序类别用OneHotEncoder。

4.2 OrdinalEncoder和OneHotEncoder分工明确

OrdinalEncoder适用于类别本身有内在顺序的场景。比如“装修等级”天然有毛坯 → 简装 → 精装的递进关系,给它0、1、2编码就没问题,因为顺序感是真实存在的。但要注意,传categories参数显式指定顺序,别放着不管:

from sklearn.preprocessing import OrdinalEncoder ord_enc = OrdinalEncoder(categories=[['毛坯', '简装', '精装']]) X_train_decor = ord_enc.fit_transform(X_train[['装修等级']])

只要你不传categories,sklearn就会按字符串字典序排,那个灾难和LabelEncoder是一样的。所以凡是有业务顺序的类别,显式写清楚categories是必须动作。

OneHotEncoder则适合“朝向”这种互斥并列的类别。它会把“朝南、朝北、东西”三列各自变成0/1,模型不再误判大小关系:

from sklearn.preprocessing import OneHotEncoder one_hot = OneHotEncoder(handle_unknown='ignore') X_train_orient = one_hot.fit_transform(X_train[['朝向']]) X_test_orient = one_hot.transform(X_test[['朝向']])

输出是稀疏矩阵,这一点需要心里有数。OneHotEncoding的本质是制造大量0值,稀疏存储能大幅省内存。如果后续要接pandas、要打印特征名,再考虑转成dense或设置set_output(transform='pandas')。

4.3 OneHot的三个参数:drop、handle_unknown和低频合并

OneHotEncoder的默认行为其实有几个粗糙之处,我逐个讲。

drop参数:当某一列的类别本身是二值(如“是否临街”: 是/否)时,OneHot会生成两列,但这两列完全线性相关(是=1-否)。很多线性模型会因此出现共线性问题,权重解释也会乱。设drop='first'或者drop='if_binary'可以去掉多余一列。树模型无所谓,但线性模型强烈建议处理。

handle_unknown:训练集里没有、测试集里突然出现的类别。如果设成默认的error,预测阶段直接崩。设成ignore后,新类别会变成一列全零,模型就当它“不属于任何已知类别”。这个参数我通常总是开着,省得线上被未知值炸断。

再往后,sklearn新版还支持了min_frequency和max_categories。前者会把出现频率低于阈值的类别自动合并成一类,后者限制最终类别总数。这项能力在处理高基数的城市名、设备型号时特别好用,比如几十万个用户ID做OneHot是不可理喻的,低频类并成“其他”是实用策略。我在模糊匹配场景里常用min_frequency=0.01这种量级的参数。

4.4 类别太多又没有业务顺序,怎么办

OneHot的高基数问题值得单独说。比如数据里有个“板块”特征,有几百个选项,OneHot之后直接给你几百维,样本量不够时模型几乎必炸。这种场景我推荐两条路:

第一条是用低频合并,前面说的min_frequency就是一种自动化做法。第二条是目标编码,把每个类别的标签均值作为该类的编码特征。但目标编码有个著名陷阱:直接全局计算均值会把标签信息泄露进特征,必须放在交叉验证折内去算。所以会复杂很多,一般等模型验证阶段再上。预处理阶段我的铁律是:高基数类别先用低频合并顶着,不擅自引入目标编码。

5. 让预处理进入工程化:Pipeline与ColumnTransformer

5.1 没有Pipeline之前,我的代码有多乱

如果你靠手写变量保存每个scaler和encoder,代码大概会长这样:先定义几个对象,fit各自列,然后一个个transform拼接。中间一旦增加一个特征或者调整一个参数,就要重新同步训练和预测两端。而一旦漏了一列没缩放,模型直接静默变差,你还要花很久查为什么分数不对。

所以我强烈建议从第一天开始就用Pipeline来组织预处理。Pipeline的核心思想很简单:把一串fit和transform步骤打包成一个对象,训练时整体fit,预测时整体transform。你不再需要各自保存五个变量,只需要保存一个流水线对象,测试和上线的变换逻辑自然不会错位。

5.2 一个直接能跑的Pipeline示例

拿只含数值列的子集举例。这组代码把缺失填充、标准化、线性回归串成一条链:

from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression pipe = Pipeline([ ('imputer', SimpleImputer(strategy='median')), ('scaler', StandardScaler()), ('regressor', LinearRegression()) ]) pipe.fit(X_train[['面积', '房龄']], y_train) pred = pipe.predict(X_test[['面积', '房龄']])

Pipeline有个内在规则:除了最后一步,中间步骤必须有transform方法;最后一步只需要fit和predict。训练时,它会自动把前一个步骤的输出喂给下一个步骤;预测时,自动执行除了最后一步之外的所有变换。这个机制就是它不串味、不遗漏的根因。

这里还有个新版本的福利:Pipeline可以将每一步的输出设置成DataFrame,而不是只会出现数组。只要你前一个步骤支持set_output,最后预测结果你就有了特征名。不过旧版本上这个能力还在演进,依赖版本较低时不要强求。

5.3 ColumnTransformer:不同类型列各自处理的正确姿势

真实数据通常是数值列和类别列混杂。ColumnTransformer做的事情很简单:指定哪些列走数值处理通道、哪些列走类别处理通道,最后把结果并排拼接。

from sklearn.compose import ColumnTransformer preprocess = ColumnTransformer([ ('num', Pipeline([ ('imputer', SimpleImputer(strategy='median')), ('scaler', RobustScaler()) ]), ['面积', '房龄', '所在楼层']), ('cat', OneHotEncoder(handle_unknown='ignore'), ['朝向']) ]) X_train_processed = preprocess.fit_transform(X_train) X_test_processed = preprocess.transform(X_test)

这样一串写下来,每条通道的逻辑一目了然:数值列走填充+抗异常缩放,类别列走OneHot。后面如果加了新列,只需要修改列名列表,不需要改任何业务代码之外的东西。我建议把所有特征都在ColumnTransformer里统一定义,像“户口本”一样把每个特征的预处理路径写明,管理大型项目时极度好用。

把ColumnTransformer和Pipeline叠起来,就是最终形态:

full_pipe = Pipeline([ ('preprocess', preprocess), ('regressor', LinearRegression()) ]) full_pipe.fit(X_train, y_train) pred = full_pipe.predict(X_test)

这时候你手里只有两个对象:一个预处理配置和一个模型。这就是我在不同项目间复用的时候最喜欢的样子。复制项目后只需要改动ColumnTransformer的列名和Pipeline的模型对象,其余逻辑全部原样继承。

5.4 其实还有更快的方法:make_pipeline和make_column_transformer

上面用的Pipeline([...])和ColumnTransformer([...])是比较显式的写法。sklearn还提供了两个快捷函数make_pipeline和make_column_transformer,它们会根据传入对象自动起名字。比如make_pipeline(SimpleImputer(), StandardScaler(), LinearRegression())就能生成同等的流水线,省去手动命名步骤名的环节。

日常脚本里我倾向于用这套简写,但进入需要长期维护的代码库时,我建议回到显式Pipeline写法。原因很简单:显式命名后,你想单独检查中间步骤的输出(比如调出某一步的对象)会方便得多,而make_这类写法虽然省事,却让中间步骤不好直接取用。

6. 常见问题与排查记录

6.1 数据泄露:最隐蔽也是最容易犯的错

这是预处理里危害最大、最难被发现的坑,必须单独讲。典型错误顺序是把scaler.fit_transform接在train_test_split之前,或者对全量数据做OneHot的fit。这么做的结果就是,测试集的统计信息悄悄流进训练阶段。

我举个直观例子:假设你整个数据集有1000套房,均价300万,你在划分前就fit标准化,用的是1000套房的均值和方差;但实际上模型只能看到700套训练样本,测试集300套房的信息被泄露了。线下验证分数比真实泛化成绩更高,上线后绩效崩掉,就是这类问题的经典表现。

正确的顺序永远是谁训练谁fit。train_test_split先执行,scaler只在训练子集上fit,然后transform测试子集。这个原则虽然简单,却值得你每次写代码前默念一遍。按这个思路,前面代码里的所有fit对象都必须在训练集上,ColumnTransformer、Pipeline的整体fit也同理。

6.2 版本差异带来的“隐性”崩溃

我踩过最莫名其妙的一个坑,是换环境后OneHotEncoder的handle_unknown变成了报错模式。旧版本里默认是error,如果测试集出现了训练集没见过的类别,预测阶段会直接崩掉。解决方案很无脑但有效:建立一个预处理“标准配置”文件,把OneHotEncoder(handle_unknown='ignore')、OrdinalEncoder的categories等敏感参数固定下来,不依赖默认值。

特别是多人协作的项目里,默认值是最不可信的东西。线上环境和你本地环境版本差几个小版本,行为就可能不一样。把关键参数的显式声明写清楚,比指望队友自觉靠谱得多。

6.3 稀疏矩阵和特征名对不上怎么办

ColumnTransformer和OneHotEncoder输出的稀疏矩阵非常省内存,但在某些场景会带来麻烦。比如你要看特征重要性,却发现特征名字只剩“列0、列1”,或者直接用pandas DataFrame去切列时类型对不上。

新版sklearn的解法是set_output。在ColumnTransformer之前调用set_output(transform="pandas"),输出就变成DataFrame,特征名也会自动拼接出每列的含义。比如朝向_朝南、朝向_朝北这种名字就出来了,后面做特征筛选、结果解释就顺很多:

preprocess.set_output(transform="pandas")

有一点要注意,set_output在proprocessor和model两端都要考虑,如果管道里的模型接受稀疏输入但你设成pandas,反而可能影响性能。实操上我一般只在调试和交付报告中开启DataFrame输出,训练阶段保持紧凑形态。

6.4 自定义变换:FunctionTransformer的正确用法

Preprocessing库里肯定覆盖不了所有奇奇怪怪的清洗逻辑,比如日期转星期几、对数值取log、文本去空格。这种时候很多人会回到pandas层面“手工操作”,但手工操作又会破坏Pipeline的完整性。sklearn给了个FunctionTransformer,能把自定义函数包成sklearn风格的变换器,顺利接进Pipeline:

from sklearn.preprocessing import FunctionTransformer import numpy as np def log1p_transform(x): return np.log1p(x) log_transformer = FunctionTransformer(log1p_transform) # 后续可以放进Pipeline的某一步

自定义函数带进Pipeline有个细节值得说:如果函数只支持一维数组,而sklearn内部传递的是二维数组,很容易把shape搞崩。稳妥做法是直接在函数里加一个reshape或确保它是向量化操作,这样不依赖外部状态,放进Pipeline也不会跟全局变量起冲突。

把这个专题串起来:我实际跑通的一条完整流水线

最后我想完整给出一条我实际用于房价Demo的预处理流水线,它是前面所有讨论的一个综合用例。数据列不复杂,但预处理逻辑完整覆盖了缺失、缩放、编码和模型组合:

from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import RobustScaler, OneHotEncoder from sklearn.ensemble import RandomForestRegressor preprocess = ColumnTransformer([ ('num', Pipeline([ ('imputer', SimpleImputer(strategy='median')), ('scaler', RobustScaler()) ]), ['面积', '房龄', '所在楼层']), ('cat', OneHotEncoder(handle_unknown='ignore'), ['朝向']), ('decor', OrdinalEncoder(categories=[['毛坯', '简装', '精装']]), ['装修等级']) ]) model = Pipeline([ ('preprocess', preprocess), ('regressor', RandomForestRegressor(n_estimators=100, random_state=42)) ]) model.fit(X_train, y_train) print(model.score(X_test, y_test))

这条流水线里我特意用了RobustScaler而不是StandardScaler,就是因为房龄和面积里都有长尾;朝向走OneHot,装修等级走OrdinalEncoder且顺序显式指定。整个流程在同一个对象里完成,训练和预测完全一致。你可以把这条代码当模板,替换成自己的列名和数据,基本可以立马上手。

预处理看起来是“脏活累活”,但它直接决定模型能爬多高。我自己在这个环节上花的时间,往往比建模调参还多,而且回报率极高。很多人抱怨同一批数据,为什么别人跑出来的效果就是好一些,差别可能不在模型,就在这些预处理细节上:缺失值是不是真按业务逻辑处理的、缩放器选对了没、编码有没有引入虚假顺序、测试集有没有被提前偷看。把这几个问题一次理清,比调两轮超参数实在得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 15:01:44

生成式引擎优化(GEO)实操指南:让内容被AI搜索引用

做内容站的朋友应该都有过这种体验&#xff1a;传统搜索里你的页面明明排在首页&#xff0c;访客数据一直很稳&#xff0c;但当你去问ChatGPT或者Perplexity这类AI搜索工具“这个行业里哪个方案最靠谱”时&#xff0c;答案里列出的参考来源翻了好几页都没有你。更扎心的是&…

作者头像 李华
网站建设 2026/10/22 15:01:30

Windows下libssh2 1.11编译库:含OpenSSL依赖的静态库与VS集成指南

简介&#xff1a;这份资源是Windows平台下完整编译的libssh2库&#xff0c;面向需要在C/C项目中集成SSH2协议通信能力的开发者&#xff0c;尤其适合不熟悉Windows编译配置、又急需可用库文件的初中级程序员。压缩包共8个文件&#xff0c;包含3个h头文件、2个lib导入库、2个dll动…

作者头像 李华
网站建设 2026/10/22 15:01:19

回看2019:AI从实验室到产线的关键转折与工程化落地

1. 回看2019&#xff1a;那一年AI行业到底在发生什么 2019年是个很有意思的年份。如果你当时正在这个圈子里&#xff0c;应该能感觉到一种微妙的气氛——深度学习的热度还在&#xff0c;但那种“随便搭个CNN就能发论文”的红利期已经明显过去了。工业界开始认真问一个问题&…

作者头像 李华
网站建设 2026/10/22 15:00:31

深入Android ALSA:snd_pcm_lib_ioctl原理、实践与避坑指南

先说一下&#xff0c;这个 snd_pcm_lib_ioctl 在 Android 的 ALSA 体系里算是一个非常"不起眼"但又绕不开的函数。很多做音频驱动开发的兄弟&#xff0c;第一次在某内核日志里看到它的时候&#xff0c;大概率是往下追过 switch 分支的。这篇文章我就把它彻底掰开…

作者头像 李华
网站建设 2026/10/22 14:56:21

从YOLOv8到PLC:光伏电池片EL隐裂检测系统零故障落地实践

1. 项目全貌与技术选型逻辑1.1 这个项目到底在解决什么问题光伏电池片生产线上有一个非常头疼的质量难题——隐裂。这种裂纹肉眼几乎看不见&#xff0c;但在后续的层压、封装工序中会逐渐扩展&#xff0c;最终导致组件功率衰减甚至形成热斑&#xff0c;严重的话整块组件直接报废…

作者头像 李华
网站建设 2026/10/22 14:55:23

并发编程六大概念与三大死锁陷阱:从进程线程到协程管程

并发编程&#xff0c;十个新手九个栽在“概念混搭”上。今天要拆的就是六个高频词&#xff1a;线程、进程、纤程、协程、管程&#xff0c;外加三个容易混淆的失败模式——死锁、活锁、锁饥饿。很多同学都能背出“进程是资源分配单位&#xff0c;线程是调度单位”这句话&#xf…

作者头像 李华