☰
2021安徽省AI竞赛赛题数据实操:特征工程、调参与避坑指南
2026/10/2 19:57:59 网站建设 项目流程

简介: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_size=0.2, stratify=y, random_state=42 ) # 用随机森林默认参数跑基线 clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) clf.fit(X_train, y_train) val_pred = clf.predict(X_val) print(f'Baseline accuracy: {accuracy_score(y_val, val_pred):.4f}')

逻辑说明:select_dtypes只保留数值列,是为了建立基线时避开字符型特征的编码麻烦;stratify=y做分层抽样,保证验证集和训练集的类别比例一致,这对类别不平衡的赛题尤其重要。参数说明:random_state=42固定随机种子,确保每次跑出来的基线一致;n_jobs=-1让随机森林用满所有CPU核心,省赛数据量一般不大,这个参数不会让机器卡死。

拿到基准确率之后,你才有参照物——后面每做一个特征、每调一次参数,都知道是进步还是退步。

3.2 缺失值处理:先看占比再决定填还是扔

省赛数据的缺失值很少是随机出现的,往往带有业务含义。比如用户行为数据里,某个字段为空可能表示“该用户没做过这个操作”,而不是“数据丢了”。所以我的处理原则是:缺失率超过70%直接删列,低于70%则要区分建模逻辑。

import numpy as np # 统计每列缺失率 missing_ratio = train.isnull().mean().sort_values(ascending=False) 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(dropna=False)看看缺失前后的分布,有时候你会发现“缺失”本身就是一个强特征——单独建一列is_missing标记它,模型往往能学到规律。

3.3 特征工程:类别编码、数值分箱与时间展开

特征工程是省赛里性价比最高的环节,但也是最容易做过头的地方。我的经验是先做三类操作:类别特征编码、数值特征分箱、时间特征展开。做完这三步,特征矩阵的可用性通常已经超过原始数据一倍以上。

# 类别编码:对低频类别归并为'other' for c in cat_cols: if c not in drop_cols: freq = train[c].value_counts(normalize=True) 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(normalize=True)算出每个类别的占比,占比低于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], q=5, labels=False, duplicates='drop') test[c + '_bin'] = pd.qcut(test[c], q=5, labels=False, duplicates='drop') except ValueError: # 如果值不够分散,直接跳过 pass

逻辑说明:pd.qcut按分位数切分,让每箱的样本量大致相等,比等距切分更适应偏态分布。duplicates='drop'处理边界值相同导致分位数重复的情况。参数说明:q=5是经验值,特征取值特别多可以调到q=10;如果特征本身是离散计数(比如只有0、1、2三个值),跳过这步,分箱反而会破坏信息。

时间特征的展开坑最多。如果数据里有时间戳字段,最简单的做法是先转成datetime类型,再拆出年、月、日、星期、小时。但从竞赛经验看,“距某个锚点时间的间隔”往往比绝对时间更有用。比如一个用户最后登录时间距当前时间的天数,直接反映了活跃度。这个特征在用户行为类赛题里几乎是必做的。

3.4 训练/验证划分:分层抽样与时间序列的特殊处理

省赛数据大多是表格型,用随机划分交叉验证没问题。但有三种情况必须换划分策略:类别极端不平衡、数据带时间顺序、同一样本多次出现。

类别不平衡时用StratifiedKFold;时间序列数据必须按时间截断,不能用未来数据训练去预测过去;同一样本多次出现时,要按“组”划分而不是按“行”划分,否则同一组样本会同时出现在训练集和验证集里,造成数据泄露。

from sklearn.model_selection import StratifiedKFold # 5折分层交叉验证 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) 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(f'Fold {fold}: train {len(train_idx)}, val {len(val_idx)}')

逻辑说明:StratifiedKFold保证每折的类别比例和全量数据一致,是分类赛题的标准选择。shuffle=True打乱顺序,避免原始数据的排布顺序影响划分。参数说明:折数n_splits=5是权衡——折数越多验证越稳但训练越慢;数据量上万时可以试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_estimators=500, learning_rate=0.05, num_leaves=31, max_depth=7, subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1 ) scores = cross_val_score(model, X, y, cv=5, scoring='accuracy') print(f'CV accuracy: {scores.mean():.4f} (+/- {scores.std():.4f})')

逻辑说明:n_estimators=500配合learning_rate=0.05是树模型的经典组合——学习率低就需要更多树来收敛,但不容易过拟合。num_leaves=31和max_depth=7控制树复杂度,这两个参数是LightGBM调参的核心。subsample=0.8和colsample_bytree=0.8分别做行采样和列采样,既加速训练又防过拟合。参数说明:verbose=-1关闭训练日志,省赛场景下你不想每一轮迭代都刷屏;scoring='accuracy'要换成赛题实际指标,如果是F1就写scoring='f1'。

这里要说明树模型的适用边界:如果特征全是高基数类别且类别数上万(比如用户ID),树模型会很吃力,此时嵌入层神经网络更合适——但省赛数据的特征维度一般不会到这个量级,所以LightGBM是第一选择。

4.2 参数搜索:三个回合的调参节奏

调参最忌讳一上来就全参数网格搜索,那会让你的机器在跑完之前你就已经提交了。我习惯用“粗调→细调→微调”三个回合收敛。

第一回合先固定learning_rate=0.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_rate=0.1, random_state=42, verbose=-1), param_grid, cv=3, scoring='accuracy', n_jobs=-1 ) grid.fit(X, y) print(f'Best params: {grid.best_params_}') print(f'Best score: {grid.best_score_:.4f}')

逻辑说明:GridSearchCV对每组参数组合做交叉验证,选得分最高的组合。cv=3比cv=5快,粗调阶段够用。参数说明:num_leaves从15到63跨度三倍,是因为这个参数对模型容量的影响最大,值得先摸清方向;max_depth和num_leaves存在联动关系,max_depth限制树的深度,num_leaves限制叶子数,两个一起搜才能避免一个限制失效。n_jobs=-1让多个参数组合并行跑,省赛机器如果不是太差,基本几分钟能出结果。

细调的时候,把learning_rate=0.05固定下来,搜subsample和colsample_bytree,范围在0.6到0.9之间。这两个参数控制随机性,调对了能稳一点,调过头会欠拟合。微调阶段就是降学习率、加大n_estimators,跑一个早停:

model = lgb.LGBMClassifier( n_estimators=2000, learning_rate=0.01, num_leaves=31, max_depth=7, subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1 ) model.fit( X_train, y_train, eval_set=[(X_val, y_val)], callbacks=[lgb.early_stopping(stopping_rounds=100)] )

逻辑说明:early_stopping在验证集得分连续100轮不提升时提前终止训练,既防止过拟合又省时间。eval_set传入验证集,让模型在训练过程中实时监控验证指标。参数说明:stopping_rounds=100是常用值,学习率0.01时模型收敛慢,早停阈值太小容易在触底前被截断,100轮是安全线。

4.3 集成与后处理:Stacking、加权与伪标签的取舍

到了集成这一步,省赛队伍就开始分化了。最实用的组合不是Stacking,而是“不同模型的加权平均”。原因很简单:Stacking需要再训练一个元模型,不仅费时间,数据量小时还容易过拟合;加权平均只要调几个权重就行。

from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression # 训练三个差异化的模型 lgb_model = lgb.LGBMClassifier(n_estimators=300, random_state=42) rf_model = RandomForestClassifier(n_estimators=300, random_state=42) lr_model = LogisticRegression(max_iter=1000) # 各自交叉验证,得到OOF预测 # 这里以LightGBM为例,其余两个模型同理 oof_lgb = np.zeros((len(X), 1)) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) 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]

逻辑说明:OOF(Out-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_state=42。更重要的是,调参对比时用同一个固定种子,保证变量唯一;最后提交前换3个种子跑一遍取平均,那个分数才是真实水平的估计。

5.5 提交文件的预测值没经过后处理

现象:二分类赛题要求提交0/1标签,你直接提交了预测概率,系统按阈值0.5判分,分数低于预期。

原因:评测系统可能期望概率文件,也可能期望标签文件,你没看README就按经验提交。或者更隐蔽——赛题要求F1分数,而F1对阈值敏感,直接用0.5做阈值不是最优。

解决:先确认README里样例提交文件是小数还是整数。如果是标签,就要对概率做阈值处理;如果指标是F1,用验证集搜索最优阈值,而不是默认0.5。这一步常常能拉回几个百分点的分。

6. 验证路径:概率校准与一次完整提交的复盘

到这一步,模型训练完了、提交文件也生成好了,但距离拿分还差最后一道工序:验证预测结果的质量,别急着交卷。

二分类最高的操作是先看预测概率的分布。用pd.Series(val_prob).hist(bins=50)看一眼,如果所有概率都集中在0.4到0.6之间,说明模型对样本很不自信——这时候你交出去的概率文件风险很大。我一般会先对预测概率做校准:

from sklearn.calibration import CalibratedClassifierCV # 用验证集做概率校准 calibrated = CalibratedClassifierCV( model, method='sigmoid', cv=3 ) calibrated.fit(X_train, y_train) val_prob_calibrated = calibrated.predict_proba(X_val)[:, 1]

逻辑说明:CalibratedClassifierCV用交叉验证拟合一个校准曲线,把模型的输出概率映射到更接近真实概率的区间。method='sigmoid'适合样本量少的情况,用逻辑回归做校准;数据量大时可以换method='isotonic',拟合能力更强但有过拟合风险。参数说明:cv=3是校准用的折数,不一定要和建模的交叉验证折数一致,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(f'Best threshold: {best_threshold:.2f}, F1: {best_f1:.4f}')

逻辑说明:穷举0.3到0.7之间的所有阈值,用验证集F1分数挑最优的。这样处理的逻辑是,类别不平衡时默认的0.5阈值往往不是最优解——正类样本少,阈值压低一点能捞回更多正类,但代价是误报增多,需要验证集来定量权衡。参数说明:0.3到0.7的搜索范围是经验区间,超出这个范围基本说明模型本身有问题,调阈值救不回来。

最后一步,在提交前做一个人工抽检:从验证集里随机抽20个样本,把模型预测结果、预测概率、真实标签、对应特征值打印出来,肉眼扫一遍。你会看到模型漏掉的样本长什么样——特征分布是否异常、是不是训练集里类似样本太少。这个动作习惯我从第一次打比赛就养成了,那时候吃过“模型记住的是特征规律不是业务规律”的亏,导致提交前完全没发现模型学偏了。从那以后我每次提交前都会强制走一遍“分布检查、概率校准、阈值搜索、人工抽检”四步,没有一次例外。希望这套流程对你有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询