资讯动态

Pandas数值运算实战:从基础到分组窗口计算

发布时间:2026/10/15 5:55:27 来源:尧图企业网站定制
1. 为什么数值运算值得单独拎出来讲很多人学 Pandas 的时候路径都差不多先学怎么读 CSV再学怎么选列选行然后就开始跑业务逻辑了。结果一到需要做批量加减乘除、条件赋值、分组统计的时候代码就开始变得又臭又长。我见过不少人的 DataFrame 里跑出来一堆 object 类型的列做加法直接字符串拼接做除法报 TypeError然后就开始怀疑人生。Pandas 的数值运算方法说白了就是让你在 DataFrame 和 Series 上做数学操作时不用写循环、不用逐行 apply、不用把数据倒腾到 NumPy 再倒回来。它解决的核心问题是让列级别的批量计算变得像 Excel 一样直观但比 Excel 快几个数量级而且可复现、可版本管理。这篇文章适合谁看如果你已经会用 Pandas 读数据、选数据但每次做计算都要查文档、每次遇到 NaN 就头疼、每次看到SettingWithCopyWarning就烦躁那这篇就是写给你的。我会把数值运算的常见方法、参数背后的逻辑、实际踩过的坑以及一些不太出现在官方文档里的经验全部摊开来讲。2. 数值运算的基础骨架与核心方法2.1 算术运算加减乘除不只是符号那么简单Pandas 的算术运算分两个层面Series 与标量、DataFrame 与标量、Series 与 Series、DataFrame 与 DataFrame。最常用的就是前两种因为实际业务里大量场景是“给某一列统一加个值”“把某列单位从元转成万元”。直接上代码import pandas as pd import numpy as np df pd.DataFrame({ 单价: [10.5, 20.0, 15.8, 8.9], 数量: [100, 200, 150, 300], 折扣: [0.9, 0.85, 0.95, 1.0] }) # 标量运算 df[总价] df[单价] * df[数量] * df[折扣]这段代码看起来简单但有几个细节值得说。第一df[单价] * df[数量]是 Series 对 Series 的运算Pandas 会自动按索引对齐。如果两个 Series 的索引不一致结果里会出现 NaN。这是很多人第一次踩的坑从两个不同来源取的列索引一个是 0-99一个是 1-100乘出来全是 NaN还找不到原因。第二标量运算不会改变原 DataFrame除非你赋值回去。df[单价] 1只是返回一个新的 Series原数据不动。这个设计是好的避免了意外修改但新手经常忘了赋值然后纳闷为什么数据没变。第三除法要特别注意除零。Pandas 里整数除以零不会报错会返回inf或-inf而0/0返回NaN。如果你后续要做聚合inf会污染结果。我的习惯是在除法之后加一句df.replace([np.inf, -np.inf], np.nan, inplaceTrue)把无穷大统一转成 NaN后续处理更干净。2.2 比较运算与布尔索引筛选的本质是数值比较比较运算返回的是布尔 Series这是布尔索引的基础。但很多人不知道的是Pandas 的比较运算在遇到 NaN 时结果永远是 False。这意味着df[df[列] 0]会自动排除 NaN 行这通常是想要的行为但如果你没意识到就会觉得“怎么少了几行”。# 多条件筛选注意括号不能省 result df[(df[单价] 10) (df[数量] 150)]这里和and的区别是高频错误点。and是 Python 关键字只能用于标量布尔值用在 Series 上会报ValueError。必须用、|、~而且每个条件都要用括号包起来因为位运算符的优先级高于比较运算符。还有一个实用技巧df[列].between(a, b)等价于(df[列] a) (df[列] b)但写起来更短可读性更好。isin([...])用于多值匹配比写一堆|干净得多。2.3 统计运算describe 只是起点df.describe()是很多人第一个学会的统计方法但它默认只统计数值列而且只给 count、mean、std、min、25%、50%、75%、max。实际业务里我们经常需要偏度、峰度、中位数绝对偏差、分位数自定义。# 自定义分位数 df[单价].quantile([0.1, 0.5, 0.9]) # 偏度和峰度 df[单价].skew() df[单价].kurt() # 累计统计 df[累计销量] df[数量].cumsum() df[累计最大值] df[数量].cummax()cumsum、cummax、cummin、cumprod这几个累计函数在时间序列场景里非常有用。比如算累计收益、累计最高点回撤都是一行搞定。但要注意cumsum默认不跳过 NaN遇到 NaN 之后的结果全是 NaN。如果数据有缺失先fillna(0)或者用skipna参数部分函数支持。idxmin和idxmax返回的是索引而不是值这个在找“哪个产品销量最高”时特别方便。但如果有多个相同最大值只返回第一个。想要所有最大值的位置得用df[df[列] df[列].max()].index。3. 对齐机制与缺失值处理数值运算的暗礁区3.1 索引对齐Pandas 最强大也最容易被忽视的设计Pandas 的运算默认按索引对齐这是它和 NumPy 最本质的区别。NumPy 数组相加是位置对应Pandas 是标签对应。这个设计在数据合并时非常有用但在简单运算时经常带来意外。s1 pd.Series([1, 2, 3], index[a, b, c]) s2 pd.Series([10, 20, 30], index[b, c, d]) s1 s2 # a NaN # b 12.0 # c 23.0 # d NaN结果里 a 和 d 都是 NaN因为对方没有对应的索引。如果你想要的是位置相加应该用.values转成 NumPy 数组再运算或者用add方法并指定fill_value。s1.add(s2, fill_value0) # a 1.0 # b 12.0 # c 23.0 # d 30.0add、sub、mul、div、floordiv、mod、pow这些方法都支持fill_value参数这是处理索引不完全重叠时的标准做法。我个人的经验是只要两个 Series 的索引不是明确一致的就用带fill_value的方法不要用运算符。这样代码意图更清晰也避免了后续调试时才发现 NaN 的尴尬。3.2 NaN 的传播与跳过策略NaN 在数值运算里会传播任何涉及 NaN 的算术运算结果都是 NaN。这是 IEEE 754 标准的行为Pandas 遵循这个标准。但统计函数默认跳过 NaN这是 Pandas 自己的设计选择。s pd.Series([1, 2, np.nan, 4]) s.sum() # 7.0跳过 NaN s.mean() # 2.333...跳过 NaN s.sum(skipnaFalse) # NaN不跳过这个差异导致一个常见困惑为什么s 1里 NaN 还是 NaN但s.sum()就把 NaN 忽略了因为算术运算是逐元素的统计函数是聚合的。理解这一点很多“奇怪”的行为就说得通了。填充 NaN 的常用方法方法适用场景注意点fillna(0)计数、求和场景会改变均值fillna(methodffill)时间序列需要数据按时间排序fillna(df.mean())近似正态分布会缩小方差interpolate()连续变化的数据默认线性插值注意fillna默认返回新对象不修改原数据。如果确定要原地修改加inplaceTrue但我不推荐这么做因为链式调用时容易出问题。3.3 类型转换object 转数值的坑从 CSV 读进来的数据如果列里有任何非数字字符整列就是 object 类型。这时候做数值运算轻则结果错误重则直接报错。df[价格] pd.to_numeric(df[价格], errorscoerce)errorscoerce会把无法转换的值变成 NaN这是最安全的做法。errorsignore会保持原样但那样你还是在 object 类型上运算问题没解决。errorsraise直接报错适合数据质量有保证的场景。转换之后建议检查一下有多少 NaNprint(df[价格].isna().sum())如果 NaN 比例很高说明数据源有问题需要回去查原始数据而不是硬着头皮往下算。4. 分组运算与窗口计算从聚合到变换4.1 groupby 后的数值运算agg、transform、apply 怎么选groupby之后的数值运算是 Pandas 最核心的功能之一但三个方法的区别经常让人困惑。agg每组返回一个标量结果行数等于组数transform每组返回与组内行数相同的序列结果行数等于原数据行数apply最灵活但最慢能返回任意形状df pd.DataFrame({ 部门: [A, A, B, B, B], 销售额: [100, 200, 150, 300, 250] }) # agg每个部门一个总和 df.groupby(部门)[销售额].agg(sum) # transform每行对应自己部门的均值 df[部门均值] df.groupby(部门)[销售额].transform(mean) # apply自定义函数 df.groupby(部门)[销售额].apply(lambda x: x.max() - x.min())实际业务里transform被低估了。比如算“每行占本部门的比例”用transform一行搞定df[占比] df[销售额] / df.groupby(部门)[销售额].transform(sum)如果用agg再 merge 回去代码量翻倍还容易出错。我的原则是能 transform 就不 merge能 agg 就不 apply。4.2 窗口计算rolling 和 expanding时间序列场景里移动平均、移动求和是刚需。rolling提供固定窗口expanding提供从开头到当前的累计窗口。s pd.Series([1, 2, 3, 4, 5]) s.rolling(window3).mean() # 0 NaN # 1 NaN # 2 2.0 # 3 3.0 # 4 4.0 s.expanding().mean() # 0 1.0 # 1 1.5 # 2 2.0 # 3 2.5 # 4 3.0rolling的前window-1个值是 NaN因为窗口不满。如果不想看到 NaN可以加min_periods1这样窗口内至少有一个值就计算。s.rolling(window3, min_periods1).mean() # 0 1.0 # 1 1.5 # 2 2.0 # 3 3.0 # 4 4.0这个参数在实际做报表时很有用因为业务方通常不想看到开头的空白。实操心得rolling的window参数可以是整数也可以是时间偏移字符串如3D、7D。用时间偏移时索引必须是 DatetimeIndex而且窗口是按时间长度算的不是按行数。这个在数据时间间隔不均匀时特别重要。4.3 分位数与排名rank 的多种策略rank方法在算排名时非常方便但method参数决定了相同值怎么处理。method相同值的排名示例值10, 20, 20, 30average平均排名1, 2.5, 2.5, 4min最小排名1, 2, 2, 4max最大排名1, 3, 3, 4first按出现顺序1, 2, 3, 4dense密集排名1, 2, 2, 3默认是average但业务上经常需要min或dense。比如算销售排名相同销售额通常给相同名次下一个名次跳号那就用min如果不跳号用dense。df[排名] df[销售额].rank(methodmin, ascendingFalse)ascendingFalse表示大的排前面这个在算“前 N 名”时很常用。5. 常见问题与排查技巧实录5.1 数值运算报错速查表报错信息常见原因解决方法TypeError: unsupported operand type(s)列是 object 类型pd.to_numeric(errorscoerce)ValueError: The truth value of a Series is ambiguous用了and/or改用/SettingWithCopyWarning链式索引后赋值用.loc或先.copy()结果全是 NaN索引不对齐检查索引用add(fill_value0)结果出现 inf除零replace([np.inf, -np.inf], np.nan)KeyError列名拼写错误或空格df.columns.tolist()检查5.2 性能优化的几个实操经验第一避免逐行 apply。df.apply(lambda row: ..., axis1)在数据量超过几万行时慢得离谱。能用向量化运算就用向量化能用transform就不用apply。第二慎用inplaceTrue。虽然看起来省内存但实际上 Pandas 很多时候还是创建了新对象而且inplace会破坏链式调用让代码更难读。我现在的习惯是全部用赋值不写inplace。第三大文件读取时指定 dtype。如果知道某列是整数读的时候直接dtype{列名: int32}比默认的int64省一半内存。浮点数用float32也是同理但要注意精度损失。第四用eval和query加速复杂表达式。Pandas 的eval底层用 NumExpr在大数据量下比普通运算快。但可读性会下降建议只在性能瓶颈时用。# 普通写法 df[结果] df[A] df[B] * df[C] # eval 写法 df.eval(结果 A B * C, inplaceTrue)5.3 几个容易忽略的细节df.round(2)返回新 DataFrame不修改原数据。而且round用的是银行家舍入四舍六入五成双不是传统的四舍五入。如果需要传统舍入得用np.floor(x 0.5)或decimal模块。df.clip(lower, upper)用于截断异常值比写df[df[列] upper] upper干净得多。做数据清洗时非常实用。df.abs()返回绝对值df.cumsum()返回累计和这些都是一行搞定的事不需要写循环。df.diff()算差分默认是后一行减前一行。做同比环比时df[列].diff(periods12)可以算年度差分月度数据。6. 从数值运算到实际业务一个完整的计算链路假设我们有一份销售明细需要算每个产品的总销售额、平均折扣、销售额排名、累计销售额、环比增长率。这个链路几乎用到了前面所有知识点。# 模拟数据 df pd.DataFrame({ 产品: [A, A, B, B, C, C], 月份: [1, 2, 1, 2, 1, 2], 单价: [10, 10, 20, 18, 15, 15], 数量: [100, 120, 50, 60, 80, 90], 折扣: [0.9, 0.85, 1.0, 0.95, 0.9, 0.88] }) # 1. 算销售额 df[销售额] df[单价] * df[数量] * df[折扣] # 2. 按产品聚合 product_stats df.groupby(产品).agg( 总销售额(销售额, sum), 平均折扣(折扣, mean) ).reset_index() # 3. 排名 product_stats[销售额排名] product_stats[总销售额].rank( methodmin, ascendingFalse ) # 4. 累计销售额按月份排序后 df df.sort_values([产品, 月份]) df[累计销售额] df.groupby(产品)[销售额].cumsum() # 5. 环比增长率 df[环比增长率] df.groupby(产品)[销售额].pct_change() print(product_stats) print(df)这段代码里pct_change()是算环比的标准方法等价于(当前 - 上一期) / 上一期。但要注意每组的第一行结果是 NaN因为前面没有数据。如果业务上要求第一行填 0得手动处理。groupby之后sort_values再cumsum的顺序不能反。如果先cumsum再排序累计值就乱了。这个顺序问题在实际写代码时经常搞错建议养成“先排序、再分组、后累计”的习惯。7. 一些不太出现在文档里的经验Pandas 的数值运算方法看起来简单但真正用好需要理解它背后的索引对齐、NaN 传播、类型系统这三套机制。我见过太多人卡在“为什么结果不对”上最后发现是索引没对齐或者类型是 object。我的建议是每次做完数值运算养成三个检查习惯。第一df.dtypes看类型对不对第二df.isna().sum()看有没有意外的 NaN第三df.describe()看数值范围合不合理。这三个检查花不了十秒钟但能省下大量调试时间。另外不要害怕写多行代码。Pandas 的链式调用很优雅但调试时很痛苦。我现在的习惯是每个关键步骤赋值给一个新变量虽然代码长一点但出问题时能快速定位是哪一步的问题。等逻辑稳定了再考虑合并成链式调用。最后分享一个小技巧pd.set_option(display.max_columns, None)和pd.set_option(display.float_format, {:.2f}.format)这两个设置能让 DataFrame 的打印输出更易读尤其是列多的时候。这个不算数值运算本身但能让你在调试数值问题时看得更清楚。

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

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

免费获取报价 →
↑