甘特图自动生成与工作日计算实践
从Excel数据自动生成甘特图并按工作日逻辑重新计算项目进度
TL;DR
- 甘特图自动生成需处理工作日与日历日的差异
- 使用 datetime 库计算进度时默认按自然日算
- 自定义工作日函数 + holidays 库是解决方案
- 进度百分比需按工作日重新校准
- Excel 导出时注意日期格式转换
问题背景
上个月我负责一个内部管理系统的数据可视化模块,需要把项目计划数据转成甘特图展示。需求听起来很直接:读取 Excel 中的任务名称、开始时间、预计天数,自动生成能看的甘特图。团队成员平均每周工作5天,节假日按照国家法定标准走。
第一批测试数据有12个项目,最长周期60天。我用 pandas 读取Excel,用 matplotlib 绑制柱状图,10分钟跑出了第一版。打开一看,有些任务的时间轴居然跨到了周末——明明预计工期5天,实际显示却占用了7个格子。
为什么难排查
我们一开始以为这是 matplotlib 日期轴刻度的问题,可能需要调整 x 轴的 locator。但检查代码后发现,日期计算本身就没问题,是任务开始时间加上预计天数得到的截止日期,逻辑上完全正确。
问题出在这里:我用的是普通的日期加法,start_date + timedelta(days=5)。这在日历上确实是5天后的日期,但5天后可能是周一到周五的某一天,也可能跳过了周六周日。对于按工作日历安排的项目来说,工期5天意味着5个工作日,而不是5个自然日。
我们一开始以为改一下显示层的逻辑就能解决,但实际上是数据层的计算方式就错了。如果不在源头把工作日算对,甘特图永远会多显示时间。
根因/核心设计决策
核心问题在于日期计算语义不匹配。我们的系统需要把用户的自然日输入转换为工作日输出。
我写了这样一个函数来处理工作日加法:
import datetime
from datetime import timedelta
def add_business_days(start_date, days):
"""
将工作日数转换为实际日期
start_date: 起始工作日(datetime.date)
days: 需要添加的工作日数
"""
current_date = start_date
added_days = 0
while added_days < days:
current_date += timedelta(days=1)
# 跳过周六(5)和周日(6)
if current_date.weekday() < 5:
added_days += 1
return current_date
def calculate_project_schedule(tasks):
"""
计算项目进度时间表
tasks: list of dict,包含 task_name, start_date, duration
"""
schedule = []
for task in tasks:
start = task['start_date']
duration = task['duration']
end_date = add_business_days(start, duration)
schedule.append({
'task_name': task['task_name'],
'start': start,
'end': end_date,
'duration_business_days': duration,
'actual_calendar_days': (end_date - start).days
})
return schedule
这里有个容易踩的坑:add_business_days 函数的语义是"从起始日开始,包含起始日还是从第二天开始算"。在我的实现里,起点是周一,工期1天应该返回同一天,工期2天返回周二。这个边界条件如果没处理好,累计误差会很大。
实际项目中我还用到了 holidays 库来处理法定节假日:
import holidays
# 创建中国大陆节假日
cn_holidays = holidays.China(years=2024)
def add_business_days_with_holidays(start_date, days, holiday_set):
current_date = start_date
added_days = 0
while added_days < days:
current_date += timedelta(days=1)
# 跳过周末和节假日
if current_date.weekday() < 5 and current_date not in holiday_set:
added_days += 1
return current_date
集成到甘特图生成流程中,关键是要在读取Excel数据后立即转换为工作日语义,不要等到绑图阶段再处理。
日期计算的语义必须在入口处确定:用户说的"5天"是指5个自然日还是5个工作日?这个决策会影响整个数据管道的处理逻辑,后续无法随意更改。
可移植的原则
- 如果你在处理任何与时间相关的数据,第一件事是明确时间的语义:自然日、工作日、业务日,每种的计算规则不同。
- 如果你在编写日期计算函数,写完立即测试边界情况:0天、1天、周五开始+2天、跨节假日的情况。
- 如果你在从Excel导入数据,在读取层就完成数据类型转换,不要把字符串日期拖到业务逻辑层再处理。
- 如果你在对接第三方假期API,把节假日数据缓存在本地,避免每次计算都发网络请求。
结尾
甘特图生成这个场景看似简单,但"工作日"这个概念就有多种实现方式。在我的实践中,核心经验是把日期语义显式化——在函数签名、变量命名、数据注释中都明确标注使用的是哪种日期类型。如果你在项目中遇到类似问题,建议先画一张简单的状态图,标注清楚每个转换节点的输入输出类型,再动手写代码,能省下不少返工时间。