客户流失预测实战:生存分析+随机森林+Flask部署全链路拆解

发布时间:2026/10/9 3:41:17
客户流失预测实战:生存分析+随机森林+Flask部署全链路拆解 简介这份资源是面向计算机、人工智能及数据科学相关专业学生与从业者的客户留存分析与流失预测完整项目适用于电信运营商、保险公司等需要提升客户留存率的业务场景也可作为毕业设计或课程作业的参考方案。压缩包共62个文件约19.89MB包含3个Jupyter Notebook分别完成探索性分析、生存分析与流失预测建模1个Flask应用脚本与HTML模板用于模型部署另有2个pkl模型文件、1个bz2解释器文件及49张png可视化图表覆盖生存曲线、特征重要性、SHAP解释与部分依赖图等分析结果。项目通过生存分析刻画客户流失概率随时间的变化趋势并计算生命周期价值同时以随机森林预测客户是否流失最终借助Flask Web应用完成交互式展示。已有163人学习适合希望掌握客户流失建模全流程、理解模型部署与可视化呈现的读者参考借鉴。1. 客户留存分析与流失预测系统从生存曲线到 Flask 部署的完整拆解电信、保险、SaaS 这类订阅制业务里最贵的成本从来不是获客而是客户悄无声息地走掉。等你从月度报表里发现流失率抬头往往已经晚了两三个月。这个Customer-Survival-Analysis-and-Churn-Prediction-master资源包把两件事揉在了一起用生存分析Survival Analysis刻画客户流失概率随时间的动态变化再用随机森林做二分类预测最后用 Flask 把模型包成一个能交互的 Web 应用。它不是一个玩具 demomodel.pkl、survivemodel.pkl、explainer.bz2三个模型文件加上app.py、templates/index.html构成了一条从数据探索到线上推理的完整链路。适合谁做客户成功、增长分析、风控建模的工程师以及需要一套能跑通、能改、能写进毕业设计的完整项目的人。下面我按自己拆包的顺序把这份资源从结构到落地讲透。2. 资源结构与技术栈先看清包里到底装了什么2.1 目录分层与文件职责拿到一个 zip我习惯先tree一遍再动手避免改到一半发现模型和代码对不上。这个包的结构大致分四层Notebook 层、模型产物层、Web 应用层、可视化素材层。# 解压后先看整体结构重点关注 .ipynb / .pkl / app.py 三类文件 unzip Customer-Survival-Analysis-and-Churn-Prediction-master.zip cd Customer-Survival-Analysis-and-Churn-Prediction-master ls -R | head -60从文件清单能读出这条链路Exploratory Data Analysis.ipynb负责探索Customers Survival Analysis.ipynb做生存建模Churn Prediction Model.ipynb训练随机森林三个 Notebook 的产物分别落到survivemodel.pkl、model.pkl、explainer.bz2最后由app.py加载并对外提供预测。Images/下几十张 pngsurvival.png、shap.png、pdp_contract.png等是分析过程的可视化留档static/和templates/是 Flask 的前端资源。文件/目录作用是否可改app.pyFlask 入口加载模型并渲染页面可改注意模型路径model.pkl随机森林流失分类模型重训后替换survivemodel.pkl生存分析模型用于生存曲线重训后替换explainer.bz2SHAP 解释器压缩存储与模型配套别单独换*.ipynb三个分析/建模 Notebook主要改动区requirements.txt依赖清单按环境补版本Procfile部署启动命令云平台部署时用2.2 技术栈选型为什么是生存分析 随机森林很多人做流失预测直接上分类模型输出一个「会不会流失」的概率就完事。问题在于流失是一个带时间维度的事件客户在第 3 个月流失和第 18 个月流失对业务的意义完全不同。生存分析的核心是生存函数 S(t) 和风险函数 h(t)它处理的是「删失数据」——那些还没流失、观察期就结束的客户普通分类模型会把它们当负样本导致偏差。这个包用生存分析回答「客户能活多久、什么时候最危险」用随机森林回答「这个具体客户现在会不会走」两者互补。随机森林在这里是合理选择特征里有大量类别变量Contract、PaymentMethod、InternetService树模型对类别和缺失不敏感还能直接输出特征重要性包里model_feat_imp.png就是它。explainer.bz2用 SHAP 做局部解释解决树模型「黑匣子」的问题——业务方问「为什么判这个客户要流失」SHAP 能给出每个特征的贡献值。2.3 环境准备与依赖安装先建虚拟环境再装依赖别直接往系统 Python 里灌这是血泪经验。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt # 若 requirements 未锁版本常见核心依赖手动补 pip install flask pandas numpy scikit-learn lifelines shap matplotlib seabornlifelines是生存分析的常用库Kaplan-Meier、Cox 比例风险模型shap负责解释flask负责部署。装完先跑python -c import lifelines, shap, flask验证避免 Notebook 跑到一半才发现缺包。注意explainer.bz2是压缩的 SHAP 解释器加载时要用shap.Explainer或joblib配合bz2解压具体看app.py里的加载写法别自己臆造。3. 生存分析与流失预测建模三个 Notebook 怎么串起来3.1 探索性分析先搞清删失和特征分布Exploratory Data Analysis.ipynb是起点。电信客户数据集里关键字段是tenure在网月数、Churn是否流失、MonthlyCharges、TotalCharges以及一堆服务订阅字段。做生存分析前必须先定义「事件」和「观察期」事件是ChurnYes观察期是tenure那些ChurnNo的客户就是删失样本。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(telco_churn.csv) # 按实际数据文件名替换 # TotalCharges 常见坑空字符串导致整列变 object df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) df df.dropna(subset[TotalCharges]) # 流失与在网时长的关系先看分布再建模 df.groupby(Churn)[tenure].describe() df[tenure_group] pd.cut(df[tenure], bins[0,12,24,48,72], labels[0-12,12-24,24-48,48-72]) df.groupby([tenure_group,Churn]).size().unstack().plot(kindbar) plt.title(Churn by tenure group) plt.show()这段代码做了两件事把TotalCharges强制转数值原始数据里新客户该字段为空字符串不处理会让后续所有数值运算翻车以及按在网时长分箱看流失分布。包里tenure-churn.png、tenure_group.png就是这类图。参数上errorscoerce把无法转换的值变 NaN 再删比直接astype(float)安全。3.2 生存分析Kaplan-Meier 与 Cox 模型Customers Survival Analysis.ipynb是这份资源最有价值的部分。它用lifelines拟合生存曲线回答「客户在第 t 个月仍在网的概率」。from lifelines import KaplanMeierFitter, CoxPHFitter kmf KaplanMeierFitter() kmf.fit(durationsdf[tenure], event_observed(df[Churn]Yes)) kmf.plot_survival_function() plt.title(Overall Survival Curve) plt.xlabel(Tenure (months)) plt.ylabel(Survival Probability) plt.show() # 按合同类型分层看不同群体的生存差异 for contract in df[Contract].unique(): mask df[Contract] contract kmf.fit(df.loc[mask,tenure], (df.loc[mask,Churn]Yes), labelcontract) kmf.plot_survival_function() plt.show()fit的两个核心参数durations是观察时长event_observed是事件是否发生True 表示流失。分层画图能直观看到月付客户Month-to-month的生存曲线下降远快于两年合约客户这正是Contract.png、survival.png想表达的。Cox 模型进一步给出各特征的风险比hazard ratiohazard.png就是它的输出。cph CoxPHFitter() # 类别变量需先做 one-hotCox 不接受字符串 cox_df pd.get_dummies(df[[tenure,MonthlyCharges,Contract,Churn]], columns[Contract], drop_firstTrue) cox_df[event] (cox_df[Churn]Yes).astype(int) cph.fit(cox_df.drop(columns[Churn]), duration_coltenure, event_colevent) cph.print_summary()风险比大于 1 表示该特征增加流失风险。月付合同的 HR 通常显著大于 1这就是业务上「推年付合约能降流失」的量化依据。3.3 随机森林分类与 SHAP 解释Churn Prediction Model.ipynb走的是标准监督学习流程特征编码、训练、评估、解释。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, classification_report import joblib X pd.get_dummies(df.drop(columns[Churn,tenure_group]), drop_firstTrue) y (df[Churn]Yes).astype(int) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42) rf RandomForestClassifier(n_estimators300, max_depth12, class_weightbalanced, random_state42) rf.fit(X_train, y_train) pred rf.predict_proba(X_test)[:,1] print(AUC:, roc_auc_score(y_test, pred)) print(classification_report(y_test, rf.predict(X_test))) joblib.dump(rf, model.pkl)参数说明class_weightbalanced应对流失样本偏少的不平衡问题stratifyy保证训练测试集流失比例一致n_estimators300是精度和训练时间的折中。评估别只看准确率流失预测里 AUC 和召回率更关键——漏掉一个真要走的客户成本远高于误报。model_feat_imp.png是特征重要性图shap.png、shap1.png是 SHAP 汇总图explainer.bz2就是训练好的解释器供app.py在线解释单条预测。4. Flask 部署与交互把模型变成能点的页面4.1 app.py 的加载逻辑与路由app.py是整条链路的出口。它要同时加载分类模型、生存模型和 SHAP 解释器再通过路由把预测结果渲染到templates/index.html。import joblib, bz2, shap from flask import Flask, render_template, request app Flask(__name__) model joblib.load(model.pkl) surv_model joblib.load(survivemodel.pkl) # explainer.bz2 是压缩的 SHAP 解释器按实际存储方式解压加载 with bz2.BZ2File(explainer.bz2, rb) as f: explainer joblib.load(f) app.route(/, methods[GET,POST]) def index(): if request.method POST: # 从表单取特征构造与训练时同顺序的 DataFrame features build_features(request.form) prob model.predict_proba(features)[0,1] shap_vals explainer.shap_values(features) return render_template(index.html, probround(prob,3), shapshap_vals.tolist()) return render_template(index.html) if __name__ __main__: app.run(debugTrue)关键点在于build_features表单提交的是原始字段必须经过和训练时完全一致的 one-hot 编码、列顺序对齐否则模型输入维度对不上直接报错。这是部署阶段最常见的翻车点。4.2 本地启动与接口验证export FLASK_APPapp.py flask run --host0.0.0.0 --port5000 # 或直接 python app.py启动后浏览器打开http://127.0.0.1:5000填表提交看是否返回流失概率和 SHAP 解释。Procfile里通常写的是web: gunicorn app:app说明生产环境用 gunicorn 而非 Flask 自带服务器。本地验证时如果页面报 500先看终端 traceback八成是模型路径或特征列顺序问题。4.3 特征对齐部署阶段最容易翻车的地方训练时pd.get_dummies生成的列和线上单条数据get_dummies生成的列几乎不可能自动一致——线上只有一条记录某些类别值没出现列就少了。正确做法是保存训练时的列清单线上用reindex对齐。import json # 训练后保存列顺序 json.dump(list(X_train.columns), open(feature_cols.json,w)) # 线上推理时对齐 cols json.load(open(feature_cols.json)) features features.reindex(columnscols, fill_value0)fill_value0表示训练时存在但线上没出现的类别默认该客户不属于该类。不做这一步模型要么报维度错误要么静默给出错误预测后者更可怕。5. 避坑与常见问题排查5.1 现象Notebook 里TotalCharges全是 NaN原因原始 CSV 里新客户的TotalCharges是空字符串pd.read_csv把它读成 object 列直接astype(float)会抛错用errorscoerce又可能把整列变 NaN。解决先pd.to_numeric(..., errorscoerce)再检查 NaN 比例少量直接dropna量大则考虑用MonthlyCharges * tenure填补。5.2 现象生存曲线画出来是平的或异常原因event_observed传成了字符串Yes/No而非布尔值lifelines把非零值都当事件发生导致曲线失真。解决统一转成(df[Churn]Yes)的布尔 Seriesdurations必须是数值型且非负。5.3 现象Flask 启动报ModuleNotFoundError: lifelines原因requirements.txt没锁全依赖或虚拟环境没激活就装了包。解决确认which python指向 venv重新pip install -r requirements.txt缺的库手动补。部署到云平台时Procfile和requirements.txt必须同时存在否则构建阶段就失败。5.4 现象SHAP 解释加载报版本不兼容原因explainer.bz2是用特定版本shap序列化的换版本后反序列化失败。解决按requirements.txt里的 shap 版本安装别盲目升级。若实在不兼容用model.pkl重新训练一个shap.TreeExplainer替代。5.5 现象预测概率全是 0.5 附近原因特征没做对齐或类别编码顺序和训练时不一致模型实际在「瞎猜」。解决核对feature_cols.json打印线上输入和训练输入的列名做 diff确保完全一致。这是最隐蔽的坑模型不报错但结果无意义。6. 进阶技巧用生存曲线做客户生命周期价值分层把生存分析和分类模型结合能做出比单纯「流失概率」更有业务价值的输出。具体做法用survivemodel.pkl预测每个客户未来 6 个月的生存概率乘以该客户的MonthlyCharges得到期望剩余价值Expected Residual Value再按价值分位切层。import numpy as np # 假设 surv_model 提供 predict_survival_function def expected_residual_value(customer, months6): surv surv_model.predict_survival_function(customer, timesrange(months1)) # 每月存活概率之和 × 月费 ≈ 期望剩余收入 return float(np.sum(surv.values)) * customer[MonthlyCharges] df[erv] df.apply(expected_residual_value, axis1) df[value_tier] pd.qcut(df[erv], q4, labels[低,中,高,极高]) # 交叉看高价值且高流失概率的客户才是优先挽留对象 priority df[(df[value_tier].isin([高,极高])) (df[churn_prob] 0.6)]这样输出的priority名单比单纯按流失概率排序精准得多——一个每月 20 元、流失概率 90% 的客户和一个每月 100 元、流失概率 70% 的客户后者才值得投入挽留成本。验证方法上我会用历史数据做回测取某个时间点切分看模型预测的高风险客户在随后 3 个月的实际流失率是否显著高于平均AUC 和提升度lift都达标才算可用。从那以后我每次拿到这类带.pkl的资源包都强制先跑一遍「训练列 → 线上列」的对齐检查再谈模型效果——模型再准特征对不上都是白搭。希望这份拆解能帮你少走几个弯路把这份资源真正跑起来、用起来。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询