数据清洗与预处理

🎯 本章目标:掌握处理真实世界中"脏数据"的各种技巧,让数据变得干净可用。


6.1 为什么数据需要清洗?

6.1.1 真实世界的数据问题

原始数据

缺失值
空单元格

重复值
重复记录

异常值
明显错误的数据

格式不一致
2024/1/1 vs 2024-01-01

类型错误
数字存成字符串

单位不统一
元 vs 万元

清洗后的
干净数据

数据科学家 80% 的时间花在数据清洗上!

6.1.2 创建脏数据示例

import pandas as pd
import numpy as np

# 模拟真实世界的脏数据
dirty_df = pd.DataFrame({
    '姓名': ['张三', '李四', '王五', '张三', '赵六', '钱七', '孙八', None],
    '年龄': [25, 30, -5, 25, 200, 28, np.nan, 35],
    '城市': ['北京', '上海', '北京 ', '北京', '广州', '深圳', '杭州', '成都'],
    '入职日期': ['2024-01-15', '2024/02/20', '2024-03-10', 
                 '2024-01-15', '2024-05-01', '2024-06-15', None, '2024-08-01'],
    '月薪': ['15000', '20000', '18000元', '15000', '25000', None, '30000', '20k'],
    '部门': ['技术', '技术', '产品', '技术', '销售', '产品', '技术', '销售']
})

print("=== 脏数据示例 ===")
print(dirty_df)
print("\n数据类型:")
print(dirty_df.dtypes)

6.2 处理缺失值

6.2.1 识别缺失值

# 🌟 查看缺失值情况
print(dirty_df.isnull())  # True 表示缺失
print(dirty_df.isnull().sum())  # 每列的缺失值数量
print(dirty_df.isnull().sum() / len(dirty_df) * 100)  # 缺失值百分比

# 🌟 查看非缺失值
print(dirty_df.notnull())

# 🌟 查看有缺失值的行
rows_with_nan = dirty_df[dirty_df.isnull().any(axis=1)]
print(rows_with_nan)

# 🌟 查看完全没有缺失值的行
complete_rows = dirty_df[dirty_df.notnull().all(axis=1)]
print(complete_rows)

6.2.2 删除缺失值

# 🌟 删除包含缺失值的行(默认)
clean_df = dirty_df.dropna()
print(f"原数据:{len(dirty_df)} 行,删除后:{len(clean_df)} 行")

# 🌟 只删除某列有缺失值的行
clean_df = dirty_df.dropna(subset=['姓名', '月薪'])

# 🌟 删除整行都是缺失值的行
clean_df = dirty_df.dropna(how='all')

# 🌟 删除缺失值超过 2 个的行
clean_df = dirty_df.dropna(thresh=2)  # 至少要有 2 个非缺失值

# 🌟 删除包含缺失值的列
clean_df = dirty_df.dropna(axis=1)

6.2.3 填充缺失值

# 🌟 用固定值填充
df_filled = dirty_df.fillna('未知')

# 🌟 用不同值填充不同列
df_filled = dirty_df.fillna({
    '姓名': '匿名',
    '年龄': dirty_df['年龄'].mean(),
    '城市': '未知',
    '入职日期': '2024-01-01',
    '月薪': 0
})

# 🌟 用前一个值填充(forward fill)
df_filled = dirty_df.fillna(method='ffill')
# 张三  李四  王五  王五(填充)  赵六...

# 🌟 用后一个值填充(backward fill)
df_filled = dirty_df.fillna(method='bfill')

# 🌟 用插值填充(数值列)
df_filled = dirty_df.interpolate()  # 线性插值

# 🌟 用众数填充(分类数据)
mode_city = dirty_df['城市'].mode()[0]
df_filled = dirty_df.fillna({'城市': mode_city})

6.2.4 缺失值处理策略

< 5%

5%-30%

> 30%

数值

分类

时间

发现缺失值

缺失比例?

直接删除

填充处理

考虑删除该列

数据类型?

均值/中位数/插值

众数/固定值

前后填充/插值


6.3 处理重复值

6.3.1 识别重复值

# 🌟 查看重复行(所有列都相同)
print(dirty_df.duplicated())  # True 表示该行是重复的

# 🌟 查看基于特定列的重复
print(dirty_df.duplicated(subset=['姓名']))

# 🌟 保留第一次出现的,标记后面的为重复
print(dirty_df.duplicated(keep='first'))  # 默认

# 🌟 保留最后一次出现的,标记前面的为重复
print(dirty_df.duplicated(keep='last'))

# 🌟 标记所有重复的行(包括第一次)
print(dirty_df.duplicated(keep=False))

# 🌟 查看重复的行
duplicates = dirty_df[dirty_df.duplicated(keep=False)]
print(duplicates)

6.3.2 删除重复值

# 🌟 删除完全重复的行
clean_df = dirty_df.drop_duplicates()

# 🌟 基于特定列删除重复(保留第一个)
clean_df = dirty_df.drop_duplicates(subset=['姓名'], keep='first')

# 🌟 保留最后一个
clean_df = dirty_df.drop_duplicates(subset=['姓名'], keep='last')

# 🌟 删除所有重复(一个都不保留)
clean_df = dirty_df.drop_duplicates(subset=['姓名'], keep=False)

# 🌟 基于多列判断重复
clean_df = dirty_df.drop_duplicates(subset=['姓名', '部门'])

6.4 处理异常值

6.4.1 识别异常值

# 创建包含异常值的数据
df_outlier = pd.DataFrame({
    '姓名': ['张三', '李四', '王五', '赵六', '钱七', '孙八'],
    '年龄': [25, 30, 28, 200, 22, 35],  # 200 是异常值
    '月薪': [15000, 20000, 18000, 25000, -5000, 30000]  # -5000 是异常值
})

# 🌟 描述统计(快速发现异常)
print(df_outlier.describe())

# 🌟 箱线图法(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

outliers_age = df_outlier[
    (df_outlier['年龄'] < lower_bound) | 
    (df_outlier['年龄'] > upper_bound)
]
print(f"年龄异常值范围: < {lower_bound} 或 > {upper_bound}")
print(outliers_age)

# 🌟 Z-score 方法
from scipy import stats
z_scores = np.abs(stats.zscore(df_outlier['年龄'].dropna()))
outliers = df_outlier[z_scores > 3]
print("Z-score > 3 的异常值:")
print(outliers)

6.4.2 处理异常值

# 🌟 方法1:删除异常值
df_clean = df_outlier[
    (df_outlier['年龄'] >= 18) & 
    (df_outlier['年龄'] <= 65) &
    (df_outlier['月薪'] >= 0)
]

# 🌟 方法2:替换为边界值(盖帽法)
df_capped = df_outlier.copy()
df_capped['年龄'] = df_capped['年龄'].clip(lower=18, upper=65)
df_capped['月薪'] = df_capped['月薪'].clip(lower=0)

# 🌟 方法3:替换为均值/中位数
df_replaced = df_outlier.copy()
median_age = df_outlier[df_outlier['年龄'] <= 100]['年龄'].median()
df_replaced.loc[df_replaced['年龄'] > 100, '年龄'] = median_age

# 🌟 方法4:标记异常值
df_flagged = df_outlier.copy()
df_flagged['年龄异常'] = (df_flagged['年龄'] < 18) | (df_flagged['年龄'] > 65)
df_flagged['月薪异常'] = df_flagged['月薪'] < 0

6.5 数据类型转换

6.5.1 类型转换基础

# 创建类型混乱的数据
df_types = pd.DataFrame({
    '订单号': ['001', '002', '003'],  # 应该是字符串
    '数量': ['10', '20', '30'],        # 应该是整数
    '价格': ['19.99', '29.99', '39.99'],  # 应该是浮点数
    '日期': ['2024-01-01', '2024-02-01', '2024-03-01'],
    '是否会员': ['是', '否', '是']       # 可以转为布尔值
})

print("原始类型:")
print(df_types.dtypes)

# 🌟 转换数值类型
df_types['数量'] = df_types['数量'].astype('int64')
df_types['价格'] = df_types['价格'].astype('float64')

# 🌟 转换日期类型
df_types['日期'] = pd.to_datetime(df_types['日期'])

# 🌟 转换为布尔值
df_types['是否会员'] = df_types['是否会员'].map({'是': True, '否': False})

# 🌟 转换为分类类型(节省内存)
df_types['订单号'] = df_types['订单号'].astype('category')

print("\n转换后类型:")
print(df_types.dtypes)

6.5.2 处理转换错误

# 创建有问题的数据
df_messy = pd.DataFrame({
    '数值': ['10', '20', 'abc', '30', None]
})

# 🌟 强制转换(遇到错误会报错)
# df_messy['数值'].astype('int64')  # ❌ 报错!

# 🌟 遇到错误转为 NaN
df_messy['数值_clean'] = pd.to_numeric(df_messy['数值'], errors='coerce')
print(df_messy)

# 🌟 遇到错误保留原值
df_messy['数值_keep'] = pd.to_numeric(df_messy['数值'], errors='ignore')
print(df_messy)

6.5.3 日期时间处理

# 各种日期格式
dates = pd.DataFrame({
    '原始日期': [
        '2024-01-15',
        '2024/02/20',
        '15-03-2024',
        '2024年04月15日',
        'May 5, 2024',
        '1680000000',  # Unix 时间戳
        None
    ]
})

# 🌟 自动解析日期
dates['日期'] = pd.to_datetime(dates['原始日期'], errors='coerce')
print(dates)

# 🌟 指定格式解析
dates['日期_指定格式'] = pd.to_datetime(
    dates['原始日期'], 
    format='%Y-%m-%d', 
    errors='coerce'
)

# 🌟 从 Unix 时间戳转换
dates['日期_时间戳'] = pd.to_datetime(
    dates['原始日期'], 
    unit='s', 
    errors='coerce'
)

# 🌟 提取日期组件
dates['年'] = dates['日期'].dt.year
dates['月'] = dates['日期'].dt.month
dates['日'] = dates['日期'].dt.day
dates['星期'] = dates['日期'].dt.dayofweek  # 0=周一, 6=周日
dates['季度'] = dates['日期'].dt.quarter

print(dates)

6.6 字符串处理

6.6.1 字符串清理

df_strings = pd.DataFrame({
    '姓名': ['  张三  ', '李四', ' 王五 ', '赵六'],
    '城市': ['北京', '上海 ', '北京', ' 广州'],
    '电话': ['138-1234-5678', '13987654321', '137 1234 5678', '136-8765-4321'],
    '邮箱': ['zhangsan@email.com', 'lisi@EMAIL.COM', 'wangwu@Email.com', None]
})

# 🌟 去除空白
df_strings['姓名'] = df_strings['姓名'].str.strip()
df_strings['城市'] = df_strings['城市'].str.strip()

# 🌟 统一大小写
df_strings['邮箱_小写'] = df_strings['邮箱'].str.lower()
df_strings['邮箱_大写'] = df_strings['邮箱'].str.upper()

# 🌟 替换字符
df_strings['电话_清洗'] = df_strings['电话'].str.replace('-', '', regex=False)
df_strings['电话_清洗'] = df_strings['电话_清洗'].str.replace(' ', '', regex=False)

# 🌟 提取部分字符串
df_strings['邮箱域名'] = df_strings['邮箱'].str.split('@').str[1]

# 🌟 检查包含
df_strings['是移动'] = df_strings['电话'].str.startswith('138')

print(df_strings)

6.6.2 正则表达式处理

# 🌟 提取数字
df_strings['电话_纯数字'] = df_strings['电话'].str.replace(r'\D', '', regex=True)

# 🌟 提取邮箱用户名
df_strings['邮箱用户名'] = df_strings['邮箱'].str.extract(r'(.+)@')

# 🌟 验证手机号格式
df_strings['电话有效'] = df_strings['电话'].str.match(r'1[3-9]\d{9}')

# 🌟 替换匹配内容
df_strings['城市_简化'] = df_strings['城市'].str.replace(r'市$', '', regex=True)

print(df_strings)

6.7 数据标准化

6.7.1 数值标准化

df_scale = pd.DataFrame({
    'A': [1, 2, 3, 4, 5],
    'B': [100, 200, 300, 400, 500],
    'C': [0.01, 0.02, 0.03, 0.04, 0.05]
})

# 🌟 Min-Max 标准化(缩放到 0-1)
df_minmax = (df_scale - df_scale.min()) / (df_scale.max() - df_scale.min())
print("Min-Max 标准化:")
print(df_minmax)

# 🌟 Z-score 标准化(均值为0,标准差为1)
df_zscore = (df_scale - df_scale.mean()) / df_scale.std()
print("\nZ-score 标准化:")
print(df_zscore)

# 🌟 小数定标标准化
df_decimal = df_scale / 10 ** np.ceil(np.log10(df_scale.abs().max()))
print("\n小数定标标准化:")
print(df_decimal)

6.7.2 类别编码

df_encode = pd.DataFrame({
    '颜色': ['红', '绿', '蓝', '红', '绿'],
    '尺寸': ['S', 'M', 'L', 'M', 'S']
})

# 🌟 标签编码(Label Encoding)
df_encode['颜色_编码'] = df_encode['颜色'].astype('category').cat.codes

# 🌟 独热编码(One-Hot Encoding)
df_onehot = pd.get_dummies(df_encode['颜色'], prefix='颜色')
df_encode = pd.concat([df_encode, df_onehot], axis=1)

# 🌟 有序编码(指定顺序)
size_order = {'S': 1, 'M': 2, 'L': 3}
df_encode['尺寸_编码'] = df_encode['尺寸'].map(size_order)

print(df_encode)

6.8 综合清洗流程

6.8.1 完整清洗示例

import pandas as pd
import numpy as np

# 创建脏数据
raw_data = pd.DataFrame({
    '客户名': [' 张三 ', '李四', '王五', '张三', '赵六', None, '钱七'],
    '年龄': [25, 30, -5, 25, 200, 28, np.nan],
    '城市': ['北京', '上海 ', '北京', '北京', '广州', '深圳', '杭州'],
    '注册日期': ['2024-01-15', '2024/02/20', '2024-03-10', 
                 '2024-01-15', '2024-05-01', None, '2024-08-01'],
    '消费金额': ['1500', '2000元', '1800', '1500', '2500', '3000', '-100']
})

print("=== 原始数据 ===")
print(raw_data)
print("\n数据信息:")
print(raw_data.info())

# 步骤1:处理缺失值
print("\n=== 步骤1:处理缺失值 ===")
clean_data = raw_data.copy()
clean_data['客户名'] = clean_data['客户名'].fillna('未知客户')
clean_data['注册日期'] = clean_data['注册日期'].fillna('2024-01-01')
clean_data['年龄'] = clean_data['年龄'].fillna(clean_data['年龄'].median())

# 步骤2:去除字符串空白
print("\n=== 步骤2:字符串清理 ===")
clean_data['客户名'] = clean_data['客户名'].str.strip()
clean_data['城市'] = clean_data['城市'].str.strip()

# 步骤3:处理消费金额(去除单位,转为数值)
print("\n=== 步骤3:数值转换 ===")
clean_data['消费金额'] = clean_data['消费金额'].str.replace('元', '', regex=False)
clean_data['消费金额'] = pd.to_numeric(clean_data['消费金额'], errors='coerce')

# 步骤4:处理异常值
print("\n=== 步骤4:异常值处理 ===")
# 年龄异常值
clean_data.loc[clean_data['年龄'] < 0, '年龄'] = clean_data['年龄'].median()
clean_data.loc[clean_data['年龄'] > 120, '年龄'] = clean_data['年龄'].median()

# 消费金额异常值
clean_data.loc[clean_data['消费金额'] < 0, '消费金额'] = 0

# 步骤5:转换日期类型
print("\n=== 步骤5:日期转换 ===")
clean_data['注册日期'] = pd.to_datetime(clean_data['注册日期'])

# 步骤6:删除重复值
print("\n=== 步骤6:去重 ===")
clean_data = clean_data.drop_duplicates(subset=['客户名'], keep='first')

# 步骤7:重置索引
print("\n=== 步骤7:重置索引 ===")
clean_data = clean_data.reset_index(drop=True)

print("\n=== 清洗后的数据 ===")
print(clean_data)
print("\n清洗后数据信息:")
print(clean_data.info())
print("\n统计摘要:")
print(clean_data.describe())

6.9 本章小结

核心要点

缺失值处理

  • 识别:isnull(), notnull()
  • 删除:dropna()
  • 填充:fillna(), interpolate()

重复值处理

  • 识别:duplicated()
  • 删除:drop_duplicates()

异常值处理

  • 识别:箱线图法(IQR)、Z-score
  • 处理:删除、盖帽、替换

类型转换

  • astype(), pd.to_numeric(), pd.to_datetime()
  • 处理错误:errors='coerce'

字符串处理

  • str.strip(), str.lower(), str.replace()
  • str.extract(), str.match()(正则表达式)

数据标准化

  • Min-Max 标准化
  • Z-score 标准化
  • 类别编码(Label, One-Hot)
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐