PERT and schedule risk
pmcontrols.pert takes three-point estimates (a optimistic, m most
likely, b pessimistic) and returns both the classical analytic result and
a Monte Carlo schedule-risk simulation in one Result.
import pmcontrols as pm
r = pm.pert([
{"id": "X", "predecessors": [], "a": 1, "m": 2, "b": 9},
{"id": "Y", "predecessors": ["X"], "a": 2, "m": 3, "b": 10},
], seed=0)
r.stats["expected_duration"] # 7.0, sum of te along the te-critical path
r.stats["sigma_critical_path"] # 1.8856, analytic standard deviation
r.stats["mc_p80"] # 80th-percentile completion from simulation
r.table[["activity", "te", "var", "criticality_index"]]
The analytic estimate
Each activity gets the classical beta approximation
te = (a + 4m + b) / 6, var = ((b - a) / 6) ** 2
expected_duration sums te along the critical path of the te-network and
sigma_critical_path is the square root of the summed variances on that
path. This is the procedure in every project-management textbook (Malcolm
et al., 1959).
Why the simulation
The analytic estimate looks only at one critical path. When a second path is nearly as long, it can finish later than the nominal critical path on any given run, so the analytic number understates the risk. The Monte Carlo run samples every activity from a PERT-beta distribution (with alpha + beta = 6, the classic four-sigma range), reschedules the whole network thousands of times, and reports:
- the empirical completion distribution:
mc_mean,mc_p50,mc_p80,mc_p95; - the per-activity criticality index, the fraction of runs in which the activity lands on the critical path. An activity with a high criticality index but positive deterministic slack is exactly the risk the analytic method hides.
Options
n_sim(default20000): number of simulation runs.seed(default0): set for reproducible output; passNonefor a fresh draw each call.optimistic/most_likely/pessimistic: field names, default"a","m","b".
The estimates require a <= m <= b for every activity; otherwise a
ValueError is raised.