web-dev-qa-db-fra.com

Application de fonctions définies par l'utilisateur sur GroupedData dans PySpark (avec un exemple fonctionnant en python)

J'ai ce code python qui s'exécute localement dans une base de données pandas:

df_result = pd.DataFrame(df
                          .groupby('A')
                          .apply(lambda x: myFunction(Zip(x.B, x.C), x.name))

Je voudrais exécuter ceci dans PySpark, mais avoir des difficultés à gérer les objets pyspark.sql.group.GroupedData.

J'ai essayé ce qui suit:

sparkDF
 .groupby('A')
 .agg(myFunction(Zip('B', 'C'), 'A')) 

qui retourne

KeyError: 'A'

Je suppose que "A" n’est plus une colonne et que je ne trouve pas l’équivalent de x.name.

Et alors

sparkDF
 .groupby('A')
 .map(lambda row: Row(myFunction(Zip('B', 'C'), 'A'))) 
 .toDF()

mais obtenez l'erreur suivante:

AttributeError: 'GroupedData' object has no attribute 'map'

Toutes les suggestions seraient vraiment appréciées!

19
arosner09

Ce que vous essayez d’écrire, c’est d’écrire un UDAF (fonction définie par l’utilisateur) par opposition à un UDF (fonction définie par l’utilisateur). Les UDAF sont des fonctions qui fonctionnent sur des données regroupées par une clé. Plus précisément, ils doivent définir comment fusionner plusieurs valeurs du groupe dans une seule partition, puis comment fusionner les résultats entre les partitions pour la clé. Il n’existe actuellement aucun moyen en python d’implémenter un UDAF, ils ne peuvent être implémentés qu’en Scala. 

Mais vous pouvez le contourner en Python. Vous pouvez utiliser collect set pour rassembler vos valeurs groupées, puis utiliser un fichier UDF standard pour en faire ce que vous voulez. Le seul inconvénient est que collect_set ne fonctionne que sur des valeurs primitives. Vous devrez donc les encoder en chaîne.

from pyspark.sql.types import StringType
from pyspark.sql.functions import col, collect_list, concat_ws, udf

def myFunc(data_list):
    for val in data_list:
        b, c = data.split(',')
        # do something

    return <whatever>

myUdf = udf(myFunc, StringType())

df.withColumn('data', concat_ws(',', col('B'), col('C'))) \
  .groupBy('A').agg(collect_list('data').alias('data'))
  .withColumn('data', myUdf('data'))

Utilisez collect_set si vous souhaitez effectuer la déduplication. De plus, si vous avez beaucoup de valeurs pour certaines de vos clés, cela sera lent car toutes les valeurs d'une clé devront être collectées dans une seule partition quelque part sur votre cluster. Si votre résultat final est une valeur que vous créez en combinant les valeurs par clé (par exemple, en les additionnant), il peut être plus rapide de l'implémenter à l'aide de la méthode RDD aggregByKey qui vous permet de générer une valeur intermédiaire pour chaque clé. dans une partition avant de mélanger les données.

EDIT: 21/11/2018

Depuis que cette réponse a été écrite, pyspark a ajouté le support pour UDAF à l’aide de Pandas. Il y a quelques améliorations de performances de Nice lors de l'utilisation des UDF et UDAF du Panda sur des fonctions python directes avec des RDD. Sous le capot, il vectorise les colonnes (combine les valeurs de plusieurs lignes pour optimiser le traitement et la compression). Jetez un oeil à ici pour une meilleure explication ou regardez la réponse de user6910411 ci-dessous pour un exemple.

31
Ryan Widmaier

Depuis Spark 2.3, vous pouvez utiliser pandas_udf. GROUPED_MAP prend Callable[[pandas.DataFrame], pandas.DataFrame] ou, en d'autres termes, une fonction qui mappe de Pandas DataFrame de la même forme que l'entrée, à la sortie DataFrame.

Par exemple, si les données ressemblent à ceci:

df = spark.createDataFrame(
    [("a", 1, 0), ("a", -1, 42), ("b", 3, -1), ("b", 10, -2)],
    ("key", "value1", "value2")
)

et si vous voulez calculer la valeur moyenne de paire par minute entre value1value2, vous devez définir le schéma de sortie:

from pyspark.sql.types import *

schema = StructType([
    StructField("key", StringType()),
    StructField("avg_min", DoubleType())
])

pandas_udf:

import pandas as pd

from pyspark.sql.functions import pandas_udf
from pyspark.sql.functions import PandasUDFType

@pandas_udf(schema, functionType=PandasUDFType.GROUPED_MAP)
def g(df):
    result = pd.DataFrame(df.groupby(df.key).apply(
        lambda x: x.loc[:, ["value1", "value2"]].min(axis=1).mean()
    ))
    result.reset_index(inplace=True, drop=False)
    return result

et l'appliquer:

df.groupby("key").apply(g).show()
+---+-------+
|key|avg_min|
+---+-------+
|  b|   -1.5|
|  a|   -0.5|
+---+-------+

En excluant la définition de schéma et le décorateur, votre code Pandas actuel peut être appliqué tel quel.

Depuis Spark 2.4.0, il existe également GROUPED_AGG variant, qui prend Callable[[pandas.Series, ...], T], où T est un scalaire primitif:

import numpy as np

@pandas_udf(DoubleType(), functionType=PandasUDFType.GROUPED_AGG)
def f(x, y):
    return np.minimum(x, y).mean()

qui peut être utilisé avec la construction standard group_by/agg:

df.groupBy("key").agg(f("value1", "value2").alias("avg_min")).show()
+---+-------+
|key|avg_min|
+---+-------+
|  b|   -1.5|
|  a|   -0.5|
+---+-------+

Veuillez noter que ni GROUPED_MAP ni GROUPPED_AGGpandas_udf ne se comportent de la même manière que UserDefinedAggregateFunction ou Aggregator et que cela est plus proche de groupByKey ou de fonctions de fenêtre avec un cadre non limité. Les données sont d'abord mélangées, et ensuite seulement, UDF est appliqué.

Pour une exécution optimisée, vous devez implémenter Scala UserDefinedAggregateFunction et ajouter un wrapper Python .

Voir aussi Fonction définie par l'utilisateur à appliquer à la fenêtre dans PySpark?

21
user6910411

Je vais prolonger la réponse ci-dessus.

Ainsi, vous pouvez implémenter la même logique que pandas.groupby (). S'appliquent dans pyspark en utilisant @pandas_udf Et qui est une méthode de vectorisation plus rapide qu'un simple udf.

from pyspark.sql.functions import pandas_udf,PandasUDFType

df3 = spark.createDataFrame(
[("a", 1, 0), ("a", -1, 42), ("b", 3, -1), ("b", 10, -2)],
("key", "value1", "value2")
)

from pyspark.sql.types import *

schema = StructType([
    StructField("key", StringType()),
    StructField("avg_value1", DoubleType()),
    StructField("avg_value2", DoubleType()),
    StructField("sum_avg", DoubleType()),
    StructField("sub_avg", DoubleType())
])

@pandas_udf(schema, functionType=PandasUDFType.GROUPED_MAP)
def g(df):
    gr = df['key'].iloc[0]
    x = df.value1.mean()
    y = df.value2.mean()
    w = df.value1.mean() + df.value2.mean()
    z = df.value1.mean() - df.value2.mean()
    return pd.DataFrame([[gr]+[x]+[y]+[w]+[z]])

df3.groupby("key").apply(g).show()

Vous obtiendrez le résultat ci-dessous:

+---+----------+----------+-------+-------+
|key|avg_value1|avg_value2|sum_avg|sub_avg|
+---+----------+----------+-------+-------+
|  b|       6.5|      -1.5|    5.0|    8.0|
|  a|       0.0|      21.0|   21.0|  -21.0|
+---+----------+----------+-------+-------+

Vous pouvez donc effectuer davantage de calculs entre les autres champs de données groupées et les ajouter à la structure de données sous forme de liste.

2
Mayur Dangar