PySpark(一)

PySpark(一)
其中Spark主要是由Scala语言开发,为了方便和其他系统集成而不引入scala相关依赖,部分实现使用Java语言开发,例如External Shuffle Service等。总体来说,Spark是由JVM语言实现,会运行在JVM中。然而,Spark除了提供Scala/Java开发接口外,还提供了Python、R等语言的开发接口,为了保证Spark核心实现的独立性,Spark仅在外围做包装,实现对不同语言的开发支持。
Spark运行时架构
首先我们先回顾下Spark的基本运行时架构,如下图所示,其中橙色部分表示为JVM,Spark应用程序运行时主要分为Driver和Executor,Driver负载总体调度及UI展示,Executor负责Task运行,Spark可以部署在多种资源管理系统中,例如Yarn、Mesos等,同时Spark自身也实现了一种简单的Standalone(独立部署)资源管理系统,可以不用借助其他资源管理系统即可运行。更多细节请参考Spark Scheduler内部原理剖析。

用户的Spark应用程序运行在Driver上(某种程度上说,用户的程序就是Spark Driver程序),经过Spark调度封装成一个个Task,再将这些Task信息发给Executor执行,Task信息包括代码逻辑以及数据信息,Executor不直接运行用户的代码。
PySpark运行时架构
为了不破坏Spark已有的运行时架构,Spark在外围包装一层Python API,借助Py4j实现Python和Java的交互,进而实现通过Python编写Spark应用程序,其运行时架构如下图所示。

其中白色部分是新增的Python进程,在Driver端,通过Py4j实现在Python中调用Java的方法,即将用户写的PySpark程序”映射”到JVM中,例如,用户在PySpark中实例化一个Python的SparkContext对象,最终会在JVM中实例化Scala的SparkContext对象;在Executor端,则不需要借助Py4j,因为Executor端运行的Task逻辑是由Driver发过来的,那是序列化后的字节码,虽然里面可能包含有用户定义的Python函数或Lambda表达式,Py4j并不能实现在Java里调用Python的方法,为了能在Executor端运行用户定义的Python函数或Lambda表达式,则需要为每个Task单独启一个Python进程,通过socket通信方式将Python函数或Lambda表达式发给Python进程执行。语言层面的交互总体流程如下图所示,实线表示方法调用,虚线表示结果返回。

下面分别详细剖析PySpark的Driver是如何运行起来的以及Executor是如何运行Task的。
Driver端运行原理
当我们通过spark-submmit提交pyspark程序,首先会上传python脚本及依赖,并申请Driver资源,当申请到Driver资源后,会通过PythonRunner(其中有main方法)拉起JVM,如下图所示。

PythonRunner入口main函数里主要做两件事:
o 开启Py4j GatewayServer
o 通过Java Process方式运行用户上传的Python脚本
用户Python脚本起来后,首先会实例化Python版的SparkContext对象,在实例化过程中会做两件事:
o 实例化Py4j GatewayClient,连接JVM中的Py4j GatewayServer,后续在Python中调用Java的方法都是借助这个Py4j Gateway
o 通过Py4j Gateway在JVM中实例化SparkContext对象
经过上面两步后,SparkContext对象初始化完毕,Driver已经起来了,开始申请Executor资源,同时开始调度任务。用户Python脚本中定义的一系列处理逻辑最终遇到action方法后会触发Job的提交,提交Job时是直接通过Py4j调用Java的PythonRDD.runJob方法完成,映射到JVM中,会转给sparkContext.runJob方法,Job运行完成后,JVM中会开启一个本地Socket等待Python进程拉取,对应地,Python进程在调用PythonRDD.runJob后就会通过Socket去拉取结果。
把前面运行时架构图中Driver部分单独拉出来,如下图所示,通过PythonRunner入口main函数拉起JVM和Python进程,JVM进程对应下图橙色部分,Python进程对应下图白色部分。Python进程通过Py4j调用Java方法提交Job,Job运行结果通过本地Socket被拉取到Python进程。还有一点是,对于大数据量,例如广播变量等,Python进程和JVM进程是通过本地文件系统来交互,以减少进程间的数据传输。

Executor端运行原理
为了方便阐述,以Spark On Yarn为例,当Driver申请到Executor资源时,会通过CoarseGrainedExecutorBackend(其中有main方法)拉起JVM,启动一些必要的服务后等待Driver的Task下发,在还没有Task下发过来时,Executor端是没有Python进程的。当收到Driver下发过来的Task后,Executor的内部运行过程如下图所示。

Executor端收到Task后,会通过launchTask运行Task,最后会调用到PythonRDD的compute方法,来处理一个分区的数据,PythonRDD的compute方法的计算流程大致分三步走:
o 如果不存在pyspark.deamon后台Python进程,那么通过Java Process的方式启动pyspark.deamon后台进程,注意每个Executor上只会有一个pyspark.deamon后台进程,否则,直接通过Socket连接pyspark.deamon,请求开启一个pyspark.worker进程运行用户定义的Python函数或Lambda表达式。pyspark.deamon是一个典型的多进程服务器,来一个Socket请求,fork一个pyspark.worker进程处理,一个Executor上同时运行多少个Task,就会有多少个对应的pyspark.worker进程。
o 紧接着会单独开一个线程,给pyspark.worker进程喂数据,pyspark.worker则会调用用户定义的Python函数或Lambda表达式处理计算。
o 在一边喂数据的过程中,另一边则通过Socket去拉取pyspark.worker的计算结果。
把前面运行时架构图中Executor部分单独拉出来,如下图所示,橙色部分为JVM进程,白色部分为Python进程,每个Executor上有一个公共的pyspark.deamon进程,负责接收Task请求,并fork pyspark.worker进程单独处理每个Task,实际数据处理过程中,pyspark.worker进程和JVM Task会较频繁地进行本地Socket数据通信。

总结
总体上来说,PySpark是借助Py4j实现Python调用Java,来驱动Spark应用程序,本质上主要还是JVM runtime,Java到Python的结果返回是通过本地Socket完成。虽然这种架构保证了Spark核心代码的独立性,但是在大数据场景下,JVM和Python进程间频繁的数据通信导致其性能损耗较多,恶劣时还可能会直接卡死,所以建议对于大规模机器学习或者Streaming应用场景还是慎用PySpark,尽量使用原生的Scala/Java编写应用程序,对于中小规模数据量下的简单离线任务,可以使用PySpark快速部署提交。

Ubuntu下安装pyspark
1 Resilient Distributed Datasets(RDD)
弹性分布式数据集(RDD)是一个不可变的JVM对象的分布式集合,是Spark的基本抽象。
1.1 创建RDD
准备工作:

import pyspark
from pyspark import SparkContext
from pyspark import SparkConf
conf = SparkConf().setAppName(‘project1’).setMaster(‘local’)
sc = SparkContext.getOrCreate(conf)

在PySpark里有两种方法创建RDD:
一是,.parallelize(…) 个collection集合 ( list or an array of some elements)。

data = sc.parallelize([(‘amber’,22),(‘alfred’,23),(‘skye’,4),(‘albert’,12),(‘amber’,9)])

二是,引用位于本地或HDFS上的某个文件(或多个文件)。

data_from_file = sc.textFile(’/home/qml/pyspark-ex/VS14MORT.txt.gz’,4)

sc.textFile(…,n)中的最后一个参数指定数据集被分区的数量,经验是分成两个四分区

Spark 支持多种数据格式:可以使用JDBC驱动程序读取文本,Parquet,JSON,Hive表和来自关系数据库的数据。请注意,Spark可以自动处理压缩的数据集(如Gzip压缩数据集)。
从文件读取的数据表示为MapPartitionsRDD,而不是像当我们.paralellize(…)一个集合的数据一样表示为ParallelCollectionRDD。

1.2 Schema

RDD是无模式的数据结构(不像DataFrames)。因此,在使用RDD时,并行化数据集对于Spark来说是完美的。

data_heterogenous = sc.parallelize([(‘Ferrari’,‘fast’),{‘Porsche’:100000},[‘Spain’,‘visited’,4504]]).collect()
1
2
所以,我们可以混合几乎任何东西:一个元组,一个字典,或一个列表。
一旦你.collect()数据集(即,运行一个动作将其返回给驱动程序),你可以像在Python中通常那样访问对象中的数据:

data_heterogenous[1][‘Porsche’]
100000
.collect()方法将RDD的所有元素返回到驱动程序,并将其作为列表序列化。

1.3 读取文件

从文本文件读取时,文件中的每一行形成RDD的一个元素。 可以创建一个元素列表,每行代表一个值列表。

data_from_file.take(1)

1.4 Lambda表达式
1.4.1 Transformations
.map(…)
该方法应用于RDD的每个元素。
data_2014_2 = data_from_file_conv.map(lambda row: (row[16], int(row[16])))
data_2014_2.take(10)
.filter(…)
允许选择符合指定条件的数据集元素。

data_filtered = data_from_file_conv.filter(lambda row: row[5] == ‘F’ and row[21] == ‘0’)
data_filtered.count()
.flatMap(…)
与map()的工作方式类似,但返回的是平铺的结果而不是列表。
In [3]:
data_2014_flat = data_from_file_conv.flatMap(lambda row: (row[16], int(row[16]) + 1))
data_2014_flat.take(10)
.distinct()
此方法返回指定列中不同值的列表。

In [5]:
distinct_gender = data_from_file_conv.map(lambda row: row[5]).distinct().collect()
distinct_gender

.sample(…)
该方法返回数据集中的随机样本。

fraction = 0.1
data_sample = data_from_file_conv.sample(False, fraction, 666)

data_sample.take(1)

.leftOuterJoin(…)
左外连接就像SQL一样,根据两个数据集中的值加入两个RDD,并从左RDD中返回从右侧追加两个RDD匹配的记录。

rd1 = sc.parallelize([(‘a’,1),(‘b’,4),(‘c’,10)])
rd2 = sc.parallelize([(‘a’,4),(‘a’,1),(‘b’,‘6’),(‘d’,15)])
rd3 = rd1.leftOuterJoin(rd2)
print rd3.take(5)

如果我们使用.join(…)方法,那么当这两个值在这两个RDD之间相交时,我们只能得到’a’和’b’的值。

rd4 = rd1.join(rd2)
print rd4.collect()
[(‘a’, (1, 4)), (‘a’, (1, 1)), (‘b’, (4, ‘6’))]

另一个有用的方法是.intersection(…),它返回两个RDD中相同的记录。

rd5 = rd1.intersection(rd2)
print rd5.collect()
[(‘a’, 1)]

1.4.2 Actions

.take(…)
该方法从单个数据分区返回n个最高行。

In [9]:
data_first = data_from_file_conv.take(1)
data_first

.reduce(…)

rd1.map(lambda row: row[1]).reduce(lambda x,y:x+y)

.reduceByKey(…)

data_key = sc.parallelize([(‘a’, 4),(‘b’, 3),(‘c’, 2),(‘a’, 8),(‘d’, 2),(‘b’, 1),(‘d’, 3)],4)
data_key.reduceByKey(lambda x, y: x + y).collect()
[(‘b’, 4), (‘c’, 2), (‘a’, 12), (‘d’, 5)]

.count()
统计RDD中元素的数量。

data_reduce.count()
.countByKey()
如果你的数据集是键值的形式,则可以使用.countByKey()方法获取不同键的数量。

data_key.countByKey().items()
[(‘a’, 2), (‘b’, 2), (‘d’, 2), (‘c’, 1)]
.saveAsTextFile(…)
将RDD保存到文本文件:每个分区保存到一个单独的文件。

data_key.saveAsTextFile(’/Users/drabast/Documents/PySpark_Data/data_key.txt’)
1
.foreach(…)
一种将函数应用同到RDD每个元素的迭代法。

def f(x):
print(x)
data_key.foreach(f)

2 DataFrames
和Python的dataframe类似,pyspark也有dataframe,其处理速度远快于无结构的RDD。

Spark 2.0 用 SparkSession 代替了SQLContext。各种 Spark contexts,包括:
HiveContext, SQLContext, StreamingContext,以及SparkContext
全部合并到了SparkSession中,以后仅用这一个包作为读取数据的入口。

2.1 创建 DataFrames
准备工作:

import pyspark
from pyspark.sql import SparkSession
spark = SparkSession.builder
.appName(“Python Spark SQL basic example”)
.config(“spark.some.config.option”, “some-value”)
.getOrCreate()

sc = spark.sparkContext
首先创建一个 stringJSONRDD的 RDD,然后将它转为一个DataFrame。

stringJSONRDD = sc.parallelize((’’’
… {‘id’:‘123’,
… ‘name’:‘Katie’,
… ‘age’:19,
… ‘eyeColor’:‘brown’}’’’,
… ‘’’{‘id’,‘234’,
… ‘name’:‘Michael’,
… ‘age’:22,
… ‘eyeColor’:‘green’}’’’,
… ‘’’{‘id’:‘345’,
… ‘name’:‘Simone’,
… ‘age’:23,
… ‘eyeColor’:‘blue’}’’’))
3 Prepare Data for Modeling
所有的数据都是脏的,不管是从网上下载的数据集,或其他来源。直到你测试和证明你的数据处于干净状态才能用来建模。因此,为了建模需要清理数据集,还需要检查数据集的特征分布,并确认它们符合预定义的标准。

3.1 检查重复项、缺失值和异常值

重复项
生成一个简单的dataframe如下:

df = spark.createDataFrame([
… (1,144.5,5.9,33,‘M’),
… (2,167.2,5.4,45,‘M’),
… (3,124.1,5.2,23,‘F’),
… (4,144.5,5.9,33,‘M’),
… (5,133.2,5.7,54,‘F’),
… (3,124.1,5.2,23,‘F’),
… (5,129.2,5.3,42,‘M’),
… ],[‘id’,‘weight’,‘height’,‘age’,‘gender’])
显然,这个数据只有几行,一眼就可以发现是否有重复值。但对于百万级别的数据呢?
第一件事,就是用.distinct()方法检查。

print(‘count of rows: {0}’.format(df.count()))
count of rows: 7

print(‘count of distinct rows: {0}’.format(df.distinct().count()))
count of distinct rows: 6
可以用.dropna(…)方法删除缺失值,用.fillna(…) 方法替换缺失值。

means = df_miss_no_income.agg(*[fn.mean©.alias© for c in df_miss_no_income.columns if c != ‘gender’]).toPandas().to_dict(‘records’)[0]
means[‘gender’]=‘missing’
df_miss_no_income.fillna(means).show()

异常值
异常值是那些明显偏离样本其余部分分布的观测值。

一般定义为,如果所有的值大致在Q1-1.5IQR和Q3 + 1.5IQR范围内,那么可以认为没有异常值,其中IQR是四分位间距; IQR被定义为第三四分位数Q3与第一四分位数Q1的差距。

备注:

第一四分位数 (Q1),又称“较小四分位数”,等于该样本中所有数值由小到大排列后第25%的数字。
第二四分位数 (Q2),又称“中位数”,等于该样本中所有数值由小到大排列后第50%的数字。
第三四分位数 (Q3),又称“较大四分位数”,等于该样本中所有数值由小到大排列后第75%的数字。

df_outliers = spark.createDataFrame([
… ], [‘id’, ‘weight’, ‘height’, ‘age’])
用 .approxQuantile(…) 方法计算四分位数,指定的第一个参数是列的名称,第二个参数可以是介于0或1之间的数字(其中0.5意味着计算中位数)或列表(在我们的例子中),第三个参数指定可接受的度量的误差(如果设置为0,它将计算度量的精确值,但这样做可能会非常耗资源)。

cols = [‘weight’,‘height’,‘age’]
bounds={}
for col in cols:
… quantiles = df_outliers.approxQuantile(col,[0.25,0.75],0.05)
… IQR = quantiles[1]-quantiles[0]
… bounds[col] = [quantiles[0]-1.5IQR,quantiles[1]+1.5IQR]

筛选出异常值:

outliers = df_outliers.select(*[‘id’] + [
(
(df_outliers[c] < bounds[c][0]) |
(df_outliers[c] > bounds[c][1])
).alias(c + ‘_o’) for c in cols
])
outliers.show()
df_outliers = df_outliers.join(outliers, on=‘id’)
df_outliers.filter(‘weight_o’).select(‘id’, ‘weight’).show()
df_outliers.filter(‘age_o’).select(‘id’, ‘age’).show()
3.2 描述性统计

按逗号切割,并将每个元素转换为一个整数:

sc = spark.sparkContext
fraud = sc.textFile(‘ccFraud.csv.gz’)
header = fraud.first()
fraud = fraud.filter(lambda row: row != header).map(lambda row: [int(elem) for elem in row.split(’,’)])
创建dataframe的schema:

fields = [typ.StructField(h[1:-1],typ.IntegerType(),True) for h in header.split(’,’)]
schema = typ.StructType(fields)
创建dataframe:

fraud_df = spark.createDataFrame(fraud,schema)
查看schema:

fraud_df.printSchema()
root
|-- custID: integer (nullable = true)
|-- gender: integer (nullable = true)
|-- state: integer (nullable = true)
|-- cardholder: integer (nullable = true)
|-- balance: integer (nullable = true)
|-- numTrans: integer (nullable = true)
|-- numIntlTrans: integer (nullable = true)
|-- creditLine: integer (nullable = true)
|-- fraudRisk: integer (nullable = true)
用.groupby(…)方法分组统计:

fraud_df.groupby(‘gender’).count().show()
±-----±-----+
|gender|count |
±-----±-----+
| 1 |6178231|
| 2 |3821769|
±-----±-----+
用 .describe()方法对数值进行描述性统计:

numerical = [‘balance’, ‘numTrans’, ‘numIntlTrans’]
desc = fraud_df.describe(numerical)
desc.show()
1
2
3

从上面的描述性统计可以看出两点:

1)所有的特征都是正倾斜的,最大值是平均数的几倍。
2)离散系数(coefficient of variation,或变异系数)非常高,接近甚至超过1,说明数据的离散程度很大,波动范围很大。

备注:

正倾斜(positively skewed): 平均数 > 中位数,由于数据中有些很大很大的极端值,使得整体平均数被极少数的极端大值拉大了,俗称“被平均”,而中位数受极端值的影响其实很小,因而此时用中位数作为中心趋势的估计比较稳健。
负倾斜:同理。
离散系数 = 标准差 / 平均值
检查某个特征的偏度:

fraud_df.agg({‘balance’: ‘skewness’}).show()
1

常用其他函数包括:avg() , count() , countDistinct() , first() , kurtosis() , max() , mean() , min() , skewness() , stddev() , stddev_pop() , stddev_samp() , sum() , sumDistinct() , var_pop() , var_samp() and variance() 等。

特征间相互关系的另一个非常有用的度量是相关性(correlation)。

你的模型通常应该只包括那些与你的目标高度相关的特征。然而,检查这些特征之间的相关性几乎是同等重要的,最好选择的是特征之间几乎不相关,而同时特征与目标高度相关。

只要数据是DataFrame格式,在PySpark中计算相关性非常容易。唯一的困难是.corr(…)方法现在支持Pearson相关系数,而它只能计算成对的相关性,如下:

fraud_df.corr(‘balance’, ‘numTrans’)
1
创建一个相关矩阵:

n_numerical = len(numerical)
corr = []
for i in range(0, n_numerical):
temp = [None] * i
for j in range(i, n_numerical):
temp.append(fraud_df.corr(numerical[i], numerical[j]))
corr.append(temp)

可以看见特征之间几乎不存在相关性,因此,所有的特征都能用到我们的模型中。

3.3 可视化

准备工作:

%matplotlib inline
import matplotlib.pyplot as plt
直方图(Histograms) 是评估特征分布最简单的方法。
用pyspark有三种方法生成直方图:

汇总workers中的数据,并返回一个汇总的bin列表,并在直方图的每个bin中计数给driver。
将所有数据返回给driver,并允许绘图库的方法为你完成这项工作。
对数据进行采样,然后将它们返回给driver进行绘图。
如果数据是几百万行,第二种方法显然不可取。因此需要先聚合数据。

hists = fraud_df.select(‘balance’).rdd.flatMap(
lambda row: row
).histogram(20)

data = {
‘bins’: hists[0][:-1],
‘freq’: hists[1]
}
plt.bar(data[‘bins’], data[‘freq’], width=2000)
plt.title(‘Histogram of ‘balance’’)

http://www.tomdrabas.com/site/blog
Introducing MLlib
MLlib 即Machine Learning Library。
4.1 载入数据并转换数据
数据集下载:births_train.csv.gz.。
创建数据集的schema:

import pyspark.sql.types as typ

labels = [
(‘INFANT_ALIVE_AT_REPORT’, typ.StringType()),
(‘BIRTH_YEAR’, typ.IntegerType()),
(‘BIRTH_MONTH’, typ.IntegerType()),
(‘BIRTH_PLACE’, typ.StringType()),
(‘MOTHER_AGE_YEARS’, typ.IntegerType()),
(‘MOTHER_RACE_6CODE’, typ.StringType()),
(‘MOTHER_EDUCATION’, typ.StringType()),
(‘FATHER_COMBINED_AGE’, typ.IntegerType()),
(‘FATHER_EDUCATION’, typ.StringType()),
(‘MONTH_PRECARE_RECODE’, typ.StringType()),
(‘CIG_BEFORE’, typ.IntegerType()),
(‘CIG_1_TRI’, typ.IntegerType()),
(‘CIG_2_TRI’, typ.IntegerType()),
(‘CIG_3_TRI’, typ.IntegerType()),
(‘MOTHER_HEIGHT_IN’, typ.IntegerType()),
(‘MOTHER_BMI_RECODE’, typ.IntegerType()),
(‘MOTHER_PRE_WEIGHT’, typ.IntegerType()),
(‘MOTHER_DELIVERY_WEIGHT’, typ.IntegerType()),
(‘MOTHER_WEIGHT_GAIN’, typ.IntegerType()),
(‘DIABETES_PRE’, typ.StringType()),
(‘DIABETES_GEST’, typ.StringType()),
(‘HYP_TENS_PRE’, typ.StringType()),
(‘HYP_TENS_GEST’, typ.StringType()),
(‘PREV_BIRTH_PRETERM’, typ.StringType()),
(‘NO_RISK’, typ.StringType()),
(‘NO_INFECTIONS_REPORTED’, typ.StringType()),
(‘LABOR_IND’, typ.StringType()),
(‘LABOR_AUGM’, typ.StringType()),
(‘STEROIDS’, typ.StringType()),
(‘ANTIBIOTICS’, typ.StringType()),
(‘ANESTHESIA’, typ.StringType()),
(‘DELIV_METHOD_RECODE_COMB’, typ.StringType()),
(‘ATTENDANT_BIRTH’, typ.StringType()),
(‘APGAR_5’, typ.IntegerType()),
(‘APGAR_5_RECODE’, typ.StringType()),
(‘APGAR_10’, typ.IntegerType()),
(‘APGAR_10_RECODE’, typ.StringType()),
(‘INFANT_SEX’, typ.StringType()),
(‘OBSTETRIC_GESTATION_WEEKS’, typ.IntegerType()),
(‘INFANT_WEIGHT_GRAMS’, typ.IntegerType()),
(‘INFANT_ASSIST_VENTI’, typ.StringType()),
(‘INFANT_ASSIST_VENTI_6HRS’, typ.StringType()),
(‘INFANT_NICU_ADMISSION’, typ.StringType()),
(‘INFANT_SURFACANT’, typ.StringType()),
(‘INFANT_ANTIBIOTICS’, typ.StringType()),
(‘INFANT_SEIZURES’, typ.StringType()),
(‘INFANT_NO_ABNORMALITIES’, typ.StringType()),
(‘INFANT_ANCEPHALY’, typ.StringType()),
(‘INFANT_MENINGOMYELOCELE’, typ.StringType()),
(‘INFANT_LIMB_REDUCTION’, typ.StringType()),
(‘INFANT_DOWN_SYNDROME’, typ.StringType()),
(‘INFANT_SUSPECTED_CHROMOSOMAL_DISORDER’, typ.StringType()),
(‘INFANT_NO_CONGENITAL_ANOMALIES_CHECKED’, typ.StringType()),
(‘INFANT_BREASTFED’, typ.StringType())
]

schema = typ.StructType([
typ.StructField(e[0], e[1], False) for e in labels
])

猜你喜欢

转载自blog.csdn.net/u014033218/article/details/86560981