J'ai un dataframe qui a une ligne et plusieurs colonnes. Certaines des colonnes sont des valeurs uniques et d'autres des listes. Toutes les colonnes de la liste ont la même longueur. Je souhaite scinder chaque colonne de la liste en une ligne distincte, tout en conservant telle quelle la colonne non-liste.
Échantillon DF:
from pyspark import Row
from pyspark.sql import SQLContext
from pyspark.sql.functions import explode
sqlc = SQLContext(sc)
df = sqlc.createDataFrame([Row(a=1, b=[1,2,3],c=[7,8,9], d='foo')])
# +---+---------+---------+---+
# | a| b| c| d|
# +---+---------+---------+---+
# | 1|[1, 2, 3]|[7, 8, 9]|foo|
# +---+---------+---------+---+
Ce que je veux:
+---+---+----+------+
| a| b| c | d |
+---+---+----+------+
| 1| 1| 7 | foo |
| 1| 2| 8 | foo |
| 1| 3| 9 | foo |
+---+---+----+------+
Si je n'avais qu'une colonne de liste, ce serait facile en faisant simplement un explode
:
df_exploded = df.withColumn('b', explode('b'))
# >>> df_exploded.show()
# +---+---+---------+---+
# | a| b| c| d|
# +---+---+---------+---+
# | 1| 1|[7, 8, 9]|foo|
# | 1| 2|[7, 8, 9]|foo|
# | 1| 3|[7, 8, 9]|foo|
# +---+---+---------+---+
Cependant, si j'essaie également de explode
la colonne c
, je me retrouve avec un cadre de données de longueur égale à ce que je veux:
df_exploded_again = df_exploded.withColumn('c', explode('c'))
# >>> df_exploded_again.show()
# +---+---+---+---+
# | a| b| c| d|
# +---+---+---+---+
# | 1| 1| 7|foo|
# | 1| 1| 8|foo|
# | 1| 1| 9|foo|
# | 1| 2| 7|foo|
# | 1| 2| 8|foo|
# | 1| 2| 9|foo|
# | 1| 3| 7|foo|
# | 1| 3| 8|foo|
# | 1| 3| 9|foo|
# +---+---+---+---+
Ce que je veux, c'est - pour chaque colonne, prenons le nième élément du tableau dans cette colonne et l'ajoutons à une nouvelle ligne. J'ai essayé de mapper un éclat sur toutes les colonnes du cadre de données, mais cela ne semble pas fonctionner non plus:
df_split = df.rdd.map(lambda col: df.withColumn(col, explode(col))).toDF()
étincelle> = 2.4
Vous pouvez remplacer Zip_
udf
avec arrays_Zip
une fonction
from pyspark.sql.functions import arrays_Zip, col
(df
.withColumn("tmp", arrays_Zip("b", "c"))
.withColumn("tmp", explode("tmp"))
.select("a", col("tmp.b"), col("tmp.c"), "d"))
étincelle <2.4
Avec DataFrames
et UDF:
from pyspark.sql.types import ArrayType, StructType, StructField, IntegerType
from pyspark.sql.functions import col, udf, explode
Zip_ = udf(
lambda x, y: list(Zip(x, y)),
ArrayType(StructType([
# Adjust types to reflect data types
StructField("first", IntegerType()),
StructField("second", IntegerType())
]))
)
(df
.withColumn("tmp", Zip_("b", "c"))
# UDF output cannot be directly passed to explode
.withColumn("tmp", explode("tmp"))
.select("a", col("tmp.first").alias("b"), col("tmp.second").alias("c"), "d"))
Avec RDDs
:
(df
.rdd
.flatMap(lambda row: [(row.a, b, c, row.d) for b, c in Zip(row.b, row.c)])
.toDF(["a", "b", "c", "d"]))
Les deux solutions sont inefficaces en raison de la surcharge de communication de Python). Si la taille des données est fixe, vous pouvez procéder comme suit:
from functools import reduce
from pyspark.sql import DataFrame
# Length of array
n = 3
# For legacy Python you'll need a separate function
# in place of method accessor
reduce(
DataFrame.unionAll,
(df.select("a", col("b").getItem(i), col("c").getItem(i), "d")
for i in range(n))
).toDF("a", "b", "c", "d")
ou même:
from pyspark.sql.functions import array, struct
# SQL level Zip of arrays of known size
# followed by explode
tmp = explode(array(*[
struct(col("b").getItem(i).alias("b"), col("c").getItem(i).alias("c"))
for i in range(n)
]))
(df
.withColumn("tmp", tmp)
.select("a", col("tmp").getItem("b"), col("tmp").getItem("c"), "d"))
Cela devrait être nettement plus rapide par rapport à UDF ou RDD. Généralisé pour supporter un nombre arbitraire de colonnes:
# This uses keyword only arguments
# If you use legacy Python you'll have to change signature
# Body of the function can stay the same
def Zip_and_explode(*colnames, n):
return explode(array(*[
struct(*[col(c).getItem(i).alias(c) for c in colnames])
for i in range(n)
]))
df.withColumn("tmp", Zip_and_explode("b", "c", n=3))
Vous devez utiliser flatMap
et non pas map
pour créer plusieurs lignes en sortie à partir de chaque ligne en entrée.
from pyspark.sql import Row
def dualExplode(r):
rowDict = r.asDict()
bList = rowDict.pop('b')
cList = rowDict.pop('c')
for b,c in Zip(bList, cList):
newDict = dict(rowDict)
newDict['b'] = b
newDict['c'] = c
yield Row(**newDict)
df_split = sqlContext.createDataFrame(df.rdd.flatMap(dualExplode))