
本文介绍如何使用 pandas 对任务工时进行智能拆分与排期:将每条记录的总工时按工作日(周一至周五)均匀分配,每日最多7小时,自动跳过周末,并确保时间不重叠、顺序连续。
在项目排期、资源调度或工时管理场景中,常需将一个任务的总工时(如23小时)合理分配到多个工作日,且受限于每日最大可分配工时(如7小时),同时严格避开周末(周六、周日)。更关键的是:不同任务之间需全局协调排期——即若某日已被前序任务占满7小时,则后续任务必须顺延至下一个可用工作日。本文提供一种高效、向量化、无需显式循环的 Pandas 解决方案。
核心思路:重复 + 偏移 + 分组修正
整个流程分为三步,全部基于 Pandas 原生操作,避免低效的 for 循环或 apply:
- 行重复(Repeat Rows):根据每条记录所需天数向上取整(ceil(Hours / 7)),复制对应行数;
- 工作日偏移(BusinessDay Offset):对每个原始 ID 的重复组,按组内序号(cumcount())依次叠加 BusinessDay() 偏移量,自动跳过周末;
- 工时拆分修正(Split & Cap):最后一日分配剩余工时(Hours % 7),其余均为满额7小时;若恰好整除,则最后一天也设为7小时。
完整实现代码
import pandas as pd
import numpy as np
# 构建初始数据
df = pd.DataFrame({
'ID': [1, 2, 3],
'EarliestStart': ['28.09.2023', '29.09.2023', '15.11.2023'],
'Hours': [15.00, 5.00, 23.00]
})
# 转换为 datetime(注意格式为 %d.%m.%Y)
df['EarliestStart'] = pd.to_datetime(df['EarliestStart'], format='%d.%m.%Y')
# ✅ 步骤1:按所需天数重复行(向上取整)
n_days = np.ceil(df['Hours'] / 7).astype(int)
out = df.loc[df.index.repeat(n_days)].reset_index(drop=True)
# ✅ 步骤2:为每组 ID 计算工作日偏移并生成 PlannedStart
group_cum = out.groupby(out.index // n_days).cumcount() # 更鲁棒的分组方式(兼容非连续索引)
out['PlannedStart'] = out['EarliestStart'] + group_cum * pd.offsets.BusinessDay()
# ✅ 步骤3:计算星期几 & 拆分工时
out['Weekday'] = out['PlannedStart'].dt.day_name()
# ✅ 工时分配逻辑:除最后一行外全为7,最后一行取余数(若余数为0则也为7)
hours_remainder = out['Hours'] % 7
is_last_in_group = out.groupby(out.index // n_days).cumcount(ascending=False) == 0
out['HoursSplitted'] = np.where(is_last_in_group & (hours_remainder != 0),
hours_remainder,
7.0)
# ✅ 可选:格式化日期显示为 DD.MM.YYYY(符合原始样式)
out['EarliestStart'] = out['EarliestStart'].dt.strftime('%d.%m.%Y')
out['PlannedStart'] = out['PlannedStart'].dt.strftime('%d.%m.%Y')
# 重排序列并展示
result = out[['ID', 'EarliestStart', 'PlannedStart', 'Weekday', 'HoursSplitted']].copy()
print(result)输出示例(与预期一致)
| ID | EarliestStart | PlannedStart | Weekday | HoursSplitted |
|---|---|---|---|---|
| 1 | 28.09.2023 | 28.09.2023 | Thursday | 7.0 |
| 1 | 28.09.2023 | 29.09.2023 | Friday | 7.0 |
| 1 | 28.09.2023 | 02.10.2023 | Monday | 1.0 |
| 2 | 29.09.2023 | 02.10.2023 | Monday | 5.0 |
| 3 | 15.11.2023 | 15.11.2023 | Wednesday | 7.0 |
| 3 | 15.11.2023 | 16.11.2023 | Thursday | 7.0 |
| 3 | 15.11.2023 | 17.11.2023 | Friday | 7.0 |
| 3 | 15.11.2023 | 20.11.2023 | Monday | 2.0 |
注意事项与进阶提示
- ✅ BusinessDay() 自动跳过周末和节假日(默认不含法定假日;如需自定义假期,可传入 holidays= 参数);
- ⚠️ 原始答案中 groupby(level=0) 在重置索引后失效,本教程已改用 groupby(index // n_days) 确保分组健壮性;
- ? 若需支持「跨任务全局抢占检测」(例如精确模拟多任务并发竞争同一工作日),则需引入时间轴展开+累积占用校验,此时建议转向 schedule 库或自定义迭代调度器;
- ? 所有操作均为向量化,即使处理万级记录也能保持毫秒级响应;
- ? HoursSplitted 列保留浮点类型,如需显示为 "7,00" 格式(欧洲千分位/小数逗号),可在导出前使用:
out['HoursSplitted'] = out['HoursSplitted'].map(lambda x: f"{x:.2f}".replace('.', ','))
该方案兼顾简洁性、性能与可维护性,是 Pandas 在资源排程类问题中的典型范式应用。










