web-dev-qa-db-de.com

Leiten Sie mehrere Spalten aus einer einzelnen Spalte in einem Spark DataFrame ab

Ich habe ein DF mit einer großen analysierbaren Metadaten als einzelne Zeichenfolgenspalte in einem Dataframe, nennen wir es DFA, mit ColmnA.

Ich möchte diese Spalte, ColmnA, durch eine Funktion, ClassXYZ = Func1 (ColmnA), in mehrere Spalten aufteilen. Diese Funktion gibt eine Klasse ClassXYZ mit mehreren Variablen zurück, und jede dieser Variablen muss jetzt einer neuen Spalte zugeordnet werden, z. B. ColmnA1, ColmnA2 usw.

Wie würde ich mit diesen zusätzlichen Spalten eine solche Transformation von einem Datenrahmen in einen anderen durchführen, indem ich diese Funktion nur einmal aufrufe und sie nicht wiederholen muss, um alle Spalten zu erstellen?.

Es ist einfach zu lösen, wenn ich diese riesige Funktion jedes Mal aufrufe, um eine neue Spalte hinzuzufügen, aber das möchte ich vermeiden.

Bitte geben Sie einen Arbeits- oder Pseudocode an.

Vielen Dank

Sanjay

47
sshroff

Im Allgemeinen ist das, was Sie wollen, nicht direkt möglich. UDF kann jeweils nur eine einzelne Spalte zurückgeben. Es gibt zwei Möglichkeiten, diese Einschränkung zu überwinden:

  1. Gibt eine Spalte eines komplexen Typs zurück. Die allgemeinste Lösung ist ein StructType, Sie können jedoch auch ArrayType oder MapType in Betracht ziehen.

    import org.Apache.spark.sql.functions.udf
    
    val df = Seq(
      (1L, 3.0, "a"), (2L, -1.0, "b"), (3L, 0.0, "c")
    ).toDF("x", "y", "z")
    
    case class Foobar(foo: Double, bar: Double)
    
    val foobarUdf = udf((x: Long, y: Double, z: String) => 
      Foobar(x * y, z.head.toInt * y))
    
    val df1 = df.withColumn("foobar", foobarUdf($"x", $"y", $"z"))
    df1.show
    // +---+----+---+------------+
    // |  x|   y|  z|      foobar|
    // +---+----+---+------------+
    // |  1| 3.0|  a| [3.0,291.0]|
    // |  2|-1.0|  b|[-2.0,-98.0]|
    // |  3| 0.0|  c|   [0.0,0.0]|
    // +---+----+---+------------+
    
    df1.printSchema
    // root
    //  |-- x: long (nullable = false)
    //  |-- y: double (nullable = false)
    //  |-- z: string (nullable = true)
    //  |-- foobar: struct (nullable = true)
    //  |    |-- foo: double (nullable = false)
    //  |    |-- bar: double (nullable = false)
    

    Dies kann später leicht abgeflacht werden, ist aber normalerweise nicht erforderlich.

  2. Wechseln Sie zu RDD, gestalten Sie DF um und erstellen Sie es neu:

    import org.Apache.spark.sql.types._
    import org.Apache.spark.sql.Row
    
    def foobarFunc(x: Long, y: Double, z: String): Seq[Any] = 
      Seq(x * y, z.head.toInt * y)
    
    val schema = StructType(df.schema.fields ++
      Array(StructField("foo", DoubleType), StructField("bar", DoubleType)))
    
    val rows = df.rdd.map(r => Row.fromSeq(
      r.toSeq ++
      foobarFunc(r.getAs[Long]("x"), r.getAs[Double]("y"), r.getAs[String]("z"))))
    
    val df2 = sqlContext.createDataFrame(rows, schema)
    
    df2.show
    // +---+----+---+----+-----+
    // |  x|   y|  z| foo|  bar|
    // +---+----+---+----+-----+
    // |  1| 3.0|  a| 3.0|291.0|
    // |  2|-1.0|  b|-2.0|-98.0|
    // |  3| 0.0|  c| 0.0|  0.0|
    // +---+----+---+----+-----+
    
64
zero323

Nehmen Sie an, dass es nach Ihrer Funktion eine Folge von Elementen gibt, und geben Sie ein Beispiel wie folgt an:

val df = sc.parallelize(List(("Mike,1986,Toronto", 30), ("Andre,1980,Ottawa", 36), ("jill,1989,London", 27))).toDF("infoComb", "age")
df.show
+------------------+---+
|          infoComb|age|
+------------------+---+
|Mike,1986,Toronto| 30|
| Andre,1980,Ottawa| 36|
|  jill,1989,London| 27|
+------------------+---+

mit dieser InfoComb können Sie nun beginnen, die Zeichenfolge zu teilen, und weitere Spalten abrufen mit:

df.select(expr("(split(infoComb, ','))[0]").cast("string").as("name"), expr("(split(infoComb, ','))[1]").cast("integer").as("yearOfBorn"), expr("(split(infoComb, ','))[2]").cast("string").as("city"), $"age").show
+-----+----------+-------+---+
| name|yearOfBorn|   city|age|
+-----+----------+-------+---+
|Mike|      1986|Toronto| 30|
|Andre|      1980| Ottawa| 36|
| jill|      1989| London| 27|
+-----+----------+-------+---+

Hoffe das hilft.

16
EdwinGuo

Wenn Ihre resultierenden Spalten dieselbe Länge wie die ursprüngliche haben, können Sie mit der Funktion withColumn und durch Anwenden eines UdF brandneue Spalten erstellen. Danach können Sie Ihre ursprüngliche Spalte ablegen, zB:

 val newDf = myDf.withColumn("newCol1", myFun(myDf("originalColumn")))
.withColumn("newCol2", myFun2(myDf("originalColumn"))
.drop(myDf("originalColumn"))

wo myFun ist ein udf wie folgt definiert:

   def myFun= udf(
    (originalColumnContent : String) =>  {
      // do something with your original column content and return a new one
    }
  )
5
Niemand

Ich habe mich dafür entschieden, eine Funktion zu erstellen, um eine Spalte zu verflachen und sie dann gleichzeitig mit dem udf aufzurufen.

Definieren Sie zuerst Folgendes:

implicit class DfOperations(df: DataFrame) {

  def flattenColumn(col: String) = {
    def addColumns(df: DataFrame, cols: Array[String]): DataFrame = {
      if (cols.isEmpty) df
      else addColumns(
        df.withColumn(col + "_" + cols.head, df(col + "." + cols.head)),
        cols.tail
      )
    }

    val field = df.select(col).schema.fields(0)
    val newCols = field.dataType.asInstanceOf[StructType].fields.map(x => x.name)

    addColumns(df, newCols).drop(col)
  }

  def withColumnMany(colName: String, col: Column) = {
    df.withColumn(colName, col).flattenColumn(colName)
  }

}

Dann ist die Benutzung ganz einfach:

case class MyClass(a: Int, b: Int)

val df = sc.parallelize(Seq(
  (0),
  (1)
)).toDF("x")

val f = udf((x: Int) => MyClass(x*2,x*3))

df.withColumnMany("test", f($"x")).show()

//  +---+------+------+
//  |  x|test_a|test_b|
//  +---+------+------+
//  |  0|     0|     0|
//  |  1|     2|     3|
//  +---+------+------+
2
Pekka