web-dev-qa-db-fra.com

Spark sql comment exploser sans perdre les valeurs nulles

J'ai un Dataframe que j'essaye d'aplatir. Dans le cadre du processus, je veux l'exploser, donc si j'ai une colonne de tableaux, chaque valeur du tableau sera utilisée pour créer une ligne distincte. Par exemple,

id | name | likes
_______________________________
1  | Luke | [baseball, soccer]

devrait devenir

id | name | likes
_______________________________
1  | Luke | baseball
1  | Luke | soccer

C'est mon code

private DataFrame explodeDataFrame(DataFrame df) {
    DataFrame resultDf = df;
    for (StructField field : df.schema().fields()) {
        if (field.dataType() instanceof ArrayType) {
            resultDf = resultDf.withColumn(field.name(), org.Apache.spark.sql.functions.explode(resultDf.col(field.name())));
            resultDf.show();
        }
    }
    return resultDf;
}

Le problème est que dans mes données, certaines des colonnes du tableau ont des valeurs nulles. Dans ce cas, la ligne entière est supprimée. Donc, cette trame de données:

id | name | likes
_______________________________
1  | Luke | [baseball, soccer]
2  | Lucy | null

devient

id | name | likes
_______________________________
1  | Luke | baseball
1  | Luke | soccer

au lieu de

id | name | likes
_______________________________
1  | Luke | baseball
1  | Luke | soccer
2  | Lucy | null

Comment puis-je faire exploser mes tableaux pour ne pas perdre les lignes nulles?

J'utilise Spark 1.5.2 et Java 8

27
alexgbelov

Spark 2.2 +

Vous pouvez utiliser la fonction explode_outer:

import org.Apache.spark.sql.functions.explode_outer

df.withColumn("likes", explode_outer($"likes")).show

// +---+----+--------+
// | id|name|   likes|
// +---+----+--------+
// |  1|Luke|baseball|
// |  1|Luke|  soccer|
// |  2|Lucy|    null|
// +---+----+--------+

Spark <= 2.1

Dans Scala mais Java doit être presque identique (pour importer des fonctions individuelles, utilisez import static)).

import org.Apache.spark.sql.functions.{array, col, explode, lit, when}

val df = Seq(
  (1, "Luke", Some(Array("baseball", "soccer"))),
  (2, "Lucy", None)
).toDF("id", "name", "likes")

df.withColumn("likes", explode(
  when(col("likes").isNotNull, col("likes"))
    // If null explode an array<string> with a single null
    .otherwise(array(lit(null).cast("string")))))

L'idée ici est essentiellement de remplacer NULL par une array(NULL) du type souhaité. Pour les types complexes (a.k.a structs), vous devez fournir un schéma complet:

val dfStruct = Seq((1L, Some(Array((1, "a")))), (2L, None)).toDF("x", "y")

val st =  StructType(Seq(
  StructField("_1", IntegerType, false), StructField("_2", StringType, true)
))

dfStruct.withColumn("y", explode(
  when(col("y").isNotNull, col("y"))
    .otherwise(array(lit(null).cast(st)))))

ou

dfStruct.withColumn("y", explode(
  when(col("y").isNotNull, col("y"))
    .otherwise(array(lit(null).cast("struct<_1:int,_2:string>")))))

Remarque:

Si le tableau Column a été créé avec containsNull défini sur false, vous devez d'abord le modifier (testé avec Spark 2.1):

df.withColumn("array_column", $"array_column".cast(ArrayType(SomeType, true)))
52
zero323

Suite à la réponse acceptée, lorsque les éléments du tableau sont de type complexe, il peut être difficile de le définir à la main (par exemple avec de grandes structures).

Pour le faire automatiquement, j'ai écrit la méthode d'assistance suivante:

  def explodeOuter(df: Dataset[Row], columnsToExplode: List[String]) = {
      val arrayFields = df.schema.fields
          .map(field => field.name -> field.dataType)
          .collect { case (name: String, type: ArrayType) => (name, type.asInstanceOf[ArrayType])}
          .toMap

      columnsToExplode.foldLeft(df) { (dataFrame, arrayCol) =>
      dataFrame.withColumn(arrayCol, explode(when(size(col(arrayCol)) =!= 0, col(arrayCol))
        .otherwise(array(lit(null).cast(arrayFields(arrayCol).elementType)))))    
 }
1
nsanglar

Vous pouvez utiliser la fonction explode_outer().

1
TopGuys