4. Pandas 数据清洗实战:处理缺失值、重复值、异常值
数据清洗是数据分析流程中最耗时、最关键的环节。原始数据通常包含缺失值、重复记录、异常数值以及不匹配的数据类型。本章将系统讲解如何使用 Pandas 进行数据清洗,确保数据质量,为后续的 ECharts 图表输出奠定可靠基础。
核心原则:数据清洗的目标不是“删除所有问题数据”,而是根据业务逻辑做出合理处理。清洗策略直接影响分析结论和图表呈现效果。
4.1 数据清洗整体流程
数据清洗通常遵循“先检查、再处理、后验证”的步骤。下图展示了从原始数据到干净数据的标准清洗流水线:
4.2 缺失值检测与处理
缺失值是数据清洗中最常见的问题。Pandas 使用 NaN(Not a Number)表示缺失值。检测缺失值的主要方法:
- isnull() / isna():返回布尔型 DataFrame,标记每个位置是否为缺失值
- notnull():与
isnull()相反 - info():快速查看每列的非空计数和数据类型
4.2.1 缺失值检测示例
import pandas as pd
import numpy as np
# 创建包含缺失值的示例数据
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '钱七'],
'年龄': [25, np.nan, 30, 28, np.nan],
'销售额': [5000, 6200, np.nan, 4800, 7100],
'部门': ['销售', '技术', '销售', np.nan, '技术']
})
print("原始数据:")
print(df)
print("\n缺失值检测(布尔矩阵):")
print(df.isnull())
print("\n每列缺失数量:")
print(df.isnull().sum())
4.2.2 使用 dropna() 删除缺失值
dropna() 用于删除包含缺失值的行或列。关键参数:
| 参数 | 说明 | 示例 |
|---|---|---|
| axis | 0 删除行(默认),1 删除列 | df.dropna(axis=0) |
| how | 'any' 有缺失即删(默认),'all' 全缺失才删 | df.dropna(how='all') |
| thresh | 保留至少有 thresh 个非空值的行/列 | df.dropna(thresh=3) |
| subset | 指定列的子集进行缺失值判断 | df.dropna(subset=['年龄']) |
# 删除包含任何缺失值的行
df_drop_any = df.dropna()
print("删除任何含缺失值的行:")
print(df_drop_any)
# 仅当整行全为缺失时才删除
df_drop_all = df.dropna(how='all')
print("\n仅删除全缺失行:")
print(df_drop_all)
# 保留至少有3个非空值的行
df_thresh = df.dropna(thresh=3)
print("\n保留至少3个非空值的行:")
print(df_thresh)
4.2.3 使用 fillna() 填充缺失值
fillna() 是处理缺失值的核心函数,支持多种填充策略:
| 填充方式 | 参数设置 | 适用场景 |
|---|---|---|
| 固定值填充 | fillna(0) 或 fillna('未知') |
数值列用 0,分类列用占位符 |
| 统计量填充 | fillna(df['列'].mean()) |
数值型数据,分布较均匀 |
| 前向填充 | fillna(method='ffill') |
时间序列,用前一个值填充 |
| 后向填充 | fillna(method='bfill') |
时间序列,用后一个值填充 |
| 插值填充 | interpolate() |
线性趋势明显的数值序列 |
# 固定值填充:年龄缺失用平均年龄填充,部门缺失用'未知'
df_filled = df.copy()
mean_age = df['年龄'].mean()
df_filled['年龄'] = df['年龄'].fillna(mean_age)
df_filled['部门'] = df['部门'].fillna('未知')
print("固定值填充结果:")
print(df_filled)
# 前向填充(ffill)
df_ffill = df.fillna(method='ffill')
print("\n前向填充结果:")
print(df_ffill)
# 不同列使用不同填充策略
df_custom = df.copy()
df_custom['年龄'] = df_custom['年龄'].fillna(df['年龄'].median())
df_custom['销售额'] = df_custom['销售额'].fillna(0)
df_custom['部门'] = df_custom['部门'].fillna(method='bfill')
print("\n自定义填充策略:")
print(df_custom)
实战建议:对于数值型缺失值,优先使用中位数填充(对异常值不敏感);对于分类变量,使用众数填充或单独标记为“未知”类。避免在缺失率超过 30% 的列上使用填充,应考虑删除该列。
4.3 重复值检测与处理
重复数据会导致统计结果偏差,尤其在聚合计算和图表展示中会产生误导。Pandas 提供 duplicated() 和 drop_duplicates() 两个核心函数。
4.3.1 重复值检测
# 创建包含重复值的示例数据
df_dup = pd.DataFrame({
'ID': [101, 102, 103, 101, 104, 102],
'产品': ['A', 'B', 'C', 'A', 'D', 'B'],
'销量': [100, 200, 150, 100, 300, 200],
'日期': ['2024-01-01', '2024-01-02', '2024-01-03',
'2024-01-01', '2024-01-04', '2024-01-02']
})
print("原始数据:")
print(df_dup)
# 检测完全重复的行
print("\n完全重复行标记:")
print(df_dup.duplicated())
# 检测基于指定列的重复
print("\n基于ID列的重复标记:")
print(df_dup.duplicated(subset=['ID']))
# 统计重复数量
print(f"\n完全重复行数:{df_dup.duplicated().sum()}")
print(f"ID重复行数:{df_dup.duplicated(subset=['ID']).sum()}")
4.3.2 使用 drop_duplicates() 删除重复值
| 参数 | 说明 | 示例 |
|---|---|---|
| subset | 指定列判断重复,默认所有列 | drop_duplicates(subset=['ID']) |
| keep | 'first' 保留第一个(默认),'last' 保留最后一个,False 删除所有重复 | drop_duplicates(keep='last') |
| inplace | 是否原地修改 DataFrame | drop_duplicates(inplace=True) |
# 删除完全重复的行(保留第一个)
df_no_dup = df_dup.drop_duplicates()
print("删除完全重复行:")
print(df_no_dup)
# 基于ID列去重,保留最后一条记录
df_by_id = df_dup.drop_duplicates(subset=['ID'], keep='last')
print("\n基于ID去重(保留最后):")
print(df_by_id)
# 删除所有重复行(不保留任何重复)
df_no_dup_all = df_dup.drop_duplicates(keep=False)
print("\n删除所有重复行:")
print(df_no_dup_all)
注意:在删除重复值之前,务必确认重复数据的业务含义。有时重复数据是合理的(如同一用户多次购买),此时应基于业务主键去重,而非简单删除所有重复行。
4.4 异常值检测与处理
异常值(Outlier)是明显偏离其他观测值的数据点。异常值可能由数据录入错误、传感器故障或真实极端事件引起。常用检测方法:
- Z-Score 方法:假设数据服从正态分布,Z-Score 值大于 3 视为异常
- IQR 方法:四分位距法,超出 Q1-1.5×IQR 或 Q3+1.5×IQR 视为异常
- 业务规则:根据领域知识设定合理范围
4.4.1 IQR 方法检测异常值
# 创建包含异常值的示例数据
df_outlier = pd.DataFrame({
'商品': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
'价格': [100, 120, 110, 105, 500, 115, 108, 2000],
'销量': [50, 45, 55, 48, 52, 200, 47, 60]
})
print("原始数据:")
print(df_outlier)
# IQR 方法检测价格列的异常值
Q1 = df_outlier['价格'].quantile(0.25)
Q3 = df_outlier['价格'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f"\n价格列 IQR 分析:")
print(f"Q1 = {Q1}, Q3 = {Q3}, IQR = {IQR}")
print(f"下界 = {lower_bound}, 上界 = {upper_bound}")
# 标记异常值
df_outlier['价格异常'] = (df_outlier['价格'] < lower_bound) | (df_outlier['价格'] > upper_bound)
print("\n异常值标记结果:")
print(df_outlier[df_outlier['价格异常']])
4.4.2 异常值处理策略
| 处理方式 | 方法 | 适用场景 |
|---|---|---|
| 删除 | 直接删除异常值所在行 | 异常值比例小(< 1%),且确认是录入错误 |
| 截尾 | 将异常值替换为边界值(上下界) | 保留数据量,避免极端值影响 |
| 填充 | 用中位数或均值替换异常值 | 异常值比例适中,需要保留样本量 |
| 单独处理 | 将异常值作为独立类别分析 | 异常值本身具有业务意义(如高额交易) |
# 截尾处理:将价格异常值替换为边界值
df_clip = df_outlier.copy()
df_clip['价格'] = df_clip['价格'].clip(lower=lower_bound, upper=upper_bound)
print("截尾处理后的价格列:")
print(df_clip[['商品', '价格']])
# 中位数填充异常值
median_price = df_outlier.loc[~df_outlier['价格异常'], '价格'].median()
df_fill_outlier = df_outlier.copy()
df_fill_outlier.loc[df_fill_outlier['价格异常'], '价格'] = median_price
print("\n中位数填充后的价格列:")
print(df_fill_outlier[['商品', '价格']])
4.5 数据类型转换
数据类型不匹配是数据分析中的常见问题。Pandas 自动推断的数据类型可能不符合实际需求,需要手动转换。
4.5.1 常见数据类型转换场景
| 原始类型 | 目标类型 | 转换方法 | 示例 |
|---|---|---|---|
| object(字符串) | datetime | pd.to_datetime() |
df['日期'] = pd.to_datetime(df['日期']) |
| object(数字字符串) | float / int | pd.to_numeric() |
df['金额'] = pd.to_numeric(df['金额'], errors='coerce') |
| float | int | astype(int) |
df['数量'] = df['数量'].astype(int) |
| int / float | category | astype('category') |
df['等级'] = df['等级'].astype('category') |
# 创建包含类型问题的示例数据
df_types = pd.DataFrame({
'日期': ['2024-01-01', '2024-01-02', '2024-01-03'],
'金额': ['1,200', '2,500', '1,800'],
'数量': [10.0, 20.0, 15.0],
'等级': ['A', 'B', 'A']
})
print("原始数据类型:")
print(df_types.dtypes)
# 字符串转日期
df_types['日期'] = pd.to_datetime(df_types['日期'])
print("\n日期转换后:")
print(df_types['日期'].dtype)
# 带千分位逗号的字符串转数值
df_types['金额'] = pd.to_numeric(
df_types['金额'].str.replace(',', ''),
errors='coerce'
)
print("\n金额转换后:")
print(df_types['金额'].dtype)
# float 转 int
df_types['数量'] = df_types['数量'].astype(int)
print("\n数量转换后:")
print(df_types['数量'].dtype)
# 字符串转分类类型
df_types['等级'] = df_types['等级'].astype('category')
print("\n等级转换后:")
print(df_types['等级'].dtype)
print("\n最终数据类型:")
print(df_types.dtypes)
关键参数:
pd.to_numeric() 的 errors 参数:'raise' 遇到错误抛出异常,'coerce' 将无效值转为 NaN,'ignore' 遇到错误返回原始输入。推荐使用 'coerce' 避免程序中断。
4.6 综合清洗实战案例
以下案例综合运用本章所有知识点,完成一个完整的清洗流程:
# 综合清洗函数
def clean_sales_data(df):
"""
销售数据清洗函数
步骤:缺失值 → 重复值 → 异常值 → 类型转换
"""
df_clean = df.copy()
# 步骤1:缺失值处理
# 数值列用中位数填充,分类列用众数填充
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
df_clean[col] = df_clean[col].fillna(df_clean[col].median())
categorical_cols = df_clean.select_dtypes(include=['object']).columns
for col in categorical_cols:
df_clean[col] = df_clean[col].fillna(df_clean[col].mode()[0])
# 步骤2:重复值处理
df_clean = df_clean.drop_duplicates(subset=['订单ID'], keep='first')
# 步骤3:异常值处理(IQR方法)
for col in numeric_cols:
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# 截尾处理
df_clean[col] = df_clean[col].clip(lower=lower, upper=upper)
# 步骤4:数据类型转换
if '日期' in df_clean.columns:
df_clean['日期'] = pd.to_datetime(df_clean['日期'], errors='coerce')
return df_clean
# 模拟脏数据
dirty_data = pd.DataFrame({
'订单ID': [1001, 1002, 1003, 1001, 1004],
'日期': ['2024-01-01', '2024-01-02', None, '2024-01-01', '2024-01-05'],
'金额': [500, 1200, 800, 500, 50000],
'数量': [10, np.nan, 8, 10, 12]
})
print("脏数据:")
print(dirty_data)
# 执行清洗
clean_data = clean_sales_data(dirty_data)
print("\n清洗后数据:")
print(clean_data)
print("\n清洗后数据类型:")
print(clean_data.dtypes)
4.7 数据清洗前后对比
以下 SVG 图展示数据清洗前后的质量变化:
4.8 本章小结
数据清洗是数据分析的基石,直接影响后续 ECharts 图表输出的准确性和可信度。本章核心要点:
- 缺失值处理:使用
dropna()删除或fillna()填充,填充策略需结合业务场景 - 重复值处理:使用
drop_duplicates()去重,注意subset和keep参数 - 异常值检测:IQR 方法和 Z-Score 方法是最常用的统计检测手段
- 数据类型转换:使用
pd.to_datetime()、pd.to_numeric()、astype()确保数据类型正确 - 清洗顺序:建议按“缺失值 → 重复值 → 异常值 → 类型转换”的顺序执行
工程实践:在实际项目中,建议将清洗逻辑封装为可复用的函数,并记录清洗日志(删除行数、填充值等),便于审计和回溯。清洗后的数据应保存为独立文件,保留原始数据作为备份。