web-dev-qa-db-fra.com

Encoder et assembler plusieurs fonctionnalités dans PySpark

J'ai une classe Python que j'utilise pour charger et traiter certaines données dans Spark. Parmi les différentes choses que je dois faire, je génère une liste de variables factices dérivées de diverses colonnes dans une trame de données Spark. Mon problème est que je ne sais pas comment définir correctement une fonction définie par l'utilisateur pour accomplir ce dont j'ai besoin.

I do possède actuellement une méthode qui, lorsqu'elle est mappée sur le RDD de la base de données sous-jacente, résout la moitié du problème (rappelez-vous qu'il s'agit d'une méthode dans une classe data_processor Plus grande):

def build_feature_arr(self,table):
    # this dict has keys for all the columns for which I need dummy coding
    categories = {'gender':['1','2'], ..}

    # there are actually two differnt dataframes that I need to do this for, this just specifies which I'm looking at, and grabs the relevant features from a config file
    if table == 'users':
        iter_over = self.config.dyadic_features_to_include
    Elif table == 'activty':
        iter_over = self.config.user_features_to_include

    def _build_feature_arr(row):
        result = []
        row = row.asDict()
        for col in iter_over:
            column_value = str(row[col]).lower()
            cats = categories[col]
            result += [1 if column_value and cat==column_value else 0 for cat in cats]
        return result
    return _build_feature_arr

Pour l'essentiel, cela signifie que, pour la trame de données spécifiée, prend les valeurs des variables catégorielles pour les colonnes spécifiées et renvoie une liste des valeurs de ces nouvelles variables factices. Cela signifie que le code suivant:

data = data_processor(init_args)
result = data.user_data.rdd.map(self.build_feature_arr('users'))

renvoie quelque chose comme:

In [39]: result.take(10)
Out[39]:
[[1, 0, 0, 0, 1, 0],
 [1, 0, 0, 1, 0, 0],
 [1, 0, 0, 0, 0, 0],
 [1, 0, 1, 0, 0, 0],
 [1, 0, 0, 1, 0, 0],
 [1, 0, 0, 1, 0, 0],
 [0, 1, 1, 0, 0, 0],
 [1, 0, 1, 1, 0, 0],
 [1, 0, 0, 1, 0, 0],
 [1, 0, 0, 0, 0, 1]]

C'est exactement ce que je veux en termes de génération de la liste des variables factices que je veux, mais voici ma question: comment puis-je (a) créer un UDF avec des fonctionnalités similaires que je peux utiliser dans une requête SQL Spark ( ou d'une autre manière, je suppose), ou (b) prendre le RDD résultant de la carte décrite ci-dessus et l'ajouter en tant que nouvelle colonne à la trame de données user_data?

Quoi qu'il en soit, ce que je dois faire est de générer une nouvelle trame de données contenant les colonnes de user_data, ainsi qu'une nouvelle colonne (appelons-la feature_array) Contenant la sortie de la fonction ci-dessus (ou quelque chose d'équivalent sur le plan fonctionnel).

23
moustachio

Spark> = 2,3,> = 3,0

Puisque Spark 2.3 OneHotEncoder est déconseillé au profit de OneHotEncoderEstimator. Si vous utilisez une version récente, veuillez modifier le code encoder

from pyspark.ml.feature import OneHotEncoderEstimator

encoder = OneHotEncoderEstimator(
    inputCols=["gender_numeric"],  
    outputCols=["gender_vector"]
)

Dans Spark 3.0, cette variante a été renommée en OneHotEncoder:

from pyspark.ml.feature import OneHotEncoder

encoder = OneHotEncoder(
    inputCols=["gender_numeric"],  
    outputCols=["gender_vector"]
)

De plus, StringIndexer a été étendu pour prendre en charge plusieurs colonnes d'entrée:

StringIndexer(inputCols=["gender"], outputCols=["gender_numeric"])

Spark <2,3

Eh bien, vous pouvez écrire un UDF mais pourquoi le feriez-vous? Il existe déjà quelques outils conçus pour gérer cette catégorie de tâches:

from pyspark.sql import Row
from pyspark.ml.linalg import DenseVector

row = Row("gender", "foo", "bar")

df = sc.parallelize([
  row("0", 3.0, DenseVector([0, 2.1, 1.0])),
  row("1", 1.0, DenseVector([0, 1.1, 1.0])),
  row("1", -1.0, DenseVector([0, 3.4, 0.0])),
  row("0", -3.0, DenseVector([0, 4.1, 0.0]))
]).toDF()

Tout d'abord StringIndexer.

from pyspark.ml.feature import StringIndexer

indexer = StringIndexer(inputCol="gender", outputCol="gender_numeric").fit(df)
indexed_df = indexer.transform(df)
indexed_df.drop("bar").show()

## +------+----+--------------+
## |gender| foo|gender_numeric|
## +------+----+--------------+
## |     0| 3.0|           0.0|
## |     1| 1.0|           1.0|
## |     1|-1.0|           1.0|
## |     0|-3.0|           0.0|
## +------+----+--------------+

OneHotEncoder suivant:

from pyspark.ml.feature import OneHotEncoder

encoder = OneHotEncoder(inputCol="gender_numeric", outputCol="gender_vector")
encoded_df = encoder.transform(indexed_df)
encoded_df.drop("bar").show()

## +------+----+--------------+-------------+
## |gender| foo|gender_numeric|gender_vector|
## +------+----+--------------+-------------+
## |     0| 3.0|           0.0|(1,[0],[1.0])|
## |     1| 1.0|           1.0|    (1,[],[])|
## |     1|-1.0|           1.0|    (1,[],[])|
## |     0|-3.0|           0.0|(1,[0],[1.0])|
## +------+----+--------------+-------------+

VectorAssembler:

from pyspark.ml.feature import VectorAssembler

assembler = VectorAssembler(
    inputCols=["gender_vector", "bar", "foo"], outputCol="features")

encoded_df_with_indexed_bar = (vector_indexer
    .fit(encoded_df)
    .transform(encoded_df))

final_df = assembler.transform(encoded_df)

Si bar contient des variables catégorielles, vous pouvez utiliser VectorIndexer pour définir les métadonnées requises:

from pyspark.ml.feature import VectorIndexer

vector_indexer = VectorIndexer(inputCol="bar", outputCol="bar_indexed")

mais ce n'est pas le cas ici.

Enfin, vous pouvez envelopper tout cela à l'aide de pipelines:

from pyspark.ml import Pipeline
pipeline = Pipeline(stages=[indexer, encoder, vector_indexer, assembler])
model = pipeline.fit(df)
transformed = model.transform(df)

On peut dire que c'est une approche beaucoup plus robuste et propre que d'écrire tout à partir de zéro. Il y a quelques mises en garde, en particulier lorsque vous avez besoin d'un codage cohérent entre différents jeux de données. Vous pouvez en savoir plus dans la documentation officielle de StringIndexer et VectorIndexer.

Une autre façon d'obtenir une sortie comparable est RFormulawhich :

RFormula produit une colonne vectorielle d'entités et une colonne double ou chaîne d'étiquette. Comme lorsque les formules sont utilisées dans R pour la régression linéaire, les colonnes d'entrée de chaîne seront codées à chaud et les colonnes numériques seront converties en doubles. Si la colonne d'étiquette est de type chaîne, elle sera d'abord transformée en double avec StringIndexer. Si la colonne d'étiquette n'existe pas dans le DataFrame, la colonne d'étiquette de sortie sera créée à partir de la variable de réponse spécifiée dans la formule.

from pyspark.ml.feature import RFormula

rf = RFormula(formula="~ gender +  bar + foo - 1")
final_df_rf = rf.fit(df).transform(df)

Comme vous pouvez le voir, c'est beaucoup plus concis, mais plus difficile à composer ne permet pas beaucoup de personnalisation. Néanmoins, le résultat pour un simple pipeline comme celui-ci sera identique:

final_df_rf.select("features").show(4, False)

## +----------------------+
## |features              |
## +----------------------+
## |[1.0,0.0,2.1,1.0,3.0] |
## |[0.0,0.0,1.1,1.0,1.0] |
## |(5,[2,4],[3.4,-1.0])  |
## |[1.0,0.0,4.1,0.0,-3.0]|
## +----------------------+


final_df.select("features").show(4, False)

## +----------------------+
## |features              |
## +----------------------+
## |[1.0,0.0,2.1,1.0,3.0] |
## |[0.0,0.0,1.1,1.0,1.0] |
## |(5,[2,4],[3.4,-1.0])  |
## |[1.0,0.0,4.1,0.0,-3.0]|
## +----------------------+

Concernant vos questions:

faire un UDF avec des fonctionnalités similaires que je peux utiliser dans une Spark requête SQL (ou d'une autre manière, je suppose)

C'est juste un UDF comme les autres. Assurez-vous d'utiliser des types pris en charge et au-delà, tout devrait fonctionner correctement.

prendre le RDD résultant de la carte décrite ci-dessus et l'ajouter en tant que nouvelle colonne à la trame de données user_data?

from pyspark.ml.linalg import VectorUDT
from pyspark.sql.types import StructType, StructField

schema = StructType([StructField("features", VectorUDT(), True)])
row = Row("features")
result.map(lambda x: row(DenseVector(x))).toDF(schema)

Remarque :

Pour Spark 1.x remplacez pyspark.ml.linalg avec pyspark.mllib.linalg.

36
zero323