Python Pyspark:将多个数组列拆分为行

Python Pyspark:将多个数组列拆分为行,python,apache-spark,dataframe,pyspark,apache-spark-sql,Python,Apache Spark,Dataframe,Pyspark,Apache Spark Sql,我有一个数据框,它有一行和几列。一些列是单个值,其他列是列表。所有列表列的长度都相同。我想将每个列表列拆分为一个单独的行,同时保留任何非列表列 样本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

我有一个数据框,它有一行和几列。一些列是单个值,其他列是列表。所有列表列的长度都相同。我想将每个列表列拆分为一个单独的行,同时保留任何非列表列

样本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|
# +---+---------+---------+---+
我想要的是:

+---+---+----+------+
|  a|  b|  c |    d |
+---+---+----+------+
|  1|  1|  7 |  foo |
|  1|  2|  8 |  foo |
|  1|  3|  9 |  foo |
+---+---+----+------+
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|
# +---+---+---+---+
如果我只有一个列表列,只需执行
分解

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|
# +---+---+---------+---+
但是,如果我尝试同时分解
c
,我最终会得到一个长度为我想要的平方的数据帧:

+---+---+----+------+
|  a|  b|  c |    d |
+---+---+----+------+
|  1|  1|  7 |  foo |
|  1|  2|  8 |  foo |
|  1|  3|  9 |  foo |
+---+---+----+------+
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|
# +---+---+---+---+
我想要的是——对于每一列,获取该列中数组的第n个元素,并将其添加到新行。我已经尝试在数据框中的所有列之间映射一个explode accross,但这似乎也不起作用:

df_split = df.rdd.map(lambda col: df.withColumn(col, explode(col))).toDF()

火花>=2.4

您可以用
数组\u zip
函数替换
udf

from pyspark.sql.functions import arrays_zip, col, explode

(df
    .withColumn("tmp", arrays_zip("b", "c"))
    .withColumn("tmp", explode("tmp"))
    .select("a", col("tmp.b"), col("tmp.c"), "d"))
火花<2.4

使用数据帧
和自定义项:

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"))
使用
RDD

(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"]))
由于Python通信开销,这两种解决方案都效率低下。如果数据大小是固定的,则可以执行以下操作:

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")
甚至:

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"))
这应该比UDF或RDD快得多。通用以支持任意数量的列:

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

您需要使用
flatMap
,而不是
map
,因为您希望从每个输入行生成多个输出行

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))
一个衬套(火花>=2.4.0时): 所需进口:
从pyspark.sql.functions导入数组\u zip


台阶-
  • 创建一个列bc,它是列
    b
    c
  • 分解
    bc
    以获取结构
    tbc
  • 选择所需的列
    a
    b
    c
    (所有列都根据需要进行分解)
  • 输出:
    如果第一个df有3个值,第二个df有2个值,那么我们的zip恰好返回两对而不是3对。Zip将一个对象的第一个元素与另一个对象的第一个元素配对,第二个元素与第二个元素配对,以此类推,直到其中一个对象的元素用完为止。在您的情况下,在2个值之后。换句话说,它将对元素进行配对,直到没有更多的项可以配对为止。若要给出任何建议,我需要知道您希望您的程序如何处理未配对的元素(例如,您希望第二个集合中的空值吗?)。此外,本例中只有1个df。如果你的问题与此不同,最好再问一个问题谢谢@David的回答。我想出来了。使用Izip帮助解决了这个问题。不过我还是很感谢你的回答,伙计。Spark>=2.4的解决方案到底能起什么作用?文档中说,explode输入“应该是数组或映射类型,而不是字符串”,并以文字形式引用了它在其他情况下引发的异常。如何处理不同列中的不均匀大小列表。要求将较短大小列表的值替换为-1。现在它显示为null。