泰迪杯数据挖掘竞赛备赛指南:从数据清洗到模型融合的完整路径 简介这份资料面向备战“泰迪杯”全国数据挖掘挑战赛的高校学生与指导教师系统梳理竞赛背景、题型分类与参赛流程帮助零基础或有一定机器学习经验的读者快速建立备赛框架。压缩包内共1个docx文件约88KB以文档形式集中呈现竞赛规则、评分标准与技术要点便于随时查阅和整理笔记。内容覆盖数据预处理、特征工程、模型构建与结果分析全流程并给出A至D题的题型定位与解题思路同时结合往届真题如城市轨道交通客流预测展示时空图神经网络等优秀方案与代码片段还整理了常见问题解决方案、学习资源推荐、历届赛题汇总与常用术语表。已有66人学习适合需要系统了解赛制、对照案例提升建模与报告撰写能力的参赛者。1. 泰迪杯数据挖掘竞赛学习资料从零到提交一条能跑通的备赛路径很多人第一次接触泰迪杯数据挖掘竞赛是被“数据挖掘”四个字吓住的——觉得要先学完机器学习、深度学习、特征工程一大堆理论才敢报名。我带过几届参赛队伍真实情况恰恰相反泰迪杯的赛题大多是结构化或半结构化的业务数据考的是你能不能把一份脏数据清洗成能建模的样子再用合适的模型跑出一个可解释的结果。换句话说它更像一场“数据处理的工程能力测试”而不是算法竞赛。这份学习资料要解决的核心问题就一个让你在有限时间内从拿到赛题数据到提交一份完整答案每一步都知道该做什么、用什么工具、参数怎么设。适合刚入门数据挖掘的学生、想带队但缺一套标准流程的指导者以及做过建模但没打过这类竞赛的工程师。2. 赛题数据到手后先做这三件事再谈模型2.1 用 pandas 做一次全量数据体检拿到赛题数据的第一反应不应该是打开 Jupyter 就开始写模型而是先搞清楚数据长什么样。泰迪杯的赛题数据通常给的是 CSV 或 Excel字段名可能是中文编码可能是 GBK缺失值可能用“-”“null”“未知”混着表示。我一般会先跑一段体检脚本把每个字段的类型、缺失率、唯一值数量、分布情况一次性打出来。import pandas as pd import numpy as np # 读取时先指定编码中文赛题数据常见 GBK 或 UTF-8 df pd.read_csv(train.csv, encodinggbk) # 基础体检字段类型、缺失率、唯一值数 check pd.DataFrame({ dtype: df.dtypes, missing_rate: df.isnull().mean().round(4), nunique: df.nunique(), sample: [df[col].dropna().iloc[0] if df[col].notna().any() else None for col in df.columns] }) print(check.sort_values(missing_rate, ascendingFalse)) # 数值字段看分布快速判断有没有极端值 num_cols df.select_dtypes(include[np.number]).columns print(df[num_cols].describe().T[[mean, std, min, 50%, max]])这段代码的逻辑是先不碰数据只做观察。missing_rate超过 0.5 的字段要重点判断是直接丢弃还是填充nunique等于 1 的字段没有信息量可以直接删sample帮你快速识别“-”“未知”这类伪缺失值。参数上encoding一定要根据实际文件调整读错了会直接报 UnicodeDecodeError这时候换成encodingutf-8-sig或encodinggb18030再试。2.2 缺失值和异常值的处理顺序不能反很多人上来就填缺失值填完才发现异常值把填充的均值拉偏了。正确顺序是先识别异常值再处理缺失值。异常值识别用 IQR 或 3σ 都行但泰迪杯的业务数据往往不是正态分布IQR 更稳。# 先标记异常值不急着删 def mark_outliers(df, col): q1, q3 df[col].quantile([0.25, 0.75]) iqr q3 - q1 lower, upper q1 - 1.5 * iqr, q3 1.5 * iqr return (df[col] lower) | (df[col] upper) for col in num_cols: df[f{col}_is_outlier] mark_outliers(df, col) # 再处理缺失值数值字段用中位数类别字段用众数 for col in df.columns: if col.endswith(_is_outlier): continue if df[col].dtype object: df[col] df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else 未知) else: df[col] df[col].fillna(df[col].median())这里的关键参数是 IQR 的 1.5 倍系数这是统计学上的常规做法但在业务数据里如果异常值本身就是重要信号比如欺诈检测就不要删而是保留并加一个标记列。填充策略上中位数比均值抗异常值众数适合类别字段。注意mode()可能返回空加一层判断防止报错。2.3 特征工程优先做业务可解释的交叉特征泰迪杯的评分往往不只看精度还看你的方案是否合理。所以特征工程不要一上来就上自动特征生成工具先做业务上说得通的交叉特征。比如赛题涉及用户行为就做“活跃天数 × 平均单次时长”涉及交易就做“交易金额 / 交易笔数”。这些特征在答辩时你能讲清楚为什么这么构造。# 示例构造两个业务交叉特征 if 活跃天数 in df.columns and 平均单次时长 in df.columns: df[活跃时长乘积] df[活跃天数] * df[平均单次时长] if 交易金额 in df.columns and 交易笔数 in df.columns: df[单笔均价] df[交易金额] / df[交易笔数].replace(0, np.nan) df[单笔均价] df[单笔均价].fillna(df[单笔均价].median())replace(0, np.nan)是为了防止除零除零后产生的 inf 会污染后续建模。填充用中位数而不是 0因为 0 在金额场景下是有业务含义的不能随便用。3. 模型选型与调参别一上来就上深度学习3.1 结构化数据优先用树模型XGBoost 和 LightGBM 怎么选泰迪杯的赛题数据量通常在几万到几十万行字段几十到几百个这种规模下树模型几乎总是优于神经网络。XGBoost 和 LightGBM 是最常用的两个选哪个我的经验是数据量小于 10 万行、特征数小于 100用 XGBoost它的默认参数更稳数据量大于 10 万行或特征数超过 200用 LightGBM它的直方图算法在速度和内存上优势明显。import lightgbm as lgb from sklearn.model_selection import StratifiedKFold from sklearn.metrics import f1_score # 假设是分类任务标签列叫 label X df.drop(columns[label]) y df[label] # 五折交叉验证泰迪杯数据量不大5 折足够 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) oof_preds np.zeros(len(X)) models [] for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model lgb.LGBMClassifier( n_estimators1000, learning_rate0.05, max_depth6, num_leaves31, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, random_state42 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metricf1, callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] ) oof_preds[val_idx] model.predict(X_val) models.append(model) print(OOF F1:, f1_score(y, oof_preds, averageweighted))参数说明learning_rate0.05配合n_estimators1000是经典的慢学习率多树组合early_stopping(50)表示验证集指标 50 轮不提升就停防止过拟合。num_leaves31是 LightGBM 的核心参数控制树的复杂度一般从 31 开始调数据量大可以加到 63 或 127。subsample和colsample_bytree都设 0.8 是常用的防过拟合手段。3.2 类别不平衡时用 class_weight 还是过采样泰迪杯有些赛题的正负样本比例可能到 1:10 甚至更极端。这时候直接训练模型会偏向多数类。常见做法有两种设置class_weightbalanced让模型自动加权或者用 SMOTE 过采样。我的建议是优先用class_weight因为它不改变数据分布训练速度也快如果 F1 仍然上不去再试 SMOTE但要注意 SMOTE 只能在训练集上做验证集和测试集绝对不能碰。# 方案一class_weight model lgb.LGBMClassifier(class_weightbalanced, ...) # 方案二SMOTE只在训练折内做 from imblearn.over_sampling import SMOTE smote SMOTE(random_state42) X_train_res, y_train_res smote.fit_resample(X_train, y_train)注意 SMOTE 对高维稀疏数据效果一般如果特征数超过 500建议先做特征选择再过采样。3.3 用网格搜索还是贝叶斯调参泰迪杯备赛时间通常只有几天到两周网格搜索太慢随机搜索又不够精细。我一般用 Optuna 做贝叶斯调参20 到 30 轮就能找到不错的参数组合。import optuna def objective(trial): params { n_estimators: trial.suggest_int(n_estimators, 300, 1500), learning_rate: trial.suggest_float(learning_rate, 0.01, 0.1, logTrue), max_depth: trial.suggest_int(max_depth, 3, 10), num_leaves: trial.suggest_int(num_leaves, 15, 127), subsample: trial.suggest_float(subsample, 0.6, 1.0), colsample_bytree: trial.suggest_float(colsample_bytree, 0.6, 1.0), } model lgb.LGBMClassifier(**params, random_state42) model.fit(X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(30)]) return f1_score(y_val, model.predict(X_val), averageweighted) study optuna.create_study(directionmaximize) study.optimize(objective, n_trials30) print(study.best_params)logTrue用于学习率这种跨数量级的参数让采样在对数空间均匀。n_trials30是时间和效果的平衡点再多提升有限。4. 避坑与排查那些年我们踩过的雷4.1 数据泄漏验证集分数高得离谱现象交叉验证 F1 到 0.99提交后测试集只有 0.6。原因特征里混入了标签相关的信息比如用未来数据预测过去或者 ID 字段被当成了特征。解决检查每个特征在业务上是否在预测时间点之前可知ID 类字段直接删时间字段只保留历史窗口的统计量。4.2 编码问题导致中文列名乱码现象df.columns打印出来是\xe6\xb4\xbb\xe8\xb7\x83这种。原因读取时编码指定错误。解决先试gbk再试utf-8-sig还不行用chardet.detect(open(train.csv,rb).read())检测编码。注意 Windows 下 Excel 导出的 CSV 默认是 GBK。4.3 提交格式对不上评测脚本现象本地跑得好好的提交后提示格式错误。原因泰迪杯通常要求提交 CSV列名和顺序必须和样例一致索引列不能多也不能少。解决提交前用pd.read_csv(sample_submission.csv)读样例对比列名和数据类型用df.to_csv(submit.csv, indexFalse, encodingutf-8-sig)输出。4.4 内存不够导致训练中断现象LightGBM 训练到一半报 MemoryError。原因数据量太大或特征太多pandas 默认的 float64 占内存。解决把数值字段降为 float32类别字段用 category 类型分批读取数据。for col in df.select_dtypes(include[float64]).columns: df[col] df[col].astype(float32) for col in df.select_dtypes(include[object]).columns: df[col] df[col].astype(category)4.5 随机种子没固定导致结果不可复现现象每次跑出来的分数都不一样答辩时说不清哪个是最终结果。原因LightGBM、sklearn 的随机过程没固定。解决在模型初始化、交叉验证划分、采样环节都设random_state42并在代码开头统一设np.random.seed(42)。5. 提分技巧后处理与模型融合的实战细节到了备赛后期单模型分数基本稳定了想再往上走就得靠后处理和融合。后处理最常用的是阈值调整分类任务默认 0.5 阈值不一定最优用验证集画 P-R 曲线找到 F1 最大的阈值。from sklearn.metrics import precision_recall_curve # 用 OOF 预测概率找最优阈值 oof_proba np.zeros(len(X)) for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): model models[fold] oof_proba[val_idx] model.predict_proba(X.iloc[val_idx])[:, 1] precision, recall, thresholds precision_recall_curve(y, oof_proba) f1_scores 2 * precision * recall / (precision recall 1e-8) best_threshold thresholds[np.argmax(f1_scores)] print(Best threshold:, best_threshold) # 用最优阈值生成最终预测 final_pred (oof_proba best_threshold).astype(int)模型融合方面泰迪杯最实用的是加权平均和 Stacking。加权平均简单用验证集分数做权重Stacking 用逻辑回归做元学习器但要注意元学习器的训练数据必须用 OOF 预测否则会过拟合。from sklearn.linear_model import LogisticRegression # 假设有三个模型的 OOF 概率 stack_X np.column_stack([oof_proba_lgb, oof_proba_xgb, oof_proba_cat]) meta_model LogisticRegression() meta_model.fit(stack_X, y) final_proba meta_model.predict_proba(stack_X)[:, 1]融合的收益通常在 0.5 到 2 个百分点如果单模型已经 0.95 以上提升空间有限不如把时间花在特征工程和错误分析上。我自己的习惯是每次提交前留一份完整的代码和参数记录包括数据版本、特征列表、模型参数、随机种子这样即使隔几天再跑也能复现。备赛期间最怕的就是“昨天那个分数是怎么跑出来的”这种问题留好后悔药比多调一轮参更重要。希望帮到你。本文还有配套的精品资源点击获取