数据清洗是数据分析和机器学习项目中最耗时的环节。模型效果不好时,原因不一定是算法复杂度不够,很多时候是缺失值、重复数据、异常值、类型错误和类别编码没有处理好。

本文整理 Pandas 中常见的数据清洗场景,包括读取数据、查看概况、处理缺失值、去重、类型转换、异常值过滤、文本字段清理和类别编码。

读取数据并查看概况

假设有一个用户行为数据文件:

1
2
3
import pandas as pd

df = pd.read_csv("users.csv")

先不要急着建模,应该先查看基本信息:

1
2
3
print(df.head())
print(df.info())
print(df.describe())

head 查看前几行,info 查看字段类型和缺失情况,describe 查看数值字段的分布。

统计缺失值:

1
2
missing = df.isna().sum().sort_values(ascending=False)
print(missing)

如果某个字段缺失率很高,要判断它是否还有业务价值。

处理重复数据

查看重复行数量:

1
2
duplicated_count = df.duplicated().sum()
print(duplicated_count)

删除完全重复的行:

1
df = df.drop_duplicates()

如果按业务主键去重,例如用户 ID:

1
df = df.drop_duplicates(subset=["user_id"], keep="last")

keep="last" 表示保留最后一条。对于按时间更新的数据,通常要先按时间排序:

1
2
df = df.sort_values("updated_at")
df = df.drop_duplicates(subset=["user_id"], keep="last")

缺失值处理

缺失值处理没有固定答案,取决于字段含义。

删除缺失严重的列:

1
2
threshold = len(df) * 0.5
df = df.dropna(axis=1, thresh=threshold)

删除关键字段缺失的行:

1
df = df.dropna(subset=["user_id", "label"])

数值字段可以用中位数填充:

1
df["age"] = df["age"].fillna(df["age"].median())

类别字段可以用特殊值填充:

1
df["city"] = df["city"].fillna("unknown")

不要盲目用平均值填充所有字段。对于收入、价格、访问次数等偏态分布明显的数据,中位数通常更稳。

类型转换

CSV 读取后,日期、金额、布尔值可能被解析成字符串。日期转换:

1
df["created_at"] = pd.to_datetime(df["created_at"], errors="coerce")

errors="coerce" 会把非法日期转成 NaT,方便后续统计和清理。

数值转换:

1
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")

布尔字段转换:

1
2
3
4
5
6
df["is_active"] = df["is_active"].map({
"yes": True,
"no": False,
"1": True,
"0": False,
})

转换后再次查看:

1
print(df.info())

文本字段清理

文本字段常见问题是前后空格、大小写不统一、空字符串和异常字符。

1
2
3
4
5
6
df["email"] = (
df["email"]
.astype("string")
.str.strip()
.str.lower()
)

把空字符串转成缺失值:

1
df["email"] = df["email"].replace("", pd.NA)

提取邮箱域名:

1
df["email_domain"] = df["email"].str.extract(r"@(.+)$")

清理手机号时,不要简单转成数字,因为前导 0、国家码和分隔符都可能有意义。通常应按字符串处理。

异常值处理

异常值要结合业务判断。比如年龄小于 0 或大于 150 明显不合理:

1
df = df[(df["age"] >= 0) & (df["age"] <= 150)]

对于金额、访问次数等字段,可以使用分位数截断:

1
2
3
lower = df["amount"].quantile(0.01)
upper = df["amount"].quantile(0.99)
df["amount_clipped"] = df["amount"].clip(lower, upper)

不要随意删除所有极端值。有些极端值可能是真实高价值用户或异常风险样本,正是模型需要学习的对象。

类别编码

少量类别可以使用 one-hot:

1
df = pd.get_dummies(df, columns=["gender", "city"], dummy_na=True)

如果类别数量很多,例如商品 ID、城市、设备型号,要谨慎 one-hot,否则维度会很高。可以考虑:

  • 合并低频类别为 other
  • 使用目标编码或频次编码。
  • 在深度学习模型中使用 embedding。

合并低频类别示例:

1
2
3
counts = df["city"].value_counts()
common_cities = counts[counts >= 100].index
df["city_grouped"] = df["city"].where(df["city"].isin(common_cities), "other")

保存清洗结果

清洗后的数据可以保存为 CSV 或 Parquet:

1
df.to_csv("users_clean.csv", index=False)

如果数据量较大,推荐 Parquet:

1
df.to_parquet("users_clean.parquet", index=False)

Parquet 能保留类型信息,读取速度和文件体积通常优于 CSV。

小结

Pandas 数据清洗的核心是先理解数据,再做处理。常见步骤包括查看概况、处理重复、填补缺失、转换类型、清理文本、过滤异常值和编码类别。每一步都要结合字段业务含义,不要机械套用规则。清洗逻辑最好写成可复用脚本,保证训练、验证和线上数据处理流程一致。