Approach: treat each cell as a binomial proportion (x successes / n cohort size at day 0 or cohort initial users). Compute a 95% pointwise CI per cell using a binomial proportion interval (I recommend Wilson interval for better coverage than normal approx). If you only have rates, you must recover or be provided the denominator (cohort sizes and number retained at each day). Then plot cohort curves with matplotlib/seaborn and use fill_between for the confidence bands.python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.stats.proportion import proportion_confint
def plot_cohort_retention(retention_rates_df, counts_df, alpha=0.05, figsize=(10,6)):
"""
retention_rates_df: DataFrame, index=cohort_label, columns=day offsets, values=rate (0..1)
counts_df: DataFrame same shape, values = number retained at that day (integers)
Alternatively, counts_df could be initial cohort sizes for day0 and retained counts per day.
"""
sns.set_style("whitegrid")
fig, ax = plt.subplots(figsize=figsize)
days = retention_rates_df.columns.astype(int)
for cohort in retention_rates_df.index:
rates = retention_rates_df.loc[cohort].astype(float).values
successes = counts_df.loc[cohort].astype(int).values
# need denominators: use successes / rates -> avoid divide by zero; prefer explicit denom_df if available
with np.errstate(divide='ignore', invalid='ignore'):
denom = np.where(rates>0, (successes / rates), np.nan)
# fallback: if denom constant (cohort size) provided in counts_df_day0
if np.isnan(denom).all():
denom = np.full_like(successes, counts_df.loc[cohort, days[0]])
lower = np.empty_like(rates); upper = np.empty_like(rates)
for i, (x, n, p) in enumerate(zip(successes, denom, rates)):
if np.isnan(p) or n==0:
lower[i], upper[i] = np.nan, np.nan
else:
lo, hi = proportion_confint(count=int(round(x)), n=int(round(n)), alpha=alpha, method='wilson')
lower[i], upper[i] = lo, hi
ax.plot(days, rates, label=str(cohort))
ax.fill_between(days, lower, upper, alpha=0.2)
ax.set_xlabel("Day offset")
ax.set_ylabel("Retention rate")
ax.set_ylim(0, 1)
ax.legend(title="Cohort")
plt.show()
Key points / reasoning:- Use binomial proportion CIs per cell because retention is a fraction of users still active. Wilson intervals are robust for small samples and edge probabilities.- You must know counts (successes and denominators). If only rates are available, request cohort sizes or retained counts.- Plot each cohort line and a semi-transparent fill_between using the lower/upper bounds.Edge cases:- Zero denominators, NaNs, tiny n (CI will be wide) — handle or mask.- Multiple-testing: these are pointwise CIs, not simultaneous bands across days. For simultaneous confidence regions, use more advanced methods (Bonferroni / bootstrap across entire curves).