Spark 之 Graphx学习笔记

前言

写此博客时,我也是刚接触Spark GraphX,很多东西都一知半解,不过还好对Spark原理有一定的了解。为了,进一步学习:可能你会有很多的手段,比如官网(这个是最直接了当,也是最可靠的方法,但需要你有一定的英语能力),博客等。最近看到了一篇博客,写的非常不错,在此以做学习笔记的方式写了这篇博客。

概述

  • GraphX是 Spark中用于图(如Web-Graphs and Social Networks)和图并行计算(如 PageRank and Collaborative Filtering)的API,可以认为是GraphLab(C++)和Pregel(C++)在Spark(Scala)上的重写及优化,跟其他分布式 图计算框架相比,GraphX最大的贡献是,在Spark之上提供一站式数据解决方案,可以方便且高效地完成图计算的一整套流水作业。
  • Graphx是Spark生态中的非常重要的组件,融合了图并行以及数据并行的优势,虽然在单纯的计算机段的性能相比不如GraphLab等计算框架,但是如果从整个图处理流水线的视角(图构建,图合并,最终结果的查询)看,那么性能就非常具有竞争性了。

图计算的应用场景

“图计算”是以“图论”为基础的对现实世界的一种“图”结构的抽象表达,以及在这种数据结构上的计算模式。通常,在图计算中,基本的数据结构表达就是:G = (V,E,D),其中: V :vertex (顶点或者节点),E :edge (边),D:data (权重)。
图数据很好的表达了数据之间的相关性,现实中很多问题都可以用此表示,一下一些问题就可以以图的事项简历模型来解决问题。

  • PangeRank让链接来投票
  • 基于GraphX的社区发现算法
  • 社交网络分析
  • 基于三角形计数关系的衡量
  • 基于随机游走的用户属性传播
  • 推荐应用

Spark中图的建立及图的基本操作(Graph Builders)

图的构建
如下图是官网给我们展示的一张图:顶点的属性带有姓名和职业,边表示不同点之间的关系。
在这里插入图片描述
那么,建立一个图有哪些方法呢:

object Graph {
  def apply[VD, ED](
      vertices: RDD[(VertexId, VD)],
      edges: RDD[Edge[ED]],
      defaultVertexAttr: VD = null)
    : Graph[VD, ED]

  def fromEdges[VD, ED](
      edges: RDD[Edge[ED]],
      defaultValue: VD): Graph[VD, ED]

  def fromEdgeTuples[VD](
      rawEdges: RDD[(VertexId, VertexId)],
      defaultValue: VD,
      uniqueEdges: Option[PartitionStrategy] = None): Graph[VD, Int]
}

从上面可以了解到,我们有三种方式构建图:
1. Graph (读取文件,创建RDD,利用RDD创建图属性)

import org.apache.spark.graphx.{Edge, Graph}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object GraphX_Text {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("GraphX_Text").setMaster("local")
    val sc = new SparkContext(conf)
//    读取集群中的文件创建RDD
    val vv: RDD[String] = sc.textFile("/graphx_vertices.txt")
    val ee: RDD[String] = sc.textFile("/graphx_edges.txt")
    //    装置顶点和边RDD
        val vertices: RDD[(Long, String)] = vv.map { line =>
          val fields: Array[String] = line.split("\t")
          (fields(0).toLong, fields(1))
        }//注意第一列为VertexId,必须为Long类型,第二列为顶点属性,可以为任意类型
    val edges = ee.map { line =>
      val fields: Array[String] = line.split("\t")
      Edge(fields(0).toLong, fields(1).toLong, "property")
    }//第一,二列为起始点ID,必须为Long类型,第三列为边属性,可以为任意类型
//    构件图    自动使用apply方法创建图
    val graph: Graph[String, String] = Graph(vertices,edges,"property").persist()
  }
}
  1. Graph.fromEdges方法:这种方法由边RDD建立图,由边中出现的所有RDD自动产生顶点vertexId,顶点属性为默认值。
object GraphX_Text02{
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("GraphX_Text02").setMaster("local")
    val sc = new SparkContext(conf)
//    读取文件创建RDD
    val e: RDD[String] = sc.textFile("/graphx_edges.txt")
    val ee: RDD[Edge[String]] = e.map { x =>
      val fields: Array[String] = x.split("\t")
      Edge(fields(1).toLong, fields(3).toLong, "property")
    }
//    通过fromEdges方法创建图
    val graph: Graph[String, String] = Graph.fromEdges(ee,"property")
  }
}
  1. Graph.fromEdge Tuples方法
    Graph.fromEdgeTuples allows creating a graph from only an RDD of edge tuples, assigning the edges the value 1, and automatically creating any vertices mentioned by edges and assigning them the default value. It also supports deduplicating the edges; to deduplicate, pass Some of a PartitionStrategy as the uniqueEdges parameter (for example, uniqueEdges = Some(PartitionStrategy.RandomVertexCut)). A partition strategy is necessary to colocate identical edges on the same partition so they can be deduplicated.
  2. GraphLoader.edgeListFile构建图的基本结构,然后join属性
val graph=GraphLoader.edgeListFile(sc, "/data/graphx/followers.txt") 
//文件的格式如下:
//2 1
//4 1
//1 2            依次为第一个顶点和第二个顶点

然后,和(1)中基本结构图join在一起,就可以组合成完整的属性图。

三种视图及操作

Spark中有一下三种视图可以访问,分别通过graph.vertices, graph.edges, graph.triplets 来访问。

在这里插入图片描述
在Scala语言中,可以用case语句进行形式简单、功能强大的模式匹配

val graph: Graph[(String, Int), Int] = Graph(vertexRDD, edgeRDD)
用case匹配可以很方便访问顶点和边的属性及id
graph.vertices.map{
      case (id,(name,age))=>//利用case进行匹配
        (age,name)//可以在这里加上自己想要的任何转换
    }

graph.edges.map{
      case Edge(srcid,dstid,weight)=>//利用case进行匹配
        (dstid,weight*0.01)//可以在这里加上自己想要的任何转换
    }

也可以通过下标访问

graph.vertices.map{
      v=>(v._1,v._2._1,v._2._2)//v._1,v._2._1,v._2._2分别对应Id,name,age
}

graph.edges.map {
      e=>(e.attr,e.srcId,e.dstId)
}

graph.triplets.map{
      triplet=>(triplet.srcAttr._1,triplet.dstAttr._2,triplet.srcId,triplet.dstId)
    }

但是,mapVertices和mapEdges及mapTriplets方法同样可以实现上述一样的效果。

graph.mapVertices{
      case (id,(name,age))=>//利用case进行匹配
        (age,name)//可以在这里加上自己想要的任何转换
}

graph.mapEdges(e=>(e.attr,e.srcId,e.dstId))

graph.mapTriplets(triplet=>(triplet.srcAttr._1))

Spark Graphx中图函数大全(Summary List of Operators)

/** Summary of the functionality in the property graph */
class Graph[VD, ED] {
  // Information about the Graph 
  //图的基本信息统计
==========================================================
  val numEdges: Long
  val numVertices: Long
  val inDegrees: VertexRDD[Int]
  val outDegrees: VertexRDD[Int]
  val degrees: VertexRDD[Int]

  // Views of the graph as collections 
  // 图的三种视图
==========================================================
  val vertices: VertexRDD[VD]
  val edges: EdgeRDD[ED]
  val triplets: RDD[EdgeTriplet[VD, ED]]

  // Functions for caching graphs ==========================================================
  def persist(newLevel: StorageLevel = StorageLevel.MEMORY_ONLY): Graph[VD, ED]
  def cache(): Graph[VD, ED]
  def unpersistVertices(blocking: Boolean = true): Graph[VD, ED]
  // Change the partitioning heuristic  ==========================================================
  def partitionBy(partitionStrategy: PartitionStrategy): Graph[VD, ED]

  // Transform vertex and edge attributes 
  // 基本的转换操作
==========================================================
  def mapVertices[VD2](map: (VertexID, VD) => VD2): Graph[VD2, ED]
  def mapEdges[ED2](map: Edge[ED] => ED2): Graph[VD, ED2]
  def mapEdges[ED2](map: (PartitionID, Iterator[Edge[ED]]) => Iterator[ED2]): Graph[VD, ED2]
  def mapTriplets[ED2](map: EdgeTriplet[VD, ED] => ED2): Graph[VD, ED2]
  def mapTriplets[ED2](map: (PartitionID, Iterator[EdgeTriplet[VD, ED]]) => Iterator[ED2])
    : Graph[VD, ED2]

  // Modify the graph structure 
  //图的结构操作(仅给出四种基本的操作,子图提取是比较重要的操作)
==========================================================
  def reverse: Graph[VD, ED]
  def subgraph(
      epred: EdgeTriplet[VD,ED] => Boolean = (x => true),
      vpred: (VertexID, VD) => Boolean = ((v, d) => true))
    : Graph[VD, ED]
  def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
  def groupEdges(merge: (ED, ED) => ED): Graph[VD, ED]

  // Join RDDs with the graph 
  // 两种聚合方式,可以完成各种图的聚合操作  ==========================================================
  def joinVertices[U](table: RDD[(VertexID, U)])(mapFunc: (VertexID, VD, U) => VD): Graph[VD, ED]
  def outerJoinVertices[U, VD2](other: RDD[(VertexID, U)])
      (mapFunc: (VertexID, VD, Option[U]) => VD2)

  // Aggregate information about adjacent triplets 
  //图的邻边信息聚合,collectNeighborIds都是效率不高的操作,优先使用aggregateMessages,这也是GraphX最重要的操作之一。
==========================================================
  def collectNeighborIds(edgeDirection: EdgeDirection): VertexRDD[Array[VertexID]]
  def collectNeighbors(edgeDirection: EdgeDirection): VertexRDD[Array[(VertexID, VD)]]
  def aggregateMessages[Msg: ClassTag](
      sendMsg: EdgeContext[VD, ED, Msg] => Unit,
      mergeMsg: (Msg, Msg) => Msg,
      tripletFields: TripletFields = TripletFields.All)
    : VertexRDD[A]

  // Iterative graph-parallel computation ==========================================================
  def pregel[A](initialMsg: A, maxIterations: Int, activeDirection: EdgeDirection)(
      vprog: (VertexID, VD, A) => VD,
      sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexID,A)],
      mergeMsg: (A, A) => A)
    : Graph[VD, ED]

  // Basic graph algorithms 
  //图的算法API(目前给出了三类四个API)  ==========================================================
  def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double]
  def connectedComponents(): Graph[VertexID, ED]
  def triangleCount(): Graph[Int, ED]
  def stronglyConnectedComponents(numIter: Int): Graph[VertexID, ED]
}

结构操作(Structural Operators)

Spark 2.0 版本中,只有4中结构操作

1)reverse
2)subgraph
3)mask
4)groupEdges
class Graph[VD, ED] {
  def reverse: Graph[VD, ED]
  def subgraph(epred: EdgeTriplet[VD,ED] => Boolean,
               vpred: (VertexId, VD) => Boolean): Graph[VD, ED]
  def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
  def groupEdges(merge: (ED, ED) => ED): Graph[VD,ED]
}
  1. reverse
	def reverse: Graph[VD, ED]

正如字面意思 (reverse 有逆向, 反转的意思), reverse 操作符将会返回一个所有边的方向都反转的新图. 此外, 在实现上, 它也不会产生任何数据移动或复制.
例如(以下代码省略构建图graph的过程):

边数据:
graph.edges.collect.foreach(println(_))
	//fromID,toID,property
	// Edge(1,2,(100,"property"))
	// Edge(2,3,(200,"property"))
	// Edge(3,1,(300,"property"))
反转后的结果:
graph.reverse.edges.collect.foreach(println(_))
	// Edge(2,1,(100,"property"))
	// Edge(3,2,(200,"property"))
	// Edge(1,3,(300,"property"))
  1. subgraph
	def subgraph(epred: EdgeTriplet[VD,ED] => Boolean,
		vpred: (VertexId, VD) => Boolean): Graph[VD, ED]

subgraph 对过滤一个图十分有用. 它接收两个返回值为布尔类型的判定函数, 第一个判定函数 epred 接受一个 EdgeTriplet 参数, 当 triplet 满足判定函数时返回 true. 同样, vpred 接受一个 (VertexId, VD), 当顶点满足判定函数时返回 true.
通过这两个判定函数, subgraph 所返回的图中仅包含满足条件的顶点和边. 默认情况下, 如果没有提供判定函数, 那么会直接返回 true. 这意味着我们仅能传入一个 edge 的判定函数, 一个 vertex 的判定函数, 或者两个同时传入. 如果 subgraph 仅传入了一个 vertex 的判定函数并且两个相邻的节点被过滤了出去, 那么这些节点所连接的边也会被自动过滤.
实际中的应用场景:1)有些图有一些独立的顶点或是缺失顶点信息的边. 我们就可以使用 subgraph 过滤掉这些元素. 2)另一个场景是当我们想要去除图中的 “超级节点”, 也就是那些与很多节点都相连的大型节点.
例如:

**1. vpred**:根据顶点条件建立子图
graph.subgraph(vpred = (vid, v) => v._2 >= 200).vertices.collect.foreach(println(_))
过滤前的数据:
	// (1,(Taro,100))
	// (2,(Jiro,200))
	// (3,(Sabo,300))
过滤后的数据:
	// (2,(Jiro,200))
	// (3,(Sabo,300))
**2.epred**:根据边条件建立子图
graph.subgraph(epred = edge => edge.attr._1 >= 200).edges.collect.foreach(println(_))
过滤前数据:
	// Edge(2,1,(100,Mon Dec 01 00:00:00 EST 2014))
	// Edge(3,2,(200,Tue Dec 02 00:00:00 EST 2014))
	// Edge(1,3,(300,Wed Dec 03 00:00:00 EST 2014))
过滤后数据:
	// Edge(2,3,(200,Tue Dec 02 00:00:00 EST 2014))
	// Edge(3,1,(300,Wed Dec 03 00:00:00 EST 2014))
**3.vpred and epred**:对顶点和边同时加限制
val subGraph = graph.subgraph(vpred = (vid, v) => v._2 >= 200, epred = edge => edge.attr._1 >= 200)
过滤后的数据:
1)点信息
subGraph.vertices.collect.foreach(println(_))
	// (2,(Jiro,200))
	// (3,(Sabo,300))
2)边信息
subGraph.edges.collect.foreach(println(_))
	// Edge(2,3,(200,Tue Dec 02 00:00:00 EST 2014))
  1. mask(返回一个子图,是原图和取子图的并集)
	def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]

mask 也是对一个图进行过滤. 与 subgraph 不同的是, mask 并不接受一个判定函数作为参数. 相反, 它的参数是另一个图. 然后,graph.mask(anotherGraph) 会构造出一个 graph 的子图, 这个所包含的顶点和边须同时在 anotherGraph 中出现. 基于另一个相关图的属性, mask 可以和 subgraph 一起用来完成对一个图的过滤.
实际应用场景:我们想要找出一个图中的连通组件, 但是同时想要移除在结果图中缺失属性的顶点. 我们可以使用 connectedComponents 算法, 然后在使用 mask 和 subgraph 来获得最终结果:
例如:

//运行连通组件
val ccGraph = graph.connectedComponents()
//删除缺少属性值的顶点和与之连接的边
val validGraph = graph.subgraph(vpred = (_, attr) => attr.info != "NA")
//将生成的组件限制为有效的子图
val validCCGraph = ccGraph.mask(validGraph)

注意mask返回的结果,是原子图和原图的交集(即是ccGraph和validGraph的交集)

  1. groupEdges
	def groupEdges(merge: (ED, ED) => ED): Graph[VD,ED]

groupEdges 可以将两个节点间的平行边变为单独的一条边. 为此, groupEdges 需要一个叫做 merge 的函数作为参数, merge 接受一个 (ED, ED) 的 pair 作为参数, 并将这两个 ED 合并为同一类型的单个属性. 因此, groupEdges 所返回的图与原图具有同样的类型.
例如:

边数据:
// edges.csv
// 1,2,100,property
// 1,2,110,property
// 2,3,200,property
// 2,3,210,property
// 3,1,300,property
// 3,1,310,property
val edgeLines2: RDD[String] = sc.textFile("/edges.csv")
val e2:RDD[Edge[((Long, java.util.Date))]] = edgeLines2.map(line => {
	val cols = line.split(",")
	Edge(cols(0).toLong, cols(1).toLong, (cols(2).toLong,cols(3)))
})
//点图 v 此处省略构建过程
val graph:Graph[(String, Long), (Long, java.util.Date)] = Graph(v, e2)
// 使用groupEdges语句将edge中相同Id的数据进行合并
val edgeGroupedGraph:Graph[(String, Long), (Long, java.util.Date)] = 
	graph.groupEdges(merge = (e1, e2) => (e1._1 + e2._1,
	if(e1._2.getTime < e2._2.getTime) e1._2 else e2._2))
	
edgeGroupedGraph.edges.collect.foreach(println(_))
	// Edge(1,2,(210,Mon Dec 01 00:00:00 EST 2014))
	// Edge(2,3,(410,Tue Dec 02 00:00:00 EST 2014))
	// Edge(3,1,(610,Wed Dec 03 00:00:00 EST 2014))

Join操作

在很多情况下需要将图和外部的RDDs进行连接,比如将一个额外的属性添加到一个已经存在的图上,或者将顶点属性从一个图导出到另一图中。在我们编写代码时,往往需要多次进行aggregateMessages和Join操作,因此这两个操作可以说是Graphx中非常重要的操作,需要非常熟练地掌握。
这里有两个join API可供使用:

class Graph[VD, ED] {
  def joinVertices[U](table: RDD[(VertexId, U)])(map: (VertexId, VD, U) => VD)
    : Graph[VD, ED]

  def outerJoinVertices[U, VD2](table: RDD[(VertexId, U)])(map: (VertexId, VD, Option[U]) => VD2)
    : Graph[VD2, ED]
}

但是两个结果差别很大:

  1. joinVertices链接:
    返回值的类型就是graph顶点属性的类型,不能新增,也不可以减少(即不能改变原始graph顶点属性类型和个数)。
  2. outJoinVertices
    更为常用,使用起来也更加自由的是outerJoinVertices,他不受原图graph顶点属性类型VD的限制,在outerJoinVertices中使用者可以随意定义自己想要的返回类型,从而可以完全改变图的顶点属性值的类型和属性的个数。
    代码:
import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD
 
object JoinTest {
    def main(args: Array[String]) = {
		val conf = new SparkConf().setAppName("JoinTest ").setMaster(args(0))
		val sc = new SparkContext(conf)
		// 利用edge信息生成图
		// edges.txt 数据:
		// 1 2
		// 2 3
		// 3 1
		val graph = GraphLoader.edgeListFile(sc, "/Jerry/test/edges.txt").cache()
		// edges.txt 数据:
		// 1 Taro
		// 2 Jiro
		val vertexLines = sc.textFile("/Jerry/text/vertices.txt")
		val users: RDD[(VertexId, String)] = vertexLines.map(line => {
				val fields = line.split(",")
				(fields(0).toLong,fields(1))
			})
		// 将users中的vertex属性添加到graph中,生成graph2
		// 使用joinVertices操作,用user中的属性替换图中对应Id的属性
		// 先将图中的顶点属性置空
		val graph2 = graph.mapVertices((id, attr) => "").joinVertices(users){(vid, empty, user) => user} 
		println("\n\n~~~~~~~~~ Confirm Vertices Internal of graph2 ")
		graph2.vertices.collect.foreach(println(_))
		// (1,Taro)
		// (2,Jiro)
		// (3,)
		// 使用outerJoinVertices将user中的属性赋给graph中的顶点,如果图中顶点不在user中,则赋值为None
		val graph3 = graph.mapVertices((id, attr) => "").outerJoinVertices(users){(vid, empty, user) => user.getOrElse("None")}
		println("\n\n~~~~~~~~~ Confirm Vertices Internal of graph3 ")
		graph3.vertices.collect.foreach(println(_))
		// (2,Jiro)
		// (1,Taro)
		// (3,None)
		// 结果表明,如果graph的顶点在user中,则将user的属性赋给graph中对应的顶点,否则赋值为None。
		sc.stop
	}

Degrees和Neighbors用法

代码:

import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object GraphX_Text01 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local").setAppName("GraphX_Text01")
    val sc = new SparkContext(conf)
    // 数据信息:edges.txt
    // 2 1
    // 3 1
    // 4 1
    // 5 1
    // 1 2
    // 4 3
    // 5 3
    // 1 4
    val graph = GraphLoader.edgeListFile(sc,"/Jerry/edges.txt").cache()

    // 一、degrees ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//    入度:
    graph.inDegrees.collect.foreach(d => println(d._1 + "'s inDegree is " + d._2))
    // 4's inDegree is 1
    // 2's inDegree is 1
    // 1's inDegree is 4
    // 3's inDegree is 2

//    出度:
    graph.outDegrees.collect.foreach(d => println(d._1 + "'s outDegree is " + d._2))
    // 4's outDegree is 2
    // 2's outDegree is 1
    // 1's outDegree is 2
    // 3's outDegree is 1
    // 5's outDegree is 2

//    求出对应点所有的度
    graph.degrees.collect.foreach(d => println(d._1 + "'s degree is " + d._2))
    // 4's degree is 3
    // 2's degree is 2
    // 1's degree is 6
    // 3's degree is 3
    // 5's degree is 2
    def max (a:(VertexId,Int),b:(VertexId,Int)): (VertexId,Int) ={
        if (a._2 > b._2) a else b
    }
    print(graph.inDegrees.reduce(max))
    // (1,4)


    // 二、collectNeighborIds ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    graph.collectNeighborIds(edgeDirection = EdgeDirection.In).collect().foreach(n => println(n._1 + "'s in neighbors : " + n._2.mkString(",")))
    // 4's in neighbors : 1
    // 2's in neighbors : 1
    // 1's in neighbors : 2,3,4,5
    // 3's in neighbors : 4,5
    // 5's in neighbors :
    graph.collectNeighborIds(EdgeDirection.Out).collect.foreach(n => println(n._1 + "'s out neighbors : " + n._2.mkString(",")))
    // 4's out neighbors : 1,3
    // 2's out neighbors : 1
    // 1's out neighbors : 2,4
    // 3's out neighbors : 1
    // 5's out neighbors : 1,3
    graph.collectNeighborIds(EdgeDirection.Either).collect.foreach(n => println(n._1 + "'s neighbors : " + n._2.distinct.mkString(",")))
    // 4's neighbors : 1,3
    // 2's neighbors : 1
    // 1's neighbors : 2,3,4,5
    // 3's neighbors : 1,4,5
    // 5's neighbors : 1,3
// collectNeighbor
    graph.collectNeighbors(EdgeDirection.Out).collect.foreach(n => println(n._1 + "'s out neighbors : " + n._2.mkString(",")))
    // 4's out neighbors : (1,1),(3,1)
    // 2's out neighbors : (1,1)
    // 1's out neighbors : (2,1),(4,1)
    // 3's out neighbors : (1,1)
    // 5's out neighbors : (1,1),(3,1)

    graph.collectNeighbors(EdgeDirection.Either).collect.foreach(n => println(n._1 + "'s neighbors : " + n._2.distinct.mkString(",")))
    // 4's neighbors : (1,1),(3,1)
    // 2's neighbors : (1,1)
    // 1's neighbors : (2,1),(3,1),(4,1),(5,1)
    // 3's neighbors : (1,1),(4,1),(5,1)
    // 5's neighbors : (1,1),(3,1)
    sc.stop
  }
}

aggregateMessages用法

graphx中一个很强大的方法 aggregateMessages,它可以将点的信息(srcId,dstId,attr,srcAttr和dstAttr)发送到src和dst的顶点,并做运算。
代码实现

package com.pingan.spark_graphx

import org.apache.spark.graphx.{Edge, Graph, VertexRDD}
import org.apache.spark.{SparkConf, SparkContext}

object Create_graphx {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("Create_graphx").setMaster("local")
    val sc = new SparkContext(conf)
    val vertices = sc.textFile("/a.txt").map { line =>
      val fields = line.split("#")
      (fields(0).toLong, fields(1).toInt)
    }
    val edges = sc.textFile("/b.txt").map { line =>
      val fields = line.split("#")
      Edge(fields(0).toLong, fields(1).toLong, fields(2))
    }
    val graph: Graph[Int, String] = Graph(vertices,edges)

//    对点的Attr做判断后再发送给上下点
    graph.aggregateMessages[(Int,Int)](
      ctx =>{
        if(ctx.srcAttr == ctx.dstAttr){
          ctx.sendToDst(1,ctx.srcAttr)
        }
      },
      (a,b) => (a._1+b._1,a._2+b._2)
    )

//    也可以求点的出入度    (入度,出度)
    val degree: VertexRDD[(Int, Int)] = graph.mapVertices((_, _) => (0, 0)).aggregateMessages[(Int, Int)](ctx => {
      ctx.sendToDst(1, 0)
      ctx.sendToSrc((0, 1))
    }, (a, b) => (a._1 + b._1, a._2 + b._2))
  }
}

connectedComponents的用法

connectedComponents(Connected components algorithm:连接组件算法):计算每个顶点的连通构件的隶属度,并返回一个顶点值包含该顶点的连通构件中最低的顶点id的图。
源码:

object ConnectedComponents {
  /**
   * Compute the connected component membership of each vertex and return a 
   * graph with the vertex value containing the lowest vertex id in the connected 
   * component containing that vertex.
   * 连接组件算法):计算每个顶点的连通构件的隶属度,并返回一个顶点值包含
   * 该顶点的连通构件中最低的顶点id的图。
   */
  def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED],
                                      maxIterations: Int): Graph[VertexId, ED] = {
    require(maxIterations > 0, s"Maximum of iterations must be greater than 0," +
      s" but got ${maxIterations}")

    val ccGraph = graph.mapVertices { case (vid, _) => vid }
    def sendMessage(edge: EdgeTriplet[VertexId, ED]): Iterator[(VertexId, VertexId)] = {
      if (edge.srcAttr < edge.dstAttr) {
        Iterator((edge.dstId, edge.srcAttr))
      } else if (edge.srcAttr > edge.dstAttr) {
        Iterator((edge.srcId, edge.dstAttr))
      } else {
        Iterator.empty
      }
    }
    val initialMessage = Long.MaxValue
    val pregelGraph = Pregel(ccGraph, initialMessage,
      maxIterations, EdgeDirection.Either)(
      vprog = (id, attr, msg) => math.min(attr, msg),
      sendMsg = sendMessage,
      mergeMsg = (a, b) => math.min(a, b))
    ccGraph.unpersist()
    pregelGraph
  } // end of connectedComponents

  /**
   * @tparam VD the vertex attribute type (discarded in the computation)
   * 顶点属性类型
   * @tparam ED the edge attribute type (preserved in the computation)
   * 边缘属性类型
   * @param graph the graph for which to compute the connected components
   * 用来计算连通分量的图形
   * @return a graph with vertex attributes containing the smallest vertex in each
   *  connected component
   * 返回包含每个连接组件中最小顶点的顶点属性的图
   */
  def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED]): Graph[VertexId, ED] = {
    run(graph, Int.MaxValue)
  }
}

代码示例:

package com.pingan.spark_graphx

import org.apache.spark.graphx.{Edge, Graph, VertexId, VertexRDD}
import org.apache.spark.{SparkConf, SparkContext}

object Create_graphx {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("Create_graphx").setMaster("local")
    val sc = new SparkContext(conf)
    val vertices = sc.textFile("/a.txt").map { line =>
      val fields = line.split("#")
      (fields(0).toLong, fields(1).toInt)
    }
    val edges = sc.textFile("/b.txt").map { line =>
      val fields = line.split("#")
      Edge(fields(0).toLong, fields(1).toLong, fields(2))
    }
    val graph: Graph[Int, String] = Graph(vertices,edges)

//    计算联通体
    val connectVertives: VertexRDD[VertexId] = graph.connectedComponents().vertices

    /*
    * 结果数据:
    * (key:vertexId类型所有顶点ID,value:vertexId类型key所在连通体ID)
    * (4,1)
    * (1,1)
    * (6,3)
    * (3,3)
    * (7,3)
    * (2,1)
    *
    * */
  }
}

grepel 的用法

1. PregelAPI
图本质上是一种递归的数据结构,其顶点的属性值依赖于其邻接顶点,而其邻接顶点属性又依赖于其邻接顶点,许多重要的图算法通过迭代计算每个顶点的属性直到到达定点条件,这些迭代的图算法被抽象成一系列图并行操作。
pregel是graphx中图的分布式迭代模型,是graphx lib中ConnectedComponents、PageRank、LabelPropagation、StronglyConnectedComponents、ShortestPaths等算法基础。可以说没有pregel模型,graphx的魅力会大打折扣。
2. Pregel 接口模型
那么graphx是如何实现Pregel迭代操作,我们应该如何使用该模型。
在这里插入图片描述
注意:
@param activeDirection the direction of edges incident to a vertex that received a message in the previous round on which to run sendMsg. For example, if this is EdgeDirection.Out, only out-edges of vertices that received a message in the previous round will run.
解释:
根据上一轮源点和终点烧到的源点和终点,判断本轮迭代中边三元组是否被激活。
Out:源点收到数据,边三元组被激活
In:终点收到数据,边三元组被激活
Either:源点和终点收到数据,边三原则被激活
Both:两点同时收到数据,边三元组被激活。
3. 接口源码

def apply[VD: ClassTag, ED: ClassTag, A: ClassTag]
     (graph: Graph[VD, ED],
      initialMsg: A,
      maxIterations: Int = Int.MaxValue,
      activeDirection: EdgeDirection = EdgeDirection.Either)
     (vprog: (VertexId, VD, A) => VD,
      sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],
      mergeMsg: (A, A) => A)
    : Graph[VD, ED] =
  {
    require(maxIterations > 0, s"Maximum number of iterations must be greater than 0," +
      s" but got ${maxIterations}")
    val checkpointInterval = graph.vertices.sparkContext.getConf
      .getInt("spark.graphx.pregel.checkpointInterval", -1)
    var g = graph.mapVertices((vid, vdata) => vprog(vid, vdata, initialMsg))
    val graphCheckpointer = new PeriodicGraphCheckpointer[VD, ED](
      checkpointInterval, graph.vertices.sparkContext)
    graphCheckpointer.update(g)
    // 计算消息
    var messages = GraphXUtils.mapReduceTriplets(g, sendMsg, mergeMsg)
    val messageCheckpointer = new PeriodicRDDCheckpointer[(VertexId, A)](
      checkpointInterval, graph.vertices.sparkContext)
    messageCheckpointer.update(messages.asInstanceOf[RDD[(VertexId, A)]])
    var activeMessages = messages.count()
    // 建图
    var prevG: Graph[VD, ED] = null
    var i = 0
    while (activeMessages > 0 && i < maxIterations) {
      // 接收消息并更新顶点
      prevG = g
      g = g.joinVertices(messages)(vprog)
      graphCheckpointer.update(g)
      val oldMessages = messages
      // 发送新消息,跳过双方都没有接收到消息的边缘。我们必须缓存消息,以便在下一行中实现它,从而允许我们解缓存上一个迭代
      messages = GraphXUtils.mapReduceTriplets(
        g, sendMsg, mergeMsg, Some((oldMessages, activeDirection))
      messageCheckpointer.update(messages.asInstanceOf[RDD[(VertexId, A)]])
      activeMessages = messages.count()
      logInfo("Pregel finished iteration " + i)
      oldMessages.unpersist(blocking = false)
      prevG.unpersistVertices(blocking = false)
      prevG.edges.unpersist(blocking = false)
      i += 1
    }
    messageCheckpointer.unpersistDataSet()
    graphCheckpointer.deleteAllCheckpoints()
    messageCheckpointer.deleteAllCheckpoints()
    g

pregel方法源码

  def pregel[A: ClassTag](
      initialMsg: A,
      maxIterations: Int = Int.MaxValue,
      activeDirection: EdgeDirection = EdgeDirection.Either)(
      vprog: (VertexId, VD, A) => VD,
      sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],
      mergeMsg: (A, A) => A)
    : Graph[VD, ED] = {
    Pregel(graph, initialMsg, maxIterations, activeDirection)(vprog, sendMsg, mergeMsg)
  }

4. 参数解释
pregel 方法在一个属性图上进行调用, 返回一个与原图有着同样类型和机构的新图. 当不再变化以后, 顶点的属性可能从一个超集中向下一个变化. pregel 有一下两个参数列表:
第一个列表包含:

initialMsg: 一个为用户定义类型 A 的初始消息 - 当算法启动时该信息会被每个顶点接收到.
maxIter: 最大迭代次数
activeDir: 发送消息所沿边的方向

当没有消息可发送或是达到最大迭代次数, 一个 pregel 算法才会终止. 在实现算法时, 非常重要的一点是要记得设置最大迭代次数, 尤其是那些无法保证收敛的算法.

如果没有指定边的方向 activeDir, pregel 会默认消息仅仅向每个顶点的出边发送. 此外, 如果一个顶点没有从上一个超集中接收到信息, 那么在当前超集结束的时候, 将不会向它的出边发送任何消息.
第二个参数列表必须包含三个函数:

vprog: (VertexId, VD, A) => VD: vprog (vertex program) 会对从上一轮迭代所有接收到消息的顶点更新它们的属性
mergeMsg: (A, A) => A):这个函数会对每个顶点接收到的消息进行合并.
sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)]: 这个函数接受一个 edge triplet 参数, 创建发送给边起点或终点的消息.

5. 代码示例

package com.pingan.spark_graphx

import org.apache.spark.graphx._
import org.apache.spark.{SparkConf, SparkContext}

object Test_Pregel {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    val sc = new SparkContext("local","Test_Pregel",conf)
    val edges = sc.makeRDD(Array((0, 1), (1, 2), (2, 3), (3, 4), (2, 4), (4, 1), (4, 5), (5, 6), (6, 1), (5, 7), (7, 8)))
      .map(x => Edge(x._1.toLong, x._2.toLong, "pp"))
    val vertices = sc.makeRDD(Array((0),(1),(2),(3),(4),(5),(6),(7),(8))).map(x => (x.toLong,"property"))
    val graph = Graph(vertices,edges)
    val cirecleGraph = Pregel(
      graph.mapVertices((id, attr) => Set[VertexId]()),
      Set[VertexId](),
      3,
      EdgeDirection.Out
    )(
      vprog = (id, attr, msg) => (msg ++ attr),
      sendMsg => Iterator((sendMsg.dstId, (sendMsg.srcAttr + sendMsg.srcId))),
      (a, b) => (a ++ b)
    ).subgraph(vpred = (id, attr) => attr.contains((id)))
    cirecleGraph.vertices.collect.map(x => (x._1,(x._2.mkString(",")))).foreach(println(_))
    sc.stop()
  }
}

结果文件:

  1. 迭代次数为:3
    在这里插入图片描述
  2. 迭代次数为:5
    在这里插入图片描述

PageRank的用法

PageRank是Google专有的算法,用于衡量特定网页相对于搜索引擎索引中的其他网页而言的重要程度。
PageRank网页排名算法的实现,该模型提供了两种调用方法:

第一种:在调用时提供一个指定迭代次数的参数number,运行迭代次数后无论结果如何,都会停止运算并返回结果图。
在这里插入图片描述
第二种:在调用时提供一个指定两次结果查值小于指定值的参数 tol,直到达到最终收敛效果后,才停止运行并返回结果图。
在这里插入图片描述
这是GraphX提供的用Pregel的模型改进后产生的图算法,通常我们直接调用:

graph.pageRank(0.0001)

源码解析:
第一种(静态)PageRank计算模型

def run[VD: ClassTag, ED: ClassTag](
      graph: Graph[VD, ED],numIter: Int, resetProb: Double = 0.15): Graph[Double, Double] =
  {
   //下列这段代码用于初始化PageRank图模型,具体内容是赋予每个顶点属性为值1.0,赋予每条边属性为值“1/该边的出发顶点的出度数”。
    val pagerankGraph: Graph[Double, Double] = graph
   //将每个顶点进行连接(度的传递)得到顶点属性值为出度数
     .outerJoinVertices(graph.outDegrees) { (vid, vdata, deg) =>deg.getOrElse(0) }
   //通过顶点的出度数为每条边设置权重值;这里是Triplet型的迭代器不停地执行一个map函数来遍历得到每条边的权重值,值为1.0/顶点出度数
      .mapTriplets( e => 1.0 / e.srcAttr )
   //设置每个顶点的初始属性值为1.0
      .mapVertices( (id,attr) => 1.0 )
      .cache()  //将完成初始化的图缓存操作


   //以下将定义三个所需函数来完成GraphX对PageRank的算法实现
    //用作 Pregel的message
    //第一个函数用于返回一个考虑“随机事件”发生后的计算结果
    def vertexProgram(id: VertexId, attr: Double, msgSum: Double): Double=
      resetProb + (1.0 - resetProb) * msgSum
    //第二个函数用于得到一个迭代器,里面包含了两个信息:该边的目的ID、该边的源属性值和权重的乘积(该边传递的实际PR值)
    def sendMessage(edge: EdgeTriplet[Double, Double]) =
      Iterator((edge.dstId, edge.srcAttr* edge.attr))
    //第三个函数用于将顶点属性值和传递的值进行累加
    def messageCombiner(a: Double, b: Double): Double = a + b
 
    //在该PageRank模型中每个顶点接受到的初始传递信息都是0.0
    val initialMessage = 0.0
 
    // 执行 pregel 模型算法(固定的迭代次数)
    Pregel(pagerankGraph, initialMessage, numIter, activeDirection= EdgeDirection.Out)(
      vertexProgram,sendMessage, messageCombiner)
  }



run中的几个参数解释:
         numIter:固定的PageRank计算的迭代次数
         resetProb:随机重置的概率,通常都是0.15
         Graph:返回值,以图的形式包括最终的顶点值(pagerank值)和边值(权重值),进而得到最终的排名结果

第二种(动态)PankRank计算模型
初始化参数和上面不同的是少了numIter(迭代次数),多了tol(比较两次迭代的结果差

def runUntilConvergence[VD: ClassTag, ED: ClassTag](
      graph: Graph[VD, ED], tol: Double, resetProb:Double = 0.15): Graph[Double,Double] =
  {
// 下段代码同样用于初始化图形
    val pagerankGraph: Graph[(Double, Double), Double] = graph
//同上,将每个顶点进行连接(度的传递)得到顶点属性值为出度数
     .outerJoinVertices(graph.outDegrees) {
        (vid, vdata, deg)=> deg.getOrElse(0)
      }
//边属性值(权重)的初始化,值为1.0/顶点出度数
      .mapTriplets( e => 1.0 / e.srcAttr )
// 顶点属性值的初始化,但是属性值带两个参数即(初始PR值,两次迭代结果的差值)
      .mapVertices( (id,attr) => (0.0, 0.0) )
      .cache()



// 第一个函数多了一个返回值delta(newPR-oldPR)
    def vertexProgram(id: VertexId, attr: (Double, Double), msgSum: Double): (Double, Double) = {
      val (oldPR, lastDelta) = attr
      val newPR = oldPR + (1.0 - resetProb) * msgSum
      (newPR, newPR - oldPR)
    }
// 第二个函数同样用于得到一个迭代器,但是多了一个条件判定:如果源顶点的delta值小于tol就清空迭代器即返回空迭代。
    def sendMessage(edge: EdgeTriplet[(Double, Double), Double]) = {
      if (edge.srcAttr._2 > tol) {
        Iterator((edge.dstId, edge.srcAttr._2 * edge.attr))
      } else {
        Iterator.empty
      }
    }
    def messageCombiner(a: Double, b: Double): Double = a + b
// 每个顶点接受到的初始传递信息值不是0,而是resetProb / (1.0 - resetProb)
    val initialMessage = resetProb / (1.0 - resetProb)
 
// 动态执行 Pregel 模型(直至结果最终收敛)
    Pregel(pagerankGraph, initialMessage, activeDirection = EdgeDirection.Out)(
      vertexProgram, sendMessage, messageCombiner)
      .mapVertices((vid, attr) => attr._1)
  }

代码示例
Pregel相当于图计算的引擎,用于图计算的大框架(对顶点的消息计算、消息发送、消息合并),它是图迭代的执行者。lib中的所有算法模型最后都会调用Pregel。
第一种(动态)PageRank计算模型

package com.pingan.spark_graphx

import org.apache.spark.graphx.{Edge, Graph, GraphLoader, VertexRDD}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object CreatePageRank {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    val sc = new SparkContext("local","CreatePageRank",conf)
//    这里加载的文件是spark自带的测试文件,上传到集群
    val graph = GraphLoader.edgeListFile(sc,"/Jerry/test/followers.txt")
    /*
      followers.txt文件数据:
      2 1
      4 1
      1 2
      6 3
      7 3
      7 6
      6 7
      3 7
    * */

//    pageRank后点信息
    val verticesPageRank: VertexRDD[Double] = graph.pageRank(0.0001).vertices
//    pageRank后边信息
    val edgesPageRank = graph.pageRank(0.0001).edges
    val users: RDD[(Long, String)] = sc.textFile("/Jerry/test/users.txt").map(x => {
      val fields = x.split(",")
      (fields(0).toLong, fields(1))
    })
    val ranksByUsername = users.join(verticesPageRank).map {
      case (id, (username, rank)) => (username, rank)
    }
    println(ranksByUsername.collect().mkString("/n"))
    println("####")
    println(edgesPageRank.collect().mkString("/n"))
  }
}

结果文件:
在这里插入图片描述

graph.pageRank(0.0001)
总结:参数值越小得到的结果越有说服力。

第二种(静态)PankRank计算模型

package com.pingan.spark_graphx

import org.apache.spark.graphx.{Edge, Graph, GraphLoader, VertexRDD}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object CreatePageRank {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    val sc = new SparkContext("local","CreatePageRank",conf)
//    这里加载的文件是spark自带的测试文件,上传到集群
    val graph = GraphLoader.edgeListFile(sc,"/Jerry/test/followers.txt")
    /*
      followers.txt文件数据:
      2 1
      4 1
      1 2
      6 3
      7 3
      7 6
      6 7
      3 7
      
      user.txt文件数据:
      1,BarackObama,Barack Obama
	2,ladygaga,Goddess of Love
	3,jeresig,John Resig
	4,justinbieber,Justin Bieber
	6,matei_zaharia,Matei Zaharia
	7,odersky,Martin Odersky
	8,anonsys
    * */

//    pageRank后点信息
    val verticesPageRank: VertexRDD[Double] = graph.staticPageRank(5).vertices
//    pageRank后边信息
    val edgesPageRank = graph.pageRank(0.0001).edges
    val users: RDD[(Long, String)] = sc.textFile("/Jerry/test/users.txt").map(x => {
      val fields = x.split(",")
      (fields(0).toLong, fields(1))
    })
    val ranksByUsername = users.join(verticesPageRank).map {
      case (id, (username, rank)) => (username, rank)
    }
    println(ranksByUsername.collect().mkString("/n"))
    println("####")
    println(edgesPageRank.collect().mkString("/n"))
  }
}

结果文件:
在这里插入图片描述

总结:用静态算法很难得到准确的结果,较少使用。

总结:以上两种模型在调用上不同,动态模型常用,参数越小,结果越有说服力。但是代入参数不同而造成的结果不同,例如静态和动态调用哪种更适合,又或者迭代次数的选择、前后两次迭代的差值限定又该选择多少,这些都是没有固定标准的。
需要说明的是:Google的网页排名并非如此单纯的PageRank算法,它考虑的综合因素至少有10点以上。

动,静态模型调用方式:

graph.staticPageRank(0.0001)				静态模型
graph.PageRank(5)						动态模型

TriangleCount 的用法

TriangleCount用于计算通过每个顶点的三角形的数量,该算法相对简单,可分三步计算:

<ul>
 <li> Compute the set of neighbors for each vertex</li>
 	第一步:计算每个顶点的邻域集合
 <li> For each edge compute the intersection of the sets and send the count to both vertices.</li>
 	第二步:对于每条边计算集合的交集并将计数发送到两个顶点
 <li> Compute the sum at each vertex and divide by two since each triangle is counted twice.</li>
	 第三步:计算每个顶点的和,然后除以2,因为每个三角形数两次
</ul>


There are two implementations.  The default `TriangleCount.run` implementation first removes self cycles and canonicalizes the graph to ensure that the following conditions hold:
这里有两个实现。默认的“TriangleCount.run”实现首先删除自循环,并将图形规范化,以确保以下条件保持不变


<ul>
  <li> There are no self edges</li>
	没有自边
  <li> All edges are oriented (src is greater than dst)</li>
	所有边都是有方向的(src大于dst)
  <li> There are no duplicate edges</li>
	没有重复的边
</ul>

However, the canonicalization procedure is costly as it requires repartitioning the graph.If the input data is already in "canonical form" with self cycles removed then the`TriangleCount.runPreCanonicalized` should be used instead.
然而,规范化过程代价高昂,因为它需要重新分区图形。如果输入数据已经是“标准形式”,并且去掉了自循环,那么应该使用“trianglecount . runprecanonicalized”。

 {{{
  val canonicalGraph = graph.mapEdges(e => 1).removeSelfEdges().canonicalizeEdges()
  val counts = TriangleCount.runPreCanonicalized(canonicalGraph).vertices
 }}}

源码:

object TriangleCount {

  def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED]): Graph[Int, ED] = {
    // 将边缘数据转换为便于洗牌和规范化的数据
    val canonicalGraph = graph.mapEdges(e => true).removeSelfEdges().convertToCanonicalEdges()
    // 得到三角形计数
    val counters = runPreCanonicalized(canonicalGraph).vertices
    // 将它们与原始图形连接起来
    graph.outerJoinVertices(counters) { (vid, _, optCounter: Option[Int]) =>
      optCounter.getOrElse(0)
    }
  }

  def runPreCanonicalized[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED]): Graph[Int, ED] = {
    // 构造邻域的集合表示
    val nbrSets: VertexRDD[VertexSet] =
      graph.collectNeighborIds(EdgeDirection.Either).mapValues { (vid, nbrs) =>
        val set = new VertexSet(nbrs.length)
        var i = 0
        while (i < nbrs.length) {
          // prevent self cycle
          if (nbrs(i) != vid) {
            set.add(nbrs(i))
          }
          i += 1
        }
        set
      }

    //用图形连接这些集合
    val setGraph: Graph[VertexSet, ED] = graph.outerJoinVertices(nbrSets) {
      (vid, _, optSet) => optSet.getOrElse(null)
    }

    // Edge function computes intersection of smaller vertex with larger vertex
    //边函数计算小点与大点的交点
    def edgeFunc(ctx: EdgeContext[VertexSet, ED, Int]) {
      val (smallSet, largeSet) = if (ctx.srcAttr.size < ctx.dstAttr.size) {
        (ctx.srcAttr, ctx.dstAttr)
      } else {
        (ctx.dstAttr, ctx.srcAttr)
      }
      val iter = smallSet.iterator
      var counter: Int = 0
      while (iter.hasNext) {
        val vid = iter.next()
        if (vid != ctx.srcId && vid != ctx.dstId && largeSet.contains(vid)) {
          counter += 1
        }
      }
      ctx.sendToSrc(counter)
      ctx.sendToDst(counter)
    }

    // 计算沿边的交点
    val counters: VertexRDD[Int] = setGraph.aggregateMessages(edgeFunc, _ + _)
    // 将计数器与图形合并并除以2,因为每个三角形计算两次
    graph.outerJoinVertices(counters) { (_, _, optCounter: Option[Int]) =>
      val dblCount = optCounter.getOrElse(0)
      // 这个算法对每个三角形重复计数,所以最终计数应该是偶数
      require(dblCount % 2 == 0, "Triangle count resulted in an invalid number of triangles.")
      dblCount / 2
    }
  }
}

代码示例:

package com.pingan.spark_graphx

import org.apache.spark.graphx.{GraphLoader, PartitionStrategy}
import org.apache.spark.{SparkConf, SparkContext}

object Test_TrangleCount {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("ConnectedComponents").setMaster("local[4]")
    val sc = new SparkContext(conf)
    val graph = GraphLoader.edgeListFile(sc, "/followers.txt").partitionBy(PartitionStrategy.RandomVertexCut)
    // 求每个顶点的三角形计数
    val triCounts = graph.triangleCount().vertices
    // 用usernames连接三角形计数
    val users = sc.textFile("/users.txt").map { line =>
      val fields = line.split(",")
      (fields(0).toLong, fields(1))
    }
    val triCountByUsername = users.join(triCounts).map {
      case (id, (username, tc)) =>
        (username, tc)
    }
    println("edges:");
    graph.edges.collect.foreach(println)
    graph.edges.collect.foreach(println)
    println("vertices:");
    graph.vertices.collect.foreach(println)
    println("triplets:");
    graph.triplets.collect.foreach(println)
    println("\nusers");
    users.collect.foreach(println)
    println("\n triCounts:");
    triCounts.collect.foreach(println)
    println("\n triCountByUsername:");
    println(triCountByUsername.collect().mkString("\n"))
  }
}

结果:

edges:
	Edge(1,2,1)
	Edge(1,2,1)
	Edge(1,4,1)
	Edge(3,6,1)
	Edge(3,7,1)
	Edge(3,7,1)
	Edge(6,7,1)
	Edge(6,7,1)
	Edge(1,2,1)
	Edge(1,2,1)
	Edge(1,4,1)
	Edge(3,6,1)
	Edge(3,7,1)
	Edge(3,7,1)
	Edge(6,7,1)
	Edge(6,7,1)
vertices:
	(4,1)
	(6,1)
	(2,1)
	(1,1)
	(3,1)
	(7,1)
triplets:
	((1,1),(2,1),1)
	((1,1),(2,1),1)
	((1,1),(4,1),1)
	((3,1),(6,1),1)
	((3,1),(7,1),1)
	((3,1),(7,1),1)
	((6,1),(7,1),1)
	((6,1),(7,1),1)
users
	(1,BarackObama)
	(2,ladygaga)
	(3,jeresig)
	(4,justinbieber)
	(6,matei_zaharia)
	(7,odersky)
	(8,anonsys)
triCounts:
	(4,0)
	(6,1)
	(2,0)
	(1,0)
	(3,1)
	(7,1)
triCountByUsername:
	(justinbieber,0)
	(matei_zaharia,1)
	(ladygaga,0)
	(BarackObama,0)
	(jeresig,1)
	(odersky,1)

**总结:**TrangleCount 用于计算图点处于三角形关系的数量(即每个点的三角关系的数量)

StronglyConnectedComponents的用法

强连通分量:
计算每个顶点的强连通分量(SCC),并返回一个顶点值,该顶点值包含包含该顶点的SCC中的最低顶点id
解释:有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
在这里插入图片描述
算法思路
1)Kosaraju算法思路
这个算法可以说是最容易理解,最通用的算法,其比较关键的部分是同时应用了原图G和反图GT。步骤1:先用对原图G进行深搜形成森林(树),步骤2:然后任选一棵树对其进行深搜(注意这次深搜节点A能往子节点B走的要求是EAB存在于反图GT),能遍历到的顶点就是一个强连通分量。余下部分和原来的森林一起组成一个新的森林,继续步骤2直到 没有顶点为止。
2)Tarjan算法思路
这个算法思路不难理解,由开篇第一句话可知,任何一个强连通分量,必定是对原图的深度优先搜索树的子树。那么其实,我们只要确定每个强连通分量的子树的根,然后根据这些根从树的最低层开始,一个一个的拿出强连通分量即可。那么剩下的问题就只剩下如何确定强连通分量的根和如何从最低层开始拿出强连通分量了。
3)Gabow算法思路
这个算法其实就是Tarjan算法的变异体,我们观察一下,只是它用第二个堆栈来辅助求出强连通分量的根,而不是Tarjan算法里面的indx[]和mlik[]数组。那么,我们说一下如何使用第二个堆栈来辅助求出强连通分量的根。

***算法总结:***做一个总结:Kosaraju算法的第二次深搜隐藏了一个拓扑性质,而Tarjan算法和Gabow算法省略了第二次深搜,所以,它们不具有拓扑性质。Tarjan算法用堆栈和标记,Gabow用两个堆栈(其中一个堆栈的实质是代替了Tarjan算法的标记部分)来代替Kosaraju算法的第二次深搜,所以只用一次深搜,效率比Kosaraju算法要高。
源码:

object StronglyConnectedComponents {
  /**
   * Compute the strongly connected component (SCC) of each vertex and return a graph with the
   * vertex value containing the lowest vertex id in the SCC containing that vertex.
   * 计算每个顶点的强连通分量(SCC),并返回一个顶点值,该顶点值包含包含该顶点的SCC中的最低顶点id
   * @tparam VD the vertex attribute type (discarded in the computation)
   * 顶点属性类型
   * @tparam ED the edge attribute type (preserved in the computation)
   * 边缘属性类型
   * @param graph the graph for which to compute the SCC
   * 用来计算SCC的图形
   * @return a graph with vertex attributes containing the smallest vertex id in each SCC
   * 返回包含每个SCC中最小顶点id的顶点属性的图
   */
  def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED], numIter: Int): Graph[VertexId, ED] = {
    require(numIter > 0, s"Number of iterations must be greater than 0," +
      s" but got ${numIter}")

    // 我们用最终SCC id更新的图形,以及在最后返回的图形
    var sccGraph = graph.mapVertices { case (vid, _) => vid }
    // 我们在迭代中要用到的图形
    var sccWorkGraph = graph.mapVertices { case (vid, _) => (vid, false) }.cache()

    // 帮助变量来取消缓存的图形的持久化
    var prevSccGraph = sccGraph

    var numVertices = sccWorkGraph.numVertices
    var iter = 0
    while (sccWorkGraph.numVertices > 0 && iter < numIter) {
      iter += 1
      do {
        numVertices = sccWorkGraph.numVertices
        sccWorkGraph = sccWorkGraph.outerJoinVertices(sccWorkGraph.outDegrees) {
          (vid, data, degreeOpt) => if (degreeOpt.isDefined) data else (vid, true)
        }.outerJoinVertices(sccWorkGraph.inDegrees) {
          (vid, data, degreeOpt) => if (degreeOpt.isDefined) data else (vid, true)
        }.cache()

        // 获取所有要移除的顶点
        val finalVertices = sccWorkGraph.vertices
            .filter { case (vid, (scc, isFinal)) => isFinal}
            .mapValues { (vid, data) => data._1}

        // 向sccGraph写入值
        sccGraph = sccGraph.outerJoinVertices(finalVertices) {
          (vid, scc, opt) => opt.getOrElse(scc)
        }.cache()
        // 使顶点和边具体化
        sccGraph.vertices.count()
        sccGraph.edges.count()
        // sccGraph实现后,可以在以前的基础上实现非持久化
        prevSccGraph.unpersist(blocking = false)
        prevSccGraph = sccGraph

        // 只保留非最终顶点
        sccWorkGraph = sccWorkGraph.subgraph(vpred = (vid, data) => !data._2).cache()
      } while (sccWorkGraph.numVertices < numVertices)

      // 如果iter < numIter此时返回sccGraph
      // 不会被重新计算并且 pregel 算法的执行是没有意义的
      if (iter < numIter) {
        sccWorkGraph = sccWorkGraph.mapVertices { case (vid, (color, isFinal)) => (vid, isFinal) }

        // 收集我邻居的所有scc值的最小值,如果它比我的值小就更新,然后通知任何scc值大于我的
        sccWorkGraph = Pregel[(VertexId, Boolean), ED, VertexId](
          sccWorkGraph, Long.MaxValue, activeDirection = EdgeDirection.Out)(
          (vid, myScc, neighborScc) => (math.min(myScc._1, neighborScc), myScc._2),
          e => {
            if (e.srcAttr._1 < e.dstAttr._1) {
              Iterator((e.dstId, e.srcAttr._1))
            } else {
              Iterator()
            }
          },
          (vid1, vid2) => math.min(vid1, vid2))

        // 从SCCs的根开始。反向遍历值,如果颜色不匹配,通知所有邻居不要传播!
        sccWorkGraph = Pregel[(VertexId, Boolean), ED, Boolean](
          sccWorkGraph, false, activeDirection = EdgeDirection.In)(
          // 如果它是颜色的根或者它的颜色和邻接的颜色一样,那么顶点是最终的
          (vid, myScc, existsSameColorFinalNeighbor) => {
            val isColorRoot = vid == myScc._1
            (myScc._1, myScc._2 || isColorRoot || existsSameColorFinalNeighbor)
          },
          // activate neighbor if they are not final, you are, and you have the same color
          e => {
            val sameColor = e.dstAttr._1 == e.srcAttr._1
            val onlyDstIsFinal = e.dstAttr._2 && !e.srcAttr._2
            if (sameColor && onlyDstIsFinal) {
              Iterator((e.srcId, e.dstAttr._2))
            } else {
              Iterator()
            }
          },
          (final1, final2) => final1 || final2)
      }
    }
    sccGraph
  }
}

代码示例:

package com.pingan.spark_graphx

import org.apache.spark.graphx._
import org.apache.spark.{SparkConf, SparkContext}

object Test_StronglyConnectedComponents {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    val sc = new SparkContext("local","Test_StronglyConnectedComponents",conf)
    val edges = sc.makeRDD(Array((0, 1), (1, 2), (2, 3), (3, 4), (2, 4), (4, 1), (4, 5), (5, 6), (6, 1), (5, 7), (7, 8)))
      .map(x => Edge(x._1.toLong, x._2.toLong, "pp"))
    val vertices = sc.makeRDD(Array((0),(1),(2),(3),(4),(5),(6),(7),(8))).map(x => (x.toLong,"property"))
    val graph: Graph[String, String] = Graph(vertices,edges)
    val stronglyGraph: Graph[VertexId, String] = graph.stronglyConnectedComponents(2)
    val stronglyVertices: VertexRDD[VertexId] = stronglyGraph.vertices
    val stronglyEdges: EdgeRDD[String] = stronglyGraph.edges
    stronglyVertices.foreach(println(_))
    println("#########")
    stronglyEdges.foreach(println(_))
  }
}

结果:

(4,1)
(0,0)
(1,1)
(6,1)
(3,1)
(7,7)
(8,8)
(5,1)
(2,1)
#########
Edge(0,1,pp)
Edge(1,2,pp)
Edge(2,3,pp)
Edge(2,4,pp)
Edge(3,4,pp)
Edge(4,1,pp)
Edge(4,5,pp)
Edge(5,6,pp)
Edge(5,7,pp)
Edge(6,1,pp)
Edge(7,8,pp)

Process finished with exit code 0

写在最后:

到此,spark之Graphx的官网知识基本梳理完了,欢迎指正:QQ号:1054227887。

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值