
本文详解如何使用 polars 高效计算同一数据集中不同交易类型(acqdisp = 'a' 或 'd')之间的日期间隔,包括同类型内(a→a、d→d)和跨类型间(a→d、d→a)的天数差,并提供可直接运行的代码实现。
本文详解如何使用 polars 高效计算同一数据集中不同交易类型(acqdisp = 'a' 或 'd')之间的日期间隔,包括同类型内(a→a、d→d)和跨类型间(a→d、d→a)的天数差,并提供可直接运行的代码实现。
在金融或交易数据分析中,常需追踪买卖行为的时间关联性——例如:上一笔卖出(D)距当前买入(A)已过去多少天?或最近一次买入(A)后多久发生首次卖出(D)?Polars 提供了高性能、表达力强的链式操作,但默认的 .diff().over("group") 仅支持组内连续差值(如所有 A 行之间、所有 D 行之间),无法直接捕获跨类型跳转(如 D → A)。要解决这一问题,核心在于识别类型切换点,并将前一类型最后的有效日期“向前填充”至当前行。
以下为完整解决方案,分步实现:
✅ 步骤 1:准备数据并确保日期类型正确
import polars as pl
df = pl.from_repr("""
┌─────────┬────────────┬────────────┬────────────┐
│ ACQDISP ┆ TRANDATE ┆ SHARES_ADJ ┆ TPRICE_ADJ │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 ┆ f64 │
╞═════════╪════════════╪════════════╪════════════╡
│ D ┆ 2001-12-31 ┆ 2000.0 ┆ 6.7 │
│ D ┆ 2001-12-31 ┆ 6000.0 ┆ 6.45 │
│ D ┆ 2001-12-31 ┆ 3000.0 ┆ 6.52 │
│ A ┆ 2004-08-09 ┆ 5.4e6 ┆ 60.75 │
│ A ┆ 2004-08-24 ┆ 2.1954e6 ┆ null │
│ D ┆ 2004-08-24 ┆ 4.63418e6 ┆ 52.435 │
│ A ┆ 2014-12-17 ┆ 3.84581e6 ┆ null │
└─────────┴────────────┴────────────┴────────────┘
""")
# 转换日期列并按时间升序排序(关键!顺序影响“前一行”的语义)
df = df.with_columns(TRANDATE=pl.col("TRANDATE").str.to_date()).sort("TRANDATE")
✅ 步骤 2:同时计算两类日期差值
- same: 同类型内(A→A 或 D→D)的相邻日期差(即原 .diff().over("ACQDISP"))
- different: 跨类型的日期差——即当前行日期减去上一次不同类型交易发生的日期
result = df.with_columns(
# 【同类型差】标准分组差值
same=pl.col("TRANDATE").diff().over("ACQDISP"),
# 【跨类型差】关键逻辑:
# 1. 当 ACQDISP 发生变化时(如 D→A),取前一行的 TRANDATE;
# 2. 对该序列做 forward_fill(),使变化点之后的行继承该“切换基准日”;
# 3. 当前行日期减去该基准日,即得跨类型天数。
different=(
pl.col("TRANDATE")
- pl.when(pl.col("ACQDISP") != pl.col("ACQDISP").shift())
.then(pl.col("TRANDATE").shift())
.forward_fill()
)
)
print(result.select(["ACQDISP", "TRANDATE", "same", "different"]))
输出结果清晰展示两类差值:
shape: (7, 4) ┌─────────┬────────────┬──────────────┬──────────────┐ │ ACQDISP ┆ TRANDATE ┆ same ┆ different │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ date ┆ duration[ms] ┆ duration[ms] │ ╞═════════╪════════════╪══════════════╪══════════════╡ │ D ┆ 2001-12-31 ┆ null ┆ null │ ← 首行无前序,both null │ D ┆ 2001-12-31 ┆ 0d ┆ null │ ← 同日D→D,same=0;仍属D组,no switch → different=null │ D ┆ 2001-12-31 ┆ 0d ┆ null │ │ A ┆ 2004-08-09 ┆ null ┆ 952d │ ← D→A切换!基准日=2001-12-31 → 2004-08-09 - 2001-12-31 = 952d │ A ┆ 2004-08-24 ┆ 15d ┆ 967d │ ← 同A组:same=15d;different仍沿用上一D日 → 2004-08-24 - 2001-12-31 = 967d │ D ┆ 2004-08-24 ┆ 967d ┆ 0d │ ← A→D切换!基准日=2004-08-24(前一行A)→ 差值为0 │ A ┆ 2014-12-17 ┆ 3767d ┆ 3767d │ ← D→A切换!基准日=2004-08-24 → 2014-12-17 - 2004-08-24 = 3767d └─────────┴────────────┴──────────────┴──────────────┘
⚠️ 注意事项与最佳实践
- 排序必须严格按时间升序:.shift() 和 .forward_fill() 均依赖行序,若未排序或降序,结果将完全错误。
- null 的语义明确:首行无前序、或同类型内首次出现时,different 为 null;而 same 在每组首行也为 null。
- 若需区分 A→D 与 D→A 方向,可额外添加标志列:
pl.col("ACQDISP").ne(pl.col("ACQDISP").shift()).alias("type_switch"), pl.when(pl.col("ACQDISP") == "A").then(pl.lit("D_to_A")).when(pl.col("ACQDISP") == "D").then(pl.lit("A_to_D")).alias("switch_direction") - 性能优势:全程使用 Polars 原生表达式,零 Python 循环,大数据集下毫秒级响应。
通过组合 shift()、when/then 条件逻辑与 forward_fill(),我们优雅地突破了 .over() 的分组限制,实现了灵活、可扩展的跨类别时间分析——这正是 Polars 函数式链式思维的核心价值。











