python
import numpy as np
import pandas as pd
from multiprocessing import Pool
from functools import partial
def retention_curve_for_cohort(user_first_day, activity_days, cohort_day, max_day=30):
"""
Build a binary matrix: rows=users in cohort, cols=0..max_day indicating activity on day t (t days since first_day).
Inputs:
- user_first_day: Series indexed by user_id -> first_day (datetime or int)
- activity_days: DataFrame with columns ['user_id', 'activity_day'] (datetime or int)
- cohort_day: value matching first_day to select cohort
Returns:
- users: np.array of user_ids in cohort
- activity_matrix: boolean ndarray shape (n_users, max_day+1)
"""
users = user_first_day[user_first_day == cohort_day].index.values
if len(users) == 0:
return users, np.zeros((0, max_day+1), dtype=bool)
# compute days_since_first per activity
act = activity_days[activity_days['user_id'].isin(users)].copy()
# assume activity_day and user_first_day in same scale (e.g., integer day index)
act = act.merge(user_first_day.rename('first_day'), left_on='user_id', right_index=True)
act['day_since'] = act['activity_day'] - act['first_day']
act = act[(act['day_since'] >= 0) & (act['day_since'] <= max_day)]
# pivot to boolean matrix
idx_map = {u:i for i,u in enumerate(users)}
mat = np.zeros((len(users), max_day+1), dtype=bool)
for _, row in act[['user_id','day_since']].drop_duplicates().iterrows():
mat[idx_map[row['user_id']], int(row['day_since'])] = True
return users, mat
def bootstrap_retention(mat, B=1000, seed=0):
"""
mat: boolean ndarray (n_users, days)
returns: point_estimate (days,), ci_lower (days,), ci_upper (days,)
"""
n, d = mat.shape
if n == 0:
return np.zeros(d), np.zeros(d), np.zeros(d)
rng = np.random.default_rng(seed)
boot = np.empty((B, d), dtype=float)
for b in range(B):
# sample user indices with replacement
idx = rng.integers(0, n, n)
sample = mat[idx].any(axis=1) if False else mat[idx] # nothing special; mat[idx] shapes (n,d)
# retention per day = fraction of sampled users active that day
boot[b] = sample.sum(axis=0) / n
point = mat.sum(axis=0) / n
lower = np.percentile(boot, 2.5, axis=0)
upper = np.percentile(boot, 97.5, axis=0)
return point, lower, upper
def process_cohort(cohort_day, user_first_day, activity_days, max_day=30, B=1000, seed_base=0):
users, mat = retention_curve_for_cohort(user_first_day, activity_days, cohort_day, max_day)
return cohort_day, bootstrap_retention(mat, B=B, seed=seed_base + hash(cohort_day) % (2**31))
# Example orchestration for many cohorts with multiprocessing
def compute_all_cohorts(user_first_day, activity_days, cohorts, max_day=30, B=1000, n_jobs=8):
func = partial(process_cohort, user_first_day=user_first_day, activity_days=activity_days,
max_day=max_day, B=B, seed_base=42)
with Pool(n_jobs) as p:
results = p.map(func, cohorts)
# results: list of (cohort_day, (point, lower, upper))
return {c:res for c,res in results}