1. Dataset and Use Case¶
Points: 5 — Define a user persona, describe the analytics use case, and justify why the chosen transformations support it.
User Persona¶
Name: Alex, 26, HSLU student (part-time MSc in Applied Information and Data Science).
Situation: Alex works at an office near Zurich and drives in. Around 17:00 the same question shows up: leave now, or wait half an hour? Traffic in the city centre can swing a 10-minute drive into a 25-minute one, and rain makes it worse.
What Alex needs: a quick read on whether the route between two points is heavier or lighter than usual for the current hour, with the rain forecast already taken into account.
Use Case¶
Rush answers one question: "Should I leave the office now, or wait?"
Inputs:
| Input | Source | When |
|---|---|---|
| Driving route between two points | OSRM public demo | on demand |
| Average count per counter, hour, dow, month | mart_traffic_baseline |
precomputed |
| Weather coefficient per counter, hour, dow, precip bucket | mart_weather_effect |
precomputed |
| Precipitation forecast at the destination | Open-Meteo forecast | on demand |
Output:
{
"route": {"duration_s": 590.1, "distance_m": 5313.3, "source": "osrm"},
"precip_mm": 0.0,
"precip_bucket": "none",
"baseline_minutes": 9.835,
"weather_multiplier": 0.994,
"expected_minutes": 9.77,
"counters_used": 15,
"verdict": "fine to leave — usual conditions"
}
The verdict is just a band on the weather multiplier:
| multiplier | verdict |
|---|---|
| ≥ 1.10 | wait — traffic is heavier than usual |
| ≤ 0.90 | leave now — lighter than usual |
| in between | fine to leave — usual conditions |
Why no model¶
The first version tried to train a linear regression on counts. It never beat the rolling average by enough to justify the extra moving parts. The honest answer is: the historical average for the right hour, day-of-week and month already does most of the job, and weather adds a small but real second signal on top. So the marts are the model: two GROUP BY queries.
Why these transformations¶
flowchart TD
R1["traffic_raw.hourly_counts"] --> S1["stg_traffic__counts<br/>(table, derived h/dow/m)"]
S1 --> S2["stg_traffic__counters"]
R2["weather_raw.hourly_history"] --> S3["stg_weather__history<br/>(precip_bucket)"]
S1 --> M1["mart_traffic_baseline<br/>(avg per counter, h, dow, m)"]
S1 --> M2["mart_weather_effect<br/>(coef per counter, h, dow, bucket)"]
S3 --> M2
M1 --> REC["recommend.py"]
M2 --> REC
| Step | Why |
|---|---|
Derive hour_of_day, day_of_week, month_of_year on counts |
grouping keys for the baseline |
| Cast LV95 to WGS84 (in Python, not SQL) | recommender needs lat/lon to match OSRM |
| Bucket precipitation (none / light / moderate / heavy) | 4 readable buckets beat a continuous mm split |
mart_traffic_baseline |
the expected count per counter and hour |
mart_weather_effect |
how much that count moves under each bucket |
Every transformation either lands a column the recommender reads, or shrinks 4M rows into a 100k-row lookup that the recommender can hit cheaply at request time.