web-dev-qa-db-fra.com

Aplatissement automatique et élégant de DataFrame dans Spark SQL

Tout,

Existe-t-il un moyen élégant et accepté d’aplatir une table Spark SQL (Parquet) avec des colonnes imbriquées StructType

Par exemple

Si mon schéma est:

foo
 |_bar
 |_baz
x
y
z

Comment puis-je le sélectionner dans une forme tabulaire aplatie sans avoir recours à l'exécution manuelle 

df.select("foo.bar","foo.baz","x","y","z")

En d'autres termes, comment puis-je obtenir le résultat du code ci-dessus donné par programme simplement avec une StructType et une DataFrame

22
echen

La réponse courte est qu'il n'y a pas de moyen "accepté" de le faire, mais vous pouvez le faire très élégamment avec une fonction récursive qui génère votre déclaration select(...) en parcourant le DataFrame.schema.

La fonction récursive devrait renvoyer un Array[Column]. Chaque fois que la fonction rencontre une StructType, elle s’appelle elle-même et ajoute le Array[Column] renvoyé à son propre Array[Column].

Quelque chose comme:

def flattenSchema(schema: StructType, prefix: String = null) : Array[Column] = {
  schema.fields.flatMap(f => {
    val colName = if (prefix == null) f.name else (prefix + "." + f.name)

    f.dataType match {
      case st: StructType => flattenSchema(st, colName)
      case _ => Array(col(colName))
    }
  })
}

Vous l'utiliseriez alors comme ceci:

df.select(flattenSchema(df.schema):_*)
48
David Griffin

J'améliore ma réponse précédente et offre une solution à mon propre problème indiqué dans les commentaires de la réponse acceptée.

Cette solution acceptée crée un tableau d'objets Column et l'utilise pour sélectionner ces colonnes. Dans Spark, si vous avez un DataFrame imbriqué, vous pouvez sélectionner la colonne enfant comme suit: df.select("Parent.Child"). Cette commande renvoie un DataFrame contenant les valeurs de la colonne enfant et est nommé Child . Mais si vous avez des noms identiques pour les attributs de structures parentes différentes, vous perdez les informations sur le parent et pouvez vous retrouver avec des noms de colonne identiques. Vous ne pouvez plus y accéder par leur nom, car ils ne sont pas ambigus. 

C'était mon problème.

J'ai trouvé une solution à mon problème, peut-être que ça peut aider quelqu'un d'autre aussi. J'ai appelé le flattenSchema séparément: 

val flattenedSchema = flattenSchema(df.schema)

et cela a retourné un tableau d'objets Column. Au lieu d’utiliser ceci dans la select(), qui renverrait un DataFrame avec des colonnes nommées par l’enfant du dernier niveau, j’ai mappé les noms de colonnes originaux sur eux-mêmes en tant que chaînes, puis après la sélection de Parent.Child column, il le renomme en Parent.Child au lieu de Child ( J'ai également remplacé les points par des traits de soulignement pour plus de commodité):

val renamedCols = flattenedSchema.map(name => col(name.toString()).as(name.toString().replace(".","_")))

Et puis vous pouvez utiliser la fonction select comme indiqué dans la réponse d'origine:

var newDf = df.select(renamedCols:_*)
17
V. Samma

Je voulais simplement partager ma solution pour Pyspark. Il s’agit plus ou moins d’une traduction de la solution de @David Griffin, qui prend en charge tous les niveaux d’objets imbriqués.

from pyspark.sql.types import StructType, ArrayType  

def flatten(schema, prefix=None):
    fields = []
    for field in schema.fields:
        name = prefix + '.' + field.name if prefix else field.name
        dtype = field.dataType
        if isinstance(dtype, ArrayType):
            dtype = dtype.elementType

        if isinstance(dtype, StructType):
            fields += flatten(dtype, prefix=name)
        else:
            fields.append(name)

    return fields


df.select(flatten(df.schema)).show()
8
Evan V

J'ai ajouté une méthode DataFrame#flattenSchema au projet open source spark-daria .

Voici comment utiliser la fonction avec votre code.

import com.github.mrpowers.spark.daria.sql.DataFrameExt._
df.flattenSchema().show()

+-------+-------+---------+----+---+
|foo.bar|foo.baz|        x|   y|  z|
+-------+-------+---------+----+---+
|   this|     is|something|cool| ;)|
+-------+-------+---------+----+---+

Vous pouvez également spécifier différents délimiteurs de nom de colonne avec la méthode flattenSchema().

df.flattenSchema(delimiter = "_").show()
+-------+-------+---------+----+---+
|foo_bar|foo_baz|        x|   y|  z|
+-------+-------+---------+----+---+
|   this|     is|something|cool| ;)|
+-------+-------+---------+----+---+

Ce paramètre de délimiteur est étonnamment important. Si vous aplatissez votre schéma pour charger la table dans Redshift, vous ne pourrez pas utiliser de points comme délimiteur.

Voici l'extrait de code complet permettant de générer cette sortie.

val data = Seq(
  Row(Row("this", "is"), "something", "cool", ";)")
)

val schema = StructType(
  Seq(
    StructField(
      "foo",
      StructType(
        Seq(
          StructField("bar", StringType, true),
          StructField("baz", StringType, true)
        )
      ),
      true
    ),
    StructField("x", StringType, true),
    StructField("y", StringType, true),
    StructField("z", StringType, true)
  )
)

val df = spark.createDataFrame(
  spark.sparkContext.parallelize(data),
  StructType(schema)
)

df.flattenSchema().show()

Le code sous-jacent est similaire au code de David Griffin (au cas où vous ne voudriez pas ajouter la dépendance spark-daria à votre projet).

object StructTypeHelpers {

  def flattenSchema(schema: StructType, delimiter: String = ".", prefix: String = null): Array[Column] = {
    schema.fields.flatMap(structField => {
      val codeColName = if (prefix == null) structField.name else prefix + "." + structField.name
      val colName = if (prefix == null) structField.name else prefix + delimiter + structField.name

      structField.dataType match {
        case st: StructType => flattenSchema(schema = st, delimiter = delimiter, prefix = colName)
        case _ => Array(col(codeColName).alias(colName))
      }
    })
  }

}

object DataFrameExt {

  implicit class DataFrameMethods(df: DataFrame) {

    def flattenSchema(delimiter: String = ".", prefix: String = null): DataFrame = {
      df.select(
        StructTypeHelpers.flattenSchema(df.schema, delimiter, prefix): _*
      )
    }

  }

}
1
Powers

Vous pouvez également utiliser SQL pour sélectionner des colonnes aussi plates.

  1. Obtenir le schéma d'origine de la trame de données
  2. Générer une chaîne SQL en parcourant le schéma
  3. Interrogez votre cadre de données d'origine

J'ai fait une implémentation en Java: https://Gist.github.com/ebuildy/3de0e2855498e5358e4eed1a4f72ea48

(utilisez également la méthode récursive, je préfère la méthode SQL pour pouvoir la tester facilement via Spark-Shell).

1
Thomas Decaux

J'utilisais des liners qui aboutissent à un schéma aplati avec 5 colonnes de barres, baz, x, y, z:

df.select("foo.*", "x", "y", "z")

En ce qui concerne explode: Je réserve généralement explode pour aplatir une liste. Par exemple, si vous avez une colonne idList qui est une liste de chaînes, vous pouvez faire:

df.withColumn("flattenedId", functions.explode(col("idList")))
  .drop("idList")

Cela se traduira par un nouveau Dataframe avec une colonne nommée flattenedId (plus une liste)

0
Kei-ven

Voici une fonction qui fait ce que vous voulez et qui peut gérer plusieurs colonnes imbriquées contenant des colonnes de même nom, avec un préfixe:

from pyspark.sql import functions as F

def flatten_df(nested_df):
    flat_cols = [c[0] for c in nested_df.dtypes if c[1][:6] != 'struct']
    nested_cols = [c[0] for c in nested_df.dtypes if c[1][:6] == 'struct']

    flat_df = nested_df.select(flat_cols +
                               [F.col(nc+'.'+c).alias(nc+'_'+c)
                                for nc in nested_cols
                                for c in nested_df.select(nc+'.*').columns])
    return flat_df

Avant:

root
 |-- x: string (nullable = true)
 |-- y: string (nullable = true)
 |-- foo: struct (nullable = true)
 |    |-- a: float (nullable = true)
 |    |-- b: float (nullable = true)
 |    |-- c: integer (nullable = true)
 |-- bar: struct (nullable = true)
 |    |-- a: float (nullable = true)
 |    |-- b: float (nullable = true)
 |    |-- c: integer (nullable = true)

Après:

root
 |-- x: string (nullable = true)
 |-- y: string (nullable = true)
 |-- foo_a: float (nullable = true)
 |-- foo_b: float (nullable = true)
 |-- foo_c: integer (nullable = true)
 |-- bar_a: float (nullable = true)
 |-- bar_b: float (nullable = true)
 |-- bar_c: integer (nullable = true)
0
steco