Spark数据集相当于scala"收集"采取部分功能

时间:2017-01-25 09:43:50

标签: scala apache-spark apache-spark-dataset

常规scala集合有一个漂亮的collect方法,它允许我使用部分函数在一次传递中执行filter-map操作。在spark Dataset s上有相同的操作吗?

我喜欢它有两个原因:

  • 句法简洁
  • 它将filter-map样式操作减少到一次通过(虽然在火花中我猜测有优化可以为你发现这些东西)

这是一个展示我的意思的例子。假设我有一系列选项,我想提取并加倍定义的整数(Some中的那些整数):

val input = Seq(Some(3), None, Some(-1), None, Some(4), Some(5)) 

方法1 - collect

input.collect {
  case Some(value) => value * 2
} 
// List(6, -2, 8, 10)

collect语法非常简洁,并且一次通过。

方法2 - filter-map

input.filter(_.isDefined).map(_.get * 2)

我可以将这种模式带到火花上,因为数据集和数据框有类似的方法。

但是我不喜欢这样,因为isDefinedget似乎代码闻起来像我。隐含的假设是地图仅接收Some s。编译器无法验证这一点。在一个更大的例子中,开发人员更难发现这种假设,开发人员可能会交换过滤器并映射,例如,不会出现语法错误。

方法3 - fold*操作

input.foldRight[List[Int]](Nil) {
  case (nextOpt, acc) => nextOpt match {
    case Some(next) => next*2 :: acc
    case None => acc
  }
}

我还没有使用过足够的火花来知道折叠是否有等价物,所以这可能有点切线。

无论如何,模式匹配,折叠锅炉板和列表的重建都混杂在一起,很难阅读。

总的来说,我觉得collect语法最好,我希望火花有这样的东西。

4 个答案:

答案 0 :(得分:5)

这里的答案是不正确的,至少是当前的Spark。

事实上,RDD确实有一个收集方法,它采用部分功能并应用过滤器和放大器。映射到数据。这与无参数.collect()方法完全不同。请参阅Spark源代码RDD.scala @ line 955:

/**
 * Return an RDD that contains all matching values by applying `f`.
 */
def collect[U: ClassTag](f: PartialFunction[T, U]): RDD[U] = withScope {
  val cleanF = sc.clean(f)
  filter(cleanF.isDefinedAt).map(cleanF)
}

这不会实现RDD中的数据,而不是RDD.scala @ line 923中的无参数.collect()方法:

/**
 * Return an array that contains all of the elements in this RDD.
 */
def collect(): Array[T] = withScope {
  val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
  Array.concat(results: _*)
}

在文档中,请注意

def collect[U](f: PartialFunction[T, U]): RDD[U]

方法有关于加载到驱动程序内存中的数据的警告:

https://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.rdd.RDD@collect[U](f:PartialFunction[T,U])(implicitevidence$29:scala.reflect.ClassTag[U]):org.apache.spark.rdd.RDD[U]

Spark让这些重载方法做了完全不同的事情,这让人非常困惑。

编辑:我的错!我误解了这个问题,我们谈论的是DataSets而不是RDD。但是,接受的答案仍然是

  但是,“Spark文档指出,”只有在期望结果数组很小的情况下才能使用此方法,因为所有数据都被加载到驱动程序的内存中。“

哪个不对!调用.collect()的部分功能版本时,数据不会加载到驱动程序的内存中 - 仅在调用无参数版本时。调用.collect(partial_function)应该具有与按顺序调用.filter()和.map()相同的性能,如上面的源代码所示。

答案 1 :(得分:3)

为了完整起见:

RDD API 具有这样的方法,因此它总是可以选择将给定的数据集/数据帧转换为RDD,执行collect操作并转换回来,例如:

val dataset = Seq(Some(1), None, Some(2)).toDS()
val dsResult = dataset.rdd.collect { case Some(i) => i * 2 }.toDS()

然而,这可能会比使用数据集上的地图和过滤器更糟糕(出于@ stefanobaghino回答的原因)。

对于DataFrames,这个特定的例子(使用Option)有点误导,因为转换为DataFrame实际上会进行" flatenning"将选项纳入其值(或nullNone),因此等效表达式为:

val dataframe = Seq(Some(1), None, Some(2)).toDF("opt")
dataframe.withColumn("opt", $"opt".multiply(2)).filter(not(isnull($"opt")))

我认为,对您进行地图操作的担忧减少了很多,而且假设"关于它的输入的任何事情。

答案 2 :(得分:2)

$('showdiv').append('<grid :options="options"></grid>') var grid = window.$grid var gridvue = new Vue({ el: '#showdiv', data: { options: options }, components: {grid} }) collect上定义的RDD方法用于实现驱动程序中的数据。

尽管没有类似于Collections API Dataset方法的东西,但你的直觉是对的:由于这两个操作都是懒惰的,因此引擎有机会优化操作并将它们链接起来,以便最大限度地执行它们局部性。

对于您特别提到的用例,我建议您考虑collect,这适用于flatMapRDD

Dataset

修改

正如在另一个问题中正确指出的那样,// Assumes the usual spark-shell environment // sc: SparkContext, spark: SparkSession val collection = Seq(Some(1), None, Some(2), None, Some(3)) val rdd = sc.parallelize(collection) val dataset = spark.createDataset(rdd) // Both operations will yield `Array(2, 4, 6)` rdd.flatMap(_.map(_ * 2)).collect dataset.flatMap(_.map(_ * 2)).collect // You can also express the operation in terms of a for-comprehension (for (option <- rdd; n <- option) yield n * 2).collect (for (option <- dataset; n <- option) yield n * 2).collect // The same approach is valid for traditional collections as well collection.flatMap(_.map(_ * 2)) for (option <- collection; n <- option) yield n * 2 实际上有RDD方法通过应用部分函数来转换collect,就像在普通集合中一样。然而,正如Spark documentation指出的那样,“只有在期望结果数组很小的情况下才能使用此方法,因为所有数据都被加载到驱动程序的内存中。”

答案 3 :(得分:1)

我只想通过在案例类中加入for理解的例子来扩展stefanobaghino的答案,因为很多用例可能涉及案例类。

另外选项是monad,这使得接受的答案在这种情况下非常简单,因为for整齐地删除了None值,但是这种方法不会扩展到像案例类这样的非monad:

case class A(b: Boolean, i: Int, d: Double)

val collection = Seq(A(true, 3), A(false, 10), A(true, -1))
val rdd = ...
val dataset = ...

// Select out and double all the 'i' values where 'b' is true:
for {
  A(b, i, _) <- dataset
  if b
} yield i * 2