Basic exploration
Totals, revenue by district, top products, orders by hour, platform split.
Data analytics portfolio
SQL & Power BI analysis of a mobile coffee-ordering app across 108 cafés in Prague
Brew Route (CUPS) is a mobile app for ordering coffee ahead and picking it up at a café. I analysed its 60-day demo database to answer business questions about customers, products, peak hours and operations — first with SQL, then with interactive Power BI dashboards.
01 · Data model
A star-like schema: orders is the central fact table, linked to customers, cafés and promo codes; each order has one or more order_items, each pointing to a product.
ordersorder_itemscustomerscafesproductspromo_codesJoined on customer_id for segmentation, frequency and spend.
Joined on cafe_id for district revenue and prep time.
Line level: product, size, milk, syrup, price.
02 · SQL analysis
Each file in /sql answers one group of business questions. Written in PostgreSQL.
Totals, revenue by district, top products, orders by hour, platform split.
Returning vs one-time, order frequency, top spenders, personas, age × gender.
Revenue share per product, categories, milk options, price with customisations.
Weekly orders, weekday comparison, new customers per week, WoW growth.
Prep time by café, cancellation rate, pickup modes, promo-code usage, dine-in vs takeaway.
sql/01_basic_exploration.sql · 1.2SELECT
c.district,
COUNT(o.order_id) AS orders,
SUM(o.total) AS revenue,
ROUND(AVG(o.total), 0) AS avg_check
FROM orders o
JOIN cafes c ON c.cafe_id = o.cafe_id
WHERE o.status = 'picked_up'
GROUP BY c.district
ORDER BY revenue DESC;
Praha 1 brings 39% of all revenue — more than the next two districts combined.
03 · SQL in practice
sql/01_basic_exploration.sql · 1.4SELECT
hour,
COUNT(*) AS orders
FROM orders
GROUP BY hour
ORDER BY hour;
Morning rush 8–10 AM = 27% of all orders; a second wave around 13:00.
sql/02_customer_analysis.sql · 2.2SELECT
CASE
WHEN cnt = 1 THEN '1 order'
WHEN cnt BETWEEN 2 AND 5 THEN '2–5 orders'
WHEN cnt BETWEEN 6 AND 15 THEN '6–15 orders'
ELSE '16+ orders'
END AS bucket,
COUNT(*) AS customers
FROM (
SELECT customer_id, COUNT(*) AS cnt
FROM orders
WHERE status = 'picked_up'
GROUP BY customer_id
) sub
GROUP BY bucket;
700 of 1,000 customers come back, averaging 13.9 orders each in 60 days.
sql/04_time_and_trends.sql · 4.4WITH weekly AS (
SELECT DATE_TRUNC('week', date)::DATE AS week, SUM(total) AS revenue
FROM orders WHERE status = 'picked_up'
GROUP BY DATE_TRUNC('week', date)
)
SELECT week, revenue,
LAG(revenue) OVER (ORDER BY week) AS prev_week,
ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY week))
/ LAG(revenue) OVER (ORDER BY week), 1) AS wow_growth_pct
FROM weekly ORDER BY week;
CTE + LAG window function: compares each week with the previous one — the standard way to track growth.
04 · Power BI
Power BI Desktop · slicers by district, café chain, age group, category and date.
dashboards/01_overview.pngdashboards/03_products.png05 · DAX & findings
-- total sales
Revenue = SUM( order_items[line_total] )
-- average order value
Avg Check = AVERAGE( orders[total] )
-- share of cancelled orders
Cancel Rate =
DIVIDE(
CALCULATE( COUNTROWS(orders),
orders[status] = "cancelled" ),
COUNTROWS(orders) )
806k CZK — the clear priority district.
~14 orders each over 60 days.
Peak at 9:00 — 1,607 orders.
Latte, Flat White, Cappuccino, Matcha Latte, Iced Latte.
Avg prep time 5.5 minutes.
57% placed via the iOS app, 43% via web.
06 · Skills demonstrated
JOIN · GROUP BY · HAVING · CASE · subqueries · CTE · LAG · DATE_TRUNC · COALESCE · FILTER
Data model · KPI cards · slicers · SUM · AVERAGE · DIVIDE · CALCULATE · COUNTROWS
PostgreSQL · SQLite · Excel · Git & GitHub · README case study