ЁЯПл The SchoolтА║ЁЯПЕ Engineering LeadershipтА║ЁЯО▓ рдзрдбрд╛ 05 тАФ рдирд┐рдпреЛрдЬрди рдЖрдгрд┐ рдЕрдВрджрд╛рдЬ: Monte Carlo forecast рд╡рд┐рд░реБрджреНрдз рдПрдХрдЪ рддрд╛рд░реАрдЦ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯО▓ рдзрдбрд╛ 05 тАФ рдирд┐рдпреЛрдЬрди рдЖрдгрд┐ рдЕрдВрджрд╛рдЬ: Monte Carlo forecast рд╡рд┐рд░реБрджреНрдз рдПрдХрдЪ рддрд╛рд░реАрдЦ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-hiring ┬╖ рдкреБрдвреЗ: lesson-06-prioritisation


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбрд╛ 04, рдЖрдгрд┐ "рд╣реЗ рдХрдзреА рдкреВрд░реНрдг рд╣реЛрдИрд▓?" рдпрд╛рдЪреЗ рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдЙрддреНрддрд░. рд╕рд░рд╛рд╕рд░реАрд╡рд░реВрди рдХрд╛рдврд▓реЗрд▓реА рдПрдХрдЪ рддрд╛рд░реАрдЦ рдмрд╣реБрддреЗрдХ рд╡реЗрд│рд╛ рдирд╛рдгреЗрдлреЗрдХ рдХрд╛ рдЕрд╕рддреЗ, рдЖрдгрд┐ рдЯреАрдордЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рдорд╛рдЧреАрд▓ throughput рд╡рд░реВрди рдмрдирд╡рд▓реЗрд▓рд╛ Monte Carlo forecast рд╕рдВрднрд╛рд╡реНрдпрддреЗрд╕рд╣ range рдХрд╢реА рджреЗрддреЛ: 50% рддрд╛рд░реАрдЦ, 85% рддрд╛рд░реАрдЦ рдЖрдгрд┐ 95% рддрд╛рд░реАрдЦ. lead/demo.py рдордзрд▓реЗ forecast(); lead/models.py рдордзрд▓реЗ monte_carlo() рдЖрдгрд┐ pct().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдореБрдЦреНрдпрд╛рдзреНрдпрд╛рдкрд┐рдХрд╛ рдХрддрд░рд┐рдирд╛рд▓рд╛ рд╡рд┐рдЪрд╛рд░рддрд╛рдд: "рд╡рд┐рднрд╛рдЧ рд╕рдЧрд│реНрдпрд╛ 40 рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛ рддрдкрд╛рд╕реВрди рдХрдзреА рдкреВрд░реНрдг рдХрд░реЗрд▓?" ЁЯУЪ

рдХрддрд░рд┐рдирд╛ рдорд╛рдЧрдЪреЗ рджрд╣рд╛ рдЖрдард╡рдбреЗ рдкрд╛рд╣рддреЗ. рдХрд╛рд╣реА рдЖрдард╡рдбреНрдпрд╛рдВрдд рд╡рд┐рднрд╛рдЧрд╛рдиреЗ 6 рдХрд┐рдВрд╡рд╛ 7 рддрдкрд╛рд╕рдгреАрдЪреА рдХрд╛рдореЗ рдкреВрд░реНрдг рдХреЗрд▓реА. рдПрдХрд╛ рдЖрдард╡рдбреНрдпрд╛рдд тАФ рдХреНрд░реАрдбрд╛ рд╕рдкреНрддрд╛рд╣рд╛рдд тАФ рдПрдХрд╣реА рдирд╛рд╣реА. рд╕рд░рд╛рд╕рд░реА, рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рдЬрд╡рд│рдкрд╛рд╕ 3.7.

рд╕реЛрдкреЗ рдЙрддреНрддрд░: "40 ├╖ 3.7 тЙИ 11 рдЖрдард╡рдбреЗ." рдкрдг рдпрд╛рдд рдЕрд╕реЗ рдЧреГрд╣реАрдд рдзрд░рд▓реЗ рдЖрд╣реЗ рдХреА рдкреНрд░рддреНрдпреЗрдХ рдЖрдард╡рдбрд╛ рд╕рд░рд╛рд╕рд░реА рдЖрдард╡рдбрд╛ рдЕрд╕реЗрд▓. рдХрд╛рд╣реА рдЖрдард╡рдбреЗ рд╕рдВрде рдЕрд╕рддреАрд▓. ЁЯРв

рдореНрд╣рдгреВрди рдХрддрд░рд┐рдирд╛ рдПрдХ рдЦреЗрд│ рдЦреЗрд│рддреЗ. ЁЯО▓ рддреА рдорд╛рдЧрдЪреЗ рджрд╣рд╛ рдЖрдард╡рдбреЗ рджрд╣рд╛ рдкрддреНрддреНрдпрд╛рдВрд╡рд░ рд▓рд┐рд╣рд┐рддреЗ. рддреА рдпрд╛рджреГрдЪреНрдЫрд┐рдХрдкрдгреЗ рдПрдХ рдкрддреНрддрд╛ рдХрд╛рдврддреЗ тАФ рддреЛ "рдкреБрдврдЪрд╛ рдЖрдард╡рдбрд╛". рддреА рддрдкрд╛рд╕рдгреАрдЪреА рдмреЗрд░реАрдЬ рдХрд░рддреЗ, рдкреБрдиреНрд╣рд╛ рдкрддреНрддрд╛ рдХрд╛рдврддреЗ, рдкреБрдиреНрд╣рд╛, 40 рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛ рдкреВрд░реНрдг рд╣реЛрдИрдкрд░реНрдпрдВрдд. рддреА рдЖрдард╡рдбреЗ рдореЛрдЬрддреЗ. рдордЧ рддреА рд╕рдВрдкреВрд░реНрдг рдЦреЗрд│ 10,000 рд╡реЗрд│рд╛ рдЦреЗрд│рддреЗ (computer рдорджрдд рдХрд░рддреЛ!).

рдЕрд░реНрдзреЗ рдЦреЗрд│ 11 рдЖрдард╡рдбреНрдпрд╛рдВрдд рдХрд┐рдВрд╡рд╛ рдХрдореА рд╡реЗрд│рд╛рдд рд╕рдВрдкрддрд╛рдд. рдкрдг 100 рдкреИрдХреА 85 рдЦреЗрд│ 13 рдЖрдард╡рдбреНрдпрд╛рдВрдд рдХрд┐рдВрд╡рд╛ рдХрдореА рд╡реЗрд│рд╛рдд рд╕рдВрдкрддрд╛рдд. рдореНрд╣рдгреВрди рддреА рдореБрдЦреНрдпрд╛рдзреНрдпрд╛рдкрд┐рдХрд╛рдВрдирд╛ рд╕рд╛рдВрдЧрддреЗ: "11 рдЖрдард╡рдбреЗ рдореНрд╣рдгрдЬреЗ рдирд╛рдгреЗрдлреЗрдХ. рддреБрдореНрд╣рд╛рд▓рд╛ рдмрд▒реНрдпрд╛рдкреИрдХреА рдЦрд╛рддреНрд░реА рд╣рд╡реА рдЕрд╕реЗрд▓ рддрд░ 13 рдЪреЗ рдирд┐рдпреЛрдЬрди рдХрд░рд╛."

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    h["ЁЯУТ last 10 weeks<br/>2 6 1 4 7 0 5 3 6 3<br/>average 3.7"] --> g["ЁЯзо single guess<br/>40 ├╖ 3.7 тЙИ 11 weeks"]
    g --> c["58% chance<br/>of finishing by then"]
    h --> mc["ЁЯО▓ 10,000 seeded trials<br/>replay random past weeks"]
    mc --> r["50%: 11 wk ┬╖ 85%: 13 wk ┬╖ 95%: 15 wk"]
    r --> s["ЁЯУИ backlog +20% тЖТ 48 items<br/>50%: 13 wk ┬╖ 85%: 16 wk"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/engineering-leadership/lesson-diagrams.html#l05

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдПрдХрдЪ рддрд╛рд░реАрдЦ рд╡рдЪрди рдореНрд╣рдгреВрди рдРрдХрд▓реА рдЬрд╛рддреЗ, рдЖрдгрд┐ рд╕рд░рд╛рд╕рд░реАрд╡рд░ рдЖрдзрд╛рд░рд▓реЗрд▓реЗ рд╡рдЪрди рдЬрд╡рд│рдкрд╛рд╕ рдЕрд░реНрдзреНрдпрд╛ рд╡реЗрд│рд╛ рдЪреБрдХрддреЗ тАФ рдЬреНрдпрд╛рдореБрд│реЗ рд╕рдЧрд│реЗ рд╢рд┐рдХрддрд╛рдд рдХреА рдЕрдВрджрд╛рдЬ рдирд┐рд░реБрдкрдпреЛрдЧреА рдЕрд╕рддрд╛рдд. рд╕рдВрднрд╛рд╡реНрдпрддреЗрд╕рд╣ range рддреБрдордЪреНрдпрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕рд▓реЗрд▓реНрдпрд╛рдВрдирд╛ рдирд┐рдпреЛрдЬрди рдХрд░реВ рджреЗрддреЗ: "рдЖрдард╡рдбрд╛ 13 рдкрд░реНрдпрдВрдд 85%" рд╣реЗ рдореБрдЦреНрдпрд╛рдзреНрдпрд╛рдкрд┐рдХрд╛рдВрдирд╛ рддреНрдпрд╛ рдХрд┐рддреА рдзреЛрдХрд╛ рдмрд╛рд│рдЧрдд рдЖрд╣реЗрдд рддреЗ рд╕рд╛рдВрдЧрддреЗ. рдЖрдгрд┐ forecast рдореЛрдЬрд▓реЗрд▓реНрдпрд╛ throughput рд╡рд░реВрди рдмрдирдд рдЕрд╕рд▓реНрдпрд╛рдиреЗ, рддреНрдпрд╛рд▓рд╛ рд▓рд╛рдВрдмрд▓рдЪрдХ estimation meetings рд▓рд╛рдЧрдд рдирд╛рд╣реАрдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

lead/models.py рдордзрд▓реЗ monte_carlo(history, backlog, trials=10000, seed=42) рдЫреЛрдЯрд╛ seeded random generator Rng рд╡рд╛рдкрд░рддреЗ, рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ run рддреЗрдЪ рдЖрдХрдбреЗ print рдХрд░рддреЛ. рдкреНрд░рддреНрдпреЗрдХ trial рд╕рд╛рдареА рддреЗ рдкреВрд░реНрдг рдЭрд╛рд▓реЗрд▓реА рдХрд╛рдореЗ backlog рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪреЗрдкрд░реНрдпрдВрдд history рдордзреВрди рдпрд╛рджреГрдЪреНрдЫрд┐рдХ рдЖрдард╡рдбреЗ рдХрд╛рдврддреЗ, рдЖрдгрд┐ рдЖрдард╡рдбреНрдпрд╛рдВрдЪреА рд╕рдВрдЦреНрдпрд╛ рдиреЛрдВрджрд╡рддреЗ. рддреЗ sorted list рдкрд░рдд рджреЗрддреЗ; pct(runs, p) рддреНрдпрд╛рддреВрди percentile рд╡рд╛рдЪрддреЗ. lead/demo.py рдордзрд▓реЗ forecast() рдПрдХрд╛рдЪ рдЕрдВрджрд╛рдЬрд╛рдЪреА 50/85/95% рддрд╛рд░рдЦрд╛рдВрд╢реА рдЖрдгрд┐ 20% рдиреЗ рд╡рд╛рдврдгрд╛рд▒реНрдпрд╛ backlog рд╢реА рддреБрд▓рдирд╛ рдХрд░рддреЗ. рд╡рд┐рдЪрд╛рд░ рдХрд░рдгреНрдпрд╛рдЪреЗ рд╕рд╛рдзрди, рдорд╛рдгрд╕рд╛рдВрд╕рд╛рдареАрдЪреЗ рд╕реВрддреНрд░ рдирд╛рд╣реА: рддреЗ рдХрд╛рдорд╛рдЪреНрдпрд╛ system рдЪрд╛ forecast рдХрд░рддреЗ, рдЖрдгрд┐ рддреНрдпрд╛рдЪреЗ рдХрдзреАрд╣реА рд╡реНрдпрдХреНрддреАрдВрд╕рд╛рдареАрдЪреНрдпрд╛ target рдордзреНрдпреЗ рд░реВрдкрд╛рдВрддрд░ рд╣реЛрддрд╛ рдХрд╛рдорд╛ рдирдпреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 lead/demo.py forecast
python3 - <<'EOF'
import sys; sys.path.insert(0, "lead"); from models import monte_carlo, pct
hist = [2, 6, 1, 4, 7, 0, 5, 3, 6, 3]
for backlog in (20, 40, 60):
    r = monte_carlo(hist, backlog)
    print(f"backlog {backlog:>2} тЖТ 50%: {pct(r, .5):>2} wk ┬╖ 85%: {pct(r, .85):>2} wk ┬╖ 95%: {pct(r, .95):>2} wk ┬╖ single guess {round(backlog / 3.7):>2} wk")
for seed in (1, 2, 3):
    r = monte_carlo(hist, 40, seed=seed)
    print(f"seed {seed} тЖТ 50%: {pct(r, .5)} ┬╖ 85%: {pct(r, .85)}")
steady = monte_carlo([4, 4, 3, 4, 4, 3, 4, 4, 3, 4], 40)
print(f"a steadier team, same 3.7 average тЖТ 50%: {pct(steady, .5)} ┬╖ 85%: {pct(steady, .85)} ┬╖ 95%: {pct(steady, .95)}")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

forecast рдЕрд╕реЗ print рдХрд░рддреЗ:

тФАтФА the last 10 weeks' throughput (items finished): [2, 6, 1, 4, 7, 0, 5, 3, 6, 3] ┬╖ average 3.7/week ┬╖ backlog 40 items
   the single-date guess: 40 ├╖ 3.7 тЙИ 11 weeks
   Monte Carlo, 10,000 seeded trials replaying random past weeks тЖТ 50%: 11 weeks ┬╖ 85%: 13 weeks ┬╖ 95%: 15 weeks
   chance of finishing within the guess (11 weeks): 58%
тФАтФА the backlog grows 20% (40 тЖТ 48 items) тЖТ 50%: 13 weeks ┬╖ 85%: 16 weeks

рддреБрдордЪрд╛ snippet рдЕрд╕реЗ print рдХрд░рддреЛ:

backlog 20 тЖТ 50%:  6 wk ┬╖ 85%:  7 wk ┬╖ 95%:  9 wk ┬╖ single guess  5 wk
backlog 40 тЖТ 50%: 11 wk ┬╖ 85%: 13 wk ┬╖ 95%: 15 wk ┬╖ single guess 11 wk
backlog 60 тЖТ 50%: 17 wk ┬╖ 85%: 19 wk ┬╖ 95%: 21 wk ┬╖ single guess 16 wk
seed 1 тЖТ 50%: 11 ┬╖ 85%: 13
seed 2 тЖТ 50%: 11 ┬╖ 85%: 13
seed 3 тЖТ 50%: 11 ┬╖ 85%: 13
a steadier team, same 3.7 average тЖТ 50%: 11 ┬╖ 85%: 12 ┬╖ 95%: 12

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдПрдХрдЪ рдЕрдВрджрд╛рдЬ (11 рдЖрдард╡рдбреЗ) median рд╡рд░ рдмрд╕рддреЛ: рддреЛ рдкрд╛рд│рд▓рд╛ рдЬрд╛рдгреНрдпрд╛рдЪреА рдлрдХреНрдд 58% рд╢рдХреНрдпрддрд╛. 85% рддрд╛рд░реАрдЦ 13 рдЖрдард╡рдбреЗ рдЖрдгрд┐ 95% рддрд╛рд░реАрдЦ 15 рдЖрд╣реЗ. рд╡реЗрдЧрд╡реЗрдЧрд│реЗ seeds рддреЗрдЪ рдЙрддреНрддрд░ рджреЗрддрд╛рдд, рдореНрд╣рдгреВрди рдпрд╛ history рд╕рд╛рдареА 10,000 trials рдкреБрд░реЗрд╕реЗ рдЖрд╣реЗрдд. рддреНрдпрд╛рдЪ рд╕рд░рд╛рд╕рд░реАрдЪреА рдЕрдзрд┐рдХ рд╕реНрдерд┐рд░ рдЯреАрдо рдЦреВрдк рдЕрд░реБрдВрдж range рджреЗрддреЗ (рдЖрдард╡рдбрд╛ 12 рдкрд░реНрдпрдВрдд 85%) тАФ рдЕрдВрджрд╛рдЬ рд╡рд░реНрддрд╡рддрд╛ рдпреЗрдгреЗ рдХрдореА рдЪрдврдЙрддрд╛рд░рд╛рддреВрди рдпреЗрддреЗ, рдЬрд╛рд╕реНрдд рд╕рд░рд╛рд╕рд░реАрддреВрди рдирд╛рд╣реА. рдЖрдгрд┐ 20% рдореЛрдареНрдпрд╛ backlog рдиреЗ 85% рддрд╛рд░реАрдЦ 13 рд╡рд░реВрди 16 рдЖрдард╡рдбреНрдпрд╛рдВрд╡рд░ рдиреЗрд▓реА.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

Throughput рдореНрд╣рдгрдЬреЗ рджрд░ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рдкреВрд░реНрдг рдЭрд╛рд▓реЗрд▓реНрдпрд╛ рдХрд╛рдорд╛рдВрдЪреА рд╕рдВрдЦреНрдпрд╛, рдЬреА рдмрд╣реБрддреЗрдХ trackers export рдХрд░реВ рд╢рдХрддрд╛рдд. рдЦрд▒реНрдпрд╛ repository рд╡рд░, рджрд░ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ merge рдЭрд╛рд▓реЗрд▓реЗ pull requests рд╣рд╛ рдвреЛрдмрд│ рдкрд░реНрдпрд╛рдп рдЖрд╣реЗ (рдПрдХ рдХрд╛рдо рдореНрд╣рдгрдЬреЗ рдЕрдиреЗрдХ PRs рдЕрд╕реВ рд╢рдХрддрд╛рдд тАФ рд╣реЗ рдЧреГрд╣реАрддрдХ рдиреЛрдВрджрд╡рд╛):

gh pr list --state merged --limit 200 --json mergedAt \
  --jq '.[].mergedAt[0:10]' | sort | uniq -c | tail -20

рд╕рдВрдкреВрд░реНрдг forecast рддреБрдордЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ script рдЪреНрдпрд╛ рдХрд╛рд╣реА рдУрд│реАрдВрдд, рдХрд┐рдВрд╡рд╛ spreadsheet рдордзреНрдпреЗ рдмрд╕рддреЛ:

import random
def forecast(history, backlog, trials=10_000):
    runs = []
    for _ in range(trials):
        done = weeks = 0
        while done < backlog:
            done += random.choice(history); weeks += 1
        runs.append(weeks)
    runs.sort()
    return {p: runs[int(p / 100 * trials) - 1] for p in (50, 85, 95)}

рд▓реЛрдХрд╛рдВрдирд╛ рдЕрд╕реЗ рд╕рд╛рдВрдЧрд╛:

Gradebook migration тАФ forecast, week 14
40 items left ┬╖ based on the last 10 weeks' throughput
50%: week 25 ┬╖ 85%: week 27 ┬╖ 95%: week 29
Assumptions: similar item mix; no new scope; Leela joins on-call in week 18.
Next re-forecast: Monday.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: "рд╣реЗ 14 рддрд╛рд░рдЦреЗрд▓рд╛ рдкреВрд░реНрдг рд╣реЛрдИрд▓" рдРрд╡рдЬреА "21 рддрд╛рд░рдЦреЗрдкрд░реНрдпрдВрдд 85%, рджрд░ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рдкреБрдиреНрд╣рд╛ forecast" рдЕрд╕реЗ рдореНрд╣рдгрд╛. рд▓реЛрдХ range рднреЛрд╡рддреА рдирд┐рдпреЛрдЬрди рдХрд░реВ рд╢рдХрддрд╛рдд; рдЕрд░реНрдзреНрдпрд╛ рд╡реЗрд│рд╛ рдмрд░реЛрдмрд░ рдард░рдгрд╛рд▒реНрдпрд╛ рддрд╛рд░рдЦреЗрднреЛрд╡рддреА рддреЗ рдирд┐рдпреЛрдЬрди рдХрд░реВ рд╢рдХрдд рдирд╛рд╣реАрдд.

тПня╕П рдкреБрдвреЗ

Forecast рдиреЗ рдЕрд╕реЗ рдЧреГрд╣реАрдд рдзрд░рд▓реЗ рдХреА рдЖрдзреА рдХрд╛рдп рдХрд░рд╛рдпрдЪреЗ рд╣реЗ рддреБрдореНрд╣рд╛рд▓рд╛ рдЖрдзреАрдЪ рдорд╛рд╣реАрдд рдЖрд╣реЗ. рдкреБрдвреЗ: рдХреНрд░рдо рдард░рд╡рдгреЗ тАФ prioritisation, рдЧреГрд╣реАрддрдХреЗ рджреГрд╢реНрдп рдХрд░реВрди.

git checkout lesson-06-prioritisation

ЁЯО▓ Lesson 05 тАФ Planning and estimation: a Monte Carlo forecast vs a single date

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-hiring ┬╖ Next: lesson-06-prioritisation


ЁЯУж What's in this branch

Lesson 04, plus answering "when will it be done?" honestly. Why a single date made from an average is usually a coin flip, and how a Monte Carlo forecast built from the team's own past throughput gives a range with a probability: a 50% date, an 85% date and a 95% date. forecast() in lead/demo.py; monte_carlo() and pct() in lead/models.py.

ЁЯзТ Explain like I'm 5

The head teacher asks Katrina: "When will the department finish marking all 40 exam papers?" ЁЯУЪ

Katrina looks at the last ten weeks. Some weeks the department finished 6 or 7 pieces of marking. One week тАФ sports week тАФ it finished none. On average, about 3.7 a week.

The easy answer: "40 ├╖ 3.7 тЙИ 11 weeks." But that assumes every week will be an average week. Some weeks will be slow. ЁЯРв

So Katrina plays a game. ЁЯО▓ She writes the ten past weeks on ten cards. She draws a card at random тАФ that is "next week". She adds up the marking, draws again, and again, until 40 papers are done. She counts the weeks. Then she plays the whole game 10,000 times (a computer helps!).

Half the games finish in 11 weeks or less. But 85 games out of 100 finish in 13 weeks or less. So she tells the head teacher: "11 weeks is a coin flip. If you need to be fairly sure, plan for 13."

ЁЯЧ║я╕П Diagram

flowchart LR
    h["ЁЯУТ last 10 weeks<br/>2 6 1 4 7 0 5 3 6 3<br/>average 3.7"] --> g["ЁЯзо single guess<br/>40 ├╖ 3.7 тЙИ 11 weeks"]
    g --> c["58% chance<br/>of finishing by then"]
    h --> mc["ЁЯО▓ 10,000 seeded trials<br/>replay random past weeks"]
    mc --> r["50%: 11 wk ┬╖ 85%: 13 wk ┬╖ 95%: 15 wk"]
    r --> s["ЁЯУИ backlog +20% тЖТ 48 items<br/>50%: 13 wk ┬╖ 85%: 16 wk"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/engineering-leadership/lesson-diagrams.html#l05

тЭУ What

ЁЯдФ Why

Because a single date is heard as a promise, and a promise built on an average is missed about half the time тАФ which teaches everyone that estimates are worthless. A range with a probability lets the people who depend on you plan: "85% by week 13" tells the head teacher how much risk she is holding. And because the forecast is built from counted throughput, it needs no long estimation meetings.

ЁЯФз How (in this repo)

monte_carlo(history, backlog, trials=10000, seed=42) in lead/models.py uses the tiny seeded random generator Rng, so every run prints the same numbers. For each trial it draws random weeks from history until the finished items reach backlog, and records the number of weeks. It returns the sorted list; pct(runs, p) reads a percentile from it. forecast() in lead/demo.py compares the single guess with the 50/85/95% dates and a backlog that grows by 20%. A thinking tool, not a formula for people: it forecasts the work system, and must never be turned into a target for individuals.

ЁЯзк Try it

python3 lead/demo.py forecast
python3 - <<'EOF'
import sys; sys.path.insert(0, "lead"); from models import monte_carlo, pct
hist = [2, 6, 1, 4, 7, 0, 5, 3, 6, 3]
for backlog in (20, 40, 60):
    r = monte_carlo(hist, backlog)
    print(f"backlog {backlog:>2} тЖТ 50%: {pct(r, .5):>2} wk ┬╖ 85%: {pct(r, .85):>2} wk ┬╖ 95%: {pct(r, .95):>2} wk ┬╖ single guess {round(backlog / 3.7):>2} wk")
for seed in (1, 2, 3):
    r = monte_carlo(hist, 40, seed=seed)
    print(f"seed {seed} тЖТ 50%: {pct(r, .5)} ┬╖ 85%: {pct(r, .85)}")
steady = monte_carlo([4, 4, 3, 4, 4, 3, 4, 4, 3, 4], 40)
print(f"a steadier team, same 3.7 average тЖТ 50%: {pct(steady, .5)} ┬╖ 85%: {pct(steady, .85)} ┬╖ 95%: {pct(steady, .95)}")
EOF

тЬЕ Verify тАФ what you should see

forecast prints:

тФАтФА the last 10 weeks' throughput (items finished): [2, 6, 1, 4, 7, 0, 5, 3, 6, 3] ┬╖ average 3.7/week ┬╖ backlog 40 items
   the single-date guess: 40 ├╖ 3.7 тЙИ 11 weeks
   Monte Carlo, 10,000 seeded trials replaying random past weeks тЖТ 50%: 11 weeks ┬╖ 85%: 13 weeks ┬╖ 95%: 15 weeks
   chance of finishing within the guess (11 weeks): 58%
тФАтФА the backlog grows 20% (40 тЖТ 48 items) тЖТ 50%: 13 weeks ┬╖ 85%: 16 weeks

Your snippet prints:

backlog 20 тЖТ 50%:  6 wk ┬╖ 85%:  7 wk ┬╖ 95%:  9 wk ┬╖ single guess  5 wk
backlog 40 тЖТ 50%: 11 wk ┬╖ 85%: 13 wk ┬╖ 95%: 15 wk ┬╖ single guess 11 wk
backlog 60 тЖТ 50%: 17 wk ┬╖ 85%: 19 wk ┬╖ 95%: 21 wk ┬╖ single guess 16 wk
seed 1 тЖТ 50%: 11 ┬╖ 85%: 13
seed 2 тЖТ 50%: 11 ┬╖ 85%: 13
seed 3 тЖТ 50%: 11 ┬╖ 85%: 13
a steadier team, same 3.7 average тЖТ 50%: 11 ┬╖ 85%: 12 ┬╖ 95%: 12

ЁЯПБ What you just proved

The single guess (11 weeks) sits at the median: only a 58% chance of being met. The 85% date is 13 weeks and the 95% date 15. Different seeds give the same answer, so 10,000 trials is enough for this history. A steadier team with the same average has a much narrower range (85% by week 12) тАФ predictability comes from less variation, not from a higher average. And a 20% bigger backlog moved the 85% date from 13 to 16 weeks.

тЪая╕П Common mistakes

ЁЯПн In production

Throughput is a count of finished items per week, which most trackers can export. On a real repository, merged pull requests per week are a rough stand-in (one item may be several PRs тАФ note that assumption):

gh pr list --state merged --limit 200 --json mergedAt \
  --jq '.[].mergedAt[0:10]' | sort | uniq -c | tail -20

The whole forecast fits in a few lines of your own script, or a spreadsheet:

import random
def forecast(history, backlog, trials=10_000):
    runs = []
    for _ in range(trials):
        done = weeks = 0
        while done < backlog:
            done += random.choice(history); weeks += 1
        runs.append(weeks)
    runs.sort()
    return {p: runs[int(p / 100 * trials) - 1] for p in (50, 85, 95)}

Say it to people like this:

Gradebook migration тАФ forecast, week 14
40 items left ┬╖ based on the last 10 weeks' throughput
50%: week 25 ┬╖ 85%: week 27 ┬╖ 95%: week 29
Assumptions: similar item mix; no new scope; Leela joins on-call in week 18.
Next re-forecast: Monday.

ЁЯПн Why this matters in production: replace "it will be done on the 14th" with "85% by the 21st, re-forecast weekly". People can plan around a range; they cannot plan around a date that is right half the time.

тПня╕П Next

The forecast assumed you already know what to do first. Next: deciding the order тАФ prioritisation, with the assumptions made visible.

git checkout lesson-06-prioritisation
тЖР PrevioushiringNext тЖТprioritisation

This page is the lesson's README from the lesson-05-forecasting branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.