次のスニペットを検討してください(spark
が既に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
に変換したいのですが、RDDを経由するのではなく、基本的なDataFrame
APIを使用してこの変換を表現したいです(JVMからPythonにすべてのデータを送信するため、処理はPythonで行われますが、SparkのCatalystオプティマイザー(yada yada)の利点は得られません)。どうすればいいですか?具体的には:
これが私が「適切な」ソリューションになると期待するものです。列の型をある型から別の型に変換したいので、キャストを使用する必要があります。ちょっとしたコンテキストとして、別の型にキャストする通常の方法を思い出させてください。
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]
は'-7.0'
です。しかし、ml Vectorにキャストすると、うまくいきません。
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]
"
うわぁ!これを修正する方法はありますか?
VectorAssembler
を使用するこのジョブにほぼ理想的と思われるTransformer
があります: VectorAssembler
。 1つ以上の列を受け取り、それらを単一のベクトルに連結します。残念ながら、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は実際にその数の列を持つ中間データセットを生成しますか、またはこれを個々のアイテムが一時的に通過する中間ステップと見なしますか(または実際に行います)これらの列の唯一の使用がベクトルにアセンブルされることがわかったときに、この離れたステップを完全に最適化しますか?)
かなり単純な代替手段は、UDFを使用して変換を行うことです。これにより、実行したいことを1行のコードで非常に直接表現できます。また、膨大な数の列を持つデータセットを作成する必要はありません。ただし、すべてのデータは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()
を使用1つの可能性は、RDD map()
メソッドを使用して、リストをVector
に変換することです。これは、UDFの考え方と似ていますが、操作対象のフィールドだけでなく、各行のすべてのフィールドでシリアル化などのコストが発生するため、さらに悪化します。記録のために、このソリューションは次のようになります。
df_with_vectors = df.rdd.map(lambda row: Row(
city=row["city"],
temperatures=Vectors.dense(row["temperatures"])
)).toDF()
必死になって、Vector
は4つのフィールドを持つ構造体によって内部的に表されていることに気付きましたが、そのタイプの構造体からの従来のキャストを使用しても機能しません。以下に図を示します(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]
"
個人的にはPython UDFを使用しますが、他のことは一切気にしません。
Vectors
はネイティブSQL型ではないため、何らかの方法でパフォーマンスのオーバーヘッドが発生します。特に、このプロセスでは、データが最初に 外部タイプから行に変換 、次に 一般的なRowEncoder
を使用して行から内部表現に変換される2つのステップが必要です。Pipeline
は、単純な変換よりもはるかに高価です。さらに、上記のプロセスとは逆のプロセスが必要ですただし、ここで他のオプションが本当に必要な場合は次のとおりです。
Pythonラッパーを使用したScala UDF:
プロジェクトサイトの指示に従って sbt をインストールします。
次の構造でScalaパッケージを作成します。
.
├── build.sbt
└── udfs.scala
build.sbt
を編集します(ScalaおよびSparkバージョンを反映するように調整します):
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に応じて同等):
$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)))
テスト:
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)
DenseVector
スキーマを反映するデータをJSON形式にダンプし、それを読み返します。
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)
私はあなたと同じ問題を抱えていたので、このようにしました。この方法には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]