← Back to projects

Shift Optimizer

prototype

A quick-service restaurant runs a 23-hour business day with a lunch rush, a dead mid-afternoon, and a skeleton night crew — and most managers cover all three by gut feel.

The problem

I had a working scheduler for exactly this, but it was buried. It lived in a 2,000-line Jupyter notebook where a simple greedy algorithm sat under config classes, abstract data-source interfaces, and a heavyweight matplotlib animator. It ran once, on my machine. Nobody else could read it — let alone trust the schedule it produced.

Why it mattered

A scheduling tool nobody can read is a scheduling tool nobody will use. As a portfolio piece it was worse than nothing: 2,000 lines of ceremony reads as over-engineering, not judgment. The one thing that actually mattered — the coverage logic — was invisible under the scaffolding.

What I built

I stripped it back to the idea: a ~75-line Python algorithm that smooths an hourly demand forecast, then greedily places shifts one at a time to fill the largest coverage gap — respecting shift-length preferences, mandatory breaks, and concurrency limits. Then I ported that same algorithm 1:1 into the browser as a single self-contained HTML page that animates each shift landing across a coverage chart, a deficit heatmap, and a live roster. No framework, no bundler — compiled Tailwind is the only build step.

  1. Step 1
    Smooth the forecast

    Exponentially smooth the raw hourly staffing forecast (70% current hour / 30% carried) with a minimum-staffing floor, so coverage targets don't whipsaw hour to hour.

  2. Step 2
    Greedily fill the deficit

    Score every candidate shift by how much understaffing it absorbs per hour — weighted by shift-length and time-of-day preferences — place the best one, and repeat until coverage meets target.

  3. Step 3
    Schedule around breaks

    Shifts of 5h+ take an unpaid break ~60% of the way through (never during the noon or dinner peaks); break hours don't count toward coverage, so the optimizer plans around them.

The three-stage pipeline that replaced 2,000 lines of notebook scaffolding with a 75-line algorithm.

Key decisions

Greedy heuristic vs. a constraint solver (OR-Tools). A solver is the right long-term answer once availability, cost, and labour law stack up — but it's a black box that demos badly and hides its reasoning. I chose greedy because every placement is explainable ("this shift absorbed the most understaffing per hour"), which is the entire point of a tool meant to show its work. I documented the solver as the honest next step rather than pretending greedy scales forever.

Port to JS vs. keep Python behind an API. Keeping Python meant a server, hosting, and network latency for what is fundamentally a client-side visualization. I ported the algorithm to JavaScript so the app is one static file that deploys anywhere — then verified the port produces schedules identical to the Python reference, so the logic stays trustworthy.

Outcome

2,000-line notebook ~75-line core algorithm
  • 2,000-line notebook → ~75-line core algorithm — the logic is now readable in one sitting.
  • Browser port verified identical to the Python reference — same inputs, same schedule.
  • 5 switchable demand scenarios (balanced weekday, lunch rush, dinner-heavy, late-night, weekend all-day), each recomputing and replaying instantly.
  • Zero runtime dependencies — one self-contained HTML page. No CDN, no framework, no server.

What a production version needs

This is a prototype, deliberately scoped to the coverage problem — not a scheduling product. To be real it would need:

  • Actual data — POS/sales-driven forecasts instead of hand-entered hourly arrays.
  • Real employees — availability, max hours, overtime, seniority, skills, and time-off. Right now every "employee" is interchangeable.
  • Cost optimization — labour cost % against sales and overtime avoidance, not just coverage.
  • Employment-standards compliance — meal/rest breaks, rest between shifts, weekly hour caps, and reporting pay. A phased plan for Saskatchewan (The Saskatchewan Employment Act) is written up in the repo.
  • A real solver — greedy demos well but makes defensibly-wrong trade-offs once availability, cost, and legal constraints stack up. Constraint programming (OR-Tools) is the right tool at that point.
Stack
PythonVanilla JSTailwind CSSIBM Plex Sans