3. 数据采集与清洗:从MES系统获取SPC数据、处理缺失值、异常值检测与处理

在半导体制造中,制造执行系统(MES)是数据中枢,实时记录每一片晶圆在各工艺站点的量测数据。本章将系统讲解如何从MES抽取SPC数据,并完成缺失值填充与异常值检测,为后续良率建模奠定干净、可靠的数据基础。

核心目标:将原始MES日志转化为结构化、无异常、无缺失的“建模就绪”数据集。数据清洗通常占整个建模项目60%~80%的工作量,其质量直接决定模型上限。

3.1 MES系统数据接口与SPC数据抽取

MES系统通常提供REST API、数据库直连(JDBC/ODBC)或消息队列(Kafka/MQ)三种数据获取方式。对于批量建模场景,推荐使用数据库直连+定时批处理架构。

实践建议:优先从MES的“量测历史表”(如 measurement_history)抽取数据,该表包含站点ID、设备ID、量测参数、量测值、时间戳、批次号等关键字段,避免直接解析设备日志文件。
MES → SPC 数据抽取架构 MES 数据库 ETL 抽取层 SPC 宽表 缺失值处理 异常值检测 建模就绪数据集 说明:ETL层负责字段映射、时间对齐、单位统一;SPC宽表每行对应一次量测事件

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%,可视为异常。适用于膜厚、关键尺寸等近似正态分布的参数。

3σ 异常值检测原理 μ μ-3σ μ+3σ 异常区 异常区 正常区 (99.73%) 说明:超出 μ±3σ 范围的数据点被标记为异常,需进一步确认是否为设备故障或量测错误
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 异常值处理策略

检测到异常值后,根据业务场景选择以下处理方式:

  1. 删除异常样本:适用于明显量测错误(如CD为负值),且异常比例<1%。
  2. 替换为边界值:将异常值截断至阈值上限或下限,保留样本量。
  3. 标记为缺失后填充:先标记异常为NaN,再使用3.3节的缺失值填充方法处理。
  4. 保留并标记:在建模时增加“是否异常”特征列,让模型自行学习异常模式。
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()}")
数据清洗完整流程 MES原始数据 字段映射与透视 缺失值处理 异常值检测 异常值处理 数据质量报告 建模就绪数据 关键检查点 ① 字段映射后检查数据类型与单位一致性 ② 缺失值处理后验证分布是否发生显著偏移 ③ 异常值处理后生成数据质量报告,记录清洗比例
数据质量报告模板:每次清洗后应输出包含以下指标的JSON报告:total_samples、missing_rate_before、missing_rate_after、outlier_rate、columns_dropped、method_used。该报告可作为模型训练日志的一部分,便于追溯。