web-dev-qa-db-fra.com

SparkSQL: Comment gérer les valeurs nulles dans une fonction définie par l'utilisateur?

Étant donné le tableau 1 avec une colonne "x" de type chaîne. Je veux créer le tableau 2 avec une colonne "y" qui est une représentation entière des chaînes de date données en "x".

Essentiel est de conserver les valeurs de null dans la colonne "y".

Tableau 1 (Dataframe df1):

+----------+
|         x|
+----------+
|2015-09-12|
|2015-09-13|
|      null|
|      null|
+----------+
root
 |-- x: string (nullable = true)

Tableau 2 (Dataframe df2):

+----------+--------+                                                                  
|         x|       y|
+----------+--------+
|      null|    null|
|      null|    null|
|2015-09-12|20150912|
|2015-09-13|20150913|
+----------+--------+
root
 |-- x: string (nullable = true)
 |-- y: integer (nullable = true)

Alors que la fonction définie par l'utilisateur (udf) pour convertir les valeurs de la colonne "x" en celles de la colonne "y" est:

val extractDateAsInt = udf[Int, String] (
  (d:String) => d.substring(0, 10)
      .filterNot( "-".toSet)
      .toInt )

et fonctionne, le traitement des valeurs nulles n'est pas possible.

Même si je peux faire quelque chose comme

val extractDateAsIntWithNull = udf[Int, String] (
  (d:String) => 
    if (d != null) d.substring(0, 10).filterNot( "-".toSet).toInt 
    else 1 )

Je n'ai trouvé aucun moyen de "produire" des valeurs null via udfs (bien sûr, comme Ints ne peut pas être null).

Ma solution actuelle pour la création de df2 (tableau 2) est la suivante:

// holds data of table 1  
val df1 = ... 

// filter entries from df1, that are not null
val dfNotNulls = df1.filter(df1("x")
  .isNotNull)
  .withColumn("y", extractDateAsInt(df1("x")))
  .withColumnRenamed("x", "right_x")

// create df2 via a left join on df1 and dfNotNull having 
val df2 = df1.join( dfNotNulls, df1("x") === dfNotNulls("right_x"), "leftouter" ).drop("right_x")

Questions:

  • La solution actuelle semble lourde (et probablement pas efficace par rapport aux performances). Y a-t-il une meilleure façon?
  • @ Spark-developers: Existe-t-il un type NullableInt prévu/disponible, tel que le udf suivant est possible (voir extrait de code)?

Extrait de code

val extractDateAsNullableInt = udf[NullableInt, String] (
  (d:String) => 
    if (d != null) d.substring(0, 10).filterNot( "-".toSet).toInt 
    else null )
27
Martin Senne

C'est là que Option est utile:

val extractDateAsOptionInt = udf((d: String) => d match {
  case null => None
  case s => Some(s.substring(0, 10).filterNot("-".toSet).toInt)
})

ou pour le rendre un peu plus sûr dans le cas général:

import scala.util.Try

val extractDateAsOptionInt = udf((d: String) => Try(
  d.substring(0, 10).filterNot("-".toSet).toInt
).toOption)

Tout le mérite revient à Dmitriy Selivanov qui a signalé cette solution comme une modification (manquante?) ici .

L'alternative consiste à gérer null en dehors de l'UDF:

import org.Apache.spark.sql.functions.{lit, when}
import org.Apache.spark.sql.types.IntegerType

val extractDateAsInt = udf(
   (d: String) => d.substring(0, 10).filterNot("-".toSet).toInt
)

df.withColumn("y",
  when($"x".isNull, lit(null))
    .otherwise(extractDateAsInt($"x"))
    .cast(IntegerType)
)
51
zero323

Scala a en fait une fonction d'usine Nice, Option (), qui peut rendre cela encore plus concis:

val extractDateAsOptionInt = udf((d: String) => 
  Option(d).map(_.substring(0, 10).filterNot("-".toSet).toInt))

En interne, la méthode apply de l'objet Option effectue simplement la vérification nulle pour vous:

def apply[A](x: A): Option[A] = if (x == null) None else Some(x)
11
tristanbuckner

Code supplémentaire

Avec la réponse Nice de @ zero323, j'ai créé le code suivant, pour avoir des fonctions définies par l'utilisateur disponibles qui gèrent les valeurs nulles comme décrit. J'espère que c'est utile pour les autres!

/**
 * Set of methods to construct [[org.Apache.spark.sql.UserDefinedFunction]]s that
 * handle `null` values.
 */
object NullableFunctions {

  import org.Apache.spark.sql.functions._
  import scala.reflect.runtime.universe.{TypeTag}
  import org.Apache.spark.sql.UserDefinedFunction

  /**
   * Given a function A1 => RT, create a [[org.Apache.spark.sql.UserDefinedFunction]] such that
   *   * if fnc input is null, None is returned. This will create a null value in the output Spark column.
   *   * if A1 is non null, Some( f(input) will be returned, thus creating f(input) as value in the output column.
   * @param f function from A1 => RT
   * @tparam RT return type
   * @tparam A1 input parameter type
   * @return a [[org.Apache.spark.sql.UserDefinedFunction]] with the behaviour describe above
   */
  def nullableUdf[RT: TypeTag, A1: TypeTag](f: Function1[A1, RT]): UserDefinedFunction = {
    udf[Option[RT],A1]( (i: A1) => i match {
      case null => None
      case s => Some(f(i))
    })
  }

  /**
   * Given a function A1, A2 => RT, create a [[org.Apache.spark.sql.UserDefinedFunction]] such that
   *   * if on of the function input parameters is null, None is returned.
   *     This will create a null value in the output Spark column.
   *   * if both input parameters are non null, Some( f(input) will be returned, thus creating f(input1, input2)
   *     as value in the output column.
   * @param f function from A1 => RT
   * @tparam RT return type
   * @tparam A1 input parameter type
   * @tparam A2 input parameter type
   * @return a [[org.Apache.spark.sql.UserDefinedFunction]] with the behaviour describe above
   */
  def nullableUdf[RT: TypeTag, A1: TypeTag, A2: TypeTag](f: Function2[A1, A2, RT]): UserDefinedFunction = {
    udf[Option[RT], A1, A2]( (i1: A1, i2: A2) =>  (i1, i2) match {
      case (null, _) => None
      case (_, null) => None
      case (s1, s2) => Some((f(s1,s2)))
    } )
  }
}
10
Martin Senne