做数据分析这些年,我有个很深的体会:拿到一份数据,别急着上模型、跑回归,先老老实实做一次“摸底体检”比什么都重要。而这体检里最基础、也最容易被忽视的三件套,就是五数概括、盒图和异常值检测。很多新手一上来就盯着均值、标准差不放,结果被几个极端值带偏了方向,后面所有分析都白做。这篇文章我用 Pandas 从头到尾把这三件事串一遍,讲清楚每个数字背后的业务含义、IQR 检测异常的完整原理,再配合一段真实销售数据的实战,帮你一次性打通“描述分布—可视化—发现问题”这条数据分析的入门主线。无论你是刚学 Python、准备转行数据岗,还是已经在用 Excel 做报表想升级一下,这篇都值得读完。
1. 先给数据做个体检:五数概括到底在说什么
1.1 五个数字分别回答什么问题
五数概括(Five-Number Summary)就是用五个关键数字描述一组数据的分布形态,分别是最小值(Min)、第一四分位数(Q1)、中位数(Median)、第三四分位数(Q3)和最大值(Max)。
我更喜欢把这五个数理解成“数据面试官”依次要问的五个问题:这组数据最低到多少?25% 的数据线划在哪?最中间的水平是多少?75% 的数据线又划在哪?最高冲到多少?答案组合在一起,基本就能在脑海里勾勒出这组数据的“骨架”。
举个例子,假设你在分析某款白酒在全国各地的月销售额,均值是 80 万,听着还不错。但如果 Q1 只有 30 万、Q3 已经到 120 万,而中位数只有 55 万,那说明大部分区域其实卖得并不好,是少数几个头部市场把均值拉高的。这种情况下“均值 80 万”完全不能代表典型水平,中位数 55 万才是更有业务参考价值的数据。
还有一个容易忽略的点:五数概括能让你迅速判断数据是不是“偏”的。中位数靠近 Q1 而远离 Q3,说明数据右偏,也就是存在一些特别大的极端值;反过来就是左偏。这一步判断,直接决定了你后续是用均值还是中位数做基准,也决定要不要对异常值动手。
1.2 四分位数的计算逻辑与 Pandas 细节
四分位数的计算看起来简单,其实里面有个小坑:不同软件、不同方法算出来的 Q1、Q3 可能不一样。Pandas 的quantile()方法默认用的是线性插值,这也是统计学里最通用的方式。
用一个最简单的小数组来演示。假设有 5 个数:
import pandas as pd s = pd.Series([1, 3, 5, 7, 9]) print(s.quantile(0.25)) # 3.0 print(s.quantile(0.5)) # 5.0 print(s.quantile(0.75)) # 7.0这个结果看起来“正好落在数据点上”,是因为 5 个数据配合 0.25 的间隔,位置刚好是整数。换成 6 个数据情况就变了:
s2 = pd.Series([1, 3, 5, 7, 9, 11]) print(s2.quantile(0.25)) # 3.50.25 分位点在排序后第 0 位和第 1 位之间 25% 的位置,也就是 1 和 3 中间取 25%,得到 1 + (3-1)×0.25 = 1.5?等等,这里就不细究插值公式了,实际结果取决于 NumPy 的默认线性插值方式。我的建议是:不要手动跟算分位数较劲,只要统一用 Pandas/NumPy 的口径,在团队内保持一致就足够了。真正要记住的是,Q1 意味着“有 25% 的数据比它小”,Q3 意味着“有 75% 的数据比它小”,这个业务解读永远不会变。
实际工作中,我们用df.describe()就能一次性把五数概括连同均值、标准差都打出来:
df = pd.DataFrame({"销售额": [12, 15, 14, 18, 120, 16, 13, 17, 500]}) print(df["销售额"].describe())你会看到 count、mean、std、min、25%、50%、75%、max 一排输出。不过要注意,describe()默认对数值列输出的是 25% 和 75%,对应的正是 Q1 和 Q3。很多人看完这行输出就走过去了,其实里面藏着大量信息:如果 max 和 75% 之间的距离远大于 Q1 到 min 的距离,基本可以断定有右偏的大尾巴。
2. 盒图不是装饰画:怎么读、怎么画、怎么对比
2.1 盒图的每个部件都有明确含义
盒图(Box Plot,也叫箱线图)是把五数概括画成一张图的可视化工具。很多人第一次看到盒图,觉得就是“一个盒子加两根须须”,没什么了不起。但你一旦学会正确读它,它的信息密度比柱状图、折线图高得多。
盒子的下边是 Q1,上边是 Q3,盒子高度就是四分位距(IQR),反映的是中间 50% 数据的波动范围。盒子中间那条线是中位数。盒子越窄,说明大部分数据挤在一起,稳定性好;盒子越宽,说明数据分散,波动大。
盒子外面那两根“须”(也叫 whiskers)并不是 min 和 max,而是在正常范围内的最远数据点。它的边界通常取 Q1 - 1.5×IQR 和 Q3 + 1.5×IQR。超出这个范围的点,会被单独画成圆点或菱形,这些就是统计意义上的异常值候选者。
我刚入门时一直以为须的端点就是最大值最小值,后来才发现这俩概念不一样。如果一组数据没有异常值,那须的端点恰好就是 min 和 max;一旦有异常值,须的端点就会“缩回来”,异常点单独飘在外面。搞清楚这一点,看盒图时才不会误读。
2.2 并排盒图:组间对比的利器
盒图最大的优势不是画一组数据,而是把多组数据并排放在一起对比。单看一组的均值和标准差,你很难感受到分布全貌;但三五个盒图一字排开,中位数差异、离散程度差异、异常值分布一目了然。
比如分析三个大区的销售数据,华北、华东、华南各画一个盒图。如果华东的盒子明显更高且中位数偏上,说明这个区域整体销售水平高且内部差距大;如果华南的盒子很矮,说明大家卖得都差不多,竞争格局稳定;如果华北出现两个孤零零的异常点飘在盒子上面,那这两个点很可能就是某个爆款活动或者某个大经销商带来的,值得单独查。
在 Python 里画并排盒图有很多方式,Pandas 自带的plot.box()最简单:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文乱码 plt.rcParams["axes.unicode_minus"] = False df[["华北", "华东", "华南"]].plot.box() plt.title("各大区销售额分布对比") plt.show()如果你用 Seaborn,画起来更灵活,可以直接按某个分组列拆盒图:
import seaborn as sns sns.boxplot(x="大区", y="销售额", data=df) plt.show()Seaborn 的写法更适合数据已经整理成长表(一行一条记录)的情况。说实话,真实项目里数据大多是长表,所以我用sns.boxplot的频率更高。
2.3 画盒图时最容易翻车的几个细节
盒图本身不难画,但细节上翻车的人真不少。第一个就是中文乱码。Matplotlib 默认字体不支持中文,图上一片方框,这个不提前设置好,后面每张图都得返工。上面代码里那两行rcParams设置建议放进你的绘图工具函数里,全局生效。
第二个是数据没排序不影响分位数,但会影响你看盒图时的直觉。盒图会自动按数值排列位置,和原始数据顺序无关,因此你不用担心导入的数据乱序。
第三个是盒图会把异常值“藏”在须外,这反而是它的优点。但如果你用plot.box()时没有单独看数据点,可能忽略了异常值的数量。建议画图归画图,数据层面还是要把异常值的具体值打出来,一张图加一张明细表配合看。
还有一个经验:如果样本量太少,比如一组只有 5 条数据,盒图会显得很“秃”,中位数、Q1、Q3 可能跟某些数据点重合,这时候盒图参考价值有限,别硬解读。
3. 异常值检测:IQR法的原理与 Pandas 落地
3.1 异常值不等于错误值:先分清业务含义
在动手检测之前,必须先明确一件事:统计上的异常值和业务上的坏数据不是一回事。
统计上的异常值,是“偏离主体分布太远”的数据点,它可能是真实发生的(比如某天促销带来销售额暴涨 10 倍),也可能是数据录入错误(比如把 15 万录成 150 万)。这两类必须区分对待:前者要保留并单独分析,后者要修正或剔除。
我在实际项目里吃过亏:一口气把检测出的异常值全删了,结果删掉的恰恰是最有价值的爆款销售记录,后面的销售预测模型完全偏离实际。从那以后,我给自己定了一条铁律:检测异常值只是第一步,人工确认异常值背后的业务原因才是关键。
3.2 IQR 检测法的完整计算过程
IQR 法(四分位距法)是目前最通用的异常值检测方式,核心思路是:先找出数据的 Q1 和 Q3,算出 IQR = Q3 - Q1,然后定义正常区间的上下限:
下限 = Q1 - 1.5 × IQR 上限 = Q3 + 1.5 × IQR所有小于下限或大于上限的数据点,统称为异常值。这个“1.5”是一个经验常数,来自统计学传统,适用面很广。如果你的数据分布特别复杂,可以把系数调成 3,得到的是“极端异常值”的判断标准;也可以改用百分位数法(比如把 1% 和 99% 以外的都当异常),但实际工作中 IQR 法因简单、鲁棒、无需假设分布形态而最常用。
这里有个关键优势:IQR 法用的是中位数和四分位数,不受极端值本身影响。对比一下,如果你用“均值 ± 3σ”来检测,数据本身包含一个 1000 倍的大异常值时,均值和标准差都会被带偏,反而检测不出这个异常值——这种“掩蔽效应”在真实数据里非常常见。IQR 法就没有这个问题,这也是它成为入门首选的根本原因。
3.3 Pandas 实现:从计算到标记的完整代码
下面这段代码是我最常用的标准写法,可以直接用到自己的项目里。假设销售额数据存在df["销售额"]列:
import pandas as pd import numpy as np # 1. 计算四分位数和 IQR Q1 = df["销售额"].quantile(0.25) Q3 = df["销售额"].quantile(0.75) IQR = Q3 - Q1 # 2. 计算正常区间 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 3. 用布尔索引筛选异常值 outliers = df[(df["销售额"] < lower_bound) | (df["销售额"] > upper_bound)] # 4. 在原表上打标记,方便后续追溯 df["是否异常"] = np.where( (df["销售额"] < lower_bound) | (df["销售额"] > upper_bound), "异常", "正常" ) print(f"正常区间: [{lower_bound:.2f}, {upper_bound:.2f}]") print(f"异常值数量: {len(outliers)}") print(outliers)这里我特别推荐第四步:不要只筛出异常值,而是给原始数据加一列标记。因为一旦你后续要复现分析、跟业务对口径,这列标记能帮你随时追溯哪些数据被认定为异常、为什么被认定为异常。分析工作最怕的就是“当时处理了,但处理过程没留痕”。
还有一种更结构化的做法:把检测逻辑封装成函数,方便对多个字段批量复用。
def detect_outliers_iqr(series, k=1.5): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower = Q1 - k * IQR upper = Q3 + k * IQR return (series < lower) | (series > upper) df["销售额_异常标记"] = detect_outliers_iqr(df["销售额"])这个函数接收一个 Series,返回一个布尔 Series,True 就是异常。用k参数控制严格程度,想宽松一点就调成 2 或 3,想收紧就保持 1.5。每次检测都在函数里留下参数记录,别人看到你的代码就知道你是用什么标准检测的。
4. 完整实战:一份销售数据的“全流程体检”
4.1 构造数据与初步观察
我在这里构造一份模拟的“白酒全国各区域月销售额”数据,方便演示完整流程,同时契合很多同学正在做的销售分析场景:
import pandas as pd import numpy as np np.random.seed(42) data = { "区域": ["华北"] * 20 + ["华东"] * 20 + ["华南"] * 20, "销售额": ( list(np.random.normal(50, 8, 20)) + list(np.random.normal(80, 12, 20)) + list(np.random.normal(60, 5, 20)) ) } df = pd.DataFrame(data) # 故意制造两个极端异常值:一个录入错误,一个真实爆款 df.loc[15, "销售额"] = 500 # 录入错误 df.loc[30, "销售额"] = 150 # 可能的真实极端值 df["销售额"] = df["销售额"].round(2)这份数据有三个区域,每个区域 20 条记录。正常情况下华北均值 50、华东均值 80、华南均值 60,但我手动塞了两个极端点。如果只看整体均值,你会觉得数据“还不错”,但拆分区域后才能看出门道。
先跑一份整体五数概括:
print(df["销售额"].describe())输出大致会显示:min 在 40 左右,25% 在 53 左右,50% 在 62 左右,75% 在 82 左右,max 是 500。max 和 75% 之间的距离非常夸张,明显右偏,这就是异常值在作祟。
4.2 绘制盒图定位异常
接着画全量数据的盒图,以及按区域分组的并排盒图:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 整体盒图 df["销售额"].plot.box(ax=axes[0]) axes[0].set_title("整体销售额盒图") # 分组盒图 df.boxplot(column="销售额", by="区域", ax=axes[1]) axes[1].set_title("各区域销售额盒图") plt.tight_layout() plt.show()整体盒图会显示一个孤零零的点飘在须的上方,那就是 500 那个极端值。分组盒图里,华东区域也会出现一个 150 的点,跟同组其他数据明显分离。到这里,异常值“长什么样”已经视觉化清楚了。
4.3 分组计算五数概括与 IQR 检测
整体做一次 IQR 检测会出问题:华东整体均值本来就高,直接用整体区间判断,可能把华南的偏高值误判为异常,而华东的低值反而成了异常。所以正确做法是按组分别计算:
def detect_outliers_iqr(series, k=1.5): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower = Q1 - k * IQR upper = Q3 + k * IQR print(f"区间: [{lower:.2f}, {upper:.2f}]") return (series < lower) | (series > upper) for region, sub_df in df.groupby("区域"): mask = detect_outliers_iqr(sub_df["销售额"]) outliers = sub_df[mask] print(f"{region} 异常值数量: {len(outliers)}") if len(outliers) > 0: print(outliers[["区域", "销售额"]])分组之下,华北那组会检测出异常值 500,华东那组会检测出 150,其他点基本都在区间内。比起整体一刀切,这种分组检测的逻辑更贴合业务分析习惯——每个区域的销售基数和波动幅度本来就不同,比“全国统一划线”合理得多。
4.4 异常值处理:先确认原因再动手
检测出异常值之后,最常见的动作就是“删掉”。但我在前面说过,不能无脑删。针对本例的两个异常值,正确的处理流程是:
- 确认数据来源:查一下 500 这条记录是不是录入时多打了一个 0。如果是,修正为 50;如果不是,核对是否来自某场大型团购。
- 确认业务背景:150 的华东记录如果对应一个真实的大订单,那就应该保留,但在做常规统计时单独标注,或者在同比环比分析中剔除,避免单月波动影响趋势判断。
- 保留处理日志:无论修正还是剔除,都在代码里加注释,甚至单独存一份“数据清洗说明”文件。这个习惯在团队协作里特别加分。
我个人的惯例是:异常值默认不直接删,先标后审。给数据加一列“是否异常”,再写清异常原因,等业务方确认后再决定是剔除、修正还是保留。这样整个分析链条是可追溯的,不会出现“上次分析结果换个说法就对不上”的尴尬。
5. 实战避坑:Pandas 做异常值检测的常见问题
5.1 常见问题速查表
我在带新人和自己实战中,发现下面这些问题出现频率最高,整理成一张速查表:
| 问题现象 | 根因 | 解决办法 |
|---|---|---|
| 画图中文变方框 | Matplotlib 默认字体不支持中文 | 设置plt.rcParams["font.sans-serif"] = ["SimHei"],或用其他中文字体 |
quantile结果和手工算法对不上 | 不同插值方法导致 | 统一用 Pandas 默认线性插值,跨工具对比时先对齐口径 |
| 整体检测把本该正常的数据标成异常 | 数据是分组结构却全局一刀切 | 按业务维度分组后再做 IQR 检测 |
数据里有 NaN,quantile结果异常 | NaN 影响分位数计算 | 先dropna()或按需用skipna=True |
加“是否异常”列时np.where报错 | 条件表达式没有加括号 | 每个比较条件单独加括号,再用|连接 |
| 异常值太多,不知道该不该删 | 正常区间设得过窄或数据确实分散 | 先调k=3看极端值,再结合业务确认 |
| 小样本数据盒图看不出异常 | 样本量太少,分位数不稳定 | 换散点图/分布图辅助判断,不强行解读 |
5.2 几个我实测后的独家心得
第一,describe()和盒图要配合使用,不是二选一。describe()给你精确数字,盒图给你直观分布。先跑describe()感知数值范围,再画盒图定位异常,最后打印异常明细,三步走下来基本不会漏。
第二,分组 IQR 检测要警惕“小分组陷阱”。如果一个组只有 5~10 条数据,分位数波动很大,正常区间会很宽或很窄,不稳定。建议对样本量太小的组不做异常值检测,直接标记为“样本不足”。我在那个白酒数据例子里每组 20 条还能用,真实业务里一个区域只有 3 家门店的情况很常见,这时候 IQR 法不如人工审核可靠。
第三,所有“自动检测”都要留手动覆盖的出口。我封装detect_outliers_iqr时,永远保留k参数和手动指定上下限的能力。因为你不知道哪个数据集会出现什么特殊情况,一个能调参、能覆盖的函数比写死的脚本好用得多。
第四,异常值检测不是一次性工作。数据是动态的,这个月的“爆款”下个月可能变成“常态”,所以要定期重新跑检测、重画盒图。我习惯把整套流程写成脚本,每次数据更新后跑一遍,输出异常值清单让业务复核,比到月底才发现数据问题强得多。
5.3 更进一步:从单变量到多变量的扩展思路
学会了单变量的五数概括和 IQR 检测,实际上你已经掌握了数据分析中最核心的“分布思维”。再往下走,有两个自然的扩展方向:
一是分组对比自动化。把上面那段groupby循环进一步封装,对所有数值列批量输出五数概括、盒图、异常值清单,一套代码搞定整个数据集的“体检报告”。我实际项目里就是这么做的,省下大量重复手工时间。
二是多变量异常检测。单变量检测只关注一个维度,比如销售额异常,但“销售额正常却利润率极低”的记录单看销售额是发现不了的。这时候可以用 Z-score 多维组合、聚类方法或孤立森林来做。不过那些都是进阶内容,先把单变量的 IQR 法和盒图用熟练,打好这个底子,后面学任何复杂方法都会轻松很多。
我个人的体会是,数据分析入门阶段最重要的不是背 API,而是建立“先描述、再可视化、最后下结论”的思维框架。五数概括、盒图、异常值检测这三件套,恰好帮你把这条链路完整走一遍。把这套流程跑顺了,再面对任何新数据集,你都会知道第一步该干什么,这比记住一百个函数重要得多。最后再分享一个小技巧:每做一个分析项目,都把上面的代码整理成自己的工具脚本,下次直接 import,你会发现效率翻倍。