Python数据清洗之缺失值处理,重复项去重与异常值检测详解
本节学习目标
- 理解缺失值的类型与成因
- 掌握缺失值的检测、填充与删除方法
- 学会识别和删除重复数据
- 掌握 IQR(四分位距)法检测异常值
- 建立数据清洗的标准化流程思维
为什么学这个?
在真实的数据分析工作中,有一句著名的格言:“Garbage in, garbage out”(垃圾进,垃圾出)。
无论你的模型多高级、分析多精巧,如果输入的数据质量有问题,得出的结论就一定是错的。
举个真实的例子:某电商公司的数据分析师在分析年度销售额时,忘记处理缺失值,结果系统把缺失值(NaN)当成了 0 参与计算,最终报告显示年度销售额比实际少了 30%。这个错误直接导致公司在年末促销预算上做出了错误的决策。
根据行业统计,数据科学家 80% 的时间都花在数据清洗上。掌握数据清洗技能,是每一个数据分析师的"基本功"。
数据清洗主要解决三大问题:
- 缺失值 —— 数据"缺了一块"
- 重复值 —— 数据"多了一份"
- 异常值 —— 数据"出了圈"
核心知识点讲解
缺失值(Missing Values)
1. 缺失值是什么?
在 Pandas 中,缺失值用 NaN(Not a Number)表示,它是 NumPy 定义的一个特殊浮点值。
常见的缺失值来源:
- 用户填写表单时漏填了某些字段
- 数据采集设备故障导致数据丢失
- 数据库迁移过程中数据不完整
- 某些测量根本无法获取(如"离职员工的离职后绩效")
import pandas as pd
import numpy as np
# 创建包含缺失值的数据
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '孙七'],
'年龄': [25, np.nan, 28, 35, np.nan], # np.nan 表示缺失值
'工资': [15000.0, 20000.0, np.nan, 25000.0, 12000.0],
'城市': ['北京', '上海', None, '深圳', '杭州'], # None 也会被转为 NaN
'部门': ['技术', '技术', '销售', '管理', '技术']
})
print(df)
# 输出:
# 姓名 年龄 工资 城市 部门
# 0 张三 25.0 15000.0 北京 技术
# 1 李四 NaN 20000.0 上海 技术
# 2 王五 28.0 NaN NaN 销售
# 3 赵六 35.0 25000.0 深圳 管理
# 4 孙七 NaN 12000.0 杭州 技术
2. 检测缺失值
# ===== isnull() 和 notnull() =====
# 检测每个单元格是否为缺失值(返回布尔 DataFrame)
print(df.isnull())
# 输出(True 表示缺失):
# 姓名 年龄 工资 城市 部门
# 0 False False False False False
# 1 False True False False False
# 2 False False True True False
# 3 False False False False False
# 4 False True False False False
# 检测非缺失值
print(df.notnull()) # 与 isnull() 相反
# ===== 统计每列的缺失值数量(最常用的检查命令)=====
print(df.isnull().sum())
# 输出:
# 姓名 0
# 年龄 2
# 工资 1
# 城市 1
# 部门 0
# dtype: int64
# ===== 统计缺失值占比 =====
missing_pct = df.isnull().mean() * 100
print("缺失值占比:")
print(missing_pct)
# 输出:
# 姓名 0.0
# 年龄 40.0
# 工资 20.0
# 城市 20.0
# 部门 0.0
# dtype: float64
# ===== 查看哪些行有缺失值 =====
rows_with_na = df[df.isnull().any(axis=1)]
print("有缺失值的行:")
print(rows_with_na)
# ===== 查看完整数据(无任何缺失的行)=====
complete_rows = df.dropna()
print("完整数据行:")
print(complete_rows)
3. 缺失值处理策略
处理缺失值有两大类策略:删除法和填充法。
策略一:删除法(dropna)
# 创建示例数据
df = pd.DataFrame({
'A': [1, 2, np.nan, 4, 5],
'B': [np.nan, 2, 3, np.nan, 5],
'C': [1, 2, 3, 4, 5]
})
# 默认:删除包含任何缺失值的行
print(df.dropna())
# 只保留第 1 行和第 4 行(索引 1 和 4)
# how='all':只删除全部为缺失值的行
df2 = pd.DataFrame({
'A': [1, np.nan, np.nan],
'B': [2, np.nan, 3]
})
print(df2.dropna(how='all')) # 不会删除任何行
# thresh=N:保留至少有 N 个非缺失值的行
print(df.dropna(thresh=2)) # 保留至少有 2 个非缺失值的行
# subset=:只检查指定列的缺失值
df3 = pd.DataFrame({
'姓名': ['张三', '李四', '王五'],
'年龄': [25, np.nan, 28],
'备注': [np.nan, np.nan, '好学生']
})
print(df3.dropna(subset=['年龄'])) # 只看"年龄"列,删除李四
# axis=1:删除包含缺失值的列(纵向删除)
print(df.dropna(axis=1)) # 删除 B 列(因为它有缺失值)
策略二:填充法(fillna)
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '孙七'],
'年龄': [25, np.nan, 28, np.nan, 22],
'工资': [15000.0, 20000.0, np.nan, 25000.0, np.nan]
})
# ===== 用固定值填充 =====
print(df.fillna(0)) # 所有缺失值填 0
print(df.fillna('未知')) # 所有缺失值填"未知"
# ===== 用统计值填充 =====
# 用均值填充(数值列)
df_filled_mean = df.fillna(df.mean(numeric_only=True))
print("均值填充:")
print(df_filled_mean)
# 用中位数填充(更稳健,不受极端值影响)
df_filled_median = df.fillna(df.median(numeric_only=True))
print("\n中位数填充:")
print(df_filled_median)
# 用众数填充(适合分类数据)
df_filled_mode = df.fillna(df.mode().iloc[0])
print("\n众数填充:")
print(df_filled_mode)
# ===== 前向填充和后向填充 =====
df_time = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=5),
'温度': [20.0, np.nan, np.nan, 25.0, np.nan]
})
# ffill(forward fill):用前一个值填充
print(df_time['温度'].ffill())
# 输出:[20.0, 20.0, 20.0, 25.0, 25.0]
# bfill(backward fill):用后一个值填充
print(df_time['温度'].bfill())
# 输出:[20.0, 25.0, 25.0, 25.0, NaN]
# ===== 按列用不同方式填充 =====
fill_values = {'年龄': df['年龄'].median(), '工资': df['工资'].mean()}
df_filled = df.fillna(fill_values)
print("按列不同方式填充:")
print(df_filled)
# ===== inplace=True:原地修改 =====
# df.fillna(0, inplace=True) # 直接修改原 DataFrame
4. 缺失值处理的选择指南
| 缺失比例 | 建议策略 | 说明 |
|---|---|---|
| < 5% | 直接删除 | 影响微乎其微 |
| 5% ~ 30% | 填充(均值/中位数/众数/插值) | 保留样本量 |
| > 30% | 删除该列或深入研究 | 数据可能不可靠 |
重复值处理
1. 检测重复值
# 创建包含重复值的数据
df = pd.DataFrame({
'订单号': ['ORD001', 'ORD002', 'ORD003', 'ORD002', 'ORD004'],
'客户': ['张三', '李四', '王五', '李四', '赵六'],
'金额': [500, 300, 800, 300, 200]
})
print("原始数据:")
print(df)
# 输出:
# 订单号 客户 金额
# 0 ORD001 张三 500
# 1 ORD002 李四 300
# 2 ORD003 王五 800
# 3 ORD002 李四 300 ← 与第1行完全相同
# 4 ORD004 赵六 200
# ===== duplicated() —— 检测重复行 =====
print(df.duplicated())
# 输出:[False, False, False, True, False]
# 第 4 行(索引3)被标记为 True,因为它是重复的
# 检测哪些列组合是重复的
print(df.duplicated(subset=['订单号']))
# 只看订单号列,ORD002 出现两次,第二次标记为 True
# ===== 统计重复行数量 =====
print(f"重复行数:{df.duplicated().sum()}") # 1
# ===== 查看所有重复行 =====
print(df[df.duplicated(keep=False)])
# keep=False 会标记所有重复的行(而不仅是后面的)
2. 删除重复值
# ===== drop_duplicates() —— 删除重复行 =====
# 默认:删除完全相同的行,保留第一条
df_clean = df.drop_duplicates()
print("删除完全重复的行:")
print(df_clean)
# keep='last':保留最后一条
df_clean2 = df.drop_duplicates(keep='last')
print("\n保留最后一条:")
print(df_clean2)
# keep=False:删除所有重复行(一条都不保留)
df_clean3 = df.drop_duplicates(keep=False)
print("\n全部删除:")
print(df_clean3)
# subset=:只根据指定列判断重复
df2 = pd.DataFrame({
'订单号': ['ORD001', 'ORD002', 'ORD003', 'ORD004'],
'客户': ['张三', '李四', '张三', '赵六'],
'金额': [500, 300, 600, 200] # 金额不同
})
# 只看"客户"列去重
df_unique = df2.drop_duplicates(subset=['客户'])
print("按客户去重:")
print(df_unique) # 张三只保留第一条
# inplace=True 原地修改
# df.drop_duplicates(inplace=True)
异常值(Outliers)检测与处理
1. 什么是异常值?
异常值是指显著偏离其他观测值的数据点。
举例:一个班级的学生年龄大多是 18~22 岁,但有一条记录是 85 岁。这可能是数据录入错误(实际应该是 18 岁),也可能是真实情况(确实有一位 85 岁的学生)。
异常值不一定是"错误数据",但它值得你特别关注。
2. IQR(四分位距)法检测异常值
IQR 法是最常用的异常值检测方法,它基于四分位数。
原理:
- Q1(第一四分位数):25% 的数据小于此值
- Q3(第三四分位数):75% 的数据小于此值
- IQR = Q3 - Q1
- 下界 = Q1 - 1.5 × IQR
- 上界 = Q3 + 1.5 × IQR
- 超出上下界的数据即为异常值
# 创建示例数据
np.random.seed(42)
salaries = np.concatenate([
np.random.normal(15000, 3000, 100), # 正常工资分布
[50000, 60000, -5000] # 异常值
])
df = pd.DataFrame({'工资': salaries})
# ===== IQR 法 =====
Q1 = df['工资'].quantile(0.25)
Q3 = df['工资'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f"Q1 = {Q1:.2f}")
print(f"Q3 = {Q3:.2f}")
print(f"IQR = {IQR:.2f}")
print(f"下界 = {lower_bound:.2f}")
print(f"上界 = {upper_bound:.2f}")
# 找出异常值
outliers = df[(df['工资'] < lower_bound) | (df['工资'] > upper_bound)]
print(f"\n异常值数量:{len(outliers)}")
print("异常值:")
print(outliers)
# ===== 处理异常值 =====
# 方法一:删除异常值
df_clean = df[(df['工资'] >= lower_bound) & (df['工资'] <= upper_bound)]
print(f"删除后数据量:{len(df_clean)}")
# 方法二:用边界值替换(Winsorization/盖帽法)
df_capped = df.copy()
df_capped.loc[df_capped['工资'] > upper_bound, '工资'] = upper_bound
df_capped.loc[df_capped['工资'] < lower_bound, '工资'] = lower_bound
print(f"盖帽后最大值:{df_capped['工资'].max():.2f}")
print(f"盖帽后最小值:{df_capped['工资'].min():.2f}")
# 方法三:用中位数替换
df_median = df.copy()
df_median.loc[
(df_median['工资'] < lower_bound) | (df_median['工资'] > upper_bound),
'工资'
] = df_median['工资'].median()
3. Z-Score 法(标准差法)
Z-Score 法适用于近似正态分布的数据。
# Z-Score = (x - 均值) / 标准差
# |Z| > 3 的数据通常被视为异常值
from scipy import stats
# 计算 Z-Score
z_scores = np.abs(stats.zscore(df['工资']))
# 找出异常值
outlier_mask = z_scores > 3
outliers_z = df[outlier_mask]
print(f"Z-Score 法检测到的异常值:{len(outliers_z)} 个")
print(outliers_z)
4. 异常值可视化(箱线图)
import matplotlib.pyplot as plt
# 箱线图是观察异常值最直观的方式
plt.figure(figsize=(8, 6))
df['工资'].plot(kind='box')
plt.title('工资分布箱线图')
plt.ylabel('工资')
plt.grid(True, alpha=0.3)
plt.show()
# 箱线图中,超出"须线"的点就是潜在的异常值
数据清洗的标准化流程
在实际工作中,建议按照以下流程进行数据清洗:
def data_cleaning_pipeline(df):
"""
标准数据清洗流程
"""
print("=" * 50)
print("数据清洗报告")
print("=" * 50)
# 第1步:查看数据概况
print(f"\n1. 数据形状:{df.shape}")
print(f" 列名:{list(df.columns)}")
# 第2步:检查缺失值
missing = df.isnull().sum()
missing_pct = (missing / len(df)) * 100
missing_summary = pd.DataFrame({
'缺失数量': missing,
'缺失比例(%)': missing_pct.round(2)
})
print("\n2. 缺失值统计:")
print(missing_summary[missing_summary['缺失数量'] > 0])
# 第3步:检查重复值
dup_count = df.duplicated().sum()
print(f"\n3. 重复行数:{dup_count}")
# 第4步:数值列基本统计
print("\n4. 数值列统计摘要:")
print(df.describe())
# 第5步:数据类型
print("\n5. 数据类型:")
print(df.dtypes)
print("\n" + "=" * 50)
print("检查完成,请根据报告决定清洗策略")
print("=" * 50)
return df
# 使用示例
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '孙七', '张三'], # 张三重复
'年龄': [25, np.nan, 28, 35, np.nan, 25],
'工资': [15000.0, 20000.0, np.nan, 25000.0, 12000.0, 15000.0],
'部门': ['技术', '技术', '销售', '管理', '技术', '技术']
})
data_cleaning_pipeline(df)
实战练习
练习 1:缺失值处理实战
题目:处理以下包含缺失值的学生成绩数据。
# 参考答案
import pandas as pd
import numpy as np
df = pd.DataFrame({
'学号': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010],
'姓名': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
'语文': [85, np.nan, 78, 92, np.nan, 88, 76, np.nan, 91, 84],
'数学': [90, 85, np.nan, 88, 92, np.nan, 78, 95, np.nan, 86],
'英语': [78, 92, 85, np.nan, 88, 76, np.nan, 82, 95, np.nan]
})
# 1. 检查缺失值
print("缺失值统计:")
print(df.isnull().sum())
# 2. 各科用中位数填充
for col in ['语文', '数学', '英语']:
df[col] = df[col].fillna(df[col].median())
print("\n填充后数据:")
print(df)
# 3. 验证填充结果
print("\n验证:还有缺失值吗?", df.isnull().sum().sum() == 0)
练习 2:去重与异常值检测
题目:以下是一份电商交易数据,包含重复记录和异常价格。
# 参考答案
import pandas as pd
import numpy as np
df = pd.DataFrame({
'订单号': ['A001', 'A002', 'A003', 'A002', 'A004', 'A005', 'A005', 'A006'],
'商品': ['手机', '电脑', '平板', '电脑', '耳机', '手表', '手表', '充电器'],
'价格': [3999, 5999, 2999, 5999, 299, 15000, 15000, -50],
'数量': [1, 1, 2, 1, 3, 1, 1, 5]
})
# 1. 去除完全重复的行
print("原始行数:", len(df))
df_clean = df.drop_duplicates()
print("去重后行数:", len(df_clean))
# 2. 检测异常价格
Q1 = df_clean['价格'].quantile(0.25)
Q3 = df_clean['价格'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
print(f"\n价格正常范围:[{lower:.0f}, {upper:.0f}]")
abnormal = df_clean[(df_clean['价格'] < lower) | (df_clean['价格'] > upper)]
print("异常价格记录:")
print(abnormal)
# 3. 修正异常值:负价格改为绝对值,过高价格用中位数替换
df_clean = df_clean.copy()
df_clean.loc[df_clean['价格'] < 0, '价格'] = df_clean.loc[df_clean['价格'] < 0, '价格'].abs()
median_price = df_clean['价格'].median()
df_clean.loc[
(df_clean['价格'] < lower) | (df_clean['价格'] > upper),
'价格'
] = median_price
print("\n修正后数据:")
print(df_clean)
练习 3:完整清洗流程
题目:对以下"脏"数据进行完整清洗。
# 参考答案
import pandas as pd
import numpy as np
# 模拟脏数据
df = pd.DataFrame({
'ID': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 2], # ID=2 重复
'年龄': [25, 30, 28, 150, 22, np.nan, 35, 27, -5, 29, 30], # 150和-5是异常值
'收入': [5000, 8000, np.nan, 12000, 6000, 7000, np.nan, 9000, 5500, 7500, 8000],
'性别': ['男', '女', '男', '女', 'M', '男', '女', np.nan, '男', '女', '女'],
'城市': ['北京', '上海', '北京', '深圳', '上海', np.nan, '上海', '北京', '深圳', '上海', '上海']
})
print("=== 清洗前 ===")
print(f"数据量:{df.shape}")
print(df.describe())
# 第1步:去重
df = df.drop_duplicates()
# 第2步:处理年龄异常值
df = df[(df['年龄'] > 0) & (df['年龄'] < 100)]
# 第3步:收入填充中位数
df['收入'] = df['收入'].fillna(df['收入'].median())
# 第4步:年龄填充中位数
df['年龄'] = df['年龄'].fillna(df['年龄'].median())
# 第5步:性别标准化
df['性别'] = df['性别'].replace({'M': '男'})
df['性别'] = df['性别'].fillna('未知')
# 第6步:城市填充众数
df['城市'] = df['城市'].fillna(df['城市'].mode()[0])
print("\n=== 清洗后 ===")
print(f"数据量:{df.shape}")
print(df)
print("\n缺失值:", df.isnull().sum().sum())
本节总结
本节我们学习了数据清洗的三大核心任务:
缺失值处理:
- 检测方法:
isnull()、isnull().sum() - 删除:
dropna()(注意how、thresh、subset参数) - 填充:
fillna()(均值/中位数/众数/前向/后向填充)
重复值处理:
- 检测:
duplicated() - 删除:
drop_duplicates()(注意keep、subset参数)
异常值检测:
- IQR 法:基于四分位距,适用范围广
- Z-Score 法:基于标准差,适合正态分布
- 处理方式:删除、盖帽法(Winsorization)、中位数替换
到此这篇关于Python数据清洗之缺失值处理,重复项去重与异常值检测详解的文章就介绍到这了,更多相关Python数据清洗内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!


最新评论