Come posso cambiare i tipi di colonna nel DataFrame di Spark SQL?


152

Supponiamo che stia facendo qualcosa del tipo:

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

Ma volevo davvero il yearas Int(e forse trasformare alcune altre colonne).

Il meglio che potevo inventare era

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]

che è un po 'contorto.

Vengo da R e sono abituato a scrivere, ad es

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

Probabilmente mi manca qualcosa, dal momento che ci dovrebbe essere un modo migliore per farlo in Spark / Scala ...


Mi piace in questo modo spark.sql ("SELECT STRING (NULLIF (column, '')) as column_string")
Eric Bellet

Risposte:


141

Modifica: versione più recente

Da spark 2.x puoi usare .withColumn. Controlla i documenti qui:

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

Risposta più antica

Dalla versione 1.4 di Spark è possibile applicare il metodo cast con DataType sulla colonna:

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

Se stai usando espressioni sql puoi anche fare:

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

Per maggiori informazioni consulta i documenti: http://spark.apache.org/docs/1.6.0/api/scala/#org.apache.spark.sql.DataFrame


4
perché hai usato conColumn seguito da drop? Non è più facile da usare conColumn con il nome della colonna originale?
Ameba Spugnosa,

@AmebaSpugnosa Penso che quando l'ho usato Spark si è schiantato se aveva ripetuto i nomi delle colonne. Non quando li crei, ma quando li usi.
msemelman,

5
non è necessario eliminare la colonna seguita da una ridenominazione. Puoi fare in una sola rigadf.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong,

1
In questo caso viene creata un'intera nuova copia del frame di dati solo per rifondere una colonna? Mi sto perdendo qualcosa? O forse c'è qualche ottimizzazione dietro le quinte?
user1814008

5
Seguendo i documenti di Spark 2.x, è df.withColumn(..)possibile aggiungere o sostituire una colonna in base colNameall'argomento
y2k-shubham,

89

[EDIT: marzo 2016: grazie per i voti! Anche se in realtà, questa non è la migliore risposta, credo che le soluzioni basate su withColumn, withColumnRenamede castavanzata dal msemelman, Martin Senne e gli altri sono più semplici e più pulito].

Penso che il tuo approccio sia ok, ricorda che uno Spark DataFrameè un RDD (immutabile) di righe, quindi non stiamo mai sostituendo una colonna, ma ne creiamo di nuove DataFrameogni volta con un nuovo schema.

Supponendo di avere un df originale con il seguente schema:

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)

E alcuni UDF sono definiti su una o più colonne:

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
 )

La modifica dei tipi di colonna o persino la creazione di un nuovo DataFrame da un altro può essere scritta in questo modo:

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")            

che produce:

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)

Questo è abbastanza vicino alla tua soluzione. Semplicemente, mantenendo le modifiche al tipo e altre trasformazioni come udf vals separate , il codice diventa più leggibile e riutilizzabile.


26
Questo non è né sicuro né efficiente. Non sicuro perché una NULLvoce singola o non valida arresta in modo anomalo un intero lavoro. Non efficiente perché gli UDF non sono trasparenti per Catalyst. L'uso di UDF per operazioni complesse va bene, ma non c'è motivo di usarli per il cast di tipo base. Questo è il motivo per cui abbiamo il castmetodo (vedi una risposta di Martin Senne ). Rendere le cose trasparenti per Catalyst richiede più lavoro, ma la sicurezza di base è solo una questione di mettere Trye Optionlavorare.
zero323

Non ho visto nulla di correlato alla conversione della stringa in data, ad esempio "05-APR-2015"
dbspace

3
C'è un modo per ridurre la tua withColumn()sezione a una generica che scorre attraverso tutte le colonne?
Boern,

Grazie zero323, dopo aver letto questo ho capito perché la soluzione udf qui si blocca. Alcuni commenti sono migliori di alcune risposte su SO :)
Simon Dirmeier,

Esiste un modo in cui possiamo conoscere la riga corrotta, significa record che hanno colonne di tipi di dati errati durante il cast. Come funzione di cast rende nulli quei campi
Etisha,

65

Poiché l' castoperazione è disponibile per Spark Column(e poiché personalmente non favorisco udfquella proposta da @ Svenda questo punto), che ne dici di:

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

trasmettere al tipo richiesto? Come effetto collaterale, diventeranno valori non calcinabili / "convertibili" in tal senso null.

Nel caso in cui sia necessario come metodo di supporto , utilizzare:

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

che viene utilizzato come:

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

2
Puoi darmi dei consigli su come procedere, se ho bisogno di eseguire il cast e rinominare un intero gruppo di colonne (ho 50 colonne e abbastanza nuovo per Scala, non sei sicuro di quale sia il modo migliore per affrontarlo senza creare una duplicazione di massa)? Alcune colonne dovrebbero rimanere String, alcune dovrebbero essere lanciate su Float.
Dmitry Smirnov,

come convertire una stringa in una data, ad esempio "25-APR-2016" nella colonna e "20160302"
dbspace

@DmitrySmirnov Hai mai ricevuto una risposta? Ho la stessa domanda ;)
Evan Zamir il

@EvanZamir no, sfortunatamente, ho finito per fare uno shitton di operazioni per poter usare i dati come rdd in altri passaggi. Mi chiedo se questo sia diventato più facile in questi giorni :)
Dmitry Smirnov

60

Per prima cosa , se vuoi lanciare il tipo, allora questo:

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

Con lo stesso nome di colonna, la colonna verrà sostituita con una nuova. Non è necessario aggiungere e cancellare passaggi.

In secondo luogo , sulla Scala vs R .
Questo è il codice che il più simile al RI può inventare:

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

Sebbene la lunghezza del codice sia leggermente più lunga di quella di R. Questo non ha nulla a che fare con la verbosità della lingua. In R mutateè una funzione speciale per R dataframe, mentre in Scala puoi facilmente farlo ad hoc grazie al suo potere espressivo.
In poche parole, evita soluzioni specifiche, perché il design della lingua è abbastanza buono da permetterti di creare rapidamente e facilmente la tua lingua di dominio.


nota a margine : df.columnsè sorprendentemente un Array[String]invece di Array[Column], forse vogliono che assomigli al frame di dati di Python Panda.


1
Potresti per favore fornire l'equivalente di pyspark?
Harit Vishwakarma,

Ricevo "inizio illegale della definizione" .withColumn ("age", $ "age" .cast (sql.types.DoubleType)) per il mio campo "age". Qualche suggerimento?
BlueDolphin,

Devi .cache () il frame di dati se stiamo eseguendo queste conversioni su molte colonne per motivi di prestazioni, o non è necessario poiché Spark le ottimizza?
skjagini,

L'importazione può essere import org.apache.spark.sql.types._e quindi anziché sql.types.IntegerTypesolo IntegerType.
nessa.gp

17

Puoi usarlo selectExprper renderlo un po 'più pulito:

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

14

Codice Java per la modifica del tipo di dati del DataFrame da String a Integer

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

Trasmetterà semplicemente l'attuale (tipo di dati String) a Integer.


1
Non c'è DataTypesdentro sql.types! lo è DataType. Inoltre, si può semplicemente importare IntegerTypee trasmettere.
Ehsan M. Kermani,

@ EhsanM.Kermani in realtà DatyaTypes.IntegerType è un riferimento legittimo.
Cupitor,

1
@Cupitor DataTypes.IntegerTypeera in modalità DeveloperAPI ed è stabile nella versione 2.1.0
Ehsan M. Kermani,

Questa è la soluzione migliore!
Simon Dirmeier,

8

Per convertire l'anno da stringa a int, è possibile aggiungere la seguente opzione al lettore csv: "inferSchema" -> "true", consultare la documentazione di DataBricks


5
Funziona bene, ma il trucco è che il lettore deve fare un secondo passaggio del tuo file
beefyhalo

@beefyhalo è assolutamente perfetto, c'è un modo per aggirare questo?
Ayush,

6

Quindi funziona davvero solo se riscontri problemi con il salvataggio su un driver jdbc come sqlserver, ma è davvero utile per gli errori che incontrerai con la sintassi e i tipi.

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)

Potete aiutarmi a implementare lo stesso codice in Java? e come registrare customJdbcDialect in DataFrame
abhijitcaps

Bello, ho fatto lo stesso con Vertica, ma dalla scintilla 2.1. JDbcUtil è necessario implementare solo il tipo di dati specifico necessario. dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)). getOrElse (lancia nuovo IllegalArgumentException (s "Impossibile ottenere il tipo JDBC per $ {dt.simpleString}"))
Arnon Rodman,

6

Genera un semplice set di dati contenente cinque valori e converti intin stringtipo:

val df = spark.range(5).select( col("id").cast("string") )

6

Penso che questo sia molto più leggibile per me.

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

Questo convertirà la colonna dell'anno IntegerTypecon la creazione di eventuali colonne temporanee e l'eliminazione di tali colonne. Se si desidera convertire in qualsiasi altro tipo di dati, è possibile verificare i tipi all'interno del org.apache.spark.sql.typespacchetto.


5

le risposte che suggeriscono di usare cast, FYI, il metodo cast in spark 1.4.1 è rotto.

ad esempio, un frame di dati con una colonna stringa con valore "8182175552014127960" quando eseguito il cast su bigint ha valore "8182175552014128100"

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

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

Abbiamo dovuto affrontare molti problemi prima di trovare questo bug perché avevamo colonne bigint in produzione.


4
psst, aggiorna il tuo spark
msemelman

2
@msemelman è ridicolo dover aggiornare a una nuova versione di Spark in produzione per un piccolo bug.
sauraI3h,

non aggiorniamo sempre tutto per piccoli bug? :)
caesarsol,


4

Utilizzando Spark Sql 2.4.0 puoi farlo:

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

3

Puoi usare il codice qui sotto.

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

Che convertirà anno colonna in IntegerTypecolonna.


2

Questo metodo eliminerà la vecchia colonna e creerà nuove colonne con gli stessi valori e il nuovo tipo di dati. I miei tipi di dati originali al momento della creazione del DataFrame erano: -

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

Dopo questo ho eseguito il seguente codice per modificare il tipo di dati: -

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

Dopo questo il mio risultato è risultato essere: -

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

Potresti fornire la tua soluzione qui.
Ajay Kharade,

1

Si può cambiare il tipo di dati di una colonna usando cast in spark sql. il nome della tabella è table e ha due colonne: il tipo di dati column1 e column2 e column1 deve essere modificato. ex-spark.sql ("seleziona cast (column1 come Double) column1NewName, column2 dalla tabella") Al posto di double scrivi il tuo tipo di dati.


1

Nel caso in cui tu debba rinominare dozzine di colonne date dal loro nome, il seguente esempio prende l'approccio di @dnlbrky e lo applica a più colonne contemporaneamente:

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

Le colonne non registrate rimangono invariate. Tutte le colonne rimangono nel loro ordine originale.


1

Così tante risposte e non molte spiegazioni approfondite

La seguente sintassi funziona usando Databricks Notebook con Spark 2.4

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

Nota che devi specificare il formato di inserimento che hai (nel mio caso "MM-gg-aaaa") e l'importazione è obbligatoria in quanto to_date è una funzione spark sql

Ho anche provato questa sintassi ma ottenuto valori nulli anziché un cast appropriato:

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

(Nota che ho dovuto usare parentesi e virgolette per essere sintatticamente corretto però)


PS: Devo ammettere che è come una giungla di sintassi, ci sono molti modi in cui i punti di ingresso e i riferimenti API ufficiali mancano di esempi corretti.


1
Sintassi giungla. Sì. Questo è il mondo di Spark in questo momento.
conner.xyz,

1

Un'altra soluzione è la seguente:

1) Mantenere "inferSchema" come False

2) Durante l'esecuzione delle funzioni 'Mappa' sulla riga, puoi leggere '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));
                }
            });


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

0

Un altro modo:

// 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")

0

Nel caso in cui si desideri modificare più colonne di un tipo specifico in un altro senza specificare i nomi delle singole colonne

/* 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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.