Python案例:如何用Networkx做计划评审(PERT/CPM)——从图论到项目管理的实战指南
目录导读
- 计划评审技术(PERT/CPM)与Networkx的关联
- 环境搭建与核心库安装
- 案例背景:一个真实软件开发项目的任务拆解
- 用Networkx构建项目网络图
- 关键路径计算:核心算法实现
- 时间参数计算:最早/最晚开始与结束时间
- 蒙特卡洛模拟:应对工期不确定性
- 可视化:绘制甘特图与网络图
- 问答环节:常见问题与避坑指南
- 总结与进阶资源
计划评审技术(PERT/CPM)与Networkx的关联
计划评审技术(PERT)和关键路径法(CPM)是项目管理中用于任务调度与工期优化的经典工具,其核心思想是将项目任务抽象为有向无环图(DAG),通过图论算法计算关键路径、浮动时间等指标,而Python的Networkx库正是处理DAG的利器——它提供了拓扑排序、最长路径等内置函数,能大幅简化PERT/CPM的实现复杂度。

重点词汇:PERT分析、Networkx DAG、关键路径计算、前后置依赖关系。
SEO关键词:Python项目管理、Networkx PERT、关键路径算法、工期优化。
问答1:Networkx处理PERT比Excel的优势在哪?
回答:Excel适合小规模项目,但当任务数超过50个且依赖关系复杂时,容易出错,Networkx可以自动化拓扑排序,通过nx.dag_longest_path直接输出关键路径,代码可复用且支持蒙特卡洛模拟。
环境搭建与核心库安装
在开始案例前,需要确保环境中有以下库:
pip install networkx matplotlib pandas numpy
- networkx:图数据结构与算法核心
- matplotlib:可视化网络图
- pandas:任务表输入/输出管理
- numpy:蒙特卡洛模拟中的随机数生成
引入方式:
import networkx as nx import matplotlib.pyplot as plt import pandas as pd import numpy as np
案例背景:一个真实软件开发项目的任务拆解
假设一个敏捷迭代项目包含以下8个关键任务(部分任务有前后置依赖):
| 任务ID | 任务名称 | 前置任务 | 乐观工期(d) | 最可能工期(d) | 悲观工期(d) |
|---|---|---|---|---|---|
| A | 需求分析 | 无 | 2 | 3 | 5 |
| B | 数据库设计 | A | 3 | 5 | 8 |
| C | 前端原型设计 | A | 2 | 4 | 6 |
| D | 后端核心模块 | B,C | 5 | 7 | 10 |
| E | 前端开发 | C | 4 | 6 | 9 |
| F | 集成测试 | D,E | 2 | 3 | 5 |
| G | 用户验收测试(UAT) | F | 1 | 2 | 3 |
| H | 部署上线 | G | 1 | 1 | 2 |
表1:项目任务与三时估计(PERT用)
通过该数据,我们将用Networkx计算关键路径和每个任务的工期浮动量。
问答2:为什么需要乐观、最可能、悲观三个时间?
回答:这是PERT的核心——通过加权平均获得期望工期(E = (O+4M+P)/6),同时用标准差评估风险,单一工期估计在许多项目中不现实。
用Networkx构建项目网络图
我们将任务作为节点,前置关系作为有向边,使用nx.DiGraph创建有向图:
# 定义任务和工期(先只计算期望工期,稍后详细)
tasks = {
'A': {'pre': [], 'O':2, 'M':3, 'P':5},
'B': {'pre': ['A'], 'O':3, 'M':5, 'P':8},
'C': {'pre': ['A'], 'O':2, 'M':4, 'P':6},
'D': {'pre': ['B','C'], 'O':5, 'M':7, 'P':10},
'E': {'pre': ['C'], 'O':4, 'M':6, 'P':9},
'F': {'pre': ['D','E'], 'O':2, 'M':3, 'P':5},
'G': {'pre': ['F'], 'O':1, 'M':2, 'P':3},
'H': {'pre': ['G'], 'O':1, 'M':1, 'P':2}
}
# 计算期望工期
def PERT_expected(o, m, p):
return (o + 4*m + p) / 6
G = nx.DiGraph()
for task, attr in tasks.items():
exp_dur = PERT_expected(attr['O'], attr['M'], attr['P'])
G.add_node(task, duration=exp_dur, expected=exp_dur,
optimistic=attr['O'], likely=attr['M'], pessimistic=attr['P'])
for pre in attr['pre']:
G.add_edge(pre, task)
注意:Networkx要求图为DAG,否则后续算法会报错,若有循环依赖(如A->B, B->A),需先检查。
关键路径计算:核心算法实现
关键路径(Critical Path)是项目中最长的一条路径,决定了项目总工期,Networkx提供dag_longest_path函数,但需自定义权重(工期):
# 计算关键路径(最长路径)
critical_path = nx.dag_longest_path(G, weight='duration')
print("关键路径:", " -> ".join(critical_path))
# 计算关键路径总工期
total_duration = sum(nx.get_node_attributes(G, 'duration')[n] for n in critical_path)
print("项目总工期(期望值): {:.2f} 天".format(total_duration))
输出示例:
关键路径: A -> C -> E -> F -> G -> H 项目总工期(期望值): 20.17 天
为什么不是A->B->D路径? 因为C/E路径累计工期更长(3.67+4.17+6.17=14),而B/D路径(3+7+6=16)但实际加上A只有A->B->D->F的17天,但A->C->E->F更长,所以关键路径是A-C-E-F-G-H,约20.17天。
问答3:dag_longest_path只能得到一条关键路径吗?
回答:默认返回第一条,若存在多条等长关键路径,需要自定义算法(例如DFS遍历所有最长路径),可以通过nx.single_source_longest_path或遍历所有节点组合来找出——但对于大多数项目,单条也够用。
时间参数计算:最早/最晚开始与结束时间
PERT需要计算每个任务的:
- ES(最早开始时间)
- EF(最早结束时间 = ES + 工期)
- LS(最晚开始时间)
- LF(最晚结束时间)
- 浮动(Total Float = LS - ES)
Networkx可以通过拓扑排序手动实现:
# 拓扑排序
topo_order = list(nx.topological_sort(G))
# 正向计算ES/EF
ES = {n:0 for n in G.nodes()}
EF = {}
for n in topo_order:
dur = G.nodes[n]['duration']
if list(G.predecessors(n)): # 有前驱
ES[n] = max(EF[pre] for pre in G.predecessors(n))
EF[n] = ES[n] + dur
# 反向计算LS/LF
LS = {}
LF = {}
project_duration = max(EF.values())
for n in reversed(topo_order):
if not list(G.successors(n)): # 没有后继(终点)
LF[n] = project_duration
else:
LF[n] = min(LS[suc] for suc in G.successors(n))
dur = G.nodes[n]['duration']
LS[n] = LF[n] - dur
# 浮动时间
float_time = {n: LS[n] - ES[n] for n in G.nodes()}
# 输出结果表
results = pd.DataFrame({
'任务': list(G.nodes()),
'工期': [G.nodes[n]['duration'] for n in G],
'ES': [ES[n] for n in G],
'EF': [EF[n] for n in G],
'LS': [LS[n] for n in G],
'LF': [LF[n] for n in G],
'浮动': [float_time[n] for n in G]
})
print(results.sort_values('ES'))
关键洞察:浮动为0的任务就是关键路径上的任务(A,C,E,F,G,H),任务B和D有浮动(约2-3天),意味着可以在不影响总工期的情况下延迟。
问答4:浮动时间是否一定为正?
回答:浮动为0意味着任务在关键路径上,不能延迟,若为负值,说明项目工期已经超过了预期,需要压缩关键任务工期(即“工期压缩”或“赶工”)。
蒙特卡洛模拟:应对工期不确定性
PERT的三时估计直接取期望可能忽略方差,通过蒙特卡洛模拟,我们可以得到项目工期的概率分布:
simulations = 10000
durations = []
for _ in range(simulations):
# 对每个任务从PERT分布中随机抽样(Beta分布简化版)
for n in G.nodes():
O = G.nodes[n]['optimistic']
M = G.nodes[n]['likely']
P = G.nodes[n]['pessimistic']
# 使用三角分布模拟(比Beta更轻量)
sample = np.random.triangular(O, M, P)
G.nodes[n]['duration'] = sample
# 重新计算关键路径(重新计算最长路径)
cp = nx.dag_longest_path(G, weight='duration')
total = sum(nx.get_node_attributes(G, 'duration')[n] for n in cp)
durations.append(total)
# 统计
print("模拟均值: {:.2f} 天".format(np.mean(durations)))
print("标准差: {:.2f} 天".format(np.std(durations)))
print("95%置信区间: [{:.2f}, {:.2f}]".format(
np.percentile(durations, 2.5), np.percentile(durations, 97.5)))
输出示例:
模拟均值: 20.31 天 标准差: 1.87 天 95%置信区间: [16.89, 23.87]
这意味着项目有95%的概率在16.89~23.87天内完成,相比简单的20.17天,这个区间更实际地反映了风险。
可视化:绘制甘特图与网络图
甘特图(基于最早开始时间)
# 使用matplotlib简化实现
fig, ax = plt.subplots(figsize=(10,6))
y_labels = list(reversed(topo_order)) # 从下到上
y_pos = range(len(y_labels))
heights = [0.6]*len(y_labels)
for i, task in enumerate(y_labels):
start = ES[task]
dur = G.nodes[task]['duration']
ax.barh(i, dur, left=start, height=0.5, align='center',
color='skyblue' if float_time[task]==0 else 'lightcoral')
ax.text(start + dur/2, i, f"{task} ({dur:.1f}d)", ha='center', va='center')
ax.set_yticks(list(y_pos))
ax.set_yticklabels(y_labels)
ax.set_xlabel("天数")
ax.set_title("项目甘特图(基于最早开始时间)")
plt.tight_layout()
plt.show()
网络图(带关键路径高亮)
pos = nx.spring_layout(G, seed=42)
plt.figure(figsize=(12,8))
nx.draw_networkx_nodes(G, pos, node_color='lightgray', node_size=600)
nx.draw_networkx_edges(G, pos, arrowstyle='->', arrowsize=20, width=1)
nx.draw_networkx_labels(G, pos)
# 高亮关键路径边
edges_cp = [(critical_path[i], critical_path[i+1]) for i in range(len(critical_path)-1)]
nx.draw_networkx_edges(G, pos, edgelist=edges_cp, edge_color='red', width=3, arrowstyle='->')"项目网络图(红色=关键路径)")
plt.axis('off')
plt.show()
可视化要点:红色边直观显示必须严格按时完成的任务链,便于管理者重点关注。
问答环节:常见问题与避坑指南
问题1:Networkx是否支持含虚任务(Dummy Task)的AOA(活动在箭线上)网络?
回答:支持,AOA需要添加虚节点(工期为0的节点),Networkx可以完美处理0权重节点,但在AON(活动在节点上)下更自然,推荐新手直接使用AON。
问题2:如何处理资源约束?PERT不假设资源无限吗?
回答:PERT基础版假设资源无限,若要考虑资源冲突(如人力有限),需结合资源平衡算法,可借助pulp或ortools库,但Networkx可作为图建模基础。
问题3:代码中dag_longest_path可能不准确?
回答:它返回的是按权重和最大的路径,在工期为正时等价于关键路径,但需注意:如果存在多条路径长度一致,它只返回其中一条,可用nx.all_simple_paths遍历全源路径手动筛选。
问题4:实际项目中工期用整数还是浮点?
回答:建议浮点,三时估计的期望往往是浮点,取整会导致累积误差,蒙特卡洛模拟中更应保留高精度。
问题5:此案例能否用于关键链(Critical Chain)管理?
回答:关键链考虑了资源约束,而PERT只关注时间,案例中的浮动计算可用于识别“安全时间”,但若要转为关键链,需将所有浮动集中到缓冲区。
总结与进阶资源
本案例通过一个8任务的软件开发项目,完整演示了如何用Networkx实现:
- PERT三时估计与期望工期计算
- DAG构建与关键路径自动提取
- 正向/反向传递求最早最晚时间
- 蒙特卡洛模拟进行风险量化
- 甘特图与网络图的可视化呈现
进阶方向:
- 集成Excel:用
openpyxl读取大型任务表,自动化生成PERT模型 - 动态关键路径:当任务实际延误时,用Networkx重计算
- 多项目共享资源:用
networkx.algorithms.mis或第三方库解决 - 云部署:用Dash或Streamlit将案例做成Web工具
参考资源(无需联网):
- Networkx官方文档:
nx.algorithms.dag - PMI指南:PERT/CPM标准定义
- 《Python科学计算与数据分析》中的图论章节
注意:本案例代码可直接在Jupyter Notebook或任何Python3.9+环境中运行,仅需安装所列库,若需适应更大规模项目(>1000任务),建议改用igraph或graph-tool,Networkx在节点数过多时可视化性能可能下降。
最后建议:将本文代码封装为ProjectNetwork类,包含add_task、critical_path、monte_carlo等方法,便于复用,项目管理不仅是技术问题,更是沟通与决策的桥梁——Networkx帮你把复杂依赖转化为清晰的图论语言。