Pyspark UDF (pyspark.sql.functions.pandas_udf) 经验总结

878 阅读4分钟

最近要把历史数据使用Pyspark做一次回归计算,遇到了OOM的问题,虽然可以通过加机器来解决,但是最好的办法是通过优化代码逻辑来实现,原因是之前的逻辑只是要实现一个简单的工作来做的,直接使用pandas的功能来实现。现在转成spark函数其中就使用了UDF 来实现

什么是UDF

UDF(User Defined Function) 是指用户定义的函数,由Spark使用Arrow来传输数据,并使用Pandas来处理数据,从而实现矢量化操作。使用pandas_udf,可以方便的在PySpark和Pandas之间进行互操作,并且保证性能;其核心思想是将Apache Arrow作为序列化格式。

UDF API 格式

Pandas UDF通常表现为常规的PySpark函数API,使用格式如下:

   pyspark.sql.functions.pandas_udf(f=None, returnType=None, functionType=None)
  • f: 表示这是用户定义的函数;

  • returnType: 用户自定义函数的返回值类型,该值可以是pyspark.sql.types.DataType对象或DDL格式的类型字符串

  • functionType: pyspark.sql.functions.PandasUDFType中的枚举值。 默认值:SCALAR.

    Pandas_UDF returnType类型

    returnType类型 | input | output | | --------------- | -------------- | ------------ | | SCALAR | 一个或多个pandas.Series | 一个pandas.Series | | GROUPED_MAP | pandas.DataFrame | pandas.DataFrame | | GROUPED_AGG | 一个或多个pandas.Series | SCALAR |

@pandas_udf 使用装饰器的方法来定义UDF

首先,创建一个基于Pyspark的环境,创建DataFrame

from pyspark.sql import SparkSession
from pyspark.sql.functions import pandas_udf, PandasUDFType
import pandas as pd
from pyspark.sql.types import StructType,StructField, StringType, MapType,IntegerType
spark = SparkSession.builder.appName("grouping_example").getOrCreate()

@pandas_udf('double', PandasUDFType.GROUPED_AGG)
def my_agg_func(df):
    # 在Pandas DataFrame上应用某个函数
    return df['value'].mean()
simpleData = [("James","Sales","NY",90000,34,10000),
              ("Michael","Sales","NY",86000,56,20000),
              ("Robert","Sales","CA",81000,30,23000),
              ("Maria","Finance","CA",90000,24,23000),
              ("Raman","Finance","CA",99000,40,24000),
              ("Scott","Finance","NY",83000,36,19000),
              ("Jen","Finance","NY",79000,53,15000),
              ("Jeff","Marketing","CA",80000,25,18000),
              ("Kumar","Marketing","NY",91000,50,21000)]

schema = ["employee_name","department","state","salary","age","bonus"]
df = spark.createDataFrame(simpleData, schema)
df.show()

输出结果为

image.png

使用GROUPED_AGG例子

AGG Panda UDF类似于Spark聚合函数。常常与groupBy().agg()和pyspark.sql.window一起使用。 这种类型的UDF不支持部分聚合,组或窗口的所有数据都将加载到内存中

@pandas_udf('string', functionType=PandasUDFType.GROUPED_AGG)
def my_agg_function(pdf):
#     df =  pd.concat({'age':pdf,'bonus':pdf1},axis=1)
    b = {
            'eincomsCount': 41,
            'eincomsFirstIncomeMonthRange': 56,
            'eincomsMissingMonthMax': 6,
            'eincomsLast18MonthsAmountMax': 29250.0,
            'eincomsLast18MonthsAmountMin': 24750.0,
            'eincomsLast18MonthsAmountAvg': 25031.25,
            'eincomsLast18MonthsAmountMed': -1,
            'eincomsLast18MonthsAmountStd': 1125.0,
            'eincomsLast18MonthsAmountMedian': 24750.0
    }
    return str(b).replace("'","\"")

new_df = df.groupby('department').agg(my_agg_function(df['age']).alias("eincome_factors"))
new_df.printSchema()
new_df.show(truncate=False)

image.png

PandasUDFType.SCALAR使用例子

UDF定义了一个转换,PandasUDFType.SCALAR函数可以输入一个或多个pd.Series,输出一个pd.Series,函数的输出和输入有相同的长度 Pandas UDF用于向量化标量操作。可以与select和withColumn等函数一起使用。其中调用的pandas.Series作为输入,返回一个具有相同长度的pandas.Series类型。具体执行流程是,Spark将列分成批(每批进行向量化计算的大小由 spark.sql.execution.arrow.maxRecordsPerBatch 参数控制,默认值10000),并将每个批作为数据的子集进行函数的调用,执行panda UDF,最后将结果连接在一起。

@pandas_udf(returnType=IntegerType(), functionType=PandasUDFType.SCALAR)
def total_monery(x, y):
    return x + y
    
df.withColumn("total",total_monery("salary","bonus")).show()

输出结果为:

image.png

Grouped Map使用例子

GROUPED_MAP UDF定义了转换:pd.DataFrame -> pd.DataFrame,returnType使用StructType描述返回值的pd.DataFrame的模式。
Grouped Map(分组映射)panda_udf与groupBy().apply()一起使用,基本实现思想是“split-apply-combine”模式。
“split-apply-combine”包括三个步骤:

  • 使用DataFrame.groupBy将数据分成多个组。
  • 对每个分组应用一个函数。函数的输入和输出都是pandas.DataFrame。输入数据包含每个组的所有行和列。
  • 将结果合并到一个新的DataFrame中。 要使用groupBy().apply(),需要定义以下内容:
  • 定义每个分组的Python计算函数,这里可以使用pandas包或者Python自带方法。
  • 一个StructType对象或字符串,它定义输出DataFrame的格式,包括输出特征以及特征类型。 在return 时候需要注意,如果穿进去的PDF 对象有几列,那么returnType的StructType需要做好对应的定义
# 定义的返回类型,除了传进来的"department","bonus",还有加上新生成的V
@pandas_udf('department string,bonus int ,v double', PandasUDFType.GROUPED_MAP)
def my_map_func(pdf):
    # 在Pandas DataFrame上应用某个函数
    v = pdf['bonus']
    return pdf.assign(v=(v - v.mean()) / v.std())

new_df = df.select("department","bonus").groupby('department').apply(my_map_func)
new_df.printSchema()
new_df.show(truncate=False)

输出结果:

image.png

注意!StructType对象中的Dataframe特征顺序需要与分组中的Python计算函数返回特征顺序保持一致。在应用该函数之前,分组中的所有数据都会加载到内存,这可能导致内存不足抛出异常。

 ⚠️Pandas_UDF与toPandas的区别

修改为Pandas_UDF 的也是因为toPandas性能问题

  • @pandas_udf 创建一个向量化的用户定义函数(UDF),利用了panda的矢量化特性,是udf的一种更快的替代方案,因此适用于分布式数据集。
  • toPandas将分布式spark数据集转换为pandas数据集,对pandas数据集进行本地化,并且所有数据都驻留在驱动程序内存中,因此此方法仅在预期生成的pandas DataFrame较小的情况下使用。

换句话说,@pandas_udf使用panda API来处理分布式数据集,而toPandas()将分布式数据集转换为本地数据