
做数据分析的人谁还没跟脏数据搏斗过好不容易从业务系统导出一张表结果一打开空值、重复行、类型错乱、日期变成字符串再加上好几张表要按关键字段拼到一起光预处理就能耗掉大半天。今天这期是“99天精通Python”系列的第49天我们把Pandas进阶的数据清洗与合并一次性讲透。不管你是刚入门Python、还在纠结怎么安装pandas包还是已经在用pycharm跑脚本、被各种清洗问题搞得头大这篇都能给你一套能直接抄作业的方案。我会用自己实际踩过的坑来拆解而不是罗列文档。你说你不记得dropna和fillna到底有什么区别没关系我会从“为什么需要”讲起再给你完整的实操代码和排查思路。数据清洗看起来琐碎但它是整个分析流程的地基数据合并则是把零散信息拼成全景的关键动作。这两件事做好后面画图、建模都会顺很多。1. 先从根儿上想明白我们为什么要清洗和合并数据1.1 数据清洗是“让脏数据变干净”的必经之路现实中的原始数据很少是教科书里那种规规矩矩的表格。最常见的几个问题字段缺失、记录重复、格式不统一、类型混乱。比如同一个用户ID一张表里是整数10001另一张表里是字符串10001再比如日期列有的存成2024-01-01有的存成2024/1/1。如果你直接拿这些数据去分析轻则图表错乱重则统计结果完全跑偏。我在处理某份销售数据时遇到过很典型的情况订单金额列里有负数还有零乍一看好像是异常值但后来跟业务确认发现负数是退款零是赠品。如果不先清洗直接把金额做sum最后销售总额就会比真实值少一大截。所以说清洗不是简单地“把脏的删掉”而是要在理解业务含义的前提下把数据规整成可分析的结构。1.2 数据合并是把“碎片信息”拼成完整视图的关键一步实际工作中数据往往分散在好几张表里。用户信息在A表订单记录在B表支付流水在C表。你想分析“哪些地区的用户最喜欢买某类商品”就必须把这几张表按用户ID、商品ID等关联键合并起来。Pandas提供了一整套合并机制从简单的concat到灵活的merge对应不同场景。我见过不少新手遇到需要合并多张表时就一脸懵明明是两张表不就是上下拼一下或者左右拼一下吗为什么还要区分inner、outer、left、right其实你只要想清楚一个核心问题合并之后不匹配的行要不要保留保留哪些这就是你选择哪种合并方式的基本逻辑。2. 数据清洗实操缺失值、重复值、类型转换与异常值2.1 缺失值处理的全套路别急着删先弄清“为什么缺”拿到数据第一步我习惯用df.info()和df.isnull().sum()快速摸清缺失情况。很多初学者一看到NaN就直接dropna()这是大忌。缺失值可能是业务上的正常情况比如“客户备注”字段本来就是可选的也可能是采集或导入时产生的错误比如某一行整列都是空。最稳妥的做法是分三步走。第一步统计每列缺失比例如果超过60%这列基本没什么价值该删就删第二步根据业务判断缺失的含义如果是“用户年龄”缺失可以用众数或中位数填补如果是“订单金额”缺失绝不能乱填因为金额是核心业务指标第三步选择合适的填充方法。Pandas里最常用的是fillna()它可以填充常量、使用前向/后向填充还可以配合groupby进行分组填充。我之前做过一个订单表不同地区的运费缺失我就按“地区分组后的平均运费”来填充比全局平均值靠谱得多。下面这段代码是我常用的缺失值处理模板import pandas as pd # 模拟一份订单数据 df pd.DataFrame({ 订单号: [A001, A002, A003, A004], 用户ID: [1, 2, 3, None], 金额: [100.5, 0, -50, 200], 日期: [2024-01-01, 2024/1/2, 2024.1.3, None] }) # 1. 查看缺失情况 print(df.isnull().sum()) # 2. 按列策略填充 df[用户ID] df[用户ID].fillna(0) # 未知用户用0占位 df[日期] df[日期].bfill() # 日期缺失用后一行值填充示意需按实际情况 # 3. 删除缺失占比过高的列 if df[备注].isnull().mean() 0.6: # 如果备注列缺失太多 df df.drop(columns[备注])这里还要提一个容易被忽略的点fillna(methodffill)和bfill()在处理时间序列时特别好用但用在普通订单数据上可能会把不相关的上下行强行关联造成数据污染。所以一定要结合上下文别过度填充。2.2 重复值检测与去重不是所有重复都要删重复值处理的核心是duplicated()和drop_duplicates()。但这里有两个坑。第一个坑你以为某两行完全一样才叫重复实际上业务上只需要判断“关键字段是否重复”。比如用户表里只要email相同就视为同一个人哪怕名字和手机号有细微差异。第二个坑去重时保留哪一行可能影响后续结果。我建议在处理重复值时先问自己重复是“完全重复”还是“主键重复”如果是后者你需要指定subset参数。比如订单表里一个订单可能出现多条记录但真正有效的可能是“状态最新”的那条。那么就可以先按时间排序再去重保留第一条。# 按订单号去重保留最新状态记录 df_orders df_orders.sort_values(更新时间, ascendingFalse) df_orders df_orders.drop_duplicates(subset[订单号], keepfirst)这里keepfirst的含义是保留第一次出现也就是最新一条因为我们排序了。如果你没有排序直接去重保留的是原表顺序里的第一条结果可能不是你想要的。这个细节我至少被坑过三次所以写出来提醒大家。2.3 数据类型转换与时间处理merge抽风的一个关键元凶Pandas里的数据类型问题非常隐蔽。比如to_numeric()可以强制转换但遇到无法转换的值时会报错或变成NaNastype()虽然简单但用不好也会报错。我遇到最多的问题是用merge合并两个DataFrame时明明键值看起来对得上但就是匹配不上。最后排查了半天发现一个键是int64另一个是object字符串。所以数据清洗阶段必须要统一关联键的类型。日期处理也是重灾区。Excel里看着是日期读出来可能变成时间戳字符串。推荐用pd.to_datetime()统一转换并指定format参数提升速度。比如df[日期] pd.to_datetime(df[日期], format%Y-%m-%d)。如果数据里有各种奇怪格式可以先errorscoerce把无法解析的置成NaT再人工处理。# 统一键类型都转成字符串 df1[用户ID] df1[用户ID].astype(str) df2[用户ID] df2[用户ID].astype(str) # 时间统一 raw_dates [2024-01-01, 2024/1/2, 2024.1.3] for d in raw_dates: parsed pd.to_datetime(d, errorscoerce) print(parsed)我个人的习惯是在数据清洗阶段把所有的ID列统一转为字符串因为ID通常没有数学意义字符串可以避免后面合并时类型不匹配也能防止整数ID因为前导零丢失比如员工编号00123变成123。2.4 异常值筛查技巧别让极端值毁掉你的统计异常值不一定是错误但你得先找出它们再判断怎么处理。最简单的筛查方法是描述统计加可视化。df.describe()可以快速看出最大值、最小值是否离谱画个箱线图能直观看到离群点。但更严谨的办法是使用z-score或IQR四分位距方法。比如金额列可以用IQR定义异常值超过Q3 1.5*IQR或低于Q1 - 1.5*IQR的点视为异常。但要注意如果你的数据本身不是正态分布或者业务上允许大额订单存在那么“统计学异常”和“业务异常”是两码事。我做电商数据时上千元的订单在“金额”列的IQR视角下可能算异常但人家就是正常消费。所以我的做法是先用IQR筛出潜在异常然后打印这些记录跟业务确认而不是直接删除。如果你实在没有业务确认的渠道也可以把它们单独标记出来比如新增一列is_anomaly保留备查。直接删掉风险很大万一那是老板的订单呢3. 数据合并实操concat、merge与join的选用3.1 concat纵向拼接和横向拼接的简单场景pd.concat是把两个DataFrame拼在一起的入门方法。最常见的用法是纵向拼接当你有多个相同结构的表比如按月份分存的销售明细需要合并成全量数据。这时直接用pd.concat([df1, df2], axis0)关键参数是ignore_indexTrue否则索引会保留原表的索引导致重复。横向拼接的使用场景是给原有数据加列比如把特征列合并成一个宽表但一般建议用merge更安全因为concat横向拼接不会帮你对齐行它是“直接排排站”。如果你的两个DataFrame行数不一致concat(axis1)会产生大量NaN容易把数据搞乱。# 纵向拼接 df_all pd.concat([df_1月, df_2月, df_3月], axis0, ignore_indexTrue, sortFalse) # 横向拼接确保索引一致 df_wide pd.concat([df_base, df_feature], axis1)3.2 merge像SQL一样做表关联merge是日常用的最多的合并函数它支持SQL风格的inner、left、right、outer四种方式。核心是how参数和on参数或left_on/right_on。什么时候用哪种我总结了一个简单判断如果你希望最终结果只包含两张表匹配上的记录用inner如果你希望保留左边的全部记录右边没有的填NaN用left保留右边全部用right两者都保留用outer。在真实业务里left是最常用的因为通常我们有一张主表比如订单表然后去补充其他维表信息比如用户地区。如果用户维表里缺少某些用户订单表还是要保留缺失的地区填NaN即可。这就是典型的left连接。merged pd.merge( df_orders, df_users, howleft, left_on用户ID, right_onid, suffixes(_order, _user) )注意这里我写了suffixes参数用来处理两个表里都有的重复列名。如果不写默认是_x、_y看起来很难受。建议指定有业务含义的后缀。3.3 join基于索引的快捷合并join其实是merge在索引上的一种快捷方式。如果你两个DataFrame的关联键就是索引那么用df1.join(df2)最方便。它默认是left连接且直接按索引对齐。比如从某个接口读到的数据索引就是用户ID另一个表也是同样的索引那么join一行代码就能完成。不过我的建议是如果数据量不大多用merge并显式指定键可读性和可维护性都更好。join更适合做索引层面的快速操作比如给某个按日期索引的数据拼一个节假日列。df_sales df_sales.set_index(日期) df_holiday df_holiday.set_index(日期) df_result df_sales.join(df_holiday, howleft)3.4 三种方法如何选型一张表帮你理清很多人在concat、merge、join之间反复横跳其实按使用场景很好区分结构相同、只是行上的拼接 →concat(axis0)结构不同、要按列横向扩展但行没对齐 →merge(how...)索引恰好是对齐键 →join或merge(left_indexTrue, right_indexTrue)我整理了一个对比表方法典型场景对齐方式关键参数缺点concat多表纵向堆叠、横向排布按位置axis, ignore_index横向无法智能对齐merge主表加维表、键匹配按指定列how, on, suffixes需要确保键类型一致join索引对齐的快捷操作按索引how, lsuffix, rsuffix不宜处理复杂关联选好方法后还要注意一个常见的“坑”merge默认是howinner。如果你以为它会保留左边的所有行但实际上把匹配不上的行全部扔掉了最后数据量变小了你才发现。所以在用merge时我会习惯先明确写出howleft而不是依赖默认值。4. 常见问题与排查技巧实录4.1 索引对齐陷阱明明有数据合并后却全是NaN有一种让我印象很深的错误用pd.concat([df1, df2], axis1)横向合并结果每一行都对不上全是NaN。原因就是两个表的索引没有对齐。比如df1的索引是0,1,2,3df2的索引是0,2,4,6横向合并时Pandas按索引对齐自然只有0和2能对上。解决方法是重置索引或者先set_index成共同键再用join。排查时可以用df.index.equals(df2.index)快速判断索引是否一致。4.2 数据类型不一致导致关联键匹配不上这是合并问题里最常见的一个我前文提过。两个表里的用户ID一个是整数一个是字符串用merge会有些能匹配上有些匹配不上。为什么因为Pandas在做匹配时会尝试类型转换但不同版本、不同数据情况下行为不一致。最保险的做法是提前把它们统一成字符串。照这个思路我写了个排查小函数专门检查两列的类型和unique值合并前跑一下就能发现隐患。def check_merge_keys(df_left, df_right, left_on, right_on): left_type df_left[left_on].dtype right_type df_right[right_on].dtype print(fLeft column type: {left_type}, Right column type: {right_type}) if left_type ! right_type: print(⚠️ 类型不一致请先转换) else: left_vals set(df_left[left_on].dropna()) right_vals set(df_right[right_on].dropna()) print(f公共值数量: {len(left_vals right_vals)} / 左侧{len(left_vals)} / 右侧{len(right_vals)})4.3 重复列名与后缀冲突用merge合并两张列名相同的表时如果不对后缀做处理Pandas会默认加_x、_y。这在短时间内没问题但当你拿到合并后的表做后续处理时就可能误用错列。比如两张表都有金额列一个是订单金额一个是退款金额如果不加后缀区分后续df[金额]会返回Series告警因为多个匹配。所以从合并一开始就要通过suffixes(_order, _refund)给列起好名字。同样join也需要通过lsuffix、rsuffix来规避冲突。4.4 数据量过大导致内存溢出处理几万行数据时Pandas很轻松但如果是几千万行merge可能直接把内存占满。这时候我一般会先优化数据类型把object列转成category把整数列降级为int32能大幅减少内存占用。如果还不够再考虑分块处理把大表拆成几个子集分别合并后拼接起来。更极端的情况就用Dask或Spark但那已经超出Pandas的范畴了。这里说一下小技巧# 压缩内存将id列转为category for col in [用户ID, 商品ID]: df[col] df[col].astype(category)这个方法在重复率高的列上效果明显。我之前处理一个千万级订单表用户ID重复率极高转成category后内存减少了一半多。5. 用一个真实案例把清洗和合并串起来5.1 案例背景某电商平台的订单与用户数据分析为了让你更直观地看到全流程我设计一个场景我们有三张表——orders订单表含订单号、用户ID、商品ID、金额、下单日期、users用户表含用户ID、注册城市、年龄段、products商品表含商品ID、分类、单价。目标是想分析“不同城市的用户对不同商品分类的消费金额”。这个分析需要先清洗各表再按用户ID和商品ID合并成一张宽表。5.2 一步一步实现清洗与合并第一步加载数据后先做基础检查orders pd.read_csv(orders.csv) users pd.read_csv(users.csv) products pd.read_csv(products.csv) # 检查基本信息 print(orders.info()) print(orders.isnull().sum())第二步清洗订单表。我通常检查订单号的唯一性删除完全重复的行金额列里面可能有负数退款但这里我们只分析正向消费所以过滤掉退款日期统一转成datetime类型。orders orders.drop_duplicates(subset[订单号]) orders orders[(orders[金额] 0) (orders[金额].notna())] orders[下单日期] pd.to_datetime(orders[下单日期])第三步清洗用户表和商品表。用户表的城市列有大小写混合、前后空格等问题统一处理商品表的单价列可能是字符串比如带“元”先提取数字再转float。users[注册城市] users[注册城市].str.strip().str.title() products[单价] products[单价].astype(str).str.replace(元, ).astype(float)第四步合并三张表。先让订单表左连接用户表得到城市和年龄段再左连接商品表得到分类和单价。左连接能确保所有有效订单都保留。analysis orders.merge(users[[用户ID, 注册城市, 年龄段]], on用户ID, howleft) analysis analysis.merge(products[[商品ID, 分类]], on商品ID, howleft)最后按城市和分类分组汇总result analysis.groupby([注册城市, 分类])[金额].sum().reset_index() print(result.head())5.3 我在实操中总结的经验与踩坑心得这个流程看起来简单但每一步都可能出问题。我踩过的最大一个坑是orders表里的用户ID是字符串而users表里的用户ID是整数第一次合并后很多用户ID匹配不上导致大量用户城市为空。后来我加了检查函数发现类型不一致统一转换后问题立即解决。所以我现在做任何合并前都会先检查关联键的类型和唯一值。另一个心得是不要在一开始就对原始数据动手。先把原始数据备份一份每次清洗操作都新建一个DataFrame或明确标注操作步骤这样万一发现搞错了还能回滚。我通常会写一个清洗函数输入原始dataframe输出清洗后的dataframe每次调用时保留原始数据。再分享一个细节对于时间字段如果你只关心日期那么在转换后可以直接astype(datetime64[ns])但如果你还需要时区或更细粒度最好保留为datetime64[ns]并注意时区问题。我在处理跨时区订单时如果不先把时间统一成UTC后面按小时统计就会乱。最后如果你在安装pandas时遇到“Could not find a version that satisfies the requirement pandas”通常是网络源的问题。我的建议是通过清华源等镜像安装pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple。安装完再确认版本import pandas as pd; print(pd.__version__)。很多看似复杂的问题其实源头都在环境和数据质量上。6. 最后再聊几句个人心得我自己在带团队做数据分析时发现很多同学把时间花在学高级函数和模型上却忽略了最基础的数据清洗与合并。但说实话一个干净的数据集能让你后面的工作变快好几倍。做清洗时心态要稳不要嫌脏数据烦。多用info()、describe()、isnull().sum()这些“体检工具”把数据先摸透再动手。合并时别偷懒该写how就写how该检查类型就检查类型。这些看起来是“笨功夫”却最能避免返工。还有一个技巧想分享给大家可以在Jupyter Notebook或PyCharm里设置一个固定的数据检查模板把df.sample(5)、df.dtypes、df.isnull().sum()这些常用操作整合成一个函数每次拿到新表先跑一遍快速建立对数据的直觉。我有时候还会把清洗和合并的代码写成独立脚本用main()函数组织起来这样数据更新后重新跑一遍就能得到最新的分析结果非常省心。Pandas本身提供了非常丰富的功能但别被它的功能淹没。记住你的目标是让数据变得可分析而实现这个目标只需要掌握几把“核心手术刀”缺失值处理、重复值处理、类型转换、以及合并策略。把这四样练扎实市面上绝大多数数据预处理问题都能解决。今天这期的全部内容就到这里希望对你有帮助。