跳到主要内容

Pandas 数据清洗

问题

如何用 Pandas 进行数据清洗?缺失值、重复值、异常值分别怎么处理?

答案

数据清洗是数据分析中最耗时的环节(占 60%~80% 的时间),也是面试中的高频考点。脏数据进去,垃圾结论出来——清洗质量直接决定分析结论的可靠性。

数据清洗流程

1. 查看数据概况

清洗前先全面了解数据质量:

数据质量检查
import pandas as pd
import numpy as np

# 基本信息
df.info() # 列名、非空数、数据类型
df.describe() # 数值列统计
df.shape # (行数, 列数)

# 缺失值检查
df.isnull().sum() # 每列缺失数
df.isnull().sum() / len(df) # 缺失率
df.isnull().any() # 哪些列有缺失

# 重复值检查
df.duplicated().sum() # 完全重复的行数
df.duplicated(subset=['id']).sum() # 按指定列检查

# 唯一值检查
df['status'].value_counts() # 各值出现次数
df['status'].nunique() # 唯一值数量

2. 缺失值处理

面试核心

缺失值处理是面试必考题,要能说清楚"什么场景用什么方法",而不是只知道 fillna

检测缺失

检测缺失值
# 单列检查
df['col'].isnull() # 返回布尔 Series
df['col'].notna() # 非缺失

# 筛选包含缺失值的行
df[df['col'].isnull()]

# 查看缺失值模式
df.isnull().sum().sort_values(ascending=False)

删除缺失

删除缺失值
# 删除任何列有缺失的行
df.dropna()

# 删除指定列有缺失的行
df.dropna(subset=['name', 'age'])

# 删除所有值都缺失的行
df.dropna(how='all')

# 删除缺失率超过 50% 的列
threshold = len(df) * 0.5
df.dropna(axis=1, thresh=threshold)

填充缺失

填充缺失值
# 固定值填充
df['city'].fillna('未知')

# 均值/中位数/众数填充
df['age'].fillna(df['age'].mean()) # 均值
df['age'].fillna(df['age'].median()) # 中位数(对异常值更稳健)
df['city'].fillna(df['city'].mode()[0]) # 众数(分类变量)

# 前向/后向填充(时间序列常用)
df['price'].ffill() # 用前一个值填充 (forward fill)
df['price'].bfill() # 用后一个值填充 (backward fill)

# 分组填充(更精确)
# 比如按城市分组,用各城市的中位数填充
df['salary'] = df.groupby('city')['salary'].transform(
lambda x: x.fillna(x.median())
)

# 插值法(连续数值)
df['temperature'].interpolate(method='linear') # 线性插值
df['temperature'].interpolate(method='time') # 按时间插值

缺失值处理策略选择

场景推荐方法原因
缺失率 < 5%删除行影响小,简单高效
缺失率 5%~30%填充删除损失太多数据
缺失率 > 50%删除列或特殊标记数据质量太差
数值型 + 无异常值均值填充保持分布
数值型 + 有异常值中位数填充抗异常值
分类型众数填充或 "Unknown"保持一致性
时间序列ffill/bfill/插值利用时序连续性
存在分组特征分组填充更精确

3. 重复值处理

重复值处理
# 查看重复行
df[df.duplicated()] # 完全重复
df[df.duplicated(subset=['user_id'])] # 按指定列判断

# 删除重复行
df.drop_duplicates() # 删除完全重复行(保留第一条)
df.drop_duplicates(subset=['user_id']) # 按 user_id 去重
df.drop_duplicates(subset=['user_id'], keep='last') # 保留最后一条
df.drop_duplicates(subset=['user_id'], keep=False) # 全部删除

# 实际场景:保留最新的一条记录
df = df.sort_values('create_time', ascending=False)
df = df.drop_duplicates(subset=['order_id'], keep='first')

4. 异常值处理

检测异常值

异常值检测
# 方法一:3σ 原则(正态分布数据)
mean = df['salary'].mean()
std = df['salary'].std()
outliers = df[(df['salary'] < mean - 3*std) | (df['salary'] > mean + 3*std)]

# 方法二:IQR 方法(箱线图原理,更稳健)
Q1 = df['salary'].quantile(0.25)
Q3 = df['salary'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[(df['salary'] < lower) | (df['salary'] > upper)]

# 方法三:分位数法
lower = df['salary'].quantile(0.01)
upper = df['salary'].quantile(0.99)
outliers = df[(df['salary'] < lower) | (df['salary'] > upper)]

处理异常值

异常值处理
# 方法一:删除
df = df[(df['salary'] >= lower) & (df['salary'] <= upper)]

# 方法二:截断(Winsorize)—— 将超出的值拉到边界
df['salary'] = df['salary'].clip(lower=lower, upper=upper)

# 方法三:替换为 NaN,后续按缺失值处理
df.loc[df['salary'] > upper, 'salary'] = np.nan

5. 类型转换

数据类型转换
# 查看类型
df.dtypes

# 数值转换
df['price'] = pd.to_numeric(df['price'], errors='coerce') # 无法转换的变 NaN

# 日期转换
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
df['date'] = pd.to_datetime(df['date'], errors='coerce') # 无法解析的变 NaT

# 分类类型(节省内存)
df['city'] = df['city'].astype('category')

# bool 转换
df['is_active'] = df['is_active'].astype(bool)

# 降低数值精度节省内存
df['id'] = df['id'].astype('int32') # int64 → int32
df['score'] = df['score'].astype('float32') # float64 → float32

6. 字符串清洗

字符串清洗
# 去除多余空格
df['name'] = df['name'].str.strip()

# 统一大小写
df['email'] = df['email'].str.lower()

# 替换特殊字符
df['phone'] = df['phone'].str.replace('-', '').str.replace(' ', '')

# 提取数字
df['amount'] = df['amount_str'].str.extract(r'(\d+\.?\d*)').astype(float)

# 统一分类值
df['gender'] = df['gender'].str.strip().str.lower()
df['gender'] = df['gender'].replace({'m': 'male', 'f': 'female', '男': 'male', '女': 'female'})

完整清洗模板

数据清洗实战模板
import pandas as pd
import numpy as np

def clean_data(df):
"""通用数据清洗流程"""
print(f"原始数据: {df.shape}")

# 1. 删除完全重复行
df = df.drop_duplicates()
print(f"去重后: {df.shape}")

# 2. 处理缺失值
# 数值列用中位数填充
num_cols = df.select_dtypes(include=[np.number]).columns
for col in num_cols:
df[col] = df[col].fillna(df[col].median())

# 分类列用众数填充
cat_cols = df.select_dtypes(include=['object', 'category']).columns
for col in cat_cols:
df[col] = df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else 'Unknown')

# 3. 字符串清洗
for col in cat_cols:
df[col] = df[col].str.strip()

# 4. 异常值处理(IQR 方法截断)
for col in num_cols:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
df[col] = df[col].clip(Q1 - 1.5*IQR, Q3 + 1.5*IQR)

print(f"清洗后: {df.shape}")
return df

常见面试问题

Q1: 缺失值填充用均值还是中位数?

答案

  • 均值适合数据近似正态分布、无明显异常值的场景
  • 中位数稳健,不受异常值影响,推荐作为默认选择
  • 分类变量用众数或特殊标记值如 "Unknown"
  • 最佳实践是按业务分组填充,如用同品类的中位数填充缺失价格

Q2: dropna()fillna() 怎么选?

答案

  • 缺失比例 < 5%dropna() 删除即可,数据损失小
  • 缺失比例 5%~30%fillna() 填充,避免丢失过多样本
  • 缺失比例 > 50%:考虑删除该列,或者缺失本身做为一个特征
  • 如果缺失不是随机的(如高收入用户不愿填写收入),删除会引入偏差,应填充

Q3: 如何检测和处理异常值?

答案

  • 3σ 法则:假设正态分布,超过均值 ± 3 倍标准差的为异常值
  • IQR 法Q1 - 1.5*IQRQ3 + 1.5*IQR 之外为异常值(更稳健)
  • 处理方式:删除、截断(clip)、替换为 NaN 再填充
  • 注意:需结合业务判断,如电商中的大额订单可能是正常的批发单

Q4: category 类型有什么好处?

答案

# 将字符串列转为 category
df['city'] = df['city'].astype('category')
  • 内存节省:重复字符串只存一次编码,可节省 50%~90% 内存
  • 性能提升:groupby、排序等操作更快
  • 适用场景:取值范围有限的分类列(如城市、状态、等级)

Q5: 如何高效处理千万级数据的清洗?

答案

  1. 分块读取pd.read_csv(chunksize=100000) 分批处理
  2. 指定类型dtype={'id': 'int32'} 减少内存
  3. 只读需要的列usecols=['col1', 'col2']
  4. 使用 Parquet:列式存储,读取比 CSV 快 5~10 倍
  5. 考虑 Polars:Rust 后端,比 Pandas 快 5~20 倍

相关链接