NumPy 的统计函数是我日常使用频率最高的一类函数,没有之一。不管是做数据分析、搭建机器学习特征管道,还是给业务方输出一份数据摘要,你几乎都会和 mean、std、median 这几个老熟人打交道。但说句实话,我带新人时发现,很多人对这些函数的理解停留在"会调用"的层面,参数怎么选、结果怎么解读、坑在哪里,往往说不出个所以然。
这篇内容我打算换个讲法:不按官方文档的顺序罗列函数,而是按实际使用场景把 NumPy 数据统计的函数拆开揉碎,讲清楚每个函数背后的设计逻辑,配上可以直接抄走的代码片段。无论你是刚接触 NumPy 的初学者,还是已经写了几年 Python 脚本的老手,只要工作中需要处理数值数据,这篇内容都能帮你少走弯路。
1. 统计函数怎么分组:先建立一张索引图
1.1 描述性统计:给数据画一幅素描
NumPy 的统计函数少说也有三四十个,如果逐个记很容易乱。我习惯把它们分成四类:位置指标、离散指标、分布指标、累计指标。
位置指标回答"数据大概在什么水平",包括 mean、median、percentile、quantile、min、max。离散指标回答"数据波动有多大",包括 var、std、ptp(极差)。分布指标回答"数据长什么样",包括 histogram、bincount、unique 的计数能力。累计指标则回答"到目前为止一共是多少",典型代表是 cumsum、cumprod。
判断一个函数属于哪一类,最简单的办法是看它的返回值形状:位置和离散类函数通常把一个数组压缩成一个数值或一组数值,累计类函数则保持输入的长度不变,每个位置都携带"到当前位置为止"的信息。理解这个分类,遇到新函数时你就能快速猜到它的用途。
这四类函数里,真正在业务分析中天天用的大约只有七八个:mean、median、std、var、percentile、min、max、corrcoef。其他的更多是特定场景的利器,比如累计类函数在做增长趋势统计时非常好用,但平时确实想不起来。
1.2 axis 参数:理解方向才能用对统计函数
axis 是 NumPy 统计函数里最容易搞混的参数,没有之一。很多人第一次接触时记"axis=0 是行,axis=1 是列",这个记法在新手期勉强能用,但遇到三维数组就彻底失效了。
我更推荐另一个心智模型:axis 指定的是你要"压缩掉"的那个维度。假设有一个形状为 (3, 4) 的二维数组,axis=0 意味着沿着第 0 个轴的方向聚合,操作会跨行进行,最终结果丢失第 0 维,变成形状 (4,);axis=1 则跨列聚合,结果变成 (3,)。你可以把统计函数想象成一台压路机,axis 决定了压路机朝哪个方向碾过去,被碾过的维度就消失了。
验证这个模型最直接的方式是拿小数组试:
import numpy as np scores = np.array([ [85, 90, 78], [80, 88, 92], [75, 82, 86] ]) # 每行是某个学生三次考试的分数,每列是一次考试的全部学生分数 row_means = np.mean(scores, axis=1) # 压缩列方向,得到每个学生的平均分 col_means = np.mean(scores, axis=0) # 压缩行方向,得到每次考试的平均分 print(row_means) # [84.33 86.67 81.0 ] print(col_means) # [80. 86.67 85.33]多维数组的道理一样,axis=2 就压缩第三维。我的经验是:每次写带 axis 的代码之前,先在脑子里回答一个问题——"我想保留下来的维度是什么",剩下的维度就是 axis 的取值。这样想比死记硬背"行还是列"可靠得多。
1.3 nan 安全版本:自动忽略缺失值是双刃剑
NumPy 为几乎所有统计函数都提供了 nan 安全版本:nanmean、nanmedian、nanstd、nanvar、nansum、nanmax、nanmin。它们的语义很直白:计算时自动跳过 NaN 值。
data = np.array([1.0, 2.0, np.nan, 4.0]) print(np.mean(data)) # nan print(np.nanmean(data)) # 2.333...普通版本遇到 NaN 会直接返回 NaN,这个行为其实是刻意的——它提醒你数据里有脏值。nan 版本则帮你跳过脏值继续计算。两者没有绝对的对错,关键看使用场景。
我个人的习惯是:探索性分析阶段用 nan 版本快速看趋势,但要同时统计 NaN 的数量和占比;正式产出指标或建模之前,必须用 np.isnan(data).sum() 摸清缺失情况,再决定是填充还是删除。如果数据里 NaN 占比很高,nanmean 给出的均值会产生严重误导,因为它等于在假装这些缺失值不存在。
另外一个容易忽略的点:np.max 和 np.min 没有对应的 nan 版本?其实有的,是 nanmax 和 nanmin,但很多人不知道,结果在含 NaN 的数组上取最大最小值时莫名其妙拿到 nan。另外 NumPy 2.0 之后函数命名也做了些调整,老代码迁移时要注意兼容性测试。
2. 最常用的六个统计函数:参数细节一次说清
2.1 mean 与 average:平均值函数里的隐藏能力
np.mean 和 np.average 表面上看都是求平均,但 np.average 多了一个 weights 参数,这是两者最本质的区别。
np.mean 等价于所有元素求和后除以元素个数,每个数据点的权重完全相同。np.average 则可以给每个数据点指定权重,内部实现是加权求和后除以权重总和。举个真实场景:你想算一批商品的加权平均成交价,价格是 [100, 120, 110],对应的成交量是 [30, 50, 20],那加权平均价就应该用 np.average:
prices = np.array([100, 120, 110]) volumes = np.array([30, 50, 20]) weighted_avg = np.average(prices, weights=volumes) print(weighted_avg) # 112.0 # 等价的手工计算:(100*30 + 120*50 + 110*20) / (30+50+20) = 11200/100如果不加权,普通均值是 110,但加权后是 112,因为成交量大的 120 元商品把均价拉高了。这个差异在业务分析里可能就是完全不同的结论。
另一个实用点是 np.mean 支持 axis,np.average 同样支持,所以按行或按列分别求加权均值完全没有问题。此外,np.mean 对整数数组会返回浮点数,这个细节很多人没注意到,它在后续计算中能帮你避免整数除法的精度丢失。
2.2 std 与 var:ddof 决定你用总体还是样本
标准差和方差可能是统计函数里最容易用错参数的地方,罪魁祸首就是 ddof。ddof 全称 delta degrees of freedom,中文叫"自由度修正",它决定了分母是 n 还是 n-1。
NumPy 默认 ddof=0,计算的是总体标准差,分母为 n。但很多从 Excel 或 pandas 转过来的同学习惯的是样本标准差,分母为 n-1,pandas 的 std() 方法默认就是 ddof=1。这意味着同一次计算,NumPy 和 pandas 的结果会不一样,这不是 Bug,是默认参数不同。
什么时候用哪个?核心判断标准只有一个:你的数据是完整总体还是抽样样本。如果手里是某公司全部员工的工资,算总体标准差,用 ddof=0;如果是从 10 万用户里抽了 1000 人做调研,算样本标准差,用 ddof=1。
sample = np.array([85, 90, 78, 92, 88]) print(np.std(sample)) # 4.883(总体标准差,默认) print(np.std(sample, ddof=1)) # 5.459(样本标准差)从 4.883 到 5.459,差别接近 12%,在数据量小时这个差异非常显著。我的建议是:在团队规范里明确统一 ddof 的用法,否则你写 4.88,同事写 5.46,同一个指标两种口径,对账时非常痛苦。
标准差和方差还有一个容易被忽略的特性:np.var 的结果是 np.std 的平方,但 np.std 在数值稳定性上更好。手工算方差时如果用"均值减每个元素再平方再求平均"的朴素公式,在大数值数组上会损失精度;NumPy 内部用的是更稳定的两遍算法,你直接用 np.var 就好,不要自己实现。
2.3 median 与 percentile:分位数的插值算法怎么选
np.median 本质上是 50% 分位数,所以它和 np.percentile 是同一个家族。np.percentile 的完整签名是 np.percentile(a, q),其中 q 可以是单个数值,也可以是数组。
data = np.array([10, 20, 30, 40, 50]) print(np.median(data)) # 30.0 print(np.percentile(data, 50)) # 30.0 print(np.percentile(data, [25, 50, 75])) # [20. 30. 40.]分位数的计算有一个隐藏参数 method,它控制插值方式。默认是 linear,意思是当目标分位点落两个数据点之间时,按线性插值取中间值。比如数据长度 10,25% 分位点落在排序后的第 2.25 个位置,linear 方法会在第 2 个和第 3 个值之间按比例取值。
这个细节在日常统计里影响不大,但在机器学习的分位数变换、异常检测阈值设定等场景中,method 的选择会影响最终结果。如果你的数据里有很多重复值,或者样本量很小,不同插值方式得出的分位数可能差异明显。NumPy 提供了 linear、lower、higher、nearest、midpoint 等选项,按需选用即可。
percentile 在做性能监控时特别有用。比如统计接口延迟,均值容易被极端值拉高,但 p50、p95、p99 能真实反映绝大多数用户的体验。我后面会用完整案例演示这个用法。
3. 实战:从一份接口耗时数据到统计摘要
3.1 模拟数据与极端值清洗
光讲参数不落地等于白讲,我用一个常见的业务场景串一遍:分析某服务的接口耗时。假设我们拿到一批延迟数据,单位是毫秒,总量 1000 条,其中混入了一些异常毛刺值。
rng = np.random.default_rng(42) latency = rng.exponential(scale=80, size=1000).round(1)指数分布模拟延迟数据是比较贴近真实的选择,因为接口耗时往往呈现右偏分布:大量请求很快返回,少数请求很慢。先看一眼数据的基本情况:
print(latency.shape) # (1000,) print(np.min(latency), np.max(latency)) print(np.isnan(latency).sum()) # 0,先确认没有缺失值拿到数据的第一个动作永远是体检:检查 NaN、检查 Inf、检查极端值。很多人在这一步偷懒,直接丢给统计函数,结果算出一堆 nan 或者被极端值污染的数字。
清洗阶段,我用一个简单规则:把超过 500 毫秒的延迟当作异常毛刺剔除。这里的 500 是一个业务阈值,实际项目中应该基于业务容忍度来确定,而不是随手拍脑袋。过滤可以用布尔索引直接完成:
valid = latency[latency < 500] print(valid.size) # 过滤后的样本量过滤后样本量如果大幅减少,本身就是一条重要信息,说明系统存在严重的毛刺问题,需要单独分析被过滤掉的那些请求。
3.2 均值、分位数与波动性的综合解读
接下来计算核心统计指标。我通常会一次性输出一组摘要指标,方便横向对比:
summary = { "count": valid.size, "mean": np.mean(valid), "std": np.std(valid, ddof=1), "min": np.min(valid), "p50": np.percentile(valid, 50), "p95": np.percentile(valid, 95), "p99": np.percentile(valid, 99), "max": np.max(valid), }输出结果后,第一件事是对比 mean 和 p50。如果 mean 明显大于 p50,说明分布右偏,少数高延迟请求把均值拉高了。此时如果只向业务方汇报均值,对方会以为系统平均表现还不错,但实际上半数用户感受到的延迟可能是另一个数字。
这类场景我踩过坑:某一次分析中,均值 90 毫秒,p50 只有 55 毫秒,但 p99 高达 320 毫秒。如果只看均值,优化方向会是"所有请求都慢一点";看了分位数才发现,问题集中在长尾请求上,优化缓存策略比盲目扩机器有效得多。
分位数还有一个用途:设定 SLO 阈值。比如约定 p95 不超过 200 毫秒,那么只要算出 p95 并和阈值对比,就能判断当前系统是否达标。这也是 NumPy 统计函数在可观测性领域的典型用法。
3.3 相关性矩阵与多维指标分析
接口延迟往往不是孤立指标,它和 CPU 使用率、内存占用、请求量之间存在关联。假设我们有一个二维数组,每一列是一个指标,用 np.corrcoef 可以一次性得到所有指标两两之间的相关系数矩阵。
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) y = np.array([2.0, 4.0, 6.0, 8.0, 10.0]) z = np.array([5.0, 4.0, 3.0, 2.0, 1.0]) corr = np.corrcoef([x, y, z]) print(corr) # [[ 1. 1. -1.] # [ 1. 1. -1.] # [-1. -1. 1.]]np.corrcoef 返回的是一个矩阵,不是单个数值。对角线永远是 1,因为指标和自身完全正相关。非对角线元素在 -1 到 1 之间,越接近 1 说明正相关越强,越接近 -1 说明负相关越强。
如果要计算协方差矩阵而不是相关系数矩阵,用 np.cov。两者区别在于:相关系数对量纲做了归一化,不随单位变化;协方差的数值受量纲影响,不能直接跨指标比较大小。业务分析中推荐优先看相关系数,因为它可解释性更强。
使用 np.corrcoef 时有一个常见错误:直接把一维数组传进去,结果得到一个 2x2 矩阵而不是单个数值。这是因为 np.corrcoef 把两个一维数组当作两个变量分别计算相关性。想算两个数组的相关系数,正确写法是 np.corrcoef(x, y),然后取结果矩阵的 (0, 1) 位置元素。
4. 高频踩坑与排查技巧:真实项目里验证过的经验
4.1 NaN 污染统计结果:先体检再计算
NaN 污染是我在工作中遇到次数最多的统计错误。典型场景是从数据库或 CSV 读取数据后,某些字段在清洗阶段被赋值为 NaN,然后直接送入 np.mean、np.std,结果所有统计指标全部变成 nan,排查半天才发现源头是一列数据里有几个空值。
最有效的防护手段是建立固定习惯:任何统计计算之前,先跑三行体检代码。
print(np.isnan(data).sum()) # NaN 数量 print(np.isinf(data).sum()) # 无穷值数量 print(data.size) # 样本总量这三行代码花不了 0.1 秒,但能帮你避免 90% 以上的脏数据问题。如果体检发现 NaN 数量不多,可以决定是填充还是删除;如果数量很大,则要回头检查数据采集和清洗流程,而不是简单跳过。
另一个隐蔽的坑是 NaN 的"传染性"。数组里即使只有一个 NaN,np.mean 也会返回 nan,这是设计好的安全机制。但如果你在循环里对每组数据分别求均值,一组数据有 NaN 会导致该组结果为 nan,而其他组正常,最终结果里出现"零星 nan"就会污染下游所有聚合结果。排查思路是:找到第一个出现 nan 的位置,往前回溯数据清洗流程。
4.2 axis 与 shape 的连带错误:用最小样例验证
axis 搞反导致的错误往往不会报异常,它只是安静地给出一个"看起来合理但实际错误"的结果,所以特别危险。我见过最典型的场景:二维数组代表"行是样本、列是特征",应该按列求每个特征的均值,结果同事写了 axis=1,求成了每个样本的均值,后续所有标准化操作全部基于错误均值进行,整个模型的输入数据都是歪的。
解决这个问题的绝招是用最小样例验证。写代码时先用一个 2x3 的固定数组跑一遍,人工心算验证结果方向是否正确,确认后再替换成真实数据。
test_arr = np.array([[1, 2, 3], [4, 5, 6]]) print(np.mean(test_arr, axis=0)) # [2.5 3.5 4.5] print(np.mean(test_arr, axis=1)) # [2. 5. ]axis=0 的结果是 2.5、3.5、4.5,即每一列两个元素的平均值;axis=1 的结果是 2.0、5.0,即每一行三个元素的平均值。心算验证只需几秒钟,却能把方向性错误扼杀在摇篮里。
还有一个小技巧:计算前后对比数组的 shape。统计函数压缩掉指定维度后,结果的 shape 应该等于原 shape 去掉 axis 对应维度后的值。例如 (100, 20) 的数组,axis=1 聚合后应该是 (100,),如果得到的是别的形状,多半是 axis 写错了。
4.3 精度、溢出与大数据量性能
NumPy 的统计函数在绝大多数场景下精度都足够,但有几个边界情况值得注意。
第一个是整数溢出。np.sum 在 uint64 或者很大的 int64 数组上累加时可能溢出,但 np.mean 通常不会,因为它内部会把整数提升为 float64 再计算。如果数组元素量级很大且是整数类型,求和前最好先转成 float64:data.astype(np.float64)。
第二个是精度累积。对一亿个浮点数求和时,朴素循环的误差会累积,NumPy 的 np.sum 内部做了成对求和,精度比 Python 内置 sum 好很多,但如果对精度有极端要求,可以用 np.longdouble 提升中间精度。统计函数的均值基于求和实现,所以同样受益于这个优化。
第三个是性能问题。NumPy 的向量化操作速度远快于 Python 循环,这个大家都知道,但很多人不知道的是,np.mean 在超大数组上会遍历两遍(一遍求和、一遍处理),对于几十 GB 级别的数据,内存和 CPU 开销都很可观。此时可以考虑先用 np.add.reduce 做流式累积,或者用分块计算。不过对于日常的百万级数据,直接调用统计函数是最省心也最快的方式。
我在一个真实项目里测过:一千万个 float64 元素,np.mean 单次调用耗时大约 20 毫秒,np.std 大约 40 毫秒,完全够用。真正拖慢性能的往往不是统计函数本身,而是前面写 Python 循环做数据清洗的过程。所以我的建议是:能用向量化操作绝不用循环,清洗阶段也尽量用 NumPy 的布尔索引和切片。
5. 值得养成的函数习惯:小而美的数据统计组合
5.1 bincount 与 histogram:快速摸清分布形态
np.bincount 的功能是统计非负整数数组中各整数值出现的次数。它的输出是一个数组,下标是原始值,值是出现次数。
labels = np.array([0, 1, 0, 2, 1, 1, 2, 0]) print(np.bincount(labels)) # [3 3 2]bincount 有两个限制:输入必须是非负整数,且输出长度等于最大值的最大值加一。这在统计类别数据、离散标签分布时非常方便,比如统计一个分类任务里各个类别的样本数量。配合 np.unique 的 return_counts 参数,还能获得更灵活的计数结果:
data = np.array([3, 1, 2, 1, 3, 3, 2, 1]) values, counts = np.unique(data, return_counts=True) print(values) # [1 2 3] print(counts) # [3 2 3]np.histogram 则是连续数据的分布统计工具。它把数据划分到若干个区间,统计每个区间内的数据量:
latency = rng.exponential(scale=80, size=1000) counts, edges = np.histogram(latency, bins=10)返回的 edges 是区间的边界,长度比 counts 多一。histogram 的 bins 参数可以传整数也可以传数组,传数组时表示自定义区间边界。做数据分析时,我经常用 histogram 快速观察数据分布形态,再决定是否要做对数变换、是否适合用正态假设。
5.2 clip、unique 与 cumsum:数据清洗与趋势观察
np.clip 的作用是把数组中的元素限制在指定范围内,小于下限的变成下限,大于上限的变成上限。它在数据清洗阶段特别常用:识别出极端值后,与其直接删除破坏样本量,不如用 clip 把极端值拉回到合理边界。
prices = np.array([10, 200, 30, 500, 5]) print(np.clip(prices, 10, 100)) # [ 10 100 30 100 10]我在处理业务指标时经常用 clip 处理明显的录入错误。比如单价不可能低于 1 元,也不可能高于 10000 元,那么直接 np.clip(data, 1, 10000) 就可以把脏数据压回合理区间。当然,clip 只适合"值异常但方向可信"的数据,如果值本身不可信,还是要回到源头排查。
np.cumsum 是前向累积求和,每个位置的输出等于从起点到当前位置的总和。它特别适合观察累计趋势:
daily_visits = np.array([1200, 1350, 980, 1500, 1100]) print(np.cumsum(daily_visits)) # [1200 2550 3530 5030 6130]累计曲线天然适合做增长趋势分析,比如月度销售目标完成进度、用户增长累积曲线。配合 np.diff(相邻元素的差),还能从累计值还原出增量变化,两个函数互为逆运算:np.diff(np.cumsum(x)) 会还原出原始序列(首元素除外)。
5.3 argmax、unravel_index 与 diff:定位异常点的三件套
np.argmax 和 np.argmin 返回数组最大最小值所在的下标。在监控场景中,这个函数的典型用途是定位异常时刻:知道了指标在哪一天达到峰值,就能快速回溯当天发生了什么。
metrics = np.array([10, 12, 9, 25, 11, 8]) peak_idx = np.argmax(metrics) print(peak_idx) # 3,第 4 个时间点达到峰值对于多维数组,np.argmax 返回的是展平后的下标,需要用 np.unravel_index 还原成多维坐标:
m = np.array([[1, 5, 2], [3, 0, 4]]) flat_idx = np.argmax(m) # 1 row, col = np.unravel_index(flat_idx, m.shape) print(row, col) # (0, 1),最大值 5 在第 0 行第 1 列这套组合拳在处理时间序列异常时非常高效。先用 np.diff 计算指标的一阶差分,找到差分绝对值最大的位置,那个位置往往就是突变点。再用 argmax 和 argmin 分别确定突变方向和幅度。整个过程不需要循环,全向量化操作,几行代码就能搞定。
我实际用过的一个场景:监控某服务的每日错误率,先用 np.diff 发现某天错误率环比暴涨 30 个百分点,再用 np.argmax 定位具体日期,最后查变更记录确认是那天的发布脚本有问题。整个定位过程不到五分钟,这套方法后来被固化成了团队的例行监控脚本。
6. 我的使用体会
写了这么多函数和案例,最后聊几句实在的。
NumPy 的统计函数看着简单,但真正用好需要的是"统计直觉"而不是"函数记忆"。你背下所有参数,不如理解 axis 是压路机方向、ddof 是总体与样本的分界线、NaN 会传染这三个核心概念。我见过太多人卡在某个函数的某个参数上,其实退一步想清楚自己在算什么,答案自己就出来了。
另外一个体会是:统计函数要搭配着用,不要孤立地依赖某一个。均值配分位数、标准差配直方图、相关性配散点图,组合起来才能还原数据的真实面貌。单看 mean 会被极端值欺骗,单看 std 无法知道分布形态,只有组合分析才能避免得出片面结论。
如果你刚接触 NumPy,我的建议是不要急着啃完整文档,先把这篇文章里的函数用一遍,拿手头真实数据跑一跑,把 axis 和 ddof 的差异亲手验证一次。这些函数熟练之后,你会发现大部分数据统计工作其实就是在它们之间组合和取舍。后续如果再遇到统计相关的需求,优先想想能不能用这些基础函数拼出来,实在不行再去看更高级的工具包——很多复杂方案,最后拆解开都是这几个老熟人在干活。