当前位置:

首页 > 编程开发 > Polars分组求交集的高效技巧

Polars分组求交集的高效技巧

本文探讨了如何在Polars中对包含字符串列表的列进行分组求交集操作。传统的reduce结合列表集合操作往往难以直接实现预期效果。文章提供了一种高效且灵活的解决方案,通过将列表列扁平化,利用行索引和组内计数来识别共同元素,最终重新聚合以获得每个分组内所有列表的交集。此方法避免了复杂的列表操作,转而利用Polars强大的表达式引擎和窗口函数,实现精确的分组交集计算。

Polars中分组列表列求交集的进阶技巧

本文探讨了如何在Polars中对包含字符串列表的列进行分组求交集操作。传统的reduce结合列表集合操作往往难以直接实现预期效果。文章提供了一种高效且灵活的解决方案,通过将列表列扁平化,利用行索引和组内计数来识别共同元素,最终重新聚合以获得每个分组内所有列表的交集。此方法避免了复杂的列表操作,转而利用Polars强大的表达式引擎和窗口函数,实现精确的分组交集计算。

1. 问题背景与挑战

在数据处理中,我们经常会遇到包含列表(list)类型数据的列。当需要对这些列表进行分组聚合,并计算每个组内所有列表的交集时,Polars提供了一些内置的列表操作,但直接应用它们可能无法达到预期效果。例如,使用 pl.reduce 结合 list.set_intersection 通常会遇到类型不匹配或逻辑不符的问题。

考虑以下示例DataFrame,其中包含 id 和 values(字符串列表)两列:

import polars as pl

df = pl.DataFrame(
   {"id": [1,1,2,2,3,3],
    "values": [["A", "B"], ["B", "C"], ["A", "B"], ["B", "C"], ["A", "B"], ["B", "C"]]
   }
)

print(df)

输出:

shape: (6, 2)
┌─────┬────────────┐
│ id  ┆ values     │
│ --- ┆ ---        │
│ i64 ┆ list[str]  │
╞═════╪════════════╡
│ 1   ┆ ["A", "B"] │
│ 1   ┆ ["B", "C"] │
│ 2   ┆ ["A", "B"] │
│ 2   ┆ ["B", "C"] │
│ 3   ┆ ["A", "B"] │
│ 3   ┆ ["B", "C"] │
└─────┴────────────┘

我们的目标是计算每个 id 组内 values 列所有列表的交集。例如,对于 id=1,其对应的列表是 ["A", "B"] 和 ["B", "C"],它们的交集应为 ["B"]。最终期望的输出是:

shape: (3, 2)
┌─────┬───────────┐
│ id  ┆ values    │
│ --- ┆ ---       │
│ i64 ┆ list[str] │
╞═════╪═══════════╡
│ 1   ┆ ["B"]     │
│ 2   ┆ ["B"]     │
│ 3   ┆ ["B"]     │
└─────┴───────────┘

直接尝试使用 pl.reduce 可能会导致不符合预期的结果:

# 尝试1:reduce直接作用于列
# df.group_by("id").agg(
#     pl.reduce(function=lambda acc, x: acc.list.set_intersection(x),
#               exprs=pl.col("values"))
# )
# 结果:list[list[str]],不是交集

# 尝试2:explode后再reduce
# df.group_by("id").agg(
#     pl.reduce(function=lambda acc, x: acc.list.set_intersection(x),
#               exprs=pl.col("values").explode())
# )
# 结果:list[str],但只是简单拼接,并非交集

这些尝试失败的原因在于 pl.reduce 在这种场景下难以正确处理列表的迭代交集,或者 explode 操作改变了数据结构,使其不再适合直接的集合操作。

2. 解决方案:扁平化、计数与重聚合

解决此问题的核心思路是:将列表列扁平化,然后利用Polars的窗口函数(over)和聚合功能来识别在每个组内所有原始列表中都出现的元素,最后再将这些元素重新聚合为列表。

2.1 步骤分解

  1. 计算组的长度: 获取每个 id 组中原始行的数量。这将用于后续判断一个元素是否在组内所有列表中都出现。
  2. 添加行索引: 为DataFrame添加一个唯一的行索引,以便在扁平化后,我们仍然可以追溯每个元素来自哪一行。
  3. 扁平化列表列: 使用 explode 将 values 列中的每个列表元素展开为单独的行。
  4. 计算元素在组内出现的原始行数: 对于每个扁平化后的元素,计算它在当前 id 组中对应了多少个不同的原始行索引。
  5. 过滤共同元素: 筛选出那些其对应原始行数等于该 id 组总行数的元素。这些元素即为所有原始列表的交集。
  6. 重新聚合: 按照 id 再次分组,并将过滤后的元素聚合回列表。

2.2 代码实现

我们将分步展示上述过程。

第一步:准备数据并添加组长度和行索引

首先,我们添加一个 group_len 列,表示每个 id 组的原始行数。这通过 pl.len().over("id") 实现。接着,使用 with_row_index() 添加一个全局唯一的 index 列。

df_prepared = (
    df.with_columns(pl.len().over("id").alias("group_len"))
    .with_row_index()
)
print(df_prepared)

输出:

shape: (6, 3)
┌───────┬─────┬────────────┬───────────┐
│ index ┆ id  ┆ values     ┆ group_len │
│ ---   ┆ --- ┆ ---        ┆ ---       │
│ u32   ┆ i64 ┆ list[str]  ┆ u32       │
╞═══════╪═════╪════════════╪═══════════╡
│ 0     ┆ 1   ┆ ["A", "B"] │ 2         │
│ 1     ┆ 1   ┆ ["B", "C"] │ 2         │
│ 2     ┆ 2   ┆ ["A", "B"] │ 2         │
│ 3     ┆ 2   ┆ ["B", "C"] │ 2         │
│ 4     ┆ 3   ┆ ["A", "B"] │ 2         │
│ 5     ┆ 3   ┆ ["B", "C"] │ 2         │
└───────┴─────┴────────────┴───────────┘

第二步:扁平化 values 列并计算元素在组内出现的原始行数

现在,我们将 values 列扁平化,然后计算每个 (id, values) 对中,有多少个唯一的原始行索引。如果这个数量等于 group_len,则说明该 values 元素在当前 id 组的所有原始列表中都出现了。

df_exploded_counted = (
    df_prepared
    .explode("values")
    .with_columns(
        pl.col("index").n_unique().over("id", "values").alias("n_unique_rows")
    )
)
print(df_exploded_counted)

输出:

shape: (12, 5)
┌───────┬─────┬────────┬───────────┬───────────────┐
│ index ┆ id  ┆ values ┆ group_len ┆ n_unique_rows │
│ ---   ┆ --- ┆ ---    ┆ ---       ┆ ---           │
│ u32   ┆ i64 ┆ str    ┆ u32       ┆ u32           │
╞═══════╪═════╪════════╪═══════════╪═══════════════╡
│ 0     ┆ 1   ┆ A      ┆ 2         ┆ 1             │
│ 0     ┆ 1   ┆ B      ┆ 2         ┆ 2             │ # 'B'在id=1组中来自index 0和1
│ 1     ┆ 1   ┆ B      ┆ 2         ┆ 2             │
│ 1     ┆ 1   ┆ C      ┆ 2         ┆ 1             │
│ 2     ┆ 2   ┆ A      ┆ 2         ┆ 1             │
│ 2     ┆ 2   ┆ B      ┆ 2         ┆ 2             │ # 'B'在id=2组中来自index 2和3
│ 3     ┆ 2   ┆ B      ┆ 2         ┆ 2             │
│ 3     ┆ 2   ┆ C      ┆ 2         ┆ 1             │
│ 4     ┆ 3   ┆ A      ┆ 2         ┆ 1             │
│ 4     ┆ 3   ┆ B      ┆ 2         ┆ 2             │ # 'B'在id=3组中来自index 4和5
│ 5     ┆ 3   ┆ B      ┆ 2         ┆ 2             │
│ 5     ┆ 3   ┆ C      ┆ 2         ┆ 1             │
└───────┴─────┴────────┴───────────┴───────────────┘

从输出可以看出,对于 id=1,元素 "A" 只有 n_unique_rows=1(因为它只在 index=0 的原始行中出现),而元素 "B" 有 n_unique_rows=2(因为它在 index=0 和 index=1 的原始行中都出现过)。由于 group_len 也是2,这表明 "B" 是 id=1 组中所有列表的交集元素。

第三步:过滤并重新聚合

最后一步是过滤出那些 n_unique_rows 等于 group_len 的行,然后按 id 分组并聚合 values 列。为了确保结果列表中没有重复项,我们使用 pl.col.values.unique()。

final_result = (
    df.with_columns(pl.len().over("id").alias("group_len"))
    .with_row_index()
    .explode("values")
    .filter(
        pl.col("index").n_unique().over("id", "values")
        == pl.col("group_len")
    )
    .group_by("id", maintain_order=True)
    .agg(pl.col("values").unique()) # 使用.unique()确保结果列表中元素唯一
)

print(final_result)

最终输出:

shape: (3, 2)
┌─────┬───────────┐
│ id  ┆ values    │
│ --- ┆ ---       │
│ i64 ┆ list[str] │
╞═════╪═══════════╡
│ 1   ┆ ["B"]     │
│ 2   ┆ ["B"]     │
│ 3   ┆ ["B"]     │
└─────┴───────────┘

这正是我们期望的结果。

3. 注意事项与总结

  • 性能考量: 这种方法涉及 explode 操作,它会增加DataFrame的行数。对于包含非常大列表的超大型数据集,这可能会消耗较多内存。然而,Polars的内部优化通常能很好地处理此类操作。
  • 通用性: 这种扁平化-计数-重聚合的模式不仅适用于列表交集,还可以推广到其他复杂的组内列表操作,例如查找在至少N个列表中出现的元素。
  • maintain_order=True: 在 group_by 中使用 maintain_order=True 可以确保最终结果的组顺序与原始DataFrame中首次出现的组顺序一致。如果顺序不重要,可以省略。
  • unique() 的使用: 在 agg 阶段使用 pl.col("values").unique() 是为了确保最终的交集列表只包含唯一的元素,因为在扁平化和过滤过程中,同一个交集元素可能会被多次选中。

通过将列表操作转换为更适合Polars表达式引擎的扁平化和窗口函数操作,我们能够高效且准确地实现分组列表的交集计算。这种方法体现了Polars在处理复杂数据转换时的强大灵活性。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。