Bagaimana saya bisa mengubah tipe kolom di DataFrame Spark SQL?

152

Misalkan saya melakukan sesuatu seperti:

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

Tetapi saya benar-benar menginginkan yearas Int(dan mungkin mengubah beberapa kolom lainnya).

Yang terbaik yang bisa saya pikirkan adalah

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]

yang agak berbelit-belit.

Saya berasal dari R, dan saya sudah terbiasa menulis, misalnya

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

Saya mungkin melewatkan sesuatu, karena harus ada cara yang lebih baik untuk melakukan ini di Spark / Scala ...

kevinykuo
sumber
Saya suka cara ini spark.sql ("SELECT STRING (NULLIF (kolom, '')) sebagai column_string")
Eric Bellet

Jawaban:

141

Sunting: Versi terbaru

Karena percikan 2.x dapat Anda gunakan .withColumn. Periksa dokumen di sini:

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

Jawaban tertua

Sejak Spark versi 1.4 Anda dapat menerapkan metode cor dengan DataType pada kolom:

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

Jika Anda menggunakan ekspresi sql, Anda juga dapat melakukan:

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

Untuk info lebih lanjut, periksa dokumen: http://spark.apache.org/docs/1.6.0/api/scala/#org.apache.spark.sql.DataFrame

msemelman
sumber
4
mengapa Anda menggunakan withColumn diikuti oleh drop? Bukankah lebih mudah menggunakan withColumn dengan nama kolom asli?
Ameba Spugnosa
@ AmebaSpugnosa Saya pikir pada saat saya menggunakannya Spark jatuh jika itu mengulangi nama kolom. Bukan saat Anda membuatnya, tetapi saat Anda menggunakannya.
msemelman
5
tidak perlu menjatuhkan kolom diikuti dengan mengganti nama. Anda dapat melakukannya dalam satu barisdf.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong
1
Apakah seluruh salinan dataframe baru dibuat hanya untuk menyusun kembali kolom dalam kasus ini? Apakah saya melewatkan sesuatu? Atau mungkin ada beberapa optimasi di belakang layar?
user1814008
5
Pergi dengan docs dari Spark 2.x, df.withColumn(..)dapat menambah atau mengganti kolom tergantung pada colNameargumen
y2k-shubham
89

[EDIT: Maret 2016: terima kasih untuk suaranya! Meskipun sungguh, ini bukan jawaban terbaik, saya pikir solusi berdasarkan withColumn, withColumnRenameddan castdiajukan oleh msemelman, Martin Senne dan yang lainnya lebih sederhana dan lebih bersih].

Saya pikir pendekatan Anda ok, ingat bahwa Spark DataFrameadalah RDD (tidak berubah) dari Baris, jadi kami tidak pernah benar-benar mengganti kolom, hanya membuat baru DataFramesetiap kali dengan skema baru.

Dengan asumsi Anda memiliki df asli dengan skema berikut:

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)

Dan beberapa UDF didefinisikan pada satu atau beberapa kolom:

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
 )

Mengubah jenis kolom atau bahkan membangun DataFrame baru dari yang lain dapat ditulis seperti ini:

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

yang menghasilkan:

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)

Ini cukup dekat dengan solusi Anda sendiri. Sederhananya, menjaga jenis perubahan dan transformasi lainnya sebagai terpisah udf valmembuat kode lebih mudah dibaca dan digunakan kembali.

Svend
sumber
26
Ini tidak aman dan juga tidak efisien. Tidak aman karena NULLentri tunggal atau cacat akan merusak seluruh pekerjaan. Tidak efisien karena UDF tidak transparan ke Catalyst. Menggunakan UDF untuk operasi yang kompleks tidak masalah, tetapi tidak ada alasan untuk menggunakannya untuk tipe casting dasar. Ini sebabnya kami memiliki castmetode (lihat jawaban oleh Martin Senne ). Membuat hal-hal yang transparan untuk Catalyst membutuhkan lebih banyak pekerjaan tetapi keselamatan dasar hanyalah masalah menempatkan Trydan Optionbekerja.
zero323
Saya tidak melihat apa pun yang terkait dengan konversi string hingga saat ini misalnya "05-APR-2015"
dbspace
3
Apakah ada cara untuk mengurangi withColumn()bagian Anda menjadi bagian umum yang beriterasi melalui semua kolom?
Boern
Terima kasih zero323, setelah membaca ini saya pikir mengapa solusi udf di sini mogok. Beberapa komentar lebih baik daripada beberapa jawaban pada SO :)
Simon Dirmeier
Apakah ada cara di mana kita bisa mengenal baris yang rusak, berarti catatan yang memiliki kolom tipe data yang salah selama casting. Sebagai fungsi pemeran menjadikan bidang-bidang tersebut sebagai null
Etisha
65

Karena castoperasi ini tersedia untuk Spark Column(dan karena saya pribadi tidak mendukung udfseperti yang diusulkan oleh @ Svendpada titik ini), bagaimana dengan:

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

untuk dilemparkan ke jenis yang diminta? Sebagai efek samping yang rapi, nilai-nilai yang tidak dapat dicastai / "dikonversi" dalam arti itu, akan menjadi null.

Jika Anda membutuhkan ini sebagai metode pembantu , gunakan:

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

yang digunakan seperti:

import DFHelper._
val df2 = castColumnTo( df, "year", IntegerType )
Martin Senne
sumber
2
Bisakah Anda memberi saran kepada saya tentang bagaimana melanjutkan, jika saya perlu membuat dan mengganti nama sejumlah besar kolom (Saya memiliki 50 kolom, dan cukup baru untuk scala, tidak yakin apa cara terbaik untuk mendekatinya tanpa membuat duplikasi besar-besaran)? Beberapa kolom harus tetap String, beberapa harus dilemparkan ke Float.
Dmitry Smirnov
cara mengonversi String ke Tanggal misalnya "25-APR-2016" di kolom dan "20160302"
dbspace
@DmitrySmirnov Apakah Anda pernah mendapat jawaban? Saya punya pertanyaan yang sama. ;)
Evan Zamir
@ EvanZamir sayangnya tidak, saya akhirnya melakukan shitton operasi untuk dapat menggunakan data sebagai rdd dalam langkah-langkah lain. Saya ingin tahu apakah ini menjadi lebih mudah hari ini :)
Dmitry Smirnov
60

Pertama , jika Anda ingin mengetikkan tipe, maka ini:

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

Dengan nama kolom yang sama, kolom akan diganti dengan yang baru. Anda tidak perlu melakukan langkah-langkah menambah dan menghapus.

Kedua , tentang Scala vs R .
Ini adalah kode yang paling mirip dengan RI dapat muncul dengan:

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

Padahal panjang kode sedikit lebih panjang dari R. Itu tidak ada hubungannya dengan verbositas bahasa. Dalam R mutateadalah fungsi khusus untuk kerangka data R, sementara di Scala Anda dapat dengan mudah menambahkannya berkat kekuatan ekspresifnya.
Singkatnya, ini menghindari solusi spesifik, karena desain bahasa cukup baik bagi Anda untuk dengan cepat dan mudah membangun bahasa domain Anda sendiri.


catatan: df.columnssecara mengejutkan Array[String]bukan Array[Column], mungkin mereka ingin itu terlihat seperti kerangka data Python panda.

WeiChing 林 煒 清
sumber
1
Bisakah Anda memberikan yang setara dengan pyspark?
Harit Vishwakarma
Saya mendapatkan "permulaan definisi ilegal" .withColumn ("umur", $ "usia" .cast (sql.types.DoubleType)) untuk bidang "usia" saya. Ada saran?
BlueDolphin
Apakah Anda harus .cache () frame data jika kami melakukan konversi ini pada banyak kolom karena alasan kinerja, atau tidak diperlukan karena Spark mengoptimalkannya?
skjagini
Impor bisa import org.apache.spark.sql.types._dan kemudian bukan sql.types.IntegerTypehanya IntegerType.
nessa.gp
17

Anda dapat menggunakannya selectExpruntuk membuatnya sedikit lebih bersih:

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

Kode Java untuk memodifikasi datatype DataFrame dari String ke Integer

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

Ini hanya akan membuang yang ada (String datatype) ke Integer.

manishbelsare
sumber
1
Tidak ada DataTypesdi dalam sql.types! itu DataType. Selain itu, seseorang dapat dengan mudah mengimpor IntegerTypedan melemparkan.
Ehsan M. Kermani
@ EhsanM.Kermani sebenarnya DatyaTypes.IntegerType adalah referensi yang sah.
Cupitor
1
@Cupitor DataTypes.IntegerTypedulunya dalam mode DeveloperAPI dan stabil di v.2.1.0
Ehsan M. Kermani
Ini solusi terbaik!
Simon Dirmeier
8

Untuk mengkonversi tahun dari string ke int, Anda dapat menambahkan opsi berikut ke pembaca csv: "inferSchema" -> "true", lihat dokumentasi DataBricks

Peter Rose
sumber
5
Ini bekerja dengan baik tetapi yang menarik adalah bahwa pembaca harus melakukan pass kedua file Anda
beefyhalo
@ Beefyhalo benar-benar tepat, apakah ada cara lain?
Ayush
6

Jadi ini hanya benar-benar berfungsi jika Anda mengalami masalah menabung ke driver jdbc seperti sqlserver, tetapi ini sangat membantu untuk kesalahan yang akan Anda hadapi dengan sintaks dan tipe.

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
sumber
Bisakah Anda membantu saya menerapkan kode yang sama di Jawa? dan bagaimana cara mendaftar customJdbcDialect ke DataFrame
abhijitcaps
Bagus saya melakukan hal yang sama dengan Vertica, tetapi karena percikan 2.1. JDbcUtil Anda hanya perlu mengimplementasikan tipe data spesifik yang Anda butuhkan. dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)). getOrElse (lempar IllegalArgumentException baru (s "Tidak bisa mendapatkan tipe JDBC untuk $ {dt.simpleString}"))
Arnon Rodman
6

Hasilkan dataset sederhana yang berisi lima nilai dan konversi intuntuk stringmengetik:

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

Saya pikir ini lebih mudah dibaca untuk saya.

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

Ini akan mengonversi kolom tahun Anda menjadi IntegerTypedengan membuat kolom sementara apa saja dan menjatuhkan kolom-kolom itu. Jika Anda ingin mengonversi ke tipe data lain, Anda dapat memeriksa jenis di dalam org.apache.spark.sql.typespaket.

Piyush Patel
sumber
5

jawaban yang menyarankan untuk menggunakan cast, FYI, metode cast dalam spark 1.4.1 rusak.

misalnya, kerangka data dengan kolom string yang memiliki nilai "8182175552014127960" ketika dicor ke bigint memiliki nilai "8182175552014128100"

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

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

Kami harus menghadapi banyak masalah sebelum menemukan bug ini karena kami memiliki kolom bigint dalam produksi.

sauraI3h
sumber
4
psst,
perbarui
2
@msemelman itu konyol harus meng-upgrade ke versi baru percikan dalam produksi untuk bug kecil.
sauraI3h
bukankah kita selalu memutakhirkan semuanya untuk bug kecil? :)
caesarsol
5
df.select($"long_col".cast(IntegerType).as("int_col"))
mesin jiwa
sumber
4

Menggunakan Spark Sql 2.4.0 Anda dapat melakukannya:

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

Anda dapat menggunakan kode di bawah ini.

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

Yang akan mengkonversi kolom tahun ke IntegerTypekolom.

Adarsh
sumber
2

Metode ini akan menjatuhkan kolom lama dan membuat kolom baru dengan nilai dan tipe data yang sama. Tipe data asli saya saat DataFrame dibuat adalah: -

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

Setelah ini saya menjalankan kode berikut untuk mengubah tipe data: -

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

Setelah ini hasil saya keluar menjadi: -

root
 |-- id: integer (nullable = true)
 |-- flag2: string (nullable = true)
 |-- name: string (nullable = true)
 |-- flag1: boolean (nullable = true)
 |-- flag3: boolean (nullable = true)
PirateJack
sumber
Bisakah Anda memberikan solusi Anda di sini.
Ajay Kharade
1

Seseorang dapat mengubah tipe data kolom dengan menggunakan cast in spark sql. nama tabel adalah tabel dan memiliki dua kolom, hanya kolom1 dan kolom2 dan tipe data1 kolom harus diubah. ex-spark.sql ("pilih cast (column1 as Double) column1NewName, column2 from table") Di tempat double tulis tipe data Anda.

Tejasvi Sharma
sumber
1

Jika Anda harus mengganti nama lusinan kolom dengan namanya, contoh berikut menggunakan pendekatan @dnlbrky dan menerapkannya ke beberapa kolom sekaligus:

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

Kolom yang belum diputar disimpan tidak berubah. Semua kolom tetap dalam urutan aslinya.

selada kubik
sumber
1

Begitu banyak jawaban dan tidak banyak penjelasan menyeluruh

Sintaks berikut berfungsi Menggunakan Notebook Databricks dengan Spark 2.4

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

Perhatikan bahwa Anda harus menentukan format entri yang Anda miliki (dalam kasus saya "MM-dd-yyyy") dan impor wajib karena to_date adalah fungsi sql percikan

Juga Mencoba sintaks ini tetapi mendapat nol alih-alih pemeran yang tepat:

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

(Catatan saya harus menggunakan tanda kurung dan tanda kutip agar benar secara sintaksis)


PS: Saya harus mengakui ini seperti hutan sintaksis, ada banyak cara masuk yang memungkinkan, dan referensi API resmi tidak memiliki contoh yang tepat.

Mehdi LAMRANI
sumber
1
Hutan sintaksis. Iya. Ini adalah dunia Spark sekarang.
conner.xyz
1

Solusi lain adalah sebagai berikut:

1) Simpan "inferSchema" sebagai False

2) Saat menjalankan fungsi 'Peta' di baris, Anda dapat membaca '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
sumber
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
sumber
0

Cara lain:

// 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")
pengguna8106134
sumber
0

Dalam kasus jika Anda ingin mengubah beberapa kolom dari jenis tertentu ke yang lain tanpa menentukan nama kolom individual

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