"为什么上个月的平均客单价是 500 万?"
周一早会,老板指着 PPT 上的数据发飙。你吓得一身冷汗,回去一查数据库,发现是实习生把"用户手机号"填到了"消费金额"那一栏。
这种因录入错误、系统故障或恶意刷单产生的"异常值"(Outliers),是数据分析中最大的隐形地雷。人工肉眼看 100 行还行,10 万行数据怎么看?
今天教大家写一个可复用的"数据安检机",利用统计学方法(IQR 和 Z-Score),自动识别并处理这些异常。
🛠️ 方法一:箱线图法则 (IQR) ------ 简单粗暴,抗干扰强
这是最常用的方法,尤其适合非正态分布的数据(比如薪资、房价,大部分人被平均,少部分人极高)。
它的逻辑是:找出数据的"上四分位数"(Q3,前 25%)和"下四分位数"(Q1,后 25%)。
中间这 50% 的范围叫 IQR 。
凡是比 Q3 + 1.5倍IQR 还大,或者比 Q1 - 1.5倍IQR 还小的,统统判定为异常。
python
import pandas as pd
import numpy as np
def detect_outliers_iqr(df, column):
"""
使用 IQR (四分位距) 检测异常值
返回: 异常值的索引列表
"""
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f"📉 [{column}] 正常范围: {lower_bound:.2f} ~ {upper_bound:.2f}")
# 找出在这个范围之外的行索引
outliers_index = df[(df[column] < lower_bound) | (df[column] > upper_bound)].index
return outliers_index
# --- 测试一下 ---
# 造点数据:正常人身高 170左右,混进去一个 3米的巨人 (异常值)
data = {'身高': [170, 172, 168, 175, 169, 171, 300, 165]}
df = pd.DataFrame(data)
bad_guys = detect_outliers_iqr(df, '身高')
print(f"🚨 发现异常值索引: {bad_guys.tolist()}")
print("异常数据内容:\n", df.loc[bad_guys])
老鸟点评 :IQR 的好处是稳。它不怎么受极端值影响。就算数据里真的混进了一个"姚明",也不会把整个监测标准拉得太离谱。
🔬 方法二:Z-Score 标准分 ------ 严谨的统计学派
如果你的数据大致符合正态分布(比如工厂零件尺寸、考试成绩),用 Z-Score 更精准。
它的逻辑是:计算每个数据距离"平均值"有几个"标准差"。
- 超过 3 个标准差(Z > 3),通常被视为异常(发生的概率小于 0.3%)。
这里我们需要用到 scipy.stats。
python
from scipy import stats
def detect_outliers_zscore(df, column, threshold=3):
"""
使用 Z-Score 检测异常值
threshold: 阈值,通常取 3,严格点取 2
"""
# 计算 Z 分数
z_scores = np.abs(stats.zscore(df[column]))
# 找出 Z 分数大于阈值的索引
outliers_index = np.where(z_scores > threshold)[0]
return outliers_index
# --- 测试一下 ---
data = {'分数': [80, 85, 82, 88, 85, 83, 0, 1000]} # 0分可能是缺考,1000分肯定是录错了
df = pd.DataFrame(data)
bad_guys = detect_outliers_zscore(df, '分数', threshold=2.5) # 稍微严一点
print(f"🚨 Z-Score 发现异常索引: {bad_guys}")
老鸟点评 :Z-Score 对平均值很敏感。如果你的异常值太大(比如 500 亿),它会把整个平均值拉高,导致原本异常的东西看起来反而"正常"了。所以数据量大且分布均匀时再用它。
🛡️ 终极武器:编写可复用的"数据清洗机"
单纯"发现"没用,我们要的是"处理"。
处理异常值通常有三种策略:
- 删除 (Drop):最省事,但会损失数据量。
- 盖帽 (Clip):超过上限的,就强制等于上限(比如超过 3 米的都算 3 米)。
- 填充 (Impute):用平均值或中位数替换异常值。
下面这个函数,建议直接收藏进你的 utils.py,以后任何项目都能用。
python
def auto_clean_data(df, columns, method='iqr', action='clip'):
"""
一站式异常值处理函数
:param df: Pandas DataFrame
:param columns: 需要清洗的列名列表
:param method: 'iqr' 或 'zscore'
:param action: 'drop' (删除), 'clip' (盖帽), 'report' (只报告不改)
:return: 清洗后的 DataFrame, 异常报告字典
"""
df_clean = df.copy()
report = {}
for col in columns:
# 1. 检测
if method == 'iqr':
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# 找到异常值的掩码 (Mask)
outlier_mask = (df_clean[col] < lower) | (df_clean[col] > upper)
elif method == 'zscore':
z_scores = np.abs(stats.zscore(df_clean[col]))
outlier_mask = z_scores > 3
lower, upper = None, None # Z-score 很难直接给出上下界数值
count = outlier_mask.sum()
report[col] = {'count': count, 'action': action}
if count > 0:
print(f"⚠️ 列 [{col}] 发现 {count} 个异常值!")
# 2. 处理
if action == 'drop':
df_clean = df_clean[~outlier_mask]
elif action == 'clip' and method == 'iqr':
# Pandas 的 clip 方法非常方便,直接把超出的部分"削平"
df_clean[col] = df_clean[col].clip(lower=lower, upper=upper)
elif action == 'report':
pass # 啥也不干,只看日志
return df_clean, report
# --- 实战演练 ---
# 模拟一份销售数据
np.random.seed(42)
df_sales = pd.DataFrame({
'订单ID': range(100),
'销售额': np.random.normal(100, 20, 100) # 均值100,标准差20
})
# 人为制造脏数据
df_sales.loc[5, '销售额'] = 5000 # 手抖多按了几个0
df_sales.loc[10, '销售额'] = -100 # 负数?不可能
# 一键清洗
print("--- 开始清洗 ---")
df_final, log = auto_clean_data(df_sales, ['销售额'], method='iqr', action='clip')
print("\n--- 清洗结果 ---")
print(f"原始最大值: {df_sales['销售额'].max()}")
print(f"清洗后最大值: {df_final['销售额'].max()}") # 应该被限制在正常范围内
🚨 进阶:如何做实时监控?
如果你每天都要处理 Excel,可以把这个脚本挂在服务器上。
- 读取 :用
pd.read_excel()读取每日最新文件。 - 检测 :调用
auto_clean_data(..., action='report')。 - 预警 :如果
report['count'] > 0,触发邮件或钉钉报警。
python
# 简单的预警逻辑示例
_, report = auto_clean_data(daily_df, ['核心指标'], action='report')
if report['核心指标']['count'] > 10:
# 异常值太多了,肯定出大事了,别清洗了,直接人工介入
send_alert_email("警告:今日数据异常值激增,请立即检查源数据!")
else:
# 只有几个异常,自动清洗掉,继续跑后面的流程
clean_df, _ = auto_clean_data(daily_df, ['核心指标'], action='clip')
save_to_database(clean_df)
💣 避坑指南
-
业务逻辑 > 统计学 :
代码只是辅助。如果你的业务是"奢侈品销售",那么偶尔出现一个 100 万的订单是正常的,不能当异常值删掉。在使用 IQR 或 Z-Score 之前,先问问业务部门"合理的范围是多少"。
-
千万别上来就 Drop :
我见过很多新手,代码写着
df = df[z_score < 3],结果一运行,数据少了一半。一定要先用report模式看看异常值长什么样,再决定是删还是改。 -
时间序列的坑 :
如果你的数据是随时间变化的(比如双11当天的销量暴增),那不是异常,那是趋势。这时候要用更高级的"移动平均"或"同比环比"来监测,而不是简单地看整体分布。
写在最后
数据清洗是数据分析工作中最脏、最累、最不起眼的一环,但它也是最能体现职业素养的一环。
如果你能写出一个脚本,在所有人发现问题之前,就指着屏幕说:"老板,这行数据的逻辑不对,我已经拦下来了。"
那一刻,你就不再是一个简单的"跑数工",而是数据的"守门人"。
代码拿去,守好你的防线。🛡️