pyspark應用技巧

1. spark sdf和pandas pdf相互轉化

一般spark sdf轉化爲pandas pdf使用sdf.toPandas(), pdf轉化爲sdf使用spark.createDataFrame(pdf),但是直接轉化中間的序列化和反序列化耗時很長,所以在執行轉化的時候使用apache arrow進行加速

pyarrow版本 >= 0.8.0

spark-defaults.conf文件添加:

spark.sql.execution.arrow.enabled true

或者在設置spark conf時設置:

conf = SparkConf().setAppName("Test").setMaster("local[*]")
conf.set("spark.sql.execution.arrow.enabled", True)

別人的對比:

execution.arrow.enabled pdf -> sdf sdf -> pdf
false 4980ms 722ms
true 72ms 79ms

tips: 儘管轉化速度提高了,但pdf是單核運算,並沒有用到分佈式處理,所以最好不要處理大數據量。
當計算不適用於用arrow優化的時候可以自動退回非arrow優化的方式,這是配置參數爲spark.sql.execution.arrow.fallback.enabled

每批進行向量化計算的數據量由spark.sql.execution.arrow.maxRecordsPerBatch參數控制,默認10000條

2. sdf構建自定義函數時優先使用pandas_udf而不是udf

pandas udf建立在Apache arrow之上,帶來了低開銷, 高性能的udf,並且使用了pandas的向量化操作;而spark的udf是對每一條數據進行操作,這樣就帶來了性能的問題。但是pandas udf有一些數據類型不支持,例如:BinaryType,MapType, TimestampType 和嵌套的 StructType。

注意:有些低級的pyarrow版本在使用pandas_udf時會出錯,因此最好使用比較高一點的版本
下面所有代碼運行於linux系統中,python3.5包:numpy (1.17.0),pandas (0.25.2),pyarrow (0.13.0)

from pyspark import SparkConf
from pyspark.sql import SparkSession, Row
from pyspark.sql.functions import pandas_udf, PandasUDFType
import pyspark.sql.functions as F
from pyspark.sql.types import StringType

conf = SparkConf().setAppName("test").setMaster("local")
spark = SparkSession.builder.config(conf=conf).getOrCreate()

SCALAR

one or more pandas.Series -> one pandas.Series, 長度必須和原來的一致,2.4.3不支持MapType和StructType.
與dataframe.withColumn或dataframe.select一起使用

df = spark.createDataFrame([(1, 'goods'), (1, 'good'), (1, 'god'), (2, 'thanks'), (2, 'thank')], schema=['x', 'y'])
# to upper strings
@pandas_udf(StringType(), PandasUDFType.SCALAR)
def to_upper(s):
    return s.str.upper()

df.select(df.x, to_upper(df.y)).show()  # 1


df = spark.createDataFrame([[1, 2, 4], [-1, 2, 2]], ['a', 'b', 'c'])
# input multi-pandas.Series, pay attention to the returnType
@pandas_udf('double', PandasUDFType.SCALAR)
def fun_function(a, b, c):
    clip = lambda x: x.where(a >= 0, 0)
    return (clip(a) - clip(b)) / clip(c)

df.withColumn('d', fun_function(df.a, df.b, df.c)).show()  # 2


df = spark.createDataFrame([(1, [1, 2, 3]), (2, [3, 4, 5])], schema=['x', 'y'])
# process ArrayType
@pandas_udf(ArrayType(IntegerType()), PandasUDFType.SCALAR)
def lens(s):
    a = s.apply(lambda x: x * 2)
    return a

df.select(df.x, lens(df.y)).show()  # 3
1.
+---+-----------+                                                               
|  x|to_upper(y)|
+---+-----------+
|  1|      GOODS|
|  1|       GOOD|
|  1|        GOD|
|  2|     THANKS|
|  2|      THANK|
+---+-----------+
2.
+---+---+---+-----+
|  a|  b|  c|    d|
+---+---+---+-----+
|  1|  2|  4|-0.25|
| -1|  2|  2| null|
+---+---+---+-----+
3.
+---+----------+
|  x|   lens(y)|
+---+----------+
|  1| [2, 4, 6]|
|  2|[6, 8, 10]|
+---+----------+

GROUPED_MAP

one DataFrame -> one transformed DataFrame, 字段類型必須和原來數據一致對應,字段標籤也必須一致對應

一般與GroupedData.apply一起使用

df = spark.createDataFrame([(1, 'goods'), (1, 'good'), (1, 'god'), (2, 'thanks'), (2, 'thank')], schema=['x', 'y'])
# the return type should be same with df
@pandas_udf("x int, y string", PandasUDFType.GROUPED_MAP)
def lens(pdf):
    y = pdf.y
    return pdf.assign(y=str(len(y)))

df.groupBy('x').apply(lens).show()  # 1


df = spark.createDataFrame([(1, [1, 2, 3]), (2, [3, 4, 5])], schema=['x', 'y'])
# use schema as returnType
@pandas_udf(df.schema, PandasUDFType.GROUPED_MAP)
def lens(pdf):
    y = pdf.y
    return pdf.assign(y=y*2)

df.groupBy('x').apply(lens).show()  # 2
   1.
   +---+---+                                                                       
   |  x|  y|
   +---+---+
   |  1|  3|
   |  1|  3|
   |  1|  3|
   |  2|  2|
   |  2|  2|
   +---+---+
   2.
   +---+----------+                                                                
   |  x|         y|
   +---+----------+
   |  1| [2, 4, 6]|
   |  2|[6, 8, 10]|
   +---+----------+

   

GROUPED_AGG

One or more pandas.Series -> A scalar,returnType必須是主類型,例如DoubleType,返回的常量可以是python的主類型(int, float)或者是numpy的數據類型(numpy.int64, numpy.float64),2.4.3不支持MapType和StructType.

一般與pyspark.sql.GroupedData.agg()pyspark.sql.Window一起使用

df = spark.createDataFrame([(1, 10), (1, 20), (1, 30), (2, 15), (2, 35)], schema=['x', 'y'])

@pandas_udf('float', PandasUDFType.GROUPED_AGG)
def gro(x):
    return x.mean()

df.groupBy('x').agg(gro(df.y)).show()  # 1

df = spark.createDataFrame([(1, [2, 3, 4], [1, 2, 3]), (1, [2, 3, 4], [2, 3, 4]), (2, [2, 3, 4], [3, 4, 5])], schema=['x', 'y', 'z'])

@pandas_udf("float", PandasUDFType.GROUPED_AGG)
def lens(y, z):
    a = 0
    b = 0
    for i in y:
        a += i.sum()
    for i in z:
        b += i.sum()
    return a + b

df.groupBy('x').agg(lens(df.y, df.z)).show()  # 2
   1.
   +---+------+                                                                    
   |  x|gro(y)|
   +---+------+
   |  1|  20.0|
   |  2|  25.0|
   +---+------+
   2.
   +---+----------+
   |  x|lens(y, z)|
   +---+----------+
   |  1|      33.0|
   |  2|      21.0|
   +---+----------+

向UDF傳入其他參數

由於一些實際應用上的原因,需要向pandas_udf傳入其他的參數,第一想到的就是使用偏函數functools.partial,但是使用functools.partial封裝pandas_udf是一種錯誤的方法,例如,我想在pandas_udf中傳入一個額外的z參數:

df = spark.createDataFrame([(1, 2), (1, 4), (2, 6), (2, 4)], schema=["x", "y"])

@pandas_udf(df.schema, PandasUDFType.GROUPED_MAP)
def f(pdf, z):
    y = pdf.y * 2 + z
    return pdf.assign(y=y)

df.groupBy(df.x).apply(partial(f, z=100)).show()

在函數f中有兩個參數,而在pandas_udf裝飾器中只有一個參數的返回類型,在使用functools.partial時會出現AttributeError: 'functools.partial' object has no attribute 'evalType'這個錯誤。

一種正確的方法就是使用另一個函數封裝這個pandas_udf,並返回它:

df = spark.createDataFrame([(1, 2), (1, 4), (2, 6), (2, 4)], schema=["x", "y"])

def f(z):
    @pandas_udf(df.schema, PandasUDFType.GROUPED_MAP)
    def _internal_udf(pdf):
        y = pdf.y * 2 + z
        return pdf.assign(y=y)
    return _internal_udf

df.groupBy(df.x).apply(f(z=100)).show()

3. 使用Java UDF

PySpark: Java UDF Integration,建立好Java udf,生成jar包xxx.jar,運行spark-submit -jars xxx.jar pyspark_demo.py

4. 分發文件至spark的各個worker

當運行一個python項目的時候,特別是在linux系統下運行項目時,運行中找不到自定義模塊可能是比較大的一個問題(解決這個問題最簡單的方法就是以包的方式把整個項目安裝到python中去。當然還要考慮項目包的衝突問題,但這個容易解決)。一般的方式就是添加文件執行路徑,在linux shell中運行python文件,它是以當前路徑進行文件查找的,爲了適應在各個路徑運行該python文件能夠成功查找到它所依賴的文件,則需要在該python文件添加絕對路徑,例如:

project
   |------base
   |	  |------__init__.py
   |	  |------a.py
   |		     |------class A
   |------utils
   |        |------__init__.py
   |        |------b.py
   |               |------class B
   |------main
   |        |------__init__.py
   |        |------test.py
# a.py
from utils.b import B

class A(object):
    def __init__(self):
        c = B()
        cc = c.get(4)
        self.b = cc

    def get(self):
        return self.b
# b.py
class B(object):
    def get(self, x):
        return x
# test.py
from base.a import A

if __name__ == "__main__":
    aa = A()
    print(aa.get())

project項目下有base和utils和main三個包,並把project部署到了linux的/home/aaa下(/home/aaa/project),現在a.py用到了b.py下的class B,現在要在test.py下測試a.py,如果在main下直接運行test.py會出現ImportError: No module named base’,因爲現在它只搜索mian路徑下有沒有XXX,而不是從project下搜索。那麼現在添加sys.path.append("../"),再次在main下運行test.py則會成功。

現在的a.py:

import sys

sys.path.append("../")
from base.a import A

if __name__ == "__main__":
    aa = A()
    print(aa.get())

現在在main的上一級目錄(即project下)運行test.py怎麼樣哪?運行python ./main/test.py這時仍出現ImportError: No module named 'base',現在即使加上sys.path.append("../")也無用,因爲他會從當前project路徑向上一級路徑查找。這時可以使用絕對路徑:

import sys
import os
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../"))
print("當前路徑{}".format(os.getcwd()))
print("查找路徑{}".format(sys.path))
from base.a import A

if __name__ == "__main__":
    aa = A()
    print(aa.get())

這樣,在任意路徑下運行python /arbitrary/path/project/main/test.py都能成功運行。

以上是在linux中查找依賴文件的問題的解決方法,但是在spark中又出現了新的問題,當使用這種方法時,spark無法把這種路徑傳遞到各個worker中去,如果在各個worker中需要一些其他依賴文件的時候,上述方法仍然失效,仍會出現ImportError: No module named 'XXX',這時就需要把所依賴的文件分發到各個worker中去,在pyspark中使用的是addFileaddPyFile方法。

首先添加依賴的文件:

# 使用默認的sc, spark
sc.addPyFile("your/pyFile/path/a.py")  # 也可以是包含多個py文件的zip文件,省的一個一個添加

然後在map或其他算子函數內添加文件路徑及導入包:

sys.path.insert(0, pyspark.SparkFiles.getRootDirectory())
from a import A

這樣就能在worker中成功運行。

綜上,最簡單的方法就是把所有的包直接安裝到python上,無需添加路徑及使用addPyFile了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章