77_Python数据清洗实战技巧

Python数据清洗实战:从脏数据到干净数据

文章目录

前言

真实世界中的数据往往是"脏"的:缺失值、重复记录、不一致的格式、异常值......这些问题如果不经过处理就投入分析,轻则得出错误结论,重则导致业务决策失误。数据分析和机器学习领域有一句名言:"Garbage In, Garbage Out "------再好的模型也救不了脏数据。数据清洗是数据分析流程中耗时最长(通常占70%-80%的时间)但也是最关键的一步。本文聚焦pandas中数据清洗的六大核心技巧,通过实战代码带你彻底搞定脏数据,建立标准化的清洗流程。

面试实录:"你如何处理一份有50%缺失值的数据?"------这不是一个简单的"用均值填充"的问题。面试官想听到的是你根据场景做决策的能力:先分析缺失原因(随机缺失还是系统缺失?),再判断能否通过其他字段推导,最后选择删除、填充或保留为特殊类别的策略。这篇文章会帮你建立这种分析思维。

一、缺失值检测与处理

缺失值是数据清洗中最常见的问题。在pandas中,Nonenp.nan 都被视为缺失值。了解缺失值的分布------哪些列缺失、缺失比例是多少------是决定处理策略的前提。

首先要了解缺失值的分布情况:

python 复制代码
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "姓名": ["张三", "李四", np.nan, "赵六", "孙七"],
    "年龄": [25, np.nan, 28, 35, np.nan],
    "薪资": [15000, 20000, np.nan, np.nan, 22000],
    "部门": ["技术", "销售", "技术", None, "销售"],
    "绩效": [85.5, 90.0, 78.0, 88.5, None],
})

# 检测缺失值
print(df.isnull().sum())           # 每列缺失数量
print(df.isnull().sum().sum())     # 总缺失数量
print(df[df["薪资"].isnull()])     # 薪资缺失的行

isnull() vs notnull()isnull() 返回布尔DataFrame(True表示缺失),配合 sum() 可以统计每列缺失数。isnull().sum().sum() 统计全部缺失值总数。notnull()isnull() 的反向操作。在实际项目中,通常先算缺失比例(df.isnull().sum() / len(df)),设定阈值决定是删除该列、删除该行还是填充。

缺失值检测完成后,有三种主流处理策略------删除、填充和插值。选择哪种策略取决于缺失比例和业务含义:

python 复制代码
# 策略一:删除缺失值
df_dropna = df.dropna()                    # 删除含任何缺失值的行
df_drop_all = df.dropna(how="all")          # 删除全为缺失值的行
df_drop_sub = df.dropna(subset=["薪资"])    # 仅关注特定列
df_drop_thresh = df.dropna(thresh=4)       # 至少有4个非空值才保留

# 策略二:填充缺失值
df["年龄"] = df["年龄"].fillna(df["年龄"].mean())    # 均值填充
df["部门"] = df["部门"].fillna("未知")               # 固定值填充
df["薪资"] = df["薪资"].fillna(method="ffill")       # 前向填充(用前一行的值)
df["薪资"] = df["薪资"].fillna(method="bfill")       # 后向填充

# 策略三:插值法填充(适合时间序列)
df["年龄"] = df["年龄"].interpolate(method="linear")

删除 vs 填充的选择依据 :如果缺失比例很低(<5%),删除缺失行通常损失不大;但如果一组关键列(如"薪资")缺失超过30%且无法从其他数据中推断,删除该列可能更合适。填充策略中,均值填充 适合对称分布的数据,中位数填充 适合有偏分布(因为中位数不受极端值影响),众数填充适合分类数据。
fillna的method参数ffill(前向填充)和 bfill(后向填充)来自pandas中 fillna 的方法参数,适合时间序列或有序数据------比如用前一天的温度填充今天的缺失值。interpolate() 则做线性插值,在两个已知值之间按距离比例估算中间值,比简单的前向/后向填充更精确。

二、重复值处理

重复数据通常来自数据采集的多次提交、日志重复记录或多源数据合并时的交叉。去重看似简单,实际需要判断"什么是真正的重复"------是按所有列完全相同才算重复,还是某几个关键字段相同就算重复?

python 复制代码
# 创建含重复的数据
df.duplicated().sum()              # 统计重复行数量
print(df[df.duplicated()])         # 查看重复行
print(df[df.duplicated(keep=False)])  # 显示所有重复行

# 删除重复值
df_unique = df.drop_duplicates()                     # 全部列重复才删除
df_unique = df.drop_duplicates(subset=["姓名"])       # 按指定列去重
df_unique = df.drop_duplicates(subset=["姓名"], keep="first")   # 保留第一个
df_unique = df.drop_duplicates(subset=["姓名"], keep="last")    # 保留最后一个
df_unique = df.drop_duplicates(subset=["姓名"], keep=False)     # 全不保留

duplicated() 的工作原理 :默认情况下,duplicated() 将第一条出现的记录标记为False(不重复),后续相同的记录标记为True(重复)。keep='last' 则反过来,保留最后一条。keep=False 把所有重复记录全部标记为True,方便查看所有重复项。
实际陷阱:去重前一定要明确"重复"的定义。比如用户表中有两条"张三",但手机号不同------这可能是两个同名的人,也可能是同一个人换了手机号。按姓名去重会误删数据。所以在实际业务中,应使用ID、手机号等唯一标识字段来去重。

三、数据类型转换

正确的数据类型是高效计算的前提。pandas在读取数据时会自动推断类型,但推断结果不一定准确------比如数字中混入了符号,被误判为字符串类型。显式检查和转换类型是数据清洗的必要步骤。

python 复制代码
# 查看列类型
print(df.dtypes)

# 字符串转数值
df["年龄"] = pd.to_numeric(df["年龄"], errors="coerce")

# 数值转类别
df["部门"] = df["部门"].astype("category")

# 字符串转日期
df["入职日期"] = pd.to_datetime(df["入职日期"], format="%Y-%m-%d")

# 类型转换通用方法
df["薪资"] = df["薪资"].astype("float64")
df["ID"] = df["ID"].astype("int64")

# 同时转换多列
df = df.astype({"年龄": "int64", "薪资": "float64", "部门": "category"})

# 处理转换异常
df["年龄"] = df["年龄"].apply(
    lambda x: pd.to_numeric(x, errors="coerce") if isinstance(x, str) else x
)

errors='coerce' 的作用 :当字符串无法转换时,coerce 会将其置为NaN而不是抛出异常。这是批量类型转换中最安全的参数。如果你希望严格校验------任何无法转换的值都应该报错提醒------可以使用 errors='raise'
category 类型的优势 :将重复值很多的列(如"部门"只有"技术/销售/市场"三个值)转换为 category 类型可以大幅节省内存(可能减少90%以上),同时加速分组和排序操作。对于大规模数据,这是一个低成本高回报的优化。

四、apply函数:灵活的数据变换

apply() 是pandas极其强大的工具,可以对每一行或每一列应用自定义函数:

python 复制代码
# 对列应用函数
df["薪资等级"] = df["薪资"].apply(lambda x: "高" if x > 20000 else "低")

# 定义复杂函数
def categorize_salary(salary, dept):
    if pd.isna(salary):
        return "未知"
    if dept == "技术":
        return "高" if salary > 18000 else "中"
    else:
        return "高" if salary > 20000 else "中"

df["薪资等级"] = df.apply(lambda row: categorize_salary(row["薪资"], row["部门"]), axis=1)

# 批量文本清理
df["姓名"] = df["姓名"].apply(lambda x: str(x).strip() if pd.notna(x) else x)
df["部门"] = df["部门"].str.replace(" ", "").str.upper()

axis=1 vs axis=0df.apply(func, axis=1) 按行应用(每行作为一个Series传给func),axis=0 按列应用。对于需要同时访问多列数据的场景(如根据"薪资"和"部门"一起判断等级),必须用 axis=1。但要注意,axis=1 的性能比 axis=0 差很多------大型DataFrame上慎用。
str访问器df["部门"].str 是pandas为字符串列提供的专用访问器,支持 .replace().upper().split() 等字符串方法。它在内部做向量化操作,比 apply(lambda x: x.upper()) 快很多。

五、异常值处理

python 复制代码
# IQR方法检测异常值
Q1 = df["薪资"].quantile(0.25)
Q3 = df["薪资"].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df["薪资"] < lower_bound) | (df["薪资"] > upper_bound)]
print(f"异常值数量: {len(outliers)}")

# 处理异常值
df_clipped = df.copy()
df_clipped["薪资"] = df_clipped["薪资"].clip(lower_bound, upper_bound)  # 截断

# Z-Score方法
from scipy import stats
z_scores = stats.zscore(df["薪资"].dropna())
outliers_z = df["薪资"].dropna()[abs(z_scores) > 3]

IQR vs Z-Score的选择:IQR方法基于四分位数,对数据分布没有假设,适合任意形状的分布,是业界默认的稳健方法。Z-Score方法假设数据近似正态分布,超过3个标准差视为异常。如果数据有偏分布(如薪资往往右偏),IQR的鲁棒性更好。
异常值不一定都要删除:有些"异常值"可能是业务最有价值的数据------比如高价值客户、峰值流量事件。处理方法除了删除和截断,还可以单独拎出来做专项分析,或者用对数变换压缩量级。不要机械地删除所有超出上下界的记录。

python 复制代码
def clean_data(df):
    """标准化数据清洗流程"""
    df = df.copy()

    # 1. 去除首尾空格
    str_cols = df.select_dtypes(include="object").columns
    for col in str_cols:
        df[col] = df[col].str.strip()

    # 2. 处理缺失值
    df["数值列"] = df["数值列"].fillna(df["数值列"].median())
    df["类别列"] = df["类别列"].fillna("未知")

    # 3. 去重
    before = len(df)
    df = df.drop_duplicates()
    print(f"去重: {before} -> {len(df)} 条记录")

    # 4. 类型转换
    df["日期列"] = pd.to_datetime(df["日期列"])

    # 5. 异常值处理
    numeric_cols = df.select_dtypes(include="number").columns
    for col in numeric_cols:
        mean, std = df[col].mean(), df[col].std()
        df = df[df[col].between(mean - 3*std, mean + 3*std)]

    return df

# 使用
df_clean = clean_data(df)

总结

数据清洗没有万能公式,但有一套标准化的方法论:检测->决策(删/填/转/去重/降噪)->验证 。本文涵盖了缺失值处理、重复值去重、类型转换、apply灵活变换、异常值处理的完整工具链。建议在每次分析前都执行一到两遍清洗流程,并在清洗前后分别用df.describe()df.info()对比数据特征,确保清洗操作符合预期。

✅ 亮点总结

  • 缺失值处理覆盖了删除(dropna)、填充(fillna)和插值三种策略,应对不同业务场景
  • drop_duplicates() 配合 subset/keep 参数可精确控制去重逻辑
  • astype()pd.to_numeric(errors='coerce') 是解决数据类型混乱的核心工具
  • IQR法则(Q1-1.5×IQR, Q3+1.5×IQR)提供了统计意义上的异常值检测标准
  • 将清洗流程封装为可复用函数,告别每次手写重复代码的低效工作方式

适用场景

  • 数据分析项目预处理:拿到一份脏数据后,执行标准化清洗流程再做分析
  • 数据迁移与整合:从多个外部系统导入数据时,统一清洗格式、剔除无效记录
  • 自动化ETL任务:编写定时脚本,自动完成数据抽取、清洗、转换和入库

扩展方向

  • 正则表达式进阶清洗:学习 str.extract()str.replace() 处理非结构化文本字段
  • 大规模数据清洗:使用 chunksize 参数分块读取和清洗超大文件,避免内存溢出
  • 推荐阅读下一篇《Python数据可视化matplotlib》,将清洗后的数据用图表直观呈现
相关推荐
落寞的电源1 小时前
Delegate = Object + MethodInfo
开发语言·数据库·c#
你怎么知道我是队长1 小时前
JavaScript的介绍
开发语言·javascript·ecmascript
江华森2 小时前
04-python-面向对象
开发语言·python
梦帮科技2 小时前
基于EVM架构的Web3音频确权与RNS Token智能合约设计深度解析
人工智能·python·架构·web3·区块链·音视频·智能合约
AI行业学习2 小时前
Notepad++ 官方纯净下载+完整安装教程(Windows)【2026.7.5】
开发语言·windows·python·前端框架·html·notepad++
伏 念2 小时前
Hermes Agent
python
江华森2 小时前
从零构建搜索引擎:Python 异步爬虫 + 倒排索引 + Sanic 前后端实战
python
2401_868534782 小时前
网络规划设计师学习笔记
python·计算机网络
前端百草阁2 小时前
JavaScript 设计模式(23 种)
开发语言·前端·javascript·设计模式