Python数据清洗实战:Pandas处理脏数据的15个技巧

3 阅读5分钟

摘要:真实世界的数据从来都不干净。缺失值、重复行、格式混乱、异常值……本文总结15个Pandas数据清洗的实用技巧,每个都配有真实场景和可运行代码。

准备工作

import pandas as pd
import numpy as np

# 创建一份"脏数据"用于演示
df = pd.DataFrame({
    '姓名': ['张三', ' 李四 ', '王五', '张三', None, '赵六', '钱七'],
    '年龄': [25, 300, 30, 25, 28, -5, None],
    '手机': ['13812345678', '138-1234-5679', '1381234', '13812345678', '13912345680', '13612345681', '13712345682'],
    '邮箱': ['zhang@test.com', 'LISI@Test.COM', 'invalid-email', 'zhang@test.com', None, 'zhao@test.com', 'qian@test.com'],
    '入职日期': ['2023-01-15', '2023/02/20', '20230315', '2023-01-15', '2023.04.10', '2023-05-01', 'not-a-date'],
    '薪资': ['8000', '12,000', '¥15000', '8000', '9000.50', '', '7500'],
})

技巧1:快速了解数据质量

def data_quality_report(df):
    report = pd.DataFrame({
        '数据类型': df.dtypes,
        '非空数量': df.count(),
        '空值数量': df.isnull().sum(),
        '空值占比': (df.isnull().sum() / len(df) * 100).round(1),
        '唯一值数': df.nunique(),
        '重复值数': len(df) - df.nunique(),
    })
    print(f'数据集:{df.shape[0]}行 × {df.shape[1]}列')
    print(f'总空值:{df.isnull().sum().sum()}')
    print(f'完全重复行:{df.duplicated().sum()}')
    return report

data_quality_report(df)

技巧2:处理缺失值

# 查看缺失值分布
print(df.isnull().sum())

# 删除全空行
df.dropna(how='all', inplace=True)

# 数值列用中位数填充(比均值更抗异常值)
df['年龄'].fillna(df['年龄'].median(), inplace=True)

# 分类列用众数填充
df['姓名'].fillna(df['姓名'].mode()[0], inplace=True)

# 前向填充(时间序列常用)
df['某指标'].fillna(method='ffill', inplace=True)

# 条件填充:根据其他列推断
df.loc[(df['薪资'].isna()) & (df['职级'] == '初级'), '薪资'] = 8000

技巧3:去除重复行

# 完全重复
df.drop_duplicates(inplace=True)

# 基于特定列去重(保留第一条)
df.drop_duplicates(subset=['姓名', '手机'], keep='first', inplace=True)

# 基于特定列去重(保留最新的)
df.sort_values('入职日期', ascending=False, inplace=True)
df.drop_duplicates(subset=['姓名'], keep='first', inplace=True)

技巧4:字符串清洗

# 去除前后空格
df['姓名'] = df['姓名'].str.strip()

# 统一大小写
df['邮箱'] = df['邮箱'].str.lower()

# 去除特殊字符
df['手机'] = df['手机'].str.replace(r'[^0-9]', '', regex=True)

# 薪资列:去除货币符号和逗号,转数值
df['薪资'] = (df['薪资']
    .str.replace(r'[¥¥,,]', '', regex=True)
    .str.strip()
    .replace('', np.nan)
    .astype(float))

技巧5:日期格式统一

def parse_date(date_str):
    if pd.isna(date_str):
        return pd.NaT
    for fmt in ['%Y-%m-%d', '%Y/%m/%d', '%Y%m%d', '%Y.%m.%d']:
        try:
            return pd.to_datetime(date_str, format=fmt)
        except (ValueError, TypeError):
            continue
    return pd.NaT

df['入职日期'] = df['入职日期'].apply(parse_date)

技巧6:异常值检测与处理

# IQR方法检测异常值
def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return (series < lower) | (series > upper)

# 标记异常值
df['年龄_异常'] = detect_outliers_iqr(df['年龄'])

# 用边界值替换异常值(截断法)
def clip_outliers(series, lower_pct=0.01, upper_pct=0.99):
    lower = series.quantile(lower_pct)
    upper = series.quantile(upper_pct)
    return series.clip(lower, upper)

df['年龄'] = clip_outliers(df['年龄'])

# 业务规则过滤
df.loc[df['年龄'] < 18, '年龄'] = np.nan
df.loc[df['年龄'] > 65, '年龄'] = np.nan

技巧7:数据类型转换

# 批量转换
df = df.astype({
    '年龄': 'Int64',      # 可空整数类型
    '薪资': 'float64',
})

# 分类变量优化内存
df['部门'] = df['部门'].astype('category')

# 查看内存优化效果
print(f'优化前: {df.memory_usage(deep=True).sum() / 1024:.1f} KB')

技巧8:手机号校验

import re

def validate_phone(phone):
    if pd.isna(phone):
        return False
    phone = str(phone).strip()
    return bool(re.match(r'^1[3-9]\d{9}$', phone))

df['手机有效'] = df['手机'].apply(validate_phone)
invalid_phones = df[~df['手机有效']]
print(f'无效手机号:{len(invalid_phones)}条')

技巧9:邮箱校验

def validate_email(email):
    if pd.isna(email):
        return False
    pattern = r'^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, str(email).strip()))

df['邮箱有效'] = df['邮箱'].apply(validate_email)

技巧10:文本标准化

# 全角转半角
def full_to_half(text):
    if pd.isna(text):
        return text
    result = ''
    for char in str(text):
        code = ord(char)
        if 0xFF01 <= code <= 0xFF5E:
            result += chr(code - 0xFEE0)
        elif code == 0x3000:  # 全角空格
            result += ' '
        else:
            result += char
    return result

df['地址'] = df['地址'].apply(full_to_half)

# 繁简转换(需要opencc)
# pip install opencc-python-reimplemented
# import opencc
# converter = opencc.OpenCC('t2s')
# df['内容'] = df['内容'].apply(converter.convert)

技巧11:合并相似记录

# 模糊匹配去重(比如"张三"和"张 三")
def normalize_name(name):
    if pd.isna(name):
        return name
    return str(name).strip().replace(' ', '')

df['姓名_标准'] = df['姓名'].apply(normalize_name)
df.drop_duplicates(subset=['姓名_标准', '手机'], inplace=True)

技巧12:批量正则提取

# 从地址中提取省市区
address_pattern = r'(?P<省>.+?省|.+?自治区|北京市|上海市|天津市|重庆市)(?P<市>.+?市|.+?自治州)(?P<区>.+?区|.+?县|.+?市)'

extracted = df['地址'].str.extract(address_pattern)
df = pd.concat([df, extracted], axis=1)

技巧13:数据一致性检查

def consistency_check(df):
    issues = []
    
    # 入职日期不能在未来
    future_dates = df[df['入职日期'] > pd.Timestamp.now()]
    if len(future_dates):
        issues.append(f'未来日期:{len(future_dates)}条')
    
    # 薪资不能为负
    negative_salary = df[df['薪资'] < 0]
    if len(negative_salary):
        issues.append(f'负薪资:{len(negative_salary)}条')
    
    # 手机号不能重复(一人一号)
    dup_phones = df[df['手机'].duplicated(keep=False)]
    if len(dup_phones):
        issues.append(f'重复手机号:{len(dup_phones)}条')
    
    return issues

print(consistency_check(df))

技巧14:管道化清洗流程

def clean_pipeline(df):
    return (df
        .pipe(lambda d: d.dropna(how='all'))
        .pipe(lambda d: d.drop_duplicates())
        .assign(
            姓名=lambda d: d['姓名'].str.strip(),
            邮箱=lambda d: d['邮箱'].str.lower().str.strip(),
            手机=lambda d: d['手机'].str.replace(r'[^0-9]', '', regex=True),
            薪资=lambda d: d['薪资'].str.replace(r'[¥¥,]', '', regex=True).astype(float),
            入职日期=lambda d: d['入职日期'].apply(parse_date),
        )
        .query('年龄 >= 18 and 年龄 <= 65')
        .reset_index(drop=True)
    )

df_clean = clean_pipeline(df)

技巧15:清洗报告生成

def cleaning_report(df_before, df_after):
    print('=' * 50)
    print('数据清洗报告')
    print('=' * 50)
    print(f'原始数据:{len(df_before)}行')
    print(f'清洗后:{len(df_after)}行')
    print(f'删除行数:{len(df_before) - len(df_after)}')
    print(f'空值变化:{df_before.isnull().sum().sum()}{df_after.isnull().sum().sum()}')
    print(f'重复行变化:{df_before.duplicated().sum()}{df_after.duplicated().sum()}')
    print('=' * 50)

cleaning_report(df, df_clean)

总结

数据清洗没有银弹,但有套路:

  1. 先看数据质量报告,了解问题全貌
  2. 处理缺失值和重复行
  3. 统一格式(日期、手机、邮箱、大小写)
  4. 检测和处理异常值
  5. 业务规则校验
  6. 用管道化流程保证可复现

记住:数据清洗花的时间通常占整个数据项目的60-80%。把清洗流程写成可复用的函数,是最值得的投资。