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

时间:2015-04-01 04:55:02

标签: scala apache-spark apache-spark-sql

假设我做了类似的事情:

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

但我真的希望yearInt(并且可能会转换其他一些列)。

我能想到的最好的是

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中执行此操作...

23 个答案:

答案 0 :(得分:122)

编辑:最新版本

自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版开始,您可以在列上应用带有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

答案 1 :(得分:87)

[编辑:2016年3月:感谢投票!虽然这确实不是最好的答案,但我认为msemelman,Martin Senne和其他人提出的基于withColumnwithColumnRenamedcast的解决方案更简单,更清晰。“ p>

我认为你的方法还可以,回想一下Spark DataFrame是一个(不可变的)ROD,所以我们从来没有真正替换一列,只是创建了新的{{ 1}}每次使用新架构。

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

DataFrame

一些UDF在一个或多个列上定义:

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)

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

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
 )

产生:

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) 可以使代码更具可读性和可重用性。

答案 2 :(得分:58)

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

答案 3 :(得分:39)

首先,如果你想要投射类型,那么:

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

使用相同的列名称,该列将替换为新列。您无需添加和删除步骤。

第二,关于Scala vs R. 这是与R I最相似的代码:

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中,mutate是R数据帧的特殊功能,而在Scala中,由于其强大的功能,您可以轻松地进行临时功能。
总之,它避免使用特定的解决方案,因为基础足以让您快速轻松地构建自己的域语言功能。

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

答案 4 :(得分:15)

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

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

答案 5 :(得分:9)

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

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

它只是将现有的(String数据类型)强制转换为Integer。

答案 6 :(得分:8)

要将年份从字符串转换为int,您可以将以下选项添加到csv阅读器:“inferSchema” - &gt; “true”,请参阅DataBricks documentation

答案 7 :(得分: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)

答案 8 :(得分:6)

生成包含五个值的简单数据集并将int转换为string类型:

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

答案 9 :(得分:5)

df.select($"long_col".cast(IntegerType).as("int_col"))

答案 10 :(得分:4)

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

例如,具有值&#34; 8182175552014127960&#34;的字符串列的数据帧;当铸造到bigint有价值&#34; 8182175552014128100&#34;

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

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

在发现这个错误之前我们不得不面对很多问题,因为我们在生产中有bigint列。

答案 11 :(得分: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)

答案 12 :(得分:2)

您可以使用以下代码。

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

列转换为IntegerType列。

答案 13 :(得分:2)

使用Spark Sql 2.4.0,您可以这样做:

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

答案 14 :(得分:1)

答案如此之多,解释不够详尽

以下语法适用于在Spark 2.4中使用Databricks Notebook

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函数

也尝试过这种语法,但是得到的是空值,而不是适当的强制转换:

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

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


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

答案 15 :(得分:1)

为什么不按照http://spark.apache.org/docs/latest/api/python/pyspark.sql.html#pyspark.sql.Column.cast

中的描述进行操作
df.select(df.year.cast("int"),"make","model","comment","blank")

答案 16 :(得分:1)

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

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

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

答案 17 :(得分:0)

如果必须重命名其名称给定的数十个列,以下示例采用@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
}):_*)

未转换的列保持不变。所有列均保持原始顺序。

答案 18 :(得分:0)

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>

答案 19 :(得分:0)

可以通过在spark sql中使用强制转换来更改列的数据类型。 表名是表,它有两列只有column1和column2,column1数据类型要更改。 ex-spark.sql(&#34; select cast(column1 as Double)column1NewName,column2 from table&#34;) 代替双重写入您的数据类型。

答案 20 :(得分: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)

答案 21 :(得分:-1)

另一种方式:

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

答案 22 :(得分:-1)

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