mirror of
https://github.com/freedbygrace/SQL.git
synced 2026-07-26 11:28:16 +00:00
Expand to Business Analytics: Add Customer, Sales, and KPI models
Major expansion from fraud detection to comprehensive business analytics: DATABASE CHANGES: - Renamed database from 'fraud_detection' to 'business_analytics' - Renamed user from 'fraud_analyst' to 'data_analyst' - Expanded from 20 to 39 tables across 4 business models NEW MODELS (19 tables): 1. Customer Analytics (5 tables): - customer_segments, customer_lifetime_value, churn_predictions - customer_satisfaction, engagement_metrics 2. Sales & Revenue Analytics (6 tables): - product_catalog, sales_transactions, sales_targets - sales_performance, revenue_forecasts 3. KPI & Metrics (8 tables): - kpi_definitions, daily_metrics, monthly_summaries - trend_analysis, dashboard_snapshots - report_definitions, report_executions, data_quality_checks DATA GENERATION: - Extended generate_data.sh with 6 new steps (now 15 total) - Added CLV calculations for all customers - Added churn predictions based on transaction recency - Added 30K customer satisfaction surveys - Added 1M sales transactions linked to 24 products - Added 90 days of daily KPI metrics - Added 24 months of business summaries SQL EXERCISES (3 new levels): - Level 2: Customer Analytics (10 exercises + 3 challenges) - Level 3: Sales & Revenue Analysis (12 exercises + 3 challenges) - Level 4: KPI Dashboards & Metrics (12 exercises + 3 challenges) DOCUMENTATION: - Updated README.md with business analytics focus - Updated QUICKSTART.md with new data generation steps - Updated SETUP_COMPLETE.md with 39-table architecture - Added DATA_MODELS.md with complete model specifications - Added WHATS_NEW.md with migration guide SEED DATA: - Added 8 customer segments (VIP, High Value, etc.) - Added 24 products across 5 categories - Added 16 KPI definitions across 4 categories - Added 8 standard report definitions All changes maintain idempotency and backward compatibility with existing fraud detection functionality.
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
# Level 2: Customer Analytics
|
||||
|
||||
## Introduction
|
||||
Learn to analyze customer behavior, calculate lifetime value, identify churn risks, and segment customers - essential skills for Data Analysts working with customer data.
|
||||
|
||||
## Learning Objectives
|
||||
- Calculate customer lifetime value (CLV)
|
||||
- Identify at-risk customers
|
||||
- Segment customers by behavior
|
||||
- Analyze satisfaction trends
|
||||
- Track engagement metrics
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.1: Customer Segmentation
|
||||
**Objective:** Find the distribution of customers across segments
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
cs.segment_name,
|
||||
COUNT(clv.customer_id) as customer_count,
|
||||
AVG(clv.clv_score) as avg_clv_score,
|
||||
SUM(clv.total_revenue) as total_segment_revenue
|
||||
FROM customer_segments cs
|
||||
LEFT JOIN customer_lifetime_value clv ON cs.segment_id = clv.segment_id
|
||||
GROUP BY cs.segment_id, cs.segment_name
|
||||
ORDER BY total_segment_revenue DESC;
|
||||
```
|
||||
|
||||
**Expected Result:** Segment breakdown with counts and revenue
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.2: High-Value Customers
|
||||
**Objective:** Identify top 10 customers by lifetime value
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Hint:** Use customer_lifetime_value table and ORDER BY
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.first_name,
|
||||
c.last_name,
|
||||
c.email,
|
||||
clv.total_revenue,
|
||||
clv.total_transactions,
|
||||
clv.clv_score,
|
||||
cs.segment_name
|
||||
FROM customer_lifetime_value clv
|
||||
JOIN customers c ON clv.customer_id = c.customer_id
|
||||
JOIN customer_segments cs ON clv.segment_id = cs.segment_id
|
||||
ORDER BY clv.clv_score DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.3: Churn Risk Analysis
|
||||
**Objective:** Find customers at CRITICAL or HIGH churn risk
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.first_name,
|
||||
c.last_name,
|
||||
c.email,
|
||||
cp.churn_probability,
|
||||
cp.risk_level,
|
||||
cp.days_since_last_transaction,
|
||||
cp.engagement_score
|
||||
FROM churn_predictions cp
|
||||
JOIN customers c ON cp.customer_id = c.customer_id
|
||||
WHERE cp.risk_level IN ('CRITICAL', 'HIGH')
|
||||
ORDER BY cp.churn_probability DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.4: Customer Satisfaction Trends
|
||||
**Objective:** Calculate average NPS score by month
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Hint:** Use DATE_TRUNC or EXTRACT to group by month
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
DATE_TRUNC('month', survey_date) as month,
|
||||
COUNT(*) as survey_count,
|
||||
AVG(nps_score) as avg_nps,
|
||||
AVG(csat_score) as avg_csat,
|
||||
COUNT(CASE WHEN sentiment = 'POSITIVE' THEN 1 END) as positive_count,
|
||||
COUNT(CASE WHEN sentiment = 'NEGATIVE' THEN 1 END) as negative_count
|
||||
FROM customer_satisfaction
|
||||
GROUP BY DATE_TRUNC('month', survey_date)
|
||||
ORDER BY month DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.5: Engagement Score Analysis
|
||||
**Objective:** Find customers with declining engagement
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Hint:** Compare recent engagement to historical average
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
WITH recent_engagement AS (
|
||||
SELECT
|
||||
customer_id,
|
||||
AVG(engagement_score) as recent_score
|
||||
FROM engagement_metrics
|
||||
WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY customer_id
|
||||
),
|
||||
historical_engagement AS (
|
||||
SELECT
|
||||
customer_id,
|
||||
AVG(engagement_score) as historical_score
|
||||
FROM engagement_metrics
|
||||
WHERE metric_date < CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY customer_id
|
||||
)
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.first_name,
|
||||
c.last_name,
|
||||
re.recent_score,
|
||||
he.historical_score,
|
||||
(re.recent_score - he.historical_score) as score_change
|
||||
FROM customers c
|
||||
JOIN recent_engagement re ON c.customer_id = re.customer_id
|
||||
JOIN historical_engagement he ON c.customer_id = he.customer_id
|
||||
WHERE re.recent_score < he.historical_score
|
||||
ORDER BY score_change ASC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.6: Customer Cohort Analysis
|
||||
**Objective:** Analyze customer retention by registration cohort
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
DATE_TRUNC('month', c.registration_date) as cohort_month,
|
||||
COUNT(DISTINCT c.customer_id) as total_customers,
|
||||
COUNT(DISTINCT CASE WHEN cp.risk_level = 'LOW' THEN c.customer_id END) as active_customers,
|
||||
COUNT(DISTINCT CASE WHEN cp.risk_level IN ('HIGH', 'CRITICAL') THEN c.customer_id END) as at_risk_customers,
|
||||
ROUND(100.0 * COUNT(DISTINCT CASE WHEN cp.risk_level = 'LOW' THEN c.customer_id END) /
|
||||
COUNT(DISTINCT c.customer_id), 2) as retention_rate
|
||||
FROM customers c
|
||||
LEFT JOIN churn_predictions cp ON c.customer_id = cp.customer_id
|
||||
WHERE c.registration_date >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', c.registration_date)
|
||||
ORDER BY cohort_month DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.7: Customer Lifetime Value by Segment
|
||||
**Objective:** Compare CLV metrics across customer segments
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
cs.segment_name,
|
||||
COUNT(clv.customer_id) as customer_count,
|
||||
MIN(clv.clv_score) as min_clv,
|
||||
AVG(clv.clv_score) as avg_clv,
|
||||
MAX(clv.clv_score) as max_clv,
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY clv.clv_score) as median_clv,
|
||||
SUM(clv.total_revenue) as total_revenue,
|
||||
AVG(clv.total_transactions) as avg_transactions
|
||||
FROM customer_segments cs
|
||||
JOIN customer_lifetime_value clv ON cs.segment_id = clv.segment_id
|
||||
GROUP BY cs.segment_id, cs.segment_name
|
||||
ORDER BY avg_clv DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.8: Satisfaction by Category
|
||||
**Objective:** Analyze satisfaction scores by feedback category
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
category,
|
||||
COUNT(*) as feedback_count,
|
||||
AVG(nps_score) as avg_nps,
|
||||
AVG(csat_score) as avg_csat,
|
||||
ROUND(100.0 * COUNT(CASE WHEN sentiment = 'POSITIVE' THEN 1 END) / COUNT(*), 2) as positive_pct,
|
||||
ROUND(100.0 * COUNT(CASE WHEN sentiment = 'NEGATIVE' THEN 1 END) / COUNT(*), 2) as negative_pct
|
||||
FROM customer_satisfaction
|
||||
GROUP BY category
|
||||
ORDER BY avg_nps DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.9: Customer Engagement Patterns
|
||||
**Objective:** Find most engaged customers in the last 30 days
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.first_name,
|
||||
c.last_name,
|
||||
SUM(em.login_count) as total_logins,
|
||||
SUM(em.page_views) as total_page_views,
|
||||
SUM(em.time_spent_minutes) as total_time_spent,
|
||||
AVG(em.engagement_score) as avg_engagement_score
|
||||
FROM customers c
|
||||
JOIN engagement_metrics em ON c.customer_id = em.customer_id
|
||||
WHERE em.metric_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY c.customer_id, c.first_name, c.last_name
|
||||
HAVING AVG(em.engagement_score) > 75
|
||||
ORDER BY avg_engagement_score DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 2.10: Churn Prevention Priority List
|
||||
**Objective:** Create a priority list for customer retention efforts
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Hint:** Combine churn risk with customer value
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.first_name,
|
||||
c.last_name,
|
||||
c.email,
|
||||
clv.clv_score,
|
||||
clv.total_revenue,
|
||||
cp.churn_probability,
|
||||
cp.risk_level,
|
||||
cp.days_since_last_transaction,
|
||||
(clv.clv_score * cp.churn_probability / 100) as retention_priority_score
|
||||
FROM customers c
|
||||
JOIN customer_lifetime_value clv ON c.customer_id = clv.customer_id
|
||||
JOIN churn_predictions cp ON c.customer_id = cp.customer_id
|
||||
WHERE cp.risk_level IN ('HIGH', 'CRITICAL')
|
||||
AND clv.clv_score > 1000
|
||||
ORDER BY retention_priority_score DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Challenge Exercises
|
||||
|
||||
### Challenge 2.1: Customer Journey Analysis
|
||||
Create a query that shows the customer journey from registration to current status, including:
|
||||
- Registration date
|
||||
- First transaction date
|
||||
- Total transactions
|
||||
- Current segment
|
||||
- Churn risk
|
||||
- Latest satisfaction score
|
||||
|
||||
### Challenge 2.2: Segment Migration Analysis
|
||||
Identify customers who have moved between segments over time (requires historical CLV data)
|
||||
|
||||
### Challenge 2.3: Engagement Correlation
|
||||
Analyze the correlation between engagement scores and satisfaction scores
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
Once you're comfortable with customer analytics, move on to:
|
||||
- **Level 3:** Sales & Revenue Analysis
|
||||
- **Level 4:** KPI Dashboards & Metrics
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
# Level 3: Sales & Revenue Analysis
|
||||
|
||||
## Introduction
|
||||
Master sales performance analysis, product analytics, and revenue forecasting - core responsibilities for Data Analysts in sales-driven organizations.
|
||||
|
||||
## Learning Objectives
|
||||
- Analyze sales performance vs targets
|
||||
- Identify top-performing products
|
||||
- Compare sales across channels and regions
|
||||
- Calculate revenue metrics
|
||||
- Analyze sales trends
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.1: Daily Sales Summary
|
||||
**Objective:** Get today's sales summary
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) as total_sales,
|
||||
SUM(total_amount) as total_revenue,
|
||||
AVG(total_amount) as avg_sale_value,
|
||||
SUM(quantity) as total_units_sold,
|
||||
COUNT(DISTINCT product_id) as products_sold
|
||||
FROM sales_transactions
|
||||
WHERE DATE(sale_date) = CURRENT_DATE;
|
||||
```
|
||||
|
||||
**Expected Result:** Summary of today's sales activity
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.2: Top Products by Revenue
|
||||
**Objective:** Find the top 10 products by total revenue
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.product_name,
|
||||
p.product_category,
|
||||
p.product_subcategory,
|
||||
COUNT(st.transaction_id) as sales_count,
|
||||
SUM(st.quantity) as units_sold,
|
||||
SUM(st.total_amount) as total_revenue,
|
||||
AVG(st.total_amount) as avg_sale_value
|
||||
FROM product_catalog p
|
||||
JOIN sales_transactions st ON p.product_id = st.product_id
|
||||
GROUP BY p.product_id, p.product_name, p.product_category, p.product_subcategory
|
||||
ORDER BY total_revenue DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.3: Sales by Channel
|
||||
**Objective:** Compare sales performance across different channels
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
sales_channel,
|
||||
COUNT(*) as transaction_count,
|
||||
SUM(total_amount) as total_revenue,
|
||||
AVG(total_amount) as avg_transaction_value,
|
||||
SUM(quantity) as total_units,
|
||||
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) as pct_of_total_transactions
|
||||
FROM sales_transactions
|
||||
GROUP BY sales_channel
|
||||
ORDER BY total_revenue DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.4: Regional Performance
|
||||
**Objective:** Analyze sales by region
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
region,
|
||||
COUNT(*) as sales_count,
|
||||
SUM(total_amount) as total_revenue,
|
||||
AVG(total_amount) as avg_sale,
|
||||
SUM(discount_amount) as total_discounts,
|
||||
ROUND(100.0 * SUM(discount_amount) / SUM(total_amount), 2) as discount_rate
|
||||
FROM sales_transactions
|
||||
GROUP BY region
|
||||
ORDER BY total_revenue DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.5: Product Category Analysis
|
||||
**Objective:** Compare performance across product categories
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
p.product_category,
|
||||
COUNT(DISTINCT p.product_id) as product_count,
|
||||
COUNT(st.transaction_id) as sales_count,
|
||||
SUM(st.total_amount) as total_revenue,
|
||||
AVG(st.total_amount) as avg_sale_value,
|
||||
SUM(st.quantity) as units_sold,
|
||||
AVG(p.margin_percentage) as avg_margin
|
||||
FROM product_catalog p
|
||||
LEFT JOIN sales_transactions st ON p.product_id = st.product_id
|
||||
GROUP BY p.product_category
|
||||
ORDER BY total_revenue DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.6: Monthly Sales Trend
|
||||
**Objective:** Show sales trends over the past 12 months
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
DATE_TRUNC('month', sale_date) as month,
|
||||
COUNT(*) as transaction_count,
|
||||
SUM(total_amount) as total_revenue,
|
||||
AVG(total_amount) as avg_transaction_value,
|
||||
SUM(quantity) as units_sold
|
||||
FROM sales_transactions
|
||||
WHERE sale_date >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', sale_date)
|
||||
ORDER BY month DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.7: Sales Performance vs Target
|
||||
**Objective:** Compare actual sales to targets
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
st.target_period,
|
||||
st.target_category,
|
||||
st.target_value,
|
||||
COALESCE(sp.actual_value, 0) as actual_value,
|
||||
COALESCE(sp.actual_value, 0) - st.target_value as variance,
|
||||
ROUND(100.0 * COALESCE(sp.actual_value, 0) / st.target_value, 2) as achievement_pct,
|
||||
CASE
|
||||
WHEN COALESCE(sp.actual_value, 0) >= st.target_value THEN 'MET'
|
||||
WHEN COALESCE(sp.actual_value, 0) >= st.target_value * 0.9 THEN 'NEAR'
|
||||
ELSE 'MISSED'
|
||||
END as status
|
||||
FROM sales_targets st
|
||||
LEFT JOIN sales_performance sp ON st.target_id = sp.target_id
|
||||
WHERE st.target_period >= CURRENT_DATE - INTERVAL '6 months'
|
||||
ORDER BY st.target_period DESC, st.target_category;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.8: Product Profitability
|
||||
**Objective:** Calculate profit margins by product
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.product_name,
|
||||
p.product_category,
|
||||
p.unit_price,
|
||||
p.cost_price,
|
||||
p.margin_percentage,
|
||||
COUNT(st.transaction_id) as sales_count,
|
||||
SUM(st.quantity) as units_sold,
|
||||
SUM(st.total_amount) as total_revenue,
|
||||
SUM(st.quantity * p.cost_price) as total_cost,
|
||||
SUM(st.total_amount) - SUM(st.quantity * p.cost_price) as gross_profit
|
||||
FROM product_catalog p
|
||||
JOIN sales_transactions st ON p.product_id = st.product_id
|
||||
GROUP BY p.product_id, p.product_name, p.product_category, p.unit_price, p.cost_price, p.margin_percentage
|
||||
HAVING SUM(st.total_amount) > 0
|
||||
ORDER BY gross_profit DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.9: Revenue Forecast Accuracy
|
||||
**Objective:** Compare forecasted revenue to actual revenue
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
rf.forecast_period,
|
||||
rf.forecast_category,
|
||||
rf.forecasted_revenue,
|
||||
rf.confidence_level,
|
||||
ms.total_revenue as actual_revenue,
|
||||
(ms.total_revenue - rf.forecasted_revenue) as variance,
|
||||
ROUND(100.0 * ABS(ms.total_revenue - rf.forecasted_revenue) / rf.forecasted_revenue, 2) as error_pct
|
||||
FROM revenue_forecasts rf
|
||||
JOIN monthly_summaries ms ON
|
||||
EXTRACT(MONTH FROM rf.forecast_period) = ms.summary_month AND
|
||||
EXTRACT(YEAR FROM rf.forecast_period) = ms.summary_year
|
||||
WHERE rf.forecast_period >= CURRENT_DATE - INTERVAL '12 months'
|
||||
ORDER BY rf.forecast_period DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.10: Sales Velocity Analysis
|
||||
**Objective:** Identify fast-moving vs slow-moving products
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
WITH product_sales AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.product_name,
|
||||
p.product_category,
|
||||
COUNT(st.transaction_id) as sales_count,
|
||||
SUM(st.quantity) as units_sold,
|
||||
MIN(st.sale_date) as first_sale,
|
||||
MAX(st.sale_date) as last_sale,
|
||||
EXTRACT(DAY FROM MAX(st.sale_date) - MIN(st.sale_date)) as days_on_market
|
||||
FROM product_catalog p
|
||||
LEFT JOIN sales_transactions st ON p.product_id = st.product_id
|
||||
WHERE p.is_active = TRUE
|
||||
GROUP BY p.product_id, p.product_name, p.product_category
|
||||
)
|
||||
SELECT
|
||||
product_id,
|
||||
product_name,
|
||||
product_category,
|
||||
sales_count,
|
||||
units_sold,
|
||||
days_on_market,
|
||||
CASE
|
||||
WHEN days_on_market > 0 THEN ROUND(units_sold::DECIMAL / days_on_market, 2)
|
||||
ELSE 0
|
||||
END as units_per_day,
|
||||
CASE
|
||||
WHEN days_on_market > 0 AND (units_sold::DECIMAL / days_on_market) > 10 THEN 'FAST'
|
||||
WHEN days_on_market > 0 AND (units_sold::DECIMAL / days_on_market) > 5 THEN 'MEDIUM'
|
||||
WHEN days_on_market > 0 THEN 'SLOW'
|
||||
ELSE 'NO_SALES'
|
||||
END as velocity_category
|
||||
FROM product_sales
|
||||
ORDER BY units_per_day DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.11: Discount Impact Analysis
|
||||
**Objective:** Analyze the impact of discounts on sales
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
CASE
|
||||
WHEN discount_amount = 0 THEN 'No Discount'
|
||||
WHEN discount_amount / total_amount < 0.1 THEN '< 10%'
|
||||
WHEN discount_amount / total_amount < 0.2 THEN '10-20%'
|
||||
WHEN discount_amount / total_amount < 0.3 THEN '20-30%'
|
||||
ELSE '> 30%'
|
||||
END as discount_tier,
|
||||
COUNT(*) as transaction_count,
|
||||
AVG(total_amount) as avg_sale_value,
|
||||
SUM(total_amount) as total_revenue,
|
||||
SUM(discount_amount) as total_discounts,
|
||||
AVG(quantity) as avg_quantity
|
||||
FROM sales_transactions
|
||||
GROUP BY discount_tier
|
||||
ORDER BY
|
||||
CASE discount_tier
|
||||
WHEN 'No Discount' THEN 1
|
||||
WHEN '< 10%' THEN 2
|
||||
WHEN '10-20%' THEN 3
|
||||
WHEN '20-30%' THEN 4
|
||||
ELSE 5
|
||||
END;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 3.12: Cross-Sell Opportunities
|
||||
**Objective:** Find products frequently purchased together
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Hint:** Use self-join on transaction_id
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
p1.product_name as product_1,
|
||||
p2.product_name as product_2,
|
||||
COUNT(*) as times_purchased_together
|
||||
FROM sales_transactions st1
|
||||
JOIN sales_transactions st2 ON st1.transaction_id = st2.transaction_id
|
||||
AND st1.product_id < st2.product_id
|
||||
JOIN product_catalog p1 ON st1.product_id = p1.product_id
|
||||
JOIN product_catalog p2 ON st2.product_id = p2.product_id
|
||||
GROUP BY p1.product_id, p1.product_name, p2.product_id, p2.product_name
|
||||
HAVING COUNT(*) > 10
|
||||
ORDER BY times_purchased_together DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Challenge Exercises
|
||||
|
||||
### Challenge 3.1: Sales Seasonality
|
||||
Identify seasonal patterns in sales data by analyzing month-over-month and year-over-year trends
|
||||
|
||||
### Challenge 3.2: Customer Purchase Patterns
|
||||
Analyze average time between purchases and identify customers with regular buying patterns
|
||||
|
||||
### Challenge 3.3: Product Launch Performance
|
||||
Compare new product performance (launched in last 6 months) vs established products
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
Once you're comfortable with sales analysis, move on to:
|
||||
- **Level 4:** KPI Dashboards & Metrics
|
||||
- **Level 6:** Fraud Detection (Advanced)
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
# Level 4: KPI Dashboards & Metrics
|
||||
|
||||
## Introduction
|
||||
Learn to build and maintain KPI dashboards - a critical skill for Data Analysts supporting business decision-making.
|
||||
|
||||
## Learning Objectives
|
||||
- Track key performance indicators
|
||||
- Calculate trend metrics
|
||||
- Build dashboard queries
|
||||
- Monitor data quality
|
||||
- Create executive reports
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.1: Current KPI Status
|
||||
**Objective:** Get current status of all active KPIs
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
kd.kpi_name,
|
||||
kd.kpi_category,
|
||||
kd.target_value,
|
||||
dm.metric_value as current_value,
|
||||
dm.status,
|
||||
kd.unit_of_measure,
|
||||
ROUND(100.0 * dm.metric_value / kd.target_value, 2) as achievement_pct
|
||||
FROM kpi_definitions kd
|
||||
LEFT JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE kd.is_active = TRUE
|
||||
AND dm.metric_date = CURRENT_DATE
|
||||
ORDER BY kd.kpi_category, kd.kpi_name;
|
||||
```
|
||||
|
||||
**Expected Result:** Dashboard view of all KPIs with current status
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.2: KPIs Below Target
|
||||
**Objective:** Identify KPIs that are underperforming
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
kd.kpi_name,
|
||||
kd.kpi_category,
|
||||
kd.target_value,
|
||||
dm.metric_value as current_value,
|
||||
kd.threshold_warning,
|
||||
kd.threshold_critical,
|
||||
dm.status,
|
||||
(dm.metric_value - kd.target_value) as variance,
|
||||
ROUND(100.0 * (dm.metric_value - kd.target_value) / kd.target_value, 2) as variance_pct
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE kd.is_active = TRUE
|
||||
AND dm.metric_date = CURRENT_DATE
|
||||
AND dm.metric_value < kd.target_value
|
||||
ORDER BY variance_pct ASC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.3: KPI Trend Analysis (7 Days)
|
||||
**Objective:** Show 7-day trend for critical KPIs
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
kd.kpi_name,
|
||||
dm.metric_date,
|
||||
dm.metric_value,
|
||||
kd.target_value,
|
||||
dm.vs_previous_day_percentage,
|
||||
dm.status
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE kd.is_active = TRUE
|
||||
AND dm.metric_date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND kd.kpi_category IN ('SALES', 'CUSTOMER')
|
||||
ORDER BY kd.kpi_name, dm.metric_date DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.4: Monthly Performance Summary
|
||||
**Objective:** Aggregate monthly business metrics
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
summary_year,
|
||||
summary_month,
|
||||
TO_CHAR(TO_DATE(summary_month::TEXT, 'MM'), 'Month') as month_name,
|
||||
total_revenue,
|
||||
total_transactions,
|
||||
total_customers,
|
||||
new_customers,
|
||||
average_transaction_value,
|
||||
ROUND(100.0 * new_customers / total_customers, 2) as new_customer_pct
|
||||
FROM monthly_summaries
|
||||
WHERE summary_year >= EXTRACT(YEAR FROM CURRENT_DATE) - 1
|
||||
ORDER BY summary_year DESC, summary_month DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.5: Year-over-Year Comparison
|
||||
**Objective:** Compare this year's performance to last year
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
WITH current_year AS (
|
||||
SELECT
|
||||
summary_month,
|
||||
total_revenue,
|
||||
total_transactions,
|
||||
new_customers
|
||||
FROM monthly_summaries
|
||||
WHERE summary_year = EXTRACT(YEAR FROM CURRENT_DATE)
|
||||
),
|
||||
previous_year AS (
|
||||
SELECT
|
||||
summary_month,
|
||||
total_revenue,
|
||||
total_transactions,
|
||||
new_customers
|
||||
FROM monthly_summaries
|
||||
WHERE summary_year = EXTRACT(YEAR FROM CURRENT_DATE) - 1
|
||||
)
|
||||
SELECT
|
||||
cy.summary_month,
|
||||
TO_CHAR(TO_DATE(cy.summary_month::TEXT, 'MM'), 'Month') as month_name,
|
||||
cy.total_revenue as current_year_revenue,
|
||||
py.total_revenue as previous_year_revenue,
|
||||
(cy.total_revenue - py.total_revenue) as revenue_change,
|
||||
ROUND(100.0 * (cy.total_revenue - py.total_revenue) / py.total_revenue, 2) as revenue_growth_pct,
|
||||
cy.total_transactions as current_year_transactions,
|
||||
py.total_transactions as previous_year_transactions
|
||||
FROM current_year cy
|
||||
LEFT JOIN previous_year py ON cy.summary_month = py.summary_month
|
||||
ORDER BY cy.summary_month;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.6: Dashboard Snapshot
|
||||
**Objective:** Create a pre-calculated dashboard snapshot
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
INSERT INTO dashboard_snapshots (
|
||||
snapshot_date, dashboard_name, metric_name, metric_value, metric_category
|
||||
)
|
||||
SELECT
|
||||
CURRENT_DATE as snapshot_date,
|
||||
'Executive Dashboard' as dashboard_name,
|
||||
kd.kpi_name as metric_name,
|
||||
dm.metric_value,
|
||||
kd.kpi_category as metric_category
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE kd.is_active = TRUE
|
||||
AND dm.metric_date = CURRENT_DATE;
|
||||
|
||||
-- View the snapshot
|
||||
SELECT * FROM dashboard_snapshots
|
||||
WHERE snapshot_date = CURRENT_DATE
|
||||
ORDER BY metric_category, metric_name;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.7: Trend Detection
|
||||
**Objective:** Identify metrics with consistent upward or downward trends
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
WITH trend_data AS (
|
||||
SELECT
|
||||
kd.kpi_name,
|
||||
kd.kpi_category,
|
||||
dm.metric_date,
|
||||
dm.metric_value,
|
||||
dm.vs_previous_day_percentage,
|
||||
ROW_NUMBER() OVER (PARTITION BY kd.kpi_id ORDER BY dm.metric_date DESC) as day_rank
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE dm.metric_date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
)
|
||||
SELECT
|
||||
kpi_name,
|
||||
kpi_category,
|
||||
COUNT(*) as days_tracked,
|
||||
AVG(vs_previous_day_percentage) as avg_daily_change,
|
||||
MIN(metric_value) as min_value,
|
||||
MAX(metric_value) as max_value,
|
||||
CASE
|
||||
WHEN AVG(vs_previous_day_percentage) > 5 THEN 'STRONG UPWARD'
|
||||
WHEN AVG(vs_previous_day_percentage) > 0 THEN 'UPWARD'
|
||||
WHEN AVG(vs_previous_day_percentage) > -5 THEN 'DOWNWARD'
|
||||
ELSE 'STRONG DOWNWARD'
|
||||
END as trend_direction
|
||||
FROM trend_data
|
||||
GROUP BY kpi_name, kpi_category
|
||||
ORDER BY avg_daily_change DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.8: Data Quality Monitoring
|
||||
**Objective:** Track data quality metrics
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
check_date,
|
||||
table_name,
|
||||
check_type,
|
||||
records_checked,
|
||||
records_failed,
|
||||
ROUND(100.0 * records_failed / records_checked, 2) as failure_rate,
|
||||
status,
|
||||
error_details
|
||||
FROM data_quality_checks
|
||||
WHERE check_date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND status IN ('WARNING', 'FAILED')
|
||||
ORDER BY check_date DESC, failure_rate DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.9: Report Execution History
|
||||
**Objective:** Monitor report generation and performance
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
rd.report_name,
|
||||
rd.report_category,
|
||||
re.execution_date,
|
||||
re.execution_status,
|
||||
re.execution_time_seconds,
|
||||
re.records_generated,
|
||||
re.file_size_kb
|
||||
FROM report_definitions rd
|
||||
JOIN report_executions re ON rd.report_id = re.report_id
|
||||
WHERE re.execution_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
ORDER BY re.execution_date DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.10: Executive Summary Dashboard
|
||||
**Objective:** Create a comprehensive executive summary
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
WITH today_metrics AS (
|
||||
SELECT
|
||||
SUM(CASE WHEN kd.kpi_category = 'SALES' THEN dm.metric_value ELSE 0 END) as total_sales,
|
||||
AVG(CASE WHEN kd.kpi_name = 'Customer Engagement Score' THEN dm.metric_value END) as avg_engagement,
|
||||
AVG(CASE WHEN kd.kpi_name = 'Net Promoter Score' THEN dm.metric_value END) as avg_nps,
|
||||
COUNT(CASE WHEN dm.status = 'CRITICAL' THEN 1 END) as critical_kpis,
|
||||
COUNT(CASE WHEN dm.status = 'WARNING' THEN 1 END) as warning_kpis,
|
||||
COUNT(CASE WHEN dm.status = 'ON_TARGET' THEN 1 END) as on_target_kpis
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE dm.metric_date = CURRENT_DATE
|
||||
),
|
||||
monthly_summary AS (
|
||||
SELECT
|
||||
total_revenue,
|
||||
total_transactions,
|
||||
new_customers,
|
||||
average_transaction_value
|
||||
FROM monthly_summaries
|
||||
WHERE summary_year = EXTRACT(YEAR FROM CURRENT_DATE)
|
||||
AND summary_month = EXTRACT(MONTH FROM CURRENT_DATE)
|
||||
)
|
||||
SELECT
|
||||
'Executive Summary' as report_title,
|
||||
CURRENT_DATE as report_date,
|
||||
tm.total_sales,
|
||||
tm.avg_engagement,
|
||||
tm.avg_nps,
|
||||
tm.critical_kpis,
|
||||
tm.warning_kpis,
|
||||
tm.on_target_kpis,
|
||||
ms.total_revenue as mtd_revenue,
|
||||
ms.total_transactions as mtd_transactions,
|
||||
ms.new_customers as mtd_new_customers
|
||||
FROM today_metrics tm
|
||||
CROSS JOIN monthly_summary ms;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.11: Moving Average Calculation
|
||||
**Objective:** Calculate 7-day moving average for key metrics
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
SELECT
|
||||
kd.kpi_name,
|
||||
dm.metric_date,
|
||||
dm.metric_value,
|
||||
AVG(dm.metric_value) OVER (
|
||||
PARTITION BY kd.kpi_id
|
||||
ORDER BY dm.metric_date
|
||||
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
|
||||
) as moving_avg_7day,
|
||||
kd.target_value
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
WHERE kd.kpi_name IN ('Daily Revenue', 'Transactions Per Day', 'Customer Engagement Score')
|
||||
AND dm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
ORDER BY kd.kpi_name, dm.metric_date DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise 4.12: Anomaly Detection
|
||||
**Objective:** Identify unusual metric values
|
||||
|
||||
```sql
|
||||
-- Your query here
|
||||
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```sql
|
||||
WITH metric_stats AS (
|
||||
SELECT
|
||||
kpi_id,
|
||||
AVG(metric_value) as avg_value,
|
||||
STDDEV(metric_value) as stddev_value
|
||||
FROM daily_metrics
|
||||
WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY kpi_id
|
||||
)
|
||||
SELECT
|
||||
kd.kpi_name,
|
||||
dm.metric_date,
|
||||
dm.metric_value,
|
||||
ms.avg_value,
|
||||
ms.stddev_value,
|
||||
(dm.metric_value - ms.avg_value) / NULLIF(ms.stddev_value, 0) as z_score,
|
||||
CASE
|
||||
WHEN ABS((dm.metric_value - ms.avg_value) / NULLIF(ms.stddev_value, 0)) > 3 THEN 'EXTREME ANOMALY'
|
||||
WHEN ABS((dm.metric_value - ms.avg_value) / NULLIF(ms.stddev_value, 0)) > 2 THEN 'ANOMALY'
|
||||
ELSE 'NORMAL'
|
||||
END as anomaly_status
|
||||
FROM kpi_definitions kd
|
||||
JOIN daily_metrics dm ON kd.kpi_id = dm.kpi_id
|
||||
JOIN metric_stats ms ON kd.kpi_id = ms.kpi_id
|
||||
WHERE dm.metric_date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND ABS((dm.metric_value - ms.avg_value) / NULLIF(ms.stddev_value, 0)) > 2
|
||||
ORDER BY ABS((dm.metric_value - ms.avg_value) / NULLIF(ms.stddev_value, 0)) DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Challenge Exercises
|
||||
|
||||
### Challenge 4.1: Custom Dashboard Builder
|
||||
Create a flexible query that can generate different dashboard views based on parameters (date range, KPI category, etc.)
|
||||
|
||||
### Challenge 4.2: Predictive Alerting
|
||||
Build a query that predicts which KPIs are likely to miss targets based on current trends
|
||||
|
||||
### Challenge 4.3: Correlation Analysis
|
||||
Identify correlations between different KPIs (e.g., does customer engagement correlate with revenue?)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
You've now mastered the core Data Analyst skills! Continue with:
|
||||
- **Level 5:** Advanced Analytics (Window Functions, CTEs)
|
||||
- **Level 6:** Fraud Detection (Advanced Pattern Recognition)
|
||||
- Build your own custom dashboards and reports!
|
||||
|
||||
Reference in New Issue
Block a user