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 特征计算逻辑可能被修改
特征相关性异常排查流程 计算训练集相关矩阵 计算线上批次相关矩阵 计算差值矩阵 差值 > 0.3? 标记为正常 触发告警并回溯

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% 低 — 正常波动
特征重要性突变检测流程 加载基线模型重要性 加载当前模型重要性 计算每个特征变化率 变化率 > 50%? 标记为正常波动 触发告警并深入分析 检查:数据漂移 / 特征泄漏 / 逻辑变更

4.5 综合排查清单

将上述四种异常检测整合到统一的 Spark 任务中,每日或每批次自动产出特征质量报告。以下为推荐的最小监控清单:

检测项 Spark 实现方式 推荐阈值 告警级别
特征缺失率 DataFrame 聚合 + 阈值过滤 连续 > 50%,离散 > 30% P0/P1
特征方差为零 Summarizer.variance 方差 == 0 P0
相关性突变 自定义相关矩阵 + 差值计算 变化 > 0.3 P1
重要性突变 模型 featureImportances 对比 变化率 > 50% 或排名下降 > 10 P0
最佳实践:将特征质量检测结果写入时序数据库(如 InfluxDB 或 OpenTSDB),配合 Grafana 构建可视化看板。当多个特征同时出现异常时,优先排查上游数据源变更,其次检查特征工程代码版本。