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 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张流程图:
这张图把今天讲的内容串起来了。从检测到删除再到填充,每一步都有对应的 Pandas 方法。你可以把它当作一个速查表。
3.5 实战建议
最后,给你几个我在项目中总结出来的经验:
- 先检测,再决策。 别一上来就 fillna。先用
isna().sum()看看缺失的分布,再决定策略。 - 分类数据用众数,连续数据用中位数。 这是最稳妥的默认方案。
- 时间序列数据优先用 ffill/bfill。 物理过程往往是连续的,前后填充比统计量更合理。
- 记录你的填充策略。 我习惯在代码注释里写明「为什么用中位数而不是均值」,方便以后复盘。
- 不要过度填充。 如果某列缺失超过 70%,直接删掉这列可能比强行填充更明智。
好了,缺失值处理的上半部分就到这里。记住,数据清洗没有标准答案,只有最适合你当前问题的方案。多动手试试,慢慢就有感觉了。