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中使用的是addFile
和addPyFile
方法。
首先添加依賴的文件:
# 使用默認的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了!