
简介2021年安徽省大数据与人工智能应用竞赛人工智能网络赛本科组赛题数据包面向该项赛事的高校参赛学生与指导教师完整收录人脸年龄预测和房价预测两大任务所需的数据文件可直接用于赛前模拟与模型验证。包内共1009个文件包括1000张人脸JPG图像及对应年龄标签4个CSV数据表train/val/test及任务表并附有3个Python脚本与2个说明文本整体约13.47MB目录紧凑、便于加载。数据集按17000:3000:3000划分训练、验证与测试集房源信息涵盖电梯、楼层、户型、区域、装修、面积、建筑时间等维度且存在部分缺失适合开展数据清洗、特征工程与回归建模实践。目前已有1066人学习下载参赛者可据此熟悉赛题数据格式减少资料收集成本专注算法调优。1. 从一份2021年安徽省AI竞赛赛题数据说起网络赛到底考什么去年帮学弟准备竞赛时翻遍全网发现省级人工智能竞赛的真题数据几乎没人分享培训机构放出来的又都是脱敏脱到没法用的玩具集。这份2021年安徽省大数据与人工智能应用竞赛人工智能方向网络赛本科组的赛题数据算是少见的“真题标本”——训练集、测试集、评分口径都在一个压缩包里。它解决的痛点很具体赛前你不知道真实比赛的数据长什么样、提交格式怎么定、评分规则按什么算而这份数据把整个流程完整还原了。适合三类人准备参赛的本科生、带赛队的指导老师以及想用真实竞赛数据练手却不想从Kaggle英文界面起步的从业者。拆完一遍你对省赛乃至同类数据竞赛的套路基本就心里有数了。2. 赛制还原与数据形态先搞清楚你拿到的是什么很多人拿到压缩包第一件事就是解压跑模型结果跑了两天发现连训练集和验证集都分错了。这种比赛数据的稀缺之处不在“有多少条记录”而在它把比赛当天你会在现场看到的所有材料完整复刻了一遍。所以第一步不是建模而是把赛制和数据形态彻底摸清。2.1 网络赛的赛程结构与评分口径安徽省大数据与人工智能应用竞赛的人工智能方向网络赛通常采用“限时线上提交”的形式队伍在规定的窗口期内下载数据、完成建模、提交预测结果文件。时间窗口短则48小时长则一周这个节奏决定了你的技术选型——没时间从头训深度模型也没时间做大规模超参搜索能快速出效果、容错率高的方案才是主线。评分口径一般分为两类。分类赛题多采用准确率或F1作为主指标回归赛题则看RMSE或MAE。这里有个关键细节很多省赛会把“提交格式合规性”作为前置门槛格式错了直接零分内容再好也没用。所以拿到数据的第一件事不是打开训练集而是找到README或赛题说明文档确认三件事任务类型是分类还是回归、评价指标是什么、提交文件的列名和顺序要求是什么。我一般会用下面这段逻辑先跑一遍信息确认而不是凭经验猜import pandas as pd import os # 先列目录看清数据包里有什么 for root, dirs, files in os.walk(./data): for f in files: print(os.path.join(root, f))这段代码的目的很朴素先把家底盘清楚。常见的解压结果是几十个文件堆在一个目录里不列一遍根本不知道哪个是训练集、哪个是样例提交文件。逻辑说明os.walk递归遍历目录把每个文件的相对路径打印出来方便你按文件名猜测用途。参数说明./data换成你实际解压路径如果文件多可以加上if f.endswith(.csv)只筛CSV减少输出噪音。2.2 数据包的文件构成与字段约定以这份2021年安徽省赛题数据为例解压后你大概率会看到四类文件它们在整场比赛中各司其职文件类型常见命名作用训练集train.csv / train_data.csv带标签的样本用于建模和交叉验证测试集test.csv / test_data.csv无标签样本你的预测对象样例提交sample_submit.csv / submit_example.csv规定提交格式列名和行序以此为准赛题说明README.md / 赛题说明.pdf任务背景、评分指标、提交要求字段约定上训练集一般由三部分构成唯一的样本ID列、若干特征列、一个目标列。测试集则只有ID和特征列目标列需要你预测后补齐。这里有个容易忽略的坑测试集的行顺序在评测时可能被重新打乱所以提交文件必须带上ID列评测系统按ID对齐打分不是按行号对齐。有个细节我特别提醒一下样例提交文件里通常只有几行示例真正的测试集可能有几万行。你要做的不是“照着样例填”而是“按样例的格式生成完整文件”。这中间差着一个pd.read_csv和pd.merge的距离但也差着零分和高分的距离。2.3 提交物与评测脚本怎么算分、怎么对齐数据竞赛的评分逻辑本质上是一次性调用评测系统读入你的提交文件和官方标注的测试集结果做比对按既定公式算出分数。所以你的提交文件必须和测试集在ID上一一对应不能多一行、不能少一行、不能乱序。这里我习惯的做法是写一个“提交前校验”脚本每次都强制跑一遍import pandas as pd # 假设test.csv是官方测试集pred.csv是你生成的预测结果 test pd.read_csv(test.csv) pred pd.read_csv(pred.csv) # 核对行数一致 assert len(pred) len(test), f行数不一致: pred {len(pred)}, test {len(test)} # 核对ID完全一致且顺序一致 assert (pred[id].values test[id].values).all(), ID顺序与测试集不一致 # 核对没有空值 assert pred[prediction].notna().all(), 预测结果存在空值 # 核对类型 print(pred[prediction].dtype) print(提交文件校验通过)逻辑说明assert不满足条件时会直接抛异常把问题暴露在提交之前。前两个检查确保行数和ID顺序和官方测试集完全对齐第三个防止你因为某些样本没预测出来留下空值。参数说明pred[prediction]要根据你实际的目标列名去改比如赛题是二分类目标列可能叫label回归任务可能叫value。这段脚本我每场比赛都用至少拦下过三次“忘记排序”的低级错误。3. 从CSV到特征矩阵清洗与特征工程的四个动作数据形态搞清楚了接下来才是重头戏把原始CSV变成能喂进模型的特征矩阵。这步做不好后面调参调得再勤也是白费力气。省赛数据的特点是“脏得真实”——缺失、异常、格式不规范全都有和你在教程里看到的整洁版完全不同。3.1 读入数据与建立基线先用最笨的模型跑通全流程我在任何赛题上做的第一件事都不是清洗而是用最朴素的逻辑先跑通全流程。目的是确认管道通畅——从读数据到出预测文件每个环节都能走通再去优化精度。import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score train pd.read_csv(train.csv) test pd.read_csv(test.csv) # 先把非数值列丢掉只留数值特征做基线 feature_cols train.select_dtypes(include[float64, int64]).columns feature_cols [c for c in feature_cols if c not in [id, label]] X train[feature_cols] y train[label] # 分层划分保证训练/验证集类别比例一致 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 用随机森林默认参数跑基线 clf RandomForestClassifier(n_estimators100, random_state42, n_jobs-1) clf.fit(X_train, y_train) val_pred clf.predict(X_val) print(fBaseline accuracy: {accuracy_score(y_val, val_pred):.4f})逻辑说明select_dtypes只保留数值列是为了建立基线时避开字符型特征的编码麻烦stratifyy做分层抽样保证验证集和训练集的类别比例一致这对类别不平衡的赛题尤其重要。参数说明random_state42固定随机种子确保每次跑出来的基线一致n_jobs-1让随机森林用满所有CPU核心省赛数据量一般不大这个参数不会让机器卡死。拿到基准确率之后你才有参照物——后面每做一个特征、每调一次参数都知道是进步还是退步。3.2 缺失值处理先看占比再决定填还是扔省赛数据的缺失值很少是随机出现的往往带有业务含义。比如用户行为数据里某个字段为空可能表示“该用户没做过这个操作”而不是“数据丢了”。所以我的处理原则是缺失率超过70%直接删列低于70%则要区分建模逻辑。import numpy as np # 统计每列缺失率 missing_ratio train.isnull().mean().sort_values(ascendingFalse) print(missing_ratio[missing_ratio 0]) # 按缺失率分策略处理 drop_cols missing_ratio[missing_ratio 0.7].index.tolist() print(f删除高缺失列: {drop_cols}) # 数值列用中位数填充类别列用众数填充 num_cols train.select_dtypes(include[float64, int64]).columns cat_cols train.select_dtypes(include[object]).columns for c in num_cols: if c not in drop_cols and train[c].isnull().sum() 0: train[c] train[c].fillna(train[c].median()) test[c] test[c].fillna(test[c].median()) for c in cat_cols: if c not in drop_cols and train[c].isnull().sum() 0: train[c] train[c].fillna(train[c].mode()[0]) test[c] test[c].fillna(test[c].mode()[0])逻辑说明isnull().mean()算出每列缺失率sort_values降序排列方便一眼看到最严重的列。数值列用中位数而不是均值填充是因为中位数对离群值更稳健类别列用众数填充相当于用“最常见的取值”去补。参数说明填充时test也必须同步做否则训练时模型看到的分布和预测时不一致这就是后面避坑章节要细说的“训练测试分布不一致”问题。这里有个很多人会忽略的动作填充前最好先看一眼缺失值到底长什么样。用train[col].value_counts(dropnaFalse)看看缺失前后的分布有时候你会发现“缺失”本身就是一个强特征——单独建一列is_missing标记它模型往往能学到规律。3.3 特征工程类别编码、数值分箱与时间展开特征工程是省赛里性价比最高的环节但也是最容易做过头的地方。我的经验是先做三类操作类别特征编码、数值特征分箱、时间特征展开。做完这三步特征矩阵的可用性通常已经超过原始数据一倍以上。# 类别编码对低频类别归并为other for c in cat_cols: if c not in drop_cols: freq train[c].value_counts(normalizeTrue) rare_cats freq[freq 0.01].index train[c] train[c].replace(rare_cats, other) test[c] test[c].replace(rare_cats, other) # 用LabelEncoder编码 from sklearn.preprocessing import LabelEncoder le LabelEncoder() train[c] le.fit_transform(train[c].astype(str)) test[c] le.transform(test[c].astype(str))逻辑说明value_counts(normalizeTrue)算出每个类别的占比占比低于1%的归并成other减少稀有类别带来的噪音。之后用LabelEncoder把字符串转成整数编码。参数说明这里的0.01并不是固定值——如果数据量大可以放宽到0.005如果数据量小0.02更合适。原则是合并后other类别的样本量至少要有几十个否则这个类别还是噪声。数值分箱一般用于对长尾分布的特征做处理。比如某个特征90%的值集中在0附近10%的值跨度很大直接喂给树模型会导致分裂点集中在密集区。用pd.qcut按分位数切成5箱变成有序类别特征往往比原始数值更稳for c in num_cols: if c not in drop_cols: # 尝试分位分箱箱数设为5 try: train[c _bin] pd.qcut(train[c], q5, labelsFalse, duplicatesdrop) test[c _bin] pd.qcut(test[c], q5, labelsFalse, duplicatesdrop) except ValueError: # 如果值不够分散直接跳过 pass逻辑说明pd.qcut按分位数切分让每箱的样本量大致相等比等距切分更适应偏态分布。duplicatesdrop处理边界值相同导致分位数重复的情况。参数说明q5是经验值特征取值特别多可以调到q10如果特征本身是离散计数比如只有0、1、2三个值跳过这步分箱反而会破坏信息。时间特征的展开坑最多。如果数据里有时间戳字段最简单的做法是先转成datetime类型再拆出年、月、日、星期、小时。但从竞赛经验看“距某个锚点时间的间隔”往往比绝对时间更有用。比如一个用户最后登录时间距当前时间的天数直接反映了活跃度。这个特征在用户行为类赛题里几乎是必做的。3.4 训练/验证划分分层抽样与时间序列的特殊处理省赛数据大多是表格型用随机划分交叉验证没问题。但有三种情况必须换划分策略类别极端不平衡、数据带时间顺序、同一样本多次出现。类别不平衡时用StratifiedKFold时间序列数据必须按时间截断不能用未来数据训练去预测过去同一样本多次出现时要按“组”划分而不是按“行”划分否则同一组样本会同时出现在训练集和验证集里造成数据泄露。from sklearn.model_selection import StratifiedKFold # 5折分层交叉验证 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) 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] print(fFold {fold}: train {len(train_idx)}, val {len(val_idx)})逻辑说明StratifiedKFold保证每折的类别比例和全量数据一致是分类赛题的标准选择。shuffleTrue打乱顺序避免原始数据的排布顺序影响划分。参数说明折数n_splits5是权衡——折数越多验证越稳但训练越慢数据量上万时可以试10折几千条的话5折足够。划分策略直接决定你后面所有调参判断是否可靠。划分错了你看到的验证分数就是假的后面第5章的“本地CV与线上分数背离”就是从这里埋下的根。4. 建模与调参把本地CV从基线拉上去的实操路径数据竞赛这件事有个残酷的规律特征决定上限模型决定你离上限有多近。当特征工程做完各队伍之间的差距就转移到建模和调参的执行力上了。省赛时间窗口短你需要一套“快速迭代、不迷路”的建模路径。4.1 为什么先上LightGBM树模型的适用边界我的建议是省赛数据第一刀直接上LightGBM别先玩神经网络。理由有三个表格数据上树模型的效果通常不输深度学习训练速度快一轮跑完只要几十秒对缺失值和异常值容错高不需要精细的标准化。import lightgbm as lgb from sklearn.model_selection import cross_val_score model lgb.LGBMClassifier( n_estimators500, learning_rate0.05, num_leaves31, max_depth7, subsample0.8, colsample_bytree0.8, random_state42, verbose-1 ) scores cross_val_score(model, X, y, cv5, scoringaccuracy) print(fCV accuracy: {scores.mean():.4f} (/- {scores.std():.4f}))逻辑说明n_estimators500配合learning_rate0.05是树模型的经典组合——学习率低就需要更多树来收敛但不容易过拟合。num_leaves31和max_depth7控制树复杂度这两个参数是LightGBM调参的核心。subsample0.8和colsample_bytree0.8分别做行采样和列采样既加速训练又防过拟合。参数说明verbose-1关闭训练日志省赛场景下你不想每一轮迭代都刷屏scoringaccuracy要换成赛题实际指标如果是F1就写scoringf1。这里要说明树模型的适用边界如果特征全是高基数类别且类别数上万比如用户ID树模型会很吃力此时嵌入层神经网络更合适——但省赛数据的特征维度一般不会到这个量级所以LightGBM是第一选择。4.2 参数搜索三个回合的调参节奏调参最忌讳一上来就全参数网格搜索那会让你的机器在跑完之前你就已经提交了。我习惯用“粗调→细调→微调”三个回合收敛。第一回合先固定learning_rate0.1搜索num_leaves和max_depth第二回合固定树结构搜索subsample和colsample_bytree第三回合把learning_rate降到0.01加大n_estimators做收尾。每回合只用GridSearchCV搜少数参数避免维度爆炸from sklearn.model_selection import GridSearchCV param_grid { num_leaves: [15, 31, 63], max_depth: [5, 7, 9] } grid GridSearchCV( lgb.LGBMClassifier(learning_rate0.1, random_state42, verbose-1), param_grid, cv3, scoringaccuracy, n_jobs-1 ) grid.fit(X, y) print(fBest params: {grid.best_params_}) print(fBest score: {grid.best_score_:.4f})逻辑说明GridSearchCV对每组参数组合做交叉验证选得分最高的组合。cv3比cv5快粗调阶段够用。参数说明num_leaves从15到63跨度三倍是因为这个参数对模型容量的影响最大值得先摸清方向max_depth和num_leaves存在联动关系max_depth限制树的深度num_leaves限制叶子数两个一起搜才能避免一个限制失效。n_jobs-1让多个参数组合并行跑省赛机器如果不是太差基本几分钟能出结果。细调的时候把learning_rate0.05固定下来搜subsample和colsample_bytree范围在0.6到0.9之间。这两个参数控制随机性调对了能稳一点调过头会欠拟合。微调阶段就是降学习率、加大n_estimators跑一个早停model lgb.LGBMClassifier( n_estimators2000, learning_rate0.01, num_leaves31, max_depth7, subsample0.8, colsample_bytree0.8, random_state42, verbose-1 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(stopping_rounds100)] )逻辑说明early_stopping在验证集得分连续100轮不提升时提前终止训练既防止过拟合又省时间。eval_set传入验证集让模型在训练过程中实时监控验证指标。参数说明stopping_rounds100是常用值学习率0.01时模型收敛慢早停阈值太小容易在触底前被截断100轮是安全线。4.3 集成与后处理Stacking、加权与伪标签的取舍到了集成这一步省赛队伍就开始分化了。最实用的组合不是Stacking而是“不同模型的加权平均”。原因很简单Stacking需要再训练一个元模型不仅费时间数据量小时还容易过拟合加权平均只要调几个权重就行。from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression # 训练三个差异化的模型 lgb_model lgb.LGBMClassifier(n_estimators300, random_state42) rf_model RandomForestClassifier(n_estimators300, random_state42) lr_model LogisticRegression(max_iter1000) # 各自交叉验证得到OOF预测 # 这里以LightGBM为例其余两个模型同理 oof_lgb np.zeros((len(X), 1)) skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(X, y): lgb_model.fit(X.iloc[train_idx], y.iloc[train_idx]) oof_lgb[val_idx] lgb_model.predict_proba(X.iloc[val_idx])[:, 1].reshape(-1, 1) # 按权重融合先给0.5/0.3/0.2 final_pred 0.5 * oof_lgb[:, 0] 0.3 * oof_rf[:, 0] 0.2 * oof_lr[:, 0]逻辑说明OOFOut-of-Fold预测让每个模型都只预测自己没见过的样本避免融合时用训练集上的表现欺骗自己。加权平均的权重初始按经验给之后用验证集上的搜索找到最优权重。参数说明[:, 1]取的是正类的预测概率二分类时要确认predict_proba输出哪一列是正类——LightGBM默认按类别排序如果标签是0/1第二列才是正类概率。伪标签技术的逻辑是用训练好的模型预测测试集把置信度高的预测当成标签加回训练集再训练一轮。它在数据量小时有用但风险也大——伪标签错误会被模型放大。省赛场景下我一般只在最后冲刺阶段用且只挑置信度超过0.9的样本。5. 避坑指南五个真实翻车现场与修复办法省赛数据竞赛的坑十个里有八个不在模型上而在数据处理的细节里。下面五条是我在带赛队和复现往年赛题时踩过的或者是看队友踩过的每一条都是真金白银换来的。5.1 漏掉ID列评测系统直接报“列名不匹配”现象提交文件里忘记带ID列或者把ID列放到了最后一列评测系统报错“列名不匹配”或“行数不一致”分数为零。原因评测系统按列名读取提交文件先校验ID列再校验预测列。你以为“顺序对就行”但系统只认列名不认顺序。解决每次提交前跑一遍第2.3节的校验脚本用assert强制检查列名和顺序。我自己的习惯是把校验脚本存成check_submit.py每改一次预测就重跑一次——不是每次都有问题但每一场至少有一次会拦下低级错误。5.2 把测试集统计量混进训练集现象本地交叉验证分数极高提交后线上分数断崖式下跌。原因特征工程阶段你可能是用全量数据包括测试集去计算填充值或标准化参数比如用fit_transform而不是fit再transform导致测试集的信息泄露到训练集。交叉验证时看起来完美但线上测试时模型见过的“信息”其实比理论上多实际表现自然缩水。解决所有统计量只能从训练集计算然后应用到测试集。填充中位数、标准化均值和方差、分箱的边界全都先fit训练集再transform测试集。简单说任何涉及“全局统计量”的操作都要写成train_df和test_df分开处理或者用sklearn的Pipeline保证流程一致。5.3 训练集和测试集分布不一致本地CV再高也白搭现象本地交叉验证0.85提交线上0.60你没改任何代码分数就是上不去。原因赛题数据可能做过特殊处理比如训练集来自某段时间的样本测试集来自另一段时间分布自然偏移。本地CV只能反映训练集内部的稳定性反映不了分布偏移。解决拿到数据后先对比训练集和测试集的特征分布。用describe()看均值、方差画分布图看形状。如果发现明显偏移优先做两件事一是用“对抗验证”训练一个分类器区分训练/测试样本如果AUC很高说明两套数据差异明显二是对偏移特征做标准化或直接删除——模型在一个分布上学到的规律在另一个分布上不成立。5.4 随机种子玄学同一个模型两次跑出不同分数现象代码一模一样只是换了个随机种子验证分数从0.82变成0.80影响了你的调参判断。原因数据划分、模型初始化、特征编码过程都带随机性。种子不同划分出的训练/验证集不同模型初始化权重不同结果自然有波动。解决在代码最开头固定所有随机源random.seed(42)、np.random.seed(42)、LightGBM和随机森林都传random_state42。更重要的是调参对比时用同一个固定种子保证变量唯一最后提交前换3个种子跑一遍取平均那个分数才是真实水平的估计。5.5 提交文件的预测值没经过后处理现象二分类赛题要求提交0/1标签你直接提交了预测概率系统按阈值0.5判分分数低于预期。原因评测系统可能期望概率文件也可能期望标签文件你没看README就按经验提交。或者更隐蔽——赛题要求F1分数而F1对阈值敏感直接用0.5做阈值不是最优。解决先确认README里样例提交文件是小数还是整数。如果是标签就要对概率做阈值处理如果指标是F1用验证集搜索最优阈值而不是默认0.5。这一步常常能拉回几个百分点的分。6. 验证路径概率校准与一次完整提交的复盘到这一步模型训练完了、提交文件也生成好了但距离拿分还差最后一道工序验证预测结果的质量别急着交卷。二分类最高的操作是先看预测概率的分布。用pd.Series(val_prob).hist(bins50)看一眼如果所有概率都集中在0.4到0.6之间说明模型对样本很不自信——这时候你交出去的概率文件风险很大。我一般会先对预测概率做校准from sklearn.calibration import CalibratedClassifierCV # 用验证集做概率校准 calibrated CalibratedClassifierCV( model, methodsigmoid, cv3 ) calibrated.fit(X_train, y_train) val_prob_calibrated calibrated.predict_proba(X_val)[:, 1]逻辑说明CalibratedClassifierCV用交叉验证拟合一个校准曲线把模型的输出概率映射到更接近真实概率的区间。methodsigmoid适合样本量少的情况用逻辑回归做校准数据量大时可以换methodisotonic拟合能力更强但有过拟合风险。参数说明cv3是校准用的折数不一定要和建模的交叉验证折数一致3折即可。校准之后再用验证集搜索最优阈值——如果赛题是F1指标这一步直接关系到得分from sklearn.metrics import f1_score best_threshold 0.5 best_f1 0 for thr in np.arange(0.3, 0.7, 0.01): pred_label (val_prob_calibrated thr).astype(int) score f1_score(y_val, pred_label) if score best_f1: best_f1 score best_threshold thr print(fBest threshold: {best_threshold:.2f}, F1: {best_f1:.4f})逻辑说明穷举0.3到0.7之间的所有阈值用验证集F1分数挑最优的。这样处理的逻辑是类别不平衡时默认的0.5阈值往往不是最优解——正类样本少阈值压低一点能捞回更多正类但代价是误报增多需要验证集来定量权衡。参数说明0.3到0.7的搜索范围是经验区间超出这个范围基本说明模型本身有问题调阈值救不回来。最后一步在提交前做一个人工抽检从验证集里随机抽20个样本把模型预测结果、预测概率、真实标签、对应特征值打印出来肉眼扫一遍。你会看到模型漏掉的样本长什么样——特征分布是否异常、是不是训练集里类似样本太少。这个动作习惯我从第一次打比赛就养成了那时候吃过“模型记住的是特征规律不是业务规律”的亏导致提交前完全没发现模型学偏了。从那以后我每次提交前都会强制走一遍“分布检查、概率校准、阈值搜索、人工抽检”四步没有一次例外。希望这套流程对你有用。本文还有配套的精品资源点击获取