资讯动态

Pandas进阶实战:分组聚合、多表合并与数据清洗一次讲透

发布时间:2026/10/16 12:33:06 来源:尧图企业网站定制
做数据分析的朋友应该都有这种感觉DataFrame的基础操作切片、筛选、排序、列运算用顺手之后真正让工作效率上一个台阶的反而是那些“一学就会、一用就多”的组合型方法。上一篇我把创建、索引、筛选这类基本功梳理了一遍这一篇继续往下走聚焦分组聚合、多表合并、缺失值清洗和宽长表转换这四个方向这几个方向覆盖了日常数据处理的绝大部分场景。内容会比较密集但每段都配有可以直接跑通的代码示例适合已经会基础的DataFrame操作、想进阶到“能独立处理一份脏数据”的朋友。1. 分组聚合groupby、agg与apply的各自边界分组聚合是DataFrame最常用的操作之一几乎任何带维度的汇总需求都得用到它。很多人从Excel转过来之后觉得groupby难其实不是难是一上来就被GroupBy对象搞晕了——明明打印出来能看但直接取列、直接赋值都不行。这一节把groupby、agg、apply三件套拆开讲清楚顺便把哪些场景该用谁、哪些场景别用谁说明白。1.1 groupby的基本形态先分组再聚合先看一段最普通的代码import pandas as pd df pd.DataFrame({ 区域: [华东, 华南, 华东, 华北, 华南, 华东], 销售: [1200, 800, 1500, 900, 1100, 1300], 成本: [900, 600, 1100, 700, 850, 950], }) result df.groupby(区域)[销售].sum() print(result)输出结果是一个Series索引是“区域”值是各区域销售合计。这个写法里有两个关键点第一df.groupby(区域)返回的是一个DataFrameGroupBy对象它本身不计算结果只是记录了“按哪一列分组”这个规则。你可以把它理解成把一摞卡片按颜色分成几叠但还没数每叠有多少张。只有后面接了聚合方法.sum()、.mean()、.count()结果才真正出来。第二[销售]放在.groupby()之后、聚合函数之前表示“我只关心销售这一列”。如果不加这一层.sum()会对所有数值列都做求和结果是一个DataFrame。这个差异在实际工作中经常影响下一步代码怎么写。如果你想让聚合结果重新变成一份规整的DataFrame而不是带索引的Series记得加reset_index()result df.groupby(区域)[销售].sum().reset_index()这步看起来没啥技术含量但很多人处理多级索引时被搞到头大根因就是忘了reset。多个分组键、多列数值的写法也很直观result df.groupby([区域, 月份])[[销售, 成本]].sum()这里得到的DataFrame包含两层索引第一层是区域第二层是月份。继续reset_index()就能得到完整的表格。我的习惯是只要后续还要跟其他表merge、或者要写入数据库就尽早reset减少索引带来的麻烦。1.2 agg一次汇总多个统计量单看sum()和mean()确实够用可现实中经常遇到“既要总和、又要均值、还要订单数”的报表需求。一个个写过去也不是不行但代码会变得又长又碎。这时候agg()就派上用场了。result df.groupby(区域).agg( 销售合计(销售, sum), 销售均值(销售, mean), 成本合计(成本, sum), )这种写法叫命名聚合等号左边是你要的列名右边是“源列名聚合函数”的元组。跟老式写法比起来最大的优势是一眼能看出新列的含义不用再回头猜(销售, sum)到底叫什么。如果你的需求更灵活还可以在agg()里传列表result df.groupby(区域)[[销售, 成本]].agg([sum, mean])这样返回的列会是多层索引比如(销售, sum)、(销售, mean)。我自己的经验是这种多层索引好看但不好用尤其是后面还要画图、筛选、rename的时候层级处理费神。所以更推荐命名聚合或字典写法保持列名的扁平结构。agg()其实是个很朴素的“分发器”它把不同列交给不同的聚合函数处理。凡是能在groupby之后做的批量计算都建议优先用agg()收敛成一条语句别在循环里逐个做。1.3 apply的适用场景和性能边界apply()大概是groupby系列里被误解最深的一个方法。很多人以为它就是“更高级的agg”什么都能干结果一跑就把性能拖垮了。实际上它的定位是当内置聚合函数和agg都表达不了你的逻辑时才轮到apply出场。一个典型场景是“分组之后每组的输出不只是一个标量而是一个Series或DataFrame”比如按区域分组后每组的销售值减去该组的均值df[销售_去均值] df.groupby(区域)[销售].transform(lambda x: x - x.mean())这个例子用transform()比apply()更合适因为transform要求输出形状与输入一致刚好符合“逐行填充”的需求。那什么时候真的要用apply()比如你要按组拟合一个简单线性回归、按组生成一个字符串摘要、或者对每组的多个列做一套复杂运算这时候apply()可以把整组数据作为一个块传进来def 区域摘要(group): return f销售额{group[销售].sum()}订单数{len(group)} df.groupby(区域).apply(区域摘要)apply很灵活但每次调用Python函数都有开销数据量一大性能会明显下降。我给团队的建议是能向量化就向量化能用内置聚合用内置聚合apply是最后手段。处理百万行以上数据时这个选择差别可能从几秒钟变成几分钟。1.4 分组聚合中我踩过的三个坑第一个坑分组键里有NaN。默认情况下NaN会被当作一个独立组参与聚合这会导致结果里出现一行不知道哪来的数据。写出来的报表一旦没检查对接的人就会问“这个空值组是什么鬼”。如果你明确知道NaN不参与统计可以用dropnaFalse或dropnaTrue控制也可以提前fillna(未分配)。第二个坑聚合后没有重置索引直接去透视或merge结果行列错乱。这个问题新手特别容易碰到现象是数据看起来没问题但索引是重复的一merge就出现笛卡尔积式的翻倍。第三个坑用apply后返回的列名和预期对不上。apply的结果经常保留原索引或原列结构如果处理不当后续代码里要么rename要么reset_index总是要多出几步。我的排查方法很简单每做完一步就打印.shape和.head()看看别等全套跑完再回头查。2. 多表合并merge、join、concat的关键差异与选型逻辑现实中数据很少整整齐齐躺在一张表里多数情况是明细表、维度表、月度报表各存各的处理的第一步就是把这些表拼起来。DataFrame里负责拼接的方法主要有三个merge()、join()、concat()。它们表面功能有重叠但设计目的完全不同用错了轻则报错重则数据翻倍还不自知。2.1 merge是SQL式关联的移植merge()的逻辑跟SQL里的JOIN基本一致核心参数是on、how、left_on/right_on。它干的事情是根据一个或多个键把左边表left和右边表right的行对应起来按需合并列。看一个最常见的场景业务明细表和产品维表按产品编号关联orders pd.DataFrame({ 订单号: [A001, A002, A003], 产品编号: [P01, P02, P01], 金额: [100, 200, 150], }) products pd.DataFrame({ 产品编号: [P01, P02, P03], 产品分类: [数码, 家居, 服饰], }) merged orders.merge(products, on产品编号, howleft)把how设为left表示以左边的订单表为主尽量保留全部订单行产品维表有匹配就带过来没有匹配就置空。这应该是业务分析里最常用的关联方式。为什么不是默认的inner因为inner只保留两边都匹配上的行一旦维表缺数据订单明细就会悄悄减少。很多人查“数据为什么变少”时第一反应都是分组或过滤写错了很少有人往merge的how上想。如果两个表用来关联的列名不一样就要用left_on和right_on分别指定merged orders.merge(products, left_on产品编号, right_on商品编码, howleft)合并完成后如果两边有重复列名DataFrame会自动加后缀。默认是_x和_y看起来丑且容易混淆我建议主动用suffixes参数手动命名merged orders.merge(products, on产品编号, howleft, suffixes(_订单, _产品))2.2 join基于索引的关联省一步set_indexjoin()和merge()最大的区别在于关联键的位置——join默认以索引为关联键不要求你先把列设为索引。举个例子region_df df.groupby(区域)[[销售]].sum() target_df pd.DataFrame({ 目标: [3000, 2800], 负责人: [张三, 李四], }, index[华东, 华南]) combined region_df.join(target_df, howleft)适合用join的场景通常是两个表都已经按同一套索引整理好了比如按年月索引的月度汇总表、按部门索引的人事表。直接join最省事不用额外set_index再merge。但有个细节要留意join本质上是merge的索引版匹配时同样受how影响也会产生一对多的行膨胀。如果两边索引有重复照样会出现笛卡尔积。所以我在用join之前都会先检查一下索引的is_unique避免数据悄悄翻倍。2.3 concat顺着方向拼接才是它的主业concat()在处理“多个结构相同的表堆在一起”时最顺手。比如三个月各有一份销售明细列结构完全一样你要做的就是把它们纵向叠起来df_q1 pd.DataFrame({区域: [华东], 销售: [1000]}) df_q2 pd.DataFrame({区域: [华南], 销售: [800]}) all_df pd.concat([df_q1, df_q2], ignore_indexTrue)ignore_indexTrue的意思是不要保留原始每个表的索引生成一套全新的连续索引。这是纵向拼接最常见的诉求不加的话索引会重复后续用起来很难受。concat也可以横向拼接只要指定axis1pd.concat([df_q1.reset_index(dropTrue), df_q2.reset_index(dropTrue)], axis1)但这种横向拼接要求两侧的行位置一一对应否则数据就对错了行。它做的是“并排摆放”不是“按逻辑匹配”这一点和merge有本质区别。实战中我更建议横向要按某种键对齐时用merge或join只有纯粹按位置并排时才用concat(axis1)。2.4 合并前的数据体检避免行数膨胀和静默丢失合并操作最怕的不是报错而是不报错却得到错误结果。我处理这类问题有个固定流程第一步检查关联键是否唯一。用df[键].duplicated().sum()看一眼如果有重复合并时就可能出现一对多。第二步记录合并前后的行数。合并后行数如果比左边表多多半是右表键重复行数如果变少多半是how选成了inner。第三步抽样比对。取几行合并前和合并后的结果验证一下关联关系是否符合预期。我之前帮一位朋友排查过这样的问题他拿订单表去merge产品表结果订单行数几乎翻倍查了半天才发现产品表里有十几条产品编码完全重复的记录。这类问题根本防不胜防只能靠合并前查重复来规避。3. 缺失值与重复值清洗流程中的判定与顺序数据处理里最花时间的不是建模不是写SQL而是清洗。DataFrame的isna、fillna、dropna、duplicated、replace这套方法就是要解决“脏数据”的问题。但方法本身不难难的是什么时候该填、什么时候该删、什么时候该替换。3.1 先摸清缺失情况再动手任何清洗动作之前第一步永远是统计缺失情况。你至少要知道三件事哪些列有缺失、每列缺多少、缺失值到底是什么含义。print(df.isna().sum()) print(df.isna().mean())isna().sum()得到每列缺失个数isna().mean()得到每列缺失比例。比例低于1%的列直接删除缺失行通常无伤大雅比例超过50%的列你就要慎重考虑——删除整列可能损失大量信息填充则可能引入较大偏差。还有一种情况经常被忽略缺失值并不都是NaN。很多系统导出数据时会用NULL、-、未知、0来填充空值。如果直接用isna()判断这些值根本不会被视为缺失。所以清洗之前应该先看一眼每一列的取值分布确认有没有这类“伪装值”。有的话用replace()先转成NaN再做后续处理df df.replace({NULL: pd.NA, -: pd.NA, 未知: pd.NA})3.2 dropna和fillna删和填的取舍逻辑先说dropna()。它最简单的用法是删掉有任意缺失值的行df_clean df.dropna()但在真实场景里整行所有字段都齐全的数据往往很少无脑dropna()会把样本删到惨不忍睹。此时thresh参数很有用它表示一行里至少要有多少个非空值才保留df_clean df.dropna(thresh5)意思是一行只要少于5个有效值就删除掉。这样能保留更多信息量足够的行而不是因为某一个次要字段为空就整行丢掉。再说fillna()。它最大的价值是让填充方式贴合业务含义。如果你的数据是时间序列比如某天的销售额缺失用前向填充可能更合理df[销售额] df[销售额].fillna(methodffill)如果是数值型字段缺失且分布呈现明显的中位数特征很多人喜欢填均值或中位数。这里我多说一句填中位数比填均值更稳健因为有极端值的时候均值会被拉偏。另一个容易被忽略的点是fillna不只是填特定值还可以按列填不同的值df df.fillna({ 销售额: 0, 区域: 未分配, 负责人: 待补充, })清洗逻辑和业务含义是对应的——“没有销售额”可以理解成0“没有区域”可能代表数据没录全填“未分配”比填0更有业务说服力。3.3 duplicated和drop_duplicates重复也要分情况处理重复值问题比缺失值更隐蔽因为它的判定完全依赖于“哪几列重复算重复”这个业务定义。duplicated()返回一个布尔序列标记每一行是否重复drop_duplicates()则直接删除重复行。关键参数有三个subset、keep、inplace。subset指定按哪些列判断重复默认是全列一致才判重。比如订单表里同一用户同一时间下的单主键通常是订单号但按“用户ID 下单日期”判断一天内一人的多次下单就算重复。keep控制保留哪一条first保留第一次出现的行last保留最后一次出现的行False则删除所有重复出现的行。实际排查时我常用duplicated().sum()统计有多少重复行再用布尔序列筛出来看看重复的具体形态dup_mask df.duplicated(subset[用户ID, 下单日期], keepFalse) df[dup_mask].sort_values([用户ID, 下单日期]).head(20)注意这里keepFalse的作用是“把所有重复行都标True”包括第一行。这样你能看到重复组的全貌不至于只看到一条而猜不出规律。还有一个决策点是判断重复时选错列会把真正需要保留的数据误删。我见过一个挺典型的例子某人按“姓名城市”判断重复结果两位同名的员工被删掉了一条记录。所以重复值的业务定义一定要先确认好。3.4 清洗流程的组合顺序清洗方法之间是有顺序讲究的。我的习惯是先处理重复值再处理缺失值最后再转类型、建新列。为什么不反过来因为重复值会影响缺失值统计的准确性。假如1000行数据里藏着200行重复先统计缺失比例很可能高估缺失严重程度后面做出的删除或填充决策也跟着偏向。一个相对完整的清洗链路大概是# 1. 先看整体形状和字段类型 print(df.info()) print(df.shape) # 2. 替换伪装空值 df df.replace({NULL: pd.NA, -: pd.NA, 未知: pd.NA}) # 3. 去重 df df.drop_duplicates(subset[订单号], keepfirst) # 4. 按列填充缺失 df[销售额] df[销售额].fillna(df[销售额].median()) df[区域] df[区域].fillna(未分配) # 5. 最后检查一遍 print(df.isna().sum()) print(df.shape)这套流程不是万能的但跑完基本能做到“数据不脏、边界可控”。尤其最后一步的print检查可以说是整个清洗过程中最容易被忽略也最救命的一步。4. 宽表与长表的切换pivot_table、melt、stack的实战场景第一次接触“宽表”、“长表”这两个词的人多半是在做透视、画图或建模型的时候。简单说宽表是每一行包含多个时间点或类别的多个列比如“区域”一行后面跟1月、2月、3月三列长表则是一行对应一个“区域-月份-数值”的三元组。Excel里的透视表用的就是宽表逻辑统计建模大多要长表。DataFrame里负责这两者转换的是pivot_table和melt以及不那么常用但关键时刻很顶用的stack/unstack。4.1 pivot_table宽表的制造机器pivot_table可以把“一列多值”的数据摊开变成“多列各值”的宽表。最经典的场景是把某个月的销售明细变成“区域×月份”的矩形表sales pd.DataFrame({ 区域: [华东, 华东, 华南, 华南], 月份: [1月, 2月, 1月, 2月], 销售额: [1200, 1300, 800, 1100], }) pivot sales.pivot_table( index区域, columns月份, values销售额, aggfuncsum, )这里index决定了行维度columns决定了列维度values是要聚合的数值列aggfunc是聚合函数。加一个marginsTrue还能顺便得到“总计”行和列pivot sales.pivot_table( index区域, columns月份, values销售额, aggfuncsum, marginsTrue, )很多人都分不清pivot()和pivot_table()的区别。一句话就能说清pivot()是格式转换不涉及聚合如果同一个“区域-月份”组合出现两条记录它会直接报错pivot_table()是“先分组再摊开”自带聚合逻辑同一个组合出现多次会被aggfunc合并掉。所以日常业务里你几乎永远该用pivot_table而不是pivot。4.2 melt把宽表变成长表越早掌握越好melt的作用刚好和pivot_table相反它把“一行的多列值”拆成“多行的一列值”。还是上面那份宽表现在想转回长表用于画图或者建模long_df pivot.reset_index().melt( id_vars区域, value_vars[1月, 2月], var_name月份, value_name销售额, )参数含义很直接id_vars是保留不变的标识列value_vars是要由宽变长的那些列var_name和value_name分别是新生成列的列名。为什么要转成长表因为大多数可视化库和统计分析函数都期望“一行一个观测值”的结构。宽表人看着舒服但机器处理起来不如长表方便。分组聚合、多表合并、透视这些操作在长表结构下也更加顺手。我在实际项目中用melt最多的场景是多个来源的数据导出为宽表格式我需要先统一转成长表才能和其他明细表合并、过滤、透视。如果你经常处理报表数据建议把这套“宽转长、长转宽”的互换练熟后面会非常省事。4.3 stack和unstack在索引层面做旋转stack/unstack和pivot_table/melt作用类似但操作对象不同——它们操作的是索引层级。例如上面生成的透视表如果用stack()列索引会被折叠成行索引的一部分宽表自动变成长表long_from_stack pivot.stack().reset_index().rename(columns{0: 销售额})unstack()则是反方向把部分行索引展开成列索引。它们的优点是“不用指定列名”直接靠索引结构旋转缺点是输出结果往往带着多层索引若不及时reset_index()和重命名可读性会比较差。实际工作中stack/unstack用得不如前两个频繁但在处理多层索引的复杂表格时它们往往能给出非常简洁的转换效果。我的看法是不必背用法但要知道有这么两个工具存在遇到“莫名其妙多了一层索引”时能想起来用它们解围。4.4 透视和逆透视时常见的报错与对策我遇到的用户提问里出现频率最高的是下面这条报错ValueError: Index contains duplicate entries, cannot reshape这句话翻译过来就是透视时出现了同一个“行标签列标签”对应多行的局面而你又用了不支持聚合的pivot()。解决办法很简单换成pivot_table(aggfuncsum)或者在透视前先对原数据groupby聚合一次。还有一个小坑pivot_table默认会把列名和索引名带进结果reset_index()之后会出现类似(区域, )的多层列。应对方法是用.columns重新赋值为扁平列表pivot.columns [col for col in pivot.columns]或者直接用.rename_axis清理轴名称。这类问题不是技术难而是细节繁琐容易让人卡住。5. 一条完整的处理链路从原始明细到可汇报数据方法讲了一堆最后用一个带一点真实复杂度的例子把它们串起来。假设现在需要生成一份“区域×月份×品类”的销售汇总表原始数据分别来自三张表订单明细表、产品维表、区域目标表。订单表里存在重复订单和缺失金额产品表里有少量品类缺失。第一步读取并预览三张表。这一步看的是整体结构和缺失情况确认处理方向。orders pd.read_csv(orders.csv) products pd.read_csv(products.csv) targets pd.read_csv(targets.csv) print(orders.shape, products.shape, targets.shape) print(orders.head()) print(orders.isna().sum())第二步合并订单表和产品表。以订单表为主表用left方式关联产品信息如果产品表关联键有重复先检查再合并products products.drop_duplicates(subset产品编号) df orders.merge(products, on产品编号, howleft)第三步清洗订单明细。默认把销量为空的记录删除或填0把品类缺失的记录填“待分类”同时对“订单号”去重df[销售额] df[销售额].fillna(0) df[品类] df[品类].fillna(待分类) df df.drop_duplicates(subset[订单号], keepfirst)第四步生成月份字段。如果订单时间是字符串先转成datetime再取出月份df[订单日期] pd.to_datetime(df[订单日期]) df[月份] df[订单日期].dt.month第五步做汇总透视。先按区域、月份、品类做一次长表聚合再把月份转成列方向形成宽表summary df.groupby([区域, 月份, 品类])[销售额].sum().reset_index() pivot_summary summary.pivot_table( index[区域, 品类], columns月份, values销售额, aggfuncsum, fill_value0, ).reset_index()跑完这条链路后你会得到一份可以直接交给业务方或写进报表的干净数据。整个处理流程里最重要的工作习惯只有一个每完成一步就用print(shape)和head()确认一遍再进下一步。别等所有步骤跑完才检查那时候排查成本已经高出太多。数据清洗和整理就是这样方法本身学起来很快真正拉开效率差距的是每一步是否想清楚“这个操作会带来什么后果、有什么前置条件需要确认”。把常用方法背后的选择逻辑弄明白了后面遇到新数据集时你自然会知道该用什么、不该用什么。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑