web-dev-qa-db-ja.com

Spark SQLでDataFrameを自動的かつエレガントにフラット化します

すべて、

SparkネストされたStructTypeの列を持つSQLテーブル(Parquet)をフラット化するエレガントで受け入れられた方法はありますか

例えば

私のスキーマが:

foo
 |_bar
 |_baz
x
y
z

手動で実行することなく、フラットな表形式に選択する方法

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

言い換えると、プログラムでStructTypeDataFrameだけを与えて、上記のコードの結果を取得するにはどうすればよいですか

31
echen

簡単な答えは、これを行うための「受け入れられた」方法はありませんが、_DataFrame.schema_を歩いてselect(...)ステートメントを生成する再帰関数を使用すると、非常にエレガントに行うことができます。

再帰関数は_Array[Column]_を返す必要があります。関数がStructTypeにヒットするたびに、それ自体を呼び出し、返された_Array[Column]_を独自の_Array[Column]_に追加します。

何かのようなもの:

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

次に、次のように使用します。

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

私は以前の回答を改善し、受け入れられた回答のコメントに記載されている自分の問題の解決策を提供しています。

この受け入れられたソリューションは、Columnオブジェクトの配列を作成し、それを使用してこれらの列を選択します。 Sparkでは、ネストされたDataFrameがある場合、次のように子列を選択できます:df.select("Parent.Child")これは、子列の値を含むDataFrameを返し、名前はChildです。しかし、異なる親構造の属性に同じ名前がある場合、親に関する情報を失い、同じ列名になり、それらが明確であるため、名前でアクセスできなくなります。

これが私の問題でした。

私は自分の問題の解決策を見つけました。他の誰かにも役立つかもしれません。 flattenSchemaを個別に呼び出しました:

_val flattenedSchema = flattenSchema(df.schema)
_

そして、これはColumnオブジェクトの配列を返しました。 select()でこれを使用する代わりに、最後のレベルの子によって名前が付けられた列を持つDataFrameを返し、元の列名を文字列として自分自身にマップし、_Parent.Child_列を選択した後、Childではなく_Parent.Child_に名前を変更します(便宜上、ドットをアンダースコアに置き換えました)。

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

そして、元の答えに示されているように、選択機能を使用できます。

_var newDf = df.select(renamedCols:_*)
_
18
V. Samma

Pysparkのソリューションを共有したかっただけです。これは、@ David Griffinのソリューションの翻訳であり、あらゆるレベルのネストされたオブジェクトをサポートします。

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()
12
Evan V

David GriffenとV. Sammaの回答を組み合わせるには、列名の重複を避けながらこれを行うだけで済みます。

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

_DataFrame#flattenSchema_メソッドをオープンソース spark-dariaプロジェクト に追加しました。

コードで関数を使用する方法は次のとおりです。

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

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

flattenSchema()メソッドを使用して、異なる列名の区切り文字を指定することもできます。

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

この区切りパラメータは驚くほど重要です。スキーマをフラット化してRedshiftでテーブルをロードする場合、区切り文字としてピリオドを使用することはできません。

この出力を生成する完全なコードスニペットを次に示します。

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

基礎となるコードはDavid Griffinのコードに似ています(プロジェクトにspark-daria依存関係を追加したくない場合)。

_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

これは、必要な処理を実行し、同じ名前の列を含む複数のネストされた列を接頭辞付きで処理できる関数です。

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

前:

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)

後:

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)
1
steco

SQLを使用して、列をフラットとして選択することもできます。

  1. 元のデータフレームスキーマを取得する
  2. スキーマを参照して、SQL文字列を生成します
  3. 元のデータフレームを照会する

Javaで実装しました: https://Gist.github.com/ebuildy/3de0e2855498e5358e4eed1a4f72ea48

(再帰的な方法も使用してください。SQLの方法が好きなので、Spark-Shellで簡単にテストできます)。

1
Thomas Decaux

Nested StructとArrayを使用している場合は、上記のコードに少し追加します。

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

フィールド名にドット「。」、ハイフン「-」などの特殊文字が含まれる場合、@ Evan Vの回答に追加されたPySpark

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

これはソリューションの修正ですが、tailrec表記を使用します


  @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

私は1つのライナーを使用しており、その結果、bar、baz、x、y、zの5つの列を持つフラット化されたスキーマになります。

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

explodeに関しては、通常、リストをフラット化するためにexplodeを予約しています。たとえば、文字列のリストである列idListがある場合、次のようにできます。

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

その結果、flattenedId(リストではない)という名前の列を持つ新しいデータフレームが作成されます。

0
Kei-ven