Comment puis-je modifier les types de colonnes dans DataFrame de Spark SQL?

152

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 le yearas Int(et peut-être transformer d'autres colonnes).

Le mieux que je pouvais trouver était

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 alambiqué.

Je viens de R, et j'ai l'habitude de pouvoir écrire, par exemple

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

Il me manque probablement quelque chose, car il devrait y avoir une meilleure façon de le faire dans Spark / Scala ...

kevinykuo
la source
J'aime cette façon spark.sql ("SELECT STRING (NULLIF (column, '')) as column_string")
Eric Bellet

Réponses:

141

Edit: version la plus récente

Depuis spark 2.x, vous pouvez utiliser .withColumn. Consultez les documents 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

La plus ancienne réponse

Depuis Spark version 1.4, vous pouvez appliquer la méthode cast 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

msemelman
la source
4
pourquoi avez-vous utilisé withColumn suivi de drop? N'est-il pas plus simple d'utiliser withColumn avec le nom de colonne d'origine?
Ameba Spugnosa
@AmebaSpugnosa Je pense qu'au moment où je l'ai utilisé, Spark s'est écrasé s'il avait des noms de colonnes répétés. Pas lorsque vous les créez, mais lorsque vous les utilisez.
msemelman
5
il n'est pas nécessaire de supprimer la colonne suivie d'un changement de nom. Vous pouvez faire en une seule lignedf.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong
1
Une nouvelle copie de dataframe est-elle créée uniquement pour refondre une colonne dans ce cas? Est-ce que je manque quelque chose? Ou peut-être y a-t-il une optimisation dans les coulisses?
user1814008
5
Aller par la documentation de Spark 2.x, df.withColumn(..)peut ajouter ou remplacer une colonne en fonction de l' colNameargument
y2k-shubham
89

[EDIT: mars 2016: merci pour les votes! Bien que vraiment, ce n'est pas la meilleure réponse, je pense que les solutions basées sur withColumn, withColumnRenamedetcast mis en avant par msemelman, Martin Senne et d' autres sont plus simples et plus propre].

Je pense que votre approche est ok, rappeler qu'une Spark DataFrameest un (immuable) RDD de lignes, donc nous ne sommes jamais vraiment remplacer une colonne, juste créer une nouvelle à DataFramechaque fois avec un nouveau schéma.

En supposant que vous ayez un df original 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 certains 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 colonnes ou même construire 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")            

ce 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 changements de type et autres transformations comme des udf vals séparés rend le code plus lisible et réutilisable.

Svend
la source
26
Ce n'est ni sûr ni efficace. Pas sûr, car une NULLentrée unique ou mal formée plantera un travail entier. Pas efficace car les UDF ne sont pas transparents pour Catalyst. Utiliser des UDF pour des opérations complexes est très bien, mais il n'y a aucune raison de les utiliser pour la conversion de type de base. C'est pourquoi nous avons la castméthode (voir une réponse de Martin Senne ). Rendre les choses transparentes pour Catalyst nécessite plus de travail, mais la sécurité de base n'est qu'une question de mise Tryet Optionde travail.
zero323
Je n'ai rien vu concernant la conversion de la chaîne en date, par exemple "05-APR-2015"
dbspace
3
Existe-t-il un moyen de réduire votre withColumn()section à une section générique qui parcourt toutes les colonnes?
Boern
Merci zero323, en lisant ceci, j'ai compris pourquoi la solution udf se bloque ici. Certains commentaires valent mieux que certaines réponses sur SO :)
Simon Dirmeier
Existe-t-il un moyen de connaître la ligne corrompue, c'est-à-dire les enregistrements contenant des colonnes de mauvais types de données lors de la conversion. Comme la fonction de cast rend ces champs comme nuls
Etisha
65

Comme l' castopération est disponible pour Spark Column(et comme je ne suis personnellement pas favorable à udfcelle proposée par @ Svendà ce stade), que diriez-vous:

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

cast au type demandé? En tant qu'effet secondaire soigné, les valeurs non castables / "convertibles" dans ce sens deviendront null.

Si vous en avez besoin comme méthode d'assistance , utilisez:

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

qui est utilisé comme:

import DFHelper._
val df2 = castColumnTo( df, "year", IntegerType )
Martin Senne
la source
2
Pouvez-vous me conseiller sur la façon de procéder, si je dois lancer et renommer tout un tas de colonnes (j'ai 50 colonnes, et assez nouveau pour scala, je ne sais pas quelle est la meilleure façon de l'aborder sans créer une duplication massive)? Certaines colonnes doivent rester String, d'autres doivent être converties en Float.
Dmitry Smirnov le
comment convertir une chaîne en date par exemple "25-APR-2016" dans la colonne et "20160302"
dbspace
@DmitrySmirnov Avez-vous déjà obtenu une réponse? J'ai la même question. ;)
Evan Zamir
@EvanZamir malheureusement pas, j'ai fini par faire une merde d'opérations pour pouvoir utiliser les données comme rdd dans d'autres étapes. Je me demande si cela est devenu plus facile ces jours-ci :)
Dmitry Smirnov
60

Premièrement , si vous voulez lancer du 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 et de supprimer des étapes.

Deuxièmement , à propos de Scala vs R .
Voici le code le plus similaire à RI:

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, il mutateexiste une fonction spéciale pour R dataframe, tandis que dans Scala, vous pouvez facilement en ad-hoc grâce à sa puissance expressive.
En bref, cela évite les solutions spécifiques, car la conception du langage est suffisamment bonne pour que vous puissiez créer rapidement et facilement votre propre langage de domaine.


note latérale: df.columnsest étonnamment un Array[String]au lieu de Array[Column], peut-être qu'ils veulent que cela ressemble au dataframe de pandas Python.

WeiChing 林 煒 清
la source
1
Pourriez-vous s'il vous plaît donner l'équivalent pour pyspark?
Harit Vishwakarma
Je reçois un "début de définition illégal" .withColumn ("age", $ "age" .cast (sql.types.DoubleType)) pour mon champ "age". Toute suggestion?
BlueDolphin
Faut-il .cache () la trame de données si nous effectuons ces conversions sur de nombreuses colonnes pour des raisons de performances, ou n'est-ce pas nécessaire car Spark les optimise?
skjagini
L'importation peut être import org.apache.spark.sql.types._et alors au lieu de sql.types.IntegerTypesimplement IntegerType.
nessa.gp
17

Vous pouvez utiliser selectExprpour le rendre un peu plus propre:

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

Code Java pour modifier le type de données du DataFrame de String en Integer

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

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

manishbelsare
la source
1
Il n'y a pas DataTypesdedans sql.types! c'est DataType. De plus, on peut simplement importer IntegerTypeet lancer.
Ehsan M. Kermani
@ EhsanM.Kermani en fait DatyaTypes.IntegerType est une référence légitime.
Cupitor
1
@Cupitor DataTypes.IntegerTypeétait en mode DeveloperAPI et il est stable dans la v.2.1.0
Ehsan M. Kermani
C'est la meilleure solution!
Simon Dirmeier
8

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

Peter Rose
la source
5
Cela fonctionne bien mais le hic, c'est que le lecteur doit faire un deuxième passage de votre fichier
beefyhalo
@beefyhalo absolument parfait, y a-t-il un moyen de contourner cela?
Ayush le
6

Donc, cela ne fonctionne vraiment que si vous rencontrez des problèmes pour enregistrer dans un pilote jdbc comme 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)
Ben Jarman
la source
Pouvez-vous m'aider à implémenter le même code en Java? et comment enregistrer le customJdbcDialect dans DataFrame
abhijitcaps
Bien, j'ai fait la même chose avec Vertica, mais depuis Spark 2.1. JDbcUtil vous devez implémenter uniquement le type de données spécifique dont vous avez besoin. dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)). getOrElse (lancer une nouvelle IllegalArgumentException (s "Impossible d'obtenir le type JDBC pour $ {dt.simpleString}"))
Arnon Rodman
6

Générez un jeu de données simple contenant cinq valeurs et convertissez-le inten stringtype:

val df = spark.range(5).select( col("id").cast("string") )
user8106134
la source
6

Je pense que c'est beaucoup plus lisible pour moi.

import org.apache.spark.sql.types._
df.withColumn("year", df("year").cast(IntegerType))

Cela convertira votre colonne d'année en IntegerTypeen créant des colonnes temporaires et en supprimant ces colonnes. Si vous souhaitez convertir en un autre type de données, vous pouvez vérifier les types à l'intérieur du org.apache.spark.sql.typespackage.

Piyush Patel
la source
5

les réponses suggérant d'utiliser la fonte, FYI, la méthode de fonte dans l'étincelle 1.4.1 est cassée.

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 à de nombreux problèmes avant de trouver ce bogue car nous avions des colonnes bigint en production.

sauraI3h
la source
4
psst, améliorez votre étincelle
msemelman
2
@msemelman c'est ridicule de devoir passer à une nouvelle version de spark en production pour un petit bug.
sauraI3h
ne mettons-nous pas toujours à jour tout pour les petits bugs? :)
caesarsol
5
df.select($"long_col".cast(IntegerType).as("int_col"))
âme machine
la source
4

En utilisant Spark Sql 2.4.0, vous pouvez le faire:

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

Vous pouvez utiliser le code ci-dessous.

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

Qui convertira la colonne de l' année enIntegerType colonne.

adarsh
la source
2

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

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 est devenu: -

root
 |-- id: integer (nullable = true)
 |-- flag2: string (nullable = true)
 |-- name: string (nullable = true)
 |-- flag1: boolean (nullable = true)
 |-- flag3: boolean (nullable = true)
PirateJack
la source
Pourriez-vous s'il vous plaît fournir votre solution ici.
Ajay Kharade
1

On peut changer le type de données d'une colonne en utilisant cast dans spark sql. Le nom de la table est table et il a deux colonnes, seuls les types de données colonne1 et colonne2 et colonne1 doivent être modifiés. ex-spark.sql ("sélectionnez cast (colonne1 comme double) colonne1NouveauNom, colonne2 de la table") À la place de double, écrivez votre type de données.

Tejasvi Sharma
la source
1

Si vous devez renommer des dizaines de colonnes données par 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 castées restent inchangées. Toutes les colonnes restent dans leur ordre d'origine.

laitue cubique
la source
1

Tant de réponses et peu d'explications approfondies

La syntaxe suivante fonctionne avec Databricks Notebook avec Spark 2.4

from pyspark.sql.functions import *
df = df.withColumn("COL_NAME", to_date(BLDFm["LOAD_DATE"], "MM-dd-yyyy"))

Notez que vous devez spécifier le format d'entrée que vous avez (dans mon cas "MM-jj-aaaa") et l'importation est obligatoire car le to_date est une fonction Spark SQL

J'ai également essayé cette syntaxe, mais j'ai obtenu des valeurs nulles au lieu d'une conversion appropriée:

df = df.withColumn("COL_NAME", df["COL_NAME"].cast("Date"))

(Notez que j'ai dû utiliser des crochets et des guillemets pour que la syntaxe soit correcte)


PS: Je dois admettre que c'est comme une jungle de syntaxe, il existe de nombreuses façons possibles de points d'entrée, et les références officielles de l'API manquent d'exemples appropriés.

Mehdi LAMRANI
la source
1
Syntaxe jungle. Oui. C'est le monde de Spark en ce moment.
conner.xyz
1

Une autre solution est la suivante:

1) Gardez "inferSchema" comme False

2) Lors de l'exécution des fonctions 'Map' sur la ligne, vous pouvez lire 'asString' (row.getString ...)

//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));
                }
            });
Vibha
la source
0
    val fact_df = df.select($"data"(30) as "TopicTypeId", $"data"(31) as "TopicId",$"data"(21).cast(FloatType).as( "Data_Value_Std_Err")).rdd
    //Schema to be applied to the table
    val fact_schema = (new StructType).add("TopicTypeId", StringType).add("TopicId", StringType).add("Data_Value_Std_Err", FloatType)

    val fact_table = sqlContext.createDataFrame(fact_df, fact_schema).dropDuplicates()
Aravind Krishnakumar
la source
0

Autrement:

// Generate a simple dataset containing five values and convert int to string type

val df = spark.range(5).select( col("id").cast("string")).withColumnRenamed("id","value")
user8106134
la source
0

Si vous souhaitez remplacer plusieurs colonnes d'un type spécifique par une autre sans spécifier de noms de colonnes individuels

/* Get names of all columns that you want to change type. 
In this example I want to change all columns of type Array to String*/
    val arrColsNames = originalDataFrame.schema.fields.filter(f => f.dataType.isInstanceOf[ArrayType]).map(_.name)

//iterate columns you want to change type and cast to the required type
val updatedDataFrame = arrColsNames.foldLeft(originalDataFrame){(tempDF, colName) => tempDF.withColumn(colName, tempDF.col(colName).cast(DataTypes.StringType))}

//display

updatedDataFrame.show(truncate = false)
Ravi
la source