异常值与边界处理
本教程共 54 篇 · 第 28 篇 · 更新于 2026-08-11 · 约 5 分钟阅读
本节目标:用 describe 定位异常值,用 clip 截断、replace 替换、条件赋值修正数据,并理解标准化的思路。
异常值不等于错误值
异常值(outlier)是明显偏离整体的值。它可能是录入错误(年龄 200 岁)、可能是特殊事件(双十一的销量)、也可能是真实现象。处理之前先搞清楚来源——直接删可能把真相删掉。
describe:先体检
describe 给出每列的 count、mean、std、min、25%、50%、75%、max(§18 详细讲过)。异常值通常让 min/max 与四分位数差距巨大:
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame(np.random.randn(100, 3), columns=["A", "B", "C"])
df.loc[0, "A"] = 50 # 人为塞两个异常值
df.loc[1, "B"] = -40
print(df.describe())
A 列的 max 是 50,而 75% 分位只有 0.7 左右,一眼就能看出不对劲。想更直观,画个箱线图(§52 会讲)能直接看到离群点。
定位异常:阈值与 IQR
数据大致服从正态分布时,|值| 超过 3 倍标准差就很少见,可以当异常处理:
mask = np.abs(df["A"]) > 3
print(df[mask])
不知道分布时,用四分位距(IQR)法:低于 Q1 - 1.5×IQR 或高于 Q3 + 1.5×IQR 的视为异常。这个标准不依赖分布假设,对偏态数据更稳:
q1 = df["A"].quantile(0.25)
q3 = df["A"].quantile(0.75)
iqr = q3 - q1
mask = (df["A"] < q1 - 1.5 * iqr) | (df["A"] > q3 + 1.5 * iqr)
print(df[mask])
想”温和地”处理极端值,还可以用分位数定边界:把低于 1% 分位、高于 99% 分位的值截断,比如收入的 1% / 99% 边界:
lo = df["A"].quantile(0.01)
hi = df["A"].quantile(0.99)
print(df["A"].clip(lower=lo, upper=hi).describe())
这种”分位数截断”对长尾分布特别实用:既去掉了极端噪声,又不用主观定阈值。
异常值怎么处理:四种选择
- 保留:确认是真实事件(比如促销日的销量),不处理,分析时单独看待
- 截断:只是”记录溢出”,用 clip 拉到边界
- 替换:已知是脏值,replace 成标准值
- 删除:确属录入错误且数量很少,直接删行
先判断再动手,顺序不要反。
clip:截断到边界
异常值只是”记录溢出”(比如传感器读数、年龄)时,最省事的是截断:超过上限按上限算,低于下限按下限算。
s = pd.Series([-5, 0, 10, 99, 120, 200])
print(s.clip(lower=0, upper=120)) # 低于 0 变 0,高于 120 变 120
DataFrame 同样支持,对单列或整个表截断:
df2 = df.copy()
df2["A"] = df2["A"].clip(lower=-3, upper=3)
print(df2["A"].describe())
clip 只改越界的值,其余数据原样保留,是一种保守的处理方式。
Tip老写法里有人用
data[np.abs(data) > 3] = np.sign(data) * 3手工截断,clip 就是它的标准替代,语义更直白,也不用担心链式赋值的坑(§09)。
replace:替换已知脏值
老系统里常用 -999 标记缺失,读进来后要换成标准缺失值。replace 按值精确替换,支持单个值、列表和字典:
s2 = pd.Series([1.0, -999.0, 2.0, -999.0, 3.0])
print(s2.replace(-999, np.nan)) # 单个值
print(s2.replace([-999, -1000], np.nan)) # 多个值换成一个结果
print(s2.replace({-999: np.nan, -1000: 0})) # 字典一一对应
Notefillna(§25)只处理缺失值,replace 什么值都能换:把 “unknown” 换成 “未知”、把 0 换成 NaN、把错别字换成标准词,都归它管。
条件修正:loc + 布尔条件
规律性的错误(年龄超过 120)用条件赋值修正。3.0 的 Copy-on-Write 下,用 loc 写最安全(§09):
df3 = pd.DataFrame({"姓名": ["张三", "李四", "王五"], "年龄": [25, 200, 12345]})
df3.loc[df3["年龄"] > 120, "年龄"] = 120
print(df3)
想保持形状做条件替换,用 where/mask(§21 讲过):df.where(条件, 替代值) 把条件为 False 的位置换成替代值。
确认是错误数据且数量少,也可以直接删行:
df4 = df3.drop(df3[df3["年龄"] > 120].index)
print(df4)
标准化:消除量纲
异常值处理完,不同列往往量纲差很远(年龄几十、收入几万)。很多分析场景下要把列拉到同一尺度:
- Z-score 标准化:减均值再除以标准差,结果均值 0、标准差 1
- Min-Max 归一化:减最小值除以极差,结果落在 [0, 1]
pandas 直接算:
z = (df["A"] - df["A"].mean()) / df["A"].std()
print(z.describe()) # mean≈0,std≈1
minmax = (df["A"] - df["A"].min()) / (df["A"].max() - df["A"].min())
print(minmax.min(), minmax.max()) # 0 和 1
Warning标准化前先处理缺失(§24/§25)和异常值:均值会被极端值拉偏,缺失会直接算出 NaN。标准顺序是:清洗缺失 → 去重 → 修异常 → 标准化。需要更专业的标准化工具时,再引入 sklearn 的 StandardScaler / MinMaxScaler,这里只给思路。
小结
异常值先定位再处理:describe、3 倍标准差、IQR 是三种定位法;clip 截断、replace 替换、loc 条件修正、删行是四种处理法。先弄清来源再动手,别把真相当噪声删掉。标准化(Z-score、Min-Max)是异常值处理完之后的事。