Amit-kr26's picture
HF Spaces deployment
c9f187d
Raw
History Blame Contribute Delete
4.11 kB
"""Cohort retention analytics queries."""
import pandas as pd
from sqlalchemy import Engine, text
def cohort_retention(engine: Engine) -> tuple[pd.DataFrame, pd.DataFrame]:
"""
Compute monthly cohort retention rates.
Returns
-------
retention_pivot : pd.DataFrame
Pivoted table with cohort months as index, month offsets (0, 1, 2, …)
as columns, and retention percentages as values.
cohort_sizes : pd.DataFrame
Cohort month and size (number of unique customers in each acquisition cohort).
"""
# NOTE: In the Olist dataset, customer_id (and therefore customer_key) is
# unique per ORDER, not per customer. customer_unique_id is the true
# cross-order customer identifier. We must join through dim_customers to
# get customer_unique_id before computing cohort membership.
sql = text("""
WITH first_purchase AS (
SELECT
dc.customer_unique_id,
strftime('%Y-%m-01', MIN(f.order_date_id)) AS cohort_month
FROM fact_orders f
JOIN dim_customers dc ON f.customer_key = dc.customer_key
WHERE f.order_status NOT IN ('canceled', 'unavailable')
GROUP BY dc.customer_unique_id
),
monthly_active AS (
SELECT DISTINCT
dc.customer_unique_id,
strftime('%Y-%m-01', f.order_date_id) AS order_month
FROM fact_orders f
JOIN dim_customers dc ON f.customer_key = dc.customer_key
WHERE f.order_status NOT IN ('canceled', 'unavailable')
),
cohort_activity AS (
SELECT
fp.cohort_month,
CAST(
(CAST(strftime('%Y', ma.order_month) AS INTEGER) - CAST(strftime('%Y', fp.cohort_month) AS INTEGER)) * 12 +
(CAST(strftime('%m', ma.order_month) AS INTEGER) - CAST(strftime('%m', fp.cohort_month) AS INTEGER))
AS INTEGER) AS month_offset,
COUNT(DISTINCT ma.customer_unique_id) AS active_customers
FROM first_purchase fp
JOIN monthly_active ma ON fp.customer_unique_id = ma.customer_unique_id
WHERE ma.order_month >= fp.cohort_month
GROUP BY fp.cohort_month, month_offset
),
cohort_sizes AS (
SELECT cohort_month, COUNT(DISTINCT customer_unique_id) AS cohort_size
FROM first_purchase
GROUP BY cohort_month
)
SELECT
ca.cohort_month,
ca.month_offset,
ROUND(100.0 * ca.active_customers / cs.cohort_size, 1) AS retention_pct,
cs.cohort_size
FROM cohort_activity ca
JOIN cohort_sizes cs ON ca.cohort_month = cs.cohort_month
ORDER BY ca.cohort_month, ca.month_offset
""")
with engine.connect() as conn:
df = pd.read_sql(sql, conn)
df["cohort_month"] = pd.to_datetime(df["cohort_month"]).dt.strftime("%Y-%m")
# Drop tiny cohorts β€” statistically meaningless (e.g. 2016-09 has 2 customers,
# 2016-12 has 1, so any single return shows as 50–100 % retention).
MIN_COHORT_SIZE = 100
valid_cohorts = df.loc[df["cohort_size"] >= MIN_COHORT_SIZE, "cohort_month"].unique()
df = df[df["cohort_month"].isin(valid_cohorts)]
# Pivot: rows = cohort month, columns = month offset
retention_pivot = df.pivot(
index="cohort_month", columns="month_offset", values="retention_pct"
)
retention_pivot.columns = [f"Month {c}" for c in retention_pivot.columns]
retention_pivot.index.name = "Cohort"
# Drop Month 0 β€” always 100 % by definition (first purchase IS the acquisition
# event), so it adds no information and compresses the colorscale for months 1+.
retention_pivot = retention_pivot.drop(columns=["Month 0"], errors="ignore")
cohort_sizes = (
df[["cohort_month", "cohort_size"]]
.drop_duplicates()
.rename(columns={"cohort_month": "Cohort", "cohort_size": "Customers"})
.reset_index(drop=True)
)
return retention_pivot, cohort_sizes