Spark和SparkSQL:如何模仿窗口功能?

时间:2015-09-04 22:29:48

标签: scala apache-spark apache-spark-sql window-functions

描述

给定数据框df

id |       date
---------------
 1 | 2015-09-01
 2 | 2015-09-01
 1 | 2015-09-03
 1 | 2015-09-04
 2 | 2015-09-04

我想创建一个运行的计数器或索引,

  • 按相同的ID和
  • 分组
  • 按该组中的日期排序,

从而

id |       date |  counter
--------------------------
 1 | 2015-09-01 |        1
 1 | 2015-09-03 |        2
 1 | 2015-09-04 |        3
 2 | 2015-09-01 |        1
 2 | 2015-09-04 |        2

这是我可以通过窗口功能实现的,例如

val w = Window.partitionBy("id").orderBy("date")
val resultDF = df.select( df("id"), rowNumber().over(w) )

不幸的是,Spark 1.4.1不支持常规数据帧的窗口函数:

org.apache.spark.sql.AnalysisException: Could not resolve window function 'row_number'. Note that, using window functions currently requires a HiveContext;

问题

  • 如何在不使用窗口函数的情况下在当前Spark 1.4.1上实现上述计算?
  • 什么时候支持常规数据帧的窗口函数?

谢谢!

3 个答案:

答案 0 :(得分:7)

您也可以将HiveContext用于本地DataFrames,除非您有充分理由不这样做,否则无论如何都可能是个好主意。它是SQLContextspark-shell shell中可用的默认pyspark(现在sparkR似乎使用普通SQLContext),{import org.apache.spark.{SparkContext, SparkConf} import org.apache.spark.sql.hive.HiveContext import org.apache.spark.sql.expressions.Window import org.apache.spark.sql.functions.rowNumber object HiveContextTest { def main(args: Array[String]) { val conf = new SparkConf().setAppName("Hive Context") val sc = new SparkContext(conf) val sqlContext = new HiveContext(sc) import sqlContext.implicits._ val df = sc.parallelize( ("foo", 1) :: ("foo", 2) :: ("bar", 1) :: ("bar", 2) :: Nil ).toDF("k", "v") val w = Window.partitionBy($"k").orderBy($"v") df.select($"k", $"v", rowNumber.over(w).alias("rn")).show } } 推荐使用其解析器3}}

{{1}}

答案 1 :(得分:6)

您可以使用RDD执行此操作。就个人而言,我发现RDD的API更有意义 - 我并不总是希望我的数据像数据帧一样“平坦”。

val df = sqlContext.sql("select 1, '2015-09-01'"
    ).unionAll(sqlContext.sql("select 2, '2015-09-01'")
    ).unionAll(sqlContext.sql("select 1, '2015-09-03'")
    ).unionAll(sqlContext.sql("select 1, '2015-09-04'")
    ).unionAll(sqlContext.sql("select 2, '2015-09-04'"))

// dataframe as an RDD (of Row objects)
df.rdd 
  // grouping by the first column of the row
  .groupBy(r => r(0)) 
  // map each group - an Iterable[Row] - to a list and sort by the second column
  .map(g => g._2.toList.sortBy(row => row(1).toString))     
  .collect()

以上结果如下:

Array[List[org.apache.spark.sql.Row]] = 
Array(
  List([1,2015-09-01], [1,2015-09-03], [1,2015-09-04]), 
  List([2,2015-09-01], [2,2015-09-04]))

如果您想要“组”中的排名,也可以使用zipWithIndex

df.rdd.groupBy(r => r(0)).map(g => 
    g._2.toList.sortBy(row => row(1).toString).zipWithIndex).collect()

Array[List[(org.apache.spark.sql.Row, Int)]] = Array(
  List(([1,2015-09-01],0), ([1,2015-09-03],1), ([1,2015-09-04],2)),
  List(([2,2015-09-01],0), ([2,2015-09-04],1)))

可以使用FlatMap将其扁平化为一个简单的Row对象列表/数组,但是如果你需要在'group'上执行任何不会很好的事情想法。

使用像这样的RDD的缺点是,从DataFrame转换为RDD并再次返回是很繁琐的。

答案 2 :(得分:3)

我完全同意,如果你有Spark版本(> =)1.5,那么DataFrames的Window函数就是你的选择。但如果你真的陷入旧版本(例如1.4.1),这是一个解决这个问题的黑客方法

val df = sc.parallelize((1, "2015-09-01") :: (2, "2015-09-01") :: (1, "2015-09-03") :: (1, "2015-09-04") :: (1, "2015-09-04") :: Nil)
           .toDF("id", "date")

val dfDuplicate = df.selecExpr("id as idDup", "date as dateDup")
val dfWithCounter = df.join(dfDuplicate,$"id"===$"idDup")
                      .where($"date"<=$"dateDup")
                      .groupBy($"id", $"date")
                      .agg($"id", $"date", count($"idDup").as("counter"))
                      .select($"id",$"date",$"counter")

现在,如果你dfWithCounter.show

你会得到:

+---+----------+-------+                                                        
| id|      date|counter|
+---+----------+-------+
|  1|2015-09-01|      1|
|  1|2015-09-04|      3|
|  1|2015-09-03|      2|
|  2|2015-09-01|      1|
|  2|2015-09-04|      2|
+---+----------+-------+

请注意,date未排序,但counter是正确的。您还可以通过将counter语句中的<=更改为>=来更改where的排序。