web-dev-qa-db-fra.com

Comment changer les types de colonnes dans Spark Le DataFrame de SQL?

Supposons que je fasse quelque chose comme:

val df = sqlContext.load("com.databricks.spark.csv", Map("path" -> "cars.csv", "header" -> "true"))
df.printSchema()

root
 |-- year: string (nullable = true)
 |-- make: string (nullable = true)
 |-- model: string (nullable = true)
 |-- comment: string (nullable = true)
 |-- blank: string (nullable = true)

df.show()
year make  model comment              blank
2012 Tesla S     No comment                
1997 Ford  E350  Go get one now th...  

mais je voulais vraiment la year comme Int (et peut-être transformer d'autres colonnes).

Le mieux que je puisse trouver est

df.withColumn("year2", 'year.cast("Int")).select('year2 as 'year, 'make, 'model, 'comment, 'blank)
org.Apache.spark.sql.DataFrame = [year: int, make: string, model: string, comment: string, blank: string]

ce qui est un peu compliqué.

Je viens de R et je suis habitué à pouvoir écrire, par exemple.

df2 <- df %>%
   mutate(year = year %>% as.integer, 
          make = make %>% toupper)

Je manque probablement quelque chose, car il devrait y avoir un meilleur moyen de le faire en spark/scala ...

136
kevinykuo

Edit: Version la plus récente

Depuis spark 2.x, vous pouvez utiliser .withColumn. Vérifiez la documentation ici:

https://spark.Apache.org/docs/latest/api/scala/index.html#org.Apache.spark.sql.Dataset@withColumn (colName: String, col: org.Apache.spark. sql.Column): org.Apache.spark.sql.DataFrame

Réponse la plus ancienne

Depuis Spark version 1.4, vous pouvez appliquer la méthode de conversion avec DataType sur la colonne:

import org.Apache.spark.sql.types.IntegerType
val df2 = df.withColumn("yearTmp", df.year.cast(IntegerType))
    .drop("year")
    .withColumnRenamed("yearTmp", "year")

Si vous utilisez des expressions SQL, vous pouvez également faire:

val df2 = df.selectExpr("cast(year as int) year", 
                        "make", 
                        "model", 
                        "comment", 
                        "blank")

Pour plus d'informations, consultez la documentation: http://spark.Apache.org/docs/1.6.0/api/scala/#org.Apache.spark.sql.DataFrame

130
msemelman

[EDIT: mars 2016: merci pour les votes! Bien que ce ne soit vraiment pas la meilleure réponse, je pense que les solutions basées sur withColumn, withColumnRenamed et cast proposées par msemelman, Martin Senne et d'autres sont plus simples et plus propres].

Je pense que votre approche est correcte, rappelez-vous qu'un Spark DataFrame est un RDD (immuable) de lignes, de sorte que nous ne sommes jamais vraiment en remplacement une colonne, créant simplement new DataFrame à chaque fois avec un nouveau schéma.

En supposant que vous avez un df d'origine avec le schéma suivant:

scala> df.printSchema
root
 |-- Year: string (nullable = true)
 |-- Month: string (nullable = true)
 |-- DayofMonth: string (nullable = true)
 |-- DayOfWeek: string (nullable = true)
 |-- DepDelay: string (nullable = true)
 |-- Distance: string (nullable = true)
 |-- CRSDepTime: string (nullable = true)

Et des UDF définis sur une ou plusieurs colonnes:

import org.Apache.spark.sql.functions._

val toInt    = udf[Int, String]( _.toInt)
val toDouble = udf[Double, String]( _.toDouble)
val toHour   = udf((t: String) => "%04d".format(t.toInt).take(2).toInt ) 
val days_since_nearest_holidays = udf( 
  (year:String, month:String, dayOfMonth:String) => year.toInt + 27 + month.toInt-12
 )

Changer les types de colonne ou même créer un nouveau DataFrame à partir d'un autre peut être écrit comme ceci:

val featureDf = df
.withColumn("departureDelay", toDouble(df("DepDelay")))
.withColumn("departureHour",  toHour(df("CRSDepTime")))
.withColumn("dayOfWeek",      toInt(df("DayOfWeek")))              
.withColumn("dayOfMonth",     toInt(df("DayofMonth")))              
.withColumn("month",          toInt(df("Month")))              
.withColumn("distance",       toDouble(df("Distance")))              
.withColumn("nearestHoliday", days_since_nearest_holidays(
              df("Year"), df("Month"), df("DayofMonth"))
            )              
.select("departureDelay", "departureHour", "dayOfWeek", "dayOfMonth", 
        "month", "distance", "nearestHoliday")            

qui donne:

scala> df.printSchema
root
 |-- departureDelay: double (nullable = true)
 |-- departureHour: integer (nullable = true)
 |-- dayOfWeek: integer (nullable = true)
 |-- dayOfMonth: integer (nullable = true)
 |-- month: integer (nullable = true)
 |-- distance: double (nullable = true)
 |-- nearestHoliday: integer (nullable = true)

C'est assez proche de votre propre solution. Simplement, garder les modifications de type et les autres transformations séparément udf vals rend le code plus lisible et réutilisable.

87
Svend

Comme l'opération cast est disponible pour les Spark Column (et personnellement, je ne suis pas en faveur de udf comme proposé par @Svend à ce stade), que diriez-vous:

df.select( df("year").cast(IntegerType).as("year"), ... )

lancer sur le type demandé? Comme effet secondaire, les valeurs non convertibles/"convertibles" dans ce sens deviendront null.

Si vous avez besoin de cela comme ne méthode d'assistance, utilisez:

object DFHelper{
  def castColumnTo( df: DataFrame, cn: String, tpe: DataType ) : DataFrame = {
    df.withColumn( cn, df(cn).cast(tpe) )
  }
}

qui s'utilise comme:

import DFHelper._
val df2 = castColumnTo( df, "year", IntegerType )
60
Martin Senne

First, si vous voulez utiliser un type, alors ceci:

import org.Apache.spark.sql
df.withColumn("year", $"year".cast(sql.types.IntegerType))

Avec le même nom de colonne, la colonne sera remplacée par une nouvelle. Vous n'avez pas besoin d'ajouter ou de supprimer des étapes.

Deuxième, à propos de Scala vs R.
C’est le code qui ressemble le plus à R que je peux trouver:

val df2 = df.select(
   df.columns.map {
     case year @ "year" => df(year).cast(IntegerType).as(year)
     case make @ "make" => functions.upper(df(make)).as(make)
     case other         => df(other)
   }: _*
)

Bien que la longueur du code soit un peu plus longue que celle de R. Cela n'a rien à voir avec la verbosité de la langue. Dans R, la mutate est une fonction spéciale pour le cadre de données R, tandis que dans Scala, vous pouvez facilement en créer une ad-hoc grâce à son pouvoir expressif.
Dans Word, cela évite des solutions spécifiques, car la base est suffisamment bonne pour vous permettre de créer rapidement et facilement vos propres fonctionnalités de langage de domaine.


note latérale: df.columns est étonnamment un Array[String] au lieu de Array[Column], peut-être veulent-ils que cela ressemble à Python le cadre de données de pandas.

43
WeiChing Lin

Vous pouvez utiliser selectExpr pour le rendre un peu plus propre:

df.selectExpr("cast(year as int) as year", "upper(make) as make",
    "model", "comment", "blank")
16
dnlbrky

Code Java permettant de modifier le type de données du DataFrame de String à Integer

df.withColumn("col_name", df.col("col_name").cast(DataTypes.IntegerType))

Il convertira simplement le type existant (type de données String) en Integer.

10
manishbelsare

Pour convertir l'année de chaîne en entier, vous pouvez ajouter l'option suivante au lecteur csv: "inferSchema" -> "true", voir documentation DataBricks

8
Peter Rose

Donc, cela ne fonctionne vraiment que si vous rencontrez des problèmes d'enregistrement sur un pilote jdbc tel que sqlserver, mais c'est vraiment utile pour les erreurs que vous rencontrerez avec la syntaxe et les types.

import org.Apache.spark.sql.jdbc.{JdbcDialects, JdbcType, JdbcDialect}
import org.Apache.spark.sql.jdbc.JdbcType
val SQLServerDialect = new JdbcDialect {
  override def canHandle(url: String): Boolean = url.startsWith("jdbc:jtds:sqlserver") || url.contains("sqlserver")

  override def getJDBCType(dt: DataType): Option[JdbcType] = dt match {
    case StringType => Some(JdbcType("VARCHAR(5000)", Java.sql.Types.VARCHAR))
    case BooleanType => Some(JdbcType("BIT(1)", Java.sql.Types.BIT))
    case IntegerType => Some(JdbcType("INTEGER", Java.sql.Types.INTEGER))
    case LongType => Some(JdbcType("BIGINT", Java.sql.Types.BIGINT))
    case DoubleType => Some(JdbcType("DOUBLE PRECISION", Java.sql.Types.DOUBLE))
    case FloatType => Some(JdbcType("REAL", Java.sql.Types.REAL))
    case ShortType => Some(JdbcType("INTEGER", Java.sql.Types.INTEGER))
    case ByteType => Some(JdbcType("INTEGER", Java.sql.Types.INTEGER))
    case BinaryType => Some(JdbcType("BINARY", Java.sql.Types.BINARY))
    case TimestampType => Some(JdbcType("DATE", Java.sql.Types.DATE))
    case DateType => Some(JdbcType("DATE", Java.sql.Types.DATE))
    //      case DecimalType.Fixed(precision, scale) => Some(JdbcType("NUMBER(" + precision + "," + scale + ")", Java.sql.Types.NUMERIC))
    case t: DecimalType => Some(JdbcType(s"DECIMAL(${t.precision},${t.scale})", Java.sql.Types.DECIMAL))
    case _ => throw new IllegalArgumentException(s"Don't know how to save ${dt.json} to JDBC")
  }
}

JdbcDialects.registerDialect(SQLServerDialect)
6
ben jarman

Générez un jeu de données simple contenant cinq valeurs et convertissez int en string tapez:

val df = spark.range(5).select( col("id").cast("string") )
6
user8106134
df.select($"long_col".cast(IntegerType).as("int_col"))
5
soulmachine

les réponses suggérant d'utiliser cast, FYI, la méthode de conversion utilisée dans spark 1.4.1 sont erronées.

par exemple, une trame de données avec une colonne de chaîne ayant la valeur "8182175552014127960" lorsqu'elle est convertie en bigint a la valeur "8182175552014128100"

    df.show
+-------------------+
|                  a|
+-------------------+
|8182175552014127960|
+-------------------+

    df.selectExpr("cast(a as bigint) a").show
+-------------------+
|                  a|
+-------------------+
|8182175552014128100|
+-------------------+

Nous avons dû faire face à beaucoup de problèmes avant de trouver ce bogue parce que nous avions des colonnes bigint en production.

5
sauraI3h

En utilisant Spark Sql 2.4.0, vous pouvez le faire:

spark.sql("SELECT STRING(NULLIF(column,'')) as column_string")
4
Eric Bellet

Vous pouvez utiliser le code ci-dessous.

df.withColumn("year", df("year").cast(IntegerType))

Ce qui convertira année colonne en IntegerType colonne.

2
adarsh

Cette méthode supprimera l'ancienne colonne et créera de nouvelles colonnes avec les mêmes valeurs et le nouveau type de données. Mes types de données d'origine lors de la création du DataFrame étaient les suivants: -

root
 |-- id: integer (nullable = true)
 |-- flag1: string (nullable = true)
 |-- flag2: string (nullable = true)
 |-- name: string (nullable = true)
 |-- flag3: string (nullable = true)

Après cela, j'ai exécuté le code suivant pour changer le type de données: -

df=df.withColumnRenamed(<old column name>,<dummy column>) // This was done for both flag1 and flag3
df=df.withColumn(<old column name>,df.col(<dummy column>).cast(<datatype>)).drop(<dummy column>)

Après cela, mon résultat s'est avéré être: -

root
 |-- id: integer (nullable = true)
 |-- flag2: string (nullable = true)
 |-- name: string (nullable = true)
 |-- flag1: boolean (nullable = true)
 |-- flag3: boolean (nullable = true)
2
PirateJack

Si vous devez renommer des dizaines de colonnes en fonction de leur nom, l'exemple suivant adopte l'approche de @dnlbrky et l'applique à plusieurs colonnes à la fois:

df.selectExpr(df.columns.map(cn => {
    if (Set("speed", "weight", "height").contains(cn)) s"cast($cn as double) as $cn"
    else if (Set("isActive", "hasDevice").contains(cn)) s"cast($cn as boolean) as $cn"
    else cn
}):_*)

Les colonnes non diffusées restent inchangées. Toutes les colonnes restent dans leur ordre d'origine.

1
cubic lettuce
Another solution is as follows:
1) Keep "inferSchema" as False
2) While running 'Map' functions on the row, you can read 'asString' (row.getString...)

<Code>
        //Read CSV and create dataset
        Dataset<Row> enginesDataSet = sparkSession
                    .read()
                    .format("com.databricks.spark.csv")
                    .option("header", "true")
                    .option("inferSchema","false")
                    .load(args[0]);

        JavaRDD<Box> vertices = enginesDataSet
                    .select("BOX","BOX_CD")
                    .toJavaRDD()
                    .map(new Function<Row, Box>() {
                        @Override
                        public Box call(Row row) throws Exception {
                            return new Box((String)row.getString(0),(String)row.get(1));
                        }
                    });
</Code>
0
Vibha

On peut changer le type de données d’une colonne en utilisant la conversion dans spark sql. nom de table est table et il a deux colonnes seulement les types de données column1 et column2 et column1 doivent être changés. ex-spark.sql ("select cast (column1 as Double) column1NewName, column2 from table") À la place de double-écrire votre type de données.

0
Tejasvi Sharma