Spark权威指南(中文版)----第6章 处理不同类型的数据(1)
Spark The Definitive Guide(Spark权威指南) 中文版。本书详细介绍了Spark2.x版本的各个模块,目前市面上最好的Spark2.x学习书籍!!!
扫码关注公众号:登峰大数据,阅读中文Spark权威指南(完整版),系统学习Spark大数据框架!
6.1. 在哪里查看API
在我们开始之前,有必要解释一下您作为用户应该在哪里查找DataFrame API。Spark是一个不断增长的项目,任何书籍(包括这一本书)都是某个时间的快照。在本书中,我们的首要任务之一是教授在撰写本文时,您应该在哪里寻找转换数据的函数。以下是关键的地方:
DataFrame(Dataset)的方法
这实际上有点小技巧,因为DataFrame只是Row类型的Dataset,你最终会看到Dataset方法,在这个链接中可以找到:
http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset
Dataset子模块如DataFrameStatFunctions(包含各种统计相关的功能) 和 DataFrameNaFunctions(处理空数据时相关的函数)有更多的方法来解决特定的问题集。
Column方法
这些在第5章的大部分内容中已经介绍过。它们包含各种与列相关的通用方法,如alias或contains。您可以在这里找到列方法的API引用。
http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Column
org.apache.spark.sql.functions包含一系列不同数据类型的函数。通常,您会看到整个包被导入,因为它们被频繁地使用。您可以在这里找到SQL和DataFrame函数。
http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.functions$
如此繁多的函数,可能会让人觉得有点难以承受,但不用担心,这些函数中的大多数都是SQL和分析系统中可以找到的。所有这些工具的存在都是为了实现一个目的,即将数据行以一种格式或结构转换为另一种格式。这可能会创建更多的行或减少可用的行数。首先,让我们创建用于分析的DataFrame:
// in Scala
val df = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load("/data/retail-data/by-day/2010-12-01.csv")
df.printSchema()
df.createOrReplaceTempView("dfTable")
下面是schema的结果和一个数据样本:
6.2. 转换为Spark类型
在本章中,您将看到我们所做的一件事是将本地原生类型转换为Spark类型。我们用我们在这里介绍的第一个函数来做这个,lit()函数。此函数将另一种语言中的类型转换为其相应的Spark类型表示。以下是我们如何将几个不同类型的Scala和Python值转换为各自的Spark类型:
// in Scala
import org.apache.spark.sql.functions.lit
df.select(lit(5), lit("five"), lit(5.0))
# in Python
from pyspark.sql.functions import lit
df.select(lit(5), lit("five"), lit(5.0))
SQL中没有等价的函数,所以我们可以直接使用这些值:
-- in SQL
SELECT 5, "five", 5.0
6.3. 使用Boolean类型
当涉及到数据分析时,布尔值非常重要,因为它们是所有过滤的基础。布尔语句由四个元素组成:and,or, true和false。我们使用这些简单的结构来构建逻辑语句,以判断真假。当一行数据必须通过测试(evaluate to true)或被过滤时,这些语句通常被用作条件要求。
让我们使用我们的零售数据集探索使用布尔值。我们可以指定等于以及小于或大于:
// in Scala
import org.apache.spark.sql.functions.col
df.where(col("InvoiceNo").equalTo(536365))
.select("InvoiceNo", "Description")
.show(5, false)
// in Scala
import org.apache.spark.sql.functions.col
df.where(col("InvoiceNo") === 536365)
.select("InvoiceNo", "Description")
.show(5, false)
注意
Scala有一些关于使用==和===的特殊语义。在Spark中,如果您想要通过等式进行过滤,您应该使用===(相等)或=!=(不相等)。您还可以使用not函数和equalTo方法。
另一个选项(可能是最干净的选项)是将谓词指定为字符串中的表达式。这对Python或Scala有效。请注意,这也使您可以使用另一种表达“不等于”的方式:
df.where("InvoiceNo = 536365")
.show(5, false)
df.where("InvoiceNo <> 536365")
.show(5, false)
我们提到,在使用and或or时,可以使用多个部分指定布尔表达式。在Spark中,您应该始终将and过滤器连接在一起作为一个序列过滤器。原因是,即使布尔语句是串行的(一个接一个),Spark将所有这些过滤器压平为一个语句,并同时执行过滤器。虽然您可以通过使用and(如果您愿意的话)显式地指定语句,但是如果您以串行方式指定语句,它们通常更容易理解和读取。or须在同一语句中指明:
// in Scala
val priceFilter = col("UnitPrice") > 600
val descripFilter = col("Description").contains("POSTAGE")
df.where(col("StockCode").isin("DOT")).where(priceFilter.or(descripFilter))
.show()
-- in SQL
SELECT * FROM dfTable WHERE StockCode in ("DOT") AND(UnitPrice > 600 OR
instr(Description, "POSTAGE") >= 1)
布尔表达式不仅仅是为过滤器保留的。要过滤一个DataFrame,您还可以指定一个布尔列:
// in Scala
val DOTCodeFilter = col("StockCode") === "DOT"
val priceFilter = col("UnitPrice") > 600
val descripFilter = col("Description").contains("POSTAGE")
df.withColumn("isExpensive", DOTCodeFilter.and(priceFilter.or(descripFilter)))
.as("isExpensive")
.select("unitPrice", "isExpensive").show(5)
-- in SQL
SELECT UnitPrice, (StockCode = 'DOT' AND
(UnitPrice > 600 OR instr(Description, "POSTAGE") >= 1)) as isExpensive
FROM dfTable
WHERE (StockCode = 'DOT' AND
(UnitPrice > 600 OR instr(Description, "POSTAGE") >= 1))
如果您有SQL背景知识,那么所有这些语句都应该非常熟悉。实际上,它们都可以表示为where子句。事实上,使用SQL语句来表达过滤器比使用编程的DataFrame接口和Spark SQL更容易,这使得我们无需付出任何性能代价就可以做到这一点。例如,以下两个表述是等价的:
// in Scala
import org.apache.spark.sql.functions.{expr, not, col}
df.withColumn("isExpensive", not(col("UnitPrice").leq(250)))
.filter("isExpensive")
.select("Description", "UnitPrice").show(5)
df.withColumn("isExpensive", expr("NOT UnitPrice <= 250"))
.filter("isExpensive")
.select("Description", "UnitPrice").show(5)
注意
一个可能出现的“陷阱”, 如果在创建布尔表达式时使用null数据。如果您的数据中有一个空值,那么您将需要以稍微不同的方式处理事情。null值等于的测试代码:
df.where(col("Description").eqNullSafe("hello")).show()
6.4. 使用 Numbers类型
为了构建一个虚构的示例,假设我们发现我们在零售数据集中错误地记录了数量,真实数量等于(当前数量*单价)的平方 + 5。
// in Scala
import org.apache.spark.sql.functions.{expr, pow}
val fabricatedQuantity = pow(col("Quantity") * col("UnitPrice"), 2) + 5
df.select(expr("CustomerId"), fabricatedQuantity.alias("realQuantity")).show(2)
from pyspark.sql.functions import expr, pow
fabricatedQuantity = pow(col("Quantity") * col("UnitPrice"), 2) + 5
df.select(expr("CustomerId"), fabricatedQuantity.alias("realQuantity")).show(2)
+----------+------------------+
|CustomerId| realQuantity|
+----------+------------------+
| 17850.0|239.08999999999997|
| 17850.0| 418.7156|
+----------+------------------+
注意我们可以把列相乘因为它们都是数值。当然,我们也可以根据需要进行加减操作。事实上,我们还可以将所有这些操作使用SQL表达式来完成:
// in Scala
df.selectExpr(
"CustomerId",
"(POWER((Quantity * UnitPrice), 2.0) + 5) as realQuantity").show(2)
df.selectExpr(
"CustomerId",
"(POWER((Quantity * UnitPrice), 2.0) + 5) as realQuantity").show(2)
-- in SQL
SELECT customerId, (POWER((Quantity * UnitPrice), 2.0) + 5) as realQuantity
FROM dfTable
另一个常见的数值操作:四舍五入操作。如果你想四舍五入到一个整数,通常你可以将值转换成一个整数,这样就可以了。然而,Spark还具有更详细的函数来显式地执行此操作并达到一定的精度。在下面的例子中,我们四舍五入到小数点后一位:
// in Scala
import org.apache.spark.sql.functions.{round, bround}
df.select(round(col("UnitPrice"), 1).alias("rounded"), col("UnitPrice")).show(5)
round()操作是向上四舍五入。bround()操作是向下舍去小数。
// in Scala
import org.apache.spark.sql.functions.lit
df.select(round(lit("2.5")), bround(lit("2.5"))).show(2)
# in Python
from pyspark.sql.functions import lit, round, bround
df.select(round(lit("2.5")), bround(lit("2.5"))).show(2)
-- in SQL
SELECT round(2.5), bround(2.5)
+-------------+--------------+
|round(2.5, 0)|bround(2.5, 0)|
+-------------+--------------+
| 3.0| 2.0|
| 3.0| 2.0|
+-------------+--------------+
另一个数值任务是计算两列的相关性。例如,我们可以看到两列的皮尔逊相关系数,看看便宜的东西是否大量购买。我们可以通过一个函数以及DataFrame统计方法来实现这一点:
// in Scala
import org.apache.spark.sql.functions.{corr}
df.stat.corr("Quantity", "UnitPrice")
df.select(corr("Quantity", "UnitPrice")).show()
# in Python
from pyspark.sql.functions import corr
df.stat.corr("Quantity", "UnitPrice")
df.select(corr("Quantity", "UnitPrice")).show()
-- in SQL
SELECT corr(Quantity, UnitPrice) FROM dfTable
+-------------------------+
|corr(Quantity, UnitPrice)|
+-------------------------+
| -0.04112314436835551|
+-------------------------+
6.5. 使用String类型
// in Scala
import org.apache.spark.sql.functions.{lower, upper}
df.select(col("Description"),
lower(col("Description")),
upper(lower(col("Description")))).show(2)
# in Python
from pyspark.sql.functions import lower, upper
df.select(col("Description"),
lower(col("Description")),
upper(lower(col("Description")))).show(2)
-- in SQL
SELECT Description, lower(Description), Upper(lower(Description)) FROM dfTable
+--------------------+--------------------+-------------------------+
| Description| lower(Description)|upper(lower(Description))|
+--------------------+--------------------+-------------------------+
|WHITE HANGING HEA...|white hanging hea...| WHITE HANGING HEA...|
| WHITE METAL LANTERN| white metal lantern| WHITE METAL LANTERN|
+--------------------+--------------------+-------------------------+
另一个简单的任务是在字符串周围添加或删除空格。你可以使用lpad、ltrim、rpad和rtrim、trim:
// in Scala
import org.apache.spark.sql.functions.{lit, ltrim, rtrim, rpad, lpad, trim}
df.select(
ltrim(lit(" HELLO ")).as("ltrim"),
rtrim(lit(" HELLO ")).as("rtrim"),
trim(lit(" HELLO ")).as("trim"),
lpad(lit("HELLO"), 3, " ").as("lp"),
rpad(lit("HELLO"), 10, " ").as("rp")).show(2)
from pyspark.sql.functions import lit, ltrim, rtrim, rpad, lpad, trim
df.select(
ltrim(lit(" HELLO ")).alias("ltrim"),
rtrim(lit(" HELLO ")).alias("rtrim"),
trim(lit(" HELLO ")).alias("trim"),
lpad(lit("HELLO"), 3, " ").alias("lp"),
rpad(lit("HELLO"), 10, " ").alias("rp")).show(2)
-- in SQL
SELECT
ltrim(' HELLLOOOO '),
rtrim(' HELLLOOOO '),
trim(' HELLLOOOO '),
lpad('HELLOOOO ', 3, ' '),
rpad('HELLOOOO ', 10, ' ')
FROM dfTable
+---------+---------+-----+---+----------+
| ltrim| rtrim| trim| lp| rp|
+---------+---------+-----+---+----------+
|HELLO | HELLO|HELLO| HE|HELLO |
|HELLO | HELLO|HELLO| HE|HELLO |
+---------+---------+-----+---+----------+
注意,如果lpad或rpad取的数字小于字符串的长度,它将总是从字符串的右侧删除值。
6.5.1. 正则表达式
可能最常执行的任务之一是搜索另一个字符串的存在,或者用另一个值替换所有提到的字符串。这通常是通过一个称为正则表达式的工具来实现的,该工具存在于许多编程语言中。正则表达式使用户能够指定一组规则,以便从字符串中提取值或用其他值替换它们。Spark利用了Java正则表达式的强大功能。Spark中有两个关键函数,您需要它们来执行正则表达式任务:regexp_extract 和 regexp_replace。两个函数分别提取值和替换值。让我们探讨如何使用regexp_replace函数替换description列中的替换颜色名称:
// in Scala
import org.apache.spark.sql.functions.regexp_replace
val simpleColors = Seq("black", "white", "red", "green", "blue")
val regexString = simpleColors.map(_.toUpperCase).mkString("|")
// 在正则表达式语法中,|表示“或”
df.select(
regexp_replace(col("Description"), regexString, "COLOR").alias("color_clean"),
col("Description")).show(2)
-- in SQL
SELECT
regexp_replace(Description, 'BLACK|WHITE|RED|GREEN|BLUE', 'COLOR') as
color_clean, Description
FROM dfTable
+--------------------+--------------------+
| color_clean| Description|
+--------------------+--------------------+
|COLOR HANGING HEA...|WHITE HANGING HEA...|
| COLOR METAL LANTERN| WHITE METAL LANTERN|
+--------------------+--------------------+
6.6. 使用Dates 和Timestamps类型
日期和时间是编程语言和数据库中经常遇到的挑战。总是需要跟踪时区,确保格式正确和有效。Spark通过明确地关注两种与时间相关的信息,尽力使事情保持简单有专门关注日历日期的date和包含日期和时间信息的timestamp。正如我们在当前数据集中看到的那样,Spark将尽力正确地识别列类型,包括在启用inferSchema时的日期和时间戳。我们可以看到,这在我们当前的数据集上运行得非常好,因为它能够识别和读取我们的日期格式,而不需要我们为它提供一些规范。如前所述,处理日期和时间戳与处理字符串密切相关,因为我们经常将时间戳或日期存储为字符串,并在运行时将它们转换为日期类型。这在处理数据库和结构化数据时不太常见,但在处理文本和CSV文件时更常见。我们将很快对此进行实验。
注意
不幸的是,在处理日期和时间戳时有很多需要注意的地方,特别是在处理时区时。在2.1版本和之前,如果您正在解析的值中没有显式地指定时区,Spark将根据机器的时区进行解析。如果需要,可以通过设置本地会话的时区。SQL配置:spark.conf.sessionLocalTimeZone
。这应该根据Java时区格式进行设置。
df.printSchema()
root
|-- InvoiceNo: string (nullable = true)
|-- StockCode: string (nullable = true)
|-- Description: string (nullable = true)
|-- Quantity: integer (nullable = true)
|-- InvoiceDate: timestamp (nullable = true)
|-- UnitPrice: double (nullable = true)
|-- CustomerID: double (nullable = true)
|-- Country: string (nullable = true)
尽管Spark会尽力读取日期或时间。然而,有时我们无法处理格式奇怪的日期和时间。理解您将要应用的转换的关键是确保您确切地知道在方法的每个给定步骤中有什么类型和格式。另一个常见的“陷阱”是Spark的TimestampType类只支持二级精度,也就是说,如果你的时间是毫秒或微秒,你需要通过long时间的操作来解决这个问题。在强制使用TimestampType时,将删除精度。Spark可以对任何给定时间点的格式进行一些特殊处理。在进行解析或转换时,务必明确说明这样做是没有问题的。最后,Spark还在使用Java日期和时间戳,因此符合这些标准。让我们从基础开始,获取当前日期和当前时间戳:
// in Scala
import org.apache.spark.sql.functions.{current_date, current_timestamp}
val dateDF = spark.range(10)
.withColumn("today", current_date())
.withColumn("now", current_timestamp())
dateDF.createOrReplaceTempView("dateTable")
dateDF.printSchema()
root
|-- id: long (nullable = false)
|-- today: date (nullable = false)
|-- now: timestamp (nullable = false)
现在我们有了一个简单的DataFrame,让我们从今天开始加减5天。这些函数接收列名和加上或减去的天数作为参数:
// in Scala
import org.apache.spark.sql.functions.{date_add, date_sub}
dateDF.select(date_sub(col("today"), 5), date_add(col("today"), 5)).show(1)
-- in SQL
SELECT date_sub(today, 5), date_add(today, 5) FROM dateTable
+------------------+------------------+
|date_sub(today, 5)|date_add(today, 5)|
+------------------+------------------+
| 2017-06-12| 2017-06-22|
+------------------+------------------+
另一个常见的任务是查看两个日期之间的差异。我们可以使用datediff函数来实现这一点,该函数将返回两个日期之间的天数。大多数时候我们只关心天数,因为每个月的天数不同,还有一个函数,months_between,它给出两个日期之间的月数:
// in Scala
import org.apache.spark.sql.functions.{datediff, months_between, to_date}
dateDF.withColumn("week_ago", date_sub(col("today"), 7))
.select(datediff(col("week_ago"), col("today"))).show(1)
dateDF.select(
to_date(lit("2016-01-01")).alias("start"),
to_date(lit("2017-05-22")).alias("end"))
.select(months_between(col("start"), col("end"))).show(1)
-- in SQL
SELECT to_date('2016-01-01'), months_between('2016-01-01', '2017-01-01'),
datediff('2016-01-01', '2017-01-01')
FROM dateTable
+---------------------------------+
|datediff(week_ago, today)|
+---------------------------------+
| -7|
+---------------------------------+
+------------------ -----------------+
|months_between(start, end)|
+------------------------------------+
| -16.67741935|
+------------------------------------+
注意,我们引入了一个新函数:to_date函数。to_date函数允许您将字符串转换为日期,可以选择指定格式。我们以JavaSimpleDateFormat指定我们的格式,如果您使用此函数,该格式将非常重要,值得您参考:
// in Scala
import org.apache.spark.sql.functions.{to_date, lit}
spark.range(5).withColumn("date", lit("2017-01-01"))
.select(to_date(col("date"))).show(1)
如果Spark不能解析日期,则不会抛出错误;而是返回null。在更大的pipeline中,这可能有点棘手,因为您可能会以一种格式期待您的数据,并在另一种格式中获取数据。为了说明这一点,让我们看一下日期格式从year-month-day 到 year-day-month的转换。
dateDF.select(to_date(lit("2016-20-12")),to_date(lit("2017-12-11"))).show(1)
+-------------------+-------------------+
|to_date(2016-20-12)|to_date(2017-12-11)|
+-------------------+-------------------+
| null| 2017-12-11|
+-------------------+-------------------+
我们发现这对bug来说是一种特别棘手的情况,因为有些日期可能匹配正确的格式,而另一些则不匹配。在前面的示例中,请注意第二个日期如何显示为12月11日,而不是11月12日。Spark不会抛出错误,因为它不知道日期是混在一起的还是特定的行是不正确的。让我们一步一步地修复这个问题,并想出一个健壮的方法来完全避免这些问题。第一步是记住,我们需要根据Java SimpleDateFormat标准指定日期格式。我们将使用两个函数来修复这个问题:to_date和to_timestamp。前者可选地要求格式,而后者需要格式:
// in Scala
import org.apache.spark.sql.functions.to_date
val dateFormat = "yyyy-dd-MM"
val cleanDateDF = spark.range(1).select(
to_date(lit("2017-12-11"), dateFormat).alias("date"),
to_date(lit("2017-20-12"), dateFormat).alias("date2"))
cleanDateDF.createOrReplaceTempView("dateTable2")
-- in SQL
SELECT to_date(date, 'yyyy-dd-MM'), to_date(date2, 'yyyy-dd-MM'), to_date(date)
FROM dateTable2
+----------+----------+
| date| date2|
+----------+----------+
|2017-11-12|2017-12-20|
+----------+----------+
现在让我们使用to_timestamp的一个例子,它总是需要指定一个格式:
// in Scala
import org.apache.spark.sql.functions.to_timestamp
cleanDateDF.select(to_timestamp(col("date"), dateFormat)).show()
-- in SQL
SELECT to_timestamp(date, 'yyyy-dd-MM'), to_timestamp(date2, 'yyyy-dd-MM')
FROM dateTable2
+----------------------------------+
|to_timestamp(`date`, 'yyyy-dd-MM')|
+----------------------------------+
| 2017-11-12 00:00:00|
+----------------------------------+
在日期和时间戳之间进行转换在所有语言中都很简单——在SQL中,我们采用以下方法:
-- in SQL
SELECT cast(to_date("2017-01-01", "yyyy-dd-MM") as timestamp)
在我们有了正确的格式和类型的日期或时间戳之后,比较它们实际上是很容易的。我们只需要确保使用日期/时间戳类型或者根据正确的yyyy - mm -dd格式指定我们的字符串,如果我们在比较日期:
cleanDateDF.filter(col("date2") > lit("2017-12-12")).show()
一个小问题是,我们还可以将其设置为字符串,它将解析为文本:
cleanDateDF.filter(col("date2") > "'2017-12-12'").show()
6.7. 数据处理中的null值
作为一种最佳实践,应该始终使用nulls来表示DataFrames中丢失的或空的数据。Spark可以比使用空字符串或其他值更优化地使用null值。在DataFrame范围内,与null值交互的主要方式是在DataFrame上使用.na子包。还有几个函数用于执行操作并显式地指定Spark应该如何处理null值。有关更多信息,请参见第5章(我们将在其中讨论排序),并参考“与boolean一起工作”。
注意
Nulls是所有编程中具有挑战性的部分,Spark也不例外。在我们看来,显式处理null值总是比隐式处理好。例如,在本书的这一部分中,我们看到了如何将列定义为具有空类型。然而,这是有问题的。当我们声明一个列不可为null时,实际上并没有强制执行。重申一下,当您定义一个模式时,其中所有列都被声明为notnull,Spark将不会强制执行该模式,并且很高兴地让空值进入该列。是否为null的标识,只是为了帮助触发SQL优化以处理该列。如果列中不应该有null值,则可能会得到不正确的结果或看到难以调试的奇怪异常。
可以对空值做两件事:可以显式地删除空值,或者可以用值(全局或每列)填充空值。现在让我们来做一个实验。
Coalesce
Spark包含一个函数,允许您使用coalesce函数从一组列中选择第一个非空值。在这种情况下,没有空值,因此它只返回第一列:
// in Scala
import org.apache.spark.sql.functions.coalesce
df.select(coalesce(col("Description"), col("CustomerId"))).show()
ifnull, nullIf, nvl, 和 nvl2
您还可以使用其他几个SQL函数来实现类似的功能。
-
ifnull:允许您在第一个值为null时选择第二个值,并默认为第一个值
-
nullif: 如果两个值相等则返回null,否则返回第二个值
-
nvl: 如果第一个值为null,则返回第二个值,但默认为第一个值
-
nvl2: 如果第一个值不是null,则返回第二个值; 否则,它将返回最后指定的值(下面示例中的else_value)
-- in SQL
SELECT
ifnull(null, 'return_value'),
nullif('value', 'value'),
nvl(null, 'return_value'),
nvl2('not_null', 'return_value', "else_value")
FROM dfTable LIMIT 1
+------------+----+------------+------------+
| a| b| c| d|
+------------+----+------------+------------+
|return_value|null|return_value|return_value|
+------------+----+------------+------------+
当然,我们也可以在DataFrames的select表达式中使用它们。
drop
最简单的函数是drop,它删除包含null的行。默认情况是删除任何值为空的行:
df.na.drop()
df.na.drop("any")
在SQL中,我们必须逐列执行此操作:
-- in SQL
SELECT * FROM dfTable WHERE Description IS NOT NULL
将“any”指定为参数将删除任一个字段为null的行。使用“all”只在该行的所有值为null或NaN时才删除行:
df.na.drop("all")
我们也可以通过传入列数组,将此应用于某些列集:
// in Scala
df.na.drop("all", Seq("StockCode", "InvoiceNo"))
fill
使用fill函数,可以用一组值填充一个或多个列。这可以通过指定一个map来完成——它是一个特定的值和一组列。例如,要在String类型的列中填充所有空值,可以指定以下内容:
df.na.fill("All Null values become this string")
对于类型为Integer的列,我们也可以使用df.na.fill(5:Integer),对于类型为Double的列,可以使用df.na.fill(5:Double)
.
要指定列,我们只需传入列名称数组,就像前面示例中的那样
// in Scala
df.na.fill(5, Seq("StockCode", "InvoiceNo"))
我们还可以使用Scala Map来实现这一点,其中键是列名称,值是我们希望用来填充空值的值:
// in Scala
val fillColValues = Map("StockCode" -> 5, "Description" -> "No Value")
df.na.fill(fillColValues)
replace
唯一的要求是该值与原始值的类型相同:
// in Scala
df.na.replace("Description", Map("" -> "UNKNOWN"))
ordering
正如我们在第5章中讨论的,您可以使用asc_nulls_first、desc_nulls_first、asc_nulls_last或desc_nulls_last,来指定您希望在有序的DataFrame中显示空值的位置
6.8. 使用复杂类型
复杂类型可以帮助您公司组织数据,从而使希望解决的问题更有意义。有三种复杂类型: structs, arrays, 和 maps。
Structs
可以将structs(结构)看作是DataFrames中的DataFrames。一个示例将更清楚地说明这一点。我们可以通过在查询的括号中封装一组列来创建一个结构体:
df.selectExpr("(Description, InvoiceNo) as complex", "*")
df.selectExpr("struct(Description, InvoiceNo) as complex", "*")
// in Scala
import org.apache.spark.sql.functions.struct
val complexDF = df.select(struct("Description", "InvoiceNo").alias("complex"))
complexDF.createOrReplaceTempView("complexDF")
现在我们有了一个包含complex列的DataFrame。我们可以像查询另一个DataFrame一样查询它,唯一的区别是我们使用点语法来查询它,或者使用列方法getField:
complexDF.select("complex.Description")
complexDF.select(col("complex").getField("Description"))
我们还可以使用*查询结构中的所有值。这将把所有列显示到顶层DataFrame:
complexDF.select("complex.*")
-- in SQL
SELECT complex.* FROM complexDF
Arrays
为了更好理解Arrays类型,先看一个例子。使用当前数据,我们的目标是将Description列中的每个单词转换为DataFrame中的一行。第一个任务是将Description列转换为复杂类型Array。
我们使用split函数来实现这一点,并指定分隔符:
// in Scala
import org.apache.spark.sql.functions.split
df.select(split(col("Description"), " ")).show(2)
# in Python
from pyspark.sql.functions import split
df.select(split(col("Description"), " ")).show(2)
-- in SQL
SELECT split(Description, ' ') FROM dfTable
这非常强大,因为Spark允许我们将这种复杂类型作为另一列来操作。我们也可以使用类似python语法查询数组的值:
// in Scala
df.select(split(col("Description"), " ").alias("array_col"))
.selectExpr("array_col[0]").show(2)
df.select(split(col("Description"), " ").alias("array_col"))\
.selectExpr("array_col[0]").show(2)
-- in SQL
SELECT split(Description, ' ')[0] FROM dfTable
Array长度
我们可以通过查询数组的大小来确定数组的长度:
// in Scala
import org.apache.spark.sql.functions.size
df.select(size(split(col("Description"), " "))).show(2) // shows 5 and 3
# in Python
from pyspark.sql.functions import size
df.select(size(split(col("Description"), " "))).show(2) # shows 5 and 3
array_contains函数
我们还可以看到这个数组是否包含某个值:
// in Scala
import org.apache.spark.sql.functions.array_contains
df.select(array_contains(split(col("Description"), " "), "WHITE")).show(2)
# in Python
from pyspark.sql.functions import array_contains
df.select(array_contains(split(col("Description"), " "), "WHITE")).show(2)
-- in SQL
SELECT array_contains(split(Description, ' '), 'WHITE') FROM dfTable
然而,这并不能解决我们当前的问题。要将复杂类型转换为一组行(数组中的每个值对应一个行),需要使用explode函数。Explode函数explode函数接受一个由数组组成的列,并为数组中的每个值创建一行(其余值重复)。图6-1说明了这个过程。
// in Scala
import org.apache.spark.sql.functions.{split, explode}
df.withColumn("splitted", split(col("Description"), " "))
.withColumn("exploded", explode(col("splitted")))
.select("Description", "InvoiceNo", "exploded").show(2)
# in Python
from pyspark.sql.functions import split, explode
df.withColumn("splitted", split(col("Description"), " "))\
.withColumn("exploded", explode(col("splitted")))\
.select("Description", "InvoiceNo", "exploded").show(2)
-- in SQL
SELECT Description, InvoiceNo, exploded
FROM (SELECT *, split(Description, " ") as splitted FROM dfTable)
LATERAL VIEW explode(splitted) as exploded
Maps
maps是通过使用map函数和列的键值对创建的。然后你就可以像从数组中选择一样选择它们:
// in Scala
import org.apache.spark.sql.functions.map
df.select(map(col("Description"), col("InvoiceNo")).alias("complex_map")).show(2)
# in Python
from pyspark.sql.functions import create_map
df.select(create_map(col("Description"), col("InvoiceNo")).alias("complex_map")).show(2)
-- in SQL
SELECT map(Description, InvoiceNo) as complex_map FROM dfTable
WHERE Description IS NOT NULL
您可以使用正确的key查询它们。如果key不存在,则返回null:
// in Scala
df.select(map(col("Description"), col("InvoiceNo")).alias("complex_map"))
.selectExpr("complex_map['WHITE METAL LANTERN']").show(2)
df.select(map(col("Description"), col("InvoiceNo")).alias("complex_map"))\
.selectExpr("complex_map['WHITE METAL LANTERN']").show(2)
你也可以explode map类型,这将把他们变成列:
// in Scala
df.select(map(col("Description"), col("InvoiceNo")).alias("complex_map"))
.selectExpr("explode(complex_map)").show(2)
df.select(map(col("Description"), col("InvoiceNo")).alias("complex_map"))\
.selectExpr("explode(complex_map)").show(2)
6.9. 使用JSON
您可以直接操作Spark中的JSON字符串,并解析JSON或提取JSON对象。让我们从创建一个JSON列开始:
// in Scala
val jsonDF = spark.range(1).selectExpr("""
'{"myJSONKey" : {"myJSONValue" : [1, 2, 3]}}' as jsonString""")
# in Python
jsonDF = spark.range(1).selectExpr("""
'{"myJSONKey" : {"myJSONValue" : [1, 2, 3]}}' as jsonString""")
可以使用get_json_object内联查询JSON对象,无论是字典还是数组。如果这个对象只有一个嵌套层,你可以使用json_tuple:
// in Scala
import org.apache.spark.sql.functions.{get_json_object, json_tuple}
jsonDF.select(
get_json_object(col("jsonString"), "$.myJSONKey.myJSONValue[1]") as "column",
json_tuple(col("jsonString"), "myJSONKey")).show(2)
from pyspark.sql.functions import get_json_object, json_tuple jsonDF.select( get_json_object(col("jsonString"), "$.myJSONKey.myJSONValue[1]") as "column", json_tuple(col("jsonString"), "myJSONKey")).show(2)
--SQL
jsonDF.selectExpr( "json_tuple(jsonString, '$.myJSONKey.myJSONValue[1]') as column").show(2)
您还可以使用to_json函数将StructType转换为JSON字符串:
// in Scala
import org.apache.spark.sql.functions.to_json
df.selectExpr("(InvoiceNo, Description) as myStruct")
.select(to_json(col("myStruct")))
# in Python
from pyspark.sql.functions import to_json
df.selectExpr("(InvoiceNo, Description) as myStruct")\
.select(to_json(col("myStruct")))
该函数还接受与JSON数据源相同的参数字典(map)。可以使用from_json函数解析这个(或其他JSON数据)。这自然要求您指定一个模式,您也可以选择指定选项的map,以及:
// in Scala
import org.apache.spark.sql.functions.from_json
import org.apache.spark.sql.types._
val parseSchema = new StructType(Array(
new StructField("InvoiceNo",StringType,true),
new StructField("Description",StringType,true)))
df.selectExpr("(InvoiceNo, Description) as myStruct")
.select(to_json(col("myStruct")).alias("newJSON"))
.select(from_json(col("newJSON"), parseSchema), col("newJSON")).show(2)
from pyspark.sql.functions import from_json
from pyspark.sql.types import *
parseSchema = StructType((
StructField("InvoiceNo",StringType(),True),
StructField("Description",StringType(),True)))
df.selectExpr("(InvoiceNo, Description) as myStruct")\
.select(to_json(col("myStruct")).alias("newJSON"))\
.select(from_json(col("newJSON"), parseSchema), col("newJSON")).show(2)
6.10. 用户自定义函数UDF
在Spark中,最强大的功能之一就是定义自己的函数。这些用户定义函数(udf)使您能够使用Python或Scala编写自己的自定义转换,甚至使用外部library库。udf可以接受并返回一个或多个列作为输入。Spark udf非常强大,因为您可以用几种不同的编程语言编写它们;您不需要以深奥的格式或领域特定的语言创建它们。它们只是对数据进行操作的函数,一条记录一条记录的操作。默认情况下,这些函数被注册为临时函数,以便在特定的SparkSession或上下文中使用。尽管您可以用Scala、Python或Java编写udf,但是您应该注意一些性能方面的考虑。为了说明这一点,我们将详细介绍创建UDF时发生的情况,如何将其传递给Spark,然后使用UDF执行代码。第一步是实际函数。我们将为本例创建一个简单的例子。让我们写一个power3函数,它接受一个数字并将其提升到3的幂:
// in Scala
val udfExampleDF = spark.range(5).toDF("num")
def power3(number:Double):Double = number * number * number
power3(2.0)
# in Python
udfExampleDF = spark.range(5).toDF("num")
def power3(double_value):
return double_value ** 3
power3(2.0)
在这个简单的例子中,我们可以看到我们的函数按预期工作。我们能够提供单独的输入并产生预期的结果(使用这个简单的测试用例)。到目前为止,我们对输入的期望很高:它必须是特定的类型,不能是空值(参见“处理数据中的空值”)。现在我们已经创建了这些函数并对它们进行了测试,我们需要将它们注册到Spark中,以便能够在所有worker上使用它们。Spark将在Driver程序上序列化该函数,并通过网络将其传输给所有Executor进程。这与语言无关。当你使用这个函数时,实际上会发生两件不同的事情。如果函数是用Scala或Java编写的,您可以在Java虚拟机(JVM)中使用它。这意味着,除了不能利用Spark为内置函数提供的代码生成功能之外,不会有什么性能损失。如果创建或使用大量对象,可能会出现性能问题;我们将在第19章的优化一节中讨论这个问题。如果函数是用Python编写的,就会发生完全不同的情况。Spark在worker上启动一个Python进程,将所有数据序列化为Python能够理解的格式(请记住,它在前面的JVM中),在Python进程中对该数据逐行执行函数,最后将行操作的结果返回给JVM和Spark。图6-2提供了该过程的概述。
警告
启动这个Python进程的代价很高,但真正的代价是将数据序列化到Python。这样做的代价很高,原因有二:它是一个昂贵的计算,而且,在数据进入Python之后,Spark无法管理worker的内存。这意味着,如果worker受到资源限制,您可能会导致它失败(因为JVM和Python都在同一台机器上争夺内存)。我们建议您使用Scala或java编写您的udf—用Scala编写函数所花费的少量时间将始终带来显著的速度提升,而且最重要的是,您仍然可以使用Python中的函数!
现在您已经了解了这个过程,让我们通过一个示例来演示。首先,我们需要注册这个函数,使它作为DataFrame的可用函数:
// in Scala
import org.apache.spark.sql.functions.udf
val power3udf = udf(power3(_:Double):Double)
udfExampleDF.select(power3udf(col("num"))).show()
# in Python
from pyspark.sql.functions import udf
power3udf = udf(power3)
from pyspark.sql.functions import col udfExampleDF.select(power3udf(col("num"))).show(2)
在这一点上,我们只能将其用作DataFrame函数。也就是说,我们不能在字符串表达式中使用它,只能在表达式中使用它。不过,我们也可以将这个UDF注册为Spark SQL函数。这很有价值,因为它使得在SQL中以及跨语言中使用这个函数变得很简单。让我们在Scala中注册这个函数:
// in Scala
spark.udf.register("power3", power3(_:Double):Double)
udfExampleDF.selectExpr("power3(num)").show(2)
因为这个函数是用Spark SQL注册的——我们已经了解到,任何Spark SQL函数或表达式在处理数据流时都可以作为表达式使用——所以我们可以转而使用用Scala和Python编写的UDF。但是,我们没有将它用作DataFrame函数,而是将它用作SQL表达式:
udfExampleDF.selectExpr("power3(num)").show(2)
我们还可以将Python函数注册为SQL函数,并在任何语言中使用它。为了确保函数正常工作,我们还可以指定一个返回类型。正如我们在本节的开头所看到的,Spark管理自己的类型信息,而这些信息与Python的类型并不完全一致。因此,最好在定义函数时为其定义返回类型。需要注意的是,没有必要指定返回类型,但这是最佳实践。如果指定的类型与函数返回的实际类型不一致,Spark将不会抛出错误,而是返回null来指定失败。如果将下面函数中的返回类型切换为double类型,就可以看到这一点:
from pyspark.sql.types import IntegerType, DoubleType
spark.udf.register("power3py", power3, DoubleType())
udfExampleDF.selectExpr("power3py(num)").show(2)
这是因为范围创建整数。当整数在Python中操作时,Python不会将它们转换为浮点数(与Spark的double类型对应的类型),因此我们看到null。我们可以通过确保Python函数返回一个浮点数而不是整数来纠正这个问题,并且函数将正确地运行。当然,在我们注册它们之后,我们也可以从SQL中使用它们中的任何一个:
-- in SQL
SELECT power3(12), power3py(12) -- doesn't work because of return type
如果你想从UDF中选择性地返回一个值,你应该在Python中返回None,在Scala中返回一个选项类型:最后,您还可以通过Hive语法使用UDF/UDAF创建。为此,首先必须在它们创建SparkSession时启用Hive支持(通过sparkssession.builder(). enablehivesupport())。然后可以用SQL注册udf。这只支持预编译的Scala和Java包,所以您需要将它们指定为依赖项:
-- in SQL
CREATE TEMPORARY FUNCTION myFunc AS 'com.organization.hive.udf.FunctionName'
此外,您可以注册这是一个永久性的功能在Hive Metastore中,通过删除TEMPORARY关键字来实现。
6.11. 结束语
本章演示了将Spark SQL扩展到您自己的目的是多么容易,而且扩展的方式不是一些深奥的、特定于领域的语言,而是一些简单的函数,即使不使用Spark,也很容易测试和维护!这是一个非常强大的工具,您可以使用它来指定复杂的业务逻辑,这些业务逻辑可以运行在本地机器上的5行数据上,或者运行在100个节点集群上的tb级数据上!