aws s3 獲取所有文件_交通大数据实战:AWS-EMR环境下基于PySpark的分布式云计算编程...

本文介绍了如何在AWS的EMR环境中配置Pyspark,并通过实例展示了Pyspark的常用操作,如DataFrame的创建、读取、转换等。强调了Spark的分布式计算优势和在大数据处理中的高效性能。
摘要由CSDN通过智能技术生成
写在前面

最近组里的一个项目,需要对TB级别的数据进行识别与聚合,并对运算的速度与算法的复杂度都有较高的要求。面对这种量级的数据,再考虑在本地用Python去处理,显然是不现实了,于是开始上AWS和Pyspark,熬了几个通宵后,总算把项目功能实现了——这是我第一次完全基于AWS与Spark处理如此大量级数据,我也因此真正认识到分布式计算的强大,当然,还有运算资源的昂贵。本文将简要介绍如何在AWS的EMR环境下配置自己的Pyspark,并对Pyspark的常用命令进行梳理。

什么是Pyspark,AWS&EMR

Apache Spark是一个非常著名的开源分布式计算框架,即便是在本地部署,如配置得当,也可以获得比传统计算快许多倍的计算速度,这得益于它对数据结构与索引的最优重构,对内存数据的直接处理(这也是其与Hadoop MapReduce的主要区别之一)以及分布式框架的完全融合。Spark的另一个好处在于具有很强的扩展性,经过历代大佬们的开发,与主流的数据语言如Python、R、SQL、Java、Scala等都可以无缝对接,这就意味着你可以不切换原有的语言环境,直接把他当做接口来使用——Pyspark则正是Python环境下与Spark的对接API。

AWS下的Amazon Elastic MapReduce (EMR)是亚马逊云服务器中的一个专门针对分布式云计算的平台,数据可以存储在S3中,代码则可以直接在内置的Notebooks中执行。EMR最大的优点就是方便,有多方便呢?几分钟内开好,无需配置任何spark相关的环境,直接进入Notebook就能开始跑。而缺点就是,,,贵。

0f34d606b05d259d93b84327ec0cdd21.png
开始一个Pyspark

进入AWS,在搜索框搜索EMR,登录后进入EMR的控制台,点击create cluster,开始创建分布集群。主要需要设置的Hardware,在这里你可以选择需要的计算资源。其中,Node type有Mater, Core, Task三种,Instance type则更多,每一类选中后,都会有对应的配置说明,第三列选择的是Instance的数量,选得越多,算力越大,费用也越高,最后一列是选择on-demand或者spot。

4ffa96956adfcc4034a663e997b179e9.png

建立好cluster之后,等候status变成ready(一般越大的cluster准备的时间越久),进入Notebook,新建一个Notebook,并选择刚刚建立的cluster,最后open in jupyter,则进入了熟悉的jupyter编译界面。最后注意将kernel切换为Pyspark。至此,配置工作完成。

7108d1ff377e2a9e97a8ad24414963a0.png

尝试着导入pyspark包,这些包是无需额外安装,AWS已经帮你装好了,导入成功,说明配置成功了。

from pyspark import SparkContext
from pyspark.sql.window import Window
from pyspark.sql.functions import pandas_udf, PandasUDFType, udf, struct, lag, col, when, lit, first, sha1, concat, lpad, substring, regexp_replace, countDistinct, row_number
from pyspark.sql import SparkSession, SQLContext
from pyspark.sql.types import StringType, IntegerType, FloatType, DoubleType, DecimalType, StructType, StructField

对于外部的包,AWS是没有安装的,需要自己进行安装。如安装pandas以及指定版本的pyarrow。在AWS中,sc默认代表一个SparkContext。

sc.install_pypi_package('pandas')
sc.install_pypi_package('pyarrow==0.10.0')
Spark中的数据结构及文件I/O

Spark最出名的数据结构是RDD(Resilient Distributed Datasets),但实际上RDD并非唯一的数据结构,对于需要对数据进行大量算法处理且熟悉Pandas的人来说,DataFrame或许更为合适。网上有人整理过这些数据结构的发展:

36d41676ff0318e934176298cf7903dc.png
From: https://stackoverflow.com/questions/31508083/difference-between-dataframe-dataset-and-rdd-in-spark

值得一提的是,如果习惯使用SQL,还可以将数据注册为table(通过registerTempTable()),之后就可以直接对数据写SQL了,非常的强大方便。

那么如何创建一个DataFrame呢?比如我要读取一个文件夹里的某个CSV:

state_weights = spark.read.format("csv").option("header", "True").option("delimiter", ",").load("s3://XX/XX.csv").coalesce(1)

注意,对于单个小的CSV,在读取时需要设置coalesce=1,这样可以防止它调起所有的nodes来给你计算,反而导致速度变慢了。

也可以直接读取多个压缩文件:

National_device_list = spark.read.format("csv").option("header", "False").option("delimiter", "t").load("s3n://XX/1/*.gz")

当然,如果需要直接读取为RDD,再转为DF,也是可以的(直接创建RDD可以用parallelize()):

sql_context = SQLContext(spark)
gzfile = main_dir + '*.gz' % base_week
sc_file = spark.textFile(gzfile)
csv = sc_file.map(lambda x: x.split("t"))
rows = csv.map(lambda p: Row(ID=p[0], Category=p[1], FIPS=p[2], date_idx=p[3]))
All_device_list = sql_context.createDataFrame(rows)

此外,spark中对读写数据非常推荐parquet格式,速度快且容量小:

device_weights.write.parquet("s3://XX/XX.parquet")
device_weights.write.parquet("s3://XX/XX.parquet") # TAG_OUTPUT
常见操作

SparkDF与PandasDF虽然都叫DF,但在操作层面的函数还是存在很多的不同(但也有很多是一致的),写的时候不要混淆。同时应当注意,Spark中采用的是Lazy Evaluation,这意味着只有当你执行transformation类型的命令时,真正的运算才会被触发。transformation主要包括collect,count,show,take,write等。下面整理了一些常用的命令,则主要是action类型的命令,在执行完成后,Spark可以判断该命令是否准确,但不会真正的跑完他:

  • 新建一列:withColumn()
National_device_list = National_device_list.withColumn('local_prev', utc_prev - col('utc_offset'))
  • 重命名(结合reduce对多列同时重命名,在读取数据后重命名列很有用):withColumnRenamed()
oldColumns = Nation_device_data_nextdate.schema.names
newColumns = ['Timestamp', 'Cuebiq_ID', 'Device_type', 'Latitude', 'Longitude', 'Accuracy', 'Time_zone_Offset',
                  'Classification_type', 'Transormation_type']
Nation_device_data_nextdate = reduce(lambda data, idx: data.withColumnRenamed(oldColumns[idx], newColumns[idx]),
                                         range(len(oldColumns)), Nation_device_data_nextdate)
  • 连接:join(),和PandasDF的merge一致
Device_home = Device_home.join(acs, 'CTFIPS', how='left')
  • 选择列并去重,类似于PandasDF中的df[['','']].drop_duplicates()
IDList = point_data.select('Cuebiq_ID').distinct()
  • 选择列并重命名:alias()
trips=trips.select(col('Latitude').alias('Latitude_2'))
  • 数据类型转变。类似于PandasDF的astype()
gpsdata=gpsdata.withColumn('Latitude', gpsdata['Latitude'].cast("float"))
  • 填充NA,与PandasDF一致:
gpsdata = gpsdata.fillna(0, subset=['speedfrom', 'timefrom', 'distfrom'])
  • 条件选择,可以用select,where,filter:
Nation_device_data_nextdate = Nation_device_data_nextdate.where((col('Timestamp') >= col('local_prev')) & (
            col('Timestamp') < col('local_now'))) 
trip_rosters = trip_rosters.filter(trip_rosters.trip_distance_mile > 0.186411)
  • 合并,类似于concat:
point_data = Nation_device_data_nextdate.union(Nation_device_data).drop_duplicates()
  • 去重,可以直接用drop_duplicates()
point_data = point_data.drop_duplicates(subset=['Cuebiq_ID', 'Timestamp'])
  • 用常数填充列:lit()
state_weights = state_weights.withColumn('trip_weight', lit(0.9176589))
  • 用0补全位数,比如35变成00035:
COUNTY = COUNTY.withColumn("CTFIPS", lpad("CTFIPS", 5, '0'))
  • 两列字符串相连形成新的一列:
trips = trips.withColumn('trip_id', concat(col('Cuebiq_ID'), lit('_'), col('trip_id')))
  • 条件语句:when ... otherwise:
gpsdata = gpsdata.withColumn("speedfrom", when(col("Shift_Longitude").isNull(), 0).otherwise(
        gpsdata["distfrom"] / gpsdata["timefrom"]))
  • 字符串切片,注意spark的字符串起点为1,比如从头往后切5个字符:
National_device_list = National_device_list.withColumn("CTFIPS", substring('BGFIPS', 1, 5))
  • 转为PandasDF
Ava_weeks=National_device_list.select("Week").distinct().toPandas()
Window操作

Window操作相当于定义了一个移动的窗口,这个窗口将按照DF的顺序进行移动,并对每一块进行对应的函数命令。Window相比于groupby更为灵活,合理使用的话可以减少很多的代码量。下图显示了一个简单的窗口:

207d2d8f9731a570b37e058e0c7bea29.png
From: https://databricks.com/blog/2015/07/15/introducing-window-functions-in-spark-sql.html

一个简单的例子就是基于某一个ID逐行相减。首先建立一个Window的规则,以ID拆分为组,并按照Timestamp排序。然后,对每一个windows,对Timestamp做一个滞后,然后与原始列进行相减。

W_Dis = Window.partitionBy("ID").orderBy('Timestamp')
gpsdata = gpsdata.withColumn('timefrom', - gpsdata['Timestamp'] + lag('Timestamp', -1).over(W_Dis)))

当然也可以进一步简化,将Window做为rowsBetween(-1,0)的形状,这样将lag的操作也在windows里面实现了:

windowSpec=Window.partitionBy('id').orderBy('start').rowsBetween(-1,0)
gpsdata = gpsdata.withColumn('timefrom', (gpsdata['Timestamp'] - first('Timestamp').over(windowSpec)))
udf

udf的好处在于,可以直接将在Pandas中建立的函数原封不动的移植到spark环境中,对于习惯使用Pandas或者原始代码在Pandas中的用户十分友好,但坏处则在于损失了一定的运算速度。一个udf常常由一个装饰器、一个函数、一个apply组成。

下面的代码显示了一个挂着pandas_udf装饰器的pandas函数。pandas_udf与传统的udf相比,主要胜在对向量化运算以及groupby(UDAF)的处理(参考https://databricks.com/blog/2017/10/30/introducing-vectorized-udfs-for-pyspark.html)。

其中schema 规定了return的DF的每一列的数据类型及名称,装饰器为GROUPED_MAP,说明这个函数之后可以对groupby后的结构进行计算。schema一定要定义对,不然很容易出错或者返回不了值(查询数据结构可以用XX.dtypes)。

schema = StructType([StructField("Cuebiq_ID", StringType()),StructField("Timestamp", IntegerType()),StructField("Latitude", FloatType()),
    StructField("Longitude", FloatType()),StructField("utc_offset", IntegerType())])
@pandas_udf(schema, PandasUDFType.GROUPED_MAP)
def TripInd(devicedata0):
    devicedata = devicedata0.sort_values(by=['Timestamp']).reset_index(drop=True)
    first_row = list(devicedata.index)[0]
    last_row = list(devicedata.index)[-1]
    if devicedata.loc[first_row, 'speedfrom'] > speed_threshold:
        devicedata.loc[first_row, 'trip_id'] = hash_generator()
    else:
        devicedata.loc[first_row, 'trip_id'] = "0"
    return devicedata[
        ['Cuebiq_ID', 'Timestamp', 'Latitude', 'Longitude', 'utc_offset']]

如何将其应用到Spark的DF上呢,非常简单,一行代码即可。注意,在AWS上可能会因为pyarrow版本问题无法使用Pandasudf,这时需要指定版本对pyarrow进行安装:sc.install_pypi_package('pyarrow==0.10.0')

gpsdata = gpsdata.groupby("Cuebiq_ID").apply(TripInd)

当然了,Spark本身的UDF也可以满足大部分的需求,但缺点在于row-by-row的运算规则,在一定程度上对速度并不友好。一个简单的例子就是我需要用四列的每一行计算距离,我先写了一个udf叫做Calculate_Distance,然后再将其apply到spark的一个DF上:

Calculate_Distance = udf(
        lambda row: geodesic((row['Shift_Latitude'], row['Shift_Longitude']), (row['Latitude'], row['Longitude'])).meters)
gpsdata = gpsdata.withColumn("distfrom", when(col("Shift_Longitude").isNull(), 0).otherwise(
        Calculate_Distance(struct([gpsdata[x] for x in ['Shift_Latitude', 'Shift_Longitude', 'Latitude', 'Longitude']]))))
小结

Spark还有很多的奇技淫巧值得长时间的摸索,但不得不说,Spark在算力上已经完完全全把我征服了,本地几天都跑不完或者根本没法跑的数据,通过Spark不到一小时就能跑出结果。希望之后还能有空继续深入研究与更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值