Project Management & Agile Module 4 – Scheduling: networks, critical path and Gantt charts
Three-point estimates and schedule risk analysis
Estimate durations as ranges, compare PERT and triangular weighting, run a seeded Monte Carlo analysis for P50 and P80 dates, and set contingency from it.
What you will learn
- Estimate activity ranges with three-point estimates and choose triangular or PERT weighting
- Calculate a single-path PERT estimate and the chance of meeting a date from it
- Interpret P50 and P80 finish dates from a Monte Carlo schedule risk analysis
- Explain merge bias and why parallel paths make finishing on time less likely
- Set schedule contingency from the analysis instead of padding
Before you start
What you’ll learn · Before you start
What you will learn
- Estimate activity ranges with three-point estimates and choose triangular or PERT weighting
- Calculate a single-path PERT estimate and the chance of meeting a date from it
- Interpret P50 and P80 finish dates from a Monte Carlo schedule risk analysis
- Explain merge bias and why parallel paths make finishing on time less likely
- Set schedule contingency from the analysis instead of padding
Before you start
On this page
The tech fest’s critical path says 38 working days, yet in 10,000 simulated runs of the same plan it was over by day 38 in only 14.7 % of them, and in 2.7 % with a different but equally reasonable way of reading the estimates. A date computed from single durations is the date of a plan in which nothing goes wrong.
You will turn each estimate into a range, add the ranges along a path with PERT, run a seeded Monte Carlo analysis of the whole network, see why paths that meet make lateness more likely, and set the contingency an 80 % date needs.
Three numbers describe an estimate better than one
Ask the sponsor team how long signing sponsors will take and the honest answer is a range: 8 days if every company replies at once, 10 most likely, 16 if two of them stall. A three-point estimate: An estimate given as three durations, optimistic, most likely and pessimistic, so that its range and its shape can be used in a risk analysis. records exactly that: optimistic (O), most likely (M) and pessimistic (P). GAO’s schedule guide asks for such fields in the schedule before a risk analysis, gathered from the people who know the work, and anonymously where that helps them speak freely. The fest’s ranges are skewed to the right, as most schedule ranges are: things can go much more wrong than right.
Two ways of weighting the three numbers are common. PERT, from the 1959 paper of Malcolm, Roseboom, Clark and Fazar, gives the most likely value four times the weight of each extreme:
In words: the expected time is a weighted average, and the standard deviation is one sixth of the range. For signing sponsors, (8 + 40 + 16) ÷ 6 = 10.67 days, with σ = 8 ÷ 6 = 1.33. A triangular distribution weights the three equally, (O + M + P) ÷ 3, which gives 11.33 days: more weight on the long tail, so later dates. The program below prints both for every activity, then uses them:
"""A schedule risk analysis of the tech fest: three-point estimates, the single-path PERT sum and a Monte Carlo run.
Each activity has an optimistic (O), a most likely (M) and a pessimistic (P) duration in working days; M is the
duration the critical path lesson used. PERT weights them (O + 4M + P) / 6 with a standard deviation of (P - O) / 6
(Malcolm, Roseboom, Clark and Fazar, Operations Research, 1959); a triangular distribution weights them equally,
(O + M + P) / 3. The Monte Carlo run draws every activity 10,000 times from each distribution, with a fixed seed so
that every run of this program prints the same numbers, and recalculates the whole network each time.
"""
import math
import random
from statistics import NormalDist
# id: (name, O, M, P, [(predecessor, type, lag)])
FEST = {
"A": ("Authorise plan and budget", 2, 3, 5, []),
"B": ("Book the auditorium", 1, 2, 4, [("A", "FS", 0)]),
"C": ("Prepare sponsor packages", 4, 5, 8, [("A", "FS", 0)]),
"D": ("Sign sponsors", 8, 10, 16, [("C", "FS", 0)]),
"E": ("Design the coding contest", 6, 8, 12, [("A", "FS", 0)]),
"F": ("Build the robotics arena", 10, 12, 18, [("D", "FS", 0)]),
"G": ("Line up speakers", 4, 6, 9, [("B", "FS", 0)]),
"H": ("Build the registration form", 3, 4, 6, [("E", "FS", 0)]),
"I": ("Run the publicity campaign", 9, 10, 13, [("H", "SS", 2)]),
"J": ("Print posters", 2, 3, 5, [("G", "FS", 0)]),
"K": ("Plan crowd control and first aid", 4, 5, 7, [("B", "FS", 0)]),
"L": ("Set up sound, lights and stage", 2, 2, 4, [("F", "FS", 0), ("K", "FS", 0)]),
"M": ("Hold the fest", 2, 2, 2, [("L", "FS", 0), ("H", "FS", 0), ("J", "FS", 0), ("I", "FF", 0)]),
"N": ("Close the accounts and report", 3, 4, 7, [("M", "FS", 0)]),
}
RUNS, SEED = 10_000, 2026
SUCC = {a: [(s, k, lag) for s in FEST for p, k, lag in FEST[s][4] if p == a] for a in FEST}
def finish_and_critical(dur):
es, ef = {}, {}
for a, (*_, links) in FEST.items():
start = 0.0
for p, kind, lag in links:
start = max(start, {"FS": ef[p] + lag, "SS": es[p] + lag, "FF": ef[p] + lag - dur[a]}[kind])
es[a], ef[a] = start, start + dur[a]
end = max(ef.values())
ls, lf = {}, {}
for a in reversed(list(FEST)):
finish = end
for s, kind, lag in SUCC[a]:
finish = min(finish, {"FS": ls[s] - lag, "SS": ls[s] - lag + dur[a], "FF": lf[s] - lag}[kind])
lf[a], ls[a] = finish, finish - dur[a]
return end, [a for a in FEST if ls[a] - es[a] < 1e-9]
def pert_draw(rng, o, m, p):
"""A draw from the PERT (beta) distribution with the same O, M and P."""
if p == o:
return float(o)
alpha = 1 + 4 * (m - o) / (p - o)
beta = 1 + 4 * (p - m) / (p - o)
return o + (p - o) * rng.betavariate(alpha, beta)
def percentile(xs, q):
xs = sorted(xs)
return xs[min(len(xs) - 1, int(math.ceil(q * len(xs))) - 1)]
most_likely = {a: FEST[a][2] for a in FEST}
end_m, path = finish_and_critical(most_likely)
print(f"With the most likely durations: finish after {end_m:.0f} days, critical path {' '.join(path)}")
print(f"{'Id':<3}{'O':>3}{'M':>3}{'P':>3}{'PERT mean':>11}{'PERT sd':>9}{'Triangular mean':>17}")
for a, (_, o, m, p, _) in FEST.items():
print(f"{a:<3}{o:>3}{m:>3}{p:>3}{(o + 4 * m + p) / 6:>11.2f}{(p - o) / 6:>9.2f}{(o + m + p) / 3:>17.2f}")
mean = sum((FEST[a][1] + 4 * FEST[a][2] + FEST[a][3]) / 6 for a in path)
sd = round(math.sqrt(sum(((FEST[a][3] - FEST[a][1]) / 6) ** 2 for a in path)), 1)
curve = NormalDist(mean, sd)
print(f"Single-path PERT on {''.join(path)}: mean {mean:.1f} days, sd {sd}")
print(f" normal curve: chance of finishing by day 38 {curve.cdf(38):.1%}, P80 {curve.inv_cdf(0.8):.1f} days")
print()
for label, draw in (("Triangular", lambda r, o, m, p: r.triangular(o, p, m)), ("PERT (beta)", pert_draw)):
rng = random.Random(SEED)
ends, critical = [], dict.fromkeys(FEST, 0)
for _ in range(RUNS):
dur = {a: draw(rng, FEST[a][1], FEST[a][2], FEST[a][3]) for a in FEST}
end, crit = finish_and_critical(dur)
ends.append(end)
for a in crit:
critical[a] += 1
on_time = sum(1 for e in ends if e <= 38) / RUNS
p50, p80 = percentile(ends, 0.5), percentile(ends, 0.8)
print(f"Monte Carlo, {label}, {RUNS} runs, seed {SEED}:")
print(f" P50 {p50:.1f} days, P80 {p80:.1f} days, finished by day 38 in {on_time:.1%} of runs")
print(f" contingency after the plan's day {end_m:.0f}: {math.ceil(p50) - end_m:.0f} days for a 50 % date "
f"(day {math.ceil(p50)}), {math.ceil(p80) - end_m:.0f} days for an 80 % date (day {math.ceil(p80)})")
top = sorted(critical.items(), key=lambda kv: -kv[1])
print(" on the critical path in this share of runs: " + ", ".join(f"{a} {n / RUNS:.0%}" for a, n in top if n)) Output
With the most likely durations: finish after 38 days, critical path A C D F L M N Id O M P PERT mean PERT sd Triangular mean A 2 3 5 3.17 0.50 3.33 B 1 2 4 2.17 0.50 2.33 C 4 5 8 5.33 0.67 5.67 D 8 10 16 10.67 1.33 11.33 E 6 8 12 8.33 1.00 8.67 F 10 12 18 12.67 1.33 13.33 G 4 6 9 6.17 0.83 6.33 H 3 4 6 4.17 0.50 4.33 I 9 10 13 10.33 0.67 10.67 J 2 3 5 3.17 0.50 3.33 K 4 5 7 5.17 0.50 5.33 L 2 2 4 2.33 0.33 2.67 M 2 2 2 2.00 0.00 2.00 N 3 4 7 4.33 0.67 4.67 Single-path PERT on ACDFLMN: mean 40.5 days, sd 2.2 normal curve: chance of finishing by day 38 12.8%, P80 42.4 days Monte Carlo, Triangular, 10000 runs, seed 2026: P50 42.9 days, P80 45.4 days, finished by day 38 in 2.7% of runs contingency after the plan's day 38: 5 days for a 50 % date (day 43), 8 days for an 80 % date (day 46) on the critical path in this share of runs: A 100%, C 100%, D 100%, F 100%, L 100%, M 100%, N 100% Monte Carlo, PERT (beta), 10000 runs, seed 2026: P50 40.4 days, P80 42.5 days, finished by day 38 in 14.7% of runs contingency after the plan's day 38: 3 days for a 50 % date (day 41), 5 days for an 80 % date (day 43) on the critical path in this share of runs: A 100%, C 100%, D 100%, F 100%, L 100%, M 100%, N 100%
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 risk.py
Choose the weighting from how the estimates were made. If M is the value people are confident about and the extremes are rare, PERT fits; if the extremes are as real as M, or the estimators doubt their M, the triangular reading is the more cautious choice. Either way, say which one the analysis used.
Adding the path gives a first answer
The quickest analysis adds up the critical path. Expected times add, and so do variances, the squares of σ, when the activities are independent. For A, C, D, F, L, M and N that gives a mean of 40.5 days and a standard deviation of 2.2, so a normal curve can say how likely day 38 is. Drag the limits or the mean to see how the chance moves:
For a normal distribution with mean 40.5 and standard deviation 2.2, the probability of a value between 30 and 38 is 0.1279 (12.8 %), from z = −4.77 to z = −1.14.
The numbers as a table
| Range | Probability |
|---|---|
| μ ± 1σ | 0.6827 |
| μ ± 2σ | 0.9545 |
| μ ± 3σ | 0.9973 |
The shaded area from day 30 to day 38 is about 0.128: a 12.8 % chance, the same as the program’s single-path line. The sum ignores every other path, though. It assumes that the critical path stays critical, and that nothing at a merge point can delay the finish. Both assumptions are tested by simulating the whole network; in the course plan of NPTEL’s Project Management for Managers from IIT Roorkee, week 9 is given to probability models in networks.
Adding standard deviations instead of variances
Wrong: 0.50 + 0.67 + 1.33 + 1.33 + 0.33 + 0 + 0.67 = 4.83 days. Right: add the variances, (P − O)² ÷ 36 for each activity, (9 + 16 + 64 + 64 + 4 + 0 + 16) ÷ 36 = 4.81, and take the square root, 2.19, about 2.2. Uncertainties partly cancel when independent activities are added.
Checkpoint
Check: Three-point estimates and weighting · The single-path PERT sum
A Monte Carlo run recalculates the whole network thousands of times
A Monte Carlo simulation: A method that draws every uncertain input from its range many times, recalculates the model each time and reports the spread of the results. draws a duration for every activity from its range, recalculates the network with its links, records the finish, and repeats. GAO’s eighth best practice is this kind of schedule risk analysis: An analysis that simulates a schedule's uncertain durations and risks to give the chance of meeting each key date and the activities that most often drive the finish., and its checklist asks for the confidence level of the key dates and for the activities that most often ended up on the critical path. The program ran 10,000 times for each weighting, with seed 2026, so every run of the page’s program prints the same numbers.
With PERT weighting, half the runs were done by day 40.4, the The duration that half of the simulated runs met or beat; P80 is likewise the duration that 80 % of the runs finished within., and 80 % by day 42.5, the P80; 14.7 % finished by day 38. With triangular weighting the same ranges gave a P50 of 42.9, a P80 of 45.4 and only 2.7 % by day 38. The critical path stayed A, C, D, F, L, M and N in every run: the events chain’s 11 days of float were never used up, which is why the PERT run agrees so closely with the single-path sum.
| Method | P50 | P80 | Finished by day 38 |
|---|---|---|---|
| Single-path PERT, normal curve | 40.5 | 42.4 | 12.8 % |
| Monte Carlo, PERT weighting | 40.4 | 42.5 | 14.7 % |
| Monte Carlo, triangular weighting | 42.9 | 45.4 | 2.7 % |
The single-path P80 is the normal curve’s 80th percentile, 42.4, which the program prints with its chance of day 38. The spread between the rows is the honest answer to “when will the fest be over”: a range, read at the confidence the sponsor chooses.
Merge bias: parallel paths make on time less likely
Where paths join, the event waits for the latest of them. GAO’s guide calls the added risk at those points merge bias: The extra risk where parallel paths join: the joined event waits for the latest path, so it is later than any one path suggests., and gives an example: two paths that are each 60 % likely to be on time are both on time only 36 % of the time. The program below runs paths of 8 to 12 days, each on time by day 10 half the time, into one milestone:
"""Merge bias: parallel paths that each have an even chance of finishing on time rarely all do.
Each path takes 8 to 12 days, most likely 10, drawn from a triangular distribution, so each finishes within 10 days
half the time. A milestone that waits for all of them is on time only when every path is. The runs use a fixed seed,
so every run of this program prints the same numbers.
"""
import random
RUNS, SEED, TARGET = 10_000, 7, 10
rng = random.Random(SEED)
print(f"Each path: 8 to 12 days, most likely 10; the milestone is due on day {TARGET}")
print(f"{'Paths':>5}{'Each path on time':>19}{'Milestone on time':>19}{'Exact':>8}{'Mean milestone day':>20}")
for paths in (1, 2, 3, 4):
each = milestone = 0
total = 0.0
for _ in range(RUNS):
days = [rng.triangular(8, 12, 10) for _ in range(paths)]
each += sum(d <= TARGET for d in days)
milestone += max(days) <= TARGET
total += max(days)
print(f"{paths:>5}{each / (RUNS * paths):>19.1%}{milestone / RUNS:>19.1%}{0.5 ** paths:>8.2%}{total / RUNS:>20.2f}") Output
Each path: 8 to 12 days, most likely 10; the milestone is due on day 10
Paths Each path on time Milestone on time Exact Mean milestone day
1 50.3% 50.3% 50.00% 10.00
2 50.1% 24.8% 25.00% 10.46
3 49.8% 12.2% 12.50% 10.70
4 50.1% 6.4% 6.25% 10.85
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 merge.py
Each path alone was on time in about 50 % of runs; two together in 24.8 %, three in 12.2 % and four in 6.4 %, close to the exact 25, 12.5 and 6.25 %. The milestone’s mean day also slid from 10.00 to 10.85, although no single path got any longer. A single-path calculation cannot see this, and a plan with many parallel chains that all finish just in time is much later than its critical path suggests. In the fest, the chains that meet at M have a lot of float, which is why merge bias barely showed there; in a plan whose parallel chains are close in length, it dominates.
Key idea
The finish of a network is the latest of many paths, so its range is wider and later than any one path’s: simulate the whole network, and read the result as a confidence, not a single date.
Checkpoint
Check: Monte Carlo schedule risk analysis · Merge bias
Contingency comes from the analysis, not from padding
schedule contingency: Time held before a committed finish, or before key milestones, for known risks and uncertainty, sized from a schedule risk analysis and released through change control. is time held before the committed finish for known risks and uncertainty. GAO’s guide says to size it from the risk analysis: the gap between the date at the chosen confidence, often 80 %, and the plan’s own finish. For the fest under PERT weighting that is day 43, the first whole day after the P80 of 42.5, minus day 38: 5 days. The program prints the margin for both weightings and both confidences:
| Committed at | PERT weighting | Triangular weighting |
|---|---|---|
| P50 | day 41, 3 days of contingency | day 43, 5 days |
| P80 | day 43, 5 days | day 46, 8 days |
The committed date carries that margin as one visible block before the finish, held by the project manager and released through change control, rather than as a day or two hidden in every estimate.
Communicate the result as a range with a confidence: “the fest is 80 % likely to be over by working day 43”, not “day 38” and not “day 45 to be safe”. Then re-run the analysis as actual durations arrive. The ranges of finished activities shrink to their actual values, so the range of the finish narrows, and the contingency still needed can be compared with what is left.
Ranges, seeds and the tools
Record the seed and the number of runs with every result, as the program does, so anyone can reproduce it. The PERT calculator works out three-point estimates for a list of tasks, and the probability calculator reads a normal curve for a single-path answer.
Checkpoint
Check: Schedule contingency
Practise: ranges, runs and dates
The questions use the fest’s ranges and runs, a plan due on working day 50 and a data migration that drives the finish in a third of the runs. Work each number out before you answer:
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what do P50 and P80 mean in a schedule risk analysis?
They are percentiles of the finish dates a Monte Carlo simulation produces. P50 is the date by which half of the simulated runs had finished, so the project is as likely to be later as earlier; P80 is the date by which 80 % had finished, a safer date to commit to. In the tech fest’s analysis with PERT weighting they were working days 40.4 and 42.5, against a deterministic plan of 38. The gap between the P80 and the plan’s finish is the contingency an 80 % commitment needs, here 5 days.
Two harder questions follow, about a sponsor who wants one date and about a risk analysis built on suspiciously narrow ranges. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A schedule risk analysis of a product launch gives P50 at working day 118 and P80 at day 131, while the plan's critical path says day 110. The sponsor wants one date for the press. What do you recommend, and how do you explain it?
-
Senior to staff
A contractor's schedule risk analysis claims a 92 % chance of meeting your deadline. You see that every pessimistic estimate is the most likely value plus 10 %, and that no two activities are correlated. How do you judge the claim?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What three numbers make a three-point estimate, and why skew to the right?
Optimistic (O), most likely (M) and pessimistic (P), gathered from the people who know the work (anonymously where that helps). Most schedule ranges skew right because things can go much more wrong than right.
-
What does PERT compute for an activity?
Expected time t_e = (O + 4M + P) / 6, standard deviation σ = (P − O) / 6. Along an independent critical path, means add and variances add (not standard deviations), so σ for the path is the square root of the summed variances.
-
What do P50 and P80 mean, and how is contingency sized?
P50 and P80 are the percentile dates of the Monte Carlo finish distribution: the dates by which 50 % and 80 % of simulated runs had finished. Schedule contingency is the gap between the chosen percentile (often P80) and the plan's finish, held as one visible block rather than hidden in each estimate.
-
What is merge bias in a schedule?
Where several paths meet, the event waits for the latest of them, so the probability of being on time multiplies. Two paths each 60 % likely to be on time are both on time only 36 % of the time. Parallel chains of similar length make a milestone later than any one of them.
-
How should a schedule risk analysis be communicated?
As a range with a confidence ("80 % likely to be over by working day 43"), not a single date. Record the seed and number of runs so it can be reproduced, and re-run as actual durations arrive so the finish range narrows.
Key takeaways
- A three-point estimate records optimistic, most likely and pessimistic durations; PERT weights them (O + 4M + P) ÷ 6 and a triangular reading weights them equally, which gives later dates.
- Adding PERT means and variances along the critical path gives a quick chance of meeting a date, but it ignores every other path.
- A seeded Monte Carlo simulation recalculates the whole network thousands of times and reports finish dates at a confidence, such as P50 and P80.
- Merge bias makes a milestone fed by several paths later than any one of them, so parallel chains of similar length lower the chance of finishing on time.
- Contingency is the gap between the P80, or another chosen confidence, and the plan’s finish, held as one visible block rather than hidden in every estimate.
Terms in this lesson
- merge bias
- The extra risk where parallel paths join: the joined event waits for the latest path, so it is later than any one path suggests.
- Monte Carlo simulation
- A method that draws every uncertain input from its range many times, recalculates the model each time and reports the spread of the results.
- schedule contingency
- Time held before a committed finish, or before key milestones, for known risks and uncertainty, sized from a schedule risk analysis and released through change control.
- schedule risk analysis
- An analysis that simulates a schedule's uncertain durations and risks to give the chance of meeting each key date and the activities that most often drive the finish.
- three-point estimate
- An estimate given as three durations, optimistic, most likely and pessimistic, so that its range and its shape can be used in a risk analysis.
References
- Application of a Technique for Research and Development Program Evaluation (Operations Research 7(5), 1959) (INFORMS)
- Schedule Assessment Guide: Best Practices for Project Schedules (GAO-16-89G) (U.S. Government Accountability Office)
- NPTEL: Project Management for Managers (IIT Roorkee), course plan (NPTEL (IITs and IISc, Ministry of Education))
- random: generate pseudo-random numbers (Python 3.14 standard library) (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress