☰
数据预处理实战指南:从数据清洗到标准化
2026/9/27 2:00:19 网站建设 项目流程

简介:这是面向Python数据分析与机器学习初学者的一份PDF学习笔记,主要讲解Pandas库中的数据预处理环节,尤其是缺失值的识别与处理。文档以“4.11 数据预处理-1”为主线,先解释了缺失值为何常用NaN表示,再系统介绍处理缺失值的四类核心操作:dropna删除、fillna填充、isnull与notnull检测,以及interpolate插值。通过员工信息表、随机生成表等多个示例,详细演示了axis行列选择、how筛选模式、thresh保留阈值、subset指定列等参数的实际用法,并对比了前向填充、后向填充与线性插值在不同场景下的效果,便于读者在真实数据清洗中做出合理选择。由于缺失值会直接影响统计汇总和模型训练结果,掌握这套方法能为后续分析打下扎实基础。资源为单个PDF文件,整体大小仅375KB,轻巧方便,可直接打印或随时查阅。目前已有1349人学习,适合作为《Python数据分析》课程配套笔记、课前预习或课后复习的补充资料。

1. 数据预处理为什么比建模更决定项目成败:先看这份PDF想让你掌握的事

拿到一份名为“Python数据分析实践:数据预处理-1-new.pdf”的资料,很多人的第一反应是翻目录找代码。但做数据分析这行越久越明白,真正让一个项目翻车的从来不是模型选型,而是数据本身。缺失值怎么填、重复值怎么去、时间字段怎么解析、文本里混着的全角空格怎么清理,这些看似琐碎的操作组成了数据预处理的核心,也决定了后续分析和建模的天花板。这份资料适合两类人:刚接触 Python 数据分析、想系统补齐数据预处理基本功的初学者,以及已经在跑数据分析项目、但经常被脏数据折磨得想摔键盘的实践者。读完并照着练,你能得到一套从数据导入、数据清洗到数据转换的完整工作流,而这一章的落点先放在一个问题上——预处理到底在解决什么。

2. 先给数据预处理定位:数据导入、数据清洗、数据转换与特征工程的关系

2.1 数据预处理在整个数据分析流程里的位置

常见的数据分析流程可以粗略分成六步:业务理解、数据采集、数据预处理、探索性分析、建模或可视化、结果解读。数据预处理卡在采集和分析之间,是承上启下的关键环节。很多人直接跳过它,把原始 CSV 读进来就画图,结果图上全是异常值拉出的长尾,或者因为日期格式不统一导致时间序列断档。数据预处理不是可有可无的步骤,而是数据分析项目里最耗时的部分,在实际项目中常常占到 60% 以上的工作量。

这份 PDF 标题里写的是“数据预处理-1”,说明它是一个系列的第一部分,通常涵盖数据导入与预处理中最基础的内容。我的习惯是把预处理拆成四个子环节:数据导入、数据清洗、数据转换、数据缩减。导入解决“怎么把数据读进来”的问题,清洗解决“数据里哪些是脏的”的问题,转换解决“数据形态能不能直接喂给算法”的问题,缩减解决“字段太多要不要降维或采样”的问题。标题里明显是前三个,数据缩减一般放在更后面的章节。

别小看这个拆解。当你遇到一个陌生的数据分析项目时,先判断当前数据处于哪个环节,才不会拿着清洗的思路去处理导入问题。比如数据读进来全是乱码,那是导入环节的编码问题,你在清洗阶段怎么填缺失值都解决不了。常见做法是先跑一条 info() 看字段类型和数据量,再针对性地做清洗。

2.2 工具选型:pandas 是主力,但别忽略 numpy 和原生 Python

数据预处理的主力工具是 pandas,这一点没什么争议。DataFrame 的向量化操作让清洗效率非常高,而且 pandas 的 API 覆盖了从读取到转换的绝大部分需求。但一个完整的预处理工作流里,numpy 也扮演着重要角色,尤其是涉及数值计算、随机采样和数组操作时。numpy 的 ndarray 比 DataFrame 轻量,某些循环场景下手写原生 Python 也未必慢。

举个例子,判断一个字段是否满足正态分布,需要计算偏度和峰度。pandas 的 Series.skew() 和 Series.kurtosis() 可以直接算,但如果你要做更细的分位数计算或者生成随机样本做 bootstrap 验证,numpy 更顺手。做数据预处理时我一般默认导入这三个库:

import numpy as np import pandas as pd import matplotlib.pyplot as plt

pandas 负责表格操作,numpy 负责数值计算,matplotlib 负责把字段分布画出来辅助判断。这三件套几乎覆盖了数据预处理的所有场景。至于是否引入 scikit-learn 里的预处理模块,看情况,做标准化的时候可以直接用 sklearn.preprocessing,但如果只是简单清洗,手写公式更快。

选型上有一条边界要记住:pandas 擅长大规模向量化操作,但不擅长超大数据集(几十 GB 级别)的单机处理。那种场景要转向 Spark 或 Dask,标题里的 PDF 既然标注为“-1”,说明是入门向,不建议一上来就引入分布式工具。

2.3 最小复现:读入一份脏数据并做初步质量体检

任何预处理项目的第一步都是把数据读进来,然后做体检。我常用 pandas 的 info() 和 describe() 做第一轮探查。下面这段代码演示了读入一份典型的“脏”CSV 后,如何在三行代码内定位主要问题。

# 读入原始数据,先不指定 dtype,让 pandas 自动推断 df = pd.read_csv("user_log_raw.csv", encoding="utf-8") # 体检第一项:字段类型、非空值数量、内存占用 print(df.info()) # 体检第二项:数值字段的描述性统计,观察 min/max 是否离谱 print(df.describe())

逻辑说明:info() 会列出每个字段的数据类型和非空值数量。如果某个字段的 count 明显小于总行数,说明存在缺失值;如果某个本该是数值的字段显示为 object,说明里面混入了非数值内容,比如“1,200”这种带千分位逗号的字符串。describe() 默认统计数值字段的 count、mean、std、min、四分位数和 max。看到 min 是负数而业务上不可能是负数时,就要考虑是异常值还是录入错误。

参数说明:read_csv 的 encoding 参数在中文场景下非常关键。UTF-8 读不了 GBK 编码的文件,会直接抛 UnicodeDecodeError。如果报错,把 encoding 换成 "gbk" 或 "gb18030" 重试。另一个常用参数是 dtype,当你知道某个字段一定是字符串(比如用户 ID 以 0 开头)时,提前指定 dtype={"user_id": str} 可以避免 pandas 自动把 "00123" 转成数值 123。

做完这两步,你已经能回答“这份数据能不能用”的问题。后面的章节根据体检结果决定清洗策略。

3. 数据清洗的三个硬仗:重复值、缺失值和异常值怎么处理才不翻车

3.1 重复值处理:drop_duplicates 的 subset 参数是双刃剑

数据清洗里最常遇到的第一个问题是重复值。重复值分两种:完全重复的行,和某些关键字段相同的行。后者才是真正的坑,比如同一用户在一小时内重复点击,可能每次点击时间不同、页面不同,看起来是新记录,但业务分析时应该按会话去重。

pandas 的 drop_duplicates 默认按所有列判断去重,这在大多数场景下过于保守。如果你知道用户 ID 是唯一标识,应该指定 subset 参数:

# 按 user_id 去重,保留第一次出现的记录 df_deduplicated = df.drop_duplicates(subset=["user_id"], keep="first") # 按 user_id 和时间窗口去重,比如同一用户同一天只保留一条 df_session = df.drop_duplicates(subset=["user_id", "event_date"], keep="first")

逻辑说明:subset 里放的是判定重复的列名列表。keep 参数控制保留哪一条记录,可选 first(保留第一条)、last(保留最后一条)或 False(全部删除)。选择 first 还是 last 要看业务场景,如果在做点击流分析,用户会话的入口通常取第一次点击;如果在做库存分析,最后一条修改记录才是最新状态。

参数说明:drop_duplicates 还有一个容易忽略的细节——inplace 参数。老版本 pandas 里常见 df.drop_duplicates(inplace=True) 的写法,新版本更推荐直接赋值返回新对象。另外,去重前先确认字段类型一致,否则“123”和 123 会被当成不同值。

踩坑提醒:subset 不要拍脑袋选。先跑 df.groupby(["user_id"]).size().sort_values(ascending=False) 看用户重复分布,确认这个字段真的是业务唯一键后再去重。我在一个电商订单项目里就因为选了 order_id 做唯一键,结果同一个订单拆分成了多条子记录,去重后订单总数对了,金额却翻倍了。去重前先想清楚业务含义。

3.2 缺失值处理:直接删除还是填充,先看业务含义再动手

缺失值处理是数据清洗里争议最大的部分。初学者最容易犯的错误是看到缺失值就填均值或者直接 dropna(),完全不考虑字段的业务含义。缺失值处理没有银弹,只有一条原则:先弄清楚这个字段为什么缺失。

字段缺失的原因通常有三类:第一类是数据本身不存在,比如未注册用户没有手机号字段,这种缺失代表信息真空,填充反而是错误;第二类是采集环节漏采,比如埋点代码在 iOS 设备上没执行,这类缺失可以填充;第三类是字段本身可选,比如用户填问卷时选了“不透露”。这三类对应的处理方式完全不同。

# 方案一:删除缺失比例过高的字段 missing_ratio = df.isnull().mean() cols_to_drop = missing_ratio[missing_ratio > 0.8].index df = df.drop(columns=cols_to_drop) # 方案二:数值字段用中位数填充(比均值更抗异常值) df["age"] = df["age"].fillna(df["age"].median()) # 方案三:分类字段填充为特殊的“未知”类别 df["channel"] = df["channel"].fillna("unknown")

逻辑说明:isnull().mean() 一口气算出每个字段的缺失比例。缺失比例超过 80% 的字段,信息量已经很低,删除是合理选择。数值字段填充时,中位数比均值稳健,因为均值会被极端异常值拉偏。分类字段填充时必须用业务上不存在的字符串,比如 “unknown”,这样后续分组统计能单独看出这个类别的表现。

参数说明:fillna 的 method 参数在时间序列里很常用,method="ffill" 表示用上一个有效值填充,method="bfill" 表示用下一个有效值填充。做时间序列预处理时,ffill 适合填充缓慢变化的指标,bfill 适合填充偶发采集中断的字段。但这两者都不适合填充趋势性很强的数据,那会制造不存在的平台期。

血泪经验:填充均值前一定要先看分布。如果字段是长尾分布(比如用户消费金额),均值会被头部大额消费拉高,填进去后大部分样本都低于这个值,相当于给模型注入了偏差。长尾分布字段优先用中位数,或者干脆分箱填充。

3.3 异常值检测:IQR 和 Z-Score 的适用边界要分清

异常值检测是数据清洗里最像“玄学”的部分,因为“异常”本身依赖业务定义。网上最流行的两种方法是 IQR(四分位距)和 Z-Score,但它们的适用场景完全不同。

IQR 法的逻辑是:计算字段的 Q1(25% 分位数)和 Q3(75% 分位数),认为小于 Q1 - 1.5×IQR 或大于 Q3 + 1.5×IQR 的值是异常值。IQR 不假设数据分布,对偏态分布更宽容,适合处理收入、点击量这类长尾字段。

Z-Score 法的逻辑是:计算每个值距离均值有多少个标准差,通常 |Z| > 3 视为异常。Z-Score 基于正态分布假设,适合处理身高、体重这类近似正态的字段。对长尾分布直接用 Z-Score,会误杀大量正常的高值样本。

# IQR 法:识别并替换极端值 Q1 = df["amount"].quantile(0.25) Q3 = df["amount"].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 先看异常值数量再决定处理 outlier_mask = (df["amount"] < lower_bound) | (df["amount"] > upper_bound) print(f"异常值数量: {outlier_mask.sum()}") # 用边界值截断而非删除(保留样本量) df["amount_clipped"] = df["amount"].clip(lower=lower_bound, upper=upper_bound)

逻辑说明:clip 做的是“截断”,把超出边界的值强拉到边界值上,而不是删除样本。这在样本量紧张时是不错的选择,缺点是引入了人为修改的数据点。异常值数量占比低于 5% 时截断影响不大,超过 10% 时说明字段本身可能是长尾幂律分布,建议换对数变换而不是截断。

参数说明:1.5 这个系数是经验值。想更保守地保留数据,把系数加大到 3;想更激进地清洗,减小到 1。注意修改系数后要在分析报告里注明,否则复现时别人不知道为什么你的清洗结果和原始数据对不上。

IQR 和 Z-Score 的关系是:IQR 对异常值本身不敏感(分位数不会因为几个极端值大幅变化),而 Z-Score 的均值和标准差都会被异常值拉偏,造成“掩蔽效应”——异常值拉大了标准差,导致真正的异常值反而落在 3 倍标准差之内。所以做 Z-Score 检测前,先把明显离谱的脏值(比如负数金额、超过 100 岁的年龄)剔除,再算均值和标准差。

4. 数据类型转换与标准化:让数据在同一把尺子上对话

4.1 数据类型转换:object 转数值、字符串清理与时间解析的完整套路

读入 CSV 后最常见的混乱是字段类型。pandas 默认把看起来像数的列转成 int64 或 float64,但一旦列里混入一个“5000 元”这样的字符串,整列都会变成 object。这时候直接做数值计算会报错。数据类型转换是数据清洗和后续分析的桥梁,停留在这个阶段的数据还谈不上给模型用。

# 字符串里的逗号和货币符号先清理再转换 df["price_str"] = df["price_str"].str.replace(",", "").str.replace("元", "") # 使用 pd.to_numeric 转换,coerce 参数让非法值变成 NaN 方便追溯 df["price"] = pd.to_numeric(df["price_str"], errors="coerce") # 时间字段解析:统一格式,dayfirst 参数应对日/月歧义 df["event_time"] = pd.to_datetime(df["event_time"], format="%Y/%m/%d %H:%M:%S")

逻辑说明:str.replace 是逐字段替换,注意要先去掉千分位逗号再去掉货币符号,顺序反了会导致“1,000元”变成“1000元”之后又误删数字里的逗号。pd.to_numeric 的 errors 参数有三个可选值,errors="raise" 遇错抛异常,errors="ignore" 保持原样,errors="coerce" 把非法值变成 NaN。生产环境里用 coerce 最稳妥,之后通过 isnull() 定位哪些行转失败,再回源数据排查。

参数说明:pd.to_datetime 的 format 参数强烈建议显式指定。让 pandas 自动推断时间格式在数据量大时会非常慢,而且遇到 01/02/2024 这种日期会默认按“月/日/年”解析。如果你的数据是“日/月/年”,不指定 format 会导致日期错位。明确写 format 是数据预处理里性价比最高的习惯。

4.2 数值字段标准化:Z-Score、Min-Max 和 RobustScaler 怎么选

做完数据类型转换,下一步是让特征在同一尺度上。这一步叫特征缩放,目的是避免量纲差异影响后续的模型训练。比如用户的年龄(0~100)和消费金额(0~10000)同时作为特征时,距离计算会完全被金额主导。

三种常见缩放方式的差异如下表所示。选型前先用 describe() 看字段的分布形态,再做决定。

方法公式核心适用场景对异常值敏感度
Z-Score 标准化(x - mean) / std数据近似正态分布,后续用 SVM、线性回归敏感,均值/方差会被拉偏
Min-Max 归一化(x - min) / (max - min)数据有明确边界,需要映射到 [0,1] 区间极值完全取决于 min/max
RobustScaler(x - median) / IQR长尾数据,存在大量异常值不敏感,基于中位数和分位数
# 手写 Z-Score 标准化 mean_val = df["amount"].mean() std_val = df["amount"].std() df["amount_zscore"] = (df["amount"] - mean_val) / std_val # 使用 sklearn 的 RobustScaler 处理长尾字段 from sklearn.preprocessing import RobustScaler scaler = RobustScaler() df["amount_robust"] = scaler.fit_transform(df[["amount"]]).ravel()

逻辑说明:手写 Z-Score 好处是零依赖,坏处是没有把均值和标准差存下来。如果做模型部署,新数据进来时要用训练集同款参数转换,这时候手动保存 mean 和 std 就很麻烦。sklearn 的 scaler 对象自带 fit_transform 和后续的 transform 方法,fit 参数会被存在对象里,部署时用 joblib 或 pickle 把 scaler 保存下来即可。

参数说明:RobustScaler 默认用 IQR(四分位距)做缩放分母,比 Z-Score 对异常值宽容得多。standardscaler、minmaxscaler、robustscaler 三者都要求输入是二维数组,所以传 df[["amount"]] 而不是 df["amount"],否则会报维度错误。

缩放时机要记住:划分训练集和测试集之后,用训练集 fit,再分别 transform 训练集和测试集。如果先全量 fit 再划分,会发生数据泄漏,测试集的信息提前进入了训练过程,验证结果会虚高。这一点在数据分析面试里是高频考点。

5. 数据预处理避坑指南:五个让新手当场翻车的经典场景

5.1 编码乱码:UTF-8 读不进 GBK 文件,当场报错怎么办

现象:pd.read_csv("数据.csv") 抛 UnicodeDecodeError,或者文件能读但中文全是乱码。

原因:文件实际编码是 GBK 或 GB2312,而默认用 UTF-8 解码。Windows 环境下从 Excel 导出的 CSV 经常是 GBK 编码。

解决:尝试用 encoding="gbk" 或 encoding="gb18030" 重新读取。gb18030 是 GBK 的超集,兼容性更好。如果文件头部有非 ASCII 字符,读前先看文件字节:

with open("数据.csv", "rb") as f: raw = f.read(100) print(raw)

看前几个字节能判断是否带 BOM。带 BOM 的 UTF-8 文件开头是 b'\xef\xbb\xbf',此时需要 encoding="utf-8-sig",否则第一列列名会混入 \ufeff 字符。

5.2 缺失值不一定显示为 NaN,空字符串和 "N/A" 都是伪装者

现象:info() 显示某个字段非空值数量等于总行数,但数值计算时又报错或结果离谱。

原因:缺失值被填充成了空字符串,或者导入工具自动写入了 "N/A"、"null" 等占位符,pandas 不认它们为 NaN。

解决:读入后主动把所有常见的占位符替换为真正的空值:

df = df.replace(["N/A", "null", "None", ""], np.nan)

助理下划线参数可以加,replace 的第一个参数可以传列表,一次替换多种占位符。替换后重新跑 info(),再看非空数量。

5.3 时间字段解析后 UTC 和本地时间混用导致时间轴错乱

现象:可视化时时间序列出现周期性跳变,每天少 8 个小时。

原因:部分数据源返回 UTC 时间,部分返回东八区时间,直接合并后没有统一时区。

解决:事先约定所有时间统一为北京时间,落库前用 tz_localize 和 tz_convert 统一:

df["event_time_utc"] = df["event_time_utc"].dt.tz_localize("UTC") df["event_time_beijing"] = df["event_time_utc"].dt.tz_convert("Asia/Shanghai")

tz_localize 给无时区的时间加上时区,tz_convert 在不同时区之间转换。这两个方法只对 datetime 类型的 Series 有效,转换前先确认字段已被 pd.to_datetime 解析过。

5.4 去重后索引断裂导致后续合并错位

现象:drop_duplicates 之后做 merge,结果行数对不上,出现大量 NaN。

原因:去重后的 DataFrame 保留了原始索引(比如 0, 2, 5, 7...),而另一个表索引是连续的。merge 默认按索引对齐或按列对齐,一旦目标列不唯一,返回的行数会翻倍。

解决:去重后重置索引。

df = df.drop_duplicates(subset=["user_id"]).reset_index(drop=True)

reset_index 的 drop=True 表示丢弃旧索引,重建从 0 开始的连续索引。这个习惯应该在所有产生新 DataFrame 的操作后养成,包括 groupby 后 reset_index(),否则后续操作很容易遇到隐藏索引坑。

5.5 标准化后线性模型预测值不符合业务边界

现象:做完标准化和建模,预测出来的金额出现负数,业务上不可能。

原因:Z-Score 标准化没有约束输出范围,模型在标准化后的特征空间里预测,反标准化回原尺度后自然可能越过业务边界。

解决:如果业务上不可能出现负数,改用手动边界处理。预测值先反标准化,再 clip 到 [0, 正无穷):

pred_amount = pred_amount * std_val + mean_val pred_amount = np.clip(pred_amount, 0, None)

另一种做法是直接用 Tweedie 回归或对数变换让目标值落在正数空间,这种方案属于模型层面的选择,但数据预处理阶段要意识到:标准化保留了数据的相对关系,没有保留边界。

6. 把预处理固化成可复用脚本:用配置驱动和校验函数兜底

预处理做到熟练之后,一个很自然的诉求是:别每次都重新写清洗代码。我一般会把预处理抽成三个文件:config.yaml 存参数,preprocess.py 存处理流程,validate.py 存质量校验。config 驱动的好处是换数据集时不用改代码,只改配置。

配置字段至少包含:数据路径、编码方式、分隔符、需要去重的字段、缺失值填充策略、异常值截断边界。每个字段的配置都要有注释说明选择理由,否则过一个月自己都忘了当初为什么这么设置。

校验函数是很多人忽略的一步。我习惯在预处理前和预处理后各跑一次质量检查,对比核心指标。检查项包括:总行数变化、关键字段缺失比例、重复值数量、数值字段最大值最小值是否在业务阈值内。这相当于给预处理流程加一层保险,防止误操作。

def validate_report(df_before, df_after, key_col): before_rows = len(df_before) after_rows = len(df_after) print(f"行数变化: {before_rows} → {after_rows}, 减少 {before_rows - after_rows}") print(f"关键字段缺失比例: {df_after[key_col].isnull().mean():.2%}") print(f"重复值数量: {df_after.duplicated(subset=[key_col]).sum()}")

逻辑说明:这套校验不需要多复杂,核心是让每次预处理的结果可回溯。预处理最大的风险不是某个操作写错,而是多个操作叠加后数据悄悄变形。有了前后对比,至少能在早期发现行数异常骤减。

这个习惯帮我挡过很多次灾。之前做一个广告点击数据的分析项目,配置里把“空字符串替换为 NaN”和“删除缺失用户”连在一起,结果因为某天埋点全挂,用户字段全部为空,处理完直接删掉大半数据。幸好校验函数在预处理前后各跑了一次,行数变化一目了然,省了半天排查时间。

如果你的场景是团队协作,建议再把预处理脚本纳入版本管理,每个清洗步骤的调整都提交一次记录。数据预处理不是一次性工作,你改需求、接新数据源、修埋点 bug 时都会回来动它。留下一份能看懂、能复现、能追溯的脚本,比什么都重要。

希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询