目录
[Lambda 表达式](#Lambda 表达式)
[.map(...) 转换](#.map(...) 转换)
[.filter(...) 转换](#.filter(...) 转换)
[.flatMap(...) 转换](#.flatMap(...) 转换)
[.distinct(...) 转换](#.distinct(...) 转换)
[.sample(...) 转换](#.sample(...) 转换)
[.leftOuterJoin(...) 转换](#.leftOuterJoin(...) 转换)
[.repartition(...) 转换](#.repartition(...) 转换)
Lambda 表达式
在这个例子中,我们将从 data_from_file 的加密外观记录中提取有用的信息。
首先,让我们使用以下代码定义一个方法,该方法将解析不可读的行,使其变得有用:
python
def extractInformation(row):
import re
import numpy as np
selected_indices = [
2,4,5,6,7,9,10,11,12,13,14,15,16,17,18,
...
77,78,79,81,82,83,84,85,87,89
]
record_split = re\
.compile(
r'([\s]{19})([0-9]{1})([\s]{40})
...
([\s]{33})([0-9\s]{3})([0-9\s]{1})([0-9\s]{1})')
try:
rs = np.array(record_split.split(row))[selected_indices]
except:
rs = np.array(['-99'] * len(selected_indices))
return rs
接下来,我们导入必要的模块:使用正则表达式解析记录的 re 模块,以及为了方便一次性选择多个元素而使用的 NumPy。
最后,我们创建一个 Regex 对象,根据指定提取信息,并通过它解析行。
一旦记录被解析,我们尝试将列表转换为 NumPy 数组并返回它;如果失败,我们返回一个默认值列表 -99,以便我们知道这个记录没有正确解析。
现在,我们将使用 extractInformation(...) 方法来拆分和转换我们的数据集。注意,我们只将方法签名传递给 .map(...):每次在每个分区中,方法将一次将 RDD 的一个元素交给 extractInformation(...) 方法:
运行 data_from_file_conv.take(1) 将产生以下结果(缩写):
全局作用域与局部作用域
作为潜在的 PySpark 用户,你需要习惯 Spark 的固有并行性。即使你精通 Python,执行 PySpark 脚本也需要稍微改变你的思维方式。
Spark 可以在两种模式下运行:本地和集群。当你在本地运行 Spark 时,你的代码可能与你目前习惯的 Python 运行没有太大不同:变化可能主要是语法上的,但增加了数据和代码可以在单独的工作进程之间复制的额外转折。
然而,如果你不小心,将相同的代码部署到集群可能会引起很多困惑。这需要理解 Spark 如何在集群上执行作业。
在集群模式下,当提交作业执行时,作业被发送到驱动程序(或主节点)。驱动程序节点为作业创建一个 DAG,并决定哪个执行器(或工作)节点将运行特定任务。
然后驱动程序指示工作执行它们的任务,并在完成后将结果返回给驱动程序。然而,在这之前,驱动程序准备每个任务的闭包:一组在驱动程序上存在,供工作在 RDD 上执行任务的变量和方法。
这组变量和方法在执行器的上下文中本质上是静态的,即每个执行器从驱动程序获得变量和方法的副本。如果执行器在运行任务时更改这些变量或覆盖方法,它这样做不会影响其他执行器的副本或驱动程序的变量和方法。这可能会导致一些意想不到的行为和运行时错误,有时可能很难追踪。
Transformations
转换可以塑造您的数据集。这些包括映射、过滤、连接和数据集中的值的转码。在本节中,我们将展示一些在 RDDs 上可用的转换。
由于 RDDs 是无模式的,因此在本节中,我们假设您知道生成的数据集的模式。如果您记不住解析后数据集中信息的位置,我们建议您参考 GitHub 上 extractInformation(...) 方法的定义,这是第 03 章的代码。
.map(...) 转换
可以说,您将最常使用 .map(...) 转换。该方法应用于 RDD 的每个元素:在 data_from_file_conv 数据集的情况下,您可以将其视为对每一行的转换。
在这个例子中,我们将创建一个新的数据集,将死亡年份转换为数值:
python
data_2014 = data_from_file_conv.map(lambda row: int(row[16]))
运行 data_2014.take(10) 将产生以下结果:
您当然可以带来更多的列,但您必须将它们打包成元组、字典或列表。让我们还包括行的第 17 个元素,以便我们可以确认我们的 .map(...) 是否按预期工作:
python
data_2014_2 = data_from_file_conv.map(
lambda row: (row[16], int(row[16]):)
data_2014_2.take(5)
前面的代码将产生以下结果:
.filter(...) 转换
另一个经常使用的转换是 .filter(...) 方法,它允许您根据指定的条件从数据集中选择元素。例如,从 data_from_file_conv 数据集中,让我们计算一下有多少人在 2014 年因意外事故死亡:
python
data_filtered = data_from_file_conv.filter(
lambda row: row[16] == '2014' and row[21] == '0')
data_filtered.count()
.flatMap(...) 转换
.flatMap(...) 方法的工作原理类似于 .map(...),但它返回的是扁平化的结果而不是列表。如果我们执行以下代码:
python
data_2014_flat = data_from_file_conv.flatMap(lambda row: (row[16],
int(row[16]) + 1))
data_2014_flat.take(10)
它将产生以下输出:
您可以将这个结果与之前生成 data_2014_2 的命令的结果进行比较。注意,正如前面提到的,当您需要解析输入时,可以使用 .flatMap(...) 方法过滤掉一些格式错误的记录。在内部,.flatMap(...) 方法将每一行视为一个列表,然后将所有记录简单相加;通过传递一个空列表,格式错误的记录就会被丢弃。
.distinct(...) 转换
这个方法返回指定列中的唯一值列表。如果您想了解或验证您的数据集,这非常有用。让我们检查性别列是否只包含男性和女性;这将验证我们是否正确解析了数据集。让我们运行以下代码:
python
distinct_gender = data_from_file_conv.map(
lambda row: row[5]).distinct()
distinct_gender.collect()
这段代码将产生以下输出:
首先,我们只提取包含性别的列。接下来,我们使用 .distinct() 方法只选择列表中的唯一值。最后,我们使用 .collect() 方法返回屏幕上的值打印。
.sample(...) 转换
.sample(...) 方法从数据集中返回一个随机样本。第一个参数指定采样是否应该有放回,第二个参数定义要返回的数据比例,第三个参数是伪随机数生成器的种子:
python
fraction = 0.1
data_sample = data_from_file_conv.sample(False, fraction, 666)
在这个例子中,我们从原始数据集中随机抽取了 10% 的样本。
为了确认这一点,让我们打印一下数据集的大小:
python
print('Original dataset: {0}, sample: {1}'\
.format(data_from_file_conv.count(), data_sample.count()))
前面的命令产生了以下输出:
我们使用 .count() 动作来计算相应 RDDs 中的所有记录数。
.leftOuterJoin(...) 转换
.leftOuterJoin(...) 与 SQL 世界中的操作类似,基于两个数据集中找到的值连接两个 RDDs,并返回左 RDD 的记录以及在两个 RDD 匹配的地方附加的右 RDD 的记录:
python
rdd1 = sc.parallelize([('a', 1), ('b', 4), ('c',10)])
rdd2 = sc.parallelize([('a', 4), ('a', 1), ('b', '6'), ('d', 15)])
rdd3 = rdd1.leftOuterJoin(rdd2)
在 rdd3 上运行 .collect(...) 将产生以下结果:
这里你可以看到来自 RDD rdd1 的所有元素以及来自 RDD rdd2 的相应值。如你所见,值 'a' 在 rdd3 中出现了两次,并且在 RDD rdd2 中也出现了两次。来自 rdd1 的值 'b' 只出现了一次,并与来自 rdd2 的值 '6' 连接。少了两件事:rdd1 中的值 'c' 在 rdd2 中没有对应的键,所以在返回的元组中显示为 None;而且,由于我们执行的是左外连接,所以 rdd2 中的值 'd' 按预期消失了。
如果我们使用 .join(...) 方法,我们只会得到 'a' 和 'b' 的值,因为这两个值在这两个 RDD 中相交。运行以下代码:
python
rdd4 = rdd1.join(rdd2)
rdd4.collect()
它将产生以下输出:
另一个有用的方法是 .intersection(...),它返回两个 RDDs 中相等的记录。执行以下代码:
python
rdd5 = rdd1.intersection(rdd2)
rdd5.collect()
输出如下:
.repartition(...) 转换
重新分区数据集会改变数据集被分成的分区数量。这个功能应该谨慎使用,只在真正必要时使用,因为它会重新整理数据,这实际上会导致性能上的显著下降:
python
rdd1 = rdd1.repartition(4)
len(rdd1.glom().collect())
前面的代码打印出 4 作为新的分区数量。
与 .collect() 相反,.glom() 方法产生的是一个列表,其中每个元素是另一个列表,包含在指定分区中存在的数据集中的所有元素;返回的主列表的元素数量与分区数量相同。