数据采集与清洗:实验数据来源、自动化采集脚本、缺失值处理与异常值检测
做材料科学AI项目,最头疼的往往不是模型选型,而是数据。我见过太多团队,算法调得飞起,结果数据一上来,模型直接崩了。说白了,数据质量决定了AI项目的天花板。这一章,我们就聊聊怎么把实验数据从源头管好,把脏数据洗干净。
实验数据来源:你从哪里搞数据?
材料科学的数据来源,我归纳下来主要有三类。每一类都有它的脾气,你得摸透了才行。
- 公开数据库:比如Materials Project、ICSD、COD这些。好处是量大管饱,坏处是数据质量参差不齐。我在项目中用过Materials Project的数据,发现有些结构的能量值明显不合理,后来一查,是计算参数没统一。所以,公开数据一定要做二次校验。
- 实验室自有数据:这是最宝贵的资产。包括XRD、SEM、DSC这些仪器输出的原始文件,还有实验记录本上的手工记录。我建议,所有仪器数据尽量直接导出为结构化格式(比如CSV),别用手工转录,容易出错。
- 文献数据:从论文里扒数据,是个体力活。以前我团队有个实习生,手动从PDF里抄了200组数据,结果抄错了30%。后来我们改用半自动脚本,先OCR识别,再人工复核,效率高了不少。
核心原则:数据来源要可追溯。每一条数据,你都得知道它从哪台仪器、哪个实验条件、哪个操作员手里出来的。否则,模型出了问题你都不知道该怪谁。
自动化采集脚本:让机器替你干活
手动采集数据?那是上个世纪的做法。现在,我习惯写自动化脚本,把重复劳动交给机器。你想想看,一个实验跑8小时,你难道要盯着屏幕等数据?不现实。
下面是我常用的一个Python脚本模板,专门用来从材料数据库API拉取数据:
import requests
import pandas as pd
import time
def fetch_materials_data(api_key, material_ids):
"""
从Materials Project API批量获取材料数据
"""
base_url = "https://api.materialsproject.org/v1/materials/"
headers = {"X-API-KEY": api_key}
all_data = []
for mid in material_ids:
try:
response = requests.get(f"{base_url}{mid}", headers=headers)
if response.status_code == 200:
data = response.json()
all_data.append({
"material_id": mid,
"formula": data.get("formula", ""),
"band_gap": data.get("band_gap", None),
"energy_per_atom": data.get("energy_per_atom", None),
"crystal_system": data.get("crystal_system", "")
})
else:
print(f"警告:材料 {mid} 获取失败,状态码 {response.status_code}")
# 礼貌性延时,别把人家服务器打崩了
time.sleep(0.5)
except Exception as e:
print(f"错误:材料 {mid} 请求异常 - {str(e)}")
return pd.DataFrame(all_data)
# 使用示例
api_key = "your_api_key_here"
material_list = ["mp-1234", "mp-5678", "mp-9012"]
df = fetch_materials_data(api_key, material_list)
df.to_csv("materials_data_raw.csv", index=False)
print(f"成功采集 {len(df)} 条数据")
我的经验:写采集脚本时,一定要加异常处理和重试机制。网络波动、API限流、数据格式变化,这些坑我都踩过。曾经有一次,脚本跑了3个小时,结果因为一个字段名变了,所有数据都解析错了。从那以后,我每次跑完脚本都会先检查数据完整性。
缺失值处理:别让空值毁了你的模型
实验数据里,缺失值太常见了。仪器没测到、记录员漏填、数据格式不兼容,原因五花八门。但模型可不管这些,你给它一个NaN,它就给你报错。
处理缺失值,我一般分三步走:
- 先诊断:看看缺失值占比多少。如果某个特征缺失超过70%,我建议直接扔掉。如果只有5%以下,可以考虑填充。
- 再分类:数值型特征和类别型特征,处理方法不一样。
- 后填充:选一个合适的填充策略。
下面是我常用的缺失值处理代码:
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
def handle_missing_values(df, strategy="auto"):
"""
智能处理缺失值
strategy: "auto" - 自动选择策略, "mean" - 均值填充, "knn" - KNN填充
"""
df_clean = df.copy()
# 先统计缺失率
missing_ratio = df_clean.isnull().mean()
print("各特征缺失率:")
print(missing_ratio[missing_ratio > 0])
# 处理高缺失率特征
high_missing = missing_ratio[missing_ratio > 0.7].index
if len(high_missing) > 0:
print(f"警告:特征 {list(high_missing)} 缺失率超过70%,建议删除")
df_clean = df_clean.drop(columns=high_missing)
# 分离数值型和类别型特征
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
categorical_cols = df_clean.select_dtypes(include=["object"]).columns
# 数值型特征填充
if strategy == "auto":
# 如果数据量小,用均值;数据量大,用KNN
if len(df_clean) < 1000:
for col in numeric_cols:
df_clean[col].fillna(df_clean[col].mean(), inplace=True)
else:
imputer = KNNImputer(n_neighbors=5)
df_clean[numeric_cols] = imputer.fit_transform(df_clean[numeric_cols])
elif strategy == "mean":
for col in numeric_cols:
df_clean[col].fillna(df_clean[col].mean(), inplace=True)
# 类别型特征填充(用众数)
for col in categorical_cols:
df_clean[col].fillna(df_clean[col].mode()[0], inplace=True)
print(f"缺失值处理完成,剩余缺失值数量:{df_clean.isnull().sum().sum()}")
return df_clean
# 使用示例
df = pd.read_csv("materials_data_raw.csv")
df_clean = handle_missing_values(df, strategy="knn")
避坑指南:我曾经在处理一组合金成分数据时,直接用均值填充了缺失的密度值。结果模型训练出来,预测的力学性能完全不对。后来才发现,不同相区的合金密度差异很大,均值填充抹平了这种差异。所以,填充策略一定要结合材料科学背景知识,不能无脑用统计方法。
异常值检测:揪出那些捣乱的数据点
异常值,说白了就是那些明显偏离正常范围的数据点。可能是仪器故障、记录错误,也可能是材料本身出现了特殊现象。不管是哪种,你都得先把它揪出来,再决定是保留还是剔除。
我常用的异常值检测方法有三种:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Z-score法 | 数据近似正态分布 | 简单快速,容易理解 | 对非正态分布效果差 |
| IQR四分位法 | 数据分布未知或偏态 | 不受极端值影响 | 对样本量有要求 |
| 孤立森林 | 高维数据、复杂分布 | 无需假设分布 | 计算量大,可解释性差 |
下面是一个综合的异常值检测脚本:
import numpy as np
import pandas as pd
from scipy import stats
from sklearn.ensemble import IsolationForest
def detect_outliers(df, method="iqr", threshold=3.0):
"""
多方法异常值检测
method: "zscore", "iqr", "isolation_forest"
"""
numeric_df = df.select_dtypes(include=[np.number])
outlier_mask = pd.DataFrame(False, index=df.index, columns=numeric_df.columns)
if method == "zscore":
z_scores = np.abs(stats.zscore(numeric_df, nan_policy='omit'))
outlier_mask = pd.DataFrame(z_scores > threshold,
index=df.index,
columns=numeric_df.columns)
print(f"Z-score法检测完成,阈值={threshold}")
elif method == "iqr":
Q1 = numeric_df.quantile(0.25)
Q3 = numeric_df.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outlier_mask = (numeric_df < lower_bound) | (numeric_df > upper_bound)
print("IQR法检测完成,使用1.5倍IQR作为阈值")
elif method == "isolation_forest":
iso_forest = IsolationForest(contamination=0.1, random_state=42)
predictions = iso_forest.fit_predict(numeric_df.fillna(numeric_df.mean()))
outlier_mask = pd.DataFrame(predictions == -1,
index=df.index,
columns=numeric_df.columns)
print("孤立森林检测完成,异常比例设为10%")
# 统计每个样本的异常特征数
outlier_count = outlier_mask.sum(axis=1)
print(f"检测到 {outlier_count[outlier_count > 0].count()} 个样本存在异常值")
return outlier_mask, outlier_count
# 使用示例
df = pd.read_csv("materials_data_clean.csv")
outlier_mask, outlier_count = detect_outliers(df, method="iqr")
# 标记异常样本
df["is_outlier"] = outlier_count > 0
df["outlier_features"] = outlier_count
# 查看异常样本详情
print("\n异常样本详情:")
print(df[df["is_outlier"]].head())
我的建议:异常值不一定要删除。有时候,异常值恰恰是新材料发现的线索。我在做热电材料项目时,有一个样本的ZT值异常高,一开始以为是测量错误,后来重复实验发现,那是一种新的掺杂机制。所以,先标记异常值,再结合实验背景判断,才是正确的做法。
知识体系总览
下面这张图,把数据采集与清洗的核心逻辑串起来了。你可以把它当作一个检查清单,每次做数据预处理时,对照着走一遍。
嗯,数据采集与清洗这块,说白了就是地基工程。地基打不牢,上面盖的模型再漂亮也是白搭。我个人习惯,每次拿到新数据,先花30%的时间做清洗和探索性分析,绝不急着建模。你想想看,如果数据里藏着10%的错误,模型精度能高到哪去?
最后提醒一句:所有清洗步骤都要记录下来,包括你删除了哪些样本、填充了什么值、用了什么方法。这样,当模型效果不好时,你还能回溯到数据层面找原因。别问我为什么知道——我曾经因为没记录清洗步骤,白白浪费了两周时间重新排查。