3. 缺失值处理(上):检测、删除与填充

各位同学,今天我们来聊聊材料数据清洗里最让人头疼,但也最基础的一个环节——缺失值处理。

说实话,我做了这么多年材料信息学项目,几乎没遇到过一份干干净净、一个空值都没有的数据集。不管是实验数据还是计算数据,总会有那么几个「坑」等着你去填。你想想看,一个高温合金的蠕变实验,可能因为设备故障少记录了几个温度点;或者一组 XRD 数据,某个样品因为制样问题导致部分峰位缺失。这些情况太常见了。

所以,掌握缺失值的处理,是每个材料数据工程师的必修课。今天我们先讲上半部分:怎么发现缺失值,怎么删掉它们,以及怎么填充它们。

3.1 检测缺失值:isnull() 与 isna()

首先,你得知道数据里哪些地方是空的。Pandas 提供了两个非常直观的方法:isnull()isna()。其实它们俩完全一样,没有区别。我个人习惯用 isna(),因为拼写短一点,哈哈。

来看个例子。假设我们有一份材料成分与性能的数据表:

import pandas as pd
import numpy as np

# 模拟一份材料数据
data = {
    '合金牌号': ['Inconel 718', 'Ti-6Al-4V', 'Al 6061', 'SS 316', None],
    '抗拉强度(MPa)': [1375, 950, 310, 580, np.nan],
    '屈服强度(MPa)': [1100, 880, 275, 290, 450],
    '延伸率(%)': [12, 14, 17, 22, None],
    '硬度(HRC)': [40, 36, None, 22, 30]
}
df = pd.DataFrame(data)
print(df)

输出结果里,你会看到 NaN 或者 None。这时候用 isna() 一照,所有缺失值的位置就都暴露了:

print(df.isna())

它会返回一个同样大小的布尔型 DataFrame,缺失的地方是 True,正常的地方是 False

如果你想快速知道每一列有多少缺失值,可以这样:

print(df.isna().sum())

嗯,这里要注意:sum() 会把 True 当作 1,False 当作 0 来加总。所以一行代码就能看到每列的缺失数量。

小技巧: 我经常在项目一开始就执行 df.isna().sum() / len(df) * 100,看看每列缺失值的百分比。如果某列缺失超过 50%,我会认真考虑要不要直接扔掉这列。

3.2 删除缺失值:dropna()

检测完了,接下来就是处理。最粗暴的方式就是——删掉。

dropna() 方法默认会删除任何包含缺失值的行。说白了,只要某一行有一个空值,整行就没了。

df_clean = df.dropna()
print(df_clean)

但这样做风险很大。我曾经在一个项目中,因为直接用了默认的 dropna(),结果把 70% 的数据都删掉了,最后模型根本没法训练。所以,我建议你慎用。

更灵活的做法是指定参数:

  • axis=0:删除行(默认)
  • axis=1:删除列
  • how='any':只要有一个缺失就删(默认)
  • how='all':全部缺失才删
  • thresh=n:保留至少有 n 个非空值的行/列

举个例子,如果你只想删除那些「全部都是空值」的行:

df.dropna(how='all', inplace=True)

或者,你只关心某几列是否有缺失,比如「抗拉强度」和「延伸率」不能为空:

df.dropna(subset=['抗拉强度(MPa)', '延伸率(%)'], inplace=True)
避坑指南: 我曾经在删除缺失值时忘了加 inplace=True,结果发现原数据根本没变。后来我养成了习惯——要么显式赋值给新变量,要么明确写上 inplace=True。别指望默认行为。

3.3 填充缺失值:fillna()

删除不是万能的,更多时候我们需要填充。Pandas 的 fillna() 方法非常强大,支持多种填充策略。

3.3.1 固定值填充

最简单的,用某个固定值去填。比如,对于缺失的硬度值,我们可以用 0 或者一个经验值:

df['硬度(HRC)'].fillna(0, inplace=True)

但说实话,用 0 填充在材料数据里往往不合理。硬度为 0 的材料?不存在嘛。所以更常见的做法是用均值、中位数或众数。

3.3.2 统计量填充

# 用均值填充
df['抗拉强度(MPa)'].fillna(df['抗拉强度(MPa)'].mean(), inplace=True)

# 用中位数填充(适合有异常值的情况)
df['硬度(HRC)'].fillna(df['硬度(HRC)'].median(), inplace=True)

# 用众数填充(适合分类数据)
df['合金牌号'].fillna(df['合金牌号'].mode()[0], inplace=True)

我个人比较喜欢用中位数。为什么?因为材料数据经常有异常值——比如某个样品的抗拉强度特别高,可能是测量误差。均值会被这个异常值拉偏,但中位数不会。你想想看,是不是这个道理?

3.3.3 前向/后向填充

对于时间序列类的材料数据(比如连续监测的腐蚀速率、疲劳循环数据),用前后值填充往往更合理。

# 前向填充:用上一个有效值填充
df.fillna(method='ffill', inplace=True)

# 后向填充:用下一个有效值填充
df.fillna(method='bfill', inplace=True)

我记得有一次处理连续退火炉的温度数据,中间有一段传感器故障导致数据缺失。用 ffill 填充后,模型预测效果反而更好了。因为温度变化是连续的,用前一个值填充比用均值更符合物理规律。

3.3.4 插值填充

如果你想要更平滑的填充效果,可以用 interpolate() 方法。它默认是线性插值:

df['延伸率(%)'].interpolate(method='linear', inplace=True)

对于材料数据,线性插值在大多数情况下已经够用了。但如果你的数据有明确的物理模型(比如 Arrhenius 关系),你也可以用 method='polynomial' 或者自定义函数。

核心要点: 填充缺失值没有银弹。你得根据数据的物理意义、缺失模式和分析目的来选择策略。我一般会先画个分布图看看,再决定用均值、中位数还是插值。

3.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张流程图:

缺失值处理知识体系 缺失值处理 检测缺失值 删除缺失值 填充缺失值 isnull() isna() how='any' how='all' thresh=n 固定值 统计量 前/后向 均值 中位数 众数

这张图把今天讲的内容串起来了。从检测到删除再到填充,每一步都有对应的 Pandas 方法。你可以把它当作一个速查表。

3.5 实战建议

最后,给你几个我在项目中总结出来的经验:

  1. 先检测,再决策。 别一上来就 fillna。先用 isna().sum() 看看缺失的分布,再决定策略。
  2. 分类数据用众数,连续数据用中位数。 这是最稳妥的默认方案。
  3. 时间序列数据优先用 ffill/bfill。 物理过程往往是连续的,前后填充比统计量更合理。
  4. 记录你的填充策略。 我习惯在代码注释里写明「为什么用中位数而不是均值」,方便以后复盘。
  5. 不要过度填充。 如果某列缺失超过 70%,直接删掉这列可能比强行填充更明智。

好了,缺失值处理的上半部分就到这里。记住,数据清洗没有标准答案,只有最适合你当前问题的方案。多动手试试,慢慢就有感觉了。


专注资料整理