<Zhuuu_ZZ>Spark项目实战-航班飞行网图分析

一 项目技能

  • Spark GraphX API
    • vertices、edges、triplets、
    • numEdges、numVertices
    • inDegrees、outDegrees、degrees
    • mapVertices、mapEdges、mapTriplets
  • Spark GraphX PageRank
  • Spark GraphX Pregel

二 项目需求

  • 探索航班飞行网图数据
  • 构建航班飞行网图
  • 使用Spark GraphX完成下列任务
    • 统计航班飞行网图中机场的数量
    • 统计航班飞行网图中航线的数量
    • 计算最长的飞行航线(Point to Point)
    • 找出最繁忙的机场
    • 找出最重要的飞行航线(PageRank)
    • 找出最便宜的飞行航线(SSSP)

三 数据探索

下载数据

链接: 航班飞行网图数据.提取码:gvyd

数据格式

  • 文件格式为CSV,字段之间分隔符为“,”
  • 依次为:#日、周#、航空公司、飞机注册号、航班号、起飞机场编号、起飞机场、到达机场编号、到达机场、预计起飞时间(时分)、起飞时间、起飞延迟(分钟)、到达预计时间、到达时间、到达延迟(分钟)、预计飞行时间、飞行距离
    在这里插入图片描述

四 项目实战

构建航班飞行网图

  • 创建属性图Graph[VD,ED]
    • 装载CSV为RDD,每个机场作为顶点。关键字段:起飞机场编号、起飞机场、到达机场编号、到达机场、飞行距离
    • 初始化顶点集airports:RDD[(VertexId,String)],顶点属性为机场名称
    • 初始化边集lines:RDD[Edge],边属性为飞行距离
  val flight: RDD[Array[String]] = sc.textFile("in/project/fly.csv").repartition(1).map(_.split(","))
    //flatMap的返回值需要是TraversableOnce,即可反复迭代的,如数组集合等
    //一行数据进来会取下标5,6做一个元素,再取下标7,8做另外一个元素,然后所有元素返回进入一个集合中使维度相同
    //flatMap是扁平化函数,如“hello world”,“hello spark”进入.flatMap(_.split(","))会是hello,world,hello,spark而进入map(_.split(","))则是Array(hello, world), Array(hello,spark)
    //也就是flatMap会切割成一个个独立的元素,并把这些元素放入一个集合中使之成为一个维度。
 val vertex: RDD[(VertexId, String)] = flight.flatMap(x=>Array((x(5).toLong,x(6)),(x(7).toLong,x(8)))).distinct()
 val lines: RDD[Edge[PartitionID]] = flight.map(x=>(x(5).toLong,x(7).toLong,x(16).toInt)).distinct().map(x=>Edge(x._1,x._2,x._3))

 val graph: Graph[String, PartitionID] = Graph(vertex,lines)

统计航班飞行网图中机场与航线的数量

  • 机场数量
  • 航线数量
println("机场数量:"+graph.numVertices)
println("航线数量:"+graph.numEdges)

计算最长的飞行航线

  • 最大的边属性
    • 对triplets按飞行距离排序(降序)并取第一个
graph.triplets.sortBy(x => x.attr * (-1)).take(2).foreach(x=>println("最长的航线:"+x))

找出最繁忙的机场

-哪个机场到达航班最多

  • 计算顶点的入度并排序
graph.degrees.sortBy(x=>x._2,false).take(1).foreach(println)

找出最重要的飞行航线

  • PageRank
    • 收敛误差:0.05
graph.pageRank(0.05).vertices.sortBy(-_._2).take(1).foreach(println)
//等价
 graph.pageRank(0.05).vertices.takeOrdered(1)(Ordering.by(-_._2)).foreach(println)

找出最便宜的飞行航线

  • 定价模型
    • price = 180.0 + distance * 0.15
  • SSSP问题
    • 从初始指定的源点到达任意点的最短距离
  • pregel
    • 初始化源点(0)与其它顶点(Double.PositiveInfinity)
  • 初始消息(Double.PositiveInfinity)
  • vprog函数计算最小值
  • sendMsg函数计算进行是否下一个迭代
  • mergeMsg函数合并接受的消息,取最小值
   //定义图中起始顶点id
    val srcVertexId=12478L
    //修改边属性,使之成为价格
    //修改顶点属性,起始顶点为0,其余全部为正无穷大
    val initialGraph=graph.mapEdges(e=>180.0+e.attr*0.15)
    .mapVertices((id,prop)=>{
      if(id==srcVertexId)
        0
      else
        Double.PositiveInfinity
    })
    //调用pregel
  val pregelGraph: Graph[Double, Double] = initialGraph.pregel(
      Double.PositiveInfinity,
      Int.MaxValue,
      EdgeDirection.Out
    )(
      //接收消息函数:接收下面sendMsg的信息
      (vid: VertexId, vd: Double, distMsg: Double) => {
        //返回相同VertexId的情况下,发送顶点的属性加上边属性和与目标顶点属性的最小值
        val minDist: Double = math.min(vd, distMsg)
       // println(s"顶点${vid},属性${vd},收到消息${distMsg},合并后的属性${minDist}")
        minDist
      },
      //发送消息函数:先发送后接受,所以先执行这一步
      (edgeTriplet: EdgeTriplet[Double, Double]) => {
        if (edgeTriplet.srcAttr + edgeTriplet.attr < edgeTriplet.dstAttr) { //如果发送顶点的属性加上边属性小于目标顶点属性
        //  println(s"顶点${edgeTriplet.srcId} 给 顶点${edgeTriplet.dstId} 发送消息 ${edgeTriplet.srcAttr + edgeTriplet.attr}")
          //则返回一个(目标顶点id,发送顶点属性加上边属性)
          Iterator[(VertexId, Double)]((edgeTriplet.dstId, edgeTriplet.srcAttr + edgeTriplet.attr))
        } else { //否则返回空,即消息发送失败
          Iterator.empty
        }
      },
      //合并消息函数:指有两个及以上的激活态顶点给同一个顶点发送消息,且都发送成功,则执行完sendMsg后调用mergeMsg再执行vprog
      (msg1: Double, msg2: Double) =>{
       // println("mergeMsg:",msg1,msg2) 
        math.min(msg1, msg2) //返回各自激活态顶点属性加上各自边的属性之和的最小值进入vprog函数
      }
    )
    pregelGraph.vertices.sortBy(_._2).take(3).foreach(println)
 

/*
(12478,0.0)
(10821,207.6)
(10721,208.05)
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,让我们先来了解一下什么是拓扑排序和关键路径。 拓扑排序是对有向无环(DAG)进行排序的一种方法。它可以将一个DAG的顶点排成一条线性序列,使得对于任何一条有向边 (u, v),顶点 u 在序列中都排在顶点 v 的前面。 关键路径是指在一个有向无环中,从起点到终点的所有路径中,耗时最长的那条路径。在实际应用中,关键路径可以用来确定项目的最短工期,以及哪些任务是关键任务,不能延误。 下面是用C语言实现拓扑排序和关键路径的代码,注释中有详细的解释。 ```c #include <stdio.h> #include <stdlib.h> #define MAX_VERTEX_NUM 100 // 中最大顶点数 #define MAX_EDGE_NUM 100 // 中最大边数 // 边的结构体,包含起点和终点 typedef struct { int from; // 起点 int to; // 终点 } Edge; // 顶点的结构体,包含入度和出度 typedef struct { int in; // 入度 int out; // 出度 } Vertex; // 的结构体,包含顶点数组、边数组、顶点数和边数 typedef struct { Vertex vertices[MAX_VERTEX_NUM]; // 顶点数组 Edge edges[MAX_EDGE_NUM]; // 边数组 int vertex_num; // 顶点数 int edge_num; // 边数 } Graph; // 初始化 void init_graph(Graph *g) { int i; g->vertex_num = 0; g->edge_num = 0; for (i = 0; i < MAX_VERTEX_NUM; i++) { g->vertices[i].in = 0; g->vertices[i].out = 0; } } // 添加边 void add_edge(Graph *g, int from, int to) { g->edges[g->edge_num].from = from; g->edges[g->edge_num].to = to; g->edge_num++; g->vertices[from].out++; // 起点出度加1 g->vertices[to].in++; // 终点入度加1 } // 拓扑排序 void topological_sort(Graph *g) { int i, j, k, n; int queue[MAX_VERTEX_NUM]; // 存储入度为0的顶点 int head = 0, tail = 0; // 队列头和尾 int count = 0; // 已排序的顶点数 Edge *e; // 将入度为0的顶点加入队列 for (i = 0; i < g->vertex_num; i++) { if (g->vertices[i].in == 0) { queue[tail++] = i; } } // 循环直到队列为空 while (head != tail) { n = tail - head; // 当前队列中的顶点数 for (i = 0; i < n; i++) { j = queue[head++]; // 取出队列头 printf("%d ", j); // 输出已排序的顶点 count++; // 已排序的顶点数加1 for (k = 0; k < g->edge_num; k++) { e = &g->edges[k]; if (e->from == j) { // 找到以j为起点的边 g->vertices[e->to].in--; // 对应终点的入度减1 if (g->vertices[e->to].in == 0) { // 如果终点入度为0,则加入队列 queue[tail++] = e->to; } } } } } if (count < g->vertex_num) { // 如果已排序的顶点数小于总顶点数,则存在环路 printf("The graph has a cycle\n"); } } // 计算关键路径 void critical_path(Graph *g) { int i, j, k, m = 0, n = 0; int earliest[MAX_VERTEX_NUM] = {0}; // 存储最早开始时间 int latest[MAX_VERTEX_NUM] = {0}; // 存储最晚开始时间 Edge *e; // 计算最早开始时间 for (i = 0; i < g->vertex_num; i++) { for (j = 0; j < g->edge_num; j++) { e = &g->edges[j]; if (e->from == i) { // 找到以i为起点的边 if (earliest[e->to] < earliest[i] + 1) { // 更新终点的最早开始时间 earliest[e->to] = earliest[i] + 1; } } } } // 计算最晚开始时间 for (i = g->vertex_num - 1; i >= 0; i--) { latest[i] = earliest[g->vertex_num - 1]; // 先初始化为总工期 for (j = 0; j < g->edge_num; j++) { e = &g->edges[j]; if (e->from == i) { // 找到以i为起点的边 if (latest[i] > latest[e->to] - 1) { // 更新起点的最晚开始时间 latest[i] = latest[e->to] - 1; } } } } printf("The critical path is: "); for (i = 0; i < g->edge_num; i++) { e = &g->edges[i]; if (earliest[e->to] - earliest[e->from] == latest[e->to] - latest[e->from]) { // 如果边的最早开始时间和最晚开始时间相等,则为关键边 printf("(%d,%d) ", e->from, e->to); if (earliest[e->to] > m) { // 找到最大的最早开始时间 m = earliest[e->to]; } if (latest[e->from] < n) { // 找到最小的最晚开始时间 n = latest[e->from]; } } } printf("\nThe minimum time to finish the project is %d\n", m - n); } int main() { Graph g; init_graph(&g); g.vertex_num = 7; // 设置顶点数 add_edge(&g, 0, 1); add_edge(&g, 0, 2); add_edge(&g, 1, 3); add_edge(&g, 1, 4); add_edge(&g, 2, 3); add_edge(&g, 2, 5); add_edge(&g, 3, 6); add_edge(&g, 4, 6); add_edge(&g, 5, 6); topological_sort(&g); critical_path(&g); return 0; } ``` 上述代码中,我们定义了一个Graph的结构体来表示,包含顶点数组、边数组、顶点数和边数。同时定义了一个Vertex的结构体来表示顶点,包含入度和出度。定义了一个Edge的结构体来表示边,包含起点和终点。然后分别实现了初始化、添加边、拓扑排序和计算关键路径的函数。 在main函数中,我们先初始化,然后添加边,设置顶点数为7。然后调用拓扑排序和计算关键路径的函数来输出结果。 这段代码可能对于数据结构初学者来说有些难度,但是只要认真看注释,理解了拓扑排序和关键路径的原理,就能够理解代码的实现过程。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值