数据清洗与预处理
·
数据清洗与预处理
🎯 本章目标:掌握处理真实世界中"脏数据"的各种技巧,让数据变得干净可用。
6.1 为什么数据需要清洗?
6.1.1 真实世界的数据问题
数据科学家 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 缺失值处理策略
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)
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)