Pandas DataFrame进阶:条件筛选、分组聚合与表连接实战

发布时间:2026/10/26 5:13:08
Pandas DataFrame进阶:条件筛选、分组聚合与表连接实战 1. 为什么DataFrame进阶用法值得单独拎出来讲很多人学Pandas的路径都差不多先看几个教程学会了read_csv读数据、df.head()看前几行、df[列名]取一列、df[df[分数]90]做个筛选然后就觉得自己“会用Pandas”了。可真拿到一份稍微像样的数据集——几十个字段、上万行、夹杂着缺失值和重复记录、需要按多个维度分组聚合再和另一张表拼接——立刻就卡住了。这不是基础没打好而是从“能读能筛”到“能洗能算能拼”之间隔着一层窗户纸这层窗户纸就是DataFrame的进阶用法。我带的几个做数据分析的实习生几乎每个人都在这个阶段卡过。基础篇教的是“单个动作”进阶篇要解决的是“组合动作”怎么把多个条件串起来筛选、怎么按组算完指标再拼回原表、怎么处理那些看起来一样但实际有细微差别的重复行、怎么在不动原始数据的前提下派生新字段。这些操作在真实项目里出现的频率远高于单纯的行列选取但很多入门材料一笔带过导致大家到了实战环节只能靠搜索引擎拼凑零散知识。这篇内容面向的是已经能跑通基础DataFrame操作、准备进入真实数据清洗与特征工程阶段的读者。不管你是做机器学习前置的数据准备还是日常业务报表的加工下面这些用法都会反复用到。我会把每个操作背后的“为什么”讲清楚把参数选择的逻辑拆开再配上可以直接抄的代码和踩过的坑。机器学习前置技术栈里DataFrame进阶用法是绕不开的一环——特征工程八成以上的操作都落在这几个API上。2. 条件筛选的进阶从单条件到多条件组合2.1 布尔索引的本质与括号陷阱基础篇里大家学过df[df[年龄] 30]这行代码能跑通但很多人没想过它为什么能跑通。方括号里那个表达式返回的是一个布尔Series长度和行数一致Pandas拿这个布尔序列去对齐行索引为True的行留下为False的行丢掉。理解这一点后面所有复杂筛选都是同一个逻辑的延伸。多条件组合时运算符的选择是第一个坑。Python的and、or、not在Pandas里不能直接用因为它们是标量逻辑运算符而我们要的是逐元素的向量化运算。正确写法是用、|、~并且每个条件必须用括号包起来# 正确写法 result df[(df[年龄] 30) (df[城市] 北京)] # 错误写法一用and result df[df[年龄] 30 and df[城市] 北京] # 报错 # 错误写法二漏括号 result df[df[年龄] 30 df[城市] 北京] # 优先级错乱结果不对漏括号这个问题特别隐蔽因为有时候它不报错只是结果莫名其妙。原因是的优先级高于30 df[城市]会先被计算产生意料之外的结果。我自己的习惯是只要条件超过一个每个条件无条件加括号不给自己留犯错空间。2.2 isin、between与query的适用场景条件多了以后代码可读性会急剧下降。Pandas提供了几个工具来缓解这个问题各有各的适用边界。isin适合“某列的值属于一个集合”的场景。比如筛选出几个指定城市的记录cities [北京, 上海, 广州] result df[df[城市].isin(cities)]比写三个再用|连起来清爽得多。反过来~df[城市].isin(cities)就是“不属于”的意思。between适合连续区间注意它默认是闭区间两端都包含result df[df[年龄].between(25, 35)] # 包含25和35 result df[df[年龄].between(25, 35, inclusiveleft)] # 只含左端query则是把条件写成字符串表达式适合条件特别长、或者列名带空格等特殊字符的情况result df.query(年龄 30 and 城市 北京)query里可以用and、or因为它是Pandas自己解析的表达式不走Python的运算符。但要注意列名如果和Python关键字冲突需要用反引号包起来。我一般只在条件特别复杂、或者需要动态拼接条件字符串时才用query日常筛选还是布尔索引更直观调试也方便。2.3 用loc做条件筛选并同时选列一个常见需求是筛出满足条件的行但只保留其中几列。新手容易写成两步temp df[df[分数] 80] result temp[[姓名, 分数]]这样写没错但多了一步中间变量。更紧凑的写法是用loc行条件在前、列选择在后result df.loc[df[分数] 80, [姓名, 分数]]loc的完整签名是df.loc[行条件, 列条件]两个位置都可以放布尔序列或列名列表。这个写法在链式操作里特别有用因为它一步到位不会产生SettingWithCopyWarning。说到这个警告下一节专门讲。3. 赋值与修改SettingWithCopyWarning的来龙去脉3.1 这个警告到底在警告什么几乎每个Pandas用户都被SettingWithCopyWarning骚扰过。它的核心含义是你可能在一个临时切片上做修改而原始DataFrame不会跟着变导致你以为改了、其实没改。Pandas没法确定你的意图所以用警告提醒你检查。触发场景通常是链式索引赋值# 可能触发警告 df[df[分数] 80][等级] 优秀这行代码先筛出一个子集可能是视图也可能是副本Pandas不保证然后在子集上赋值。如果子集是副本赋值只改了副本原df纹丝不动。正确做法是用loc一步完成df.loc[df[分数] 80, 等级] 优秀3.2 视图与副本的判断逻辑Pandas底层基于NumPy切片操作有时返回视图共享内存、有时返回副本独立内存规则并不总是直观。简单记用[]做列选择通常返回视图用布尔索引筛选通常返回副本但这不是铁律。与其去记规则不如养成习惯——凡是“筛选后赋值”一律用loc凡是需要独立修改的显式加.copy()。subset df[df[分数] 80].copy() subset[等级] 优秀 # 明确知道改的是副本不碰原表.copy()会带来内存开销大数据集上要留意。如果只是读取不修改不需要copy只有确定要改子集且不希望影响原表时才值得付出这份开销。3.3 批量赋值的几种高效写法给整列赋同一个值直接df[新列] 0即可。按条件赋不同值除了loc还可以用np.where或np.selectimport numpy as np # 二选一 df[等级] np.where(df[分数] 60, 及格, 不及格) # 多选一 conditions [df[分数] 90, df[分数] 60, df[分数] 60] choices [优秀, 及格, 不及格] df[等级] np.select(conditions, choices, default未知)np.select按顺序匹配第一个为True的条件生效所以条件顺序要从严格到宽松排。这个细节很多人忽略导致“90分以上”被“60分以上”抢先匹配。数据量大时np.select比逐条件loc赋值快不少因为它是一次向量化计算。4. 分组聚合groupby的进阶玩法4.1 分组-聚合-合并的三段式心智模型groupby是DataFrame进阶里最核心的API没有之一。它的心智模型可以概括为三步split按key拆成若干组→ apply每组算一个聚合结果→ combine把结果拼回一个表。理解这三步各种变体用法都能推导出来。最简单的形式df.groupby(城市)[销售额].sum()这行代码按城市分组对销售额求和返回一个以城市为索引的Series。如果要多列多指标df.groupby(城市).agg({ 销售额: sum, 订单量: count, 客单价: mean })agg接受字典键是列名、值是聚合函数名或函数列表。同一个列要多个指标时值传列表df.groupby(城市).agg({销售额: [sum, mean, max]})这样会产生多级列索引后续取用需要用元组比如result[(销售额, sum)]。嫌麻烦可以在agg后加.droplevel(0, axis1)或者重命名。4.2 自定义聚合函数与命名聚合内置函数不够用时可以传自定义函数def range_func(x): return x.max() - x.min() df.groupby(城市)[销售额].agg(range_func)但自定义函数走的是Python循环大数据集上慢。能用内置向量化函数就别自己写。Pandas 0.25之后引入了命名聚合named aggregation可以一步搞定“聚合重命名”避免多级索引的麻烦df.groupby(城市).agg( 总销售额(销售额, sum), 平均客单价(客单价, mean), 订单数(订单号, count) )关键字就是结果列名值是(原列名, 聚合函数)元组。这个写法我强烈推荐代码自解释结果列名干净省去后续重命名步骤。4.3 transform把组内统计量拼回原表agg返回的是每组一行行数变少了。但很多场景需要“组内统计量作为新列拼回原表”比如算每个城市的平均销售额然后给每条记录标注“该城市平均销售额”。这时候用transformdf[城市平均销售额] df.groupby(城市)[销售额].transform(mean)transform返回的Series和原表行数一致、索引对齐可以直接赋值成新列。这是特征工程里极高频的操作——比如“用户消费金额减去该用户所在城市的平均消费”这类特征一行transform就出来了。transform也支持自定义函数但同样要注意性能。另外transform和agg的区别要记牢agg降维transform保维。选错了要么行数对不上要么赋值报错。4.4 filter按组筛选整组保留或丢弃还有一种需求是“只保留满足条件的组”比如“只保留订单数超过100的城市的所有记录”。这不是筛行是筛组用filterdf.groupby(城市).filter(lambda g: len(g) 100)filter的函数接收每个组的子DataFrame返回True则整组保留。注意它返回的是原表结构所有列都在只是行被过滤了。这个操作在数据清洗阶段很有用比如剔除样本量过少的组避免后续统计不可靠。5. 表连接与重塑merge、concat、pivot5.1 merge的四种连接方式与键的选择单表操作玩熟了真实项目里八成要面对多表关联。merge是主力工具核心参数是how和onhow取值含义结果行数inner交集两边都有的键才保留最少left左表全保留右表匹配不上的填NaN等于左表right右表全保留等于右表outer并集两边都保留最多result pd.merge(orders, users, on用户ID, howleft)键的选择有个坑如果两表键名不同用left_on和right_on分别指定如果键有重复值会产生笛卡尔积式的行数膨胀merge前务必确认键的唯一性。我习惯在merge前先df[键].duplicated().sum()看一眼非零就得想清楚是数据问题还是业务上本就一对多。merge后行数对不上是新手最常见的困惑。排查顺序先看两表键是否有重复再看how选得对不对最后看键的数据类型是否一致比如一边是int一边是str匹配不上但不报错。数据类型不一致这个问题特别隐蔽建议merge前统一用astype转一下。5.2 concat的轴向与索引处理concat用于拼接axis0是纵向堆叠行增加axis1是横向拼接列增加。纵向拼接时索引会重复加ignore_indexTrue重新生成result pd.concat([df1, df2], axis0, ignore_indexTrue)横向拼接按索引对齐索引对不上的位置填NaN。如果两表行数相同但索引不同结果会出乎意料所以横向拼接前最好先reset_index。5.3 pivot_table从长表到宽表的转换做报表或特征矩阵时经常需要把长表转宽表。pivot_table是透视表工具pivot df.pivot_table( index城市, columns月份, values销售额, aggfuncsum, fill_value0 )index是行维度columns是列维度values是填充值aggfunc决定同一单元格有多个值时怎么聚合。fill_value0把空单元格填0避免后续计算出NaN。透视表在特征工程里常用于构造“用户×品类”的消费矩阵转完再拉平就是特征。6. 缺失值与重复值清洗环节的硬功夫6.1 缺失值的识别与统计isnull()返回同形状的布尔表sum()按列统计缺失数df.isnull().sum() df.isnull().mean() # 缺失比例比绝对数更有参考价值缺失比例超过一定阈值比如50%的列要考虑是否直接丢弃。阈值没有统一标准看业务含义——关键字段哪怕缺10%也得想办法补边缘字段缺一半可能直接删。6.2 填充策略的选择逻辑fillna的填充值选择是门学问。均值填充简单但会压缩方差、扭曲分布中位数填充对异常值稳健前向填充ffill适合时间序列分组均值填充结合transform更精细df[销售额] df.groupby(城市)[销售额].transform( lambda x: x.fillna(x.mean()) )这行代码按城市分组用组内均值填充组内缺失。比全局均值填充合理得多因为不同城市的销售额量级可能差很远。填充策略没有标准答案核心原则是填充后的数据分布要尽量接近真实分布且不能引入未来信息时间序列场景尤其注意。6.3 重复值的判定与去重duplicated()标记重复行默认所有列都相同才算重复。可以指定子集df.duplicated(subset[用户ID, 日期]).sum() df.drop_duplicates(subset[用户ID, 日期], keeplast)keep参数决定保留哪一条first保留第一条last保留最后一条False把所有重复的都删掉。业务上“保留最新一条”很常见这时候通常需要先按时间排序再keeplast。去重前一定要想清楚“重复”的业务定义——是整行重复还是某个业务主键重复定义错了去重就是灾难。7. 常见问题与排查速查7.1 高频报错与对应解法现象可能原因解法SettingWithCopyWarning链式索引赋值改用loc一步赋值或显式copymerge后行数暴增键有重复值检查键唯一性确认连接逻辑筛选结果为空但数据明明有数据类型不一致或空格检查dtype字符串stripgroupby结果列名多级agg传了列表用命名聚合或dropleveltransform赋值报索引错索引未对齐reset_index或确认分组键7.2 几个踩过的坑第一个坑字符串列里混了空格 北京匹配不上北京 。清洗时统一str.strip()或者筛选前先看一眼唯一值。第二个坑数值列被读成了object类型因为混了非数字字符。pd.to_numeric(errorscoerce)可以强制转换转不了的变NaN再决定怎么处理。第三个坑groupby默认dropnaTrue分组键为NaN的组会被静默丢弃。如果NaN本身是一个有意义的组要显式dropnaFalse。第四个坑链式操作里inplaceTrue和赋值混用导致后续拿不到返回值。我的建议是尽量不用inplace统一用赋值代码更可预测。7.3 性能上的实操心得数据量上了百万行几个习惯能省不少时间筛选时先选列再筛行减少中间数据量groupby前把分组键设为索引或转成category类型避免在循环里逐行操作能用向量化就用向量化apply能不用就不用它比内置聚合慢一个量级。这些不是玄学都是实测出来的差距数据越大越明显。8. 把这些用法串起来一个完整的特征加工流程光看单个API容易忘串成一个流程才记得住。假设有一份订单表和一份用户表目标是构造“用户级特征表”喂给模型。流程大致是读入两表→清洗缺失和重复→订单表按用户分组聚合出消费统计→用transform派生“单笔金额占用户均值比例”→merge用户表补上画像字段→pivot出品类偏好矩阵→拉平→最终得到一行一用户的特征表。这个流程里第2节的条件筛选用于剔除异常订单第3节的loc赋值用于打标签第4节的agg和transform用于构造统计特征第5节的merge和pivot用于整合多源信息第6节的清洗贯穿始终。每个环节单独看都不复杂组合起来就是机器学习前置技术栈里最实在的那部分工作。我个人的体会是这些API的语法查文档十分钟能记住但“什么时候该用哪个、参数怎么选、结果不对怎么排查”这套判断力只能靠反复在真实数据上练。练的时候刻意记录每次踩坑的原因比刷十篇教程管用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询