尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

客户留存分析与流失预测系统实战:从特征工程到模型部署

客户留存分析与流失预测系统实战:从特征工程到模型部署 简介这套客户留存分析与流失预测项目包面向电信、保险等行业的客户分析场景也适合人工智能、计算机科学等专业用于毕业设计或课程作业。方案基于生存分析模型刻画客户流失随时间变化的趋势并计算生命周期价值同时使用随机森林模型预测流失概率通过Flask Web应用完成交互式部署与结果展示。压缩包共62个文件容量19.89MB主体包括3个Jupyter Notebook数据探索、生存分析、流失预测建模、1个Python应用入口、1个前端HTML模板以及49张结果可视化图片如生存曲线、特征重要性、SHAP解释、部分依赖图等另有训练好的模型pkl文件、依赖清单和说明文档。已有163人学习下载。下载后可按README指引快速复现流程直观获取完整代码、图表与模型文件便于对照学习客户生命周期价值计算、特征解释及模型部署等关键环节。1. 客户留存分析与流失预测系统先想清楚拿到zip之后要干什么拿到客户留存分析与流失预测系统.zip的那个下午我以为是解压就能跑的现成工具结果从理解业务到真正用它做出判断花了两周。这套系统要解决的问题有两层一层是留存分析——用户来了之后有多少留到今天有多少在第几天离开另一层是流失预测——在用户彻底离开之前用一个概率分圈出高流失风险名单交给运营去触达。它适合的不是算法团队而是手里有用户行为数据、想用低成本方式建一套留存监控与预警流程的运营或开发。下面讲的是我在落地这套系统时的完整路径包括数据清洗、建模、打包交付和最常见的坑。2. 数据清洗与特征工程留存分析不是跑个模型的事2.1 用户行为数据长什么样从订单表到用户日活表很多团队的数据现状是订单表、注册表、登录日志各存各的甚至取数的口径都要问三个部门。做留存分析之前第一步不是写模型而是把数据在用户维度上拉平。常见的做法是以 user_id 为主键把订单表和日志表聚合成一张宽表每一行是一个用户字段是“最近一次下单距今多少天”、“累计订单金额”、“最近7天活跃天数”等聚合值。宽表的产出质量决定了后面模型的上限。先别急着造高阶特征先用 SQL 或 pandas 跑一遍留存率验证数据能和业务直觉对上。比如你定义“第7日留存率”为某天新增用户中注册后第7天仍有活跃行为的占比。这里的活跃行为是“登录”还是“下单”要和业务方确认口径不同数字能差出一倍。我一般会先按天算一张留存矩阵确认没有明显的周末效应或数据断档再继续造特征。数据样例可以这样理解订单表有三个字段 user_id、order_id、order_time、amount行为日志表有 user_id、log_time、action。你的目标是把这些变成每用户一行。如果日志表特别大千万级以上别在 pandas 里硬做 groupby先在数据库或数仓里用 SQL 做预聚合只把用户维度的统计结果取回来。这不是优化建议是血泪经验。2.2 留存分析的三个核心特征RFM、使用频次与沉默期RFM 是客户分析里的老框架Recency最近一次消费距今多久、Frequency消费频次、Monetary消费金额。在流失预测里R 的权重通常最高因为沉默的定义本身就依赖“最近一次活跃距今多久”。F 和 M 则能区分两类用户高频低客单的用户和低频高客单的用户它们的流失信号完全不一样前者可能只是最近没下单后者可能是彻底换供应商了。沉默期怎么定不要拍脑袋。常见做法是统计全体用户相邻两次活跃的间隔天数取90分位数。比如你算出来90%的间隔都小于15天那“15天没有活跃”就可以作为沉默阈值。这个阈值直接用来打标签在历史数据里如果一个用户最后一次活跃距离预测时点超过了15天就标记为流失。这个标签定义是后续所有模型的地基得先把它写进配置中心而不是散落在脚本里。使用频次特征比金额特征要稳定。我个人的做法是先按自然周统计每周活跃天数再做一个28天滑动窗口的加权和时间越近权重越高。权重可以用指数衰减weight 0.95 ** (days_diff)。写成代码并不复杂但收益明显因为用户的近期行为模式比累计值更能反映当下的状态。注意所有特征都必须基于预测时点之前的数据这个“时点”是最后一道红线后面避坑章会专门讲。2.3 用Python把原始表转成训练集完整代码与参数说明下面这个脚本是聚合训练集的最小实现。假设你有 users.csv 和 actions.csv目标是生成一个宽表包含 user_id、last_active_days、recent_days、recent_count、total_amount。为了后续做时间切分代码里显式引入 prediction_date。import pandas as pd import numpy as np # 读数据注意解析事件时间 users pd.read_csv(users.csv, parse_dates[reg_date]) actions pd.read_csv(actions.csv, parse_dates[log_time]) prediction_date 2024-06-01 # 预测时点用字符串方便改 # 特征1最近一次活跃距今天数 last_active ( actions.groupby(user_id)[log_time] .max() .reset_index() .rename(columns{log_time: last_active_time}) ) last_active[last_active_days] ( pd.Timestamp(prediction_date) - last_active[last_active_time] ).dt.days # 特征2/3近30天活跃天数和行为次数只统计预测时点之前的30天 window_start pd.Timestamp(prediction_date) - pd.DateOffset(days30) mask (actions[log_time] window_start) (actions[log_time] pd.Timestamp(prediction_date)) recent actions[mask].groupby(user_id).agg( recent_days(log_time, lambda x: (x.max() - x.min()).days 1), recent_count(log_time, size), ).reset_index() # 特征4累计金额 amount_agg actions.groupby(user_id)[amount].sum().rename(total_amount).reset_index() # 合并成宽表 feat users[[user_id]].merge(last_active[[user_id, last_active_days]], onuser_id, howleft) feat feat.merge(recent, onuser_id, howleft) feat feat.merge(amount_agg, onuser_id, howleft) # 没有行为记录的用户用999标记沉默时间其他缺失填0 feat[last_active_days] feat[last_active_days].fillna(999) feat[recent_days] feat[recent_days].fillna(0) feat[recent_count] feat[recent_count].fillna(0) feat[total_amount] feat[total_amount].fillna(0) print(feat.head())代码逻辑分成三步先按 groupby 把最近一次活跃时间取出来再拿时间差得到 last_active_days第二步用条件过滤得到近30天行为数据用 agg 聚合第三步把各聚合结果在 user_id 上合并。注意 recent_days 这里用的是近似“首尾活跃时间跨度加一”如果用户在这个窗口内活跃不连续它会比真实活跃天数偏大。更精确的做法是 x.dt.date.nunique()但那样在千万级数据上会慢很多看你的数据量取舍。参数说明prediction_date 是整套系统的锚点训练时要把锚点滚动到每个历史月份用来生成不同月份的样本。last_active_days 用999填充是因为这些用户没有任何活跃记录他们不是“沉默”而是“从未激活”应当被模型识别为特殊群体。recent_days 和 recent_count 的缺失填0代表观察窗口内没有活跃行为这个0是真实事件不是缺失。真实业务中你可能还要加更多特征注册天数、平均消费间隔、商品类目偏好、客服反馈次数。我给的原则是先把 RFM 和沉默期做成可运行版本再逐步加特征每加一个就用时间切分验证 AUC 是否真的提升而不是一味堆特征。这个系统里的特征工程不是一次性的它要能跟着业务周期持续迭代。提示千万不要在造特征时混入预测时点之后的数据比如用“这个用户后面有没有下单”来填充近期行为。这是最常见的特征泄漏后面避坑章有一条专门讲它。3. 流失预测模型选择算法与调参的实战思路3.1 为什么先跑逻辑回归和XGBoost做基线模型选择上我见过最常翻车的不是模型不够强而是上来就调 LightGBM 的黑匣子最后解释不了业务方的问题“为什么他分高”。所以建议先跑逻辑回归它的系数能直接解释特征方向。比如 last_active_days 的系数是正说明最近一次活跃距今越久流失概率越高符合直觉。逻辑回归作为基线也能给出一个 AUC如果 XGBoost 比它高不了多少那说明特征工程才是瓶颈。XGBoost 适合表格数据对特征缩放不敏感还能自动处理缺失值。我们用它做主力模型但注意树的深度、叶子数和正则参数会直接影响过拟合。数据量只有几千到几万条的留存场景太深的树几乎必定过拟合。常见初始参数是 max_depth3、learning_rate0.05、n_estimators300然后用早停法选迭代轮数。这里有一个容易被忽略的点流失场景的正样本往往很少。一个月流失10%用户听起来不少但在特征空间里树模型很容易通过切分把这些样本捡出来然后在验证集上表现得很好等上线遇到分布变化就失效。因此基线模型不只是看 AUC还要看它预测出的概率分布是否合理。我常用的办法是把预测概率分桶看每个桶里的真实流失率是否单调递增如果不是单调说明模型学到了噪声或者特征有泄漏。3.2 流失阈值怎么定基于业务沉默天数而非模型输出很多系统的预测结果是一堆0到1的概率但业务方问“那到底谁流失了”你如果回答“大于0.5的流失”这个阈值会让运营抓狂。正确做法是先定沉默期再根据沉默期生成历史标签模型只是学“哪些特征能预测这种沉默行为”。所以模型输出的 score 其实是“在沉默期定义下该用户未来7天内进入沉默的概率”而不是绝对的流失概率。阈值的选择取决于你的运营资源资源多阈值调低多捞人资源少阈值调高只保重点。具体怎么定阈值用验证集画出精确率-召回率曲线运营能接受的精确率比如30%就选对应的阈值。这里的精确率意思是“预测流失的人里真的流失了多少”召回率是“真流失的人里被预测到了多少”。客户留存系统的目标通常不是精确率最大化而是在保证一定精确率的前提下提高召回率因为漏掉一个高价值用户的损失远大于打扰一个普通用户。如果每个用户的贡献金额已知可以把阈值做成代价敏感形式。比如一个高价值用户流失一个月损失1000元一次无效触达成本5元。那么只有当预计流失概率 × 1000 5 时才值得触达算出的阈值为0.5%。这个计算比拍脑袋强但前提是你有用户贡献金额数据并且愿意维护这组系数。我把这个逻辑写进配置每次预测后直接输出“建议触达”和“可以再等等”两个名单。3.3 训练与评估代码P/R曲线、AUC与代价矩阵下面的代码展示了用逻辑回归和 XGBoost 训练并比较的流程。假设你已经有了 feat 和 labellabel 是1表示该用户在未来一个周期内沉默。import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score, precision_recall_curve, precision_score, recall_score # 准备数据假设 feat 有 user_id 和特征列label 单独一列 X feat.drop([user_id], axis1) y feat[label] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 逻辑回归基线 lr LogisticRegression(max_iter1000, C1.0, class_weightbalanced) lr.fit(X_train, y_train) y_prob_lr lr.predict_proba(X_test)[:, 1] print(LR AUC:, roc_auc_score(y_test, y_prob_lr)) # XGBoost 主力模型 xgb XGBClassifier( max_depth3, learning_rate0.05, n_estimators500, subsample0.8, colsample_bytree0.8, scale_pos_weightsum(y_train 0) / sum(y_train 1), eval_metriclogloss, early_stopping_rounds50, verbosity0, ) xgb.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) y_prob_xgb xgb.predict_proba(X_test)[:, 1] print(XGB AUC:, roc_auc_score(y_test, y_prob_xgb)) # 根据运营目标选阈值 precision, recall, thresholds precision_recall_curve(y_test, y_prob_xgb) # 找精确率超过30%且召回率最高的阈值 valid [(t, p, r) for t, p, r in zip(thresholds, precision[:-1], recall[:-1]) if p 0.3] if valid: best max(valid, keylambda x: x[2]) print(建议阈值:, best[0], 精确率:, best[1], 召回率:, best[2])参数说明max_depth3 是为了控制模型复杂度当前量级的数据不需要太深。learning_rate0.05 配合 n_estimators500让模型一步一步走不容易在训练集上飙升。subsample 和 colsample_bytree 都取0.8相当于每棵树只用80%的样本和特征增加随机性减小方差。scale_pos_weight 写成正负样本比例的倒数让少数类被漏掉时损失更大。early_stopping_rounds50 在验证集 logloss 不再下降时提前停止避免过拟合。逻辑回归的 C1.0 是正则强度的倒数越小正则越强。如果特征之间存在共线性比如 recent_count 和 total_amount 高度相关可以调小 C 或先做标准化否则系数的绝对值大但方向不稳定。class_weightbalanced 会自动调整正负样本权重但代价是预测概率会出现整体偏移阈值也需要重新校准所以我不建议在最终交付时用这个参数只把它作为基线检查用。评估部分AUC 看整体排序能力但真正决定业务动作的是精确率和召回率的取舍。我给的这个阈值搜索逻辑是“运营只能跟进精确率30%以上的客户在这个前提下挑召回率最高的点”这是一个可落地的折中。实际你还要看每一档阈值下会捞到多少人、多少奖励金额这需要结合客户分群下一章会讲。4. 把系统打包成zip从训练脚本到可交付的预测工具4.1 目录结构设计模型文件、配置、增量更新脚本交付不是把几个 .py 丢给对方就行。一个能跑起来的客户留存分析与流失预测系统至少要有训练脚本、预测脚本、配置文件、数据样例、模型输出目录和 README。我通常按下面这样的目录组织retention_system/ ├── config.yaml ├── train.py ├── predict.py ├── data/ │ ├── users.csv │ └── actions.csv ├── model/ │ └── xgb_model.json └── README.md配置文件里写数据路径、预测时点、沉默阈值、模型参数、特征列表。这样换一批数据跑的时候不用改代码。训练脚本读配置产出的模型放在 model/ 下。预测脚本读配置和模型输入当天的特征表输出带 churn_score 的用户名单。config.yaml 的样式我一般这样写data: users_path: data/users.csv actions_path: data/actions.csv prediction: date: 2024-06-01 silent_days: 15 model: type: xgboost params: max_depth: 3 learning_rate: 0.05 n_estimators: 500 subsample: 0.8 colsample_bytree: 0.8 output: score_path: output/churn_scores.csv为什么用 YAML 而不是命令行参数因为运营同事要改的是数据路径和阈值不该改代码。把参数从代码里剥离出来这个系统才谈得上可维护。README 里除了安装命令还要写清楚 Python 版本和依赖我建议用 requirements.txt并告诉对方用虚拟环境。别假设对方会用 conda直接把命令写出来。4.2 用zipfile和命令行实现一键打包把整个文件夹打成 zip 是交付前最后一步也是需求量极高的动作。在 Linux 上最省事的命令是zip -r retention_system.zip retention_system/如果需要排除体积大的缓存目录比如 data/raw/用zip -r retention_system.zip retention_system/ -x retention_system/data/raw/* -x *.pyc-x 参数接的是通配符排除规则*.pyc 能去掉 Python 缓存文件这个一定要加否则对方解压后会看到一堆pycache显得很不专业。如果输出包要带日期可以这样命名retention_system_20250611.zip方便后续回溯。如果你希望打包过程可重复我建议写成脚本尤其是当模型文件越来越多的时候import zipfile import os def zip_dir(dir_path, zip_path, excludes): with zipfile.ZipFile(zip_path, w, zipfile.ZIP_DEFLATED) as zf: for root, dirs, files in os.walk(dir_path): for f in files: full os.path.join(root, f) rel os.path.relpath(full, dir_path) if any(ex in rel for ex in excludes): continue zf.write(full, rel) if __name__ __main__: zip_dir( retention_system, retention_system_20250611.zip, excludes[__pycache__, .pyc, /data/raw/] )这段脚本用 os.walk 遍历目录把文件写入 zipZIP_DEFLATED 表示压缩。excludes 里的子串如果出现在相对路径中就跳过。这样每次交付跑一遍python package.py就能得到新包不会漏文件。打包之前先在干净的临时目录里跑一遍 train.py确认数据路径不是硬编码的绝对路径我见过太多次本地能跑、zip 包解压到别处就报 FileNotFoundError 的情况原因就是代码里写了C:/Users/xxx/data/actions.csv。路径要写成相对于配置文件的相对路径或者运行时自动获取脚本所在目录。4.3 交付后如何运行解压即用的最小命令与验证交付后对方第一步是解压。如果对方拿到的包是加密的先要移除 zip 密码通常做法是问打包人要密码别去用什么暴力破解工具那是给自己找坑。解压后按 README 跑python -m venv .venv source .venv/bin/activate pip install -r requirements.txt python train.py --config config.yaml python predict.py --config config.yaml --score output/churn_scores.csv预测脚本里要加一个 --score 参数让结果输出到指定路径。第一次运行完验证三步训练日志里有没有 AUC 指标预测输出里 churn_score 是不是0到1之间的分布随机抽一个明显很久没活跃的用户看他的 score 是否偏高。如果这几步都正常系统才算真正跑通。我还会给一个最小验证清单像这样检查项命令/操作期望结果训练能跑通python train.py --config config.yaml输出 AUC 和模型文件预测能跑通python predict.py --score output/churn_scores.csv输出 CSV 且不少于100行概率分布合理查看 score 列大部分在0.1~0.5之间少量接近1沉默用户命中抽查 15 天未活跃的用户score 排名靠前跨平台注意点Windows 上压缩的 zip 在 Linux 解压后中文文件名容易乱码所以项目文件和 README 最好都用英文命名内容可以中文。另外 zip 包里的换行符也会在跨平台时出问题代码文件尽量用 LF 换行这个可以在打包前用 dos2unix 统一一下。如果对方要在 Windows 上跑记得把 .venv 相关命令换成venv\Scripts\activateREADME 写两个版本。5. 客户流失预测落地避坑这5个问题最容易翻车5.1 现象训练时AUC很高上线后预测全偏向不流失这是一个典型的“离线很爽、线上翻车”的例子。我见过有人把 AUC 做到 0.98结果第二轮预测出来的高分用户全是些已经消失很久的老用户对运营毫无指导意义。原因是特征泄漏造特征时用了未来数据离线验证时又用随机切分而不是时间切分模型在训练集里见过“答案”了。解决方法是严格按时间切分用预测时点之前的数据训练之后的数据验证特征工程里禁用未来函数。检查方法也很简单把预测时点换个时间窗口看 AUC 是否基本稳定如果大幅下跌先查特征里有没有用到标签信息。5.2 现象新用户没有历史数据特征全部为空新注册用户没有近30天活跃记录也没有累计金额模型会给它们一个不高不低的分数。但运营真正关心的是“刚进来还没留存的用户”你如果只用一个模型覆盖所有用户新用户永远被忽略。原因是没有区分用户生命周期阶段。解决方法是先给每个用户打一个 has_history 标志注册不足30天的用户单独建模或者用冷启动特征比如注册渠道、注册时间、首日是否完成关键行为。在填充缺失时不要一律用0因为0可能表示“没有行为”也可能表示“不适用”建议同时保留一个“数据不足”标记列。5.3 现象zip包换台机器跑路径全是反斜杠错误这类问题几乎每个交付的人都踩过。代码里写了 Windows 的绝对路径C:\Users\xxx\data\actions.csv打包到 Linux 服务器或 Mac 上跑一启动就报文件不存在。原因很简单路径分隔符硬编码。解决方法是统一用 pathlib.Path 拼接路径配置里的路径全部写成相对路径然后在代码入口通过Path(__file__).parent定位项目根目录。打包前我还会用grep -r C:扫一遍代码看到绝对路径就先改掉再压缩省得对方解压后拿着日志截图来找你。5.4 现象流失标签定义错了模型学到的其实是“定义本身”这是最隐蔽的坑。我见过有人把“近30天无活跃”当作流失标签但特征里又包含“近30天活跃次数”。标签和特征直接重叠模型根本不是在预测而是在把特征里的0映射成标签里的1AUC自然很高上线后却一无是处。解决方法是标签和特征之间必须有严格的时间间隔。用 t-1 月的特征预测 t 月是否沉默特征窗口和标签窗口不交叉。我在代码里把 prediction_date 作为硬边界生成样本时确保特征截止日比标签开始日早至少一个沉默期。5.5 现象模型每周要重训数据量太大内存直接崩客户留存系统的数据是不断累积的几周后全量行为日志可能过亿行。直接用 pandas 读进来做 groupby内存很容易撑爆。原因不是代码写错而是聚合方式不适合增量场景。解决方法是把特征聚合下沉到数据库或数仓用 SQL 先算好每周的最新统计量Python 只负责读宽表。如果一定要在 Python 里做可以按用户分片用 Dask 或 Polars 代替 pandas另外把用户ID、渠道ID这些低基数字段转成 category 类型能省不少内存。更聪明的做法是设计成增量特征表每周只更新近30天的窗口统计历史累计值用上次结果加本周增量而不是全量重算。6. 进阶用生存分析和流失概率做分群运营6.1 为什么不只预测“会不会流失”还要预测“还有多久”流失预测给的是一个概率但运营接到名单后会问下一句“他还能留多久”如果预测发现一个用户本月会流失触达动作可以更紧一点如果预测他三个月后才流失那可能是正常的使用周期不需要打扰。生存分析解决的正是“还要多久”的问题。它不把流失当成一个二分类标签而是把每个用户看成一个带时间的事件样本输出的是“存活到下一个周期”的概率曲线。6.2 用lifelines做Cox回归的落地代码我常用 lifelines 库跑 Cox 比例风险模型。它接受两条信息用户观察时长 T以及是否在观察期内流失 E。下面是最小用法from lifelines import CoxPHFitter df_surv feat.copy() df_surv[T] df_surv[observe_days] # 从第一次活跃到观察结束的天数 df_surv[E] df_surv[label] # 1流失0未流失 cph CoxPHFitter() cph.fit(df_surv[[T, E, last_active_days, recent_count, total_amount]], duration_colT, event_colE) cph.print_summary()Cox 模型的参数解读比树模型直观如果 last_active_days 的风险比大于1说明最近一次活跃距今越久流失风险越高。有了存活曲线你还可以对每个用户预测未来30天的存活概率把概率和 XGBoost 的流失分结合做一个二维分群高流失分且低存活概率的优先触达高流失分但存活概率还高的可能是需要长期培育的用户不用急着打扰。6.3 从预测到行动生命周期积分卡与触达时机最后这套系统真正产生价值的不是模型文件而是那张输出名单。我会把预测结果做成一张积分卡流失风险分、预计剩余活跃天数、用户贡献等级、最近触达时间。运营只需要关注“预计剩余活跃天数小于30天”的用户再结合风险分决定用短信还是电话。我第一次上线时只顾着把 AUC 调高忽略了阈值和行动策略结果运营拿着一张全是中低分用户的名单不知道干嘛。后来我把阈值和触达成本写进配置每个月复盘一次预测命中率这个系统才算真正转起来了。希望帮到你。本文还有配套的精品资源点击获取
返回列表