跳到主要内容

数据清洗

数据清洗是数据分析中最耗时的环节,约占整个分析过程的 60-80%。Pandas 提供了丰富的工具来处理缺失值、重复值、异常值和数据类型转换。

处理缺失值

检测缺失值

import pandas as pd
import numpy as np

df = pd.DataFrame({
'name': ['张三', '李四', '王五', '赵六', None],
'age': [25, np.nan, 35, 28, 40],
'salary': [8000, 12000, np.nan, 10000, 20000],
'city': ['北京', '上海', '广州', None, '杭州']
})

# 检测缺失值
print(df.isnull()) # 返回布尔 DataFrame
print(df.isna()) # 与 isnull 相同

# 统计每列缺失值数量
print(df.isnull().sum())
# name 1
# age 1
# salary 1
# city 1

# 统计每行缺失值数量
print(df.isnull().sum(axis=1))

# 统计总缺失值数量
print(df.isnull().sum().sum()) # 4

# 检测非缺失值
print(df.notnull()) # 与 isnull 相反
print(df.notna()) # 与 notnull 相同

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

# 查看没有缺失值的行
print(df[df.notnull().all(axis=1)])

删除缺失值

# 删除包含缺失值的行(默认)
df_cleaned = df.dropna()

# 删除包含缺失值的列
df_cleaned = df.dropna(axis=1)

# 只删除全为缺失值的行
df_cleaned = df.dropna(how='all')

# 只删除全为缺失值的列
df_cleaned = df.dropna(axis=1, how='all')

# 保留至少 n 个非缺失值的行
df_cleaned = df.dropna(thresh=3) # 至少3个非缺失值

# 在特定列中检查缺失值
df_cleaned = df.dropna(subset=['age', 'salary'])

# 就地修改
df.dropna(inplace=True)

填充缺失值

# 用固定值填充
df_filled = df.fillna(0)
df_filled = df.fillna('Unknown')

# 用不同值填充不同列
df_filled = df.fillna({
'age': df['age'].mean(),
'salary': df['salary'].median(),
'city': 'Unknown'
})

# 前向填充(用前一个值填充)
df_filled = df.fillna(method='ffill') # 或 df.ffill()

# 后向填充(用后一个值填充)
df_filled = df.fillna(method='bfill') # 或 df.bfill()

# 限制填充次数
df_filled = df.fillna(method='ffill', limit=1)

# 按列方向填充
df_filled = df.fillna(method='ffill', axis=1)

# 使用插值
df_filled = df.interpolate() # 线性插值

# 就地填充
df.fillna(0, inplace=True)

处理重复值

检测重复值

df = pd.DataFrame({
'name': ['张三', '李四', '张三', '王五', '李四'],
'age': [25, 30, 25, 35, 30],
'city': ['北京', '上海', '北京', '广州', '上海']
})

# 检测重复行(默认保留第一个)
print(df.duplicated())
# 0 False
# 1 False
# 2 True
# 3 False
# 4 True

# 保留最后一个
df.duplicated(keep='last')

# 标记所有重复项
df.duplicated(keep=False)

# 基于特定列检测重复
df.duplicated(subset=['name'])
df.duplicated(subset=['name', 'age'])

# 统计重复行数
print(df.duplicated().sum()) # 2

删除重复值

# 删除重复行(保留第一个)
df_unique = df.drop_duplicates()

# 保留最后一个
df_unique = df.drop_duplicates(keep='last')

# 删除所有重复项
df_unique = df.drop_duplicates(keep=False)

# 基于特定列删除重复
df_unique = df.drop_duplicates(subset=['name'])
df_unique = df.drop_duplicates(subset=['name', 'age'])

# 就地修改
df.drop_duplicates(inplace=True)

数据类型转换

查看数据类型

df = pd.DataFrame({
'id': ['001', '002', '003'],
'age': ['25', '30', '35'],
'salary': ['8000.50', '12000.00', '15000.00'],
'date': ['2024-01-01', '2024-01-02', '2024-01-03'],
'is_active': ['True', 'False', 'True']
})

print(df.dtypes)
# id object
# age object
# salary object
# date object
# is_active object

转换数据类型

# 转换为数值类型
df['age'] = df['age'].astype(int)
df['salary'] = df['salary'].astype(float)

# 转换为字符串
df['id'] = df['id'].astype(str)

# 转换为布尔值
df['is_active'] = df['is_active'].astype(bool)

# 转换为日期时间
df['date'] = pd.to_datetime(df['date'])

# 转换为类别类型(节省内存)
df['category'] = df['category'].astype('category')

# 使用字典批量转换
df = df.astype({
'age': int,
'salary': float,
'id': str
})

# 处理转换错误
df['age'] = pd.to_numeric(df['age'], errors='coerce') # 错误转为 NaN
df['age'] = pd.to_numeric(df['age'], errors='ignore') # 错误保留原值

日期时间处理

# 转换为日期时间
df['date'] = pd.to_datetime(df['date'])
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')

# 从多个列组合日期
df['date'] = pd.to_datetime(df[['year', 'month', 'day']])

# 提取日期组件
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['weekday'] = df['date'].dt.dayofweek # 0=周一
df['is_weekend'] = df['date'].dt.dayofweek >= 5

# 日期运算
df['next_day'] = df['date'] + pd.Timedelta(days=1)
df['next_month'] = df['date'] + pd.DateOffset(months=1)

字符串处理

基础字符串操作

df = pd.DataFrame({
'name': [' zhang san ', 'LI SI', 'Wang Wu', 'zhao6'],
'email': ['[email protected]', '[email protected]', '[email protected]', '[email protected]'],
'phone': ['138-1234-5678', '139-8765-4321', '137-1111-2222', '136-3333-4444']
})

# 去除空白
df['name'] = df['name'].str.strip()
df['name'] = df['name'].str.lstrip()
df['name'] = df['name'].str.rstrip()

# 大小写转换
df['name_lower'] = df['name'].str.lower()
df['name_upper'] = df['name'].str.upper()
df['name_title'] = df['name'].str.title() # 首字母大写

# 字符串替换
df['name_clean'] = df['name'].str.replace('zhang', '张')
df['phone_clean'] = df['phone'].str.replace('-', '')

# 使用正则表达式
df['name_clean'] = df['name'].str.replace(r'\d+', '', regex=True) # 移除数字
df['name_clean'] = df['name'].str.replace(r'^\s+|\s+$', '', regex=True) # 去除空白

# 分割字符串
df['email_name'] = df['email'].str.split('@').str[0]
df['email_domain'] = df['email'].str.split('@').str[1]

# 提取子串
df['phone_prefix'] = df['phone'].str[:3]
df['phone_suffix'] = df['phone'].str[-4:]

高级字符串操作

# 包含判断
df[df['name'].str.contains('zhang', case=False)]
df[df['email'].str.contains(r'@test\.com$', regex=True)]

# 开头/结尾判断
df[df['name'].str.startswith('zhang')]
df[df['email'].str.endswith('@test.com')]

# 匹配正则
df['phone'].str.match(r'^1[3-9]\d{9}$')
df['email'].str.extract(r'(.+)@(.+)')

# 查找所有匹配
df['name'].str.findall(r'[a-z]+')

# 字符串长度
df['name_len'] = df['name'].str.len()

# 填充字符串
df['id'] = df['id'].str.zfill(5) # 前导零填充

# 删除字符
df['name'] = df['name'].str.strip('z') # 删除首尾的 'z'

异常值处理

检测异常值

df = pd.DataFrame({
'age': [25, 30, 35, 200, 28, 150, 32],
'salary': [8000, 12000, 15000, 50000, 10000, 8000, 9000]
})

# 基于统计方法
Q1 = df['age'].quantile(0.25)
Q3 = df['age'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['age'] < lower_bound) | (df['age'] > upper_bound)]

# 基于 Z-score
from scipy import stats
z_scores = np.abs(stats.zscore(df['age']))
outliers = df[z_scores > 3]

# 基于百分位数
lower = df['salary'].quantile(0.01)
upper = df['salary'].quantile(0.99)
outliers = df[(df['salary'] < lower) | (df['salary'] > upper)]

处理异常值

# 删除异常值
df_clean = df[(df['age'] >= 0) & (df['age'] <= 100)]

# 替换异常值
df.loc[df['age'] > 100, 'age'] = df['age'].median()
df.loc[df['age'] < 0, 'age'] = df['age'].median()

# 使用边界值替换(缩尾处理)
df['age'] = df['age'].clip(lower=0, upper=100)

# 使用分位数替换
lower = df['salary'].quantile(0.01)
upper = df['salary'].quantile(0.99)
df['salary'] = df['salary'].clip(lower=lower, upper=upper)

数据标准化

重命名列

# 重命名特定列
df.rename(columns={'old_name': 'new_name'}, inplace=True)

# 重命名多列
df.rename(columns={
'name': '姓名',
'age': '年龄',
'salary': '薪资'
}, inplace=True)

# 使用函数重命名
df.rename(columns=str.upper, inplace=True)
df.rename(columns=lambda x: x.strip(), inplace=True)

# 替换列名中的字符
df.columns = df.columns.str.replace(' ', '_')
df.columns = df.columns.str.lower()

列重新排序

# 手动指定顺序
cols = ['name', 'age', 'city', 'salary']
df = df[cols]

# 将某列移到最前
cols = ['name'] + [col for col in df.columns if col != 'name']
df = df[cols]

# 按字母顺序排序
df = df[sorted(df.columns)]
df = df[sorted(df.columns, reverse=True)]

实战示例

示例 1:完整的数据清洗流程

def clean_data(df):
"""完整的数据清洗函数"""
# 复制数据
df = df.copy()

# 1. 处理缺失值
# 删除关键字段缺失的行
df.dropna(subset=['name', 'email'], inplace=True)

# 填充其他缺失值
df['age'].fillna(df['age'].median(), inplace=True)
df['city'].fillna('Unknown', inplace=True)

# 2. 处理重复值
df.drop_duplicates(subset=['email'], keep='first', inplace=True)

# 3. 数据类型转换
df['age'] = pd.to_numeric(df['age'], errors='coerce')
df['salary'] = pd.to_numeric(df['salary'], errors='coerce')
df['date'] = pd.to_datetime(df['date'], errors='coerce')

# 4. 处理异常值
df['age'] = df['age'].clip(lower=18, upper=100)
df['salary'] = df['salary'].clip(lower=0)

# 5. 字符串清洗
df['name'] = df['name'].str.strip().str.title()
df['email'] = df['email'].str.strip().str.lower()
df['city'] = df['city'].str.strip().str.title()

# 6. 删除清洗后产生的缺失值
df.dropna(inplace=True)

return df

# 使用
df_clean = clean_data(df)

示例 2:处理脏数据

# 模拟脏数据
dirty_data = pd.DataFrame({
'name': [' 张三 ', '李四', None, '王五 ', ' 赵六 '],
'age': ['25', '30', 'abc', '35', '40'],
'salary': ['8000', '12000元', '15000', None, '10000'],
'date': ['2024-01-01', '01/02/2024', '2024年3月1日', None, '2024-04-01']
})

# 清洗函数
def clean_dirty_data(df):
df = df.copy()

# 处理 name:去除空白,填充缺失
df['name'] = df['name'].str.strip()
df['name'].fillna('Unknown', inplace=True)

# 处理 age:提取数字,转换类型
df['age'] = df['age'].str.extract(r'(\d+)')[0]
df['age'] = pd.to_numeric(df['age'], errors='coerce')
df['age'].fillna(df['age'].median(), inplace=True)

# 处理 salary:提取数字
df['salary'] = df['salary'].str.extract(r'(\d+)')[0]
df['salary'] = pd.to_numeric(df['salary'], errors='coerce')
df['salary'].fillna(df['salary'].median(), inplace=True)

# 处理 date:多种格式解析
df['date'] = pd.to_datetime(df['date'], errors='coerce', infer_datetime_format=True)

return df

df_clean = clean_dirty_data(dirty_data)

小结

数据清洗的关键步骤

  1. 检测问题:缺失值、重复值、异常值、错误类型
  2. 处理缺失:删除或填充
  3. 处理重复:去重
  4. 类型转换:确保正确的数据类型
  5. 处理异常:识别并处理异常值
  6. 字符串清洗:标准化文本数据

最佳实践

  • 清洗前先备份原始数据
  • 记录清洗步骤和原因
  • 使用函数封装清洗流程
  • 验证清洗后的数据质量

练习

  1. 创建一个包含缺失值、重复值、异常值的 DataFrame
  2. 编写一个完整的数据清洗函数
  3. 处理包含多种日期格式的列
  4. 使用正则表达式清洗电话号码
  5. 检测并处理使用 IQR 方法检测的异常值

下一步

数据清洗完成后,让我们学习数据转换!