如何将跨天登录会话准确拆分至4个班次时段?
问题描述
我有一张包含数十万行的用户登录登出数据表,已转为Pandas DataFrame,示例数据如下:
data = [['aa', '2020-05-31 00:00:01', '2020-05-31 00:00:31'], ['bb','2020-05-31 00:01:01', '2020-05-31 00:02:01'], ['aa','2020-05-31 00:02:01', '2020-05-31 00:06:03'], ['cc','2020-05-31 00:03:01', '2020-05-31 00:04:01'], ['dd','2020-05-31 00:04:01', '2020-05-31 00:34:01'], ['aa', '2020-05-31 00:05:01', '2020-05-31 00:07:31'], ['bb','2020-05-31 00:05:01', '2020-05-31 00:06:01'], ['aa','2020-05-31 22:05:01', '2020-06-01 09:08:03'], # 修正原数据日期错误:2020年6月无31日 ['cc','2020-05-31 22:10:01', '2020-06-01 09:40:01'], # 同上修正 ['dd','2020-05-31 00:20:01', '2020-05-31 15:35:01']] df_test = pd.DataFrame(data, columns=['user_id','login', 'logout'], dtype='datetime64[ns]')
需要统计每个会话在4个班次的耗时:
- 夜班:00:00-06:00
- 早班:06:00-12:00
- 中班:12:00-18:00
- 晚班:18:00-24:00
现有代码无法正确处理跨天会话(比如22点登录次日9点登出的情况),同时原代码用循环处理数十万行数据效率极低,求Python优化方案。原有代码如下:
shifting = df_test.copy() # 提取日期,用于循环中动态生成班次时间 shifting['day'] = shifting['login'].dt.floor("D") # 添加4个班次的空列 shifting['night'] = '' shifting['morning'] = '' shifting['afternoon'] = '' shifting['evening'] = '' # 计算会话在单个班次的耗时逻辑 def time_in_shift(start, end, shift_start, shift_end): """ 计算会话在指定班次的耗时,但未正确处理跨多天的情况,仅简单将超过24小时的会话均分至4个班次。 参数: start (datetime): 登录时间 end (datetime): 登出时间 shift_start (datetime): 班次开始时间 shift_end (datetime): 班次结束时间 返回: 该班次的耗时(小时) """ # 会话超过24小时则均分至4个班次 if (end - start).total_seconds()/3600 > 24: return (end - start).total_seconds()/3600/4 else: if start < shift_start: start = shift_start if end > shift_end: end = shift_end # 计算耗时 time_spent = (end-start).total_seconds()/3600 # 负耗时置为0 if time_spent < 0: time_spent = 0 return time_spent # 逐行应用函数 for i in shifting.index: # 生成当前登录日期的4个班次时间 shift_start=(shifting.loc[i,'day'], shifting.loc[i,'day'] + timedelta(hours = 6), shifting.loc[i,'day'] + timedelta(hours = 12), shifting.loc[i,'day'] + timedelta(hours = 18)) shift_end= (shift_start[1], shift_start[2], shift_start[3], shift_start[0] + timedelta(days=1)) # 遍历4个班次计算耗时 for shift in range(4): shift_time = time_in_shift(shifting.loc[i,'login'], shifting.loc[i,'logout'], shift_start[shift], shift_end[shift]) # 原代码未将结果赋值到对应列,此处保留原代码的不完整
优化方案
针对跨天会话处理和大数据量效率问题,采用生成会话覆盖的所有日期区间+向量化计算的方案,避免循环,同时精准计算每个班次的耗时。
核心思路
- 生成每个会话覆盖的所有完整日期,结合班次规则得到会话涉及的所有班次时间段
- 对每个时间段计算会话与班次的交集时长
- 按用户会话和班次分组求和,得到每个会话在各班次的总耗时
完整代码
import pandas as pd from datetime import timedelta # 修正示例数据的日期错误(2020-06没有31号) data = [['aa', '2020-05-31 00:00:01', '2020-05-31 00:00:31'], ['bb','2020-05-31 00:01:01', '2020-05-31 00:02:01'], ['aa','2020-05-31 00:02:01', '2020-05-31 00:06:03'], ['cc','2020-05-31 00:03:01', '2020-05-31 00:04:01'], ['dd','2020-05-31 00:04:01', '2020-05-31 00:34:01'], ['aa', '2020-05-31 00:05:01', '2020-05-31 00:07:31'], ['bb','2020-05-31 00:05:01', '2020-05-31 00:06:01'], ['aa','2020-05-31 22:05:01', '2020-06-01 09:08:03'], ['cc','2020-05-31 22:10:01', '2020-06-01 09:40:01'], ['dd','2020-05-31 00:20:01', '2020-05-31 15:35:01']] df_test = pd.DataFrame(data, columns=['user_id','login', 'logout'], dtype='datetime64[ns]') # 定义班次信息:(班次名称, 开始小时, 结束小时) shifts = [ ('night', 0, 6), ('morning', 6, 12), ('afternoon', 12, 18), ('evening', 18, 24) ] # 步骤1:生成每个会话覆盖的所有日期 df_test['start_date'] = df_test['login'].dt.floor('D') df_test['end_date'] = df_test['logout'].dt.floor('D') # 生成日期序列:每个会话从start_date到end_date的所有日期 df_dates = df_test.assign( date=lambda x: x.apply(lambda row: pd.date_range(row['start_date'], row['end_date'], freq='D'), axis=1) ).explode('date').drop(columns=['start_date', 'end_date']) # 步骤2:为每个日期添加对应的班次时间段 df_shifts = df_dates.assign( shift=lambda x: x['date'].apply(lambda d: [(name, d + timedelta(hours=s), d + timedelta(hours=e)) for name, s, e in shifts]) ).explode('shift') # 拆分班次信息为单独列 df_shifts[['shift_name', 'shift_start', 'shift_end']] = pd.DataFrame(df_shifts['shift'].tolist(), index=df_shifts.index) df_shifts = df_shifts.drop(columns=['shift']) # 步骤3:计算会话与班次的交集时长(小时) def calculate_overlap(login, logout, shift_start, shift_end): # 交集的开始时间是两者较晚的那个,结束时间是两者较早的那个 overlap_start = max(login, shift_start) overlap_end = min(logout, shift_end) if overlap_start >= overlap_end: return 0.0 return (overlap_end - overlap_start).total_seconds() / 3600 df_shifts['hours'] = df_shifts.apply( lambda row: calculate_overlap(row['login'], row['logout'], row['shift_start'], row['shift_end']), axis=1 ) # 步骤4:按用户会话和班次分组求和,得到每个会话的各班次耗时 # 为每个会话添加唯一标识(user_id+登录时间) df_test['session_id'] = df_test['user_id'] + '_' + df_test['login'].astype(str) df_shifts['session_id'] = df_shifts['user_id'] + '_' + df_shifts['login'].astype(str) # 透视表得到最终结果 result = df_shifts.pivot_table( index=['session_id', 'user_id', 'login', 'logout'], columns='shift_name', values='hours', aggfunc='sum', fill_value=0.0 ).reset_index() # 重新排列列顺序 result = result[['user_id', 'login', 'logout', 'night', 'morning', 'afternoon', 'evening']] print(result)
方案优势
- 精准处理跨天/跨多天会话:通过生成会话覆盖的所有日期,逐个计算每个日期下各班次的交集时长,避免原代码仅计算登录当天班次的错误。
- 高效处理大数据量:使用Pandas的向量化操作和
explode,避免逐行循环,数十万行数据处理效率提升显著。 - 逻辑清晰可扩展:班次定义与计算逻辑分离,后续调整班次时间只需修改
shifts列表即可。
内容的提问来源于stack exchange,提问作者Nikita Voevodin
相关产品推荐
相关产品推荐

