Як я можу змінити типи стовпців у DataFrame Spark SQL?


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

Але мені дуже хотілося 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)

Я, ймовірно, щось пропускаю, оскільки має бути кращий спосіб зробити це в Spark / Scala ...


Мені подобається такий спосіб spark.sql ("ВИБІРТИ СТРИНГ (NULLIF (стовпець, '')) як стовпчик")
Ерік Беллет

Відповіді:


141

Правка: найновіша версія

Оскільки іскру 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 з подальшим падінням? Чи не простіше просто використовувати withColumn з початковою назвою стовпця?
Ameba Spugnosa

@AmebaSpugnosa Я думаю, що до моменту його використання Спарк зазнав аварії, якщо він повторював назви стовпців. Не тоді, коли ви їх створюєте, а коли ви їх використовуєте.
msemelman

5
немає необхідності скидати стовпчик з наступним перейменуванням. Можна зробити в один рядокdf.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong

1
Чи створена ціла нова копія фрейму даних просто для того, щоб переробляти стовпець у цьому випадку? Я щось пропускаю? Чи, можливо, є якась оптимізація за лаштунками?
користувач1814008

5
Судячи з документації на Spark 2.x, df.withColumn(..)можна додати або замінити стовпець в залежності від colNameаргументу
Y2K-Shubham

89

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


26
Це ні безпечно, ні ефективно. Небезпечно, тому що один NULLабо неправильно сформований запис призведе до збою всієї роботи. Чи не ефективний , тому що UDFs не є прозорим для каталізатора. Використовувати UDF для складних операцій просто чудово, але немає причин використовувати їх для кастингу базового типу. Ось чому ми маємо castметод (див. Відповідь Мартіна Сенна ). Здійснення прозорості для Catalyst потребує більше роботи, але основна безпека - лише питання роботи Tryта Optionроботи.
нуль323

Я не бачив нічого, пов’язаного з перетворенням рядка на дату, наприклад "05-APR-2015"
dbspace

3
Чи є спосіб зменшити свій withColumn()розділ до загального, який повторюється через усі стовпці?
Борн

Дякую zero323, прочитавши це, я зрозумів, чому тут відбувається збій рішення udf. Деякі коментарі є кращими, ніж відповіді на SO :)
Simon Dirmeier

Чи є спосіб, яким ми можемо ознайомитись із корумпованим рядком, означає записи, які мають стовпці неправильних типів даних під час кастингу. Як функція лиття робить ці поля нульовими
Етіша,

65

Оскільки castоперація доступна для Spark Column's (і, як я особисто не віддаю перевагу 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 стовпців, і досить новий для масштабування, не впевнений, який найкращий спосіб підійти до нього, не створюючи масового дублювання)? Деякі стовпці повинні залишатись String, деякі - передати на Float.
Дмитро Смирнов

як перетворити рядок у дату, наприклад "25 квітня 2016" у стовпці та "20160302"
dbspace

@DmitrySmirnov Ви коли-небудь отримували відповідь? У мене те саме питання. ;)
Еван Замір

@EvanZamir, на жаль, ні, я в кінцевому підсумку робив шитт операцій, щоб мати можливість використовувати дані як rdd в інших кроках. Цікаво, чи стало простіше в ці дні :)
Дмитро Смирнов

60

По-перше , якщо ви хочете ввести тип, то це:

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

З такою ж назвою стовпця стовпчик буде замінено новим. Вам не потрібно робити кроки додавання та видалення.

По- друге , про Scala проти 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 mutate- це спеціальна функція для R фрейму даних, тоді як у Scala ви можете легко отримати спеціальну функцію завдяки своїй виразній силі.
Словом, уникайте конкретних рішень, оскільки мовна конструкція досить хороша для того, щоб швидко та легко побудувати власну мову домену.


бічне зауваження: df.columnsнапрочуд, Array[String]замість цього Array[Column], можливо, вони хочуть, щоб він виглядав як фрейм фреймів панд Python.


1
Скажіть, будь ласка, еквівалент pyspark?
Харіт Вішвакарма

Я отримую "незаконний початок визначення" .withColumn ("вік", $ "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
Там немає DataTypesв sql.types! це DataType. Більше того, їх можна просто імпортувати IntegerTypeта подавати.
Ехсан М. Кермані

@ EhsanM.Kermani насправді DatyaTypes.IntegerType є законним посиланням.
Купітор

1
@Cupitor DataTypes.IntegerTypeраніше знаходився в режимі DeveloperAPI і стабільний в v.2.1.0
Ehsan M. Kermani

Це найкраще рішення!
Саймон Дірмайє


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)

Чи можете ви допомогти мені реалізувати той самий код на Java? і як зареєструвати customJdbcDialect у DataFrame
abhijitcaps

Приємно, що я зробив те саме з Vertica, але з іскри 2.1. JDbcUtil вам потрібно реалізувати лише певний тип даних, який вам потрібен. dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)). getOrElse (киньте новий IllegalArgumentException (s "Не вдається отримати тип JDBC за $ {dt.simpleString}"))
Арнон Родман

6

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

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

6

Я думаю, що це набагато читабельніше для мене.

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

Це перетворить ваш стовпець року в IntegerTypeстворення будь-яких тимчасових стовпців і випадання цих стовпців. Якщо ви хочете перетворити на будь-який інший тип даних, ви можете перевірити типи всередині org.apache.spark.sql.typesпакету.


5

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

наприклад, фрейм даних із стовпчиком рядка, що має значення "8182175552014127960", коли передається на bigint, має значення "8182175552014128100"

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

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

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


4
psst, оновіть свою іскру
msemelman

2
@msemelman це смішно, щоб перейти на нову версію іскри у виробництві для невеликої помилки.
sauraI3h

чи не завжди ми все оновлюємо для невеликих помилок? :)
цезарсол


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)

Чи можете ви надати тут своє рішення.
Аджай Хараде

1

Можна змінити тип даних стовпця, використовуючи "cast in spark sql". Ім'я таблиці - це таблиця, і вона має два стовпці тільки колонку1 та тип2 та колонку1. ex-spark.sql ("виберіть кастинг (стовпець1 як подвійний) колонку1NewName, колонку2 з таблиці") На місце подвійного напишіть тип даних.


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 із іскоркою 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 є функцією sql іскри

Також випробував цей синтаксис, але отримав нулі замість правильного амплуа:

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

(Зауважте, мені довелося використовувати дужки та лапки, щоб це було синтаксично правильним)


PS: Я мушу визнати, що це як синтаксичний джунгль, є багато можливих способів вхідних точок, а в офіційних посиланнях API відсутні належні приклади.


1
Синтаксичні джунглі. Так. Це світ Іскри прямо зараз.
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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.