4. 特征工程异常排查:特征缺失率过高、特征方差为零、特征相关性异常、特征重要性突变检测
特征工程是模型风险链条中最隐蔽的环节。特征质量下降往往不会直接报错,而是通过“模型效果缓慢衰减”或“线上分数分布偏移”间接暴露。本章聚焦四种高频特征异常:缺失率过高、方差为零、相关性异常、重要性突变,并给出 Spark 环境下的排查方法与阈值参考。
4.1 特征缺失率过高
特征缺失率过高会导致模型在推理时无法计算,或被迫使用填充值引入偏差。在 Spark 中,通常通过聚合统计每列的非空比例来检测。
排查要点:
- 对训练集和线上实时特征分别统计缺失率,阈值建议:连续特征 > 50% 报警,离散特征 > 30% 报警。
- 关注缺失率在时间窗口内的趋势,避免因上游数据源短暂故障导致误报。
- 对缺失率突增的特征,需回溯 ETL 日志,确认是否因字段映射变更或源表 schema 演化导致。
// Spark Scala 示例:计算 DataFrame 每列缺失率
import org.apache.spark.sql.functions._
val missingRates = df.columns.map { colName =>
val totalCount = df.count()
val missingCount = df.filter(col(colName).isNull || col(colName) === "").count()
(colName, missingCount.toDouble / totalCount)
}.toSeq
missingRates.filter(_._2 > 0.5).foreach { case (name, rate) =>
println(s"ALERT: 特征 [$name] 缺失率 = ${rate * 100}%.2f%%")
}
| 特征名称 | 训练集缺失率 | 线上缺失率 | 风险等级 | 建议操作 |
|---|---|---|---|---|
| user_income | 12% | 68% | 高 | 检查上游收入字段映射是否失效 |
| device_model | 5% | 7% | 低 | 持续监控 |
| credit_score | 22% | 55% | 中 | 确认第三方数据源接口是否限流 |
4.2 特征方差为零
方差为零的特征在模型训练中不提供任何区分度,且可能导致树模型分裂点无效或线性模型系数不稳定。在 Spark ML 中,可通过 Summarizer 或自定义聚合快速检测。
常见原因:上游数据被硬编码为常量、ETL 中使用了错误的聚合函数(如
first() 而非 avg())、特征工程逻辑中条件分支未覆盖所有情况导致默认值填充。
// 使用 Spark ML Summarizer 检测零方差特征
import org.apache.spark.ml.stat.Summarizer
import org.apache.spark.sql.functions._
val featuresCol = "features" // 假设已通过 VectorAssembler 组装
val summary = df.select(Summarizer.metrics("variance").summary(col(featuresCol)).alias("stats"))
.select("stats.variance")
.collect()(0).getAs[org.apache.spark.ml.linalg.Vector](0)
val zeroVarIndices = summary.toArray.zipWithIndex
.filter(_._1 == 0.0)
.map(_._2)
println(s"零方差特征索引: ${zeroVarIndices.mkString(", ")}")
对于零方差特征,建议直接剔除或结合业务逻辑重新设计特征。若特征在训练集有方差但在线上变为零,需立即阻断模型上线。
4.3 特征相关性异常
特征间相关性过高会导致多重共线性,影响线性模型的系数解释性;相关性在训练集与测试集之间发生突变,则暗示数据分布漂移或特征构造逻辑不一致。
排查策略:计算训练集与线上最新批次数据的特征相关性矩阵,重点关注相关性变化超过 0.3 的特征对。
// 计算特征对相关性变化
val trainCorr = computeCorrelationMatrix(trainDF) // 自定义函数
val onlineCorr = computeCorrelationMatrix(onlineDF)
val driftPairs = for {
i <- trainCorr.indices
j <- (i + 1) until trainCorr(i).length
diff = math.abs(trainCorr(i)(j) - onlineCorr(i)(j))
if diff > 0.3
} yield (s"feat_$i", s"feat_$j", trainCorr(i)(j), onlineCorr(i)(j), diff)
driftPairs.foreach(println)
| 特征对 | 训练集相关系数 | 线上相关系数 | 变化幅度 | 可能原因 |
|---|---|---|---|---|
| age, income | 0.45 | 0.12 | 0.33 | 线上客群年龄分布变化 |
| click_cnt, view_cnt | 0.82 | 0.85 | 0.03 | 正常 |
| loan_amount, debt_ratio | 0.38 | 0.71 | 0.33 | 特征计算逻辑可能被修改 |
4.4 特征重要性突变检测
特征重要性突变是模型风险最直接的信号之一。当某个特征在模型中的贡献度(如 SHAP 值、Gain、Cover)在短时间内发生剧烈变化,往往意味着特征与目标变量的关系发生了结构性改变。
检测方法:
- 基于树模型的特征重要性:对比相邻两个训练周期的特征 Gain 排名,关注排名下降超过 10 位或重要性绝对值下降超过 50% 的特征。
- 基于 SHAP 的全局重要性:计算线上最新批次数据的平均 SHAP 绝对值,与基线批次对比,变化超过 2 倍标准差视为异常。
- 分位数稳定性:对每个特征按分箱计算 SHAP 均值,若某个分箱的 SHAP 均值符号反转(正变负或负变正),立即告警。
# PySpark 示例:计算特征 Gain 变化
from pyspark.ml.classification import RandomForestClassificationModel
model = RandomForestClassificationModel.load("path/to/model")
old_importance = model.featureImportances.toArray()
# 假设新模型已加载
new_model = RandomForestClassificationModel.load("path/to/new_model")
new_importance = new_model.featureImportances.toArray()
for i, (old_val, new_val) in enumerate(zip(old_importance, new_importance)):
change = (new_val - old_val) / (old_val + 1e-8)
if abs(change) > 0.5:
print(f"特征 {i}: 重要性从 {old_val:.4f} 变为 {new_val:.4f}, 变化率 {change:.2%}")
| 特征索引 | 基线重要性 | 当前重要性 | 变化率 | 风险判定 |
|---|---|---|---|---|
| feat_12 | 0.085 | 0.012 | -85.9% | 高 — 特征失效 |
| feat_7 | 0.032 | 0.098 | +206.3% | 高 — 可能引入泄漏 |
| feat_23 | 0.045 | 0.041 | -8.9% | 低 — 正常波动 |
4.5 综合排查清单
将上述四种异常检测整合到统一的 Spark 任务中,每日或每批次自动产出特征质量报告。以下为推荐的最小监控清单:
| 检测项 | Spark 实现方式 | 推荐阈值 | 告警级别 |
|---|---|---|---|
| 特征缺失率 | DataFrame 聚合 + 阈值过滤 | 连续 > 50%,离散 > 30% | P0/P1 |
| 特征方差为零 | Summarizer.variance | 方差 == 0 | P0 |
| 相关性突变 | 自定义相关矩阵 + 差值计算 | 变化 > 0.3 | P1 |
| 重要性突变 | 模型 featureImportances 对比 | 变化率 > 50% 或排名下降 > 10 | P0 |
最佳实践:将特征质量检测结果写入时序数据库(如 InfluxDB 或 OpenTSDB),配合 Grafana 构建可视化看板。当多个特征同时出现异常时,优先排查上游数据源变更,其次检查特征工程代码版本。