PySpark: как преобразовать столбец массива (т. е. списка) в вектор

краткая версия вопроса!

рассмотрим следующий фрагмент (предполагая spark уже установлено значение some 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)

обратите внимание, что поле температур является список поплавки. Я хотел бы преобразовать эти списки поплавков в тип MLlib Vector, и я хотел бы, чтобы это преобразование было выражено с помощью basic DataFrame API, а не через RDDs (что неэффективно, потому что он отправляет все данные из JVM в Python, обработка сделано в Python, мы не получаем преимущества оптимизатора катализатора Spark, yada yada). Как мне это сделать? В частности:

  1. есть ли способ получить прямой литой работает? См. ниже для деталей (и неудачной попытки обходного пути)? Или есть какая-то другая операция, которая имеет эффект, который я искал?
  2. что более эффективно из двух альтернативных решений, которые я предлагаю ниже (UDF против взрыва/повторной сборки элементов в списке)? Или есть какие-то другие почти-но-не-совсем-правильные альтернативы, которые лучше, чем любой из них?

прямой бросок не работает

это то, что я ожидал бы быть "правильным" решением. Я хочу преобразовать тип столбца из одного типа в другой, поэтому я должен использовать приведение. В качестве небольшого контекста позвольте мне напомнить вам о нормальном способе приведения его к другому типу:

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

сейчас, например,df_with_strings.collect()[0]["temperatures"][1] is '-7.0'. Но если я приведу к вектору ml, тогда все не пойдет так:

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

это дает ошибку:

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]
"

хлоп! Есть идеи, как это исправить?

возможны варианты

Вариант 1: С Помощью VectorAssembler

есть Transformer это кажется почти идеальным для этой работы:VectorAssembler. Он принимает один или несколько столбцов и объединяет их в один вектор. К сожалению, это толькоVector и Float колонки, а не Array колонки, поэтому следуйте не работает:

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

это дает эту ошибку:

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

лучшая работа, которую я могу придумать, - это взорвать список на несколько столбцов, а затем использовать VectorAssembler, чтобы собрать их всех снова:

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")

это кажется идеальным, за исключением того, что TEMPERATURE_COUNT больше чем 100, и иногда больше чем 1000. (Другая проблема заключается в том, что код будет сложнее, если вы заранее не знаете размер массива, хотя это это не относится к моим данным.) Действительно ли Spark генерирует промежуточный набор данных с таким количеством столбцов, или он просто считает этот промежуточный шаг, через который отдельные элементы проходят транзитивно (или действительно оптимизирует этот шаг полностью, когда видит, что единственное использование этих столбцов должно быть собрано в вектор)?

Альтернатива 2: Используйте UDF

более простой альтернативой является использование UDF для преобразования. Это позволяет мне выразить довольно непосредственно то, что я хочу сделать в одной строке кода, и не требует создания набора данных с сумасшедшим количеством столбцов. Но все эти данные должны обмениваться между Python и JVM, и каждый отдельный номер должен обрабатываться Python (который, как известно, медленный для итерации по отдельным элементам данных). Вот как это выглядит:

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")
)

игнорируемые замечания

остальные разделы этого бессвязного вопроса - это некоторые дополнительные вещи, которые я придумал, пытаясь найти ответ. Вероятно, большинство людей, читающих это, могут пропустить их.

не решение: использовать Vector начнем с

в этом тривиальном примере можно создать данные с использованием векторного типа для начала, но, конечно, мои данные на самом деле не являются списком Python, который я распараллеливаю, а вместо этого читается из источника данных. Но для записи, вот как это будет выглядеть:

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()

одна из возможностей-использовать RDD map() метод преобразования списка в Vector. Это похоже на идею UDF, за исключением того, что ее еще хуже, потому что стоимость сериализации и т. д. выполняется для всех полей в каждой строке, а не только для того, на котором выполняется операция. Для записи, вот как это решение будет выглядеть:

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

неудачная попытка обходного пути для cast

в отчаянии, я заметил, что Vector is представленный внутренне структурой с четырьмя полями, но использование традиционного приведения из этого типа структуры также не работает. Вот иллюстрация (где я построил структуру с помощью udf, но udf не является важной частью):

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())
)

это дает ошибку:

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]
"

2 ответов


лично я бы пошел с Python UDF и не стал бы беспокоиться ни о чем другом:

но если вы действительно хотите другие варианты здесь вы:

  • Scala UDF с оболочкой Python:

    установить sbt следуя инструкциям на сайте проекта.

    создайте пакет Scala со следующей структурой:

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

    редактировать build.sbt (отрегулируйте для отражения Scala и искры версия):

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

    редактировать 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))
    }
    

    пакет:

    sbt package
    

    и включить (или эквивалент в зависимости от Scala vers:

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

    в качестве аргумента --driver-class-path при запуске оболочки / подача заявления.

    в PySpark определите оболочку:

    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)))
    

у меня была такая же проблема,как у вас, и я сделал так. Этот способ включает преобразование RDD, поэтому производительность не критична, но она работает.

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

в результате

DataFrame[city: string, temperatures: vector]