Spaces:
Sleeping
Sleeping
| """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 | |