客户留存分析与流失预测系统实战:从留存口径到模型落地

📅 发布时间:2026/10/10 8:03:42
客户留存分析与流失预测系统实战:从留存口径到模型落地
简介这是一套面向计算机与人工智能相关专业学生及数据分析初学者的客户留存分析与流失预测实战项目适用于电信运营商、保险公司等需要提升客户留存率的业务场景。项目以生存分析模型刻画客户流失概率随时间变化的趋势并计算生命周期价值同时采用随机森林模型预测客户是否流失最终通过Flask Web应用完成模型部署与交互并提供可视化留存分析与预测结果展示。压缩包共62个文件约19.89MB包含49张png可视化图表、3个ipynb实验笔记、3个md说明文档、2个pkl模型文件以及py脚本、html模板、Procfile等部署配置覆盖从探索性分析、生存建模到预测部署的完整链路。已有163人学习下载适合用作毕业设计、课程作业或技能练手可帮助读者掌握生存分析与随机森林的建模思路、特征重要性解释及Web端部署方法。1. 客户留存分析与流失预测系统从“拍脑袋”到“算概率”的那道坎很多团队第一次认真做客户留存分析都是被老板一句话逼出来的“上个月又掉了两百个客户谁能告诉我为什么”这时候大家才发现后台只有订单流水没有留存口径更别提流失预测。客户留存分析与流失预测系统要解决的就是把“谁走了、为什么走、谁快走了”这三件事从运营的直觉变成可复现的数据流程。它适合两类人一类是手里有订单/行为数据、想搭一套能跑起来的留存看板的工程师另一类是已经会用 Python但不知道流失标签怎么定义、模型怎么评估的业务分析师。这套系统不是买个 BI 工具拖几个图就完事核心难点在标签定义和特征口径模型反而是最后一步。下面我按自己落地的顺序把口径、特征、建模、排错一条线讲清楚。2. 留存口径与流失标签先把“谁算流失”这件事吵明白2.1 留存率到底按天、按周还是按月算留存分析的第一步不是写代码是定口径。最常见的翻车现场是运营说“月留存 60%”数据说“只有 35%”两边都没错只是口径不同。留存率的分母是“某段时间内首次产生行为的客户数”分子是“这批客户在之后第 N 个周期仍然有行为的数量”。周期选天、周、月直接决定数字大小。我一般会先跟业务确认三件事行为定义是什么下单、登录还是咨询、周期粒度是什么快消品看周SaaS 看月、观察窗口多长至少覆盖一个完整复购周期。这三件事没定后面所有图表都是玄学。import pandas as pd # 假设 orders 有 customer_id, order_date 两列 orders pd.read_csv(orders.csv, parse_dates[order_date]) # 1. 找到每个客户的首次下单日期作为留存起点 first_order orders.groupby(customer_id)[order_date].min().rename(first_date) orders orders.join(first_order, oncustomer_id) # 2. 计算每笔订单距离首次下单的周期数这里按周 orders[period] ((orders[order_date] - orders[first_date]).dt.days // 7) # 3. 按首次下单周分组统计每个周期内有行为的客户数 cohort orders.groupby([first_date, period])[customer_id].nunique().reset_index() cohort_pivot cohort.pivot(indexfirst_date, columnsperiod, valuescustomer_id) # 4. 除以首周人数得到留存率 retention cohort_pivot.div(cohort_pivot[0], axis0) print(retention.head())这段代码的逻辑是“同期群cohort分析”先锁定每个客户的首次行为时间再看他后续每个周期是否回来。period用整除天数得到按周就是//7按月可以换成dt.to_period(M)再做差。cohort_pivot[0]是首周人数作为分母。参数上唯一要调的是周期长度和观察窗口窗口太短会低估留存太长会引入太多未成熟数据。提示首次行为日期一定要用“业务意义上的首次”比如退款订单要不要剔除、测试账号要不要过滤这些不处理干净留存曲线会莫名其妙偏高。2.2 流失标签怎么定义才不误导模型留存是群体视角流失预测是个体视角。要给每个客户打一个“是否流失”的标签常见做法是“观察窗口 表现窗口”在某个时间点 T看客户过去一段时间的行为作为特征再看 T 之后一段时间内是否还有行为没有就标为流失。这里最大的坑是“标签泄漏”。比如你用 T 之后的数据去算特征模型准确率能到 99%上线就废。正确做法是特征只取 T 之前标签取 T 之后中间留一个缓冲期避免边界模糊。# 设定观察点 T特征窗口 90 天表现窗口 30 天缓冲 7 天 T pd.Timestamp(2024-06-01) feat_start T - pd.Timedelta(days90) label_start T pd.Timedelta(days7) label_end label_start pd.Timedelta(days30) # 特征T 之前 90 天内的行为 feat orders[(orders[order_date] feat_start) (orders[order_date] T)] # 标签缓冲期后 30 天内是否有行为 future orders[(orders[order_date] label_start) (orders[order_date] label_end)] churn_ids set(feat[customer_id]) - set(future[customer_id]) feat[is_churn] feat[customer_id].isin(churn_ids).astype(int)feat_start和T之间是特征窗口label_start到label_end是表现窗口中间 7 天缓冲是为了避免“刚好在 T 附近下单”的客户被误判。is_churn为 1 表示在表现窗口内没有回来。参数上特征窗口一般取 2 到 3 个复购周期表现窗口取 1 个复购周期缓冲期取 3 到 7 天。这套口径定下来后面特征工程才有意义。注意流失标签一旦定义整个项目周期内不要随意改。改一次标签模型、报表、运营策略全要重来这是血泪经验。3. 特征工程把订单流水变成模型能吃的 RFM 与行为特征3.1 RFM 三个维度的计算与分箱RFM 是留存分析里最稳的起步特征Recency最近一次消费距今天数、Frequency消费频次、Monetary消费金额。它不依赖复杂行为埋点有订单表就能算。但直接拿原始值喂模型效果一般通常要做分箱或对数变换。snapshot T # 观察点 rfm feat.groupby(customer_id).agg( recency(order_date, lambda x: (snapshot - x.max()).days), frequency(order_date, count), monetary(amount, sum) ).reset_index() # 对 frequency 和 monetary 做对数变换缓解长尾 import numpy as np rfm[frequency_log] np.log1p(rfm[frequency]) rfm[monetary_log] np.log1p(rfm[monetary]) # recency 分箱越近分数越高 rfm[r_score] pd.qcut(rfm[recency], 5, labels[5,4,3,2,1]).astype(int)recency用观察点减去最近一次下单日期单位是天。frequency和monetary直接聚合但金额和频次往往长尾严重log1p是常用平滑手段。qcut做等频分箱把 recency 分成 5 档越近分数越高。参数上分箱数一般 4 到 5 档太多会导致每档样本太少。3.2 行为特征与趋势特征怎么加光有 RFM 不够流失往往有前兆下单间隔变长、客单价下降、咨询变少。这些趋势特征比静态值更有预测力。常见做法是算“最近 30 天 vs 前 30 天”的变化率。# 最近30天与前30天的订单数变化 recent feat[feat[order_date] T - pd.Timedelta(days30)] prev feat[(feat[order_date] T - pd.Timedelta(days60)) (feat[order_date] T - pd.Timedelta(days30))] recent_cnt recent.groupby(customer_id).size().rename(recent_cnt) prev_cnt prev.groupby(customer_id).size().rename(prev_cnt) trend pd.concat([recent_cnt, prev_cnt], axis1).fillna(0) trend[cnt_trend] (trend[recent_cnt] - trend[prev_cnt]) / (trend[prev_cnt] 1)cnt_trend为正说明最近更活跃为负说明在降温。分母加 1 是防止除零。这个特征在树模型里往往能排进重要性前五。参数上窗口可以按业务调整快消品用 7 天SaaS 用 30 天。提示趋势特征一定要保证两个窗口长度一致否则变化率没有可比性这是很多人忽略的细节。4. 流失预测建模从逻辑回归到树模型的选型与评估4.1 为什么先跑逻辑回归再上 XGBoost很多人一上来就 XGBoost结果特征没调好模型过拟合还不如逻辑回归。我的习惯是先跑一个逻辑回归做基线它的系数可解释能快速验证特征方向对不对。如果逻辑回归 AUC 能到 0.7 以上再上树模型提升。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score from sklearn.preprocessing import StandardScaler features [recency, frequency_log, monetary_log, cnt_trend] X rfm[features].fillna(0) y rfm[is_churn] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, stratifyy, random_state42) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) lr LogisticRegression(class_weightbalanced, max_iter1000) lr.fit(X_train_s, y_train) print(AUC:, roc_auc_score(y_test, lr.predict_proba(X_test_s)[:,1]))class_weightbalanced是因为流失样本通常远少于留存样本不加这个参数模型会偏向多数类。stratifyy保证训练测试集流失比例一致。AUC 是流失预测最常用的指标因为它不受阈值影响。参数上max_iter调到 1000 是为了避免不收敛警告。4.2 XGBoost 调参与阈值选择树模型能自动捕捉非线性关系但参数多。我一般先固定n_estimators200、max_depth4再调learning_rate和scale_pos_weight。scale_pos_weight设为负样本数除以正样本数处理不平衡。import xgboost as xgb scale (y_train 0).sum() / (y_train 1).sum() model xgb.XGBClassifier( n_estimators200, max_depth4, learning_rate0.05, scale_pos_weightscale, eval_metricauc, random_state42 ) model.fit(X_train, y_train) proba model.predict_proba(X_test)[:,1] print(AUC:, roc_auc_score(y_test, proba))max_depth4是防止过拟合的常用值再深容易记住噪声。learning_rate0.05配合 200 棵树是比较稳的组合。阈值不要用默认 0.5而是根据业务成本选如果触达成本低阈值可以降到 0.3 多捞人如果成本高阈值提到 0.6 只打高概率。注意AUC 高不代表业务好用一定要看 top 10% 高风险客户的命中率这才是运营真正会用的部分。5. 避坑与排查留存预测项目里最容易翻车的 4 件事5.1 现象留存曲线首周之后突然反弹原因首次行为日期被后续订单覆盖或者退款订单没剔除导致同一客户被算进多个 cohort。解决在计算first_date前先过滤无效订单并确认groupby().min()只取一次。5.2 现象模型 AUC 0.95上线后运营说“推的人根本不像要走的”原因标签泄漏特征里混入了表现窗口的数据或者用了未来才知道的字段。解决严格按时间切分特征窗口和标签窗口之间留缓冲所有特征必须能在预测时刻拿到。5.3 现象高风险名单里全是新客原因新客天然 recency 小、frequency 低模型把“新”误判成“要流失”。解决把客户按生命周期分层新客单独建模或者加入“注册天数”作为特征让模型区分。5.4 现象每次跑出来的流失名单都不一样原因随机种子没固定或者数据快照时间不统一。解决random_state固定观察点 T 写死在配置里每次跑用同一份快照数据。提示这四条我几乎每个项目都会遇到至少两条提前在流程里加校验能省很多返工。6. 把模型变成运营动作打分、分层与效果回测的一个具体技巧模型跑出概率只是半成品真正值钱的是把概率变成可执行的名单。我常用的技巧是“概率分桶 业务规则叠加”先按预测概率把客户分成高、中、低三档再在高档里叠加一个简单规则比如“近 30 天有咨询但未下单”这样捞出来的人既有模型依据又有业务解释运营接受度高得多。# 给全量客户打分 rfm[churn_proba] model.predict_proba(rfm[features].fillna(0))[:,1] # 按分位数分三档 rfm[risk_level] pd.qcut(rfm[churn_proba], 3, labels[低, 中, 高]) # 高风险的优先名单高风险 近30天有咨询 high_risk rfm[rfm[risk_level] 高].merge( recent.groupby(customer_id).size().rename(recent_orders), oncustomer_id, howleft ).fillna({recent_orders: 0}) priority high_risk[high_risk[recent_orders] 0]qcut按概率分位数分档保证每档人数均衡。priority是高风险且有近期行为的客户这类人最值得优先触达。参数上分档数可以按运营人力调整人少就只取 top 10%。效果回测是很多人跳过的一步但它是唯一能证明模型有用的环节。做法是把 priority 名单随机分成实验组和对照组实验组触达对照组不触达两周后对比两组的留存率差异。如果实验组留存率显著更高模型就值得继续投入如果没有差异要么特征不够要么触达方式不对。我自己踩过最大的坑是模型 AUC 很漂亮就直接全量推给运营结果运营按名单打了一周电话转化没变化后来才发现名单里一半是已经自然流失的沉默客户触达成本全浪费了。从那以后我养成了一个习惯任何模型上线前先拿历史数据做一次“如果当时按这个名单触达能捞回多少人”的回测数字对不上就不推。希望帮到你。本文还有配套的精品资源点击获取