3、数据质量异常排查:缺失值检测、异常值检测、数据分布偏移检测、数据倾斜检测与处理方法
数据质量是模型稳定性的基石。在 Spark 生产环境中,数据质量异常通常表现为四种形态:缺失值、异常值、分布偏移和数据倾斜。本章将逐一拆解每种异常的检测原理、Spark 实现方法及处理策略。
3.1 缺失值检测与处理
缺失值检测的核心是识别 DataFrame 中每个字段的 null 或 NaN 比例。Spark 提供 DataFrameNaFunctions 接口进行高效统计。
3.1.1 缺失率统计
// Scala 示例:计算每列缺失率
val df = spark.read.parquet("/data/feature_store")
val totalCount = df.count()
val missingStats = df.columns.map { colName =>
val missingCount = df.filter(df(colName).isNull || df(colName).isNaN).count()
(colName, missingCount, missingCount.toDouble / totalCount)
}
missingStats.foreach { case (col, cnt, ratio) =>
println(f"$col%s: 缺失 $cnt%d 条, 占比 ${ratio * 100}%.2f%%")
}
3.1.2 缺失值处理策略
| 策略 | 适用场景 | Spark 实现 |
|---|---|---|
| 删除行 | 缺失率 < 1%,且样本充足 | df.na.drop() |
| 均值/中位数填充 | 连续型特征,分布较对称 | df.na.fill(meanVal) |
| 众数填充 | 离散型特征 | df.na.fill(modeVal) |
| 前向/后向填充 | 时序特征 | df.withColumn("col", last("col", true).over(...)) |
| 模型预测填充 | 重要特征,缺失率 5%~20% | 使用 Spark ML 回归/分类模型 |
实践建议:在风控模型中,缺失值本身可能携带信息。建议将“是否缺失”作为独立二值特征加入模型,再配合填充值,避免信息损失。
3.2 异常值检测
异常值检测的目标是识别与正常分布偏离过大的样本点。Spark 环境下常用以下三种方法。
3.2.1 基于统计的检测:IQR 与 Z-Score
// IQR 方法检测异常值
val quantiles = df.stat.approxQuantile("amount", Array(0.25, 0.75), 0.01)
val Q1 = quantiles(0)
val Q3 = quantiles(1)
val IQR = Q3 - Q1
val lowerBound = Q1 - 1.5 * IQR
val upperBound = Q3 + 1.5 * IQR
val outliers = df.filter($"amount" < lowerBound || $"amount" > upperBound)
println(s"IQR 异常值数量: ${outliers.count()}")
3.2.2 基于模型的方法:孤立森林
Spark ML 3.0+ 支持 IsolationForest,适合高维特征异常检测。
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.iforest.{IForest, IForestModel}
val assembler = new VectorAssembler()
.setInputCols(Array("feat1", "feat2", "feat3"))
.setOutputCol("features")
val featureDF = assembler.transform(df)
val iforest = new IForest()
.setNumTrees(100)
.setMaxSamples(256)
.setContamination(0.05)
.setFeaturesCol("features")
.setPredictionCol("anomaly")
val model: IForestModel = iforest.fit(featureDF)
val result = model.transform(featureDF)
result.filter($"anomaly" === 1).show()
3.2.3 异常值处理方法对比
| 方法 | 优点 | 缺点 | Spark 适用性 |
|---|---|---|---|
| 截尾(Winsorize) | 保留样本量,简单快速 | 改变分布形态 | 高 |
| 删除 | 彻底消除干扰 | 损失样本,可能引入偏差 | 高 |
| 对数/Box-Cox 变换 | 保留异常趋势信息 | 需逆变换解释 | 中 |
| 单独建模 | 对极端值单独处理 | 增加复杂度 | 低 |
注意:风控场景中,异常值往往是欺诈或高风险样本。删除前务必评估业务含义,建议先标记后分析,而非直接丢弃。
3.3 数据分布偏移检测
分布偏移指训练集与上线后实时数据的统计分布发生显著变化。Spark 流式作业中需持续监控。
3.3.1 PSI(群体稳定性指标)
PSI 是风控领域最常用的分布偏移指标,衡量两个分布在各分箱上的差异。
def calculatePSI(expected: DataFrame, actual: DataFrame, col: String, bins: Int = 10): Double = {
val minVal = expected.agg(min(col)).collect()(0).getDouble(0)
val maxVal = expected.agg(max(col)).collect()(0).getDouble(0)
val step = (maxVal - minVal) / bins
// 分箱统计期望分布
val expectedBins = expected.withColumn("bin",
floor(($"$col" - minVal) / step).cast("int"))
.groupBy("bin").count()
.withColumn("ratio", $"count" / sum("count").over())
// 分箱统计实际分布
val actualBins = actual.withColumn("bin",
floor(($"$col" - minVal) / step).cast("int"))
.groupBy("bin").count()
.withColumn("ratio", $"count" / sum("count").over())
// 计算 PSI
val joined = expectedBins.alias("e").join(actualBins.alias("a"), "bin")
joined.agg(sum(
(($"a.ratio" - $"e.ratio") * log($"a.ratio" / $"e.ratio"))
)).collect()(0).getDouble(0)
}
3.3.2 分布偏移阈值与响应
| PSI 值 | 偏移程度 | 建议操作 |
|---|---|---|
| < 0.1 | 无显著偏移 | 继续监控 |
| 0.1 ~ 0.25 | 轻度偏移 | 触发告警,检查特征来源 |
| > 0.25 | 严重偏移 | 暂停模型,重新训练或回滚 |
关键点:分布偏移检测应纳入 Spark Structured Streaming 的 foreachBatch 中,每批次计算 PSI,实现实时监控。
3.4 数据倾斜检测与处理
数据倾斜是 Spark 作业性能下降的首要原因。倾斜表现为某些分区数据量远大于其他分区,导致长尾任务。
3.4.1 倾斜检测方法
// 检测各分区数据量
val partitionSizes = df.rdd.mapPartitionsWithIndex { (idx, iter) =>
Iterator((idx, iter.length))
}.collect()
partitionSizes.sortBy(-_._2).take(10).foreach { case (pid, size) =>
println(f"分区 $pid%d: $size%d 条")
}
// 计算倾斜系数(最大分区 / 平均分区)
val avgSize = partitionSizes.map(_._2).sum / partitionSizes.length
val maxSize = partitionSizes.map(_._2).max
val skewRatio = maxSize.toDouble / avgSize
println(f"倾斜系数: $skewRatio%.2f (阈值 > 3 视为严重倾斜)")
3.4.2 倾斜处理策略
| 策略 | 原理 | 代码示例 |
|---|---|---|
| 加盐(Salting) | 对倾斜 key 添加随机前缀,分散到多个分区 | df.withColumn("salt", rand() * 10).withColumn("new_key", concat($"key", lit("_"), $"salt")) |
| 调整并行度 | 增大 shuffle 分区数 | df.repartition(200, $"key") |
| 广播小表 | 将小表广播避免 shuffle | df.join(broadcast(smallDF), "key") |
| 两阶段聚合 | 先局部聚合,再全局聚合 | df.withColumn("salt", rand() * 10).groupBy("salt", "key").agg(...).groupBy("key").agg(...) |
最佳实践:在 join 前先对两表做
skewJoinHint(Spark 3.4+),或使用 Adaptive Query Execution (AQE) 自动处理倾斜。开启 AQE:spark.sql.adaptive.enabled=true。
3.5 综合排查流程
在实际 Spark 模型风险排查中,数据质量异常往往同时出现。建议按以下顺序执行排查流水线:
- 第一步:运行缺失率统计,标记缺失率 > 30% 的特征为高风险。
- 第二步:对连续型特征执行 IQR 异常检测,输出异常样本清单。
- 第三步:计算每个特征的 PSI,对比训练集与最近 7 天生产数据。
- 第四步:检查 Spark UI 中 Stage 任务耗时分布,识别倾斜分区。
- 第五步:根据检测结果,依次执行填充、截尾、加盐等处理。
- 第六步:将处理后的数据写入新的特征表,并更新模型监控基线。
自动化建议:将上述流程封装为 Spark 作业,使用
spark.sql.adaptive.coalescePartitions.enabled=true 和动态资源分配,实现数据质量自愈。
3.6 总结
数据质量异常排查是 Spark 模型风险防控的第一道防线。本章覆盖了四种核心异常类型的检测与处理方法:
- 缺失值:使用
na.drop/na.fill快速处理,注意保留缺失指示特征。 - 异常值:IQR 适合快速筛查,孤立森林适合高维场景。
- 分布偏移:PSI 是标准指标,需在流式作业中持续计算。
- 数据倾斜:加盐和 AQE 是最高效的解决方案。
建议将上述检测逻辑封装为 Spark 监控作业,每日定时运行并输出报告,实现数据质量的自动化巡检。