数据清洗实战:处理缺失值与异常值的技巧
大家好!欢迎来到我的数据科学博客。今天我们要深入探讨数据分析中至关重要却常被忽视的环节——数据清洗。无论你是刚入门的数据新手,还是有一定经验的分析师,处理数据中的缺失值和异常值都是必不可少的技能。这篇实战指南将带你从零开始,掌握处理这些问题的高效技巧。
想象一下:你拿到一个数据集,满心欢喜地准备开始分析,却发现许多值缺失了,或者有些数值明显不合理。这时如果不进行适当的处理,直接进行分析或建模,得到的结论很可能是有偏差甚至完全错误的。数据清洗就像是烹饪前的食材准备,虽然不如正式烹饪那样光鲜亮丽,但却是做出美味佳肴的基础保障。
在这篇长文中,我将通过一个完整的实战案例,详细介绍如何处理缺失值和异常值。我们会使用Python和常见的库如Pandas、NumPy和Matplotlib,并提供详细的代码解释。让我们开始这段数据清洗之旅吧!
I. 数据清洗概述
数据清洗是数据预处理的关键步骤,指的是检测和修正(或移除)数据集中不准确、不完整或不合理的部分的过程。它就像是数据分析的"家务活",虽然不那么令人兴奋,但却至关重要。
为什么数据清洗如此重要?
- 提高数据质量:脏数据会导致错误的分析结果和决策
- 增强模型性能:机器学习模型对数据质量非常敏感
- 保证结果可靠性:清洗后的数据能够产生更加可信的结论
- 节省时间成本:前期适当清洗可以避免后期反复调试
数据清洗通常包括处理缺失值、异常值、重复数据、不一致数据等。本文将重点讨论前两个方面:缺失值和异常值的处理。
数据清洗的主要挑战
挑战 | 描述 | 影响 |
---|---|---|
识别问题 | 如何准确发现数据中的问题 | 决定后续处理方法的有效性 |
处理方法选择 | 选择最适合当前数据的处理方式 | 直接影响分析结果的准确性 |
处理程度把握 | 避免过度清洗或清洗不足 | 平衡数据完整性和数据真实性 |
计算资源 | 大规模数据清洗需要相应资源 | 影响清洗效率和可行性 |
让我们通过一个Mermaid图来总结数据清洗的概念框架:
现在我们已经了解了数据清洗的基本概念,接下来让我们深入探讨缺失值的处理技巧。
II. 缺失值处理技巧
缺失值是实际数据集中最常见的问题之一。它们可能由于各种原因产生:信息无法获取、数据录入错误、设备故障等。正确处理缺失值对于保证分析结果的准确性至关重要。
缺失值类型
了解缺失值的类型对于选择正确的处理方法非常重要:
缺失类型 | 描述 | 示例 |
---|---|---|
完全随机缺失(MCAR) | 缺失的发生与任何变量无关 | 调查问卷随机丢失页面 |
随机缺失(MAR) | 缺失与已观察变量相关但与未观察值无关 | 男性更可能不回答收入问题 |
非随机缺失(MNAR) | 缺失与未观察值本身相关 | 高收入人群更可能隐瞒收入 |
缺失值识别方法
在开始处理缺失值之前,我们需要先识别它们的存在和模式。Python的Pandas库提供了多种方法来识别缺失值。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 创建示例数据集
data = {
'Age': [25, 32, np.nan, 45, 33, np.nan, 29, 41, 36, 28],
'Income': [50000, np.nan, 75000, 62000, np.nan, 58000, 71000, np.nan, 68000, 53000],
'Gender': ['M', 'F', 'M', np.nan, 'F', 'M', 'F', 'F', np.nan, 'M'],
'Score': [3.5, 4.0, 2.8, 3.2, 3.9, 2.5, 4.0, 3.7, 3.1, 2.9]
}
df = pd.DataFrame(data)
print("数据集形状:", df.shape)
print("\n前5行数据:")
print(df.head())
# 检查缺失值
print("\n缺失值统计:")
print(df.isnull().sum())
print("\n缺失值比例:")
print(df.isnull().mean().round(4) * 100)
代码解释:
- 首先我们导入必要的库:pandas用于数据处理,numpy用于数值计算,matplotlib和seaborn用于可视化
- 创建一个包含故意缺失值(np.nan)的示例数据集
- 使用
isnull().sum()
计算每列的缺失值数量 - 使用
isnull().mean()
计算每列的缺失值比例
缺失值可视化
可视化可以帮助我们更直观地理解缺失值的模式和分布:
# 设置图形风格
plt.style.use('seaborn-v0_8')
# 创建缺失值可视化
fig, ax = plt.subplots(2, 2, figsize=(15, 12))
# 缺失值数量柱状图
missing_count = df.isnull().sum()
ax[0, 0].bar(missing_count.index, missing_count.values, color='skyblue')
ax[0, 0].set_title('各变量缺失值数量', fontsize=14, fontweight='bold')
ax[0, 0].set_ylabel('缺失值数量')
# 缺失值比例饼图
missing_percentage = df.isnull().mean()
labels = [f'{col}\n({pct:.1%})' for col, pct in zip(missing_percentage.index, missing_percentage.values)]
ax[0, 1].pie(missing_percentage.values, labels=labels, autopct='%1.1f%%')
ax[0, 1].set_title('各变量缺失值比例', fontsize=14, fontweight='bold')
# 缺失值热力图
sns.heatmap(df.isnull(), cbar=False, cmap='viridis', ax=ax[1, 0])
ax[1, 0].set_title('缺失值分布热力图', fontsize=14, fontweight='bold')
# 缺失值模式相关图
missing_corr = df.isnull().corr()
sns.heatmap(missing_corr, annot=True, cmap='coolwarm', center=0, ax=ax[1, 1])
ax[1, 1].set_title('缺失值模式相关性', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
代码解释:
- 创建一个2x2的子图布局来展示不同的缺失值可视化
- 第一个子图显示各变量缺失值数量的柱状图
- 第二个子图显示各变量缺失值比例的饼图
- 第三个子图使用热力图展示缺失值的具体分布位置
- 第四个子图展示不同变量缺失模式之间的相关性,这有助于判断缺失机制
缺失值处理方法
根据缺失值的类型和比例,我们可以选择不同的处理方法:
方法 | 描述 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
删除法 | 直接删除含有缺失值的行或列 | 缺失值比例很小或缺失值比例很大的变量 | 简单易行 | 可能丢失有用信息 |
均值/中位数/众数填充 | 用变量的集中趋势度量填充缺失值 | 数值型变量,随机缺失 | 简单快速 | 低估方差,扭曲分布 |
前后向填充 | 用前一个或后一个值填充缺失值 | 时间序列数据 | 保持数据顺序 | 不适用于非时间序列数据 |
插值法 | 使用数学插值方法估计缺失值 | 有序数据,连续变量 | 相对精确 | 计算复杂,可能过拟合 |
模型填充 | 使用预测模型估计缺失值 | 复杂数据集,变量间有关系 | 精度高 | 计算复杂,可能引入偏差 |
下面是几种常见处理方法的代码实现:
# 方法1: 删除缺失值
df_dropped = df.dropna() # 删除任何含有缺失值的行
print("删除缺失值后的数据集形状:", df_dropped.shape)
# 方法2: 简单填充
df_filled = df.copy()
# 数值变量用中位数填充
df_filled['Age'] = df_filled['Age'].fillna(df_filled['Age'].median())
df_filled['Income'] = df_filled['Income'].fillna(df_filled['Income'].median())
# 分类变量用众数填充
df_filled['Gender'] = df_filled['Gender'].fillna(df_filled['Gender'].mode()[0])
print("\n简单填充后的数据集:")
print(df_filled.isnull().sum())
# 方法3: 插值法
df_interpolated = df.copy()
# 对数值变量使用线性插值
df_interpolated['Age'] = df_interpolated['Age'].interpolate(method='linear')
df_interpolated['Income'] = df_interpolated['Income'].interpolate(method='linear')
print("\n插值法处理后的数据集:")
print(df_interpolated.isnull().sum())
# 方法4: 模型填充(使用KNN)
from sklearn.impute import KNNImputer
df_knn = df.copy()
# 先将分类变量转换为数值
df_knn['Gender'] = df_knn['Gender'].map({'M': 0, 'F': 1, np.nan: np.nan})
# 使用KNN填充
imputer = KNNImputer(n_neighbors=2)
df_knn_imputed = pd.DataFrame(imputer.fit_transform(df_knn), columns=df.columns)
print("\nKNN填充后的数据集:")
print(df_knn_imputed.isnull().sum())
代码解释:
- 删除法:使用
dropna()
直接删除含有缺失值的行 - 简单填充:数值变量使用中位数填充,分类变量使用众数填充
- 插值法:使用线性插值方法估计缺失值
- 模型填充:使用K近邻算法,基于相似样本的值来填充缺失值
缺失值处理策略选择
选择哪种缺失值处理方法取决于多个因素:
def handle_missing_data(df, strategy='auto', threshold=0.3):
"""
根据策略自动处理缺失值
参数:
df: 输入DataFrame
strategy: 处理策略 ('auto', 'delete', 'simple', 'interpolate', 'model')
threshold: 删除变量的阈值,缺失比例高于此值的变量将被删除
返回:
处理后的DataFrame
"""
df_processed = df.copy()
# 首先删除缺失值比例过高的变量
missing_ratio = df_processed.isnull().mean()
cols_to_drop = missing_ratio[missing_ratio > threshold].index
df_processed = df_processed.drop(columns=cols_to_drop)
print(f"已删除缺失比例超过{threshold:.0%}的变量: {list(cols_to_drop)}")
# 自动选择策略
if strategy == 'auto':
# 根据数据特征自动选择最佳策略
numeric_cols = df_processed.select_dtypes(include=[np.number]).columns
categorical_cols = df_processed.select_dtypes(exclude=[np.number]).columns
# 对数值变量使用插值
for col in numeric_cols:
if df_processed[col].isnull().sum() > 0:
df_processed[col] = df_processed[col].interpolate(method='linear')
# 如果还有缺失值(如首尾缺失),使用中位数填充
df_processed[col] = df_processed[col].fillna(df_processed[col].median())
# 对分类变量使用众数填充
for col in categorical_cols:
if df_processed[col].isnull().sum() > 0:
df_processed[col] = df_processed[col].fillna(df_processed[col].mode()[0])
elif strategy == 'delete':
df_processed = df_processed.dropna()
elif strategy == 'simple':
numeric_cols = df_processed.select_dtypes(include=[np.number]).columns
categorical_cols = df_processed.select_dtypes(exclude=[np.number]).columns
for col in numeric_cols:
if df_processed[col].isnull().sum() > 0:
df_processed[col] = df_processed[col].fillna(df_processed[col].median())
for col in categorical_cols:
if df_processed[col].isnull().sum() > 0:
df_processed[col] = df_processed[col].fillna(df_processed[col].mode()[0])
return df_processed
# 使用自动策略处理缺失值
df_processed = handle_missing_data(df, strategy='auto', threshold=0.4)
print("\n自动处理后的数据集缺失值统计:")
print(df_processed.isnull().sum())
代码解释:
- 创建一个综合函数来处理缺失值,根据预设策略自动选择最佳方法
- 首先删除缺失比例过高的变量(超过阈值)
- 根据变量类型(数值型或分类型)选择不同的填充策略
- 提供多种策略选项:自动、删除、简单填充等
通过上述方法,我们可以系统地处理数据集中的缺失值问题。接下来,让我们探讨另一个重要主题:异常值处理。
现在我们已经掌握了缺失值处理的技巧,接下来让我们转向数据清洗的另一个重要方面:异常值处理。
III. 异常值处理技巧
异常值是指与数据集中其他观测值显著不同的数据点,它们可能是由于测量错误、数据录入错误、抽样问题或真实但罕见的事件产生的。正确识别和处理异常值对于保证数据分析结果的准确性至关重要。
异常值的类型
了解异常值的类型有助于我们选择正确的检测和处理方法:
类型 | 描述 | 示例 |
---|---|---|
单变量异常值 | 在单一变量上取值异常 | 年龄值为200岁 |
多变量异常值 | 多个变量组合异常,但每个变量单独看正常 | 年龄5岁但学历为博士 |
点异常 | 单个数据点与其余数据明显不同 | 正常温度20-30度,出现100度 |
上下文异常 | 在特定上下文中异常 | 夏季出现零下温度 |
集体异常 | 一组数据集体与其余数据不同 | 连续多次相同的异常测量值 |
异常值检测方法
检测异常值有多种统计和可视化方法,下面我们通过代码演示几种常用方法:
# 创建包含异常值的示例数据
np.random.seed(42)
normal_data = np.random.normal(50, 10, 100) # 正常数据:均值50,标准差10
outlier_data = np.array([120, 130, -15, 140, -20]) # 异常值
data_with_outliers = np.concatenate([normal_data, outlier_data])
# 创建DataFrame
df_outliers = pd.DataFrame({
'Value': data_with_outliers,
'Type': ['Normal'] * 100 + ['Outlier'] * 5
})
print("数据集描述统计:")
print(df_outliers['Value'].describe())
# 异常值检测可视化
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
# 1. 箱线图
df_outliers.boxplot(column='Value', ax=axes[0, 0])
axes[0, 0].set_title('箱线图检测异常值', fontweight='bold')
# 2. 直方图
axes[0, 1].hist(df_outliers['Value'], bins=30, alpha=0.7, color='skyblue')
axes[0, 1].axvline(df_outliers['Value'].mean(), color='red', linestyle='dashed', linewidth=1, label='均值')
axes[0, 1].axvline(df_outliers['Value'].mean() + 3*df_outliers['Value'].std(), color='orange', linestyle='dashed', linewidth=1, label='3σ范围')
axes[0, 1].axvline(df_outliers['Value'].mean() - 3*df_outliers['Value'].std(), color='orange', linestyle='dashed', linewidth=1)
axes[0, 1].set_title('直方图检测异常值', fontweight='bold')
axes[0, 1].legend()
# 3. Z-score分布
from scipy import stats
z_scores = np.abs(stats.zscore(df_outliers['Value']))
axes[1, 0].scatter(range(len(z_scores)), z_scores, alpha=0.6)
axes[1, 0].axhline(y=3, color='r', linestyle='--', label='Z=3阈值')
axes[1, 0].set_title('Z-score异常值检测', fontweight='bold')
axes[1, 0].set_ylabel('Z-score')
axes[1, 0].set_xlabel('数据点索引')
axes[1, 0].legend()
# 4. 散点图(展示异常值位置)
axes[1, 1].scatter(range(len(df_outliers)), df_outliers['Value'], alpha=0.6,
c=df_outliers['Type'].map({'Normal': 'blue', 'Outlier': 'red'}))
axes[1, 1].set_title('异常值位置分布', fontweight='bold')
axes[1, 1].set_ylabel('值')
axes[1, 1].set_xlabel('数据点索引')
plt.tight_layout()
plt.show()
代码解释:
- 创建包含正常值和异常值的混合数据集
- 使用箱线图可视化,箱线图可以直观显示异常值(箱体外的点)
- 使用直方图展示数据分布,并结合均值和标准差线标识异常值范围
- 计算Z-score(标准分数),绝对值大于3的通常被认为是异常值
- 使用散点图直接展示异常值的位置和分布
异常值检测的统计方法
除了可视化方法,我们还可以使用多种统计方法来检测异常值:
# 定义异常值检测函数
def detect_outliers(df, column, method='iqr', threshold=3):
"""
使用不同方法检测异常值
参数:
df: 输入DataFrame
column: 要检测的列名
method: 检测方法 ('iqr', 'zscore', 'modified_zscore')
threshold: 检测阈值
返回:
异常值索引列表
"""
data = df[column].copy()
if method == 'iqr':
# IQR方法
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - threshold * IQR
upper_bound = Q3 + threshold * IQR
outliers = data[(data < lower_bound) | (data > upper_bound)]
elif method == 'zscore':
# Z-score方法
z_scores = np.abs(stats.zscore(data.dropna()))
outliers = data[z_scores > threshold]
elif method == 'modified_zscore':
# 修正Z-score方法,对异常值更稳健
median = np.median(data.dropna())
mad = np.median(np.abs(data.dropna() - median))
modified_z_scores = 0.6745 * np.abs(data - median) / mad
outliers = data[modified_z_scores > threshold]
return outliers.index.tolist()
# 应用不同的异常值检测方法
iqr_outliers = detect_outliers(df_outliers, 'Value', method='iqr', threshold=1.5)
zscore_outliers = detect_outliers(df_outliers, 'Value', method='zscore', threshold=3)
mod_z_outliers = detect_outliers(df_outliers, 'Value', method='modified_zscore', threshold=3.5)
print(f"IQR方法检测到的异常值数量: {len(iqr_outliers)}")
print(f"Z-score方法检测到的异常值数量: {len(zscore_outliers)}")
print(f"修正Z-score方法检测到的异常值数量: {len(mod_z_outliers)}")
# 比较不同方法的检测结果
plt.figure(figsize=(10, 6))
plt.scatter(range(len(df_outliers)), df_outliers['Value'], alpha=0.6,
label='正常值', c='blue')
# 标记不同方法检测到的异常值
plt.scatter(iqr_outliers, df_outliers.loc[iqr_outliers, 'Value'],
color='red', marker='o', s=100, label='IQR异常值')
plt.scatter(zscore_outliers, df_outliers.loc[zscore_outliers, 'Value'],
color='green', marker='s', s=80, label='Z-score异常值')
plt.scatter(mod_z_outliers, df_outliers.loc[mod_z_outliers, 'Value'],
color='orange', marker='^', s=80, label='修正Z-score异常值')
plt.title('不同异常值检测方法比较', fontweight='bold')
plt.ylabel('值')
plt.xlabel('数据点索引')
plt.legend()
plt.show()
代码解释:
- 定义了一个综合函数,支持三种常见的异常值检测方法:
- IQR方法:基于四分位距,适用于非正态分布数据
- Z-score方法:基于标准差,适用于近似正态分布数据
- 修正Z-score方法:使用中位数和绝对偏差,对异常值更稳健
- 应用这三种方法检测异常值,并比较它们的检测结果
- 可视化展示不同方法检测到的异常值,帮助理解各方法的特点和差异
异常值处理方法
检测到异常值后,我们需要决定如何处理它们。以下是常见的异常值处理方法:
方法 | 描述 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
删除 | 直接删除异常值 | 异常值数量少且明显错误 | 简单直接 | 可能丢失重要信息 |
替换 | 用合理值替换异常值 | 异常值可能包含有用信息 | 保留数据点 | 可能引入偏差 |
转换 | 使用数学转换减小异常值影响 | 需要保留异常值但减小其影响 | 保持数据完整性 | 解释性变差 |
分箱 | 将数值离散化到有限的"箱子"中 | 不需要精确数值,只需范围 | 减少异常值影响 | 丢失数值精度 |
保留 | 不处理异常值,使用稳健算法 | 异常值是真实且有意义的 | 不丢失信息 | 需要特殊算法 |
下面是几种常见处理方法的代码实现:
# 创建处理异常值的函数
def handle_outliers(df, column, method='cap', threshold=3):
"""
使用不同方法处理异常值
参数:
df: 输入DataFrame
column: 要处理的列名
method: 处理方法 ('remove', 'cap', 'transform', 'mean')
threshold: 处理阈值
返回:
处理后的DataFrame
"""
df_processed = df.copy()
data = df_processed[column]
# 首先检测异常值
outlier_indices = detect_outliers(df_processed, column, 'zscore', threshold)
if method == 'remove':
# 删除异常值
df_processed = df_processed.drop(outlier_indices)
print(f"删除了 {len(outlier_indices)} 个异常值")
elif method == 'cap':
# 封顶法:将异常值替换为阈值
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
df_processed[column] = np.where(data < lower_bound, lower_bound, data)
df_processed[column] = np.where(data > upper_bound, upper_bound, data)
print(f"封顶处理了异常值,下限: {lower_bound:.2f}, 上限: {upper_bound:.2f}")
elif method == 'transform':
# 数据转换:使用对数转换减小异常值影响
if (data > 0).all(): # 确保所有值都大于0
df_processed[column] = np.log1p(data)
print("应用了对数转换")
else:
print("数据包含非正值,无法应用对数转换")
elif method == 'mean':
# 用均值或中位数替换异常值
median_val = data.median()
df_processed.loc[outlier_indices, column] = median_val
print(f"用中位数 {median_val:.2f} 替换了 {len(outlier_indices)} 个异常值")
return df_processed
# 比较不同处理方法的效果
methods = ['cap', 'transform', 'mean']
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 原始数据分布
axes[0, 0].hist(df_outliers['Value'], bins=30, alpha=0.7, color='skyblue')
axes[0, 0].set_title('原始数据分布', fontweight='bold')
# 应用不同方法并可视化
for i, method in enumerate(methods):
df_treated = handle_outliers(df_outliers, 'Value', method=method)
row, col = (i+1) // 2, (i+1) % 2
axes[row, col].hist(df_treated['Value'], bins=30, alpha=0.7, color='lightgreen')
axes[row, col].set_title(f'使用{method}方法处理后的分布', fontweight='bold')
plt.tight_layout()
plt.show()
代码解释:
- 定义了一个综合函数来处理异常值,支持多种处理方法
- 删除法:直接删除检测到的异常值
- 封顶法:将异常值替换为正常值的上下限(基于IQR)
- 转换法:使用对数转换减小异常值的影响
- 均值/中位数替换:用集中趋势度量替换异常值
- 比较不同处理方法对数据分布的影响,可视化展示处理效果
多变量异常值检测
有时候,单个变量看起来正常,但多个变量的组合可能异常。这种情况下,我们需要多变量异常值检测方法:
# 创建包含多变量异常值的示例数据
np.random.seed(42)
n_samples = 200
# 正常数据
normal_data = pd.DataFrame({
'Age': np.random.normal(35, 5, n_samples),
'Income': np.random.normal(50000, 10000, n_samples),
'Spending': np.random.normal(2500, 500, n_samples)
})
# 多变量异常值(年龄小但收入高)
outlier_data = pd.DataFrame({
'Age': [22, 25, 19, 21],
'Income': [120000, 110000, 130000, 115000],
'Spending': [4800, 4500, 5100, 4900]
})
# 合并数据
df_multi = pd.concat([normal_data, outlier_data], ignore_index=True)
# 多变量异常值检测(使用隔离森林)
from sklearn.ensemble import IsolationForest
# 训练隔离森林模型
clf = IsolationForest(contamination=0.05, random_state=42)
clf.fit(df_multi[['Age', 'Income']])
df_multi['Outlier_Score'] = clf.decision_function(df_multi[['Age', 'Income']])
df_multi['Is_Outlier'] = clf.predict(df_multi[['Age', 'Income']])
# 可视化多变量异常值
plt.figure(figsize=(12, 6))
# 正常点和异常点
normal_points = df_multi[df_multi['Is_Outlier'] == 1]
outlier_points = df_multi[df_multi['Is_Outlier'] == -1]
plt.scatter(normal_points['Age'], normal_points['Income'],
alpha=0.6, c='blue', label='正常点')
plt.scatter(outlier_points['Age'], outlier_points['Income'],
alpha=0.8, c='red', s=100, marker='X', label='异常值')
plt.xlabel('年龄')
plt.ylabel('收入')
plt.title('多变量异常值检测', fontweight='bold')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
print(f"检测到 {len(outlier_points)} 个多变量异常值")
代码解释:
- 创建包含多变量异常值的数据集(年龄小但收入高的异常组合)
- 使用隔离森林(Isolation Forest)算法检测多变量异常值
- 该算法通过随机划分特征空间来隔离异常点,适用于高维数据
- 可视化展示多变量异常值的检测结果,异常值用红色X标记
通过上述方法,我们可以有效地检测和处理数据集中的异常值。接下来,让我们通过一个综合案例将这些技巧应用到实际数据中。
现在我们已经掌握了异常值处理的技巧,接下来让我们通过一个综合案例将这些技术应用到实际场景中。
IV. 综合实战案例
在本节中,我们将通过一个完整的实战案例,展示如何综合运用前面介绍的缺失值和异常值处理技巧。我们将使用一个模拟的客户数据集,它包含真实世界中常见的各种数据质量问题。
案例背景
假设我们是一家电子商务公司的数据分析师,我们获得了客户数据集的访问权限,该数据集包含以下信息:
- 人口统计学信息(年龄、性别)
- 经济状况(收入、职业)
- 消费行为(购买频率、平均消费额)
- 客户满意度(评分、投诉次数)
我们的任务是清洗这个数据集,为后续的客户细分和预测建模做准备。
数据加载与初步探索
首先,让我们创建并加载这个模拟数据集:
# 创建模拟客户数据集
np.random.seed(42)
n_customers = 500
# 创建基本数据
data = {
'CustomerID': range(1, n_customers + 1),
'Age': np.random.normal(45, 15, n_customers).astype(int),
'Gender': np.random.choice(['M', 'F', 'Other', np.nan], n_customers, p=[0.48, 0.48, 0.02, 0.02]),
'Income': np.random.normal(75000, 25000, n_customers),
'Occupation': np.random.choice(['Employed', 'Self-Employed', 'Student', 'Retired', 'Unemployed', np.nan],
n_customers, p=[0.5, 0.2, 0.1, 0.1, 0.08, 0.02]),
'Purchase_Frequency': np.random.poisson(5, n_customers),
'Avg_Spending': np.random.normal(120, 40, n_customers),
'Satisfaction_Score': np.random.randint(1, 11, n_customers),
'Complaints': np.random.poisson(1.5, n_customers)
}
df_customers = pd.DataFrame(data)
# 故意添加一些缺失值
missing_indices = np.random.choice(n_customers, size=30, replace=False)
df_customers.loc[missing_indices, 'Income'] = np.nan
missing_indices = np.random.choice(n_customers, size=20, replace=False)
df_customers.loc[missing_indices, 'Age'] = np.nan
# 故意添加一些异常值
outlier_indices = np.random.choice(n_customers, size=10, replace=False)
df_customers.loc[outlier_indices, 'Income'] *= 3 # 异常高收入
outlier_indices = np.random.choice(n_customers, size=8, replace=False)
df_customers.loc[outlier_indices, 'Age'] = np.random.randint(100, 120, size=8) # 异常年龄
outlier_indices = np.random.choice(n_customers, size=15, replace=False)
df_customers.loc[outlier_indices, 'Avg_Spending'] *= 4 # 异常高消费
print("数据集形状:", df_customers.shape)
print("\n前5行数据:")
print(df_customers.head())
print("\n数据集信息:")
print(df_customers.info())
print("\n缺失值统计:")
print(df_customers.isnull().sum())
print("\n数值变量描述统计:")
print(df_customers.describe())
代码解释:
- 创建一个模拟的客户数据集,包含500个客户记录
- 故意添加缺失值和异常值,模拟真实世界的数据质量问题
- 使用
info()
查看数据集基本信息 - 使用
isnull().sum()
统计缺失值数量 - 使用
describe()
查看数值变量的描述统计
数据清洗流程
现在让我们实施完整的数据清洗流程:
# 第一步:创建数据清洗管道
def data_cleaning_pipeline(df):
"""
完整的数据清洗管道
"""
df_clean = df.copy()
# 1. 处理缺失值
print("步骤1: 处理缺失值")
print("-" * 50)
# 分析缺失值模式
missing_percentage = df_clean.isnull().mean() * 100
print("缺失值比例:")
for col, pct in missing_percentage.items():
if pct > 0:
print(f" {col}: {pct:.2f}%")
# 删除缺失比例高的变量(如果存在)
cols_to_drop = missing_percentage[missing_percentage > 30].index
if len(cols_to_drop) > 0:
df_clean = df_clean.drop(columns=cols_to_drop)
print(f"已删除缺失比例高的列: {list(cols_to_drop)}")
# 对数值变量使用中位数填充
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if df_clean[col].isnull().sum() > 0:
median_val = df_clean[col].median()
df_clean[col] = df_clean[col].fillna(median_val)
print(f" 用中位数 {median_val:.2f} 填充 {col} 的缺失值")
# 对分类变量使用众数填充
categorical_cols = df_clean.select_dtypes(exclude=[np.number]).columns
for col in categorical_cols:
if df_clean[col].isnull().sum() > 0:
mode_val = df_clean[col].mode()[0]
df_clean[col] = df_clean[col].fillna(mode_val)
print(f" 用众数 '{mode_val}' 填充 {col} 的缺失值")
# 2. 处理异常值
print("\n步骤2: 处理异常值")
print("-" * 50)
# 检测和处理数值变量的异常值
for col in numeric_cols:
if col == 'CustomerID': # 跳过ID列
continue
# 使用IQR方法检测异常值
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df_clean[(df_clean[col] < lower_bound) | (df_clean[col] > upper_bound)]
if len(outliers) > 0:
# 使用封顶法处理异常值
df_clean[col] = np.where(df_clean[col] < lower_bound, lower_bound, df_clean[col])
df_clean[col] = np.where(df_clean[col] > upper_bound, upper_bound, df_clean[col])
print(f" 封顶处理 {col} 的异常值,下限: {lower_bound:.2f}, 上限: {upper_bound:.2f}")
# 3. 数据一致性检查
print("\n步骤3: 数据一致性检查")
print("-" * 50)
# 检查年龄和职业的一致性
invalid_age_occupation = df_clean[
((df_clean['Age'] < 18) & (df_clean['Occupation'].isin(['Employed', 'Self-Employed']))) |
((df_clean['Age'] > 70) & (df_clean['Occupation'] == 'Student'))
]
if len(invalid_age_occupation) > 0:
print(f" 发现 {len(invalid_age_occupation)} 条年龄与职业不一致的记录")
# 标记这些记录以供进一步审查
df_clean['Data_Quality_Flag'] = False
df_clean.loc[invalid_age_occupation.index, 'Data_Quality_Flag'] = True
else:
df_clean['Data_Quality_Flag'] = False
print("数据清洗完成!")
return df_clean
# 应用数据清洗管道
df_cleaned = data_cleaning_pipeline(df_customers)
# 比较清洗前后的数据
print("\n清洗前后对比:")
print("=" * 50)
print("原始数据集形状:", df_customers.shape)
print("清洗后数据集形状:", df_cleaned.shape)
print("\n原始数据集缺失值总数:", df_customers.isnull().sum().sum())
print("清洗后数据集缺失值总数:", df_cleaned.isnull().sum().sum())
# 可视化清洗效果
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
# 年龄分布对比
axes[0, 0].hist(df_customers['Age'].dropna(), bins=20, alpha=0.7, color='red', label='原始')
axes[0, 0].hist(df_cleaned['Age'], bins=20, alpha=0.7, color='green', label='清洗后')
axes[0, 0].set_title('年龄分布对比')
axes[0, 0].legend()
# 收入分布对比
axes[0, 1].hist(df_customers['Income'].dropna(), bins=20, alpha=0.7, color='red', label='原始')
axes[0, 1].hist(df_cleaned['Income'], bins=20, alpha=0.7, color='green', label='清洗后')
axes[0, 1].set_title('收入分布对比')
axes[0, 1].legend()
# 平均消费分布对比
axes[0, 2].hist(df_customers['Avg_Spending'], bins=20, alpha=0.7, color='red', label='原始')
axes[0, 2].hist(df_cleaned['Avg_Spending'], bins=20, alpha=0.7, color='green', label='清洗后')
axes[0, 2].set_title('平均消费分布对比')
axes[0, 2].legend()
# 购买频率箱线图对比
df_boxplot = pd.DataFrame({
'原始': df_customers['Purchase_Frequency'],
'清洗后': df_cleaned['Purchase_Frequency']
})
df_boxplot.boxplot(ax=axes[1, 0])
axes[1, 0].set_title('购买频率箱线图对比')
# 满意度评分对比
satisfaction_original = df_customers['Satisfaction_Score'].value_counts().sort_index()
satisfaction_cleaned = df_cleaned['Satisfaction_Score'].value_counts().sort_index()
axes[1, 1].bar(satisfaction_original.index - 0.2, satisfaction_original.values,
width=0.4, alpha=0.7, color='red', label='原始')
axes[1, 1].bar(satisfaction_cleaned.index + 0.2, satisfaction_cleaned.values,
width=0.4, alpha=0.7, color='green', label='清洗后')
axes[1, 1].set_title('满意度评分分布对比')
axes[1, 1].legend()
# 投诉次数对比
complaints_original = df_customers['Complaints'].value_counts().sort_index()
complaints_cleaned = df_cleaned['Complaints'].value_counts().sort_index()
axes[1, 2].bar(complaints_original.index - 0.2, complaints_original.values,
width=0.4, alpha=0.7, color='red', label='原始')
axes[1, 2].bar(complaints_cleaned.index + 0.2, complaints_cleaned.values,
width=0.4, alpha=0.7, color='green', label='清洗后')
axes[1, 2].set_title('投诉次数分布对比')
axes[1, 2].legend()
plt.tight_layout()
plt.show()
代码解释:
- 创建了一个完整的数据清洗管道函数,包含缺失值处理、异常值处理和数据一致性检查
- 对缺失值:删除缺失比例高的变量(如果存在),数值变量用中位数填充,分类变量用众数填充
- 对异常值:使用IQR方法检测,并用封顶法处理
- 进行数据一致性检查(如年龄与职业的合理性)
- 比较清洗前后的数据分布,可视化展示清洗效果
数据质量评估
清洗完成后,我们需要评估数据质量:
# 数据质量评估报告
def data_quality_report(df):
"""生成数据质量评估报告"""
report = {}
# 基本统计
report['总记录数'] = len(df)
report['总变量数'] = len(df.columns)
# 缺失值统计
missing_stats = df.isnull().sum()
report['缺失值总数'] = missing_stats.sum()
report['有缺失值的变量数'] = len(missing_stats[missing_stats > 0])
# 数据类型分布
dtype_counts = df.dtypes.value_counts()
report['数值变量数'] = dtype_counts.get('int64', 0) + dtype_counts.get('float64', 0)
report['分类变量数'] = len(df.columns) - report['数值变量数']
# 数据质量评分(0-100)
quality_score = 100
# 扣除缺失值影响
missing_penalty = (report['缺失值总数'] / (report['总记录数'] * report['总变量数'])) * 50
quality_score -= missing_penalty
# 异常值影响(粗略估计)
numeric_cols = df.select_dtypes(include=[np.number]).columns
outlier_penalty = 0
for col in numeric_cols:
if col == 'CustomerID': # 跳过ID列
continue
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 3 * IQR
upper_bound = Q3 + 3 * IQR
outliers = df[(df[col] < lower_bound) | (df[col] > upper_bound)]
outlier_ratio = len(outliers) / len(df)
outlier_penalty += outlier_ratio * 10
quality_score -= min(outlier_penalty, 30) # 最多扣30分
report['数据质量评分'] = round(max(quality_score, 0), 2)
return report
# 生成清洗前后的数据质量报告
quality_before = data_quality_report(df_customers)
quality_after = data_quality_report(df_cleaned)
print("数据质量对比报告:")
print("=" * 50)
for metric in quality_before:
print(f"{metric}: {quality_before[metric]} → {quality_after[metric]}")
代码解释:
- 定义数据质量评估函数,从多个维度评估数据质量
- 考虑缺失值、异常值等因素,计算综合数据质量评分(0-100分)
- 比较清洗前后的数据质量,展示清洗效果
通过这个综合案例,我们演示了如何在实际项目中应用数据清洗技巧。接下来,让我们总结本文的主要内容。
V. 总结与最佳实践
通过本文的详细讲解和实战演示,我们已经全面了解了数据清洗中处理缺失值和异常值的各种技巧。让我们总结一下关键要点和最佳实践。
数据清洗的关键要点
- 理解数据:在开始清洗之前,务必充分了解数据的业务背景、含义和特点
- 系统性方法:采用系统化的清洗流程,而不是随意处理数据问题
- 文档记录:详细记录每一步清洗操作和决策原因,便于追溯和复现
- 迭代过程:数据清洗往往是一个迭代过程,可能需要多次循环调整
处理缺失值的最佳实践
场景 | 推荐方法 | 注意事项 |
---|---|---|
缺失比例高(>30%) | 删除变量 | 确保该变量不是关键变量 |
数值变量随机缺失 | 中位数/均值填充 | 考虑使用插值法提高精度 |
分类变量随机缺失 | 众数填充 | 或创建"缺失"类别 |
时间序列缺失 | 时间序列插值 | 考虑序列自相关性 |
非随机缺失 | 模型填充 | 使用预测模型估计缺失值 |
处理异常值的最佳实践
场景 | 推荐方法 | 注意事项 |
---|---|---|
明显错误异常值 | 删除或修正 | 基于业务知识判断 |
极端但可能的值 | 封顶法 | 保留数据点但限制影响 |
需要保留异常值 | 数据转换 | 使用对数或Box-Cox转换 |
多变量异常值 | 多变量检测方法 | 如隔离森林、聚类方法 |
异常值分析 | 单独分析 | 异常值可能包含重要信息 |
数据清洗的常见陷阱
- 过度清洗:删除过多数据导致样本不足或偏差
- 忽略数据关系:未考虑变量间的相关性,导致处理不当
- 忽视业务背景:仅从统计角度处理,忽略业务逻辑和常识
- 不记录处理过程:导致无法追溯和复现清洗步骤
- 一次性处理:认为数据清洗是一次性任务,而非迭代过程
持续数据质量管理
数据清洗不应只是一次性任务,而应建立持续的数据质量管理体系:
- 建立数据质量标准:定义清晰的数据质量维度和阈值
- 自动化检查:开发自动化脚本定期检查数据质量
- 监控关键指标:跟踪关键数据质量指标的变化趋势
- 建立反馈机制:与数据生产部门合作,从源头改善数据质量
- 定期审计:定期进行数据质量全面审计和评估
结语
数据清洗是数据分析过程中至关重要但常被低估的环节。通过本文介绍的方法和技巧,您可以更有效地处理缺失值和异常值,提高数据质量,从而为后续分析奠定坚实基础。
记住,高质量的数据是高质量分析的前提。投资时间在数据清洗上,往往会获得比使用高级算法更大的回报。
希望这篇实战指南对您的数据科学之旅有所帮助!如果您有任何问题或想分享您的数据清洗经验,欢迎在评论区交流。
- 点赞
- 收藏
- 关注作者
评论(0)