
Python数据预处理完整指南:从混乱到清晰,解决缺失值与异常数据清洗问题
你好,我是源码库的一名技术博主。在数据分析与机器学习的实战中,我踩过最多的“坑”,往往不是来自复杂的模型算法,而是数据预处理这一关。真实世界的数据,就像刚从矿场挖出来的原石,充满了缺失值、异常值和不一致的格式。今天,我就结合自己的实战经验,带你系统性地走一遍用Python进行数据清洗的核心流程,重点攻克“缺失值”和“异常值”这两大顽敌。准备好了吗?我们开始吧。
一、 战前准备:搭建环境与理解数据
工欲善其事,必先利其器。我们首先需要核心的库:pandas 用于数据处理,numpy 提供数学支持,matplotlib 和 seaborn 用于可视化,帮助我们“看见”数据的问题。
pip install pandas numpy matplotlib seaborn scikit-learn
数据加载是第一步。我习惯先用 .info() 和 .describe() 快速浏览数据的全貌,这能立刻暴露出缺失值和数值分布的大致问题。
import pandas as pd
import numpy as np
# 假设我们有一个名为‘sales_data.csv’的数据集
df = pd.read_csv('sales_data.csv')
print("数据形状(行,列):", df.shape)
print("n--- 数据概览(info)---")
print(df.info()) # 查看列类型、非空计数
print("n--- 描述性统计 ---")
print(df.describe()) # 重点关注数值型字段的统计量(均值、标准差、分位数等)
踩坑提示:.info() 显示的非空计数如果小于总行数,那一列就存在缺失值。而 .describe() 中的 `min`、`max` 和 `75%` 分位数如果差距异常,可能就是异常值的信号。
二、 正面攻坚:系统化处理缺失值
缺失值处理没有“一刀切”的黄金法则,必须根据数据含义和业务场景来选择。我的决策流程通常是:识别 -> 分析原因 -> 选择策略。
1. 识别与可视化缺失值
光看数字不够直观,我常用热图来可视化缺失情况,这能快速定位缺失严重的列或是否存在特定模式的缺失。
import seaborn as sns
import matplotlib.pyplot as plt
# 计算缺失比例
missing_ratio = df.isnull().sum() / len(df) * 100
missing_ratio = missing_ratio[missing_ratio > 0].sort_values(ascending=False)
print("缺失比例(>0的列):n", missing_ratio)
# 绘制缺失值热图
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis', yticklabels=False)
plt.title('缺失值分布热图')
plt.show()
2. 五大处理策略实战
策略一:直接删除 —— 当缺失数据极少(如<5%)且随机时,或整行/整列大部分为空时使用。
# 删除任何包含缺失值的行(慎用!可能丢失大量数据)
df_dropped_rows = df.dropna()
# 删除缺失值超过50%的列
threshold = len(df) * 0.5
df_dropped_cols = df.dropna(axis=1, thresh=threshold)
print(f"原始数据形状: {df.shape}")
print(f"删除含缺失行后形状: {df_dropped_rows.shape}")
print(f"删除高缺失列后形状: {df_dropped_cols.shape}")
策略二:固定值填充 —— 适用于类别特征或含义明确的数值。
# 对于‘类别’列,用‘Unknown’填充
df['category'].fillna('Unknown', inplace=True)
# 对于数值列‘age’,用0填充(需谨慎,可能引入偏差)
df['age'].fillna(0, inplace=True)
策略三:统计值填充 —— 最常用的方法,用均值、中位数或众数填充。
# 数值列用中位数填充(对异常值不敏感)
df['income'].fillna(df['income'].median(), inplace=True)
# 类别列用众数填充
df['education'].fillna(df['education'].mode()[0], inplace=True)
策略四:前后值填充 —— 适用于时间序列或有序数据。
# 用前一个有效值填充(ffill),或用后一个值填充(bfill)
df['stock_price'].fillna(method='ffill', inplace=True)
策略五:模型预测填充 —— 高级方法,利用其他特征来预测缺失值。
from sklearn.ensemble import RandomForestRegressor
# 假设我们要填充‘price’列
# 1. 分离出有‘price’值和无‘price’值的数据集
df_with_price = df[df['price'].notnull()]
df_missing_price = df[df['price'].isnull()]
# 2. 选择用于预测的特征(需确保这些特征本身无缺失或已处理)
features = ['area', 'bedrooms', 'year_built']
X_train = df_with_price[features]
y_train = df_with_price['price']
X_predict = df_missing_price[features]
# 3. 训练模型并预测
model = RandomForestRegressor()
model.fit(X_train, y_train)
predicted_prices = model.predict(X_predict)
# 4. 填充回原数据集
df.loc[df['price'].isnull(), 'price'] = predicted_prices
实战感言:模型填充虽然精准,但计算成本高,且可能造成“数据泄漏”的错觉(用未来数据预测过去)。在大多数业务场景中,策略二和策略三的组合已经足够优秀。
三、 精准狙击:检测与处理异常值
异常值就像数据中的“噪音”,会严重扭曲统计分析结果和机器学习模型的性能。我的原则是:先检测,再判断,最后处理。
1. 可视化检测:让异常值无所遁形
# 箱线图是检测异常值的利器
plt.figure(figsize=(8, 5))
sns.boxplot(x=df['transaction_amount'])
plt.title('交易金额箱线图(异常值显示为圆点)')
plt.show()
# 对于多变量,可以用散点图观察
plt.figure(figsize=(8, 5))
plt.scatter(df['customer_age'], df['transaction_amount'], alpha=0.5)
plt.xlabel('客户年龄')
plt.ylabel('交易金额')
plt.title('年龄-金额散点图')
plt.show()
2. 统计方法检测
Z-Score法:假设数据服从正态分布,通常将 |Z| > 3 的数据点视为异常。
from scipy import stats
z_scores = np.abs(stats.zscore(df['transaction_amount']))
outliers_z = df[z_scores > 3]
print(f"Z-Score法检测到异常值数量: {len(outliers_z)}")
IQR(四分位距)法:更稳健,不依赖于正态分布假设。这是我最常用的方法。
Q1 = df['transaction_amount'].quantile(0.25)
Q3 = df['transaction_amount'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_iqr = df[(df['transaction_amount'] upper_bound)]
print(f"IQR法检测到异常值数量: {len(outliers_iqr)}")
print(f"下限: {lower_bound}, 上限: {upper_bound}")
3. 异常值处理策略
删除:确认是错误数据且数量很少时。
df_cleaned = df[(df['transaction_amount'] >= lower_bound) & (df['transaction_amount'] <= upper_bound)]
替换(盖帽法):将超出边界的值替换为边界值。这是处理“真异常但需保留”情况的常用技巧。
df['transaction_amount_capped'] = df['transaction_amount'].clip(lower=lower_bound, upper=upper_bound)
分箱离散化:将连续值转换为类别,可以削弱异常值的影响。
df['amount_bin'] = pd.qcut(df['transaction_amount'], q=5, labels=['很低', '低', '中', '高', '很高'])
踩坑提示:不要盲目删除异常值!在金融反欺诈或设备故障预测中,异常值本身就是关键信号。务必结合业务背景判断它是“脏数据”还是“有价值的信息”。
四、 综合实战:一个完整的清洗流水线
现在,让我们把这些技巧串起来,为一个模拟的客户数据集进行清洗。
def data_cleaning_pipeline(df):
"""一个简单的数据清洗流水线示例"""
df_clean = df.copy()
# 1. 处理缺失值
# 类别列用众数
df_clean['gender'].fillna(df_clean['gender'].mode()[0], inplace=True)
# 数值列用中位数
df_clean['annual_income'].fillna(df_clean['annual_income'].median(), inplace=True)
# 时间序列列用前向填充
df_clean['last_login_days'].fillna(method='ffill', inplace=True)
# 2. 处理异常值(以‘annual_income’为例,使用IQR盖帽法)
Q1 = df_clean['annual_income'].quantile(0.25)
Q3 = df_clean['annual_income'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
df_clean['annual_income'] = df_clean['annual_income'].clip(lower, upper)
# 3. 重置索引(如果进行了删除操作)
df_clean.reset_index(drop=True, inplace=True)
print("清洗完成!")
print(f"原始数据缺失值总数: {df.isnull().sum().sum()}")
print(f"清洗后缺失值总数: {df_clean.isnull().sum().sum()}")
return df_clean
# 应用流水线
cleaned_df = data_cleaning_pipeline(df)
五、 总结与最佳实践
走完这一趟,相信你对数据清洗有了更系统的认识。最后,分享几条我总结的“血泪”经验:
- 永远备份原始数据:任何清洗操作都在副本上进行,这是铁律。
- 理解业务重于技巧:一个字段的缺失或一个异常值,其处理方式必须结合业务逻辑来判断。
- 迭代与记录:数据清洗很少一步到位。记录下你每一步的操作和原因,方便回溯和团队协作。
- 可视化是关键:在清洗前后多画图对比,你的眼睛是最好的异常检测器。
数据预处理是一门艺术,也是数据科学项目成功的基石。希望这篇指南能帮你把混乱的“原石”打磨成可供分析的“美玉”。如果在实践中遇到具体问题,欢迎来源码库社区一起探讨。祝你清洗愉快!

评论(0)