Pyspark编程实践(运用Pandas_UDF快速改造spark代码)
前言
pandas作为一个常用的数据处理与运算的框架,以其编程灵活方便受到许多数据爱好者的喜爱。在spark2.2中也添加了Pandas_UDF这一API,使得工程师们在编写spark程序时也可以运用Pandas_UDF方法可以快速改造pandas代码转向pyspark
Pyspark和Pandas之间改进性能和互操作的核心思想是将Apache Arrow作为序列化格式,以减少PySpark和Pandas之间的开销.
Pandas_UDF是使用关键字pandas_udf作为装饰器或包装函数来定义的,不需要额外的配置. 目前,有两种类型的Pandas_UDF, 分别是Scalar(标量映射)和GroupMap(分组映射)
1.1 Scalar
Scalar Pandas UDF用于向量化标量操作. 常常与select和withColumn等函数一起使用. 在运算的时候是将pandas.Series类型数据作为输入并返回一个长度相等的pandas.Series类型数据. 具体执行流程是, Spark将列分成多批次, 并将每个批次作为数据的子集进行函数的调用,进而执行Pandas_UDF,最后将结果连接到一起.
import pandas as pd
from pyspark.sql.function import col, pandas_udf
from pyspark.sql.types import Longtype
def multiply_func(a, b):
return a * b
# col: Returns a Column based on the given column name.
multiply = pandas_udf(multiply_func, returnType=LongType())
x = pd.Series([1,2,3])
df = spark.createDataFrame(pd.DataFrame(x, columns=['x']))
df.select(multiply(col('x'), col('x'))).show()
OUT:
+-------------------+
|multiply_func(x, x)|
+-------------------+
| 1|
| 4|
| 9|
+-------------------+
1.2 Grouped Map
Grouped map(分组映射)panda_udf与groupBy().apply()一起使用,后者实现了“split-apply-combine”模式。“split-apply-combine”包括三个步骤:
- 使用DataFrame.groupBy将数据分成多个组。
- 对每个分组应用一个函数。函数的输入和输出都是pandas.DataFrame。输入数据包含每个组的所有行和列。
- 将结果合并到一个新的DataFrame中。
要使用groupBy().apply(),需要定义以下内容:
-
定义每个分组的Python计算函数,这里可以使用pandas包或者Python自带方法(可以使用pandas和python自带的方法, 这将是非常方便的)。
-
一个StructType对象或字符串,它定义输出DataFrame的格式,包括输出特征以及特征类型(需要指出数据类型)。
需要注意的是,StructType对象中的Dataframe特征顺序需要与分组中的Python计算函数返回特征顺序保持一致。
此外,在应用该函数之前,分组中的所有数据都会加载到内存,这可能导致内存不足抛出异常。
下面的例子展示了如何使用groupby().apply() 对分组中的每个值减去分组平均值。
from pyspark.sql.functions import pandas_udf, PandasUDFType
df = spark.createDataFrame(
[(1,1.0),(1,2.0),(2, 3.0),(2,5.0),(2,10.0)],
('id','v'))
@pandas_udf('id long, v double, a double', PandasUDFType.GROUPED_MAP)
def substract_mean(pdf):
pdf['v'] = pdf.v.cumsum()
pdf['a'] = pdf.v * 2
return pdf
df.groupby('id').apply(substract_mean).show()
OUT:
+---+----+----+
| id| v| a|
+---+----+----+
| 1| 1.0| 2.0|
| 1| 3.0| 6.0|
| 2| 3.0| 6.0|
| 2| 8.0|16.0|
| 2|18.0|36.0|
+---+----+----+
# 可以注意到的是, 上述的groupby操作之后的对象都是包含了多个分组key索引的, 目前还没有找到能够从
# 源头上解决这个问题的办法, 但是可以对结果去重达到这个目的(但前面这个用到cumsum操作,是不用去重的)
df.groupby('id').apply(substract_mean).distinct()
# 注意到下面的写法其实可以说是完全一致的,但是没有上述的问题,这样看来问题应该是在于用的函数类型上面
import pandas as pd
from pyspark.sql.functions import pandas_udf
from pyspark.sql.functions import PandasUDFType
df = spark.createDataFrame(
[("a", 1, 0), ("a", -1, 42), ("b", 3, -1), ("b", 10, -2)],
("key", "value1", "value2")
)
def func(x):
return x.loc[:, ["value1", "value2"]].min(axis=1).mean()
@pandas_udf(schema, functionType=PandasUDFType.GROUPED_MAP)
def g(df):
result = pd.DataFrame(df.groupby(df.key).apply(
lambda x: func(x)))
result.reset_index(inplace=True, drop=False)
result.columns = ['key', 'key_mean']
return result
df.groupby("key").apply(g).show()
OUT:
+---+--------+
|key|key_mean|
+---+--------+
| b| -1.5|
| a| -0.5|
+---+--------+
1.3 Grouped Aggregate
Grouped aggregate Pandas UDF类似于spark的聚合函数. Grouped aggregate Pandas UDF常常与groupby().agg()和pyspark.sql.window一起使用.它定义了来自一个或多个的聚合.需要注意的是,这种类型的UDF不支持部分聚合,组或窗口的所有数据都将加载到内存中。此外,目前只支持Grouped aggregate Pandas UDFs的无界窗口。
from pyspark.sql.functions import pandas_udf, PandasUDFType
from pyspark.sql import Window
df = spark.createDataFrame(
[(1, 1.0), (1, 2.0), (2, 3.0), (2, 5.0), (2, 10.0)],
("id", "v"))
@pandas_udf('double', PandasUDFType.GROUPED_AGG)
def mean_udf(v):
return v.mean()
df.groupby('id').agg(mean_udf(df['v'])).show()
OUT:
+---+-----------+
| id|mean_udf(v)|
+---+-----------+
| 1| 2.0|
| 2| 10.0|
+---+-----------+
# 这里直接用withColumn实现了分组的聚合运算
w = Window \
.partitionBy('id') \
.rowsBetween(Window.unboundedPreceding, Window.unboundedFollowing)
df.withColumn('mean_v', mean_udf(df['v'].over(w))).show()
OUT:
# +---+----+------+
# | id| v|mean_v|
# +---+----+------+
# | 1| 1.0| 1.5|
# | 1| 2.0| 1.5|
# | 2| 3.0| 6.0|
# | 2| 5.0| 6.0|
# | 2|10.0| 6.0|
# +---+----+------+
2.1 快速使用Pandas_UDF
需要注意的是schema变量里的字段名称为pandas_dfs() 返回的spark dataframe中的字段,字段对应的格式为符合spark的格式。
这里,由于pandas_dfs()功能只是选择若干特征,所以没有涉及到字段变化,具体的字段格式在进入pandas_dfs()之前已通过printSchema()打印。如果在pandas_dfs()中使用了pandas的reset_index()方法,且保存index,那么需要在schema变量中第一个字段处添加'index'字段及对应类型(下段代码注释内容)
import pandas as pd
from pyspark.sql.types import *
from pyspark.sql import SparkSession
from pyspark.sql.functions import pandas_udf, PandasUDFType
spark = SparkSession.builder.appName("demo3").config("spark.some.config.option", "some-value").getOrCreate()
df3 = spark.createDataFrame(
[(18862669710, '/未知类型', 'IM传文件', 'QQ接收文件', 39.0, '2018-03-08 21:45:45', 178111558222, 1781115582),
(18862669710, '/未知类型', 'IM传文件', 'QQ接收文件', 39.0, '2018-03-08 21:45:45', 178111558222, 1781115582),
(18862228190, '/移动终端', '移动终端应用', '移动腾讯视频', 292.0, '2018-03-08 21:45:45', 178111558212, 1781115582),
(18862669710, '/未知类型', '访问网站', '搜索引擎', 52.0, '2018-03-08 21:45:46', 178111558222, 1781115582)],
('online_account', 'terminal_type', 'action_type', 'app', 'access_seconds', 'datetime', 'outid', 'class'))
def compute(x):
result = x[
['online_account', 'terminal_type', 'action_type', 'app', 'access_seconds', 'datetime', 'outid', 'class', 'start_time', 'end_time']]
result['a'] = result['app'] + '+_+'
return result
schema = StructType([
# StructField("index", DoubleType()),
StructField("online_account", LongType()),
StructField("terminal_type", StringType()),
StructField("action_type", StringType()),
StructField("app", StringType()),
StructField("access_seconds", DoubleType()),
StructField("datetime", StringType()),
StructField("outid", LongType()),
StructField("class", LongType()),
StructField("end_time", TimestampType()),
StructField("start_time", TimestampType()),
StructField('a', StringType())
])
@pandas_udf(schema, functionType=PandasUDFType.GROUPED_MAP)
def g(df):
print('ok')
mid = df.groupby(['online_account']).apply(lambda x: compute(x))
result = pd.DataFrame(mid)
return result
# result.reset_index(inplace=True, drop=False)
# return result
df3 = df3.withColumn("end_time", df3['datetime'].cast(TimestampType()))
df3 = df3.withColumn('end_time_convert_seconds', df3['end_time'].cast('long').cast('int'))
time_diff = df3.end_time_convert_seconds - df3.access_seconds
df3 = df3.withColumn('start_time', time_diff.cast('int').cast(TimestampType()))
df3 = df3.drop('end_time_convert_seconds')
df3.printSchema()
aa = df3.groupby(['online_account']).apply(g)
aa.show()
2.2 优化Pandas_UDF
上面我们是通过Spark方法进行特征的处理,然后对处理好的数据应用@pandas_udf装饰器调用自定义函数。但这样看起来有些凌乱,因此可以把这些Spark操作都写入pandas_udf方法中。
from pyspark.sql.function import pandas_udf, PandasUDFType
import pandas as pd
from pyspark.sql.types import *
from pyspark.sql import SparkSession
spark =SparkSession.builder.appName('demo').master('local[2]').getOrCreate()
df3 = spark.createDataFrame(
[(18862669710, '/未知类型', 'IM传文件', 'QQ接收文件', 39.0, '2018-03-08 21:45:45', 178111558222, 1781115582),
(18862669710, '/未知类型', 'IM传文件', 'QQ接收文件', 39.0, '2018-03-08 21:45:45', 178111558222, 1781115582),
(18862228190, '/移动终端', '移动终端应用', '移动腾讯视频', 292.0, '2018-03-08 21:45:45', 178111558212, 1781115582),
(18862669710, '/未知类型', '访问网站', '搜索引擎', 52.0, '2018-03-08 21:45:46', 178111558222, 1781115582)],
('online_account', 'terminal_type', 'action_type', 'app', 'access_seconds', 'datetime', 'outid', 'class'))
# 把相应的操作全部写入compute操作中, 使用pandas的方法去方便解决
# 需要注意到的是, compute运算返回的是pandas.DataFrame对象
# apply虽然无法完成传参的工作, 但是可以在pandas_udf的方法引用的函数中设置参数
def compute(x, a, b, cal=False):
x['end_time'] = pd.to_datetime(x['datetime'], errors='coerce', format='%Y-%m-%d')
x['end_time_convert_seconds'] = pd.to_timedelta(x['end_time']).dt.total_seconds().astype(int)
x['start_time'] = pd.to_datetime(x['end_time_convert_seconds'] - x['access_seconds'], unit='s')
x = x.sort_values(by=['start_time'], ascending=True)
result = x[['online_account', 'terminal_type', 'action_type', 'app', 'access_seconds', 'datetime', 'outid', 'class','start_time', 'end_time']]
if cal:
result = x
return result
# pandas_UDF需要定义的schema
schema = StructType([
StructField("online_account", LongType()),
StructField("terminal_type", StringType()),
StructField("action_type", StringType()),
StructField("app", StringType()),
StructField("access_seconds", DoubleType()),
StructField("datetime", StringType()),
StructField("outid", LongType()),
StructField("class", LongType()),
StructField("start_time", TimestampType()),
StructField("end_time", TimestampType()),
])
@pandas_udf(schema, functionType=PandasUDFType.GROUPED_MAP)
def g(df):
print('ok')
mid = df.groupby(['online_account']).apply(x:compute(x))
result = pd.DataFrame(mid)
return result
aa = df3.groupby(['online_account']).apply(g)