数据清洗占数据分析工作流的 60%-80%。这不是夸张,是血泪经验。
我用 Pandas 处理过从几百行到几亿行的数据集,踩过不少坑。今天把最常遇到的 5 个清洗问题和解决方案整理出来,全是能直接用的代码。
一、缺失值:删还是填?怎么选?
看到 DataFrame 里有 NaN,第一反应是赶紧处理。但怎么填,是有讲究的。
先看缺失分布:
import pandas as pd
df = pd.read_csv('sales_data.csv')
print(df.isnull().sum())
print(df.isnull().mean() * 100) # 各列缺失百分比
如果某一列缺失率超过 70%,直接删掉这一列通常是最省事的:
# 删除缺失率 > 70% 的列
high_null_cols = df.columns[df.isnull().mean() > 0.7]
df.drop(columns=high_null_cols, inplace=True)
对于数值型列,用中位数填充比均值更稳健,因为异常值会影响均值:
# 数值列 - 中位数填充
df['age'] = df['age'].fillna(df['age'].median())
# 也可以用众数(最频繁出现的值)填充分类变量
df['city'] = df['city'].fillna(df['city'].mode()[0])
如果你需要更智能的填充,比如根据其他列来预测缺失值,可以用 KNN 填充:
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=3)
numeric_cols = df.select_dtypes(include=['number']).columns
df[numeric_cols] = imputer.fit_transform(df[numeric_cols])
记住:不要盲目全用均值填充。先看看缺失是不是有规律的——比如某些地区的收入数据系统性缺失,那就值得单独分析,而不是简单掩盖。
二、重复数据:怎么识别和处理?
重复数据是分析的大敌。一条重复的记录会把平均值、总和全部扭曲。
# 查看总重复行数
print(f"重复行数: {df.duplicated.sum()}")
print(f"重复比例: {df.duplicated.sum() / len(df) * 100:.2f}%")
默认 duplicated() 只保留第一次出现的那一行,后续重复的都标记为 True。
删除重复行很简单:
# 删除所有重复行,只保留第一行
df.drop_duplicates(inplace=True)
# 也可以指定哪些列作为判断重复的依据
df.drop_duplicates(subset=['user_id', 'order_date'], keep='last', inplace=True)
注意:keep='last' 会保留最后一次出现的记录,keep=False 则把所有重复的都删掉。
有时候重复并不是完全一样,而是非常相似。比如用户名字拼写稍有不同:
# 模糊去重需要先安装 fuzzywuzzy
# from fuzzywuzzy import fuzz
def fuzzy_dedup(df, column, threshold=90):
"""基于相似度阈值进行模糊去重"""
indices_to_keep = []
for i, row in df.iterrows():
is_duplicate = False
for j in indices_to_keep:
if fuzz.ratio(row[column], df.loc[j, column]) > threshold:
is_duplicate = True
break
if not is_duplicate:
indices_to_keep.append(i)
return df.loc[indices_to_keep]
# df = fuzzy_dedup(df, 'company_name', threshold=85)
这个方法计算量较大,适合中小数据集。上百万行时建议先用聚类算法预筛选。
三、数据类型:整型被当成字符串了
读取 CSV 后,最容易遇到的问题是类型错误。数字被读成了字符串,日期没被正确解析。
# 检查当前类型
print(df.dtypes)
# 批量转换类型
df['price'] = pd.to_numeric(df['price'], errors='coerce') # 转换失败变成 NaN
df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce')
# 修复价格中的符号干扰
df['price'] = df['price'].replace('[\$,]', '', regex=True).astype(float)
errors='coerce' 是关键——它会让无法转换的值变成 NaN,而不是报错崩溃。之后再用之前说的方法处理这些新生成的缺失值。
整数类型也可以优化内存占用:
# 将 int64 转为更小的整数类型(如果值在范围内)
df['category_id'] = pd.to_numeric(df['category_id'], downcast='integer')
# 同样可以优化 float
df['rating'] = pd.to_numeric(df['rating'], downcast='float')
使用 df.info(memory_usage='deep') 可以查看实际内存占用,优化后可以节省大量内存。
四、字符串清洗:空格、大小写、特殊字符
文本数据经常带着各种脏东西。空格、多余换行、大小写混用,都是常见问题。
# 去除首尾空格
df['product_name'] = df['product_name'].str.strip()
# 统一大小写
df['email'] = df['email'].str.lower()
df['country'] = df['country'].str.title() # 每个单词首字母大写
# 替换内部多余空格
df['description'] = df['description'].str.replace(r'\s+', ' ', regex=True)
正则清洗也很常用:
# 去除所有非字母数字字符(除空格外)
df['phone'] = df['phone'].replace(r'[^\d]', '', regex=True)
# 提取邮箱用户名部分
df['username'] = df['email'].str.extract(r'(.+)@')
如果要做批量标准化,可以定义函数:
def normalize_text(text):
if pd.isna(text):
return text
text = str(text).strip().lower()
text = re.sub(r'\s+', ' ', text)
return text
df['comment'] = df['comment'].apply(normalize_text)
记住:清洗前先备份原始数据。有些清洗操作是不可逆的,万一发现有问题,得有原始数据回滚。
五、异常值检测:哪些数据需要特别关注?
异常值不一定要删掉,但它们需要被识别和处理。不同的场景有不同的处理方式。
方法 1:Z-score 法(适用于正态分布数据)
from scipy import stats
z_scores = stats.zscore(df['sales_amount'])
abs_z_scores = abs(z_scores)
# 超出 3 个标准差的视为异常
filtered = df[abs_z_scores < 3]
print(f"异常值数量: {(abs_z_scores >= 3).sum()}")
方法 2:IQR 法(四分位距,更鲁棒)
Q1 = df['sales_amount'].quantile(0.25)
Q3 = df['sales_amount'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['sales_amount'] < lower_bound) | (df['sales_amount'] > upper_bound)]
print(f"异常值数量: {len(outliers)}")
处理方式取决于业务场景:
- 明显输入错误(如年龄写成 200 岁)→ 修正或删除
- 极端但合理的值(如某次促销活动销量暴增)→ 保留,但做单独分析
- 系统错误导致的负值 → 检查数据来源,必要时过滤
六、性能提示:大数据量怎么处理?
当数据量大到内存吃紧时,上面的方法可能跑得慢或者报错。这里有几个提速技巧:
# 1. 只读取需要的列
df = pd.read_csv('large_data.csv', usecols=['id', 'date', 'amount'])
# 2. 指定数据类型,减少内存占用
dtype_map = {
'id': 'int32',
'date': 'object', # 可以先存字符串,处理后转 datetime
'amount': 'float32'
}
df = pd.read_csv('large_data.csv', dtype=dtype_map)
# 3. 分块处理
for chunk in pd.read_csv('huge_file.csv', chunksize=100000):
# 对每个块做清洗
chunk['amount'] = chunk['amount'].fillna(chunk['amount'].median())
# 保存或进一步处理
# 4. 使用 category 类型降低分类变量内存消耗
df['category'] = df['category'].astype('category')
数据清洗没有银弹,每一步都需要结合业务理解来做判断。上面这 5 个问题覆盖了日常 80% 的场景,掌握它们你已经能应对大多数情况了。
你的数据清洗过程中遇到过什么头疼的问题?是怎么解决的?欢迎分享交流。