Как изменить типы столбцов в DataFrame Spark SQL?

124

Предположим, что я делаю что-то вроде:

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

но я действительно хотел year как 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)

Скорее всего, я что-то пропустил, так как в искрах / scala... должен быть лучший способ сделать это...

Теги:
apache-spark
apache-spark-sql

16 ответов

110

Редактировать: новейшая версия

Начиная с 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

  • 3
    почему вы использовали withColumn с последующим отбрасыванием? Не проще ли просто использовать withColumn с оригинальным именем столбца?
  • 0
    @AmebaSpugnosa Я думаю, что к тому времени, когда я использовал его, Spark потерпел крах, если у него были повторенные имена столбцов. Не когда вы их создаете, а когда вы их используете.
Показать ещё 6 комментариев
81

[EDIT: март 2016: спасибо за голоса! Хотя на самом деле это не лучший ответ, я думаю, что решения, основанные на withColumn, withColumnRenamed и cast, выдвинутые msemelman, Martin 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, сделать код более читаемым и повторно использоваться.

  • 16
    Это не безопасно и не эффективно. Не безопасно, потому что одна NULL или неправильная запись может привести к сбою всей работы. Не эффективен , потому что UDFs не является прозрачным для катализатора. Использование UDF для сложных операций - это нормально, но нет причин использовать их для приведения базовых типов. Это то, почему мы cast метод (см ответа на Мартине Senne ). Чтобы сделать Catalyst прозрачным, требуется больше работы, но базовая безопасность - это всего лишь вопрос применения Try and Option .
  • 0
    Я не видел ничего связанного с преобразованием строки в дату, например "05-APR-2015"
Показать ещё 2 комментария
56

Поскольку операция 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.
  • 0
    как преобразовать строку в дату, например, «25 апреля 2016 года» в столбце и «20160302»
Показать ещё 2 комментария
31

Сначала, если вы хотите создать тип

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

С таким же именем столбца столбец будет заменен новым, вам не нужно добавлять и удалять.

Во-вторых, около Scala vs R. код Scala, наиболее похожий на R, который я могу достичь:

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. Обратите внимание, что mutate является функцией для кадра данных R, поэтому Scala очень хорош в выразительной мощности, заданной без использования специальной функции.

(df.columns на удивление представляет собой Array [String] вместо Array [Column], возможно, они хотят, чтобы он выглядел как кадр данных Python pandas.)

  • 1
    Не могли бы вы дать эквивалент для pyspark?
  • 0
    Я получаю "незаконное начало определения" .withColumn ("age", $ "age" .cast (sql.types.DoubleType)) для моего поля "age". Любое предложение?
Показать ещё 1 комментарий
15

Вы можете использовать selectExpr, чтобы сделать его немного чище:

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

Java-код для изменения типа данных DataFrame от String до Integer

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

Он просто преобразует существующий (тип данных String) в Integer.

  • 1
    В DataTypes нет DataTypes sql.types ! это DataType . Кроме того, можно просто импортировать IntegerType и привести.
  • 0
    @ EhsanM.Kermani на самом деле DatyaTypes.IntegerType является законной ссылкой.
Показать ещё 2 комментария
8

Чтобы преобразовать год из строки в int, вы можете добавить в csv-считыватель следующую опцию: "inferSchema" → "true", см. Документация DataBricks

  • 5
    Это работает хорошо, но суть в том, что читатель должен сделать второй проход вашего файла
  • 0
    @beefyhalo абсолютно точно, есть ли способ обойти это?
6

Создайте простой набор данных, содержащий пять значений, и преобразуйте int в string тип:

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

Итак, это действительно работает, если у вас есть проблемы с сохранением драйвера jdbc, такого как sqlserver, но он действительно полезен для ошибок, с которыми вы столкнетесь с синтаксисом и типами.

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)
  • 0
    Можете ли вы помочь мне реализовать тот же код в Java? и как зарегистрировать customJdbcDialect в DataFrame
  • 0
    Хорошо, что я сделал то же самое с Vertica, но с тех пор, как спарк 2.1. JDbcUtil вам нужно реализовать только тот тип данных, который вам нужен. dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)). getOrElse (выдает новое исключение IllegalArgumentException (s "Невозможно получить тип JDBC для $ {dt.simpleString}"))
4
df.select($"long_col".cast(IntegerType).as("int_col"))
3

Вы можете использовать приведенный ниже код.

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

Который будет конвертировать столбец год в столбец IntegerType.

3

ответы, предлагающие использовать литые, FYI, метод литья в искре 1.4.1 нарушен.

например, фреймворк данных со строковым столбцом, имеющим значение "8182175552014127960" при передаче в bigint, имеет значение "8182175552014128100"

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

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

Нам пришлось столкнуться с большим количеством проблем, прежде чем найти эту ошибку, потому что у нас были большие столбцы в производстве.

  • 4
    psst, обнови свою искру
  • 1
    @msemelman смешно, чтобы из-за небольшой ошибки обновиться до новой версии spark в производстве.
Показать ещё 2 комментария
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)
  • 0
    Не могли бы вы предоставить свое решение здесь.
0

Можно изменить тип данных столбца, используя метод cast in spark sql. имя таблицы - это таблица, и в ней есть только два столбца: только столбцы column1 и column2 и тип данных столбца1 должны быть изменены. ex-spark.sql( "select cast (column1 as Double) column1NewName, column2 из таблицы" ) Вместо двойной записи введите свой тип данных.

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню