DataBrick-Spark-认证助理开发者指南-全-

DataBrick Spark 认证助理开发者指南(全)

原文:zh.annas-archive.org/md5/5e217f5bf66327f4a93816a8c91503e2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎您加入 Apache Spark Python 认证的综合指南,这是为有志于通过 Databricks 获得认证的开发者准备的。

在本书中,《使用 Python 的 Apache Spark 认证 Databricks 认证助理开发者》,我将多年的专业知识和实践经验提炼成一本全面指南,以帮助您导航数据科学、人工智能和云计算技术的复杂性,并帮助您为 Spark 认证做准备。通过有洞察力的轶事、可操作的见解和经过验证的策略,我将为您提供在不断发展的大数据和人工智能技术领域中茁壮成长的工具和知识。

Apache Spark 已成为处理大规模数据的首选框架,使组织能够提取有价值的见解并推动明智的决策制定。凭借其强大的功能和多功能性,Spark 已成为全球数据工程师、分析师和科学家工具箱中的基石。本书旨在成为您掌握 Apache Spark 的全面伴侣,通过结构化的方法来理解核心概念、高级技术和利用 Spark 全部潜力的最佳实践。

本书精心制作,旨在指导您走上成为认证 Apache Spark 开发者的旅程。专注于认证准备,我提供了一种结构化的方法来掌握使用 Python 的 Apache Spark,确保您为通过认证考试和验证您的专业知识做好了充分准备。

本书面向的对象

本书专为有志于成为使用 Python 进行 Apache Spark 认证的开发者而定制。无论您是一位经验丰富的数据专业人士,希望验证您的专业知识,还是一位渴望深入了解大数据分析的新手,本指南都能满足所有技能水平的需求。从寻求在 Spark 中建立坚实基础的新手到希望提高技能并为认证做准备的经验丰富的从业者,本书为任何热衷于利用 Apache Spark 力量的个人提供了宝贵的资源。

无论您是希望提升职业前景、验证您的技能,还是在数据工程领域寻求新的机会,本指南都针对您的认证目标量身定制。专注于考试准备,我们提供针对性的资源和实用见解,以确保您在认证旅程中的成功。

本书提供了指导性的建议和相关方法,帮助您在拥有 Spark 工作知识的基础上在大数据领域留下足迹,并帮助您通过 Spark 认证考试。本书期望您具备 Python 的工作知识,但不需要任何先前的 Spark 知识,尽管拥有 PySpark 的工作知识将非常有帮助。

本书涵盖的内容

在接下来的章节中,我们将涵盖以下主题。

第一章认证指南和考试概述,介绍了 PySpark 认证考试的基础知识以及如何准备考试。

第二章理解 Apache Spark 及其应用,深入探讨了 Apache Spark 的基本原理,探讨了其核心功能、生态系统和实际应用。它介绍了 Spark 在处理各种数据处理任务中的多功能性,如批量处理、实时分析、机器学习和图处理。实际示例说明了 Spark 在各个行业中的应用以及其在现代数据架构中的演变角色。

第三章Spark 架构和转换,深入剖析了 Apache Spark 的架构,阐述了 RDD(弹性分布式数据集)抽象、Spark 的执行模型以及转换和操作的重要性。它探讨了窄转换和宽转换的概念,它们对性能的影响,以及 Spark 的执行计划如何优化分布式计算。实际示例有助于更好地理解这些概念。

第四章Spark DataFrame 及其操作,专注于 Spark 的 DataFrame API,并探讨了它在结构化数据处理和分析中的作用。它涵盖了 DataFrame 的创建、操作以及各种操作,如过滤、聚合、连接和分组。示例说明了 DataFrame API 在处理结构化数据中的易用性和优势。

第五章Spark 的高级操作和优化,在扩展你的基础知识的基础上,深入探讨了 Spark 的高级操作,包括广播变量、累加器、自定义分区以及与外部库协同工作。它探讨了处理复杂数据类型、优化内存使用以及利用 Spark 的可扩展性进行高级数据处理任务的技术。

本章还探讨了 Spark 的性能优化策略,强调了自适应查询执行的重要性。它探讨了优化 Spark 作业动态技术,包括运行时查询规划、自适应连接和数据倾斜处理。提供了实用技巧和最佳实践,以微调 Spark 作业以增强性能。

第六章Spark 中的 SQL 查询,专注于 Spark 的 SQL 模块,并探讨了 Spark 中类似 SQL 的查询能力。它涵盖了 DataFrame API 与 SQL 的互操作性,使用户能够在分布式数据集上运行 SQL 查询。示例展示了如何使用 SQL 查询在 Spark 中表达复杂的数据操作和分析。

第七章Spark 中的结构化流,专注于实时数据处理,并介绍了 Spark 处理连续数据流的 API——结构化流。它涵盖了事件时间处理、水印、触发器和输出模式等概念。实际示例演示了如何使用结构化流构建和部署流应用程序。

本章不包括在 Spark 认证考试中,但了解流式处理概念是有益的,因为它们是现代数据工程世界中的核心概念。

第八章使用 Spark ML 进行机器学习,探讨了 Spark 的机器学习库 Spark ML,深入监督和无监督机器学习技术。它涵盖了各种算法的模型构建、评估和超参数调整。实际示例说明了 Spark ML 在现实世界机器学习任务中的应用。

本章不包括在 Spark 认证考试中,但了解 Spark 中的机器学习概念是有益的,因为它们是现代数据科学世界中的核心概念。

第九章模拟测试 1,为你提供了第一个模拟测试,以备实际认证考试之用。

第十章模拟测试 2,为你提供了第二个模拟测试,以备实际认证考试之用。

要充分利用本书

在深入研究章节之前,对 Python 编程的基本理解以及熟悉基本数据处理概念是必不可少的。此外,掌握分布式计算原理以及数据操作和分析经验将大有裨益。全书假设您具备 Python 的工作知识以及数据工程和数据分析的基础概念。具备这些先决条件后,您将准备好开始您的 Apache Spark 认证开发者之旅。

本书涵盖的软件/硬件 操作系统要求
Python Windows、macOS 或 Linux
Spark

代码在您注册 Databricks 社区版并导入您的账户中的 python 文件时将运行最佳。

如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将有助于避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Databricks-Certified-Associate-Developer-for-Apache-Spark-Using-Python。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包可供使用,请访问 github.com/PacktPublishing/。查看它们!

使用的约定

本书中使用了多种文本约定。

文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“createOrReplaceTempView() 方法允许我们将处理后的数据作为视图保存在 Spark SQL 中。”

代码块设置如下:

# Perform an aggregation to calculate the average salary
average_salary = spark.sql("SELECT AVG(Salary) AS average_salary FROM employees")

粗体: 表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“考试由60 个问题组成。您尝试这些问题的时间是120 分钟。”

小贴士或重要提示

看起来像这样。

联系我们

读者反馈始终欢迎。

一般反馈: 如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。

盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

分享您的想法

现在您已经完成了 Databricks 认证的 Apache Spark 使用 Python 的副开发人员,我们很乐意听听您的想法!如果您从亚马逊购买了这本书,请点击此处直接进入本书的亚马逊评论页面并分享您的反馈或留下评论

您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?

您的电子书购买是否与您选择的设备不兼容?

请放心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何地点、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不仅限于此,您还可以获得独家折扣、新闻通讯和每天收件箱中的优质免费内容。

按照以下简单步骤获取福利:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781804619780

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱

第一部分:考试概述

这一部分将展示 PySpark 认证考试的基础知识和需要牢记的规则。它将展示考试中提出的不同类型的问题以及如何准备这些问题。

这一部分包含以下章节:

  • 第一章认证指南和考试概述

第一章:认证指南和考试的概述

准备任何任务最初涉及彻底理解手头的问题,然后制定一个应对挑战的策略。在这个规划阶段,为应对挑战的每个方面创建一个逐步的方法是有效的方法。这种方法使得可以单独处理较小的任务,有助于系统地通过挑战,而无需感到不知所措。

本章旨在展示通过 Spark 认证考试逐步方法。在本章中,我们将涵盖以下主题:

  • 认证考试的概述

  • 考试中可能遇到的不同类型的问题

  • 本书其余章节的概述

我们首先概述一下认证考试。

认证考试的概述

考试由 60 个问题 组成。你被给予的时间来尝试这些问题是 120 分钟。这给你大约 每题 2 分钟

要通过考试,你需要得到 70% 的分数,这意味着你需要在 60 个问题中正确回答 42 个才能通过。

如果你准备充分,这段时间应该足够你回答问题,并在时间结束前进行复习。

接下来,我们将看到这些问题如何在整个考试中分布。

问题的分布

考试问题被分为以下几个广泛的类别。以下表格根据不同类别提供了问题的细分:

主题 考试百分比 问题数量
Spark 架构:概念理解 17% 10
Spark 架构:应用理解 11% 7
Spark DataFrame API 应用 72% 43

表 1.1:考试细分

通过观察这个分布,你可能会想要在考试准备中更加关注 Spark DataFrame API,因为这个部分涵盖了大约 72% 的考试(大约 43 个问题)。如果你能正确回答这些问题,通过考试就会变得更容易。

但这并不意味着你不应该关注 Spark 架构领域。Spark 架构问题难度各异,有时可能会令人困惑。同时,它们允许你轻松得分,因为架构问题通常很简单。

让我们看看一些其他可用的资源,这些资源可以帮助你为这次考试做准备。

准备考试的资源

当你开始计划参加认证考试时,你必须做的第一件事是掌握 Spark 概念。这本书将帮助你理解这些概念。一旦你完成了这些,进行模拟考试将很有用。这本书中提供了两个模拟考试供你利用。

此外,Databricks 提供了模拟考试,这对考试准备非常有用。您可以在以下位置找到它:files.training.databricks.com/assessments/practice-exams/PracticeExam-DCADAS3-Python.pdf.

考试期间可用的资源

在考试期间,您将能够访问 Spark 文档。这是通过 Webassessor 实现的,其界面与您在互联网上找到的常规 Spark 文档略有不同。熟悉这个界面会很好。您可以在 www.webassessor.com/zz/DATABRICKS/Python_v2.html 找到该界面。我建议您浏览它,并尝试通过此文档找到 Spark 的不同包和函数,以便在考试期间更舒适地导航。

接下来,我们将查看如何注册考试。

注册您的考试

Databricks 是准备这些考试和认证的公司。您可以在此处注册考试:www.databricks.com/learn/certification/apache-spark-developer-associate.

接下来,我们将查看考试的一些先决条件。

考试先决条件

在您参加考试之前,需要一些先决条件,以便您能够成功通过认证。以下是一些主要的先决条件:

  • 掌握 Spark 架构的基本原理,包括自适应查询执行的原则。

  • 熟练使用 Spark DataFrame API 进行各种数据操作任务,如下所示:

    • 执行列操作,例如选择、重命名和操作

    • 执行行操作,包括过滤、删除、排序和聚合数据

    • 执行与 DataFrame 相关的任务,例如连接、读取、写入和实现分区策略

    • 展示使用 用户定义函数UDFs)和 Spark SQL 函数的熟练程度

  • 虽然没有明确测试,但预期您对 Python 或 Scala 有功能性的理解。考试可用两种编程语言进行。

希望到这本书的结尾,您能够完全掌握所有这些概念,并且已经足够练习,以便对考试充满信心。

现在,让我们讨论一下在线监考考试期间可以期待什么。

在线监考考试

Spark 认证考试是一个在线监考考试。这意味着您将在家中参加考试,但有人将在网上监考。我鼓励您提前了解监考考试的程序和规则。这将为您在考试时节省很多麻烦和焦虑。

为了给你一个概述,在整个考试过程中,以下程序将生效:

  • Webassessor 监考员将进行网络摄像头监控,以确保考试诚信。

  • 你需要出示带有照片的有效身份证明。

  • 你需要独自进行考试。

  • 你的桌子需要整理干净,并且除了你用于考试的笔记本电脑外,房间里不应有其他电子设备。

  • 房间的墙上不应有任何可能帮助你考试的海报或图表。

  • 监考员在考试期间也会监听你,所以你想要确保你坐在一个安静舒适的环境中。

  • 建议不要使用你的工作笔记本电脑参加这次考试,因为它需要安装软件,并且需要禁用你的防病毒软件和防火墙。

监考员的职责如下:

  • 监督你的考试过程以保持考试诚信。

  • 解决与考试交付过程相关的任何疑问。

  • 如有必要,提供技术支持。

  • 需要注意的是,监考员不会就考试内容提供任何形式的帮助。

我建议你在考试前有足够的时间设置你将进行考试的环境。这将确保一个顺畅的在线考试流程,让你可以专注于问题,不必担心其他任何事情。

现在,让我们谈谈考试中可能出现的不同类型的题目。

题目类型。

考试中你会遇到不同类别的题目。它们可以大致分为理论题和代码题。在本节中,我们将探讨这两个类别及其各自的子类别。

理论题。

理论题是那些会要求你对某些主题的概念性理解的问题。理论题可以进一步细分为不同的类别。让我们看看这些类别,以及从之前的考试中选取的属于这些类别的示例问题。

解释题。

解释题是需要定义和解释某事的问题。它也可以包括某物是如何工作的以及它做什么。让我们看看一个例子。

以下哪项描述了一个工作节点?

  1. 工作节点是集群中执行计算的节点。

  2. 工作节点与执行器是同义词。

  3. 工作节点总是与执行器保持一对一的关系。

  4. 工作节点是 Spark 执行层次结构中最细粒度的执行级别。

  5. 工作节点是 Spark 执行层次结构中最粗粒度的执行级别。

连接题。

连接题是需要定义不同事物之间是如何相互关联的,或者它们是如何相互区别的问题。让我们通过一个例子来展示这一点。

以下哪项描述了工作节点与执行器之间的关系?

  1. 执行器是在工作节点上运行的 Java 虚拟机(JVM)。

  2. 工作节点是在执行器上运行的 JVM。

  3. 总是存在比执行器更多的工作节点。

  4. 总是存在相同数量的执行器和工作节点。

  5. 执行器和工作节点之间没有关系。

场景问题

场景问题涉及定义在不同 if-else 场景中事物是如何工作的——例如,“如果 ______ 发生,那么 _____ 就会发生。”此外,它还包括关于场景的陈述不正确的问题。让我们通过一个例子来展示这一点。

如果 Spark 以集群模式运行,以下关于节点的说法中哪一个是错误的?

  1. 有一个包含 Spark 驱动程序和执行器的工作节点。

  2. Spark 驱动程序在其自己的非工作节点上运行,没有任何执行器。

  3. 每个执行器都是工作节点内部运行的 JVM。

  4. 总是存在多个节点。

  5. 可能会有比总节点更多的执行器,或者比执行器更多的总节点。

分类问题

分类问题是这样的问题,你需要描述某个事物所属的类别。让我们通过一个例子来展示这一点。

以下哪个说法准确地描述了阶段?

  1. 阶段内的任务可以由多台机器同时执行。

  2. 作业中的各个阶段可以并发运行。

  3. 阶段包括一个或多个作业。

  4. 阶段在提交之前暂时存储事务。

配置问题

配置问题是这样的问题,你需要根据不同的集群配置概述事物的行为。让我们通过一个例子来展示这一点。

以下哪个说法准确地描述了 Spark 的集群执行模式?

  1. 集群模式在网关节点上运行执行器进程。

  2. 集群模式涉及驱动程序托管在网关机器上。

  3. 在集群模式下,Spark 驱动程序和集群管理器不在同一位置。

  4. 集群模式下的驱动程序位于工作节点上。

接下来,我们将探讨基于代码的问题及其子类别。

基于代码的问题

下一个类别是基于代码的问题。大量基于 Spark API 的问题属于这个类别。基于代码的问题是你会得到一个代码片段,然后你会被问及关于它的问题。基于代码的问题可以进一步细分为不同的类别。让我们看看这些类别,以及从之前的考试中选取的属于这些不同子类别的示例问题。

函数识别问题

函数识别问题是这样的问题,你需要定义某个事物是由哪个函数执行的。了解 Spark 中用于数据操作的不同函数及其语法非常重要。让我们通过一个例子来展示这一点。

以下哪个代码块返回了 df DataFrame 的副本,其中 columnsalary 已重命名为 employeeSalary

  1. df.withColumn(["salary", "employeeSalary"])

  2. df.withColumnRenamed("salary").alias("employeeSalary ")

  3. df.withColumnRenamed("salary", " employeeSalary ")

  4. df.withColumn("salary", " employeeSalary ")

填空题

填空题是这样的问题,您需要通过填写空白来完成代码块。让我们通过一个示例来演示这一点。

以下代码块应返回一个 DataFrame,其中包含transactionsDf DataFrame 中的employeeIdsalarybonusdepartment列。请选择正确填充空白的答案来完成此操作。

df.__1__(__2__)
    1. drop

    2. "employeeId", "salary", "``bonus", "department"

    1. filter

    2. "employeeId, salary, bonus, department"

    1. select

    2. ["employeeId", "salary", "``bonus", "department"]

    1. select

    2. col(["employeeId", "salary", "``bonus", "department"])

代码行顺序问题

代码行顺序问题是这样的问题,您需要将代码行按特定顺序排列,以便正确执行操作。让我们通过一个示例来演示这一点。

以下哪个代码块创建了一个 DataFrame,该 DataFrame 显示了基于departmentstate列的salary列的平均值,其中age大于 35?

  1. salaryDf.filter(col("age") > 35)

  2. .``filter(col("employeeID")

  3. .``filter(col("employeeID").isNotNull())

  4. .``groupBy("department")

  5. .``groupBy("department", "state")

  6. .``agg(avg("salary").alias("mean_salary"))

  7. .``agg(average("salary").alias("mean_salary"))

  8. i, ii, v, vi

  9. i, iii, v, vi

  10. i, iii, vi, vii

  11. i, ii, iv, vi

摘要

本章提供了认证考试的概述。到目前为止,您已经知道考试中可以期待什么,以及如何最好地准备它。为此,我们介绍了您将遇到的不同类型的问题。

今后,本书的每一章都将为您提供实用的知识和动手实践示例,以便您能够利用 Apache Spark 进行各种数据处理和分析任务。

第二部分:介绍 Spark

本部分将为您提供对 Spark 功能和操作原理的全面了解。它将涵盖 Spark 是什么,为什么它很重要,以及 Spark 最有用的应用领域。它将介绍可以从 Spark 中受益的不同类型的用户。它还将涵盖 Spark 的基本架构以及如何在 Spark 中导航应用程序。它将详细说明窄和宽 Spark 转换,并讨论 Spark 中的懒加载评估。这种理解很重要,因为 Spark 的工作方式与其他传统框架不同。

本部分包含以下章节:

  • 第二章**,理解 Apache Spark 及其应用

  • 第三章**,Spark 架构和转换

第二章:理解 Apache Spark 及其应用

随着机器学习和数据科学的兴起,世界正在经历一个范式转变。每秒钟都在收集大量的数据,而计算能力难以跟上这种快速数据增长的速度。为了利用所有这些数据,Spark 已经成为大数据处理的事实标准。将数据处理迁移到 Spark 不仅是一个节省资源的问题,让你能专注于你的业务;它也是一种现代化你的工作负载,利用 Spark 的能力和现代技术堆栈来创造新的商业机会的手段。

在本章中,我们将涵盖以下主题:

  • 什么是 Apache Spark?

  • 为什么选择 Apache Spark?

  • Spark 的不同组件

  • Spark 有哪些用例?

  • Spark 的用户是谁?

什么是 Apache Spark?

Apache Spark 是一个开源的大数据框架,用于多个大数据应用。Spark 的强大之处在于其卓越的并行处理能力,使其在其领域成为领导者。

根据其网站(spark.apache.org/),“最广泛使用的可扩展计算引擎

Apache Spark 的历史

Apache Spark 始于 2009 年在加州大学伯克利分校 AMPLab 的一个研究项目,并于 2010 年转为开源许可。后来,在 2013 年,它成为 Apache 软件基金会的一部分(spark.apache.org/)。它在 2013 年之后获得了流行,如今,它已成为众多财富 500 强公司大数据产品的支柱,有成千上万的开发者正在积极为其工作。

Spark 的出现是由于 Hadoop MapReduce 框架的限制。MapReduce 的主要前提是从磁盘读取数据,将数据分发以进行并行处理,对数据应用 map 函数,然后将这些函数减少并保存回磁盘。这种来回读取和保存到磁盘的过程很快就会变得耗时且成本高昂。

为了克服这一限制,Spark 引入了内存计算的概念。除此之外,Spark 还拥有由不同研究倡议带来的几个能力。你将在下一节中了解更多关于它们的信息。

理解 Spark 的不同之处

Spark 的基础在于其主要能力,如内存计算、延迟评估、容错性和支持 Python、SQL、Scala 和 R 等多种语言。我们将在下一节中详细讨论每一个。

让我们从内存计算开始。

内存计算

Spark 基础构建的第一个主要区分性技术是它利用内存计算。记得我们讨论 Hadoop MapReduce 技术时吗?它的一个主要限制是在每个步骤都将数据写回磁盘。Spark 将其视为改进的机会,并引入了内存计算的概念。主要思想是数据在处理过程中始终保持在内存中。如果我们能够处理一次性存储在内存中的数据量,我们就可以消除在每个步骤中写入磁盘的需要。因此,如果我们能够处理所有计算的数据量,整个计算周期就可以在内存中完成。现在,需要注意的是,随着大数据的出现,很难将所有数据都包含在内存中。即使我们在云计算世界的重型服务器和集群中看,内存仍然是有限的。这就是 Spark 并行处理内部框架发挥作用的地方。Spark 框架以最有效的方式利用底层硬件资源。它将计算分布在多个核心上,并充分利用硬件能力。

这极大地减少了计算时间,因为只要数据可以适应 Spark 计算内存,写入磁盘和读取回磁盘的开销就会最小化。

延迟评估

通常,当我们使用编程框架时,后端编译器会查看每个语句并执行它。虽然这对于编程范式来说效果很好,但在大数据和并行处理的情况下,我们需要转向一种前瞻性的模型。Spark 因其并行处理能力而闻名。为了实现更好的性能,Spark 不会在读取代码时执行代码,而是在代码存在并且我们提交 Spark 语句以执行时,第一步是 Spark 构建查询的逻辑映射。一旦这个映射建立,然后它会规划最佳的执行路径。你将在 Spark 架构章节中了解更多关于其复杂性的内容。一旦计划确定,执行才会开始。即使执行开始,Spark 也会推迟执行所有语句,直到遇到一个“操作”语句。Spark 中有两种类型的语句:

  • 转换

  • 操作

你将在第三章中详细了解 Spark 语句的不同类型,其中我们讨论了 Spark 架构。以下是延迟评估的一些优点:

  • 效率

  • 代码可管理性

  • 查询和资源优化

  • 简化复杂性

弹性数据集/容错性

Spark 的基础是建立在弹性分布式数据集RDDs)之上的。它是一个不可变的分布式对象集合,代表了一组记录。RDDs 分布在多个服务器上,它们在多个集群节点上并行计算。RDDs 可以通过代码生成。当我们从外部存储位置读取数据到 Spark 中时,RDDs 会保存这些数据。这些数据可以在多个集群之间共享,并且可以并行计算,从而为 Spark 提供了非常高效的在 RDD 数据上运行计算的方法。RDDs 被加载到内存中进行处理;因此,与 Hadoop 不同,不需要将数据加载到和从内存中进行计算。

RDDs 具有容错性。这意味着如果出现故障,RDDs 有自我恢复的能力。Spark 通过将这些 RDD 分布到不同的工作节点上,同时考虑到每个工作节点执行的任务,来实现这一点。这种对工作节点的处理由 Spark 驱动器完成。我们将在后续章节中详细讨论这一点。

RDDs 在弹性和容错性方面赋予了 Spark 很大的能力。这种能力,结合其他特性,使得 Spark 成为任何生产级应用的工具选择。

多语言支持

Spark 支持多种开发语言,如 Java、R、Scala 和 Python。这使用户能够灵活地使用任何选择的编程语言在 Spark 中构建应用程序。

Spark 的组件

让我们来谈谈 Spark 的不同组件。正如你在图 1.1中可以看到的,Spark Core 是 Spark 操作的核心,横跨 Spark 的所有其他组件。本节我们将讨论的其他组件包括 Spark SQL、Spark Streaming、Spark MLlib 和 GraphX。

图 2.1:Spark 组件

图 2.1:Spark 组件

让我们来看看 Spark 的第一个组件。

Spark Core

Spark Core 是 Spark 所有其他组件的核心。它为所有不同的组件提供功能和核心特性。Spark SQL、Spark Streaming、Spark MLlib 和 GraphX 都使用 Spark Core 作为其基础。Spark 的所有功能和特性都由 Spark Core 控制。它提供了内存计算能力以提供速度,一个通用的执行模型以支持广泛的各类应用,以及 Java、Scala 和 Python API 以简化开发。

在所有这些不同的组件中,你可以使用支持的语言编写查询。然后 Spark 将这些查询转换为有向无环图DAGs),而 Spark Core 负责执行它们。

Spark Core 的关键职责如下:

  • 与存储系统交互

  • 内存管理

  • 任务分配

  • 任务调度

  • 任务监控

  • 内存计算

  • 容错性

  • 优化

Spark Core 包含一个用于 RDDs 的 API,RDDs 是 Spark 的组成部分。它还提供了不同的 API 来交互和工作与 RDDs。Spark 的所有组件都使用底层的 RDDs 进行数据处理。RDDs 使得 Spark 能够拥有数据的历史记录,因为它们是不可变的。这意味着每次对 RDD 执行需要更改的操作时,Spark 都会为它创建一个新的 RDD。因此,它维护 RDD 及其对应操作的历史记录信息。

Spark SQL

SQL 是数据库和数据仓库应用中最流行的语言。分析师使用这种语言进行所有基于关系数据库和传统数据仓库的探索性数据分析。Spark SQL 将这一优势添加到 Spark 生态系统中。Spark SQL 用于使用 DataFrame API 以 SQL 查询结构化数据。

如其名称所示,Spark SQL 为 Spark 提供了 SQL 支持。这意味着我们可以使用 DataFrame API 查询 RDD 和其他外部源中的数据。这是 Spark 的一个强大功能,因为它为开发者提供了在 RDD 和其他文件格式之上使用关系表结构的灵活性,并允许在上面编写 SQL 查询。这也增加了在必要时使用 SQL 的能力,并将其与分析应用程序和用例统一,从而提供了平台的统一。

使用 Spark SQL,开发者可以轻松完成以下操作:

  • 他们可以从不同的文件格式和不同的来源读取数据到 RDDs 和 DataFrames 中

  • 他们可以在 DataFrame 中的数据上运行 SQL 查询,从而为开发者提供使用编程语言或 SQL 处理数据的灵活性

  • 一旦完成数据处理,他们就有能力将 RDDs 和 DataFrames 写入外部源

Spark SQL 包含一个基于成本的优化器,它优化查询,同时考虑资源;它还具有生成这些优化代码的能力,这使得这些查询非常快速和高效。为了支持更快的查询时间,它可以在 Spark Core 的帮助下扩展到多个节点,并提供诸如容错和弹性等特性。这被称为 Catalyst 优化器。我们将在第五章中了解更多关于它。

Spark SQL 最显著的特点如下:

  • 它提供了一个高级结构化 API 的引擎

  • 读取/写入到和从大量文件格式,如 Avro、Delta、逗号分隔值CSV)和 Parquet

  • 提供了开放数据库连接ODBC)和Java 数据库连接JDBC)连接器,用于连接商业智能(BI)工具,如 PowerBI 和 Tableau,以及流行的关系数据库RDBMs

  • 提供了一种将文件中的结构化数据查询为表和视图的方法

  • 它支持符合 ANSI SQL:2003 命令和 HiveQL

既然我们已经涵盖了 SparkSQL,让我们来讨论 Spark Streaming 组件。

Spark Streaming

我们已经讨论了当今时代数据的快速增长。如果我们将这些数据分组,实际上有两种数据集类型,批处理和流式处理:

  • 批处理数据是指存在一块数据,你必须一次性摄取并转换。想象一下,当你想要获取一个月内所有销售的报告时。你将拥有作为批处理的月度数据,并一次性处理它。

  • 流数据是指你需要实时数据的输出。为了满足这一需求,你必须实时摄取和处理这些数据。这意味着每个数据点都可以作为一个单独的数据元素摄取,我们不会等待收集到数据块后再进行摄取。想象一下,自动驾驶汽车需要根据收集到的数据实时做出决策。所有数据都需要实时摄取和处理,以便汽车在特定时刻做出有效的决策。

有许多行业正在生成流数据。为了利用这些数据,你需要实时摄取、处理和管理这些数据。对于组织来说,使用流数据作为实时分析和其他用例已经成为一项基本要求。这使他们比竞争对手具有优势,因为这使他们能够实时做出决策。

Spark Streaming 使组织能够利用流数据。Spark Streaming 最重要的因素之一是它易于使用,并且可以与批处理数据一起使用。你可以在一个框架内结合批处理和流数据,并使用它来增强你的分析应用程序。Spark Streaming 还继承了 Spark Core 的弹性和容错特性,使其在行业中占据主导地位。它集成了大量流数据源,如 HDFS、Kafka 和 Flume。

Spark Streaming 的美丽之处在于,批处理数据可以作为流进行处理,以利用流数据的内置范式和回溯能力。当我们处理实时数据时,需要考虑某些因素。当我们处理实时数据流时,可能会因为系统故障或完全失败而错过一些数据。Spark Streaming 以无缝的方式处理这个问题。为了满足这些需求,它有一个内置机制,称为检查点。这些检查点的目的是跟踪传入的数据,了解下游处理了什么数据,以及在下一次周期中还有哪些数据需要处理。我们将在详细讨论 Spark Streaming 的第七章中了解更多关于这一点。

这使得 Spark 对故障具有弹性。如果有任何故障,您需要做最少的工作来重新处理旧数据。您还可以定义缺失数据或延迟处理数据的机制和算法。这为数据管道提供了很大的灵活性,并使它们在大规模生产环境中更容易维护。

Spark MLlib

Spark 提供了一个用于分布式和可扩展机器学习的框架。它将计算分布在不同的节点上,从而在模型训练方面实现更好的性能。它还分布了超参数调整。您将在第八章中了解更多关于超参数调整的内容,我们将讨论机器学习。因为 Spark 可以扩展到大型数据集,所以它是机器学习生产管道的首选框架。当您构建产品时,执行和计算速度非常重要。Spark 让您能够处理大量数据,并构建运行非常高效的先进机器学习模型。与需要数天训练的模型相比,Spark 将时间缩短到数小时。此外,处理更多数据在大多数情况下会导致性能更好的模型。

大多数常用的机器学习算法都是 Spark 库的一部分。Spark 中有两个机器学习包可用:

  • Spark MLlib

  • Spark ML

这两个之间的主要区别是它们处理的数据类型。Spark MLlib 建立在 RDD 之上,而 Spark ML 与 DataFrame 一起工作。Spark MLlib 是较旧的库,现在已进入维护模式。更先进的库是 Spark ML。您还应注意,Spark ML 不是库本身的官方名称,但它通常用于指代 Spark 中基于 DataFrame 的 API。官方名称仍然是 Spark MLlib。然而,了解这些差异是很重要的。

Spark MLlib 包含了最常用的机器学习库,用于分类回归聚类推荐系统。它还支持一些频繁模式挖掘算法。

当需要将这些模型服务于数百万甚至数十亿用户时,Spark 也非常有帮助。您可以使用 Spark 分发和并行化数据处理(提取、转换、加载ETL))和模型评分。

GraphX

GraphX 是 Spark 的图和图并行计算的 API。GraphX 扩展了 Spark 的 RDD 以支持图,并允许您使用图对象运行并行计算。这显著提高了计算速度。

这里有一个表示图外观的网络图。

图 2.2:一个网络图

图 2.2:一个网络图

图是一个具有顶点和边的对象。属性附加到每个顶点和边上。Spark 支持一些主要的图操作,例如subgraphjoinVertices

主要前提是你可以使用 GraphX 进行探索性分析和 ETL,并使用 RDD 高效地转换和连接图。有两种类型的操作符——GraphGraphOps。在此基础上,还有图聚合操作符。Spark 还包括许多在常见用例中使用的图算法。以下是一些最受欢迎的算法:

  • PageRank

  • 连通分量

  • 标签传播

  • SVD++

  • 强连通分量

  • 三角形计数

现在,让我们讨论为什么我们想在应用中使用 Spark 以及它提供的一些特性。

为什么选择 Apache Spark?

在本节中,我们将讨论 Apache Spark 的应用及其特性,例如速度、可重用性、内存计算以及 Spark 是如何成为一个统一平台的。

速度

Apache Spark 是目前可用的最快数据处理框架之一。它比 Hadoop MapReduce 快得多。主要原因在于其内存计算能力和延迟评估。我们将在下一章讨论 Spark 架构时了解更多关于这一点。

可重用性

可重用性对于使用现代平台的大型组织来说是一个非常重要的考虑因素。Spark 可以无缝地连接批处理和流数据。此外,你可以通过添加历史数据来增强数据集,以更好地满足你的用例。这为运行查询或构建现代分析系统提供了大量历史数据视图。

内存计算

使用内存计算,消除了读取和写入磁盘的所有开销。数据被缓存,在每一步中,所需的数据已经存在于内存中。在处理结束时,结果被汇总并发送回驱动程序以进行后续步骤。

所有这些都得益于 Spark 固有的 DAG 创建过程。在执行之前,Spark 创建必要的步骤的 DAG 并根据其内部算法对其进行优先排序。我们将在下一章中了解更多关于这一点。这些功能支持内存计算,从而实现快速处理速度。

统一平台

Spark 提供了一个统一的数据工程、数据科学、机器学习、分析、流处理和图处理平台。所有这些组件都与 Spark Core 集成。核心引擎非常高速,并概括了其其他组件所需的一些常用任务。这使得 Spark 在与其他平台相比时具有优势,因为其不同组件的统一。这些组件可以协同工作,为软件应用提供统一的体验。在现代应用中,这种统一使得使用变得容易,并且应用的不同部分可以充分利用这些组件的核心功能,而不会牺牲功能。

现在你已经了解了使用 Spark 的好处,让我们来谈谈 Spark 在行业中的不同用例。

Spark 有哪些用例?

在本节中,我们将了解 Spark 在行业中的应用。目前 Spark 有各种用例,包括大数据处理、机器学习应用、近实时和实时流处理,以及使用图分析。

大数据处理

Spark 最流行的用例之一是大数据处理。你可能想知道什么是大数据,那么让我们来看看标记数据为大数据的组成部分。

大数据的第一个组成部分是数据量。按数据量来说,数据非常大,在某些情况下,数据量可能达到太字节、拍字节甚至更多。多年来,组织收集了大量的数据。这些数据可以用于分析。然而,在这个活动的第一步是处理这些大量的数据。此外,只有最近,计算能力才增长到能够处理如此庞大的数据量。

大数据的第二个组成部分是数据速度。数据速度指的是数据生成、摄取和分布的速度。这意味着近年来数据生成的速度已经大幅增加。以你的智能设备为例,它每秒向服务器发送数据。在这个过程中,服务器还需要跟上数据的摄取,然后可能需要将数据分布到不同的来源。

大数据的第三个组成部分是数据多样性。数据多样性指的是生成数据的不同来源。它还指的是生成的不同类型的数据。那些只有结构化格式可以保存为数据库中的表的数据生成时代已经过去了。目前,数据可以是结构化的、半结构化的或非结构化的。现在的系统必须处理所有这些不同的数据类型,工具应该能够操作这些不同的数据类型。想想需要处理的照片或可以使用高级分析进行分析的音频和视频文件。

还可以将其他一些组件添加到原始的三个 V 中,例如真实性和价值。然而,这些组件超出了我们讨论的范围。

大数据太大,常规机器无法处理。这就是为什么它被称为大数据。高容量、高速度、高多样性的大数据需要使用像 Spark 这样的高级分析工具进行处理,Spark 可以在不同的机器或集群之间分配工作负载,并并行处理以利用机器上的所有可用资源。因此,Spark 使我们能够将数据分成不同的部分,并在不同的机器上并行处理。这极大地加快了整个过程,并利用了所有可用资源。

由于上述所有原因,Spark 是使用最广泛的的大数据处理技术之一。大型组织利用 Spark 来分析和操作他们的大数据堆栈。Spark 在复杂分析用例中作为大数据处理的基础。

以下是一些大数据用例的例子:

  • 报告和仪表板业务智能

  • 复杂应用的数据仓库

  • 应用监控的操作分析

这里需要特别注意的是,与 Spark 合作需要从单节点处理模式转变为大数据处理模式。你现在必须开始思考如何最好地利用和优化大型集群进行处理,以及并行处理的一些最佳实践。

机器学习应用

随着数据的增长,对机器学习模型利用更多数据的需要也在增加。目前在机器学习社区中普遍认为,提供给模型的数据越多,模型就越好。这导致了需要大量数据提供给模型进行预测分析的需求。当我们处理大量数据时,训练机器学习模型的挑战比数据处理更加复杂。原因是机器学习模型通过压缩数据并运行统计估计来达到最小误差点。为了达到这个最小误差,模型必须执行复杂的数学运算,如矩阵乘法。这些计算需要在内存中提供大量数据,并在其上运行计算。这为机器学习中的并行处理提供了案例。

机器学习为产品增加了预测元素。我们不再只是对已经发生的变化做出反应,而是可以根据历史数据和趋势主动寻找改进我们产品和服务的途径。组织的每个方面都可以利用机器学习进行预测分析。机器学习可以应用于许多行业,从医院到零售店再到制造组织。我们在互联网上完成任务时,如在线买卖、浏览和搜索网站、使用社交媒体平台,都遇到过某种机器学习算法。不知不觉中,机器学习已经成为我们生活的重要组成部分。

尽管在机器学习方面,组织可以利用大量用例,但我在这里只突出强调几个:

  • 个性化购物

  • 网站搜索和排名

  • 银行和保险业的欺诈检测

  • 客户情感分析

  • 客户细分

  • 推荐引擎

  • 价格优化

  • 预测性维护和支持

  • 文本和视频分析

  • 客户/患者 360 度

让我们继续讨论实时流处理。

实时流处理

实时流是 Spark 真正发光的用例之一。提供与 Spark Streaming 相同灵活性的竞争框架非常少。

Spark Streaming 提供了一种机制,可以从多个流数据源(如 Kafka 和 Amazon Kinesis)中摄取数据。一旦数据被摄取,就可以使用非常高效的 Spark Streaming 处理实时处理。

有许多实时用例可以利用 Spark Streaming。以下是一些例子:

  • 自动驾驶汽车

  • 实时报告和分析

  • 提供股市数据的更新

  • 物联网(IoT)数据摄取和处理

  • 实时新闻数据处理

  • 实时分析以优化库存和运营

  • 信用卡实时欺诈检测系统

  • 实时事件检测

  • 实时推荐

大型全球组织利用 Spark Streaming 实时处理数十亿甚至数万亿的数据行。我们在日常生活中也能看到一些这样的应用。例如,当你外出购物时,你的信用卡阻止了一笔交易,这就是实时欺诈检测的一个例子。Netflix 和 YouTube 使用实时交互,视频平台推荐用户观看下一部视频。

随着我们进入一个每个设备都将数据发送回其服务器进行分析的世界,对流和实时分析的需求增加。使用 Spark Streaming 进行此类数据的主要优势之一是其内置的回溯和延迟处理数据的能力。我们之前也讨论了这种方法的实用性,并且由于这些能力,大量手动管道处理工作被移除。当我们讨论 Spark Streaming 时,我们将了解更多关于这一点,第七章 将会涉及。

图分析

图分析通过分析不同实体之间的关系,提供了一种独特的数据观察方式。图中的顶点代表实体,图的边代表两个实体之间的关系。以你在 Facebook 或 Instagram 上的社交网络为例。你代表一个实体,而你连接的人代表另一个实体。你和你朋友之间的联系(连接)就是边。同样,你在社交媒体上的兴趣可能都是不同的边。然后,可以有一个位置类别,属于同一位置的所有人都会与该位置有一个边(关系),依此类推。因此,可以与任何不同类型的实体建立联系。你连接得越多,你与志同道合的人或兴趣相连接的可能性就越高。这是衡量不同实体之间关系的一种方法。这些类型的图有几种用途。Spark 的美丽之处在于,它可以通过分布式处理这些图来快速找到这些关系。对于数十亿个实体,可能会有数百万甚至数十亿个连接。Spark 具有分配这些工作负载和快速计算复杂算法的能力。

以下是一些图分析的应用场景:

  • 社交网络分析

  • 欺诈检测

  • 基于相关性的页面排名

  • 天气预测

  • 搜索引擎优化

  • 供应链分析

  • 在社交媒体上寻找影响者

  • 洗钱和欺诈检测

随着图分析用例数量的不断增长,这证明在当今行业中,我们需要分析实体之间关系的网络,这是一个关键的应用场景。

在下一节中,我们将讨论 Spark 用户是谁以及他们在组织中的典型角色。

Spark 用户是谁?

随着世界向数据驱动的决策方法转变,数据和能够利用它做出关键业务决策的不同类型用户的作用变得至关重要。在数据中存在不同类型的用户,他们可以利用 Spark 实现不同的目的。在本节中,我将介绍其中一些不同的用户。这不是一个详尽的列表,但它应该能给你一个关于今天数据驱动组织中存在的不同角色的概念。然而,随着行业的发展,许多新的角色正在出现,它们与以下章节中提到的角色相似,尽管每个角色可能都有其独特的职责。

我们将从数据分析师的角色开始。

数据分析师

在当今的数据领域,更传统的角色是数据分析师。数据分析师通常是数据的第一层级角色。这意味着数据分析师是组织决策的核心。这个角色跨越组织的不同业务部门,并且通常,数据分析师需要与多个业务利益相关者互动,以传达他们的需求。这需要了解业务领域及其流程。当分析师对业务及其目标有了解时,他们才能最好地履行他们的职责。此外,很多时候,需求是使当前流程更有效率,这最终会为业务带来更好的底线。因此,不仅需要了解业务目标,还需要了解它们是如何协同工作的,这是这个角色的一项主要要求。

数据分析师的一个典型工作角色可能如下所示:

  1. 当数据分析师在一个组织中接到一个项目时,项目的第一步是从多个利益相关者那里收集需求。让我们用一个例子来说明。假设你加入了一个组织作为数据分析师。这个组织生产和销售计算机硬件。你被分配的任务是报告过去 10 年中每个月的收入。对你来说,第一步就是收集所有需求。可能有些利益相关者想知道每个月销售了多少个特定产品的单位,而其他人可能想知道收入是否持续增长。记住,你的报告的最终用户可能工作在组织的不同业务部门。

  2. 一旦你收集了所有相关利益相关者的需求,接下来就是进行下一步,也就是寻找相关的数据源来回答你所负责的问题。你可能需要与组织或平台架构师中的数据库管理员交谈,以了解不同数据源的位置,这些数据源中包含对你提取信息有用的相关信息。

  3. 一旦你有了所有相关的来源,那么你想要以编程方式(在大多数情况下)与这些来源连接,清理并合并一些数据,以满足你的要求,从而得出相关的统计数据。这就是 Spark 可以帮助你连接到这些不同的数据来源,并高效地读取和操作数据的地方。你还需要根据业务需求对数据进行切割和细分。一旦数据清理完成并生成了统计数据,你想要基于这些统计数据生成一些报告。市场上有很多生成报告的工具,如 Qlik 和 Tableau,你可以使用它们。一旦生成了报告,你可能想要与利益相关者分享你的结果。你可以向他们展示你的结果,或者与他们分享报告,具体取决于首选的媒介。这将帮助利益相关者做出基于数据的、信息化的、业务关键性的决策。

不同角色之间的协作对于数据分析师来说也起着重要的作用。由于组织已经收集数据很长时间了,最重要的事情是与多年来收集的所有数据进行工作,并从中找到意义,帮助企业在关键决策中做出贡献。帮助进行数据驱动的决策是成为一名成功的数据分析师的关键。

这是前几段讨论的项目中采取的步骤的总结:

  1. 从利益相关者那里收集需求。

  2. 确定相关的数据来源。

  3. 与主题专家(SMEs)合作。

  4. 切割和细分数据。

  5. 生成报告。

  6. 分享结果。

接下来让我们看看数据工程师。这个角色在当今行业中获得了很大的关注。

数据工程师

在行业中越来越普遍的新角色是数据工程师。这是一个相对较新的角色,但近年来获得了巨大的流行度。原因是数据增长速度极快。现在每秒钟产生的数据比几年前一个月产生的数据还要多。处理所有这些数据需要专业的技能。这些数据已经无法被大多数计算机的适度内存所容纳,因此我们必须利用云计算的巨大规模来满足这一需求。随着数据需求变得更加复杂,我们需要复杂的架构来处理和使用这些数据以进行商业决策。这就是数据工程师角色发挥作用的地方。数据工程师的主要工作是准备数据以供不同目的的摄取。利用这些准备好的数据的下游系统可能是基于这些数据运行报告的仪表板,也可能是与高级机器学习算法合作进行预测分析的解决方案,以便根据数据做出主动决策。

更广泛地说,数据工程师负责创建、维护、优化和监控为组织中的不同用例提供服务的管道。这些管道通常被称为提取、转换、加载(ETL)管道。主要区别在于数据工程师必须处理的数据规模之大。当有下游需求用于 BI 报告、高级分析以及/或机器学习时,这就是数据管道在大项目中发挥作用的地方。

组织中数据工程师的一个典型工作角色可能如下。当数据工程师被分配创建一个项目数据管道的任务时,他们首先需要考虑的是应用程序的整体架构。在一些组织中可能有数据架构师来帮助处理一些架构需求,但这并不总是如此。因此,数据工程师会提出如下问题:

  • 数据的不同来源有哪些?

  • 数据的大小是多少?

  • 数据目前存储在哪里?

  • 我们是否需要在不同的工具之间迁移数据?

  • 我们如何连接到数据?

  • 需要什么样的数据转换?

  • 数据多久更新一次?

  • 我们是否应该预期新数据会有模式变更?

  • 如果出现故障,我们如何监控管道?

  • 我们是否需要创建一个用于故障的通知系统?

  • 我们是否需要为故障添加重试机制?

  • 故障的超时策略是什么?

  • 如果出现故障,我们如何运行过期的管道?

  • 我们如何处理不良数据?

  • 我们应该遵循什么策略——ETL 还是 ELT?

  • 我们如何节省计算成本?

一旦他们回答了这些问题,他们就开始着手构建一个具有弹性的架构来构建数据管道。一旦这些管道运行并经过测试,下一步就是维护这些管道,使处理更加高效和易于故障检测。目标是构建这些管道,以便一旦运行完毕,数据的最终状态对于不同的下游用例是一致的。过于频繁的是,数据工程师必须与数据分析师和数据科学家合作,根据所需用例制定正确的数据转换要求。

让我们谈谈数据科学家,这是一个在多个论坛上被宣传为“21 世纪最性感的工作”的职位。

数据科学家

传统上,数据一直被用于基于过去发生的事情进行决策。这意味着组织是基于数据做出反应的。现在,在高级和预测分析方面已经发生了范式转变。这意味着组织在决策上可以变得主动,而不是被动。他们通过现在组织可用的所有数据来实现这一点。为了有效地利用这些数据,数据科学家扮演着重要的角色。他们将分析提升到下一个层次,而不是仅仅查看过去发生的事情,他们拥有复杂的机器学习算法来预测未来可能发生的事情。所有这些都是基于他们可用的海量数据。

在一个组织中,数据科学家的典型工作角色可能如下所示。

数据科学家被分配了一个需要解决的问题或一个需要回答的问题。首要任务是查看他们可以用来回答这个问题的数据类型。他们会基于给定的数据提出一些假设进行测试。如果结果积极,并且数据能够回答一些问题陈述,那么他们就会继续通过实验来处理数据,并寻求更有效地回答手头问题的方法。为此,他们会将不同的数据集合并在一起,并且也会转换数据,使其适合某些机器学习算法使用。在这个阶段,他们还需要决定他们旨在解决的机器学习问题类型。

他们可以使用三种主要的机器学习技术:

  • 回归

  • 分类

  • 聚类

根据决定的技术和数据转换,他们接下来会使用几个机器学习算法进行原型设计,以创建一个基线模型。基线模型是一个非常基本的模型,用于回答原始问题。基于这个基线模型,可以创建其他模型,这些模型能够更好地回答问题。在某些情况下,一些预定义的规则也可以作为基线模型。这意味着企业可能已经在一些预定义的规则上运行,这些规则可以作为基准来比较机器学习模型。一旦完成初步的原型设计,数据科学家就会继续进行更高级的模型优化。他们可以与模型的不同的超参数一起工作,或者尝试不同的数据转换和样本大小。所有这些都可以在 Spark 或其他工具和语言中完成,具体取决于他们的偏好。Spark 具有并行运行这些算法的优势,使整个过程非常高效。一旦数据科学家根据不同的指标对模型结果感到满意,他们就会将模型移动到生产环境,在这些环境中,这些模型可以服务于客户解决特定问题。在这个阶段,他们会将这些模型交给机器学习工程师,开始将它们集成到管道中。

以下是前一段讨论的项目中采取的步骤总结:

  1. 创建并测试一个假设。

  2. 转换数据。

  3. 我们决定使用哪种机器学习算法?

  4. 使用不同的机器学习模型进行原型设计。

  5. 创建一个基线模型。

  6. 调整模型。

  7. 调整数据。

  8. 将模型过渡到生产环境。

接下来,让我们讨论机器学习工程师的角色。

机器学习工程师

与数据工程师一样,机器学习工程师也构建管道,但这些管道主要是为机器学习模型部署而构建的。机器学习工程师通常使用数据科学家创建的原型模型,并围绕它们构建机器学习管道。我们将讨论机器学习管道是什么,以及构建这些管道需要回答的一些问题。

机器学习模型是为了解决复杂问题并提供高级分析方法以服务于企业而构建的。在原型设计之后,这些模型需要在组织的生产环境中运行并部署以服务于客户。对于部署,需要考虑以下几个因素:

  • 用于模型训练的数据有多少?

  • 我们计划同时服务于多少客户?

  • 我们需要多久重新训练一次模型?

  • 我们预计数据多久会变化一次?

  • 我们如何根据需求进行管道的扩展和缩减?

  • 我们如何监控模型训练中的失败?

  • 我们是否需要为失败发送通知?

  • 我们是否需要为失败添加重试机制?

  • 失败的超时策略是什么?

  • 我们如何在生产中衡量模型性能?

  • 我们如何应对数据漂移?

  • 我们如何应对模型漂移?

一旦这些问题得到解答,下一步就是围绕这些模型构建一个管道。管道的主要目的是,当新数据到来时,预训练的模型能够根据新的数据集回答问题。

让我们用一个例子来更好地理解这些管道。我们将继续使用一个组织销售计算机硬件的第一个例子:

  1. 假设该组织希望在网站上构建一个推荐系统,向用户推荐购买哪些产品。

  2. 数据科学家已经构建了一个与测试数据表现良好的原型模型。现在,他们想将其部署到生产环境中。

  3. 为了部署这个模型,机器学习工程师需要看看他们如何将这个模型整合到网站上。

  4. 他们将从从网站上获取数据开始,以获取用户信息。

  5. 一旦他们有了信息,他们就会将其通过数据管道进行清洗和合并数据。

  6. 他们还可能想将一些预计算的特征添加到模型中,例如一年中的时间,以更好地了解是否是假日季节以及是否有一些特别优惠正在进行。

  7. 然后,他们需要一个 REST API 端点来获取网站上每个用户的最新推荐。

  8. 之后,网站需要连接到 REST 端点以服务实际客户。

  9. 一旦这些模型部署到实际系统中(在我们的例子中是网站),就需要有一个监控系统来监控模型或数据中的任何错误和变化。这分别被称为模型漂移数据漂移

数据漂移

数据可能会随时间变化。在我们的例子中,人们的偏好可能会随着时间的推移或季节性而变化,数据可能不同。例如,在假日季节,人们的偏好可能会略有变化,因为他们正在寻找为朋友和家人购买礼物,因此根据这些偏好推荐相关产品对业务至关重要。监控这些趋势和数据变化将随着时间的推移产生更好的模型,并最终有利于业务。

模型漂移

与数据漂移类似,我们还有模型漂移的概念。这意味着模型会随时间变化,最初构建的旧模型在向网站访客推荐项目方面不再是表现最佳的。随着数据的变化,模型也需要不时地进行更新。为了了解何时需要更新模型,我们需要对模型进行监控。这种监控会持续比较旧模型的结果与新数据,看模型性能是否下降。如果是这样,那就需要更新模型了。

这个模型部署的全生命周期通常是机器学习工程师的责任。请注意,对于不同的问题,这个过程会有所不同,但总体思路保持不变。

摘要

在本章中,我们学习了 Apache Spark 的基础知识以及为什么 Spark 在工业界的大数据应用中变得越来越普遍。我们还学习了 Spark 的不同组件以及这些组件在应用开发方面的帮助。然后,我们讨论了当前行业中存在的不同角色以及谁可以利用 Spark 的能力。最后,我们讨论了 Spark 在不同行业用例中的现代应用。

样题

虽然这些问题不是 Spark 认证的一部分,但回答这些问题以评估你对 Spark 基础知识的理解是很好的:

  1. Spark 的核心组件有哪些?

  2. 我们在什么时候想使用 Spark Streaming?

  3. Spark Streaming 中的回溯机制是什么?

  4. Spark 有哪些好的用例?

  5. 在组织中哪些角色应该使用 Spark?

第三章:Spark 架构和转换

Spark 在数据处理方面与传统工具和技术不同。为了理解 Spark 的独特方法,我们必须了解其基本架构。深入了解 Spark 的架构及其组件将给你一个关于 Spark 如何实现其在大数据分析中突破性处理速度的思路。

在本章中,你将了解以下更广泛的话题:

  • Spark 架构和执行层次结构

  • Spark 的不同组件

  • Spark 驱动程序和 Spark 执行器的角色

  • Spark 的不同部署模式

  • 作为 Spark 操作,转换和动作

到本章结束时,你将对 Spark 的内部工作原理有宝贵的见解,并知道如何有效地应用这些知识以通过你的认证考试。

Spark 架构

在前面的章节中,我们讨论了 Apache Spark 是一个开源的分布式计算框架,旨在处理大数据分析和处理。其架构旨在高效地处理各种工作负载,提供速度、可扩展性和容错性。理解 Spark 的架构对于理解其处理大量数据的能力至关重要。

Spark 架构的组成部分协同工作以高效处理数据。以下是一些主要组件:

  • Spark 驱动程序

  • SparkContext

  • 集群管理器

  • 工作节点

  • Spark 执行器

  • 任务

在我们讨论这些组件之前,了解它们的执行层次结构对于知道每个组件在 Spark 程序启动时如何交互非常重要。

执行层次结构

让我们借助图 3.1中的架构来查看 Spark 应用程序的执行流程:

图 3.1:Spark 架构

图 3.1:Spark 架构

这些步骤概述了从提交 Spark 作业到作业完成后释放资源的流程:

  1. Spark 执行开始于用户向 Spark 引擎提交spark-submit请求。这将创建一个 Spark 应用程序。一旦执行了操作,就会创建一个作业

  2. 此请求将启动与集群管理器的通信。作为回应,集群管理器初始化 Spark 驱动程序以执行 Spark 应用程序的main()方法。为了执行此方法,将创建SparkSession

  3. 驱动程序开始与集群管理器通信,并请求资源以开始规划执行。

  4. 集群管理器随后启动执行器,它们可以直接与驱动程序通信。

  5. 驱动程序创建一个逻辑计划,称为有向无环图(DAG),以及基于需要执行的任务总数的执行计划。

  6. 驱动程序还将数据分配给每个执行器运行,包括任务。

  7. 每个任务运行完成后,驱动程序会获取结果。

  8. 当程序运行完成后,main()方法退出,Spark 释放所有 executor 和驱动器资源。

现在你已经了解了执行层次结构,让我们详细讨论 Spark 的每个组件。

Spark 组件

让我们深入了解每个 Spark 组件的内部工作原理,以了解它们如何在每个组件中发挥关键作用,从而实现高效的分布式数据处理。

Spark 驱动器

Spark 驱动器是 Spark 中智能和高效计算的核心。Spark 遵循一种在网络拓扑中通常被称为主从架构的架构。将 Spark 驱动器视为主节点,将 Spark executors 视为从节点。驱动器在任何给定时间都控制并了解所有 executor。知道有多少 executor 存在以及是否有 executor 失败是驱动器的责任,以便它可以回退到其替代方案。Spark 驱动器还始终与 executor 保持通信。驱动器在机器或集群的主节点上运行。当 Spark 应用程序开始运行时,驱动器会跟踪所有成功运行应用程序所需的信息。

图 3.1所示,驱动器节点包含SparkSession,这是 Spark 应用程序的入口点。以前,这被称为SparkContext对象,但在 Spark 2.0 中,SparkSession处理所有上下文以启动执行。应用程序的主方法在驱动器上运行以协调整个应用程序。它在自己的Java 虚拟机JVM)上运行。Spark 驱动器可以作为一个独立进程运行,也可以根据架构在工作者节点之一上运行。

Spark 驱动器负责将应用程序划分为更小的执行实体。这些实体被称为任务。你将在本章接下来的部分中了解更多关于任务的内容。Spark 驱动器还决定 executor 将处理哪些数据以及哪些任务将在哪个 executor 上运行。这些任务将在集群管理器的帮助下在 executor 节点上调度运行。由驱动器驱动的这些信息使得容错成为可能。由于驱动器拥有所有关于可用工作者数量以及每个工作者上运行的任务的信息,以及数据,以防工作者失败,因此可以将该任务重新分配到不同的集群。即使一个任务运行时间过长,如果另一个 executor 空闲,它也可以被分配到另一个 executor。在这种情况下,哪个 executor 先返回任务,哪个就占上风。Spark 驱动器还维护关于弹性分布式数据集RDD)及其分区的元数据。

设计完整的执行图是 Spark 驱动程序的责任。它确定哪些任务在哪些执行器上运行,以及数据如何在这些执行器之间分布。这是通过内部创建 RDDs 来实现的。基于这种数据分布,确定所需的操作,例如程序中定义的转换和动作。基于这些决策创建一个 DAG。Spark 驱动程序优化逻辑计划(DAG),并为 DAG 寻找最佳执行策略,除了确定特定任务执行的最优位置。这些执行是并行进行的。执行器只是遵循这些命令,而不会在其端进行任何优化。

考虑性能因素,Spark 驱动程序靠近执行器工作是最优的。这大大减少了延迟。这意味着在进程的响应时间上会有更少的延迟。这里要注意的另一点是,这也适用于数据。读取数据的执行器靠近它会有比其他情况下更好的性能。理想情况下,驱动程序和工作节点应该在同一个 局域网LAN)中运行以获得最佳性能。

Spark 驱动程序还会为执行细节创建一个 Web UI。这个 UI 在确定应用程序性能方面非常有帮助。在需要故障排除并需要在 Spark 过程中识别瓶颈的情况下,这个 UI 非常有用。

SparkSession

SparkSession 是与 Spark 交互的主要入口点。如前所述,在 Spark 的早期版本中,SparkContext 扮演着这个角色,但在 Spark 2.0 中,可以为此创建 SparkSession。Spark 驱动程序创建一个 SparkSession 对象来与集群管理器交互,并通过它获取资源分配。

在应用程序的生命周期中,SparkSession 也用于与所有底层 Spark API 交互。我们曾在 第二章 中讨论了不同的 Spark API,即 SparkSQL、Spark Streaming、MLlib 和 GraphX。所有这些 API 都从其核心使用 SparkSession 来与 Spark 应用程序交互。

SparkSession 会跟踪整个应用程序执行过程中的 Spark 执行器。

集群管理器

Spark 是一个分布式框架,它需要访问计算资源。这种访问由称为集群管理器的过程进行管理和控制。当应用程序开始执行时,集群管理器的责任是为 Spark 应用程序分配计算资源。这些资源在应用程序主节点的请求下变得可用。在 Apache Spark 生态系统中,应用程序主节点在管理和协调分布式集群环境中 Spark 应用程序的执行中起着至关重要的作用。它是一个基本组件,负责协商资源、调度任务和监控应用程序的执行。

一旦资源可用,驱动程序就会知道这些资源。根据 Spark 应用程序需要执行的任务,管理这些资源是驱动程序的责任。一旦应用程序完成执行,这些资源就会释放回集群管理器。

应用程序有自己的专用执行程序进程,这些进程并行运行任务。其优势是每个应用程序都是独立的,并且按照自己的时间表运行。数据对于这些应用程序中的每一个也是独立的,因此数据共享只能通过将数据写入磁盘来实现,以便可以在应用程序之间共享。

集群模式

集群模式定义了 Spark 应用程序如何利用集群资源、管理任务执行以及与集群管理器进行资源分配的交互。

如果集群上有多个用户共享资源,无论是 Spark 应用程序还是需要集群资源的其他应用程序,它们必须根据不同的模式进行管理。集群管理器提供了两种模式类型——独立客户端模式和集群模式。以下表格突出了这两种模式之间的一些差异:

客户端模式 集群模式
在客户端模式中,驱动程序程序在提交 Spark 应用程序的机器上运行。 在集群模式中,驱动程序程序在集群内部运行,在工作节点之一上。
驱动程序程序负责协调 Spark 应用程序的执行,包括创建 SparkContext 和协调任务。 集群管理器负责启动驱动程序程序并为执行分配资源。
客户端机器直接与集群管理器交互,请求资源并在工作节点上启动执行程序。 一旦启动驱动程序程序,它就会与集群管理器协调,请求资源并将任务分配给工作节点。
它可能不适合大规模应用程序的生产部署。 它通常用于生产部署,因为它允许更好的资源利用和可伸缩性。它还确保了容错性。

表 3.1:客户端模式与集群模式

现在,我们将讨论不同的部署模式和 Spark 中相应的管理器:

  • 内置独立模式Spark 的原生管理器):Spark 随带的一个简单集群管理器,适用于无需外部依赖的小到中等规模部署。

  • Apache YARNHadoop 的资源管理器):与 Spark 集成,YARN 使 Spark 应用程序能够高效地共享 Hadoop 的集群资源。

  • Apache Mesos资源共享平台):Mesos 提供了跨多个应用程序的高效资源共享,允许 Spark 与其他框架并行运行。

我们将在本章后面更详细地讨论部署模式。

Spark 执行器

Spark 执行器是在工作节点上运行的进程,执行由驱动程序发送的任务。数据主要存储在内存中,但也可以写入它们最近的磁盘存储。驱动程序根据 Spark 为其执行生成的 DAG 启动执行器。一旦任务执行完成,执行器将结果发送回驱动程序。

由于驱动程序是 Spark 应用的主要控制器,如果执行器失败或执行任务花费时间过长,驱动程序可以选择将任务发送到其他可用的执行器。这确保了 Spark 的可靠性和容错性。我们将在本章后面了解更多关于这一点。

执行器的责任是从外部读取运行任务所需的数据。它还可以根据需要将其分区数据写入磁盘。一个任务的所有处理都由执行器完成。

执行器的主要功能如下:

  • 任务执行:执行器运行 Spark 应用程序分配的任务,处理存储在 RDD 或 DataFrame 中的数据

  • 资源分配:每个 Spark 应用程序都有一组由集群管理器分配的执行器,用于管理资源,如 CPU 内核和内存

在 Apache Spark 中,作业、阶段和任务的概念构成了其分布式计算框架的基本构建块。理解这些组件对于掌握 Spark 并行处理和任务执行的核心工作至关重要。见图 3.2* 了解这些概念之间的关系,同时我们详细讨论它们:

图 3.2:作业、阶段和任务之间的交互

图 3.2:作业、阶段和任务之间的交互

让我们更仔细地看看:

  • collect)。我们将在后面了解更多关于动作的内容。当在数据集上调用动作(如 collectcount)时,它将触发一个或多个作业的执行。

    一个作业由几个阶段组成,每个阶段包含执行数据分区上一系列转换的任务。

  • 阶段:每个作业被划分为可能依赖于其他阶段的阶段。阶段充当转换边界 - 它们在需要跨分区进行数据洗牌的宽转换的边界处创建。如果一个阶段依赖于前一个阶段的输出,那么这个阶段将不会开始执行,直到依赖的前一个阶段完成执行。

    每个阶段被划分为一组在集群节点上执行的任务,以并行方式处理数据。

  • 任务:在 Spark 中,任务是最小的执行单元。它是 Spark 编译和运行以执行一组操作的最小对象。它在 Spark 执行器上执行。任务本质上是一系列操作,如过滤、groupBy 等。

    任务在执行器上并行运行。它们可以在多个节点上运行,并且彼此独立。这是通过槽位来实现的。每个任务处理数据分区的一部分。偶尔,一组任务需要完成执行才能开始下一个任务的执行。

现在我们已经理解了这些概念,让我们看看为什么它们在 Spark 中很重要:

  • 并行处理:执行器、作业、阶段和任务协作以实现计算的并行执行,通过利用分布式计算优化性能。

  • 任务粒度和效率:任务将计算分解成更小的单元,便于在集群节点间实现高效的资源利用和并行处理。

接下来,我们将讨论一个增强计算效率的重要概念。

Spark 中的分区

在 Apache Spark 中,分区是一个关键概念,用于在集群的多个节点上划分数据以实现并行处理。分区提高了数据局部性,增强了性能,并通过以结构化的方式分配数据来实现高效的计算。Spark 支持静态和动态分区策略来组织集群节点上的数据:

  • 资源的静态分区:静态分区在所有集群管理器上可用。使用静态分区时,每个应用程序都分配了最大资源,并且这些资源在其生命周期内保持专用。

  • 动态资源共享:动态分区仅在 Mesos 上可用。当动态共享资源时,Spark 应用程序获得固定的独立内存分配,类似于静态分区。主要区别在于,当任务没有被应用程序运行时,这些核心也可以被其他应用程序使用。

让我们讨论一下为什么分区很重要:

  • 性能优化:有效的分区策略,无论是静态还是动态,都能通过提高数据局部性和减少数据洗牌来显著影响 Spark 的性能。

  • 适应性和灵活性:动态分区能够在无需人工干预的情况下适应变化的数据大小或分布模式。

  • 控制和可预测性:静态分区提供了对数据分布的控制和可预测性,这在某些用例中可能是有利的。

总结来说,在 Spark 中,无论是静态还是动态的分区策略,在优化跨集群节点数据分布、提高性能和确保数据高效并行处理方面都发挥着至关重要的作用。

Apache Spark 提供了不同的集群和部署模式,以在分布式计算环境中运行应用程序。我们将在下一节中探讨它们。

部署模式

Spark 中有几种不同的部署模式。这些部署模式定义了 Spark 应用程序如何在不同的计算基础设施中启动、执行和管理。基于这些不同的部署模式,将决定 Spark 驱动程序、执行器和集群管理器将在哪里运行。

Spark 中可用的不同部署模式如下:

  • 本地:在本地模式下,Spark 驱动程序和执行器在单个 JVM 上运行,集群管理器在驱动程序和执行器相同的宿主机上运行。

  • 独立:在独立模式下,驱动程序可以在集群的任何节点上运行,执行器将启动自己的独立 JVM。集群管理器可以保留在集群中的任何主机上。

  • YARN (客户端):在此模式下,Spark 驱动程序在客户端运行,YARN 的资源管理器在 NodeManagers 上为执行器分配容器。

  • YARN (集群):在此模式下,Spark 驱动程序与 YARN 应用程序主控一起运行,而 YARN 的资源管理器在 NodeManagers 上为执行器分配容器。

  • Kubernetes:在此模式下,驱动程序在 Kubernetes 容器中运行。执行器有自己的容器。

让我们看看关于不同部署模式的一些重要点:

  • 资源利用率:不同的部署模式通过确定驱动程序程序运行的位置以及客户端和集群之间如何分配资源来优化资源利用率。

  • 可访问性和控制:客户端模式提供对驱动程序日志和输出的轻松访问,便于开发和调试,而集群模式则更有效地利用集群资源来处理生产工作负载。

  • 与容器编排集成:Kubernetes 部署模式允许与容器化环境无缝集成,利用 Kubernetes 的编排能力进行高效的资源管理。

选择部署模式时有一些考虑事项需要注意:

  • 开发和生产:客户端模式适合开发和调试,而集群模式则适用于生产工作负载。

  • 资源管理:根据应用程序的需求评估客户端和集群节点之间资源的分配

  • 容器化需求:考虑在容器化环境中使用 Kubernetes 部署,利用 Kubernetes 的功能进行高效的容器管理

总结来说,Apache Spark 的部署模式提供了灵活性,以适应不同的开发、生产和容器化部署场景。

接下来,我们将探讨 RDD,它是 Apache Spark 中的基础数据抽象,使分布式处理、容错性和处理大规模数据操作时的灵活性成为可能。虽然 RDD 仍然是一个基本概念,但 Spark 的 DataFrame 和 Dataset API 在结构化数据处理和性能优化方面提供了进步。

RDD

Apache Spark 的 RDD 作为基础抽象,支撑了 Spark 框架内的分布式计算能力。RDD 是 Spark 中的核心数据结构,它使对大规模分布式数据集进行容错性和并行操作成为可能,并且它们是不可变的。这意味着它们不能随时间改变。对于任何操作,都必须从现有的 RDD 生成一个新的 RDD。当一个新 RDD 从原始 RDD 起源时,新 RDD 有一个指向其起源的 RDD 的指针。这是 Spark 记录 RDD 上所有变换谱系的方式。这种谱系使得 Spark 中的延迟评估成为可能,它为不同的操作生成 DAGs。

这种不可变性和谱系赋予了 Spark 在失败情况下重新生成任何 DataFrame 的能力,并使其设计上具有容错性。由于 RDD 是 Spark 中抽象层次最低的,因此所有建立在 RDD 之上的其他数据集都共享这些属性。高级 DataFrame API 也是建立在低级 RDD API 之上的,因此 DataFrame 也共享相同的属性。

RDD 也被 Spark 分区,并且每个分区被分配到集群中的多个节点。

这里是 Spark RDD 的一些关键特性:

  • 不可变性质:RDD 是不可变的,确保一旦创建,就不能被修改,从而允许有变换的谱系。

  • 通过谱系实现弹性:RDD 存储谱系信息,在发生故障时能够重建丢失的分区。Spark 被设计成具有容错性。因此,如果一个工作节点上的执行器在计算 RDD 时失败,可以使用 Spark 创建的谱系由另一个执行器重新计算该 RDD。

  • 分区数据:RDD 将数据划分为分区,这些分区分布在集群中的多个节点上以实现并行处理。

  • 并行执行:Spark 在分布式分区上并行执行 RDD 上的操作,从而提高性能。

让我们详细讨论一些其他特性。

懒计算

RDDs 支持惰性评估,将转换的执行延迟到动作被调用。Spark 在处理和容错方面的效率是通过惰性评估实现的。Spark 中的代码执行是延迟的。除非调用一个动作操作,否则 Spark 不会开始代码执行。这有助于 Spark 实现优化。对于所有转换和动作,Spark 通过为这些操作创建一个 DAG 来跟踪代码中需要执行的步骤。因为 Spark 在执行之前创建查询计划,所以它可以就执行层次结构做出明智的决策。为了实现这一点,Spark 使用的一个功能称为谓词下推

谓词下推意味着 Spark 可以优先执行操作,使其最有效。一个例子可以是一个过滤操作。如果过滤操作可以在其他转换之前应用,那么过滤操作通常会减少后续操作需要处理的数据量。这正是 Spark 的操作方式。它会在流程中尽可能早地执行过滤操作,从而使后续操作更高效。

这也意味着 Spark 作业只有在执行时才会失败。由于 Spark 使用惰性评估,直到调用动作之前,代码不会执行,某些错误可能会被忽略。为了捕获这些错误,Spark 代码需要有一个动作用于执行和错误处理。

转换

转换通过将函数应用于现有的 RDDs(例如,mapfilterreduce)来创建新的 RDDs。转换是那些不会导致任何代码执行的操作。这些语句会导致 Spark 为执行创建一个 DAG。一旦创建了 DAG,Spark 就需要在最后运行代码时需要一个动作操作。由于这个原因,当某些开发者尝试测量 Spark 中的代码时间时,他们会看到某些操作的运行时间非常快。可能的原因是,代码直到那个点只包含转换。由于没有动作,代码不会运行。为了准确测量每个操作的运行时间,必须调用动作来强制 Spark 执行这些语句。

以下是一些可以被归类为转换的操作:

  • orderBy()

  • groupBy()

  • filter()

  • select()

  • join()

当这些命令执行时,它们是惰性评估的。这意味着所有这些对 DataFrames 的操作都会产生一个新的 DataFrame,但它们不会执行,直到一个动作跟随它们。当触发动作时,这将返回一个 DataFrame 或 RDD。

动作和计算执行

行动(例如,collectcountsaveAsTextFile)会触发 RDD 上的变换执行。执行仅由行动触发,而不是由变换触发。当调用行动时,这是 Spark 开始在代码分析阶段创建的 DAG 上执行的时候。有了创建的 DAG,Spark 会根据其内部优化创建多个查询计划。然后,它执行最有效和成本效益最高的计划。我们将在本书的后面讨论查询计划。

这里有一些可以归类为行动的操作:

  • show()

  • take()

  • count()

  • collect()

  • save()

  • foreach()

  • first()

所有这些操作都会导致 Spark 触发代码执行,因此操作会运行。

让我们看一下以下代码,以更好地理解这些概念:

# Python
>>> df = spark.read.text("{path_to_data_file}")
>>> names_df = df.select(col("firstname"),col("lastname"))
>>> names_df.show()

在前面的代码中,直到第 2 行,什么都不会执行。在第 3 行,触发了一个行动,因此触发了整个代码的执行。因此,如果你在第 1 行提供了错误的数据路径或在第 2 行提供了错误的列名,Spark 不会在执行到第 3 行之前检测到这一点。这与大多数其他编程范式不同。这就是我们所说的 Spark 中的懒加载。

行动会导致计算并收集结果以发送给驱动程序。

既然我们已经了解了 Spark 中变换和行动的基础知识,让我们继续了解它提供的两种变换类型。

变换类型

Apache Spark 的变换大致分为窄变换和宽变换,每种变换在分布式数据处理环境中都服务于不同的目的。

窄变换

窄变换,也称为本地变换,在数据单个分区上操作,不涉及在分区之间洗牌或重新分配数据。这些变换使 Spark 能够独立地在单个分区内处理数据。在窄变换中,Spark 将与单个输入分区和单个输出分区一起工作。这意味着这些类型的变换会导致可以在单个分区上执行的操作。数据不必从多个分区中取出或写回到多个分区。这导致不需要洗牌的操作。

这里是它们的一些特征:

  • 分区级操作:窄变换在分区级别处理数据,在每个分区内执行计算。

  • 独立性和本地处理:它们不需要数据在分区之间移动或通信,允许在分区内并行执行。

  • mapfilterflatMap 是窄变换的典型例子。

现在,让我们看看它们的重要性:

  • 效率和速度:窄变换是高效的,因为它们涉及分区内的本地处理,减少了通信开销。

  • 并行性:它们通过独立地对分区进行操作,实现最大并行性,从而优化性能

宽转换

宽转换,也称为全局或洗牌相关转换,涉及需要跨分区进行数据洗牌和重新分配的操作。这些转换涉及分区之间的依赖关系,需要数据交换。使用宽转换时,Spark 将使用多个分区上的数据,并且也可能将结果写回到多个分区。这些转换将强制进行洗牌操作,因此它们也被称为洗牌转换。

宽转换是复杂的操作。如果需要,它们需要在操作之间写入结果,并且在某些情况下还必须跨不同机器聚合数据。

这里是一些它们的特征:

  • 数据洗牌:宽转换通过重新排列或聚合来自多个分区的数据来跨分区重新组织数据

  • 对多个分区的依赖性:它们依赖于来自各个分区的数据,导致在集群中跨分区进行数据交换和重组

  • groupByjoinsortByKey是宽转换的典型例子

现在,让我们看看它们的显著性:

  • 网络和磁盘开销:宽转换由于数据洗牌而引入网络和磁盘开销,影响性能

  • 阶段边界创建:它们在 Spark 作业中定义阶段边界,导致在作业执行期间出现不同的阶段

以下是比较窄转换和宽转换之间的差异:

  • 数据移动:窄转换在分区内部本地处理数据,最小化数据移动,而宽转换涉及数据洗牌和跨分区的数据移动

  • 性能影响:窄转换通常由于数据移动减少而提供更高的性能,而宽转换由于数据洗牌而涉及额外的开销

  • 并行性范围:窄转换在分区内实现最大并行性,而宽转换可能会由于对多个分区的依赖而限制并行性

在 Apache Spark 中,理解窄转换和宽转换之间的区别至关重要。窄转换在分区内的本地处理中表现出色,优化性能,而宽转换,尽管对于某些操作是必要的,但由于数据洗牌和跨分区的全局重组而引入了开销。

让我们看看 Spark RDDs 的重要性:

  • 分布式数据处理:RDDs 允许在机器集群上分布式处理大规模数据,促进并行性和可伸缩性

  • 容错性和可靠性:它们的不可变性和基于血缘的恢复确保了在分布式环境中的容错性和可靠性

  • 操作的灵活性:RDDs 支持广泛的转换和操作,允许多样化的数据处理和操作。

进化与替代

虽然 RDDs 仍然是基础性的,但 Spark 的 DataFrame 和 Dataset API 提供了优化、高级别的抽象,适合结构化数据处理和优化。

Spark RDDs 是 Apache Spark 框架内分布式数据处理的基础,提供不可变性、容错性和在分布式数据集上执行并行操作的基础结构。尽管 RDDs 是基础性的,但 Spark 的 DataFrame 和 Dataset API 提供了性能和结构化数据处理方面的进步,满足 Spark 生态系统中的各种用例和偏好。

摘要

在本章中,我们学习了 Spark 的架构及其内部工作原理。这次对 Spark 分布式计算景观的探索涵盖了不同的 Spark 组件,如 Spark 驱动程序和 SparkSession。我们还讨论了 Spark 中可用的不同类型的集群管理器。然后,我们简要介绍了 Spark 及其部署模式的不同分区类型。

接下来,我们讨论了 Spark 执行器、作业、阶段和任务,在学习 RDDs 及其转换类型之前,强调了它们之间的区别,并更多地了解了窄转换和宽转换。

这些概念构成了利用 Spark 在分布式数据处理和分析中巨大能力的基础。

在下一章中,我们将讨论 Spark DataFrame 及其相应的操作。

样题

问题 1:

关于 Spark 的执行层次结构,以下哪项是正确的?

  1. 在 Spark 的执行层次结构中,一个作业可能会达到多个阶段边界。

  2. 在 Spark 的执行层次结构中,作业描述文件位于作业之上的一层。

  3. 在 Spark 的执行层次结构中,一个阶段包含多个作业。

  4. 在 Spark 的执行层次结构中,执行器是最小的单元。

  5. 在 Spark 的执行层次结构中,任务位于槽位之上的一层。

问题 2:

执行器的作用是什么?

  1. 执行器在每个工作节点上托管 Spark 驱动程序。

  2. 执行器负责执行由驱动程序分配给它们的工作。

  3. Spark 应用程序启动后,每个任务都会启动执行器。

  4. 执行器位于工作节点内的槽位中。

  5. 执行器的存储是短暂的,因此它将直接缓存数据的任务推迟到工作节点线程。

答案

  1. A

  2. B

第三部分:Spark 操作

在本部分,我们将涵盖 Spark DataFrame 及其操作,强调它们在结构化数据处理和分析中的作用。这包括 DataFrame 的创建、操作以及各种操作,如过滤、聚合、连接和分组,通过示例进行演示。然后,我们将讨论高级操作和优化技术,包括广播变量、累加器和自定义分区。本部分还将讨论性能优化策略,强调自适应查询执行的重要性,并提供提高 Spark 作业性能的实用技巧。此外,我们将探索 Spark 中的 SQL 查询,重点关注其类似 SQL 的查询能力和与 DataFrame API 的互操作性。示例将通过 Spark 中的 SQL 查询展示复杂的数据操作和分析。

本部分包含以下章节:

  • 第四章, Spark DataFrame 及其操作

  • 第五章, Spark 中的高级操作和优化

  • 第六章, Spark 中的 SQL 查询

第四章:Spark DataFrame 及其操作

在本章中,我们将学习 Spark 中的一些不同 API,并讨论它们的特性。我们还将开始学习 Spark 的 DataFrame 操作,并查看不同的数据查看和操作技术,例如过滤、添加、重命名和删除 Spark 中可用的列。

我们将在以下主题下介绍这些概念:

  • Spark DataFrame API

  • 创建 DataFrame

  • 查看 DataFrame

  • 操作 DataFrame

  • 聚合 DataFrame

到本章结束时,你将了解如何使用 PySpark DataFrame。你还将发现各种数据操作技术,并了解在操作数据后如何查看数据。

PySpark 入门

在前面的章节中,我们讨论了 Spark 主要使用四种语言,即 Scala、Python、R 和 SQL。当使用这些语言中的任何一种时,底层的执行引擎是相同的。这为我们之前在第二章中提到的必要统一性提供了支持。这意味着开发者可以使用他们选择的任何语言,也可以在应用程序中在不同 API 之间切换。

对于本书的上下文,我们将重点关注 Python 作为主要语言。与 Python 一起使用的 Spark 被称为PySpark

让我们开始安装 Spark。

安装 Spark

要开始使用 Spark,你首先需要在你的电脑上安装它。安装 Spark 有几种方法。在本节中,我们将只关注其中一种。

PySpark 提供了从PyPIpip安装。你可以按照以下方式安装它:

pip install pyspark

一旦 Spark 安装完成,你需要创建一个 Spark 会话。

创建 Spark 会话

一旦你在系统中安装了 Spark,你就可以开始创建 Spark 会话。Spark 会话是任何 Spark 应用程序的入口点。要创建 Spark 会话,你需要按照以下方式初始化它:

from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()

当你在 Spark shell 中运行代码时,Spark 会话会自动为你创建,因此你不需要手动执行此代码来创建 Spark 会话。这个会话通常在一个名为spark的变量中创建。

重要的是要注意,在任何给定时间,我们只能创建一个 Spark 会话。在 Spark 中无法复制 Spark 会话。

现在,让我们看看 Spark DataFrames 中的不同数据 API。

Dataset API

Dataset 是添加到Spark 1.6的新接口。它是一个数据分布式集合。Dataset API 在 Java 和 Scala 中可用,但在 Python 和 R 中不可用。Dataset API 使用Resilient Distributed DatasetsRDDs),因此提供了 RDD 的附加功能,如固定类型。它还使用 Spark SQL 的优化引擎以实现更快的查询。

由于许多数据工程和数据科学社区已经熟悉 Python 并在生产中的数据架构中广泛使用它,PySpark 也为此提供了等效的 DataFrame API。让我们在下一节中看看它。

DataFrame API

Spark DataFrame 的动机来自 Python 中的 Pandas DataFrame。DataFrame 实质上是一组行和列。你可以将其想象成一个表格,其中表格头作为列名,在这些头下面是相应排列的数据。这种类似表格的格式已经在诸如关系数据库和逗号分隔文件等工具的计算中存在很长时间了。

Spark 的 DataFrame API 是建立在 RDD 之上的。存储数据的底层结构仍然是 RDD,但 DataFrame 在 RDD 上创建了一个抽象,以隐藏其复杂性。正如 RDD 是惰性评估且不可变的一样,DataFrame 也是惰性评估且不可变的。如果你还记得前面的章节,惰性评估通过仅在需要时运行计算为 Spark 带来了性能提升和优化。这也使得 Spark 在其 DataFrame 中通过规划如何最佳地计算操作而拥有大量的优化。计算是在对 DataFrame 调用动作时开始的。有无数种不同的方法可以创建 Spark DataFrame。我们将在本章中学习其中的一些。

让我们看看在 Spark 中 DataFrame 是什么。

创建 DataFrame 操作

正如我们已经讨论过的,DataFrame 是 Spark 数据的主要构建块。它们由行和列数据结构组成。

PySpark 中的 DataFrame 是通过 pyspark.sql.SparkSession.createDataFrame 函数创建的。你可以使用列表、列表的列表、元组、字典、Pandas DataFrame、RDD 和 pyspark.sql.Rows 来创建 DataFrame。

Spark DataFrame 也有一个名为 schema 的参数,用于指定 DataFrame 的模式。你可以选择显式指定模式,或者让 Spark 从 DataFrame 本身推断模式。如果你在代码中没有指定此参数,Spark 将会自行推断模式。

在 Spark 中创建 DataFrame 有不同的方法。其中一些将在以下章节中解释。

使用数据行列表

我们看到创建 DataFrame 的第一种方法是通过使用数据行。你可以将数据行想象成列表。它们将为列表中的每个值共享共同的头值。

创建新的 DataFrame 时使用数据行列表的代码如下:

import pandas as pd
from datetime import datetime, date
from pyspark.sql import Row
data_df = spark.createDataFrame([
    Row(col_1=100, col_2=200., col_3='string_test_1', col_4=date(2023, 1, 1), col_5=datetime(2023, 1, 1, 12, 0)),
    Row(col_1=200, col_2=300., col_3='string_test_2', col_4=date(2023, 2, 1), col_5=datetime(2023, 1, 2, 12, 0)),
    Row(col_1=400, col_2=500., col_3='string_test_3', col_4=date(2023, 3, 1), col_5=datetime(2023, 1, 3, 12, 0))
])

因此,你会看到具有我们指定的列及其数据类型的 DataFrame:

DataFrame[col_1: bigint, col_2: double, col_3: string, col_4: date, col_5: timestamp]

现在,我们将看到如何可以显式指定 Spark DataFrame 的模式。

使用具有模式的行列表

DataFrame 的模式定义了 DataFrame 的每一行和每一列中可能存在的不同数据类型。显式定义模式有助于我们想要强制某些数据类型到数据集的情况。

现在,我们将明确告诉 Spark 我们创建的 DataFrame 应使用哪种模式。请注意,大部分代码保持不变——我们只是在创建 DataFrame 的代码中添加了一个名为schema的另一个参数,以明确指定哪些列将有什么样的数据类型:

import pandas as pd
from datetime import datetime, date
from pyspark.sql import Row
data_df = spark.createDataFrame([
    Row(col_1=100, col_2=200., col_3='string_test_1', col_4=date(2023, 1, 1), col_5=datetime(2023, 1, 1, 12, 0)),
    Row(col_1=200, col_2=300., col_3='string_test_2', col_4=date(2023, 2, 1), col_5=datetime(2023, 1, 2, 12, 0)),
    Row(col_1=400, col_2=500., col_3='string_test_3', col_4=date(2023, 3, 1), col_5=datetime(2023, 1, 3, 12, 0))
], schema=' col_1 long, col_2 double, col_3 string, col_4 date, col_5 timestamp')

因此,您将看到包含我们指定列及其数据类型的 DataFrame:

data_df
DataFrame[col_1: bigint, col_2: double, col_3: string, col_4: date, col_5: timestamp]

现在,让我们看看如何使用 Pandas DataFrame 创建 DataFrame。

使用 Pandas DataFrame

DataFrame 也可以使用 Pandas DataFrame 创建。为此,您首先需要在 Pandas 中创建一个 DataFrame。一旦创建,然后将其转换为 PySpark DataFrame。以下代码演示了此过程:

from datetime import datetime, date
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
rdd = spark.sparkContext.parallelize([
    (100, 200., 'string_test_1', date(2023, 1, 1), datetime(2023, 1, 1, 12, 0)),
    (200, 300., 'string_test_2', date(2023, 2, 1), datetime(2023, 1, 2, 12, 0)),
    (300, 400., 'string_test_3', date(2023, 3, 1), datetime(2023, 1, 3, 12, 0))
])
data_df = spark.createDataFrame(rdd, schema=['col_1', 'col_2', 'col_3', 'col_4', 'col_5'])

因此,您将看到包含我们指定列及其数据类型的 DataFrame:

DataFrame[col_1: bigint, col_2: double, col_3: string, col_4: date, col_5: timestamp]

现在,让我们看看如何使用元组创建 DataFrame。

使用元组

创建 DataFrame 的另一种方式是通过元组。这意味着我们可以创建一个元组作为一行,并将每个元组作为 DataFrame 中的单独一行添加。每个元组将包含 DataFrame 中每列的数据。以下代码演示了这一点:

import pandas as pd
from datetime import datetime, date
from pyspark.sql import Row
rdd = spark.sparkContext.parallelize([
    (100, 200., 'string_test_1', date(2023, 1, 1), datetime(2023, 1, 1, 12, 0)),
    (200, 300., 'string_test_2', date(2023, 2, 1), datetime(2023, 1, 2, 12, 0)),
    (300, 400., 'string_test_3', date(2023, 3, 1), datetime(2023, 1, 3, 12, 0))
])
data_df = spark.createDataFrame(rdd, schema=['col_1', 'col_2', 'col_3', 'col_4', 'col_5'])

因此,您将看到包含我们指定列及其数据类型的 DataFrame:

DataFrame[col_1: bigint, col_2: double, col_3: string, col_4: date, col_5: timestamp]

现在,让我们看看在 Spark 中我们可以以不同的方式查看 DataFrame,并查看我们刚刚创建的 DataFrame 的结果。

如何查看 DataFrame

Spark 中有不同的语句用于查看数据。我们在上一节中通过不同方法创建的 DataFrame,其结果都与 DataFrame 相同。让我们看看几种不同的查看 DataFrame 的方法。

查看 DataFrame

显示 DataFrame 的第一种方式是通过DataFrame.show()语句。以下是一个示例:

data_df.show()

因此,您将看到包含我们指定列及其数据类型的 DataFrame:

+-----+-----+-------------+----------+-------------------+
|col_1|col_2|   col_3     |  col_4   |       col_5       |
+-----+-----+-------------+----------+-------------------+
|  100|200.0|string_test_1|2023-01-01|2023-01-01 12:00:00|
|  200|300.0|string_test_2|2023-02-01|2023-01-02 12:00:00|
|  300|400.0|string_test_3|2023-03-01|2023-01-03 12:00:00|
+-----+-----+-------------+----------+-------------------+

我们还可以在单个语句中选择要查看的总行数。让我们在下一个主题中看看如何做到这一点。

查看前 n 行

我们也可以在单个语句中指定要查看的行数。我们可以通过DataFrame.show()中的参数来控制这一点。以下是一个仅查看 DataFrame 前两行的示例。

如果您指定n为特定的数字,则只会显示那些行集。以下是一个示例:

data_df.show(2)

因此,您将看到包含其前两行的 DataFrame:

+-----+-----+-------------+----------+-------------------+
|col_1|col_2|    col_3    |   col_4  |        col_5      |
+------+------+-----------+----------+-------------------+
|  100|200.0|string_test_1|2023-01-01|2023-01-01 12:00:00|
|  200|300.0|string_test_2|2023-02-01|2023-01-02 12:00:00|
------+-----+-------------+----------+-------------------+
only showing top 2 rows.

查看 DataFrame 模式

我们还可以选择使用printSchema()函数查看 DataFrame 的模式:

data_df.printSchema()

因此,您将看到 DataFrame 的模式,包括我们指定的列及其数据类型:

root
 |-- col_1: long (nullable = true)
 |-- col_2: double (nullable = true)
 |-- col_3: string (nullable = true)
 |-- col_4: date (nullable = true)
 |-- col_5: timestamp (nullable = true)

垂直查看数据

当数据太长而无法适应屏幕时,以垂直格式查看数据而不是水平表格视图有时很有用。以下是一个示例,说明您如何以垂直格式查看数据:

data_df.show(1, vertical=True)

因此,您将看到包含我们指定列及其数据但以垂直格式的 DataFrame:

-RECORD 0------------------
 col_1   | 100
 col_2   | 200.0
 col_3   | string_test_1
 col_4   | 2023-01-01
 col_5   | 2023-01-01 12:00:00
only showing top 1 row

查看数据列

当我们只需要查看 DataFrame 中存在的列时,我们会使用以下方法:

data_df.columns

因此,您将看到一个 DataFrame 中列的列表:

['col_1', 'col_2', 'col_3', 'col_4', 'col_5']

查看摘要统计信息

现在,让我们看看我们如何查看 DataFrame 的摘要统计信息:

Show the summary of the DataFrame
data_df.select('col_1', 'col_2', 'col_3').describe().show()

因此,您将看到一个 DataFrame,其中定义了每列的摘要统计信息:

+-------+-------+-------+-------------+
|summary| col_1 | col_2 |    col_3    |
+-------+-------+-------+-------------+
|  count|   3   |   3   |            3|
|   mean| 200.0 | 300.0 |         null|
| stddev| 100.0 | 100.0 |         null|
|    min| 100   | 200.0 |string_test_1|
|    max| 300   | 400.0 |string_test_3|
+-------+-------+-------+-------------+

现在,让我们看一下 collect 语句。

收集数据

当我们想要将不同集群中正在处理的所有数据收集回驱动器时,会使用 collect 语句。在使用 collect 语句时,我们需要确保驱动器有足够的内存来存储处理后的数据。如果驱动器没有足够的内存来存储数据,我们将遇到内存不足错误。

这就是显示 collect 语句的方法:

data_df.collect()

此语句将显示如下结果:

[Row(col_1=100, col_2=200.0, col_3='string_test_1', col_4=datetime.date(2023, 1, 1), col_5=datetime.datetime(2023, 1, 1, 12, 0)),
 Row(col_1=200, col_2=300.0, col_3='string_test_2', col_4=datetime.date(2023, 2, 1), col_5=datetime.datetime(2023, 1, 2, 12, 0)),
 Row(col_1=300, col_2=400.0, col_3='string_test_3', col_4=datetime.date(2023, 3, 1), col_5=datetime.datetime(2023, 1, 3, 12, 0))]

有几种方法可以避免内存不足错误。我们将探讨一些避免内存不足错误的方法,例如 take、tail 和 head 语句。这些语句仅返回数据的一个子集,而不是 DataFrame 中的所有数据,因此,它们在无需将所有数据加载到驱动器内存中时非常有用。

现在,让我们看一下 take 语句。

使用 take

take 语句接受一个参数,用于从 DataFrame 顶部返回元素的数量。我们将在下面的代码示例中看到它是如何使用的:

data_df.take(1)

因此,您将看到一个包含其顶部行的 DataFrame:

[Row(col_1=100, col_2=200.0, col_3='string_test_1', col_4=datetime.date(2023, 1, 1), col_5=datetime.datetime(2023, 1, 1, 12, 0))]

在这个例子中,我们通过将1作为take()函数的参数值来仅返回 DataFrame 的第一个元素。因此,结果中只返回了一行。

现在,让我们看一下 tail 语句。

使用 tail

tail 语句接受一个参数,用于从 DataFrame 底部返回元素的数量。我们将在下面的代码示例中看到它是如何使用的:

data_df.tail(1)

因此,您将看到一个包含其最后一行数据的 DataFrame:

[Row(col_1=300, col_2=400.0, col_3='string_test_3', col_4=datetime.date(2023, 3, 1), col_5=datetime.datetime(2023, 1, 3, 12, 0))]

在这个例子中,我们通过将1作为tail()函数的参数值来仅返回 DataFrame 的最后一个元素。因此,结果中只返回了一行。

现在,让我们看一下 head 语句。

使用 head

head 语句接受一个参数,用于从 DataFrame 顶部返回元素的数量。我们将在下面的代码示例中看到它是如何使用的:

data_df.head(1)

因此,您将看到一个包含其数据顶部行的 DataFrame:

[Row(col_1=100, col_2=200.0, col_3='string_test_1', col_4=datetime.date(2023, 1, 1), col_5=datetime.datetime(2023, 1, 1, 12, 0))]

在这个例子中,我们通过将1作为head()函数的参数值来仅返回 DataFrame 的第一个元素。因此,结果中只返回了一行。

现在,让我们看看我们如何计算 DataFrame 中的行数。

计算数据行数

当我们只需要计算 DataFrame 中的行数时,我们会使用以下方法:

data_df.count()

因此,您将看到一个 DataFrame 中的总行数:

3

在 PySpark 中,有几种方法可以用于从 DataFrame 或 RDD 中检索数据,每种方法都有其自身的特性和用例。以下是我们在本节早期用于数据检索的 takecollectshowheadtail 之间主要差异的总结。

take(n)

此函数返回一个包含 DataFrame 或 RDD 的前 n 个元素的数组

  • 它用于快速检查数据的小子集

  • 它执行惰性评估,这意味着它只计算所需数量的元素

collect()

此函数从 DataFrame 或 RDD 中检索所有元素,并将它们作为列表返回

  • 应谨慎使用,因为它会将所有数据带到驱动节点,这可能导致大型数据集出现内存不足错误

  • 它适用于小型数据集或处理适合内存的聚合结果时

show(n)

此函数以表格格式显示 DataFrame 的前 n

  • 它主要用于在 探索性数据分析EDA) 或调试期间对数据进行视觉检查

  • 它以具有列标题和格式的用户友好的方式显示数据

head(n)

此函数返回 DataFrame 的前 n 行,作为 Row 对象的列表

  • 它与 take(n) 类似,但返回 Row 对象而不是简单值

  • 当处理结构化数据时,需要访问特定列值时经常使用

tail(n)

此函数返回 DataFrame 的最后 n

  • 它在检查数据集的末尾时很有用,尤其是在数据按降序排序的情况下

  • head(n) 相比,它执行的操作更昂贵,因为它可能涉及扫描整个数据集

总结来说,takecollect 用于检索数据元素,其中 take 更适合小子集,而 collect 用于检索所有数据(需谨慎)。show 用于视觉检查,head 检索前几行作为 Row 对象,而 tail 检索数据集的最后几行。每种方法都有不同的用途,应根据数据分析任务的具体要求进行选择。

当在 PySpark 中处理数据时,有时您需要在 DataFrames 上使用一些 Python 函数。为了实现这一点,您必须将 PySpark DataFrames 转换为 Pandas DataFrames。现在,让我们看看如何将 PySpark DataFrame 转换为 Pandas DataFrame。

将 PySpark DataFrame 转换为 Pandas DataFrame

在您的工作流程的各个阶段,您可能希望从 PySpark DataFrame 切换到 Pandas DataFrame。有选项可以将 PySpark DataFrame 转换为 Pandas DataFrame。此选项是 toPandas()

这里需要注意的是,Python 本身不是分布式的。因此,当 PySpark DataFrame 转换为 Pandas 时,驱动程序需要收集其内存中的所有数据。我们需要确保驱动程序的内存能够收集自身的数据。如果数据无法适应驱动程序的内存,将导致内存不足错误。

以下是一个示例,说明如何将 PySpark DataFrame 转换为 Pandas DataFrame:

data_df.toPandas()

因此,您将看到具有我们指定的列及其数据类型的 DataFrame:

col_1 col_2 col_3 col_4 col_5
0 100 200.0 String_test_1 2023-01-01 2023-01-01 12:00:00
1 200 300.0 String_test_2 2023-02-01 2023-01-02 12:00:00
2 300 400.0 String_test_3 2023-03-01 2023-01-03 12:00:00

表 4.1:我们指定的列和数据类型的 DataFrame

在下一节中,我们将了解不同的数据操作技术。您将需要根据不同的目的根据不同的标准对数据进行筛选、切片和切块。因此,数据操作在处理数据时是必不可少的。

如何在行和列上操作数据

在本节中,我们将学习如何在 Spark DataFrame 的行和列上执行不同的数据操作。

我们将首先看看如何在 Spark DataFrame 中选择列。

选择列

我们可以在 Spark DataFrame 中使用列函数在列级别进行数据操作。要选择 DataFrame 中的列,我们将使用 select() 函数如下:

from pyspark.sql import Column
data_df.select(data_df.col_3).show()

因此,您将只看到 DataFrame 中的一个列及其数据:

+-------------+
|    col_3    |
+-------------+
|string_test_1|
|string_test_2|
|string_test_3|
+-------------+
The important thing to note here is that the resulting DataFrame with one column is a new DataFrame. Recalling what we discussed in *Chapter 3*, RDDs are immutable. The underlying data structure for DataFrames is RDDs, therefore, DataFrames are also immutable. This means every time you make a change to a DataFrame, a new DataFrame would be created out of it. You would either have to assign the resultant DataFrame to a new DataFrame or overwrite the original DataFrame.

在 PySpark 中也有其他一些方法可以达到相同的结果。其中一些在这里进行了演示:

data_df.select('col_3').show()
data_df.select(data_df['col_3']).show()

一旦我们选择了所需的列,就可能出现需要向 DataFrame 中添加新列的情况。现在我们将看看如何在 Spark DataFrame 中创建列。

创建列

我们可以使用 withColumn() 函数在 DataFrame 中创建新列。要创建新列,我们需要传递列名和列值以填充该列。在以下示例中,我们创建了一个名为 col_6 的新列,并在该列中放置了一个常量字面量 A

from pyspark.sql import functions as F
data_df = data_df.withColumn("col_6", F.lit("A"))
data_df.show()

因此,您将看到具有一个名为 col_6 的附加列,该列填充了多个 A 实例:

图片

lit() 函数用于在列中填充常量值。

您还可以删除 DataFrame 中不再需要的列。现在我们将看看如何在 Spark DataFrame 中删除列。

删除列

如果我们需要从 Spark DataFrame 中删除列,我们将使用 drop() 函数。我们需要提供要删除的列的名称。以下是如何使用此函数的示例:

data_df = data_df.drop("col_5")
data_df.show()

因此,您将看到 DataFrame 中已删除 col_5

+-----+-----+-------------+----------+------+
|col_1|col_2|    col_3    |  col_4   | col_6|
+-----+-----+-------------+----------+------+
|  100|200.0|string_test_1|2023-01-01|     A|
|  200|300.0|string_test_2|2023-02-01|     A|
|  300|400.0|string_test_3|2023-03-01|     A|
+-----+-----+-------------+----------+------+

我们已成功从该 DataFrame 中删除了 col_5

你也可以在同一个删除语句中删除多个列:

data_df = data_df.drop("col_4", "col_5")

还要注意,如果我们删除 DataFrame 中不存在的列,不会产生任何错误。结果 DataFrame 将保持原样。

就像删除列一样,你还可以在 Spark DataFrame 中更新列。现在,我们将看看如何在 Spark DataFrame 中更新列。

更新列

更新列也可以通过 Spark 中的 withColumn() 函数来完成。我们需要提供要更新的列的名称以及更新的值。注意,我们还可以使用此函数为列计算一些新值。以下是一个示例:

data_df.withColumn("col_2", F.col("col_2") / 100).show()

这将给我们以下更新的框架:

+-----+-----+-------------+----------+------+
|col_1|col_2|    col_3    |   col_4  | col_6|
+-----+-----+-------------+----------+------+
|  100|200.0|string_test_1|2023-01-01|     A|
|  200|300.0|string_test_2|2023-02-01|     A|
|  300|400.0|string_test_3|2023-03-01|     A|
+-----+-----+-------------+----------+------+

这里需要注意的是,在更新列时使用 col 函数。此函数用于列操作。如果我们不使用此函数,我们的代码将返回错误。

如果你只需要重命名列,而不需要更新 DataFrame 中的列,你不必总是更新列。现在,我们将看看如何在 Spark DataFrame 中重命名列。

重命名列

为了更改列名,我们会在 Spark 中使用 withColumnRenamed() 函数。我们需要提供需要更改的列名以及新的列名。以下是说明这一点的代码示例:

data_df = data_df.withColumnRenamed("col_3", "string_col")
data_df.show()

因此,我们将看到以下变化:

+-----+-----+-------------+----------+------+
|col_1|col_2|  string_col |   col_4  | col_6|
+-----+-----+-------------+----------+------+
|  100|200.0|string_test_1|2023-01-01|   A  |
|  200|300.0|string_test_2|2023-02-01|   A  |
|  300|400.0|string_test_3|2023-03-01|   A  |
+-----+-----+-------------+----------+------+

注意,更改后 col_3 现在被称为 string_col

现在,让我们将注意力转向一些 Spark DataFrame 中的数据处理技术。你可以在 Spark DataFrame 中拥有类似搜索的功能,用于查找列中的不同值。现在,让我们看看如何在 Spark DataFrame 的列中查找唯一值。

在列中查找唯一值

查找唯一值是一个非常实用的函数,它将给我们列中的不同值。为此,我们可以使用 Spark DataFrame 的 distinct() 函数,如下所示:

data_df.select("col_6").distinct().show()

这里是结果:

+------+
|col_6 |
+------+
|   A  |
+------+

我们在 col_6 上应用了 distinct 函数,以获取该列中所有唯一的值。在我们的例子中,该列只有一个不同的值,即 A,所以显示了它。

我们还可以使用它来查找给定列中不同值的数量。以下是如何使用此函数的示例:

data_df.select(F.countDistinct("col_6").alias("Total_Unique")).show()

这里是结果:

+------+
|col_6 |
+------+
|  1   |
+------+

在这个例子中,我们可以看到 col_6 中不同值的总数。目前,它是这个列中唯一的不同值类型,因此返回了 1

Spark 数据操作中另一个有用的函数是更改列的大小写。现在,让我们看看如何在 Spark DataFrame 中更改列的大小写。

更改列的大小写

Spark 中也存在一个用于更改列大小写的函数。我们不需要分别指定列的每个值来使用此函数。一旦应用,整个列的值都会更改大小写。以下是一个这样的例子:

from pyspark.sql.functions import upper
data_df.withColumn('upper_string_col', upper(data_df.string_col)).show()

这里是结果:

在这个例子中,我们将 string_col 的字母大小写改为全部大写。我们需要将这个结果分配给一个新列,因此,我们创建了一个名为 upper_string_col 的列来存储这些大写值。同时,请注意,这个列没有被添加到原始的 data_df 中,因为我们没有将结果保存回 data_df

在数据处理中,很多时候我们需要函数来过滤 DataFrame。现在,我们将看看如何在 Spark DataFrame 中过滤数据。

过滤 DataFrame

过滤 DataFrame 意味着我们可以从 DataFrame 中获取行或列的子集。有几种不同的方法可以过滤 Spark DataFrame。在这里,我们将看看一个例子:

data_df.filter(data_df.col_1 == 100).show()

这里是结果:

+-----+-----+-------------+----------+------+
|col_1|col_2| string_col  |   col_4  |col_6 |
+-----+-----+-------------+----------+------+
|  100|200.0|string_test_1|2023-01-01|   A  |
+-----+-----+-------------+----------+------+

在这个例子中,我们过滤 data_df 以仅包括 col_1 的列值等于 100 的行。只有一行满足这个标准,因此,结果 DataFrame 中只返回一行。

您可以使用此函数根据要求以多种方式对数据进行切片和切块。

由于我们正在讨论数据过滤,我们也可以根据逻辑运算符来过滤数据。现在,我们将看看如何在 DataFrames 中使用逻辑运算符来过滤数据。

DataFrame 中的逻辑运算符

我们还可以将逻辑运算符与 DataFrame 中的过滤操作结合使用,这是一组重要的运算符。这些包括 AND 和 OR 运算符等。它们用于根据复杂条件过滤 DataFrame。让我们看看如何使用 AND 运算符:

data_df.filter((data_df.col_1 == 100)
                  & (data_df.col_6 == 'A')).show()

这里是结果:

+-----+------+-------------+----------+------+
|col_1| col_2|  string_col |   col_4  |col_6 |
+-----+------+-------------+----------+------+
|  100| 200.0|string_test_1|2023-01-01|   A  |
+-----+------+-------------+----------+------+

在这个例子中,我们试图获取 col_1 的值为 100col_6 的值为 A 的行。目前,只有一行满足这个条件,因此,只有一行作为结果返回。

现在,让我们看看如何使用 OR 运算符来组合条件:

data_df.filter((data_df.col_1 == 100)
                  | (data_df.col_2 == 300.00)).show()

此语句将给出以下结果:

+-----+------+-------------+----------+------+
|col_1| col_2|  string_col |   col_4  | col_6|
+-----+------+-------------+----------+------+
|  100| 200.0|string_test_1|2023-01-01|   A  |
|  200| 300.0|string_test_2|2023-02-01|   A  |
+-----+------+-------------+----------+------+

在这个例子中,我们试图获取 col_1 的值为 100col_2 的值为 300.0 的行。目前,有两行满足这个条件,因此,它们被作为结果返回。

在数据过滤中,还有一个重要的函数用于在列表中查找值。现在,我们将看看如何在 PySpark 中使用 isin() 函数。

使用 isin()

isin() 函数用于在 DataFrame 列中查找存在于列表中的值。为此,我们需要创建一个包含一些值的列表。一旦我们有了这个列表,我们就会使用 isin() 函数来查看列表中的某些值是否存在于 DataFrame 中。以下是一个演示此功能的例子:

list = [100, 200]
data_df.filter(data_df.col_1.isin(list)).show()

这里是结果:

+-----+-----+-------------+----------+------+
|col_1|col_2|  string_col |   col_4  |col_6 |
+-----+-----+-------------+----------+------+
|  100|200.0|string_test_1|2023-01-01|   A  |
|  200|300.0|string_test_2|2023-02-01|   A  |
+-----+-----+-------------+----------+------+

在这个例子中,我们可以看到值 100200 出现在 data_df DataFrame 的两行中,因此,这两行都被返回。

我们还可以在 Spark DataFrames 的不同列中转换数据类型。现在,让我们看看如何在 Spark DataFrames 中转换不同的数据类型。

数据类型转换

在本节中,我们将看到在 Spark DataFrame 列中转换数据类型的不同方法。

我们将首先使用 Spark 中的 cast 函数。以下代码说明了这一点:

from pyspark.sql.functions import col
from pyspark.sql.types import StringType,BooleanType,DateType,IntegerType
data_df_2 = data_df.withColumn("col_4",col("col_4").cast(StringType())) \
    .withColumn("col_1",col("col_1").cast(IntegerType()))
data_df_2.printSchema()
data_df.show()

这里是结果:

root
 |-- col_1: integer (nullable = true)
 |-- col_2: double (nullable = true)
 |-- string_col: string (nullable = true)
 |-- col_4: string (nullable = true)
 |-- col_6: string (nullable = false)
+-----+-----+-------------+----------+-----+
|col_1|col_2|   string_col|     col_4|col_6|
+-----+-----+-------------+----------+-----+
|  100|200.0|string_test_1|2023-01-01|    A|
|  200|300.0|string_test_2|2023-02-01|    A|
|  300|400.0|string_test_3|2023-03-01|    A|
+-----+-----+-------------+----------+-----+

在前面的代码中,我们看到我们正在更改两个列的数据类型,即 col_4col_1。首先,我们将 col_4 改为字符串类型。这个列之前是日期列。然后,我们将 col_1long 类型改为整数类型。

这里是 data_df 的模式,仅供参考:

root
 |-- col_1: long (nullable = true)
 |-- col_2: double (nullable = true)
 |-- string_col: string (nullable = true)
 |-- col_4: date (nullable = true)
 |-- col_5: timestamp (nullable = true)
 |-- col_6: string (nullable = false)

我们看到 col_1col_4 是不同的数据类型。

下一个示例是通过使用 selectExpr() 函数来更改列的数据类型。以下代码说明了这一点:

data_df_3 = data_df_2.selectExpr("cast(col_4 as date) col_4",
    "cast(col_1 as long) col_1")
data_df_3.printSchema()
data_df_3.show(truncate=False)

这里是结果:

root
 |-- col_4: date (nullable = true)
 |-- col_1: long (nullable = true)
+----------+-----+
|  col_4   |col_1|
+----------+-----+
|2023-01-01|  100|
|2023-02-01|  200|
|2023-03-01|  300|
+----------+-----+

在前面的代码中,我们看到我们正在更改两个列的数据类型,即 col_4col_1。首先,我们将 col_4 改回 date 类型。然后,我们将 col_1 改为 long 类型。

下一个示例是通过使用 SQL 来更改列的数据类型。以下代码说明了这一点:

data_df_3.createOrReplaceTempView("CastExample")
data_df_4 = spark.sql("SELECT DOUBLE(col_1), DATE(col_4) from CastExample")
data_df_4.printSchema()
data_df_4.show(truncate=False)

这里是结果:

root
 |-- col_1: double (nullable = true)
 |-- col_4: date (nullable = true)
+-----+----------+
|col_1|  col_4   |
+-----+----------+
|100.0|2023-01-01|
|200.0|2023-02-01|
|300.0|2023-03-01|
+-----+----------+

在前面的代码中,我们看到我们正在更改两个列的数据类型,即 col_4col_1。首先,我们使用 createOrReplaceTempView() 创建一个名为 CastExample 的表。然后,我们使用这个表将 col_4 改回 date 类型。然后,我们将 col_1 改为 double 类型。

在数据分析领域,处理空值非常有价值。现在,让我们看看我们如何从 DataFrame 中删除空值。

从 DataFrame 中删除空值

有时,在数据中存在可能导致清洁数据变得混乱的空值。删除空值是许多数据分析师和数据工程师需要做的基本练习。PySpark 提供了相关的函数来完成这项工作。

让我们创建另一个名为 salary_data 的 DataFrame 来展示一些后续操作:

salary_data = [("John", "Field-eng", 3500),
    ("Michael", "Field-eng", 4500),
    ("Robert", None, 4000),
    ("Maria", "Finance", 3500),
    ("John", "Sales", 3000),
    ("Kelly", "Finance", 3500),
    ("Kate", "Finance", 3000),
    ("Martin", None, 3500),
    ("Kiran", "Sales", 2200),
    ("Michael", "Field-eng", 4500)
  ]
columns= ["Employee", "Department", "Salary"]
salary_data = spark.createDataFrame(data = salary_data, schema = columns)
salary_data.printSchema()
salary_data.show()

这里是结果:

root
 |-- Employee: string (nullable = true)
 |-- Department: string (nullable = true)
 |-- Salary: long (nullable = true)
+--------+----------+------+
|Employee|Department|Salary|
+--------+----------+------+
|    John| Field-eng| 3500 |
| Michael| Field-eng| 4500 |
|  Robert|      null| 4000 |
|   Maria|   Finance| 3500 |
|    John|     Sales| 3000 |
|   Kelly|   Finance| 3500 |
|    Kate|   Finance| 3000 |
|  Martin|      null| 3500 |
|   Kiran|     Sales| 2200 |
| Michael| Field-eng| 4500 |
+--------+----------+------+

现在,让我们看看 dropna() 函数;这将帮助我们删除 DataFrame 中的空值:

salary_data.dropna().show()

这里是结果:

+--------+----------+------+
|Employee|Department|Salary|
+--------+----------+------+
| John   | Field-eng| 3500 |
| Michael| Field-eng| 4500 |
| Maria  |   Finance| 3500 |
| John   |     Sales| 3000 |
| Kelly  |   Finance| 3500 |
| Kate   |   Finance| 3000 |
| Kiran  |     Sales| 2200 |
| Michael| Field-eng| 4500 |
+--------+----------+------+

在结果 DataFrame 中,我们看到当使用 dropna() 函数时,带有 RobertMartin 的行从新的 DataFrame 中被删除。

去重数据是数据分析任务中经常需要的一种有用技术。现在,让我们看看我们如何从 DataFrame 中删除重复值。

从 DataFrame 中删除重复项

有时,在数据中存在冗余值,这会使清洁数据变得混乱。删除这些值可能在许多用例中是必要的。PySpark 提供了 dropDuplicates() 函数来执行此操作。以下是说明此操作的代码:

new_salary_data = salary_data.dropDuplicates().show()

这里是结果:

+--------+----------+------+
|Employee|Department|Salary|
+--------+----------+------+
|    John| Field-eng|  3500|
| Michael| Field-eng|  4500|
|   Maria|   Finance|  3500|
|    John|     Sales|  3000|
|   Kelly|   Finance|  3500|
|    Kate|   Finance|  3000|
|   Kiran|     Sales|  2200|
+--------+----------+------+

在这个例子中,我们看到在将 dropDuplicates() 函数应用于原始 DataFrame 后,名为 Michael 的员工在结果 DataFrame 中只显示一次。这个名称及其对应值在原始 DataFrame 中出现了两次。

现在我们已经了解了不同的数据过滤技术,接下来我们将看看如何在 PySpark DataFrame 中进行数据聚合。

在 DataFrame 中使用聚合

Spark 中用于聚合数据的一些方法如下:

  • agg

  • avg

  • count

  • max

  • mean

  • min

  • pivot

  • sum

我们将在下面的代码示例中看到一些实际操作。

平均值(avg)

在下面的示例中,我们看到如何在 Spark 中使用聚合函数。我们将从计算列中所有值的平均值开始:

from pyspark.sql.functions import countDistinct, avg
salary_data.select(avg('Salary')).show()

这里是结果:

+-----------+
|avg(Salary)|
+-----------+
|     3520.0|
+-----------+

这个示例计算了 salary_data DataFrame 中薪水列的平均值。我们将 Salary 列传递给了 avg 函数,它为我们计算了该列的平均值。

现在,让我们看看如何在 PySpark DataFrame 中计数不同的元素。

计数

在下面的代码示例中,我们可以看到如何在 Spark 中使用聚合函数:

salary_data.agg({'Salary':'count'}).show()

这里是结果:

+-------------+
|count(Salary)|
+-------------+
|           10|
+-------------+

这个示例计算了 salary_data DataFrame 中 Salary 列的值的总数。我们将 Salary 列传递给了 agg 函数,并将 count 作为其另一个参数,它为我们计算了该列的计数。

现在,让我们看看如何在 PySpark DataFrame 中计数不同的元素。

计数独立值

在下面的示例中,我们将查看如何在 PySpark DataFrame 中计数不同的元素:

salary_data.select(countDistinct("Salary").alias("Distinct Salary")).show()

这里是结果:

+---------------+
|Distinct Salary|
+---------------+
|              5|
+---------------+

这个示例计算了 salary_data DataFrame 中薪水列的总独立值。我们将 Salary 列传递给了 countDistinct 函数,它为我们计算了该列的计数。

现在,让我们看看如何在 PySpark DataFrame 中查找最大值。

查找最大值(max)

在下面的代码示例中,我们将查看如何在 PySpark DataFrame 的列中查找最大值:

salary_data.agg({'Salary':'max'}).show()

这里是结果:

+-----------+
|max(Salary)|
+-----------+
|       4500|
+-----------+

这个示例计算了 salary_data DataFrame 中 Salary 列的所有值的最大值。我们将 Salary 列传递给了 agg 函数,并将 max 作为其另一个参数,它为我们计算了该列的最大值。

现在,让我们看看如何在 PySpark DataFrame 中获取所有元素的总和。

总和

在下面的代码示例中,我们将查看如何在 PySpark DataFrame 中求和所有值:

salary_data.agg({'Salary':'sum'}).show()

这里是结果:

+-----------+
|sum(Salary)|
+-----------+
|      35200|
+-----------+

这个示例计算了 salary_data DataFrame 中 Salary 列所有值的总和。我们将 Salary 列传递给了 agg 函数,并将 sum 作为其另一个参数,它为我们计算了该列的总和。

现在,让我们看看如何在 PySpark DataFrame 中排序数据。

使用 OrderBy 排序数据

在下面的代码示例中,我们将查看如何在 PySpark DataFrame 中按升序排序数据:

salary_data.orderBy("Salary").show()

这里是结果:

+--------+----------+------+
|Employee|Department|Salary|
+--------+----------+------+
| Kiran  | Sales    | 2200 |
| Kate   | Finance  | 3000 |
| John   | Sales    | 3000 |
| John   | Field-eng| 3500 |
| Martin | null     | 3500 |
| Kelly  | Finance  | 3500 |
| Maria  | Finance  | 3500 |
| Robert | null     | 4000 |
| Michael| Field-eng| 4500 |
| Michael| Field-eng| 4500 |
+--------+----------+------+

本例根据salary_data DataFrame 中Salary列的值对整个 DataFrame 进行排序。我们将Salary列传递给orderBy函数,它根据此列对 DataFrame 进行了排序。

我们还可以通过向原始orderBy函数中添加另一个函数desc()来按降序格式对数据进行排序。以下示例说明了这一点:

salary_data.orderBy(salary_data["Salary"].desc()).show()

这里是结果:

+--------+----------+------+
|Employee|Department|Salary|
+--------+----------+------+
| Michael| Field-eng| 4500 |
| Michael| Field-eng| 4500 |
| Robert | null     | 4000 |
| Martin | null     | 3500 |
| Kelly  | Finance  | 3500 |
| Maria  | Finance  | 3500 |
| John   | Field-eng| 3500 |
| John   | Sales    | 3000 |
| Kate   | Finance  | 3000 |
| Kiran  | Sales    | 2200 |
+--------+----------+------+

本例中,根据salary_data DataFrame 中Salary列的值按降序对整个 DataFrame 进行排序。我们将Salary列传递给orderBy函数,并附加了desc()函数调用,它根据此列对 DataFrame 进行了降序排序。

摘要

在本章中,我们学习了如何在 Spark DataFrame 中操作数据。

我们讨论了 Spark DataFrame API 以及 Spark 中的不同数据类型。我们还学习了如何在 Spark 中创建 DataFrame 以及如何创建后查看这些 DataFrame。最后,我们学习了不同的数据操作和数据聚合函数。

在下一章中,我们将介绍 Spark 中与数据处理相关的一些高级操作。

样题

  1. 以下哪个操作会触发评估?

  2. DataFrame.filter()

  3. DataFrame.distinct()

  4. DataFrame.intersect()

  5. DataFrame.join()

  6. DataFrame.count()

答案

  1. E

第五章:Spark 的高级操作和优化

在本章中,我们将深入探讨 Apache Spark 的高级功能,为您提供优化数据处理工作流程所需的知识和技术。从 Catalyst 优化器的内部工作原理到不同类型连接的复杂性,我们将探索高级 Spark 操作,让您能够充分利用这个强大框架的全部潜力。

本章将涵盖以下主题:

  • 在 Spark DataFrame 中对数据进行分组的不同选项。

  • Spark 中的各种连接类型,包括内连接、左连接、右连接、外连接、交叉连接、广播连接和洗牌连接,每种连接都有其独特的用例和影响

  • Shuffle 和广播连接,重点关注广播哈希连接和洗牌排序合并连接,以及它们的应用和优化策略

  • 使用不同的数据格式(如 CSV、Parquet 等)在 Spark 中读取和写入数据

  • 使用 Spark SQL 进行不同操作

  • Catalyst 优化器,Spark 查询执行引擎中的一个关键组件,它采用基于规则和基于成本的优化来提高查询性能

  • Spark 中窄变换和宽变换的区别以及何时使用每种类型以实现最佳并行性和资源效率

  • 数据持久化和缓存技术以减少重复计算并加速数据处理,以及高效内存管理的最佳实践

  • 通过 repartition 和 coalesce 进行数据分区,以及如何使用这些操作来平衡工作负载和优化数据分布

  • 用户定义函数UDFs)和自定义函数,允许您实现专门的数据处理逻辑,以及何时以及如何有效地利用它们

  • 使用 Catalyst 优化器和自适应查询执行AQE)进行 Spark 的高级优化

  • 基于数据的优化技术及其好处

每个部分都将提供深入见解、实际示例和最佳实践,确保您能够应对 Apache Spark 中的复杂数据处理挑战。到本章结束时,您将掌握利用 Spark 高级功能的知识和技能,并解锁其在数据驱动项目中的全部潜力。

在 Spark 中对数据进行分组和不同的 Spark 连接

我们将从最重要的数据处理技术之一开始:分组和连接数据。当我们进行数据探索时,根据不同标准对数据进行分组成为数据分析的关键。我们将探讨如何使用groupBy对不同的数据进行分组。

在 DataFrame 中使用 groupBy

我们可以根据不同的标准在 DataFrame 中对数据进行分组——例如,我们可以根据 DataFrame 中的不同列对数据进行分组。我们还可以应用不同的聚合,如sumaverage,来获取数据切片的整体视图。

为了这个目的,在 Spark 中,我们有groupBy操作。groupBy操作与 SQL 中的groupBy类似,因为我们可以在这些分组数据集上执行分组操作。此外,我们可以在一个groupBy语句中指定多个groupBy标准。以下示例展示了如何在 PySpark 中使用groupBy。我们将使用上一章中创建的 DataFrame 薪水数据。

在以下groupBy语句中,我们根据Department列对薪水数据进行分组:

salary_data.groupby('Department')

因此,这个操作返回了一个按Department列分组的分组数据对象:

<pyspark.sql.group.GroupedData at 0x7fc8495a3c10>

这可以分配给一个单独的 DataFrame,并且可以对这组数据执行更多操作。所有聚合操作也可以用于 DataFrame 的不同组。

我们将使用以下语句来获取salary_data DataFrame 中不同部门的平均薪水:

salary_data.groupby(‘Department’).avg().show()

这里是结果:

+----------+------------------+  
|Department| avg(Salary)      |  
+----------+------------------+  
| null     |            3750.0|  
| Sales    |            2600.0|  
| Field-eng| 4166.666666666667|  
| Finance  |3333.3333333333335|  
+----------+------------------+ 

在这个例子中,我们可以看到每个部门的平均薪水是基于salary_data DataFrame 中的salary列计算的。包括null(因为我们 DataFrame 中有空值),所有四个部门都包含在结果 DataFrame 中。

现在,让我们看看如何将复杂的groupBy操作应用于 PySpark DataFrame 中的数据。

一个复杂的 groupBy 语句

groupBy可以在复杂的数据操作中使用,例如在单个groupBy语句中进行多个聚合。

在下面的代码片段中,我们将使用groupBy操作,通过对每个部门的薪水列求和。然后,我们将刚刚创建的sum(Salary)列四舍五入到小数点后两位。之后,我们将sum(Salary)列重命名为Salary。所有这些操作都在一个groupBy语句中完成:

from pyspark.sql.functions import col, round
salary_data.groupBy('Department')\
  .sum('Salary')\
  .withColumn('sum(Salary)',round(col('sum(Salary)'), 2))\
  .withColumnRenamed('sum(Salary)', 'Salary')\
  .orderBy('Department')\
  .show()

因此,我们将看到以下 DataFrame,它显示了基于每个部门的Salary列的聚合总和:

+----------+------------------+
|Department|    sum(Salary)   |
+----------+------------------+
| null     |              7500|
| Field-eng|             12500|
| Finance  |             10000|
| Sales    |              5200|
+----------+------------------+

在这个例子中,我们可以看到每个部门的总薪水被计算在一个名为sum(Salary)的新列中,之后我们将这个总数四舍五入到两位小数。在下一个语句中,我们将sum(Salary)列重命名为Salary,然后根据Department对结果 DataFrame 进行排序。在结果 DataFrame 中,我们可以看到每个部门的薪水总和被计算在新列中。

现在我们知道了如何使用不同的聚合函数来分组数据,让我们看看如何在 Spark 中将两个 DataFrame 合并在一起。

Spark 中的 DataFrame 连接

连接操作是数据处理任务的基本操作,也是 Apache Spark 的核心组件之一。Spark 提供了多种类型的连接操作,用于将来自不同 DataFrame 或数据集的数据合并在一起。在本节中,我们将探讨不同的 Spark 连接操作以及何时使用每种类型。

连接操作用于根据公共列将两个或多个数据帧中的数据合并在一起。这些操作对于合并数据集、聚合信息和执行关系操作等任务至关重要。

在 Spark 中,执行连接的主要语法是使用 .join() 方法,该方法接受以下参数:

  • other: 要与之连接的其他数据帧

  • on: 要连接数据帧的列

  • how: 要执行的连接类型(内部、外部、左连接或右连接)

  • suffixes: 在两个数据帧中具有相同名称的列中添加的后缀

这些参数在连接操作的主要语法中使用,如下所示:

Dataframe1.join(Dataframe2, on, how)

在这里,Dataframe1 将位于连接的左侧,而 Dataframe2 将位于连接的右侧。

数据帧或数据集可以根据数据帧内的公共列进行连接,连接查询的结果是一个新的数据帧。

我们将在两个新的数据帧上演示连接操作。首先,让我们创建这些数据帧。第一个数据帧称为 salary_data_with_id

salary_data_with_id = [(1, "John", "Field-eng", 3500), \
    (2, "Robert", "Sales", 4000), \
    (3, "Maria", "Finance", 3500), \
    (4, "Michael", "Sales", 3000), \
    (5, "Kelly", "Finance", 3500), \
    (6, "Kate", "Finance", 3000), \
    (7, "Martin", "Finance", 3500), \
    (8, "Kiran", "Sales", 2200), \
  ]
columns= ["ID", "Employee", "Department", "Salary"]
salary_data_with_id = spark.createDataFrame(data = salary_data_with_id, schema = columns)
salary_data_with_id.show()

结果数据帧,命名为 salary_data_with_id,看起来如下:

+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
| 1 | John   | Field-eng|  3500|
| 2 | Robert | Sales    |  4000|
| 3 | Maria  | Finance  |  3500|
| 4 | Michael| Sales    |  3000|
| 5 | Kelly  | Finance  |  3500|
| 6 | Kate   | Finance  |  3000|
| 7 | Martin | Finance  |  3500|
| 8 | Kiran  | Sales    |  2200|
+---+--------+----------+------+

现在,我们将创建另一个名为 employee_data 的数据帧:

employee_data = [(1, "NY", "M"), \
    (2, "NC", "M"), \
    (3, "NY", "F"), \
    (4, "TX", "M"), \
    (5, "NY", "F"), \
    (6, "AZ", "F") \
  ]
columns= ["ID", "State", "Gender"]
employee_data = spark.createDataFrame(data = employee_data, schema = columns)
employee_data.show()

结果数据帧,命名为 employee_data,看起来如下:

+---+-----+------+
| ID|State|Gender|
+---+-----+------+
|  1|   NY|     M|
|  2|   NC|     M|
|  3|   NY|     F|
|  4|   TX|     M|
|  5|   NY|     F|
|  6|   AZ|     F|
+---+-----+------+

现在,假设我们想要根据 ID 列将这两个数据帧连接在一起。

如我们之前提到的,Spark 提供了不同类型的连接操作。我们将在本章中探索其中的一些。让我们从内部连接开始。

内部连接

当我们想要根据两个数据帧中共同存在的值来连接两个数据帧时,使用内部连接。任何在任何一个数据帧中不存在的值都不会是结果数据帧的一部分。默认情况下,Spark 中的连接类型是内部连接。

用例

内部连接在合并数据时非常有用,当你对两个数据帧中的共同元素感兴趣时——例如,将销售数据与客户数据连接起来,以查看哪些客户进行了购买。

以下代码演示了如何使用我们之前创建的数据帧进行内部连接:

salary_data_with_id.join(employee_data,salary_data_with_id.ID ==  employee_data.ID,"inner").show()

结果数据帧现在包含两个数据帧的所有列——salary_data_with_idemployee_data——在单个数据帧中连接在一起。它只包括两个数据帧中都有的行。下面是它的样子:

+---+--------+----------+------+---+-----+------+
| ID|Employee|Department|Salary| ID|State|Gender|
+---+--------+----------+------+---+-----+------+
|  1|    John| Field-eng|  3500|  1|   NY|     M|
|  2|  Robert|     Sales|  4000|  2|   NC|     M|
|  3|   Maria|   Finance|  3500|  3|   NY|     F|
|  4| Michael|     Sales|  3000|  4|   TX|     M|
|  5|   Kelly|   Finance|  3500|  5|   NY|     F|
|  6|    Kate|   Finance|  3000|  6|   AZ|     F|
+---+--------+----------+------+---+-----+------+

你会注意到,how 参数定义了此语句中正在进行的连接类型。目前,它显示为 inner,因为我们想要数据帧根据内部连接进行连接。我们还可以看到 ID 78 缺失。原因是 employee_data 数据帧不包含 ID 78。由于我们使用的是内部连接,它只基于两个数据帧中共同的数据元素进行连接。任何在任一数据帧中不存在的数据都不会是结果数据帧的一部分。

接下来,我们将探索外部连接。

外部连接

一个 null

当我们想要根据两个 DataFrame 中都存在的值来连接两个 DataFrame,无论这些值是否存在于另一个 DataFrame 中时,我们应该使用外连接。任何存在于任何一个 DataFrame 中的值都将成为结果 DataFrame 的一部分。

用例

外连接适用于需要包含两个 DataFrame 中的所有记录,同时容纳不匹配值的情况——例如,当合并员工数据与项目数据以查看哪些员工被分配到哪些项目时,包括那些目前没有被分配到任何项目的员工。

以下代码演示了我们如何使用之前创建的 DataFrame 进行外连接:

salary_data_with_id.join(employee_data,salary_data_with_id.ID ==  employee_data.ID,"outer").show()

结果 DataFrame 包含 salary_data_with_idemployee_data DataFrame 中所有员工的资料。它看起来是这样的:

+---+--------+----------+------+----+-----+------+
| ID|Employee|Department|Salary|  ID|State|Gender|
+---+--------+----------+------+----+-----+------+
|  1|    John| Field-eng|  3500|   1|   NY|     M|
|  2|  Robert|     Sales|  4000|   2|   NC|     M|
|  3|   Maria|   Finance|  3500|   3|   NY|     F|
|  4| Michael|     Sales|  3000|   4|   TX|     M|
|  5|   Kelly|   Finance|  3500|   5|   NY|     F|
|  6|    Kate|   Finance|  3000|   6|   AZ|     F|
|  7|  Martin|   Finance|  3500|null| null|  null|
|  8|   Kiran|     Sales|  2200|null| null|  null|
+---+--------+----------+------+----+-----+------+

你会注意到 how 参数已更改,并显示为 outer。在结果 DataFrame 中,现在存在 ID 78。然而,也请注意,ID 78IDStateGender 列是 null。原因是 employee_data DataFrame 不包含 ID 78。任何在两个 DataFrame 中都不存在的数据将成为结果 DataFrame 的一部分,但对应列将显示为 null,正如员工 ID 78 的情况所示。

接下来,我们将探讨左连接。

左连接

左连接返回左侧 DataFrame 的所有行和右侧 DataFrame 的匹配行。如果右侧 DataFrame 中没有匹配项,则结果将包含 null 值。

用例

当你想要保留左侧 DataFrame 的所有记录,并且只保留右侧 DataFrame 的匹配记录时,左连接很有用——例如,当合并客户数据与交易数据以查看哪些客户进行了购买时。

以下代码演示了我们如何使用之前创建的 DataFrame 进行左连接:

salary_data_with_id.join(employee_data,salary_data_with_id.ID ==  employee_data.ID,"left").show()

结果 DataFrame 包含来自左侧 DataFrame 的所有数据——即 salary_data_with_id。它看起来是这样的:

+---+--------+----------+------+----+-----+------+
| ID|Employee|Department|Salary|  ID|State|Gender|
+---+--------+----------+------+----+-----+------+
|  1|    John| Field-eng|  3500|   1|   NY|     M|
|  2|  Robert|     Sales|  4000|   2|   NC|     M|
|  3|   Maria|   Finance|  3500|   3|   NY|     F|
|  4| Michael|     Sales|  3000|   4|   TX|     M|
|  5|   Kelly|   Finance|  3500|   5|   NY|     F|
|  6|    Kate|   Finance|  3000|   6|   AZ|     F|
|  7|  Martin|   Finance|  3500|null| null|  null|
|  8|   Kiran|     Sales|  2200|null| null|  null|
+---+--------+----------+------+----+-----+------+

注意,how 参数已更改,并显示为 left。现在,ID 78 存在。然而,也请注意,ID 78IDStateGender 列是 null。原因是 employee_data DataFrame 不包含 ID 78。由于 salary_data_with_id 是连接语句中的左侧 DataFrame,其值在连接中具有优先权。

所有来自左侧 DataFrame 的记录都包含在结果 DataFrame 中,并且包含来自右侧 DataFrame 的匹配记录。右侧 DataFrame 中的不匹配条目在结果中用 null 值填充。

接下来,我们将探讨右连接。

右连接

右连接与左连接类似,但它返回右侧 DataFrame 的所有行和左侧 DataFrame 的匹配行。左侧 DataFrame 中的不匹配行包含 null 值。

用例

右连接是左连接的相反,当您想保留右侧 DataFrame 的所有记录并包含来自左侧 DataFrame 的匹配记录时使用。

以下代码演示了如何使用我们之前创建的 DataFrame 进行右连接:

salary_data_with_id.join(employee_data,salary_data_with_id.ID ==  employee_data.ID,"right").show()

结果 DataFrame 包含右侧 DataFrame 的所有数据——即 employee_data。它看起来如下:

+---+--------+----------+------+---+-----+------+
| ID|Employee|Department|Salary| ID|State|Gender|
+---+--------+----------+------+---+-----+------+
|  1|    John| Field-eng|  3500|  1|   NY|     M|
|  2|  Robert|     Sales|  4000|  2|   NC|     M|
|  3|   Maria|   Finance|  3500|  3|   NY|     F|
|  4| Michael|     Sales|  3000|  4|   TX|     M|
|  5|   Kelly|   Finance|  3500|  5|   NY|     F|
|  6|    Kate|   Finance|  3000|  6|   AZ|     F|
+---+--------+----------+------+---+-----+------+

注意到 how 参数已更改,现在显示为 right。结果 DataFrame 显示 IDs 78 不存在。原因是 employee_data DataFrame 不包含 IDs 78。由于 employee_data 是连接语句中的右侧 DataFrame,其值在连接中具有优先权。

所有来自右侧 DataFrame 的记录都包含在结果 DataFrame 中,并且包含来自左侧 DataFrame 的匹配记录。左侧 DataFrame 中的不匹配条目在结果中用 null 值填充。

接下来,我们将探索交叉连接。

交叉连接

交叉连接,也称为笛卡尔连接,将左侧 DataFrame 的每一行与右侧 DataFrame 的每一行组合。这导致了一个大型的笛卡尔积 DataFrame。

用例

由于它们可能生成大量数据集,因此应谨慎使用交叉连接。它们通常用于探索所有可能的数据组合,例如在生成测试数据时。

接下来,我们将探索并集选项以连接两个 DataFrame。

并集

并集用于连接具有相似模式的两个 DataFrame。为了说明这一点,我们将创建另一个名为 salary_data_with_id_2 的 DataFrame,其中包含一些额外的值。这个 DataFrame 的模式与 salary_data_with_id 的模式相同:

salary_data_with_id_2 = [(1, "John", "Field-eng", 3500), \
    (2, "Robert", "Sales", 4000), \
    (3, "Aliya", "Finance", 3500), \
    (4, "Nate", "Sales", 3000), \
  ]
columns2= ["ID", "Employee", "Department", "Salary"]
salary_data_with_id_2 = spark.createDataFrame(data = salary_data_with_id_2, schema = columns2)
salary_data_with_id_2.printSchema()
salary_data_with_id_2.show(truncate=False)

因此,你将首先看到 DataFrame 的模式,然后是实际的 DataFrame 及其值:

root
 |-- ID: long (nullable = true)
 |-- Employee: string (nullable = true)
 |-- Department: string (nullable = true)
 |-- Salary: long (nullable = true)
+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
|1  |John    |Field-eng |3500  |
|2  |Robert  |Sales     |4000  |
|3  |Aliya   |Finance   |3500  |
|4  |Nate    |Sales     |3000  |
+---+--------+----------+------+

一旦我们有了这个 DataFrame,我们就可以使用 union() 函数将 salary_data_with_idsalary_data_with_id_2 DataFrame 连接在一起。以下示例说明了这一点:

unionDF = salary_data_with_id.union(salary_data_with_id_2)
unionDF.show(truncate=False)

结果 DataFrame,命名为 unionDF,看起来如下:

+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
|1  |John    |Field-eng |3500  |
|2  |Robert  |Sales     |4000  |
|3  |Maria   |Finance   |3500  |
|4  |Michael |Sales     |3000  |
|5  |Kelly   |Finance   |3500  |
|6  |Kate    |Finance   |3000  |
|7  |Martin  |Finance   |3500  |
|8  |Kiran   |Sales     |2200  |
|1  |John    |Field-eng |3500  |
|2  |Robert  |Sales     |4000  |
|3  |Aliya   |Finance   |3500  |
|4  |Nate    |Sales     |3000  |
+---+--------+----------+------+

如您所见,两个 DataFrame 已连接在一起,因此结果 DataFrame 中添加了新行。最后四行来自 salary_data_with_id_2 并添加到 salary_data_with_id 的行中。这是将两个 DataFrame 连接在一起的一种方法。

在本节中,我们探讨了不同类型的 Spark 连接及其适用场景。选择正确的连接类型对于确保 Spark 中高效的数据处理至关重要,并且理解每种类型的含义将帮助你在数据分析和处理任务中做出明智的决定。

现在,让我们看看如何在 Spark 中读取和写入数据。

读取和写入数据

当我们使用 Spark 并在 Spark 中进行数据操作的所有操作时,我们需要做的最重要的事情之一是将数据读写到磁盘上。记住,Spark 是一个内存框架,这意味着所有操作都在计算或集群的内存中执行。一旦这些操作完成,我们就会希望将数据写入磁盘。同样,在我们操作任何数据之前,我们可能还需要从磁盘读取数据。

Spark 支持多种数据格式用于读取和写入不同类型的数据文件。在本章中,我们将讨论以下格式。

  • 逗号分隔值 (CSV)

  • Parquet

  • 优化行 列式 (ORC)

请注意,这些并不是 Spark 支持的唯一格式,但这是一个非常流行的格式子集。Spark 还支持许多其他格式,例如 Avro、文本、JDBC、Delta 等。

在下一节中,我们将讨论 CSV 文件格式以及如何读取和写入 CSV 格式的数据文件。

读取和写入 CSV 文件

在本节中,我们将讨论如何从 CSV 文件格式读取和写入数据。在这个文件格式中,数据由逗号分隔。这是一个非常流行的数据格式,因为它易于使用且简单。

让我们通过运行以下代码来查看如何使用 Spark 写入 CSV 文件:

salary_data_with_id.write.csv('salary_data.csv', mode='overwrite', header=True)
spark.read.csv('/salary_data.csv', header=True).show()

生成的 DataFrame,命名为 salary_data_with_id,如下所示:

+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
| 1 | John   | Field-eng|  3500|
| 2 | Robert | Sales    |  4000|
| 3 | Maria  | Finance  |  3500|
| 4 | Michael| Sales    |  3000|
| 5 | Kelly  | Finance  |  3500|
| 6 | Kate   | Finance  |  3000|
| 7 | Martin | Finance  |  3500|
| 8 | Kiran  | Sales    |  2200|
+---+--------+----------+------+

在这里我们可以看到 dataframe.write.csv() 函数中的一些参数。第一个参数是我们需要写入磁盘的 DataFrame 名称。第二个参数 header 指定我们需要写入的文件是否应该带有标题行。

dataframe.read.csv() 函数中,有一些参数我们需要讨论。第一个参数是我们需要读取的文件的 path/name 值。第二个参数 header 指定文件是否有标题行需要读取。

在第一个语句中,我们将 salary_data DataFrame 写入名为 salary_data.csv 的 CSV 文件。

在下一个语句中,我们正在读取我们写入的相同文件以查看其内容。我们可以看到,生成的文件包含我们写入的相同数据。

让我们看看另一个可以用于使用 Spark 读取 CSV 文件的函数:

from pyspark.sql.types import *
filePath = '/salary_data.csv'
columns= ["ID", "State", "Gender"] 
schema = StructType([
      StructField("ID", IntegerType(),True),
  StructField("State",  StringType(),True),
  StructField("Gender",  StringType(),True)
])
read_data = spark.read.format("csv").option("header","true").schema(schema).load(filePath)
read_data.show()

生成的 DataFrame,命名为 read_data,如下所示:

+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
| 1 | John   | Field-eng|  3500|
| 2 | Robert | Sales    |  4000|
| 3 | Maria  | Finance  |  3500|
| 4 | Michael| Sales    |  3000|
| 5 | Kelly  | Finance  |  3500|
| 6 | Kate   | Finance  |  3000|
| 7 | Martin | Finance  |  3500|
| 8 | Kiran  | Sales    |  2200|
+---+--------+----------+------+

spark.read.format() 函数中,有一些参数。首先,我们指定需要读取的文件格式。然后,我们可以为不同的选项执行不同的函数调用。在下一个调用中,我们指定文件有标题,因此 DataFrame 预期会有标题。然后,我们指定我们需要为这些数据有一个模式,该模式在 schema 变量中定义。最后,在 load 函数中,我们定义要加载的文件的路径。

接下来,我们将学习如何使用 Spark 读取和写入 Parquet 文件。

读取和写入 Parquet 文件

在本节中,我们将讨论 Parquet 文件格式。Parquet 是一种列式文件格式,使得数据读取和写入非常高效。它也是一种紧凑的文件格式,有助于加快读取和写入速度。

让我们通过运行以下代码来学习如何使用 Spark 写入 Parquet 文件:

salary_data_with_id.write.parquet('salary_data.parquet', mode='overwrite')
spark.read.parquet(' /salary_data.parquet').show()

生成的 DataFrame,命名为salary_data_with_id,看起来如下:

+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
| 1 | John   | Field-eng|  3500|
| 2 | Robert | Sales    |  4000|
| 3 | Maria  | Finance  |  3500|
| 4 | Michael| Sales    |  3000|
| 5 | Kelly  | Finance  |  3500|
| 6 | Kate   | Finance  |  3000|
| 7 | Martin | Finance  |  3500|
| 8 | Kiran  | Sales    |  2200|
+---+--------+----------+------+

dataframe.write()函数中,有一些参数我们可以在这里看到。第一个调用是parquet函数,用于定义文件类型。然后,作为下一个参数,我们指定了需要将这个 Parquet 文件写入的路径。

在下一个语句中,我们正在读取我们写入的相同文件以查看其内容。我们可以看到,生成的文件包含了我们写入的数据。

接下来,我们将探讨如何使用 Spark 读取和写入 ORC 文件。

读取和写入 ORC 文件

在本节中,我们将讨论 ORC 文件格式。与 Parquet 类似,ORC 也是一种列式和紧凑的文件格式,使得数据读取和写入非常高效。

让我们通过运行以下代码来学习如何使用 Spark 写入 ORC 文件:

salary_data_with_id.write.orc('salary_data.orc', mode='overwrite')
spark.read.orc(' /salary_data.orc').show()

生成的 DataFrame,命名为salary_data_with_id,看起来如下:

+---+--------+----------+------+
|ID |Employee|Department|Salary|
+---+--------+----------+------+
| 1 | John   | Field-eng|  3500|
| 2 | Robert | Sales    |  4000|
| 3 | Maria  | Finance  |  3500|
| 4 | Michael| Sales    |  3000|
| 5 | Kelly  | Finance  |  3500|
| 6 | Kate   | Finance  |  3000|
| 7 | Martin | Finance  |  3500|
| 8 | Kiran  | Sales    |  2200|
+---+--------+----------+------+

dataframe.write()函数中,有一些参数我们可以看到。第一个调用是orc函数,用于定义文件类型。然后,作为下一个参数,我们指定了需要将这个 Parquet 文件写入的路径。

在下一个语句中,我们正在读取我们写入的相同文件以查看其内容。我们可以看到,生成的文件包含了我们写入的相同数据。

接下来,我们将探讨如何使用 Spark 读取和写入 Delta 文件。

读取和写入 Delta 文件

Delta 文件格式是一种比 Parquet 和其他列式格式更优化的开放格式。当数据以 Delta 格式存储时,你会注意到底层文件是 Parquet 格式的。Delta 格式在 Parquet 文件之上添加了一个事务日志,使得数据读取和写入更加高效。

让我们通过运行以下代码来学习如何使用 Spark 读取和写入 Delta 文件:

salary_data_with_id.write.format("delta").save("/FileStore/tables/salary_data_with_id", mode='overwrite')
df = spark.read.load("/FileStore/tables/salary_data_with_id")
df.show()

生成的 DataFrame,命名为salary_data_with_id,看起来如下:

+---+--------+----------+------+
| ID|Employee|Department|Salary|
+---+--------+----------+------+
|  7|  Martin|   Finance|  3500|
|  4| Michael|     Sales|  3000|
|  6|    Kate|   Finance|  3000|
|  2|  Robert|     Sales|  4000|
|  1|    John| Field-eng|  3500|
|  5|   Kelly|   Finance|  3500|
|  3|   Maria|   Finance|  3500|
|  8|   Kiran|     Sales|  2200|
+---+--------+----------+------+

在这个示例中,我们将salary_data_with_id写入一个 Delta 文件。我们在format函数中添加了delta参数,之后将文件保存到某个位置。

在下一个语句中,我们正在将我们写入的相同 Delta 文件读取到一个名为df的 DataFrame 中。文件的内容与用于写入它的 DataFrame 保持一致。

现在我们知道了如何在 Spark 中使用高级操作来操作和合并数据,我们将探讨如何使用 SQL 与 Spark DataFrames 进行交互,以在 Python 和 SQL 之间切换语言。这为 Spark 用户提供了很大的权力,因为它允许他们根据用例和他们对不同语言的知识使用多种语言。

在 Spark 中使用 SQL

第二章中,我们讨论了 Spark Core 以及它是如何跨 Spark 的不同组件共享的。DataFrame 和 Spark SQL 也可以互换使用。我们还可以使用 DataFrame 中存储的数据进行 Spark SQL 查询。

以下代码演示了如何利用此功能:

salary_data_with_id.createOrReplaceTempView("SalaryTable")
spark.sql("SELECT count(*) from SalaryTable").show()

结果 DataFrame 看起来像这样:

+--------+
|count(1)|
+--------+
|       8|
+--------+

createOrReplaceTempView函数用于将 DataFrame 转换为名为SalaryTable的表。一旦完成此转换,我们就可以在此表上运行常规 SQL 查询。我们正在运行一个count *查询来计算表中的元素总数。

在下一节中,我们将了解 UDF 是什么以及如何在 Spark 中使用它。

Apache Spark 中的 UDFs

UDFs(用户定义函数)是 Apache Spark 中的一个强大功能,它允许你通过定义自定义函数来扩展 Spark 的功能。UDFs 对于以 Spark 内置函数不支持的方式转换和操作数据至关重要。在本节中,我们将深入探讨 UDFs 在 Spark 中的概念、实现和最佳实践。

什么是 UDFs?

UDFs 是由用户创建的用于在 Spark 中对数据进行特定操作的定制函数。UDFs 扩展了你可以应用于数据转换和操作的范围,使 Spark 在多样化的用例中更加灵活。

这里是 UDFs 的一些关键特性:

  • 用户自定义逻辑:UDFs 允许你将用户特定的逻辑或自定义算法应用于你的数据

  • 支持多种语言:Spark 支持用 Scala、Python、Java 和 R 等多种编程语言编写的 UDFs

  • 与 DataFrame 和弹性分布式数据集(RDD)的兼容性:UDFs 可以与 DataFrame 和 RDD 一起使用

  • 利用外部库:你可以在 UDFs 中使用外部库来执行高级操作

让我们看看如何创建 UDFs。

创建和注册 UDFs

要在 Spark 中使用 UDFs,你需要创建并注册它们。这个过程涉及定义一个函数并将其注册到 Spark 中。你可以为 SQL 和 DataFrame 操作定义 UDFs。在本节中,你将看到在 Spark 中定义 UDF 的基本语法,然后将其注册到 Spark 中。你可以在 UDF 中编写任何自定义 Python 代码以用于你的应用程序逻辑。第一个例子是 Python;下一个例子是 Scala。

在 Python 中创建 UDFs

我们可以使用以下代码在 Python 中创建一个 UDF:

from pyspark.sql.functions import udf
from pyspark.sql.types import IntegerType
# Define a UDF in Python
def my_udf_function(input_param):
# Your custom logic here
return processed_value
# Register the UDF with Spark
my_udf = udf(my_udf_function, IntegerType())
# Using the UDF in a DataFrame operation
df = df.withColumn("new_column", my_udf(df["input_column"]))

在 Scala 中创建 UDFs

我们可以使用以下代码在 Scala 中创建一个 UDF:

import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
// Define a UDF in Scala
val myUDF: UserDefinedFunction = udf((inputParam: InputType) => {
// Your custom logic here
processedValue }, OutputType)
// Using the UDF in a DataFrame operation
val df = df.withColumn("newColumn", myUDF(col("inputColumn")))

UDFs 的用例

UDFs 非常灵活,可以在广泛的应用场景中使用,包括但不限于以下内容:

  • 数据转换:应用自定义逻辑以转换数据,例如数据丰富、清洗和特征工程

  • 复杂计算:实现 Spark 标准函数中不可用的复杂数学或统计操作

  • 字符串操作:解析和格式化字符串、正则表达式和文本处理

  • 机器学习:在机器学习工作流程中创建用于特征提取、预处理或后处理的自定义函数

  • 特定领域逻辑:实现特定于您用例的特定领域相关逻辑

使用 UDF 的最佳实践

当在 Spark 中使用 UDF 时,请考虑以下最佳实践:

  • 避免性能瓶颈:UDF 可能会影响性能,尤其是在与大数据集一起使用时。分析并监控您的应用程序以识别性能瓶颈。

  • 最小化 UDF 复杂性:保持 UDF 简单和高效,以避免减慢您的 Spark 应用程序。复杂的操作可能导致更长的执行时间。

  • 检查数据类型兼容性:确保 UDF 的输出数据类型与列数据类型匹配,以避免错误和数据类型不匹配。

  • 优化数据处理:尽可能使用内置的 Spark 函数,因为它们针对分布式数据处理进行了高度优化。

  • 使用矢量化 UDF:在某些 Spark 版本中,可用的矢量化 UDF 可以通过一次处理多个值来显著提高 UDF 性能。

  • 测试和验证:在将 UDF(用户定义函数)应用于整个数据集之前,先在数据的小子集上彻底测试它们。确保它们产生预期的结果。

  • 记录 UDF:使用注释和描述记录您的 UDF,使您的代码更易于维护和理解他人。

在本节中,我们探讨了 Apache Spark 中 UDF(用户定义函数)的概念。UDF 是扩展 Spark 功能并执行自定义数据转换和操作的强大工具。当谨慎且高效地使用时,UDF 可以帮助您解决 Spark 中广泛的数据处理挑战。

现在我们已经涵盖了 Spark 的高级操作,我们将深入探讨 Spark 优化的概念。

Apache Spark 的优化

以其分布式计算能力而闻名的 Apache Spark 提供了一套高级优化技术,这些技术对于最大化性能、提高资源利用率和增强数据处理作业的效率至关重要。这些技术超越了基本优化,使用户能够针对最佳执行对 Spark 应用程序进行微调和优化。

理解 Spark 中的优化

Spark 中的优化旨在微调作业的执行以提高速度、资源利用率和整体性能。

以其强大的优化能力而闻名的 Apache Spark,显著提高了分布式数据处理任务的性能。在这个优化框架的核心是 Catalyst 优化器,这是一个关键组件,在提高查询执行效率方面发挥着至关重要的作用。这是在查询执行之前完成的。

Catalyst 优化器主要在查询编译期间生成的静态优化计划上工作。然而,AQE(在 Spark 3.0 中引入)是一种动态和自适应的方法,在运行时根据实际数据特性和执行环境优化查询计划。我们将在下一节中了解更多关于这两种范例的信息。

Catalyst 优化器

Catalyst 优化器是 Apache Spark 查询执行引擎的一个关键部分。它是一个强大的工具,使用高级技术来优化查询计划,从而提高 Spark 应用程序的性能。术语“催化剂”指的是它激发查询计划中的转换并使其更高效的能力。

让我们看看 Catalyst 优化器的一些关键特性:

  • 基于规则的优化:Catalyst 优化器采用一组规则和优化来转换和增强查询计划。这些规则涵盖了广泛的查询优化场景。

  • 逻辑和物理查询计划:它同时适用于逻辑和物理查询计划。逻辑计划表示查询的抽象结构,而物理计划概述了如何执行它。

  • 可扩展性:用户可以定义自定义规则和优化。这种可扩展性允许你根据特定的用例定制优化器。

  • 基于成本的优化:Catalyst 优化器可以评估不同查询计划的成本,并根据成本估计选择最有效的一个。这在处理复杂查询时特别有用。

让我们看看构成 Catalyst 优化器的不同组件。

Catalyst 优化器组件

要深入了解 Catalyst 优化器,必须检查其核心组件。

逻辑查询计划

逻辑查询计划表示查询的高级、抽象结构。它定义了你想要完成的事情,而不指定如何实现它。Spark 的 Catalyst 优化器与这个逻辑计划一起工作,以确定最佳物理计划。

基于规则的优化

基于规则的优化是 Catalyst 优化器的核心。它包括一组规则,将逻辑查询计划转换为一个更高效的版本。每个规则都专注于优化的一个特定方面,例如谓词下沉、常量折叠或列剪枝。

物理查询计划

物理查询计划定义了如何执行查询。一旦逻辑计划使用基于规则的技巧优化,它就被转换为一个物理计划,考虑到可用的资源和执行环境。这一阶段确保计划可以在分布式和并行方式下执行。

基于成本的优化

除了基于规则的优化外,Catalyst 优化器还可以使用基于成本的优化。它估计不同执行计划的成本,考虑数据分布、连接策略和可用资源等因素。这种方法有助于 Spark 根据实际的执行特征选择最有效的计划。

Catalyst 优化器在行动

为了见证 Catalyst 优化器的实际应用,让我们考虑一个使用 Spark SQL API 的实际示例。

在这个代码示例中,我们正在从 CSV 文件加载数据,应用选择操作以选择特定列,并根据条件过滤行。通过在生成的 DataFrame 上调用explain(),我们可以看到由 Catalyst 优化器生成的优化查询计划。输出提供了关于 Spark 将执行的物理执行步骤的见解:

# SparkSession setup
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("CatalystOptimizerExample").getOrCreate()
# Load data
df = spark.read.csv("/salary_data.csv", header=True, inferSchema=True) 
# Query with Catalyst Optimizer 
result_df = df.select("employee", "department").filter(df["salary"] > 3500) 
# Explain the optimized query plan 
result_df.explain() 

这个来自explain()方法的解释通常包括关于物理执行计划、特定优化使用以及查询执行所选策略的详细信息。

通过检查查询计划并理解 Catalyst 优化器如何增强它,你可以获得关于 Spark 优化引擎内部工作原理的宝贵见解。

本节提供了对 Catalyst 优化器、其组件和实际示例的坚实基础介绍。你可以在此基础上进一步深入研究基于规则和基于成本的优化技术,并讨论 Catalyst 优化器在查询性能上产生重大影响的实际场景。

接下来,我们将看到 AQE 如何将优化提升到 Spark 的下一个层次。

自适应查询执行(AQE)

Apache Spark,一个强大的分布式计算框架,提供了多种优化技术来增强数据处理作业的性能。其中一项高级优化功能是 AQE,这是一种动态方法,可以显著提高查询处理效率。

AQE 根据实际数据统计信息和硬件条件在运行时动态调整执行计划。它收集并利用运行时统计信息来优化连接策略、分区方法和广播操作。

让我们看看它的关键组件:

  • 运行时统计信息收集:AQE 在查询执行期间收集运行时统计信息,如数据大小、偏差和分区。

  • 自适应优化规则:它利用收集到的统计信息动态调整和优化连接策略、分区方法和广播操作。

现在,让我们考虑它的好处和重要性:

  • 性能提升:AQE 通过动态优化执行计划,显著提高了性能,从而实现了更好的资源利用和减少的执行时间。

  • 处理可变性:它在查询执行期间有效地处理数据大小、数据分布偏差和硬件条件的变化。

  • 高效资源利用:它实时优化查询计划,从而提高资源利用率和减少执行时间

AQE 工作流程

让我们看看 AQE 如何在 Spark 3.0 中优化工作流程:

  • 运行时统计收集:在查询执行期间,Spark 收集与数据分布、分区大小和连接键基数相关的统计信息

  • 自适应优化:利用收集到的统计信息,Spark 动态调整查询执行计划,优化连接策略、分区方法和数据重分布技术

  • 增强性能:自适应优化确保 Spark 适应不断变化的数据和运行时条件,从而提高查询性能和资源利用率

AQE 在 Apache Spark 中代表了查询优化的重要进步,它超越了静态规划,以适应运行时条件和数据特征。通过根据实时统计信息动态调整执行计划,它优化查询性能,确保大规模数据集的高效和可扩展处理。

接下来,我们将看到 Spark 如何进行基于成本的优化。

基于成本的优化

Spark 根据数据大小、连接操作和洗牌阶段等因素估计执行不同查询计划的成本。它利用成本估计来选择最有效的查询执行计划。

这里是其好处:

  • 最优计划选择:基于成本的优化在选择最经济的执行计划时考虑因素,如连接策略和数据分布

  • 性能提升:最小化不必要的洗牌和计算提高查询性能

接下来,我们将看到 Spark 如何利用内存管理和调整进行优化。

内存管理和调整

Spark 还应用了有效的内存分配策略,包括存储和执行内存,以避免不必要的溢出并提高处理效率。它微调垃圾收集设置以最小化中断并提高整体作业性能。

这里是其好处:

  • 降低开销:优化的内存使用最小化不必要的磁盘溢出,降低开销并提高作业性能

  • 稳定性和可靠性:调整垃圾收集设置提高稳定性并减少暂停,确保更一致的作业执行

包括 AQE、基于成本的优化、Catalyst 优化器和内存管理在内的高级 Spark 优化技术,在提高 Spark 作业性能、资源利用率和整体效率方面发挥着至关重要的作用。通过利用这些技术,用户可以优化 Spark 应用程序以满足不同的数据处理需求,并提高其可扩展性和性能。

到目前为止,我们已经看到了 Spark 如何内部优化其查询计划。然而,还有其他用户可以实施以进一步提高 Spark 性能的优化。接下来,我们将讨论一些这些优化。

Apache Spark 中的基于数据的优化

除了 Spark 的内部优化之外,我们还可以在实现方面做一些事情来使 Spark 更加高效。这些是用户控制的优化。如果我们了解这些挑战以及如何在现实世界的数据应用中处理它们,我们就可以充分利用 Spark 的分布式架构。

我们将从分布式框架中一个非常常见的问题——小文件问题开始讨论。

解决 Apache Spark 中的小文件问题

小文件问题在分布式计算框架(如 Apache Spark)中提出了重大挑战,因为它影响了性能和效率。当数据存储在众多小文件中而不是合并成大文件时,会导致开销增加和资源利用不充分。在本节中,我们将深入探讨 Spark 中小文件问题的含义,并探讨缓解其影响的有效解决方案。

与小文件问题相关的主要挑战如下:

  • 增加的元数据开销:将数据存储在众多小文件中会导致更高的元数据开销,因为每个文件都占用单独的块并产生额外的文件处理 I/O 操作。

  • 吞吐量降低:处理众多小文件效率较低,因为它涉及打开、读取和关闭文件的高开销,导致吞吐量降低。

  • 资源利用效率低下:Spark 的并行性依赖于数据分区,而小文件可能导致分区不足,资源利用率低,并阻碍并行处理。

既然我们已经讨论了主要挑战,让我们讨论一些缓解小文件问题的解决方案:

  • 文件连接或合并:将小文件合并成大文件可以显著缓解小文件问题。通过手动或自动化流程进行文件连接或合并等技术有助于减少单个文件的数量。

  • 文件压缩或合并:将小文件压缩或合并成更少、更实质性的文件的工具或流程可以简化数据存储。这种整合减少了元数据开销并提高了数据访问效率。

  • 文件格式优化:选择高效的文件格式,如 Parquet 或 ORC,这些格式支持列式存储和压缩,可以减少小文件的影响。这些格式便于高效的数据访问并减少存储空间。

  • 分区策略:在 Spark 的数据摄入或处理过程中应用适当的分区策略可以减轻小文件问题的影响。这涉及到将数据组织成更大的分区以提高并行性。

  • 数据预取或缓存:在处理之前将小文件预取或缓存到内存中可以最小化 I/O 开销。使用 Spark 的能力进行缓存或加载数据等技术可以提高性能。

  • AQE: 利用 Spark 的 AQE 功能可以帮助根据运行时统计信息优化查询计划。这可以在查询执行期间减轻小文件的影响。

  • 数据湖架构变更: 重新评估数据湖架构并采用最小化小文件创建的数据摄取策略,可以从源头上防止问题。

让我们来看看处理小文件的最佳实践:

  • 定期监控和清理: 实施定期监控和清理流程,以识别和合并随着时间的推移生成的小文件

  • 优化存储布局: 设计数据存储布局,在考虑块大小和文件系统设置等因素的同时,最大限度地减少小文件的创建。

  • 自动化流程: 使用自动化流程或工具来有效地合并和管理小文件,减少人工工作量。

  • 教育数据生产者: 教育数据生产者关于小文件的影响,并鼓励生成更大文件或优化文件创建的实践。

通过采用这些策略和最佳实践,组织可以有效地缓解 Apache Spark 中的小文件问题,确保性能提升、资源利用增强以及高效的数据处理能力。这些方法赋予用户克服小文件问题带来的挑战,并优化其 Spark 工作流程以实现最佳性能和可扩展性。

接下来,我们将看到数据倾斜如何影响 Spark 的性能。

解决 Apache Spark 中的数据倾斜问题

数据倾斜在分布式数据处理框架(如 Apache Spark)中是一个重大挑战,导致工作负载分布不均并阻碍并行性。

当某些键或分区持有的数据量显著多于其他分区时,就会发生数据倾斜。这种不平衡导致不同分区的处理时间不均,造成延迟。倾斜的数据分布可能导致某些工作节点过载,而其他节点利用率不足,导致资源分配效率低下。处理倾斜数据分区的任务需要更长的时间完成,从而造成作业执行延迟并影响整体性能。

这里有一些我们可以用来解决数据倾斜的解决方案:

  • 分区技术:

    • Salting: 通过向键添加盐来引入随机性,以更均匀地分布数据到分区中。这有助于防止热点并平衡工作负载。

    • 自定义分区: 通过不同地分组键来实现自定义分区逻辑,以重新分配倾斜数据,确保分区之间分布更加平衡。

  • 倾斜感知算法: 利用诸如倾斜连接优化等技术,这些技术将倾斜键与常规连接分开处理,更有效地重新分配和处理它们。

  • 复制小倾斜数据: 在多个节点上复制小倾斜分区,以并行处理并减轻单个节点的负载

  • AQE:利用 Spark 的 AQE 功能根据运行时统计信息动态调整执行计划,减轻数据偏差的影响

  • 采样和过滤:应用采样和过滤技术事先识别偏差数据分区,允许在处理过程中主动处理偏差键

  • 动态资源分配:实现动态资源分配,为处理偏差数据分区的任务分配额外资源,优化资源利用

让我们讨论处理数据偏差的最佳实践:

  • 定期分析:持续分析并监控数据分布,以在处理管道早期识别并解决偏差问题

  • 优化分区:根据数据特性选择合适的分区策略,以防止或减轻数据偏差

  • 分布式处理:利用分布式处理框架将偏差数据分布到多个节点以实现并行执行

  • 任务重试机制:为处理偏差数据的任务实现重试机制,以适应潜在的延迟并避免作业失败

  • 数据预处理:在数据处理前应用预处理技术以减轻偏差,确保更均衡的工作负载

通过采用这些策略和最佳实践,组织可以有效应对 Apache Spark 中的数据偏差,确保更均衡的工作负载、改进的资源利用,并在分布式数据处理工作流程中提高整体性能。这些方法赋予用户克服数据偏差带来的挑战,并优化 Spark 应用以实现高效和可扩展的数据处理。

在 Apache Spark 中解决数据偏差对于优化性能和确保分布式计算环境中的资源高效利用至关重要。通过理解数据偏差的原因和影响,并采用缓解策略,用户可以显著提高 Spark 作业的效率和可靠性,减轻数据偏差的负面影响。

在下一节中,我们将讨论 Spark 中的数据溢出以及如何管理它们。

在 Apache Spark 中管理数据溢出

数据溢出,这是在 Apache Spark 等分布式处理框架中经常遇到的问题,当正在处理的数据超过可用内存容量时发生,导致数据被写入磁盘。这种现象可能会显著影响性能和整体效率。在本节中,我们将深入探讨 Spark 中数据溢出的影响以及缓解其影响的有效策略,以优化数据处理。

数据溢出发生在 Spark 的内存容量超出时,导致过多的数据写入磁盘操作,这比内存操作慢得多。将数据写入磁盘会产生高 I/O 开销,由于延迟增加,导致处理性能显著下降。数据溢出可能导致资源竞争,因为磁盘操作与其他计算任务竞争,导致资源利用效率低下。

这里是我们可以实施的解决数据溢出的一些方案:

  • 内存管理技术

    • 增加执行器内存:为 Spark 执行器分配更多内存可以帮助减少数据溢出的可能性,通过在内存中容纳更大的数据集来实现

    • 调整内存配置:优化 Spark 的内存配置,例如调整存储和执行内存的分数,以更好地利用可用内存

  • 分区和缓存策略

    • 重新分区:将数据重新分区到最佳分区数,可以帮助管理内存使用并最小化数据溢出,确保节点间更好的数据分布

    • 缓存中间结果:在内存中缓存或持久化中间数据集可以防止重复计算,并减少后续操作中数据溢出的可能性

  • 高级优化技术

    • Shuffle 调整:通过调整如 shuffle 分区和缓冲区大小等参数来调整 shuffle 操作,以降低在 shuffle 阶段数据溢出的可能性

    • 数据压缩:在内存或磁盘上存储中间数据时利用数据压缩技术,以减少存储占用并缓解内存压力

  • AQE:利用 Spark 的 AQE 能力,根据运行时统计信息动态调整执行计划,优化内存使用并减少溢出。

  • 处理任务和数据倾斜:应用技术来减轻任务和数据倾斜。倾斜的数据会加剧内存压力并增加数据溢出的可能性。

这里是处理数据溢出的最佳实践:

  • 资源监控:定期监控内存使用和资源分配,以识别和预防潜在的数据溢出问题

  • 优化数据结构:利用优化的数据结构和格式(如 Parquet 或 ORC)以减少内存开销和存储需求

  • 高效的缓存策略:策略性地缓存或持久化中间结果,以最小化重复计算并降低数据溢出的概率

  • 增量处理:采用增量处理技术,以可管理的块处理大数据集,减少内存压力

通过采用这些策略和最佳实践,组织可以有效管理 Apache Spark 中的数据溢出,确保高效内存利用、优化处理性能,并在分布式数据处理工作流程中提高整体可伸缩性。这些方法使用户能够主动解决数据溢出挑战,并优化 Spark 应用程序以提高效率和性能。

在下一节中,我们将讨论什么是数据洗牌以及如何处理它以优化性能。

在 Apache Spark 中管理数据洗牌

数据洗牌,在 Apache Spark 等分布式处理框架中的基本操作,涉及在集群节点间移动数据。虽然洗牌操作对于各种转换,如连接和聚合,是必不可少的,但它们也可能引入性能瓶颈和资源开销。在本节中,我们将探讨 Spark 中数据洗牌的影响以及优化和减轻其影响的有效策略,以实现高效的数据处理。

数据洗牌涉及大量的网络和磁盘 I/O 操作,导致延迟增加和资源利用率提高。在节点间移动大量数据可能会因为数据移动和处理过多而引入性能瓶颈。密集的洗牌操作可能导致节点间的资源竞争,影响整体集群性能。

让我们讨论优化数据洗牌的解决方案:

  • 数据分区技术:实施优化的数据分区策略以减少洗牌开销,确保更平衡的工作负载分配

  • 倾斜处理:通过采用盐分或自定义分区等技术来减轻数据倾斜,防止热点并平衡数据分布

  • 洗牌分区调整:根据数据特性和作业要求调整洗牌分区的数量,以优化洗牌性能并减少开销

  • 内存管理:优化洗牌操作的内存分配,以最小化数据溢出到磁盘并提高整体洗牌性能

  • 数据过滤和修剪:应用过滤或修剪技术以减少节点间洗牌的数据量,仅关注相关数据子集

  • 连接优化

    • 广播连接:对于较小的数据集,利用广播连接在节点间复制数据,最小化数据洗牌并提高连接性能

    • 排序合并连接:对于大型数据集,采用排序合并连接算法以最小化连接操作中的数据移动

  • AQE:利用 Spark 的 AQE 功能,根据运行时统计和数据分布动态优化洗牌操作

管理数据洗牌的最佳实践如下:

  • 分析和监控:持续分析和监控洗牌操作,以识别瓶颈并优化配置

  • 优化的分区大小: 根据数据特性确定最佳分区大小,并相应地调整 shuffle 分区

  • 缓存和持久化: 缓存或持久化中间 shuffle 结果以减少重复计算并减轻 shuffle 开销

  • 常规调优: 根据工作负载需求和集群资源,定期调整与 shuffle 操作相关的 Spark 配置

通过实施这些策略和最佳实践,组织可以有效地优化 Apache Spark 中的数据 shuffle 操作,确保性能提升、减少资源竞争,并提高分布式数据处理工作流程的整体效率。这些方法赋予用户主动管理和优化 shuffle 操作的能力,以实现数据处理的简化并提高集群性能。

尽管用户需要了解所有与数据相关的挑战,但 Spark 在其内部工作中有某些类型的 join 可供我们利用以获得更好的性能。我们将接下来查看这些内容。

Shuffle 和广播 join

Apache Spark 提供了两种基本方法来执行 join 操作:shuffle join 和广播 join。每种方法都有其优势和用例,了解何时使用它们对于优化 Spark 应用程序至关重要。请注意,这些 join 操作是由 Spark 自动执行的,以将不同的数据集连接在一起。您可以在代码中强制执行某些 join 类型,但 Spark 负责执行。

Shuffle joins

Shuffle join 是分布式计算环境中连接大型数据集的常用方法。这些 join 将数据重新分配到分区中,确保匹配的键最终位于同一 worker 节点上。Spark 通过其底层执行引擎高效地执行 shuffle join。

下面是 shuffle join 的一些关键特性:

  • 数据重分布: Shuffle join 重新分配数据以确保具有匹配键的行位于同一 worker 节点上。此过程可能需要大量的网络和磁盘 I/O。

  • 适用于大型数据集: Shuffle join 非常适合连接大小相当的大型 DataFrame。

  • 数据复制: 在 shuffle join 过程中,数据可能会在 worker 节点上临时复制,以方便高效地执行 join 操作。

  • 网络和磁盘 I/O 成本高昂: 由于数据 shuffle,shuffle join 可能非常消耗资源,这使得它们与其他 join 技术相比在处理较小数据集时速度较慢。

  • 示例: 内连接、左连接、右连接和全外连接通常作为 shuffle join 实现。

用例

Shuffle joins 通常用于连接两个大小没有显著差异的大型 DataFrame。

Shuffle sort-merge joins

Shuffle sort-merge join 是一种 shuffle join,它利用排序和合并技术的组合来执行 join 操作。它根据 join 键对两个 DataFrame 进行排序,然后高效地合并它们。

下面是洗牌排序合并连接的一些关键特性:

  • 数据排序:洗牌排序合并连接在两边对数据进行排序,以确保有效的合并

  • 适用于大型数据集:它们在连接具有倾斜数据分布的大型 DataFrame 时效率很高

  • 复杂性:这种类型的洗牌连接比简单的洗牌连接更复杂,因为它涉及到排序操作

用例

洗牌排序合并连接对于大规模连接非常有效,尤其是在数据分布不均匀时,确保数据在分区之间平衡分布至关重要。

让我们来看看广播连接。

广播连接

广播连接是将小 DataFrame 与较大 DataFrame 连接的一种高度有效技术。在这种方法中,较小的 DataFrame 被广播到所有工作节点,消除了在网络中洗牌数据的需求。广播连接是一种特定的优化技术,可以在其中一个 DataFrame 足够小,可以放入内存时应用。在这种情况下,小 DataFrame 被广播到所有工作节点,避免了昂贵的洗牌。

让我们来看看广播连接的一些关键特性:

  • 小 DataFrame 广播:较小的 DataFrame 被广播到所有工作节点,确保它可以在本地使用

  • 减少网络开销:广播连接显著减少了网络和磁盘 I/O,因为它们避免了数据洗牌

  • 适用于维度表:广播连接在将事实表与较小的维度表连接时常用,例如在数据仓库场景中

  • 适用于小到大型连接:它们在其中一个 DataFrame 远小于另一个 DataFrame 的连接操作中效率很高

用例

当你将一个大型 DataFrame 与一个远小于它的 DataFrame 连接时,广播连接非常有用,例如在数据仓库中将事实表与维度表连接。

广播哈希连接

广播哈希连接是一种特定的广播连接。在这种情况下,较小的 DataFrame 被广播为一个哈希表到所有工作节点,这允许在大 DataFrame 中进行有效的查找。

用例

广播哈希连接适用于以下场景:其中一个 DataFrame 足够小,可以广播,并且需要执行基于等式的连接。

在本节中,我们讨论了 Spark 中的两种基本连接技术——洗牌连接和广播连接——包括特定的变体,如广播哈希连接和洗牌排序合并连接。选择正确的连接方法取决于你的 DataFrame 的大小、数据分布和网络考虑因素,并且做出明智的决定对于优化你的 Spark 应用程序至关重要。在下一节中,我们将介绍 Spark 中存在的不同类型的转换。

Apache Spark 中的窄和宽转换

第三章所述,变换是处理数据的核心操作。变换分为两大类:窄变换和宽变换。理解这两种变换之间的区别对于优化 Spark 应用程序的性能至关重要。

窄变换

窄变换是一种不需要数据洗牌或跨分区进行大量数据移动的操作。它们可以在单个分区上执行,无需与其他分区通信。这种固有的局部性使得窄变换非常高效且执行速度快。

以下是一些窄变换的关键特性:

  • 单分区处理:窄变换独立地对数据的单个分区进行操作,这最小化了通信开销。

  • 速度和效率:由于它们的分区特性,窄变换既快又高效。

map()filter()union()groupBy() 是窄变换的典型例子。

宽变换

相比之下,宽变换涉及数据洗牌,这需要分区之间的数据交换。这些变换需要多个分区之间的通信,并且可能非常资源密集。因此,它们通常执行速度较慢,在计算方面成本更高。

下面是宽变换的一些关键特性:

  • 数据洗牌:宽变换涉及跨分区重新组织数据,需要不同工作者之间的数据交换。

  • 执行速度较慢:由于需要洗牌,宽变换相对于窄变换来说,执行速度较慢且资源密集。

groupByKey()reduceByKey()join() 是宽变换的常见例子。

让我们讨论根据操作选择哪种变换效果最好。

选择窄变换和宽变换

选择合适的变换类型取决于具体用例和现有数据。以下是选择窄变换和宽变换时的一些考虑因素:

  • 数据大小:如果你的数据足够小,可以舒适地放入单个分区,那么使用窄变换是首选。这可以最小化与洗牌相关的开销。

  • 数据分布:如果你的数据在分区之间分布不均匀,可能需要宽变换来重新组织和平衡数据。

  • 性能:窄变换通常更快、更高效,因此如果性能是关键关注点,则更倾向于选择它们。

  • 复杂操作:一些操作,例如连接大型 DataFrame,通常需要宽变换。在这种情况下,性能权衡是不可避免的。

  • 集群资源:考虑可用的集群资源。资源密集型的广泛转换可能导致共享集群中的资源争用。

接下来,我们将学习如何在必要时优化广泛转换。

优化广泛转换

虽然对于某些操作来说,广泛的转换是必要的,但优化它们以减少对性能的影响至关重要。以下是一些优化广泛转换的策略:

  • 最小化数据洗牌:尽可能使用技术来最小化数据洗牌。例如,考虑使用广播连接来处理小的数据框。

  • 分区:仔细选择分区数量和分区键以确保数据均匀分布,减少大量洗牌的需求。

  • 缓存和持久化:缓存频繁使用的 DataFrame 可以帮助减少后续阶段中重新计算和洗牌的需求。

  • 调整集群资源:调整集群配置,例如执行器和内存分配的数量,以满足广泛转换的需求。

  • 分析和监控:定期分析和监控您的 Spark 应用程序以识别性能瓶颈,尤其是在广泛转换的情况下。

在本节中,我们探讨了 Apache Spark 中窄转换和广泛转换的概念。理解何时以及如何使用这些转换对于优化 Spark 应用程序的性能至关重要,尤其是在处理大型数据集和复杂操作时。

在下一节中,我们将介绍 Spark 中的持久化和缓存操作。

Spark 中的持久化和缓存

在 Apache Spark 中,优化数据处理操作的性能至关重要,尤其是在处理大型数据集和复杂工作流时。缓存和持久化是允许您在内存或磁盘上存储中间或频繁使用数据的技术,从而减少重新计算的需求并提高整体性能。本节探讨了 Spark 中持久化和缓存的概念。

理解数据持久化

数据持久化是将 Spark 转换的中间或最终结果存储在内存或磁盘上的过程。通过持久化数据,您可以减少从源数据重新计算的需求,从而提高查询性能。

以下关键概念与数据持久化相关:

  • 存储级别:Spark 提供多种数据存储级别,从仅内存到磁盘,根据您的需求而定。每个存储级别都带来了速度和持久性方面的权衡。

  • 延迟评估:Spark 遵循延迟评估模型,这意味着转换只有在调用操作时才会执行。数据持久化确保中间结果可用于重用,而无需重新计算。

  • 缓存与持久化:缓存是一种特定形式的数据持久化,它将数据存储在内存中,而持久化则包括内存和磁盘上的存储。

缓存数据

缓存是一种数据持久化形式,它将 DataFrame、RDD 或数据集存储在内存中以实现快速访问。它是一种重要的优化技术,可以提高 Spark 应用程序的性能,尤其是在处理迭代算法或重复计算时。

要缓存 DataFrame 或 RDD,您可以在指定存储级别时使用.cache().persist()方法:

  • .cache().persist(StorageLevel.MEMORY_ONLY).

  • .persist(StorageLevel.MEMORY_ONLY_SER).

  • .persist(StorageLevel.MEMORY_AND_DISK).

  • .persist(StorageLevel.DISK_ONLY).

缓存在以下场景中特别有益:

  • 迭代算法:缓存对于迭代算法至关重要,如机器学习、图处理和优化问题,在这些算法中,相同的数据被反复使用

  • 多个操作:当 DataFrame 用于多个操作时,在第一次操作后缓存它可以提高性能

  • 避免重复计算:缓存有助于避免在多个转换依赖于它时重复计算相同的数据

  • 交互式查询:在交互式数据探索或查询中,缓存常用的中间结果可以加快临时分析

取消持久化数据

缓存会消耗内存,在集群环境中,高效管理内存至关重要。您可以使用.unpersist()方法从内存中释放缓存数据。此方法允许您指定是立即释放数据还是仅在不再需要时释放。

下面是一个取消持久化数据的示例:

# Cache a DataFrame
df.cache()
# Unpersist the cached DataFrame
df.unpersist()

最佳实践

要在您的 Spark 应用程序中有效地使用缓存和持久化,请考虑以下最佳实践:

  • 仅缓存必要的数据:缓存会消耗内存,因此仅缓存频繁使用或计算成本高的数据

  • 监控内存使用:定期监控内存使用情况,以避免内存耗尽或磁盘溢出

  • 自动化取消持久化:如果您有有限的内存资源,请自动化较少使用的数据的取消持久化,以释放内存用于更关键的操作

  • 考虑序列化:根据您的用例,考虑使用序列化存储级别以减少内存开销

在本节中,我们探讨了 Apache Spark 中持久化和缓存的概念。缓存和持久化是优化 Spark 应用程序性能的强大技术,尤其是在处理迭代算法或重复使用相同数据的场景中。了解何时以及如何使用这些技术可以显著提高您数据处理工作流程的效率。

在下一节中,我们将学习如何在 Spark 中实现分区和合并。

Apache Spark 中的分区和合并

在 Apache Spark 中优化数据处理工作流程时,高效的数据分区起着至关重要的作用。重新分区和合并是允许您控制数据在分区间分布的操作。在本节中,我们将探讨重新分区和合并的概念及其在 Spark 应用程序中的重要性。

理解数据分区

Apache Spark 中的数据分区涉及将数据集划分为更小、更易于管理的单元,称为分区。每个分区包含数据的一个子集,并由分布式集群中不同的工作节点独立处理。适当的数据分区可以显著影响 Spark 应用程序的效率和性能。

重新分区数据

重新分区是将数据重新分配到不同数量的分区中的过程。这个操作可以帮助平衡数据分布,提高并行性,并优化数据处理。您可以使用.repartition()方法来指定所需的分区数。

下面是关于重新分区数据的一些关键点:

  • 增加或减少分区:重新分区允许您根据处理需求增加或减少分区数。

  • 数据洗牌:重新分区通常涉及数据洗牌,这可能非常消耗资源。因此,应谨慎使用。

  • 均匀数据分布:当原始数据在分区中分布不均匀,导致工作负载倾斜时,重新分区很有用。

  • 优化连接操作:在执行连接操作时,重新分区可以有益于最小化数据洗牌。

下面是重新分区数据的示例:

# Repartition a DataFrame into 8 partitions
df.repartition(8)

合并数据

合并是在保持数据局部性的同时减少分区数量的过程。它比重新分区更高效,因为它尽可能避免不必要的洗牌操作。您可以使用.coalesce()方法来指定目标分区数。

下面是关于合并数据的一些关键点:

  • 减少分区:当您想减少分区数量以优化数据处理时,使用合并。

  • 最小化数据移动:与重新分区不同,合并通过尽可能在本地合并分区来最小化洗牌操作。

  • 高效的数据减少:当您需要减少分区数量而不承担数据洗牌的全部成本时,合并操作是高效的。

下面是合并数据的示例:

# Coalesce a DataFrame to 4 partitions
df.coalesce(4)

重新分区和合并的使用案例

理解何时重新分区和合并对于优化您的 Spark 应用程序至关重要。

以下是一些重新分区的用例:

  • 数据倾斜:当数据在分区中倾斜时,重新分区可以帮助平衡工作负载。

  • 连接优化:通过确保连接键是本地化的来优化连接操作。

  • 并行度控制:调整并行度以优化资源利用率。

现在,让我们看看合并的一些用例:

  • 减少数据: 当你需要减少分区数量以节省内存和降低开销时

  • 最小化洗牌: 为了避免不必要的洗牌并最小化网络通信

  • 后过滤: 在应用过滤器或转换显著减少数据集大小之后

最佳实践

为了在 Spark 应用程序中有效地重新分区和合并,请考虑以下最佳实践:

  • 分析和监控: 分析你的应用程序以识别与数据分区相关的性能瓶颈。使用 Spark 的 UI 和监控工具跟踪数据洗牌。

  • 考虑数据大小: 在决定分区数量时,考虑你的数据集大小和可用的集群资源。

  • 平衡工作负载: 旨在使分区之间的工作负载分布平衡,以优化并行性。

  • 尽可能合并: 在减少分区数量时,优先选择合并而不是重新分区,以最小化数据洗牌。

  • 规划连接: 在执行连接操作时,规划最佳分区数量以最小化洗牌开销。

在本节中,我们探讨了 Apache Spark 中的重新分区和合并的概念。了解如何有效地控制数据分区可以显著影响 Spark 应用程序的性能,尤其是在处理大型数据集和复杂操作时。

摘要

在本章中,我们深入探讨了 Apache Spark 的高级数据处理功能,增强了你对关键概念和技术理解。我们探讨了 Spark 的 Catalyst 优化器的复杂性,不同类型 Spark 连接的力量,数据持久化和缓存的重要性,窄转换和宽转换的意义,以及使用重新分区和合并进行数据分区的作用。此外,我们还发现了 UDFs 的灵活性和实用性。

随着你使用 Apache Spark 的旅程不断深入,这些高级功能将证明对优化和定制数据处理工作流程非常有价值。通过利用 Catalyst 优化器的潜力,你可以微调查询执行以改善性能。了解 Spark 连接的细微差别使你能够针对特定用例做出明智的决定选择哪种类型的连接。当你寻求减少重复计算和加速迭代过程时,数据持久化和缓存变得不可或缺。

窄转换和宽转换在 Spark 应用程序中实现所需的并行性和资源效率中起着关键作用。通过重新分区和合并进行适当的数据分区确保了平衡的工作负载和最佳的数据分布。

UDFs(用户定义函数)打开了无限可能的大门,使你能够实现自定义数据处理逻辑,从数据清洗和特征工程到复杂计算和特定领域的操作。然而,明智地使用 UDFs 至关重要,优化它们以获得性能,并遵循最佳实践。

通过本章的知识,你将更好地应对 Apache Spark 中的复杂数据处理挑战,使你能够高效且有效地从数据中提取有价值的见解。这些高级功能使你能够充分利用 Spark 的潜力,并在数据驱动的努力中实现最佳性能。

在下一章中,我们将介绍 SparkSQL,并学习如何在 Spark 中创建和操作 SQL 查询。

样题问题

问题 1:

以下哪个代码块返回了一个 DataFrame,显示了按department列分组的df DataFrame 中salary列的平均值?

  1. df.groupBy("department").agg(avg("salary"))

  2. df.groupBy(col(department).avg())

  3. df.groupBy("department").avg(col("salary"))

  4. df.groupBy("department").agg(average("salary"))

问题 2:

以下哪个代码块返回了dfstatedepartment列的所有唯一值?

  1. df.select(state).join(transactionsDf.select('department'), col(state) == col('department'), 'outer').show()

  2. df.select(col('state'), col('department')).agg({'*': 'count'}).show()

  3. df.select('state', 'department').distinct().show()

  4. df.select('state').union(df.select('department')).distinct().show()

答案

  1. A

  2. D

第六章:Spark SQL 中的 SQL 查询

在本章中,我们将探索 Spark SQL 在结构化数据处理方面的广泛功能。我们将深入了解加载数据、操作数据、执行 SQL 查询、执行高级分析和将 Spark SQL 与外部系统集成。到本章结束时,您将深入了解 Spark SQL 的功能,并具备利用其在数据处理任务中发挥其强大功能的知识。

我们将涵盖以下主题:

  • 什么是 Spark SQL?

  • Spark SQL 入门

  • 高级 Spark SQL 操作

什么是 Spark SQL?

Spark SQL 是 Apache Spark 生态系统中的一个强大模块,它允许高效地处理和分析结构化数据。它提供了一个比 Apache Spark 传统的基于 RDD 的 API 更高级的接口来处理结构化数据。Spark SQL 结合了关系和过程处理的优势,使用户能够无缝地将 SQL 查询与复杂分析集成。通过利用 Spark 的分布式计算能力,Spark SQL 实现了可扩展和高效的数据处理。

它提供了一个编程接口,使用 SQL 查询、DataFrame API 和 Dataset API 来处理结构化数据。

它允许用户使用类似 SQL 的语法查询数据,并为在大型数据集上执行 SQL 查询提供强大的引擎。Spark SQL 还支持从各种结构化数据源(如 Hive 表、Parquet 文件和 JDBC 数据库)读取和写入数据。

Spark SQL 的优势

Spark SQL 提供了几个关键优势,使其成为结构化数据处理的热门选择:

使用 Spark SQL 进行统一数据处理

使用 Spark SQL,用户可以使用单个引擎处理结构化和非结构化数据。这意味着用户可以使用相同的编程接口查询存储在不同格式(如 JSON、CSV 和 Parquet)中的数据。

用户可以在 SQL 查询、DataFrame 转换和 Spark 的机器学习 API 之间无缝切换。这种统一的数据处理方法使得在单个应用程序中集成不同的数据处理任务变得更加容易,从而降低了开发复杂性。

性能和可扩展性

Spark SQL 利用 Apache Spark 的分布式计算能力,在机器集群上处理大规模数据集。它使用高级查询优化技术,如 Catalyst 优化器(在第五章中详细讨论),来优化和加速查询执行。此外,Spark SQL 支持数据分区和缓存机制,进一步提高了性能和可扩展性。

Spark SQL 使用优化的执行引擎,可以比传统的 SQL 引擎更快地处理查询。它通过使用内存缓存和优化的查询执行计划来实现这一点。

Spark SQL 被设计为可以在机器集群上水平扩展。它可以通过将数据集分区到多台机器上并并行处理来处理大型数据集。

与现有基础设施的无缝集成

Spark SQL 与现有的 Apache Spark 基础设施和工具无缝集成。它与其他 Spark 组件(如 Spark Streaming 用于实时数据处理和 Spark MLlib 用于机器学习任务)提供互操作性。此外,Spark SQL 与流行的存储系统和数据格式(包括 Parquet、Avro、ORC 和 Hive)集成,使其与广泛的数据源兼容。

高级分析功能

Spark SQL 通过使用高级分析功能扩展了传统的 SQL 功能。它支持窗口函数,使用户能够执行复杂的分析操作,例如排名、滑动窗口上的聚合和累积聚合。与 Spark 中的机器学习库的集成允许预测分析和数据科学工作流程的无缝集成。

易用性

Spark SQL 提供了一个简单的编程接口,允许用户使用类似 SQL 的语法查询数据。这使得熟悉 SQL 的用户能够轻松开始使用 Spark SQL。

与 Apache Spark 的集成

Spark SQL 是 Apache Spark 框架的组成部分,与 Spark 的其他组件无缝协作。它利用 Spark 的核心功能,如容错、数据并行和分布式计算,以提供可扩展和高效的数据处理。Spark SQL 可以从各种来源读取数据,包括分布式文件系统(如 HDFS)、对象存储(如 Amazon S3)和关系数据库(通过 JDBC)。它还与外部系统(如 Hive)集成,使用户能够利用现有的 Hive 元数据和查询。

现在,让我们看看 Spark SQL 的一些基本结构。

关键概念 – DataFrame 和 Dataset

Spark SQL 引入了两个基本抽象,用于处理结构化数据:DataFrame 和 Dataset。

DataFrame

DataFrame 表示组织成命名列的分布式数据集合。它们提供了高级接口,用于处理结构化数据,并提供了丰富的 API 用于数据操作、过滤、聚合和查询。DataFrame 是不可变的,并且是惰性评估的,通过 Spark 的 Catalyst 优化器实现优化执行计划。它们可以从各种数据源创建,包括结构化文件(CSV、JSON 和 Parquet)、Hive 表和现有的 RDD。

Dataset

Dataset 是 DataFrame 的扩展,提供了一种类型安全、面向对象的编程接口。Dataset 结合了 Spark 的 RDD(强类型和用户定义函数)的优点与 DataFrame 的性能优化。Dataset 允许编译时类型检查,并且可以无缝转换为 DataFrame,从而实现灵活高效的数据处理。

现在我们已经知道了 DataFrame 和 Dataset 是什么,我们将在下一节中看到如何将这些结构应用于不同的 Spark SQL 操作。

开始使用 Spark SQL

要开始使用 Spark SQL 操作,我们首先需要将数据加载到 DataFrame 中。我们将在下一节中看到如何做到这一点。然后,我们将看到如何在不同数据之间切换 PySpark 和 Spark SQL,并对它应用不同的转换。

加载数据和保存数据

在本节中,我们将探索从不同来源将数据加载到 Spark SQL 中并保存为表的各种技术。我们将深入研究 Python 代码示例,演示如何有效地将数据加载到 Spark SQL 中,执行必要的转换,并将处理后的数据保存为表以供进一步分析。

在 Spark SQL 中执行 SQL 查询使我们能够利用熟悉的 SQL 语法并利用其表达力。让我们看看执行 SQL 查询的语法和示例:

在 Spark SQL 中执行 SQL 查询,我们使用 spark.sql() 方法,如下所示:

results = spark.sql("SELECT * FROM tableName")
  • spark.sql() 方法用于在 Spark SQL 中执行 SQL 查询

  • 在方法内部,我们提供 SQL 查询作为字符串参数。在这个例子中,我们从 tableName 表中选择所有列。

  • 查询的结果存储在 results 变量中,可以进一步处理或按需显示。

为了开始本章的代码示例,我们将使用在 第四章 中创建的 DataFrame。

salary_data_with_id = [(1, "John", "Field-eng", 3500, 40), \
    (2, "Robert", "Sales", 4000, 38), \
    (3, "Maria", "Finance", 3500, 28), \
    (4, "Michael", "Sales", 3000, 20), \
    (5, "Kelly", "Finance", 3500, 35), \
    (6, "Kate", "Finance", 3000, 45), \
    (7, "Martin", "Finance", 3500, 26), \
    (8, "Kiran", "Sales", 2200, 35), \
  ]
columns= ["ID", "Employee", "Department", "Salary", "Age"]
salary_data_with_id = spark.createDataFrame(data = salary_data_with_id, schema = columns)
salary_data_with_id.show()

输出结果如下:

+---+--------+----------+------+---+
| ID|Employee|Department|Salary|Age|
+---+--------+----------+------+---+
|  1|    John| Field-eng|  3500| 40|
|  2|  Robert|     Sales|  4000| 38|
|  3|   Maria|   Finance|  3500| 28|
|  4| Michael|     Sales|  3000| 20|
|  5|   Kelly|   Finance|  3500| 35|
|  6|    Kate|   Finance|  3000| 45|
|  7|  Martin|   Finance|  3500| 26|
|  8|   Kiran|     Sales|  2200| 35|
+---+--------+----------+------+---+

我在这个 DataFrame 中添加了一个 Age 列以进行进一步处理。

记住,在 第四章 中,我们将这个 DataFrame 保存为 CSV 文件。我们用于读取 CSV 文件的代码片段可以在以下代码中看到。

如你所回忆的,我们使用以下代码行用 Spark 编写 CSV 文件:

salary_data_with_id.write.format("csv").mode("overwrite").option("header", "true").save("salary_data.csv")

输出结果如下:

+---+--------+----------+------+---+
| ID|Employee|Department|Salary|Age|
+---+--------+----------+------+---+
|  1|    John| Field-eng|  3500| 40|
|  2|  Robert|     Sales|  4000| 38|
|  3|   Maria|   Finance|  3500| 28|
|  4| Michael|     Sales|  3000| 20|
|  5|   Kelly|   Finance|  3500| 35|
|  6|    Kate|   Finance|  3000| 45|
|  7|  Martin|   Finance|  3500| 26|
|  8|   Kiran|     Sales|  2200| 35|
+---+--------+----------+------+---+

现在我们有了 DataFrame,我们可以使用 SQL 操作来处理它:

# Perform transformations on the loaded data
processed_data = csv_data.filter(csv_data["Salary"] > 3000)
# Save the processed data as a table
processed_data.createOrReplaceTempView("high_salary_employees")
# Perform SQL queries on the saved table
results = spark.sql("SELECT * FROM high_salary_employees ")
results.show()

输出结果如下:

+---+--------+----------+------+---+
| ID|Employee|Department|Salary|Age|
+---+--------+----------+------+---+
|  1|    John| Field-eng|  3500| 40|
|  2|  Robert|     Sales|  4000| 38|
|  3|   Maria|   Finance|  3500| 28|
|  5|   Kelly|   Finance|  3500| 35|
|  7|  Martin|   Finance|  3500| 26|
+---+--------+----------+------+---+

上述代码片段展示了如何对加载的数据执行转换。在这种情况下,我们过滤数据,只包括 Salary 列大于 3,000 的行。

通过使用 filter() 函数,我们可以应用特定条件来选择所需的数据子集。

转换后的数据将存储在 results 变量中,并准备好进行进一步分析。

将转换后的数据保存为视图

一旦我们完成了必要的转换,通常将处理后的数据保存为视图以便于访问和未来的分析是有用的。让我们看看如何在 Spark SQL 中实现这一点:

createOrReplaceTempView() 方法允许我们将处理后的数据作为 Spark SQL 中的视图保存。我们为视图提供名称,在本例中为 high_salary_employees

通过给表赋予一个有意义的名称,我们可以在后续的操作和查询中轻松引用它。保存的表作为处理数据的结构化表示,便于进一步分析和探索。

将转换后的数据保存为表格后,我们可以利用 SQL 查询的功能来获取洞察力并提取有价值的信息。

通过使用spark.sql()方法,我们可以对保存的视图high_salary_employees执行 SQL 查询。

在前面的示例中,我们执行了一个简单的查询,根据过滤条件从视图中选择所有列。

show()函数显示了 SQL 查询的结果,使我们能够检查从数据集中提取的所需信息。

在 Spark SQL 中创建视图的另一种方法是createTempView()。与createOrReplaceTempView()方法相比,createTempView()只会尝试创建一个视图。如果该视图名称已存在于目录中,则会抛出TempTableAlreadyExistsException异常。

利用 Spark SQL 根据特定标准过滤和选择数据

在本节中,我们将探讨执行 SQL 查询和应用转换的语法和实际示例。

让我们考虑一个实际示例,其中我们执行一个 SQL 查询来过滤和选择表中的特定数据:

# Save the processed data as a view
salary_data_with_id.createOrReplaceTempView("employees")
#Apply filtering on data
filtered_data = spark.sql("SELECT Employee, Department, Salary, Age FROM employees WHERE age > 30")
# Display the results
filtered_data.show()

输出结果如下:

+--------+----------+------+---+
|Employee|Department|Salary|Age|
+--------+----------+------+---+
|    John| Field-eng|  3500| 40|
|  Robert|     Sales|  4000| 38|
|   Kelly|   Finance|  3500| 35|
|    Kate|   Finance|  3000| 45|
|   Kiran|     Sales|  2200| 35|
+--------+----------+------+---+

在本例中,我们创建一个临时视图,名为employees,并使用 Spark SQL 执行 SQL 查询以过滤和选择employees表中的特定列。

查询选择了employeedepartmentsalaryage列,其中age大于 30。查询的结果存储在filtered_data变量中。

最后,我们调用show()方法来显示过滤后的数据。

探索使用 Spark SQL 进行排序和聚合操作

Spark SQL 提供了一组丰富的转换函数,可以应用于操作和转换数据。让我们探索一些 Spark SQL 中转换的实际示例:

聚合

在本例中,我们使用 Spark SQL 执行聚合操作,从employees表中计算平均工资。

# Perform an aggregation to calculate the average salary
average_salary = spark.sql("SELECT AVG(Salary) AS average_salary FROM employees")
# Display the average salary
average_salary.show()

输出结果如下:

+--------------+
|average_salary|
+--------------+
|        3275.0|
+--------------+

AVG()函数计算salary列的平均值。我们使用 AS 关键字将结果别名为average_salary

结果存储在average_salary变量中,并使用show()方法显示:

排序

在本例中,我们使用 Spark SQL 对employees表应用排序转换。

# Sort the data based on the salary column in descending order
sorted_data = spark.sql("SELECT * FROM employees ORDER BY Salary DESC")
# Display the sorted data
sorted_data.show()

输出结果如下:

+---+--------+----------+------+---+
| ID|Employee|Department|Salary|Age|
+---+--------+----------+------+---+
|  2|  Robert|     Sales|  4000| 38|
|  1|    John| Field-eng|  3500| 40|
|  5|   Kelly|   Finance|  3500| 35|
|  3|   Maria|   Finance|  3500| 28|
|  7|  Martin|   Finance|  3500| 26|
|  6|    Kate|   Finance|  3000| 45|
|  4| Michael|     Sales|  3000| 20|
|  8|   Kiran|     Sales|  2200| 35|
+---+--------+----------+------+---+

使用ORDER BY子句来指定排序标准,在本例中是对salary列进行降序排序。

排序后的数据存储在sorted_data变量中,并使用show()方法进行显示。

结合聚合

我们还可以在一个 SQL 命令中结合不同的聚合操作,如下面的代码示例所示:

# Sort the data based on the salary column in descending order
filtered_data = spark.sql("SELECT Employee, Department, Salary, Age FROM employees WHERE age > 30 AND Salary > 3000 ORDER BY Salary DESC")
# Display the results
filtered_data.show()

输出结果如下:

+--------+----------+------+---+
|Employee|Department|Salary|Age|
+--------+----------+------+---+
|  Robert|     Sales|  4000| 38|
|   Kelly|   Finance|  3500| 35|
|    John| Field-eng|  3500| 40|
+--------+----------+------+---+

在这个例子中,我们使用 Spark SQL 对 employees 表进行不同的转换。首先,我们选择那些年龄大于 30 岁且工资大于 3,000 的员工。ORDER BY 子句用于指定排序标准;在这种情况下,按 salary 列降序排序。

结果数据存储在“filtered_data”变量中,并使用 show() 方法显示。

在本节中,我们探讨了使用 Spark SQL 执行 SQL 查询和应用转换的过程。我们学习了执行 SQL 查询的语法,并展示了执行查询、过滤数据、执行聚合和排序数据的实际示例。通过利用 SQL 的表达能力和 Spark SQL 的灵活性,您可以高效地分析和操作结构化数据,以完成各种数据分析任务。

根据特定列进行分组和聚合数据 – 基于特定列进行分组并执行聚合函数

在 Spark SQL 中,分组和聚合数据是常见的操作,用于从大型数据集中获取洞察和总结信息。本节将探讨如何使用 Spark SQL 根据特定列分组数据并执行各种聚合函数。我们将通过代码示例演示 Spark SQL 在此方面的功能。

数据分组

当我们想要根据特定列对数据进行分组时,可以利用 SQL 查询中的 GROUP BY 子句。让我们考虑一个例子,其中我们有一个包含 departmentsalary 列的员工 DataFrame。我们想要计算每个部门的平均工资:

# Group the data based on the Department column and take average salary for each department
grouped_data = spark.sql("SELECT Department, avg(Salary) FROM employees GROUP BY Department")
# Display the results
grouped_data.show()

输出将如下所示:

+----------+------------------+
|Department|       avg(Salary)|
+----------+------------------+
| Field-eng|            3500.0|
|     Sales|3066.6666666666665|
|   Finance|            3375.0|
+----------+------------------+

在这个例子中,我们使用 Spark SQL 对 employees 表的不同转换进行数据分组。首先,我们根据 Department 列对员工进行分组。我们从 employees 表中获取每个部门的平均工资。

结果数据存储在 grouped_data 变量中,并使用 show() 方法显示。

数据聚合

Spark SQL 提供了广泛的聚合函数,用于对分组数据进行汇总统计。让我们考虑另一个例子,其中我们想要计算每个部门的总工资和最高工资:

# Perform grouping and multiple aggregations
aggregated_data = spark.sql("SELECT Department, sum(Salary) AS total_salary, max(Salary) AS max_salary FROM employees GROUP BY Department")
# Display the results
aggregated_data.show()

输出将如下所示:

+----------+-----------+-----------+
|Department|sum(Salary)|max(Salary)|
+----------+-----------+-----------+
| Field-eng|       3500|       3500|
|     Sales|       9200|       4000|
|   Finance|      13500|       3500|
+----------+-----------+-----------+

在这个例子中,我们使用 Spark SQL 对 employees 表的不同转换进行合并和分组。首先,我们根据 Department 列对员工进行分组。我们从员工表中获取每个部门的总工资和最高工资。我们还为这些聚合列使用了别名。

结果数据存储在 aggregated_data 变量中,并使用 show() 方法显示。

在本节中,我们探讨了 Spark SQL 在数据分组和聚合方面的功能。我们看到了如何根据特定列分组数据并执行各种聚合函数的示例。Spark SQL 提供了广泛的聚合函数,并允许创建自定义聚合函数以满足特定需求。利用这些功能,您可以使用 Spark SQL 高效地总结和从大量数据集中获得洞察。

在下一节中,我们将探讨用于复杂数据操作的高级 Spark SQL 函数。

高级 Spark SQL 操作

让我们探索 Apache Spark 高级操作的关键功能。

利用窗口函数在 DataFrame 上执行高级分析操作

在本节中,我们将探讨 Spark SQL 中窗口函数的强大功能,用于在 DataFrame 上执行高级分析操作。窗口函数提供了一种在分区内对一组行进行计算的方法,使我们能够获得洞察并有效地执行复杂计算。在本节中,我们将深入研究窗口函数的主题,并通过展示 Spark SQL 查询中其使用的代码示例来展示其用法。

理解窗口函数

Spark SQL 中的窗口函数通过根据指定标准将数据集划分为组或分区,从而实现高级分析操作。这些函数在每个分区内部对滑动窗口中的行进行计算或聚合。

在 Spark SQL 中使用窗口函数的一般语法如下:

function().over(Window.partitionBy("column1", "column2").orderBy("column3").rowsBetween(start, end))

function() 代表您想要应用的窗口函数,例如 sumavgrow_number 或自定义定义的函数。over() 子句定义了函数应用的窗口。Window.partitionBy() 指定用于将数据集划分为分区的列。它还确定了每个分区内部行的顺序。rowsBetween(start, end) 指定窗口中包含的行范围。它可以是不限定的或相对于当前行的相对定义。

使用窗口函数计算累积和

让我们通过一个实际示例来探索窗口函数的使用,以计算 DataFrame 中某列的累积和:

from pyspark.sql.window import Window
from pyspark.sql.functions import col, sum
# Define the window specification
window_spec = Window.partitionBy("Department").orderBy("Age")
# Calculate the cumulative sum using window function
df_with_cumulative_sum = salary_data_with_id.withColumn("cumulative_sum", sum(col("Salary")).over(window_spec))
# Display the result
df_with_cumulative_sum.show()

输出将如下所示:

+---+--------+----------+------+---+--------------+
| ID|Employee|Department|Salary|Age|cumulative_sum|
+---+--------+----------+------+---+--------------+
|  1|    John| Field-eng|  3500| 40|          3500|
|  7|  Martin|   Finance|  3500| 26|          3500|
|  3|   Maria|   Finance|  3500| 28|          7000|
|  5|   Kelly|   Finance|  3500| 35|         10500|
|  6|    Kate|   Finance|  3000| 45|         13500|
|  4| Michael|     Sales|  3000| 20|          3000|
|  8|   Kiran|     Sales|  2200| 35|          5200|
|  2|  Robert|     Sales|  4000| 38|          9200|
+---+--------+----------+------+---+--------------+

在本例中,我们首先导入必要的库。我们使用与之前示例相同的 DataFrame:salary_data_with_id

接下来,我们使用 Window.partitionBy("Department").orderBy("Age") 定义窗口规范,根据 Department 列对数据进行分区,并在每个分区内部根据 Age 列对行进行排序。

然后,我们将 sum() 函数用作窗口函数,在定义的窗口规范上应用,以计算 Salary 列的累积和。结果存储在一个名为 cumulative_sum 的新列中。

最后,我们调用 show() 方法来显示具有附加累积和列的 DataFrame。通过利用窗口函数,我们可以在 Spark SQL 中高效地计算累积和、滚动总和、滚动平均值以及其他复杂分析计算。

在本节中,我们探讨了 Spark SQL 中窗口函数的强大功能,用于高级分析。我们讨论了窗口函数的语法和用法,使我们能够在定义的分区和窗口内执行复杂计算和聚合。通过将窗口函数集成到 Spark SQL 查询中,您可以获得有价值的见解,并深入了解您的数据,以便进行高级分析操作。

在下一节中,我们将探讨 Spark 用户定义函数。

用户定义函数

在本节中,我们将深入探讨 Spark SQL 中的 用户定义函数UDFs)主题。UDFs 允许我们通过定义自己的函数来扩展 Spark SQL 的功能,这些函数可以应用于 DataFrame 或 SQL 查询。在本节中,我们将探讨 UDF 的概念,并提供代码示例以展示它们在 Spark SQL 中的使用和优势。

Spark SQL 中的 UDF 允许我们创建自定义函数,以在 DataFrame 或 SQL 查询中对列进行转换或计算。当 Spark 的内置函数不能满足我们的特定要求时,UDF 特别有用。

要在 Spark SQL 中定义 UDF,我们使用 pyspark.sql.functions 模块中的 udf() 函数。其一般语法如下:

from pyspark.sql.functions import udf
udf_name = udf(lambda_function, return_type)

首先,我们从 pyspark.sql.functions 模块导入 udf() 函数。接下来,我们通过提供 lambda 函数或常规 Python 函数作为 lambda_function 参数来定义 UDF。此函数封装了我们想要应用的自定义逻辑。

我们还指定了 UDF 的 return_type,它表示 UDF 将返回的数据类型。

将 UDF 应用到 DataFrame

让我们通过一个实际示例来探索 UDF 在 Spark SQL 中的使用,该示例通过将自定义函数应用于 DataFrame 来演示:

from pyspark.sql import SparkSession
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType
# Define a UDF to capitalize a string
capitalize_udf = udf(lambda x: x.upper(), StringType())
# Apply the UDF to a column
df_with_capitalized_names = salary_data_with_id.withColumn("capitalized_name", capitalize_udf("Employee"))
# Display the result
df_with_capitalized_names.show()

输出将是以下内容:

+---+--------+----------+------+---+----------------+
| ID|Employee|Department|Salary|Age|capitalized_name|
+---+--------+----------+------+---+----------------+
|  1|    John| Field-eng|  3500| 40|            JOHN|
|  2|  Robert|     Sales|  4000| 38|          ROBERT|
|  3|   Maria|   Finance|  3500| 28|           MARIA|
|  4| Michael|     Sales|  3000| 20|         MICHAEL|
|  5|   Kelly|   Finance|  3500| 35|           KELLY|
|  6|    Kate|   Finance|  3000| 45|            KATE|
|  7|  Martin|   Finance|  3500| 26|          MARTIN|
|  8|   Kiran|     Sales|  2200| 35|           KIRAN|
+---+--------+----------+------+---+----------------+

在本例中,我们首先使用 udf() 函数定义一个名为 capitalize_udf 的 UDF。它应用一个 lambda 函数,将输入字符串转换为大写。我们使用 withColumn() 方法将 UDF capitalize_udf 应用到 name 列,在结果 DataFrame 中创建一个名为 capitalized_name 的新列。

最后,我们调用 show() 方法来显示具有转换列的 DataFrame。

UDFs 允许我们对 DataFrame 中的列应用自定义逻辑和转换,使我们能够处理复杂计算、执行字符串操作或应用在 Spark 内置函数中不可用的特定领域操作。

在本节中,我们探讨了 Spark SQL 中 UDFs 的概念。我们讨论了定义 UDFs 的语法,并通过代码示例演示了其用法。UDFs 通过允许我们对 DataFrame 或 SQL 查询应用自定义转换和计算,提供了一个强大的机制来扩展 Spark SQL 的功能。通过将 UDFs 纳入您的 Spark SQL 工作流程,您可以处理复杂的数据操作,并定制数据处理管道以满足特定要求或特定领域的需求。

应用函数

PySpark 也支持各种 UDFs 和 API,允许用户在 Python 原生函数中直接使用这些 API。例如,以下示例允许用户在 Python 原生函数中直接使用 pandas 序列中的 API:

import pandas as pd
from pyspark.sql.functions import pandas_udf
@pandas_udf('long')
def pandas_plus_one(series: pd.Series) -> pd.Series:
    # Simply plus one by using pandas Series.
    return series + 1
salary_data_with_id.select(pandas_plus_one(salary_data_with_id.Salary)).show()

输出将如下所示:

+-----------------------+
|pandas_plus_one(Salary)|
+-----------------------+
|                   3501|
|                   4001|
|                   3501|
|                   3001|
|                   3501|
|                   3001|
|                   3501|
|                   2201|
+-----------------------+

在本例中,我们首先使用 @pandas_udf() 函数定义一个名为 pandas_plus_one 的 pandas UDF。我们定义此函数以便将其添加到 pandas 序列中。我们使用已创建的名为 salary_data_with_id 的 DataFrame,并调用 pandas UDF 将此函数应用于 DataFrame 的 salary 列。

最后,我们在同一语句中调用 show() 方法,以显示转换后的列的 DataFrame。

此外,UDFs 可以直接在 SQL 中注册和调用。以下是一个示例,说明我们如何实现这一点:

@pandas_udf("integer")
def add_one(s: pd.Series) -> pd.Series:
    return s + 1
spark.udf.register("add_one", add_one)
spark.sql("SELECT add_one(Salary) FROM employees").show()

输出将如下所示:

+---------------+
|add_one(Salary)|
+---------------+
|           3501|
|           4001|
|           3501|
|           3001|
|           3501|
|           3001|
|           3501|
|           2201|
+---------------+

在本例中,我们首先使用 @pandas_udf() 函数定义一个名为 add_one 的 pandas UDF。我们定义此函数以便将其添加到 pandas 序列中。然后,我们将此 UDF 注册以用于 SQL 函数。我们使用已创建的员工表,并调用 pandas UDF 将此函数应用于表的 salary 列。

最后,我们在同一语句中调用 show() 方法以显示结果。

在本节中,我们探讨了 UDFs 的强大功能以及我们如何在使用聚合计算中使用它们。

在下一节中,我们将探讨旋转和逆旋转函数。

处理复杂数据类型 – 旋转和逆旋转

旋转和逆旋转操作用于将数据从基于行的格式转换为基于列的格式,反之亦然。在 Spark SQL 中,可以使用旋转和逆旋转函数执行这些操作。

旋转函数用于将行转换为列。它接受三个参数:用作新列标题的列,用作新行标题的列,以及用作新表中值的列。结果表将具有行标题列中每个唯一值的一行,以及列标题列中每个唯一值的一列。

逆旋转函数用于将列转换为行。它接受两个参数:用作新行标题的列,以及用作新表中值的列。结果表将具有每个行标题列中值的唯一组合的一行,以及每个值的一列。

pivot 和 unpivot 操作的一些用例包括以下内容:

  • 将数据从宽格式转换为长格式或反之亦然

  • 通过多个维度聚合数据

  • 创建汇总表或报告

  • 准备数据以进行可视化或分析

总体而言,pivot 和 unpivot 操作是 Spark SQL 中转换数据的实用工具。

摘要

在本章中,我们探讨了在 Spark SQL 中转换和分析数据的过程。我们学习了如何过滤和操作加载的数据,将转换后的数据保存为表,并执行 SQL 查询以提取有意义的见解。通过遵循提供的 Python 代码示例,你可以将这些技术应用到自己的数据集中,释放 Spark SQL 在数据分析和解探中的潜力。

在介绍那些主题之后,我们探讨了 Spark SQL 中窗口函数的强大功能,用于高级分析。我们讨论了窗口函数的语法和用法,使我们能够在定义的分区和窗口内执行复杂的计算和聚合。通过将窗口函数纳入 Spark SQL 查询,你可以获得有价值的见解,并更深入地理解你的数据,以便进行高级分析操作。

然后,我们讨论了一些在 Spark 中使用 UDF 的方法以及它们如何在 DataFrame 的多行和多列的复杂聚合中变得有用。

最后,我们介绍了一些在 Spark SQL 中使用 pivot 和 unpivot 的方法。

样题

问题 1

以下哪个代码片段会在 Spark SQL 中创建一个视图,如果已经存在则替换现有视图?

  1. dataframe.createOrReplaceTempView()

  2. dataframe.createTempView()

  3. dataFrame.createTableView()

  4. dataFrame.createOrReplaceTableView()

  5. dataDF.write.path(filePath)

问题 2

我们使用哪个函数将两个 DataFrame 合并在一起?

  1. DataFrame.filter()

  2. DataFrame.distinct()

  3. DataFrame.intersect()

  4. DataFrame.join()

  5. DataFrame.count()

答案

  1. A

  2. D

第四部分:Spark 应用程序

在这部分,我们将介绍 Spark 的结构化流,重点关注使用事件时间处理、水印、触发器和输出模式等概念进行实时数据处理。实际示例将说明如何使用结构化流构建和部署流应用程序。此外,我们还将深入研究 Spark ML,Spark 的机器学习库,探索监督和非监督技术,模型构建、评估以及跨各种算法的超参数调整。实际示例将展示 Spark ML 在现实世界机器学习任务中的应用,这对于当代数据科学至关重要。虽然这些内容不包括在 Spark 认证考试中,但理解这些概念对于现代数据工程至关重要。

本部分包含以下章节:

  • 第七章Spark 中的结构化流

  • 第八章使用 Spark ML 进行机器学习

第七章:Spark 中的结构化流

随着数据量和数据速度的每日增长,数据处理的世界已经迅速发展。因此,从实时数据中分析和提取洞察的需求变得越来越关键。作为 Apache Spark 的一个组件,结构化流已经出现成为一个强大的框架,用于实时处理和分析数据流。本章深入探讨结构化流的领域,探索其功能、特性和实际应用。

在本章中,我们将涵盖以下主题:

  • 实时数据处理

  • 流式处理的基本原理

  • 流式架构

  • Spark Streaming

  • 结构化流

  • 流式源和接收器

  • 结构化流的进阶主题

  • 结构化流中的连接

到本章结束时,你将了解 Spark Streaming 和实时数据洞察的力量。

我们将首先探讨实时数据处理的意义。

实时数据处理

在当今快节奏和以数据驱动为主的世界中,实时数据处理变得越来越关键。组织需要分析并从到达的数据中提取洞察,使他们能够及时做出决策并采取即时行动。Apache Spark 的一个强大组件 Spark Streaming 通过提供一个可扩展和容错的框架来处理实时数据流来满足这一需求。

实时数据处理在各个行业中获得了巨大的重要性,从金融和电子商务到物联网(IoT)和社交媒体。虽然传统的批量处理方法在许多场景中适用,但在需要即时洞察和行动时却力不从心。实时数据处理通过允许在数据到达时进行分析和处理来填补这一空白,使组织能够及时做出决策并迅速应对变化的情况。

实时数据处理涉及对流数据的持续摄取、处理和分析。与批量处理不同,批量处理在静态数据集上运行,实时数据处理系统处理的是实时生成和更新的数据。这些数据可以来自各种渠道,包括传感器、日志、社交媒体流和金融交易。

实时数据处理的关键特性如下:

  • 低延迟:实时数据处理旨在最小化数据生成和处理之间的时间延迟。它需要快速高效的处理能力,以提供近乎瞬间的洞察和响应。

  • 可扩展性:实时数据处理系统必须能够处理高容量和高速度的数据流。能够水平扩展并在多个节点上分配处理能力对于适应不断增长的数据负载是至关重要的。

  • 容错性:鉴于流数据的连续性,实时处理系统需要能够抵御故障。它们应具备恢复失败的机制,并确保处理不间断。

  • 流数据模型:实时数据处理系统在流数据上运行,这是一个无界的事件或记录序列。流数据模型旨在处理数据的连续流动,并提供基于事件时间和窗口计算的机制。

实时数据处理的特点导致以下几项优势:

  • 快速响应:实时处理使组织能够快速响应变化的情况、事件或机会。它允许及时采取行动,例如欺诈检测、异常检测、实时监控和警报。

  • 个性化:实时处理通过实时分析和采取用户行为,实现个性化体验。它推动了实时推荐、动态定价、定向广告和内容个性化。

  • 操作效率:实时处理提供了对操作流程的洞察,使组织能够优化其操作,识别瓶颈,并在实时内提高效率。它促进了预测性维护、供应链优化和实时资源分配。

  • 情境感知:实时数据处理通过持续分析和汇总来自各种来源的数据,帮助组织获得情境感知。它使网络安全、金融市场和应急响应系统等领域的实时分析、监控和决策成为可能。

总结来说,实时流涉及数据的持续传输和处理,从而实现即时洞察和快速决策。它在不同行业中具有广泛的应用,并利用各种技术来促进高效和可靠的流处理。

在下一节中,我们将探讨流式的基础知识,并了解流式如何对实时操作有益。

什么是流?

流是指数据的连续和实时处理,这些数据是在生成或接收时产生的。与在固定间隔以块或批量处理数据的批处理不同,流允许连续和增量地处理数据。它允许应用程序实时摄取、处理和分析数据,从而实现及时决策和对事件的即时响应。

有多种类型的流式架构可供处理流数据。我们将在下一节中探讨它们。

流式架构

流式架构旨在处理流数据的连续和高速度特性。它们通常由三个关键组件组成:

  • 流式处理源:这些是流数据的来源,例如物联网设备、传感器、日志、社交媒体流或消息系统。流式源持续产生和实时发射数据。

  • 流处理引擎:流处理引擎负责摄取、处理和分析流数据。它提供了处理流数据的连续性和增量性质所必需的基础设施和计算能力。

  • 流式处理接收器:流式处理接收器是处理后的数据存储、可视化或采取行动的目的地。它们可以是数据库、数据仓库、仪表板或消费处理数据的系统。

存在着各种流处理架构,包括以下几种:

  • 事件驱动架构:在事件驱动架构中,事件由源生成,然后由引擎捕获和处理,导致即时反应并触发实时动作或更新。这个框架促进了实时事件处理,支持事件驱动微服务的发展,并有助于创建反应式系统。

    事件驱动架构的优势在于其提供响应性、可扩展性和灵活性。这允许对事件进行快速反应,从而在系统响应中培养敏捷性。

  • Lambda 架构:Lambda 架构无缝集成批处理和流处理,有效地管理历史数据和实时数据。这涉及到数据流的并行处理,以实现实时分析,并辅以离线批处理进行深入和全面的分析。

    这种方法特别适合需要平衡实时洞察和深入历史分析的应用。Lambda 架构的优势在于其提供容错性、可扩展性和处理大量数据的能力。这是通过利用批处理和流处理技术的综合力量实现的。

  • 统一流处理架构:统一流处理架构,如 Apache Spark 的 Structured Streaming,旨在为批处理和流处理提供统一的 API 和处理模型。它们通过抽象管理单独的批处理和流处理系统的复杂性,简化了实时应用程序的开发和部署。

    这种架构通过提供一种简化的方法来开发和部署实时应用程序,抽象了复杂性。这对于简单性和易于开发至关重要的场景来说非常理想,允许开发者更多地关注业务逻辑而不是复杂的技术细节。

    优点在于它简化了开发,减少了运营开销,并确保批处理和流处理的一致性。

这些架构根据特定应用的特定需求满足不同的需求。事件驱动适用于实时反应,lambda 在实时和历史数据之间取得平衡,统一流处理则提供了一种简化和统一的方法来处理批处理和流处理。每种方法都有其优势和权衡,使它们根据系统的特定需求适用于各种场景。

在以下章节中,我们将深入探讨结构化流的具体内容、其关键概念以及它与 Spark Streaming 的比较。我们还将探索无状态和有状态流处理、流源和流目的地,提供代码示例和实际说明以增强理解。

介绍 Spark Streaming

如您迄今为止所看到的,Spark Streaming 是基于 Apache Spark 构建的强大实时数据处理框架。它扩展了 Spark 引擎的功能,以支持高吞吐量、容错和可扩展的流处理。Spark Streaming 使开发者能够使用与批处理相同的编程模型来处理实时数据流,从而简化从批处理到流工作负载的过渡。

在核心上,Spark Streaming 将实时数据流划分为小批量或微批量,然后使用 Spark 的分布式计算能力进行处理。每个微批量被视为弹性分布式数据集RDD),这是 Spark 用于分布式数据处理的根本抽象。这种方法允许开发者利用 Spark 广泛的生态系统中的库,如 Spark SQL、MLlib 和 GraphX,用于实时分析和机器学习任务。

探索 Spark Streaming 的架构

Spark Streaming 遵循主从架构,其中驱动程序作为主节点,工作节点处理数据。高级架构包括以下组件:

  • 驱动程序: 驱动程序运行主应用程序并管理 Spark Streaming 应用程序的整体执行。它将数据流划分为批量,在工作节点上调度任务,并协调处理。

  • 接收器: 接收器负责连接到流数据源并接收数据。它们在工作节点上运行,并从 Kafka、Flume 或 TCP 套接字等源拉取数据。接收到的数据随后存储在工作节点的内存中。

  • 离散流(DStream): DStream 是 Spark Streaming 中的基本抽象。它表示被划分为小而离散的 RDD 的数据连续流。DStream 提供了一个高级 API 来对流数据进行转换和操作。

  • mapfilterreduceByKey应用于 DStream 中的每个 RDD。例如countsaveAsTextFilesforeachRDD等操作触发流计算的执行并产生结果。

  • 输出操作:输出操作允许将处理后的数据写入外部系统或存储。Spark Streaming 支持各种输出操作,例如写入文件、数据库或将数据发送到仪表板进行可视化。

关键概念

为了有效地使用 Spark Streaming,理解一些关键概念是很重要的:

  • DStreams:如前所述,DStreams 表示 Spark Streaming 中的连续数据流。它们是一系列 RDD,其中每个 RDD 包含特定时间间隔的数据。DStreams 支持各种转换和操作,使得在流上执行复杂计算成为可能。

  • 窗口操作:窗口操作允许你在数据流中的滑动窗口上应用转换。它支持固定窗口大小或基于时间段的计算,使得窗口聚合或基于时间的连接等任务成为可能。

  • 有状态操作:Spark Streaming 允许你在批次之间维护有状态信息。它使得需要维护和更新状态的操作成为可能,例如累积计数。

  • Checkpointing:Checkpointing 是 Spark Streaming 中确保容错和恢复的关键机制。它定期保存有关流应用程序的元数据,包括配置、DStream 操作和已处理的数据。它使得在出现故障时能够恢复应用程序。

优势

现在,我们将看到在实时操作中使用 Spark Streaming 的不同优势:

  • 统一的处理模型:Spark Streaming 的一个显著优势是其与更大的 Spark 生态系统的集成。它利用与批量处理相同的编程模型,使用户能够无缝地在批处理和实时处理之间切换。这种统一的处理模型简化了开发,并降低了熟悉 Spark 的用户的学习曲线。

  • 高级抽象:Spark Streaming 提供了高级抽象,如 DStreams 来表示流数据。DStreams 旨在处理连续数据流,并允许轻松集成现有的 Spark API、库和数据源。这些抽象提供了一个熟悉且表达性强的编程接口,用于处理实时数据。

  • 容错性和可伸缩性:Spark Streaming 通过利用 Spark 的 RDD 抽象提供容错处理。它通过重新计算丢失的数据来自动从故障中恢复,确保处理管道保持弹性和健壮。此外,Spark Streaming 可以通过在机器集群中分配工作负载来实现水平扩展,从而有效地处理大规模数据流。

  • 窗口计算:Spark Streaming 支持窗口计算,这允许在滑动或翻滚窗口的数据上执行基于时间的分析。窗口操作提供了在执行聚合、时间序列分析和窗口级转换方面的灵活性。这种能力在根据时间特征或模式分析流式数据时特别有用。

  • 广泛的数据源:Spark Streaming 可以无缝集成到各种数据源中,包括 Kafka、Flume、Hadoop 分布式文件系统 (HDFS) 和 Amazon S3。这一广泛的数据源允许用户从多个流中摄取数据并将其与现有数据管道集成。Spark Streaming 还支持自定义数据源,使得可以与专有或专业化的流式平台集成。

虽然 Spark Streaming 提供了强大的实时数据处理能力,但也有一些挑战需要考虑。

挑战

为应用程序构建流式架构带来了一系列挑战,如下所述:

  • 端到端延迟:当 Spark Streaming 以微批处理方式处理数据时,会引入延迟。端到端延迟可能因批处理间隔、数据源和计算复杂度等因素而变化。

  • 容错性:Spark Streaming 通过 RDD 线性和检查点提供容错性。然而,接收器或驱动程序程序中的故障仍然可能干扰流式处理。处理和恢复故障是确保 Spark Streaming 应用程序可靠性的重要考虑因素。

  • 可扩展性:将 Spark Streaming 应用程序扩展以处理大量数据并满足高吞吐量要求可能是一个挑战。适当的资源分配、调整和集群管理对于实现可扩展性至关重要。

  • 数据排序:Spark Streaming 在多个工作节点上并行处理数据,这可能会影响事件的顺序。在某些用例中,确保事件顺序的正确性变得非常重要,开发者在设计应用程序时需要考虑这一点。

总结来说,Spark Streaming 将 Apache Spark 的强大功能带到了实时数据处理中。它与 Spark 生态系统的集成、高级抽象、容错性、可扩展性和对窗口计算的支撑使其成为处理流式数据的诱人选择。通过利用 Spark Streaming 的优势,组织可以解锁宝贵的见解并在实时做出明智的决策。

在下一节中,我们将探讨 Structured Streaming,这是 Apache Spark 中一个更新、更具有表现力的流式 API,它克服了 Spark Streaming 的一些限制和挑战。我们将讨论其核心概念、与 Spark Streaming 的区别以及其在实时数据处理方面的优势。

引入 Structured Streaming

Structured Streaming 是 Apache Spark 的一个革命性补充,为实时数据处理带来了新的范式。它引入了一个高级 API,无缝集成批处理和流处理,提供了一个统一的编程模型。Structured Streaming 将流数据视为一个无界的表或 DataFrame,使开发者能够使用熟悉的类似 SQL 的查询和转换来表示复杂的计算。

与 Spark Streaming 的微批处理模型不同,Structured Streaming 遵循连续处理模型。它随着数据的到来增量处理数据,提供低延迟和接近实时的结果。这种向连续处理的转变为交互式分析、动态可视化和实时决策打开了新的可能性。

关键特性和优势

Structured Streaming 在几个关键方面优于传统的流处理框架:

  • 表达性 API:Structured Streaming 提供了一个声明式 API,允许开发者使用 SQL 查询、DataFrame 操作和 Spark SQL 函数来表示复杂的流计算。这使得具有 SQL 或 DataFrame 专长的开发者可以轻松过渡到实时数据处理。

  • 容错性和精确一次语义:Structured Streaming 通过维护必要的元数据和状态信息来保证端到端容错性和精确一次语义。它优雅地处理故障,并确保即使在出现故障或重试的情况下,数据也只被处理一次。

  • 可伸缩性:Structured Streaming 利用 Spark 引擎的可伸缩性,通过向集群添加更多工作节点来实现水平扩展。它可以处理高吞吐量的数据流,并在数据量增加时无缝扩展。

  • 统一批处理和流处理:使用 Structured Streaming,开发者可以为批处理和流处理使用相同的 API 和编程模型。这种统一简化了应用程序的开发和维护,因为不需要管理单独的批处理和流处理代码库。

  • 生态系统集成:Structured Streaming 无缝集成到更广泛的 Spark 生态系统中,使得可以使用 Spark SQL、MLlib 和 GraphX 等库进行实时分析、机器学习和流数据的图处理。

现在,让我们看看 Structured Streaming 和 Spark Streaming 之间的一些区别。

Structured Streaming 与 Spark Streaming 的比较

Structured Streaming 在几个基本方面与 Spark Streaming 不同:

  • 处理模型:Spark Streaming 以微批处理的方式处理数据,其中每个批次都被视为一个独立的 RDD。相比之下,Structured Streaming 以连续的方式增量处理数据,将流视为一个无界的表或 DataFrame。

  • API 和查询语言:Spark Streaming 主要提供基于 RDD 转换和操作的底层 API。而 Structured Streaming 则提供了一种更高级的 API,具有类似 SQL 的查询、DataFrame 操作和 Spark SQL 函数。这使得表达复杂的计算并利用 SQL 的力量进行实时分析变得更加容易。

  • 容错性:Spark Streaming 和 Structured Streaming 都提供容错性。然而,Structured Streaming 的容错性是通过维护必要的元数据和状态信息来实现的,而 Spark Streaming 则依赖于 RDD 线性和检查点来进行故障恢复。

  • 数据处理保证:Spark Streaming 默认提供至少一次处理保证,即在出现故障时可能会处理一些重复数据。另一方面,Structured Streaming 提供了精确一次处理语义,确保即使在出现故障或重试的情况下,每个事件也只被处理一次。

限制和注意事项

虽然 Structured Streaming 提供了显著的优势,但也有一些限制和注意事项需要考虑:

  • 事件时间处理:在 Structured Streaming 中,正确处理事件时间(如时间戳提取、水印和迟到数据处理)至关重要。应确保正确处理乱序事件。

  • 状态管理:Structured Streaming 允许你在批次之间维护状态信息,这可能会引入与状态管理和可扩展性相关的挑战。监控内存使用量和配置适当的状态保留策略对于最佳性能至关重要。

  • 生态系统兼容性:虽然 Structured Streaming 与 Spark 生态系统集成良好,但某些库和功能可能不完全兼容实时流式处理用例。在使用 Structured Streaming 应用程序之前,评估特定库和功能之间的兼容性非常重要。

  • 性能考虑:Structured Streaming 的连续处理模型与微批处理相比引入了不同的性能考虑因素。事件速率、处理时间和资源分配等因素需要仔细监控和优化,以实现高效的实时数据处理。

在下一节中,我们将深入探讨无状态和有状态流式处理的概念,探讨它们在 Structured Streaming 上下文中的差异和用例。

流式处理基础

让我们从查看一些流式处理的基本概念开始,这些概念将帮助我们熟悉流式处理的不同范式。

无状态流式处理 – 一次处理一个事件

无状态流指的是独立处理每个事件,不考虑任何来自先前事件的上下文或历史。在这种方法中,每个事件都是独立处理的,处理逻辑不依赖于任何累积的状态或来自过去事件的信息。

无状态流非常适合每个事件可以独立处理的情况,输出完全由事件本身的内容决定。这种方法通常用于简单的过滤、转换或增强操作,不需要你在事件之间维护任何上下文信息。

状态流 – 维护状态信息

状态流涉及在处理过程中维护和利用跨多个事件的环境信息或状态。处理逻辑考虑事件的历史,并使用累积的信息做出决策或执行计算。状态流使更复杂的分析和依赖上下文或累积知识的复杂计算成为可能。

状态流要求你在新事件到达时维护和更新状态信息。状态可以很简单,如运行计数,也可以更复杂,涉及聚合、窗口计算或维护会话信息。在状态流应用程序中,适当的状态管理对于确保正确性、可扩展性和容错性至关重要。

让我们了解无状态流和状态流之间的区别。

无状态流和状态流之间的区别

无状态流和状态流之间的主要区别可以总结如下:

  • 无状态流独立处理事件,而状态流在事件之间维护和使用累积的状态信息。

  • 无状态流适用于不依赖过去事件的简单操作,而状态流使复杂的计算成为可能,这些计算需要上下文或累积的知识。

  • 无状态流通常更容易实现和推理,而状态流在管理状态、容错性和可扩展性方面引入了额外的挑战。

  • 无状态流通常用于实时过滤、转换或基本聚合,而状态流对于窗口计算、会话化和状态化连接是必要的。

在设计实时数据处理系统时,理解无状态流和状态流之间的区别至关重要,因为它有助于确定给定用例的适当处理模型和需求。

现在,让我们来看看结构化流的一些基本概念。

结构化流概念

要理解结构化流,了解数据到达时在近实时场景中发生的不同操作对我们来说很重要。我们将在下一节中理解它们。

事件时间和处理时间

在 Structured Streaming 中,有两个重要的时间概念——事件时间和处理时间:

  • 事件时间: 事件时间指的是事件发生或生成的时刻。它通常嵌入在数据本身中,代表时间戳或一个字段,指示事件在现实世界中发生的时刻。事件时间对于基于时间顺序分析数据或执行基于窗口的计算至关重要。

  • 处理时间: 另一方面,处理时间指的是事件被流应用程序处理的时间。它由系统时钟或事件被处理引擎摄取的时间决定。处理时间对于需要低延迟或即时响应的任务很有用,但可能无法准确反映实际事件顺序。

基于这些不同的时间概念,我们可以确定哪个最适合给定的用例。理解两者之间的区别很重要。基于这一点,可以确定数据处理策略。

水印和迟到数据处理

现在,我们将讨论如何处理在实时应用程序中未在定义时间到达的数据。有不同方式来处理这种情况。Structured Streaming 有一个内置机制来处理这类数据。这些机制包括:

  • 水印: 水印是 Structured Streaming 中用于处理事件时间和处理延迟或迟到数据的机制。水印是一个阈值时间戳,指示系统在某个时刻之前看到的最大事件时间。它允许系统跟踪事件时间的进度并确定何时可以安全地发出特定窗口的结果。

  • 处理延迟数据: 迟到数据指的是时间戳超过水印阈值的事件。Structured Streaming 提供了处理迟到数据的选择,例如丢弃它、更新现有结果或将它单独存储以供进一步分析。

这些内置机制为用户节省了大量时间,并有效地处理迟到数据。

接下来,我们将看到,一旦数据到达,我们将如何开始对其进行流处理操作。

触发器和输出模式

触发器决定了流应用程序何时应发出结果或触发计算的执行。Structured Streaming 支持不同类型的触发器:

  • 事件时间触发器: 事件时间触发器基于新事件的到达或水印超过某个阈值时进行操作。它们基于事件时间语义,实现更准确和高效的处理。

  • 处理时间触发器: 这些触发器基于处理时间进行操作,允许您指定计算应执行的时间间隔或持续时间。

Structured Streaming 还提供了不同的输出模式。输出模式决定了数据在接收器中的更新方式。接收器是我们会在流操作后写入输出的地方:

  • 完整模式:在此模式下,整个更新后的结果,包括输出中的所有行,都被写入到接收器。这种模式提供了最全面的数据视图,但对于大型结果集来说可能非常消耗内存。

  • 追加模式:在追加模式下,只有自上次触发器以来添加到结果表中的新行被写入到接收器。这种模式适用于结果是一个只追加流的情况。

  • 更新模式:更新模式只将更改的行写入到接收器,保留自上次触发器以来未更改的现有行。这种模式适用于结果表是增量更新的情况。

现在,让我们看看在流数据上可以执行的不同类型的聚合操作。

窗口操作

Structured Streaming 中的窗口操作允许您在特定的时间窗口内对数据进行分组和聚合。这些操作的窗口可以根据事件时间或处理时间定义,并提供了一种在给定时间范围内对事件子集进行计算的方法。

常见的窗口操作类型包括以下几种:

  • 滚动窗口:滚动窗口将流划分为非重叠的固定大小窗口。每个事件恰好属于一个窗口,并且每个窗口的计算是独立进行的。

  • 滑动窗口:滑动窗口创建重叠窗口,这些窗口以固定的时间间隔在流上滑动或移动。每个事件可以贡献给多个窗口,并且可以在重叠部分进行计算。

  • 会话窗口:会话窗口根据指定的会话超时将时间上接近或属于同一会话的事件分组。会话被定义为一系列彼此之间在一定时间阈值内的事件。

在流处理中,我们经常使用的下一个操作是连接操作。现在,我们将看看如何使用连接操作处理流数据。

连接和聚合

Structured Streaming 支持在流数据上执行连接和聚合,从而实现复杂的分析和数据转换:

  • 连接:流连接允许您根据公共键或条件结合两个或多个流或一个流与静态/参考数据。连接操作可以使用事件时间或处理时间执行,并支持不同的连接类型,如内连接、外连接和左/右连接。

  • count, sum, average, min, 和 max.

Structured Streaming 提供了灵活且表达丰富的 API 来处理事件时间、触发器、输出模式、窗口操作、连接和聚合,这使得开发者能够对流数据进行全面的实时分析和计算。通过理解这些概念,开发者可以轻松且精确地构建复杂的流应用程序。

在下一节中,我们将探讨如何使用流数据源和接收器读取和写入数据。

流数据源和接收器

流式源和汇是流式系统中不可或缺的组件,它们使得从外部系统摄取数据并将处理后的数据输出到外部目标成为可能。它们构成了流式应用程序与数据源或汇之间的连接器。

流式源从各种输入系统中检索数据,如消息队列、文件系统、数据库或外部 API,并将数据提供给流式应用程序进行处理。另一方面,流式汇接收应用程序处理后的数据并将其写入外部存储、数据库、文件系统或其他系统以进行进一步分析或消费。

存在着不同类型的流式源和汇。接下来我们将探索其中的一些。

内置流式源

结构化流为各种流式源提供了内置支持,使得与流行的数据系统集成变得容易。一些常用的内置流式源包括以下内容:

  • 文件源:文件源允许您从目录中的文件或基于文件系统的文件流(如 HDFS 或 Amazon S3)中读取数据。

  • KafkaSource:KafkaSource 允许从 Apache Kafka(一个分布式流平台)中消费数据。它提供了容错、可伸缩和高吞吐量的数据流摄取。

  • 套接字源:套接字源允许流式应用程序从传输控制协议TCP)套接字读取数据。这对于数据通过网络连接发送的场景很有用,例如日志流或外部系统发送的数据。

  • 结构化流源:结构化流源允许开发者通过扩展内置源接口来定义自己的流式源。它提供了与自定义或专有数据源集成的灵活性。

自定义流式源

除了内置的流式源之外,结构化流允许开发者创建自定义流式源,以从任何可以通过编程访问的系统摄取数据。自定义流式源可以通过扩展结构化流 API 提供的Source接口来实现。

在实现自定义流式源时,开发者需要考虑数据摄取、事件时间管理、容错性和可伸缩性等方面。他们必须定义如何获取数据,如何将其分区和分配给工作节点,以及如何处理迟到数据和模式演变。

与不同的流式源类似,我们也有流式汇。接下来让我们来探索它们。

内置流式汇

结构化流为各种流式汇提供了内置支持,使得将处理后的数据输出到不同的系统变得容易。一些常用的内置流式汇包括以下内容:

  • 控制台源: 控制台源将输出数据写入控制台或标准输出。它对于调试和快速原型设计很有用,但不适合生产使用。

  • 文件源: 文件源将输出数据写入目录或基于文件的系统(如 HDFS 或 Amazon S3)。它允许数据被存储并在以后用于批量处理或归档目的。

  • Kafka 源: Kafka 源允许您将数据写入 Apache Kafka 主题。它为 Kafka 提供了容错、可扩展和高吞吐量的输出,以便其他系统进行消费。

  • ForeachWriter 接口。它提供了将数据写入外部系统或对输出数据进行自定义操作的灵活性。

自定义流式源

与自定义流式源类似,开发者可以通过扩展 Sink 接口在结构化流中实现自定义流式源。在某些情况下,您可能需要将数据写回到可能不支持流式写入的系统。这可能是一个数据库或基于文件的存储系统。自定义流式源允许与内置源不支持的外部系统或数据库集成。

当实现自定义流式源时,开发者需要定义外部系统如何写入或处理输出数据。这可能涉及建立连接、处理批处理或缓冲,并确保容错和一次精确语义。

在下一节中,我们将讨论结构化流的高级技术。

结构化流的高级技术

结构化流具有某些内置功能,使其成为某些批量操作的默认选择。您不必自己设计架构,结构化流会为您处理这些属性。以下是一些例子。

处理容错

在流式系统中,容错至关重要,以确保数据完整性和可靠性。结构化流提供了内置的容错机制来处理流式源和源端的故障:

  • 源容错: 结构化流通过使用水印跟踪事件时间进度并检查点与流相关的元数据,确保源端端到端的容错。如果出现故障,系统可以恢复并从最后一个一致状态继续处理。

  • 源容错: 源的容错依赖于特定源实现提供的保证。某些源可能天生提供一次精确语义,而其他源可能依赖于幂等写入或去重技术来实现至少一次语义。源实现应仔细选择,以确保数据一致性和可靠性。

开发者应该考虑他们使用的流源和流的容错特性,并配置适当的检查点间隔、保留策略和恢复机制,以确保其流应用的可靠性。

Structured Streaming 同样内置了对模式演化的支持。让我们在下一节中探讨这一点。

处理模式演化

Structured Streaming 为处理流数据源中的模式演化提供了支持。模式演化指的是随时间变化的数据结构或模式的变化。

Structured Streaming 可以通过应用模式推断或模式合并的概念来处理模式演化。在从流源读取时,初始模式从传入的数据中推断出来。随着数据的发展,后续的 DataFrame 将与初始模式合并,以适应任何新或更改的字段。

以下代码片段展示了在 Structured Streaming 中处理模式演化的方法:

stream = spark.readStream \
  .format("csv") \
  .option("header", "true") \
  .schema(initialSchema) \
  .load("data/input")
mergedStream = stream \
  .selectExpr("col1", "col2", "new_col AS col3")

在这个示例中,初始模式通过 schema 方法显式提供。当新数据到达并带有额外的字段,如 new_col 时,可以使用 selectExpr 方法选择并将其合并到流中。

处理模式演化对于确保流应用中的兼容性和灵活性至关重要,在这些应用中,数据模式可能会随时间变化或演化。

Structured Streaming 中的不同连接方式

Structured Streaming 的一个关键特性是它能够在一个汇集中将不同类型的数据流连接在一起。

流-流连接

流-流连接,也称为 流-流组流-流关联,涉及根据公共键或条件将两个或多个流数据源连接起来。在这种类型的连接中,每个来自流的传入事件都与具有相同键或满足指定条件的其他流的事件相匹配。

流-流连接允许实时数据关联和丰富,使得能够将多个数据流合并以获得更深入的见解和执行复杂分析。然而,与批处理或流-静态连接相比,流-流连接由于流数据的无界性和潜在的事件时间偏移,带来了独特的挑战。

流-流连接的一种常见方法是使用窗口操作。通过在流上定义重叠或滚动窗口,可以基于它们的键将同一窗口内的事件连接起来。为了确保准确和有意义的连接,需要对窗口大小、水印和事件时间特性进行仔细考虑。

这是一个使用 Structured Streaming 进行流-流连接的示例:

stream1 = spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092") .option("subscribe", "topic1") .load()
stream2 = spark.readStream.format("kafka").option("kafka.bootstrap.servers","localhost:9092") .option("subscribe", "topic2") .load()
 joinedStream =stream1.join(stream2, "common_key")

在这个示例中,从不同的主题读取了两个 Kafka 流,stream1stream2。然后应用 join 方法,基于两个流共有的 common_key 字段执行连接操作。

流-静态连接

流式静态连接,也称为流式批量连接,涉及将流数据源与静态或参考数据集连接起来。静态数据集通常表示随时间保持恒定的参考数据,例如配置数据或维度表。

流式静态连接用于使用来自静态数据集的附加信息或属性丰富流数据。例如,您可以将用户活动事件流与静态用户配置文件表连接起来,以丰富每个事件的用户相关细节。

在 Structured Streaming 中执行流式静态连接时,您可以加载静态数据集作为静态 DataFrame,然后使用连接方法与流 DataFrame 执行连接操作。由于静态数据集不会改变,连接操作可以使用默认的“右外连接”模式执行。

这里是一个 Structured Streaming 中流式静态连接的示例:

stream =spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092") .option("subscribe", "topic") .load()
  staticData = spark.read.format("csv") .option("header", "true") .load("data/static_data.csv")
 enrichedStream = stream.join(staticData,"common_key")

在此示例中,流数据是从 Kafka 源读取的,静态数据集是从 CSV 文件加载的。然后使用连接方法根据“common_key”字段执行流式静态连接。

流式流连接和流式静态连接都为实时数据分析和数据增强提供了强大的功能。当使用这些连接操作时,必须仔细管理事件时间特性、窗口选项和数据一致性,以确保准确可靠的结果。此外,还应考虑性能因素,以处理大量数据并满足实时流应用中的低延迟要求。

最后的想法和未来的发展

Structured Streaming 已经成为 Apache Spark 中实时数据处理的一个强大框架。其统一的编程模型、容错性和与 Spark 生态系统的无缝集成使其成为构建可扩展和健壮流式应用的理想选择。

随着 Structured Streaming 的不断发展,有几个领域有望在未来得到发展。以下是一些:

  • 增强对流式源和目的地的支持:提供更多内置连接器以支持流行的流式系统和数据库,以及改进与自定义源和目的地的集成和兼容性。

  • 高级事件时间处理:引入更多高级事件时间处理功能,包括支持事件时间偏斜检测和处理、事件去重和水印优化。

  • 性能优化:持续改进 Structured Streaming 的性能,特别是在数据量高和计算复杂的场景中。这可能包括内存管理、查询计划和查询优化技术的优化。

  • 与 AI 和机器学习的集成:进一步将结构化流与 Spark 中的 AI 和机器学习库(如 MLlib 和 TensorFlow)集成,以实现流式数据的实时机器学习和预测分析。

  • 与流式数据仓库的无缝集成:提供与流式数据仓库或数据湖(如 Apache Iceberg 或 Delta Lake)更好的集成,以实现可扩展和高效的存储以及流式数据的查询。

总结来说,结构化流(Structured Streaming)为 Apache Spark 中的实时数据处理提供了一种现代且表达力强的方法。其易用性、可扩展性、容错性和与 Spark 生态系统的集成使其成为构建健壮和可扩展流式应用的有价值工具。通过利用本章涵盖的概念和技术,开发者可以充分发挥结构化流实时数据处理的潜力。

摘要

在本章中,我们探讨了结构化流的基本概念和高级技术。

我们首先理解了结构化流的基本原理、其优势以及支撑其运作的核心概念。然后,我们讨论了 Spark Streaming 及其提供的功能。

之后,我们深入探讨了结构化流的核心功能。然后,我们进一步深入到高级主题,例如结构化流中的窗口操作。我们探讨了滑动窗口和滚动窗口,这些窗口使我们能够在指定的时间窗口内执行聚合和计算,从而允许对流式数据进行基于时间的分析。此外,我们还探讨了有状态流式处理,这涉及到在流式应用中维护和更新状态,以及集成外部库和 API 以增强结构化流的功能。

最后,我们探讨了实时数据处理的最新趋势,并在总结关键要点和洞察中结束本章。

在下一章中,我们将探讨机器学习技术以及如何使用 Spark 进行机器学习。

第八章:使用 Spark ML 进行机器学习

近年来,机器学习越来越受欢迎。在本章中,我们将全面探索 Apache Spark 上的强大框架 Spark 机器学习ML),它是一个用于可扩展机器学习的框架。我们将深入研究机器学习的基础概念以及 Spark ML 如何利用这些原则来实现高效和可扩展的数据驱动洞察。

我们将涵盖以下主题:

  • 机器学习的关键概念

  • 机器学习的不同类型

  • 使用 Spark 进行机器学习

  • 通过一个真实世界的例子考虑机器学习生命周期

  • 机器学习的不同案例研究

  • Spark ML 和分布式机器学习的未来趋势

机器学习包含针对不同数据场景的多种方法。我们将从学习机器学习的不同关键概念开始。

机器学习简介

机器学习是一个研究领域,专注于开发算法和模型,使计算机系统能够在没有被明确编程的情况下学习和做出预测或决策。它是人工智能AI)的一个子集,旨在为系统提供自动从数据和经验中学习和改进的能力。

在今天这个数据以前所未有的速度产生的大量数据的世界里,机器学习在提取有意义的洞察、做出准确预测和自动化决策过程中发挥着关键作用。随着数据的增长,机器可以更好地学习模式,从而更容易从这些数据中获得洞察。它在金融、医疗保健、营销、图像和语音识别、推荐系统等多个领域都有应用。

机器学习的核心概念

要理解机器学习,重要的是要掌握支撑其方法论的基本概念。

数据

数据是任何机器学习过程的基础。它可以是有结构的、半结构的或非结构的,并包括各种类型,如数值、分类、文本、图像等。机器学习算法需要高质量、相关和代表性的数据来学习模式和做出准确的预测。在处理机器学习问题时,拥有能够回答我们试图解决的问题的数据至关重要。在任何分析或模型构建过程中使用的数据质量将显著影响其结果和决策。不良或低质量的数据可能导致不准确、不可靠或误导性的结果,最终影响任何分析或模型的总体性能和可信度。

在不良数据上训练的机器学习模型可能会做出不准确或错误的预测或分类。例如,在不完整或具有偏见的数据上训练的模型可能会错误地将忠诚的客户识别为潜在的流失者,反之亦然。

依赖于从不良数据中得出的有缺陷或偏见的分析决策者可能会实施基于不准确洞察的策略。例如,由于有缺陷的流失预测而针对错误客户群体的营销活动可能导致资源浪费和错失机会。

因此,我们需要确保用于机器学习问题的数据能够代表我们想要构建模型的受众群体。另外一点需要注意的是,数据可能存在一些固有的偏差。我们有责任寻找这些偏差,并在使用这些数据构建机器学习模型时保持警觉。

特征

特征是机器学习算法用于做出预测或决策的数据的可测量属性或特征。它们是捕获数据中相关信息的变量或属性。在大量的数据中,我们希望了解哪些特征对解决特定问题是有用的。相关的特征会生成更好的模型。

特征工程,即选择、提取和转换特征的过程,在提高机器学习模型性能方面起着至关重要的作用。

标签和目标

标签或目标是指机器学习模型旨在预测或分类的期望输出或结果。在监督学习中,模型从标记数据中学习,标签代表与输入数据相关的正确答案或类别标签。在无监督学习中,模型在数据中识别模式或簇,而不需要任何明确的标签。

训练和测试

在机器学习中,模型使用可用数据的一个子集进行训练,这个子集被称为训练集。训练过程涉及将输入数据和相应的标签输入到模型中,模型从这些数据中学习以做出预测。一旦模型训练完成,其性能将使用另一个称为测试集的独立数据子集进行评估。这种评估有助于评估模型泛化能力和在未见数据上做出准确预测的能力。

算法和模型

机器学习算法是学习数据中的模式和关系的数学或统计过程,并做出预测或决策。它们可以分为各种类型,包括回归、分类、聚类、降维和强化学习。这些算法在数据上训练后,会生成捕获学习模式的模型,可用于对新未见数据做出预测。

深入讨论不同的机器学习算法超出了本书的范围。我们将在下一节讨论不同类型的机器学习问题。

机器学习的类型

机器学习问题可以大致分为两大类。在本节中,我们将探讨这两类。

监督学习

监督学习是一种机器学习方法,其中算法从标记的训练数据中学习以做出预测或决策。在监督学习中,训练数据包括输入特征和相应的输出标签或目标值。目标是学习一个映射函数,可以准确地预测新输入的输出。

监督学习的过程包括以下步骤:

  1. 数据准备:第一步是收集和预处理训练数据。这包括清理数据、处理缺失值以及将数据转换成适合学习算法的格式。数据应分为特征(输入变量)和标签(输出变量)。

  2. 模型训练:一旦数据已经准备好,监督学习算法就在标记的训练数据上训练。算法学习输入特征和相应输出标签之间的模式和关系。目标是找到一个可以很好地泛化到未见数据并做出准确预测的模型。

  3. 模型评估:在训练模型后,需要评估其性能。这是通过使用称为测试集或验证集的单独数据集来完成的。将模型的预测与测试集中的实际标签进行比较,并计算各种评估指标,如准确率、精确率、召回率或均方误差。

  4. 模型部署和预测:一旦模型经过训练和评估,就可以部署到对新数据做出预测。训练好的模型接受新数据的输入特征,并根据训练阶段学到的知识产生预测或决策。

监督学习算法的例子包括线性回归、逻辑回归、支持向量机SVM)、决策树、随机森林、梯度提升和神经网络。再次强调,深入探讨这些算法超出了本书的范围。你可以在这里了解更多关于它们的信息:spark.apache.org/docs/latest/ml-classification-regression.html

无监督学习

无监督学习是一种机器学习类型,其中算法在没有任何标记输出的情况下学习数据中的模式和关系。在无监督学习中,训练数据仅由输入特征组成,目标是发现数据中的隐藏模式、结构或聚类。

无监督学习的过程包括以下步骤:

  1. 数据准备:与监督学习类似,第一步是收集和预处理数据。然而,在无监督学习中,没有标记的输出值或目标变量。数据需要以适合特定无监督学习算法的方式转换和准备。

  2. 模型训练:在无监督学习中,算法在没有任何特定目标变量的输入特征上训练。算法通过统计属性或相似度度量探索数据,并识别模式或聚类。目标是提取数据中的有意义信息,而不需要任何预定义的标签。

  3. 模型评估(可选):与监督学习不同,无监督学习没有基于已知标签的直接评估指标。无监督学习中的评估通常是主观的,并取决于特定的任务或问题领域。它也比监督学习更手动。评估可能包括可视化发现的聚类、评估降维的质量或使用领域知识来验证结果。

  4. 模式发现和洞察:无监督学习的主要目标是发现数据中的隐藏模式、结构或聚类。无监督学习算法可以揭示关于数据的洞察,识别异常或离群值,执行降维或生成推荐。

无监督学习算法的例子包括 K-means 聚类、层次聚类、主成分分析(PCA)、关联规则挖掘和自组织映射(SOM)。

总结来说,监督学习和无监督学习是机器学习中的两种关键类型。监督学习依赖于标记数据来学习模式和进行预测,而无监督学习则在未标记数据中探索模式和结构。这两种类型都有它们自己的算法和技术,以及不同的选择。深入讨论无监督学习超出了本书的范围。

在下一节中,我们将探讨监督机器学习,这是人工智能和数据科学领域的一个基石,代表了构建预测模型和做出数据驱动决策的强大方法。

监督学习类型

如我们所知,监督学习是机器学习的一个分支,其中的算法从标记的训练数据中学习模式和关系。它涉及通过展示输入数据及其相应的输出标签来教授或监督模型,使算法能够学习输入和输出变量之间的映射。我们将探讨三种关键的监督学习类型——分类、回归和时间序列。

分类

分类是一种机器学习任务,其目标是根据其特征将数据分类或归类到预定义的类别或类别中。算法从标记的训练数据中学习,以构建一个可以预测新、未见数据实例类别标签的模型。

在分类中,输出是离散的,代表类别标签。用于分类任务的一些常见算法包括逻辑回归、决策树、随机森林、SVM 和朴素贝叶斯。

例如,考虑一个垃圾邮件分类任务,其目标是预测一封 incoming 邮件是否为垃圾邮件。算法在标记的邮件数据集上训练,其中每封邮件都与一个表示是否为垃圾邮件的类别标签相关联。训练好的模型可以根据其特征(如内容、主题或发件人)将新的邮件分类为垃圾邮件或非垃圾邮件。

回归

回归是另一种机器学习任务,它侧重于根据输入特征预测连续或数值值。在回归中,算法从标记的训练数据中学习,以构建一个模型,该模型可以估计或预测给定一组输入特征的目标变量的数值。

当输出是一个连续值时,例如预测房价、股票市场趋势或根据历史数据预测产品的销售,就会使用回归模型。一些常用的回归算法包括线性回归、决策树、随机森林、梯度提升和神经网络。

例如,考虑一个案例,你想要根据房屋的各种特征(如面积、卧室数量、位置等)来预测房价。在这种情况下,算法是在一个标记的房屋数据集上训练的,其中每座房屋都与相应的价格相关联。训练好的回归模型可以基于新房屋的特征来预测其价格。

时间序列

时间序列分析是机器学习的一个专门领域,它处理随时间收集的数据,其中观察的顺序很重要。在时间序列分析中,目标是理解和预测数据中的模式、趋势和依赖关系。

时间序列模型用于根据历史数据点预测未来值。它们在金融、股票市场预测、天气预报和需求预测等领域得到广泛应用。一些流行的时序算法包括自回归积分移动平均ARIMA)、指数平滑方法和长短期记忆LSTM)网络。

例如,假设你有一个特定公司的历史股票市场数据,包括日期和相应的股票价格。时间序列算法可以分析数据中的模式和趋势,并根据历史价格波动预测未来的股票价格。

总之,监督学习包括各种类型,如分类、回归和时间序列分析。每种类型都针对特定的学习任务,并需要不同的算法和技术。了解这些类型有助于选择适合特定数据分析与预测任务的最佳算法和方法。

接下来,我们将探讨如何利用 Spark 进行机器学习任务。

Spark 机器学习

Spark 提供了一个强大且可扩展的平台,用于执行大规模机器学习任务。Spark 的ML 库,也称为MLlib,提供了一系列算法和工具,用于构建和部署机器学习模型。

使用 Spark 进行机器学习的优点包括其分布式计算能力、高效的数据处理、可扩展性和与其他 Spark 组件(如 Spark SQL 和 Spark Streaming)的集成。Spark 的 MLlib 支持批处理和流数据处理,使得实时机器学习应用的开发成为可能。

机器学习是一个变革性的领域,它使计算机能够从数据中学习并做出预测或决策。通过理解关键概念并利用 Spark 的 MLlib 等工具,我们可以利用机器学习的力量来获得洞察力,自动化流程,并在各个领域推动创新。

现在,让我们来看看使用 Spark 进行机器学习任务的好处。

Apache Spark 在大规模机器学习中的优势

通过利用 Spark 的分布式计算能力和丰富的生态系统,数据科学家和工程师可以有效地解决大规模数据集上的复杂机器学习挑战。由于其分布式计算能力,它提供了各种优势,以下是一些:

  • 速度和性能:Apache Spark 的一个关键优势是它能够以非凡的速度处理大规模数据。Spark 利用内存计算和优化的数据处理技术,如数据并行任务管道化,来加速计算。这使得它在机器学习中常用的迭代算法中效率极高,显著减少了整体处理时间。

  • 分布式计算:Spark 的分布式计算模型允许它在集群的多个节点上分配数据和计算,实现并行处理。这种分布式特性使得 Spark 能够水平扩展,利用多台机器的计算能力并行处理数据。这使得它非常适合需要处理大量数据的机器学习任务。

  • 容错性:Apache Spark 的另一个优点是其内置的容错机制。Spark 自动跟踪弹性分布式数据集(RDDs)的 lineage,RDDs 是 Spark 中的基本数据抽象,这使得它能够从故障中恢复并重新运行失败的任务。这确保了 Spark 应用程序的可靠性和弹性,使其成为处理大规模机器学习工作负载的强大平台。

  • 通用性和灵活性:Spark 提供了一整套 API 和库,这些库简化了各种数据处理和分析任务,包括机器学习。Spark 的 MLlib 库提供了一套丰富的分布式机器学习算法和实用工具,使得开发可扩展的机器学习模型变得容易。此外,Spark 与流行的数据处理框架和工具集成良好,能够无缝集成到现有的数据管道和生态系统中。

  • 实时和流处理能力:正如我们在上一章中讨论的,Spark 通过其名为 Spark Streaming 的流组件扩展了其批处理之外的特性。这在需要基于持续到达的数据立即获得洞察或决策的场景中特别有价值,例如实时欺诈检测、传感器数据分析或社交媒体流上的情感分析。

  • 生态系统和社区支持:Apache Spark 拥有一个充满活力和活跃的开发者和贡献者社区,确保了持续的开发、改进和支持。Spark 从丰富的工具和扩展生态系统中受益,提供了额外的功能性和集成选项。Spark 的社区驱动特性确保了丰富的资源、文档、教程和在线论坛,用于学习和故障排除。

因此,Apache Spark 为大规模 ML 任务提供了显著的优势。其速度、可扩展性、容错性、多功能性和实时能力使其成为处理大数据和开发可扩展 ML 模型的强大框架。

现在,让我们看看 Spark 提供的不同库,以在分布式框架中利用 ML 功能。

Spark MLlib 与 Spark ML

Apache Spark 为 ML 提供了两个库:Spark MLlib 和 Spark ML。尽管它们名称相似,但在设计、API 和功能方面,这两个库之间有一些关键差异。让我们比较 Spark MLlib 和 Spark ML,以了解它们的特性和用例。

Spark MLlib

Spark MLlib 是 Apache Spark 中的原始 ML 库。它在 Spark 的早期版本中引入,提供了一套丰富的分布式 ML 算法和实用工具。MLlib 是建立在 RDD API 之上的,这是 Spark 中的核心数据抽象。

Spark MLlib 与其他非分布式 ML 库(如 scikit-learn)相比,有几个关键特性使其脱颖而出。让我们看看其中的一些:

  • 基于 RDD 的 API:MLlib 利用 RDD 抽象进行分布式数据处理,使其适用于批量处理和迭代算法。RDD API 允许进行高效的分布式计算,但对于某些用例来说可能低级且复杂。

  • 多样化的算法:MLlib 提供了广泛的分布式 ML 算法,包括分类、回归、聚类、协同过滤、降维等。这些算法旨在处理大规模数据,并且可以在 ML 管道中处理各种任务。

  • 特征工程:MLlib 提供了特征提取、转换和选择的实用工具。它包括处理分类和数值特征、文本处理和特征缩放的方法。

  • 模型持久化:MLlib 支持模型持久化,允许将训练好的模型保存到磁盘,并在以后用于部署或进一步分析。

在下一节中,我们将探讨 Spark ML 库。这是另一个也提供 ML 功能的新库。

Spark ML

Spark ML 是在 Spark 2.0 中引入的 Apache Spark 中的较新 ML 库。它旨在更易于使用,具有高级 API 并专注于 DataFrame,这是 Spark SQL 中引入的具有结构和优化的分布式数据集合。

Spark ML 的关键特性如下:

  • 基于 DataFrame 的 API:Spark ML 利用 DataFrame API,它比 RDD API 提供了更直观和更高层次的接口。DataFrame 提供了结构化和表格化的数据表示,使得处理结构化数据以及与 Spark SQL 集成变得更加容易。

  • 管道:Spark ML 引入了管道的概念,为构建机器学习工作流程提供了更高层次的抽象。管道允许将多个数据转换和模型训练阶段链接成一个单一的管道,简化了复杂机器学习管道的开发和部署。

  • 集成特征转换器:Spark ML 包含了一系列特征转换器,如 StringIndexer、OneHotEncoder、VectorAssembler 等。这些转换器与 DataFrame 无缝集成,简化了特征工程过程。

  • 统一 API:Spark ML 统一了不同机器学习任务的 API,例如分类、回归、聚类和推荐。这为不同算法提供了一个一致且统一的编程接口,简化了学习曲线。

既然我们已经了解了 Spark MLlib 和 Spark ML 的关键特性,让我们来探讨何时使用它们。

在以下场景中,使用 Spark MLlib 将受益:

  • 你正在使用不支持 Spark ML 的 Spark 旧版本

  • 你需要低级控制,并需要直接与 RDDs 工作

  • 你需要访问 Spark ML 中不可用的特定算法或功能

在以下场景中,你应该优先使用 Spark ML:

  • 你正在使用 Spark 2.0 或更高版本

  • 你偏好更高层次的 API,并希望利用 DataFrames 和 Spark SQL 的功能

  • 你需要构建包含集成特征转换器和管道的端到端机器学习管道

Spark MLlib 和 Spark ML 都在 Apache Spark 中提供了强大的机器学习功能。正如我们所见,Spark MLlib 是原始库,拥有丰富的分布式算法集,而 Spark ML 是一个更新的库,具有更用户友好的 API 和与 DataFrames 的集成。两者之间的选择取决于你的 Spark 版本、对 API 风格的偏好以及你机器学习任务的具体要求。

机器学习生命周期

机器学习生命周期涵盖了开发和部署机器学习模型的端到端过程。它包括几个阶段,每个阶段都有其自身的任务和考虑因素。理解机器学习生命周期对于构建稳健且成功的机器学习解决方案至关重要。在本节中,我们将探讨机器学习生命周期的关键阶段:

  1. 问题定义:机器学习生命周期的第一阶段是问题定义。这涉及到明确界定你想要解决的问题,以及理解你的机器学习项目的目标和目的。这一阶段需要领域专家和数据科学家之间的协作,以识别问题、定义成功指标和确定项目的范围。

  2. 数据获取和理解:一旦问题被定义,下一步就是获取用于训练和评估的必要数据。数据获取可能涉及从数据库、API 或外部数据集中收集数据。确保数据质量、完整性和与当前问题的相关性非常重要。此外,数据理解涉及探索和分析获取到的数据,以深入了解其结构、分布和潜在问题。

  3. 数据准备和特征工程:数据准备和特征工程是机器学习生命周期中的关键步骤。它涉及转换和预处理数据,使其适合训练机器学习模型。这包括诸如清理数据、处理缺失值、编码分类变量、缩放特征以及通过特征工程技术创建新特征等任务。适当的数据准备和特征工程对机器学习模型的性能和准确性有重大影响。

  4. 模型训练和评估:在这个阶段,机器学习模型在准备好的数据上进行训练。模型训练涉及选择合适的算法、定义模型架构,并使用训练数据优化其参数。然后使用评估指标和验证技术对训练好的模型进行评估,以评估其性能。这个阶段通常需要迭代和微调模型,以达到所需的准确性和泛化能力。

  5. 模型部署:一旦模型经过训练和评估,它就准备好进行部署。模型部署包括将模型集成到生产环境中,对新数据进行预测,并监控其性能。这可能涉及设置 API、创建批量或实时推理系统,并确保模型的可扩展性和可靠性。部署还包括对模型版本控制、监控和重新训练的考虑,以保持模型随时间保持有效性。

  6. 模型监控和维护:一旦模型部署,持续监控其性能并保持其有效性至关重要。监控包括跟踪模型预测、检测异常,并从用户或领域专家那里收集反馈。它还包括使用新数据定期重新训练模型,以适应变化的模式或概念。模型维护涉及解决模型漂移、更新依赖项,并在生产环境中管理模型的生命周期。

  7. 模型迭代和改进:机器学习生命周期是一个迭代的过程,模型通常需要随着时间的推移进行改进。基于用户反馈、性能指标和不断变化的企业需求,模型可能需要更新、重新训练或替换。迭代和改进对于保持模型更新并确保它们继续提供准确的预测至关重要。

机器学习生命周期包括问题定义、数据获取、数据准备、模型训练、模型部署、模型监控和模型迭代。每个阶段在开发成功的机器学习解决方案中都发挥着关键作用。通过遵循一个定义良好的生命周期,组织可以有效地构建、部署和维护机器学习模型,以解决复杂问题并从其数据中提取有价值的见解。

问题陈述

让我们深入一个案例研究,我们将探讨使用历史数据预测房价的艺术。想象一下:我们有一座关于房屋的宝贵信息宝库,包括分区、地块面积、建筑类型、整体状况、建造年份和销售价格等细节。我们的目标是利用机器学习的力量,准确预测即将到来的新房屋的价格。

为了完成这一壮举,我们将踏上构建一个专门用于预测房价的机器学习模型的旅程。该模型将利用现有的历史数据并纳入额外的特征。通过仔细分析和理解这些特征与相应销售价格之间的关系,我们的模型将成为估计任何新进入市场的房屋价值的可靠工具。

为了实现这一点,我们将回顾上一节中定义的一些步骤,其中我们讨论了机器学习生命周期。由于房价是连续的,我们将使用线性回归模型来预测这些价格。

我们将首先准备数据,使其可用于机器学习模型。

数据准备和特征工程

正如我们所知,数据准备和特征工程是机器学习过程中的关键步骤。适当的数据准备和特征工程技术可以显著提高模型的性能和准确性。在本节中,我们将通过代码示例探索常见的数据准备和特征工程任务。

数据集介绍

构建模型的第一步是找到相关数据。我们将为此目的使用房价数据(位于docs.google.com/spreadsheets/d/1caaR9pT24GNmq3rDQpMiIMJrmiTGarbs/edit#gid=1150341366)。这些数据有 2,920 行和 13 列。

该数据集有以下列:

  • Id: 数据表中每行的唯一标识符

  • MSSubClass: 房地产的子类

  • MSZoning: 房地产的分区

  • LotArea: 该房产所在地块的总面积

  • LotConfig: 地块配置 – 例如,是否为角地块

  • BldgType: 住宅类型 – 例如,单户住宅、联排别墅等

  • OverallCond: 房屋的一般状况

  • YearBuilt: 房屋建造的年份

  • YearRemodAdd: 进行任何翻修的年份

  • Exterior1st: 外部类型 – 例如,乙烯基、外墙板等

  • BsmtFinSF2: 完成地下室的总面积

  • TotalBsmtSF: 地下室的总面积

  • SalePrice: 房屋的销售价格

我们将从本节开头提供的链接下载这些数据。

现在我们已经了解了一些数据中的数据点,让我们学习如何加载数据。

加载数据

到目前为止,我们已经在我们的计算机和 Databricks 环境中下载了数据,并以 CSV 文件的形式存在。如您从前面的章节中回忆起来,我们学习了如何通过各种技术将数据集加载到 DataFrame 中。在这里,我们将使用 CSV 文件来加载数据:

housing_data = spark.read.csv("HousePricePrediction.csv")
# Printing first 5 records of the dataset
housing_data.show(5)

我们可以在图 8.1 中看到结果。请注意,由于数据集太大,无法全部显示,我们只能看到部分结果:

图片

让我们打印这个数据集的模式:

housing_data.printSchema

我们将得到以下模式:

<bound method DataFrame.printSchema of DataFrame[Id: bigint, MSSubClass: bigint, MSZoning: string, LotArea: bigint, LotConfig: string, BldgType: string, OverallCond: bigint, YearBuilt: bigint, YearRemodAdd: bigint, Exterior1st: string, BsmtFinSF2: bigint, TotalBsmtSF: bigint, SalePrice: bigint]>

如您可能已经注意到的,一些列的类型是字符串。我们将在下一节中清理这些数据。

清洗数据

数据清洗涉及处理缺失值、异常值和不一致的数据。在我们清理数据之前,我们将查看数据中有多少行。我们可以通过使用count()函数来完成:

housing_data.count()

该语句的结果如下所示:

2919

这意味着在我们应用任何清洗之前,数据中包含 2,919 行。现在,我们将从该数据集中删除缺失值,如下所示:

# Remove rows with missing values
cleaned_data = housing_data.dropna()
cleaned_data.count()

以下是该代码的结果:

1460

这表明我们删除了一些数据行,现在数据量变小了。

在下一节中,我们将讨论分类变量以及如何处理它们,特别是我们例子中用字符串表示的那些。

处理分类变量

在统计学和数据分析领域,分类变量是一种表示类别或组别的变量类型,它可以取有限、固定的不同值或级别。这些变量表示定性特征,不具有固有的数值意义或大小。相反,它们代表不同的属性或标签,将数据分类到特定的组或类别中。在训练机器学习模型之前,分类变量需要被编码成数值。

在我们的例子中,我们有一些是字符串类型的列。这些需要被编码成数值,以便模型能够正确使用它们。为此,我们将使用 Spark 的StringIndexer库来索引字符串列。

以下代码显示了如何使用StringIndexer

#import required libraries
from pyspark.ml.feature import StringIndexer
mszoning_indexer = StringIndexer(inputCol="MSZoning", outputCol="MSZoningIndex")
#Fits a model to the input dataset with optional parameters.
df_mszoning = mszoning_indexer.fit(cleaned_data).transform(cleaned_data)
df_mszoning.show()

在前面的代码中,我们正在将MSZoning列转换为索引列。为了实现这一点,我们创建了一个名为mszoningStringIndexer值。我们将其MSZoning作为要处理的输入列。输出列的名称是MSZoningIndex。我们将在下一步中使用这个输出列。之后,我们将mszoning_indexer拟合到cleaned_data

在生成的 DataFrame 中,您将注意到增加了一个名为MSZoningIndex的额外列。

现在,我们将使用管道来转换 DataFrame 中的所有特征。

管道汇集了一系列必要的步骤,每个步骤都致力于将原始数据转换为有价值的预测和分析。管道作为一个结构化的路径,由不同的阶段或组件组成,按照特定的顺序排列。每个阶段代表一个独特的操作或转换,它精炼数据,使其更适合机器学习任务。

管道(pipeline)的核心在于其无缝连接这些阶段的能力,形成一个协调一致的转换流程。这种编排确保数据能够轻松地通过每个阶段,一个阶段的输出成为下一个阶段的输入。它消除了手动干预的需要,自动化整个过程,节省了我们宝贵的时间和精力。我们将各种操作集成到管道中,如数据清洗、特征工程、编码分类变量、缩放数值特征等等。每个操作都在转换数据,使其可用于机器学习模型。

机器学习管道使我们能够简化我们的工作流程,尝试不同的转换组合,并在数据处理任务中保持一致性。它提供了一个结构化的框架,使我们能够轻松地重现和分享我们的工作,促进协作,并加深我们对数据转换过程的理解。

在机器学习和数据预处理中,独热编码器是一种将分类变量转换为数值格式的技术,使算法能够更好地理解和处理分类数据。当处理缺乏序数关系或数值表示的分类特征时,它特别有用。

我们将在管道中使用 StringIndexerOneHotEncoder。让我们看看我们如何实现这一点:

from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml import Pipeline
mszoning_indexer = StringIndexer(inputCol="MSZoning", outputCol="MSZoningIndex")
lotconfig_indexer = StringIndexer(inputCol="LotConfig", outputCol="LotConfigIndex")
bldgtype_indexer = StringIndexer(inputCol="BldgType", outputCol="BldgTypeIndex")
exterior1st_indexer = StringIndexer(inputCol="Exterior1st", outputCol="Exterior1stIndex")
onehotencoder_mszoning_vector = OneHotEncoder(inputCol="MSZoningIndex", outputCol="MSZoningVector")
onehotencoder_lotconfig_vector = OneHotEncoder(inputCol="LotConfigIndex", outputCol="LotConfigVector")
onehotencoder_bldgtype_vector = OneHotEncoder(inputCol="BldgTypeIndex", outputCol="BldgTypeVector")
onehotencoder_exterior1st_vector = OneHotEncoder(inputCol="Exterior1stIndex", outputCol="Exterior1stVector")
#Create pipeline and pass all stages
pipeline = Pipeline(stages=[mszoning_indexer,
                            lotconfig_indexer,
                            bldgtype_indexer,
                            exterior1st_indexer,
                            onehotencoder_mszoning_vector,
                            onehotencoder_lotconfig_vector,
                            onehotencoder_bldgtype_vector,
                            onehotencoder_exterior1st_vector])

要开始我们的代码,我们需要从 PySpark 库中导入所需的模块。StringIndexerOneHotEncoder 模块将被用来处理住房数据集的字符串列。

当我们开始将分类列转换为机器学习算法可以理解的数值表示的过程时,让我们更仔细地看看代码中发生的魔法。

第一步是为我们希望转换的每个分类列创建 StringIndexer 实例。每个实例接受一个输入列,例如 MSZoningLotConfig,并生成一个相应的输出列,带有数值索引。例如,MSZoningIndex 列捕获了 MSZoning 列的转换索引值。

分类列成功索引后,我们进入下一阶段。现在,我们希望将这些索引转换为二进制向量。为此,我们可以使用 OneHotEncoder。生成的向量将每个分类值表示为一个二进制数组,其中值为 1 表示该类别的存在,否则为 0。

我们为每个索引列创建 OneHotEncoder 实例,例如 MSZoningIndexLotConfigIndex,并生成包含二进制向量表示的新输出列。这些输出列,如 MSZoningVectorLotConfigVector,用于捕获编码信息。

随着代码的进展,我们组装了一个管道——一系列转换,其中每个转换代表一个阶段。在我们的案例中,每个阶段包括特定分类列的索引和独热编码步骤。我们在管道中安排这些阶段,确保转换的正确顺序。

通过构建我们的管道,我们协调了操作的流畅流程。管道连接了不同阶段之间的点,使得将这些转换应用到整个数据集上变得毫不费力。我们的管道充当指挥,引导数据通过转换,最终使其成为适合机器学习的格式。

现在,我们将此管道拟合到我们的清理数据集,以便所有列都可以一起转换:

df_transformed = pipeline.fit(cleaned_data).transform(cleaned_data)
df_transformed.show(5)

结果 DataFrame 将包含我们在管道中通过转换创建的附加列。我们为每个字符串列创建了索引和向量列。

现在,我们需要从我们的数据集中删除不必要的冗余列。我们将在下一节中这样做。

数据清理

在这一步中,我们将确保我们只使用 ML 所需的特征。为了实现这一点,我们将删除不同的附加列,例如不服务于模型的身份列。此外,我们还将删除我们已经应用了转换的特征,例如字符串列。

以下代码展示了如何删除列:

drop_column_list = ["Id", "MSZoning","LotConfig","BldgType", "Exterior1st"]
df_dropped_cols = df_transformed.select([column for column in df_transformed.columns if column not in drop_column_list])
df_dropped_cols.columns

这是结果:

['MSSubClass',
 'LotArea',
 'OverallCond',
 'YearBuilt',
 'YearRemodAdd',
 'BsmtFinSF2',
 'TotalBsmtSF',
 'SalePrice',
 'MSZoningIndex',
 'LotConfigIndex',
 'BldgTypeIndex',
 'Exterior1stIndex',
 'MSZoningVector',
 'LotConfigVector',
 'BldgTypeVector',
 'Exterior1stVector']

如您从结果列列表中看到的,IdMSZoningLotConfigBldgTypeExterior1st 列已从结果 DataFrame 中删除。

过程中的下一步是组装数据。

组装向量

在这一步中,我们将根据我们想要的特性组装一个向量。这一步对于 Spark ML 与数据一起工作来说是必要的。

以下代码捕捉了我们如何实现这一点:

from pyspark.ml.feature import VectorAssembler
#Assembling features
feature_assembly = VectorAssembler(inputCols = ['MSSubClass',
 'LotArea',
 'OverallCond',
 'YearBuilt',
 'YearRemodAdd',
 'BsmtFinSF2',
 'TotalBsmtSF',
 'MSZoningIndex',
 'LotConfigIndex',
 'BldgTypeIndex',
 'Exterior1stIndex',
 'MSZoningVector',
 'LotConfigVector',
 'BldgTypeVector',
 'Exterior1stVector'], outputCol = 'features')
output = feature_assembly.transform(df_dropped_cols)
output.show(3)

在前面的代码块中,我们创建了一个包含组装向量的 features 列。在对其进行缩放后,我们将使用此列进行模型训练。

向量组装完成后,过程的下一步是对数据进行缩放。

特征缩放

特征缩放确保所有特征都在相似的尺度上,防止某些特征主导学习过程。

为了做到这一点,我们可以使用以下代码:

#Normalizing the features
from pyspark.ml.feature import StandardScaler
scaler = StandardScaler(inputCol="features", outputCol="scaledFeatures",withStd=True, withMean=False)
# Compute summary statistics by fitting the StandardScaler
scalerModel = scaler.fit(output)
# Normalize each feature to have unit standard deviation.
scaledOutput = scalerModel.transform(output)
scaledOutput.show(3)

以下代码仅选择缩放后的特征和目标列——即 SalePrice

#Selecting input and output column from output
df_model_final = scaledOutput.select(['SalePrice', 'scaledFeatures'])
df_model_final.show(3)

我们将得到以下输出:

+---------+--------------------+
|SalePrice|   scaledFeatures   |
+---------+--------------------+
|   208500|(37,[0,1,2,3,4,6,...|
|   181500|(37,[0,1,2,3,4,6,...|
|   223500|(37,[0,1,2,3,4,6,...|
+---------+--------------------+

如您所见,df_model_final 现在只有两列。SalePrice 是我们将要预测的列,因此是我们的目标列。scaledFeatures 包含我们将用于训练 ML 模型的所有特征。

这些示例展示了使用 PySpark 进行常见的数据准备和特征工程任务。然而,具体的技术和方法可能因数据集和机器学习任务的要求而异。理解数据的特征并选择适当的预处理和特征工程技术至关重要。适当的数据准备和特征工程是构建准确和稳健的机器学习模型的基础。

此过程的下一步是训练和评估机器学习模型。

模型训练和评估

模型训练和评估是机器学习过程中的关键步骤。在本节中,我们将探讨如何使用各种指标和技术来训练机器学习模型并评估其性能。我们将使用 PySpark 作为模型训练和评估的框架。

数据拆分

在训练模型之前,将数据集拆分为训练集和测试集非常重要。训练集用于训练模型,而测试集用于评估其性能。以下是如何使用 PySpark 拆分数据的示例:

#test train split
df_train, df_test = df_model_final.randomSplit([0.75, 0.25])

在前面的代码中,我们正在进行数据的随机拆分,将 75%的数据放入训练集,将 25%的数据放入测试集。还有其他拆分技术。你应该仔细查看你的数据,然后定义最适合你的数据和模型训练的拆分方式。

我们拆分数据的原因是,一旦我们训练了模型,我们想看看训练好的模型在它从未见过的数据集上的预测效果如何。在这种情况下,那就是我们的测试数据集。这将帮助我们评估模型并确定模型的质量。基于此,我们可以采用不同的技术来改进我们的模型。

下一步是模型训练。

模型训练

数据拆分后,我们可以在训练数据上训练一个机器学习模型。PySpark 为各种类型的机器学习任务提供了一系列算法。在这个例子中,我们将使用线性回归作为我们的选择模型。

下面是一个训练线性回归模型的示例:

from pyspark.ml.regression import LinearRegression
# Instantiate the linear regression model
regressor = LinearRegression(featuresCol = 'scaledFeatures', labelCol = 'SalePrice')
# Fit the model on the training data
regressor = regressor.fit(df_train)

在前面的代码中,我们正在使用训练数据并将其拟合到线性回归模型中。我们还添加了一个名为labelCol的参数,告诉模型这是我们的目标列,用于预测。

模型训练完成后,下一步是确定模型的好坏。我们将在下一节通过评估模型来完成这一步。

模型评估

模型训练后,我们需要评估其在测试数据上的性能。评估指标提供了关于模型性能的见解。

均方误差MSE)是一个基本的统计指标,用于通过量化预测值和实际值之间平方差的平均值来评估回归模型的性能。

R 平方,通常表示为R2,是一个统计量,表示在回归模型中,因变量的方差中有多少是可预测的或由自变量解释的。它作为独立变量解释因变量变异性好坏的指标。

这里是使用均方误差(MSE)和 R2 指标评估回归模型的一个示例:

#MSE for the train data
pred_results = regressor.evaluate(df_train)
print("The train MSE for the model is: %2f"% pred_results.meanAbsoluteError)
print("The train r2 for the model is: %2f"% pred_results.r2)

这里是结果:

The MSE for the model is: 32287.153682
The r2 for the model is: 0.614926

我们可以检查测试数据的性能,如图所示:

#Checking test performance
pred_results = regressor.evaluate(df_test)
print("The test MSE for the model is: %2f"% pred_results.meanAbsoluteError)
print("The test r2 for the model is: %2f"% pred_results.r2)

这里是结果:

The MSE for the model is: 31668.331218
The r2 for the model is: 0.613300

根据模型的结果,我们可以进一步调整它。我们将在下一节中看到实现这一目标的一些技术。

交叉验证

交叉验证是提高机器学习模型性能的不同方法之一。

通过将数据分成多个子集进行训练和评估,交叉验证用于更稳健地评估模型性能。因此,我们不仅使用训练和测试数据,还使用一个验证集,模型从未见过这些数据,仅用于衡量性能。

交叉验证遵循一个简单的原则:而不是依赖于单一的训练-测试分割,我们将数据集分成多个子集,或称为。每个折作为一个迷你训练-测试分割,其中一部分数据用于训练,其余部分保留用于测试。通过旋转折,我们确保每个数据点都有机会成为测试集的一部分,从而减轻偏差,提供更具有代表性的评估。

最常见的交叉验证形式是k 折交叉验证。在这种方法中,数据集被分为 k 个大小相等的折。模型被训练和评估 k 次,每次每个折作为测试集,而其余的折共同组成训练集。通过平均每次迭代获得的表现指标,我们得到对模型性能的更稳健估计。

通过交叉验证,我们获得了关于模型泛化能力的宝贵见解。它使我们能够衡量模型在不同数据子集中的性能,捕捉数据集中存在的固有变化和细微差别。这种技术帮助我们检测潜在的过拟合问题,即模型在训练集上表现异常出色,但无法泛化到未见过的数据。

除了 k 折交叉验证之外,还有针对特定场景的变体和扩展。分层交叉验证确保每个折保持与原始数据集相同的类别分布,从而保持分割的代表性。另一方面,留一法交叉验证将每个数据点视为一个单独的折,提供严格的评估,但代价是增加了计算复杂性。

接下来,我们将学习超参数调优。

超参数调优

超参数调优是优化机器学习算法超参数的过程,以提高其性能。超参数是模型外部的设置或配置,不能直接从训练数据中学习。与在训练过程中学习的模型参数不同,超参数需要在事先指定,并在确定机器学习模型的行为和性能方面至关重要。我们将使用超参数调优来提高模型性能。超参数是在训练之前设置的参数,不是从数据中学习的。调整超参数可以显著影响模型的表现。

想象一下:我们的模型是一个复杂的机械装置,由各种被称为超参数的旋钮和杠杆组成。这些超参数控制着我们的模型的行为和特征,影响着其学习、泛化和做出准确预测的能力。然而,找到这些超参数的最佳配置并非易事。

超参数调优是系统地搜索和选择最佳超参数组合的艺术。它使我们能够超越默认设置,发现与我们的数据和谐一致的配置,提取最有意义的见解并实现卓越的性能。

超参数调优的目标是获得最佳值。我们探索不同的超参数设置,穿越一个可能性的多维景观。这种探索可以采取多种形式,例如网格搜索、随机搜索,或者更高级的技术,如贝叶斯优化或遗传算法。

网格搜索是一种流行的方法,它涉及为每个超参数定义一个潜在值的网格。然后,模型在网格中的每个可能组合上进行训练和评估。通过彻底搜索网格,我们发现了产生最高性能的配置,为我们进一步优化提供了坚实的基础。

随机搜索采取不同的方法。它从预定义的分布中随机采样超参数值,并对每个采样的配置评估模型的性能。这种随机探索使我们能够覆盖更广泛的可能范围,可能发现非常规但非常有效的配置。

这些例子展示了使用 PySpark 进行模型训练和评估的过程。然而,具体算法、评估指标和技术可能因所面临的机器学习任务而异。理解问题域、选择合适的算法和选择相关的评估指标对于有效地训练和评估模型至关重要。

模型部署

模型部署是将训练好的机器学习模型用于生产环境的过程。在本节中,我们将探讨有效部署机器学习模型的多种方法和技巧:

  • 序列化和持久化:一旦模型被训练,它需要被序列化并持久化到磁盘以供以后使用。序列化是将模型对象转换为可以存储的格式的过程,而持久化涉及将序列化的模型保存到存储系统中。

  • 模型服务:模型服务涉及将训练好的模型作为 API 端点或服务提供,该服务可以接收输入数据并返回预测结果。这允许其他应用程序或系统集成并使用该模型进行实时预测。

模型监控和管理

一旦模型被部署,重要的是要监控其在生产环境中的性能和行为,并保持其长期的有效性。监控可以帮助识别数据漂移、模型退化或异常等问题。此外,模型管理涉及版本控制、跟踪和维护已部署模型的多个版本。这些做法确保模型保持最新状态,并随着时间的推移保持最佳性能。在本节中,我们将探讨模型监控和维护的关键方面:

  • 可扩展性和性能:在部署机器学习模型时,可扩展性和性能是至关重要的考虑因素。模型应设计并部署为能够高效处理大量数据并满足高吞吐量需求的方式。例如,Apache Spark 等技术提供了分布式计算能力,使得可扩展且高性能的模型部署成为可能。

  • 模型更新和重新训练:机器学习模型可能需要定期更新或重新训练,以适应变化的数据模式或提高性能。部署的模型应具备机制,以便在不中断服务流程的情况下方便地进行更新和重新训练。这可能涉及自动化流程,例如监控数据漂移或基于特定条件的重新训练触发器。

  • 性能指标:为了监控已部署的模型,定义和跟踪相关的性能指标非常重要。这些指标可能因机器学习问题的类型和应用程序的具体要求而异。一些常用的性能指标包括准确率、精确率、召回率、F1 分数和ROC 曲线下面积AUC)。通过定期评估这些指标,可以识别出与预期性能的偏差,这表明需要进行进一步调查或维护操作。

  • 数据漂移检测:数据漂移是指输入数据的统计属性随时间变化的现象,这会导致模型性能下降。监控数据漂移对于确保部署的模型持续提供准确预测至关重要。可以使用诸如统计测试、特征分布比较和异常值检测等技术来检测数据漂移。当检测到数据漂移时,可能需要更新模型或使用更近期的数据进行重新训练。

  • 模型性能监控:监控部署模型的性能涉及跟踪其预测并将其与真实值进行比较。这可以通过定期采样预测子集并评估其实际结果来实现。监控还可以包括分析预测错误、识别模式或异常以及调查任何性能下降的根本原因。通过定期监控模型的性能,可以及早发现问题并采取纠正措施。

  • 模型重新训练和更新:在生产中部署的模型可能需要定期更新或重新训练以保持其有效性。当有新数据可用或应用领域发生重大变化时,使用新鲜数据重新训练模型可以帮助提高其性能。此外,错误修复、功能增强或算法改进可能需要更新部署的模型。建立良好的流程和基础设施来高效地处理模型重新训练和更新至关重要。

  • 版本控制和模型治理:维护部署模型的适当版本控制和治理对于跟踪变更、保持可重复性和确保合规性至关重要。版本控制系统可用于管理模型版本、跟踪变更并提供模型更新的历史记录。此外,维护与模型变更、依赖关系和相关流程相关的文档有助于有效的模型治理。

  • 协作与反馈:模型监控和维护通常涉及不同利益相关者之间的协作,包括数据科学家、工程师、领域专家和业务用户。建立反馈和沟通渠道可以促进见解的交流、问题的识别和必要变更的实施。定期的会议或反馈循环有助于将模型的性能与应用的演变需求保持一致。

总体而言,模型部署是机器学习生命周期中的关键步骤。它涉及序列化和持久化训练好的模型,将它们作为 API 或服务提供,监控其性能,确保可扩展性和性能,以及管理更新和重新训练。

通过积极监控和维护部署的模型,组织可以确保其机器学习系统持续提供准确和可靠的预测。有效的模型监控技术与主动维护策略相结合,能够及时识别性能问题,并支持必要的行动,以保持模型更新并与业务目标保持一致。

模型迭代与改进

模型迭代与改进是机器学习生命周期中的一个关键阶段,专注于提升部署模型的性能和有效性。通过持续优化和改进模型,组织可以实现更好的预测,并从其机器学习项目中获得更大的价值。在本节中,我们将探讨模型迭代和改进的关键方面:

  • 收集反馈和获取洞察:模型迭代和改进的第一步是从各种利益相关者那里收集反馈,包括最终用户、领域专家和业务团队。反馈可以提供有关模型性能、改进领域和在实际场景中遇到的潜在问题的宝贵见解。可以通过调查、用户访谈或在生产环境中监控模型的运行行为来收集此类反馈。

  • 分析模型性能:为了确定改进领域,重要的是要彻底分析模型的性能。这包括检查性能指标、评估预测误差以及深入分析被错误分类或预测不佳的实例。通过了解模型的优点和缺点,数据科学家可以将精力集中在需要关注的特定领域。

  • 探索新特征和数据:提升模型性能的一种方法是通过整合新特征或利用额外的数据源。探索性数据分析有助于识别可能对预测有重大影响的潜在特征。可以使用特征工程技术,如创建交互项、缩放或转换变量,来增强数据的表示。此外,整合来自不同来源的新数据可以提供新的见解并提高模型的一般化能力。

  • 算法选择和超参数调整:通过实验不同的算法和超参数,可以显著提升模型性能。数据科学家可以探索替代算法或现有算法的变体,以确定针对特定问题的最佳方法。可以使用网格搜索或贝叶斯优化等超参数调整技术来找到模型参数的最优值。这一迭代过程有助于确定最佳算法和参数设置,从而产生更优的结果。

  • 集成方法:集成方法涉及结合多个模型以创建更稳健和准确的预测。可以应用诸如 bagging、boosting 或 stacking 等技术,从多个基础模型构建集成模型。集成方法通常可以通过减少偏差、方差和过拟合来提高模型性能。尝试不同的集成策略和模型组合可以进一步提高预测准确性。

  • A/B 测试和受控实验:可以在受控环境中进行 A/B 测试或受控实验,以评估模型改进的影响。通过随机分配用户或数据样本到不同版本的模型,组织可以衡量新模型与现有模型的性能。这种方法提供了具有统计学意义的成果,以确定所提出的更改是否导致了预期的改进。

  • 持续监控与评估:一旦改进后的模型部署,持续的监控与评估对于确保其持续性能至关重要。监控数据漂移、分析性能指标以及定期评估有助于识别潜在的退化或进一步改进的需求。这个反馈循环允许对部署的模型进行持续迭代和优化。

通过拥抱迭代和改进的文化,组织可以持续提升其机器学习模型的性能和准确性。通过收集反馈、分析模型性能、探索新的特性和算法、进行实验以及持续监控,模型可以迭代优化以实现更好的预测并推动可衡量的业务成果。

案例研究和现实世界案例

在本节中,我们将探讨机器学习的两个突出用例:客户流失预测和欺诈检测。这些示例展示了机器学习技术在解决现实世界挑战和实现显著商业价值方面的实际应用。

客户流失预测

客户流失是指客户与公司终止关系的现象,通常是通过取消订阅或转向竞争对手来实现的。预测客户流失对商业至关重要,因为它允许企业主动识别有离开风险的客户并采取适当的措施来留住他们。机器学习模型可以分析各种客户属性和行为模式来预测流失的可能性。让我们深入了解一个客户流失预测案例研究。

案例研究 – 电信公司

一家电信公司希望通过预测哪些客户最有可能取消他们的订阅来降低客户流失率。公司收集了大量的客户数据,包括人口统计信息、通话记录、服务使用情况和客户投诉。通过利用机器学习,他们旨在识别客户流失的关键指标并构建一个预测模型来预测未来的流失者:

  • 数据准备:公司收集并预处理客户数据,确保数据清洁、格式化并准备好分析。他们将客户档案与历史流失信息相结合,创建标记数据集。

  • 特征工程:为了捕捉有意义的模式,公司从可用数据中工程相关特征。这些特征可能包括平均通话时长、投诉数量、月度服务使用量和任期等变量。

  • 模型选择和训练:公司选择合适的机器学习算法,例如逻辑回归、决策树或随机森林,来构建流失预测模型。他们将数据集分为训练集和测试集,在训练数据上训练模型,并在测试数据上评估其性能。

  • 模型评估:使用评估指标如准确率、精确率、召回率和 F1 分数来评估模型的表现。公司分析模型正确识别流失客户和非流失客户的能力,在误报和漏报之间取得平衡。

  • 模型部署和监控:一旦模型达到预期的性能标准,它就被部署到生产环境中。模型持续监控传入的客户数据,生成流失预测,并为处于风险中的客户触发适当的保留策略。

欺诈检测

欺诈检测是机器学习的另一个关键应用,旨在识别欺诈活动并防止财务损失。机器学习模型可以从历史数据中学习欺诈行为的模式,并在实时中标记可疑的交易或活动。让我们探讨一个欺诈检测案例研究。

案例研究 - 金融机构

一家金融机构希望实时检测欺诈交易,以保护其客户并防止货币损失。该机构收集交易数据,包括交易金额、时间戳、商户信息和客户详情。通过利用机器学习算法,他们旨在构建一个强大的欺诈检测系统:

  • 数据预处理:金融机构处理和清理交易数据,确保数据完整性和一致性。他们还可能通过整合额外的信息,如 IP 地址或设备标识符,来增强欺诈检测能力。

  • 特征工程:从交易数据中提取相关特征,以捕捉潜在的欺诈活动指标。这些特征可能包括交易金额、频率、地理位置、与典型消费模式的偏差以及客户交易历史。

  • 模型训练:金融机构选择合适的机器学习算法,例如异常检测技术或监督学习方法(例如,逻辑回归和梯度提升),以训练欺诈检测模型。该模型在标记为欺诈或非欺诈的历史数据上训练。

  • 实时监控:一旦模型经过训练,它就会被部署以实时分析传入的交易。模型为每笔交易分配一个欺诈概率分数,超过一定阈值的交易将被标记为需要进一步调查或干预。

  • 持续改进:金融机构通过监控其性能并整合新数据,持续优化欺诈检测模型。他们定期评估模型的有效性,调整阈值,并更新模型以适应不断变化的欺诈模式和技巧。

通过将 ML 技术应用于客户流失预测和欺诈检测,组织可以增强其决策过程,提高客户保留率,并减轻财务风险。这些案例研究突出了 ML 在现实场景中的实际应用,展示了其在各个行业中的价值。

Spark ML 和分布式 ML 的未来趋势

随着机器学习领域的持续发展,我们可以在 Spark ML 和分布式 ML 领域期待一些未来的趋势和进步。以下是一些关键领域值得关注:

  • 深度学习集成:Spark ML 很可能将更深入地与深度学习框架(如 TensorFlow 和 PyTorch)集成。这将使用户能够无缝地将深度学习模型集成到他们的 Spark ML 管道中,释放神经网络在复杂任务(如图像识别和自然语言处理)中的力量。

  • 自动化机器学习:自动化将在简化并加速机器学习过程中发挥重要作用。我们可以在 Spark ML 中期待自动化特征工程、超参数调整和模型选择技术的进步。这些进步将使用户能够以最小的手动努力构建高性能模型。

  • 可解释人工智能:随着机器学习模型中透明度和可解释性的需求增长,Spark ML 很可能将采用模型可解释性的技术。这将使用户能够理解和解释模型做出的预测,使模型更加可靠,并符合监管要求。

  • 生成式人工智能(GenAI):GenAI 是当前的热门话题。随着 GenAI 用例的需求增加,现有平台可能会整合一些用于 GenAI 的 LLMs(大型语言模型)。

  • 边缘计算和物联网:随着边缘计算和 物联网(IoT) 的兴起,Spark ML 预计将扩展其功能,以支持在边缘设备上进行 ML 推理和训练。这将实现实时、低延迟的预测和跨边缘设备的分布式学习,为智能城市、自动驾驶汽车和工业物联网等领域的应用开辟新的可能性。

由此,我们结束了本书的学习部分。让我们简要回顾一下我们所学的内容。

概述

总结来说,Spark ML 为分布式机器学习任务提供了一个强大且可扩展的框架。它与 Apache Spark 的集成在处理大规模数据集、并行计算和容错方面提供了显著优势。在本章中,我们探讨了 Spark ML 的关键概念、技术和实际应用案例。

我们讨论了机器学习生命周期,强调了数据准备、模型训练、评估、部署、监控和持续改进的重要性。我们还比较了 Spark MLlib 和 Spark ML,突出了它们各自的特点和用例。

在本章中,我们讨论了与 Spark ML 相关的各种关键概念和技术。我们探讨了不同类型的机器学习,如分类、回归、时间序列分析、监督学习和无监督学习。我们强调了数据准备和特征工程在构建有效的机器学习管道中的重要性。我们还简要提到了 Spark ML 中的容错和可靠性方面,确保了鲁棒性和数据完整性。

此外,我们考察了实际应用案例,包括客户流失预测和欺诈检测,以展示 Spark ML 在解决复杂商业挑战中的实际应用。这些案例研究展示了组织如何利用 Spark ML 来增强决策能力、提高客户保留率并减轻财务风险。

在你继续使用 Spark ML 进行机器学习之旅时,重要的是要记住该领域的迭代和动态特性。保持对最新进展的了解,探索新技术,并拥抱持续学习和改进的心态。

通过利用 Spark ML 的力量,你可以从数据中解锁有价值的见解,构建复杂的机器学习模型,并做出推动业务成功的明智决策。因此,利用 Spark ML 的能力,拥抱未来趋势,踏上掌握分布式机器学习的旅程。

本章到此结束。希望它能帮助你在机器学习模型世界的精彩旅程中取得进步。接下来的两章是模拟测试,旨在帮助你为认证考试做好准备。

第五部分:模拟试卷

本部分将提供两份模拟试卷,帮助读者通过练习题目来准备认证考试。

本部分包含以下章节:

  • 第九章**, 模拟试卷 1

  • 第十章**, 模拟试卷 2

第九章:模拟测试 1

问题

尝试回答这些问题以测试你对 Apache Spark 的了解:

问题 1

以下哪个陈述没有准确地描述 Spark 驱动程序的功能?

  1. Spark 驱动程序作为运行 Spark 应用程序主方法的节点,用于协调应用程序。

  2. Spark 驱动程序可以水平扩展以提高整体处理吞吐量。

  3. Spark 驱动程序包含 SparkContext 对象。

  4. Spark 驱动程序负责使用集群模式下的不同工作节点调度数据的执行。

  5. 最佳性能要求 Spark 驱动程序应尽可能靠近工作节点。

问题 2

以下哪个陈述准确地描述了阶段?

  1. 阶段内的任务可以由多台机器同时执行。

  2. 作业中的各个阶段可以并发运行。

  3. 阶段由一个或多个作业组成。

  4. 阶段在提交之前暂时存储事务。

问题 3

以下哪个陈述准确地描述了 Spark 的集群执行模式?

  1. 集群模式在网关节点上运行执行器进程。

  2. 集群模式涉及驱动程序托管在网关机器上。

  3. 在集群模式下,Spark 驱动程序和集群管理器不是位于同一位置的。

  4. 集群模式下的驱动程序位于工作节点上。

问题 4

以下哪个陈述准确地描述了 Spark 的客户端执行模式?

  1. 客户端模式在网关节点上运行执行器进程。

  2. 在客户端模式下,驱动程序与执行器位于同一位置。

  3. 在客户端模式下,Spark 驱动程序和集群管理器是位于同一位置的。

  4. 在客户端模式下,驱动程序位于边缘节点上。

问题 5

以下哪个陈述准确地描述了 Spark 的独立部署模式?

  1. 独立模式为每个应用程序在每个工作节点上使用一个执行器。

  2. 在独立模式下,驱动程序位于工作节点上。

  3. 在独立模式下,集群不需要驱动程序。

  4. 在独立模式下,驱动程序位于边缘节点上。

问题 6

Spark 中的任务是什么?

  1. 每个数据分区在任务中执行的工作单元是槽。

  2. 任务是 Spark 中可以执行的第二小实体。

  3. 具有广泛依赖关系的任务可以合并为单个任务。

  4. 任务是 Spark 中分区执行的单个工作单元。

问题 7

以下哪个是 Spark 执行层次结构中的最高级别?

  1. 任务

  2. 任务

  3. 执行器

  4. 阶段

问题 8

如何在 Spark 的上下文中准确描述槽的概念?

  1. 槽的创建和终止与执行器的工作负载相一致。

  2. Spark 通过在各个槽之间策略性地存储数据来增强 I/O 性能。

  3. 每个槽始终被限制在单个核心上。

  4. 槽允许任务并行运行。

问题 9

Spark 中执行器的角色是什么?

  1. 执行器的角色是将操作请求转换为 DAG。

  2. Spark 环境中只能有一个执行器。

  3. 执行器以优化和分布式的方式处理分区

  4. 执行器安排查询以执行

问题 10:

Shuffle 在 Spark 中的作用是什么?

  1. Shuffle 将变量广播到不同的分区

  2. 使用 shuffle,数据会被写入磁盘

  3. Shuffle 命令在 Spark 中转换数据

  4. Shuffle 是一种窄转换

问题 11:

Actions 在 Spark 中的作用是什么?

  1. Actions 只从磁盘读取数据

  2. Actions 用于修改现有的 RDD

  3. Actions 触发任务的执行

  4. Actions 用于建立阶段边界

问题 12:

以下哪项是 Spark 中集群管理器的一项任务?

  1. 在执行器失败的情况下,集群管理器将与驱动器协作以启动一个新的执行器

  2. 集群管理器可以将分区合并以增加复杂数据处理的速度

  3. 集群管理器收集查询的运行时统计信息

  4. 集群管理器创建查询计划

问题 13:

以下哪项是 Spark 中自适应查询执行的一项任务?

  1. 自适应查询执行可以合并分区以增加复杂数据处理的速度

  2. 在执行器失败的情况下,自适应查询执行功能将与驱动器协作以启动一个新的执行器

  3. 自适应查询执行创建查询计划

  4. 自适应查询执行负责在 Spark 中生成多个执行器以执行任务

问题 14:

以下哪项操作被认为是转换?

  1. df.select()

  2. df.show()

  3. df.head()

  4. df.count()

问题 15:

Spark 中懒加载评估的一个特性是什么?

  1. Spark 只在执行期间失败作业,而不是在定义期间

  2. Spark 只在定义期间失败作业

  3. Spark 在收到转换操作时会执行

  4. Spark 在收到操作时会失败

问题 16:

以下关于 Spark 执行层次结构的哪个陈述是正确的?

  1. 在 Spark 的执行层次结构中,任务位于作业之上

  2. 在 Spark 的执行层次结构中,多个作业包含在一个阶段中

  3. 在 Spark 的执行层次结构中,一个作业可能跨越多个阶段边界

  4. 在 Spark 的执行层次结构中,slot 是最小的单元

问题 17:

以下哪项是 Spark 驱动的特征?

  1. 当驱动器发送命令时,工作节点负责将 Spark 操作转换为 DAG

  2. Spark 驱动负责执行任务并将结果返回给执行器

  3. Spark 驱动可以通过添加更多机器来扩展,从而提高 Spark 任务的性能

  4. Spark 驱动以优化和分布式的方式处理分区

问题 18:

以下关于广播变量的哪个陈述是准确的?

  1. 广播变量仅存在于驱动节点上

  2. 广播变量只能用于适合内存的表

  3. 广播变量不是不可变的,这意味着它们可以在集群之间共享

  4. 广播变量不会在工作节点之间共享

问题 19:

以下哪个代码块返回了 DataFrame dfemployee_stateemployee_salary 列的唯一值?

  1. Df.select('employee_state').join(df.select('employee_salary'), col('employee_state')==col('employee_salary'), 'left').show()

  2. df.select(col('employee_state'), col('employee_salary')).agg({'*': 'count'}).show()

  3. df.select('employee_state', 'employee_salary').distinct().show()

  4. df.select('employee_state').union(df.select('employee_salary')).distinct().show()

问题 20:

以下哪个代码块从 my_fle_path 位置读取名为 my_file.parquet 的 Parquet 文件到 DataFrame df

  1. df = spark.mode("parquet").read("my_fle_path/my_file.parquet")

  2. df = spark.read.path("my_fle_path/my_file.parquet")

  3. df = spark.read().parquet("my_fle_path/my_file.parquet")

  4. df = spark.read.parquet("/my_fle_path/my_file.parquet")

问题 21:

以下哪个代码块对 salarydfemployeedf DataFrame 的 employeeSalaryIDemployeeID 列执行了内连接?

  1. salarydf.join(employeedf, salarydf.employeeID == employeedf.employeeSalaryID)

    1. Salarydf.createOrReplaceTempView(salarydf)

    2. employeedf.createOrReplaceTempView('employeedf')

    3. spark.sql("SELECT * FROM salarydf CROSS JOIN employeedf ON employeeSalaryID ==employeeID")

    1. salarydf

    2. .``join(employeedf, col(employeeID)==col(employeeSalaryID))

    1. Salarydf.createOrReplaceTempView(salarydf)

    2. employeedf.createOrReplaceTempView('employeedf')

    3. SELECT * FROM salarydf

    4. INNER JOIN employeedf

    5. ON salarydf.employeeSalaryID == employeedf. employeeID

问题 22:

以下哪个代码块按列 salary 降序排序返回 df DataFrame,并显示最后的缺失值?

  1. df.sort(nulls_last("salary"))

  2. df.orderBy("salary").nulls_last()

  3. df.sort("salary", ascending=False)

  4. df.nulls_last("salary")

问题 23:

以下代码块包含一个错误。该代码块应该返回一个 df DataFrame 的副本,其中列名 state 被更改为 stateID。找出错误。

代码块:

df.withColumn("stateID", "state")
  1. 方法中的参数 "stateID""state" 应该交换

  2. 应该将 withColumn 方法替换为 withColumnRenamed 方法

  3. 应该将 withColumn 方法替换为 withColumnRenamed 方法,并且需要重新排序方法的参数

  4. 没有这样的方法可以更改列名

问题 24:

以下哪个代码块在 salarydfemployeedf DataFrame 之间使用 employeeIDsalaryEmployeeID 列作为连接键执行了内连接?

  1. salarydf.join(employeedf, "inner", salarydf.employeedf == employeeID.salaryEmployeeID)

  2. salarydf.join(employeedf, employeeID == salaryEmployeeID)

  3. salarydf.join(employeedf, salarydf.salaryEmployeeID == employeedf.employeeID, "inner")

  4. salarydf.join(employeedf, salarydf.employeeID == employeedf.salaryEmployeeID, "inner")`

问题 25:

以下代码块应返回一个df DataFrame,其中employeeID列被转换为整数。请选择正确填充代码块空白的答案以完成此操作:

df.__1__(__2__.__3__(__4__))
    1. select

    2. col("employeeID")

    3. as

    4. IntegerType

    1. select

    2. col("employeeID")

    3. as

    4. Integer

    1. cast

    2. "``employeeID"

    3. as

    4. IntegerType()

    1. select

    2. col("employeeID")

    3. cast

    4. IntegerType()

问题 26:

查找在将employeedfsalarydf DataFrames 按employeeIDemployeeSalaryID列分别连接后,结果 DataFrame 中列 department 不为空的记录数。以下哪些代码块(按顺序)应执行以实现此目的?

  1. .filter(col("department").isNotNull())

  2. .count()

  3. employeedf.join(salarydf, employeedf.employeeID == salarydf.employeeSalaryID)

  4. employeedf.join(salarydf, employeedf.employeeID ==salarydf. employeeSalaryID, how='inner')`

  5. .filter(col(department).isnotnull())

  6. .sum(col(department))

  7. 3, 1, 6

  8. 3, 1, 2

  9. 4, 1, 2

  10. 3, 5, 2

问题 27:

以下哪个代码块返回了df DataFrame 中列 state 值唯一的那些行?

  1. df.dropDuplicates(subset=["state"]).show()

  2. df.distinct(subset=["state"]).show()

  3. df.drop_duplicates(subset=["state"]).show()

  4. df.unique("state").show()

问题 28:

以下代码块包含一个错误。该代码块应返回一个包含额外列squared_numberdf DataFrame 副本,该列是列 number 的平方。请找出错误。

代码块:

df.withColumnRenamed(col("number"), pow(col("number"), 0.2).alias("squared_number"))
  1. withColumnRenamed方法的参数需要重新排序

  2. 应将withColumnRenamed方法替换为withColumn方法

  3. 应将withColumnRenamed方法替换为select方法,并将0.2替换为2

  4. 应将参数0.2替换为2

问题 29:

以下哪个代码块返回了一个新的 DataFrame,其中列 salary 被重命名为new_salary,employee 被重命名为new_employeedf DataFrame 中?

  1. df.withColumnRenamed(salary, new_salary).withColumnRenamed(employee, new_employee)

  2. df.withColumnRenamed("salary", "new_salary")

  3. df.withColumnRenamed("employee", "new_employee")

  4. df.withColumn("salary", "``new_salary").withColumn("employee", "new_employee")

  5. df.withColumnRenamed("salary", "``new_salary").withColumnRenamed("employee", "new_employee")

问题 30:

以下哪个代码块返回了一个df DataFrame 的副本,其中列 salary 已被重命名为employeeSalary

  1. df.withColumn(["salary", "employeeSalary"])

  2. df.withColumnRenamed("salary").alias("employeeSalary ")

  3. df.withColumnRenamed("salary", "``employeeSalary ")

  4. df.withColumn("salary", "``employeeSalary ")

问题 31:

以下代码块包含一个错误。代码块应该将 df DataFrame 保存到 my_file_path 路径作为 Parquet 文件,并追加到任何现有的 Parquet 文件。找出错误。

df.format("parquet").option("mode", "append").save(my_file_path)
  1. 代码没有保存到正确的路径

  2. 应该交换 save()format 函数

  3. 代码块缺少对 DataFrameWriter 的引用

  4. 应该覆盖 option 模式以正确写入文件

问题 32:

我们如何将 df DataFrame 从 12 个分区减少到 6 个分区?

  1. df.repartition(12)

  2. df.coalesce(6).shuffle()

  3. df.coalesce(6, shuffle=True)

  4. df.repartition(6)

问题 33:

以下哪个代码块返回一个 DataFrame,其中时间戳列被转换为名为 record_timestamp 的新列,格式为日、月和年?

  1. df.withColumn("record_timestamp", from_unixtime(unix_timestamp(col("timestamp")), "dd-MM-yyyy"))

  2. df.withColumnRenamed("record_timestamp", from_unixtime(unix_timestamp(col("timestamp")), "dd-MM-yyyy"))

  3. df.select ("record_timestamp", from_unixtime(unix_timestamp(col("timestamp")), "dd-MM-yyyy"))

  4. df.withColumn("record_timestamp", from_unixtime(unix_timestamp(col("timestamp")), "MM-dd-yyyy"))

问题 34:

以下哪个代码块通过将 DataFrame salaryDf 的行追加到 DataFrame employeeDf 的行来创建一个新的 DataFrame,而不考虑两个 DataFrame 都有不同的列名?

  1. salaryDf.join(employeeDf)

  2. salaryDf.union(employeeDf)

  3. salaryDf.concat(employeeDf)

  4. salaryDf.unionAll(employeeDf)

问题 35:

以下代码块包含一个错误。代码块应该计算每个部门 employee_salary 列中所有工资的总和。找出错误。

df.agg("department").sum("employee_salary")
  1. 应该使用 avg(col("value")) 而不是 avg("value")

  2. 所有列名都应该用 col() 运算符包裹

  3. "storeId" 和 “value" 应该交换

  4. Agg 应该替换为 groupBy

问题 36:

以下代码块包含一个错误。代码块旨在对 salarydfemployeedf DataFrame 的 employeeSalaryIDemployeeID 列分别执行交叉连接。找出错误。

employeedf.join(salarydf, [salarydf.employeeSalaryID, employeedf.employeeID], "cross")
  1. 参数中的连接类型 "cross" 需要替换为 crossJoin

  2. salarydf.employeeSalaryID, employeedf.employeeID 应替换为 salarydf.employeeSalaryID == employeedf.employeeID

  3. 应该删除 "cross" 参数,因为 "cross" 是默认的连接类型

  4. 应从调用中删除 "cross" 参数,并用 crossJoin 替换 join

问题 37:

以下代码块包含一个错误。代码块应该显示 df DataFrame 的模式。找出错误。

df.rdd.printSchema()
  1. 在 Spark 中,我们无法打印 DataFrame 的模式

  2. printSchema 不能通过 df.rdd 调用,而应该直接从 df 调用

  3. Spark 中没有名为 printSchema() 的方法

  4. 应该使用 print_schema() 方法而不是 printSchema()

问题 38:

以下代码块应该将 df DataFrame 写入到 filePath 路径的 Parquet 文件中,替换任何现有文件。选择正确填充代码块空白处的答案以完成此操作:

df.__1__.format("parquet").__2__(__3__).__4__(filePath)
    1. save

    2. mode

    3. "``ignore"

    4. path

    1. store

    2. with

    3. "``replace"

    4. path

    1. write

    2. mode

    3. "``overwrite"

    4. save

    1. save

    2. mode

    3. "``overwrite"

    4. path

问题 39:

以下代码块包含一个错误。代码块本应按薪资降序对 df DataFrame 进行排序。然后,它应该根据奖金列进行排序,将 nulls 放在最后。找出错误。

df.orderBy ('salary', asc_nulls_first(col('bonus')))
transactionsDf.orderBy('value', asc_nulls_first(col('predError')))
  1. 应该以降序对 salary 列进行排序。此外,它应该被包裹在 col() 操作符中

  2. 应该用 col() 操作符将 salary 列包裹起来

  3. 应该以降序对 bonus 列进行排序,将 nulls 放在最后

  4. 应该使用 desc_nulls_first()bonus 列进行排序

问题 40:

以下代码块包含一个错误。代码块应该使用 square_root_method Python 方法找到 df DataFrame 中 salary 列的平方根,并在新列 sqrt_salary 中返回它。找出错误。

square_root_method_udf = udf(square_root_method)
df.withColumn("sqrt_salary", square_root_method("salary"))
  1. square_root_method 没有指定返回类型

  2. 在第二行代码中,Spark 需要调用 squre_root_method_udf 而不是 square_root_method

  3. udf 未在 Spark 中注册

  4. 需要添加一个新列

问题 41:

以下代码块包含一个错误。代码块应该返回将 employeeID 重命名为 employeeIdColumndf DataFrame。找出错误。

df.withColumn("employeeIdColumn", "employeeID")
  1. 应该使用 withColumnRenamed 方法而不是 withColumn

  2. 应该使用 withColumnRenamed 方法而不是 withColumn,并且参数 "employeeIdColumn" 应该与参数 "employeeID" 交换

  3. 参数 "employeeIdColumn""employeeID" 应该交换

  4. 应该将 withColumn 操作符替换为 withColumnRenamed 操作符

问题 42:

以下哪个代码块会返回一个新的 DataFrame,其列与 DataFrame df 相同,除了 salary 列?

  1. df.drop("salary")

  2. df.drop(col(salary))

  3. df.drop(salary)

  4. df.delete("salary")

问题 43:

以下哪个代码块返回一个 DataFrame,显示 df DataFrame 中 salary 列的平均值,按 department 列分组?

  1. df.groupBy("department").agg(avg("salary"))

  2. df.groupBy(col(department).avg())

  3. df.groupBy("department").avg(col("salary"))

  4. df.groupBy("department").agg(average("salary"))

问题 44:

以下哪个代码块创建了一个 DataFrame,显示基于部门和国家/地区列,年龄大于 35 的 salaryDf DataFrame 中 salary 列的平均值?

  1. salaryDf.filter(col("age") > 35)

  2. .``filter(col("employeeID")

  3. .``filter(col("employeeID").isNotNull())

  4. .``groupBy("department")

  5. .``groupBy("department", "state")

  6. .``agg(avg("salary").alias("mean_salary"))

  7. .``agg(average("salary").alias("mean_salary"))

    1. 1,2,5,6

    2. 1,3,5,6

    3. 1,3,6,7

    4. 1,2,4,6

问题 45:

以下代码块包含一个错误。该代码块需要缓存df DataFrame,以便此 DataFrame 具有容错性。找出错误。

df.persist(StorageLevel.MEMORY_AND_DISK_3)
  1. persist()不是 DataFrame API 的一个函数

  2. 应将df.write()df.persist结合使用以正确写入 DataFrame

  3. 存储级别不正确,应为MEMORY_AND_DISK_2

  4. 应使用df.cache()而不是df.persist()

问题 46:

以下哪个代码块在不重复的情况下连接了salaryDfemployeeDf DataFrame 的行(假设两个 DataFrame 的列相似)?

  1. salaryDf.concat(employeeDf).unique()

  2. spark.union(salaryDf, employeeDf).distinct()

  3. salaryDf.union(employeeDf).unique()

  4. salaryDf.union(employeeDf).distinct()

问题 47:

以下哪个代码块从filePath读取一个完整的 CSV 文件文件夹,包含列标题?

  1. spark.option("header",True).csv(filePath)

  2. spark.read.load(filePath)

  3. spark.read().option("header",True).load(filePath)

  4. spark.read.format("csv").option("header",True).load(filePath)

问题 48:

以下代码块包含一个错误。df DataFrame 包含列[employeeID, salary, 和 department]。该代码块应返回一个仅包含employeeIDsalary`列的 DataFrame。找出错误。

df.select(col(department))
  1. 应在select参数中指定df DataFrame 的所有列名

  2. 应将select运算符替换为drop运算符,并列出df DataFrame 中的所有列名作为列表

  3. 应将select运算符替换为drop运算符

  4. 列名department应列出为col("department")

问题 49:

以下代码块包含一个错误。该代码块应将 DataFrame df作为 Parquet 文件写入到filePath位置,在按department列分区后。找出错误。

df.write.partition("department").parquet()
  1. 应使用partitionBy()方法而不是partition()方法。

  2. 应使用partitionBy()方法而不是partition(),并将filePath添加到parquet方法中

  3. 在写入方法之前应调用partition()方法,并将filePath添加到parquet方法中

  4. 应将"department"列用col()运算符包裹

问题 50:

以下哪个代码块从内存和磁盘中移除了缓存的df DataFrame?

  1. df.unpersist()

  2. drop df

  3. df.clearCache()

  4. df.persist()

问题 51:

以下代码块应该返回一个包含额外列:test_column,其值为19df DataFrame 的副本。请选择正确填充代码块空白处的答案以完成此操作:

df.__1__(__2__, __3__)
    1. withColumn

    2. '``test_column'

    3. 19

    1. withColumnRenamed

    2. test_column

    3. lit(19)

    1. withColumn

    2. 'test_column'

    3. lit(19)

    1. withColumnRenamed

    2. test_column

    3. 19

问题 52:

以下代码块应该返回一个包含 employeeIdsalarybonusdepartment 列的 DataFrame,来自 transactionsDf DataFrame。选择正确填充空白的答案以完成此操作:

df.__1__(__2__)
    1. drop

    2. "employeeId", "salary", "bonus", "department"

    1. filter

    2. "employeeId, salary, bonus, department"

    1. select

    2. ["employeeId", "salary", "bonus", "department"]

    1. select

    2. col(["employeeId", "salary", "bonus", "department"])

问题 53:

以下哪个代码块返回了一个 DataFrame,其中 salary 列在 df DataFrame 中被转换为字符串?

  1. df.withColumn("salary", castString("salary", "string"))`

  2. df.withColumn("salary", col("salary").cast("string"))

  3. df.select(cast("salary", "string"))

  4. df.withColumn("salary", col("salary").castString("string"))

问题 54:

以下代码块包含错误。该代码块应该结合来自 salaryDfemployeeDf DataFrame 的数据,显示 salaryDf DataFrame 中所有与 employeeDf DataFrame 中 employeeSalaryID 列的值匹配的行。找出错误。

employeeDf.join(salaryDf, employeeDf.employeeID==employeeSalaryID)
  1. join 语句缺少右侧的 DataFrame,其中列名为 employeeSalaryID

  2. 应该使用 union 方法而不是 join

  3. 应该使用 innerJoin 而不是 join

  4. salaryDf 应该替换 employeeDf

问题 55:

以下哪个代码块读取存储在 my_file_path 的 JSON 文件作为 DataFrame?

  1. spark.read.json(my_file_path)

  2. spark.read(my_file_path, source="json")

  3. spark.read.path(my_file_path)

  4. spark.read().json(my_file_path)

问题 56:

以下代码块包含错误。该代码块应该返回一个新的 DataFrame,通过过滤 df DataFrame 中 salary 列大于 2000 的行。找出错误。

df.where("col(salary) >= 2000")
  1. 应该使用 filter() 而不是 where()

  2. where 方法的参数应该是 "col(salary) > 2000"

  3. 应该使用 > 操作符而不是 >=

  4. where 方法的参数应该是 "salary > 2000"

问题 57:

以下哪个代码块返回了一个 DataFrame,其中从 df DataFrame 中删除了 salarystate 列?

  1. df.withColumn ("salary", "state")

  2. df.drop(["salary", "state"])

  3. df.drop("salary", "state")

  4. df.withColumnRenamed ("salary", "state")

问题 58:

以下哪个代码块返回了一个包含 df DataFrame 中每个部门计数的两列 DataFrame?

  1. df.count("department").distinct()

  2. df.count("department")

  3. df.groupBy("department").count()

  4. df.groupBy("department").agg(count("department"))

问题 59:

以下哪个代码块打印了 DataFrame 的模式,并包含列名和类型?

  1. print(df.columns)

  2. df.printSchema()

  3. df.rdd.printSchema()

  4. df.print_schema()

问题 60:

以下哪个代码块创建了一个新的 DataFrame,包含三个列:department(部门),age(年龄)和max_salary(最高薪水),并且对于每个部门以及每个年龄组的每个员工都有最高的薪水?

  1. df.max(salary)

  2. df.groupBy(["department", "age"]).agg(max("salary").alias("max_salary"))

  3. df.agg(max(salary).alias(max_salary')

  4. df.groupby(department).agg(max(salary).alias(max_salary)

答案

  1. B

  2. A

  3. D

  4. D

  5. A

  6. D

  7. A

  8. D

  9. C

  10. B

  11. C

  12. A

  13. A

  14. A

  15. A

  16. C

  17. B

  18. B

  19. D

  20. D

  21. D

  22. C

  23. C

  24. D

  25. D

  26. C

  27. A

  28. C

  29. E

  30. C

  31. C

  32. D

  33. A

  34. B

  35. D

  36. B

  37. B

  38. C

  39. A

  40. B

  41. B

  42. A

  43. A

  44. A

  45. C

  46. D

  47. D

  48. C

  49. B

  50. A

  51. C

  52. C

  53. B

  54. A

  55. A

  56. D

  57. C

  58. C

  59. B

  60. B

第十章:模拟测试 2

问题

尝试回答这些问题以测试你对 Apache Spark 的了解:

问题 1:

Spark 中的任务是什么?

  1. 在任务中为每个数据分区执行的工作单元是插槽

  2. 任务是 Spark 中可以执行的第二小实体

  3. 具有宽依赖的任务可以合并为单个任务

  4. 任务是 Spark 中可以执行的最小组件

问题 2:

执行器在 Spark 中的角色是什么?

  1. 执行器的角色是请求将操作转换为有向无环图 (DAG)

  2. Spark 环境中只能有一个执行器

  3. 执行器负责执行驱动程序分配给它们的任务

  4. 执行器安排查询以执行

问题 3:

以下哪个是自适应查询执行在 Spark 中的任务之一?

  1. 自适应查询执行在查询执行期间收集运行时统计信息以优化查询计划

  2. 自适应查询执行负责将任务分配给执行器

  3. 自适应查询执行负责 Spark 中的宽操作

  4. 自适应查询执行负责 Spark 中的容错

问题 4:

Spark 执行层次结构中的最低级别是什么?

  1. 任务

  2. 插槽

  3. 作业

  4. 阶段

问题 5:

以下哪个操作是动作?

  1. DataFrame.count()

  2. DataFrame.filter()

  3. DataFrame.select()

  4. DataFrame.groupBy()

问题 6:

以下哪个描述了 DataFrame API 的特性?

  1. DataFrame API 在后端基于弹性分布式数据集 (RDD)

  2. DataFrame API 在 Scala 中可用,但在 Python 中不可用

  3. DataFrame API 没有数据操作函数

  4. DataFrame API 用于在执行器中分配任务

问题 7:

以下哪个关于执行器的陈述是准确的?

  1. 插槽不是执行器的一部分

  2. 执行器能够通过插槽并行运行任务

  3. 执行器始终等于任务

  4. 执行器负责为作业分配任务

问题 8:

以下哪个关于 Spark 驱动程序的陈述是准确的?

  1. Spark 应用程序中有多个驱动程序

  2. 插槽是驱动程序的一部分

  3. 驱动程序并行执行任务

  4. 将操作转换为 DAG 计算的责任在于 Spark 驱动程序

问题 9:

以下哪个操作是宽转换?

  1. DataFrame.show()

  2. DataFrame.groupBy()

  3. DataFrame.repartition()

  4. DataFrame.select()

  5. DataFrame.filter()

问题 10:

以下哪个关于惰性评估的陈述是正确的?

  1. 执行是由转换触发的

  2. 执行是由动作触发的

  3. 语句按照代码中的顺序执行

  4. Spark 将任务分配给不同的执行器

问题 11:

以下哪个关于 Spark 中的 DAGs 的陈述是正确的?

  1. DAGs 是惰性评估的

  2. DAGs 可以在 Spark 中水平扩展

  3. DAGs 负责以优化和分布式的方式处理分区

  4. DAG 由可以并行运行的任务组成

问题 12:

以下哪个关于 Spark 容错机制的陈述是正确的?

  1. Spark 通过 DAGs 实现容错能力

  2. 使 Spark 具备容错能力是执行器的责任

  3. 由于容错能力,Spark 可以重新计算任何失败的 RDD

  4. Spark 在传统的 RDD 数据系统之上构建了一个容错层,而 RDD 本身并不具备容错能力

问题 13:

Spark 容错机制的核心是什么?

  1. RDD 是 Spark 的核心,它设计上具有容错能力

  2. 数据分区,因为数据可以被重新计算

  3. DataFrame 是 Spark 的核心,因为它是不变的

  4. 执行器确保 Spark 保持容错能力

问题 14:

Spark 中的作业有哪些准确之处?

  1. 作业的不同阶段可以并行执行

  2. 作业的不同阶段不能并行执行

  3. 一个任务由许多作业组成

  4. 一个阶段由许多作业组成

问题 15:

Spark 中的 shuffle 有哪些准确之处?

  1. 在 shuffle 过程中,数据被发送到多个分区进行处理

  2. 在 shuffle 过程中,数据被发送到单个分区进行处理

  3. Shuffle 是一个触发 Spark 评估的操作

  4. 在 shuffle 过程中,所有数据都保留在内存中以便处理

问题 16:

Spark 中的集群管理器有哪些准确之处?

  1. 集群管理器负责管理 Spark 的资源

  2. 集群管理器负责直接与执行器协同工作

  3. 集群管理器负责创建查询计划

  4. 集群管理器负责优化 DAGs

问题 17:

以下代码块需要计算df DataFrame 中每个部门的salary列的总和和平均值。然后,它应该计算bonus列的总和和最大值:

df.___1___ ("department").___2___ (sum("salary").alias("sum_salary"), ___3___ ("salary").alias("avg_salary"), sum("bonus").alias("sum_bonus"), ___4___("bonus").alias("max_bonus") )

选择正确的答案来填充代码块中的空白,以完成此任务:

    1. groupBy

    2. agg

    3. avg

    4. max

    1. filter

    2. agg

    3. avg

    4. max

    1. groupBy

    2. avg

    3. agg

    4. max

    1. groupBy

    2. agg

    3. avg

    4. avg

问题 18:

以下代码块中包含一个错误。代码块需要将salaryDf DataFrame 与较大的employeeDf DataFrame 在employeeID列上连接:

salaryDf.join(employeeDf, "employeeID", how="broadcast")

识别错误:

  1. 代码应该使用innerJoin而不是join

  2. broadcast不是 Spark 中用于连接两个 DataFrames 的join类型

  3. salaryDfemployeeDf应该交换

  4. how参数中,应该使用crossJoin而不是broadcast

问题 19:

以下哪个代码块将df DataFrame 的 shuffle 操作从 5 个分区变为 20 个分区?

  1. df.repartition(5)

  2. df.repartition(20)

  3. df.coalesce(20)

  4. df.coalesce(5)

问题 20:

以下哪个操作将触发评估?

  1. df.filter()

  2. df.distinct()

  3. df.intersect()

  4. df.join()

  5. df.count()

问题 21:

以下哪个代码块返回df DataFrame 中agename列的唯一值,并在各自的列中保持所有值唯一?

  1. df.select('age').join(df.select('name'), col(state) == col('name'), 'inner').show()

  2. df.select(col('age'), col('name')).agg({'*': 'count'}).show()

  3. df.select('age', 'name').distinct().show()

  4. df.select('age').unionAll(df.select('name')).distinct().show()

问题 22

以下哪个代码块返回df DataFrame 中总行数的计数?

  1. df.count()

  2. df.select(col('state'), col('department')).agg({'*': 'count'}).show()

  3. df.select('state', 'department').distinct().show()

  4. df.select('state').union(df.select('department')).distinct().show()

问题 23

以下代码块包含一个错误。代码块应该将df DataFrame 保存为新的 parquet 文件到filePath路径:

df.write.mode("append").parquet(filePath)

识别错误:

  1. 代码块应该有overwrite选项而不是append

  2. 代码应该是write.parquet而不是write.mode

  3. 不能直接从 DataFrame 中调用df.write操作

  4. 代码的第一部分应该是df.write.mode(append)

问题 24

以下哪个代码块向df DataFrame 中添加了一个salary_squared列,该列是salary列的平方?

  1. df.withColumnRenamed("salary_squared", pow(col("salary"), 2))

  2. df.withColumn("salary_squared", col("salary"*2))

  3. df.withColumn("salary_squared", pow(col("salary"), 2))

  4. df.withColumn("salary_squared", square(col("salary")))

问题 25

以下哪个代码块执行了一个连接操作,其中小的salaryDf DataFrame 被发送到所有执行器,以便可以在employeeSalaryIDEmployeeID列上与employeeDf DataFrame 进行连接?

  1. employeeDf.join(salaryDf, "employeeDf.employeeID == salaryDf.employeeSalaryID", "inner")

  2. employeeDf.join(salaryDf, "employeeDf.employeeID == salaryDf.employeeSalaryID", "broadcast")

  3. employeeDf.join(broadcast(salaryDf), employeeDf.employeeID == salaryDf.employeeSalaryID)

  4. salaryDf.join(broadcast(employeeDf), employeeDf.employeeID == salaryDf.employeeSalaryID)

问题 26

以下哪个代码块在salarydf DataFrame 和employeedf DataFrame 之间执行了外连接,使用employeeIDsalaryEmployeeID列作为连接键分别?

  1. Salarydf.join(employeedf, "outer", salarydf.employeedf == employeeID.salaryEmployeeID)

  2. salarydf.join(employeedf, employeeID == salaryEmployeeID)

  3. salarydf.join(employeedf, salarydf.salaryEmployeeID == employeedf.employeeID, "outer")

  4. salarydf.join(employeedf, salarydf.employeeID == employeedf.salaryEmployeeID, "outer")

问题 27

以下哪个代码块会打印出df DataFrame 的模式?

  1. df.rdd.printSchema

  2. df.rdd.printSchema()

  3. df.printSchema

  4. df.printSchema()

问题 28

以下哪个代码块在 salarydf DataFrame 和 employeedf DataFrame 之间执行左连接,使用 employeeID 列?

  1. salaryDf.join(employeeDf, salaryDf["employeeID"] == employeeDf["employeeID"], "outer")

  2. salaryDf.join(employeeDf, salaryDf["employeeID"] == employeeDf["employeeID"], "left")

  3. salaryDf.join(employeeDf, salaryDf["employeeID"] == employeeDf["employeeID"], "inner")

  4. salaryDf.join(employeeDf, salaryDf["employeeID"] == employeeDf["employeeID"], "right")

问题 29:

以下哪个代码块按升序聚合了df DataFrame 中的bonus列,并且nulls值排在最后?

  1. df.agg(asc_nulls_last("bonus").alias("bonus_agg"))

  2. df.agg(asc_nulls_first("bonus").alias("bonus_agg"))

  3. df.agg(asc_nulls_last("bonus", asc).alias("bonus_agg"))

  4. df.agg(asc_nulls_first("bonus", asc).alias("bonus_agg"))

问题 30:

以下代码块包含一个错误。该代码块应该通过在 employeeIDemployeeSalaryID 列上分别连接 employeeDfsalaryDf DataFrame 来返回一个 DataFrame,同时从最终的 DataFrame 中排除 employeeDf DataFrame 中的 bonusdepartment 列以及 salaryDf DataFrame 中的 salary 列。

employeeDf.groupBy(salaryDf, employeeDf.employeeID == salaryDf.employeeSalaryID, "inner").delete("bonus", "department", "salary")

识别错误:

  1. groupBy 应该替换为 innerJoin 操作符

  2. groupBy 应该替换为一个 join 操作符,并且 delete 应该替换为 drop

  3. groupBy 应该替换为 crossJoin 操作符,并且 delete 应该替换为 withColumn

  4. groupBy 应该替换为一个 join 操作符,并且 delete 应该替换为 withColumnRenamed

问题 31:

以下哪个代码块将 /loc/example.csv CSV 文件作为 df DataFrame 读取?

  1. df = spark.read.csv("/loc/example.csv")

  2. df = spark.mode("csv").read("/loc/example.csv")

  3. df = spark.read.path("/loc/example.csv")

  4. df = spark.read().csv("/loc/example.csv")

问题 32:

以下哪个代码块使用名为 my_schema 的模式文件在 my_path 位置读取一个 parquet 文件?

  1. spark.read.schema(my_schema).format("parquet").load(my_path)

  2. spark.read.schema("my_schema").format("parquet").load(my_path)

  3. spark.read.schema(my_schema).parquet(my_path)

  4. spark.read.parquet(my_path).schema(my_schema)

问题 33:

我们想要找到在将employeedfsalarydf DataFrame 在employeeIDemployeeSalaryID列上分别连接时,结果 DataFrame 中的记录数。应该执行哪些代码块来实现这一点?

  1. .``filter(~isnull(col(department)))

  2. .``count()

  3. employeedf.join(salarydf, col("employeedf.employeeID")==col("salarydf.employeeSalaryID"))

  4. employeedf.join(salarydf, employeedf. employeeID ==salarydf. employeeSalaryID, how='inner')

  5. .``filter(col(department).isnotnull())

  6. .``sum(col(department))

    1. 3, 1, 6

    2. 3, 1, 2

    3. 4, 2

    4. 3, 5, 2

问题 34:

以下哪个代码块返回一个 df DataFrame 的副本,其中 state 列的名称已更改为 stateID

  1. df.withColumnRenamed("state", "stateID")

  2. df.withColumnRenamed("stateID", "state")

  3. df.withColumn("state", "stateID")

  4. df.withColumn("stateID", "state")

问题 35:

以下哪个代码块返回一个 df DataFrame 的副本,其中 salary 列已转换为 integer

  1. df.col("salary").cast("integer"))

  2. df.withColumn("salary", col("salary").castType("integer"))

  3. df.withColumn("salary", col("salary").convert("integerType()"))

  4. df.withColumn("salary", col("salary").cast("integer"))

问题 36:

以下哪个代码块将 df DataFrame 分成两半,即使代码多次运行,值也完全相同?

  1. df.randomSplit([0.5, 0.5], seed=123)

  2. df.split([0.5, 0.5], seed=123)

  3. df.split([0.5, 0.5])

  4. df.randomSplit([0.5, 0.5])

问题 37:

以下哪个代码块按两个列,salarydepartment,排序 df DataFrame,其中 salary 是升序,department 是降序?

  1. df.sort("salary", asc("department"))

  2. df.sort("salary", desc(department))

  3. df.sort(col(salary)).desc(col(department))

  4. df.sort("salary", desc("department"))

问题 38:

以下哪个代码块从 salaryDf DataFrame 的 bonus 列计算平均值,并将其添加到名为 average_bonus 的新列中?

  1. salaryDf.avg("bonus").alias("average_bonus"))

  2. salaryDf.agg(avg("bonus").alias("average_bonus"))

  3. salaryDf.agg(sum("bonus").alias("average_bonus"))

  4. salaryDf.agg(average("bonus").alias("average_bonus"))

问题 39:

以下哪个代码块将 df DataFrame 保存到 /FileStore/file.csv 位置作为 CSV 文件,如果位置中已存在文件则抛出错误?

  1. df.write.mode("error").csv("/FileStore/file.csv")

  2. df.write.mode.error.csv("/FileStore/file.csv")

  3. df.write.mode("exception").csv("/FileStore/file.csv")

  4. df.write.mode("exists").csv("/FileStore/file.csv")

问题 40:

以下哪个代码块读取位于 /my_path/my_csv.csv CSV 文件到 DataFrame 中?

  1. spark.read().mode("csv").path("/my_path/my_csv.csv")

  2. spark.read.format("csv").path("/my_path/my_csv.csv")

  3. spark.read("csv", "/my_path/my_csv.csv")

  4. spark.read.csv("/my_path/my_csv.csv")

问题 41:

以下哪个代码块显示 df DataFrame 的前 100 行,其中包含 salary 列,按降序排列?

  1. df.sort(asc(value)).show(100)

  2. df.sort(col("value")).show(100)

  3. df.sort(col("value").desc()).show(100)

  4. df.sort(col("value").asc()).print(100)

问题 42:

以下哪个代码块创建了一个 DataFrame,它显示了基于 departmentstate 列的 salary 列的平均值,其中 age 大于 35,并且返回的 DataFrame 应该按 employeeID 列升序排序,以确保该列没有空值?

  1. salaryDf.filter(col("age") > 35)

  2. .``filter(col("employeeID")

  3. .``filter(col("employeeID").isNotNull())

  4. .``groupBy("department")

  5. .``groupBy("department", "state")

  6. .``agg(avg("salary").alias("mean_salary"))

  7. .``agg(average("salary").alias("mean_salary"))

  8. .``orderBy("employeeID")

    1. 1, 2, 5, 6, 8

    2. 1, 3, 5, 6, 8

    3. 1, 3, 6, 7, 8

    4. 1, 2, 4, 6, 8

问题 43:

以下代码块包含一个错误。代码块应该返回一个新的 DataFrame,不包含 employeesalary 列,并添加一个 fixed_value 列,其值为 100

df.withColumnRenamed(fixed_value).drop('employee', 'salary')

确定错误:

  1. withcolumnRenamed 应该替换为 withcolumn,并且应该使用 lit() 函数来填充 100 的值

  2. withcolumnRenamed 应该替换为 withcolumn

  3. drop 函数中应该交换 employeesalary

  4. lit() 函数调用缺失

问题 44:

以下哪个代码块返回了 df DataFrame 中数值和字符串列的基本统计信息?

  1. df.describe()

  2. df.detail()

  3. df.head()

  4. df.explain()

问题 45:

以下哪个代码块返回了 df DataFrame 的前 5 行?

  1. df.select(5)

  2. df.head(5)

  3. df.top(5)

  4. df.show()

问题 46:

以下哪个代码块创建了一个新的 DataFrame,包含来自 df DataFrame 的 departmentagesalary 列?

  1. df.select("department", "``age", "salary")

  2. df.drop("department", "``age", "salary")

  3. df.filter("department", "``age", "salary")

  4. df.where("department", "``age", "salary")

问题 47:

以下哪个代码块创建了一个新的 DataFrame,包含三个列,departmentagemax_salary,其中每个部门以及每个年龄组的最高工资来自 df DataFrame?

df.___1___ (["department", "age"]).___2___ (___3___ ("salary").alias("max_salary"))

确定正确答案:

    1. filter

    2. agg

    3. max

    1. groupBy

    2. agg

    3. max

    1. filter

    2. agg

    3. sum

    1. groupBy

    2. agg

    3. sum

问题 48:

以下代码块包含一个错误。代码块应该返回一个新的 DataFrame,通过行筛选,其中 salary 列在 df DataFrame 中大于或等于 1000

df.filter(F(salary) >= 1000)

确定错误:

  1. 应该使用 where() 而不是 filter()

  2. 应该将 F(salary) 操作替换为 F.col("salary")

  3. 应该使用 > 操作符而不是 >=

  4. where 方法的参数应该是 "salary > 1000"

问题 49:

以下哪个代码块返回了一个 df DataFrame 的副本,其中 department 列已被重命名为 business_unit

  1. df.withColumn(["department", "business_unit"])

  2. itemsDf.withColumn("department").alias("business_unit")

  3. itemsDf.withColumnRenamed("department", "business_unit")

  4. itemsDf.withColumnRenamed("business_unit", "department")

问题 50:

以下哪个代码块从df DataFrame 返回包含每个部门员工总数的数据帧?

  1. df.groupBy("department").agg(count("*").alias("total_employees"))

  2. df.filter("department").agg(count("*").alias("total_employees"))

  3. df.groupBy("department").agg(sum("*").alias("total_employees"))

  4. df.filter("department").agg(sum("*").alias("total_employees"))

问题 51:

以下哪个代码块从df DataFrame 返回将employee列转换为字符串类型的 DataFrame?

  1. df.withColumn("employee", col("employee").cast_type("string"))

  2. df.withColumn("employee", col("employee").cast("string"))

  3. df.withColumn("employee", col("employee").cast_type("stringType()"))

  4. df.withColumnRenamed("employee", col("employee").cast("string"))

问题 52:

以下哪个代码块返回一个新的 DataFrame,其中包含一个新的fixed_value列,该列在df DataFrame 的所有行中都有Z

  1. df.withColumn("fixed_value", F.lit("Z"))

  2. df.withColumn("fixed_value", F("Z"))

  3. df.withColumnRenamed("fixed_value", F.lit("Z"))

  4. df.withColumnRenamed("fixed_value", lit("Z"))

问题 53:

以下哪个代码块返回一个新的 DataFrame,其中包含一个新的upper_string列,它是df DataFrame 中employeeName列的大写版本?

  1. df.withColumnRenamed('employeeName', upper(df.upper_string))

  2. df.withColumnRenamed('upper_string', upper(df.employeeName))

  3. df.withColumn('upper_string', upper(df.employeeName))

  4. df.withColumn(' employeeName', upper(df.upper_string))

问题 54:

以下代码块包含一个错误。该代码块本应使用 udf 将员工姓名转换为大写:

capitalize_udf = udf(lambda x: x.upper(), StringType())
df_with_capitalized_names = df.withColumn("capitalized_name", capitalize("employee"))

识别错误:

  1. 应该使用capitalize_udf函数而不是capitalize

  2. udf函数capitalize_udf没有正确地转换为大写。

  3. 应该使用IntegerType()而不是StringType()

  4. 应该使用df.withColumn("employee", capitalize("capitalized_name"))代替df.withColumn("capitalized_name", capitalize("employee")),而不是df.withColumn("capitalized_name", capitalize("employee"))

问题 55:

以下代码块包含一个错误。该代码块本应按薪资升序对df DataFrame 进行排序。然后,它应该根据bonus列进行排序,将nulls放在最后。

df.orderBy ('salary', asc_nulls_first(col('bonus')))

识别错误:

  1. salary列应该以降序排序,并使用desc_nulls_last代替asc_nulls_first。此外,它应该被col()运算符包裹。

  2. salary列应该被col()运算符包裹。

  3. 奖金列应该以降序排序,并将 null 值放在最后。

  4. 奖金列应该按照desc_nulls_first()进行排序。

问题 56:

以下代码块包含一个错误。该代码块需要根据 department 列对 df DataFrame 进行分组,并计算每个部门的总工资和平均工资。

df.filter("department").agg(sum("salary").alias("sum_salary"), avg("salary").alias("avg_salary"))

识别错误:

  1. avg 方法也应该通过 agg 函数调用

  2. 应该使用 groupBy 而不是 filter

  3. agg 方法的语法不正确

  4. 应该在 salary 上进行过滤,而不是在 department 上进行过滤

问题 57

哪个代码块将 df DataFrame 写入到 filePath 路径上的 parquet 文件,并按 department 列进行分区?

  1. df.write.partitionBy("department").parquet(filePath)

  2. df.write.partition("department").parquet(filePath)

  3. df.write.parquet("department").partition(filePath)

  4. df.write.coalesce("department").parquet(filePath)

问题 58

df DataFrame 包含列 [employeeID, salary, department]。以下哪段代码将返回只包含列 [employeeID, salary]df DataFrame?

  1. df.drop("department")

  2. df.select(col(employeeID))

  3. df.drop("department", "salary")

  4. df.select("employeeID", "department")

问题 59

以下哪个代码块返回一个新的 DataFrame,其列与 df DataFrame 相同,除了 salary 列?

  1. df.drop(col("salary"))

  2. df.delete(salary)

  3. df.drop(salary)

  4. df.delete("salary")

问题 60

以下代码块包含一个错误。该代码块应该返回将 employeeID 重命名为 employeeIdColumndf DataFrame。

df.withColumnRenamed("employeeIdColumn", "employeeID")

识别错误:

  1. 代替 withColumnRenamed,应该使用 withColumn 方法

  2. 应该使用 withColumn 方法代替 withColumnRenamed,并且将 "employeeIdColumn" 参数与 "employeeID" 参数交换

  3. "employeeIdColumn""employeeID" 参数应该交换

  4. withColumnRenamed 不是一个 DataFrame 的方法

答案

  1. D

  2. C

  3. A

  4. A

  5. A

  6. A

  7. B

  8. D

  9. C

  10. B

  11. C

  12. C

  13. A

  14. B

  15. A

  16. A

  17. A

  18. B

  19. B

  20. E

  21. C

  22. A

  23. A

  24. C

  25. C

  26. D

  27. D

  28. B

  29. A

  30. B

  31. A

  32. A

  33. C

  34. A

  35. D

  36. A

  37. D

  38. B

  39. A

  40. D

  41. C

  42. B

  43. A

  44. A

  45. B

  46. A

  47. B

  48. B

  49. C

  50. A

  51. B

  52. A

  53. C

  54. A

  55. A

  56. B

  57. A

  58. A

  59. A

  60. C

posted @ 2025-10-01 11:27  绝不原创的飞龙  阅读(23)  评论(0)    收藏  举报