web-dev-qa-db-ja.com

pysparkで効率的に(pyspark.sql.Rowを使用せずに)大きなDataFrameを生成する

問題の要約は次のとおりです。既存の並列化された入力のコレクションと、1つの入力で比較的大きな行のバッチを生成できる関数を使用して、pysparkでDataFrameを生成します。以下の例では、10 ^ 12行のデータフレームを生成したいとします。 1000人の執行者:

def generate_data(one_integer):
  import numpy as np
  from pyspark.sql import Row
  M = 10000000 # number of values to generate per seed, e.g. 10M
  np.random.seed(one_integer)
  np_array = np.random.random_sample(M) # generates an array of M random values
  row_type = Row("seed", "n", "x")
  return [row_type(one_integer, i, float(np_array[i])) for i in range(M)]

N = 100000 # number of seeds to try, e.g. 100K
list_of_integers = [i for i in range(N)]
list_of_integers_rdd = spark.sparkContext.parallelize(list_of_integers)
row_rdd = list_of_integers_rdd.flatMap(list_of_integers_rdd)
from pyspark.sql.types import StructType, StructField, FloatType, IntegerType
my_schema = StructType([
       StructField("seed", IntegerType()),
       StructField("n", IntegerType()),
       StructField("x", FloatType())])
df = spark.createDataFrame(row_rdd, schema=my_schema)

(シードが与えられた乱数の分布を実際に研究したくありません-これは、大きなデータフレームがウェアハウスから読み込まれず、コードによって生成される状況を説明するために思いついた例にすぎません)

上記のコードは、私が望んでいるものとほぼ同じです。問題は、それが非常に非効率的な方法でそれを行うことです-各行のpython Rowオブジェクトを作成し、次にpython Rowオブジェクトを内部Sparkカラム表現に変換することを犠牲にして。

これらが値のバッチの列であることをsparkに知らせるだけで、すでに列表現になっている行のバッチ(たとえば、上記の1つまたはいくつかのnumpy配列np_array)を変換できる方法はありますか?

例えば。各要素がpyarrow.RecordBatchまたはpandas.DataFrameであるpythonコレクションRDDを生成するコードを記述できますが、作成せずにこれらのいずれかをSpark DataFrameに変換する方法が見つかりませんプロセス内のpyspark RowオブジェクトのRDD。

Pyarrow + pandasを使用してローカル(ドライバーへ)pandasデータフレームをSparkデータフレームに効率的に変換する方法の例を含む記事が少なくとも数十ありますが、これはオプションではありません私にとっては、ドライバーで1つのpandasデータフレームを生成してエグゼキューターに送信するのではなく、実際にエグゼキューターで分散してデータを生成する必要があるからです。

PD。 pythonタプルのRDDを使用して、行オブジェクトの作成を回避する1つの方法を見つけました。予想どおり、速度はまだ遅すぎますが、Rowオブジェクトを使用するよりも少し高速です。それでも、これは私が探しているものではありません(これは、pythonからSparkに列データを渡す非常に効率的な方法です)。

また、マシンで特定の操作を実行するために測定された時間(測定時間にはかなりのばらつきがありますが、それでも私の意見ではそれは代表的です):問題のデータセットは10M行、3列です(1列は定数整数、その​​他0から10M-1の整数の範囲で、3番目はnp.random.random_sampleを使用して生成された浮動小数点値です。

  • ローカルでpandasデータフレームを生成(1,000万行):約440〜450ミリ秒
  • ローカルでspark.sql.Rowオブジェクトのリストpythonを生成(10M行):〜12-15秒
  • 行(1000万行)を表すタプルのpythonリストをローカルに生成:〜3.4-3.5s

1つのエグゼキュータと1つの初期シード値だけを使用して、Sparkデータフレームを生成します。

  • spark.createDataFrame(row_rdd, schema=my_schema)の使用:〜70-80秒
  • spark.createDataFrame(Tuple_rdd, schema=my_schema)の使用:〜40〜45秒
  • (非分散作成)spark.createDataFrame(pandas_df, schema=my_schema)を使用:〜0.4-0.5s(pandas df生成自体はほぼ同じ時間がかかりません)-spark.sql.execution.arrow.enabledをtrueに設定します。

Local-to-driver pandasデータフレームが10M行の〜1sでSparkデータフレームに変換される例は、エグゼキューターで生成されたデータフレームでも同じことが可能であると考える理由を私に与えます。ただし、pythonタプルのRDDを使用すると、10M行で最大40秒が達成できます。

だから問題はまだ残っています-pysparkで効率的に分散された方法で大きなSparkデータフレームを生成する方法はありますか?

2

ボトルネックはRDD->データフレームからの変換であり、手元の関数はかなり高速で、pandas DFへの変換がspark DFはpyarrow経由でかなり高速です。以下に2つの解決策を示します。

  1. pandas dfを並行して作成するのは簡単なので、エグゼキュータから返すのではなく、df.to_parquetを使用して結果のdfを記述します。つまり、
def generate_data(seed):
    M = 10
    np.random.seed(seed)
    np_array = np.random.random_sample(M) # generates an array of M random values
    df = pd.DataFrame(np_array, columns=["x"])
    df["seed"] = seed
    df.reset_index().to_parquet(f"s3://bucket/part-{str(seed).zfill(5)}.parquet"

結果として得られる寄木細工ファイルでのSparkの読み取りは、後で簡単になります。次に、ボトルネックがIOの制限になり、sparkのタプル/行タイプの変換よりも高速になります。

  1. ファイルへの保存を許可されていない場合、sparkのバージョンが十分に新しいものであると想定して、pandas_udfおよびGROUPED_MAPが役立つ場合があります。 pyarrowも使用してspark DFとpandas DFの間で変換するため、タプルを使用するよりも高速で、pandas DFを作成して返すことができます分散方式のUDF。
import numpy as np
import pandas as pd
from pyspark.sql.functions import pandas_udf, PandasUDFType

N = 10

df = spark.createDataFrame(
    [(i,) for i in range(N)], ["seed"]
)

def generate_data(seed):
    M = 10
    np.random.seed(seed)
    np_array = np.random.random_sample(M) # generates an array of M random values
    df = pd.DataFrame(np_array, columns=["x"])
    df["seed"] = seed
    return df.reset_index()

@pandas_udf("index long, x double, seed long", PandasUDFType.GROUPED_MAP)
def generate_data_udf(pdf):
    output = []
    for idx, row in pdf.iterrows():
        output.append(generate_data(row["seed"]))
    return pd.concat(output)


df.groupby("seed").apply(generate_data_udf).show()

遅い部分はgroupbyであり、シードをgenerate_data_udfにバッチ処理する方法に応じて高速化できる場合があります。

@udf(returnType=IntegerType())
def batch_seed(seed):
    return seed // 10

df.withColumn("batch_seed", batch_seed(col("seed"))). \
groupBy("batch_seed").apply(generate_data_udf).show()
1
ayplam

以下は、RDDを使用していないか、行を作成していないが、データフレーム操作でのみ使用できるソリューションです。
(コードはscalaですが、pythonで同じことを行う必要があります)

val N = 100000

//for seed return array of index and random_value
def generate_data(i: Int): Array[(Int, Double)] = ???
val generate_data_udf = udf (generate_data _)

spark
  .range(N)
  .toDF("seed")
  .withColumn("arr", generate_data_udf($"seed"))
  .select(
    $"seed",
    explode($"arr") as "exp"
  )
  .select(
    $"seed",
    $"exp._1" as "n",
    $"exp._2" as "x"
  )
0
lev