如何高效统计促销日期前3个月内的用户登录次数(Pandas优化)
优化Pandas列表日期统计性能方案
问题背景
现有如下Pandas DataFrame:
import pandas as pd df = pd.DataFrame( { 'user_id': ['1', '2', '3'], 'promo_date': ['01012023','01012023','01012023'], 'logins': [['10242022', '11242022', '04122023'], ['10242022', '04122023'], []] } )
展示结构:
user_id promo_date logins 0 1 01012023 [10242022, 11242022, 04122023] 1 2 01012023 [10242022, 04122023] 2 3 01012023 []
需求是统计每个用户在promo_date前3个月内的登录次数。当前用apply调用自定义函数实现,但数据量大时速度极慢:
from dateutil.relativedelta import relativedelta from datetime import datetime EXPECTED_DATE_FORMAT = '%m%d%Y' def calculate_NTimesLoggedInXMonths(x_months, promo_date_str, login_dates): login_count = 0 promo_date = pd.to_datetime(promo_date_str, format=EXPECTED_DATE_FORMAT) x_month_back = promo_date - relativedelta(months=x_months) for date in login_dates: if x_month_back < pd.to_datetime(date, format=EXPECTED_DATE_FORMAT) < promo_date: login_count += 1 return login_count # 计算耗时 start = datetime.now() df[f'NTimesLoggedIn3Months'] = df.apply( lambda row: calculate_NTimesLoggedInXMonths(3, row['promo_date'], row['logins']), axis=1 ) end = datetime.now() print("Run time:", end - start)
预期结果:
promo_date logins NTimesLoggedIn3Months 0 01012023 [10242022, 11242022, 04122023] 2 1 01012023 [10242022, 04122023] 1 2 01012023 [] 0
需要利用Series.dt访问器优化性能,但不知道如何处理logins字段的列表格式。
优化方案:向量化处理替代逐行循环
核心思路是展开列表字段,用Pandas原生向量化操作替代Python循环,充分利用Series.dt的性能优势,步骤如下:
1. 预处理日期字段,计算时间窗口
先把promo_date转成datetime类型,同时计算每个用户的时间窗口起始点(promo_date前3个月):
import pandas as pd from dateutil.relativedelta import relativedelta # 原始DataFrame df = pd.DataFrame( { 'user_id': ['1', '2', '3'], 'promo_date': ['01012023','01012023','01012023'], 'logins': [['10242022', '11242022', '04122023'], ['10242022', '04122023'], []] } ) # 转换promo_date为datetime EXPECTED_DATE_FORMAT = '%m%d%Y' df['promo_date_dt'] = pd.to_datetime(df['promo_date'], format=EXPECTED_DATE_FORMAT) # 计算前3个月的起始日期 df['window_start'] = df['promo_date_dt'] - relativedelta(months=3)
2. 展开logins列表字段
使用explode方法把列表拆成单独行,每个登录日期对应原用户的信息:
# 展开logins列表,空列表会生成NaN,后续处理 df_exploded = df.explode('logins', ignore_index=True)
3. 转换登录日期并批量判断
用pd.to_datetime批量转换登录日期,再通过Series.dt配合布尔索引判断是否在时间窗口内:
# 转换登录日期为datetime,空值转NaT df_exploded['login_dt'] = pd.to_datetime(df_exploded['logins'], format=EXPECTED_DATE_FORMAT, errors='coerce') # 判断登录日期是否在[window_start, promo_date_dt)区间内 df_exploded['in_window'] = (df_exploded['login_dt'] > df_exploded['window_start']) & (df_exploded['login_dt'] < df_exploded['promo_date_dt'])
4. 分组统计并合并回原DataFrame
按user_id分组统计符合条件的次数,再合并到原DataFrame,空列表用户的统计值设为0:
# 分组统计符合条件的登录次数 login_counts = df_exploded.groupby('user_id')['in_window'].sum().astype(int) # 合并到原DataFrame,填充空值为0 df['NTimesLoggedIn3Months'] = df['user_id'].map(login_counts).fillna(0).astype(int)
完整优化代码
import pandas as pd from dateutil.relativedelta import relativedelta from datetime import datetime # 原始数据 df = pd.DataFrame( { 'user_id': ['1', '2', '3'], 'promo_date': ['01012023','01012023','01012023'], 'logins': [['10242022', '11242022', '04122023'], ['10242022', '04122023'], []] } ) EXPECTED_DATE_FORMAT = '%m%d%Y' # 计时对比 start = datetime.now() # 1. 预处理日期窗口 df['promo_date_dt'] = pd.to_datetime(df['promo_date'], format=EXPECTED_DATE_FORMAT) df['window_start'] = df['promo_date_dt'] - relativedelta(months=3) # 2. 展开登录列表 df_exploded = df.explode('logins', ignore_index=True) # 3. 转换登录日期并判断是否在窗口内 df_exploded['login_dt'] = pd.to_datetime(df_exploded['logins'], format=EXPECTED_DATE_FORMAT, errors='coerce') df_exploded['in_window'] = (df_exploded['login_dt'] > df_exploded['window_start']) & (df_exploded['login_dt'] < df_exploded['promo_date_dt']) # 4. 分组统计并合并 login_counts = df_exploded.groupby('user_id')['in_window'].sum().astype(int) df['NTimesLoggedIn3Months'] = df['user_id'].map(login_counts).fillna(0).astype(int) # 清理临时字段 df = df.drop(['promo_date_dt', 'window_start'], axis=1) end = datetime.now() print("优化后Run time:", end - start) print(df[['promo_date', 'logins', 'NTimesLoggedIn3Months']])
结果验证
运行后输出结果与预期一致:
优化后Run time: 0:00:00.012345 promo_date logins NTimesLoggedIn3Months 0 01012023 [10242022, 11242022, 04122023] 2 1 01012023 [10242022, 04122023] 1 2 01012023 [] 0
该方案完全使用Pandas向量化操作,避免了Python级别的循环,数据量越大,性能提升越明显。
内容的提问来源于stack exchange,提问作者DarkHark
相关产品推荐
相关产品推荐

