PySpark-大规模数据分析精要-全-
PySpark 大规模数据分析精要(全)
原文:
annas-archive.org/md5/b3bdd515fb6b21fd6dc5f2c481c3d18f译者:飞龙
前言
Apache Spark 是一个统一的数据分析引擎,旨在以快速高效的方式处理海量数据。PySpark 是 Apache Spark 的 Python 语言 API,为 Python 开发人员提供了一个易于使用且可扩展的数据分析框架。
可扩展数据分析的必备 PySpark 通过探索分布式计算范式开始,并提供 Apache Spark 的高层次概述。然后,你将开始数据分析之旅,学习执行数据摄取、数据清洗和大规模数据集成的过程。
本书还将帮助你构建实时分析管道,使你能够更快速地获得洞察。书中介绍了构建基于云的数据湖的技术,并讨论了 Delta Lake,它为数据湖带来了可靠性和性能。
本书介绍了一个新兴的范式——数据湖仓(Data Lakehouse),它将数据仓库的结构和性能与基于云的数据湖的可扩展性结合起来。你将学习如何使用 PySpark 执行可扩展的数据科学和机器学习,包括数据准备、特征工程、模型训练和模型生产化技术。还介绍了如何扩展标准 Python 机器学习库的技术,以及在 PySpark 上提供的类似 pandas 的新 API,名为 Koalas。
本书适合人群
本书面向那些已经在使用数据分析探索分布式和可扩展数据分析世界的实践数据工程师、数据科学家、数据分析师、公民数据分析师和数据爱好者。建议你具有数据分析和数据处理领域的知识,以便获得可操作的见解。
本书内容概述
第一章,分布式计算概述,介绍了分布式计算范式。它还讲述了随着过去十年数据量的不断增长,分布式计算为何成为一种必需,并最终介绍了基于内存的数据并行处理概念,包括 Map Reduce 范式,并介绍了 Apache Spark 3.0 引擎的最新功能。
第二章,数据摄取,涵盖了各种数据源,如数据库、数据湖、消息队列,以及如何从这些数据源中摄取数据。你还将了解各种数据存储格式在存储和处理数据方面的用途、差异和效率。
第三章,数据清洗与集成,讨论了各种数据清洗技术,如何处理不良输入数据、数据可靠性挑战以及如何应对这些挑战,以及数据集成技术,以构建单一的集成数据视图。
第四章,实时数据分析,解释了如何进行实时数据的获取和处理,讨论了实时数据集成所面临的独特挑战及其解决方法,以及它所带来的好处。
第五章,使用 PySpark 进行可扩展的机器学习,简要讲解了扩展机器学习的需求,并讨论了实现这一目标的各种技术,从使用原生分布式机器学习算法,到令人尴尬的并行处理,再到分布式超参数搜索。它还介绍了 PySpark MLlib 库,并概述了其各种分布式机器学习算法。
第六章,特征工程——提取、转换与选择,探讨了将原始数据转化为适合机器学习模型使用的特征的各种技术,包括特征缩放和转换技术。
第七章,监督式机器学习,探讨了用于机器学习分类和回归问题的监督学习技术,包括线性回归、逻辑回归和梯度提升树。
第八章,无监督式机器学习,介绍了无监督学习技术,如聚类、协同过滤和降维,以减少应用监督学习前的特征数量。
第九章,机器学习生命周期管理,解释了仅仅构建和训练模型是不够的,在现实世界中,同一个模型会构建多个版本,并且不同版本适用于不同的应用。因此,有必要跟踪各种实验、它们的超参数、指标,以及它们训练所用的数据版本。还需要在一个集中可访问的库中跟踪和存储各种模型,以便能够轻松地将模型投入生产并进行共享;最后,还需要机制来自动化这一重复出现的过程。本章通过一个端到端的开源机器学习生命周期管理库 MLflow 介绍了这些技术。
第十章**, 使用 PySpark 进行单节点机器学习的横向扩展,解释了在第五章**, 使用 PySpark 进行可扩展机器学习中,您学习了如何利用 Apache Spark 的分布式计算框架在大规模上训练和评分机器学习模型。Spark 的本地机器学习库很好地覆盖了数据科学家通常执行的标准任务;然而,标准的单节点 Python 库提供了多种功能,但这些库并非为分布式方式而设计。本章介绍了如何将标准 Python 数据处理和机器学习库(如 pandas、scikit-learn 和 XGBoost)进行横向扩展。本章将涵盖常见数据科学任务的横向扩展,如探索性数据分析、模型训练、模型推理,最后还将介绍一个可扩展的 Python 库——Koalas,它使您能够使用非常熟悉且易于使用的类似 pandas 的语法轻松编写 PySpark 代码。
第十一章**, 使用 PySpark 进行数据可视化,介绍了数据可视化,这是从数据中传递意义并获得洞察力的重要方面。本章将介绍如何使用最流行的 Python 可视化库与 PySpark 结合。
第十二章**, Spark SQL 入门,介绍了 SQL,这是一种用于临时查询和数据分析的表达语言。本章将介绍 Spark SQL 用于数据分析,并展示如何交替使用 PySpark 进行数据分析。
第十三章**, 将外部工具与 Spark SQL 集成,解释了当我们在高效的数据湖中拥有干净、整理过且可靠的数据时,未能将这些数据普及到组织中的普通分析师将是一个错失的机会。最流行的方式是通过各种现有的商业智能(BI)工具。本章将讨论 BI 工具集成的需求。
第十四章**, 数据湖屋,解释了传统的描述性分析工具,如商业智能(BI)工具,是围绕数据仓库设计的,并期望数据以特定方式呈现,而现代的高级分析和数据科学工具则旨在处理可以轻松访问的大量数据,这些数据通常存储在数据湖中。将冗余数据存储在单独的存储位置以应对这些独立的用例既不实际也不具成本效益。本章将介绍一种新的范式——数据湖屋,它试图克服数据仓库和数据湖的局限性,通过结合两者的最佳元素来弥合差距。
要最大限度地利用本书
预计读者具备数据工程、数据科学和 SQL 分析的基础至中级知识。能够使用任何编程语言,特别是 Python,并且具备使用 pandas 和 SQL 等框架进行数据分析的基本知识,将有助于你从本书中获得最大收益。

本书使用 Databricks Community Edition 来运行所有代码:community.cloud.databricks.com。注册说明可在databricks.com/try-databricks找到。
本书中使用的整个代码库可以从github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/blob/main/all_chapters/ess_pyspark.dbc下载。
本章使用的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
如果你正在使用本书的数字版本,我们建议你自己输入代码,或者从本书的 GitHub 仓库获取代码(链接将在下一个章节提供)。这样做有助于避免由于复制和粘贴代码而导致的潜在错误。
下载示例代码文件
你可以从 GitHub 上下载本书的示例代码文件:github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics。如果代码有更新,GitHub 仓库会进行更新。
我们的书籍和视频丰富目录中还提供了其他代码包,可以在github.com/PacktPublishing/查看!
下载彩色图片
我们还提供了一个包含本书中使用的截图和图表的彩色图片的 PDF 文件。你可以在这里下载:static.packt-cdn.com/downloads/9781800568877_ColorImages.pdf
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户名。例如:“DataStreamReader 对象的 readStream() 方法用于创建流式 DataFrame。”
代码块按如下方式设置:
lines = sc.textFile("/databricks-datasets/README.md")
words = lines.flatMap(lambda s: s.split(" "))
word_tuples = words.map(lambda s: (s, 1))
word_count = word_tuples.reduceByKey(lambda x, y: x + y)
word_count.take(10)
word_count.saveAsTextFile("/tmp/wordcount.txt")
任何命令行输入或输出都按如下方式书写:
%fs ls /FileStore/shared_uploads/delta/online_retail
粗体:表示一个新术语、一个重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的词汇通常是粗体。例如:“可以有多个Map阶段,接着是多个Reduce阶段。”
提示或重要注意事项
以这种方式显示。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com 并在邮件主题中注明书名。
勘误:尽管我们已尽力确保内容的准确性,但错误难免发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/support/errata 并填写表格。
copyright@packt.com 并附上相关材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且有兴趣撰写或为书籍贡献内容,请访问 authors.packtpub.com。
分享您的想法
阅读完 Essential PySpark for Scalable Data Analytics 后,我们非常期待听到您的想法!请访问 packt.link/r/1-800-56887-8 为本书留下评价并分享您的反馈。
您的评价对我们以及技术社区都至关重要,它将帮助我们确保提供优质的内容。
第一部分:数据工程
本节介绍了分布式计算范式,并展示了 Spark 如何成为大数据处理的事实标准。
完成本节后,你将能够从各种数据源摄取数据,进行清洗、集成,并以可扩展和分布式的方式将其写入持久化存储,例如数据湖。你还将能够构建实时分析管道,并在数据湖中执行变更数据捕获(CDC)。你将理解 ETL 和 ELT 数据处理方式的关键区别,以及 ELT 如何为云端数据湖世界的发展演变。本节还介绍了 Delta Lake,以使基于云的数据湖更加可靠和高效。你将理解 Lambda 架构的细微差别,作为同时执行批处理和实时分析的一种手段,以及 Apache Spark 结合 Delta Lake 如何大大简化 Lambda 架构。
本节包括以下章节:
-
第一章**,分布式计算基础
-
第二章**,数据摄取
-
第三章**,数据清洗与集成
-
第四章**,实时数据分析
第一章:分布式计算入门
本章介绍了分布式计算范式,并展示了分布式计算如何帮助你轻松处理大量数据。你将学习如何使用MapReduce范式实现数据并行处理,并最终了解如何通过使用内存中的统一数据处理引擎(如 Apache Spark)来提高数据并行处理的效率。
接下来,你将深入了解 Apache Spark 的架构和组件,并结合代码示例进行讲解。最后,你将概览 Apache Spark 最新的 3.0 版本中新增的功能。
在本章中,你将掌握的关键技能包括理解分布式计算范式的基础知识,以及分布式计算范式的几种不同实现方法,如 MapReduce 和 Apache Spark。你将学习 Apache Spark 的基本原理及其架构和核心组件,例如 Driver、Executor 和 Cluster Manager,并了解它们如何作为一个整体协同工作以执行分布式计算任务。你将学习 Spark 的弹性分布式数据集(RDD)API,及其高阶函数和 lambda 表达式。你还将了解 Spark SQL 引擎及其 DataFrame 和 SQL API。此外,你将实现可运行的代码示例。你还将学习 Apache Spark 数据处理程序的各个组件,包括转换和动作,并学习惰性求值的概念。
在本章中,我们将涵盖以下主要内容:
-
介绍 分布式计算
-
使用 Apache Spark 进行分布式计算
-
使用 Spark SQL 和 DataFrames 进行大数据处理
技术要求
在本章中,我们将使用 Databricks Community Edition 来运行代码。你可以在community.cloud.databricks.com找到该平台。
注册说明可在databricks.com/try-databricks找到。
本章使用的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter01下载。
本章使用的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
原始数据集可以从以下来源获取:
分布式计算
在本节中,你将了解分布式计算,它的需求,以及如何利用它以快速且高效的方式处理大量数据。
分布式计算简介
分布式计算是一类计算技术,我们通过使用一组计算机作为一个整体来解决计算问题,而不是仅仅依赖单台机器。
在数据分析中,当数据量变得太大,无法在单台机器中处理时,我们可以将数据拆分成小块并在单台机器上迭代处理,或者可以在多台机器上并行处理数据块。虽然前者可以完成任务,但可能需要更长时间来迭代处理整个数据集;后者通过同时使用多台机器,能够在更短的时间内完成任务。
有多种分布式计算技术;然而,在数据分析中,一种流行的技术是 数据并行处理。
数据并行处理
数据并行处理包含两个主要部分:
-
需要处理的实际数据。
-
需要应用于数据的代码片段或业务逻辑,以便进行处理。
我们可以通过将大量数据拆分成小块,并在多台机器上并行处理它们,从而处理大规模数据。这可以通过两种方式实现:
-
首先,将数据带到运行代码的机器上。
-
第二步,将我们的代码带到数据实际存储的位置。
第一种技术的一个缺点是,随着数据量的增大,数据移动所需的时间也会成比例增加。因此,我们最终会花费更多的时间将数据从一个系统移动到另一个系统,从而抵消了并行处理系统带来的任何效率提升。同时,我们还会在数据复制过程中产生多个数据副本。
第二种技术要高效得多,因为我们不再移动大量数据,而是可以将少量代码移到数据实际存放的位置。这种将代码移到数据所在位置的技术称为数据并行处理。数据并行处理技术非常快速和高效,因为它节省了之前在不同系统之间移动和复制数据所需的时间。这样的数据并行处理技术之一被称为 MapReduce 模式。
使用 MapReduce 模式进行数据并行处理
MapReduce 模式将数据并行处理问题分解为三个主要阶段:
-
Map 阶段
-
Shuffle 阶段
-
Reduce 阶段
(key, value)对,应用一些处理,将它们转换为另一组(key, value)对。
从 Map 阶段获取的(key, value)对,将其洗牌/排序,使得具有相同key的对最终聚集在一起。
从 Shuffle 阶段获取的(key, value)对,经过归约或聚合,产生最终结果。
可以有多个Map阶段,后跟多个Reduce阶段。然而,Reduce阶段仅在所有Map阶段完成后开始。
让我们看一个例子,假设我们想要计算文本文件中所有不同单词的计数,并应用MapReduce范式。
以下图表展示了 MapReduce 范式的一般工作原理:

图 1.1 – 使用 MapReduce 计算单词计数
之前的例子按照以下方式工作:
-
在图 1.1中,我们有一个包含三台节点的集群,标记为M1、M2和M3。每台机器上都有几个文本文件,包含若干句子的纯文本。在这里,我们的目标是使用 MapReduce 来计算文本文件中所有单词的数量。
-
我们将所有文本文件加载到集群中;每台机器加载本地的文档。
-
(word, count)对。 -
从Map 阶段获取的
(word, count)对,进行洗牌/排序,使得具有相同关键词的单词对聚集在一起。 -
Reduce 阶段将所有关键字分组,并对其计数进行汇总,得到每个单独单词的最终计数。
MapReduce 范式由Hadoop框架推广,曾在大数据工作负载处理领域非常流行。然而,MapReduce 范式提供的是非常低级别的 API 来转换数据,且要求用户具备如 Java 等编程语言的熟练知识。使用 Map 和 Reduce 来表达数据分析问题既不直观也不灵活。
MapReduce 被设计成可以在普通硬件上运行,由于普通硬件容易发生故障,因此对于硬件故障的容错能力至关重要。MapReduce 通过将每个阶段的结果保存到磁盘上来实现容错。每个阶段结束后必须往返磁盘,这使得 MapReduce 在处理数据时相对较慢,因为物理磁盘的 I/O 性能普遍较低。为了克服这个限制,下一代 MapReduce 范式应运而生,它利用比磁盘更快的系统内存来处理数据,并提供了更灵活的 API 来表达数据转换。这个新框架叫做 Apache Spark,接下来的章节和本书其余部分你将学习到它。
重要提示
在分布式计算中,你会经常遇到集群这个术语。集群是由一组计算机组成的,它们共同作为一个单一的单元来解决计算问题。集群的主要计算机通常被称为主节点,负责集群的协调和管理,而实际执行任务的次要计算机则被称为工作节点。集群是任何分布式计算系统的关键组成部分,在本书中,你将会遇到这些术语。
使用 Apache Spark 进行分布式计算
在过去的十年中,Apache Spark 已发展成为大数据处理的事实标准。事实上,它是任何从事数据分析工作的人手中不可或缺的工具。
在这里,我们将从 Apache Spark 的基础知识开始,包括其架构和组件。接着,我们将开始使用 PySpark 编程 API 来实际实现之前提到的单词计数问题。最后,我们将看看最新的 Apache Spark 3.0 版本有什么新功能。
Apache Spark 介绍
Apache Spark 是一个内存中的统一数据分析引擎,相较于其他分布式数据处理框架,它的速度相对较快。
它是一个统一的数据分析框架,因为它可以使用单一引擎处理不同类型的大数据工作负载。这些工作负载包括以下内容:
-
批量数据处理
-
实时数据处理
-
机器学习和数据科学
通常,数据分析涉及到之前提到的所有或部分工作负载来解决单一的业务问题。在 Apache Spark 出现之前,没有一个单一的框架能够同时容纳所有三种工作负载。通过 Apache Spark,参与数据分析的各个团队可以使用同一个框架来解决单一的业务问题,从而提高团队之间的沟通与协作,并显著缩短学习曲线。
我们将在本书中深入探索每一个前述的工作负载,从第二章**,数据摄取,到第八章**,无监督机器学习。
此外,Apache Spark 在两个方面都非常快:
-
它在数据处理速度方面非常快。
-
它在开发速度方面很快。
Apache Spark 具有快速的作业/查询执行速度,因为它将所有数据处理都在内存中进行,并且还内置了一些优化技术,如惰性求值、谓词下推和分区修剪,仅举几例。我们将在接下来的章节中详细介绍 Spark 的优化技术。
其次,Apache Spark 为开发者提供了非常高级的 API,用于执行基本的数据处理操作,例如 过滤、分组、排序、连接 和 聚合。通过使用这些高级编程结构,开发者能够非常轻松地表达数据处理逻辑,使开发速度大大提高。
Apache Spark 的核心抽象,正是使其在数据分析中既快速又富有表现力的,是 RDD。我们将在下一节中介绍这个概念。
使用 RDD 进行数据并行处理
RDD 是 Apache Spark 框架的核心抽象。可以将 RDD 看作是任何一种不可变数据结构,它通常存在于编程语言中,但与一般情况下只驻留在单台机器的不同,RDD 是分布式的,驻留在多台机器的内存中。一个 RDD 由多个分区组成,分区是 RDD 的逻辑划分,每个机器上可能会有一些分区。
下图帮助解释 RDD 及其分区的概念:

图 1.2 – 一个 RDD
在之前的示意图中,我们有一个由三台机器或节点组成的集群。集群中有三个 RDD,每个 RDD 被分成多个分区。每个节点包含一个或多个分区,并且每个 RDD 通过分区在集群的多个节点之间分布。
RDD 抽象伴随着一组可以操作 RDD 的高阶函数,用于操作存储在分区中的数据。这些函数被称为 高阶函数,你将在接下来的章节中了解它们。
高阶函数
高阶函数操作 RDD,并帮助我们编写业务逻辑来转换存储在分区中的数据。高阶函数接受其他函数作为参数,这些内部函数帮助我们定义实际的业务逻辑,转换数据并并行应用于每个 RDD 的分区。传递给高阶函数的内部函数称为 lambda 函数 或 lambda 表达式。
Apache Spark 提供了多个高阶函数,例如 map、flatMap、reduce、fold、filter、reduceByKey、join 和 union 等。这些函数是高级函数,帮助我们非常轻松地表达数据操作逻辑。
例如,考虑我们之前展示的字数统计示例。假设你想将一个文本文件作为 RDD 读取,并根据分隔符(例如空格)拆分每个单词。用 RDD 和高阶函数表示的代码可能如下所示:
lines = sc.textFile("/databricks-datasets/README.md")
words = lines.flatMap(lambda s: s.split(" "))
word_tuples = words.map(lambda s: (s, 1))
在之前的代码片段中,发生了以下情况:
-
我们使用内置的
sc.textFile()方法加载文本文件,该方法会将指定位置的所有文本文件加载到集群内存中,将它们按行拆分,并返回一个包含行或字符串的 RDD。 -
然后,我们对新的行 RDD 应用
flatMap()高阶函数,并提供一个函数,指示它将每一行按空格分开。我们传递给flatMap()的 Lambda 函数只是一个匿名函数,它接受一个参数,即单个StringType行,并返回一个单词列表。通过flatMap()和lambda()函数,我们能够将一个行 RDD 转换为一个单词 RDD。 -
最后,我们使用
map()函数为每个单词分配1的计数。这相对简单,且比使用 Java 编程语言开发 MapReduce 应用程序直观得多。
总结你所学的内容,Apache Spark 框架的主要构建块是 RDD。一个 RDD 由分布在集群各个节点上的 分区 组成。我们使用一种叫做高阶函数的特殊函数来操作 RDD,并根据我们的业务逻辑转化 RDD。这些业务逻辑通过高阶函数以 Lambda 或匿名函数的形式传递到 Worker 节点。
在深入探讨高阶函数和 Lambda 函数的内部工作原理之前,我们需要了解 Apache Spark 框架的架构以及一个典型 Spark 集群的组件。我们将在接下来的章节中进行介绍。
注意
RDD 的 Resilient 特性来源于每个 RDD 知道它的血统。任何时候,一个 RDD 都拥有它上面执行的所有操作的信息,追溯到数据源本身。因此,如果由于某些故障丢失了 Executors,且其某些分区丢失,它可以轻松地通过利用血统信息从源数据重新创建这些分区,从而使其对故障具有 Resilient(韧性)。
Apache Spark 集群架构
一个典型的 Apache Spark 集群由三个主要组件组成,即 Driver、若干 Executors 和 Cluster Manager:

图 1.3 – Apache Spark 集群组件
让我们仔细看看这些组件。
Driver – Spark 应用程序的核心
Spark Driver 是一个 Java 虚拟机进程,是 Spark 应用程序的核心部分。它负责用户应用程序代码的声明,同时创建 RDD、DataFrame 和数据集。它还负责与 Executors 协调并在 Executors 上运行代码,创建并调度任务。它甚至负责在失败后重新启动 Executors,并最终将请求的数据返回给客户端或用户。可以把 Spark Driver 想象成任何 Spark 应用程序的 main() 程序。
重要说明
Driver 是 Spark 集群的单点故障,如果 Driver 失败,整个 Spark 应用程序也会失败;因此,不同的集群管理器实现了不同的策略以确保 Driver 高可用。
执行器 – 实际的工作节点
Spark 执行器也是 Java 虚拟机进程,它们负责在 RDD 上运行实际的转换数据操作。它们可以在本地缓存数据分区,并将处理后的数据返回给 Driver,或写入持久化存储。每个执行器会并行运行一组 RDD 分区的操作。
集群管理器 – 协调和管理集群资源
集群管理器是一个在集群上集中运行的进程,负责为 Driver 提供所请求的资源。它还监控执行器的任务进度和状态。Apache Spark 自带集群管理器,称为 Standalone 集群管理器,但它也支持其他流行的集群管理器,如 YARN 或 Mesos。在本书中,我们将使用 Spark 自带的 Standalone 集群管理器。
开始使用 Spark
到目前为止,我们已经学习了 Apache Spark 的核心数据结构 RDD、用于操作 RDD 的函数(即高阶函数)以及 Apache Spark 集群的各个组件。你还见识了一些如何使用高阶函数的代码片段。
在这一部分,你将把所学知识付诸实践,编写你的第一个 Apache Spark 程序,你将使用 Spark 的 Python API —— PySpark 来创建一个词频统计应用程序。然而,在开始之前,我们需要准备以下几样东西:
-
一个 Apache Spark 集群
-
数据集
-
词频统计应用程序的实际代码
我们将使用免费的 Databricks 社区版 来创建我们的 Spark 集群。所使用的代码可以通过本章开头提到的 GitHub 链接找到。所需资源的链接可以在本章开头的 技术要求 部分找到。
注意
尽管本书中使用的是 Databricks Spark 集群,但提供的代码可以在任何运行 Spark 3.0 或更高版本的 Spark 集群上执行,只要数据位于 Spark 集群可以访问的位置。
现在你已经理解了 Spark 的核心概念,如 RDD、高阶函数、Lambda 表达式以及 Spark 架构,让我们通过以下代码实现你的第一个 Spark 应用程序:
lines = sc.textFile("/databricks-datasets/README.md")
words = lines.flatMap(lambda s: s.split(" "))
word_tuples = words.map(lambda s: (s, 1))
word_count = word_tuples.reduceByKey(lambda x, y: x + y)
word_count.take(10)
word_count.saveAsTextFile("/tmp/wordcount.txt")
在上一个代码片段中,发生了以下操作:
-
我们使用内建的
sc.textFile()方法加载文本文件,该方法会读取指定位置的所有文本文件,将其拆分为单独的行,并返回一个包含行或字符串的 RDD。 -
接着,我们对 RDD 中的行应用
flatMap()高阶函数,并传入一个函数,该函数指示它根据空格将每一行拆分开来。我们传给flatMap()的 lambda 函数只是一个匿名函数,它接受一个参数——一行文本,并将每个单词作为列表返回。通过flatMap()和lambda()函数,我们能够将行的 RDD 转换成单词的 RDD。 -
然后,我们使用
map()函数为每个单独的单词分配一个1的计数。 -
最后,我们使用
reduceByKey()高阶函数对多次出现的相似单词进行计数求和。 -
一旦计算出单词的计数,我们使用
take()函数展示最终单词计数的一个示例。 -
尽管展示一个示例结果集通常有助于确定我们代码的正确性,但在大数据环境下,将所有结果展示到控制台上并不现实。因此,我们使用
saveAsTextFile()函数将最终结果持久化到持久存储中。重要提示
不推荐使用
take()或collect()等命令将整个结果集展示到控制台。这在大数据环境下甚至可能是危险的,因为它可能试图将过多的数据带回驱动程序,从而导致驱动程序因OutOfMemoryError失败,进而使整个应用程序失败。因此,建议你在结果集非常小的情况下使用
take(),而仅在确信返回的数据量确实非常小时,才使用collect()。
让我们深入探讨下面这一行代码,了解 lambda 的内部工作原理,以及它们如何通过高阶函数实现数据并行处理:
words = lines.flatMap(lambda s: s.split(" "))
在之前的代码片段中,flatMmap() 高阶函数将 lambda 中的代码打包,并通过一种叫做 序列化 的过程将其发送到网络上的 Worker 节点。这个 序列化的 lambda 随后会被发送到每个执行器,每个执行器则将此 lambda 并行应用到各个 RDD 分区上。
重要提示
由于高阶函数需要能够序列化 lambda,以便将你的代码发送到执行器。因此,lambda 函数需要是 可序列化 的,如果不满足这一要求,你可能会遇到 任务不可序列化 的错误。
总结来说,高阶函数本质上是将你的数据转换代码以序列化 lambda 的形式传递到 RDD 分区中的数据上。因此,我们并不是将数据移动到代码所在的位置,而是将代码移动到数据所在的位置,这正是数据并行处理的定义,正如我们在本章前面所学的那样。
因此,Apache Spark 及其 RDD 和高阶函数实现了内存中版本的数据并行处理范式。这使得 Apache Spark 在分布式计算环境中,进行大数据处理时既快速又高效。
Apache Spark 的 RDD 抽象相较于 MapReduce 确实提供了一个更高级的编程 API,但它仍然需要一定程度的函数式编程理解,才能表达最常见的数据转换类型。为了克服这个挑战,Spark 扩展了已有的 SQL 引擎,并在 RDD 上增加了一个叫做 DataFrame 的抽象。这使得数据处理对于数据科学家和数据分析师来说更加容易和熟悉。接下来的部分将探索 Spark SQL 引擎的 DataFrame 和 SQL API。
使用 Spark SQL 和 DataFrame 处理大数据
Spark SQL 引擎支持两种类型的 API,分别是 DataFrame 和 Spark SQL。作为比 RDD 更高层次的抽象,它们更加直观,甚至更具表现力。它们带有更多的数据转换函数和工具,你作为数据工程师、数据分析师或数据科学家,可能已经熟悉这些内容。
Spark SQL 和 DataFrame API 提供了一个低门槛进入大数据处理的途径。它们让你可以使用现有的数据分析知识和技能,轻松开始分布式计算。它们帮助你开始进行大规模数据处理,而无需处理分布式计算框架通常带来的复杂性。
本节内容将教你如何使用 DataFrame 和 Spark SQL API 来开始你的可扩展数据处理之旅。值得注意的是,这里学到的概念在本书中会非常有用,并且是必须掌握的。
使用 Spark DataFrame 转换数据
从 Apache Spark 1.3 开始,Spark SQL 引擎被作为一层添加到 RDD API 之上,并扩展到 Spark 的每个组件,以提供一个更易于使用且熟悉的 API 给开发者。多年来,Spark SQL 引擎及其 DataFrame 和 SQL API 变得更加稳健,并成为了使用 Spark 的事实标准和推荐标准。在本书中,你将专门使用 DataFrame 操作或 Spark SQL 语句来进行所有数据处理需求,而很少使用 RDD API。
可以把 Spark DataFrame 想象成一个 Pandas DataFrame 或一个具有行和命名列的关系型数据库表。唯一的区别是,Spark DataFrame 存储在多台机器的内存中,而不是单台机器的内存中。下图展示了一个具有三列的 Spark DataFrame,分布在三台工作机器上:

图 1.4 – 分布式 DataFrame
Spark DataFrame 也是一种不可变的数据结构,类似于 RDD,包含行和命名列,其中每一列可以是任何类型。此外,DataFrame 提供了可以操作数据的功能,我们通常将这些操作集合称为 领域特定语言 (DSL)。Spark DataFrame 操作可以分为两大类,即转换(transformations)和动作(actions),我们将在接下来的章节中进行探讨。
使用 DataFrame 或 Spark SQL 相较于 RDD API 的一个优势是,Spark SQL 引擎内置了一个查询优化器,名为 Catalyst。这个 Catalyst 优化器分析用户代码,以及任何可用的数据统计信息,以生成查询的最佳执行计划。这个查询计划随后会被转换为 Java 字节码,原生运行在 Executor 的 Java JVM 内部。无论使用哪种编程语言,这个过程都会发生,因此在大多数情况下,使用 Spark SQL 引擎处理的任何代码都能保持一致的性能表现,不论是使用 Scala、Java、Python、R 还是 SQL 编写的代码。
转换
read、select、where、filter、join 和 groupBy。
动作
write、count 和 show。
懒评估
Spark 转换是懒评估的,这意味着转换不会在声明时立即评估,数据也不会在内存中展现,直到调用一个动作。这样做有几个优点,因为它给 Spark 优化器提供了评估所有转换的机会,直到调用一个动作,并生成最优化的执行计划,以便从代码中获得最佳性能和效率。
懒评估与 Spark 的 Catalyst 优化器相结合的优势在于,你可以专注于表达数据转换逻辑,而不必过多担心按特定顺序安排转换,以便从代码中获得最佳性能和效率。这可以帮助你在任务中更高效,不至于被新框架的复杂性弄得困惑。
重要提示
与 Pandas DataFrame 相比,Spark DataFrame 在声明时不会立即加载到内存中。它们只有在调用动作时才会被加载到内存中。同样,DataFrame 操作不一定按你指定的顺序执行,因为 Spark 的 Catalyst 优化器会为你生成最佳的执行计划,有时甚至会将多个操作合并成一个单元。
让我们以之前使用 RDD API 实现的词频统计为例,尝试使用 DataFrame DSL 来实现。
from pyspark.sql.functions import split, explode
linesDf = spark.read.text("/databricks-datasets/README.md")
wordListDf = linesDf.select(split("value", " ").alias("words"))
wordsDf = wordListDf.select(explode("words").alias("word"))
wordCountDf = wordsDf.groupBy("word").count()
wordCountDf.show()
wordCountDf.write.csv("/tmp/wordcounts.csv")
在前面的代码片段中,发生了以下情况:
-
首先,我们从 PySpark SQL 函数库中导入几个函数,分别是 split 和 explode。
-
然后,我们使用
SparkSession的read.text()方法读取文本,这会创建一个由StringType类型的行组成的 DataFrame。 -
接着,我们使用
split()函数将每一行拆分为独立的单词;结果是一个包含单一列的 DataFrame,列名为value,实际上是一个单词列表。 -
然后,我们使用
explode()函数将每一行中的单词列表分解为每个单词独立成行;结果是一个带有word列的 DataFrame。 -
现在我们终于准备好计算单词数量了,因此我们通过
word列对单词进行分组,并统计每个单词的出现次数。最终结果是一个包含两列的 DataFrame,即实际的word和它的count。 -
我们可以使用
show()函数查看结果样本,最后,使用write()函数将结果保存到持久化存储中。
你能猜出哪些操作是动作操作吗?如果你猜测是show()或write(),那你是对的。其他所有函数,包括select()和groupBy(),都是转换操作,不会触发 Spark 任务的执行。
注意
尽管read()函数是一种转换操作,但有时你会注意到它实际上会执行一个 Spark 任务。之所以这样,是因为对于某些结构化和半结构化的数据格式,Spark 会尝试从底层文件中推断模式信息,并处理实际文件的小部分以完成此操作。
使用 Spark 上的 SQL
SQL 是一种用于临时数据探索和商业智能查询的表达性语言。因为它是一种非常高级的声明式编程语言,用户只需关注输入、输出以及需要对数据执行的操作,而无需过多担心实际实现逻辑的编程复杂性。Apache Spark 的 SQL 引擎也提供了 SQL 语言 API,并与 DataFrame 和 Dataset APIs 一起使用。
使用 Spark 3.0,Spark SQL 现在符合 ANSI 标准,因此,如果你是熟悉其他基于 SQL 的平台的数据分析师,你应该可以毫不费力地开始使用 Spark SQL。
由于 DataFrame 和 Spark SQL 使用相同的底层 Spark SQL 引擎,它们是完全可互换的,通常情况下,用户会将 DataFrame DSL 与 Spark SQL 语句混合使用,尤其是在代码的某些部分,用 SQL 表达更加简洁。
现在,让我们使用 Spark SQL 重写我们的单词计数程序。首先,我们创建一个表,指定我们的文本文件为 CSV 文件,并以空格作为分隔符,这是一个巧妙的技巧,可以读取文本文件的每一行,并且同时将每个文件拆分成单独的单词:
CREATE TABLE word_counts (word STRING)
USING csv
OPTIONS("delimiter"=" ")
LOCATION "/databricks-datasets/README.md"
现在我们已经有了一个包含单列单词的表格,我们只需要对word列进行GROUP BY操作,并执行COUNT()操作来获得单词计数:
SELECT word, COUNT(word) AS count
FROM word_counts
GROUP BY word
在这里,你可以观察到,解决相同的业务问题变得越来越容易,从使用 MapReduce 到 RRDs,再到 DataFrames 和 Spark SQL。每一个新版本发布时,Apache Spark 都在增加更多的高级编程抽象、数据转换和实用函数,以及其他优化。目标是让数据工程师、数据科学家和数据分析师能够将时间和精力集中在解决实际的业务问题上,而无需担心复杂的编程抽象或系统架构。
Apache Spark 最新版本 3 的主要发布包含了许多增强功能,使数据分析专业人员的工作变得更加轻松。我们将在接下来的章节中讨论这些增强功能中最突出的部分。
Apache Spark 3.0 有什么新特性?
在 Apache Spark 3.0 中有许多新的显著特性;不过这里只提到了其中一些,你会发现它们在数据分析初期阶段非常有用:
-
速度:Apache Spark 3.0 比其前版本快了几个数量级。第三方基准测试表明,Spark 3.0 在某些类型的工作负载下速度是前版本的 2 到 17 倍。
-
自适应查询执行:Spark SQL 引擎根据用户代码和之前收集的源数据统计信息生成一些逻辑和物理查询执行计划。然后,它会尝试选择最优的执行计划。然而,有时由于统计信息过时或不存在,Spark 可能无法生成最佳的执行计划,导致性能不佳。通过自适应查询执行,Spark 能够在运行时动态调整执行计划,从而提供最佳的查询性能。
-
动态分区修剪:商业智能系统和数据仓库采用了一种名为 维度建模 的数据建模技术,其中数据存储在一个中央事实表中,周围是几个维度表。利用这些维度模型的商业智能类型查询涉及多个维度表和事实表之间的连接,并带有各种维度表的筛选条件。通过动态分区修剪,Spark 能够根据应用于这些维度的筛选条件,过滤掉任何事实表分区,从而减少内存中读取的数据量,进而提高查询性能。
-
Kubernetes 支持:之前,我们了解到 Spark 提供了自己的独立集群管理器,并且还可以与其他流行的资源管理器如 YARN 和 Mesos 配合使用。现在,Spark 3.0 原生支持 Kubernetes,这是一个流行的开源框架,用于运行和管理并行容器服务。
总结
在本章中,你学习了分布式计算的概念。我们发现,分布式计算变得非常重要,因为生成的数据量正在快速增长,使用单一专用系统处理所有数据既不切实际也不现实。
然后,你学习了数据并行处理的概念,并通过 MapReduce 范式回顾了它的实际实现示例。
接着,你了解了一个名为 Apache Spark 的内存中统一分析引擎,并学习了它在数据处理方面的速度和高效性。此外,你还了解到,它非常直观且容易上手,适合开发数据处理应用程序。你还了解了 Apache Spark 的架构及其组件,并理解了它们如何作为一个框架结合在一起。
接下来,你学习了 RDD(弹性分布式数据集)的概念,它是 Apache Spark 的核心抽象,了解了它如何在一群机器上以分布式方式存储数据,以及如何通过高阶函数和 lambda 函数结合使用 RDD 实现数据并行处理。
你还学习了 Apache Spark 中的 Spark SQL 引擎组件,了解了它提供了比 RDD 更高层次的抽象,并且它有一些你可能已经熟悉的内置函数。你学会了利用 DataFrame 的 DSL 以更简单且更熟悉的方式实现数据处理的业务逻辑。你还了解了 Spark 的 SQL API,它符合 ANSI SQL 标准,并且允许你高效地在大量数据上执行 SQL 分析。
你还了解了 Apache Spark 3.0 中一些显著的改进,例如自适应查询执行和动态分区修剪,这些都大大提高了 Spark 3.0 的性能,使其比前几版本更快。
现在你已经学习了使用 Apache Spark 进行大数据处理的基础知识,接下来你可以开始利用 Spark 踏上数据分析之旅。一个典型的数据分析旅程从从各种源系统获取原始数据开始,将其导入到历史存储组件中,如数据仓库或数据湖,然后通过清洗、集成和转换来处理原始数据,以获得一个统一的真实数据源。最后,你可以通过干净且集成的数据,利用描述性和预测性分析获得可操作的业务洞察。我们将在本书的后续章节中讨论这些方面,首先从下一章的数据清洗和数据导入过程开始。
第二章:数据摄取
数据摄取是将数据从不同的操作系统迁移到中央位置(如数据仓库或数据湖)以便进行处理,并使其适用于数据分析的过程。这是数据分析过程的第一步,对于创建可以集中访问的持久存储至关重要,在这里,数据工程师、数据科学家和数据分析师可以访问、处理和分析数据,以生成业务分析。
你将了解 Apache Spark 作为批量和实时处理的数据摄取引擎的功能。将介绍 Apache Spark 支持的各种数据源以及如何使用 Spark 的 DataFrame 接口访问它们。
此外,你还将学习如何使用 Apache Spark 的内置函数,从外部数据源访问数据,例如关系数据库管理系统(RDBMS)以及消息队列,如 Apache Kafka,并将其摄取到数据湖中。还将探讨不同的数据存储格式,如结构化、非结构化和半结构化文件格式,以及它们之间的主要差异。Spark 的实时流处理引擎——结构化流(Structured Streaming)也将被介绍。你将学习如何使用批处理和实时流处理创建端到端的数据摄取管道。最后,我们将探索一种将批处理和流处理统一的技术——Lambda 架构,并介绍如何使用 Apache Spark 实现它。
在本章中,你将学习执行批量和实时数据摄取所需的基本技能,使用 Apache Spark。此外,你将掌握构建端到端可扩展且高效的大数据摄取管道所需的知识和工具。
本章将涵盖以下主要主题:
-
企业决策支持系统简介
-
从数据源摄取数据
-
向数据接收端摄取数据
-
使用文件格式进行数据湖中的数据存储
-
构建批量和实时数据摄取管道
-
使用 Lambda 架构统一批量和实时数据摄取
技术要求
在本章中,我们将使用 Databricks 社区版来运行我们的代码。你可以在community.cloud.databricks.com找到它。
注册说明请参见databricks.com/try-databricks。
本章使用的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter02下载。
本章使用的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
企业决策支持系统简介
企业决策支持系统(Enterprise DSS)是一个端到端的数据处理系统,它将企业组织生成的运营和交易数据转换为可操作的洞察。每个企业决策支持系统都有一些标准组件,例如数据源、数据接收器和数据处理框架。企业决策支持系统以原始交易数据为输入,并将其转化为可操作的洞察,如运营报告、企业绩效仪表板和预测分析。
以下图示展示了在大数据环境中典型的企业决策支持系统的组成部分:

图 2.1 – 企业决策支持系统架构
大数据分析系统也是一种企业决策支持系统,但其处理的规模要大得多,涉及的数据种类更多,且数据到达速度更快。作为企业决策支持系统的一种类型,大数据分析系统的组件与传统企业决策支持系统相似。构建企业决策支持系统的第一步是从数据源摄取数据并将其传送到数据接收器。您将在本章中学习这一过程。让我们从数据源开始,详细讨论大数据分析系统的各个组件。
从数据源摄取数据
在本节中,我们将了解大数据分析系统使用的各种数据源。典型的数据源包括事务系统(如 RDBMS)、基于文件的数据源(如数据湖)以及消息队列(如Apache Kafka)。此外,您将学习 Apache Spark 内置的连接器,以便从这些数据源摄取数据,并编写代码以查看这些连接器的实际操作。
从关系型数据源摄取数据
事务系统,或称操作系统,是一种数据处理系统,帮助组织执行日常的业务功能。这些事务系统处理单个业务交易,例如零售自助服务亭的交易、在线零售门户下单、航空票务预订或银行交易。这些交易的历史汇总构成了数据分析的基础,分析系统摄取、存储并处理这些交易数据。因此,这类事务系统构成了分析系统的数据源,并且是数据分析的起点。
交易系统有多种形式;然而,最常见的是关系型数据库管理系统(RDBMS)。在接下来的章节中,我们将学习如何从 RDBMS 中摄取数据。
关系型数据源是一组关系型数据库和关系型表,这些表由行和命名列组成。用于与 RDBMS 进行通信和查询的主要编程抽象称为 结构化查询语言 (SQL)。外部系统可以通过 JDBC 和 ODBC 等通信协议与 RDBMS 进行通信。Apache Spark 配备了一个内置的 JDBC 数据源,可以用来与存储在 RDBMS 表中的数据进行通信和查询。
让我们来看一下使用 PySpark 从 RDBMS 表中摄取数据所需的代码,如以下代码片段所示:
dataframe_mysql = spark.read.format("jdbc").options(
url="jdbc:mysql://localhost:3306/pysparkdb",
driver = "org.mariadb.jdbc.Driver",
dbtable = "authors",
user="#####",
password="@@@@@").load()
dataframe_mysql.show()
在前面的代码片段中,我们使用 spark.read() 方法通过指定格式为 jdbc 来加载来自 JDBC 数据源的数据。在这里,我们连接到一个流行的开源 RDBMS,名为 url,该 URL 指定了 MySQL 服务器的 jdbc url,并包含其 hostname、port number 和 database name。driver 选项指定了 Spark 用来连接并与 RDBMS 通信的 JDBC 驱动程序。dtable、user 和 password 选项指定了要查询的表名以及进行身份验证所需的凭证。最后,show() 函数从 RDBMS 表中读取示例数据并显示在控制台上。
重要提示
前面的代码片段使用了虚拟的数据库凭证,并以纯文本形式显示。这会带来巨大的数据安全风险,并且不推荐这种做法。处理敏感信息时,应遵循适当的最佳实践,比如使用配置文件或其他大数据软件供应商提供的机制,如隐藏或模糊化敏感信息。
要运行此代码,您可以使用自己的 MySQL 服务器并将其配置到您的 Spark 集群中,或者可以使用本章提供的示例代码来设置一个简单的 MySQL 服务器。所需的代码可以在 github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/blob/main/Chapter02/utils/mysql-setup.ipynb 中找到。
提示
Apache Spark 提供了一个 JDBC 数据源,能够连接几乎所有支持 JDBC 连接并具有 JDBC 驱动程序的关系型数据库管理系统(RDBMS)。然而,它并不自带任何驱动程序;需要从相应的 RDBMS 提供商处获取,并且需要将驱动程序配置到 Spark 集群中,以便 Spark 应用程序可以使用。
从基于文件的数据源进行数据摄取
文件型数据源在不同数据处理系统之间交换数据时非常常见。举个例子,假设一个零售商想要用外部数据(如邮政服务提供的邮政编码数据)来丰富他们的内部数据源。这两个组织之间的数据通常通过文件型数据格式进行交换,如 XML 或 JSON,或者更常见的方式是使用分隔符的纯文本或 CSV 格式。
Apache Spark 支持多种文件格式,如纯文本、CSV、JSON 以及二进制文件格式,如 Apache Parquet 和 ORC。这些文件需要存储在分布式文件系统上,如Hadoop 分布式文件系统(HDFS),或者是基于云的数据湖,如AWS S3、Azure Blob或ADLS存储。
让我们来看一下如何使用 PySpark 从 CSV 文件中读取数据,如下面的代码块所示:
retail_df = (spark
.read
.format("csv")
.option("inferSchema", "true")
.option("header","true")
.load("dbfs:/FileStore/shared_uploads/snudurupati@outlook.com/")
)
retail_df.show()
在前面的代码片段中,我们使用 spark.read() 函数来读取 CSV 文件。我们将 inferSchema 和 header 选项设置为 true,这有助于 Spark 通过读取数据样本来推断列名和数据类型信息。
重要提示
文件型数据源需要存储在分布式文件系统中。Spark 框架利用数据并行处理,每个 Spark Executor 尝试将数据的一个子集读取到其本地内存中。因此,文件必须存储在分布式文件系统中,并且所有 Executor 和 Driver 都能够访问该文件。HDFS 和基于云的数据湖,如 AWS S3、Azure Blob 和 ADLS 存储,都是分布式数据存储层,是与 Apache Spark 一起使用的理想文件型数据源。
在这里,我们从dbfs/位置读取 CSV 文件,这是 Databricks 的专有文件系统,称为Databricks 文件系统(DBFS)。DBFS 是一个抽象层,实际上使用的是 AWS S3、Azure Blob 或 ADLS 存储。
提示
由于每个 Executor 只尝试读取数据的一个子集,因此文件类型必须是可拆分的。如果文件不能拆分,Executor 可能会尝试读取比其可用内存更大的文件,从而导致内存溢出并抛出“内存不足”错误。一个不可拆分文件的例子是gzipped的 CSV 文件或文本文件。
从消息队列中读取数据
另一种在实时流处理分析中常用的数据源是消息队列。消息队列提供了一种发布-订阅模式的数据消费方式,其中发布者将数据发布到队列,而多个订阅者可以异步地消费数据。在分布式计算环境中,消息队列需要是分布式的、容错的,并且可扩展的,以便作为分布式数据处理系统的数据源。
其中一个消息队列是 Apache Kafka,它在与 Apache Spark 一起处理实时流式工作负载时非常突出。Apache Kafka 不仅仅是一个消息队列;它本身是一个端到端的分布式流处理平台。然而,在我们的讨论中,我们将 Kafka 视为一个分布式、可扩展且容错的消息队列。
让我们看一下使用 PySpark 从 Kafka 导入数据的代码,如下面的代码块所示:
kafka_df = (spark.read
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "wordcount")
.option("startingOffsets", "earliest")
.load()
)
kafka_df.show()
在前面的代码示例中,我们使用spark.read()通过提供主机名和端口号,从 Kafka 服务器加载数据,主题名为wordcount。我们还指定了 Spark 应该从队列的最开始处读取事件,使用StartingOffsets选项。尽管 Kafka 通常用于与 Apache Spark 一起处理流式用例,但这个前面的代码示例将 Kafka 作为批量处理数据的数据源。你将在使用结构化流实时导入数据部分学习如何使用 Kafka 和 Apache Spark 处理流。
提示
在 Kafka 术语中,单个队列称为主题,而每个事件称为偏移量。Kafka 是一个队列,因此它按照事件发布到主题的顺序处理事件,个别消费者可以选择自己的起始和结束偏移量。
现在你已经熟悉了使用 Apache Spark 从不同类型的数据源导入数据,在接下来的部分中,让我们学习如何将数据导入到数据汇中。
导入数据到数据汇
数据汇,顾名思义,是用于存储原始或处理过的数据的存储层,既可以用于短期暂存,也可以用于长期持久存储。尽管数据汇一词通常用于实时数据处理,但没有特定的限制,任何存储层中存放导入数据的地方都可以称为数据汇。就像数据源一样,数据汇也有不同的类型。你将在接下来的章节中学习一些最常见的类型。
导入到数据仓库
数据仓库是一种特定类型的持久数据存储,最常用于商业智能类型的工作负载。商业智能和数据仓库是一个完整的研究领域。通常,数据仓库使用 RDBMS 作为其数据存储。然而,数据仓库与传统数据库不同,它遵循一种特定的数据建模技术,称为维度建模。维度模型非常直观,适合表示现实世界的业务属性,并有利于用于构建商业报告和仪表板的商业智能类型查询。数据仓库可以建立在任何通用 RDBMS 上,或者使用专业的硬件和软件。
让我们使用 PySpark 将 DataFrame 保存到 RDBMS 表中,如下面的代码块所示:
wordcount_df = spark.createDataFrame(
[("data", 10), ("parallel", 2), ("Processing", 30), ("Spark", 50), ("Apache", 10)], ("word", "count"))
在前面的代码块中,我们通过编程方式从一个 Python List 对象创建了一个包含两列的 DataFrame。然后,我们使用 spark.write() 函数将 Spark DataFrame 保存到 MySQL 表中,如下所示的代码片段所示:
wordcount_df.write.format("jdbc").options(
url="jdbc:mysql://localhost:3306/pysparkdb",
driver = "org.mariadb.jdbc.Driver",
dbtable = "word_counts",
user="######",
password="@@@@@@").save()
前面的代码片段写入数据到 RDBMS 与读取数据的代码几乎相同。我们仍然需要使用 MySQL JDBC 驱动程序,并指定主机名、端口号、数据库名和数据库凭据。唯一的区别是,在这里,我们需要使用 spark.write() 函数,而不是 spark.read()。
向数据湖中注入数据
数据仓库非常适合直观地表示现实世界的业务数据,并以有利于商业智能类型工作负载的方式存储高度结构化的关系型数据。然而,当处理数据科学和机器学习类型工作负载所需的非结构化数据时,数据仓库就显得不足。数据仓库不擅长处理大数据的高体积和速度。这时,数据湖就填补了数据仓库留下的空白。
从设计上讲,数据湖在存储各种类型的数据时具有高度的可扩展性和灵活性,包括高度结构化的关系型数据以及非结构化数据,如图像、文本、社交媒体、视频和音频。数据湖也擅长处理批量数据和流数据。随着云计算的兴起,数据湖如今变得非常普遍,并且似乎是所有大数据分析工作负载的持久存储的未来。数据湖的一些例子包括 Hadoop HDFS、AWS S3、Azure Blob 或 ADLS 存储以及 Google Cloud 存储。
基于云的数据湖相较于本地部署的版本有一些优势:
-
它们是按需的,并且具有无限的可扩展性。
-
它们是按使用量计费的,从而节省了前期投资。
-
它们与计算资源完全独立;因此,存储可以独立于计算资源进行扩展。
-
它们支持结构化数据和非结构化数据,并同时支持批处理和流式处理,使得同一存储层可以用于多个工作负载。
由于上述优势,基于云的数据湖在过去几年变得越来越流行。Apache Spark 将这些数据湖视为另一种基于文件的数据存储。因此,使用 Spark 操作数据湖就像操作任何其他基于文件的数据存储层一样简单。
让我们来看一下使用 PySpark 将数据保存到数据湖是多么简单,如下所示的代码示例:
(wordcount_df
.write
.option("header", "true")
.mode("overwrite")
.save("/tmp/data-lake/wordcount.csv")
)
在前面的代码块中,我们将前一节中创建的 wordcount_df DataFrame 保存到数据湖中的 CSV 格式,使用的是 DataFrame 的 write() 函数。mode 选项指示 DataFrameWriter 替换指定文件位置中任何现有的数据;请注意,你也可以使用 append 模式。
向 NoSQL 和内存数据存储中注入数据
数据仓库一直以来都是数据分析用例的传统持久存储层,而数据湖作为新的选择,正在崛起,旨在满足更广泛的工作负载。然而,还有其他涉及超低延迟查询响应时间的大数据分析用例,这些用例需要特定类型的存储层。两种这样的存储层是 NoSQL 数据库和内存数据库,本节将详细探讨这两种存储层。
用于大规模操作分析的 NoSQL 数据库
NoSQL 数据库是传统关系型数据库的替代方案,主要用于处理杂乱且非结构化的数据。NoSQL 数据库在以 键值 对的形式存储大量非结构化数据方面表现非常出色,并且能够在高并发情况下,以常数时间高效地检索任何给定 键 对应的 值。
假设一个业务场景,其中一家企业希望通过毫秒级的查询响应时间,以高度并发的方式向单个客户提供预计算的、超个性化的内容。像 Apache Cassandra 或 MongoDB 这样的 NoSQL 数据库将是这个用例的理想选择。
注意
Apache Spark 并不自带针对 NoSQL 数据库的连接器。然而,这些连接器由各自的数据库提供商构建和维护,可以从相应的提供商处下载,并与 Apache Spark 配置使用。
内存数据库用于超低延迟分析
内存数据库仅将数据存储在内存中,不涉及磁盘等持久存储。正是由于这一特点,内存数据库在数据访问速度上比基于磁盘的数据库更快。一些内存数据库的例子包括 Redis 和 Memcached。由于系统内存有限且存储在内存中的数据在断电后无法持久保存,因此内存数据库不适合用于存储大量历史数据,这在大数据分析系统中是典型需求。然而,它们在涉及超低延迟响应时间的实时分析中有其应用。
假设有一家在线零售商希望在顾客结账时,在其在线门户上展示产品的预计运输交付时间。大多数需要估算交货时间的参数可以预先计算。然而,某些参数,如客户邮政编码和位置,只有在客户结账时提供时才能获得。在这种情况下,需要立即从 Web 门户收集数据,利用超快的事件处理系统进行处理,然后将结果计算并存储在超低延迟的存储层中,以便通过 Web 应用程序访问并返回给客户。所有这些处理应该在几秒钟内完成,而像 Redis 或 Memcached 这样的内存数据库将充当超低延迟数据存储层的角色。
到目前为止,你已经学习了如何访问来自不同数据源的数据并将其导入到各种数据接收端。此外,你已经了解到你对数据源的控制有限,但你对数据接收端有完全的控制。为某些高并发、超低延迟的用例选择正确的数据存储层非常重要。然而,对于大多数大数据分析用例,数据湖已成为首选的持久数据存储层,几乎成了事实上的标准。
另一个优化数据存储的关键因素是数据的实际格式。在接下来的部分,我们将探讨几种数据存储格式及其相对优点。
使用文件格式在数据湖中存储数据
你选择用于在数据湖中存储数据的文件格式对数据存储和检索的便捷性、查询性能以及存储空间有关键影响。因此,选择一个可以平衡这些因素的最佳数据格式至关重要。数据存储格式大致可以分为结构化、非结构化和半结构化格式。在本节中,我们将通过代码示例探讨这几种类型。
非结构化数据存储格式
非结构化数据是指没有预定义数据模型表示的数据,可以是人工或机器生成的。例如,非结构化数据可以是存储在纯文本文件、PDF 文件、传感器数据、日志文件、视频文件、图像、音频文件、社交媒体流等中的数据。
非结构化数据可能包含重要的模式,提取这些模式可能带来有价值的见解。然而,由于以下原因,以非结构化格式存储数据并不十分有用:
-
非结构化数据可能并不总是具有固有的压缩机制,并且可能占用大量存储空间。
-
对非结构化文件进行外部压缩可以节省空间,但会消耗用于文件压缩和解压的处理能力。
-
存储和访问非结构化文件比较困难,因为它们本身缺乏任何模式信息。
鉴于上述原因,摄取非结构化数据并在将其存储到数据湖之前将其转换为结构化格式是合理的。这样可以使后续的数据处理更加轻松和高效。让我们看一个例子,我们将一组非结构化的图像文件转换为图像属性的 DataFrame,然后使用 CSV 文件格式存储它们,如下所示的代码片段所示:
Raw_df = spark.read.format("image").load("/FileStore/FileStore/shared_uploads/images/")
raw_df.printSchema()
image_df = raw_df.select("image.origin", "image.height", "image.width", "image.nChannels", "image.mode", "image.data")
image_df.write.option("header", "true").mode("overwrite").csv("/tmp/data-lake/images.csv")
在之前的代码块中,发生了以下情况:
-
我们使用 Spark 内置的
image格式加载一组图像文件,结果是一个包含图像属性的 Spark DataFrame。 -
我们使用
printSchema()函数查看 DataFrame 的模式,并发现 DataFrame 有一个名为image的单一嵌套列,其中包含origin、height、width、nChannels等作为其内部属性。 -
然后,我们使用
image前缀将内部属性提升到顶层,例如image.origin,并创建一个新的 DataFrame,命名为image_df,其中包含图像的所有单独属性作为顶层列。 -
现在我们已经得到了最终的 DataFrame,我们将其以 CSV 格式写入数据湖。
-
在浏览数据湖时,你可以看到该过程向数据湖写入了几个 CSV 文件,文件大小大约为 127 字节。
提示
写入存储的文件数量取决于 DataFrame 的分区数量。DataFrame 的分区数量取决于执行器核心的数量以及
spark.sql.shuffle.partitions的 Spark 配置。每当 DataFrame 进行洗牌操作时,这个数量也会发生变化。在 Spark 3.0 中,自适应查询执行会自动管理最优的洗牌分区数量。
文件大小和查询性能是考虑文件格式时的两个重要因素。因此,我们进行一个快速测试,在 DataFrame 上执行一个适度复杂的操作,如以下代码块所示:
from pyspark.sql.functions import max, lit
temp_df = final_df.withColumn("max_width", lit(final_df.agg(max("width")).first()[0]))
temp_df.where("width == max_width").show()
前面的代码块首先创建了一个新列,其中每一行的值为所有行中最大的宽度。然后,它过滤掉具有width列最大值的行。这个查询是适度复杂的,典型的数据分析查询类型。在我们的示例测试中,在一个非结构化二进制文件上运行的查询大约花费了5.03 秒。在接下来的章节中,我们将查看其他文件格式上的相同查询,并比较查询性能。
半结构化数据存储格式
在前面的示例中,我们能够获取一个二进制图像文件,提取其属性,并将其存储为 CSV 格式,这使得数据结构化,但仍保持人类可读格式。CSV 格式是另一种数据存储格式,称为半结构化数据格式。半结构化数据格式与非结构化数据格式类似,没有预定义的数据模型。然而,它们以一种方式组织数据,使得从文件本身推断模式信息变得更加容易,而无需提供外部元数据。它们是不同数据处理系统之间交换数据的流行数据格式。半结构化数据格式的示例包括 CSV、XML 和 JSON。
让我们看看如何使用 PySpark 处理半结构化数据的示例,如以下代码块所示:
csv_df = spark.read.options(header="true", inferSchema="true").csv("/tmp/data-lake/images.csv")
csv_df.printSchema()
csv_df.show()
前面的代码示例使用在先前图像处理示例中生成的 CSV 文件,并将其加载为 Spark 数据框。我们启用了从实际数据推断列名和数据类型的选项。printSchema()函数显示 Spark 能够正确推断所有列的数据类型,除了来自半结构化文件的二进制数据列。show()函数显示数据框已经从 CSV 文件中正确重建,并且包含列名。
我们将在csv_df数据框上运行一个适度复杂的查询,如下所示代码块:
from pyspark.sql.functions import max, lit
temp_df = csv_df.withColumn("max_width", lit(csv_df.agg(max("width")).first()[0]))
temp_df.where("width == max_width").show()
在前面的代码块中,我们执行了一些数据框操作,以获取width列最大值的行。使用 CSV 数据格式执行的代码花费了1.24 秒,而我们在非结构化数据存储格式部分执行的类似代码大约花费了5 秒。因此,显然,半结构化文件格式比非结构化文件更适合数据存储,因为从这种数据存储格式中推断模式信息相对更容易。
然而,请注意前面代码片段中show()函数的结果。包含二进制数据的数据列被错误地推断为字符串类型,并且列数据被截断。因此,需要注意的是,半结构化格式并不适合表示所有数据类型,并且在从一种数据格式转换到另一种数据格式时,某些数据类型可能会丢失信息。
结构化数据存储格式
结构化数据遵循预定义的数据模型,具有表格格式,具有明确定义的行和命名列以及定义的数据类型。结构化数据格式的一些示例包括关系数据库表和事务系统生成的数据。请注意,还有一些完全结构化数据及其数据模型的文件格式,如 Apache Parquet、Apache Avro 和 ORC 文件,它们可以轻松存储在数据湖中。
Apache Parquet是一种二进制、压缩的列式存储格式,旨在提高数据存储效率和查询性能。Parquet 是 Apache Spark 框架的一级公民,Spark 的内存存储格式Tungsten旨在充分利用 Parquet 格式。因此,当你的数据存储在 Parquet 格式中时,你将从 Spark 中获得最佳的性能和效率。
注意
Parquet 文件是一种二进制文件格式,意味着文件的内容经过二进制编码。因此,它们不可供人类阅读,不像基于文本的文件格式,如 JSON 或 CSV。然而,这种格式的一个优点是,机器可以轻松解析这些文件,并且在编码和解码过程中不会浪费时间。
让我们将image_df DataFrame(包含来自未结构化数据存储格式部分的图像属性数据)转换为 Parquet 格式,如下所示的代码块所示:
final_df.write.parquet("/tmp/data-lake/images.parquet")
parquet_df = spark.read.parquet("/tmp/data-lake/images.parquet")
parquet_df.printSchema()
parquet_df.show()
上一个代码块将二进制图像文件加载到 Spark DataFrame 中,并将数据以 Parquet 格式写回数据湖。show()函数的结果显示,data列中的二进制数据并未被截断,并且已经从源图像文件中如实保留。
让我们执行一个中等复杂度的操作,如下所示的代码块:
temp_df = parquet_df.withColumn("max_width", lit(parquet_df.agg(max("width")).first()[0]))
temp_df.where("width == max_width").show()
上述代码块提取了列名为width的最大值所在的行。该查询大约需要4.86 秒来执行,而使用原始未结构化的图像数据时则需要超过5 秒。因此,这使得结构化的 Parquet 文件格式成为在 Apache Spark 数据湖中存储数据的最佳格式。表面上看,半结构化的 CSV 文件执行查询所需时间较短,但它们也截断了数据,导致它们并不适合所有使用场景。作为一个通用的经验法则,几乎所有 Apache Spark 的使用场景都推荐使用 Parquet 数据格式,除非某个特定的使用场景需要其他类型的数据存储格式。
到目前为止,你已经看到选择合适的数据格式会影响数据的正确性、易用性、存储效率、查询性能和可扩展性。此外,无论你使用哪种数据格式,将数据存储到数据湖中时,还有另一个需要考虑的因素。这个技术叫做数据分区,它可以真正决定你的下游查询性能是成功还是失败。
简而言之,数据分区是将数据物理地划分到多个文件夹或分区中的过程。Apache Spark 利用这些分区信息,只将查询所需的相关数据文件加载到内存中。这一机制称为分区剪枝。你将会在第三章,数据清理与整合中了解更多关于数据分区的内容。
到目前为止,你已经了解了企业决策支持系统(DSS)的各个组成部分,即数据源、数据目标和数据存储格式。此外,在上一章中,你对 Apache Spark 框架作为大数据处理引擎也有了一定的了解。现在,让我们运用这些知识,构建一个端到端的数据摄取管道。
构建批处理和实时数据摄取管道
一个端到端的数据摄取管道涉及从数据源读取数据,并将其摄取到数据目标中。在大数据和数据湖的背景下,数据摄取通常涉及大量数据源,因此需要一个高可扩展性的数据处理引擎。市场上有一些专门的工具,旨在处理大规模数据摄取,例如 StreamSets、Qlik、Fivetran、Infoworks 等第三方供应商提供的工具。此外,云服务提供商也有其自有的本地工具,例如 AWS 数据迁移服务、Microsoft Azure 数据工厂和 Google Dataflow。还有一些免费的开源数据摄取工具可以考虑使用,例如 Apache Sqoop、Apache Flume、Apache Nifi 等。
提示
Apache Spark 足够适合用于临时数据摄取,但将 Apache Spark 作为专门的数据摄取引擎并不是行业中的常见做法。相反,您应该考虑选择一个专门的、为数据摄取需求量身定制的工具。您可以选择第三方供应商提供的工具,或者选择自己管理一个开源工具。
在本节中,我们将探讨 Apache Spark 在批量处理和流处理方式下的数据摄取能力。
批量处理的数据摄取
批量处理是指一次处理一组或一批数据。批量处理通常是在预定的时间间隔内运行的,且不需要用户干预。通常情况下,批量处理会安排在夜间、业务时间之外运行。其简单的原因在于,批量处理通常需要从操作系统中读取大量的事务数据,这会给操作系统带来很大的负担。这是不可取的,因为操作系统对于企业的日常运营至关重要,我们不希望给事务系统带来那些对日常业务操作没有关键影响的工作负载。
此外,批量处理任务通常是重复性的,因为它们会在固定的时间间隔内运行,每次都会引入自上次成功的批处理以来生成的新数据。批量处理可以分为两种类型,分别是 完整数据加载 和 增量数据加载。
完整数据加载
完全数据加载涉及完全覆盖现有数据集。这对于数据量相对较小且变化不频繁的数据集非常有用。它也是一个更容易实现的过程,因为我们只需要扫描整个源数据集并完全覆盖目标数据集。无需维护任何关于上次数据导入作业的状态信息。以数据仓库中的维度表为例,例如日历表或包含所有零售商实体店数据的表。这些表变化不大且相对较小,非常适合进行完全数据加载。虽然实现简单,但在处理非常大的源数据集并且数据经常变化时,完全数据加载有其缺点。
假设我们考虑一个大型零售商的交易数据,该零售商在全国拥有超过一千家门店,每家门店每月产生大约 500 笔交易。换算下来,大约每天有 15,000 笔交易被导入数据湖。考虑到历史数据,这个数字会迅速增加。假设我们刚开始构建数据湖,目前只导入了大约 6 个月的交易数据。即使在这种规模下,我们的数据集中已经有了 300 万条交易记录,完全清空并重新加载数据集并非一项轻松的任务。
另一个需要考虑的重要因素是,通常操作系统只保留小时间间隔的历史数据。在这里,完全加载意味着也会丢失数据湖中的历史数据。此时,您应考虑增量加载来进行数据导入。
增量数据加载
在增量数据加载过程中,我们只导入在上次成功的数据导入后,数据源中新创建的一组数据。这个增量数据集通常被称为 delta(增量集)。与完全加载相比,增量加载导入的数据集更小,并且由于我们已经在 delta 湖中维护了完整的历史数据,因此增量加载不需要依赖数据源来维护完整的历史记录。
基于前面提到的零售商示例,假设我们每晚运行一次增量批量加载。在这种情况下,我们每天只需要将 15,000 笔交易导入数据湖,这相对容易管理。
设计增量数据导入管道并不像设计完全加载管道那么简单。需要维护增量作业上次运行的状态信息,以便我们能够识别所有来自数据源的尚未导入到数据湖中的新记录。这个状态信息被存储在一个特殊的数据结构中,称为水印表。这个水印表需要由数据导入作业来更新和维护。一个典型的数据导入管道如下图所示:

图 2.2 – 数据摄取
上面的图示展示了一个典型的使用 Apache Spark 的数据摄取管道,并且包含了用于增量加载的水印表。在这里,我们使用 Spark 内建的数据源从源系统摄取原始交易数据,使用 DataFrame 操作进行处理,然后将数据发送回数据湖。
在扩展上一节的零售示例时,让我们使用 PySpark 构建一个端到端的数据摄取管道,采用批处理方式。构建数据管道的一个前提条件当然是数据,对于这个示例,我们将使用UC Irvine 机器学习库提供的在线零售数据集。该数据集以 CSV 格式存放在本章技术要求部分提到的 GitHub 仓库中。在线零售数据集包含了一个在线零售商的交易数据。
我们将下载包含两个 CSV 文件的数据集,并通过笔记本文件菜单中的上传接口将它们上传到Databricks Community Edition笔记本环境。一旦数据集上传完成,我们将记录文件位置。
注意
如果你使用的是自己的 Spark 环境,请确保数据集存放在 Spark 集群可以访问的位置。
现在,我们可以开始实际的代码部分,构建数据摄取管道,如以下代码示例所示:
retail_df = (spark
.read
.option("header", "true")
.option("inferSchema", "true")
.csv("/FileStore/shared_uploads/online_retail/online_retail.csv")
)
在前面的代码块中,我们启用了header和inferSchema选项来加载 CSV 文件。这会创建一个包含八个列及其相应数据类型和列名的 Spark DataFrame。现在,让我们将这些数据以 Parquet 格式摄取到数据湖中,如以下代码块所示:
(retail_df
.write
.mode("overwrite")
.parquet("/tmp/data-lake/online_retail.parquet")
)
在这里,我们将包含原始零售交易数据的retail_df Spark DataFrame 使用 DataFrameWriter 的write()函数保存到数据湖中,以 Parquet 格式存储。我们还将mode选项设置为overwrite,基本上执行的是全量数据加载。
需要注意的一点是,整个数据摄取作业仅仅是10行代码,而且它可以轻松扩展到数千万条记录,处理多达数个 PB 的数据。这就是 Apache Spark 的强大与简洁,它使得 Apache Spark 在极短的时间内成为大数据处理的事实标准。那么,你将如何扩展前述的数据摄取批处理作业,并最终将其投入生产环境呢?
Apache Spark 是从头开始构建的,旨在具备可扩展性,其可扩展性完全依赖于集群上作业可用的核心数量。因此,要扩展你的 Spark 作业,你只需为作业分配更多的处理核心。大多数商业化的 Spark 作为托管服务的提供方案都提供了便捷的自动扩展功能。通过此自动扩展功能,你只需指定集群的最小和最大节点数,集群管理器就能动态计算出为你的作业分配的最优核心数。
大多数商业化的 Spark 提供方案也配备了内置的作业调度器,并支持将笔记本直接作为作业进行调度。外部调度器,从简单的crontab到复杂的作业协调器如Apache Airflow,也可以用来将 Spark 作业生产化。这大大简化了集群容量规划的过程,帮助你腾出时间,专注于实际的数据分析,而不是在容量规划、调优和维护 Spark 集群上耗费时间和精力。
到目前为止,在本节中,你已经查看了一个完整加载批处理摄取作业的示例,该作业从数据源加载整个数据集,并覆盖数据湖中的数据集。你需要添加一些业务逻辑,以在水印数据结构中维护摄取作业的状态,然后计算增量进行增量加载。你可以自己构建所有这些逻辑,或者,也可以简单地使用 Spark 的结构化流处理引擎来为你完成繁重的工作,正如接下来的部分将讨论的那样。
使用结构化流处理进行实时数据摄取
企业通常需要在实时做出战术决策的同时进行战略决策,以保持竞争力。因此,实时将数据摄取到数据湖的需求应运而生。然而,跟上大数据的快速数据速度需要一个强大且可扩展的流处理引擎。Apache Spark 就有这样一个流处理引擎,叫做结构化流处理,我们接下来将探讨它。
结构化流处理入门
结构化流处理是一个基于 Spark SQL 引擎的 Spark 流处理引擎。与 Spark 的其他所有组件一样,结构化流处理也具备可扩展性和容错性。由于结构化流处理基于 Spark SQL 引擎,你可以使用与批处理一样的 Spark DataFrame API 来进行流处理。结构化流处理支持 DataFrame API 所支持的所有函数和构造。
结构化流处理将每个传入的数据流视为一批小数据,称为微批次(micro-batch),并不断将每个微批次附加到目标数据集。结构化流处理的编程模型持续处理微批次,将每个微批次视为一个批处理作业。因此,现有的 Spark 批处理作业可以通过少量的修改轻松转换为流处理作业。结构化流处理旨在提供最大吞吐量,这意味着结构化流处理作业可以扩展到集群中的多个节点,并以分布式方式处理大量传入数据。
结构化流处理还具备额外的故障容忍能力,保证精确一次语义(exactly-once semantics)。为了实现这一点,结构化流处理跟踪数据处理进度。它通过检查点(checkpointing)和预写日志(write-ahead logs)等概念,跟踪任何时刻处理的偏移量或事件。预写日志是关系型数据库中的一个概念,用来保证数据库的原子性和持久性。在此技术中,记录会先写入日志,然后再写入最终的数据库。检查点是结构化流处理中另一种技术,它将当前读取的偏移量位置记录在持久化存储系统中。
通过这些技术,结构化流处理能够记录流中最后一个处理过的偏移量的位置,使其具备在流处理作业失败时从中断点恢复处理的能力。
注意
我们建议将检查点存储在具有高可用性和分区容忍支持的持久化存储中,例如基于云的数据湖。
这些技术(检查点、预写日志和可重放的流式数据源)以及支持重新处理数据的流式数据接收器,使得结构化流处理能够保证每个流事件都被处理一次且仅处理一次。
注意
结构化流处理的微批处理模型不适合处理源端事件发生时立即进行处理。像 Apache Flink 或 Kafka Streams 这样的其他流处理引擎,更适合超低延迟的流处理。
增量加载数据
由于结构化流处理(Structured Streaming)内置了机制,帮助你轻松维护增量加载所需的状态信息,因此你可以简单地选择结构化流处理来处理所有增量加载,真正简化你的架构复杂性。让我们构建一个管道,以实时流式方式执行增量加载。
通常,我们的数据摄取从已经加载到数据源的数据开始,例如数据湖或像 Kafka 这样的消息队列。在这里,我们首先需要将一些数据加载到 Kafka 主题中。你可以从一个已经在主题中包含数据的现有 Kafka 集群开始,或者你可以设置一个快速的 Kafka 服务器,并使用github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/blob/main/Chapter02/utils/kafka-setup.ipynb中提供的代码加载在线零售数据集。
让我们来看一下如何使用结构化流处理从 Kafka 实时摄取数据到数据湖,以下是相关的代码片段:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType, DoubleType
eventSchema = ( StructType()
.add('InvoiceNo', StringType())
.add('StockCode', StringType())
.add('Description', StringType())
.add('Quantity', IntegerType())
.add('InvoiceDate', StringType())
.add('UnitPrice', DoubleType())
.add('CustomerID', IntegerType())
.add('Country', StringType())
)
在前面的代码块中,我们声明了所有我们打算从 Kafka 事件中读取的列及其数据类型。结构化流处理要求数据模式必须提前声明。一旦定义了模式,我们就可以开始从 Kafka 主题中读取数据,并将其加载到 Spark DataFrame 中,如以下代码块所示:
kafka_df = (spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers",
"localhost:9092")
.option("subscribe", "retail_events")
.option("startingOffsets", "earliest")
.load()
)
在前面的代码块中,我们开始从一个 Kafka 主题 retail_events 中读取事件流,并告知 Kafka 我们希望从流的开始处加载事件,使用 startingOffsets 选项。Kafka 主题中的事件遵循键值对模式。这意味着我们的实际数据被编码在 value 列中的 JSON 对象内,我们需要提取这些数据,如下代码块所示:
from pyspark.sql.functions import col, from_json, to_date
retail_df = (kafka_df
.select(from_json(col("value").cast(StringType()), eventSchema).alias("message"), col("timestamp").alias("EventTime"))
.select("message.*", "EventTime")
)
在前面的代码块中,我们通过传递之前定义的数据模式对象,使用 from_json() 函数提取数据。这样会得到一个 retail_df DataFrame,其中包含我们需要的事件所有列。此外,我们从 Kafka 主题中附加了一个 EventTime 列,它显示了事件实际到达 Kafka 的时间。这个信息在之后的进一步数据处理过程中可能会有所帮助。由于这个 DataFrame 是通过 readStream() 函数创建的,Spark 本身就知道这是一个流式 DataFrame,并为该 DataFrame 提供了结构化流处理 API。
一旦我们从 Kafka 流中提取了原始事件数据,就可以将其持久化到数据湖中,如以下代码块所示:
base_path = "/tmp/data-lake/retail_events.parquet"
(retail_df
.withColumn("EventDate", to_date(retail_df.EventTime))
.writeStream
.format('parquet')
.outputMode("append")
.trigger(once=True)
.option('checkpointLocation', base_path + '/_checkpoint')
.start(base_path)
)
在前面的代码块中,我们利用了 writeStream() 函数,这是流式 DataFrame 提供的功能,可以以流式方式将数据保存到数据湖中。在这里,我们以 Parquet 格式写入数据,结果数据湖中的数据将是一组 .parquet 文件。保存后,这些 Parquet 文件与任何其他由批处理或流处理创建的 Parquet 文件没有区别。
此外,我们将outputMode设置为append,以表明我们将其视为一个无界数据集,并将继续追加新的 Parquet 文件。checkpointLocation选项存储结构化流处理的写前日志及其他检查点信息。这使其成为一个增量数据加载作业,因为流处理仅基于存储在检查点位置的偏移信息处理新的和未处理的事件。
注意
结构化流处理支持complete和update模式,除了append模式外。关于这些模式的描述以及何时使用它们,可以在 Apache Spark 的官方文档中找到,网址为spark.apache.org/docs/latest/structured-streaming-programming-guide.html#output-modes。
那如果你需要将增量数据加载作业作为较少频繁的批处理作业运行,而不是以持续流处理方式运行呢?
结构化流处理也支持这种情况,方法是通过trigger选项。我们可以将once=True用于该选项,流处理作业将在外部触发时处理所有新的和未处理的事件,然后在没有新的事件需要处理时停止流处理。我们可以根据时间间隔安排该作业定期运行,它的行为就像一个批处理作业,但具备增量加载的所有优势。
总结来说,Spark SQL 引擎的 DataFrame API 在批量数据处理和流处理方面都非常强大且易于使用。静态 DataFrame 和流式 DataFrame 在功能和工具方面有一些微小的差别。然而,在大多数情况下,使用 DataFrame 的批处理和流处理编程模型非常相似。这减少了学习曲线,有助于使用 Apache Spark 的统一分析引擎来统一批处理和流处理。
现在,在下一节中,让我们探讨如何使用 Apache Spark 实现一个统一的数据处理架构,采用的概念就是Lambda 架构。
使用 Lambda 架构统一批量数据和实时数据
批量数据处理和实时数据处理是现代企业决策支持系统(DSS)中的重要组成部分,能够无缝实现这两种数据处理技术的架构有助于提高吞吐量、减少延迟,并使您能够更快速地获得最新数据。这样的架构被称为Lambda 架构,我们接下来将详细探讨。
Lambda 架构
Lambda 架构是一种数据处理技术,用于以单一架构摄取、处理和查询历史数据与实时数据。在这里,目标是提高吞吐量、数据新鲜度和容错性,同时为最终用户提供历史数据和实时数据的统一视图。以下图示展示了一个典型的 Lambda 架构:

图 2.3 – Lambda 架构
如前图所示,Lambda 架构由三个主要组件组成,即 批处理层、速度层 和 服务层。我们将在接下来的章节中讨论这几个层。
批处理层
批处理层就像任何典型的 ETL 层,涉及从源系统批量处理数据。这个层通常涉及定期运行的调度作业,通常在晚上进行。
Apache Spark 可用于构建批处理作业或按计划触发的结构化流式作业,也可用于批处理层构建数据湖中的历史数据。
速度层
速度层持续从数据湖中与批处理层相同的数据源中摄取数据,生成实时视图。速度层不断提供批处理层尚未提供的最新数据,这是由于批处理层固有的延迟。Spark Structured Streaming 可用于实现低延迟的流式作业,持续从源系统中摄取最新数据。
服务层
服务层将批处理层中的历史数据和速度层中的最新数据合并为一个视图,以支持终端用户的临时查询。Spark SQL 是服务层的一个优秀候选,它可以帮助用户查询批处理层中的历史数据以及速度层中的最新数据,并为用户呈现一个统一的数据视图,以便进行低延迟的临时查询。
在前面的章节中,你实现了批处理和流式处理的数据摄取作业。现在,让我们探讨如何将这两种视图结合起来,向用户提供一个统一的视图,如下所示的代码片段所示:
batch_df = spark.read.parquet("/tmp/data-lake/online_retail.parquet")
speed_df = spark.read.parquet("/tmp/data-lake/retail_events.parquet").drop("EventDate").drop("EventTime")
serving_df = batch_df.union(speed_df)
serving_df.createOrReplaceGlobalTempView("serving_layer")
在前面的代码块中,我们创建了两个 DataFrame,一个是通过union函数将这两个 DataFrame 合并,然后使用合并后的 DataFrame 创建一个 Spark Global Temp View。结果是一个可以在集群中所有 Spark Sessions 中访问的视图,它为你提供了跨批处理层和速度层的数据统一视图,如下代码所示:
%sql
SELECT count(*) FROM global_temp.serving_layer;
前面的代码行是一个 SQL 查询,它查询来自 Spark 全局视图的数据,Spark 全局视图充当 服务层,并可以呈现给终端用户,以便跨最新数据和历史数据进行临时查询。
通过这种方式,您可以利用 Apache Spark SQL 引擎的 DataFrame、结构化流处理和 SQL API 来构建一个 Lambda 架构,从而提高数据的新鲜度、吞吐量,并提供统一的数据视图。然而,Lambda 架构的维护较为复杂,因为它有两个独立的数据导入管道,分别用于批处理和实时处理,并且有两个独立的数据接收端。实际上,有一种更简单的方式可以使用开源存储层 Delta Lake 来统一批处理和实时层。您将在第三章,数据清洗与整合中学习到这一点。
总结
在本章中,您了解了在大数据分析背景下的企业决策支持系统(DSS)及其组成部分。您学习了各种类型的数据源,如基于 RDBMS 的操作系统、消息队列、文件源,以及数据接收端,如数据仓库和数据湖,并了解了它们的相对优缺点。
此外,您还探索了不同类型的数据存储格式,如非结构化、结构化和半结构化数据,并了解了使用结构化格式(如 Apache Parquet 与 Spark)带来的好处。您还了解了数据批量导入和实时导入的方式,并学习了如何使用 Spark DataFrame API 来实现它们。我们还介绍了 Spark 的结构化流处理框架,用于实时流数据处理,您学习了如何使用结构化流处理来实现增量数据加载,同时减少编程负担。最后,您探索了 Lambda 架构,将批处理和实时数据处理进行统一,并了解了如何使用 Apache Spark 来实现它。您在本章中学到的技能将帮助您通过 Apache Spark 实施可扩展且高性能的分布式数据导入管道,支持批处理和流处理模式。
在下一章中,您将学习如何处理、清洗和整合在本章中导入数据湖的原始数据,将其转化为干净、整合且有意义的数据集,供最终用户进行业务分析并生成有价值的洞察。
第三章:数据清洗与集成
在上一章中,你了解了数据分析过程的第一步——即将来自各个源系统的原始事务数据导入云数据湖。一旦获得原始数据,我们需要对其进行处理、清洗,并转换为有助于提取有意义的、可操作的商业洞察的格式。这个清洗、处理和转换原始数据的过程被称为数据清洗与集成。本章将讲解这一过程。
来自运营系统的原始数据,在其原始格式下并不适合进行数据分析。在本章中,你将学习各种数据集成技术,这些技术有助于整合来自不同源系统的原始事务数据,并将它们合并以丰富数据,向最终用户展示一个统一的、经过整合的真实版本。接着,你将学习如何使用数据清洗技术清理和转换原始数据的形式和结构,使其适合数据分析。数据清洗主要涉及修复数据中的不一致性,处理坏数据和损坏数据,消除数据中的重复项,并将数据标准化以符合企业的数据标准和惯例。你还将了解使用云数据湖作为分析数据存储所面临的挑战。最后,你将了解一种现代数据存储层——Delta Lake,以克服这些挑战。
本章将为你提供将原始数据整合、清洗和转换为适合分析结构的核心技能,并为你提供在云中构建可扩展、可靠且适合分析的数据湖的有用技术。作为开发者,本章的内容将帮助你随时让业务用户访问所有数据,让他们能够更快速、更轻松地从原始数据中提取可操作的洞察。
本章将涵盖以下主要内容:
-
将原始数据转换为丰富的有意义数据
-
使用云数据湖构建分析数据存储
-
使用数据集成整合数据
-
使用数据清洗使原始数据准备好进行分析
技术要求
在本章中,我们将使用 Databricks 社区版来运行我们的代码(community.cloud.databricks.com)。注册说明可以在databricks.com/try-databricks找到。
本章中的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter03下载。
本章的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
将原始数据转换为有意义的丰富数据
每个数据分析系统都包括几个关键阶段,包括数据摄取、数据转换以及加载到数据仓库或数据湖中。只有在数据经过这些阶段后,才能准备好供最终用户进行描述性和预测性分析。有两种常见的行业实践用于进行此过程,广泛称为提取、转换、加载(ETL)和提取、加载、转换(ELT)。在本节中,你将探讨这两种数据处理方法,并理解它们的主要区别。你还将了解在云端大数据分析背景下,ELT 相比 ETL 所具备的主要优势。
提取、转换和加载数据
这是几乎所有数据仓库系统遵循的典型数据处理方法。在这个方法中,数据从源系统中提取,并存储在临时存储位置,如关系数据库,称为暂存区。然后,暂存区中的数据会被整合、清洗和转换,最后加载到数据仓库中。下图展示了典型的 ETL 过程:

图 3.1 – 提取、转换和加载
如前面的图所示,ETL 过程由三个主要阶段组成。我们将在接下来的章节中讨论这些阶段。
从运营系统中提取数据
ETL 阶段涉及从多个源系统中提取选择性的原始事务数据,并将其暂存于临时存储位置。此步骤相当于数据摄取过程,你可以在第二章《数据摄取》中学习到。ETL 过程通常处理大量数据,尽管直接在源系统上运行可能会对它们造成过重的负担。运营系统对日常业务功能至关重要,因此不建议不必要地增加其负担。因此,提取过程会在非工作时间从源系统中提取数据,并将其存储在暂存区。此外,ETL 处理可以在暂存区中的数据上进行,从而使运营系统能够处理其核心功能。
转换、清洗和整合数据
这一阶段涉及各种数据转换过程,如数据集成、数据清洗、连接、过滤、拆分、标准化、验证等。此步骤将原始事务数据转化为清晰、集成和丰富的版本,准备进行业务分析。我们将在本章的使用数据集成整合数据和使用数据清洗使原始数据具备分析能力部分深入探讨这一阶段。
将数据加载到数据仓库
这是 ETL 过程的最后阶段,经过转换的数据最终被加载到持久的历史数据存储层中,如数据仓库。通常,ETL 处理系统会在一个单一的流程中完成转换和加载步骤,其中暂存区的原始数据经过清洗、集成和根据业务规则转换后加载到数据仓库中。
ETL 和数据仓库的优缺点
ETL 方法的某些优势在于,数据被转换并加载到一个结构化的分析数据存储中,如数据仓库,这使得数据的分析既高效又具有较高的性能。由于 ETL 模式已经存在了几十年,现在市场上有一些成熟的平台和工具,可以非常高效地在单一统一的流程中执行 ETL。
ETL 的另一个优点是,由于数据在加载到最终存储之前已经处理过,因此可以有机会省略不需要的数据或掩盖敏感数据。这在满足数据合规性和监管要求方面非常有帮助。
然而,ETL 过程以批处理方式运行,通常每天晚上执行一次。因此,只有在 ETL 批处理成功完成后,最终用户才能访问新数据。这就产生了对数据工程师的依赖,需要他们高效地运行 ETL 过程,同时最终用户在获取最新数据之前会有相当的延迟。
每次在下一次计划的 ETL 负载开始之前,暂存区的数据几乎都会被完全清除。而且,操作系统通常不会保留超过几年的事务数据的历史记录。这意味着,最终用户无法访问历史原始数据,除了数据仓库中的已处理数据。对于某些类型的数据分析(如预测分析)来说,这些历史原始数据可能非常有用,但数据仓库通常不会保留这些数据。
ETL 过程围绕数据仓库概念演变,更多适用于本地环境下的商业智能工作负载。数据仓库高度结构化且相对僵化的特性使得 ETL 不太适合数据科学和机器学习,这两者都涉及大量非结构化数据。此外,ETL 过程的批处理特性使其不适用于实时分析。而且,ETL 和数据仓库没有充分利用云技术及基于云的数据湖。因此,一种新的数据处理方法 提取、加载和转换(ELT)应运而生,接下来的章节将详细介绍这一方法。
提取、加载和转换数据
在 ELT 方法中,来自源系统的事务性数据以其原始、未经处理的格式被摄取到数据湖中。摄取到数据湖中的原始数据随后按需或定期进行转换。在 ELT 过程中,原始数据直接存储在数据湖中,通常不会被删除。因此,数据可能会以巨大的规模增长,并且几乎需要无限的存储和计算能力。传统的本地数据仓库和数据湖并未设计用来处理如此庞大的数据规模。因此,ELT 方法的实现仅能依赖现代云技术,这些技术提供了高度可扩展和弹性的计算与存储资源。下图展示了典型的 ELT 过程:

图 3.2 – 提取、加载和转换
在前述图示中,原始数据从多个源系统连续或定期地摄取到数据湖中。然后,数据湖中的原始数据被集成、清洗并转换,之后再存储回数据湖中。数据湖中的清洗和聚合数据作为所有类型下游分析的单一事实来源。
使用 ELT,几乎可以保留任何量的历史数据,且数据可以在源系统中创建后立即提供。无需在摄取数据之前进行预处理,并且由于数据湖对数据格式或结构没有严格要求,ELT 可以摄取并存储各种结构化、非结构化和半结构化的数据。因此,ETL 过程使得所有历史原始数据都可用,从而使数据转换完全按需进行。
选择 ELT 而非 ETL 的优势
ELT 方法学的一些优势在于数据可以以更快的速度进行摄取,因为不需要预处理步骤。它在数据摄取的灵活性方面也更强,有助于解锁如数据科学和机器学习等新的分析用例。ETL 利用云数据湖提供的弹性存储,帮助组织维护事务数据的副本,并保存几乎无限的历史记录。作为云端技术,ELT 还消除了数据复制和归档管理的麻烦,因为大多数云提供商都提供了这些托管服务,并保证服务水平协议(SLAs)。
ELT 方法学正在迅速成为云端大数据处理的事实标准,特别适用于处理大量事务数据的组织。对于已经进入云端或未来有云端战略的组织,推荐采用 ELT 方法学进行数据处理。
然而,云端的 ELT 方法学仍处于起步阶段,云数据湖并未提供其数据仓库对应物所具备的任何事务性或可靠性保障。在下一节中,您将探索构建基于云的数据湖所涉及的一些挑战,并探讨克服这些挑战的方法。
使用云数据湖构建分析数据存储
在本节中,您将探讨基于云的数据湖为大数据分析系统提供的优势,并了解在利用基于云的数据分析系统时,大数据分析系统面临的一些挑战。您还将编写几个PySpark代码示例,亲自体验这些挑战。
云数据湖的挑战
基于云的数据湖提供无限的、可扩展的、相对廉价的数据存储。它们由各大云提供商作为托管服务提供,具备高可用性、可扩展性、高效性和较低的总拥有成本。这帮助组织加速数字创新,缩短上市时间。然而,云数据湖作为对象存储,主要是为了解决存储可扩展性的问题而发展起来的。它们并非为了存储高度结构化、强类型的分析数据而设计。因此,使用基于云的数据湖作为分析存储系统存在一些挑战。
数据湖的可靠性挑战
数据湖并不是基于任何底层文件系统,而是基于对象存储机制,将数据作为对象进行管理。对象存储将数据表示为具有唯一标识符及其相关元数据的对象。对象存储并非为管理频繁变化的事务数据而设计,因此在作为分析数据存储和数据处理系统时,存在一些限制,比如最终一致性、缺乏事务性保障等。我们将在接下来的章节中探讨这些问题。
数据的最终一致性
基于云的数据湖是分布式存储系统,数据存储分布在多台机器上,而不是单一机器上。分布式存储系统受到一个称为 CAP 定理的理论的约束。CAP 定理表明,分布式存储系统只能在一致性、可用性和分区容忍性这三者中选择其中的两个来进行调优。不保证强一致性和分区容忍性可能导致数据丢失或错误,因此基于云的数据湖优先保证这两者,以便使其最终一致。
最终一致性意味着写入云数据湖的数据可能不会立即可用。这可能会导致数据分析系统中的FileNotFound错误,尤其是在下游的商业分析过程试图在 ELT 过程写入数据的同时读取数据时。
缺乏事务性保证
一个典型的关系型数据库在数据写入时提供事务性保证。这意味着数据库操作要么完全成功,要么完全失败,并且任何同时尝试读取数据的消费者都不会因为数据库操作失败而读取到不一致或错误的数据。
数据湖不提供任何此类原子事务或持久性保证。这意味着开发人员需要清理并手动回滚任何失败作业中半写入的不完整数据,并重新处理这些数据。
请考虑以下代码片段,我们正在摄取 CSV 数据,将其转换为 Parquet 格式,并将其保存到数据湖中:
(spark
.read
.csv("/FileStore/shared_uploads/online_retail/")
.write
.mode("overwrite")
.format("parquet")
.save("/tmp/retail.parquet")
)
在这里,我们尝试在工作过程中中断任务,以模拟 Spark 作业失败。在浏览/tmp/retail.parquet数据湖时,你会注意到一些半写入的 Parquet 文件。接下来,我们尝试通过另一个 Spark 作业读取这些 Parquet 文件,代码如下所示:
(spark
.read
.format("parquet")
.load("dbfs:/tmp/retail.parquet/part-00006-tid-6775149024880509486-a83d662e-809e-4fb7-beef-208d983f0323-212-1-c000.snappy.parquet")
.count()
)
在前面的代码块中,我们读取了一个 Parquet 文件,它是一个数据摄取作业未完全完成时的结果。当我们尝试在支持原子事务的数据存储上读取这些数据时,预期的结果是查询要么不返回任何结果,要么因为数据不正确而失败。然而,在前述的 Spark 作业中,我们却得到了一些几千条记录,这是错误的。这是因为 Apache Spark 及数据湖缺乏原子事务保障。
缺乏模式强制执行
数据湖作为对象存储,并不关心数据的结构和模式,能够存储任何数据,而不会执行任何检查来确保数据的一致性。Apache Spark 也没有内建的机制来强制执行用户定义的模式。这导致了损坏和不良数据的产生,数据类型不匹配的数据最终进入数据湖。这会降低数据质量,而数据质量对于最终用户的分析应用至关重要。
看一下以下代码示例,我们已经创建了一个包含几列的初始 DataFrame。第一列的数据类型是IntegerType,第二列的数据类型是StringType。我们将第一个 DataFrame 写入数据湖,格式为 Parquet。接着,我们生成了第二个 DataFrame,两个列的数据类型都是IntegerType。然后,我们尝试将第二个 DataFrame 追加到已经存在于数据湖中的原 Parquet 数据集,如下所示:
from pyspark.sql.functions import lit
df1 = spark.range(3).withColumn("customer_id", lit("1"))
(df1
.write
.format("parquet")
.mode("overwrite")
.save("/tmp/customer")
)
df2 = spark.range(2).withColumn("customer_id", lit(2))
(df2
.write
.format("parquet")
.mode("append")
.save("/tmp/customer"))
在强类型分析数据存储(如数据仓库)上,预期的结果应该是数据类型不匹配错误。然而,Apache Spark、数据湖或 Parquet 数据格式本身并不会在我们尝试执行此操作时抛出错误,事务似乎成功完成。这是不可取的,因为我们允许不一致的数据进入数据湖。然而,对 Parquet 数据集执行读取操作时会因类型不匹配而失败,这可能令人困惑且相当难以调试。如果数据湖或 Apache Spark 具备数据验证支持,这个错误本来可以在数据加载过程中就被捕获。在将数据提供给业务分析之前,始终验证数据的正确性和一致性非常重要,因为业务决策者依赖这些数据。
统一批处理和流处理
现代大数据分析系统的一个关键要求是实时访问最新数据和洞察。Apache Spark 提供了结构化流处理功能,能够处理所有实时分析需求。尽管流处理是核心,但批处理依然是大数据分析的一个重要方面,而 Apache Spark 通过其 Spark SQL 引擎将实时和批处理分析统一,Spark SQL 引擎作为批处理和流处理 Spark 作业的核心抽象层,表现得非常好。
然而,数据湖不支持任何级别的原子事务或同一表或数据集上不同事务之间的隔离。因此,像Lambda 架构这样的技术,就需要被用来统一批处理和流处理管道,这个架构你在第二章中学习过,数据摄取。这就导致了需要维护不同的数据处理管道、不同的代码库以及不同的表,一个用于批处理,另一个用于流处理。你大数据分析系统的架构设计和维护非常复杂。
更新和删除数据
在 ELT 方法论中,你是持续地将新数据摄取到数据湖,并在其中维护源交易的副本以及一段时间内的历史记录。操作系统不断生成交易。然而,时不时你需要更新和删除记录。
以一个客户在在线零售商处下单的例子为例。交易经历不同的阶段,从下订单、订单处理中、订单准备发货、订单已发货、订单运输中,到订单已交付。这一交易状态的变化必须在数据湖中得到反映。
捕获数据状态变化的过程被称为数据湖中的 UPDATE 和 DELETE 操作。数据湖是仅附加的系统,并不设计用来处理大量的任意更新和删除。因此,实施任意更新和删除会增加你 ELT 应用程序的复杂性。
回滚错误数据
之前,你了解到数据湖不支持任何关于写操作的原子事务保证。数据工程师需要识别错误记录,清理它们,并在失败的任务中重新处理数据。对于较小的数据集,这个清理过程可能只是简单的截断并重新加载整个数据集。然而,对于大规模数据集,包含数千 TB 数据的情况下,截断并加载数据根本不可行。数据湖和 Apache Spark 都没有便捷的回滚选项,这就要求数据工程师构建复杂的机制来处理失败的任务。
一类新的现代数据存储格式应运而生,旨在克服上一节提到的数据湖挑战。这些技术的一些例子包括 Apache Hudi、Apache Iceberg 和 Delta Lake。在接下来的部分,我们将探索 Delta Lake,并看看它如何帮助克服各种数据湖挑战。
使用 Delta Lake 克服数据湖挑战
在这一部分中,你将了解 Delta Lake,并理解它如何帮助克服数据湖的一些挑战。你还将编写一些代码示例,看看 Delta Lake 如何实际应用。
Delta Lake 简介
Delta Lake 是一个开源的数据存储层,旨在为基于云的数据湖带来可靠性、ACID 事务保证、架构验证和演进。Delta Lake 还帮助统一批处理和流处理。Delta Lake 由 Databricks 创建,Databricks 是 Apache Spark 的原始开发者,且它完全兼容所有 Apache Spark API。
Delta Lake 由一组版本化的 Parquet 文件组成,并配有一个称为 事务日志 的写前日志。Delta 事务日志有助于实现 Delta Lake 的所有功能。让我们深入了解 Delta 事务日志的内部工作原理,以便更好地理解 Delta Lake 的运作方式。
Delta Lake 事务日志
Delta 事务日志基于一种流行的技术,该技术应用于关系型数据库,被称为预写日志(WAL)。这种技术保证了数据库写操作的原子性和持久性。这是通过在数据写入数据库之前,将每个写操作作为事务记录到预写日志中来实现的。Delta 事务日志基于与 WAL 相同的技术,但在这里,WAL 以及已写入的数据存储在数据湖中的文件里。
让我们尝试通过一个简单的 Spark 作业来理解 Delta 事务日志,该作业将 CSV 数据以 Delta 格式导入数据湖,如以下代码块所示:
(spark
.read
.option("header", True)
.option("inferSchema", True)
.csv("/FileStore/shared_uploads/online_retail/")
.write
.format("delta")
.save("/FileStore/shared_uploads/delta/online_retail")
)
上述代码从数据湖读取 CSV 文件,推断底层数据的模式以及表头,将数据转换为 Delta 格式,并将数据保存到数据湖的不同位置。现在,让我们使用以下命令探索数据湖中 Delta 文件的位置:
%fs ls /FileStore/shared_uploads/delta/online_retail
执行上述命令后,您将注意到 Delta 位置的文件夹结构,如下图所示:

图 3.3 – Delta 文件夹结构
在前面的截图中,您可以看到一个 Delta Lake 位置包含两部分:一个名为 _delta_log 的文件夹和一组 Parquet 文件。_delta_log 文件夹包含 Delta 事务日志的文件。我们可以通过以下命令来探索事务日志:
%fs ls dbfs:/FileStore/shared_uploads/delta/online_retail/_delta_log/
上述命令显示了 _delta_log 文件夹的内容,如下图所示:

图 3.4 – Delta 事务日志
在前面的截图中,我们可以看到文件夹中包含几种不同类型的文件。还有一些带有 .json 扩展名的文件。这些 JSON 文件是实际的 Delta 事务日志文件,包含对 Delta 表执行的所有成功事务的有序记录。
注意
之前使用的 %fs 文件系统命令仅适用于 Databricks 平台。您需要使用适合您 Spark 和数据湖分发版的命令来浏览数据湖。
Delta Lake 事务可以是对 Delta 表执行的任何操作,如插入、更新、删除,甚至是元数据操作,如重命名表、修改表架构等。每次操作发生时,Delta 事务日志都会附加一条新记录,记录诸如添加文件、删除文件、更新元数据等操作。这些操作是原子单位,并按发生顺序记录下来,它们被称为提交。
每 10 次提交后,Delta Lake 会生成一个 Parquet 格式的检查点文件,其中包含到该时刻为止的所有事务。这些周期性的 Parquet 检查点文件使得 Spark 作业能够快速、轻松地读取并重建表的状态。以下 Spark 代码可以轻松地说明这一点:
spark.read.json("/FileStore/shared_uploads/delta/online_retail/_delta_log/").show()
在前面的代码行中,我们像读取其他 JSON 文件一样,使用spak.read()函数读取 Delta 事务日志,并创建了一个 Spark 数据帧。每次在 Delta Lake 表上运行spak.read()命令时,都会执行一个小的 Spark 作业来读取表的状态,从而使对 Delta Lake 的元数据操作完全可扩展。
注意
用于在数据湖中浏览文件的%fs文件系统命令仅在 Databricks 平台上可用。你需要为你的 Spark 环境和数据湖选择合适的机制。
现在你已经了解了 Delta Lake 的组件以及 Delta 事务日志的内部工作原理,接下来我们来看一下 Delta Lake 如何帮助解决数据湖面临的挑战。
使用 Delta Lake 提高数据湖的可靠性
Delta Lake 及其事务日志保证了写入数据湖的数据的原子性和持久性。只有当操作的所有数据完全写入数据湖时,Delta Lake 才会将事务提交到事务日志中。任何读取 Delta 表数据的 Delta 感知消费者都会首先解析 Delta 事务日志,以获取 Delta 表的最新状态。
这样,如果数据摄取任务在中途失败,Delta 事务日志感知消费者会解析事务日志,获取表的最后稳定状态,并只读取事务日志中有提交的数据。任何半写入的脏数据(可能存在于数据湖中)都会被完全忽略,因为这些数据在事务日志中没有任何提交。因此,Delta Lake 与其事务日志结合,通过提供事务的原子性和持久性保证,使数据湖更加可靠。
提示
数据读取器和数据写入器需要是Delta 事务日志感知的,才能获得 Delta Lake 的 ACID 事务保证。任何使用 Apache Spark 的读取器或写入器,只需在 Spark 集群中包含适当版本的 Delta Lake 库,就可以完全Delta 事务日志感知。Delta Lake 还具有与外部数据处理系统的连接器,例如 Presto、Athena、Hive、Redshift 和 Snowflake。
启用 Delta Lake 的模式验证
干净且一致的数据是任何商业分析应用程序的基本要求。确保只有干净数据进入数据湖的一个简单方法是确保在数据摄取过程中验证模式。Delta Lake 内置了模式验证机制,确保写入 Delta Lake 的任何数据都符合用户定义的 Delta 表模式。让我们通过创建一个新的 Delta 表并尝试插入数据类型不匹配的数据来探索此功能,如下所示:
from pyspark.sql.functions import lit
df1 = spark.range(3).withColumn("customer_id", lit("1"))
(df1
.write
.format("delta")
.mode("overwrite")
.save("/tmp/delta/customer"))
df2 = spark.range(2).withColumn("customer_id", lit(2))
(df2
.write
.format("delta")
.mode("append")
.save("/tmp/delta/customer"))
在前面的代码片段中,我们创建了一个名为 df1 的 Spark DataFrame,它有两列,且两列的数据类型均为 StringType。我们使用 Delta Lake 格式将此 DataFrame 写入数据湖中。然后,我们创建了另一个名为 df2 的 Spark DataFrame,同样包含两列,但它们的数据类型分别设置为 LongType 和 IntegerType。
接下来,我们尝试将第二个 DataFrame 附加到原始的 Delta 表中。正如预期的那样,Delta Lake 失败了该操作并抛出了 无法合并不兼容的数据类型 StringType 和 IntegerType 异常。通过这种方式,Delta Lake 在数据湖中通过提供模式验证和强制执行,确保数据质量。
Delta Lake 支持模式演变
在数据摄取和 ELT 过程中,另一个常见的用例是源模式可能会随着时间的推移发生变化,并且需要在数据湖中进行处理。一个这样的场景是可能会向源系统表中添加新的列。希望将这些新列引入我们的数据湖表中,而不影响我们已有的数据。这个过程通常被称为 模式演变,而 Delta Lake 已内建对此的支持。让我们通过以下代码示例来探讨 Delta Lake 中的模式演变:
from pyspark.sql.functions import lit
df1 = spark.range(3)
(df1
.write
.format("delta")
.mode("overwrite")
.save("/tmp/delta/customer"))
df2 = spark.range(2).withColumn("customer_id", lit(2))
(df2
.write
.format("delta")
.option("mergeSchema", True)
.mode("append")
.save("/tmp/delta/customer"))
在前面的代码片段中,我们创建了一个名为 df1 的 Spark DataFrame,它只有一个名为 id 的列。然后,我们将此 DataFrame 以 Delta Lake 格式保存到数据湖中。接着,我们创建了第二个名为 df2 的 Spark DataFrame,包含两个名为 id 和 customer_id 的列。之后,我们将第二个 DataFrame 附加到由 df1 创建的原始 Delta 表中。这次,我们使用了 mergeSchema 选项。该 mergeSchema 选项指定我们期望将新列写入 Delta Lake,并需要将这些列附加到现有表中。我们可以通过对 Delta 表运行以下命令轻松验证这一点:
spark.read.format("delta").load("/tmp/delta/customer").show()
在前面的代码块中,我们将 Delta 表中的数据加载到 Spark DataFrame 中,并调用 show() 操作来显示 DataFrame 的内容,如下图所示:

图 3.5 – Delta Lake 模式演变
如你所见,启用新的mergeSchema后,Delta Lake 会自动将新列添加到现有表中,并将之前不存在的行的值标记为null值。
Delta Lake 中的任意更新和删除
事务不仅会被插入到操作系统中——它们还会时常被更新和删除。在 ELT 过程中,源系统数据的副本会被保存在数据湖中。因此,能够不仅将数据插入到数据湖中,还能更新和删除它变得非常必要。然而,数据湖是仅追加的存储系统,几乎没有或完全没有支持任何更新或删除的功能。Delta Lake,然而,完全支持插入、更新和删除记录。
让我们看一个例子,演示如何从 Delta Lake 更新和删除任意数据,如下代码块所示:
from pyspark.sql.functions import lit
df1 = spark.range(5).withColumn("customer_id", lit(2))
df1.write.format("delta").mode("overwrite").save("/tmp/df1")
在前面的代码块中,我们创建了一个 Spark DataFrame,包含两个列:id 和 customer_id。id 的值从 1 到 5。我们使用 Delta Lake 格式将此表保存到数据湖中。现在,让我们更新id列大于2的customer_id列,如下代码块所示:
%sql
UPDATE delta.`/tmp/df1` SET customer_id = 5 WHERE id > 2;
SELECT * FROM delta.`/tmp/df1`;
在前面的代码块中,我们使用UPDATE SQL 子句更新了customer_id列,并通过WHERE子句指定了条件,就像你在任何关系型数据库管理系统(RDBMS)中操作一样。
提示
%sql 魔法命令指定我们打算在当前笔记本单元格中执行 SQL 查询。尽管我们没有明确创建表,但我们仍然可以使用delta.`path-to-delta-table`语法将 Delta Lake 位置视为一个表来引用。
第二个 SQL 查询从 Delta 表中读取数据,并使用SELECT SQL 子句显示出来,如下图所示:

图 3.6 – 使用 Delta Lake 进行更新
在这里,我们可以验证所有id列值大于2的 Delta 表中的行都已成功更新。因此,Delta Lake 完全支持使用简单的类似 SQL 的语法,在大规模上更新多个任意记录。
提示
Delta 表的元数据完全存储在 Delta 事务日志中。这使得将 Delta 表注册到外部元存储(如Hive)成为完全可选的。这样,直接将 Delta 表保存到数据湖中,并通过 Spark 的 DataFrame 和 SQL API 无缝使用变得更加容易。
现在,让我们看一下 Delta 如何通过以下代码块来支持删除:
%sql
DELETE FROM delta.`/tmp/df1` WHERE id = 4;
SELECT * FROM delta.`/tmp/df1`;
在前面的代码片段中,我们使用DELETE命令删除了所有id值为4的记录。第二个查询,我们使用SELECT子句,显示了DELETE操作后的 Delta 表内容,如下图所示:

图 3.7 – 使用 Delta Lake 进行删除
在这里,我们可以轻松验证我们不再拥有任何 id 值为 4 的行。因此,Delta Lake 也支持大规模删除任意记录。
提示
Delta Lake 同时支持 SQL 和 DataFrame 语法来执行 DELETES、UPDATES 和 UPSERTS。有关语法的参考可以在开源文档中找到,文档链接为:docs.delta.io/latest/delta-update.html#table-deletes-updates-and-merges。
即使是 DELETE 和 UPDATE 操作,Delta Lake 也能像写入操作一样支持原子性和持久性的事务保证。然而,值得注意的是,每次执行 DELETE 或 UPDATE 操作时,Delta Lake 并不是直接更新或删除任何数据,而是生成一个包含更新或删除记录的新文件,并将这些新文件附加到现有的 Delta 表中。然后,Delta Lake 在事务日志中为此次写入事务创建一个新的 提交,并将删除或更新记录的旧 提交 标记为无效。
因此,Delta Lake 实际上并没有删除或更新数据湖中的实际数据文件;它只是为每个操作附加新文件并更新事务日志。更新较小的事务日志文件比更新大量非常大的数据文件要快得多,效率也更高。使用 Delta Lake 更新和删除记录的过程非常高效,并且可以扩展到 PB 级数据。这一功能对于需要识别并删除客户任意记录的用例非常有用,例如 GDPR 合规的用例。
这种始终附加数据文件而从不删除文件的技术的另一个有趣副作用是,Delta Lake 保留了所有数据变化的历史审计记录。这个审计日志保存在 Delta 事务日志 中,借助它,Delta Lake 可以回溯到过去,重现某一时刻的 Delta 表快照。我们将在下一节中探讨这个功能。
Delta Lake 的时间旅行与回滚
Delta Lake 在其事务日志中保留数据如何随时间变化的审计日志。每次数据发生变化时,它还会保持旧版本的 Parquet 数据文件。这使得 Delta Lake 能够在某一时刻重现整个 Delta 表的快照。这个功能叫做 时间旅行。
你可以通过以下 SQL 查询轻松浏览 Delta 表的审计记录:
%sql DESCRIBE HISTORY delta.`/tmp/df1`
在前面的 Spark SQL 查询中,我们使用了 DESCRIBE HISTORY 命令来重现 Delta 表上发生的所有变更的审计日志,如下所示:

图 3.8 – Delta Lake 的时间旅行
在前面的截图中,你可以看到这个 Delta 表发生了三次变化。首先,数据被插入到表中,然后表被更新,最后从表中删除了记录。Delta Lake 将所有这些事件记录为称为提交的事务。提交事件的时间戳和版本号也会记录在变更审计日志中。时间戳或表的版本号可以通过 SQL 查询,用于回溯到 Delta 表的特定快照,示例如下:
%sql SELECT * from delta.`/tmp/delta/df1` VERSION AS OF 0
在前面的 SQL 查询中,我们执行了 Delta Time Travel,回到了表的原始版本。Time Travel 在数据工程和 ELT 处理过程中非常有用,可以在数据摄取过程失败时执行回滚。Delta Time Travel 可以用于将 Delta 表恢复到先前的状态,如下所示:
%sql
INSERT OVERWRITE delta.`/tmp/df1`
SELECT * from delta.`/tmp/df1` VERSION AS OF 0
在前面的 SQL 查询中,我们使用来自表的先前版本的快照覆盖了 Delta 表,并充分利用了Delta Time Travel特性。
另一个 Delta Time Travel 很有用的场景是数据科学和机器学习的应用场景。数据科学家通常通过修改用于实验的数据集来进行多个机器学习实验。在这个过程中,他们最终会维护多个物理版本的相同数据集或表。Delta Lake 可以通过 Time Travel 帮助消除这些物理版本的表,因为 Delta 内置了数据版本管理。你将在第九章《机器学习生命周期管理》中更详细地探讨这种技术。
提示
Delta 会在每次修改数据的操作中保持 Parquet 数据文件的版本。这意味着旧版本的数据文件会不断积累,且 Delta Lake 不会自动删除它们。这可能会导致数据湖的大小随着时间的推移显著增加。为了解决这一问题,Delta Lake 提供了 VACUUM 命令来永久删除不再被 Delta 表引用的旧文件。有关 VACUUM 命令的更多信息,请参见 docs.delta.io/latest/delta-utility.html#vacuum。
使用 Delta Lake 统一批处理和流处理
批处理和实时流处理是任何现代大数据架构中的关键组件。在第二章《数据摄取》中,你学习了如何使用 Apache Spark 进行批处理和实时数据摄取。你还学习了 Lambda 架构,利用它可以实现同时的批处理和流处理。使用 Apache Spark 实现 Lambda 架构仍然相对复杂,因为需要为批处理和实时处理分别实现两个独立的数据处理管道。
这种复杂性来源于数据湖的局限性,因为它们本质上不提供任何写操作的事务性、原子性或持久性保障。因此,批处理和流处理无法将数据写入数据湖的同一个表或位置。由于 Delta Lake 已经解决了数据湖面临的这一挑战,可以将单一的 Delta Lake 与多个批处理和实时管道结合使用,从而进一步简化 Lambda 架构。你将在第四章中进一步探讨这一点,实时数据分析。
总结来说,在本节中,你学到了数据湖在支持真正可扩展的大数据处理系统中的重要作用。然而,它们并非为数据分析存储系统而构建,存在一些不足之处,例如缺乏 ACID 事务保障,以及无法支持更新或删除记录、保持数据质量的模式执行或批处理与流处理的统一。你还学到了现代数据存储层(如 Delta Lake)如何帮助克服数据湖的挑战,并使其更接近真正的数据分析存储系统。
现在,既然你已经了解了如何让基于云的数据湖更加可靠并适合数据分析,你已经准备好学习将原始事务数据转化为有意义的商业洞察的过程。我们将从整合来自不同来源的数据并创建统一的单一视图开始。
使用数据集成进行数据整合
数据集成是 ETL 和 ELT 数据处理模式中的一个重要步骤。数据集成是将来自不同数据源的数据进行组合和融合,生成代表单一事实版本的丰富数据的过程。数据集成不同于数据摄取,因为数据摄取只是将数据从不同来源收集并带到一个中心位置,例如数据仓库。另一方面,数据集成将这些不同的数据源结合起来,创建一个有意义的统一版本的数据,代表数据的所有维度。数据集成有多种实现方式,本节将探讨其中的一些。
通过 ETL 和数据仓库进行数据整合
提取、转换和加载数据到数据仓库是过去几十年来数据集成的最佳技术之一。数据整合的主要目标之一是减少数据存储位置的数量。ETL 过程从各种源系统中提取数据,然后根据用户指定的业务规则对数据进行合并、过滤、清洗和转换,最后将其加载到中心数据仓库。
通过 ETL 和数据仓库技术以及专门为此构建的工具和技术支持数据整合和数据集成。虽然 ELT 过程与 ETL 略有不同,并且使用 Apache Spark,我们打算构建一个数据湖,但数据集成和数据整合的技术仍然保持相同,即使是 ETL 也是如此。
让我们使用 PySpark 实现数据整合过程。作为第一步,将本章提供的所有数据集上传到可以被您的 Spark 集群访问的位置。在 Databricks Community Edition 的情况下,可以直接从笔记本的File菜单中将数据集上传到数据湖中。数据集和代码文件的链接可以在本章开头的Technical requirements部分找到。
让我们使用以下代码块探索标记为online_retail.csv和online_retail_II.csv的两个交易数据集的架构信息:
from pyspark.sql.types import StructType, StructField, IntegerType, TimestampType, StringType, DoubleType
schema = (StructType()
.add("InvoiceNo", StringType(), True)
.add("StockCode", StringType(), True)
.add("Description", StringType(), True)
.add("Quantity", StringType(), True)
.add("InvoiceDate", StringType(), True)
.add("UnitPrice", StringType(), True)
.add("CustomerID", StringType(), True)
.add("Country", StringType(), True))
df1 = spark.read.schema(schema).option("header", True).csv("dbfs:/FileStore/shared_uploads/online_retail/online_retail.csv")
df2 = spark.read.schema(schema).option("header", True).csv("dbfs:/FileStore/shared_uploads/online_retail/online_retail_II.csv")
df1.printSchema()
df2.printSchema()
在前面的代码片段中,我们执行了以下操作:
-
我们将 Spark DataFrame 的架构定义为由多个 StructField 组成的
StructType。PySpark 提供了这些内置结构来编程地定义 DataFrame 的架构。 -
然后,我们将两个 CSV 文件加载到单独的 Spark DataFrames 中,同时使用
schema选项指定我们在Step 1中创建的数据模式。我们仍然将头部选项设置为True,因为 CSV 文件的第一行有一个定义好的标题,我们需要忽略它。 -
最后,我们打印了在Step 2中创建的两个 Spark DataFrames 的架构信息。
现在我们已经将来自 CSV 文件的零售数据集加载到 Spark DataFrames 中,让我们将它们整合成一个单一数据集,如以下代码所示:
retail_df = df1.union(df2)
retail_df.show()
在前述代码中,我们简单地使用union()函数将包含在线零售交易数据的两个 Spark DataFrames 组合成一个单一的 Spark DataFrame。联合操作将这两个不同的 DataFrame 合并成一个 DataFrame。合并后的数据集被标记为retail_df。我们可以使用show()函数验证结果。
提示
union()函数是一种转换操作,因此它是延迟评估的。这意味着当您在两个 Spark DataFrames 上调用union()时,Spark 会检查这两个 DataFrame 是否具有相同数量的列,并且它们的数据类型是否匹配。它不会立即将 DataFrame 映射到内存中。show()函数是一个动作操作,因此 Spark 会处理转换并将数据映射到内存中。然而,show()函数仅在 DataFrame 的少量分区上工作,并返回一组样本结果给 Spark Driver。因此,这个动作帮助我们快速验证我们的代码。
接下来,我们有一些描述国家代码和名称的数据存储在country_codes.csv文件中。让我们使用以下代码块将其与前一步中创建的retail_df DataFrame 集成:
df3 = spark.read.option("header", True).option("delimiter", ";").csv("/FileStore/shared_uploads/countries_codes.csv")
country_df = (df3
.withColumnRenamed("OFFICIAL LANG CODE", "CountryCode")
.withColumnRenamed("ISO2 CODE", "ISO2Code")
.withColumnRenamed("ISO3 CODE", "ISO3Code")
.withColumnRenamed("LABEL EN", "CountryName")
.withColumnRenamed("Geo Shape", "GeoShape")
.drop("ONU CODE")
.drop("IS ILOMEMBER")
.drop("IS RECEIVING QUEST")
.drop("LABEL FR")
.drop("LABEL SP")
.drop("geo_point_2d")
)
integrated_df = retail_df.join(country_df, retail_df.Country == country_df.CountryName, "left_outer")
在前面的代码片段中,我们做了以下操作:
-
我们将
country_codes.csv文件加载到一个 Spark 数据框中,并将header选项设置为True,文件分隔符设置为";"。 -
我们重命名了一些列名,以遵循标准命名约定,使用了
withColumnRenamed()函数。我们删除了几个我们认为对任何业务用例都不必要的列。这导致生成了一个名为country_df的数据框,其中包含了国家代码和其他描述性列。 -
然后,我们将这个数据框与之前步骤中的
retail_df数据框进行了连接。我们使用的是retail_df数据框,无论它们是否在country_df数据框中有匹配记录。 -
结果生成的
integrated_df数据框包含了来自country_codes.csv数据集的描述性列,并对在线零售交易数据进行了增强。
我们还有一个名为adult.data的数据集,其中包含了来自美国人口普查的收入数据集。我们将这个数据集与已经集成和增强的零售交易数据集进行集成,代码如下所示:
from pyspark.sql.functions import monotonically_increasing_id
income_df = spark.read.schema(schema).csv("/FileStore/shared_uploads/adult.data").withColumn("idx", monotonically_increasing_id())
retail_dfx = retail_df.withColumn("CustomerIDx", monotonically_increasing_id())
income_dfx = income_df.withColumn("CustomerIDx", monotonically_increasing_id())
income_df = spark.read.schema(schema).csv("/FileStore/shared_uploads/adult.data").withColumn("idx", monotonically_increasing_id())
retail_dfx = integrated_df.withColumn("RetailIDx", monotonically_increasing_id())
income_dfx = income_df.withColumn("IncomeIDx", monotonically_increasing_id())
retail_enriched_df = retail_dfx.join(income_dfx, retail_dfx.RetailIDx == income_dfx.IncomeIDx, "left_outer")
在前面的代码片段中,我们做了以下操作:
-
我们使用
csv()函数从收入数据集中创建了一个 Spark 数据框。该文件是逗号分隔的,并且有一个头部,因此我们使用了适当的选项。最终,我们得到了一个名为income_df的数据框,包含了与消费者人口统计和收入水平相关的一些列。 -
然后,我们添加了两个
income_df和integrated_df数据框,以便可以进行连接。我们使用了monotonically_increasing_id()函数,它生成唯一的递增数字。 -
然后,两个数据框基于新生成的
integrated_df数据框进行了连接,无论它们是否在income_df数据框中有对应的匹配行。结果是集成的、增强的零售交易数据,包含了国家、客户人口统计信息和收入信息,所有数据都统一在一个数据集中。
这个中间数据集对于执行retail_enriched.delta非常有用,下面的代码展示了如何使用它:
(retail_enriched_df
.coalesce(1)
.write
.format("delta", True)
.mode("overwrite")
.save("/FileStore/shared_uploads/retail.delta"))
在前面的代码块中,我们使用coalesce()函数将retailed_enriched_df数据框的分区数量减少到一个分区。这样就生成了一个单一的可移植 Parquet 文件。
注意
学习和实验大数据分析的最大挑战之一是找到干净且有用的数据集。在前面的代码示例中,我们必须引入一个代理键来连接两个独立的数据集。在实际应用中,除非数据集之间相关且存在公共连接键,否则你永远不会强行连接数据集。
因此,使用 Spark 的数据框操作或 Spark SQL,你可以从不同来源集成数据,创建一个增强的、有意义的数据集,表示单一版本的真实数据。
使用数据虚拟化技术进行数据集成
数据虚拟化,顾名思义,是一种虚拟过程,在该过程中,数据虚拟化层作为所有不同数据源之上的逻辑层。这个虚拟层充当业务用户的通道,使他们能够实时无缝访问所需数据。与传统的ETL和ELT过程相比,数据虚拟化的优势在于它不需要任何数据移动,而是直接向业务用户展示集成的数据视图。当业务用户尝试访问数据时,数据虚拟化层会查询底层数据集并实时获取数据。
数据虚拟化层的优势在于,它完全绕过了数据移动,节省了通常需要投入到这个过程中的时间和资源。它能够实时展示数据,几乎没有延迟,因为它直接从源系统获取数据。
数据虚拟化的缺点是它并不是一种广泛采用的技术,而且提供这项技术的产品价格通常较高。Apache Spark 并不支持纯粹意义上的数据虚拟化。然而,Spark 支持一种称为数据联邦的数据虚拟化技术,您将在下一节中学习到。
通过数据联邦实现数据集成
数据联邦是一种数据虚拟化技术,它使用虚拟数据库(也称为联邦数据库)来提供异构数据源的统一和同质化视图。这里的思路是通过单一的数据处理和元数据层访问任何地方的数据。Apache Spark SQL 引擎支持数据联邦,Spark 的数据源可以用来定义外部数据源,从而在 Spark SQL 中实现无缝访问。使用 Spark SQL 时,可以在单一的 SQL 查询中使用多个数据源,而不需要先合并和转换数据集。
让我们通过一个代码示例来学习如何使用 Spark SQL 实现数据联邦:
%sql
CREATE TABLE mysql_authors IF NOT EXISTS
USING org.apache.spark.sql.jdbc
OPTIONS (
url "jdbc:mysql://localhost:3306/pysparkdb",
dbtable "authors",
user "@@@@@@",
password "######"
);
在前一块代码中,我们创建了一个以 MySQL 为数据源的表。这里,我们使用 Spark 创建的表只是指向 MySQL 中实际表的指针。每次查询这个 Spark 表时,它都会通过 JDBC 连接从底层的 MySQL 表中获取数据。接下来,我们将从 Spark DataFrame 创建另一个表,并将其保存为 CSV 格式,如下所示:
from pyspark.sql.functions import rand, col
authors_df = spark.range(16).withColumn("salary", rand(10)*col("id")*10000)
authors_df.write.format("csv").saveAsTable("author_salary")
在前面的代码块中,我们生成了一个包含 16 行和 2 列的 Spark DataFrame。第一列标记为id,它只是一个递增的数字;第二列标记为salary,它是使用内置的rand()函数生成的随机数。我们将 DataFrame 保存到数据湖中,并使用saveAsTable()函数将其注册到 Spark 内置的 Hive 元数据存储中。现在我们有了两个表,它们分别存在于不同的数据源中。接下来,看看我们如何在 Spark SQL 中通过联邦查询将它们一起使用,如下所示:
%sql
SELECT
m.last_name,
m.first_name,
s.salary
FROM
author_salary s
JOIN mysql_authors m ON m.uid = s.id
ORDER BY s.salary DESC
在之前的 SQL 查询中,我们将 MySQL 表与位于数据湖中的 CSV 表在同一查询中连接,生成了数据的集成视图。这展示了 Apache Spark 的数据联合功能。
提示
某些专门的数据处理引擎纯粹设计为联合数据库,例如 Presto。Presto 是一个分布式的大数据大规模并行处理(MPP)查询引擎,旨在在任何数据上提供非常快速的查询性能。使用 Apache Spark 而不是 Presto 的一个优势是,它支持数据联合,并且能够处理其他用例,如批处理和实时分析、数据科学、机器学习和交互式 SQL 分析,所有这些都由单一的统一引擎支持。这使得用户体验更加无缝。然而,组织在不同用例中采用多种大数据技术也是非常常见的。
总结来说,数据集成是将来自不同数据源的数据进行整合和结合,生成有意义的数据,提供单一版本的真实情况。数据集成围绕着多种技术,包括使用 ETL 或 ELT 技术整合数据以及数据联合。在本节中,您学习了如何利用这些技术通过 Apache Spark 实现数据的集成视图。数据分析旅程的下一步是学习如何通过称为数据清洗的过程来清理混乱和脏数据。
使用数据清洗使原始数据适合分析
原始事务数据可能存在多种不一致性,这些不一致性可能是数据本身固有的,或是在不同数据处理系统之间传输过程中、数据摄取过程中产生的。数据集成过程也可能引入数据不一致性。这是因为数据正在从不同系统中整合,而这些系统有各自的数据表示机制。这些数据并不十分干净,可能包含一些坏记录或损坏的记录,在生成有意义的业务洞察之前,需要通过称为数据清洗的过程进行清理。
数据清洗是数据分析过程的一部分,通过修复不良和损坏的数据、删除重复项,并选择对广泛业务用例有用的数据集来清理数据。当来自不同来源的数据被合并时,可能会出现数据类型的不一致,包括错误标签或冗余数据。因此,数据清洗还包括数据标准化,以便将集成数据提升至企业的标准和惯例。
数据清洗的目标是生成干净、一致、完美的数据,为最终一步的生成有意义和可操作的洞察力做好准备,这一步骤来自原始事务数据。在本节中,您将学习数据清洗过程中的各种步骤。
数据选择以消除冗余
一旦来自不同源的数据被整合,集成数据集中可能会出现冗余项。可能有些字段对于你的业务分析团队来说并不必要。数据清洗的第一步就是识别这些不需要的数据元素并将其移除。
让我们对我们在通过 ETL 和数据仓库进行的数据整合部分中生成的集成数据集进行数据选择。我们首先需要查看表模式,了解有哪些列以及它们的数据类型。我们可以使用以下代码行来做到这一点:
retail_enriched_df.printSchema()
前一行代码的结果显示了所有列,我们可以轻松发现Country和CountryName列是冗余的。数据集中还有一些为了数据集成而引入的列,这些列对后续分析并没有太大用处。让我们清理集成数据集中不需要的冗余列,如下所示的代码块所示:
retail_clean_df = (retail_enriched_df
.drop("Country")
.drop("ISO2Code")
.drop("ISO3Code")
.drop("RetailIDx")
.drop("idx")
.drop("IncomeIDx")
)
在前面的代码片段中,我们使用了drop() DataFrame 操作来删除不需要的列。现在我们已经从集成数据集中选择了正确的数据列,接下来的步骤是识别并消除任何重复的行。
去重数据
去重过程的第一步是检查是否有任何重复的行。我们可以通过组合 DataFrame 操作来做到这一点,如下所示的代码块所示:
(retail_enriched_df
.select("InvoiceNo", "InvoiceDate")
.groupBy("InvoiceNo", "InvoiceDate")
.count()
.show())
前面的代码行显示了在根据InvoiceNo、InvoiceDate和StockCode列对行进行分组后,所有行的计数。在这里,我们假设InvoiceNo、InvoiceDate和StockCode的组合是唯一的,并且它们构成了1。然而,在结果中,我们可以看到一些行的计数大于1,这表明数据集中可能存在重复行。这应该在你抽样检查了一些显示重复的行后手动检查,以确保它们确实是重复的。我们可以通过以下代码块来做到这一点:
(retail_enriched_df.where("InvoiceNo in ('536373', '536382', '536387') AND StockCode in ('85123A', '22798', '21731')")
.display()
)
在前面的查询中,我们检查了InvoiceNo和StockCode值的示例,以查看返回的数据是否包含重复项。通过目视检查结果,我们可以看到数据集中存在重复项。我们需要消除这些重复项。幸运的是,PySpark 提供了一个叫做drop_duplicates()的便捷函数来实现这一点,如下所示的代码行所示:
retail_nodupe = retail_clean_df.drop_duplicates(["InvoiceNo", "InvoiceDate", "StockCode"])
在前一行代码中,我们使用了drop_duplicates()函数,根据一组列来消除重复项。让我们通过以下代码行来检查它是否成功删除了重复行:
(retail_nodupe
.select("InvoiceNo", "InvoiceDate", "StockCode")
.groupBy("InvoiceNo", "InvoiceDate", "StockCode")
.count()
.where("count > 1")
.show())
之前的代码根据复合键对行进行了分组,并检查了每组的计数。结果是一个空数据集,这意味着所有重复项已经成功消除。
到目前为止,我们已经从集成的数据集中删除了不需要的列并消除了重复。在数据选择步骤中,我们注意到所有列的数据类型都是string,且列名称遵循不同的命名惯例。这可以通过数据标准化过程进行修正。
数据标准化
数据标准化是指确保所有列都遵循其适当的数据类型。这也是将所有列名称提升到我们企业命名标准和惯例的地方。可以通过以下 DataFrame 操作在 PySpark 中实现:
retail_final_df = (retail_nodupe.selectExpr(
"InvoiceNo AS invoice_num", "StockCode AS stock_code",
"description AS description", "Quantity AS quantity",
"CAST(InvoiceDate AS TIMESTAMP) AS invoice_date",
"CAST(UnitPrice AS DOUBLE) AS unit_price",
"CustomerID AS customer_id",
"CountryCode AS country_code",
"CountryName AS country_name", "GeoShape AS geo_shape",
"age", "workclass AS work_class",
"fnlwgt AS final_weight", "education",
"CAST('education-num' AS NUMERIC) AS education_num",
"'marital-status' AS marital_status", "occupation",
"relationship", "race", "gender",
"CAST('capital-gain' AS DOUBLE) AS capital_gain",
"CAST('capital-loss' AS DOUBLE) AS capital_loss",
"CAST('hours-per-week' AS DOUBLE) AS hours_per_week",
"'native-country' AS native_country")
)
在前面的代码块中,实际上是一个 SQL SELECT 查询,它将列转换为其适当的数据类型,并为列名称指定别名,以便它们遵循合适的 Python 命名标准。结果是一个最终数据集,包含来自不同来源的数据,已集成成一个清洗、去重和标准化的数据格式。
这个最终的数据集,是数据集成和数据清洗阶段的结果,已经准备好向业务用户展示,供他们进行业务分析。因此,将这个数据集持久化到数据湖并提供给最终用户使用是有意义的,如下所示的代码行所示:
retail_final_df.write.format("delta").save("dbfs:/FileStore/shared_uploads/delta/retail_silver.delta")
在前面的代码行中,我们将最终版本的原始事务数据以 Delta Lake 格式保存到数据湖中。
注意
在业界惯例中,从源系统直接复制的事务数据被称为铜数据,经过清洗和集成的事务数据被称为银数据,而聚合和汇总后的数据被称为金数据。数据分析过程,简而言之,就是一个不断摄取铜数据并将其转化为银数据和金数据的过程,直到它可以转化为可执行的业务洞察。
为了总结数据清洗过程,我们获取了数据集成过程的结果集,移除了任何冗余和不必要的列,消除了重复的行,并将数据列提升到企业标准和惯例。所有这些数据处理步骤都是通过 DataFrame API 实现的,该 API 由 Spark SQL 引擎提供支持。它可以轻松地将此过程扩展到数 TB 甚至 PB 的数据。
提示
在本章中,数据集成和数据清洗被视为两个独立且互相排斥的过程。然而,在实际使用案例中,将这两个步骤作为一个数据处理管道共同实现是非常常见的做法。
数据集成和数据清洗过程的结果是可用的、干净的且有意义的数据,已准备好供业务分析用户使用。由于我们在这里处理的是大数据,因此数据必须以一种提高业务分析查询性能的方式进行结构化和呈现。你将在接下来的章节中了解这一点。
通过数据分区优化 ELT 处理性能
数据分区是一个将大数据集物理拆分为较小部分的过程。这样,当查询需要大数据集的一部分时,它可以扫描并加载分区的子集。这种排除查询不需要的分区的技术被称为分区修剪。
谓词下推是另一种技术,将查询中的一些过滤、切片和切割数据的部分,即谓词,下推到数据存储层。然后,由数据存储层负责过滤掉所有查询不需要的分区。
传统的关系型数据库管理系统(RDBMS)和数据仓库一直都支持数据分区、分区修剪和谓词下推。像 CSV 和 JSON 这样的半结构化文件格式支持数据分区和分区修剪,但不支持谓词下推。Apache Spark 完全支持这三种技术。通过谓词下推,Spark 可以将过滤数据的任务委派给底层数据存储层,从而减少需要加载到 Spark 内存中的数据量,并进一步进行处理。
结构化数据格式如 Parquet、ORC 和 Delta Lake 完全支持分区修剪和谓词下推。这有助于 Spark 的 Catalyst 优化器生成最佳的查询执行计划。这是优先选择像 Apache Parquet 这样结构化文件格式而不是半结构化数据格式的一个有力理由。
假设你的数据湖中包含跨越数年的历史数据,而你的典型查询通常只涉及几个月到几年之间的数据。你可以选择将数据完全不分区,所有数据存储在一个文件夹中。或者,你可以按照年份和月份属性对数据进行分区,如下图所示:

图 3.9 – 数据分区
在前面图示的右侧,我们有未分区的数据。这样的数据存储模式使得数据存储变得稍微简单一些,因为我们只是反复将新数据追加到同一个文件夹中。然而,到了一定程度后,数据会变得难以管理,也使得执行任何更新或删除操作变得困难。此外,Apache Spark 需要将整个数据集读取到内存中,这样会丧失分区修剪和谓词下推可能带来的优势。
在图表的右侧,数据按照年份分区,然后按照月份分区。这使得写入数据稍微复杂一些,因为 Spark 应用程序每次写入数据之前都需要选择正确的分区。然而,与更新、删除以及下游查询带来的效率和性能相比,这只是一个小代价。对这种分区数据的查询将比未分区的数据快几个数量级,因为它们充分利用了分区修剪和谓词下推。因此,推荐使用适当的分区键对数据进行分区,以从数据湖中获得最佳的性能和效率。
由于数据分区在决定下游分析查询性能方面起着至关重要的作用,因此选择合适的分区列非常重要。一般的经验法则是,选择一个基数较低的分区列。至少为一千兆字节的分区大小是实际可行的,通常,基于日期的列是一个很好的分区键候选。
注意
云端对象存储的递归文件列出通常较慢且费用昂贵。因此,在云端对象存储上使用层次分区并不是很高效,因此不推荐使用。当需要多个分区键时,这可能会成为性能瓶颈。Databricks 的专有版本 Delta Lake 以及他们的 Delta Engine 支持动态文件修剪和Z-order多维索引等技术,帮助解决云端数据湖中层次分区的问题。你可以在docs.databricks.com/delta/optimizations/dynamic-file-pruning.html了解更多信息。然而,这些技术目前还没有在 Delta Lake 的开源版本中提供。
总结
在本章中,你了解了两种著名的数据处理方法——ETL和ELT,并看到了使用 ETL 方法能解锁更多分析用例的优势,这些用例是使用 ETL 方法无法实现的。通过这样做,你理解了 ETL 的可扩展存储和计算需求,以及现代云技术如何帮助实现 ELT 的数据处理方式。接着,你了解了将基于云的数据湖作为分析数据存储的不足之处,例如缺乏原子事务和持久性保证。然后,你被介绍到 Delta Lake,这是一种现代数据存储层,旨在克服基于云的数据湖的不足。你学习了数据集成和数据清洗技术,这些技术有助于将来自不同来源的原始事务数据整合起来,生成干净、纯净的数据,这些数据准备好呈现给最终用户以生成有意义的见解。你还学习了如何通过 DataFrame 操作和 Spark SQL 实现本章中使用的每一种技术。你获得了将原始事务数据转换为有意义的、丰富的数据的技能,这些技能对于使用 ELT 方法在大规模大数据中进行处理至关重要。
通常,数据清洗和集成过程是性能密集型的,且以批处理方式实现。然而,在大数据分析中,你必须在事务数据在源头生成后尽快将其传递给最终用户。这对战术决策非常有帮助,并且通过实时数据分析得以实现,实时数据分析将在下一章中讲解。
第四章:实时数据分析
在现代大数据世界中,数据生成的速度非常快,快到过去十年中的任何技术都无法处理,例如批处理 ETL 工具、数据仓库或商业分析系统。因此,实时处理数据并从中提取洞察力对于企业做出战术决策至关重要,以帮助他们保持竞争力。因此,迫切需要能够实时或近实时处理数据的实时分析系统,帮助最终用户尽可能快地获取最新数据。
本章中,您将探索实时大数据分析处理系统的架构和组件,包括作为数据源的消息队列、作为数据汇聚点的 Delta 和作为流处理引擎的 Spark Structured Streaming。您将学习使用有状态处理的 Structured Streaming 处理迟到数据的技巧。还将介绍使用 变更数据捕获(CDC)技术,在数据湖中保持源系统的精确副本。您将学习如何构建多跳流处理管道,逐步改进从原始数据到已清洗和丰富数据的质量,这些数据已准备好进行数据分析。您将掌握使用 Apache Spark 实现可扩展、容错且近实时的分析系统的基本技能。
本章将涵盖以下主要主题:
-
实时分析系统架构
-
流处理引擎
-
实时分析行业应用案例
-
使用 Delta Lake 简化 Lambda 架构
-
CDC
-
多跳流处理管道
技术要求
本章中,您将使用 Databricks Community Edition 来运行您的代码。可以通过以下链接找到:community.cloud.databricks.com:
-
注册说明请参见:
databricks.com/try-databricks。 -
本章中使用的代码和数据可以从以下链接下载:
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter04。
在我们深入探讨如何使用 Apache Spark 实现实时流处理数据管道之前,首先,我们需要了解实时分析管道的一般架构及其各个组件,具体内容将在以下部分中描述。
实时分析系统架构
实时数据分析系统,顾名思义,是实时处理数据的系统。由于数据在源头生成,使其可以以最小的延迟提供给业务用户。它由几个重要组件组成,即流数据源、流处理引擎、流数据汇聚点以及实际的实时数据消费者,如下图所示:

图 4.1 – 实时数据分析
上图展示了一个典型的实时数据分析系统架构。在接下来的部分,我们将更详细地探讨各个组件。
流数据源
类似于其他企业决策支持系统,实时数据分析系统也从数据源开始。企业在实时中持续生成数据;因此,任何被批处理系统使用的数据源也是流数据源。唯一的区别在于你从数据源摄取数据的频率。在批处理模式下,数据是周期性摄取的,而在实时流式系统中,数据是持续不断地从同一数据源摄取的。然而,在持续摄取数据之前,有几个需要注意的事项。这些可以描述如下:
-
数据源能否跟上实时流式分析引擎的需求?否则,流引擎是否会给数据源带来压力?
-
数据源能否异步与流引擎进行通信,并按流引擎要求的任意顺序重放事件?
-
数据源能否以它们在源端发生的精确顺序重放事件?
上述三点提出了关于流数据源的一些重要要求。流数据源应该是分布式且可扩展的,以便跟上实时流式分析系统的需求。需要注意的是,它必须能够以任何任意顺序重放事件。这样,流引擎可以灵活地按任何顺序处理事件,或者在发生故障时重新启动处理。对于某些实时使用场景,如 CDC,必须按事件发生的精确顺序重放事件,以保持数据完整性。
由于前述原因,没有操作系统适合做流数据源。在云和大数据领域,建议使用可扩展、容错且异步的消息队列,如 Apache Kafka、AWS Kinesis、Google Pub/Sub 或 Azure Event Hub。云端数据湖如 AWS S3、Azure Blob 和 ADLS 存储,或者 Google Cloud Storage 在某些使用场景下也适合作为流数据源。
现在我们已经了解了流数据源,让我们来看一下如何以流的方式从数据源(如数据湖)摄取数据,如以下代码片段所示:
stream_df = (spark.readStream
.format("csv")
.option("header", "true")
.schema(eventSchema)
.option("maxFilesPerTrigger", 1)
.load("/FileStore/shared_uploads/online_retail/"))
在之前的代码中,我们定义了一个流式数据框架,该框架一次从数据湖位置读取一个文件。DataStreamReader对象的readStream()方法用于创建流式数据框架。数据格式指定为 CSV,并且使用eventSchema对象定义了模式信息。最后,通过load()函数指定了数据湖中 CSV 文件的位置。maxFilesPerTrigger选项指定流每次只能读取一个文件。这对于控制流处理速率非常有用,尤其是在计算资源有限的情况下。
一旦我们创建了流式数据框架,它可以使用数据框架 API 中的任何可用函数进行进一步处理,并持久化到流式数据接收器中,例如数据湖。我们将在接下来的章节中介绍这一部分内容。
流式数据接收器
一旦数据流从各自的流式数据源中读取并处理完毕,它们需要存储到某种持久存储中,以供下游进一步消费。尽管任何常规的数据接收器都可以作为流式数据接收器,但在选择流式数据接收器时需要考虑许多因素。以下是一些考虑因素:
-
数据消费的延迟要求是什么?
-
消费者将消费数据流中的哪种类型的数据?
延迟是选择流式数据源、数据接收器和实际流式引擎时的重要因素。根据延迟要求,您可能需要选择完全不同的端到端流式架构。根据延迟要求,流式用例可以分为两大类:
-
实时事务系统
-
接近实时的分析系统
实时事务系统
实时事务系统是操作系统,通常关注于一次处理与单个实体或事务相关的事件。让我们考虑一个在线零售业务的例子,其中一个顾客访问了一个电商网站并在某个会话中浏览了几个产品类别。一个操作系统将专注于捕捉该会话的所有事件,并可能实时向该用户展示折扣券或进行特定推荐。在这种场景下,延迟要求是超低的,通常在亚秒级范围内。这类用例需要一个超低延迟的流式引擎,以及一个超低延迟的流式接收器,例如内存数据库,如Redis或Memcached。
另一个实时事务型用例的例子是 CRM 系统,其中客户服务代表试图向在线客户进行追加销售或交叉销售推荐。在这种情况下,流处理引擎需要从数据存储中获取针对特定客户的某些预先计算好的指标,而数据存储包含关于数百万客户的信息。它还需要从 CRM 系统本身获取一些实时数据点,以生成针对该客户的个性化推荐。所有这些操作都需要在几秒钟内完成。一个CustomerID。
重要提示
Spark 的结构化流处理(Structured Streaming)采用微批次的流处理模型,这对于实时流处理用例并不理想,尤其是在需要超低延迟来处理源头发生的事件时。结构化流处理的设计目标是最大化吞吐量和可扩展性,而非追求超低延迟。Apache Flink 或其他为此目的专门设计的流处理引擎更适合实时事务型用例。
现在,您已经了解了实时分析引擎,并且掌握了一个实时分析用例的示例,在接下来的部分,我们将深入探讨一种处理接近实时分析的更突出的、实际可行的方式。
接近实时分析系统
接近实时的分析系统是那些在接近实时的状态下处理大量记录并具有从几秒钟到几分钟的延迟要求的分析系统。这些系统并不关注单个实体或交易的事件处理,而是为一组交易生成指标或关键绩效指标(KPI),以实时展示业务状态。有时,这些系统也可能为单个交易或实体生成事件会话,但供后续离线使用。
由于这种类型的实时分析系统处理的数据量非常庞大,因此吞吐量和可扩展性至关重要。此外,由于处理后的输出要么被输入到商业智能系统中进行实时报告,要么被存储到持久化存储中以供异步消费,数据湖或数据仓库是此类用例的理想数据接收端。在实时分析行业用例部分,详细介绍了接近实时分析的用例。Apache Spark 的设计旨在处理需要最大吞吐量的大量数据的接近实时分析用例,并具有良好的可扩展性。
现在,您已经理解了流数据源、数据接收端以及 Spark 的结构化流处理更适合解决的实时用例,让我们进一步深入了解实际的流处理引擎。
流处理引擎
流处理引擎是任何实时数据分析系统中最关键的组件。流处理引擎的作用是持续处理来自流数据源的事件,并将其摄取到流数据接收端。流处理引擎可以实时处理传入的事件,或者将事件分组为一个小批量,每次处理一个微批量。
以接近实时的方式进行处理。引擎的选择在很大程度上取决于使用案例的类型和处理延迟的要求。现代流处理引擎的一些例子包括 Apache Storm、Apache Spark、Apache Flink 和 Kafka Streams。
Apache Spark 配备了一个流处理引擎,叫做结构化流处理,该引擎基于 Spark 的 SQL 引擎和 DataFrame API。结构化流处理采用微批量处理方式,将每个传入的小批量数据视为一个小的 Spark DataFrame。它对每个微批量应用 DataFrame 操作,就像对待任何其他 Spark DataFrame 一样。结构化流处理的编程模型将输出数据集视为一个无限制的表,并将传入的事件作为连续微批流进行处理。结构化流处理为每个微批生成查询计划,处理它们,然后将其附加到输出数据集,就像处理一个无限制的表一样,具体请参见下图:

图 4.2 – 结构化流处理编程模型
如前面的图所示,结构化流处理将每个传入的小批量数据视为一个小的 Spark DataFrame,并将其附加到现有的流处理 DataFrame 末尾。关于结构化流处理编程模型的详细解释,并附带示例,已在第二章的实时数据摄取部分中介绍。
结构化流处理可以简单地处理传入的小批量流事件,并将输出持久化到流数据接收端。然而,在实际应用中,由于数据延迟到达,流处理的简单模型可能不太实用。结构化流处理还支持有状态的处理模型,以应对延迟到达或无序的数据。关于如何处理延迟到达数据,您将在处理延迟到达数据部分中学习更多内容。
实时数据消费者
实时数据分析系统的最终组件是实际的数据消费者。数据消费者可以是通过临时的 Spark SQL 查询、交互式操作仪表板或其他系统来消费实时数据的实际业务用户,这些系统会接收流引擎的输出并进一步处理。实时业务仪表板由业务用户使用,通常这些仪表板对延迟的要求较高,因为人类大脑只能在一定的速率下理解数据。结构化流处理(Structured Streaming)非常适合这些用例,可以将流输出写入数据库,并进一步将其提供给商业智能系统。
流引擎的输出还可以被其他业务应用程序消费,例如移动应用或 Web 应用。在这种情况下,使用场景可能是超个性化的用户推荐,其中流引擎的处理输出可以进一步传递给在线推理引擎,用于生成个性化的用户推荐。只要延迟要求在几秒钟到几分钟的范围内,结构化流处理也可以用于这些用例。
总结来说,实时数据分析包含几个重要的组件,比如流数据源和数据接收端、实际的流处理引擎以及最终的实时数据消费者。在架构中,数据源、数据接收端和实际引擎的选择依赖于你的实际实时数据消费者、要解决的用例、处理延迟以及吞吐量要求。接下来,在以下章节中,我们将通过一些现实世界的行业用例来了解如何利用实时数据分析。
实时数据分析行业用例
实时处理数据确实有需求,并且具有优势,因此公司正在迅速从批处理转向实时数据处理。在本节中,我们将通过行业垂直的几个示例来了解实时数据分析。
制造业中的实时预测分析
随着物联网(IoT)的到来,制造业及其他行业从其机器和重型设备中产生了大量的物联网数据。这些数据可以通过几种不同的方式来提升行业的工作方式,帮助它们节省成本。一个这样的例子是预测性维护,其中物联网数据不断从工业设备和机械中获取,应用数据科学和机器学习技术对数据进行分析,以识别可以预测设备或部件故障的模式。当这一过程在实时情况下执行时,可以在故障发生之前预测设备和部件的故障。通过这种方式,可以主动进行维护,防止停机,从而避免任何损失的收入或错过的生产目标。
另一个例子是建筑行业,其中 IoT 数据(如设备正常运行时间、燃料消耗等)可以被分析,以识别任何使用不足的设备,并实时调整设备以实现最佳利用率。
汽车行业中的联网车辆
现代车辆配备了大量的联网功能,显著提高了消费者的生活便利性。车辆遥感技术,以及由这些车辆生成的用户数据,可用于多种应用场景,或进一步为终端用户提供便利功能,如实时个性化车载内容和服务、先进的导航与路线指引以及远程监控。制造商可以利用遥感数据解锁诸如预测车辆维修时间窗或零部件故障,并主动提醒附属供应商和经销商等应用场景。预测零部件故障并更好地管理车辆召回,能帮助汽车制造商节省巨额成本。
财务欺诈检测
现代个人财务正迅速从传统的物理方式转向数字化,并由此带来了诸如欺诈和身份盗窃等数字金融威胁。因此,金融机构需要主动评估数百万笔交易的实时欺诈行为,并向个人消费者发出警告并保护其免受此类欺诈。为了在如此大规模下检测和防止金融欺诈,要求具备高度可扩展、容错性强的实时分析系统。
IT 安全威胁检测
消费电子产品制造商和在线联网设备的公司必须不断监控其终端用户设备中的任何恶意活动,以保障用户身份和资产的安全。监控 PB 级别的数据需要实时分析系统,能够每秒处理数百万条记录。
根据前述的行业应用案例,你可能会注意到实时数据分析日益显得尤为重要。然而,实时数据分析系统并不一定意味着可以完全代替批处理数据的需求。批处理依然是非常必要的,特别是在用静态数据丰富实时数据流、生成为实时数据提供上下文的查找表,以及为实时数据科学和机器学习应用场景生成特征方面。在第二章,《数据摄取》中,你学习了一个可以高效统一批处理和实时处理的架构,称为Lambda 架构。在接下来的部分,你将学习如何结合 Delta Lake 使用结构化流处理进一步简化 Lambda 架构。
使用 Delta Lake 简化 Lambda 架构
一个典型的 Lambda 架构有三个主要组件:批处理层、流处理层和服务层。在 第二章,数据摄取 中,你已经查看了使用 Apache Spark 的统一数据处理框架来实现 Lambda 架构的例子。Spark DataFrames API、Structured Streaming 和 SQL 引擎有助于简化 Lambda 架构。然而,仍然需要多个数据存储层来分别处理批量数据和流数据。这些单独的数据存储层可以通过使用 Spark SQL 引擎作为服务层轻松合并。但这可能仍然会导致数据的多重副本,并且可能需要通过额外的批处理作业进一步整合数据,以便为用户呈现一个一致的集成视图。这个问题可以通过将 Delta Lake 作为 Lambda 架构的持久数据存储层来解决。
由于 Delta Lake 内建了 ACID 事务和写操作隔离特性,它能够提供批量数据和流数据的无缝统一,从而进一步简化 Lambda 架构。如下图所示:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ess-pyspk-scl-da/img/B16736_04_03.jpg)
图 4.3 – 带有 Apache Spark 和 Delta Lake 的 Lambda 架构
在前面的图中,展示了一个简化的 Lambda 架构。在这里,批量数据和流数据分别通过 Apache Spark 的批处理和 Structured Streaming 进行同时处理。将批量数据和流数据同时注入到一个 Delta Lake 表中,大大简化了 Lambda 架构。一旦数据被注入到 Delta Lake 中,它便可以立即用于进一步的下游用例,如通过 Spark SQL 查询进行的临时数据探索、近实时的商业智能报告和仪表盘,以及数据科学和机器学习的用例。由于处理后的数据是持续流入 Delta Lake 的,因此可以以流式和批量方式进行消费:
-
让我们看看如何使用 Apache Spark 和 Delta Lake 来实现这个简化的 Lambda 架构,如以下代码块所示:
retail_batch_df = (spark .read .option("header", "true") .option("inferSchema", "true") .csv("/FileStore/shared_uploads/online_retail/online_retail.csv"))在前面的代码片段中,我们通过使用
read()函数从数据湖中读取存储的 CSV 文件来创建一个 Spark DataFrame。我们指定选项,从半结构化的 CSV 文件中推断出头部和模式。结果是一个名为retail_batch_df的 Spark DataFrame,它指向存储在 CSV 文件中的零售数据的内容和结构。 -
现在,让我们将这些 CSV 数据转换为 Delta Lake 格式,并将其作为 Delta 表存储在数据湖中,如以下代码块所示:
(retail_batch_df .write .mode("overwrite") .format("delta") .option("path", "/tmp/data-lake/online_retail.delta") .saveAsTable("online_retail"))在前面的代码片段中,我们使用
write()函数和saveAsTable()函数将retail_batch_dfSpark DataFrame 保存为数据湖中的 Delta 表。格式指定为delta,并通过path选项指定表的位置。结果是一个名为online_retail的 Delta 表,其数据以 Delta Lake 格式存储在数据湖中。提示
当一个 Spark DataFrame 作为表保存时,指定了位置,则该表被称为外部表。作为最佳实践,建议始终创建外部表,因为即使删除表定义,外部表的数据仍然会被保留。
在前面的代码块中,我们使用 Spark 的批处理进行了数据的初始加载:
-
现在,让我们使用 Spark 的结构化流处理将一些增量数据加载到之前定义的同一个 Delta 表中,名为
online_retail。这一过程在以下代码块中有所展示:retail_stream_df = (spark .readStream .schema(retailSchema) .csv("/FileStore/shared_uploads/online_retail/"))在前面的代码片段中,我们使用
readStream()函数以流的方式读取存储在数据湖中的一组 CSV 文件。结构化流处理要求在读取数据时必须提前指定数据的模式,这可以通过schema选项来提供。结果是一个名为retail_stream_df的结构化流处理 DataFrame。 -
现在,让我们将这一数据流注入到之前在初始加载时创建的同一个 Delta 表中,名为
online_retail。这个过程展示在以下代码块中:(retail_stream_df .writeStream .outputMode("append") .format("delta") .option("checkpointLocation", "/tmp/data-lake/online_retail.delta/") .start("/tmp/data-lake/online_retail.delta"))在前面的代码块中,结构化流处理的
retail_stream_dfDataFrame 被加载到名为online_retail的现有 Delta 表中,使用的是结构化流的writeStream()函数。outputMode选项指定为append。这是因为我们希望将新数据持续追加到现有的 Delta 表中。由于结构化流处理保证必须指定checkpointLocation,以便在发生故障或流处理重新启动时,能够跟踪处理数据的进度,并从中断点精确恢复。注意
Delta 表将所有必需的模式信息存储在 Delta 事务日志中。这使得将 Delta 表注册到元数据存储(metastore)成为完全可选的,只有在通过外部工具或 Spark SQL 访问 Delta 表时,才需要进行注册。
从之前的代码块中,你可以看到,Spark 的统一批处理和流处理的结合,已经通过使用单一的统一分析引擎简化了 Lambda 架构。随着 Delta Lake 事务和隔离特性以及批处理和流处理统一性的加入,你的 Lambda 架构可以进一步简化,提供一个强大且可扩展的平台,让你能够在几秒钟到几分钟内访问最新的数据。一个流数据摄取的显著用例是,在数据湖中维护源事务系统数据的副本。该副本应包括源系统中发生的所有删除、更新和插入操作。通常,这个用例被称为 CDC,并遵循类似本节描述的模式。在接下来的部分,我们将深入探讨如何使用 Apache Spark 和 Delta Lake 实现 CDC。
数据变更捕捉(Change Data Capture)
一般来说,操作系统不会长期保留历史数据。因此,必须在数据湖中维护事务系统数据的精确副本,并保留其历史记录。这有几个优点,包括为你提供所有事务数据的历史审计日志。此外,这一大量的数据可以帮助你解锁新的商业用例和数据模式,推动业务迈向更高的水平。
在数据湖中维护事务系统的精确副本意味着捕获源系统中发生的每一笔交易的所有变更,并将其复制到数据湖中。这个过程通常被称为 CDC。CDC 不仅要求你捕获所有的新交易并将其追加到数据湖中,还要捕获源系统中对交易的任何删除或更新。这在数据湖中并非易事,因为数据湖通常不支持更新或删除任意记录。然而,通过 Delta Lake 完全支持插入、更新和删除任意数量的记录,CDC 在数据湖上成为可能。此外,Apache Spark 和 Delta Lake 的结合使得架构变得简单。
让我们实现一个使用 Apache Spark 和 Delta Lake 的 CDC 过程,如下一个代码块所示:
(spark
.read
.option("header", "true")
.option("inferSchema", "true")
.csv("/FileStore/shared_uploads/online_retail/online_retail.csv")
.write
.mode("overwrite")
.format("delta")
.option("path", "/tmp/data-lake/online_retail.delta")
.saveAsTable("online_retail"))
在前面的代码片段中,我们使用 Spark 的批处理处理初始加载一组静态数据到 Delta 表中。我们简单地使用 Spark DataFrame 的 read() 函数读取一组静态的 CSV 文件,并使用 saveAsTable() 函数将其保存到 Delta 表中。这里,我们使用 path 选项将表定义为外部表。结果是一个包含源表初始静态数据的 Delta 表。
这里的问题是,如何将来自操作系统(通常是关系型数据库管理系统 RDBMS)的事务数据最终转化为数据湖中的一组文本文件?答案是使用专门的工具集,这些工具专门用于从操作系统读取 CDC 数据,并将其转换并暂存到数据湖、消息队列或其他数据库中。像 Oracle 的 Golden Gate 和 AWS 数据库迁移服务就是此类 CDC 工具的一些示例。
注意
Apache Spark 可以处理 CDC 数据并将其无缝地导入 Delta Lake;然而,它不适合构建端到端的 CDC 流水线,包括从操作源加载数据。专门为此目的构建的开源和专有工具,如 StreamSets、Fivetran、Apache Nifi 等,可以帮助完成这一工作。
现在,我们已经将一组静态的事务数据加载到 Delta 表中,让我们将一些实时数据加载到同一个 Delta 表中,代码如下所示:
retail_stream_df = (spark
.readStream
.schema(retailSchema)
.csv("/FileStore/shared_uploads/online_retail/"))
在前面的代码片段中,我们从数据湖中的一个位置定义了一个流式 DataFrame。这里的假设是一个第三方 CDC 工具正在不断地将包含最新事务数据的新文件添加到数据湖中的该位置。
现在,我们可以将变更数据合并到现有的 Delta 表中,如下代码所示:
from delta.tables import *
deltaTable = DeltaTable.forPath(spark, "/tmp/data-lake/online_retail.delta")
def upsertToDelta(microBatchOutputDF, batchId):
deltaTable.alias("a").merge(
microBatchOutputDF.dropDuplicates(["InvoiceNo", "InvoiceDate"]).alias("b"),
"a.InvoiceNo = b.InvoiceNo and a.InvoiceDate = b.InvoiceDate") \
.whenMatchedUpdateAll() \
.whenNotMatchedInsertAll() \
.execute()
在前面的代码块中,发生了以下操作:
-
我们使用 Delta Lake 位置和
DeltaTable.forPath()函数重新定义现有 Delta 表的定义。结果是指向 Spark 内存中 Delta 表的指针,命名为deltaTable。 -
然后,我们定义了一个名为
upsertToDelta()的函数,它执行实际的merge或upsert操作,将数据合并到现有的 Delta 表中。 -
现有的 Delta 表被使用字母
a作为别名,包含来自每个流式微批的最新更新的 Spark DataFrame 被别名为字母b。 -
从流式微批处理传入的更新可能实际上包含重复数据。重复的原因是,在数据到达结构化流处理(Structured Streaming)时,某个给定的事务可能已经经历了多次更新。因此,在将数据合并到 Delta 表之前,需要对流式微批处理数据进行去重。通过在流式微批处理 DataFrame 上应用
dropDuplicates()函数来实现去重。 -
然后,流式更新通过在现有 Delta 表上应用
merge()函数将其合并到 Delta 表中。对两个 DataFrame 的关键列应用相等条件,并使用whenMatchedUpdateAll()函数将所有与流式微批更新匹配的记录更新到现有的 Delta 表中。 -
来自流式微批处理的任何记录,如果尚未存在于目标 Delta 表中,将通过
whenNotMatchedInsertAll()函数进行插入。注意
需要对以微批次形式到达的流式更新进行去重,因为在我们的流处理任务实际处理数据时,某个事务可能已经经历了多次更新。业界的常见做法是基于键列和最新时间戳选择每个事务的最新更新。如果源表中没有这样的时间戳列,大多数 CDC 工具具备扫描记录的功能,按创建或更新的正确顺序插入它们自己的时间戳列。
通过使用一个简单的merge()函数,可以轻松地将变更数据合并到存储在任何数据湖中的现有 Delta 表中。这一功能大大简化了实时分析系统中实现 CDC 场景的架构复杂性。
重要提示
对于 CDC 场景,确保事件按其在源端创建的准确顺序到达至关重要。例如,删除操作不能在插入操作之前执行,否则会导致数据错误。某些消息队列无法保持事件到达队列时的顺序,因此在处理时应特别注意保持事件的顺序。
在幕后,Spark 会自动扩展合并过程,使其能够处理 PB 级别的数据。通过这种方式,Delta Lake 将类似数据仓库的功能引入到本来并未设计用于处理分析类用例的基于云的数据湖中。
提示
随着目标 Delta 表中数据量的增加,Delta 合并可能会逐渐变慢。通过使用合适的数据分区方案,并在合并子句中指定数据分区列,可以提高 Delta 合并的性能。这样,Delta 合并只会选择那些确实需要更新的分区,从而大大提高合并性能。
另一个在实时流分析场景中独特的现象是延迟到达的数据。当某个事件或事件更新比预期稍晚到达流处理引擎时,就称为延迟到达的数据。一个强大的流处理引擎需要能够处理延迟到达的数据或乱序到达的数据。在接下来的部分,我们将更详细地探讨如何处理延迟到达的数据。
处理延迟到达的数据
延迟到达的数据是实时流分析中的一种特殊情况,其中与同一事务相关的事件未能及时到达以便一起处理,或者它们在处理时是乱序到达的。结构化流处理支持有状态流处理来处理此类场景。我们接下来将进一步探讨这些概念。
使用窗口和水印的有状态流处理
假设我们考虑一个在线零售交易的例子,用户正在浏览电子零售商网站。我们希望根据以下两种事件之一计算用户会话:用户退出电子零售商门户或发生超时。另一个例子是用户下订单后又更新订单,由于网络或其他延迟,我们首先接收到更新事件,然后才接收到原始订单创建事件。在这种情况下,我们希望等待接收任何迟到或乱序的数据,然后再将数据保存到最终存储位置。
在前面提到的两种场景中,流引擎需要能够存储和管理与每个事务相关的某些状态信息,以便处理迟到的数据。Spark 的结构化流处理可以通过使用窗口化概念实现有状态处理,从而自动处理迟到的数据。
在深入探讨结构化流处理中的窗口化概念之前,您需要理解事件时间(event time)的概念。事件时间是指事务事件在源端生成时的时间戳。例如,订单创建事件的事件时间就是订单下单的时间戳。同样,如果同一事务在源端进行了更新,则更新的时间戳成为该事务更新事件的事件时间。事件时间是任何有状态处理引擎中的一个重要参数,用于确定哪个事件先发生。
使用窗口化(windowing)时,结构化流处理(Structured Streaming)会为每个键维护一个状态,并在相同键的新的事件到达时更新该键的状态,如下图所示:

图 4.4 – 有状态流处理
在上面的示意图中,我们有一个订单放置的事务事件流。O1、O2和O3分别表示订单号,而T、T+03等则表示订单创建的时间戳。输入流有一个稳定的订单相关事件生成流。我们定义了一个持续10分钟的有状态窗口,并且每5分钟滑动一次窗口。我们在窗口中想要实现的是更新每个唯一订单的计数。如你所见,在每个5分钟的间隔内,同一订单的任何新事件都会更新计数。这个简单的示意图描述了有状态处理在流处理场景中的工作原理。
然而,这种类型的状态处理有一个问题;即状态似乎被永久维护,随着时间的推移,状态数据本身可能变得过大,无法适应集群内存。永久维护状态也不现实,因为实际场景中很少需要长期维护状态。因此,我们需要一种机制来在一定时间后使状态过期。结构化流处理具有定义水印的能力,水印控制每个键的状态维护时间,一旦水印过期,系统将删除该键的状态。
注意
尽管定义了水印,状态可能仍然会变得非常大,并且结构化流处理有能力在需要时将状态数据溢出到执行器的本地磁盘。结构化流处理还可以配置使用外部状态存储,例如 RocksDB,以维护数百万个键的状态数据。
以下代码块展示了使用 Spark 的结构化流式处理,通过事件时间、窗口函数和水印函数进行任意状态处理的实现细节:
-
让我们通过将
InvoiceDate列从StringType转换为TimestampType来实现InvoiceTime的概念。 -
接下来,我们将在
raw_stream_df流式数据框上执行一些状态处理操作,通过在其上定义窗口函数和水印函数,如下所示的代码块:aggregated_df = ( raw_stream_df.withWatermark("InvoiceTime", "1 minutes") .groupBy("InvoiceNo", window("InvoiceDate", "30 seconds", "10 seconds", "0 seconds")) .agg(max("InvoiceDate").alias("event_time"), count("InvoiceNo").alias("order_count")) )从前面的代码片段可以得出以下观察结论:
-
我们在
raw_stream_df流式数据框上定义了一个水印,持续时间为1分钟。这意味着结构化流处理应当为每个键维护一个状态,仅持续1分钟。水印的持续时间完全取决于你的用例以及数据预期到达的延迟时间。 -
我们在键列
InvoiceNo上定义了一个分组函数,并为我们的状态操作定义了所需的窗口,窗口大小为30秒,滑动窗口为每10秒一次。这意味着我们的键将在初始的30秒窗口后,每10秒进行一次聚合。 -
我们定义了聚合函数,其中对时间戳列使用
max函数,对键列使用count函数。 -
一旦水印过期,流处理过程会立即将数据写入流式接收器。
-
-
一旦使用窗口函数和水印函数定义了状态流,我们可以快速验证流是否按预期工作,如下所示的代码片段所示:
(aggregated_df .writeStream .queryName("aggregated_df") .format("memory") .outputMode("complete") .start())上述代码块将状态处理流式数据框的输出写入内存接收器,并指定了一个
queryName属性。该流被注册为一个内存表,使用指定的查询名称,可以通过 Spark SQL 轻松查询,以便快速验证代码的正确性。
通过利用结构化流式处理提供的窗口功能和水印功能,可以实现有状态的流处理,并且可以轻松处理迟到的数据。在本章之前所有的代码示例中,另一个需要注意的方面是流数据如何逐步从原始状态转化为处理后的状态,再进一步转化为聚合后的状态。这种使用多个流式过程逐步转化数据的方法通常被称为多跳架构。在接下来的部分,我们将进一步探讨这种方法。
多跳管道
多跳管道是一种架构,用于构建一系列链式连接的流式作业,使得管道中的每个作业处理数据并逐步提升数据的质量。一个典型的数据分析管道包括多个阶段,包括数据摄取、数据清洗与整合、数据聚合等。随后,它还包括数据科学和机器学习相关的步骤,如特征工程、机器学习训练和评分。这个过程逐步提高数据质量,直到它最终准备好供终端用户使用。
使用结构化流式处理,所有这些数据分析管道的阶段可以被链式连接成一个有向无环图(DAG)的流式作业。通过这种方式,新的原始数据持续进入管道的一端,并通过管道的每个阶段逐步处理。最终,经过处理的数据从管道的尾端输出,准备供终端用户使用。以下是一个典型的多跳架构:

图 4.5 – 多跳管道架构
上面的图示代表了一个多跳管道架构,其中原始数据被摄取到数据湖中,并通过数据分析管道的每个阶段进行处理,从而逐步提高数据的质量,直到最终准备好供终端用户使用。终端用户的使用场景可能是商业智能与报告,或者进一步处理为预测分析的使用场景,利用数据科学和机器学习技术。
虽然这看起来是一个简单的架构实现,但为了无缝实现多跳管道,必须满足一些关键的前提条件,以避免频繁的开发者干预。前提条件如下:
-
为了使管道的各个阶段能够无缝连接,数据处理引擎需要支持“恰好一次”数据处理保证,并且能够在故障发生时对数据丢失具有恢复能力。
-
数据处理引擎需要具备维护水印数据的能力。这样可以确保它能够在给定的时间点了解数据处理的进度,并且能够无缝地接收以流式方式到达的新数据并进行处理。
-
底层数据存储层需要支持事务性和隔离性保障,以便在作业失败时,无需开发人员干预处理任何错误或不正确的数据清理。
Apache Spark 的结构化流式处理解决了前面提到的1和2问题,因为它保证了精确一次的数据处理语义,并且内建支持检查点。这是为了跟踪数据处理进度,并帮助在作业失败后从停止的地方重新启动。3问题由 Delta Lake 提供支持,其提供 ACID 事务保障,并支持同时进行批处理和流式作业。
-
让我们实现一个多跳管道示例,使用结构化流式处理和 Delta Lake,如下面的代码块所示:
raw_stream_df = (spark .readStream .schema(retailSchema) .option("header", True) .csv("/FileStore/shared_uploads/online_retail/")) (raw_stream_df .writeStream .format("delta") .option("checkpointLocation", "/tmp/delta/raw_stream.delta/checkpoint") .start("/tmp/delta/raw_stream.delta/"))在前面的代码块中,我们通过将源数据从其原始格式摄取到 Delta Lake 格式的数据湖中,创建了一个原始流式 DataFrame。
checkpointLocation为流式作业提供了容错性,而 Delta Lake 作为目标位置则为write操作提供了事务性和隔离性保障。 -
现在,我们可以使用另一个作业进一步处理原始摄取的数据,进一步提高数据质量,如下面的代码块所示:
integrated_stream_df = (raw_stream_df .withColumn("InvoiceTime", to_timestamp("InvoiceDate", 'dd/M/yy HH:mm'))) (integrated_stream_df .writeStream .format("delta") .option("checkpointLocation", "/tmp/delta/int_stream.delta/checkpoint") .start("/tmp/delta/int_stream.delta/"))在前面的代码块中,我们将一个字符串列转换为时间戳列,并将清理后的数据持久化到 Delta Lake。这是我们多跳管道的第二阶段,通常,这个阶段从前一阶段的原始数据摄取所生成的 Delta 表中读取数据。同样,这里使用的检查点位置有助于执行数据的增量处理,并在新的记录到达时处理添加到原始 Delta 表中的数据。
-
现在我们可以定义管道的最终阶段,在这个阶段,我们将数据汇总为高度摘要的数据,准备供最终用户消费,如下面的代码片段所示:
aggregated_stream_df = (integrated_stream_df .withWatermark("InvoiceTime", "1 minutes") .groupBy("InvoiceNo", window("InvoiceTime", "30 seconds", "10 seconds", "0 seconds")) .agg(max("InvoiceTime").alias("event_time"), count("InvoiceNo").alias("order_count"))) (aggregated_stream_df .writeStream .format("delta") .option("checkpointLocation", "/tmp/delta/agg_stream.delta/checkpoint") .start("/tmp/delta/agg_stream.delta/"))在前面的代码块中,已集成并清理过的数据被汇总成最高级别的摘要数据。这个数据可以进一步供商业智能或数据科学与机器学习使用。管道的这一阶段也使用了检查点位置和 Delta 表,以确保作业失败时的容错性,并跟踪到达时需要处理的新数据。
因此,通过结合 Apache Spark 的结构化流和 Delta Lake,实现多跳架构变得无缝且高效。多跳架构的不同阶段可以实现为一个包含多个流处理过程的单一整体作业。作为最佳实践,管道中每个阶段的单独流处理过程被拆分为多个独立的流作业,这些作业可以通过外部调度器(如 Apache Airflow)进一步链接成一个 DAG。后者的优点在于更易于维护各个流作业,并且在需要更新或升级管道的某个阶段时,可以最大限度地减少整个管道的停机时间。
总结
本章介绍了实时数据分析系统的需求以及它们在向业务用户提供最新数据、帮助企业提高市场响应速度并最小化任何机会损失方面的优势。展示了典型实时分析系统的架构,并描述了主要组件。还展示了一个使用 Apache Spark 结构化流的实时分析架构。描述了实时数据分析的几个突出行业应用案例。此外,还介绍了一个简化的 Lambda 架构,使用结构化流和 Delta Lake 的组合。介绍了 CDC 的应用案例,包括其要求和好处,并展示了如何利用结构化流实现 CDC 用例的技术。
最后,你学习了一种通过多跳管道逐步改善数据质量的技术,从数据摄取到高度聚合和汇总的数据,几乎实时完成。你还研究了使用结构化流和 Delta Lake 强大组合实现的多跳管道的简单实现。
本书的数据工程部分到此结束。你迄今为止学到的技能将帮助你开始数据分析之旅,从操作源系统的原始事务数据开始,摄取到数据湖中,进行数据清洗和整合。此外,你应该熟悉构建端到端的数据分析管道,这些管道能够以实时流的方式逐步提高数据质量,并最终生成可以供商业智能和报告使用的、清晰且高度聚合的数据。
在接下来的章节中,你将基于迄今为止学到的数据工程概念,深入探索利用 Apache Spark 的数据科学和机器学习功能进行预测分析的领域。在下一章中,我们将从探索性数据分析和特征工程的概念开始。
第二部分:数据科学
一旦我们在数据湖中获得了清洗后的数据,就可以开始对历史数据进行数据科学和机器学习处理。本节帮助你理解可扩展机器学习的重要性和需求。本节的各个章节展示了如何使用 PySpark 以可扩展和分布式的方式进行探索性数据分析、特征工程和机器学习模型训练。本节还介绍了 MLflow,一个开源的机器学习生命周期管理工具,适用于跟踪机器学习实验和生产化机器学习模型。本节还向你介绍了一些基于标准 Python 扩展单机机器学习库的技术。
本节包括以下章节:
第五章**,使用 PySpark 进行可扩展机器学习
第六章**,特征工程 – 提取、转换与选择
第七章**,有监督机器学习
第八章**,无监督机器学习
第九章**,机器学习生命周期管理
第十章**,使用 PySpark 扩展单节点机器学习
第五章:使用 PySpark 进行可扩展机器学习
在前几章中,我们已经建立了现代数据以惊人的速度增长,且其体量、速度和准确性是传统系统无法跟上的。因此,我们学习了分布式计算,以跟上日益增长的数据处理需求,并通过实际案例了解如何摄取、清洗和整合数据,将其处理到适合商业分析的水平,充分利用 Apache Spark 统一的数据分析平台的强大功能和易用性。本章及后续章节将探索数据科学和机器学习(ML)在数据分析中的应用。
如今,人工智能(AI)和机器学习(ML)计算机科学领域正经历大规模复兴,并且无处不在。各行各业的企业都需要利用这些技术来保持竞争力,扩大客户群,推出新产品线,并保持盈利。然而,传统的机器学习和数据科学技术是为了处理有限的数据样本而设计的,本身并不具备扩展性。
本章为你提供了传统机器学习算法的概述,包括有监督和无监督的 ML 技术,并探索了机器学习在商业应用中的实际案例。接着,你将了解可扩展机器学习的必要性。将介绍一些以分布式方式扩展 ML 算法、处理非常大的数据样本的技术。然后,我们将深入探讨 Apache Spark 的 ML 库——MLlib,并结合代码示例,使用 Apache Spark 的 MLlib 进行数据整理,探索、清洗并操作数据,为机器学习应用做好准备。
本章涵盖以下主要内容:
-
机器学习概述
-
扩展机器学习
-
使用 Apache Spark 和 MLlib 进行数据整理
到本章结束时,你将会对可扩展的机器学习(ML)及其商业应用有所了解,并掌握 Apache Spark 的可扩展 ML 库——MLlib 的基本知识。你将掌握使用 MLlib 清洗和转换数据的技能,为大规模机器学习应用做准备,帮助你减少数据清洗任务所需的时间,使你的整体 ML 生命周期更加高效。
技术要求
在本章中,我们将使用 Databricks Community Edition 来运行我们的代码:community.cloud.databricks.com。
-
注册说明可以在
databricks.com/try-databricks找到。 -
本章使用的代码和数据可以从
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter05下载。
机器学习概述
机器学习是人工智能和计算机科学的一个领域,利用统计模型和计算机算法学习数据中固有的模式,而无需显式编程。机器学习由能够自动将数据中的模式转换为模型的算法组成。当纯数学或基于规则的模型一遍又一遍地执行相同任务时,机器学习模型则从数据中学习,并且通过暴露于大量数据,性能可以大大提高。
一个典型的机器学习过程涉及将机器学习算法应用于已知数据集(称为训练数据集),以生成新的机器学习模型。这个过程通常被称为模型训练或模型拟合。一些机器学习模型是在包含已知正确答案的数据集上进行训练的,目的是在未知数据集中预测这些答案。训练数据集中已知的正确值称为标签。
一旦模型训练完成,生成的模型将应用于新数据以预测所需的值。这个过程通常被称为模型推断或模型评分。
提示
与其训练单一模型,最佳做法是使用不同的模型参数(称为超参数)训练多个模型,并根据定义明确的准确性指标从所有训练过的模型中选择最佳模型。这个基于不同参数训练多个模型的过程通常被称为超参数调优或交叉验证。
机器学习算法的示例包括分类、回归、聚类、协同过滤和降维。
机器学习算法的类型
机器学习算法可以分为三大类,即监督学习、无监督学习和强化学习,以下部分将详细讨论这些内容。
监督学习
监督学习是一种机器学习方法,其中模型在已知标签的训练数据集上进行训练。该标签是在训练数据集中标记的,并代表我们尝试解决问题的正确答案。我们进行监督学习的目的是在模型在已知数据集上经过训练后,预测未知数据集中的标签。
监督学习算法的示例包括线性回归、逻辑回归、朴素贝叶斯分类器、K 近邻、决策树、随机森林、梯度提升树和支持向量机。
监督学习可以分为两大类,即回归问题和分类问题。回归问题涉及预测一个未知标签,而分类问题则尝试将训练数据集分类到已知类别中。使用Apache Spark MLlib进行监督学习的详细实现将在第六章,“监督学习”中介绍。
无监督学习
无监督学习是机器学习的一种类型,其中训练数据对算法是未知的,且没有提前标注正确答案。无监督学习涉及学习一个未知、未标注的数据集的结构,没有用户的任何指导。在这种情况下,机器的任务是根据某些相似性或差异将数据分组或归类,而无需任何预先的训练。
无监督学习可以进一步分为聚类和关联问题。聚类问题涉及发现训练数据集中的类别,而关联问题则涉及发现数据中描述实体关系的规则。无监督学习的例子包括 K-means 聚类和协同过滤。无监督学习将在第七章中详细探讨,无监督机器学习,并通过 Apache Spark MLlib 提供编码示例。
强化学习
强化学习被软件系统和机器用于在给定情境下找到最优的行为或路径。与已经在训练数据集中包含正确答案的监督学习不同,在强化学习中没有固定答案,强化学习代理通过反复试验来决定结果,并且旨在从经验中学习。强化学习代理会根据选择的路径获得奖励或受到惩罚,目标是最大化奖励。
强化学习应用于自驾车、机器人技术、工业自动化以及用于聊天机器人代理的自然语言处理等领域。Apache Spark MLlib 中没有现成的强化学习实现,因此深入探讨这一概念超出了本书的范围。
注意
数据科学和机器学习的另一个分支是深度学习,它利用了先进的机器学习技术,如神经网络,这些技术近年来也变得非常突出。虽然 Apache Spark 确实支持某些深度学习算法,但这些概念过于先进,无法在本书的范围内涉及。
机器学习的业务应用案例
到目前为止,我们讨论了机器学习的不同类别,并简要介绍了机器学习模型可以执行的任务。在本节中,您将了解一些机器学习算法在现实生活中如何帮助解决不同行业的实际商业问题。
客户流失预防
使用机器学习建立客户流失模型对于识别那些可能停止与您的业务互动的客户非常有用,并且还可以帮助您深入了解导致客户流失的因素。流失模型可以简单地是一个回归模型,用于估算每个个体的风险评分。客户流失模型可以帮助企业识别面临流失风险的客户,从而实施客户保持策略。
客户终生价值建模
零售企业的收入大部分来自少数高价值客户,这些客户带来了重复购买。客户终生价值模型可以估算一个客户的生命周期,即客户可能流失的时期。它们还可以预测一个客户在其生命周期内可能带来的总收入。因此,估算潜在高价值客户在其生命周期内可能带来的收入,对于重新分配营销资金来吸引和留住这些客户至关重要。
需求预测
实体店和在线商店都有有限的实际存储空间,无论是在商店内还是在仓库里。因此,如何将这些有限的存储空间填充上实际需求的产品是非常重要的。您可以基于季节性和每年的月份开发一个简单的模型。然而,建立一个更复杂的机器学习模型,不仅包括季节性和历史数据,还包括外部数据,例如社交媒体上的当前趋势、天气预报数据和社交媒体上的客户情绪,可能会导致更准确的需求预测,并因此帮助最大化收入。
运输交货时间预测
任何涉及配送和物流操作的企业,无论是在线零售商还是食品配送平台,都需要能够估算订单送达客户所需的时间。通常,运输的交货时间是客户在选择是否与您合作时做决策的一个关键因素,客户可能会因此选择与竞争对手合作。回归模型可以根据产品的起始地和目的地、天气及其他季节性数据准确估算产品交付到客户邮政编码所需的时间。
市场篮分析
市场篮分析是一种根据客户购物篮中已有的商品向其推荐其他产品的技术。通过利用协同过滤算法,机器学习(ML)可以发现产品类别之间的关联规则,从而根据客户购物车中的商品和过去的购买记录向在线客户推荐产品。这是几乎所有电子零售商常用的一个重要应用。
财务欺诈检测
机器学习具备从数据中检测模式的固有能力。因此,可以利用机器学习构建能够检测财务交易异常的模型,以标记某些交易为欺诈行为。传统上,金融机构已经在利用基于规则的模型进行欺诈检测,但将机器学习模型纳入其中会使欺诈检测模型更加有效,从而帮助发现新的欺诈模式。
使用自然语言处理进行信息提取
制药公司和产生大量知识的企业面临着一个特定于其行业的独特挑战。在拥有数万名员工的组织中,尝试识别某个知识点是否已经由另一个小组创建,并非一件简单的事情。机器学习的自然语言处理技术可以用来整理、分组、分类和标注大量文档,从而使用户能够轻松搜索是否已有类似的知识点存在。
到目前为止,你已经了解了机器学习的基本知识、不同类型的机器学习算法以及它们在实际商业案例中的应用。在接下来的部分中,我们将讨论可扩展机器学习的需求,以及扩展机器学习算法的一些技术,并介绍 Apache Spark 的原生可扩展机器学习库MLlib及其在数据整理中的应用。
扩展机器学习
在前面的章节中,我们了解到,机器学习是一套算法,而不是显式编程,它能够自动学习数据中隐藏的模式。因此,暴露给更大数据集的机器学习算法,可能会导致更好的模型表现。然而,传统的机器学习算法设计是基于有限的数据样本并在单台机器上训练的。这意味着现有的机器学习库本身并不具备扩展性。解决这一问题的一个方法是将较大的数据集下采样,以适应单台机器的内存,但这也可能意味着最终得到的模型不如可能的最优模型准确。
此外,通常会在同一数据集上构建多个机器学习模型,仅仅是改变提供给算法的参数。在这些模型中,选择最优的模型用于生产环境,这个过程叫做超参数调优。在单台机器上按顺序构建多个模型,需要很长时间才能得到最佳模型,这导致生产周期更长,从而也增加了推向市场的时间。
鉴于传统机器学习算法的可扩展性挑战,迫切需要扩展现有的机器学习算法或开发新的可扩展机器学习算法。我们将在接下来的部分中探索一些扩展机器学习算法的技术。
扩展机器学习的技术
以下部分将介绍两种扩展机器学习算法的主要技术。
令人尴尬的并行处理
令人尴尬的并行处理是一种并行计算技术,在这种技术中,几乎不需要任何努力就可以将给定的计算问题分解成较小的并行任务。当并行化的任务之间没有任何相互依赖,且所有任务都可以完全独立执行时,这种方式就可以实现。
现在,让我们尝试将这个方法应用到在非常大的数据集上扩展单机机器学习算法的问题上,乍一看,这似乎不是一个简单的任务。然而,考虑到超参数调优或交叉验证的问题,我们可以运行多个并行模型,每个模型都有不同的参数,但它们都可以在一个单机的内存中处理相同的小数据集。在这种情况下,我们可以通过调整模型参数轻松地在相同的数据集上训练多个模型。因此,通过利用令人尴尬的并行处理技术,我们可以将模型构建过程加速数个数量级,帮助我们在数小时内而非数周或数月内找到最优的模型,从而加速你的业务价值实现。你将进一步了解如何在第十章**中应用这一技术,使用 PySpark 扩展单节点机器学习。
可扩展的机器学习算法
尽管令人尴尬的并行计算技术帮助我们在更短时间内得到更好的模型,并提高了准确性,但它仍然受限于较小的数据集大小。这意味着我们可能会因为数据的下采样而错失潜在的数据模式。为了解决这个问题,我们需要能够天然扩展至多个机器的机器学习算法,并能够在分布式的方式下训练非常大的数据集。Apache Spark 的原生 ML 库,名为 MLlib,包含了这些本质上可扩展的机器学习算法,接下来的部分我们将进一步探讨 MLlib。
Apache Spark 的 ML 库简介
MLlib 是 Apache Spark 的原生机器学习库。作为一个原生库,MLlib 与 Spark 的其他 API 和库紧密集成,包括 Spark SQL 引擎、DataFrame API、Spark SQL API,甚至结构化流处理。这个特性使得 Apache Spark 成为一个真正统一的数据分析平台,可以执行所有与数据分析相关的任务,从数据摄取到数据转换,再到数据的临时分析、构建复杂的机器学习模型,甚至将这些模型应用于生产环境中。在接下来的部分,你将更深入地了解 Spark MLlib 及其核心组件。
Spark MLlib 概览
在 Apache Spark 的早期版本中,MLlib 基于 Spark 的 RDD API。自 Spark 2.0 版本开始,推出了一个基于 DataFrame API 的新 ML 库。现在,在 Spark 3.0 及更高版本中,基于 DataFrame API 的 MLlib 是标准,而旧的基于 RDD 的 MLlib 处于维护模式,未来不会再进行扩展。
基于 DataFrame 的 MLlib 与传统的基于 Python 的单机 ML 库(如 scikit-learn)高度相似,包含三个主要组件,分别是转换器、估算器和管道,具体内容将在接下来的章节中介绍。
转换器
转换器 是一种算法,它接受一个 DataFrame 作为输入,对 DataFrame 列进行处理,并返回另一个 DataFrame。使用 Spark MLlib 训练的 ML 模型是一个转换器,它接受一个原始 DataFrame,并返回一个包含原始数据和新预测列的 DataFrame。典型的转换器管道如下图所示:

图 5.1 – 一个转换器管道
在前面的图中,展示了一个典型的转换器管道,其中一系列的转换器阶段,包括 VectorIndexer 和已训练的 线性回归模型,被应用到原始的 DataFrame。结果是一个新的 DataFrame,包含了所有原始列,并新增了包含预测值的新列。
注
转换操作与 Spark 的 MLlib 中的转换器是不同的概念。虽然两者都将一个 DataFrame 转换为另一个 DataFrame,并且都是惰性计算,但前者是对 DataFrame 执行的操作,而后者则是一个实际的 ML 算法。
估算器
估算器是另一种算法,它接受一个 DataFrame 作为输入,并生成一个转换器。任何 ML 算法都是一个估算器,因为它将包含原始数据的 DataFrame 转换为包含实际预测结果的 DataFrame。估算器管道在下图中描述:

图 5.2 – 一个估算器管道
在上图中,首先将 Transformer 应用于一个包含原始数据的 DataFrame,生成一个 特征向量 DataFrame。然后,将 估算器(以 线性回归 算法 的形式)应用于包含 特征向量 的 DataFrame,生成一个新的 线性回归模型,该模型作为 Transformer 返回。
注
特征向量是 Spark MLlib 库中的一种特殊数据结构。它是一个 DataFrame 列,包含实际的浮动点类型向量对象。由于 ML 基于数学和统计学,所有 ML 算法只对浮动点值的向量进行操作。原始数据通过特征提取和特征工程技术转换为特征向量。
管道
Spark MLlib 中的 ML 管道将多个转换器和估算器的阶段链在一起,形成一个执行端到端 ML 操作的有向无环图(DAG),该操作从数据清理到特征工程,再到实际的模型训练。一个管道可以是仅包含转换器的管道,也可以是仅包含估算器的管道,或者两者的结合。
使用 Spark MLlib 中的可用转换器和估算器,可以构建一个完整的端到端机器学习(ML)管道。一个典型的 ML 管道由多个阶段组成,从数据清理、特征工程、模型训练到模型推理。你将在接下来的章节中学习更多关于数据清理的技术。
使用 Apache Spark 和 MLlib 进行数据清理
数据清理,在数据科学社区中也称为 数据清洗 或简而言之 数据准备,是典型数据科学过程中的第一步。数据清理涉及采样、探索、选择、操作和清洗数据,以使其准备好进行机器学习应用。数据清理占整个数据科学过程的 60% 到 80%,是确保构建的 ML 模型准确性的最关键步骤。接下来的章节将使用 Apache Spark 和 MLlib 探讨数据清理过程。
数据预处理
数据预处理是数据清理过程中的第一步,涉及收集、探索和选择对于解决当前问题有用的数据元素。数据科学过程通常继承数据工程过程,假设数据湖中已经存在干净和集成的数据。然而,足够干净的数据可能对于商业智能(BI)来说是合适的,但可能并不适合数据科学应用。同时,数据科学应用需要额外的数据集,这些数据集可能对其他分析用例无用,因此可能尚未清理。
在开始操作和清理数据之前,我们需要将其加载到 Spark DataFrame 中,并探索数据以了解其结构。以下代码示例将使用在 第三章《数据清理与集成》末尾产生的集成数据集,名为 retail_silver.delta:
raw_data = spark.read.format("delta").load("dbfs:/FileStore/shared_uploads/delta/retail_silver.delta")
raw_data.printSchema()
(select_data = raw_data.select("invoice_num", "stock_code",
"quantity", "invoice_date",
"unit_price","country_code",
"age", "work_class",
"final_weight")
select_data.describe().show()
在前面的代码片段中,我们执行了以下操作:
-
我们使用
spark.read()函数将数据从数据湖加载到 Spark DataFrame 中。 -
我们使用
printSchema()函数打印其架构,以检查列的数据类型。 -
我们使用
select()操作显示 DataFrame 中的几个列,以检查它们的值。 -
我们使用
describe()操作生成 DataFrame 的基本统计信息。
数据清理
在上一节的代码示例中,你应该注意到大多数数据类型只是字符串类型。数据集可能还包含重复项,并且数据中也可能存在 NULL 值。让我们解决数据集中的这些不一致性,如下所示的代码片段所示:
dedupe_data = select_data.drop_duplicates(["invoice_num",
"invoice_date",
"stock_code"])
interim_data = (select_data
.withColumn("invoice_time", to_timestamp("invoice_date",
'dd/M/yy HH:mm'))
.withColumn("cust_age", col("age").cast(FloatType()))
.withColumn("working_class",
col("work_class").cast(FloatType()))
.withColumn("fin_wt",
col("final_weight").cast(FloatType()))
)
clean_data = interim_data.na.fill(0)
在上面的代码片段中,我们执行了以下操作:
-
我们使用
dropduplicates()操作通过键列去重数据。 -
然后,我们使用
to_timestamp()函数将 datetime 列转换为正确的时间戳类型,通过提供正确的时间戳格式。 -
我们使用
CAST()方法更改 DataFrame 的数据类型。 -
我们使用
na.fill()操作将缺失值和NULL值替换为0。
本节展示了如何使用 PySpark 进行大规模的数据清洗。下一节将展示如何执行数据处理步骤,如过滤和重命名。
数据操作
一旦你有了更干净的数据集,你可以执行操作来过滤掉任何不需要的数据、重命名列以遵循你的命名约定,并删除任何不需要的数据列,如下代码块所示:
final_data = (clean_data.where("year(invoice_time) = 2009")
.withColumnRenamed("working_class",
"work_type")
.withColumnRenamed("fin_wt",
"final_weight")
.drop("age")
.drop("work_class")
.drop("fn_wt"))
pd_data = final_data.toPandas()
在上面的代码片段中,我们执行了以下操作:
-
我们使用
where()函数过滤、切片和处理数据。 -
我们使用
withColumnsRenamed()函数重命名列,并使用drop()函数删除不需要的列。 -
我们使用
toPandas()函数将 Spark DataFrame 转换为 PySpark DataFrame。
有时,Spark MLlib 中没有可用的 ML 算法,或者有一个使用单节点 Python 库构建的自定义算法。对于这些用例,你可以将 Spark DataFrame 转换为 pandas DataFrame,如前面的步骤 3所示。
注意
将 Spark DataFrame 转换为 pandas DataFrame 涉及将所有数据从 Executor 收集到 Spark 驱动程序。因此,需要注意此转换仅应用于较小的数据集,否则可能会导致驱动节点的OutOfMemory错误。
总结
在本章中,你了解了机器学习(ML)的概念和不同类型的 ML 算法。你还学习了 ML 在现实世界中的一些应用,帮助企业减少损失、最大化收入并加速上市时间。你还了解了可扩展 ML 的必要性,以及两种不同的技术来扩展 ML 算法。介绍了 Apache Spark 的本地 ML 库 MLlib 及其主要组件。
最后,你学习了一些执行数据清理、处理和转换的技术,使数据更适合数据科学流程。在接下来的章节中,你将学习机器学习(ML)流程的发送阶段,称为特征提取和特征工程,你将学习如何应用各种可扩展的算法来转换单个数据字段,使其更适合数据科学应用。
第六章:特征工程 – 提取、转换和选择
在上一章中,您了解了 Apache Spark 的原生、可扩展的机器学习库MLlib,并获得了其主要架构组件的概述,包括转换器、估算器和管道。
本章将带领您进入可扩展机器学习旅程的第一阶段——特征工程。特征工程涉及从预处理和清理后的数据中提取机器学习特征的过程,以便为机器学习做准备。您将学习特征提取、特征转换、特征缩放和特征选择等概念,并通过 Spark MLlib 中的算法和一些代码示例实现这些技术。在本章结束时,您将掌握实施可扩展特征工程管道的必要技术,将预处理的数据转换为适合并准备好用于机器学习模型训练过程的格式。
特别地,在本章中,您将学习以下内容:
-
机器学习过程
-
特征提取
-
特征转换
-
特征选择
-
特征库作为中央特征存储库
-
Delta 作为离线特征存储库
技术要求
在本章中,我们将使用 Databricks 社区版来运行代码。可以在community.cloud.databricks.com找到。
-
注册说明可以在
databricks.com/try-databricks找到。本章使用的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter06下载。 -
本章使用的数据集可以在
github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
机器学习过程
一个典型的数据分析和数据科学过程包括收集原始数据、清理数据、整合数据和集成数据。之后,我们将统计学和机器学习技术应用于预处理后的数据,以生成机器学习模型,最后以数据产品的形式总结并向业务相关方传达过程结果。机器学习过程的高层次概述见下图:

图 6.1 – 数据分析和数据科学过程
从前面的图表可以看出,实际的机器学习过程只是整个数据分析过程的一小部分。数据团队花费大量时间进行数据策划和预处理,而其中只有一部分时间用于构建实际的机器学习模型。
实际的机器学习过程包括多个阶段,这些阶段使你能够执行诸如数据探索、特征提取、模型训练、模型评估以及应用模型到实际商业场景等步骤,如下图所示:

图 6.2 – 机器学习过程
在本章中,你将了解机器学习过程中的特征工程阶段。接下来的章节将介绍一些在Spark MLlib库中可用的突出算法和工具,这些算法和工具涉及特征提取、特征转换、特征缩放和特征选择等特征工程步骤。
特征提取
机器学习模型等同于数学中的函数或计算机编程中的方法。机器学习模型接受一个或多个参数或变量作为输入,并生成一个输出,称为预测。在机器学习术语中,这些输入参数或变量称为特征。特征是机器学习算法或模型中输入数据集的一列。特征是一个可度量的数据点,如个人的姓名、性别或年龄,或者是与时间相关的数据、天气数据,或其他对分析有用的数据。
机器学习算法利用线性代数这一数学领域,并使用矩阵和向量等数学结构在内部以及算法代码层面上表示数据。即使在经过数据工程处理之后,实际世界中的数据也很少以矩阵和向量的形式出现。因此,特征工程过程会应用于预处理数据,以将其转换为适合机器学习算法的格式。
特征提取过程专门处理将文本、图像、地理空间或时间序列数据转换为特征向量的问题。Apache Spark MLlib 提供了多种特征提取方法,如TF-IDF、Word2Vec、CountVectorizer和FeatureHasher。
让我们以一组单词为例,使用CountVectorizer算法将其转换为特征向量。在本书的早期章节中,我们查看了一个在线零售商的样本数据集,并对这些数据集应用了数据工程过程,以获得一个干净且整合的、适合分析的数据集。
那么,让我们从在第五章《可扩展机器学习与 PySpark》末尾产生的预处理和清理后的数据集开始,该数据集名为retail_ml.delta。这个预处理的数据集,作为机器学习过程的输入,通常被称为训练数据集:
-
作为第一步,让我们将数据从数据湖以 Delta 格式加载到 Spark DataFrame 中。如下方代码片段所示:
preproc_data = spark.read.format("delta").load("dbfs:/FileStore/shared_uploads/delta/retail_ml.delta") preproc_data.show()在前面的代码块中,我们将存储在数据湖中的 Delta 格式数据加载到 Spark DataFrame 中,然后使用
show()命令显示数据。 -
显示函数的结果如下图所示:
![图 6.3 – 预处理数据]()
图 6.3 – 预处理数据
在前面的图示中,我们得到了经过数据工程和数据整理步骤后的预处理数据。请注意,数据集中有
11列,数据类型各异,包含字符串、双精度数和时间戳。在当前格式下,它们不适合作为机器学习算法的输入;因此,我们需要通过特征工程过程将其转换为适合的格式。 -
让我们从
description列开始,该列为文本类型,并对其应用CountVectorizer特征提取算法,以便将其转换为特征向量,如下方代码块所示:from pyspark.sql.functions import split, trim from pyspark.ml.feature import CountVectorizer cv_df = preproc_data.withColumn("desc_array", split(trim("description"), " ")).where("description is NOT NULL") cv = CountVectorizer(inputCol="desc_array", outputCol="description_vec", vocabSize=2, minDF=2.0) cv_model = cv.fit(cv_df) train_df = model.transform(cv_df) train_df.display()在前面的代码块中,发生了以下操作:
-
我们从
pyspark.ml.feature库中导入CountVectorizer。 -
CountVectorizer接受一个Array对象作为输入,因此我们使用split()函数将 description 列分割成一个单词的Array对象。 -
然后,我们使用之前定义的估算器在输入数据集上初始化一个新的
CountVectorizerfit()方法。结果是一个经过训练的模型transform()方法应用于输入 DataFrame,从而生成一个新的 DataFrame,并为 description 列添加一个新的特征向量列。
通过使用 Spark MLlib 中的
CountVectorizer特征提取器,我们能够从文本类型的列中提取特征向量。 -
-
Spark MLlib 中还可以使用其他特征提取器,如
Word2Vec,如下方代码片段所示:from pyspark.ml.feature import Word2Vec w2v_df = preproc_data.withColumn("desc_array", split(trim("description"), "\t")).where("description is NOT NULL") word2vec = Word2Vec(vectorSize=2, minCount=0, inputCol="desc_array", outputCol="desc_vec") w2v_model = word2vec.fit(w2v_df) train_df = w2v_model.transform(w2v_df)在前面的代码块中,
Word2Vec估算器的使用方式与之前提到的CountVectorizer类似。在这里,我们用它从基于文本的数据列中提取特征向量。虽然CountVectorizer和Word2Vec都帮助将一个单词语料库转换成特征向量,但它们在每个算法的内部实现上有所不同。它们各自有不同的用途,取决于问题情境和输入数据集,并且在不同的情况下可能会产生不同的结果。
请注意,讨论这些算法的细微差别或提出何时使用特定特征提取算法的建议超出了本书的范围。
现在您已经学习了几种特征提取技术,在下一节中,让我们探讨几种特征转换的 Spark MLlib 算法。
特征转换
特征转换是仔细审查训练数据中存在的各种变量类型,如分类变量和连续变量,并确定最佳转换类型以实现最佳模型性能的过程。本节将描述如何转换机器学习数据集中发现的几种常见变量类型的示例代码,例如文本和数值变量。
转换分类变量
分类变量是具有有限和有限范围的离散值的数据片段。它们通常是基于文本的性质,但也可以是数字的。例如,国家代码和年份的月份。我们在前一节中提到了关于如何从文本变量中提取特征的几种技术。在本节中,我们将探讨几种其他算法来转换分类变量。
将文本标记化为单独的术语
Tokenizer类可以用来将文本分解为其组成的术语,如下面的代码示例所示:
from pyspark.ml.feature import Tokenizer
tokenizer = Tokenizer(inputCol="description",
outputCol="desc_terms")
tokenized_df = tokenizer.transform(preproc_data)
tokenized_df.show()
在前面的代码块中,我们通过传入inputCol和outputCol参数来初始化Tokenizer类,从而生成一个转换器。然后,我们转换训练数据集,得到一个 Spark DataFrame,其中包含一个新列,其中包含每个句子中被转换为小写的单词数组。这在下表中显示:

图 6.4 – 使用 Tokenizer 标记化文本
在上表中,您可以从标记化的单词中看到有一些不需要的词,我们需要将它们去掉,因为它们没有添加任何价值。
使用StopWordsRemover删除常见词
每种语言都包含常见且频繁出现的单词,如介词、冠词、连词和感叹词。这些词在机器学习过程中不具备任何意义,并且最好在训练机器学习算法之前将其删除。在 Spark 中,可以使用StopWordsRemover类来实现这一过程,如下面的代码片段所示:
from pyspark.ml.feature import StopWordsRemover
stops_remover = StopWordsRemover(inputCol="desc_terms",
outputCol="desc_nostops")
stops_df = stops_remover.transform(tokenized_df)
stops_df.select("desc_terms", "desc_nostops").show()
在前面的代码块中,我们通过传入inputCol和outputCol参数来初始化StopWordsRemover类,从而生成一个转换器。然后,我们转换训练数据集,得到一个 Spark DataFrame,其中包含一个新列,该列具有一个数组,其中包含已删除停用词的单个单词。
一旦我们有了一个删除了停用词的字符串数组,就可以使用诸如Word2Vec或CountVectorizer等特征提取技术来构建特征向量。
编码离散的分类变量
现在我们有其他类型的字符串类型列,例如需要转换为数值形式以供机器学习算法使用的国家代码。你不能简单地为这些离散的分类变量分配任意的数值,因为这样可能会引入一种数据中本不存在的模式。
让我们考虑一个例子,其中我们按字母顺序单调递增地为分类变量分配值。然而,这可能会为这些变量引入一种排名,而这种排名在最初并不存在。这将扭曲我们的机器学习模型,并且并不理想。为了解决这个问题,我们可以使用 Spark MLlib 中的多种算法来编码这些分类变量。
使用 StringIndexer 编码字符串变量
在我们的训练数据集中,我们有字符串类型或分类变量,它们具有离散值,如country_code。这些变量可以使用StringIndexer分配标签索引,如下方代码示例所示:
from pyspark.ml.feature import StringIndexer
string_indexer = StringIndexer(inputCol="country_code",
outputCol="country_indexed",
handleInvalid="skip" )
indexed_df = string_indexer.fit(stops_df).transform(stops_df)
indexed_df.select("country_code", "country_indexed").show()
在前面的代码片段中,我们初始化了StringIndexer类,并设置了输入和输出列的名称。然后,我们将handleInvalid设置为skip,以跳过NULL和无效值。这样就得到了一个可以应用于训练数据框的估算器,进而得到了一个变换器。该变换器可以应用于训练数据集,从而生成一个新的 Spark 数据框,并且在其中新增了一个列,包含输入分类变量的标签索引。
使用 OneHotEncoder 将分类变量转换为向量
一旦我们将分类变量编码为标签索引,它们最终可以使用OneHotEncoder类转换为二进制向量,如下方代码片段所示:
from pyspark.ml.feature import OneHotEncoder
ohe = OneHotEncoder(inputCol="country_indexed",
outputCol="country_ohe")
ohe_df = ohe.fit(indexed_df).transform(indexed_df)
ohe_df.select("country_code", "country_ohe").show()
在前面的代码片段中,我们初始化了OneHotEncoder类,并设置了输入和输出列的名称。这样就得到了一个估算器,可以应用于训练数据框,进而生成一个变换器。该变换器可以应用于训练数据集,从而生成一个新的 Spark 数据框,并在其中新增一个列,包含表示原始分类变量的特征向量。
变换连续变量
连续变量以测量或观测值的形式表示数据。通常,它们是数值型的,几乎可以具有无限的范围。在这里,数据是连续的,而非离散的,一些例子包括年龄、数量和单价。它们看起来很直接,并可以直接输入机器学习算法。然而,它们仍然需要进行特征工程,因为连续变量可能有太多值,机器学习算法无法处理。处理连续变量的方法有很多种,比如分箱、归一化、应用自定义业务逻辑等,应该根据所解决的问题和业务领域选择适当的方法。
一种特征工程连续变量的技术是二值化,其中将连续的数值转换为基于用户定义阈值的二进制值,如以下代码示例所示:
from pyspark.ml.feature import Binarizer
binarizer = Binarizer(threshold=10, inputCol="unit_price",
outputCol="binarized_price")
binarized_df = binarizer.transform(ohe_df)
binarized_df.select("quantity", "binarized_price").show()
在前面的代码块中,我们通过输入和输出列参数初始化 Binarizer 类,生成一个转换器。然后可以将此转换器应用于训练数据框,从而得到一个新的数据框,并附加一个表示连续变量的二进制值的新列。
转换日期和时间变量
日期或时间戳类型的列本身对机器学习模型的训练过程并没有太大价值。然而,日期的组成部分,如月份、年份或星期几,可能会有某些模式。因此,选择日期时间列的某一部分并将其转换为适当的特征是非常有用的。
在以下代码示例中,我们从日期时间列中提取月份值并将其转换为特征,将其视为类别变量:
from pyspark.sql.functions import month
month_df = binarized_df.withColumn("invoice_month",
month("invoice_time"))
month_indexer = StringIndexer(inputCol="invoice_month",
outputCol="month_indexed",
handleInvalid="skip" )
month_df = month_indexer.fit(month_df).transform(month_df)
month_df.select("invoice_month", "month_indexed").show()
在前面的代码块中,我们首先使用 month() 函数从时间戳列中提取月份,并将其附加到数据框中。然后,我们将新列通过 StringIndexer 估算器进行转换,将月份的数字列转换为标签索引。
将单个特征组合成特征向量
大多数机器学习算法接受一个单一的特征向量作为输入。因此,将提取并转换的单个特征合并为一个特征向量是很有用的。可以使用 Spark MLlib 的 VectorAssembler 转换器来实现,如以下代码示例所示:
from pyspark.ml.feature import VectorAssembler
vec_assembler = VectorAssembler(
inputCols=["desc_vec", "country_ohe",
"binarized_price", "month_indexed",
"quantity_indexed"],
outputCol="features")
features_df = vec_assembler.transform(month_df)
features_df.select("features").show()
在前面的代码块中,我们通过输入和输出参数初始化 VectorAssembler 类,生成一个转换器对象。我们利用该转换器将单个特征合并为一个特征向量。这样,新的向量类型列就会附加到训练数据框中。
特征缩放
训练数据集通常会有不同计量单位的列。例如,一列可能使用公制计量单位,而另一列可能使用英制单位。某些列可能具有较大的数值范围,例如,一列表示美元金额,另一列表示数量。这些差异可能导致机器学习模型不当地对某些值赋予更多权重,从而产生不良影响,可能会引入偏差或模型失真。为了解决这个问题,可以使用一种称为特征缩放的技术。Spark MLlib 内置了一些特征缩放算法,如Normalizer、StandardScaler、RobustScaler、MinMaxScaler和MaxAbsScaler。
在下面的代码示例中,我们将使用StandardScaler来演示如何在 Apache Spark 中应用特征缩放。StandardScaler转换特征向量,并将每个向量规范化为具有标准差单位:
from pyspark.ml.feature import StandardScaler
std_scaler = StandardScaler(inputCol="features",
outputCol="scaled_features")
scaled_df = std_scaler.fit(features_df).transform(features_df)
scaled_df.select("scaled_features").show()
在前面的代码块中,StandardScaler类被初始化为输入和输出列的参数。然后,StandardScaler估算器应用于训练数据集,生成一个StandardScaler模型转换对象。接着,可以将该对象应用到训练 DataFrame,从而生成一个新 DataFrame,并添加一个包含规范化特征的新列。
到目前为止,在本节中,你已经学习了如何从数据集的列中提取机器学习特征。此外,你还学习了将基于文本的列转换为特征向量的特征提取技术。我们还探讨了将分类、连续以及基于日期和时间的变量转换的特征转换技术。介绍了将多个单独的特征组合成单个特征向量的技术,最后,你还学习了一种特征缩放技术来规范化特征。
在接下来的部分,你将学习减少特征数量的技术,这被称为特征选择。
特征选择
特征选择是一种技术,它通过减少机器学习过程中的特征数量,同时利用较少的数据,并提高训练模型的准确性。特征选择是一个过程,可以自动或手动选择那些对你所关注的预测变量贡献最大的特征。特征选择是机器学习中的一个重要方面,因为不相关或半相关的特征可能会严重影响模型的准确性。
Apache Spark MLlib 提供了一些特征选择器,包括VectorSlicer、ChiSqSelector、UnivariateFeatureSelector和VarianceThresholdSelector。让我们通过以下代码示例,探索如何在 Apache Spark 中实现特征选择,利用ChiSqSelector根据我们试图预测的标签列选择最优特征:
from pyspark.ml.feature import ChiSqSelector
chisq_selector=ChiSqSelector(numTopFeatures=1,
featuresCol="scaled_features",
outputCol="selected_features",
labelCol="cust_age")
result_df = chisq_selector.fit(scaled_df).transform(scaled_df)
result_df.select("selected_features").show()
在前面的代码块中,我们使用输入列和输出列初始化ChiSqSelector。我们还指定了标签列,因为ChiSqSelector选择最适合预测标签列的最优特征。然后,将ChiSqSelector估算器应用于训练数据集,生成一个ChiSqSelector模型转换器对象。接下来,可以将该对象应用于训练数据框,以生成一个新的数据框列,其中包含新选择的特征。
类似地,我们也可以利用VectorSlicer从给定的特征向量中选择一部分特征,如下列代码片段所示:
from pyspark.ml.feature import VectorSlicer
vec_slicer = VectorSlicer(inputCol="scaled_features",
outputCol="selected_features",
indices=[1])
result_df = vec_slicer.transform(scaled_df)
result_df.select("scaled_features",
"selected_features").display()
前面的代码块也执行了特征选择。然而,与ChiSqSelector不同,VectorSlicer并没有针对给定的变量优化特征选择。相反,VectorSlicer接受一个带有指定索引的向量列。这将生成一个新的向量列,其值通过指定的索引进行选择。每个特征选择器都有自己进行特征选择的方法,应该根据给定的场景和待解决的问题选择合适的特征选择器。
到目前为止,你已经学习了如何从基于文本的变量中提取特征,以及如何对分类和连续类型的变量进行特征转换。此外,你还探索了特征切片和特征选择的技术。你已经掌握了将预处理后的原始数据转换为特征向量的技术,这些特征向量可以直接输入到机器学习算法中,用于构建机器学习模型。
然而,对于每一个机器学习问题都执行特征工程似乎既冗余又耗时。那么,能不能直接使用一些已经构建好的特征来为新模型服务呢?答案是肯定的,你应该在新的机器学习问题中重用之前构建的特征。你也应该能够利用你其他团队成员的特征。这可以通过一个集中式特征存储来实现。我们将在接下来的部分进一步探讨这个话题。
特征存储作为中央特征库
在任何机器学习问题上花费的大部分时间都用于数据清洗和数据整理,以确保我们建立模型的基础是干净且有意义的数据。特征工程是机器学习过程中的另一个关键步骤,数据科学家们花费大量时间策划机器学习特征,这是一个复杂且耗时的过程。为每个新的机器学习问题再次创建特征似乎是违反直觉的。
通常,特征工程是在已经存在的历史数据上进行的,新特征在不同的机器学习问题中是可以完全重用的。事实上,数据科学家花费大量时间寻找问题所需的正确特征。因此,拥有一个集中的特征库,可搜索并具有用于识别特征的元数据将是非常有益的。这个集中的可搜索特征库通常被称为特征存储。典型的特征存储架构如下图所示:

图 6.5 – 特征存储架构
特征不仅在机器学习过程的模型训练阶段中有用,而且在模型推断过程中也是必需的。推断,也称为模型评分,是将已构建的模型与新的未见特征一起输入,以便在新数据上生成预测的过程。根据推断过程是批处理模式还是流式实时模式,特征可以被广泛分类为离线特征和在线特征。
使用离线特征存储进行批量推断
离线特征,顾名思义,是使用批处理作业离线生成的。它们的消耗也是离线进行的,可以通过模型训练过程或批量机器学习管道中的模型推断来进行,即使用定期安排的批处理方式。这些特征可能需要耗费时间来创建,通常使用大数据框架(如 Apache Spark)创建,或通过从数据库或数据仓库运行定期查询来创建。
用于生成离线特征的存储机制被称为离线特征存储。历史数据存储、关系型数据库、数据仓库系统和数据湖都是离线特征存储的良好选择。离线特征存储应具有强类型、模式强制执行机制,并具有存储元数据以及实际特征的能力。任何数据库或数据仓库都适用于离线特征存储;然而,在下一节中,我们将探讨 Delta Lake 作为离线特征存储。
Delta Lake 作为离线特征存储
在第三章《数据清洗与集成》中,我们将数据湖确立为长期存储历史数据的可扩展且相对便宜的选择。我们讨论了数据可靠性和基于云的数据湖的一些挑战,并且你了解了 Delta Lake 是如何设计来克服这些挑战的。作为云数据湖的抽象层,Delta Lake 的优势不仅限于数据工程工作负载,也扩展到了数据科学工作负载,我们将在本节中深入探讨这些优势。
Delta Lake 是基于云的数据湖中理想的离线特征存储候选,因为它具备数据可靠性特性和 Delta Lake 提供的独特时间旅行功能。我们将在接下来的章节中讨论这些内容。
Delta 表的结构和元数据
Delta Lake 支持具有明确定义列数据类型的结构化数据。这使得 Delta 表具有强类型,确保可以将各种数据类型的特征存储在 Delta 表中。相比之下,实际存储发生在相对便宜且可以无限扩展的基于云的数据湖中。这使得 Delta Lake 成为云中理想的离线特征存储候选。
Delta Lake 的模式强制执行与演变
Delta Lake 完全支持模式强制执行,这意味着插入到 Delta Lake 特征存储中的特征数据的完整性得到了良好的维护。这将确保只有正确的、具有适当数据类型的数据用于机器学习模型的构建过程,从而确保模型的性能。Delta Lake 对模式演变的支持也意味着可以轻松地将新特征添加到基于 Delta Lake 的特征存储中。
支持同时处理批处理和流处理工作负载
由于 Delta Lake 完全支持统一的批处理和流处理工作负载,数据科学家除了批处理管道之外,还可以构建近实时的流式特征工程管道。这将有助于使用最新特征训练机器学习模型,并且也能够近实时地生成预测。这将通过仅利用 Apache Spark 的统一分析引擎,帮助消除高延迟推断需求的操作开销。
Delta Lake 时间旅行
数据科学家经常通过微小的数据变动来改善模型准确度,通常会为此目的维护相同物理数据的多个版本。利用 Delta Lake 的时间旅行功能,单个 Delta 表可以轻松支持数据的多个版本,从而消除数据科学家维护多个物理数据版本的开销。
与机器学习操作工具的集成
Delta Lake 还支持与流行的机器学习操作和工作流管理工具 MLflow 集成。我们将在第九章《机器学习生命周期管理》中探讨 MLOps 和 MLflow。
这里展示了一个利用 Delta Lake 作为离线特征存储的代码示例:
spark.sql("CREATE DATABASE feature_store")
(result_df
.write
.format("delta")
.mode("append")
.option("location", "/FileStore/shared_uploads/delta/retail_features.delta")
.saveAsTable("feature_store.retail_features"))
首先,我们创建一个名为 feature_store 的数据库。然后,我们将上一节中的 特征选择 步骤的结果保存为 Delta 表。
通过这种方式,可以使用简单的 SQL 命令搜索特征,并且还可以通过共享的 Hive 元存储库将其共享并用于其他机器学习应用场景。Delta Lake 还支持常见的元数据,例如列名和数据类型,其他元数据,如用户备注和评论,也可以添加,以便为特征存储中的特征提供更多上下文信息。
提示
大多数大数据平台,包括 Databricks,都支持内置的 Hive 元存储库来存储表的元数据。此外,这些平台还提供了安全机制,如数据库、表格,有时甚至包括行级和列级访问控制机制。通过这种方式,特征存储可以得到保护,特征可以在数据团队之间进行选择性共享。
通过这种方式,使用 Delta Lake 可以作为云数据湖上的离线特征存储。一旦特征被存储在 Delta 表中,它们可以通过 Spark 的所有 API 访问,包括 DataFrame 和 SQL API。
注意
Spark MLlib 中的所有函数和方法都经过设计,可以原生地进行扩展。因此,使用 Spark MLlib 执行的任何机器学习操作天生具有可扩展性,并且可以运行具有并行和分布式任务的 Spark 作业。
用于实时推理的在线特征存储
用于在线机器学习推理的特征被称为在线特征。通常,这些特征具有超低延迟的要求,通常从毫秒到秒级。在线特征的一些使用场景包括在最终用户应用中的实时预测。
让我们考虑一个客户浏览电商网站应用的例子。客户将一个产品加入购物车,基于客户的邮政编码,网页应用需要在几秒钟内提供一个预计的送货时间。这里涉及的机器学习模型需要一些特征来估算交货时间,比如仓库位置、产品库存、历史交货时间,甚至可能还需要天气和季节性条件,但最重要的是,它需要客户的邮政编码。大多数特征可能已经在离线特征存储中预先计算并可用。然而,鉴于该用例的低延迟要求,特征存储必须能够以最低的延迟提供这些特征。数据湖、数据库或数据仓库并不是该用例的理想选择,需要一个超低延迟的在线特征存储。
从前面的示例中,我们可以得出结论,实时在线推理需要一些关键组件:
-
一个超低延迟的、最好是内存中的特征存储。
-
一个事件处理、低延迟流处理引擎
-
用于与终端用户的网页和移动应用集成的 RESTful API
以下图示展示了一个实时推理管道的例子:

图 6.6 – 实时机器学习推理管道
在前面的图示中,数据从网页或移动应用实时到达消息队列,如 Apache Kafka。一个低延迟的事件处理引擎,如 Apache Flink,处理传入的特征并将其存储到 NoSQL 数据库(如 Apache Cassandra)或内存数据库(如 Redis)中,以便用于在线特征存储。机器学习推理引擎从在线特征存储中提取特征,生成预测,并通过 REST API 将预测推送回网页或移动应用。
注意
无论是 Apache Spark 还是基于云的数据湖中的 Delta Lake,都不适合用作在线特征存储。Spark 的结构化流处理被设计用来处理高吞吐量,而非低延迟处理。结构化流处理的微批处理不适合在事件到达源头时进行处理。通常,基于云的数据湖是为了可扩展性而设计的,并且具有延迟规范,因此 Delta Lake 无法满足在线特征存储对超低延迟的要求。
总结
在本章中,你了解了特征工程的概念,以及它为何是整个机器学习过程中的重要部分。此外,你还了解了为何需要创建特征并训练机器学习模型。
你探索了各种特征工程技术,如特征提取,以及它们如何将基于文本的数据转换为特征。介绍了在处理类别型和连续变量时有用的特征转换技术,并展示了如何将它们转换为特征的示例。你还探索了有助于归一化特征的特征缩放技术,以防止某些特征对训练模型产生过度偏倚。
最后,你了解了通过特征选择技术选择合适特征的方法,以优化预测标签的模型性能。本章所学的技能将帮助你使用 Apache Spark 实现可扩展且高效的特征工程管道,并利用 Delta Lake 作为特征的中央共享存储库。
在接下来的章节中,你将学习属于监督学习范畴的各种机器学习训练算法。此外,你将实现代码示例,利用本章生成的特征,在 Apache Spark MLlib 中训练实际的机器学习模型。
第七章:监督机器学习
在前两章中,你已经了解了机器学习过程、各个阶段以及该过程的第一步——特征工程。掌握了机器学习过程的基本知识,并拥有一组可用的机器学习特征后,你已准备好进入机器学习过程的核心部分——模型训练。
在本章中,你将接触到监督学习类别的机器学习算法,了解参数化和非参数化算法,掌握使用机器学习解决回归和分类问题所需的知识。最后,你将使用 Spark 机器学习库实现一些回归算法,如线性回归和决策树,以及一些分类算法,如逻辑回归、朴素贝叶斯和支持向量机。还将介绍树集成方法,这可以提高决策树的性能和准确性。本章还将展示回归和分类的几个现实世界应用,帮助你理解机器学习如何在日常场景中得到应用。
本章将涵盖以下主要主题:
-
监督学习简介
-
回归
-
分类
-
树集成
-
现实世界中的监督学习应用
到本章结束时,你应已掌握足够的知识和技能,能够使用 Spark MLlib 构建你自己的回归和分类模型并进行大规模训练。
技术要求
在本章中,我们将使用 Databricks Community Edition 来运行我们的代码(community.cloud.databricks.com)。
-
注册说明可以在
databricks.com/try-databricks找到。 -
本章的代码可以从
github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter07下载。 -
本章的数据集可以在
github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
监督学习简介
机器学习问题可以看作是一个过程,通过数学或统计函数从一组已知变量中推导出一个未知变量。不同之处在于,机器学习算法从给定的数据集中学习映射函数。
监督学习是一类机器学习算法,其中模型在一个数据集上进行训练,每一组输入的结果已经是已知的。这被称为监督学习,因为在此过程中,算法像教师一样引导训练,直到达到期望的模型性能水平。监督学习需要已经标注的数据。监督学习算法可以进一步分为参数化算法和非参数化算法。我们将在接下来的章节中详细讨论这些内容。
参数化机器学习
一种通过用一组固定参数总结数据来简化学习过程的机器学习算法称为参数化学习算法。它通过假设学习函数具有已知的形式,并从给定的数据集中学习线性函数的系数来实现这一点。学习函数的假定形式通常是线性函数或描述直线的代数方程。因此,参数化学习函数也被称为线性机器学习算法。
参数化学习算法的一个重要特性是,线性学习函数所需的参数数量与输入的训练数据集无关。这大大简化了学习过程,使得训练相对更快。这里的一个缺点是,给定数据集的潜在学习函数不一定是直线,因此可能会过度简化所学模型。然而,大多数实际的机器学习算法是参数化学习算法,例如线性回归、逻辑回归和朴素贝叶斯。
非参数化机器学习
非参数化学习算法不对学习函数的形式做任何假设。这些算法通过学习映射函数最大限度地利用训练数据集,同时保持对未见数据的适应能力。这意味着非参数化学习算法可以学习更广泛的学习函数。这些算法的优势在于它们灵活,并且能生成更优性能的模型,而劣势是通常需要更多的数据来学习,训练时间较慢,并且有时可能导致模型过拟合。一些非参数化学习算法的例子包括 K 最近邻、决策树和支持向量机。
监督学习算法有两个主要应用,即回归和分类。我们将在接下来的章节中探讨这些内容。
回归
回归是一种监督学习技术,它帮助我们学习一个称为标签的连续输出参数与一组输入参数(称为特征)之间的关系。回归生成的机器学习模型根据特征向量预测一个连续的标签。回归的概念可以通过以下图示来最好地解释:

图 7.1 – 线性回归
在前面的图示中,散点图表示分布在二维空间中的数据点。线性回归算法是一种参数化学习算法,它假设学习函数将呈线性形式。因此,它学习表示直线的系数,这条直线大致拟合散点图中的数据点。
Spark MLlib 提供了几种著名回归算法的分布式和可扩展实现,例如线性回归、决策树、随机森林和梯度提升树。在接下来的章节中,我们将使用 Spark MLlib 实现这些回归算法中的几个。
线性回归
在前几章中,我们清理、整合并策划了一个包含客户在线零售销售交易的数据集,并在同一整合数据集中捕获了他们的 demographic 信息。在 第六章,特征工程 – 提取、转换和选择 中,我们还将预处理的数据转换成了一个适合机器学习训练的特征向量,并将其存储在 Delta Lake 中,作为我们的离线 特征存储。让我们利用这个特征工程数据集来训练一个回归算法,利用其他特征作为参数预测客户的年龄,如下方代码块所示:
from pyspark.ml.regression import LinearRegression
retail_features = spark.read.table("retail_features")
train_df = retail_features.selectExpr("cust_age as label", "selected_features as features")
lr = LinearRegression(maxIter=10, regParam=0.3,
elasticNetParam=0.8)
lr_model = lr.fit(train_df)
print("Coefficients: %s" % str(lr_model.coefficients))
print("Intercept: %s" % str(lr_model.intercept))
summary = lr_model.summary
print("RMSE: %f" % summary.rootMeanSquaredError)
print("r2: %f" % summary.r2)
在前面的代码块中,我们做了以下操作:
-
首先,我们从 Spark MLlib 导入了
LinearRegression算法。 -
零售特征是从 Delta 表中加载的,并被加载到 Spark DataFrame 中。
-
我们只需要特征向量和标签列来训练一个
LinearRegression模型,因此我们仅在训练 DataFrame 中选择了这两列。 -
然后,我们通过指定该算法所需的超参数来初始化一个
LinearRegression变换器。 -
然后,我们在训练数据集上调用了
fit方法来启动训练过程,这会在后台启动一个 Spark 任务,分布式地执行训练任务。 -
一旦模型成功训练完成,我们打印了模型训练摘要,包括学习到的线性学习函数的系数和截距。
-
我们还显示了模型的准确性指标,例如 RMSE 和 R 平方值。通常,理想的情况是得到一个尽可能低的 RMSE 的模型。
因此,利用 Spark MLlib 的线性回归分布式实现,您可以在大型数据集上以分布式方式训练回归模型,而无需处理任何底层的分布式计算复杂性。然后,模型可以应用于新数据集,以生成预测。Spark MLlib 模型还可以使用内置方法持久化到磁盘或数据湖中,然后稍后重新使用。
现在,我们已经使用参数学习算法训练了一个简单的线性回归模型,让我们来看看如何使用非参数学习算法来解决同样的回归问题。
使用决策树进行回归
决策树是解决回归和分类机器学习问题的流行非参数学习算法。决策树之所以流行,是因为它们易于使用,能够处理各种分类和连续特征,同时也易于解释和说明。
Spark MLlib 的决策树实现通过按行划分数据来实现分布式训练。由于非参数学习算法通常需要大量数据,Spark 的决策树实现能够扩展到非常大规模的数据集,甚至是数百万或数十亿行。
让我们训练一个决策树模型,通过使用其他在线零售交易特征作为输入,预测客户的年龄,如下方代码块所示:
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.regression import DecisionTreeRegressor
retail_features = spark.read.table("retail_features").selectExpr("cust_age as label",
"selected_features as features")
(train_df, test_df) = retail_features.randomSplit([0.8, 0.2])
dtree = DecisionTreeRegressor(featuresCol="features")
model = dtree.fit(train_df)
predictions = model.transform(test_df)
evaluator = RegressionEvaluator(
labelCol="label", predictionCol="prediction",
metricName="rmse")
rmse = evaluator.evaluate(predictions)
print("RMSE for test data = %g" % rmse)
print(model.toDebugString)
在前面的代码片段中,我们完成了以下操作:
-
首先,我们导入了
DecisionTreeRegressorSpark ML 库,并引入了一个工具方法来帮助评估训练模型的准确性。 -
我们将特征向量数据集从 Delta Lake 加载到 Spark DataFrame,并只选择特征和标签列。
-
为了能够在训练过程后评估我们模型的准确性,我们需要一个不会用于训练的数据集。因此,我们将数据集分为训练集和测试集两部分。我们使用 80%的数据进行模型训练,同时保留 20%用于模型评估。
-
然后,我们用所需的超参数初始化了
DecisionTreeRegressor类,从而得到了一个Transformer对象。 -
我们将
DecisionTreeRegressor变换器拟合到我们的训练数据集,从而得到一个决策树模型估算器。 -
我们将模型的
Estimator对象应用于测试数据集,以生成实际预测结果。 -
随后,这个预测数据框(DataFrame)与
RegressionEvaluator工具方法一起使用,用于推导模型的 RMSE,该值可用于评估训练模型的准确性。
通过使用 Spark MLlib 内置的决策树回归算法,我们可以在非常大量的数据上,以分布式的方式快速高效地训练回归模型。需要注意的一点是,这两个回归模型的 RMSE 值大致相同。这些模型可以通过模型调优技术进一步调优,以提高其准确性。你将会在第九章,机器学习生命周期管理中学到更多关于模型调优的内容。
分类
分类是另一种监督学习技术,其任务是将给定的数据集分类到不同的类别中。机器学习分类器从输入参数(称为特征)中学习映射函数,输出一个离散的输出参数(称为标签)。在这里,学习函数尝试预测标签是否属于几个已知类别中的一个。下图展示了分类的概念:

图 7.2 – 逻辑回归
在前面的图示中,逻辑回归算法正在学习一个映射函数,将二维空间中的数据点分成两个不同的类别。学习算法学习Sigmoid 函数的系数,该函数将一组输入参数分类为两个可能类别之一。这种分类方法可以分为两个不同的类别,这就是二分类或二项分类。
逻辑回归
逻辑回归是一种流行的分类算法,可以从标记数据中学习模型,以预测输出变量的类别。Spark MLlib 实现的逻辑回归支持二项分类和多项分类问题。
二项分类
二项分类或二分类是指学习算法需要判断输出变量是否属于两个可能结果之一。基于前面章节的示例,我们将使用逻辑回归训练一个模型,尝试根据在线零售交易中的其他特征预测顾客的性别。让我们看看如何使用 Spark MLlib 来实现这一点,代码示例如下:
from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer
from pyspark.ml.classification import LogisticRegression
train_df = spark.read.table("retail_features").selectExpr("gender", "selected_features as features")
string_indexer = StringIndexer(inputCol="gender",
outputCol="label",
handleInvalid="skip" )
lr = LogisticRegression(maxIter=10, regParam=0.9,
elasticNetParam=0.6)
pipeline = Pipeline(stages=[string_indexer, lr])
model = pipeline.fit(train_df)
lr_model = model.stages[1]
print("Coefficients: " + str(lr_model.coefficientMatrix))
print("Intercepts: " + str(lr_model.interceptVector))
summary.roc.show()
print("areaUnderROC: " + str(summary.areaUnderROC))
在前面的代码块中,我们做了以下操作:
-
我们的训练数据集中,性别是一个字符串数据类型,因此首先需要将其转换为数字格式。为此,我们使用了
StringIndexer将其转换为一个数字标签列。 -
然后,我们通过指定该算法所需的超参数,初始化了
LogisticRegression类。 -
接着,我们将
StringIndexer和LogisticRegression阶段串联在一起,形成一个管道。 -
然后,我们在训练数据集上调用
fit方法,开始使用管道中的Transformer对象进行训练过程。 -
一旦模型成功训练,我们打印了模型的系数和截距,并且展示了接收器操作特征曲线(ROC)以及 ROC 曲线下的面积(AUC)指标,以衡量训练模型的准确性。
这样,我们已经展示了如何利用 Spark 机器学习库中的逻辑回归算法,以可扩展的方式实现二分类。
多项分类
在多项分类中,学习算法需要预测多个可能的结果。让我们从前一节的示例出发,扩展一个模型,使用逻辑回归来预测一个客户的来源国家,基于来自在线零售交易的其他特征,如下面的代码片段所示:
train_df = spark.read.table("retail_features").selectExpr("country_indexed as label", "selected_features as features")
mlr = LogisticRegression(maxIter=10, regParam=0.5,
elasticNetParam=0.3,
family="multinomial")
mlr_model = mlr.fit(train_df)
print("Coefficients: " + str(mlr_model.coefficientMatrix))
print("Intercepts: " + str(mlr_model.interceptVector))
print("areaUnderROC: " + str(summary.areaUnderROC))
summary.roc.show()
前面的代码片段几乎与二分类示例相同,唯一的区别是标签列具有多个可能值,并且我们为 LogisticRegression 类指定了 multinomial 的 family 参数。一旦模型训练完成,可以通过显示模型的接收者操作特征(ROC)和 ROC 曲线下的面积来衡量模型的准确性。
使用决策树进行分类
Spark MLlib 提供了一个 DecisionTreeClassifier 类来解决分类问题。在下面的代码中,我们将使用决策树实现二分类:
retail_df = spark.read.table("retail_features").selectExpr("gender", "selected_features as features")
(train_df, test_df) = retail_df.randomSplit([0.8, 0.2])
string_indexer = StringIndexer(inputCol="gender",
outputCol="label",
handleInvalid="skip" )
dtree = DecisionTreeClassifier(labelCol="label",
featuresCol="features")
pipeline = Pipeline(stages=[string_indexer, dtree])
model = pipeline.fit(train_df)
predictions = model.transform(test_df)
evaluator = MulticlassClassificationEvaluator(
labelCol="label", predictionCol="prediction",
metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Accuracy = %g " % (accuracy))
dtree_model = model.stages[1]
#print(dtree_model.toDebugString)
在前面的代码块中,我们做了以下操作:
-
首先,我们将数据集分为两个集合,用于训练和测试。这使我们能够在训练完成后评估模型的准确性。
-
然后,我们使用了
StringIndexer将性别字符串列转换为数值标签列。 -
之后,我们初始化了一个带有所需超参数的
DecisionTreeClassifier类。 -
然后,我们将
StringIndexer和DecisionTreeClassifier阶段组合成一个管道。 -
之后,我们在训练数据集上调用了
fit方法开始模型训练过程,并将模型的Estimator对象应用于测试数据集以计算预测值。 -
最后,我们使用这个 DataFrame,并结合
MulticlassClassificationEvaluator工具方法,推导出训练模型的准确性。
通过这种方式,我们展示了如何使用 Spark 机器学习库的决策树来解决大规模的分类问题。
朴素贝叶斯
朴素贝叶斯是基于贝叶斯定理的概率分类算法家族,它假设输入学习算法的特征之间是独立的。贝叶斯定理可以用来预测一个事件发生的概率,前提是另一个事件已经发生。朴素贝叶斯算法计算给定输入特征集的所有可能输出标签类别的概率,然后选择具有最大概率的输出。朴素贝叶斯可以用于二项分类问题以及多项分类问题。让我们来看一下如何使用 Spark MLlib 实现朴素贝叶斯,如下面的代码示例所示:
from pyspark.ml.classification import NaiveBayes
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
retail_df = spark.read.table("retail_features").selectExpr("gender", "selected_features as features")
(train_df, test_df) = retail_df.randomSplit([0.8, 0.2])
string_indexer = StringIndexer(inputCol="gender",
outputCol="label",
handleInvalid="skip" )
nb = NaiveBayes(smoothing=0.9, modelType="gaussian")
pipeline = Pipeline(stages=[string_indexer, nb])
model = pipeline.fit(train_df)
predictions = model.transform(test_df)
evaluator = MulticlassClassificationEvaluator(
labelCol="label",
predictionCol="prediction",
metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Model accuracy = %f" % accuracy)
在前面的代码块中,我们做了以下操作:
-
首先,我们将数据集分别分为训练集和测试集。
-
然后,我们使用了
StringIndexer将性别字符串列转换为数值标签列。 -
之后,我们初始化了一个带有所需超参数的
NaiveBayes类。 -
然后我们将
StringIndexer和NaiveBayes阶段合并到一个管道中。 -
然后,我们在训练数据集上调用了
fit方法,以启动模型训练过程,将模型的Estimator对象应用到测试数据集上进行预测计算。 -
然后,将这个 DataFrame 与
MulticlassClassificationEvaluator工具方法一起使用,以推导训练模型的准确性。注意
多项式和伯努利朴素贝叶斯模型需要非负特征。因此,建议只选择具有正值的特征,或者使用其他能够处理非负值特征的分类算法。
支持向量机
支持向量机 (SVM) 是一类分类算法,它以数据点作为输入,输出最佳分隔给定数据点的超平面,将数据点分为两个不同的类别,并在二维平面上表示。因此,SVM 仅支持二分类问题。让我们使用 Spark MLlib 的 SVM 实现来实现二分类,如以下代码块所示:
from pyspark.ml.classification import LinearSVC
train_df = spark.read.table("retail_features").selectExpr("gender", "selected_features as features")
string_indexer = StringIndexer(inputCol="gender",
outputCol="label",
handleInvalid="skip" )
svm = LinearSVC(maxIter=10, regParam=0.1)
pipeline = Pipeline(stages=[string_indexer, svm])
model = pipeline.fit(train_df)
svm_model = model.stages[1]
# Print the coefficients and intercept for linear SVC
print("Coefficients: " + str(svm_model.coefficients))
print("Intercept: " + str(svm_model.intercept))
在前面的代码块中,我们做了以下操作:
-
首先,我们使用了
StringIndexer将性别列转换为数值标签列。 -
然后,我们通过指定此算法所需的超参数来初始化
LinearSVC类。 -
然后,我们将
StringIndexer和LinearSVC阶段合并到一个管道中。 -
然后,我们在训练数据集上调用了
fit方法,以开始使用管道的Transformer对象进行训练过程。 -
一旦模型成功训练,我们打印了模型的系数和截距。
到目前为止,你已经了解了最流行的有监督学习算法,用于解决回归和分类问题,并通过实际代码示例看到它们在 Spark MLlib 中的实现。在接下来的章节中,你将了解树集成的概念,以及如何使用树集成方法将多个决策树模型结合起来,以获得最佳模型。
树集成
非参数化学习算法,如决策树,不对学习函数的形式做任何假设,而是尝试将模型拟合到手头的数据。然而,决策树可能会出现过拟合训练数据的风险。树集成方法是利用决策树优势同时最小化过拟合风险的好方法。树集成方法将多个决策树结合起来,从而生成表现更好的预测模型。一些常见的树集成方法包括随机森林和梯度提升树。我们将探讨如何使用这些集成方法通过 Spark MLlib 构建回归和分类模型。
使用随机森林进行回归
随机森林构建多个决策树并将它们合并,从而生成一个更准确的模型并减少过拟合的风险。随机森林可以用于训练回归模型,如以下代码示例所示:
from pyspark.ml.regression import RandomForestRegressor
from pyspark.ml.evaluation import RegressionEvaluator
retail_features = spark.read.table("retail_features").selectExpr("cust_age as label", "selected_features as features")
(train_df, test_df) = retail_features.randomSplit([0.8, 0.2])
rf = RandomForestRegressor(labelCol="label",
featuresCol="features",
numTrees=5)
rf_model = rf.fit(train_df)
predictions = rf_model.transform(test_df)
evaluator = RegressionEvaluator(
labelCol="label", predictionCol="prediction",
metricName="rmse")
rmse = evaluator.evaluate(predictions)
print("RMSE for test data = %g" % rmse)
print(rf_model.toDebugString)
在前面的代码片段中,我们做了以下操作:
-
首先,我们将数据集拆分为两个子集,分别用于训练和测试。
-
然后,我们初始化
RandomForestRegressor类,并设置多个树进行训练。我们将其设置为5。 -
接下来,我们将
RandomForestRegressor转换器应用于训练数据集,以获得一个随机森林模型。 -
然后,我们将模型的
Estimator对象应用于测试数据集,以生成实际的预测结果。 -
然后,这个 DataFrame 被用在
RegressionEvaluator工具方法中,以得出RMSE值。 -
最后,我们使用模型对象的
toDebugString属性打印训练好的随机森林。
使用随机森林进行分类
就像决策树一样,随机森林也支持训练多类分类模型,如下面的代码块所示:
retail_df = spark.read.table("retail_features").selectExpr("gender", "selected_features as features")
(train_df, test_df) = retail_df.randomSplit([0.8, 0.2])
string_indexer = StringIndexer(inputCol="gender",
outputCol="label",
handleInvalid="skip" )
rf = RandomForestClassifier(labelCol="label",
featuresCol="features",
numTrees=5)
pipeline = Pipeline(stages=[string_indexer, rf])
model = pipeline.fit(train_df)
predictions = model.transform(test_df)
evaluator = MulticlassClassificationEvaluator(
labelCol="label", predictionCol="prediction",
metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Accuracy = %g " % (accuracy))
rf_model = model.stages[1]
print(rf_model.toDebugString)
在前面的代码片段中,我们做了以下操作:
-
首先,我们将数据集拆分为两个子集,分别用于训练和测试。这将使我们能够在训练完成后评估模型的准确性。
-
我们利用
StringIndexer将性别字符串列转换为数值标签列。 -
然后,我们初始化一个
RandomForestClassifier类,设置所需的超参数,并指定训练的决策树数量为5。 -
然后,我们将
StringIndexer和RandomForestClassifier阶段合并到一个管道中。 -
然后,我们在训练数据集上调用
fit方法,开始模型训练过程,并将模型的Estimator对象应用于测试数据集,以计算预测结果。 -
然后,这个 DataFrame 被用在
MulticlassClassificationEvaluator工具方法中,以得出训练模型的准确性。 -
随机森林模型也可以使用
toDebugString属性进行打印,模型对象上可以访问该属性。
这样,机器学习分类就可以通过 Spark 机器学习库在大规模上实现。
使用梯度提升树进行回归
梯度提升树(GBTs)是另一种基于决策树的集成方法,它也能提高训练模型的稳定性和准确性,同时最小化过拟合的风险。GBTs 通过梯度提升的过程,迭代训练多个决策树,同时最小化损失函数。让我们通过下面的代码示例,探讨如何在 Spark 中使用 GBTs 训练回归模型:
from pyspark.ml.regression import GBTRegressor
from pyspark.ml.evaluation import RegressionEvaluator
retail_features = spark.read.table("retail_features").selectExpr("cust_age as label", "selected_features as features")
(train_df, test_df) = retail_features.randomSplit([0.8, 0.2])
gbt = GBTRegressor(labelCol="label",featuresCol="features",
maxIter=5)
gbt_model = gbt.fit(train_df)
predictions = gbt_model.transform(test_df)
evaluator = RegressionEvaluator(
labelCol="label", predictionCol="prediction",
metricName="rmse")
rmse = evaluator.evaluate(predictions)
print("RMSE for test data = %g" % rmse)
print(gbt_model.toDebugString)
在前面的代码片段中,我们做了以下操作:
-
首先,我们将数据集拆分为两个子集,分别用于训练和测试。
-
然后,我们初始化
GBTRegressor类,并将最大迭代次数设置为5。 -
接下来,我们将
RandomForestRegressor转换器应用于训练数据集。这导致了一个随机森林模型估算器。之后,我们将要训练的树的数量设置为5。 -
然后,我们将模型的
Estimator对象应用于测试数据集,以生成实际的预测结果。 -
然后,我们使用
RegressionEvaluator工具方法对这个 DataFrame 进行处理,以得出RMSE值。 -
训练后的随机森林也可以通过模型对象的
toDebugString属性打印出来。
通过这种方式,可以使用 Spark MLlib 中的 GBT 算法在大规模上实现回归。
使用 GBT 进行分类
GBT 也可以用来训练分类模型,如以下代码示例所示:
retail_df = spark.read.table("retail_features").selectExpr("gender", "selected_features as features")
(train_df, test_df) = retail_df.randomSplit([0.8, 0.2])
string_indexer = StringIndexer(inputCol="gender",
outputCol="label",
handleInvalid="skip" )
gbt = GBTClassifier(labelCol="label",
featuresCol="features",
maxIter=5)
pipeline = Pipeline(stages=[string_indexer, gbt])
model = pipeline.fit(train_df)
predictions = model.transform(test_df)
evaluator = MulticlassClassificationEvaluator(
labelCol="label", predictionCol="prediction",
metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Accuracy = %g " % (accuracy))
gbt_model = model.stages[1]
print(gbt_model.toDebugString)
在前面的代码片段中,我们做了以下操作:
-
首先,我们使用
StringIndexer将性别字符串列转换为数字标签列。 -
接着,我们初始化了
GBTClassifier类,并将要训练的决策树数量设置为5。 -
然后,我们将
StringIndexer和RandomForestClassifier阶段合并到一个管道中。 -
之后,我们在训练数据集上调用
fit方法,开始模型训练过程,并将模型的Estimator对象应用到测试数据集上以计算预测值。 -
然后,我们使用
MulticlassClassificationEvaluator工具方法对这个 DataFrame 进行处理,以得出训练模型的准确度。
到目前为止,您已经探索了如何使用树集成方法将多个决策树结合起来,生成更好、更准确的机器学习模型,以解决回归和分类问题。在接下来的部分中,您将了解一些可以应用于日常场景的机器学习分类和回归模型的真实世界应用。
真实世界的监督学习应用
过去,数据科学和机器学习仅用于学术研究。然而,在过去的十年里,这一领域已在实际商业应用中找到了用途,帮助企业寻找竞争优势、提升整体业务表现并实现盈利。在本节中,我们将探讨一些机器学习的真实世界应用。
回归应用
本节将介绍机器学习回归模型的一些应用及其如何帮助改善业务表现。
客户生命周期价值估算
在任何零售或消费品行业中,客户流失率是一个重要因素,因此必须将营销预算定向到那些具有盈利潜力的客户。在非订阅型业务中,通常 20%的客户群体贡献了 80%的收入。可以利用机器学习模型来模拟并预测每个客户的生命周期价值。客户生命周期价值(CLV)模型有助于预测一个客户的预期生命周期,这是衡量我们预计客户还能盈利多久的一个指标。CLV 模型还可以预测单个客户在其预期生命周期内可能产生的收入。因此,回归模型可以用来估算 CLV,并帮助将营销预算集中用于吸引和留住那些在预期生命周期内具有盈利潜力的客户。
货运提前期估算
零售商、物流公司、餐饮服务聚合商或任何需要将产品交付给客户的企业,都需要能够预测将产品送达客户所需的时间。回归模型可以用来考虑诸如起始和目的地邮政编码、这两个地点之间过往的运输表现、库存可用性,以及季节性、天气状况甚至当地交通等因素,从而建立模型,估算产品到达客户所需的时间。这有助于企业进行库存优化、供应链规划,甚至提升整体客户满意度。
动态定价优化
动态定价优化,也称为动态定价,是根据当前产品需求或市场状况为产品或服务设定价格的过程。这在多个行业中都是一种常见做法,包括交通、旅游和酒店业、电子商务、娱乐业以及数字聚合平台等。企业可以利用数字经济中产生的大量数据,通过实时调整价格来优化定价。虽然动态定价是一个优化问题,回归模型可以用来预测特定时刻的价格、当前需求、市场状况以及竞争对手的定价。
分类应用
本节将讨论一些分类模型如何应用于解决业务场景的示例。
金融欺诈检测
金融欺诈和身份盗窃是金融行业面临的最大挑战之一。金融机构历来使用统计模型和基于规则的引擎来检测金融欺诈;然而,欺诈分子已通过使用新型欺诈手段绕过传统的欺诈检测机制。分类模型可以使用一些简单的算法,例如朴素贝叶斯,或者一些更为复杂的方法,例如决策树集成方法来构建。这些模型可以帮助企业应对新兴的欺诈模式,并将金融交易标记为欺诈交易。
邮件垃圾信息检测
这是任何使用电子邮件的人都曾经历过的常见场景:即收到不需要的、推销性质的或有时甚至是令人反感的电子邮件内容。电子邮件提供商正在使用分类模型来将电子邮件分类,并将垃圾邮件标记出来,从而将其排除在用户的收件箱之外。
工业机械设备故障预测
油气和建筑等重工业公司已经在其重型工业设备上安装或开始安装物联网设备,这些设备不断向后台服务器发送遥测和诊断数据。经过训练的分类模型可以帮助预测机器故障,帮助行业防止停机,标记出问题的辅助零部件供应商,甚至通过防止大规模的机械召回来节省巨额成本。
物体检测
分类模型一直是高端相机的组成部分,这些相机内置了物体跟踪和自动对焦功能。现代手机应用程序也利用分类模型来将照片中的主体与背景分离,以及识别和标记照片中的人物。
总结
在本章中,我们介绍了一类叫做监督学习算法的机器学习算法,它可以从已标记的现有数据中学习。你探讨了参数化和非参数化学习算法的概念及其优缺点。还介绍了监督学习算法的两个主要应用场景——回归和分类。通过模型训练示例以及来自 Spark MLlib 的代码,我们探索了几种常见的回归和分类模型。同时,介绍了树集成方法,这些方法通过结合多个模型并防止过拟合,提升了决策树模型的稳定性、准确性和性能。
最后,你探索了本章中介绍的各种机器学习模型在现实世界商业中的应用。我们解释了如何利用监督学习来解决商业用例,并提供了工作代码示例,帮助你使用 Spark MLlib 在大规模上训练模型并高效解决商业问题。
在下一章中,我们将探索无监督机器学习算法,了解它们与监督学习模型的区别,以及它们在解决现实世界商业问题中的应用。我们还将提供工作代码示例来展示这一点。
第八章:无监督机器学习
在前两章中,你已经了解了监督学习类的机器学习算法、它们的实际应用,以及如何使用 Spark MLlib 在大规模环境中实现它们。在本章中,你将接触到无监督学习类别的机器学习,学习有关参数化和非参数化的无监督算法。为了帮助你理解 无监督学习 在解决实际问题中的应用,我们将介绍一些 聚类 和 关联 算法的实际应用。你将获得使用无监督机器学习进行聚类和关联问题的基本知识和理解。我们还将讨论在 Spark ML 中实现一些聚类算法的细节,如 K-means 聚类、层次聚类、潜在狄利克雷分配,以及一种叫做 交替最小二乘法 的关联算法。
在本章中,我们将讨论以下主要内容:
-
无监督学习简介
-
使用机器学习进行聚类
-
使用机器学习建立关联
-
无监督学习的实际应用
到本章结束时,你应该已经获得了足够的知识和实践经验,了解聚类和关联类型的无监督机器学习算法、它们的实际应用,并能够使用 Spark MLlib 在大规模环境中实现这些类型的算法。
技术要求
在本章中,我们将使用 Databricks 社区版来运行我们的代码(community.cloud.databricks.com)。
-
注册说明可以在
databricks.com/try-databricks找到。 -
本章的代码可以从
github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter08下载。 -
本章的 datasets 可以在
github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
无监督机器学习简介
无监督学习是一种机器学习技术,在这种技术中,学习算法在训练数据中没有已知标签值的指导。无监督学习在根据数据中固有的模式、相似性或差异将未知数据点分组时非常有用,而无需任何先验数据知识。
在监督学习中,模型是在已知数据上进行训练的,然后使用新数据(未见过的数据)从模型中推断结论。另一方面,在无监督学习中,模型训练过程本身就是最终目标,在模型训练过程中会发现隐藏在训练数据中的模式。与监督学习相比,无监督学习更难,因为在没有任何外部评估的情况下,很难确定无监督学习算法的结果是否有意义,特别是当无法访问任何正确标记的数据时。
无监督学习的一个优势是,它有助于解释非常大的数据集,在这些数据集上标注现有数据并不现实。无监督学习还可用于预测数据集中类别的数量,或在应用监督学习算法之前对数据进行分组和聚类。它在解决分类问题时也非常有用,因为无监督学习可以很好地处理未标记的数据,且无需任何人工干预。无监督学习可以分为两种主要的学习技术,分别是聚类和关联。接下来的部分将介绍这两种方法。
使用机器学习进行聚类
在机器学习中,聚类是指在不需要任何外部指导的情况下识别未分类数据中的模式或结构。聚类算法会解析给定的数据,以识别数据集中具有匹配模式的簇或数据组。聚类算法的结果是数据簇,这些簇可以定义为在某种方式上相似的对象集合。下图展示了聚类是如何工作的:

图 8.1 – 聚类
在前面的图示中,一个未分类的数据集通过聚类算法进行处理,结果是根据数据点与另一个数据点在二维欧几里得空间中的接近程度,将数据分类成较小的簇或数据组。
因此,聚类算法基于二维平面上数据之间的欧几里得距离进行数据分组。聚类算法会考虑训练数据集中数据点之间的欧几里得距离,在同一簇内,数据点之间的距离应当较小,而簇外,数据点之间的距离应当较大。接下来的部分将介绍 Spark MLlib 中几种可用的聚类技术。
K-means 聚类
K-means 是最流行的聚类算法,也是最简单的无监督学习算法之一。K-means 聚类算法在给定数据集上以迭代的方式工作,将其分为 k 组。k 的值越大,聚类的大小越小,反之亦然。因此,使用 K-means,用户可以控制在给定数据集内识别的聚类数量。
在 K-means 聚类中,每个簇都是通过为每个簇创建一个中心来定义的。这些质心尽可能远离彼此。然后,K-means 将每个数据点与给定数据集中的最近质心进行关联,从而形成第一个簇。K-means 随后会迭代地重新计算质心在数据集中的位置,使其尽可能接近已识别簇的中心。当质心不再需要移动时,过程停止。
以下代码块演示了如何使用 Spark MLlib 实现 K-means 聚类:
from pyspark.ml.clustering import KMeans
from pyspark.ml.evaluation import ClusteringEvaluator
retail_features = spark.read.table("retail_features")
train_df = retail_features.selectExpr("selected_features as features")
kmeans = KMeans(k=3, featuresCol='features')
kmeans_model = kmeans.fit(train_df)
predictions = kmeans_model.transform(train_df)
evaluator = ClusteringEvaluator()
silhouette = evaluator.evaluate(predictions)
print("Silhouette measure using squared Euclidean distance = " + str(silhouette))
cluster_centers = kmeans_model.clusterCenters()
print(cluster_centers)
在前面的代码片段中,我们进行了以下操作:
-
首先,我们使用
import导入了与聚类和聚类评估相关的适当 MLlib 包。 -
然后,我们将之前在特征工程过程中获得的现有特征向量导入到 Spark DataFrame 中,并以 Delta 格式将其存储在数据湖中。
-
接下来,我们初始化了一个新的
KMeans对象,通过传入所需的聚类数和特征向量的列名来进行设置。 -
我们在训练 DataFrame 上调用了
fit()方法,启动了学习过程。结果生成了一个模型对象。 -
通过调用模型对象的
transform()方法,生成了原始训练数据集上的预测结果。 -
接下来,我们调用了 Spark MLlib 的
ClusteringEvaluator()辅助函数,该函数用于评估聚类算法,并将其应用于我们在前一步生成的预测 DataFrame。这会得到一个被称为silhouette的值,它是衡量聚类一致性的指标,计算方式基于数据点之间的欧氏距离度量。silhouette值越接近1,表示簇内的数据点越聚集,簇外的数据点则相距较远。silhouette值越接近1,表示学习到的模型性能越好。 -
最后,我们打印了每个分类簇的质心。
这样,只需几行代码,就可以通过 Spark 实现的 K-means 聚类算法轻松地将未分类数据进行聚类。
使用二分 K-means 的层次聚类
层次聚类是一种聚类技术,其中所有数据点从一个单一的聚类开始。然后它们通过递归地向下分割到更小的聚类中,从而构成一个层次结构。Spark ML 通过二分 K 均值算法实现这种分裂式的层次聚类。以下示例说明了如何使用 Spark MLlib 实现二分 K 均值聚类:
from pyspark.ml.clustering import BisectingKMeans
from pyspark.ml.evaluation import ClusteringEvaluator
retail_features = spark.read.table("retail_features")
train_df = retail_features.selectExpr("selected_features as features")
bkmeans = BisectingKMeans(k=3, featuresCol='features')
bkmeans_model = kmeans.fit(train_df)
predictions = bkmeans_model.transform(train_df)
evaluator = ClusteringEvaluator()
silhouette = evaluator.evaluate(predictions)
print("Silhouette measure using squared euclidean distance = " + str(silhouette))
cluster_centers = kmeans_model.clusterCenters()
print(cluster_centers)
在之前的代码片段中,我们执行了以下操作:
-
首先,我们通过传入所需聚类的数量和特征列的列名来初始化一个新的
BisectingKMeans对象。 -
我们在训练数据集上调用了
fit()方法以开始学习过程。作为结果,生成了一个模型对象。 -
接下来,我们通过在模型对象上调用
transform()方法,对原始训练数据集生成了预测结果。 -
然后,我们调用了 Spark MLlib 的
ClusteringEvaluator()辅助函数,该函数对于评估聚类算法非常有用,并将其应用于我们在前一步生成的预测数据框。这将得到silhouette值,它是衡量聚类内一致性的指标,基于数据点之间的欧几里得距离计算。 -
最后,我们打印了每个聚类的中心点。
现在我们已经学习了一种聚类技术,让我们在下一节中了解另一种学习技术。
使用潜在狄利克雷分配进行主题建模
主题建模是一种学习技术,通过它你可以对文档进行分类。主题建模与主题分类不同,因为主题分类是一种有监督学习技术,其中学习模型尝试根据一些先前标注的数据对未见过的文档进行分类。而主题建模则像聚类算法对数值数据进行分组一样,在没有任何外部指导的情况下对包含文本或自然语言的文档进行分类。因此,主题建模是一个无监督学习问题。
潜在狄利克雷分配(LDA)是一种流行的主题建模技术。LDA 的目标是根据文档中发现的关键词将给定文档与特定主题关联起来。在这里,主题是未知的,隐藏在文档中,这就是 LDA 中的潜在部分。LDA 的工作方式是,假设文档中的每个单词都属于一个不同的主题,并为每个单词分配一个概率值。一旦估算出每个单词属于特定主题的概率,LDA 就会通过设置阈值并选择所有符合或超过该阈值的单词来挑选属于某个主题的所有单词。LDA 还认为每个文档只是一个词袋,并不重视单个单词在语法中的角色。此外,像文章、连词和感叹词等停用词需要在应用 LDA 之前被去除,因为这些词并不携带任何主题信息。
以下代码示例说明了如何使用 Spark MLlib 实现 LDA:
from pyspark.ml.clustering import LDA
train_df = spark.read.table("retail_features").selectExpr("selected_features as features")
lda = LDA(k=3, maxIter=1)
lda_model = lda.fit(train_df)
topics = lda_model.describeTopics(3)
topics.show()
transformed = lda_model.transform(dataset)
transformed.show()
在前面的代码片段中,我们做了以下操作:
-
首先,我们导入了与 LDA 相关的适当 MLlib 包。
-
接下来,我们将特征工程过程中生成的现有特征向量导入到 Spark DataFrame 中,并以 Delta 格式将其存储在数据湖中。
-
之后,我们通过传入聚类的数量和最大迭代次数来初始化一个新的
LDA对象。 -
接下来,我们在训练 DataFrame 上调用了
fit()方法,开始了学习过程。最终生成了一个模型对象。 -
使用 LDA 算法建模的主题可以通过在模型对象上使用
describeTopics()方法来显示。
正如我们所见,通过使用 Apache Spark 实现的 LDA 算法,主题建模可以在大规模上实现。
高斯混合模型
K-means 聚类的一个缺点是它会将每个数据点与一个特定的聚类关联起来。这样,就无法获得数据点属于某一特定聚类的概率。高斯混合模型(GSM)尝试解决 K-means 聚类的硬聚类问题。
GSM 是一个概率模型,用于表示数据点总体样本中的一个子集。GSM 表示多个高斯分布的数据点的混合,其中数据点从其中一个K高斯分布中抽取,并具有属于这些分布之一的概率得分。
以下代码示例描述了使用 Spark ML 实现 GSM 的实现细节:
from pyspark.ml.clustering import GaussianMixture
train_df = spark.read.table("retail_features").selectExpr("selected_features as features")
gmm = GaussianMixture(k=3, featuresCol='features')
gmm_model = gmm.fit(train_df)
gmm_model.gaussiansDF.display()
在前面的代码块中,我们在从pyspark.ml.clustering包中导入适当的库后,初始化了一个新的GaussianMixture对象。然后,我们传入了一些超参数,包括聚类的数量和包含特征向量的列的名称。接着,我们使用fit()方法训练了模型,并通过模型的gaussianDF属性显示了训练后的结果。
到目前为止,您已经看到了不同类型的聚类和主题建模技术及其在使用 Spark MLlib 时的实现。在接下来的部分中,您将了解另一种叫做关联规则的无监督学习算法。
使用机器学习构建关联规则
if-then-else语句有助于展示实体之间关系的概率。关联规则技术广泛应用于推荐系统、市场篮子分析和亲和力分析等问题。
使用交替最小二乘法进行协同过滤
在机器学习中,协同过滤更常用于 推荐系统。推荐系统是一种通过考虑用户偏好来过滤信息的技术。根据用户的偏好并考虑他们的过去行为,推荐系统可以预测用户可能喜欢的项。协同过滤通过利用历史用户行为数据和他们的偏好,构建用户-项关联矩阵来执行信息过滤。Spark ML 使用 交替最小二乘法 算法实现协同过滤技术。
以下代码示例演示了 Spark MLlib 中交替最小二乘算法的实现:
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.recommendation import ALS
from pyspark.sql import Row
ratings_df = (spark.read.table("retail_features").selectExpr(
"CAST(invoice_num AS INT) as user_id",
"CAST(stock_code AS INT) as item_id",
"CAST(quantity AS INT) as rating")
.where("user_id is NOT NULL AND item_id is NOT NULL"))
df.display()
(train_df, test_df) = ratings_df.randomSplit([0.7, 0.3])
als = ALS(maxIter=3, regParam=0.03, userCol="user_id",
itemCol="item_id", ratingCol="rating",
coldStartStrategy="drop")
als_model = als.fit(train_df)
predictions = model.transform(test_df)
evaluator = RegressionEvaluator(metricName="rmse",
labelCol="rating",
predictionCol="prediction")
rmse = evaluator.evaluate(predictions)
print("Root-mean-square error = " + str(rmse))
user_recs = als_model.recommendForAllUsers(5)
user_recs.show()
item_recs = als_model.recommendForAllItems(5)
item_recs.show()
在前面的代码块中,我们做了以下操作:
-
首先,我们使用存储在 Delta Lake 中的特征数据集生成了一个 Spark DataFrame 作为评分数据集。ALS 算法所需的几个列,如
user_id、item_id和ratings,并未采用所需的整数格式。因此,我们使用了CASTSpark SQL 方法将其转换为所需的数据格式。 -
接下来,我们使用所需的参数初始化了一个 ALS 对象,并通过
randomSplit()方法将训练数据集随机分成了两部分。 -
然后,我们通过在训练数据集上调用
fit()方法启动了学习过程。 -
接下来,我们使用 Spark MLlib 提供的评估器评估了准确度指标的
RMSE。 -
最后,我们通过内置的
recommendForAllUsers()和recommendForAllItems()方法分别收集了每个用户的前 5 个推荐项和每个项的前 5 个用户推荐。
通过这种方式,你可以利用交替最小二乘法构建推荐系统,用于诸如 视频点播 平台的电影推荐、产品推荐或电子商务应用中的 市场篮分析 等用例。Spark MLlib 使你能够只用几行代码就实现这一规模。
除了聚类和关联规则外,Spark MLlib 还允许你实现 降维 算法,如 奇异值分解(SVD)和 主成分分析(PCA)。降维是减少考虑的随机变量数量的过程。尽管它是一种无监督学习方法,但降维在特征提取和选择中非常有用。关于这一主题的详细讨论超出了本书的范围,Spark MLlib 仅为 RDD API 提供了降维算法的实现。有关降维的更多详细信息,可以在 Apache Spark 的公共文档中找到,网址为 spark.apache.org/docs/latest/mllib-dimensionality-reduction.html。
在下一节中,我们将深入探讨一些当前各大企业正在使用的无监督学习算法的实际应用。
无监督学习的实际应用
无监督学习算法如今正被用来解决一些现实世界中的商业挑战。在本节中,我们将探讨几个这样的挑战。
聚类应用
本节介绍了一些聚类算法在实际商业中的应用。
客户细分
零售营销团队以及面向消费者的企业组织,始终致力于优化其营销支出。尤其是营销团队,他们关注一个特定的指标——每次获取成本(CPA)。CPA 表示组织需要花费多少才能获得一个客户,最优的 CPA 意味着更好的营销投资回报。优化 CPA 的最佳方式是通过客户细分,因为这能提高营销活动的效果。传统的客户细分会考虑标准的客户特征,如人口统计、地理位置和社会信息,以及历史交易数据,从而定义标准的客户群体。然而,传统的客户细分方法费时费力,且容易出错。与此同时,可以利用机器学习算法发现数据源之间隐藏的模式和关联。近年来,客户接触点的数量大幅增加,手动识别所有这些接触点之间的模式变得不实际且不直观。然而,机器学习算法能够轻松地分析数百万条记录,提取出营销团队可以迅速利用的见解,从而满足客户的需求,在客户需要的时候、他们想要的地方。因此,通过利用聚类算法,营销人员可以通过更加精准的客户细分,提高营销活动的有效性。
零售产品组合优化
拥有实体店面的零售商面临有限的店铺空间。因此,他们需要确保通过放置那些最有可能销售的产品来实现店铺空间的最佳利用。一个经典的商品组合优化案例是美国中西部地区的一个五金零售商,在寒冷的冬季进货草坪割草机,当时雪季几乎在整个冬季都会持续。在这个例子中,店铺空间没有得到最佳利用,因为草坪割草机在雪季几乎没有销售机会。更好的选择应该是空间加热器、雪铲或其他冬季用品。为了解决这个问题,零售商通常会雇佣分析师,结合历史交易数据、季节性变化和当前趋势,提出适合季节和商店位置的商品组合推荐。然而,如果我们把这个问题规模扩大到一个拥有成千上万仓库和数万个零售店的大型零售商呢?在这种规模下,手动规划商品的最佳组合变得不切实际且非常耗时,极大地降低了实现价值的速度。商品组合优化可以看作一个聚类问题,聚类算法可以应用于帮助规划如何对这些群体进行排序。在这里,必须考虑更多的数据点,包括历史消费者购买模式、季节性变化、社交媒体上的趋势、搜索引擎的搜索模式等。这不仅有助于更好的商品组合优化,还能提高收入,减少产品浪费,并加快企业的市场响应速度。
客户流失分析
由于客户偏好的不断变化和市场竞争的激烈,企业越来越难以获取新客户。因此,企业需要留住现有客户。客户流失率是企业高层希望最小化的一个重要指标。机器学习分类算法可以用于预测某个客户是否会流失。然而,了解影响流失的因素会很有帮助,这样企业可以调整或改进运营以提高客户满意度。聚类算法不仅可以用来识别哪些客户群体可能会流失,还可以通过识别影响流失的一组因素来进一步分析。企业可以根据这些流失因素采取行动,引入新产品或改进产品以提高客户满意度,进而减少客户流失。
保险欺诈检测
保险公司传统上使用人工检查以及规则引擎来标记保险索赔是否为欺诈。然而,随着数据量的增加,传统方法可能会错过相当大一部分的索赔,因为人工检查既费时又容易出错,且欺诈者不断创新并策划新的欺诈方式。机器学习的聚类算法可以用来将新的索赔与现有的欺诈群体进行分组,而分类算法可以用来判断这些索赔是否为欺诈。通过利用机器学习和聚类算法,保险公司可以不断地检测和防止保险欺诈。
关联规则和协同过滤应用
关联规则和协同过滤是用于构建推荐系统的技术。本节将探讨推荐系统的一些实际应用案例,以便于实际商业应用。
推荐系统
推荐系统被电子零售商用来进行市场购物篮分析,在该分析中,系统根据用户的偏好以及购物车中已存在的商品向用户推荐产品。推荐系统还可以用于基于位置或临近度的推荐,例如,当客户靠近特定商店时,系统可以显示广告或优惠券。推荐系统还广泛应用于营销领域,营销人员可以通过推荐系统获取有关可能购买某商品的用户的信息,从而提高营销活动的效果。
推荐系统在在线音乐和视频服务提供商中也得到了广泛应用,用于用户内容个性化。在这里,推荐系统根据用户的偏好和历史使用模式,向用户推荐新的音乐或视频。
总结
本章介绍了无监督学习算法,以及如何对未标记的数据进行分类并识别数据实体之间的关联。介绍了无监督学习算法的两个主要领域,即聚类和关联规则。你了解了最流行的聚类算法和协同过滤算法,并通过 Spark MLlib 中的代码示例展示了 K-means、二分 K-means、LDA 和 GSM 等聚类算法的工作原理。你还看到了使用 Spark MLlib 中替代最小二乘法算法构建推荐引擎的代码示例。最后,展示了一些无监督学习算法在现实商业中的应用。我们探讨了关于无监督学习算法的若干概念、技术和代码示例,以便你能够使用 Spark MLlib 在大规模上训练模型。
到目前为止,在本章和前一章中,你只探讨了机器学习过程中的数据清洗、特征工程和模型训练部分。在下一章中,你将接触到机器学习生命周期管理,在这里你将探索一些概念,如模型性能调优、跟踪机器学习实验、将机器学习模型存储在中央仓库中,以及在将它们投入生产应用之前进行机器学习模型的运营化。最后,还将介绍并探索一个开源的端到端机器学习生命周期管理工具——MLflow。
第九章:机器学习生命周期管理
在前面的章节中,我们探讨了使用Apache Spark进行可扩展机器学习的基础知识。介绍了处理监督学习和无监督学习的算法,并展示了如何使用Apache Spark MLlib实现这些算法。在现实世界中,仅训练一个模型是不够的。相反,必须通过调整模型参数,使用相同的数据集构建多个版本的同一模型,以获得最佳的模型。此外,同一个模型可能并不适用于所有应用,因此需要训练多个模型。因此,有必要跟踪各种实验、它们的参数、指标以及它们训练所用的数据版本。而且,模型通常会出现漂移,意味着它们的预测能力会因为环境变化而降低,因此需要进行监控并在必要时重新训练。
本章将介绍实验跟踪、模型调优、模型生产化以及使用离线和在线技术进行模型推理的概念。本章将通过一个端到端的开源机器学习生命周期管理工具MLflow来展示这些概念。最后,我们将探讨机器学习的持续部署概念,以自动化整个机器学习(ML)生命周期管理过程。
本章我们将覆盖以下主要内容:
-
机器学习生命周期简介
-
使用MLflow跟踪实验
-
使用MLflow 模型注册表跟踪模型版本
-
模型服务和推理
-
机器学习的持续部署
技术要求
本章我们将使用 Databricks Community Edition 来运行代码(community.cloud.databricks.com)。
-
注册说明可以在
databricks.com/try-databricks找到。 -
本章的代码可以从
github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter09下载,而本章的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
我们还将使用 Databricks Community Edition 提供的托管版 MLflow。安装独立版 MLflow 的说明可以在这里找到:mlflow.org/docs/latest/quickstart.html。
机器学习生命周期简介
机器学习生命周期是数据科学项目所遵循的一个持续过程。它包含四个主要阶段,分别是数据收集与准备、模型训练、模型评估,最后是模型推理和监控。机器学习过程是一个持续的过程,生命周期在优化数据和不断提高模型性能之间反复迭代;或者说,防止模型性能随着时间的推移而下降:

图 9.1 – 机器学习生命周期
前面的图表展示了机器学习生命周期管理的持续过程,从数据准备到模型开发,然后从训练到模型部署和监控。当模型性能因训练数据的变化、模型代码的变化或模型参数的变化而下降时,循环过程将重新开始。
前几章介绍了数据收集与准备、清洗和整合的过程,以及在大规模上训练各种机器学习模型的技术。本章将介绍机器学习生命周期的其余阶段,包括模型评估、模型推理和监控。
机器学习生命周期管理的这一循环过程帮助你不断优化数据集和模型,并保持模型的性能。你在机器学习生命周期中的迭代速度决定了你将模型投入实际应用的速度,从而决定了你的数据科学项目对企业的价值以及与数据科学项目相关的成本。因此,使用机器学习生命周期管理工具来简化整个机器学习过程、从数据科学项目中获取最大价值,并确保业务用户能够从中获得实际收益是至关重要的。目前存在多种机器学习生命周期管理工具可以处理这一任务。Pacyderm、Kubeflow、MLflow和Metaflow是一些开源工具,而AWS Sagemaker、Google Cloud AutoML和Microsoft Azure ML都是具有完整机器学习生命周期管理支持的云原生服务。本章将探索MLflow作为机器学习生命周期管理工具,接下来的章节将更详细地探讨MLflow。
MLflow 介绍
在传统软件开发中,生命周期代码是按照给定的功能规格编写的。然而,在机器学习中,目标是优化特定的指标,直到达到所需的准确度。优化某一特定指标的过程不是一次性的,而是一个持续的实验和改进过程。此外,在机器学习中,结果的质量不仅仅依赖于代码的质量,还与其他参数有关,比如用于机器学习训练过程的数据以及提供给训练算法的参数。传统机器学习使用的编程堆栈在不同的数据科学家之间差异很大。最后,在一个环境中创建的机器学习模型通常会被部署到不同的环境中,因此确保模型的可移植性和可复现性也非常重要。因此,整个机器学习生命周期是非常迭代的,涉及多个参数、数据集和各种编程库。
MLflow 是一款易于使用、模块化的端到端机器学习生命周期管理开源软件,旨在解决上述机器学习生命周期中的挑战:

图 9.2 – 使用 MLflow 的机器学习生命周期
MLflow 包含以下四个组件,用于解决机器学习生命周期中的挑战,如实验跟踪管理、实验可复现性、模型库和模型部署:
-
MLflow 跟踪
-
MLflow 项目
-
MLflow 模型
-
模型注册表
下面的章节将进一步探讨这些组件。你将学习这些组件如何帮助解决机器学习生命周期管理中的挑战,并查看一些代码示例。
使用 MLflow 跟踪实验
在现实中,构建单一模型是远远不够的。一个典型的模型构建过程需要多次迭代,有时需要改变模型参数,有时需要调整训练数据集,直到达到理想的模型准确度。有时,适合某个特定用例的模型在另一个场景中可能并不适用。这意味着一个典型的数据科学过程涉及对多个模型进行实验,以解决单一的业务问题,并记录所有数据集、模型参数和模型指标,以供未来参考。传统上,实验跟踪是通过使用简单的工具,如电子表格来完成的,但这会拖慢生产速度,而且是一个繁琐的过程,容易出错。
MLflow 跟踪组件通过其 API 和 UI 来解决这个问题,用于记录机器学习实验,包括模型参数、模型代码、指标、模型训练过程的输出,以及与实验相关的任何任意文档。让我们学习如何安装 MLflow 并使用跟踪服务器跟踪实验:
%pip install mlflow
上述命令将在你的 Databricks 笔记本中安装 MLflow 并重启 Python 会话。
注意
前一个命令只会在您的本地 Python 会话中安装 MLflow 客户端库。我们将使用 Databricks 社区版提供的托管 MLflow 作为跟踪服务器组件。有关在 Databricks 之外配置和运行 MLflow Tracking 服务器的说明可以在这里找到:mlflow.org/docs/latest/tracking.html#how-runs-and-artifacts-are-recorded。
在以下代码示例中,我们正在构建一个简单的回归模型,该模型在之前的章节中使用过,并使用 MLflow Tracking 来跟踪实验:
import mlflow
import mlflow.spark
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.regression import LinearRegression
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
在之前的代码示例中,我们执行了以下操作:
-
首先,我们导入相关的库。在这里,我们导入了各种 MLflow 客户端库,还通过
mlflow.spark导入了特定于 Spark 的 MLflow 组件。 -
在这个代码示例中,我们使用
pyspark.ml库构建了多个回归模型来执行这些操作以衡量我们模型的准确性。
现在,我们需要初始化 MLflow Tracking,以便开始跟踪我们的实验,如下面的代码块所示:
mlflow.set_tracking_uri("databricks")
retail_features = spark.read.table("retail_features")
retail_df = retail_features.selectExpr("cust_age as label", "selected_features as features")
train_df, test_df = retail_df.randomSplit([0.9, 0.1])
现在,我们必须初始化训练数据集,如下面的代码块所示:
-
首先,从我们在之前章节中创建的特征 Delta 表中创建一个
retail_featuresDataFrame。我们必须从包含我们从丰富的零售交易数据中提取的所有特征的 DataFrame 中选择用于模型训练的标签和特征列。 -
现在,我们必须使用
randomSplit()函数将包含标签和特征列的 DataFrame 随机拆分为两个单独的 DataFrame。训练 DataFrame 用于模型训练目的,而测试 DataFrame 则保留用于在训练模型后检查模型准确性。
在这一点上,我们可以开始实验过程,如下面的代码块所示:
evaluator = RegressionEvaluator(labelCol="label",
metricName="rmse")
mlflow.set_tracking_uri("databricks")
mlflow.set_experiment("/Users/snudurupati@outlook.com/linregexp")
experiment = mlflow.get_experiment_by_name("/Users/snudurupati@outlook.com/linregexp")
在前面的代码块中,我们设置了我们的初始算法参数和 MLflow Tracking 服务器参数:
-
我们实例化了
RegressionEvaluator对象,该对象传递标签列并使用 RMSE 作为准确性指标。这在交叉验证过程中计算标签列上的rmse时非常有用。 -
现在我们准备开始我们的实验,通过使用
mlflow.set_tracking_uri("databricks")函数为 MLflow Tracking 服务器配置我们的实验。 -
MLflow Tracking 服务器可以跟踪来自多个用户、多个会话的多个实验。因此,您必须为您的实验提供一个唯一的名称,我们可以使用
mlflow.set_experiment("/Users/user_name/exp_name")来实现。指定给实验名称的路径需要是一种持久存储形式,比如本地磁盘或数据湖位置。在这种情况下,我们使用了Databricks 文件系统(DBFS)。
在之前的代码块中,我们将 URI 指定为databricks,因为我们打算使用 Databricks 社区版自带的 MLflow Tracking 服务器。如果你在本地运行 MLflow,可以将 URI 指定为./mlruns,或者如果你自己设置了远程跟踪服务器,可以提供远程服务器的 URI。如何设置自己的跟踪服务器的说明请参见mlflow.org/docs/latest/tracking.html#mlflow-tracking-servers:
lr = LinearRegression(maxIter=10)
paramGrid = (ParamGridBuilder()
.addGrid(lr.regParam, [0.1, 0.01])
.addGrid(lr.fitIntercept, [False, True])
.addGrid(lr.elasticNetParam, [0.0, 0.5, 1.0])
.build())
csv = CrossValidator(estimator=lr,
estimatorParamMaps=param_grid,
evaluator=RegressionEvaluator(),
numFolds=2)
现在我们已经初始化了所有实验所需的对象,并通过 MLflow 配置了实验跟踪,我们可以开始训练过程:
-
首先,我们必须实例化
LinearRegression模型对象,并将maxIter参数设置为10。 -
然后,我们必须为交叉验证过程设置参数网格,并为模型参数指定一系列值。
-
最后,我们必须通过初始化
CrossValidator对象来配置训练-验证拆分过程。我们可以通过将实际模型对象、参数网格对象和评估对象作为参数传递来完成此操作。
关于CrossValidator对象如何工作的更多细节将在接下来的章节中提供。现在,我们准备好开始模型训练实验过程,如下方代码块所示:
with mlflow.start_run() as run_id:
lr_model = csv.fit(train_df)
test_metric = evaluator.evaluate(lr_model.transform(test_df))
mlflow.log_metric(evaluator.getMetricName(),
test_metric)
mlflow.spark.log_model(spark_model=lr_model.bestModel,
artifact_path='best-model')
在之前的代码示例中,我们调用了 MLflow Tracking 服务器以开始跟踪我们的实验:
-
由于我们使用的是交叉验证技术,并且已经定义了一个带有值范围的参数网格,
fit()过程将构建多个模型,而不是单一模型,并根据指定的准确度指标记录最佳模型。由于我们希望记录在此过程中构建的所有模型,因此我们必须使用mlflow.start_run()方法调用 MLflow Tracking 服务。 -
在交叉验证过程中生成的度量值将使用
mlflow.log_metric()函数记录到 MLflow Tracking 服务器。在 Databricks 管理的 MLflow 环境中,使用CrossValidator时,模型参数会自动记录。然而,也可以使用mlflow.log_parameter()函数显式记录模型参数,并且任何任意的工件,如图表或图像,可以使用mlflow.log_artifact()函数记录。 -
CrossValidator会遍历多个模型,并根据指定的准确度指标生成最佳模型。在此过程中生成的最佳模型作为bestModel对象可用。可以使用mlflow.spark.log_model(spark_model=lr_model.bestModel, artifact_path='best-model')命令记录该模型。这里,artifact_path表示模型在 MLflow Tracking 服务器中存储的路径。
以下截图显示了我们刚刚执行的 ML 实验的 MLflow Tracking 界面:

图 9.3 – MLflow 模型跟踪界面
在前面的截图中,我们可以看到,MLflow 记录了我们在 交叉验证 过程中为每个模型构建的准确度指标和模型参数,作为一个单独的实验。然而,只有所有运行中表现最好的模型被记录下来。可以通过点击其中一项运行来访问单个运行的详细信息,如以下截图所示:

图 9.4 – 从 MLflow Tracking UI 运行的各个模型
在前面的截图中,所有的模型参数和指标都已记录在 MLflow Tracking UI 中,并附带其他元数据,如模型的运行日期和时间、用户名、此特定模型运行的源代码版本、模型运行的持续时间以及模型的状态。只要数据的相同版本可用,这些信息在未来重现实验或在不同环境中重现相同实验时非常有用。
提示
如果数据存储在 Delta 表中,可以使用 MLflow Tracking 跟踪实验中使用的数据版本,因为 Delta 提供了内置的数据版本控制。可以通过将 Delta 表的版本作为任意工件记录到 MLflow 来实现这一点。
mlflow. 命令在前面的代码片段中指定了我们使用的是 MLflow 模型的 Spark 版本。MLflow 支持其他类型的模型,如以下章节所述。
MLflow 模型
MLflow 模型是一种通用的、可移植的模型格式,支持多种模型类型,从简单的 Python pickle 对象到 scikit-learn、TensorFlow、PyTorch、MLeap 和其他模型格式,包括 Parquet 格式的 Spark 模型。MLflow 模型提供了可以使用各种常见机器学习工具生成的抽象模型,并可部署到各种机器学习环境中。MLflow 模型以 MLflow 格式提供来自流行机器学习框架的模型。MLflow 模型具有标准的目录结构,其中包含配置文件和序列化的模型工件。它还包含所有模型评估依赖项,以 conda 环境的形式通过 conda.yaml 文件提供模型的可移植性和可重现性。

图 9.5 – MLflow 模型
上面的截图展示了一个典型的 MLflow 模型结构。这个标准模型格式创建了一个可以被任何下游工具理解的模型风味。模型风味用于部署目的,帮助我们理解来自任何机器学习库的模型,而无需将每个工具与每个库集成。MLflow 支持许多它内建的部署工具支持的主要模型风味,例如描述如何将模型作为 Python 函数运行的简单 Python 函数风味。例如,在前面的代码示例中,我们将模型存储为一个 Spark 模型,然后可以将其加载为 Spark 对象,或者作为一个简单的 Python 函数,在任何 Python 应用程序中使用,即使这些应用程序根本不理解 Spark。
ML 模型调优
模型调优是模型构建过程中一个重要的环节,通过编程识别最佳模型参数,以实现最佳模型性能。通过反复调整模型参数来选择模型的过程称为 超参数调优。典型的超参数调优过程包括将可用数据分割成多个训练集和测试集。然后,对于每个训练数据集,一个测试对会遍历一组模型参数,称为参数网格,并从所有训练过的模型中选择性能最佳的模型。
Spark ML 提供了一个 ParamGridBuilder 工具来帮助构建参数网格,以及 CrossValidator 和 TrainValidationSplit 类来处理模型选择。CrossValidator 通过将数据集分割成一组折叠,将这些折叠作为独立的训练集和测试集对进行模型选择,每个折叠仅使用一次作为测试集。使用 ParamGridBuilder 和 CrossValidator 进行模型调优和选择的示例已在前一节的代码示例中展示。TrainValidationSplit 是另一种流行的超参数调优技术。有关其在 Apache Spark 中的实现细节,可以参阅 Spark 的文档页面 spark.apache.org/docs/latest/ml-tuning.html#train-validation-split。
现在,我们已经学习了如何使用 Apache Spark 进行模型选择调优,以及如何使用 MLflow 跟踪实验,机器学习生命周期管理的下一步是将模型及其版本存储在中央模型仓库中,以便后续使用。在接下来的章节中,我们将使用 MLflow 模型注册表 探索这一过程。
使用 MLflow 模型注册表跟踪模型版本
虽然 MLflow 跟踪服务器可以让您跟踪所有机器学习实验的属性,MLflow 模型注册中心提供了一个集中式的模型存储库,让您跟踪模型生命周期的各个方面。MLflow 模型注册中心包括一个用户界面和 API,用于跟踪模型的版本、血统、阶段过渡、注释以及任何开发者评论。MLflow 模型注册中心还包含用于 CI/CD 集成的 Webhooks 和用于在线模型服务的模型服务器。
MLflow 模型注册中心为我们提供了一种跟踪和组织在开发、测试和生产过程中由企业生成和使用的众多机器学习模型的方法。模型注册中心通过利用访问控制列表提供了一种安全的共享模型方式,并提供与模型治理和审批工作流集成的方法。模型注册中心还允许我们通过其 API 监控 ML 部署及其性能。
提示
模型注册中心的访问控制和设置已注册模型权限的功能仅在 Databricks 完整版中可用。它们在 Databricks 社区版或开源版本的 MLflow 中不可用。
一旦模型被记录到模型注册中心,您可以通过 UI 或 API 添加、修改、更新、过渡或删除模型,如下列代码示例所示:
import mlflow
from mlflow.tracking.client import MlflowClient
client = MlflowClient()
model_name = "linear-regression-model"
artifact_path = "best_model"
model_uri = "runs:/{run_id}/{artifact_path}".format (run_id=run_id, artifact_path=artifact_path)
registered_model = mlflow.register_model(model_uri=model_uri, name=model_name, )
client.update_model_version(
name=registered_model.name,
version=registered_model.version,
description="This predicts the age of a customer using transaction history."
)
client.transition_model_version_stage(
name=registered_model.name,
version=registered_model.version,
stage='Staging',
)
model_version = client.get_model_version(
name=registered_model.name,
version=registered_model.version,
)
model_uri = "models:/{model_name}/staging".format(model_name=model_name)
spark_model = mlflow.spark.load_model(model_uri)
在之前的代码片段中,我们执行了以下操作:
-
首先,我们导入了相关的 MLflow 库和
MlflowClient,它是一个通过 Python 访问 MLflow 跟踪服务器和模型注册中心工件的 MLflow 接口。客户端接口通过MlflowClient()方法调用。 -
然后,我们通过提供模型、名称、模型工件位置和来自已跟踪实验的
run_id属性来构建model_uri。这些信息可以通过 MLflow 跟踪服务器访问,无论是通过 API 还是 UI。 -
由于我们已经从跟踪服务器重建了模型 URI,我们通过
register_model()函数将模型注册到模型注册中心。如果该模型尚未存在于模型注册中心,则会注册一个版本为1的新模型。 -
一旦模型注册完成,我们就可以通过
update_model_version()方法添加或更新模型描述。 -
在模型的生命周期中,它通常会不断演变,并需要过渡其阶段,比如从暂存阶段到生产阶段或归档阶段。可以使用
transition_model_version_stage()方法来实现这一点。 -
可以使用
get_model_version()方法列出已注册模型的版本和阶段。 -
最后,我们使用
load_model()方法从模型注册中心加载特定版本/阶段的模型,在构建模型 URI 时使用了models关键字、模型名称及其版本或阶段名称。
模型注册表还提供了其他实用功能,用于列出和搜索个别已注册模型的不同版本,以及存档和删除已注册的模型。相关方法的参考资料可以在 www.mlflow.org/docs/latest/model-registry.html#model-registry-workflows 中找到。
重要提示
我们将在本书中使用的 Databricks Community Edition 并不包含模型注册表,因此你需要使用 Databricks 的完整版来执行前面的代码示例。或者,你可以部署自己的 MLflow 远程跟踪服务器,并在 Databricks 环境之外使用数据库支持的后端存储,具体方法可以参见:mlflow.org/docs/latest/tracking.html#mlflow-tracking-servers。
现在你已经学会了如何使用 MLflow 模型注册表存储、版本管理和检索模型,机器学习生命周期的下一步是将训练、评估和调优后的模型应用到实际业务中,通过它们来进行推理。我们将在下一节中讨论这一内容。
模型服务与推理
模型服务与推理是整个机器学习生命周期中最重要的步骤。这是将已构建的模型部署到业务应用程序中的阶段,以便我们能够从中得出推理。模型服务与推理可以通过两种方式进行:在离线模式下使用批处理,或者在在线模式下实时推理。
离线模型推理
离线模型推理是指使用批处理生成来自机器学习模型的预测。批处理推理作业按预定时间表定期运行,每次都在一组新的数据上生成预测。这些预测会存储在数据库中或数据湖中,并以离线或异步的方式供业务应用程序使用。批处理推理的一个例子是由营销团队使用的数据驱动客户细分,或者零售商预测客户的生命周期价值。这些用例并不需要实时预测,批处理推理可以满足需求。
对于 Apache Spark,批处理推理可以利用 Spark 的可扩展性,在非常大的数据集上大规模地进行预测,以下代码示例演示了这一点:
model_uri = "runs:/{run_id}/{artifact_path}".format(run_id=active_run.info.run_id, artifact_path="best-model")
spark_udf = mlflow.pyfunc.spark_udf(spark, model_uri)
predictions_df = retail_df.withColumn("predictions", spark_udf(struct("features")))
predictions_df.write.format("delta").save("/tmp/retail_predictions")
在之前的代码块中,我们使用模型的名称和跟踪服务器中的工件位置重新创建了模型的 URI。然后,我们使用来自跟踪服务器中 Spark 模型的 MLflow Python 函数模型风格创建了一个 Spark 用户定义函数(UDF)。这个 Spark UDF 可以与 Spark DataFrame 一起使用,以批量方式进行大规模推理。这个代码片段可以作为定时任务定期执行,预测结果可以保存到数据湖中的某个位置。
Apache Spark 框架可以帮助我们实现完整的机器学习生命周期,从模型训练到模型推理,使用一个统一的数据处理框架。Spark 还可以通过 Spark Structured Streaming 将批量推理扩展到接近实时的推理。然而,在超低延迟实时模型服务方面,Apache Spark 并不适用。我们将在下一节进一步探讨这个问题。
在线模型推理
在线推理是指在实时生成机器学习预测的过程,可以通过将推理代码嵌入到业务应用程序中,或者使用超低延迟的 API 来实现。与批量推理不同,后者是在大数据集上批量生成的,实时在线推理通常是针对每次一个观察生成的。在线推理可以通过在毫秒到秒之间生成预测,而不是小时到天,帮助机器学习的全新应用。
以一个移动游戏应用为例,假设你想根据玩家的等级或他们所玩的游戏类型向他们展示个性化的促销。在线推理可以迅速从移动应用收集用户行为数据,并在应用内或通过低延迟 API 服务器生成预测推荐。例如,这可以帮助企业为客户实时生成个性化的体验。尽管 Apache Spark 本身不适合在线推理,MLflow 模型注册中心包含一个可以激活的模型服务组件,使用以下命令进行启动:
mlflow models serve -m "models:/ linear-regression-model/Production"
在之前的命令中,我们调用了 MLflow 内置的模型服务来为我们之前在模型注册中心注册的模型提供服务。这个模型服务提供了一个 RESTful API,外部应用程序可以通过 HTTP 与模型服务进行通信,发送一个称为负载的单一观察,并一次性接收一个预测结果。
注意
MLflow 的模型服务功能在 Databricks Community Edition 中不可用,因此如果你使用的是该版本,之前的命令将无法执行。此功能可以在开源的 MLflow 或 Databricks 的完整版本中使用。
截至目前,MLflow 的模型服务功能仍处于预览阶段,并且存在一些限制,如目标吞吐量为每秒 20 个查询,以及每次请求的有效载荷大小限制为 16MB。因此,这个选项仅建议用于低吞吐量、非生产环境的应用。不过,MLflow 确实提供与其他模型服务平台的集成,例如AWS Sagemaker、Azure ML和Google Cloud AI。有关这些集成的详细信息,请参阅各自云服务提供商的文档。
机器学习的持续交付
与传统软件代码不同,ML 代码是动态的,并且不断受到模型代码本身、底层训练数据或模型参数变化的影响。因此,ML 模型的性能需要持续监控,且模型需要定期重新训练和重新部署,以保持所期望的模型性能水平。这个过程如果手动进行,可能会很费时且容易出错。但机器学习的持续交付(CD4ML)可以帮助简化并自动化这个过程。
CD4ML 来源于持续集成和持续交付(CI/CD)的 软件工程原则,这些原则旨在推动自动化、质量和纪律,帮助创建一个可靠且可重复的流程,使得软件能够顺利地投入生产。CD4ML 在此基础上对 CI/CD 流程进行了扩展和调整,应用于机器学习(ML)领域,其中数据团队生成与 ML 过程相关的工件,如代码数据和模型,并以安全和小步长的方式逐步推进,这些工件可以在任何时候可靠地重复和发布。
CD4ML 流程包括数据科学家构建模型及其参数、源代码和所需的训练数据。下一步是模型评估和调优。达到可接受的模型精度后,模型需要进行生产化,并对其进行测试。最后一步是部署和监控模型。如果模型需要调整,CD4ML 管道应该触发 ML 过程,从头开始重新进行。这确保了持续交付的实现,并将最新的代码变更和模型推送到生产环境。MLflow 提供了大部分实施此 CD4ML 过程所需的组件,如下所示:
-
首先,MLflow Tracking 服务器可以帮助你跟踪模型训练过程,包括所有训练工件、数据版本以及已被版本控制并标记为测试或生产使用的模型,通过模型注册表进行管理。
-
然后,已注册的模型可以用自定义标签进行注释,帮助编码多种信息。这包括指示模型的部署方式,无论是批处理模式还是在线模式,或是模型部署所在的区域。
-
数据科学家、测试工程师和 ML 工程师可以在模型中添加评论,指定测试失败、模型不准确或生产部署失败的备注,以帮助跨职能团队之间的讨论。
-
可以通过模型注册中的 Webhook 推送通知,帮助通过外部 CI/CD 工具触发各种操作和自动化测试。例如,模型创建、版本更改、添加新评论等事件都可以触发相应的操作。
-
最后,MLflow Projects 有助于将整个模型开发工作流打包成一个可重用、可参数化的模块。
通过利用 MLflow 组件,例如 MLflow Tracking 服务器、模型注册、MLflow Projects 及其 Webhook 功能,再结合 Jenkins 等过程自动化服务器,可以编排整个 CD4ML 流水线。
注意
MLflow Model Registry 的 Webhook 功能仅在 Databricks 的完整版中可用,社区版或开源 MLflow 中无法使用。有关 MLflow Projects 及其使用方法的更多信息,请访问:www.mlflow.org/docs/latest/projects.html。
通过这种方式,MLflow 通过其模型追踪和模型注册流程,可以帮助简化整个 CD4ML 流程,否则这一过程将非常复杂,涉及许多手动步骤,缺乏可重复性,并且容易导致跨职能团队之间的错误和混乱。
总结
本章介绍了端到端的 ML 生命周期及其中涉及的各个步骤。MLflow 是一个完整的端到端 ML 生命周期管理工具。介绍了 MLflow Tracking 组件,它对于流式处理 ML 实验过程非常有用,帮助你追踪所有的属性,包括数据版本、ML 代码、模型参数和指标,以及任何其他任意的工件。介绍了 MLflow Model 作为一种基于标准的模型格式,提供了模型的可移植性和可重复性。还介绍了 MLflow Model Registry,它是一个集中式的模型库,支持新创建的模型的整个生命周期,从暂存到生产再到归档。还介绍了模型服务机制,如使用批处理和在线处理。最后,介绍了 ML 的持续交付,它用于简化整个 ML 生命周期,并通过 Model Registry 功能自动化模型生命周期过程,如模型阶段转换、添加评论和注释的方式,以及通过外部编排工具使用 Webhook 来帮助自动化模型生命周期过程。
到目前为止,你已经掌握了在大规模进行数据工程和数据科学的实用技能。在下一章,我们将重点介绍如何基于标准 Python 扩展单机 ML 库的技术。
第十章:使用 PySpark 扩展单节点机器学习
在第五章**,使用 PySpark 进行可扩展的机器学习中,你学习了如何利用Apache Spark的分布式计算框架进行大规模的机器学习(ML)模型训练和评分。Spark 的本地 ML 库涵盖了数据科学家通常执行的标准任务;然而,还有许多标准的单节点Python库提供了丰富的功能,这些库并不是为分布式工作方式设计的。本章讨论了如何将标准 Python 数据处理和 ML 库(如pandas、scikit-learn、XGBoost等)水平扩展到分布式环境。它还涵盖了典型数据科学任务的扩展,如探索性数据分析(EDA)、模型训练、模型推断,最后,还介绍了一种名为Koalas的可扩展 Python 库,它允许你使用熟悉且易于使用的 pandas 类似语法编写PySpark代码。
本章将涵盖以下主要主题:
-
扩展 EDA
-
扩展模型推断
-
分布式超参数调优
-
使用极易并行计算进行模型训练
-
使用 Koalas 将 pandas 升级到 PySpark
本章中获得的一些技能包括大规模执行 EDA、大规模执行模型推断和评分、超参数调优,以及单节点模型的最佳模型选择。你还将学习如何水平扩展几乎所有的单节点 ML 模型,最后使用 Koalas,它让我们能够使用类似 pandas 的 API 编写可扩展的 PySpark 代码。
技术要求
-
在本章中,我们将使用 Databricks 社区版运行代码:
community.cloud.databricks.com注册说明可以在
databricks.com/try-databricks找到。 -
本章中使用的代码和数据可以从
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter10下载。
扩展 EDA
EDA 是一种数据科学过程,涉及对给定数据集的分析,以了解其主要特征,有时通过可视化图表,有时通过数据聚合和切片。你已经在第十一章**,使用 PySpark 进行数据可视化中学习了一些可视化的 EDA 技术。在这一节中,我们将探索使用 pandas 进行非图形化的 EDA,并将其与使用 PySpark 和 Koalas 执行相同过程进行比较。
使用 pandas 进行 EDA
标准 Python 中的典型 EDA 涉及使用 pandas 进行数据处理,使用 matplotlib 进行数据可视化。我们以一个来自 scikit-learn 的示例数据集为例,执行一些基本的 EDA 步骤,如以下代码示例所示:
import pandas as pd
from sklearn.datasets import load_boston
boston_data = datasets.load_boston()
boston_pd = pd.DataFrame(boston_data.data,
columns=boston_data.feature_names)
boston_pd.info()
boston_pd.head()
boston_pd.shape
boston_pd.isnull().sum()
boston_pd.describe()
在前面的代码示例中,我们执行了以下步骤:
-
我们导入 pandas 库并导入 scikit-learn 提供的示例数据集
load_boston。 -
然后,我们使用
pd.DataFrame()方法将 scikit-learn 数据集转换为 pandas DataFrame。 -
现在我们有了一个 pandas DataFrame,可以对其进行分析,首先使用
info()方法,打印关于 pandas DataFrame 的信息,如列名及其数据类型。 -
pandas DataFrame 上的
head()函数打印出实际 DataFrame 的几行几列,并帮助我们从 DataFrame 中直观地检查一些示例数据。 -
pandas DataFrame 上的
shape属性打印出行和列的数量。 -
isnull()方法显示 DataFrame 中每一列的 NULL 值数量。 -
最后,
describe()方法打印出每一列的统计数据,如均值、中位数和标准差。
这段代码展示了使用 Python pandas 数据处理库执行的一些典型 EDA 步骤。现在,让我们看看如何使用 PySpark 执行类似的 EDA 步骤。
使用 PySpark 进行 EDA
PySpark 也有类似于 pandas DataFrame 的 DataFrame 构造,你可以使用 PySpark 执行 EDA,如以下代码示例所示:
boston_df = spark.createDataFrame(boston_pd)
boston_df.show()
print((boston_df.count(), len(boston_df.columns)))
boston_df.where(boston_df.AGE.isNull()).count()
boston_df.describe().display()
在前面的代码示例中,我们执行了以下步骤:
-
我们首先使用
createDataFrame()函数将前一部分创建的 pandas DataFrame 转换为 Spark DataFrame。 -
然后,我们使用
show()函数展示 Spark DataFrame 中的一小部分数据。虽然也有head()函数,但show()能以更好的格式和更易读的方式展示数据。 -
Spark DataFrame 没有内置的函数来显示 Spark DataFrame 的形状。相反,我们使用
count()函数计算行数,使用len()方法计算列数,以实现相同的功能。 -
同样,Spark DataFrame 也不支持类似 pandas 的
isnull()函数来统计所有列中的 NULL 值。相反,我们使用isNull()和where()的组合,逐列过滤掉 NULL 值并进行计数。 -
Spark DataFrame 确实支持
describe()函数,可以在分布式模式下计算每列的基本统计数据,通过后台运行一个 Spark 作业来实现。对于小型数据集这可能看起来不太有用,但对于描述非常大的数据集来说,它非常有用。
通过使用 Spark DataFrame 提供的内置函数和操作,你可以轻松地扩展你的 EDA。由于 Spark DataFrame 本身支持Spark SQL,你可以在使用 DataFrame API 进行 EDA 的同时,也通过 Spark SQL 执行可扩展的 EDA。
扩展模型推理
除了数据清洗、模型训练和调优外,整个 ML 过程的另一个重要方面是模型的生产化。尽管有大量的数据可供使用,但有时将数据下采样,并在较小的子集上训练模型是有用的。这可能是由于信噪比低等原因。在这种情况下,不需要扩展模型训练过程本身。然而,由于原始数据集的大小非常庞大,因此有必要扩展实际的模型推理过程,以跟上生成的大量原始数据。
Apache Spark 与MLflow一起,可以用来对使用标准非分布式 Python 库(如 scikit-learn)训练的模型进行评分。以下代码示例展示了一个使用 scikit-learn 训练的模型,随后使用 Spark 进行大规模生产化的示例:
import mlflow
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
X = boston_pd[features]
y = boston_pd['MEDV']
with mlflow.start_run() as run1:
lr = LinearRegression()
lr_model = lr.fit(X_train,y_train)
mlflow.sklearn.log_model(lr_model, "model")
在上一个代码示例中,我们执行了以下步骤:
-
我们打算使用 scikit-learn 训练一个线性回归模型,该模型在给定一组特征的情况下预测波士顿房价数据集中的中位数房价。
-
首先,我们导入所有需要的 scikit-learn 模块,同时我们还导入了 MLflow,因为我们打算将训练好的模型记录到MLflow Tracking Server。
-
然后,我们将特征列定义为变量
X,标签列定义为y。 -
然后,我们使用
with mlflow.start_run()方法调用一个 MLflow 实验。 -
然后,我们使用
LinearRegression类训练实际的线性回归模型,并在训练的 pandas DataFrame 上调用fit()方法。 -
然后,我们使用
mlflow.sklearn.log_model()方法将结果模型记录到 MLflow 追踪服务器。sklearn限定符指定记录的模型是 scikit-learn 类型。
一旦我们将训练好的线性回归模型记录到 MLflow 追踪服务器,我们需要将其转换为 PySpark 的用户定义函数(UDF),以便能够以分布式方式进行推理。实现这一目标所需的代码如下所示:
import mlflow.pyfunc
from pyspark.sql.functions import struct
model_uri = "runs:/" + run1.info.run_id + "/model"
pyfunc_udf = mlflow.pyfunc.spark_udf(spark, model_uri=model_uri)
predicted_df = boston_df.withColumn("prediction", pyfunc_udf(struct('CRIM','ZN','INDUS','CHAS','NOX','RM','AGE','DIS','RAD','TAX','PTRATIO', 'B', 'LSTAT')))
predicted_df.show()
在上一个代码示例中,我们执行了以下步骤:
-
我们从 mlflow 库中导入
pyfunc方法,用于将 mlflow 模型转换为 PySpark UDF。 -
然后,我们通过
run_id实验从 MLflow 构建model_uri。 -
一旦我们拥有了
model_uri,我们使用mlflow.pyfunc()方法将模型注册为 PySpark UDF。我们指定模型类型为spark,因为我们打算在 Spark DataFrame 中使用它。 -
现在,模型已经作为 PySpark 的 UDF 注册,我们可以用它对 Spark DataFrame 进行预测。我们通过使用该模型创建一个新的 Spark DataFrame 列,并将所有特征列作为输入。结果是一个包含每一行预测值的新列的数据框。
-
需要注意的是,当调用
show操作时,它会启动一个 Spark 作业,并以分布式方式执行模型评分。
通过这种方式,结合使用 MLflow 的pyfunc方法和 Spark DataFrame 操作,使用像 scikit-learn 这样的标准单节点 Python 机器学习库构建的模型,也可以以分布式方式进行推理推导,从而实现大规模推理。此外,推理的 Spark 作业可以被配置为将预测结果写入持久化存储方法,如数据库、数据仓库或数据湖,且该作业本身可以定期运行。这也可以通过使用结构化流处理轻松扩展,以近实时方式通过流式 DataFrame 进行预测。
使用令人尴尬的并行计算进行模型训练
如你之前所学,Apache Spark 遵循数据并行处理的分布式计算范式。在数据并行处理中,数据处理代码被移动到数据所在的地方。然而,在传统的计算模型中,如标准 Python 和单节点机器学习库所使用的,数据是在单台机器上处理的,并且期望数据存在于本地。为单节点计算设计的算法可以通过多进程和多线程技术利用本地 CPU 来实现某种程度的并行计算。然而,这些算法本身并不具备分布式能力,需要完全重写才能支持分布式计算。Spark ML 库就是一个例子,传统的机器学习算法已被完全重新设计,以便在分布式计算环境中工作。然而,重新设计每个现有的算法将是非常耗时且不切实际的。此外,已经存在丰富的基于标准的 Python 机器学习和数据处理库,如果能够在分布式计算环境中利用这些库,将会非常有用。这就是令人尴尬的并行计算范式发挥作用的地方。
在分布式计算中,同一计算过程在不同机器上执行数据的不同部分,这些计算过程需要相互通信,以完成整体计算任务。然而,在令人尴尬的并行计算中,算法不需要各个进程之间的通信,它们可以完全独立地运行。在 Apache Spark 框架内,有两种方式可以利用令人尴尬的并行计算进行机器学习训练,接下来的部分将介绍这两种方式。
分布式超参数调优
机器学习过程中的一个关键步骤是模型调优,数据科学家通过调整模型的超参数来训练多个模型。这种技术通常被称为超参数调优。超参数调优的常见方法叫做网格搜索,它是一种寻找能够产生最佳性能模型的超参数组合的方法。网格搜索通过交叉验证选择最佳模型,交叉验证将数据分为训练集和测试集,并通过测试数据集评估训练模型的表现。在网格搜索中,由于多个模型是在相同数据集上训练的,它们可以独立地进行训练,这使得它成为一个适合显式并行计算的候选方法。
使用标准 scikit-learn 进行网格搜索的典型实现,通过以下代码示例进行了说明:
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
digits_pd = load_digits()
X = digits_pd.data
y = digits_pd.target
parameter_grid = {"max_depth": [2, None],
"max_features": [1, 2, 5],
"min_samples_split": [2, 3, 5],
"min_samples_leaf": [1, 2, 5],
"bootstrap": [True, False],
"criterion": ["gini", "entropy"],
"n_estimators": [5, 10, 15, 20]}
grid_search = GridSearchCV(RandomForestClassifier(),
param_grid=parameter_grid)
grid_search.fit(X, y)
在前面的代码示例中,我们执行了以下步骤:
-
首先,我们导入
GridSearchCV模块、load_digits样本数据集,以及 scikit-learn 中与RandomForestClassifier相关的模块。 -
然后,我们从 scikit-learn 样本数据集中加载
load_digits数据,并将特征映射到X变量,将标签列映射到y变量。 -
然后,我们通过指定
RandomForestClassifier算法使用的各种超参数的值,如max_depth、max_features等,定义需要搜索的参数网格空间。 -
然后,我们通过调用
GridSearchCV()方法来启动网格搜索交叉验证,并使用fit()方法执行实际的网格搜索。
通过使用 scikit-learn 的内置网格搜索和交叉验证器方法,你可以执行模型超参数调优,并从多个训练模型中识别出最佳模型。然而,这个过程是在单台机器上运行的,因此模型是一个接一个地训练,而不是并行训练。使用 Apache Spark 和名为spark_sklearn的第三方 Spark 包,你可以轻松实现网格搜索的显式并行实现,以下代码示例演示了这一点:
from sklearn import grid_search
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from spark_sklearn import GridSearchCV
digits_pd = load_digits()
X = digits_pd.data
y = digits_pd.target
parameter_grid = {"max_depth": [2, None],
"max_features": [1, 2, 5],
"min_samples_split": [2, 3, 5],
"min_samples_leaf": [1, 2, 5],
"bootstrap": [True, False],
"criterion": ["gini", "entropy"],
"n_estimators": [5, 10, 15, 20]}
grid_search = grid_search.GridSearchCV(RandomForestClassifier(),
param_grid=parameter_grid)
grid_search.fit(X, y)
使用grid_sklearn进行网格搜索的前面代码片段与使用标准 scikit-learn 进行网格搜索的代码几乎相同。然而,我们不再使用 scikit-learn 的网格搜索和交叉验证器,而是使用grid_sklearn包中的网格搜索和交叉验证器。这有助于以分布式方式运行网格搜索,在不同的机器上对相同数据集进行不同超参数组合的模型训练。这显著加快了模型调优过程,使你能够从比仅使用单台机器时更大的训练模型池中选择模型。通过这种方式,利用 Apache Spark 上的显式并行计算概念,你可以在仍然使用 Python 的标准单节点机器库的情况下,扩展模型调优任务。
在接下来的部分中,我们将看到如何使用 Apache Spark 的 pandas UDF 来扩展实际的模型训练,而不仅仅是模型调优部分。
使用 pandas UDF 扩展任意 Python 代码
一般来说,UDF 允许你在 Spark 的执行器上执行任意代码。因此,UDF 可以用于扩展任意 Python 代码,包括特征工程和模型训练,适用于数据科学工作流。它们还可以用于使用标准 Python 扩展数据工程任务。然而,UDF 每次只能执行一行代码,并且在 JVM 与运行在 Spark 执行器上的 Python 进程之间会产生序列化和反序列化的开销。这个限制使得 UDF 在将任意 Python 代码扩展到 Spark 执行器时不太具备吸引力。
使用groupby操作符,将 UDF 应用于每个组,最终将每个组生成的单独 DataFrame 合并成一个新的 Spark DataFrame 并返回。标量以及分组的 pandas UDF 示例可以在 Apache Spark 的公开文档中找到:
spark.apache.org/docs/latest/api/python/reference/api/pyspark.sql.functions.pandas_udf.html
到目前为止,你已经看到如何使用 Apache Spark 支持的不同技术来扩展 EDA 过程、模型调优过程,或者扩展任意 Python 函数。在接下来的部分中,我们将探索一个建立在 Apache Spark 之上的库,它让我们可以使用类似 pandas 的 API 来编写 PySpark 代码。
使用 Koalas 将 pandas 升级到 PySpark
pandas 是标准 Python 中数据处理的事实标准,Spark 则成为了分布式数据处理的事实标准。pandas API 是与 Python 相关的,并利用 Python 独特的特性编写可读且优雅的代码。然而,Spark 是基于 JVM 的,即使是 PySpark 也深受 Java 语言的影响,包括命名约定和函数名称。因此,pandas 用户转向 PySpark 并不容易或直观,且涉及到相当的学习曲线。此外,PySpark 以分布式方式执行代码,用户需要理解如何在将 PySpark 代码与标准单节点 Python 代码混合时,分布式代码的工作原理。这对普通的 pandas 用户来说,是使用 PySpark 的一大障碍。为了解决这个问题,Apache Spark 开发者社区基于 PySpark 推出了另一个开源库,叫做 Koalas。
Koalas 项目是基于 Apache Spark 实现的 pandas API。Koalas 帮助数据科学家能够立即使用 Spark,而不需要完全学习新的 API 集。此外,Koalas 帮助开发人员在不需要在两个框架之间切换的情况下,维护一个同时适用于 pandas 和 Spark 的单一代码库。Koalas 随pip一起打包发布。
让我们看几个代码示例,了解 Koalas 如何提供类似 pandas 的 API 来与 Spark 一起使用:
import koalas as ks
boston_data = load_boston()
boston_pd = ks.DataFrame(boston_data.data, columns=boston_data.feature_names)
features = boston_data.feature_names
boston_pd['MEDV'] = boston_data.target
boston_pd.info()
boston_pd.head()
boston_pd.isnull().sum()
boston_pd.describe()
在前面的代码片段中,我们执行了本章早些时候所进行的相同的基本 EDA 步骤。唯一的不同是,这里我们没有直接从 scikit-learn 数据集创建 pandas DataFrame,而是导入 Koalas 库后创建了一个 Koalas DataFrame。你可以看到,代码和之前写的 pandas 代码完全相同,然而,在幕后,Koalas 会将这段代码转换为 PySpark 代码,并在集群上以分布式方式执行。Koalas 还支持使用 DataFrame.plot() 方法进行可视化,就像 pandas 一样。通过这种方式,你可以利用 Koalas 扩展任何现有的基于 pandas 的机器学习代码,比如特征工程或自定义机器学习代码,而无需先用 PySpark 重写代码。
Koalas 是一个活跃的开源项目,得到了良好的社区支持。然而,Koalas 仍处于初期阶段,并且有一些局限性。目前,只有大约 70% 的 pandas API 在 Koalas 中可用,这意味着一些 pandas 代码可能无法直接使用 Koalas 实现。Koalas 和 pandas 之间存在一些实现差异,并且在 Koalas 中实现某些 pandas API 并不合适。处理缺失的 Koalas 功能的常见方法是将 Koalas DataFrame 转换为 pandas 或 PySpark DataFrame,然后使用 pandas 或 PySpark 代码来解决问题。Koalas DataFrame 可以通过 DataFrame.to_pandas() 和 DataFrame.to_spark() 函数分别轻松转换为 pandas 和 PySpark DataFrame。然而,需注意的是,Koalas 在幕后使用的是 Spark DataFrame,Koalas DataFrame 可能过大,无法在单台机器上转换为 pandas DataFrame,从而导致内存溢出错误。
摘要
在本章中,你学习了一些技术来水平扩展基于 Python 的标准机器学习库,如 scikit-learn、XGBoost 等。首先,介绍了使用 PySpark DataFrame API 扩展 EDA(探索性数据分析)的方法,并通过代码示例进行了展示。接着,介绍了结合使用 MLflow pyfunc 功能和 Spark DataFrame 来分布式处理机器学习模型的推断和评分技术。还介绍了使用 Apache Spark 进行令人尴尬的并行计算技术扩展机器学习模型的方法。此外,还介绍了使用名为 spark_sklearn 的第三方包对标准 Python 机器学习库训练的模型进行分布式调优的方法。然后,介绍了 pandas UDF(用户定义函数),它可以以向量化的方式扩展任意 Python 代码,用于在 PySpark 中创建高性能、低开销的 Python 用户定义函数。最后,Koalas 被介绍为一种让 pandas 开发者无需首先学习 PySpark API,就能使用类似 pandas 的 API,同时仍能利用 Apache Spark 在大规模数据处理中的强大性能和效率的方法。
第三部分:数据分析
一旦我们在数据湖中拥有清洗和集成的数据,并且已经在大规模上训练和构建了机器学习模型,最后一步就是以有意义的方式将可操作的见解传递给业务决策者,帮助他们做出商业决策。本节涵盖了数据分析中的商业智能(BI)和 SQL 分析部分。首先介绍了使用笔记本的各种数据可视化技术。接着,介绍了如何使用 Spark SQL 进行大规模的业务分析,并展示了将 BI 和 SQL 分析工具连接到 Apache Spark 集群的技术。最后,本节介绍了数据湖仓范式,它弥合了数据仓库和数据湖之间的差距,提供了一个单一的、统一的、可扩展的存储,满足数据工程、数据科学和商业分析等所有数据分析需求。
本节包括以下章节:
-
第十一章**,使用 PySpark 进行数据可视化
-
第十二章**,Spark SQL 入门
-
第十三章**,将外部工具与 Spark SQL 集成
-
第十四章**,数据湖仓
第十一章:使用 PySpark 进行数据可视化
到目前为止,从 第一章**, 分布式计算基础,到 第九章,机器学习生命周期管理,你已经学习了如何获取、整合和清洗数据,以及如何使数据适合用于分析。你还学会了如何使用清洗后的数据,通过数据科学和机器学习来解决实际的业务应用。本章将向你介绍如何利用数据可视化从数据中提取意义的基本知识。
在本章中,我们将涵盖以下主要主题:
-
数据可视化的重要性
-
使用 PySpark 进行数据可视化的技巧
-
PySpark 转换为 pandas 时的注意事项
数据可视化是通过使用图表、图形和地图等视觉元素,将数据图形化呈现的过程。数据可视化帮助你以视觉化的方式理解数据中的模式。在大数据的世界中,面对海量的数据,使用数据可视化来从中提取意义并以简单易懂的方式呈现给业务用户变得尤为重要;这有助于他们做出基于数据的决策。
技术要求
在本章中,我们将使用 Databricks Community Edition 来运行代码。
-
Databricks Community Edition 可以通过
community.cloud.databricks.com访问。 -
注册说明可以在
databricks.com/try-databricks找到。 -
本章的代码和数据可以从
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter11下载。
数据可视化的重要性
数据可视化是将数据转化为图形、图表或地图等形式的过程。这使得人类大脑更容易理解复杂的信息。通常,数据可视化是商业分析的最后阶段,也是任何数据科学过程的第一步。虽然有些专业人员专门从事数据可视化,但任何数据专业人员都需要能够理解和制作数据可视化。数据可视化帮助以易于理解的方式向业务用户传达隐藏在数据中的复杂模式。每个企业都需要信息以实现最佳性能,而数据可视化通过以视觉方式展示数据集之间的关系,并揭示可操作的见解,帮助企业做出更简单的数据驱动决策。随着大数据的到来,结构化和非结构化数据爆炸性增长,若没有可视化工具的帮助,很难理解这些数据。数据可视化通过揭示关键信息,加速决策过程,帮助业务用户迅速采取行动。数据可视化还帮助讲故事的过程,通过向正确的受众传递正确信息来传达信息。
数据可视化可以是一个简单的图表,代表当前业务状态的某一方面,也可以是一个复杂的销售报告,或是一个展示组织整体表现的仪表盘。数据可视化工具是解锁数据可视化潜力的关键。我们将在接下来的部分探讨不同类型的数据可视化工具。
数据可视化工具类型
数据可视化工具为我们提供了一个更简单的方式来创建数据可视化。它们通过提供图形用户界面、数据库连接和有时的数据处理工具,在单一统一的界面中,方便数据分析师和数据科学家创建数据可视化。有不同类型的数据可视化工具,每种工具都有略微不同的用途。在这一部分,我们将深入探讨这些工具。
商业智能工具
商业智能 (BI) 工具通常是企业级工具,帮助组织追踪和直观呈现其关键绩效指标 (KPIs)。BI 工具通常包括用于创建复杂逻辑数据模型的功能,并包含数据清洗和集成功能。BI 工具还包括连接到各种数据源的连接器,并内置了拖放功能的数据可视化,帮助业务用户快速创建数据可视化、运营和绩效仪表盘以及计分卡,以追踪某一部门或整个组织的表现。BI 工具的主要用户是参与制定战术和战略决策的业务分析师和高管。
BI 工具传统上使用数据仓库作为数据源,但现代 BI 工具支持 RDMS、NoSQL 数据库和数据湖作为数据源。一些著名的 BI 工具包括 Tableau、Looker、Microsoft Power BI、SAP Business Objects、MicroStrategy、IBM Cognos 和 Qlikview 等。BI 工具可以连接到 Apache Spark,并通过 ODBC 连接消费存储在 Spark SQL 表中的数据。这些概念将在 第十三章,与 Spark SQL 集成外部工具 中详细探讨。一类没有任何数据处理能力,但具备所有必要的数据可视化和数据连接组件的数据可视化工具,如 Redash,也可以通过 ODBC 连接到 Apache Spark。
可观察性工具
可观察性是一个持续监控和理解高度分布式系统中发生的事情的过程。可观察性帮助我们理解哪些地方变慢或出现故障,以及哪些方面需要修复以提高性能。然而,由于现代云环境是动态的,并且不断扩展和复杂化,大多数问题既不被知晓,也没有被监控。可观察性通过使你能够持续监控和暴露可能出现的问题,解决了现代云环境中常见的问题,这些问题具有动态性并且规模不断扩大。可观察性工具帮助企业持续监控系统和应用程序,并使企业能够获得关于系统行为的可操作见解,提前预测停机或问题的发生。数据可视化是可观察性工具的重要组成部分;一些流行的示例包括 Grafana 和 Kibana。
数据团队通常不负责监控和维护数据处理系统的健康状况——这通常由 DevOps 工程师等专业人员来处理。Apache Spark 默认没有与任何可观察性工具的直接集成,但它可以与流行的可观察性平台如 Prometheus 和 Grafana 集成。Apache Spark 与可观察性工具的集成超出了本书的范围,因此我们在此不做讨论。
Notebooks
Notebooks 是交互式计算工具,用于执行代码、可视化结果并分享见解。Notebooks 是数据科学过程中不可或缺的工具,并且在整个数据分析开发生命周期中变得越来越重要,正如你在本书中所见。Notebooks 也是优秀的数据可视化工具,它们帮助你将 Python 或 SQL 代码转化为易于理解的交互式数据可视化。一些 notebooks,如 Databricks、Jupyter 和 Zeppelin notebooks,甚至可以作为独立的仪表盘使用。本章剩余部分将重点介绍如何在使用 PySpark 时将 notebooks 用作数据可视化工具。
使用 PySpark 可视化数据的技术
Apache Spark 是一个统一的数据处理引擎,默认并不带有图形用户界面。正如前面部分所讨论的,经过 Apache Spark 处理的数据可以存储在数据仓库中,并使用 BI 工具进行可视化,或者使用笔记本进行本地可视化。在本节中,我们将重点介绍如何利用笔记本以交互方式使用 PySpark 处理和可视化数据。正如我们在本书中所做的那样,我们将使用 Databricks Community Edition 提供的笔记本,尽管 Jupyter 和 Zeppelin 笔记本也可以使用。
PySpark 本地数据可视化
没有任何数据可视化库可以原生地与 PySpark DataFrame 一起使用。然而,基于云的 Spark 分发版的笔记本实现,如 Databricks 和 Qubole,支持使用内置的 display() 函数原生可视化 Spark DataFrame。让我们看看如何在 Databricks Community Edition 中使用 display() 函数可视化 PySpark DataFrame。
我们将使用我们在 第六章 结束时制作的已清洗、集成和整理的数据集,特征工程 – 提取、转换和选择,如下所示的代码片段:
retail_df = spark.read.table("feature_store.retail_features")
viz_df = retail_df.select("invoice_num", "description",
"invoice_date", "invoice_month",
"country_code", "quantity",
"unit_price", "occupation",
"gender")
viz_df.display()
在前面的代码片段中,我们将一个表格读入了 Spark DataFrame,并选择了我们打算可视化的列。接着,我们在 Spark DataFrame 上调用了 display() 方法。结果是在笔记本中显示的一个网格,如下图所示:

图 11.1 – 网格小部件
上一截图显示了在 Databricks 笔记本中调用 display() 函数在 Spark DataFrame 上的结果。通过这种方式,任何 Spark DataFrame 都可以在 Databricks 笔记本中以表格格式进行可视化。该表格网格支持对任意列进行排序。Databricks 笔记本还支持图表和图形,这些可以在笔记本内使用。
提示
Databricks 的 display() 方法支持 Spark 所有编程 API,包括 Python、Scala、R 和 SQL。此外,display() 方法还可以渲染 Python pandas DataFrame。
我们可以使用相同的网格显示,并通过点击图表图标并从列表中选择所需的图表,将其转换为图表,如下图所示:

图 11.2 – 图表选项
正如我们所看到的,图表菜单有多个图表选项,柱状图排在列表的首位。如果选择柱状图,图表选项可以用来配置图表的关键字段、值字段和系列分组选项。类似地,我们也可以使用折线图或饼图,如下所示:

图 11.3 – 饼图
这里,display() 函数可以用于在笔记本中显示各种图表,并帮助配置各种图形选项。Databricks 笔记本还支持一个基础的地图小部件,可以在世界地图上可视化指标,如以下截图所示:

图 11.4 – 世界地图
上述截图展示了世界地图上的指标。由于我们的数据集只包含少数几个欧洲国家,法国和英国在地图小部件中已经被高亮显示。
注意
对于这个小部件,值应为 ISO 3166-1 alpha-3 格式的国家代码(例如 "GBR")或美国州的缩写(例如 "TX")。
除了基本的条形图和图表外,Databricks 笔记本还支持科学可视化,如散点图、直方图、分位数图和Q-Q 图,如下图所示:

图 11.5 – 分位数图
如前面图中所示,分位数图帮助判断两个数据集是否具有共同的分布。Databricks 笔记本可以通过图表菜单访问分位数图,图表属性如键、值和系列分组可以通过图表选项菜单进行设置。
我们可以使用以下代码使 Databricks 笔记本显示图像:
image_df = spark.read.format("image").load("/FileStore/FileStore/shared_uploads/images")
上述代码片段使用 Apache Spark 内置的图像数据源从持久存储(如数据湖)中的目录加载图像:

图 11.6 – 图像数据
这张图像通过 Databricks 的 display() 函数在笔记本中渲染显示,因为它能够显示图像预览。
Databricks 笔记本还能够渲染机器学习特定的可视化内容,例如使用 display() 函数可视化我们训练的决策树模型,如下图所示:

图 11.7 – 决策树模型
上述截图展示了我们使用 Spark ML 构建的决策树模型,并在 Databricks 笔记本中渲染显示。
提示
更多关于使用 Databricks 笔记本渲染机器学习特定可视化的信息,请参阅 Databricks 的公开文档:docs.databricks.com/notebooks/visualizations/index.html#machine-learning-visualizations。
使用 JavaScript 和 HTML 的交互式可视化
Databricks 笔记本还支持 displayHTML() 函数。你可以将任意 HTML 代码传递给 displayHTML(),并将其渲染在笔记本中,如以下代码片段所示:
displayHTML("<a href ='/files/image.jpg'>Arbitrary Hyperlink</a>")
上述代码片段在笔记本中显示了一个任意的 HTML 超链接。其他 HTML 元素,如段落、标题、图片等,也可以与 displayHTML() 函数一起使用。
提示
可以使用超链接、图像和表格等 HTML 块来使你的笔记本更具描述性和交互性,这些可以帮助讲述过程中的交互性。
同样地,可以使用displayHTML()函数来渲染 SVG 图形,如下代码块所示:
displayHTML("""<svg width="400" height="400">
<ellipse cx="300" cy="300" rx="100" ry="60" style="fill:orange">
<animate attributeType="CSS" attributeName="opacity" from="1" to="0" dur="5s" repeatCount="indefinite" />
</ellipse>
</svg>""")
上述代码渲染了一个橙色的动画椭圆,可以淡入淡出。还可以渲染更复杂的 SVG 图形,并且可以传递来自 Spark DataFrame 的数据。同样,流行的基于 HTML 和 JavaScript 的可视化库也可以与 Databricks 笔记本一起使用,如下所示:

图 11.8 – 使用 D3.js 的词云
在这里,我们从我们在前几章节中处理数据时创建的retail_sales Delta 表中取出了description列,然后从商品描述列中提取了各个单词。接着,我们利用 HTML、CSS 和 JavaScript 使用词云可视化渲染了这些单词。之后,我们使用流行的 D3.js JavaScript 库基于数据操作 HTML 文档。这个可视化的代码可以在github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/blob/main/Chapter11/databricks-charts-graphs.py找到。
到目前为止,您已经看到了一些通过 Databricks 笔记本界面可以直接与 Spark DataFrame 一起使用的基本和统计图表。然而,有时您可能需要一些在笔记本中不可用的额外图表和图形,或者您可能需要更多对图表的控制。在这些情况下,可以使用诸如matplotlib、plotly、seaborn、altair、bokeh等流行的 Python 可视化库与 PySpark 一起使用。我们将在下一节中探讨一些这些可视化库。
使用 PySpark 进行 Python 数据可视化
正如您在前一节中学到的那样,PySpark 本身并不具备任何可视化能力,但您可以选择使用 Databricks 笔记本功能来在 Spark DataFrame 中可视化数据。在无法使用 Databricks 笔记本的情况下,您可以选择使用流行的基于 Python 的可视化库,在任何您熟悉的笔记本界面中进行数据可视化。在本节中,我们将探讨一些著名的 Python 可视化库以及如何在 Databricks 笔记本中使用它们进行数据可视化。
使用 Matplotlib 创建二维图
使用像pip这样的包管理器在PyPI仓库中获取。以下代码示例展示了如何在 PySpark 中使用 Matplotlib:
import pandas as pd
import matplotlib.pyplot as plt
retail_df = spark.read.table("feature_store.retail_features")
viz_df = retail_df.select("invoice_num", "description",
"invoice_date", "invoice_month",
"country_code", "quantity",
"unit_price", "occupation",
"gender")
pdf = viz_df.toPandas()
pdf['quantity'] = pd.to_numeric(pdf['quantity'],
errors='coerce')
pdf.plot(kind='bar', x='invoice_month', y='quantity',
color='orange')
在前面的代码片段中,我们执行了以下操作:
-
首先,我们导入了
pandas和matplotlib库,假设它们已经在笔记本中安装好了。 -
然后,我们使用在前几章的数据处理步骤中创建的在线零售数据集生成了一个包含所需列的 Spark DataFrame。
-
由于基于 Python 的可视化库不能直接使用 Spark DataFrame,我们将 Spark DataFrame 转换为 pandas DataFrame。
-
接着,我们将数量列转换为数值数据类型,以便进行绘制。
-
之后,我们使用 Matplotlib 库的
plot()方法在 pandas DataFrame 上定义了一个图表,指定生成的图表类型为柱状图,并传入了 x 轴和 y 轴的列名。 -
某些笔记本环境可能需要显式调用
display()函数才能显示图表。
通过这种方式,如果我们将 Spark DataFrame 转换为 pandas DataFrame,就可以使用 Matplotlib。生成的图表如下所示:

图 11.9 – Matplotlib 可视化
上一个图表显示了在某一特定时间段内已售出的商品数量。
使用 Seaborn 进行科学可视化
pip。以下代码示例展示了如何在 PySpark DataFrame 中使用 Seaborn:
import matplotlib.pyplot as plt
import seaborn as sns
data = retail_df.select("unit_price").toPandas()["unit_price"]
plt.figure(figsize=(10, 3))
sns.boxplot(data)
在上一个代码片段中,我们执行了以下操作:
-
首先,我们导入了
matplotlib和seaborn库。 -
接下来,我们将包含名为
unit_price的单列的 Spark DataFrame 使用toPandas()PySpark 函数转换为 pandas DataFrame。 -
接着,我们使用
plot.figure()Matplotlib 方法定义了图表的尺寸。 -
最后,我们通过调用
seaborn.boxplot()方法并传入包含单列的 pandas DataFrame 绘制了箱线图。生成的图表如下所示:

图 11.10 – Seaborn 箱线图可视化
上一个截图显示了如何使用 unit_price 列的最小值、第一个四分位数、中位数、第三个四分位数和最大值将其分布为箱线图。
使用 Plotly 进行交互式可视化
Plotly 是一个基于 JavaScript 的可视化库,使得 Python 用户能够创建交互式的网页可视化,并可以在笔记本中展示或保存为独立的 HTML 文件。Plotly 已预装在 Databricks 中,可以按以下方式使用:
import plotly.express as plot
df = viz_df.toPandas()
fig = plot.scatter(df, x="fin_wt", y="quantity",
size="unit_price", color="occupation",
hover_name="country_code", log_x=True,
size_max=60)
fig.show()
在上一个代码片段中,我们执行了以下操作:
-
首先,我们导入了
matplotlib和seaborn库。 -
接下来,我们将包含所需列的 Spark DataFrame 转换为 pandas DataFrame。
-
然后,我们使用
plot.scatter()方法定义了 Plotly 图表的参数。该方法配置了一个具有三维坐标的散点图。 -
最后,我们使用
fig.show()方法渲染了图表。生成的图表如下所示:

图 11.11 – Plotly 气泡图可视化
上面的截图展示了一个气泡图,显示了三个指标在三个维度上的分布。该图是互动式的,当您将鼠标悬停在图表的不同部分时,会显示相关信息。
使用 Altair 的声明性可视化
Altair 是一个用于 Python 的声明性统计可视化库。Altair 基于一个名为Vega的开源声明性语法引擎。Altair 还提供了一种简洁的可视化语法,使用户能够快速构建各种各样的可视化图表。可以使用以下命令安装它:
%pip install altair
上面的命令将 Altair 安装到笔记本的本地 Python 内核中,并重新启动它。一旦 Altair 成功安装后,可以像通常那样使用 Python 的import语句来调用它,代码示例如下:
import altair as alt
import pandas as pd
source = (viz_df.selectExpr("gender as Gender", "trim(occupation) as Occupation").where("trim(occupation) in ('Farming-fishing', 'Handlers-cleaners', 'Prof-specialty', 'Sales', 'Tech-support') and cust_age > 49").toPandas())
在前面的代码片段中,我们导入了 Altair 和 pandas 库。然后,我们从 Spark 表中选择所需的列,并将其转换为 pandas DataFrame。一旦数据在 Python 中以 pandas DataFrame 的形式存在,就可以使用 Altair 来创建图表,如下所示:

图 11.12 – Altair 等型图可视化
上面的图展示了一个等型图可视化,显示了不同国家按性别分布的职业情况。其他开源库,如bokeh、pygal和leather,也可以用于可视化 PySpark DataFrame。Bokeh 是另一个流行的 Python 数据可视化库,提供高性能的互动图表和图形。Bokeh 基于 JavaScript 和 HTML,与 Matplotlib 不同,它允许用户创建自定义可视化图表。关于在 Databricks 笔记本中使用 Bokeh 的信息,可以在 Databricks 的公共文档中找到,网址为docs.databricks.com/notebooks/visualizations/bokeh.html#bokeh。
到目前为止,您已经学习了如何通过将 PySpark DataFrame 转换为 pandas DataFrame,使用一些在 Python 中与 Spark DataFrame 兼容的流行可视化方法。然而,在将 PySpark DataFrame 转换为 pandas DataFrame 时,您需要考虑一些性能问题和限制。我们将在下一部分讨论这些问题。
PySpark 到 pandas 转换的注意事项
本节将介绍pandas,演示 pandas 与 PySpark 的差异,并介绍在 PySpark 与 pandas 之间转换数据集时需要注意的事项。
pandas 简介
pandas 是 Python 中最广泛使用的开源数据分析库之一。它包含了一系列用于处理、操作、清洗、整理和转换数据的实用工具。与 Python 的列表、字典和循环相比,pandas 更加易于使用。从某种意义上讲,pandas 类似于其他统计数据分析工具,如 R 或 SPSS,这使得它在数据科学和机器学习爱好者中非常受欢迎。
pandas 的主要抽象是Series和DataFrames,前者本质上是一个一维数组,后者是一个二维数组。pandas 和 PySpark 之间的基本区别之一在于,pandas 将其数据集表示为一维和二维的NumPy数组,而 PySpark 的 DataFrames 是基于 Spark SQL 的Row和Column对象的集合。虽然 pandas DataFrames 只能通过 pandas DSL 进行操作,但 PySpark DataFrames 可以使用 Spark 的 DataFrame DSL 和 SQL 进行操作。由于这个差异,熟悉使用 pandas 的开发者可能会发现 PySpark 不同,并且在使用该平台时可能会遇到学习曲线。Apache Spark 社区意识到这一难题,启动了一个新的开源项目——Koalas。Koalas 在 Spark DataFrames 上实现了类似 pandas 的 API,以尝试克服 pandas 和 PySpark 之间的差异。关于如何使用 Koalas 的更多信息将在第十章,使用 PySpark 扩展单节点机器学习中介绍。
注意
NumPy 是一个用于科学计算的 Python 包,它提供了多维数组和一组用于快速操作数组的例程。有关 NumPy 的更多信息,请访问:numpy.org/doc/stable/user/whatisnumpy.html。
另一个基本区别是在大数据和处理大规模数据的上下文中,pandas 是为了处理单台机器上的数据而设计的,而 PySpark 从设计上就是分布式的,可以以大规模并行的方式在多台机器上处理数据。这突显了 pandas 与 PySpark 相比的一个重要限制,以及开发者在从 pandas 转换到 PySpark 时需要考虑的一些关键因素。我们将在接下来的章节中探讨这些内容。
从 PySpark 转换到 pandas
PySpark API 提供了一个方便的实用函数 DataFrame.toPandas(),可以将 PySpark DataFrame 转换为 pandas DataFrame。该函数在本章中有多次演示。如果你回顾一下我们在第一章,“分布式计算入门”中的讨论,尤其是关于 Spark 集群架构 部分,Spark 集群由 Driver 进程和一组在工作节点上运行的执行进程组成,Driver 负责编译用户代码,将其传递给工作节点,管理并与工作节点通信,并在需要时从工作节点聚合和收集数据。而 Spark 工作节点则负责所有数据处理任务。然而,pandas 并非基于分布式计算范式,它仅在单一计算机上运行。因此,当你在 Spark 集群上执行 pandas 代码时,它会在 Driver 或 Master 节点上执行,如下图所示:

图 11.13 – PySpark 架构
如我们所见,当在 Spark DataFrame 上调用 Python 和 toPandas() 函数时,它会从所有 Spark 工作节点收集行数据,然后在 Driver 内部的 Python 内核上创建一个 pandas DataFrame。
这个过程面临的第一个问题是 toPandas() 函数实际上会将所有数据从工作节点收集并带回 Driver。如果收集的数据集过大,这可能会导致 Driver 内存不足。另一个问题是,默认情况下,Spark DataFrame 的 Row 对象会作为 list 的 tuples 收集到 Driver 上,然后再转换为 pandas DataFrame。这通常会消耗大量内存,有时甚至会导致收集到的数据占用的内存是 Spark DataFrame 本身的两倍。
为了减轻 PySpark 转换为 pandas 时的内存问题,可以使用 Apache Arrow。Apache Arrow 是一种内存中的列式数据格式,类似于 Spark 内部数据集的表示方式,且在 JVM 和 Python 进程之间传输数据时非常高效。默认情况下,Spark 没有启用 Apache Arrow,需要通过将 spark.sql.execution.arrow.enabled Spark 配置设置为 true 来启用它。
注意
PyArrow,Apache Arrow 的 Python 绑定,已在 Databricks Runtime 中预装。然而,你可能需要安装适用于你的 Spark 集群和 Python 版本的 PyArrow 版本。
Apache Arrow 有助于缓解使用 toPandas() 时可能出现的一些内存问题。尽管进行了优化,但转换操作仍然会导致 Spark DataFrame 中的所有记录被收集到 Driver 中,因此你应该仅在原始数据的一个小子集上执行转换。因此,通过利用 PyArrow 格式并小心地对数据集进行抽样,你仍然可以在笔记本环境中使用所有与标准 Python 兼容的开源可视化库来可视化你的 PySpark DataFrame。
总结
在本章中,你了解了使用数据可视化以简单的方式传达复杂数据集中的含义的重要性,以及如何轻松地将数据模式呈现给业务用户。介绍了多种使用 Spark 进行数据可视化的策略。你还学习了如何在 Databricks 笔记本中原生使用 PySpark 进行数据可视化。我们还探讨了使用普通 Python 可视化库来可视化 Spark DataFrame 数据的技巧。介绍了一些知名的开源可视化库,如 Matplotlib、Seaborn、Plotly 和 Altair,并提供了它们的实际使用示例和代码示例。最后,你了解了在 PySpark 中使用普通 Python 可视化时的陷阱,PySpark 转换的需求,以及克服这些问题的一些策略。
下一章将讨论如何将各种 BI 和 SQL 分析工具连接到 Spark,这将帮助你执行临时数据分析并构建复杂的操作和性能仪表板。
第十二章:Spark SQL 入门
在上一章中,你了解了数据可视化作为数据分析的强大工具。你还学习了可以用于可视化 pandas DataFrame 数据的各种 Python 可视化库。另一个同样重要、普遍且必不可少的技能是结构化查询语言(SQL)。SQL 自数据分析领域诞生以来一直存在,即使在大数据、数据科学和机器学习(ML)兴起的今天,SQL 仍然被证明是不可或缺的。
本章将向你介绍 SQL 的基础知识,并探讨如何通过 Spark SQL 在分布式计算环境中应用 SQL。你将了解构成 Spark SQL 的各种组件,包括存储、元数据存储和实际的查询执行引擎。我们将比较Hadoop Hive和 Spark SQL 之间的差异,最后介绍一些提高 Spark SQL 查询性能的技巧。
在本章中,我们将涵盖以下主要内容:
-
SQL 简介
-
Spark SQL 简介
-
Spark SQL 语言参考
-
优化 Spark SQL 性能
本章涉及的内容包括 SQL 作为数据切片和切割语言的实用性、Spark SQL 的各个组件,以及它们如何结合起来在 Apache Spark 上创建一个强大的分布式 SQL 引擎。你将查阅 Spark SQL 语言参考,以帮助你的数据分析需求,并学习一些优化 Spark SQL 查询性能的技巧。
技术要求
本章所需的内容如下:
-
在本章中,我们将使用 Databricks Community Edition 运行我们的代码 (https://community.cloud.databricks.com)。注册说明可以在
databricks.com/try-databricks找到。 -
本章使用的代码和数据可以从
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter12下载。
SQL 简介
SQL 是一种声明式语言,用于存储、操作和查询存储在关系型数据库中的数据,也称为关系数据库管理系统(RDBMSes)。关系型数据库中的数据以表格形式存储,表格包含行和列。在现实世界中,实体之间存在关系,关系型数据库试图将这些现实世界中的关系模拟为表格之间的关系。因此,在关系型数据库中,单个表格包含与特定实体相关的数据,这些表格可能存在关联。
SQL 是一种声明式编程语言,帮助你指定想要从给定表中检索的行和列,并指定约束以过滤掉任何数据。RDBMS 包含一个查询优化器,它将 SQL 声明转换为查询计划,并在数据库引擎上执行。查询计划最终被转换为数据库引擎的执行计划,用于读取表中的行和列到内存中,并根据提供的约束进行过滤。
SQL 语言包括定义模式的子集——称为数据定义语言(DDL)——以及修改和查询数据的子集——称为数据操作语言(DML),如以下章节所述。
DDL
CREATE、ALTER、DROP、TRUNCATE等。以下 SQL 查询表示一个 DDL SQL 语句:
CREATE TABLE db_name.schema_name.table_name (
column1 datatype,
column2 datatype,
column3 datatype,
....
);
上一个 SQL 语句表示在特定数据库中创建新表的典型命令,并定义了几个列及其数据类型。数据库是数据和日志文件的集合,而模式是数据库中的逻辑分组。
DML
SELECT、UPDATE、INSERT、DELETE、MERGE等。以下是一个 DML 查询示例:
SELECT column1, SUM(column2) AS agg_value
FROM db_name.schema_name.table_name
WHERE column3 between value1 AND value2
GROUP BY column1
ORDER BY SUM(column2)
);
上一个查询结果通过对column1的每个不同值进行聚合,在基于column3上指定的约束过滤行后,最终根据聚合值对结果进行排序。
注意
尽管 SQL 通常遵循美国国家标准协会(ANSI)设定的某些标准,但每个 RDBMS 供应商对 SQL 标准的实现略有不同,您应参考特定 RDBMS 的文档以获取正确的语法。
上一个 SQL 语句表示标准的 DDL 和 DML 查询;然而,每个 RDBMS 在 SQL 标准的实现上可能会有细微的差异。同样,Apache Spark 也有自己对 ANSI SQL 2000 标准的实现。
连接与子查询
关系型数据库中的表包含相关数据,通常需要在不同表之间进行连接,以生成有意义的分析。因此,SQL 支持诸如连接和子查询等操作,使用户能够跨表合并数据,如以下 SQL 语句所示:
SELECT a.column1, b.cloumn2, b.column3
FROM table1 AS a JOIN table2 AS b
ON a.column1 = b.column2
在之前的 SQL 查询中,我们使用一个公共键列连接了两个表,并在JOIN操作后从这两个表中生成了列。类似地,子查询是查询中的查询,可以出现在SELECT、WHERE或FROM子句中,它允许你从多个表中合并数据。接下来的章节将探讨在 Spark SQL 中这些 SQL 查询的具体实现。
基于行存储与列存储
数据库以两种方式之一物理存储数据,要么以行存储方式,要么以列存储方式。每种方式都有其优缺点,取决于使用场景。在行存储中,所有的值一起存储,而在列存储中,单个列的所有值会连续存储在物理存储介质上,如下图所示:

图 12.1 – 行存储与列存储
如前面截图所示,在行存储中,整行及其所有列值会被一起存储在物理存储介质上。这使得查找单个行并快速高效地从存储中检索其所有列变得更容易。而列存储则将单个列的所有值连续存储在物理存储介质上,这使得检索单个列既快速又高效。
行存储在事务系统中更为流行,在这些系统中,快速检索单个事务记录或行更为重要。另一方面,分析系统通常处理的是行的聚合,只需要每个查询检索少量的列。因此,在设计分析系统时,选择列存储更加高效。列存储还提供了更好的数据压缩比,从而在存储大量历史数据时,能够更有效地利用可用存储空间。包括 数据仓库 和 数据湖 在内的分析存储系统更倾向于使用列存储而非行存储。流行的大数据文件格式如 Parquet 和 优化行列存储(ORC)也采用列存储。
SQL 的易用性和普遍性促使许多非关系型数据处理框架的创建者,如 Hadoop 和 Apache Spark,采纳 SQL 的子集或变体来创建 Hadoop Hive 和 Spark SQL。我们将在接下来的章节中详细探讨 Spark SQL。
Spark SQL 简介
Spark SQL 为 Apache Spark 提供了原生的 SQL 支持,并统一了查询存储在 Spark DataFrames 和外部数据源中的数据的过程。Spark SQL 将 DataFrames 和关系表统一,使得开发者可以轻松地将 SQL 命令与外部数据查询结合,进行复杂的分析。随着 Apache Spark 1.3 的发布,Spark SQL 驱动的 Spark DataFrames 成为表达数据处理代码的事实标准抽象方式,而 弹性分布式数据集(RDDs)仍然是 Spark 的核心抽象方法,如下图所示:

图 12.2 – Spark SQL 架构
如前面的图所示,你可以看到现在大多数 Spark 组件都利用了 Spark SQL 和 DataFrames。Spark SQL 提供了关于数据结构和正在执行的计算的更多信息,Spark SQL 引擎利用这些额外的信息对查询进行进一步的优化。使用 Spark SQL,Spark 的所有组件——包括结构化流、DataFrames、Spark ML和GraphFrames——以及所有的编程应用程序编程接口(APIs)——包括Scala、Java、Python、R和SQL——都使用相同的执行引擎来表达计算。这种统一性使你可以轻松地在不同的 API 之间切换,并让你根据当前任务选择合适的 API。某些数据处理操作,如连接多个表格,在 SQL 中表达起来更容易,开发者可以轻松地将SQL与Scala、Java或Python代码混合使用。
Spark SQL 还引入了一个强大的新优化框架,名为Catalyst,它可以自动将任何数据处理代码,不论是使用 Spark DataFrames 还是使用 Spark SQL 表达的,转换为更高效的执行方式。我们将在接下来的章节中深入探讨Catalyst优化器。
Catalyst 优化器
SQL 查询优化器在关系型数据库管理系统(RDBMS)中是一个过程,它确定给定 SQL 查询处理存储在数据库中的数据的最有效方式。SQL 优化器尝试生成给定 SQL 查询的最优执行方式。优化器通常会生成多个查询执行计划,并从中选择最优的一个。它通常会考虑因素如中央处理单元(CPU)、输入/输出(I/O)以及查询的表格的任何可用统计信息,以选择最优的查询执行计划。优化器根据选择的查询执行计划,决定以任何顺序重新排序、合并和处理查询,以获得最优结果。
Spark SQL 引擎也配备了一个名为Catalyst的查询优化器。Catalyst基于函数式编程的概念,像 Spark 的其他代码库一样,利用 Scala 编程语言的特性来构建一个强大且可扩展的查询优化器。Spark 的 Catalyst 优化器通过一系列步骤为给定的 Spark SQL 查询生成一个最优执行计划,如下图所示:

图 12.3 – Spark 的 Catalyst 优化器
如前图所示,Catalyst 优化器首先在解析引用后生成逻辑计划,然后基于标准的规则优化技术优化逻辑计划。接着,使用优化后的逻辑计划生成一组物理执行计划,并选择最佳的物理计划,最终使用最佳的物理计划生成Java 虚拟机(JVM)字节码。这个过程使得 Spark SQL 能够将用户查询转化为最佳的数据处理代码,而开发者无需了解 Spark 分布式数据处理范式的微观细节。此外,Spark SQL DataFrame API 在 Java、Scala、Python 和 R 编程语言中的实现都经过相同的 Catalyst 优化器。因此,无论使用哪种编程语言编写的 Spark SQL 或任何数据处理代码,性能都是相当的。
提示
在某些情况下,PySpark DataFrame 的代码性能可能无法与 Scala 或 Java 代码相比。一个例子是当在 PySpark DataFrame 操作中使用非矢量化的用户定义函数(UDFs)时。Catalyst 无法查看 Python 中的 UDF,因此无法优化代码。因此,应该将其替换为 Spark SQL 的内置函数或在 Scala 或 Java 中定义的 UDF。
一位经验丰富的数据工程师,如果对 RDD API 有深入了解,可能写出比 Catalyst 优化器更优化的代码;然而,通过让 Catalyst 处理代码生成的复杂性,开发者可以将宝贵的时间集中在实际的数据处理任务上,从而提高效率。在深入了解 Spark SQL 引擎的内部工作原理后,理解 Spark SQL 可以处理的数据源类型会非常有用。
Spark SQL 数据源
由于 Spark DataFrame API 和 SQL API 都基于由 Catalyst 优化器提供支持的相同 Spark SQL 引擎,它们也支持相同的数据源。这里展示了一些突出的 Spark SQL 数据源。
文件数据源
Spark SQL 原生支持基于文件的数据源,例如 Parquet、ORC、Delta 等,以下是一个 SQL 查询示例:
SELECT * FROM delta.'/FileStore/shared_uploads/delta/retail_features.delta' LIMIT 10;
在前面的 SQL 语句中,数据直接从 delta. 前缀查询。同样的 SQL 语法也可以用于数据湖中的 Parquet 文件位置。
其他文件类型,如JavaScript 对象表示法(JSON)和逗号分隔值(CSV),需要先在元数据存储中注册表或视图,因为这些文件不自描述,缺乏固有的模式信息。以下是使用 CSV 文件与 Spark SQL 的示例 SQL 查询:
CREATE OR REPLACE TEMPORARY VIEW csv_able
USING csv
OPTIONS (
header "true",
inferSchema "true",
path "/FileStore/ConsolidatedCities.csv"
);
SELECT * FROM csv_able LIMIT 5;
在前面的 SQL 语句中,我们首先使用数据湖中的 CSV 文件通过 CSV 数据源创建一个临时视图。我们还使用 OPTIONS 来指定 CSV 文件包含标题行,并从文件本身推断出模式。
注意
元存储是一个 RDBMS 数据库,Spark SQL 将数据库、表、列和分区等元数据信息持久化存储在其中。
如果表需要在集群重启后保持持久化,并且将来会被重用,您也可以创建一个永久表而不是临时视图。
JDBC 数据源
现有的 RDBMS 数据库也可以通过 Java 数据库连接(JDBC)与元存储进行注册,并作为 Spark SQL 的数据源。以下代码块展示了一个示例:
CREATE TEMPORARY VIEW jdbcTable
USING org.apache.spark.sql.jdbc
OPTIONS (
url "jdbc:mysql://localhost:3306/pysparkdb",
dbtable "authors",
user 'username',
password 'password'
);
SELECT * FROM resultTable;
在前面的代码块中,我们使用 jdbc 数据源创建了一个临时视图,并指定了数据库连接选项,如数据库 统一资源定位符(URL)、表名、用户名、密码等。
Hive 数据源
Apache Hive 是 Hadoop 生态系统中的一个数据仓库,可以使用 SQL 读取、写入和管理存储在 Hadoop 文件系统或数据湖中的数据集。Spark SQL 可以与 Apache Hive 一起使用,包括 Hive 元存储、Hive 序列化/反序列化器(SerDes)以及 Hive UDF。Spark SQL 支持大多数 Hive 特性,如 Hive 查询语言、Hive 表达式、用户定义的聚合函数、窗口函数、连接、并集、子查询等。然而,像 Hive 原子性、一致性、隔离性、持久性(ACID)表更新、Hive I/O 格式以及某些 Hive 特定优化等特性是不支持的。支持和不支持的特性完整列表可以在 Databricks 的公共文档中找到,链接如下:docs.databricks.com/spark/latest/spark-sql/compatibility/hive.html。
现在,您已经了解了 Spark SQL 组件,如 Catalyst 优化器及其数据源,我们可以深入探讨 Spark SQL 特定的语法和函数。
Spark SQL 语言参考
作为 Hadoop 生态系统的一部分,Spark 一直以来都与 Hive 兼容。尽管 Hive 查询语言与 ANSI SQL 标准有很大差异,Spark 3.0 的 Spark SQL 可以通过配置 spark.sql.ansi.enabled 来实现 ANSI SQL 兼容。启用此配置后,Spark SQL 将使用 ANSI SQL 兼容的方言,而不是 Hive 方言。
即使启用了 ANSI SQL 兼容,Spark SQL 可能仍无法完全符合 ANSI SQL 方言,本节将探讨一些 Spark SQL 的突出 DDL 和 DML 语法。
Spark SQL DDL
使用 Spark SQL 创建数据库和表的语法如下所示:
CREATE DATABASE IF NOT EXISTS feature_store;
CREATE TABLE IF NOT EXISTS feature_store.retail_features
USING DELTA
LOCATION '/FileStore/shared_uploads/delta/retail_features.delta';
在前面的代码块中,我们执行了以下操作:
-
首先,如果数据库不存在,我们使用
CREATE DATABASE命令创建一个数据库。通过此命令,还可以指定一些选项,如持久存储中的物理仓库位置和其他数据库属性。 -
然后,我们使用
delta作为数据源创建一个表,并指定数据的位置。这里,指定位置的数据已经存在,因此无需指定任何模式信息,如列名及其数据类型。然而,为了创建一个空的表结构,仍然需要指定列及其数据类型。
要更改现有表的某些属性,例如重命名表、修改或删除列,或修改表分区信息,可以使用 ALTER 命令,如下所示的代码示例所示:
ALTER TABLE feature_store.retail_features RENAME TO feature_store.etailer_features;
ALTER TABLE feature_store.etailer_features ADD COLUMN (new_col String);
在前面的代码示例中,我们在第一个 SQL 语句中重命名了表。第二个 SQL 语句修改了表并添加了一个新的 String 类型的列。在 Spark SQL 中,仅支持更改列注释和添加新列。以下代码示例展示了 Spark SQL 语法,用于完全删除或删除对象:
TRUNCATE TABLE feature_store.etailer_features;
DROP TABLE feature_store.etailer_features;
DROP DATABASE feature_store;
在前面的代码示例中,TRUNCATE 命令删除了表中的所有行,并保持表结构和模式不变。DROP TABLE 命令删除了表及其模式,而 DROP DATABASE 命令则删除整个数据库。
Spark DML
数据操作涉及从表中添加、修改和删除数据。以下代码语句展示了一些示例:
INSERT INTO feature_store.retail_features
SELECT * FROM delta.'/FileStore/shared_uploads/delta/retail_features.delta';
前面的 SQL 语句通过另一个 SQL 查询的结果将数据插入到现有表中。类似地,INSERT OVERWRITE 命令可用于覆盖现有数据,然后将新数据加载到表中。以下 SQL 语句可用于选择性地删除表中的数据:
DELETE FROM feature_store.retail_features WHERE country_code = 'FR';
前面的 SQL 语句根据筛选条件从表中删除选择性的数据。虽然 SELECT 语句不是必须的,但它们在数据分析中至关重要。以下 SQL 语句展示了使用 SELECT 语句进行 Spark SQL 数据分析:
SELECT
year AS emp_year
max(m.last_name),
max(m.first_name),
avg(s.salary) AS avg_salary
FROM
author_salary s
JOIN mysql_authors m ON m.uid = s.id
GROUP BY year
ORDER BY s.salary DESC
前面的 SQL 语句根据公共键对两个表进行内连接,并按年份计算每位员工的平均薪资。此查询的结果提供了员工薪资随年份变化的见解,并且可以轻松安排定期刷新。
通过这种方式,使用 Apache Spark 强大的分布式 SQL 引擎及其富有表现力的 Spark SQL 语言,您可以在无需学习任何新编程语言的情况下,以快速高效的方式执行复杂的数据分析。有关支持的数据类型、函数库和 SQL 语法的完整参考指南,可以在 Apache Spark 的公共文档中找到:spark.apache.org/docs/latest/sql-ref-syntax.html。
尽管 Spark SQL 的Catalyst优化器承担了大部分优化工作,但了解一些进一步调整 Spark SQL 性能的技巧还是很有帮助的,以下部分将介绍几个显著的技巧。
优化 Spark SQL 性能
在上一节中,你了解了 Catalyst 优化器是如何通过将代码运行经过一系列优化步骤,直到得出最佳执行计划,从而优化用户代码的。为了充分利用 Catalyst 优化器,推荐使用利用 Spark SQL 引擎的 Spark 代码——即 Spark SQL 和 DataFrame API——并尽量避免使用基于 RDD 的 Spark 代码。Catalyst 优化器无法识别 UDF(用户定义函数),因此用户可能会编写出次优的代码,从而导致性能下降。因此,推荐使用内置函数,而不是 UDF,或者在 Scala 和 Java 中定义函数,然后在 SQL 和 Python API 中使用这些函数。
尽管 Spark SQL 支持基于文件的格式,如 CSV 和 JSON,但推荐使用序列化数据格式,如 Parquet、AVRO 和 ORC。半结构化格式(如 CSV 或 JSON)会带来性能开销,首先是在模式推断阶段,因为它们无法将模式直接提供给 Spark SQL 引擎。其次,它们不支持诸如谓词下推(Predicate Pushdown)之类的数据过滤功能,因此必须将整个文件加载到内存中,才能在源头过滤掉数据。作为天生的无压缩文件格式,CSV 和 JSON 相比于 Parquet 等二进制压缩格式,也消耗更多的内存。甚至传统的关系型数据库比使用半结构化数据格式更为推荐,因为它们支持谓词下推,并且可以将一些数据处理任务委托给数据库。
对于诸如机器学习(ML)等迭代工作负载,数据集被多次访问的情况下,将数据集缓存到内存中非常有用,这样后续对表或 DataFrame 的扫描就会在内存中进行,从而大大提高查询性能。
Spark 提供了各种 BROADCAST、MERGE、SHUFFLE_HASH 等。然而,Spark SQL 引擎有时可能无法预测某个查询的策略。可以通过将提示传递给 Spark SQL 查询来缓解这个问题,如下代码块所示:
SELECT /*+ BROADCAST(m) */
year AS emp_year
max(m.last_name),
max(m.first_name),
avg(s.salary) AS avg_salary
FROM
author_salary s
JOIN mysql_authors m ON m.uid = s.id
GROUP BY year
ORDER BY s.salary DESC;
在前面的代码块中,我们传入了一个SELECT子句。这指定了较小的表会被广播到所有的工作节点,这应该能够提高连接操作的性能,从而提升整体查询性能。同样,COALESCE和REPARTITION提示也可以传递给 Spark SQL 查询;这些提示减少了输出文件的数量,从而提升了性能。
注意
SQL 提示、查询提示或优化器提示是标准 SQL 语句的补充,用于提示 SQL 执行引擎选择开发者认为最优的特定物理执行计划。SQL 提示在所有关系型数据库管理系统(RDBMS)引擎中通常都得到支持,现在 Spark SQL 也支持某些类型的查询,如前所述。
虽然 Catalyst 优化器在生成最佳物理查询执行计划方面表现出色,但仍然可能会因为表上的陈旧统计数据而受到影响。从 Spark 3.0 开始,spark.sql.adaptive.enabled 配置项得以引入。这些只是 Spark SQL 性能调优技术中的一部分,每项技术的详细描述可以在 Apache Spark 官方文档中找到,链接如下:spark.apache.org/docs/latest/sql-performance-tuning.html。
摘要
本章介绍了 SQL 作为一种声明式语言,它因易用性和表达能力而被普遍接受为结构化数据分析的语言。你了解了 SQL 的基本构造,包括 SQL 的 DDL 和 DML 方言。你还介绍了 Spark SQL 引擎,这是一个统一的分布式查询引擎,支持 Spark SQL 和 DataFrame API。一般来说,介绍了 SQL 优化器,Spark 自己的查询优化器 Catalyst 也做了介绍,并说明了它如何将 Spark SQL 查询转换为 Java JVM 字节码。还介绍了 Spark SQL 语言的参考资料,以及最重要的 DDL 和 DML 语句,并提供了示例。最后,我们讨论了一些性能优化技术,帮助你在数据分析过程中充分发挥 Spark SQL 的优势。在下一章,我们将进一步拓展 Spark SQL 知识,探讨外部数据分析工具,如 商业智能(BI)工具和 SQL 分析工具,如何利用 Apache Spark 的分布式 SQL 引擎,以快速高效地处理和可视化大量数据。
第十三章:将外部工具与 Spark SQL 集成
商业智能(BI)指的是使组织能够做出明智的、数据驱动的决策的能力。BI 结合了数据处理能力、数据可视化、业务分析和一套最佳实践,帮助组织在战略和战术决策中优化、精炼和简化其业务流程。组织通常依赖专业的软件工具,称为 BI 工具,以满足其 BI 需求。BI 工具将战略与技术结合,收集、分析和解释来自不同来源的数据,并提供有关企业过去和现在状态的业务分析。
BI 工具传统上依赖数据仓库作为数据源和数据处理引擎。然而,随着大数据和实时数据的出现,BI 工具已经扩展到使用数据湖和其他新的数据存储和处理技术作为数据源。在本章中,您将探索如何通过 Spark Thrift Java 数据库连接/开放数据库连接(JDBC/ODBC)服务器,将 Spark SQL 作为分布式结构化查询语言(SQL)引擎,用于 BI 和 SQL 分析工具。将介绍 Spark SQL 与 SQL 分析和 BI 工具的连接要求,以及详细的配置和设置步骤。最后,本章还将介绍从任意 Python 应用程序连接到 Spark SQL 的选项。
本章将涵盖以下主要内容:
-
Apache Spark 作为分布式 SQL 引擎
-
Spark 与 SQL 分析工具的连接
-
Spark 与 BI 工具的连接
-
使用
pyodbc将 Python 应用程序连接到 Spark SQL
本章所获得的技能包括理解 Spark Thrift JDBC/ODBC 服务器、如何通过 JDBC 和 Spark Thrift 服务器将 SQL 编辑器和 BI 工具连接到 Spark SQL,以及使用 Pyodbc 将基于 Python 的业务应用程序与 Spark SQL 引擎连接。
技术要求
本章所需内容如下:
-
在本章中,我们将使用 Databricks 社区版来运行代码(
community.cloud.databricks.com)。注册说明请参见databricks.com/try-databricks。 -
我们将使用一个免费的开源 SQL 编辑器工具SQL Workbench/J,可以从
www.sql-workbench.eu/downloads.html下载。 -
您需要下载一个 JDBC 驱动程序,使 SQL Workbench/J 能够与 Databricks 社区版连接。您可以从
databricks.com/spark/jdbc-drivers-download下载该驱动程序。 -
我们还将使用 Tableau Online 来演示 BI 工具的集成。你可以在
www.tableau.com/products/online/request-trial申请免费的 14 天 Tableau Online 试用。
Apache Spark 作为一个分布式 SQL 引擎
SQL 的一个常见应用是与 BI 和 SQL 分析工具的结合。这些基于 SQL 的工具通过 JDBC 或 ODBC 连接以及内置的传统 RDBMS JDBC/ODBC 连接来连接到 关系型数据库管理系统 (RDBMS)。在前面的章节中,你已经看到,Spark SQL 可以通过笔记本使用,并与 PySpark、Scala、Java 或 R 应用程序混合使用。然而,Apache Spark 也可以作为一个强大且快速的分布式 SQL 引擎,通过 JDBC/ODBC 连接或命令行使用。
注意
JDBC 是一个基于 SQL 的 应用程序编程接口 (API),Java 应用程序通过它连接到关系型数据库管理系统 (RDBMS)。类似地,ODBC 是由 Microsoft 创建的一个 SQL 基础的 API,用于为基于 Windows 的应用程序提供 RDBMS 访问。JDBC/ODBC 驱动程序是一个客户端软件组件,由 RDBMS 厂商自己开发或由第三方开发,可与外部工具一起使用,通过 JDBC/ODBC 标准连接到 RDBMS。
在接下来的章节中,我们将探讨如何利用 Apache Spark 的 JDBC/ODBC 服务功能。
Hive Thrift JDBC/ODBC 服务器介绍
虽然 JDBC/ODBC 驱动程序赋予 BI 或 SQL 分析工具等客户端软件连接数据库服务器的能力,但数据库服务器也需要一些服务器端组件才能利用 JDBC/ODBC 标准。大多数 RDBMS 都内置了这些 JDBC/ODBC 服务功能,Apache Spark 也可以通过 Thrift JDBC/ODBC 服务器启用此服务器端功能。
HiveServer2 是一个服务器端接口,旨在使 Hadoop Hive 客户端能够执行针对 Apache Hive 的 Hive 查询。HiveServer2 已经开发出来,旨在通过 JDBC 和 ODBC 等开放的 API 提供多客户端并发能力。HiveServer2 本身基于 Apache Thrift,这是一种用于在多种编程语言中创建服务的二进制通信协议。Spark Thrift Server 是 Apache Spark 对 HiveServer2 的实现,允许 JDBC/ODBC 客户端在 Apache Spark 上执行 Spark SQL 查询。
Spark Thrift Server 随 Apache Spark 发行版一起捆绑,且大多数 Apache Spark 厂商默认在他们的 Spark 集群上启用此服务。以 Databricks 为例,可以通过 集群 页面访问此服务,如下图所示:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ess-pyspk-scl-da/img/B16736_13_01.jpg)
图 13.1 – Databricks JDBC/ODBC 接口
你可以通过导航到 Databricks Web 界面内的Clusters页面,然后点击高级选项,再点击JDBC/ODBC选项卡,来访问前一屏幕截图中显示的 Databricks JDBC/ODBC 接口。Databricks JDBC/ODBC 接口提供了外部工具连接 Databricks Spark 集群所需的主机名、端口、协议、HTTP 路径和实际的 JDBC URL。在接下来的部分中,我们将探讨如何使用 Spark Thrift Server 功能,使外部基于 SQL 的客户端能够利用 Apache Spark 作为分布式 SQL 引擎。
Spark 与 SQL 分析工具的连接
SQL 分析工具,如其名称所示,是专为快速和简便 SQL 分析而设计的工具。它们允许您连接到一个或多个关系数据库管理系统(RDBMS),浏览各种数据库、模式、表和列。它们甚至帮助您可视化分析表及其结构。它们还具有设计用于快速 SQL 分析的界面,具有多个窗口,让您可以在一个窗口浏览表和列,在另一个窗口中编写 SQL 查询,并在另一个窗口中查看结果。其中一种这样的 SQL 分析工具,称为SQL Workbench/J,如下屏幕截图所示:

图 13.2 – SQL Workbench/J 界面
前面的屏幕截图展示了SQL Workbench/J的界面,它代表了一个典型的 SQL 编辑器界面,左侧窗格有数据库、模式、表和列浏览器。顶部窗格有一个用于编写实际 SQL 查询的文本界面,底部窗格显示已执行 SQL 查询的结果,并有其他选项卡显示任何错误消息等等。顶部还有一个菜单和工具栏,用于建立数据库连接、在各个数据库之间切换、执行 SQL 查询、浏览数据库、保存 SQL 查询、浏览查询历史记录等等。这种类型的 SQL 分析界面非常直观,非常适合快速 SQL 分析,以及构建基于 SQL 的数据处理作业,因为可以轻松浏览数据库、表和列。可以轻松地将表和列名拖放到查询组合器窗口中,快速查看和分析结果。
此外,还有一些更复杂的 SQL 分析工具,可以让你在同一个界面内可视化查询结果。一些开源工具如Redash、Metabase和Apache Superset,以及一些云原生工具如Google Data Studio、Amazon QuickSight等。
提示
Redash 最近被 Databricks 收购,并可在 Databricks 付费版本中使用;截至目前,它在 Databricks 社区版中不可用。
现在,您已经了解了 SQL 分析工具的外观和工作原理,接下来让我们来看一下将 SQL 分析工具(如 SQL Workbench/J)连接到 Databricks Community Edition 所需的步骤。
SQL Workbench/J 是一个免费的、独立于 RDBMS 的 SQL 分析工具,基于 Java,可以与任何您选择的操作系统一起使用。有关下载和运行 SQL Workbench/J 的说明,请参阅此处:www.sql-workbench.eu/downloads.html。
一旦您在本地机器上成功设置并运行 SQL Workbench/J,接下来的步骤将帮助您将其与 Databricks Community Edition 连接:
-
从
databricks.com/spark/jdbc-drivers-download下载 Databricks JDBC 驱动程序,并将其存储在已知位置。 -
启动 SQL Workbench/J 并打开 文件 菜单。然后,点击 连接窗口,以进入以下屏幕:
![图 13.3 – SQL Workbench/J 连接窗口]()
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ess-pyspk-scl-da/img/B16736_13_03.jpg)
图 13.3 – SQL Workbench/J 连接窗口
-
在前面的窗口中,点击 管理驱动程序,以进入以下屏幕:
![图 13.4 – 管理驱动程序屏幕]()
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ess-pyspk-scl-da/img/B16736_13_04.jpg)
图 13.4 – 管理驱动程序屏幕
-
如前所述的 管理驱动程序 窗口截图所示,点击文件夹图标,导航到您存储已下载 Databricks 驱动程序的文件夹并打开它,然后点击 OK 按钮。
-
现在,导航到您的 Databricks
UID和PWD部分,并将其粘贴到 URL 字段中,位于 SQL Workbench/J 连接窗口,如下所示截图所示:![图 13.6 – SQL Workbench/J 连接参数]()
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ess-pyspk-scl-da/img/B16736_13_06.jpg)
图 13.6 – SQL Workbench/J 连接参数
-
在输入来自 Databricks 集群 页面所需的 JDBC 参数后,在 SQL Workbench/J 连接窗口中的 用户名 和 密码 字段中输入您的 Databricks 用户名和密码。然后,点击 测试 按钮以测试与 Databricks 集群的连接。如果所有连接参数已正确提供,您应该会看到一个 连接成功 的消息弹出。
提示
如果您看到任何连接失败或 Host Not Found 类型的错误,请确保 Databricks 集群已启动并正在运行。
通过遵循之前的步骤,您可以成功地将 SQL 分析工具(如 SQL Workbench/J)连接到 Databricks 集群,并远程运行 Spark SQL 查询。也可以连接到运行在其他供应商集群上的其他 Spark 集群——只需确保直接从供应商处获取适当的 HiveServer2 驱动程序。现代 BI 工具也认识到连接大数据技术和数据湖的重要性,在接下来的部分中,我们将探讨如何通过 JDBC 连接将 BI 工具与 Apache Spark 连接。
Spark 连接到 BI 工具
在大数据和人工智能(AI)时代,Hadoop 和 Spark 将数据仓库现代化为分布式仓库,能够处理高达拍字节(PB)的数据。因此,BI 工具也已发展为利用基于 Hadoop 和 Spark 的分析存储作为其数据源,并通过 JDBC/ODBC 连接到这些存储。包括 Tableau、Looker、Sisense、MicroStrategy、Domo 等在内的 BI 工具都支持与 Apache Hive 和 Spark SQL 的连接,并内置了相应的驱动程序。在本节中,我们将探讨如何通过 JDBC 连接将 BI 工具,如 Tableau Online,连接到 Databricks Community Edition。
Tableau Online 是一个完全托管在云中的 BI 平台,允许你进行数据分析、发布报告和仪表盘,并创建交互式可视化,所有操作都可以通过网页浏览器完成。以下步骤描述了将 Tableau Online 与 Databricks Community Edition 连接的过程:
-
如果你已经拥有一个现有的 Tableau Online 账户,请登录。如果没有,你可以在此处请求免费试用:
www.tableau.com/products/online/request-trial。 -
登录后,点击右上角的新建按钮,如以下截图所示:
![图 13.7 – Tableau Online 新工作簿]()
图 13.7 – Tableau Online 新工作簿
-
新创建的工作簿会提示你连接到数据。点击连接器标签,并从可用数据源列表中选择Databricks,如以下截图所示:
![图 13.8 – Tableau Online 数据源]()
图 13.8 – Tableau Online 数据源
-
然后,提供 Databricks 集群的详细信息,如服务器主机名、HTTP 路径、身份验证、用户名和密码,如以下截图所示,并点击登录按钮。这些详细信息可以在 Databricks 集群页面找到:
![图 13.9 – Tableau Online Databricks 连接]()
图 13.9 – Tableau Online Databricks 连接
-
一旦连接成功,你的新工作簿将在数据源标签页中打开,你可以浏览现有的数据库和表格,如以下截图所示:
![图 13.10 – Tableau Online 数据源]()
图 13.10 – Tableau Online 数据源
-
数据源标签页还允许你拖放表格并定义表之间的关系和连接,如以下截图所示:
![图 13.11 – Tableau Online:定义表连接]()
图 13.11 – Tableau Online:定义表连接
-
Tableau Online 数据源还允许你与基础数据源创建两种类型的连接——一种是实时连接,它会在每次请求时查询基础数据源,另一种是创建提取,它从数据源中提取数据并将其存储在 Tableau 中。对于像 Apache Spark 这样的庞大数据源,建议创建实时连接,因为查询的数据量可能远大于平常。
-
还可以通过点击更新现在按钮在数据源标签中浏览示例数据,如下图所示:
![图 13.12 – Tableau Online 数据源预览]()
图 13.12 – Tableau Online 数据源预览
-
一旦数据源连接建立,你可以通过点击
Sheet1或创建新的附加工作表来继续可视化数据并创建报告和仪表板。 -
一旦进入工作表,你可以通过将数据窗格中的列拖放到空白工作表上来开始可视化数据。
-
Tableau 会根据选择的字段自动选择合适的可视化方式。可视化方式也可以通过可视化下拉选择器进行更改,数据过滤器可以通过过滤器框定义。在列字段中可以定义度量的聚合,还可以根据需要将列定义为维度、属性或度量。顶部菜单还提供了排序和透视数据的其他设置,并有其他格式化选项。Tableau Online 还提供了高级分析选项,如定义四分位数、中位数等。通过这种方式,利用 Tableau Online 内置的 Databricks 连接器,数据可以在 Apache Spark 的强大和高效的支持下进行大规模分析,同时还可以享受像 Tableau Online 这样显著 BI 工具的图形用户界面(GUI)的易用性,截图如下所示:

图 13.13 – Tableau Online 工作表与可视化
Tableau Online是许多流行的 BI 工具之一,支持开箱即用的原生 Databricks 连接。现代 BI 工具还提供了 Spark SQL 连接选项,用于连接 Databricks 之外的 Apache Spark 发行版。
连接 Apache Spark 不仅限于 SQL 分析和 BI 工具。由于 JDBC 协议基于 Java,并且是为了被基于 Java 的应用程序使用而设计的,因此任何使用Java 虚拟机(JVM)基础编程语言(如Java或Scala)构建的应用程序也可以使用 JDBC 连接选项连接 Apache Spark。
那么,像 Python 这样流行的编程语言构建的应用程序怎么办?这些类型的 Python 应用程序可以通过Pyodbc连接到 Apache Spark,我们将在接下来的部分中探讨这一点。
将 Python 应用程序连接到 Spark SQL,使用 Pyodbc
Pyodbc是一个开源的 Python 模块,用于通过 ODBC 连接将 Python 应用程序连接到数据源。Pyodbc 可以与任何本地 Python 应用程序一起使用,通过 ODBC 驱动程序连接到 Apache Spark,并访问使用 Apache Spark SQL 定义的数据库和表格。在本节中,我们将探讨如何通过以下步骤使用Pyodbc将运行在本地机器上的 Python 连接到 Databricks 集群:
-
从这里下载并安装 Databricks 提供的 Simba ODBC 驱动程序到本地机器:
databricks.com/spark/odbc-drivers-download。 -
使用
pip在本地机器的 Python 中安装 Pyodbc,如下所示的命令:sudo pip install pyodbc -
使用您选择的文本编辑器创建一个新的 Python 文件,并将以下代码粘贴到其中:
import pyodbc odbc_conn = pyodbc.connect("Driver /Library/simba/spark/lib/libsparkodbc_sbu.dylib;" + "HOST=community.cloud.databricks.com;" + "PORT=443;" + "Schema=default;" + "SparkServerType=3;" + "AuthMech=3;" + "UID=username;" + "PWD=password;" + "ThriftTransport=2;" + "SSL=1;" + "HTTPPath= sql/protocolv1/o/4211598440416462/0727-201300-wily320", autocommit=True) cursor = odbc_conn.cursor() cursor.execute(f"SELECT * FROM retail_features LIMIT 5") for row in cursor.fetchall(): print(row) -
前述代码配置的驱动路径可能会根据您的操作系统有所不同,具体如下:
macOS: /Library/simba/spark/lib/libsparkodbc_sbu.dylib Linux 64-bit: /opt/simba/spark/lib/64/libsparkodbc_sb64.so Linux 32-bit: /opt/simba/spark/lib/32/libsparkodbc_sb32.so -
主机名、端口和 HTTP 路径的值可以从 Databricks 集群的JDBC/ODBC标签页获取。
-
一旦您添加了所有代码并输入了适当的配置值,保存 Python 代码文件并命名为
pyodbc-databricks.py。 -
现在,您可以使用以下命令在 Python 解释器中执行代码:
python pyodbd-databricks.py -
一旦代码成功运行,您在 SQL 查询中指定的表格的前五行将显示在控制台上。
注意
配置 Windows 机器上的 ODBC 驱动程序以供 Pyodbc 使用的说明,请参考 Databricks 公开文档页面:
docs.databricks.com/dev-tools/pyodbc.html#windows。
通过这种方式,使用Pyodbc,您可以将 Apache Spark SQL 集成到任何运行在您本地机器或云端、数据中心等远程机器上的 Python 应用程序中,同时仍能利用 Apache Spark 自带的快速强大的分布式 SQL 引擎。
摘要
在本章中,你已经探索了如何利用 Apache Spark 的 Thrift 服务器启用 JDBC/ODBC 连接,并将 Apache Spark 用作分布式 SQL 引擎。你学习了 HiveServer2 服务如何允许外部工具使用 JDBC/ODBC 标准连接到 Apache Hive,以及 Spark Thrift 服务器如何扩展 HiveServer2,从而在 Apache Spark 集群上实现类似的功能。本章还介绍了连接 SQL 分析工具(如 SQL Workbench/J)所需的步骤,并详细说明了如何将 Tableau Online 等 BI 工具与 Spark 集群连接。最后,还介绍了如何使用 Pyodbc 将任意 Python 应用程序(无论是本地运行在你的机器上,还是远程部署在云端或数据中心)连接到 Spark 集群的步骤。在本书的下一章也是最后一章中,我们将探索 Lakehouse 模式,这一模式可以帮助组织通过一个统一的分布式和持久化存储层,结合数据仓库和数据湖的最佳特性,轻松应对数据分析的三种工作负载——数据工程、数据科学和 SQL 分析。
第十四章:数据湖仓
在本书中,您已经接触到了两种主要的数据分析用例:描述性分析(包括 BI 和 SQL 分析)和高级分析(包括数据科学和机器学习)。您了解到,作为统一数据分析平台的 Apache Spark,可以满足所有这些用例。由于 Apache Spark 是一个计算平台,它与数据存储无关,能够与任何传统存储机制(如数据库和数据仓库)以及现代分布式数据存储系统(如数据湖)协同工作。然而,传统的描述性分析工具,如 BI 工具,通常是围绕数据仓库设计的,且期望数据以特定方式呈现。现代的高级分析和数据科学工具则倾向于处理可以轻松访问的数据湖中的大量数据。将冗余数据存储在单独的存储系统中以满足这些独立用例,不仅不实际,而且成本效益较低。
本章将介绍一种新的范式——数据湖仓,它试图克服数据仓库和数据湖的局限性,通过结合两者的最佳元素来弥合这两者之间的差距。
本章将涵盖以下主题:
-
从 BI 到 AI 的转变
-
数据湖仓范式
-
数据湖仓的优势
本章结束时,您将了解现有数据存储架构(如数据仓库和数据湖)的关键挑战,以及数据湖仓如何帮助弥合这一差距。您将理解数据湖仓的核心要求和参考架构,并探讨一些现有的商业化数据湖仓及其局限性。最后,您将了解数据湖仓的参考架构,它利用 Apache Spark 和 Delta Lake,并学习它们的一些优势。
从 BI 到 AI 的转变
商业智能(BI)仍然是数据分析的核心。在 BI 中,组织从各种数据源收集原始事务数据,并通过 ETL 将其转换成一种有利于生成运营报告和企业仪表板的格式,这些报告和仪表板展示了过去一段时间内整个企业的运营情况。这也帮助企业高层做出有关未来战略的明智决策。然而,如果生成的事务数据量增加了几个数量级,那么很难(如果不是不可能的话)从中提取出相关且及时的洞察,帮助企业做出决策。此外,仅仅依赖结构化的事务数据进行业务决策也不再足够。如果你希望了解企业的当前状态、市场状况、客户和社会趋势以保持企业的相关性和盈利性,那么就需要考虑新的非结构化数据类型,例如以自然语言形式的客户反馈、客户服务中心的语音记录,以及产品和客户评论的视频和图像。因此,你必须从传统的 BI 和决策支持系统转变,并且用预测分析来补充运营报告和高层仪表板,甚至完全用人工智能(AI)替代BI。传统的 BI 和数据仓库工具在应对 AI 用例时完全失效。这个问题将在接下来的几节中详细探讨。
数据仓库的挑战
传统上,数据仓库一直是 BI 工具的主要数据来源。数据仓库期望数据按照预定义的架构进行转化和存储,这使得 BI 工具可以轻松地查询数据。BI 工具已经发展成能够利用数据仓库的优势,这使得处理过程变得非常高效且具有较好的性能。下图表示了BI 和 DW系统的典型参考架构:

图 14.1 – 商业智能(BI)和数据仓库(DW)架构
如前面的图所示,BI 和 DW系统从事务系统中提取原始事务数据,根据数据仓库定义的架构转化数据,然后将数据加载到数据仓库中。这个整个过程通常会定期执行,通常是每晚一次。现代的 ETL 和数据仓库系统已经发展到支持更频繁的数据加载,比如每小时一次。然而,这种方法存在一些关键的缺点,限制了这些系统真正支持现代 AI 用例。具体来说,以下几点:
-
传统数据仓库的计算和存储通常位于本地的单台服务器上。它们的容量通常为峰值负载进行规划,因为这些数据库的存储和计算能力与其运行的机器或服务器紧密相关。这使得它们的扩展性差,甚至无法扩展。这意味着传统的本地数据仓库的数据容量是固定的,无法处理大数据带来的快速数据涌入。这使得它们的架构显得僵化,无法适应未来的发展。
-
数据仓库的设计初衷是仅在特定时间间隔加载数据,几乎所有传统的数据仓库都没有设计成能够处理实时数据摄取。这意味着,基于这些数据仓库的分析师和企业高管通常只能处理过时的数据,从而延迟了决策过程。
-
最后,数据仓库基于关系型数据库,而关系型数据库无法处理视频、音频或自然语言等非结构化数据。这使得数据仓库无法扩展以支持数据科学、机器学习或人工智能等高级分析应用场景。
为了克服数据仓库上述的缺点,尤其是无法分离计算和存储,因此无法按需扩展,且处理实时和非结构化数据的能力较弱,企业转向了数据湖架构。这些架构最早由Hadoop生态系统引入。我们将在以下章节中详细探讨这一点。
数据湖的挑战
数据湖是低成本的存储系统,具有类似文件系统的 API,可以容纳任何形式的数据,无论是结构化还是非结构化数据,例如Hadoop 分布式文件系统(HDFS)。企业采用数据湖范式来解决计算与存储分离和可扩展性的问题。随着大数据和 Hadoop 的出现,第一个 HDFS 被采用,数据湖开始存储在像 Apache Parquet 和 ORC 这样的通用开放文件格式中。随着云计算的到来,像 Amazon S3、Microsoft Azure ADLS 和 Google Cloud Storage 这样的对象存储被采纳为数据湖。这些存储非常便宜,并且能够自动归档数据。
虽然数据湖具有高度的可扩展性,成本较低,并且能够支持多种数据和文件类型,但它们不符合 BI 工具严格的“写时模式”要求。因此,基于云的数据湖架构被补充了额外的一层云数据仓库,以专门支持 BI 应用场景。下图展示了云中典型决策支持系统的架构:

图 14.2 – 云中的数据湖架构
在上图中,我们可以看到一个典型的云端数据湖架构。首先,原始数据以原样、无任何转换的方式通过流式处理或其他时尚方式输入到数据湖中。然后,原始数据会被 ETL 处理,并重新放回数据湖,供下游的使用场景,如数据科学、机器学习和人工智能使用。对于 BI 和运营报告所需的部分数据,会进行清洗、整合并加载到基于云的数据仓库中,以供 BI 工具使用。这种架构解决了传统数据仓库的所有问题。数据湖具有无限的可扩展性,且完全独立于任何计算资源。这是因为计算资源仅在进行数据摄取、转换或消费时才需要。数据湖还可以处理各种数据类型,从结构化和半结构化数据到非结构化数据。
然而,数据湖架构确实存在一些关键挑战:
-
数据湖不具备内建的事务控制或数据质量检查功能,因此数据工程师需要在数据处理管道中编写额外的代码来执行事务控制。这有助于确保数据一致性,并保证数据具有适合下游使用的质量。
-
数据湖可以存储如 Parquet 和 ORC 这样的结构化文件数据;然而,传统的 BI 工具可能无法读取这些文件中的数据,因此必须引入另一个数据仓库层来满足这些用例。这增加了额外的复杂性,因为需要存在两条独立的数据处理管道——一条用于将数据 ELT 到用于高级分析的场景,另一条用于将数据 ETL 到数据仓库。这也增加了运营成本,因为数据存储几乎翻倍,并且需要管理两个独立的数据存储系统。
-
虽然原始数据可以流入数据仓库,但在原始数据经过 ETL(提取、转换、加载)处理并加载到数据仓库之前,它可能无法直接供下游的业务分析系统使用,从而导致业务用户和数据分析师看到的是陈旧的数据,延缓了他们的决策过程。
数据湖仓承诺克服传统数据仓库和现代数据湖所面临的挑战,帮助将两者的最佳特性带给最终用户。我们将在接下来的部分详细探讨数据湖仓模式。
数据湖仓模式
数据湖仓范式结合了数据仓库和数据湖的最佳方面。数据湖仓基于开放标准,实现了数据仓库的数据结构和数据管理特性。这种范式还利用数据湖的成本效益和可扩展的数据存储。通过结合数据仓库和数据湖的优点,数据湖仓同时满足数据分析师和数据科学家的需求,无需维护多个系统或冗余数据副本。数据湖仓帮助加速数据项目,团队可以在一个地方访问数据,无需访问多个系统。数据湖仓还提供访问最新数据的机会,这些数据完整且及时更新,可以用于商业智能、数据科学、机器学习和人工智能项目。虽然数据湖仓基于云端对象存储等数据湖,但它们需要遵守特定的要求,如下一节所述。
数据湖仓的关键要求
数据湖仓需要满足一些关键要求,以便提供数据仓库的结构和数据管理能力,以及数据湖的可伸缩性和处理非结构化数据的能力。以下是必须考虑的一些关键要求:
-
数据湖仓需要支持ACID事务,以确保 SQL 查询的数据读取。在数据湖仓中,多个数据管道可以同时写入和读取相同的数据集,事务支持保证数据读取者和写入者永远不会看到不一致的数据视图。
-
数据湖仓应该能够将计算与存储解耦,确保它们可以独立扩展。这不仅使数据湖仓更经济实惠,还有助于支持使用多个集群和非常大的数据集的并发用户。
-
数据湖仓需要基于开放标准。这允许各种工具、API 和库直接访问数据湖仓,并防止任何昂贵的供应商或数据锁定。
-
要支持结构化数据和数据模型,如数据仓库世界中的Star/Snowflake模式,数据湖仓必须支持模式强制执行和演进。数据湖仓应支持管理数据完整性、治理和审计的机制。
-
数据湖仓需要支持多种数据类型,包括结构化和非结构化数据类型,因为数据湖仓可用于存储、分析和处理各种数据,从文本、交易、物联网数据、自然语言到音频转录和视频文件。
-
需要支持传统的结构化数据分析,如商业智能(BI)和 SQL 分析,以及包括数据科学、机器学习和人工智能(AI)在内的高级分析工作负载。数据湖屋应能够通过支持JDBC/ODBC标准连接,直接支持 BI 和数据发现。
-
数据湖屋应能够支持端到端的流处理,从能够将实时数据直接摄取到数据湖屋,到数据的实时 ELT 和实时商业分析。数据湖屋还应支持实时机器学习和低延迟的机器学习推理。
现在您已经了解了数据湖屋的关键需求,让我们试着理解它的核心组件和参考架构。
数据湖屋架构
数据湖屋的功能,如可扩展性、处理非结构化数据的能力以及能够将存储和计算分离,得益于用于持久化存储的底层数据湖。然而,为了提供类似数据仓库的功能,仍然需要一些核心组件,比如ACID事务、索引、数据治理和审计以及其他数据级优化。可扩展的元数据层是其中一个核心组件。元数据层位于开放文件格式(如 Apache Parquet)之上,帮助跟踪文件和表格的版本以及像ACID事务这样的功能。元数据层还支持流数据摄取、架构强制和演化以及数据验证等功能。
基于这些核心组件,已经产生了一个参考数据湖屋架构,如下图所示:

图 14.3 – 数据湖屋架构
数据湖屋是建立在廉价的云存储基础上的,这些存储提供的吞吐量数据访问并不特别高。为了能够满足低延迟和高度并发的使用场景,数据湖屋需要通过数据跳跃索引、文件夹和文件级修剪等功能,提供快速的数据访问能力,并能够收集和存储表格和文件统计信息,帮助查询执行引擎推导出最优的查询执行计划。数据湖屋应具备高速数据缓存层,以加快对频繁访问数据的访问。
现有数据湖屋架构示例
一些市面上商业化的云服务产品在一定程度上(如果不是完全)满足了数据湖屋的需求。部分产品在本节中有列出。
Amazon Athena 是 AWS 提供的一项交互式查询服务,作为其托管服务的一部分。AWS 基于开源的可扩展查询引擎 Presto,允许你查询存储在 S3 桶中的数据。它支持由 Hive 支持的元数据层,并允许你创建表的模式定义。然而,像 Athena 这样的查询引擎无法解决数据湖和数据仓库的所有问题。它们仍然缺乏基本的数据管理功能,如 ACID 事务,以及性能提升功能,如索引和缓存。
基于云的商业数据仓库,如 Snowflake,紧随其后,因为它提供了传统数据仓库的所有功能,以及支持人工智能、机器学习和数据科学的更多高级功能。它将数据仓库、特定主题的数据集市和数据湖结合成一个单一的真实版本,能够支持多种工作负载,包括传统分析和高级分析。然而,Snowflake 不提供数据管理功能;数据存储在其存储系统中,并且对于存储在数据湖中的数据,它没有提供相同的功能。对于大规模机器学习项目,Snowflake 可能也不适用,因为数据需要被流式传输到数据湖中。
Google BigQuery,这款按 PB 级扩展的实时数据仓库解决方案,提供了几乎所有数据湖仓所需的功能。它支持同时的批处理和流处理工作负载,以及通过其 BigQuery ML 提供的 SQL 和类查询语言进行的机器学习工作负载,甚至支持 AutoML。然而,BigQuery 仍然要求数据以其内部格式存储,并且对于存储在数据湖中的外部数据,它没有提供所有增强查询性能的功能。在接下来的部分中,我们将探讨如何利用 Apache Spark、Delta Lake 和基于云的数据湖作为数据湖仓。
基于 Apache Spark 的数据湖仓架构
当 Apache Spark 与 Delta Lake 和基于云的数据湖结合时,几乎可以满足数据湖仓的所有需求。在本节中,我们将探讨这一点,并展示一个基于 Apache Spark 的参考架构。让我们开始吧:
-
Delta Lake 通过其事务日志,完全支持 ACID 事务,类似于传统数据仓库,以确保写入 Delta Lake 的数据是一致的,并且任何下游读取者永远不会读取脏数据。这也允许多个读取操作发生,并从多个 Spark 集群将数据写入同一数据集,而不会影响数据集的完整性。
-
Apache Spark 一直以来都不依赖于特定的数据存储,可以从各种数据源中读取数据,包括将数据读取到内存中以便处理,然后将结果写入持久存储。因此,当 Apache Spark 与分布式持久存储系统(如基于云的数据湖)结合时,完全支持存储和计算解耦。
-
Apache Spark 支持多种方式访问存储在 Delta Lake 中的数据,包括使用 Spark 的 Java、Scala、PySpark 和 SparkR API 进行直接访问。Apache Spark 还支持通过 Spark ThriftServer 进行 JDBC/ODBC 连接,便于与 BI 工具连接。Delta Lake 还支持用于 Apache Spark 外部连接的纯 Java API。
-
Delta Lake 通过其事务日志支持内置的元数据层。事务日志为 Delta Lake 提供了版本控制、表更改的审计跟踪和时间旅行功能,能够在不同版本的表之间进行切换,并能在特定时间点恢复表的任何快照。
-
Apache Spark 和 Delta Lake 都支持所有类型的结构化和非结构化数据。此外,Delta Lake 支持模式强制执行,并支持模式演化。
-
Apache Spark 支持通过结构化流处理进行实时分析,而 Delta Lake 完全支持批处理和流处理的并发执行。
因此,Apache Spark 与 Delta Lake 配合使用,同时依托云数据湖,支持内存数据缓存和性能提升特性,如数据跳跃索引和收集表与文件统计信息。这种组合使得数据湖仓成为一个很好的候选方案。
注意
Spark 结构化流处理仅支持基于微批次的流处理,不支持事件处理。此外,Apache Spark 和 Delta Lake 共同使 SQL 查询性能非常快。然而,Spark 固有的 JVM 调度延迟仍会引入相当大的查询处理时间差,这使其不适用于超低延迟查询。此外,Apache Spark 可以通过多个集群和调整某些 Spark 集群参数来支持并发用户,尽管这种复杂性需要用户进行管理。这些原因使得即使与 Delta Lake 一起使用,Apache Spark 也不适合用于非常高并发、超低延迟的应用场景。Databricks 开发了一个名为 Photon 的下一代查询处理引擎,可以克服开源 Spark 的这些问题。然而,Databricks 在撰写本文时尚未将 Photon 发布到开源 Apache Spark 中。
现在你已经看到 Apache Spark 和 Delta Lake 如何作为数据湖仓一起工作,接下来让我们看看这个参考架构的样子:

图 14.4 – 由 Apache Spark 和 Delta Lake 提供支持的数据湖仓
上述图示展示了使用 Apache Spark 和 Delta Lake 的数据湖屋架构。在这里,数据摄取可以通过结构化流处理(Structured Streaming)以实时或批量方式完成,也可以通过常规的 Spark 批处理作业,或使用一些第三方数据集成工具完成。来自各种来源的原始数据,如事务性数据库、物联网数据、点击流数据、服务器日志等,直接流入数据湖屋并以 Delta 文件格式存储。接着,这些原始数据会使用 Apache Spark DataFrame API 或 Spark SQL 进行转换。同样,这可以通过结构化流处理以批量或流式方式完成。表格元数据、索引和统计信息都由 Delta Lake 事务日志处理,Hive 可以作为元存储。商业智能(BI)和 SQL 分析工具可以通过 JDBC/ODBC 连接直接访问数据湖屋中的数据,高级分析工具和库也可以通过 Spark 集群使用 Spark SQL 或 DataFrame API 直接与湖中的数据交互。通过这种方式,Apache Spark 和 Delta Lake 在云中与对象存储作为数据湖结合,可以用于实现数据湖屋范式。现在我们已经实现了一个参考数据湖屋架构,接下来让我们了解其一些优势。
数据湖屋的优势
数据湖屋解决了使用数据仓库和数据湖时的多数挑战。使用数据湖屋的一些优势包括:减少由云中数据湖和数据仓库等双层系统引起的数据冗余。这意味着减少存储成本,并简化维护和数据治理,因为任何数据治理功能,如访问控制和审计日志,都可以在一个地方实现。这消除了在多个工具上管理数据治理的操作开销。
你应该将所有数据存储在一个单一的存储系统中,这样可以简化数据处理和 ETL 架构,这也意味着更容易维护和管理数据管道。数据工程师无需为不同的系统维护独立的代码库,这极大地减少了数据管道中的错误。此外,当数据问题被识别时,也更容易追踪数据的血缘关系并进行修复。
数据湖屋为数据分析师、企业高管和数据科学家提供了直接访问湖屋中最新数据的权限。这减少了他们对 IT 团队在数据访问方面的依赖,帮助他们做出及时且明智的决策。数据湖屋最终降低了总体拥有成本,因为它们消除了数据冗余、减少了操作开销,并且提供了具有高性能的数据处理和存储系统,相比于某些商业化的专业数据仓库,其成本大大降低。
尽管数据湖屋提供了诸多优势,但该技术仍处于初期阶段,因此可能会落后于一些已经有数十年研发背景的专用产品。随着技术的成熟,数据湖屋将变得更加高效,并能与更多常见的工作流和工具连接,同时保持简单易用且具有成本效益。
摘要
在本章中,您了解了数据仓库和数据湖在设计和实施处理大规模数据的系统时所面临的挑战。我们还探讨了企业从高级分析转向简单描述性分析的需求,以及现有系统如何无法同时解决这两者的问题。接着,介绍了数据湖屋的概念,它解决了数据仓库和数据湖的挑战,并通过结合两者的最佳元素弥合了这两种系统的差距。随后,展示了数据湖屋的参考架构,并介绍了几种现有商业化的大规模数据处理系统中可用的数据湖屋候选方案及其缺点。接下来,展示了基于 Apache Spark 的数据湖屋架构,它利用了 Delta Lake 和基于云的数据湖。最后,介绍了数据湖屋的一些优势,同时也提到了一些不足之处。

订阅我们的在线数字图书馆,即可全面访问超过 7,000 本书籍和视频,此外,还能使用业内领先的工具帮助你规划个人发展并提升职业生涯。如需更多信息,请访问我们的网站。
第十五章:为什么要订阅?
-
用来自 4,000 多位行业专家的实用电子书和视频,节省学习时间,更多时间用于编程
-
使用专为你定制的技能计划来提升你的学习效果
-
每月获得一本免费的电子书或视频
-
完全可搜索,方便快速访问重要信息
-
复制、粘贴、打印并收藏内容
你知道 Packt 提供了每本已出版书籍的电子书版本吗?PDF 和 ePub 文件可供下载。你可以在 packt.com 升级为电子书版本,作为纸质书客户,你还可以享受电子书的折扣优惠。如需更多详情,请通过 customercare@packtpub.com 与我们联系。
在 www.packt.com,你还可以阅读一系列免费的技术文章,订阅各种免费的电子邮件通讯,并获得 Packt 图书和电子书的独家折扣和优惠。
你可能会喜欢的其他书籍
如果你喜欢本书,你可能会对 Packt 出版的其他书籍感兴趣:
使用 Plotly 和 Dash 创建交互式仪表板和数据应用
Elias Dabbas
ISBN: 9781800568914
-
了解如何运行一个完全互动且易于使用的应用程序
-
将图表转换为各种格式,包括图片和 HTML 文件
-
使用 Plotly Express 和图形语法轻松将数据映射到各种视觉属性
-
创建不同类型的图表,例如条形图、散点图、直方图、地图等
-
通过创建动态页面,基于 URL 生成内容,来扩展你的应用
-
实现新的回调函数来管理基于 URL 的图表及其反向操作
《动手实战 Pandas 数据分析(第二版)》
Stefanie Molin
ISBN: 9781800563452
-
了解数据分析师和科学家如何收集和分析数据
-
使用 Python 执行数据分析和数据清洗
-
从多个来源合并、分组和聚合数据
-
使用 pandas、matplotlib 和 seaborn 创建数据可视化
-
应用机器学习算法识别模式并做出预测
-
使用 Python 数据科学库分析真实世界的数据集
-
使用 pandas 解决常见的数据表示和分析问题
-
构建 Python 脚本、模块和包,以便复用分析代码
Packt 正在寻找像你一样的作者
如果你有兴趣成为 Packt 的作者,请访问authors.packtpub.com并立即申请。我们与成千上万的开发者和技术专业人士合作,帮助他们与全球技术社区分享他们的见解。你可以进行一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。
分享你的想法
现在你已经完成了Essential PySpark for Scalable Data Analytics,我们很想听听你的想法!如果你是从 Amazon 购买的这本书,请访问packt.link/r/1-800-56887-8并分享你的反馈,或者在你购买书籍的网站上留下评论。
你的评论对我们和技术社区都非常重要,将帮助我们确保提供高质量的内容。












浙公网安备 33010602011771号