In [5]:
import sqlite3
import pandas as pd
import os

print("Libraries imported successfully.")
print("Current folder:", os.getcwd())
print("Files in this folder:", os.listdir())
Libraries imported successfully.
Current folder: C:\Users\pravi\SQL Project
Files in this folder: ['.ipynb_checkpoints', 'E commerce sales Analytics.ipynb', 'olist.db', 'olist_customers_dataset (1).csv', 'olist_geolocation_dataset.csv', 'olist_orders_dataset.csv', 'olist_order_items_dataset.csv', 'olist_order_payments_dataset.csv', 'olist_order_reviews_dataset.csv', 'olist_products_dataset.csv', 'olist_sellers_dataset.csv', 'product_category_name_translation.csv']
In [7]:
# This creates olist.db in the SAME folder as this notebook
conn = sqlite3.connect('olist.db')
cur = conn.cursor()

print("Connected to olist.db")
print("Database file created at:", os.path.abspath('olist.db'))
Connected to olist.db
Database file created at: C:\Users\pravi\SQL Project\olist.db
In [19]:
import os
[f for f in os.listdir() if f.endswith('.csv')]
Out[19]:
['olist_customers_dataset .csv',
 'olist_geolocation_dataset.csv',
 'olist_orders_dataset.csv',
 'olist_order_items_dataset.csv',
 'olist_order_payments_dataset.csv',
 'olist_order_reviews_dataset.csv',
 'olist_products_dataset.csv',
 'olist_sellers_dataset.csv',
 'product_category_name_translation.csv']

Loading CSV Files into the Database¶

In [21]:
DATA_FOLDER = "."   # "." means "this same folder". Change this if your CSVs are elsewhere, e.g. "C:/Users/YourName/Downloads"

# table_name : csv_filename  --- EDIT THE FILENAMES BELOW IF YOURS ARE DIFFERENT
csv_files = {
    'customers':            'olist_customers_dataset .csv',
    'geolocation':          'olist_geolocation_dataset.csv',
    'order_items':          'olist_order_items_dataset.csv',
    'order_payments':       'olist_order_payments_dataset.csv',
    'order_reviews':        'olist_order_reviews_dataset.csv',
    'orders':                'olist_orders_dataset.csv',
    'sellers':               'olist_sellers_dataset.csv',
    'category_translation': 'product_category_name_translation.csv',
}

for table_name, filename in csv_files.items():
    filepath = os.path.join(DATA_FOLDER, filename)
    if not os.path.exists(filepath):
        print(f"NOT FOUND (skipped): {filepath}  <-- fix the filename above")
        continue
    df = pd.read_csv(filepath)
    df.columns = [c.strip() for c in df.columns]           # remove accidental spaces in column names
    df.to_sql(table_name, conn, if_exists='replace', index=False)
    print(f"Loaded '{table_name}' -> {df.shape[0]} rows, {df.shape[1]} columns")

conn.commit()
print("\nAll available files loaded and saved into olist.db")
Loaded 'customers' -> 99441 rows, 5 columns
Loaded 'geolocation' -> 1000163 rows, 5 columns
Loaded 'order_items' -> 112650 rows, 7 columns
Loaded 'order_payments' -> 103886 rows, 5 columns
Loaded 'order_reviews' -> 99224 rows, 7 columns
Loaded 'orders' -> 99441 rows, 8 columns
Loaded 'sellers' -> 3095 rows, 4 columns
Loaded 'category_translation' -> 71 rows, 2 columns

All available files loaded and saved into olist.db

Double check the tables actually loaded¶

In [23]:
tables = pd.read_sql_query("SELECT name FROM sqlite_master WHERE type='table'", conn)
print("Tables currently in olist.db:")
print(tables)
Tables currently in olist.db:
                   name
0             customers
1           geolocation
2           order_items
3        order_payments
4         order_reviews
5                orders
6               sellers
7  category_translation

Data cleaning¶

In [27]:
cleaning_sql = """
CREATE VIEW IF NOT EXISTS clean_order_reviews AS
SELECT * FROM (
    SELECT r.*,
           ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY review_answer_timestamp DESC) AS rn
    FROM order_reviews r
) WHERE rn = 1;

CREATE VIEW IF NOT EXISTS order_revenue AS
SELECT order_id,
       SUM(price) AS items_revenue,
       SUM(freight_value) AS freight_revenue,
       SUM(price + freight_value) AS total_revenue,
       COUNT(*) AS item_count
FROM order_items
GROUP BY order_id;

CREATE VIEW IF NOT EXISTS fact_orders AS
SELECT
    o.order_id,
    o.customer_id,
    c.customer_unique_id,
    c.customer_state,
    c.customer_city,
    o.order_status,
    o.order_purchase_timestamp,
    o.order_approved_at,
    o.order_delivered_carrier_date,
    o.order_delivered_customer_date,
    o.order_estimated_delivery_date,
    r.total_revenue,
    r.item_count,
    rv.review_score,
    JULIANDAY(o.order_delivered_customer_date) - JULIANDAY(o.order_purchase_timestamp) AS delivery_days,
    JULIANDAY(o.order_estimated_delivery_date) - JULIANDAY(o.order_delivered_customer_date) AS delivery_buffer_days
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
LEFT JOIN order_revenue r ON r.order_id = o.order_id
LEFT JOIN clean_order_reviews rv ON rv.order_id = o.order_id
WHERE o.order_status = 'delivered'
  AND o.order_delivered_customer_date IS NOT NULL;
"""

cur.executescript(cleaning_sql)
conn.commit()
print("Cleaning views created: clean_order_reviews, order_revenue, fact_orders")
Cleaning views created: clean_order_reviews, order_revenue, fact_orders
In [29]:
# Confirm fact_orders was built correctly
pd.read_sql_query("SELECT COUNT(*) AS total_delivered_orders FROM fact_orders", conn)
Out[29]:
total_delivered_orders
0 96470
In [31]:
# Q1: Headline KPIs — total revenue, total orders, total customers, average order value
query = """
SELECT
    ROUND(SUM(total_revenue),2) AS total_revenue,
    COUNT(DISTINCT order_id) AS total_orders,
    COUNT(DISTINCT customer_unique_id) AS total_customers,
    ROUND(SUM(total_revenue)/COUNT(DISTINCT order_id),2) AS avg_order_value
FROM fact_orders;
"""
pd.read_sql_query(query, conn)
Out[31]:
total_revenue total_orders total_customers avg_order_value
0 15418394.83 96470 93350 159.83
In [33]:
# Q2: Monthly revenue trend
query = """
SELECT strftime('%Y-%m', order_purchase_timestamp) AS month,
       ROUND(SUM(total_revenue),2) AS monthly_revenue,
       COUNT(DISTINCT order_id) AS orders
FROM fact_orders
GROUP BY month
ORDER BY month;
"""
pd.read_sql_query(query, conn)
Out[33]:
month monthly_revenue orders
0 2016-09 143.46 1
1 2016-10 46490.66 265
2 2016-12 19.62 1
3 2017-01 127482.37 750
4 2017-02 271239.32 1653
5 2017-03 414330.95 2546
6 2017-04 390812.40 2303
7 2017-05 566657.40 3545
8 2017-06 490050.37 3135
9 2017-07 566299.08 3872
10 2017-08 645832.36 4193
11 2017-09 701077.49 4150
12 2017-10 751117.01 4478
13 2017-11 1153229.37 7288
14 2017-12 843078.29 5513
15 2018-01 1077887.46 7069
16 2018-02 966168.41 6555
17 2018-03 1120598.24 7003
18 2018-04 1132878.93 6798
19 2018-05 1128774.52 6749
20 2018-06 1011448.96 6096
21 2018-07 1027286.52 6156
22 2018-08 985491.64 6351
In [35]:
# Q3: Repeat customer rate — the single most important retention metric
query = """
SELECT
    ROUND(100.0 *
        (SELECT COUNT(*) FROM (
            SELECT customer_unique_id FROM fact_orders
            GROUP BY customer_unique_id HAVING COUNT(DISTINCT order_id) > 1
        ))
        / (SELECT COUNT(DISTINCT customer_unique_id) FROM fact_orders)
    , 2) AS repeat_customer_rate_pct;
"""
pd.read_sql_query(query, conn)
Out[35]:
repeat_customer_rate_pct
0 3.0
In [37]:
# Q4: On-time vs late delivery rate, and its effect on review scores
query = """
SELECT
    CASE WHEN order_delivered_customer_date <= order_estimated_delivery_date
         THEN 'On-Time' ELSE 'Late' END AS delivery_status,
    COUNT(*) AS orders,
    ROUND(AVG(review_score),2) AS avg_review_score
FROM fact_orders
WHERE review_score IS NOT NULL
GROUP BY delivery_status;
"""
pd.read_sql_query(query, conn)
Out[37]:
delivery_status orders avg_review_score
0 Late 7661 2.57
1 On-Time 88163 4.29
In [39]:
# Q5: Revenue by customer state, ranked highest to lowest
query = """
SELECT customer_state,
       ROUND(SUM(total_revenue),2) AS revenue,
       COUNT(DISTINCT order_id) AS orders,
       DENSE_RANK() OVER (ORDER BY SUM(total_revenue) DESC) AS revenue_rank
FROM fact_orders
GROUP BY customer_state
ORDER BY revenue_rank;
"""
pd.read_sql_query(query, conn)
Out[39]:
customer_state revenue orders revenue_rank
0 SP 5768518.23 40494 1
1 RJ 2055401.57 12350 2
2 MG 1818891.67 11354 3
3 RS 861278.79 5344 4
4 PR 781708.80 4923 5
5 SC 595127.78 3546 6
6 BA 591137.81 3256 7
7 DF 346123.35 2080 8
8 GO 334212.35 1957 9
9 ES 317657.93 1995 10
10 PE 308972.05 1593 11
11 CE 266436.77 1279 12
12 PA 212023.57 946 13
13 MT 181224.42 886 14
14 MA 147803.55 717 15
15 PB 137838.55 517 16
16 MS 134367.55 701 17
17 PI 105178.19 476 18
18 RN 100714.78 474 19
19 AL 94172.49 397 20
20 SE 70289.13 335 21
21 TO 60007.37 274 22
22 RO 56966.00 243 23
23 AM 27585.47 145 24
24 AC 19575.33 80 25
25 AP 16141.81 67 26
26 RR 9039.52 41 27
In [40]:
# Q6: Top 10 sellers by revenue
query = """
SELECT oi.seller_id, s.seller_state,
       ROUND(SUM(oi.price),2) AS revenue,
       COUNT(DISTINCT oi.order_id) AS orders_fulfilled
FROM order_items oi
JOIN sellers s ON s.seller_id = oi.seller_id
JOIN fact_orders fo ON fo.order_id = oi.order_id
GROUP BY oi.seller_id, s.seller_state
ORDER BY revenue DESC
LIMIT 10;
"""
pd.read_sql_query(query, conn)
Out[40]:
seller_id seller_state revenue orders_fulfilled
0 4869f7a5dfa277a7dca6462dcf3b52b2 SP 226987.93 1124
1 53243585a1d6dc2643021fd1853d8905 BA 217940.44 348
2 4a3ca9315b744ce9f8e9374361493884 SP 196882.12 1772
3 fa1c13f2614d7b5c4749cbc52fecda94 SP 190917.14 578
4 7c67e1448b00f6e969d365cea6b010ab SP 186570.05 973
5 7e93a43ef30c4f03f38b393420bc753a SP 165981.49 319
6 da8622b14eb17ae2831f4ac5b9dab84a SP 159816.87 1311
7 7a67c85e85bb2ce8582c35f2203ad736 SP 139658.69 1145
8 1025f0e2d44d7041d6cf58b6550e0bfa SP 138208.56 910
9 955fee9216a65b617aa5c0531780ce60 SP 131836.71 1261
In [43]:
# Q7: Payment method mix
query = """
SELECT payment_type,
       COUNT(*) AS transactions,
       ROUND(SUM(payment_value),2) AS total_value
FROM order_payments
GROUP BY payment_type
ORDER BY total_value DESC;
"""
pd.read_sql_query(query, conn)
Out[43]:
payment_type transactions total_value
0 credit_card 76795 12542084.19
1 boleto 19784 2869361.27
2 voucher 5775 379436.87
3 debit_card 1529 217989.79
4 not_defined 3 0.00
In [45]:
# Q8: Month-over-month revenue growth — using LAG() to compare each month to the previous one
query = """
WITH monthly AS (
    SELECT strftime('%Y-%m', order_purchase_timestamp) AS month,
           SUM(total_revenue) AS revenue
    FROM fact_orders
    GROUP BY month
)
SELECT month, ROUND(revenue,2) AS revenue,
       ROUND(revenue - LAG(revenue) OVER (ORDER BY month), 2) AS mom_change,
       ROUND(100.0*(revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month), 2) AS mom_growth_pct
FROM monthly
ORDER BY month;
"""
pd.read_sql_query(query, conn)
Out[45]:
month revenue mom_change mom_growth_pct
0 2016-09 143.46 NaN NaN
1 2016-10 46490.66 46347.20 32306.71
2 2016-12 19.62 -46471.04 -99.96
3 2017-01 127482.37 127462.75 649657.24
4 2017-02 271239.32 143756.95 112.77
5 2017-03 414330.95 143091.63 52.75
6 2017-04 390812.40 -23518.55 -5.68
7 2017-05 566657.40 175845.00 44.99
8 2017-06 490050.37 -76607.03 -13.52
9 2017-07 566299.08 76248.71 15.56
10 2017-08 645832.36 79533.28 14.04
11 2017-09 701077.49 55245.13 8.55
12 2017-10 751117.01 50039.52 7.14
13 2017-11 1153229.37 402112.36 53.54
14 2017-12 843078.29 -310151.08 -26.89
15 2018-01 1077887.46 234809.17 27.85
16 2018-02 966168.41 -111719.05 -10.36
17 2018-03 1120598.24 154429.83 15.98
18 2018-04 1132878.93 12280.69 1.10
19 2018-05 1128774.52 -4104.41 -0.36
20 2018-06 1011448.96 -117325.56 -10.39
21 2018-07 1027286.52 15837.56 1.57
22 2018-08 985491.64 -41794.88 -4.07
In [47]:
# Q9: Cumulative revenue over time — shows total business growth trajectory
query = """
SELECT strftime('%Y-%m', order_purchase_timestamp) AS month,
       ROUND(SUM(total_revenue),2) AS monthly_revenue,
       ROUND(SUM(SUM(total_revenue)) OVER (ORDER BY strftime('%Y-%m', order_purchase_timestamp)), 2) AS running_total_revenue
FROM fact_orders
GROUP BY month
ORDER BY month;
"""
pd.read_sql_query(query, conn)
Out[47]:
month monthly_revenue running_total_revenue
0 2016-09 143.46 143.46
1 2016-10 46490.66 46634.12
2 2016-12 19.62 46653.74
3 2017-01 127482.37 174136.11
4 2017-02 271239.32 445375.43
5 2017-03 414330.95 859706.38
6 2017-04 390812.40 1250518.78
7 2017-05 566657.40 1817176.18
8 2017-06 490050.37 2307226.55
9 2017-07 566299.08 2873525.63
10 2017-08 645832.36 3519357.99
11 2017-09 701077.49 4220435.48
12 2017-10 751117.01 4971552.49
13 2017-11 1153229.37 6124781.86
14 2017-12 843078.29 6967860.15
15 2018-01 1077887.46 8045747.61
16 2018-02 966168.41 9011916.02
17 2018-03 1120598.24 10132514.26
18 2018-04 1132878.93 11265393.19
19 2018-05 1128774.52 12394167.71
20 2018-06 1011448.96 13405616.67
21 2018-07 1027286.52 14432903.19
22 2018-08 985491.64 15418394.83
In [49]:
# Q10: 3-month moving average — removes month-to-month noise to see the real trend
query = """
WITH monthly AS (
    SELECT strftime('%Y-%m', order_purchase_timestamp) AS month, SUM(total_revenue) AS revenue
    FROM fact_orders GROUP BY month
)
SELECT month, ROUND(revenue,2) AS revenue,
       ROUND(AVG(revenue) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS moving_avg_3mo
FROM monthly ORDER BY month;
"""
pd.read_sql_query(query, conn)
Out[49]:
month revenue moving_avg_3mo
0 2016-09 143.46 143.46
1 2016-10 46490.66 23317.06
2 2016-12 19.62 15551.25
3 2017-01 127482.37 57997.55
4 2017-02 271239.32 132913.77
5 2017-03 414330.95 271017.55
6 2017-04 390812.40 358794.22
7 2017-05 566657.40 457266.92
8 2017-06 490050.37 482506.72
9 2017-07 566299.08 541002.28
10 2017-08 645832.36 567393.94
11 2017-09 701077.49 637736.31
12 2017-10 751117.01 699342.29
13 2017-11 1153229.37 868474.62
14 2017-12 843078.29 915808.22
15 2018-01 1077887.46 1024731.71
16 2018-02 966168.41 962378.05
17 2018-03 1120598.24 1054884.70
18 2018-04 1132878.93 1073215.19
19 2018-05 1128774.52 1127417.23
20 2018-06 1011448.96 1091034.14
21 2018-07 1027286.52 1055836.67
22 2018-08 985491.64 1008075.71

Pareto Analysis¶

In [52]:
# Q11: What % of revenue comes from the top 15% of customers? (Pareto / 80-20 style analysis)
query = """
WITH cust_rev AS (
    SELECT customer_unique_id, SUM(total_revenue) AS rev
    FROM fact_orders GROUP BY customer_unique_id
),
tiled AS (
    SELECT *, NTILE(100) OVER (ORDER BY rev DESC) AS pct_tile FROM cust_rev
)
SELECT ROUND(100.0*SUM(CASE WHEN pct_tile <= 15 THEN rev ELSE 0 END)/SUM(rev), 2) AS revenue_pct_from_top15pct_customers
FROM tiled;
"""
pd.read_sql_query(query, conn)
Out[52]:
revenue_pct_from_top15pct_customers
0 46.71

Repeat Purchase Gap (self-JOIN + ROW_NUMBER)¶

In [55]:
# Q12: Average days between a customer's 1st and 2nd purchase — reveals the retention window
query = """
WITH ordered_purchases AS (
    SELECT customer_unique_id, order_id, order_purchase_timestamp,
           ROW_NUMBER() OVER (PARTITION BY customer_unique_id ORDER BY order_purchase_timestamp) AS purchase_seq
    FROM fact_orders
)
SELECT AVG(JULIANDAY(o2.order_purchase_timestamp) - JULIANDAY(o1.order_purchase_timestamp)) AS avg_days_between_1st_2nd_purchase
FROM ordered_purchases o1
JOIN ordered_purchases o2
  ON o1.customer_unique_id = o2.customer_unique_id
  AND o1.purchase_seq = 1 AND o2.purchase_seq = 2;
"""
pd.read_sql_query(query, conn)
Out[55]:
avg_days_between_1st_2nd_purchase
0 81.206685

Seller Revenue Concentration (RANK + Pareto)¶

In [62]:
# Q13: Are we dependent on a small number of sellers? Top 10 sellers' share of platform revenue
query = """
WITH seller_rev AS (
    SELECT oi.seller_id, SUM(oi.price) AS revenue
    FROM order_items oi JOIN fact_orders fo ON fo.order_id = oi.order_id
    GROUP BY oi.seller_id
),
ranked AS (SELECT *, RANK() OVER (ORDER BY revenue DESC) AS rnk FROM seller_rev)
SELECT ROUND(100.0*SUM(CASE WHEN rnk<=10 THEN revenue ELSE 0 END)/(SELECT SUM(revenue) FROM seller_rev), 2) AS top10_seller_revenue_pct
FROM ranked;
"""
pd.read_sql_query(query, conn)
Out[62]:
top10_seller_revenue_pct
0 13.27

Sellers with Declining Monthly Revenue (LAG-based anomaly flag)¶

Q14: Detect sellers whose revenue dropped vs. the previous month — an early-warning signal¶

query = """ WITH seller_monthly AS ( SELECT oi.seller_id, strftime('%Y-%m', fo.order_purchase_timestamp) AS month, SUM(oi.price) AS revenue FROM order_items oi JOIN fact_orders fo ON fo.order_id = oi.order_id GROUP BY oi.seller_id, month ), with_lag AS ( SELECT *, LAG(revenue) OVER (PARTITION BY seller_id ORDER BY month) AS prev_month_revenue FROM seller_monthly ) SELECT seller_id, month, ROUND(revenue,2) AS revenue, ROUND(prev_month_revenue,2) AS prev_month_revenue FROM with_lag WHERE revenue < prev_month_revenue LIMIT 15; """ pd.read_sql_query(query, conn)

Delivery Delay Deciles (NTILE for percentile buckets)¶

In [69]:
# Q15: Break delivery times into 10 equal buckets to see the full distribution, not just the average
query = """
WITH d AS (
    SELECT delivery_days, NTILE(10) OVER (ORDER BY delivery_days) AS decile FROM fact_orders
)
SELECT decile, ROUND(MIN(delivery_days),1) AS min_days, ROUND(MAX(delivery_days),1) AS max_days
FROM d GROUP BY decile ORDER BY decile;
"""
pd.read_sql_query(query, conn)
Out[69]:
decile min_days max_days
0 1 0.5 4.2
1 2 4.2 6.0
2 3 6.0 7.3
3 4 7.3 8.7
4 5 8.7 10.2
5 6 10.2 12.1
6 7 12.1 14.2
7 8 14.2 17.4
8 9 17.4 23.1
9 10 23.1 209.6

Correlated Subquery: Above-Average Installment Buyers¶

In [72]:
# Q16: Find orders where a customer paid in MORE installments than their own personal average
# (a correlated subquery — the inner query depends on the outer row, re-runs per row)
query = """
SELECT op.order_id, op.payment_installments
FROM order_payments op
WHERE op.payment_installments > (
    SELECT AVG(op2.payment_installments)
    FROM order_payments op2
    JOIN orders o2 ON o2.order_id = op2.order_id
    JOIN orders o1 ON o1.order_id = op.order_id
    WHERE o2.customer_id = o1.customer_id
)
LIMIT 10;
"""
pd.read_sql_query(query, conn)
Out[72]:
order_id payment_installments
0 332c6742772f2df936696b6512b10edb 6
1 97e8d3eb8f48e3610beed991e5f32296 4
2 ac14a1a3d8c2b071a150eb2990b5ecd8 2
3 d289934bddf878e1cac62c95c8ee5402 3
4 54066aeaaf3ac32e7bb6e45aa3bf65e4 2
5 20b44662cba0e4e87a3d794343c1076c 4
6 7525e8f8b1a51d44f9912ce81146ac7d 10
7 50517e1413e46c6df586274af938d47e 2
8 4821d5af4c2ac98b0f70e47c5d845520 3
9 ce79ae0eb1e344fbf19835147efc64e4 6

Reusable VIEW: Monthly KPI Dashboard Source¶

In [74]:
# Q17: Build a reusable VIEW so any future query/dashboard just references vw_monthly_kpis
query_create = """
CREATE VIEW IF NOT EXISTS vw_monthly_kpis AS
SELECT strftime('%Y-%m', order_purchase_timestamp) AS month,
       COUNT(DISTINCT order_id) AS orders,
       COUNT(DISTINCT customer_unique_id) AS customers,
       ROUND(SUM(total_revenue),2) AS revenue,
       ROUND(AVG(review_score),2) AS avg_review_score,
       ROUND(AVG(delivery_days),1) AS avg_delivery_days
FROM fact_orders GROUP BY month;
"""
cur.executescript(query_create)
conn.commit()
pd.read_sql_query("SELECT * FROM vw_monthly_kpis ORDER BY month LIMIT 5", conn)
Out[74]:
month orders customers revenue avg_review_score avg_delivery_days
0 2016-09 1 1 143.46 1.00 54.8
1 2016-10 265 262 46490.66 4.01 19.6
2 2016-12 1 1 19.62 5.00 4.7
3 2017-01 750 718 127482.37 4.20 12.6
4 2017-02 1653 1630 271239.32 4.20 13.2

Index Optimization + EXPLAIN QUERY PLAN¶

In [76]:
# Q18: Add indexes to speed up the joins/filters used throughout, then verify SQLite is using them
index_sql = """
CREATE INDEX IF NOT EXISTS idx_orders_customer_id ON orders(customer_id);
CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
CREATE INDEX IF NOT EXISTS idx_order_items_seller_id ON order_items(seller_id);
CREATE INDEX IF NOT EXISTS idx_order_payments_order_id ON order_payments(order_id);
"""
cur.executescript(index_sql)
conn.commit()

plan = pd.read_sql_query("EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = '0000366f3b9a7992bf8c76cfdf3221e2'", conn)
plan
Out[76]:
id parent notused detail
0 3 0 0 SEARCH orders USING INDEX idx_orders_customer_...

Statistically Significant Finding (via correlation)¶

In [78]:
# Q19: Correlation between delivery days and review score — quantifies the delay-satisfaction link
query = """
SELECT delivery_days, review_score
FROM fact_orders
WHERE review_score IS NOT NULL;
"""
df_corr = pd.read_sql_query(query, conn)
correlation = df_corr['delivery_days'].corr(df_corr['review_score'])
print(f"Correlation between delivery days and review score: {round(correlation, 3)}")
Correlation between delivery days and review score: -0.334
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]: