☰
Python描述性统计实战:平均数、中位数、标准差与极差详解
2026/10/23 4:00:32 网站建设 项目流程

在真实的数据处理工作里,拿到一批数据后的第一个动作往往不是画图,也不是急着建模,而是先把最基础的描述性统计量算出来。平均数、中位数、标准差、极差这四个指标,几乎是所有数据项目的共同起点。它们能帮你快速判断数据有没有明显异常、分布大概长什么样、后续应该走哪条分析路线。这篇内容就是围绕这四个指标的Python实现展开,从统计含义到代码写法,从标准库到numpy,再到实际处理中容易踩的坑,一次讲透。适合刚入门Python数据分析的读者,也适合经常处理表格数据、想快速核对数据状态的开发者。看完之后,你至少能搞明白:这四个数各自回答什么问题、什么时候该用哪一个、代码怎么写才不会在空数据、缺失值这些边界条件下翻车。

1. 为什么先做描述性统计:四个指标的分工

1.1 数据摸底是项目的第一道工序

我见过不少刚开始做数据分析的人,拿到一份几千行的表格,第一反应就是到处画图,或者直接上聚类、回归这些"高级货"。结果往往是图是画了不少,但对数据本身到底是什么状态,心里根本没底。这种思路很容易翻车——因为后续所有分析都建立在"数据质量过关"这个前提上,而质量好不好,恰恰得靠基础统计量来验证。

换句话说,平均数、中位数这些简单的计算,不只是算几个数值交差,而是整个项目的地基。举个例子:某销售团队上个月的业绩数据,如果平均数明显高于中位数,说明头部几个人把整体水平拉高了,大多数人可能还在均值以下。这时候你去做业绩考核、定下月目标,就必须知道这个分布特征,否则按平均数定目标,大部分人是根本够不着的。

这个环节在所有行业都通用。电商看客单价、工厂看良品率、运营看用户时长,第一步都是先算这几个数。它们不负责回答"为什么",但能非常高效地回答"是什么样",而"是什么样"往往是所有深入分析的前提。

1.2 四个指标回答四个不同的问题

这四个指标不是重复劳动,它们的视角完全不一样:

平均数回答的是"整体水平大概是多少"。它的计算最简单,把所有值加起来除以个数就行,但代价是对极端值非常敏感。一个程序员小组的月薪,普通成员两万,来了个年薪千万的专家,平均数立刻被拉得没法看。

中位数回答的是"排在中间的那个人处在什么水平"。它把数据排序后取中间值,不管最大值多大、最小值多小,只要不跨过中间位置,就影响不到它。正因为这种抗干扰特性,它特别适合收入、房价这类天然带有长尾分布的数据。

标准差回答的是"数据之间的波动有多大"。它衡量每个值偏离平均数的平均程度。两组数据的平均数可能完全一样,但一组是十、十、十,另一组是一、十、十九,标准差会清晰地告诉你:第二组的数据波动远大于第一组。

极差回答的是"最大值和最小值之间的跨度有多大"。它用最大值减最小值,一次减法就得到结果,是最直观的波动度量。缺点也很明显——只看了头尾两个点,中间怎么波动它完全不管。

这四者合在一起,基本能在一分钟之内给你一份数据的"体检报告"。

1.3 工具选型:标准库、numpy、pandas怎么选

Python里实现这四个指标,有不止一条路。最基础的是内置的statistics模块,它的优势是零依赖,任何环境装了Python就能跑,不需要额外安装任何包。数据量不大、只想快速验证一下的场景,用它最方便。

数据量一上来,或者需要跟其他数值运算配合,numpy就是更好的选择。numpy的数组运算走的是C底层,几千几万个元素的均值、标准差计算,速度比纯Python循环快几个数量级。我自己实测过,同样是算一百万个数,用纯Python列表加sum和len,耗时秒级起步;换numpy,毫秒级就出结果。

如果数据是表格形态,尤其是带着分组信息的场景,pandas的agg方法可以直接按组聚合,一行代码同时算出多个组的平均数、中位数、标准差。这在实际项目里太常用了——"每个城市的人均消费""每个类目的销量波动",都是groupby加agg的典型场景。

选型的大原则就一条:能少装依赖就少装,但数据规模上来之后,别跟性能较劲。小数据用statistics,大数据用numpy,表格分组用pandas。下面我会把三条路线都演示一遍,你完全可以根据自己的场景选。

2. 四个统计量的底层原理与使用边界

2.1 平均数:最直观,但最怕离群值

平均数的公式大家都熟,累计求和除以个数。但在Python里实现的时候,有几个细节特别容易忽略。

第一是数据类型的坑。如果列表里的元素是字符串形式的数字,比如["25", "30", "28"],直接sum会报TypeError,必须先转成int或float。这个错误在接手别人导出的Excel数据时特别常见,因为Excel里的数字列有时候会被读成文本格式。

第二是空数据的问题。对一个空列表求平均数,Python会直接抛异常,因为除数为零。这个场景在真实项目里不是"会不会发生",而是"迟早会发生"——上游接口返回空数组、数据库查询结果为空、筛选条件把数据全过滤掉,都可能触发。所以规范的写法是先判断序列长度。

第三是outlier的影响。平均数用的是每一个数据点,一个极端值就能把它往一个方向拽。比如一组房屋面积数据,百分之九十九都在80到120平米之间,混进一个两千平米的豪宅,平均值立刻被拉到一百开外,完全不能代表普通房源的状况。这种情况下,平均数这个指标本身没有错,是数据分布让它"失真"了。

所以我的习惯是:计算平均数之前,先顺手看看最大值、最小值,再用后面的标准差辅助判断数据离散程度。如果发现平均数和直觉差得离谱,优先怀疑数据里有脏值或者极端离群点。

2.2 中位数:抗干扰的稳健代表

中位数的定义很好理解:将数据从小到大排序后取中间位置的值。如果个数是奇数,直接取正中间那个;如果是偶数,取中间两个数的平均值。

实现逻辑不复杂,但有几个使用上的重点。

首先要注意,中位数对离群值的抵抗不是无条件的。它只关心排序后的位置,所以无论最大值被改成多离谱的大数,只要它依然排在最后一位,中位数就完全不受影响。这是它和平均数最大的区别,也正是它在收入、房价这类数据上被广泛使用的原因。

第二点是中位数和平均数的配合使用。当一个数据集的中位数显著小于平均数时,说明数据分布是右偏的,也就是少数大值把平均数拉高了;反过来,中位数大于平均数,说明有少数极小值把平均数拉低了。这个对比本身就是一个非常有价值的判断信息。我在做业务报表时,经常会把这两个数并排展示,让业务方直观地看到"平均水平"和"典型水平"的差异。

第三点是Python标准库的一个细节。statistics模块里除了median,还提供了median_low和median_high。偶数个数时,median返回中间两数的平均数,median_low返回较小的那个,median_high返回较大的那个。绝大多数场景用median就够了,但在某些要求结果必须是原始数据中某个值的场景下,median_low或median_high会更合适。

2.3 标准差:波动的量化标尺

标准差描述的是数据的离散程度,计算路径是:先求每个数据和平均数的差,把差平方后求和,除以个数(或个数减一),得到方差,再开平方。

你可能要问:为什么不直接算差的绝对值?因为平方赋予了离群值更大的权重,让标准差对"极端偏差"更敏感,而且平方之后数学性质好,在后续推断统计中可以跟正态分布、显著性检验这些工具无缝衔接。

标准差最值得讲清楚的是分母的n和n-1之争。n,也就是除以数据个数,得到的是总体标准差;n-1,得到的是样本标准差。当你手头的数据已经是全部个体时,用总体标准差,公式里除以n;当你只是抽取了一个样本来估计总体的波动时,用样本标准差,公式里除以n-1。这个n-1叫贝塞尔修正,本质上是给样本带来的"低估偏差"做一个补偿。

在Python里,statistics模块的stdev默认是样本标准差,pstdev是总体标准差。numpy里则是np.std默认是总体标准差,需要设置ddof=1才能得到样本标准差。这两个库的默认方向刚好相反,是最容易踩的坑。我用一张表放出来,建议你收藏:

目标statistics模块numpy
样本标准差statistics.stdev(data)np.std(data, ddof=1)
总体标准差statistics.pstdev(data)np.std(data)

实际业务里,绝大多数数据分析场景处理的是样本数据,因为我们很难拿到全量总体,所以"默认到底该用哪个"这个问题,最好根据业务上下文明确下来,不要随手写一个默认值就交差。

2.4 极差:简单直接但信息量有限

极差是所有波动类指标里最"省事"的一个:最大值减最小值。它用一行代码就能算出来,在快速检查数据范围、发现明显异常值时非常好用。

比如一条生产线的产品重量数据,正常情况下极差在5克以内,某天突然算出来极差是50克,那大概率是混入了一个外星数据点,值得立刻查一下。这种"先看极差,再决定要不要深挖"的思路,效率非常高。

但极差的两大缺陷必须心里有数。第一,它只依赖两个极值,中间九千九百九十八个数据分布得多散、多集中,它一概不知。两组数据可能极差完全一样,但只要最大最小值位置相同,极差就给出相同的结果,哪怕一组数据是均匀分布、另一组数据是俩极端加一堆中间值。第二,它天然被离群值绑架,一个脏数据就能把极差搞得巨大。

所以在实际项目中,我从来不会单看极差做判断,而是把它当作一个"早期预警器"——极差异常,说明数据里有值得关注的东西;极差正常,也只能说明整体范围没有离谱,波动细节还得靠标准差来刻画。

3. 动手实现:从标准库到numpy的完整流程

3.1 用statistics模块完成基础计算

先看标准库的写法。这是最干净、最少依赖的方案,适合脚本里快速算一算,或者写单元测试时做断言。

import statistics data = [25, 30, 28, 35, 22, 31, 27, 29] mean = statistics.mean(data) median = statistics.median(data) std_sample = statistics.stdev(data) # 样本标准差 std_pop = statistics.pstdev(data) # 总体标准差 data_range = max(data) - min(data) print(f"平均: {mean:.2f}") print(f"中位数: {median}") print(f"样本标准差: {std_sample:.4f}") print(f"总体标准差: {std_pop:.4f}") print(f"极差: {data_range}")

这个代码的优点是直白,每一行都在说人话。但statistics模块有一个需要注意的点:它对输入类型有要求。列表里的元素必须是数值类型,如果混入None或者字符串数字,就会报错。而且空序列直接调用mean会抛statistics.StatisticsError,我在实际项目里就因为这个被测试用例抓过好几次。

所以如果你要把它封装成函数,我建议加一层防御:

import statistics def calc_desc_stats(data): if not data: return {"mean": None, "median": None, "std": None, "range": None} numeric_data = [x for x in data if isinstance(x, (int, float))] if not numeric_data: return {"mean": None, "median": None, "std": None, "range": None} return { "mean": statistics.mean(numeric_data), "median": statistics.median(numeric_data), "std": statistics.stdev(numeric_data) if len(numeric_data) > 1 else 0.0, "range": max(numeric_data) - min(numeric_data), }

这里面有个细节:当数据只有一个元素时,样本标准差的分母是n-1,等于0,会导致除以零的崩溃。所以我用了"个数大于1才算标准差,否则返回0"的处理。这个边界条件写文档的人不会特别提醒你,但在真实数据里非常容易触发。

3.2 用numpy处理更大规模的数据

数据量超过几万条,或者你后面还要做归一化、矩阵运算这些操作,换numpy就是自然的选择。

import numpy as np arr = np.array([25, 30, 28, 35, 22, 31, 27, 29]) mean = np.mean(arr) median = np.median(arr) std_sample = np.std(arr, ddof=1) std_population = np.std(arr) data_range = np.max(arr) - np.min(arr) print(f"平均: {mean:.2f}") print(f"中位数: {median}") print(f"样本标准差: {std_sample:.4f}") print(f"总体标准差: {std_population:.4f}") print(f"极差: {data_range}")

numpy的优势不只是性能。它的数组天然支持向量化计算,你不需要写循环就能对整个数组做条件过滤、数学变换。比如你想剔除极端值之后再算平均数,一行就能搞定:

filtered = arr[(arr > np.percentile(arr, 5)) & (arr < np.percentile(arr, 95))] print(np.mean(filtered))

这个p5到p95的截断思路,比直接用一个魔法阈值靠谱得多,因为它基于数据自身分布来界定"正常范围"。

另外提醒一个numpy特有的坑:如果数组里存在np.nan(缺失值),np.mean的结果会是nan,而且不会报任何警告。很多人在这一步就会被坑,计算结果里面混进nan,还得回头排查半天。建议在使用numpy计算描述性统计量之前,用np.isnan检查一遍,或者直接用np.nanmean、np.nanmedian这些函数。下面的代码片段可以作为固定套路:

clean_arr = arr[~np.isnan(arr)]

3.3 手写实现,彻底理解公式本质

虽然现成函数很方便,但我一直建议初学者至少手写一遍这四个指标。不是为了在生产环境里用,而是为了真正理解代码背后发生了什么。手写过程会逼着你把排序、求和、开平方这些细节都过一遍。

def my_mean(data): return sum(data) / len(data) def my_median(data): sorted_data = sorted(data) n = len(sorted_data) mid = n // 2 if n % 2 == 1: return sorted_data[mid] return (sorted_data[mid - 1] + sorted_data[mid]) / 2 def my_std(data, ddof=0): n = len(data) mean_val = sum(data) / n variance = sum((x - mean_val) ** 2 for x in data) / (n - ddof) return variance ** 0.5 def my_range(data): return max(data) - min(data) data = [25, 30, 28, 35, 22, 31, 27, 29] print(my_mean(data), my_median(data), my_std(data, ddof=1), my_range(data))

这套手写代码的核心价值在于:一旦你能用自己的代码算出跟statistics模块一致的结果,你对这些指标的理解就从"背公式"升级成了"懂机制"。特别是my_std里的ddof参数,亲手把分母从n换成n-1,对比结果的变化,你会对"为什么样本标准差要除n-1"有一个切身的体感。

顺便说一句,my_median要记得先排序。很多人第一次写中位数,忘了排序这步,直接取中间下标,结果自然不对。这也是为什么我建议手写一遍——排序这个动作虽然不起眼,但它是中位数公式成立的前提,手写时你才会真正注意到它。

3.4 一个完整的表格分组处理流程

实际项目里,数据很少是孤零零一个列表,更多是带着分组标签的表格。下面这个场景很典型:一个电商运营同学拿来一张订单明细表,想知道不同类目的客单价水平、波动情况,以及有没有异常偏大的订单。

import pandas as pd df = pd.DataFrame({ "category": ["数码", "数码", "数码", "家居", "家居", "家居", "家居", "服饰", "服饰", "服饰"], "amount": [1999, 258, 3499, 89, 120, 150, 76, 59, 89, 45] }) result = df.groupby("category")["amount"].agg( mean="mean", median="median", std="std", min="min", max="max" ) result["range"] = result["max"] - result["min"] print(result)

这个agg写法的好处是语法简洁,而且一次性算完所有指标,不用分开写好几遍groupby。pandas的agg也支持自定义lambda,比如你想直接算出极差,可以这样:

result = df.groupby("category")["amount"].agg( mean="mean", range=lambda x: x.max() - x.min() )

但lambda在groupby里每次都会调用,数据量大时性能会受影响。我的建议是:先用内置的min和max算出来,再直接做列相减,性能和可读性都要更好。

4. 数据清洗与边界条件:实战中的隐形关卡

4.1 缺失值:静默出错的最大隐患

我处理过的真实数据里,几乎没有哪份是干干净净的。最常见的问题就是缺失值——可能是用户没填的字段,可能是抓取失败的记录,也可能是日期格式问题导致读进来变成空值。这些缺失值进入计算流程后,行为非常隐蔽。

在statistics模块里,如果列表中有None,mean会直接报TypeError;如果列表元素是数字但有个别值确实缺失,你可能用None占位,同样会报错。而在numpy的世界里,缺失值通常以np.nan形式存在,np.mean返回nan时连个异常都不抛。这种"静默失败"最可怕,因为结果看起来像是一个正常数值,实际上是nan,一旦你把它写进报表或者喂给下游模型,问题会在很远处爆出来。

所以我的固定习惯是:任何统计计算之前,先检查缺失值占比。占比小,直接删除;占比大,就要考虑填充。填充方式也要区分场景,数值型数据最粗浅的填充是用平均数或中位数,但要注意填充本身会引入偏差。稍微讲究一点的做法是用前后值填充、插值,或者建立模型预测缺失值——不过这些属于进阶话题,日常快速分析里,大多数人选前一种也就够用了。

import numpy as np data = np.array([25, 30, np.nan, 35, 22, 31, 27, 29]) # 检查缺失 print(np.isnan(data).sum()) # 1 print(len(data) - np.isnan(data).sum()) # 有效值个数 # 删除缺失后计算 clean_data = data[~np.isnan(data)] print(np.mean(clean_data))

这个检查步骤花不了多少时间,但能省掉后面一大把排查的功夫。

4.2 空序列与"只有一个数"的尴尬

空序列的问题前面提过:除以零会崩溃。但"只有一个数"这个边界稍微隐蔽一点。当len(data)等于1时,mean能算,median能算,range能算——极差为0。偏偏标准差在这种场景下会出问题,因为样本标准差的公式里分母是n-1,也就是0除以0,直接抛ZeroDivisionError。

在真实业务里,这种情况远比你想象的多。比如按城市分组后,有的城市可能只有一条销售记录;按日期分组后,某天可能只有一单。如果你在agg里用了"std"聚合,这些组就会报错。pandas的agg遇到这种情况,默认会给这个组的std填充NaN,这倒是不报错,但你要知道这个NaN的含义是"数据不足,无法计算",而不是"波动为零"。这两者的业务解读完全不同:"无法计算"意味着你根本不该拿这个组跟别的组比波动;"波动为零"才说明所有值完全相同。

所以我的建议是:分组聚合后,检查一下结果里的NaN,单独标注"样本量不足",避免在下游被当成正常数值使用。

4.3 样本与总体的选择:不只是数学问题

样本标准差和总体标准差的区别,表面上是公式里分母n和n-1的区别,本质上是你对数据来源的认知。我在实际项目里发现,很多开发者不是不会用这两个函数,而是没想清楚自己的数据到底算样本还是总体。

举一个具体的例子:假设你要评估某条生产线当天的产品重量波动。如果"当天生产的全部产品"就是你的数据,那它就是总体,应该用总体标准差。但如果你只抽检了其中一小部分,想用这批抽检的结果去推断整条生产线的稳定性,那它是样本,必须用样本标准差。

这个问题在业务上是有实际后果的。用样本标准差去近似总体标准差,由于n-1的修正,得到的估计会更保守、更不低估波动,这在做质量控制、安全阈值设定时是有利的。反过来该用样本时误用了总体标准差,波动会被低估,等于把风险藏起来了。

在代码层面,我的建议是培养一个"手动指定"的习惯:每次计算标准差的时候,在注释里写清楚这是样本还是总体。哪怕只是写着"这份数据只是某周的部分记录,算样本标准差",也能避免三个月后再看这段代码时一脸懵。

# 抽检样本,使用样本标准差 std = statistics.stdev(sample_data) # 全量数据,使用总体标准差 std_pop = statistics.pstdev(all_data)

5. 常见问题速查与踩坑实录

5.1 高频报错与解决方案

我把这些年做统计计算时踩过的高频报错整理成一张速查表,方便你对着排查:

错误现象可能原因解决办法
TypeError: can only concatenate str列表里混入了字符串数字用float(x)统一转换
statistics.StatisticsError: mean requires at least one data point传入空列表计算前判断len(data)
ZeroDivisionError只有一个样本却算样本标准差当len(data)>1时才计算
结果为nan且无警告numpy数组里有np.nan先删缺失或用np.nanmean
结果跟Excel对不上Excel用的是总体标准差,statistics默认用样本标准差确认计算口径

最后一条其实很常见。很多人拿Python算出来的标准差跟Excel的STDEV.S(样本)或STDEV.P(总体)对比,发现数值不一致,第一反应是"代码有bug"。其实不是,纯属两个工具默认口径不同。Excel的STDEV.S对应statistics.stdev,STDEV.P对应statistics.pstdev,知道了这层关系,对账就对得上了。

5.2 离群值该不该剔除?先看业务再动手

很多人一看到离群值就想着删掉,我劝你先按住这个冲动。离群值的处理,本质上是业务问题,不是数学问题。

举两个截然不同的场景。第一个是传感器异常值检测:一个温度传感器突然跳出一个几万度的读数,这明显是设备故障,必须剔除,否则平均数会被污染。第二个是销售大客户数据:某客户单笔采购金额是普通客户的几十倍,看起来像离群值,但如果这个客户是真实存在的,你把他删掉,平均数就低估了真实的单均价值。

所以正确的做法是:先用IQR或者Z-score的办法把离群值标记出来,然后交给业务方确认这些值合不合理。标记的方法很简单:

def find_outliers_iqr(data): q1 = np.percentile(data, 25) q3 = np.percentile(data, 75) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr return data[(data < lower) | (data > upper)]

这比裸眼看数据靠谱多了。IQR方法的好处是不假设数据分布形态,对偏态分布也适用。但注意,它在小样本下可能会把很多本不该算异常的值标出来,所以IQR结果只能当线索,不能当结论。

5.3 一些让计算更稳的小技巧

最后分享几个日常用得上的习惯。

第一个是用百分位数辅助判断分布。平均数、中位数之外,我通常还会算一下p10和p90。这样即使不看直方图,也能在脑子里大概勾勒出数据的分布范围。两个数据集的平均数可能一样,p90差别很大,说明一个是大多数人在均值附近,另一个是少数人撑起了均值。

p10, p90 = np.percentile(data, [10, 90])

第二个是在输出结果时统一保留位数。平均值、标准差这些数值,保留到小数点后两位就够直观了,但中间计算过程千万别轻易四舍五入。我见过有人每一步都round,最后累积误差大到结果错得离谱,这个坑一定要避开。中间过程用完整精度,只在最终展示时格式化。

第三个是多利用"交叉验证"来发现数据问题。如果发现mean和median差异巨大,这是一个非常强的信号:数据不是正态分布,或者有离群值。这种时候先别急着算后续指标,停下来看看数据分布,往往能避免后面一系列错误结论。

我自己做数据项目时,描述性统计这关永远不会跳过。它看起来简单,但恰恰是这种"简单",能在早期暴露最多的问题。把平均数、中位数、标准差、极差这些基本功练扎实,你对数据的敏感性会明显提升,后面不管做可视化还是建模,心里都会稳很多。这套流程建议你在自己的数据上完整跑一遍,遇到跟直觉不符的结果,多问一句为什么,收获会比看十篇文章都大。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询