AI工程SynapseB类

甘特图自动生成与工作日计算实践

从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个工作日?这个决策会影响整个数据管道的处理逻辑,后续无法随意更改。

可移植的原则

  1. 如果你在处理任何与时间相关的数据,第一件事是明确时间的语义:自然日、工作日、业务日,每种的计算规则不同。
  2. 如果你在编写日期计算函数,写完立即测试边界情况:0天、1天、周五开始+2天、跨节假日的情况。
  3. 如果你在从Excel导入数据,在读取层就完成数据类型转换,不要把字符串日期拖到业务逻辑层再处理。
  4. 如果你在对接第三方假期API,把节假日数据缓存在本地,避免每次计算都发网络请求。

结尾

甘特图生成这个场景看似简单,但"工作日"这个概念就有多种实现方式。在我的实践中,核心经验是把日期语义显式化——在函数签名、变量命名、数据注释中都明确标注使用的是哪种日期类型。如果你在项目中遇到类似问题,建议先画一张简单的状态图,标注清楚每个转换节点的输入输出类型,再动手写代码,能省下不少返工时间。