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
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):_*)
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:_*)
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()
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): _*
)
}
}
}
Vous pouvez également utiliser SQL pour sélectionner des colonnes aussi plates.
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).
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)
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)