资讯详情

资讯详情

Pandas数据清洗与合并实战:从缺失值到DataFrame关联

做数据处理的人几乎每天都要跟“脏数据”打交道。你可能从 CSV 里读到一列本该是数字的字段结果混进来几个“N/A”和空字符串也可能要把两份来源不同的表按某个键拼起来却发现同一批订单号在两个表里格式都不一样。这也是我坚持更新《99天精通Python》这个系列的原因——Day 49 这一篇我想认真聊聊 Pandas 进阶里最实用、也最容易被忽视的两个基本功数据清洗与数据合并。这里说的清洗不是简单 dropna 一下就行而是包括缺失值处理、重复值识别、数据类型矫正、文本格式统一这一整套动作合并也不是 Excel 里 vlookup 一把梭而是搞清楚 concat、merge、join 分别在什么场景下用、参数怎么配、性能怎么控制。这篇笔记面向的是已经有 Python 和 Pandas 基础、但还没系统梳理过“脏数据处理流程”的读者。我把实际项目中踩过的坑、验证过的写法都放进来尽量做到让你看完就能直接拿去用。1. 数据处理的两块硬骨头为什么先把清洗和合并学透1.1 真实项目里的数据到底有多“脏”很多初学者有个错觉觉得拿到的数据都是像教材里那样干干净净的 DataFrame。但真实环境完全不是这样。我接过一份网约车订单数据光时间字段就有三种格式有“2024-01-01 12:30:00”这样的标准时间有“2024/1/1 12:30”这种斜杠格式还有少数直接是 Excel 导出的序列号数值。再看金额字段一部分是浮点数一部分是字符串还混着“--”表示缺失。这种数据直接拿来分析结果必然离谱。数据清洗本质上做的是“把数据恢复成它本来应该有的样子”而不是“把数据硬改成你想要的样子”。前者是对数据负责后者是自欺欺人。做清洗之前你要先搞清楚每列数据的业务含义、取值范围、合理格式再动手处理。这也是我在团队里反复强调的一点清洗脚本不能乱写每一步操作都要有依据要能解释“为什么这个缺失值要填充、那个缺失值要删除”。1.2 清洗和合并是分析流程的两道闸门在一套完整的数据分析流程里数据清洗和合并处在“获取数据”和“正式分析”之间位置非常关键。数据获取阶段可能产生缺失、重复、格式错乱这些脏数据如果不处理干净后面无论做统计、可视化还是建模都等于在沙地上盖楼。尤其是建模阶段模型对缺失值和数据格式异常敏感一列本该是数值的数据被读成了字符串训练出来的模型精度可能直接掉一大截。合并则是把分散在不同数据源里的信息重新拼回一起。比如做招聘数据分析你可能有岗位信息表、公司信息表、城市信息表需要按公司 ID 或城市码关联起来。这时候用错合并方式轻则数据行数翻倍重则把本来无关的记录错误地拼在一起。我见过一个项目就是因为 merge 时没有注意一对多关系导致结果里出现大量重复行业务指标全部失真最后排查了两天才发现。清洗和合并这个位置相当于两道闸门守不住后面全白搭。2. 数据清洗把脏数据收拾得服服帖帖2.1 缺失值处理先分清“真缺”和“假缺”Pandas 里表示缺失值的方式不止一种。最常见的是 NaNNot a Number来自 float 类型的空位还有 None来自 Python 对象的缺失再就是各种业务自定义的缺失符号比如空字符串、N/A、NULL、--。如果只用 isnull() 去判断会把 NaN 和 None 识别出来但空字符串和“--”就会被漏掉。这就导致明明数据里有缺失统计出来的非空数量却不少。我的处理习惯是先把数据“统一缺失表示”。用一个自定义函数把常见的缺失符号全部替换成 np.nan再进行统一的缺失值处理。这一步看着简单却是我在多个项目里都踩过的坑。比如某次处理商品订单表优惠券字段里大量是空字符串我直接用 fillna(“无”) 去填充结果发现空字符串根本没被填上因为空字符串并不是 NaN。让我浪费了不少时间。统一缺失表示之后再决定是删除还是填充。删除适用于缺失比例很低、且行列之间关联不强的场景填充则更多用于时间序列、连续型数值字段。填充方法有固定值填充、前向填充 ffill、后向填充 bfill、均值或中位数填充。我一般不会无脑用均值填充因为均值对异常值很敏感如果有明显的极值干扰用中位数更稳妥。比如薪资字段个别高管年薪很高拉高了均值这时候用均值填低薪员工的缺失值就会失真。2.2 重复值处理去重之前先想清楚“重复”的定义重复值去重看起来简单一个 drop_duplicates 就搞定但真正的难点是定义什么样的记录算重复。是整行所有列都一致才算重复还是只要某个关键列一样就算重复比如订单表里同一个订单号可能因为系统异常出现了两条记录但这两条记录的创建时间和状态不同你能说它们是重复的吗我会把去重分成两层来看。第一层是整行完全重复这种几乎可以无脑删除第二层是基于指定列的重复这种需要结合业务规则来判断。用 drop_duplicates 时subset 参数指定判断重复的列keep 参数决定保留哪一条。保留第一条还是保留最后一条取决于数据产生的时序。比如一批操作日志用户 ID 相同、动作为空但时间戳不同可能应该保留最后一条因为那才是用户最终的状态。还有一个很容易忽略的点去重之前先做排序。如果你打算保留每个用户的最后一条记录但原始数据顺序是乱的直接 drop_duplicates(keeplast) 可能保留的并不是真正的最后一条。所以稳妥的操作是先用 sort_values 按时间排序再去重然后重置索引。这看起来是小事对结果的影响却可能是决定性的。2.3 数据类型转换astype 和 pd.to_datetime 的坑Pandas 读 CSV 时经常会把应该是数值的列读成 object把应该是时间的列读成字符串。如果不做类型转换后面排序、聚合、画图都会出问题。astype 是最常用的类型转换方法但它在遇到无法转换的值时会直接报错。比如一列数据是 [1,2,3,abc]你用 astype(float) 就会抛异常而实际业务场景中你更希望知道“哪些值转不了”而不是让整个程序停下来。这个场景下我通常会先用 pd.to_numeric(errorscoerce) 做安全转换。errorscoerce 的含义是把无法转换的值强制变成 NaN而不是报错。这样既能保留能转的部分又能把脏数据统一成缺失值交给后续清洗步骤处理。同理处理时间字段时用 pd.to_datetime(col, errorscoerce) 能把各种格式的时间字符串统一成 datetime64 类型解析不了的变成 NaT。在转换完以后一定要检查转换前后的行数是否一致如果减少了不少说明有一批值没解析成功需要回头看看原始的格式是什么样的。2.4 文本清洗与格式统一数值和时间字段处理完文本字段往往是清洗工作的重头戏。一个用户名字段里可能带着空格、换行符、全角空格一个地址字段可能混着大小写和繁体字一个城市字段可能既有“北京市”又有“北京”。这些看起来不影响大局的小差异在 groupby 聚合时就会变成两个不同的组导致统计结果被拆散。我的文本清洗流程是先 strip 去掉首尾空白再 replace 干掉内部的全角空格和特殊换行符然后按业务规则做大小写统一或中文格式统一。如果有多个名称指向同一个实体我还会维护一个映射字典用 map 或 replace 做统一的替换。比如把“北京”、“北京市”、“北京地区”都映射成“北京”。虽然这步操作看起来繁琐但确实能避免很多下游分析中的诡异问题。3. 数据合并掌握 concat 和 merge 的选型逻辑3.1 concat纵向堆叠还是横向拼接concat 是最直观的合并方式把两个 DataFrame 直接在轴向上拼起来。axis0 时是纵向堆叠也就是把行拼在一起常用于汇总多个结构相同的表比如把 1 月、2 月、3 月的订单表堆叠成一张季度总表axis1 时是横向拼接也就是把列拼在一起常用于给同一批样本补充特征比如把订单基础表和订单扩展信息表按行号对齐拼起来。使用 concat 时有一个参数我经常强调ignore_index。纵向堆叠时如果两个 DataFrame 的索引都是默认从 0 开始直接 concat 后会出现重复索引后续用索引做操作时容易出问题。设成 ignore_indexTrue 后Pandas 会重新生成一套连续索引省去事后 reset_index 的麻烦。还有一个是 join 参数默认是 outer也就是并集若想只保留两边索引都有的记录可以设成 inner。这个参数选择非常关键不管你用哪个函数都要想清楚自己是要保留全部数据还是要保留交集。3.2 mergeSQL 风格的关联操作merge 是 Pandas 里最强大、也最容易出错的合并方式。它本质上对应 SQL 里的 join 操作支持一对一、一对多、多对多关联。最常用的参数是 how 和 on。how 有四种子集left左连接、right右连接、outer全连接、inner内连接。on 指定连接键可以用单个列名也可以用列表传多个列名。实际项目中我最常用的是 left 和 inner。left 表示以左边表为基准左边表的所有行都保留匹配不上的右边列填 NaNinner 表示只保留两边都能匹配上的行。这两个选择的业务含义有本质区别你做一张“所有用户及其订单数”的报表用户表在左边用 left join 才能保证没有订单的用户也出现如果你想分析“有订单的用户行为”才用 inner join 过滤掉无订单用户。很多人合并结果行数不对就是这里搞反了。还有一点值得注意merge 的时候如果没有显式指定 onPandas 会自动使用两个 DataFrame 里所有共同列作为连接键。这看起来很智能但实际很容易造成意外的笛卡尔积或匹配错误。我的习惯是永远显式指定 on 列哪怕只有一列。同时建议用 validate 参数来校验合并关系例如 validateone_to_one 或 validateone_to_many。一旦实际数据结构和预期不符Pandas 会直接报错这能帮你尽早发现数据问题。3.3 join索引对齐场景下的便捷选择join 方法可以看作是 merge 的一个特例它默认按索引进行连接而不是按列。在数据清洗和合并的实际操作中当两个 DataFrame 都经过处理、索引代表同一含义比如用户 ID 或日期时用 df1.join(df2) 会比写一长串 merge 参数更简洁。不过我用 join 时特别小心一种情况两个 DataFrame 索引有重复。如果索引不唯一join 会产生笛卡尔积式的膨胀行数突然暴增。所以用 join 之前我一般会先确认 df.index.is_unique 是否为 True。如果不是先做去重或 reset_index再决定是否用 join。另外join 默认是左连接如果你想做全连接需要显式传入 howouter。3.4 合并后处理索引重置、列名冲突和性能控制合并完成不代表工作结束后续收尾同样重要。首先要检查合并结果的行数是否符合预期比如左连接后的行数不应超过左表的行数如果超过大概率是右表连接键有重复形成了一对多膨胀。其次要关注列名冲突。merge 时如果两边有相同的非连接列Pandas 会自动生成 _x 和 _y 后缀这很容易让人迷惑。我的做法是合并前先用 rename 把重复列名改掉或者在 merge 后立即对 _x、_y 列做清理和重命名。在做多个大表合并时concat 和 merge 的性能也不容忽视。如果 DataFrame 达到几千万行内存占用会迅速上升。我通常会在合并前删除不需要的临时列尽量缩小数据规模用 categories 类型替换字符串类型也能显著减少内存占用如果数据量实在太大可以考虑分块读取加逐块 concat 的方式避免一次性把全部数据载入内存。4. 实战案例招聘数据清洗与合并全流程模拟4.1 场景设定与模拟数据为了把前面这些方法串起来我用一个招聘数据场景来做完整演示。假设你有两份数据源岗位信息表 job_info.csv包含岗位名称、薪资区间、城市、发布时间公司信息表 company_info.csv包含公司名称、公司 ID、融资阶段、行业标签。两份数据都来自不同渠道存在格式不统一、缺失、重复等问题。目标是把两张表合并成一张分析宽表供后续统计“不同城市的岗位薪资分布”和“不同行业的岗位数量”使用。先造一份模拟数据。岗位表可能有 2000 行左右包含约 5% 的薪资缺失、部分城市字段为空字符串、还有少量整行重复。公司表相对干净一些但公司 ID 可能和岗位表中的关联键单位不一致比如一个是字符串“C001”一个是数值 1这在 merge 时会导致匹配不上。为了真实模拟我在演示代码里刻意保留了这些问题然后一步步处理。4.2 分步实现缺失值处理、类型转换、去重与合并第一步读取数据后先做概览。我会用 df.info() 看每列的非空数量和类型再用 df.head() 和 df.sample(10) 随机查看数据内容快速建立对数据的直觉。这一步不能省因为后续所有清洗策略都建立在你对数据现状的了解上。第二步统一缺失表示。把空字符串、N/A、-- 等符号替换为 np.nan。对岗位薪资缺失我选择用“所在城市岗位级别”的中位数填充而不是全局均值因为不同城市的薪资水平差异很大全局均值会把高城市和低城市混在一起。实现上可以先 groupby 城市求中位数再用 fillna 配合 transform 回填。对城市缺失的记录如果数量很少我会直接删除如果数量较多则用众数填充并在后续分析中注意这部分可能带来的偏差。第三步类型转换。把发布时间统一成 datetime64把薪资字段中的“15K-25K”解析成最低薪资和最高薪资两列。解析文本字段时我习惯用正则表达式提取数字部分再转成数值类型。这里有个技巧如果解析失败用 errorscoerce 转成 NaN后续再按缺失值处理。经过转换后数据集的可用性大幅提升。第四步去重。岗位表里可能出现“同一岗位名称、同一公司、同一发布时间”重复记录我判断为重复保留第一条。去重完成后检查一下还剩多少行。做这类操作时我通常会打印清洗前后行数对比方便在文档里直观展示清洗效果。第五步合并。先把公司表的公司 ID 统一成与岗位表一致的类型再执行 left merge以岗位表为基准关联公司信息。合并后立刻检查行数和列名确认没有行数膨胀也没有出现意外的 _x、_y 后缀。最后把合并结果保存成清洗后的 parquet 或 CSV 文件供后续分析使用。import pandas as pd import numpy as np # 读取数据 job pd.read_csv(job_info.csv) company pd.read_csv(company_info.csv) # 统一缺失表示 def normalize_missing(df): placeholders [, N/A, NULL, --, null] return df.replace(placeholders, np.nan) job normalize_missing(job) company normalize_missing(company) # 转换时间字段 job[publish_time] pd.to_datetime(job[publish_time], errorscoerce) # 薪资解析提取最低和最高薪资 job[salary_min] job[salary_range].str.extract(r(\d)K).astype(float) job[salary_max] job[salary_range].apply( lambda x: float(x.split(-)[1].replace(K, )) if - in x else np.nan ) # 城市缺失值填充众数填充 job[city] job[city].fillna(job[city].mode()[0]) # 薪资缺失值按城市中位数填充 city_median job.groupby(city)[salary_min].transform(median) job[salary_min].fillna(city_median, inplaceTrue) # 删除整行重复 job.drop_duplicates(inplaceTrue) # 统一公司 ID 类型 company[company_id] company[company_id].astype(str) job[company_id] job[company_id].astype(str) # 左连接合并 merged job.merge(company, oncompany_id, howleft, validatemany_to_one) print(merged.shape)这段代码把前面讲的清洗步骤串成了一条流水线。实际项目中可能更复杂但思路是一样的先摸清数据再统一缺失再纠正格式再去重最后关联。每一步之间都有依赖关系顺序基本不能乱。4.3 性能优化与内存控制招聘数据规模不算大但处理网约车订单那种千万级数据时性能问题就凸显出来了。我常用的三个优化手段是减少列只保留后续分析需要的字段多用 category 类型压缩重复字符串能分块处理的不要一次性读入。Pandas 在 2.0 之后还提供了 PyArrow 后端对大数据集的内存占用和计算速度都有明显提升我建议新项目直接尝试 pd.set_option(mode.string_storage, pyarrow) 配合新式 dtype。另外merge 操作本身也有性能差异。把连接键设为索引再用 join 方法有时候比 on 参数的列关联更快如果连接键列是字符串先排序或者先转成 category 都有可能提升匹配速度。这些优化手段不需要在一开始就全部铺开而是在数据量大到影响效率时再逐步加进去避免过度设计。5. 常见问题与排查技巧实录5.1 经典错误合并后行数异常膨胀我做过好几个项目最后发现结果不对一查都是 merge 产生了行数膨胀。最常见的原因就是连接键在右表中不是唯一的。比如你拿公司表和岗位表做 left 合并如果公司表里某个公司 ID 出现了两次这个公司的每一个岗位都会被复制成两份。行数从左表行数变成了左表行数与匹配次数的乘积。排查办法很简单合并前用 df[col].value_counts() 或者 df.duplicated().sum() 检查连接键是否有重复。如果重复就先把连接键去重或者确认业务上是否允许一对多。我还习惯在 merge 时加 validateone_to_many 或 validatemany_to_one让 Pandas 在关系不符时主动报错而不是默默产生错误数据。5.2 类型不一致导致的匹配失败连接键类型不一致是个隐蔽的问题。一边是整数类型的公司 ID一边是字符串类型的“C001”merge 直接把结果全设为 NaN看起来没有报错但实际匹配率为零。这种问题最坑的地方在于表面上看整个流程都走完了输出结果也不缺行但关键字段全是空的。遇到这种情况我会先打印两张表连接键的 dtype再随机看几个样本值。如果一个是 object一个是 int64就先统一成字符串再合并。这种检查虽然简单却是在团队协作中特别容易遗漏的地方。我建议把这项检查固化成自己编写合并逻辑时的固定步骤别管多急着出结果这一步不能跳。5.3 清洗方向搞反过度清洗反而破坏数据有一类错误很隐蔽不是操作失误而是“清洗过度”。比如某列是“评价星级”取值范围 1 到 5有人看到有几个值明显偏离比如“11”和“99”就顺手当成异常值删掉了。但业务上“11”可能是用户手滑输入错误直接删除或替换成中位数都算合理而“99”可能是业务方设定的“默认高分”标记有特殊含义不该被当成异常值。我的原则是清洗之前必须弄明白每个字段的取值范围和业务含义不确定的字段先不要动。如果确实拿不准某些值是不是脏数据可以把它们单独提取出来列成清单和懂业务的人确认后再决定怎么处理。这个习惯帮我避免了很多次“好心办坏事”的情况。5.4 常见问题速查表问题现象可能原因排查思路解决办法合并后行数暴增连接键在另一张表存在重复检查连接键唯一性去掉重复值或调整合并关系合并后关键字段大量为空连接键类型不一致或含不同格式查看两表连接键 dtype 和样本统一类型后再合并缺失值填充没生效缺失值可能是空字符串而非 NaN查看字段的值计数统一替换为 np.nan 后再处理时间字段排序异常时间列还是字符串类型查看 dtype用 pd.to_datetime 转换去重后数据大量减少把不该去重的行也去掉了检查 subset 参数和去重逻辑明确重复定义后再操作列名出现 _x、_y 后缀两张表有相同的非连接列查看 columns 列表合并前先 rename 清洗列名5.5 实操心得清洗和合并后一定要校验每次做完清洗和合并我还会额外做一遍数据质量校验而不是直接进分析。比如检查合并结果的表行数和左表是否一致、检查关键列的非空率是否达到预期、抽样打印几行数据看看关联是否合理。这个校验步骤不复杂但能在第一时间发现问题省掉后面返工的时间。还有一个细节值得提一下如果清洗和合并操作特别复杂我会用日志或注释记录每一步的输入输出行数和关键变化。这样中间任何一步出了问题都能快速定位是哪一步导致的而不是从头开始重新排查。这也是从多个数据项目中攒下来的经验分享给大家参考。数据清洗和合并从来都不是一次性的工作。同一份数据不同时间拿到的版本可能都有变化这次清洗没有出现的问题下次可能就会出现。我自己在实战中体会最深的一点就是把每一步操作的原因和结果都理解到位比背一堆 API 写法更重要。希望这篇 Day 49 的笔记能帮你在 Pandas 进阶的路上少踩几个坑把数据收拾得明明白白再去放心做后续的分析。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →