Spark2 数据处理和实时分析(二)
原文:
zh.annas-archive.org/md5/16D84784AD68D8BF20A18AC23C62DD82译者:飞龙
第九章:测试和调试 Spark
在理想世界中,我们编写的 Spark 代码完美无缺,一切总是运行得完美无瑕,对吧?开个玩笑;实际上,我们知道处理大规模数据集几乎从未那么简单,总会有一些数据点暴露出你代码中的边缘情况。
考虑到上述挑战,因此,在本章中,我们将探讨如果应用程序是分布式的,测试它会有多困难;然后,我们将探讨一些应对方法。简而言之,本章将涵盖以下主题:
-
分布式环境下的测试
-
测试 Spark 应用程序
-
调试 Spark 应用程序
分布式环境下的测试
Leslie Lamport 将分布式系统定义如下:
"分布式系统是指由于某些我从未听说过的机器崩溃,导致我无法完成任何工作的系统。"
通过万维网(又称WWW),一个连接的计算机网络(又称集群)共享资源,是分布式系统的一个好例子。这些分布式环境通常很复杂,经常出现大量异质性。在这些异质环境中进行测试也是具有挑战性的。在本节中,首先,我们将观察在处理此类系统时经常出现的一些常见问题。
分布式环境
分布式系统有众多定义。让我们看一些定义,然后我们将尝试将上述类别与之关联。Coulouris 将分布式系统定义为一个系统,其中位于网络计算机上的硬件或软件组件仅通过消息传递进行通信和协调其动作。另一方面,Tanenbaum 以几种方式定义了这个术语:
-
一组独立的计算机,对系统用户而言,它们表现为一台单一的计算机。
-
由两个或多个独立计算机组成的系统,它们通过同步或异步消息传递协调其处理。
-
分布式系统是一组通过网络连接的自主计算机,其软件设计旨在提供一个集成的计算设施。
现在,基于前面的定义,分布式系统可以分类如下:
-
只有硬件和软件是分布式的:通过 LAN 连接的本地分布式系统。
-
用户是分布式的,但存在运行后端的计算和硬件资源,例如 WWW。
-
用户和硬件/软件都是分布式的:通过 WAN 连接的分布式计算集群。例如,在使用 Amazon AWS、Microsoft Azure、Google Cloud 或 Digital Ocean 的 droplets 时,你可以获得这类计算设施。
分布式系统中的问题
我们将在此讨论软件和硬件测试期间需要注意的一些主要问题,以确保 Spark 作业在集群计算中顺畅运行,集群计算本质上是一种分布式计算环境。
请注意,所有这些问题都是不可避免的,但我们可以至少对其进行优化。您应遵循上一章节中给出的指导和建议。根据卡马尔·希尔·米什拉和阿尼尔·库马尔·特里帕蒂在《国际计算机科学与信息技术杂志》第 5 卷(4),2014 年,4922-4925 页中的《分布式软件系统的某些问题、挑战和问题》,网址为pdfs.semanticscholar.org/4c6d/c4d739bad13bcd0398e5180c1513f18275d8.pdf,其中...
分布式环境中的软件测试挑战
在敏捷软件开发中,与任务相关的一些常见挑战,在最终部署前在分布式环境中测试软件时变得更加复杂。团队成员经常需要在错误激增后并行合并软件组件。然而,根据紧急程度,合并往往发生在测试阶段之前。有时,许多利益相关者分布在不同的团队中。因此,存在巨大的误解潜力,团队往往在其中迷失。
例如,Cloud Foundry(www.cloudfoundry.org/)是一个开源的、高度分布式的 PaaS 软件系统,用于管理云中应用程序的部署和可扩展性。它承诺提供诸如可扩展性、可靠性和弹性等特性,这些特性在 Cloud Foundry 上的部署中是固有的,需要底层分布式系统实施措施以确保鲁棒性、弹性和故障转移。
软件测试的过程早已被熟知包括单元测试、集成测试、冒烟测试、验收测试、可扩展性测试、性能测试和服务质量测试。在 Cloud Foundry 中,分布式系统的测试过程如下图所示:
图 1: 类似 Cloud 的分布式环境中软件测试的一个示例
如前图(第一列)所示,在云这样的分布式环境中进行测试的过程始于对系统中最小的接触点运行单元测试。在所有单元测试成功执行后,运行集成测试以验证作为单个连贯软件系统(第二列)一部分的交互组件的行为,该系统在单个盒子(例如,虚拟机(VM)或裸机)上运行。然而,虽然这些测试验证了系统作为单体的整体行为,但它们并不能保证系统在分布式部署中的有效性。一旦集成测试通过,下一步(第三列)就是验证系统的分布式部署并运行冒烟测试。
如你所知,软件的成功配置和单元测试的执行使我们能够验证系统行为的可接受性。这种验证是通过运行验收测试(第四列)来完成的。现在,为了克服分布式环境中上述问题和挑战,还有其他隐藏的挑战需要由研究人员和大数据工程师解决,但这些实际上超出了本书的范围。
既然我们知道在分布式环境中软件测试面临的真正挑战是什么,现在让我们开始测试我们的 Spark 代码。下一节专门介绍测试 Spark 应用程序。
测试 Spark 应用程序
尝试测试 Spark 代码的方法有很多,取决于它是 Java(你可以进行基本的 JUnit 测试来测试非 Spark 部分)还是 ScalaTest 用于你的 Scala 代码。你还可以通过在本地或小型测试集群上运行 Spark 来进行完整的集成测试。Holden Karau 提供的另一个很棒的选择是使用 Spark-testing base。你可能知道,到目前为止,Spark 还没有原生的单元测试库。尽管如此,我们可以使用以下两个库作为替代方案:
-
ScalaTest
-
Spark-testing base
然而,在开始测试用 Scala 编写的 Spark 应用程序之前,了解单元测试和测试 Scala 方法的一些背景知识是必要的。
测试 Scala 方法
在这里,我们将看到一些测试 Scala 方法的简单技巧。对于 Scala 用户来说,这是最熟悉的单元测试框架(你也可以用它来测试 Java 代码,很快也可以用于 JavaScript)。ScalaTest 支持多种不同的测试风格,每种风格都是为了支持特定类型的测试需求而设计的。详情请参阅 ScalaTest 用户指南,网址为www.scalatest.org/user_guide/selecting_a_style。尽管 ScalaTest 支持多种风格,但最快上手的方法之一是使用以下 ScalaTest 特性,并以TDD(测试驱动开发)风格编写测试:
-
FunSuite -
Assertions -
BeforeAndAfter
欢迎浏览上述 URL 以了解更多关于这些特性的信息,这将使本教程的其余部分顺利进行。
需要注意的是,TDD 是一种开发软件的编程技术,它指出您应该从测试开始开发。因此,它不影响测试的编写方式,而是影响测试的编写时机。在ScalaTest.FunSuite、Assertions和BeforeAndAfter中没有特质或测试风格来强制或鼓励 TDD,它们仅与 xUnit 测试框架更为相似。
在 ScalaTest 的任何风格特质中,有三种断言可用:
-
assert:这在您的 Scala 程序中用于通用断言。 -
assertResult:这有助于区分预期值与实际值。 -
assertThrows:这用于确保一段代码抛出预期的异常。
ScalaTest 的断言定义在特质Assertions中,该特质进一步被Suite扩展。简而言之,Suite特质是所有风格特质的超特质。根据 ScalaTest 文档(www.scalatest.org/user_guide/using_assertions),Assertions特质还提供了以下功能:
-
assume用于条件性地取消测试 -
fail无条件地使测试失败 -
cancel无条件取消测试 -
succeed使测试无条件成功 -
intercept确保一段代码抛出预期的异常,然后对异常进行断言 -
assertDoesNotCompile确保一段代码无法编译 -
assertCompiles确保一段代码能够编译 -
assertTypeError确保一段代码因类型(非解析)错误而无法编译 -
withClue用于添加有关失败的更多信息
从上述列表中,我们将展示其中几个。在您的 Scala 程序中,您可以通过调用assert并传递一个Boolean表达式来编写断言。您可以简单地开始编写您的简单单元测试用例,使用Assertions。Predef是一个对象,其中定义了 assert 的这种行为。请注意,Predef的所有成员都会被导入到您的每个 Scala 源文件中。以下源代码将针对以下情况打印Assertion success:
package com.chapter16.SparkTesting
object SimpleScalaTest {
def main(args: Array[String]):Unit= {
val a = 5
val b = 5
assert(a == b)
println("Assertion success")
}
}
然而,如果您设置a = 2和b = 1,例如,断言将失败,您将看到以下输出:
**图 2:**断言失败的示例
如果您传递一个真表达式,assert 将正常返回。然而,如果提供的表达式为假,assert 将以 AssertionError 异常突然终止。与AssertionError和TestFailedException形式不同,ScalaTest 的 assert 提供了更多信息,它会告诉您确切在哪一行测试用例失败或对于哪个表达式。因此,ScalaTest 的 assert 提供的错误信息比 Scala 的 assert 更优。
例如,对于以下源代码,您应该会遇到TestFailedException,它会告诉您 5 不等于 4:
package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object SimpleScalaTest {
def main(args: Array[String]):Unit= {
val a = 5
val b = 4
assert(a == b)
println("Assertion success")
}
}
下图显示了前述 Scala 测试的输出:
**图 3:**TestFailedException 的一个示例
以下源代码说明了使用assertResult单元测试来测试您方法结果的用法:
package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object AssertResult {
def main(args: Array[String]):Unit= {
val x = 10
val y = 6
assertResult(3) {
x - y
}
}
}
上述断言将会失败,Scala 将抛出异常TestFailedException并打印出Expected 3 but got 4(图 4):
**图 4:**TestFailedException 的另一个示例
现在,让我们看一个单元测试,展示预期的异常:
package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object ExpectedException {
def main(args: Array[String]):Unit= {
val s = "Hello world!"
try {
s.charAt(0)
fail()
} catch {
case _: IndexOutOfBoundsException => // Expected, so continue
}
}
}
如果您尝试访问超出索引范围的数组元素,上述代码将告诉您是否允许访问前述字符串Hello world!的第一个字符。如果您的 Scala 程序能够访问索引中的值,断言将会失败。这也意味着测试案例失败了。因此,由于第一个索引包含字符H,上述测试案例自然会失败,您应该会遇到以下错误信息(图 5):
**图 5:**TestFailedException 的第三个示例
然而,现在让我们尝试访问位于-1位置的索引,如下所示:
package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object ExpectedException {
def main(args: Array[String]):Unit= {
val s = "Hello world!"
try {
s.charAt(-1)
fail()
} catch {
case _: IndexOutOfBoundsException => // Expected, so continue
}
}
}
现在断言应为真,因此测试案例将会通过。最后,代码将正常终止。现在,让我们检查我们的代码片段是否能编译。很多时候,您可能希望确保代表潜在“用户错误”的特定代码顺序根本不编译。目的是检查库对错误的抵抗力,以防止不希望的结果和行为。ScalaTest 的Assertions特质为此目的包括了以下语法:
assertDoesNotCompile("val a: String = 1")
如果您想确保由于类型错误(而非语法错误)某段代码不编译,请使用以下方法:
assertTypeError("val a: String = 1")
语法错误仍会导致抛出TestFailedException。最后,如果您想声明某段代码确实编译通过,您可以通过以下方式使其更加明显:
assertCompiles("val a: Int = 1")
完整示例如下所示:
package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object CompileOrNot {
def main(args: Array[String]):Unit= {
assertDoesNotCompile("val a: String = 1")
println("assertDoesNotCompile True")
assertTypeError("val a: String = 1")
println("assertTypeError True")
assertCompiles("val a: Int = 1")
println("assertCompiles True")
assertDoesNotCompile("val a: Int = 1")
println("assertDoesNotCompile True")
}
}
上述代码的输出显示在以下图中:
**图 6:**多个测试合并进行
由于篇幅限制,我们希望结束基于 Scala 的单元测试。但对于其他单元测试案例,您可以参考Scala 测试指南。
单元测试
在软件工程中,通常会对源代码的各个单元进行测试,以确定它们是否适合使用。这种软件测试方法也称为单元测试。这种测试确保软件工程师或开发者编写的源代码符合设计规范并按预期工作。
另一方面,单元测试的目标是将程序的每个部分(即以模块化的方式)分开。然后尝试观察所有单独的部分是否正常工作。单元测试在任何软件系统中都有几个好处:
-
早期发现问题: 它在开发周期的早期发现错误或规范中缺失的部分。
-
便于变更: 它有助于重构...
测试 Spark 应用程序
我们已经看到了如何使用 Scala 内置的ScalaTest包测试 Scala 代码。然而,在本小节中,我们将看到如何测试我们用 Scala 编写的 Spark 应用程序。以下三种方法将被讨论:
-
方法 1: 使用 JUnit 测试 Spark 应用程序
-
方法 2: 使用
ScalaTest包测试 Spark 应用程序 -
方法 3: 使用 Spark 测试基进行 Spark 应用程序测试
方法 1 和 2 将在这里讨论,并附带一些实际代码。然而,方法 3 的详细讨论将在下一小节中提供。为了保持理解简单明了,我们将使用著名的单词计数应用程序来演示方法 1 和 2。
方法 1:使用 Scala JUnit 测试
假设你已经编写了一个 Scala 应用程序,它可以告诉你文档或文本文件中有多少单词,如下所示:
package com.chapter16.SparkTestingimport org.apache.spark._import org.apache.spark.sql.SparkSessionclass wordCounterTestDemo { val spark = SparkSession .builder .master("local[*]") .config("spark.sql.warehouse.dir", "E:/Exp/") .appName(s"OneVsRestExample") .getOrCreate() def myWordCounter(fileName: String): Long = { val input = spark.sparkContext.textFile(fileName) val counts = input.flatMap(_.split(" ")).distinct() val counter = counts.count() counter }}
前面的代码简单地解析一个文本文件,并通过简单地分割单词执行flatMap操作。然后,它执行...
方法 2:使用 FunSuite 测试 Scala 代码
现在,让我们通过仅返回文档中文本的 RDD 来重新设计前面的测试案例,如下所示:
package com.chapter16.SparkTesting
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
class wordCountRDD {
def prepareWordCountRDD(file: String, spark: SparkSession): RDD[(String, Int)] = {
val lines = spark.sparkContext.textFile(file)
lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
}
}
因此,前面类中的prepareWordCountRDD()方法返回一个字符串和整数值的 RDD。现在,如果我们想要测试prepareWordCountRDD()方法的功能,我们可以通过扩展测试类与FunSuite和BeforeAndAfterAll从 Scala 的ScalaTest包来更明确地进行。测试工作的方式如下:
-
通过扩展
FunSuite和BeforeAndAfterAll从 Scala 的ScalaTest包来扩展测试类 -
覆盖
beforeAll()方法以创建 Spark 上下文 -
使用
test()方法执行测试,并在test()方法内部使用assert()方法 -
覆盖
afterAll()方法以停止 Spark 上下文
基于前面的步骤,让我们看一个用于测试前面prepareWordCountRDD()方法的类:
package com.chapter16.SparkTesting
import org.scalatest.{ BeforeAndAfterAll, FunSuite }
import org.scalatest.Assertions._
import org.apache.spark.sql.SparkSession
import org.apache.spark.rdd.RDD
class wordCountTest2 extends FunSuite with BeforeAndAfterAll {
var spark: SparkSession = null
def tokenize(line: RDD[String]) = {
line.map(x => x.split(' ')).collect()
}
override def beforeAll() {
spark = SparkSession
.builder
.master("local[*]")
.config("spark.sql.warehouse.dir", "E:/Exp/")
.appName(s"OneVsRestExample")
.getOrCreate()
}
test("Test if two RDDs are equal") {
val input = List("To be,", "or not to be:", "that is the question-", "William Shakespeare")
val expected = Array(Array("To", "be,"), Array("or", "not", "to", "be:"), Array("that", "is", "the", "question-"), Array("William", "Shakespeare"))
val transformed = tokenize(spark.sparkContext.parallelize(input))
assert(transformed === expected)
}
test("Test for word count RDD") {
val fileName = "C:/Users/rezkar/Downloads/words.txt"
val obj = new wordCountRDD
val result = obj.prepareWordCountRDD(fileName, spark)
assert(result.count() === 214)
}
override def afterAll() {
spark.stop()
}
}
第一个测试表明,如果两个 RDD 以两种不同的方式实现,内容应该相同。因此,第一个测试应该通过。我们将在下面的例子中看到这一点。现在,对于第二个测试,正如我们之前所见,RDD 的单词计数为 214,但让我们暂时假设它是未知的。如果它恰好是 214,测试案例应该通过,这是其预期行为。
因此,我们期望两个测试都通过。现在,在 Eclipse 中,运行测试套件为ScalaTest-File,如下所示:
图 10: 以 ScalaTest-File 形式运行测试套件
现在您应该观察到以下输出(图 11)。输出显示了我们执行了多少测试案例,以及其中有多少通过了、失败了、被取消了、被忽略了或处于待定状态。它还显示了执行整个测试所需的时间。
图 11: 运行两个测试套件作为 ScalaTest-file 时的测试结果
太棒了!测试案例通过了。现在,让我们尝试在两个单独的测试中通过使用test()方法改变断言中的比较值:
test("Test for word count RDD") {
val fileName = "data/words.txt"
val obj = new wordCountRDD
val result = obj.prepareWordCountRDD(fileName, spark)
assert(result.count() === 210)
}
test("Test if two RDDs are equal") {
val input = List("To be", "or not to be:", "that is the question-", "William Shakespeare")
val expected = Array(Array("To", "be,"), Array("or", "not", "to", "be:"), Array("that", "is", "the", "question-"), Array("William", "Shakespeare"))
val transformed = tokenize(spark.sparkContext.parallelize(input))
assert(transformed === expected)
}
现在,您应该预料到测试案例会失败。现在运行之前的类作为ScalaTest-File(图 12):
图 12: 运行前两个测试套件作为 ScalaTest-File 时的测试结果
做得好!我们已经学会了如何使用 Scala 的 FunSuite 进行单元测试。然而,如果您仔细评估前面的方法,您应该同意存在一些缺点。例如,您需要确保SparkContext的创建和销毁有明确的管理。作为开发者或程序员,您需要为测试一个示例方法编写更多行代码。有时,代码重复发生,因为Before和After步骤必须在所有测试套件中重复。然而,这一点有争议,因为公共代码可以放在一个公共特质中。
现在的问题是我们如何能改善我们的体验?我的建议是使用 Spark 测试基底来使生活更轻松、更直接。我们将讨论如何使用 Spark 测试基底进行单元测试。
方法 3:利用 Spark 测试基底简化生活
Spark 测试基底助您轻松测试大部分 Spark 代码。那么,这种方法的优势何在?实际上,优势颇多。例如,使用此方法,代码不会冗长,却能得到非常简洁的代码。其 API 本身比 ScalaTest 或 JUnit 更为丰富。支持多种语言,如 Scala、Java 和 Python。内置 RDD 比较器。还可用于测试流应用程序。最后且最重要的是,它支持本地和集群模式测试。这对于分布式环境中的测试至关重要。
GitHub 仓库位于github.com/holdenk/spark-testing-base。
开始之前...
在 Windows 上配置 Hadoop 运行时
我们已经看到如何在 Eclipse 或 IntelliJ 上测试用 Scala 编写的 Spark 应用程序,但还有一个潜在问题不容忽视。虽然 Spark 可以在 Windows 上运行,但 Spark 设计为在类 UNIX 操作系统上运行。因此,如果您在 Windows 环境中工作,则需要格外小心。
在使用 Eclipse 或 IntelliJ 为 Windows 上的数据分析、机器学习、数据科学或深度学习应用程序开发 Spark 应用程序时,您可能会遇到 I/O 异常错误,您的应用程序可能无法成功编译或可能被中断。实际上,Spark 期望 Windows 上也有 Hadoop 的运行时环境。例如,如果您第一次在 Eclipse 上运行 Spark 应用程序,比如KMeansDemo.scala,您将遇到一个 I/O 异常,如下所示:
17/02/26 13:22:00 ERROR Shell: Failed to locate the winutils binary in the hadoop binary path java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries.
原因是默认情况下,Hadoop 是为 Linux 环境开发的,如果您在 Windows 平台上开发 Spark 应用程序,则需要一个桥梁,为 Spark 提供一个 Hadoop 运行时环境,以便正确执行。I/O 异常的详细信息可以在下图看到:
**图 14:**由于未能在 Hadoop 二进制路径中定位 winutils 二进制文件,导致发生了 I/O 异常
那么,如何解决这个问题呢?解决方案很简单。正如错误信息所说,我们需要一个可执行文件,即winutils.exe。现在从github.com/steveloughran/winutils/tree/master/hadoop-2.7.1/bin下载winutils.exe文件,将其粘贴到 Spark 分发目录中,并配置 Eclipse。更具体地说,假设包含 Hadoop 的 Spark 分发位于C:/Users/spark-2.1.0-bin-hadoop2.7。在 Spark 分发中,有一个名为 bin 的目录。现在,将可执行文件粘贴到那里(即path = C:/Users/spark-2.1.0-binhadoop2.7/bin/)。
解决方案的第二阶段是前往 Eclipse,然后选择主类(即本例中的KMeansDemo.scala),接着进入运行菜单。从运行菜单中,选择运行配置选项,并从那里选择环境标签,如图所示:
**图 15:**解决因 Hadoop 二进制路径中缺少 winutils 二进制文件而发生的 I/O 异常
如果您选择了该标签,您将有机会使用 JVM 为 Eclipse 创建一个新的环境变量。现在创建一个名为HADOOP_HOME的新环境变量,并将其值设置为C:/Users/spark-2.1.0-bin-hadoop2.7/。现在点击应用按钮并重新运行您的应用程序,您的问题应该得到解决。
需要注意的是,在使用 PySpark 在 Windows 上运行 Spark 时,也需要winutils.exe文件。
请注意,上述解决方案也适用于调试您的应用程序。有时,即使出现上述错误,您的 Spark 应用程序仍能正常运行。然而,如果数据集规模较大,很可能会出现上述错误。
调试 Spark 应用程序
在本节中,我们将了解如何调试在本地(在 Eclipse 或 IntelliJ 上)、独立模式或 YARN 或 Mesos 集群模式下运行的 Spark 应用程序。然而,在深入之前,有必要了解 Spark 应用程序中的日志记录。
Spark 使用 log4j 进行日志记录的回顾
如前所述,Spark 使用 log4j 进行自己的日志记录。如果正确配置了 Spark,所有操作都会记录到 shell 控制台。可以从以下图表中看到文件的示例快照:
**图 16:**log4j.properties 文件的快照
将默认的 spark-shell 日志级别设置为 WARN。运行 spark-shell 时,此类的日志级别用于覆盖根日志记录器的日志级别,以便用户可以为 shell 和常规 Spark 应用设置不同的默认值。我们还需要在启动由执行器执行并由驱动程序管理的作业时附加 JVM 参数。为此,您应该编辑conf/spark-defaults.conf。简而言之,可以添加以下选项:
spark.executor.extraJavaOptions=-Dlog4j.configuration=file:/usr/local/spark-2.1.1/conf/log4j.properties spark.driver.extraJavaOptions=-Dlog4j.configuration=file:/usr/local/spark-2.1.1/conf/log4j.properties
为了使讨论更清晰,我们需要隐藏所有由 Spark 生成的日志。然后我们可以将它们重定向到文件系统中进行记录。另一方面,我们希望自己的日志记录在 shell 和单独的文件中,以免与 Spark 的日志混淆。从这里开始,我们将指示 Spark 指向存放我们自己日志的文件,在本例中为/var/log/sparkU.log。当应用程序启动时,Spark 会拾取这个log4j.properties文件,因此我们除了将其放置在提及的位置外,无需做其他事情:
package com.chapter14.Serilazition
import org.apache.log4j.LogManager
import org.apache.log4j.Level
import org.apache.spark.sql.SparkSession
object myCustomLog {
def main(args: Array[String]): Unit = {
val log = LogManager.getRootLogger
//Everything is printed as INFO once the log level is set to INFO untill you set the level to new level for example WARN.
log.setLevel(Level.INFO)
log.info("Let's get started!")
// Setting logger level as WARN: after that nothing prints other than WARN
log.setLevel(Level.WARN)
// Creating Spark Session
val spark = SparkSession
.builder
.master("local[*]")
.config("spark.sql.warehouse.dir", "E:/Exp/")
.appName("Logging")
.getOrCreate()
// These will note be printed!
log.info("Get prepared!")
log.trace("Show if there is any ERROR!")
//Started the computation and printing the logging information
log.warn("Started")
spark.sparkContext.parallelize(1 to 20).foreach(println)
log.warn("Finished")
}
}
在前面的代码中,一旦将日志级别设置为INFO,所有内容都会作为 INFO 打印,直到您将级别设置为新的级别,例如WARN。然而,在那之后,不会有任何信息、跟踪等被打印出来。此外,log4j 支持 Spark 的几个有效日志级别。成功执行前面的代码应该会产生以下输出:
17/05/13 16:39:14 INFO root: Let's get started!
17/05/13 16:39:15 WARN root: Started
4
1
2
5
3
17/05/13 16:39:16 WARN root: Finished
您还可以在conf/log4j.properties中设置 Spark shell 的默认日志记录。Spark 提供了一个 log4j 的属性文件模板,我们可以扩展和修改该文件以在 Spark 中进行日志记录。转到SPARK_HOME/conf目录,您应该会看到log4j.properties.template文件。在重命名后,您应该使用以下conf/log4j.properties.template作为log4j.properties。在基于 IDE 的环境(如 Eclipse)中开发 Spark 应用程序时,您可以将log4j.properties文件放在项目目录下。但是,要完全禁用日志记录,只需将log4j.logger.org标志设置为OFF,如下所示:
log4j.logger.org=OFF
到目前为止,一切都很容易。然而,我们还没有注意到前述代码段中的一个问题。org.apache.log4j.Logger类的一个缺点是它不是可序列化的,这意味着我们在使用 Spark API 的某些部分进行操作时,不能在闭包内部使用它。例如,假设我们在 Spark 代码中执行以下操作:
object myCustomLogger {
def main(args: Array[String]):Unit= {
// Setting logger level as WARN
val log = LogManager.getRootLogger
log.setLevel(Level.WARN)
// Creating Spark Context
val conf = new SparkConf().setAppName("My App").setMaster("local[*]")
val sc = new SparkContext(conf)
//Started the computation and printing the logging information
//log.warn("Started")
val i = 0
val data = sc.parallelize(i to 100000)
data.map{number =>
log.info(“My number”+ i)
number.toString
}
//log.warn("Finished")
}
}
你应该会遇到一个异常,它会说Task不可序列化,如下所示:
org.apache.spark.SparkException: Job aborted due to stage failure: Task not serializable: java.io.NotSerializableException: ...
Exception in thread "main" org.apache.spark.SparkException: Task not serializable
Caused by: java.io.NotSerializableException: org.apache.log4j.spi.RootLogger
Serialization stack: object not serializable
首先,我们可以尝试用一种简单的方法来解决这个问题。你可以做的就是让执行实际操作的 Scala 类Serializable,使用extends Serializable。例如,代码如下所示:
class MyMapper(n: Int) extends Serializable {
@transient lazy val log = org.apache.log4j.LogManager.getLogger("myLogger")
def logMapper(rdd: RDD[Int]): RDD[String] =
rdd.map { i =>
log.warn("mapping: " + i)
(i + n).toString
}
}
本节旨在进行关于日志记录的讨论。然而,我们借此机会使其更适用于通用 Spark 编程和问题。为了更有效地克服task not serializable错误,编译器将尝试发送整个对象(不仅仅是 lambda),使其可序列化,并强制 Spark 接受它。然而,这会显著增加数据混洗,尤其是对于大型对象!其他方法是将整个类设为Serializable,或者仅在传递给 map 操作的 lambda 函数中声明实例。有时,在节点之间保留不可Serializable的对象可能有效。最后,使用forEachPartition()或mapPartitions()而不是仅使用map(),并创建不可Serializable的对象。总之,这些是解决问题的方法:
-
序列化该类
-
仅在传递给 map 的 lambda 函数中声明实例
-
将不可序列化对象设为静态,并在每台机器上创建一次
-
调用
forEachPartition ()或mapPartitions()而不是map(),并创建不可序列化对象
在前述代码中,我们使用了注解@transient lazy,它标记Logger类为非持久性的。另一方面,包含应用方法(即MyMapperObject)的对象,用于实例化MyMapper类的对象,如下所示:
//Companion object
object MyMapper {
def apply(n: Int): MyMapper = new MyMapper(n)
}
最后,包含main()方法的对象如下:
//Main object
object myCustomLogwithClosureSerializable {
def main(args: Array[String]) {
val log = LogManager.getRootLogger
log.setLevel(Level.WARN)
val spark = SparkSession
.builder
.master("local[*]")
.config("spark.sql.warehouse.dir", "E:/Exp/")
.appName("Testing")
.getOrCreate()
log.warn("Started")
val data = spark.sparkContext.parallelize(1 to 100000)
val mapper = MyMapper(1)
val other = mapper.logMapper(data)
other.collect()
log.warn("Finished")
}
现在,让我们看另一个例子,它提供了更好的洞察力,以继续解决我们正在讨论的问题。假设我们有以下类,用于计算两个整数的乘法:
class MultiplicaitonOfTwoNumber {
def multiply(a: Int, b: Int): Int = {
val product = a * b
product
}
}
现在,本质上,如果你尝试使用这个类来计算 lambda 闭包中的乘法,使用map(),你将会遇到我们之前描述的Task Not Serializable错误。现在我们只需简单地使用foreachPartition()和内部的 lambda,如下所示:
val myRDD = spark.sparkContext.parallelize(0 to 1000)
myRDD.foreachPartition(s => {
val notSerializable = new MultiplicaitonOfTwoNumber
println(notSerializable.multiply(s.next(), s.next()))
})
现在,如果你编译它,它应该返回期望的结果。为了方便,包含main()方法的完整代码如下:
package com.chapter16.SparkTesting
import org.apache.spark.sql.SparkSession
class MultiplicaitonOfTwoNumber {
def multiply(a: Int, b: Int): Int = {
val product = a * b
product
}
}
object MakingTaskSerilazible {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder
.master("local[*]")
.config("spark.sql.warehouse.dir", "E:/Exp/")
.appName("MakingTaskSerilazible")
.getOrCreate()
val myRDD = spark.sparkContext.parallelize(0 to 1000)
myRDD.foreachPartition(s => {
val notSerializable = new MultiplicaitonOfTwoNumber
println(notSerializable.multiply(s.next(), s.next()))
})
}
}
输出如下:
0
5700
1406
156
4032
7832
2550
650
调试 Spark 应用程序
在本节中,我们将讨论如何在本地 Eclipse 或 IntelliJ 上调试运行在独立模式或集群模式(在 YARN 或 Mesos 上)的 Spark 应用程序。在开始之前,您还可以阅读调试文档:hortonworks.com/hadoop-tutorial/setting-spark-development-environment-scala/。
在 Eclipse 上以 Scala 调试方式调试 Spark 应用程序
要实现这一目标,只需将您的 Eclipse 配置为将 Spark 应用程序作为常规 Scala 代码进行调试。配置方法为选择运行 | 调试配置 | Scala 应用程序,如图所示:
**图 17:**配置 Eclipse 以将 Spark 应用程序作为常规 Scala 代码进行调试
假设我们想要调试我们的KMeansDemo.scala,并要求 Eclipse(您可以在 InteliJ IDE 中拥有类似选项)从第 56 行开始执行并在第 95 行设置断点。为此,请以调试模式运行您的 Scala 代码,您应该在 Eclipse 上观察到以下场景:
**图 18:**在 Eclipse 上调试 Spark 应用程序
然后,Eclipse 将在您要求它停止执行的第 95 行暂停,如下面的截图所示:
**图 19:**在 Eclipse 上调试 Spark 应用程序(断点)
总之,为了简化上述示例,如果在第 56 行和第 95 行之间出现任何错误,Eclipse 将显示错误实际发生的位置。否则,如果没有中断,它将遵循正常的工作流程。
调试作为本地和独立模式运行的 Spark 作业
在本地或独立模式下调试您的 Spark 应用程序时,您应该知道调试驱动程序程序和调试其中一个执行程序是不同的,因为使用这两种节点需要向spark-submit传递不同的提交参数。在本节中,我将使用端口 4000 作为地址。例如,如果您想调试驱动程序程序,您可以在您的spark-submit命令中添加以下内容:
--driver-java-options -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4000
之后,您应将远程调试器设置为连接到您提交驱动程序程序的节点。对于上述情况,端口号 4000 是...
在 YARN 或 Mesos 集群上调试 Spark 应用程序
当您在 YARN 上运行 Spark 应用程序时,有一个选项可以通过修改yarn-env.sh来启用:
YARN_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=4000 $YARN_OPTS"
现在,远程调试将通过 Eclipse 或 IntelliJ IDE 上的 4000 端口可用。第二种方法是设置SPARK_SUBMIT_OPTS。您可以使用 Eclipse 或 IntelliJ 开发可以提交到远程多节点 YARN 集群执行的 Spark 应用程序。我所做的是在 Eclipse 或 IntelliJ 上创建一个 Maven 项目,将我的 Java 或 Scala 应用程序打包成 jar 文件,然后作为 Spark 作业提交。然而,为了将 IDE(如 Eclipse 或 IntelliJ)调试器附加到您的 Spark 应用程序,您可以使用SPARK_SUBMIT_OPTS环境变量定义所有提交参数,如下所示:
$ export SPARK_SUBMIT_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4000
然后如下提交您的 Spark 作业(请根据您的需求和设置相应地更改值):
$ SPARK_HOME/bin/spark-submit \
--class "com.chapter13.Clustering.KMeansDemo" \
--master yarn \
--deploy-mode cluster \
--driver-memory 16g \
--executor-memory 4g \
--executor-cores 4 \
--queue the_queue \
--num-executors 1\
--executor-cores 1 \
--conf "spark.executor.extraJavaOptions=-agentlib:jdwp=transport=dt_socket,server=n,address= host_name_to_your_computer.org:4000,suspend=n" \
--driver-java-options -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4000 \
KMeans-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt
执行上述命令后,它将等待您连接调试器,如下所示:Listening for transport dt_socket at address: 4000。现在,您可以在 IntelliJ 调试器中配置 Java 远程应用程序(Scala 应用程序也可以),如下面的截图所示:
**图 20:**在 IntelliJ 上配置远程调试器
对于上述情况,10.200.1.101 是远程计算节点上运行 Spark 作业的基本 IP 地址。最后,您需要通过点击 IntelliJ 的运行菜单下的调试来启动调试器。然后,如果调试器连接到您的远程 Spark 应用程序,您将在 IntelliJ 的应用程序控制台中看到日志信息。现在,如果您可以设置断点,其余的调试就是正常的了。
下图展示了在 IntelliJ 中暂停带有断点的 Spark 作业时的示例视图:
**图 21:**在 IntelliJ 中暂停带有断点的 Spark 作业时的示例视图
尽管效果良好,但有时我发现使用SPARK_JAVA_OPTS在 Eclipse 甚至 IntelliJ 的调试过程中帮助不大。相反,在运行 Spark 作业的真实集群(YARN、Mesos 或 AWS)上,使用并导出SPARK_WORKER_OPTS和SPARK_MASTER_OPTS,如下所示:
$ export SPARK_WORKER_OPTS="-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=4000,suspend=n"
$ export SPARK_MASTER_OPTS="-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=4000,suspend=n"
然后如下启动 Master 节点:
$ SPARKH_HOME/sbin/start-master.sh
现在打开一个 SSH 连接到运行 Spark 作业的远程机器,并将本地主机映射到 4000(即localhost:4000)到host_name_to_your_computer.org:5000,假设集群位于host_name_to_your_computer.org:5000并监听端口 5000。现在,您的 Eclipse 将认为您只是在调试本地 Spark 应用程序或进程。然而,要实现这一点,您需要在 Eclipse 上配置远程调试器,如下所示:
**图 22:**在 Eclipse 上连接远程主机以调试 Spark 应用程序
就这样!现在你可以在实时集群上调试,就像在桌面一样。前面的示例是在 Spark Master 设置为 YARN-client 的情况下运行的。然而,在 Mesos 集群上运行时也应该有效。如果你使用的是 YARN-cluster 模式,你可能需要将驱动程序设置为附加到调试器,而不是将调试器附加到驱动程序,因为你事先不一定知道驱动程序将执行的模式。
使用 SBT 调试 Spark 应用程序
上述设置主要适用于使用 Maven 项目的 Eclipse 或 IntelliJ。假设你已经完成了应用程序,并正在你喜欢的 IDE(如 IntelliJ 或 Eclipse)中工作,如下所示:
object DebugTestSBT { def main(args: Array[String]): Unit = { val spark = SparkSession .builder .master("local[*]") .config("spark.sql.warehouse.dir", "C:/Exp/") .appName("Logging") .getOrCreate() spark.sparkContext.setCheckpointDir("C:/Exp/") println("-------------Attach debugger now!--------------") Thread.sleep(8000) // code goes here, with breakpoints set on the lines you want to pause }}
现在,如果你想将这项工作部署到本地集群(独立模式),第一步是打包...
总结
在本章中,你看到了测试和调试 Spark 应用程序的难度。在分布式环境中,这些甚至可能更为关键。我们还讨论了一些高级方法来全面应对这些问题。总之,你学习了在分布式环境中的测试方法。然后你学习了测试 Spark 应用程序的更好方法。最后,我们讨论了一些调试 Spark 应用程序的高级方法。
这基本上是我们关于 Spark 高级主题的小旅程的结束。现在,我们给读者的一般建议是,如果你是数据科学、数据分析、机器学习、Scala 或 Spark 的相对新手,你应该首先尝试了解你想执行哪种类型的分析。更具体地说,例如,如果你的问题是机器学习问题,尝试猜测哪种学习算法最适合,即分类、聚类、回归、推荐或频繁模式挖掘。然后定义和制定问题,之后你应该根据我们之前讨论的 Spark 特征工程概念生成或下载适当的数据。另一方面,如果你认为你可以使用深度学习算法或 API 解决问题,你应该使用其他第三方算法并与 Spark 集成,直接工作。
我们给读者的最终建议是定期浏览 Spark 官网(位于spark.apache.org/)以获取更新,并尝试将常规的 Spark 提供的 API 与其他第三方应用程序或工具结合使用,以实现最佳的协同效果。
第十章:使用 Spark 和 Scala 进行实用机器学习
在本章中,我们将涵盖:
-
配置 IntelliJ 以与 Spark 配合工作并运行 Spark ML 示例代码
-
运行 Spark 中的示例 ML 代码
-
识别实用机器学习的数据源
-
使用 IntelliJ IDE 运行您的第一个 Apache Spark 2.0 程序
-
如何向您的 Spark 程序添加图形
简介
随着集群计算的最新进展,以及大数据的兴起,机器学习领域已被推到了计算的前沿。长期以来,人们一直梦想有一个能够实现大规模数据科学的交互式平台,现在这个梦想已成为现实。
以下三个领域的结合使得大规模交互式数据科学得以实现并加速发展:
-
Apache Spark:一个统一的数据科学技术平台,它将快速计算引擎和容错数据结构结合成一个设计精良且集成的解决方案
-
机器学习:人工智能的一个领域,使机器能够模仿原本专属于人脑的一些任务
-
Scala:一种基于现代 JVM 的语言,它建立在传统语言之上,但将函数式和面向对象的概念结合在一起,而不会像其他语言那样冗长
首先,我们需要设置开发环境,它将包括以下组件:
-
Spark
-
IntelliJ 社区版 IDE
-
Scala
本章中的配方将为您提供详细的安装和配置 IntelliJ IDE、Scala 插件和 Spark 的说明。开发环境设置完成后,我们将继续运行一个 Spark ML 示例代码来测试设置。
Apache Spark
Apache Spark 正成为大数据分析的事实标准平台和行业语言,并作为Hadoop范式的补充。Spark 使数据科学家能够以最有利于其工作流程的方式直接开始工作。Spark 的方法是在完全分布式的方式下处理工作负载,无需MapReduce(MR)或重复将中间结果写入磁盘。
Spark 提供了一个易于使用的统一技术栈中的分布式框架,这使其成为数据科学项目的首选平台,这些项目往往需要一个最终合并到解决方案的迭代算法。由于这些算法的内部工作原理,它们会产生大量的...
机器学习
机器学习的目的是制造能够模仿人类智能并自动化一些传统上由人脑完成的任务的机器和设备。机器学习算法旨在在相对较短的时间内处理大量数据集,并近似出人类需要更长时间才能处理出的答案。
(机器学习领域可以分为多种形式,从高层次上可以分为监督学习和无监督学习。监督学习算法是一类使用训练集(即标记数据)来计算概率分布或图形模型的 ML 算法,进而使它们能够在没有进一步人工干预的情况下对新数据点进行分类。无监督学习是一种机器学习算法,用于从没有标签响应的输入数据集中提取推断。)
(Spark 开箱即提供丰富的 ML 算法集合,无需进一步编码即可部署在大型数据集上。下图展示了 Spark 的 MLlib 算法作为思维导图。Spark 的 MLlib 旨在利用并行性,同时拥有容错分布式数据结构。Spark 将此类数据结构称为 弹性分布式数据集 或 RDD。)
Scala
Scala 是一种新兴的现代编程语言,作为传统编程语言如 Java 和 C++ 的替代品而崭露头角。Scala 是一种基于 JVM 的语言,不仅提供简洁的语法,避免了传统的样板代码,还将面向对象和函数式编程融合到一个极其精炼且功能强大的类型安全语言中。
Scala 采用灵活且富有表现力的方法,使其非常适合与 Spark 的 MLlib 交互。Spark 本身是用 Scala 编写的,这一事实有力地证明了 Scala 语言是一种全功能编程语言,可用于创建具有高性能需求的复杂系统代码。
Scala 基于 Java 的传统...
Software versions and libraries used in this book
The following table provides a detailed list of software versions and libraries used in this book. If you follow the installation instructions covered in this chapter, it will include most of the items listed here. Any other JAR or library files that may be required for specific recipes are covered via additional installation instructions in the respective recipes:
| Core systems | Version |
|---|---|
| Spark | 2.0.0 |
| Java | 1.8 |
| IntelliJ IDEA | 2016.2.4 |
| Scala-sdk | 2.11.8 |
Miscellaneous JARs that will be required are as follows:
| Miscellaneous JARs | Version |
|---|---|
bliki-core | 3.0.19 |
breeze-viz | 0.12 |
Cloud9 | 1.5.0 |
Hadoop-streaming | 2.2.0 |
JCommon | 1.0.23 |
JFreeChart | 1.0.19 |
lucene-analyzers-common | 6.0.0 |
Lucene-Core | 6.0.0 |
scopt | 3.3.0 |
spark-streaming-flume-assembly | 2.0.0 |
spark-streaming-kafka-0-8-assembly | 2.0.0 |
We have additionally tested all the recipes in this book on Spark 2.1.1 and found that the programs executed as expected. It is recommended for learning purposes you use the software versions and libraries listed in these tables.
为了跟上快速变化的 Spark 环境和文档,本书中提到的 Spark 文档的 API 链接指向最新的 Spark 2.x.x 版本,但食谱中的 API 参考明确针对 Spark 2.0.0。
本书提供的所有 Spark 文档链接将指向 Spark 网站上的最新文档。如果您希望查找特定版本的 Spark(例如,Spark 2.0.0)的文档,请使用以下 URL 在 Spark 网站上查找相关文档:
spark.apache.org/documentation.html
为了清晰起见,我们已尽可能简化代码,而不是展示 Scala 的高级特性。
配置 IntelliJ 以配合 Spark 运行 Spark ML 示例代码
在运行 Spark 或本书列出的任何程序提供的示例之前,我们需要进行一些配置以确保项目设置正确。
准备就绪
在配置项目结构和全局库时,我们需要特别小心。设置完成后,我们运行 Spark 团队提供的示例 ML 代码以验证安装。示例代码可在 Spark 目录下找到,或通过下载包含示例的 Spark 源代码获取。
如何操作...
以下是配置 IntelliJ 以配合 Spark MLlib 工作以及在示例目录中运行 Spark 提供的示例 ML 代码的步骤。示例目录可在您的 Spark 主目录中找到。使用 Scala 示例继续:
- 点击“项目结构...”选项,如以下截图所示,以配置项目设置:
- 验证设置:
-
配置全局库。选择 Scala SDK 作为您的全局库:
-
选择新的 Scala SDK 的 JAR 文件并允许下载...
还有更多...
在 Spark 2.0 之前,我们需要 Google 的另一个库Guava来促进 I/O 并提供定义表的一组丰富方法,然后让 Spark 在集群中广播它们。由于难以解决的依赖问题,Spark 2.0 不再使用 Guava 库。如果您使用的是 2.0 之前的 Spark 版本(在 1.5.2 版本中需要),请确保使用 Guava 库。Guava 库可从此 URL 访问:
您可能希望使用 Guava 版本 15.0,该版本可在此处找到:
mvnrepository.com/artifact/com.google.guava/guava/15.0
如果您使用的是之前博客中的安装说明,请确保从安装集中排除 Guava 库。
另请参见
如果完成 Spark 安装还需要其他第三方库或 JAR,您可以在以下 Maven 仓库中找到它们:
repo1.maven.org/maven2/org/apache/spark/
从 Spark 运行样本 ML 代码
我们可以通过简单地下载 Spark 源树中的样本代码并将其导入 IntelliJ 以确保其运行来验证设置。
准备就绪
我们首先运行样本中的逻辑回归代码以验证安装。在下一节中,我们将编写自己的版本并检查输出,以便理解其工作原理。
如何操作...
- 转到源目录并选择一个 ML 样本代码文件运行。我们选择了逻辑回归示例。
如果您在目录中找不到源代码,您可以随时下载 Spark 源码,解压缩,然后相应地提取示例目录。
- 选择示例后,选择“编辑配置...”,如下面的截图所示:
-
在配置选项卡中,定义以下选项:
-
VM 选项:所示选项允许您运行独立 Spark 集群
-
程序参数:我们需要传递给程序的内容
-
- 通过转到运行'LogisticRegressionExample'来运行逻辑回归,如下面的截图所示:
- 验证退出代码,并确保它与下面的截图所示相同:
识别实用机器学习的数据源
过去为机器学习项目获取数据是一个挑战。然而,现在有一系列特别适合机器学习的公共数据源。
准备就绪
除了大学和政府来源外,还有许多其他开放数据源可用于学习和编写自己的示例和项目。我们将列出数据源,并向您展示如何最好地获取和下载每章的数据。
如何操作...
以下是一些值得探索的开源数据列表,如果您想在此领域开发应用程序:
-
UCI 机器学习库:这是一个具有搜索功能的广泛库。在撰写本文时,已有超过 350 个数据集。您可以点击
archive.ics.uci.edu/ml/index.html链接查看所有数据集,或使用简单搜索(Ctrl + F)查找特定数据集。 -
Kaggle 数据集:你需要创建一个账户,但你可以下载任何用于学习和参加机器学习竞赛的数据集。
www.kaggle.com/competitions链接提供了探索和了解更多关于 Kaggle 以及机器学习竞赛内部运作的详细信息。...
另请参阅
机器学习数据的其它来源:
-
来自 Lending Club 的金融数据集
www.lendingclub.com/info/download-data.action -
亚马逊 AWS 公共数据集
aws.amazon.com/public-data-sets/ -
来自 ImageNet 的标记视觉数据
www.image-net.org -
人口普查数据集
www.census.gov -
编译的 YouTube 数据集
netsg.cs.sfu.ca/youtubedata/ -
来自 MovieLens 网站的收集评分数据
grouplens.org/datasets/movielens/ -
公开的安然数据集
www.cs.cmu.edu/~enron/ -
经典书籍《统计学习要素》的数据集
statweb.stanford.edu/~tibs/ElemStatLearn/data.htmlIMDB -
电影数据集
www.imdb.com/interfaces -
语音和音频数据集
labrosa.ee.columbia.edu/projects/ -
人脸识别数据
www.face-rec.org/databases/ -
康奈尔大学的大量数据集
arxiv.org/help/bulk_data_s3 -
世界银行数据集
data.worldbank.org -
世界词网词汇数据库
wordnet.princeton.edu -
纽约警察局的碰撞数据
nypd.openscrape.com/#/ -
国会唱名表决等数据集
voteview.com/dwnl.htm -
斯坦福大学的大型图数据集
snap.stanford.edu/data/index.html -
来自 datahub 的丰富数据集
datahub.io/dataset -
Yelp 的学术数据集
www.yelp.com/academic_dataset -
GitHub 上的数据源
github.com/caesar0301/awesome-public-datasets -
来自 Reddit 的数据集存档
www.reddit.com/r/datasets/
有一些专业数据集(例如,西班牙语文本分析数据集,以及基因和 IMF 数据)可能对您有所帮助:
-
来自哥伦比亚的数据集(西班牙语):
www.datos.gov.co/frm/buscador/frmBuscador.aspx -
来自癌症研究的数据集
www.broadinstitute.org/cgi-bin/cancer/datasets.cgi -
来自皮尤研究中心的研究数据
www.pewinternet.org/datasets/ -
来自美国伊利诺伊州的数据
data.illinois.gov -
来自 freebase.com 的数据
www.freebase.com -
联合国及其附属机构的数据集
data.un.org -
国际货币基金组织数据集
www.imf.org/external/data.htm -
英国政府数据
data.gov.uk -
来自爱沙尼亚的开放数据
pub.stat.ee/px-web.2001/Dialog/statfile1.asp -
R 语言中许多包含数据并可导出为 CSV 的 ML 库
www.r-project.org -
基因表达数据集
www.ncbi.nlm.nih.gov/geo/
使用 IntelliJ IDE 运行您的第一个 Apache Spark 2.0 程序
本程序的目的是让您熟悉使用刚设置的 Spark 2.0 开发环境编译和运行示例。我们将在后续章节中探讨组件和步骤。
我们将编写自己的 Spark 2.0.0 程序版本,并检查输出,以便理解其工作原理。需要强调的是,这个简短的示例仅是一个简单的 RDD 程序,使用了 Scala 的糖语法,以确保在开始处理更复杂的示例之前,您已正确设置了环境。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。
-
下载本书的示例代码,找到
myFirstSpark20.scala文件,并将代码放置在以下目录中。
我们在 Windows 机器上的C:\spark-2.0.0-bin-hadoop2.7\目录下安装了 Spark 2.0。
- 将
myFirstSpark20.scala文件放置在C:\spark-2.0.0-bin-hadoop2.7\examples\src\main\scala\spark\ml\cookbook\chapter1目录下:
Mac 用户请注意,我们在 Mac 机器上的/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/目录下安装了 Spark 2.0。
将myFirstSpark20.scala文件放置在/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/examples/src/main/scala/spark/ml/cookbook/chapter1目录下。
- 设置程序将驻留的包位置:
package spark.ml.cookbook.chapter1
- 为了使 Spark 会话能够访问集群并使用
log4j.Logger减少 Spark 产生的输出量,导入必要的包:
import org.apache.spark.sql.SparkSession
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将输出级别设置为
ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
- 通过使用构建器模式指定配置来初始化 Spark 会话,从而为 Spark 集群提供一个入口点:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myFirstSpark20")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
myFirstSpark20对象将在本地模式下运行。前面的代码块是创建SparkSession对象的典型方式。
- 然后我们创建两个数组变量:
val x = Array(1.0,5.0,8.0,10.0,15.0,21.0,27.0,30.0,38.0,45.0,50.0,64.0)
val y = Array(5.0,1.0,4.0,11.0,25.0,18.0,33.0,20.0,30.0,43.0,55.0,57.0)
- 然后让 Spark 基于之前创建的数组创建两个 RDD:
val xRDD = spark.sparkContext.parallelize(x)
val yRDD = spark.sparkContext.parallelize(y)
- 接下来,我们让 Spark 对
RDD进行操作;zip()函数将从之前提到的两个 RDD 创建一个新的RDD:
val zipedRDD = xRDD.zip(yRDD)
zipedRDD.collect().foreach(println)
在运行时控制台输出(关于如何在 IntelliJ IDE 中运行程序的更多详细信息将在后续步骤中介绍),您将看到这个:
- 现在,我们汇总
xRDD和yRDD的值,并计算新的zipedRDD总和值。我们还计算了zipedRDD的项目计数:
val xSum = zipedRDD.map(_._1).sum()
val ySum = zipedRDD.map(_._2).sum()
val xySum= zipedRDD.map(c => c._1 * c._2).sum()
val n= zipedRDD.count()
- 我们在控制台上打印出之前计算的值:
println("RDD X Sum: " +xSum)
println("RDD Y Sum: " +ySum)
println("RDD X*Y Sum: "+xySum)
println("Total count: "+n)
这里是控制台输出:
- 我们通过停止 Spark 会话来关闭程序:
spark.stop()
- 程序完成后,
myFirstSpark20.scala在 IntelliJ 项目资源管理器中的布局将如下所示:
- 确保没有编译错误。您可以通过重建项目来测试这一点:
一旦重建完成,控制台上应该会出现构建完成的消息:
Information: November 18, 2016, 11:46 AM - Compilation completed successfully with 1 warning in 55s 648ms
- 您可以通过在项目资源管理器中右键点击
myFirstSpark20对象并选择上下文菜单选项(如下一张截图所示)运行 myFirstSpark20来运行前面的程序。
您也可以从菜单栏的“运行”菜单执行相同的操作。
- 一旦程序成功执行,您将看到以下消息:
Process finished with exit code 0
这也显示在下面的截图中:
- IntelliJ 的 Mac 用户可以使用相同的上下文菜单执行此操作。
将代码放置在正确的路径上。
工作原理...
在本例中,我们编写了第一个 Scala 程序myFirstSpark20.scala,并在 IntelliJ 中展示了执行该程序的步骤。我们按照步骤中描述的路径,在 Windows 和 Mac 上都放置了代码。
在myFirstSpark20代码中,我们看到了创建SparkSession对象的典型方式,以及如何使用master()函数配置它以在本地模式下运行。我们从数组对象创建了两个 RDD,并使用简单的zip()函数创建了一个新的 RDD。
我们还对创建的 RDD 进行了简单的求和计算,并在控制台中显示了结果。最后,我们通过调用spark.stop()退出并释放资源。
还有更多...
Spark 可以从spark.apache.org/downloads.html下载。
Spark 2.0 关于 RDD 的文档可以在spark.apache.org/docs/latest/programming-guide.html#rdd-operations找到。
另请参见
- 关于 JetBrain IntelliJ 的更多信息,请访问
www.jetbrains.com/idea/。
如何向你的 Spark 程序添加图形
在本食谱中,我们讨论了如何使用 JFreeChart 向你的 Spark 2.0.0 程序添加图形图表。
如何操作...
-
设置 JFreeChart 库。JFreeChart 的 JAR 文件可以从
sourceforge.net/projects/jfreechart/files/网站下载。 -
本书中介绍的 JFreeChart 版本为 JFreeChart 1.0.19,如以下截图所示。它可以从
sourceforge.net/projects/jfreechart/files/1.%20JFreeChart/1.0.19/jfreechart-1.0.19.zip/download网站下载:
-
下载 ZIP 文件后,将其解压。我们在 Windows 机器上的
C:\下解压了 ZIP 文件,然后继续在解压的目标目录下找到lib目录。 -
接着,我们找到了所需的两个库(JFreeChart...
工作原理...
在本例中,我们编写了MyChart.scala,并看到了在 IntelliJ 中执行程序的步骤。我们按照步骤中描述的路径在 Windows 和 Mac 上放置了代码。
在代码中,我们看到了创建SparkSession对象的典型方法以及如何使用master()函数。我们创建了一个 RDD,其元素为 1 到 15 范围内的随机整数数组,并将其与索引进行了压缩。
然后,我们使用 JFreeChart 制作了一个包含简单x和y轴的基本图表,并提供了我们从前几步中的原始 RDD 生成的数据集。
我们为图表设置了架构,并在 JFreeChart 中调用show()函数,以显示一个带有x和y轴的线性图形图表的框架。
最后,我们通过调用spark.stop()退出并释放资源。
还有更多...
更多关于 JFreeChart 的信息,请访问:
另请参见
关于 JFreeChart 功能和能力的更多示例,请访问以下网站:
www.jfree.org/jfreechart/samples.html
第十一章:Spark 的机器学习三剑客 - 完美结合
本章我们将涵盖以下内容:
-
使用 Spark 2.0 的内部数据源创建 RDD
-
使用 Spark 2.0 的外部数据源创建 RDD
-
使用 Spark 2.0 的 filter() API 转换 RDD
-
使用非常有用的 flatMap() API 转换 RDD
-
使用集合操作 API 转换 RDD
-
使用 groupBy() 和 reduceByKey() 进行 RDD 转换/聚合
-
使用 zip() API 转换 RDD
-
使用配对键值 RDD 进行连接转换
-
使用配对键值 RDD 进行归约和分组转换
-
从 Scala 数据结构创建 DataFrame
-
以编程方式操作 DataFrame 而不使用 SQL
-
从外部源加载 DataFrame 并进行设置...
引言
Spark 高效处理大规模数据的三驾马车是 RDD、DataFrames 和 Dataset API。虽然每个都有其独立的价值,但新的范式转变倾向于将 Dataset 作为统一的数据 API,以满足单一接口中的所有数据处理需求。
Spark 2.0 的新 Dataset API 是一种类型安全的领域对象集合,可以通过转换(类似于 RDD 的过滤、map、flatMap() 等)并行使用函数或关系操作。为了向后兼容,Dataset 有一个名为 DataFrame 的视图,它是一个无类型的行集合。在本章中,我们展示了所有三种 API 集。前面的图总结了 Spark 数据处理关键组件的优缺点:
机器学习的高级开发者必须理解并能够无障碍地使用所有三种 API 集,无论是为了算法增强还是遗留原因。虽然我们建议每位开发者都应向高级 Dataset API 迁移,但你仍需了解 RDD,以便针对 Spark 核心系统编程。例如,投资银行和对冲基金经常阅读机器学习、数学规划、金融、统计学或人工智能领域的领先期刊,然后使用低级 API 编码研究以获得竞争优势。
RDDs - 一切的起点...
RDD API 是 Spark 开发者的重要工具,因为它在函数式编程范式中提供了对数据底层控制的偏好。RDD 的强大之处同时也使得新程序员更难以使用。虽然理解 RDD API 和手动优化技术(例如,在 groupBy() 操作之前使用 filter())可能很容易,但编写高级代码需要持续的练习和熟练度。
当数据文件、块或数据结构转换为 RDD 时,数据被分解为称为 分区(类似于 Hadoop 中的拆分)的较小单元,并分布在节点之间,以便它们可以同时并行操作。Spark 直接提供了这种功能...
数据帧——通过高级 API 统一 API 和 SQL 的自然演进
Spark 开发者社区始终致力于从伯克利的 AMPlab 时代开始为社区提供易于使用的高级 API。数据 API 的下一个演进是在 Michael Armbrust 向社区提供 SparkSQL 和 Catalyst 优化器时实现的,这使得使用简单且易于理解的 SQL 接口进行数据虚拟化成为可能。数据帧 API 是利用 SparkSQL 的自然演进,通过将数据组织成关系表那样的命名列来实现。
数据帧 API 通过 SQL 使数据整理对众多熟悉 R(data.frame)或 Python/Pandas(pandas.DataFrame)中的数据帧的数据科学家和开发者可用。
数据集——一个高级的统一数据 API
数据集是一个不可变的对象集合,这些对象被建模/映射到传统的关系模式。有四个属性使其成为未来首选的方法。我们特别发现数据集 API 具有吸引力,因为它与 RDD 相似,具有常规的转换操作符(例如,filter()、map()、flatMap()等)。数据集将遵循与 RDD 类似的惰性执行范式。尝试调和数据帧和数据集的最佳方式是将数据帧视为可以被认为是Dataset[Row]的别名。
- 强类型安全:我们现在在统一的数据 API 中既有编译时(语法错误)也有运行时安全,这有助于 ML 开发者...
使用 Spark 2.0 通过内部数据源创建 RDD
在 Spark 中创建 RDD 有四种方式,从用于客户端驱动程序中简单测试和调试的parallelize()方法,到用于近实时响应的流式 RDD。在本节中,我们将提供多个示例,展示如何使用内部数据源创建 RDD。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置:
package spark.ml.cookbook.chapter3
- 导入必要的包:
import breeze.numerics.pow
import org.apache.spark.sql.SparkSession
import Array._
- 导入用于设置
log4j日志级别的包。此步骤是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误以减少输出。参见上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...
工作原理...
客户端驱动程序中的数据通过分区 RDD 的数量(第二个参数)作为指导进行并行化和分布。生成的 RDD 是 Spark 的魔力,它开启了这一切(参阅 Matei Zaharia 的原始白皮书)。
生成的 RDD 现在是具有容错性和血统的完全分布式数据结构,可以使用 Spark 框架并行操作。
我们从www.gutenberg.org/读取文本文件查尔斯·狄更斯的《双城记》到 Spark RDDs 中。然后我们继续分割和标记化数据,并使用 Spark 的操作符(例如,map,flatMap()等)打印出总单词数。
使用外部数据源创建 Spark 2.0 的 RDDs
在本配方中,我们为您提供了几个示例,以展示使用外部源创建 RDD。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置:
package spark.ml.cookbook.chapter3
- 导入必要的包:
import breeze.numerics.pow
import org.apache.spark.sql.SparkSession
import Array._
- 导入用于设置
log4j日志级别的包。这一步是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession
.builder
.master("local[*]")
.appName("myRDD")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
-
我们从古腾堡项目获取数据。这是一个获取实际文本的绝佳来源,涵盖了从莎士比亚全集到查尔斯·狄更斯的作品。
-
从以下来源下载文本并将其存储在本地目录中:
-
选定书籍:查尔斯·狄更斯的《双城记》
-
再次,我们使用
SparkContext,通过SparkSession可用,并使用其textFile()函数读取外部数据源并在集群上并行化它。值得注意的是,所有工作都是由 Spark 在幕后为开发者完成的,只需一次调用即可加载多种格式(例如,文本、S3 和 HDFS),并使用protocol:filepath组合在集群上并行化数据。 -
为了演示,我们加载了这本书,它以 ASCII 文本形式存储,使用
SparkContext通过SparkSession的textFile()方法,后者在幕后工作,并在集群上创建分区 RDDs。
val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt")
输出将如下所示:
Number of lines = 16271
-
尽管我们尚未涉及 Spark 转换操作符,我们将查看一小段代码,该代码使用空格作为分隔符将文件分解成单词。在实际情况下,需要一个正则表达式来处理所有边缘情况以及所有空白变化(请参考本章中的使用 filter() API 的 Spark 中转换 RDDs配方)。
-
我们使用 lambda 函数接收每行读取的内容,并使用空格作为分隔符将其分解成单词。
-
我们使用 flatMap 来分解单词列表的数组(即,每行的一组单词对应于该行的不同数组/列表)。简而言之,我们想要的是每行的单词列表,而不是单词列表的列表。
-
val book2 = book1.flatMap(l => l.split(" "))
println(book1.count())
输出将如下所示:
Number of words = 143228
它是如何工作的...
我们从www.gutenberg.org/读取查尔斯·狄更斯的《双城记》文本文件到一个 RDD 中,然后通过使用空格作为分隔符在 lambda 表达式中使用.split()和.flatmap()方法对 RDD 本身进行单词分词。然后,我们使用 RDD 的.count()方法输出单词总数。虽然这很简单,但您必须记住,该操作是在 Spark 的分布式并行框架中进行的,仅用了几行代码。
还有更多...
使用外部数据源创建 RDD,无论是文本文件、Hadoop HDFS、序列文件、Casandra 还是 Parquet 文件,都异常简单。再次,我们使用SparkSession(Spark 2.0 之前的SparkContext)来获取集群的句柄。一旦执行了函数(例如,textFile 协议:文件路径),数据就会被分解成更小的部分(分区),并自动流向集群,这些数据作为可以在并行操作中使用的容错分布式集合变得可用。
-
在处理实际场景时,必须考虑多种变体。根据我们的经验,最好的建议是在编写自己的函数或连接器之前查阅文档。Spark 要么直接支持您的数据源,要么供应商有一个可下载的连接器来实现相同功能。
-
我们经常遇到的另一种情况是,许多小文件(通常在
HDFS目录中生成)需要并行化为 RDD 以供消费。SparkContext有一个名为wholeTextFiles()的方法,它允许您读取包含多个文件的目录,并将每个文件作为(文件名, 内容)键值对返回。我们发现这在使用 lambda 架构的多阶段机器学习场景中非常有用,其中模型参数作为批处理计算,然后每天在 Spark 中更新。
在此示例中,我们读取多个文件,然后打印第一个文件以供检查。
spark.sparkContext.wholeTextFiles()函数用于读取大量小文件,并将它们呈现为(K,V),即键值对:
val dirKVrdd = spark.sparkContext.wholeTextFiles("../data/sparkml2/chapter3/*.txt") // place a large number of small files for demo
println ("files in the directory as RDD ", dirKVrdd)
println("total number of files ", dirKVrdd.count())
println("Keys ", dirKVrdd.keys.count())
println("Values ", dirKVrdd.values.count())
dirKVrdd.collect()
println("Values ", dirKVrdd.first())
运行前面的代码后,您将得到以下输出:
files in the directory as RDD ,../data/sparkml2/chapter3/*.txt
WholeTextFileRDD[10] at wholeTextFiles at myRDD.scala:88)
total number of files 2
Keys ,2
Values ,2
Values ,(file:/C:/spark-2.0.0-bin-hadoop2.7/data/sparkml2/chapter3/a.txt,
The Project Gutenberg EBook of A Tale of Two Cities,
by Charles Dickens
参见
Spark 文档中关于textFile()和wholeTextFiles()函数的说明:
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.SparkContext
textFile()API 是与外部数据源接口的单一抽象。协议/路径的制定足以调用正确的解码器。我们将演示从 ASCII 文本文件、Amazon AWS S3 和 HDFS 读取,用户可以利用这些代码片段来构建自己的系统。
- 路径可以表示为简单路径(例如,本地文本文件)到完整的 URI,包含所需协议(例如,s3n 用于 AWS 存储桶),直至具有服务器和端口配置的完整资源路径(例如,从 Hadoop 集群读取 HDFS 文件)。...
使用 Spark 2.0 的 filter() API 转换 RDD
在本食谱中,我们探讨了 RDD 的filter()方法,该方法用于选择基础 RDD 的子集并返回新的过滤 RDD。格式类似于map(),但 lambda 函数决定哪些成员应包含在结果 RDD 中。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置:
package spark.ml.cookbook.chapter3
- 导入必要的包:
import breeze.numerics.pow
import org.apache.spark.sql.SparkSession
import Array._
- 导入用于设置
log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...
工作原理...
filter() API 通过几个示例进行了演示。在第一个示例中,我们遍历了一个 RDD,并通过使用 lambda 表达式.filter(i => (i%2) == 1)输出了奇数,该表达式利用了模(取模)函数。
在第二个示例中,我们通过使用 lambda 表达式num.map(pow(_,2)).filter(_ %2 == 1)将结果映射到平方函数,使其变得更有趣。
在第三个示例中,我们遍历文本并使用 lambda 表达式.filter(_.length < 30).filter(_.length > 0)过滤掉短行(例如,长度小于 30 个字符的行),以打印短行与总行数的对比(.count())作为输出。
还有更多...
filter() API 遍历并行分布式集合(即 RDD),并应用作为 lambda 提供给filter()的选择标准,以便将元素包含或排除在结果 RDD 中。结合使用map()(转换每个元素)和filter()(选择子集),在 Spark ML 编程中形成强大组合。
稍后我们将通过DataFrame API 看到,如何使用类似Filter() API 在 R 和 Python(pandas)中使用的高级框架实现相同效果。
另请参阅
-
.filter()方法的文档,作为 RDD 的方法调用,可访问spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD。 -
关于
BloomFilter()的文档——为了完整性,请注意已存在一个布隆过滤器函数,建议您避免自行编码。相关链接为spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.util.sketch.BloomFilter。
使用极其有用的 flatMap() API 转换 RDD
在本节中,我们探讨了常令初学者困惑的flatMap()方法;然而,通过深入分析,我们展示了它是一个清晰的概念,它像 map 一样将 lambda 函数应用于每个元素,然后将结果 RDD 扁平化为单一结构(不再是列表的列表,而是由所有子列表元素构成的单一列表)。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。
-
设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 导入必要的包
import breeze.numerics.pow
import org.apache.spark.sql.SparkSession
import Array._
- 导入设置
log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包需求。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 设置 Spark 上下文和应用程序参数,以便 Spark 能够运行。
val spark = SparkSession
.builder
.master("local[*]")
.appName("myRDD")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 我们使用
textFile()函数从之前下载的文本文件创建初始(即基础 RDD):www.gutenberg.org/cache/epub/98/pg98.txt。
val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt")
-
我们对 RDD 应用 map 函数以展示
map()函数的转换。首先,我们错误地尝试仅使用map()根据正则表达式*[\s\W]+]*分离所有单词,以说明结果 RDD 是列表的列表,其中每个列表对应一行及其内的分词单词。此例展示了初学者在使用flatMap()时可能遇到的困惑。 -
以下代码行修剪每行并将其分割成单词。结果 RDD(即 wordRDD2)将是单词列表的列表,而不是整个文件的单一单词列表。
val wordRDD2 = book1.map(_.trim.split("""[\s\W]+""") ).filter(_.length > 0)
wordRDD2.take(3)foreach(println(_))
运行上述代码后,您将得到以下输出。
[Ljava.lang.String;@1e60b459
[Ljava.lang.String;@717d7587
[Ljava.lang.String;@3e906375
- 我们使用
flatMap()方法不仅进行映射,还扁平化列表的列表,最终得到由单词本身构成的 RDD。我们修剪并分割单词(即分词),然后筛选出长度大于零的单词,并将其映射为大写。
val wordRDD3 = book1.flatMap(_.trim.split("""[\s\W]+""") ).filter(_.length > 0).map(_.toUpperCase())
println("Total number of lines = ", book1.count())
println("Number of words = ", wordRDD3.count())
在此情况下,使用flatMap()扁平化列表后,我们能如预期般取回单词列表。
wordRDD3.take(5)foreach(println(_))
输出如下:
Total number of lines = 16271
Number of words = 141603
THE
PROJECT
GUTENBERG
EBOOK
OF
它是如何工作的...
在这个简短的示例中,我们读取了一个文本文件,然后使用flatMap(_.trim.split("""[\s\W]+""") lambda 表达式对单词进行分割(即,令牌化),以获得一个包含令牌化内容的单一 RDD。此外,我们使用filter() API filter(_.length > 0)来排除空行,并在输出结果之前使用.map() API 中的 lambda 表达式.map(_.toUpperCase())映射为大写。
在某些情况下,我们不希望为基 RDD 的每个元素返回一个列表(例如,为对应于一行的单词获取一个列表)。有时我们更倾向于拥有一个单一的扁平列表,该列表对应于文档中的每个单词。简而言之,我们不想要一个列表的列表,而是想要一个包含...的单一列表。
还有更多...
glom()函数允许你将 RDD 中的每个分区建模为数组,而不是行列表。虽然在大多数情况下可以产生结果,但glom()允许你减少分区之间的数据移动。
尽管在表面上,文本中提到的第一种和第二种方法在计算 RDD 中的最小数时看起来相似,但glom()函数将通过首先对所有分区应用min(),然后发送结果数据,从而在网络上引起更少的数据移动。要看到差异的最佳方式是在 10M+ RDD 上使用此方法,并相应地观察 IO 和 CPU 使用情况。
- 第一种方法是在不使用
glom()的情况下找到最小值:
val minValue1= numRDD.reduce(_ min _)
println("minValue1 = ", minValue1)
运行上述代码后,你将得到以下输出:
minValue1 = 1.0
- 第二种方法是通过使用
glom()来找到最小值,这会导致对一个分区进行本地应用的最小函数,然后通过 shuffle 发送结果。
val minValue2 = numRDD.glom().map(_.min).reduce(_ min _)
println("minValue2 = ", minValue2)
运行上述代码后,你将得到以下输出:
minValue1 = 1.0
另请参见
-
flatMap()、PairFlatMap()及其他 RDD 下的变体的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD找到。 -
RDD 下
FlatMap()函数的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.function.FlatMapFunction找到。 -
PairFlatMap()函数的文档——针对成对数据元素的非常便捷的变体,可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.function.PairFlatMapFunction找到。 -
flatMap()方法将提供的函数(lambda 表达式或通过 def 定义的命名函数)应用于每个元素,展平结构,并生成一个新的 RDD。
使用集合操作 API 转换 RDD
在本食谱中,我们探索了 RDD 上的集合操作,如intersection()、union()、subtract()、distinct()和Cartesian()。让我们以分布式方式实现常规集合操作。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 导入必要的包
import breeze.numerics.pow
import org.apache.spark.sql.SparkSession
import Array._
- 导入用于设置
log4j日志级别的包。此步骤是可选的,但我们强烈建议您(根据开发周期适当更改级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...
它是如何工作的...
在本例中,我们以三组数字数组(奇数、偶数及其组合)开始,然后将它们作为参数传递给集合操作 API。我们介绍了如何使用intersection()、union()、subtract()、distinct()和cartesian() RDD 操作符。
另请参见
虽然 RDD 集合操作符易于使用,但必须注意 Spark 在后台为完成某些操作(例如,交集)而必须进行的数据洗牌。
值得注意的是,union 操作符不会从结果 RDD 集合中删除重复项。
RDD 转换/聚合与groupBy()和reduceByKey()
在本食谱中,我们探讨了groupBy()和reduceBy()方法,这些方法允许我们根据键对值进行分组。由于内部洗牌,这是一个昂贵的操作。我们首先更详细地演示groupby(),然后介绍reduceBy(),以展示编写这些代码时的相似性,同时强调reduceBy()操作符的优势。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置:
package spark.ml.cookbook.chapter3
- 导入必要的包:
import breeze.numerics.pow
import org.apache.spark.sql.SparkSession
import Array._
- 导入用于设置
log4j日志级别的包。此步骤是可选的,但我们强烈建议您(根据开发周期适当更改级别):
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...
它是如何工作的...
在本例中,我们创建了数字一到十二,并将它们放置在三个分区中。然后,我们继续使用简单的模运算将它们分解为奇数/偶数。groupBy()用于将它们聚合为两个奇数/偶数组。这是一个典型的聚合问题,对于 SQL 用户来说应该很熟悉。在本章后面,我们将使用DataFrame重新审视此操作,DataFrame也利用了 SparkSQL 引擎提供的更好的优化技术。在后面的部分,我们展示了groupBy()和reduceByKey()的相似性。我们设置了一个字母数组(即,a和b),然后将它们转换为 RDD。然后,我们根据键(即,唯一的字母 - 在本例中只有两个)进行聚合,并打印每个组的总数。
还有更多...
鉴于 Spark 的发展方向,它更倾向于 Dataset/DataFrame 范式而不是低级 RDD 编码,因此必须认真考虑在 RDD 上执行groupBy()的原因。虽然有些情况下确实需要此操作,但建议读者重新制定解决方案,以利用 SparkSQL 子系统和称为Catalyst的优化器。
Catalyst 优化器在构建优化查询计划时考虑了 Scala 的强大功能,如模式匹配和准引用。
-
有关 Scala 模式匹配的文档可在
docs.scala-lang.org/tutorials/tour/pattern-matching.html找到 -
有关 Scala 准引用的文档可在
docs.scala-lang.org/overviews/quasiquotes/intro.html找到
另请参见
RDD 下的groupBy()和reduceByKey()操作文档:
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD
使用 zip() API 转换 RDD
在本配方中,我们探讨了zip()函数。对于我们这些在 Python 或 Scala 中工作的人来说,zip()是一个熟悉的方法,它允许你在应用内联函数之前配对项目。使用 Spark,它可以用来促进成对 RDD 之间的算术运算。从概念上讲,它以这样的方式组合两个 RDD,即一个 RDD 的每个成员与第二个 RDD 中占据相同位置的成员配对(即,它对齐两个 RDD 并从成员中制作配对)。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 导入必要的包
import org.apache.spark.sql.SparkSession
- 导入用于设置
log4j日志级别的包。此步骤是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 设置 Spark 上下文和应用程序参数,以便 Spark 能够运行。
val spark = SparkSession
.builder
.master("local[*]")
.appName("myRDD")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 设置示例的数据结构和 RDD。在本例中,我们创建了两个从
Array[]生成的 RDD,并让 Spark 决定分区数量(即,parallize()方法中的第二个参数未设置)。
val SignalNoise: Array[Double] = Array(0.2,1.2,0.1,0.4,0.3,0.3,0.1,0.3,0.3,0.9,1.8,0.2,3.5,0.5,0.3,0.3,0.2,0.4,0.5,0.9,0.1)
val SignalStrength: Array[Double] = Array(6.2,1.2,1.2,6.4,5.5,5.3,4.7,2.4,3.2,9.4,1.8,1.2,3.5,5.5,7.7,9.3,1.1,3.1,2.1,4.1,5.1)
val parSN=spark.sparkContext.parallelize(SignalNoise) // parallelized signal noise RDD
val parSS=spark.sparkContext.parallelize(SignalStrength) // parallelized signal strength
-
我们对 RDD 应用
zip()函数以演示转换。在示例中,我们取分区 RDD 的范围,并使用模函数将其标记为奇数/偶数。我们使用zip()函数将来自两个 RDD(SignalNoiseRDD 和 SignalStrengthRDD)的元素配对,以便我们可以应用map()函数并计算它们的比率(噪声与信号比率)。我们可以使用此技术执行几乎所有类型的算术或非算术操作,涉及两个 RDD 的单个成员。 -
两个 RDD 成员的配对行为类似于元组或行。通过
zip()创建的配对中的单个成员可以通过其位置访问(例如,._1和._2)
val zipRDD= parSN.zip(parSS).map(r => r._1 / r._2).collect()
println("zipRDD=")
zipRDD.foreach(println)
运行前面的代码后,您将得到以下输出:
zipRDD=
0.03225806451612903
1.0
0.08333333333333334
0.0625
0.05454545454545454
工作原理...
在本例中,我们首先设置两个数组,分别代表信号噪声和信号强度。它们只是一系列测量数字,我们可以从物联网平台接收这些数字。然后,我们将两个独立的数组配对,使得每个成员看起来像是原始输入的一对(x, y)。接着,我们通过以下代码片段将配对分割并计算噪声与信号的比率:
val zipRDD= parSN.zip(parSS).map(r => r._1 / r._2)
zip()方法有许多涉及分区的变体。开发者应熟悉带有分区的zip()方法的变体(例如,zipPartitions)。
另请参阅
- RDD 下的
zip()和zipPartitions()操作的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD找到。
使用配对键值 RDD 的连接转换
在本配方中,我们介绍了KeyValueRDD对 RDD 及其支持的连接操作,如join()、leftOuterJoin、rightOuterJoin()和fullOuterJoin(),作为通过集合操作 API 提供的更传统且更昂贵的集合操作(如intersection()、union()、subtraction()、distinct()、cartesian()等)的替代方案。
我们将演示join()、leftOuterJoin、rightOuterJoin()和fullOuterJoin(),以解释键值对 RDD 的强大功能和灵活性。
println("Full Joined RDD = ")
val fullJoinedRDD = keyValueRDD.fullOuterJoin(keyValueCity2RDD)
fullJoinedRDD.collect().foreach(println(_))
如何操作...
- 设置示例的数据结构和 RDD:
val keyValuePairs = List(("north",1),("south",2),("east",3),("west",4))
val keyValueCity1 = List(("north","Madison"),("south","Miami"),("east","NYC"),("west","SanJose"))
val keyValueCity2 = List(("north","Madison"),("west","SanJose"))
- 将列表转换为 RDD:
val keyValueRDD = spark.sparkContext.parallelize(keyValuePairs)
val keyValueCity1RDD = spark.sparkContext.parallelize(keyValueCity1)
val keyValueCity2RDD = spark.sparkContext.parallelize(keyValueCity2)
- 我们可以访问配对 RDD 中的
键和值。
val keys=keyValueRDD.keys
val values=keyValueRDD.values
- 我们对配对 RDD 应用
mapValues()函数来演示这一转换。在此示例中,我们使用 map 函数将值提升,为每个元素增加 100。这是一种向数据引入噪声(即抖动)的流行技术。
val kvMappedRDD = keyValueRDD.mapValues(_+100)
kvMappedRDD.collect().foreach(println(_))
运行上述代码后,您将得到以下输出:
(north,101)
(south,102)
(east,103)
(west,104)
- 我们对 RDD 应用
join()函数来演示这一转换。我们使用join()来连接两个 RDD。我们基于键(即北、南等)连接两个 RDD。
println("Joined RDD = ")
val joinedRDD = keyValueRDD.join(keyValueCity1RDD)
joinedRDD.collect().foreach(println(_))
运行上述代码后,您将得到以下输出:
(south,(2,Miami))
(north,(1,Madison))
(west,(4,SanJose))
(east,(3,NYC))
- 我们对 RDD 应用
leftOuterJoin()函数来演示这一转换。leftOuterjoin的作用类似于关系左外连接。Spark 用None替换成员资格的缺失,而不是NULL,这在关系系统中很常见。
println("Left Joined RDD = ")
val leftJoinedRDD = keyValueRDD.leftOuterJoin(keyValueCity2RDD)
leftJoinedRDD.collect().foreach(println(_))
运行上述代码后,您将得到以下输出:
(south,(2,None))
(north,(1,Some(Madison)))
(west,(4,Some(SanJose)))
(east,(3,None))
- 我们将对 RDD 应用
rightOuterJoin()来演示这一转换。这与关系系统中的右外连接类似。
println("Right Joined RDD = ")
val rightJoinedRDD = keyValueRDD.rightOuterJoin(keyValueCity2RDD)
rightJoinedRDD.collect().foreach(println(_))
运行上述代码后,您将得到以下输出:
(north,(Some(1),Madison))
(west,(Some(4),SanJose))
- 然后,我们对 RDD 应用
fullOuterJoin()函数来演示这一转换。这与关系系统中的全外连接类似。
val fullJoinedRDD = keyValueRDD.fullOuterJoin(keyValueCity2RDD)
fullJoinedRDD.collect().foreach(println(_))
运行上述代码后,您将得到以下输出:
Full Joined RDD =
(south,(Some(2),None))
(north,(Some(1),Some(Madison)))
(west,(Some(4),Some(SanJose)))
(east,(Some(3),None))
工作原理...
在本食谱中,我们声明了三个列表,代表关系表中可用的典型数据,这些数据可通过连接器导入 Casandra 或 RedShift(为简化本食谱,此处未展示)。我们使用了三个列表中的两个来表示城市名称(即数据表),并将它们与第一个列表连接,该列表代表方向(例如,定义表)。第一步是定义三个配对值的列表。然后我们将它们并行化为键值 RDD,以便我们可以在第一个 RDD(即方向)和其他两个代表城市名称的 RDD 之间执行连接操作。我们对 RDD 应用了 join 函数来演示这一转换。
我们演示了join()、leftOuterJoin和rightOuterJoin()...
还有更多...
RDD 下join()及其变体的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD找到。
配对键值 RDD 的 reduce 和分组转换
在本食谱中,我们探讨了 reduce 和按 key 分组。reduceByKey()和groupbyKey()操作在大多数情况下比reduce()和groupBy()更高效且更受青睐。这些函数提供了便捷的设施,通过减少洗牌来聚合值并按 key 组合它们,这在大型数据集上是一个问题。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。
-
设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 导入必要的包
import org.apache.spark.sql.SparkSession
- 导入用于设置
log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误以减少输出。请参阅前一步骤了解包要求:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 设置 Spark 上下文和应用程序参数,以便 Spark 能够运行。
val spark = SparkSession
.builder
.master("local[*]")
.appName("myRDD")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 设置示例所需的数据结构和 RDD:
val signaltypeRDD = spark.sparkContext.parallelize(List(("Buy",1000),("Sell",500),("Buy",600),("Sell",800)))
- 我们应用
groupByKey()以演示转换。在此示例中,我们在分布式环境中将所有买卖信号分组在一起。
val signaltypeRDD = spark.sparkContext.parallelize(List(("Buy",1000),("Sell",500),("Buy",600),("Sell",800)))
val groupedRDD = signaltypeRDD.groupByKey()
groupedRDD.collect().foreach(println(_))
运行前面的代码,您将得到以下输出:
Group By Key RDD =
(Sell, CompactBuffer(500, 800))
(Buy, CompactBuffer(1000, 600))
- 我们对 RDD 对应用
reduceByKey()函数以演示转换。在此示例中,该函数用于计算买卖信号的总成交量。Scala 符号(_+_)简单表示每次添加两个成员并从中产生单个结果。就像reduce()一样,我们可以应用任何函数(即简单函数的内联和更复杂情况下的命名函数)。
println("Reduce By Key RDD = ")
val reducedRDD = signaltypeRDD.reduceByKey(_+_)
reducedRDD.collect().foreach(println(_))
运行前面的代码,您将得到以下输出:
Reduce By Key RDD =
(Sell,1300)
(Buy,1600)
它是如何工作的...
在此示例中,我们声明了一个商品买卖清单及其对应价格(即典型的商业交易)。然后,我们使用 Scala 简写符号(_+_)计算总和。最后一步,我们为每个键组(即Buy或Sell)提供了总计。键值 RDD 是一个强大的结构,可以在减少代码量的同时提供所需的聚合功能,将配对值分组到聚合桶中。groupByKey()和reduceByKey()函数模拟了相同的聚合功能,而reduceByKey()由于在组装最终结果时数据移动较少,因此更高效。
另请参阅
有关 RDD 下的groupByKey()和reduceByKey()操作的文档,请访问spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD。
从 Scala 数据结构创建 DataFrames
在本节中,我们探讨了DataFrame API,它为处理数据提供了比 RDD 更高的抽象层次。该 API 类似于 R 和 Python 数据帧工具(pandas)。
DataFrame简化了编码,并允许您使用标准 SQL 检索和操作数据。Spark 保留了关于 DataFrames 的额外信息,这有助于 API 轻松操作框架。每个DataFrame都将有一个模式(从数据推断或显式定义),允许我们像查看 SQL 表一样查看框架。SparkSQL 和 DataFrame 的秘诀在于催化优化器将在幕后工作,通过重新排列管道中的调用来优化访问。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。
-
设置程序所在包的位置:
package spark.ml.cookbook.chapter3
- 设置与 DataFrames 相关的导入以及所需的数据结构,并根据示例需要创建 RDD:
import org.apache.spark.sql._
- 为
log4j设置日志级别导入所需的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。有关包要求的详细信息,请参阅前一步骤。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession
.builder
.master("local[*]")
.appName("myDataFrame")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 我们设置了两个
List()对象和一个序列(即Seq())的 Scala 数据结构。然后,我们将List结构转换为 RDD,以便转换为DataFrames进行后续步骤:
val signaltypeRDD = spark.sparkContext.parallelize(List(("Buy",1000),("Sell",500),("Buy",600),("Sell",800)))
val numList = List(1,2,3,4,5,6,7,8,9)
val numRDD = spark.sparkContext.parallelize(numList)
val myseq = Seq( ("Sammy","North",113,46.0),("Sumi","South",110,41.0), ("Sunny","East",111,51.0),("Safron","West",113,2.0 ))
- 我们取一个列表,使用
parallelize()方法将其转换为 RDD,并使用 RDD 的toDF()方法将其转换为 DataFrame。show()方法允许我们查看类似于 SQL 表的 DataFrame。
val numDF = numRDD.toDF("mylist")
numDF.show
运行上述代码后,您将获得以下输出:
+------+
|mylist|
+------+
| 1|
| 2|
| 3|
| 4|
| 5|
| 6|
| 7|
| 8|
| 9|
+------+
- 在以下代码片段中,我们取一个通用的 Scala Seq(序列)数据结构,并使用
createDataFrame()显式创建一个 DataFrame,同时命名列。
val df1 = spark.createDataFrame(myseq).toDF("Name","Region","dept","Hours")
- 在接下来的两个步骤中,我们使用
show()方法查看内容,然后使用printSchema()方法显示基于类型的推断方案。在此示例中,DataFrame 正确识别了 Seq 中的整数和双精度数作为两个数字列的有效类型。
df1.show()
df1.printSchema()
运行上述代码后,您将获得以下输出:
+------+------+----+-----+
| Name|Region|dept|Hours|
+------+------+----+-----+
| Sammy| North| 113| 46.0|
| Sumi| South| 110| 41.0|
| Sunny| East| 111| 51.0|
|Safron| West| 113| 2.0|
+------+------+----+-----+
root
|-- Name: string (nullable = true)
|-- Region: string (nullable = true)
|-- dept: integer (nullable = false)
|-- Hours: double (nullable = false)
工作原理...
在本示例中,我们取两个列表和一个 Seq 数据结构,将它们转换为 DataFrame,并使用df1.show()和df1.printSchema()显示表的内容和模式。
DataFrames 可以从内部和外部源创建。与 SQL 表类似,DataFrames 具有与之关联的模式,这些模式可以被推断或使用 Scala case 类或map()函数显式转换,同时摄取数据。
还有更多...
为确保完整性,我们包含了在 Spark 2.0.0 之前使用的import语句以运行代码(即,Spark 1.5.2):
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext
import org.apache.spark.mllib.linalg
import org.apache.spark.util
import Array._
import org.apache.spark.sql._
import org.apache.spark.sql.types
import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.Row;
import org.apache.spark.sql.types.{ StructType, StructField, StringType};
另请参阅
DataFrame 文档可在此处找到:spark.apache.org/docs/latest/sql-programming-guide.html。
如果遇到隐式转换问题,请确保已包含隐式导入语句。
示例代码适用于 Spark 2.0:
import sqlContext.implicits
以编程方式操作 DataFrames,无需 SQL
在本教程中,我们探索如何仅通过代码和方法调用(不使用 SQL)来操作数据框。数据框拥有自己的方法,允许您使用编程方式执行类似 SQL 的操作。我们展示了一些命令,如select()、show()和explain(),以说明数据框本身能够不使用 SQL 进行数据整理和操作。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置。
package spark.ml.cookbook.chapter3
- 设置与数据框相关的导入以及所需的数据结构,并根据示例需要创建 RDD。
import org.apache.spark.sql._
- 导入设置
log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...
工作原理...
在本例中,我们从文本文件加载数据到 RDD,然后使用.toDF()API 将其转换为数据框结构。接着,我们使用内置方法如select()、filter()、show()和explain()来模拟 SQL 查询,以编程方式探索数据(无需 SQL)。explain()命令显示查询计划,这对于消除瓶颈非常有用。
数据框提供了多种数据整理方法。
对于熟悉数据框 API 和 R 语言包(如cran.r-project.org的 dplyr 或旧版本)的用户,我们提供了一个具有丰富方法集的编程 API,让您可以通过 API 进行所有数据整理。
对于更熟悉 SQL 的用户,您可以简单地使用 SQL 来检索和操作数据,就像使用 Squirrel 或 Toad 查询数据库一样。
还有更多...
为确保完整性,我们包含了在 Spark 2.0.0 之前运行代码(即 Spark 1.5.2)所需的import语句。
import org.apache.spark._ import org.apache.spark.rdd.RDD import org.apache.spark.sql.SQLContext import org.apache.spark.mllib.linalg._ import org.apache.spark.util._ import Array._ import org.apache.spark.sql._ import org.apache.spark.sql.types._ import org.apache.spark.sql.DataFrame import org.apache.spark.sql.Row; import org.apache.spark.sql.types.{ StructType, StructField, StringType};
另请参阅
数据框的文档可在spark.apache.org/docs/latest/sql-programming-guide.html获取。
如果遇到隐式转换问题,请再次检查以确保您已包含隐式import语句。
Spark 2.0 的示例import语句:
import sqlContext.implicits._
从外部源加载数据框并进行设置
在本教程中,我们探讨使用 SQL 进行数据操作。Spark 提供实用且兼容 SQL 的接口,在生产环境中表现出色,我们不仅需要机器学习,还需要使用 SQL 访问现有数据源,以确保与现有 SQL 系统的兼容性和熟悉度。使用 SQL 的数据框在实际环境中实现集成是一个优雅的过程。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置:
package spark.ml.cookbook.chapter3
- 设置与 DataFrame 相关的导入和所需的数据结构,并根据示例需要创建 RDD:
import org.apache.spark.sql._
- 导入设置
log4j日志级别的包。此步骤是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
- 将日志级别设置为警告和
Error以减少输出。请参阅前面的步骤了解包要求:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession
.builder
.master("local[*]")
.appName("myDataFrame")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 我们创建对应于
customer文件的 DataFrame。在此步骤中,我们首先创建一个 RDD,然后使用toDF()将 RDD 转换为 DataFrame 并命名列。
val customersRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/customers13.txt") //Customer file
val custRDD = customersRDD.map {
line => val cols = line.trim.split(",")
(cols(0).toInt, cols(1), cols(2), cols(3).toInt)
}
val custDF = custRDD.toDF("custid","name","city","age")
客户数据内容参考:
custDF.show()
运行前面的代码,您将得到以下输出:
- 我们创建对应于
product文件的 DataFrame。在此步骤中,我们首先创建一个 RDD,然后使用toDF()将 RDD 转换为 DataFrame 并命名列。
val productsRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/products13.txt") //Product file
val prodRDD = productsRDD.map {
line => val cols = line.trim.split(",")
(cols(0).toInt, cols(1), cols(2), cols(3).toDouble)
}
- 我们将
prodRDD转换为 DataFrame:
val prodDF = prodRDD.toDF("prodid","category","dept","priceAdvertised")
- 使用 SQL select,我们显示表格内容。
产品数据内容:
prodDF.show()
运行前面的代码,您将得到以下输出:
- 我们创建对应于
sales文件的 DataFrame。在此步骤中,我们首先创建一个 RDD,然后使用toDF()将 RDD 转换为 DataFrame 并命名列。
val salesRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/sales13.txt") *//Sales file* val saleRDD = salesRDD.map {
line => val cols = line.trim.split(",")
(cols(0).toInt, cols(1).toInt, cols(2).toDouble)
}
- 我们将
saleRDD转换为 DataFrame:
val saleDF = saleRDD.toDF("prodid", "custid", "priceSold")
- 我们使用 SQL select 来显示表格。
销售数据内容:
saleDF.show()
运行前面的代码,您将得到以下输出:
- 我们打印客户、产品和销售 DataFrame 的架构,以验证列定义和类型转换后的架构:
custDF.printSchema()
productDF.printSchema()
salesDF. printSchema()
运行前面的代码,您将得到以下输出:
root
|-- custid: integer (nullable = false)
|-- name: string (nullable = true)
|-- city: string (nullable = true)
|-- age: integer (nullable = false)
root
|-- prodid: integer (nullable = false)
|-- category: string (nullable = true)
|-- dept: string (nullable = true)
|-- priceAdvertised: double (nullable = false)
root
|-- prodid: integer (nullable = false)
|-- custid: integer (nullable = false)
|-- priceSold: double (nullable = false)
它是如何工作的...
在此示例中,我们首先将数据加载到 RDD 中,然后使用toDF()方法将其转换为 DataFrame。DataFrame 非常擅长推断类型,但有时需要手动干预。我们在创建 RDD 后使用map()函数(应用惰性初始化范式)来处理数据,无论是通过类型转换还是调用更复杂的用户定义函数(在map()方法中引用)来进行转换或数据整理。最后,我们继续使用show()和printSchema()检查三个 DataFrame 的架构。
还有更多...
为了确保完整性,我们包含了在 Spark 2.0.0 之前用于运行代码的import语句(即,Spark 1.5.2):
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext
import org.apache.spark.mllib.linalg._
import org.apache.spark.util._
import Array._
import org.apache.spark.sql._
import org.apache.spark.sql.types._
import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.Row;
import org.apache.spark.sql.types.{ StructType, StructField, StringType};
另请参阅
DataFrame 的文档可在spark.apache.org/docs/latest/sql-programming-guide.html找到。
如果遇到隐式转换问题,请再次检查以确保您已包含 implicits import语句。
Spark 1.5.2 的示例import语句:
import sqlContext.implicits._
使用标准 SQL 语言与 DataFrames - SparkSQL
在本食谱中,我们展示了如何使用 DataFrame 的 SQL 功能执行基本的 CRUD 操作,但没有任何限制您使用 Spark 提供的 SQL 接口达到所需的任何复杂程度(即 DML)。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 设置与 DataFrames 相关的导入以及所需的数据结构,并根据示例需要创建 RDDs
import org.apache.spark.sql._
- 导入用于设置
log4j日志级别的包。此步骤是可选的,但我们强烈建议您根据开发周期的不同阶段适当调整级别。
import org.apache.log4j.Logger import org.apache.log4j.Level
- 将日志级别设置为警告和
ERROR以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger( ...
工作原理...
使用 SQL 的基本 DataFrame 工作流程是首先通过内部 Scala 数据结构或外部数据源填充 DataFrame,然后使用createOrReplaceTempView()调用将 DataFrame 注册为类似 SQL 的工件。
使用 DataFrames 时,您可以利用 Spark 存储的额外元数据(无论是 API 还是 SQL 方法),这可以在编码和执行期间为您带来好处。
虽然 RDD 仍然是核心 Spark 的主力,但趋势是向 DataFrame 方法发展,该方法已成功展示了其在 Python/Pandas 或 R 等语言中的能力。
还有更多...
将 DataFrame 注册为表的方式已发生变化。请参考此内容:
-
对于 Spark 2.0.0 之前的版本:
registerTempTable() -
对于 Spark 2.0.0 及更早版本:
createOrReplaceTempView()
在 Spark 2.0.0 之前,将 DataFrame 注册为类似 SQL 表的工件:
在我们能够使用 DataFrame 通过 SQL 进行查询之前,我们必须将 DataFrame 注册为临时表,以便 SQL 语句可以引用它而无需任何 Scala/Spark 语法。这一步骤可能会让许多初学者感到困惑,因为我们并没有创建任何表(临时或永久),但调用registerTempTable()在 SQL 领域创建了一个名称,SQL 语句可以引用它而无需额外的 UDF 或无需任何特定领域的查询语言。
- 注册...
另请参阅
数据框(DataFrame)的文档可在此处获取。
如果遇到隐式转换问题,请再次检查以确保您已包含 implicits import语句。
Spark 1.5.2 的示例import语句
import sqlContext.implicits._
DataFrame 是一个广泛的子系统,值得用一整本书来介绍。它使 SQL 程序员能够大规模地进行复杂的数据操作。
使用 Scala 序列与数据集 API 协同工作
在本示例中,我们探讨了新的数据集以及它如何与 Scala 数据结构seq协同工作。我们经常看到 LabelPoint 数据结构与 ML 库一起使用,以及与数据集配合良好的 Scala 序列(即 seq 数据结构)之间的关系。
数据集正被定位为未来统一的 API。值得注意的是,DataFrame 仍然可用,作为Dataset[Row]的别名。我们已经通过 DataFrame 的示例广泛地介绍了 SQL 示例,因此我们将重点放在数据集的其他变体上。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 导入必要的包以获取 Spark 会话访问集群,并导入
Log4j.Logger以减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
- 定义一个 Scala
case class来建模处理数据,Car类将代表电动和混合动力汽车。
case class Car(make: String, model: String, price: Double,
style: String, kind: String)
- 让我们创建一个 Scala 序列,并用电动和混合动力汽车填充它。
val *carData* =
*Seq*(
*Car*("Tesla", "Model S", 71000.0, "sedan","electric"),
*Car*("Audi", "A3 E-Tron", 37900.0, "luxury","hybrid"),
*Car*("BMW", "330e", 43700.0, "sedan","hybrid"),
*Car*("BMW", "i3", 43300.0, "sedan","electric"),
*Car*("BMW", "i8", 137000.0, "coupe","hybrid"),
*Car*("BMW", "X5 xdrive40e", 64000.0, "suv","hybrid"),
*Car*("Chevy", "Spark EV", 26000.0, "coupe","electric"),
*Car*("Chevy", "Volt", 34000.0, "sedan","electric"),
*Car*("Fiat", "500e", 32600.0, "coupe","electric"),
*Car*("Ford", "C-Max Energi", 32600.0, "wagon/van","hybrid"),
*Car*("Ford", "Focus Electric", 29200.0, "sedan","electric"),
*Car*("Ford", "Fusion Energi", 33900.0, "sedan","electric"),
*Car*("Hyundai", "Sonata", 35400.0, "sedan","hybrid"),
*Car*("Kia", "Soul EV", 34500.0, "sedan","electric"),
*Car*("Mercedes", "B-Class", 42400.0, "sedan","electric"),
*Car*("Mercedes", "C350", 46400.0, "sedan","hybrid"),
*Car*("Mercedes", "GLE500e", 67000.0, "suv","hybrid"),
*Car*("Mitsubishi", "i-MiEV", 23800.0, "sedan","electric"),
*Car*("Nissan", "LEAF", 29000.0, "sedan","electric"),
*Car*("Porsche", "Cayenne", 78000.0, "suv","hybrid"),
*Car*("Porsche", "Panamera S", 93000.0, "sedan","hybrid"),
*Car*("Tesla", "Model X", 80000.0, "suv","electric"),
*Car*("Tesla", "Model 3", 35000.0, "sedan","electric"),
*Car*("Volvo", "XC90 T8", 69000.0, "suv","hybrid"),
*Car*("Cadillac", "ELR", 76000.0, "coupe","hybrid")
)
- 将输出级别配置为
ERROR以减少 Spark 的日志输出。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 创建一个 SparkSession,以访问 Spark 集群,包括底层会话对象属性和功能。
val spark = SparkSession
.builder
.master("local[*]")
.appName("mydatasetseq")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 导入 Spark 隐式,从而仅通过导入添加行为。
import spark.implicits._
- 接下来,我们将利用 Spark 会话的
createDataset()方法从汽车数据序列创建一个数据集。
val cars = spark.createDataset(MyDatasetData.carData)
// carData is put in a separate scala object MyDatasetData
- 让我们打印出结果,以确认我们的方法调用通过调用 show 方法将序列转换为 Spark 数据集。
infecars.show(false)
+----------+--------------+--------+---------+--------+
|make |model |price |style |kind |
- 打印出数据集的隐含列名。我们现在可以使用类属性名称作为列名。
cars.columns.foreach(println)
make
model
price
style
kind
- 让我们展示自动生成的模式,并验证推断的数据类型。
println(cars.schema)
StructType(StructField(make,StringType,true), StructField(model,StringType,true), StructField(price,DoubleType,false), StructField(style,StringType,true), StructField(kind,StringType,true))
- 最后,我们将根据价格对数据集进行过滤,参考
Car类属性价格作为列,并展示结果。
cars.filter(cars("price") > 50000.00).show()
- 我们通过停止 Spark 会话来关闭程序。
spark.stop()
工作原理...
在本示例中,我们介绍了 Spark 的数据集功能,该功能首次出现在 Spark 1.6 中,并在后续版本中得到进一步完善。首先,我们借助 Spark 会话的createDataset()方法从 Scala 序列创建了一个数据集实例。接下来,我们打印出有关生成数据集的元信息,以确认创建过程如预期进行。最后,我们使用 Spark SQL 片段根据价格列过滤数据集,筛选出价格大于$50,000.00 的记录,并展示最终执行结果。
还有更多...
数据集有一个名为DataFrame的视图,它是行的未类型化数据集。数据集仍然保留了 RDD 的所有转换能力,如filter()、map()、flatMap()等。这就是为什么如果我们使用 RDD 编程 Spark,我们会发现数据集易于使用的原因之一。
另请参阅
从 RDD 创建和使用数据集,以及反向操作
在本食谱中,我们探讨了如何使用 RDD 与 Dataset 交互,以构建多阶段机器学习管道。尽管 Dataset(概念上被认为是具有强类型安全的 RDD)是未来的方向,但您仍然需要能够与其他机器学习算法或返回/操作 RDD 的代码进行交互,无论是出于遗留还是编码原因。在本食谱中,我们还探讨了如何创建和从 Dataset 转换为 RDD 以及反向操作。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
设置程序将驻留的包位置:
package spark.ml.cookbook.chapter3
- 为 Spark 会话导入必要的包以访问集群,并使用
Log4j.Logger来减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSession
- 定义一个 Scala 样例类来模拟处理数据。
case class Car(make: String, model: String, price: Double,style: String, kind: String)
- 让我们创建一个 Scala 序列,并用电动和混合动力汽车填充它。
val carData =Seq(Car("Tesla", "Model S", 71000.0, "sedan","electric"), ...
工作原理...
在本节中,我们将 RDD 转换为 Dataset,最终又转换回 RDD。我们从一个 Scala 序列开始,将其转换为 RDD。创建 RDD 后,调用 Spark 会话的createDataset()方法,将 RDD 作为参数传递,并接收作为结果的 Dataset。
接下来,数据集按制造商列分组,统计各种汽车制造商的存在情况。下一步涉及对特斯拉制造商的数据集进行过滤,并将结果转换回 RDD。最后,我们通过 RDD 的foreach()方法显示了最终的 RDD。
还有更多...
Spark 中的数据集源文件仅包含约 2500+行 Scala 代码。这是一段非常优秀的代码,可以在 Apache 许可证下进行专业化利用。我们列出了以下 URL,并鼓励您至少浏览该文件,了解在使用数据集时缓冲是如何发挥作用的。
数据集的源代码托管在 GitHub 上,地址为github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/Dataset.scala。
参见
-
数据集的文档可以在
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset找到 -
键值分组的数据集可以在
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.KeyValueGroupedDataset找到 -
关系分组的数据集可以在
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.RelationalGroupedDataset找到
结合使用数据集 API 和 SQL 处理 JSON
在本节中,我们探讨如何使用 JSON 与数据集。在过去的 5 年中,JSON 格式迅速成为数据互操作性的实际标准。
我们探讨数据集如何使用 JSON 并执行 API 命令,如select()。然后,我们通过创建一个视图(即createOrReplaceTempView())并执行 SQL 查询来演示如何使用 API 和 SQL 轻松查询 JSON 文件。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
我们将使用一个名为
cars.json的 JSON 数据文件,该文件是为这个示例创建的:
{"make": "Telsa", "model": "Model S", "price": 71000.00, "style": "sedan", "kind": "electric"}
{"make": "Audi", "model": "A3 E-Tron", "price": 37900.00, "style": "luxury", "kind": "hybrid"}
{"make": "BMW", "model": "330e", "price": 43700.00, "style": "sedan", "kind": "hybrid"}
- 设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
- 为 Spark 会话导入必要的包以访问集群,并使用
Log4j.Logger来减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
- 定义一个 Scala
case class来建模处理数据。
case class Car(make: String, model: String, price: Double,
style: String, kind: String)
- 将输出级别设置为
ERROR,以减少 Spark 的日志输出。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
- 初始化一个 Spark 会话,创建访问 Spark 集群的入口点。
val spark = SparkSession
.builder
.master("local[*]")
.appName("mydatasmydatasetjsonetrdd")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
- 导入 Spark 隐式,从而仅通过导入添加行为。
import spark.implicits._
- 现在,我们将 JSON 数据文件加载到内存中,并指定类类型为
Car。
val cars = spark.read.json("../data/sparkml2/chapter3/cars.json").as[Car]
- 让我们打印出我们生成的
Car类型数据集中的数据。
cars.show(false)
- 接下来,我们将显示数据集的列名,以验证汽车的 JSON 属性名称是否已正确处理。
cars.columns.foreach(println)
make
model
price
style
kind
- 让我们查看自动生成的模式并验证推断的数据类型。
println(cars.schema)
StructType(StructField(make,StringType,true), StructField(model,StringType,true), StructField(price,DoubleType,false), StructField(style,StringType,true), StructField(kind,StringType,true))
- 在这一步中,我们将选择数据集的
make列,通过应用distinct方法去除重复项,并展示结果。
cars.select("make").distinct().show()
- 接下来,在 cars 数据集上创建一个视图,以便我们可以对数据集执行一个字面上的 Spark SQL 查询字符串。
cars.createOrReplaceTempView("cars")
- 最后,我们执行一个 Spark SQL 查询,筛选数据集中的电动汽车,并仅返回定义的三个列。
spark.sql("select make, model, kind from cars where kind = 'electric'").show()
- 我们通过停止 Spark 会话来结束程序。
spark.stop()
工作原理...
使用 Spark 读取JavaScript 对象表示法(JSON)数据文件并将其转换为数据集非常简单。JSON 在过去几年中已成为广泛使用的数据格式,Spark 对这种格式的支持非常充分。
在第一部分中,我们展示了通过 Spark 会话内置的 JSON 解析功能将 JSON 加载到数据集的方法。您应该注意 Spark 的内置功能,它将 JSON 数据转换为 car 案例类。
在第二部分中,我们展示了如何将 Spark SQL 应用于数据集,以将所述数据整理成理想状态。我们利用数据集的 select 方法检索make列,并应用distinct方法去除...
还有更多...
要全面理解和掌握数据集 API,务必理解Row和Encoder的概念。
数据集遵循惰性执行范式,意味着执行仅在 Spark 中调用操作时发生。当我们执行一个操作时,Catalyst 查询优化器生成一个逻辑计划,并为并行分布式环境中的优化执行生成物理计划。请参阅引言中的图表了解所有详细步骤。
Row的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset找到。
Encoder的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Encoder找到。
参见
-
数据集的文档可在
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset找到。 -
KeyValue 分组数据集的文档可在
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.KeyValueGroupedDataset找到。 -
关系分组数据集的文档可在
spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.RelationalGroupedDataset找到。
再次确保下载并探索来自 GitHub 的 Dataset 源文件,该文件约有 2500+行。探索 Spark 源代码是学习 Scala、Scala 注解以及 Spark 2.0 本身高级编程的最佳方式。
对于 Spark 2.0 之前的用户值得注意:
- SparkSession 是单一入口...
使用 Dataset API 进行领域对象的函数式编程
在本教程中,我们探讨了如何使用 Dataset 进行函数式编程。我们利用 Dataset 和函数式编程将汽车(领域对象)按其车型进行分类。
如何操作...
-
在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。
-
使用包指令提供正确的路径
package spark.ml.cookbook.chapter3
- 导入必要的包以获取 Spark 上下文对集群的访问权限,并使用
Log4j.Logger减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.{Dataset, SparkSession}import spark.ml.cookbook.{Car, mydatasetdata}import scala.collection.mutableimport scala.collection.mutable.ListBufferimport org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSession
- 定义一个 Scala 案例类来包含我们处理的数据,我们的汽车类将代表电动和...
工作原理...
在此示例中,我们使用 Scala 序列数据结构来存储原始数据,即一系列汽车及其属性。通过调用createDataset(),我们创建了一个 DataSet 并填充了它。接着,我们使用'make'属性配合groupBy和mapGroups(),以函数式范式列出按车型分类的汽车。在 DataSet 出现之前,使用领域对象进行这种形式的函数式编程并非不可能(例如,使用 RDD 的案例类或 DataFrame 的 UDF),但 DataSet 结构使得这一过程变得简单且自然。
还有更多...
确保在所有 DataSet 编码中包含implicits声明:
import spark.implicits._
参见
Dataset 的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset访问。