如何在Spark SQL的DataFrame中更改列类型?


152

假设我正在做类似的事情:

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

但是我真的很想要yearas Int(也许可以转换其他一些列)。

我能想到的最好的是

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]

这有点令人费解。

我来自R,而且我习惯于写,例如

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

我可能会丢失一些东西,因为应该在Spark / Scala中有更好的方法来做到这一点...


我喜欢这种方式spark.sql(“ SELECT STRING(NULLIF(column,”))as column_string“)
Eric Bellet

Answers:


141

编辑:最新版本

从spark 2.x开始,您可以使用.withColumn。在这里检查文档:

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

最早的答案

从Spark 1.4版开始,您可以在列上将Cast方法与DataType一起应用:

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

如果您使用的是SQL表达式,也可以执行以下操作:

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

有关更多信息,请检查文档:http : //spark.apache.org/docs/1.6.0/api/scala/#org.apache.spark.sql.DataFrame


4
为什么在withColumn之后使用drop?仅使用带有原始列名的withColumn难道不是吗?
Ameba Spugnosa

@AmebaSpugnosa我认为,如果使用重复的列名,Spark会崩溃。不是在创建它们时,而是在使用它们时。
msemelman'8

5
无需删除列,然后重新命名。您可以在一行中进行操作df.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong

1
在这种情况下,是否创建了整个新的数据框副本只是为了重铸列?我想念什么吗?还是幕后有一些优化?
user1814008

5
由去文档Spark 2.xdf.withColumn(..)可以添加或更换取决于列colName的说法
2000年,shubham

89

[编辑:2016年3月:感谢您的投票!但实际上,这不是最好的答案,我想基础上的解决方案withColumnwithColumnRenamedcast通过msemelman提出,马丁Senne等是简单和清晰。

我认为您的方法还可以,请记住,Spark DataFrame是行的(不可变的)RDD,因此我们从来没有真正替换过列,只是DataFrame每次使用新的架构创建新列。

假设您有一个具有以下架构的原始df:

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)

在一列或几列上定义了一些UDF:

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
 )

更改列类型,甚至从另一个构建新的DataFrame都可以这样写:

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

产生:

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)

这非常接近您自己的解决方案。简单来说,将类型更改和其他转换保持为单独udf val的,可使代码更具可读性和重用性。


26
这既不安全也不高效。不安全,因为单个NULL或格式错误的条目将使整个作业崩溃。效率不高,因为UDF对Catalyst不透明。使用UDF进行复杂的操作就可以了,但是没有理由将它们用于基本类型转换。这就是为什么我们要使用cast方法(请参阅Martin Senne的答案)。使事物对Catalyst透明需要更多的工作,但基本的安全性只是放置TryOption工作的问题。
0323年

我没有看到任何与将字符串转换为日期相关的信息,例如“ 05-APR-2015”
dbspace

3
有没有一种方法可以将您的withColumn()部分缩减为遍历所有列的通用部分?
布尔恩

感谢zero323,在阅读本文时,我明白了为什么此处的udf解决方案会崩溃。有些评论比SO上的一些答案要好:)
Simon Dirmeier

有什么方法可以让我们了解损坏的行,这意味着在转换过程中记录中包含错误数据类型的列。由于
强制转换

65

由于该cast操作可用于Spark 的操作Column(而且我个人目前不赞成udf@提出的@ Svend),如何:

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

转换为请求的类型?作为一个纯净的副作用,在这种情况下,不可铸造/“可转换”的值将变为null

如果您需要此作为辅助方法,请使用:

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

用法如下:

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

2
如果我需要强制转换并重命名一整列列(我有50列,并且对scala来说还很陌生,不知道在不创建大量重复项的情况下最好的方法是什么),您能为我提供建议吗?有些列应保留为String,有些应强制转换为Float。
德米特里·斯米尔诺夫

如何将字符串转换为日期,例如列和“ 20160302”中的“ 25-APR-2016”
dbspace

@DmitrySmirnov你有没有得到答案?我也有同样的问题。;)
Evan Zamir

@EvanZamir不幸的是,我最终做了一堆操作,以便能够在其他步骤中像rdd一样使用数据。我想知道这几天是否变得容易了:)
德米特里·斯米尔诺夫

60

首先,如果您想强制转换类型,请执行以下操作:

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

具有相同列名的列将被新列替换。您无需执行添加和删除步骤。

其次,关于斯卡拉 VS [R
这是最类似于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)
   }: _*
)

虽然代码长度比R的长度略长。这与语言的冗长无关。在R中,R mutate是R数据帧的一项特殊功能,而在Scala中,由于其强大的表达能力,您可以轻松地临时创建一个。
换句话说,它避免了特定的解决方案,因为语言设计足以让您快速轻松地构建自己的领域语言。


旁注:df.columns令人惊讶地是,Array[String]而不是Array[Column],也许他们希望它看起来像Python pandas的数据框。


1
您能给pyspark等效吗?
Harit Vishwakarma

我正在为“年龄”字段获取“定义的非法开始”。withColumn(“ age”,$“ age” .cast(sql.types.DoubleType))。有什么建议吗?
BlueDolphin

如果出于性能原因在许多列上进行这些转换,是否必须对数据帧进行.cache(),还是因为Spark优化它们而不需要它?
Skjagini

可以先导入import org.apache.spark.sql.types._,然后再导入,而sql.types.IntegerType不仅仅是IntegerType
nessa.gp

17

您可以使用selectExpr它来使其更清洁:

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

14

Java代码,用于将DataFrame的数据类型从String修改为Integer

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

它将简单地将现有的(String数据类型)转换为Integer。


1
有没有DataTypessql.types!是DataType。而且,可以简单地导入IntegerType和转换。
Ehsan M. Kermani

@ EhsanM.Kermani实际上是DatyaTypes.IntegerType是合法的引用。
Cupitor

1
@Cupitor DataTypes.IntegerType以前处于DeveloperAPI模式,并且在v.2.1.0中保持稳定
Ehsan M. Kermani

这是最好的解决方案!
西蒙·迪迈尔

8

要将年份从字符串转换为int,可以向csv阅读器添加以下选项:“ inferSchema”->“ true”,请参见DataBricks文档


5
效果很好,但要注意的是读者必须对文件进行第二次通过
Beemyhalo 2015年

@beefyhalo绝对正确,有什么办法吗?
Ayush

6

因此,这仅在将问题保存到sqlserver之类的jdbc驱动程序时才有效,但是对于语法和类型会遇到的错误确实有帮助。

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)

您能帮我用Java实现相同的代码吗?以及如何将customJdbcDialect注册到DataFrame中
Abhijitcaps

不错,我在Vertica上也做过同样的事情,但是从spark 2.1开始。JDbcUtil您仅需要实现所需的特定数据类型。dialect.getJDBCType(dt).orElse(getCommonJDBCType(dt))。getOrElse(throw new IllegalArgumentException(s“ Ca n't get JDBC type for $ {dt.simpleString}”))
Arnon Rodman

6

生成一个包含五个值的简单数据集并转换intstring类型:

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

6

我认为这对我来说更具可读性。

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

这会将您的year列转换为IntegerType创建任何临时列并删除这些列。如果要转换为任何其他数据类型,可以检查org.apache.spark.sql.types包中的类型。


5

建议使用强制转换(FYI)的答案已被破坏,火花1.4.1中的强制转换方法已损坏。

例如,当字符串转换为bigint时,其字符串列的值为“ 8182175552014127960”的数据框的值为“ 8182175552014128100”

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

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

找到此错误之前,我们不得不面对很多问题,因为我们在生产中有bigint列。


4
psst,升级您的火花
msemelman

2
@msemelman必须在生产中升级到新版本的Spark以解决一个小错误,这很荒谬。
sauraI3h

我们不是总会升级所有小错误吗?:)
caesarsol


4

使用Spark Sql 2.4.0可以做到这一点:

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

3

您可以使用以下代码。

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

它将把年份列转换为IntegerType列。


2

此方法将删除旧列并创建具有相同值和新数据类型的新列。创建DataFrame时,我的原始数据类型是:

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

之后,我运行以下代码来更改数据类型:-

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

在此之后,我的结果是:

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

您能在这里提供您的解决方案吗?
Ajay Kharade

1

可以通过使用spark sql中的强制转换来更改列的数据类型。表名是表,只有两列,即column1和column2,column1的数据类型将被更改。ex-spark.sql(“从表中选择cast(column1作为Double)column1NewName,column2”)在double位置写入您的数据类型。


1

如果必须重命名其名称给定的几十个列,以下示例采用@dnlbrky的方法并将其一次应用于多个列:

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

未铸造的列保持不变。所有列均保持原始顺序。


1

这么多的答案,但没有太多详尽的解释

以下语法适用于将Databricks Notebook与Spark 2.4结合使用

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

请注意,您必须指定输入格式(在我的示例中为“ MM-dd-yyyy”),并且由于to_date是spark sql函数,因此必须强制导入

还尝试了这种语法,但是得到了null而不是适当的强制转换:

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

(请注意,我必须使用方括号和引号来使其在语法上正确无误)


PS:我必须承认这就像语法丛林一样,入口点有很多可能的方式,并且官方API引用缺少适当的示例。


1
语法丛林。是。这就是Spark的世界。
conner.xyz

1

另一个解决方案如下:

1)保持“ inferSchema”为假

2)在该行上运行“地图”功能时,您可以读取“ 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

其他方式:

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

如果您想将特定类型的多个列更改为另一个而不指定各个列的名称

/* 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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.