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