资讯动态

pandas统计函数默认参数与数据口径避坑指南

发布时间:2026/10/27 15:54:39 来源:尧图企业网站定制
做数据分析这些年被问得最多的一类问题不是这段代码怎么写而是这个数算出来到底对不对。前者查文档十分钟能解决后者往往要翻半天的数据字典和业务口径。pandas数据统计函数恰好站在这个分界线上——describe、sum、mean、value_counts这些方法谁都会敲可真到了要给一份业务报表签字的时候能拍着胸脯说这个平均数没问题的人并不多。我在实际项目里见过太多因为一个默认参数没注意把整张看板的结论带偏的情况比如标准差用错了自由度比如空值被静默跳过导致分母变小比如滚动窗口没设最小观测数前几行算出来一个漂亮的假曲线。这篇内容想聊的就是这一块。它不是一份 API 罗列清单而是把 pandas 里跟统计沾边的那些函数——描述性统计、聚合、排名、相关性、累积与滚动计算、分组统计——按真实使用场景重新捋一遍说清楚每个函数背后的默认行为是什么、为什么是这个默认值、什么情况下必须改、改了之后影响哪些下游环节。适合已经能读写 DataFrame、但还没把统计口径这件事彻底想明白的人也适合做数据看板、写分析报告、搭特征工程管道的同学当作速查手册。代码示例统一用一张模拟的订单明细表字段是常见的订单号、地区、品类、金额、下单日期你可以直接替换成自己手上的数据。1. 统计函数为什么值得单独拉出来讲1.1 从算得出来到算得对之间的那段距离很多人学 pandas 的路径是这样的先学筛选和切片再学 groupby最后顺手把mean()sum()当附属品用一下。结果就是这批函数从来没被认真对待过。可现实是一份分析报告 90% 的结论都建立在这几个数字上它们错了后面画多漂亮的图都是白搭。算得出来和算得对之间隔着三件事。第一件是缺失值的处理策略——pandas 绝大多数统计函数默认skipnaTrue也就是遇到空值直接跳过。这听起来很贴心但它意味着你算的mean()是非空样本的均值而不是全量样本的均值。如果空值本身不是随机的比如高价值客户的金额字段更容易缺失那你算出的平均值就是有偏的。第二件是分母的定义——count()数的是非空个数size()数的是包括空值在内的总行数两个数字放在同一张表里如果没有标注清楚读者会默认它们是同一个东西。第三件是统计量的适用条件——均值对极端值极其敏感中位数稳健但丢失了数值信息众数只对分类变量有意义选错了统计量结论的方向都可能反过来。这三件事没有任何一个能靠背 API 解决它们属于口径层面的判断。所以我把统计函数单独拎出来讲核心目的不是教你怎么调用而是帮你在按下回车之前多问一句这个默认行为是不是我想要的。1.2 三种调用姿势的差别与适用边界同样是求和pandas 里至少有三种写法很多新手混着用出了问题也说不清是哪一步错了。第一种是Series/DataFrame 自带方法比如df[amount].sum()。这是最推荐的姿势因为它完整继承了 pandas 的缺失值语义、轴参数、min_count控制等能力。第二种是NumPy 函数直接套比如np.sum(df[amount])。这种写法在数据干净的时候结果一样但它会先把 Series 转成 ndarrayskipna的语义就丢了——遇到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这一行用的是样本标准差自由度ddof1分母是 n-1。这跟 NumPy 的np.std默认ddof0不一样。数据量大的时候差异可以忽略但样本量只有几十的时候这个差别能到百分之几做 A/B 实验的方差分析时足以影响显著性判断。百分位那三行默认是 25%、50%、75%用的是线性插值interpolationlinear。也就是说如果样本量不能被 4 整除这四个分位点不是数据里真实存在的某个值而是两个相邻值插出来的。对外汇报时如果说75% 的用户消费在 128.5 元以下这个 128.5 可能是编出来的严格讲应该说约。还有一个高频误区describe()对 object 列会输出 count、unique、top、freq 四行很多人不知道这个行为看到输出里混着字符串就慌了。想统一看所有列用df.describe(includeall)只想看分类列用include[object]想排除某些类型用exclude。另外对 datetime 列describe会给出 count、mean、min、25%、50%、75%、max那个mean是时间戳的算术中心实际业务含义很弱基本可以忽略。import pandas as pd df pd.DataFrame({ order_id: [fSO{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(includeall).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_count1全空时返回NaN反而诚实。我现在的习惯是凡是涉及金额、数量这类零和缺失含义完全不同的字段一律显式写min_count1。mean()在布尔列上会直接算成比例True记 1False记 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 分位以上的值砍掉时我倾向用interpolationlower保证截断阈值本身就是数据里存在的值避免引入一个凭空造出来的数。mode()要单独提醒一句它返回的是 DataFrame 而不是标量因为一列可能有多个众数。你要是直接拿去和标量比较会得到一个形状不匹配的报错。取第一个众数写df[col].mode().iloc[0]但要心里清楚这可能只是并列第一里的一个。3. value_counts、rank 与相关性分类、排序与关系3.1 value_counts 的排序、归一化与分箱value_counts()是分类变量分析的第一站比groupby().size()写起来短输出还自带降序排列。但它有几个参数组合用好了能省很多步骤。normalizeTrue直接给占比省掉手动除以总数的步骤。dropnaFalse把空值也纳入统计——默认是丢弃的这意味着你看到的占比分母不包含空值如果空值占比有 20%你算出来的所有比例都被放大了 25%。做数据质量报告时我基本都会打开这个参数因为缺失本身就是一个类别。bins参数是个隐藏好功能只对数值型 Series 有效能把连续值直接切成区间统计。比如把订单金额按 0-100、100-500、500-1000、1000 以上分箱一行代码出分布。注意bins给出的区间默认是左开右闭(0, 100]第一个区间会包含最小值如果业务上习惯左闭右开得自己用pd.cut配rightFalse。sort和ascending的组合也值得说。默认按频次降序但如果你要做的是按类别名称排序的对比图就得写sortFalse然后再自己reindex否则图上的柱子顺序会按频次乱跳读者对比起来很累。还有一个性能层面的经验在千万行级别的 Series 上value_counts()比groupby().size()快不少因为它内部走了专门的哈希路径。所以做基数统计比如统计不同用户数时优先用它只有需要多列联合分组时才回到groupby。3.2 rank 的 method 参数到底在排什么rank()处理的是并列情况method参数决定并列的名次怎么分配。这个参数平时用得少一旦用上就是关键。假设三个值 90、90、80。methodaverage默认给两个 90 都排 1.580 排 3min给两个 90 都排 180 排 3max给两个 90 都排 280 排 3first按出现顺序给 1 和 2dense给 1、1、2名次连续不跳号。什么时候在意这个做排行榜的时候。如果两个并列第一下一个应该是第三min或average还是第二dense业务规则不同选法不同。做分位数分组、十分位打标时dense更常用因为它保证名次是连续整数方便后续做等频分箱。ascending默认 True也就是小值排前面。算金额排名前十的时候别忘了改成ascendingFalse否则你排出来的是金额最小的十个。na_option控制空值的位置keep保留 NaN默认、top排最前、bottom排最后。做 TopN 筛选时用keep最安全因为 NaN 不该参与排名。pctTrue直接把名次转成百分位0 到 1这个在做百分位打标时很省事等价于rank(pctTrue)。3.3 corr 与 cov 的选型别默认用皮尔逊corr()默认是皮尔逊相关系数衡量的是线性关系。这句话的重点在线性两个字。两个变量完全相关但关系是 U 型的皮尔逊系数可能接近 0你会得出没关系的错误结论。所以选型逻辑是数据近似正态、关系大致线性用pearson存在单调但非线性的关系或者有极端值用spearman基于秩抗异常值样本量小、有很多并列秩用kendall。我做过一个用户活跃度与消费额的分析皮尔逊算出来 0.21看起来弱相关换成斯皮尔曼变成 0.58原因就是消费额有几个超大值把线性关系压扁了。corr()还有两个参数要注意。min_periods控制最少有效样本数默认是按列对计算样本太少时会给出一个基于极少点的高相关系数很误导。我一般设成 30 以上。numeric_only在 pandas 2.x 的行为有过调整显式写numeric_onlyTrue最稳不然遇到字符串列可能直接抛错。cov()算协方差量纲受原始单位影响跨变量比较没有意义只适合看方向。真要比较强度还是用相关系数。corrwith()是算一个 Series 与 DataFrame 各列的相关系数做和目标变量逐一比对的特征初筛时比循环写起来干净。4. 累积、滚动与变化率时间维度上的统计4.1 cumsum 与 cummax 的轴和跳过逻辑累积类函数有四个cumsum、cumprod、cummax、cummin。它们的共同点是按顺序逐个计算返回和原数据同形状的结果。cumsum的skipna参数默认 True遇到 NaN 时跳过继续累加。设成 False 的话一旦遇到空值后面所有结果都变成 NaN。这两种行为对应两种业务含义跳过相当于这笔没记录忽略它继续累计不跳过相当于数据断了后面的累计不可信。做资金流水累计的时候我倾向用skipnaFalse因为中间断了一笔后面的余额就是错的宁可让它显示为缺失也不能给出一个看起来正常的错数。axis参数决定方向。axis0沿行方向每列内部从上到下axis1沿列方向每行内部从左到右。默认 0做列间累计比如多期付款的累计到账时才用 1。cummax/cummin是求到目前为止的最大值/最小值典型用途是算历史最高水位。比如跟踪某个指标的历史峰值或者做回撤分析——当前值相对历史最高值的跌幅用(df[price] / df[price].cummax() - 1)一行就出来了。这块有个性能提醒累积函数是逐元素计算在千万行级别上比向量化运算慢一个量级。如果只是要最终结果用sum()确实需要每一步的中间值才用cumsum()。4.2 rolling 的窗口、min_periods 与 center滚动窗口是时间序列分析的核心工具rolling(windown)表示以当前行为终点、往前取 n 行计算。最容易踩的坑是min_periods。它的默认值等于window意思是在前 n-1 行因为凑不满窗口结果全部是 NaN。这其实是个合理的默认但很多人看到前几行是空的就慌了把min_periods改成 1结果前几行用 1 个、2 个点算出来的均值被当成正常值画进了趋势图曲线开头一段剧烈波动完全是假象。我的建议是展示用图可以设min_periodswindow让前面留白一定要填补的话明确标注样本不足别让读者误以为是真实趋势。centerTrue让窗口以当前行为中心前后各取一半。它只影响对齐位置不改变计算内容。做信号平滑时用 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第 n1 期突然变成 0一个老数据掉出窗口会让均值跳一下。指数加权是平滑衰减没有这个突变适合做平滑序列比如监控指标的异常检测基线。adjust参数默认 True做的是标准的加权平均分母是权重之和设成 False 则是递归形式前期结果受初始值影响较大。做长期平滑我一般保留默认 True结果更稳定。pct_change()算的是环比变化率公式是(当前 - 前一期) / 前一期。periods参数可以指定间隔期数periods7就是七日环比做周同比时很常用。这里要提醒一个版本变化早期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, dropnaFalse).agg( 订单数(order_id, count), 总金额(amount, sum), 均单价(amount, mean), 金额中位数(amount, median), 金额标准差(amount, std), 最大单笔(amount, max) ).reset_index() summary[变异系数] summary[金额标准差] / summary[均单价]好处是列名一步到位输出直接就是干净的表格能直接导出。注意命名聚合要求每个值是一个二元组(源列名, 函数名)同一源列可以被多次使用这比字典写法灵活得多。dropnaFalse这个参数值得单独说。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默认丢弃分组键的空值加dropnaFalse后重新汇总全空列求和显示 0sum()默认min_count0显式设min_count1标准差和别的工具算出来不一样自由度差异pandas 默认ddof1确认对方用的是样本还是总体滚动均值开头一段剧烈波动min_periods被改成了 1恢复为window或明确标注样本不足相关系数偏低但图上明显有关系关系非线性皮尔逊失效换spearman或先做散点图变化率出现inf前一期值为 0先replace成 NaN或改用差值求和结果是一长串文字列是 object 类型字符串被拼接astype(float)前先查dtypesmode()结果不能直接比较返回的是 DataFrame可能多行用.iloc[0]并确认是否多众数describe里没有某列该列非数值类型被默认排除用includeall查看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这几个开关当成必须显式确认的项统计结果的可靠性会上一个台阶。后续如果你要把这套东西接到自动化报表里建议再补一层校验每次跑完对比上一次的结果波动超过阈值就报警这比事后人工核对省力得多。

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

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

免费获取报价 →
↑