
简介针对2022年第5届泰迪杯数据分析技能赛B题这份个人复盘资源提供了完整的代码实现面向参赛学生及数据分析初学者围绕银行客户忠诚度分析场景覆盖数据探索与清洗、产品营销可视化、客户流失因素分析、特征构建和长期忠诚度预测建模等完整任务链。压缩包共28个文件10个ipynb为可运行的Jupyter解题代码5个html便于直接查看代码结果8个xlsx为各任务输出表另有csv数据与pdf赛题说明整体约12MB目录结构清晰可对照任务模块逐项学习。已有695人学习下载通过学习这份资源能够理解泰迪杯B题从原始数据到预测结果的全流程思路适合需要参加数据分析竞赛或提升Python建模能力的读者。1. 泰迪杯 B 题想考察的到底是什么拿一道竞赛题练手很多人第一反应是打开代码编辑器开始跑模型但泰迪杯这类数据分析赛题真正拉开差距的环节往往在前面没被注意到的地方。2022 年第 5 届泰迪杯数据分析技能赛 B 题本质上是一道给出一份业务数据、要求选手完成从数据清洗到结论输出的完整任务它既不单纯考算法深度也不只考代码能力而是把数据理解、字段口径、建模稳定性和报告表达放在同一张卷子里。这道题的受众很明确准备参加数据分析竞赛的大学生或者想通过真题检验自己数据分析全流程能力的转行从业者。B 题通常是带标签的表格数据目标是做分类或回归预测并据此完成一份可读性强的分析报告。数据量一般在几千到几十万行之间字段有数值型也有文本型缺失值和异常值不会缺席。你需要掌握的并不是某个高深的模型而是生产环境下最常用的一整套操作读入数据、看清口径、补齐缺失、构造特征、训练基线模型、输出可视化图表、最后组织成书面结论。这篇文章就按这套顺序把每一个环节拆成可以直接上手的代码和参数说明帮你在做任何一道类似赛题时有一条清晰的执行路径。配套的 GitHub 开源项目内包含完整代码与实验报告可以作为参考实现来对照学习。2. 用 Pandas 做主数据检查与缺失值处理2.1 拿到 B 题数据先别急着算先确认字段口径泰迪杯 B 题的数据通常以 CSV 或 Excel 格式提供字段命名可能带中文也可能是英文缩写加下划线的风格。建议拿到数据的第一时间不急着做分布图而是先用一个脚本把数据结构完整扫一遍确认每个字段的真实含义、数据类型和取值空间。常见做法是先读入训练集和测试集随后输出形状、字段类型和基础的统计量从这些表层信息里判断字段之间的关系。import pandas as pd train pd.read_csv(train.csv, encodinggbk) test pd.read_csv(test.csv, encodinggbk) print(训练集形状:, train.shape) print(测试集形状:, test.shape) print(train.info()) print(train.describe(includeall).T)读文件时遇到编码报错一般不是文件坏了而是编码判断错误。优先尝试encodingutf-8报错了再换成gbk或gb18030这几种是中文 CSV 最常见的编码。info()会输出每列的非空计数和 dtype从中能快速看出哪些列有缺失、哪些列被读成了 object 但其实应该是数值。describe(includeall)对数值列给出 min、max、mean对 object 列给出 unique 数量和最常见值一次就能把两类字段的概览都拿到手。需要注意describe()默认只统计数值列加includeall才会把文本列也纳入统计。检查完结构后还应该对字段做命名统一处理比如把列名里的空格和特殊符号去掉、统一转成小写。用下面这条就能一次完成train.columns [c.strip().lower().replace( , _) for c in train.columns] test.columns [c.strip().lower().replace( , _) for c in test.columns]字段名不稳定是很多人后期出错的原因。模型训练时特征矩阵的列名必须和预测时完全一致早做归一化能省掉后面对齐特征的大量时间。建议把处理完的列名清单存一份或者直接把列名列表保存到变量里后面做特征工程时随时对照。2.2 数据清洗不是删行缺失值、异常值与重复数据数据清洗的核心原则是保留信息而不是把不完整的行一股脑删掉。遇到数值型字段缺失时直接用fillna(0)或者删行都是比较粗糙的做法这类莽撞处理会让模型学到错误的数据分布。B 题数据里常见的情况是某个字段对多数样本有值、少数样本缺失或者某几列之间有业务上的联动关系比如“下单时间”缺失的那几行往往“消费金额”也是空值这种联动本身就是特征。先系统性地统计每列缺失比例再决定用哪种策略missing_ratio train.isnull().mean().sort_values(ascendingFalse) print(missing_ratio[missing_ratio 0])缺失比例低于 5% 的数值列可以用该列的中位数填充缺失比例在 20% 上下且分布有偏斜时可以考虑用分组中位数比如按目标类别分组后再填充超过 50% 的列填充带来的噪声可能大于收益直接删掉这一列往往更省事。下面是具体操作示例# 全量中位数填充 train[age] train[age].fillna(train[age].median()) # 按目标字段分组填充 train[income] train.groupby(target)[income].transform(lambda s: s.fillna(s.median())) # 缺失太多的直接删列 train train.drop(columns[remark], errorsignore)这里的groupby.transform很关键它让每一行填充的是同组样本的中位数而不是全局值在类别不平衡的场景下能保留更多组间差异。errorsignore是防御写法如果列名不存在也不会报错中断流程。处理完缺失值后还要排查明显的脏数据比如年龄字段出现 200、收入字段出现负数、数值列里有空格导致被读取成字符串等这类数据不会通过缺失值检查暴露出来需要结合业务常识逐一确认。重复行也值得检查同一用户的多条记录可能是误录也可能是同一个人不同的业务记录保留还是合并要看业务背景不建议无脑删除。处理对象常用方法适用场景注意事项数值缺失中位数填充分布偏态强、缺失率低不要用均值抗噪能力差类别缺失众数填充或单独标记为“未知”类别特征缺失新的“未知”类别需在测试集同样存在异常值分位数截断或取对数收入、金额等长尾分布用 IQR 判断要先看字段分布重复行drop_duplicates(subset...)确认无业务意义先按关键字段查重再确认2.3 特征工程用现有字段造出建模用的特征B 题想把分数做上去特征工程比模型更重要。原始字段直接送入模型往往不够因为模型只能看到字段本身看不到字段之间的组合关系。常见思路是先从单个字段变形入手比如把年龄切分成年龄段、把时间戳拆成星期几和上下午、把收入与消费组合成比率这些变换能帮树模型找到更清晰的切分点。下面是一个完整特征构造示例import numpy as np # 把年龄切成有序分箱 bins [0, 18, 30, 45, 60, 100] labels [少年, 青年, 壮年, 中年, 老年] train[age_group] pd.cut(train[age], binsbins, labelslabels) # 构造收入消费比防止除零加极小值 train[income_consume_ratio] train[income] / (train[consume] 1e-6) # 从交易日期中提取时间特征 train[trans_date] pd.to_datetime(train[trans_date]) train[trans_weekday] train[trans_date].dt.weekday train[trans_hour] train[trans_date].dt.hour类别型字段可以直接做标签编码或独热编码。如果某个类别型字段的取值特别多比如城市有几百个取值独热编码会造成维度爆炸这时可以只保留出现频率最高的前 50 个城市其余归入“其他”类既能保留信息又不会让特征矩阵过大。from sklearn.preprocessing import LabelEncoder le LabelEncoder() train[city_code] le.fit_transform(train[city]) # 低频类别合并 freq train[city].value_counts() keep_cities freq[freq 10].index train[city] train[city].where(train[city].isin(keep_cities), 其他)注意 LabelEncoder 只适合树模型如果后面要用线性模型或深度学习就要改成OneHotEncoder或者pd.get_dummies()。在做特征工程时一定确保训练集和测试集使用同一套编码规则最简单的做法是先把两份数据拼接起来统一处理建模前再拆分。分组统计特征也值得做比如按城市计算用户的平均消费水平再把这个均值作为新特征放到原数据中模型就能捕获“某个用户所在城市的整体消费层级”这类信息。到这里数据准备工作基本完成可以进入建模阶段。3. 从基线模型到调参泰迪杯 B 题的多分类建模3.1 为什么从树模型开始而不是神经网络B 题通常给出的是结构化表格数据行数是几千到几十万字段多是混合类型此时随机森林或 XGBoost 这类树模型是比神经网络更合适的起点。树模型对字段尺度不敏感不需要做标准化能直接处理类别特征和非线性关系训练速度快在小数据量下也不容易过拟合到无法收拾最重要的是可以输出特征重要性方便反推业务结论。第一次建模的目标不是冲高分而是跑通一个稳定的基线拿到一组可以对比的指标。先把训练集和测试集按同样方式切分成特征矩阵和标签用 sklearn 自带的数据切分函数保证训练和验证集的类别分布一致。from sklearn.model_selection import train_test_split feature_cols [c for c in train.columns if c not in [target, id]] X train[feature_cols].copy() y train[target].copy() X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(X_train.shape, X_val.shape)参数test_size0.2表示验证集占 20%stratifyy表示按标签的比例分层抽样这样即使某个类别的样本很少训练集和验证集里它们的占比也能和原始数据保持一致。3.2 用交叉验证与 GridSearchCV 找稳定参数基线模型一般用随机森林就能拿到不错的分数。先设置一个相对合理的初始参数组合把模型跑出来用交叉验证替代单一的验证集切分来评估稳定性。固定一个随机种子很关键否则每次运行结果不一样后面对比调参效果时就无法判断改善到底来自参数还是噪声。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score rf RandomForestClassifier( n_estimators200, max_depthNone, min_samples_split4, min_samples_leaf2, random_state42, n_jobs-1, ) scores cross_val_score(rf, X_train, y_train, cv5, scoringf1_macro) print(CV F1:, scores.mean(), /-, scores.std())n_estimators200是树的数量太多会拉长训练时间但对精度提升有限max_depthNone表示不限制深度随机森林本身有随机采样控制的机制一般不限制深度问题不大min_samples_split4和min_samples_leaf2是防止树在叶子节点上拟合到过细的样本n_jobs-1让所有 CPU 核心并行训练。f1_macro是宏平均 F1对少数类的敏感度比准确率高适合类别不平衡的数据集。拿到基线分数后用GridSearchCV做小范围网格搜索。网格搜索不是参数组合越多越好组合数一多计算量指数上涨大多数时候只需要调三个关键参数max_depth、min_samples_split、min_samples_leaf。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [10, 20, None], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4], } grid GridSearchCV( RandomForestClassifier(n_estimators200, random_state42), param_grid, cv5, scoringf1_macro, n_jobs-1, ) grid.fit(X_train, y_train) print(最佳参数:, grid.best_params_) print(最佳分数:, grid.best_score_)搜索结束后用grid.best_estimator_在验证集上做最终评估不要直接用grid.score(X_val, y_val)因为它会把内部留出的那段数据也算进去分数会虚高。正确做法是重新用完整训练集拟合最优参数模型再单独对验证集做预测。如果网格搜索出来的最佳参数落在边界上比如max_depth的最优值是 10 而搜索范围是 [10, 20, None]说明还可以往更小的方向再搜一轮边界参数往往意味着搜索范围没覆盖到位。3.3 多分类评估准确率、混淆矩阵与 F1 的取舍分类问题的评估不能只看准确率。B 题数据里各类别样本数量往往不均衡准确率会被多数类带偏一个只预测多数类的模型也能拿到很高的准确率但没有实际价值。正确做法是用多分类 F1 结合混淆矩阵来看每个类别的表现。from sklearn.metrics import classification_report, confusion_matrix best_rf grid.best_estimator_ y_pred best_rf.predict(X_val) print(classification_report(y_val, y_pred)) print(confusion_matrix(y_val, y_pred))classification_report会输出每个类别的 precision、recall、f1-score 和样本数能一眼看出哪些类别被模型忽略。confusion_matrix的行是真实标签列是预测标签对角线越亮说明分对的越多。如果某一类的 recall 明显低于其他类说明这个类别的特征不够突出或者样本量太少需要去检查训练集里这个类的样本分布必要时可以做简单的过采样或调整类别权重比如class_weightbalanced。评估指标关注内容适用场景accuracy全部分类正确的比例类别均衡时precision预测为正例的样本中真实为正例的比例误报代价高的场景recall真实正例中被正确识别出来的比例漏报代价高的场景f1-macro各类别 F1 的算术平均类别不平衡且各类别同等重要f1-weighted按样本量加权的 F1 平均类别不平衡但大类权重更高更合理如果只追求比赛分数直接看f1_macro就行但写报告时要把混淆矩阵里暴露出的问题分析给评委看这比“准确率 0.9”更有说服力。模型参数稳定之后还有一件事容易忽略用全部训练数据再拟合一次模型而不是用切分后留下的 80%。交叉验证和调参阶段需要留出验证集但到了最终产出阶段把模型在全量数据上重新训练能让预测结果更稳特别是数据量少时多 20% 的样本对树模型的效果有明显帮助。4. 可视化与结果解读让结果可以直接写进报告4.1 用 Matplotlib 快速画出变量与目标的关系建模完成只是第一步泰迪杯的评分里报告占的比重相当大。评委不会因为你用了多复杂的模型就给高分他们更关心你能不能把数据里的规律说清楚。可视化的作用不是装饰而是为报告里的每一个结论提供证据。用 Matplotlib 和 Pandas 的内置绘图接口就能完成大部分需求。画图之前先设置中文字体否则图表里会出现方块乱码。Windows 下一般用“SimHei”或“Microsoft YaHei”Linux 服务器上如果没有中文字体需要先安装字体文件或在代码里指定一个存在的字体路径。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] matplotlib.rcParams[axes.unicode_minus] False先画类别标签的分布图直观看出各类样本数量train[target].value_counts().sort_index().plot(kindbar) plt.title(各类别样本数量分布) plt.xlabel(目标类别) plt.ylabel(样本数) plt.show()连续型变量与目标类别的关系一般用箱线图或小提琴图展示。箱线图能同时看出中位数、四分位数和离群点适合判断某个特征是否在类别之间有明显差异。train.boxplot(columnage, bytarget) plt.title(不同类别的年龄分布) plt.suptitle() plt.show()plt.suptitle()这行容易漏掉Pandas 的df.boxplot(by...)会自动生成一行“Boxplot grouped by target”的默认标题叠加在自定义标题之上把它清空才能让图表更干净。观察箱线图时重点看中位数线和盒子位置如果不同类别的箱子高度重叠说明这个特征区分度低如果箱子位置有明显错开说明这个特征对分类有贡献可以考虑在报告中引用。4.2 用特征重要性筛选变量树模型自带的特征重要性属性是分析字段贡献度最直接的依据。对随机森林模型feature_importances_反映的是每个特征在所有树中被用作分裂点时带来的不纯度平均减少量。把重要性排序画成横向条形图既能帮你删掉无关特征又能给报告提供一张现成的图import pandas as pd importance pd.Series(best_rf.feature_importances_, indexfeature_cols).sort_values(ascendingFalse) importance.head(15).plot(kindbarh) plt.gca().invert_yaxis() plt.title(特征重要性 Top15) plt.xlabel(重要性得分) plt.show()sort_values(ascendingFalse)让重要性从高到低排列plot(kindbarh)画横向条形图时默认第一条在最下方加invert_yaxis()翻转坐标轴让最高的特征出现在最顶上视觉上更符合阅读习惯。特征重要性的作用不只是选特征它还能帮你做一次建模逻辑的自我检查排名靠前的特征是否符合业务直觉。比如做客户分类的题目消费金额和频次在重要性前列是合理的如果年龄排第一而业务上并不该如此突出往往是某个数据处理环节出了问题比如字段里含有泄漏信息或者缺失值填充方式影响了树的分裂。顺带说一句特征重要性有随机性波动如果两次运行的结果差异很大说明模型本身不稳定建议用多轮训练取平均的方式获得更可靠的重要性排序。4.3 可视化图表的评审标准图要服务于结论报告里的每一张图都应该能回答一个具体问题。绘制之前先写下这张图想说明什么画完再检查读者能不能从图里读出同样的结论。常见的无效图有几种x 轴标签挤成一团看不清、图例和线条颜色相似导致无法区分、堆叠柱状图只堆叠却不说明每层的含义。以下表格列出 B 题报告中最高频用到的图表类型与对应场景图表类型回答的问题对应报告章节条形图各类别样本有多少数据概览箱线图不同类别下特征的分布差异特征分析相关性热力图哪些特征之间强相关特征筛选特征重要性条形图哪些特征对预测贡献大建模分析混淆矩阵热力图分类错误集中在哪些类模型评估画混淆矩阵热力图时可以先打印confusion_matrix再用plt.imshow或 sns 的 heatmap 做成可视化更直观。不引入额外库时用 Matplotlib 也能完成但若环境允许Seaborn 的heatmap在配色和坐标标签上更省事适合直接把图贴进报告。5. 报告写作与答辩技巧把分析结果讲给评委听5.1 用“结论先行”组织报告结构泰迪杯的报告评审节奏很快评委不会逐字读完全文后再判断而是先看摘要、看结论、看图再决定要不要细看正文。因此报告的第一页就要把最重要的结论写清楚题目是什么、用了什么数据、哪些特征最关键、最终模型达到了什么分数。结论先行不是把摘要写完就结束正文每一章的标题也应该直接透露结论比如“低收入群体集中在夜间消费”就比“消费行为分析”更能引导评委读下去。分析和建模部分不要按代码执行的顺序写而要按信息量组织先展示数据整体面貌再用一两张区分度最高的图引出关键特征最后介绍基线模型和优化方向。模型部分重点写三件事为什么选这个模型、调整了什么参数、参数调整后指标提升了多少。调参过程的中间结果不需要全部列出选一两个有代表性的对比就好对比时用表格呈现评委可以一眼看到变化趋势。5.2 数据表格与参数对比怎么放写作时把主要结果汇总成一张表能大幅降低阅读成本。保证表格格式整齐清晰、数字单位统一、核心数值保留四位小数并在表格下方用一句话点出关键变化这样既不显得在做数字堆砌又能传递出你理解这些指标背后的意义。import pandas as pd results pd.DataFrame({ 模型: [Baseline, GridSearch, Final], CV分数: [0.8321, 0.8476, 0.8523], 验证集F1: [0.8275, 0.8412, 0.8498], }) pd.set_option(display.precision, 4) print(results.to_string(indexFalse))display.precision只影响打印显示不影响存储值写报告时建议关闭科学计数法pd.set_option(display.float_format, lambda x: f{x:.4f})否则出现8.5e-01这种格式在文档里很不美观。打印出来的表格可以直接截图或复制进报告保证小数位数统一即可不必在复制后额外调整格式。5.3 一个涨分技巧主动写出失败案例与模型局限评委给高分的关键通常落在对模型局限性的分析上。与其等评委提问时临时解释不如在报告里主动指出模型在哪些样本上表现不好、可能的原因是什么、后续准备如何改进。比如混淆矩阵里某个类别的 recall 特别低可以结合数据分布说明是样本量不足导致。如果测试集的预测结果被明显偏向多数类可以写出你尝试过class_weight但没有显著改善这比只展示最终分数更显严谨。答辩时被问“模型哪里不好”就据实回答不要含糊带过。主动暴露局限并在下一步规划中给出应对方案往往比一味强调自己的准确率更有说服力。最后的建议是把模型输出的错误样本打印出几条观察它们的共同点这既是对项目的复盘也是面试环节最容易被追问的问题——把这条经验保留下来会比分数本身更长久。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。