异常值(Outlier)简介
异常值是那些与数据集其他观测值显著不同的异常观测。它们可能由于实验误差、测量误差,或仅仅是数据本身存在的变异性而出现。这些异常值会严重影响模型的表现,导致结果出现偏差——就像大学相对评分中顶尖学生能拉高平均分并影响评分标准一样。处理异常值是数据清洗过程中至关重要的一环。
本文将分享如何识别异常值,以及在数据集中应对异常值的不同方法。
检测异常值
检测异常值有多种方法。如果要对这些方法进行分类,主要包括:
1. 基于可视化的方法:
通过绘制散点图或箱线图来观察数据分布,并检查是否存在异常数据点。
2. 基于统计的方法:
如z分数和IQR(四分位距)法。这些方法更可靠,但可能不如可视化直观。
本文将重点介绍IQR方法,并在最后附上其他方法的参考资料供进一步学习。
IQR方法简介
IQR(四分位距)= Q3(第75百分位数) - Q1(第25百分位数)
IQR方法认为,任何低于 Q1 - 1.5 × IQR 或高于 Q3 + 1.5 × IQR 的数据点都被标记为异常值。下面通过生成一些随机数据,并用此方法检测异常值。
代码示例:生成数据并检测异常值
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 生成随机数据
np.random.seed(42)
data = pd.DataFrame({
'value': np.random.normal(0, 1, 1000)
})
# IQR方法检测异常值
def detect_outliers_iqr(data):
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return (data < lower_bound) | (data > upper_bound)
outliers = detect_outliers_iqr(data['value'])
print(f"检测到的异常值数量:{sum(outliers)}")
输出:
检测到的异常值数量:8
可视化异常值:散点图与箱线图
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# 散点图
ax1.scatter(range(len(data)), data['value'], c=['blue' if not x else 'red' for x in outliers])
ax1.set_title('突出显示异常值的数据集(散点图)')
ax1.set_xlabel('索引')
ax1.set_ylabel('数值')
# 箱线图
sns.boxplot(x=data['value'], ax=ax2)
ax2.set_title('包含异常值的数据集(箱线图)')
ax2.set_xlabel('数值')
plt.tight_layout()
plt.show()
异常值处理方法
1. 删除异常值
这是最简单的处理方式,但并不总是最佳选择。需要考虑以下因素:如果删除异常值会显著减少数据集规模,或这些异常值包含重要信息,则不宜随意删除。但如果异常值是测量误差且数量少,这种方法是合适的。下面演示如何应用这一方法:
# 删除异常值
data_cleaned = data[~outliers]
print(f"原始数据集大小:{len(data)}")
print(f"清洗后数据集大小:{len(data_cleaned)}")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# 散点图
ax1.scatter(range(len(data_cleaned)), data_cleaned['value'])
ax1.set_title('去除异常值后的数据集(散点图)')
ax1.set_xlabel('索引')
ax1.set_ylabel('数值')
# 箱线图
sns.boxplot(x=data_cleaned['value'], ax=ax2)
ax2.set_title('去除异常值后的数据集(箱线图)')
ax2.set_xlabel('数值')
plt.tight_layout()
plt.show()
注意:
去除异常值后,数据分布可能发生变化。最初的异常值被移除后,新的分布下,原本正常的点可能变成新的异常值。
2. 限制(Capping)异常值
如果你不想丢弃极端数据点,但保留它们会影响分析,可以设置最大值和最小值阈值,把异常值“拉回”合理范围。这里以5%和95%分位数为阈值进行处理:
def cap_outliers(data, lower_percentile=5, upper_percentile=95):
lower_limit = np.percentile(data, lower_percentile)
upper_limit = np.percentile(data, upper_percentile)
return np.clip(data, lower_limit, upper_limit)
data['value_capped'] = cap_outliers(data['value'])
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# 散点图
ax1.scatter(range(len(data)), data['value_capped'])
ax1.set_title('限制异常值后的数据集(散点图)')
ax1.set_xlabel('索引')
ax1.set_ylabel('数值')
# 箱线图
sns.boxplot(x=data['value_capped'], ax=ax2)
ax2.set_title('限制异常值后的数据集(箱线图)')
ax2.set_xlabel('数值')
plt.tight_layout()
plt.show()
说明:
此时极端值被“压缩”到指定范围,散点图顶端与底端的点呈现一条直线。
3. 异常值填补(Imputing)
有时不能删除异常值,也不想像限制法一样将其设为最大或最小值。这时可以用更有意义的值(如均值、中位数或众数)替换异常值。以中位数为例:
data['value_imputed'] = data['value'].copy()
median_value = data['value'].median()
data.loc[outliers, 'value_imputed'] = median_value
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# 散点图
ax1.scatter(range(len(data)), data['value_imputed'])
ax1.set_title('填补异常值后的数据集(散点图)')
ax1.set_xlabel('索引')
ax1.set_ylabel('数值')
# 箱线图
sns.boxplot(x=data['value_imputed'], ax=ax2)
ax2.set_title('填补异常值后的数据集(箱线图)')
ax2.set_xlabel('数值')
plt.tight_layout()
plt.show()
提示:
填补后不一定完全消除异常值,因为IQR也可能发生变化。建议结合实际情况多次尝试。
4. 数据变换(Transformation)
通过对整个数据集做变换,可以减少异常值的影响。常见的变换包括对数变换、平方根变换、Box-Cox变换、Z-score标准化、Yeo-Johnson变换、最小-最大缩放等。变换方法的选择取决于数据特性和分析目标。
选择技巧:
-
右偏分布:用对数、平方根或Box-Cox变换
-
左偏分布:先反转数据,再用右偏的技术
-
稳定方差:用Box-Cox或Yeo-Johnson(后者能处理零和负值)
-
均值居中与标准化:用z-score标准化
-
固定范围缩放:用min-max缩放
示例:对右偏数据做对数变换
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 生成右偏数据
np.random.seed(42)
data = np.random.exponential(scale=2, size=1000)
df = pd.DataFrame(data, columns=['value'])
# 对数变换(避免log(0),使用log1p)
df['log_value'] = np.log1p(df['value'])
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 原始数据 - 散点图
axes[0, 0].scatter(range(len(df)), df['value'], alpha=0.5)
axes[0, 0].set_title('原始数据(散点图)')
axes[0, 0].set_xlabel('索引')
axes[0, 0].set_ylabel('数值')
# 原始数据 - 箱线图
sns.boxplot(x=df['value'], ax=axes[0, 1])
axes[0, 1].set_title('原始数据(箱线图)')
axes[0, 1].set_xlabel('数值')
# 对数变换后 - 散点图
axes[1, 0].scatter(range(len(df)), df['log_value'], alpha=0.5)
axes[1, 0].set_title('对数变换后数据(散点图)')
axes[1, 0].set_xlabel('索引')
axes[1, 0].set_ylabel('Log(数值)')
# 对数变换后 - 箱线图
sns.boxplot(x=df['log_value'], ax=axes[1, 1])
axes[1, 1].set_title('对数变换后数据(箱线图)')
axes[1, 1].set_xlabel('Log(数值)')
plt.tight_layout()
plt.show()
说明:
对数变换后,绝大多数异常值已被消除,仅剩下极少数异常点。选择合适的变换要充分了解数据,否则可能带来新的问题。
总结
本文介绍了异常值的概念、常见检测方法及应对策略。希望你能根据实际业务需求,灵活选择最适合的数据清洗方案。