【Spark指南】- 图分析

第一部分 Spark介绍
第二部分 Spark的使用基础
第三部分 Spark工具箱
第四部分 使用不同的数据类型
第五部分 高级分析和机器学习
第六部分 MLlib应用
第七部分 图分析
第八部分 深度学习

本章会摄入一个更专业的工具箱:图形处理。graphs中,nodes或vertics是基本单元,edges定义他们之间的关系。图分析的过程就是分析这些关系的过程。一个图的例子就是你的朋友圈,在图分析的语境下,每个 节点 代表一个人,而每个边代表关系。

下图表示一个边带有方向的 有向图。

【Spark指南】- 图分析_第1张图片

同样地也有无向图。

用我们的例子,边的权重 代表不同朋友之间的亲密程度。熟人间会有底权重的边,而已婚的个体间会有高权重的边。我们可以通过观察节点之间的通信频率来推断出这一点,并相应地对边缘进行加权。图是一种自然的方式 来描述许多不同问题集之间的关系,Spark提供了处理这种范式的很多方法。有一些商用案例,如信用卡诈骗,书目网络中的论文的重要性(哪些论文是被引用最多的),网页排名如谷歌使用了PageRank算法。

当Spark第一次出现时,包含了一个叫做GraphX的函数库来执行图像处理。GraphX提供了一个在RDD API上执行图像分析的 交互接口。者提供了一个非常底层的交互,且非常强大,但就像RDDs,不是最强大的。GraphX依然是Spark的一个核心部分。一些公司仍在其之上构建产品应用,它仍然可以看到一些较小的特性开发。这个API有很好的文档说明,因为自从其被创建依赖没有变换太多。然而,一些Spark的开发者(包括一些GraphX的原作者)最近创造了下一代图分析函数库,叫做GraphFrames。GraphFrames扩展了GraphX,提供一个DataFrame API 并支持Spark的多语言绑定。所以Python用户也可以利用该工具的可伸缩性。

GraphFrames目前作为一个Spark Package 是可用的,一个在启动Spark application前需要加载的外部包,但以后会融合进Spark的核心包。当使用graphframe时,会有一些小开销,但是在大多数情况下,它会在适当的地方调用GraphX,并且对于大多数情况,用户体验获得的好处要远远大于这个小开销。

GraphFrames和Graph Databases相比区别在哪?Spark不是一个数据库,是一个分布式计算引擎。你可以在Spark上构建一个graph API,但它的处理问题从根本上不同于一个数据库。GraphFrames 相比于许多Graph Databases可以扩展到更大的工作负载,并在分析上执行的很好,而不是在事务数据处理和服务上。

本章的目的是向你展示如何使用GraphFrames在Spark上执行图形分析。我们使用公共自行车数据。

为了进行设置,你需要指向正确的包。

./bin/spark-shell --packages graphframes:graphframes:0.5.0-spark2.1-s_2.11

%scala
val bikeStations = spark.read
  .option("header","true")
  .csv("/mnt/defg/bike-data/201508_station_data.csv")
val tripData = spark.read
  .option("header","true")
  .csv("/mnt/defg/bike-data/201508_trip_data.csv")

%python
bikeStations = spark.read\
  .option("header","true")\
  .csv("/mnt/defg/bike-data/201508_station_data.csv")
tripData = spark.read\
  .option("header","true")\
  .csv("/mnt/defg/bike-data/201508_trip_data.csv")


构建图

第一步是构建图,我们需要定义边和节点。在我们的例子中我们创建一个有向图。这个图会从原点指向位置点。在自行车旅行的背景下,就是从旅行起点指向旅行终点。为了定义图,我们使用GraphFrames 函数库中提出的命名规格。在节点表中我们定义我们的标识符为id,在边表中我们标记原id为src ,目的地id为dst。

%scala
val stationVertices = bikeStations
  .withColumnRenamed("name", "id")
  .distinct()
val tripEdges = tripData
  .withColumnRenamed("Start Station", "src")
  .withColumnRenamed("End Station", "dst")

%python
stationVertices = bikeStations\
  .withColumnRenamed("name", "id")\
  .distinct()
tripEdges = tripData\
  .withColumnRenamed("Start Station", "src")\
  .withColumnRenamed("End Station", "dst")

这允许我们根据已有的DataFrame来构建图。我们也会利用缓存,因为我们会在后面的查询中频繁访问这个数据。

%scala
import org.graphframes.GraphFrame
val stationGraph = GraphFrame(stationVertices, tripEdges)
stationGraph.cache()

%python
from graphframes import GraphFrame
stationGraph = GraphFrame(stationVertices, tripEdges)
stationGraph.cache()

现在我们可以看到数据的基本统计(查询原始DataFrame以确保我们得到期望的结果)

%scala
println(s"Total Number of Stations: ${stationGraph.vertices.count()}")
println(s"Total Number of Trips in Graph: ${stationGraph.edges.count()}")
println(s"Total Number of Trips in Original Data: ${tripData.count()}")

%python
print "Total Number of Stations: " + str(stationGraph.vertices.count())
print "Total Number of Trips in Graph: " + str(stationGraph.edges.count())
print "Total Number of Trips in Original Data: " + str(tripData.count())

>>>
Total Number of Stations: 70
Total Number of Trips in Graph: 354152
Total Number of Trips in Original Data: 354152


查询图

与图交互的最基本的方法是对其进行查询,执行类似 对旅行数计数 和 通过给定目的地进行过滤 的东西。就像DataFrames,GraphFrames 提供了对节点 和边的简单交互。

%scala
import org.apache.spark.sql.functions.desc
stationGraph
  .edges
  .groupBy("src", "dst")
  .count()
  .orderBy(desc("count"))
  .show(10)

%python
from pyspark.sql.functions import desc
stationGraph\
  .edges\
  .groupBy("src", "dst")
  .count()\
  .orderBy(desc("count"))\
  .show(10)

>>>
+--------------------+--------------------+-----+
|                 src|                 dst|count|
+--------------------+--------------------+-----+
|San Francisco Cal...|     Townsend at 7th| 3748|
|Harry Bridges Pla...|Embarcadero at Sa...| 3145|
...
|     Townsend at 7th|San Francisco Cal...| 2192|
|Temporary Transba...|San Francisco Cal...| 2184|
+--------------------+--------------------+-----+

我们也可以通过任意有效的DataFrame表达式来进行过滤。本例中我想查看一个特定站点,及进入和离开这个站点的数量。

%scala
stationGraph
  .edges
  .where("src = ‘Townsend at 7th’ OR dst = ‘Townsend at 7th’")
  .groupBy("src", "dst")
  .count()
  .orderBy(desc("count"))
  .show(10)

%python
stationGraph\
  .edges\
  .where("src = ‘Townsend at 7th’ OR dst = ‘Townsend at 7th’")\
  .groupBy("src", "dst")\
  .count()\
  .orderBy(desc("count"))\
  .show(10)

>>>
+--------------------+--------------------+-----+
|                 src|                 dst|count|
+--------------------+--------------------+-----+
|San Francisco Cal...|     Townsend at 7th| 3748|
|     Townsend at 7th|San Francisco Cal...| 2734|
...
|   Steuart at Market|     Townsend at 7th|  746|
|     Townsend at 7th|Temporary Transba...|  740|
+--------------------+--------------------+-----+
子图

子图就是大图中的较小的图。我们在上面已经看到如何来查询给定的节点和变得集合。我们可以用它来建立子图。

%scala
val townAnd7thEdges = stationGraph
  .edges
  .where("src = ‘Townsend at 7th’ OR dst = ‘Townsend at 7th’")
val subgraph = GraphFrame(stationGraph.vertices, townAnd7thEdges)

%python
townAnd7thEdges = stationGraph\
  .edges\
  .where("src = ‘Townsend at 7th’ OR dst = ‘Townsend at 7th’")
subgraph = GraphFrame(stationGraph.vertices, townAnd7thEdges)

然后,我们可以运用接下来的算法到原始图和子图上。


图算法

一个图只是一个 数据的逻辑表示法。图理论提供了很多算法来 以这种格式描述数据,GraphFrames允许我们利用许多现成的算法。随着新的算法被添加到GraphFrames中,开发还在继续,所以这个列表可能会继续增长。

PageRank

PageRank可以说是最多产的图形算法之一。Larry Page,谷歌的创始人,创造了用于如何排列网页的研究项目的PageRank。引用维基百科上对其的高级解释是:
PageRank 通过计算连接到一个网页的数量和质量 来 决定一个关于这个网页有多重要的粗略估计。底层的假设是更重要的网站可能会收到更多来自其他网站的链接。

而PageRank在Web领域之外也推广得很好。我们可以将其用到我们的数据中,对重要的自行车站有一个了解。在本例中,重要自行车站会被分配大的PageRank值。

图算法APIs:GraphFrames中大多数算法的参数和返回值 通过获取参数的方法(如PageRank算法为resetProbability)来访问。大多数算法 返回一个新的GraphFrames或一个DataFrame。算法的结果被存储为GraphFrames向量(和/或 点)或DataFrame中的一个或多个列。对于PageRank,算法返回一个GraphFrame,我们可以从新的pagerank列中为每个点 提取估计的PageRank值。

注意:依赖于机器的可用资源,计算会花些时间。你也可以在运行这些来获得结果前,尝试一个较小的数据集。在Databricks 社区版中,会花费大概20秒来运行,但一些评论者发现在他们的个人电脑上会花费更长的时间。

%scala
import org.apache.spark.sql.functions.desc
val ranks = stationGraph.pageRank
  .resetProbability(0.15)
  .maxIter(10)
  .run()
ranks.vertices
  .orderBy(desc("pagerank"))
  .select("id", "pagerank")
  .show(10)

%python
from pyspark.sql.functions import desc
ranks = stationGraph.pageRank(resetProbability=0.15, maxIter=10)
ranks.vertices\
  .orderBy(desc("pagerank"))\
  .select("id", "pagerank")\
  .show(10)

>>>
+--------------------+------------------+
|                  id|          pagerank|
+--------------------+------------------+
|San Jose Diridon ...| 4.051504835989922|
|San Francisco Cal...|3.3511832964279518|
...
|     Townsend at 7th| 1.568456580534273|
|Embarcadero at Sa...|1.5414242087749768|
+--------------------+------------------+

有趣的是,我们看到Caltrin站排名非常靠前。这是有道理的,因为那是自然连接点,许多自行车旅行会在这里结束。

In and Out Degrees

我们的图是有向图。这是由于自行车旅行是有向的,从一个地方开始到另一个地方结束。一个常见任务是计算一个给定车站的进出数目。我们之前为旅行计数,在本示例中,我们计算给定站的出入数。我们分别用in-degree和out-degree来表示。

【Spark指南】- 图分析_第2张图片

这特别适用于社交网络背景,因为某个用户会 比 出连接 有更多 入连接。使用下面的查询,你会发现在社交网络中 有趣的人会比其他人更有影响力。GraphFrames 提供一个简单地方法来查询我们图中的这些信息。

%scala
val inDeg = stationGraph.inDegrees
inDeg.orderBy(desc("inDegree")).show(5, false)

%python
inDeg = stationGraph.inDegrees
inDeg.orderBy(desc("inDegree")).show(5, False)

查询的结果 用 站的最高in-degree来排序。

+----------------------------------------+--------+
|                                     id |inDegree|
+----------------------------------------+--------+
|San Francisco Caltrain (Townsend at 4th)|  34810 |
|San Francisco Caltrain 2 (330 Townsend) |  22523 |
|   Harry Bridges Plaza (Ferry Building) |  17810 |
|                        2nd at Townsend |  15463 |
|                        Townsend at 7th |  15422 |
+----------------------------------------+--------+

通过同样的方式查询out degree。

%scala
val outDeg = stationGraph.outDegrees
outDeg.orderBy(desc("outDegree")).show(5, false)

%python
outDeg = stationGraph.outDegrees
outDeg.orderBy(desc("outDegree")).show(5, False)

>>>
+---------------------------------------------+---------+
|id |outDegree|
+---------------------------------------------+---------+
|San Francisco Caltrain (Townsend at 4th) |26304 |
|San Francisco Caltrain 2 (330 Townsend) |21758 |
|Harry Bridges Plaza (Ferry Building) |17255 |
|Temporary Transbay Terminal (Howard at Beale)|14436 |
|Embarcadero at Sansome |14158 |
+---------------------------------------------+---------+

这两个值的比 是一个有趣的度量。更高的比例说明更多旅行结束于此。

%scala
val degreeRatio = inDeg.join(outDeg, Seq("id"))
  .selectExpr("id", "double(inDegree)/double(outDegree) as degreeRatio")
degreeRatio
  .orderBy(desc("degreeRatio"))
  .show(10, false)
degreeRatio
  .orderBy("degreeRatio")
  .show(10, false)

%python
degreeRatio = inDeg.join(outDeg, "id")\
  .selectExpr("id", "double(inDegree)/double(outDegree) as degreeRatio")
degreeRatio\
  .orderBy(desc("degreeRatio"))\
  .show(10, False)
degreeRatio\
  .orderBy("degreeRatio")\
  .show(10, False)


深度优先搜索

广度优先搜索会搜索图,基于图中的边来连接两个给定点。在本例中,我们想要找到去不同的点的最短路径。我们可以通过maxPathLength指定后继的最大边数,也可以指定edgeFileter来过滤掉不符合特定要求的边,如在非工作时间出行。
我们选择两个靠近的两个车站。然而 在你有一些有长距离连接的稀疏点你可以做一些相当有趣的图遍历。

%scala
val bfsResult = stationGraph.bfs
  .fromExpr("id = ‘Townsend at 7th’")
  .toExpr("id = ‘Spear at Folsom’")
  .maxPathLength(2)
  .run()
bfsResult.show(10)

%python
bfsResult = stationGraph.bfs(
fromExpr="id = ‘Townsend at 7th’",
toExpr="id = ‘Spear at Folsom’",
maxPathLength=2)
bfsResult.show(10)

>>>
+--------------------+--------------------+--------------------+
|                from|                  e0|                  to|
+--------------------+--------------------+--------------------+
|[65,Townsend at 7...|[913371,663,8/31/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[913265,658,8/31/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[911919,722,8/31/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[910777,704,8/29/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[908994,1115,8/27...|[49,Spear at Fols...|
|[65,Townsend at 7...|[906912,892,8/26/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[905201,980,8/25/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[904010,969,8/25/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[903375,850,8/24/...|[49,Spear at Fols...|
|[65,Townsend at 7...|[899944,910,8/21/...|[49,Spear at Fols...|
+--------------------+--------------------+--------------------+
连通区域

一个连通区域(connected component)定义一个内部连接但没有连接到更大的图的图。

【Spark指南】- 图分析_第3张图片

连通区域不直接涉及我们现在的问题,因为我们的图是一个有向图。但我们仍可以运行算法,假定我们的边是无向的。实际上如果我们查看自行车分享地图,我们假定我们会得到两个不同的连通区域。

注意:为了运行这个算法,你需要设置一个检查点目录,在每次迭代时存储作业状态。这允许你从 由于作业崩溃导致的中止处 继续执行。这么做的原因是这可能是最密集的算法之一,所以预计这需要一些时间来完成,且存在潜在的不稳定性。这在Databricks社区版中大约运行三分钟。

为了在本地运行算法,一件你可能必须要做的事情 是取一个数据的样本,就像前面我们做的那样。这会帮助你得到一个结果,而不会有Spark应用程序崩溃及垃圾回收的问题。我们也会清除缓存为了确保足够的内存来运行计算。

%scala
spark.sqlContext.clearCache()
spark.sparkContext.setCheckpointDir("/tmp/checkpoints")

%python
spark.sparkContext.setCheckpointDir("/tmp/checkpoints")

%scala
val minGraph = GraphFrame(stationVertices, tripEdges.sample(false, 0.1))
val cc = minGraph.connectedComponents.run()

%python
minGraph = GraphFrame(stationVertices, tripEdges.sample(False, 0.2))
cc = minGraph.connectedComponents()

同样的我们得到两个连通区域,这似乎有点奇怪。我们得到这样结果的原因可能是进一步分析的机会。一个可能的想法是一个自行车租户的朋友开车去接他们,把他们送到一个遥远的车站,从而连接了两个原本不会连接在一起的站点集。我们的样本数据也可能没有包含所有的正确数据和信息,我们可能需要更多的计算资源来进一步研究。

%scala
cc.where("component != 0").show()

%python
cc.where("component != 0").show()

>>>
+----------+--------------------+---------+-----------+---------+-------------+------------+------------+
|station_id|                  id|      lat|       long|dockcount|     landmark|installation|   component|
+----------+--------------------+---------+-----------+---------+-------------+------------+------------+
|        47|     Post at Kearney|37.788975|-122.403452|       19|San Francisco|   8/19/2013|317827579904|
|        46|Washington at Kea...|37.795425|-122.404767|       15|San Francisco|   8/19/2013| 17179869184|
+----------+--------------------+---------+-----------+---------+-------------+------------+------------+
强连通区域

GraphFrame还包含该算法的另一个版本,与有向图有关,称为强连通分量。其做类似寻找连通分量的任务,但会考虑方向性。
一个强连通分量 有一条路径进入子图,但没有路径从子图出来。

这在Databricks社区版上大概运行三秒。

%scala
val scc = minGraph
  .stronglyConnectedComponents
  .maxIter(3)
  .run()

%python
scc = minGraph.stronglyConnectedComponents(maxIter=3)
scc.groupBy("component").count().show()


Motif Finding

Motif 是 表示 图中结构模式的 方式。当我们指定一个motif,我们会查询数据中的模式 而不是实际的数据。我们现在的数据集不适用这种查询,因为我们的图由各个体旅程组成,某些个体或标识符之间没有重复的交互。
在GraphFrames中,我们用Domain-Specific语言来指定查询。我们指定 点和边的组合,比如,如果我们向指定给定的点连接到另外一个点,我们会指定 (a)-[ab]->(b) 。 圆括号或方括号内的字母不表示值,而代表 在结果DataFrame中哪些列需要被命名。如果我们不想查询结果值,我们可以忽略命名(如:(a)-[]->())。

让我们执行一个查询,说的清楚一些,让我们找到所有 有两个站点在中间的 往返旅程。我们用下面的motif来表示,使用find方法来查询我们的GraphFrame来获得模式。
(a)表示其实站,[ab]表示从(a)到另一个站(b)的边。我们在(b)(c)之间,(c)(a)重复这个过程。

%scala
val motifs = stationGraph
  .find("(a)-[ab]->(b); (b)-[bc]->(c); (c)-[ca]->(a)")

%python
motifs = stationGraph\
  .find("(a)-[ab]->(b); (b)-[bc]->(c); (c)-[ca]->(a)")

如下是一个插叙你的可视化形式:

【Spark指南】- 图分析_第4张图片

结果DataFrame中包含顶点a,b,c各自的边 及 嵌套字段。
现在我们可以像DataFrame一样查询该数据。可以通过查询来回答特定问题。对给定一个自行车,从a出发,骑到b,再骑到c,最后骑回a,最短的往返时间是多少。
下面的逻辑会将我们的时间戳解析为spark的时间戳,然后我们会进行比较,来确保是同一辆自行车,从一个站骑到另一个站,并且每段旅程的开始时间是正确的。

%scala
import org.apache.spark.sql.functions.expr
motifs// first simplify dates for comparisons
  .selectExpr("*", """
  to_timestamp(ab.`Start Date`, ‘MM/dd/yyyy HH:mm’) as abStart
  """,
  """
  to_timestamp(bc.`Start Date`, ‘MM/dd/yyyy HH:mm’) as bcStart
  """,
  """
  to_timestamp(ca.`Start Date`, ‘MM/dd/yyyy HH:mm’) as caStart
  """)
  .where("ca.`Bike #` = bc.`Bike #`") // ensure the same bike
  .where("ab.`Bike #` = bc.`Bike #`")
  .where("a.id != b.id") // ensure different stations
  .where("b.id != c.id")
  .where("abStart < bcStart") // start times are correct
  .where("bcStart < caStart")
  .orderBy(expr("cast(caStart as long) - cast(abStart as long)")) // order them all
  .selectExpr("a.id", "b.id", "c.id",
  "ab.`Start Date`", "ca.`End Date`")
  .limit(1)
  .show(false)

%python
from pyspark.sql.functions import expr
motifs\
  .selectExpr("*", """
  to_timestamp(ab.`Start Date`, ‘MM/dd/yyyy HH:mm’) as abStart
  """,
  """
  to_timestamp(bc.`Start Date`, ‘MM/dd/yyyy HH:mm’) as bcStart
  """,
  """
  to_timestamp(ca.`Start Date`, ‘MM/dd/yyyy HH:mm’) as caStart
  """)\
  .where("ca.`Bike #` = bc.`Bike #`")\
  .where("ab.`Bike #` = bc.`Bike #`")\
  .where("a.id != b.id")\
  .where("b.id != c.id")\
  .where("abStart < bcStart")\
  .where("bcStart < caStart")\
  .orderBy(expr("cast(caStart as long) - cast(abStart as long)"))\
  .selectExpr("a.id", "b.id", "c.id",
  "ab.`Start Date`", "ca.`End Date`")\
  .limit(1)\
  .show(1, False)

>>>
+---------------------------------------+---------------+----------------------------------------+---------------+---------------+
|                                    id |            id |                                     id |     Start Date|      End Date |
+---------------------------------------+---------------+----------------------------------------+---------------+---------------+
|San Francisco Caltrain 2 (330 Townsend)|Townsend at 7th|San Francisco Caltrain (Townsend at 4th)|5/19/2015 16:09|5/19/2015 16:33|
+---------------------------------------+---------------+----------------------------------------+---------------+---------------+

我们看到最快的旅程大约20分钟。


分析任务

这是GraphFrames允许你实现的一小部分功能。开发还在继续,你会不断发现新的算法和特性加入到函数库中。
一些高级特性包括 通过信息传递接口编写你自己的算法,三角形计算,在其他任务中 进行GraphFrames和GraphX的互相转换。并且在以后 很可能将GraphX加入到Spark的核心库中。

你可能感兴趣的:(【Spark指南】- 图分析)