Hadoop3-大数据分析-二-

67 阅读50分钟

Hadoop3 大数据分析(二)

原文:Big Data Analytics with Hadoop 3

协议:CC BY-NC-SA 4.0

四、基于 Python 和 Hadoop 的科学计算和大数据分析

在本章中,我们将介绍 Python 以及使用 Hadoop 和 Python 包分析大数据。我们将看到一个基本的 Python 安装,打开一个 Jupyter 笔记本,并通过一些例子进行工作。

简而言之,本章将涵盖以下主题:

  • 安装:
    • 下载并安装 Python
    • 下载并安装 Anaconda
    • 安装 Jupyter 笔记本
  • 数据分析

装置

在本节中,我们将了解使用 Python 解释器安装和设置 Jupyter Notebook 以执行数据分析所涉及的步骤。

安装标准 Python

用你的网络浏览器去www.python.org/download/的 Python 下载页面。Python 在 Windows、macOS 和 Linux 上受支持,您会发现不同的安装:

单击下载页面时,您将看到以下屏幕:

如果您点击一个特定的版本,如 3.6.5,那么您将进入一个不同的页面,如下图所示:

您可以阅读发行说明,然后通过向下滚动页面继续下载 Python 版本,如下图所示:

单击适合您的操作系统的正确版本并下载安装程序。下载完成后,在您的计算机上安装 Python。

安装蟒蛇

标准的 Python 安装有局限性,所以你必须安装 Jupyter、其他包、pip等等,才能让安装生产为你做好准备。Anaconda 是一个强调科学的一体化安装程序:它包括 Python、标准库和许多有用的第三方库。

使用浏览器,输入网址www.anaconda.com/download/–这将带您进入 Anaconda 下载页面,如下图所示:

下载适合您平台的 Anaconda 版本,然后按照网页docs.anaconda.com/anaconda/in…上的说明进行安装。

安装完成后,您应该可以打开 Anaconda Navigator(在 Windows 上,这是在“开始”菜单中,在 Mac 上,您可以简单地搜索)。

On Linux, typically you have to use the command line to launch Jupyter Notebook.

例如,在苹果电脑上,出现了如下截图所示的 Anaconda 导航器:

如果您使用的是 Anaconda Navigator,只需点击 Jupyter 笔记本启动按钮即可启动 Jupyter,如下图截图所示:

使用 Conda

到目前为止,Conda 命令行是成功设置 Python 安装最有用、最易于使用的工具。Conda 支持多个可以共存的环境,因此您可以设置 Python 2.7 环境和 Python 3.6 环境。如果你对深度学习感兴趣,你可以将 TensorFlow 设置为一个独立的环境,等等。

You can download and install conda by browsing to the URL conda.io/docs/user-g….

下面的截图是 Conda 安装页面:

从链接下载 Conda 后,按照说明在您的机器上安装 Conda,如下图所示:

在命令行上输入conda list会显示所有安装的软件包。这将帮助您了解安装了哪些版本的软件包:

使用conda安装包装很容易。就像conda install <package name>一样简单。

例如,键入:

conda install scikit-learn

更重要的是,conda install Jupyter安装 Jupyter 笔记本,需要很多其他的包:

让我们尝试另一个重要的包:

conda install pandas

其他重要的包有:

conda install scikit-learn
conda install matplotlib
conda install seaborn

除了conda安装,我们还需要安装软件包来访问 HDFS (Hadoop)和打开文件(拼花格式):

pip install hdfs
pip install pyarrow

Jupyter 笔记本配置可以通过运行如下命令来生成:

[root@4b726275a804 /]# jupyter notebook --generate-config
 Writing default config to: /root/.jupyter/jupyter_notebook_config.py

Jupyter 需要身份验证,默认情况下这是一个令牌。但是,如果您想要创建基于密码的身份验证,那么只需运行下面代码中显示的命令来设置密码:

[root@4b726275a804 /]# jupyter notebook password
 Enter password:
 Verify password:
 [NotebookPasswordApp] Wrote hashed password to /root/.jupyter/jupyter_notebook_config.json

现在,我们已经准备好启动笔记本,因此键入以下命令:

jupyter notebook --allow-root --no-browser --ip=* --port=8888

以下是运行上述命令时的控制台:

当您打开浏览器并输入localhost:8888时,浏览器将打开登录屏幕,然后您必须输入前面步骤中设置的密码:

一旦提供了密码,Jupyter 笔记本门户就会打开,显示任何现有的笔记本。在这种情况下,我们没有以前的笔记本,所以下一步是创建一个。单击新建,然后为您的新笔记本选择 Python 2:

下面是一个新的笔记本,您现在可以在其中键入一些测试代码,如下图所示:

现在我们已经安装了 Python 和 Jupyter Notebook,我们准备使用 Notebooks 和 Python 语言进行数据分析。在下一节中,我们将深入研究可以进行的不同类型的数据分析。

数据分析

从随书提供的链接下载OnlineRetail.csv。然后,您可以使用熊猫加载文件。

以下是使用 Pandas 读取本地文件的简单方法:

import pandas as pd
path = '/Users/sridharalla/Documents/OnlineRetail.csv'
df = pd.read_csv(path)

然而,由于我们是在 Hadoop 集群中分析数据,我们应该使用hdfs而不是本地系统。以下是如何将hdfs文件加载到pandas数据帧的示例:

import pandas as pd
from hdfs import InsecureClient
client_hdfs = InsecureClient('http://localhost:9870')
with client_hdfs.read('/user/normal/OnlineRetail.csv', encoding = 'utf-8') as reader:
 df = pd.read_csv(reader,index_col=0)

下面是下面一行代码的作用:

df.head(3)

您将获得以下结果:

基本上,它显示了数据框中的前三个条目。

我们现在可以用数据做实验。输入以下内容:

len(df)

这将输出以下内容:

65499

这仅仅意味着数据帧的长度或大小。它告诉我们整个文件中有65,499个条目。

现在这样做:

df2 = df.loc[df.UnitPrice > 3.0]
df2.head(3)

我们定义了一个名为df2的新数据框,并将其设置为单价大于 3 的原始数据框中的所有条目。

然后,我们告诉它显示前三个条目,如下图所示:

以下代码行选择单价高于3.0的指数,并将其描述设置为Miscellaneous。然后显示前三项:

df.loc[df.UnitPrice > 3.0, ['Description']] = 'Miscellaneous'
df.head(3)

这就是结果:

如您所见,条目 2(索引为 1)的描述被更改为Miscellaneous,因为它的单价是 3.39 美元(正如我们之前指定的,这已经超过了 3 美元)。

代码行输出索引为 2 的数据:

df.loc[2]

输出如下:

最后,我们可以创建一个数量列的图,如下代码所示:

df['Quantity'].plot()

还有很多功能需要探索。

这里有一个使用.append()函数的例子。

我们定义了一个新的df对象df3,并将其设置为等于df的前 10 行加上df的第 200–209 行。换句话说,我们将第 200-209 行追加到df的第 0-9 行:

df3 = df[0:10].append(df[200:210])
df3

这是结果输出:

现在,假设您只关心几列,即库存代码数量发票日期单价。我们可以定义一个新的DataFrame对象,只包含数据中的那些列:

df4 = pd.DataFrame(df, columns=['StockCode', 'Quantity', 'InvoiceDate', 'UnitPrice']
df4.head(3)

这是以下结果:

熊猫提供了不同的方式来组合数据。更具体地说,我们可以合并连接加入,以及追加。我们已经介绍了 append,所以现在我们来看看连接数据。

看看这个代码块:

d1 = df[0:10]
d2 = df[10:20]

d3 = pd.concat([d1, d2])
d3

基本上,我们将d1设置为一个包含df前 10 个指数的DataFrame对象。然后,我们将d2设置为df的下十个指数。最后,我们将d3设置为d1d2的串联。这是它们连接后的结果:

我们可以做得更多。我们可以指定按键,这样可以更容易区分d1d2。看看下面的代码行:

d3 = pd.concat([d1, d2], keys=['d1', 'd2'])

如您所见,区分这两个数据集要容易得多。我们可以随意调用这些键,甚至像 xy 这样的简单键也可以。如果我们有三个数据集d1d2和一些d3,我们可以说键是( xyz ),这样我们就可以区分所有三个数据集。

现在,我们继续讨论不同列的连接。默认情况下,concat()功能使用外部连接。这意味着它组合了所有的列。想想 A 和 B 两组,其中 A 组包含所有属于d1的列名,B 组包含所有属于d2的列名。如果我们使用前面使用的代码行连接d1d2,我们将看到的列由 A 和 b 的并集表示

我们也可以指定要使用内部连接,用 A 和 b 的交集表示,看看下面几行代码:

d4 = pd.DataFrame(df, columns=['InvoiceNo', 'StockCode', 'Description'])[0:10]
d5 = pd.DataFrame(df, columns=['StockCode', 'Description', 'Quantity'])[0:10]

pd.concat([d4, d5])

如您所见,它使用了所有的列标签。

请记住,默认情况下,concat()使用外部连接。所以,说pd.concat([d4, d5])和说:

pd.concat([d4, d5], join='outer')

现在,我们使用内部连接。保持其他一切不变,但改变对concat()函数的调用。请看下面一行代码:

pd.concat([d4, d5], join='inner')

现在应该输出:

可以看到,这次我们只有d4d5共有的列标签。同样,我们可以添加键,以便更容易区分表中的两个数据集。

合并稍微复杂一些。这一次,您可以在外部联接、内部联接、左侧联接和右侧联接之间进行选择,还可以选择要合并的列。

让我们继续修改我们最初对d4d5的定义:

d4 = pd.DataFrame(df, columns=['InvoiceNo', 'StockCode', 'Description'])[0:11]
d5 = pd.DataFrame(df, columns=['StockCode', 'Description', 'Quantity'])[10:20]

你在d4定义的末尾看到的括号意味着我们将按照定义获取该特定DataFrame的前 11 个元素。d5 定义末尾的括号表示我们将元素 10 到 20 放入d5,而不是整个元素。

值得注意的是,他们将有一个重叠的元素,这将很快发挥作用。

首先从merge功能开始。让我们对d4d5进行左连接合并:

pd.merge(d4, d5, how='left')

这样做是使用了对d4d5中左侧数据框的所有列,并在此基础上添加了d5的列。如您所见,由于我们定义d5包含元素 10 到 20,因此没有从索引 0 到 10 的数量值。然而,由于元素 11 同时在d5d4中,我们在数量下看到了该元素的数据值。

同样,我们可以对右连接做同样的事情:

pd.merge(d4, d5, how='right')

现在,它使用d5的列标签以及d5的数据(从元素 10 到 20)。如您所见,索引 0 处的数据是与d4共享的,因此它在这个特定的表中完成。这是因为元素编号 11(索引 10)与d5(索引 10)的第一个元素重叠。

现在我们做内部连接:

pd.merge(d4, d5, how='inner')

内部连接意味着它只包含两个数据框共有的元素。在这种情况下,显示的元素是元素编号 11,索引 10 在df中。因为它存在于d4d5中,所以它既有发票号的数据,也有数量的数据(因为发票号的数据在d4中,而数量的数据在d5中)。

现在,我们将进行外部连接:

pd.merge(d4, d5, how='outer')

如您所见,外部连接意味着它包括所有列(列在d4d5中的并集)。

任何不存在的数据值都被标记为 NaN。例如d5中没有标注 InvoiceNo 的列,所以那里所有的数据值都显示为 NaN。

现在,让我们谈谈加入一个专栏。我们可以在函数调用中引入一个新参数on=。以下是股票代码栏的合并示例:

pd.merge(d4, d5, on='StockCode', how='left')

该图类似于我们使用左连接合并d4d5时生成的表格。但是,例外的是由于说明d4d5共有的一列,所以增加了两者,但分别用 _x_y 来区分。

正如你在最后一个条目中看到的,它被d4d5共享,所以 Description_xDescription_y 是相同的。

请记住,我们只能输入两个数据框共有的列名。所以,我们可以做股票代码或者描述来合并。

如果我们合并到描述上,看起来就是这样:

pd.merge(d4, d5, on='Description', how='left')

再次,通过添加 _x_y 分别表示d4d5来区分它们共享的列。

我们实际上可以传入一个列名列表,而不是一个列名。所以,现在我们有:

pd.merge(d4, d5, on=['StockCode', 'Description'], how='left')

然而,在这种情况下,我们可以看到,这是同一个表:

pd.merge(d4, d5, how='left')

这是因为在这种特殊情况下,我们传入的列表包含了他们共享的所有列名。如果他们共享三列,而我们只传入两列,情况就不是这样了。

为了说明这一点,假设这样:

d4 = pd.DataFrame(df, columns=['InvoiceNo', 'StockCode', 'Description', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=['StockCode', 'Description', 'Quantity', 'UnitPrice'])[10:20]

现在,让我们再试一次:

pd.merge(d4, d5, on=['StockCode', 'Description'], how='left')

所以,现在我们的桌子看起来像:

我们还可以指定希望所有的列都存在,即使是共享的列。

考虑一下:

pd.merge(d4, d5, left_index = True, right_index=True, how='outer')

您可以指定所需的任何连接类型,它仍会显示所有列。但是,在本例中,它将使用外部联接:

现在,我们可以进入join()功能。需要注意的一点是,如果两个数据框共享一个列名,它将不允许我们连接它们。所以,以下是不允许的:

d4 = pd.DataFrame(df, columns=['StockCode', 'Description', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=[ 'Description', 'Quantity', 'InvoiceNo'])[10:20]
d4.join(d5)

否则,会导致错误。

现在,看看下面几行代码:

d4 = pd.DataFrame(df, columns=['StockCode', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=[ 'Description', 'Quantity'])[10:20]
d4.join(d5)

这将产生这个表:

所以取d4 表,从d5添加列和对应的数据。由于d5没有从指数 0 到 9 的描述或数量数据,它们都显示为 NaN。由于d5d4都共享索引 10 的数据,因此该元素的所有数据都显示在相应的列中。

我们也可以反过来加入他们:

d4 = pd.DataFrame(df, columns=['StockCode', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=[ 'Description', 'Quantity'])[10:20]
d5.join(d4)

这是同样的逻辑,除了d4的列和相应的数据被添加到d5的表中。

接下来,我们可以使用combine_first()来组合数据。

请看下面的代码:

d6 = pd.DataFrame.copy(df)[0:5]
d7 = pd.DataFrame.copy(df)[2:8]

d6.loc[3, ['Quantity']] = 110
d6.loc[4, ['Quantity']] = 110

d7.loc[3, ['Quantity']] = 210
d7.loc[4, ['Quantity']] = 210
pd.concat([d6, d7], keys=['d6', 'd7'])

pd.DataFrame之后添加的.copy确保我们复制了原始的df,而不是编辑原始的df本身。这样,d6将指数34的数量改为110应该不会影响d7,反之亦然。请记住,如果您传入要选择的列列表,这将不起作用,因此您不能有类似以下的内容:

pd.DataFrame(df, columns=['Quantity', 'UnitPrice'])

运行前面的代码后,这就是结果表:

注意d6d7都有共同的元素,即索引为 2 到 4 的元素。

现在,看看这段代码:

d6.combine_first(d7)

这样做是把d7的数据和d6的数据结合起来,但是优先选择d6。请记住,我们在d6中将指数 3 和 4 的数量设置为110。如您所见,d6的数据保存在两个数据集有共同索引的地方。现在看看这一行代码:

d7.combine_first(d6)

现在你会看到,当两个元素有共同的索引时(在索引 3 和 4 处),保留d7 的数据。

您也可以使用value_counts()获得选择类别中每个值的出现次数。看看这段代码:

pd.value_counts(df['Country'])

在合并过程中需要考虑的一件事是,您可能会遇到重复的数据值。要解决这些问题,请使用.drop_duplicates()

考虑一下:

d1 = pd.DataFrame(df, columns = ['InvoiceNo', 'StockCode', 'Description'])[0:100]
d2 = pd.DataFrame(df, columns = ['Description', 'InvoiceDate', 'Quantity'])[0:100]

pd.merge(d1, d2)

如果我们一直滚动到底部:

如您所见,有许多重复的数据条目。要全部移除,我们可以使用drop_duplicates()。此外,我们可以指定可以使用哪些列数据来确定哪些条目是要删除的重复条目。例如,我们可以使用StockCode删除所有重复的条目,假设每个项目都有一个唯一的股票代码。我们还可以假设每个项目都有一个唯一的描述,并以这种方式删除项目。现在看看这段代码:

d1 = pd.DataFrame(df, columns = ['InvoiceNo', 'StockCode', 'Description'])[0:100]
d2 = pd.DataFrame(df, columns = ['Description', 'InvoiceDate', 'Quantity'])[0:100]

pd.merge(d1, d2).drop_duplicates(['StockCode'])

如果我们滚动到底部:

您将看到许多重复条目被删除。我们也可以通过DescriptionStockCodeDescription,它会产生同样的结果。

你会注意到指数到处都是。我们可以用reset_index()来修复。请看下面的代码:

d1 = pd.DataFrame(df, columns = ['InvoiceNo', 'StockCode', 'Description'])[0:100]
d2 = pd.DataFrame(df, columns = ['Description', 'InvoiceDate', 'Quantity'])[0:100]

d3 = pd.merge(d1, d2).drop_duplicates(['StockCode'])
d3.reset_index()

这就是它的样子:

显然,这不是你想要的。是的,它重置了索引,但是它添加了旧索引作为列。有一个简单的方法,那就是引入一个新的参数。现在,看看这段代码:

d3.reset_index(drop=True)

好多了。默认情况下,drop=False,所以如果不希望旧索引作为新列添加到数据中,那么记得设置drop=True

你可能还记得之前的.plot()功能。您可以使用它来帮助可视化数据帧,尤其是在数据帧很大的情况下。

这里有一个涉及单个列的例子:

d8 = pd.DataFrame(df, columns=['Quantity'])[0:100]
d8.plot()

这里,只选择前 100 个元素,以使图形不那么拥挤,并更好地说明示例。

现在,你将拥有:

现在,假设您希望显示多个列。请看以下内容:

d8 = pd.DataFrame(df, columns=['Quantity', 'UnitPrice'])[0:100]
d8.plot()

请记住,它不会绘制描述等定性数据列,只会绘制数量单价等可以绘制的东西。

摘要

在本章中,我们已经讨论了 Python 以及如何使用 Python 使用 Jupyter Notebook 执行数据分析。我们还研究了使用 Python 可以完成的几种不同的操作。

在下一章中,我们将研究另一种流行的分析语言 R,以及如何使用 R 来执行数据分析。

五、基于 R 和 Hadoop 的统计大数据计算

本章介绍了 R 以及如何使用 R 使用 Hadoop 对大数据进行统计计算。我们将看到从工作站上的开源 R 到像 Revolution R Enterprise 这样的并行化商业产品的各种选择,以及介于两者之间的许多其他选择。在这两个极端之间是一系列具有独特能力的选项:扩展数据、性能、功能和易用性。因此,正确的选择取决于您的数据大小、预算、技能、耐心和治理限制。

在本章中,我们将总结使用纯开源 r 的替代方案及其一些优势。此外,我们将描述通过结合开源和商业技术来实现更大规模、速度、稳定性和易开发性的选项。

简而言之,本章将涵盖以下主题:

  • 将 R 与 Hadoop 集成介绍
  • R 与 Hadoop 的集成方法
  • 用 R 进行数据分析

介绍

这一章是为了帮助目前对 Hadoop 不熟悉的 R 用户理解和选择要评估的解决方案而写的。和大多数开源的东西一样,首先考虑的当然是货币。不是一直都是吗?好消息是有多种免费的替代方案,并且在各种开源项目中正在开发额外的功能。

我们通常会看到使用完全开源的堆栈构建 R 和 Hadoop 集成的四个选项:

  • 在工作站上安装 R 并连接到 Hadoop 中的数据
  • 在共享服务器上安装 R 并连接到 Hadoop
  • 利用旋转打开
  • 使用 RMR2 在 MapReduce 内部执行 R

让我们在以下几节中详细介绍每个选项。

在工作站上安装 R 并连接到 Hadoop 中的数据

这种基线方法的最大优势是简单和成本。免费的。端到端免费。生活中还有什么?通过以开源形式提供的包 Revolution,包括rhdfsrhbase,R 用户可以直接从 Hadoop 中的hdfs文件系统和hbase数据库子系统中获取数据。这两个连接器都是 Revolution 创建和维护的 RHadoop 包的一部分,是首选。

还有其他选择。RHive 包直接从 R 执行 Hive 的 HQL(类似 SQL 的查询语言),并提供从 Hive 检索元数据的功能,如数据库名、表名、列名等。尤其是rhive包的优势在于,它的数据操作需要将一些工作下推到 Hadoop 中,避免了数据移动和大速度提升的并行操作。类似的下推也可以通过rhbase实现。然而,两者都不是特别丰富的环境,复杂的分析问题总是会暴露出一些能力差距。

除了有限的下推能力,R 最擅长处理从hdfshbasehive采集的适度数据;这样,当前的 R 用户就可以快速上手 Hadoop。

在共享服务器上安装 R 并连接到 Hadoop

一旦你厌倦了笔记本电脑上的内存障碍,显而易见的下一条路就是共享服务器。有了今天的技术,你只需花几千美元就可以装备一台强大的服务器,并在几个用户之间轻松共享。当使用具有 256 GB 或 512 GB 内存的窗口或 Linux 时,R 可以用来分析高达数百千兆字节的文件,尽管没有您希望的那么快。

与选项一一样,共享服务器上的 R 也可以利用rhbaserhive包的下推功能来实现并行性并避免数据移动。然而,与工作站一样,rhiverhbase的下推功能有限。

当然,虽然大量内存可以防止可怕的内存耗尽,但它对计算性能影响不大,并且依赖于分享在幼儿园学到的(或者可能没有学到的)技能。出于这些原因,可以认为共享服务器是工作站上 R 的一个很好的补充,但不是完全的替代品。

利用旋转打开

用 R 发行版Revolution R Open(RRO)取代 R 的 CRAN 下载,性能进一步提升。RRO 和 R 本身一样,是开源的,100% R,免费下载。它使用英特尔数学内核库加速数学计算,并且 100%兼容 CRAN 和其他存储库中的算法,如 BioConductor。不需要对 R 脚本进行任何更改,对于大量使用某些数学和线性代数原语的脚本,MKL 库提供的加速从可以忽略到一个数量级不等。如果你用语言做数学运算,你可以预期 RRO 可以让你的平均成绩翻倍。与选项一和选项二一样,RRO 可以与rhdfs等连接器一起使用,它可以通过rhbaserhive将工作连接并下推到 Hadoop 中。

使用 RMR2 在 MapReduce 内部执行 R

一旦你发现你的问题集太大,或者你的耐心在工作站或服务器上被耗尽,并且rhbaserhive下推的限制阻碍了进展,你就准备好在 Hadoop 内部运行 R 了。

开源 RHadoop 项目包括rhdfsrhbaseplyrmr,还有一个名为rmr2的包,可以让 R 用户使用 R 函数构建 Hadoop MapReduce 操作。使用映射器,R 函数被应用于组成一个hdfs文件、hbase表或其他数据集的所有数据块;结果可以发送到一个减速器,也是一个 R 函数,用于聚合或分析。所有的工作都是在 Hadoop 内部进行的,但内置于 R 中。让我们明确一点:将 R 函数应用于每个hdfs文件段是加速计算的好方法。但在很大程度上,避免移动数据才是真正强调性能的原因。为此,rmr2对 Hadoop 节点上的数据应用 R 函数,而不是将数据移动到 R 所在的位置。

虽然rmr2给出了本质上无限的能力,但作为数据科学家或统计学家,你的想法很快就会转向在大数据集上用 R 计算整个算法。以这种方式使用rmr2使 R 程序员的开发变得复杂,因为他或她必须编写所需算法的整个逻辑或修改现有的 CRAN 算法。然后,他/她必须验证算法是否准确,是否反映了预期的数学结果,并为诸如数据缺失等各种情况编写代码。

rmr2需要你自己编码来管理并行化。对于数据转换操作、聚合等来说,这可能是微不足道的,如果您试图在大型数据集上训练预测模型或构建分类器,这可能是相当繁琐的。虽然rmr2可能比其他方法更繁琐,但这并不是站不住脚的,大多数 R 程序员会发现rmr2比求助于基于 Java 的 Hadoop mappers 和 reducers 开发要容易得多。虽然有些乏味,但它:

  • 是完全开源的
  • 有助于并行化计算以处理更大的数据集
  • 跳过痛苦的数据移动
  • 被广泛使用,所以你会发现有帮助
  • 是免费的

rmr2不是该类别中的唯一选项;一个类似的名为rhipe的包也在那里,并提供类似的功能。rhipewww.rhipe.com/download-co…有描述,可从 GitHub 下载。

纯开源选项的总结和展望

在 Hadoop 中使用 R 的基于开源的选项的范围正在扩大。例如,Apache Spark 社区正在通过可预见的名称 SparkR 快速改进 R 集成。如今,SparkR 提供了从 R 到 Spark 的访问,就像今天rmr2rhipe为 Hadoop MapReduce 所做的那样。

我们预计,在未来,SparkR 团队将增加对 Spark 的 MLlib 机器学习算法库的支持,直接从 r 提供执行。可用性日期尚未广泛发布。

也许最令人兴奋的观察是,R 已经成为平台厂商的桌赌注。我们在 Cloudera、Hortonworks、MapR 和其他公司的合作伙伴,以及数据库供应商和其他公司,都敏锐地意识到了 R 在庞大且不断增长的数据科学社区中的主导地位,以及 R 作为从构建在 Hadoop 之上的新兴数据存储库中获取见解和价值的一种手段的重要性。

在随后的文章中,我将回顾通过将范围从仅开源解决方案扩展到像 Hadoop 的 Revolution R Enterprise 这样的解决方案,为 R 用户创造更高性能、简单性、可移植性和可扩展性的选项。

r 是一个惊人的数据科学编程工具,可以对模型进行统计数据分析,并将分析结果转换成彩色图形。毫无疑问,R 是统计学家、数据科学家、数据分析师和数据架构师最喜欢的编程工具,但在处理大型数据集时,它却有所欠缺。R 编程语言的一个主要缺点是所有对象都被加载到单机的主内存中。以千兆字节为单位的大型数据集无法加载到内存中;这也是 Hadoop 与 R 集成是理想解决方案的时候。为了适应 R 编程语言的内存、单机限制,数据科学家不得不将他们的数据分析限制在大数据集的数据样本上。当处理大数据时,R 编程语言的这种局限性是一个主要障碍。由于 R 的可伸缩性不是很高,核心 R 引擎只能处理有限的数据。

相反,像 Hadoop 这样的分布式处理框架对于大型数据集(petabyte 范围)上的复杂操作和任务是可扩展的,但不具备强大的统计分析能力。由于 Hadoop 是一个流行的大数据处理框架,将 R 与 Hadoop 集成是下一个合乎逻辑的步骤。在 Hadoop 上使用 R 将提供一个高度可扩展的数据分析平台,该平台可以根据数据集的大小进行扩展。将 Hadoop 与 R 集成可以让数据科学家在大型数据集上并行运行 R,因为 R 语言中的数据科学库都不能在大于内存的数据集上工作。借助 R 和 Hadoop 的大数据分析与商品硬件集群在垂直扩展方面提供的成本价值回报相竞争。

R 与 Hadoop 的集成方法

使用 Hadoop 的数据分析师或数据科学家可能有他们用于数据处理的 R 包或 R 脚本。为了在 Hadoop 中使用这些 R 脚本或 R 包,他们需要用 Java 编程语言或任何其他实现 Hadoop MapReduce 的语言重写这些 R 脚本。这是一个繁重的过程,可能会导致不必要的错误。为了将 Hadoop 与 R 编程语言集成,我们需要使用一个已经为 R 编写的软件,数据存储在 Hadoop 的分布式存储中。有许多使用 R 语言来执行大型计算的解决方案,但所有这些解决方案都要求在将数据分发到计算节点之前将其加载到内存中。这不是大型数据集的理想解决方案。以下是一些常用的将 Hadoop 与 R 集成的方法,以最大限度地利用 R 对大型数据集的分析能力。

RHADOOP–在工作站上安装 R 并连接到 HADOOP 中的数据

最常用的将 R 编程语言与 Hadoop 集成的开源分析解决方案是 RHadoop 。由 Revolution analytics 开发的 RHadoop 允许用户直接从 HBase 数据库子系统和 HDFS 文件系统中摄取数据。RHadoop 包是在 Hadoop 上使用 R 的最佳解决方案,因为它简单且具有成本优势。RHadoop 是五个不同包的集合,允许 Hadoop 用户使用 R 编程语言管理和分析数据。RHadoop 包与开源 Hadoop 兼容,也与流行的 Hadoop 发行版 Cloudera、Hortonworks 和 MapR 兼容:

  • rhbase:rhbase包使用一个节俭服务器为 R 内的 HBase 提供数据库管理功能。此包需要安装在将运行 R 客户端的节点上。使用rhbase,数据工程师和数据科学家可以从 r
  • rhdfs:rhdfs包为 R 程序员提供了与 Hadoop 分布式文件系统的连接,以便他们读取、写入或修改存储在 Hadoop HDFS 中的数据。
  • plyrmr:这个包支持对 Hadoop 管理的大数据集进行数据操作。plyrmr ( plyr用于 MapReduce)提供流行包中存在的数据操作操作,如reshape2plyr。这个包依赖 Hadoop MapReduce 来执行操作,但抽象了大部分 MapReduce 细节。
  • ravro:这个包允许用户从本地和 HDFS 文件系统读写 Avro 文件。
  • rmr2(在 Hadoop MapReduce 内部执行 R):使用这个包,R 程序员可以对一个 Hadoop 集群中存储的数据进行统计分析。使用rmr2将 R 与 Hadoop 集成可能是一个麻烦的过程,但是许多 R 程序员发现使用rmr2比依赖基于 Java 的 Hadoop 映射器和减压器要容易得多。rmr2可能有点乏味,但它消除了数据移动,并有助于处理大型数据集的并行计算。

RHIPE–在 Hadoop MapReduce 中执行 R

R 和 Hadoop 集成编程环境(RHIPE) 是一个 R 库,允许用户在 R 编程语言内运行 Hadoop MapReduce 作业。R 程序员只需要编写 R Map 和 R Reduce 函数,RHIPE 库会进行传递,调用相应的 Hadoop Map 和 Hadoop Reduce 任务。RHIPE 使用协议缓冲编码方案来传输映射和减少输入。与其他并行 R 包相比,使用 RHIPE 的优势在于它与 Hadoop 集成良好,并提供了一种在机器集群中使用 HDFS 的数据分发方案,该方案提供了容错能力并优化了处理器的使用。

R 和 Hadoop 流

Hadoop Streaming API 允许用户使用任何可执行脚本运行 Hadoop MapReduce 作业,该脚本从标准输入中读取数据,并将数据作为映射器或缩减器写入标准输出。因此,Hadoop 流应用编程接口可以在映射或缩减阶段与 R 编程脚本一起使用。这种集成 R 和 Hadoop 的方法不需要任何客户端集成,因为流作业是通过 Hadoop 命令行启动的。提交的 MapReduce 作业将通过 UNIX 标准流和序列化进行数据转换,以确保 Java 投诉输入到 Hadoop,而与程序员提供的输入脚本的语言无关。

你认为 R 与 Hadoop 集成的最佳方式是什么?

RHIVE–在工作站上安装 R 并连接到 Hadoop 中的数据

如果您希望从 R 接口启动 Hive 查询,那么 r Hive 是一个定位包,具有从 Apache Hive 中检索元数据(如数据库名、列名和表名)的功能。RHIVE 通过用 R 语言函数扩展 HiveQL,为存储在 Hadoop 中的数据提供了丰富的 R 编程语言可用的统计库和算法。RHIVE 函数允许用户将 R 统计学习模型应用于已经使用 Apache Hive 编目的 Hadoop 集群中存储的数据。使用 RHIVE 进行 Hadoop R 集成的优势在于,由于数据操作被下推到 Hadoop 中,因此可以并行化操作,避免数据移动。

ORCH–面向 Hadoop 的甲骨文连接器

ORCH 可用于非 Oracle Hadoop 集群或任何其他 Oracle 大数据设备。Mappers 和 Reduce 是用 R 编写的,MapReduce 作业是通过高级接口从 R 环境中执行的。有了 ORCH for R Hadoop 集成,R 程序员不必学习一门新的编程语言(如 Java)就能了解 Hadoop 环境的细节,如 Hadoop 集群硬件或软件。ORCH 连接器还允许用户通过相同的函数调用在本地测试 MapReduce 程序的能力,这要在它们部署到 Hadoop 集群之前很久。

使用 R 和 Hadoop 执行大数据分析的开源选项的数量在不断增加,但对于简单的 Hadoop MapReduce 作业,R 和 Hadoop Streaming 仍然被证明是最佳解决方案。R 和 Hadoop 的结合是从事大数据工作的专业人员的必备工具包,可结合您所需的性能、可扩展性和灵活性创建快速预测分析。

大多数 Hadoop 用户声称,使用 R 的优势在于其用于统计和数据可视化的详尽的数据科学库列表。然而,R 中的数据科学库本质上是非分布式的,这使得数据检索成为一件耗时的事情。这是 R 编程语言的一个内在限制,但是如果我们忽略它,那么 R 和 Hadoop 一起可以让大数据分析成为一种享受!

数据分析

r 允许我们进行各种各样的数据分析。我们用 Python 中的pandas所做的一切,我们也可以用 R 来做。

看看下面的代码:

df = read.csv(file=file.choose(), header=T, fill=T, sep=",", stringsAsFactors=F)

file.choose()表示将有一个新窗口,允许您选择要打开的数据文件。header=T表示会读取表头。fill=T表示它将为任何未定义或缺失的数据值填写 NaN。最后,sep=","表示知道如何区分.csv文件中不同的数据值。在这种情况下,它们都用逗号隔开。stringsAsFactors告诉它把所有的字符串值都当成字符串,而不是因子。这允许我们稍后替换数据中的值。

现在,你应该看到这个:

Figure: Screenshot of output you will obtain

进入。如果您在 Windows 上,应该会看到类似这样的内容:

无论操作系统如何,您都应该会看到一个窗口,允许您选择文件。接下来,您应该会看到:

如果你向右看,你会看到一个名为df的新字段。如果你点击它,你可以看到它的内容:

现在,我们已经创建了一个数据框架,我们可以开始一些分析。

我们可以获得一些关于行数和列数的信息,以及数据框的长度和列名。请看下面几行代码及其各自的输出:

> is.data.frame(df)
[1] TRUE
> ncol(df)
[1] 8
> length(df)
[1] 8
> nrow(df)
[1] 27080
> names(df)
[1] "InvoiceNo" "StockCode" "Description" "Quantity" "InvoiceDate"
    "UnitPrice" "CustomerID" "Country"
> colnames(df)
[1] "InvoiceNo" "StockCode" "Description" "Quantity" "InvoiceDate"
    "UnitPrice" "CustomerID" "Country"

现在,我们可以继续创建数据子集。看看这段代码:

d1 = df[1:3]

这就是它的结果:

所以基本上,我们选择了第 1、2、3 列作为d1的数据集。除了我们想要的列之外,我们还可以选择我们想要的行。让我们重新定义d1:

d1 = df[1:10, c(1:3)]

我们还可以访问数据框的单个列。看看这个:

v1 = df[[3]]

这会将整列数据分配给v1。现在,让我们进入v1的前五个要素:

v1[1:5]

我们也可以这样做:

v2 = df$Description
v2[1:5]

假设我们知道一个特定的数据值,我们甚至可以访问每个单独的行。这里,我们使用股票代码:

d1[d1$StockCode == "85123A", ]

我们可以访问我们想要的特定行:

d1 = df[1:10, c(1:8)]
d1[2, c(1:8)]

类似于 Python 中的.head()函数,r 中有一个head()函数,看看这段代码:

head(df)

我们可以添加另一个参数来选择要显示的行数。假设我们要显示前10行。下面是代码:

head(df, 10)

我们可以有一个负数作为第二个参数。请看以下内容:

head(d1, -2)

同样,我们可以使用tail()显示最后的 n 行。请看以下内容:

tail(d1, 4)

我们也可以有一个负数作为第二个参数,就像head()一样。看看这一行代码:

tail(d1, -2)

这会显示 nrow(d1) + n 行,其中 n 是传递到tail()函数的参数:

我们可以对一个栏目做一些基本的统计分析。但是,我们必须先转换数据。我们可以做min()max()mean()等等。看看这个:

min(as.numeric(df$UnitPrice))
[1] 0
min(df$UnitPrice)
[1] 0

as.numeric()表示任何字符串形式的数据值都将被转换为数字。在这种情况下,它们都不是字符串值,否则您会在0中看到min(df$UnitPrice)结果:

max(df$UnitPrice)
[1] 16888.02
mean(df$UnitPrice)
[1] 5.857586
median(df$UnitPrice)
[1] 2.51
quantile(df$UnitPrice)

我们可以在这里添加另一个参数来自定义我们想要的百分比值:

quantile(df$UnitPrice, c(0, .1, .5, .9)

sd(df$UnitPrice)

这告诉我们df$UnitPrice的标准差。我们还可以找到方差:

var(df$UnitPrice)

range(df$UnitPrice)

我们还可以得到一个五位数的总结,它告诉我们最小值、第一个分位数、中间值(也是 50%标记)、第三个分位数(75%标记)和最大值:

fivenum(df$UnitPrice)

我们还可以绘制一列选择。看看这个:

plot(df$UnitPrice)

我们可以有不同类型的情节。我们可以引入另一个参数来指定我们想要的绘图类型。请看下面几行代码及其结果图:

plot(df$UnitPrice, type="p")

如你所见,它和我们之前看到的图是一样的。但是,图表有点拥挤,所以我们使用一个较小的范围:

d1 = df[0:30, c(1:8)]
plot(d1$UnitPrice)

让我们更简单地重新定义d1使其只有UnitPrice列:

d1 = d1$UnitPrice
plot(d1, type="p")

该图应该与前一个图相同。

现在,让我们继续:

plot(d1, type="l")

这是d1的线图:

plot(d1, type="b")

这是d1的组合线图和点图。然而,它们并不相互重叠:

plot(d1, type="c")

该图仅是我们之前看到的type="b"组合图中的线条图:

plot(d1, type="o")

这是d1的一个过奖图。这意味着线图和点图相互重叠:

plot(d1, type="h")

这是d1的直方图:

plot(d1, type="s")

这是一个阶梯图:

plot(d1, type="S")

两个图的区别在于第一步图,其中type="s"是图先水平后垂直的地方。第二步图有type="S",先垂直移动后水平移动。通过看图表可以看出这种差异。

我们还可以使用其他参数,例如:

#Note: these are parameters, not individual lines of code.

#The title of the graph
main="Title" 

#Subtitle for the graph
sub="title"

#Label for the x-axis
xlab="X Axis"

#Label for the y-axis
ylab="Y Axis"

#The aspect ratio between y and x.
asp=1

现在举个例子:

plot(d1, type="h", main="Graph of Unit Prices vs Index", sub ="First 30 Rows", xlab = "Row Index", ylab="Prices", asp=1.4)

要将两个不同的数据帧添加在一起,我们使用rbind()

请看下面的代码:

d2 = df[0:10, c(1:8)]
d3 = df[21:30, c(1:8)]
d4 = rbind(d2, d3)

这是d2:

这是d3:

现在,这是d4:

需要注意的一点是,所有传入rbind()的数据帧必须有相同的列。顺序不重要。

我们也可以合并两个数据帧。

看看这段代码:

d2 = df[0:11, c("InvoiceNo", "StockCode", "Description")]
d3 = df[11:20, c("StockCode", "Description", "Quantity")]
d4 = merge(d2, d3)

这是d2:

这是d3:

这是d4:

所以默认情况下,merge()使用内部连接。

现在,让我们看看外部连接:

d4 = merge(d2, d3, all=T)

这是左外连接:

d4 = merge(d2, d3, all.x=T)

这是右外连接:

d4 = merge(d2, d3, all.y=T)

最后,交叉连接:

d4 = merge(d2, d3, by=NULL)

就像在熊猫中一样,我们可以用by=来指定两个数据项之间的一个.x.y,而不是_x_y。请看以下内容:

d4 = merge(d2, d3, by="StockCode", all=T)

这是StockCode列上的外部连接。

这就是结果:

我们可以随时记录下所有的命令,以防万一。执行以下代码保存命令日志:

savehistory(file="logname.Rhistory")

要加载历史记录:

loadhistory(file="logname.Rhistory")

如果您想查看您的历史记录,只需执行以下操作:

history()

我们可以检查数据,看看是否有空白数据。看看代码:

colSums(is.na(df))

现在,让我们再重复一遍。回想一下,当我们合并两个数据帧时,有些数据值是 NaN:

d2 = df[0:11, c("InvoiceNo", "StockCode", "Description")]
d3 = df[11:20, c("StockCode", "Description", "Quantity")]

现在,让我们对它们进行外部合并:

d4 = merge(d2, d3, all=T)

现在,让我们试试这一行代码:

colSums(is.na(d4))

我们也可以替换数据中的值。

现在,假设您想将价格大于 3 的每件商品的描述更改为"Miscellaneous"。看看这个示例代码:

d1 = df[0:30, c(1:8)]

现在,看看这个:

d1[d1$UnitPrice > 3, "Description"] <- "Miscellaneous"

现在我们看到单价大于三的东西都有"Miscellaneous"的描述。

我们可以使用除>以外的其他运算符,也可以替换其他列中的值。

这里还有一个例子。

假设发票号为536365的每一项实际上都来自United States

现在,由于它们都共享相同的发票号和发票日期,我们可以使用其中任何一个来选择所需的行:

d1[d1$InvoiceNo == 536365, "Country"] = "United States"

注意这次我们用=代替了<-。在这种情况下,它们都在分配一些东西,所以任何一个都可以使用。

摘要

在本章中,我们讨论了如何使用 R 来执行数据分析。我们还描述了集成 R 和 Hadoop 的不同选项。

在下一章中,我们将了解 Apache Spark,以及如何基于批处理模型将其用于大数据分析。

六、Apache Spark 批处理分析

在本章中,您将了解 Apache Spark 以及如何将其用于基于批处理模型的大数据分析。Spark SQL 是 Spark Core 之上的一个组件,可用于查询结构化数据。它正在成为事实上的工具,取代 Hive 成为 Hadoop 上批处理分析的选择。

此外,您将学习如何使用 Spark 来分析结构化数据(非结构化数据,如包含任意文本的文档,或必须转换为结构化形式的其他格式)。我们将在这里看到数据框架/数据集是如何成为基石的,以及 SparkSQL 的 API 如何使查询结构化数据变得简单而健壮。

我们还将介绍数据集,并了解数据集、数据框架和关系数据库之间的区别。简而言之,本章将涵盖以下主题:

  • 迷你图 SQL 和数据框
  • 数据框架和 SQL 应用编程接口
  • 数据框模式
  • 数据集和编码器
  • 加载和保存数据
  • 聚集
  • 连接

迷你图 SQL 和数据框

在 Apache Spark 之前,Apache Hive 是任何人想要对大量数据运行类似于 SQL 的查询时的首选技术。Apache Hive 本质上是将一个 SQL 查询翻译成 MapReduce,就像逻辑自动使对大数据执行多种分析变得非常容易,而无需实际学习用 Java 和 Scala 编写复杂的代码。

随着 Apache Spark 的出现,我们如何在大数据规模上执行分析发生了范式转变。Spark SQL 在 Apache Spark 的分布式计算能力之上提供了一个类似 SQL 的层,使用起来相当简单。事实上,Spark SQL 可以用作在线分析处理数据库。Spark SQL 的工作原理是将类似 SQL 的语句解析成抽象语法树 ( AST ),随后将该计划转换为逻辑计划,然后将逻辑计划优化为可执行的物理计划,如下图所示:

最终的执行使用底层的数据框架应用编程接口,通过简单地使用类似于 SQL 的接口,而不是学习所有的内部,任何人都可以非常容易地使用数据框架应用编程接口。由于本书深入探讨了各种应用编程接口的技术细节,我们将主要介绍数据框架应用编程接口,在一些地方展示了 Spark SQL 应用编程接口,以对比使用这些应用编程接口的不同方式。因此,数据框架应用编程接口是 Spark SQL 下面的底层。在本章中,我们将向您展示如何使用各种技术创建数据框,包括 SQL 查询和对数据框执行操作。

数据框架是对弹性分布式数据集 ( RDD )的抽象,处理使用催化剂优化器优化的更高级功能,并且通过钨计划也是高性能的。

自成立以来,钨项目一直是 Spark 执行引擎的最大变化。它的主要焦点在于提高 Spark 应用的 CPU 和内存效率。该项目包括三项举措:

  • 内存管理和二进制处理
  • 缓存感知计算
  • 代码生成

For more information, you can check out databricks.com/blog/2015/0….

您可以将数据集视为 RDD 上的一个高效表,它具有高度优化的数据二进制表示。二进制表示是使用编码器实现的,编码器将各种对象序列化为二进制结构,性能比 RDD 表示好得多。因为数据框在内部使用 RDD,所以数据框/数据集也像 RDD 一样分布,因此也是一个分布式数据集。显然,这也意味着数据集是不可变的。

以下是数据的二进制表示的说明:

数据集是在 Spark 1.6 中添加的,并提供了在数据框之上进行强类型化的优势。事实上,由于 Spark 2.0,数据框只是数据集的别名。

spark.apache.org/sql/ defines the DataFrame type as a Dataset[Row], which means that most of the APIs will work well with both dataset and DataFrame.type DataFrame = Dataset[Row].

数据框在概念上类似于关系数据库中的表。因此,数据帧包含多行数据,每行由几列组成。我们需要记住的第一件事是,就像关系数据库一样,数据帧也是不可变的。数据帧不可变的这一特性意味着每次转换或操作都会创建一个新的数据帧。

让我们从更多地研究数据帧以及它们与关系数据库有什么不同开始。如前所述,RDDs 代表了 Apache Spark 中数据操作的低级 API。数据框架是在关系数据库之上创建的,以抽象关系数据库的低级内部工作方式,并公开更易于使用的高级应用编程接口,并提供大量现成的功能。DataFrame 是按照 Python pandas 包、R 语言、Julia 语言等类似概念创建的。

正如我们之前提到的,数据框架将 SQL 代码和特定于域的语言表达式转换成优化的执行计划,在 Spark Core APIs 之上运行,以便 SQL 语句执行各种各样的操作。数据框支持许多不同类型的输入数据源和许多类型的操作。这包括所有类型的 SQL 操作,如连接、分组依据、聚合和窗口函数。

Spark SQL 也非常类似于 Hive 查询语言,由于 Spark 为 Apache Hive 提供了一个自然的适配器,所以一直在 Apache Hive 工作的用户可以轻松地将自己的知识转移并应用到 Spark SQL 中,从而最大限度地减少转换时间。如前所述,数据帧本质上依赖于表的概念。

该表的操作方式与 Apache Hive 的工作方式非常相似。事实上,Apache Spark 中对表的许多操作类似于 Apache Hive 处理表和对表进行操作的方式。一旦您有了一个作为数据框的表,数据框就可以注册为一个表,并且您可以使用 Spark SQL 语句代替数据框 API 来操作数据。

数据框架取决于催化剂优化器和钨性能的提高,所以让我们简单地研究一下催化剂优化器是如何工作的。catalyst 优化器根据输入的 SQL 创建一个解析的逻辑计划,然后通过查看 SQL 语句中使用的各种属性和列来分析逻辑计划。一旦分析的逻辑计划被创建,catalyst 优化器通过组合几个操作并重新安排逻辑以获得更好的性能来进一步尝试优化计划。

In order to understand the catalyst optimizer, think about it as a common sense logic optimizer which can reorder operations such as filters and transformations, sometimes grouping several operations into one so as to minimize the amount of data that is shuffled across the worker nodes. For example, the catalyst optimizer may decide to broadcast the smaller datasets when performing joint operations between different datasets. Use explain to look at the execution plan of any DataFrame. The catalyst optimizer also computes statistics of the DataFrames columns and partitions improving the speed of execution.

例如,如果数据分区上有转换和过滤器,那么我们过滤数据和应用转换的顺序对操作的整体性能非常重要。作为所有优化的结果,生成优化的逻辑计划,然后将其转换为物理计划。

显然,几个物理计划可以执行相同的 SQL 语句并生成相同的结果。成本优化逻辑基于成本优化和估计来确定和挑选好的物理计划。与之前的版本(如 Spark 1.6 或更早版本)相比,Spark 2.x 提供了显著的性能提升,钨性能的提升是其背后的另一个关键因素。

钨实现了对内存管理和其他性能改进的全面检修。最重要的内存管理改进使用对象的二进制编码,并在堆外和堆内内存中引用它们。因此,通过使用二进制编码机制对所有对象进行编码,钨允许使用办公室堆内存。二进制编码对象占用的内存少得多。

项目钨也提高洗牌性能。数据通常通过DataFrameReader加载到数据框中,数据通过DataFrameWriter从数据框中保存。

数据框架应用编程接口和 SQL 应用编程接口

数据框可以通过几种方式创建;其中一些如下:

  • 执行 SQL 查询,加载外部数据,如 Parquet、JSON、CSV、Text、Hive、JDBC 等
  • 将关系数据库转换为数据帧
  • 加载一个 CSV 文件

我们将在这里看一下statesPopulation.csv,然后我们将它作为数据帧加载。

CSV 包含 2010 年至 2016 年美国各州人口的以下格式:

| 状态 | | 人口 | | 亚拉巴马州 | Two thousand and ten | 47,85,492 | | 阿拉斯加 | Two thousand and ten | Seven hundred and fourteen thousand and thirty-one | | 亚利桑那州 | Two thousand and ten | 64,08,312 | | 阿肯色州 | Two thousand and ten | Two million nine hundred and twenty-one thousand nine hundred and ninety-five | | 加利福尼亚 | Two thousand and ten | Thirty-seven million three hundred and thirty-two thousand six hundred and eighty-five |

由于这个 CSV 有一个头,我们可以使用它来快速加载到带有隐式模式检测的数据帧中:

scala> val statesDF = spark.read.option("header",
"true").option("inferschema", "true").option("sep",
",").csv("statesPopulation.csv")
statesDF: org.apache.spark.sql.DataFrame = [State: string, Year: int ... 1
more field]

一旦我们加载了数据帧,就可以检查它的模式:

scala> statesDF.printSchema
root
|-- State: string (nullable = true)
|-- Year: integer (nullable = true)
|-- Population: integer (nullable = true)

option("header", "true").option("inferschema", "true").option("sep", ",") tells Spark that the CSV has a header; a comma separator is used to separate the fields/columns and also that schema can be inferred implicitly.

DataFrame 的工作原理是解析逻辑计划、分析逻辑计划、优化计划,然后最终执行物理执行计划。

使用数据框上的解释显示执行计划:

scala> statesDF.explain(true)
== Parsed Logical Plan ==
Relation[State#0,Year#1,Population#2] csv
== Analyzed Logical Plan ==
State: string, Year: int, Population: int
Relation[State#0,Year#1,Population#2] csv
== Optimized Logical Plan ==
Relation[State#0,Year#1,Population#2] csv
== Physical Plan ==
*FileScan csv [State#0,Year#1,Population#2] Batched: false, Format: CSV,
Location: InMemoryFileIndex[file:/Users/salla/states.csv],
PartitionFilters: [], PushedFilters: [], ReadSchema:
struct<State:string,Year:int,Population:int>

数据框也可以注册为表名(如下所示),这样就可以像关系数据库一样键入 SQL 语句:

scala> statesDF.createOrReplaceTempView("states")

一旦我们将数据框作为结构化数据框或表,我们就可以运行命令对数据进行操作:

scala> statesDF.show(5)
scala> spark.sql("select * from states limit 5").show
+----------+----+----------+
| State|Year|Population|
+----------+----+----------+
| Alabama|2010| 4785492|
| Alaska|2010| 714031|
| Arizona|2010| 6408312|
| Arkansas|2010| 2921995|
|California|2010| 37332685|
+----------+----+----------+

如果您在前面的代码中看到,我们已经编写了一个类似于 SQL 的语句,并使用spark.sql API 执行了它。

Note that the Spark SQL is simply converted to the DataFrame API for execution and the SQL is only a DSL for ease of use.

使用数据框上的sort操作,可以按任意列对数据框中的行进行排序。我们看到使用Population列进行降序排序的效果如下。这些行由Population按降序排列:

scala> statesDF.sort(col("Population").desc).show(5)
scala> spark.sql("select * from states order by Population desc limit
5").show
+----------+----+----------+
| State|Year|Population|
 +----------+----+----------+
|California|2016| 39250017|
|California|2015| 38993940|
|California|2014| 38680810|
|California|2013| 38335203|
|California|2012| 38011074|
+----------+----+----------+

使用groupBy我们可以按任意列对数据帧进行分组。以下是通过State对行进行分组,然后对每个State累加Population计数的代码:

scala> statesDF.groupBy("State").sum("Population").show(5)
scala> spark.sql("select State, sum(Population) 
from states group by State
limit 5").show
+---------+---------------+
| State|sum(Population)|
+---------+---------------+
| Utah| 20333580|
| Hawaii| 9810173|
|Minnesota| 37914011|
| Ohio| 81020539|
| Arkansas| 20703849|
+---------+---------------+

使用agg操作,您可以对数据框的列执行许多不同的操作,例如查找列的minmaxavg。您还可以同时执行该操作并重命名该列,以适合您的用例:

scala>
statesDF.groupBy("State").agg(sum("Population").alias("Total")).show(5)
scala> spark.sql("select State, sum(Population) as Total from states group
by State limit 5").show
+---------+--------+
| State| Total|
+---------+--------+
| Utah|20333580|
| Hawaii| 9810173|
|Minnesota|37914011|
| Ohio|81020539|
| Arkansas|20703849|
+---------+--------+

自然,逻辑越复杂,执行计划也就越复杂。让我们看看groupByagg API 调用的前一个操作的计划,以便更好地理解幕后到底发生了什么。以下是显示group by条款执行计划和每个State人口总和的代码:

scala>
statesDF.groupBy("State").agg(sum("Population").alias("Total")).explain(true)
== Parsed Logical Plan ==
'Aggregate [State#0], [State#0, sum('Population) AS Total#31886]
+- Relation[State#0,Year#1,Population#2] csv
== Analyzed Logical Plan ==
State: string, Total: bigint
Aggregate [State#0], [State#0, sum(cast(Population#2 as bigint)) AS
Total#31886L]
+- Relation[State#0,Year#1,Population#2] csv
== Optimized Logical Plan ==
Aggregate [State#0], [State#0, sum(cast(Population#2 as bigint)) AS
Total#31886L]
+- Project [State#0, Population#2]
+- Relation[State#0,Year#1,Population#2] csv
== Physical Plan ==
*HashAggregate(keys=[State#0], functions=[sum(cast(Population#2 as
bigint))], output=[State#0, Total#31886L])
+- Exchange hashpartitioning(State#0, 200)
+- *HashAggregate(keys=[State#0], functions=[partial_sum(cast(Population#2
as bigint))], output=[State#0, sum#31892L])
+- *FileScan csv [State#0,Population#2] Batched: false, Format: CSV,
Location: InMemoryFileIndex[file:/Users/salla/states.csv],
PartitionFilters: [], PushedFilters: [], ReadSchema:
struct<State:string,Population:int>

数据框操作可以很好地链接在一起,这样执行就可以利用成本优化(钨性能改进和催化剂优化器一起工作)。我们还可以在一条语句中将操作链接在一起,如下所示,其中我们不仅按State列对数据进行分组,然后对Population值求和,还按求和列对数据帧进行排序:

scala>
statesDF.groupBy("State").agg(sum("Population").alias("Total")).sort(col("Total").desc).show(5)
scala> spark.sql("select State, sum(Population) as Total from states group
by State order by Total desc limit 5").show
+----------+---------+
| State| Total|
+----------+---------+
|California|268280590
| Texas|185672865|
| Florida|137618322|
| New York|137409471|
| Illinois| 89960023|
+----------+---------+

前面的链式操作由多个转换和动作组成, 可以使用下图可视化:

也可以同时创建多个聚合,如下所示:

scala> statesDF.groupBy("State").agg(
min("Population").alias("minTotal"),
max("Population").alias("maxTotal"),
avg("Population").alias("avgTotal"))
.sort(col("minTotal").desc).show(5)
scala> spark.sql("select State, min(Population) as minTotal,
max(Population) as maxTotal, avg(Population) as avgTotal from states group
by State order by minTotal desc limit 5").show
+----------+--------+--------+--------------------+
| State|minTotal|maxTotal| avgTotal|
+----------+--------+--------+--------------------+
|California|37332685|39250017|3.8325798571428575E7|
| Texas|25244310|27862596| 2.6524695E7|
| New York|19402640|19747183| 1.962992442857143E7|
| Florida|18849098|20612439|1.9659760285714287E7|
| Illinois|12801539|12879505|1.2851431857142856E7|
+----------+--------+--------+--------------------+

中心

为了创建更适合执行多个汇总和聚合的不同视图,转换表的最佳方法之一是旋转。我们可以通过取一个列的值并使每个值成为一个实际的列来实现这一点。

让我们借助一个例子更好地理解这一点。我们将按Year旋转数据帧的行,并检查结果。我们现在获得的结果描述了来自Year列的值,每个值都形成了一个新列。这样做的最终结果是,我们可以使用Year创建的年度列进行汇总和汇总,而不仅仅是查看年度列:

scala> statesDF.groupBy("State").pivot("Year").sum("Population").show(5)
+---------+--------+--------+--------+--------+--------+--------+--------+
| State| 2010| 2011| 2012| 2013| 2014| 2015| 2016|
+---------+--------+--------+--------+--------+--------+--------+--------+
| Utah| 2775326| 2816124| 2855782| 2902663| 2941836| 2990632| 3051217|
| Hawaii| 1363945| 1377864| 1391820| 1406481| 1416349| 1425157| 1428557|
|Minnesota| 5311147| 5348562| 5380285| 5418521| 5453109| 5482435| 5519952|
| Ohio|11540983|11544824|11550839|11570022|11594408|11605090|11614373|
| Arkansas| 2921995| 2939493| 2950685| 2958663| 2966912| 2977853| 2988248|
+---------+--------+--------+--------+--------+--------+--------+--------+

过滤

数据框也支持筛选,可以通过筛选数据框行来生成新的数据框。Filter实现了一个非常重要的数据转换,将数据框架缩小到我们的用例。让我们看一下数据帧过滤的执行计划,只考虑California的状态:

scala> statesDF.filter("State == 'California'").explain(true)
== Parsed Logical Plan ==
'Filter ('State = California)
+- Relation[State#0,Year#1,Population#2] csv
== Analyzed Logical Plan ==
State: string, Year: int, Population: int
Filter (State#0 = California)
+- Relation[State#0,Year#1,Population#2] csv
== Optimized Logical Plan ==
Filter (isnotnull(State#0) && (State#0 = California))
+- Relation[State#0,Year#1,Population#2] csv
== Physical Plan ==
*Project [State#0, Year#1, Population#2]
+- *Filter (isnotnull(State#0) && (State#0 = California))
+- *FileScan csv [State#0,Year#1,Population#2] Batched: false, Format:
CSV, Location: InMemoryFileIndex[file:/Users/salla/states.csv],
PartitionFilters: [], PushedFilters: [IsNotNull(State),
EqualTo(State,California)], ReadSchema:
struct<State:string,Year:int,Population:int>

现在我们已经看到了执行计划,现在让我们执行filter命令如下:

scala> statesDF.filter("State == 'California'").show
+----------+----+----------+
| State|Year|Population|
+----------+----+----------+
|California|2010| 37332685|
|California|2011| 37676861|
|California|2012| 38011074|
|California|2013| 38335203|
|California|2014| 38680810|
|California|2015| 38993940|
|California|2016| 39250017|
+----------+----+----------+

用户定义的函数

用户定义函数 ( UDFs )定义了新的基于列的函数,扩展了 Spark SQL 的功能。当 Spark 中的内置函数无法处理我们的需求时,创建 UDF 会有所帮助。

udf() internally calls a case class UserDefinedFunction which in turn calls ScalaUDF internally.

让我们来看一个简单地将State列值转换为大写的 UDF 的例子。首先,我们在 Scala 中创建我们需要的函数,如以下代码片段所示:

import org.apache.spark.sql.functions._
scala> val toUpper: String => String = _.toUpperCase
toUpper: String => String = <function1>

然后我们必须将创建的函数封装在udf中,以创建 UDF:

scala> val toUpperUDF = udf(toUpper)
toUpperUDF: org.apache.spark.sql.expressions.UserDefinedFunction =
UserDefinedFunction(<function1>,StringType,Some(List(StringType)))

现在我们已经创建了udf,我们可以使用它将State列转换为大写:

scala> statesDF.withColumn("StateUpperCase",
toUpperUDF(col("State"))).show(5)
+----------+----+----------+--------------+
| State|Year|Population|StateUpperCase|
+----------+----+----------+--------------+
| Alabama|2010| 4785492| ALABAMA|
| Alaska|2010| 714031| ALASKA|
| Arizona|2010| 6408312| ARIZONA|
| Arkansas|2010| 2921995| ARKANSAS|
|California|2010| 37332685| CALIFORNIA|
+----------+----+----------+--------------+

模式-数据结构

模式是对数据结构的描述,可以是隐式的,也可以是显式的。将现有关系数据库转换为数据集有两种主要方法,因为数据框架在内部基于 RDDs 它们如下:

  • 用反射来推断 RDD 的图式
  • 通过一个编程接口,在这个接口的帮助下,您可以获取一个现有的 RDD 并呈现一个模式,从而将 RDD 转换为一个具有模式的数据集

隐式模式

让我们看一个例子,将一个逗号分隔值 ( CSV )文件加载到一个数据帧中。每当文本文件包含标题时,读取应用编程接口就可以通过读取标题行来推断模式。我们还可以选择指定用于拆分文本文件行的分隔符。

我们从标题行读取csv推断模式,并使用逗号(,)作为分隔符。我们还展示了使用schema命令和printSchema命令来验证输入文件的模式:

scala> val statesDF = spark.read.option("header", "true")
 .option("inferschema", "true")
 .option("sep", ",")
 .csv("statesPopulation.csv")
statesDF: org.apache.spark.sql.DataFrame = [State: string, Year: int ... 1
more field]
scala> statesDF.schema
res92: org.apache.spark.sql.types.StructType = StructType(
StructField(State,StringType,true),
StructField(Year,IntegerType,true),
StructField(Population,IntegerType,true))
scala> statesDF.printSchema
root
|-- State: string (nullable = true)
|-- Year: integer (nullable = true)
|-- Population: integer (nullable = true)

显式模式

使用StructType描述模式,T0 是StructField对象的集合。

StructType and StructField belong to the  org.apache.spark.sql.types package. DataTypes such as IntegerType and StringType also belong to the org.apache.spark.sql.types package.

使用这些导入,我们可以定义一个自定义的显式模式。

首先,导入必要的类:

scala> import org.apache.spark.sql.types.{StructType, IntegerType,
StringType}
import org.apache.spark.sql.types.{StructType, IntegerType, StringType}

定义带有两个列/字段和一个后跟字符串的整数的模式:

scala> val schema = new StructType().add("i", IntegerType).add("s",
StringType)
schema: org.apache.spark.sql.types.StructType =
StructType(StructField(i,IntegerType,true), StructField(s,StringType,true))

很容易打印刚刚创建的schema:

scala> schema.printTreeString
root
|-- i: integer (nullable = true)
|-- s: string (nullable = true)

还有一个打印 JSON 的选项,如下,使用prettyJson功能:

scala> schema.prettyJson
res85: String =
{
"type" : "struct",
"fields" : [ {
"name" : "i",
"type" : "integer",
"nullable" : true,
"metadata" : { }
}, {
"name" : "s",
"type" : "string",
"nullable" : true,
"metadata" : { }
} ]
}

Spark SQL 的所有数据类型都位于包org.apache.spark.sql.types中。

您可以通过以下方式访问它们:

import org.apache.spark.sql.types._

编码器

Spark 2.x 支持为复杂数据类型定义模式的不同方式。首先,让我们看一个简单的例子。Encoders必须使用导入语句导入,以便您使用Encoders:

import org.apache.spark.sql.Encoders

让我们看一个简单的例子,将元组定义为数据集 API 中使用的数据类型:

scala> Encoders.product[(Integer, String)].schema.printTreeString
root
|-- _1: integer (nullable = true)
|-- _2: string (nullable = true)

前面的代码看起来总是很复杂,所以我们也可以为我们的需求定义一个case class,然后使用它。

我们可以用两个字段定义一个案例class Record,一个Integer和一个String:

scala> case class Record(i: Integer, s: String)

defined class Record

使用Encoders我们可以很容易地在case class之上创建一个模式,从而允许我们轻松地使用各种应用编程接口:

scala> Encoders.product[Record].schema.printTreeString
root
|-- i: integer (nullable = true)
|-- s: string (nullable = true)

Spark SQL 的所有数据类型都位于包org.apache.spark.sql.types中。

您可以通过以下方式访问它们:

import org.apache.spark.sql.types._

您应该在代码中使用DataTypes对象来创建复杂的Spark SQL类型,如数组或映射,如下所示:

scala> import org.apache.spark.sql.types.DataTypes
import org.apache.spark.sql.types.DataTypes
scala> val arrayType = DataTypes.createArrayType(IntegerType)
arrayType: org.apache.spark.sql.types.ArrayType =
ArrayType(IntegerType,true)

以下是 SparkSQL APIs 支持的数据类型:

| 数据类型 | Scala 中的值类型 | API 访问 或创建数据类型 | | ByteType | Byte | ByteType | | ShortType | Short | ShortType | | IntegerType | Int | IntegerType | | LongType | Long | LongType | | FloatType | Float | FloatType | | DoubleType | Double | DoubleType | | DecimalType  | java.math.BigDecimal | DecimalType | | StringType | String  | StringType | | BinaryType | Array[Byte]  | BinaryType | | BooleanType | Boolean | BooleanType | | TimestampType | java.sql.Timestamp | TimestampType | | DateType | java.sql.Date  | DateType | | ArrayType | scala.collection.Seq | ArrayType(elementType, [containsNull]) | | MapType | scala.collection.Map | MapType(keyType, valueType, [valueContainsNull])Note:默认valueContainsNulltrue。 | | StructType | org.apache.spark.sql.Row | StructType(fields).Note:菲尔兹是StructFieldsSeq。此外,不允许两个字段同名。 |

正在加载数据集

Spark SQL 可以通过DataFrameReader界面从文件、Hive 表、JDBC 数据库等外部存储系统读取数据。

API 调用的格式为spark.read.inputtype:

  • 镶木地板
  • 战斗支援车
  • Hive 表
  • JDBC
  • 妖魔
  • 文本
  • JSON

让我们看几个将 CSV 文件读入数据帧的简单例子:

scala> val statesPopulationDF = spark.read.option("header",
"true").option("inferschema", "true").option("sep",
",").csv("statesPopulation.csv")
statesPopulationDF: org.apache.spark.sql.DataFrame = [State: string, Year:
int ... 1 more field]
scala> val statesTaxRatesDF = spark.read.option("header",
"true").option("inferschema", "true").option("sep",
",").csv("statesTaxRates.csv")
statesTaxRatesDF: org.apache.spark.sql.DataFrame = [State: string, TaxRate:
double]

保存数据集

Spark SQL 可以通过DataFrameWriter界面将数据保存到文件、Hive 表和 JDBC 数据库等外部存储系统中。

API 调用的格式为dataframe.write.outputtype:

  • 镶木地板
  • 妖魔
  • 文本
  • Hive 表
  • JSON
  • 战斗支援车
  • JDBC

让我们看几个将数据帧写入或保存到 CSV 文件的例子:

scala> statesPopulationDF.write.option("header",
"true").csv("statesPopulation_dup.csv")
scala> statesTaxRatesDF.write.option("header",
"true").csv("statesTaxRates_dup.csv")

聚集

聚合是根据条件将数据收集在一起并对数据进行分析的方法。聚合对于理解各种大小的数据非常重要,因为仅仅拥有原始数据记录对于大多数用例来说并不那么有用。

Imagine a table containing one temperature measurement per day for every city in the world for five years.

例如,如果您看到下表,然后看到相同数据的聚合视图,那么很明显,仅仅原始记录并不能帮助您理解数据。下表显示了原始数据:

| 城市 | 日期 | 温度 | | 波士顿 | 12/23/2016 | Thirty-two | | 纽约 | 12/24/2016 | Thirty-six | | 波士顿 | 12/24/2016 | Thirty | | 费城 | 12/25/2016 | Thirty-four | | 波士顿 | 12/25/2016 | Twenty-eight |

下图是每个城市的平均温度:

| 城市 | 平均T2气温 | | 波士顿 | 30 - (32 + 30 + 28)/3 | | 纽约 | Thirty-six | | 费城 | Thirty-four |

聚合函数

可以在org.apache.spark.sql.functions包中找到的函数的帮助下执行聚合。除此之外,还可以创建自定义聚合函数,也称为用户自定义聚合函数 ( UDAF )。

Each grouping operation returns a RelationalGroupedDataset on which aggregations can be specified.

我们将加载示例数据来说明本节中所有不同类型的聚合函数:

val statesPopulationDF = spark.read.option("header", "true").
 option("inferschema", "true").
 option("sep", ",").csv("statesPopulation.csv")

数数

Count 是最基本的聚合函数,它只计算指定列的行数。countDistinctcount的延伸;它还消除了重复。

count API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def count(columnName: String): TypedColumn[Any, Long]
 Aggregate function: returns the number of items in a group.
def count(e: Column): Column
 Aggregate function: returns the number of items in a group.
def countDistinct(columnName: String, columnNames: String*): Column
 Aggregate function: returns the number of distinct items in a group.
def countDistinct(expr: Column, exprs: Column*): Column
 Aggregate function: returns the number of distinct items in a group.

让我们看一下在数据框中调用countcountDistinct来打印行数的一些例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(col("*")).agg(count("State")).show
scala> statesPopulationDF.select(count("State")).show
+------------+
|count(State)|
+------------+
| 350|
+------------+
scala> statesPopulationDF.select(col("*")).agg(countDistinct("State")).show
scala> statesPopulationDF.select(countDistinct("State")).show
+---------------------+
|count(DISTINCT State)|
+---------------------+
| 50|

第一

获取RelationalGroupedDataset中的第一条记录。

first API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def first(columnName: String): Column
 Aggregate function: returns the first value of a column in a group.
def first(e: Column): Column
 Aggregate function: returns the first value in a group.
def first(columnName: String, ignoreNulls: Boolean): Column
 Aggregate function: returns the first value of a column in a group.
def first(e: Column, ignoreNulls: Boolean): Column 
 Aggregate function: returns the first value in a group.

让我们看一下在数据帧上调用 first 来输出第一行的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(first("State")).show
+-------------------+
|first(State, false)|
+-------------------+
| Alabama|
+-------------------+

最后的

获取RelationalGroupedDataset中的最后一条记录。

last API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def last(columnName: String): Column
 Aggregate function: returns the last value of the column in a group.
def last(e: Column): Column
 Aggregate function: returns the last value in a group.
def last(columnName: String, ignoreNulls: Boolean): Column
 Aggregate function: returns the last value of the column in a group.
def last(e: Column, ignoreNulls: Boolean): Column
 Aggregate function: returns the last value in a group.

让我们看一下调用 DataFrame 上的 last 来输出最后一行的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(last("State")).show
 +------------------+
 |last(State, false)|
 +------------------+
 | Wyoming|
 +------------------+

近似 _ 计数 _ 独特

如果您需要不同记录的近似计数,近似不同计数是一种更快的方法,而不是执行通常需要大量洗牌和其他操作的精确计数。

approx_count_distinct API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def approx_count_distinct(columnName: String, rsd: Double): Column
 Aggregate function: returns the approximate number of distinct items in a
 group.
def approx_count_distinct(e: Column, rsd: Double): Column
 Aggregate function: returns the approximate number of distinct items in a group.
def approx_count_distinct(columnName: String): Column
 Aggregate function: returns the approximate number of distinct items in a group.
def approx_count_distinct(e: Column): Column
 Aggregate function: returns the approximate number of distinct items in a group.

让我们看一下在数据帧上调用approx_count_distinct来打印数据帧的大概计数的例子:

import org.apache.spark.sql.functions._
 scala>
 statesPopulationDF.select(col("*")).agg(approx_count_distinct("State")).show
 +----------------------------+
 |approx_count_distinct(State)|
 +----------------------------+
 | 48|
 +----------------------------+
 scala> statesPopulationDF.select(approx_count_distinct("State", 0.2)).show
 +----------------------------+
 |approx_count_distinct(State)|
 +----------------------------+
 | 49|
 +----------------------------+

min是数据框中某一列的最小列值。min的一个例子就是如果要查一个城市的最低气温。

min API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def min(columnName: String): Column
 Aggregate function: returns the minimum value of the column in a group.
def min(e: Column): Column
 Aggregate function: returns the minimum value of the expression in a group.

让我们看一下在数据帧上调用min来打印最小值Population的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(min("Population")).show
 +---------------+
 |min(Population)|
 +---------------+
 | 564513|
+---------------+

最大

max是数据框中某一列的最大列值。这方面的一个例子是,如果你想检查一个城市的最高温度。

max API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def max(columnName: String): Column
 Aggregate function: returns the maximum value of the column in a group.
def max(e: Column): Column
 Aggregate function: returns the maximum value of the expression in a group.

让我们看一下在数据框上调用max打印最大值Population的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(max("Population")).show
+---------------+
 |max(Population)|
 +---------------+
 | 39250017|
 +---------------+

平均值

这些值的平均值是通过将这些值相加并除以值的数量来计算的。

The average of 123 is (1 + 2 + 3) / 3 = 6/3 = z.

avg API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def avg(columnName: String): Column
 Aggregate function: returns the average of the values in a group.
def avg(e: Column): Column
 Aggregate function: returns the average of the values in a group.

让我们看一下在数据框上调用avg来打印平均人口的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(avg("Population")).show
 +-----------------+
 | avg(Population)|
 +-----------------+
 |6253399.371428572|
 +----------------+

总和

计算列值的总和。可选地,sumDistinct只能用于累加不同的值。

sum API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def sum(columnName: String): Column
 Aggregate function: returns the sum of all values in the given column.
def sum(e: Column): Column
 Aggregate function: returns the sum of all values in the expression.
def sumDistinct(columnName: String): Column
 Aggregate function: returns the sum of distinct values in the expression
def sumDistinct(e: Column): Column
 Aggregate function: returns the sum of distinct values in the expression.

让我们看一下在数据帧上调用 sum 来打印 sum(total)Population的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(sum("Population")).show
 +---------------+
 |sum(Population)|
 +---------------+
 | 2188689780|
 +---------------+

峭度

kurtosis是一种量化分布形状差异的方法,在平均值和方差方面看起来非常相似,但实际上是不同的。

kurtosis API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def kurtosis(columnName: String): Column
 Aggregate function: returns the kurtosis of the values in a group.
def kurtosis(e: Column): Column
 Aggregate function: returns the kurtosis of the values in a group.

让我们看一个在Population列的数据框上调用kurtosis的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(kurtosis("Population")).show
 +--------------------+
 |kurtosis(Population)|
 +--------------------+
 | 7.727421920829375|
 +--------------------+

歪斜

skewness测量数据中的值围绕平均值或平均值的不对称性。

skewness API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def skewness(columnName: String): Column
 Aggregate function: returns the skewness of the values in a group.
def skewness(e: Column): Column
 Aggregate function: returns the skewness of the values in a group.

让我们看一下在Population列的数据框上调用skewness的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(skewness("Population")).show
 +--------------------+
 |skewness(Population)|
 +--------------------+
 | 2.5675329049100024|
 +--------------------+

差异

方差是每个值与平均值的平方差的平均值。

var API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def var_pop(columnName: String): Column
 Aggregate function: returns the population variance of the values in a group.
def var_pop(e: Column): Column
 Aggregate function: returns the population variance of the values in a group.
def var_samp(columnName: String): Column
 Aggregate function: returns the unbiased variance of the values in a group.
def var_samp(e: Column): Column
 Aggregate function: returns the unbiased variance of the values in a group.

现在,让我们看看在测量Population方差的数据帧上调用var_pop的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(var_pop("Population")).show
 +--------------------+
 | var_pop(Population)|
 +--------------------+
 |4.948359064356177E13|
 +--------------------+

标准偏差

标准差是方差的平方根(见上一节)。

stddev API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def stddev(columnName: String): Column
 Aggregate function: alias for stddev_samp.
def stddev(e: Column): Column
 Aggregate function: alias for stddev_samp.
def stddev_pop(columnName: String): Column
 Aggregate function: returns the population standard deviation of the
 expression in a group.
def stddev_pop(e: Column): Column
 Aggregate function: returns the population standard deviation of the
 expression in a group.
def stddev_samp(columnName: String): Column
 Aggregate function: returns the sample standard deviation of the expression in a group.
def stddev_samp(e: Column): Column
Aggregate function: returns the sample standard deviation of the expression in a group.

我们来看一个在数据框上调用stddev的例子,打印Population的标准 偏差:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(stddev("Population")).show
 +-----------------------+
 |stddev_samp(Population)|
 +-----------------------+
 | 7044528.191173398|
 +-----------------------+

协方差

协方差是两个随机变量联合可变性的度量。

covar API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def covar_pop(columnName1: String, columnName2: String): Column
 Aggregate function: returns the population covariance for two columns.
def covar_pop(column1: Column, column2: Column): Column
 Aggregate function: returns the population covariance for two columns.
def covar_samp(columnName1: String, columnName2: String): Column
 Aggregate function: returns the sample covariance for two columns.
def covar_samp(column1: Column, column2: Column): Column
 Aggregate function: returns the sample covariance for two columns.

让我们看一个在数据帧上调用covar_pop来计算YearPopulation列之间协方差的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(covar_pop("Year", "Population")).show
 +---------------------------+
 |covar_pop(Year, Population)|
 +---------------------------+
 | 183977.56000006935|
 +---------------------------+

群组依据

数据分析中常见的任务是将数据分成不同的类别,然后对结果数据组进行计算。

让我们在数据框上运行groupBy功能,打印每个State的聚合计数:

scala> statesPopulationDF.groupBy("State").count.show(5)
 +---------+-----+
| State|count|
 +---------+-----+
 | Utah| 7|
 | Hawaii| 7|
 |Minnesota| 7|
 | Ohio| 7|
 | Arkansas| 7|
 +---------+-----+

您也可以groupBy然后应用之前看到的任何聚合函数,如minmaxavgstddev等:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.groupBy("State").agg(min("Population"),
 avg("Population")).show(5)
+---------+---------------+--------------------+
 | State|min(Population)| avg(Population)|
 +---------+---------------+--------------------+
 | Utah| 2775326| 2904797.1428571427|
 | Hawaii| 1363945| 1401453.2857142857|
 |Minnesota| 5311147| 5416287.285714285|
 | Ohio| 11540983|1.1574362714285715E7|
 | Arkansas| 2921995| 2957692.714285714|
 +---------+---------------+--------------------+

到达

Rollup 是用于执行分层或嵌套计算的多维聚合。例如,如果我们想要显示每个StateYear组以及每个State的记录数量(汇总所有年份以给出每个State的总计,而不考虑Year,我们可以如下使用rollup:

scala> statesPopulationDF.rollup("State", "Year").count.show(5)
 +------------+----+-----+
 | State|Year|count|
 +------------+----+-----+
 |South Dakota|2010| 1|
 | New York|2012| 1|
 | California|2014| 1|
 | Wyoming|2014| 1|
 | Hawaii|null| 7|
 +------------+----+-----+

立方

Cube是一个多维聚合,用于执行分层或嵌套计算,就像rollup一样,不同的是cube对所有维度执行相同的操作。例如,如果我们想要显示每个StateYear组以及每个State的记录数量(汇总一整年以给出每个State的总计,与Year无关),我们可以使用cube如下:

scala> statesPopulationDF.cube("State", "Year").count.show(5)
 +------------+----+-----+
 | State|Year|count|
 +------------+----+-----+
 |South Dakota|2010| 1|
 | New York|2012| 1|
 | null|2014| 50|
 | Wyoming|2014| 1|
 | Hawaii|null| 7|
 +------------+----+-----+

窗口功能

窗口函数允许您在一个数据窗口上执行聚合,而不是在整个数据或某些筛选数据上执行聚合。此类窗口函数的用例有:

  • 累计总和
  • 同一个键的上一个值的增量
  • 加权移动平均线

您可以通过执行简单的计算,指定一个窗口查看三行 T-1TT+1 。您还可以在最近/最近的 10 个值上指定一个窗口:

Window规范的 API 需要三个属性,partitionBy()orderBy()rowsBetween()partitionBy按照partitionBy()的规定将数据分块到分区/组中。orderBy()用于对每个数据分区内的数据进行排序。

rowsBetween()指定执行计算的窗框或滑动窗口的跨度。

要试用 Windows 功能,需要某些软件包。可以使用导入指令导入必要的包,如下所示:

import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions.col
import org.apache.spark.sql.functions.max

现在我们准备写一些代码来学习Window函数。让我们为按Population排序和按State分区的分区创建一个窗口规范。此外,指定我们要将当前行之前的所有行视为Window的一部分:

val windowSpec = Window
 .partitionBy("State")
 .orderBy(col("Population").desc)
 .rowsBetween(Window.unboundedPreceding, Window.currentRow)

计算超过Window规格的等级。只要在指定的Window范围内,结果将是添加到每行的等级(行号)。在这个例子中,我们选择按State进行划分,然后按降序对每个State的行进行排序。因此,每个State行都分配有自己的等级编号:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(col("State"), col("Year"),
 max("Population").over(windowSpec), rank().over(windowSpec)).sort("State",
 "Year").show(10)
 +-------+----+-------------------------------------------------------------
 -----------------------------------------------------------------+---------
 ---------------------------------------------------------------------------
 ---------------------------------+
| State|Year|max(Population) OVER (PARTITION BY State ORDER BY Population
 DESC NULLS LAST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)|RANK()
 OVER (PARTITION BY State ORDER BY Population DESC NULLS LAST ROWS BETWEEN
 UNBOUNDED PRECEDING AND CURRENT ROW)|
 +-------+----+-------------------------------------------------------------
 -----------------------------------------------------------------+---------
 ---------------------------------------------------------------------------
 ---------------------------------+
|Alabama|2010| 4863300| 6|
 |Alabama|2011| 4863300| 7|
 |Alabama|2012| 4863300| 5|
 |Alabama|2013| 4863300| 4|
 |Alabama|2014| 4863300| 3|

奈尔斯

ntiles是一种流行的窗口聚合,通常用于将输入数据集划分为 n 个部分。

例如,如果我们想将statesPopulationDF划分为State(窗口说明如前所示),按人口排序,然后分成两部分,我们可以在windowspec上使用ntile:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(col("State"), col("Year"),
 ntile(2).over(windowSpec), rank().over(windowSpec)).sort("State",
 "Year").show(10)
+-------+----+-------------------------------------------------------------
 ----------------------------------------------------------+----------------
 ---------------------------------------------------------------------------
 --------------------------+
| State|Year|ntile(2) OVER (PARTITION BY State ORDER BY Population DESC
 NULLS LAST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)|RANK() OVER
 (PARTITION BY State ORDER BY Population DESC NULLS LAST ROWS BETWEEN
 UNBOUNDED PRECEDING AND CURRENT ROW)|
 +-------+----+-------------------------------------------------------------
 ----------------------------------------------------------+----------------
 ---------------------------------------------------------------------------
 --------------------------+
 |Alabama|2010| 2| 6|
 |Alabama|2011| 2| 7|
 |Alabama|2012| 2| 5|
 |Alabama|2013| 1| 4|
 |Alabama|2014| 1| 3|
 |Alabama|2015| 1| 2|
 |Alabama|2016| 1| 1|
 | Alaska|2010| 2| 7|
 | Alaska|2011| 2| 6|
 | Alaska|2012| 2| 5|
 +-------+----+-------------------------------------------------------------
 ----------------------------------------------------------+----------------
 --------------------------------------------------------------

如前所示,我们使用Window功能和ntile()一起将每个State的行分成两个相等的部分。

A popular use of this function is to compute decile used in data science models.

连接

在传统数据库中,连接用于将一个事务表与另一个查找表连接起来,以生成更完整的视图。例如,如果您有一个按客户标识排序的在线交易表和另一个包含客户城市和客户标识的表,则可以使用联接生成按城市排序的交易报告。

交易表:本表有三栏,客户编号采购项目,客户支付项目金额:

| 客户名称 | 采购项目 | 支付的价格 | | one | 耳机 | Twenty-five | | Two | 看 | Twenty | | three | 键盘 | Twenty | | one | 老鼠 | Ten | | four | 电缆 | Ten | | three | 耳机 | Thirty |

客户信息表:该表有两列客户信息和客户居住的城市:

| 客户编号 | 城市 | | one | 波士顿 | | Two | 纽约 | | three | 费城 | | four | 波士顿 |

将交易表与客户信息表连接起来将生成如下视图:

| 客户编号 | 采购项目 | 支付的价格 | 城市 | | one | 双耳式耳机 | Twenty-five | 波士顿 | | Two | 看 | One hundred | 纽约 | | three | 键盘 | Twenty | 费城 | | one | 老鼠 | Ten | 波士顿 | | four | 电缆 | Ten | 波士顿 | | three | 耳机 | Thirty | 费城 |

现在,我们可以使用这个连接的视图来生成按City排序的Total销售价格报告:

| 城市 | #项 | 销售总价 | | 波士顿 | three | Forty-five | | 费城 | Two | Fifty | | 纽约 | one | One hundred |

连接是 Spark SQL 的一个重要功能,因为它们使您能够将两个数据集结合在一起,如前所述。当然,Spark 不仅仅意味着生成一些报告,还用于以 Peta 字节规模处理数据,以处理实时流用例、机器学习算法或简单分析。为了实现这些目标,Spark 提供了所需的 API 函数。

两个数据集之间的典型连接使用左右数据集的一个或多个键进行,然后将键集上的条件表达式计算为布尔表达式。如果布尔表达式的结果返回true,则连接成功,否则连接的数据帧将不包含相应的连接。join应用编程接口有六种不同的实现:

join(right: Dataset[_]): DataFrame
 Condition-less inner join
 join(right: Dataset[_], usingColumn: String): DataFrame
 Inner join with a single column
 join(right: Dataset[_], usingColumns: Seq[String]): DataFrame
 Inner join with multiple columns
 join(right: Dataset[_], usingColumns: Seq[String], joinType: String):
 DataFrame
Join with multiple columns and a join type (inner, outer,....)

 join(right: Dataset[_], joinExprs: Column): DataFrame
 Inner Join using a join expression
join(right: Dataset[_], joinExprs: Column, joinType: String): DataFrame
 Join using a Join expression and a join type (inner, outer, ...)

我们将使用其中一个 API 来了解如何使用joinAPI;但是,您可以根据用例选择使用其他 API:

def join(right: Dataset[_], joinExprs: Column, joinType: String):
 DataFrame
Join with another DataFrame using the given join expression
 right: Right side of the join.
joinExprs: Join expression.
 joinType : Type of join to perform. Default is inner join
// Scala:
 import org.apache.spark.sql.functions._
 import spark.implicits._
 df1.join(df2, $"df1Key" === $"df2Key", "outer")

请注意,在接下来的几节中将详细介绍连接。

连接的内部工作方式

Join 通过使用多个执行器对数据帧的分区进行操作来工作。但是,实际操作和后续性能取决于连接的类型和要连接的数据集的性质。在下一节中,我们将研究不同类型的连接。

随机加入

两个大数据集之间的连接包括无序连接,其中左右数据集的分区分布在执行器中。混洗是昂贵的,分析逻辑以确保分区和混洗的分布是最佳的是很重要的。

以下是 shuffle join 内部工作原理的说明:

广播加入

一个大数据集和一个小数据集之间的连接是通过将小数据集广播给从左数据集开始存在分区的所有执行器来实现的,这种连接称为广播连接

以下是广播连接如何在内部工作的说明:

连接类型

下表列出了不同类型的联接。这一点很重要,因为连接两个数据集时所做的选择对输出和性能都有很大影响:

| 连接类型 | 描述 | | 内部的 | 内部连接从左到右比较每一行,并且仅当左右数据集都具有非NULL值时,才组合匹配的行对。 | | 外侧,全外侧,全外侧 | 完全外部联接给出了左侧和右侧表中的所有行。如果我们想保留两个表中的所有行,我们使用完全外部连接。当其中一个表匹配时,完全外部联接返回所有行 | | 左反 | 左反连接只给出基于左侧表的那些行,这些行不在右侧表中。 | | 左,左外侧 | 左外连接给出了左中的所有行加上左和右的公共行(内连接)。如果不正确,填写NULL。 | | 左半 | 左半连接根据右侧的存在只给出左侧的行。不包括右侧的值。 | | 右,右外侧 | 右外连接给出了右中的所有行加上左和右的公共行(内连接)。如果不在左边,填写NULL。 |

我们将通过使用示例数据集来研究不同的连接类型是如何工作的:

scala> val statesPopulationDF = spark.read.option("header",
 "true").option("inferschema", "true").option("sep",
 ",").csv("statesPopulation.csv")
 statesPopulationDF: org.apache.spark.sql.DataFrame = [State: string, Year:
 int ... 1 more field]
scala> val statesTaxRatesDF = spark.read.option("header",
 "true").option("inferschema", "true").option("sep",
 ",").csv("statesTaxRates.csv")
 statesTaxRatesDF: org.apache.spark.sql.DataFrame = [State: string, TaxRate:
 double]
scala> statesPopulationDF.count
 res21: Long = 357
scala> statesTaxRatesDF.count
 res32: Long = 47
%sql
 statesPopulationDF.createOrReplaceTempView("statesPopulationDF")
 statesTaxRatesDF.createOrReplaceTempView("statesTaxRatesDF")

内部连接

当两个数据集中的State都不为NULL时,内部连接会产生来自statesPopulationDFstatesTaxRatesDF的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "inner")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF INNER JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
 res22: Long = 329
scala> joinDF.show
 +--------------------+----+----------+--------------------+-------+
 | State|Year|Population| State|TaxRate|
+--------------------+----+----------+--------------------+-------+
 | Alabama|2010| 4785492| Alabama| 4.0|
 | Arizona|2010| 6408312| Arizona| 5.6|
 | Arkansas|2010| 2921995| Arkansas| 6.5|
 | California|2010| 37332685| California| 7.5|
 | Colorado|2010| 5048644| Colorado| 2.9|
 | Connecticut|2010| 3579899| Connecticut| 6.35|

你可以在joinDF上运行explain()查看执行计划:

scala> joinDF.explain
 == Physical Plan ==
*BroadcastHashJoin [State#570], [State#577], Inner, BuildRight
 :- *Project [State#570, Year#571, Population#572]
 : +- *Filter isnotnull(State#570)
 : +- *FileScan csv [State#570,Year#571,Population#572] Batched: false,
Format: CSV, Location: InMemoryFileIndex[file:/Users/salla/spark-2.1.0-binhadoop2.7/
 statesPopulation.csv], PartitionFilters: [], PushedFilters:
 [IsNotNull(State)], ReadSchema:
 struct<State:string,Year:int,Population:int>
 +- BroadcastExchange HashedRelationBroadcastMode(List(input[0, string,
 true]))
 +- *Project [State#577, TaxRate#578]
 +- *Filter isnotnull(State#577)
 +- *FileScan csv [State#577,TaxRate#578] Batched: false, Format: CSV,
Location: InMemoryFileIndex[file:/Users/salla/spark-2.1.0-binhadoop2.7/
 statesTaxRates.csv], PartitionFilters: [], PushedFilters:[IsNotNull(State)], ReadSchema: struct<State:string,TaxRate:double>

左外连接

左外连接导致从statesPopulationDF开始的所有行,包括在statesPopulationDFstatesTaxRatesDF中常见的任何行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "leftouter")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF LEFT OUTER JOIN
statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
 scala> joinDF.count
 res22: Long = 357
 scala> joinDF.show(5)
 +----------+----+----------+----------+-------+
 | State|Year|Population| State|TaxRate|
 +----------+----+----------+----------+-------+
 | Alabama|2010| 4785492| Alabama| 4.0|
 | Alaska|2010| 714031| null| null|
 | Arizona|2010| 6408312| Arizona| 5.6|
 | Arkansas|2010| 2921995| Arkansas| 6.5|
 |California|2010| 37332685|California| 7.5|
 +----------+----+----------+----------+-------+

右外连接

右外连接导致从statesTaxRatesDF开始的所有行,包括在statesPopulationDFstatesTaxRatesDF中常见的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "rightouter")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF RIGHT OUTER JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
 res22: Long = 323
scala> joinDF.show
 +--------------------+----+----------+--------------------+-------+
 | State|Year|Population| State|TaxRate|
 +--------------------+----+----------+--------------------+-------+
 | Colorado|2011| 5118360| Colorado| 2.9|
 | Colorado|2010| 5048644| Colorado| 2.9|
 | null|null| null|Connecticut| 6.35|
 | Florida|2016| 20612439| Florida| 6.0|
 | Florida|2015| 20244914| Florida| 6.0|
 | Florida|2014| 19888741| Florida| 6.0|

外部连接

外部连接导致从statesPopulationDFstatesTaxRatesDF的所有行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "fullouter")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF FULL OUTER JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
 res22: Long = 351
scala> joinDF.show
 +--------------------+----+----------+--------------------+-------+
 | State|Year|Population| State|TaxRate|
 +--------------------+----+----------+--------------------+-------+
 | Delaware|2010| 899816| null| null|
 | Delaware|2011| 907924| null| null|
 | West Virginia|2010| 1854230| West Virginia| 6.0|
 | West Virginia|2011| 1854972| West Virginia| 6.0|
 | Missouri|2010| 5996118| Missouri| 4.225|
 | null|null| null| Connecticut| 6.35|

左反连接

当且仅当statesTaxRatesDF中没有对应的行时,左反连接仅导致从statesPopulationDF开始的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "leftanti")
 %sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF LEFT ANTI JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
res22: Long = 28
 scala> joinDF.show(5)
 +--------+----+----------+
 | State|Year|Population|
 +--------+----+----------+
 | Alaska|2010| 714031|
 |Delaware|2010| 899816|
 | Montana|2010| 990641|
 | Oregon|2010| 3838048|
 | Alaska|2011| 722713|
 +--------+----+----------+

左半连接

当且仅当在statesTaxRatesDF中有对应的行时,左半连接导致仅从statesPopulationDF开始的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "leftsemi")
 %sql

val joinDF = spark.sql("SELECT * FROM statesPopulationDF LEFT SEMI JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
res22: Long = 322
 scala> joinDF.show(5)
 +----------+----+----------+
 | State|Year|Population|
 +----------+----+----------+
 | Alabama|2010| 4785492|
 | Arizona|2010| 6408312|
 | Arkansas|2010| 2921995|
 |California|2010| 37332685|
 | Colorado|2010| 5048644|
 +----------+----+----------+

交叉连接

交叉连接将左边的每一行与右边的每一行进行匹配,生成笛卡尔叉积:

通过State列连接两个数据集,如下所示:

scala> val joinDF=statesPopulationDF.crossJoin(statesTaxRatesDF)
 joinDF: org.apache.spark.sql.DataFrame = [State: string, Year: int ... 3
 more fields]
%sql
val joinDF = spark.sql("SELECT * FROM statesPopulationDF CROSS JOIN
 statesTaxRatesDF")
 scala> joinDF.count
res46: Long = 16450
 scala> joinDF.show(10)
 +-------+----+----------+-----------+-------+
 | State|Year|Population| State|TaxRate|
 +-------+----+----------+-----------+-------+
 |Alabama|2010| 4785492| Alabama| 4.0|
 |Alabama|2010| 4785492| Arizona| 5.6|
 |Alabama|2010| 4785492| Arkansas| 6.5|
 |Alabama|2010| 4785492| California| 7.5|
 |Alabama|2010| 4785492| Colorado| 2.9|
 |Alabama|2010| 4785492|Connecticut| 6.35|
 |Alabama|2010| 4785492| Florida| 6.0|
 |Alabama|2010| 4785492| Georgia| 4.0|
 |Alabama|2010| 4785492| Hawaii| 4.0|
 |Alabama|2010| 4785492| Idaho| 6.0|
 +-------+----+----------+-----------+-------+

You can also use join with cross joinType instead of calling the cross join API: statesPopulationDF.join(statesTaxRatesDF, statesPopulationDF("State").isNotNull, "cross").count.

加入对性能的影响

选择的连接类型直接影响连接的性能。这是因为联接需要在执行器之间进行数据洗牌来执行任务,因此在使用联接时需要考虑不同的联接,甚至联接的顺序。下面是一个在编写连接代码时可以用作参考的表:

| 连接类型 | 性能注意事项和提示 | | 内部的 | 内部联接要求左右表具有相同的列。如果您在左侧或右侧有重复或多个键的副本,连接将很快变成某种笛卡尔连接,比正确设计以最小化多个键花费更长的时间来完成。 | | 跨过 | 交叉连接将左边的每一行与右边的每一行进行匹配,生成笛卡尔叉积。由于这是性能最差的连接,因此应谨慎使用,仅在特定用例中使用。 | | 外侧,全外侧,全外侧 | 完全外部联接给出联接子句左侧和右侧表中的所有(匹配和不匹配)行。当我们想要保留两个表中的所有行时,我们使用完全外部连接。当其中一个表匹配时,完全外部联接返回所有行。如果在几乎没有共同点的表上使用,可能会导致非常大的结果,从而降低性能。 | | 左反 | 左反连接只给出基于左侧表的那些行,这些行不在右侧表中。当我们希望只保留左表中的行而不保留右表中的行时,请使用此选项。非常好的性能,因为只充分考虑了一个表,并且只检查了另一个表的连接条件。 | | 左,左外侧 | 左外连接给出了左侧表中的所有行,以及两个表共有的行(内连接)。如果在几乎没有共同点的表上使用,会导致非常大的结果,从而降低性能。 | | 左半 | 当且仅当左侧表中的行存在于右侧表中时,左侧半联接才给出左侧表中的行。当且仅当行在右表中找到时,使用此选项从左表中获取行。这与上面看到的 leftanti join 相反。不包括右侧值。非常好的性能,因为只充分考虑了一个表,并且只检查了另一个表的连接条件。 | | 右,右外侧 | 右外连接给出了右侧表中的所有行以及左侧和右侧的公共行(内连接)。使用它可以获得右表中的所有行以及左表和右表中的行。如果不在左边,填写NULL。性能类似于本表前面提到的左外连接。 |

摘要

在本章中,我们讨论了数据框的起源,以及 Spark SQL 如何在数据框之上提供 SQL 接口。数据帧的强大之处在于,与最初基于 RDD 的计算相比,执行时间减少了。拥有这样一个功能强大的层和一个简单的类似于 SQL 的接口使它变得更加强大。我们还研究了创建和操作数据帧的各种 API,并深入挖掘了聚合的复杂特性,包括groupByWindowrollupcubes。最后,我们还研究了连接数据集的概念以及各种可能的连接类型,如内部、外部、交叉等。

我们将通过 Apache Spark 在第 7 章实时分析中探索令人兴奋的实时数据处理和分析世界。