web-dev-qa-db-de.com

Automatisches und elegantes Abflachen von DataFrame in Spark SQL

Alles,

Gibt es eine elegante und akzeptierte Möglichkeit, eine Spark-SQL-Tabelle (Parquet) mit geschachtelten Spalten zu verkleinern, StructType

Zum Beispiel

Wenn mein Schema lautet:

foo
 |_bar
 |_baz
x
y
z

Wie wähle ich es in einer abgeflachten tabellarischen Form aus, ohne auf manuelles Ausführen zurückgreifen zu müssen? 

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

Mit anderen Worten, wie erhalte ich das Ergebnis des obigen Codes programmgesteuert, wenn nur eine StructType und eine DataFrame gegeben wird?

22
echen

Die kurze Antwort lautet: Es gibt keinen "akzeptierten" Weg, dies zu tun, aber Sie können dies sehr elegant mit einer rekursiven Funktion tun, die Ihre select(...)-Anweisung generiert, indem Sie durch den DataFrame.schema gehen.

Die rekursive Funktion sollte einen Array[Column] zurückgeben. Jedes Mal, wenn die Funktion eine StructType trifft, würde sie sich selbst aufrufen und den zurückgegebenen Array[Column] an seinen eigenen Array[Column] anhängen.

So etwas wie:

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

Sie würden es dann so verwenden:

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

Ich verbessere meine vorherige Antwort und biete eine Lösung für mein eigenes Problem an, das in den Kommentaren der akzeptierten Antwort angegeben ist.

Diese akzeptierte Lösung erstellt ein Array von Column-Objekten und verwendet sie zur Auswahl dieser Spalten. Wenn Sie in Spark über ein verschachteltes DataFrame verfügen, können Sie die untergeordnete Spalte folgendermaßen auswählen: df.select("Parent.Child"). Dies gibt ein DataFrame mit den Werten der untergeordneten Spalte zurück und heißt Child . Wenn Sie jedoch identische Namen für Attribute verschiedener übergeordneter Strukturen haben, verlieren Sie die Informationen zum übergeordneten Element und erhalten möglicherweise identische Spaltennamen. Sie können nicht mehr auf den Namen zugreifen, da sie eindeutig sind. 

Das war mein Problem.

Ich habe eine Lösung für mein Problem gefunden, vielleicht kann es auch jemand anderem helfen. Ich habe die flattenSchema separat aufgerufen: 

val flattenedSchema = flattenSchema(df.schema)

und dies gab ein Array von Spaltenobjekten zurück. Anstatt dies in select() zu verwenden, was einen DataFrame mit Spalten zurückgeben würde, die vom untergeordneten Element der letzten Ebene benannt wurden, habe ich die ursprünglichen Spaltennamen als Zeichenfolgen zugeordnet, und nachdem ich die Parent.Child-Spalte ausgewählt habe, benennt sie diese in Parent.Child anstelle von Child ( Ich habe auch Punkte durch Unterstriche ersetzt (für meine Bequemlichkeit):

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

Und dann können Sie die Auswahlfunktion wie in der Originalantwort verwenden:

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

Ich wollte nur meine Lösung für Pyspark freigeben - es ist mehr oder weniger eine Übersetzung der Lösung von @David Griffin und unterstützt somit jede Ebene verschachtelter Objekte.

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

Um die Antworten von David Griffen und V. Samma zu kombinieren, können Sie dies tun, um eine Reduzierung zu erreichen und doppelte Spaltennamen zu vermeiden:

import org.Apache.spark.sql.types.StructType
import org.Apache.spark.sql.Column
import org.Apache.spark.sql.DataFrame

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).as(colName.replace(".","_")))
    }
  })
}

def flattenDataFrame(df:DataFrame): DataFrame = {
    df.select(flattenSchema(df.schema):_*)
}

var my_flattened_json_table = flattenDataFrame(my_json_table)
1
swdev

Ich habe eine DataFrame#flattenSchema-Methode zum Open Source spark-daria-Projekt hinzugefügt.

So können Sie die Funktion mit Ihrem Code verwenden.

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

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

Mit der Methode flattenSchema() können Sie auch andere Trennzeichen für Spaltennamen angeben.

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

Dieser Begrenzungsparameter ist überraschend wichtig. Wenn Sie Ihr Schema reduzieren, um die Tabelle in Redshift zu laden, können Sie keine Punkte als Trennzeichen verwenden.

Hier ist der vollständige Code-Ausschnitt, um diese Ausgabe zu generieren.

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

Der zugrunde liegende Code ähnelt dem von David Griffin (falls Sie die Funken-Daria-Abhängigkeit nicht zu Ihrem Projekt hinzufügen möchten).

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

Sie können SQL auch verwenden, um Spalten als flach auszuwählen.

  1. Holen Sie sich das ursprüngliche Datenrahmenschema
  2. Generieren Sie eine SQL-Zeichenfolge, indem Sie das Schema durchsuchen
  3. Fragen Sie Ihren ursprünglichen Datenrahmen ab

Ich habe eine Implementierung in Java durchgeführt: https://Gist.github.com/ebuildy/3de0e2855498e5358e4eed1a4f72ea48

(Verwenden Sie auch eine rekursive Methode, ich bevorzuge die SQL-Methode, sodass Sie sie problemlos über die Spark-Shell testen können.).

1
Thomas Decaux

Eine kleine Ergänzung zum obigen Code, wenn Sie mit verschachtelten Strukturen und Arrays arbeiten.

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 match {
        case StructField(_, struct:StructType, _, _) => flattenSchema(struct, colName)
        case StructField(_, ArrayType(x :StructType, _), _, _) => flattenSchema(x, colName)
        case StructField(_, ArrayType(_, _), _, _) => Array(col(colName))
        case _ => Array(col(colName))
      }
    })
  }

0

PySpark, hinzugefügt zu @Evan Vs Antwort, wenn Ihre Feldnamen Sonderzeichen enthalten, wie einen Punkt '.', Einen Bindestrich '-', ...:

from pyspark.sql.types import StructType, ArrayType  

def normalise_field(raw):
    return raw.strip().lower() \
            .replace('`', '') \
            .replace('-', '_') \
            .replace(' ', '_') \
            .strip('_')

def flatten(schema, prefix=None):
    fields = []
    for field in schema.fields:
        name = "%s.`%s`" % (prefix, field.name) if prefix else "`%s`" % field.name
        dtype = field.dataType
        if isinstance(dtype, ArrayType):
            dtype = dtype.elementType
        if isinstance(dtype, StructType):
            fields += flatten(dtype, prefix=name)
        else:
            fields.append(col(name).alias(normalise_field(name)))

    return fields

df.select(flatten(df.schema)).show()
0
Averell

Dies ist eine Modifikation der Lösung, die jedoch die Tailrec-Notation verwendet


  @tailrec
  def flattenSchema(
      splitter: String,
      fields: List[(StructField, String)],
      acc: Seq[Column]): Seq[Column] = {
    fields match {
      case (field, prefix) :: tail if field.dataType.isInstanceOf[StructType] =>
        val newPrefix = s"$prefix${field.name}."
        val newFields = field.dataType.asInstanceOf[StructType].fields.map((_, newPrefix)).toList
        flattenSchema(splitter, tail ++ newFields, acc)

      case (field, prefix) :: tail =>
        val colName = s"$prefix${field.name}"
        val newCol  = col(colName).as(colName.replace(".", splitter))
        flattenSchema(splitter, tail, acc :+ newCol)

      case _ => acc
    }
  }
  def flattenDataFrame(df: DataFrame): DataFrame = {
    val fields = df.schema.fields.map((_, ""))
    df.select(flattenSchema("__", fields.toList, Seq.empty): _*)
  }
0
fhuertas

Ich habe einen Liner verwendet, der ein abgeflachtes Schema mit 5 Spalten mit Balken, Baz, x, y, z ergibt:

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

Was explode betrifft, so reserviere ich explode normalerweise für das Reduzieren einer Liste. Wenn Sie beispielsweise eine Spalte idList haben, bei der es sich um eine Liste von Strings handelt, können Sie Folgendes tun:

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

Dies führt zu einem neuen Dataframe mit einer Spalte namens flattenedId (keine Liste mehr).

0
Kei-ven

Hier ist eine Funktion, die das tut, was Sie wollen, und das mit mehreren verschachtelten Spalten umgehen kann, die Spalten mit demselben Namen und einem Präfix enthalten:

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

Vor:

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)

Nach dem:

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