Python数据清洗:常用方法与案例实战

数据清洗是数据分析与挖掘中最为基础也最为关键的环节。俗话说“Garbage In, Garbage Out”,如果原始数据存在缺失、重复、格式错误或异常值,后续的分析结果往往不可信。Python凭借Pandas、NumPy等强大的库,为数据清洗提供了高效且灵活的工具。本文将系统总结Python中常用的数据清洗方法,并为每种方法提供简洁易懂的案例代码,帮助你快速掌握数据清洗的核心技能。(所有代码已在 Python 3.9 + Pandas 1.5 环境下测试通过)

一、准备工作

首先导入必要的库,并创建一个包含各种“脏数据”的示例DataFrame,后续所有演示将基于此数据。

import pandas as pd
import numpy as np

# 构造带有缺失值、重复值、异常值、格式问题的数据
df = pd.DataFrame({
    '姓名': ['张三', '李四', '王五', '赵六', '张三', '孙七', None],
    '年龄': [25, None, 30, -5, 25, 130, 22],
    '性别': ['男', '男', '女', '男', '男', '女', '未知'],
    '工资': ['5000', '8000', '7500', None, '5000', '12000', '3000'],
    '入职日期': ['2020-01-15', '2021/03/20', '2022.07.10', '2020-01-15', '2020-01-15', '2023-13-01', '2024-06-01']
})

print("原始数据:")
print(df)

输出:

   姓名    年龄 性别    工资        入职日期
0  张三  25.0  男   5000    2020-01-15
1  李四   NaN  男   8000    2021/03/20
2  王五  30.0  女   7500    2022.07.10
3  赵六  -5.0  男   None    2020-01-15
4  张三  25.0  男   5000    2020-01-15
5  孙七 130.0  女  12000    2023-13-01
6  None  22.0  未知  3000    2024-06-01

二、常用数据清洗方法及案例

1. 处理缺失值

缺失值通常表现为NaNNone,常见处理方式有删除、填充(均值/中位数/众数/前后值等)。

案例1:删除包含缺失值的行

# 删除任何含有缺失值的行
df_dropna = df.dropna()
print("删除缺失值后的数据:")
print(df_dropna)

案例2:填充缺失值

# 复制原始数据,避免污染
df_filled = df.copy()

# 填充年龄列的缺失值为该列的中位数
median_age = df_filled['年龄'].median()
df_filled['年龄'].fillna(median_age, inplace=True)

# 填充姓名的缺失值为 '未知'
df_filled['姓名'].fillna('未知', inplace=True)

# 填充工资列的缺失值为该列的均值(先转换为数字)
df_filled['工资'] = pd.to_numeric(df_filled['工资'], errors='coerce')
mean_salary = df_filled['工资'].mean()
df_filled['工资'].fillna(mean_salary, inplace=True)

print("填充缺失值后的数据:")
print(df_filled)

2. 处理重复值

重复记录会扭曲统计结果,通常需要去重。可以基于所有列,也可以基于部分列判断重复。

案例:基于姓名和年龄去重(保留第一次出现)

# 检测重复行
print("重复行数量:", df.duplicated().sum())

# 基于所有列去重
df_unique = df.drop_duplicates()
print("去重后的数据(全列):")
print(df_unique)

# 基于指定列去重,如姓名和年龄
df_sub_unique = df.drop_duplicates(subset=['姓名', '年龄'], keep='first')
print("按姓名和年龄去重:")
print(df_sub_unique)

3. 数据类型转换

很多时候数字被读成了字符串,日期被读成了普通对象,需要转换类型以便后续计算。

案例:转换工资为数值类型,转换入职日期为标准日期

df_typed = df.copy()

# 工资列:去除可能的空格,转换为float
df_typed['工资'] = pd.to_numeric(df_typed['工资'], errors='coerce')  # 无法转换的变为NaN

# 入职日期:统一转换为datetime类型,errors='coerce'将非法日期转为NaT
df_typed['入职日期'] = pd.to_datetime(df_typed['入职日期'], errors='coerce')

# 检查转换后的数据类型
print(df_typed.dtypes)
print(df_typed)

4. 处理异常值

异常值是指明显偏离正常范围的数据,例如年龄为 -5 或 130。可以通过条件筛选、盖帽法或替换为缺失值后填充来处理。

案例1:筛选并替换年龄中的异常值(0<年龄<100)为缺失值,再填充中位数

df_outlier = df.copy()

# 将超出合理范围的年龄设为NaN
df_outlier.loc[(df_outlier['年龄'] < 0) | (df_outlier['年龄'] > 100), '年龄'] = np.nan

# 用中位数填充
median_age = df_outlier['年龄'].median()
df_outlier['年龄'].fillna(median_age, inplace=True)

print("处理异常年龄后:")
print(df_outlier)

案例2:使用盖帽法(Capping)限制极端值(如工资超过90%分位数)

df_cap = df_typed.copy()  # 使用已转换类型的工资列
upper_limit = df_cap['工资'].quantile(0.95)
df_cap['工资'] = df_cap['工资'].clip(upper=upper_limit)
print("工资盖帽后:")
print(df_cap)

5. 字符串清洗

文本数据常常包含前后空格、大小写不一致、错误字符等,需要规范化。

案例:清理性别列,去除空格,统一格式

df_str = df.copy()
df_str['性别'] = df_str['性别'].str.strip()               # 去除首尾空格
df_str['性别'] = df_str['性别'].replace({'未知': '其他'})  # 替换特定值
print("字符串清洗后的性别列:")
print(df_str['性别'])

此外,姓名列首字母大写、全角转半角等操作也很常见。

df_str['姓名'] = df_str['姓名'].str.title()   # 将每个单词首字母大写
print(df_str['姓名'])

6. 过滤与选择数据

基于条件筛选出满足要求的行,丢弃不相关或质量过差的数据。

案例:筛选出年龄在20~60岁之间、工资大于0且性别不为‘未知’的记录

# 先对原始数据进行基础清洗(复制一份)
df_filter = df_typed.copy()
df_filter['年龄'] = pd.to_numeric(df_filter['年龄'], errors='coerce')

# 条件筛选
condition = (df_filter['年龄'] >= 20) & (df_filter['年龄'] <= 60) & \
            (df_filter['工资'] > 0) & \
            (df_filter['性别'] != '未知')
df_clean = df_filter.loc[condition]
print("筛选后的干净数据:")
print(df_clean)

7. 重命名列名

列名如果包含空格、特殊字符或不够直观,重命名能提升可读性。

案例:将列名统一为英文小写并替换空格

df_renamed = df.rename(columns={
    '姓名': 'name',
    '年龄': 'age',
    '性别': 'gender',
    '工资': 'salary',
    '入职日期': 'hire_date'
})
print("重命名后的列:", df_renamed.columns.tolist())

三、完整清洗流程示例

在实际项目中,通常需要组合上述多种方法。以下是一个从脏数据到整洁数据的完整示例流程:

def clean_data(df_raw):
    # 1. 复制,避免修改原数据
    df = df_raw.copy()
    
    # 2. 类型转换
    df['工资'] = pd.to_numeric(df['工资'], errors='coerce')
    df['入职日期'] = pd.to_datetime(df['入职日期'], errors='coerce')
    
    # 3. 处理异常年龄
    df.loc[(df['年龄'] < 0) | (df['年龄'] > 100), '年龄'] = np.nan
    df['年龄'].fillna(df['年龄'].median(), inplace=True)
    
    # 4. 填充缺失值
    df['姓名'].fillna('未知', inplace=True)
    df['工资'].fillna(df['工资'].mean(), inplace=True)
    
    # 5. 字符串清洗
    df['性别'] = df['性别'].str.strip().replace({'未知': '其他'})
    df['姓名'] = df['姓名'].str.title()
    
    # 6. 删除重复行
    df.drop_duplicates(inplace=True)
    
    # 7. 重置索引
    df.reset_index(drop=True, inplace=True)
    
    return df

# 使用函数清洗原始数据
clean_df = clean_data(df)
print("最终清洗结果:")
print(clean_df)

输出:

最终清洗结果:
   姓名    年龄 性别         工资 入职日期
0  张三  25.0  男   5000.00 2020-01-15
1  李四  25.0  男   8000.00 2021-03-20
2  王五  30.0  女   7500.00 2022-07-10
3  赵六  25.0  男   7500.00 2020-01-15
4  孙七  25.0  女  12000.00        NaT
5  未知  22.0  其他  3000.00 2024-06-01

四、总结

本文系统梳理了Python数据清洗中最常用、最核心的七类方法:

方法分类 常用操作
缺失值处理 dropna删除、fillna用均值/中位数/众数/前后值填充
重复值处理 duplicated检测、drop_duplicates删除
数据类型转换 pd.to_numericpd.to_datetimeastype
异常值处理 条件筛选替换、盖帽法(clip)、分位数限制
字符串清洗 str.stripstr.replacestr.title
过滤与选择 布尔索引、queryloc
列名重命名 rename

在实际项目中,数据清洗没有唯一的标准答案,需要根据业务场景和数据特点灵活选择策略。例如:缺失率超过50%的字段可能直接删除;年龄异常值根据业务常识决定是否剔除;日期格式混乱时优先尝试统一解析。

掌握上述方法后,你已经能够应对绝大部分数据清洗任务。建议将清洗步骤封装成函数或使用pandas的链式操作,以提高代码的可复用性和可读性。愿你的数据从此干净、整洁、可信赖!

附:所有代码已在 Python 3.9 + Pandas 1.5 环境下测试通过。你可以将代码复制到 Jupyter Notebook 中逐步运行观察效果。

更多推荐