web-dev-qa-db-fra.com

Dériver plusieurs colonnes d'une seule colonne dans un Spark DataFrame

J'ai un DF avec une énorme métadonnée analysable sous la forme d'une colonne de chaîne unique dans un Dataframe, appelons-le DFA, avec ColmnA.

Je voudrais diviser cette colonne, ColmnA en plusieurs colonnes à travers une fonction, ClassXYZ = Func1 (ColmnA). Cette fonction retourne une classe ClassXYZ, avec plusieurs variables, et chacune de ces variables doit maintenant être mappée sur une nouvelle colonne, telle que ColmnA1, ColmnA2, etc.

Comment pourrais-je effectuer une telle transformation de 1 Dataframe à une autre avec ces colonnes supplémentaires en appelant ce Func1 une seule fois, sans avoir à le répéter pour créer toutes les colonnes.

C'est facile à résoudre si je devais appeler cette fonction énorme chaque fois pour ajouter une nouvelle colonne, mais c'est ce que je souhaite éviter.

Merci de bien vouloir indiquer si vous utilisez un pseudo-code ou un pseudo-code.

Merci

Sanjay

47
sshroff

D'une manière générale, ce que vous voulez n'est pas directement possible. UDF ne peut renvoyer qu'une seule colonne à la fois. Vous pouvez surmonter cette limitation de deux manières différentes:

  1. Renvoie une colonne de type complexe. La solution la plus générale est un StructType mais vous pouvez également considérer ArrayType ou MapType.

    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)
    

    Cela peut être facilement résolu plus tard, mais cela n’est généralement pas nécessaire.

  2. Basculer vers RDD, remodeler et reconstruire DF:

    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

Supposons qu'après votre fonction, il y aura une séquence d'éléments, en donnant un exemple comme ci-dessous:

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|
+------------------+---+

maintenant, ce que vous pouvez faire avec cet infoComb est que vous pouvez commencer à scinder la chaîne et obtenir plus de colonnes avec:

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|
+-----+----------+-------+---+

J'espère que cela t'aides.

16
EdwinGuo

Si vos colonnes résultantes ont la même longueur que la colonne d'origine, vous pouvez créer de toutes nouvelles colonnes avec la fonction withColumn et en appliquant un udf. Après cela, vous pouvez supprimer votre colonne d'origine, par exemple:

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

où myFun est un fichier UDF défini comme suit:

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

J'ai choisi de créer une fonction pour aplatir une colonne, puis de l'appeler simultanément avec le format udf.

Définissons d'abord ceci:

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)
  }

}

Alors l'utilisation est très simple:

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