1. 这第五次作业,我把它当成了一堂完整的项目课
先说说这份作业的背景。其实我在接手“第5次作业”之前,已经陆续做过几次课程作业了,前四次基本是“照着模板写答案、提交完就完事”的状态。但第五次不一样,这次的作业题目没有限定具体方向,只给了一个很宽泛的任务:完成一次端到端的分析演练。也就是说,要从问题定义、数据获取、清洗、探索、建模到结果解读,走完一整条链路。起初我有点懵,因为太开放的任务反而不知道怎么下手,但回头看,恰恰是这份“自由度过高”的作业,把我逼成了一名勉强合格的“业余全职分析师”。
这篇博文,我就把这次的完整过程拆开讲,包括我是怎么选题的、数据从哪里来、踩过哪些坑、怎么处理脏数据和缺失值、模型迭代的时候被哪些细节卡住过,以及最后做结果汇报时学到的一些经验。如果你也正在做类似的作业、或者刚入行想练手一个完整的数据分析项目,这篇应该能给你一些可以直接照搬的套路,也能帮你少走几次弯路。
我的整体设计思路很简单:先定一个能让评委一眼看懂的题目,再找一份公开数据集,用最主流的工具链(Python + Pandas + scikit-learn)走完流程,最后把结论落到业务建议上。这个思路听起来很常规,但真正动手之后你会发现,每个环节都有无数个让你崩溃的细节。下面按步骤写。
2. 选题与数据来源:为什么我最终选了“用户流失预测”这个方向
2.1 选题逻辑:评分导向、数据可得性与业务可解释性
开放题最常见的陷阱是“想搞个大新闻”,比如上来就想预测股票、诊断疾病。这类题不是不行,但对一门课程作业来说,有三个硬伤:第一,数据不好拿;第二,模型效果很难做好,容易让自己的整个分析过程看起来很业余;第三,也是最关键的,如果业务背景太复杂,评委很难快速理解你想干什么。
所以我给自己定了三条选题标准:
- 数据要公开、结构清晰,最好有一份现成的带标签的表格型数据;
- 问题要足够经典,方便套用成熟的建模流程;
- 结论要能落地,可以给出“我们该对哪类用户做点什么”这种可执行建议。
综合比对下来,我选了“电信用户流失预测”这个经典二分类问题。原因很简单:一是公开数据集很成熟;二是流失预测本身就是业务界经典场景,通过用户属性、消费行为、服务订阅情况,预测用户下个月会不会跑路,既有一定的技术深度,又有明确的业务价值。换句话说,评委看完题目就知道你在做什么,而且是认同这个分析方向的。
2.2 数据集的获取与预审:先盯住数据字典
数据集选定了之后,第一件事不是跑代码,而是读数据说明(数据字典)。这一步很多人会忽视,直接pd.read_csv()之后就开始画图,结果后面做着做着才发现“这个特征不是我想的那个意思”。我这次先花了一个多小时把字段逐个过了一遍,把每个变量的类型、取值范围、潜在缺失风险都列了一个Excel表。
这个动作在后面帮了大忙。比如数据里有个TotalCharges字段,看着像是数值型,读进来之后发现是object类型。如果没看过说明,很可能忽略掉。类似这种小问题,后面单独说。总之,数据预审不是浪费时间,它决定了你后面写的代码是“一次过”还是要反复推翻重来。
我的建议是,拿到任何一份数据集,先回答三个问题:
- 每条样本代表什么?(比如一个用户)
- 预测目标是什么?(比如用户是否流失)
- 有哪些特征?每个特征是什么类型、单位、取值范围?
这三点理不清,后面的特征工程基本就是瞎做。
3. 环境准备与数据预处理的几个关键动作
3.1 工具链的选型:怎么选最简单的组合
这次作业我用了Visual Studio Code + Python 3.10,核心库是Pandas、NumPy、Matplotlib、Seaborn和scikit-learn。没有上太复杂的工具,原因很简单:作业场景下,效率和可复制性比炫技重要。如果你电脑环境还没有配好,直接安装Anaconda也行,它会帮你把大多数数据科学常用库一次配齐。
有一个小建议:最好在项目目录下建一个虚拟环境,不要图省事把包都往全局装。课程作业做到后面,各种库的版本冲突很常见,比如numpy版本太新导致某些旧接口被移除,这种问题排查起来非常耗时间。虚拟环境能帮你把“项目依赖”和“系统环境”隔离,属于花五分钟省五小时的投入。
创建虚拟环境很简单:
python -m venv .venv激活之后,再安装依赖:
pip install pandas numpy matplotlib seaborn scikit-learn3.2 数据加载与快速体检:一上来就建模的人才最危险
数据加载很简单,但加载完千万别急着进入建模环节。我用一个半小时做了一次完整的“数据体检”,步骤基本固定:
- 看形状(多少行多少列)
- 看每列类型和缺失率
- 看数值型特征的分布(均值、标准差、分位数)
- 看类别型特征的唯一值数量
- 看目标变量的类别平衡度
具体来说,核心几条命令敲完之后,我得到这样一些初步结论:
数据一共7043行、21列,目标变量是Churn,取值为Yes和No,其中流失样本占比约26.5%。这个比例不算极端不平衡,但也需要留意——后面评估模型时不能只看准确率,否则模型全预测“不流失”也能拿到73%多的准确率,看上去还不错,实际啥也没学到。
这个体检阶段最重要的产物是一张数据结构清单,我后面每处理一个字段都有据可查。强烈建议大家把这五步养成肌肉记忆,总时间成本不高,但对全局的理解完全不一样。
3.3 脏数据和缺失值的经典处理合集
这个数据集最大的坑集中在几个字段,我挨个说。
第一个坑:TotalCharges字段类型是字符串。因为数据在导出时可能混入了空字符串,导致字段被识别为object。我的处理方式是先用pd.to_numeric()强制转换,出错的地方变成NaN,再统一填充。另外要注意,它和MonthlyCharges、tenure之间存在天然关系:TotalCharges ≈ MonthlyCharges × tenure。如果某行这三个字段对不上,基本能反推数据质量有问题,这是很实用的业务校验思路。
第二个坑:缺失值其实很少,但处理方式仍然要讲究。检查下来发现只有TotalCharges存在11个空值,占比极低。对这种情况,直接删除这几行就行,不需要做复杂插补。因为只有11个样本,插补引入的偏差可能大于直接删除的影响。这个思路要记住:缺失值处理不是越高级越好,而是要看缺失比例和业务含义。缺失很少,直接删;缺失较多,就得分析缺失机制再决定用均值/中位数/模型预测哪种方式填充。
第三个坑:类别型变量里藏着“误导性数字”。有些特征看起来是数字,其实是类别编码,比如SeniorCitizen,取值为0和1,表示是否老年人,根本不是数值大小意义上的“等级”。如果直接拿去算距离,模型会把它当成连续变量,逻辑上就错了。处理方式是按分类型变量处理,该独热编码就独热编码。这个点看起来很简单,但实际做作业时会有一大批人踩坑。
第四个坑:tenure字段看起来是连续值,但分布是“高门槛型”。新用户集中在前几个月,老用户则缓慢递减。这种分布直方图很容易被“均值±标准差”概括掉,但流失分析里恰恰是用户生命周期早期最值得关注,所以后面特征工程时我把它做了分段处理。
3.4 特征工程的取舍:独热编码、特征衍生与陷阱排除
基线模型可以直接用原始特征跑,但想要效果好一点,特征工程还是很值的。我从两个方向做:
- 把类别型变量做独热编码,包括
InternetService、Contract、PaymentMethod等; - 把
tenure这种有业务含义的量做成用户生命周期分段,比如“未满一个月”“1到12个月”“1年到2年”“2年以上”,保留具体数值的同时增加一个可解释的分组。
这里也踩了一个小坑:数据集里有一组与“补充服务”相关的字段,比如OnlineSecurity、DeviceProtection、TechSupport等,它们的取值为Yes/No/No internet service。粗看是三类,其实“No internet service”和“No”在业务上完全不同——前者是因为没开通网络服务,后者是开通了但没买补充包。如果直接独热编码,会让模型认为这是三个独立状态,反而丢失了层级关系。我是手动把“No internet service”跟“No”合并,或者单独衍生一个标志位“是否开通了某种补充服务”,只要是合理的业务理解,模型效果和可解释性都会更好。
特征工程的最终结果,是从20个原始特征扩展到六十多个模型输入列。听起来数量变多了,但大多数是独热编码产生的0/1列,不增加太多计算负担。
4. 建模与调参:从基线模型到“效果看得过去”
4.1 数据集划分与交叉验证:别让模型“偷看答案”
在建模前,我严格按70%训练、30%测试的比例划分数据,并且固定了随机种子。这样后面的每次实验都可以直接对比,不会因为随机划分导致结果差异。
这一步也是很多初学者的“隐藏翻车点”:特征工程时必须先划分训练集和测试集,再在训练集上做编码和填充,否则测试集的信息会通过“全局统计量”泄露进模型。我这次的所有独热编码都用fit_transform在训练集上完成、再用transform处理测试集,确保测试集是完全没被“见过”的。
除了单次划分,我还加了5折交叉验证来评估模型稳定性,避免“某一刀切得好导致虚高”的情况。交叉验证的作用是:用多份不同子集轮流当验证集,最终得分是所有折的平均值,能更真实地反映模型泛化能力。
4.2 三个模型的横向对比:逻辑回归、随机森林、XGBoost
基线实验我一次跑了三个模型——逻辑回归、随机森林、XGBoost。这里先不追求极致调参,先看它们在默认参数下的表现,选出一个有潜力的方向再深入。
关键评估指标我选了ROC-AUC、召回率和F1分数。原因前面提过:数据本身有26.5%的流失率,准确率容易被“多数类”主导。流失预测业务里,我们更关心的是“真正流失的人里,模型能召回多少”。比如100个会流失的用户,模型只找到了30个,那召回率就是0.3,这个数字比准确率有价值得多。
from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.model_selection import cross_val_score models = { 'LogisticRegression': LogisticRegression(max_iter=1000, random_state=42), 'RandomForest': RandomForestClassifier(n_estimators=200, random_state=42), 'XGBoost': XGBClassifier(eval_metric='logloss', random_state=42) } for name, model in models.items(): scores = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc') print(f'{name}: {scores.mean():.4f} (±{scores.std():.4f})')第一轮结果如下:
| 模型 | ROC-AUC(交叉验证) |
|---|---|
| 逻辑回归 | 0.8421 |
| 随机森林 | 0.8314 |
| XGBoost | 0.8467 |
说实话,这个结果出乎我意料。我本来以为随机森林会碾压逻辑回归,但实际上在这个数据集上,树模型的优势没有体现出来。原因可能是很多特征本身就是类别型变量,经过独热编码之后,特征空间比较稀疏,逻辑回归在这种场景下表现并不差。XGBoost略微领先,但和逻辑回归相差不大,这让我意识到一个问题:先跑基线模型再下结论非常重要,凭经验拍脑袋猜哪个模型最好,往往会被现实打脸。
4.3 类别不平衡的处理:不做其实也行,但做了更稳
虽然流失样本占比26.5%还算能接受,但我还是试了一下类别不平衡的经典解决方案——调整类别权重和过采样。scikit-learn里很多模型直接支持class_weight='balanced',这是最省事的做法。
我对比了一组实验:逻辑回归默认配置 vsclass_weight='balanced'。结果发现ROC-AUC小幅提升,但召回率显著提高。代价是精确率下降,也就是模型更倾向于“宁杀错不放过”,会把一些本来不流失的用户也预测成流失。在真实业务里,这意味着运营团队需要花更多精力去触达“假阳性”用户。
权衡之后,我觉得在作业场景下,“召回率提升”带来的分析价值大于“精确率下降”带来的损失,因为我们的目标是识别潜在流失用户,哪怕误报一些,也还有挽回空间。所以最终选用了平衡版本。
4.4 调参与验证:网格搜索的收获与陷阱
接下来针对XGBoost做了简单的网格搜索调参,重点关注n_estimators、max_depth、learning_rate和subsample。因为计算资源有限,我没有做全网格搜索,而是按“先粗后细”的思路,分两轮:
第一轮粗调范围:
param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [3, 4, 5], 'learning_rate': [0.01, 0.05, 0.1], 'subsample': [0.7, 0.8, 1.0] }第二轮在最优参数的周围细调,比如learning_rate换成0.03和0.07,max_depth换成4和6。这种做法的效率远高于一次性把所有参数组合都跑完,而且更容易找到“局部甜点”。
调完参数后,测试集ROC-AUC稳定在0.849左右,比默认参数略好一点。说实话,提升幅度不大,但这个过程让我学到了一个很重要的点:在这个数据集上,特征与业务理解带来的收益,远大于模型结构和参数微调带来的收益。这也解释了为什么很多人拿到一份结构复杂但是特征无区分度的数据,怎么调都上不去。反过来,如果先想清楚业务逻辑、做几个高质量特征,可能一个最普通的逻辑回归就能打赢XGBoost。
5. 模型解释与结果落地:不只是跑个准确率交差
5.1 从“黑盒”到“可解释”:特征重要性怎么读
作业要求写分析结论,所以模型不能只有预测能力,还要能回答“哪些因素在影响流失”。这里我用了两种方式:
- 树模型自带的
feature_importances_,看哪些特征对分裂的贡献大; - 逻辑回归的系数大小和方向,作为交叉验证。
两者的结论高度一致:Contract(特别是按月签约)、tenure(用户在网时长)和MonthlyCharges(月消费金额)是最重要的三个因素。具体来说,长期合同用户的流失概率显著低于按月用户;在网时间越长流失风险越低;月消费越高,流失风险越高,这很可能是因为高消费用户对价格更敏感、预期更高,一旦服务体验不达标更容易离开。
这些结论不需要复杂的SHAP值分析就能得到,而且放到业务语境里完全说得通。我第一次跑出这些系数的时候,特别有“做分析的感觉”——并不是用一个复杂的模型去炫技,而是真正回答了业务方最关心的“哪些人要走、为什么走”的问题。
5.2 目标群体画像:把模型结果翻译成业务动作
模型跑完之后,我还没有直接写结论,而是做了一步“目标群体画像”。我按模型的预测概率,把测试集里预测流失概率排名前20%的样本筛出来,然后统计这批用户在各特征上的分布。结果非常直观:
这部分用户的画像大概是:
- 大部分没有签长期合同,而是按月付费;
- 在网时间普遍小于12个月;
- 月消费金额处于中高水平;
- 很多没有开通在线安全服务、技术支持等附加服务。
对照这个画像,可以提出一个具体的运营建议:在用户入网的前三个月建立流失预警机制,通过赠送附加服务或提供套餐折扣来提升粘性。这比一句“我们要降低流失率”有价值得多。
5.3 关于汇报材料的一个体会
最后交作业的时候,我把所有代码整理成一个带注释的Notebook,再额外写了一份两页的报告,核心结构只有四个部分:
- 问题背景与目标;
- 数据处理思路;
- 模型效果对比;
- 业务建议。
说实话,汇报材料不在多,而在逻辑清晰。评委最怕看到的是“代码堆了一堆,但不知道你想表达什么”。我这次写报告时,每个图表都配上了一句“所以这意味着什么”,而不是单纯展示图长什么样。这一点,我觉得是这次作业里最有收获的地方之一。
6. 这五次作业走下来,我记住的几件事
这次“第5次作业”从头到尾做完,回头总结几个实操层面的经验,按优先级排序写一下。
数据质量决定模型上限。这句话听了无数遍,但这次是真的亲手验证了。TotalCharges一个字段的类型问题、No internet service的合并策略,哪怕只改一处,最后AUC都会有所变化。数据清洗阶段多花一小时,后面建模阶段就能少折腾十小时。
基线模型一定要先跑。不要一上来就上深度学习或者各种AutoML,先用最普通的模型把流程跑通,拿到一个基准分,再去逐步优化。这样做既能在有限时间内完成作业,也能帮你建立对数据的判断——“这个数据大概能做到什么水平”。
特征工程要有业务逻辑支撑。不要为了造特征而造特征。你在做的是“用户流失预测”,那就应该从用户生命周期、消费习惯、服务体验这些角度去想哪些信息可能有关系,而不是让模型自己从一堆噪声里挑信号。
评估指标一定要贴合任务目标。这个流失预测数据如果用准确率评估,73%多就能“自我感觉良好”,但实际对业务毫无贡献。换成召回率、F1、ROC-AUC之后,模型的真实水平才显现出来。关键不是代码多花哨,而是知道“该看什么数字”。
做作业不等于做研究,好讲清楚比做得复杂更重要。我这次没有用任何深度学习模型,全程就是三个经典模型跑对比,但最后报告拿到的反馈比前四次都正向。因为整个分析链路是可信的、可复现的、有业务落地的。只要逻辑清晰、结论明确,朴素的方法一样能打动听众,反而堆砌复杂模型容易让结果无法解释,自己和评委都看得一头雾水。
最后再分享一个小习惯:每次跑实验之前,我都会在代码开头留一个“实验说明”的注释,写清楚这次实验想验证什么、改了哪些变量、预期是什么效果。后续回头复盘的时候,这些注释就是一个很清晰的实验日志。对这个项目来说,它帮助我在写最终报告时能快速回忆起每一次尝试的动机和结果,而不是面对一堆没有名字的输出文件发呆。如果你也在做类似的项目作业,强烈建议试一试。