0

0

使用 Polars 高效计算 DataFrame 中按 ID 分组的时间间隔

DDD

DDD

发布时间:2025-08-11 17:32:27

|

513人浏览过

|

来源于php中文网

原创

使用 polars 高效计算 dataframe 中按 id 分组的时间间隔

本文详细阐述了如何利用 Polars 库的窗口函数 pl.Expr.over(),高效地计算 Pandas 或 Polars DataFrame 中每个独立标识符(ID)内部连续事件之间的时间间隔。通过避免传统的 map 或 apply 操作,我们展示了如何利用 Polars 原生表达式 API,结合 diff() 和 dt.total_seconds() 等函数,实现高性能的分组时间序列数据处理,最终生成新的时间间隔列。

在数据分析和处理中,我们经常会遇到需要计算时间序列数据中事件间时间间隔的场景。尤其是在处理用户行为日志、会话数据或传感器读数时,按特定实体(如用户ID、设备ID)分组并计算其内部连续事件的时间差,是常见的分析需求。Polars 作为一个高性能的 DataFrame 库,提供了强大的表达式系统和窗口函数功能,能够以极高的效率完成此类任务。

1. 问题背景与 Polars 优势

假设我们有一个包含 ID 和 Timestamp 列的 DataFrame,其中 Timestamp 表示某个会话或事件的结束时间。我们的目标是为每个唯一的 ID 计算其连续会话之间的时间间隔,并将结果存储在一个新列 time_between_sessions 中。

传统的 Pandas 操作可能会倾向于使用 groupby() 结合 apply() 或 transform(),但在大数据集上,这些方法可能效率不高。Polars 的设计理念是充分利用多核 CPU 和并行化,其表达式系统和惰性计算能力使其在处理大规模数据时表现出色。对于按组计算的需求,Polars 提供了更为优化的窗口函数(Window Functions)。

2. 核心概念:窗口函数 pl.Expr.over()

Polars 的 pl.Expr.over() 是实现分组计算的关键。它允许您在不显式执行 group_by() 操作的情况下,对数据进行分组并在每个组内应用表达式。这与 SQL 中的 OVER 子句概念类似,使得聚合、排名或差值计算等操作能够作用于数据的特定分区。

当与 diff() 函数结合使用时,over("ID") 会确保 diff() 操作仅在其所属的 ID 分组内进行,从而正确计算每个 ID 内部的连续时间差。

3. 解决方案详解

我们将通过一个最小可复现示例来演示如何使用 Polars 高效地计算每个 ID 的会话间隔。

3.1 准备示例数据

首先,我们创建一个包含 ID 和 Timestamp 的示例 DataFrame,并将其转换为 Polars DataFrame,确保 Timestamp 列是 Polars 的 Datetime 类型。

import polars as pl
import pandas as pd

# 创建一个示例 Pandas DataFrame
data = {
    'ID': ['A', 'A', 'A', 'B', 'B', 'B'],
    'Timestamp': ['2023-01-01 10:00:00', '2023-01-01 10:30:00' ,'2023-01-01 11:00:00', '2023-01-01 12:00:00', '2023-01-01 12:30:00', '2023-01-01 13:00:00']
}

df_pandas = pd.DataFrame(data)

# 转换为 Polars DataFrame 并确保 Timestamp 为 Datetime 类型
sessions_features = pl.from_pandas(df_pandas).with_columns(
   pl.col("Timestamp").str.to_datetime()
)

print("原始 Polars DataFrame:")
print(sessions_features)

输出示例:

Pic Copilot
Pic Copilot

AI时代的顶级电商设计师,轻松打造爆款产品图片

下载
原始 Polars DataFrame:
shape: (6, 2)
┌─────┬─────────────────────┐
│ ID  ┆ Timestamp           │
│ --- ┆ ---                 │
│ str ┆ datetime[μs]        │
╞═════╪═════════════════════╡
│ A   ┆ 2023-01-01 10:00:00 │
│ A   ┆ 2023-01-01 10:30:00 │
│ A   ┆ 2023-01-01 11:00:00 │
│ B   ┆ 2023-01-01 12:00:00 │
│ B   ┆ 2023-01-01 12:30:00 │
│ B   ┆ 2023-01-01 13:00:00 │
└─────┴─────────────────────┘

3.2 使用 pl.Expr.over() 计算时间间隔

现在,我们将使用 with_columns() 结合 pl.Expr.over() 来创建新的时间间隔列。

# 计算每个 ID 内部的会话时间间隔
result_df = sessions_features.with_columns(
  pl.col("Timestamp")
    .diff() # 计算当前行与上一行的时间差 (结果为 Duration 类型)
    .dt.total_seconds() # 将 Duration 转换为总秒数 (i64 类型)
    .fill_null(0) # 每个 ID 的第一个会话没有前一个会话,diff 结果为 null,此处填充为 0
    .over("ID") # 关键:此表达式在每个唯一的 "ID" 分组内执行
    .alias("time_between_sessions") # 为新列命名
)

print("\n计算时间间隔后的 DataFrame:")
print(result_df)

代码解析:

  1. pl.col("Timestamp"): 选择 Timestamp 列。
  2. .diff(): 计算当前行 Timestamp 与其前一行 Timestamp 的差值。对于每个 ID 的第一条记录,由于没有前一行,diff() 的结果将是 null。
  3. .dt.total_seconds(): diff() 的结果是一个 Duration(持续时间)类型。.dt.total_seconds() 将这个持续时间转换为总秒数,返回一个整数类型(i64)。
  4. .fill_null(0): 由于每个 ID 分组的第一条记录的 diff() 结果是 null,我们使用 fill_null(0) 将这些 null 值填充为 0,符合预期结果。
  5. .over("ID"): 这是核心步骤。它告诉 Polars,前面的 diff().dt.total_seconds().fill_null(0) 表达式应该在每个唯一的 ID 值所定义的“窗口”内独立执行。这意味着对于 ID='A' 的所有行,diff() 只会考虑 A 组内部的顺序;对于 ID='B' 的行,也只考虑 B 组内部的顺序。
  6. .alias("time_between_sessions"): 将新生成的列命名为 time_between_sessions。

预期输出:

计算时间间隔后的 DataFrame:
shape: (6, 3)
┌─────┬─────────────────────┬───────────────────────┐
│ ID  ┆ Timestamp           ┆ time_between_sessions │
│ --- ┆ ---                 ┆ ---                   │
│ str ┆ datetime[μs]        ┆ i64                   │
╞═════╪═════════════════════╪═══════════════════════╡
│ A   ┆ 2023-01-01 10:00:00 ┆ 0                     │
│ A   ┆ 2023-01-01 10:30:00 ┆ 1800                  │
│ A   ┆ 2023-01-01 11:00:00 ┆ 1800                  │
│ B   ┆ 2023-01-01 12:00:00 ┆ 0                     │
│ B   ┆ 2023-01-01 12:30:00 ┆ 1800                  │
│ B   ┆ 2023-01-01 13:00:00 ┆ 1800                  │
└─────┴─────────────────────┴───────────────────────┘

4. 避免 map 或 apply 的原因

在 Polars 中,强烈建议避免使用 map_groups()、apply() 或其他 Python 原生循环的函数,原因如下:

  • 性能瓶颈: map 或 apply 函数通常会在 Python 解释器层面操作,无法充分利用 Polars 底层的 Rust 优化和并行化能力。对于大型数据集,这会导致显著的性能下降。
  • 惰性求值: Polars 的许多操作都支持惰性求值,这意味着它会构建一个执行计划,只有在需要结果时才真正执行计算。而 map 或 apply 会立即触发计算,打破了惰性求值的优势。
  • 内存效率: Polars 的表达式 API 经过优化,通常能以更低的内存消耗完成任务。map 或 apply 可能会在 Python 对象之间进行不必要的转换,增加内存开销。
  • 可读性与维护性: 虽然 map 看起来灵活,但当有原生表达式可以实现相同功能时,使用原生表达式通常代码更简洁、意图更明确,也更容易被 Polars 优化器理解和加速。

5. 注意事项与最佳实践

  • 数据排序: diff() 函数计算的是当前行与其“前一行”的差值。因此,为了确保 time_between_sessions 的计算逻辑正确,数据在每个 ID 分组内部必须按照 Timestamp 列进行升序排序。如果您的原始数据未排序,请务必在执行 diff() 之前添加 .sort("Timestamp") 操作,例如:
    result_df = sessions_features.sort("ID", "Timestamp").with_columns(
      pl.col("Timestamp").diff().dt.total_seconds().fill_null(0).over("ID").alias("time_between_sessions")
    )

    在我们的示例中,数据已经按 ID 和 Timestamp 排序,所以可以省略 sort。

  • 数据类型: 确保时间戳列是 Polars 的 Datetime 类型。如果它是字符串,需要使用 str.to_datetime() 进行转换。
  • 结果类型: dt.total_seconds() 返回的是 i64(64位整数),这对于大多数时间间隔计算是足够的。如果需要浮点数精度(例如毫秒),可以使用 dt.total_milliseconds() 或 dt.total_microseconds()。

总结

通过本教程,我们学习了如何利用 Polars 的 pl.Expr.over() 窗口函数,结合 diff() 和 dt.total_seconds() 等表达式,高效且优雅地计算 DataFrame 中按组划分的时间间隔。这种方法充分利用了 Polars 的高性能特性,避免了传统 map 或 apply 可能带来的性能瓶颈,是处理大规模时间序列分组数据的推荐实践。掌握 over() 函数的使用,将极大地提升您在 Polars 中处理复杂数据转换任务的能力。

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

752

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

636

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

758

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

618

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1262

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

547

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

577

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

706

2023.08.11

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

36

2026.01.14

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 0.7万人学习

Django 教程
Django 教程

共28课时 | 3.1万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号