Làm cách nào tôi có thể thay đổi các loại cột trong DataFrame của Spark SQL?


152

Giả sử tôi đang làm một cái gì đó như:

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

Nhưng tôi thực sự muốn yearnhư Int(và có lẽ biến đổi một số cột khác).

Điều tốt nhất tôi có thể nghĩ ra là

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]

đó là một chút phức tạp.

Tôi đến từ R và tôi đã quen viết lách, vd

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

Tôi có thể thiếu một cái gì đó, vì sẽ có cách tốt hơn để làm điều này trong Spark / Scala ...


Tôi thích cách này spark.sql ("CHỌN CHUINGI (NULLIF (cột, '')) dưới dạng cột_ chuỗi")
Eric Bellet

Câu trả lời:


141

Chỉnh sửa: Phiên bản mới nhất

Kể từ spark 2.x bạn có thể sử dụng .withColumn. Kiểm tra các tài liệu ở đây:

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

Câu trả lời cũ nhất

Kể từ phiên bản Spark 1.4, bạn có thể áp dụng phương thức truyền với DataType trên cột:

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

Nếu bạn đang sử dụng biểu thức sql, bạn cũng có thể làm:

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

Để biết thêm thông tin, hãy kiểm tra các tài liệu: http://spark.apache.org/docs/1.6.0/api/scala/#org.apache.spark.sql.DataFrame


4
Tại sao bạn lại sử dụng withColumn sau khi thả? Không dễ dàng hơn khi chỉ sử dụng withColumn với tên cột ban đầu?
Ameba Spugnosa

@AmebaSpugnosa Tôi nghĩ rằng khi tôi sử dụng nó, Spark đã bị hỏng nếu nó có tên cột lặp lại. Không phải khi bạn tạo ra chúng, mà là khi bạn sử dụng chúng.
msasherman

5
không cần phải thả cột theo sau là đổi tên. Bạn có thể làm trong một dòngdf.withColumn("ctr", temp("ctr").cast(DecimalType(decimalPrecision, decimalScale)))
ruhong

1
Là một bản sao toàn bộ khung dữ liệu mới được tạo chỉ để lấy lại một cột trong trường hợp này? Tui bỏ lỡ điều gì vậy? Hoặc có lẽ có một số tối ưu hóa đằng sau hậu trường?
user1814008

5
Đi theo các tài liệu của Spark 2.x, df.withColumn(..)có thể thêm hoặc thay thế một cột tùy thuộc vào colNameđối số
y2k-shubham

89

[EDIT: Tháng 3 năm 2016: cảm ơn vì đã bỏ phiếu! Mặc dù thực sự, đây không phải là câu trả lời tốt nhất, tôi nghĩ rằng giải pháp dựa trên withColumn, withColumnRenamedcastđưa ra bởi msemelman, Martin Senne và những người khác đơn giản hơn và sạch hơn].

Tôi nghĩ rằng cách tiếp cận của bạn là ổn, hãy nhớ rằng Spark DataFramelà một RDD của Hàng (không thay đổi), vì vậy chúng tôi không bao giờ thực sự thay thế một cột, chỉ tạo mới DataFramemỗi lần bằng một lược đồ mới.

Giả sử bạn có một df gốc với lược đồ sau:

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)

Và một số UDF được định nghĩa trên một hoặc một số cột:

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
 )

Thay đổi loại cột hoặc thậm chí xây dựng DataFrame mới từ loại khác có thể được viết như sau:

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

mang lại:

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)

Điều này là khá gần với giải pháp của riêng bạn. Đơn giản, việc giữ các thay đổi loại và các biến đổi khác dưới dạng riêng biệt udf vallàm cho mã dễ đọc hơn và có thể sử dụng lại.


26
Điều này không an toàn cũng không hiệu quả. Không an toàn vì một mục đơn NULLhoặc không đúng định dạng sẽ sụp đổ toàn bộ công việc. Không hiệu quả vì UDF không minh bạch với Catalyst. Sử dụng UDF cho các hoạt động phức tạp là tốt, nhưng không có lý do gì để sử dụng chúng cho việc đúc kiểu cơ bản. Đây là lý do tại sao chúng ta có castphương pháp (xem câu trả lời của Martin Senne ). Làm cho mọi thứ trở nên minh bạch đối với Catalyst đòi hỏi nhiều công việc hơn nhưng an toàn cơ bản chỉ là vấn đề đặt TryOptionlàm việc.
zero323

Tôi không thấy bất cứ điều gì liên quan đến chuyển đổi chuỗi thành ngày, ví dụ "05-APR-2015"
dbspace

3
Có cách nào để giảm withColumn()phần của bạn thành một phần chung lặp đi lặp lại qua tất cả các cột không?
Boern 17/05/2016

Cảm ơn zero323, khi đọc nó, tôi đã hiểu tại sao giải pháp udf ở đây gặp sự cố. Một số ý kiến ​​tốt hơn một số câu trả lời trên SO :)
Simon Dirmeier

Có cách nào để chúng ta có thể biết được hàng bị hỏng không, có nghĩa là các bản ghi có các cột có kiểu dữ liệu sai trong quá trình truyền. Vì chức năng cast làm cho các trường đó là null
Etisha

65

casthoạt động có sẵn cho Spark Column(và như cá nhân tôi không ủng hộ udfnhư đề xuất của @ Svendtại thời điểm này), nên:

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

để đúc theo loại yêu cầu? Như một hiệu ứng phụ gọn gàng, các giá trị không thể cast / "convertable" theo nghĩa đó, sẽ trở thành null.

Trong trường hợp bạn cần điều này như một phương thức trợ giúp , hãy sử dụng:

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

được sử dụng như:

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

2
Bạn có thể tư vấn cho tôi về cách tiến hành, nếu tôi cần truyền và đổi tên cả đống cột (tôi có 50 cột và khá mới đối với scala, không chắc cách nào là tốt nhất để tiếp cận nó mà không tạo ra sự trùng lặp lớn)? Một số cột nên giữ String, một số cột nên được chuyển thành Float.
Dmitry Smirnov

cách chuyển đổi Chuỗi thành Ngày, ví dụ "25-APR-2016" trong cột và "20160302"
dbspace

@DmitrySmirnov Bạn đã bao giờ nhận được câu trả lời chưa? Tôi có cùng một câu hỏi. ;)
Evan Zamir

@EvanZamir rất tiếc là không, cuối cùng tôi đã thực hiện một thao tác để có thể sử dụng dữ liệu như rdd trong các bước khác. Tôi tự hỏi nếu điều này trở nên dễ dàng hơn những ngày này :)
Dmitry Smirnov

60

Đầu tiên , nếu bạn muốn loại diễn viên, thì đây:

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

Với cùng tên cột, cột sẽ được thay thế bằng tên mới. Bạn không cần phải thêm và xóa các bước.

Thứ hai , về Scala vs R .
Đây là mã giống với RI nhất có thể xuất hiện:

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

Mặc dù độ dài mã dài hơn R một chút. Điều đó không liên quan gì đến tính dài dòng của ngôn ngữ. Trong R, mutatelà một chức năng đặc biệt cho khung dữ liệu R, trong khi ở Scala, bạn có thể dễ dàng quảng cáo nhờ vào sức mạnh biểu cảm của nó.
Nói cách khác, nó tránh các giải pháp cụ thể, vì thiết kế ngôn ngữ đủ tốt để bạn nhanh chóng và dễ dàng xây dựng ngôn ngữ tên miền của riêng mình.


lưu ý phụ: df.columnsđáng ngạc nhiên là Array[String]thay vì Array[Column], có lẽ họ muốn nó trông giống như khung dữ liệu của gấu trúc Python.


1
Bạn có thể vui lòng cung cấp tương đương cho pyspark?
Harit Vishwakarma

Tôi đang nhận được "bắt đầu bất hợp pháp của định nghĩa" .withColumn ("tuổi", $ "tuổi" .cast (sql.types.DoubleType)) cho trường "tuổi" của tôi. Bất kì lời đề nghị nào?
BlueDolphin

Bạn có phải .cache () khung dữ liệu nếu chúng tôi thực hiện các chuyển đổi này trên nhiều cột vì lý do hiệu suất hoặc không bắt buộc vì Spark tối ưu hóa chúng?
skjagini

Việc nhập có thể import org.apache.spark.sql.types._và sau đó thay vì sql.types.IntegerTypechỉ IntegerType.
nessa.gp

17

Bạn có thể sử dụng selectExprđể làm cho nó sạch hơn một chút:

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

14

Mã Java để sửa đổi kiểu dữ liệu của DataFrame từ Chuỗi thành Số nguyên

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

Nó chỉ đơn giản sẽ truyền (kiểu dữ liệu chuỗi) hiện có sang Integer.


1
Không có DataTypestrong sql.types! nó DataType. Hơn nữa, người ta có thể chỉ cần nhập IntegerTypevà đúc.
Ehsan M. Kermani

@ EhsanM.Kermani thực sự DatyaTypes.IntegerType là một tài liệu tham khảo hợp pháp.
Cupitor

1
@Cupitor đã DataTypes.IntegerTypetừng ở chế độ DeveloperAPI và nó ổn định trong v.2.1.0
Ehsan M. Kermani

Đây là giải pháp tốt nhất!
Simon Dirmeier

8

Để chuyển đổi năm từ chuỗi thành int, bạn có thể thêm tùy chọn sau vào trình đọc csv: "inferSchema" -> "true", xem tài liệu DataBricks


5
Điều này hoạt động độc đáo nhưng điều thú vị là người đọc phải thực hiện lần thứ hai trong tệp của bạn
cowyhalo

@beefyhalo hoàn toàn phát hiện ra, có cách nào khác không?
Ayush

6

Vì vậy, điều này chỉ thực sự hoạt động nếu bạn gặp vấn đề khi lưu vào trình điều khiển jdbc như sqlserver, nhưng nó thực sự hữu ích cho các lỗi bạn sẽ gặp phải với cú pháp và loại.

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)

Bạn có thể giúp tôi thực hiện cùng một mã trong Java không? và cách đăng ký customJdbcDialect vào DataFrame
abhijitcaps

Tôi đã làm điều tương tự với Vertica, nhưng kể từ spark 2.1. JDbcUtil bạn chỉ cần triển khai kiểu dữ liệu cụ thể mà bạn cần. . dialect.getJDBCType (dt) .orElse (getCommonJDBCType (dt)) getOrElse (ném IllegalArgumentException mới (s "Không thể có được JDBC kiểu cho $ {dt.simpleString}"))
Arnon Rodman

6

Tạo một bộ dữ liệu đơn giản chứa năm giá trị và chuyển đổi intthành stringloại:

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

6

Tôi nghĩ rằng điều này là dễ đọc hơn đối với tôi.

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

Điều này sẽ chuyển đổi cột năm của bạn thành IntegerTypevới việc tạo bất kỳ cột tạm thời và thả các cột đó. Nếu bạn muốn chuyển đổi sang bất kỳ kiểu dữ liệu nào khác, bạn có thể kiểm tra các loại bên trong org.apache.spark.sql.typesgói.


5

các câu trả lời gợi ý sử dụng cast, FYI, phương thức cast trong spark 1.4.1 bị hỏng.

ví dụ: khung dữ liệu có cột chuỗi có giá trị "8182175552014127960" khi được truyền tới bigint có giá trị "8182175552014128100"

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

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

Chúng tôi đã phải đối mặt với rất nhiều vấn đề trước khi tìm thấy lỗi này vì chúng tôi có các cột bigint trong sản xuất.


4
psst, nâng cấp tia lửa của bạn
msasherman

2
@msemelman thật lố bịch khi phải nâng cấp lên phiên bản tia lửa mới trong sản xuất cho một lỗi nhỏ.
sauraI3h

không phải chúng ta luôn nâng cấp mọi thứ cho những lỗi nhỏ sao? :)
caesarsol


4

Sử dụng Spark Sql 2.4.0, bạn có thể làm điều đó:

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

3

Bạn có thể sử dụng mã dưới đây.

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

Mà sẽ chuyển đổi cột năm sang IntegerTypecột.


2

Phương thức này sẽ bỏ cột cũ và tạo các cột mới có cùng giá trị và kiểu dữ liệu mới. Các kiểu dữ liệu ban đầu của tôi khi DataFrame được tạo là: -

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

Sau đó, tôi chạy mã sau để thay đổi kiểu dữ liệu: -

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

Sau đó, kết quả của tôi được đưa ra là: -

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

Bạn có thể vui lòng cung cấp giải pháp của bạn ở đây.
Ajay Kharade

1

Người ta có thể thay đổi kiểu dữ liệu của một cột bằng cách sử dụng cast trong spark sql. Tên bảng là bảng và nó chỉ có hai cột là cột dữ liệu cột1 và cột2 và cột1 sẽ được thay đổi. ex-spark.sql ("chọn cast (cột1 là Double) cột1NewName, cột2 từ bảng") Thay vào đó hãy viết kiểu dữ liệu của bạn.


1

Trong trường hợp bạn phải đổi tên hàng chục cột được đặt theo tên của chúng, ví dụ sau đây sử dụng cách tiếp cận của @dnlbrky và áp dụng nó cho nhiều cột cùng một lúc:

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

Các cột không được giữ nguyên được giữ nguyên. Tất cả các cột ở theo thứ tự ban đầu của họ.


1

Vì vậy, nhiều câu trả lời và không có nhiều giải thích kỹ lưỡng

Cú pháp sau hoạt động bằng cách sử dụng Databricks Notebook với Spark 2.4

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

Lưu ý rằng bạn phải chỉ định định dạng mục nhập bạn có (trong trường hợp của tôi là "MM-dd-yyyy") và việc nhập là bắt buộc vì to_date là hàm spark sql

Cũng đã thử cú pháp này nhưng có null thay vì chọn đúng:

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

(Lưu ý tôi đã phải sử dụng dấu ngoặc và dấu ngoặc kép cho nó sẽ được syntaxically đúng mặc dù)


PS: Tôi phải thừa nhận điều này giống như một khu rừng cú pháp, có nhiều điểm cách có thể nhập cảnh, và các tài liệu tham khảo API chính thức thiếu ví dụ thích hợp.


1
Cú pháp rừng rậm. Đúng. Đây là thế giới của Spark ngay bây giờ.
conner.xyz

1

Một giải pháp khác như sau:

1) Giữ "inferSchema" là Sai

2) Trong khi chạy các chức năng 'Bản đồ' trên hàng, bạn có thể đọc '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

Cách khác:

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

Trong trường hợp nếu bạn muốn thay đổi nhiều cột của một loại cụ thể sang loại khác mà không chỉ định tên cột riêng lẻ

/* 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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.