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:
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?
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)?
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")
)
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.
Vector
pour commencerDans 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)
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()
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]
"
Personnellement, j'irais avec Python UDF et je ne m'embêterai pas avec autre chose:
Vectors
ne sont pas des types SQL natifs, donc il y aura une surcharge de performances d'une manière ou d'une autre. En particulier, ce processus nécessite deux étapes dans lesquelles les données sont d'abord converties du type externe en ligne , puis de la ligne en représentation interne à l'aide de generic RowEncoder
.Pipeline
sera beaucoup plus chère qu'une simple conversion. De plus, il nécessite un processus qui, contrairement à celui décrit ci-dessusMais 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)
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]