用pd.to_period('m')计算首月与行为月的整数月差,避免跨年错误;以cohort_month和month_diff为索引透视,reindex补齐列,div(cohort_size, axis=0)归一化;heatmap需mask=nan、vmin/vmax=0/1、fmt='.0%'及高dpi导出。

怎么用 pandas 计算每个 cohort 的月度留存率
核心是把用户首次行为时间(如注册日)归到“首月”,再按自然月追踪后续活跃。关键不是按绝对时间分组,而是按“距首月的月数”偏移——pandas.DateOffset 或 pd.to_period 都能做,但后者更稳,避免月末日期计算出错。
常见错误:直接用 df.groupby('user_id').date.min() 得到首日,再用 dt.month 减,结果跨年时出错(比如 2023-12 和 2024-01 相减得 -11)。正确做法是统一转成 Period:
- 先算每个用户的首月:
df.groupby('user_id')['date'].min().dt.to_period('M') - 再算每次行为所属月:
df['date'].dt.to_period('M') - 用
period - first_period得到整数月差(自动处理跨年)
如何构造 cohort 矩阵并填充留存率数值
矩阵行是 cohort(首月),列是“第 N 月”,值是该 cohort 在第 N 月仍有行为的用户占比。难点不在计算,在对齐维度——pandas 的 crosstab 不适合,容易漏掉全零列;推荐用 pivot_table + reindex 控制行列顺序。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 用
cohort_month和period_diff(上一步算出的月差)作为 pivot 的 index/columns -
aggfunc='nunique'统计每格的用户数,再除以 cohort 总人数(需先存好cohort_sizeSeries) - 务必用
reindex补齐缺失的月差列(比如某 cohort 只活到第 3 个月,但你要画到第 12 个月) - 最后用
div(cohort_size, axis=0)做逐行归一化
用 seaborn.heatmap 绘热力图时为什么颜色总不对
默认会把 NaN 当 0 处理,导致空白 cohort(比如没数据的右下角)被染成最浅色;另外,保留率是 0–1 小数,但 heatmap 默认按整数刻度分段,视觉上全挤在底部。
必须显式控制:
- 传入
mask=df.isna()把空值真正隐藏(不是靠背景色) - 加
vmin=0, vmax=1锁定色阶范围 - 用
annot=True, fmt='.0%'让数字显示为百分比(注意fmt是字符串格式,不是小数位数) - 如果 cohort 标签太长(如 '2023-01'),用
plt.xticks(rotation=45)调整,否则重叠
为什么导出的 PNG 文字模糊、行列标签错位
seaborn 默认分辨率低,且 plt.tight_layout() 对 heatmap 的坐标轴 label 支持不好,尤其当 cohort 名含短横线时易截断。
解决办法很实在:
- 绘图前加
plt.rcParams['savefig.dpi'] = 150提升输出质量 - 不用
tight_layout,改用plt.subplots_adjust(left=0.2, bottom=0.2)手动留白 - 热力图生成后,立刻调
ax.set_xlabel('Months Since Join')和ax.set_ylabel('Cohort Month'),避免依赖 seaborn 自动命名 - 保存时用
plt.savefig('cohort.png', bbox_inches='tight'),不然右边标签可能被切掉
真正麻烦的是 cohort 边界定义——比如“注册即算 cohort”,还是“完成首单才算”?这个逻辑一旦写错,后面全白算。别急着画图,先拿 10 个用户手算两遍首月和第 1 月留存,对得上再跑全量。










