0
votes

I have a sample Dataframe that I create using below code

val data = Seq(
  Row(20.0, "dog"),
  Row(3.5, "cat"),
  Row(0.000006, "ant")
)

val schema = StructType(
  List(
    StructField("weight", DoubleType, true),
    StructField("animal_type", StringType, true)
  )
)

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

val actualDF = df.withColumn(
  "animal_interpretation",
  struct(
    (col("weight") > 5).as("is_large_animal"),
    col("animal_type").isin("rat", "cat", "dog").as("is_mammal")
  )
)

actualDF.show(false)

+------+-----------+---------------------+
|weight|animal_type|animal_interpretation|
+------+-----------+---------------------+
|20.0  |dog        |[true,true]          |
|3.5   |cat        |[false,true]         |
|6.0E-6|ant        |[false,false]        |
+------+-----------+---------------------+

The schema of this Spark DF can be printed using -

scala> actualDF.printSchema
root
 |-- weight: double (nullable = true)
 |-- animal_type: string (nullable = true)
 |-- animal_interpretation: struct (nullable = false)
 |    |-- is_large_animal: boolean (nullable = true)
 |    |-- is_mammal: boolean (nullable = true)

However, I would like to get this schema in the form of a dataframe that has 3 columns - field, type, nullable. The output dataframe from the schema would something like this -

+-------------------------------------+--------------+--------+
|field                                |type          |nullable|
+-------------------------------------+--------------+--------+
|weight                               |double        |true    |        
|animal_type                          |string        |true    |       
|animal_interpretation                |struct        |false   |
|animal_interpretation.is_large_animal|boolean       |true    |
|animal_interpretation.is_mammal      |boolean       |true    |     
+----------------------------------------------------+--------+

How can I achieve this in Spark. I am using Scala for coding.

2

2 Answers

0
votes

You could do something like this

def flattenSchema(schema: StructType, prefix: String = null) : Seq[(String, String, Boolean)] = {
  schema.fields.flatMap(field => {
    val col = if (prefix == null) field.name else (prefix + "." + field.name)
    field.dataType match {
      case st: StructType => flattenSchema(st, col)
      case _ => Array((col, field.dataType.simpleString, field.nullable))
    }
  })
}

flattenSchema(actualDF.schema).toDF("field", "type", "nullable").show()

Hope this helps!

0
votes

Here is a complete example including your code. I used the somewhat common flattenSchema method for matching like Shankar did to traverse the Struct but rather than having this method return the flattened schema I used an ArrayBuffer to aggregate the datatypes of the StructType and returned the ArrayBuffer. I then turned the ArrayBuffer into a Sequence and finally, using Spark, converted the Sequence to a DataFrame.

import org.apache.spark.sql.types.{StructType, StructField, DoubleType, StringType}
import org.apache.spark.sql.functions.{struct, col}
import scala.collection.mutable.ArrayBuffer

val data = Seq(
  Row(20.0, "dog"),
  Row(3.5, "cat"),
  Row(0.000006, "ant")
)

val schema = StructType(
  List(
    StructField("weight", DoubleType, true),
    StructField("animal_type", StringType, true)
  )
)

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

val actualDF = df.withColumn(
  "animal_interpretation",
  struct(
    (col("weight") > 5).as("is_large_animal"),
    col("animal_type").isin("rat", "cat", "dog").as("is_mammal")
  )
)

var fieldStructs = new ArrayBuffer[(String, String, Boolean)]()

def flattenSchema(schema: StructType, fieldStructs: ArrayBuffer[(String, String, Boolean)], prefix: String = null): ArrayBuffer[(String, String, Boolean)] = {
  schema.fields.foreach(field => {
    val col = if (prefix == null) field.name else (prefix + "." + field.name)
    field.dataType match {
      case st: StructType => {
        fieldStructs += ((col, field.dataType.typeName, field.nullable))
        flattenSchema(st, fieldStructs, col)
      }
      case _ => {
        fieldStructs += ((col, field.dataType.simpleString, field.nullable))
      }
    }}
  )
  fieldStructs
}

val foo = flattenSchema(actualDF.schema, fieldStructs).toSeq.toDF("field", "type", "nullable")
foo.show(false)

If you run the above you should get the following.

+-------------------------------------+-------+--------+
|field                                |type   |nullable|
+-------------------------------------+-------+--------+
|weight                               |double |true    |
|animal_type                          |string |true    |
|animal_interpretation                |struct |false   |
|animal_interpretation.is_large_animal|boolean|true    |
|animal_interpretation.is_mammal      |boolean|true    |
+-------------------------------------+-------+--------+