在真实的数据处理工作里拿到一批数据后的第一个动作往往不是画图也不是急着建模而是先把最基础的描述性统计量算出来。平均数、中位数、标准差、极差这四个指标几乎是所有数据项目的共同起点。它们能帮你快速判断数据有没有明显异常、分布大概长什么样、后续应该走哪条分析路线。这篇内容就是围绕这四个指标的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默认是总体标准差需要设置ddof1才能得到样本标准差。这两个库的默认方向刚好相反是最容易踩的坑。我用一张表放出来建议你收藏目标statistics模块numpy样本标准差statistics.stdev(data)np.std(data, ddof1)总体标准差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, ddof1) 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, ddof0): 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, ddof1), 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( meanmean, medianmedian, stdstd, minmin, maxmax ) result[range] result[max] - result[min] print(result)这个agg写法的好处是语法简洁而且一次性算完所有指标不用分开写好几遍groupby。pandas的agg也支持自定义lambda比如你想直接算出极差可以这样result df.groupby(category)[amount].agg( meanmean, rangelambda x: x.max() - x.min() )但lambda在groupby里每次都会调用数据量大时性能会受影响。我的建议是先用内置的min和max算出来再直接做列相减性能和可读性都要更好。4. 数据清洗与边界条件实战中的隐形关卡4.1 缺失值静默出错的最大隐患我处理过的真实数据里几乎没有哪份是干干净净的。最常见的问题就是缺失值——可能是用户没填的字段可能是抓取失败的记录也可能是日期格式问题导致读进来变成空值。这些缺失值进入计算流程后行为非常隐蔽。在statistics模块里如果列表中有Nonemean会直接报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.stdevSTDEV.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差异巨大这是一个非常强的信号数据不是正态分布或者有离群值。这种时候先别急着算后续指标停下来看看数据分布往往能避免后面一系列错误结论。我自己做数据项目时描述性统计这关永远不会跳过。它看起来简单但恰恰是这种简单能在早期暴露最多的问题。把平均数、中位数、标准差、极差这些基本功练扎实你对数据的敏感性会明显提升后面不管做可视化还是建模心里都会稳很多。这套流程建议你在自己的数据上完整跑一遍遇到跟直觉不符的结果多问一句为什么收获会比看十篇文章都大。
阅读完成 · 觉得有帮助?