4. Spark GraphX 入门:核心概念、RDD 与 PropertyGraph、创建 Graph 的三种方式
在欺诈风控场景中,实体(用户、设备、账户)与关系(交易、登录、担保)天然构成一张异构图。Spark GraphX 是 Spark 生态中专门用于图并行计算的 API,它基于 RDD 构建,提供了统一的属性图模型。本节将系统讲解 GraphX 的核心概念、RDD 与 PropertyGraph 的关系,以及创建 Graph 的三种标准方式。
4.1 GraphX 核心概念
GraphX 将图抽象为 PropertyGraph,即带属性的有向多重图。其核心由两个 RDD 构成:
| 概念 | 类型 | 说明 | 欺诈场景示例 |
|---|---|---|---|
| Vertex (顶点) | VertexRDD[VD] |
每个顶点由唯一 ID 和属性组成 | 用户 ID (1001) + 风险标签 (0/1) |
| Edge (边) | EdgeRDD[ED] |
每条边由源顶点 ID、目标顶点 ID 和属性组成 | 交易金额 (5000)、时间戳、设备指纹 |
| Triplet (三元组) | EdgeTriplet[VD, ED] |
边 + 两端顶点属性的联合视图 | 源用户风险分 + 交易金额 + 目标用户风险分 |
关键理解:GraphX 的图不是邻接矩阵,而是两个分布式 RDD 的逻辑组合。顶点和边可以独立进行转换、过滤、聚合,最后再组合成新图。这种设计使得图计算可以无缝融入 Spark 的 DAG 调度和容错机制。
4.2 RDD 与 PropertyGraph 的关系
PropertyGraph 本质上是一个 逻辑容器,它同时持有 vertices 和 edges 两个 RDD 的引用。GraphX 提供的所有图算子(如 mapVertices、aggregateMessages)最终都会转化为对这两个 RDD 的转换操作。
下图展示了 RDD 与 PropertyGraph 之间的映射关系:
重要特性:由于 PropertyGraph 只是 RDD 的封装,因此它天然继承了 RDD 的不可变性、容错性和惰性求值。每次图转换(如
graph.filter)都会生成新的 PropertyGraph,而不会修改原图。
4.3 创建 Graph 的三种方式
在实际欺诈风控项目中,数据源通常来自 Hive 表、Kafka 流或 Parquet 文件。GraphX 提供了三种标准创建方式,以适应不同场景。
方式一:从 RDD 直接构建
最通用的方式,分别构造顶点 RDD 和边 RDD,然后调用 Graph(vertices, edges) 或 Graph.fromEdges。
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD
// 1. 构造顶点 RDD: (VertexId, 属性)
val vertices: RDD[(VertexId, (String, Int))] = sc.parallelize(Seq(
(1L, ("Alice", 0)), // 0: 正常用户
(2L, ("Bob", 1)), // 1: 欺诈用户
(3L, ("Charlie", 0))
))
// 2. 构造边 RDD: Edge(源ID, 目标ID, 属性)
val edges: RDD[Edge[Double]] = sc.parallelize(Seq(
Edge(1L, 2L, 5000.0), // 转账 5000 元
Edge(2L, 3L, 200.0),
Edge(1L, 3L, 1000.0)
))
// 3. 创建属性图
val graph: Graph[(String, Int), Double] = Graph(vertices, edges)
// 4. 查看顶点数
println(s"顶点数: ${graph.vertices.count()}") // 输出: 3
println(s"边数: ${graph.edges.count()}") // 输出: 3
方式二:从边 RDD 自动推断顶点
当只有边数据,且顶点属性可以后续补充时,使用 Graph.fromEdges。顶点属性会被设为默认值(通常为 1)。
// 只有边数据,顶点自动从边的两端推断
val edgesOnly: RDD[Edge[String]] = sc.parallelize(Seq(
Edge(1L, 2L, "转账"),
Edge(2L, 3L, "登录"),
Edge(3L, 1L, "担保")
))
// 顶点属性默认为 1 (空值占位)
val graphFromEdges: Graph[Int, String] = Graph.fromEdges(edgesOnly, defaultValue = 1)
// 后续通过 mapVertices 补充真实属性
val enrichedGraph = graphFromEdges.mapVertices { (id, _) =>
// 从外部表查询用户风险分
lookupRiskScore(id)
}
注意:
Graph.fromEdges 会自动去重顶点 ID,但不会校验边中引用的顶点是否在后续操作中有效。如果边数据中存在孤立顶点(只有出边或入边),它们仍会被包含在图中。
方式三:从 CSV/Parquet 文件加载
生产环境中,顶点和边通常存储在分布式文件系统中。推荐先加载为 DataFrame,再转换为 RDD 构建图。
// 1. 从 Parquet 加载顶点表
val vertexDF = spark.read.parquet("/data/fraud/vertices")
// 假设 schema: vertex_id: Long, risk_label: Int, device_id: String
// 2. 从 Parquet 加载边表
val edgeDF = spark.read.parquet("/data/fraud/edges")
// 假设 schema: src_id: Long, dst_id: Long, amount: Double, timestamp: Long
// 3. 转换为 RDD 并构建图
val vertexRDD: RDD[(VertexId, (Int, String))] = vertexDF.rdd.map(row => (
row.getLong(0),
(row.getInt(1), row.getString(2))
))
val edgeRDD: RDD[Edge[Double]] = edgeDF.rdd.map(row => Edge(
row.getLong(0),
row.getLong(1),
row.getDouble(2)
))
val graph: Graph[(Int, String), Double] = Graph(vertexRDD, edgeRDD)
下表总结了三种创建方式的适用场景:
| 创建方式 | 适用场景 | 顶点属性 | 性能特点 |
|---|---|---|---|
Graph(vertices, edges) |
顶点和边数据都已就绪 | 显式提供 | 最灵活,可分别优化两个 RDD |
Graph.fromEdges |
只有边数据,顶点属性可后续补充 | 默认值占位 | 自动推断顶点,减少一次 RDD 构造 |
| 从文件加载 (DataFrame → RDD) | 生产环境,数据存储在 HDFS/S3 | 从文件解析 | 可充分利用 Spark SQL 优化(谓词下推、列裁剪) |
4.4 创建流程总览
下图展示了从原始数据到 PropertyGraph 的完整创建流程,以及后续图计算在欺诈评分中的位置:
实践建议:在欺诈风控场景中,推荐优先使用 方式三(文件加载),因为顶点和边通常来自不同的数据管道(如用户画像表、交易流水表)。通过 DataFrame 的 join 和过滤,可以在构建图之前完成数据清洗,减少图计算阶段的无效数据。