跳至主要内容

如何在 Python 中使用 Matplotlib 制作甘特图

学习如何在 Python 中使用 matplotlib 制作甘特图,以及这类可视化为何有用。
· 10分钟 阅读

使用 AI 探索

在这份循序渐进的指南中,我们将讨论什么是甘特图、这类可视化为何以及何时有用、如何用 matplotlib 在 Python 中制作甘特图,以及如何进一步自定义。过程中,我们还会在 matplotlib 中构建一些甘特图示例。

什么是甘特图,它为何有用?

甘特图是一种条形图,用于展示完整的项目规划与管理进度。它通常显示项目任务名称、任务的开始与结束日期、任务所属团队、任务完成状态(以百分比或分数表示)、各团队绩效以及项目的重要里程碑等信息。

在甘特图中,任务名称沿 y 轴排列,通常按其开始日期自上而下按时间顺序排序。x 轴用于显示时间区间,每个条形的宽度对应于该任务的实施时长。

由于甘特图本质上是特定形式的水平条形图,我们可以使用 Python 的多种数据可视化库来创建它,包括 matplotlib、Plotly、Bokeh 和 Altair。本教程将重点介绍在最为流行的库 – matplotlib 中制作甘特图。

如果您想探索用其他方式在 Python 中创建甘特图和其他类型的图表,欢迎查看课程 Introduction to Data Visualization with Plotly in Python。如果您需要巩固或提升 matplotlib 技能,这些课程会很有帮助:Introduction to Data Visualization with Matplotlib 和 Python for MATLAB Users。

如何在 Python 中用 Matplotlib 制作甘特图

使用 barh()

先从使用 matplotlib.pyplot 的 barh() 方法构建一个基础的 matplotlib 甘特图开始。

首先,我们需要导入必要的库:

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import datetime as dt

接下来,创建一些示例数据:

df = pd.DataFrame({'task': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'],
                  'team': ['R&D', 'Accounting', 'Sales', 'Sales', 'IT', 'R&D', 'IT', 'Sales', 'Accounting', 'Accounting', 'Sales', 'IT'],
                  'start': pd.to_datetime(['20 Oct 2022', '24 Oct 2022', '26 Oct 2022', '31 Oct 2022', '3 Nov 2022', '7 Nov 2022', '10 Nov 2022', '14 Nov 2022', '18 Nov 2022', '23 Nov 2022', '28 Nov 2022', '30 Nov 2022']),
                  'end': pd.to_datetime(['31 Oct 2022', '28 Oct 2022', '31 Oct 2022', '8 Nov 2022', '9 Nov 2022', '18 Nov 2022', '17 Nov 2022', '22 Nov 2022', '23 Nov 2022', '1 Dec 2022', '5 Dec 2022', '5 Dec 2022']),
                  'completion_frac': [1, 1, 1, 1, 1, 0.95, 0.7, 0.35, 0.1, 0, 0, 0]})
print(df)

输出:

  task        team      start        end  completion_frac
0     A         R&D 2022-10-20 2022-10-31             1.00
1     B  Accounting 2022-10-24 2022-10-28             1.00
2     C       Sales 2022-10-26 2022-10-31             1.00
3     D       Sales 2022-10-31 2022-11-08             1.00
4     E          IT 2022-11-03 2022-11-09             1.00
5     F         R&D 2022-11-07 2022-11-18             0.95
6     G          IT 2022-11-10 2022-11-17             0.70
7     H       Sales 2022-11-14 2022-11-22             0.35
8     I  Accounting 2022-11-18 2022-11-23             0.10
9     J  Accounting 2022-11-23 2022-12-01             0.00
10    K       Sales 2022-11-28 2022-12-05             0.00
11    L          IT 2022-11-30 2022-12-05             0.00

为便于基于此数据构建甘特图,我们需要再计算四列,以表示以下信息:

  1. 从整个项目开始到每个任务开始日期已过去/将过去多少天:
df['days_to_start'] = (df['start'] - df['start'].min()).dt.days
  1. 从整个项目开始到每个任务结束日期已过去/将过去多少天:
df['days_to_end'] = (df['end'] - df['start'].min()).dt.days
  1. 每个任务的持续时间,包含开始和结束日期:
df['task_duration'] = df['days_to_end'] - df['days_to_start'] + 1  # to include also the end date
  1. 将每个任务的完成状态从分数转换为分配给该任务的天数:
df['completion_days'] = df['completion_frac'] * df['task_duration']

检查结果:

print(df)

输出:

  task        team      start        end  completion_frac  days_to_start  \
0     A         R&D 2022-10-20 2022-10-31             1.00              0  
1     B  Accounting 2022-10-24 2022-10-28             1.00              4  
2     C       Sales 2022-10-26 2022-10-31             1.00              6  
3     D       Sales 2022-10-31 2022-11-08             1.00             11  
4     E          IT 2022-11-03 2022-11-09             1.00             14  
5     F         R&D 2022-11-07 2022-11-18             0.95             18  
6     G          IT 2022-11-10 2022-11-17             0.70             21  
7     H       Sales 2022-11-14 2022-11-22             0.35             25  
8     I  Accounting 2022-11-18 2022-11-23             0.10             29  
9     J  Accounting 2022-11-23 2022-12-01             0.00             34  
10    K       Sales 2022-11-28 2022-12-05             0.00             39  
11    L          IT 2022-11-30 2022-12-05             0.00             41  

    days_to_end  task_duration  completion_days 
0            11             12            12.00 
1             8              5             5.00 
2            11              6             6.00 
3            19              9             9.00 
4            20              7             7.00 
5            29             12            11.40 
6            28              8             5.60 
7            33              9             3.15 
8            34              6             0.60 
9            42              9             0.00 
10           46              8             0.00 
11           46              6             0.00 

现在,我们可以在 matplotlib 中生成一个基础甘特图:

plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'])
plt.show()

输出:

Gantt Chart in Matplotlib

上述图表仍需做一些调整,才能最大化地传达信息:

  • 添加有意义的标题和 x 轴刻度标签(日期)
  • 反转 y 轴,使任务按时间顺序自上而下排列
  • 添加网格
  • 按团队为任务着色
  • 显示图例
  • 添加任务完成状态

我们很快就会实现这些改进。不过在此之前,先来看如何绘制基础甘特图。

使用 broken_barh()

当某些任务包含在一段时间内分散的两个或更多子任务时,应使用另一个 matplotlib 方法 – broken_barh()。

为便于尝试这种方式,我们先准备一些示例数据:

  1. 从初始数据框中仅选取分配给 Sales 团队的任务(暂不考虑 completion_frac 列)。
  2. 将 start 和 end 两列分别重命名为 start_1 和 end_1 (便于处理)。
  3. 为部分任务添加更多子任务。
  4. 对每个子任务,按上文方式计算三列,表示以下信息:
    • 从整个项目开始到每个子任务开始日期已过去/将过去多少天。
    • 从整个项目开始到每个子任务结束日期已过去/将过去多少天。
    • 每个子任务的持续时间,包含开始和结束日期。
# 1
df2 = df[df['team']=='Sales'][['task', 'team', 'start', 'end']]

# 2
df2.rename(columns={'start': 'start_1', 'end': 'end_1'}, inplace=True)
df2.reset_index(drop=True, inplace=True)

# 3
df2['start_2'] = pd.to_datetime([None, '10 Nov 2022', '25 Nov 2022', None])
df2['end_2'] = pd.to_datetime([None, '14 Nov 2022', '28 Nov 2022', None])
df2['start_3'] = pd.to_datetime([None, None, '1 Dec 2022', None])
df2['end_3'] = pd.to_datetime([None, None, '5 Dec 2022', None])

# 4
for i in [1, 2, 3]:
    suffix = '_' + str(i)
    df2['days_to_start' + suffix] = (df2['start' + suffix] - df2['start_1'].min()).dt.days
    df2['days_to_end' + suffix] = (df2['end' + suffix] - df2['start_1'].min()).dt.days
    df2['task_duration' + suffix] = df2['days_to_end' + suffix] - df2['days_to_start' + suffix] + 1


print(df2)

输出:

task   team    start_1      end_1    start_2      end_2    start_3  \
0    C  Sales 2022-10-26 2022-10-31        NaT        NaT        NaT  
1    D  Sales 2022-10-31 2022-11-08 2022-11-10 2022-11-14        NaT  
2    H  Sales 2022-11-14 2022-11-22 2022-11-25 2022-11-28 2022-12-01  
3    K  Sales 2022-11-28 2022-12-05        NaT        NaT        NaT  

      end_3  days_to_start_1  days_to_end_1  task_duration_1  \
0        NaT                0              5                6  
1        NaT                5             13                9  
2 2022-12-05               19             27                9  
3        NaT               33             40                8  

  days_to_start_2  days_to_end_2  task_duration_2  days_to_start_3  \
0              NaN            NaN              NaN              NaN  
1             15.0           19.0              5.0              NaN  
2             30.0           33.0              4.0             36.0  
3              NaN            NaN              NaN              NaN  

  days_to_end_3  task_duration_3 
0            NaN              NaN 
1            NaN              NaN 
2           40.0              5.0 
3            NaN              NaN 

请注意,现在任务 D 有两个子任务,任务 H 有三个子任务,而任务 C 和 K 各有一个子任务。因此,我们将对 D 和 H 使用 broken_barh() 方法。由于该方法的语法比 barh() 略不直观,我们先看看它的主要参数及格式。broken_barh() 的必填参数有:

  • xranges – 一个形如 (xmin, xwidth) 的元组序列,用于表示每个条形的起点和延伸长度。这里,每个条形代表一个子任务。因此,该参数用于显示每个子任务的开始日期和持续时间。
  • yrange – 一个形如 (ymin, yheight) 的元组,用于表示每个条形的 y 位置和高度。

现在,我们来绘制“分段”的甘特图。步骤如下:

  1. 创建带子图的图形。
  2. 遍历数据框的行,检查任务有一、二或三个子任务。根据情况执行以下操作:
    • 一个子任务:使用 barh() 方法绘制一个条形,如前所示。
    • 两个子任务:使用 broken_barh() 方法绘制两个条形。
    • 三个子任务:使用 broken_barh() 方法绘制三个条形。
  3. 添加基础调整:设置并标注 y 轴刻度。
# 1
fig, ax = plt.subplots()

# 2
for index, row in df2.iterrows():
    if row['start_2'] is None:
        ax.barh(y=df2['task'], width=df2['task_duration_1'], left=df2['days_to_start_1'])
    elif row['start_2'] is not None and row['start_3'] is None:
        ax.broken_barh(xranges=[(row['days_to_start_1'], row['task_duration_1']), (row['days_to_start_2'], row['task_duration_2'])], yrange=(index + 1, 0.5))
    else:
        ax.broken_barh(xranges=[(row['days_to_start_1'], row['task_duration_1']), (row['days_to_start_2'], row['task_duration_2']), (row['days_to_start_3'], row['task_duration_3'])], yrange=(index + 1, 0.5))

# 3
ax.set_yticks([1.25, 2.25, 3.25, 4.25])
ax.set_yticklabels(df2['task'])

plt.show()

输出:

broken_barh() Gantt Chart

在上述代码中,对于只有一个子任务的任务,我们其实也可以用 broken_barh() 来绘制条形。在这种情况下,需要将这行代码:

ax.barh(y=df2['task'], width=df2['task_duration_1'], left=df2['days_to_start_1'])

替换为:

ax.broken_barh(xranges=[(row['days_to_start_1'], row['task_duration_1'])], yrange=(index + 1, 0.5))

这种做法还能让代码更加统一、易读。

如何在 Matplotlib 中自定义甘特图

让我们回到最初的数据框:

print(df)

输出:

  task        team      start        end  completion_frac  days_to_start  \
0     A         R&D 2022-10-20 2022-10-31             1.00              0  
1     B  Accounting 2022-10-24 2022-10-28             1.00              4  
2     C       Sales 2022-10-26 2022-10-31             1.00              6  
3     D       Sales 2022-10-31 2022-11-08             1.00             11  
4     E          IT 2022-11-03 2022-11-09             1.00             14  
5     F         R&D 2022-11-07 2022-11-18             0.95             18  
6     G          IT 2022-11-10 2022-11-17             0.70             21  
7     H       Sales 2022-11-14 2022-11-22             0.35             25  
8     I  Accounting 2022-11-18 2022-11-23             0.10             29  
9     J  Accounting 2022-11-23 2022-12-01             0.00             34  
10    K       Sales 2022-11-28 2022-12-05             0.00             39  
11    L          IT 2022-11-30 2022-12-05             0.00             41  

    days_to_end  task_duration  completion_days 
0            11             12            12.00 
1             8              5             5.00 
2            11              6             6.00 
3            19              9             9.00 
4            20              7             7.00 
5            29             12            11.40 
6            28              8             5.60 
7            33              9             3.15 
8            34              6             0.60 
9            42              9             0.00 
10           46              8             0.00 
11           46              6             0.00 

以及第一个甘特图:

plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'])
plt.show()

输出:

Gantt Chart in Matplotlib

我们希望对图表进行一些自定义,使其更具洞察力。

想要更深入地了解在 matplotlib 中自定义图表,请参考以下资源:Matplotlib Tutorial: Python Plotting 和 Matplotlib Cheat Sheet: Plotting in Python。

基础甘特图调整

包括:

  • 添加图表标题
  • 调整并自定义坐标轴
  • 添加网格

添加图表标题

任何图表(包括甘特图)都需要一个恰当的标题。假设我们的项目名为 Project X:

plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'])
plt.title('Project Management Schedule of Project X', fontsize=15)
plt.show()

输出:

Gantt Chart Matplotlib Plot Title

调整并自定义坐标轴并添加网格

在此步骤,我们将在前述代码的基础上,进行以下操作:

  1. 创建带子图的图形,以便使用 ax 对象。
  2. 反转 y 轴,使任务按时间顺序自上而下排列。
  3. 确定 x 轴刻度的最佳位置。
    • 假设我们想把 x 轴刻度放在每个周一,同时记住 Project X 是在周四开始的。
    • 为避免图表杂乱,我们只保留主刻度。
  4. 确定合适的 x 轴刻度标签及其格式。
    • 同样要记住 Project X 在周四开始,我们只需给周一加标签。
    • 由于 Project X 完全发生在 2022 年,不必在每个日期中标注年份。
    • 务必确保 x 轴刻度的数量与刻度标签的数量一致。
  5. 添加 x 轴刻度与刻度标签。
  6. 添加垂直网格。
# 1
fig, ax = plt.subplots()

plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'] + 1)
plt.title('Project Management Schedule of Project X', fontsize=15)

# 2
plt.gca().invert_yaxis()

# 3
xticks = np.arange(5, df['days_to_end'].max() + 2, 7)

# 4
xticklabels = pd.date_range(start=df['start'].min() + dt.timedelta(days=4), end=df['end'].max()).strftime("%d/%m")
# 5
ax.set_xticks(xticks)
ax.set_xticklabels(xticklabels[::7])

# 6
ax.xaxis.grid(True, alpha=0.5)

plt.show()

输出:

Gantt Chart Custom Axis

如果您需要更多 Python 练习以便更轻松地编写和阅读任何 Python 代码,我们的 Intermediate Python 课程是一个不错的参考。

按团队为任务着色

现在,我们根据任务所属团队为每个条形(代表特定任务)着色。为此我们将:

  1. 创建一个字典,以团队名称为键、matplotlib 基础颜色为值。
  2. 创建带子图的图形。
  3. 遍历数据框的每一行,为每行创建一个条形并添加对应团队的颜色。
  4. 加入之前应用的所有基础调整。
# 1
team_colors = {'R&D': 'c', 'Accounting': 'm', 'Sales': 'y', 'IT': 'b'}

# 2
fig, ax = plt.subplots()

# 3
for index, row in df.iterrows():
    plt.barh(y=row['task'], width=row['task_duration'], left=row['days_to_start'] + 1, color=team_colors[row['team']])

# 4
plt.title('Project Management Schedule of Project X', fontsize=15)
plt.gca().invert_yaxis()
ax.set_xticks(xticks)
ax.set_xticklabels(xticklabels[::7])
ax.xaxis.grid(True, alpha=0.5)
plt.show()

输出:

Gantt chart with tasks colored by team

添加图例

上面的甘特图缺少一个图例来说明各颜色对应的团队。为了解决这个问题,我们来创建 patches —— 使用预定义颜色填充的 matplotlib 2D 图形对象。我们将使用 matplotlib.patches.Patch 类生成一个补丁列表,每个团队一个,并按对应颜色填充:

patches = []
for team in team_colors:
    patches.append(matplotlib.patches.Patch(color=team_colors[team]))

现在,我们就可以为图表添加图例了:

fig, ax = plt.subplots()
for index, row in df.iterrows():
    plt.barh(y=row['task'], width=row['task_duration'], left=row['days_to_start'] + 1, color=team_colors[row['team']])
plt.title('Project Management Schedule of Project X', fontsize=15)
plt.gca().invert_yaxis()
ax.set_xticks(xticks)
ax.set_xticklabels(xticklabels[::7])
ax.xaxis.grid(True, alpha=0.5)

# Adding a legend
ax.legend(handles=patches, labels=team_colors.keys(), fontsize=11)

plt.show()

输出:

Gantt chart with a legend

现在的甘特图看起来信息量更大了。

关于提升图表美观度和可读性的更多技巧,请参阅课程 Improving Your Data Visualizations in Python。

添加完成状态

到目前为止,我们还没有使用在本教程开头计算的 completion_days column。现在,我们将用它来显示每个任务的完成阶段,使甘特图更具洞察力。

技术上,我们会在每个既有条形的上方再叠加一个条形。下方条形表示每个任务分配天数内的总体持续时间,上方条形表示该任务在分配天数内的完成状态。

对每个任务,这两条条形都会使用对应团队的相同颜色,但下方条形会更浅,以便区分两者。

换言之,如果某个团队在任务上超前,那么这两个条形叠加起来会显示该团队在该任务上已工作的天数多于预期(意味着该任务可能会在截止日期之前完成)。

反之,如果某个团队落后,则两条对应条形叠加起来会显示该团队在该任务上的工作天数少于预期(意味着该任务可能会在截止日期之后完成)。

fig, ax = plt.subplots()

for index, row in df.iterrows():

    # Adding a lower bar - for the overall task duration
    plt.barh(y=row['task'], width=row['task_duration'], left=row['days_to_start'] + 1, color=team_colors[row['team']], alpha=0.4)

    # Adding an upper bar - for the status of completion
    plt.barh(y=row['task'], width=row['completion_days'], left=row['days_to_start'] + 1, color=team_colors[row['team']])

plt.title('Project Management Schedule of Project X', fontsize=15)
plt.gca().invert_yaxis()
ax.set_xticks(xticks)
ax.set_xticklabels(xticklabels[::7])
ax.xaxis.grid(True, alpha=0.5)
ax.legend(handles=patches, labels=team_colors.keys(), fontsize=11)
plt.show()

输出:

Gantt chart with the status of completion

从上图可以看到,各团队完成任务的节奏并不一致,这在许多现实项目中很常见。为了从图中获取更多信息,假设当前日期为 2022 年 11 月 17 日,并在图上标记该日期:

fig, ax = plt.subplots()
for index, row in df.iterrows():
    plt.barh(y=row['task'], width=row['task_duration'], left=row['days_to_start'] + 1, color=team_colors[row['team']], alpha=0.4)
    plt.barh(y=row['task'], width=row['completion_days'], left=row['days_to_start'] + 1, color=team_colors[row['team']])

plt.title('Project Management Schedule of Project X', fontsize=15)
plt.gca().invert_yaxis()
ax.set_xticks(xticks)
ax.set_xticklabels(xticklabels[::7])
ax.xaxis.grid(True, alpha=0.5)
ax.legend(handles=patches, labels=team_colors.keys(), fontsize=11)

# Marking the current date on the chart
ax.axvline(x=29, color='r', linestyle='dashed')
ax.text(x=29.5, y=11.5, s='17/11', color='r')

plt.show()

输出:

Gantt chart with the current date

关于 Project X 当前状态,我们可以做出以下观察:

  • R&D 团队在任务 F 上超前,可能会提前完成。
  • IT 团队在任务 G 上有延迟,可能需要一些支持。
  • Sales 团队在任务 H 上按计划推进。
  • Accounting 团队较项目计划提前开始了任务 I。

这些观察有助于团队负责人和项目经理相应地调整项目进度,并及时分配必要资源,以避免交付延误。

其他可能的调整

我们还可以为 matplotlib 甘特图添加更多细节,例如:

  • 在每个条形上添加任务完成百分比。
  • 将某些任务细分为子任务(前文已介绍过技术实现)。
  • 标注相关联的任务。
  • 标记关键里程碑与截止日期。
  • 高亮周末与节假日。
  • 按团队或任务类型对条形排序。
  • 在 x 轴添加次刻度以提升信息粒度。
  • 改进图表的整体格式与布局。

不过,需要注意的是,保持甘特图的简洁,避免堆砌过多细节同样重要。与任何其他数据可视化一样,我们需要在图表的信息承载能力与其可读性之间保持平衡,同时也要兼顾用于构建图表的代码的可读性。

开始用 Matplotlib 在 Python 中制作您的甘特图

总而言之,在本教程中,我们探讨了:

  • 甘特图是什么
  • 它展示哪些信息
  • 在什么情况下使用
  • 使用 matplotlib 制作甘特图的不同方法与要点
  • 如何用 matplotlib 在 Python 中制作甘特图 
  • 如何进一步调整使其更具洞察力
  • 如何解读最终结果。

此外,我们查看了多个甘特图示例以及其构建的分步过程。

如果您希望以更系统的方式学习如何在 Python 中创建有说服力且信息丰富的图表,请查看我们的 Data Visualization with Python 技能路径。

主题