web-dev-qa-db-ja.com

TypeError:列は反復可能ではありません-ArrayType()を反復する方法は?

次のDataFrameについて考えます。

_+------+-----------------------+
|type  |names                  |
+------+-----------------------+
|person|[john, sam, jane]      |
|pet   |[whiskers, rover, fido]|
+------+-----------------------+
_

これは、次のコードで作成できます。

_import pyspark.sql.functions as f
data = [
    ('person', ['john', 'sam', 'jane']),
    ('pet', ['whiskers', 'rover', 'fido'])
]

df = sqlCtx.createDataFrame(data, ["type", "names"])
df.show(truncate=False)
_

udfを使用せずに各要素に関数を適用することにより、ArrayType()列_"names"_を直接変更する方法はありますか?

たとえば、関数fooを_"names"_列に適用したいとします。 (説明のためにfooが_str.upper_である例を使用しますが、私の質問は、イテラブルの要素に適用できる有効な関数に関するものです。)

_foo = lambda x: x.upper()  # defining it as str.upper as an example
df.withColumn('X', [foo(x) for x in f.col("names")]).show()
_

TypeError:列は反復可能ではありません

udfを使用してこれを行うことができます:

_foo_udf = f.udf(lambda row: [foo(x) for x in row], ArrayType(StringType()))
df.withColumn('names', foo_udf(f.col('names'))).show(truncate=False)
#+------+-----------------------+
#|type  |names                  |
#+------+-----------------------+
#|person|[JOHN, SAM, JANE]      |
#|pet   |[WHISKERS, ROVER, FIDO]|
#+------+-----------------------+
_

この特定の例では、I could列を分解してudfを回避し、pyspark.sql.functions.upper()を呼び出してから、groupByおよび_collect_list_を呼び出します。 :

_df.select('type', f.explode('names').alias('name'))\
    .withColumn('name', f.upper(f.col('name')))\
    .groupBy('type')\
    .agg(f.collect_list('name').alias('names'))\
    .show(truncate=False)
#+------+-----------------------+
#|type  |names                  |
#+------+-----------------------+
#|person|[JOHN, SAM, JANE]      |
#|pet   |[WHISKERS, ROVER, FIDO]|
#+------+-----------------------+
_

しかし、これは単純なことを行うための多くのコードです。 spark-dataframe関数を使用してArrayType()の要素を反復するより直接的な方法はありますか?

11
pault

Spark <2.4では、ユーザー定義関数を使用できます。

from pyspark.sql.functions import udf
from pyspark.sql.types import ArrayType, DataType, StringType

def transform(f, t=StringType()):
    if not isinstance(t, DataType):
       raise TypeError("Invalid type {}".format(type(t)))
    @udf(ArrayType(t))
    def _(xs):
        if xs is not None:
            return [f(x) for x in xs]
    return _

foo_udf = transform(str.upper)

df.withColumn('names', foo_udf(f.col('names'))).show(truncate=False)
+------+-----------------------+
|type  |names                  |
+------+-----------------------+
|person|[JOHN, SAM, JANE]      |
|pet   |[WHISKERS, ROVER, FIDO]|
+------+-----------------------+

explode + collect_list イディオムの高コストを考えると、このアプローチは、本質的なコストにもかかわらず、ほぼ独占的に推奨されます。

Spark 2.4以降では、 transform *を upper とともに使用できます(参照 SPARK-23909 ):

from pyspark.sql.functions import expr

df.withColumn(
    'names', expr('transform(names, x -> upper(x))')
).show(truncate=False)
+------+-----------------------+
|type  |names                  |
+------+-----------------------+
|person|[JOHN, SAM, JANE]      |
|pet   |[WHISKERS, ROVER, FIDO]|
+------+-----------------------+

pandas_udfを使用することもできます

from pyspark.sql.functions import pandas_udf, PandasUDFType

def transform_pandas(f, t=StringType()):
    if not isinstance(t, DataType):
       raise TypeError("Invalid type {}".format(type(t)))
    @pandas_udf(ArrayType(t), PandasUDFType.SCALAR)
    def _(xs):
        return xs.apply(lambda xs: [f(x) for x in xs] if xs is not None else xs)
    return _

foo_udf_pandas = transform_pandas(str.upper)

df.withColumn('names', foo_udf(f.col('names'))).show(truncate=False)
+------+-----------------------+
|type  |names                  |
+------+-----------------------+
|person|[JOHN, SAM, JANE]      |
|pet   |[WHISKERS, ROVER, FIDO]|
+------+-----------------------+

ただし、最新のArrow/PySparkの組み合わせのみがArrayType列の処理をサポートしています( SPARK-24259SPARK-21187 )。それでも、このオプションは標準のUDFよりも効率的で(特にserdeオーバーヘッドが低い)、任意のPython関数をサポートしています。


* 他の多くの高次関数もサポートされていますfilter および aggregate を含みますが、これらに限定されません。例を見る

はい、RDDに変換してからDFに戻すことで実行できます。

>>> df.show(truncate=False)
+------+-----------------------+
|type  |names                  |
+------+-----------------------+
|person|[john, sam, jane]      |
|pet   |[whiskers, rover, fido]|
+------+-----------------------+

>>> df.rdd.mapValues(lambda x: [y.upper() for y in x]).toDF(["type","names"]).show(truncate=False)
+------+-----------------------+
|type  |names                  |
+------+-----------------------+
|person|[JOHN, SAM, JANE]      |
|pet   |[WHISKERS, ROVER, FIDO]|
+------+-----------------------+
1
Bala