有没有办法为Spark数据帧添加额外的元数据?

时间:2015-09-17 11:06:50

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

是否可以向DataFrame添加额外的元数据?

原因

我有Spark DataFrame,我需要保留额外的信息。示例:一个DataFrame,我想“记住”整数id列中使用率最高的索引。

当前解决方案

我使用单独的DataFrame来存储此信息。当然,分开保存这些信息既繁琐又容易出错。

是否有更好的解决方案可以在DataFrame上存储此类额外信息?

5 个答案:

答案 0 :(得分:12)

扩展和Scala-fy nealmcb的答案(问题标记为scala,而不是python,所以我认为这个答案不会是主题或冗余),假设你有一个DataFrame:

import org.apache.spark.sql
val df = sc.parallelize(Seq.fill(100) { scala.util.Random.nextInt() }).toDF("randInt")

以某种方式获取最大值或想要在DataFrame上记忆的任何内容:

val randIntMax = df.rdd.map { case sql.Row(randInt: Int) => randInt }.reduce(math.max)

sql.types.Metadata只能包含字符串,布尔值,某些类型的数字和其他元数据结构。所以我们必须使用Long:

val metadata = new sql.types.MetadataBuilder().putLong("columnMax", randIntMax).build()

DataFrame.withColumn()实际上有一个重载,允许在最后提供元数据参数,但它被莫名其妙地标记为[私有],所以我们只是做它做的事情 - 使用Column.as(alias, metadata)

val newColumn = df.col("randInt").as("randInt_withMax", metadata)
val dfWithMax = df.withColumn("randInt_withMax", newColumn)

dfWithMax现在有一个(有一列)您想要的元数据!

dfWithMax.schema.foreach(field => println(s"${field.name}: metadata=${field.metadata}"))
> randInt: metadata={}
> randInt_withMax: metadata={"columnMax":2094414111}

或者以编程方式和类型安全(排序; Metadata.getLong()和其他人不返回Option并且可能抛出“未找到密钥”异常):

dfWithMax.schema("randInt_withMax").metadata.getLong("columnMax")
> res29: Long = 209341992

在您的情况下将max附加到列是有意义的,但是在将元数据附加到DataFrame而不是特定列的一般情况下,看起来您必须采用其他答案描述的包装器路径。

答案 1 :(得分:7)

从Spark 1.2开始,StructType模式具有metadata属性,该属性可以保存Dataframe中每个列的任意映射/信息字典。例如。 (与单独的spark-csv库一起使用时):

customSchema = StructType([
  StructField("cat_id", IntegerType(), True,
    {'description': "Unique id, primary key"}),
  StructField("cat_title", StringType(), True,
    {'description': "Name of the category, with underscores"}) ])

categoryDumpDF = (sqlContext.read.format('com.databricks.spark.csv')
 .options(header='false')
 .load(csvFilename, schema = customSchema) )

f = categoryDumpDF.schema.fields
["%s (%s): %s" % (t.name, t.dataType, t.metadata) for t in f]

["cat_id (IntegerType): {u'description': u'Unique id, primary key'}",
 "cat_title (StringType): {u'description': u'Name of the category, with underscores.'}"]

这是在[SPARK-3569] Add metadata field to StructField - ASF JIRA中添加的,旨在用于机器学习管道,以跟踪有关列中存储的要素的信息,例如分类/连续,数字类别,类别到索引映射。请参阅SPARK-3569: Add metadata field to StructField设计文档。

我希望看到这种广泛使用,例如列的描述和文档,列中使用的度量单位,坐标轴信息等

问题包括如何在转换列时适当地保留或操作元数据信息,如何处理多种元数据,如何使其全部可扩展等等。

为了那些考虑在Spark数据帧中扩展此功能的人的利益,我参考了一些围绕Pandas的类似讨论。

例如,请参阅支持标记数组元数据的xray - bring the labeled data power of pandas to the physical sciences

请参阅Allow custom metadata to be attached to panel/df/series? · Issue #2485 · pydata/pandas的Pandas元数据讨论。

另见与单位相关的讨论:ENH: unit of measurement / physical quantities · Issue #10349 · pydata/pandas

答案 2 :(得分:2)

如果你想减少繁琐的工作,我想你可以在DataFrame和你的自定义包装器之间添加一个隐式转换(虽然还没有测试过。)

   implicit class WrappedDataFrame(val df: DataFrame) {
        var metadata = scala.collection.mutable.Map[String, Long]()

        def addToMetaData(key: String, value: Long) {
           metadata += key -> value
        }
     ...[other methods you consider useful, getters, setters, whatever]...
      }

如果隐式包装器位于DataFrame的范围内,您可以使用普通的DataFrame,就像它是您的包装器一样,即:。

df.addtoMetaData("size", 100)

这种方式也会使您的元数据变得可变,因此您不应该只强制计算一次并随身携带它。

答案 3 :(得分:0)

我会在数据框周围存储一个包装器。例如:

case class MyDFWrapper(dataFrame: DataFrame, metadata: Map[String, Long])
val maxIndex = df1.agg("index" ->"MAX").head.getLong(0)
MyDFWrapper(df1, Map("maxIndex" -> maxIndex))

答案 4 :(得分:0)

很多人看到“元数据”一词,直接进入“列元数据”。这似乎不是您想要的,也不是我遇到类似问题时想要的。最终,这里的问题是DataFrame是一个不变的数据结构,每当对其执行操作时,数据都会继续传递,而其余的DataFrame则不会。这意味着您不能简单地在其上放一个包装器,因为执行操作后,您将拥有一个全新的DataFrame(可能是全新的数据框,尤其是Scala / Spark倾向于隐式转换)。最后,如果DataFrame逃脱了其包装器,则无法从DataFrame重建元数据。

我在Spark Streaming中遇到了这个问题,它主要关注RDD(DataFrame的底层数据结构),并得出一个简单的结论:存储元数据的唯一位置是RDD的名称。除报告外,核心Spark系统从不使用RDD名称,因此重新使用它是安全的。然后,您可以基于RDD名称创建包装器,并在 any DataFrame和包装器之间进行显式转换,并使用元数据完成。

不幸的是,这仍然给您带来了不变性的问题,并且每次操作都会创建新的RDD。每个新的RDD都会丢失RDD名称(我们的元数据字段)。这意味着您需要一种将名称重新添加到新的RDD中的方法。这可以通过提供一种以函数作为参数的方法来解决。它可以在函数之前提取元数据,调用函数并获取新的RDD / DataFrame,然后使用元数据对其进行命名:

def withMetadata(fn: (df: DataFrame) => DataFrame): MetaDataFrame = {
  val meta = df.rdd.name
  val result = fn(wrappedFrame)
  result.rdd.setName(meta)
  MetaDataFrame(result)
}

您的包装类(MetaDataFrame)可以提供方便的方法来解析和设置元数据值,以及在Spark DataFrame和MetaDataFrame之间来回隐式转换。只要您通过withMetadata方法运行所有变异,您的元数据就会通过整个转换管道传递。是的,是的,为每个呼叫使用这种方法有点麻烦,但是简单的现实是,Spark中没有一流的元数据概念。