Python数据清洗实战:5种最常见的脏数据处理技巧(含完整代码)

0 阅读6分钟

前言

数据分析中,70%的时间都在处理脏数据

这不是夸张。我在几份数据分析岗位工作下来,发现真实业务数据的质量普遍很差——缺失值、异常值、重复行、格式不统一、类型错误,几乎每个项目都有。

今天这篇文章,不讲理论,只讲实战。覆盖5种最常见的脏数据场景,每种都有可以直接复制使用的Python代码。

环境:Python 3.9+,pandas 2.0+


一、缺失值处理(Missing Values)

缺失值是最常见的数据质量问题。处理方式不是只有"删除"和"均值填充",要根据业务场景选择。

1.1 先探索,再处理

import pandas as pd
import numpy as np

df = pd.read_csv('your_data.csv')

# 查看每列缺失值数量和比例
missing_info = pd.DataFrame({
    'missing_count': df.isnull().sum(),
    'missing_ratio': df.isnull().sum() / len(df) * 100
}).sort_values('missing_ratio', ascending=False)

print(missing_info[missing_info['missing_count'] > 0])

输出示例:

                missing_count  missing_ratio
age                      1250          25.0
income                    890          17.8
phone                     234           4.7
email                      89           1.8

1.2 四种填充策略(按场景选择)

# 策略1:数值型 → 用中位数填充(不用均值,避免受极值影响)
df['age'].fillna(df['age'].median(), inplace=True)

# 策略2:分类型 → 用众数填充
df['city'].fillna(df['city'].mode()[0], inplace=True)

# 策略3:时序数据 → 用前向填充(forward fill)
# 适合日期连续的股票、传感器数据
df['price'].fillna(method='ffill', inplace=True)

# 策略4:缺失率 > 30% → 直接删除该列
threshold = 0.3
cols_to_drop = missing_info[missing_info['missing_ratio'] > 30].index.tolist()
df.drop(columns=cols_to_drop, inplace=True)
print(f"删除了 {len(cols_to_drop)} 列: {cols_to_drop}")

# 策略5:行级别删除(关键字段为空的行)
df.dropna(subset=['user_id', 'order_id'], inplace=True)

踩坑提醒:电话、邮箱等字段的缺失,不要随意填充——它们是业务上的"真实缺失",填充会产生错误数据。


二、重复值处理(Duplicates)

重复数据比你想象的更常见,尤其是多系统数据合并后。

2.1 识别重复行

# 全行重复
full_duplicates = df[df.duplicated()]
print(f"完全重复的行:{len(full_duplicates)}")

# 关键字段重复(如同一用户同一天的订单重复上传)
key_duplicates = df[df.duplicated(subset=['user_id', 'order_date'])]
print(f"关键字段重复的行:{len(key_duplicates)}")

# 查看重复的具体内容
print(df[df.duplicated(subset=['user_id', 'order_date'], keep=False)].sort_values('user_id'))

2.2 智能去重(不是简单drop_duplicates)

# 场景:同一用户同一天有多条记录,保留金额最大的那条
df_deduped = df.sort_values('amount', ascending=False).drop_duplicates(
    subset=['user_id', 'order_date'], 
    keep='first'   # 保留排序后的第一条(即金额最大)
)

# 场景:保留最新的记录(按时间戳去重)
df_latest = df.sort_values('created_at', ascending=False).drop_duplicates(
    subset=['user_id'],
    keep='first'
)

print(f"去重前:{len(df)} 行,去重后:{len(df_deduped)} 行")


三、数据类型修复(Type Conversion)

从数据库或CSV读入的数据,类型经常是错的。字符串型的数字、对象型的日期,都会让你的分析出错。

3.1 批量诊断类型问题

# 查看所有列的类型
print(df.dtypes)

# 找出"看起来是数字但实际是字符串"的列
for col in df.select_dtypes(include='object').columns:
    try:
        pd.to_numeric(df[col], errors='raise')
        print(f"{col}: 应该是数值型,当前是object")
    except:
        pass

3.2 常见类型转换代码

# 1. 字符串 → 数值(含脏字符清理)
# 原始数据: "¥1,234.56" → 1234.56
df['amount'] = (
    df['amount']
    .str.replace('¥', '', regex=False)
    .str.replace(',', '', regex=False)
    .astype(float)
)

# 2. 字符串 → 日期(多格式兼容)
df['date'] = pd.to_datetime(df['date'], 
                             format='mixed',  # pandas 2.0+ 自动识别混合格式
                             dayfirst=False)

# 3. 类别型数据(节省内存)
# 当某列的唯一值数量  upper)

# 方法2:Z-score法(适合正态分布数据)
def detect_outliers_zscore(series, threshold=3):
    z_scores = (series - series.mean()) / series.std()
    return z_scores.abs() > threshold

# 方法3:业务规则法(最可靠)
# 年龄:0-120岁合理范围
# 订单金额:0-100万合理范围
age_outliers = (df['age']  120)
amount_outliers = (df['amount']  1_000_000)

print(f"IQR检测异常值: {detect_outliers_iqr(df['amount']).sum()}")
print(f"业务规则检测异常值: {amount_outliers.sum()}")

4.2 异常值处理策略

# 策略1:Winsorize(缩尾处理)——不删数据,只截断极值
# 适合:保留样本量,降低极值影响
from scipy.stats import mstats

df['amount_winsorized'] = mstats.winsorize(df['amount'], limits=[0.01, 0.01])
# 将最低1%和最高1%的值分别替换为第1百分位和第99百分位

# 策略2:直接删除(适合明显录入错误)
df_clean = df[~detect_outliers_iqr(df['amount'])]

# 策略3:分箱处理(转为区间型数据)
df['amount_bin'] = pd.cut(df['amount'], 
                           bins=[0, 100, 500, 1000, 5000, float('inf')],
                           labels=['0-100', '100-500', '500-1000', '1000-5000', '5000+'])


五、字符串标准化(String Normalization)

这是最容易被忽略的脏数据类型。"北京"和"北京市"、"male"和"Male",分析时会被当成不同类别。

5.1 常见字符串清洗操作

# 1. 去除首尾空格(最常见问题)
df['city'] = df['city'].str.strip()

# 2. 统一大小写
df['gender'] = df['gender'].str.lower()
df['status'] = df['status'].str.upper()

# 3. 去除特殊字符
import re
df['name'] = df['name'].str.replace(r'[^\u4e00-\u9fa5a-zA-Z\s]', '', regex=True)

# 4. 统一城市名称(模糊匹配)
city_mapping = {
    '北京市': '北京',
    '上海市': '上海',
    'BJ': '北京',
    'SH': '上海',
}
df['city'] = df['city'].replace(city_mapping)

# 5. 电话号码标准化(去除横线和空格)
df['phone'] = df['phone'].str.replace(r'[\s\-]', '', regex=True)

# 验证手机号格式(中国大陆)
valid_phone_mask = df['phone'].str.match(r'^1[3-9]\d{9}$')
print(f"格式正确的手机号: {valid_phone_mask.sum()}/{len(df)}")

5.2 用fuzzywuzzy处理相似字符串(进阶)

from fuzzywuzzy import process

# 从一组混乱的公司名中,找到标准名称的最佳匹配
standard_companies = ['阿里巴巴', '腾讯', '百度', '字节跳动', '美团']
messy_names = ['阿里', '腾讯公司', '百度科技', '字节', '美团点评']

for name in messy_names:
    match, score = process.extractOne(name, standard_companies)
    if score >= 80:
        print(f"'{name}' → '{match}' (相似度: {score}%)")


六、完整清洗流水线(Pipeline)

把上面5个步骤整合成可复用的清洗函数:

def clean_dataframe(df, config=None):
    """
    通用数据清洗流水线
    config 示例:
    {
        'drop_missing_threshold': 0.3,    # 缺失率超过30%的列删除
        'key_columns': ['user_id'],        # 这些列不能为空
        'numeric_columns': ['age', 'amount'],
        'date_columns': ['created_at'],
        'text_columns': ['city', 'name'],
        'outlier_columns': ['amount'],     # 需要异常值处理的列
    }
    """
    if config is None:
        config = {}
    
    df = df.copy()
    
    # Step 1: 删除缺失率过高的列
    threshold = config.get('drop_missing_threshold', 0.3)
    missing_ratio = df.isnull().sum() / len(df)
    df = df.loc[:, missing_ratio <= threshold]
    
    # Step 2: 删除关键字段为空的行
    key_cols = [c for c in config.get('key_columns', []) if c in df.columns]
    if key_cols:
        df = df.dropna(subset=key_cols)
    
    # Step 3: 数值型转换
    for col in config.get('numeric_columns', []):
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors='coerce')
            df[col].fillna(df[col].median(), inplace=True)
    
    # Step 4: 日期型转换
    for col in config.get('date_columns', []):
        if col in df.columns:
            df[col] = pd.to_datetime(df[col], errors='coerce')
    
    # Step 5: 文本标准化
    for col in config.get('text_columns', []):
        if col in df.columns:
            df[col] = df[col].str.strip().str.lower()
    
    # Step 6: 去除完全重复的行
    df = df.drop_duplicates()
    
    # Step 7: 异常值处理(IQR Winsorize)
    for col in config.get('outlier_columns', []):
        if col in df.columns:
            Q1, Q3 = df[col].quantile([0.25, 0.75])
            IQR = Q3 - Q1
            df[col] = df[col].clip(Q1 - 1.5 * IQR, Q3 + 1.5 * IQR)
    
    print(f"✅ 清洗完成:{len(df)} 行 x {len(df.columns)} 列")
    return df

# 使用示例
config = {
    'drop_missing_threshold': 0.3,
    'key_columns': ['user_id', 'order_id'],
    'numeric_columns': ['age', 'amount', 'quantity'],
    'date_columns': ['created_at', 'updated_at'],
    'text_columns': ['city', 'category'],
    'outlier_columns': ['amount'],
}

df_clean = clean_dataframe(df, config)


总结

5种脏数据场景快速回顾:

📌 缺失值:先看比例,再选策略(中位数/众数/前向填充/删列)

📌 重复值:不能无脑drop_duplicates,要保留"对"的那条

📌 类型错误:字符串型数字、混合格式日期,用专用函数处理

📌 异常值:先判断是否合理,IQR法+业务规则双保险

📌 字符串混乱:标准化三步走:strip + lower + replace统一

数据不干净,结论不可信。花在数据清洗上的时间,永远不是浪费。


作者:CaptainTalk | 数据分析 + 职场真相 + 投资洞察

关注公众号:CaptainTalk,每天分享数据分析实战和职场思考