简介:这份资源面向数据挖掘初学者与算法实践者,围绕电信客户流失预测这一经典分类任务,提供从数据到建模再到实验报告的完整方案。数据源自Kaggle公开数据集,共7043条记录、21个字段,涵盖客户个人信息、账户信息、订阅服务及流失评价指标四类属性,适合练习特征工程与朴素贝叶斯分类建模。压缩包共9个文件,约3.1MB,包含csv与tsv格式的数据集、ipynb与html形式的完整代码、tfevents训练日志、json配置及docx实验报告,覆盖数据读取、模型训练与结果输出全流程。已有297人学习下载。读者可借助8000字实验报告理解建模思路与评估指标,对照代码复现朴素贝叶斯分类过程,并参考训练日志与结果文件排查调参问题,快速完成一份可复用的课程设计或入门项目。
1. 电信客户流失预测:一份能直接跑通的朴素贝叶斯实战包
电信行业拉新成本是留存成本的数倍,客户流失预测几乎是数据挖掘入门最经典的落地场景。这份资源包围绕 Kaggle 开源的 Telco Customer Churn 数据集展开,共 7043 条记录、21 个字段,用朴素贝叶斯算法搭建了一套完整的流失预测模型,附带完整代码(.ipynb 与 .html 双版本)、data.csv 原始数据、result.csv 预测结果,以及一份 8000 字实验报告。它解决的不是"算法怎么推导"的问题,而是"拿到一份真实业务数据,从字段理解到模型评估怎么走完全流程"的问题。适合正在做课程设计、需要复现一个端到端数据挖掘项目的学生,也适合想快速验证朴素贝叶斯在分类任务上表现的从业者。下面按数据理解、特征工程、建模调参、避坑排查、进阶验证的顺序拆开讲。
2. 数据字段拆解与朴素贝叶斯选型:21 个字段怎么归类
2.1 四类字段的业务含义与建模角色
拿到 data.csv 第一件事不是急着 fit,而是把 21 个字段按业务逻辑分组,这直接决定后面哪些字段能进模型、哪些要转换。按摘要里的分类:
| 类别 | 字段 | 建模角色 |
|---|---|---|
| 客户个人信息 | CustomerID、gender、SeniorCitizen、Partner、Dependents | CustomerID 是唯一标识,必须剔除;其余为类别特征 |
| 客户账户信息 | tenure、Contract、PaperlessBilling、PaymentMethod、MonthlyCharges、TotalCharges | 数值+类别混合,tenure 和 MonthlyCharges 是强信号 |
| 客户订阅服务 | PhoneService、MultipleLines、InternetService、OnlineSecurity、OnlineBackup、DeviceProtection、TechSupport、StreamingTV、StreamingMovies | 全部类别特征,反映用户粘性 |
| 流失评价指标 | Churn | 目标变量,Yes/No 二分类 |
这里有个容易翻车的点:TotalCharges 在原始数据里是字符串类型,因为新入网用户(tenure=0)的该字段是空字符串而非 0。直接读进来会变成 object 类型,后面做数值计算就报错。常见做法是先pd.to_numeric(df['TotalCharges'], errors='coerce')再决定填充策略。
2.2 为什么选朴素贝叶斯而不是逻辑回归
朴素贝叶斯的核心假设是特征条件独立,在电信流失场景里这个假设明显不成立——比如 InternetService 和 OnlineSecurity 高度相关。但它依然能work,原因有三:一是特征维度不高(去掉 ID 后 19 个),独立性假设带来的偏差可控;二是它计算的是后验概率排序,分类边界对概率估计的绝对准确性不敏感;三是训练速度极快,适合做基线模型快速验证特征有效性。
逻辑回归在这个场景其实表现可能更好,因为它不假设独立、能输出校准过的概率。但这份资源选朴素贝叶斯的价值在于:它是理解贝叶斯推断的最小可用案例,代码量少、可解释性强,适合作为数据挖掘课程的第一个完整项目。如果你要做生产级流失预警,建议把朴素贝叶斯当基线,再上 XGBoost 或 LightGBM 对比。
2.3 环境准备与数据加载
import pandas as pd import numpy as np from sklearn.naive_bayes import GaussianNB, BernoulliNB from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score # 加载数据,注意 TotalCharges 的空格问题 df = pd.read_csv('data.csv') print(df.shape) # (7043, 21) print(df['TotalCharges'].dtype) # object,需要转换 # 处理 TotalCharges:空字符串转 NaN 再转数值 df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce') print(df['TotalCharges'].isnull().sum()) # 11 条缺失这段代码做了三件事:加载数据、暴露 TotalCharges 的类型问题、统计缺失数量。参数说明:errors='coerce'是关键,它把无法转换的空字符串变成 NaN 而不是直接抛异常,让你能先看到缺失规模再决定填充还是删除。11 条缺失相对于 7043 条总量占比极低,直接 drop 或填 0 都行,我一般倾向填 0,因为 tenure=0 的用户 TotalCharges 逻辑上就是 0。
3. 特征工程与模型训练:从原始字段到预测结果
3.1 类别特征编码的三种策略与选择
朴素贝叶斯有三种常见变体:GaussianNB 假设特征服从正态分布,适合连续数值;MultinomialNB 适合计数型特征;BernoulliNB 适合二值特征。电信数据里既有数值(tenure、MonthlyCharges)又有类别(Contract、PaymentMethod),所以要么全转成数值用 GaussianNB,要么全转成二值用 BernoulliNB。
我一般会走两条路对比:路线 A 用 LabelEncoder 把类别转成整数,配合 GaussianNB;路线 B 用 OneHotEncoder 转成 0/1 矩阵,配合 BernoulliNB。资源包里的代码用的是路线 A,代码简洁但有个隐患——LabelEncoder 给无序类别赋了大小关系,比如 Contract 的 Month-to-month=0、One year=1、Two year=2,GaussianNB 会误以为存在序关系。更稳妥的做法是 OneHot,但维度会从 19 涨到 40+。
# 路线 A:LabelEncoder + GaussianNB df_model = df.drop(['CustomerID'], axis=1).copy() df_model['TotalCharges'] = df_model['TotalCharges'].fillna(0) le = LabelEncoder() for col in df_model.columns: if df_model[col].dtype == 'object': df_model[col] = le.fit_transform(df_model[col]) X = df_model.drop('Churn', axis=1) y = df_model['Churn'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) gnb = GaussianNB() gnb.fit(X_train_scaled, y_train) y_pred = gnb.predict(X_test_scaled) print(classification_report(y_test, y_pred))逻辑说明:先剔除 CustomerID,填充 TotalCharges 缺失,然后对所有 object 列做 LabelEncoder。stratify=y保证训练集和测试集的流失比例一致,这个在类别不平衡时很重要——电信数据里流失用户大约占 26.5%,不 stratify 可能导致测试集流失样本过少。StandardScaler 对 GaussianNB 是必要的,因为它假设各维度方差相近,不缩放会让 MonthlyCharges(范围 18-118)压制 tenure(范围 0-72)。
3.2 模型评估:为什么准确率会骗你
跑完上面的代码,你会看到 accuracy 大约 0.75 左右,看起来还行。但看 confusion_matrix 就会发现:模型把大部分样本都预测成"不流失",因为不流失样本占 73.5%,全猜"不流失"也有 73.5% 准确率。这就是类别不平衡下的准确率陷阱。
真正该看的是 recall(召回率)和 AUC。流失预测的业务目标是"尽量抓出要流失的人",漏报(把流失用户预测成不流失)的代价远大于误报。所以优化方向是提高流失类的 recall,哪怕牺牲一些 precision。
# 查看混淆矩阵和各类指标 print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) # 计算 AUC y_prob = gnb.predict_proba(X_test_scaled)[:, 1] print(f"AUC: {roc_auc_score(y_test, y_prob):.4f}") # 调整先验概率,提升流失类召回 gnb_balanced = GaussianNB(priors=[0.5, 0.5]) gnb_balanced.fit(X_train_scaled, y_train) y_pred_balanced = gnb_balanced.predict(X_test_scaled) print(classification_report(y_test, y_pred_balanced))参数说明:priors=[0.5, 0.5]手动设置先验概率,告诉模型"我认为流失和不流失一样重要"。默认情况下 GaussianNB 从数据里学先验(约 0.265 vs 0.735),导致流失类被压制。设成 0.5/0.5 后 recall 会明显上升,但 precision 会下降,具体取舍得看业务能接受多少误报。资源包里的 result.csv 就是模型对测试集的预测输出,可以拿来和你的结果对比。
3.3 交叉验证与参数稳定性检查
单次 train_test_split 的结果有随机性,换个 random_state 可能差几个百分点。要确认模型稳定性,跑 5 折交叉验证:
from sklearn.model_selection import StratifiedKFold cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(gnb, X_train_scaled, y_train, cv=cv, scoring='roc_auc') print(f"CV AUC: {scores.mean():.4f} (+/- {scores.std():.4f})")如果标准差超过 0.03,说明模型对数据划分敏感,可能需要更多数据或更稳定的特征。我一般会把这个结果和单次划分的 AUC 对比,差距大就说明单次结果不可信。
4. 避坑与排查:五个真实翻车记录
4.1 TotalCharges 空字符串导致 fit 报错
现象:执行gnb.fit(X_train, y_train)时报ValueError: could not convert string to float。原因:TotalCharges 列里有 11 个空字符串,LabelEncoder 把它当普通类别编码了,但 StandardScaler 无法处理。解决:在读数据后立即pd.to_numeric(df['TotalCharges'], errors='coerce'),然后 fillna(0) 或 dropna。这个坑几乎每个新手都会踩一次。
4.2 LabelEncoder 对测试集编码不一致
现象:训练时 Contract 编码为 Month-to-month=0、One year=1,但测试集里如果只有 One year 和 Two year,LabelEncoder 会重新映射成 0 和 1,导致训练和测试的编码含义错位。原因:每次 fit LabelEncoder 都是独立映射。解决:用同一个 encoder 对象 transform 测试集,或者改用pd.get_dummies做 OneHot。资源包代码里如果对全量数据先编码再 split,就没这个问题,但那是数据泄露——测试集信息泄漏到了训练阶段。
4.3 类别不平衡导致 recall 极低
现象:accuracy 0.75 看着不错,但流失类的 recall 只有 0.2 左右。原因:模型偏向多数类。解决:设置priors=[0.5, 0.5],或用 SMOTE 过采样,或改用 class_weight 可调的模型(逻辑回归、随机森林)。朴素贝叶斯本身不支持 class_weight,所以 priors 是主要手段。
4.4 数值特征未标准化导致高斯假设失效
现象:GaussianNB 的 AUC 只有 0.6 出头,比预期低很多。原因:MonthlyCharges 范围 18-118,tenure 范围 0-72,TotalCharges 范围 0-8000+,方差差异巨大,高斯NB 的对数概率计算被大方差特征主导。解决:StandardScaler 标准化所有数值特征。注意 fit 只能在训练集上做,然后 transform 测试集。
4.5 把 CustomerID 当特征导致过拟合
现象:训练集准确率 0.99,测试集 0.6。原因:CustomerID 是唯一标识,每个值只出现一次,模型把它当成了强特征记住了每个样本。解决:建模前 drop 掉 CustomerID。任何唯一标识列(订单号、用户ID)都不能进模型,这是铁律。
5. 进阶验证:用 BernoulliNB 和特征重要性做二次确认
5.1 OneHot + BernoulliNB 路线对比
GaussianNB 在混合类型特征上其实不是最优选择。更合理的做法是把所有类别特征 OneHot 成 0/1,数值特征分箱后也转成 0/1,然后用 BernoulliNB。这样完全避开了高斯假设,也更贴合朴素贝叶斯的原始形式。
# 路线 B:OneHot + BernoulliNB df_b = df.drop(['CustomerID'], axis=1).copy() df_b['TotalCharges'] = pd.to_numeric(df_b['TotalCharges'], errors='coerce').fillna(0) # 数值特征分箱转类别 df_b['tenure_bin'] = pd.cut(df_b['tenure'], bins=[-1, 12, 24, 48, 72], labels=['0-12', '13-24', '25-48', '49-72']) df_b['MonthlyCharges_bin'] = pd.cut(df_b['MonthlyCharges'], bins=[0, 35, 70, 120], labels=['low', 'mid', 'high']) df_b = df_b.drop(['tenure', 'MonthlyCharges', 'TotalCharges'], axis=1) # OneHot 编码 df_encoded = pd.get_dummies(df_b, drop_first=True) X_b = df_encoded.drop('Churn_Yes', axis=1) y_b = df_encoded['Churn_Yes'] X_train_b, X_test_b, y_train_b, y_test_b = train_test_split( X_b, y_b, test_size=0.2, random_state=42, stratify=y_b ) bnb = BernoulliNB(alpha=1.0) bnb.fit(X_train_b, y_train_b) y_pred_b = bnb.predict(X_test_b) print(classification_report(y_test_b, y_pred_b))参数说明:alpha=1.0是拉普拉斯平滑系数,防止某个特征在训练集里没出现过导致概率为 0。alpha 越大平滑越强,一般从 1.0 开始调,范围 0.1-10。drop_first=True在 OneHot 时去掉第一列避免多重共线性,虽然朴素贝叶斯不关心共线性,但能减少特征数。
5.2 用 feature_log_prob_ 看哪些特征最影响流失
BernoulliNB 训练完后,feature_log_prob_属性记录了每个特征在每个类别下的对数概率。对比流失类和不流失类的差值,就能看出哪些特征最有区分度:
# 查看特征重要性 feature_names = X_b.columns log_prob_diff = bnb.feature_log_prob_[1] - bnb.feature_log_prob_[0] importance = pd.DataFrame({ 'feature': feature_names, 'log_prob_diff': log_prob_diff }).sort_values('log_prob_diff', ascending=False) print(importance.head(10)) print(importance.tail(10))正差值大的特征表示在流失用户中出现概率更高,比如 Contract_Month-to-month、InternetService_Fiber optic、PaymentMethod_Electronic check 通常是 top 流失信号。负差值大的则是留存信号,比如 Contract_Two year、tenure_bin_49-72。这个结果可以和实验报告里的分析对照,验证你的特征工程是否合理。
5.3 验证清单与我的习惯
跑完两条路线后,我一般会做三件事确认结果可信:第一,对比 GaussianNB 和 BernoulliNB 的 AUC,如果差距超过 0.05,说明高斯假设偏差较大,优先信 BernoulliNB;第二,检查 top 10 重要特征是否符合业务直觉,如果出现 CustomerID 或某个无意义字段,说明编码有问题;第三,把 result.csv 的预测结果和你的 y_pred 做交叉比对,看一致率有多少,低于 90% 就要排查数据处理流程差异。
从那以后我每次做分类项目,都会先跑一遍全猜多数类的基线,再跑模型,只有明显超过基线才认为模型有效。这个习惯帮我避开了好几次"准确率虚高"的假象。希望这份拆解能帮你把这份资源真正跑通,而不是只停留在看代码的层面。
本文还有配套的精品资源,点击获取