
本文介绍一种使用 pandas 高效实现“按 7 小时/天上限、跳过周末、顺序填充工时”的自动化方案,适用于项目排期、资源调度等场景。核心思路是基于业务日偏移动态生成 plannedstart,并智能拆分 hours 字段。
在实际项目管理或人力资源排班中,常需将总工时(如 15 小时)按「单日最多 7 小时」「仅限周一至周五」的约束,从最早起始日开始连续分配。难点在于:工时分配不是独立的——后序 ID 必须避开前序已占满的工作日,即需全局顺序排程,而非简单按 ID 分组处理。
幸运的是,pandas 提供了优雅的向量化解决方案,无需显式循环或状态维护。关键在于利用 BusinessDay 偏移与 groupby.cumcount() 的协同,实现「按需重复行 + 按组递增工作日 + 精确拆分尾数」三步闭环。
✅ 实现步骤详解
首先确保日期列已转为 datetime64 类型(已含在原始代码中):
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]
})
df['EarliestStart'] = pd.to_datetime(df['EarliestStart'], format='%d.%m.%Y')接着执行核心逻辑:
# Step 1: 按所需天数重复每行(向上取整:ceil(Hours / 7))
out = df.loc[df.index.repeat(np.ceil(df['Hours'].div(7)))]
# Step 2: 为每组(原 ID)添加递增的 BusinessDay 偏移
n = out.groupby(level=0).cumcount()
out['PlannedStart'] = out['EarliestStart'] + n * pd.offsets.BusinessDay()
# Step 3: 提取星期名称(自动本地化,如 'Thursday')
out['Weekday'] = out['PlannedStart'].dt.day_name()
# Step 4: 计算每日分配工时 —— 前 N−1 天为 7h,最后一天为余数(若余数为 0,则最后两天均分?不,此处按题意设为 7)
s = out.pop('Hours').mod(7)
out['HoursSplitted'] = np.where(
out.index.duplicated(keep='last') | s.eq(0),
7.0,
s
)
# 可选:格式化输出为欧洲风格(dd.MM.yyyy)及千分位逗号
out['PlannedStart'] = out['PlannedStart'].dt.strftime('%d.%m.%Y')
out['EarliestStart'] = out['EarliestStart'].dt.strftime('%d.%m.%Y')
out['HoursSplitted'] = out['HoursSplitted'].apply(lambda x: f"{x:.2f}".replace('.', ','))⚠️ 注意事项与边界说明
- BusinessDay 默认跳过周六、周日及节假日(如需自定义节假日,可传入 holidays= 参数);
- groupby(level=0) 依赖 repeat 后索引保留原始位置,因此必须使用 .loc[...] 而非 .reindex(...);
- s.eq(0) 判断余数为 0 的情况(如 14h → 2×7h),此时末尾两行均应为 7h,代码中通过 duplicated(keep='last') | s.eq(0) 确保倒数第二行也设为 7;
- 若存在跨月/跨年排期,BusinessDay 仍能正确计算(如 2023-12-29 → 2024-01-02);
- 输出顺序严格保持原始 ID 顺序,且同一 ID 的多行按 PlannedStart 递增排列,天然满足“最早可用”语义。
? 总结
该方案以声明式风格替代过程式逻辑,充分利用 pandas 的索引对齐与时间偏移能力,兼具性能与可读性。它不仅解决了示例中的 7 小时/天、跳过周末、全局顺序占用等约束,还可轻松扩展支持:
? 自定义日工作上限(修改除数 7);
? 多班次/多资源并行排程(增加分组维度);
? 加入优先级权重或截止日期约束(配合 sort_values 预排序)。
对于中等规模数据(










