摘要:真实世界的数据从来都不干净。缺失值、重复行、格式混乱、异常值……本文总结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)
总结
数据清洗没有银弹,但有套路:
- 先看数据质量报告,了解问题全貌
- 处理缺失值和重复行
- 统一格式(日期、手机、邮箱、大小写)
- 检测和处理异常值
- 业务规则校验
- 用管道化流程保证可复现
记住:数据清洗花的时间通常占整个数据项目的60-80%。把清洗流程写成可复用的函数,是最值得的投资。