一、数据清洗概述
数据清洗是数据分析流程中最为关键且耗时最长的环节。真实世界中的数据往往是"脏"的,包含缺失值、重复记录、异常数据、格式不一致等诸多问题。据业界统计,数据科学家通常将约80%的工作时间用于数据清洗和准备工作。Pandas作为Python生态系统中最核心的数据分析库,提供了丰富而高效的数据清洗工具集。
数据清洗的核心目标:
- 完整性: 处理缺失数据,使数据完整可用
- 唯一性: 消除重复记录,避免统计偏差
- 一致性: 统一数据格式和类型,确保可比性
- 准确性: 检测并处理异常值,提高数据质量
- 规范性: 清理字符串空格、特殊字符等格式问题
二、缺失值处理
缺失值是数据清洗中最常见的问题。在Pandas中,缺失值通常用 NaN(Not a Number)表示。处理缺失值前首先需要高效地检测和定位缺失数据。
2.1 缺失值检测
Pandas提供了一系列用于检测缺失值的核心函数:
核心检测方法一览
| 方法 | 功能 | 返回值 |
| isna() | 检测缺失值 | 布尔型DataFrame/Series |
| isnull() | isna的别名,二者等价 | 布尔型DataFrame/Series |
| notna() | 检测非缺失值 | 布尔型DataFrame/Series |
| info() | 概览DataFrame信息 | 列名、非空计数、数据类型等 |
import pandas as pd
import numpy as np
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '钱七'],
'年龄': [28, 35, np.nan, 42, np.nan],
'薪资': [15000, np.nan, 22000, 18000, 25000],
'部门': ['技术部', '市场部', '技术部', np.nan, '财务部']
})
print(df.isna())
姓名 年龄 薪资 部门
0 False False False False
1 False False True False
2 False True False False
3 False False False True
4 False True False False
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5 entries, 0 to 4
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 姓名 5 non-null object
1 年龄 3 non-null float64
2 薪资 4 non-null float64
3 部门 4 non-null object
dtypes: float64(2), object(2)
memory usage: 292.0+ bytes
print(df.isna().sum())
print(df.notna().sum())
姓名 0
年龄 2
薪资 1
部门 1
dtype: int64
姓名 5
年龄 3
薪资 4
部门 4
dtype: int64
2.2 缺失值删除——dropna()
当缺失数据占比很小或缺失行可舍弃时,可直接删除包含缺失值的行或列。
df_clean = df.dropna()
print(df_clean)
df_clean2 = df.dropna(how='all')
df_clean3 = df.dropna(thresh=2)
df_clean4 = df.dropna(subset=['年龄', '薪资'])
df_clean5 = df.dropna(axis=1, how='all')
姓名 年龄 薪资 部门
0 张三 28.0 15000.0 技术部
注:只有第0行没有缺失值,因此dropna()默认只保留了这一行。
2.3 前向填充与后向填充——ffill() / bfill()
在时间序列数据中,缺失值通常使用邻近的数据进行填充。前向填充(ffill)用上一个有效值填充,后向填充(bfill)用下一个有效值填充。
ts_data = pd.Series(
[100, np.nan, np.nan, 120, 130, np.nan, 145],
index=pd.date_range('2024-01-01', periods=7, freq='D')
)
print("前向填充 (ffill):")
print(ts_data.ffill())
print("\n后向填充 (bfill):")
print(ts_data.bfill())
print("\n限制填充步数 (limit=1):")
print(ts_data.ffill(limit=1))
前向填充 (ffill):
2024-01-01 100.0
2024-01-02 100.0
2024-01-03 100.0
2024-01-04 120.0
2024-01-05 130.0
2024-01-06 130.0
2024-01-07 145.0
Freq: D, dtype: float64
后向填充 (bfill):
2024-01-01 100.0
2024-01-02 120.0
2024-01-03 120.0
2024-01-04 120.0
2024-01-05 130.0
2024-01-06 145.0
2024-01-07 145.0
Freq: D, dtype: float64
2.4 插值填充——interpolate()
interpolate()方法利用数学插值算法来填补缺失值,适用于具有一定变化趋势的数据。
ts_linear = ts_data.interpolate(method='linear')
print("线性插值:")
print(ts_linear)
ts_time = ts_data.interpolate(method='time')
print("\n时间插值:")
print(ts_time)
ts_poly = ts_data.interpolate(method='polynomial', order=2)
print("\n多项式插值 (order=2):")
print(ts_poly)
线性插值:
2024-01-01 100.0
2024-01-02 106.7
2024-01-03 113.3
2024-01-04 120.0
2024-01-05 130.0
2024-01-06 137.5
2024-01-07 145.0
Freq: D, dtype: float64
2.5 灵活填充——fillna() 多种策略
fillna()是Pandas中最灵活、最强大的缺失值填充方法,支持常量填充、字典填充、统计值填充等。
df['年龄'] = df['年龄'].fillna(0)
df['年龄'] = df['年龄'].fillna(df['年龄'].mean())
df['薪资'] = df['薪资'].fillna(df['薪资'].median())
df['薪资'] = df.groupby('部门')['薪资'].transform(
lambda x: x.fillna(x.median())
)
fill_values = {'年龄': 30, '薪资': 20000, '部门': '未知'}
df_filled = df.fillna(value=fill_values)
print(df_filled)
姓名 年龄 薪资 部门
0 张三 28.0 15000.0 技术部
1 李四 35.0 20000.0 市场部
2 王五 30.0 22000.0 技术部
3 赵六 42.0 18000.0 未知
4 钱七 30.0 25000.0 财务部
df.fillna(method='ffill', inplace=True)
df.fillna(method='bfill', inplace=True)
df.fillna(method='ffill', limit=2, inplace=True)
三、重复值处理
重复数据会导致统计结果出现偏差,尤其是在聚合计算(求和、计数、均值等)中影响尤为显著。Pandas提供了完整的重复值检测与删除工具。
3.1 重复值检测——duplicated()
df_dup = pd.DataFrame({
'产品': ['A', 'B', 'A', 'C', 'B', 'A'],
'价格': [100, 200, 100, 150, 200, 100],
'数量': [10, 20, 10, 15, 20, 30]
})
print(df_dup.duplicated())
print(df_dup.duplicated(subset=['产品']))
print(df_dup.duplicated(keep='last'))
print(df_dup.duplicated(keep=False))
0 False
1 False
2 True # 行0和行2完全重复
3 False
4 True # 行1和行4完全重复
5 False # 行5价格、产品和行0相同,但数量不同,不算完全重复
dtype: bool
按'产品'列检测:
0 False
1 False
2 True
3 False
4 True
5 True
dtype: bool
3.2 重复值删除——drop_duplicates()
df_unique = df_dup.drop_duplicates()
print("删除完全重复行:")
print(df_unique)
df_unique_subset = df_dup.drop_duplicates(subset=['产品'])
print("\n按'产品'列去重 (保留首次):")
print(df_unique_subset)
df_unique_last = df_dup.drop_duplicates(subset=['产品'], keep='last')
print("\n按'产品'列去重 (保留末次):")
print(df_unique_last)
df_unique_all = df_dup.drop_duplicates(subset=['产品'], keep=False)
print("\n按'产品'列去重 (删除所有重复):")
print(df_unique_all)
df_dup.drop_duplicates(inplace=True)
df_reset = df_dup.drop_duplicates(ignore_index=True)
删除完全重复行:
产品 价格 数量
0 A 100 10
1 B 200 20
3 C 150 15
5 A 100 30
按'产品'列去重 (保留首次):
产品 价格 数量
0 A 100 10
1 B 200 20
3 C 150 15
duplicated()和drop_duplicates()的keep参数对比:
- keep='first'(默认): 标记/保留第一次出现的行,后续重复行视为重复
- keep='last': 标记/保留最后一次出现的行,前面的重复行视为重复
- keep=False: 所有重复行都标记为重复 / 全部删除
四、异常值检测与处理
异常值(Outlier)是指明显偏离其他观测值的数据点。异常值可能由数据录入错误、测量误差或真实的极端情况引起。正确的做法是检测出异常值后,结合业务场景判断是修正还是保留。
4.1 IQR方法(四分位距法)
IQR方法基于数据的四分位数来识别异常值。通常将低于Q1-1.5*IQR或高于Q3+1.5*IQR的数据点视为异常值。
data = pd.Series([10, 12, 11, 13, 100, 9, 11, 12, 10, 500])
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data < lower_bound) | (data > upper_bound)]
print(f"Q1 = {Q1}, Q3 = {Q3}, IQR = {IQR}")
print(f"下界 = {lower_bound}, 上界 = {upper_bound}")
print(f"异常值:\n{outliers}")
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[(series < lower) | (series > upper)]
Q1 = 10.0, Q3 = 12.5, IQR = 2.5
下界 = 6.25, 上界 = 16.25
异常值:
4 100
9 500
dtype: int64
4.2 Z-score方法(标准差法)
Z-score基于正态分布假设,计算每个数据点与均值的标准差距离。通常将Z-score绝对值大于3的数据点视为异常值。
from scipy import stats
z_scores = np.abs(stats.zscore(data))
threshold = 3
outliers_z = data[z_scores > threshold]
print("Z-scores:", z_scores.round(2))
print(f"Z-score方法检测到的异常值:\n{outliers_z}")
mean_val = data.mean()
std_val = data.std()
manual_z = (data - mean_val) / std_val
print("\n手动计算Z-scores:", manual_z.round(2))
Z-scores: [0.32 0.31 0.32 0.31 1.93 0.32 0.32 0.31 0.32 2.24]
Z-score方法检测到的异常值:
Series([], dtype: float64)
注:本例中数据量小且100和500的Z-score未超过3,说明Z-score在数据量较小时对极端值敏感度不如IQR方法。实际应用中建议结合多种方法。
4.3 百分位数法
百分位数法直接指定数据分布的上限和下限百分位,适用于对数据分布有明确业务约束的场景。
def detect_outliers_percentile(series, lower_pct=0.01, upper_pct=0.99):
lower = series.quantile(lower_pct)
upper = series.quantile(upper_pct)
return series[(series < lower) | (series > upper)]
outliers_pct = detect_outliers_percentile(data, 0.1, 0.9)
print("百分位数法(10%-90%)异常值:", outliers_pct.tolist())
lower_10 = data.quantile(0.1)
upper_90 = data.quantile(0.9)
data_capped = data.clip(lower=lower_10, upper=upper_90)
print("截断后:", data_capped.tolist())
百分位数法(10%-90%)异常值: [100.0, 500.0]
截断后: [10.0, 12.0, 11.0, 13.0, 13.0, 9.0, 11.0, 12.0, 10.0, 13.0]
4.4 条件筛选法
结合业务规则进行条件筛选,是最灵活且最贴近实际应用场景的方法。
df_biz = pd.DataFrame({
'员工': ['A', 'B', 'C', 'D', 'E'],
'年龄': [28, 35, 150, 42, -5],
'月薪': [15000, 20000, 999999, 18000, 0],
'入职年份': [2020, 2018, 2023, 2015, 2099]
})
age_rule = (df_biz['年龄'] >= 18) & (df_biz['年龄'] <= 65)
salary_rule = (df_biz['月薪'] >= 2000) & (df_biz['月薪'] <= 100000)
year_rule = df_biz['入职年份'] <= 2026
df_biz['是否异常'] = ~(age_rule & salary_rule & year_rule)
print(df_biz)
df_normal = df_biz[age_rule & salary_rule & year_rule].copy()
print("\n正常数据:")
print(df_normal)
员工 年龄 月薪 入职年份 是否异常
0 A 28 15000 2020 False
1 B 35 20000 2018 False
2 C 150 999999 2023 True # 年龄和月薪异常
3 D 42 18000 2015 False
4 E -5 0 2099 True # 年龄、月薪、入职年份均异常
五、数据替换与条件修改
5.1 replace()——精确替换
replace()用于将DataFrame或Series中的特定值替换为目标值,支持单值替换、列表替换和字典替换。
df_rep = pd.DataFrame({
'城市': ['北京', '上海', '广州', '深圳', 'BJ', 'SH'],
'等级': ['A', 'B', 'C', 'A', 'B+', 'A-'],
'评分': [95, 85, 70, 90, 88, 92]
})
df_rep['城市'] = df_rep['城市'].replace('BJ', '北京')
df_rep['城市'] = df_rep['城市'].replace(['SH', 'GZ'], ['上海', '广州'])
df_rep['等级'] = df_rep['等级'].replace({
'A': '优秀',
'B': '良好',
'C': '一般',
'B+': '良好+',
'A-': '优秀-'
})
df_rep['城市'] = df_rep['城市'].replace(r'[A-Z]+', '其他', regex=True)
df_rep.replace({'城市': {'北京': '首都'}, '评分': {95: 100}})
5.2 mask()与where()——条件替换
mask()将满足条件的值替换为指定值,where()将不满足条件的值替换为指定值。二者互为补充。
s = pd.Series([1, 2, 3, 4, 5, 6])
print("mask (x>3 → 0):")
print(s.mask(s > 3, 0))
print("\nwhere (x<=3 → 0):")
print(s.where(s > 3, 0))
median_salary = df_biz.loc[df_biz['月薪'].between(2000, 100000), '月薪'].median()
df_biz['月薪_修正'] = df_biz['月薪'].mask(
~df_biz['月薪'].between(2000, 100000), median_salary
)
print("\n月薪修正结果:")
print(df_biz[['员工', '月薪', '月薪_修正']])
mask (x>3 → 0):
0 1
1 2
2 3
3 0
4 0
5 0
dtype: int64
where (x<=3 → 0):
0 0
1 0
2 0
3 4
4 5
5 6
dtype: int64
月薪修正结果:
员工 月薪 月薪_修正
0 A 15000 15000.0
1 B 20000 20000.0
2 C 999999 18000.0 # 替换为中位数
3 D 18000 18000.0
4 E 0 18000.0 # 替换为中位数
六、字符串空格清理
从外部导入的文本数据往往包含多余的空格、换行符或制表符。Pandas的str访问器提供了便捷的字符串清理方法。
6.1 strip()系列方法
df_str = pd.DataFrame({
'姓名': [' 张三 ', ' 李四', '王五 ', ' 赵 六 '],
'邮箱': ['zhangsan@test.com\n', ' lisi@test.com', 'wangwu@test.com', 'zhaoliu@test.com '],
'备注': [' 正常 ', ' 异常\t', '待确认\n', ' 已完成 ']
})
df_str['姓名'] = df_str['姓名'].str.strip()
df_str['邮箱'] = df_str['邮箱'].str.strip()
df_str['备注'] = df_str['备注'].str.strip()
df_str['姓名无空格'] = df_str['姓名'].str.replace(' ', '')
print(df_str)
str_cols = df_str.select_dtypes(include=['object']).columns
for col in str_cols:
df_str[col] = df_str[col].str.strip()
姓名 邮箱 姓名无空格
0 张三 zhangsan@test.com 张三
1 李四 lisi@test.com 李四
2 王五 wangwu@test.com 王五
3 赵 六 zhaoliu@test.com 赵六
df_str['邮箱'] = df_str['邮箱'].str.strip().str.lower()
df_str['备注'] = df_str['备注'].str.replace(r'\s+', ' ', regex=True)
df_str['姓名'] = df_str['姓名'].replace('', np.nan)
print("空字符串统计:")
print((df_str == '').sum())
七、数据类型转换
数据类型不匹配是数据分析中常见的"隐形"问题。错误的类型会导致运算异常、排序错误、内存浪费等问题。
7.1 astype()——通用类型转换
df_type = pd.DataFrame({
'ID': ['1001', '1002', '1003', '1004'],
'年龄': ['28', '35', '42', '29'],
'价格': ['99.9', '150.5', '200.0', '88.8'],
'活跃': ['是', '否', '是', '是']
})
print("转换前的类型:")
print(df_type.dtypes)
df_type['ID'] = df_type['ID'].astype('int')
df_type['年龄'] = df_type['年龄'].astype('int')
df_type['价格'] = df_type['价格'].astype('float')
df_type['活跃'] = df_type['活跃'].astype('category')
cat_order = pd.CategoricalDtype(categories=['是', '否'], ordered=True)
df_type['活跃'] = df_type['活跃'].astype(cat_order)
print("\n转换后的类型:")
print(df_type.dtypes)
print(f"\n内存占用: {df_type.memory_usage(deep=True)}")
转换前的类型:
ID object
年龄 object
价格 object
活跃 object
dtype: object
转换后的类型:
ID int64
年龄 int64
价格 float64
活跃 category
dtype: object
7.2 to_numeric()——智能数值转换
相比astype(),to_numeric()能智能处理各种数值格式字符串,遇到无法转换的值时可选择报错、忽略或强制转为NaN。
s_messy = pd.Series(['100', '200.5', '$300', '400元', 'N/A', '五佰'])
s_clean = pd.to_numeric(s_messy, errors='coerce')
print("errors='coerce':")
print(s_clean)
try:
pd.to_numeric(s_messy, errors='raise')
except Exception as e:
print(f"\nerrors='raise' 抛出异常: {e}")
s_ignore = pd.to_numeric(s_messy, errors='ignore')
print("\nerrors='ignore':")
print(s_ignore)
s_downcast = pd.to_numeric(s_clean, downcast='integer')
print(f"\ndowncast后类型: {s_downcast.dtype}")
errors='coerce':
0 100.0
1 200.5
2 NaN
3 NaN
4 NaN
5 NaN
dtype: float64
errors='ignore':
0 100
1 200.5
2 $300
3 400元
4 N/A
5 五佰
dtype: object
7.3 to_datetime()——智能日期转换
日期格式的多样性是数据清洗中的常见挑战。Pandas的to_datetime()能够自动识别绝大多数常见日期格式。
dates = pd.Series([
'2024-01-15',
'2024/02/20',
'03-15-2024',
'2024年4月10日',
'2024.05.01',
'2024-06-30 14:30:00'
])
dates_parsed = pd.to_datetime(dates)
print("解析后的日期:")
print(dates_parsed)
dates_format = pd.to_datetime(dates, format='mixed')
dates_messy = pd.Series([
'2024-01-15',
'not-a-date',
'2024-13-01',
'2024-02-30'
])
dates_clean = pd.to_datetime(dates_messy, errors='coerce')
print("\n无效日期处理 (errors='coerce'):")
print(dates_clean)
df_date = pd.DataFrame({
'date': dates_parsed,
'year': dates_parsed.dt.year,
'month': dates_parsed.dt.month,
'day': dates_parsed.dt.day,
'dayofweek': dates_parsed.dt.dayofweek,
'quarter': dates_parsed.dt.quarter
})
print("\n日期组件提取:")
print(df_date)
解析后的日期:
0 2024-01-15 00:00:00
1 2024-02-20 00:00:00
2 2024-03-15 00:00:00
3 2024-04-10 00:00:00
4 2024-05-01 00:00:00
5 2024-06-30 14:30:00
dtype: datetime64[ns]
无效日期处理 (errors='coerce'):
0 2024-01-15
1 NaT
2 NaT
3 2024-02-29 # 2024年是闰年,2月29日有效!
dtype: datetime64[ns]
日期清洗最佳实践:
- 先使用errors='coerce'安全转换,避免程序中断
- 转换后检查NaT的数量,评估数据质量
- 对于已知格式,指定format参数可极大提高解析速度(10倍以上)
- 利用dt访问器可以方便地提取年、月、日、星期等组件
八、综合实战案例:完整数据清洗流水线
下面展示一个从CSV文件读取脏数据到输出干净数据的完整流水线:
import pandas as pd
import numpy as np
raw_data = pd.DataFrame({
'姓名': [' 张三 ', '李四', ' 王五 ', '赵六', ' 钱七 ', '张三'],
'年龄': ['28', '35', 'N/A', '-5', '42', '28'],
'月薪': ['15000', '20000', 'N/A', '999999', '25000', '15000'],
'入职日期': ['2020-01-15', '2021/03/20', '无效日期', '2019/06/01', '2022.11.30', '2020-01-15'],
'部门': ['技术部', ' 市场部 ', '技术部', '财务部', '市场部', '技术部']
})
df = raw_data.copy()
print(f"原始数据: {df.shape}")
str_cols = df.select_dtypes(include=['object']).columns
for col in str_cols:
df[col] = df[col].str.strip()
df = df.dropna(how='all').drop_duplicates()
print(f"去重后: {df.shape}")
df['年龄'] = pd.to_numeric(df['年龄'], errors='coerce')
df['月薪'] = pd.to_numeric(df['月薪'], errors='coerce')
df['入职日期'] = pd.to_datetime(df['入职日期'], errors='coerce')
median_age = df.loc[df['年龄'].between(18, 65), '年龄'].median()
median_salary = df.loc[df['月薪'].between(2000, 100000), '月薪'].median()
df['年龄'] = df['年龄'].mask(
~df['年龄'].between(18, 65), median_age
)
df['月薪'] = df['月薪'].mask(
~df['月薪'].between(2000, 100000), median_salary
)
df['年龄'] = df['年龄'].fillna(median_age)
df['月薪'] = df['月薪'].fillna(median_salary)
df = df.reset_index(drop=True)
print(f"\n最终清洗结果:")
print(df)
print(f"\n数据类型:\n{df.dtypes}")
原始数据: (6, 5)
去重后: (5, 5) # 删除了一条完全重复的"张三"记录
最终清洗结果:
姓名 年龄 月薪 入职日期 部门
0 张三 28.0 15000.0 2020-01-15 技术部
1 李四 35.0 20000.0 2021-03-20 市场部
2 王五 28.0 20000.0 NaT 技术部
3 赵六 42.0 20000.0 2019-06-01 财务部
4 钱七 42.0 25000.0 2022-11-30 市场部
数据类型:
姓名 object
年龄 float64
月薪 float64
入职日期 datetime64[ns]
部门 object
dtype: object
九、核心要点总结
- 缺失值处理三步骤: 先检测(isna/isnull/notna/info),再分析缺失模式,最后选择合适的填充策略(dropna/ffill/bfill/interpolate/fillna)
- 重复值管理: duplicated()用于检测,drop_duplicates()用于删除,subset参数控制检测列范围,keep参数控制保留策略
- 异常值检测三大方法: IQR法(基于四分位距,适合偏态分布)、Z-score法(基于正态分布,适合大样本)、百分位数法(基于业务规则,最灵活)
- 替换与条件修改: replace()用于精确值替换,mask()满足条件时替换,where()不满足条件时替换
- 字符串清理: strip()去除首尾空格,结合str访问器进行批量正则清理
- 类型转换铁三角: astype()通用转换、to_numeric()智能数值转换、to_datetime()智能日期转换,errors='coerce'是安全转换的关键参数
- 完整流水线: 备份原数据 -> 去空格 -> 去重 -> 类型转换 -> 异常值修正 -> 缺失值填充 -> 重置索引
- 不修改原数据原则: 清洗前先备份原始数据,保证数据可溯源
十、进一步学习建议
推荐学习路径
- 基础夯实: 熟练掌握本文介绍的每种方法,用真实数据集反复练习
- 进阶技巧: 学习pandas-profiling / ydata-profiling自动生成数据质量报告
- 可视化辅助: 结合matplotlib/seaborn绘制箱线图、直方图辅助异常值判断
- 大数据场景: 研究Dask、Modin等并行计算框架中的数据处理方法
- 自动化流水线: 将清洗逻辑封装为可复用的函数或类,构建自动化ETL流程
常用Pandas数据清洗速查表
| 任务 | 推荐方法 |
| 检测缺失值 | df.isna().sum() |
| 删除缺失行 | df.dropna() |
| 填补缺失值 | df.fillna(value) |
| 前向填充 | df.ffill() / df.fillna(method='ffill') |
| 线性插值 | df.interpolate() |
| 检测重复行 | df.duplicated() |
| 删除重复行 | df.drop_duplicates() |
| 替换特定值 | df.replace(old, new) |
| 条件替换 | df.mask(cond, value) / df.where(cond, value) |
| 去除首尾空格 | df['col'].str.strip() |
| 转换数值类型 | pd.to_numeric(df['col'], errors='coerce') |
| 转换日期类型 | pd.to_datetime(df['col'], errors='coerce') |
| 箱线图异常检测 | df.boxplot(column=['col']) |