Spark-数据科学-全-

Spark 数据科学(全)

原文:zh.annas-archive.org/md5/D6F94257998256DE126905D8038FBE11

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在这个智能时代,数据分析是维持和促进业务增长的关键。每家企业都在尝试尽可能利用他们的数据,借助各种数据科学工具和技术沿着分析成熟度曲线前进。数据科学需求的突然增加是数据科学家短缺的明显原因。很难满足市场需求,因为独角兽数据科学家是统计学、机器学习、数学建模以及编程方面的专家。

独角兽数据科学家的可用性只会随着市场需求的增加而减少,并将继续如此。因此,需要一个解决方案,不仅能够赋予独角兽数据科学家更多的权力,而且还能创造 Gartner 所称的“公民数据科学家”。公民数据科学家不是其他人,而是开发人员、分析师、BI 专业人员或其他主要工作职能不在统计或分析之外,但足够热衷于学习数据科学。他们正在成为组织和整个行业中民主化数据分析的关键推动者。

有越来越多的工具和技术旨在促进大规模的大数据分析。这本书试图创建能够利用 Apache Spark 分布式计算平台进行数据分析的公民数据科学家。

这本书是一个实用指南,教授统计分析和机器学习,以构建可扩展的数据产品。它有助于掌握数据科学的核心概念,以及 Apache Spark,帮助您在任何实际的数据分析项目上快速启动。整本书的每一章都有足够的例子支持,可以在家用电脑上执行,以便读者可以轻松地跟踪和吸收概念。每一章都试图是独立的,以便读者可以从任何章节开始,并指向相关章节以获取详细信息。虽然章节从基础知识开始,供初学者学习和理解,但同时也足够全面,供高级架构师使用。

本书内容

第一章, 大数据和数据科学-简介,本章简要讨论了大数据分析中的各种挑战,以及 Apache Spark 如何在单一平台上解决这些问题。本章还解释了数据分析是如何演变成现在的样子,也对 Spark 堆栈有了基本的了解。

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

第三章, 数据框简介,本章介绍了数据框,这是数据科学家最方便和有用的组件,可以轻松工作。它解释了 Spark SQL 和 Catalyst 优化器如何赋予数据框权力。还演示了各种数据框操作的代码示例。

第四章, 统一数据访问,本章讨论了我们从不同来源获取数据的各种方式,以统一的方式 consolide 和工作。它涵盖了实时数据收集和操作的流方面。它还讨论了这些 API 的底层基础知识。

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

第六章,机器学习,本章解释了各种机器学习算法,它们是如何在 MLlib 库中实现的,以及如何使用流水线 API 进行流畅的执行。本章涵盖了所有算法的基础知识,因此可以作为一个一站式参考。

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

第八章,分析非结构化数据,本章仅讨论非结构化数据分析。它解释了如何获取非结构化数据,处理它并对其进行机器学习。它还涵盖了一些在“机器学习”章节中未涵盖的降维技术。

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

第十章,将所有内容放在一起,到目前为止,本书已经分别讨论了不同章节中的大多数数据分析组件。本章是为了将典型的数据科学项目的各个步骤串联起来,并演示一个完整的分析项目执行的逐步方法。

第十一章,构建数据科学应用,到目前为止,本书主要讨论了数据科学组件以及一个完整的执行示例。本章提供了如何构建可以部署到生产环境中的数据产品的概述。它还介绍了 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 等)中获取数据。”

注意

警告或重要提示会以这样的方式显示在框中。

提示

提示和技巧会显示为这样。

第一章:大数据和数据科学-介绍

大数据绝对是一件大事!它承诺通过从庞大的数据堆中获取隐藏的见解,并开辟新的业务发展途径,为组织创造和保持竞争优势提供了丰富的机会。通过先进的分析技术利用大数据已经成为组织的必然选择。

本章解释了大数据的全部内容,大数据分析的各种挑战,以及 Apache Spark 如何成为解决计算挑战的事实标准,并且还作为数据科学平台。

本章涵盖的主题如下:

  • 大数据概述-为什么如此重要?

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

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

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

  • Spark 堆栈-构成完整大数据解决方案的一切

大数据概述

关于大数据已经有很多言论和文章,但没有明确的标准来清晰地定义它。在某种程度上,这实际上是一个相对的术语。无论数据是小还是大,只有当你能够正确地分析它时,你才能利用它。为了从数据中得出一些意义,需要正确的分析技术,并且在数据分析中选择正确的工具和技术至关重要。然而,当数据本身成为问题的一部分,并且在执行数据分析之前需要解决计算挑战时,它就成为了一个大数据问题。

在万维网上发生了一场革命,也被称为 Web 2.0,改变了人们使用互联网的方式。静态网页变成了互动网站,并开始收集越来越多的数据。云计算、社交媒体和移动计算的技术进步创造了数据爆炸。每个数字设备开始发出数据,许多其他来源开始驱动数据洪流。来自各个角落的数据流产生了各种大量的数据,速度之快!以这种方式形成大数据是一种自然现象,因为这就是万维网的演变方式,没有明确的特定努力。这是关于过去的事情!如果考虑到正在发生的变化,以及未来将会发生的变化,数据生成的数量和速度超出了人们的预期。我之所以要做出这样的表态,是因为如今每个设备都变得更加智能,这要感谢物联网(IoT)。

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

组织非常确信,利用大数据不仅可以回答特定的业务问题,还可以带来机会,以覆盖业务中未发现的可能性,并解决与此相关的不确定性。因此,除了自然的数据涌入外,组织开始制定策略,产生越来越多的数据,以保持其竞争优势并做好未来准备。举个例子来更好地理解这一点。想象一下,在制造工厂的机器上安装了传感器,这些传感器不断地发出数据,因此可以得知机器零部件的状态,公司能够预测机器何时会发生故障。这让公司能够预防故障或损坏,避免计划外停机,从而节省大量资金。

大数据分析的挑战

在大数据分析中,主要存在两种类型的严峻挑战。第一个挑战是需要一个庞大的计算平台,一旦建立起来,第二个挑战就是在规模上分析和理解大量数据。

计算挑战

随着数据量的增加,大数据的存储需求也越来越大。数据管理变成了一项繁琐的任务。尽管处理器的处理速度和 RAM 的频率达到了标准,但由于寻道时间导致的访问磁盘存储的延迟成为了主要瓶颈。

从各种业务应用程序和数据孤岛中提取结构化和非结构化数据,对其进行整合并加工以找到有用的业务见解是具有挑战性的。只有少数应用程序能够解决任何一个领域,或者只能解决少数多样化的业务需求。然而,将这些应用程序集成在一起以统一方式解决大部分业务需求只会增加复杂性。

为了解决这些挑战,人们转向了具有分布式文件系统的分布式计算框架,例如 Hadoop 和Hadoop 分布式文件系统HDFS)。这可以消除由于磁盘 I/O 而产生的延迟,因为数据可以在机器集群上并行读取。

分布式计算技术在之前已存在几十年,但直到行业意识到大数据的重要性后才变得更加突出。因此,诸如 Hadoop 和 HDFS 或 Amazon S3 之类的技术平台成为了行业标准。除了 Hadoop 之外,还开发了许多其他解决方案,如 Pig、Hive、Sqoop 等,以满足不同类型的行业需求,如存储、提取、转换和加载ETL)以及数据集成,从而使 Hadoop 成为一个统一的平台。

分析挑战

分析数据以发现一些隐藏的见解一直是具有挑战性的,因为处理大型数据集涉及到额外的复杂性。传统的 BI 和 OLAP 解决方案无法解决由大数据带来的大部分挑战。举个例子,如果数据集有多个维度,比如 100 个,那么很难将这些变量相互比较以得出结论,因为会有大约 100C2 种组合。这种情况需要使用统计技术,如相关性等,来发现隐藏的模式。

尽管许多问题都有统计解决方案,但对于数据科学家或分析专业人员来说,除非他们将整个数据集加载到内存中的DataFrame中,否则很难对数据进行切片和切块以找到智能洞见。主要障碍在于,大多数用于统计分析和机器学习的通用算法都是单线程的,并且是在数据集通常不那么庞大且可以适应单台计算机的 RAM 中编写的时代编写的。这些用 R 或 Python 编写的算法在原始形式上在分布式计算环境中部署时不再非常有用,因为存在内存计算的限制。

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

大数据分析的演变

前一节概述了如何解决大数据需求的计算和数据分析挑战。这是可能的,因为多个相关趋势的融合,如低成本的通用硬件、大数据的可访问性和改进的数据分析技术。Hadoop 成为许多大型分布式数据处理基础设施的基石。

然而,人们很快意识到 Hadoop 的局限性。Hadoop 解决方案最适合特定类型的大数据需求,如 ETL;它只在这些需求中才变得流行。

有时数据工程师或分析师需要对数据集执行即席查询进行交互式数据分析。每次在 Hadoop 上运行查询时,数据都会从磁盘(HDFS 读取)读取并加载到内存中,这是一项昂贵的工作。实际上,作业的运行速度取决于网络和磁盘集群上的 I/O 传输速度,而不是 CPU 和 RAM 的速度。

以下是情景的图示表示:

大数据分析的演变

Hadoop 的 MapReduce 模型无法很好地适应迭代性质的机器学习算法。Hadoop MapReduce 在迭代计算中性能不佳,延迟巨大。由于 Map 和 Reduce 工作者之间禁止通信的受限编程模型,中间结果需要存储在稳定的存储器中。因此,这些结果被推送到 HDFS,然后写入磁盘,而不是保存在 RAM 中,然后在后续迭代中重新加载到内存中,其他迭代也是如此。磁盘 I/O 的数量取决于算法中涉及的迭代次数,这还伴随着在保存和加载数据时的序列化和反序列化开销。总的来说,这是计算昂贵的,与预期相比,并没有达到预期的受欢迎程度。

以下是这种情景的图示表示:

大数据分析的演变

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

不需要重新设计所有算法,而是需要一个通用引擎,大多数算法可以利用它在分布式计算平台上进行内存计算。人们也期望这样的设计会导致迭代计算和临时数据分析的更快执行。这就是 Spark 项目在加州大学伯克利分校的 AMPLab 中开辟道路的方式。

用于数据分析的 Spark

在 AMPLab 中,Spark 项目成功之后,它于 2010 年开源,并于 2013 年转移到 Apache 软件基金会。目前由 Databricks 领导。

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

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

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

  • 通过隐藏分布式编程的复杂性,提供高级 API 来开发各种分布式应用程序

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

用于数据分析的 Spark

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

用于数据分析的 Spark

Spark 隐藏了编写核心 MapReduce 作业的复杂性,并通过简单的函数调用提供了大部分功能。由于其简单性,它能够满足更广泛和更大的受众群体,如数据科学家、数据工程师、统计学家和 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 堆栈:

Spark 堆栈

Spark 核心

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

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

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

Spark SQL

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

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

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

Spark 流处理

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

它提供了一个名为离散流DStream)的 API,用于操作实时数据流。实时数据流被切分成小批次,比如说,x秒。Spark 将每个批次视为 RDD 并对它们进行基本的 RDD 操作。DStreams 可以从 HDFS、Kafka、Flume 或任何其他能够通过 TCP 套接字流式传输数据的源创建出来。通过在 DStreams 上应用一些高级操作,可以产生其他 DStreams。

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

MLlib

MLlib 是 Spark 堆栈中内置的机器学习库。它是在 Spark 0.8 中引入的。其目标是使机器学习变得可扩展和简单。开发人员可以无缝地在他们选择的编程语言(Java、Python 或 Scala)中使用 Spark SQL、Spark 流处理和 GraphX。MLlib 提供了执行各种统计分析(如相关性、抽样、假设检验等)所需的函数。此组件还涵盖了分类、回归、协同过滤、聚类和分解等领域的广泛应用和算法。

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

GraphX

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

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

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)以及处理网络规模数据的存储应用程序)方面非常成功。然而,开发人员在使用这些框架时需要使用大量的工具,以及成熟的 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 内置对多种语言的支持,可以通过一个称为读取-求值-打印-循环REPL)的 shell 进行交互式使用,这对任何语言的开发人员来说都会感到熟悉。开发人员可以使用他们选择的语言,利用现有的库,并与 Spark 及其生态系统无缝交互。让我们看看 Spark 支持的语言以及它们如何适应 Spark 生态系统。

Scala

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

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

Java

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

这个 Java API 只缺少一个基于 shell 的接口,用于在 Spark 上进行交互式数据分析。

Python

Python 通过 PySpark 在 Spark 上得到支持,它是建立在 Spark 的 Java API(使用 Py4J)之上的。从现在开始,我们将使用术语PySpark来指代 Spark 上的 Python 环境。Python 在数据整理、数据处理和其他数据科学相关任务方面已经非常受开发人员欢迎。随着 Spark 能够解决可伸缩计算的挑战,对 Python 在 Spark 上的支持变得更加流行。

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

R

R 通过 SparkR 支持 Spark,这是一个 R 包,通过它可以通过 R 访问 Spark 的可伸缩性。SparkR 使 R 能够解决单线程运行时的限制,因此计算仅限于单个节点。

由于 R 最初只设计用于统计分析和机器学习,它已经丰富了大部分的包。数据科学家现在可以在大规模数据上工作,学习曲线很小。R 仍然是许多数据科学家的首选。

选择合适的语言

除了开发人员的语言偏好之外,有时还有其他约束条件可能会引起注意。在选择一种语言而不是另一种语言时,以下方面可能会补充您的开发经验:

  • 在开发复杂逻辑时,交互式 shell 非常方便。除了 Java 之外,Spark 支持的所有语言都有交互式 shell。

  • R 是数据科学家的通用语言。由于其更丰富的库集,它绝对更适合纯数据分析。R 支持是在 Spark 1.4.0 中添加的,以便 Spark 能够接触到使用 R 的数据科学家。

  • Java 拥有更广泛的开发人员基础。Java 8 已经包含了 lambda 表达式,因此具有函数式编程方面。尽管如此,Java 往往冗长。

  • Python 在数据科学领域逐渐变得更受欢迎。Pandas 和其他数据处理库的可用性,以及其简单和表达性的特性,使 Python 成为一个强有力的选择。Python 在数据聚合、数据清洗、自然语言处理等方面比 R 更灵活。

  • Scala 可能是实时分析的最佳选择,因为它与 Spark 最接近。对于来自其他语言的开发人员来说,初始学习曲线不应成为严重生产系统的障碍。Spark 的最新增加通常首先在 Scala 中可用。其静态类型和复杂的类型推断提高了效率以及编译时检查。Scala 可以利用 Java 的库,因为 Scala 自己的库基础仍处于早期阶段,但正在迎头赶上。

Spark 引擎

要使用 Spark 进行编程,需要对 Spark 组件有基本的了解。在本节中,将解释一些重要的 Spark 组件以及它们的执行机制,以便开发人员和数据科学家可以编写程序和构建应用程序。

在深入细节之前,我们建议您查看以下图表,以便在阅读更多内容时更容易理解 Spark 齿轮的描述:

The Spark engine

驱动程序

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

主要是在驱动程序端使用数据源(可能是 RDD)和转换构建 RDD 谱系有向无环图(DAG)。当遇到action方法时,此 DAG 被提交给 DAG 调度程序。然后 DAG 调度程序将 DAG 拆分为逻辑工作单元(例如 map 或 reduce)称为阶段。每个阶段又是一组任务,每个任务由任务调度程序分配给执行者(工作节点)。作业可以按 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 对象。您也可以通过传递一个用于设置各种 Spark 配置参数的 SparkConf 对象来创建它。请注意,在一个 JVM 中只能有一个 SparkContext 对象。

工作节点

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

执行者

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

共享变量

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

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

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

执行流程

一个 Spark 应用程序由一个驱动程序和多个工作(执行器)程序组成。驱动程序包含应用程序的main函数和一个代表与 Spark 集群的连接的 SparkContext 对象。驱动程序和其他进程之间的协调通过 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,以便在程序中重复使用。

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

RDD 可以被视为一组分区(拆分),具有对父 RDD 的依赖关系列表和一个计算分区的函数。有时,父 RDD 的每个分区被单个子 RDD 使用。这被称为窄依赖。窄依赖是可取的,因为当父 RDD 分区丢失时,只需要重新计算一个子分区。另一方面,计算涉及group-by-keys等操作的单个子 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。我们将在本书的后面更详细地讨论这个方面。

创建 RDDs

在本节中,我们将使用 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的文件。这个语句只是一个转换,直到遇到一个动作才会被执行。你可以尝试给一个不存在的文件名,但直到执行下一个语句(也就是一个动作语句)时才会得到任何错误。

我们已经完成了启动 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 的图形表示,它是使用一个包含 14 条记录(或元组)的数据集创建的,并且被分区为 3 个,分布在 3 个节点上:

创建 RDDs

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

创建 RDDs

这是因为 RDD 是不可变(只读)的抽象设计。从动作中产生的输出可以被写回到存储系统,也可以返回给驱动程序进行本地计算,以便产生最终输出。

到目前为止,我们已经看到了一些简单的转换来定义 RDD,并进行了一些处理和生成一些输出的动作。让我们快速浏览一些方便的转换和转换对配对 RDD 的转换。

对普通 RDD 的转换

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

filter 操作

filter操作返回一个只包含满足filter条件的元素的 RDD,类似于 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 

交集操作

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

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 操作

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 操作使用关联的 reduce 函数合并每个键的值。这也会在将结果发送到 reducer 并生成哈希分区输出之前在每个 mapper 上本地执行合并。

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))  

aggregate 操作

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)不一定要与此处显示的输出完全匹配。这取决于各个任务返回其输出的顺序。

动作

一旦创建了 RDD,各种转换只有在对其执行动作时才会执行。动作的结果可以是写回存储系统的数据,也可以返回给启动此操作的驱动程序,以便在本地进行进一步计算以生成最终结果。

我们已经在之前的转换示例中涵盖了一些动作函数。以下是一些更多的示例,但还有很多需要您去探索。

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。它返回一个(K, Int)对的表,其中包含每个键的计数。

以下是一些关于 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 并对其执行转换和操作。我们还讨论了一些 RDD 的高级操作。

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

参考资料

Scala 语言:

Apache Spark 架构:

Spark 编程指南是概念的主要资源;参考特定语言的 API 文档以获取可用操作的完整列表。

弹性分布式数据集:Matei Zaharia 等人的《内存集群计算的容错抽象》是 RDD 基础知识的原始来源:

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

第三章:DataFrames 简介

要解决任何真实的大数据分析问题,绝对需要访问一个高效且可扩展的计算系统。然而,如果计算能力对目标用户不易于访问,那么这几乎没有任何意义。交互式数据分析通过可以表示为命名列的数据集变得更加容易,而这在普通的 RDDs 中是不可能的。因此,需要一种基于模式的方法来以标准化的方式表示数据,这就是 DataFrames 背后的灵感来源。

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

这个主题将作为许多即将到来的章节的基础,并且我们强烈建议您非常了解这里涵盖的概念。作为本章的先决条件,需要对 SQL 和 Spark 有基本的了解。本章涵盖的主题如下:

  • 为什么要使用 DataFrames?

  • Spark SQL

  • Catalyst 优化器

  • DataFrame API

  • DataFrame 基础知识

  • RDD 与 DataFrame

  • 创建 DataFrames

  • 从 RDDs

  • 从 JSON

  • 从 JDBC 数据源

  • 从其他数据源

  • 操作 DataFrames

为什么要使用 DataFrames?

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

尽管 Spark 提供了一个用于操作分布式数据集的函数式编程 API,但最终却以元组(_1、_2 等)结束。对元组进行操作的编码有时会有些复杂和混乱,有时还会很慢。因此,需要一个标准化的层,具有以下特点:

  • 具有模式的命名列(比元组更高级的抽象),使得操作和跟踪它们变得容易

  • 从各种数据源(如 Hive、Parquet、SQL Server、PostgreSQL、JSON 以及 Spark 的本地 RDDs)整合数据的功能,并将它们统一到一个通用格式中

  • 利用特殊文件格式(如 Avro、CSV、JSON 等)中的内置模式的能力

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

  • 消除了基于特定领域任务定义列对象的需求,以便 ML 算法能够正常工作,并为 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 中的一个实验性层,因此我们将限制我们的讨论只涉及 DataFrames。

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

Catalyst 优化器

Catalyst 优化器是 Spark SQL 和 DataFrame 的支点。它是使用 Scala 的函数式编程构造构建的,并具有以下功能:

  • 来自各种数据格式的模式推断:

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

  • RDDs 是 Scala 对象;类型信息从 Scala 的类型系统中提取,即case classes,如果它们包含 case classes。

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

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

  • 内置支持广泛的数据源和查询联合以实现高效的数据导入:

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

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

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

  • 控制和优化代码生成:

  • 优化实际上发生在整个执行管道的非常晚期。

  • Catalyst 旨在优化查询执行的所有阶段:分析、逻辑优化、物理规划和代码生成,以将查询的部分编译为 Java 字节码。

DataFrame API

类似 Excel 电子表格的数据表示,或者来自数据库投影的输出(select 语句的输出),最接近人类的数据表示始终是一组具有多行统一列的数据。这种通常具有标记行和列的二维数据结构在某些领域被称为 DataFrame,例如 R DataFrames 和 Python 的 Pandas DataFrames。在 DataFrame 中,通常单个列具有相同类型的数据,并且行描述了关于该列的数据点,这些数据点一起表示某种含义,无论是关于一个人、一次购买还是一场棒球比赛的结果。您可以将其视为矩阵、电子表格或 RDBMS 表。

R 和 Pandas 中的 DataFrames 非常方便地对数据进行切片、重塑和分析-这是任何数据整理和数据分析工作流程中必不可少的操作。这启发了在 Spark 上开发类似概念的 DataFrames。

DataFrame 基础知识

DataFrame API 首次在 2015 年 3 月发布的 Spark 1.3.0 中引入。它是 Spark SQL 的编程抽象,用于结构化和半结构化数据处理。它使开发人员能够通过 Python,Java,Scala 和 R 利用 DataFrame 数据结构的强大功能。与 RDD 类似,Spark DataFrame 是一个分布式记录集合,组织成命名列,类似于 RDBMS 表或 R 或 Pandas 的 DataFrame。但是,与 RDD 不同的是,它们跟踪模式并促进关系操作以及map等过程操作。在内部,DataFrame 以列格式存储数据,但在需要时通过过程函数构造行对象。

DataFrame API 带来了两个特性:

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

  • 具有为常见任务设计的函数的更健壮和功能丰富的 DSL,例如:

  • 元数据

  • 抽样

  • 关系数据处理 - 项目,过滤,聚合,连接

  • UDFs

DataFrame API 建立在 Spark SQL 查询优化器之上,可以在机器集群上自动高效地执行代码。

RDD 与 DataFrame

RDD 和 DataFrame 是 Spark 提供的两种不同类型的容错和分布式数据抽象。它们在某种程度上相似,但在实现时有很大的不同。开发人员需要清楚地了解它们的差异,以便能够将其需求与正确的抽象匹配。

相似之处

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

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

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

  • 两者都是惰性评估的(在它们上执行输出操作时发生执行),因此具有最优化的执行计划的能力

  • 这两个 API 在 Scala,Python,Java 和 R 中都可用

差异

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

  • 数据框架比 RDDs 更高级的抽象。

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

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

DataFrame API 实际上是 SchemaRDD 重命名。重命名是为了表示它不再继承自 RDD,并且以熟悉的名称和概念安慰数据科学家。

创建数据框架

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

创建数据框架

从 RDD 创建数据框架

以下代码从颜色列表创建了一个 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的模糊名称。列的类型推断隐式发生。

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

  • 第一种方法使用反射来推断包含特定类型对象的 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 中的 people 数据写入 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]

从其他数据源创建数据框架

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

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 数据框架 API 背后的动机,以及 Spark 开发如何变得比以往更容易。我们简要介绍了数据框架 API 的设计方面,以及它是如何构建在 Spark SQL 之上的。我们讨论了从不同数据源(如 RDD、JSON、Parquet 和 JDBC)创建数据框架的各种方法。在本章末尾,我们简要介绍了如何对数据框架执行操作。在接下来的章节中,我们将更详细地讨论数据科学和机器学习中的 DataFrame 操作。

在下一章中,我们将学习 Spark 如何支持统一数据访问,并详细讨论数据集和结构化流组件。

参考资料

Apache Spark 官方资源的 SQL 编程指南上的 DataFrame 参考:

Databricks:介绍 Apache Spark 用于大规模数据科学的数据框架:

Databricks:从 Pandas 到 Apache Spark 的 DataFrame:

Scala 中 Spark 数据框架的 API 参考指南:

Cloudera 博客关于 Parquet - 一种高效的通用列式文件格式,用于 Apache Hadoop:

第四章:统一数据访问

来自不同数据源的数据集成一直是一项艰巨的任务。大数据的三个 V 和不断缩短的处理时间框架使这项任务变得更加具有挑战性。在几乎实时地提供清晰的精心策划的数据对于业务来说非常重要。然而,实时策划的数据以及在统一方式中执行 ETL、临时查询和机器学习等不同操作的能力正在成为关键的业务差异化因素。

Apache Spark 的创建是为了提供一个可以处理各种数据源数据并支持各种不同操作的单一通用引擎。Spark 使开发人员能够在单个工作流中结合 SQL、流式处理、图形和机器学习算法!

在前几章中,我们讨论了弹性分布式数据集RDDs)以及数据框架。在第三章中,数据框架简介,我们介绍了 Spark SQL 和 Catalyst 优化器。本章将在此基础上深入探讨这些主题,帮助您认识到统一数据访问的真正本质。我们将介绍新的构造,如数据集和结构化流。具体来说,我们将讨论以下内容:

  • Apache Spark 中的数据抽象

  • 数据集

  • 使用数据集

  • 数据集 API 的限制

  • Spark SQL

  • SQL 操作

  • 底层

  • 结构化流

  • Spark 流式编程模型

  • 底层

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

  • 连续应用

  • 总结

Apache Spark 中的数据抽象

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

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

数据框架抽象建立在 RDD 之上,并添加了“命名”列。因此,Spark 数据框架具有类似关系数据库表和 R 和 Python(pandas)中的数据框架的命名列行。这种熟悉的高级抽象使开发工作变得更加容易,因为它让您可以像处理 SQL 表或 Excel 文件一样处理数据。此外,底层的 Catalyst 优化器编译操作并生成 JVM 字节码以进行高效执行。然而,命名列方法也带来了一个新问题。编译器不再具有静态类型信息,因此我们失去了编译时类型安全的优势。

数据集 API 被引入,以结合 RDD 和数据框架的最佳特性,以及一些自己的特性。数据集提供了类似数据框架的行和列数据抽象,但在其之上定义了一种结构。这种结构可以由 Scala 中的 case 类或 Java 中的类定义。它们提供了类型安全和类似 RDD 的 lambda 函数。因此,它们支持诸如mapgroupByKey之类的类型化方法,也支持诸如selectgroupBy之类的无类型方法。除了 Catalyst 优化器外,数据集还利用了 Tungsten 执行引擎提供的内存编码,进一步提高了性能。

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

现在,考虑到现有数据抽象的基础知识,让我们了解一下我们所说的统一数据访问平台到底是什么:

Apache Spark 中的数据抽象

统一平台背后的意图是它不仅可以让您将静态和流式数据结合在一起,还可以以统一的方式对数据进行各种不同类型的操作!从开发人员的角度来看,数据集是与之一起工作的核心抽象,而 Spark SQL 是与 Spark 功能交互的主要接口。与 SQL 声明式编程接口相结合的二维数据结构一直是处理数据的一种熟悉方式,从而缩短了数据工程师的学习曲线。因此,理解统一平台意味着理解数据集和 Spark SQL。

数据集

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

数据集抽象的核心是编码器。这些编码器在 JVM 对象和 Spark 内部 Tungsten 二进制格式之间进行转换。这种内部表示绕过了 JVM 的内存管理和垃圾回收。Spark 有自己的 C 风格内存访问,专门用于解决它支持的工作流类型。由此产生的内部表示占用更少的内存,并具有高效的内存管理。紧凑的内存表示导致在洗牌操作期间减少网络负载。编码器生成紧凑的字节码,直接在序列化对象上操作,而无需反序列化,从而提高性能。早期了解模式会导致在缓存数据集时内存布局更加优化。

使用数据集

在这一部分,我们将创建数据集并执行转换和操作,就像 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 简介。这些转换在现实世界中非常方便。考虑向不完整的数据添加结构(也称为案例类)。您可以首先将数据读入 DataFrame,进行清洗,然后将其转换为数据集。另一个用例可能是,您希望基于某些运行时信息(例如user_id)仅公开数据的子集(行和列)。您可以将数据读入 DataFrame,将其注册为临时表,应用条件,并将子集公开为数据集。以下示例首先创建一个DataFrame,然后将其转换为Dataset。请注意,DataFrame 列名必须与案例类匹配。

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

//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] 

解释命令的响应显示WholeStageCodegen,它将多个操作融合为单个 Java 函数调用。这通过减少多个虚拟函数调用来增强性能。自 1.1 以来,代码生成一直存在于 Spark 引擎中,但当时它仅限于表达式评估和一小部分操作,如过滤。相比之下,Tungsten 的整个阶段代码生成为整个查询计划生成代码。

从 JSON 创建数据集

数据集可以像 DataFrame 一样从 JSON 文件中创建。请注意,JSON 文件可能包含多个记录,但每个记录必须在一行上。如果您的源 JSON 有换行符,您必须以编程方式将其删除。JSON 记录可能包含数组并且可能是嵌套的。它们不需要具有统一的模式。以下示例文件包含具有附加标记和数据数组的 JSON 记录。

示例 4-从 JSON 创建数据集

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    | 
+------------------------+----------+---------+ 

数据集 API 的限制

尽管数据集 API 是使用 RDD 和 DataFrame 的最佳部分创建的,但在当前开发阶段仍存在一些限制:

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

  • Python 和 R 在本质上是动态的,因此类型化的数据集不适合。

Spark SQL

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

Spark 2.0 版本带来了 API 的显著统一和扩展的 SQL 功能,包括对子查询的支持。数据集 API 和 DataFrame API 现在是统一的,DataFrame 是数据集的一种“类型”。统一的 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 来说是不可用的。相比之下,当开发人员使用 DataFrames 和 Spark SQL 时,底层引擎具有关于模式和操作的信息。在这种情况下,开发人员可以编写更少的代码,而优化器会做所有的艰苦工作。

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

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

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

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

  • 内存布局是以列为基础的,内存中的 parquet 格式,因为这样可以实现矢量化处理,并且更接近通常的数据访问操作。

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

结构化流处理

流处理似乎是一个广泛的话题!如果您仔细观察现实世界的问题,企业不仅希望流处理引擎实时做出决策。一直以来,都需要集成批处理栈和流处理栈,并与外部存储系统和应用程序集成。此外,解决方案应该能够适应业务逻辑的动态变化,以满足新的和不断变化的业务需求。

Apache Spark 2.0 具有称为结构化流引擎的高级流处理 API 的第一个版本。这个可扩展和容错的引擎依赖于 Spark SQL API 来简化实时连续的大数据应用程序的开发。这可能是统一批处理和流处理计算的第一次成功尝试。

在技术层面上,结构化流依赖于 Spark SQL API,它扩展了数据框/数据集,我们在前面的部分已经讨论过。Spark 2.0 让您以统一的方式执行根本不同的活动,例如:

  • 构建 ML 模型并将其应用于流数据

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

  • 执行临时、交互和批量查询

  • 在运行时更改查询

  • 聚合数据流并使用 Spark SQL JDBC 提供服务

与其他流式引擎不同,Spark 允许您将实时流数据静态数据结合,并执行前述操作。

结构化流

从根本上讲,结构化流由 Spark SQL 的 Catalyst 优化器赋予了能力。因此,它使开发人员不必担心处理静态或实时数据流时使查询更有效的底层管道。

截至目前,Spark 2.0 的结构化流主要集中在 ETL 上,以后的版本将具有更多的操作符和库。

让我们看一个简单的例子。以下示例监听 Linux 上本地机器上的系统活动报告sar)并计算平均空闲内存。系统活动报告提供系统活动统计信息,当前示例收集内存使用情况,以 2 秒的间隔报告 20 次。Spark 流读取这个流式输出并计算平均内存。我们使用一个方便的网络实用工具netcatnc)将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 来处理静态和流数据。其想法是将实时数据流视为不断追加的表,如下图所示:

Spark 流式编程模型

因此,无论是静态数据还是流数据,您只需像在静态数据表上那样启动类似批处理的查询,Spark 会将其作为无界输入表上的增量查询运行,如下图所示:

Spark 流式编程模型

因此,开发人员以相同的方式在输入表上定义查询,无论是静态有界表还是动态无界表。让我们了解整个过程的各种技术术语,以了解它是如何工作的:

  • 输入:来自源的追加表的数据

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

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

  • 结果:每个触发间隔的结果表

  • 输出:选择在每个触发后写入数据接收器的结果的哪一部分

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

Spark 流式编程模型

提供:Databricks

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

Spark 流式编程模型

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

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

  • Complete:在外部存储中,整个更新的结果表将按原样写入。

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

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

底层原理

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

底层原理

请注意,Planner事先知道如何将流式Logical Plan转换为一系列Incremental Execution Plans。这可以用以下图表示:

底层原理

Planner可以轮询数据源以便以优化的方式规划执行。

与其他流式引擎的比较

我们已经讨论了结构化流的许多独特特性。现在让我们与其他可用的流式引擎进行比较:

与其他流式引擎的比较

提供:Databricks

连续应用程序

我们讨论了 Spark 如何赋予统一数据访问的能力。它让您以多种方式处理数据,通过启用各种分析工作负载来构建端到端的连续应用程序,例如 ETL 处理、adhoc 查询、在线机器学习建模,或生成必要的报告...所有这些都可以通过让您使用高级的、类似 SQL 的 API 来处理静态和流式数据的方式来统一进行,从而大大简化了实时连续应用程序的开发和维护。

连续应用程序

提供:Databricks

摘要

在本章中,我们讨论了统一数据访问的真正含义以及 Spark 如何实现这一目的。我们仔细研究了 Datasets API 以及实时流如何通过它得到增强。我们了解了 Datasets 的优势以及它们的局限性。我们还研究了连续应用程序背后的基本原理。

在下一章中,我们将探讨利用 Spark 平台进行规模化数据分析操作的各种方法。

参考文献

第五章:Spark 上的数据分析

大规模数据分析领域一直在不断发展。为数据分析开发了各种库和工具,具有丰富的算法集。与此同时,分布式计算技术也在不断发展,以便规模化处理大型数据集。这两种特征必须融合,这是开发 Spark 的主要意图。

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

作为本章的先决条件,对 DataFrame API 和统计基础的基本理解是有益的。然而,我们已经尽量简化内容,并详细介绍了一些重要的基础知识,以便任何人都可以开始使用 Spark 进行统计分析。本章涵盖的主题如下:

  • 数据分析生命周期

  • 数据获取

  • 数据准备

  • 数据整合

  • 数据清洗

  • 数据转换

  • 统计基础

  • 抽样

  • 数据分布

  • 描述性统计

  • 位置测量

  • 传播测量

  • 总结统计

  • 图形技术

  • 推断统计

  • 离散概率分布

  • 连续概率分布

  • 标准误差

  • 置信水平

  • 误差边界和置信区间

  • 总体变异性

  • 估计样本大小

  • 假设检验

  • 卡方检验

  • F 检验

  • 相关性

数据分析生命周期

对于大多数现实项目,需要遵循一定的步骤顺序。然而,对于数据分析和数据科学,没有普遍认可的定义或界限。一般来说,“数据分析”这个术语包括检查数据、发现有用见解和传达这些见解所涉及的技术和过程。术语“数据科学”可以最好地被视为一个跨学科领域,涵盖统计学计算机科学数学。这两个术语都涉及处理原始数据以获取知识或见解,通常是迭代的过程,有些人将它们互换使用。

根据不同的业务需求,有不同的解决问题的方式,但没有一个适合所有可能情况的唯一标准流程。典型的流程工作流程可以总结为制定问题、探索、假设、验证假设、分析结果,并重新开始的循环。这在下图中用粗箭头表示。从数据角度看,工作流程包括数据获取、预处理、数据探索、建模和传达结果。这在图中显示为圆圈。分析和可视化发生在每个阶段,从数据收集到结果传达。数据分析工作流程包括两个视图中显示的所有活动:

数据分析生命周期

整个生命周期中最重要的是提出的问题。可能包含答案(相关数据!)的数据紧随其后。根据问题,第一个任务是根据需要从一个或多个数据源收集正确的数据。组织通常维护数据湖,这是数据以其原始格式存储的巨大存储库。

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

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

本章的其余部分将更深入地探讨每个步骤以及如何使用 Spark 实现这些步骤。还包括一些统计学的基础知识,以便读者能够轻松地跟随代码片段。

数据获取

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

本节的大部分内容已经在上一章中涵盖了,概述了如何从不同的数据源获取数据并将数据存储在 DataFrames 中以便进行更轻松的分析。Spark 中有一种内置机制,可以从一些常见的数据源中获取数据,并为那些不受 Spark 支持的数据源提供数据源 API

为了更好地理解数据获取和准备阶段,让我们假设一个场景,并尝试用示例代码片段解决所有涉及的步骤。假设员工数据分布在本地 RDD、JSON 文件和 SQL 服务器上。那么,让我们看看如何将它们转换为 Spark DataFrames:

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_id name age role salary
1 John 25 职员 10,000 美元
2 Ray 35 经理 12,000 美元
3 Mike 24 经理 12,000 美元
4 Jane 28 职员 null
5 Kevin 26 经理 12,000 美元
6 Vincent 35 高级经理 22,000 美元
7 James 38 高级经理 20,000 美元
8 Shane 32 经理 12,000 美元
9 Larry 29 经理 10,000 美元
10 Kimberly 29 职员 8,000 美元
11 Alex 28 经理 12,000 美元
12 Garry 25 经理 12,000 美元
13 Max 31 经理 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)的完美例子。

统计学基础

统计学领域主要是关于使用数学程序以某种有意义的方式对数据集的原始事实和数字进行总结,以便您能够理解。这包括但不限于:收集数据,分析数据,解释数据和表示数据。

统计学领域的存在主要是因为通常不可能收集整个人口的数据。因此,使用统计技术,我们通过处理不确定性,利用样本统计来估计总体参数。

在本节中,我们将介绍一些基本的统计和分析技术,这些技术将帮助我们全面理解本书涵盖的概念。

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

  • 描述性统计

  • 推断统计

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

统计学基础

在开始这些工作之前,重要的是要对抽样和分布有一些了解。

抽样

通过抽样技术,我们只需取一部分人口数据集并对其进行处理:

抽样

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

  • 难以获取整个人口的数据;例如,任何国家的公民身高。

  • 难以处理整个数据集。当我们谈论像 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包,了解伯努利、泊松或随机抽样器。

数据分布

了解数据的分布是您需要执行的主要任务之一,以将数据转化为信息。分析变量的分布有助于检测异常值,可视化数据中的趋势,并且还可以塑造您对手头数据的理解。这有助于正确思考并采取正确的方法来解决业务问题。绘制分布使其在视觉上更直观,我们将在描述性统计部分中涵盖这一方面。

频率分布

频率分布解释了变量取值和它们出现的频率。通常用一个表格表示,其中包含每个可能的值及其相应的出现次数。

让我们考虑一个例子,我们掷一个六面骰子 100 次,并观察以下频率:

频率分布

频率表

同样,您可能会观察到每组 100 次掷骰子的不同分布,因为这将取决于机会。

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

频率分布

相对频率表

概率分布

在掷骰子的相同例子中,我们知道总概率为 1 分布在骰子的所有面上。这意味着 1/6(约 0.167)的概率与面 1 到面 6 相关联。无论你掷骰子的次数多少(一个公平的骰子!),1/6 的相同概率将均匀分布在骰子的所有面上。因此,如果你绘制这个分布,它将如下所示:

概率分布

概率分布

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

这个概率分布实际上是人口的分布。在现实世界中,有时我们对人口分布有先验知识(在我们的例子中,是一个公平骰子的六个面上的概率为 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|
+---+----+

传播措施

传播措施描述了特定变量或数据项的数据是多么接近或分散。

范围

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

方差

要找到数据集中的变异性,我们可以从平均值中减去每个值,将它们平方以消除负号(也扩大幅度),然后将它们全部相加并除以总值的数量:

方差

如果数据更分散,方差将是一个很大的数字。它的一个缺点是它给异常值赋予了不应有的权重。

标准差

与方差类似,标准差也是数据内部分散的一种度量。方差的局限性在于数据的单位也被平方,因此很难将方差与数据集中的值联系起来。因此,标准差被计算为方差的平方根:

标准差

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)

推断统计

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

  • 人口参数的估计

  • 假设检验

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

离散概率分布

离散概率分布用于对离散性数据进行建模,这意味着数据只能取特定的值,如整数。与分类变量不同,离散变量只能取数值数据,尤其是来自一组不同整数值的计数数据。此外,随机变量所有可能值的概率之和为 1。离散概率分布是用概率质量函数描述的。可以有各种类型的离散概率分布。以下是一些例子。

伯努利分布

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

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

伯努利分布

这种分布无法回答诸如学生的预期通过率之类的问题,因为预期值(μ)将是该分布无法取得的某个分数。它只能意味着如果你抽取 1,000 名学生,那么有 600 名会通过,400 名会不及格。

二项分布

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

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

μ = n * p

方差(σ2x)由以下公式给出:

σ2x = np(1-p).

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

二项分布二项分布

在这里,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个事件的概率可以用概率质量函数描述:

泊松分布

这里,k = 0, 1, 2, 3...

前面的方程描述了泊松分布。

对于泊松分布,均值和方差是相同的。此外,泊松分布在其均值或方差增加时更趋于对称。

示例问题

假设你知道工作日向消防站打电话的平均次数是 8。在给定的工作日中有 11 通电话的概率是多少?这个问题可以使用基于泊松分布的以下公式来解决:

示例问题

连续概率分布

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

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

可以有各种类型的连续概率分布。以下部分是一些示例。

正态分布

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

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

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

正态分布

正态分布之所以成为最受欢迎的分布之一,其中一个原因是中心极限定理CLT)。它规定,无论人口分布如何,从同一人口分布独立抽取的样本均值几乎呈正态分布,随着样本量的增加,这种正态性会越来越明显。这种行为实际上是统计假设检验的基础。

此外,每个正态分布,无论其均值和标准差如何,都遵循经验法则(68-95-99.7 法则),该法则规定曲线下约 68%的面积落在均值的一个标准差内,曲线下约 95%的面积落在均值的两个标准差内,曲线下约 99.7%的面积落在均值的三个标准差内。

现在,要找到事件的概率,可以使用积分微积分,也可以将分布转换为标准正态分布,如下一节所述。

标准正态分布

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

假设随机变量 X 正态分布,均值(μ)和标准差(σ),那么随机变量 Z 将具有均值 0 和标准差 1 的标准正态分布。可以找到 Z 的值如下:

标准正态分布

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

您可以在以下场景中找到正态分布的应用,其中一个想要找到落在指定范围内的百分比 - 假设分布近似正态。

考虑以下例子:

如果店主在某一天经营店铺的时间遵循均值为 8 小时和标准差为 0.5 小时的正态分布,那么他在店里待的时间少于 7.5 小时的概率是多少?

概率分布如下:

标准正态分布

数据分布

标准正态分布标准正态分布

标准正态分布

因此,店主在店里待的时间少于 7.5 小时的概率为:

P(z = -1) = 0.1587 = 15.87

注意

这是使用 Z-表找出的。

请注意,数据集中的正态性大多是一种近似。您首先需要检查数据的正态性,然后如果您的分析基于数据的正态性假设,可以进一步进行。有各种不同的检查正态性的方法:您可以选择直方图(使用数据的均值和标准差拟合的曲线)、正态概率图或 QQ 图。

卡方分布

卡方分布是统计推断中最广泛使用的分布之一。它是伽玛分布的特例,用于对不是负数的变量的偏斜分布进行建模。它规定,如果随机变量 X 正态分布,Z 是其标准正态变量之一,则 Z[2] 将具有一个自由度的 X[²] 分布。同样,如果我们从相同分布中取出许多这样的随机独立标准正态变量,对它们进行平方并相加,那么结果也将遵循 X[²] 分布,如下所示:

Z[12] + Z[22] + ... + Z[k2] 将具有 k 自由度的 X[2] 分布。

卡方分布主要用于推断给定样本方差或标准差的总体方差或总体标准差。这是因为 X[2]分布是用另一种方式定义的,即以样本方差与总体方差的比率来定义。

为了证明这一点,让我们从方差为卡方分布的正态分布中随机抽取一个样本(x[1], x[2],...,xn)。

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

卡方分布

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

卡方分布

考虑到前面提到的事实,我们可以定义卡方统计量如下:

卡方分布

(记住卡方分布Z[2]将具有 X[2]分布。)

所以,卡方分布

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

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

卡方分布

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

请注意,卡方分布呈正偏态,但偏斜度随着自由度的增加而减小,并趋近于正态分布。

样本问题

找到方差和标准差的 90%置信区间,以美元表示成成年人单张电影票的价格。给定的数据代表全国电影院的选定样本。假设变量服从正态分布。

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

解:

N = 10

样本均值:

样本问题

样本的方差:

样本问题

样本的标准差:

S = sqrt(9.61)

自由度:

10-1 = 9

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

样本问题

现在,让我们使用公式:

样本问题样本问题

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

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

因此,代入数字后,我们得到:

样本问题样本问题样本问题

因此,我们可以得出结论,我们有 90%的把握,认为全国电影票价格的标准差在 2.26 美元和 5.10 美元之间,基于对 10 个全国电影票价格的样本。

学生 t 分布

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

这个分布是钟形的,对称的,就像正态分布,但尾部更重。当样本量大时,t 分布变成正态分布。

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

样本均值将是学生 t 分布和样本方差学生 t 分布

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

学生 t 分布

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

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

现在,为了更清楚地说明问题,让我们回顾一下并考虑一下当总体σ已知时的情况。当总体正态分布时,样本均值大多数情况下也是正态分布的,无论样本大小和的任何线性变换,如学生 t 分布也会遵循正态分布。

如果总体不是正态分布呢?即使在这种情况下,当样本量足够大时,(即抽样分布)或学生 t 分布的分布也会遵循中心极限定理的正态分布!

另一种情况是总体σ未知。在这种情况下,如果总体正态分布,样本均值大多数情况下也是正态分布的,但随机变量学生 t 分布不会遵循正态分布;它遵循具有(n-1)自由度的 t 分布。原因是因为分母中S的随机性,对于不同的样本是不同的。

在上述情况下,如果总体不是正态分布,当样本量足够大时,学生 t 分布的分布将遵循中心极限定理的正态分布(而不是在样本量较小的情况下!)。因此,样本量足够大时,学生 t 分布的分布遵循正态分布,可以安全地假设它遵循 t 分布,因为 t 分布随着样本量的增加而接近正态分布。

F 分布

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

如果样本 1 的样本方差为F 分布,如果样本 2 的样本方差为F 分布,那么,F 分布将具有 F 分布(σ12 = σ22)。

从上述事实中,我们也可以说F 分布也将遵循 F 分布。

在前面的卡方分布部分,我们也可以说

F 分布也将具有n1-1n2-1自由度的 F 分布。对于这些自由度的每种组合,都会有不同的 F 分布。

标准误差

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

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

标准误差

n为样本量)

标准误差

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

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

置信水平

这是一个衡量你希望在通过样本统计估计总体参数时有多大把握(概率),以便期望值落入所需范围或置信区间的度量。它通过从显著水平(α)中减去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 年。

解决方案:

估计样本量估计样本量

假设检验

假设检验是关于检验对总体参数所做的假设。这有助于确定一个结果是否具有统计学意义或是偶然发生的。这是统计研究中最重要的工具。我们将讨论一些测试,以查看总体中变量之间的关系。

零假设和备择假设

零假设(表示为 H0)通常是关于总体参数的初始声明,大多数情况下表明没有影响没有关系。在我们的假设检验中,我们的目标是否定和拒绝零假设,以便接受备择假设(表示为 H1)。备择假设表明实验中的某种影响。在实验中,请注意,您要么拒绝零假设,要么未能拒绝零假设。如果您成功地拒绝了零假设,那么备择假设将被考虑,如果您未能拒绝零假设,则将被考虑零假设(尽管可能不是真的)。

因此,我们通常希望获得非常小的 P 值(低于定义的显著性水平α),以便拒绝零假设。如果 P 值大于α,则未能拒绝零假设。

卡方检验

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

输入数据类型决定是否进行适合度独立性检验,而无需明确指定它们作为开关。因此,如果您提供向量作为输入,则进行适合度检验,如果您提供矩阵作为输入,则进行独立性检验。在任何情况下,都需要提供作为输入的事件频率向量或列联表,您需要首先计算它们。让我们通过示例来探讨这些问题:

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) 。

相关性

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

在 Spark 中,这两种都受支持。如果您输入两个RDD[Double],输出是Double,如果您输入一个RDD[Vector],输出是相关矩阵。在 Scala 和 Python 的实现中,如果您没有提供相关性的类型作为输入,那么默认考虑的始终是 Pearson。

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 统计的 OOTB 库函数的详细信息:

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

第六章:机器学习

我们每天都在使用机器学习,无论我们是否注意到。例如,谷歌等电子邮件提供商会自动将一些收件箱中的邮件推送到“垃圾邮件”文件夹中,亚马逊等在线购物网站或 Facebook 等社交网络网站会提供出人意料的有用的推荐。那么,是什么使这些软件产品能够重新连接失散已久的朋友呢?这些只是机器学习在实际中的一些例子。

从形式上讲,机器学习是人工智能AI)的一部分,它处理一类可以从数据中学习并进行预测的算法。这些技术和基本概念来自统计学领域。机器学习存在于计算机科学和统计学的交叉点,被认为是数据科学中最重要的组成部分之一。它已经存在了一段时间,但随着数据量和可扩展性要求的增加,其复杂性也在增加。机器学习算法往往需要大量资源,并且具有迭代性质,这使它们不适合 MapReduce 范式。MapReduce 非常适用于单次遍历算法,但对于多次遍历的算法并不那么适用。Spark 研究项目正是为了解决这一挑战而启动的。Apache Spark 在其 MLlib 库中配备了高效的算法,即使在迭代计算需求下也能表现良好。

上一章概述了数据分析的生命周期及其各个组成部分,如数据清洗、数据转换、抽样技术和可视化数据的图形技术,以及涵盖描述性统计和推断统计的概念。我们还研究了一些可以在 Spark 平台上执行的统计测试。在上一章中建立的基础上,我们将在本章中涵盖大部分机器学习算法以及如何使用它们在 Spark 上构建模型。

作为本章的先决条件,对机器学习算法和计算机科学基础的基本理解是很有帮助的。然而,我们已经涵盖了一些算法的理论基础,并配以一套合适的实际例子,使这些更易于理解和实施。本章涵盖的主题有:

  • 机器学习介绍

  • 演变

  • 监督学习

  • 无监督学习

  • MLlib 和 Pipeline API

  • MLlib

  • ML 管道

  • 机器学习介绍

  • 参数方法

  • 非参数方法

  • 回归方法

  • 线性回归

  • 回归正则化

  • 分类方法

  • 逻辑回归

  • 线性支持向量机(SVM)

  • 决策树

  • 不纯度度量

  • 停止规则

  • 分裂候选

  • 决策树的优势

  • 例子

  • 集成

  • 随机森林

  • 梯度提升树

  • 多层感知器分类器

  • 聚类技术

  • K 均值聚类

  • 总结

介绍

机器学习就是通过示例数据进行学习的过程;这些示例为给定输入产生特定输出。机器学习有各种各样的商业用例。让我们看一些例子,以了解它到底是什么:

  • 推荐引擎,推荐用户可能感兴趣的购买商品

  • 客户细分(将具有相似特征的客户分组)用于营销活动

  • 癌症的疾病分类-恶性/良性

  • 预测建模,例如,销售预测,天气预测

  • 绘制业务推论,例如,了解产品价格变化对销售的影响

演变

统计学习的概念甚至在第一台计算机系统出现之前就已存在。在 19 世纪,最小二乘法(现在称为线性回归)已经被发展出来。对于分类问题,费舍尔提出了线性判别分析LDA)。大约在 20 世纪 40 年代,LDA 的替代方案,即逻辑回归,被提出,所有这些方法不仅随着时间的推移得到改进,而且还激发了其他新算法的发展。

在那些时代,计算是一个大问题,因为它是用纸和笔完成的。因此,拟合非线性方程并不太可行,因为它需要大量的计算。20 世纪 80 年代后,随着技术的改进和计算机系统的引入,分类/回归树被引入。随着技术和计算系统的进一步发展,统计学习在某种程度上与现在所称的机器学习融合在一起。

监督学习

如前一节所讨论的,机器学习完全是基于示例数据的学习。根据算法如何理解数据并对其进行训练,它们大致分为两类:监督学习无监督学习

监督统计学习涉及基于一个或多个输入构建模型以获得特定输出。这意味着我们获得的输出可以根据我们提供的输入监督我们的分析。换句话说,对于预测变量的每个观察(例如年龄、教育和费用变量),都有一个相关的结果变量的响应测量(例如工资)。参考以下表格,以了解我们正在尝试根据年龄教育费用变量预测工资的示例数据集:

监督学习

监督算法可用于预测、估计、分类和其他类似要求,我们将在以下部分进行介绍。

无监督学习

无监督统计学习涉及基于一个或多个输入构建模型,但没有产生特定输出的意图。这意味着没有明确的响应/输出变量需要预测;但输出通常是共享某些相似特征的数据点的组。与监督学习不同,您不知道要将数据点分类到哪些组/标签中,而是让算法自行决定。

在这里,没有“训练”数据集的概念,该数据集用于通过构建模型将结果变量与“预测”变量相关联,然后使用“测试”数据集验证模型。无监督算法的输出不能监督您基于您提供的输入进行分析。这样的算法可以从数据中学习关系和结构。聚类关联规则学习是无监督学习技术的例子。

以下图像描述了聚类如何用于将共享某些相似特征的数据项分组:

无监督学习

MLlib 和管道 API

让我们首先学习一些 Spark 基础知识,以便能够在其上执行机器学习操作。我们将在本节讨论 MLlib 和管道 API。

MLlib

MLlib 是建立在 Apache Spark 之上的机器学习库,其中包含大多数可以大规模实施的算法。MLlib 与 GraphX、SQL 和 Streaming 等其他组件的无缝集成为开发人员提供了一个相对容易地组装复杂、可扩展和高效的工作流的机会。MLlib 库包括常见的学习算法和实用程序,包括分类、回归、聚类、协同过滤和降维。

MLlib 与spark.ml包配合使用,后者提供了高级 Pipeline API。这两个包之间的基本区别在于 MLlib(spark.mllib)在 RDD 之上工作,而 ML(spark.ml)包在 DataFrame 之上工作,并支持 ML Pipeline。目前,Spark 支持这两个包,但建议使用spark.ml包。

此库中的基本数据类型是向量和矩阵。向量是本地的,可以是密集的或稀疏的。密集向量存储为值数组。稀疏向量存储为两个数组;第一个数组存储非零值索引,第二个数组存储实际值。所有元素值都存储为双精度浮点数,索引存储为从零开始的整数。了解基本结构对于有效使用库非常重要,它应该有助于从头开始编写任何新算法。让我们看一些示例代码,以更好地理解这两种向量表示:

Scala

//Create vectors
scala> import org.apache.spark.ml.linalg.{Vector, Vectors}
import org.apache.spark.ml.linalg.{Vector, Vectors}

//Create dense vector
scala> val dense_v: Vector = Vectors.dense(10.0,0.0,20.0,30.0,0.0)
dense_v: org.apache.spark.ml.linalg.Vector = [10.0,0.0,20.0,30.0,0.0]
scala>

//Create sparse vector: pass size, position index array and value array
scala> val sparse_v1: Vector = Vectors.sparse(5,Array(0,2,3),
       Array(10.0,20.0,30.0))
sparse_v1: org.apache.spark.ml.linalg.Vector = (5,[0,2,3],[10.0,20.0,30.0])
scala>

//Another way to create sparse vector with position, value tuples
scala> val sparse_v2: Vector = Vectors.sparse(5,
        Seq((0,10.0),(2,20.0),(3,30.0)))
sparse_v2: org.apache.spark.ml.linalg.Vector = (5,[0,2,3],[10.0,20.0,30.0])
scala>  
 Compare vectors 
--------------- cala> sparse_v1 == sparse_v2
res0: Boolean = true
scala> sparse_v1 == dense_v
res1: Boolean = true      //All three objects are equal but...
scala> dense_v.toString()
res2: String = [10.0,0.0,20.0,30.0,0.0]
scala> sparse_v2.toString()
res3: String = (5,[0,2,3],[10.0,20.0,30.0]) //..internal representation
differs
scala> sparse_v2.toArray
res4: Array[Double] = Array(10.0, 0.0, 20.0, 30.0, 0.0)

Interchangeable ---------------
scala> dense_v.toSparse
res5: org.apache.spark.mllib.linalg.SparseVector = (5,[0,2,3]
[10.0,20.0,30.0])
scala> sparse_v1.toDense
res6: org.apache.spark.mllib.linalg.DenseVector = [10.0,0.0,20.0,30.0,0.0]
scala>

A common operation ------------------
scala> Vectors.sqdist(sparse_v1,
        Vectors.dense(1.0,2.0,3.0,4.0,5.0))
res7: Double = 1075.0

Python:

//Create vectors
>>> from pyspark.ml.linalg import Vector, Vectors
//Create vectors
>>> dense_v = Vectors.dense(10.0,0.0,20.0,30.0,0.0)
//Pass size, position index array and value array
>>> sparse_v1 = Vectors.sparse(5,[0,2,3],
                    [10.0,20.0,30.0])
>>> 

//Another way to create sparse vector with position, value tuples
>>> sparse_v2 = Vectors.sparse(5,
                  [[0,10.0],[2,20.0],[3,30.0]])
>>> 

Compare vectors 
--------------- >>> sparse_v1 == sparse_v2
True
>>> sparse_v1 == dense_v
True      //All three objects are equal but...
>>> dense_v
DenseVector([10.0, 0.0, 20.0, 30.0, 0.0])
>>> sparse_v1
SparseVector(5, {0: 10.0, 2: 20.0, 3: 30.0}) //..internal representation
differs
>>> sparse_v2
SparseVector(5, {0: 10.0, 2: 20.0, 3: 30.0})

Interchangeable 
---------------- //Note: as of Spark 2.0.0, toDense and toSparse are not available in pyspark
 A common operation 
------------------- >>> Vectors.squared_distance(sparse_v1,
        Vectors.dense(1.0,2.0,3.0,4.0,5.0))
1075.0

矩阵可以是本地的或分布式的,密集的或稀疏的。本地矩阵存储在单个机器上作为一维数组。密集本地矩阵按列主序存储(列成员是连续的),而稀疏矩阵值以压缩稀疏列CSC)格式按列主序存储。在这种格式中,矩阵以三个数组的形式存储。第一个数组包含非零值的行索引,第二个数组包含每列的起始值索引,第三个数组是所有非零值的数组。索引的类型为从零开始的整数。第一个数组包含从零到行数减一的值。第三个数组的元素类型为双精度浮点数。第二个数组需要一些解释。该数组中的每个条目对应于每列中第一个非零元素的索引。例如,假设在一个 3 乘 3 的矩阵中每列只有一个非零元素。那么第二个数组的元素将包含 0,1,2。第一个数组包含行位置,第三个数组包含三个值。如果某列中的元素都不是非零的,你会注意到第二个数组中重复相同的索引。让我们看一些示例代码:

Scala:

scala> import org.apache.spark.ml.linalg.{Matrix,Matrices}
import org.apache.spark.ml.linalg.{Matrix, Matrices}

Create dense matrix 
------------------- //Values in column major order
Matrices.dense(3,2,Array(9.0,0,0,0,8.0,6))
res38: org.apache.spark.mllib.linalg.Matrix =
9.0  0.0
0.0  8.0
0.0  6.0
 Create sparse matrix 
-------------------- //1.0 0.0 4.0
0.0 3.0 5.0
2.0 0.0 6.0//
val sm: Matrix = Matrices.sparse(3,3,
        Array(0,2,3,6), Array(0,2,1,0,1,2),
        Array(1.0,2.0,3.0,4.0,5.0,6.0))
sm: org.apache.spark.mllib.linalg.Matrix =
3 x 3 CSCMatrix
(0,0) 1.0
(2,0) 2.0
(1,1) 3.0
(0,2) 4.0
(1,2) 5.0
(2,2) 6.0
 Sparse matrix, a column of all zeros 
------------------------------------ //third column all zeros
Matrices.sparse(3,4,Array(0,2,3,3,6),
    Array(0,2,1,0,1,2),values).toArray
res85: Array[Double] = Array(1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0,
4.0, 5.0, 6.0)

Python:

//Create dense matrix
>>> from pyspark.ml.linalg import Matrix, Matrices

//Values in column major order
>>> Matrices.dense(3,2,[9.0,0,0,0,8.0,6])
DenseMatrix(3, 2, [9.0, 0.0, 0.0, 0.0, 8.0, 6.0], False)
>>> 

//Create sparse matrix
//1.0 0.0 4.0
0.0 3.0 5.0
2.0 0.0 6.0//
>>> sm = Matrices.sparse(3,3,
        [0,2,3,6], [0,2,1,0,1,2],
        [1.0,2.0,3.0,4.0,5.0,6.0])
>>> 

//Sparse matrix, a column of all zeros
//third column all zeros
>>> Matrices.sparse(3,4,[0,2,3,3,6],
        [0,2,1,0,1,2],
    values=[1.0,2.0,3.0,4.0,5.0,6.0]).toArray()
array([[ 1.,  0.,  0.,  4.],
       [ 0.,  3.,  0.,  5.],
       [ 2.,  0.,  0.,  6.]])
>>> 

分布式矩阵是最复杂的,选择正确的分布式矩阵类型非常重要。分布式矩阵由一个或多个 RDD 支持。行和列的索引类型为long,以支持非常大的矩阵。分布式矩阵的基本类型是RowMatrix,它简单地由其行的 RDD 支持。

每一行依次是一个本地向量。当列数非常低时,这是合适的。记住,我们需要传递 RDD 来创建分布式矩阵,不像本地矩阵。让我们看一个例子:

Scala:

scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
import org.apache.spark.mllib.linalg.{Vector, Vectors}
scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.mllib.linalg.distributed.RowMatrix

scala>val dense_vlist: Array[Vector] = Array(
    Vectors.dense(11.0,12,13,14),
    Vectors.dense(21.0,22,23,24),
    Vectors.dense(31.0,32,33,34))
dense_vlist: Array[org.apache.spark.mllib.linalg.Vector] =
Array([11.0,12.0,13.0,14.0], [21.0,22.0,23.0,24.0], [31.0,32.0,33.0,34.0])
scala>

//Distribute the vector list
scala> val rows  = sc.parallelize(dense_vlist)
rows: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] =
ParallelCollectionRDD[0] at parallelize at <console>:29
scala> val m: RowMatrix = new RowMatrix(rows)
m: org.apache.spark.mllib.linalg.distributed.RowMatrix =
org.apache.spark.mllib.linalg.distributed.RowMatrix@5c5043fe
scala> print("Matrix size is " + m.numRows()+"X"+m.numCols())
Matrix size is 3X4
scala>

Python:

>>> from pyspark.mllib.linalg import Vector,Vectors
>>> from pyspark.mllib.linalg.distributed import RowMatrix

>>> dense_vlist = [Vectors.dense(11.0,12,13,14),
         Vectors.dense(21.0,22,23,24), Vectors.dense(31.0,32,33,34)]
>>> rows  = sc.parallelize(dense_vlist)
>>> m = RowMatrix(rows)
>>> "Matrix size is {0} X {1}".format(m.numRows(), m.numCols())
'Matrix size is 3 X 4'

IndexedRowMatrix将行索引前缀到行条目中。这在执行连接时非常有用。您需要传递IndexedRow对象来创建IndexedRowMatrixIndexedRow对象是一个包装器,带有长Index和一组行元素的Vector

CoordinatedMatrix将数据存储为行、列索引和元素值的元组。BlockMatrix表示分布式矩阵,以本地矩阵块的形式存储。提供了从一种类型转换为另一种类型的方法,但这些是昂贵的操作,应谨慎使用。

ML pipeline

现实生活中的机器学习工作流程是数据提取、数据清洗、预处理、探索、特征提取、模型拟合和评估的迭代循环。Spark 上的 ML Pipeline 是用户设置复杂 ML 工作流的简单 API。它旨在解决一些痛点,如参数调整,或基于数据不同拆分(交叉验证)或不同参数集训练多个模型。编写脚本来自动化整个过程不再是必需的,可以在 Pipeline API 中处理。

Pipeline API 由一系列流水线阶段(实现为transformersestimators等抽象)组成,以按所需顺序执行。

在 ML Pipeline 中,您可以调用前一章中讨论的数据清洗/转换函数,并调用 MLlib 中可用的机器学习算法。这可以以迭代的方式进行,直到获得所需的模型性能。

ML 流水线

Transformer

Transformer 是一个抽象,实现transform()方法将一个 DataFrame 转换为另一个。如果该方法是特征转换器,则生成的 DataFrame 可能包含基于您执行的操作的一些额外转换列。但是,如果该方法是学习模型,则生成的 DataFrame 将包含一个带有预测结果的额外列。

Estimator

Estimator 是一个抽象,可以是任何实现fit()方法以在 DataFrame 上进行训练以生成模型的学习算法。从技术上讲,该模型是给定 DataFrame 的 transformer。

示例:逻辑回归是一种学习算法,因此是一个 estimator。调用fit()训练逻辑回归模型,这是一个结果模型,因此是一个 transformer,可以生成包含预测列的 DataFrame。

以下示例演示了一个简单的单阶段流水线。

Scala:

//Pipeline example with single stage to illustrate syntax
scala> import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.feature._
import org.apache.spark.ml.feature._

//Create source data frame
scala> val df = spark.createDataFrame(Seq(
         ("Oliver Twist","Charles Dickens"),
        ("Adventures of Tom Sawyer","Mark Twain"))).toDF(
        "Title","Author")

//Split the Title to tokens
scala> val tok = new Tokenizer().setInputCol("Title").
          setOutputCol("words")
tok: org.apache.spark.ml.feature.Tokenizer = tok_2b2757a3aa5f

//Define a pipeline with a single stage
scala> val p = new Pipeline().setStages(Array(tok))
p: org.apache.spark.ml.Pipeline = pipeline_f5e0de400666

//Run an Estimator (fit) using the pipeline
scala> val model = p.fit(df)
model: org.apache.spark.ml.PipelineModel = pipeline_d00989625bb2

//Examine stages
scala> p.getStages   //Returns a list of stage objects
res1: Array[org.apache.spark.ml.PipelineStage] = Array(tok_55af0061af6d)

// Examine the results
scala> val m = model.transform(df).select("Title","words")
m: org.apache.spark.sql.DataFrame = [Title: string, words: array<string>]
scala> m.select("words").collect().foreach(println)
[WrappedArray(oliver, twist)]
[WrappedArray(adventures, of, tom, sawyer)]

Python:

//Pipeline example with single stage to illustrate syntax
//Create source data frame
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import Tokenizer
>>>  df = sqlContext.createDataFrame([
    ("Oliver Twist","Charles Dickens"),
    ("Adventures of Tom Sawyer","Mark Twain")]).toDF("Title","Author")
>>> 

//Split the Title to tokens
>>> tok = Tokenizer(inputCol="Title",outputCol="words")

//Define a pipeline with a single stage
>>> p = Pipeline(stages=[tok])

//Run an Estimator (fit) using the pipeline
>>> model = p.fit(df)

//Examine stages
>>> p.getStages()  //Returns a list of stage objects
[Tokenizer_4f35909c4c504637a263]

// Examine the results
>>> m = model.transform(df).select("Title","words")
>>> [x[0] for x in m.select("words").collect()]
[[u'oliver', u'twist'], [u'adventures', u'of', u'tom', u'sawyer']]
>>> 

上面的示例展示了流水线的创建和执行,尽管只有一个阶段,在这种情况下是一个分词器。Spark 提供了几种“特征转换器”作为开箱即用的功能。这些特征转换器在数据清洗和数据准备阶段非常方便。

以下示例展示了将原始文本转换为特征向量的真实示例。如果您对 TF-IDF 不熟悉,请阅读来自www.tfidf.com的简短教程。

Scala:

scala> import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.feature._
import org.apache.spark.ml.feature._
scala> 

//Create a dataframe
scala> val df2 = spark.createDataset(Array(
         (1,"Here is some text to illustrate pipeline"),
         (2, "and tfidf, which stands for term frequency inverse document
frequency"
         ))).toDF("LineNo","Text")

//Define feature transformations, which are the pipeline stages
// Tokenizer splits text into tokens
scala> val tok = new Tokenizer().setInputCol("Text").
             setOutputCol("Words")
tok: org.apache.spark.ml.feature.Tokenizer = tok_399dbfe012f8

// HashingTF maps a sequence of words to their term frequencies using hashing
// Larger value of numFeatures reduces hashing collision possibility
scala> val tf = new HashingTF().setInputCol("Words").setOutputCol("tf").setNumFeatures(100)
tf: org.apache.spark.ml.feature.HashingTF = hashingTF_e6ad936536ea
// IDF, Inverse Docuemnt Frequency is a statistical weight that reduces weightage of commonly occuring words
scala> val idf = new IDF().setInputCol("tf").setOutputCol("tf_idf")
idf: org.apache.spark.ml.feature.IDF = idf_8af1fecad60a
// VectorAssembler merges multiple columns into a single vector column
scala> val va = new VectorAssembler().setInputCols(Array("tf_idf")).setOutputCol("features")
va: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_23205c3f92c8
//Define pipeline
scala> val tfidf_pipeline = new Pipeline().setStages(Array(tok,tf,idf,va))
val tfidf_pipeline = new Pipeline().setStages(Array(tok,tf,idf,va))
scala> tfidf_pipeline.getStages
res2: Array[org.apache.spark.ml.PipelineStage] = Array(tok_399dbfe012f8, hashingTF_e6ad936536ea, idf_8af1fecad60a, vecAssembler_23205c3f92c8)
scala>

//Now execute the pipeline
scala> val result = tfidf_pipeline.fit(df2).transform(df2).select("words","features").first()
result: org.apache.spark.sql.Row = [WrappedArray(here, is, some, text, to, illustrate, pipeline),(100,[0,3,35,37,69,81],[0.4054651081081644,0.4054651081081644,0.4054651081081644,0.4054651081081644,0.4054651081081644,0.4054651081081644])]

Python:

//A realistic, multi-step pipeline that converts text to TF_ID
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import Tokenizer, HashingTF, IDF, VectorAssembler, \
               StringIndexer, VectorIndexer

//Create a dataframe
>>> df2 = sqlContext.createDataFrame([
    [1,"Here is some text to illustrate pipeline"],
    [2,"and tfidf, which stands for term frequency inverse document
frequency"
    ]]).toDF("LineNo","Text")

//Define feature transformations, which are the pipeline stages
//Tokenizer splits text into tokens
>>> tok = Tokenizer(inputCol="Text",outputCol="words")

// HashingTF maps a sequence of words to their term frequencies using
hashing

// Larger the numFeatures, lower the hashing collision possibility
>>> tf = HashingTF(inputCol="words", outputCol="tf",numFeatures=1000)

// IDF, Inverse Docuemnt Frequency is a statistical weight that reduces
weightage of commonly occuring words
>>> idf = IDF(inputCol = "tf",outputCol="tf_idf")

// VectorAssembler merges multiple columns into a single vector column
>>> va = VectorAssembler(inputCols=["tf_idf"],outputCol="features")

//Define pipeline
>>> tfidf_pipeline = Pipeline(stages=[tok,tf,idf,va])
>>> tfidf_pipeline.getStages()
[Tokenizer_4f5fbfb6c2a9cf5725d6, HashingTF_4088a47d38e72b70464f, IDF_41ddb3891541821c6613, VectorAssembler_49ae83b800679ac2fa0e]
>>>

//Now execute the pipeline
>>> result = tfidf_pipeline.fit(df2).transform(df2).select("words","features").collect()
>>> [(x[0],x[1]) for x in result]
[([u'here', u'is', u'some', u'text', u'to', u'illustrate', u'pipeline'], SparseVector(1000, {135: 0.4055, 169: 0.4055, 281: 0.4055, 388: 0.4055, 400: 0.4055, 603: 0.4055, 937: 0.4055})), ([u'and', u'tfidf,', u'which', u'stands', u'for', u'term', u'frequency', u'inverse', u'document', u'frequency'], SparseVector(1000, {36: 0.4055, 188: 0.4055, 333: 0.4055, 378: 0.4055, 538: 0.4055, 597: 0.4055, 727: 0.4055, 820: 0.4055, 960: 0.8109}))]
>>> 

此示例已创建并执行了一个多阶段流水线,将文本转换为可以由机器学习算法处理的特征向量。在我们继续之前,让我们看看更多功能。

Scala:

scala> import org.apache.spark.ml.feature._
import org.apache.spark.ml.feature._
scala>

//Basic examples illustrating features usage
//Look at model examples for more feature examples
//Binarizer converts continuous value variable to two discrete values based on given threshold
scala> import scala.util.Random
import scala.util.Random
scala> val nums = Seq.fill(10)(Random.nextDouble*100)
...
scala> val numdf = spark.createDataFrame(nums.map(Tuple1.apply)).toDF("raw_nums")
numdf: org.apache.spark.sql.DataFrame = [raw_nums: double]
scala> val binarizer = new Binarizer().setInputCol("raw_nums").
            setOutputCol("binary_vals").setThreshold(50.0)
binarizer: org.apache.spark.ml.feature.Binarizer = binarizer_538e392f56db
scala> binarizer.transform(numdf).select("raw_nums","binary_vals").show(2)
+------------------+-----------+
|          raw_nums|binary_vals|
+------------------+-----------+
|55.209245003482884|        1.0|
| 33.46202184060426|        0.0|
+------------------+-----------+
scala>

//Bucketizer to convert continuous value variables to desired set of discrete values
scala> val split_vals:Array[Double] = Array(0,20,50,80,100) //define intervals
split_vals: Array[Double] = Array(0.0, 20.0, 50.0, 80.0, 100.0)
scala> val b = new Bucketizer().
           setInputCol("raw_nums").
           setOutputCol("binned_nums").
           setSplits(split_vals)
b: org.apache.spark.ml.feature.Bucketizer = bucketizer_a4dd599e5977
scala> b.transform(numdf).select("raw_nums","binned_nums").show(2)
+------------------+-----------+
|          raw_nums|binned_nums|
+------------------+-----------+
|55.209245003482884|        2.0|
| 33.46202184060426|        1.0|
+------------------+-----------+
scala>

//Bucketizer is effectively equal to binarizer if only two intervals are
given 
scala> new Bucketizer().setInputCol("raw_nums").
        setOutputCol("binned_nums").setSplits(Array(0,50.0,100.0)).
        transform(numdf).select("raw_nums","binned_nums").show(2)
+------------------+-----------+
|          raw_nums|binned_nums|
+------------------+-----------+
|55.209245003482884|        1.0|
| 33.46202184060426|        0.0|
+------------------+-----------+
scala>

Python:

//Some more features
>>> from pyspark.ml import feature, pipeline
>>> 

//Basic examples illustrating features usage
//Look at model examples for more examples
//Binarizer converts continuous value variable to two discrete values based on given threshold
>>> import random
>>> nums = [random.random()*100 for x in range(1,11)]
>>> numdf = sqlContext.createDataFrame(
             [[x] for x in nums]).toDF("raw_nums")
>>> binarizer = feature.Binarizer(threshold= 50,
       inputCol="raw_nums", outputCol="binary_vals")
>>> binarizer.transform(numdf).select("raw_nums","binary_vals").show(2)
+------------------+-----------+
|          raw_nums|binary_vals|
+------------------+-----------+
| 95.41304359504672|        1.0|
|41.906045589243405|        0.0|
+------------------+-----------+
>>> 

//Bucketizer to convert continuous value variables to desired set of discrete values
>>> split_vals = [0,20,50,80,100] //define intervals
>>> b =
feature.Bucketizer(inputCol="raw_nums",outputCol="binned_nums",splits=split
vals)
>>> b.transform(numdf).select("raw_nums","binned_nums").show(2)
+------------------+-----------+
|          raw_nums|binned_nums|
+------------------+-----------+
| 95.41304359504672|        3.0|
|41.906045589243405|        1.0|
+------------------+-----------+

//Bucketizer is effectively equal to binarizer if only two intervals are
given 
>>> feature.Bucketizer(inputCol="raw_nums",outputCol="binned_nums",                  
                       splits=[0,50.0,100.0]).transform(numdf).select(
                       "raw_nums","binned_nums").show(2)
+------------------+-----------+
|          raw_nums|binned_nums|
+------------------+-----------+
| 95.41304359504672|        1.0|
|41.906045589243405|        0.0|
+------------------+-----------+
>>> 

机器学习简介

在本书的前几节中,我们学习了响应/结果变量如何与预测变量相关联,通常在监督学习环境中。这些类型的变量人们现在使用各种不同的名称。让我们看看它们的一些同义词,并在书中交替使用它们:

  • 输入变量(X):特征,预测变量,解释变量,自变量

  • 输出变量(Y):响应变量,因变量

如果YX之间存在关系,其中X=X[1], X[2], X[3],..., X[n](n 个不同的预测变量),则可以写成如下形式:

机器学习简介

这里机器学习简介是一个表示X描述Y且未知的函数!这是我们使用手头观察到的数据点来找出的。术语

机器学习简介

是一个均值为零且与X无关的随机误差项。

与这样一个方程相关的基本上有两种类型的错误 - 可减少的错误和不可减少的错误。顾名思义,可减少的错误与函数相关,可以通过提高准确性来最小化

机器学习简介

通过使用更好的学习算法或调整相同的算法。由于Y也是一个函数

机器学习简介

,这是独立于X的,仍然会有一些与之相关的错误,无法解决。这被称为不可减少的错误(

机器学习简介

)。总是有一些因素影响结果变量,但在建模时未考虑(因为大多数情况下它们是未知的),并且导致不可减少的错误项。因此,我们在本书中讨论的方法只关注最小化可减少的错误。

我们构建的大多数机器学习模型可以用于预测或推断,或者两者结合。对于一些算法,函数

机器学习简介

可以表示为一个方程,告诉我们因变量Y如何与自变量(X1X2,...,Xn)相关。在这种情况下,我们既可以进行推断,也可以进行预测。然而,一些算法是黑匣子,我们只能进行预测,无法进行推断,因为YX的关系是未知的。

请注意,线性机器学习模型可能更适合推断设置,因为它们对业务用户更具可解释性。然而,在预测设置中,可能有更好的算法提供更准确的预测,但它们的可解释性较差。当推断是目标时,我们应该更喜欢使用诸如线性回归之类的限制性模型,以获得更好的可解释性,而当只有预测是目标时,我们可以选择使用高度灵活的模型,例如支持向量机SVM),这些模型不太可解释,但更准确(然而,这在所有情况下可能并不成立)。在选择算法时,您需要根据业务需求来权衡可解释性和准确性之间的权衡。让我们深入了解这些概念背后的基本原理。

基本上,我们需要一组数据点(训练数据)来构建一个模型来估计

机器学习简介

(X),以便Y =

机器学习简介

(X)。广义上说,这样的学习方法可以是参数化的,也可以是非参数化的。

参数方法

参数方法遵循两步过程。在第一步中,您假设

参数方法

()。例如,XY呈线性关系,因此X的函数,即

参数方法

(X),可以用下面显示的线性方程表示:

参数方法

模型选择后,第二步是通过使用手头的数据点来训练模型来估计参数β0β1,...,βn,以便:

参数方法

这种参数化方法的一个缺点是我们对于参数方法 () 在现实生活中的情况下可能不成立。

非参数方法

我们不对YX之间的线性关系以及变量的数据分布做任何假设,因此

非参数方法

() 在非参数化中。因为它不假设任何形式的

非参数方法

(),通过与数据点很好地拟合,可以产生更好的结果,这可能是一个优势。

因此,与参数方法相比,非参数方法需要更多的数据点来估计

非参数方法

()准确。但是请注意,如果处理不当,它可能会导致过度拟合问题。随着我们的进展,我们将更多地讨论这个问题。

回归方法

回归方法是一种监督学习的类型。如果响应变量是定量/连续的(取数值,如年龄、工资、身高等),则无论解释变量的类型如何,问题都可以称为回归问题。有各种建模技术来解决回归问题。在本节中,我们将重点放在线性回归技术和一些不同的变体上。

回归方法可用于预测任何实值结果。以下是一些例子:

  • 根据教育水平、地点、工作类型等预测员工的工资

  • 预测股票价格

  • 预测客户的购买潜力

  • 预测机器故障前需要的时间

线性回归

在前一节参数方法中讨论的内容之后,假设线性是

线性回归

(X),我们需要训练数据来拟合一个描述解释变量(表示为X)和响应变量(表示为Y)之间关系的模型。当只有一个解释变量时,称为简单线性回归,当有多个解释变量时,称为多元线性回归。简单线性回归就是在二维设置中拟合一条直线,当有两个预测变量时,它将在三维设置中拟合一个平面,以此类推,当有两个以上的变量时,它将在更高维的设置中拟合一个平面。

线性回归方程的通常形式可以表示为:

Y' =

线性回归

(X) +

线性回归

这里Y'代表了预测的结果变量。

只有一个预测变量的线性回归方程可以表示为:

线性回归

具有多个预测变量的线性回归方程可以表示为:

线性回归

这里线性回归是与X无关的不可减小的误差项,均值为零。我们无法控制它,但我们可以努力优化

线性回归

(X)。由于没有任何模型可以达到 100%的准确性,总会有一些与之相关的误差,因为不可减小的误差组成部分(

线性回归

)。

拟合线性回归最常见的方法称为最小二乘法,也称为普通最小二乘法OLS)方法。该方法通过最小化每个数据点到回归线的垂直偏差的平方和来找到最适合观察数据点的回归线。为了更好地理解线性回归的工作原理,让我们现在看一个简单线性回归的形式:

线性回归

其中,β0是回归线的 Y 截距,β1定义了线的斜率。这意味着β1X每变化一个单位时Y的平均变化。让我们举个XY的例子:

X Y
1 12
2 20
3 13
4 38
5 27

如果我们通过前面表格中显示的数据点拟合一条线性回归线,那么它将如下所示:

线性回归

上图中的红色垂直线表示预测误差,可以定义为实际 Y 值与预测 Y'值之间的差异。如果平方这些差异并将它们相加,就称为残差平方和(SSE),这是用于找到最佳拟合线的最常用的度量。下表显示了如何计算 SSE:

X Y Y' Y-Y' (Y-Y') 2
1 12 12.4 0.4 0.16
2 20 17.2 2.8 7.84
3 13 22 -9 81
4 38 26.8 11.2 125.44
5 27 31.6 -4.6 21.16
总和 235.6

在上表中,术语(Y-Y')称为残差。残差平方和(RSS)可以表示为:

RSS = 残差[1]² + 残差[2]² + 残差[3]² + ......+ 残差[n]²

请注意,回归对异常值非常敏感,如果在应用回归之前不加以处理,可能会引入巨大的 RSS 误差。

在观察到的数据点中拟合回归线后,应该通过将它们在 Y 轴上绘制出来,并将解释变量放在 X 轴上来检查残差。如果图表几乎是一条直线,那么你对线性关系的假设是有效的,否则可能表明存在某种非线性关系。在存在非线性关系的情况下,可能需要考虑非线性。其中一种技术是将高阶多项式添加到方程中。

我们看到 RSS 是拟合回归线时的一个重要特征(在构建模型时)。现在,为了评估回归拟合的好坏(一旦模型建立好),你需要另外两个统计量 - 残差标准误差(RSE)和 R²统计量。

我们讨论了不可减小的误差组件ε,因此即使你的方程完全拟合数据点并且正确估计了系数,你的回归仍然会有一定水平的误差。RSE 是ε的标准差的估计,可以定义如下:

线性回归

这意味着实际值与真实回归线的偏差平均为 RSE 的因素。

由于 RSE 实际上是以 Y 的单位来衡量的(参考我们在上一节中如何计算 RSS),很难说它是模型准确性的唯一最佳统计量。

因此,引入了一种另类方法,称为 R²统计量(也称为决定系数)。计算 R²的公式如下:

线性回归

总平方和(TSS)可以计算如下:

线性回归

在这里要注意,TSS 测量了在执行回归预测 Y 之前 Y 中固有的总方差。注意它里面没有 Y'。相反,RSS 代表了回归后未能解释的 Y 中的变异性。这意味着(TSS - RSS)能够解释回归后响应的变异性。

R²统计量通常在 0 到 1 之间,但如果拟合比拟合水平线更差,可能会是负数,但这种情况很少见。接近 1 的值表示回归方程可以解释响应变量中大部分的变异性,是一个很好的拟合。相反,接近 0 的值表示回归没有解释响应变量中的大部分方差,不是一个很好的拟合。例如,R²为 0.25 意味着 25%的 Y 的方差由 X 解释,并且表明需要调整模型以改进。

现在让我们讨论如何通过回归来处理数据集中的非线性。正如前面讨论的,当发现非线性关系时,需要妥善处理。为了使用相同的线性回归技术建模非线性方程,您必须创建更高阶的特征,这些特征将被回归技术视为另一个变量。例如,如果薪水是一个特征/变量,用于预测购买潜力,并且我们发现它们之间存在非线性关系,那么我们可能会创建一个名为(salary3)的特征,具体取决于需要解决多少非线性。请注意,当您创建这些更高阶特征时,您还必须保留基本特征。在这个例子中,您必须在回归方程中同时使用(salary)和(salary3)。

到目前为止,我们有点假设所有的预测变量都是连续的。如果有分类预测变量怎么办?在这种情况下,我们必须对这些变量进行虚拟编码(比如男性为 1,女性为 0),以便回归技术生成两个方程,一个用于性别=男性(方程将包含性别变量),另一个用于性别=女性(方程将不包含性别变量,因为它将被编码为 0)。有时,对于非常少的分类变量,根据分类变量的级别划分数据集并为其构建单独的模型可能是一个好主意。

最小二乘线性回归的一个主要优势是它解释了结果变量与预测变量的关系。这使得它非常可解释,并且可以用于推断以及预测。

损失函数

许多机器学习问题可以被制定为凸优化问题。这个问题的目标是找到使平方损失最小的系数值。这个目标函数基本上有两个组成部分 - 正则化器和损失函数。正则化器用于控制模型的复杂性(以防止过拟合),损失函数用于估计回归函数的系数,使得平方损失(RSS)最小。

最小二乘法使用的损失函数称为平方损失,如下所示:

损失函数

这里Y是响应变量(实值),W是权重向量(系数的值),X是特征向量。所以

损失函数

给出了预测值,我们将其与实际值Y相等,以找到需要最小化的平方损失。

用于估计系数的算法称为梯度下降。不同类型的损失函数和优化算法适用于不同类型的机器学习算法,我们将根据需要进行介绍。

优化

最终,线性方法必须优化损失函数。在幕后,线性方法使用凸优化方法来优化目标函数。MLlib 支持随机梯度下降SGD)和有限内存 - Broyden-Fletcher-Goldfarb-ShannoL-BFGS)。目前,大多数算法 API 支持 SGD,少数支持 L-BFGS。

SGD 是一种适用于大规模数据和分布式计算环境的一阶优化技术。目标函数(损失函数)被写成求和形式的优化问题最适合使用 SGD 来解决。

L-BFGS 是一种在拟牛顿方法家族中的优化算法,用于解决优化问题。与 SGD 等一阶优化技术相比,L-BFGS 通常能够实现更快的收敛。

MLlib 中提供的一些线性方法都支持 SGD 和 L-BFGS。您应该根据所考虑的目标函数选择其中一种。一般来说,L-BFGS 比 SGD 更快地收敛,但您需要根据需求进行仔细评估。

回归的正则化

具有较大权重(系数值)时,容易过拟合模型。正则化是一种主要用于通过控制模型复杂性来消除过拟合问题的技术。通常在看到模型在训练数据和测试数据上的性能差异时进行。如果训练性能高于测试数据,可能是过拟合(高方差)的情况。

为了解决这个问题,引入了一种会对损失函数进行惩罚的正则化技术。在训练数据观测数量较少时,通常建议使用任何一种正则化技术。

在进一步讨论正则化技术之前,我们必须了解在监督学习环境中,“偏差”和“方差”是什么意思,以及为什么总是存在相关的权衡。虽然两者都与错误有关,“偏差”模型意味着它偏向于某些错误的假设,并且可能在一定程度上忽略预测变量和响应变量之间的关系。这是欠拟合的情况!另一方面,“高方差”模型意味着它试图触及每个数据点,并最终对数据集中存在的随机噪声进行建模。这代表了过拟合的情况。

带有 L2 惩罚(L2 正则化)的线性回归称为岭回归,带有 L1 惩罚(L1 正则化)的线性回归称为套索回归。当同时使用 L1 和 L2 惩罚时,称为弹性网络回归。我们将在下一节依次讨论它们。

与 L1 正则化问题相比,L2 正则化问题通常更容易解决,因为它更加平滑,但 L1 正则化问题可能导致权重的稀疏性,从而导致更小且更可解释的模型。因此,套索有时用于特征选择。

岭回归

当我们在最小二乘损失函数中加入 L2 惩罚(也称为收缩惩罚)时,就变成了岭回归,如下所示:

岭回归

这里λ(大于 0)是一个单独确定的调整参数。在前述方程的第二项被称为收缩惩罚,只有当系数(β0β1...等等)很小时并且接近 0 时,它才会很小。当λ=0时,岭回归变为最小二乘法。当 lambda 趋近于无穷大时,回归系数趋近于零(但永远不会为零)。

岭回归为每个λ值生成不同的系数值集。因此,需要使用交叉验证来谨慎选择 lambda 值。随着 lambda 值的增加,回归线的灵活性减少,从而减少方差并增加偏差。

请注意,收缩惩罚适用于除截距项β0之外的所有解释变量。

当训练数据较少或者预测变量或特征的数量超过观测数量时,岭回归效果非常好。此外,岭回归所需的计算几乎与最小二乘法相同。

由于岭回归不会将任何系数值减少到零,所有变量都将出现在模型中,这可能会使模型在变量数量较多时变得不太可解释。

套索回归

套索回归是在岭回归之后引入的。当我们在最小二乘损失函数中加入 L1 惩罚时,就变成了套索回归,如下所示:

套索回归

这里的区别在于,它不是取平方系数,而是取系数的模。与岭回归不同,它可以强制一些系数为零,这可能导致一些变量的消除。因此,Lasso 也可以用于变量选择!

Lasso 为每个 lambda 值生成不同的系数值集。因此需要使用交叉验证来谨慎选择 lambda 值。与岭回归一样,随着 lambda 的增加,方差减小,偏差增加。

Lasso 相对于岭回归产生更好的可解释模型,因为它通常只有总变量数的子集。当存在许多分类变量时,建议选择 Lasso 而不是岭回归。

实际上,岭回归和 Lasso 并不总是一个比另一个更好。Lasso 通常在具有实质性系数的少量预测变量和其余具有非常小系数的情况下表现良好。当存在许多预测变量且几乎所有预测变量具有实质性但相似的系数大小时,岭回归通常表现更好。

岭回归适用于分组选择,也可以解决多重共线性问题。另一方面,Lasso 不能进行分组选择,倾向于只选择一个预测变量。此外,如果一组预测变量彼此高度相关,Lasso 倾向于只选择其中一个,并将其他收缩为零。

弹性网络回归

当我们在最小二乘的损失函数中同时添加 L1 和 L2 惩罚时,它就成为了弹性网络回归,如下所示:

弹性网络回归

以下是弹性网络回归的优点:

  • 强制稀疏性并帮助去除最不有效的变量

  • 鼓励分组效应

  • 结合了岭回归和 Lasso 的优点

Naive 版本的弹性网络回归存在双收缩问题,导致增加偏差和较差的预测准确性。为了解决这个问题,一种方法是通过将估计系数乘以(1+ λ2)来重新缩放它们:

Scala

import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.regression.LinearRegressionModel
import org.apache.spark.mllib.regression.LinearRegressionWithSGD
scala> import org.apache.spark.ml.regression.{LinearRegression,LinearRegressionModel}
import org.apache.spark.ml.regression.{LinearRegression,LinearRegressionModel}
// Load the data
scala> val data = spark.read.format("libsvm").load("data/mllib/sample_linear_regression_data.txt")
data: org.apache.spark.sql.DataFrame = [label: double, features: vector]

// Build the model
scala> val lrModel = new LinearRegression().fit(data)

//Note: You can change ElasticNetParam, MaxIter and RegParam
// Defaults are 0.0, 100 and 0.0
lrModel: org.apache.spark.ml.regression.LinearRegressionModel = linReg_aa788bcebc42

//Check Root Mean Squared Error
scala> println("Root Mean Squared Error = " + lrModel.summary.rootMeanSquaredError)
Root Mean Squared Error = 10.16309157133015

Python

>>> from pyspark.ml.regression import LinearRegression, LinearRegressionModel
>>>

// Load the data
>>> data = spark.read.format("libsvm").load("data/mllib/sample_linear_regression_data.txt")
>>> 

// Build the model
>>> lrModel = LinearRegression().fit(data)

//Note: You can change ElasticNetParam, MaxIter and RegParam
// Defaults are 0.0, 100 and 0.0
//Check Root Mean Squared Error
>>> print "Root Mean Squared Error = ", lrModel.summary.rootMeanSquaredError
Root Mean Squared Error = 10.16309157133015
>>> 

分类方法

如果响应变量是定性/分类的(取诸如性别、贷款违约、婚姻状况等分类值),那么问题可以被称为分类问题,而不管解释变量的类型。有各种类型的分类方法,但在本节中我们将专注于逻辑回归和支持向量机。

以下是一些分类方法的一些含义的例子:

  • 一个顾客购买产品或不购买产品

  • 一个人是否患有糖尿病

  • 一个申请贷款的个人是否违约

  • 一个电子邮件接收者是否阅读电子邮件

逻辑回归

逻辑回归衡量了解释变量和分类响应变量之间的关系。我们不使用线性回归来处理分类响应变量,因为响应变量不是在连续尺度上,因此误差项不是正态分布的。

因此,逻辑回归是一种分类算法。逻辑回归不直接对响应变量Y建模,而是对Y属于特定类别的概率分布P(Y|X)进行建模。条件分布(Y|X)是伯努利分布,而不是高斯分布。逻辑回归方程可以表示如下:

逻辑回归

对于二分类,模型的输出应该限制为两个类中的一个(比如 0 或 1)。由于逻辑回归预测的是概率而不是直接的类,我们使用逻辑函数(也称为sigmoid 函数)来将输出限制为单个类:

逻辑回归

解决上述方程得到以下结果:

逻辑回归

可以进一步简化为:

逻辑回归

左边的数量 P(X)/1-P(X) 被称为 赔率。赔率的值范围从 0 到无穷大。接近 0 的值表示概率很低,而数字较大的值表示高概率。有时根据情况直接使用赔率而不是概率。

如果我们取赔率的对数,它就变成了对数赔率或 logit,可以表示如下:

逻辑回归

从前面的方程可以看出,logit 与 X 线性相关。

在有两个类别 1 和 0 的情况下,如果 p >= 0.5 则预测 Y = 1,如果 p < 0.5 则预测 Y = 0。因此,逻辑回归实际上是一个决策边界在 p = 0.5 处的线性分类器。在某些业务案例中,p 并不是默认设置为 0.5,您可能需要使用一些数学技术来找出正确的值。

一种称为最大似然的方法用于通过计算回归系数来拟合模型,算法可以是梯度下降,就像在线性回归设置中一样。

在逻辑回归中,损失函数应该解决误分类率。因此,逻辑回归使用的损失函数称为 逻辑损失,如下所示:

逻辑回归

注意

请注意,当您使用高阶多项式更好地拟合模型时,逻辑回归也容易过拟合。为了解决这个问题,您可以像在线性回归中那样使用正则化项。截至目前,Spark 不支持正则化的逻辑回归,因此我们暂时跳过这部分。

线性支持向量机(SVM)

支持向量机SVM)是一种监督式机器学习算法,可用于分类和回归。但是,它在解决分类问题方面更受欢迎,由于 Spark 将其作为 SVM 分类器提供,因此我们将仅限于讨论分类设置。在用作分类器时,与逻辑回归不同,它是一种非概率分类器。

SVM 已经从一个称为最大间隔分类器的简单分类器发展而来。由于最大间隔分类器要求类别可由线性边界分开,因此它无法应用于许多数据集。因此,它被扩展为一个称为支持向量分类器的改进版本,可以处理类别重叠且类别之间没有明显分离的情况。支持向量分类器进一步扩展为我们所说的 SVM,以适应非线性类边界。让我们逐步讨论 SVM 的演变,以便更清楚地了解它的工作原理。

如果数据集中有 p 个维度(特征),那么我们在 p 维空间中拟合一个超平面,其方程可以定义如下:

线性支持向量机(SVM)

这个超平面被称为形成决策边界的分离超平面。结果将根据结果进行分类;如果大于 0,则在一侧,如果小于 0,则在另一侧,如下图所示:

线性支持向量机(SVM)

观察前面的图表,可以有多个超平面(它们可以是无限的)。应该有一个合理的方法来选择最佳的超平面。这就是我们选择最大间隔超平面的地方。如果计算所有数据点到分离超平面的垂直距离,那么最小距离将被称为间隔。因此,对于最大间隔分类器,超平面应具有最大间隔。

距离分隔超平面接近但等距离的训练观测被称为支持向量。对支持向量进行微小改变会导致超平面重新定位。这些支持向量实际上定义了边缘。那么,如果考虑的两个类别是不可分的呢?我们可能希望有一个分类器,它不完全分离两个类别,并且具有一个更柔和的边界,允许一定程度的误分类。这一要求导致了支持向量分类器的引入(也称为软边界分类器)。

从数学上讲,正是方程中的松弛变量允许了误分类。此外,在支持向量分类器中有一个调节参数,应该使用交叉验证来选择。这个调节参数是在偏差和方差之间进行权衡的参数,应该小心处理。当它很大时,边缘会更宽,包含许多支持向量,具有低方差和高偏差。如果它很小,那么边缘将有更少的支持向量,分类器将具有低偏差但高方差。

SVM 的损失函数可以表示如下:

线性支持向量机(SVM)

截至目前,Spark 仅支持线性 SVM。默认情况下,线性 SVM 使用 L2 正则化进行训练。Spark 还支持替代的 L1 正则化。

到目前为止一切顺利!但是当类别之间存在非线性边界时,支持向量分类器会如何工作呢,就像下面的图片所示的那样:

线性支持向量机(SVM)

任何线性分类器,比如支持向量分类器,在前述情况下表现都非常糟糕。如果它通过数据点画一条直线,那么类别就无法正确分离。这是非线性类边界的情况。解决这个问题的方法是支持向量机(SVM)。换句话说,当支持向量分类器与非线性核融合时,它就成为了 SVM。

与我们在回归方程中引入高阶多项式项以解决非线性问题的方式类似,在 SVM 的情境下也可以做一些处理。SVM 使用称为核的东西来处理数据集中不同类型的非线性;不同类型的非线性需要不同的核。核方法将数据映射到更高维的空间,这样做可能会使数据得到更好的分离。同时,它也使得区分不同类别变得更容易。让我们讨论一下一些重要的核,以便能够选择合适的核。

线性核

这是最基本类型的核之一,它允许我们只选择线或超平面。它相当于支持向量分类器。如果数据集中存在非线性,它就无法解决。

多项式核

这允许我们在多项式阶数的范围内解决一定程度的非线性。当训练数据被归一化时,这种方法效果很好。这个核通常有更多的超参数,因此增加了模型的复杂性。

径向基函数核

当你不确定使用哪种核时,径向基函数(RBF)可能是一个不错的默认选择。它允许你选择甚至是圆或超球体。尽管这通常比线性或多项式核表现更好,但当特征数量很大时,它的表现就不那么好了。

Sigmoid 核

Sigmoid 核源自神经网络。因此,具有 Sigmoid 核的 SVM 等效于具有两层感知器的神经网络。

训练 SVM

在训练 SVM 时,建模者需要做出一些决策:

  • 如何预处理数据(转换和缩放)。分类变量应通过虚拟化转换为数值变量。此外,需要对数值进行缩放(0 到 1 或-1 到+1)。

  • 要使用哪种核(如果无法可视化数据和/或对其进行结论,则使用交叉验证进行检查)。

  • SVM 的参数设置:惩罚参数和核参数(使用交叉验证或网格搜索进行查找)

如果需要,可以使用基于熵的特征选择来在模型中仅包括重要特征。

Scala

scala> import org.apache.spark.mllib.classification.{SVMModel, SVMWithSGD}
import org.apache.spark.mllib.classification.{SVMModel, SVMWithSGD}
scala> import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
scala> import org.apache.spark.mllib.util.MLUtils
import org.apache.spark.mllib.util.MLUtils
scala>

// Load training data in LIBSVM format.
scala> val data = MLUtils.loadLibSVMFile(sc, "data/mllib/sample_libsvm_data.txt")
data: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[6] at map at MLUtils.scala:84
scala>

// Split data into training (60%) and test (40%).
scala> val splits = data.randomSplit(Array(0.6, 0.4), seed = 11L)
splits: Array[org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint]] = Array(MapPartitionsRDD[7] at randomSplit at <console>:29, MapPartitionsRDD[8] at randomSplit at <console>:29)
scala> val training = splits(0).cache()
training: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[7] at randomSplit at <console>:29
scala> val test = splits(1)
test: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[8] at randomSplit at <console>:29
scala>

// Run training algorithm to build the model
scala> val model = SVMWithSGD.train(training, numIterations=100)
model: org.apache.spark.mllib.classification.SVMModel = org.apache.spark.mllib.classification.SVMModel: intercept = 0.0, numFeatures = 692, numClasses = 2, threshold = 0.0
scala>

// Clear the default threshold.
scala> model.clearThreshold()
res1: model.type = org.apache.spark.mllib.classification.SVMModel: intercept =
0.0, numFeatures = 692, numClasses = 2, threshold = None
scala>

// Compute raw scores on the test set.
scala> val scoreAndLabels = test.map { point =>
       val score = model.predict(point.features)
      (score, point.label)
      }
scoreAndLabels: org.apache.spark.rdd.RDD[(Double, Double)] =
MapPartitionsRDD[213] at map at <console>:37
scala>

// Get evaluation metrics.
scala> val metrics = new BinaryClassificationMetrics(scoreAndLabels)
metrics: org.apache.spark.mllib.evaluation.BinaryClassificationMetrics = org.apache.spark.mllib.evaluation.BinaryClassificationMetrics@3106aebb
scala> println("Area under ROC = " + metrics.areaUnderROC())
Area under ROC = 1.0
scala>

注意

mllib已经进入维护模式,SVM 在 ml 下仍不可用,因此仅提供 Scala 代码以供说明。

决策树

决策树是一种非参数的监督学习算法,可用于分类和回归。决策树就像倒置的树,根节点在顶部,叶节点向下形成。有不同的算法将数据集分割成类似分支的段。每个叶节点分配给代表最合适目标值的类。

决策树不需要对数据集进行任何缩放或转换,并且可以处理分类和连续特征,还可以处理数据集中的非线性。在其核心,决策树是一种贪婪算法(它考虑当前的最佳分割,并不考虑未来的情况),它对特征空间进行递归二元分区。分割是基于每个节点的信息增益进行的,因为信息增益衡量了给定属性如何根据目标类别或值分隔训练示例。第一个分割发生在生成最大信息增益的特征上,并成为根节点。

节点的信息增益是父节点不纯度与两个子节点不纯度加权和之间的差异。为了估计信息增益,Spark 目前针对分类问题有两种不纯度度量,针对回归问题有一种不纯度度量,如下所述。

不纯度度量

不纯度是同质性的度量,也是递归分区的最佳标准。通过计算不纯度,决定最佳的分割候选。大多数不纯度度量都是基于概率的:

类的概率=该类的观察次数/总观察次数

让我们花一些时间来了解 Spark 支持的不同类型的重要不纯度度量。

基尼指数

基尼指数主要用于数据集中的连续属性或特征。如果不是,它将假定所有属性和特征都是连续的。分割使得子节点比父节点更纯净。基尼倾向于找到最大的类 - 响应变量的类别,其观察次数最多。可以定义如下:

基尼指数

如果响应的所有观察属于单个类,则该类的概率P,即(Pj),将为 1,因为只有一个类,(Pj)2也将为 1。这使得基尼指数为零。

熵主要用于数据集中的分类属性或特征。可以定义如下:

熵

如果响应的所有观察属于单个类,则该类的概率(Pj)将为 1,log(P)将为零。这使得熵为零。

以下图表描述了公平硬币抛掷的概率:

熵

仅为了解释前面的图表,如果抛掷一个公平硬币,正面或反面的概率将为 0.5,因此在概率为 0.5 时观察次数最多。

如果数据样本完全同质,则熵将为零,如果样本可以平均分为两部分,则熵将为一。

与 Gini 相比,计算速度稍慢,因为它还必须计算对数。

方差

与基尼指数和熵不同,方差用于计算回归问题的信息增益。方差可以定义为:

方差

停止规则

当满足以下条件之一时,递归树构造停止在一个节点上:

  • 节点深度等于maxDepth训练参数

  • 没有分裂候选者导致信息增益大于minInfoGain

  • 没有分裂候选者产生子节点,每个子节点至少有一个minInstancesPerNode训练实例

分裂候选者

数据集通常包含混合的分类和连续特征。我们应该了解特征如何进一步分裂为分裂候选者,因为有时我们需要一定程度的控制来构建更好的模型。

分类特征

对于具有M个可能值(类别)的分类特征,可以提出2(M-ˆ’1)-ˆ’1个分裂候选者。无论是二元分类还是回归,通过按平均标签对分类特征值进行排序,可以将分裂候选者的数量减少到M-ˆ’1

例如,考虑一个具有三个类别 A、B 和 C 的分类特征的二元分类(0/1)问题,它们对应的标签-1 响应变量的比例分别为 0.2、0.6 和 0.4。在这种情况下,分类特征可以被排序为 A、C、B。因此,两个分裂候选者(M-1 = 3-1 = 2)可以是A | (C, B)A, (C | B),其中“|”表示分裂。

连续特征

对于连续特征变量,可能存在没有两个相同值的情况(至少我们可以假设如此)。如果有n个观察结果,那么n个分裂候选者可能不是一个好主意,特别是在大数据环境中。

在 Spark 中,通过对数据样本进行分位数计算,并相应地对数据进行分箱来实现。您仍然可以通过使用maxBins参数来控制允许的最大箱数。maxBins的最大默认值为32

决策树的优势

  • 它们易于理解和解释,因此易于向业务用户解释

  • 它们适用于分类和回归

  • 在构建决策树时,可以容纳定性和定量数据

决策树中的信息增益偏向于具有更多级别的属性。

决策树的缺点

  • 它们对于连续结果变量的有效性不是很好

  • 当类别很多且数据集很小时,性能较差。

  • 轴平行分裂降低了准确性

  • 它们因试图拟合几乎所有数据点而遭受高方差

例子

实现方面,在分类和回归树之间没有主要区别。让我们在 Spark 上实际实现它。

Scala:

//Assuming ml.Pipeline and ml.features are already imported
scala> import org.apache.spark.ml.classification.{
        DecisionTreeClassifier, DecisionTreeClassificationModel}
import org.apache.spark.ml.classification.{DecisionTreeClassifier,
DecisionTreeClassificationModel}
scala>
/prepare train data
scala> val f:String = "<Your path>/simple_file1.csv"
f: String = <your path>/simple_file1.csv
scala> val trainDF = spark.read.options(Map("header"->"true",
            "inferSchema"->"true")).csv(f)
trainDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int]

scala>

 //define DecisionTree pipeline
//StringIndexer maps labels(String or numeric) to label indices
//Maximum occurrence label becomes 0 and so on
scala> val lblIdx = new StringIndexer().
                setInputCol("Label").
                setOutputCol("indexedLabel")
lblIdx: org.apache.spark.ml.feature.StringIndexer = strIdx_3a7bc9c1ed0d
scala>

// Create labels list to decode predictions
scala> val labels = lblIdx.fit(trainDF).labels
labels: Array[String] = Array(2, 1, 3)
scala>

//Define Text column indexing stage
scala> val fIdx = new StringIndexer().
                setInputCol("Text").
              setOutputCol("indexedText")
fIdx: org.apache.spark.ml.feature.StringIndexer = strIdx_49253a83c717

// VectorAssembler
scala> val va = new VectorAssembler().
              setInputCols(Array("indexedText")).
              setOutputCol("features")
va: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_764720c39a85

//Define Decision Tree classifier. Set label and features vector
scala> val dt = new DecisionTreeClassifier().
            setLabelCol("indexedLabel").
            setFeaturesCol("features")
dt: org.apache.spark.ml.classification.DecisionTreeClassifier = dtc_84d87d778792

//Define label converter to convert prediction index back to string
scala> val lc = new IndexToString().
                setInputCol("prediction").
                setOutputCol("predictedLabel").
                setLabels(labels)
lc: org.apache.spark.ml.feature.IndexToString = idxToStr_e2f4fa023665
scala>

//String the stages together to form a pipeline
scala> val dt_pipeline = new Pipeline().setStages(
          Array(lblIdx,fIdx,va,dt,lc))
dt_pipeline: org.apache.spark.ml.Pipeline = pipeline_d4b0e884dcbf
scala>
//Apply pipeline to the train data
scala> val resultDF = dt_pipeline.fit(trainDF).transform(trainDF)

//Check results. Watch Label and predictedLabel column values match
resultDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int ... 6 more
fields]
scala>
resultDF.select("Text","Label","features","prediction","predictedLabel").show()
+----+-----+--------+----------+--------------+
|Text|Label|features|prediction|predictedLabel|
+----+-----+--------+----------+--------------+
|   A|    1|   [1.0]|       1.0|             1|
|   B|    2|   [0.0]|       0.0|             2|
|   C|    3|   [2.0]|       2.0|             3|
|   A|    1|   [1.0]|       1.0|             1|
|   B|    2|   [0.0]|       0.0|             2|
+----+-----+--------+----------+--------------+
scala>

//Prepare evaluation data
scala> val eval:String = "€œ<Your path>/simple_file2.csv"
eval: String = <Your path>/simple_file2.csv
scala> val evalDF = spark.read.options(Map("header"->"true",
            "inferSchema"->"true")).csv(eval)
evalDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int]
scala>

//Apply the same pipeline to the evaluation data
scala> val eval_resultDF = dt_pipeline.fit(evalDF).transform(evalDF)
eval_resultDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int ... 7
more fields]

//Check evaluation results
scala>
eval_resultDF.select("Text","Label","features","prediction","predictedLabel").sh
w()
+----+-----+--------+----------+--------------+
|Text|Label|features|prediction|predictedLabel|
+----+-----+--------+----------+--------------+
|   A|    1|   [0.0]|       1.0|             1|
|   A|    1|   [0.0]|       1.0|             1|
|   A|    2|   [0.0]|       1.0|             1|
|   B|    2|   [1.0]|       0.0|             2|
|   C|    3|   [2.0]|       2.0|             3|
+----+-----+--------+----------+--------------+
//Note that predicted label for the third row is 1 as against Label(2) as
expected

Python:

//Model training example
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import StringIndexer, VectorIndexer, VectorAssembler,
IndexToString
>>> from pyspark.ml.classification import DecisionTreeClassifier,
DecisionTreeClassificationModel
>>> 

//prepare train data
>>> file_location = "../work/simple_file1.csv"
>>> trainDF = spark.read.csv(file_location,header=True,inferSchema=True)

 //Read file
>>>

//define DecisionTree pipeline
//StringIndexer maps labels(String or numeric) to label indices
//Maximum occurrence label becomes 0 and so on
>>> lblIdx = StringIndexer(inputCol = "Label",outputCol = "indexedLabel")

// Create labels list to decode predictions
>>> labels = lblIdx.fit(trainDF).labels
>>> labels
[u'2', u'1', u'3']
>>> 

//Define Text column indexing stage
>>> fidx = StringIndexer(inputCol="Text",outputCol="indexedText")

// Vector assembler
>>> va = VectorAssembler(inputCols=["indexedText"],outputCol="features")

//Define Decision Tree classifier. Set label and features vector
>>> dt = DecisionTreeClassifier(labelCol="indexedLabel",featuresCol="features")

//Define label converter to convert prediction index back to string
>>> lc = IndexToString(inputCol="prediction",outputCol="predictedLabel",
                       labels=labels)

//String the stages together to form a pipeline
>>> dt_pipeline = Pipeline(stages=[lblIdx,fidx,va,dt,lc])
>>>
>>> 

//Apply decision tree pipeline
>>> dtModel = dt_pipeline.fit(trainDF)
>>> dtDF = dtModel.transform(trainDF)
>>> dtDF.columns
['Text', 'Label', 'indexedLabel', 'indexedText', 'features', 'rawPrediction',
'probability', 'prediction', 'predictedLabel']
>>> dtDF.select("Text","Label","indexedLabel","prediction",
"predictedLabel").show()
+----+-----+------------+----------+--------------+
|Text|Label|indexedLabel|prediction|predictedLabel|
+----+-----+------------+----------+--------------+
|   A|    1|         1.0|       1.0|             1|
|   B|    2|         0.0|       0.0|             2|
|   C|    3|         2.0|       2.0|             3|
|   A|    1|         1.0|       1.0|             1|
|   B|    2|         0.0|       0.0|             2|
+----+-----+------------+----------+--------------+

>>>

>>> //prepare evaluation dataframe
>>> eval_file_path = "../work/simple_file2.csv"
>>> evalDF = spark.read.csv(eval_file_path,header=True, inferSchema=True) 

//Read eval file
>>> eval_resultDF = dt_pipeline.fit(evalDF).transform(evalDF)
>>> eval_resultDF.columns
['Text', 'Label', 'indexedLabel', 'indexedText', 'features', 'rawPrediction', 'probability', 'prediction', 'predictedLabel']
>>> eval_resultDF.select("Text","Label","indexedLabel","prediction",
"predictedLabel").show()
+----+-----+------------+----------+--------------+
|Text|Label|indexedLabel|prediction|predictedLabel|
+----+-----+------------+----------+--------------+
|   A|    1|         1.0|       1.0|             1|
|   A|    1|         1.0|       1.0|             1|
|   A|    2|         0.0|       1.0|             1|
|   B|    2|         0.0|       0.0|             2|
|   C|    3|         2.0|       2.0|             3|
+----+-----+------------+----------+--------------+
>>> 

Accompanying data files:
simple_file1.csv Text,Label
A,1
B,2
C,3
A,1
B,2simple_file2.csv Text,Label
A,1
A,1
A,2
B,2
C,3

集成

正如其名称所示,集成方法使用多个学习算法来获得更准确的模型,通常这些技术需要更多的计算能力,并使模型更复杂,这使得难以解释。让我们讨论 Spark 上可用的各种类型的集成技术。

随机森林

随机森林是决策树的集成技术。在我们讨论随机森林之前,让我们看看它是如何发展的。我们知道决策树通常存在高方差问题,并且倾向于过度拟合模型。为了解决这个问题,引入了一个称为bagging(也称为自举聚合)的概念。对于决策树,想法是从数据集中获取多个训练集(自举训练集),并从中创建单独的决策树,然后对回归树进行平均。对于分类树,我们可以从所有树中获取多数投票或最常出现的类。这些树生长深入,并且根本没有被修剪。这确实减少了方差,尽管单个树可能具有高方差。

纯粹的 bagging 方法的一个问题是,对于大多数自举训练集,强预测变量占据了顶部分裂的位置,这几乎使得袋装树看起来相似。这意味着预测也看起来相似,如果你对它们进行平均,那么它并没有像预期的那样减少方差。为了解决这个问题,需要一种技术,它将采用与袋装树类似的方法,但消除树之间的相关性,因此产生了随机森林

在这种方法中,您构建自举训练样本以创建决策树,但唯一的区别是每次发生分裂时,从总共 K 个预测变量中选择 P 个预测变量的随机样本。这就是随机森林向这种方法注入随机性的方式。作为一个经验法则,我们可以将 P 取为 Q 的平方根。

就像在 bagging 的情况下,如果你的目标是回归,你也会平均预测结果,如果目标是分类,你会采取多数投票。Spark 提供了一些调整参数来调整这个模型,如下所示:

  • numTrees:您可以指定在随机森林中考虑的树的数量。如果数字很高,那么预测的方差会较小,但所需的时间会更长。

  • maxDepth:您可以指定每棵树的最大深度。增加深度会使树在预测准确度方面更加强大。尽管它们倾向于过度拟合单独的树,但总体输出仍然很好,因为我们无论如何都会平均结果,从而减少方差。

  • subsamplingRate:这个参数主要用于加速训练。它用于设置自举训练样本的大小。小于 1.0 的值可以加快性能。

  • featureSubsetStrategy:这个参数也可以帮助加快执行。它用于设置每个节点用作分裂候选的特征数。它应该谨慎设置,因为太低或太高的值可能会影响模型的准确性。

随机森林的优势

  • 它们运行速度更快,因为执行是并行进行的

  • 它们不太容易过度拟合

  • 它们易于调整

  • 与树或袋装树相比,预测准确度更高

  • 它们即使在预测变量是分类和连续特征的混合时也能很好地工作,并且不需要缩放

梯度提升树

与随机森林一样,梯度提升树GBTs)也是一种树的集成。它们可以应用于分类和回归问题。与袋装树或随机森林不同,树是顺序构建的。每棵树都是使用先前生长树的结果来生长的。请注意,GBT 不适用于自举样本。

在每次迭代中,GBT 使用当前集成来预测训练实例的标签,并将它们与真实标签进行比较,并估计错误。预测准确度较差的训练实例将被重新标记,以便基于先前错误的错误率在下一次迭代中纠正决策树。

找到错误率并重新标记实例的机制是基于损失函数的。GBT 旨在减少每次迭代的损失函数。Spark 支持以下类型的损失函数:

  • 对数损失:这用于分类问题。

  • 平方误差(L2 损失):这用于回归问题,并且默认设置。它是所有观察值的实际值和预测输出之间的平方差异的总和。对于这种损失函数,异常值应该得到很好的处理才能表现良好。

  • 绝对误差(L1 损失):这也用于回归问题。它是所有观察值的实际值和预测输出之间的绝对差异的总和。与平方误差相比,它对异常值更具鲁棒性。

Spark 提供了一些调整参数来调整此模型,如下所示:

  • loss:您可以根据前面讨论的数据集和您打算进行分类或回归的意图,传递一个损失函数。

  • numIterations:每次迭代只生成一棵树!如果将此设置得很高,那么执行所需的时间也会很长,因为操作将是顺序的,并且还可能导致过拟合。应该谨慎设置以获得更好的性能和准确性。

  • learningRate:这实际上不是一个调整参数。如果算法的行为不稳定,那么减小这个值可以帮助稳定模型。

  • algo分类回归是根据您的需求设置的。

GBT 可能会过度拟合具有更多树的模型,因此 Spark 提供了runWithValidation方法来防止过拟合。

提示

截至目前,Spark 上的 GBT 尚不支持多类分类。

让我们看一个示例来说明 GBT 的工作原理。示例数据集包含二十名学生的平均分和出勤情况。数据还包含结果为通过或失败,遵循一组标准。然而,一对学生(id 为 1009 和 1020)被“授予”通过状态,尽管他们实际上并没有资格。现在我们的任务是检查模型是否选择了这两名学生。

通过标准如下:

  • 分数应至少为 40,出勤应至少为“足够”

  • 如果分数在 40 到 60 之间,则出勤应为“全勤”才能通过

以下示例还强调了在多个模型中重复使用管道阶段。因此,我们首先构建一个 DecisionTree 分类器,然后构建一个 GBT。我们构建了两个共享阶段的不同管道。

输入

// Marks < 40 = Fail
// Attendence == Poor => Fail
// Marks >40 and attendence Full => Pass
// Marks > 60 and attendence Enough or Full => Pass
// Two exceptions were studentId 1009 and 1020 who were granted Pass
//This example also emphasizes the reuse of pipeline stages
// Initially the code trains a DecisionTreeClassifier
// Then, same stages are reused to train a GBT classifier

Scala:

scala> import org.apache.spark.ml.feature._
scala> import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.classification.{DecisionTreeClassifier,
                                   DecisionTreeClassificationModel}
scala> case class StResult(StudentId:String, Avg_Marks:Double,
        Attendance:String, Result:String)
scala> val file_path = "../work/StudentsPassFail.csv"
scala> val source_ds = spark.read.options(Map("header"->"true",
            "inferSchema"->"true")).csv(file_path).as[StResult]
source_ds: org.apache.spark.sql.Dataset[StResult] = [StudentId: int, Avg_Marks:
double ... 2 more fields]
scala>
//Examine source data
scala> source_ds.show(4)
+---------+---------+----------+------+
|StudentId|Avg_Marks|Attendance|Result|
+---------+---------+----------+------+
|     1001|     48.0|      Full|  Pass|
|     1002|     21.0|    Enough|  Fail|
|     1003|     24.0|    Enough|  Fail|
|     1004|      4.0|      Poor|  Fail|
+---------+---------+----------+------+

scala>           
//Define preparation pipeline
scala> val marks_bkt = new Bucketizer().setInputCol("Avg_Marks").
        setOutputCol("Mark_bins").setSplits(Array(0,40.0,60.0,100.0))
marks_bkt: org.apache.spark.ml.feature.Bucketizer = bucketizer_5299d2fbd1b2
scala> val att_idx = new StringIndexer().setInputCol("Attendance").
        setOutputCol("Att_idx")
att_idx: org.apache.spark.ml.feature.StringIndexer = strIdx_2db54ba5200a
scala> val label_idx = new StringIndexer().setInputCol("Result").
        setOutputCol("Label")
label_idx: org.apache.spark.ml.feature.StringIndexer = strIdx_20f4316d6232
scala>

//Create labels list to decode predictions
scala> val resultLabels = label_idx.fit(source_ds).labels
resultLabels: Array[String] = Array(Fail, Pass)
scala> val va = new VectorAssembler().setInputCols(Array("Mark_bins","Att_idx")).
                  setOutputCol("features")
va: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_5dc2dbbef48c
scala> val dt = new DecisionTreeClassifier().setLabelCol("Label").
         setFeaturesCol("features")
dt: org.apache.spark.ml.classification.DecisionTreeClassifier = dtc_e8343ae1a9eb
scala> val lc = new IndexToString().setInputCol("prediction").
             setOutputCol("predictedLabel").setLabels(resultLabels)
lc: org.apache.spark.ml.feature.IndexToString = idxToStr_90b6693d4313
scala>

//Define pipeline
scala>val dt_pipeline = new
Pipeline().setStages(Array(marks_bkt,att_idx,label_idx,va,dt,lc))
dt_pipeline: org.apache.spark.ml.Pipeline = pipeline_95876bb6c969
scala> val dtModel = dt_pipeline.fit(source_ds)
dtModel: org.apache.spark.ml.PipelineModel = pipeline_95876bb6c969
scala> val resultDF = dtModel.transform(source_ds)
resultDF: org.apache.spark.sql.DataFrame = [StudentId: int, Avg_Marks: double ...
10 more fields]
scala> resultDF.filter("Label != prediction").select("StudentId","Label","prediction","Result","predictedLabel").show()
+---------+-----+----------+------+--------------+
|StudentId|Label|prediction|Result|predictedLabel|
+---------+-----+----------+------+--------------+\
|     1009|  1.0|       0.0|  Pass|          Fail|
|     1020|  1.0|       0.0|  Pass|          Fail|
+---------+-----+----------+------+--------------+

//Note that the difference is in the student ids that were granted pass

//Same example using Gradient boosted tree classifier, reusing the pipeline stages
scala> import org.apache.spark.ml.classification.GBTClassifier
import org.apache.spark.ml.classification.GBTClassifier
scala> val gbt = new GBTClassifier().setLabelCol("Label").
              setFeaturesCol("features").setMaxIter(10)
gbt: org.apache.spark.ml.classification.GBTClassifier = gbtc_cb55ae2174a1
scala> val gbt_pipeline = new
Pipeline().setStages(Array(marks_bkt,att_idx,label_idx,va,gbt,lc))
gbt_pipeline: org.apache.spark.ml.Pipeline = pipeline_dfd42cd89403
scala> val gbtResultDF = gbt_pipeline.fit(source_ds).transform(source_ds)
gbtResultDF: org.apache.spark.sql.DataFrame = [StudentId: int, Avg_Marks: double ... 8 more fields]
scala> gbtResultDF.filter("Label !=
prediction").select("StudentId","Label","Result","prediction","predictedLabel").show()
+---------+-----+------+----------+--------------+
|StudentId|Label|Result|prediction|predictedLabel|
+---------+-----+------+----------+--------------+
|     1009|  1.0|  Pass|       0.0|          Fail|
|     1020|  1.0|  Pass|       0.0|          Fail|
+---------+-----+------+----------+--------------+

Python:

>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import Bucketizer, StringIndexer, VectorAssembler, IndexToString
>>> from pyspark.ml.classification import DecisionTreeClassifier,
DecisionTreeClassificationModel
>>> 

//Get source file
>>> file_path = "../work/StudentsPassFail.csv"
>>> source_df = spark.read.csv(file_path,header=True,inferSchema=True)
>>> 

//Examine source data
>>> source_df.show(4)
+---------+---------+----------+------+
|StudentId|Avg_Marks|Attendance|Result|
+---------+---------+----------+------+
|     1001|     48.0|      Full|  Pass|
|     1002|     21.0|    Enough|  Fail|
|     1003|     24.0|    Enough|  Fail|
|     1004|      4.0|      Poor|  Fail|
+---------+---------+----------+------+

//Define preparation pipeline
>>> marks_bkt = Bucketizer(inputCol="Avg_Marks",
        outputCol="Mark_bins", splits=[0,40.0,60.0,100.0])
>>> att_idx = StringIndexer(inputCol = "Attendance",
        outputCol="Att_idx")
>>> label_idx = StringIndexer(inputCol="Result",
                   outputCol="Label")
>>> 

//Create labels list to decode predictions
>>> resultLabels = label_idx.fit(source_df).labels
>>> resultLabels
[u'Fail', u'Pass']
>>> 
>>> va = VectorAssembler(inputCols=["Mark_bins","Att_idx"],
                         outputCol="features")
>>> dt = DecisionTreeClassifier(labelCol="Label", featuresCol="features")
>>> lc = IndexToString(inputCol="prediction",outputCol="predictedLabel",
             labels=resultLabels)
>>> dt_pipeline = Pipeline(stages=[marks_bkt, att_idx, label_idx,va,dt,lc])
>>> dtModel = dt_pipeline.fit(source_df)
>>> resultDF = dtModel.transform(source_df)
>>>

//Look for obervatiuons where prediction did not match
>>> resultDF.filter("Label != prediction").select(
         "StudentId","Label","prediction","Result","predictedLabel").show()
+---------+-----+----------+------+--------------+
|StudentId|Label|prediction|Result|predictedLabel|
+---------+-----+----------+------+--------------+
|     1009|  1.0|       0.0|  Pass|          Fail|
|     1020|  1.0|       0.0|  Pass|          Fail|
+---------+-----+----------+------+--------------+

//Note that the difference is in the student ids that were granted pass
>>> 
//Same example using Gradient boosted tree classifier, reusing the pipeline
stages
>>> from pyspark.ml.classification import GBTClassifier
>>> gbt = GBTClassifier(labelCol="Label", featuresCol="features",maxIter=10)
>>> gbt_pipeline = Pipeline(stages=[marks_bkt,att_idx,label_idx,va,gbt,lc])
>>> gbtResultDF = gbt_pipeline.fit(source_df).transform(source_df)
>>> gbtResultDF.columns
['StudentId', 'Avg_Marks', 'Attendance', 'Result', 'Mark_bins', 'Att_idx',
'Label', 'features', 'prediction', 'predictedLabel']
>>> gbtResultDF.filter("Label !=
prediction").select("StudentId","Label","Result","prediction","predictedLabel").show()
+---------+-----+------+----------+--------------+
|StudentId|Label|Result|prediction|predictedLabel|
+---------+-----+------+----------+--------------+
|     1009|  1.0|  Pass|       0.0|          Fail|
|     1020|  1.0|  Pass|       0.0|          Fail|
+---------+-----+------+----------+--------------+

多层感知器分类器

多层感知器分类器MLPC)是一种前馈人工神经网络,具有多层节点以有向方式相互连接。它使用一种称为反向传播的监督学习技术来训练网络。

中间层的节点使用 sigmoid 函数将输出限制在 0 和 1 之间,输出层的节点使用softmax函数,这是 sigmoid 函数的广义版本。

Scala:

scala> import org.apache.spark.ml.classification.MultilayerPerceptronClassifier
import org.apache.spark.ml.classification.MultilayerPerceptronClassifier
scala> import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
scala> import org.apache.spark.mllib.util.MLUtils
import org.apache.spark.mllib.util.MLUtils

// Load training data
scala> val data = MLUtils.loadLibSVMFile(sc,
"data/mllib/sample_multiclass_classification_data.txt").toDF()
data: org.apache.spark.sql.DataFrame = [label: double, features: vector]

//Convert mllib vectors to ml Vectors for spark 2.0+. Retain data for previous versions
scala> val data2 = MLUtils.convertVectorColumnsToML(data)
data2: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]

// Split the data into train and test
scala> val splits = data2.randomSplit(Array(0.6, 0.4), seed = 1234L)
splits: Array[org.apache.spark.sql.Dataset[org.apache.spark.sql.Row]] = Array([label: double, features: vector], [label: double, features: vector])
scala> val train = splits(0)
train: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
scala> val test = splits(1)
test: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]

// specify layers for the neural network:
// input layer of size 4 (features), two intermediate of size 5 and 4 and output of size 3 (classes)
scala> val layers = ArrayInt
layers: Array[Int] = Array(4, 5, 4, 3)

// create the trainer and set its parameters
scala> val trainer = new MultilayerPerceptronClassifier().
           setLayers(layers).setBlockSize(128).
           setSeed(1234L).setMaxIter(100)
trainer: org.apache.spark.ml.classification.MultilayerPerceptronClassifier = mlpc_edfa49fbae3c

// train the model
scala> val model = trainer.fit(train)
model: org.apache.spark.ml.classification.MultilayerPerceptronClassificationModel = mlpc_edfa49fbae3c

// compute accuracy on the test set
scala> val result = model.transform(test)
result: org.apache.spark.sql.DataFrame = [label: double, features: vector ... 1 more field]
scala> val predictionAndLabels = result.select("prediction", "label")
predictionAndLabels: org.apache.spark.sql.DataFrame = [prediction: double, label: double]
scala> val evaluator = new MulticlassClassificationEvaluator().setMetricName("accuracy")
evaluator: org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator = mcEval_a4f43d85f261
scala> println("Accuracy:" + evaluator.evaluate(predictionAndLabels))
Accuracy:0.9444444444444444

Python: >>> from pyspark.ml.classification import MultilayerPerceptronClassifier
>>> from pyspark.ml.evaluation import MulticlassClassificationEvaluator
>>> from pyspark.mllib.util import MLUtils
>>>

  //Load training data
>>> data = spark.read.format("libsvm").load(      "data/mllib/sample_multiclass_classification_data.txt")

//Convert mllib vectors to ml Vectors for spark 2.0+. Retain data for previous versions
>>> data2 = MLUtils.convertVectorColumnsToML(data)
>>>

 // Split the data into train and test
>>> splits = data2.randomSplit([0.6, 0.4], seed = 1234L)
>>> train, test = splits[0], splits[1]
>>>

 // specify layers for the neural network:
 // input layer of size 4 (features), two intermediate of size 5 and 4 and output of size 3 (classes)
>>> layers = [4,5,4,3] 

// create the trainer and set its parameters
>>> trainer = MultilayerPerceptronClassifier(layers=layers, blockSize=128,
                 seed=1234L, maxIter=100)
// train the model
>>> model = trainer.fit(train)
>>>

// compute accuracy on the test set
>>> result = model.transform(test)
>>> predictionAndLabels = result.select("prediction", "label")
>>> evaluator = MulticlassClassificationEvaluator().setMetricName("accuracy")
>>> print "Accuracy:",evaluator.evaluate(predictionAndLabels)
Accuracy: 0.901960784314
>>> 

聚类技术

聚类是一种无监督学习技术,其中没有响应变量来监督模型。其思想是对具有某种相似性水平的数据点进行聚类。除了探索性数据分析外,它还可作为监督管道的一部分,其中可以在不同的簇上构建分类器或回归器。有许多聚类技术可用。让我们看一下由 Spark 支持的一些重要技术。

K-means 聚类

K-means 是最常见的聚类技术之一。k-means 问题是找到最小化簇内方差的簇中心,即,从要进行聚类的每个数据点到其簇中心(最接近它的中心)的平方距离之和。您必须预先指定数据集中要使用的簇的数量。

由于它使用欧几里得距离度量来找到数据点之间的差异,因此在使用 k-means 之前,需要将特征缩放到可比较的单位。欧几里得距离可以用图形方式更好地解释如下:

K-means 聚类

给定一组数据点(x1x2,...,xn),具有与变量数量相同的维度,k-means 聚类旨在将 n 个观察结果分成 k(小于n)个集合,其中S = {S1,S2,...,Sk},以最小化簇内平方和WCSS)。换句话说,它的目标是找到:

K-means 聚类

Spark 需要将以下参数传递给此算法:

  • k:这是所需簇的数量。

  • maxIterations:这是运行的最大迭代次数。

  • initializationMode:这指定随机初始化或通过 k-means||初始化。

  • runs:这是运行 k-means 算法的次数(k-means 不能保证找到全局最优解,当在给定数据集上运行多次时,算法返回最佳的聚类结果)。

  • initializationSteps:这确定 k-means||算法中的步数。

  • epsilon:这确定我们认为 k-means 已经收敛的距离阈值。

  • initialModel:这是用于初始化的一组可选的聚类中心。如果提供了此参数,将只执行一次运行。

k-means 的缺点

  • 它只适用于数值特征

  • 在实施算法之前需要进行缩放

  • 它容易受到局部最优解的影响(解决方法是 k-means++)

示例

让我们在相同的学生数据上运行 k-means 聚类。

scala> import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
scala> import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.linalg.Vectors
scala>

//Define pipeline for kmeans. Reuse the previous stages in ENSEMBLES
scala> val km = new KMeans()
km: org.apache.spark.ml.clustering.KMeans = kmeans_b34da02bd7c8
scala> val kmeans_pipeline = new
Pipeline().setStages(Array(marks_bkt,att_idx,label_idx,va,km,lc))
kmeans_pipeline: org.apache.spark.ml.Pipeline = pipeline_0cd64aa93a88

//Train and transform
scala> val kmeansDF = kmeans_pipeline.fit(source_ds).transform(source_ds)
kmeansDF: org.apache.spark.sql.DataFrame = [StudentId: int, Avg_Marks: double ... 8 more fields]

//Examine results
scala> kmeansDF.filter("Label != prediction").count()
res17: Long = 13

Python

>>> from pyspark.ml.clustering import KMeans, KMeansModel
>>> from pyspark.ml.linalg import Vectors
>>> 

//Define pipeline for kmeans. Reuse the previous stages in ENSEMBLES
>>> km = KMeans()
>>> kmeans_pipeline = Pipeline(stages = [marks_bkt, att_idx, label_idx,va,km,lc])

//Train and transform
>>> kmeansDF = kmeans_pipeline.fit(source_df).transform(source_df)
>>> kmeansDF.columns
['StudentId', 'Avg_Marks', 'Attendance', 'Result', 'Mark_bins', 'Att_idx', 'Label', 'features', 'prediction', 'predictedLabel']
>>> kmeansDF.filter("Label != prediction").count()
4

总结

在本章中,我们解释了各种机器学习算法,以及它们在 MLlib 库中的实现方式,以及如何在管道 API 中使用它们进行流畅的执行。这些概念通过 Python 和 Scala 代码示例进行了解释,以供参考。

在下一章中,我们将讨论 Spark 如何支持 R 编程语言,重点关注一些算法及其执行,类似于我们在本章中涵盖的内容。

参考资料

MLlib 中支持的算法:

Spark ML 编程指南:

2015 年 6 月峰会幻灯片中的高级数据科学在 spark.pdf 中:

第七章:使用 SparkR 扩展 Spark

统计学家和数据科学家一直在几乎所有领域使用 R 解决具有挑战性的问题,从生物信息学到选举活动。他们喜欢 R 是因为它具有强大的可视化能力、强大的社区以及丰富的统计和机器学习包生态系统。世界各地的许多学术机构使用 R 语言教授数据科学和统计学。

R 最初是在 1990 年代中期由统计学家创建的,目标是提供更好、更用户友好的数据分析方式。R 最初用于学术和研究。随着企业越来越意识到数据科学在业务增长中的作用,企业部门使用 R 的数据分析师数量也在增长。在存在了 20 年后,R 语言用户基数被认为超过 200 万。

所有这一切成功背后的推动因素之一是,R 旨在使分析师的生活更轻松,而不是计算机的生活。R 本质上是单线程的,它只能处理完全适合单台计算机内存的数据集。但如今,R 用户正在处理越来越大的数据集。现代分布式处理能力与成熟的 R 语言的无缝集成,使数据科学家能够充分利用两者的优势。他们可以满足不断增长的业务需求,并继续从他们喜爱的 R 语言的灵活性中受益。

本章介绍了 SparkR,这是一个面向 R 程序员的 R API,使他们可以利用 Spark 的强大功能,而无需学习一种新语言。由于已经假定具有 R、R Studio 和数据分析技能的先验知识,因此本章不试图介绍 R。提供了 Spark 计算引擎的非常简要的概述作为快速回顾。读者应该阅读本书的前三章,以更深入地了解 Spark 编程模型和 DataFrames。这些知识非常重要,因为开发人员必须了解他的代码的哪一部分在本地 R 环境中执行,哪一部分由 Spark 计算引擎处理。本章涵盖的主题如下:

  • SparkR 基础知识

  • R 与 Spark 的优势及其局限性

  • 使用 SparkR 进行编程

  • SparkR DataFrames

  • 机器学习

SparkR 基础知识

R 是一种用于统计计算和图形的语言和环境。SparkR 是一个 R 包,提供了一个轻量级的前端,以便从 R 中访问 Apache Spark。SparkR 的目标是结合 R 环境提供的灵活性和易用性,以及 Spark 计算引擎提供的可伸缩性和容错性。在讨论 SparkR 如何实现其目标之前,让我们回顾一下 Spark 的架构。

Apache Spark 是一个快速、通用、容错的框架,用于大规模分布式数据集的交互式和迭代计算。它支持各种数据源和存储层。它提供统一的数据访问,可以结合不同的数据格式、流数据,并使用高级、可组合的操作定义复杂的操作。您可以使用 Scala、Python 或 R shell(或没有 shell 的 Java)交互式地开发应用程序。您可以将其部署在家用台式机上,也可以在成千上万个节点的大型集群上运行,处理 PB 级数据。

注意

SparkR 起源于 AMPLab(amplab.cs.berkeley.edu/),旨在探索将 R 的易用性与 Spark 的可伸缩性相结合的不同技术。它作为 Apache Spark 1.4 中的一个 alpha 组件发布,该版本于 2015 年 6 月发布。Spark 1.5 版本改进了 R 的可用性,并引入了带有广义线性模型GLMs)的 MLlib 机器学习包。2016 年 1 月发布的 Spark 1.6 版本增加了一些功能,例如模型摘要和特征交互。2016 年 7 月发布的 Spark 2.0 版本带来了一些重要功能,例如 UDF,改进的模型覆盖范围,DataFrames 窗口函数 API 等。

从 R 环境访问 SparkR

您可以从 R shell 或 R Studio 启动 SparkR。SparkR 的入口点是 SparkSession 对象,它表示与 Spark 集群的连接。运行 R 的节点成为驱动程序。由 R 程序创建的任何对象都驻留在此驱动程序上。通过 SparkSession 创建的任何对象都将创建在集群中的工作节点上。以下图表描述了 R 与运行在集群上的 Spark 的运行时视图。请注意,R 解释器存在于集群中的每个工作节点上。以下图表不显示集群管理器,也不显示存储层。您可以使用任何集群管理器(例如 Yarn 或 Mesos)和任何存储选项,例如 HDFS、Cassandra 或 Amazon S3:

从 R 环境访问 SparkR

来源:http://www.slideshare.net/Hadoop_Summit/w-145p210-avenkataraman。

通过传递应用程序名称、内存、核心数和要连接的集群管理器等信息来创建 SparkSession 对象。与 Spark 引擎的任何交互都是通过此 SparkSession 对象启动的。如果使用 SparkR shell,则已为您创建了 SparkSession 对象。否则,您必须显式创建它。此对象替换了 Spark 1.x 版本中存在的 SparkContext 和 SQLContext 对象。这些对象仍然存在以确保向后兼容性。即使前面的图表显示了 SparkContext,您也应该将其视为 Spark 2.0 之后的 SparkSession。

现在我们已经了解了如何从 R 环境访问 Spark,让我们来看看 Spark 引擎提供的核心数据抽象。

RDD 和 DataFrames

在 Spark 引擎的核心是其主要数据抽象,称为弹性分布式数据集RDD)。RDD 由一个或多个数据源组成,并由用户定义为对一个或多个稳定(具体)数据源的一系列转换(也称为血统)。每个 RDD 或 RDD 分区都知道如何使用血统图在失败时重新创建自己,从而提供容错性。RDD 是不可变的数据结构,这意味着它可以在线程之间共享而无需同步开销,因此适合并行化。RDD 上的操作要么是转换,要么是动作。转换是血统中的单个步骤。换句话说,它们是创建 RDD 的操作,因为每个转换都是从稳定数据源获取数据或转换不可变 RDD 并创建另一个 RDD。转换只是声明;直到对该 RDD 应用动作操作之前,它们才会被评估。动作是利用 RDD 的操作。

Spark 根据手头的动作优化 RDD 计算。例如,如果动作是读取第一行,只计算一个分区,跳过其余部分。它会自动执行内存计算,并在内存不足时将其溢出到磁盘,并在所有核心上分布处理。如果在程序逻辑中频繁访问 RDD,则可以对其进行缓存,从而避免重新计算开销。

R 语言提供了一个称为DataFrame的二维数据结构,使数据操作变得方便。Apache Spark 带有自己的 DataFrames,受到了 R 和 Python(通过 Pandas)中的 DataFrame 的启发。Spark DataFrame 是一种专门的数据结构,建立在 RDD 数据结构抽象之上。它提供了分布式 DataFrame 实现,从开发者的角度看,它看起来非常类似于 R DataFrame,同时可以支持非常大的数据集。Spark 数据集 API 为 DataFrame 添加了结构,这种结构在底层提供了更多的优化信息。

入门

现在我们已经了解了底层数据结构和运行时视图,是时候运行一些命令了。在本节中,我们假设您已经成功安装了 R 和 Spark,并将其添加到了路径中。我们还假设SPARK_HOME环境变量已设置。让我们看看如何从 R shell 或 R Studio 访问 SparkR:

> R  // Start R shell  
> Sys.getenv("SPARK_HOME") //Confirm SPARK_HOME is set 
  <Your SPARK_HOME path> 
> library(SparkR, lib.loc = 
    c(file.path(Sys.getenv("SPARK_HOME"), "R", "lib"))) 

Attaching package: 'SparkR' 
The following objects are masked from 'package:stats': 

    cov, filter, lag, na.omit, predict, sd, var, window 

The following objects are masked from 'package:base': 

    as.data.frame, colnames, colnames<-, drop, endsWith, intersect, 
    rank, rbind, sample, startsWith, subset, summary, transform, union 
> 

> //Try help(package=SparkR) if you want to more information 
//initialize SparkSession object 
>  sparkR.session()  
Java ref type org.apache.spark.sql.SparkSession id 1  
> 
Alternatively, you may launch sparkR shell which comes with predefined SparkSession. 

> bin/sparkR  // Start SparkR shell  
>      // For simplicity sake, no Log messages are shown here 
> //Try help(package=SparkR) if you want to more information 
> 

这就是您从 R 环境中访问 Spark DataFrames 的全部内容。

优势和限制

R 语言长期以来一直是数据科学家的通用语言。它简单易懂的 DataFrame 抽象、表达丰富的 API 和充满活力的包生态系统正是分析师所需要的。主要挑战在于可扩展性。SparkR 通过提供分布式内存中的 DataFrame 来弥合这一差距,而不会离开 R 生态系统。这种共生关系使用户能够获得以下好处:

  • 分析师无需学习新语言

  • SparkR 的 API 与 R 的 API 类似

  • 您可以从 R Studio 访问 SparkR,还可以使用自动完成功能

  • 大规模数据集的交互式探索性分析不再受内存限制或长时间的等待时间的限制

  • 从不同类型的数据源访问数据变得更加容易。大多数以前必须是命令式的任务现在已经变成了声明式的。查看第四章统一数据访问,了解更多信息

  • 您可以自由混合 dplyr、Spark 函数、SQL 和仍未在 Spark 中可用的 R 库

尽管结合两者最好的优势令人兴奋,但这种组合仍然存在一些限制。这些限制可能不会影响每种用例,但我们无论如何都需要意识到它们:

  • R 的固有动态特性限制了可用于催化剂优化器的信息。与静态类型语言(如 Scala)相比,我们可能无法充分利用优化,例如谓词推回。

  • SparkR 不支持所有已经在其他 API(如 Scala API)中可用的机器学习算法。

总之,在数据预处理方面使用 Spark,而在分析和可视化方面使用 R 似乎是未来最好的方法。

使用 SparkR 进行编程

到目前为止,我们已经了解了 SparkR 的运行时模型和提供容错性和可扩展性的基本数据抽象。我们已经了解了如何从 R shell 或 R Studio 访问 Spark API。现在是时候尝试一些基本和熟悉的操作了:

> 
> //Open the shell 
> 
> //Try help(package=SparkR) if you want to more information 
> 
> df <- createDataFrame(iris) //Create a Spark DataFrame 
> df    //Check the type. Notice the column renaming using underscore 
SparkDataFrame[Sepal_Length:double, Sepal_Width:double, Petal_Length:double, Petal_Width:double, Species:string] 
> 
> showDF(df,4) //Print the contents of the Spark DataFrame 
+------------+-----------+------------+-----------+-------+ 
|Sepal_Length|Sepal_Width|Petal_Length|Petal_Width|Species| 
+------------+-----------+------------+-----------+-------+ 
|         5.1|        3.5|         1.4|        0.2| setosa| 
|         4.9|        3.0|         1.4|        0.2| setosa| 
|         4.7|        3.2|         1.3|        0.2| setosa| 
|         4.6|        3.1|         1.5|        0.2| setosa| 
+------------+-----------+------------+-----------+-------+ 
>  
> head(df,2)  //Returns an R data.frame. Default 6 rows 
  Sepal_Length Sepal_Width Petal_Length Petal_Width Species 
1          5.1         3.5          1.4         0.2  setosa 
2          4.9         3.0          1.4         0.2  setosa 
> //You can use take(df,2) to get the same results 
//Check the dimensions 
> nrow(df) [1] 150 > ncol(df) [1] 5 

操作看起来与 R DataFrame 函数非常相似,因为 Spark DataFrames 是基于 R DataFrames 和 Python(Pandas)DataFrames 建模的。但是,如果不小心,这种相似性可能会引起混淆。您可能会在 R 的data.frame上运行计算密集型函数,以为负载会被分布,从而意外地使本地机器崩溃。例如,intersect 函数在两个包中具有相同的签名。您需要注意对象是SparkDataFrame(Spark DataFrame)还是data.frame(R DataFrame)。您还需要尽量减少本地 R data.frame对象和 Spark DataFrame 对象之间的来回转换。让我们通过尝试一些示例来感受这种区别:

> 
> //Open the SparkR shell 
> df <- createDataFrame(iris) //Create a Spark DataFrame 
> class(df) [1] "SparkDataFrame" attr(,"package") [1] "SparkR" 
> df2 <- head(df,2) //Create an R data frame 
> class(df2) 
 [1] "data.frame" 
> //Now try running some R command on both data frames 
> unique(df2$Species)   //Works fine as expected [1] "setosa" > unique(df$Species)    //Should fail Error in unique.default(df$Species) : unique() applies only to vectors > class(df$Species)   //Each column is a Spark's Column class [1] "Column" attr(,"package") [1] "SparkR" > class(df2$Species) [1] "character" 

函数名称屏蔽

现在我们已经尝试了一些基本操作,让我们稍微偏离一下。我们必须了解当加载的库具有与基本包或已加载的其他包重叠的函数名称时会发生什么。有时这被称为函数名称重叠、函数屏蔽或名称冲突。您可能已经注意到在加载 SparkR 包时提到了被屏蔽的对象的消息。这对于加载到 R 环境中的任何包都很常见,不仅仅是 SparkR。如果 R 环境已经包含与要加载的包中的函数同名的函数,那么对该函数的任何后续调用都会表现出最新加载的包中函数的行为。如果您想访问以前的函数而不是SparkR函数,您需要显式使用其包名称作为前缀,如下所示:

//First try in R environment, without loading sparkR 
//Try sampling from a column in an R data.frame 
>sample(iris$Sepal.Length,6,FALSE) //Returns any n elements [1] 5.1 4.9 4.7 4.6 5.0 5.4 >sample(head(iris),3,FALSE) //Returns any 3 columns 
//Try sampling from an R data.frame 
//The Boolean argument is for with_replacement 
> sample(head 
> head(sample(iris,3,TRUE)) //Returns any 3 columns
  Species Species.1 Petal.Width
1  setosa    setosa         0.2 
2  setosa    setosa         0.2 
3  setosa    setosa         0.2 
4  setosa    setosa         0.2 
5  setosa    setosa         0.2 
6  setosa    setosa         0.4 

//Load sparkR, initialize sparkSession and then execute this  
> df <- createDataFrame(iris) //Create a Spark DataFrame 
> sample_df <- sample(df,TRUE,0.3) //Different signature 
> dim(sample_df)  //Different behavior [1] 44  5 
> //Returned 30% of the original data frame and all columns 
> //Try with base prefix 
> head(base::sample(iris),3,FALSE)  //Call base package's sample
  Species Petal.Width Petal.Length 
1  setosa         0.2          1.4
2  setosa         0.2          1.4 
3  setosa         0.2          1.3 
4  setosa         0.2          1.5 
5  setosa         0.2          1.4 
6  setosa         0.4          1.7 

子集数据

R DataFrame 上的子集操作非常灵活,SparkR 试图保留这些操作或类似的等价操作。我们已经在前面的示例中看到了一些操作,但本节以有序的方式呈现它们:

//Subsetting data examples 
> b1 <- createDataFrame(beaver1) 
//Get one column 
> b1$temp 
Column temp    //Column class and not a vector 
> //Select some columns. You may use positions too 
> select(b1, c("day","temp")) 
SparkDataFrame[day:double, temp:double] 
>//Row subset based on conditions 
> head(subset(b1,b1$temp>37,select= c(2,3))) 
  time  temp 
1 1730 37.07 
2 1740 37.05 
3 1940 37.01 
4 1950 37.10 
5 2000 37.09 
6 2010 37.02 
> //Multiple conditions with AND and OR 
> head(subset(b1, between(b1$temp,c(36.0,37.0)) |  
        b1$time %in% 900 & b1$activ == 1,c(2:4)),2) 
 time  temp activ 
1  840 36.33     0 
2  850 36.34     0 

提示

在撰写本书时(Apache Spark 2.0 发布),基于行索引的切片不可用。您将无法使用df[n,]df[m:n,]语法获取特定行或行范围。

//For example, try on a normal R data.frame 
> beaver1[2:4,] 
  day time  temp activ 
2 346  850 36.34     0 
3 346  900 36.35     0 
4 346  910 36.42     0 
//Now, try on Spark Data frame 
> b1[2:4,] //Throws error 
Expressions other than filtering predicates are not supported in the first parameter of extract operator [ or subset() method. 
> 

列函数

您可能已经注意到在子集数据部分中的列函数between。这些函数在Column类上操作。正如名称所示,这些函数一次在单个列上操作,并且通常用于子集 DataFrame。除了在列内的值上工作之外,您还可以将列附加到 DataFrame 或从 DataFrame 中删除一个或多个列。负列下标可以用于省略列,类似于 R。以下示例展示了在子集操作中使用Column类函数,然后添加和删除列:

> //subset using Column operation using airquality dataset as df 
> head(subset(df,isNull(df$Ozone)),2) 
  Ozone Solar_R Wind Temp Month Day 
1    NA      NA 14.3   56     5   5 
2    NA     194  8.6   69     5  10 
> 
> //Add column and drop column examples 
> b1 <- createDataFrame(beaver1) 

//Add new column 
> b1$inRetreat <- otherwise(when(b1$activ == 0,"No"),"Yes") 
 head(b1,2) 
  day time  temp activ inRetreat 
1 346  840 36.33     0        No 
2 346  850 36.34     0        No 
> 
//Drop a column.  
> b1$day <- NULL 
> b1  // Example assumes b1$inRetreat does not exist 
SparkDataFrame[time:double, temp:double, activ:double] 
> //Drop columns using negative subscripts 
> b2 <- b1[,-c(1,4)]  > head(b2) 
   time  temp 
1  840 36.33 
2  850 36.34 
3  900 36.35 
4  910 36.42 
5  920 36.55 
6  930 36.69 
>  

分组数据

DataFrame 数据可以使用group_by函数进行分组,类似于 SQL。执行此类操作的多种方式。我们在本节中介绍了一个稍微复杂的示例。此外,我们使用了magrittr库提供的%>%,也称为前向管道运算符,它提供了一个链接命令的机制:

> //GroupedData example using iris data as df 
> //Open SparkR shell and create df using iris dataset  
> groupBy(df,"Species") 
GroupedData    //Returns GroupedData object 
> library(magrittr)  //Load the required library 
//Get group wise average sepal length 
//Report results sorted by species name 
>df2 <- df %>% groupBy("Species") %>%  
          avg("Sepal_Length") %>%  
          withColumnRenamed("avg(Sepal_Length)","avg_sepal_len") %>% 
          orderBy ("Species") 
//Format the computed double column 
df2$avg_sepal_len <- format_number(df2$avg_sepal_len,2) 
showDF(df2) 
+----------+-------------+ 
|   Species|avg_sepal_len| 
+----------+-------------+ 
|    setosa|         5.01| 
|versicolor|         5.94| 
| virginica|         6.59| 
+----------+-------------+ 

您可以继续使用前向管道运算符链接操作。仔细查看代码中的列重命名部分。列名参数是前面操作的输出,在此操作开始之前已经完成,因此您可以安全地假定avg(sepal_len)列已经存在。format_number按预期工作,这是另一个方便的Column操作。

下一节有一个类似的示例,使用GroupedData及其等效的dplyr实现。

SparkR DataFrames

在本节中,我们尝试一些有用的常用操作。首先,我们尝试传统的 R/dplyr操作,然后展示使用 SparkR API 的等效操作:

> //Open the R shell and NOT SparkR shell  
> library(dplyr,warn.conflicts=FALSE)  //Load dplyr first 
//Perform a common, useful operation  
> iris %>%               
+   group_by(Species) %>% +   summarise(avg_length = mean(Sepal.Length),  
+             avg_width = mean(Sepal.Width)) %>% +   arrange(desc(avg_length)) 
Source: local data frame [3 x 3] 
     Species avg_length avg_width 
      (fctr)      (dbl)     (dbl) 
1  virginica      6.588     2.974 
2 versicolor      5.936     2.770 
3     setosa      5.006     3.428 

//Remove from R environment 
> detach("package:dplyr",unload=TRUE) 

此操作与 SQL 分组非常相似,并且后跟排序。它在 SparkR 中的等效实现也与dplyr示例非常相似。查看以下示例。注意方法名称并将其与前面的dplyr示例进行比较:

> //Open SparkR shell and create df using iris dataset  
> collect(arrange(summarize(groupBy(df,df$Species),  +     avg_sepal_length = avg(df$Sepal_Length), +     avg_sepal_width = avg(df$Sepal_Width)), +     "avg_sepal_length", decreasing = TRUE))  
     Species avg_sepal_length avg_sepal_width 
1     setosa            5.006           3.428 
2 versicolor            5.936           2.770 
3  virginica            6.588           2.974 

SparkR 旨在尽可能接近现有的 R API。因此,方法名称看起来与dplyr方法非常相似。例如,看看具有groupBy的示例,而dplyr具有group_by。SparkR 支持冗余函数名称。例如,它有group_by以及groupBy,以满足来自不同编程环境的开发人员。dplyr和 SparkR 中的方法名称再次非常接近 SQL 关键字GROUP BY。但是这些方法调用的顺序不同。示例还显示了将 Spark DataFrame 转换为 R data.frame的附加步骤,使用collect。这些方法是从内到外排列的,意思是首先对数据进行分组,然后进行汇总,然后进行排列。这是可以理解的,因为在 SparkR 中,内部方法中创建的 DataFrame 成为其直接前任的参数,依此类推。

SQL 操作

如果您对前面示例中的语法不太满意,您可能希望尝试编写一个 SQL 字符串,如所示,它与前面的示例完全相同,但使用了传统的 SQL 语法:

> //Register the Spark DataFrame as a table/View 
> createOrReplaceTempView(df,"iris_vw")  
//Look at the table structure and some rows
> collect(sql(sqlContext, "SELECT * FROM iris_tbl LIMIT 5"))
    Sepal_Length Sepal_Width Petal_Length Petal_Width Species 
1          5.1         3.5          1.4         0.2  setosa 
2          4.9         3.0          1.4         0.2  setosa 
3          4.7         3.2          1.3         0.2  setosa 
4          4.6         3.1          1.5         0.2  setosa 
5          5.0         3.6          1.4         0.2  setosa 
> //Try out the above example using SQL syntax 
> collect(sql(sqlContext, "SELECT Species,       avg(Sepal_Length) avg_sepal_length,      avg(Sepal_Width) avg_sepal_width       FROM iris_tbl        GROUP BY Species       ORDER BY avg_sepal_length desc")) 

  Species avg_sepal_length avg_sepal_width 

1  virginica            6.588           2.974 
2 versicolor            5.936           2.770 
3     setosa            5.006           3.428 

如果您习惯从 RDBMS 表中获取数据,前面的示例看起来像是实现手头操作的最自然方式。但我们是如何做到这一点的呢?第一条语句告诉 Spark 注册一个临时表(或者,如其名称所示,一个视图,表的逻辑抽象)。这并不完全等同于数据库表。它是临时的,意味着在销毁 SparkSession 对象时会被销毁。您并没有将数据明确写入任何 RDBMS 数据存储(您必须使用SaveAsTable)。但是一旦您将 Spark DataFrame 注册为临时表,就可以自由地使用 SQL 语法对该 DataFrame 进行操作。下一条语句是一个基本的SELECT语句,显示列名,然后是五行,由LIMIT关键字指定。下一个 SQL 语句创建了一个包含 Species 列的 Spark DataFrame,后跟两个平均列,按平均萼片长度排序。然后,通过使用 collect 将此 DataFrame 作为 R data.frame收集。最终结果与前面的示例完全相同。您可以自由选择使用任何语法。有关更多信息和示例,请查看第四章中的 SQL 部分,统一数据访问

集合操作

SparkR 中可以直接使用常见的集合操作,如unionintersectionminus。实际上,当加载 SparkR 时,警告消息显示intersect作为其中一个屏蔽函数。以下示例基于beaver数据集:

> //Create b1 and b2 DataFrames using beaver1 and beaver2 datasets 
> b1 <- createDataFrame(beaver1) 
> b2 <- createDataFrame(beaver2) 
//Get individual and total counts 
> > c(nrow(b1), nrow(b2), nrow(b1) + nrow(b2)) 
[1] 114 100 214 
//Try adding both data frames using union operation 
> nrow(unionAll(b1,b2)) 
[1] 214     //Sum of two datsets 
> //intersect example 
//Remove the first column (day) and find intersection 
showDF(intersect(b1[,-c(1)],b2[,-c(1)])) 

+------+-----+-----+ 
|  time| temp|activ| 
+------+-----+-----+ 
|1100.0|36.89|  0.0| 
+------+-----+-----+ 
> //except (minus or A-B) is covered in machine learning examples   

合并 DataFrame

下一个示例说明了使用merge命令连接两个 DataFrame。示例的第一部分显示了 R 的实现,下一部分显示了 SparkR 的实现:

> //Example illustrating data frames merging using R (Not SparkR) 
> //Create two data frames with a matching column 
//Products df with two rows and two columns 
> products_df <- data.frame(rbind(c(101,"Product 1"), 
                    c(102,"Product 2"))) 
> names(products_df) <- c("Prod_Id","Product") 
> products_df 
 Prod_Id   Product 
1     101 Product 1 
2     102 Product 2 

//Sales df with sales for each product and month 24x3 
> sales_df <- data.frame(cbind(rep(101:102,each=12), month.abb, 
                    sample(1:10,24,replace=T)*10)) 
> names(sales_df) <- c("Prod_Id","Month","Sales") 

//Look at first 2 and last 2 rows in the sales_df 
> sales_df[c(1,2,23,24),] 
   Prod_Id Month Sales 
1      101   Jan    60 
2      101   Feb    40 
23     102   Nov    20 
24     102   Dec   100 

> //merge the data frames and examine the data 
> total_df <- merge(products_df,sales_df) 
//Look at the column names 
> colnames(total_df) 
> [1] "Prod_Id" "Product" "Month"   "Sales" 

//Look at first 2 and last 2 rows in the total_df 
> total_df[c(1,2,23,24),]     
   Prod_Id   Product Month Sales 
1      101 Product 1   Jan    10 
2      101 Product 1   Feb    20 
23     102 Product 2   Nov    60 
24     102 Product 2   Dec    10 

上述代码完全依赖于 R 的基本包。为简单起见,我们在两个 DataFrame 中使用了相同的连接列名称。下一段代码演示了使用 SparkR 的相同示例。它看起来与前面的代码类似,因此请仔细查看其中的区别:

> //Example illustrating data frames merging using SparkR 
> //Create an R data frame first and then pass it on to Spark 
> //Watch out the base prefix for masked rbind function 
> products_df <- createDataFrame(data.frame( 
    base::rbind(c(101,"Product 1"), 
    c(102,"Product 2")))) 
> names(products_df) <- c("Prod_Id","Product") 
>showDF(products_df) 
+-------+---------+ 
|Prod_Id|  Product| 
+-------+---------+ 
|    101|Product 1| 
|    102|Product 2| 
+-------+---------+ 
> //Create Sales data frame 
> //Notice the as.data.frame similar to other R functions 
> //No cbind in SparkR so no need for base:: prefix 
> sales_df <- as.DataFrame(data.frame(cbind( 
             "Prod_Id" = rep(101:102,each=12), 
"Month" = month.abb, 
"Sales" = base::sample(1:10,24,replace=T)*10))) 
> //Check sales dataframe dimensions and some random rows  
> dim(sales_df) 
[1] 24  3 
> collect(sample(sales_df,FALSE,0.20)) 
  Prod_Id Month Sales 
1     101   Sep    50 
2     101   Nov    80 
3     102   Jan    90 
4     102   Jul   100 
5     102   Nov    20 
6     102   Dec    50 
> //Merge the data frames. The following merge is from SparkR library 
> total_df <- merge(products_df,sales_df) 
// You may try join function for the same purpose 
//Look at the columns in total_df 
> total_df 
SparkDataFrame[Prod_Id_x:string, Product:string, Prod_Id_y:string, Month:string, Sales:string] 
//Drop duplicate column 
> total_df$Prod_Id_y <- NULL    
> head(total_df) 
  Prod_Id_x   Product Month Sales 
1       101 Product 1   Jan    40 
2       101 Product 1   Feb    10 
3       101 Product 1   Mar    90 
4       101 Product 1   Apr    10 
5       101 Product 1   May    50 
6       101 Product 1   Jun    70 
> //Note: As of Spark 2.0 version, SparkR does not support 
    row sub-setting  

您可能想尝试不同类型的连接,例如左外连接和右外连接,或不同的列名,以更好地理解此函数。

机器学习

SparkR 提供了现有 MLLib 函数的包装器。R 公式被实现为 MLLib 特征转换器。转换器是一个 ML 管道(spark.ml)阶段,它以 DataFrame 作为输入并产生另一个 DataFrame 作为输出,通常包含一些附加列。特征转换器是一种将输入列转换为特征向量的转换器,这些特征向量被附加到源 DataFrame。例如,在线性回归中,字符串输入列被独热编码,数值被转换为双精度数。标签列将被附加(如果数据框中没有的话)作为响应变量的副本。

在这一部分,我们涵盖了朴素贝叶斯和高斯 GLM 模型的示例代码。我们不解释模型本身或它们产生的摘要。相反,我们直接讨论如何使用 SparkR 来完成这些操作。

朴素贝叶斯模型

朴素贝叶斯模型是一个直观简单的模型,适用于分类数据。我们将使用朴素贝叶斯模型训练一个样本数据集。我们不会解释模型的工作原理,而是直接使用 SparkR 来训练模型。如果您想要更多信息,请参考第六章 机器学习

这个例子使用了一个包含二十名学生的平均分数和出勤情况的数据集。实际上,这个数据集已经在第六章 机器学习中被引入,用于训练集成。然而,让我们重新审视一下它的内容。

学生根据一组明确定义的规则被授予及格不及格。两名 ID 为10091020的学生被授予及格,即使在其他情况下他们本来会不及格。尽管我们没有向模型提供实际规则,但我们期望模型预测这两名学生的结果为不及格。以下是及格/不及格的标准:

  • 分数 < 40 => 不及格

  • 出勤不足 => 不及格

  • 分数超过 40 且出勤全 => 及格

  • 分数 > 60 且至少出勤足够 => 及格以下是训练朴素贝叶斯模型的示例:

//Example to train Naïve Bayes model 

//Read file 
> myFile <- read.csv("../work/StudentsPassFail.csv") //R data.frame 
> df <- createDataFrame(myFile) //sparkDataFrame 
//Look at the data 
> showDF(df,4) 
+---------+---------+----------+------+ 
|StudentId|Avg_Marks|Attendance|Result| 
+---------+---------+----------+------+ 
|     1001|     48.0|      Full|  Pass| 
|     1002|     21.0|    Enough|  Fail| 
|     1003|     24.0|    Enough|  Fail| 
|     1004|      4.0|      Poor|  Fail| 
+---------+---------+----------+------+ 

//Make three buckets out of Avg_marks 
// A >60; 40 < B < 60; C > 60 
> df$marks_bkt <- otherwise(when(df$Avg_marks < 40, "C"), 
                           when(df$Avg_marks > 60, "A")) 
> df$marks_bkt <- otherwise(when(df$Avg_marks < 40, "C"), 
                           when(df$Avg_marks > 60, "A")) 
> df <- fillna(df,"B",cols="marks_bkt") 
//Split train and test 
> trainDF <- sample(df,TRUE,0.7) 
> testDF <- except(df, trainDF) 

//Build model by supplying RFormula, training data 
> model <- spark.naiveBayes(Result ~ Attendance + marks_bkt, data = trainDF) 
> summary(model) 
$apriori 
          Fail      Pass 
[1,] 0.6956522 0.3043478 

$tables 
     Attendance_Poor Attendance_Full marks_bkt_C marks_bkt_B 
Fail 0.5882353       0.1764706       0.5882353   0.2941176   
Pass 0.125           0.875           0.125       0.625       

//Run predictions on test data 
> predictions <- predict(model, newData= testDF) 
//Examine results 
> showDF(predictions[predictions$Result != predictions$prediction, 
     c("StudentId","Attendance","Avg_Marks","marks_bkt", "Result","prediction")]) 
+---------+----------+---------+---------+------+----------+                     
|StudentId|Attendance|Avg_Marks|marks_bkt|Result|prediction| 
+---------+----------+---------+---------+------+----------+ 
|     1010|      Full|     19.0|        C|  Fail|      Pass| 
|     1019|    Enough|     45.0|        B|  Fail|      Pass| 
|     1014|      Full|     12.0|        C|  Fail|      Pass| 
+---------+----------+---------+---------+------+----------+ 
//Note that the predictions are not exactly what we anticipate but models are usually not 100% accurate 

高斯 GLM 模型

在这个例子中,我们尝试根据臭氧、太阳辐射和风的值来预测温度:

> //Example illustrating Gaussian GLM model using SparkR 
> a <- createDataFrame(airquality) 
//Remove rows with missing values 
> b <- na.omit(a) 
> //Inspect the dropped rows with missing values 
> head(except(a,b),2)    //MINUS set operation 
  Ozone Solar_R Wind Temp Month Day 
1    NA     186  9.2   84     6   4 
2    NA     291 14.9   91     7  14 

> //Prepare train data and test data 
traindata <- sample(b,FALSE,0.8) //Not base::sample 
testdata <- except(b,traindata) 

> //Build model 
> model <- glm(Temp ~ Ozone + Solar_R + Wind,  
          data = traindata, family = "gaussian") 
> // Get predictions 
> predictions <- predict(model, newData = testdata) 
> head(predictions[,c(predictions$Temp, predictions$prediction)], 
                 5) 
  Temp prediction 
1   90   81.84338 
2   79   80.99255 
3   88   85.25601 
4   87   76.99957 
5   76   71.75683 

总结

到目前为止,SparkR 不支持 Spark 中所有可用的算法,但正在积极开发以弥合差距。Spark 2.0 版本已经改进了算法覆盖范围,包括朴素贝叶斯、k 均值聚类和生存回归。查看最新的支持算法文档。在将来,我们将继续努力推出 SparkR 的 CRAN 版本,更好地与 R 包和 Spark 包集成,并提供更好的 RFormula 支持。

参考资料

第八章:分析非结构化数据

在这个大数据时代,非结构化数据的激增是令人震惊的。存在许多方法,如数据挖掘、自然语言处理(NLP)、信息检索等,用于分析非结构化数据。由于各种业务中非结构化数据的快速增长,可扩展的解决方案已成为当务之急。Apache Spark 配备了用于文本分析的开箱即用算法,并支持自定义开发默认情况下不可用的算法。

在上一章中,我们已经展示了 SparkR 如何利用 Spark 的 R API 来发挥其强大功能,而无需学习一种新语言。在本章中,我们将步入一个全新的维度,探索利用 Spark 从非结构化数据中提取信息的算法和技术。

作为本章的先决条件,对 Python 或 Scala 编程的基本理解以及对文本分析和机器学习的整体理解是很有帮助的。然而,我们已经涵盖了一些理论基础,并提供了一套实际示例,使其更易于理解和实施。本章涵盖的主题包括:

  • 非结构化数据的来源

  • 处理非结构化数据

  • 计数向量化器

  • TF-IDF

  • 停用词去除

  • 归一化/缩放

  • Word2Vec

  • n-gram 建模

  • 文本分类

  • 朴素贝叶斯分类器

  • 文本聚类

  • K 均值

  • 降维

  • 奇异值分解

  • 主成分分析

  • 总结

非结构化数据的来源

自上世纪八九十年代的电子表格和商业智能工具以来,数据分析已经取得了长足的进步。计算能力的巨大提升、复杂算法和开源文化的推动,促成了数据分析以及其他领域的前所未有的增长。这些技术的进步为新的机遇和新的挑战铺平了道路。企业开始着眼于从以往难以处理的数据源中生成见解,如内部备忘录、电子邮件、客户满意度调查等。数据分析现在包括这种非结构化的、通常是基于文本的数据,以及传统的行和列数据。在关系型数据库管理系统表中存储的高度结构化数据和完全非结构化的纯文本之间,我们有 NoSQL 数据存储、XML 或 JSON 文档以及图形或网络数据源等半结构化数据源。根据目前的估计,非结构化数据约占企业数据的 80%,并且正在迅速增长。卫星图像、大气数据、社交网络、博客和其他网页、患者记录和医生笔记、公司内部通信等等 - 所有这些组合只是非结构化数据源的一个子集。

我们已经看到了成功利用非结构化数据和结构化数据的数据产品。一些公司利用社交网络的力量为他们的客户提供可操作的见解。新兴领域,如情感分析和多媒体分析,正在从非结构化数据中获取见解。然而,分析非结构化数据仍然是一项艰巨的任务。例如,当代文本分析工具和技术无法识别讽刺。然而,潜在的好处无疑超过了局限性。

处理非结构化数据

非结构化数据不适用于大多数编程任务。它必须以各种不同的方式进行处理,以便作为任何机器学习算法的输入或进行可视化分析。广义上,非结构化数据分析可以被视为以下图表所示的一系列步骤:

处理非结构化数据

数据预处理是任何非结构化数据分析中最关键的步骤。幸运的是,随着时间的积累,已经积累了几种被证明有效的技术,这些技术非常有用。Spark 通过ml.features包提供了大部分这些技术。大多数技术旨在将文本数据转换为简洁的数字向量,这些向量可以轻松地被机器学习算法消化。开发人员应该了解其组织的具体要求,以确定最佳的预处理工作流程。请记住,更好、相关的数据是产生更好洞察的关键。

让我们探讨一些处理原始文本并将其转换为数据框的示例。第一个示例将一些文本作为输入并提取所有类似日期的字符串,而第二个示例从 twitter 文本中提取标签。第一个示例只是一个热身,使用简单的正则表达式分词器特征转换器,而不使用任何特定于 spark 的库。它还引起了您对可能的误解的注意。例如,形式为 1-11-1111 的产品代码可能被解释为日期。第二个示例说明了一个非平凡的、多步骤的提取过程,最终只得到了所需的标签。用户定义的函数udf)和 ML 管道在开发这种多步骤的提取过程中非常有用。本节的其余部分描述了 apache Spark 中提供的一些更方便的工具。

示例-1: 从文本中提取类似日期的字符串

Scala

scala> import org.apache.spark.ml.feature.RegexTokenizer
import org.apache.spark.ml.feature.RegexTokenizer
scala> val date_pattern: String = "\\d{1,4}[/ -]\\d{1,4}[/ -]\\d{1,4}"
date_pattern: String = \d{1,4}[/ -]\d{1,4}[/ -]\d{1,4}
scala> val textDF  = spark.createDataFrame(Seq(
    (1, "Hello 1996-12-12 this 1-21-1111 is a 18-9-96 text "),
    (2, "string with dates in different 01/02/89 formats"))).
    toDF("LineNo","Text")
textDF: org.apache.spark.sql.DataFrame = [LineNo: int, Text: string]
scala> val date_regex = new RegexTokenizer().
        setInputCol("Text").setOutputCol("dateStr").
        setPattern(date_pattern).setGaps(false)
date_regex: org.apache.spark.ml.feature.RegexTokenizer = regexTok_acdbca6d1c4c
scala> date_regex.transform(textDF).select("dateStr").show(false)
+--------------------------------+
|dateStr                         |
+--------------------------------+
|[1996-12-12, 1-21-1111, 18-9-96]|
|[01/02/89]                      |
+--------------------------------+

Python

// Example-1: Extract date like strings from text
>>> from pyspark.ml.feature import RegexTokenizer
>>> date_pattern = "\\d{1,4}[/ -]\\d{1,4}[/ -]\\d{1,4}"
>>> textDF  = spark.createDataFrame([
        [1, "Hello 1996-12-12 this 1-21-1111 is a 18-9-96 text "],
        [2, "string with dates in different 01/02/89 formats"]]).toDF(
        "LineNo","Text")
>>> date_regex = RegexTokenizer(inputCol="Text",outputCol="dateStr",
            gaps=False, pattern=date_pattern)
>>> date_regex.transform(textDF).select("dateStr").show(5,False)
+--------------------------------+
|dateStr                         |
+--------------------------------+
|[1996-12-12, 1-21-1111, 18-9-96]|
|[01/02/89]                      |
+--------------------------------+

前面的例子定义了一个正则表达式模式,用于识别日期字符串。正则表达式模式和示例文本 DataFrame 被传递给RegexTokenizer以提取匹配的日期字符串。gaps=False选项选择匹配的字符串,False的值将使用给定的模式作为分隔符。请注意,显然不是日期的1-21-1111也被选中。

下一个示例从 twitter 文本中提取标签并识别最流行的标签。您也可以使用相同的方法收集哈希(#)标签。

此示例使用内置函数explode,它将单个具有值数组的行转换为多个行,每个数组元素一个值。

示例-2:从 twitter“文本”中提取标签

Scala

//Step1: Load text containing @ from source file
scala> val path = "<Your path>/tweets.json"
path: String = <Your path>/tweets.json
scala> val raw_df = spark.read.text(path).filter($"value".contains("@"))
raw_df: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [value: string]
//Step2: Split the text to words and filter out non-tag words
scala> val df1 = raw_df.select(explode(split('value, " ")).as("word")).
        filter($"word".startsWith("@"))
df1: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [word: string]
//Step3: compute tag-wise counts and report top 5
scala> df1.groupBy($"word").agg(count($"word")).
        orderBy($"count(word)".desc).show(5)
+------------+-----------+
+                                                     
|        word|count(word)|
+------------+-----------+
|@ApacheSpark|         15|
|    @SSKapci|          9|
|@databricks:|          4|
|     @hadoop|          4|
| @ApacheApex|          4|
+------------+-----------+

Python

>> from pyspark.sql.functions import explode, split
//Step1: Load text containing @ from source file
>>> path ="<Your path>/tweets.json"
>>> raw_df1 = spark.read.text(path)
>>> raw_df = raw_df1.where("value like '%@%'")
>>> 
//Step2: Split the text to words and filter out non-tag words
>>> df = raw_df.select(explode(split("value"," ")))
>>> df1 = df.where("col like '@%'").toDF("word")
>>> 
//Step3: compute tag-wise counts and report top 5
>>> df1.groupBy("word").count().sort(
     "count",ascending=False).show(5)
+------------+-----+
+                                                        
|        word|count|
+------------+-----+
|@ApacheSpark|   15|
|    @SSKapci|    9|
|@databricks:|    4|
| @ApacheApex|    4|
|     @hadoop|    4|
+------------+-----+

计数向量化器

计数向量化器从文档中提取词汇(标记)并在没有字典的情况下生成CountVectorizerModel模型。正如其名称所示,文本文档被转换为标记和计数的向量。该模型生成文档对词汇的稀疏表示。

您可以微调行为以限制词汇量大小、最小标记计数等,使其适用于您的业务案例。

//示例 3:计数向量化器示例

Scala

scala> import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel}
import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel}
scala> import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.DataFrame
scala> import org.apache.spark.ml.linalg.Vector
import org.apache.spark.ml.linalg.Vector
scala> val df: DataFrame = spark.createDataFrame(Seq(
  (0, Array("ant", "bat", "cat", "dog", "eel")),
  (1, Array("dog","bat", "ant", "bat", "cat"))
)).toDF("id", "words")
df: org.apache.spark.sql.DataFrame = [id: int, words: array<string>]
scala>
// Fit a CountVectorizerModel from the corpus 
// Minimum occurrences (DF) is 2 and pick 10 top words(vocabsize) only scala> val cvModel: CountVectorizerModel = new CountVectorizer().
        setInputCol("words").setOutputCol("features").
        setMinDF(2).setVocabSize(10).fit(df)
cvModel: org.apache.spark.ml.feature.CountVectorizerModel = cntVec_7e79157ba561
// Check vocabulary. Words are arranged as per frequency 
// eel is dropped because it is below minDF = 2 scala> cvModel.vocabulary
res6: Array[String] = Array(bat, dog, cat, ant)
//Apply the model on document
scala> val cvDF: DataFrame = cvModel.transform(df)
cvDF: org.apache.spark.sql.DataFrame = [id: int, words: array<string> ... 1 more field]
//Check the word count scala> cvDF.select("features").collect().foreach(row =>
println(row(0).asInstanceOf[Vector].toDense))

[1.0,1.0,1.0,1.0]
[2.0,1.0,1.0,1.0]

Python

>>> from pyspark.ml.feature import CountVectorizer,CountVectorizerModel
>>> from pyspark.ml.linalg import Vector
>>> 
// Define source DataFrame
>>> df = spark.createDataFrame([
    [0, ["ant", "bat", "cat", "dog", "eel"]],
    [1, ["dog","bat", "ant", "bat", "cat"]]
  ]).toDF("id", "words")
>>> 
// Fit a CountVectorizerModel from the corpus
// Minimum occorrences (DF) is 2 and pick 10 top words(vocabsize) only
>>> cvModel = CountVectorizer(inputCol="words", outputCol="features",
        minDF = 2, vocabSize = 10).fit(df)
>>> 
// Check vocabulary. Words are arranged as per frequency
// eel is dropped because it is below minDF = 2
>>> cvModel.vocabulary
[u'bat', u'ant', u'cat', u'dog']
//Apply the model on document
>>> cvDF = cvModel.transform(df)
//Check the word count
>>> cvDF.show(2,False)
+---+-------------------------+-------------------------------+
|id |words                    |features                       |
+---+-------------------------+-------------------------------+
|0  |[ant, bat, cat, dog, eel]|(4,[0,1,2,3],[1.0,1.0,1.0,1.0])|
|1  |[dog, bat, ant, bat, cat]|(4,[0,1,2,3],[2.0,1.0,1.0,1.0])|
+---+-------------------------+-------------------------------+

输入

 |id | text                  
 +---+-------------------------+-------------------------------+
 |0  | "ant", "bat", "cat", "dog", "eel"     
 |1  | "dog","bat", "ant", "bat", "cat"

输出

id| text                               | Vector 
--|------------------------------------|-------------------- 
0 | "ant", "bat", "cat", "dog", "eel" |[1.0,1.0,1.0,1.0] 
1 | "dog","bat", "ant", "bat", "cat"   |[2.0,1.0,1.0,1.0]

前面的例子演示了CountVectorizer作为估计器的工作原理,用于提取词汇并生成CountVectorizerModel。请注意,特征向量的顺序对应于词汇而不是输入序列。让我们也看看如何通过预先构建字典来实现相同的效果。但是,请记住它们有自己的用例。

示例 4:使用先验词汇定义 CountVectorizerModel

Scala

// Example 4: define CountVectorizerModel with a-priori vocabulary
scala> val cvm: CountVectorizerModel = new CountVectorizerModel(
        Array("ant", "bat", "cat")).
        setInputCol("words").setOutputCol("features")
cvm: org.apache.spark.ml.feature.CountVectorizerModel = cntVecModel_ecbb8e1778d5

//Apply on the same data. Feature order corresponds to a-priory vocabulary order scala> cvm.transform(df).select("features").collect().foreach(row =>
        println(row(0).asInstanceOf[Vector].toDense))
[1.0,1.0,1.0]
[1.0,2.0,1.0]

Python

截至 Spark 2.0.0 尚不可用

TF-IDF

词频-逆文档频率TF-IDF)可能是文本分析中最流行的度量之一。这个度量指示了给定术语在一组文档中的重要性。它包括两个度量,词频TF)和逆文档频率IDF)。让我们逐一讨论它们,然后看看它们的综合效果。

TF 是一个词在文档中相对重要性的度量,通常是该词频率除以文档中的词数。假设一个文本文档包含 100 个单词,其中单词apple出现了八次。apple的 TF 将是TF = (8 / 100) = 0.08。因此,一个词在文档中出现的频率越高,其 TF 系数就越大。

IDF 是一个词在整个文档集合中的重要性的度量,也就是说,这个词在所有文档中出现的频率有多低。一个词的重要性与其频率成反比。Spark 提供了两种单独的方法来执行这些任务。假设我们有 600 万个文档,单词apple出现在其中的 6000 个文档中。那么,IDF 被计算为IDF = Log(6,000,000 / 6,000) = 3。如果你仔细观察这个例子,分母越小,IDF 值就越大。这意味着包含特定词的文档数量越少,它的重要性就越高。

因此,TF-IDF 得分将是TF * IDF = 0.08 * 3 = 0.24。请注意,它会惩罚在文档中更频繁出现且不重要的词,比如thethisa等,并赋予更重要的词更大的权重。

在 Spark 中,TF 被实现为 HashingTF。它接受一系列术语(通常是分词器的输出)并产生一个固定长度的特征向量。它执行特征哈希将术语转换为固定长度的索引。然后 IDF 接受这些特征向量(HashingTF 的输出)作为输入,并根据文档集中的词频进行缩放。上一章有一个这种转换的示例。

停用词移除

常见的词如iswasthe被称为停用词。它们通常不会增加分析的价值,并且在数据准备阶段应该被删除。Spark 提供了StopWordsRemover转换器,它可以做到这一点。它接受一系列标记作为字符串输入的序列,比如分词器的输出,并移除所有的停用词。Spark 默认有一个停用词列表,你可以通过提供自己的停用词列表来覆盖它。你可以选择打开默认关闭的caseSensitive匹配。

示例 5:停用词移除

Scala:

scala> import org.apache.spark.ml.feature.StopWordsRemover
import org.apache.spark.ml.feature.StopWordsRemover
scala> import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.DataFrame
scala> import org.apache.spark.ml.linalg.Vector
import org.apache.spark.ml.linalg.Vector
scala> val rawdataDF = spark.createDataFrame(Seq(
        (0, Array("I", "ate", "the", "cake")),
        (1, Array("John ", "had", "a", " tennis", "racquet")))).
        toDF("id","raw_text")
rawdataDF: org.apache.spark.sql.DataFrame = [id: int, raw_text: array<string>]
scala> val remover = new StopWordsRemover().setInputCol("raw_text").
                setOutputCol("processed_text")
remover: org.apache.spark.ml.feature.StopWordsRemover = stopWords_55edbac88edb
scala> remover.transform(rawdataDF).show(truncate=false)
+---+---------------------------------+-------------------------+
|id |raw_text                         |processed_text           |
+---+---------------------------------+-------------------------+
|0  |[I, ate, the, cake]              |[ate, cake]              |
|1  |[John , had, a,  tennis, racquet]|[John ,  tennis, racquet]|
+---+---------------------------------+-------------------------+

Python:

>>> from pyspark.ml.feature import StopWordsRemover
>>> RawData = sqlContext.createDataFrame([
    (0, ["I", "ate", "the", "cake"]),
    (1, ["John ", "had", "a", " tennis", "racquet"])
    ], ["id", "raw_text"])
>>> 
>>> remover = StopWordsRemover(inputCol="raw_text",
        outputCol="processed_text")
>>> remover.transform(RawData).show(truncate=False)
+---+---------------------------------+-------------------------+
|id |raw_text                         |processed_text           |
+---+---------------------------------+-------------------------+
|0  |[I, ate, the, cake]              |[ate, cake]              |
|1  |[John , had, a,  tennis, racquet]|[John ,  tennis, racquet]|
+---+---------------------------------+-------------------------+

假设我们有以下带有idraw_text列的 DataFrame:

 id | raw_text 
----|---------- 
 0  | [I, ate, the, cake] 
 1  | [John, had, a, tennis, racquet] 

在对前面的示例应用StopWordsRemover,将raw_text作为输入列,processed_text作为输出列后,我们应该得到以下输出:


 id | raw_text                       | processed_text 
----|--------------------------------|-------------------- 
 0  | [I, ate, the, cake]            |  [ate, cake] 
 1  |[John, had, a, tennis, racquet] |[John, tennis, racquet] 

归一化/缩放

归一化是数据准备中常见的预处理步骤。大多数机器学习算法在所有特征处于相同尺度时效果更好。例如,如果有两个特征,其中一个的值大约是另一个的 100 倍,将它们调整到相同的尺度可以反映出两个变量之间有意义的相对活动。任何非数值的值,比如高、中、低,最好都转换为适当的数值量化作为最佳实践。然而,在这样做时需要小心,因为可能需要领域专业知识。例如,如果你为高、中、低分别分配 3、2 和 1,那么应该检查这三个单位是否相互等距。

特征归一化的常见方法包括缩放均值减法特征标准化,仅举几例。在缩放中,每个数值特征向量被重新缩放,使其值范围在-1+101或类似的范围内。在均值减法中,你计算数值特征向量的均值,并从每个值中减去该均值。我们对相对于均值的相对偏差感兴趣,而绝对值可能并不重要。特征标准化是指将数据设置为零均值和单位(1)方差。

Spark 提供了Normalizer特征转换器,将每个向量归一化为单位范数;StandardScaler将单位范数和零均值;MinMaxScaler将每个特征重新缩放到特定范围的值。默认情况下,最小值和最大值为 0 和 1,但您可以根据数据要求自行设置值参数。

Word2Vec

Word2Vec 是一种 PCA(您很快会了解更多)的类型,它接受一系列单词并生成一个映射(字符串,向量)。字符串是单词,向量是一个独特的固定大小的向量。生成的单词向量表示在许多机器学习和自然语言处理应用中非常有用,比如命名实体识别和标记。让我们看一个例子。

示例 6:Word2Vec

Scala

scala> import org.apache.spark.ml.feature.Word2Vec
import org.apache.spark.ml.feature.Word2Vec

//Step1: Load text file and split to words scala> val path = "<Your path>/RobertFrost.txt"
path: String = <Your path>/RobertFrost.txt
scala> val raw_text = spark.read.text(path).select(
        split('value, " ") as "words")
raw_text: org.apache.spark.sql.DataFrame = [words: array<string>]

//Step2: Prepare features vector of size 4 scala> val resultDF = new Word2Vec().setInputCol("words").
        setOutputCol("features").setVectorSize(4).
        setMinCount(2).fit(raw_text).transform(raw_text)
resultDF: org.apache.spark.sql.DataFrame = [words: array<string>, features: vector]

//Examine results scala> resultDF.show(5)
+--------------------+--------------------+
|               words|            features|
+--------------------+--------------------+
|[Whose, woods, th...|[-0.0209098898340...|
|[His, house, is, ...|[-0.0013444167044...|
|[He, will, not, s...|[-0.0058525378408...|
|[To, watch, his, ...|[-0.0189630933296...|
|[My, little, hors...|[-0.0084691265597...|
+--------------------+--------------------+

Python:

>>> from pyspark.ml.feature import Word2Vec
>>> from pyspark.sql.functions import explode, split
>>>

//Step1: Load text file and split to words >>> path = "<Your path>/RobertFrost.txt"
>>> raw_text = spark.read.text(path).select(
        split("value"," ")).toDF("words")

//Step2: Prepare features vector of size 4 >>> resultDF = Word2Vec(inputCol="words",outputCol="features",
                 vectorSize=4, minCount=2).fit(
                 raw_text).transform(raw_text)

//Examine results scala> resultDF.show(5)
+--------------------+--------------------+
|               words|            features|
+--------------------+--------------------+
|[Whose, woods, th...|[-0.0209098898340...|
|[His, house, is, ...|[-0.0013444167044...|
|[He, will, not, s...|[-0.0058525378408...|
|[To, watch, his, ...|[-0.0189630933296...|
|[My, little, hors...|[-0.0084691265597...|
+--------------------+--------------------+

n-gram 建模

n-gram 是给定文本或语音序列中n个项目的连续序列。大小为1的 n-gram 称为unigram,大小为2的称为bigram,大小为3的称为trigram。或者,它们可以根据n的值来命名,例如 four-gram,five-gram 等。让我们看一个例子来理解这个模型可能的结果:


 input |1-gram sequence  | 2-gram sequence | 3-gram sequence 
-------|-----------------|-----------------|--------------- 
 apple | a,p,p,l,e       |  ap,pp,pl,le    |  app,ppl,ple 

这是一个单词到 n-gram 字母的例子。对于句子(或标记化的单词)到 n-gram 单词也是一样的。例如,句子Kids love to eat chocolates的 2-gram 等效于:

'Kids love', 'love to', 'to eat', 'eat chocolates'.

n-gram 建模在文本挖掘和自然语言处理中有各种应用。其中一个例子是预测每个单词在先前上下文中出现的概率(条件概率)。

在 Spark 中,NGram是一个特征转换器,它将字符串的输入数组(例如,分词器的输出)转换为 n-gram 的数组。默认情况下,输入数组中的空值将被忽略。它返回一个 n-gram 的数组,其中每个 n-gram 由一个用空格分隔的单词字符串表示。

示例 7:NGram

Scala

scala> import org.apache.spark.ml.feature.NGram
import org.apache.spark.ml.feature.NGram
scala> val wordDF = spark.createDataFrame(Seq(
        (0, Array("Hi", "I", "am", "a", "Scientist")),
        (1, Array("I", "am", "just", "learning", "Spark")),
        (2, Array("Coding", "in", "Scala", "is", "easy"))
        )).toDF("label", "words")

//Create an ngram model with 3 words length (default is 2) scala> val ngramModel = new NGram().setInputCol(
                "words").setOutputCol("ngrams").setN(3)
ngramModel: org.apache.spark.ml.feature.NGram = ngram_dc50209cf693

//Apply on input data frame scala> ngramModel.transform(wordDF).select("ngrams").show(false)
+--------------------------------------------------+
|ngrams                                            |
+--------------------------------------------------+
|[Hi I am, I am a, am a Scientist]                 |
|[I am just, am just learning, just learning Spark]|
|[Coding in Scala, in Scala is, Scala is easy]     |
+--------------------------------------------------+

//Apply the model on another dataframe, Word2Vec raw_text scala>ngramModel.transform(raw_text).select("ngrams").take(1).foreach(println)
[WrappedArray(Whose woods these, woods these are, these are I, are I think, I think I, think I know.)]

Python:

>>> from pyspark.ml.feature import NGram
>>> wordDF = spark.createDataFrame([
         [0, ["Hi", "I", "am", "a", "Scientist"]],
         [1, ["I", "am", "just", "learning", "Spark"]],
         [2, ["Coding", "in", "Scala", "is", "easy"]]
         ]).toDF("label", "words")

//Create an ngram model with 3 words length (default is 2) >>> ngramModel = NGram(inputCol="words", outputCol= "ngrams",n=3)
>>> 

//Apply on input data frame >>> ngramModel.transform(wordDF).select("ngrams").show(4,False)
+--------------------------------------------------+
|ngrams                                            |
+--------------------------------------------------+
|[Hi I am, I am a, am a Scientist]                 |
|[I am just, am just learning, just learning Spark]|
|[Coding in Scala, in Scala is, Scala is easy]     |
+--------------------------------------------------+

//Apply the model on another dataframe from Word2Vec example >>> ngramModel.transform(resultDF).select("ngrams").take(1)
[Row(ngrams=[u'Whose woods these', u'woods these are', u'these are I', u'are I think', u'I think I', u'think I know.'])]

文本分类

文本分类是指将主题、主题类别、流派或类似内容分配给文本块。例如,垃圾邮件过滤器将垃圾邮件或非垃圾邮件分配给电子邮件。

Apache Spark 通过 MLlib 和 ML 包支持各种分类器。SVM 分类器和朴素贝叶斯分类器是流行的分类器,前者已经在前一章中介绍过。现在让我们来看看后者。

朴素贝叶斯分类器

朴素贝叶斯NB)分类器是一种多类概率分类器,是最好的分类算法之一。它假设每对特征之间有很强的独立性。它计算每个特征和给定标签的条件概率分布,然后应用贝叶斯定理来计算给定观察结果的标签的条件概率。在文档分类方面,观察结果是要分类到某个类别的文档。尽管它对数据有很强的假设,但它非常受欢迎。它可以处理少量的训练数据-无论是真实的还是离散的。它非常高效,因为它只需一次通过训练数据;一个约束是特征向量必须是非负的。默认情况下,ML 包支持多项式 NB。但是,如果需要伯努利 NB,可以将参数modelType设置为Bernoulli

拉普拉斯平滑技术可以通过指定平滑参数来应用,并且在您想要为罕见的单词或新单词分配一个小的非零概率以使后验概率不会突然降为零的情况下非常有用。

Spark 还提供了一些其他超参数,如thresholds,以获得细粒度控制。以下是一个将 Twitter 文本分类的示例。此示例包含一些手工编码的规则,将类别分配给训练数据。如果文本包含相应的单词,则分配特定的类别。例如,如果文本包含"survey"或"poll",则类别为"survey"。该模型是基于此训练数据进行训练的,并且在不同时间收集的不同文本样本上进行评估。

示例 8:朴素贝叶斯

Scala:

// Step 1: Define a udf to assign a category // One or more similar words are treated as one category (eg survey, poll)
// If input list contains any of the words in a category list, it is assigned to that category
// "General" is assigned if none of the categories matched
scala> import scala.collection.mutable.WrappedArray
import scala.collection.mutable.WrappedArray
scala> val findCategory = udf ((words: WrappedArray[String]) =>
    { var idx = 0; var category : String = ""
    val categories : List[Array[String]] =  List(
     Array("Python"), Array("Hadoop","hadoop"),
     Array("survey","poll"),
      Array("event","training", "Meetup", "summit",
          "talk", "talks", "Setting","sessions", "workshop"),
     Array("resource","Guide","newsletter", "Blog"))
    while(idx < categories.length && category.isEmpty ) {
        if (!words.intersect(categories(idx)).isEmpty) {
         category = categories(idx)(0) }  //First word in the category list
     idx += 1 }
    if (category.isEmpty) {
    category = "General"  }
    category
  })
findCategory: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(ArrayType(StringType,true))))

//UDF to convert category to a numerical label scala> val idxCategory = udf ((category: String) =>
        {val catgMap = Map({"General"->1},{"event"->2},{"Hadoop"->3},
                             {"Python"->4},{"resource"->5})
         catgMap(category)})
idxCategory: org.apache.spark.sql.expressions.UserDefinedFunction =
UserDefinedFunction(<function1>,IntegerType,Some(List(StringType)))
scala> val labels = Array("General","event","Hadoop","Python","resource")
 //Step 2: Prepare train data 
//Step 2a: Extract "text" data and split to words scala> val path = "<Your path>/tweets_train.txt"
path: String = <Your path>../work/tweets_train.txt
scala> val pattern = ""text":"
pattern: String = "text":
scala> val raw_text = spark.read.text(path).filter($"value".contains(pattern)).
               select(split('value, " ") as "words")
raw_text: org.apache.spark.sql.DataFrame = [words: array<string>]
scala>

//Step 2b: Assign a category to each line scala> val train_cat_df = raw_text.withColumn("category",

findCategory(raw_text("words"))).withColumn("label",idxCategory($"category"))
train_cat_df: org.apache.spark.sql.DataFrame = [words: array<string>, category:
string ... 1 more field]

//Step 2c: Examine categories scala> train_cat_df.groupBy($"category").agg(count("category")).show()
+--------+---------------+                                                     
|category|count(category)|
+--------+---------------+
| General|            146|
|resource|              1|
|  Python|              2|
|   event|             10|
|  Hadoop|              6|
+--------+---------------+ 

//Step 3: Build pipeline scala> import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.feature.{StopWordsRemover, CountVectorizer,
                  IndexToString}
import org.apache.spark.ml.feature.{StopWordsRemover, CountVectorizer,
StringIndexer, IndexToString}
scala> import org.apache.spark.ml.classification.NaiveBayes
import org.apache.spark.ml.classification.NaiveBayes
scala>

//Step 3a: Define pipeline stages 
//Stop words should be removed first scala> val stopw = new StopWordsRemover().setInputCol("words").
                setOutputCol("processed_words")
stopw: org.apache.spark.ml.feature.StopWordsRemover = stopWords_2fb707daa92e
//Terms to term frequency converter scala> val cv = new CountVectorizer().setInputCol("processed_words").
             setOutputCol("features")
cv: org.apache.spark.ml.feature.CountVectorizer = cntVec_def4911aa0bf
//Define model scala> val model = new NaiveBayes().
                setFeaturesCol("features").
                setLabelCol("label")
model: org.apache.spark.ml.classification.NaiveBayes = nb_f2b6c423f12c
//Numerical prediction label to category converter scala> val lc = new IndexToString().setInputCol("prediction").
              setOutputCol("predictedCategory").
              setLabels(labels)
lc: org.apache.spark.ml.feature.IndexToString = idxToStr_3d71be25382c
 //Step 3b: Build pipeline with desired stages scala> val p = new Pipeline().setStages(Array(stopw,cv,model,lc))
p: org.apache.spark.ml.Pipeline = pipeline_956942e70b3f
 //Step 4: Process train data and get predictions 
//Step 4a: Execute pipeline with train data scala> val resultsDF = p.fit(train_cat_df).transform(train_cat_df)
resultsDF: org.apache.spark.sql.DataFrame = [words: array<string>, category:
string ... 7 more fields]

//Step 4b: Examine results scala> resultsDF.select("category","predictedCategory").show(3)
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
|   event|            event|
|   event|            event|
| General|          General|
+--------+-----------------+
 //Step 4c: Look for prediction mismatches scala> resultsDF.filter("category != predictedCategory").select(
         "category","predictedCategory").show(3)
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
| General|            event|
| General|           Hadoop|
|resource|           Hadoop|
+--------+-----------------+
 //Step 5: Evaluate model using test data 
//Step5a: Prepare test data scala> val path = "<Your path> /tweets.json"
path: String = <Your path>/tweets.json
scala> val raw_test_df =
spark.read.text(path).filter($"value".contains(pattern)).
               select(split('value, " ") as "words"

raw_test_df: org.apache.spark.sql.DataFrame = [words: array<string>]
scala> val test_cat_df = raw_test_df.withColumn("category",

findCategory(raw_test_df("words")))withColumn("label",idxCategory($"category"))
test_cat_df: org.apache.spark.sql.DataFrame = [words: array<string>, category:
string ... 1 more field]
scala> test_cat_df.groupBy($"category").agg(count("category")).show()
+--------+---------------+                                                     
|category|count(category)|
+--------+---------------+
| General|              6|
|   event|             11|
+--------+---------------+
 //Step 5b: Run predictions on test data scala> val testResultsDF = p.fit(test_cat_df).transform(test_cat_df)
testResultsDF: org.apache.spark.sql.DataFrame = [words: array<string>,
category: string ... 7 more fields]
//Step 5c:: Examine results
scala> testResultsDF.select("category","predictedCategory").show(3)
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
| General|            event|
|   event|          General|
|   event|          General|
+--------+-----------------+

//Step 5d: Look for prediction mismatches scala> testResultsDF.filter("category != predictedCategory").select(
         "category","predictedCategory").show()
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
|   event|          General|
|   event|          General|
+--------+-----------------+

Python:

// Step 1: Initialization 
//Step1a: Define a udfs to assign a category // One or more similar words are treated as one category (eg survey, poll)
// If input list contains any of the words in a category list, it is assigned to that category
// "General" is assigned if none of the categories matched
>>> def findCategory(words):
        idx = 0; category  = ""
        categories = [["Python"], ["Hadoop","hadoop"],
          ["survey","poll"],["event","training", "Meetup", "summit",
          "talk", "talks", "Setting","sessions", "workshop"],
          ["resource","Guide","newsletter", "Blog"]]
        while(not category and idx < len(categories)):
          if len(set(words).intersection(categories[idx])) > 0:
             category = categories[idx][0] #First word in the category list
          else:
             idx+=1
        if not category:   #No match found
          category = "General"
        return category
>>> 
//Step 1b: Define udf to convert string category to a numerical label >>> def idxCategory(category):
       catgDict = {"General" :1, "event" :2, "Hadoop" :2,
             "Python": 4, "resource" : 5}
       return catgDict[category]
>>> 
//Step 1c: Register UDFs >>> from pyspark.sql.functions import udf
>>> from pyspark.sql.types import StringType, IntegerType
>>> findCategoryUDF = udf(findCategory, StringType())
>>> idxCategoryUDF = udf(idxCategory, IntegerType())

//Step 1d: List categories >>> categories =["General","event","Hadoop","Python","resource"]
//Step 2: Prepare train data 
//Step 2a: Extract "text" data and split to words >>> from pyspark.sql.functions import split
>>> path = "../work/tweets_train.txt"
>>> raw_df1 = spark.read.text(path)
>>> raw_df = raw_df1.where("value like '%"text":%'").select(
             split("value", " ")).toDF("words")

//Step 2b: Assign a category to each line >>> train_cat_df = raw_df.withColumn("category",\
        findCategoryUDF("words")).withColumn(
        "label",idxCategoryUDF("category"))

//Step 2c: Examine categories scala> train_cat_df.groupBy("category").count().show()
+--------+---------------+                                                     
|category|count(category)|
+--------+---------------+
| General|            146|
|resource|              1|
|  Python|              2|
|   event|             10|
|  Hadoop|              6|
+--------+---------------+

//Step 3: Build pipeline >>> from pyspark.ml import Pipeline
>>> from pyspark.ml.feature import StopWordsRemover, CountVectorizer,
IndexToString
>>> from pyspark.ml.classification import NaiveBayes
>>>

//Step 3a: Define pipeline stages 
//Stop words should be removed first >>> stopw = StopWordsRemover(inputCol = "words",
                  outputCol = "processed_words")
//Terms to term frequency converter >>> cv = CountVectorizer(inputCol = "processed_words",
             outputCol = "features")
//Define model >>> model = NaiveBayes(featuresCol="features",
                   labelCol = "label")
//Numerical prediction label to category converter >>> lc = IndexToString(inputCol = "prediction",
           outputCol = "predictedCategory",
           labels = categories)
>>> 

//Step 3b: Build pipeline with desired stages >>> p = Pipeline(stages = [stopw,cv,model,lc])
>>> 
 //Step 4: Process train data and get predictions 
//Step 4a: Execute pipeline with train data >>> resultsDF = p.fit(train_cat_df).transform(train_cat_df)

//Step 4b: Examine results >>> resultsDF.select("category","predictedCategory").show(3)
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
|   event|            event|
|   event|            event|
| General|          General|
+--------+-----------------+
 //Step 4c: Look for prediction mismatches >>> resultsDF.filter("category != predictedCategory").select(
         "category","predictedCategory").show(3)
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
|  Python|           Hadoop|
|  Python|           Hadoop|
|  Hadoop|            event|
+--------+-----------------+
 //Step 5: Evaluate model using test data 
//Step5a: Prepare test data >>> path = "<Your path>/tweets.json">>> raw_df1 = spark.read.text(path)
>>> raw_test_df = raw_df1.where("va
ue like '%"text":%'").select(
               split("value", " ")).toDF("words")
>>> test_cat_df = raw_test_df.withColumn("category",
        findCategoryUDF("words")).withColumn(
        "label",idxCategoryUDF("category"))
>>> test_cat_df.groupBy("category").count().show()
+--------+---------------+                                                     
|category|count(category)|
+--------+---------------+
| General|              6|
|   event|             11|
+--------+---------------+
 //Step 5b: Run predictions on test data >>> testResultsDF = p.fit(test_cat_df).transform(test_cat_df)
//Step 5c:: Examine results >>> testResultsDF.select("category","predictedCategory").show(3)
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
| General|          General|
|   event|            event|
|   event|            event|
+--------+-----------------+
//Step 5d: Look for prediction mismatches >>> testResultsDF.filter("category != predictedCategory").select(
         "category","predictedCategory").show()
+--------+-----------------+
|category|predictedCategory|
+--------+-----------------+
|   event|          General|
|   event|          General|
+--------+-----------------+

完成后,可以使用此步骤的输出训练模型,该模型可以对文本块或文件进行分类。

文本聚类

聚类是一种无监督学习技术。直观地,聚类将对象分成不相交的集合。我们不知道数据中存在多少组,或者这些组(簇)之间可能存在什么共性。

文本聚类有几个应用。例如,组织实体可能希望根据某种相似度度量将其内部文档组织成相似的簇。相似性或距离的概念对聚类过程至关重要。常用的度量包括 TF-IDF 和余弦相似度。余弦相似度或余弦距离是两个文档的词频向量的余弦乘积。Spark 提供了各种聚类算法,可以有效地用于文本分析。

K-means

也许 K-means 是所有聚类算法中最直观的。其想法是根据某种相似度度量(如余弦距离或欧几里得距离)将数据点分隔为K个不同的簇。该算法从K个随机单点簇开始,然后将其余数据点分配到最近的簇。然后重新计算簇中心,并且算法再次循环遍历数据点。这个过程迭代地继续,直到没有重新分配或达到预定义的迭代次数为止。

如何确定簇的数量(K)并不明显。确定初始簇中心也不明显。有时业务需求可能决定簇的数量;例如,将所有现有文档分成 10 个不同的部分。但在大多数真实世界的场景中,我们需要通过试错找到K。一种方法是逐渐增加K值并计算簇质量,例如簇方差。在某个K值之后,质量停止显着改善,这可能是您理想的K。还有其他各种技术,如肘部法,阿卡奇信息准则AIC)和贝叶斯信息准则BIC)。

同样,从不同的起始点开始,直到簇的质量令人满意。然后您可能希望使用 Silhouette Score 等技术验证您的结果。但是,这些活动需要大量计算。

Spark 提供了来自 MLlib 和 ml 包的 K-means。您可以指定最大迭代次数或收敛容差来微调算法性能。

降维

想象一个有许多行和列的大矩阵。在许多矩阵应用中,这个大矩阵可以由一些窄矩阵代表,这些窄矩阵具有少量行和列,但仍代表原始矩阵。然后处理这个较小的矩阵可能会产生与原始矩阵相似的结果。这可能是计算效率高的。

降维是关于找到那个小矩阵的。MLLib 支持 RowMatrix 类的两种算法,SVD 和 PCA,用于降维。这两种算法都允许我们指定我们感兴趣的保留维度的数量。让我们先看一个例子,然后深入研究其中的理论。

示例 9:降维

Scala:

scala> import scala.util.Random
import scala.util.Random
scala> import org.apache.spark.mllib.linalg.{Vector, Vectors}
import org.apache.spark.mllib.linalg.{Vector, Vectors}
scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.mllib.linalg.distributed.RowMatrix

//Create a RowMatrix of 6 rows and 5 columns scala> var vlist: Array[Vector] = Array()
vlist: Array[org.apache.spark.mllib.linalg.Vector] = Array()
scala> for (i <- 1 to 6) vlist = vlist :+ Vectors.dense(
       Array.fill(5)(Random.nextInt*1.0))
scala> val rows_RDD = sc.parallelize(vlist)
rows_RDD: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] =
ParallelCollectionRDD[0] at parallelize at <console>:29
scala> val row_matrix = new RowMatrix(rows_RDD)
row_matrix: org.apache.spark.mllib.linalg.distributed.RowMatrix = org.apache.spark.mllib.linalg.distributed.RowMatrix@348a6639
 //SVD example for top 3 singular values scala> val SVD_result = row_matrix.computeSVD(3)
SVD_result:
org.apache.spark.mllib.linalg.SingularValueDecomposition[org.apache.spark.mlli
.linalg.distributed.RowMatrix,org.apache.spark.mllib.linalg.Matrix] =
SingularValueDecomposition(null,
[4.933482776606544E9,3.290744495921952E9,2.971558550447048E9],
-0.678871347405378    0.054158900880961904  -0.23905281217240534
0.2278187940802       -0.6393277579229861   0.078663353163388
0.48824560481341733   0.3139021297613471    -0.7800061948839081
-0.4970903877201546   2.366428606359744E-4  -0.3665502780139027
0.041829015676406664  0.6998515759330556    0.4403374382132576    )

scala> SVD_result.s   //Show the singular values (strengths)
res1: org.apache.spark.mllib.linalg.Vector =
[4.933482776606544E9,3.290744495921952E9,2.971558550447048E9]

//PCA example to compute top 2 principal components scala> val PCA_result = row_matrix.computePrincipalComponents(2)
PCA_result: org.apache.spark.mllib.linalg.Matrix =
-0.663822435334425    0.24038790854106118
0.3119085619707716    -0.30195355896094916
0.47440026368044447   0.8539858509513869
-0.48429601343640094  0.32543904517535094
-0.0495437635382354   -0.12583837216152594

Python:

截至 Spark 2.0.0,Python 中不可用。

奇异值分解

奇异值分解SVD)是线性代数的重要组成部分之一,广泛用于许多实际建模需求。它提供了一种将矩阵分解为更简单、更小的矩阵的便捷方法。这导致了高维矩阵的低维表示。它帮助我们消除矩阵中不太重要的部分,以产生一个近似表示。这种技术在降维和数据压缩中非常有用。

M是一个大小为 m 行 n 列的矩阵。矩阵的秩是线性无关的行数。如果一行至少有一个非零元素,并且不是一个或多个行的线性组合,则该行被认为是独立的。如果我们考虑列而不是行,那么将得到相同的秩 - 就像在线性代数中一样。

如果一行的元素是两行的和,则该行不是独立的。因此,作为 SVD 的结果,我们找到了满足以下方程的三个矩阵UV

M = U∑VT

这三个矩阵具有以下特性:

  • U:这是一个具有 m 行和 r 列的列正交矩阵。正交矩阵意味着每个列都是单位向量,并且任意两列之间的点积为 0。

  • V:这是一个具有n行和r列的列正交矩阵。

  • :这是一个r x r对角矩阵,其主对角线值为非负实数,按降序排列。在对角矩阵中,除了主对角线上的元素外,其他元素都是零。

矩阵中的主对角线值被称为奇异值。它们被认为是连接矩阵的行和列的基本概念组件。它们的大小代表了相应组件的强度。例如,想象一下,前面例子中的矩阵包含了六个读者对五本书的评分。SVD 允许我们将它们分成三个矩阵:包含奇异值,代表了基本主题的强度U将人连接到概念;V将概念连接到书籍。

在一个大矩阵中,我们可以将较低幅度的奇异值替换为零,从而减少剩余两个矩阵中的相应行。请注意,如果我们在右侧重新计算矩阵乘积,并将其与左侧的原始矩阵进行比较,它们将几乎相似。我们可以使用这种技术来保留所需的维度。

主成分分析

主成分分析PCA)是一种将 n 维数据点投影到具有最小信息损失的较小(更少维度)子空间的技术。在高维空间中,一组数据点找到了这些元组最佳排列的方向。换句话说,我们需要找到一个旋转,使得第一个坐标具有可能的最大方差,然后依次每个坐标具有可能的最大方差。这个想法是将元组集合视为矩阵M,并找到 MMT 的特征向量。

如果A是一个方阵,e是一个列矩阵,行数与A相同,λ是一个常数,使得Me = λe,那么e被称为M的特征向量,λ被称为M的特征值。在 n 维平面上,特征向量是方向,特征值是沿着该方向的方差的度量。我们可以丢弃特征值较低的维度,从而找到一个更小的子空间,而不会丢失信息。

总结

在本章中,我们研究了非结构化数据的来源以及分析非结构化数据背后的动机。我们解释了预处理非结构化数据所需的各种技术,以及 Spark 如何提供大部分这些工具。我们还介绍了 Spark 支持的一些可用于文本分析的算法。

在下一章中,我们将介绍不同类型的可视化技术,这些技术在数据分析生命周期的不同阶段都具有洞察力。

参考资料:

以下是参考资料:

计数向量化器:

n-gram 建模:

第九章:可视化大数据

适当的数据可视化在过去解决了许多业务问题,而没有涉及太多统计学或机器学习。即使在今天,随着技术的不断进步、应用统计学和机器学习,适当的可视化仍然是业务用户消化信息或某些分析的最终交付物。以正确的格式传达正确的信息是数据科学家渴望的,有效的可视化价值连城。此外,以一种易于业务消化的方式表示生成的模型和见解也非常重要。尽管如此,通过可视化探索大数据是非常繁琐和具有挑战性的。由于 Spark 是为大数据处理而设计的,它也支持大数据可视化。已经在 Spark 上为此目的构建了许多工具和技术。

前几章概述了如何对结构化和非结构化数据进行建模,并从中生成见解。在本章中,我们将从两个广泛的视角来看数据可视化——一个是从数据科学家的视角,可视化是探索和有效理解数据的基本需求,另一个是从业务用户的视角,其中可视化是业务的最终交付物,必须易于理解。我们将探索各种数据可视化工具,如IPythonNotebookZeppelin,可以在 Apache Spark 上使用。

作为本章的先决条件,对 SQL 和 Python、Scala 或其他类似框架的编程有基本的了解是很有帮助的。本章涵盖的主题如下:

  • 为什么要可视化数据?

  • 数据工程师的视角

  • 数据科学家的视角

  • 业务用户的视角

  • 数据可视化工具

  • IPython 笔记本

  • Apache Zeppelin

  • 第三方工具

  • 数据可视化技术

  • 总结和可视化

  • 子集和可视化

  • 抽样和可视化

  • 建模和可视化

为什么要可视化数据?

数据可视化涉及以视觉形式表示数据,以便使人们能够理解其中的模式和趋势。地理地图,十七世纪的条形图和折线图,是早期数据可视化的一些例子。Excel 可能是我们大多数人已经使用过的熟悉的数据可视化工具。所有数据分析工具都配备了复杂的交互式数据可视化仪表板。然而,大数据、流式数据和实时分析的最近激增一直在推动这些工具的边界,它们似乎已经到了极限。其想法是使可视化看起来简单、准确和相关,同时隐藏所有复杂性。根据业务需求,任何可视化解决方案理想上应具有以下特点:

  • 互动性

  • 可重现性

  • 对细节的控制

除此之外,如果解决方案允许用户在可视化或报告上进行协作并相互分享,那么这将构成一个端到端的可视化解决方案。

特别是大数据可视化本身就存在着自己的挑战,因为我们可能会得到比屏幕上的像素更多的数据。处理大量数据通常需要内存和 CPU 密集型处理,并可能具有较长的延迟。将实时或流式数据添加到混合中,问题变得更加具有挑战性。Apache Spark 是从头开始设计的,专门用于通过并行化 CPU 和内存使用来解决这种延迟。在探索可视化和处理大数据的工具和技术之前,让我们首先了解数据工程师、数据科学家和业务用户的可视化需求。

数据工程师的视角

数据工程师在几乎每一个数据驱动的需求中扮演着至关重要的角色:从不同数据源获取数据,整合它们,清洗和预处理它们,分析它们,然后通过可视化和仪表板进行最终报告。他们的活动可以广泛地陈述如下:

  • 可视化来自不同来源的数据,以便将其集成和 consolida te 成一个单一的数据矩阵

  • 可视化并发现数据中的各种异常,如缺失值、异常值等(这可能是在抓取、获取、ETL 等过程中),并将其修复

  • 就数据集的属性和特征向数据科学家提供建议

  • 探索可视化数据的各种可能方式,并最终确定根据业务需求更具信息量和直观性的方式

请注意,数据工程师不仅在获取和准备数据方面起着关键作用,还会根据商业用户的需求选择最合适的可视化输出。他们通常也与业务密切合作,以对业务需求和手头的具体问题有非常清晰的理解。

数据科学家的视角

数据科学家对可视化数据的需求与数据工程师不同。请注意,在一些企业中,有一些专业人员既扮演数据工程师又扮演数据科学家的双重角色。

数据科学家需要可视化数据,以便在进行统计分析时做出正确的决策,并确保分析项目的正确执行。他们希望以各种可能的方式切分数据,以找到隐藏的见解。让我们看一些数据科学家可能需要可视化数据的示例要求:

  • 查看各个变量的数据分布

  • 可视化数据中的异常值

  • 可视化数据集中所有变量的缺失数据百分比

  • 绘制相关矩阵以找到相关的变量

  • 绘制回归后残差的行为

  • 在数据清洗或转换活动之后,重新绘制变量并观察其行为

请注意,刚才提到的一些事情与数据工程师的情况非常相似。然而,数据科学家可能在这些分析背后有更科学/统计的意图。例如,数据科学家可能从不同的角度看待异常值并进行统计处理,而数据工程师可能会考虑触发这种异常的各种选项。

商业用户的视角

一个商业用户的视角与数据工程师或数据科学家完全不同。商业用户通常是信息的消费者!他们希望从数据中提取更多信息,为此,正确的可视化起着关键作用。此外,大多数商业问题如今更加复杂和因果关系。老式报告已经不再足够。让我们看一些商业用户希望从报告、可视化和仪表板中提取的示例查询:

  • 在某个地区,高价值客户是谁?

  • 这些客户的共同特征是什么?

  • 预测新客户是否会是高价值客户

  • 在哪种媒体上做广告会带来最大的投资回报?

  • 如果我不在报纸上做广告会怎样?

  • 影响客户购买行为的因素是什么?

数据可视化工具

在许多不同的可视化选项中,选择合适的可视化取决于具体的需求。同样,选择可视化工具取决于目标受众和业务需求。

数据科学家或数据工程师更倾向于一个更具交互性的控制台进行快速而肮脏的分析。他们使用的可视化通常不是为业务用户而设计的。他们希望以各种可能的方式剖析数据,以获得更有意义的见解。因此,他们通常更喜欢支持这些活动的笔记本类型界面。笔记本是一个交互式的计算环境,他们可以在其中组合代码块和绘制数据进行探索。有一些可用选项,如IPython/JupyterDataBricks等笔记本。

业务用户更倾向于更直观和信息丰富的可视化,他们可以分享给彼此或用来生成报告。他们期望通过可视化收到最终结果。有数以百计的工具,包括一些流行的工具如Tableau,企业使用;但很多时候,开发人员必须根据一些独特的需求定制特定类型,并通过 Web 应用程序公开它们。微软的PowerBIZeppelin等开源解决方案就是其中的几个例子。

IPython 笔记本

在 Spark 的PySpark API 之上的 IPython/Jupyter 笔记本是数据科学家探索和可视化数据的绝佳组合。笔记本内部会启动一个新的 PySpark 内核实例。还有其他可用的内核;例如,Apache Toree内核可以用于支持 Scala。

对于许多数据科学家来说,它是默认选择,因为它能够在一个 JSON 文档文件中集成文本、代码、公式和图形。IPython 笔记本支持matplotlib,这是一个可以生成高质量可视化的 2D 可视化库。生成图表、直方图、散点图、图表等变得简单而容易。它还支持seaborn库,实际上是建立在 matplotlib 之上的,但易于使用,因为它提供了更高级的抽象并隐藏了底层复杂性。

Apache Zeppelin

Apache Zeppelin 是建立在 JVM 之上的,并与 Apache Spark 很好地集成在一起。它是一个基于浏览器或前端的开源工具,具有自己的笔记本。它支持 Scala、Python、R、SQL 和其他图形模块,不仅为业务用户提供可视化解决方案,也为数据科学家提供支持。在下面关于可视化技术的部分,我们将看看 Zeppelin 如何支持 Apache Spark 代码生成有趣的可视化。您需要下载 Zeppelin(zeppelin.apache.org/)来尝试这些示例。

第三方工具

有许多产品支持 Apache Spark 作为底层数据处理引擎,并且构建以适应组织的大数据生态系统。在利用 Spark 的处理能力的同时,它们提供了支持各种交互式可视化的可视化界面,它们还支持协作。Tableau 就是一个利用 Spark 的工具的例子。

数据可视化技术

数据可视化是数据分析生命周期的每个阶段的核心。它对于探索性分析和沟通结果尤为重要。在任何情况下,目标都是将数据转换为人类消费的高效格式。将转换委托给客户端库的方法无法扩展到大型数据集。转换必须在服务器端进行,仅将相关数据发送到客户端进行渲染。大多数常见的转换在 Apache Spark 中都是开箱即用的。让我们更仔细地看看这些转换。

总结和可视化

总结和可视化是许多商业智能BI)工具使用的技术。由于总结将是一个简洁的数据集,无论底层数据集的大小如何,图表看起来都足够简单且易于呈现。有各种各样的数据总结方法,例如聚合、透视等。如果呈现工具支持交互性并具有钻取功能,用户可以从完整数据中探索感兴趣的子集。我们将展示如何通过 Zeppelin 笔记本快速和交互式地使用 Spark 进行总结。

下面的图片显示了带有源代码和分组条形图的 Zeppelin 笔记本。数据集包含 24 个观测值,其中包含两种产品P1P2的 12 个月销售信息。第一个单元格包含读取文本文件并将数据注册为临时表的代码。这个单元格使用默认的 Spark 解释器使用 Scala。第二个单元格使用了 SQL 解释器,该解释器支持开箱即用的可视化选项。您可以通过点击右侧图标切换图表类型。请注意,无论是 Scala 还是 Python 还是 R 解释器,可视化都是相似的。

总结示例如下:

  1. 读取数据并注册为 SQL 视图的源代码:

Scala(默认)

总结和可视化

PySpark

总结和可视化

R

总结和可视化

所有三个都是读取数据文件并将其注册为临时 SQL 视图。请注意,在前面的三个脚本中存在一些细微差异。例如,我们需要删除 R 的标题行并设置列名。下一步是生成可视化,它可以从%sql解释器中工作。下面的第一张图片显示了生成每种产品季度销售额的脚本。它还显示了开箱即用的图表类型,然后是设置及其选择。在进行选择后,您可以折叠设置。您甚至可以利用 Zeppelin 内置的动态表单,比如在运行时接受一个产品。第二张图片显示了实际输出。

  1. 用于生成两种产品季度销售额的脚本:总结和可视化

  2. 生成的输出:总结和可视化

我们在前面的例子中已经看到了 Zeppelin 内置的可视化。但是我们也可以使用其他绘图库。我们的下一个例子利用了 PySpark 解释器和 Zeppelin 中的 matplotlib 来绘制直方图。这个例子的代码使用 RDD 的直方图函数计算箱子间隔和箱子计数,并将这些总结数据带到驱动节点。在绘制箱子时,频率被作为权重提供,以便给出与普通直方图相同的视觉理解,但数据传输非常低。

直方图示例如下:

总结和可视化

这是生成的输出(可能会显示为单独的窗口):

总结和可视化

在前面的直方图准备示例中,请注意可以使用内置的动态表单支持来参数化桶计数。

子集和可视化

有时,我们可能有一个大型数据集,但我们可能只对其中的一个子集感兴趣。分而治之是一种方法,我们可以一次探索一小部分数据。Spark 允许使用类似 SQL 的过滤器和聚合在行列数据集以及图形数据上对数据进行子集化。让我们先进行 SQL 子集化,然后进行一个 GraphX 示例。

以下示例获取了 Zeppelin 提供的银行数据,并提取了与仅经理相关的几列数据。它使用了google 可视化库来绘制气泡图。数据是使用 PySpark 读取的。数据子集和可视化是使用 R 进行的。请注意,我们可以选择任何解释器来执行这些任务,这里的选择只是任意的。

使用 SQL 进行数据子集示例如下:

  1. 读取数据并注册 SQL 视图:子集和可视化

  2. 子集经理的数据并显示气泡图:子集和可视化

下一个示例演示了使用Stanford Network Analysis Project (SNAP)提供的数据进行的一些 GraphX 处理。脚本提取了覆盖给定节点集的子图。在这里,每个节点代表一个 Facebook ID,边代表两个节点(或人)之间的连接。此外,脚本识别了给定节点(id: 144)的直接连接。这些是级别 1 节点。然后它识别了这些级别 1 节点的直接连接,这些连接形成了给定节点的级别 2 节点。即使第二级联系可能连接到多个第一级联系,但它只显示一次,从而形成一个没有交叉边的连接树。由于连接树可能有太多的节点,脚本限制了级别 1 和级别 2 的连接,因此在给定根节点下只显示 12 个节点(一个根+三个级别 1 节点+每个级别 2 节点三个)。

Scala

//Subset and visualize 
//GraphX subset example 
//Datasource: http://snap.stanford.edu/data/egonets-Facebook.html  
import org.apache.spark.graphx._ 
import org.apache.spark.graphx.util.GraphGenerators 
//Load edge file and create base graph 
val base_dir = "../data/facebook" 
val graph = GraphLoader.edgeListFile(sc,base_dir + "/0.edges") 

//Explore subgraph of a given set of nodes 
val circle = "155  99  327  140  116  147  144  150  270".split("\t").map( 
       x=> x.toInt) 
val subgraph = graph.subgraph(vpred = (id,name) 
     => circle.contains(id)) 
println("Edges: " + subgraph.edges.count +  
       " Vertices: " + subgraph.vertices.count) 

//Create a two level contact tree for a given node  
//Step1: Get all edges for a given source id 
val subgraph_level1 = graph.subgraph(epred= (ed) =>  
    ed.srcId == 144) 

//Step2: Extract Level 1 contacts 
import scala.collection.mutable.ArrayBuffer 
val lvl1_nodes : ArrayBuffer[Long] = ArrayBuffer() 
subgraph_level1.edges.collect().foreach(x=> lvl1_nodes+= x.dstId) 

//Step3: Extract Level 2 contacts, 3 each for 3 lvl1_nodes 
import scala.collection.mutable.Map 
val linkMap:Map[Long, ArrayBuffer[Long]] = Map() //parent,[Child] 
val lvl2_nodes : ArrayBuffer[Long] = ArrayBuffer() //1D Array 
var n : ArrayBuffer[Long] = ArrayBuffer() 
for (i <- lvl1_nodes.take(3)) {    //Limit to 3 
    n = ArrayBuffer() 
    graph.subgraph(epred = (ed) => ed.srcId == i && 
        !(lvl2_nodes contains ed.dstId)).edges.collect(). 
             foreach(x=> n+=x.dstId) 
    lvl2_nodes++=n.take(3)    //Append to 1D array. Limit to 3 
  linkMap(i) = n.take(3)  //Assign child nodes to its parent 
 } 

 //Print output and examine the nodes 
 println("Level1 nodes :" + lvl1_nodes) 
 println("Level2 nodes :" + lvl2_nodes) 
 println("Link map :" + linkMap) 

 //Copy headNode to access from another cell 
 z.put("headNode",144) 
 //Make a DataFrame out of lvl2_nodes and register as a view 
 val nodeDF = sc.parallelize(linkMap.toSeq).toDF("parentNode","childNodes") 
 nodeDF.createOrReplaceTempView("node_tbl") 

注意

请注意z.putz.get的使用。这是在 Zeppelin 中在单元格/解释器之间交换数据的机制。

现在我们已经创建了一个包含级别 1 联系人及其直接联系人的数据框,我们已经准备好绘制树了。以下脚本使用了图形可视化库 igraph 和 Spark R。

提取节点和边。绘制树:

子集和可视化

前面的脚本从节点表中获取父节点,这些父节点是级别 2 节点的父节点,也是给定头节点的直接连接。创建头节点和级别 1 节点的有序对,并分配给edges1。下一步将级别 2 节点的数组展开,形成每个数组元素一行。因此获得的数据框被转置并粘贴在一起形成边对。由于粘贴将数据转换为字符串,因此它们被重新转换为数字。这些是级别 2 的边。级别 1 和级别 2 的边被连接在一起形成一个边的单个列表。这些被用来形成下面显示的图形。请注意,headNode中的模糊是 144,尽管在下图中看不到:

子集和可视化

给定节点的连接树

抽样和可视化

抽样和可视化长期以来一直被统计学家使用。通过抽样技术,我们可以取得数据集的一部分并对其进行处理。我们将展示 Spark 如何支持不同的抽样技术,如随机抽样分层抽样sampleByKey等。以下示例是使用 Jupyter 笔记本、PySpark 内核和seaborn库创建的。数据文件是 Zeppelin 提供的银行数据集。第一个图显示了每个教育类别的余额。颜色表示婚姻状况。

读取数据并随机抽样 5%:

抽样和可视化

使用stripplot渲染数据:

抽样和可视化

前面的示例展示了可用数据的随机样本,这比完全绘制总体要好得多。但是,如果感兴趣的分类变量(在本例中是education)的级别太多,那么这个图就会变得难以阅读。例如,如果我们想要绘制job的余额而不是education,那么会有太多的条带,使图片看起来凌乱。相反,我们可以只取所需分类级别的所需样本,然后检查数据。请注意,这与子集不同,因为我们无法使用 SQL 的WHERE子句来指定正常子集中的样本比例。我们需要使用sampleByKey来做到这一点,如下所示。以下示例仅采用两种工作和特定的抽样比例:

抽样和可视化

分层抽样

建模和可视化

建模和可视化是可能的,使用 Spark 的MLLibML模块。Spark 的统一编程模型和多样的编程接口使得将这些技术结合到一个单一的环境中以从数据中获得洞察成为可能。我们已经在前几章中涵盖了大部分建模技术。然而,以下是一些示例供您参考:

  • 聚类:K 均值,高斯混合建模

  • 分类和回归:线性模型,决策树,朴素贝叶斯,支持向量机

  • 降维:奇异值分解,主成分分析

  • 协同过滤

  • 统计测试:相关性,假设检验

以下示例来自第七章,用 SparkR 扩展 Spark,它尝试使用朴素贝叶斯模型预测学生的及格或不及格结果。这个想法是利用 Zeppelin 提供的开箱即用的功能,并检查模型的行为。因此,我们加载数据,进行数据准备,构建模型,并运行预测。然后我们将预测注册为 SQL 视图,以便利用内置的可视化:

//Model visualization example using zeppelin visualization  
 Prepare Model and predictions 

建模和可视化

下一步是编写所需的 SQL 查询并定义适当的设置。请注意 SQL 中 UNION 运算符的使用以及匹配列的定义方式。

定义 SQL 以查看模型性能:

建模和可视化

以下图片帮助我们理解模型预测与实际数据的偏差。这样的可视化有助于接受业务用户的输入,因为他们不需要任何数据科学的先验知识来理解:

建模和可视化

可视化模型性能

我们通常使用误差度量来评估统计模型,但是将它们以图形方式可视化而不是看到数字,使它们更直观,因为通常更容易理解图表而不是表中的数字。例如,前面的可视化也可以被数据科学社区之外的人轻松理解。

总结

在本章中,我们探讨了在大数据设置中支持的大多数常用可视化工具和技术。我们通过代码片段解释了一些技术,以更好地理解数据分析生命周期不同阶段的可视化需求。我们还看到了如何通过适当的可视化技术满足业务需求,以解决大数据的挑战。

下一章是到目前为止解释的所有概念的高潮。我们将通过一个示例数据集走完完整的数据分析生命周期。

参考

数据来源引用

银行数据来源(引用)

  • [Moro 等人,2011] S. Moro,R. Laureano 和 P. Cortez。使用数据挖掘进行银行直接营销:CRISP-DM 方法论的应用

  • 在 P. Novais 等人(编),欧洲模拟与建模会议 - ESM'2011 论文集,第 117-121 页,葡萄牙吉马良斯,2011 年 10 月。EUROSIS

  • 可在[pdf] hdl.handle.net/1822/14838找到

  • [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt

Facebook 数据来源(引用)

  • J. McAuley 和 J. Leskovec。学习在自我网络中发现社交圈。NIPS,2012 年。

第十章:将所有内容整合在一起

大数据分析正在改变企业经营的方式,并为许多此前无法想象的机会铺平了道路。几乎每个企业、个人研究人员或调查记者都有大量数据需要处理。我们需要一种简洁的方法,从原始数据开始,根据手头的问题得出有意义的见解。

我们在先前的章节中使用 Apache Spark 涵盖了数据科学的各个方面。我们开始讨论大数据分析需求以及 Apache Spark 的适用性。逐渐地,我们深入研究了 Spark 编程模型、RDD 和 DataFrame 抽象,并学习了 Spark 数据集实现的统一数据访问以及连续应用的流式方面。然后,我们涵盖了使用 Apache Spark 进行整个数据分析生命周期,随后是机器学习。我们学习了 Spark 上的结构化和非结构化数据分析,并探索了数据工程师和科学家以及业务用户的可视化方面。

所有先前讨论的章节都帮助我们理解每个章节中的一个简洁方面。我们现在已经具备了遍历整个数据科学生命周期的能力。在本章中,我们将进行一个端到端的案例研究,并应用到目前为止学到的所有知识。我们不会介绍任何新概念;这将有助于应用到目前为止所获得的知识,并加强我们的理解。但是,我们已经重申了一些概念,而没有过多地详细介绍,以使本章内容自成一体。本章涵盖的主题与数据分析生命周期中的步骤大致相同:

  • 快速回顾

  • 引入案例研究

  • 构建业务问题

  • 数据获取和数据清洗

  • 制定假设

  • 数据探索

  • 数据准备

  • 模型构建

  • 数据可视化

  • 将结果传达给业务用户

  • 总结

快速回顾

我们已经在不同的章节中详细讨论了典型数据科学项目中涉及的各种步骤。让我们快速浏览一下我们已经涵盖的内容,并触及一些重要方面。所涉步骤的高级概述可能如下图所示:

快速回顾

在前面的图示中,我们试图解释了涉及数据科学项目的步骤,大部分是通用于许多数据科学任务的。实际上,在每个阶段都存在许多子步骤,但可能因项目而异。

对于数据科学家来说,很难在开始时找到最佳方法和步骤。通常,数据科学项目没有像软件开发生命周期SDLC)那样定义明确的生命周期。通常情况下,数据科学项目会因为生命周期中的大多数步骤都是迭代的而陷入交付延迟。此外,团队之间可能存在循环依赖,增加了复杂性并导致执行延迟。然而,在处理大数据分析项目时,对数据科学家来说,遵循明确定义的数据科学工作流程是重要且有利的,无论不同的业务案例如何。这不仅有助于组织执行,还有助于我们专注于目标,因为在大多数情况下,数据科学项目本质上是敏捷的。此外,建议您计划对任何给定项目的数据、领域和算法进行一定程度的研究。

在本章中,我们可能无法在单个流程中容纳所有细粒度步骤,但将讨论重要领域,以便提前了解。我们将尝试查看一些在先前章节中未涵盖的不同编码示例。

引入案例研究

在本章中,我们将探讨奥斯卡奖的人口统计学。你可以从 GitHub 仓库下载数据www.crowdflower.com/wp-content/uploads/2016/03/Oscars-demographics-DFE.csv

这个数据集是基于www.crowdflower.com/data-for-everyone提供的数据。它包含人口统计学细节,如种族、出生地和年龄。行数大约为 400,可以在普通家用电脑上轻松处理,因此你可以在 Spark 上执行数据科学项目的概念验证POC)。

从下载文件并检查数据开始。数据可能看起来不错,但当你仔细查看时,你会注意到它并不是“干净”的。例如,出生日期列的格式不一致。有些年份是两位数格式,而有些是四位数格式。出生地没有美国境内地点的国家信息。

同样,你也会注意到数据看起来有偏差,美国有更多“白人”种族的人。但你可能会感觉到趋势在后来的年份有所改变。到目前为止,你还没有使用任何工具或技术,只是快速浏览了一下数据。在数据科学的现实世界中,这种看似琐碎的活动在生命周期的后期可能会非常有帮助。你可以对手头的数据产生一种感觉,并同时对数据进行假设。这将带你进入工作流程的第一步。

业务问题

正如之前所述,任何数据科学项目最重要的方面是所面临的问题。对于我们试图解决什么问题?有清晰的理解至关重要。这对项目的成功至关重要。它还决定了什么是相关数据,什么不是。例如,在当前案例研究中,如果我们想要研究的是人口统计学,那么电影名称和人名就是无关的。有时候,手头上没有具体的问题!那么呢?即使没有具体的问题,业务可能仍然有一些目标,或者数据科学家和领域专家可以共同努力找到要处理的业务领域。为了理解业务、功能、问题陈述或数据,数据科学家从“质疑”开始。这不仅有助于定义工作流程,还有助于获取正确的数据。

例如,如果业务重点是人口统计信息,一个正式的业务问题陈述可以被定义为:

种族和出生国家在奥斯卡奖得主中的影响是什么?

在现实世界中,这一步并不会如此直接。提出正确的问题是数据科学家、战略团队、领域专家和项目所有者的共同责任。如果不符合目的,整个练习就是徒劳的,数据科学家必须咨询所有利益相关者,并尽可能从他们那里获取尽可能多的信息。然而,他们可能最终得到宝贵的见解或“直觉”。所有这些结合起来构成了最初的假设的核心,并帮助数据科学家了解他们应该寻找什么。

在业务没有明确问题需要寻找答案的情况下,处理起来更有趣,但在执行上可能更复杂!

数据获取和数据清洗

数据获取是下一个逻辑步骤。它可能只是从单个电子表格中选择数据,也可能是一个独立的几个月的项目。数据科学家必须收集尽可能多的相关数据。这里的关键词是“相关”。记住,更相关的数据胜过聪明的算法。

我们已经介绍了如何从异构数据源获取数据并 consoli,以形成单个数据矩阵,因此我们将不在这里重复相同的基础知识。相反,我们从单一来源获取数据并提取其子集。

现在是时候查看数据并开始清理了。本章中呈现的脚本往往比以前的示例要长,但仍然不是生产质量的。现实世界的工作需要更多的异常检查和性能调优:

Scala

//Load tab delimited file 
scala> val fp = "<YourPath>/Oscars.txt" 
scala> val init_data = spark.read.options(Map("header"->"true", "sep" -> "\t","inferSchema"->"true")).csv(fp) 
//Select columns of interest and ignore the rest 
>>> val awards = init_data.select("birthplace", "date_of_birth", 
        "race_ethnicity","year_of_award","award").toDF( 
         "birthplace","date_of_birth","race","award_year","award") 
awards: org.apache.spark.sql.DataFrame = [birthplace: string, date_of_birth: string ... 3 more fields] 
//register temporary view of this dataset 
scala> awards.createOrReplaceTempView("awards") 

//Explore data 
>>> awards.select("award").distinct().show(10,false) //False => do not truncate 
+-----------------------+                                                        
|award                  | 
+-----------------------+ 
|Best Supporting Actress| 
|Best Director          | 
|Best Actress           | 
|Best Actor             | 
|Best Supporting Actor  | 
+-----------------------+ 
//Check DOB quality. Note that length varies based on month name 
scala> spark.sql("SELECT distinct(length(date_of_birth)) FROM awards ").show() 
+---------------------+                                                          
|length(date_of_birth)| 
+---------------------+ 
|                   15| 
|                    9| 
|                    4| 
|                    8| 
|                   10| 
|                   11| 
+---------------------+ 

//Look at the value with unexpected length 4 Why cant we show values for each of the length type ?  
scala> spark.sql("SELECT date_of_birth FROM awards WHERE length(date_of_birth) = 4").show() 
+-------------+ 
|date_of_birth| 
+-------------+ 
|         1972| 
+-------------+ 
//This is an invalid date. We can either drop this record or give some meaningful value like 01-01-1972 

Python

    //Load tab delimited file
    >>> init_data = spark.read.csv("<YOURPATH>/Oscars.txt",sep="\t",header=True)
    //Select columns of interest and ignore the rest
    >>> awards = init_data.select("birthplace", "date_of_birth",
            "race_ethnicity","year_of_award","award").toDF(
             "birthplace","date_of_birth","race","award_year","award")
    //register temporary view of this dataset
    >>> awards.createOrReplaceTempView("awards")
    scala>
    //Explore data
    >>> awards.select("award").distinct().show(10,False) //False => do not truncate
    +-----------------------+                                                       
    |award                  |
    +-----------------------+
    |Best Supporting Actress|
    |Best Director          |
    |Best Actress           |
    |Best Actor             |
    |Best Supporting Actor  |
    +-----------------------+
    //Check DOB quality
    >>> spark.sql("SELECT distinct(length(date_of_birth)) FROM awards ").show()
    +---------------------+                                                         
    |length(date_of_birth)|
    +---------------------+
    |                   15|
    |                    9|
    |                    4|
    |                    8|
    |                   10|
    |                   11|
    +---------------------+
    //Look at the value with unexpected length 4\. Note that length varies based on month name
    >>> spark.sql("SELECT date_of_birth FROM awards WHERE length(date_of_birth) = 4").show()
    +-------------+
    |date_of_birth|
    +-------------+
    |         1972|
    +-------------+
    //This is an invalid date. We can either drop this record or give some meaningful value like 01-01-1972

Most of the datasets contain a date field and unless they come from a single, controlled data source, it is highly likely that they will differ in their formats and are almost always a candidate for cleaning.

对于手头的数据集,您可能还注意到date_of_birthbirthplace需要大量清理。以下代码显示了分别清理date_of_birthbirthplace的两个用户定义函数UDFs)。这些 UDFs 一次处理一个数据元素,它们只是普通的 Scala/Python 函数。这些用户定义函数应该被注册,以便它们可以从 SQL 语句中使用。最后一步是创建一个经过清理的数据框,以便参与进一步的分析。

注意清理birthplace的逻辑。这是一个薄弱的逻辑,因为我们假设任何以两个字符结尾的字符串都是美国州。我们必须将它们与有效缩写列表进行比较。同样,假设两位数年份总是来自二十世纪是另一个容易出错的假设。根据使用情况,数据科学家/数据工程师必须决定保留更多行是否重要,或者只应包含质量数据。所有这些决定都应该被清晰地记录以供参考:

Scala:

//UDF to clean date 
//This function takes 2 digit year and makes it 4 digit 
// Any exception returns an empty string 
scala> def fncleanDate(s:String) : String = {  
  var cleanedDate = "" 
  val dateArray: Array[String] = s.split("-") 
  try{    //Adjust year 
     var yr = dateArray(2).toInt 
     if (yr < 100) {yr = yr + 1900 } //make it 4 digit 
     cleanedDate = "%02d-%s-%04d".format(dateArray(0).toInt, 
                dateArray(1),yr) 
     } catch { case e: Exception => None } 
     cleanedDate } 
fncleanDate: (s: String)String 

Python:

    //This function takes 2 digit year and makes it 4 digit
    // Any exception returns an empty string
    >>> def fncleanDate(s):
          cleanedDate = ""
          dateArray = s.split("-")
          try:    //Adjust year
             yr = int(dateArray[2])
             if (yr < 100):
                  yr = yr + 1900 //make it 4 digit
             cleanedDate = "{0}-{1}-{2}".format(int(dateArray[0]),
                      dateArray[1],yr)
          except :
              None
          return cleanedDate

清理日期的 UDF 接受一个连字符日期字符串并拆分它。如果最后一个组件,即年份,是两位数长,则假定它是二十世纪的日期,并添加 1900 以将其转换为四位数格式。

以下 UDF 附加了国家作为美国,如果国家字符串是纽约市或最后一个组件为两个字符长,那么假定它是美国的一个州:

//UDF to clean birthplace 
// Data explorartion showed that  
// A. Country is omitted for USA 
// B. New York City does not have State code as well 
//This function appends country as USA if 
// A. the string contains New York City  (OR) 
// B. if the last component is of length 2 (eg CA, MA) 
scala> def fncleanBirthplace(s: String) : String = { 
        var cleanedBirthplace = "" 
        var strArray : Array[String] =  s.split(" ") 
        if (s == "New York City") 
           strArray = strArray ++ Array ("USA") 
        //Append country if last element length is 2 
        else if (strArray(strArray.length-1).length == 2) 
            strArray = strArray ++ Array("USA") 
        cleanedBirthplace = strArray.mkString(" ") 
        cleanedBirthplace } 

Python:

    >>> def fncleanBirthplace(s):
            cleanedBirthplace = ""
            strArray = s.split(" ")
            if (s == "New York City"):
                strArray += ["USA"]  //Append USA
            //Append country if last element length is 2
            elif (len(strArray[len(strArray)-1]) == 2):
                strArray += ["USA"]
            cleanedBirthplace = " ".join(strArray)
            return cleanedBirthplace

如果要从 SELECT 字符串中访问 UDFs,则应注册 UDFs:

Scala:

//Register UDFs 
scala> spark.udf.register("fncleanDate",fncleanDate(_:String)) 
res10: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(StringType))) 
scala> spark.udf.register("fncleanBirthplace", fncleanBirthplace(_:String)) 
res11: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(StringType))) 

Python:

    >>> from pyspark.sql.types import StringType
    >>> sqlContext.registerFunction("cleanDateUDF",fncleanDate, StringType())
    >>> sqlContext.registerFunction( "cleanBirthplaceUDF",fncleanBirthplace, StringType())

使用 UDFs 清理数据框。执行以下清理操作:

  1. 调用 UDFs fncleanDatefncleanBirthplace来修复出生地和国家。

  2. award_year中减去出生年份以获得获奖时的age

  3. 保留raceaward

Scala:

//Create cleaned data frame 
scala> var cleaned_df = spark.sql ( 
            """SELECT fncleanDate (date_of_birth) dob, 
               fncleanBirthplace(birthplace) birthplace, 
               substring_index(fncleanBirthplace(birthplace),' ',-1)  
                               country, 
               (award_year - substring_index(fncleanDate( date_of_birth),'-',-1)) age, race, award FROM awards""") 
cleaned_df: org.apache.spark.sql.DataFrame = [dob: string, birthplace: string ... 4 more fields] 

Python:

//Create cleaned data frame 
>>> from pyspark.sql.functions import substring_index>>> cleaned_df = spark.sql (            """SELECT cleanDateUDF (date_of_birth) dob,               cleanBirthplaceUDF(birthplace) birthplace,               substring_index(cleanBirthplaceUDF(birthplace),' ',-1) country,               (award_year - substring_index(cleanDateUDF( date_of_birth),               '-',-1)) age, race, award FROM awards""")

最后一行需要一些解释。UDFs 类似于 SQL 函数,并且表达式被别名为有意义的名称。我们添加了一个计算列age,因为我们也想验证年龄的影响。substring_index函数搜索第一个参数的第二个参数。-1表示从右边查找第一次出现。

制定假设

假设是关于结果的最佳猜测。您根据问题、与利益相关者的对话以及查看数据形成初始假设。对于给定的问题,您可能会形成一个或多个假设。这个初始假设作为指导您进行探索性分析的路线图。制定假设对于统计上批准或不批准一个陈述非常重要,而不仅仅是通过查看数据作为数据矩阵或甚至通过视觉来进行。这是因为我们仅仅通过查看数据建立的认知可能是不正确的,有时甚至是具有欺骗性的。

现在你知道你的最终结果可能证明假设是正确的,也可能不是。来到我们为这节课考虑的案例研究,我们得出以下初始假设:

  • 获奖者大多是白人

  • 大多数获奖者来自美国

  • 最佳男演员和女演员往往比最佳导演年轻

现在我们已经明确了我们的假设,我们已经准备好继续进行生命周期的下一步了。

数据探索

现在我们有了一个包含相关数据和初始假设的干净数据框架,是时候真正探索我们拥有的东西了。数据框架抽象提供了group by等函数,供您随时查看。您可以将清理后的数据框架注册为表,并运行经过时间考验的 SQL 语句来执行相同的操作。

现在也是绘制一些图表的时候了。这个可视化阶段是数据可视化章节中提到的探索性分析。这次探索的目标受到您从业务利益相关者和假设中获得的初始信息的极大影响。换句话说,您与利益相关者的讨论帮助您知道要寻找什么。

有一些通用准则适用于几乎所有数据科学任务,但又因不同的使用情况而异。让我们看一些通用的准则:

  • 查找缺失数据并处理它。我们已经讨论了在第五章Spark 上的数据分析中执行此操作的各种方法。

  • 查找数据集中的异常值并处理它们。我们也讨论了这个方面。请注意,有些情况下,我们认为是异常值和正常数据点的界限可能会根据使用情况而改变。

  • 进行单变量分析,其中您单独探索数据集中的每个变量。频率分布或百分位数分布是相当常见的。也许绘制一些图表以获得更好的想法。这也将帮助您在进行数据建模之前准备数据。

  • 验证您的初始假设。

  • 检查数值数据的最小值和最大值。如果任何列中的变化太大,那可能是数据归一化或缩放的候选项。

  • 检查分类数据(如城市名称等字符串值)中的不同值及其频率。如果任何列中有太多不同的值(也称为级别),则可能需要寻找减少级别数量的方法。如果一个级别几乎总是出现,那么该列对模型区分可能结果没有帮助。这样的列很可能被移除。在探索阶段,您只需找出这样的候选列,让数据准备阶段来处理实际操作。

在我们当前的数据集中,我们没有任何缺失数据,也没有任何可能造成挑战的数值数据。但是,当处理无效日期时,可能会出现一些缺失值。因此,以下代码涵盖了剩余的操作项目。此代码假定cleaned_df已经创建:

Scala/Python:

cleaned_df = cleaned_df.na.drop //Drop rows with missing values 
cleaned_df.groupBy("award","country").count().sort("country","award","count").show(4,False) 
+-----------------------+---------+-----+                                        
|award                  |country  |count| 
+-----------------------+---------+-----+ 
|Best Actor             |Australia|1    | 
|Best Actress           |Australia|1    | 
|Best Supporting Actor  |Australia|1    | 
|Best Supporting Actress|Australia|1    | 
+-----------------------+---------+-----+ 
//Re-register data as table 
cleaned_df.createOrReplaceTempView("awards") 
//Find out levels (distinct values) in each categorical variable 
spark.sql("SELECT count(distinct country) country_count, count(distinct race) race_count, count(distinct award) award_count from awards").show() 
+-------------+----------+-----------+                                           
|country_count|race_count|award_count| 
+-------------+----------+-----------+ 
|           34|         6|          5| 
+-------------+----------+-----------+ 

以下可视化与初始假设相对应。请注意,我们发现两个假设是正确的,但第三个假设不正确。这些可视化是使用 zeppelin 创建的:

数据探索

请注意,并非所有假设都可以通过可视化来验证,因为它们有时可能具有欺骗性。因此,需要根据适用情况执行适当的统计检验,如 t 检验、ANOVA、卡方检验、相关性检验等。我们不会在本节详细介绍。有关详细信息,请参阅第五章Spark 上的数据分析

数据准备

数据探索阶段帮助我们确定了在进入建模阶段之前需要修复的所有问题。每个单独的问题都需要仔细思考和审议,以选择最佳的修复方法。以下是一些常见问题和可能的修复方法。最佳修复取决于手头的问题和/或业务背景。

分类变量中的太多级别

这是我们面临的最常见问题之一。解决此问题取决于多个因素:

  • 如果列几乎总是唯一的,例如,它是一个交易 ID 或时间戳,那么它在建模过程中不参与,除非你正在从中派生新特征。您可以安全地删除该列而不会丢失任何信息内容。通常在数据清洗阶段就会删除它。

  • 如果可能用粗粒度级别(例如,州或国家而不是城市)替换级别,这在当前情境下通常是解决此问题的最佳方式。

  • 您可能希望为每个不同级别添加具有 0 或 1 值的虚拟列。例如,如果单个列中有 100 个级别,则添加 100 个列。最多,一个观察(行)中将有一个列具有 1。这称为独热编码,Spark 通过ml.features包提供了这个功能。

  • 另一个选择是保留最频繁的级别。您甚至可以将这些级别中的每一个附加到某个被认为与该级别“更接近”的主导级别中。此外,您可以将其余级别捆绑到一个单一的桶中,例如Others

  • 没有绝对限制级别的硬性规定。这取决于您对每个单独特征所需的粒度以及性能约束。

当前数据集在分类变量country中有太多级别。我们选择保留最频繁的级别,并将其余级别捆绑到Others中:

Scala:

//Country has too many values. Retain top ones and bundle the rest 
//Check out top 6 countries with most awards. 
scala> val top_countries_df = spark.sql("SELECT country, count(*) freq FROM awards GROUP BY country ORDER BY freq DESC LIMIT 6") 
top_countries_df: org.apache.spark.sql.DataFrame = [country: string, freq: bigint] 
scala> top_countries_df.show() 
+-------+----+                                                                   
|country|freq| 
+-------+----+ 
|    USA| 289| 
|England|  57| 
| France|   9| 
| Canada|   8| 
|  Italy|   7| 
|Austria|   7| 
+-------+----+ 
//Prepare top_countries list 
scala> val top_countries = top_countries_df.select("country").collect().map(x => x(0).toString) 
top_countries: Array[String] = Array(USA, England, New York City, France, Canada, Italy) 
//UDF to fix country. Retain top 6 and bundle the rest into "Others" 
scala> import org.apache.spark.sql.functions.udf 
import org.apache.spark.sql.functions.udf 
scala > val setCountry = udf ((s: String) => 
        { if (top_countries.contains(s)) {s} else {"Others"}}) 
setCountry: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(StringType))) 
//Apply udf to overwrite country 
scala> cleaned_df = cleaned_df.withColumn("country", setCountry(cleaned_df("country"))) 
cleaned_df: org.apache.spark.sql.DataFrame = [dob: string, birthplace: string ... 4 more fields] 

Python:

    //Check out top 6 countries with most awards.
    >>> top_countries_df = spark.sql("SELECT country, count(*) freq FROM awards GROUP BY country ORDER BY freq DESC LIMIT 6")
    >>> top_countries_df.show()
    +-------+----+                                                                  
    |country|freq|
    +-------+----+
    |    USA| 289|
    |England|  57|
    | France|   9|
    | Canada|   8|
    |  Italy|   7|
    |Austria|   7|
    +-------+----+
    >>> top_countries = [x[0] for x in top_countries_df.select("country").collect()]
    //UDF to fix country. Retain top 6 and bundle the rest into "Others"
    >>> from pyspark.sql.functions import udf
    >>> from pyspark.sql.types import StringType
    >>> setCountry = udf(lambda s: s if s in top_countries else "Others", StringType())
    //Apply UDF
    >>> cleaned_df = cleaned_df.withColumn("country", setCountry(cleaned_df["country"]))

具有太多变化的数值变量

有时,数值数据值可能相差几个数量级。例如,如果您正在查看个人的年收入,它可能会有很大变化。Z 分数标准化(标准化)和最小-最大缩放是处理这种数据的两种常用选择。Spark 在ml.features包中提供了这两种转换。

我们当前的数据集没有这样的变量。我们唯一的数值变量是年龄,其值均匀为两位数。这是一个问题少了一个问题。

请注意,并非总是需要对此类数据进行标准化。如果您正在比较两个不同规模的变量,或者如果您正在使用聚类算法或 SVM 分类器,或者任何其他真正需要对数据进行标准化的情况,您可以对数据进行标准化。

缺失数据

这是一个重要的关注领域。任何目标本身缺失的观察应该从训练数据中删除。根据要求,剩下的观察可以保留一些填充值或删除。在填充缺失值时,您应该非常小心;否则可能会导致误导性的输出!看起来很容易只需继续并在连续变量的空白单元格中替换平均值,但这可能不是正确的方法。

我们当前的案例研究没有任何缺失数据,因此没有处理的余地。不过,让我们看一个例子。

假设您正在处理一个学生数据集,其中包含从一年级到五年级的数据。如果有一些缺失的Age值,您只需找到整个列的平均值并替换,那么这将成为一个异常值,并可能导致模糊的结果。您可以选择仅找到学生所在班级的平均值,然后填充该值。这至少是一个更好的方法,但可能不是完美的方法。在大多数情况下,您还必须给其他变量赋予权重。如果这样做,您可能会建立一个预测模型来查找缺失的值,这可能是一个很好的方法!

连续数据

数值数据通常是连续的,必须离散化,因为这是一些算法的先决条件。它通常被分成不同的桶或值的范围。然而,可能存在这样的情况,你不仅仅是根据数据的范围均匀地分桶,你可能需要考虑方差或标准差或任何其他适用的原因来正确地分桶。现在,决定桶的数量也取决于数据科学家的判断,但这也需要仔细分析。太少的桶会降低粒度,而太多的桶与拥有太多的分类级别几乎是一样的。在我们的案例研究中,“年龄”就是这样的数据的一个例子,我们需要将其离散化。我们将其分成不同的桶。例如,看看这个管道阶段,它将“年龄”转换为 10 个桶:

Scala:

scala> val splits = Array(Double.NegativeInfinity, 35.0, 45.0, 55.0, 
          Double.PositiveInfinity) 
splits: Array[Double] = Array(-Infinity, 35.0, 45.0, 55.0, Infinity) 
scala> val bucketizer = new Bucketizer().setSplits(splits). 
                 setInputCol("age").setOutputCol("age_buckets") 
bucketizer: org.apache.spark.ml.feature.Bucketizer = bucketizer_a25c5d90ac14 

Python:

    >>> splits = [-float("inf"), 35.0, 45.0, 55.0,
                   float("inf")]
    >>> bucketizer = Bucketizer(splits = splits, inputCol = "age",
                        outputCol = "age_buckets")

分类数据

我们已经讨论了将连续数据离散化并转换为类别或桶的需要。我们还讨论了引入虚拟变量,每个分类变量的每个不同值都有一个。还有一个常见的数据准备做法,即将分类级别转换为数值(离散)数据。这是因为许多机器学习算法使用数值数据、整数和实值数字,或者其他情况可能需要。因此,我们需要将分类数据转换为数值数据。

这种方法可能存在缺点。在本质上无序的数据中引入顺序有时可能是不合逻辑的。例如,将数字 0、1、2、3 分配给颜色“红色”、“绿色”、“蓝色”和“黑色”是没有意义的。这是因为我们不能说红色距离“绿色”一单位,绿色距离“蓝色”也是如此!如果适用,在许多这种情况下引入虚拟变量更有意义。

准备数据

在讨论了常见问题和可能的修复方法之后,让我们看看如何准备我们当前的数据集。我们已经涵盖了与太多级别问题相关的代码修复。以下示例显示了其余部分。它将所有特征转换为单个特征列。它还将一些数据设置为测试模型。这段代码严重依赖于ml.features包,该包旨在支持数据准备阶段。请注意,这段代码只是定义了需要做什么。转换尚未进行。这些将成为随后定义的管道中的阶段。执行被尽可能地推迟,直到实际模型建立。Catalyst 优化器找到了实施管道的最佳路径:

Scala:

//Define pipeline to convert categorical labels to numerical labels 
scala> import org.apache.spark.ml.feature.{StringIndexer, Bucketizer, VectorAssembler} 
import org.apache.spark.ml.feature.{StringIndexer, Bucketizer, VectorAssembler} 
scala> import org.apache.spark.ml.Pipeline 
import org.apache.spark.ml.Pipeline 
//Race 
scala> val raceIdxer = new StringIndexer(). 
           setInputCol("race").setOutputCol("raceIdx") 
raceIdxer: org.apache.spark.ml.feature.StringIndexer = strIdx_80eddaa022e6 
//Award (prediction target) 
scala> val awardIdxer = new StringIndexer(). 
         setInputCol("award").setOutputCol("awardIdx") 
awardIdxer: org.apache.spark.ml.feature.StringIndexer = strIdx_256fe36d1436 
//Country 
scala> val countryIdxer = new StringIndexer(). 
         setInputCol("country").setOutputCol("countryIdx") 
countryIdxer: org.apache.spark.ml.feature.StringIndexer = strIdx_c73a073553a2 

//Convert continuous variable age to buckets 
scala> val splits = Array(Double.NegativeInfinity, 35.0, 45.0, 55.0, 
          Double.PositiveInfinity) 
splits: Array[Double] = Array(-Infinity, 35.0, 45.0, 55.0, Infinity) 

scala> val bucketizer = new Bucketizer().setSplits(splits). 
                 setInputCol("age").setOutputCol("age_buckets") 
bucketizer: org.apache.spark.ml.feature.Bucketizer = bucketizer_a25c5d90ac14 

//Prepare numerical feature vector by clubbing all individual features 
scala> val assembler = new VectorAssembler().setInputCols(Array("raceIdx", 
          "age_buckets","countryIdx")).setOutputCol("features") 
assembler: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_8cf17ee0cd60 

//Define data preparation pipeline 
scala> val dp_pipeline = new Pipeline().setStages( 
          Array(raceIdxer,awardIdxer, countryIdxer, bucketizer, assembler)) 
dp_pipeline: org.apache.spark.ml.Pipeline = pipeline_06717d17140b 
//Transform dataset 
scala> cleaned_df = dp_pipeline.fit(cleaned_df).transform(cleaned_df) 
cleaned_df: org.apache.spark.sql.DataFrame = [dob: string, birthplace: string ... 9 more fields] 
//Split data into train and test datasets 
scala> val Array(trainData, testData) = 
        cleaned_df.randomSplit(Array(0.7, 0.3)) 
trainData: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [dob: string, birthplace: string ... 9 more fields] 
testData: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [dob: string, birthplace: string ... 9 more fields] 

Python:

    //Define pipeline to convert categorical labels to numcerical labels
    >>> from pyspark.ml.feature import StringIndexer, Bucketizer, VectorAssembler
    >>> from pyspark.ml import Pipelin
    //Race
    >>> raceIdxer = StringIndexer(inputCol= "race", outputCol="raceIdx")
    //Award (prediction target)
    >>> awardIdxer = StringIndexer(inputCol = "award", outputCol="awardIdx")
    //Country
    >>> countryIdxer = StringIndexer(inputCol = "country", outputCol = "countryIdx")

    //Convert continuous variable age to buckets
    >>> splits = [-float("inf"), 35.0, 45.0, 55.0,
                   float("inf")]
    >>> bucketizer = Bucketizer(splits = splits, inputCol = "age",
                        outputCol = "age_buckets")
    >>>
    //Prepare numerical feature vector by clubbing all individual features
    >>> assembler = VectorAssembler(inputCols = ["raceIdx", 
              "age_buckets","countryIdx"], outputCol = "features")

    //Define data preparation pipeline
    >>> dp_pipeline = Pipeline(stages = [raceIdxer,
             awardIdxer, countryIdxer, bucketizer, assembler])
    //Transform dataset
    >>> cleaned_df = dp_pipeline.fit(cleaned_df).transform(cleaned_df)
    >>> cleaned_df.columns
    ['dob', 'birthplace', 'country', 'age', 'race', 'award', 'raceIdx', 'awardIdx', 'countryIdx', 'age_buckets', 'features']

    //Split data into train and test datasets
    >>> trainData, testData = cleaned_df.randomSplit([0.7, 0.3])

在进行所有数据准备活动之后,您将得到一个完全数字化的数据,没有缺失值,并且每个属性中的级别是可管理的。您可能已经删除了可能对手头的分析没有太大价值的任何属性。这就是我们所说的最终数据矩阵。现在您已经准备好开始对数据进行建模了。因此,首先将源数据分成训练数据和测试数据。模型使用训练数据进行“训练”,并使用测试数据进行“测试”。请注意,拆分是随机的,如果重新进行拆分,您可能会得到不同的训练和测试分区。

模型构建

模型是事物的表现,现实的描述或描述。就像物理建筑的模型一样,数据科学模型试图理解现实;在这种情况下,现实是特征和预测变量之间的基本关系。它们可能不是 100%准确,但仍然非常有用,可以根据数据为我们的业务空间提供一些深刻的见解。

有几种机器学习算法可以帮助我们对数据进行建模,Spark 提供了其中许多。然而,要构建哪种模型仍然是一个价值百万的问题。这取决于各种因素,比如解释性和准确性的权衡、手头有多少数据、分类或数值变量、时间和内存限制等等。在下面的代码示例中,我们随机训练了一些模型,以展示如何完成这些步骤。

我们将根据种族、年龄和国家来预测奖项类型。我们将使用 DecisionTreeClassifier、RandomForestClassifier 和 OneVsRest 算法。这三个是任意选择的。它们都适用于多类标签,并且易于理解。我们使用了ml包提供的以下评估指标:

  • 准确性:正确预测的观察比例。

  • 加权精确度:精确度是正确的正例观察值与所有正例观察值的比率。加权精确度考虑了各个类别的频率。

  • 加权召回率:召回率是正例与实际正例的比率。实际正例是真正例和假负例的总和。加权召回率考虑了各个类别的频率。

  • F1:默认的评估指标。这是精确度和召回率的加权平均值。

Scala:

scala> import org.apache.spark.ml.Pipeline 
import org.apache.spark.ml.Pipeline 
scala> import org.apache.spark.ml.classification.DecisionTreeClassifier 
import org.apache.spark.ml.classification.DecisionTreeClassifier 

//Use Decision tree classifier 
scala> val dtreeModel = new DecisionTreeClassifier(). 
           setLabelCol("awardIdx").setFeaturesCol("features"). 
           fit(trainData) 
dtreeModel: org.apache.spark.ml.classification.DecisionTreeClassificationModel = DecisionTreeClassificationModel (uid=dtc_76c9e80680a7) of depth 5 with 39 nodes 

//Run predictions using testData 
scala> val dtree_predictions = dtreeModel.transform(testData) 
dtree_predictions: org.apache.spark.sql.DataFrame = [dob: string, birthplace: string ... 12 more fields] 

//Examine results. Your results may vary due to randomSplit 
scala> dtree_predictions.select("award","awardIdx","prediction").show(4) 
+--------------------+--------+----------+ 
|               award|awardIdx|prediction| 
+--------------------+--------+----------+ 
|       Best Director|     1.0|       1.0| 
|        Best Actress|     0.0|       0.0| 
|        Best Actress|     0.0|       0.0| 
|Best Supporting A...|     4.0|       3.0| 
+--------------------+--------+----------+ 

//Compute prediction mismatch count 
scala> dtree_predictions.filter(dtree_predictions("awardIdx") =!= dtree_predictions("prediction")).count() 
res10: Long = 88 
scala> testData.count 
res11: Long = 126 
//Predictions match with DecisionTreeClassifier model is about 30% ((126-88)*100/126) 

//Train Random forest 
scala> import org.apache.spark.ml.classification.RandomForestClassifier 
import org.apache.spark.ml.classification.RandomForestClassifier 
scala> import org.apache.spark.ml.classification.RandomForestClassificationModel 
import org.apache.spark.ml.classification.RandomForestClassificationModel 
scala> import org.apache.spark.ml.feature.{StringIndexer, IndexToString, VectorIndexer} 
import org.apache.spark.ml.feature.{StringIndexer, IndexToString, VectorIndexer} 

//Build model 
scala> val RFmodel = new RandomForestClassifier(). 
        setLabelCol("awardIdx"). 
        setFeaturesCol("features"). 
        setNumTrees(6).fit(trainData) 
RFmodel: org.apache.spark.ml.classification.RandomForestClassificationModel = RandomForestClassificationModel (uid=rfc_c6fb8d764ade) with 6 trees 
//Run predictions on the same test data using Random Forest model 
scala> val RF_predictions = RFmodel.transform(testData) 
RF_predictions: org.apache.spark.sql.DataFrame = [dob: string, birthplace: string ... 12 more fields] 
//Check results 
scala> RF_predictions.filter(RF_predictions("awardIdx") =!= RF_predictions("prediction")).count() 
res29: Long = 87 //Roughly the same as DecisionTreeClassifier 

//Try OneVsRest Logistic regression technique 
scala> import org.apache.spark.ml.classification.{LogisticRegression, OneVsRest} 
import org.apache.spark.ml.classification.{LogisticRegression, OneVsRest} 
//This model requires a base classifier 
scala> val classifier = new LogisticRegression(). 
            setLabelCol("awardIdx"). 
            setFeaturesCol("features"). 
            setMaxIter(30). 
            setTol(1E-6). 
            setFitIntercept(true) 
classifier: org.apache.spark.ml.classification.LogisticRegression = logreg_82cd24368c87 

//Fit OneVsRest model 
scala> val ovrModel = new OneVsRest(). 
           setClassifier(classifier). 
           setLabelCol("awardIdx"). 
           setFeaturesCol("features"). 
           fit(trainData) 
ovrModel: org.apache.spark.ml.classification.OneVsRestModel = oneVsRest_e696c41c0bcf 
//Run predictions 
scala> val OVR_predictions = ovrModel.transform(testData) 
predictions: org.apache.spark.sql.DataFrame = [dob: string, birthplace: string ... 10 more fields] 
//Check results 
scala> OVR_predictions.filter(OVR_predictions("awardIdx") =!= OVR_predictions("prediction")).count()          
res32: Long = 86 //Roughly the same as other models 

Python:

    >>> from pyspark.ml import Pipeline
    >>> from pyspark.ml.classification import DecisionTreeClassifier

    //Use Decision tree classifier
    >>> dtreeModel = DecisionTreeClassifier(labelCol = "awardIdx", featuresCol="features").fit(trainData)

    //Run predictions using testData
    >>> dtree_predictions = dtreeModel.transform(testData)

    //Examine results. Your results may vary due to randomSplit
    >>> dtree_predictions.select("award","awardIdx","prediction").show(4)
    +--------------------+--------+----------+
    |               award|awardIdx|prediction|
    +--------------------+--------+----------+
    |       Best Director|     1.0|       4.0|
    |       Best Director|     1.0|       1.0|
    |       Best Director|     1.0|       1.0|
    |Best Supporting A...|     4.0|       3.0|
    +--------------------+--------+----------+

    >>> dtree_predictions.filter(dtree_predictions["awardIdx"] != dtree_predictions["prediction"]).count()
    92
    >>> testData.count()
    137
    >>>
    //Predictions match with DecisionTreeClassifier model is about 31% ((133-92)*100/133)

    //Train Random forest
    >>> from pyspark.ml.classification import RandomForestClassifier, RandomForestClassificationModel
    >>> from pyspark.ml.feature import StringIndexer, IndexToString, VectorIndexer
    >>> from pyspark.ml.evaluation import MulticlassClassificationEvaluator

    //Build model
    >>> RFmodel = RandomForestClassifier(labelCol = "awardIdx", featuresCol = "features", numTrees=6).fit(trainData)

    //Run predictions on the same test data using Random Forest model
    >>> RF_predictions = RFmodel.transform(testData)
    //Check results
    >>> RF_predictions.filter(RF_predictions["awardIdx"] != RF_predictions["prediction"]).count()
    94     //Roughly the same as DecisionTreeClassifier

    //Try OneVsRest Logistic regression technique
    >>> from pyspark.ml.classification import LogisticRegression, OneVsRest

    //This model requires a base classifier
    >>> classifier = LogisticRegression(labelCol = "awardIdx", featuresCol="features",
                  maxIter = 30, tol=1E-6, fitIntercept = True)
    //Fit OneVsRest model
    >>> ovrModel = OneVsRest(classifier = classifier, labelCol = "awardIdx",
                    featuresCol = "features").fit(trainData)
    //Run predictions
    >>> OVR_predictions = ovrModel.transform(testData)
    //Check results
    >>> OVR_predictions.filter(OVR_predictions["awardIdx"] != OVR_predictions["prediction"]).count()
    90  //Roughly the same as other models

到目前为止,我们尝试了一些模型,并发现它们大致表现相同。还有其他各种验证模型性能的方法。这再次取决于你使用的算法、业务背景和产生的结果。让我们看看spark.ml.evaluation包中提供的一些开箱即用的指标:

Scala:

scala> import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator 
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator 
//F1 
scala> val f1_eval = new MulticlassClassificationEvaluator(). 
                     setLabelCol("awardIdx") //Default metric is F1 
f1_eval: org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator = mcEval_e855a949bb0e 

//WeightedPrecision 
scala> val wp_eval = new MulticlassClassificationEvaluator(). 
                     setMetricName("weightedPrecision").setLabelCol("awardIdx") 
wp_eval: org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator = mcEval_44fd64e29d0a 

//WeightedRecall 
scala> val wr_eval = new MulticlassClassificationEvaluator(). 
                     setMetricName("weightedRecall").setLabelCol("awardIdx") 
wr_eval: org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator = mcEval_aa341966305a 
//Compute measures for all models 
scala> val f1_eval_list = List (dtree_predictions, RF_predictions, OVR_predictions) map ( 
           x => f1_eval.evaluate(x)) 
f1_eval_list: List[Double] = List(0.2330854098674473, 0.2330854098674473, 0.2330854098674473) 
scala> val wp_eval_list = List (dtree_predictions, RF_predictions, OVR_predictions) map ( 
           x => wp_eval.evaluate(x)) 
wp_eval_list: List[Double] = List(0.2661599224979506, 0.2661599224979506, 0.2661599224979506) 

scala> val wr_eval_list = List (dtree_predictions, RF_predictions, OVR_predictions) map ( 
           x => wr_eval.evaluate(x)) 
wr_eval_list: List[Double] = List(0.31746031746031744, 0.31746031746031744, 0.31746031746031744) 

Python:

    >>> from pyspark.ml.evaluation import MulticlassClassificationEvaluator

    //F1
    >>> f1_eval = MulticlassClassificationEvaluator(labelCol="awardIdx") //Default metric is F1
    //WeightedPrecision
    >>> wp_eval = MulticlassClassificationEvaluator(labelCol="awardIdx", metricName="weightedPrecision")
    //WeightedRecall
    >>> wr_eval = MulticlassClassificationEvaluator(labelCol="awardIdx", metricName="weightedRecall")
    //Accuracy
    >>> acc_eval = MulticlassClassificationEvaluator(labelCol="awardIdx", metricName="Accuracy")
    //Compute measures for all models
    >>> f1_eval_list = [ f1_eval.evaluate(x) for x in [dtree_predictions, RF_predictions, OVR_predictions]]
    >>> wp_eval_list = [ wp_eval.evaluate(x) for x in [dtree_predictions, RF_predictions, OVR_predictions]]
    >>> wr_eval_list = [ wr_eval.evaluate(x) for x in [dtree_predictions, RF_predictions, OVR_predictions]]
    //Print results for DecisionTree, Random Forest and OneVsRest
    >>> f1_eval_list
    [0.2957949866055487, 0.2645186821042419, 0.2564967990214734]
    >>> wp_eval_list
    [0.3265407181548341, 0.31914852065228005, 0.25295826631254753]
    >>> wr_eval_list
    [0.3082706766917293, 0.2932330827067669, 0.3233082706766917]

输出:

决策树 随机森林 OneVsRest
F1 0.29579 0.26451 0.25649
加权精确度 0.32654 0.26451 0.25295
加权召回率 0.30827 0.29323 0.32330

在验证模型性能后,你将不得不尽可能调整模型。现在,调整可以在数据级别和算法级别两种方式进行。提供算法期望的正确数据非常重要。问题在于,无论你提供什么数据,算法可能仍然会给出一些输出-它从不抱怨!因此,除了通过处理缺失值、处理一元和多元异常值等来正确清理数据之外,你还可以创建更多相关的特征。这种特征工程通常被视为数据科学中最重要的方面。具有良好的领域专业知识有助于构建更好的特征。现在,来到调整的算法方面,总是有优化我们传递给算法的参数的空间。你可以选择使用网格搜索来找到最佳参数。此外,数据科学家应该质疑自己要使用哪种损失函数以及为什么,以及在 GD、SGD、L-BFGS 等中,要使用哪种算法来优化损失函数以及为什么。

请注意,前面的方法仅用于演示如何在 Spark 上执行这些步骤。仅仅通过准确性水平来选择一种算法可能不是最佳方式。选择算法取决于你处理的数据类型、结果变量、业务问题/需求、计算挑战、可解释性等等。

数据可视化

数据可视化是在从事数据科学任务时经常需要的东西。在构建任何模型之前,最好是要可视化每个变量,以了解它们的分布,理解它们的特征,并找到异常值,以便进行处理。散点图、箱线图、条形图等简单工具是用于这些目的的一些多功能、方便的工具。此外,您将不得不在大多数步骤中使用可视化工具,以确保您朝着正确的方向前进。

每次您想与业务用户或利益相关者合作时,通过可视化传达您的分析总是一个很好的做法。可视化可以更有意义地容纳更多的数据,并且本质上是直观的。

请注意,大多数数据科学任务的结果最好通过可视化和仪表板向业务用户呈现。我们已经有了一个专门讨论这个主题的章节,所以我们不会深入讨论。

向业务用户传达结果

在现实生活中,通常情况下,您必须不断与业务进行沟通。在最终确定生产就绪模型之前,您可能必须构建多个模型,并将结果传达给业务。

可实施的模型并不总是取决于准确性;您可能需要引入其他措施,如灵敏度、特异性或 ROC 曲线,并通过可视化来表示您的结果,比如增益/提升图表或具有统计显著性的 K-S 测试的输出。请注意,这些技术需要业务用户的输入。这种输入通常指导您构建模型或设置阈值的方式。让我们看一些例子,以更好地理解它是如何工作的:

  • 如果一个回归器预测事件发生的概率,那么盲目地将阈值设置为 0.5,并假设大于 0.5 的为 1,小于 0.5 的为 0 可能不是最佳方式!您可以使用 ROC 曲线,并做出更科学或更合乎逻辑的决定。

  • 对于癌症测试的假阴性预测可能根本不可取!这是一种极端的生命风险。

  • 与寄送硬拷贝相比,电子邮件营销成本更低。因此,企业可能决定向预测概率低于 0.5(比如 0.35)的收件人发送电子邮件。

请注意,前述决策受到业务用户或问题所有者的严重影响,数据科学家与他们密切合作,以在这些情况下做出决策。

正如前面已经讨论过的,正确的可视化是向业务传达结果的最佳方式。

摘要

在本章中,我们进行了一个案例研究,并完成了数据分析的整个生命周期。在构建数据产品的过程中,我们应用了之前章节中所学到的知识。我们提出了一个业务问题,形成了一个初始假设,获取了数据,并准备好了模型构建。我们尝试构建多个模型,并找到了一个合适的模型。

在下一章,也是最后一章中,我们将讨论使用 Spark 构建真实世界的应用程序。

参考文献

www2.sas.com/proceedings/forum2007/073-2007.pdf

azure.microsoft.com/en-in/documentation/articles/machine-learning-algorithm-choice/

www.cs.cornell.edu/courses/cs578/2003fa/performance_measures.pdf

第十一章:构建数据科学应用

数据科学应用引起了很多兴奋,主要是因为它们在利用数据和提取可消费的结果方面的承诺。已经有几个成功的数据产品对我们的日常生活产生了变革性影响。无处不在的推荐系统、电子邮件垃圾邮件过滤器、定向广告和新闻内容已经成为生活的一部分。音乐和电影已经成为来自 iTunes 和 Netflix 等提供商的数据产品。企业,特别是在零售等领域,正在积极寻求通过使用数据驱动的方法来研究市场和客户行为以获得竞争优势的方式。

到目前为止,在前几章中,我们已经讨论了数据分析工作流程直到模型构建阶段。但是模型真正的价值在于它实际部署到生产系统中。数据科学工作流的最终产品是一个操作化的数据产品。在本章中,我们将讨论数据分析工作流的这个最终阶段。我们不会涉及实际的代码片段,而是退一步,获取完整的画面,包括非技术方面。

完整的画面不仅仅局限于开发过程本身。它包括用户应用程序、Spark 本身的发展,以及大数据领域正在发生的快速变化。我们将首先从用户应用程序的开发过程开始,讨论每个阶段的各种选项。然后我们将深入探讨最新的 Spark 2.0 版本和未来计划中的功能和增强功能。最后,我们将尝试对大数据趋势,特别是 Hadoop 生态系统,进行广泛的概述。参考资料和有用的链接将包括在各个部分中,以及章节末尾,以获取有关特定上下文的更多信息。

开发范围

数据分析工作流程大致可以分为两个阶段,即构建阶段和操作化阶段。第一阶段通常是一次性的练习,需要大量人为干预。一旦我们获得了合理的最终结果,我们就准备好将产品操作化。第二阶段从第一阶段生成的模型开始,并将其作为某个生产工作流程的一部分提供。在本节中,我们将讨论以下内容:

  • 期望

  • 演示选项

  • 开发和测试

  • 数据质量管理

期望

数据科学应用的主要目标是构建“可操作”的洞察力,可操作是关键词。许多用例,如欺诈检测,需要以可消费的方式生成并在几乎实时中提供洞察力,如果您期望有任何可操作性。数据产品的最终用户因用例而异。他们可能是电子商务网站的客户,也可能是大型企业的决策者。最终用户不一定总是人类。它可能是金融机构中的风险评估软件工具。一刀切的方法并不适用于许多软件产品,数据产品也不例外。然而,对于数据产品,有一些共同的期望,如下所列:

  • 首要和最重要的期望是基于现实世界数据的洞察力生成时间应该在“可操作”的时间范围内。实际时间范围根据用例而异。

  • 数据产品应该整合到一些(通常已经存在的)生产工作流程中。

  • 洞察力应该被转化为人们可以使用的东西,而不是晦涩的数字或难以解释的图表。演示应该是不显眼的。

  • 数据产品应该能够根据输入的数据自我调整(自适应)。

  • 理想情况下,应该有一种方式来接收人类反馈,这可以作为自我调整的来源。

  • 应该有一个定期和自动地定量评估其有效性的机制。

演示选项

数据产品的多样性要求多样的展示方式。有时,数据分析练习的最终结果是发表研究论文。有时它可能是仪表板的一部分,其中它成为单个网页上发布结果的几个来源之一。它们可能是公开的,针对人类消费,也可能是隐蔽的,供其他软件应用程序使用。您可以使用通用引擎,如 Spark 来构建解决方案,但展示必须与目标用户群高度对齐。

有时,您只需要写一封电子邮件,附上您的发现,或者只是导出一个洞察力的 CSV 文件。或者您可能需要围绕您的数据产品开发一个专用的 Web 应用程序。这里讨论了一些其他常见的选项,您必须选择适合手头问题的正确选项。

交互式笔记本

交互式笔记本是允许您创建和共享包含代码块、结果、方程、图像、视频和解释文本的文档的 Web 应用程序。它们可以被视为可执行文档或带有可视化和方程支持的 REPL shell。这些文档可以导出为 PDF、Markdown 或 HTML。笔记本包含多个“内核”或“计算引擎”来执行代码块。

如果您的数据分析工作流的最终目标是生成书面报告,交互式笔记本是最合适的选择。有几种笔记本,其中许多都支持 Spark。这些笔记本在探索阶段也是有用的工具。我们在之前的章节中已经介绍了 IPython 和 Zeppelin 笔记本。

参考资料

Web API

应用程序编程接口API)是软件到软件的接口;描述可用功能、如何使用以及输入和输出的规范。软件(服务)提供者将其部分功能公开为 API。开发人员可以开发一个消耗此 API 的软件组件。例如,Twitter 提供 API 以在 Twitter 上获取或发布数据,或以编程方式查询数据。Spark 爱好者可以编写一个软件组件,自动收集所有关于#Spark 的推文,根据他们的要求进行分类,并在其个人网站上发布这些数据。Web API 是 API 的一种类型,其中接口被定义为一组超文本传输协议HTTP)请求消息以及响应消息结构的定义。如今,REST-ful(表述性状态转移)已成为事实上的标准。

您可以将数据产品实现为 API,也许这是最强大的选择。然后可以将其插入一个或多个应用程序,比如管理仪表板以及营销分析工作流。您可以开发一个特定领域的“见解即服务”作为公共 Web API,并采用订阅模式。Web API 的简单性和普遍性使其成为构建数据产品的最具吸引力的选择。

参考资料

PMML 和 PFA

有时,您可能需要以其他数据挖掘工具能够理解的方式公开您的模型。模型和完整的预处理和后处理步骤应转换为标准格式。PMML 和 PFA 是数据挖掘领域的两种标准格式。

预测模型标记语言PMML)是基于 XML 的预测模型交换格式,Apache Spark API 可以将模型转换为 PMML。 PMML 消息可能包含多种数据转换以及一个或多个预测模型。不同的数据挖掘工具可以导出或导入 PMML 消息,无需自定义代码。

Analytics 的便携格式PFA)是下一代预测模型交换格式。它交换 JSON 文档,并立即继承 JSON 文档相对于 XML 文档的所有优势。此外,PFA 比 PMML 更灵活。

参考资料

开发和测试

Apache Spark 是一种通用的集群计算系统,可以独立运行,也可以在几个现有的集群管理器上运行,如 Apache Mesos、Hadoop、Yarn 和 Amazon EC2。此外,一些大数据和企业软件公司已经将 Spark 集成到其产品中:Microsoft Azure HDInsight、Cloudera、IBM Analytics for Apache Spark、SAP HANA 等等。由 Apache Spark 的创始人创立的 Databricks 公司拥有自己的产品,用于数据科学工作流程,从数据摄取到生产。您的责任是了解组织的要求和现有的人才储备,并决定哪个选项对您最好。

无论选择哪个选项,都要遵循任何软件开发生命周期中的通常最佳实践,例如版本控制和同行审查。尽量在适用的地方使用高级 API。生产中使用的数据转换流水线应与构建模型时使用的流水线相同。记录在数据分析工作流程中出现的任何问题。通常这些问题可能导致业务流程改进。

一如既往,测试对于产品的成功非常重要。您必须维护一组自动化脚本,以提供易于理解的结果。测试用例应至少涵盖以下内容:

  • 遵守时间框架和资源消耗要求

  • 对不良数据的弹性(例如,数据类型违规)

  • 在模型构建阶段未遇到的分类特征中的新值

  • 在目标生产系统中预期的非常少的数据或过重的数据

监视日志、资源利用率等,以发现任何性能瓶颈。Spark UI 提供了大量信息来监视 Spark 应用程序。以下是一些常见的提示,将帮助您提高性能:

  • 缓存可能多次使用的任何输入或中间数据。

  • 查看 Spark UI 并识别导致大量洗牌的作业。检查代码,看看是否可以减少洗牌。

  • 操作可能会将数据从工作节点传输到驱动程序。请注意,您不要传输任何绝对不必要的数据。

  • Stragglers;比其他任务运行速度慢;可能会增加整体作业完成时间。出现任务运行缓慢可能有几个原因。如果作业因为一个慢节点而运行缓慢,您可以将spark.speculation设置为true。然后 Spark 会自动在不同节点上重新启动这样的任务。否则,您可能需要重新审视逻辑,看看是否可以改进。

参考

数据质量管理

首先,让我们不要忘记,我们正在尝试从不可靠、通常是非结构化和不受控的数据源构建容错的软件数据产品。因此,在数据科学工作流程中,数据质量管理变得更加重要。有时数据可能仅来自受控数据源,例如组织中的自动化内部流程工作流。但在所有其他情况下,您需要仔细制定数据清洗流程,以保护后续处理。

元数据包括数据的结构和含义,显然是要处理的最关键的存储库。它是关于单个数据源结构和该结构中每个组件含义的信息。您可能并不总是能够编写一些脚本并提取这些数据。单个数据源可能包含具有不同结构的数据,或者单个组件(列)在不同时间可能意味着不同的事情。例如,标签如所有者或高在不同的数据源中可能意味着不同的事情。收集和理解所有这样的细微差别并记录是一项繁琐的迭代任务。元数据的标准化是数据转换开发的先决条件。

适用于大多数用例的一些广泛指导原则列在这里:

  • 所有数据源必须进行版本控制和时间戳标记

  • 数据质量管理过程通常需要最高管理机构的参与

  • 屏蔽或匿名化敏感数据

  • 经常被忽视的一个重要步骤是保持可追溯性;每个数据元素(比如一行)与其原始来源之间的链接

Scala 的优势

Apache Spark 允许您使用 Python、R、Java 或 Scala 编写应用程序。这种灵活性带来了选择适合您需求的正确语言的责任。但无论您通常选择的语言是什么,您可能会考虑为您的 Spark 应用程序选择 Scala。在本节中,我们将解释为什么。

首先,让我们离题一下,以便对命令式和函数式编程范式有一个高层次的理解。像 C、Python 和 Java 这样的语言属于命令式编程范式。在命令式编程范式中,程序是一系列指令,它有一个程序状态。程序状态通常表示为一组变量及其在任何给定时间点的值。赋值和重新赋值是相当常见的。变量的值预计会在执行期间由一个或多个函数改变。函数中的变量值修改不仅限于局部变量。全局变量和公共类变量是这些变量的一些例子。

相比之下,使用函数式编程语言编写的程序可以被视为无状态的表达式求值器。数据是不可变的。如果一个函数使用相同的输入参数集合调用,那么预期会产生相同的结果(即引用透明)。这是由于全局变量等变量上下文的干扰的缺失。这意味着函数求值的顺序并不重要。函数可以作为参数传递给其他函数。递归调用取代了循环。无状态使得并行编程更容易,因为它消除了锁定和可能的死锁的需要。当执行顺序不那么重要时,协调变得更加简单。这些因素使得函数式编程范式非常适合并行编程。

纯函数式编程语言很难使用,因为大多数程序需要状态改变。大多数函数式编程语言,包括老式的 Lisp,都允许在变量中存储数据(副作用)。一些语言,如 Scala,汲取了多种编程范式。

回到 Scala,它是一种基于 JVM 的、静态类型的多范式编程语言。它的内置类型推断机制允许程序员省略一些冗余的类型信息。这给人一种动态语言所提供的灵活性的感觉,同时保留了更好的编译时检查和快速运行时的健壮性。Scala 是一种面向对象的语言,因为每个值都是一个对象,包括数值值。函数是一级对象,可以用作任何数据类型,并且可以作为参数传递给其他函数。Scala 与 Java 及其工具很好地互操作,因为 Scala 在 JVM 上运行。Java 和 Scala 类可以自由混合。这意味着 Scala 可以轻松地与 Hadoop 生态系统进行交互。

选择适合你的应用程序的编程语言时,所有这些因素都应该被考虑进去。

Spark 的开发状态

Apache Spark 已成为截至 2015 年底 Hadoop 生态系统中活跃度最高的项目,以贡献者数量计算。Spark 始于 2009 年的加州大学伯克利分校 AMPLAB 的一个研究项目,与 Apache Hadoop 等项目相比,Spark 仍然相对年轻,并且仍在积极开发中。2015 年有三个版本发布,从 1.3 到 1.5,分别包含了 DataFrames API、SparkR 和 Project Tungsten 等功能。1.6 版本于 2016 年初发布,包括新的 Dataset API 和数据科学功能的扩展。Spark 2.0 于 2016 年 7 月发布,作为一个重大版本,具有许多新功能和增强功能,值得单独介绍。

Spark 2.0 的功能和增强

Apache Spark 2.0 包括三个主要的新功能和其他性能改进和底层更改。本节试图给出一个高层次的概述,同时在必要时深入细节,以便理解概念。

统一 Datasets 和 DataFrames

DataFrames 是支持数据抽象概念上等同于关系数据库中的表或 R 和 Python 中的 DataFrame(pandas 库)的高级 API。Datasets 是 DataFrame API 的扩展,提供了一个类型安全的、面向对象的编程接口。Datasets 为 DataFrames 添加了静态类型。在 DataFrames 之上定义结构提供了信息给核心,从而实现了优化。它还有助于在分布式作业开始之前及早捕捉分析错误。

RDD、Dataset 和 DataFrame 是可互换的。RDD 仍然是低级 API。DataFrame、Dataset 和 SQL 共享相同的优化和执行管道。机器学习库可以使用 DataFrame 或 Dataset。DataFrame 和 Dataset 都在钨上运行,这是一个改进运行时性能的倡议。它们利用钨的快速内存编码,负责在 JVM 对象和 Spark 内部表示之间进行转换。相同的 API 也适用于流,引入了连续 DataFrame 的概念。

结构化流

结构化流 API 是构建在 Spark SQL 引擎上并扩展了 DataFrame 和 Dataset 的高级 API。结构化流统一了流式、交互式和批处理查询。在大多数情况下,流数据需要与批处理和交互式查询相结合,形成连续的应用程序。这些 API 旨在满足这一要求。Spark 负责在流数据上增量和连续地运行查询。

结构化流的第一个版本将专注于 ETL 工作负载。用户将能够指定输入、查询、触发器和输出类型。输入流在逻辑上等同于只追加的表。用户定义查询的方式与传统 SQL 表相同。触发器是一个时间段,比如一秒。提供的输出模式包括完整输出、增量或原地更新(例如,DB 表)。

举个例子:您可以在流中聚合数据,使用 Spark SQL JDBC 服务器提供数据,并将其传递给 MySQL 等数据库进行下游应用。或者您可以运行针对最新数据的临时 SQL 查询。您还可以构建和应用机器学习模型。

项目钨 2 期

项目钨的核心思想是通过本机内存管理和运行时代码生成将 Spark 的性能更接近于裸金属。它首次包含在 Spark 1.4 中,并在 1.5 和 1.6 中添加了增强功能。它主要通过以下方式显着提高 Spark 应用程序的内存和 CPU 效率:

  • 显式管理内存并消除 JVM 对象模型和垃圾收集的开销。例如,一个四字节的字符串在 JVM 对象模型中占用大约 48 字节。由于 Spark 不是通用应用程序,并且对内存块的生命周期比垃圾收集器更了解,因此它可以比 JVM 更有效地管理内存。

  • 设计友好缓存的算法和数据结构。

  • Spark 执行代码生成以将查询的部分编译为 Java 字节码。这将被扩展以覆盖大多数内置表达式。

Spark 2.0 推出了第 2 阶段,速度提高了一个数量级,并包括:

  • 通过消除昂贵的迭代器调用和在多个运算符之间融合来进行整体代码生成,从而使生成的代码看起来像手动优化的代码

  • 优化的输入和输出

未来会有什么?

预计 Apache Spark 2.1 将具有以下功能:

  • 连续 SQLCSQL

  • BI 应用程序集成

  • 更多流式数据源和接收器的支持

  • 包括用于结构化流的额外运算符和库

  • 机器学习包的增强功能

  • 钨中的列式内存支持

大数据趋势

大数据处理一直是 IT 行业的一个重要组成部分,尤其是在过去的十年中。Apache Hadoop 和其他类似的努力致力于构建存储和处理海量数据的基础设施。在经过 10 多年的发展后,Hadoop 平台被认为是成熟的,几乎可以与大数据处理等同起来。Apache Spark 是一个通用计算引擎,与 Hadoop 生态系统兼容,并且在 2015 年取得了相当大的成功。

构建数据科学应用程序需要了解大数据领域和可用软件产品。我们需要仔细地映射符合我们要求的正确模块。有几个具有重叠功能的选项,选择合适的工具说起来容易做起来难。应用程序的成功很大程度上取决于组装正确的技术和流程。好消息是,有几个开源选项可以降低大数据分析的成本;同时,你也可以选择由 Databricks 等公司支持的企业级端到端平台。除了手头的用例,跟踪行业趋势同样重要。

最近 NOSQL 数据存储的激增及其自己的接口正在添加基于 SQL 的接口,尽管它们不是关系数据存储,也可能不遵守 ACID 属性。这是一个受欢迎的趋势,因为在关系和非关系数据存储之间趋同于一个古老的接口可以提高程序员的生产力。

在过去的几十年里,操作(OLTP)和分析(OLAP)系统一直被维护为独立的系统,但这是又一个融合正在发生的地方。这种融合使我们接近实时的用例,比如欺诈预防。Apache Kylin 是 Hadoop 生态系统中的一个开源分布式分析引擎,提供了一个极快的大规模 OLAP 引擎。

物联网的出现正在加速实时和流式分析,带来了许多新的用例。云释放了组织的运营和 IT 管理开销,使他们可以集中精力在他们的核心竞争力上,特别是在大数据处理方面。基于云的分析引擎、自助数据准备工具、自助 BI、及时数据仓库、高级分析、丰富的媒体分析和敏捷分析是一些常用的词汇。大数据这个词本身正在慢慢消失或变得隐含。

在大数据领域有许多具有重叠功能的软件产品和库,如此信息图所示(http://mattturck.com/wp-content/uploads/2016/02/matt_turck_big_data_landscape_v11.png)。选择适合你的应用程序的正确模块是一项艰巨但非常重要的任务。以下是一些让你开始的项目的简短列表。该列表不包括像 Cassandra 这样的知名名称,而是试图包括具有互补功能的模块,大多来自 Apache 软件基金会:

  • Apache Arrowarrow.apache.org/)是用于加速分析处理和交换的内存中的列式层。这是一个高性能、跨系统和内存数据表示,预计将带来 100 倍的性能改进。

  • Apache Parquetparquet.apache.org/)是一种列式存储格式。Spark SQL 提供对读写 parquet 文件的支持,同时自动捕获数据的结构。

  • Apache Kafkakafka.apache.org/)是一种流行的、高吞吐量的分布式消息系统。Spark streaming 具有直接的 API,支持从 Kafka 进行流式数据摄入。

  • Alluxioalluxio.org/),以前称为 Tachyon,是一个以内存为中心的虚拟分布式存储系统,可以在内存速度下跨集群共享数据。它旨在成为大数据的事实存储统一层。Alluxio 位于计算框架(如 Spark)和存储系统(如 Amazon S3、HDFS 等)之间。

  • GraphFrameshttps://databricks.com/blog/2016/03/03/introducing-graphframes.html)是建立在 DataFrames API 之上的 Apache Spark 的图处理库。

  • Apache Kylin(kylin.apache.org/)是一个分布式分析引擎,旨在提供 SQL 接口和 Hadoop 上的多维分析(OLAP)支持,支持极大的数据集。

  • Apache Sentry(sentry.apache.org/)是一个用于强制执行基于角色的细粒度授权的系统,用于存储在 Hadoop 集群上的数据和元数据。在撰写本书时,它处于孵化阶段。

  • Apache Solr(lucene.apache.org/solr/)是一个极快的搜索平台。查看这个演示,了解如何集成 Solr 和 Spark。

  • TensorFlow(www.tensorflow.org/)是一个具有广泛内置深度学习支持的机器学习库。查看这篇博客了解它如何与 Spark 一起使用。

  • Zeppelin(zeppelin.incubator.apache.org/)是一个基于 Web 的笔记本,可以进行交互式数据分析。它在数据可视化章节中有所涉及。

摘要

在本章中,我们讨论了如何使用 Spark 构建真实世界的应用程序。我们讨论了由技术和非技术方面组成的数据分析工作流的大局。

参考资料

  • Spark Summit 网站上有大量关于 Apache Spark 和相关项目的信息,来自已完成的活动

  • KDnuggets 对 Matei Zaharia 的采访

  • 2015 年,为什么 Spark 达到了临界点,来自 KDnuggets 的 Matthew Mayo

  • 上线:准备您的第一个 Spark 生产部署是一个非常好的起点

  • 来自 Scala 官网的“什么是 Scala?”

  • Scala 的创始人 Martin Odersky 解释了为什么 Scala 将命令式和函数式编程融合在一起的原因

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(33)  评论(0编辑  收藏  举报