¿Cómo puedo cambiar los tipos de columna en el DataFrame de Spark SQL?

152

Supongamos que estoy haciendo algo como:

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

Pero realmente quería el yearas Int(y quizás transformar algunas otras columnas).

Lo mejor que se me ocurrió fue

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]

lo cual es un poco complicado.

Vengo de R y estoy acostumbrado a poder escribir, p. Ej.

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

Es probable que me falte algo, ya que debería haber una mejor manera de hacer esto en Spark / Scala ...

kevinykuo
fuente
Me gusta de esta manera spark.sql ("SELECT STRING (NULLIF (column, '')) as column_string")
Eric Bellet

Respuestas:

141

Editar: versión más nueva

Desde spark 2.x puedes usar .withColumn. Consulta los documentos aquí:

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

Respuesta más antigua

Desde Spark versión 1.4, puede aplicar el método de conversión con DataType en la columna:

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

Si está utilizando expresiones sql, también puede hacer:

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

Para obtener más información, consulte los documentos: http://spark.apache.org/docs/1.6.0/api/scala/#org.apache.spark.sql.DataFrame

msemelman
fuente
44
¿Por qué usaste con Columna seguido de soltar? ¿No es más fácil usar con Columna con el nombre de columna original?
Ameba Spugnosa
@AmebaSpugnosa Creo que cuando lo usé Spark se bloqueó si tenía nombres de columna repetidos. No cuando los creas, sino cuando los usas.
msemelman
55
no es necesario soltar la columna seguida de un cambio de nombre. Puedes hacerlo en una líneadf.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong el
1
¿Se crea una copia de marco de datos completamente nueva solo para refundir una columna en este caso? ¿Me estoy perdiendo de algo? ¿O tal vez hay alguna optimización detrás de escena?
user1814008
55
Siguiendo los documentos de Spark 2.x, df.withColumn(..)puede agregar o reemplazar una columna según el colNameargumento
y2k-shubham
89

[EDITAR: marzo de 2016: gracias por los votos! Aunque en realidad, esto no es la mejor respuesta, creo que las soluciones basadas en withColumn, withColumnRenamedy castpresentada por msemelman, Martin Senne y otros son más simples y más limpio].

Creo que su enfoque está bien, recuerde que un Spark DataFramees un RDD (inmutable) de filas, por lo que nunca estamos reemplazando realmente una columna, solo creando nuevas DataFramecada vez con un nuevo esquema.

Suponiendo que tiene un df original con el siguiente esquema:

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)

Y algunos UDF definidos en una o varias columnas:

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
 )

Cambiar los tipos de columna o incluso crear un nuevo DataFrame a partir de otro se puede escribir de esta manera:

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

cuyos rendimientos:

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)

Esto está bastante cerca de su propia solución. Simplemente, mantener los cambios de tipo y otras transformaciones como elementos separados udf valhace que el código sea más legible y reutilizable.

Svend
fuente
26
Esto no es seguro ni eficiente. No es seguro porque una NULLentrada única o con formato incorrecto bloqueará un trabajo completo. No es eficiente porque los UDF no son transparentes para Catalyst. El uso de UDF para operaciones complejas está bien, pero no hay razón para usarlos para la conversión de tipos básicos. Por eso tenemos castmétodo (ver una respuesta de Martin Senne ). Hacer las cosas transparentes para Catalyst requiere más trabajo, pero la seguridad básica es solo cuestión de poner Tryy Optiontrabajar.
zero323
No vi nada relacionado con la conversión de cadenas a la fecha, por ejemplo, "05-APR-2015"
dbspace
3
¿Hay alguna manera de reducir su withColumn()sección a una genérica que recorra todas las columnas?
Boern
Gracias zero323, al leer esto, me di cuenta de por qué la solución de udf aquí falla. Algunos comentarios son mejores que algunas respuestas sobre SO :)
Simon Dirmeier
¿Hay alguna forma de conocer la fila corrupta? Significa registros que tienen columnas de tipos de datos incorrectos durante la conversión. Como la función de
conversión
65

Como la castoperación está disponible para Spark Column(y como yo personalmente no estoy a favor udfde la propuesta por @ Svenden este momento), ¿qué tal si:

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

para emitir al tipo solicitado? Como un efecto secundario ordenado, se convertirán en valores no moldeables / "convertibles" en ese sentido null.

En caso de que necesite esto como método auxiliar , use:

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

que se usa como:

import DFHelper._
val df2 = castColumnTo( df, "year", IntegerType )
Martin Senne
fuente
2
¿Me puede aconsejar sobre cómo proceder, si necesito emitir y cambiar el nombre de un grupo completo de columnas (tengo 50 columnas, y bastante nuevo en scala, no estoy seguro de cuál es la mejor manera de abordarlo sin crear una duplicación masiva)? Algunas columnas deben permanecer String, algunas deben lanzarse a Float.
Dmitry Smirnov
cómo convertir una cadena en una fecha, por ejemplo, "25-ABR-2016" en la columna y "20160302"
dbspace
@DmitrySmirnov ¿Alguna vez recibiste una respuesta? Tengo la misma pregunta. ;)
Evan Zamir
@EvanZamir desafortunadamente no, terminé haciendo un montón de operaciones para poder usar datos como rdd en otros pasos. Me pregunto si esto se volvió más fácil en estos días :)
Dmitry Smirnov
60

Primero , si quieres emitir un tipo, entonces esto:

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

Con el mismo nombre de columna, la columna será reemplazada por una nueva. No es necesario agregar ni eliminar pasos.

En segundo lugar , sobre Scala vs R .
Este es el código que más parecido a RI puede tener:

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

Aunque la longitud del código es un poco más larga que la de R. Eso no tiene nada que ver con la verbosidad del lenguaje. En R the mutatees una función especial para el marco de datos R, mientras que en Scala puede ad-hoc fácilmente gracias a su poder expresivo.
En resumen, evita soluciones específicas, porque el diseño del lenguaje es lo suficientemente bueno para que pueda construir rápida y fácilmente su propio idioma de dominio.


nota al margen: df.columnses sorprendentemente un en Array[String]lugar de Array[Column], tal vez quieren que se vea como el marco de datos de los pandas de Python.

WeiChing 林 煒 清
fuente
1
¿Podría dar el equivalente de pyspark?
Harit Vishwakarma
Estoy obteniendo "inicio de definición ilegal" .withColumn ("age", $ "age" .cast (sql.types.DoubleType)) para mi campo "age". ¿Cualquier sugerencia?
BlueDolphin
¿Tiene que .cache () el marco de datos si estamos haciendo estas conversiones en muchas columnas por razones de rendimiento, o no es necesario ya que Spark las optimiza?
skjagini
La importación puede ser import org.apache.spark.sql.types._y luego en lugar de sql.types.IntegerTypesolo IntegerType.
nessa.gp
17

Puedes usar selectExprpara hacerlo un poco más limpio:

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

Código Java para modificar el tipo de datos del DataFrame de String a Integer

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

Simplemente lanzará el (tipo de datos de cadena) existente a Integer.

manishbelsare
fuente
1
No hay DataTypesadentro sql.types! es DataType. Además, uno simplemente puede importar IntegerTypey emitir.
Ehsan M. Kermani
@ EhsanM.Kermani en realidad DatyaTypes.IntegerType es una referencia legítima.
Cupitor
1
@Cupitor DataTypes.IntegerTypesolía estar en modo DeveloperAPI y es estable en v.2.1.0
Ehsan M. Kermani
¡Esta es la mejor solución!
Simon Dirmeier
8

Para convertir el año de cadena a int, puede agregar la siguiente opción al lector csv: "inferSchema" -> "true", consulte la documentación de DataBricks

Peter Rose
fuente
55
Esto funciona bien, pero el problema es que el lector debe hacer una segunda pasada de su archivo
beefyhalo
@beefyhalo es perfecto, ¿hay alguna forma de evitarlo?
Ayush
6

Entonces, esto realmente solo funciona si tiene problemas para guardar en un controlador jdbc como sqlserver, pero es realmente útil para los errores con los que se encontrará con la sintaxis y los tipos.

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
fuente
¿Me pueden ayudar a implementar el mismo código en Java? y cómo registrar el customJdbcDialect en DataFrame
abhijitcaps
Agradable, hice lo mismo con Vertica, pero desde la chispa 2.1. JDbcUtil necesita implementar solo el tipo de datos específico que necesita. dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)). getOrElse (lanzar una nueva IllegalArgumentException (s "No se puede obtener el tipo JDBC para $ {dt.simpleString}"))
Arnon Rodman
6

Genere un conjunto de datos simple que contenga cinco valores y conviértalo inta stringtipo:

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

Creo que esto es mucho más legible para mí.

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

Esto convertirá su columna de año en IntegerTypecrear columnas temporales y soltar esas columnas. Si desea convertir a cualquier otro tipo de datos, puede verificar los tipos dentro del org.apache.spark.sql.typespaquete.

Piyush Patel
fuente
5

las respuestas sugieren usar cast, para su información, el método de lanzamiento en spark 1.4.1 está roto.

por ejemplo, un marco de datos con una columna de cadena que tiene el valor "8182175552014127960" cuando se convierte en bigint tiene el valor "8182175552014128100"

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

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

Tuvimos que enfrentar muchos problemas antes de encontrar este error porque teníamos grandes columnas en producción.

sauraI3h
fuente
44
psst, actualiza tu chispa
msemelman
2
@msemelman es ridículo tener que actualizar a una nueva versión de spark en producción por un pequeño error.
sauraI3h
¿No siempre actualizamos todo para pequeños errores? :)
caesarsol
5
df.select($"long_col".cast(IntegerType).as("int_col"))
almamaquina
fuente
4

Usando Spark Sql 2.4.0 puede hacer eso:

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

Puedes usar el siguiente código.

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

Lo que convertirá año columna en IntegerTypecolumna.

fuerte
fuente
2

Este método eliminará la columna anterior y creará nuevas columnas con los mismos valores y un nuevo tipo de datos. Mis tipos de datos originales cuando se creó el DataFrame fueron: -

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

Después de esto, ejecuté el siguiente código para cambiar el tipo de datos: -

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

Después de esto, mi resultado resultó ser: -

root
 |-- id: integer (nullable = true)
 |-- flag2: string (nullable = true)
 |-- name: string (nullable = true)
 |-- flag1: boolean (nullable = true)
 |-- flag3: boolean (nullable = true)
PirateJack
fuente
¿Podría proporcionar su solución aquí?
Ajay Kharade
1

Se puede cambiar el tipo de datos de una columna utilizando cast in spark sql. el nombre de la tabla es table y solo tiene dos columnas column1 y column2 y el tipo de datos column1 se debe cambiar. ex-spark.sql ("seleccione cast (column1 como Double) column1NewName, column2 from table") En lugar de double escriba su tipo de datos.

Tejasvi Sharma
fuente
1

En caso de que tenga que cambiar el nombre de docenas de columnas dadas por su nombre, el siguiente ejemplo toma el enfoque de @dnlbrky y lo aplica a varias columnas a la vez:

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

Las columnas no emitidas se mantienen sin cambios. Todas las columnas permanecen en su orden original.

lechuga cubica
fuente
1

Tantas respuestas y pocas explicaciones exhaustivas

La siguiente sintaxis funciona con Databricks Notebook con Spark 2.4

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

Tenga en cuenta que debe especificar el formato de entrada que tiene (en mi caso "MM-dd-aaaa") y la importación es obligatoria ya que to_date es una función de sql spark

También probé esta sintaxis pero obtuve nulos en lugar de un reparto adecuado:

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

(Tenga en cuenta que tuve que usar corchetes y citas para que sea sintácticamente correcto)


PD: Tengo que admitir que esto es como una jungla de sintaxis, hay muchas formas posibles de puntos de entrada, y las referencias oficiales de la API carecen de ejemplos adecuados.

Mehdi LAMRANI
fuente
1
Sintaxis Jungla. Si. Este es el mundo de Spark en este momento.
conner.xyz
1

Otra solución es la siguiente:

1) Mantenga "inferSchema" como falso

2) Mientras ejecuta las funciones 'Mapa' en la fila, puede leer '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
fuente
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
fuente
0

De otra manera:

// 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")
usuario8106134
fuente
0

En caso de que desee cambiar varias columnas de un tipo específico a otro sin especificar nombres de columnas individuales

/* 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
fuente