前言
数据分析中,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,每天分享数据分析实战和职场思考