常规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)
我可以将这种模式带到火花上,因为数据集和数据框有类似的方法。
但是我不喜欢这样,因为isDefined
和get
似乎代码闻起来像我。隐含的假设是地图仅接收Some
s。编译器无法验证这一点。在一个更大的例子中,开发人员更难发现这种假设,开发人员可能会交换过滤器并映射,例如,不会出现语法错误。
方法3 - fold*
操作
input.foldRight[List[Int]](Nil) {
case (nextOpt, acc) => nextOpt match {
case Some(next) => next*2 :: acc
case None => acc
}
}
我还没有使用过足够的火花来知道折叠是否有等价物,所以这可能有点切线。
无论如何,模式匹配,折叠锅炉板和列表的重建都混杂在一起,很难阅读。
总的来说,我觉得collect
语法最好,我希望火花有这样的东西。
答案 0 :(得分:5)
这里的答案是不正确的,至少是当前的Spark。
/**
* 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]
方法不有关于加载到驱动程序内存中的数据的警告:
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"将选项纳入其值(或null
为None
),因此等效表达式为:
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
,这适用于flatMap
和RDD
:
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