3. 数据采集与清洗
在半导体制造中,制造执行系统(MES)是数据中枢,实时记录每一片晶圆在各工艺站点的量测数据。本章将系统讲解如何从MES抽取SPC数据,并完成缺失值填充与异常值检测,为后续良率建模奠定干净、可靠的数据基础。
核心目标:将原始MES日志转化为结构化、无异常、无缺失的“建模就绪”数据集。数据清洗通常占整个建模项目60%~80%的工作量,其质量直接决定模型上限。
3.1 MES系统数据接口与SPC数据抽取
MES系统通常提供REST API、数据库直连(JDBC/ODBC)或消息队列(Kafka/MQ)三种数据获取方式。对于批量建模场景,推荐使用数据库直连+定时批处理架构。
实践建议:优先从MES的“量测历史表”(如
measurement_history)抽取数据,该表包含站点ID、设备ID、量测参数、量测值、时间戳、批次号等关键字段,避免直接解析设备日志文件。
3.2 数据字段定义与宽表构建
从MES抽取的原始数据通常为长表格式(每行一个量测值),需要透视(Pivot)为宽表,使每个参数成为独立列。标准SPC宽表字段如下:
| 字段名 | 类型 | 说明 | 示例 |
|---|---|---|---|
lot_id |
string | 批次号 | LOT20241001A |
wafer_id |
string | 晶圆ID | W01 |
site_id |
string | 工艺站点 | PHOTO_L1 |
equip_id |
string | 设备编号 | ETCH_03 |
measure_time |
datetime | 量测时间戳 | 2024-10-01 08:15:00 |
param_CD |
float | 关键尺寸 (nm) | 45.2 |
param_THK |
float | 膜厚 (Å) | 1250.8 |
param_OVL |
float | 套刻精度 (nm) | 3.1 |
yield |
float | 良率 (%) | 97.5 |
常见陷阱:不同站点可能使用不同单位(如nm vs μm),必须在ETL层统一;时间戳需转换为统一时区(建议UTC),避免因跨天导致时间序列断裂。
3.3 缺失值处理策略
半导体量测数据缺失原因包括:设备未量测、数据传输出错、站点跳过量测。缺失率超过50%的参数应直接剔除;低于5%的缺失可采用填充策略。
3.3.1 缺失值处理方法对比
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 前向填充 (ffill) | 时间序列连续缺失 | 保持趋势连续性 | 可能引入滞后偏差 |
| 后向填充 (bfill) | 时间序列开头缺失 | 利用后续信息 | 不适用于实时场景 |
| 均值/中位数填充 | 随机缺失 | 简单快速 | 降低方差,影响分布 |
| 线性插值 | 连续数值型参数 | 平滑过渡 | 对突变不敏感 |
| KNN填充 | 多维特征相关 | 利用特征间关系 | 计算量大 |
import pandas as pd
import numpy as np
# 加载SPC宽表
df = pd.read_parquet('spc_wide.parquet')
# 1. 统计缺失率
missing_rate = df.isnull().mean()
print("各列缺失率:\n", missing_rate[missing_rate > 0])
# 2. 剔除缺失率 > 50% 的列
cols_to_drop = missing_rate[missing_rate > 0.5].index.tolist()
df.drop(columns=cols_to_drop, inplace=True)
print(f"剔除高缺失列: {cols_to_drop}")
# 3. 按批次分组,使用前向填充+线性插值
df.sort_values(['lot_id', 'measure_time'], inplace=True)
df['param_CD'] = df.groupby('lot_id')['param_CD'].transform(
lambda x: x.ffill().interpolate(method='linear', limit_direction='both')
)
df['param_THK'] = df.groupby('lot_id')['param_THK'].transform(
lambda x: x.fillna(x.median())
)
print("缺失值处理完成,剩余缺失数:", df.isnull().sum().sum())
最佳实践:对于膜厚(THK)等随时间缓慢变化的参数,优先使用线性插值;对于关键尺寸(CD)等受设备状态影响较大的参数,使用同批次中位数填充更稳健。
3.4 异常值检测:3σ原则
3σ原则基于正态分布假设:数据落在均值±3σ区间外的概率仅为0.27%,可视为异常。适用于膜厚、关键尺寸等近似正态分布的参数。
def detect_3sigma(df, column, k=3):
"""
3σ异常值检测
Parameters:
df: DataFrame
column: 列名
k: 标准差倍数,默认3
Returns:
df: 带异常标记的DataFrame
"""
mean = df[column].mean()
std = df[column].std()
lower = mean - k * std
upper = mean + k * std
df[f'{column}_outlier_3sigma'] = ~df[column].between(lower, upper)
print(f"{column}: 均值={mean:.2f}, 标准差={std:.2f}")
print(f"下限={lower:.2f}, 上限={upper:.2f}")
print(f"异常数量: {df[f'{column}_outlier_3sigma'].sum()}")
return df
# 应用3σ检测
df = detect_3sigma(df, 'param_CD', k=3)
df = detect_3sigma(df, 'param_THK', k=3)
3.5 异常值检测:IQR方法
IQR(四分位距)方法不依赖正态分布假设,对偏态分布更鲁棒。定义:IQR = Q3 - Q1,异常阈值为 [Q1 - 1.5×IQR, Q3 + 1.5×IQR]。
| 方法 | 分布假设 | 阈值计算 | 对极端值敏感度 | 推荐场景 |
|---|---|---|---|---|
| 3σ原则 | 正态分布 | μ ± 3σ | 高(受极端值影响) | 膜厚、CD等对称分布 |
| IQR方法 | 无严格假设 | Q1 - 1.5×IQR, Q3 + 1.5×IQR | 低(基于分位数) | 套刻精度、缺陷密度等偏态分布 |
def detect_iqr(df, column, multiplier=1.5):
"""
IQR异常值检测
Parameters:
df: DataFrame
column: 列名
multiplier: IQR倍数,默认1.5
Returns:
df: 带异常标记的DataFrame
"""
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - multiplier * IQR
upper = Q3 + multiplier * IQR
df[f'{column}_outlier_iqr'] = ~df[column].between(lower, upper)
print(f"{column}: Q1={Q1:.2f}, Q3={Q3:.2f}, IQR={IQR:.2f}")
print(f"下限={lower:.2f}, 上限={upper:.2f}")
print(f"异常数量: {df[f'{column}_outlier_iqr'].sum()}")
return df
# 应用IQR检测
df = detect_iqr(df, 'param_OVL', multiplier=1.5)
# 合并两种方法的异常标记(取并集)
df['is_anomaly'] = (
df['param_CD_outlier_3sigma'] |
df['param_THK_outlier_3sigma'] |
df['param_OVL_outlier_iqr']
)
print(f"总异常样本数: {df['is_anomaly'].sum()}")
工程经验:建议同时使用3σ和IQR两种方法,取并集作为异常标记。对于关键参数(如CD),异常值不应直接删除,而应标记后由工艺工程师复核,确认是否为设备偏移或量测系统误差。
3.6 异常值处理策略
检测到异常值后,根据业务场景选择以下处理方式:
- 删除异常样本:适用于明显量测错误(如CD为负值),且异常比例<1%。
- 替换为边界值:将异常值截断至阈值上限或下限,保留样本量。
- 标记为缺失后填充:先标记异常为NaN,再使用3.3节的缺失值填充方法处理。
- 保留并标记:在建模时增加“是否异常”特征列,让模型自行学习异常模式。
def handle_outliers(df, column, method='clip'):
"""
异常值处理
method: 'drop' - 删除, 'clip' - 截断, 'fill_nan' - 标记为缺失
"""
outlier_col = f'{column}_outlier_iqr'
if outlier_col not in df.columns:
df = detect_iqr(df, column)
if method == 'drop':
df_clean = df[~df[outlier_col]].copy()
print(f"删除异常: {df[outlier_col].sum()} 行")
elif method == 'clip':
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
df[column] = df[column].clip(lower, upper)
df_clean = df.copy()
print(f"截断异常至 [{lower:.2f}, {upper:.2f}]")
elif method == 'fill_nan':
df.loc[df[outlier_col], column] = np.nan
df_clean = df.copy()
print(f"标记 {df[outlier_col].sum()} 个异常为缺失")
return df_clean
# 对CD使用截断,对OVL使用标记为缺失
df = handle_outliers(df, 'param_CD', method='clip')
df = handle_outliers(df, 'param_OVL', method='fill_nan')
# 最终检查
print("\n清洗后数据概况:")
print(f"总样本数: {len(df)}")
print(f"缺失值总数: {df.isnull().sum().sum()}")
print(f"异常标记总数: {df['is_anomaly'].sum()}")
数据质量报告模板:每次清洗后应输出包含以下指标的JSON报告:
total_samples、missing_rate_before、missing_rate_after、outlier_rate、columns_dropped、method_used。该报告可作为模型训练日志的一部分,便于追溯。