Pandas缺失值处理全攻略:从诊断到修复的完整工作流

📅 2026/7/29 17:28:18 👁️ 阅读次数 📝 编程学习
Pandas缺失值处理全攻略:从诊断到修复的完整工作流

1. 项目概述:从数据“体检”到“治疗”的完整闭环

做数据分析的朋友,尤其是刚入门用Python和Pandas的,估计都遇到过这种情况:从数据库、Excel或者爬虫抓来的数据,打开一看,好家伙,一堆NaNNone或者干脆就是空单元格。这些就是数据里的“缺失值”,它们就像机器里的沙子,不处理干净,后续的分析、建模基本就是“垃圾进,垃圾出”,结果根本没法看。

今天要聊的,就是处理这些缺失值最核心、最常用的一套“组合拳”:info()isnull()dropna()fillna()。你别看就四个函数,它们构成了一个从“诊断”到“治疗”的完整工作流。很多新手一上来就急着用fillna()填数,或者用dropna()删行,这其实很危险。就像医生看病,不先做检查(info()isnull())就直接开刀,很容易误诊。

这套方法适用于任何需要处理表格数据的场景,无论是金融风控、电商用户分析、科研实验数据清洗,还是简单的个人记账整理。只要你用Pandas读入了DataFrame,这套流程就是你的标准操作程序。接下来,我会以一个模拟的电商用户数据集为例,带你走一遍完整的流程,把每个函数的“为什么用”、“怎么用”以及“用的时候要注意什么”都掰开揉碎了讲清楚。

2. 核心思路:诊断先行,治疗在后

处理缺失值,最忌讳的就是拍脑袋决策。一个成熟的流程,一定是先搞清楚“病情”,再决定“治疗方案”。我们的核心思路可以概括为“四步走”:

  1. 全局扫描 (info()): 快速了解数据的“体检报告”,看看有多少列,每列有多少非空值,对数据整体健康状况有个数。
  2. 精准定位 (isnull()): 拿着“显微镜”和“CT机”,精确找出每一个缺失值的位置,量化缺失的严重程度。
  3. 评估与决策: 这是最关键的一步,基于前两步的诊断结果,结合业务知识,决定是“切除”(删除)还是“修补”(填充)。决策依据包括缺失比例、列的重要性、数据量大小等。
  4. 执行治疗 (dropna()fillna()): 根据决策,执行删除或填充操作,并验证效果。

这个流程的核心思想是“数据驱动决策”。我们不是凭感觉去删或填,而是基于数据本身呈现出的客观事实来做选择。比如,一列数据缺失了90%,那它很可能已经失去了分析价值,直接删除这列比绞尽脑汁去填充要明智得多。反之,如果一列至关重要(比如用户ID)且只缺失了0.1%,那我们就需要非常谨慎地采用合适的策略进行填充。

3. 数据准备与初步诊断:info()函数详解

在开始治疗前,我们得先知道病人长什么样。假设我们手头有一个user_data.csv文件,记录了电商平台的部分用户信息。

import pandas as pd import numpy as np # 模拟创建一份有缺失值的数据 data = { ‘user_id‘: [1001, 1002, 1003, 1004, 1005, 1006], ‘age‘: [25, np.nan, 30, 35, np.nan, 40], ‘gender‘: [‘M‘, ‘F‘, np.nan, ‘M‘, ‘F‘, ‘M‘], ‘city‘: [‘Beijing‘, ‘Shanghai‘, ‘Guangzhou‘, np.nan, ‘Shenzhen‘, ‘Beijing‘], ‘last_purchase_amount‘: [150.0, 89.5, np.nan, 200.0, 120.0, np.nan], ‘vip_level‘: [1, np.nan, 2, 1, 3, 2] } df = pd.DataFrame(data) print(“原始数据:“) print(df)

运行后,你会看到一个典型的包含缺失值(显示为NaN)的DataFrame。现在,我们请出第一位“医生”——info()

df.info()

这行简单的命令会输出一份“数据体检报告”:

<class ‘pandas.core.frame.DataFrame‘> RangeIndex: 6 entries, 0 to 5 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 6 non-null int64 1 age 4 non-null float64 2 gender 5 non-null object 3 city 5 non-null object 4 last_purchase_amount 4 non-null float64 5 vip_level 5 non-null float64 dtypes: float64(3), int64(1), object(2) memory usage: 420.0+ bytes

报告解读与实操要点:

  • 数据概览:首先告诉你这是一个DataFrame,有6行(索引0到5),6列。
  • 核心信息——Non-Null Count:这是info()在缺失值诊断中最重要的输出。它直接显示了每一列非空值的数量。
    • user_id: 6 non-null。完美,没有缺失。
    • age: 4 non-null。总共有6行,说明有2个缺失值(6-4=2)。
    • gender: 5 non-null。缺失1个。
    • city: 5 non-null。缺失1个。
    • last_purchase_amount: 4 non-null。缺失2个。
    • vip_level: 5 non-null。缺失1个。
  • 数据类型Dtypeagelast_purchase_amountvip_level显示为float64,这是因为Pandas用np.nan(一个浮点数)来表示缺失,导致整数列被向上转型为浮点型。这是一个非常重要的细节,如果你期望vip_level是整数,在填充后可能需要用astype(int)转换回来。
  • 内存占用:约420字节,数据量很小。

注意info()默认只显示数据概览。如果你想看到更详细的信息,比如每列的唯一值数量、最大值、最小值等,应该使用df.describe(include=‘all‘)info()的核心优势在于快速定位缺失列,而不是做全面的统计分析。

为什么先看info()因为它快。一秒钟之内,你就能对数据集的完整性和结构有一个宏观把握,知道该把注意力重点放在哪几列上。在我们的例子里,agelast_purchase_amount缺失最多(2处),是需要重点关注的“重症”列。

4. 深度检测与量化分析:isnull()与相关函数

info()告诉了我们哪些列“病了”,但没告诉我们具体是哪些“细胞”坏了。isnull()(或它的别名isna())就是我们的“显微镜”。

4.1 基本使用:定位每一个缺失值

df.isnull()会对整个DataFrame进行扫描,返回一个形状相同的布尔型DataFrame,缺失值位置为True,非缺失为False

missing_mask = df.isnull() print(“缺失值布尔掩码:“) print(missing_mask)

输出:

user_id age gender city last_purchase_amount vip_level 0 False False False False False False 1 False True False False False True 2 False False True False True False 3 False False False True False False 4 False True False False False False 5 False False False False True False

现在一目了然了。例如,第1行(索引1)的agevip_level是缺失的;第2行的genderlast_purchase_amount是缺失的。

4.2 进阶应用:统计与可视化缺失情况

单纯看布尔表不够直观,我们通常需要一些统计量。

1. 按列统计缺失数量/比例:这是最常用的操作,它能量化问题的严重性。

# 计算每列的缺失数量 missing_count = df.isnull().sum() print(“每列缺失数量:“) print(missing_count) # 计算每列的缺失比例(更直观) missing_ratio = df.isnull().sum() / len(df) print(“\n每列缺失比例:“) print(missing_ratio)

输出:

每列缺失数量: user_id 0 age 2 gender 1 city 1 last_purchase_amount 2 vip_level 1 dtype: int64 每列缺失比例: user_id 0.000000 age 0.333333 gender 0.166667 city 0.166667 last_purchase_amount 0.333333 vip_level 0.166667 dtype: float64

决策参考:通常,我们会设定一个阈值。比如,如果一列缺失比例超过30%(0.3),我们就倾向于直接删除该列,因为填充的可靠性太低,且可能引入巨大偏差。在我们的数据中,agelast_purchase_amount缺失33%,处于需要谨慎评估的临界点。

2. 按行统计缺失数量:有时我们需要知道哪些行的数据质量特别差。

# 计算每行的缺失数量 missing_per_row = df.isnull().sum(axis=1) print(“每行缺失数量:“) print(missing_per_row) # 找出缺失严重的行(例如缺失2列及以上) severe_missing_rows = df[df.isnull().sum(axis=1) >= 2] print(“\n缺失超过2列的行:“) print(severe_missing_rows)

输出:

每行缺失数量: 0 0 1 2 2 2 3 1 4 1 5 1 dtype: int64 缺失超过2列的行: user_id age gender city last_purchase_amount vip_level 1 1002.0 NaN F Shanghai 89.5 NaN 2 1003.0 30.0 NaN Guangzhou NaN 2.0

我们发现第1行和第2行都缺失了2个字段。在后续处理时,这些行可能需要特别关注。

3. 可视化缺失矩阵(高级技巧):对于大型数据集,用seabornmissingno库进行可视化可以瞬间掌握全局缺失模式。

# 需要安装: pip install matplotlib missingno import missingno as msno import matplotlib.pyplot as plt msno.matrix(df) plt.show()

这张图能清晰显示数据缺失的分布情况,是否存在某些区块整体缺失(可能源于数据源问题),还是随机缺失。

实操心得df.isnull().sum()是我日常使用频率最高的缺失值分析命令,没有之一。它给出的比例是决策的黄金依据。记住,不要一上来就处理,先量化

5. 治疗方案一:直接删除 - dropna()函数详解

诊断完毕,如果决定“切除”,那就轮到dropna()上场了。它的逻辑很简单:丢掉含有缺失值的行或列。

5.1 基本用法与参数解析

# 默认参数:删除所有包含任何缺失值的行 df_dropped_rows = df.dropna() print(“删除任何缺失行后的数据(默认):“) print(df_dropped_rows) print(f“原始形状:{df.shape}, 删除后形状:{df_dropped_rows.shape}“)

输出:

删除任何缺失行后的数据(默认): user_id age gender city last_purchase_amount vip_level 0 1001.0 25.0 M Beijing 150.0 1.0 原始形状:(6, 6), 删除后形状:(1, 6)

结果触目惊心!因为默认参数how=‘any‘,只要某行有一个NaN,整行就被删除。6行数据只剩下了第0行这1行“完人”。在数据量本就不大的情况下,这种操作是毁灭性的,会损失大量信息。

所以,dropna()的关键在于参数调节

  • how: 删除条件。
    • ‘any‘(默认): 行或列中有任何缺失值就删除。
    • ‘all‘: 行或列中所有值都缺失才删除。这个参数实用得多。
  • axis: 删除方向。
    • 0‘index‘(默认): 删除行。
    • 1‘columns‘: 删除列。这是清理无效列的利器。
  • thresh: 阈值。这是一个非常强大但常被忽略的参数。它要求行或列中至少有多少个非缺失值才得以保留。
  • subset: 子集。指定只根据某些列来判断是否删除行,其他列的缺失值忽略。这是最常用、最精细的控制方式。

5.2 实战场景与参数选择

场景1:删除全为空的列有时候数据里会有一些完全没用的空列。

# 假设我们数据里多了一列全为空 df[‘useless_col‘] = np.nan print(“添加全空列后:“) print(df.columns.tolist()) df_cleaned_cols = df.dropna(axis=1, how=‘all‘) # 只删除整列为空的列 print(“\n删除全空列后:“) print(df_cleaned_cols.columns.tolist())

场景2:基于关键列删除行(最常用!)业务上,有些列是分析的核心,绝对不能缺失。比如,在用户分析中,user_id缺失的行毫无意义。又或者,我们只关心agelast_purchase_amount都存在的记录。

# 只删除在‘age‘和‘last_purchase_amount‘这两列上同时缺失的行 # 注意:其他列(如gender)有缺失不影响 df_selective_drop = df.dropna(subset=[‘age‘, ‘last_purchase_amount‘]) print(“仅删除‘age‘和‘last_purchase_amount‘同时缺失的行:“) print(df_selective_drop)

输出显示,第2行和第5行因为在这两列上都有缺失(一个缺失last_purchase_amount,一个缺失age),所以被删除了。第1行虽然age缺失,但last_purchase_amount存在,所以被保留。这比一刀切合理得多。

场景3:使用thresh保留数据相对完整的行我们希望保留那些至少填充了N个字段的行。

# 保留至少有4个非缺失值的行 (总共6列,允许缺失2列) df_thresh = df.dropna(thresh=4) print(“保留至少4个非空值的行:“) print(df_thresh)

这样,只缺失了1列的第3、4、5行都被保留了,缺失2列的第1、2行被删除。

避坑指南:使用dropna()前,务必先用info()isnull().sum()评估删除会损失多少数据。如果删除行会导致样本量锐减(比如超过20%),就要慎重考虑填充方案。永远记住,删除是最简单但也最粗暴的方案,可能带来样本偏差。

6. 治疗方案二:智能填充 - fillna()函数详解

当删除数据代价太大时,“修补”就成了首选。fillna()是填充缺失值的瑞士军刀,功能强大,但用法不当也会“补”出问题。

6.1 基本填充方法

# 1. 用单一值填充所有缺失值(慎用!) df_filled_0 = df.fillna(0) print(“用0填充所有缺失:“) print(df_filled_0) # 问题:gender列填了0, city列填了0,这从业务上解释不通。 # 2. 前向填充 (ffill) / 后向填充 (bfill) # 假设数据是按时间或逻辑顺序排列的,可以用前面或后面的值来填 df_filled_ffill = df.fillna(method=‘ffill‘) # 用上一个有效值填充 print(“\n前向填充:“) print(df_filled_ffill) # 注意:第1行的age用第0行的25填充了,但第0行之前没有数据,所以第0行如果是NaN则无法填充。

6.2 按列差异化填充(核心技巧)

这才是实战中最常用的方式。不同的列,业务含义不同,应该采用不同的填充策略。

# 定义一个填充字典,key是列名,value是填充值或策略 fill_values = { ‘age‘: df[‘age‘].mean(), # 数值型:用均值填充(假设年龄分布均匀) ‘gender‘: ‘Unknown‘, # 分类型:用‘Unknown‘这个新类别填充 ‘city‘: df[‘city‘].mode()[0], # 分类型:用众数填充(出现最多的城市) ‘last_purchase_amount‘: df[‘last_purchase_amount‘].median(), # 数值型:用中位数填充(抗干扰) ‘vip_level‘: df[‘vip_level‘].median() # 有序分类:用中位数填充 } # 注意:我们没有填充‘user_id‘,因为它没有缺失。 df_filled_smart = df.fillna(fill_values) print(“按列智能填充后的数据:“) print(df_filled_smart) print(“\n检查是否还有缺失:“) print(df_filled_smart.isnull().sum())

填充策略选择的“为什么”:

  • age(连续数值): 使用mean()均值。前提是年龄分布没有严重偏态,且缺失是随机的。如果数据有异常值(比如有个200岁的用户),均值会被拉偏,此时用median()中位数更稳健。
  • gender(名义分类): 使用‘Unknown‘。绝对不要用均值或众数!给性别填个“1.5”或强行指定为“男”都会扭曲分析。增加一个“未知”类别是更诚实的做法。
  • city(名义分类): 使用mode()[0]众数。填充为最常见的城市,这是一个合理的猜测。
  • last_purchase_amount(连续数值,可能有偏): 使用median()中位数。消费金额通常存在少数极高值(长尾分布),中位数比均值更能代表普通水平。
  • vip_level(有序分类,如1,2,3): 使用median()中位数。它保留了等级的序数意义。

6.3 高级填充技巧:插值法

对于时间序列数据(如股价、传感器读数),简单的均值填充可能不合适,因为数据点之间存在时间相关性。这时可以使用插值法。

# 假设df按时间排序,且‘price‘列有缺失 # df[‘price‘].interpolate(method=‘time‘) # 按时间索引插值 # df[‘price‘].interpolate(method=‘linear‘) # 线性插值(默认) # df[‘price‘].interpolate(method=‘polynomial‘, order=2) # 多项式插值

核心经验没有“最好”的填充方法,只有“最合适”的。填充的本质是用估计值代替真实值,必然会引入不确定性或偏差。你的填充策略必须建立在对业务的理解之上。在报告或模型中,必须明确指出哪些数据是经过填充的,填充的方法是什么,这关系到结果的可靠性。

7. 综合实战与决策流程

现在,我们把所有步骤串联起来,形成一个处理一份新数据集的标准化决策流程。

假设新数据集sales_data有1000行,20列。

  1. 第一步:快速体检 (df.info())

    • 发现‘customer_income‘缺失40%,‘last_login_days‘缺失5%,‘product_category‘缺失1%。
  2. 第二步:深度诊断 (df.isnull().sum(),df.isnull().sum(axis=1))

    • 确认缺失比例。发现‘customer_income‘缺失太高。
    • 检查是否有行大量缺失。发现10行数据缺失超过10个字段,这些可能是无效记录。
  3. 第三步:制定决策

    • 对于列:
      • ‘customer_income‘(缺失40%):比例过高,填充不可靠。决策:删除该列df = df.drop(columns=[‘customer_income‘])
      • ‘last_login_days‘(缺失5%):重要指标,需要填充。决策:用中位数填充,因为登录天数可能受极端值影响。df[‘last_login_days‘].fillna(df[‘last_login_days‘].median(), inplace=True)
      • ‘product_category‘(缺失1%):分类变量,缺失很少。决策:用**“Unknown”** 填充。df[‘product_category‘].fillna(‘Unknown‘, inplace=True)
    • 对于行:
      • 缺失超过10个字段的10行:数据质量极差,保留价值低。决策:删除这些行df = df[df.isnull().sum(axis=1) <= 10]
      • 对于剩下的行,可能还有零星缺失在其他非关键列,可以用fillna(method=‘ffill‘)或特定值填充。
  4. 第四步:执行与验证

    • 执行上述dropfillna操作。
    • 最终验证:再次运行df.info()df.isnull().sum(),确认所有缺失值已被妥善处理。

这个流程的核心是权衡:在数据量损失引入的偏差分析的便利性之间找到平衡点。对于关键特征的小比例缺失,精心选择的填充优于简单删除。对于高比例缺失的非关键特征,删除则是更干净利落的选择。

8. 常见陷阱、疑难杂症与排查技巧

在实际操作中,你会遇到比教程更复杂的情况。下面是一些踩坑实录和解决方案。

问题1:为什么填充了缺失值,但统计结果(如mean())还是不对?可能原因:Pandas的统计函数默认skipna=True,会自动跳过缺失值。但如果填充的值不合理(比如用0填充了年龄),计算出的均值自然会被拉低。排查:填充后,用df.describe()df[‘col‘].hist()快速查看数据分布,检查是否存在不合理的值域。

问题2:inplace=True参数用不用?df.fillna(0, inplace=True)会直接修改原dfdf_filled = df.fillna(0)会返回一个新DataFrame,原df不变。建议:在调试和探索阶段,不要用inplace=True,保留原始数据。只有当清洗流程完全确定后,才考虑使用inplace=True来节省内存。新手最容易犯的错就是一路inplace下去,想回头看看原始数据都没了。

问题3:缺失值不止有NaN怎么办?真实数据中,缺失可能以各种形式存在:空字符串“““NULL““N/A“-1999等。处理:在读取数据时就用pd.read_csv(…, na_values=[““, “NULL“, “N/A“, -1])参数指定哪些值应被识别为缺失值。或者读取后用df.replace(‘NULL‘, np.nan)进行替换。

问题4:填充后数据类型变了怎么办?如前所述,用浮点数np.nan填充整数列,该列会变成float64解决:填充完成后,使用df[‘col‘] = df[‘col‘].astype(‘int‘)转换回来。但要注意,如果填充的是小数(如均值),转换会截断小数部分。

问题5:如何处理时间序列中的缺失?简单的前后填充(ffill/bfill)或插值(interpolate)是常用方法,但要根据序列特性选择。对于有周期性的数据(如每日销售额),可以考虑用“去年同期”或“上周同天”的数据来填充,这需要更复杂的逻辑,通常需要自己写函数。

问题6:多重填充与不确定性(高级)对于严谨的建模任务,单一的确定性填充(如均值)会低估模型的不确定性。更先进的方法是多重插补,即创建多个填充后的数据集,分别建模后再合并结果。这可以通过scikit-learnIterativeImputer或专门的statsmodels等库来实现,但这已超出基础数据预处理的范围。

记住,数据清洗没有银弹。info()isnull()dropna()fillna()这四个函数是你工具箱里的核心工具,但如何运用它们,取决于你对数据的理解和要解决的问题。每一次处理,都是一次业务逻辑与数据事实之间的对话。