ARTICLE DETAIL

资讯详情

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

基于生存分析的电信客户流失预测:从KM曲线到Cox回归实战

基于生存分析的电信客户流失预测:从KM曲线到Cox回归实战 简介这份资源以Kaggle电信客户流失数据集为基础面向希望掌握生存分析实战的数据分析与机器学习初学者完整演示了利用生存分析方法预测客户流失的整个流程。资源包共7个文件涵盖CSV格式的原始数据、Python脚本、Jupyter Notebook分析文档、说明文档与许可文件压缩包仅186KB轻量易用。数据集中包含客户编号、性别、是否老年、电话与网络服务、合同方式、月费用、总费用等20项字段可系统练习特征工程、数据清洗、多变量探索、Kaplan-Meier生存曲线绘制、Log-rank检验及Cox比例风险回归建模等关键步骤。Notebook中的代码与笔记相互对应方便逐步复现。目前已有566人学习下载适合作为客户流失预测或生存分析入门项目的参考资料能帮助读者快速建立分析框架理解生存分析在电信客户生命周期管理中的实用价值。1. 流失预测的本质是时间问题不是分类问题kaggle 上这份 WA_Fn-UseC_-Telco-Customer-Churn.csv 共 7043 行、20 个字段很多人拿到手直接跑 XGBoost 刷 AUC。但真正有信息量的是 tenure 那一列——客户已使用月份数。二分类把第 3 个月就跑和扛了 60 个月才走压成同一个 1时间信息全丢。生存分析的切入点是先把每个客户表示成 (T, E) 二元组T 是观测时长E 是流失是否发生没流失的样本作为右删失保留而不是当负样本扔掉。这样能回答二分类答不了的问题合同类型让流失风险翻几倍生存率曲线在哪个月出现拐点预测风险最高的四分之一客户中位存活期是多少电信、SaaS 续费、保单到期这类业务都适用。下面就从数据清洗开始把整套流程在 lifelines 上完整跑一遍。2. 生存分析数据清洗从原始 CSV 拆出 (T, E) 事件表2.1 先建立右删失的概念再动手Churn No 的样本不是没有发生事件而是观察窗口结束时还没发生事件。这类客户能够确认的信息是他至少存活了 tenure 个月之后可能流失也可能继续在网。这就是右删失。生存分析里删失样本在事件发生的各个时点仍然计入风险集分母直到它自己的观测终点才退出信息量因此被完整保留。如果按二分类的思路把它们简单归为负类等于人为截断了观测时间后续所有风险估计都会偏乐观。理解这一点才能解释为什么同一个 CSV 在两种建模框架下的预处理方式完全不同。2.2 三个必须处理的字段字段原始类型典型问题处理方式tenureint无原样作为 duration_colChurnobjectYes/No 字符串map 成 1/0作为 event_colTotalChargesobject空字符串导致解析成字符串pd.to_numeric(errorscoerce) 后删缺失SeniorCitizenint64读入为数值原样保留建模时直接进特征TotalCharges 列里有 11 行是空白不处理的话 CoxPHFitter 在拟合时直接报 NaN 相关错误。这列本身近似等于 tenure 和 MonthlyCharges 的乘积删除这几行不会影响整体分布也省去后续插值的争议。SeniorCitizen 虽然是 0/1 编码但 read_csv 会把它读成 int64不需要再转 category。2.3 用 pandas 整理成 lifelines 可接受的 DataFrameimport pandas as pd df pd.read_csv(WA_Fn-UseC_-Telco-Customer-Churn.csv) df[Churn] df[Churn].map({Yes: 1, No: 0}) df[tenure] df[tenure].astype(int) df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) df df.dropna(subset[TotalCharges]) survival_df df.drop(columns[customerID]) print(survival_df.shape, round(df[Churn].mean(), 3))这段代码完成四件事把事件指示列转成数值型、把持续时间列固定为 int、把金额列从字符串解码成 float、删掉 customerID 主键。输出里 Churn 均值约 0.26说明整体流失率接近四分之一但这个静态比例的意义有限生存分析关心的是生存概率随 tenure 的衰减形状。压缩包里附带的 ipython 分析.ipynb 逐列做过探索README.md 有完整字段说明动手前扫一遍能省掉不少排查时间。提示tenure 为 0 的客户是当月入网当月流失lifelines 允许 0 作为观测时长只是画曲线时起始点会从第 0 个月开始个别项目希望曲线从第 1 个月起步可以统一加 0.5 个月偏移对结论影响很小。3. Kaplan-Meier 分层曲线与 log-rank 检验先切合同与支付方式3.1 整体生存曲线怎么读KM 估计量是不带协变量的非参数方法对每个事件时点计算当前风险集里未流失比例的连乘S(t) Π_{t_i ≤ t} (1 - d_i / n_i)。d_i 是 t_i 时点流失人数n_i 是该时点仍处在风险集里的人数。注意删失样本在事件时点还留在分母里它退出风险集和流失退出是两种不同的机制这正是 KM 与普通存活率统计的核心差异。from lifelines import KaplanMeierFitter kmf KaplanMeierFitter() kmf.fit(survival_df[tenure], event_observedsurvival_df[Churn], labelall) print(kmf.median_survival_time_) kmf.plot_survival_function()median_survival_time_ 是生存曲线首次跌破 0.5 的位置这份数据复现出来的中位生存期通常在 3 年上下比 26% 的静态流失率更有运营含义。kmf.confidence_interval_ 是 Greenwood 公式给出的对数尺度区间样本量小的尾部会明显变宽读 60 个月之后的区间要记得看上下界而不是只看点估计。fit 的两个关键入参是持续时间列和事件指示列删失信息完全由这两列的组合表达。3.2 用 log-rank 检验比较合同类型分层做 KM 不能只靠眼睛看差异log-rank 检验给出一个零假设为两条生存曲线相同的统计量。按 Contract 分成月付与非月付两组from lifelines.statistics import logrank_test monthly survival_df.query(Contract Month-to-month) annual survival_df.query(Contract ! Month-to-month) res logrank_test( monthly[tenure], annual[tenure], event_observed_Amonthly[Churn], event_observed_Bannual[Churn], ) print(res.p_value)两个核心入参是两组的持续时间数组和事件指示数组。p 值远小于 0.001说明两组生存曲线的差异不是抽样波动。配合 KM 图看月付组的曲线在 6 到 18 个月掉得最快这正是合同保护期结束后流失集中爆发的窗口。log-rank 的局限是只回答是否不同不回答差多少倍量化差异要交给第 4 章的 Cox 模型。3.3 支付方式分层观察对 PaymentMethod 的多个类别分别估计 KM 曲线是建模前最便宜的探索方式分层维度分组观察重点ContractMonth-to-month / One year / Two year前 12 个月的曲线分离度PaymentMethodElectronic check / 其余三种20 个月以后的尾部落差InternetServiceDSL / Fiber optic / No光纤组是否存在更陡的下降段实际跑下来Electronic check 组的生存曲线在 20 个月以后明显低于自动转账和信用卡组这部分客户对自动扣款有抵触或存在绑卡问题和 Contract 的交互也很强。这一步的价值是给 Cox 模型的变量清单排序强分层的变量必须进分层不明显的变量可以放到后面再加。4. Cox 比例风险回归把流失风险拆成可解释因子4.1 为什么 KM 分层解决不了多变量问题KM 一次只能切一个离散变量数据里同时存在 Contract、PaymentMethod、InternetService、OnlineSecurity 等多个因子还有 MonthlyCharges 这样的连续变量。Cox 模型把风险拆成两部分h(t | X) h0(t) * exp(Xβ)。h0(t) 是基准风险函数不假设任何分布形态所以 Cox 是半参数模型exp(Xβ) 是携带协变量后的相对风险倍数。每个变量的效应被独立估计出来输出是运营能直接用的风险比 HR这是 KM 分层给不了的东西。4.2 lifelines 建模与变量选择from lifelines import CoxPHFitter model_df survival_df.drop(columns[PhoneService, MultipleLines]) cph CoxPHFitter() cph.fit( model_df, duration_coltenure, event_colChurn, formula( C(Contract) C(PaymentMethod) C(InternetService) C(OnlineSecurity) C(OnlineBackup) C(DeviceProtection) C(TechSupport) C(StreamingTV) C(StreamingMovies) SeniorCitizen Partner Dependents MonthlyCharges ), ) cph.print_summary()删掉 PhoneService 和 MultipleLines 是因为前者几乎没有区分度、后者与其强相关留着会放大方差。C() 是 lifelines 的哑变量语法以字母序第一个类别为参照组。duration_col 和 event_col 对应第 2 章准备好的 tenure 与 Churn这两列的语义必须准确否则风险集计算从一开始就是错的。4.3 风险比的解读边界print_summary 直接读 exp(coef) 列变量复现常见区间业务解读ContractOne year / Two year0.3 ~ 0.5长合同把流失风险压掉一半以上PaymentMethodElectronic check1.3 ~ 1.6电子支票是支付方式里风险最高的一组InternetServiceFiber optic1.7 ~ 2.5光纤组风险显著偏高和资费及迁移成本相关TechSupportYes0.5 ~ 0.7购买技术支持的客户粘性更强MonthlyCharges 每 1 美元1.01 以内价格敏感但影响量级小置信区间跨 1 的变量不要急着下结论p 值小于 0.05 才谈得上统计显著。coef 列本身是 log HR建模时要同时看标准误和置信区间7000 行样本下很多交互项会因稀疏而不稳定不要被点估计带偏。4.4 比例风险假设的第一次体检cph.check_assumptions(model_df, p_value_threshold0.05)这步基于 scaled Schoenfeld 残差逐变量检验风险比是否恒定输出里会标出违反假设的变量和随时间变化的趋势。这份数据跑下来tenure、MonthlyCharges、Contract 通常都会被点名月付客户的风险倍率在早期和后期完全不同这是业务逻辑在统计上的体现。直接忽略继续解释 HR得到的是误导性的平均效应第 6 章给出两条处理路线。5. 模型评价与风险分层用 C-index 和 KM 曲线验证排序能力5.1 C-index 是生存版的 AUC二分类看 AUC生存模型对应的一致性指数print(C-index:, cph.concordance_index_)C-index 的含义是任意抽两个客户模型对两人风险高低的排序与真实流失先后一致的概率0.5 等于随机。这份数据用原始特征拟合复现值一般在 0.82 到 0.85。它不能和分类模型的 AUC 直接比因为两者回答的问题不同C-index 度量时序排序AUC 度量静态判别。想严格对齐要用 time-dependent AUC 或 integrated Brier scorescikit-survival 里的 cumulative_dynamic_auc 是常用选择。5.2 用预测风险做四分位分层C-index 是全局指标落地要看分层是否真的拉开。把 predict_partial_hazard 的输出分四档逐档画 KMimport numpy as np import pandas as pd from lifelines import KaplanMeierFitter model_df model_df.copy() model_df[risk_score] cph.predict_partial_hazard(model_df) model_df[risk_group] pd.qcut( model_df[risk_score], q4, labels[Q1_low, Q2, Q3, Q4_high], ) km_risk KaplanMeierFitter() for group in [Q1_low, Q2, Q3, Q4_high]: sub model_df[model_df[risk_group] group] km_risk.fit(sub[tenure], event_observedsub[Churn], labelgroup) km_risk.plot_survival_function()predict_partial_hazard 返回 exp(Xβ)是相对风险分数不含基准风险不能当绝对概率用。pd.qcut 按分位数切成四等份各组样本量基本均匀比人工定阈值稳定。复现时 Q4 和 Q1 的中位生存期通常能拉开 15 个月以上这个差距可以直接换算成续费干预的优先级和触达频次。5.3 和二分类方案明确分工逻辑回归或 LightGBM 做流失概率不是不行而是回答不同层次的问题方案输出核心问题推荐指标Logistic / LightGBMP(Churn)哪些客户会流失AUC / PrecisionKKaplan-MeierS(t)整体存活趋势中位生存时间CoxPHHR / risk_score因子影响与时点C-index / log-rank p一线落地时我一般让二分类先圈高概率名单再用 Cox 的生存函数决定这批人的干预时点比如预测 90 天内存活率跌破 0.5 的客户才触发挽留动作。两个模型并行而不是二选一避免把时间信息白白扔掉。6. PH 假设违背的两条出路分层基线与时间交互项6.1 用 strata 消化离散变量的非比例效应PH 假设被违背意味着变量的风险比随时间变化最简单可靠的对策是把该变量移出系数项让它拥有各自的基准风险函数。Contract 是典型年付客户前 12 个月处于合同保护期和月付客户的风险基线完全不在一个量级。续跑前先把第 5 章加进 model_df 的 risk_score、risk_group 两列删掉避免它们混入协变量。cph_strata CoxPHFitter() cph_strata.fit( model_df, duration_coltenure, event_colChurn, strata[Contract], ) cph_strata.print_summary()strata 影响的是基准风险函数分层系数仍然全样本共享所以 print_summary 里 Contract 不再作为变量出现。对比原模型C-index 可能持平或略降但 log-likelihood 会改善说明模型更贴近真实生成过程。这个技巧对 PaymentMethod 这种无序分类同样适用代价是每层估一条基线层数太多会稀释样本一般控制在一到两个维度。6.2 用 tt 交互项处理连续变量的时变效应MonthlyCharges 没法分层常见做法是让系数随时间变化lifelines 的 tt 参数支持自定义交互函数。提示tt 函数里引用的列不能再同时出现在 formula 中否则会报 A variable is in both the tt and non-tt terms。import numpy as np from lifelines import CoxPHFitter def mc_log_t(time, var): return np.log(time 1e-8) * var[MonthlyCharges] cph_tt CoxPHFitter() cph_tt.fit( model_df, duration_coltenure, event_colChurn, formula( C(PaymentMethod) C(InternetService) C(OnlineSecurity) C(OnlineBackup) C(DeviceProtection) C(TechSupport) SeniorCitizen Partner Dependents ), tt[mc_log_t], ) cph_tt.print_summary()tt 函数接收两个参数时间数组和完整 DataFrame 切片返回一个交互项向量。这里建模的是 log(t) 与 MonthlyCharges 的乘积含义从每多 1 美元风险乘 1.01变成在网越短、价格越敏感系数要两项合起来解释。拟合后再跑一次 check_assumptionsMonthlyCharges 那一行通常不再报警。tenure 本身如果持续违规先检查是否有大量当月入网当月流失的样本堆积必要时对 tenure 取对数重新拟合比盲目加交互项更干净。把所有候选模型放在同一份测试集上比较 C-index 与 log-likelihood选结构最简且两个指标都不差的版本上线。本文还有配套的精品资源点击获取
返回列表