4. 数据清洗实战:缺失值处理、重复值删除、异常值识别与过滤、数据类型转换
数据清洗是良率报告自动化中最耗时但最关键的一步。原始测试数据通常包含测试机台未采集、多站点重复记录、探针卡异常导致的离群值以及字段类型不匹配等问题。本章将围绕四个核心清洗任务,结合 Python 与 Pandas 给出可直接复用的实战方案。
核心原则:清洗操作必须记录日志(删除行数、填充均值、类型变更字段),以便后续审计与回溯。切勿在原文件上直接修改,始终维护清洗副本。
4.1 缺失值处理
良率数据中缺失值常见于:测试项未执行(如 CP1 未测某参数)、机台通讯超时(Die 坐标存在但电性值为空)、手动录入遗漏。处理策略取决于缺失率与业务含义。
| 缺失率范围 | 推荐策略 | 适用场景 |
|---|---|---|
| < 5% | 删除含缺失的行(dropna) | 随机缺失,样本量充足 |
| 5% – 20% | 均值/中位数填充 | 连续型电性参数(如 Iddq) |
| > 20% | 标记为独立类别或建模预测 | 分类字段(如 Bin Code) |
import pandas as pd
import numpy as np
# 读取原始良率数据
df = pd.read_csv("wafer_test_raw.csv")
# 1. 查看缺失概况
print(df.isnull().sum())
print(f"缺失率: {df.isnull().mean().round(4)}")
# 2. 删除缺失率 < 5% 的列中的缺失行
threshold = 0.05
cols_low_missing = df.columns[df.isnull().mean() < threshold]
df_clean = df.dropna(subset=cols_low_missing)
# 3. 对连续型参数用中位数填充
fill_cols = ['Vt_avg', 'Idsat', 'Ioff']
for col in fill_cols:
median_val = df_clean[col].median()
df_clean[col].fillna(median_val, inplace=True)
print(f"{col} 填充中位数: {median_val:.4f}")
实战提示:对于良率 Bin 映射表,缺失的 Bin Code 应填充为 "Unknown" 而非数值 0,避免与良品 Bin 1 混淆。
4.2 重复值删除
重复数据主要来源于:多站点合并时重复读取、同一 Die 被多次测试(Rerun)、数据库 ETL 重复推送。删除前必须明确“唯一键”定义。
- 完全重复:所有列值相同,直接删除。
- 部分重复:基于关键列(Wafer_ID + Die_X + Die_Y + Test_Time)去重,保留最新记录。
# 查看重复行数量
print(f"完全重复行数: {df_clean.duplicated().sum()}")
# 删除完全重复
df_clean.drop_duplicates(inplace=True)
# 基于关键列去重,保留最后一条(最新测试结果)
key_cols = ['Wafer_ID', 'Die_X', 'Die_Y', 'Test_Item']
df_clean.drop_duplicates(subset=key_cols, keep='last', inplace=True)
print(f"去重后行数: {len(df_clean)}")
注意:对于良率分析,同一 Die 的多次测试应保留最后一次(Rerun 结果),而非第一次。使用
keep='last' 参数。
4.3 异常值识别与过滤
异常值在良率数据中通常对应:探针卡接触不良(开路/短路)、测试机台通道损坏(固定值)、工艺缺陷(真实但极端的电性参数)。识别方法需结合统计与业务规则。
| 方法 | 原理 | 适用参数 |
|---|---|---|
| IQR 四分位距 | Q1 - 1.5*IQR ~ Q3 + 1.5*IQR | Vt、Idsat 等对称分布参数 |
| Z-Score | |Z| > 3 视为异常 | 正态分布参数(如 Ioff) |
| 固定阈值 | Spec Limit 上下限 | 所有测试项(业务强制) |
# IQR 方法过滤
Q1 = df_clean['Vt_avg'].quantile(0.25)
Q3 = df_clean['Vt_avg'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
mask_iqr = (df_clean['Vt_avg'] >= lower) & (df_clean['Vt_avg'] <= upper)
print(f"IQR 过滤前: {len(df_clean)}, 过滤后: {mask_iqr.sum()}")
# Z-Score 方法
from scipy import stats
z_scores = np.abs(stats.zscore(df_clean['Idsat']))
mask_z = z_scores < 3
print(f"Z-Score 过滤前: {len(df_clean)}, 过滤后: {mask_z.sum()}")
# 固定 Spec Limit 过滤(业务规则优先)
spec_lower, spec_upper = 0.1, 2.0 # 示例 Iddq 规格
mask_spec = (df_clean['Iddq'] >= spec_lower) & (df_clean['Iddq'] <= spec_upper)
# 综合过滤:取交集
df_final = df_clean[mask_iqr & mask_z & mask_spec].copy()
print(f"最终有效数据: {len(df_final)}")
4.4 数据类型转换
原始 CSV 或数据库导出常导致类型错乱:数值列被读为字符串(如 Bin Code 带引号)、时间列未解析、分类列未转为 category 类型。类型转换直接影响后续聚合与绘图效率。
| 原始类型 | 目标类型 | 转换方法 | 常见问题 |
|---|---|---|---|
| object(数字字符串) | float64 / int64 | pd.to_numeric() |
含逗号或单位符号 |
| object(日期字符串) | datetime64 | pd.to_datetime() |
格式不统一(MM/DD vs DD-MM) |
| int64(分类编码) | category | astype('category') |
编码无实际顺序意义 |
# 1. 数值列转换(处理含逗号的数字)
df_final['Yield_Pct'] = pd.to_numeric(df_final['Yield_Pct'], errors='coerce')
# errors='coerce' 将无法转换的值变为 NaN,便于后续处理
# 2. 时间列统一解析
df_final['Test_Date'] = pd.to_datetime(df_final['Test_Date'], format='%Y-%m-%d')
# 若格式不统一,可先尝试 infer_datetime_format=True
# 3. 分类列优化内存
df_final['Bin_Code'] = df_final['Bin_Code'].astype('category')
df_final['Test_Item'] = df_final['Test_Item'].astype('category')
# 4. 验证转换结果
print(df_final.dtypes)
print(f"内存占用: {df_final.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
性能建议:将重复值高的文本列(如 Bin_Code、Test_Item)转为 category 类型,可减少 50%–70% 内存占用,加速 groupby 与绘图操作。
4.5 清洗流水线封装
将上述四个步骤封装为可复用的清洗函数,支持参数化配置,便于集成到自动化报表流程中。
def clean_yield_data(df, config):
"""
良率数据清洗流水线
config: dict, 包含缺失阈值、异常方法、类型映射等
"""
df_clean = df.copy()
# Step 1: 缺失值处理
for col, strategy in config['missing'].items():
if strategy == 'drop':
df_clean.dropna(subset=[col], inplace=True)
elif strategy == 'median':
df_clean[col].fillna(df_clean[col].median(), inplace=True)
elif strategy == 'unknown':
df_clean[col].fillna('Unknown', inplace=True)
# Step 2: 重复值删除
if config.get('dedup_keys'):
df_clean.drop_duplicates(subset=config['dedup_keys'], keep='last', inplace=True)
else:
df_clean.drop_duplicates(inplace=True)
# Step 3: 异常值过滤
for col, method in config['outlier'].items():
if method == 'iqr':
Q1, Q3 = df_clean[col].quantile([0.25, 0.75])
IQR = Q3 - Q1
mask = (df_clean[col] >= Q1 - 1.5*IQR) & (df_clean[col] <= Q3 + 1.5*IQR)
df_clean = df_clean[mask]
elif method == 'zscore':
from scipy import stats
df_clean = df_clean[np.abs(stats.zscore(df_clean[col])) < 3]
# Step 4: 类型转换
for col, dtype in config['dtype_map'].items():
if dtype == 'datetime':
df_clean[col] = pd.to_datetime(df_clean[col])
elif dtype == 'category':
df_clean[col] = df_clean[col].astype('category')
else:
df_clean[col] = pd.to_numeric(df_clean[col], errors='coerce')
return df_clean
# 使用示例
config = {
'missing': {'Vt_avg': 'median', 'Bin_Code': 'unknown'},
'dedup_keys': ['Wafer_ID', 'Die_X', 'Die_Y'],
'outlier': {'Idsat': 'iqr', 'Ioff': 'zscore'},
'dtype_map': {'Test_Date': 'datetime', 'Bin_Code': 'category'}
}
df_final = clean_yield_data(df, config)
print(f"清洗完成,最终行数: {len(df_final)}")
清洗日志示例:
缺失填充: Vt_avg 填充 0.4521 (中位数), Bin_Code 填充 'Unknown' (12 条)
重复删除: 基于 Wafer_ID+Die_X+Die_Y 删除 34 条重复
异常过滤: Idsq IQR 过滤 8 条, Ioff Z-Score 过滤 3 条
类型转换: Test_Date → datetime, Bin_Code → category
缺失填充: Vt_avg 填充 0.4521 (中位数), Bin_Code 填充 'Unknown' (12 条)
重复删除: 基于 Wafer_ID+Die_X+Die_Y 删除 34 条重复
异常过滤: Idsq IQR 过滤 8 条, Ioff Z-Score 过滤 3 条
类型转换: Test_Date → datetime, Bin_Code → category
通过以上四个步骤的实战清洗,良率数据从原始杂乱状态变为结构化、可分析的高质量数据集,为后续趋势图绘制与 Pareto 分析奠定坚实基础。