End-to-End Multi-Source Surge Pricing
Demand + driver supply + weather, traffic, and events β explainable pricing decision
Educational model: This project demonstrates the decision architecture behind dynamic pricing. It is not Uber's proprietary algorithm, does not claim to reproduce Uber prices, and deliberately uses transparent weights so students can debate them.
Learning Outcomes and Timeline
- Explain why a pricing decision needs multiple live sources rather than demand alone.
- Join demand, supply, and context streams into zone-level state.
- Read and challenge an explainable surge-pricing formula.
- Use a dashboard to separate a genuine demand shock from a data-quality problem.
- Recommend pricing guardrails based on customer, driver, and regulatory impact.
| Time | Segment | Output |
|---|---|---|
| 0β10 | Business problem | Why demand-only pricing fails |
| 10β25 | Architecture + formula | Source map and pricing policy |
| 25β40 | Start the pipeline | Three live Kafka sources |
| 40β65 | Grafana build | Five-panel pricing control room |
| 65β78 | Scenario observation | Normal β surge β recovery evidence |
| 78β88 | Policy challenge | Revised weights or guardrails |
| 88β90 | Briefing | 60-second management recommendation |
Section 1: The Business Case
A ride platform must balance two competing outcomes: riders need cars quickly, while drivers need a reason to move toward high-demand zones. A multiplier can help rebalance the marketβbut a weak rule can create unfair prices, false scarcity, or reputational risk.
The 60-Second Scenario
| Phase | Seconds | What Changes | Expected Decision |
|---|---|---|---|
| Normal | 0β19 | Balanced requests and drivers; light traffic | Multiplier stays near 1.0Γ |
| Surge | 20β44 | Concert ends during heavy rain; CBD and station demand rises while available drivers fall | CBD rises fastest, but never above 2.5Γ |
| Recovery | 45β59 | Rain reduces and drivers return | Multiplier declines gradually, not instantly |
Section 2: Architecture and Files
ride_requests_17.csv driver_supply_17.csv zone_context_17.csv
| | |
v v v
ride_request_producer driver_supply_producer context_producer
| | |
ride-requests-l17 driver-supply-l17 zone-context-l17
\_________________________|_________________________/
|
v
surge_pricing_consumer_17.py
10-second zone state + formula
|
v
MySQL: sda_course_l17 (4 audit tables)
|
v
Grafana pricing dashboard
Why separate topics? Demand, supply, and context have different owners, update rates, and failure modes. Kafka lets the pricing engine consume them together without forcing the source teams into one application.
Download Project Files
Section 3: Pricing Logic and Guardrails
For each zone, the consumer counts requests received in the last 10 seconds, combines the latest driver supply and context, then calculates:
| Guardrail | Management Purpose |
|---|---|
| Minimum 1.0Γ | This classroom model does not apply discounts. |
| Maximum 2.5Γ | Limits extreme customer impact during shocks. |
| Β±0.20 per decision | Avoids abrupt jumps caused by one update. |
| Visible components | Every decision stores the uncapped proposal, capped/rate-limited result, factor contributions, and a human-readable reason. |
| Isolated database | sda_course_l17 cannot overwrite Lecture 10 or 16 data. |
Decision question: Should rain increase price when demand remains below supply? The current formula says βslightly.β A team could instead use context only to explain demand pressure, not directly change price. There is no neutral choiceβonly a policy with consequences.
Section 4: Run the End-to-End Pipeline
1. Start Existing Infrastructure
docker start sda-mysql grafana # Kafka and ZooKeeper should also be running from earlier lectures. pip install kafka-python mysql-connector-python
If MySQL or Grafana has not been created, use the Docker setup from Lecture 16. The consumer creates the new database and grants the existing grafana user read-only access.
2. Generate the Three Sources
python generate_surge_data_17.py # Expected: # Ride requests : 735 # Supply updates: 300 # Context rows : 300
Every run receives a timestamp-based run ID, so event primary keys do not conflict with previous classroom runs.
Scenario checkpoint: With the default policy, the simulated peak is 2.5Γ in the Central Business District and Railway Station, about 1.41Γ at the Airport, and about 1.21Γ in the Tech Park and Residential zone.
3. Optional Clean Reset
docker exec -it sda-mysql mysql -uroot -padmin \ -e "DROP DATABASE IF EXISTS sda_course_l17;"
Classroom only: This deletes previous Lecture 17 evidence. Never use a destructive reset on a production database.
4. Start Four Terminals
| Terminal | Command | Role |
|---|---|---|
| 1 | python surge_pricing_consumer_17.py | Start first; creates schema and listens to all topics |
| 2 | python ride_request_producer_17.py | Demand stream |
| 3 | python driver_supply_producer_17.py | Supply stream |
| 4 | python context_producer_17.py | Weather, traffic, and event stream |
Launch the three producers close together. Each groups records by scenario second, publishes that second's events, then waits one real second. The scenario therefore unfolds live for approximately one minute.
5. Verify MySQL
docker exec sda-mysql mysql -uroot -padmin -e " SELECT COUNT(*) AS requests FROM sda_course_l17.ride_requests; SELECT COUNT(*) AS decisions FROM sda_course_l17.pricing_decisions; SELECT zone, MAX(final_multiplier) AS peak FROM sda_course_l17.pricing_decisions GROUP BY zone ORDER BY peak DESC;"
Section 5: Grafana Pricing Control Room
- Add a MySQL datasource named
MySQL β Lecture 17 Surge. - Host:
sda-mysql:3306| Database:sda_course_l17. - User/password:
grafana/grafana; disable TLS for the local lab. - Create dashboard
Surge Pricing Control Room β Lecture 17. - Set range to Last 5 minutes and refresh to 5s.
Variable β Zone
SELECT DISTINCT zone AS __text, zone AS __value FROM pricing_decisions ORDER BY zone;
Enable Multi-value and Include All. Use custom All value .*.
Panel A β Current Multiplier by Zone (Bar Gauge)
SELECT p.zone, p.final_multiplier AS value
FROM pricing_decisions p
JOIN (
SELECT zone, MAX(decision_id) AS latest_id
FROM pricing_decisions
GROUP BY zone
) latest ON p.decision_id = latest.latest_id
WHERE p.zone REGEXP '${zone:regex}'
ORDER BY value DESC;
Panel B β Multiplier over Time (Time Series)
SELECT
decision_time AS time,
final_multiplier AS value,
zone AS metric
FROM pricing_decisions
WHERE $__timeFilter(decision_time)
AND zone REGEXP '${zone:regex}'
ORDER BY decision_time;
Panel C β Demand versus Supply (Time Series)
SELECT
decision_time AS time,
demand_window AS `Requests in 10s`,
available_drivers AS `Available Drivers`
FROM pricing_decisions
WHERE $__timeFilter(decision_time)
AND zone = '${zone}'
ORDER BY decision_time;
For this panel, select one zone rather than All.
Panel D β Latest Factor Breakdown (Table)
SELECT
zone,
demand_supply_ratio,
demand_adjustment,
weather_adjustment,
traffic_adjustment,
event_adjustment,
final_multiplier,
reason
FROM pricing_decisions
WHERE zone REGEXP '${zone:regex}'
ORDER BY decision_id DESC
LIMIT 20;
Panel E β Pricing Decision Audit (Table)
SELECT
decision_time,
phase,
zone,
demand_window,
available_drivers,
raw_multiplier,
final_multiplier,
reason
FROM pricing_decisions
WHERE $__timeFilter(decision_time)
AND zone REGEXP '${zone:regex}'
ORDER BY decision_time DESC
LIMIT 50;
Section 6: Interpret, Challenge, and Govern
What Students Should Observe
- Normal phase multipliers remain close to 1.0Γ.
- The CBD and Railway Station respond more strongly than other zones.
- The final multiplier moves gradually because each decision is rate-limited.
- Recovery lags the source change; smoothing trades responsiveness for stability.
- The audit table explains whether demand, weather, traffic, or an event contributed.
Policy Challenge β Change One Rule
Each group chooses one change, predicts the effect, edits surge_pricing_logic_17.py, reruns the scenario, and compares the dashboard:
- Reduce the cap from 2.5Γ to 1.8Γ.
- Remove weather as a direct price adjustment.
- Increase the demand weight from 0.35 to 0.50.
- Slow the maximum change from Β±0.20 to Β±0.10.
- Require a minimum of 10 requests before any multiplier above 1.0Γ.
Fairness and Governance Discussion
| Risk | Management Question | Possible Control |
|---|---|---|
| Emergency price shock | Should surge apply during floods or evacuations? | Emergency-mode cap or disable rule |
| Geographic proxy bias | Do some neighbourhoods face systematically higher prices? | Zone fairness audit and outcome monitoring |
| False scarcity | What if driver telemetry is delayed? | Freshness checks; freeze multiplier on stale supply |
| Customer transparency | Can a rider understand why price increased? | Reason codes and pre-booking disclosure |
| Driver gaming | Can coordinated sign-offs create artificial shortage? | Anomaly detection and minimum evidence window |
60-Second Management Briefing
- Situation: Which zone changed, and when?
- Evidence: What happened to demand, supply, and context?
- Decision: Keep, cap, or suspend the multiplier?
- Guardrail: What customer or driver risk must be monitored?
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| No decisions | Supply or context producer has not started | Run all three producers; the engine needs complete zone state |
| Only one topic has rows | Producer started in the wrong folder | Keep all generated CSVs and scripts together |
| Dashboard is empty | Wrong database or time range | Use sda_course_l17 and Last 5 minutes |
| Multiplier changes too quickly | Producers were not launched together | Reset the database and restart the four terminals |
| Duplicate run data | Database was not reset | Filter by time or use the optional clean reset |
Key takeaway: Dynamic pricing is not a formula alone. It is a governed decision system: multiple sources, stateful processing, explainable rules, operational monitoring, and explicit limits on harm.