【问题标题】:UnFlatten Dataframe to a specific structureUnFlatten Dataframe 到特定结构
【发布时间】:2019-05-07 11:37:51
【问题描述】:

我有一个平面数据框 (df),其结构如下:

root
 |-- first_name: string (nullable = true)
 |-- middle_name: string (nullable = true)
 |-- last_name: string (nullable = true)
 |-- title: string (nullable = true)
 |-- start_date: string (nullable = true)
 |-- end_Date: string (nullable = true)
 |-- city: string (nullable = true)
 |-- zip_code: string (nullable = true)
 |-- state: string (nullable = true)
 |-- country: string (nullable = true)
 |-- email_name: string (nullable = true)
 |-- company: struct (nullable = true)
 |-- org_name: string (nullable = true)
 |-- company_phone: string (nullable = true)
 |-- partition_column: string (nullable = true)

而且我需要将此数据框转换为类似的结构(因为我的下一个数据将采用这种格式):

root
 |-- firstName: string (nullable = true)
 |-- middleName: string (nullable = true)
 |-- lastName: string (nullable = true)
 |-- currentPosition: array (nullable = true)
 |    |-- element: struct (containsNull = true)
 |    |    |-- title: string (nullable = true)
 |    |    |-- startDate: string (nullable = true)
 |    |    |-- endDate: string (nullable = true)
 |    |    |-- address: struct (nullable = true)
 |    |    |    |-- city: string (nullable = true)
 |    |    |    |-- zipCode: string (nullable = true)
 |    |    |    |-- state: string (nullable = true)
 |    |    |    |-- country: string (nullable = true)
 |    |    |-- emailName: string (nullable = true)
 |    |    |-- company: struct (nullable = true)
 |    |    |    |-- orgName: string (nullable = true)
 |    |    |    |-- companyPhone: string (nullable = true)
 |-- partitionColumn: string (nullable = true)

到目前为止,我已经实现了这个:

case class IndividualCompany(orgName: String,
                             companyPhone: String)

case class IndividualAddress(city: String,
                   zipCode: String,
                   state: String,
                   country: String)

case class IndividualPosition(title: String,
                              startDate: String,
                              endDate: String,
                              address: IndividualAddress,
                              emailName: String,
                              company: IndividualCompany)

case class Individual(firstName: String,
                     middleName: String,
                     lastName: String,
                     currentPosition: Seq[IndividualPosition],
                     partitionColumn: String)


val makeCompany = udf((orgName: String, companyPhone: String) => IndividualCompany(orgName, companyPhone))
val makeAddress = udf((city: String, zipCode: String, state: String, country: String) => IndividualAddress(city, zipCode, state, country))

val makePosition = udf((title: String, startDate: String, endDate: String, address: IndividualAddress, emailName: String, company: IndividualCompany) 
                    => List(IndividualPosition(title, startDate, endDate, address, emailName, company)))


val selectData = df.select(
      col("first_name").as("firstName"),
      col("middle_name).as("middleName"),
      col("last_name").as("lastName"),
      makePosition(col("job_title"),
        col("start_date"),
        col("end_Date"),
        makeAddress(col("city"),
          col("zip_code"),
          col("state"),
          col("country")),
        col("email_name"),
        makeCompany(col("org_name"),
          col("company_phone"))).as("currentPosition"),
      col("partition_column").as("partitionColumn")
    ).as[Individual]

select_data.printSchema()
select_data.show(10)

我可以看到为select_data 生成了正确的架构,但它在我试图获取一些实际数据的最后一行出现错误。我收到一条错误消息,提示无法执行用户定义的函数。

 org.apache.spark.SparkException: Failed to execute user defined function(anonfun$4: (string, string, string, struct<city:string,zipCode:string,state:string,country:string>, string, struct<orgName:string,companyPhone:string>) => array<struct<title:string,startDate:string,endDate:string,address:struct<city:string,zipCode:string,state:string,country:string>,emailName:string,company:struct<orgName:string,companyPhone:string>>>)

有没有更好的方法来实现这一点?

【问题讨论】:

标签: scala apache-spark dataframe apache-spark-sql user-defined-functions


【解决方案1】:

我也有类似的要求。
我所做的是创建一个typed user defined aggregation,它将产生一个List 元素。

import org.apache.spark.sql.{Encoder, TypedColumn}
import org.apache.spark.sql.expressions.Aggregator
import scala.collection.mutable

object ListAggregator {
  private type Buffer[T] = mutable.ListBuffer[T]

  /** Returns a column that aggregates all elements of type T in a List. */
  def create[T](columnName: String)
               (implicit listEncoder: Encoder[List[T]], listBufferEncoder: Encoder[Buffer[T]]): TypedColumn[T, List[T]] =
    new Aggregator[T, Buffer[T], List[T]] {
      override def zero: Buffer[T] =
        mutable.ListBuffer.empty[T]

      override def reduce(buffer: Buffer[T], elem: T): Buffer[T] =
        buffer += elem

      override def merge(b1: Buffer[T], b2: Buffer[T]): Buffer[T] =
        if (b1.length >= b2.length) b1 ++= b2 else b2 ++= b1

      override def finish(reduction: Buffer[T]): List[T] =
        reduction.toList

      override def bufferEncoder: Encoder[Buffer[T]] =
        listBufferEncoder

      override def outputEncoder: Encoder[List[T]] =
        listEncoder
    }.toColumn.name(columnName)
}

现在你可以像这样使用它了。

import org.apache.spark.sql.SparkSession

val spark =
  SparkSession
    .builder
    .master("local[*]")
    .getOrCreate()

import spark.implicits._

final case class Flat(id: Int, name: String, age: Int)
final case class Grouped(age: Int, users: List[(Int, String)])

val data =
  List(
    (1, "Luis", 21),
    (2, "Miguel", 21),
    (3, "Sebastian", 16)
  ).toDF("id", "name", "age").as[Flat]

val grouped =
  data
    .groupByKey(flat => flat.age)
    .mapValues(flat => (flat.id, flat.name))
    .agg(ListAggregator.create(columnName = "users"))
    .map(tuple => Grouped(age = tuple._1, users = tuple._2))
// grouped: org.apache.spark.sql.Dataset[Grouped] = [age: int, users: array<struct<_1:int,_2:string>>]

grouped.show(truncate = false)
// +---+------------------------+
// |age|users                   |
// +---+------------------------+
// |16 |[[3, Sebastian]]        |
// |21 |[[1, Luis], [2, Miguel]]|
// +---+------------------------+

【讨论】:

    【解决方案2】:

    这里的问题是udf 不能直接将IndividualAddressIndividualCompany 作为输入。这些在 Spark 中表示为结构,并且要在 udf 中使用它们,正确的输入类型是 Row。这意味着您需要将makePosition 的声明更改为:

    val makePosition = udf((title: String, 
                            startDate: String, 
                            endDate: String, 
                            address: Row, 
                            emailName: String, 
                            company: Row)
    

    udf 中,您现在需要使用例如address.getAs[String]("city") 要访问案例类元素,并且要将类作为一个整体使用,您需要再次创建它。

    更简单、更好的选择是在一个 udf 中完成所有操作,如下所示:

    val makePosition = udf((title: String, 
        startDate: String, 
        endDate: String, 
        city: String, 
        zipCode: String, 
        state: String, 
        country: String,
        emailName: String, 
        orgName: String, 
        companyPhone: String) => 
            Seq(
              IndividualPosition(
                title, 
                startDate, 
                endDate, 
                IndividualAddress(city, zipCode, state, country),
                emailName, 
                IndividualCompany(orgName, companyPhone)
              )
            )
    )
    

    【讨论】:

    • 感谢您的解决方案。在一个 udf 中做所有事情不允许您传递超过 10 个参数,这就是我要嵌套 udf 的原因。
    • 对于第一个解决方案,我如何将Row 类型从df.select() 方法传递给这个udf
    • 知道了。我刚刚将我的 makeAddress 和 makeCompany 方法修改为 val makeCompany = udf((orgName: String, companyPhone: String) =&gt; {Row(orgName, companyPhone)},companySchema)
    猜你喜欢
    • 2021-11-18
    • 1970-01-01
    • 1970-01-01
    • 2014-12-31
    • 2023-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多