You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将跨天登录会话准确拆分至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])
        # 原代码未将结果赋值到对应列,此处保留原代码的不完整
优化方案

针对跨天会话处理和大数据量效率问题,采用生成会话覆盖的所有日期区间+向量化计算的方案,避免循环,同时精准计算每个班次的耗时。

核心思路

  1. 生成每个会话覆盖的所有完整日期,结合班次规则得到会话涉及的所有班次时间段
  2. 对每个时间段计算会话与班次的交集时长
  3. 按用户会话和班次分组求和,得到每个会话在各班次的总耗时

完整代码

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)

方案优势

  1. 精准处理跨天/跨多天会话:通过生成会话覆盖的所有日期,逐个计算每个日期下各班次的交集时长,避免原代码仅计算登录当天班次的错误。
  2. 高效处理大数据量:使用Pandas的向量化操作和explode,避免逐行循环,数十万行数据处理效率提升显著。
  3. 逻辑清晰可扩展:班次定义与计算逻辑分离,后续调整班次时间只需修改shifts列表即可。

内容的提问来源于stack exchange,提问作者Nikita Voevodin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 06:01:17