ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

生存分析实战:电信客户流失预测与剩余价值计算

生存分析实战:电信客户流失预测与剩余价值计算 简介面向数据科学与机器学习初学者的实战资源围绕Kaggle电信客户流失数据集演示如何运用生存分析方法对客户流失进行建模与预测。资源共7个文件、压缩包仅186KB包含完整CSV数据集、Jupyter Notebook分析脚本、Python辅助模块以及README说明文档其中Notebook清晰呈现从数据清洗、特征理解到生存曲线估计与流失风险预测的完整流程md与txt文件则补充了数据集字段含义与链接信息。目前已有566人学习下载适合希望将生存分析落地到商业场景的学习者。通过该资源读者可以快速获取可直接运行的代码与数据集理解tenure、合约类型、月费用等字段对流失风险的影响并掌握利用生存模型处理客户流失问题的基本思路。1. 流失预测为什么要换成生存分析这套时钟流失预测在电信行业通常被当成一场二分类竞赛拉出特征、跑一版XGBoost、出一个流失概率、按阈值圈人。但这种方式天生丢掉了一样信息——时间。它只回答了“哪些客户会走”回答不了“大概在哪个时间窗口内走”。生存分析换了一个提问方式把每个客户看成一条持续运行的“生命周期时钟”流失是时钟最终会走到的事件未流失只是观察终止时时钟还没走完。这个视角在Kaggle的电信客户流失数据集Telco Customer Churn上值得完整跑一遍从构造删失标记、画Kaplan-Meier曲线到拟合Cox比例风险模型最后输出比分类模型更贴合业务口径的剩余客户价值也给运营一个可以排期干预的动态清单。2. 从“是否流失”到“何时流失”把电信流失数据集转成生存数据2.1 原生数据与生存数据格式的差异打开Kaggle上这个数据集变量是标准的客户档案表。典型列包括gender、SeniorCitizen、Partner、Dependents、tenure、PhoneService、InternetService、OnlineSecurity、Contract、PaperlessBilling、PaymentMethod、MonthlyCharges、TotalCharges而目标列Churn只有Yes/No两种取值。这和生存数据最明显的一个差别是没有任何一个字段直接告诉你“事件发生在第几个月”。你只知道客户在快照时刻是否已经流失至于他是什么时候开始有流失风险的以及流失之前“存活”了多久只藏在tenure这一个字段里。另一个差别是数据的“删失结构”完全缺失。在标准生存数据里每行至少有两个必要字段duration观察到的事件时间或删失时间event1表示事件发生0表示右删失。而这个数据集里event就是Churnduration只能通过tenure来扮演。所以建模的第一步不是调参而是把表格从“二分类标签”翻译成“存活时间删失标记”。2.2 时间列与删失标记的构造逻辑把数据集想象成在某个采样日对全体客户拍了一张快照已经流失的客户快照记录他们流失时累计服务了tenure个月此时事件发生TtenureE1尚未流失的客户我们只知道“到快照日为止还没流失”TtenureE0。这个E0表示的是右删失事件可能在快照之后随时发生只是我们在观察窗口内没有看到它。右删失是生存分析里最常见也最容易被弄反的概念它的反面是“真的不会流失”而这个数据集里根本没有“永远不会流失”的客户。字段映射如下表原字段生存数据中的角色处理方式tenureduration存活月数数值直接使用Churnevent事件指示器Yes→1No→0TotalCharges时间衍生校验与MonthlyCharges*tenure互查Contract/PaymentMethod等协变量one-hot编码校验逻辑也顺手做一步TotalCharges在原始数据里的定义是累计费用正常情况下TotalCharges/MonthlyCharges应该约等于tenure。若个别客户偏差极大通常说明计费周期或优惠活动导致口径不一致可以按原始值保留也可以用MonthlyCharges*tenure覆盖影响很小。2.3 Kaggle环境下的数据预处理代码在Kaggle上直接搜索“Telco Customer Churn”数据集选择Add Notebook后数据会自动挂载在/kaggle/input目录下。注册登录即可使用不需要额外从网盘下载数据集。以下是完整的预处理逻辑import pandas as pd import numpy as np df pd.read_csv(/kaggle/input/telco-customer-churn/WA_Fn-UseC_-Telco-Customer-Churn.csv) df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) df df.dropna(subset[TotalCharges]) df[Churn] df[Churn].map({Yes: 1, No: 0}) # 数据质量校验Tenure与累计费用/月费的比值应当接近1 df[implied_tenure] df[TotalCharges] / df[MonthlyCharges] mismatch (df[implied_tenure] - df[tenure]).abs().median() print(fmedian |implied_tenure - tenure| {mismatch:.2f} months) # 删失率速览 print(event rate:, df[Churn].mean().round(4)) print(censoring rate:, (1 - df[Churn].mean()).round(4))这段代码里把TotalCharges先转成数值类型是因为原CSV的空值在读取后表现为空字符串用errorscoerce会把非法值变成NaN再用dropna去掉全部11行空记录放在7043行的总样本里舍弃后对模型影响极小。后面的Churn映射把文本标签转成0/1方便lifelines和sklearn统一处理。implied_tenure列不做入模特征只用来验证字段间的一致性。如果mismatch的中位数明显大于0.5个月就要回看总费用字段是不是有优惠抵扣混在里面避免后续分箱和统计走偏。到这一步数据已经具备生存分析的两根柱子durationtenure和eventChurn。下一章就可以直接在其上画曲线、跑模型。3. 从粗到细Kaplan-Meier曲线与Cox比例风险模型3.1 用Kaplan-Meier曲线看整体流失时钟的形状Kaplan-Meier估计是生存分析的第一张图它不假设生存函数服从任何分布直接按每个事件发生时间点计算条件存活概率的连乘。在lifelines里只需要一次fit和一次plotfrom lifelines import KaplanMeierFitter kmf KaplanMeierFitter() kmf.fit(durationsdf[tenure], event_observeddf[Churn], labelAll customers) kmf.plot_survival_function() plt.xlabel(Tenure (months)) plt.ylabel(P(Still Active)) plt.title(Overall Survival Curve of Telecom Churn)说明duration传入tenureevent_observed传入0/1的Churn二者对齐后lifelines会自己处理删失。默认情况下曲线从t0的存活概率1.0开始每个阶梯代表一批流失事件。整体看这条曲线最初6-12个月下降速度最快说明新客户阶段是流失的高危窗口。3.2 分组对比合同类型决定生存曲线形状把客户按Contract分成月签与长签两组并做log-rank检验from lifelines.statistics import logrank_test group_1 df[Contract] Month-to-month kmf_mtm KaplanMeierFitter().fit(df.loc[group_1, tenure], df.loc[group_1, Churn], labelMonth-to-month) kmf_long KaplanMeierFitter().fit(df.loc[~group_1, tenure], df.loc[~group_1, Churn], label1 or 2 year contract) ax kmf_mtm.plot_survival_function() kmf_long.plot_survival_function(axax) plt.title(Survival Curves by Contract Type) plt.xlabel(Tenure (months)) result logrank_test(df.loc[group_1, tenure], df.loc[~group_1, tenure], df.loc[group_1, Churn], df.loc[~group_1, Churn]) print(log-rank p-value:, round(result.p_value, 10))逻辑说明logrank_test的前两组参数是不同分组下的duration后两组参数是两组对应的事件指示器。返回的p-value非常小说明两条曲线之间的差异不是抽样波动。图上的含义也更直接月签客户在50个月时存活概率只剩20%上下而长签客户同期还在60%以上。3.3 Cox比例风险模型让多个协变量同时参与竞争KM曲线适合展示一两个分组维度的差异但电信流失数据里有几十个特征业务想知道的不是“月签会不会流失”这种单变量问题而是“在控制其他因素后OnlineSecurity这个服务到底带来多少保护”。这就要用到Cox比例风险模型。Cox模型的数学内核是风险函数h(t|X) h_0(t) · exp(βᵀX)h_0(t)是基线风险不要求服从任何分布这让Cox成为半参数模型。β直接以风险比的形式参与解释exp(β)大于1表示该变量加大流失风险小于1则相反。from lifelines import CoxPHFitter df_model df.copy() categorical_features [gender, Partner, Dependents, PhoneService, MultipleLines, InternetService, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies, Contract, PaperlessBilling, PaymentMethod] df_model pd.get_dummies(df_model, columnscategorical_features, drop_firstTrue, dtypeint) # tenure同时是时间轴不能再次作为协变量 feature_cols [c for c in df_model.columns if c not in [customerID, tenure, Churn, implied_tenure]] cph CoxPHFitter(penalizer0.1) cph.fit(df_model[feature_cols [tenure, Churn]], duration_coltenure, event_colChurn, show_progressTrue) cph.print_summary()特别说明一个容易踩的坑这里显式把tenure从特征列中去掉。Cox模型的风险函数以t为条件duration本身同时出现在时间轴和协变量里会造成逻辑循环——模型的含义会变成“已经活到第t个月的客户他在第t个月的风险还受到‘他已经活了t个月’这个数字的影响”参数估计会发生扭曲。如果确实想研究在网时长对流失的影响应该改用时变协变量或参数化生存模型而不是直接把tenure放进X。penalizer0.1是lifelines对系数施加的L2正则化强度。在7043样本和30多个one-hot特征下它可以让系数估计更稳定尤其让那些稀疏类别比如StreamingTV_No internet service不会产生夸张的系数。如果想要不打折的解释性报告可以跑penalizer0的版本做对照系数方向一般不变但标准误会更大。print_summary输出里值得优先看三样东西exp(coef)一列就是风险比HRp0.05的变量才有统计显著性置信区间不要太宽否则即使显著也没有操作意义。从系数方向上看Contract_Month-to-month的风险比通常在2.5到3之间是所有变量里最强的一个OnlineSecurity_Yes与TechSupport_Yes的风险比远小于1是明显保护因素。这类发现比单纯的特征重要性更能支撑运营动作。4. 模型评估与假设检验C-index、PH假设与特征筛选的坑4.1 C-index两两比较的一致性生存模型的AUC分类模型看AUC生存模型对应的是C-index。它的含义是随机抽一对客户预测的风险排序与实际事件发生的先后顺序一致的概率。C-index0.5相当于随机猜0.8以上在流失场景里已经是相当好的排序能力。在lifelines里直接这样算from lifelines.utils import concordance_index pred_risk cph.predict_partial_hazard(df_model[feature_cols]) # 风险值越大预期存活时间越短因此传入负风险作为“生存时间打分” c_index concordance_index(df_model[tenure], -pred_risk, df_model[Churn]) print(fCox C-index: {c_index:.4f})逻辑说明concordance_index的三个参数分别是事件时间、预测分数、事件指示器。它内部按“预测分数更高应该对应事件时间更短”的方向比较而Cox模型的predict_partial_hazard返回的是hazard风险越高活的时间越短所以这里取负号把方向对齐。若忘了取负号算出来的C-index会明显小于0.5这是最常见的方向性bug。4.2 比例风险假设什么时候必须管什么时候可以放宽Cox模型有一个硬性假设任意两个客户的hazard比随时间保持不变。对流失数据来说这意味着“OnlineSecurity的作用在第3个月和第30个月应该是同样强度”。现实往往不是这样。用check_assumptions可以快速体检cph.check_assumptions(df_model[feature_cols [tenure, Churn]], p_value_threshold0.05, plot_normalityTrue)输出结果会列出违反PH假设的变量并给出两种建议做strata分层或把变量改成时变系数。实际业务项目中几乎每个电信数据集都会有几个变量过不了检验原因有两个一是大样本下假设检验本身敏感二是流失数据的时间跨度通常在72个月以内短时段内比例性偏差不像长期随访数据那么严重。我的处理顺序是先看plot画出的scaled Schoenfeld残差图残差如果只是在零点附近随机波动而p值小通常可以忽略若存在明显趋势优先把该变量放入strata参数strata变量不估计系数但允许不同水平拥有不同基线风险。cph_strata CoxPHFitter(penalizer0.1) cph_strata.fit(df_model[feature_cols [tenure, Churn]], duration_coltenure, event_colChurn, strata[Contract_Month-to-month]) cph_strata.print_summary()说明把Contract_Month-to-month放进strata后月签与长签客户共享一套其他变量的系数却拥有各自独立的基线风险曲线。这会牺牲一点可解释性无法直接读出合同类型的HR但得到的其他变量估计更稳健。换一个方向理解这也是为什么前面留了penalizer这个旋钮当你的目标是预测排序而不是因果解释时0.1的惩罚项往往比strata更省事。4.3 特征筛选与业务解释对照Cox模型不像XGBoost那样天然输出feature_importance筛特征更依赖系数显著性、方向一致性和稳定性。我一般这样操作一步剔除看p值删掉gender、PhoneService这类明显不显著的变量。gender里female的基线以male为参照系数通常接近于0说明男性女性的流失风险没有系统性差异逻辑方向校验PaperlessBilling的HR大于1解释是开通电子账单的人更熟悉线上操作、对切换服务商的心理门槛更低这个方向在多个数据集上都稳定交互补充如果想看“月签且无在线安全”这类组合直接在特征列表里加乘积项并观察它是否压低单项系数。组合项显著时单项系数会变得难以单独解释报告时建议二者一起呈现。特征筛完重新fit一次并对比C-index。如果筛掉的变量让C-index下降了0.01以内就值得保留精简版本因为线上部署或后续开发干预策略时特征越少越容易维护。5. 把生存曲线翻译成运营策略计算剩余客户生命周期价值5.1 从S(t)到未来期望现金流生存模型输出的是S(t)也就是客户活到第t个月的概率。把S(t)乘上该客户的月费再对未来N个月求和就得到剩余客户生命周期价值Remaining Customer Lifetime Value简称SLV。这个指标直接对应运营排期高SLV客户值得优先干预因为保住他们带来的现金流更大。计算逻辑如下import matplotlib.pyplot as plt # 预测某个客户的生存函数 sample_id 42 surv cph.predict_survival_function(df_model[feature_cols].iloc[[sample_id]]) # 取前12个月的存活概率按离散月求和 surv_12 surv.loc[:12].sum(axis0).iloc[0] monthly df_model[MonthlyCharges].iloc[sample_id] slv_12 surv_12 * monthly print(fCustomer {df_model[customerID].iloc[sample_id]}: 12-month SLV {slv_12:.2f})说明surv返回的DataFrame以月为索引存活概率递减loc[:12]截取到第12个月sum()相当于对未来12个月的存活概率做离散积分。月费来自原始数据。最后的结果可以理解为“在不干预的情况下这个客户未来12个月的期望收入”。如果换成全体客户就得到了运营部门直接能用的排序表。5.2 阈值决策与干预成本有了SLV之后干预策略不再是拍脑袋定阈值而是比较“干预成本”和“干预带来的增量现金流”。比如定向发放10美元优惠券只对SLV_12大于50的客户发放因为即使保留成功率只有20%期望收益也高于成本。实际操作中可以用分位数来圈定名单df_model[slv_12] (cph.predict_survival_function(df_model[feature_cols]) .loc[:12].sum(axis0).values * df_model[MonthlyCharges].values) df_model[risk_group] pd.qcut(df_model[slv_12], q4, labels[low, mid-low, mid-high, high]) df_model.groupby(risk_group, observedTrue)[slv_12].describe()这段代码把全体客户按SLV分成四组。运营可以只对high组设计保留计划对low组采取低成本自助化触达既避免打扰原本就稳定的客户又把预算集中在现金流大的名单上。在这个数据集上跑通整个流程后可以顺手把Notebook提交到Kaggle作为公开基线。面试时讲这个项目不用强调准确率多高而是讲清楚“为什么二分类不是这个问题的最优表达”“删失数据如何处理”“C-index和AUC的适用边界”这几句话比一张高分的混淆矩阵更能体现建模功底。最后留一个常用的小技巧在Notebook末尾用cph.plot()输出系数森林图并配上check_assumptions的截图一起存档。Kaggle提交页面的版本历史会自动保留每次改动回看实验记录时对比不同penalizer和strata设置下的系数变化比看散落在本地终端的日志直观得多。把这一段连同数据和模型代码整理成公开Notebook本身就是一份可复现的生存分析流失预测样板。本文还有配套的精品资源点击获取
返回列表