客户流失预测实战:生存分析+随机森林+Flask部署全流程 简介这份资源是面向计算机、人工智能及数据科学相关专业学生与从业者的客户留存分析与流失预测完整项目适用于电信运营商、保险公司等需要处理客户数据的业务场景可作为毕业设计、课程作业或技能练手参考。压缩包共62个文件约19.89MB以49张png可视化图表、3个ipynb分析笔记、3个md说明文档为主另含pkl模型文件、py部署脚本、html页面及bz2解释器文件覆盖从数据探索到模型上线的完整链路。项目围绕生存分析模型刻画客户流失概率随时间变化的趋势并计算生命周期价值同时用随机森林预测客户是否流失再通过Flask Web应用完成部署与交互配套图表直观展示留存分析与预测结果。目前已有163人学习下载读者可据此理解生存分析与分类模型的组合思路、特征重要性及部分依赖图等可解释性分析并参考Web部署与目录组织方式快速复现一套可运行的客户流失分析系统。1. 客户留存分析与流失预测系统从生存曲线到 Flask 部署的完整拆解电信、保险、SaaS 订阅这类行业里客户流失从来不是「走或不走」的二值问题而是「什么时候走、为什么走、走了值多少钱」的时间问题。这份Customer-Survival-Analysis-and-Churn-Prediction-master资源包恰好把这两条线都铺齐了一条用生存分析Survival Analysis刻画流失概率随账期tenure变化的动态曲线另一条用随机森林做二分类流失预测最后用 Flask 把模型包成可交互的 Web 应用。包里既有Exploratory Data Analysis.ipynb、Customers Survival Analysis.ipynb、Churn Prediction Model.ipynb三个 Notebook也有app.py、templates/index.html、model.pkl、survive model.pkl、explainer.bz2这些部署件还有一整套Images/可视化图。适合谁做客户留存分析、流失预测系统课程设计或毕业设计的同学以及想快速搭一个「生存分析 机器学习 Web 部署」闭环的从业者。下面按「资源是什么 → 怎么跑起来 → 坑在哪 → 怎么用透」的顺序拆。2. 生存分析与随机森林双模型原理选型与数据准备2.1 为什么流失预测要同时上生存分析和随机森林单用分类模型做流失预测有个绕不开的硬伤它只回答「这个客户会不会流失」不回答「多久会流失」。而业务上真正值钱的是时间维度——一个还有 2 个月就到期的高价值客户和一个刚续约 12 个月的客户哪怕流失概率都是 0.7运营动作完全不同。生存分析里的 Kaplan-Meier 曲线和 Cox 比例风险模型Cox PH就是干这个的它把「事件发生时间」和「是否删失」一起建模输出的是生存函数 S(t) 和风险函数 h(t)。这份资源的分工很清晰Customers Survival Analysis.ipynb负责生存分析产出survival.png、SurvivalCurve.png、hazard.png、survive model.pklChurn Prediction Model.ipynb负责随机森林分类产出model.pkl、model_feat_imp.png、shap.png、eli51.png、eli52.png这些解释性图。选随机森林而不是逻辑回归是因为电信客户数据里Contract、InternetService、PaymentMethod这类类别特征多、非线性交互强树模型不用手工做太多交叉特征就能吃到internetservice-contract.png、payment-contract.png这种组合信号。2.2 环境依赖与数据字段确认先看requirements.txt这是复现的第一道关。常见做法是建独立虚拟环境再装避免和系统里的 pandas、scikit-learn 版本打架python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt逻辑说明requirements.txt里通常锁定了pandas、numpy、scikit-learn、lifelines生存分析核心库、flask、shap、matplotlib、seaborn。参数上要留意lifelines和scikit-learn的版本兼容——lifelines对scikit-learn版本比较敏感装完先python -c import lifelines; print(lifelines.__version__)验一下。数据字段方面从Images/里的图能反推出核心列tenure在网月数生存分析的时间变量、Churn是否流失事件标记、MonthlyCharges、TotalCharges、Contract、PaymentMethod、InternetService、TechSupport、OnlineSecurity、StreamingTV、StreamingMovies、PaperlessBilling、SeniorCitizen、Partner、Dependents、MultipleLines、DeviceProtection、OnlineBackup。TotalCharges这列在原始 Telco 数据里常带空字符串读进来会变 object 类型必须先转数值。import pandas as pd import numpy as np df pd.read_csv(telco_churn.csv) # TotalCharges 常见坑空字符串导致整列是 object df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) df[TotalCharges].fillna(df[TotalCharges].median(), inplaceTrue) # 生存分析需要数值型时间与事件标记 df[tenure] df[tenure].astype(int) df[Churn] df[Churn].map({Yes: 1, No: 0}) print(df[[tenure, Churn, MonthlyCharges, TotalCharges]].dtypes)逻辑说明errorscoerce把无法解析的值变 NaN再用中位数填充避免直接dropna丢掉整行样本。Churn映射成 0/1 是生存分析事件标记的硬要求lifelines只认数值。参数上中位数填充比均值稳因为TotalCharges分布右偏均值会被高消费客户拉高。2.3 生存分析建模Kaplan-Meier 与 Cox 的落地步骤生存分析的核心是把「删失」处理对。所谓删失就是到观察期结束时客户还没流失——你不能当他没流失也不能当他流失了只能标记为删失。lifelines的KaplanMeierFitter和CoxPHFitter分别对应非参数和半参数两条路。from lifelines import KaplanMeierFitter, CoxPHFitter from lifelines.statistics import logrank_test kmf KaplanMeierFitter() kmf.fit(durationsdf[tenure], event_observeddf[Churn], labelOverall) kmf.plot_survival_function() # 按合同类型分组对比生存曲线 for contract in df[Contract].unique(): mask df[Contract] contract kmf.fit(df.loc[mask, tenure], df.loc[mask, Churn], labelcontract) kmf.plot_survival_function() # Cox 比例风险模型 covariates [MonthlyCharges, TotalCharges, tenure, SeniorCitizen] cph CoxPHFitter() cph.fit(df[[tenure, Churn] covariates], duration_coltenure, event_colChurn) cph.print_summary() cph.plot()逻辑说明fit的durations是时间列event_observed是事件列两个参数顺序别搞反反了曲线会完全失真。分组画 Kaplan-Meier 是为了看Contract这种类别对生存曲线的影响——月付合同Month-to-month的曲线通常掉得最快这就是Contract.png、payment-contract.png想表达的信号。Cox 模型输出的是风险比hazard ratioprint_summary()里exp(coef)大于 1 表示该变量增加流失风险。参数上tenure同时做时间列和协变量在业务上有点循环实操里我一般把tenure从协变量里拿掉只留消费和人口属性。2.4 随机森林流失预测与模型解释分类这条线相对标准但这份资源在可解释性上做得比较足shap.png、eli51.png、eli52.png、perm_imp.png、pdp_*.png一整套都在。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score import shap X pd.get_dummies(df.drop(columns[Churn, customerID]), drop_firstTrue) y df[Churn] 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, min_samples_leaf5, class_weightbalanced, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) proba rf.predict_proba(X_test)[:, 1] print(classification_report(y_test, (proba 0.5).astype(int))) print(AUC:, roc_auc_score(y_test, proba)) explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values[1], X_test)逻辑说明stratifyy保证训练测试集流失比例一致流失样本通常只占两成多不分层会导致测试集里正样本太少、指标抖动。class_weightbalanced是应对类别不平衡的常规手段比盲目上 SMOTE 更省事。n_estimators300是精度和训练时间的折中max_depth12防止树长太深过拟合min_samples_leaf5让叶子节点有足够样本、预测更稳。SHAP 的summary_plot输出全局特征重要性方向TreeExplainer对随机森林是精确解比KernelExplainer快得多。explainer.bz2就是序列化后的解释器部署时直接加载省去重算。3. Flask 部署把两个模型包成可交互应用3.1 app.py 的路由结构与模板渲染app.py是整个部署的入口templates/index.html是前端表单static/放样式和图片。典型结构是首页展示表单、提交后返回预测结果和可视化图。from flask import Flask, render_template, request import joblib import numpy as np import pandas as pd app Flask(__name__) model joblib.load(model.pkl) survive_model joblib.load(survive model.pkl) app.route(/) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): features { tenure: int(request.form[tenure]), MonthlyCharges: float(request.form[MonthlyCharges]), TotalCharges: float(request.form[TotalCharges]), SeniorCitizen: int(request.form[SeniorCitizen]), } row pd.DataFrame([features]) row row.reindex(columnsmodel.feature_names_in_, fill_value0) proba model.predict_proba(row)[0, 1] label 会流失 if proba 0.5 else 不会流失 return render_template(index.html, predictionlabel, probabilityround(proba, 4)) if __name__ __main__: app.run(debugTrue)逻辑说明joblib.load加载model.pkl和survive model.pkl注意文件名里带空格路径要写对或用引号包住。reindex(columnsmodel.feature_names_in_, fill_value0)是关键一步——训练时pd.get_dummies展开的列和前端表单提交的列不可能完全对齐用feature_names_in_对齐并补 0能避免「特征数量不匹配」的报错。predict_proba取[0, 1]是正类概率。参数上debugTrue只用于本地调试上线要关掉。3.2 Procfile 与生产环境启动Procfile是给平台化部署用的内容通常是一行web: gunicorn app:app逻辑说明gunicorn是生产级 WSGI 服务器比 Flask 自带的开发服务器稳。app:app指app.py里的app对象。本地想模拟生产可以gunicorn -w 4 -b 0.0.0.0:8000 app:app-w 4是 4 个 worker 进程按 CPU 核数调。注意Procfile不带扩展名Windows 下创建容易变成Procfile.txt这是个高频翻车点。3.3 前端表单与结果可视化对接index.html里表单字段要和app.py里request.form取的 key 严格一致否则KeyError。可视化图放在static/images/下模板里用url_for(static, filenameimages/xxx.png)引用。app-pic.png是应用截图Images/里那批图是分析结果两者别混。前端提交后如果只返回文字结果体验偏干常见做法是把survival.png、shap.png一起渲染出来让用户看到「为什么是这个预测」。4. 避坑与排查这份资源跑起来最容易卡的地方4.1 现象ModuleNotFoundError: No module named lifelines原因requirements.txt没装全或者装到了系统 Python 而不是虚拟环境。生存分析这条线强依赖lifelines缺了Customers Survival Analysis.ipynb直接跑不动。解决确认虚拟环境已激活命令行前缀有(venv)再pip install lifelines。如果lifelines装完报scikit-learn版本冲突按requirements.txt里的版本重装scikit-learn别用--no-deps硬跳。4.2 现象TotalCharges转数值后大量 NaN模型 AUC 异常低原因原始数据里TotalCharges对tenure0的新客户是空字符串pd.to_numeric后变 NaN如果直接dropna会丢掉一批新客户样本而新客户恰恰是流失高发群体。解决用中位数或 0 填充别删行。填充后检查df[TotalCharges].isna().sum()是否为 0。另外确认填充发生在train_test_split之前还是之后——填充要在划分前用训练集统计量严谨做法是先划分再各自填充避免数据泄漏。4.3 现象Flask 提交表单报KeyError或特征维度不匹配原因前端表单字段名和request.form取的 key 不一致或者pd.get_dummies展开后的列顺序、数量与训练时不同。解决用model.feature_names_in_做列对齐见 3.1 代码前端字段名逐个核对。如果训练时用了drop_firstTrue预测时也要保持一致否则会多出冗余列。4.4 现象Procfile部署后启动失败日志显示找不到app原因Procfile文件名带了.txt后缀或者app.py不在根目录或者gunicorn没装。解决确认文件名就是Procfileapp.py在项目根目录pip install gunicorn。本地先gunicorn app:app跑通再上平台。4.5 现象SHAP 图跑得极慢或内存爆掉原因用了KernelExplainer而不是TreeExplainer或者shap_values在测试集全量上算。解决随机森林必须用TreeExplainer。算 SHAP 时抽样X_test.sample(200, random_state42)就够画 summary plot全量算没必要。explainer.bz2已经序列化了 explainer直接加载能省重算时间。5. 进阶用法用生存曲线算客户生命周期价值CLV把生存分析和 CLV 接起来是这份资源最值钱的延伸。生存函数 S(t) 给出客户活过 t 个月的概率把它和月消费乘起来积分就是期望生命周期价值。这一步能把「预测流失」升级成「预测流失带来的收入损失」业务说服力完全不一样。import numpy as np from lifelines import KaplanMeierFitter kmf KaplanMeierFitter() kmf.fit(df[tenure], df[Churn]) survival_probs kmf.survival_function_.reset_index() survival_probs.columns [timeline, survival_prob] # 按月消费均值估算 CLVdiscount_rate 为月折现率 monthly_charge df[MonthlyCharges].mean() discount_rate 0.01 clv 0.0 for _, row in survival_probs.iterrows(): t row[timeline] clv row[survival_prob] * monthly_charge / ((1 discount_rate) ** t) print(f平均客户生命周期价值估算: {clv:.2f})逻辑说明survival_function_是 Kaplan-Meier 估计出的生存概率序列timeline是在网月数。循环里survival_prob * monthly_charge是该月的期望收入除以(1 discount_rate) ** t做折现。discount_rate0.01是月折现率示例实际按企业资金成本调。这个 CLV 是群体均值想算个体级 CLV就把 Cox 模型的个体生存曲线代进去按Contract、MonthlyCharges分组算能直接支撑「哪些客户值得花预算挽留」的决策。验证方法上我一般做两件事一是把 Kaplan-Meier 曲线和SurvivalCurve.png、survival.png对照确认形状一致二是用lifelines.statistics.logrank_test检验不同合同类型的生存曲线差异是否显著p 值小于 0.05 才认为分组有意义。参数边界上tenure最大值通常在 72 个月左右曲线尾部样本少、置信区间宽别对尾部做过度解读。血泪经验是生存分析里时间列和事件列一旦搞反曲线会画成一条诡异的上升线而且不报错纯靠肉眼发现。从那以后我每次kmf.fit之后都强制先print(kmf.survival_function_.head())看一眼生存概率是不是从 1 开始单调不增不对就立刻回头查列。希望帮到你。本文还有配套的精品资源点击获取