做数据分析这些年,被问得最多的一类问题不是"这段代码怎么写",而是"这个数算出来到底对不对"。前者查文档十分钟能解决,后者往往要翻半天的数据字典和业务口径。pandas数据统计函数恰好站在这个分界线上——describe、sum、mean、value_counts这些方法谁都会敲,可真到了要给一份业务报表签字的时候,能拍着胸脯说"这个平均数没问题"的人并不多。我在实际项目里见过太多因为一个默认参数没注意,把整张看板的结论带偏的情况:比如标准差用错了自由度,比如空值被静默跳过导致分母变小,比如滚动窗口没设最小观测数,前几行算出来一个漂亮的假曲线。
这篇内容想聊的就是这一块。它不是一份 API 罗列清单,而是把 pandas 里跟"统计"沾边的那些函数——描述性统计、聚合、排名、相关性、累积与滚动计算、分组统计——按真实使用场景重新捋一遍,说清楚每个函数背后的默认行为是什么、为什么是这个默认值、什么情况下必须改、改了之后影响哪些下游环节。适合已经能读写 DataFrame、但还没把统计口径这件事彻底想明白的人;也适合做数据看板、写分析报告、搭特征工程管道的同学当作速查手册。代码示例统一用一张模拟的订单明细表,字段是常见的订单号、地区、品类、金额、下单日期,你可以直接替换成自己手上的数据。
1. 统计函数为什么值得单独拉出来讲
1.1 从"算得出来"到"算得对"之间的那段距离
很多人学 pandas 的路径是这样的:先学筛选和切片,再学 groupby,最后顺手把mean()sum()当附属品用一下。结果就是这批函数从来没被认真对待过。可现实是,一份分析报告 90% 的结论都建立在这几个数字上,它们错了,后面画多漂亮的图都是白搭。
"算得出来"和"算得对"之间隔着三件事。第一件是缺失值的处理策略——pandas 绝大多数统计函数默认skipna=True,也就是遇到空值直接跳过。这听起来很贴心,但它意味着你算的mean()是"非空样本的均值",而不是"全量样本的均值"。如果空值本身不是随机的,比如高价值客户的金额字段更容易缺失,那你算出的平均值就是有偏的。第二件是分母的定义——count()数的是非空个数,size()数的是包括空值在内的总行数,两个数字放在同一张表里如果没有标注清楚,读者会默认它们是同一个东西。第三件是统计量的适用条件——均值对极端值极其敏感,中位数稳健但丢失了数值信息,众数只对分类变量有意义,选错了统计量,结论的方向都可能反过来。
这三件事没有任何一个能靠背 API 解决,它们属于"口径"层面的判断。所以我把统计函数单独拎出来讲,核心目的不是教你怎么调用,而是帮你在按下回车之前多问一句:这个默认行为,是不是我想要的。
1.2 三种调用姿势的差别与适用边界
同样是求和,pandas 里至少有三种写法,很多新手混着用,出了问题也说不清是哪一步错了。
第一种是Series/DataFrame 自带方法,比如df['amount'].sum()。这是最推荐的姿势,因为它完整继承了 pandas 的缺失值语义、轴参数、min_count控制等能力。
第二种是NumPy 函数直接套,比如np.sum(df['amount'])。这种写法在数据干净的时候结果一样,但它会先把 Series 转成 ndarray,skipna的语义就丢了——遇到NaN,结果直接是nan。而且对 object 类型的列,NumPy 的行为和 pandas 完全不同,很容易出一个你意想不到的结果。
第三种是agg()批量调用,比如df.agg({'amount': ['sum', 'mean']})。它适合一次要出多个指标的场景,返回的是带层级索引的 Series 或 DataFrame,方便直接拼成报表。
我一般的取舍是:单个指标用方法直调,多个指标用agg,永远不用np.xxx去套 pandas 对象——除非我在做纯数值计算且已经确认没有空值。这个习惯帮我避掉了很多"明明数据一样,两次跑出来结果不同"的诡异问题。
2. describe 与基础聚合:最常用也最容易翻车的一层
2.1 describe 输出的每一行到底怎么来的
df.describe()大概是所有人学 pandas 时敲的第二个命令。它默认只统计数值型列,输出 count、mean、std、min、25%、50%、75%、max 八行。这里有几个细节值得掰开说。
count这一行是非空计数,不是总行数。如果你有 1000 行数据,某个字段有 30 个空值,这里显示 970,很多人第一次看会以为数据丢了。
std这一行用的是样本标准差,自由度ddof=1,分母是 n-1。这跟 NumPy 的np.std默认ddof=0不一样。数据量大的时候差异可以忽略,但样本量只有几十的时候,这个差别能到百分之几,做 A/B 实验的方差分析时足以影响显著性判断。
百分位那三行默认是 25%、50%、75%,用的是线性插值(interpolation='linear')。也就是说,如果样本量不能被 4 整除,这四个分位点不是数据里真实存在的某个值,而是两个相邻值插出来的。对外汇报时如果说"75% 的用户消费在 128.5 元以下",这个 128.5 可能是编出来的,严格讲应该说"约"。
还有一个高频误区:describe()对 object 列会输出 count、unique、top、freq 四行,很多人不知道这个行为,看到输出里混着字符串就慌了。想统一看所有列,用df.describe(include='all');只想看分类列,用include=['object'];想排除某些类型,用exclude。另外对 datetime 列,describe会给出 count、mean、min、25%、50%、75%、max,那个mean是时间戳的算术中心,实际业务含义很弱,基本可以忽略。
import pandas as pd df = pd.DataFrame({ "order_id": [f"SO{i:04d}" for i in range(1, 9)], "region": ["华东", "华北", "华东", "华南", "华北", "华东", "华南", "华北"], "category": ["数码", "家居", "数码", "服饰", "家居", "服饰", "数码", "家居"], "amount": [1299.0, 268.5, 899.0, 158.0, 430.0, 76.0, 2150.0, None], "order_date": pd.to_datetime([ "2024-03-01", "2024-03-02", "2024-03-05", "2024-03-06", "2024-03-09", "2024-03-11", "2024-03-15", "2024-03-18" ]) }) print(df["amount"].describe()) print(df["region"].describe()) print(df.describe(include="all").T[["count", "unique", "top", "freq"]].head())2.2 count、sum、mean 对空值的不同态度
这一组函数放一起看,才能看出 pandas 的设计逻辑。count()数非空,sum()、mean()、std()默认跳过空值,而size属性数的是总行数(含空值)。这四个数字放在一起应该是自洽的:count + 空值数 = size。如果你的报表里这三者对不上,说明中间有人做过过滤或者类型转换。
sum()有一个特别容易被忽略的参数min_count。默认是 0,意思是哪怕整列全是空值,也返回 0。这个行为在业务上很危险——一个字段全缺失,报表上显示销售额为 0,看的人会以为真的没有销售。加上min_count=1,全空时返回NaN,反而诚实。我现在的习惯是,凡是涉及金额、数量这类"零和缺失含义完全不同"的字段,一律显式写min_count=1。
mean()在布尔列上会直接算成比例,True记 1,False记 0。这个特性用来算转化率、留存率特别顺手,df['is_paid'].mean()就是付费率,不用先转 int 再算。但要注意布尔列里如果混进了NaN,结果就变成 object 类型,mean()会报错或者算出奇怪的东西。
还有一个坑:sum()作用在字符串列上会做字符串拼接,不是报错。想象一下订单备注列求和,得到一坨连在一起的文本,看起来还挺像那么回事。所以聚合之前确认列类型,df.dtypes看一眼,比事后 debug 省事得多。
| 函数 | 默认是否跳过 NaN | 关键参数 | 影响范围 |
|---|---|---|---|
count() | 是(本质就是数非空) | axis | 分母口径、覆盖率计算 |
size | 不涉及(数总行) | 无 | 样本总量校验 |
sum() | 是 | min_count、axis | 全空列返回 0 的假象 |
mean() | 是 | axis、numeric_only | 转化率、均单价 |
std()/var() | 是 | ddof(默认 1) | 波动率、显著性检验 |
median() | 是 | axis | 抗极端的中心趋势 |
mode() | 是 | 返回 DataFrame | 分类变量的众数,可能多行 |
quantile() | 是 | q、interpolation | 分位阈值、异常值截断 |
2.3 中位数、众数、分位数的选型逻辑
均值、中位数、众数这三个,教科书上讲得很清楚,但落到实际数据上,选哪个往往决定了结论的方向。
我的一般判断顺序是:先看分布形状,再看业务关心什么。如果数据近似对称、没有明显长尾,均值信息量最大,因为它用到了每一个观测值。如果存在长尾——比如客单价,大部分人几十块,少数人几万块——均值会被那几个人拉高,这时候中位数更能代表"典型用户"。如果变量是纯分类的,比如支付方式、地区,那只能看众数。
但中位数也不是万能的。它对数据的"两端"完全不敏感,一个把 10% 的用户从 100 元提到 200 元的活动,中位数可能纹丝不动,因为它没改变中间那个人的位置。所以我实际做报表时经常是均值和中位数一起给,再配一个标准差或者四分位距(IQR = 75% 分位 - 25% 分位),三个数字放一起,读者对分布的感知会比单看一个数字立体得多。
分位数这块,quantile()的interpolation参数值得记一下。默认linear是线性插值;nearest取最近的真实值;lower/higher取相邻两个真实值的下界或上界;midpoint取中点。做异常值截断(比如把 99 分位以上的值砍掉)时,我倾向用interpolation='lower',保证截断阈值本身就是数据里存在的值,避免引入一个凭空造出来的数。
mode()要单独提醒一句:它返回的是 DataFrame 而不是标量,因为一列可能有多个众数。你要是直接拿去和标量比较,会得到一个形状不匹配的报错。取第一个众数写df['col'].mode().iloc[0],但要心里清楚,这可能只是并列第一里的一个。
3. value_counts、rank 与相关性:分类、排序与关系
3.1 value_counts 的排序、归一化与分箱
value_counts()是分类变量分析的第一站,比groupby().size()写起来短,输出还自带降序排列。但它有几个参数组合,用好了能省很多步骤。
normalize=True直接给占比,省掉手动除以总数的步骤。dropna=False把空值也纳入统计——默认是丢弃的,这意味着你看到的占比分母不包含空值,如果空值占比有 20%,你算出来的所有比例都被放大了 25%。做数据质量报告时,我基本都会打开这个参数,因为"缺失本身就是一个类别"。
bins参数是个隐藏好功能,只对数值型 Series 有效,能把连续值直接切成区间统计。比如把订单金额按 0-100、100-500、500-1000、1000 以上分箱,一行代码出分布。注意bins给出的区间默认是左开右闭((0, 100]),第一个区间会包含最小值,如果业务上习惯左闭右开,得自己用pd.cut配right=False。
sort和ascending的组合也值得说。默认按频次降序,但如果你要做的是按类别名称排序的对比图,就得写sort=False然后再自己reindex,否则图上的柱子顺序会按频次乱跳,读者对比起来很累。
还有一个性能层面的经验:在千万行级别的 Series 上,value_counts()比groupby().size()快不少,因为它内部走了专门的哈希路径。所以做基数统计(比如统计不同用户数)时,优先用它;只有需要多列联合分组时才回到groupby。
3.2 rank 的 method 参数到底在排什么
rank()处理的是并列情况,method参数决定并列的名次怎么分配。这个参数平时用得少,一旦用上就是关键。
假设三个值 90、90、80。method='average'(默认)给两个 90 都排 1.5,80 排 3;min给两个 90 都排 1,80 排 3;max给两个 90 都排 2,80 排 3;first按出现顺序给 1 和 2;dense给 1、1、2,名次连续不跳号。
什么时候在意这个?做排行榜的时候。如果两个并列第一,下一个应该是第三(min或average),还是第二(dense)?业务规则不同,选法不同。做分位数分组、十分位打标时,dense更常用,因为它保证名次是连续整数,方便后续做等频分箱。
ascending默认 True,也就是小值排前面。算"金额排名前十"的时候别忘了改成ascending=False,否则你排出来的是金额最小的十个。na_option控制空值的位置,keep保留 NaN(默认)、top排最前、bottom排最后。做 TopN 筛选时用keep最安全,因为 NaN 不该参与排名。
pct=True直接把名次转成百分位(0 到 1),这个在做百分位打标时很省事,等价于rank(pct=True)。
3.3 corr 与 cov 的选型:别默认用皮尔逊
corr()默认是皮尔逊相关系数,衡量的是线性关系。这句话的重点在"线性"两个字。两个变量完全相关但关系是 U 型的,皮尔逊系数可能接近 0,你会得出"没关系"的错误结论。
所以选型逻辑是:数据近似正态、关系大致线性,用pearson;存在单调但非线性的关系,或者有极端值,用spearman(基于秩,抗异常值);样本量小、有很多并列秩,用kendall。我做过一个用户活跃度与消费额的分析,皮尔逊算出来 0.21,看起来弱相关,换成斯皮尔曼变成 0.58,原因就是消费额有几个超大值把线性关系压扁了。
corr()还有两个参数要注意。min_periods控制最少有效样本数,默认是按列对计算,样本太少时会给出一个基于极少点的高相关系数,很误导。我一般设成 30 以上。numeric_only在 pandas 2.x 的行为有过调整,显式写numeric_only=True最稳,不然遇到字符串列可能直接抛错。
cov()算协方差,量纲受原始单位影响,跨变量比较没有意义,只适合看方向。真要比较强度,还是用相关系数。corrwith()是算一个 Series 与 DataFrame 各列的相关系数,做"和目标变量逐一比对"的特征初筛时比循环写起来干净。
4. 累积、滚动与变化率:时间维度上的统计
4.1 cumsum 与 cummax 的轴和跳过逻辑
累积类函数有四个:cumsum、cumprod、cummax、cummin。它们的共同点是按顺序逐个计算,返回和原数据同形状的结果。
cumsum的skipna参数默认 True,遇到 NaN 时跳过继续累加。设成 False 的话,一旦遇到空值,后面所有结果都变成 NaN。这两种行为对应两种业务含义:跳过相当于"这笔没记录,忽略它继续累计";不跳过相当于"数据断了,后面的累计不可信"。做资金流水累计的时候,我倾向用skipna=False,因为中间断了一笔,后面的余额就是错的,宁可让它显示为缺失,也不能给出一个看起来正常的错数。
axis参数决定方向。axis=0沿行方向(每列内部从上到下),axis=1沿列方向(每行内部从左到右)。默认 0,做列间累计(比如多期付款的累计到账)时才用 1。
cummax/cummin是求到目前为止的最大值/最小值,典型用途是算"历史最高水位"。比如跟踪某个指标的历史峰值,或者做回撤分析——当前值相对历史最高值的跌幅,用(df['price'] / df['price'].cummax() - 1)一行就出来了。
这块有个性能提醒:累积函数是逐元素计算,在千万行级别上比向量化运算慢一个量级。如果只是要最终结果,用sum();确实需要每一步的中间值,才用cumsum()。
4.2 rolling 的窗口、min_periods 与 center
滚动窗口是时间序列分析的核心工具,rolling(window=n)表示以当前行为终点、往前取 n 行计算。
最容易踩的坑是min_periods。它的默认值等于window,意思是在前 n-1 行因为凑不满窗口,结果全部是 NaN。这其实是个合理的默认,但很多人看到前几行是空的就慌了,把min_periods改成 1,结果前几行用 1 个、2 个点算出来的均值被当成正常值画进了趋势图,曲线开头一段剧烈波动,完全是假象。我的建议是:展示用图可以设min_periods=window让前面留白;一定要填补的话,明确标注"样本不足",别让读者误以为是真实趋势。
center=True让窗口以当前行为中心,前后各取一半。它只影响对齐位置,不改变计算内容。做信号平滑时用 center 更合理,因为不引入滞后;但做实时预测时必须用默认的右对齐,否则你用了未来的数据——这是典型的未来信息泄露。
closed参数控制窗口区间的开闭,默认是right(右闭),也就是(t-n, t]。改这个参数主要影响边界值的归属,日常用得不多,但做严格的窗口计数时得留意。
win_type可以指定窗口函数类型,比如'gaussian'、'triang',配合std参数做加权平滑。不过用之前需要装 scipy,且 performance 会下降,除非有明确的平滑需求,普通移动平均用默认的等权就够了。
4.3 ewm 与 pct_change 的实际用法
ewm()是指数加权移动,特点是越近的数据权重越大,衰减由span(跨度)、halflife(半衰期)、alpha(平滑系数)三个参数之一控制,三者是等价的,给一个就行。关系是alpha = 2 / (span + 1),halflife是权重衰减到一半所需的期数。
为什么用指数加权而不是简单移动平均?因为简单移动平均有个"悬崖效应"——窗口内第 n 期的数据权重是 1/n,第 n+1 期突然变成 0,一个老数据掉出窗口会让均值跳一下。指数加权是平滑衰减,没有这个突变,适合做平滑序列,比如监控指标的异常检测基线。
adjust参数默认 True,做的是标准的加权平均(分母是权重之和);设成 False 则是递归形式,前期结果受初始值影响较大。做长期平滑我一般保留默认 True,结果更稳定。
pct_change()算的是环比变化率,公式是(当前 - 前一期) / 前一期。periods参数可以指定间隔期数,periods=7就是七日环比,做周同比时很常用。
这里要提醒一个版本变化:早期pct_change有个fill_method参数,默认会用前值填充缺失再算变化率,这会掩盖数据中断。新版 pandas 已经弱化了自动填充的默认行为,但为了稳妥,我现在的做法都是先显式处理缺失(ffill或者直接删),再调pct_change(),不让函数替我做决定。另外分母为 0 时结果会是inf,这个值在后续计算里会污染一切,用replace([np.inf, -np.inf], np.nan)清一下比较安全。
5. 分组统计与 agg 组合拳
5.1 groupby 加 agg 的命名聚合写法
groupby().agg()是日常用得最多的组合,但老写法(传字典)出来的列名是多层索引,拼报表时还得手动改,很啰嗦。命名聚合(named aggregation)解决了这个问题:
summary = df.groupby("region", dropna=False).agg( 订单数=("order_id", "count"), 总金额=("amount", "sum"), 均单价=("amount", "mean"), 金额中位数=("amount", "median"), 金额标准差=("amount", "std"), 最大单笔=("amount", "max") ).reset_index() summary["变异系数"] = summary["金额标准差"] / summary["均单价"]好处是列名一步到位,输出直接就是干净的表格,能直接导出。注意命名聚合要求每个值是一个二元组(源列名, 函数名),同一源列可以被多次使用,这比字典写法灵活得多。
dropna=False这个参数值得单独说。groupby默认会把分组键里的空值丢掉,如果某个地区字段有缺失,那部分订单在汇总表里就凭空消失了,各分组金额加起来对不上总数。做对账类报表时,我一定会打开这个参数,把空值单独作为一个分组显示出来。
还有observed参数,对 category 类型的分组键有用,默认 False 会把所有类别都列出来(包括数据里没出现的),设成 True 只保留实际出现的类别。前者适合做全量对照表,后者适合做数据概览。
5.2 transform、apply 与 agg 的职责边界
这三个方法经常被混用,其实职责很清楚。
agg是降维:n 行进,m 组出。适合做汇总表。
transform是保持形状:n 行进,n 行出,结果按原索引对齐。适合做"把组内统计量贴回原表"。最典型的场景是算占比——每个订单金额占本地区总额的比例,用df['amount'] / df.groupby('region')['amount'].transform('sum')一行搞定。如果用agg再merge回去,代码长三倍还容易出错。
apply是万能但慢:把每个分组当成一个小 DataFrame 传给你的函数,你可以返回任意形状的结果。它灵活,但因为是 Python 层面循环,性能比内置聚合慢一个数量级以上。我的原则是:能用内置函数(字符串形式的聚合名)就不用agg传自定义函数,能用agg就不用apply。
transform有个细节要注意:传入自定义函数时,函数的返回值必须与组内长度一致,或者返回标量(会自动广播)。返回长度不一致会直接报错。这也是它和apply的核心区别。
5.3 自定义统计函数怎么接进去
内置聚合名覆盖了大部分场景,但业务上总有些特殊指标。比如"去重用户数",用nunique;比如"金额的 90 分位",用lambda x: x.quantile(0.9);比如"变异系数",标准库里没有,得自己写。
自定义函数接入agg的时候,性能是主要考量。纯 Python 函数在大数据上很慢,能用 NumPy 实现的尽量用 NumPy。比如算变异系数,写成lambda x: x.std() / x.mean() if x.mean() != 0 else np.nan,比先取出来再循环快得多。注意那个除零保护,不加的话某些组会返回inf,一路污染到最终报表。
还有一个组合技巧:agg里可以同时混用字符串和函数,{'amount': ['sum', 'mean', lambda x: x.max() - x.min()]},输出的列名会是<lambda>,很难看。这种情况更推荐用命名聚合,逐个指定清晰的列名,可读性和可维护性都好得多。
6. 常见问题与排查实录
6.1 常见问题速查表
下面这张表是我这几年被问得最多、自己也踩过的坑,按现象倒推原因,应急时可以直接查。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 均值比预期高很多 | 空值被跳过,分母变小;或存在极端值 | 检查count与size差值;对比中位数 |
| 各分组金额加总不等于总额 | groupby默认丢弃分组键的空值 | 加dropna=False后重新汇总 |
| 全空列求和显示 0 | sum()默认min_count=0 | 显式设min_count=1 |
| 标准差和别的工具算出来不一样 | 自由度差异,pandas 默认ddof=1 | 确认对方用的是样本还是总体 |
| 滚动均值开头一段剧烈波动 | min_periods被改成了 1 | 恢复为window或明确标注样本不足 |
| 相关系数偏低但图上明显有关系 | 关系非线性,皮尔逊失效 | 换spearman;或先做散点图 |
变化率出现inf | 前一期值为 0 | 先replace成 NaN;或改用差值 |
| 求和结果是一长串文字 | 列是 object 类型,字符串被拼接 | astype(float)前先查dtypes |
mode()结果不能直接比较 | 返回的是 DataFrame,可能多行 | 用.iloc[0]并确认是否多众数 |
describe里没有某列 | 该列非数值类型,被默认排除 | 用include='all'查看 |
6.2 几个我实际踩过的坑
第一个坑是关于分母的。早些年做月度复购率报表,我图省事用了df['is_repurchase'].mean(),跑出来 34%。后来业务方质疑说他们手工算是 28%,查了半天才发现,is_repurchase字段在部分历史数据里是空的,mean()自动跳过了这些行,分母比实际用户数少了近两成。改成先fillna(False)再算,数字立刻对上了。这件事之后我养成了习惯:凡是比率类指标,先确认分子分母各自怎么定义,再动手写代码。
第二个坑是标准差的口径。同一个指标,我用 pandas 算出来是 12.4,同事用另一套工具算出来是 12.1,两个人对着屏幕争了半天。最后发现是ddof的差别,样本量一百多的时候差了 2.5%。数字本身不大,但那次是要写进合同附件里的,差一点就得出去了。现在的做法是,只要标准差要对外,一律在文档里标注清楚自由度设定。
第三个坑是滚动窗口的边界。做一个七日移动平均的监控曲线,图上开头几天的数值上蹿下跳,我以为是有异常数据,排查了两个小时才发现min_periods被之前的人设成了 1。前六天分别用 1 到 6 个点算平均,样本量这么小,波动当然大。把min_periods改回窗口长度,前面留白,曲线立刻就正常了。这个教训是:图表上的"异常"先怀疑计算逻辑,再怀疑数据本身。
第四个坑稍微隐蔽一点,是链式操作里的类型退化。我用groupby().agg()出了一张汇总表,里面有一列全是整数,然后做除法算比率,结果全是 0。原因是那列在聚合后变成了 object(因为某些组返回了字符串),除法做了整数整除。加上astype(float)就好了。现在我习惯在聚合之后print(df.dtypes)看一眼,三秒钟的事,能省掉半小时的排查。
6.3 让统计结果可信的几个习惯
最后分享几个我自己坚持的做法,都是被坑出来的。
第一,报表里同时给出样本量和统计量。只给一个均值,读者不知道背后是 10 条数据还是 10 万条;加上count,可信度立刻不一样。
第二,异常值先看再处理,不要直接删。quantile(0.99)看一眼上边界,和业务方确认那些大值是真实交易还是录入错误。直接clip掉可能把真实的大客户砍没了。
第三,关键指标做交叉验证。用groupby().sum()算完总额,再用整表sum()对一次,两者应该相等(考虑空值分组的前提下)。这个动作花不了几秒,但能抓住大部分逻辑错误。
第四,把口径写进代码注释。# 均值计算排除未支付订单,分母为非空已支付单量这样一句话,半年后自己回头看能救命,交接给同事也能省很多解释。
这套函数本身不难,难的是每次调用前都想清楚默认行为是不是符合业务口径。我在实际项目里的体会是,凡是数字对不上,八成不是代码写错,而是某个默认参数替你做了一次你没授权的决定。把skipna、ddof、min_periods、dropna、min_count这几个开关当成必须显式确认的项,统计结果的可靠性会上一个台阶。后续如果你要把这套东西接到自动化报表里,建议再补一层校验:每次跑完对比上一次的结果,波动超过阈值就报警,这比事后人工核对省力得多。