☰
Pandas数据预处理实战:缺失值、异常值与标准化全攻略
2026/10/7 11:43:48 网站建设 项目流程

简介:这是一份面向Python数据分析学习的PDF笔记,主题围绕Pandas数据预处理中的缺失值处理,适合刚开始接触数据清洗的初学者,也适合需要在建模前夯实数据质量基础的实践者。笔记从缺失值产生的原因讲起,系统梳理了isnull/notnull检测缺失值、dropna按axis/how/thresh/subset等参数过滤缺失行或列、fillna使用指定值或ffill/bfill方向填充,以及interpolate线性插值等常用方法。每个知识点都配有可直接运行的DataFrame示例,例如对学生信息、商品销售等数据进行缺失值处理,并穿插动手练习,便于读者边学边练。资源为1个PDF文件,大小仅375KB,内容精炼、结构清晰,适合下载后随时查看。目前已有1349人学习下载,是一份能帮助快速掌握缺失值处理流程、提升数据预处理效率的实用资料。

1. 数据预处理在 Python 数据分析里到底在解决什么

看到“Python数据分析实践:数据预处理-1-new.pdf”这个标题里的“-1”和“new”,基本能判断这是一份系列讲义的开篇,或是某个项目更新后的第一版。数据预处理这个概念,说大不大,说小不小,但它是所有 Python 数据分析项目里最花时间、也最容易让结果翻车的环节。多数人以为拿到数据就能画图跑模型,实际上一份真实数据里,缺失值、重复记录、异常点、量纲不统一、编码错乱,随便哪一样都能让后续的统计分析偏到离谱。这个标题里如果要找一个最值得反复琢磨的词,就是“预处理”。

这份讲义解决的,正是从原始数据到一个能放心分析的数据表中间那段路。适合两类人:一类是刚学会 pandas 基础语法的新手,想系统走一遍 dirty data 的清洗流程;另一类是已经跑过几个分析项目但总被结果打脸的老手,想回过头看看数据是在哪一步被污染了。我自己做数据分析的习惯是七成时间花在预处理上,三成时间才轮到分析和可视化,顺序一旦反了,后面全是徒劳。接下来就按这个顺序,把数据预处理这件事拆开讲透。标题里的“new”也提醒了一件事:pandas 版本更新很快,几年前的预处理写法放到今天未必还能跑通,参数和 API 也一直在变,所以这篇文章里的代码我都会标注版本相关的地雷。

2. 为什么数据预处理决定了分析的下限:Pandas + NumPy 的取舍与分工

2.1 预处理到底包含哪几件事:清洗、转换、归约的边界

数据预处理不是某一个操作,而是一组操作的组合。按我平时的项目习惯,会把它拆成三个层次:数据清洗、数据转换、数据归约。清洗处理的是“脏”问题,包括缺失值、重复值、异常值,这部分解决的是数据对不对;转换处理的是“形”问题,包括数据类型转换、编码转换、衍生字段,这部分解决的是数据能不能用;归约处理的是“量”问题,包括降维、采样、特征筛选,这部分解决的是数据够不够精。这三个层次在实践里会交替出现,并不严格按顺序执行,比如清洗过程中发现某列全是空值,可能直接删掉,这既算清洗也算归约。

在 Python 生态里,pandas 是所有这些操作的载体。DataFrame 的向量化操作把原来需要写循环的脏活变成了单行调用,而 NumPy 提供了底层的数组计算能力,是 pandas 性能的基石。选型上没有太多悬念:如果你拿到的是结构化表格数据,pandas + NumPy 就是最稳的组合。有人拿 Spark 做预处理,那是数据量到了几十 GB 以上才需要考虑的事;有人拿纯 Python 手写清洗逻辑,那是数据量小到可以直接读进内存时自找麻烦。对于一份 PDF 讲义里讲的“实践”,默认场景一定是中小规模数据集,pandas 是唯一的合理答案。

2.2 从数据类型到计算速度:为什么这两件套就够用

pandas 的 DataFrame 里,每一列都有一个 dtype,读入数据后第一件事不是看统计量,而是先看 dtype 对不对。比如“白酒销售额”列如果是 object 类型,说明数值大概率被读成了字符串,这时候画图、算均值都会报错或得到荒谬结果,必须先做 astype(float) 转换。这种事在真实项目里几乎每天都会发生,尤其是从 CSV、Excel 或网页爬虫拿到的数据,格式很少是规整的。NumPy 在这里起的作用是提供底层数值计算支持,比如用 np.where 做条件赋值、用 np.abs 对全列做绝对值计算,它们比纯 Python 的列表推导式快出一个数量级。

这里需要强调一个性能边界:pandas 处理的数据量在千万行以内是舒适的,超过这个量级,groupby 和 merge 这类操作会明显变慢。我处理“网约车大数据综合项目”里那份订单数据时,两千多万行做一次去重用了将近两分钟,这时候就忍不住想起 Spark。但如果你只是处理几百万行的电商或餐饮数据,pandas 完全足够。另外还有一个容易被忽略的点:pandas 2.x 之后引入了 PyArrow 后端,某些操作在 arrow dtype 下更快,但兼容性问题也随之而来,我建议新手先锁死在默认的 NumPy 后端,等流程跑通了再考虑性能优化。

2.3 拿到一份新数据应该先做什么:三行命令快速体检

预处理的第一步不是写清洗函数,而是快速摸底。我一般会在拿到 DataFrame 后立刻执行三条命令:

import pandas as pd df = pd.read_csv('sales_data.csv', encoding='utf-8') # 第一件事:看数据的形状和字段类型 print(df.info()) # 第二件事:看数值列的分布形态 print(df.describe()) # 第三件事:看缺失值总量 print(df.isnull().sum())

这三行代码能在一分钟内告诉你:数据有多少行多少列,哪些列是数值型、哪些是对象型,数值列的均值、标准差、最小值最大值是否合理,哪些字段存在空值以及空值数量有多大。基于这三个结果,就能决定接下来要处理的优先级:对象类型但实际是数值的列需要转换类型,空值比例超过 70% 的列可以直接考虑删掉,最小值是负数但业务上不可能为负的列基本就是异常值。这一步做完,那份 PDF 标题里所说的“数据预处理”才真正进入动手阶段。

这里补充一个热词场景中很常见的例子:有人拿“白酒销售数据分析和可视化”练手,读取 Excel 后发现有“销售额”列是文本,而且在数值里有“万”字,这时候直接 astype(float) 会报错,需要先把字符串里的“万”替换掉再转换。这就是 dtype 体检的价值。另外,对于“npp夜间灯光数据预处理”这类遥感数据,通常不是 CSV 而是栅格格式,需要先转成 DataFrame 或者 NumPy 数组才能走 pandas 的清洗流程,这个转换本身也是预处理的一部分。

3. 用 Pandas 做缺失值与重复值处理:三种填充策略和参数上的坑

3.1 缺失值处理的三种策略:删除、填充、留空

缺失值处理是数据预处理里最基础、也最有争议的部分。拿到df.isnull().sum()的结果后,对应每一列要做一个决策:这个字段的空值,我是删、是填、还是先留着不处理?删除适用于空值比例高且字段本身不重要的列,或者那一行缺失多个关键字段的情况;填充适用于数值型字段且有业务逻辑可以依据的情况;留空则要谨慎,只有像“用户备注”这种不影响分析结果的字段才敢留着。我见过一些数据分析报告里,对所有缺失值一律填充 0,这是最粗暴也最容易误导后续结果的做法,因为缺失本身可能代表“没有记录”,也可能代表“数值极小无法记录”,两者意义完全不同。

填充的方式有三种常见的:用固定值填充,比如缺失的年龄统一填 -1 或 0;用统计量填充,比如用该列的中位数或众数;用前后数据填充,比如时间序列里的前向填充和后向填充。这三种没有绝对的好坏,取决于你分析的目标和数据的产生机制。处理白酒销量数据时,如果某一天的销量因门店未报数而缺失,用前一天的销量做前向填充是合理的;如果缺失的是用户年龄,用众数填充比用均值更稳,因为年龄分布通常有众数年龄段的聚集效应,均值会把分布拉偏。

3.2 dropna 和 fillna 的参数详解:thresh、method 与 inplace 的真实语义

用代码实现上述三种策略时,主要围绕dropna()和fillna()两个方法展开。下面这段代码覆盖了最常见的几种用法:

import pandas as pd import numpy as np df = pd.read_csv('sales_data.csv') # 策略一:删除空值比例超过 70% 的列 threshold = int(0.7 * len(df)) df.dropna(axis=1, thresh=threshold, inplace=True) # 策略二:对数值列用中位数填充 num_cols = df.select_dtypes(include=[np.number]).columns df[num_cols] = df[num_cols].fillna(df[num_cols].median()) # 策略三:时间序列列用前向填充 df['sales_amount'] = df['sales_amount'].fillna(method='ffill') # 策略四:对分类字段用众数填充 df['store_type'] = df['store_type'].fillna(df['store_type'].mode()[0])

这段代码里的几个参数需要逐个理解。dropna(axis=1, thresh=threshold)中,axis=1表示按列方向处理,thresh表示保留该列所需的最小非空值个数,这里计算的是“至少要有 30% 的非空才保留”,所以空值比例超过 70% 的列被删除。fillna(df[num_cols].median())表示用各列自己的中位数填充,注意.median()返回的是一个 Series,按列索引对齐到原 DataFrame,所以不同列会得到不同的填充值,这是正确的用法,如果填一个固定标量,则所有列都被填成同一个数。

fillna(method='ffill')是前向填充,即用上一行的非空值填补当前行的空值,这个方法在时间序列场景很常用。不过要注意,pandas 2.1 版本开始,fillna(method='ffill')这个写法被标记为弃用,官方推荐直接用df['sales_amount'].ffill()。也就是说,如果你的 pandas 版本较新,运行这段代码会出现 FutureWarning 警告,分析结果不受影响,但如果想消除警告,就改成.ffill()形式。

3.3 duplicated 和 drop_duplicates 的升级玩法:处理“伪重复”记录

重复值处理比缺失值看起来简单,实际坑更多。最基础的去重是df.drop_duplicates()按整行完全相同的标准去重,但真实业务里的重复往往不是这样:同一笔订单可能因为系统重传而多出几条记录,其他字段完全相同,但“上传时间”这一秒不同;也可能是用户反复提交表单,核心字段一致但附加字段有细微差异。这种“伪重复”记录如果按整行判断就永远去不掉,需要用subset参数指定判断重复的字段集合。

实际操作时,我会先看重复记录到底长什么样,再决定去重的字段范围:

import pandas as pd df = pd.read_csv('order_data.csv') # 第一步:看完全重复的行有多少 print(df.duplicated().sum()) # 第二步:按核心业务字段查找重复,比如订单号+门店+金额 dup_mask = df.duplicated(subset=['order_id', 'store_name', 'amount'], keep=False) print(df[dup_mask].sort_values('order_id').head(20)) # 第三步:去重,保留每组重复里第一次出现的那行 df.drop_duplicates(subset=['order_id', 'store_name', 'amount'], keep='first', inplace=True) # 第四步:售后数据里,重复上传的时间字段会被保留下来 df['upload_time'] = df['upload_time'].astype(str)

这里有两个关键参数要留意。keep参数有三个值:keep='first'保留每组重复的第一行,keep='last'保留最后一行,keep=False会把这组重复的行全部删除。选first还是last,取决于你的业务逻辑里哪一次记录更可信,比如“上传时间”字段的值,通常最后上传的那次数据修正过,反而更可靠,这时用keep='last'更合理。

另一个是subset的陷阱:如果去重时选择的字段过少,比如一个订单表里只按order_id去重,但同一个订单号在不同门店都出现过,这可能是同一个用户在两家门店分别下单,结果被误删了一条。所以subset字段的选择必须基于业务含义来判断,宁可在去重前多打印几条重复记录看一眼,也不要闭着眼睛直接传一堆字段进去。把这种“伪重复”处理掉之后,才能放心地跑后续的 groupby 聚合和透视表。

4. 异常值识别与量纲统一:从 Z-score 到标准化到底怎么选

4.1 用 IQR 和 Z-score 识别异常值:两种方法各自的适用脸

异常值检测是数据预处理里最像“玄学”的部分。同一个数据点,在这个分析方法里是错误,在另一个业务场景里可能就是有效信号。比如白酒销售数据里,春节前一周的销量突然暴涨 5 倍,这在常规统计里是明显的异常值,但如果分析目标是评估全年销售节奏,这个暴涨恰恰是必须保留的核心信息。所以我做异常值识别时,第一步永远是回到业务场景里确认:这个异常值是录入错误、系统 bug,还是真实业务波动?只有前两种才需要用算法识别并处理。

算法层面,最常用的两个方法是 IQR(四分位距)和 Z-score。IQR 方法不假设数据服从正态分布,用Q1 - 1.5 * IQR和Q3 + 1.5 * IQR作为上下界,偏态分布的数据用它更稳;Z-score 方法假设数据大致服从正态分布,通过(x - mean) / std将数据转换为标准正态分布,通常以绝对值大于 3 为异常点阈值。用代码实现如下:

import pandas as pd import numpy as np df = pd.read_csv('sales_data.csv') # IQR 方法:对销售额列计算上下界 Q1 = df['sales_amount'].quantile(0.25) Q3 = df['sales_amount'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 找出 IQR 方法下的异常值 iqr_outliers = df[(df['sales_amount'] < lower_bound) | (df['sales_amount'] > upper_bound)] print(len(iqr_outliers)) # Z-score 方法:用标准差来定位异常 from scipy import stats z_scores = np.abs(stats.zscore(df['sales_amount'])) z_outliers = df[z_scores > 3] print(len(z_outliers)) # 两种方法交叉比对,看哪些点是共识异常 common_outliers = iqr_outliers.index.intersection(z_outliers.index) print(common_outliers)

这段代码先分别用 IQR 和 Z-score 找出各自的异常点,最后做一个交集比对。实际项目里不建议只信一个方法,因为两种方法对同一条数据的判断经常不一致:IQR 方法对厚尾分布更敏感,Z-score 对极大值更敏感。如果两种方法都判定某条数据是异常,那它基本就是录入错误,可以放心处理;如果只有一种方法判定异常,就要回去看原始数据记录,做人工确认。

4.2 标准化与归一化:MinMaxScaler 和 StandardScaler 不是一回事

很多基于距离的算法,比如 K-Means、KNN、SVM,对特征的量纲极其敏感。量纲一不统一,数值范围大的特征会在距离计算中碾压其他特征,导致模型被某个无关字段主导。这时候就需要做量纲统一,常用两种手段:标准化(Standardization)和归一化(Normalization)。这两个词经常被混用,但在 sklearn 里它们是两个不同的类,行为差异也很明显。

标准化的公式是(x - mean) / std,处理后的数据均值为 0,标准差为 1,不改变数据分布形态,适合数据本身有异常值但又不打算删除的场景。归一化的公式是(x - min) / (max - min),把数据压缩到 0 到 1 之间,对最大值和最小值极其敏感,如果数据里有一个极端异常值,绝大多数归一化后的数据会被压到很小的数值区间里,分布信息被压缩。用代码实现如下:

import pandas as pd from sklearn.preprocessing import StandardScaler, MinMaxScaler df = pd.read_csv('sales_data.csv') # 特征列:销售额和门店面积,量纲差异大 feature_cols = ['sales_amount', 'store_area'] # 标准化 scaler_std = StandardScaler() df['sales_std'] = scaler_std.fit_transform(df[['sales_amount']]) # 归一化 scaler_mm = MinMaxScaler() df['sales_norm'] = scaler_mm.fit_transform(df[['sales_amount']]) print(df[feature_cols + ['sales_std', 'sales_norm']].head())

选择哪一种,取决于后续算法和业务解释需求。如果后面要做回归系数解释,标准化后的系数可以直接互相比较;如果后续要做图像类数据的像素值处理,归一化到 0~1 是标准做法。另外还有一个实操细节:fit_transform是在训练集上计算均值和标准差,然后应用变换;在测试集上必须用同一个 scaler 的transform()方法,而不能重新fit,否则测试集和训练集的数据分布不一致,模型评估就会失真。

4.3 异常值处理不是只有删除一条路:封顶和分箱

识别出异常值后,处理方式有三种:删除、封顶、保留。删除操作在数据量充足且异常值占比很小(比如小于 1%)时是合理的;数据量本身就不够时,删除会损失采样信息。封顶的意思是只把超出上限的值压到上限位置,比如把销售额超过 99% 分位数的值统一改成 99% 分位数对应的那个数值,这种做法保留了记录的存在性,但消除了极端值对后续统计的影响。

具体实现时,我用np.where来做条件替换:

import pandas as pd import numpy as np df = pd.read_csv('sales_data.csv') # 计算 99% 分位数,作为封顶上限 cap = df['sales_amount'].quantile(0.99) # 超出上限的数值全部替换为上限值 df['sales_amount_capped'] = np.where(df['sales_amount'] > cap, cap, df['sales_amount']) # 对比封顶前后的均值变化 print(df['sales_amount'].mean()) print(df['sales_amount_capped'].mean()) # 分箱操作:把连续变量转为分类变量,常用于消除量纲不稳定 df['sales_bin'] = pd.cut(df['sales_amount_capped'], bins=4, labels=['低', '中低', '中高', '高'])

封顶的阈值选择不能是拍脑袋的,常见做法是选 95% 或 99% 分位数,然后在分析报告里注明这个处理方式,因为变换之后统计指标的含义会变,读者需要知道这些数字是怎么来的。分箱操作则是另一种思路,把连续变量切成几个区间,后续做分组对比会更稳定,但会损失区间内的细节差异。

5. 数据预处理避坑手册:5 个让我返工重跑的常犯错误

5.1 读 CSV 时编码和分隔符不对,数据全变成一列

现象:pd.read_csv('销售数据.csv')运行后,DataFrame 只有一列,字段名和值混在一起,或者所有中文都变成了乱码方块。

原因:CSV 文件的实际编码不是默认的utf-8,可能是gbk或gb2312;或者是分隔符不是英文逗号,可能是制表符或者分号,导致整行内容被读进同一个字段。

解决:用pd.read_csv('销售数据.csv', encoding='gbk')先试中文编码;如果文件是爬虫抓下来的数据,很多来源会用 tab 或分号分隔,要显式指定sep='\t'或sep=';'。如果分不清编码,用with open(file, 'rb') as f: raw = f.read()去看二进制前几位的 BOM 标记。这个坑在“电商快递账单数据分析”里特别常见,快递公司导出的账单经常是 gbk 编码加 tab 分隔,读进来就翻车。

5.2 处理时顺手改了原始数据,分析结果再也没法复现

现象:第一次清洗跑完,得到一组分析结果;过了两天增加一个数据源后重新分析,结果对不上,回看时才发现自己当时忘记保存清洗后的副本,或者直接在变量上做了覆盖操作。

原因:在 Jupyter 里,df这个变量被反复修改,每次df = df.dropna()之类的操作都没有保存快照,原始数据被破坏了。

解决:养成一开始就把原始数据复制一份的习惯:df_raw = pd.read_csv('原始数据.csv'),之后的清洗只在df_clean上操作。每次都把清洗完成后的结果导出成新的 CSV,命名带版本号,比如sales_clean_v2.csv。这样踩坑后还有后悔药可吃。这一步不是代码问题,是工作习惯问题,但它比任何代码 bug 都容易毁掉一个分析项目。

5.3 inplace=True 的异常行为:某些 pandas 版本下不起作用

现象:运行df.drop_duplicates(inplace=True)后,程序没有报错,但df的行数没有变。

原因:pandas 从 1.x 到 2.x 版本,部分方法的inplace参数行为发生变化,还有一类方法本身支持inplace,但 DataFrame 被切片或视图引用时,inplace操作可能不会作用到原变量。

解决:不要依赖inplace=True,统一写成重新赋值的形式:df = df.drop_duplicates()。这样虽然多复制一次内存,但行为特别明确,没有歧义。如果你已经用了inplace=True而且发现没生效,先检查有没有 FutureWarning,那通常是弃用警告,升级 pandas 版本后这个参数会被彻底移除。

5.4 缺失值填充顺序没想清楚,填充后反而制造了错误数据

现象:对时间序列数据先做fillna(method='ffill'),但这个字段最开始连续 10 行都是空值,前向填充没有值可以借,于是这 10 行仍然是空值;再做dropna(),把这 10 行全删了,可这 10 行本来应该是有效数据,业务上只因为前期系统没上线才没有记录。

原因:前向填充只能补中间的空值,补不了开头和结尾的连续空值段;结尾连续空值也不能用前向填充,应该使用后向填充bfill()。

解决:先检查每个连续空值段的长度和位置,开头空值用bfill(),中间空值用ffill()或插值interpolate(),结尾空值用bfill()。更稳妥的做法是先看这段数据的业务,确认空值就是“系统未记录”之后,再决定是填一个业务占位值 -1 还是直接删除。

5.5 标准化后忘了保留 scaler 对象,测试阶段全部重做

现象:训练阶段用scaler.fit_transform()转换了特征,模型效果良好;到预测阶段对测试集直接套公式(test_df - 训练集均值) / 训练集标准差,结果漏了保存训练集的均值和标准差,于是在测试集上重新调用fit,训练集和测试集的分布不一致,模型评估结果完全不可信。

原因:StandardScaler.fit_transform()在训练时已经把均值、标准差保存在 scaler 对象内部,但很多人只记住了转换公式,不知道要持久化这个对象。

解决:把 scaler 对象保存下来,后续处理时test_transformed = scaler.transform(test_df);跨脚本使用时用pickle.dump(scaler, open('scaler.pkl', 'wb'))保存,新脚本里pickle.load读回来直接用。这个坑我记得特别清楚,之前做“制造业数据分析”项目时就是因为忘了这步,标准化后的训练集和测试集范围不一致,一个准确度还不错的回归模型彻底不可信了。

6. 用缺失率矩阵给数据预处理做验收:三天不用返工的工作流

预处理做完之后,怎么知道做得干不干净?我的习惯是做一个“缺失率+重复率+异常值占比”的综合体检表,一次性输出所有字段的预处理效果,再针对不合格的字段做第二轮定向处理。这个思路用一个函数就能实现:

import pandas as pd import numpy as np def preprocess_audit(df, id_col=None): result = pd.DataFrame({ '缺失率': df.isnull().mean(), '重复行数': [df.duplicated(subset=[col]).sum() for col in df.columns], '数值异常占比': [ np.nan if not pd.api.types.is_numeric_dtype(df[col]) else ((np.abs(df[col] - df[col].mean()) / df[col].std()) > 3).mean() for col in df.columns ], '数据类型': df.dtypes.astype(str) }) return result.sort_values('缺失率', ascending=False) df = pd.read_csv('sales_clean_v2.csv') audit = preprocess_audit(df) # 只看缺失率高于 5% 的字段 print(audit[audit['缺失率'] > 0.05])

这个函数会输出三列关键指标:缺失率反映有没有漏掉的空值字段,重复行数反映去重是否彻底(对非主键字段这个值允许非零),数值异常占比反映 Z-score 方法下异常值的残留比例。字段如果同时满足缺失率低于 5%、异常占比低于 1%、数据类型全部正确,那这份数据基本可以放心进入下一阶段。

我的个人习惯是,每轮预处理完都跑一次这个审计并保存结果,然后带着审计表去做分析。之前有一次处理中药材数据分析可视化的源数据时,我跳过审计直接画图,结果图里出现了几根畸形柱子,回头一查才发现是某个中药材的采购量字段混入了异常大值,Z-score 高达 8,当时要是先跑审计就不会浪费一整天排查。后来就养成了“先审计、后分析”的固定动作,数据分析的被认可度也明显提升了。

数据预处理的终局不是把所有数据都洗成完美的样子,而是让你清楚地知道每一列数据经历过什么处理、为什么这样处理、处理后还剩什么问题。这份 Python 数据分析实践讲义如果你跟着做一遍,最大的收获就是把数据预处理从“玄学”变成了“流程”。希望我的这些边角料经验,能帮你在自己的项目里少走几次弯路。

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

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

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

立即咨询