简介:这份资料面向备战“泰迪杯”全国数据挖掘挑战赛的高校学生与指导教师,系统梳理竞赛背景、题型分类与参赛流程,帮助零基础或有一定机器学习经验的读者快速建立备赛框架。压缩包内共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", encoding="gbk") # 基础体检:字段类型、缺失率、唯一值数 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", ascending=False)) # 数值字段看分布,快速判断有没有极端值 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,这时候换成encoding="utf-8-sig"或encoding="gb18030"再试。
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_splits=5, shuffle=True, random_state=42) 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_estimators=1000, learning_rate=0.05, max_depth=6, num_leaves=31, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=0.1, random_state=42 ) model.fit( X_train, y_train, eval_set=[(X_val, y_val)], eval_metric="f1", 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, average="weighted"))参数说明:learning_rate=0.05配合n_estimators=1000是经典的慢学习率多树组合,early_stopping(50)表示验证集指标 50 轮不提升就停,防止过拟合。num_leaves=31是 LightGBM 的核心参数,控制树的复杂度,一般从 31 开始调,数据量大可以加到 63 或 127。subsample和colsample_bytree都设 0.8 是常用的防过拟合手段。
3.2 类别不平衡时用 class_weight 还是过采样
泰迪杯有些赛题的正负样本比例可能到 1:10 甚至更极端。这时候直接训练模型会偏向多数类。常见做法有两种:设置class_weight="balanced"让模型自动加权,或者用 SMOTE 过采样。我的建议是优先用class_weight,因为它不改变数据分布,训练速度也快;如果 F1 仍然上不去,再试 SMOTE,但要注意 SMOTE 只能在训练集上做,验证集和测试集绝对不能碰。
# 方案一:class_weight model = lgb.LGBMClassifier(class_weight="balanced", ...) # 方案二:SMOTE,只在训练折内做 from imblearn.over_sampling import SMOTE smote = SMOTE(random_state=42) 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, log=True), "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_state=42) 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), average="weighted") study = optuna.create_study(direction="maximize") study.optimize(objective, n_trials=30) print(study.best_params)log=True用于学习率这种跨数量级的参数,让采样在对数空间均匀。n_trials=30是时间和效果的平衡点,再多提升有限。
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", index=False, encoding="utf-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_state=42,并在代码开头统一设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 以上,提升空间有限,不如把时间花在特征工程和错误分析上。我自己的习惯是:每次提交前留一份完整的代码和参数记录,包括数据版本、特征列表、模型参数、随机种子,这样即使隔几天再跑也能复现。备赛期间最怕的就是“昨天那个分数是怎么跑出来的”这种问题,留好后悔药比多调一轮参更重要。希望帮到你。
本文还有配套的精品资源,点击获取