☰
客户流失预测实战:生存分析+随机森林+Flask部署全流程
2026/10/1 4:54:53 网站建设 项目流程

简介:这份资源是面向计算机、人工智能及数据科学相关专业学生与从业者的客户留存分析与流失预测完整项目,适用于电信运营商、保险公司等需要处理客户数据的业务场景,可作为毕业设计、课程作业或技能练手参考。压缩包共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.pkl;Churn 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"], errors="coerce") df["TotalCharges"].fillna(df["TotalCharges"].median(), inplace=True) # 生存分析需要数值型时间与事件标记 df["tenure"] = df["tenure"].astype(int) df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0}) print(df[["tenure", "Churn", "MonthlyCharges", "TotalCharges"]].dtypes)

逻辑说明:errors="coerce"把无法解析的值变 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(durations=df["tenure"], event_observed=df["Churn"], label="Overall") kmf.plot_survival_function() # 按合同类型分组对比生存曲线 for contract in df["Contract"].unique(): mask = df["Contract"] == contract kmf.fit(df.loc[mask, "tenure"], df.loc[mask, "Churn"], label=contract) kmf.plot_survival_function() # Cox 比例风险模型 covariates = ["MonthlyCharges", "TotalCharges", "tenure", "SeniorCitizen"] cph = CoxPHFitter() cph.fit(df[["tenure", "Churn"] + covariates], duration_col="tenure", event_col="Churn") cph.print_summary() cph.plot()

逻辑说明:fit的durations是时间列,event_observed是事件列,两个参数顺序别搞反,反了曲线会完全失真。分组画 Kaplan-Meier 是为了看Contract这种类别对生存曲线的影响——月付合同(Month-to-month)的曲线通常掉得最快,这就是Contract.png、payment-contract.png想表达的信号。Cox 模型输出的是风险比(hazard ratio),print_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_first=True) y = df["Churn"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) rf = RandomForestClassifier( n_estimators=300, max_depth=12, min_samples_leaf=5, class_weight="balanced", random_state=42, 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)

逻辑说明:stratify=y保证训练测试集流失比例一致,流失样本通常只占两成多,不分层会导致测试集里正样本太少、指标抖动。class_weight="balanced"是应对类别不平衡的常规手段,比盲目上 SMOTE 更省事。n_estimators=300是精度和训练时间的折中,max_depth=12防止树长太深过拟合,min_samples_leaf=5让叶子节点有足够样本、预测更稳。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(columns=model.feature_names_in_, fill_value=0) proba = model.predict_proba(row)[0, 1] label = "会流失" if proba > 0.5 else "不会流失" return render_template("index.html", prediction=label, probability=round(proba, 4)) if __name__ == "__main__": app.run(debug=True)

逻辑说明:joblib.load加载model.pkl和survive model.pkl,注意文件名里带空格,路径要写对或用引号包住。reindex(columns=model.feature_names_in_, fill_value=0)是关键一步——训练时pd.get_dummies展开的列和前端表单提交的列不可能完全对齐,用feature_names_in_对齐并补 0,能避免「特征数量不匹配」的报错。predict_proba取[0, 1]是正类概率。参数上debug=True只用于本地调试,上线要关掉。

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', filename='images/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对tenure=0的新客户是空字符串,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_first=True,预测时也要保持一致,否则会多出冗余列。

4.4 现象:Procfile部署后启动失败,日志显示找不到app

原因:Procfile文件名带了.txt后缀,或者app.py不在根目录,或者gunicorn没装。

解决:确认文件名就是Procfile,app.py在项目根目录,pip install gunicorn。本地先gunicorn app:app跑通再上平台。

4.5 现象:SHAP 图跑得极慢或内存爆掉

原因:用了KernelExplainer而不是TreeExplainer,或者shap_values在测试集全量上算。

解决:随机森林必须用TreeExplainer。算 SHAP 时抽样,X_test.sample(200, random_state=42)就够画 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"] # 按月消费均值估算 CLV,discount_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_rate=0.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 开始单调不增,不对就立刻回头查列。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询