Skip to content

SharjeelJalil/courier-shift-optimization

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Courier Shift Optimization and Demand Planning

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


System Architecture

Problem

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.

Approach

Demand Forecasting

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.

Shift Generation

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:

  1. Ranks all candidates by forecasted demand (highest first)
  2. Greedily selects shifts, skipping any that overlap with already-selected shifts in the same city
  3. Computes couriers needed per shift: max(demand x coverage_ratio / courier_capacity, minimum_floor)
  4. 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.

Staffing Formula

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)

Dispatch Simulation

After generating shifts, the system validates the schedule through discrete-event simulation:

  1. Spawn courier objects at their assigned shift start times
  2. Generate orders according to the forecasted hourly demand pattern
  3. For each order, match to the nearest available on-shift courier in the same city
  4. 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?"

Results

Backtest Performance

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.

How the Improvement Breaks Down

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

What Was Real vs What Is Reconstructed

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

Validation Approach

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.

Limitations and What I Would Test Next

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.

Key Learnings

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Repository Structure

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

How to Run

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 pipeline

Note: 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.

About

Courier shift optimization and demand planning. Generates demand-aligned schedules across multiple cities using median demand forecasting, greedy shift selection, and dispatch simulation validation. Improved utilization from 0.8 to 1.1 orders per active hour.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors