Python案例如何用Networkx做计划评审

wen python案例 1

Python案例:如何用Networkx做计划评审(PERT/CPM)——从图论到项目管理的实战指南

目录导读

  1. 计划评审技术(PERT/CPM)与Networkx的关联
  2. 环境搭建与核心库安装
  3. 案例背景:一个真实软件开发项目的任务拆解
  4. 用Networkx构建项目网络图
  5. 关键路径计算:核心算法实现
  6. 时间参数计算:最早/最晚开始与结束时间
  7. 蒙特卡洛模拟:应对工期不确定性
  8. 可视化:绘制甘特图与网络图
  9. 问答环节:常见问题与避坑指南
  10. 总结与进阶资源

计划评审技术(PERT/CPM)与Networkx的关联

计划评审技术(PERT)和关键路径法(CPM)是项目管理中用于任务调度与工期优化的经典工具,其核心思想是将项目任务抽象为有向无环图(DAG),通过图论算法计算关键路径、浮动时间等指标,而Python的Networkx库正是处理DAG的利器——它提供了拓扑排序、最长路径等内置函数,能大幅简化PERT/CPM的实现复杂度。

Python案例如何用Networkx做计划评审

重点词汇: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天。

问答3dag_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基础版假设资源无限,若要考虑资源冲突(如人力有限),需结合资源平衡算法,可借助pulportools库,但Networkx可作为图建模基础。

问题3:代码中dag_longest_path可能不准确?
回答:它返回的是按权重和最大的路径,在工期为正时等价于关键路径,但需注意:如果存在多条路径长度一致,它只返回其中一条,可用nx.all_simple_paths遍历全源路径手动筛选。

问题4:实际项目中工期用整数还是浮点?
回答:建议浮点,三时估计的期望往往是浮点,取整会导致累积误差,蒙特卡洛模拟中更应保留高精度。

问题5:此案例能否用于关键链(Critical Chain)管理?
回答:关键链考虑了资源约束,而PERT只关注时间,案例中的浮动计算可用于识别“安全时间”,但若要转为关键链,需将所有浮动集中到缓冲区。


总结与进阶资源

本案例通过一个8任务的软件开发项目,完整演示了如何用Networkx实现:

  • PERT三时估计与期望工期计算
  • DAG构建与关键路径自动提取
  • 正向/反向传递求最早最晚时间
  • 蒙特卡洛模拟进行风险量化
  • 甘特图与网络图的可视化呈现

进阶方向

  1. 集成Excel:用openpyxl读取大型任务表,自动化生成PERT模型
  2. 动态关键路径:当任务实际延误时,用Networkx重计算
  3. 多项目共享资源:用networkx.algorithms.mis或第三方库解决
  4. 云部署:用Dash或Streamlit将案例做成Web工具

参考资源(无需联网):

  • Networkx官方文档:nx.algorithms.dag
  • PMI指南:PERT/CPM标准定义
  • 《Python科学计算与数据分析》中的图论章节

注意:本案例代码可直接在Jupyter Notebook或任何Python3.9+环境中运行,仅需安装所列库,若需适应更大规模项目(>1000任务),建议改用igraphgraph-tool,Networkx在节点数过多时可视化性能可能下降。

最后建议:将本文代码封装为ProjectNetwork类,包含add_taskcritical_pathmonte_carlo等方法,便于复用,项目管理不仅是技术问题,更是沟通与决策的桥梁——Networkx帮你把复杂依赖转化为清晰的图论语言。

抱歉,评论功能暂时关闭!