web-dev-qa-db-fra.com

Comment convertir une colonne de tableau (liste, par exemple) en vecteur

Version courte de la question!

Considérez le fragment de code suivant (en supposant que spark est déjà défini sur certains SparkSession):

from pyspark.sql import Row
source_data = [
    Row(city="Chicago", temperatures=[-1.0, -2.0, -3.0]),
    Row(city="New York", temperatures=[-7.0, -7.0, -5.0]), 
]
df = spark.createDataFrame(source_data)

Notez que le champ de températures est une liste de flotteurs. Je voudrais convertir ces listes de floats au type MLlib Vector, et j'aimerais que cette conversion soit exprimée à l'aide de l'API de base DataFrame plutôt que de passer par des RDD (ce qui est inefficace, il envoie toutes les données de la machine virtuelle à Python, le traitement est effectué en Python, nous ne bénéficions pas des avantages de l'optimiseur Catalyst de Spark (yada yada). Comment puis-je faire cela? Plus précisément:

  1. Y a-t-il un moyen de faire fonctionner une distribution directe? Voir ci-dessous pour plus de détails (et une tentative infructueuse de solution de contournement). Ou, y a-t-il une autre opération qui a l'effet que j'étais après?
  2. Quelle solution est la plus efficace parmi les deux solutions alternatives que je suggère ci-dessous (FDU vs décomposition/réassemblage des éléments de la liste)? Ou existe-t-il d'autres solutions presque meilleures, mais pas tout à fait, meilleures que l'une ou l'autre?

Un casting droit ne fonctionne pas

C’est ce que je pense être la "bonne" solution. Je veux convertir le type d'une colonne d'un type à un autre, je dois donc utiliser un cast. En guise de contexte, permettez-moi de vous rappeler la manière habituelle de le convertir en un autre type:

from pyspark.sql import types
df_with_strings = df.select(
    df["city"], 
    df["temperatures"].cast(types.ArrayType(types.StringType()))),
)

Maintenant, par exemple df_with_strings.collect()[0]["temperatures"][1] est '-7.0'. Mais si je lance en un vecteur ml alors les choses ne vont pas si bien:

from pyspark.ml.linalg import VectorUDT
df_with_vectors = df.select(df["city"], df["temperatures"].cast(VectorUDT()))

Cela donne une erreur:

pyspark.sql.utils.AnalysisException: "cannot resolve 'CAST(`temperatures` AS STRUCT<`type`: TINYINT, `size`: INT, `indices`: ARRAY<INT>, `values`: ARRAY<DOUBLE>>)' due to data type mismatch: cannot cast ArrayType(DoubleType,true) to org.Apache.spark.ml.linalg.VectorUDT@3bfc3ba7;;
'Project [city#0, unresolvedalias(cast(temperatures#1 as vector), None)]
+- LogicalRDD [city#0, temperatures#1]
"

Beurk! Une idée de comment réparer ça?

Alternatives possibles

Alternative 1: Utiliser VectorAssembler

Il y a un Transformer qui semble presque idéal pour ce travail: le VectorAssembler . Il prend une ou plusieurs colonnes et les concatène en un seul vecteur. Malheureusement, il ne faut que Vector et Float colonnes, pas Array colonnes, la suite ne fonctionne donc pas:

from pyspark.ml.feature import VectorAssembler
assembler = VectorAssembler(inputCols=["temperatures"], outputCol="temperature_vector")
df_fail = assembler.transform(df)

Cela donne cette erreur:

pyspark.sql.utils.IllegalArgumentException: 'Data type ArrayType(DoubleType,true) is not supported.'

Le meilleur moyen de contourner le problème est d’éclater la liste en plusieurs colonnes, puis d’utiliser le VectorAssembler pour les rassembler à nouveau:

from pyspark.ml.feature import VectorAssembler
TEMPERATURE_COUNT = 3
assembler_exploded = VectorAssembler(
    inputCols=["temperatures[{}]".format(i) for i in range(TEMPERATURE_COUNT)], 
    outputCol="temperature_vector"
)
df_exploded = df.select(
    df["city"], 
    *[df["temperatures"][i] for i in range(TEMPERATURE_COUNT)]
)
converted_df = assembler_exploded.transform(df_exploded)
final_df = converted_df.select("city", "temperature_vector")

Cela semble être l’idéal, sauf que TEMPERATURE_COUNT Est supérieur à 100 et parfois supérieur à 1000. (Un autre problème est que le code serait plus compliqué si vous ne connaissiez pas la taille du tableau dans avance, bien que ce ne soit pas le cas pour mes données.) Est-ce que Spark génère en fait un jeu de données intermédiaire avec autant de colonnes, ou considère-t-il simplement qu'il s'agit d'une étape intermédiaire que les éléments individuels franchissent de manière transitoire? (ou bien optimise-t-il entièrement cette étape d'absence lorsqu'il voit que la seule utilisation de ces colonnes doit être assemblée en un vecteur)?

Alternative 2: utiliser un fichier UDF

Une alternative plutôt simple consiste à utiliser un fichier UDF pour effectuer la conversion. Cela me permet d'exprimer assez directement ce que je veux faire dans une ligne de code et ne nécessite pas de créer un ensemble de données avec un nombre de colonnes incroyable. Mais toutes ces données doivent être échangées entre Python et la machine virtuelle Java, et chaque numéro individuel doit être géré par Python (qui est notoirement lent pour une itération). éléments de données individuels). Voici à quoi cela ressemble:

from pyspark.ml.linalg import Vectors, VectorUDT
from pyspark.sql.functions import udf
list_to_vector_udf = udf(lambda l: Vectors.dense(l), VectorUDT())
df_with_vectors = df.select(
    df["city"], 
    list_to_vector_udf(df["temperatures"]).alias("temperatures")
)

Remarques ignorables

Les sections restantes de cette question décousue sont des choses supplémentaires que j'ai trouvées en essayant de trouver une réponse. La plupart des gens qui lisent ceci peuvent probablement les ignorer.

Pas une solution: utilisez Vector pour commencer

Dans cet exemple trivial, il est possible de commencer par créer les données en utilisant le type de vecteur, mais bien sûr, mes données ne sont pas vraiment une liste Python que je parallélise, mais est en train d'être lu depuis une source de données. Mais pour mémoire, voici à quoi cela ressemblerait:

from pyspark.ml.linalg import Vectors
from pyspark.sql import Row
source_data = [
    Row(city="Chicago", temperatures=Vectors.dense([-1.0, -2.0, -3.0])),
    Row(city="New York", temperatures=Vectors.dense([-7.0, -7.0, -5.0])),
]
df = spark.createDataFrame(source_data)

Solution inefficace: utilisez map()

Une possibilité consiste à utiliser la méthode RDD map() pour transformer la liste en un fichier Vector. Ceci est similaire à l'idée UDF, sauf que c'est encore pire car le coût de la sérialisation, etc. est engagé pour tous les champs de chaque ligne, pas seulement celui sur lequel on opère. Pour mémoire, voici à quoi cette solution ressemblerait:

df_with_vectors = df.rdd.map(lambda row: Row(
    city=row["city"], 
    temperatures=Vectors.dense(row["temperatures"])
)).toDF()

Échec de la tentative de solution de contournement pour la distribution

En désespoir de cause, j'ai remarqué que Vector est représenté en interne par une structure à quatre champs, mais l'utilisation d'un transtypage traditionnel à partir de ce type de structure ne fonctionne pas non plus. Voici une illustration (où j'ai construit la structure en utilisant un udf, mais ce n'est pas la partie importante):

from pyspark.ml.linalg import Vectors, VectorUDT
from pyspark.sql.functions import udf
list_to_almost_vector_udf = udf(lambda l: (1, None, None, l), VectorUDT.sqlType())
df_almost_vector = df.select(
    df["city"], 
    list_to_almost_vector_udf(df["temperatures"]).alias("temperatures")
)
df_with_vectors = df_almost_vector.select(
    df_almost_vector["city"], 
    df_almost_vector["temperatures"].cast(VectorUDT())
)

Cela donne l'erreur:

pyspark.sql.utils.AnalysisException: "cannot resolve 'CAST(`temperatures` AS STRUCT<`type`: TINYINT, `size`: INT, `indices`: ARRAY<INT>, `values`: ARRAY<DOUBLE>>)' due to data type mismatch: cannot cast StructType(StructField(type,ByteType,false), StructField(size,IntegerType,true), StructField(indices,ArrayType(IntegerType,false),true), StructField(values,ArrayType(DoubleType,false),true)) to org.Apache.spark.ml.linalg.VectorUDT@3bfc3ba7;;
'Project [city#0, unresolvedalias(cast(temperatures#5 as vector), None)]
+- Project [city#0, <lambda>(temperatures#1) AS temperatures#5]
+- LogicalRDD [city#0, temperatures#1]
"
51
Arthur Tacca

Personnellement, j'irais avec Python UDF et je ne m'embêterai pas avec autre chose:

Mais si vous voulez vraiment d'autres options, vous êtes:

  • Scala UDF avec Python wrapper:

    Installez sbt en suivant les instructions du site du projet.

    Créez un package Scala avec la structure suivante:

    .
    ├── build.sbt
    └── udfs.scala
    

    Modifier build.sbt _ (ajuster pour refléter Scala et Spark version)):

    scalaVersion := "2.11.8"
    
    libraryDependencies ++= Seq(
      "org.Apache.spark" %% "spark-sql" % "2.1.0",
      "org.Apache.spark" %% "spark-mllib" % "2.1.0"
    )
    

    Modifier udfs.scala:

    package com.example.spark.udfs
    
    import org.Apache.spark.sql.functions.udf
    import org.Apache.spark.ml.linalg.DenseVector
    
    object udfs {
      val as_vector = udf((xs: Seq[Double]) => new DenseVector(xs.toArray))
    }
    

    Paquet:

    sbt package
    

    et inclure (ou équivalent selon Scala vers:

    $PROJECT_ROOT/target/scala-2.11/udfs_2.11-0.1-SNAPSHOT.jar
    

    comme argument pour --driver-class-path lorsque vous démarrez Shell/soumettez une application.

    Dans PySpark, définissez un wrapper:

    from pyspark.sql.column import _to_Java_column, _to_seq, Column
    from pyspark import SparkContext
    
    def as_vector(col):
        sc = SparkContext.getOrCreate()
        f = sc._jvm.com.example.spark.udfs.udfs.as_vector()
        return Column(f.apply(_to_seq(sc, [col], _to_Java_column)))
    

    Tester:

    with_vec = df.withColumn("vector", as_vector("temperatures"))
    with_vec.show()
    
    +--------+------------------+----------------+
    |    city|      temperatures|          vector|
    +--------+------------------+----------------+
    | Chicago|[-1.0, -2.0, -3.0]|[-1.0,-2.0,-3.0]|
    |New York|[-7.0, -7.0, -5.0]|[-7.0,-7.0,-5.0]|
    +--------+------------------+----------------+
    
    with_vec.printSchema()
    
    root
     |-- city: string (nullable = true)
     |-- temperatures: array (nullable = true)
     |    |-- element: double (containsNull = true)
     |-- vector: vector (nullable = true)
    
  • Sauvegardez les données dans un format JSON reflétant le schéma DenseVector et relisez-les:

    from pyspark.sql.functions import to_json, from_json, col, struct, lit
    from pyspark.sql.types import StructType, StructField
    from pyspark.ml.linalg import VectorUDT
    
    json_vec = to_json(struct(struct(
        lit(1).alias("type"),  # type 1 is dense, type 0 is sparse
        col("temperatures").alias("values")
    ).alias("v")))
    
    schema = StructType([StructField("v", VectorUDT())])
    
    with_parsed_vector = df.withColumn(
        "parsed_vector", from_json(json_vec, schema).getItem("v")
    )
    
    with_parsed_vector.show()
    
    +--------+------------------+----------------+
    |    city|      temperatures|   parsed_vector|
    +--------+------------------+----------------+
    | Chicago|[-1.0, -2.0, -3.0]|[-1.0,-2.0,-3.0]|
    |New York|[-7.0, -7.0, -5.0]|[-7.0,-7.0,-5.0]|
    +--------+------------------+----------------+
    
    with_parsed_vector.printSchema()
    
    root
     |-- city: string (nullable = true)
     |-- temperatures: array (nullable = true)
     |    |-- element: double (containsNull = true)
     |-- parsed_vector: vector (nullable = true)
    
15
user6910411

J'ai eu le même problème que toi et c'est ce que j'ai fait. De cette façon, la transformation RDD est incluse. Les performances ne sont donc pas critiques, mais cela fonctionne.

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

source_data = [
    Row(city="Chicago", temperatures=[-1.0, -2.0, -3.0]),
    Row(city="New York", temperatures=[-7.0, -7.0, -5.0]), 
]
df = spark.createDataFrame(source_data)

city_rdd = df.rdd.map(lambda row:row[0])
temp_rdd = df.rdd.map(lambda row:row[1])
new_df = city_rdd.Zip(temp_rdd.map(lambda x:Vectors.dense(x))).toDF(schema=['city','temperatures'])

new_df

le résultat est,

DataFrame[city: string, temperatures: vector]
2
GGDammy