
本文介绍如何使用 pandas 的向量化操作高效统计每个用户在各自日期前3个月内的累计出现次数,避免低效的 apply 或循环,适用于大规模数据场景。
在时间序列分析中,常需按用户(或分组键)统计“截至当前日期、过去 N 个月内的累计频次”。一个典型误区是误以为 cumcount() 能直接表达“滑动窗口内计数”——实际上,cumcount() 统计的是组内从首行到当前行的累计序号,它隐含的前提是:数据已按时间升序排列,且所有历史记录均落在当前行的“3个月窗口内”。
但注意:本题目标输出中的 N_Past_3_Months 列实际并非真正的滚动窗口计数,而是对每个用户按时间顺序的全局累计序号(+1)。观察预期结果可发现:
- Alice 的四条记录日期严格递增,且每条都比前一条晚于3个月?不,2023-01-01 → 2023-02-01 仅隔1个月,却仍计为2;
- 同时,Date_3_Months_Ago 列只是 Date - 3M 的简单偏移,并未用于过滤;
- 最终 N_Past_3_Months 值恰好等于该用户在排序后的位置索引 +1(即 cumcount() + 1)。
这说明:题目真实需求是“每个用户按时间先后顺序的序号”,而非动态滑动窗口计数。若需真正满足“仅统计 Date ∈ (Date−3M, Date] 区间内的同名记录数”,则必须使用 rolling 或 searchsorted 等高级向量化技巧。但根据答案与输出一致性,此处采用的是更轻量、完全向量化的方案:
✅ 正确向量化步骤(适用于累计序号型需求)
import pandas as pd
from pandas import DateOffset
# 原始数据
df = pd.DataFrame({
'Name': ['Alice', 'Alice', 'Bob', 'Alice', 'Bob', 'Alice'],
'Date': ['2023-01-01', '2023-02-01', '2023-02-15', '2023-03-01', '2023-03-20', '2023-04-01']
})
# 关键三步:类型转换 → 排序 → 向量计算
df['Date'] = pd.to_datetime(df['Date'])
df = df.sort_values(['Name', 'Date']).reset_index(drop=True) # 必须按 Name+Date 升序!
df['Date_3_Months_Ago'] = df['Date'] - DateOffset(months=3)
df['N_Past_3_Months'] = df.groupby('Name').cumcount() + 1
print(df)输出:
Name Date Date_3_Months_Ago N_Past_3_Months 0 Alice 2023-01-01 2022-10-01 1 1 Alice 2023-02-01 2022-11-01 2 2 Alice 2023-03-01 2022-12-01 3 3 Alice 2023-04-01 2023-01-01 4 4 Bob 2023-02-15 2022-11-15 1 5 Bob 2023-03-20 2022-12-20 2
⚠️ 重要注意事项
- 排序是前提:cumcount() 依赖行序,必须先 sort_values(['Name', 'Date']),否则结果无意义;
- DateOffset 的边界行为:DateOffset(months=3) 智能处理月末日期(如 2023-01-31 - 3M → 2022-10-31),比简单减90天更准确;
- 非滚动窗口:此解法 不检查 历史记录是否真在3个月内,仅做序号映射。若需严格滚动窗口(例如剔除超期记录),应改用如下模式:
# ✨ 真正的滚动窗口计数(推荐用于大数据,O(n log n))
df['Date_int'] = df['Date'].astype('int64') # 转纳秒级整数便于二分查找
def count_in_window(group):
dates = group['Date_int'].values
counts = []
for i, d in enumerate(dates):
window_start = d - 3 * 30 * 24 * 3600 * 10**9 # 近似3个月(纳秒)
# 更精确可用 pd.offsets.MonthBegin(3),但需配合 searchsorted
left = np.searchsorted(dates, window_start, side='left')
counts.append(i - left + 1)
return pd.Series(counts)
df['N_Rolling_3M'] = df.sort_values(['Name','Date']).groupby('Name').apply(count_in_window).explode().astype(int).values但该方案复杂度更高,且 DateOffset 不支持直接向量化滚动窗口。因此,请先确认业务语义:是“累计序号”还是“动态窗口频次” —— 二者算法与性能差异巨大。
✅ 总结
对于本题所示数据与预期输出,groupby(...).cumcount() + 1 是最简洁、零循环、全向量化的最优解。它内存友好、执行迅速,完美适配百万级用户-时间数据。牢记:向量化效能的前提,是精准理解指标定义;盲目套用“窗口”逻辑反而会引入错误或性能瓶颈。










