如何用Pandas按班次类型分组日期并计算员工班次信息
问题描述
我有某公司员工不同班次的模拟数据,班次以每8天为一个周期,包含工作/休息组合,如5-3(工作5天、休息3天)、6-2、4-4等。
原始数据
| Date | Employee | Hours worked |
|---|---|---|
| 2022-01-01 | Alice | 8 |
| 2022-01-02 | Alice | 8 |
| 2022-01-03 | Alice | 8 |
| 2022-01-03 | Bob | 8 |
| 2022-01-04 | Alice | 8 |
| 2022-01-04 | Bob | 8 |
| 2022-01-05 | Alice | 8 |
| 2022-01-05 | Bob | 8 |
| 2022-01-06 | Bob | 8 |
| 2022-01-07 | Bob | 8 |
| 2022-01-08 | Bob | 8 |
| 2022-01-09 | Alice | 8 |
| 2022-01-10 | Alice | 4 |
| 2022-01-11 | Alice | 8 |
| 2022-01-11 | Bob | 8 |
其中Alice的初始班次为5-3(1月1日至5日工作,6日至8日休息),Bob的初始班次为6-2(1月3日至8日工作,9日至10日休息)。
需求
用Pandas计算每位员工每天所属的班次类型,以及该班次内的具体天数(例如Alice1月3日是班次第3天,Bob则是第1天),且班次类型可能在每次周期结束后变更。
尝试过的方法
先处理单个员工:
- 将日期设为索引并补全日期间隙:
df.index = pd.DatetimeIndex(df['Date']) df = df.reindex(pd.date_range("2022/01/01","2022/03/31"))
- 创建"working"列,工作日报1,休息日报0:
df['working'] = 1 df['working'][df['Hours worked'].isnull()] = 0
原本想使用可重置的1日滚动求和,但无法实现,也不知道如何无需循环推广到所有员工。
期望输出
| Date | Employee | Hours worked | Shift type | Shift day |
|---|---|---|---|---|
| 2022-01-01 | Alice | 8 | 5-3 | 1 |
| 2022-01-02 | Alice | 8 | 5-3 | 2 |
| 2022-01-03 | Alice | 8 | 5-3 | 3 |
| 2022-01-03 | Bob | 8 | 6-2 | 1 |
| 2022-01-04 | Alice | 8 | 5-3 | 4 |
| 2022-01-04 | Bob | 8 | 6-2 | 2 |
| 2022-01-05 | Alice | 8 | 5-3 | 5 |
| 2022-01-05 | Bob | 8 | 6-2 | 3 |
| 2022-01-06 | Bob | 8 | 6-2 | 4 |
| 2022-01-07 | Bob | 8 | 6-2 | 5 |
| 2022-01-08 | Bob | 8 | 6-2 | 6 |
| 2022-01-09 | Alice | 8 | 6-2 | 1 |
| 2022-01-10 | Alice | 4 | 6-2 | 2 |
| 2022-01-11 | Alice | 8 | 6-2 | 3 |
| 2022-01-11 | Bob | 8 | 5-3 | 1 |
解决方案
核心思路是按员工分组处理,利用Pandas的groupby+自定义函数,结合日期差划分周期,同时识别班次类型、计算班次内天数。
步骤1:数据预处理
先补全每个员工的所有日期记录,避免遗漏休息天:
import pandas as pd # 转换日期格式 df['Date'] = pd.to_datetime(df['Date']) # 生成目标日期范围 date_range = pd.date_range(start='2022-01-01', end='2022-03-31') # 生成员工和日期的笛卡尔积,确保每个员工每天都有记录 full_index = pd.MultiIndex.from_product( [date_range, df['Employee'].unique()], names=['Date', 'Employee'] ).to_frame(index=False) # 合并原始数据,补全缺失工时为0 full_df = pd.merge(full_index, df, on=['Date', 'Employee'], how='left') full_df['Hours worked'] = full_df['Hours worked'].fillna(0) # 标记是否工作 full_df['working'] = (full_df['Hours worked'] > 0).astype(int)
步骤2:分组计算班次信息
为每个员工单独计算周期、班次类型和班次内天数:
def process_single_employee(group): # 按日期排序 group = group.sort_values('Date').reset_index(drop=True) # 以员工第一条记录为起点,每8天划分一个周期 group['cycle_num'] = ((group['Date'] - group['Date'].min()).dt.days) // 8 # 统计每个周期的工作/休息天数,生成班次类型 cycle_stats = group.groupby('cycle_num')['working'].agg(['sum', 'count']) cycle_stats['shift_type'] = cycle_stats['sum'].astype(str) + '-' + (cycle_stats['count'] - cycle_stats['sum']).astype(str) group = group.merge(cycle_stats['shift_type'], on='cycle_num', how='left') # 计算班次内天数:每个周期从1开始计数 group['shift_day'] = group.groupby('cycle_num').cumcount() + 1 return group # 按员工分组处理 result_df = full_df.groupby('Employee').apply(process_single_employee).reset_index(drop=True) # 整理成期望的输出格式 result_df = result_df[['Date', 'Employee', 'Hours worked', 'shift_type', 'shift_day']] result_df.columns = ['Date', 'Employee', 'Hours worked', 'Shift type', 'Shift day'] # 筛选到目标日期范围(可选,匹配示例输出) result_df = result_df[result_df['Date'] <= '2022-01-11'].sort_values(['Date', 'Employee'])
补充说明
如果班次类型是预设规则(而非通过工时统计),可以提前维护一个员工班次变更表(包含员工、生效日期、班次类型),再通过日期区间匹配的方式关联班次类型,之后再按周期计算班次内天数。
内容的提问来源于stack exchange,提问作者datadatadata
相关产品推荐
相关产品推荐

