跳到主要内容

数据清洗

真实世界的数据往往存在各种问题:缺失值、重复数据、异常值、格式不一致等。数据清洗是数据分析中至关重要的一步,直接影响分析结果的准确性。本章将详细介绍各种数据清洗技术。

数据清洗概述

处理缺失值

缺失值(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']])

小结

本章我们学习了:

  1. 缺失值处理:检测、删除、填充(均值、中位数、众数、插值)
  2. 重复数据处理:检测、删除重复行
  3. 异常值处理:IQR 方法、Z-Score 检测、处理方法
  4. 数据类型转换:数值、字符串、日期时间转换
  5. 字符串清洗:去除空白、大小写转换、替换
  6. 数据标准化:Z-Score、Min-Max、标签编码、独热编码
  7. 数据重塑:透视表、熔化

练习

  1. 创建一个包含缺失值、重复值、异常值的数据集并进行清洗
  2. 对用户数据进行完整的清洗流程
  3. 将宽格式数据转换为长格式

参考资源

下一步

数据清洗完成后,你已经掌握了数据预处理的核心技能。继续练习这些技术,并尝试应用到实际数据分析项目中!