A shift planning system that generates demand-aligned courier schedules across multiple cities. Replaces fixed shift templates with data-driven scheduling based on historical demand patterns, improving courier utilization from 0.8 to 1.1 orders per active hour in backtests.
Industry: On-Demand Food Delivery (MENA Region)
Role: Senior Performance Analyst | Designed the demand forecasting, shift generation, and simulation framework
Tools: Python, SimPy, Pandas, GeoPandas
Context: Built for a multi-city delivery marketplace to optimize courier staffing against actual demand curves
The platform scheduled couriers using fixed shift templates: same start times, same durations, same staffing levels every day. This ignored the reality that demand has sharp peaks (lunch 12-2 PM, dinner 7-10 PM) and deep valleys (early morning, mid-afternoon).
The consequences were visible in two metrics:
- Low utilization during off-peak: Couriers sitting idle between 3-5 PM earning minimum guarantees with nothing to deliver
- Poor service during peaks: Not enough couriers during lunch and dinner rushes, leading to long ETAs and customer churn
Courier utilization was 0.8 orders per active hour, meaning the average courier delivered less than one order per hour they were being paid for. The goal was to push this above 1.0 by aligning shift timing with demand.
The system forecasts hourly demand per city using the median of historical order counts for each (city, day-of-week, hour) combination.
Median was chosen over mean deliberately: one abnormally busy Friday shouldn't inflate forecasts for all future Fridays. For shift planning, slightly conservative staffing estimates are better than overestimates that waste courier-hours.
The forecast horizon is 17 days ahead, covering the next scheduling cycle.
The optimizer generates every feasible shift window for every city and day:
- Duration range: 3-8 hours (configurable)
- Start hour: every hour from 0-23
- Each shift is scored by its total forecasted demand within the window
This produces thousands of candidate shifts per city-day. The system then:
- Ranks all candidates by forecasted demand (highest first)
- Greedily selects shifts, skipping any that overlap with already-selected shifts in the same city
- Computes couriers needed per shift:
max(demand x coverage_ratio / courier_capacity, minimum_floor) - Distributes couriers across starting points within each city
The greedy approach was chosen over integer programming because it produces near-optimal results in seconds and runs weekly without computational overhead.
couriers_needed = max(
forecasted_demand * DEMAND_COVERAGE_RATIO / courier_capacity,
MIN_COURIERS_PER_SHIFT
)
Where:
DEMAND_COVERAGE_RATIO = 0.26(what fraction of hourly demand to staff for)courier_capacity= average deliveries per courier per hour (computed from historical data)MIN_COURIERS_PER_SHIFT = 20(minimum viable shift size for operational reasons)
After generating shifts, the system validates the schedule through discrete-event simulation:
- Spawn courier objects at their assigned shift start times
- Generate orders according to the forecasted hourly demand pattern
- For each order, match to the nearest available on-shift courier in the same city
- Track service rate, pickup wait time, courier utilization, and unserved orders by hour
The simulation answers the question: "If we staff according to this schedule, what fraction of orders get served and how busy are the couriers?"
The shift optimizer was backtested against historical data by generating schedules from the first 3 weeks of data and evaluating against the 4th week's actual orders.
| Metric | Fixed Shifts (Before) | Optimized Shifts (After) |
|---|---|---|
| Courier utilization (orders/active hour) | 0.8 | 1.1 |
The 37.5% improvement in utilization came from two changes: concentrating courier-hours around peak demand windows, and reducing idle staffing during predictable off-peak periods.
The utilization gain comes from shift timing, not from adding or removing couriers:
- Peak coverage improved: More couriers available during lunch and dinner rushes
- Off-peak waste reduced: Fewer courier-hours allocated to periods with minimal demand
- Same total courier cost: The optimizer redistributes existing staffing budget, not increases it
| Layer | Status |
|---|---|
| Shift optimization logic (demand scoring, overlap removal, staffing formula) | Real — from original production code |
| Demand forecasting (median by city x day-of-week x hour) | Real — from original code |
| Starting point distribution | Real — from original code |
| Courier utilization improvement (0.8 to 1.1 orders/hour) | Real — from backtest results |
| Dispatch simulator | Reconstructed — original was a SimPy skeleton; rebuilt as modular validation tool |
| Sample data | Synthetic — matches production schema, values are generated |
| CLI packaging, tests, diagrams | Reconstructed — added for portfolio |
The optimizer was validated through backtesting: train on 3 weeks of historical orders, generate shifts, then measure simulated utilization against the 4th week's actual demand. The 0.8 to 1.1 improvement was measured in this backtest framework.
Assumptions:
- Median historical demand is a reasonable proxy for future demand. This holds for regular weekdays but breaks during holidays, Ramadan, or major events where manual overrides would be needed.
- Courier capacity (deliveries per hour) is roughly constant across shifts and cities. In reality, capacity varies by zone density, time of day, and individual courier speed.
- The coverage ratio (0.26) is constant. In practice, peak hours may need a higher ratio because order clustering creates temporary surges within the hour.
Known limitations:
- The demand forecast uses a simple median, which doesn't capture trends (growing/declining demand) or special events. A time-series model would improve forecast accuracy.
- The greedy shift selection is near-optimal but not provably optimal. Integer programming with shift-overlap constraints would guarantee the best solution.
- The simulation uses a simplified dispatch model (nearest available courier). Real dispatch considers courier speed, current direction, stacking, and zone boundaries.
- Courier preferences and labor constraints (max hours per week, minimum rest between shifts) are not modeled.
What I would test next:
- Demand forecasting with trend decomposition (e.g. Prophet) to capture week-over-week growth.
- Integer programming formulation to guarantee optimal shift selection under overlap and budget constraints.
- Zone-level shift planning instead of city-level, since demand within a city varies significantly by neighborhood.
- Dynamic re-optimization: adjust the next day's shifts based on today's actual demand vs forecast.
-
Fixed schedules waste money. Aligning shifts to demand curves is the single highest-ROI change in courier operations. The 0.8 to 1.1 utilization improvement represents 37.5% more output from the same staffing budget.
-
Median beats mean for shift planning. Outlier days (promotions, weather events) inflate mean demand and lead to overstaffing. Median captures the typical pattern that shift planning should target.
-
Simulation validates what math alone can't. The staffing formula tells you how many couriers to schedule. The simulation tells you whether those couriers can actually serve the orders given arrival timing, service duration, and geographic spread.
-
Greedy is good enough for weekly planning. The difference between greedy and optimal shift selection is small when the candidate pool is large. The computational simplicity of greedy makes it practical for a weekly cadence without infrastructure investment.
README.md
src/
shift_optimizer.py # Original shift optimization pipeline (preserved)
dispatch_simulator.py # Original SimPy dispatch skeleton (preserved)
demand_forecast.py # Modular demand forecasting (extracted from original)
shift_simulator.py # Rebuilt dispatch simulator for shift validation
__init__.py
tests/
test_shift_simulator.py # Simulation edge case tests
sample_data/
orders_sample.csv # Synthetic order data (3 cities, 30 days)
starting_points_sample.csv # Courier starting locations per city
notebooks/
methodology_walkthrough.ipynb
config/
model_config.yaml
diagrams/
architecture.png
requirements.txt
.gitignore
pip install -r requirements.txt
# Run tests
pytest tests/ -v
# The original optimizer expects order data in the schema shown in sample_data/
# See src/shift_optimizer.py for the full pipelineNote: This repository contains the shift optimization methodology and code. The dispatch simulator provides a validation framework for testing schedule quality. Production order data is excluded; synthetic sample data matching the original schema is provided.
