☰
数据预处理全流程:从缺失值处理到特征编码的完整指南
2026/10/16 11:56:06 网站建设 项目流程

前面五篇笔记我们一直在处理“脏数据”:缺失值怎么发现、重复记录怎么剔除、格式怎么统一、异常字符串怎么修正。这些动作本质上是把数据变得“能用”。但“能用”和“好用”之间还有一段路——数据预处理,恰恰就是这段路的施工阶段。

很多入门教程把数据清洗和数据预处理混着讲,我写这一篇的时候特意把它们分开。清洗解决的是“数据错不错”的问题,预处理解决的是“数据适不适合建模”的问题。你花大力气把空值、重复、乱码都收拾干净了,回头发现模型跑出来的效果依然稀烂,十有八九是预处理环节没做到位:数值字段量纲差别太大、类别字段没有编码、特征分布偏得离谱、时间字段还躺在字符串里。所以这篇笔记我不打算堆概念,而是用一套完整的模拟数据从头到尾走一遍,把缺失值处理、类型转换、异常值检测、标准化、编码、分箱这些核心动作逐个拆开讲明白,讲清楚“为什么这么做”和“不这么做会怎样”。

1. 数据预处理到底在干什么

1.1 先把它和“清洗”的边界划清楚

我经常看到有人用数据清洗这个词指代所有建模前的处理,这在面试和日常交流里容易造成误解。我习惯把整个流水线拆成三段:接入与清洗、预处理、特征工程。清洗负责把脏数据修成“干净但原始”的状态,预处理负责把干净数据变成“算法友好的形态”,特征工程则是从数据里挖出更有表达力的新字段。这三段有重叠,但职责不同,尤其是预处理这一步,它直接对接模型输入,稍有闪失,前面的清洗功夫可能白费。

举个例子:你有一列“商品单价”,清洗阶段把负值、空值都处理掉了,看起来很干净。但如果这列数据的范围是0.5到8800,而另一列“购买数量”的范围是1到5,你把它们直接丢进基于距离的模型(比如K近邻、聚类),单价会凭借数值优势主导整个距离计算,数量特征基本被淹没。预处理阶段的标准化(或归一化)就是用来解决这类问题的。

1.2 预处理的六个核心模块

预处理不是一个单独的函数,而是一组策略的组合。我把它拆成六个模块,这也是我这篇笔记的讲解主线。

模块核心问题常见手段
缺失值处理空值应该删除还是填充?填充用什么策略?dropna、fillna、插值、模型预测
重复值与类型转换重复记录如何精确识别?字段类型是否匹配算法?duplicated、drop_duplicates、astype、to_datetime
异常值检测哪些“极值”是真实业务,哪些是数据错误?IQR法、3σ法则、业务规则
标准化与归一化不同量纲的特征如何放在同一尺度比较?StandardScaler、MinMaxScaler
特征编码类别字段如何转成数值?有序和无序类别有何区别?LabelEncoder、OneHotEncoder、映射
分箱与特征构造连续数值如何分段?时间字段如何提取周期性?cut、qcut、dt属性

这六个模块之间是有顺序讲究的。我自己的习惯是:先处理重复值和类型转换,再做缺失值,然后做异常值,最后做标准化和编码。因为编码之前要先确认字段是离散还是连续,异常值检测之前要先保证缺失值不会干扰计算,标准化一定要放在最后,否则一旦先缩放了数据,后续做编码或分箱时会引入不必要的复杂度。

1.3 一句话说清全流程的做法

我贴一张自己常用的流程图思路(文字版):

原始数据 → 重复值检查与去除 → 字段类型统一(字符串时间转datetime、object转category) → 缺失值策略(删除/填充/预测) → 异常值识别与处理 → 特征构造与分箱 → 数据集划分(训练/测试) → 标准化与编码 → 建模输入

这个顺序我建议入门阶段直接照着抄,比我当年闷着头想效率高得多。到了后面你有经验了,就会发现有些步骤可以合并,有些场景可以跳过,但那都是以后的事。先把这条流水线跑通,你手里这批数据才算真正“预处理”完了。

2. 缺失值处理:删还是填,这是个策略问题

2.1 先给数据造个场景

为了让操作落地,我杜撰一个订单数据表来做演示(字段和值都是我编的,覆盖了常见的坑)。

import pandas as pd import numpy as np df = pd.DataFrame({ "订单号": ["A001", "A002", "A003", "A004", "A005", "A006"], "会员等级": ["普通", "黄金", "钻石", "普通", "黄金", "钻石"], "商品类别": ["数码", "服饰", "数码", "食品", None, "数码"], "单价": [2999, 199, 4599, 25, 399, None], "数量": [1, 2, 1, 3, 1, 2], "订单时间": ["2025-06-01 10:22:13", "2025-06-01 11:45:01", "2025-06-02 09:30:45", "2025-06-03 14:12:32", None, "2025-06-04 20:18:09"], "城市": ["北京", "上海", "广州", "深圳", "杭州", "成都"], "备注": ["优惠券", np.nan, "加急", np.nan, "礼品", np.nan] }) print(df)

输出长这样:

订单号 会员等级 商品类别 单价 数量 订单时间 城市 备注 0 A001 普通 数码 2999.0 1 2025-06-01 10:22:13 北京 优惠券 1 A002 黄金 服饰 199.0 2 2025-06-01 11:45:01 上海 NaN 2 A003 钻石 数码 4599.0 1 2025-06-02 09:30:45 广州 加急 3 A004 普通 食品 25.0 3 2025-06-03 14:12:32 深圳 NaN 4 A005 黄金 NaN 399.0 1 NaN 杭州 礼品 5 A006 钻石 数码 NaN 2 2025-06-04 20:18:09 成都 NaN

这6行数据看着不大,但把常见的缺失场景全涵盖了:连续数值缺(单价)、离散类别缺(商品类别)、关键时间缺(订单时间)、纯文本缺(备注)。不同字段的缺失,处理逻辑完全不一样,这也是入门的人最容易犯迷糊的地方。

2.2 数值型缺失:别急着填平均值

看到单价有缺失,很多刚入门的朋友第一反应是“用平均值填充”。我当年也是这样,后来被一个问题点醒了:这一单是数码类商品,单价4599,你用平均值填,打算填多少?如果全表平均值是1413左右,填进去之后这个订单的金额预测特征就失真了。

更合理的思路是按分组统计来填充。同类商品的单价分布更接近,你要预测某个数码商品的缺失价格,参考其他数码商品的中位数,比参考全表所有商品的平均值靠谱得多。实现方式很简单:

# 先看每类商品单价的中位数 print(df.groupby("商品类别")["单价"].median()) # 用同类中位数填充缺失单价 df["单价"] = df["单价"].fillna( df.groupby("商品类别")["单价"].transform("median") )

这个思路推而广之:填充连续值之前,先想想“跟这个缺失值最像的样本是什么”。如果数据没有明显的分组维度,再退而求其次用全局中位数或均值。均值容易受极端值影响,中位数更稳,这是统计知识的直接应用。

2.3 时间型缺失:有时删除才是正解

订单时间缺失就比较特殊了。时间字段不像单价可以用中位数填充,因为订单时间的缺失往往意味着这个记录本身不完整,或者是数据采集环节出了问题。你很难用“前后两单的平均时间”去伪造一个时间,这在业务上站不住脚。

我当时踩过一次坑:为了凑足训练数据量,我用前后订单时间的中位数把缺失时间填了,结果后面做人流量时段分析,这个伪造的时间直接让某个时段统计失真。后来我的原则就是:关键业务时间字段缺失,如果缺失率很低(比如低于5%),直接删除该行;如果缺失率很高,说明这个字段本身要么采集失败,要么根本不可靠,这时候要考虑是否放弃这个字段。

# 订单时间缺失的行,直接删除 df = df.dropna(subset=["订单时间"]) print(df)

有人可能会问:那什么时候时间字段也可以填充?我的一般判断标准是:如果你只是在做“天数差”这种粗粒度特征,填充可能影响不大;但如果你要做“星期几”“几点钟”这种细粒度时序特征,填充基本等于造假。删除缺时间的行,损失的是少量样本,换取的是整体可信度。

2.4 文本型缺失和离散类别缺失

备注缺失不是错误,但建模时如果要把备注纳入分析,空值会带来麻烦。比较通用的做法是填充一个占位符,比如“无备注”,表示这条记录本身没有附加信息,而不是数据丢失。

df["备注"] = df["备注"].fillna("无备注")

商品类别缺失就相对敏感了。如果类别字段缺失,你可以用其他字段做简单推断(比如根据订单标题关键词),但正常情况下不要觉得自己很聪明去“猜”类别。我之前在一个模拟项目里遇到20%的类别缺失,尝试用KNN填充,效果时好时坏,后来干脆把缺失值单独作为一类“未知类别”,让模型自己去学习这个标签的含义。这样处理在树模型上表现很稳定。

2.5 缺失值处理顺序为什么重要

我先做类型转换再处理缺失值,是有原因的。比如某些字段是object类型,fillna的默认行为会受字段类型影响;时间字段如果还是字符串,根本没法用dropna去判断“是否缺失”。先把字段类型统一,缺失值的形态才更清晰,后续处理才会稳定。

另外,我建议你把缺失值处理完之后的记录个数、每个字段的缺失率打印出来,看一眼再往下走。这一步多花10秒,能帮你预防一大半“预处理做好了、喂给模型却报错”的情况。

print(df.isnull().sum()) print(df.shape)

3. 类型转换与异常值:这两步决定了特征质量

3.1 用category类型减少内存并提升语义清晰度

pandas里object类型虽然啥都能放,但它本质上是一个个Python对象的指针,内存开销大,而且不携带“离散类别”的语义信息。对于会员等级、城市、商品类别这类取值有限的列,我建议转成category类型。

# object转category for col in ["会员等级", "商品类别", "城市"]: df[col] = df[col].astype("category")

转完之后你会感觉到两个变化:肉眼可见的便利是DataFrame内存占用下降,尤其是数据量大、类别少时特别明显;更深一层的好处是,category类型会暴露出你数据里实际有哪些类目,后续做编码时心里有底。比如你发现“商品类别”里居然有个“数码 ”(带空格),category会把这个当成单独一类,这又倒逼你回到清洗阶段把空格处理掉——这种回环在真实项目中经常发生,没必要懊恼,流程跑多了自然顺。

3.2 时间字符串转datetime并提取周期特征

订单时间现在是字符串,如果不转,你没法计算“距今天数”,也没法提取“星期几”。处理方式很直接:

df["订单时间"] = pd.to_datetime(df["订单时间"]) df["下单星期"] = df["订单时间"].dt.dayofweek # 周一=0, 周日=6 df["下单小时"] = df["订单时间"].dt.hour

这里有个容易踩的坑:pandas的to_datetime在面对“2025/06/01”和“2025-06-01”混着来的情况时能自动识别,但如果你的时间格式是“2025年6月1日”,需要先统一格式或者显式指定format参数。我处理过一批乱糟糟的时间数据,最后用format逐个匹配才搞定。建议你的正则清洗在前一篇笔记阶段就把时间格式统一好,预处理阶段再转换会更省心。

时间字段转datetime后,还能做排序、做窗口统计、计算时间间隔,这些是后面分析功能的基础。我在预处理阶段至少会提取星期和小时两个维度来做演示,因为销售业务的“周末效应”“高峰时段”都靠这两个特征才能体现。

3.3 重复值处理:subset参数才是重点

重复值看着简单,但“全字段完全重复”和“关键字段重复”是两回事。我的建议是:先想想业务上怎么定义“一个重复的订单”。如果订单号相同但收货地址不同,这可能是用户改了地址后重新生成了订单,旧订单其实应该作废,而不是当成一条重复记录删掉。

# 完全重复的行,删除 df = df.drop_duplicates() # 按订单号、商品类别去重,保留最后一条 df = df.drop_duplicates(subset=["订单号", "商品类别"], keep="last")

实操里我最常用的是subset参数,因为业务上几乎没有“所有字段都一模一样”的重复数据,基本都是主键重复或关键维度组合重复。你还需要想清楚keep用first还是last:通常情况下保留最早的那条,但如果是订单状态字段,反而要保留最后更新的那条。这取决于你的业务语义。

3.4 异常值:IQR法、3σ法和业务规则怎么选

异常值检测在入门阶段经常被简单化成“超出均值±3倍标准差就删掉”。这个做法在数据近似正态分布时有一定道理,但绝大多数业务数据是偏态的(销售额、价格、用户贡献值大多呈长尾分布),用3σ法则会把很多正常的高价值用户误判成异常值。

我演示一下IQR(四分位距)做法,它在偏态分布下稳定不少:

Q1 = df["单价"].quantile(0.25) Q3 = df["单价"].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR print("下界:", lower, "上界:", upper) outlier_mask = (df["单价"] < lower) | (df["单价"] > upper) print(df[outlier_mask])

原理很好理解:把数据分成四等份,上下四分位数各处在25%和75%的位置,它们之间的跨度就是中段数据的离散程度。如果某个值偏离这个中段超过1.5倍跨度,就有理由怀疑它是异常值。

但更重要的一点是:异常值未必是错误值。我之前在某零售场景做过一个分析,订单金额特别大的那些记录,其实是批发客户,完全正常。我的处理原则是:先用IQR或3σ把可疑点标记出来,再结合业务判断。比如单价为负这是明显的错误,直接删除或修正;单价高到夸张但确实存在,可能是真实业务,就让模型自己处理或单独做一个离群值特征。

# 业务规则:单价为负直接修正为缺失,再填充 df.loc[df["单价"] < 0, "单价"] = np.nan

把它和缺失值处理放在一起理解:异常值识别出来之后,处理手段通常是修正、删除、单独标记三种。不要默认“删”,先想想这个数据在业务上意味着什么。后面建模时会发现,有些模型的稳健性其实比你想的好,异常值未必多么致命,乱删反而会损失信息。

4. 标准化与特征编码:让模型真正看得懂你的数据

4.1 标准化不是所有场景必需,但你要知道什么时候必需

很多入门资料把标准化吹成万物前提,实际上树模型(比如随机森林、XGBoost)对量纲不敏感,标准化对它们影响不大。真正需要标准化的场景是:要算距离(KNN、聚类、SVM)、要梯度下降(线性回归、神经网络、PCA、正则化)。

道理不复杂:距离计算要各维度和睦相处,如果单价范围0.5-8800,数量范围1-5,距离会被单价主导;梯度下降需要各个特征的尺度差不多,否则收敛路径歪歪扭扭,训练效率直线下降。

4.2 StandardScaler和MinMaxScaler怎么选

我用一张表总结两者的差异和适用场景。

方法公式输出范围适用场景
StandardScaler(x - mean) / std理论上无上限,均值0,标准差1数据近似正态、后续用线性模型或PCA
MinMaxScaler(x - min) / (max - min)[0, 1]数据分布不明显、神经网络输入、需要固定边界

实操时我的建议是:看不准就先标准化(StandardScaler),因为它保留分布形状,对异常值稍敏感;如果你的特征本身有硬性上下界(比如比例、得分),用MinMaxScaler更符合常识。

4.3 数据泄露:先划分数据集再缩放

这是新手最容易忽略的一个大问题。假设你全量数据都用StandardScaler的fit_transform,然后划分训练集和测试集,那测试集的信息(均值和标准差)已经通过全局统计泄露给了训练过程。评估结果会虚高,上线后模型一遇到新数据就“露馅”。

正确做法是先拆数据再缩放:

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df.drop("订单号", axis=1) y = df["订单号"] # 这里无监督意义,仅演示 X_train, X_test = train_test_split(X, test_size=0.3, random_state=42) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 只transform,不fit

理解了fit和transform的区别,这个操作就通了。fit是学习训练集上的统计量,transform是把统计量应用到具体数据上。核心就一句话:测试集永远不参与fit,只被transform。这也是后面用Pipeline自动管理的逻辑起点。

4.4 类别特征编码:有序与无序要分开

类别特征编码有不少坑,最常见的错误是把所有类别一视同仁丢进LabelEncoder。这里的核心区分是:类别之间有无顺序关系。

  • 有序类别(Ordinal):会员等级“普通 < 黄金 < 钻石”,是有先后之分的,用整数映射最合理。
  • 无序类别(Nominal):城市“北京、上海、广州”,没有大小之说,用LabelEncoder强行给编号,会让模型误以为“北京=1 < 上海=2”有大小关系,反而引入噪声,最稳妥的做法是OneHotEncoding。
# 有序:自行定义映射 df["会员等级编码"] = df["会员等级"].map({"普通": 0, "黄金": 1, "钻石": 2}) # 无序:OneHot from sklearn.preprocessing import OneHotEncoder enc = OneHotEncoder(sparse_output=False, handle_unknown="ignore") city_encoded = enc.fit_transform(df[["城市"]])

OneHot要注意稀疏矩阵的维度:类别数一多,列数暴涨是正常的。比如城市有50个,就会生成50个二值列。初学时容易做成一张巨大的稀疏DataFrame,后面做特征选择还是喂模型,都会变笨重。如果类别太多,要么先做基数压缩(合并小众类别),要么用稀疏矩阵的存储格式。

4.5 分箱:把连续特征离散化

连续金额特征有时候直接喂模型也行,但要提取“高价值/中价值/低价值”这种粗粒度特征,分箱是标准操作。

df["单价分箱"] = pd.cut(df["单价"], bins=3, labels=["低", "中", "高"]) df["数量分箱"] = pd.qcut(df["数量"], q=3, labels=["少", "中", "多"])

pd.cut按数值等宽划分,适合分布均匀的场景;pd.qcut按分位数等频划分,适合偏态分布,能保证每个箱子里样本量接近。我实际做项目时偏好用qcut,因为大多数业务数值都是长尾的,等频划分不容易出现某个箱子空空的局面。分箱之后,连续值变成了有序类别,再按有序类别做映射编码,整个链条就通了。

5. 完整案例:从原始订单表到模型输入

5.1 目标设定

我把前面零散的操作串成一个完整流程。目标很明确:给定一份含脏数据的订单表,输出一份可以直接喂给模型做分类/回归训练的干净数据。

为了贴近实战,我重新合成一份稍大一点的模拟数据(代码里可能是随机生成的,但你实际操作时换成自己的数据就行)。这份数据包含:订单号、会员等级、商品类别、单价、数量、订单时间、城市、备注、是否投诉(目标列,1表示投诉,0表示不投诉)。

5.2 完整代码流程

下面这段代码是我按自己项目的习惯整理的,你可以当模板用,改字段名和数据路径就能跑。

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, OneHotEncoder, MinMaxScaler from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # ===== 1. 读取原始数据 ===== df = pd.read_csv("orders.csv", encoding="utf-8-sig", parse_dates=["订单时间"]) # ===== 2. 清洗阶段(快速过一遍) ===== df = df.drop_duplicates(subset=["订单号"], keep="last") # 删除关键时间缺失的行 df = df.dropna(subset=["订单时间"]) # ===== 3. 预处理阶段 ===== # 3.1 类型统一 for col in ["会员等级", "商品类别", "城市"]: df[col] = df[col].astype("category") # 3.2 缺失值填充:单价用同类中位数,备注填"无备注" df["单价"] = df["单价"].fillna( df.groupby("商品类别", observed=False)["单价"].transform("median") ) df["备注"] = df["备注"].fillna("无备注") # 3.3 特征构造 df["下单星期"] = df["订单时间"].dt.dayofweek df["下单小时"] = df["订单时间"].dt.hour df["订单金额"] = df["单价"] * df["数量"] # 3.4 异常值检测(标记行,不直接删) Q1 = df["订单金额"].quantile(0.25) Q3 = df["订单金额"].quantile(0.75) IQR = Q3 - Q1 df["金额异常标记"] = np.where( (df["订单金额"] < Q1 - 1.5 * IQR) | (df["订单金额"] > Q3 + 1.5 * IQR), 1, 0 ) # ===== 4. 划分数据集 ===== target = "是否投诉" X = df.drop(columns=[target, "订单号", "订单时间"]) y = df[target] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # ===== 5. 用ColumnTransformer组合数值和类别两套处理 ===== numeric_cols = ["单价", "数量", "订单金额", "下单星期", "下单小时"] categorical_cols = ["会员等级", "商品类别", "城市"] preprocessor = ColumnTransformer([ ("num", StandardScaler(), numeric_cols), ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols) ]) # ===== 6. 把预处理丢进Pipeline,和模型一起管理 ===== from sklearn.ensemble import RandomForestClassifier pipeline = Pipeline(steps=[ ("preprocessor", preprocessor), ("classifier", RandomForestClassifier(random_state=42)) ]) # 训练时自动完成 预处理 + 建模 pipeline.fit(X_train, y_train) # 测试时自动用训练集学到的规则处理测试集 score = pipeline.score(X_test, y_test) print("准确率:", round(score, 4))

5.3 ColumnTransformer和Pipeline带来的体验升级

这段代码里有两个进阶用法,我花点力气说明白。

ColumnTransformer让你可以把“数值列标准化”“类别列OneHot”两条处理流写在一个对象里。这个设计有现实意义:一张表很少有清一色的数值列或清一色的类别列,多数情况是混杂的。以前的做法是先手动把两拨列分开处理再拼回来,手忙脚乱还容易对错列名。ColumnTransformer帮我们把这些步骤统一起来,fit之后自动记住每个列的统计量,transform时自动套用。

Pipeline把预处理和模型绑成一个整体,意义不只是代码整洁。我最大的感受是它强制保证了“训练/测试的一致性”:训练集fit完了,测试集自动只transform,不会出现你忘了同步scaler的尴尬。而且调参时可以直接对preprocessor里的参数做网格搜索,而不是手动改一堆全局变量。这是从“会调包”过渡到“工程化处理数据”的关键一步。

5.4 模型选择会影响预处理策略

我在上面用了随机森林,它对标准化不敏感,所以我加StandardScaler更多是演示,如果换成逻辑回归或SVM,标准化就是必需品。这个点要放在脑子里:预处理策略不是一套走天下,而是要配合算法类型。

树模型:标准化可选,类别编码支持度好,缺失值部分可以交给树自己处理(但你最好还是先自己处理一遍)。 线性模型/距离模型:标准化必需,类别编码尽量OneHot或有序编码,缺失值必须填充,不能交给模型。 神经网络:MinMaxScaler或StandardScaler都比较常用,类别数据做OneHot,数值输入最好在相近量级。

我在实际项目中换模型时,通常会把Pipeline里的预处理部分一块改了,不会只换Classifier那一行。

6. 常见问题与排查技巧

6.1 高频踩坑速查表

我把自己和身边同事踩过的坑整理成下面这张表,按“问题-原因-解决”列出来,遇到情况直接对照。

问题表现常见原因解决方式
模型训练报“could not convert string to float”某些类别列或文本列没有编码检查DataFrame的dtypes,确认类别列已编码或OneHot
训练集效果正常,测试集效果崩数据处理时在全集上fit了scaler或编码器严格先split再fit/transform,或全部放进Pipeline
时间列排序不对字段还是字符串类型pd.to_datetime统一转换,必要时指定format
OneHot后列数爆炸类别基数太高做低频类别合并,或改用稀疏矩阵、CatBoost原生类别支持
缺失值填充后,字段方差变成0比如全部填了同一个中位数改用分组填充、插值,或把“是否缺失”单独做成一个标记列
drop_duplicates没删掉重复没有指定合适的subset明确业务主键,比如“订单号”或“订单号+商品编码”
标准化后模型没变化用了树模型这不是问题,树模型本就不依赖尺度,为官方演示可以换线性模型看效果

6.2 缺失率过高的字段:果断放弃还是抢救一下

缺失率超过40%的字段,很多人会纠结。我的经验是分情况:如果这个字段业务价值很强,比如“用户年龄”缺失率50%,我会考虑用其他特征做预测填充(KNN或回归);如果字段本身只是辅助信息,比如“用户职业”缺失率70%,我通常直接删掉,因为强行填充容易制造大量虚假信号,得不偿失。

如果你要试KNN填充,有个实用提醒:KNN填充的k值不要太大,5到10即可,而且先标准化特征再算距离,语义才正确。代价是计算量大,数据量超10万行会明显变慢。我一般不把它作为默认手段,只在业务上确实需要这个字段时才用。

6.3 inplace=True到底要不要用

pandas 2.x对inplace参数的警告越来越多,很多方法已经明确标注弃用。我现在的习惯是:一律不用,用链式写法或直接赋值。比如:

# 不建议 df.dropna(inplace=True) # 建议 df = df.dropna()

表面看只是写法差异,但前者调试时容易忽略“当前df已经被修改”的状态,而直接用赋值语法,每一步的返回结果都清清楚楚。在处理一张几百列的大表时,这种可读性差异会直接决定你排查bug的效率。

6.4 稀疏向量和DataFrame之间的坑

OneHotEncoder返回稀疏矩阵,很多人直接pd.DataFrame(sparse_matrix)会发现列名丢失、内存翻倍。我的建议是:如果是为了建模,保持稀疏矩阵直接喂给sklearn模型,性能更好;如果为了看数据或导出csv,再转DataFrame,并且要带上get_feature_names_out()的列名。

encoder = OneHotEncoder(sparse_output=True) encoded = encoder.fit_transform(df[["城市"]]) # 建模直接用encoded,不需要转DataFrame # 如需导出,可这样处理 dense_cols = pd.DataFrame( encoded.toarray(), columns=encoder.get_feature_names_out(["城市"]) )

在sklearn 1.2之后的版本里,sparse_output参数取代了旧的sparse,这个细节容易踩坑。版本差异带来的API变化是常态,我遇到报错的第一反应是查当前sklearn版本的文档,而不是随便搜一段旧代码复制过来。

6.5 一个治本的习惯:把预处理过程做成函数

我在项目里会把整个预处理流程包装成函数,输入是原始DataFrame,输出是模型输入矩阵。这样做的直接好处:新数据来了,一行调用就能走完所有流程;而且能保证训练和上线用的是同一套逻辑,不会出现“训练集手工处理、线上接口缺了个转换”的经典事故。

def preprocess_for_model(raw_df, is_train=True): df = raw_df.copy() # 清洗 + 预处理全步骤... return X, y

这个习惯看似简单,但在实际工程里价值极大。你会发现当数据更新了一轮,重新训练模型时,不需要把清洗脚本翻出来一行行看,只要验证函数输出符合预期就行。

7. 最后说点个人体会

我真正把数据预处理当成一个独立环节来认真对待,是从第一次被“数据泄露”打败开始的。当时我用全局标准化做完了训练和测试,准确率高得离谱,一上线真实预测就拉胯。后来查了一下午才发现就栽在fit和transform的顺序上。从那以后,我给自己定了一条铁律:所有涉及数据转换的环节,必须先划分数据集,再套Pipeline。这条规矩救了我无数次,希望你也能从我的翻车经历里提前躲开这个坑。

数据预处理不像建模调参那么有“技术含量”,但它决定了模型的上限。一个字段类型没统一,可能让后续所有特征工程白做;一个缺失值处理策略拍脑袋,可能让整个分析结论失真。我的建议是,用这篇笔记里串好的顺序和模板,先把手头数据老老实实跑一遍,遇到意外情况翻开这篇对照排查。等你跑熟了,自然会对哪些步骤该合并、哪些参数该调整有自己的判断。后面笔记我会继续写特征工程的内容,那就是在预处理基础上更进一步了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询