3. 原始数据探查与质量评估:使用 Spark DataFrame API 进行空值率、唯一值、分布统计

在风控特征工程中,数据质量直接决定模型的上限。本章将使用 Spark DataFrame API 对原始数据进行系统性探查,包括空值率、唯一值计数以及数值/类别分布统计,为后续的特征分层与样本清洗提供量化依据。

3.1 数据探查的核心维度

风控原始数据通常包含用户基础信息、交易流水、设备指纹、外部征信等异构数据源。探查阶段需要覆盖以下三个核心维度:

维度 评估指标 风控意义
空值率 每列缺失值占比 判断字段可用性;高缺失率字段(>80%)通常直接剔除
唯一值 Distinct Count / 总行数 识别主键、低基数类别特征、高基数噪声特征
分布统计 数值型:均值/分位数/偏度;类别型:频次/占比 发现异常分布、长尾类别、数据倾斜
实战经验:在风控场景中,空值率并非越低越好。例如“逾期天数”字段在正常用户中天然为空,此时空值代表“无逾期”,属于有效业务语义,不应简单填充或删除。

3.2 空值率统计与字段分级

使用 Spark DataFrame API 进行空值率计算,核心思路是对每列调用 isNull 或 isNaN 聚合。以下代码展示了批量计算所有字段空值率并分级标注:

import org.apache.spark.sql.functions._

// 计算每列空值数量与空值率
val nullCounts = df.select(
  df.columns.map(c => sum(when(col(c).isNull, 1).otherwise(0)).alias(c)): _*
)

val totalRows = df.count()
val nullRateDF = nullCounts.toDF()
  .select(
    explode(array(df.columns.map(c => 
      struct(lit(c).alias("column"), (col(c) / totalRows).alias("null_rate"))
    ): _*)).alias("stats")
  )
  .select("stats.column", "stats.null_rate")
  .withColumn("grade", 
    when(col("null_rate") >= 0.8, "D-高缺失")
    .when(col("null_rate") >= 0.5, "C-中高缺失")
    .when(col("null_rate") >= 0.2, "B-中低缺失")
    .otherwise("A-低缺失")
  )

nullRateDF.show(10, false)

根据空值率分级结果,风控特征分层策略如下:

  • D级(≥80%):直接剔除,或保留为“是否缺失”的哑变量。
  • C级(50%~80%):结合业务判断,若缺失机制为随机,可做模型填充;若为非随机,保留缺失指示器。
  • B级(20%~50%):使用均值/中位数/众数填充,或使用树模型自动处理。
  • A级(<20%):低缺失率,直接填充或删除少量缺失行。
空值率分级与处理策略流程图 原始数据空值率计算 D级 ≥80% 直接剔除 C级 50%~80% 保留缺失指示器 B级 20%~50% 均值/中位数填充 A级 <20% 直接填充或删除 输出:字段空值分级表 + 处理策略映射 注意:空值率分级需结合业务语义,如“逾期天数”空值代表正常用户

3.3 唯一值统计与特征类型识别

唯一值统计用于区分三类特征:

  • 高基数特征(如用户ID、设备指纹):唯一值接近总行数,通常作为主键或关联键,不直接入模。
  • 中基数特征(如城市、渠道来源):唯一值在几十到几百之间,可做独热编码或目标编码。
  • 低基数特征(如性别、是否逾期):唯一值较少,通常为类别型,可直接入模或做哑变量。
// 批量计算每列唯一值数量与唯一值率
val distinctCounts = df.select(
  df.columns.map(c => countDistinct(col(c)).alias(c)): _*
)

val distinctRateDF = distinctCounts.toDF()
  .select(
    explode(array(df.columns.map(c => 
      struct(lit(c).alias("column"), (col(c) / totalRows).alias("distinct_rate"))
    ): _*)).alias("stats")
  )
  .select("stats.column", "stats.distinct_rate")
  .withColumn("feature_type",
    when(col("distinct_rate") >= 0.9, "高基数-主键/关联键")
    .when(col("distinct_rate") >= 0.05, "中基数-类别特征")
    .otherwise("低基数-类别/二值特征")
  )

distinctRateDF.show(10, false)
风控特征分层要点:高基数特征(如设备ID)虽然唯一值率高,但若做频次编码或聚合统计(如该设备过去7天关联的账户数),可转化为强风控特征。不要直接剔除,而是设计聚合衍生逻辑。

3.4 分布统计与异常检测

分布统计分为数值型和类别型两种处理方式:

3.4.1 数值型字段分布

使用 describe() 或自定义分位数计算,重点关注:

  • 均值与中位数偏差:偏差大说明数据偏态严重,需做对数变换或分箱。
  • 标准差与极值:超过3倍标准差的值可视为异常,需结合风控业务判断是否截断。
  • 零值占比:风控中“零值”可能代表无交易、无额度,需单独处理。
// 自定义分位数统计
val quantileDF = df.select(
  df.columns.filter(c => df.schema(c).dataType.typeName == "double").map(c => 
    struct(
      lit(c).alias("column"),
      percentile_approx(col(c), lit(0.25)).alias("p25"),
      percentile_approx(col(c), lit(0.50)).alias("p50"),
      percentile_approx(col(c), lit(0.75)).alias("p75"),
      percentile_approx(col(c), lit(0.99)).alias("p99")
    )
  ): _*
).select(explode(array(df.columns.filter(c => 
  df.schema(c).dataType.typeName == "double"
).map(c => 
  struct(lit(c).alias("column"), 
    percentile_approx(col(c), lit(0.25)).alias("p25"),
    percentile_approx(col(c), lit(0.50)).alias("p50"),
    percentile_approx(col(c), lit(0.75)).alias("p75"),
    percentile_approx(col(c), lit(0.99)).alias("p99"))
): _*)).alias("stats"))
.select("stats.*")

quantileDF.show(10, false)

3.4.2 类别型字段分布

使用 groupBy 统计频次与占比,识别长尾类别:

// 类别字段频次统计,标注长尾类别
val catCol = "city"
val freqDF = df.groupBy(catCol)
  .agg(count("*").alias("cnt"))
  .withColumn("ratio", col("cnt") / totalRows)
  .orderBy(col("cnt").desc)

// 将占比低于0.1%的类别归为"其他"
val threshold = 0.001
val cleanDF = freqDF.withColumn("category_group",
  when(col("ratio") < threshold, "其他").otherwise(col(catCol))
)

cleanDF.show(10, false)
避坑提示:类别型字段中若存在“未知”或“空字符串”类别,需先统一映射为 null 再进行统计,否则会扭曲分布。建议在探查前执行 df.na.replace() 或 when(col(c) === "", lit(null)) 清洗。

3.5 质量评估报告输出

将上述三个维度的统计结果合并为一份质量评估报告,便于后续特征分层决策:

// 合并空值率、唯一值率、分布统计为统一报告
val qualityReport = nullRateDF
  .join(distinctRateDF, Seq("column"), "inner")
  .join(quantileDF, Seq("column"), "left")
  .select(
    col("column"),
    col("null_rate"),
    col("grade").alias("null_grade"),
    col("distinct_rate"),
    col("feature_type"),
    col("p25"),
    col("p50"),
    col("p75"),
    col("p99")
  )
  .orderBy(col("null_rate").desc)

qualityReport.coalesce(1).write
  .mode("overwrite")
  .option("header", "true")
  .csv("/tmp/quality_report")
数据质量评估报告结构 空值率统计 每列缺失占比 A/B/C/D 分级 唯一值统计 Distinct Count 高/中/低基数分类 分布统计 数值:分位数/偏度 类别:频次/长尾 质量评估报告 字段级:空值率 | 唯一值率 | 分布特征 输出格式:CSV/Parquet,供特征分层与清洗决策使用

3.6 本章小结

通过 Spark DataFrame API 对原始数据进行空值率、唯一值、分布统计的三维探查,可以快速形成字段级质量评估报告。该报告是后续特征分层(强特征/弱特征/噪声特征)和样本清洗(缺失处理/异常截断/类别聚合)的核心输入。在实际风控项目中,建议将探查脚本封装为可复用的 DataQualityReport 工具类,每次接入新数据源时自动生成质量看板。