[Spark的TopN算法实现]

11 篇文章 0 订阅
10 篇文章 0 订阅

一、TopN算法

    MapReduce中的TopN算法是一个经典的算法,由于每个map都只是实现了本地的TopN算法,而假设map有M个,在归约的阶段只有M x N次运算,这个结果是可以接受的并不会造成性能瓶颈。

  MapReduce中的TopN算法在map阶段将使用TreeMap来实现排序,以到达可伸缩的目的。

      本文将介绍三种TopN的算法:

       1.唯一键的TopN算法,就是Key的值是唯一的;

       2.非唯一键的TopN算法,比如key值可能会有A、B、C三种,然后分别对他们求TopN;

       3.分组中TopN的算法,就是同一分组Key中取出TopN的数据。

二、TopN算法流程图


三.代码实现

3.1 唯一键的TopN算法

输入数据

AAA 23
BBB 20
CCC 22
DDD 35
EEE 78
FFF 73
GGG 89

代码实现

/**
  * Spark-Rdd唯一键的TopN的实现
  **/
object SparkUniqueTopN {
    def main(args: Array[String]): Unit = {
        //TopN的值,参数传递
        val num: Int = args(0).toInt
        //读取文件路径
        val path: String = args(1)
        val config: SparkConf = new SparkConf().setMaster("local").setAppName("SparkUniqueTopN")
        //构建Spark上下文
        val sparkContext: SparkContext = SparkSession.builder().config(config).getOrCreate().sparkContext
        //广播变量
        val topN: Broadcast[Int] = sparkContext.broadcast(num)

        val rdd: RDD[String] = sparkContext.textFile(path)
        val pairRdd: RDD[(Int, Array[String])] = rdd.map(line => {
            val tokens: Array[String] = line.split(" ")
            (tokens(1).toInt, tokens)
        })

        val partitions: RDD[(Int, Array[String])] = pairRdd.mapPartitions(iterator => {
            var sortedMap = SortedMap.empty[Int, Array[String]]
            iterator.foreach({ tuple => {
                sortedMap += tuple
                if (sortedMap.size > topN.value) {
                    sortedMap = sortedMap.takeRight(topN.value)
                }
            }
            })
            sortedMap.takeRight(topN.value).toIterator
        })

        val alltopN: Array[(Int, Array[String])] = partitions.collect()
        val finaltopN: SortedMap[Int, Array[String]] = SortedMap.empty[Int, Array[String]].++:(alltopN)
        val resultUsingMapPartition: SortedMap[Int, Array[String]] = finaltopN.takeRight(topN.value)

        println("+---+---+ TopN的结果1:")
        resultUsingMapPartition.foreach {
            case (k, v) => println(s"$k \t ${v.asInstanceOf[Array[String]].mkString(",")}")
        }
        val moreConciseApproach: Array[(Int, Iterable[Array[String]])] = pairRdd.groupByKey().sortByKey(ascending = false).take(topN.value)
        println("+---+---+ TopN的结果2:")
        moreConciseApproach.foreach {
            case (k, v) => println(s"$k \t ${v.flatten.mkString(",")}")
        }
    }
}

输出结果

+---+---+ TopN的结果1:
73 	 FFF,73
78 	 EEE,78
89 	 GGG,89

+---+---+ TopN的结果2:
89 	 GGG,89
78 	 EEE,78
73 	 FFF,73

3.2 非唯一键的TopN算法

输入数据

AAA 23
BBB 20
CCC 22
DDD 35
EEE 78
FFF 73
GGG 89
AAA 45
BBB 76
CCC 54
DDD 76
EEE 75
FFF 53
GGG 43

代码实现

/**
  * Spark-Rdd非唯一键的TopN的实现
  **/
object SparkNonUniqueTopN {
    def main(args: Array[String]): Unit = {
        //TopN的值,参数传递
        val num: Int = args(0).toInt
        //读取文件路径
        val path: String = args(1)
        val config: SparkConf = new SparkConf().setMaster("local").setAppName("SparkNonUniqueTopN")
        //构建Spark上下文
        val sparkContext: SparkContext = SparkSession.builder().config(config).getOrCreate().sparkContext
        //广播变量
        val topN: Broadcast[Int] = sparkContext.broadcast(num)

        val rdd: RDD[String] = sparkContext.textFile(path)
        val kv: RDD[(String, Int)] = rdd.map(line => {
            val tokens = line.split(" ")
            (tokens(0), tokens(1).toInt)
        })
        //将非唯一键转换为唯一键
        val uniqueKeys: RDD[(String, Int)] = kv.reduceByKey(_ + _)
        val partitions: RDD[(Int, String)] = uniqueKeys.mapPartitions(itr => {
            var sortedMap = SortedMap.empty[Int, String]
            itr.foreach { tuple => {
                sortedMap += tuple.swap
                if (sortedMap.size > topN.value) {
                    sortedMap = sortedMap.takeRight(topN.value)
                }
            }
            }
            sortedMap.takeRight(topN.value).toIterator
        })

        val alltopN = partitions.collect()
        val finaltopN = SortedMap.empty[Int, String].++:(alltopN)
        val resultUsingMapPartition = finaltopN.takeRight(topN.value)
        //打印结果
        println("+---+---+ TopN的结果1:")
        resultUsingMapPartition.foreach {
            case (k, v) => println(s"$k \t ${v.mkString(",")}")
        }

        val createCombiner: Int => Int = (v: Int) => v
        val mergeValue: (Int, Int) => Int = (a: Int, b: Int) => a + b
        val moreConciseApproach: Array[(Int, Iterable[String])] =
            kv.combineByKey(createCombiner, mergeValue, mergeValue)
                .map(_.swap)
                .groupByKey()
                .sortByKey(ascending = false)
                .take(topN.value)

        //打印结果
        println("+---+---+ TopN的结果2:")
        moreConciseApproach.foreach {
            case (k, v) => println(s"$k \t ${v.mkString(",")}")
        }
    }
}

输出结果

+---+---+ TopN的结果1:
126 	 FFF
132 	 GGG
153 	 EEE

+---+---+ TopN的结果2:
153 	 EEE
132 	 GGG
126 	 FFF

3.3 Group的TopN算法

输入数据

spark 78
sql 98
java 80
javascript 98
scala 69
hadoop 87
hbase 97
hive 86

spark 89
sql 65
java 45
javascript 76
scala 34
hadoop 87
hbase 43
hive 76

spark 65
sql 53
java 65
javascript 54
scala 98
hadoop 32
hbase 76
hive 87

spark 38
sql 42
java 97
javascript 34
scala 76
hadoop 90
hbase 55
hive 88

spark 35
sql 76
java 98
javascript 34
scala 65
hadoop 76
hbase 67
hive 56

代码实现

/**
  * Spark-Rdd计算分组数据中TopN的实现
  **/
object SparkGroupTopN {
    def main(args: Array[String]): Unit = {
        val num = args(0).toInt
        //读取文件路径
        val path: String = args(1)
        //Spark配置
        val config: SparkConf = new SparkConf().setMaster("local").setAppName("SparkGroupTopN")
        //创建上下文
        val sparkConext: SparkContext = SparkSession.builder().config(config).getOrCreate().sparkContext
        //读取文件数据形成RDD
        val rdd: RDD[String] = sparkConext.textFile(path)
        //过滤空行,将rdd转换为键值对PairRDD
        val mapredRDD: RDD[(String, Int)] = rdd.filter(line => line.length > 0)
            .map(line => line.split(" "))
            .map(arr => (arr(0).trim, arr(1).trim.toInt))
        //缓存RDD,方便后期的使用
        mapredRDD.cache()
        val topN: Broadcast[Int] = sparkConext.broadcast(num)
        //1、使用groupByKey的方式实现读取TopN的数据
        //缺点:
        //(1)使用groupByKey,在相同的key所对应的数据形成的迭代器在处理过程中的全部数据会加在到内存中,
        //   如果一个key的数据特别多的情况下,就会很容易出现内存溢出(OOM)
        //(2)在同组key中进行数据聚合并汇总,groupByKey的性能不是很高的,因为没有事先对分区数据进行一个临时聚合运算
        val topNResult1: RDD[(String, Seq[Int])] = mapredRDD.groupByKey().map(tuple2 => {
            //获取values里面的topN
            val topn = tuple2._2.toList.sorted.takeRight(topN.value).reverse
            (tuple2._1, topn)
        })

        println("+---+---+ 使用groupByKey获取TopN的结果:")
        println(topNResult1.collect().mkString("\n"))

        //2.使用两阶段聚合,先使用随机数进行分组聚合取出局部topn,再聚合取出全局topN的数据
        val topNResult2: RDD[(String, List[Int])] = mapredRDD.mapPartitions(iterator => {
            iterator.map(tuple2 => {
                ((Random.nextInt(10), tuple2._1), tuple2._2)
            })
        }).groupByKey().flatMap({
            //获取values中的前N个值 ,并返回topN的集合数据
            case ((_, key), values) =>
                values.toList.sorted.takeRight(topN.value).map(value => (key, value))
        }).groupByKey().map(tuple2 => {
            val topn = tuple2._2.toList.sorted.takeRight(topN.value).reverse
            (tuple2._1, topn)
        })
        println("+---+---+ 使用两阶段集合获取TopN的结果:")
        println(topNResult2.collect().mkString("\n"))

        //3、使用aggregateByKey获取topN的记录
        val topNResult3: RDD[(String, List[Int])] = mapredRDD.aggregateByKey(ArrayBuffer[Int]())(
            (u, v) => {
                u += v
                u.sorted.takeRight(topN.value)
            },
            (u1, u2) => {
                //对任意的两个局部聚合值进行聚合操作,可以会发生在combiner阶段和shuffle之后的最终的数据聚合的阶段
                u1 ++= u2
                u1.sorted.takeRight(topN.value)
            }
        ).map(tuple2 => (tuple2._1, tuple2._2.toList.reverse))

        println("+---+---+ 使用aggregateByKey获取TopN的结果:")
        println(topNResult3.collect().mkString("\n"))
    }
}

输出结果

+---+---+ 使用groupByKey获取TopN的结果:
(scala,List(98, 76, 69))
(spark,List(89, 78, 65))
(hive,List(88, 87, 86))
(hadoop,List(90, 87, 87))
(java,List(98, 97, 80))
(javascript,List(98, 76, 54))
(sql,List(98, 76, 65))
(hbase,List(97, 76, 67))

+---+---+ 使用两阶段集合获取TopN的结果:
(scala,List(98, 76, 69))
(spark,List(89, 78, 65))
(hive,List(88, 87, 86))
(hadoop,List(90, 87, 87))
(java,List(98, 97, 80))
(javascript,List(98, 76, 54))
(sql,List(98, 76, 65))
(hbase,List(97, 76, 67))

+---+---+ 使用aggregateByKey获取TopN的结果:
(scala,List(98, 76, 69))
(spark,List(89, 78, 65))
(hive,List(88, 87, 86))
(hadoop,List(90, 87, 87))
(java,List(98, 97, 80))
(javascript,List(98, 76, 54))
(sql,List(98, 76, 65))
(hbase,List(97, 76, 67))
以上是Spark中几种常用的TopN的算法,在日常的工作中我们会经常使用。


  • 23
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: Spark RDD中分组取TopN案例是指在一个RDD中,根据某个键值进行分组,然后对每个组内的数据进行排序,取出每个组内的前N个数据。这种操作在数据分析和处理中非常常见,可以用于统计每个地区的销售额排名前N的产品、每个用户的消费排名前N的商品等。 优化方面,可以考虑使用Spark SQL或DataFrame来实现分组取TopN操作,因为它们提供了更高级的API和优化技术,可以更快速地处理大规模数据。另外,可以使用分布式缓存技术将数据缓存到内存中,以加快数据访问速度。还可以使用分区和并行计算等技术来提高计算效率。 ### 回答2: Spark RDD中分组取Top N的案例可以是对一个大数据集中的用户数据进行分组,然后取每个组中消费金额最高的前N个用户。这个案例可以通过以下步骤来实现: 1. 将用户数据载入Spark RDD中,每个数据记录包含用户ID和消费金额。 2. 使用groupBy函数将RDD按照用户ID进行分组,得到一个以用户ID为key,包含相同用户ID的数据记录的value的RDD。 3. 对每个分组的value调用top函数,指定N的值,以获取每个分组中消费金额最高的前N个用户。 4. 可以将每个分组中Top N的用户使用flatMap函数展开为多个记录,并可以添加一个新的字段表示该记录属于哪个分组。 5. 最后,可以使用collect函数将结果转化为数组或者保存到文件或数据库中。 在这个案例中,进行优化的关键是减少数据的传输和处理开销。可以使用缓存或持久化函数对RDD进行优化,以减少重复计算。另外,可以使用并行操作来加速计算,如使用并行的排序算法,或向集群中的多个节点分发计算任务。 对于分组取Top N的优化,还可以考虑使用局部聚合和全局聚合的策略。首先对每个分组内的数据进行局部聚合,例如计算每个分组的前M个最大值。然后,对所有分组的局部聚合结果进行全局聚合,例如计算所有分组的前K个最大值。 另一个优化策略是使用采样技术,例如随机采样或分层采样,以减少需要处理的数据量。 最后,还可以考虑使用Spark的其他高级功能,如Broadcast变量共享数据,使用累加器进行计数或统计等,来进一步提高性能和效率。 ### 回答3: Spark RDD 是 Spark 提供的一种基于内存的分布式数据处理模型,其核心数据结构是弹性分布式数据集(RDD)。 在 Spark RDD 中,分组取TopN 是一种常见的需求,即对 RDD 中的数据按某个字段进行分组,并取出每个分组中字段值最大的前 N 个数据。 下面以一个示例来说明分组取 TopN 的用法和优化方法: 假设有一个包含学生信息的 RDD,其中每条数据都包括学生的学科和分数,我们希望对每个学科取出分数最高的前 3 名学生。 ```python # 创建示例数据 data = [ ("语文", 80), ("数学", 90), ("语文", 85), ("数学", 95), ("语文", 75), ("数学", 92), ("英语", 88) ] rdd = sc.parallelize(data) # 分组取TopN top3 = rdd.groupByKey().mapValues(lambda x: sorted(x, reverse=True)[:3]) # 输出结果 for subject, scores in top3.collect(): print(subject, scores) # 输出结果: # 数学 [95, 92, 90] # 语文 [85, 80, 75] # 英语 [88] ``` 在上述代码中,我们先使用 `groupByKey()` 对 RDD 进行分组操作,然后使用 `mapValues()` 对每个分组内的数据进行排序并取前 3 个值。 这种方式的优化点在于,通过将分组操作和取 TopN 操作分开,可以减轻数据倾斜的问题。同时,对每个分组进行排序会占用大量计算资源,可以考虑将数据转换为 Pair RDD,并利用 Spark 提供的 `top()` 算子来优化取 TopN 的操作。 ```python # 转换为 Pair RDD pair_rdd = rdd.map(lambda x: (x[0], x[1])) # 分组并取TopN,使用top()算子代替排序操作 top3 = pair_rdd.groupByKey().mapValues(lambda x: sorted(x, reverse=True)).mapValues(lambda x: x[:3]) # 输出结果 for subject, scores in top3.collect(): print(subject, scores) # 输出结果: # 数学 [95, 92, 90] # 语文 [85, 80, 75] # 英语 [88] ``` 通过以上优化,我们可以更好地处理大规模数据集下的分组取 TopN 的需求,提高计算性能和资源利用率。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值