🧹 数据清洗

从零开始的数据分析之旅

📖 数据清洗:数据科学的80%工作

业界有句名言:数据科学家80%的时间花在数据清洗上。现实中的数据永远不是干净的——缺失值、重复行、错误类型、异常值、格式混乱……掌握数据清洗技能,是从"会写代码"到"能做分析"的关键跨越。

数据清洗流程: 原始数据 诊断问题 清洗处理 验证结果 ──────── ──────── ──────── ──────── CSV/Excel/DB df.isnull() 缺失值处理 df.isnull().sum() ├ 缺失值 df.duplicated() ├ 删除 dropna() df.duplicated() ├ 重复行 df.dtypes ├ 填充 fillna() df.dtypes ├ 错误类型 df.describe() └ 插值 interpolate() df.describe() ├ 异常值 异常值检测 重复行去重 └ 格式混乱 ├ IQR方法 df.drop_duplicates() ├ Z-score 类型转换 └ 业务规则 df['col'].astype() 日期解析 pd.to_datetime()

1. 缺失值处理

import pandas as pd
import numpy as np

# 创建含缺失值的数据
df = pd.DataFrame({
    '姓名': ['张三', '李四', None, '赵六', '张三'],
    '年龄': [22, 25, np.nan, 22, 22],
    '成绩': [85, 92, 78, np.nan, 85]
})

# 检查缺失值
print(df.isnull().sum())        # 每列缺失数
print(df.isnull().mean())       # 缺失比例

# 处理方式1: 删除
df_clean = df.dropna()                    # 删除任何含缺失值的行
df_clean = df.dropna(subset=['姓名'])     # 只看姓名列

# 处理方式2: 填充
df['年龄'] = df['年龄'].fillna(df['年龄'].mean())   # 均值填充
df['成绩'] = df['成绩'].fillna(df['成绩'].median())  # 中位数填充

🔄 重复值与类型转换

重复值处理

# 检查重复
df.duplicated()               # 返回布尔Series
df.duplicated().sum()         # 重复行数

# 去重
df = df.drop_duplicates()                # 完全重复的行
df = df.drop_duplicates(subset=['姓名'])  # 按姓名去重
df = df.drop_duplicates(keep='last')      # 保留最后一个

类型转换

# 数值类型
df['年龄'] = df['年龄'].astype(int)

# 日期类型
df['日期'] = pd.to_datetime(df['日期'], format='mixed', dayfirst=True)

# 分类类型(节省内存)
df['城市'] = df['城市'].astype('category')

# 字符串操作
names = names.str.strip()           # 去除首尾空格
names = names.str.replace(r'\s+', '', regex=True)  # 去除所有空格
names = names.str.lower()           # 转小写

🎯 异常值检测与处理

IQR方法(最常用)

异常值 < Q1 − 1.5×IQR 或 异常值 > Q3 + 1.5×IQR
其中 IQR = Q3 − Q1
# IQR异常值检测
Q1 = df['值'].quantile(0.25)
Q3 = df['值'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR

outliers = df[(df['值'] < lower) | (df['值'] > upper)]
clean = df[(df['值'] >= lower) & (df['值'] <= upper)]

Z-score方法

Z = (x − μ) / σ   |   |Z| > 3 为异常值
from scipy import stats
z_scores = stats.zscore(df['值'])
outliers = df[np.abs(z_scores) > 3]
异常值不一定是错误值!在删除之前,先思考:这是数据录入错误,还是真实的极端情况?业务理解比统计方法更重要。

📐 2024-2025 数据清洗前沿

📋 数据质量检查

import pandas as pd

def quality_report(df):
    report = pd.DataFrame({
        '列名': df.columns,
        '数据类型': df.dtypes.values,
        '缺失数': df.isnull().sum().values,
        '缺失率': (df.isnull().mean()*100).round(2).values,
        '唯一值': df.nunique().values,
    })
    return report

🧪 字符串清洗进阶

df = pd.DataFrame({
    '电话': ['138-1234-5678', '13812345678'],
    '金额': ['1,234.56', '2,345.67']
})

# 电话号码统一格式
df['电话_clean'] = df['电话'].str.replace(r'[-\s]', '', regex=True)

# 金额数值化
df['金额_num'] = (df['金额'].str.replace(',', '').astype(float))
数据清洗黄金法则:永远保留原始数据!创建新列存放清洗结果,而非覆盖原始列。

💻 完整实战代码

#!/usr/bin/env python3
# 数据清洗 — 完整实战

import pandas as pd
import numpy as np

# ============ 脏数据 ============
df = pd.DataFrame({
    '姓名': ['张三', '李四', None, '赵六', '张三', '孙七', '周八'],
    '年龄': [22, 25, np.nan, 22, 22, 30, np.nan],
    '成绩': [85, 92, 78, np.nan, 85, 88, 95],
    '城市': ['北京', '上海', '广州', '北京', '北京', '深圳', '杭州'],
    '日期': ['2024-01-15', '2024/02/20', '2024-03-10',
             '15-01-2024', '2024-01-15', '2024-04-05', '2024-05-20']
})
print(f"原始数据:\n{df}")
print(f"\n缺失值:\n{df.isnull().sum()}")

# ============ 缺失值处理 ============
df_clean = df.copy()
df_clean['年龄'] = df_clean['年龄'].fillna(df_clean['年龄'].mean())
df_clean['成绩'] = df_clean['成绩'].fillna(df_clean['成绩'].median())
df_clean = df_clean.dropna(subset=['姓名'])
print(f"\n清洗后缺失值:\n{df_clean.isnull().sum()}")

# ============ 去重 ============
before = len(df_clean)
df_clean = df_clean.drop_duplicates()
after = len(df_clean)
print(f"\n去重: {before} → {after}")

# ============ 类型转换 ============
df_clean['年龄'] = df_clean['年龄'].astype(int)
df_clean['日期'] = pd.to_datetime(df_clean['日期'], format='mixed', dayfirst=True)
print(f"\n数据类型:\n{df_clean.dtypes}")

# ============ 异常值检测 ============
data = pd.DataFrame({'值': [10,12,11,13,100,12,11,14,12,-50]})
Q1, Q3 = data['值'].quantile(0.25), data['值'].quantile(0.75)
IQR = Q3 - Q1
outliers = data[(data['值'] < Q1-1.5*IQR) | (data['值'] > Q3+1.5*IQR)]
print(f"\nIQR异常值: {list(outliers['值'])}")

# ============ 字符串清洗 ============
names = pd.Series(['  张三 ', '李四', '王  五', '  赵六  '])
cleaned = names.str.strip().str.replace(r'\s+', '', regex=True)
print(f"\n字符串清洗: {list(cleaned)}")

print("\n✅ Python验证通过 — 缺失值/重复/类型转换")
🏆 成就解锁:缺失值/重复/类型转换
Python验证通过 — 缺失值填充(均值/中位数)、行删除、去重(6→5行)、类型转换(float→int, str→datetime)、IQR异常值检测(发现100和-50)、字符串清洗全部正确。
思考题:
① 均值填充和中位数填充各适合什么场景?
② dropna和fillna各自的优缺点是什么?
③ 为什么异常值处理需要结合业务理解?
④ astype转换失败时如何处理?

📝 课后练习

  1. 清洗一个真实CSV文件(Kaggle下载)
  2. 实现IQR和Z-score两种异常值检测并对比结果
  3. 处理包含多种日期格式的列
  4. 使用pandera为DataFrame定义Schema验证
  5. 实现一个数据质量报告生成器
📚 参考资料:
• Pandas官方文档: pandas.pydata.org/docs/user_guide/missing_data
• Python for Data Analysis (Wes McKinney, 2022)
• Great Expectations: greatexpectations.io
• Pandera: pandera.readthedocs.io

🗺️ 数据清洗检查清单

数据清洗完整检查清单: 1.结构检查 2.缺失值 3.重复值 ──────── ──────── ──────── □ 行列数 □ 各列缺失率 □ 完全重复行 □ 列名规范 □ 缺失模式(MCAR/MAR) □ 关键字段重复 □ 数据类型 □ 填充策略 □ 保留策略 □ 索引是否正确 ├ 均值/中位数 ├ first/last ├ 前向/后向 └ 标记后保留 4.异常值 ├ 插值法 ──────── └ 删除 5.格式统一 □ IQR检测 ──────── □ Z-score检测 6.一致性 □ 日期格式 □ 业务规则 ──────── □ 字符串编码 □ 处理策略 □ 枚举值一致 □ 数值精度 □ 大小写统一 □ 电话/身份证 □ 单位统一 □ 地址标准化

缺失值处理策略对比

策略方法适用场景风险
删除dropna()缺失率低(<5%)信息损失
均值填充fillna(mean)正态分布数据方差低估
中位数填充fillna(median)偏态分布方差低估
众数填充fillna(mode)分类变量分布偏移
前向填充ffill()时间序列趋势延续
插值法interpolate()有序数据过拟合
模型预测sklearn imputer多特征相关计算成本

🔑 本课关键要点

数据清洗核心5条:
① 先诊断再治疗——isnull().sum()duplicated()是第一步
② 缺失值处理:删除(低缺失率) > 填充(中缺失率) > 建模(高缺失率)
③ 类型转换是数据清洗的"隐藏关卡"——尤其是日期和数值
④ IQR方法是异常值检测的标准方法——但别盲目删除
⑤ 永远保留原始数据,在新列上操作!
# 数据清洗一页速查
# 1. 诊断
df.isnull().sum() / len(df)    # 缺失率
df.duplicated().sum()          # 重复数
df.dtypes                      # 类型
df.describe()                  # 统计

# 2. 处理
df = df.dropna(subset=['关键列'])
df['列'] = df['列'].fillna(df['列'].median())
df = df.drop_duplicates()
df['列'] = df['列'].astype(int)
df['日期'] = pd.to_datetime(df['日期'])

# 3. 验证
df.isnull().sum().sum() == 0   # 无缺失
len(df) == len(df.drop_duplicates())  # 无重复