Pandas按type分组统计当前行日期2年内的历史记录数
高效统计Pandas分组内过去2年的记录数
我有一个包含id、type、date字段的Pandas DataFrame,需要新增past_records_in_2_years列,实现按type分组后,统计当前行日期过去2年内的同组记录数(不包含当前行本身)。
示例数据
输入数据
id type date 1 a 2023-06-18 2 a 2022-06-18 3 a 2021-06-18 4 b 2023-06-18 5 b 2020-06-18 6 c 2023-06-18
输出数据
id type date past_records_in_2_years 1 a 2023-06-18 2 2 a 2022-06-18 1 3 a 2021-06-18 0 4 b 2023-06-18 0 5 b 2020-06-18 0 6 c 2023-06-18 0
原低效实现问题
最初用嵌套for循环实现,但百万级数据量下运行效率极低,原代码如下:
for i in range(len(df)): temp = df[df['type'] == df.loc[i]['type']].reset_index(drop = True) if len(temp) > 1: past_dates = 0 for j in range(len(temp)): if (temp.loc[j]['date'] - df.loc[i]['date']) / np.timedelta64(1, 'Y') < 3: past_dates += 1 if past_dates >= 2: df[i]['date'] = 1 else: df[i]['date'] = 0 else: df[i]['date'] = 0
高效解决方案
要处理百万级数据,必须避免循环,利用Pandas的向量化操作和分组窗口函数,以下是两种高效实现方式:
方法一:分组滚动窗口(rolling)
这种方法利用Pandas的滚动时间窗口功能,分组后统计每个日期前2年内的记录数:
import pandas as pd # 1. 将date列转换为datetime类型(确保日期格式正确) df['date'] = pd.to_datetime(df['date']) # 2. 按type分组后对date排序,滚动窗口依赖有序序列 df_sorted = df.sort_values(['type', 'date']).reset_index(drop=True) # 3. 定义2年的时间窗口(365*2天,可根据实际需求调整闰年逻辑) two_years_window = pd.Timedelta(days=365*2) # 4. 分组计算滚动窗口内的记录数,closed='left'表示不包含当前行 df_sorted['past_records_in_2_years'] = df_sorted.groupby('type')['date'] \ .rolling(window=two_years_window, closed='left') \ .count() \ .reset_index(level=0, drop=True) # 5. 合并回原DataFrame,恢复原始顺序 df = df.merge(df_sorted[['id', 'past_records_in_2_years']], on='id')
方法二:merge_asof区间匹配(大数据量首选)
merge_asof是基于二分查找的高效区间匹配方法,适合百万级以上数据:
import pandas as pd # 1. 转换日期类型 df['date'] = pd.to_datetime(df['date']) # 2. 生成匹配用的数据集,计算每个日期的2年前时间点 df_match = df.copy() df_match['lower_date'] = df_match['date'] - pd.Timedelta(days=365*2) # 3. 按type和date排序,merge_asof要求双方都按匹配键排序 df_sorted = df.sort_values(['type', 'date']) df_match_sorted = df_match.sort_values(['type', 'date']) # 4. 执行区间匹配:匹配同组内,date在[lower_date, 当前date)之间的记录 merged = pd.merge_asof( df_sorted, df_match_sorted[['type', 'date', 'id']], on='date', by='type', direction='backward', tolerance=pd.Timedelta(days=365*2) ) # 5. 统计每个原始id的匹配数量,减去1排除自身 record_counts = merged.groupby('id_x').size() - 1 # 映射回原DataFrame,空值填充为0并转为整数 df['past_records_in_2_years'] = df['id'].map(record_counts).fillna(0).astype(int)
关键优化说明
- 抛弃循环:用Pandas向量化操作替代O(n²)的嵌套循环,时间复杂度降至O(n log n)
- 排序前置:时间窗口和区间匹配都依赖有序序列,排序后能大幅提升计算效率
- 工具选择:数据量超大时优先用
merge_asof,它的底层是二分查找,比滚动窗口更高效
内容的提问来源于stack exchange,提问作者ANUJ PATEL
相关产品推荐
相关产品推荐

