☰
电信客户流失预测:朴素贝叶斯实战全流程解析
2026/9/25 1:58:58 网站建设 项目流程

简介:面向电信行业数据分析与数据挖掘初学者,本资源提供一套完整的基于朴素贝叶斯算法的电信客户流失分析预测模型。原始数据取自Kaggle开源社区,含7043条客户记录、21项属性,可细分为客户个人信息、账户信息、订阅服务与流失评价指标四类:例如性别、老年客户、合作伙伴等基础画像,合同期限、付费方式、月消费等账户行为,以及电话、网络、在线安全保障等服务属性,便于直接开展特征理解与分类建模。压缩包共9个文件、3.1MB,核心内容包括CSV/TSV数据文件、Jupyter Notebook完整代码、HTML运行结果、TensorBoard训练日志及8000字实验报告,兼顾复现流程与文档解读。已有297人学习下载,适合课程设计、毕业设计或入门实战演练;使用者可获得可复用代码、实验数据与撰写范本,快速掌握数据预处理、模型训练和结果分析的关键步骤。

1. 电信客户流失分析预测模型:朴素贝叶斯在数据挖掘实战里怎么用

基于朴素贝叶斯算法的电信客户流失分析预测模型,是数据挖掘课程里出现频率最高的实战题目之一,也是电信运营里真正能落到业务上的分类场景。但多数教程只讲算法推导,不讲数据怎么收拾。这份资源在 7043 条客户样本上跑完了完整流程:从 Kaggle 原始数据清洗、21 个字段分类、特征编码,到模型训练、结果评估,附带可运行的 ipynb 代码和 8000 字实验报告。适合三类人:课程设计要交完整报告的学生、刚入门分类算法想找全程范例的初学者、想在电信场景验证朴素贝叶斯效果的从业者。核心价值在于每步操作和参数都摊开写在代码里,照着复现就能出结果,不用自己拼凑零散教程。

2. 数据预处理:把 7043 条样本和 21 个字段整理成模型能吃的形状

2.1 字段分类与业务含义:四组特征决定编码策略

拿到 data.csv 之后别急着建模,先花十分钟把 21 个字段按业务含义分组,这个动作直接决定后面的编码方案。数据集来自 Kaggle 的电信客户流失公开数据,7043 条样本,每条记录一位客户的套餐和消费情况。21 个属性大致分成四组:个人信息(CustomerID、gender、SeniorCitizen、Partner、Dependents),账户信息(tenure、Contract、PaperlessBilling、PaymentMethod、MonthlyCharges、TotalCharges),订阅服务(PhoneService、MultipleLines、InternetService、OnlineSecurity、OnlineBackup、DeviceProtection、TechSupport、StreamingTV、StreamingMovies),以及目标变量 Churn。

分组不是走形式。个人信息里的性别、是否老年人,和流失概率是弱相关;账户信息里的 tenure、Contract 是强信号——在网时间越短、月付合约,流失倾向越高;订阅服务里的 OnlineSecurity、TechSupport 能反映客户对服务的依赖度。不同组的字段类型差异很大,tenure、MonthlyCharges、TotalCharges 是连续数值,gender、Partner 这类是二值,InternetService、PaymentMethod 是多值无序类别,编码策略必须分开处理。

分组字段取值特征编码建议
个人信息CustomerID唯一字符串直接删除
个人信息gender、Partner、Dependents二值 Yes/NoLabelEncoder 转 0/1
个人信息SeniorCitizen0/1保留原值
账户信息tenure、MonthlyCharges、TotalCharges连续数值标准化后直接使用
账户信息Contract3 种取值One-Hot
账户信息PaymentMethod4 种取值One-Hot
账户信息PaperlessBilling二值 Yes/NoLabelEncoder 转 0/1
订阅服务PhoneService 等 8 个二值字段Yes/NoLabelEncoder 转 0/1
订阅服务InternetService3 种取值One-Hot
目标ChurnYes/No映射 1/0

有个细节容易忽略:Kaggle 原始数据里 DeviceProtection、StreamingTV、StreamingMovies 字段名自带拼写错误(原始 CSV 里是 DeciveProtection、SteamingTV、SteamingMovies)。这是数据集的原始状态,读取时不用纠正,改了反而跟后续代码对不上。做数据挖掘项目,原始数据的别名要留个记录,报告里写清楚也是一处加分细节。

2.2 数据分布检查:流失和非流失客户差在哪

编码之前先看一眼两组客户的差异,这一步能验证数据是否符合业务直觉,也能帮你判断特征有没有被搞反。常用的方法是按 Churn 分组算连续变量均值,再对类别变量做交叉表。

import pandas as pd # 读取原始数据,先看类型和缺失 df = pd.read_csv('data.csv', encoding='utf-8') print(df.dtypes) print(df.isnull().sum()) # 按是否流失分组,看连续变量的均值差异 print(df.groupby('Churn')[['tenure', 'MonthlyCharges', 'TotalCharges']].mean()) # 合约类型与流失的交叉占比 print(pd.crosstab(df['Contract'], df['Churn'], normalize='index'))

逻辑说明:groupby 的 mean 能快速暴露连续变量和流失的关系。这个数据集里通常能看到流失客户的平均 tenure 明显低于不流失客户,月费则偏高,说明在网时间短、客单价高的客户更容易走。crosstab 加 normalize='index' 算的是行占比,能看出月付合约的流失率远高于年付和两年付。这些规律如果和业务常识对不上,说明数据清洗出了问题,要回头查字段映射。

这里同时也是查缺失的时机:isnull().sum() 打印出来,TotalCharges 大概率有几十个空值,原因下一节讲。

2.3 缺失值与类型转换:TotalCharges 的空值不是坏数据

TotalCharges 在原始 CSV 里是 object 类型,不是数值类型,原因是部分行存的是空字符串。这些空字符串来自 tenure 为 0 的新客户——刚入网还没产生账单,累计费用为空是正常业务状态,不是数据损坏。

# TotalCharges 转数值,无法解析的转成 NaN df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce') # 确认缺失数量,再和 tenure=0 的客户数对比 missing_charges = df['TotalCharges'].isnull() print(f'TotalCharges 缺失数: {missing_charges.sum()}') print(f'tenure=0 的客户数: {(df["tenure"] == 0).sum()}') # 按业务逻辑填充为 0 df.loc[missing_charges, 'TotalCharges'] = 0 # 删除 CustomerID,它对分类预测没有贡献 df = df.drop(columns=['CustomerID'])

pd.to_numeric 加 errors='coerce' 会把空字符串转成 NaN,缺失数量一下就暴露了。关键是不要 dropna——直接删行会损失一批新客户样本,模型也学不到"新客户更容易流失"这个模式。按 0 填充保留了样本,tenure 本身的数值也能让模型区分新老客户。至于 CustomerID,每条记录一个唯一值,放进训练集只会让模型当噪声特征学,删掉干净。

提示:处理缺失前先打印缺失行和 tenure 的交叉情况,确认缺失是业务规则导致的,而不是随机丢失,再决定填充策略。

2.4 特征编码:LabelEncoder 与 One-Hot 的边界在哪

编码这一步的错误率最高,错在把无序类别当成有序类别处理。二值字段 gender、Partner、Dependents 这类 Yes/No,用 LabelEncoder 编成 0/1 没问题。但 PaymentMethod 有四种取值,本身没有大小关系,如果也用 LabelEncoder 编成 0、1、2、3,GaussianNB 会把相邻数值当成相似特征,概率估计被带偏。

from sklearn.preprocessing import LabelEncoder # 二值字段统一转 0/1 binary_cols = ['gender', 'Partner', 'Dependents', 'PhoneService', 'MultipleLines', 'OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies', 'PaperlessBilling'] label_enc = LabelEncoder() for col in binary_cols: df[col] = label_enc.fit_transform(df[col]) # 多值无序字段 One-Hot,drop_first 去掉一列避免冗余 multi_cols = ['InternetService', 'Contract', 'PaymentMethod'] df = pd.get_dummies(df, columns=multi_cols, drop_first=True) print(f'编码后特征数量: {df.shape[1]}') print(df.head())

逻辑说明:二值字段编码顺序无所谓,模型只认 0 和 1 两个状态。多值无序字段必须 One-Hot,Contract 虽然看起来有顺序(月付、年付、两年付),但朴素贝叶斯本身不做顺序假设,One-Hot 更稳妥。drop_first=True 对三个多值字段各去掉一列,特征矩阵少三列冗余,条件概率也少算几组。编码完成后特征数量从 20 左右涨到 20 多,这是正常的 One-Hot 结果。

还有个业务细节:MultipleLines 在原始数据里除了 Yes/No 还有 "No phone service" 取值,它和 PhoneService=No 是对应关系。LabelEncoder 会把三个取值当作三个独立状态处理,对朴素贝叶斯来说没问题,两个字段的相关性由模型自己去学,不需要手动合并。

3. 朴素贝叶斯原理:条件独立假设为什么能在流失预测里立足

3.1 从贝叶斯公式到流失概率:先验 26% 是怎么来的

朴素贝叶斯的出发点是贝叶斯公式:

P(Churn=Yes | X) = P(X | Churn=Yes) × P(Churn=Yes) / P(X)

P(Churn=Yes) 是先验概率,不看任何特征时客户流失的概率。这个数据集里流失客户大约占四分之一,约 26%。P(X | Churn=Yes) 是似然,表示在流失客户群体里出现特征组合 X 的概率。P(X) 是归一化常数,所有类别共用同一个值,分类时可以直接忽略。朴素贝叶斯算的是后验概率 P(Churn=Yes | X),超过阈值就判为流失。

"朴素"二字体现在一个强假设:给定类别后所有特征条件独立。于是 P(X | Churn=Yes) 拆成每个特征条件概率的乘积:

P(X | Churn=Yes) = P(x1 | Churn=Yes) × P(x2 | Churn=Yes) × ... × P(xn | Churn=Yes)

这个假设在电信数据里明显不成立:用了光纤宽带(InternetService=FTTH)的客户大概率同时没买 OnlineSecurity,两个特征有业务相关性。但实战里朴素贝叶斯依然能打,原因是分类任务关心的是后验概率的相对排序,不是绝对数值。特征相关性主要扭曲概率幅值,对排序影响有限,这就是朴素贝叶斯"理论上有硬伤、实战中能用"的玄学之处。

举个例子帮助理解。假设训练集中流失客户有 1800 人,其中月付合约 1500 人,那么 P(月付 | 流失) ≈ 0.83;不流失客户 5200 人,其中月付合约 2500 人,P(月付 | 不流失) ≈ 0.48。新来一个月付客户,两个似然乘上各自的先验,再归一化,流失概率就明显高于不流失概率。这个计算过程完全可解释,每一步都能在报告里写清楚,这是朴素贝叶斯比黑匣子模型更适合课程设计的原因。

3.2 三种变体怎么选:Gaussian、Bernoulli、Multinomial

sklearn 里朴素贝叶斯有三个常用变体,选哪个取决于特征类型。

变体分布假设适用特征关键参数
GaussianNB正态分布连续数值var_smoothing
BernoulliNB二值分布0/1 字段alpha
MultinomialNB多项式分布计数特征alpha

GaussianNB 用每个类别下特征的均值和方差估计条件概率,适合 tenure、MonthlyCharges、TotalCharges 这类连续字段。BernoulliNB 假设特征是二值 0/1,适合 gender、Partner、Dependents 这类 Yes/No 字段。MultinomialNB 面向计数特征,常用于文本词频统计,电信流失数据里没有合适的计数特征,基本用不上。

这份资源里的 ipynb 用 GaussianNB 直接吃全特征矩阵,二值字段也按连续值处理。优点是代码短、流程简单;缺点是二值特征的 0/1 分布离正态分布很远,概率估计有些偏差。想更讲究的话,可以把连续字段和二值字段拆开,分别训练 GaussianNB 和 BernoulliNB,选测试集 AUC 更高的那个。对课程设计来说,GaussianNB 直接跑已经足够,报告里解释一句"二值特征近似按连续值处理"就能说清楚。

3.3 拉普拉斯平滑与 var_smoothing:零概率问题的后悔药

条件概率计算有个经典坑:某个特征取值在某类别里从未出现,P(xi | Churn) 就是 0,连乘之后整个后验概率归零。比如训练集里所有流失客户都是月付合约,那"年付合约 + 流失"组合的概率就是 0,年付费客户会被武断判为不流失。拉普拉斯平滑给每个计数分子加一个 alpha:

P(xi | class) = (count_i + alpha) / (count + alpha × n_values)

alpha 默认 1.0,属于中等强度的起步值,越大概率越往均匀方向拉。比如某特征取值在流失类里出现 5 次,流失类总样本 1800,n_values 取 2 时,平滑后概率是 (5+1)/(1800+2) ≈ 0.0033,而不是 5/1800 里的零概率。这个参数就是你处理零概率问题的后悔药,换成 BernoulliNB 或 MultinomialNB 时记得显式传 alpha。

GaussianNB 没有 alpha 参数,对应的是 var_smoothing,默认 1e-9。它往每个特征的方差上加极小值,防止方差为 0 时概率密度爆炸。如果发现预测概率全挤在 0 或 1 附近,先别怀疑模型,试着把 var_smoothing 调到 1e-5 甚至 1e-3,看概率分布是否恢复合理。这两个参数是你在 ipynb 里最常需要动的地方。

4. 模型训练与预测:跑通 ipynb 的完整流程和参数设置

4.1 数据集划分:stratify 分层抽样保住流失比例

划分数据集是第一个决定结果可复现性的环节。流失样本只占四分之一,如果不分层抽样,随机划分的测试集流失占比可能忽高忽低,后面所有指标都没法对比。

from sklearn.model_selection import train_test_split # X 去掉目标列,y 取 Churn 并映射为 0/1 X = df.drop(columns=['Churn']) y = df['Churn'].map({'Yes': 1, 'No': 0}) # 分层抽样:训练集和测试集都保持约 26% 的流失占比 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) print(f'训练集样本数: {X_train.shape[0]}, 流失占比: {y_train.mean():.4f}') print(f'测试集样本数: {X_test.shape[0]}, 流失占比: {y_test.mean():.4f}')

stratify=y 是最容易漏的参数。加了之后训练集和测试集的流失占比都会稳定在 26% 左右,模型评估才有可比性。random_state=42 固定随机种子,保证每次运行划分结果一致——这是报告里数字能复现的前提。test_size=0.25 是常见比例,7043 条样本切出约 1760 条测试集,足够评估用。如果你觉得测试集太小,可以用 0.2,但样本量变化会影响后续指标的稳定性,选定一个就不动。

提示:想复现实验结果,random_state 必须写进报告。换了种子结果可能差一到两个百分点,评审老师最常问的就是这个。

4.2 训练 GaussianNB:参数与预测结果保存

from sklearn.naive_bayes import GaussianNB from sklearn.metrics import classification_report, confusion_matrix # 高斯朴素贝叶斯,var_smoothing 先用默认值 1e-9 model = GaussianNB() model.fit(X_train, y_train) # 测试集预测 y_pred = model.predict(X_test) y_proba = model.predict_proba(X_test)[:, 1] # 输出评估结果 print(classification_report(y_test, y_pred, target_names=['不流失', '流失'])) print(confusion_matrix(y_test, y_pred)) # 保存预测明细,后续业务分析要用 result = X_test.copy() result['Churn_actual'] = y_test.values result['Churn_pred'] = y_pred result['Churn_prob'] = y_proba result.to_csv('result.csv', index=False)

predict 返回 0/1 类别,predict_proba 返回每个类别的概率,取 [:, 1] 就是不流失。做业务分析一定要用概率列,而不是只看类别标签。result.csv 同时保留实际值、预测值和概率,方便后面做阈值扫描和流失名单排序。

classification_report 里重点看流失类别(正类)的召回率,它代表"真正会流失的客户被抓住多少"。这里的整体准确率看着还行,但这是因为不流失客户占多数,模型即使全部判不流失也有约 74% 的基线准确率。所以评估必须结合正类的精确率和召回率一起看,不能只盯 accuracy。

4.3 阈值调整:0.5 不是唯一选择

朴素贝叶斯默认按后验概率是否大于 0.5 判类别,但流失数据不平衡,0.5 未必是最优分界。

from sklearn.metrics import f1_score import numpy as np # 扫描阈值,选出 F1 最高的分界点 best_f1 = 0 best_thr = 0.5 for thr in np.arange(0.2, 0.8, 0.05): y_pred_adj = (y_proba >= thr).astype(int) f1 = f1_score(y_test, y_pred_adj) if f1 > best_f1: best_f1 = f1 best_thr = thr print(f'最优阈值: {best_thr:.2f}, 对应 F1: {best_f1:.4f}')

阈值调低会误报变多,但能抓出更多潜在流失客户;调高则相反。在电信业务里,把高概率客户提前圈出来做挽留,比漏掉一个真正要流失的客户代价小,所以很多人会故意把阈值往低调。扫描区间和步长根据业务容忍度调整,评估指标也可以换成召回率,看你的侧重点。这一步不用改模型,改的是业务决策边界。

5. 避坑指南:朴素贝叶斯实战里的五个翻车点

这五个坑是我复现这类资源时实际踩过的,每一条都对应 ipynb 里能看到的真实报错或结果异常。

5.1 现象:测试集准确率比训练集低一大截

原因最常见的有两个:一是划分数据集时没加 stratify,测试集里流失样本占比和训练集差很远;二是 LabelEncoder 或 One-Hot 在 train_test_split 之前就用全量数据 fit,测试集的信息通过编码器泄漏进了训练过程。

解决:划分必须加 stratify=y,所有编码器只 fit 训练集,测试集只做 transform。这样写:

# 先划分 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) # 再编码:编码器只 fit 训练集 label_enc = LabelEncoder() X_train['gender'] = label_enc.fit_transform(X_train['gender']) X_test['gender'] = label_enc.transform(X_test['gender'])

看到 fit_transform 出现在 train_test_split 之前,基本就是这个坑。泄漏会让测试集指标虚高,真实场景里根本达不到,这也是评审最容易挑出的问题。

5.2 现象:TotalCharges 转 float 直接报错

原因:data.csv 里 TotalCharges 是 object 类型,tenure 为 0 的新客户该字段是空字符串,直接 astype(float) 抛 ValueError。

# 错误写法 df['TotalCharges'] = df['TotalCharges'].astype(float) # ValueError # 正确写法 df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce') df['TotalCharges'] = df['TotalCharges'].fillna(0)

血泪经验:不要看到空字符串就当成脏数据删行,先查 tenure 分布。很多所谓缺失是业务规则的一部分,删掉反而让模型学到错误的样本分布。

5.3 现象:预测概率全挤在 0 或 1 附近

原因:GaussianNB 的方差估计接近 0,概率密度变成尖峰;或者特征里混进了常量列,比如某字段所有样本取值都相同,方差为 0。

# 检查常量列 constant_cols = [c for c in X.columns if X[c].nunique() <= 1] print('常量列:', constant_cols) # 调整方差平滑 model = GaussianNB(var_smoothing=1e-3)

解决分两步:先删掉方差为 0 的列,再把 var_smoothing 从默认 1e-9 往上调,看概率分布是否恢复正常。调到多少没有标准答案,以验证集表现为准,我一般从 1e-6 开始试。

5.4 现象:特征编码后模型反而变差

原因:把无序类别用 LabelEncoder 强加顺序。PaymentMethod 的四种取值没有大小关系,编成 0/1/2/3 之后 GaussianNB 会把相邻数值当相似特征,概率估计被带偏。

解决:无序多取值字段必须 One-Hot;Contract 虽然看起来有序,在朴素贝叶斯下也没差别,One-Hot 更稳。特征编码选择错误是数据预处理环节最隐蔽的问题,指标下降时优先怀疑这里。

5.5 现象:报告里的数字自己复现不出来

原因:train_test_split 没有固定 random_state,每次划分结果不同;或者 ipynb 中间 cell 被改过,从某个位置继续运行时变量状态和从头跑不一致。

解决:固定 random_state=42,复现实验时先 Restart & Run All,保证从头到尾完整执行。8000 字实验报告里写准确率时,把随机种子和数据集划分比例一起写进去,这是评审老师最爱追问的细节,也是很多数据挖掘项目被质疑"结果不可信"的根源。

6. 结果验证:ROC 曲线、阈值调整与流失名单的业务落地

6.1 ROC 与 AUC:评估指标怎么读

from sklearn.metrics import roc_auc_score, roc_curve import matplotlib.pyplot as plt # 计算 AUC auc = roc_auc_score(y_test, y_proba) print(f'AUC: {auc:.4f}') # 绘制 ROC 曲线 fpr, tpr, _ = roc_curve(y_test, y_proba) plt.plot(fpr, tpr, label=f'GaussianNB (AUC={auc:.4f})') plt.plot([0, 1], [0, 1], 'k--', label='随机猜测') plt.xlabel('假正率') plt.ylabel('真正率') plt.legend() plt.savefig('roc_curve.png', dpi=120)

AUC 是不依赖阈值的综合指标,0.5 等于随机猜,0.8 以上算有实用价值。朴素贝叶斯在这个数据集上跑到 0.8 上下是正常水平,如果明显低于这个数,回头查编码和阈值两个环节,大概率问题出在数据准备而不是算法本身。

6.2 从预测概率到流失名单:业务侧怎么用

result.csv 里的 Churn_prob 就是流失概率。业务落地时我会按概率降序排列,取前 20% 的客户作为重点挽留名单,再叠加 MonthlyCharges 过滤出高价值客户,优先分配客服资源。资源包里还带了 catboost_info 目录,说明作者实际训练过 CatBoost 做对比实验。如果你的报告需要提升准确率,可以把 CatBoost 结果作为强基线写进对比,朴素贝叶斯作为可解释性强的基线模型,两者互补地呈现。

从那以后,我每次做分类项目都强制自己过三关:划分数据集先查 stratify、编码器只 fit 训练集、随机种子写死在代码里。朴素贝叶斯的理论门槛不高,翻车几乎都翻在数据准备和复现细节上。这份资源的 ipynb 和实验报告把全过程都留了底,照着走一遍,你就能在 7043 条样本上拿到自己的完整结果。希望帮到你。

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

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

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

立即咨询