资讯动态

Pandas DataFrame进阶实战:分组聚合、表连接与缺失值处理

发布时间:2026/10/26 5:13:20 来源:尧图企业网站定制
做数据分析这行十来年我见过太多人卡在同一个坎上read_csv会了df.head()会了增删改查也能凑合写可一旦要按条件分组算点东西、把两张表拼起来、或者处理一下缺失值立马就开始翻文档、搜问答、复制粘贴试错。这个坎不是语法问题是用法层次的问题——从“会操作DataFrame”到“会用DataFrame表达分析逻辑”中间隔着一层窗户纸。这篇内容就是来捅破这层窗户纸的围绕DataFrame的进阶用法展开把分组聚合、表连接、缺失值处理、透视变形这几块真正干活时绕不开的东西讲透。适合已经能写基础DataFrame操作、想往实战分析能力上走一步的读者也适合做机器学习前置数据处理时总觉得数据准备环节别扭的人。下面这些内容都是我在实际项目里反复用、反复踩坑之后沉淀下来的不是文档搬运你照着敲一遍基本就能把日常八成的数据整理活儿拿下。1. 进阶用法的整体设计思路与选型考量1.1 为什么基础操作不够用从“改数据”到“算数据”的思维切换基础篇里我们干的事本质上是数据编辑选列、改值、加行、删行。这些操作的对象是单个单元格或单行逻辑是线性的写起来像流水账。但真实分析场景里你面对的往往是“按某个维度把数据切成若干块每块算一个指标再拼回来”这种结构线性写法会立刻崩掉。举个具体例子。假设你手上有一张订单表字段是用户ID、下单日期、订单金额、商品类别。现在要回答“每个类别每个月的平均客单价是多少”。用基础操作怎么做你得先按类别筛一遍再按月份筛一遍算平均存起来循环几十次。这不叫分析这叫体力活。而进阶用法的核心思路是声明式表达你告诉DataFrame“我要按这两列分组对那一列求平均”它自己去组织计算过程。这个思维切换是分水岭跨过去之后你会发现大量重复代码直接消失。我个人的判断标准很简单如果你写的数据处理代码里出现了显式的for循环去遍历行八成是没用对进阶用法。groupby、merge、pivot_table这些工具的设计初衷就是把“分组—计算—合并”这套模式抽象成一行声明。理解这一点比记住任何单个函数的参数都重要。1.2 工具选型背后的逻辑groupby、merge、pivot 各自解决什么问题DataFrame进阶用法里最常被混淆的是三个东西的适用边界groupby、merge、pivot_table。很多人拿到需求就凭感觉挑一个结果写出来的代码又长又难维护。我按实际用途给它们划个界工具解决的问题典型场景输出形态groupby按维度切分后聚合计算各类别销售额、各用户行为次数通常是一行一个分组的汇总表merge两张表按关联键横向拼接订单表关联用户信息表列数增加行数按连接方式变化pivot_table把长表转成行列交叉的宽表月份×类别的销售矩阵行列都是维度值是聚合指标选型的判断依据是你想要的输出形状。想要“每个分组一行”用groupby想要“把另一张表的信息贴过来”用merge想要“行一个维度、列一个维度、中间填数值”用pivot_table。这个判断顺序我建议固定下来先想输出形状再选工具能省掉大量返工。注意这三个工具不是互斥的实际分析里经常是merge完再groupbygroupby完再pivot_table做展示。把它们当成流水线上的不同工位而不是三选一的单选题。1.3 进阶用法的性能意识什么时候该警惕基础操作慢一点无所谓数据量小。但进阶用法往往作用在整表上性能问题会被放大。我踩过最典型的一个坑对一张几百万行的表做groupby分组键是一列高基数的字符串几乎每行都不同结果跑了很久还没出结果。原因不是groupby本身慢而是分组键的选择让分组数量爆炸聚合的计算量跟着爆炸。这里有个经验法则分组键的基数不同值的个数决定了分组数量分组数量乘以聚合函数的复杂度就是总计算量。如果分组键基数接近行数那groupby就退化成了逐行处理还不如直接向量化运算。所以动手前先看一眼分组键的nunique()心里有个数。另外merge的时候如果关联键有大量重复值会产生笛卡尔积式的行数膨胀这个后面会专门讲排查方法。性能意识不是让你提前优化而是让你在数据量上来时知道问题出在哪。2. 分组聚合的核心细节与实操要点2.1 groupby 的“拆分—应用—合并”三段式理解了这个就理解了一切groupby看起来是一个函数实际上背后是三步split按key把数据切成若干块、apply对每块施加计算、combine把结果拼回一个表。这三步里你能控制的是split的key和apply的函数combine是自动的。理解这个三段式很多看似奇怪的行为就解释得通了。比如为什么df.groupby(类别)[金额].mean()返回的是一个以类别为索引的Series因为combine阶段默认把分组键作为索引。为什么有时候要加as_indexFalse因为你想让分组键变回普通列方便后续操作。为什么agg能一次传多个函数因为apply阶段可以对同一块数据施加多个计算。这些都不是孤立的知识点而是三段式在不同参数下的表现。我建议新手在写groupby时养成一个习惯先在脑子里过一遍“我要按什么切、每块算什么、结果要什么形状”这三问对应三段式答清楚了代码基本不会写错。这个习惯比背参数表有用得多。2.2 聚合函数的选择内置、自定义与 agg 的组合拳groupby后面能接的聚合方式有三类用途和写法差别很大内置聚合sum、mean、count、max、min、std、median等直接.mean()这样调用最快最省事。agg传函数名或函数对象agg(mean)或agg(np.mean)适合需要统一管理多个聚合的场景。agg传字典agg({金额:sum, 数量:mean})对不同列施加不同聚合这是实战里最常用的形态。字典形态值得单独说。真实需求经常是“金额求和、数量求平均、订单号计数”三列三种算法。用字典一次表达清楚比写三行再拼起来干净得多。而且字典的key是列名value是聚合方式读起来就是一句自然语言可维护性极高。自定义函数通过agg传入时要注意它接收的是每个分组的Series返回一个标量。如果你写的函数返回的不是标量而是Series结果结构会变得复杂容易出意外。我一般建议自定义聚合函数保持“输入Series、输出单值”这个契约简单可靠。import pandas as pd # 模拟一张订单表 df pd.DataFrame({ 类别: [A, A, B, B, A, B], 月份: [1月, 2月, 1月, 2月, 1月, 2月], 金额: [100, 150, 200, 120, 130, 180], 数量: [2, 3, 4, 2, 2, 3] }) # 字典形态不同列不同聚合 result df.groupby(类别).agg({ 金额: sum, 数量: mean }) print(result)这段代码的输出是以类别为索引、两列分别是金额总和与数量均值的表。注意数量那列是均值不是总和这就是字典形态的价值——一次表达多种聚合意图。2.3 多列分组与结果重塑as_index 和 reset_index 的取舍按多列分组时groupby([类别, 月份])会产生一个多级索引MultiIndex的结果。多级索引在后续操作里经常碍事比如你想按列名取值、想和别的表merge多级索引都会带来额外处理。这时候as_indexFalse或者.reset_index()就派上用场了。两者的区别as_indexFalse是在groupby阶段就告诉它“别把分组键设成索引”一步到位reset_index()是先得到带索引的结果再转回来多一步但更灵活比如你中途想用索引做点事。我个人的习惯是能用as_indexFalse就用代码更短意图更直接。多列分组还有一个容易忽略的点分组键的顺序影响结果的索引层级顺序。groupby([类别,月份])和groupby([月份,类别])算出来的数值一样但索引层级顺序相反后续如果按索引取值顺序错了会取错数据。这个坑我在早期项目里踩过排查了半天才发现是分组键顺序的问题。2.4 transform 与 apply不改变形状的分组计算agg会把每个分组压缩成一个值行数变少。但有些需求要求保持原表行数不变把分组计算的结果广播回每一行这时候用transform。典型场景是“计算每个值占其所在分组总和的比例”——你需要每行的值除以它所在组的总和结果还是每行一个数行数不变。# 每行金额占其类别总金额的比例 df[类别总额] df.groupby(类别)[金额].transform(sum) df[占比] df[金额] / df[类别总额]transform的关键特征是返回的Series和原表行数、索引完全对齐可以直接赋值成新列。这个能力在特征工程里极其常用比如“每个用户的行为次数”“每个类别内的排名”这类特征都是transform的活儿。apply则更通用也更慢它把每个分组作为一个DataFrame传给你的函数你可以返回任意形状。灵活但代价是性能因为它是逐组调用Python函数没法向量化。我的原则是能用agg或transform表达的绝不用apply只有当逻辑复杂到内置方法搞不定时才动用apply并且要接受它的性能代价。实操心得transform后面跟字符串函数名如sum、mean时走的是优化路径比传自定义函数快很多。如果逻辑能用内置函数拼出来优先拼别急着写lambda。3. 表连接与数据合并的完整实操3.1 merge 的四种连接方式用生活场景理解 inner、left、right、outermerge的本质是按关联键把两张表横向拼起来四种连接方式的区别在于保留哪些行。用生活场景类比最直观inner内连接只保留两张表都能匹配上的行。像相亲双方都到场才见面。left左连接保留左表所有行右表匹配不上的填缺失值。像以左表为主名单右表是补充信息补充不到就空着。right右连接反过来保留右表所有行。outer外连接两张表的行全保留匹配不上的互相填缺失值。像合并两个名单谁都不丢。实战里用得最多的是left因为常见模式是“以主表为准把维表信息贴过来”。比如订单表主关联用户信息表维你肯定不想因为某个用户信息缺失就把订单行丢掉所以用left。inner适合“只分析两边都有的数据”outer适合“做全量对账看两边差异”。orders pd.DataFrame({ 订单号: [1, 2, 3, 4], 用户ID: [u1, u2, u1, u3], 金额: [100, 200, 150, 300] }) users pd.DataFrame({ 用户ID: [u1, u2, u4], 城市: [北京, 上海, 广州] }) # 左连接订单全保留用户信息能匹配就贴匹配不上填NaN merged orders.merge(users, on用户ID, howleft) print(merged)这段代码里订单号3的用户u3在用户表里没有左连接后城市列是NaN订单本身没丢。这就是left的价值。3.2 关联键不一致时的处理left_on、right_on 与列名冲突两张表的关联键列名不一样是常态。订单表叫用户ID用户表叫uid直接on用户ID会报错。这时候用left_on和right_on分别指定merged orders.merge(users, left_on用户ID, right_onuid, howleft)这样写有个副作用结果表里会同时保留用户ID和uid两列内容一样但名字不同。如果嫌冗余merge完drop掉一列即可。另一个常见问题是两张表有同名的非关联列比如都叫金额merge后pandas会自动加后缀_x、_y区分。这个后缀可以自定义用suffixes(_订单, _用户)这样更可读。我踩过的一个坑关联键的数据类型不一致。左表用户ID是字符串右表是整数merge时pandas不会报错但匹配结果全是空——因为1和1不相等。这种问题极其隐蔽排查时先看两边的dtype不一致就先统一类型再merge。这个检查我现在已经固化成肌肉记忆了。3.3 merge 后的行数膨胀排查笛卡尔积是怎么来的merge最危险的副作用是行数膨胀。如果关联键在某一侧有重复值匹配时会产生多对多的组合行数按乘积增长。比如左表某个用户有3条订单右表同一用户有2条地址记录merge后这个用户会变成6行。这不是bug是连接语义的必然结果但如果你没预期到后续统计全错。排查方法很直接merge前后各看一次行数len(before)和len(after)。如果after明显大于before就去查关联键的重复情况# 检查关联键是否有重复 print(orders[用户ID].duplicated().sum()) print(users[用户ID].duplicated().sum())如果维表右表的关联键有重复说明它一个键对应多条记录这时候要么先去重drop_duplicates要么明确接受膨胀并调整后续逻辑。我的习惯是merge前对维表做一次关联键唯一性检查不唯一就停下来想清楚别让问题流到下游。注意merge默认的howinner很多人不写how参数以为默认是左连接结果发现行数变少了。养成显式写how的习惯代码意图一目了然。3.4 concat 的纵向拼接行合并与列合并的边界merge是横向按key拼concat是纵向或横向直接堆。纵向拼接axis0是把多张结构相同的表摞起来比如多个月份的销售数据分文件存储读进来后拼成一张大表。横向拼接axis1是按索引对齐后并排用得相对少。纵向拼接的关键是列名对齐。如果两张表列名不完全一致concat会取并集缺失的列填NaN。这个行为有时候是想要的容忍结构差异有时候是灾难列名拼写错误导致大量NaN。我一般会在concat前打印两张表的columns对比一下确认一致再拼。# 纵向拼接多张结构相同的表 df_jan pd.DataFrame({类别: [A], 金额: [100]}) df_feb pd.DataFrame({类别: [B], 金额: [200]}) combined pd.concat([df_jan, df_feb], ignore_indexTrue)ignore_indexTrue的作用是重建连续索引否则拼接后索引会重复两张表各自的0、1、2摞在一起。这个参数在纵向拼接时几乎必加不加的话后续按索引操作会出乱子。4. 缺失值处理与数据变形的高频操作4.1 缺失值的识别与统计isnull 的正确打开方式处理缺失值的第一步不是填是看清楚缺失的分布。isnull()返回一个同形状的布尔表直接看它没意义要看统计# 每列缺失数量 print(df.isnull().sum()) # 每列缺失比例 print(df.isnull().mean().round(4))isnull().mean()这个写法很妙因为布尔值True算1、False算0求平均就是缺失比例。这个技巧比sum()/len()简洁我一直在用。看缺失分布的目的是判断处理策略某列缺失比例极低比如1%直接删行影响不大缺失比例很高比如80%这列可能本身就没啥信息考虑整列删掉缺失比例中等才值得花心思填充。还有一个容易忽略的点缺失值的表现形式不止NaN。有些数据源用空字符串、NULL、-1、9999表示缺失isnull()识别不出来。这类“伪缺失”要先替换成真正的NaN再处理df df.replace([, NULL, -1], pd.NA)这一步在真实数据里非常必要我见过太多因为伪缺失没处理导致统计结果离谱的案例。4.2 填充策略的选择均值、前向填充与业务含义填充缺失值没有万能公式核心原则是填充值要有业务含义。数值列填均值是常见做法但如果数据分布偏斜严重均值会被极端值拉偏这时候中位数更稳。分类列填众数或者单独设一个“未知”类别后者在机器学习里往往更合理因为“缺失”本身可能就是一种信息。时间序列数据常用前向填充ffill用上一个有效值补当前缺失。这个策略的前提是“状态有延续性”比如设备读数、库存数量前一时刻的值对当前时刻有参考意义。但如果是独立事件比如每日新增用户数前向填充就是错的会人为制造出虚假的平稳。# 数值列用中位数填充 df[金额] df[金额].fillna(df[金额].median()) # 分类列填“未知” df[城市] df[城市].fillna(未知) # 时间序列前向填充 df[库存] df[库存].ffill()实操心得填充之前先把填充前的分布和填充后的分布各画一次或者打印分位数对比一下。如果填充把分布形状明显改变了说明策略有问题。这个检查花不了几分钟但能避免很多隐蔽的错误。4.3 pivot_table 做交叉汇总从长表到宽表的思维转换pivot_table是把“长表”转成“宽表”的工具。长表是每行一条记录宽表是行列交叉、中间填聚合值。举个典型场景原始数据是“类别、月份、金额”的长表你想看“行是类别、列是月份、值是金额总和”的矩阵这就是pivot_table的活儿。pivot df.pivot_table( values金额, index类别, columns月份, aggfuncsum, fill_value0 ) print(pivot)四个核心参数values是填进格子的值index是行维度columns是列维度aggfunc是聚合方式。fill_value0处理某些格子没有数据的情况避免出现NaN。这个工具在报表制作、交叉分析里几乎是标配因为它输出的形状直接就是人眼友好的矩阵。pivot_table和groupby的关系值得说清楚pivot_table内部其实就是groupby加一个重塑步骤。你用groupby([类别,月份])[金额].sum()得到的是长表多级索引unstack()之后就是宽表。所以两者不是替代关系是同一件事的两种表达。我一般用pivot_table做最终展示用groupby做中间计算各取所长。4.4 排序与排名sort_values 与 rank 的实战差异排序看着简单但sort_values和rank解决的是不同问题。sort_values改变行的物理顺序用于展示或后续取topNrank不改变顺序而是给每行算一个排名值用于“排名”这个特征本身。# 按金额降序排列 df_sorted df.sort_values(金额, ascendingFalse) # 算每个金额在其类别内的排名 df[排名] df.groupby(类别)[金额].rank(ascendingFalse, methodmin)rank的method参数处理并列值min是并列取最小名次1,1,3dense是密集排名1,1,2average是平均名次1.5,1.5,3。选哪个取决于业务定义比如比赛排名通常用min而做特征时dense更常见。这个细节不注意排名结果会和预期对不上。5. 常见问题与排查技巧实录5.1 SettingWithCopyWarning这个警告到底在说什么这个警告大概是pandas新手最常遇到的也是最容易被忽略的。它的本质是你对一个可能是切片view的DataFrame做赋值pandas不确定你改的是原表还是副本于是警告你。触发场景通常是链式索引比如df[df[金额]100][折扣] 0.9。正确做法是用.loc一步到位# 错误写法触发警告且可能不生效 df[df[金额] 100][折扣] 0.9 # 正确写法 df.loc[df[金额] 100, 折扣] 0.9.loc[行条件, 列名]明确告诉pandas“我要改原表的这些位置”不会有歧义。我现在的习惯是任何条件赋值都用.loc从源头上避免这个警告。如果确实想要副本显式.copy()意图清晰。5.2 groupby 结果不符合预期索引、类型、空值三连查groupby算出来的数不对我一般按三步排查。第一查索引结果是不是多级索引导致你取错了列打印result.index看看结构。第二查类型分组键或聚合列是不是被当成了字符串比如金额列里混了100和100求和结果会变成字符串拼接。用df.dtypes确认。第三查空值groupby默认会丢弃分组键为NaN的行如果你的数据里有空的分组键这些行会静默消失导致总数对不上。加dropnaFalse可以保留它们。# 保留分组键为NaN的行 result df.groupby(类别, dropnaFalse)[金额].sum()这个dropna参数默认是True很多人不知道结果发现“怎么少了一部分数据”。这个坑我踩过不止一次现在写groupby都会下意识想一下数据里有没有空的分组键。5.3 merge 结果行数对不上关联键重复与类型不一致前面提过行数膨胀这里给一个完整的排查流程。merge后行数异常按顺序查关联键类型是否一致dtypes对比、关联键是否有重复duplicated().sum()、是否有前后空格字符串键常见问题u1和u1 匹配不上。字符串键的空格问题特别隐蔽我一般merge前对字符串键统一做一次str.strip()。现象可能原因排查方法解决行数变多关联键重复导致多对多查两侧duplicated().sum()去重或调整连接逻辑行数变少用了inner连接丢了匹配不上的行检查how参数改left或outer匹配全空类型或空格不一致对比dtypes、检查空格统一类型、strip列名带后缀两表有同名非关联列看结果列名用suffixes自定义这张表我建议存下来遇到merge问题对着查比漫无目的试错快得多。5.4 性能突然变慢从分组键基数和 apply 滥用入手数据处理跑得慢先看两个地方。一是分组键基数前面说过基数接近行数时groupby退化。用df[key].nunique()看一眼如果接近len(df)考虑是不是选错了分组维度。二是apply的使用apply逐组调用Python函数没法向量化数据量大时慢得明显。能换成agg或transform就换实在要自定义逻辑看看能不能用numpy的向量化操作重写。还有一个隐蔽的性能杀手在循环里反复对DataFrame做筛选和赋值。这种写法每次操作都可能触发整表复制数据量大时是灾难。正确做法是先构造好条件列用一次.loc批量赋值或者用np.where做向量化条件替换。import numpy as np # 慢循环逐行判断 # 快向量化条件替换 df[等级] np.where(df[金额] 200, 高, 低)np.where这种向量化写法比逐行循环快一到两个数量级是处理条件逻辑的首选。5.5 我个人的几条避坑经验最后分享几条不成文但很实用的经验。第一任何merge、groupby、pivot操作前后都打印一次形状df.shape行数列数的变化是最快的异常信号。第二链式操作别写太长超过三四个方法连在一起就拆开中间结果赋个变量出问题时好定位。第三涉及数值计算前先describe()看一眼最大值最小值有没有离谱的能提前发现伪缺失和异常值。第四养成写注释的习惯尤其是分组键的选择和聚合函数的意图过两周回来看代码没注释的分组逻辑基本要靠猜。这些经验没有一条是文档里会写的都是实际项目里被问题逼出来的。DataFrame进阶用法的门槛不在语法在于对数据形状和业务含义的敏感度多练多查多打印手感自然就上来了。

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

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

免费获取报价 →
↑