数据清洗
真实世界的数据往往存在各种问题:缺失值、重复数据、异常值、格式不一致等。数据清洗是数据分析中至关重要的一步,直接影响分析结果的准确性。本章将详细介绍各种数据清洗技术。
数据清洗概述
处理缺失值
缺失值(Missing Values)在数据分析中非常常见,可能是由于数据未收集、数据录入错误或数据源问题导致的。
创建包含缺失值的数据
import pandas as pd
import numpy as np
# 创建包含缺失值的数据
df = pd.DataFrame({
'name': ['张三', '李四', '王五', '赵六', '钱七'],
'age': [25, 30, np.nan, 28, 45],
'salary': [10000, 15000, 20000, np.nan, 30000],
'city': ['北京', np.nan, '广州', '深圳', np.nan]
})
print(df)
# name age salary city
# 0 张三 25.0 10000.0 北京
# 1 李四 30.0 15000.0 NaN
# 2 王五 NaN 20000.0 广州
# 3 赵六 28.0 NaN 深圳
# 4 钱七 45.0 30000.0 NaN
检测缺失值
# 检测每列的缺失值
print(df.isnull())
# name age salary city
# 0 False False False False
# 1 False False False True
# 2 False True False False
# 3 False False True False
# 4 False False False True
# 统计每列缺失值数量
print(df.isnull().sum())
# name 0
# age 1
# salary 1
# city 2
# dtype: int64
# 统计总缺失值数量
print(df.isnull().sum().sum()) # 4
# 计算缺失值比例
print(df.isnull().sum() / len(df))
删除缺失值
# 删除有缺失值的行(默认)
df_cleaned = df.dropna()
print(df_cleaned)
# name age salary city
# 0 张三 25.0 10000.0 北京
# 删除有缺失值的列
df_cleaned = df.dropna(axis=1)
# 只删除全部为缺失值的行/列
df_cleaned = df.dropna(how='all') # 全部为NaN才删除
df_cleaned = df.dropna(how='any') # 任意NaN就删除(默认)
# 根据阈值删除
df_cleaned = df.dropna(thresh=3) # 至少3个非NaN值
df_cleaned = df.dropna(thresh=2, axis=1) # 列至少2个非NaN值
填充缺失值
# 用固定值填充
df_filled = df.fillna(0)
df_filled = df.fillna('未知')
# 用均值填充(数值列)
df_filled = df.fillna(df.mean())
# 或者指定列
df_filled = df['age'].fillna(df['age'].mean())
# 用中位数填充(对异常值更稳健)
df_filled = df['age'].fillna(df['age'].median())
# 用众数填充(类别列)
df_filled = df['city'].fillna(df['city'].mode()[0])
# 用前向/后向值填充
df_filled = df.fillna(method='ffill') # 用前一个值填充
df_filled = df.fillna(method='bfill') # 用后一个值填充
# 用插值填充
df_filled = df.interpolate() # 线性插值
# 方法包括: 'linear', 'time', 'index', 'values', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'
# 用分组均值填充
df['age'] = df.groupby('city')['age'].transform(lambda x: x.fillna(x.mean()))
处理重复数据
重复数据会影响分析结果的准确性,需要及时处理。
检测重复数据
# 创建包含重复值的数据
df = pd.DataFrame({
'name': ['张三', '李四', '王五', '张三', '李四', '王五'],
'age': [25, 30, 35, 25, 30, 35],
'city': ['北京', '上海', '广州', '北京', '上海', '广州']
})
# 检测重复行
print(df.duplicated())
# 0 False
# 1 False
# 2 False
# 3 True # 张三重复
# 4 True # 李四重复
# 5 True # 王五重复
# dtype: bool
# 统计重复数量
print(df.duplicated().sum()) # 3
# 检测特定列的重复
print(df.duplicated(subset=['name']))
删除重复数据
# 删除重复行(保留第一个)
df_cleaned = df.drop_duplicates()
# 保留最后一个
df_cleaned = df.drop_duplicates(keep='last')
# 删除所有重复
df_cleaned = df.drop_duplicates(keep=False)
# 根据特定列删除重复
df_cleaned = df.drop_duplicates(subset=['name'], keep='first')
# 查看重复数据
duplicated_data = df[df.duplicated(subset=['name'], keep=False)]
print(duplicated_data.sort_values('name'))
处理异常值
异常值(Outliers)是远离其他数据点的值,可能是测量错误或真实的极端值。
检测异常值
# 创建包含异常值的数据
df = pd.DataFrame({
'name': ['员工' + str(i) for i in range(1, 11)],
'salary': [8000, 9000, 10000, 11000, 12000, 13000, 14000, 15000, 100000, 11000]
})
# 1. 使用 IQR 方法(四分位距)
Q1 = df['salary'].quantile(0.25)
Q3 = df['salary'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f'Q1: {Q1}, Q3: {Q3}, IQR: {IQR}')
print(f'正常范围: [{lower_bound}, {upper_bound}]')
outliers = df[(df['salary'] < lower_bound) | (df['salary'] > upper_bound)]
print(outliers)
# 2. 使用 Z-Score
from scipy import stats
z_scores = np.abs(stats.zscore(df['salary']))
outliers = df[z_scores > 2] # Z-score > 2 视为异常
print(outliers)
# 3. 使用describe查看
print(df['salary'].describe())
处理异常值
# 方法1:删除异常值
df_cleaned = df[(df['salary'] >= lower_bound) & (df['salary'] <= upper_bound)]
# 方法2:用边界值替换
df['salary_clipped'] = df['salary'].clip(lower_bound, upper_bound)
# 方法3:用均值/中位数替换
outlier_idx = (df['salary'] < lower_bound) | (df['salary'] > upper_bound)
df.loc[outlier_idx, 'salary'] = df['salary'].median()
# 方法4:用缺失值填充后处理
df.loc[outlier_idx, 'salary'] = np.nan
df['salary'] = df['salary'].fillna(df['salary'].median())
数据类型转换
查看数据类型
df = pd.DataFrame({
'age': ['25', '30', '35'],
'salary': [10000.5, 15000.3, 20000.8],
'date': ['2024-01-01', '2024-01-02', '2024-01-03']
})
print(df.dtypes)
# age object
# salary float64
# date object
类型转换
# 转换为数值类型
df['age'] = pd.to_numeric(df['age']) # 自动推断类型
df['age'] = df['age'].astype(int) # 指定类型
df['age'] = df['age'].astype('int32')
# 转换为字符串
df['age'] = df['age'].astype(str)
# 转换为日期时间
df['date'] = pd.to_datetime(df['date'])
print(df.dtypes)
# age int64
# salary float64
# date datetime64[ns]
# 转换时处理错误
df = pd.DataFrame({'numbers': ['1', '2', 'abc', '4']})
df['numbers'] = pd.to_numeric(df['numbers'], errors='coerce') # 无效值变为NaN
# 或者
df['numbers'] = pd.to_numeric(df['numbers'], errors='ignore') # 保持原样
字符串清洗
# 创建包含需要清洗的字符串数据
df = pd.DataFrame({
'name': [' 张三 ', '李四', '王五'],
'city': ['BEIJING', 'Shanghai', 'GUANGZHOU'],
'phone': ['138-0000-0001', '13900000002', ' 13800000003 ']
})
# 去除空白
df['name'] = df['name'].str.strip()
df['phone'] = df['phone'].str.strip()
# 大小写转换
df['city_lower'] = df['city'].str.lower()
df['city_upper'] = df['city'].str.upper()
df['city_title'] = df['city'].str.title()
# 字符串替换
df['phone'] = df['phone'].str.replace('-', '')
# 字符串分割
df['phone_prefix'] = df['phone'].str[:3]
# 提取数字/字母
df['digits'] = df['phone'].str.extract(r'(\d+)')
数据标准化
数值标准化
from sklearn.preprocessing import StandardScaler, MinMaxScaler
df = pd.DataFrame({
'salary': [5000, 8000, 12000, 20000, 30000],
'age': [22, 25, 30, 35, 45]
})
# Z-Score 标准化(均值0,标准差1)
scaler = StandardScaler()
df['salary_scaled'] = scaler.fit_transform(df[['salary']])
# Min-Max 归一化(0-1范围)
scaler = MinMaxScaler()
df['salary_normalized'] = scaler.fit_transform(df[['salary']])
# 手动归一化
df['salary_manual'] = (df['salary'] - df['salary'].min()) / (df['salary'].max() - df['salary'].min())
print(df)
类别编码
# 创建示例数据
df = pd.DataFrame({
'city': ['北京', '上海', '广州', '深圳', '北京']
})
# 1. 标签编码(Label Encoding)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['city_encoded'] = le.fit_transform(df['city'])
# 北京=0, 广州=1, 上海=2, 深圳=3
# 2. 独热编码(One-Hot Encoding)
df_onehot = pd.get_dummies(df, columns=['city'])
# 产生 city_北京, city_上海, city_广州, city_深圳 列
# 3. 自定义映射
mapping = {'北京': 1, '上海': 2, '广州': 3, '深圳': 4}
df['city_mapped'] = df['city'].map(mapping)
数据重塑
透视表
# 创建数据
df = pd.DataFrame({
'date': ['2024-01'] * 3 + ['2024-02'] * 3,
'product': ['A', 'B', 'C'] * 2,
'sales': [100, 200, 150, 120, 220, 180]
})
# 创建透视表
pivot = pd.pivot_table(df,
values='sales',
index='product',
columns='date',
aggfunc='sum')
print(pivot)
# 填充缺失值
pivot = pd.pivot_table(df,
values='sales',
index='product',
columns='date',
aggfunc='sum',
fill_value=0)
熔化(melt)
# 宽格式转换为长格式
df = pd.DataFrame({
'name': ['张三', '李四'],
'math': [90, 85],
'english': [88, 92],
'chinese': [92, 88]
})
df_melted = df.melt(id_vars=['name'],
var_name='subject',
value_name='score')
print(df_melted)
# name subject score
# 0 张三 math 90
# 1 李四 math 85
# 2 张三 english 88
# 3 李四 english 92
# 4 张三 chinese 92
# 5 李四 chinese 88
实战示例
示例:电商用户数据清洗
# 创建原始数据
raw_data = {
'user_id': [1, 2, 3, 3, 4, 5, 6],
'name': [' 张三 ', '李四', '王五', '王五', '赵六', None, '钱七'],
'age': [25, 30, 35, 35, 28, 999, 30],
'city': ['BEIJING', 'shanghai', 'GUANGZHOU', 'GUANGZHOU', 'shenzhen', 'beijing', None],
'salary': [10000, 15000, 20000, 20000, 12000, 10000, 30000],
'register_date': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-03', '2024-01-05', None, '2024-01-07']
}
df = pd.DataFrame(raw_data)
print("原始数据:")
print(df)
print("\n缺失值统计:")
print(df.isnull().sum())
# 1. 删除重复数据
df = df.drop_duplicates()
print("\n删除重复后:")
print(df)
# 2. 处理缺失值
df['name'] = df['name'].str.strip().fillna('未知')
df['city'] = df['city'].str.strip().str.lower().fillna('未知')
df['salary'] = df['salary'].fillna(df['salary'].median())
# 3. 处理异常年龄
median_age = df[df['age'] < 100]['age'].median()
df.loc[df['age'] >= 100, 'age'] = median_age
# 4. 转换日期格式
df['register_date'] = pd.to_datetime(df['register_date'], errors='coerce')
# 5. 数据类型标准化
print("\n清洗后数据:")
print(df)
print("\n数据类型:")
print(df.dtypes)
示例:问卷调查数据清洗
# 创建问卷数据
survey_data = {
'respondent': [f'R{i}' for i in range(1, 11)],
'q1_age': [25, 30, '20岁以下', 35, '45岁以上', 28, 32, '未知', 40, 26],
'q2_income': ['5k-10k', '10k-20k', '20k-30k', '10k-20k', '30k以上',
'5k以下', '10k-20k', '20k-30k', '30k以上', '未知'],
'q3_satisfaction': [4, 5, 3, 4, 2, 5, 4, 1, 3, 4]
}
df = pd.DataFrame(survey_data)
# 清洗年龄
age_mapping = {
'20岁以下': 20,
'45岁以上': 50,
'未知': np.nan
}
df['age_cleaned'] = df['q1_age'].replace(age_mapping).astype(float)
df['age_cleaned'] = df['age_cleaned'].fillna(df['age_cleaned'].median())
# 清洗收入
income_mapping = {
'5k以下': 3,
'5k-10k': 7,
'10k-20k': 15,
'20k-30k': 25,
'30k以上': 35,
'未知': np.nan
}
df['income_cleaned'] = df['q2_income'].map(income_mapping)
df['income_cleaned'] = df['income_cleaned'].fillna(df['income_cleaned'].median())
print("清洗后数据:")
print(df[['respondent', 'age_cleaned', 'income_cleaned', 'q3_satisfaction']])
小结
本章我们学习了:
- 缺失值处理:检测、删除、填充(均值、中位数、众数、插值)
- 重复数据处理:检测、删除重复行
- 异常值处理:IQR 方法、Z-Score 检测、处理方法
- 数据类型转换:数值、字符串、日期时间转换
- 字符串清洗:去除空白、大小写转换、替换
- 数据标准化:Z-Score、Min-Max、标签编码、独热编码
- 数据重塑:透视表、熔化
练习
- 创建一个包含缺失值、重复值、异常值的数据集并进行清洗
- 对用户数据进行完整的清洗流程
- 将宽格式数据转换为长格式
参考资源
下一步
数据清洗完成后,你已经掌握了数据预处理的核心技能。继续练习这些技术,并尝试应用到实际数据分析项目中!