拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Pandas数据清洗与预处理全指南:缺失值、重复值、异常值处理实战

Pandas数据清洗与预处理全指南:缺失值、重复值、异常值处理实战

做数据相关工作的人,多少都经历过这种场面:辛辛苦苦把数据拿回来,打开一看,日期列里混着"2024/03/01""2024-3-1""20240301"三种写法;订单金额有几千个空值;同一个客户在同一分钟里下了三遍单;商品名称列里的中文字符全变成了乱码。这时候脑子里只有一个念头:用Pandas清洗数据,赶紧把这些乱七八糟的东西收拾成能用的样子。数据清洗和数据预处理,是数据分析流程里最不性感但最决定成败的一步。这篇文章不讲大道理,就围绕Pandas库,把数据清洗和数据预处理中那些高频操作、踩坑细节,以及真实项目里的处理思路完整过一遍。适合刚接手脏数据的分析新人,也适合准备大数据开发面试的同学,以及所有被各种奇奇怪怪数据折磨过的朋友。

1. 数据清洗到底在解决什么问题

1.1 数据清洗解决的核心矛盾

很多人以为数据清洗就是把空值删掉、把重复行去掉,好像是一个机械劳动。实际上,数据清洗解决的是四个层面的矛盾:可信度、一致性、可用性和性能。

可信度,指的是数据本身能不能信。空值、重复值、异常值都会让统计结果失真。比如一份订单表里金额为空,你直接算平均客单价,分母把空值排除掉,算出来可能虚高。一致性,指的是同一件事在不同行里表达得不一样。最典型的就是日期格式、单位、编码方式不统一,还有城市字段里"北京"和"北京市"并存。可用性,指的是字段本身能不能直接用。很多原始字段是一大坨文本,比如地址是"广东省深圳市南山区XXX路1号",你要分析城市维度,就得把它拆出来。性能,则是数据类型不合理导致内存爆炸,比如把整数存成了字符串,1亿行数据白白多占几倍内存。

这里可以用一个生活类比:从超市买回来的菜不是直接下锅的,要摘掉烂叶子、洗掉泥、按菜谱切好配好。Pandas就是那把锋利的菜刀,但菜刀不会替你做判断——哪些叶子该扔、哪些该留,是清洗者根据业务规则决定的。这个"判断"才是数据清洗里最值钱的部分。

1.2 清洗和预处理的分界线

我经常被新手问:数据清洗和数据预处理到底有什么区别?我的判断标准很粗暴:凡是让数据"变正确"的操作叫清洗,凡是让数据"更适合算法或分析"的操作叫预处理。

清洗是删、填、改、去重,目标是把错误消除掉。预处理是变换、缩放、编码、构造特征,目标是让数据形态更规整。举例来说,把字符串"10万"转成数值100000,这是清洗,因为它把不规范的表达改正确了;然后把所有价格做归一化,让特征落在0到1之间,这是预处理,因为这个操作没有改变"正确性",只是调整了量纲。

实际操作中两者会反复重叠。把字符串转成datetime类型,既是在清洗,因为日期原来没法比较大小,也是为了后续时间序列分析做预处理。一条清洗流程里经常混着预处理的动作,这不奇怪。关键是心里要有数:这一步到底是在消除错误,还是在为建模做准备。逻辑清晰了,脚本才不会写着写着变成一团浆糊。

1.3 不管项目大小,先搭一条清洗流水线

不管拿到的是几千行的表格,还是几千万行的日志,我基本都会走同一条流水线:读取数据、概览、结构修复、缺失值处理、重复值处理、异常值处理、派生字段、保存输出。

读取之后第一件事不是急着清洗,而是先看全貌。df.shape看行列数,df.info()看字段类型和空值情况,df.describe()看数值分布,df.head()看前几行的样子。这一套下来,数据长什么样、哪里容易出问题,基本心里有数了。概览这一步很多人跳过,结果清洗到一半发现列名全是空格,或者某个字段被读成了object类型,又返工。

结构修复的意思是先把列名规范化、把明显的类型错误修正,再把清洗动作接上去。我在每个关键步骤后面都会打印一版数据量对比,比如"去重前10000行,去重后9800行,去掉200行"。这在项目汇报里特别有用,也能帮自己快速定位是哪一步把数据改坏了。

提示:清洗脚本一定要留中间结果。我习惯在每个阶段保存一份带后缀的CSV,比如raw、clean1、clean2,宁可多占点磁盘,也比出了问题找不到原因强得多。

2. 动手前的准备:安装、数据结构和文件读写

2.1 在PyCharm里装Pandas的正确姿势

安装Pandas是很多人卡住的第一道坎。其实就一条命令:pip install pandas。在PyCharm里,最省事的方式是直接打开Terminal窗口执行,PyCharm会自动使用当前项目的解释器。

实测下来,现代Python版本的Pandas都是二进制轮子装包,pip会直接下载编译好的文件,基本几分钟内就能装完。真正容易出问题的是网络环境:如果下载慢或者超时,建议换国内镜像源,比如在pip后面加 -i 参数指定镜像。装完以后验证方法很简单,在Python环境里执行 import pandas as pd,然后打印 pd.version,能输出版本号就说明成功了。如果用的是Anaconda,那Pandas通常已经预装,直接import即可。

很多教程会让你用PyCharm的Settings -> Project -> Python Interpreter里点加号搜索安装,这条路也可以,但对于公司内网和网络受限的环境,直接在Terminal里敲命令更可控。

2.2 用最顺手的姿势创建Series和DataFrame

Pandas最常用的两个数据结构就是Series和DataFrame。Series可以理解成一列带标签的数据,DataFrame就是多行多列的表。刷头歌课程里"pandas数据结构创建"练习的时候,很多人被绕晕,其实只需要记住三种最常用的构造方式:用字典构造DataFrame、用列表构造DataFrame、用numpy数组构造DataFrame。

import pandas as pd # 字典构造:每个键成为一列 df1 = pd.DataFrame({ "订单号": ["A001", "A002", "A003"], "金额": [100, 200, None], "城市": ["北京", "上海", "广州"] }) # 列表构造:列表里套字典或列表 df2 = pd.DataFrame([["A001", 100], ["A002", 200]], columns=["订单号", "金额"]) # 从numpy数组构造 import numpy as np df3 = pd.DataFrame(np.random.rand(3, 2), columns=["x", "y"])

从字典构造最符合业务直觉,键就是列名,值就是列数据。从列表构造适合数据量小、手动拼接的场景。从numpy构造适合后面要接机器学习的场景。Series则更轻量,比如单独取一列出来分析时,它本质就是Series。理解了这个,后面所有操作都不会跑偏。

2.3 CSV、Excel和文本文件读写的坑,一次讲完

文件读写是数据清洗的入口,读写错了,后面全白搭。读CSV最常踩的几个坑:编码不对导致乱码或报错、分隔符不是逗号、第一行不是列名、某些列被自动读成了object类型。

我在处理中文CSV时,read_csv里最常用的是这套组合:

df = pd.read_csv( "订单数据.csv", encoding="utf-8", # 如果乱码试 gbk/gb18030 dtype={"订单号": str}, # 防止前导0被丢掉 parse_dates=["下单时间"], # 把日期列直接解析成datetime keep_default_na=False # 避免把空字符串也当成NaN )

encoding是重灾区。CSV文件可能是utf-8、gbk、gb18030,还有带BOM的。遇到UnicodeDecodeError,不要慌,逐个试,用gb18030的成功率很高,因为它覆盖的字符集比gbk更全。订单号这类字段一定要用dtype指定成字符串,否则"012345"会被读成12345,前导0直接消失,这个错误非常隐蔽。

读到Excel文件的坑相对少一点,重点是sheet_name参数。默认读第一个sheet,如果你要的数据在第二个表里,却忘了指定,分析结果就会莫名其妙地不对。文本文件用read_table,本质是read_csv的变体,只是默认分隔符是制表符,读日志类数据时经常用到。

写文件的坑也有一个很典型:to_csv之后Excel打开乱码。解决办法是保存时指定encoding="utf-8-sig",它会写入带BOM的utf-8,Excel就能正常识别了。我踩过好多次,后来直接把这个参数写进习惯操作里。

3. 清洗三大件:缺失值、重复值、异常值的处理

3.1 缺失值:先搞清楚数据为什么缺

处理缺失值的第一步不是fillna,也不是dropna,而是先统计一下缺哪些列、缺多少、缺的分布有没有规律。用一行代码可以快速看全局:

df.isna().sum()

拿到结果后,要判断缺失的机制。数据缺失通常有三种情况:完全随机缺失、随机缺失、非随机缺失。完全随机缺失影响相对小,删掉或者用均值填充影响都不大。随机缺失跟其他字段有关联,比如高收入人群更不愿意填年龄,这时候简单填均值就会引入偏差。非随机缺失则是最麻烦的,缺失本身就携带业务信息,比如某个传感器坏了导致数据断档,这种缺失恰恰是需要发现的信号。

处理方式上,如果缺失行数占总量的比例极低,比如5%以内,而且这些行对分析目标没有特殊意义,直接dropna是成本最低的。如果缺失字段是有业务含义的重要字段,比如价格、年龄、评分,那就需要考虑fillna。填什么值取决于业务场景:连续数值优先考虑中位数或均值;趋势性数据用前向填充ffill或后向填充bfill;分类字段可以用众数。但要注意,fillna只是把洞补上,不代表数据变准了,它只是让后续的统计计算能跑起来。

3.2 重复值:去重前先想想"业务重复"这回事

df.duplicated()和df.drop_duplicates()是去重的一对组合拳。但最容易出问题的是"重复"的定义。完全相同的整行,去掉没有争议。但现实中更多是部分字段重复——同一订单出现两次,订单号相同但价格字段有一个为空;同一用户在同一时间重复提交了表单;同一职位在不同渠道被重复采集。

处理这类业务重复,要用subset参数指定判断依据。比如招聘数据里,判断"同一岗位在同一天内重复发布",依据应该是公司名、职位名、发布日期三个字段的组合:

df.drop_duplicates(subset=["公司名", "职位名", "发布日期"], keep="first", inplace=True)

keep参数也很关键。keep="first"保留第一次出现的行,keep="last"保留最后一次。我一般先按时间排序再决定保留哪一条,因为业务上"最后一次状态"往往更接近真实。如果同一条数据在不同渠道被重复采集,保留哪份、合并哪些字段,都必须在清洗文档里写明。

3.3 异常值:别上来就删,先分类

异常值处理是我见过最容易犯错的环节,很多人一看到数值离谱就直接删掉,结果把真实波动当成噪声清走了。我建议先把异常值分成三类:录入错误型、业务特殊型、真实极端型。

录入错误型,比如金额为负数、年龄为200岁、价格突然变成0,这种通常是系统bug或人工误录,可以直接修正或删除。业务特殊型,比如促销订单里出现1分钱商品,虽然不符合常规价格分布,但它是真实现象,直接删掉会把活动分析结果带偏。真实极端型,比如股价暴涨暴跌,这种恰恰是分析的重点。

筛查异常值的常用方法有两个:IQR法和3σ法。IQR法用四分位距,把小于Q1-1.5倍IQR或大于Q3+1.5倍IQR的点标出来。3σ法假设数据近似正态,把偏离均值超过3个标准差的值标出来。这两种方法都只是候选名单,最终要不要处理,得回到业务逻辑判断。

我自己处理订单金额异常时,会把异常数据单独导出来逐条看,而不是直接删。很多次都发现"异常"背后是真实业务活动,比如大客户批发采购、测试订单、退款前的状态。清洗的目标不是把数据洗得好看,而是把数据洗得可信。

4. 类型转换、正则提取与时间序列预处理

4.1 数据类型转换:astype和to_datetime怎么配合

Pandas读入数据后,经常出现"数字变字符串"的情况。最直接的手段是astype,但astype有个毛病:遇到无法转换的值会直接报错。比如一列价格里有"暂无"两个字,你执行df['价格'].astype(float),整个程序直接崩溃。这时候要用pd.to_numeric,配合errors="coerce",转不了的值变成NaN,后续再单独处理这些NaN。

df["价格"] = pd.to_numeric(df["价格"], errors="coerce") df["下单时间"] = pd.to_datetime(df["下单时间"], errors="coerce")

to_datetime也是清洗日期数据的主力。它最厉害的地方是能自动识别"2024/03/01""2024-3-1""20240301"这几种常见格式,把整列统一成datetime类型。遇到完全解析不了的值,errors="coerce"可以把它变成NaT,也就是缺失时间。这一步做完,日期排序、按月聚合、算时间间隔就都顺了。

还有一个常被忽略的优化:把低基数的字符串列转成category类型。比如城市列就几十个不同取值,用category存,内存能减少一大截,groupby的速度也会提升。这个操作不会改变数据内容,属于低成本高收益的预处理。

4.2 正则表达式在清洗里的高频用法

正则表达式在数据清洗里的地位,几乎是"万能拆字段工具"。Pandas的str系列方法里,我用得最多的是str.extract、str.replace、str.contains。

比如从地址里提取省和市:

df["省份"] = df["地址"].str.extract(r"(.+?省|.+?自治区|北京|上海|天津|重庆)") df["城市"] = df["地址"].str.extract(r"(.+?市)")

从招聘信息里拆分薪资范围:

df["月薪下限"] = df["薪资"].str.extract(r"(\d+)k-(\d+)k")[0] df["月薪上限"] = df["薪资"].str.extract(r"(\d+)k-(\d+)k")[1]

实际写法上,str.extract会把括号里的内容提取成一个新列,如果有多个括号,就提取成多列。str.replace支持正则替换,可以批量清理文本里的特殊符号、全角空格、连续空白符。str.contains则用来筛选包含特定关键词的行,比如判断职位名称里是否包含"数据"。

正则写起来容易出错,我的经验是先在在线正则工具或Python里对一两行数据反复测试,确认无误后再套到整列上。坑主要在中文匹配上:中文里有些字符全角和半角混用,建议先做统一的字符清洗,比如把全角数字转半角、去掉空格,再做正则提取。

4.3 ewm函数参数到底怎么选

搜索热词里专门有"pandas中ewm函数参数",说明大家在用ewm时确实容易卡住。ewm是指数加权移动平均,用于平滑时间序列、突出趋势、减少噪音。它比简单移动平均的加权方式更聪明:离当前时刻越近的数据权重越大。

ewm函数的主要参数有com、span、halflife、alpha,这四者是等价的,指定其中任意一个,Pandas会自动推算出对应的alpha。alpha是基础加权系数,公式是:span = 2/alpha - 1,com = (1-alpha)/alpha,halflife对应权重衰减到一半所需的时间。实际业务里,span最直觉:你想让平均窗口大约覆盖最近多少个周期,就填多少。比如按天数据想平滑最近7天的影响,可以设span=7。

还有一个参数min_periods很实用,它控制窗口里至少要有多少期数据才输出结果。数据开头部分样本太少,均线会剧烈跳动,设一个min_periods可以避免前几个月被极端值带偏。

注意:ewm做平滑不等于预测,它只是把历史序列变顺,为后续的趋势分析提供更稳的输入。我用它处理农产品价格日数据时,效果比直接看原始价格曲线清楚很多,但周期性的节假日价格波动还是需要结合业务判断,不能盲目依赖平滑结果。

4.4 编码与标准化:把分类字段变成算法能吃的东西

进入机器学习前的预处理,绕不开分类编码和数值标准化。Pandas里最方便的分类编码是pd.get_dummies,把一列城市名拆成一堆0/1列。这种方法简单直观,适合类别数量少的场景。类别一多,列数爆炸,就需要考虑用factorize或者更专业的编码方式。

pd.factorize会把每个类别映射成一个整数编号,适合对类别间的顺序没有特别要求的情况。如果要排序性质的自定义映射,比如把"低/中/高"映射成1/2/3,直接用map或replace更可控。

数值标准化我常用的是min-max归一化和z-score标准化。前者把数值缩放到0到1,后者把数据变成均值为0、标准差为1。选择标准很简单:如果后续算法对数值范围敏感,比如聚类、KNN、神经网络,就要做标准化;如果是树模型,标准化一般不是强制要求。注意,如果数据里有异常值,min-max归一化会被拉偏,最好先处理异常值再做归一化。

5. 三个真实场景复盘:招聘、农产品价格、网约车

5.1 招聘数据清洗:拆薪资、修城市、去重复职位

招聘数据清洗几乎涵盖了所有基础操作,是特别好的练手项目。很多实验课程里都有"MapReduce综合应用案例——招聘数据清洗",这在大数据框架里能做,但在数据量不大时,用Pandas清洗会更高效,逻辑也更直观。

拿到一份招聘数据,通常先解决几个问题。第一,薪资字段往往是一个完整字符串"15k-25k"或者"15-25K·14薪",要用正则拆成最低薪资、最高薪资两个数值列,再把K换算成真实金额。第二,城市字段可能是"北京-海淀区"这种组合,需要split拆出城市,或者保留"城市-区县"作为单独维度。第三,同一职位被多渠道重复采集,需要按公司、职位、发布日期去重。第四,有些字段缺失严重,比如学历要求有60%是空的,这时候要评估是删行还是把缺失单独作为一个类别。

我用Pandas处理这类数据时,习惯先做一遍"逐字段体检",输出每个字段的非空率、唯一值数量、样例。体检报告一出来,哪些字段可以放心用、哪些字段要重建,一目了然。这个习惯比任何高级算法都管用。

5.2 农产品价格数据清洗:单位、市场和异常价格

农产品价格数据清洗是另一个经典场景,对应热搜里"农产品价格数据清洗--python"和"农产品价格数据清洗-spark"。这种数据通常来自不同市场的采集系统,第一个大坑就是单位不统一:有的市场记元/公斤,有的记元/斤,还有的记元/吨。清洗时要先把单位统一,否则算出的平均价完全失真。

第二个大坑是价格异常。农产品价格波动剧烈是正常的,但清洗时要把录入错误识别出来:价格为0、价格为负数、价格突然变成前一天的10倍以上。我的处理方式是先算每天每种产品的价格中位数和标准差,把偏离中位数超过一定倍数的记录单独拎出来人工核对。因为节假日、天气灾害导致的价格暴涨是真实信息,不能一刀切删掉。

日期字段也经常有问题。采集员可能漏填日期,导致时间序列断档。此时需要看有没有相邻的市场、同类产品价格可参考,或者用前向填充,但一定要在报告里标明哪些日期是补的。对农产品价格这种强周期数据,做ewm平滑后再看趋势,图会干净很多。

5.3 网约车数据清洗:时间、坐标、轨迹去噪

网约车大数据项目(基于Spark或MapReduce)看起来很"大数据",但清洗的核心逻辑和Pandas是一样的,只是换了个执行引擎。Pandas适合做抽样数据和小规模数据的探索,等逻辑跑通了,再平移到Spark里去处理全量数据,这是很合理的路线。

网约车数据清洗绕不开这几个点:时间戳格式要统一,下单时间、上车时间、下车时间要能被比较先后顺序;订单记录里会出现同一订单号重复上报,而且不同副本之间经纬度有偏差,去重时不能只看订单号,还要结合时间戳;坐标字段会出现明显的越界值,比如经纬度落在城市范围之外,这是GPS漂移,需要按城市边界过滤。

还有一种更隐蔽的问题:行程时长和距离算出来的速度异常。比如两分钟开了50公里,这种记录大概率是GPS漂移或数据采集错误。我的经验是给速度设业务上下限,超过就标记成异常,而不是直接删除,因为有时候是司机跨城市接单,速度上限可以适当放宽。

6. 实战里最烦人的几个坑和排查思路

6.1 中文乱码与编码打架

中文乱码是我见过最多的抱怨。读取时解码错误,要么抛异常,要么读出满屏"锟斤拷"。排查思路是先搞清楚文件是什么编码。Windows下Excel另存的CSV默认是gbk,很多工具生成的CSV是utf-8,还有带BOM和不带BOM的区别。在不确定时,我建议用二进制方式打开文件,把开头几个字节打印出来判断。

写入时也容易出问题。前面提到的to_csv(encoding="utf-8-sig")是解决Excel打开乱码的万能钥匙。如果要在Windows命令行里看打印结果不乱码,还可以考虑控制台编码设置,但更推荐避免在终端打印大量中文,把结果写到文件里看。

6.2 SettingWithCopyWarning:不是报错,但比报错更烦

Pandas里最常见的警告就是SettingWithCopyWarning。它不是报错,但意味着你的赋值操作可能没有作用到原DataFrame上。典型场景是:你先用df[df["城市"]=="北京"]筛选出一个子集df_sub,然后执行df_sub["价格"]=100,这时警告出现,而且价格可能根本没有写入原表。

根因是链式索引导致的视图和副本歧义。正确做法是筛选时直接加.copy(),比如df_sub = df[df["城市"]=="北京"].copy()。或者用loc在原始DataFrame上直接赋值:df.loc[df["城市"]=="北京", "价格"]=100。我在带新人时经常强调:筛选用于查询没问题,但要修改数据,要么copy出来改,要么用loc在原表上改,千万别在链式索引的中间结果上赋值。

6.3 索引错乱导致数据张冠李戴

Pandas的索引是隐形的,但一旦错乱,数据就会悄悄对不上。最常见的是dropna、drop_duplicates之后,索引还保留原来的行号。如果你这时候用索引去对齐另一个DataFrame,就会错位。解决方案很简单:清洗完成后执行reset_index(drop=True),把索引变成连续的0到n-1。

还有一个更隐蔽的坑:concat或merge两个DataFrame时,如果索引没有重置,会出现大量NaN。合并前后检查一遍索引类型和唯一性,能避免半夜调试到崩溃。

6.4 数据量大时内存和性能优化

Pandas处理几百万行是家常便饭,但到了几千万行就开始吃力。我的优化顺序是:先看数据类型,把数值列从float64降到float32,整数列尽量用int32或int64,字符串列转category;再看是否有用不到的列,读文件时直接用usecols只读需要的列;最后考虑分块读取,用chunksize参数边读边处理。

如果数据量真的到了Spark的规模,就不要硬用Pandas扛了。Pandas的价值在于快速验证逻辑、做小样本探索,而全量清洗可以交给Spark或数据库。记住这个分工,能省下大量时间。

我自己在实际操作中的一个体会是:数据清洗这门手艺,60%靠熟练,40%靠对业务的理解。同一个空值,在不同项目里该填均值还是该删行,没有标准答案。那些看起来很琐碎的判断,最后决定了一个模型是靠谱还是跑偏。踩过几次坑之后,我现在做任何清洗任务都会习惯性地先写一份数据体检摘要,把每个字段的非空率、类型、样例统计出来贴到项目文档里。最后再分享一个小技巧:把清洗流程封装成一个个小函数,用管道思想串起来,每一步都留日志。这样不管数据来源怎么换,清洗逻辑都能复用,项目交接的时候,别人接手也轻松得多。

返回列表