如何在Pyspark中有效地创建一个多维交叉表和每个维度的汇总?

roejwanj  于 2023-10-15  发布在  Spark
关注(0)|答案(1)|浏览(67)

TL;DR:还有比这更好的方法吗?

columns = ['sex', 'class', 'survived'] # for many columns
grouped_crosstab = sdf.groupBy(*columns).count()

for column in columns:
    grouped_crosstab = grouped_crosstab.join(
        grouped_crosstab.groupBy(column).agg(F.sum('count').alias(f'{column}_total')),
        column,
        'left')

问题设置

在Pyspark中,您可以在DataFrame上使用crosstab方法来获得数据的二维交叉表格。类似地,groupBy方法可以返回数据的多维“交叉表”,尽管它是高瘦格式的。
举例来说:

columns = ['x', 'y', 'z'] # columns are assumed to be rather low in cardinality, such as categorical values, not continuous values
two_dimensional_crosstab = df.crosstab(columns[0], columns[1]) # only compares 'x' and 'y'
multi_dimensional_view = df.groupBy(*columns).count() # compares 'x', 'y', and 'z'

让我们用一些示例数据来可视化它

import seaborn
df = seaborn.load_dataset('titanic')
sdf = spark.createDataFrame(df) # how to setup a spark context is outside the scope of this question

数据如下所示:

让我们在sexclass上创建一个二维交叉表,并使用crosstabgroupBy函数来展示这两个方法的比较:

two_d_crosstab = sdf.crosstab('sex', 'class')
grouped_crosstab = sdf.groupBy('sex', 'class').count()

这些框架看起来像这样:

crosstab不同,groupBy方法可以很好地推广到多列,但必须注意表的格式。

列和行合计

为了统计目的,通常需要在交叉表上具有行和列的总和,例如,调查排序。在二维的情况下,人们可以通过这种(公认的复杂)方法获得这样的信息:

index_column = two_d_crosstab.columns[0]
col_list = two_d_crosstab.columns[1:]
two_d_crosstab = two_d_crosstab.withColumn('column_total', sum([F.col(c) for c in col_list]))
transposed_df = two_d_crosstab.pandas_api()\
    .set_index(index_column)\
    .T.reset_index()\
    .rename(columns = {'index':index_column})\
    .to_spark()
col_list = transposed_df.columns[1:]
two_d_crosstab = transposed_df.withColumn('row_total', sum([F.col(c) for c in col_list]))

two_d_crosstab看起来像这样:

多维合计

如何在多维交叉选项卡上进行此计算?
以下是我尝试过的:

sex_tot = grouped_crosstab.groupBy('sex').agg(F.sum('count').alias('sex_total'))
class_tot = grouped_crosstab.groupBy('class').agg(F.sum('count').alias('class_total'))
grouped_crosstab = grouped_crosstab.join(sex_tot, 'sex', 'left').join(class_tot, 'class', 'left')

输出如下所示:

让我们添加survived作为第三个维度:

columns = ['sex', 'class', 'survived']
grouped_crosstab = sdf.groupBy(*columns).count()

for column in columns:
    grouped_crosstab = grouped_crosstab.join(
        grouped_crosstab.groupBy(column).agg(F.sum('count').alias(f'{column}_total')),
        column,
        'left')

它看起来像这样:

注意,输出中有很多重复的信息。随着列数的增加,group by和join操作的数量也会增加,因此这将变得相当笨拙,特别是在具有数百万行的大型 Dataframe 上。

  • 是否有更好的(可扩展的)方法?*
ekqde3dh

ekqde3dh1#

我尝试的解决方案(我在写这个问题时发现的)是使用cube方法而不是groupBy。从二维情况开始:

columns = ['sex', 'class']
cubed_crosstab = sdf.cube(*columns).count()

生产:

其中,当显示null时,计数被视为“不关心此列中的值”。因此,在屏幕截图的第三行,491是针对任何性别的三等舱乘客的计数,第5行产生整个数据框的总数。请注意,这些计数与之前生成的计数一致。
推广到多个维度是微不足道的:

columns = ['sex', 'class', 'survived']
cubed_crosstab = sdf.cube(*columns).count()

此答案的限制

当两列中都有空值时,答案变得不明确。举例来说:

columns = ['deck', 'embarked']
cubed_crosstab = sdf.cube(*columns).count()

存在具有不同计数的重复条目。可以表示“不关心”状态,也可以表示数据中的合法值。建议将空值重新编码为cubeing之前的另一个值(这是一个单词吗?)以避免这种歧义。

相关问题