用 Pandas 做一次完整的用户行为分析)
写在前面上一篇讲相关性这一篇做实战用户行为分析。这是数据分析岗最高频的题型之一面试会问、工作会用。好消息是它不需要复杂算法——留存、复购、活跃度这些指标本质都是「按用户分组 按时间分组 数人头」。用 Pandas 的groupby和pivot_table就能全部算出来。先说清楚本文数据是模拟的2000 个用户、5000 笔订单但口径和真实项目一致。你把数据换成自己的代码不用改。先确认示例数据在手边importosimportmkdataifnotos.path.exists(data/sales.csv):mkdata.main()print(示例数据就绪,os.path.exists(data/sales.csv))这段会检查第 1 篇生成的data/目录在不在不在就自动补一次已经有数据时只会打印一行示例数据就绪 True不影响后面的结果。一、造一份用户订单数据真实数据长这样一行一笔订单有用户 ID、下单时间、金额。这里用随机数模拟注意要让一部分用户重复下单否则复购率永远是 0。importosimportnumpyasnpimportpandasaspd rngnp.random.default_rng(11)n_users,n_orders2000,5000# 让一部分用户多下单按幂律权重抽用户weights1/(np.arange(1,n_users1)**0.8)weightsweights/weights.sum()usersrng.choice(np.arange(1,n_users1),n_orders,pweights)monthsrng.choice(pd.period_range(2024-01,2024-06,freqM),n_orders,p[0.28,0.22,0.18,0.13,0.11,0.08],)dfpd.DataFrame({用户ID:[fU{u:04d}foruinusers],月份:months.astype(str),金额:rng.gamma(2,180,n_orders).round(2),})os.makedirs(data,exist_okTrue)df.to_csv(data/user_orders.csv,indexFalse,encodingutf-8-sig)print(订单数,len(df))print(用户数,df[用户ID].nunique())print(时间范围,df[月份].min(),~,df[月份].max())print(df.head(5).to_string(indexFalse))订单数 5000 用户数 1385 时间范围 2024-01 ~ 2024-06 用户ID 月份 金额 U0005 2024-02 268.69 U0151 2024-01 452.95 U0288 2024-02 290.66 U0001 2024-04 1517.78 U0006 2024-01 245.99这里用「幂律权重」抽用户是为了让少数用户下很多单——真实业务里就是这样头部用户贡献大头这直接决定了后面「头部贡献」那个指标长什么样。二、四个基础指标先有人再有钱importpandasaspd dfpd.read_csv(data/user_orders.csv,encodingutf-8-sig)total_orderslen(df)total_usersdf[用户ID].nunique()total_amountdf[金额].sum()per_userdf.groupby(用户ID).agg(订单数(金额,size),金额(金额,sum))repeat_usersint((per_user[订单数]2).sum())print(f总订单{total_orders}笔 | 总用户{total_users}人 | 总金额{total_amount:,.2f}元)print(f人均订单{total_orders/total_users:.2f}笔 | 客单价{df[金额].mean():.2f}元)print(f复购用户{repeat_users}人复购率{repeat_users/total_users:.1%})print(----)print(订单数分布每个用户下了几单)print(per_user[订单数].value_counts().sort_index().to_string())总订单 5000 笔 | 总用户 1385 人 | 总金额 1,786,477.76 元 人均订单 3.61 笔 | 客单价 357.30 元 复购用户 806 人复购率 58.2% ---- 订单数分布每个用户下了几单 订单数 1 579 2 344 3 180 4 84 5 47 6 30 7 17 8 19 9 17 10 3 11 6 12 6 13 5 14 6 15 3 16 3 17 1 18 5 19 2 20 3 22 1 23 1 24 1 26 3 27 1 28 2 34 1 35 2 37 1 38 1 39 1 40 1 50 1 54 1 57 1 58 1 80 1 102 1 105 1 156 1 253 1口径陷阱复购率至少有两种算法——「下过 ≥2 单的用户数 / 总用户数」上面用的和「复购订单数 / 总订单数」。前者看用户质量后者看订单结构写报告时必须注明用的是哪种否则两个人会拿不同数字吵架。三、月活与新增按时间看盘子大小importpandasaspd dfpd.read_csv(data/user_orders.csv,encodingutf-8-sig)# 每个用户的首单月份 新增月份df[首单月]df.groupby(用户ID)[月份].transform(min)maudf.groupby(月份)[用户ID].nunique().rename(月活用户)new_usersdf.drop_duplicates(用户ID).groupby(首单月)[用户ID].nunique().rename(新增用户)summarypd.concat([mau,new_users],axis1).fillna(0).astype(int)summary[新增占比(%)](summary[新增用户]/summary[月活用户]*100).round(1)print(summary.to_string())print(----)print(月活峰值,summary[月活用户].idxmax(),summary[月活用户].max(),人)月活用户 新增用户 新增占比(%) 2024-01 724 724 100.0 2024-02 563 266 47.2 2024-03 488 159 32.6 2024-04 382 102 26.7 2024-05 346 85 24.6 2024-06 258 49 19.0 ---- 月活峰值 2024-01 724 人transform(min)是这里的关键它把每个用户的首单月份「贴回」每一行不打乱行数。换成groupby().min()会得到一张只有用户和月份的表再 merge 就多一步。四、留存矩阵同批用户后面还在不在留存是用户分析的灵魂。定义某月首单的用户在第 N 个月还下过单的比例。importnumpyasnpimportpandasaspd dfpd.read_csv(data/user_orders.csv,encodingutf-8-sig)df[首单月]df.groupby(用户ID)[月份].transform(min)monthssorted(df[月份].unique())retentionpd.DataFrame(indexmonths,columnsmonths,dtypefloat)forfirstinmonths:cohortdf[df[首单月]first]basecohort[用户ID].nunique()forminmonths:ifmonths.index(m)months.index(first):continue# 首单之前的月份留空alivecohort[cohort[月份]m][用户ID].nunique()retention.loc[first,m]round(alive/base*100,1)ifbaseelsenp.nanprint(留存矩阵%行首单月列该月是否仍活跃)print(retention.round(1).to_string())print(----)# 次月留存首单月的下一个月second[retention.loc[m,months[months.index(m)1]]forminmonths[:-1]ifmonths.index(m)1len(months)]print(平均次月留存,round(np.nanmean([sforsinsecondifss]),1),%)留存矩阵%行首单月列该月是否仍活跃 2024-01 2024-02 2024-03 2024-04 2024-05 2024-06 2024-01 100.0 41.0 34.8 27.9 22.7 19.9 2024-02 NaN 100.0 28.9 18.0 22.9 13.5 2024-03 NaN NaN 100.0 18.9 14.5 8.2 2024-04 NaN NaN NaN 100.0 12.7 11.8 2024-05 NaN NaN NaN NaN 100.0 4.7 2024-06 NaN NaN NaN NaN NaN 100.0 ---- 平均次月留存 21.2 %图 1首单月分组留存矩阵。左下半边是空白首单之前的月份没有意义越往右越淡就是留存随时间的自然衰减。看矩阵的方式每一行是一批用户对角线附近最亮首月 100%往右越来越淡说明留存随时间衰减。右上角是空白因为「首单在 6 月的用户」没有 7 月数据——这是留存矩阵常见的「三角缺失」别当成丢数据。五、头部用户贡献20% 的人贡献多少importpandasaspd dfpd.read_csv(data/user_orders.csv,encodingutf-8-sig)per_userdf.groupby(用户ID)[金额].sum().sort_values(ascendingFalse)totalper_user.sum()forpctin[0.05,0.10,0.20]:kmax(int(len(per_user)*pct),1)shareper_user.head(k).sum()/total*100print(fTop{pct:.0%}用户{k}人贡献{share:.1f}% 的金额)print(----)print(金额分位数元)print(per_user.quantile([0.5,0.9,0.99]).round(2).to_string())Top 5% 用户69 人贡献 40.9% 的金额 Top 10% 用户138 人贡献 51.4% 的金额 Top 20% 用户277 人贡献 64.7% 的金额 ---- 金额分位数元 0.50 649.55 0.90 2237.11 0.99 12040.60如果 Top 10% 贡献了 50% 以上说明业务对头部依赖重运营策略就该是「重点维护头部 拉新补长尾」如果分布很均匀则说明没有明显大客户适合走规模化投放。六、一手结论把数字翻译成人话importpandasaspd dfpd.read_csv(data/user_orders.csv,encodingutf-8-sig)per_userdf.groupby(用户ID).agg(订单数(金额,size),金额(金额,sum))total,usersdf[金额].sum(),df[用户ID].nunique()repeatint((per_user[订单数]2).sum())top10_shareper_user[金额].nlargest(max(int(len(per_user)*0.1),1)).sum()/total*100month_amtdf.groupby(月份)[金额].sum()print(*50)print(用户行为分析小结2024-01 ~ 2024-06)print(*50)print(f1. 平台累计{users}个用户、{len(df)}笔订单总金额{total:,.2f}元。)print(f2. 复购用户{repeat}人{repeat/users:.1%}人均{len(df)/users:.2f}单、{total/users:,.2f}元。)print(f3. 客单价{df[金额].mean():.2f}元金额最高的月份是{month_amt.idxmax()}{month_amt.max():,.2f}元。)print(f4. 头部 10% 用户贡献了{top10_share:.1f}% 的金额运营应重点维护。)print(*50)print(下一步建议对下过 2 单以上的用户做二次触达对首单未复购的用户发券召回。) 用户行为分析小结2024-01 ~ 2024-06 1. 平台累计 1385 个用户、5000 笔订单总金额 1,786,477.76 元。 2. 复购用户 806 人58.2%人均 3.61 单、1,289.88 元。 3. 客单价 357.30 元金额最高的月份是 2024-01523,672.34 元。 4. 头部 10% 用户贡献了 51.4% 的金额运营应重点维护。 下一步建议对下过 2 单以上的用户做二次触达对首单未复购的用户发券召回。这段 f-string 就是交付物所有数字都是算出来的没有一个是手写的数据一换重跑就对。常见问题Q1复购率算出来是 0。用户 ID 全是唯一值一人一单或者你按「订单 ID」分组建了表。复购必须按用户聚合先groupby(用户ID).size()看有没有人下单 ≥2 次。Q2留存矩阵里有一堆 NaN是不是数据缺失不是。是「首单在最近月份的用户还没来得及观察后续月份」属于结构性缺失报告里注明样本区间即可。Q3月活和新增加起来对不上月活是「当月下过单的去重人数」新增是「首单在当月的人数」新增的人可能当月就重复下单两者不是简单可加关系。别硬凑。小结用户行为分析的骨架是四步按用户聚合groupby(用户ID)拿到人均指标、按时间聚合月活、新增、按批次交叉留存矩阵用transform(min)打首单月、看结构头部贡献、分位数。指标定义比代码重要复购率、留存、客单价都有多种口径报告里写清楚比算得漂亮更有价值。下一篇换工具同样是把结果讲给人听这次用 Seaborn 把统计图做得更好看。