Spark 数据科学(一)

原文:annas-archive.org/md5/8cd899a961e3fea998473427a1ba1c82

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在这个智能时代,数据分析是保持和促进商业增长的关键。每个企业都在努力最大限度地利用其数据,采用各种数据科学工具和技术,在分析成熟度曲线上不断前进。数据科学需求的突然增加是数据科学家稀缺的明显原因。要满足市场对既精通统计学、机器学习、数学建模又具备编程能力的独角兽数据科学家的需求,实属不易。

独角兽数据科学家的可用性将随着市场需求的增加而减少,并且这种情况将持续下去。因此,迫切需要一种解决方案,不仅能够让独角兽数据科学家做得更多,还能创造出 Gartner 所称的“公民数据科学家”。公民数据科学家不是其他人,而是那些主要职能不在统计或分析领域,但足够热衷于学习数据科学的开发人员、分析师、商业智能专业人士或其他技术人员。他们正成为推动整个组织和行业实现数据分析民主化的关键推动力。

不断涌现的各种工具和技术旨在促进大数据分析的规模化。此书旨在培养能够利用 Apache Spark 分布式计算平台进行数据分析的公民数据科学家。

本书是一本学习统计分析和机器学习以构建可扩展数据产品的实用指南。它帮助掌握数据科学的核心概念,并且通过 Apache Spark 帮助你快速启动任何实际数据分析项目。全书的各章都有充足的示例,读者可以在家庭计算机上执行这些示例,轻松跟随并吸收概念。每章都力求自成一体,因此读者可以从任何一章开始,相关章节提供详细内容的指引。尽管各章从基础知识入手,供初学者学习和理解,但同时也足够全面,适合资深架构师阅读。

本书内容涵盖

第一章,大数据与数据科学 – 简介,本章简要讨论了大数据分析中面临的各种挑战,以及 Apache Spark 如何在单一平台上解决这些问题。还介绍了数据分析是如何发展成现在的样子,并简要阐述了 Spark 技术栈的基本概念。

第二章,Spark 编程模型,本章讲述了 Apache Spark 的设计考虑因素及其支持的编程语言,还详细解释了 Spark 的核心组件,并详细介绍了 RDD API,这是 Spark 的基本构建块。

第三章,数据框架简介,本章介绍了数据框架,它是数据科学家们工作中最方便、最有用的组件。它解释了 Spark SQL 和赋能数据框架的 Catalyst 优化器。此外,本章还通过代码示例展示了各种数据框架操作。

第四章,统一数据访问,本章讨论了我们从不同来源获取数据、整合并以统一方式处理的各种方法。它涵盖了实时数据收集的流式处理方面,并讲解了这些 API 的底层原理。

第五章,Spark 上的数据分析,本章讨论了完整的数据分析生命周期。通过大量代码示例,解释了如何从不同来源获取数据,使用数据清理和转化技术准备数据,并进行描述性统计和推论统计,以从数据中挖掘隐藏的洞见。

第六章,机器学习,本章解释了各种机器学习算法,如何在 MLlib 库中实现这些算法,以及如何使用管道 API 进行流畅的执行。本章涵盖了所有算法的基本原理,因此可以作为一个一站式参考。

第七章,使用 SparkR 扩展 Spark,本章主要面向那些希望利用 Spark 进行数据分析的 R 语言程序员。它解释了如何使用 SparkR 编程,以及如何使用 R 库中的机器学习算法。

第八章,分析非结构化数据,本章仅讨论非结构化数据分析。它解释了如何获取非结构化数据、处理这些数据并进行机器学习分析。它还介绍了一些在“机器学习”章节中没有涉及的降维技术。

第九章,可视化大数据,本章介绍了在 Spark 上支持的各种可视化技术。它解释了数据工程师、数据科学家和业务用户的不同可视化需求,并且还建议了合适的工具和技术。它还讨论了如何利用 IPython/Jupyter 笔记本和 Apache 项目 Zeppelin 进行数据可视化。

第十章,将所有内容整合起来,到目前为止,书中已经在不同的章节分别讨论了大部分的数据分析组件。本章旨在将典型数据科学项目的各个步骤串联起来,并展示一个逐步实施完整分析项目的过程。

第十一章,构建数据科学应用程序,到目前为止,本书主要讨论了数据科学组件以及完整的执行示例。本章提供了关于如何构建可部署到生产环境中的数据产品的概述。它还介绍了 Apache Spark 项目的当前开发状态以及未来的计划。

本书所需内容

在执行书中提到的代码之前,您的系统必须安装以下软件。不过,并非所有软件组件都适用于所有章节:

  • Ubuntu 14.4 或 Windows 7 及以上版本

  • Apache Spark 2.0.0

  • Scala: 2.10.4

  • Python 2.7.6

  • R 3.3.0

  • Java 1.7.0

  • Zeppelin 0.6.1

  • Jupyter 4.2.0

  • IPython 内核 5.1

本书适用对象

本书适合任何想要利用 Apache Spark 进行数据科学和机器学习的人。如果你是一个技术专家,想扩展自己的知识以在 Spark 中执行数据科学操作,或者是一个数据科学家,想了解 Spark 中算法的实现,或是一个经验较少的新手,想学习大数据分析,本书适合你!

约定

本书中有多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号如下所示:“当程序在 Spark shell 上运行时,它被称为驱动程序,其中包含用户的 main 方法。”

代码块的格式如下:

Scala> sc.parallelize(List(2, 3, 4)).count()
res0: Long = 3
Scala> sc.parallelize(List(2, 3, 4)).collect()
res1: Array[Int] = Array(2, 3, 4)
Scala> sc.parallelize(List(2, 3, 4)).first()
res2: Int = 2
Scala> sc.parallelize(List(2, 3, 4)).take(2)
res3: Array[Int] = Array(2, 3)

新术语重要词汇 用粗体显示。你在屏幕上看到的词汇,例如在菜单或对话框中,文本中会像这样显示:“它还允许用户通过 数据源 API 从未开箱支持的数据源(例如 CSV、Avro HBase、Cassandra 等)获取数据。”

注释

警告或重要说明将显示在像这样的框中。

小贴士

小窍门和技巧以这种方式呈现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对本书的看法——你喜欢什么,或者不喜欢什么。读者反馈对我们非常重要,它帮助我们开发出你真正能从中受益的书籍。若要向我们提供一般反馈,请通过电子邮件发送至 feedback@packtpub.com,并在邮件主题中提及书名。如果你在某个领域具有专业知识,并有兴趣写书或为书籍贡献内容,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些工具帮助你从购买中获得最大收益。

下载示例代码

您可以从您的帐户中下载本书的示例代码文件,网址为www.packtpub.com。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册以便将文件通过电子邮件直接发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持标签上。

  3. 点击代码下载与勘误表

  4. 搜索框中输入书名。

  5. 选择您要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的渠道。

  7. 点击代码下载

文件下载后,请确保使用最新版本的工具解压或提取文件夹:

  • Windows 版的 WinRAR / 7-Zip

  • Mac 版的 Zipeg / iZip / UnRarX

  • Linux 版的 7-Zip / PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Spark-for-Data-Science。我们还有其他来自我们丰富书籍和视频目录的代码包,您可以在github.com/PacktPublishing/查看它们!

下载本书的彩色图像

我们还为您提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。这些彩色图像将帮助您更好地理解输出结果的变化。您可以从www.packtpub.com/sites/default/files/downloads/SparkforDataScience_ColorImages.pdf下载该文件。

勘误表

尽管我们已尽最大努力确保内容的准确性,但错误总会发生。如果您在我们的书中发现了错误——可能是文本或代码中的错误——我们将非常感激您能向我们报告。这样,您可以帮助其他读者避免困扰,并帮助我们改进后续版本的内容。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata报告,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误被验证,您的提交将被接受,勘误将上传到我们的网站或添加到该书标题下的现有勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需的信息将出现在勘误表部分。

盗版

互联网盗版版权材料是一个涉及所有媒体的持续性问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的任何非法复制,请立即向我们提供网址或网站名称,以便我们采取措施。

请通过电子邮件联系 copyright@packtpub.com,并提供涉嫌盗版材料的链接。

感谢您帮助我们保护作者的权益,以及我们向您提供有价值内容的能力。

问题

如果您对本书的任何内容有问题,可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。

第一章 大数据与数据科学——简介

大数据绝对是一个大问题! 它承诺通过在巨大的数据孤岛中提取隐藏的洞察力,并开辟新的商业发展途径,带来丰富的机会。通过先进的分析技术利用大数据,已经成为组织创造和保持竞争优势的一种理所当然的选择。

本章介绍了大数据的概念、大数据分析面临的各种挑战,以及Apache Spark如何作为事实标准来应对计算挑战,并作为一个数据科学平台。

本章涵盖的主题如下:

  • 大数据概述——到底有什么大惊小怪的?

  • 大数据分析的挑战——为什么它如此困难?

  • 大数据分析的演变——数据分析趋势

  • Spark 数据分析——解决大数据挑战的方案

  • Spark 堆栈——构成完整大数据解决方案的所有组件

大数据概述

关于大数据是什么,已经有很多讨论和书面材料,但实际上并没有一个明确的标准来清晰定义它。它在某种程度上是一个相对的术语。无论数据是大还是小,只有在能够正确分析数据的情况下,才能有效利用它。为了从数据中提取一些有意义的内容,需要正确的分析技术,而选择合适的工具和技术在数据分析中至关重要。然而,当数据本身成为问题的一部分,且需要在进行数据分析之前解决计算挑战时,这就成了一个大数据问题。

在万维网(也称为 Web 2.0)发生了一场革命,改变了人们使用互联网的方式。静态网页变成了互动网站,开始收集越来越多的数据。云计算、社交媒体和移动计算的技术进步引发了数据的爆炸。每个数字设备都开始生成数据,许多其他来源也推动了数据洪流。来自每个角落的数据流以惊人的速度生成各种海量数据!大数据以这种方式的形成是一种自然现象,因为这正是万维网的演变方式,具体细节并没有刻意推动。这是关于过去的!如果考虑现在正在发生并将在未来发生的变化,数据生成的体量和速度超出了任何人的预期。我之所以做出这样的陈述,是因为每个设备现在都变得更加智能,这要归功于物联网IoT)。

信息技术的趋势是,技术进步也促进了数据的爆炸。随着更便宜的在线存储池集群的出现以及基本硬件的低价可用性,数据存储经历了范式转变。将来自不同来源的数据以原始形式存储在单一数据湖中,迅速取代了精心设计的数据集市和数据仓库。使用模式也从严格的模式驱动的基于 RDBMS 的方法转向了无模式、持续可用的NoSQL数据存储驱动的解决方案。因此,无论是结构化数据、半结构化数据,还是非结构化数据,其创建速度都前所未有地加快。

组织非常确信,利用大数据不仅可以回答特定的业务问题,还能带来覆盖尚未探索的业务可能性并解决相关的不确定性。因此,除了自然的数据涌入,组织开始制定战略,以生成越来越多的数据来维持其竞争优势,并为未来做好准备。在这里,一个例子有助于更好地理解这一点。假设在一个制造工厂的机器上安装了传感器,传感器不断发送数据,从而实时了解机器部件的状态,而公司能够预测机器何时会发生故障。这使得公司能够防止故障或损坏,避免计划外停机,节省大量资金。

大数据分析的挑战

在大数据分析中,通常有两类主要挑战。第一个挑战是需要一个庞大的计算平台,一旦平台建立,第二个挑战就是如何在大规模上分析并理解海量数据。

计算挑战

随着数据的增加,大数据的存储需求也不断增长。数据管理变得愈加繁琐。由于磁盘存储的寻址时间,访问数据时的延迟成为了主要瓶颈,尽管处理器的处理速度和内存的频率已经达到了标准。

从各种业务应用程序和数据孤岛中获取结构化和非结构化数据,将其整合并处理以发现有用的业务洞察是一项挑战。只有少数应用程序能够解决任何一个领域或仅几个多样化业务需求的领域。然而,将这些应用程序整合以统一方式解决大多数业务需求,只会增加复杂性。

为了应对这些挑战,人们转向了分布式计算框架和分布式文件系统,例如 Hadoop 和Hadoop 分布式文件系统HDFS)。这可以消除磁盘 I/O 带来的延迟,因为数据可以在机器集群中并行读取。

分布式计算技术在此之前已经存在了几十年,但直到行业认识到大数据的重要性后,它们才逐渐变得更加突出。因此,像 Hadoop、HDFS 和 Amazon S3 这样的技术平台成为了行业标准。在 Hadoop 之上,开发了许多其他解决方案,如 Pig、Hive、Sqoop 等,以应对不同的行业需求,如存储、提取、转换和加载ETL)和数据集成,旨在使 Hadoop 成为一个统一的平台。

分析挑战

分析数据以发现隐藏的洞察力一直以来都是一项挑战,尤其是在处理庞大数据集时所涉及的额外复杂性。传统的 BI 和 OLAP 解决方案无法解决由于大数据带来的大多数挑战。例如,如果数据集有多个维度,比如 100 个,就很难将这些变量彼此进行比较以得出结论,因为这样会有大约 100C2 种组合。此类情况需要使用统计学技术,如相关性等,来发现隐藏的模式。

尽管存在许多统计学解决方案来解决这些问题,但数据科学家或分析专业人员要切分数据并挖掘有价值的洞察力变得异常困难,除非他们将整个数据集加载到内存中的DataFrame中。主要的障碍是,大多数通用的统计分析和机器学习算法都是单线程的,并且是在数据集通常不那么庞大、可以适应单台计算机的 RAM 时编写的。这些用 R 或 Python 编写的算法,在分布式计算环境中部署时已经不再非常有用,因为存在内存计算的限制。

为了应对这一挑战,统计学家和计算机科学家不得不共同合作,重写大多数能够在分布式计算环境中良好运行的算法。因此,开发了一个名为Mahout的机器学习算法库,用于在 Hadoop 上进行并行处理。它包含了行业中最常用的多数算法。类似的举措也在其他分布式计算框架中进行。

大数据分析的发展

前一节概述了如何应对大数据需求的计算和数据分析挑战。这得以实现,是因为若干相关趋势的融合,如低成本的商品硬件、大数据的可获取性以及改进的数据分析技术。Hadoop 成为了许多大型分布式数据处理基础设施的基石。

然而,人们很快意识到 Hadoop 的局限性。Hadoop 解决方案仅适用于特定类型的大数据需求,如 ETL;因此,它只因应这些需求而获得了广泛的应用。

有些情况下,数据工程师或分析师需要对数据集执行临时查询以进行交互式数据分析。每次他们在 Hadoop 上运行查询时,数据都会从磁盘(HDFS 读取)中读取并加载到内存中——这是一项代价高昂的操作。实际上,作业运行的速度受限于网络和磁盘集群的 I/O 传输速度,而不是 CPU 和 RAM 的速度。

以下是该场景的图示表示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_01_001.jpg

Hadoop 的 MapReduce 模型无法很好适应的另一个场景是迭代性机器学习算法。Hadoop MapReduce 的性能不佳,迭代计算存在巨大延迟。由于 MapReduce 有一个受限的编程模型,并且不允许 Map 和 Reduce 工作节点之间进行通信,所需的中间结果必须存储在持久化存储中。因此,这些结果被推送到 HDFS 中,而不是保存在 RAM 中,然后在后续的迭代中重新加载到内存。磁盘 I/O 的数量依赖于算法中的迭代次数,而每次保存和加载数据时都会有序列化和反序列化的开销。总体而言,这在计算上是昂贵的,且未能达到预期的流行程度。

以下是该场景的图示表示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_01_002.jpg

为了解决这个问题,开发了定制的解决方案,例如 Google 的 Pregel,这是一种迭代图处理算法,优化了进程间通信和中间结果的内存存储,以加快运行速度。类似地,许多其他解决方案被开发或重新设计,以更好地满足某些算法的特定需求。

与其重新设计所有算法,不如需要一个通用引擎,能够被大多数算法用来在分布式计算平台上进行内存计算。预计这种设计将导致迭代计算和临时数据分析的执行速度更快。这也是 Spark 项目在 UC 伯克利 AMPLab 中崭露头角的原因。

数据分析的 Spark

在 Spark 项目在 AMP 实验室成功之后,它于 2010 年开源,并于 2013 年转交给了 Apache 软件基金会。目前,它由 Databricks 主导。

Spark 相较于其他分布式计算平台,具有许多显著优势,例如:

  • 用于迭代性机器学习和交互式数据分析的更快执行平台

  • 单一堆栈用于批处理、SQL 查询、实时流处理、图形处理和复杂数据分析

  • 提供高层次的 API,开发多种分布式应用程序,隐藏分布式编程的复杂性

  • 无缝支持各种数据源,如 RDBMS、HBase、Cassandra、Parquet、MongoDB、HDFS、Amazon S3 等

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_01_003.jpg

以下是迭代算法的内存数据共享示意图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_01_004.jpg

Spark 隐藏了编写核心 MapReduce 作业的复杂性,并通过简单的函数调用提供大多数功能。由于其简单性,Spark 能够满足更广泛的受众群体,如数据科学家、数据工程师、统计学家以及 R/Python/Scala/Java 开发者。

Spark 架构大体上包括一个数据存储层、管理框架和 API。它被设计为运行在 HDFS 文件系统之上,因此可以利用现有的生态系统。部署方式可以是独立服务器,也可以是在分布式计算框架上,如 Apache Mesos 或 YARN。提供了 Scala 语言的 API,Spark 就是用这种语言编写的,同时还支持 Java、R 和 Python。

Spark 技术栈

Spark 是一个通用的集群计算系统,它赋能其他更高层次的组件利用其核心引擎。它与 Apache Hadoop 兼容,意味着它可以从 HDFS 读取和写入数据,并且还可以与 Hadoop API 支持的其他存储系统集成。

尽管它允许在其基础上构建其他更高层次的应用程序,但它已经有几个紧密集成的组件,这些组件与其核心引擎高度结合,以利用未来的核心增强功能。这些应用程序与 Spark 一起捆绑,旨在满足行业中更广泛的需求。大多数现实世界的应用程序需要跨项目进行集成,以解决通常有一组特定需求的业务问题。Apache Spark 使这一过程变得更加轻松,因为它允许其更高层次的组件无缝集成,例如在开发项目中的库。

此外,得益于 Spark 内置对 Scala、Java、R 和 Python 的支持,更广泛的开发者和数据工程师能够利用整个 Spark 技术栈:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_01_005.jpg

Spark 核心

Spark 核心在某种程度上类似于操作系统的内核。它是通用的执行引擎,既快速又容错。整个 Spark 生态系统都建立在这个核心引擎之上。它主要负责作业调度、任务分配和在工作节点之间监控作业。它还负责内存管理、与各种异构存储系统的交互,以及其他各种操作。

Spark 核心的主要构建模块是弹性分布式数据集RDD),它是一个不可变的、容错的元素集合。Spark 可以从多种数据源创建 RDD,例如 HDFS、本地文件系统、Amazon S3、其他 RDD、Cassandra 等 NoSQL 数据存储。它们是具有弹性的,意味着在失败时会自动重建。RDD 通过惰性并行转换构建。它们可以被缓存和分区,并且可能是或不是物化的。

整个 Spark 核心引擎可以视为对分布式数据集进行的一组简单操作。在 Spark 中,所有作业的调度和执行都基于与每个 RDD 关联的方法。此外,和每个 RDD 关联的方法定义了它们自己的分布式内存计算方式。

Spark SQL

该模块设计用于查询、分析和对结构化数据进行操作。这个组件在整个 Spark 堆栈中非常重要,因为大多数组织数据是结构化的,尽管非结构化数据正在快速增长。作为一个分布式查询引擎,它使得 Hadoop Hive 查询在不做任何修改的情况下能够提升最多 100 倍的速度。除了 Hive,它还支持高效的列式存储 Apache Parquet、JSON 以及其他结构化数据格式。Spark SQL 使得可以运行 SQL 查询并与用 Python、Scala 和 Java 编写的复杂程序一起使用。

Spark SQL 提供了一种分布式编程抽象,称为数据框,之前被称为 SchemaRDD,它具有较少的相关功能。数据框是命名列的分布式集合,类似于 SQL 表或 Python 的 Pandas 数据框。它们可以通过多种具有模式的数据源构建,例如 Hive、Parquet、JSON、其他关系型数据库源以及 Spark RDD。

Spark SQL 可用于跨不同格式进行 ETL 处理,并进行临时分析。Spark SQL 配备了一个名为 Catalyst 的优化框架,它可以将 SQL 查询转换为更高效的形式。

Spark 流处理

企业数据的处理窗口正在比以往任何时候都要短。为了解决行业的实时处理需求,Spark 设计了这一组件,它具有容错性和可扩展性。Spark 支持对实时数据流进行数据分析、机器学习和图处理,从而实现实时数据分析。

它提供了一个名为离散化流DStream)的 API,用于操作实时数据流。实时数据流被切分成小批次,比如每 x 秒。Spark 将每个批次当作 RDD 进行处理,执行基本的 RDD 操作。DStream 可以通过来自 HDFS、Kafka、Flume 或任何其他可以通过 TCP 套接字传输数据的源创建。通过对 DStream 应用一些更高级的操作,可以生成其他 DStream。

Spark 流处理的最终结果可以写回 Spark 支持的各种数据存储,或者可以推送到任何仪表盘进行可视化。

MLlib

MLlib 是 Spark 堆栈中的内置机器学习库。它在 Spark 0.8 中被引入。其目标是使机器学习具有可扩展性且易于使用。开发人员可以无缝地在其选择的编程语言中使用 Spark SQL、Spark Streaming 和 GraphX,无论是 Java、Python 还是 Scala。MLlib 提供了执行各种统计分析所需的功能,如相关性、抽样、假设检验等。该组件还涵盖了分类、回归、协同过滤、聚类和分解等领域的广泛应用和算法。

机器学习工作流程涉及收集和预处理数据、构建和部署模型、评估结果以及优化模型。在现实中,预处理步骤需要大量的工作。通常这些是多阶段工作流程,涉及昂贵的中间读/写操作。通常,这些处理步骤可能会在一段时间内多次执行。为了简化这些预处理步骤,提出了一个新概念——ML 管道。管道是一个变换序列,其中一个阶段的输出是另一个阶段的输入,形成一个链条。ML 管道利用 Spark 和 MLlib,允许开发人员定义可重用的变换序列。

GraphX

GraphX 是 Spark 上的一个薄层统一图分析框架。它被设计为一个通用的分布式数据流框架,替代了专门的图处理框架。它是容错的,并且利用了内存计算。

GraphX 是一个嵌入式图处理 API,用于操作图(例如社交网络)并进行图并行计算(例如 Google 的 Pregel)。它结合了图并行和数据并行系统在 Spark 堆栈中的优势,统一了探索性数据分析、迭代图计算和 ETL 处理。它扩展了 RDD 抽象,介绍了 弹性分布式图RDG),这是一个有向图,每个顶点和边都有相关的属性。

GraphX 包含了一组相当大的图算法,例如 PageRank、K-Core、Triangle Count、LDA 等等。

SparkR

SparkR 项目的启动是为了将 R 的统计分析和机器学习功能与 Spark 的可扩展性相结合。它解决了 R 的局限性——即只能处理适合单台机器内存的数据。现在,R 程序可以通过 SparkR 在分布式环境中扩展。

SparkR 实际上是一个 R 包,它提供了一个 R shell 来利用 Spark 的分布式计算引擎。借助 R 丰富的内置数据分析包,数据科学家可以在大规模上交互式地分析大型数据集。

摘要

在本章中,我们简要介绍了大数据的概念。然后,我们讨论了大数据分析中涉及的计算和分析挑战。接着,我们回顾了大数据分析领域在一段时间内如何发展以及趋势如何。我们还介绍了 Spark 如何解决大多数大数据分析挑战,并成为一个通用的统一分析平台,适用于数据科学以及并行计算。本章的最后,我们只是简要介绍了 Spark 堆栈及其组件。

在下一章中,我们将学习 Spark 编程模型。我们将深入了解 Spark 的基本构建块——RDD。此外,我们还将学习如何在 Scala 和 Python 上使用 RDD API 进行编程。

参考文献

Apache Spark 概述:

Apache Spark 架构:

第二章。Spark 编程模型

由于开源框架的普及,大规模数据处理已成为常见做法,借助成千上万节点和内建的容错能力,Hadoop 成为了一个流行的选择。这些框架在执行特定任务(如 提取、转换和加载ETL)以及处理 Web 规模数据的存储应用)上非常成功。然而,开发人员需要面对大量工具的选择,以及已经建立完善的 Hadoop 生态系统。业界急需一个单一的、通用的开发平台,能满足批处理、流处理、交互式和迭代式的需求。这正是 Spark 的动机所在。

上一章概述了大数据分析面临的挑战,以及 Spark 如何在高层次上解决其中的大部分问题。本章将深入探讨 Spark 的设计目标与选择,以更清晰地了解其作为大数据数据科学平台的适用性。我们还将深入讲解核心抽象 弹性分布式数据集RDD)并通过示例进行说明。

本章的前提是需要具备基本的 Python 或 Scala 知识,以及对 Spark 的初步理解。本章涵盖的主题如下:

  • 编程范式 - 语言支持与设计优势

    • 支持的编程语言

    • 选择合适的语言

  • Spark 引擎 - Spark 核心组件及其含义

    • 驱动程序

    • Spark Shell

    • SparkContext

    • 工作节点

    • 执行器

    • 共享变量

    • 执行流程

  • RDD API - 理解 RDD 基础

    • RDD 基础

    • 持久化

  • RDD 操作 - 让我们动手试试

    • 开始使用 Shell

    • 创建 RDD

    • 对普通 RDD 的转换

    • 对配对 RDD 的转换

    • 操作

编程范式

为了解决大数据挑战并作为数据科学及其他可扩展应用的平台,Spark 在设计时考虑了周密的设计因素和语言支持。

Spark 提供了适用于各种应用开发者的 API,以便开发者使用标准的 API 接口创建基于 Spark 的应用程序。Spark 提供了适用于 Scala、Java、R 和 Python 编程语言的 API,详细内容在以下章节中讲解。

支持的编程语言

Spark 内建支持多种语言,可以通过 Shell 进行交互式使用,这种方式称为 读取-评估-打印-循环REPL),对任何语言的开发者来说都非常熟悉。开发者可以选择自己熟悉的语言,利用现有的库,轻松与 Spark 及其生态系统进行交互。接下来,我们将介绍 Spark 支持的语言以及它们如何融入 Spark 生态系统。

Scala

Spark 本身是用 Scala 编写的,Scala 是一种基于 Java 虚拟机 (JVM) 的函数式编程语言。Scala 编译器生成的字节码在 JVM 上执行。因此,它可以与其他基于 JVM 的系统(如 HDFS、Cassandra、HBase 等)无缝集成。选择 Scala 作为编程语言,是因为它简洁的编程接口、交互式命令行以及能够捕获函数并高效地将其传输到集群中的各个节点。Scala 是一种可扩展的(因此得名)、静态类型、有效率的多范式语言,支持函数式和面向对象的语言特性。

除了完全成熟的应用程序,Scala 还支持 Shell(Spark shell)用于在 Spark 上进行交互式数据分析。

Java

由于 Spark 是基于 JVM 的,它自然支持 Java。这有助于现有的 Java 开发者开发数据科学应用程序以及其他可扩展的应用程序。几乎所有内置的库函数都可以通过 Java 访问。在 Spark 中用 Java 编写数据科学任务相对较难,但如果非常熟悉 Java 的人,可能会觉得很容易。

这个 Java API 唯一缺少的是用于 Spark 上交互式数据分析的基于 Shell 的接口。

Python

Python 通过 PySpark 在 Spark 上得到支持,PySpark 是构建在 Spark 的 Java API 之上的(使用 Py4J)。从现在起,我们将使用 PySpark 这个术语来指代 Spark 上的 Python 环境。Python 已经在开发者中因数据处理、数据清洗和其他数据科学相关任务而广受欢迎。随着 Spark 可以解决可扩展计算的挑战,Python 在 Spark 上的支持变得更加流行。

通过 Python 在 Spark 上的交互式命令行(PySpark),可以在大规模数据上进行交互式数据分析。

R

R 通过 SparkR 在 Spark 上得到支持,SparkR 是一个 R 包,借此包 Spark 的可扩展性可以通过 R 来访问。SparkR 使得 R 克服了单线程运行时的局限性,这也是计算仅限于单个节点的原因。

由于 R 最初仅为统计分析和机器学习设计,因此它已经包含了大多数包。数据科学家现在可以在极大数据量下工作,并且几乎不需要学习曲线。R 仍然是许多数据科学家的首选。

选择正确的语言

除了开发者的语言偏好之外,有时还有其他一些约束条件可能会引起关注。以下几点可能会在选择语言时,补充你的开发体验:

  • 在开发复杂逻辑时,交互式命令行非常有用。Spark 支持的所有语言中,除了 Java 外,其他都提供了交互式命令行。

  • R 是数据科学家的通用语言。由于其拥有更丰富的库集,它显然更适合纯数据分析。Spark 1.4.0 中加入了对 R 的支持,这使得 Spark 可以覆盖在 R 上工作的数据科学家。

  • Java 拥有更广泛的开发者基础。Java 8 引入了 Lambda 表达式,从而支持了函数式编程。然而,Java 往往显得冗长。

  • Python 在数据科学领域的受欢迎程度逐渐上升。Pandas 等数据处理库的可用性以及 Python 简单而富有表现力的特性,使其成为一个强有力的候选语言。在数据聚合、数据清理、自然语言处理等场景中,Python 比 R 更具灵活性。

  • Scala 可能是进行实时分析的最佳选择,因为它与 Spark 最为接近。对于来自其他语言的开发者而言,初期的学习曲线不应成为开发生产系统的障碍。Spark 的最新功能通常会首先在 Scala 中发布。Scala 的静态类型和复杂的类型推断提高了效率,并且增强了编译时的检查。Scala 可以利用 Java 的库,因为 Scala 自身的库基础仍处于早期阶段,但正在逐步追赶。

Spark 引擎

要使用 Spark 编程,首先需要对 Spark 组件有基本的理解。在本节中,我们将解释一些重要的 Spark 组件及其执行机制,以帮助开发者和数据科学家编写程序并构建应用程序。

在深入了解细节之前,我们建议你先查看以下图表,以便在进一步阅读时能够更好地理解 Spark 各部分的描述:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_02_001.jpg

驱动程序

Spark Shell 是驱动程序的一个示例。驱动程序是一个在 JVM 中执行并运行用户 main 函数的进程。它拥有一个 SparkContext 对象,该对象是与底层集群管理器的连接。当驱动程序启动时,Spark 应用程序开始;当驱动程序停止时,Spark 应用程序完成。驱动程序通过 SparkContext 实例来协调 Spark 应用程序中的所有进程。

首先,一个 RDD 的谱系 有向无环图DAG)在驱动程序端构建,包含数据源(可能是 RDD)和转换操作。当遇到 action 方法时,这个 DAG 会被提交给 DAG 调度器。DAG 调度器随后将 DAG 分割成逻辑工作单元(例如 map 或 reduce),称为阶段(stages)。每个阶段又由一组任务组成,每个任务由任务调度器分配给一个执行器(worker)。任务可以按 FIFO 顺序或轮询顺序执行,具体取决于配置。

提示

在单个 Spark 应用程序中,如果多个任务是从不同的线程提交的,它们可以并行执行。

Spark Shell

Spark shell 其实就是由 Scala 和 Python 提供的接口。它看起来与其他任何交互式 shell 非常相似。它有一个 SparkContext 对象(默认为你创建)让你能够利用分布式集群。交互式 shell 对于探索性或临时分析非常有用。你可以通过 shell 一步步地开发复杂的脚本,而不需要经历编译-构建-执行的循环过程。

SparkContext

SparkContext 是进入 Spark 核心引擎的入口点。此对象用于创建和操作 RDD,以及在集群上创建共享变量。SparkContext 对象连接到集群管理器,集群管理器负责资源分配。Spark 自带独立的集群管理器。由于集群管理器是 Spark 中的可插拔组件,因此可以通过外部集群管理器(如 Apache Mesos 或 YARN)来管理它。

当你启动 Spark shell 时,默认会为你创建一个 SparkContext 对象。你也可以通过传递一个 SparkConf 对象来创建它,该对象用于设置各种 Spark 配置参数,以键值对的形式传递。请注意,JVM 中只能有一个 SparkContext 对象。

工作节点

工作节点是集群中运行应用程序代码的节点,听从驱动程序的指令。实际的工作是由工作节点执行的。集群中的每台机器可能有一个或多个工作实例(默认为一个)。一个工作节点执行一个或多个属于一个或多个 Spark 应用程序的执行进程。它由一个 块管理器 组件组成,负责管理数据块。数据块可以是缓存的 RDD 数据、中间的洗牌数据或广播数据。当可用的 RAM 不足时,它会自动将一些数据块移到磁盘。数据在节点之间的复制是块管理器的另一项职责。

执行进程

每个应用程序都有一组执行进程。执行进程驻留在工作节点上,并在集群管理器建立连接后与驱动程序直接通信。所有执行进程都由 SparkContext 管理。执行进程是一个独立的 JVM 实例,服务于一个 Spark 应用程序。执行进程负责通过任务在每个工作节点上管理计算、存储和缓存。它可以并行运行多个任务。

共享变量

通常,代码会随着变量的独立副本一起发送到各个分区。这些变量不能用于将结果(例如中间工作计数)传递回驱动程序。共享变量用于这个目的。有两种共享变量,广播变量累加器

广播变量使程序员能够在每个节点上保留只读副本,而不是将其与任务一起传输。如果在多个操作中使用大型的只读数据,可以将其指定为广播变量,并只将其传输一次到所有工作节点。以这种方式广播的数据是以序列化形式缓存的,在运行每个任务之前会被反序列化。后续操作可以访问这些变量以及与代码一起传输的本地变量。并非所有情况都需要创建广播变量,只有在跨多个阶段的任务需要相同的只读数据副本时,才需要使用广播变量。

累加器是始终递增的变量,例如计数器或累计和。Spark 本地支持数值类型的累加器,但允许程序员为新类型添加支持。请注意,工作节点不能读取累加器的值;它们只能修改其值。

执行流程

一个 Spark 应用程序由一组进程组成,其中有一个驱动程序程序和多个工作程序执行器)程序。驱动程序包含应用程序的main函数和一个 SparkContext 对象,后者表示与 Spark 集群的连接。驱动程序和其他进程之间的协调是通过 SparkContext 对象完成的。

一个典型的 Spark 客户端程序执行以下步骤:

  1. 当程序在 Spark shell 上运行时,它被称为驱动程序,包含用户的main方法。它在运行驱动程序的系统的 JVM 中执行。

  2. 第一步是使用所需的配置参数创建一个 SparkContext 对象。当你运行 PySpark 或 Spark shell 时,它会默认实例化,但对于其他应用程序,你需要显式地创建它。SparkContext 实际上是访问 Spark 的入口。

  3. 下一步是定义一个或多个 RDD,方法是加载文件或通过传递项的数组(称为并行集合)以编程方式定义。

  4. 然后可以通过一系列转换定义更多的 RDD,这些转换通过祖先图进行追踪和管理。这些 RDD 转换可以看作是 UNIX 命令管道,其中一个命令的输出作为下一个命令的输入,以此类推。每个转换步骤的结果 RDD 都有指向其父 RDD 的指针,并且有一个用于计算其数据的函数。RDD 只有在遇到动作语句后才会被操作。所以,转换是懒操作,用于定义新的 RDD,而动作启动计算并返回值给程序或将数据写入外部存储。我们将在以下部分进一步讨论这一方面。

  5. 在这个阶段,Spark 创建一个执行图,其中节点表示 RDD,边表示转换步骤。Spark 将作业分解为多个任务,在不同的机器上运行。这是 Spark 如何将计算发送到集群中的节点上的一种方法,而不是将所有数据一起获取并进行计算。

RDD API

RDD 是一个只读、分区的、容错的记录集合。从设计的角度来看,需要一个单一的数据结构抽象,隐藏处理各种数据源(如 HDFS、文件系统、RDBMS、NOSQL 数据结构或任何其他数据源)的复杂性。用户应能够从这些源中定义 RDD。目标是支持广泛的操作,并允许用户以任何顺序组合它们。

RDD 基础知识

每个数据集在 Spark 编程接口中表示为一个名为 RDD 的对象。Spark 提供了两种创建 RDD 的方式。一种方式是并行化现有集合。另一种方式是引用外部存储系统(如文件系统)中的数据集。

RDD 由一个或多个数据源组成,可能在执行一系列包括多个运算符的转换后。每个 RDD 或 RDD 分区都知道如何在失败时重新创建自己。它具有转换的日志或血统,从稳定存储或另一个 RDD 中重新创建自己所需。因此,使用 Spark 的任何程序都可以确保具有内置的容错性,无论底层数据源和 RDD 的类型如何。

RDD 上有两种方法可用:转换和操作。转换是用于创建 RDD 的方法。操作是利用 RDD 的方法。RDD 通常被分区。用户可以选择持久化 RDD,以便在其程序中重复使用。

RDDs 是不可变(只读)数据结构,因此任何转换都会导致新的 RDD 的创建。这些转换是惰性应用的,只有在应用任何操作时才会应用它们,而不是在定义 RDD 时。每次在操作中使用 RDD 时,RDD 都会重新计算,除非用户显式将 RDD 持久化到内存中。将数据保存在内存中可以节省大量时间。如果内存不足以完全容纳 RDD,则该 RDD 的剩余部分将自动存储(溢写)到硬盘上。惰性转换的一个优点是可以优化转换步骤。例如,如果操作是返回第一行,则 Spark 只计算单个分区并跳过其余部分。

一个 RDD 可以视为一组分区(拆分),它具有一份父 RDD 的依赖列表,以及一个给定父 RDD 的函数,用于计算该分区。有时,每个父 RDD 的分区只会被一个子 RDD 使用。这叫做 窄依赖。窄依赖是理想的,因为当父 RDD 分区丢失时,只需要重新计算单个子分区。另一方面,计算一个包含诸如 按键分组 等操作的单个子 RDD 分区时,依赖于多个父 RDD 分区。每个父 RDD 分区中的数据在生成多个子 RDD 分区数据时都需要用到。这样的依赖叫做 宽依赖。在窄依赖的情况下,父 RDD 和子 RDD 分区可以保持在同一个节点上(共同分区)。但在宽依赖的情况下,由于父数据分散在多个分区中,这是不可能的。在这种情况下,数据应该在分区间进行 洗牌。数据洗牌是一个资源密集型操作,应尽可能避免。宽依赖的另一个问题是,即使只有一个父 RDD 分区丢失,所有子 RDD 分区也需要重新计算。

持久化

每当 RDD 被操作时,它都会即时计算。开发者可以覆盖这个默认行为,指示要在分区之间 持久化缓存 数据集。如果该数据集需要参与多个操作,那么持久化可以节省大量时间、CPU 周期、磁盘 I/O 和网络带宽。容错机制同样适用于缓存的分区。当任何分区由于节点故障丢失时,会通过谱系图重新计算该分区。如果可用内存不足,Spark 会优雅地将持久化的分区溢出到磁盘。开发者可以使用 unpersist 移除不需要的 RDD。然而,Spark 会自动监控缓存,并使用 最近最少使用LRU)算法移除旧的分区。

提示

Cache()persist()persist (MEMORY_ONLY) 相同。虽然 persist() 方法可以接受许多其他参数来指定不同的持久化级别,例如仅内存、内存和磁盘、仅磁盘等,但 cache() 方法仅用于内存中的持久化。

RDD 操作

Spark 编程通常从选择一个你熟悉的界面开始。如果你打算进行交互式数据分析,那么 shell 提示符显然是一个不错的选择。然而,选择 Python shell(PySpark)或 Scala shell(Spark-Shell)在一定程度上取决于你对这些语言的熟练程度。如果你正在构建一个完整的可扩展应用程序,那么熟练度就显得尤为重要,因此你应该根据自己擅长的语言(Scala、Java 或 Python)来开发应用,并提交到 Spark。我们将在本书后面详细讨论这一方面。

创建 RDD

在本节中,我们将使用 Python shell(PySpark)和 Scala shell(Spark-Shell)来创建 RDD。两个 shell 都具有预定义的、能够感知解释器的 SparkContext,并将其分配给变量 sc

让我们通过一些简单的代码示例开始。请注意,代码假定当前工作目录是 Spark 的主目录。以下代码片段启动 Spark 交互式 shell,从本地文件系统读取文件,并打印该文件的第一行:

Python

> bin/pyspark  // Start pyspark shell  
>>> _         // For simplicity sake, no Log messages are shown here 

>>> type(sc)    //Check the type of Predefined SparkContext object 
<class 'pyspark.context.SparkContext'> 

//Pass the file path to create an RDD from the local file system 
>>> fileRDD = sc.textFile('RELEASE') 

>>> type(fileRDD)  //Check the type of fileRDD object  
<class 'pyspark.rdd.RDD'> 

>>>fileRDD.first()   //action method. Evaluates RDD DAG and also returns the first item in the RDD along with the time taken 
took 0.279229 s 
u'Spark Change Log' 

Scala

> bin/Spark-Shell  // Start Spark-shell  
Scala> _      // For simplicity sake, no Log messages are shown here 

Scala> sc   //Check the type of Predefined SparkContext object 
res1: org.apache.spark.SparkContext = org.apache.spark.SparkContext@70884875 

//Pass the file path to create an RDD from the local file system 

Scala> val fileRDD = sc.textFile("RELEASE") 

Scala> fileRDD  //Check the type of fileRDD object  
res2: org.apache.spark.rdd.RDD[String] = ../ RELEASE
MapPartitionsRDD[1] at textFile at <console>:21 

Scala>fileRDD.first()   //action method. Evaluates RDD DAG and also returns the first item in the RDD along with the time taken 
0.040965 s 
res6: String = Spark Change Log 

在前面的示例中,第一行已经调用了交互式 shell。SparkContext 变量 sc 已按预期定义。我们创建了一个名为 fileRDD 的 RDD,指向文件 RELEASE。此语句只是一个转换,直到遇到操作时才会执行。你可以尝试给一个不存在的文件名,但在执行下一个语句之前不会报错,而该语句恰好是一个 action 操作。

我们已经完成了启动 Spark 应用程序(shell)、创建 RDD 并消费它的整个过程。由于 RDD 每次执行操作时都会重新计算,fileRDD 并不会持久化到内存或硬盘中。这使得 Spark 可以优化步骤顺序并智能地执行。事实上,在之前的示例中,优化器只会读取输入文件的一个分区,因为 first() 不需要进行完整的文件扫描。

回顾一下,创建 RDD 有两种方式:一种是创建指向数据源的指针,另一种是并行化现有的集合。之前的示例展示了第一种方式,即从存储系统加载文件。接下来我们将看到第二种方式,即并行化现有的集合。通过传递内存中的集合来创建 RDD 非常简单,但对于大型集合可能效果不佳,因为输入集合必须完全适应驱动节点的内存。

以下示例通过使用 parallelize 函数将 Python/Scala 列表传递来创建 RDD:

Python

// Pass a Python collection to create an RDD 
>>> numRDD = sc.parallelize([1,2,3,4],2) 
>>> type(numRDD) 
<class 'pyspark.rdd.RDD'> 
>>> numRDD 
ParallelCollectionRDD[1] at parallelize at PythonRDD.scala:396 
>>> numRDD.first() 
1 
>>> numRDD.map(lambda(x) : x*x).collect() 
[1,4,9,16] 
>>> numRDD.map(lambda(x) : x * x).reduce(lambda a,b: a+b) 
30 

提示

lambda 函数是一个没有名称的函数,通常作为其他函数的参数传递给函数。Python 中的 lambda 函数只能是一个单一的表达式。如果你的逻辑需要多个步骤,可以创建一个独立的函数,并在 lambda 表达式中使用它。

Scala

// Pass a Scala collection to create an RDD 
Scala> val numRDD = sc.parallelize(List(1,2,3,4),2) 
numRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[8] at parallelize at <console>:21 

Scala> numRDD 
res15: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[8] at parallelize at <console>:21 

Scala> numRDD.first() 
res16: Int = 1 

Scala> numRDD.map(x => x*x).collect() 
res2: Array[Int] = Array(1, 4, 9, 16) 

Scala> numRDD.map(x => x * x).reduce(_+_) 
res20: Int = 30 

正如我们在前面的示例中所看到的,我们能够传递一个 Scala/Python 集合来创建一个 RDD,同时我们也可以自由指定将这些集合切分成多少个分区。Spark 为集群中的每个分区运行一个任务,因此必须仔细决定如何优化计算工作量。虽然 Spark 会根据集群自动设置分区数,但我们可以通过将其作为第二个参数传递给 parallelize 函数来手动设置分区数(例如,sc.parallelize(data, 3))。以下是一个 RDD 的示意图,该 RDD 是通过一个包含 14 条记录(或元组)的数据集创建的,并被分成 3 个分区,分布在 3 个节点上:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/1-1.jpg

编写 Spark 程序通常包括转换和操作。转换是延迟操作,用于定义如何构建 RDD。大多数转换接受一个函数作为参数。所有这些方法将一种数据源转换为另一种数据源。每次对任何 RDD 执行转换时,即使只是一个小的改变,也会生成一个新的 RDD,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_02_003.jpg

这是因为 RDD 是不可变(只读)抽象设计的。操作的结果可以写回存储系统,或者在需要生成最终输出时返回给驱动程序进行本地计算。

到目前为止,我们已经看到了一些简单的转换操作,它们定义了 RDD 以及一些操作来处理它们并生成输出。接下来,让我们快速浏览一些常用的转换和操作,随后再看看对成对 RDD 的转换操作。

普通 RDD 的转换操作

Spark API 包含一套丰富的转换操作符,开发人员可以将它们以任意方式组合。请在交互式 shell 中尝试以下示例,以更好地理解这些操作。

filter 操作

filter 操作返回一个 RDD,其中只包含满足 filter 条件的元素,类似于 SQL 中的 WHERE 条件。

Python:

a = sc.parallelize([1,2,3,4,5,6], 3) 
b = a.filter(lambda x: x % 3 == 0) 
b.collect() 
[3,6] 

Scala:

val a = sc.parallelize(1 to 10, 3) 
val b = a.filter(_ % 3 == 0) 
b.collect 

res0: Array[Int] = Array(3, 6, 9) 

distinct 操作

distinct ([numTasks]) 操作返回一个经过去重的新数据集的 RDD:

Python:

c = sc.parallelize(["John", "Jack", "Mike", "Jack"], 2) 
c.distinct().collect() 

['Mike', 'John', 'Jack'] 

Scala:

val c = sc.parallelize(List("John", "Jack", "Mike", "Jack"), 2) 
c.distinct.collect 
res6: Array[String] = Array(Mike, John, Jack) 

val a = sc.parallelize(List(11,12,13,14,15,16,17,18,19,20)) 
a.distinct(2).partitions.length      //create 2 tasks on two partitions of the same RDD for parallel execution 

res16: Int = 2 

交集操作

交集操作接受另一个数据集作为输入。它返回一个包含共同元素的数据集:

Python:

x = sc.parallelize([1,2,3,4,5,6,7,8,9,10]) 
y = sc.parallelize([5,6,7,8,9,10,11,12,13,14,15]) 
z = x.intersection(y) 
z.collect() 

[8, 9, 10, 5, 6, 7] 

Scala:

val x = sc.parallelize(1 to 10) 
val y = sc.parallelize(5 to 15) 
val z = x.intersection(y) 
z.collect 

res74: Array[Int] = Array(8, 9, 5, 6, 10, 7) 

union 操作

union 操作接受另一个数据集作为输入。它返回一个包含自身和输入数据集元素的数据集。如果两个集合中有共同的值,则它们会在联合后的结果集中作为重复值出现:

Python:

a = sc.parallelize([3,4,5,6,7], 1) 
b = sc.parallelize([7,8,9], 1) 
c = a.union(b) 
c.collect() 

[3, 4, 5, 6, 7, 7, 8, 9] 

Scala:

val a = sc.parallelize(3 to 7, 1) 
val b = sc.parallelize(7 to 9, 1) 
val c = a.union(b)     // An alternative way is (a ++ b).collect 

res0: Array[Int] = Array(3, 4, 5, 6, 7, 7, 8, 9) 

map 操作

map 操作通过在输入数据集的每个元素上执行一个输入函数,返回一个分布式数据集:

Python:

a = sc.parallelize(["animal", "human", "bird", "rat"], 3) 
b = a.map(lambda x: len(x)) 
c = a.zip(b) 
c.collect() 

[('animal', 6), ('human', 5), ('bird', 4), ('rat', 3)] 

Scala:

val a = sc.parallelize(List("animal", "human", "bird", "rat"), 3) 
val b = a.map(_.length) 
val c = a.zip(b) 
c.collect 

res0: Array[(String, Int)] = Array((animal,6), (human,5), (bird,4), (rat,3)) 

flatMap 操作

flatMap 操作类似于 map 操作。虽然 map 对每个输入元素返回一个元素,flatMap 对每个输入元素返回一个零个或多个元素的列表:

Python:

a = sc.parallelize([1,2,3,4,5], 4) 
a.flatMap(lambda x: range(1,x+1)).collect() 
   // Range(1,3) returns 1,2 (excludes the higher boundary element) 
[1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5] 

sc.parallelize([5, 10, 20], 2).flatMap(lambda x:[x, x, x]).collect() 
[5, 5, 5, 10, 10, 10, 20, 20, 20] 

Scala:

val a = sc.parallelize(1 to 5, 4) 
a.flatMap(1 to _).collect 
res47: Array[Int] = Array(1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5) 

//One more example 
sc.parallelize(List(5, 10, 20), 2).flatMap(x => List(x, x, x)).collect 
res85: Array[Int] = Array(5, 5, 5, 10, 10, 10, 20, 20, 20) 

keys 操作

keys 操作返回一个 RDD,其中包含每个元组的键:

Python:

a = sc.parallelize(["black", "blue", "white", "green", "grey"], 2) 
b = a.map(lambda x:(len(x), x)) 
c = b.keys() 
c.collect() 

[5, 4, 5, 5, 4] 

Scala:

val a = sc.parallelize(List("black", "blue", "white", "green", "grey"), 2) 
val b = a.map(x => (x.length, x)) 
b.keys.collect 

res2: Array[Int] = Array(5, 4, 5, 5, 4) 

笛卡尔积操作

cartesian 操作接受另一个数据集作为参数,返回两个数据集的笛卡尔积。这是一个可能比较昂贵的操作,会返回一个大小为 m x n 的数据集,其中 mn 是输入数据集的大小:

Python:

x = sc.parallelize([1,2,3]) 
y = sc.parallelize([10,11,12]) 
x.cartesian(y).collect() 

[(1, 10), (1, 11), (1, 12), (2, 10), (2, 11), (2, 12), (3, 10), (3, 11), (3, 12)] 

Scala:

val x = sc.parallelize(List(1,2,3)) 
val y = sc.parallelize(List(10,11,12)) 
x.cartesian(y).collect 

res0: Array[(Int, Int)] = Array((1,10), (1,11), (1,12), (2,10), (2,11), (2,12), (3,10), (3,11), (3,12))  

对配对 RDD 的变换

一些 Spark 操作仅适用于键值对类型的 RDD。请注意,除了计数操作之外,这些操作通常涉及洗牌,因为与某个键相关的数据可能并不总是驻留在同一个分区中。

groupByKey 操作

类似于 SQL 中的 groupBy 操作,它根据键将输入数据分组,你可以使用 aggregateKeyreduceByKey 来执行聚合操作:

Python:

a = sc.parallelize(["black", "blue", "white", "green", "grey"], 2) 
b = a.groupBy(lambda x: len(x)).collect() 
sorted([(x,sorted(y)) for (x,y) in b]) 

[(4, ['blue', 'grey']), (5, ['black', 'white', 'green'])] 

Scala:

val a = sc.parallelize(List("black", "blue", "white", "green", "grey"), 2) 
val b = a.keyBy(_.length) 
b.groupByKey.collect 

res11: Array[(Int, Iterable[String])] = Array((4,CompactBuffer(blue, grey)), (5,CompactBuffer(black, white, green))) 

join 操作

join 操作接受另一个数据集作为输入。两个数据集应该是键值对类型。结果数据集是另一个键值对数据集,包含两个数据集的键和值:

Python:

a = sc.parallelize(["blue", "green", "orange"], 3) 
b = a.keyBy(lambda x: len(x)) 
c = sc.parallelize(["black", "white", "grey"], 3) 
d = c.keyBy(lambda x: len(x)) 
b.join(d).collect() 
[(4, ('blue', 'grey')), (5, ('green', 'black')), (5, ('green', 'white'))] 

//leftOuterJoin 
b.leftOuterJoin(d).collect() 
[(6, ('orange', None)), (4, ('blue', 'grey')), (5, ('green', 'black')), (5, ('green', 'white'))] 

//rightOuterJoin 
b.rightOuterJoin(d).collect() 
[(4, ('blue', 'grey')), (5, ('green', 'black')), (5, ('green', 'white'))] 

//fullOuterJoin 
b.fullOuterJoin(d).collect() 
[(6, ('orange', None)), (4, ('blue', 'grey')), (5, ('green', 'black')), (5, ('green', 'white'))] 

Scala:

val a = sc.parallelize(List("blue", "green", "orange"), 3) 
val b = a.keyBy(_.length) 
val c = sc.parallelize(List("black", "white", "grey"), 3) 
val d = c.keyBy(_.length) 
b.join(d).collect 
res38: Array[(Int, (String, String))] = Array((4,(blue,grey)), (5,(green,black)), (5,(green,white))) 

//leftOuterJoin 
b.leftOuterJoin(d).collect 
res1: Array[(Int, (String, Option[String]))] = Array((6,(orange,None)), (4,(blue,Some(grey))), (5,(green,Some(black))), (5,(green,Some(white)))) 

//rightOuterJoin 
b.rightOuterJoin(d).collect 
res1: Array[(Int, (Option[String], String))] = Array((4,(Some(blue),grey)), (5,(Some(green),black)), (5,(Some(green),white))) 

//fullOuterJoin 
b.fullOuterJoin(d).collect 
res1: Array[(Int, (Option[String], Option[String]))] = Array((6,(Some(orange),None)), (4,(Some(blue),Some(grey))), (5,(Some(green),Some(black))), (5,(Some(green),Some(white))))  

reduceByKey 操作

reduceByKey 操作使用一个结合性减少函数来合并每个键的值。这也将在每个映射器上本地执行合并,然后将结果发送到减少器并生成哈希分区输出:

Python:

a = sc.parallelize(["black", "blue", "white", "green", "grey"], 2) 
b = a.map(lambda x: (len(x), x)) 
b.reduceByKey(lambda x,y: x + y).collect() 
[(4, 'bluegrey'), (5, 'blackwhitegreen')] 

a = sc.parallelize(["black", "blue", "white", "orange"], 2) 
b = a.map(lambda x: (len(x), x)) 
b.reduceByKey(lambda x,y: x + y).collect() 
[(4, 'blue'), (6, 'orange'), (5, 'blackwhite')] 

Scala:

val a = sc.parallelize(List("black", "blue", "white", "green", "grey"), 2) 
val b = a.map(x => (x.length, x)) 
b.reduceByKey(_ + _).collect 
res86: Array[(Int, String)] = Array((4,bluegrey), (5,blackwhitegreen)) 

val a = sc.parallelize(List("black", "blue", "white", "orange"), 2) 
val b = a.map(x => (x.length, x)) 
b.reduceByKey(_ + _).collect 
res87: Array[(Int, String)] = Array((4,blue), (6,orange), (5,blackwhite))  

聚合操作

aggregrate 操作返回一个 RDD,其中包含每个元组的键:

Python:

z = sc.parallelize([1,2,7,4,30,6], 2) 
z.aggregate(0,(lambda x, y: max(x, y)),(lambda x, y: x + y)) 
37 
z = sc.parallelize(["a","b","c","d"],2) 
z.aggregate("",(lambda x, y: x + y),(lambda x, y: x + y)) 
'abcd' 
z.aggregate("s",(lambda x, y: x + y),(lambda x, y: x + y)) 
'ssabsscds' 
z = sc.parallelize(["12","234","345","56789"],2) 
z.aggregate("",(lambda x, y: str(max(len(str(x)), len(str(y))))),(lambda x, y: str(y) + str(x))) 
'53' 
z.aggregate("",(lambda x, y: str(min(len(str(x)), len(str(y))))),(lambda x, y: str(y) + str(x))) 
'11' 
z = sc.parallelize(["12","234","345",""],2) 
z.aggregate("",(lambda x, y: str(min(len(str(x)), len(str(y))))),(lambda x, y: str(y) + str(x))) 
'01' 

Scala:

val z = sc.parallelize(List(1,2,7,4,30,6), 2) 
z.aggregate(0)(math.max(_, _), _ + _) 
res40: Int = 37 

val z = sc.parallelize(List("a","b","c","d"),2) 
z.aggregate("")(_ + _, _+_) 
res115: String = abcd 

z.aggregate("x")(_ + _, _+_) 
res116: String = xxabxcd 

val z = sc.parallelize(List("12","234","345","56789"),2) 
z.aggregate("")((x,y) => math.max(x.length, y.length).toString, (x,y) => x + y) 
res141: String = 53 

z.aggregate("")((x,y) => math.min(x.length, y.length).toString, (x,y) => x + y) 
res142: String = 11 

val z = sc.parallelize(List("12","234","345",""),2) 
z.aggregate("")((x,y) => math.min(x.length, y.length).toString, (x,y) => x + y) 
res143: String = 01 

注意

请注意,在前面的聚合示例中,结果字符串(例如,abcdxxabxcd5301)不需要与这里显示的输出完全匹配。它取决于各个任务返回其输出的顺序。

Actions

一旦创建了 RDD,各种变换只有在对其执行 action 操作时才会被执行。一个 action 操作的结果可以是写回存储系统的数据,或者返回给发起该操作的驱动程序,以便进一步在本地计算产生最终结果。

我们已经在前面的变换示例中涵盖了一些 action 函数。下面是更多的示例,但还有很多你需要探索的。

collect() 函数

collect() 函数将 RDD 操作的所有结果作为数组返回给驱动程序。通常,针对生成足够小的数据集的操作,这个函数非常有用。理想情况下,结果应该能够轻松适应承载驱动程序的系统的内存。

count() 函数

该操作返回数据集中的元素数量或 RDD 操作的结果输出。

take(n) 函数

take(n)函数返回数据集的前n个元素或 RDD 操作的结果输出。

first()函数

first()函数返回数据集的第一个元素或 RDD 操作的结果输出。它的工作方式与take(1)函数类似。

takeSample()函数

takeSample(withReplacement, num, [seed])函数返回一个包含数据集随机样本元素的数组。它有三个参数,如下所示:

  • withReplacement/withoutReplacement:表示是否进行有放回或无放回的抽样(在多次抽样时,表示是否将旧样本放回集合中再抽取新的样本,或者是直接不放回)。对于withReplacement,参数应为True,否则为False

  • num:表示样本中的元素数量。

  • Seed:这是一个随机数生成器种子(可选)。

countByKey()函数

countByKey()函数仅在键值类型的 RDD 上可用。它返回一个包含(KInt)对的表,表中记录了每个键的计数。

以下是一些 Python 和 Scala 的示例代码:

Python

>>> sc.parallelize([2, 3, 4]).count() 
3 

>>> sc.parallelize([2, 3, 4]).collect() 
[2, 3, 4] 

>>> sc.parallelize([2, 3, 4]).first() 
2 

>>> sc.parallelize([2, 3, 4]).take(2) 
[2, 3] 

Scala

Scala> sc.parallelize(List(2, 3, 4)).count() 
res0: Long = 3 

Scala> sc.parallelize(List(2, 3, 4)).collect() 
res1: Array[Int] = Array(2, 3, 4) 

Scala> sc.parallelize(List(2, 3, 4)).first() 
res2: Int = 2 

Scala> sc.parallelize(List(2, 3, 4)).take(2) 
res3: Array[Int] = Array(2, 3)  

总结

在本章中,我们涉及了支持的编程语言、它们的优缺点以及何时选择一种语言而不是另一种语言。我们讨论了 Spark 引擎的设计以及它的核心组件和执行机制。我们了解了 Spark 如何将待处理的数据发送到多个集群节点。接着我们讨论了一些 RDD 概念,学习了如何通过 Scala 和 Python 创建 RDD 并对其进行转换和操作。我们还讨论了一些 RDD 的高级操作。

在下一章中,我们将详细学习 DataFrame 及其如何证明适用于各种数据科学需求。

参考文献

Scala 语言:

Apache Spark 架构:

Spark 编程指南是学习概念的主要资源;有关可用操作的完整列表,请参阅语言特定的 API 文档:

《Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing》由 Matei Zaharia 等人编写,是 RDD 基础的原始来源:

Spark Summit,Apache Spark 的官方活动系列,提供了丰富的最新信息。查看过去活动的演示文稿和视频:

第三章:DataFrame 介绍

要解决任何实际的“大数据分析”问题,访问一个高效且可扩展的计算系统是绝对必要的。然而,如果计算能力对目标用户而言不易获取且不熟悉,那它几乎毫无意义。当数据集可以表示为命名列时,交互式数据分析会变得更简单,而这在普通的 RDD 中并不适用。因此,采用基于架构的方式来标准化数据表示的需求,正是 DataFrame 的灵感来源。

上一章概述了 Spark 的一些设计方面。我们了解了 Spark 如何通过内存计算在分布式数据集合(RDD)上实现分布式数据处理。它涵盖了大部分揭示 Spark 作为一个快速、高效和可扩展计算平台的要点。在本章中,我们将看到 Spark 如何引入 DataFrame API,使数据科学家能够轻松地进行他们日常的数据分析工作。

本章内容将作为接下来许多章节的基础,我们强烈建议你充分理解这里涵盖的概念。作为本章的前提,需具备 SQL 和 Spark 的基本理解。本章涵盖的主题如下:

  • 为什么选择 DataFrame?

  • Spark SQL

    • Catalyst 优化器
  • DataFrame API

    • DataFrame 基础

    • RDD 与 DataFrame

  • 创建 DataFrame

    • 来自 RDD

    • 来自 JSON

    • 来自 JDBC 数据源

    • 来自其他数据源

  • 操作 DataFrame

为什么选择 DataFrame?

除了强大的、可扩展的计算能力外,大数据应用还需要结合一些额外的特性,例如支持交互式数据分析的关系系统(简单的 SQL 风格)、异构数据源以及不同的存储格式和处理技术。

尽管 Spark 提供了一个函数式编程 API 来操作分布式数据集合,但最终结果还是元组(_1、_2、…)。在元组上编写代码稍显复杂且凌乱,且有时速度较慢。因此,迫切需要一个标准化的层,具备以下特点:

  • 使用带有架构的命名列(比元组更高层次的抽象),使得对它们进行操作和追踪变得容易

  • 提供将来自不同数据源的数据(如 Hive、Parquet、SQL Server、PostgreSQL、JSON,以及 Spark 原生的 RDD)整合并统一为一个公共格式的功能

  • 能够利用 Avro、CSV、JSON 等特殊文件格式中的内置架构。

  • 支持简单的关系运算以及复杂的逻辑操作

  • 消除了基于领域特定任务为机器学习算法定义列对象的需求,作为机器学习库(MLlib)中所有算法的公共数据层

  • 一个语言无关的实体,可以在不同语言的函数之间传递

为了满足上述需求,DataFrame API 被构建为在 Spark SQL 之上的又一抽象层。

Spark SQL

执行 SQL 查询以满足基本业务需求是非常常见的,几乎每个企业都通过某种数据库进行此操作。所以 Spark SQL 也支持执行使用基本 SQL 语法或 HiveQL 编写的 SQL 查询。Spark SQL 还可以用来从现有的 Hive 安装中读取数据。除了这些简单的 SQL 操作外,Spark SQL 还解决了一些棘手的问题。通过关系查询设计复杂逻辑曾经是繁琐的,甚至在某些时候几乎是不可能的。因此,Spark SQL 旨在将关系处理和函数式编程的能力结合起来,从而实现、优化和在分布式计算环境中扩展复杂逻辑。基本上有三种方式与 Spark SQL 交互,包括 SQL、DataFrame API 和 Dataset API。Dataset API 是在写本书时 Spark 1.6 中加入的实验性层,因此我们将仅限于讨论 DataFrame。

Spark SQL 将 DataFrame 显示为更高级的 API,处理所有涉及的复杂性并执行所有后台任务。通过声明式语法,用户可以专注于程序应该完成什么,而不必担心控制流,因为这将由内置于 Spark SQL 中的 Catalyst 优化器处理。

Catalyst 优化器

Catalyst 优化器是 Spark SQL 和 DataFrame 的支点。它通过 Scala 的函数式编程构造构建,具有以下特点:

  • 从各种数据格式中推断模式:

    • Spark 内置支持 JSON 模式推断。用户只需将任何 JSON 文件注册为表,并通过 SQL 语法进行查询,即可创建该表。

    • RDD 是 Scala 对象;类型信息通过 Scala 的类型系统提取,也就是案例类,如果它们包含案例类的话。

    • RDD 是 Python 对象;类型信息通过不同的方式提取。由于 Python 不是静态类型的,遵循动态类型系统,因此 RDD 可以包含多种类型。因此,Spark SQL 会对数据集进行采样,并使用类似于 JSON 模式推断的算法推断模式。

    • 未来将提供对 CSV、XML 和其他格式的内置支持。

  • 内置支持多种数据源和查询联合,以高效导入数据:

    • Spark 具有内置机制,可以通过查询联合从某些外部数据源(例如 JSON、JDBC、Parquet、MySQL、Hive、PostgreSQL、HDFS、S3 等)获取数据。它可以通过使用开箱即用的 SQL 数据类型和其他复杂数据类型,如 Struct、Union、Array 等,精确建模源数据。

    • 它还允许用户使用数据源 API从 Spark 原生不支持的数据源中获取数据(例如 CSV、Avro、HBase、Cassandra 等)。

    • Spark 使用谓词下推(将过滤或聚合操作推送到外部存储系统)来优化从外部系统获取数据,并将它们结合形成数据管道。

  • 代码生成的控制与优化:

    • 优化实际上发生在整个执行管道的最后阶段。

    • Catalyst 被设计用来优化查询执行的所有阶段:分析、逻辑优化、物理规划和代码生成,将查询的部分内容编译为 Java 字节码。

DataFrame API

类似于 Excel 表格的数据表示,或来自数据库投影(select 语句的输出),数据表示最接近人类理解的始终是由多个行和统一列组成的集合。这样的二维数据结构,通常具有带标签的行和列,在一些领域被称为 DataFrame,例如 R 的 DataFrame 和 Python 的 Pandas DataFrame。在 DataFrame 中,通常单列包含相同类型的数据,行描述与该列相关的数据点,这些数据点一起表达某些含义,无论是关于一个人的信息、一次购买,还是一场棒球比赛的结果。你可以将它视为矩阵、电子表格或关系型数据库表。

在 R 和 Pandas 中,DataFrame 在切片、重塑和分析数据方面非常方便——这些是任何数据清洗和数据分析工作流中的基本操作。这启发了在 Spark 中开发类似的概念,称为 DataFrame。

DataFrame 基础

DataFrame API 首次在 Spark 1.3.0 版本中引入,发布于 2015 年 3 月。它是 Spark SQL 对结构化和半结构化数据处理的编程抽象。它使开发人员能够通过 Python、Java、Scala 和 R 等语言利用 DataFrame 数据结构的强大功能。与 RDD 相似,Spark DataFrame 是一组分布式记录,按命名列组织,类似于关系型数据库管理系统(RDBMS)中的表或 R 或 Pandas 中的 DataFrame。与 RDD 不同的是,DataFrame 会跟踪模式(schema),并促进关系型操作以及程序化操作,如 map。在内部,DataFrame 以列式格式存储数据,但在需要程序化函数时,会动态构建行对象。

DataFrame API 带来了两个特点:

  • 内置对多种数据格式的支持,如 Parquet、Hive 和 JSON。然而,通过 Spark SQL 的外部数据源 API,DataFrame 可以访问各种第三方数据源,如数据库和 NoSQL 存储。

  • 一个更强大、功能丰富的 DSL,内置了许多用于常见任务的函数,如:

    • 元数据

    • 抽样

    • 关系型数据处理 - 投影、过滤、聚合、连接

    • 用户定义函数(UDFs)

DataFrame API 基于 Spark SQL 查询优化器,自动在集群的机器上高效执行代码。

RDDs 与 DataFrames 的区别

RDD 和 DataFrame 是 Spark 提供的两种不同类型的容错分布式数据抽象。它们在某些方面相似,但在实现上有很大区别。开发者需要清楚理解它们之间的差异,以便能够将需求与合适的抽象匹配。

相似性

以下是 RDD 和 DataFrame 之间的相似性:

  • 两者都是 Spark 中容错的、分区的数据抽象

  • 两者都能处理不同的数据源

  • 两者都是惰性求值的(执行发生在对它们进行输出操作时),从而能够采用最优化的执行计划。

  • 这两个 API 在四种语言中都可用:Scala、Python、Java 和 R。

区别

以下是 RDD 和 DataFrame 之间的区别:

  • DataFrame 是比 RDD 更高级的抽象。

  • 定义 RDD 意味着定义一个 有向无环图 (DAG),而定义 DataFrame 则会创建一个 抽象语法树 (AST)。AST 将由 Spark SQL catalyst 引擎使用并优化。

  • RDD 是一种通用的数据结构抽象,而 DataFrame 是专门用于处理二维表格数据的结构。

DataFrame API 实际上是 SchemaRDD 的重命名。重命名的目的是为了表明它不再继承自 RDD,并且为了让数据科学家对这个熟悉的名称和概念感到安心。

创建 DataFrame

Spark DataFrame 的创建方式类似于 RDD 的创建方式。要访问 DataFrame API,你需要 SQLContext 或 HiveContext 作为入口点。在本节中,我们将演示如何从各种数据源创建 DataFrame,从基本的内存集合代码示例开始:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_03_001.jpg

从 RDD 创建 DataFrame

以下代码从一个包含颜色的列表创建一个 RDD,然后是包含颜色名称及其长度的元组集合。它使用 toDF 方法将 RDD 转换为 DataFrame。toDF 方法接受一个可选的列标签列表作为参数:

Python

   //Create a list of colours 
>>> colors = ['white','green','yellow','red','brown','pink'] 
//Distribute a local collection to form an RDD 
//Apply map function on that RDD to get another RDD containing colour, length tuples 
>>> color_df = sc.parallelize(colors) 
        .map(lambda x:(x,len(x))).toDF(["color","length"]) 

>>> color_df 
DataFrame[color: string, length: bigint] 

>>> color_df.dtypes        //Note the implicit type inference 
[('color', 'string'), ('length', 'bigint')] 

>>> color_df.show()  //Final output as expected. Order need not be the same as shown 
+------+------+ 
| color|length| 
+------+------+ 
| white|     5| 
| green|     5| 
|yellow|     6| 
|   red|     3| 
| brown|     5| 
|  pink|     4| 
+------+------+ 

Scala

//Create a list of colours 
Scala> val colors = List("white","green","yellow","red","brown","pink") 
//Distribute a local collection to form an RDD 
//Apply map function on that RDD to get another RDD containing colour, length tuples 
Scala> val color_df = sc.parallelize(colors) 
         .map(x => (x,x.length)).toDF("color","length") 

Scala> color_df 
res0: org.apache.spark.sql.DataFrame = [color: string, length: int] 

Scala> color_df.dtypes  //Note the implicit type inference   
res1: Array[(String, String)] = Array((color,StringType), (length,IntegerType)) 

Scala> color_df.show()//Final output as expected. Order need not be the same as shown 
+------+------+ 
| color|length| 
+------+------+ 
| white|     5| 
| green|     5| 
|yellow|     6| 
|   red|     3| 
| brown|     5| 
|  pink|     4| 
+------+------+ 

从前面的例子可以看出,从开发者的角度来看,创建 DataFrame 与创建 RDD 类似。我们在这里创建了一个 RDD,然后将其转换为元组,这些元组接着被传递到 toDF 方法中。请注意,toDF 接受的是元组列表,而不是标量元素。即使是创建单列的 DataFrame,你也需要传递元组。每个元组类似于一行。你可以选择为列命名,否则 Spark 会创建一些模糊的名称,如 _1_2。列的类型推断是隐式进行的。

如果你已经拥有作为 RDD 的数据,Spark SQL 支持两种不同的方法将现有的 RDD 转换为 DataFrame:

  • 第一种方法通过反射推断 RDD 的模式,这些 RDD 包含特定类型的对象,意味着你已经知道该模式。

  • 第二种方法是通过编程接口,它让你可以构建一个模式,并将其应用到现有的 RDD 上。虽然这种方法更加冗长,但它允许在运行时列类型未知时构建 DataFrame。

从 JSON 创建 DataFrame

JavaScript 对象表示法(JSON)是一种独立于语言的、自描述的、轻量级的数据交换格式。JSON 已经成为一种流行的数据交换格式,并且无处不在。除了 JavaScript 和 RESTful 接口之外,像 MySQL 这样的数据库已经接受 JSON 作为数据类型,MongoDB 将所有数据作为二进制格式的 JSON 文档存储。数据与 JSON 之间的转换对于任何现代数据分析工作流来说都是至关重要的。Spark 的 DataFrame API 让开发人员可以将 JSON 对象转换为 DataFrame,反之亦然。让我们通过以下示例深入了解,帮助更好地理解:

Python

//Pass the source json data file path 
>>> df = sqlContext.read.json("./authors.json") 
>>> df.show() //json parsed; Column names and data    types inferred implicitly 
+----------+---------+ 
|first_name|last_name| 
+----------+---------+ 
|      Mark|    Twain| 
|   Charles|  Dickens| 
|    Thomas|    Hardy| 
+----------+---------+ 

Scala

//Pass the source json data file path 
Scala> val df = sqlContext.read.json("./authors.json") 
Scala> df.show()  //json parsed; Column names and    data types inferred implicitly 
+----------+---------+ 
|first_name|last_name| 
+----------+---------+ 
|      Mark|    Twain| 
|   Charles|  Dickens| 
|    Thomas|    Hardy| 
+----------+---------+ 

Spark 会自动推断模式并根据键创建相应的 DataFrame。

使用 JDBC 从数据库创建 DataFrame

Spark 允许开发人员通过 JDBC 从其他数据库创建 DataFrame,前提是你确保目标数据库的 JDBC 驱动程序可以访问。JDBC 驱动程序是一个软件组件,它允许 Java 应用程序与数据库进行交互。不同的数据库需要不同的驱动程序。通常,像 MySQL 这样的数据库提供商会提供这些驱动程序组件来访问他们的数据库。你必须确保你拥有适用于目标数据库的正确驱动程序。

以下示例假设你已经在给定的 URL 上运行了 MySQL 数据库,并且数据库 test 中有一个名为 people 的表,并且其中有一些数据,此外,你也有有效的凭证用于登录。还有一个额外的步骤是使用适当的 JAR 文件重新启动 REPL shell:

注意

如果你系统中尚未拥有 JAR 文件,可以从 MySQL 网站通过以下链接下载:dev.mysql.com/downloads/connector/j/

Python

//Launch shell with driver-class-path as a command line argument 
pyspark --driver-class-path /usr/share/   java/mysql-connector-java.jar 
   //Pass the connection parameters 
>>> peopleDF = sqlContext.read.format('jdbc').options( 
                        url = 'jdbc:mysql://localhost', 
                        dbtable = 'test.people', 
                        user = 'root', 
                        password = 'mysql').load() 
   //Retrieve table data as a DataFrame 
>>> peopleDF.show() 
+----------+---------+------+----------+----------+---------+ 
|first_name|last_name|gender|       dob|occupation|person_id| 
+----------+---------+------+----------+----------+---------+ 
|    Thomas|    Hardy|     M|1840-06-02|    Writer|      101| 
|     Emily|   Bronte|     F|1818-07-30|    Writer|      102| 
| Charlotte|   Bronte|     F|1816-04-21|    Writer|      103| 
|   Charles|  Dickens|     M|1812-02-07|    Writer|      104| 
+----------+---------+------+----------+----------+---------+ 

Scala

//Launch shell with driver-class-path as a command line argument 
spark-shell --driver-class-path /usr/share/   java/mysql-connector-java.jar 
   //Pass the connection parameters 
scala> val peopleDF = sqlContext.read.format("jdbc").options( 
           Map("url" -> "jdbc:mysql://localhost", 
               "dbtable" -> "test.people", 
               "user" -> "root", 
               "password" -> "mysql")).load() 
peopleDF: org.apache.spark.sql.DataFrame = [first_name: string, last_name: string, gender: string, dob: date, occupation: string, person_id: int] 
//Retrieve table data as a DataFrame 
scala> peopleDF.show() 
+----------+---------+------+----------+----------+---------+ 
|first_name|last_name|gender|       dob|occupation|person_id| 
+----------+---------+------+----------+----------+---------+ 
|    Thomas|    Hardy|     M|1840-06-02|    Writer|      101| 
|     Emily|   Bronte|     F|1818-07-30|    Writer|      102| 
| Charlotte|   Bronte|     F|1816-04-21|    Writer|      103| 
|   Charles|  Dickens|     M|1812-02-07|    Writer|      104| 
+----------+---------+------+----------+----------+---------+ 

从 Apache Parquet 创建 DataFrame

Apache Parquet 是一种高效的、压缩的列式数据表示方式,任何 Hadoop 生态系统中的项目都可以使用它。列式数据表示通过列来存储数据,而不是传统的按行存储数据。需要频繁查询多列(通常是两三列)的使用场景特别适合这种存储方式,因为列在磁盘上是连续存储的,而不像行存储那样需要读取不需要的列。另一个优点是在压缩方面。单列中的数据属于同一类型,值通常相似,有时甚至完全相同。这些特性极大地提高了压缩和编码的效率。Parquet 允许在列级别指定压缩方案,并允许随着新压缩编码的发明与实现而添加更多的编码方式。

Apache Spark 提供对 Parquet 文件的读取和写入支持,这些操作会自动保留原始数据的模式。以下示例将前面例子中加载到 DataFrame 中的人员数据写入 Parquet 格式,然后重新读取到 RDD 中:

Python

//Write DataFrame contents into Parquet format 
>>> peopleDF.write.parquet('writers.parquet') 
//Read Parquet data into another DataFrame 
>>> writersDF = sqlContext.read.parquet('writers.parquet')  
writersDF: org.apache.spark.sql.DataFrame = [first_name:    string, last_name: string, gender: string, dob:    date, occupation: string, person_id: int]

Scala

//Write DataFrame contents into Parquet format 
scala> peopleDF.write.parquet("writers.parquet") 
//Read Parquet data into another DataFrame 
scala> val writersDF = sqlContext.read.parquet("writers.parquet")  
writersDF: org.apache.spark.sql.DataFrame = [first_name:    string, last_name: string, gender: string, dob:    date, occupation: string, person_id: int]

从其他数据源创建 DataFrame

Spark 为多个数据源(如 JSON、JDBC、HDFS、Parquet、MYSQL、Amazon S3 等)提供内建支持。此外,它还提供了一个数据源 API,提供了一种可插拔的机制,通过 Spark SQL 访问结构化数据。基于这个可插拔组件,构建了多个库,例如 CSV、Avro、Cassandra 和 MongoDB 等。这些库不属于 Spark 代码库,它们是为特定数据源构建的,并托管在社区网站 Spark Packages 上。

DataFrame 操作

在本章的上一节中,我们学习了许多不同的方法来创建 DataFrame。在本节中,我们将重点介绍可以在 DataFrame 上执行的各种操作。开发者通过链式调用多个操作来过滤、转换、聚合和排序 DataFrame 中的数据。底层的 Catalyst 优化器确保这些操作的高效执行。这些函数类似于你在 SQL 操作中对表常见的操作:

Python

//Create a local collection of colors first 
>>> colors = ['white','green','yellow','red','brown','pink'] 
//Distribute the local collection to form an RDD 
//Apply map function on that RDD to get another RDD containing colour, length tuples and convert that RDD to a DataFrame 
>>> color_df = sc.parallelize(colors) 
        .map(lambda x:(x,len(x))).toDF(['color','length']) 
//Check the object type 
>>> color_df 
DataFrame[color: string, length: bigint] 
//Check the schema 
>>> color_df.dtypes 
[('color', 'string'), ('length', 'bigint')] 

//Check row count 
>>> color_df.count() 
6 
//Look at the table contents. You can limit displayed rows by passing parameter to show 
color_df.show() 
+------+------+ 
| color|length| 
+------+------+ 
| white|     5| 
| green|     5| 
|yellow|     6| 
|   red|     3| 
| brown|     5| 
|  pink|     4| 
+------+------+ 

//List out column names 
>>> color_df.columns 
[u'color', u'length'] 

//Drop a column. The source DataFrame color_df remains the same. //Spark returns a new DataFrame which is being passed to show 
>>> color_df.drop('length').show() 
+------+ 
| color| 
+------+ 
| white| 
| green| 
|yellow| 
|   red| 
| brown| 
|  pink| 
+------+ 
//Convert to JSON format 
>>> color_df.toJSON().first() 
u'{"color":"white","length":5}' 
//filter operation is similar to WHERE clause in SQL 
//You specify conditions to select only desired columns and rows 
//Output of filter operation is another DataFrame object that is usually passed on to some more operations 
//The following example selects the colors having a length of four or five only and label the column as "mid_length" 
filter 
------ 
>>> color_df.filter(color_df.length.between(4,5)) 
      .select(color_df.color.alias("mid_length")).show() 
+----------+ 
|mid_length| 
+----------+ 
|     white| 
|     green| 
|     brown| 
|      pink| 
+----------+ 

//This example uses multiple filter criteria 
>>> color_df.filter(color_df.length > 4) 
     .filter(color_df[0]!="white").show() 
+------+------+ 
| color|length| 
+------+------+ 
| green|     5| 
|yellow|     6| 
| brown|     5| 
+------+------+ 

//Sort the data on one or more columns 
sort 
---- 
//A simple single column sorting in default (ascending) order 
>>> color_df.sort("color").show() 
+------+------+ 
| color|length| 
+------+------+ 
| brown|     5| 
| green|     5| 
|  pink|     4| 
|   red|     3| 
| white|     5| 
|yellow|     6| 
+------+------+ 
//First filter colors of length more than 4 and then sort on multiple columns 
//The Filtered rows are sorted first on the column length in default ascending order. Rows with same length are sorted on color in descending order   
>>> color_df.filter(color_df['length']>=4).sort("length", 'color',ascending=False).show()
+------+------+ 
| color|length| 
+------+------+ 
|yellow|     6| 
| white|     5| 
| green|     5| 
| brown|     5| 
|  pink|     4| 
+------+------+ 

//You can use orderBy instead, which is an alias to sort 
>>> color_df.orderBy('length','color').take(4)
[Row(color=u'red', length=3), Row(color=u'pink', length=4), Row(color=u'brown', length=5), Row(color=u'green', length=5)]

//Alternative syntax, for single or multiple columns.  
>>> color_df.sort(color_df.length.desc(),   color_df.color.asc()).show() 
+------+------+ 
| color|length| 
+------+------+ 
|yellow|     6| 
| brown|     5| 
| green|     5| 
| white|     5| 
|  pink|     4| 
|   red|     3| 
+------+------+ 
//All the examples until now have been acting on one row at a time, filtering or transforming or reordering.  
//The following example deals with regrouping the data 
//These operations require "wide dependency" and often involve shuffling.  
groupBy 
------- 
>>> color_df.groupBy('length').count().show() 
+------+-----+ 
|length|count| 
+------+-----+ 
|     3|    1| 
|     4|    1| 
|     5|    3| 
|     6|    1| 
+------+-----+ 
//Data often contains missing information or null values. We may want to drop such rows or replace with some filler information. dropna is provided for dropping such rows 
//The following json file has names of famous authors. Firstname data is missing in one row. 
dropna 
------ 
>>> df1 = sqlContext.read.json('./authors_missing.json')
>>> df1.show() 
+----------+---------+ 
|first_name|last_name| 
+----------+---------+ 
|      Mark|    Twain| 
|   Charles|  Dickens| 
|      null|    Hardy| 
+----------+---------+ 

//Let us drop the row with incomplete information 
>>> df2 = df1.dropna() 
>>> df2.show()  //Unwanted row is dropped 
+----------+---------+ 
|first_name|last_name| 
+----------+---------+ 
|      Mark|    Twain| 
|   Charles|  Dickens| 
+----------+---------+ 

Scala

//Create a local collection of colors first 
Scala> val colors = List("white","green","yellow","red","brown","pink") 
//Distribute a local collection to form an RDD 
//Apply map function on that RDD to get another RDD containing color, length tuples and convert that RDD to a DataFrame 
Scala> val color_df = sc.parallelize(colors) 
        .map(x => (x,x.length)).toDF("color","length") 
//Check the object type 
Scala> color_df 
res0: org.apache.spark.sql.DataFrame = [color: string, length: int] 
//Check the schema 
Scala> color_df.dtypes 
res1: Array[(String, String)] = Array((color,StringType), (length,IntegerType)) 
//Check row count 
Scala> color_df.count() 
res4: Long = 6 
//Look at the table contents. You can limit displayed rows by passing parameter to show 
color_df.show() 
+------+------+ 
| color|length| 
+------+------+ 
| white|     5| 
| green|     5| 
|yellow|     6| 
|   red|     3| 
| brown|     5| 
|  pink|     4| 
+------+------+ 
//List out column names 
Scala> color_df.columns 
res5: Array[String] = Array(color, length) 
//Drop a column. The source DataFrame color_df remains the same. 
//Spark returns a new DataFrame which is being passed to show 
Scala> color_df.drop("length").show() 
+------+ 
| color| 
+------+ 
| white| 
| green| 
|yellow| 
|   red| 
| brown| 
|  pink| 
+------+ 
//Convert to JSON format 
color_df.toJSON.first() 
res9: String = {"color":"white","length":5} 

//filter operation is similar to WHERE clause in SQL 
//You specify conditions to select only desired columns and rows 
//Output of filter operation is another DataFrame object that is usually passed on to some more operations 
//The following example selects the colors having a length of four or five only and label the column as "mid_length" 
filter 
------ 
Scala> color_df.filter(color_df("length").between(4,5)) 
       .select(color_df("color").alias("mid_length")).show() 
+----------+ 
|mid_length| 
+----------+ 
|     white| 
|     green| 
|     brown| 
|      pink| 
+----------+ 

//This example uses multiple filter criteria. Notice the not equal to operator having double equal to symbols  
Scala> color_df.filter(color_df("length") > 4).filter(color_df( "color")!=="white").show() 
+------+------+ 
| color|length| 
+------+------+ 
| green|     5| 
|yellow|     6| 
| brown|     5| 
+------+------+ 
//Sort the data on one or more columns 
sort 
---- 
//A simple single column sorting in default (ascending) order 
Scala> color_df..sort("color").show() 
+------+------+                                                                  
| color|length| 
+------+------+ 
| brown|     5| 
| green|     5| 
|  pink|     4| 
|   red|     3| 
| white|     5| 
|yellow|     6| 
+------+------+ 
//First filter colors of length more than 4 and then sort on multiple columns 
//The filtered rows are sorted first on the column length in default ascending order. Rows with same length are sorted on color in descending order  
Scala> color_df.filter(color_df("length")>=4).sort($"length", $"color".desc).show() 
+------+------+ 
| color|length| 
+------+------+ 
|  pink|     4| 
| white|     5| 
| green|     5| 
| brown|     5| 
|yellow|     6| 
+------+------+ 
//You can use orderBy instead, which is an alias to sort. 
scala> color_df.orderBy("length","color").take(4) 
res19: Array[org.apache.spark.sql.Row] = Array([red,3], [pink,4], [brown,5], [green,5]) 
//Alternative syntax, for single or multiple columns 
scala> color_df.sort(color_df("length").desc, color_df("color").asc).show() 
+------+------+ 
| color|length| 
+------+------+ 
|yellow|     6| 
| brown|     5| 
| green|     5| 
| white|     5| 
|  pink|     4| 
|   red|     3| 
+------+------+ 
//All the examples until now have been acting on one row at a time, filtering or transforming or reordering. 
//The following example deals with regrouping the data.  
//These operations require "wide dependency" and often involve shuffling. 
groupBy 
------- 
Scala> color_df.groupBy("length").count().show() 
+------+-----+ 
|length|count| 
+------+-----+ 
|     3|    1| 
|     4|    1| 
|     5|    3| 
|     6|    1| 
+------+-----+ 
//Data often contains missing information or null values.  
//The following json file has names of famous authors. Firstname data is missing in one row. 
dropna 
------ 
Scala> val df1 = sqlContext.read.json("./authors_missing.json") 
Scala> df1.show() 
+----------+---------+ 
|first_name|last_name| 
+----------+---------+ 
|      Mark|    Twain| 
|   Charles|  Dickens| 
|      null|    Hardy| 
+----------+---------+ 
//Let us drop the row with incomplete information 
Scala> val df2 = df1.na.drop() 
Scala> df2.show()  //Unwanted row is dropped 
+----------+---------+ 
|first_name|last_name| 
+----------+---------+ 
|      Mark|    Twain| 
|   Charles|  Dickens| 
+----------+---------+ 

底层原理

你现在已经知道,DataFrame API 由 Spark SQL 赋能,而 Spark SQL 的 Catalyst 优化器在优化性能中起着至关重要的作用。

尽管查询是惰性执行的,但它使用了 Catalyst 的catalog组件来识别程序或表达式中使用的列名是否存在于正在使用的表中,且数据类型是否正确,并采取了许多其他预防性措施。采用这种方法的优点是,用户一输入无效表达式,错误就会立刻弹出,而不是等到程序执行时才发现。

总结

在本章中,我们解释了开发 Spark DataFrame API 的动机,以及开发 Spark 使得编程变得比以往更加容易。我们简要介绍了 DataFrame API 的设计理念,以及它如何建立在 Spark SQL 之上。我们讨论了从不同数据源(如 RDD、JSON、Parquet 和 JDBC)创建 DataFrame 的多种方式。在本章结束时,我们简单提到了如何在 DataFrame 上执行操作。我们将在接下来的章节中,结合数据科学和机器学习,更详细地讨论 DataFrame 操作。

在下一章中,我们将学习 Spark 如何支持统一的数据访问,并详细讨论 Dataset 和 Structured Stream 组件。

参考文献

Apache Spark 官方文档中的 DataFrame 参考:

Databricks:在 Apache Spark 中引入 DataFrames,用于大规模数据科学:

Databricks:从 Pandas 到 Apache Spark 的 DataFrame:

Scala API 参考指南,适用于 Spark DataFrames:

Cloudera 博客文章:Parquet——一种高效的通用列式存储格式,适用于 Apache Hadoop:

第四章:统一数据访问

来自不同数据源的数据集成一直是一个艰巨的任务。大数据的三大特征(量、速度、种类)和不断缩短的处理时间框架使得这一任务更加具有挑战性。以接近实时的方式提供清晰且精心整理的数据视图对于企业至关重要。然而,实时整理的数据以及以统一方式执行不同操作(如 ETL、临时查询和机器学习)的能力,正在成为企业的关键差异化因素。

Apache Spark 的创建旨在提供一个通用引擎,能够处理来自各种数据源的数据,并支持大规模的数据处理,适用于各种不同的操作。Spark 使得开发人员能够将 SQL、流处理、图计算和机器学习算法结合到一个工作流中!

在前几章中,我们讨论了弹性分布式数据集RDD)以及数据框(DataFrames)。在第三章,数据框简介中,我们介绍了 Spark SQL 和 Catalyst 优化器。本章在此基础上进行扩展,深入探讨这些主题,帮助你理解统一数据访问的真正本质。我们将介绍新概念,如数据集(Datasets)和结构化流处理(Structured Streaming)。具体来说,我们将讨论以下内容:

  • Apache Spark 中的数据抽象

  • 数据集

    • 使用数据集

    • 数据集 API 限制

  • Spark SQL

    • SQL 操作

    • 底层实现

  • 结构化流处理

    • Spark 流处理编程模型

    • 底层实现

    • 与其他流处理引擎的比较

  • 持续应用

  • 总结

Apache Spark 中的数据抽象

MapReduce 框架及其流行的开源实现 Hadoop 在过去十年中得到了广泛的应用。然而,迭代算法和交互式临时查询并不被很好地支持。在算法中的作业或阶段之间,任何数据共享总是通过磁盘读写实现,而不是内存数据共享。因此,逻辑上的下一步应该是拥有一种机制,能够在多个作业之间复用中间结果。RDD 是一种通用数据抽象,旨在解决这一需求。

RDD 是 Apache Spark 中的核心抽象。它是一个不可变的、容错的、分布式的静态类型对象集合,通常存储在内存中。RDD API 提供了简单的操作,如 map、reduce 和 filter,这些操作可以按任意方式组合。

DataFrame 抽象是在 RDD 之上构建的,并增加了“命名”列。因此,Spark DataFrame 具有类似于关系型数据库表格和 R、Python(pandas)中的 DataFrame 的命名列行。这种熟悉的高级抽象大大简化了开发工作,因为它让你可以像对待 SQL 表或 Excel 文件一样处理数据。此外,Catalyst 优化器在背后会编译操作并生成 JVM 字节码,以实现高效执行。然而,命名列的方法带来了一个新问题。静态类型信息不再对编译器可用,因此我们失去了编译时类型安全的优势。

Dataset API 的引入结合了 RDD 和 DataFrame 的优点,并增加了一些独特的功能。Datasets 提供了类似于 DataFrame 的行列数据抽象,但在其之上定义了结构。这个结构可以通过 Scala 中的 case class 或 Java 中的类来定义。它们提供了类型安全和类似 RDD 的 Lambda 函数。因此,它们支持类型化方法,如mapgroupByKey,也支持非类型化方法,如selectgroupBy。除了 Catalyst 优化器外,Datasets 还利用了 Tungsten 执行引擎提供的内存编码,这进一步提升了性能。

到目前为止,介绍的数据抽象构成了核心抽象。在这些抽象之上,还有一些更为专门化的数据抽象。Streaming API 被引入用于处理来自 Flume 和 Kafka 等各种来源的实时流数据。这些 API 协同工作,为数据工程师提供了一个统一的、连续的 DataFrame 抽象,可以用于交互式和批量查询。另一种专门化的数据抽象是 GraphFrame,它使开发者能够分析社交网络和其他图形数据,同时处理类似 Excel 的二维数据。

现在,了解了可用数据抽象的基本概念后,让我们来理解“统一数据访问平台”到底意味着什么:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_001.jpg

这个统一平台的目的是不仅可以将静态数据和流式数据结合在一起,还可以以统一的方式对数据进行各种不同类型的操作!从开发者的角度来看,Dataset 是核心抽象,Spark SQL 是与 Spark 功能交互的主要接口。结合 SQL 声明式编程接口的二维数据结构已成为处理数据的常见方式,从而缩短了数据工程师的学习曲线。因此,理解统一平台就意味着理解 Datasets 和 Spark SQL。

Datasets

Apache Spark 数据集是 DataFrame API 的扩展,提供了一种类型安全的面向对象编程接口。这个 API 最早在 1.6 版本中引入。Spark 2.0 版本实现了 DataFrame 和 Dataset API 的统一。DataFrame 变成了一个通用的、无类型的数据集;或者说,数据集是一个添加了结构的 DataFrame。这里的“结构”一词指的是底层数据的模式或组织,类似于关系型数据库中的表模式。结构对底层数据中可以表达或包含的内容施加了限制。这反过来能够在内存组织和物理执行上实现更好的优化。编译时的类型检查可以比运行时更早地捕获错误。例如,SQL 比较中的类型不匹配直到运行时才会被发现,而如果它是作为数据集操作序列表达的,则会在编译时被捕获。然而,Python 和 R 的固有动态特性意味着这些语言没有编译时类型安全,因此数据集的概念不适用于这些语言。数据集和 DataFrame 的统一仅适用于 Scala 和 Java API。

数据集抽象的核心是 编码器。这些编码器用于在 JVM 对象和 Spark 内部的 Tungsten 二进制格式之间进行转换。此内部表示绕过了 JVM 的内存管理和垃圾回收。Spark 有自己专门为其支持的工作流编写的 C 风格内存访问方式。最终的内部表示占用更少的内存,并且具有高效的内存管理。紧凑的内存表示在 Shuffle 操作中减少了网络负载。编码器生成的紧凑字节码直接在序列化对象上操作,而无需反序列化,从而提高了性能。提前了解模式能够在缓存数据集时实现更优化的内存布局。

使用数据集

在本节中,我们将创建数据集,并执行转换和操作,类似于 DataFrame 和 RDD。

示例 1 - 从简单集合创建数据集:

Scala:

//Create a Dataset from a simple collection 
scala> val ds1 = List.range(1,5).toDS() 
ds1: org.apache.spark.sql.Dataset[Int] = [value: int] 
//Perform an action 
scala> ds1.collect() 
res3: Array[Int] = Array(1, 2, 3, 4) 

//Create from an RDD 
scala> val colors = List("red","orange","blue","green","yellow") 
scala> val color_ds = sc.parallelize(colors).map(x => 
     (x,x.length)).toDS() 
//Add a case class 
case class Color(var color: String, var len: Int) 
val color_ds = sc.parallelize(colors).map(x => 
     Color(x,x.length)).toDS() 

如前面代码中的最后一个示例所示,case class 添加了结构信息。Spark 使用此结构来创建最佳的数据布局和编码。以下代码展示了我们要查看的结构和执行计划:

Scala:

//Examine the structure 
scala> color_ds.dtypes 
res26: Array[(String, String)] = Array((color,StringType), (len,IntegerType)) 
scala> color_ds.schema 
res25: org.apache.spark.sql.types.StructType = StructType(StructField(color,StringType,true), 
StructField(len,IntegerType,false)) 
//Examine the execution plan 
scala> color_ds.explain() 
== Physical Plan == 
Scan ExistingRDD[color#57,len#58] 

上面的示例展示了预期的结构和物理执行计划。如果你想查看更详细的执行计划,必须传入 explain(true),这会打印扩展信息,包括逻辑计划。

我们已经检查了如何从简单集合和 RDD 创建数据集。我们已经讨论过,DataFrame 只是无类型的数据集。以下示例展示了数据集和 DataFrame 之间的转换。

示例 2 - 将数据集转换为 DataFrame

Scala:

//Convert the dataset to a DataFrame 
scala> val color_df = color_ds.toDF() 
color_df: org.apache.spark.sql.DataFrame = [color: string, len: int] 

scala> color_df.show() 
+------+---+ 
| color|len| 
+------+---+ 
|   red|  3| 
|orange|  6| 
|  blue|  4| 
| green|  5| 
|yellow|  6| 
+------+---+ 

这个示例与我们在第三章,数据框介绍中看到的示例非常相似。这些转换在现实世界中非常实用。考虑为不完美的数据添加一个结构(也叫案例类)。您可以先将这些数据读取到 DataFrame 中,进行清洗,然后转换成 Dataset。另一个应用场景是,您可能希望根据某些运行时信息(如 user_id)仅暴露数据的子集(行和列)。您可以将数据读取到 DataFrame 中,将其注册为临时表,应用条件,然后将子集暴露为 Dataset。以下示例首先创建一个 DataFrame,然后将其转换为 Dataset。请注意,DataFrame 的列名必须与案例类匹配。

示例 3 - 将 DataFrame 转换为 Dataset

//Construct a DataFrame first 
scala> val color_df = sc.parallelize(colors).map(x => 
           (x,x.length)).toDF("color","len") 
color_df: org.apache.spark.sql.DataFrame = [color: string, len: int] 
//Convert the DataFrame to a Dataset with a given structure 
scala> val ds_from_df = color_df.as[Color] 
ds_from_df: org.apache.spark.sql.Dataset[Color] = [color: string, len: int] 
//Check the execution plan 
scala> ds_from_df.explain 
== Physical Plan == 
WholeStageCodegen 
:  +- Project [_1#102 AS color#105,_2#103 AS len#106] 
:     +- INPUT 
+- Scan ExistingRDD[_1#102,_2#103] 

explain 命令的响应显示 WholeStageCodegen,它将多个操作融合成一个 Java 函数调用。由于减少了多个虚拟函数调用,这增强了性能。代码生成自 Spark 1.1 以来就存在,但当时仅限于表达式评估和少量操作,如过滤。而与此不同,Tungsten 的整个阶段代码生成会为整个查询计划生成代码。

从 JSON 创建 Datasets

数据集可以通过 JSON 文件创建,类似于 DataFrame。请注意,一个 JSON 文件可以包含多个记录,但每条记录必须在一行内。如果源 JSON 文件中有换行符,您需要通过编程手段去除它们。JSON 记录可能包含数组,并且可能是嵌套的。它们不需要具有统一的模式。以下示例文件包含 JSON 记录,其中一条记录具有额外的标签和数据数组。

示例 4 - 从 JSON 创建 Dataset

Scala:

//Set filepath 
scala> val file_path = <Your path> 
file_path: String = ./authors.json 
//Create case class to match schema 
scala> case class Auth(first_name: String, last_name: String,books: Array[String]) 
defined class Auth 

//Create dataset from json using case class 
//Note that the json document should have one record per line 
scala> val auth = spark.read.json(file_path).as[Auth] 
auth: org.apache.spark.sql.Dataset[Auth] = [books: array<string>, firstName: string ... 1 more field] 

//Look at the data 
scala> auth.show() 
+--------------------+----------+---------+ 
|               books|first_name|last_name| 
+--------------------+----------+---------+ 
|                null|      Mark|    Twain| 
|                null|   Charles|  Dickens| 
|[Jude the Obscure...|    Thomas|    Hardy| 
+--------------------+----------+---------+ 

//Try explode to see array contents on separate lines 

scala> auth.select(explode($"books") as "book", 
            $"first_name",$"last_name").show(2,false) 
+------------------------+----------+---------+ 
|book                    |first_name|last_name| 
+------------------------+----------+---------+ 
|Jude the Obscure        |Thomas    |Hardy    | 
|The Return of the Native|Thomas    |Hardy    | 
+------------------------+----------+---------+ 

Datasets API 的限制

即使 Datasets API 已经结合了 RDD 和 DataFrame 的优势,但它仍然存在一些局限性,特别是在当前的开发阶段:

  • 在查询数据集时,选定的字段应赋予与案例类相同的特定数据类型,否则输出将变成 DataFrame。例如,auth.select(col("first_name").as[String])

  • Python 和 R 本质上是动态的,因此类型化的 Datasets 并不适用。

Spark SQL

Spark SQL 是 Spark 1.0 引入的一个用于结构化数据处理的 Spark 模块。这个模块是一个紧密集成的关系引擎,与核心 Spark API 协同工作。它使得数据工程师可以编写应用程序,从不同的来源加载结构化数据并将它们连接成统一的,可能是连续的,类似 Excel 的数据框;然后他们可以实施复杂的 ETL 工作流和高级分析。

Spark 2.0 版本带来了 API 的重要统一,并扩展了 SQL 功能,包括支持子查询。现在,数据集 API 和数据框架 API 已经统一,数据框架是数据集的一种“形式”。这些统一的 API 为 Spark 的未来奠定了基础,涵盖了所有库。开发者可以为他们的数据施加“结构”,并可以使用高级声明式 API,从而提高性能和生产力。性能的提升来源于底层的优化层。数据框架、数据集和 SQL 共享相同的优化和执行管道。

SQL 操作

SQL 操作是最广泛使用的数据处理构造。常见的操作包括选择所有或部分列、根据一个或多个条件进行过滤、排序和分组操作,以及计算诸如average等汇总函数在分组数据上的应用。JOIN操作用于多个数据源之间的操作,set操作如unionintersectminus也是常见的操作。此外,数据框架作为临时表注册,并通过传统的 SQL 语句执行上述操作。用户定义函数UDF)可以在注册与未注册的情况下定义和使用。我们将重点关注窗口操作,这些操作在 Spark 2.0 中刚刚引入,主要用于滑动窗口操作。例如,如果你想报告过去七天内每天的平均峰值温度,那么你就是在一个滑动的七天窗口上操作,直到今天为止。以下是一个示例,计算过去三个月的每月平均销售额。数据文件包含 24 个观测值,显示了 P1 和 P2 两个产品的每月销售数据。

示例 5-窗口示例与移动平均计算

Scala:

scala> import org.apache.spark.sql.expressions.Window 
import org.apache.spark.sql.expressions.Window 
//Create a DataFrame containing monthly sales data for two products 
scala> val monthlySales = spark.read.options(Map({"header"->"true"},{"inferSchema" -> "true"})). 
                            csv("<Your Path>/MonthlySales.csv") 
monthlySales: org.apache.spark.sql.DataFrame = [Product: string, Month: int ... 1 more field] 

//Prepare WindowSpec to create a 3 month sliding window for a product 
//Negative subscript denotes rows above current row 
scala> val w = Window.partitionBy(monthlySales("Product")).orderBy(monthlySales("Month")).rangeBetween(-2,0) 
w: org.apache.spark.sql.expressions.WindowSpec = org.apache.spark.sql.expressions.WindowSpec@3cc2f15 

//Define compute on the sliding window, a moving average in this case 
scala> val f = avg(monthlySales("Sales")).over(w) 
f: org.apache.spark.sql.Column = avg(Sales) OVER (PARTITION BY Product ORDER BY Month ASC RANGE BETWEEN 2 PRECEDING AND CURRENT ROW) 
//Apply the sliding window and compute. Examine the results 
scala> monthlySales.select($"Product",$"Sales",$"Month", bround(f,2).alias("MovingAvg")). 
                    orderBy($"Product",$"Month").show(6) 
+-------+-----+-----+---------+                                                  
|Product|Sales|Month|MovingAvg| 
+-------+-----+-----+---------+ 
|     P1|   66|    1|     66.0| 
|     P1|   24|    2|     45.0| 
|     P1|   54|    3|     48.0| 
|     P1|    0|    4|     26.0| 
|     P1|   56|    5|    36.67| 
|     P1|   34|    6|     30.0| 
+-------+-----+-----+---------+ 

Python:

    >>> from pyspark.sql import Window
    >>> import pyspark.sql.functions as func
    //Create a DataFrame containing monthly sales data for two products
    >> file_path = <Your path>/MonthlySales.csv"
    >>> monthlySales = spark.read.csv(file_path,header=True, inferSchema=True)

    //Prepare WindowSpec to create a 3 month sliding window for a product
    //Negative subscript denotes rows above current row
    >>> w = Window.partitionBy(monthlySales["Product"]).orderBy(monthlySales["Month"]).rangeBetween(-2,0)
    >>> w
    <pyspark.sql.window.WindowSpec object at 0x7fdc33774a50>
    >>>
    //Define compute on the sliding window, a moving average in this case
    >>> f = func.avg(monthlySales["Sales"]).over(w)
    >>> f
    Column<avg(Sales) OVER (PARTITION BY Product ORDER BY Month ASC RANGE BETWEEN 2 PRECEDING AND CURRENT ROW)>
    >>>
    //Apply the sliding window and compute. Examine the results
    >>> monthlySales.select(monthlySales.Product,monthlySales.Sales,monthlySales.Month,
                          func.bround(f,2).alias("MovingAvg")).orderBy(
                          monthlySales.Product,monthlySales.Month).show(6)
    +-------+-----+-----+---------+                                                 
    |Product|Sales|Month|MovingAvg|
    +-------+-----+-----+---------+
    |     P1|   66|    1|     66.0|
    |     P1|   24|    2|     45.0|
    |     P1|   54|    3|     48.0|
    |     P1|    0|    4|     26.0|
    |     P1|   56|    5|    36.67|
    |     P1|   34|    6|     30.0|
    +-------+-----+-----+---------+

底层原理

当开发者使用 RDD API 编写程序时,工作负载的高效执行是开发者的责任。Spark 并不提供数据类型和计算的支持。而当开发者使用数据框架和 Spark SQL 时,底层引擎已经了解模式和操作信息。在这种情况下,开发者可以写更少的代码,同时优化器负责处理所有复杂工作。

Catalyst 优化器包含用于表示树并应用规则进行转换的库。这些树的转换应用于创建最优化的逻辑和物理执行计划。在最终阶段,它使用 Scala 语言的特殊特性 quasiquotes 生成 Java 字节码。优化器还允许外部开发者通过添加特定于数据源的规则来扩展优化器,这些规则会将操作推送到外部系统,或者支持新的数据类型。

Catalyst 优化器会生成最优化的计划来执行当前操作。实际执行和相关改进由 Tungsten 引擎提供。Tungsten 的目标是提高 Spark 后端执行的内存和 CPU 效率。以下是该引擎的一些显著特点:

  • 通过绕过(堆外)Java 内存管理来减少内存占用并消除垃圾回收的开销。

  • 代码生成跨多个操作符融合,并避免了过多的虚拟函数调用。生成的代码看起来像手动优化过的代码。

  • 内存布局采用列式存储的内存中 Parquet 格式,因为这能够支持矢量化处理,且更贴近常见的数据访问操作。

  • 使用编码器进行内存中的编码。编码器通过运行时代码生成构建自定义字节码,实现更快速且紧凑的序列化与反序列化。许多操作可以在内存中就地执行,无需反序列化,因为它们已经是 Tungsten 二进制格式。

结构化流处理

流处理是一个看似广泛的话题!如果深入观察实际问题,企业不仅希望有一个流处理引擎来实时做出决策。一直以来,都有需求将批处理和流处理栈结合,并与外部存储系统和应用程序集成。此外,解决方案应能适应业务逻辑的动态变化,以应对新的和不断变化的业务需求。

Apache Spark 2.0 引入了首个高层次的流处理 API,称为 结构化流处理(Structured Streaming) 引擎。这个可扩展且容错的引擎依赖于 Spark SQL API,简化了实时、连续大数据应用的开发。这可能是统一批处理和流处理计算的首次成功尝试。

从技术角度讲,结构化流处理依赖于 Spark SQL API,该 API 扩展了我们之前讨论的 DataFrames/Datasets。Spark 2.0 让你以统一的方式执行完全不同的活动,例如:

  • 构建机器学习模型并将其应用于流式数据

  • 将流式数据与其他静态数据结合

  • 执行临时查询、交互式查询和批处理查询

  • 运行时改变查询

  • 聚合数据流并通过 Spark SQL JDBC 提供服务

与其他流处理引擎不同,Spark 允许你将实时 流式数据(Streaming Data)静态数据(Static data) 结合,并执行前述操作。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_002.jpg

从本质上讲,结构化流处理(Structured Streaming)得益于 Spark SQL 的 Catalyst 优化器。因此,它让开发者无需担心底层的查询优化问题,能够更高效地处理静态或实时数据流。

截至本文撰写时,Spark 2.0 的结构化流处理专注于 ETL,后续版本将引入更多操作符和库。

让我们看一个简单的例子。以下示例在本地机器上监听 Linux 上的 系统活动报告 (sar) 并计算平均空闲内存。系统活动报告提供系统活动统计信息,当前示例收集内存使用情况,每隔 2 秒报告 20 次。Spark 流读取这个流式输出并计算平均内存。我们使用一个方便的网络工具 netcat (nc) 将 sar 输出重定向到指定的端口。选项 lk 指定 nc 应该监听传入连接,并且即使当前连接完成后,它也必须继续监听另一个连接。

Scala:

示例 6 - 流式示例

//Run the following command from one terminal window 
sar -r 2 20 | nc -lk 9999 

//In spark-shell window, do the following 
//Read stream 
scala> val myStream = spark.readStream.format("socket"). 
                       option("host","localhost"). 
                       option("port",9999).load() 
myStream: org.apache.spark.sql.DataFrame = [value: string] 

//Filter out unwanted lines and then extract free memory part as a float 
//Drop missing values, if any 
scala> val myDF = myStream.filter($"value".contains("IST")). 
               select(substring($"value",15,9).cast("float").as("memFree")). 
               na.drop().select($"memFree") 
myDF: org.apache.spark.sql.DataFrame = [memFree: float] 

//Define an aggregate function 
scala> val avgMemFree = myDF.select(avg("memFree")) 
avgMemFree: org.apache.spark.sql.DataFrame = [avg(memFree): double] 

//Create StreamingQuery handle that writes on to the console 
scala> val query = avgMemFree.writeStream. 
          outputMode("complete"). 
          format("console"). 
          start() 
query: org.apache.spark.sql.streaming.StreamingQuery = Streaming Query - query-0 [state = ACTIVE] 

Batch: 0 
------------------------------------------- 
+-----------------+ 
|     avg(memFree)| 
+-----------------+ 
|4116531.380952381| 
+-----------------+ 
.... 

Python:

    //Run the following command from one terminal window
     sar -r 2 20 | nc -lk 9999

    //In another window, open pyspark shell and do the following
    >>> import pyspark.sql.functions as func
    //Read stream
    >>> myStream = spark.readStream.format("socket"). \
                           option("host","localhost"). \
                           option("port",9999).load()
    myStream: org.apache.spark.sql.DataFrame = [value: string]

    //Filter out unwanted lines and then extract free memory part as a float
    //Drop missing values, if any
    >>> myDF = myStream.filter("value rlike 'IST'"). \
               select(func.substring("value",15,9).cast("float"). \
               alias("memFree")).na.drop().select("memFree")

    //Define an aggregate function
    >>> avgMemFree = myDF.select(func.avg("memFree"))

    //Create StreamingQuery handle that writes on to the console
    >>> query = avgMemFree.writeStream. \
              outputMode("complete"). \
              format("console"). \
              start()
    Batch: 0
    -------------------------------------------
    +------------+
    |avg(memFree)|
    +------------+
    |   4042749.2|
    +------------+
    .....

前面的示例定义了一个连续数据框(也称为流),用于监听特定端口,执行一些转换和聚合操作,并显示连续输出。

Spark 流式编程模型

正如本章前面所展示的,只有一个 API 可以同时处理静态数据和流式数据。其思想是将实时数据流视为一个不断追加的表,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_003.jpg

因此,无论是静态数据还是流式数据,你都可以像对待静态数据表一样启动批处理查询,Spark 会将其作为增量查询在无界输入表上执行,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_004.jpg

因此,开发人员以相同的方式在输入表上定义查询,适用于静态有界表和动态无界表。为了理解它是如何工作的,我们来了解一下这个过程中的各种技术术语:

  • 输入: 作为追加式表格的数据源

  • 触发器: 何时检查输入数据以获取新数据

  • 查询: 对数据执行的操作,如过滤、分组等

  • 结果: 每次触发间隔后的结果表

  • 输出: 每次触发后,选择要写入数据接收器的结果部分

现在让我们看看 Spark SQL 计划器是如何处理整个过程的:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_005.jpg

特别感谢:Databricks

前面的截图在官方 Apache Spark 网站的结构化编程指南中有非常简单的解释,如 参考文献 部分所示。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_006.jpg

在这一点上,我们需要了解支持的输出模型。每次结果表更新时,必须将更改写入外部系统,如 HDFS、S3 或其他数据库。我们通常更倾向于增量写入输出。为此,结构化流提供了三种输出模式:

  • 追加模式: 在外部存储中,只有自上次触发以来追加到结果表的新行会被写入。此模式仅适用于查询中结果表中的现有行不能更改的情况(例如,对输入流的映射)。

  • 完整模式: 在外部存储中,整个更新后的结果表将被完整写入。

  • 更新模式: 在外部存储中,只有自上次触发以来更新过的行会被更改。此模式适用于可以就地更新的输出接收器,例如 MySQL 表。

在我们的示例中,我们使用了完整模式,这直接将结果写入控制台。你可能希望将数据写入某些外部文件(例如 Parquet),以便更好地理解。

幕后机制

如果你查看在 DataFrames/Datasets 上执行操作的“幕后”执行机制,它将呈现如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_007.jpg

请注意,Planner 已知如何将流处理的逻辑计划转换为一系列连续的增量执行计划。这可以通过以下图示表示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_008.jpg

Planner 可以轮询数据源中的新数据,以便能够以优化的方式规划执行。

与其他流处理引擎的比较

我们已经讨论了结构化流处理的许多独特功能。现在让我们与其他流处理引擎做一个比较:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_009.jpg

由此提供:Databricks

连续应用程序

我们讨论了 Spark 如何使统一数据访问成为可能。它让你可以以多种方式处理数据,构建端到端的连续应用程序,通过启用各种分析工作负载,如 ETL 处理、临时查询、在线机器学习建模,或生成必要的报告……这一切都通过高层次的类似 SQL 的 API 实现统一方式,让你同时处理静态和流式数据。通过这种方式,结构化流处理大大简化了实时连续应用程序的开发和维护。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_04_010.jpg

由此提供:Databricks

总结

在本章中,我们讨论了统一数据访问的真正含义以及 Spark 如何实现这一目标。我们详细介绍了 Datasets API 以及如何通过它支持实时流处理。我们学习了 Datasets 的优点,也了解了它们的局限性。我们还研究了连续应用程序背后的基本原理。

在下一章中,我们将探讨如何利用 Spark 平台进行大规模数据分析操作。

参考资料

第五章:Spark 上的数据分析

数据分析领域的规模化发展前所未有。为数据分析开发了各种库和工具,拥有丰富的算法集。同时,分布式计算技术随着时间的推移也在不断发展,以便在大规模处理庞大数据集。这两个特性必须结合起来,这就是 Spark 开发的主要目的。

前两章概述了数据科学的技术方面,涵盖了 DataFrame API、数据集、流数据的基础知识,以及它如何通过 DataFrame 促进数据表示,这是 R 和 Python 用户熟悉的方式。在介绍了这个 API 后,我们看到操作数据集变得比以往更加简单。我们还看到了 Spark SQL 如何通过其强大的功能和优化技术在后台支持 DataFrame API。在本章中,我们将介绍大数据分析的科学方面,并学习可以在 Spark 上执行的各种数据分析技术。

本章的前提是对 DataFrame API 和统计学基础有基本了解。然而,我们已尽力将内容简化,并详细涵盖了一些重要的基础知识,以便任何人都能在 Spark 上开始进行统计分析。本章将涵盖以下主题:

  • 数据分析生命周期

  • 数据采集

  • 数据准备

    • 数据整合

    • 数据清理

    • 数据转换

  • 统计学基础

    • 抽样

    • 数据分布

  • 描述性统计

    • 位置度量

    • 离散度量

    • 汇总统计

    • 图形技术

  • 推断统计

    • 离散概率分布

    • 连续概率分布

    • 标准误差

    • 置信度水平

    • 错误边际和置信区间

    • 人群中的变异性

    • 样本量估算

    • 假设检验

    • 卡方检验

    • F 检验

    • 相关性

数据分析生命周期

对于大多数现实世界的项目,都有一套定义好的步骤顺序需要遵循。然而,数据分析和数据科学并没有普遍认同的定义或边界。通常,“数据分析”一词涵盖了检查数据、发现有用洞察并传达这些洞察的技术和过程。而“数据科学”一词最好被视为一个跨学科的领域,涉及统计学计算机科学数学。这两个术语都涉及对原始数据的处理,以得出知识或洞察,通常是迭代进行的,有些人会交替使用这两个术语。

根据不同的业务需求,解决问题的方式有很多种,但并没有一个适用于所有可能场景的标准流程。一个典型的流程可以概括为:提出问题、探索、假设、验证假设、分析结果,然后重新开始。这一点通过下图中的粗箭头进行了展示。从数据的角度来看,工作流程包括数据获取、预处理、数据探索、建模和结果传达。这些通过图中的圆圈来表示。分析和可视化在每个阶段都会发生,从数据收集到结果传达都贯穿其中。数据分析工作流程涵盖了两个视图中展示的所有活动:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_001.jpg

在整个生命周期中,最重要的事情是当前的问题。接下来是可能包含答案(相关数据!)的数据。根据问题的不同,第一项任务是根据需要从一个或多个数据源收集正确的数据。组织通常维护数据湖,这是存储原始格式数据的庞大库。

下一步是清洗/转换数据到所需的格式。数据清洗也称为数据处理、数据整理或数据挖掘。这包括在评估手头数据质量时进行缺失值处理和异常值处理等活动。你可能还需要对数据进行汇总/绘制图表,以便更好地理解数据。这个制定最终数据矩阵的过程被认为是最耗时的步骤。这也是一个被低估的部分,通常与特征提取和数据转换等其他活动一起被视为预处理的一部分。

数据科学的核心,训练模型和提取模式,紧接其后,这需要大量使用统计学和机器学习。最后一步是发布结果。

本章剩余部分将深入探讨这些步骤,并讲解如何使用 Spark 实现这些步骤。章节中还包括了一些基础的统计知识,以便让读者更轻松地理解代码片段。

数据获取

数据获取,或数据收集,是任何数据科学项目中的第一步。通常,你不会在一个地方找到完整的所需数据,因为数据分布在业务线LOB)应用程序和系统中。

本节的大部分内容已经在上一章中讨论过,上一章概述了如何从不同的数据源获取数据并将其存储在 DataFrame 中,以便于分析。Spark 内置了从一些常见数据源获取数据的机制,对于不支持的源,则提供了数据源 API

为了更好地理解数据采集和准备阶段,让我们假设一个场景,并尝试通过示例代码片段来解决所有涉及的步骤。场景假设员工数据分布在本地 RDD、JSON 文件和 SQL 服务器中。那么,让我们看看如何将这些数据导入到 Spark DataFrame 中:

Python

// From RDD: Create an RDD and convert to DataFrame
>>> employees = sc.parallelize([(1, "John", 25), (2, "Ray", 35), (3, "Mike", 24), (4, "Jane", 28), (5, "Kevin", 26), (6, "Vincent", 35), (7, "James", 38), (8, "Shane", 32), (9, "Larry", 29), (10, "Kimberly", 29), (11, "Alex", 28), (12, "Garry", 25), (13, "Max", 31)]).toDF(["emp_id","name","age"])
>>>

// From JSON: reading a JSON file
>>> salary = sqlContext.read.json("./salary.json")
>>> designation = sqlContext.read.json("./designation.json")

Scala

// From RDD: Create an RDD and convert to DataFrame
scala> val employees = sc.parallelize(List((1, "John", 25), (2, "Ray", 35), (3, "Mike", 24), (4, "Jane", 28), (5, "Kevin", 26), (6, "Vincent", 35), (7, "James", 38), (8, "Shane", 32), (9, "Larry", 29), (10, "Kimberly", 29), (11, "Alex", 28), (12, "Garry", 25), (13, "Max", 31))).toDF("emp_id","name","age")
employees: org.apache.spark.sql.DataFrame = [emp_id: int, name: string ... 1 more field]
scala> // From JSON: reading a JSON file
scala> val salary = spark.read.json("./salary.json")
salary: org.apache.spark.sql.DataFrame = [e_id: bigint, salary: bigint]
scala> val designation = spark.read.json("./designation.json")
designation: org.apache.spark.sql.DataFrame = [id: bigint, role: string]

数据准备

数据质量一直是行业中的一个普遍问题。错误或不一致的数据可能会导致分析结果的误导。如果数据没有按照要求进行清理和准备,实施更好的算法或构建更好的模型也无法取得显著效果。有一个行业术语叫做数据工程,它指的是数据的来源和准备工作。这通常由数据科学家完成,在一些组织中,会有专门的团队负责这项工作。然而,在准备数据时,通常需要一种科学的视角来确保正确处理。例如,处理缺失值时,你可能不仅仅采用均值替代,而是需要查看数据分布,找到更合适的值进行替代。另一个例子是,你可能不仅仅看箱型图或散点图来查找异常值,因为可能存在多变量异常值,这些异常值在单变量图中可能不可见。有不同的方法,比如高斯混合模型GMMs)和期望最大化EM)算法,使用马氏距离来寻找多变量异常值。

数据准备阶段是一个极为重要的阶段,不仅仅是为了让算法正确工作,还能帮助你更好地理解数据,从而在实施算法时采取正确的处理方法。

一旦从不同的数据源获取到数据,下一步就是将它们整合起来,以便可以对整个数据进行清理、格式化和转换,以适应分析所需的格式。请注意,根据场景的不同,您可能需要从数据源中抽取样本,然后准备数据以进行进一步分析。本章后续将讨论可以使用的各种抽样技术。

数据整合

在这一部分,我们将了解如何将来自各种数据源的数据结合起来:

Python

// Creating the final data matrix using the join operation
>>> final_data = employees.join(salary, employees.emp_id == salary.e_id).join(designation, employees.emp_id == designation.id).select("emp_id", "name", "age", "role", "salary")
>>> final_data.show(5)
+------+-----+---+---------+------+
|emp_id| name|age|     role|salary|
+------+-----+---+---------+------+
|     1| John| 25|Associate| 10000|
|     2|  Ray| 35|  Manager| 12000|
|     3| Mike| 24|  Manager| 12000|
|     4| Jane| 28|Associate|  null|
|     5|Kevin| 26|  Manager|   120|
+------+-----+---+---------+------+
only showing top 5 rows

Scala

// Creating the final data matrix using the join operation
scala> val final_data = employees.join(salary, $"emp_id" === $"e_id").join(designation, $"emp_id" === $"id").select("emp_id", "name", "age", "role", "salary")
final_data: org.apache.spark.sql.DataFrame = [emp_id: int, name: string ... 3 more fields]

在将来自这些数据源的数据整合后,最终的数据集(在本例中为final_data)应具有如下格式(示例数据):

emp_idnameagerolesalary
1John25助理10,000 $
2Ray35经理12,000 $
3Mike24经理12,000 $
4Jane28助理null
5Kevin26经理12,000 $
6Vincent35高级经理22,000 $
7James38高级经理20,000 $
8Shane32经理12,000 $
9Larry29经理10,000 $
10Kimberly29助理8,000 $
11Alex28经理12,000 $
12Garry25经理12,000 $
13Max31经理12,000 $

数据清洗

一旦将数据集中到一个地方,非常重要的一点是,在进行分析之前,要花费足够的时间和精力清洗数据。这是一个迭代过程,因为你必须验证你对数据所做的操作,并持续进行直到对数据质量感到满意。建议你花时间分析数据中发现的异常的原因。

数据中通常存在一定程度的杂质。在任何数据集中都可能存在各种问题,但我们将讨论一些常见的情况,如缺失值、重复值、数据转换或格式化(例如,给数字添加或删除数字,拆分一列成两列,或者将两列合并为一列)。

缺失值处理

处理缺失值的方式有很多种。一个方法是删除包含缺失值的行。即使只有某一列有缺失值,我们也可能希望删除这一行,或者对于不同的列采用不同的策略。我们还可以设定一个阈值,只要该行缺失值的总数低于阈值,就保留该行。另一种方法是用常数值替换空值,例如在数值型变量中使用均值替代。

在本节中,我们将提供一些 Scala 和 Python 中的示例,并尝试涵盖各种场景,以帮助你从更广泛的角度理解。

Python

// Dropping rows with missing value(s)
>>> clean_data = final_data.na.drop()
>>> 
// Replacing missing value by mean
>>> import math
>>> from pyspark.sql import functions as F
>>> mean_salary = math.floor(salary.select(F.mean('salary')).collect()[0][0])
>>> clean_data = final_data.na.fill({'salary' : mean_salary})
>>> 
//Another example for missing value treatment
>>> authors = [['Thomas','Hardy','June 2, 1840'],
       ['Charles','Dickens','7 February 1812'],
        ['Mark','Twain',None],
        ['Jane','Austen','16 December 1775'],
      ['Emily',None,None]]
>>> df1 = sc.parallelize(authors).toDF(
       ["FirstName","LastName","Dob"])
>>> df1.show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|  Charles| Dickens| 7 February 1812|
|     Mark|   Twain|            null|
|     Jane|  Austen|16 December 1775|
|    Emily|    null|            null|
+---------+--------+----------------+

// Drop rows with missing values
>>> df1.na.drop().show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|  Charles| Dickens| 7 February 1812|
|     Jane|  Austen|16 December 1775|
+---------+--------+----------------+

// Drop rows with at least 2 missing values
>>> df1.na.drop(thresh=2).show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|  Charles| Dickens| 7 February 1812|
|     Mark|   Twain|            null|
|     Jane|  Austen|16 December 1775|
+---------+--------+----------------+

// Fill all missing values with a given string
>>> df1.na.fill('Unknown').show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|  Charles| Dickens| 7 February 1812|
|     Mark|   Twain|         Unknown|
|     Jane|  Austen|16 December 1775|
|    Emily| Unknown|         Unknown|
+---------+--------+----------------+

// Fill missing values in each column with a given string
>>> df1.na.fill({'LastName':'--','Dob':'Unknown'}).show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|  Charles| Dickens| 7 February 1812|
|     Mark|   Twain|         Unknown|
|     Jane|  Austen|16 December 1775|
|    Emily|      --|         Unknown|
+---------+--------+----------------+

Scala

//Missing value treatment
// Dropping rows with missing value(s)
scala> var clean_data = final_data.na.drop() //Note the var declaration instead of val
clean_data: org.apache.spark.sql.DataFrame = [emp_id: int, name: string ... 3 more fields]
scala>

// Replacing missing value by mean
scal> val mean_salary = final_data.select(floor(avg("salary"))).
            first()(0).toString.toDouble
mean_salary: Double = 20843.0
scal> clean_data = final_data.na.fill(Map("salary" -> mean_salary)) 

//Reassigning clean_data
clean_data: org.apache.spark.sql.DataFrame = [emp_id: int, name: string ... 3 more fields]
scala>

//Another example for missing value treatment
scala> case class Author (FirstName: String, LastName: String, Dob: String)
defined class Author
scala> val authors = Seq(
        Author("Thomas","Hardy","June 2, 1840"),
        Author("Charles","Dickens","7 February 1812"),
        Author("Mark","Twain",null),
        Author("Emily",null,null))
authors: Seq[Author] = List(Author(Thomas,Hardy,June 2, 1840),
   Author(Charles,Dickens,7 February 1812), Author(Mark,Twain,null),
   Author(Emily,null,null))
scala> val ds1 = sc.parallelize(authors).toDS()
ds1: org.apache.spark.sql.Dataset[Author] = [FirstName: string, LastName: string ... 1 more field]
scala> ds1.show()
+---------+--------+---------------+
|FirstName|LastName|            Dob|
+---------+--------+---------------+
|   Thomas|   Hardy|   June 2, 1840|
|  Charles| Dickens|7 February 1812|
|     Mark|   Twain|           null|
|    Emily|    null|           null|
+---------+--------+---------------+
scala>

// Drop rows with missing values
scala> ds1.na.drop().show()
+---------+--------+---------------+
|FirstName|LastName|            Dob|
+---------+--------+---------------+
|   Thomas|   Hardy|   June 2, 1840|
|  Charles| Dickens|7 February 1812|
+---------+--------+---------------+
scala>

//Drop rows with at least 2 missing values
//Note that there is no direct scala function to drop rows with at least n missing values
//However, you can drop rows containing under specified non nulls
//Use that function to achieve the same result
scala> ds1.na.drop(minNonNulls = df1.columns.length - 1).show()
//Fill all missing values with a given string
scala> ds1.na.fill("Unknown").show()
+---------+--------+---------------+
|FirstName|LastName|            Dob|
+---------+--------+---------------+
|   Thomas|   Hardy|   June 2, 1840|
|  Charles| Dickens|7 February 1812|
|     Mark|   Twain|        Unknown|
|    Emily| Unknown|        Unknown|
+---------+--------+---------------+
scala>

//Fill missing values in each column with a given string
scala> ds1.na.fill(Map("LastName"->"--",
                    "Dob"->"Unknown")).show()
+---------+--------+---------------+
|FirstName|LastName|            Dob|
+---------+--------+---------------+
|   Thomas|   Hardy|   June 2, 1840|
|  Charles| Dickens|7 February 1812|
|     Mark|   Twain|        Unknown|
|    Emily|      --|        Unknown|
+---------+--------+---------------+

异常值处理

了解什么是异常值也很重要,这样才能正确处理它。简单来说,异常值是与其余数据点特征不一致的数据点。例如:如果你有一个学生的年龄数据集,而其中有几个年龄值在 30-40 岁范围内,那么它们可能是异常值。现在让我们看一个不同的例子:如果你有一个数据集,其中某个变量的值只能在两个范围内,如 10-20 岁或 80-90 岁,那么介于这两个范围之间的数据点(例如,40 或 55 岁)也可能是异常值。在这个例子中,40 或 55 既不属于 10-20 岁范围,也不属于 80-90 岁范围,因此它们是异常值。

另外,数据中可以存在单变量异常值,也可以存在多变量异常值。为了简化起见,本书将重点讨论单变量异常值,因为在本书写作时,Spark MLlib 可能没有所有需要的算法。

为了处理异常值,你首先需要判断是否存在异常值。可以通过不同的方式来发现异常值,例如使用汇总统计和绘图技术。你可以使用 Python 的内置库函数,如matplotlib,来可视化数据。通过连接到 Spark(例如,通过 Jupyter 笔记本),你可以生成这些可视化,这在命令行中可能是无法实现的。

一旦发现异常值,你可以选择删除包含异常值的行,或者用均值替换异常值,或者根据具体情况做出更相关的处理。让我们来看看均值替代方法:

Python

// Identify outliers and replace them with mean
//The following example reuses the clean_data dataset and mean_salary computed in previous examples
>>> mean_salary
20843.0
>>> 
//Compute deviation for each row
>>> devs = final_data.select(((final_data.salary - mean_salary) ** 2).alias("deviation"))

//Compute standard deviation
>>> stddev = math.floor(math.sqrt(devs.groupBy().
          avg("deviation").first()[0]))

//check standard deviation value
>>> round(stddev,2)
30351.0
>>> 
//Replace outliers beyond 2 standard deviations with the mean salary
>>> no_outlier = final_data.select(final_data.emp_id, final_data.name, final_data.age, final_data.salary, final_data.role, F.when(final_data.salary.between(mean_salary-(2*stddev), mean_salary+(2*stddev)), final_data.salary).otherwise(mean_salary).alias("updated_salary"))
>>> 
//Observe modified values
>>> no_outlier.filter(no_outlier.salary != no_outlier.updated_salary).show()
+------+----+---+------+-------+--------------+
|emp_id|name|age|salary|   role|updated_salary|
+------+----+---+------+-------+--------------+
|    13| Max| 31|120000|Manager|       20843.0|
+------+----+---+------+-------+--------------+
>>>

Scala

// Identify outliers and replace them with mean
//The following example reuses the clean_data dataset and mean_salary computed in previous examples
//Compute deviation for each row
scala> val devs = clean_data.select(((clean_data("salary") - mean_salary) *
        (clean_data("salary") - mean_salary)).alias("deviation"))
devs: org.apache.spark.sql.DataFrame = [deviation: double]

//Compute standard deviation
scala> val stddev = devs.select(sqrt(avg("deviation"))).
            first().getDouble(0)
stddev: Double = 29160.932595617614

//If you want to round the stddev value, use BigDecimal as shown
scala> scala.math.BigDecimal(stddev).setScale(2,
             BigDecimal.RoundingMode.HALF_UP)
res14: scala.math.BigDecimal = 29160.93
scala>

//Replace outliers beyond 2 standard deviations with the mean salary
scala> val outlierfunc = udf((value: Long, mean: Double) => {if (value > mean+(2*stddev)
            || value < mean-(2*stddev)) mean else value})

//Use the UDF to compute updated_salary
//Note the usage of lit() to wrap a literal as a column
scala> val no_outlier = clean_data.withColumn("updated_salary",
            outlierfunc(col("salary"),lit(mean_salary)))

//Observe modified values
scala> no_outlier.filter(no_outlier("salary") =!=  //Not !=
             no_outlier("updated_salary")).show()
+------+----+---+-------+------+--------------+
|emp_id|name|age|   role|salary|updated_salary|
+------+----+---+-------+------+--------------+
|    13| Max| 31|Manager|120000|       20843.0|
+------+----+---+-------+------+--------------+

处理重复值

处理数据集中重复记录的方式有多种。我们将在以下代码片段中演示这些方法:

Python

// Deleting the duplicate rows
>>> authors = [['Thomas','Hardy','June 2,1840'],
    ['Thomas','Hardy','June 2,1840'],
    ['Thomas','H',None],
    ['Jane','Austen','16 December 1775'],
    ['Emily',None,None]]
>>> df1 = sc.parallelize(authors).toDF(
      ["FirstName","LastName","Dob"])
>>> df1.show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|   Thomas|   Hardy|    June 2, 1840|
|   Thomas|       H|            null|
|     Jane|  Austen|16 December 1775|
|    Emily|    null|            null|
+---------+--------+----------------+

// Drop duplicated rows
>>> df1.dropDuplicates().show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|    Emily|    null|            null|
|     Jane|  Austen|16 December 1775|
|   Thomas|       H|            null|
|   Thomas|   Hardy|    June 2, 1840|
+---------+--------+----------------+

// Drop duplicates based on a sub set of columns
>>> df1.dropDuplicates(subset=["FirstName"]).show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|    Emily|    null|            null|
|   Thomas|   Hardy|    June 2, 1840|
|     Jane|  Austen|16 December 1775|
+---------+--------+----------------+
>>> 

Scala:

//Duplicate values treatment
// Reusing the Author case class
// Deleting the duplicate rows
scala> val authors = Seq(
            Author("Thomas","Hardy","June 2,1840"),
            Author("Thomas","Hardy","June 2,1840"),
            Author("Thomas","H",null),
            Author("Jane","Austen","16 December 1775"),
            Author("Emily",null,null))
authors: Seq[Author] = List(Author(Thomas,Hardy,June 2,1840), Author(Thomas,Hardy,June 2,1840), Author(Thomas,H,null), Author(Jane,Austen,16 December 1775), Author(Emily,null,null))
scala> val ds1 = sc.parallelize(authors).toDS()
ds1: org.apache.spark.sql.Dataset[Author] = [FirstName: string, LastName: string ... 1 more field]
scala> ds1.show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|     June 2,1840|
|   Thomas|   Hardy|     June 2,1840|
|   Thomas|       H|            null|
|     Jane|  Austen|16 December 1775|
|    Emily|    null|            null|
+---------+--------+----------------+
scala>

// Drop duplicated rows
scala> ds1.dropDuplicates().show()
+---------+--------+----------------+                                          
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|     Jane|  Austen|16 December 1775|
|    Emily|    null|            null|
|   Thomas|   Hardy|     June 2,1840|
|   Thomas|       H|            null|
+---------+--------+----------------+
scala>

// Drop duplicates based on a sub set of columns
scala> ds1.dropDuplicates("FirstName").show()
+---------+--------+----------------+                                           
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|    Emily|    null|            null|
|     Jane|  Austen|16 December 1775|
|   Thomas|   Hardy|     June 2,1840|
+---------+--------+----------------+

数据转换

数据转换的需求可能有多种,而且每个案例大多是独特的。我们将覆盖以下一些基本的转换类型:

  • 将两列合并为一列

  • 向现有数据添加字符/数字

  • 删除或替换现有字符/数字

  • 更改日期格式

Python

// Merging columns
//Create a udf to concatenate two column values
>>> import pyspark.sql.functions
>>> concat_func = pyspark.sql.functions.udf(lambda name, age: name + "_" + str(age))

//Apply the udf to create merged column
>>> concat_df = final_data.withColumn("name_age", concat_func(final_data.name, final_data.age))
>>> concat_df.show(4)
+------+----+---+---------+------+--------+
|emp_id|name|age|     role|salary|name_age|
+------+----+---+---------+------+--------+
|     1|John| 25|Associate| 10000| John_25|
|     2| Ray| 35|  Manager| 12000|  Ray_35|
|     3|Mike| 24|  Manager| 12000| Mike_24|
|     4|Jane| 28|Associate|  null| Jane_28|
+------+----+---+---------+------+--------+
only showing top 4 rows
// Adding constant to data
>>> data_new = concat_df.withColumn("age_incremented",concat_df.age + 10)
>>> data_new.show(4)
+------+----+---+---------+------+--------+---------------+
|emp_id|name|age|     role|salary|name_age|age_incremented|
+------+----+---+---------+------+--------+---------------+
|     1|John| 25|Associate| 10000| John_25|             35|
|     2| Ray| 35|  Manager| 12000|  Ray_35|             45|
|     3|Mike| 24|  Manager| 12000| Mike_24|             34|
|     4|Jane| 28|Associate|  null| Jane_28|             38|
+------+----+---+---------+------+--------+---------------+
only showing top 4 rows
>>> 

//Replace values in a column
>>> df1.replace('Emily','Charlotte','FirstName').show()
+---------+--------+----------------+
|FirstName|LastName|             Dob|
+---------+--------+----------------+
|   Thomas|   Hardy|    June 2, 1840|
|  Charles| Dickens| 7 February 1812|
|     Mark|   Twain|            null|
|     Jane|  Austen|16 December 1775|
|Charlotte|    null|            null|
+---------+--------+----------------+

// If the column name argument is omitted in replace, then replacement is applicable to all columns
//Append new columns based on existing values in a column
//Give 'LastName' instead of 'Initial' if you want to overwrite
>>> df1.withColumn('Initial',df1.LastName.substr(1,1)).show()
+---------+--------+----------------+-------+
|FirstName|LastName|             Dob|Initial|
+---------+--------+----------------+-------+
|   Thomas|   Hardy|    June 2, 1840|      H|
|  Charles| Dickens| 7 February 1812|      D|
|     Mark|   Twain|            null|      T|
|     Jane|  Austen|16 December 1775|      A|
|    Emily|    null|            null|   null|
+---------+--------+----------------+-------+

Scala:

// Merging columns
//Create a udf to concatenate two column values
scala> val concatfunc = udf((name: String, age: Integer) =>
                           {name + "_" + age})
concatfunc: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function2>,StringType,Some(List(StringType, IntegerType)))
scala>

//Apply the udf to create merged column
scala> val concat_df = final_data.withColumn("name_age",
                         concatfunc($"name", $"age"))
concat_df: org.apache.spark.sql.DataFrame =
         [emp_id: int, name: string ... 4 more fields]
scala> concat_df.show(4)
+------+----+---+---------+------+--------+
|emp_id|name|age|     role|salary|name_age|
+------+----+---+---------+------+--------+
|     1|John| 25|Associate| 10000| John_25|
|     2| Ray| 35|  Manager| 12000|  Ray_35|
|     3|Mike| 24|  Manager| 12000| Mike_24|
|     4|Jane| 28|Associate|  null| Jane_28|
+------+----+---+---------+------+--------+
only showing top 4 rows
scala>

// Adding constant to data
scala> val addconst = udf((age: Integer) => {age + 10})
addconst: org.apache.spark.sql.expressions.UserDefinedFunction =
      UserDefinedFunction(<function1>,IntegerType,Some(List(IntegerType)))
scala> val data_new = concat_df.withColumn("age_incremented",
                 addconst(col("age")))
data_new: org.apache.spark.sql.DataFrame =
     [emp_id: int, name: string ... 5 more fields]
scala> data_new.show(4)
+------+----+---+---------+------+--------+---------------+
|emp_id|name|age|     role|salary|name_age|age_incremented|
+------+----+---+---------+------+--------+---------------+
|     1|John| 25|Associate| 10000| John_25|             35|
|     2| Ray| 35|  Manager| 12000|  Ray_35|             45|
|     3|Mike| 24|  Manager| 12000| Mike_24|             34|
|     4|Jane| 28|Associate|  null| Jane_28|             38|
+------+----+---+---------+------+--------+---------------+
only showing top 4 rows

// Replace values in a column
//Note: As of Spark 2.0.0, there is no replace on DataFrame/ Dataset does not work so .na. is a work around
scala> ds1.na.replace("FirstName",Map("Emily" -> "Charlotte")).show()
+---------+--------+---------------+
|FirstName|LastName|            Dob|
+---------+--------+---------------+
|   Thomas|   Hardy|   June 2, 1840|
|  Charles| Dickens|7 February 1812|
|     Mark|   Twain|           null|
|Charlotte|    null|           null|
+---------+--------+---------------+
scala>

// If the column name argument is "*" in replace, then replacement is applicable to all columns
//Append new columns based on existing values in a column
//Give "LastName" instead of "Initial" if you want to overwrite
scala> ds1.withColumn("Initial",ds1("LastName").substr(1,1)).show()
+---------+--------+---------------+-------+
|FirstName|LastName|            Dob|Initial|
+---------+--------+---------------+-------+
|   Thomas|   Hardy|   June 2, 1840|      H|
|  Charles| Dickens|7 February 1812|      D|
|     Mark|   Twain|           null|      T|
|    Emily|    null|           null|   null|
+---------+--------+---------------+-------+

现在我们已经了解了基本的示例,让我们来处理一个稍微复杂一点的例子。你可能已经注意到,作者数据中的日期列有不同的日期格式。在某些情况下,月份在前,日期在后;而在其他情况下则相反。这种异常在现实世界中很常见,因为数据可能来自不同的来源。在这里,我们看到的情况是日期列中有多个不同的日期格式数据点。我们需要将所有不同的日期格式标准化为一个统一的格式。为此,我们首先需要创建一个用户定义函数udf),它可以处理不同的格式并将它们转换为统一的格式。

// Date conversions
//Create udf for date conversion that converts incoming string to YYYY-MM-DD format
// The function assumes month is full month name and year is always 4 digits
// Separator is always a space or comma
// Month, date and year may come in any order
//Reusing authors data
>>> authors = [['Thomas','Hardy','June 2, 1840'],
        ['Charles','Dickens','7 February 1812'],
        ['Mark','Twain',None],
        ['Jane','Austen','16 December 1775'],
        ['Emily',None,None]]
>>> df1 = sc.parallelize(authors).toDF(
      ["FirstName","LastName","Dob"])
>>> 

// Define udf
//Note: You may create this in a script file and execute with execfile(filename.py)
>>> def toDate(s):
 import re
 year = month = day = ""
 if not s:
  return None
 mn = [0,'January','February','March','April','May',
  'June','July','August','September',
  'October','November','December']

 //Split the string and remove empty tokens
 l = [tok for tok in re.split(",| ",s) if tok]

//Assign token to year, month or day
 for a in l:
  if a in mn:
   month = "{:0>2d}".format(mn.index(a))
  elif len(a) == 4:
   year = a
  elif len(a) == 1:
   day = '0' + a
  else:
   day = a
 return year + '-' + month + '-' + day
>>> 

//Register the udf
>>> from pyspark.sql.functions import udf
>>> from pyspark.sql.types import StringType
>>> toDateUDF = udf(toDate, StringType())

//Apply udf
>>> df1.withColumn("Dob",toDateUDF("Dob")).show()
+---------+--------+----------+
|FirstName|LastName|       Dob|
+---------+--------+----------+
|   Thomas|   Hardy|1840-06-02|
|  Charles| Dickens|1812-02-07|
|     Mark|   Twain|      null|
|     Jane|  Austen|1775-12-16|
|    Emily|    null|      null|
+---------+--------+----------+
>>> 

Scala

//Date conversions
//Create udf for date conversion that converts incoming string to YYYY-MM-DD format
// The function assumes month is full month name and year is always 4 digits
// Separator is always a space or comma
// Month, date and year may come in any order
//Reusing authors case class and data
>>> val authors = Seq(
        Author("Thomas","Hardy","June 2, 1840"),
        Author("Charles","Dickens","7 February 1812"),
        Author("Mark","Twain",null),
        Author("Jane","Austen","16 December 1775"),
        Author("Emily",null,null))
authors: Seq[Author] = List(Author(Thomas,Hardy,June 2, 1840), Author(Charles,Dickens,7 February 1812), Author(Mark,Twain,null), Author(Jane,Austen,16 December 1775), Author(Emily,null,null))
scala> val ds1 = sc.parallelize(authors).toDS()
ds1: org.apache.spark.sql.Dataset[Author] = [FirstName: string, LastName: string ... 1 more field]
scala>

// Define udf
//Note: You can type :paste on REPL to paste  multiline code. CTRL + D signals end of paste mode
def toDateUDF = udf((s: String) => {
    var (year, month, day) = ("","","")
    val mn = List("","January","February","March","April","May",
        "June","July","August","September",
        "October","November","December")
    //Tokenize the date string and remove trailing comma, if any
    if(s != null) {
      for (x <- s.split(" ")) {
        val token = x.stripSuffix(",")
        token match {
        case "" =>
        case x if (mn.contains(token)) =>
            month = "%02d".format(mn.indexOf(token))
        case x if (token.length() == 4) =>
            year = token
        case x =>
            day = token
        }
     }   //End of token processing for
     year + "-" + month + "-" + day=
   } else {
       null
   }
})
toDateUDF: org.apache.spark.sql.expressions.UserDefinedFunction
scala>

//Apply udf and convert date strings to standard form YYYY-MM-DD
scala> ds1.withColumn("Dob",toDateUDF(ds1("Dob"))).show()
+---------+--------+----------+
|FirstName|LastName|       Dob|
+---------+--------+----------+
|   Thomas|   Hardy| 1840-06-2|
|  Charles| Dickens| 1812-02-7|
|     Mark|   Twain|      null|
|     Jane|  Austen|1775-12-16|
|    Emily|    null|      null|
+---------+--------+----------+

这会整齐地排列出生日期字符串。当我们遇到更多不同的日期格式时,可以继续调整这个用户定义函数(udf)。

在这一阶段,在开始进行数据分析之前,非常重要的一点是你应该暂停片刻,重新评估从数据采集到数据清理和转换过程中所采取的所有步骤。有很多情况下,涉及大量时间和精力的工作最后失败,因为分析和建模的数据是不正确的。这些情况成了著名计算机谚语的完美例证——垃圾进,垃圾出GIGO)。

统计学基础

统计学领域主要是通过数学方法将数据集中的原始事实和数字以某种有意义的方式进行总结,使其对你有意义。这包括但不限于:收集数据、分析数据、解释数据和展示数据。

统计学的存在主要是因为通常无法收集整个总体的数据。因此,通过使用统计技术,我们利用样本统计量来估计总体参数,并应对不确定性。

在这一部分,我们将介绍一些基础的统计学和分析技术,在这些基础上,我们将逐步建立对本书中所涵盖概念的全面理解。

统计学的研究可以大致分为两个主要分支:

  • 描述性统计

  • 推断统计

以下图表展示了这两个术语,并说明了我们如何从样本中估计总体参数:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_002.jpg

在开始之前,了解抽样和分布是很重要的。

抽样

通过抽样技术,我们只需从总体数据集中取出一部分并进行处理:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_003.jpg

但为什么我们要进行抽样?以下是进行抽样的各种原因:

  • 很难获得整个总体的数据;例如,某个国家公民的身高。

  • 处理整个数据集是很困难的。当我们谈论大数据计算平台(如 Spark)时,这个挑战几乎消失了。然而,也有可能遇到需要将手头的整个数据视为样本,并将分析结果推断到未来时间或更大总体的情况。

  • 难以绘制大量数据以便可视化。技术上可能存在限制。

  • 为了验证你的分析或验证预测模型——特别是在处理小数据集时,你需要依赖交叉验证。

为了有效的抽样,有两个重要的约束:一是确定样本大小,二是选择抽样技术。样本大小对总体参数的估计有很大的影响。我们将在本章后面讨论这一方面,首先会介绍一些先决的基础知识。在这一节中,我们将重点讨论抽样技术。

有多种基于概率(每个样本被选择的概率已知)和非概率(每个样本被选择的概率未知)抽样技术可供选择,但我们将仅讨论基于概率的技术。

简单随机抽样

简单随机抽样SRS)是最基本的概率抽样方法,其中每个元素被选择的概率相同。这意味着每个可能的n元素样本都有相等的选择机会。

系统抽样

系统抽样可能是所有基于概率的抽样技术中最简单的一种,在这种方法中,总体的每个第 k个元素都会被抽取。因此,这也叫做间隔抽样。它从一个随机选择的固定起点开始,然后估算一个间隔(即第 k个元素,其中k = (总体大小)/(样本大小))。这里,元素的选择会循环进行,直到达到样本大小,即从开始重新选择直到结束。

分层抽样

当种群内的子群体或子群体差异较大时,这种抽样技术是首选,因为其他抽样技术可能无法提取出一个能够良好代表整个种群的样本。通过分层抽样,将种群划分为同质的子群体,称为,并通过按照种群比例从这些层中随机选择样本来进行抽样。因此,样本中的层大小与种群大小的比例保持一致:

Python

/* ”Sample” function is defined for DataFrames (not RDDs) which takes three parameters:
withReplacement - Sample with replacement or not (input: True/False)
fraction - Fraction of rows to generate (input: any number between 0 and 1 as per your requirement of sample size)
seed - Seed for sampling (input: Any random seed)
*/
>>> sample1 = data_new.sample(False, 0.6) //With random seed as no seed value specified
>>> sample2 = data_new.sample(False, 0.6, 10000) //With specific seed value of 10000

Scala:

scala> val sample1 = data_new.sample(false, 0.6) //With random seed as no seed value specified
sample1: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [emp_id: int, name: string ... 5 more fields]
scala> val sample2 = data_new.sample(false, 0.6, 10000) //With specific seed value of 10000
sample2: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [emp_id: int, name: string ... 5 more fields]

注意

我们只看了 DataFrame 上的抽样;在 MLlib 库中也有如sampleByKeysampleByKeyExact等函数,可以在键值对的 RDD 上进行分层抽样。可以查看spark.util.random包,了解 Bernoulli、Poisson 或随机抽样器。

数据分布

了解数据的分布情况是将数据转化为信息的主要任务之一。分析变量的分布有助于发现异常值、可视化数据的趋势,并且有助于你对数据的理解。这有助于正确思考并采取正确的方式解决业务问题。绘制分布图使得直观性更强,我们将在描述性统计部分讨论这一方面。

频率分布

频率分布解释了一个变量可以取哪些值,以及它取这些值的频率。通常用一个表格来表示每个可能值及其对应的出现次数。

假设我们掷一颗六面骰子 100 次,并观察到以下频率:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Chapter-5_NEw.jpg

频率表

同样地,你可能会在每一组 100 次掷骰子中观察到不同的分布,因为这将取决于运气。

有时,你可能对出现次数的比例感兴趣,而不仅仅是出现的次数。在前面的掷骰子例子中,我们总共掷了 100 次骰子,因此比例分布或相对频率分布将呈现如下:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Final-5-RT-3.jpg

相对频率表

概率分布

在同一个掷骰子的例子中,我们知道一个总概率为 1 的值是分布在骰子的所有面上的。这意味着每一面(从 1 到 6)上都与概率 1/6(约为 0.167)相关联。无论你掷骰子的次数是多少(假设是公平的骰子!),1/6 的概率都会均匀地分布在骰子的所有面上。所以,如果你绘制这个分布图,它将呈现如下:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Chapter-new.jpg

概率分布

我们在这里查看了三种分布——频率分布、相对频率分布和概率分布。

这个概率分布实际上是总体的分布。在实际情况中,有时我们已经知道总体分布(在我们的例子中,它是公平骰子六个面的概率为 0.167),而有时我们不知道。在没有总体分布的情况下,找出总体分布本身就成为推断统计的一部分。而且,与公平骰子的例子不同,所有面上的概率相同,在其他情况下,变量可能的值可以与不同的概率相关联,并且这些值也可以遵循某种特定类型的分布。

现在是揭开秘密的时候了!相对频率分布与概率分布之间的关系是统计推断的基础。相对频率分布也叫做经验分布,它是基于我们在样本中观察到的内容(这里是 100 次的样本)。如前所述,每 100 次掷骰子的经验分布会根据机会而有所不同。现在,掷骰子的次数越多,相对频率分布与概率分布的差异就越小。因此,无限次掷骰子的相对频率分布即为概率分布,而概率分布反过来就是总体分布。

概率分布有多种类型,这些分布根据变量类型(分类变量或连续变量)再次分类。我们将在本章的后续部分详细讨论这些分布。不过,我们应该了解这些分类所代表的含义!分类变量只能取几个类别。例如,及格/不及格、零/一、癌症/恶性是具有两个类别的分类变量的例子。同样,分类变量也可以有更多的类别,比如红/绿/蓝、类型 1/类型 2/类型 3/类型 4,等等。连续变量可以在给定的范围内取任意值,并且是以连续的尺度进行度量的,例如年龄、身高、工资等。理论上,在连续变量的任意两个值之间可以有无限多个可能值。例如,在身高范围 5’6"到 6’4"(英尺和英寸尺度)之间,可能有许多分数值。同样,在厘米尺度下也是如此。

描述性统计

在前一节中,我们学习了分布是如何形成的。在本节中,我们将学习如何通过描述性统计来描述这些分布。描述分布的两个重要组成部分是其位置和分布的扩展。

位置度量

位置度量是一个描述数据中心位置的单一值。三种最常见的定位度量是均值、中位数和众数。

均值

到目前为止,最常见且广泛使用的集中趋势度量是均值,也称为平均数。无论是样本还是总体,均值或平均数是所有元素的总和除以元素的总数。

中位数

中位数是当数据按任意顺序排序时位于中间的数值,使得一半数据大于中位数,另一半小于中位数。当有两个中位数值(数据项数目为偶数)时,中位数是这两个中间值的平均值。中位数是处理数据集中的异常值(极端值)时更好的位置度量。

众数

众数是出现频率最高的数据项。它可以用于定性数据和定量数据的确定。

Python

//重用在重复值处理中创建的 data_new

>>> mean_age = data_new.agg({'age': 'mean'}).first()[0]
>>> age_counts = data_new.groupBy("age").agg({"age": "count"}).alias("freq")
>>> mode_age = age_counts.sort(age_counts["COUNT(age)"].desc(), age_counts.age.asc()).first()[0]
>>> print(mean_age, mode_age)
(29.615384615384617, 25)
>>> age_counts.sort("count(age)",ascending=False).show(2)
+---+----------+                                                               
|age|count(age)|
+---+----------+
| 28|         3|
| 29|         2|
+---+----------+
only showing top 2 rows

Scala

//Reusing data_new created 
scala> val mean_age = data_new.select(floor(avg("age"))).first().getLong(0)
mean_age: Long = 29
scala> val mode_age = data_new.groupBy($"age").agg(count($"age")).
                 sort($"count(age)".desc, $"age").first().getInt(0)
mode_age: Int = 28
scala> val age_counts = data_new.groupBy("age").agg(count($"age") as "freq")
age_counts: org.apache.spark.sql.DataFrame = [age: int, freq: bigint]
scala> age_counts.sort($"freq".desc).show(2)
+---+----+                                                                     
|age|freq|
+---+----+
| 35|   2|
| 28|   2|
+---+----+

离散度度量

离散度度量描述了某一特定变量或数据项的数据的集中程度或分散程度。

范围

范围是变量最小值和最大值之间的差异。它的一个缺点是没有考虑到数据中的每一个值。

方差

为了找出数据集中的变异性,我们可以将每个值与均值相减,平方它们以去除负号(同时放大数值),然后将所有结果相加,并除以总的值个数:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_007.jpg

如果数据分布较广,方差会是一个较大的数字。它的一个缺点是对异常值赋予了过多的权重。

标准差

与方差类似,标准差也是衡量数据离散程度的指标。方差的一个局限性是数据的单位也被平方,因此很难将方差与数据集中的数值相关联。因此,标准差是通过方差的平方根计算出来的:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_008.jpg

Python

//Reusing data_new created before
import math
>>> range_salary = data_new.agg({'salary': 'max'}).first()[0] - data_new.agg({'salary': 'min'}).first()[0]
>>> mean_salary = data_new.agg({'salary': 'mean'}).first()[0]
>>> salary_deviations = data_new.select(((data_new.salary - mean_salary) *
       (data_new.salary - mean_salary)).alias("deviation"))
>>> stddev_salary = math.sqrt(salary_deviations.agg({'deviation' : 
'avg'}).first()[0])
>>> variance_salary = salary_deviations.groupBy().avg("deviation").first()[0]
>>> print(round(range_salary,2), round(mean_salary,2),
      round(variance_salary,2), round(stddev_salary,2))
(119880.0, 20843.33, 921223322.22, 30351.66)
>>> 

Scala

//Reusing data_new created before
scala> val range_salary = data_new.select(max("salary")).first().
          getLong(0) - data_new.select(min("salary")).first().getLong(0)
range_salary: Long = 119880
scala> val mean_salary = data_new.select(floor(avg("salary"))).first().getLong(0)
mean_salary: Long = 20843
scala> val salary_deviations = data_new.select(((data_new("salary") - mean_salary)
                     * (data_new("salary") - mean_salary)).alias("deviation"))
salary_deviations: org.apache.spark.sql.DataFrame = [deviation: bigint]
scala> val variance_salary = { salary_deviations.select(avg("deviation"))
                                       .first().getDouble(0) }
variance_salary: Double = 9.212233223333334E8
scala> val stddev_salary = { salary_deviations
                    .select(sqrt(avg("deviation")))
                    .first().getDouble(0) }
stddev_salary: Double = 30351.660948510435

汇总统计

数据集的汇总统计信息是非常有用的信息,可以帮助我们快速了解当前的数据。通过使用统计学中提供的colStats函数,我们可以获得RDD[Vector]的多变量统计汇总,其中包含按列计算的最大值、最小值、均值、方差、非零值的数量和总计数。让我们通过一些代码示例来探索这个:

Python

>>> import numpy
>>> from pyspark.mllib.stat import Statistics
// Create an RDD of number vectors
//This example creates an RDD with 5 rows with 5 elements each
>>> observations = sc.parallelize(numpy.random.random_integers(0,100,(5,5)))
// Compute column summary statistics.
//Note that the results may vary because of random numbers
>>> summary = Statistics.colStats(observations)
>>> print(summary.mean())       // mean value for each column
>>> print(summary.variance())  // column-wise variance
>>> print(summary.numNonzeros())// number of nonzeros in each column

Scala

scala> import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.stat.{
          MultivariateStatisticalSummary, Statistics}
import org.apache.spark.mllib.stat.{MultivariateStatisticalSummary, Statistics}
// Create an RDD of number vectors
//This example creates an RDD with 5 rows with 5 elements each
scala> val observations = sc.parallelize(Seq.fill(5)(Vectors.dense(Array.fill(5)(
                    scala.util.Random.nextDouble))))
observations: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = ParallelCollectionRDD[43] at parallelize at <console>:27
scala>
// Compute column summary statistics.
//Note that the results may vary because of random numbers
scala> val summary = Statistics.colStats(observations)
summary: org.apache.spark.mllib.stat.MultivariateStatisticalSummary = org.apache.spark.mllib.stat.MultivariateOnlineSummarizer@36836161
scala> println(summary.mean)  // mean value for each column
[0.5782406967737089,0.5903954680966121,0.4892908815930067,0.45680701799234835,0.6611492334819364]
scala> println(summary.variance)    // column-wise variance
[0.11893608153330748,0.07673977181967367,0.023169197889513014,0.08882605965192601,0.08360159585590332]
scala> println(summary.numNonzeros) // number of nonzeros in each column
[5.0,5.0,5.0,5.0,5.0]

提示

Apache Spark MLlib 基于 RDD 的 API 从 Spark 2.0 开始进入维护模式。预计在 2.2+ 中将被弃用,并在 Spark 3.0 中移除。

图形化技术

要了解数据点的行为,您可能需要绘制它们并查看。但是,您需要一个平台来将您的数据可视化,例如箱线图散点图直方图等。 iPython/Jupyter 笔记本或任何由 Spark 支持的第三方笔记本都可以在浏览器中用于数据可视化。 Databricks 提供他们自己的笔记本。可视化在自己的章节中进行讨论,本章专注于完整的生命周期。但是,Spark 提供了直方图数据准备,使得桶范围和频率可以与客户端机器传输,而不是完整数据集。以下示例显示了相同。

Python

//Histogram
>>>from random import randint
>>> numRDD = sc.parallelize([randint(0,9) for x in xrange(1,1001)])
// Generate histogram data for given bucket count
>>> numRDD.histogram(5)
([0.0, 1.8, 3.6, 5.4, 7.2, 9], [202, 213, 215, 188, 182])
//Alternatively, specify ranges
>>> numRDD.histogram([0,3,6,10])
([0, 3, 6, 10], [319, 311, 370])

Scala:

//Histogram
scala> val numRDD = sc.parallelize(Seq.fill(1000)(
                    scala.util.Random.nextInt(10)))
numRDD: org.apache.spark.rdd.RDD[Int] =
     ParallelCollectionRDD[0] at parallelize at <console>:24
// Generate histogram data for given bucket count
scala> numRDD.histogram(5)
res10: (Array[Double], Array[Long]) = (Array(0.0, 1.8, 3.6, 5.4, 7.2, 9.0),Array(194, 209, 215, 195, 187))
scala>
//Alternatively, specify ranges
scala> numRDD.histogram(Array(0,3.0,6,10))
res13: Array[Long] = Array(293, 325, 382)

推断统计学

我们看到描述统计学在描述和展示数据方面非常有用,但它们并没有提供一种使用样本统计量来推断总体参数或验证我们可能提出的任何假设的方法。因此,推断统计学的技术出现以满足这些需求。推断统计学的一些重要用途包括:

  • 估计总体参数

  • 假设检验

请注意,样本永远无法完美地代表总体,因为每次抽样都会自然产生抽样误差,因此需要推断统计学!让我们花些时间了解可以帮助推断总体参数的各种概率分布类型。

离散概率分布

离散概率分布用于建模本质上是离散的数据,这意味着数据只能取特定的值,如整数。与分类变量不同,离散变量可以仅取数值数据,特别是一组不同整数值的计数数据。此外,随机变量所有可能值的概率之和为一。离散概率分布以概率质量函数描述。可以有各种类型的离散概率分布。以下是一些示例。

伯努利分布

伯努利分布是一种描述只有两种可能结果的试验的分布,例如成功/失败,正面/反面,六面骰子的面值为 4 或不为 4,发送的消息是否被接收等。伯努利分布可以推广为任何具有两个或更多可能结果的分类变量。

让我们以“学生通过考试的概率”为例,其中 0.6(60%)是学生通过考试的概率P,而 0.4(40%)是学生未能通过考试的概率(1-P)。让我们将失败表示为0,通过表示为1

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_011.jpg

这样的分布无法回答诸如学生的预期通过率的问题,因为预期值(μ)将是一个分数,而该分布不能取这个值。它只能意味着,如果你对 1000 名学生进行抽样,那么 600 名会通过,400 名会失败。

二项分布

该分布可以描述一系列伯努利试验(每个试验只有两种可能结果)。此外,它假设一个试验的结果不会影响后续的试验,并且每次试验中事件发生的概率是相同的。一个二项分布的例子是抛掷硬币五次。在这里,第一个投掷的结果不会影响第二次投掷的结果,每次投掷的概率是相同的。

如果n是试验次数,p是每次试验中成功的概率,那么该二项分布的均值(μ)可以表示为:

μ = n * p

方差(σ2x)可以表示为:

σ2x = np*(1-p)。*

一般来说,遵循二项分布的随机变量X,其参数为np,可以表示为X ~ B(n, p)。对于这样的分布,获得在n次试验中恰好* k *次成功的概率可以通过概率质量函数来描述,如下所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_012.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_013.jpg

这里,k = 0, 1, 2, …, n

示例问题

假设一个假设场景。假设某城市的 24%公司宣布将向受到海啸影响的地区提供支持,作为其社会责任活动的一部分。在一个随机选择的 20 家公司样本中,求出有多少公司宣布将帮助海啸受灾地区的概率:

  • 恰好三次

  • 少于三次

  • 三次或更多

解答

样本大小 = n = 20。

随机选择的公司宣布将提供帮助的概率 = P = 0.24

a) P(x = 3) = ²⁰C[3] (0.24)³ (0.76) ¹⁷ = 0.15

b) P(x < 3) = P(0) + P(1) + P(2)

= (0.76) ²⁰ + ²⁰C[1] (0.24) (0.76)¹⁹ + ²⁰C[2] (0.24)² (0.76)¹⁸

= 0.0041 + 0.0261 + 0.0783 = 0.11

c) P(x >= 3) = 1 - P(x <= 2) = 1 - 0.11 = 0.89

请注意,二项分布广泛应用于你想要模拟从一个规模为N的总体中抽取大小为n的样本成功率的场景,且在抽样过程中允许重复。如果没有替换,那么抽样将不再独立,因此不能正确遵循二项分布。然而,确实存在这样的场景,可以通过不同类型的分布进行建模,如超几何分布。

泊松分布

泊松分布可以描述在固定时间或空间区间内,以已知平均速率发生的一定数量的独立事件的概率。请注意,事件应该只有二元结果,例如:你每天接到的电话次数,或者每小时通过某个信号的汽车数量。你需要仔细观察这些例子。请注意,这里没有提供信息的另一半,比如:你每天没有接到多少个电话,或者每小时没有通过多少辆车。这样的数据点没有另一半的信息。相反,如果我说 50 个学生中有 30 个通过了考试,你可以很容易地推断出有 20 个学生没有通过!你有了这另一半的信息。

如果µ是发生事件的平均次数(在固定时间或空间区间内的已知平均速率),那么在同一时间区间内发生k次事件的概率可以通过概率质量函数来描述:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_014.jpg

其中, k = 0, 1, 2, 3…

前面的公式描述了泊松分布。

对于泊松分布,均值和方差是相同的。同时,随着均值或方差的增加,泊松分布趋向于更加对称。

示例问题

假设你知道在一个工作日,消防站接到的电话平均次数是八次。那么,在某个工作日接到 11 个电话的概率是多少呢?这个问题可以通过以下基于泊松分布的公式来解决:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_015.jpg

连续概率分布

连续概率分布用于对连续性质的数据进行建模,这意味着数据只能取特定范围内的任何值。因此,我们处理的是与区间相关的概率,而不是与某个特定值相关的概率,因为它的概率为零。连续概率分布是实验的理论模型;它是通过无限次观测建立的相对频率分布。这意味着当你缩小区间时,观测次数会增加,而随着观测次数越来越多并接近无穷大时,它就形成了一个连续概率分布。曲线下的总面积为 1,若要找到与某个特定范围相关的概率,我们必须找到曲线下的面积。因此,连续分布通常通过概率密度函数PDF)来描述,其形式如下:

P(a ≤ X ≤ b) = a∫^b f(x) dx

连续概率分布有多种类型。以下章节展示了几个例子。

正态分布

正态分布是一种简单、直观,但非常重要的连续概率分布。因其绘制时的外观而被称为高斯分布或钟形曲线。此外,对于完美的正态分布,均值、中位数和众数都是相同的。

许多自然现象遵循正态分布(它们也可能遵循不同的分布!),例如人的身高、测量误差等。但是,正态分布不适合模拟高度倾斜或固有为正值的变量(例如股票价格或学生的测试成绩,其中难度水平很低)。这些变量可能更适合用不同的分布描述,或者在数据转换(如对数转换)后再用正态分布描述。

正态分布可以用两个描述符来描述:均值代表中心位置,标准差代表扩展(高度和宽度)。表示正态分布的概率密度函数如下:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_016.jpg

正态分布之所以在流行度榜首,其中一个原因是中心极限定理CLT)。它指出,无论是什么种群分布,从同一种群分布独立抽取的样本的均值几乎服从正态分布,这种正态性随着样本量的增加而增强。这种行为实际上是统计假设检验的基础。

此外,每个正态分布,无论其均值和标准差如何,都遵循经验法则(68-95-99.7 法则),即约 68%的面积位于均值的一个标准差内,约 95%的面积位于均值的两个标准差内,约 99.7%的面积位于均值的三个标准差内。

现在,为了找到事件的概率,您可以使用积分微积分,或者按照下一节中的说明将分布转换为标准正态分布。

标准正态分布

标准正态分布是一种均值为0,标准差为1的正态分布类型。这种分布在自然界中很少见。它主要设计用于找出正态分布曲线下的面积(而不是使用微积分积分)或者标准化数据点。

假设随机变量X服从均值(μ)和标准差(σ)的正态分布,则随机变量Z将服从均值0和标准差1的标准正态分布。可以通过以下方式找到Z的值:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_017.jpg

由于数据可以通过这种方式标准化,因此数据点可以表示为离均值有多少个标准差,并可以在分布中进行解读。这有助于比较两个具有不同尺度的分布。

你可以在那些想要找到落入特定范围的百分比的场景中使用正态分布——前提是分布大致正态。

考虑以下示例:

如果商店老板在某一天经营商店的时间遵循正态分布,且μ = 8小时,σ = 0.5小时,那么他在商店待少于 7.5 小时的概率是多少?

概率分布将如下所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_018.jpg

数据分布

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Capture.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_020.jpg

标准正态分布

所以,商店老板待在商店少于 7.5 小时的概率由以下公式给出:

P(z = -1) = 0.1587 = 15.87

注意

这是通过 Z 表计算出来的。

请注意,数据集的正态性通常是一个近似值。你首先需要检查数据的正态性,如果你的分析假设数据为正态分布,则可以继续进行。检查正态性的方法有很多种:你可以选择直方图(并且拟合一条与数据均值和标准差相符的曲线)、正态概率图或 QQ 图等技术。

卡方分布

卡方分布是统计推断中最广泛使用的分布之一。它是伽玛分布的一个特例,适用于建模非负变量的偏斜分布。它表示,如果随机变量X是正态分布的,且Z是其标准正态变量之一,那么*Z[2]将具有X[²]分布,且自由度为 1。类似地,如果我们从相同分布中抽取多个这样的独立标准正态变量,将它们平方并相加,那么它们也将遵循X[²]*分布,如下所示:

Z[12] + Z[22] + … + Z[k2]将具有X[2]分布,且自由度为k

卡方分布主要用于基于样本方差或标准差推断总体方差或总体标准差。这是因为*X[2]*分布是通过另一种方式定义的,涉及样本方差与总体方差的比率。

为了证明这一点,让我们从一个正态分布中随机抽取样本(x[1], x[2],…,xn),其方差为https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Ch.jpg

样本均值由以下公式给出:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_021.jpg

然而,样本方差由以下公式给出:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_022.jpg

考虑到上述事实,我们可以将卡方统计量定义如下:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_023.jpg

(记住https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_024.jpgZ[2] 将会有 X[2] 分布。)

因此,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_025.jpg

因此,卡方统计量的抽样分布将遵循自由度为*(n-1)*的卡方分布。

具有n自由度和伽马函数Г的卡方分布的概率密度函数如下:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_026.jpg

对于具有k自由度的χ2分布,均值(µ)= k,方差(σ2)= 2k

请注意,卡方分布是正偏的,但随着自由度增加,偏度减小,并接近正态分布。

示例问题

找到成人单人电影票价格的方差和标准差的 90%置信区间。所给数据代表全国电影院的选定样本。假设变量呈正态分布。

给定样本(以$计算):10, 08, 07, 11, 12, 06, 05, 09, 15, 12

解决方案:

N = 10

样本均值:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Mean-of-sample.jpg

样本的方差:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Variance.jpg

样本的标准差:

S = sqrt(9.61)

自由度:

10-1 = 9

现在我们需要找到 90%的置信区间,这意味着数据的 10%将留在尾部。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_027.jpg

现在,让我们使用公式:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_028.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_029.jpg

然后我们可以使用表格或计算机程序找到卡方值。

要找到中间 90%的置信区间,我们可以考虑左边 95%和右边 5%。

所以,代入数字后,我们得到:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_030.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_031.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_032.jpg

因此,我们可以得出结论,基于全国 10 家电影票价格样本,我们对整个国家的电影票价格标准差有 90%的信心区间在$2.26 和$5.10 之间。

学生 t 分布

学生 t 分布用于估算正态分布总体的均值,当总体标准差未知或样本量过小时。在这种情况下,只能通过样本估算总体参数μσ

这个分布呈钟形,对称,类似正态分布,但尾部更重。当样本量大时,t 分布趋于正态分布。

让我们从均值为μ和方差为σ2的正态分布中随机抽取样本(x1, x2,…,xn)。

样本均值为 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_033.jpg 和样本方差 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_034.jpg

考虑到上述事实,t 统计量可以定义为:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_035.jpg

t 统计量的抽样分布将遵循具有 (n-1) 自由度 (df) 的 t 分布。自由度越高,t 分布越接近标准正态分布。

t 分布的均值 (μ) = 0,方差 (σ2) = df/df-2

现在,为了让事情更清晰,让我们回顾一下,考虑总体 σ 已知的情况。当总体是正态分布时,无论样本量大小如何,样本均值 通常也是正态分布,且任何线性变换的 ,例如https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_037.jpg,也将遵循正态分布。

如果总体不是正态分布怎么办?即使如此,根据中心极限定理,当样本量足够大时,(即抽样分布)或https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_037.jpg的分布将趋近于正态分布!

另一种情况是总体的 σ 不为已知。如果总体是正态分布,那么样本均值 通常也是正态分布,但随机变量 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_039.jpg 将不遵循正态分布;它遵循具有 (n-1) 自由度的 t 分布。原因是因为分母中的 S 存在随机性,不同的样本会导致不同的值。

在上述情况下,如果总体不是正态分布,按照中心极限定理(CLT),当样本量足够大时,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_040.jpg的分布将趋近于正态分布(但小样本量时则不适用!)。因此,当样本量较大时,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_040.jpg的分布会趋近于正态分布,可以安全地假设它符合 t 分布,因为 t 分布随着样本量增加而逐渐接近正态分布。

F-分布

在统计推断中,F-分布用于研究两个正态分布总体的方差。它表明,来自两个独立正态分布总体的样本方差的抽样分布,如果这两个总体的方差相同,则遵循 F-分布。

如果样本 1 的样本方差是https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_041.jpg,并且样本 2 的样本方差是https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_042.jpg,那么https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_043.jpg将遵循 F-分布(σ12 = σ22)。

根据上述事实,我们还可以说,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_044.jpg也将遵循 F-分布。

在前一节的卡方分布中,我们还可以说:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_045.jpg也将遵循具有 n1-1n2-1 自由度的 F 分布。对于这些自由度的每种组合,会有不同的 F 分布。

标准误差

统计量(如均值或方差)抽样分布的标准差称为标准误差SE),它是衡量变异性的一个指标。换句话说,均值的标准误差SEM)可以定义为样本均值对总体均值估计的标准差。

随着样本量的增加,均值的抽样分布变得越来越接近正态分布,标准差也变得越来越小。已证明:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_046.jpg

(n 代表样本大小)

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_047.jpg

标准误差越小,样本对总体的代表性就越强。此外,样本量越大,标准误差越小。

标准误差在统计推断的其他度量中非常重要,例如误差范围和置信区间。

置信水平

它是衡量通过样本统计量估计总体参数时,你希望有多大的置信度(概率),从而使得期望值落在一个期望的区间或置信区间内。它是通过从 1 中减去显著性水平(α)来计算的(即,置信水平 = 1 - α)。所以,如果α = 0.05,则置信水平为1-0.05 = 0.95

通常,置信水平越高,所需的样本量也越大。然而,通常存在一定的权衡,您需要决定希望达到的置信度,从而估算所需的样本量。

误差范围和置信区间

如前所述,由于样本永远不能完全代表总体,通过推断估算总体参数时,总会存在一些抽样误差,导致误差范围。通常,样本越大,误差范围越小。然而,您需要决定接受多少误差,所需的适当样本大小也会取决于这一点。

因此,基于误差范围的样本统计量上下的数值范围称为置信区间。换句话说,置信区间是一个数值区间,我们相信总体的真实参数会有某个百分比的时间落在这个区间内(置信水平)。

请注意,像“我有 95%的把握置信区间包含真实值”这样的说法可能会误导!正确的表述方式应该是“如果我从相同大小的样本中抽取无限多次样本,那么在 95%的情况下,置信区间将包含真实值”。

例如,当你将置信水平设为 95%,并且将置信区间设为 4%,对于一个样本统计量 58(这里,58 是任何样本统计量,如均值、方差或标准差),你可以说你有 95%的把握认为总体的真实百分比在 58 - 4 = 54%和 58 + 4 = 62%之间。

总体的变异性

总体的变异性是我们在推断统计中应该考虑的最重要因素之一。它在估算样本大小时起着重要作用。无论你选择什么抽样算法来最好地代表总体,样本大小仍然起着至关重要的作用——这一点显而易见!

如果总体的变异性较大,那么所需的样本大小也会更大。

估算样本大小

我们已经在前面的章节中讨论了抽样技术。在本节中,我们将讨论如何估算样本大小。假设你必须证明某个概念或评估某个行动的结果,那么你会收集一些相关数据并试图证明你的观点。然而,如何确保你拥有足够的数据呢?样本过大浪费时间和资源,样本过小可能导致误导性的结果。估算样本大小主要取决于误差范围或置信区间、置信水平以及总体的变异性等因素。

考虑以下示例:

大学校长要求统计学老师估算其学院学生的平均年龄。需要多大的样本?统计学老师希望能有 99%的置信度,使得估算值在 1 年以内是准确的。根据以往的研究,已知年龄的标准差为 3 年。

解决方案:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_048.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_05_049.jpg

假设检验

假设检验是关于测试为总体参数所做假设的过程。这有助于确定一个结果是否具有统计学意义,或是否是偶然发生的。这是统计研究中最重要的工具之一。我们将讨论一些检验方法,看看变量在总体中是如何相互关联的。

原假设与备择假设

原假设(记作 H0)通常是关于总体参数的初始假设,通常表示无效应无关联。在我们的假设检验中,我们的目的是否定并拒绝原假设,以便接受备择假设(记作 H1)。备择假设表示实验中存在某种效应。在实验过程中,请注意你要么拒绝原假设,要么无法拒绝原假设。如果你成功地拒绝了原假设,那么备择假设就应该被考虑;如果你未能拒绝原假设,那么原假设就被视为成立(尽管它可能不成立!)。

所以,我们通常希望得到一个非常小的 P 值(低于定义的显著性水平 alpha),以便拒绝原假设。如果 P 值大于 alpha,那么你就无法拒绝原假设。

卡方检验

大多数统计推断技术用于估计总体参数或使用样本统计量(如均值)来检验假设。然而,卡方统计量采取完全不同的方法,通过检查整个分布或两个分布之间的关系来进行分析。在推断统计学领域,许多检验统计量类似于卡方分布。使用这种分布的最常见检验是卡方拟合优度检验(单向表)和卡方独立性检验(双向表)。拟合优度检验是在你想要查看样本数据是否遵循总体中的相同分布时进行的,而独立性检验是在你想要查看两个分类变量是否在总体中相互关联时进行的。

输入数据类型决定了是否进行拟合优度检验或独立性检验,而不需要明确指定这些作为开关。因此,如果你提供一个向量作为输入,则进行拟合优度检验;如果你提供一个矩阵作为输入,则进行独立性检验。在这两种情况下,都需要先提供事件的频率向量或列联矩阵作为输入,然后进行计算。让我们通过实例来探讨这些:

Python

 //Chi-Square test
>>> from pyspark.mllib.linalg import Vectors, Matrices
>>> from pyspark.mllib.stat import Statistics
>>> import random
>>> 
//Make a vector of frequencies of events
>>> vec = Vectors.dense( random.sample(xrange(1,101),10))
>>> vec
DenseVector([45.0, 40.0, 93.0, 66.0, 56.0, 82.0, 36.0, 30.0, 85.0, 15.0])
// Get Goodnesss of fit test results
>>> GFT_Result = Statistics.chiSqTest(vec)
// Here the ‘goodness of fit test’ is conducted because your input is a vector
//Make a contingency matrix
>>> mat = Matrices.dense(5,6,random.sample(xrange(1,101),30))\
//Get independense test results\\
>>> IT_Result = Statistics.chiSqTest(mat)
// Here the ‘independence test’ is conducted because your input is a vector
//Examine the independence test results
>>> print(IT_Result)
Chi squared test summary:
method: pearson
degrees of freedom = 20
statistic = 285.9423808343265
pValue = 0.0
Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent..

Scala

scala> import org.apache.spark.mllib.linalg.{Vectors, Matrices}
import org.apache.spark.mllib.linalg.{Vectors, Matrices} 

scala> import org.apache.spark.mllib.stat.Statistics 

scala> val vec = Vectors.dense( Array.fill(10)(               scala.util.Random.nextDouble))vec: org.apache.spark.mllib.linalg.Vector = [0.4925741159101148,....] 

scala> val GFT_Result = Statistics.chiSqTest(vec)GFT_Result: org.apache.spark.mllib.stat.test.ChiSqTestResult =Chi squared test summary:
method: pearson
degrees of freedom = 9
statistic = 1.9350768763253192
pValue = 0.9924531181394086
No presumption against null hypothesis: observed follows the same distribution as expected..
// Here the ‘goodness of fit test’ is conducted because your input is a vector
scala> val mat = Matrices.dense(5,6, Array.fill(30)(scala.util.Random.nextDouble)) // a contingency matrix
mat: org.apache.spark.mllib.linalg.Matrix =..... 
scala> val IT_Result = Statistics.chiSqTest(mat)
IT_Result: org.apache.spark.mllib.stat.test.ChiSqTestResult =Chi squared test summary:
method: pearson
degrees of freedom = 20
statistic = 2.5401190679900663
pValue = 0.9999990459111089
No presumption against null hypothesis: the occurrence of the outcomes is statistically independent..
// Here the ‘independence test’ is conducted because your input is a vector

F 检验

我们已经在前面的章节中介绍了如何计算 F 统计量。现在我们将解决一个样本问题。

问题:

你想要检验一个假设:硕士学位持有者的收入波动性是否大于学士学位持有者的收入波动性。随机抽取了 21 名毕业生和 30 名硕士生。毕业生样本的标准差为$180,硕士生样本的标准差为$112。

解决方案:

零假设是:H[0] : σ[1]² =σ[2]²

给定 S[1] = $180n[1] = 21S[2] = $112n[2] = 30

假设显著性水平为α = 0.05

F = S[1]² /S[2]² = 180²/112² = 2.58

从显著性水平为 0.05 的 F 表中,df1=20 和 df2=29,我们可以看到 F 值为 1.94

由于计算出的 F 值大于表中 F 值,我们可以拒绝零假设并得出结论:σ[1]² >σ[2] ^(2)。

相关性

相关性提供了一种衡量两个数值型随机变量之间统计依赖关系的方法。它展示了两个变量相互变化的程度。基本上有两种类型的相关性度量:皮尔逊相关性和斯皮尔曼相关性。皮尔逊相关性更适合于区间尺度数据,如温度、身高等;斯皮尔曼相关性则更适合于有序尺度数据,如满意度调查,其中 1 表示不满意,5 表示最满意。此外,皮尔逊相关性是基于真实值计算的,适用于找到线性关系,而斯皮尔曼相关性则是基于排名顺序计算的,适用于找到单调关系。单调关系意味着变量确实会一起变化,但变化速率不一定恒定。请注意,这两种相关性度量只能衡量线性或单调关系,不能描绘其他类型的关系,如非线性关系。

在 Spark 中,这两者都得到了支持。如果输入是两个 RDD[Double],输出是一个Double;如果输入是一个 RDD[Vector],输出是一个相关性矩阵。在 Scala 和 Python 的实现中,如果没有提供相关性类型作为输入,则默认考虑为皮尔逊相关性。

Python

>>> from pyspark.mllib.stat import Statistics
>>> import random 
// Define two series
//Number of partitions and cardinality of both Ser_1 and Ser_2 should be the same
>>> Ser_1 = sc.parallelize(random.sample(xrange(1,101),10))       
// Define Series_1>>> Ser_2 = sc.parallelize(random.sample(xrange(1,101),10))       
// Define Series_2 
>>> correlation = Statistics.corr(Ser_1, Ser_2, method = "pearson") 
//if you are interested in Spearman method, use “spearman” switch instead
>>> round(correlation,2)-0.14
>>> correlation = Statistics.corr(Ser_1, Ser_2, method ="spearman")
>>> round(correlation,2)-0.19//Check on matrix//The following statement creates 100 rows of 5 elements each
>>> data = sc.parallelize([random.sample(xrange(1,51),5) for x in range(100)])
>>> correlMatrix = Statistics.corr(data, method = "pearson") 
//method may be spearman as per you requirement
>>> correlMatrix
array([[ 1.        ,  0.09889342, -0.14634881,  0.00178334,  0.08389984],       [ 0.09889342,  1.        , -0.07068631, -0.02212963, -0.1058252 ],       [-0.14634881, -0.07068631,  1.        , -0.22425991,  0.11063062],       [ 0.00178334, -0.02212963, -0.22425991,  1.        , -0.04864668],       [ 0.08389984, -0.1058252 ,  0.11063062, -0.04864668,  1.        
]])
>>> 

Scala

scala> val correlation = Statistics.corr(Ser_1, Ser_2, "pearson")correlation: Double = 0.43217145308272087 
//if you are interested in Spearman method, use “spearman” switch instead
scala> val correlation = Statistics.corr(Ser_1, Ser_2, "spearman")correlation: Double = 0.4181818181818179 
scala>
//Check on matrix
//The following statement creates 100 rows of 5 element Vectors
scala> val data = sc.parallelize(Seq.fill(100)(Vectors.dense(Array.fill(5)(              scala.util.Random.nextDouble))))
data: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = ParallelCollectionRDD[37] at parallelize at <console>:27 
scala> val correlMatrix = Statistics.corr(data, method="pearson") 
//method may be spearman as per you requirement
correlMatrix: org.apache.spark.mllib.linalg.Matrix =1.0                    -0.05478051936343809  ... (5 total)-0.05478051936343809   1.0                   ..........

总结

在本章中,我们简要介绍了数据科学生命周期中的几个步骤,如数据获取、数据准备和通过描述性统计进行数据探索。我们还学习了如何通过一些流行的工具和技术,利用样本统计量估计总体参数。

我们从理论和实践两个方面解释了统计学的基础,通过深入研究几个领域的基本概念,能够解决业务问题。最后,我们学习了如何在 Apache Spark 上进行统计分析的几个示例,利用开箱即用的功能,这也是本章的主要目标。

我们将在下一章讨论数据科学中机器学习部分的更多细节,因为本章已经建立了统计学的基础。通过本章的学习,应该能够更有根据地连接到机器学习算法。

参考文献

Spark 支持的统计:

spark.apache.org/docs/latest/mllib-statistics.html

Databricks 的绘图功能:

docs.cloud.databricks.com/docs/latest/databricks_guide/04%20Visualizations/4%20Matplotlib%20and%20GGPlot.html

MLLIB 统计的开箱即用库函数的详细信息:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.stat.Statistics$

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值