ARTICLE DETAIL

资讯详情

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

用生存分析做电信客户流失预测:从“谁走”到“何时走”

用生存分析做电信客户流失预测:从“谁走”到“何时走” 简介基于Kaggle平台电信客户流失数据集利用生存分析方法构建客户流失预测模型适合具备一定机器学习基础、希望进阶学习生存分析在商业场景中应用的数据科学学习者。分析内容以交互式文档为核心完整呈现从数据探索、缺失值处理、特征编码到KM生存曲线绘制、Log-Rank检验以及Cox比例风险回归的建模全流程每个环节均配有可运行代码与必要解释同时提供独立脚本便于在本地环境直接复现和扩展。压缩包内共7个文件除核心分析代码与脚本外还包含原始CSV数据集、Markdown说明文档、数据来源链接以及许可文件目录清晰、用途明确。整个资源包仅186KB下载与存储都非常轻量。目前已有567人学习浏览适合希望借助真实电信业务数据快速理解生存分析原理、掌握流失预测方法的数据分析学习者。1. 用生存分析做电信客户流失预测多回答一个什么时候走同样的Kaggle电信客户流失数据集换一套建模思路能比普通分类模型多回答一个问题。随机森林、XGBoost做流失预测输出的是一张谁可能走的名单而生存分析输出的是他在第几个合同期走、用了多久开始想走、什么时间点干预性价比最高。对于做续费运营和用户挽留的团队来说后者才是真正能排进日历表的动作依据。这篇笔记从Kaggle平台下载电信客户流失数据集开始用Python的lifelines库把生存分析完整跑一遍覆盖数据处理、模型训练、评估和分层干预名单的生成重点写清楚哪些参数能调、哪些坑我替你先踩过了。2. 生存分析为什么适合流失预测从会不会走到多久会走先建立概念框架。流失预测本质上是一个事件发生时间time-to-event问题生存分析恰好是专门处理这类问题的统计框架它把流失重新定义为一种随在网时长变化的风险。2.1 生存函数、风险函数和删失换一套语言看用户生命周期生存分析里有三个基础概念理解它们之后再看数据会完全不同。生存函数 S(t) 表示用户存活到时间 t 之后仍未流失的概率这是一条从1开始、随时间单调下降的曲线。风险函数 h(t) 表示在存活到时刻 t 的条件下下一瞬间发生流失的瞬时概率它不一定是单调的电信场景里往往在合约到期前后出现明显的峰值。第三个概念是删失censoring这是生存分析和普通分类任务最本质的差异。在这个数据集中Churn 0 的用户并不是永远不会流失而是在观测窗口结束时仍然没有流失。他们的真实流失时间未知只知道至少存活了这么久。这种类型叫右删失right censoring占了数据集的绝大多数。常规分类模型把这一类样本当作负样本丢掉时间信息逻辑上是有损失的。概念数学符号电信场景含义生存函数S(t)用户在网超过 t 个月仍未流失的概率风险函数h(t)在网第 t 个月的瞬时流失强度右删失—观测期结束时用户仍在网真实流失时间未知流失预测换成生存分析的视角后输入变成了 (时间 T, 事件标记 E)输出变成了整条生存曲线。这个结构上的变化决定了你能回答的问题类型完全不同。2.2 为什么选Cox比例风险模型而不是XGBoost明确一下选型逻辑。XGBoost、LightGBM 做流失概率预测效果不差但在用户生命周期这个语义下有两个短板。第一它们输出的是静态概率无法直接回答第6个月风险最高这类时间维度的结论第二删失样本在常规分类里很难优雅处理——直接丢掉会损失信息标成负样本又引入偏差。Cox比例风险模型Cox Proportional Hazards Model是生存分析领域最常用的半参数模型它假设风险函数可以拆成基准风险 h0(t) 和协变量效应的乘积h(t|X) h0(t) × exp(β₁X₁ β₂X₂ ...)协变量部分不依赖时间模型不关心基准风险的形状因此拟合时不需要对生存时间分布做假设。这个半参数特性很关键——电信用户流失的时间分布往往不是简单的指数分布或Weibull分布早期活跃流失和合约到期流失混合在一起参数模型容易过度约束Cox能避开这个坑。它的输出有三个层次每个特征的系数 β对应风险比 exp(β)每个样本的生存曲线预测以及风险分数。这三个输出分别对应策略层、个体层和排序层覆盖面足够广。可解释性也是一个现实考量。流失预警系统通常需要向业务方解释为什么给这批用户发优惠券Cox模型的风险比可以直接说月租每增加一档流失风险提升多少倍这是树模型和深度学习模型给不了的。但是Cox模型的前提是比例风险假设PH假设也就是协变量对风险的影响不随时间变化。如果某个特征明显违反这个假设后文会给出具体的检测和修正方法。2.3 lifelines和scikit-survival两个库怎么选Python生态里做生存分析lifelines和scikit-survival是两个主流选择。lifelines的使用体验最接近统计建模习惯API设计偏R语言风格内置CoxPHFitter、KaplanMeierFitter、NelsonAalenFitter而且自带PH假设检验、残差诊断这些模型体检功能做小型数据集探索和业务分析很顺手。scikit-survivalsksurv的优势在于和scikit-learn生态的集成它的CoxnetSurvivalAnalysis支持L1/L2正则化适合特征维度较高的场景。但它对数据格式要求较严苛——时间列必须是结构化数组np.array([(time, event)], dtype[(time, float), (event, bool)])新手容易在这里踩坑。我一般用lifelines做数据分析因为电信客户流失数据集只有二十个左右的特征不需要上正则化生存模型lifelines的诊断工具更成熟。唯一要注意的是lifelines 0.27版本之后CoxPHFitter的输入必须是干净的pandas DataFrame梳理好的数据可以直接喂进去不用额外指定时间列名和事件列名的类型。3. 从Kaggle拉取电信客户流失数据集注册、下载与字段盘点很多人的第一道坎不是建模而是怎么把数据从Kaggle上拿到本地。这里把完整流程和踩坑点一次说清。3.1 Kaggle注册弹不出验证码高频报错的处理办法Kaggle注册时最常见的问题是验证码不显示或者填写后提示 captcha must be filled out。这不是你操作的问题多数情况下是浏览器对Google reCAPTCHA脚本的加载限制或者网络环境本身对Google域名的访问不稳定。遇到验证码弹窗不出来先不要反复刷新页面。打开浏览器开发者工具F12切到网络面板刷新注册页看是否有指向www.google.com/recaptcha的请求被拦截或被挂起。确认是这类加载问题后换一个浏览器内核差异较大的环境比如Chrome不行就换Firefox或者检查系统级代理设置。注册页面本身不需要额外插件不要把时间浪费在找特殊脚本上。如果你的网络环境访问Google验证码服务确实不稳定还有一个折中方案直接在Kaggle页面右上角选择Continue with GitHub或邮箱注册入口邮箱注册流程有时候能绕过验证码强制校验。注册完先在个人主页确认邮箱验证通过再进API设置页生成Token。3.2 下载数据集网页直下和API两种方式对比Kaggle下载电信客户流失数据集有两种方式场景不同选法不同。网页方式最直观登录后搜索 Telco Customer Churn进入数据集页面点右上角Download按钮浏览器会开始下载一个约几百KB的压缩包。这个数据量不大网页下载完全够用不需要纠结API。API方式适合你后续还有多数据集下载需求的情况。Kaggle官网账号设置里生成API Token即kaggle.json放到用户目录Windows是C:\Users\你的用户名\.kaggle\kaggle.json然后命令行执行pip install kaggle # 安装完成后把kaggle.json放到 ~/.kaggle/ 目录Windows路径同上 kaggle datasets download -d blastchar/telco-customer-churn # 解压 unzip telco-customer-churn.zip说明一下每个步骤的作用。pip install kaggle安装官方命令行工具它会读取~/.kaggle/kaggle.json里的用户名和API Key来完成身份认证。datasets download -d指定数据集标识blastchar/telco-customer-churn是这个数据集的规范路径格式如果你在网页端打开数据集详情页URL路径里的那串字符串就是它的唯一标识。注意API下载默认不覆盖已存在的同名压缩包重复执行时会看到403或者already exists类的提示需要先删掉旧的zip再重新下载。另外kaggle.json文件的权限在Linux/Mac下要求是600仅当前用户可读写否则API会报认证错误这一点文档里不会主动提醒。3.3 字段盘点与关键的时间变量构造下载完成后通过pandas读入并检查结构import pandas as pd df pd.read_csv(WA_Fn-UseC_-Telco-Customer-Churn.csv) print(df.shape) # 输出 (7043, 21) print(df.dtypes) print(df.head())这个数据集有7043条记录、21个字段。其中tenure字段是用户在网月数Churn字段是是否流失标志这两列是做生存分析最重要的输入——前者对应事件时间T后者对应事件标记E。但这里有一个大多数教程没讲透的点原始数据的Churn列是字符串Yes/No必须先转换成数值。更重要的是要意识到数据集的观测窗口对每个用户并不同时结束我们没有合同到期时间这类更细的日历时间只能用tenure作为生存时间的近似。这在电信流失分析里是可以接受的常见近似做法但后文会讲到它带来的一个隐蔽坑。字段层面还需要做一次类型校正TotalCharges列本身是数值型但读入时如果有空值会被pandas识别为object类型。需要先处理掉空值这个数据集只有个别行的TotalCharges为空占比极小再pd.to_numeric转换。MonthlyCharges和TotalCharges存在强相关做Cox模型时建议保留MonthlyChargesTotalCharges可以丢弃或做特征组合两个都放进去会有共线性问题。# 处理Churn标记字符串转数值 df[Churn_Flag] df[Churn].map({Yes: 1, No: 0}) # TotalCharges类型校正 df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) df df.dropna(subset[TotalCharges]) # 时间列直接使用tenure事件列使用Churn_Flag T df[tenure].values E df[Churn_Flag].valueserrorscoerce参数会把无法解析的非数值串转为NaN这一步宁可让数据少几行也不要让模型吃掉脏数据。到这里数据准备工作就完成了核心是把是否流失换成(时间, 事件)二元组。4. 用lifelines跑通Cox流失预测完整代码与参数解释数据就绪后开始建模。先用Kaplan-Meier方法画整体生存曲线快速感知流失节奏再训练Cox模型进入正式的流失预测流程。4.1 Kaplan-Meier生存曲线先看整体流失节奏from lifelines import KaplanMeierFitter kmf KaplanMeierFitter() kmf.fit(durationsT, event_observedE, label整体用户) kmf.plot_survival_function() # 查看中位生存时间 print(kmf.median_survival_time_)fit()接受最低限度的两个参数durations是观测时间数组event_observed是0/1标记数组。模型内部会维护一张生存概率随时间变化的表曲线每一步下降的位置对应有用户在该时间点发生了流失。中位生存时间指生存概率降到50%的时刻这个数据集中通常是几十个月的量级——先从这一步感受用户整体生命周期再决定之后对时间窗口的分层尺度。拿到整体曲线后按合同类型分组比较是个能直接指导业务的切入点from lifelines import KaplanMeierFitter kmf_monthly KaplanMeierFitter() kmf_one_year KaplanMeierFitter() kmf_two_year KaplanMeierFitter() mask_monthly df[Contract] Month-to-month mask_one df[Contract] One year mask_two df[Contract] Two year kmf_monthly.fit(T[mask_monthly], E[mask_monthly], label月付) kmf_one_year.fit(T[mask_one], E[mask_one], label一年期) kmf_two_year.fit(T[mask_two], E[mask_two], label两年期) kmf_monthly.plot_survival_function() kmf_one_year.plot_survival_function() kmf_two_year.plot_survival_function()三条曲线在平台上几乎分离月付合同的生存曲线掉得最快一年期居中两年期最平缓。这一步的价值在于定性确认合约长度是最强的流失分层变量之一模型里如果没它或者它不显著大概率是编码或数据处理出了问题。4.2 Cox比例风险模型训练核心参数与理解from lifelines import CoxPHFitter # 准备建模特征去掉文本标签列和强共线性特征 drop_cols [customerID, Churn, Churn_Flag, TotalCharges] df_model df.drop(columnsdrop_cols) # 有序多分类变量做数值映射 df_model[Contract] df_model[Contract].map({ Month-to-month: 1, One year: 2, Two year: 3 }) # 二元分类变量做0/1映射 binary_cols [Partner, Dependents, PhoneService, PaperlessBilling] for col in binary_cols: df_model[col] df_model[col].map({Yes: 1, No: 0}) # 哑变量编码其余多分类变量 df_model pd.get_dummies( df_model, columns[InternetService, PaymentMethod, MultipleLines, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies, Gender], drop_firstTrue ) # 训练Cox模型 cph CoxPHFitter() cph.fit(df_model, duration_coltenure, event_colChurn_Flag, show_progressTrue, step_size0.5, penalizer0.1) cph.print_summary()逐段解释这段代码的意图。drop()删除的列里customerID是纯标识符Churn是流失标签原文Churn_Flag是事件标记本身三者都不能进特征矩阵。TotalCharges删除是因为与tenure和MonthlyCharges存在严重的线性相关同时放进Cox模型会让系数标准误会膨胀减弱可解释性。Contract映射成1/2/3后模型把合约时长当作连续风险因子处理。这里有个选择如果担心效应不是线性递增应该改用哑变量编码三个合约类型取两个哑变量让每个级别的风险独立估计。实际操作里我建议用哑变量因为月付→一年期→两年期的风险不一定是等差增长的。其余变量处理遵循一个原则二元变量直接映射0/1多分类变量用get_dummies且drop_firstTrue防止共线性陷阱。drop_firstTrue会为每个类别变量生成k-1列本质是选择一个类别作参照系。fit()里三个参数值得说。duration_col明确告诉模型哪一列是时间event_col是事件标记。step_size默认是0.1数据量较小或特征尺度差异大时调大一点能加快收敛但太大容易跳过最优解。penalizer是L2正则强度默认0当特征间存在较强相关性或想压低系数波动时才需要开启。这个数据集特征不算多penalizer0.1的目的是把稀疏哑变量带来的过拟合压住。print_summary()输出每个特征的coef、exp(coef)、标准误、z值和p值。exp(coef)大于1表示该特征增加流失风险小于1表示保护效应。比如Contract若系数为负说明合同越长风险越低这和KM曲线的观察一致。4.3 评估C-index与随时间变化的AUC生存模型的评估不能直接用准确率因为预测输出是生存曲线而非硬分类。最常用的判别指标是C-index一致性指数含义是随机挑一对可比较的样本模型判断出的风险排序和真实事件发生顺序一致的概率。C-index为1完美0.5等于随机。from lifelines.utils import concordance_index c_index concordance_index( df_model[tenure].values, -cph.predict_partial_hazard(df_model).values, df_model[Churn_Flag].values ) print(fC-index: {c_index:.4f})这里细节在第二参数是predict_partial_hazard的负值。predict_partial_hazard返回的是模型输出的风险分数风险分数越高表示越容易流失。而concordance_index默认认为数值越大越不容易发生事件所以要把风险分数取负再传入顺序颠倒了会得到一个1减去真实值的错误结果。C-index只评价排序质量不评价概率校准。如果你要回答预测在网第12个月的流失率是15%这个数字准不准需要看随时间变化的AUCtime-dependent AUClifelines里可以用lifelines.utils.survival_table_from_events配合scikit-learn的roc_auc_score自行计算也可以换用sksurv的cumulative_dynamic_auc。对大多数业务决策来说C-index在0.8以上就已经能支撑分层运营了不必追求极端高值排序能力够用、曲线可解释是关键。5. 电信流失预测的典型坑删失、零时、PH假设、业务解读这一章是血泪经验汇总。现象、原因、解决三步写有同样报错直接对照排查。5.1 现象模型跑出来C-index超过0.95高到可疑原因这不是模型厉害多半是时间列使用错误。tenure是合同期内累计在网月数而Contract和部分消费特征本身和tenure高度纠缠。更常见的问题是把TotalCharges留在特征里它由MonthlyCharges × tenure构成等于把一个线性派生的时间变量放进了模型模型几乎只是靠数学变换重算了一遍时间导致C-index虚高。解决TotalCharges这类由时间直接线性生成的列必须剔除。自查方法是删除该特征后重新训练如果C-index从0.95掉到0.8左右说明之前的好效果是泄漏而不是预测能力。判断有没有特征泄漏除了值本身还要看你手里有没有未来才能知道的信息。这个数据集的泄漏源就是TotalCharges去掉它模型才是在真正做预测。5.2 现象Cox模型输出警告Binned fit did not converge或系数全不显著原因特征里某个二元变量的类占比极度不平衡或者多个哑变量之间存在近似线性依赖。OnlineSecurity、TechSupport这类字段没有服务的档位占比很高哑变量生成后彼此相关性强导致信息矩阵接近奇异。解决先跑一遍相关性矩阵把相关系数超过0.8的特征删掉一组。然后检查print_summary()里的coef值如果不显著的系数大多是稀疏哑变量可以手工合并类别。比如把No internet service和No合并成一个未开通或不使用的语义减少哑变量数。合并后重新训练系数和标准误会健康很多。5.3 现象按预测风险分组画的生存曲线在中途交叉了原因Cox模型的比例风险假设被违反。这个假设要求任何两个用户的风险比随时间保持恒定。电信场景里最常见的违反者是Contract——月付合约用户早期流失剧烈存活下来的很快就变得稳定而长期合约用户早期风险很低但合约到期前后上升。两条风险曲线随时间变化的方向不一致生存曲线就会交叉。解决先做PH假设检验cph.check_assumptions(df_model, p_value_threshold0.05)lifelines会报告哪些变量违反了假设。标准的处理方案是把变量按时间分层strata对于违反PH假设的特征把它从协变量移到strata参数里让模型为它的每个取值单独估计基准风险函数而不再假设这个特征的风险比恒定。cph CoxPHFitter() cph.fit(df_model, duration_coltenure, event_colChurn_Flag, strata[Contract])分层后模型不再给出Contract的系数但其余变量的解释基本不变。一般来说只要核心业务特征的风险比稳定个别控制变量分层掉并不影响整体分析。5.4 现象KM曲线在时间点为0处直线下坠原因tenure为0的用户被当作事件直接纳入计算但他们在观测窗口开始时就已流失KM曲线在零点就会被拉低一段失真明显。解决检查df[tenure].min()如果存在0值判断这些用户的第0月流失是业务事实还是数据录入规则。如果是数据录入规则——比如入网登记和离网登记发生在同一个月标记为0——把这些样本的tenure统一改成0.5或者其他微小正值避免0参与时间比例运算。这个方法有争议但实操中很常见前提是你要能向业务方解释这个调整的含义不解释清楚就是玄学。5.5 现象生存概率和流失概率总是对不上业务质疑你们预测说30%实际只有12%在三个月内走了原因生存概率S(t)描述的是从入网开始存活超过t个月的概率它是从入网那一刻起算的累计量。业务要的未来3个月流失率应该是在当前已经存活到第12个月的条件下未来3个月内的流失条件概率P(第12~15月内流失 | 已存活到第12月) 1 - S(15) / S(12)不少初学者直接把1-S(12)当作第12月流失率这是概念错位。输出预测时必须先明确参考时间点再换算成业务口径。解决写一个转换函数输入当前存活时间和预测窗口长度输出窗口内的条件流失概率import numpy as np def conditional_churn_probability(cph, df_row, current_t12, window3): # 取该用户的生存函数 surv cph.predict_survival_function(df_row) times surv.index.values # 找最接近的两个时间点的生存概率 s_current np.interp(current_t, times, surv[0].values) s_future np.interp(current_t window, times, surv[0].values) return 1 - (s_future / s_current) prob conditional_churn_probability(cph, df_model.iloc[[0]], 12, 3) print(f未来3个月条件流失概率: {prob:.2%})np.interp做线性插值因为我们只需要近似值不需要精确到月插值精度足够。这段函数会成为一个标准化工具后续所有给业务方的数据都要过这一层转换避免口径争论。6. 把生存曲线变成续费干预名单分层阈值与校准验证风险评估的最终产物是一张可执行的名单不是一个C-index数字。以月付合约用户为例在训练好的Cox模型上对全体用户的生存曲线做预测然后生成三个层面的动作。第一层是短期风险名单。对每个用户取当前在网月数 未来30天的条件流失概率把概率从高到低排序超过某一阈值比如20%的进入高优先级唤醒名单推送优惠券或专属客服。第二层是中期风险名单。取未来90天的条件流失概率筛选出当前稳定但90天内有风险的用户这批人适合安排合约升级引导。第三层是曲线形状分析。对每个用户预测的生存曲线做一阶差分找到风险陡增的拐点月份把干预动作提前到拐点之前一个月。阈值怎么定画出条件流失概率的分布直方图找自然断档或者直接按运营承接能力控制名单量。假设客服团队每天能触达200人就把阈值调到名单人数恰好等于承接上限。最后做一次校准验证。把测试集用户按预测概率分桶比如0-10%、10-20%……计算每个桶内实际发生流失的比例画出校准曲线。如果实测值系统性高于预测值说明模型过度自信需要回去检查是否有什么高影响特征没编码完全如果实测值低于预测值可能删失处理过于激进把本来会在窗口外流失的用户错误标记成了删失。一个我自己沉淀下来的习惯模型输出永远不直接给流失概率而是给未来N天流失风险和风险等级。前者是条件概率后者是分层标签。这样做业务方不会拿生存函数去和当月实际流失率硬比减少大量解释成本。这套流程从Kaggle数据下载到名单输出在真实业务环境里大概率能直接复用希望帮到你。本文还有配套的精品资源点击获取
返回列表