2021安徽AI竞赛赛题数据实战:从数据处理到模型提交全流程
发布时间:2026/9/29 15:43:09来源:尧图网络
简介面向备战2021年安徽省大数据与人工智能应用竞赛人工智能网络赛本科组的选手及机器学习学习者这份资料包含当年赛题的两部分核心数据人脸图像和对应年龄标签用于年龄估计任务电梯、楼层、户型、区域、装修情况、面积、建筑时间等房源字段用于预测房屋价格。数据按训练集、验证集、测试集约17000、3000、3000划分部分字段有缺失能较好检验数据清洗与特征处理功底。压缩包共1009个文件以1000张JPG人脸图像和4个CSV数据表为主体数据表对应训练、验证、测试等赛题数据另附Python脚本与TXT说明辅助理解整体仅13.47MB。当前已有1066人浏览学习。借助这些原始数据学习者可跑通从数据读取、特征工程到模型训练与结果评估的完整流程无论参赛备赛还是巩固机器学习实战能力都有实际价值。1. 这份赛题数据到底考什么从「2021年安徽省大数据与人工智能应用竞赛人工智能(网络赛)-本科组赛题数据」说起很多人搜到这份赛题数据第一反应是「里面有标准答案」。实际上数据包里没有标准答案只有带标签的训练样本、不带标签的待预测样本和提交模板评分靠主办方后台脚本不在数据包里。所以玩法很明确在有限时间内从训练样本里学规律在测试样本上做出尽量接近真实标签的预测再按模板格式提交。这份数据适合三类人当年参赛想复盘的本科组选手、准备下一届比赛想熟悉真实题型的学生、拿它当教学案例或大数据课程设计素材的老师。一个带明确评分导向的赛题数据本身就是完整的人工智能项目实战样本。后面所有步骤以「拿到手是一个压缩包」为前提展开处理思路可平移到绝大多数同类竞赛。2. 数据落地与全局体检先把赛题数据变成能跑的训练集拿到数据的第一个动作不是建模而是把它变成一份「读得进、看得懂、跑得通」的 DataFrame。很多人在这一步图快直接双击打开看一眼就开始了等写到建模代码才发现列名带隐藏字符、行数和提交模板对不上返工成本极高。我一般会先用十分钟做一次全局体检把赛题数据的规模、字段类型、缺失情况和标签分布全部摸一遍再决定后面的处理路线。2.1 解压第一眼先看文件组织不要急着读 CSV这类竞赛数据包的常见组织方式是一个说明文档配若干数据文件。说明文档通常会写明字段含义、训练集与测试集的行数约定、提交格式和评分方式它比网上任何二手介绍都可靠。先看一眼解压出来的文件列表unzip -o ai_competition_2021_data.zip -d ./data cd ./data ls -lh find . -maxdepth 2 -type f | sortunzip后面的参数-o表示覆盖解压避免重复解压时提示交互-d指定解压到./data目录。ls -lh看的是文件大小find用来看完整路径和嵌套结构有些包会把训练集、测试集、提交模板分放在不同子目录里只靠肉眼扫文件名容易漏。如果包里有 README 或题目说明文件马上打开。我会先确认四件事训练样本数量、测试样本数量、字段个数、评分指标。这四件事决定后面所有参数。比如测试集有单独文件就要保证预处理时用同一套规则处理训练和测试评分指标是准确率还是 F1直接决定类别不均衡时要不要做重采样。多数竞赛为了避免参赛队伍手工标注测试集测试样本一般不会太少但也不会大到本地机器跑不动徘徊在几千到几万行的量级是常态。2.2 读取赛题数据的三件套编码、分隔符、列名规范化赛题数据文件最常见的格式是 CSV但它在不同操作系统和导出工具手里长得很不一样。最典型的问题有三个中文数据用了 GBK 编码、分隔符不是逗号而是\t、列名里带着前后空格或不可见字符。假设包里的主数据文件叫train.csv和test.csv实际文件名以你解压出来的为准下面的读取逻辑不用变import glob import pandas as pd files glob.glob(./data/*.csv) print(找到的文件:, files) def read_csv_auto(path): for enc in [utf-8, gbk, utf-8-sig, latin1]: try: df pd.read_csv(path, encodingenc, sepNone, enginepython) print(f{path} 用 {enc} 读取成功, 形状: {df.shape}) return df except UnicodeDecodeError: continue except Exception as e: print(f{path} 用 {enc} 读取失败:, e) raise ValueError(f无法读取 {path}) train read_csv_auto(./data/train.csv) test read_csv_auto(./data/test.csv)sepNone配合enginepython是让 pandas 自己猜分隔符对\t、逗号、空格混合的情况比较稳代价是速度慢一点。utf-8-sig专门处理带 BOM 的 Excel 导出文件gbk处理 Windows 中文环境导出的文件latin1是兜底方案保证文件能被读进来而不是直接崩掉。读取成功之后先把列名里肉眼看不见的空格清理掉统一成小写带下划线的风格。这一步省的是后面的命手写特征公式时不用再纠结某个列到底叫User ID还是user_id。train.columns [str(c).strip().replace( , _).lower() for c in train.columns] test.columns [str(c).strip().replace( , _).lower() for c in test.columns] print(train.head(3).T)strip()去掉首尾空格replace( , _)把列名里的空格替换成下划线.lower()统一小写。列名规范化之后字典访问和get_dummies展开都不容易打错键名。2.3 标签与特征的全局体检缺失率、分布、字段类型数据读进来了下一件事是弄清楚每列长什么样。我会同时做三个动作看字段类型分布、看缺失率、看标签分布。这三个数字能过滤掉一大半后期故障。def profile_df(df, namedataset): info pd.DataFrame({ dtype: df.dtypes.astype(str), n_unique: df.nunique(), missing_ratio: df.isna().mean().round(4), }) print(f {name} 字段体检 ) print(info.sort_values(missing_ratio, ascendingFalse)) return info train_info profile_df(train, train) if label in train.columns: print(标签分布:) print(train[label].value_counts(normalizeTrue)) print(训练集样本数:, len(train)) print(测试集样本数:, len(test))df.isna().mean()直接给出每列的缺失比例缺失率超过 0.5 的列要么扔掉要么单独做「是否有值」的特征nunique()对离散特征尤其重要一个号称数值型的列如果只有三五个不同的取值那它更可能是类别编码而不是连续值。标签分布用normalizeTrue看占比如果某个类别占了九成以上后续训练时要考虑class_weight不然模型学出来的就是「全预测多数类」的复读机。3. 建模前的特征工程与数据切分把赛题数据从「能读」变「能学」特征工程的价值在竞赛场景里被高估也被低估。高估的是那些炫技的组合特征低估的是「先清理干净再做基础编码」这套基本功。网络赛时间紧最稳妥的做法是先把缺失值、类别编码、字段筛选这三件事做扎实让模型吃到的是一份语义干净、类型正确的输入。3.1 缺失值处理要按列的类型分开处理缺失值处理最大的误区是一刀切要么全用均值填要么全删行。实战里我按列的类型分三套规则连续数值列用中位数填充因为中位数对分布偏斜和离群点不敏感低基数的类别列用众数填充同时保留一个「该列原本缺失」的布尔特征高基数的类别列不填充直接把缺失当作一种类别值参与编码。这样做的理由是模型不止需要「缺失变成什么值」更需要「这里缺过」这个信号本身。import pandas as pd def fill_missing_by_type(df, ignore_cols(label, id)): df df.copy() for col in df.columns: if col in ignore_cols or df[col].isna().sum() 0: continue if pd.api.types.is_numeric_dtype(df[col]): # 数值列中位数填充并记录缺失标记 df[col _is_missing] df[col].isna().astype(int) df[col] df[col].fillna(df[col].median()) else: # 类别列填充缺失标记不强行猜众数 df[col] df[col].fillna(__MISSING__) return df train fill_missing_by_type(train) test fill_missing_by_type(test)pd.api.types.is_numeric_dtype比np.issubdtype更省事能覆盖 int、float、bool 这些常见数值类型。数值列生成_is_missing标记列是因为缺失本身有时和标签相关——比如用户没填收入往往代表收入不稳定这类模式模型单独从数值里学不出来。类别列填__MISSING__而不是众数是为了避免把「缺失」这种特殊状态强行归入某一个常见类别让编码阶段自己决定怎么对待它。3.2 类别特征编码的次序不能一上来就 LabelEncoder很多人拿到赛题数据里的字符串列第一反应就是LabelEncoder把它变成 0、1、2。这个操作对树模型勉强能用对线性模型就是灾难等于给类别强加了本不存在的顺序关系。我的默认顺序是这样基数低不超过 20 个取值的类别列用 one-hot基数高几百上千个取值的用计数编码只有标签列才允许用 LabelEncoder。# 低基数类别one-hot low_card_cols [c for c in train.columns if train[c].dtype object and train[c].nunique() 20] train pd.get_dummies(train, columnslow_card_cols) test pd.get_dummies(test, columnslow_card_cols) # 高基数类别计数编码出现次数本身作为特征 for col in [high_card_col_1, high_card_col_2]: # 换成实际的高基数列名 if col not in train.columns: continue train[col _count] train.groupby(col)[col].transform(count) test[col _count] test.groupby(col)[col].transform(count)pd.get_dummies的columns参数只对指定列做 one-hot不会把数值列一起展开。高基数列用groupby(col)[col].transform(count)得到每个取值在全体样本里出现的次数这个数字对树模型是有区分度的。注意训练和测试要各自算 count不要混在一起算否则测试集的信息会渗进训练特征这就是一种很隐蔽的数据泄露。提示one-hot 展开后训练集和测试集的列数很可能不一样。遇到这种情况用test test.reindex(columnstrain.columns, fill_value0)对齐能避免「测试集少一列」这种低级报错。3.3 时间型与文本型字段怎么处理网络赛的赛题数据里经常混着一两个看起来是字符串、实际是时间戳的列以及一两个自由文本列。时间戳列的常见处理是把「可读性」翻译成「规律性」拆出年、月、日、星期、小时必要时算它与数据里最早日期的间隔天数。文本列则要看长度如果平均每行只有几个词做成 TF-IDF 特征就能给模型增量如果是一大段话说明赛题本身偏 NLP后面按文本处理单独走。最怕的是把时间戳直接当字符串扔进类别编码白白丢掉时序信息。def parse_time_col(df, col): if col not in df.columns: return df s pd.to_datetime(df[col], errorscoerce) df[col _year] s.dt.year df[col _month] s.dt.month df[col _day] s.dt.day df[col _wday] s.dt.dayofweek df[col _hour] s.dt.hour return df.drop(columns[col]) train parse_time_col(train, create_time) test parse_time_col(test, create_time)pd.to_datetime加errorscoerce可以把解析失败的字符串置为 NaT避免整列报错dt.year、dt.month这些是 pandas 的 datetime 访问器。如果时间列里混着「2021-09-01 08:30」和「2021/09/01」两种格式不要自己写正则去切直接交给to_datetime兜底。最后drop掉原始列因为原始字符串对模型通常是噪音。3.4 训练集与验证集切分不要直接在原来的 train 上跑全量特征工程做完很多人直接拿整个训练集去训模型然后用测试集提交。这中间的评估是缺失的你不知道这个模型在没见过的样本上到底行不行。正确做法是先切出一部分做验证集并且切分方式要跟赛题的真实评分逻辑对齐。如果数据没有时间属性用带分层抽样的随机切分如果有时间戳按时间切分训练集用前 80% 的时间段验证集用后 20%。from sklearn.model_selection import train_test_split X train.drop(columns[label]) y train[label] 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)stratifyy保证训练子集和验证子集里的标签比例和全集一致这一步在类别不均衡时是必需的否则验证集可能一个少数类都没有。random_state42固定随机种子让每次重跑得到同样的切分不要留空不然你调特征时看到的分数变化可能只是换了个切分的运气。验证集切好后训练和验证两路走同一个预处理管道测试集则只在最后预测时过一遍。这里最容易翻车的是「预处理在切分之后做」先切分再填充、再编码两边的统计量会不一致特征分布全乱。正确顺序是先合并处理规则、再切分。4. 网络赛典型任务的模型选型与快速基线从 LR 到树模型再到深度模型特征处理完之后进模型就快多了。竞赛环境的评分只认提交结果不认模型复杂度所以选型的第一原则不是先进而是「在最短时间内跑出一个可信的分数作为参照」。我一般按三档递进先用线性模型或随机森林建基线再用 GBDT 系刷分最后根据数据类型决定要不要上深度学习。4.1 先跑一个「不调参也能交差」的基线网络赛通常给三到五个小时第一个小时内必须有一个能提交的结果。这时候最合适的是逻辑回归或者随机森林逻辑回归在标准化后的特征上跑得很快随机森林不用归一化也能用二者都能在十分钟内给出一个不算丢人的分数。这个基线分数不是用来吹的是用来垫底的——后面所有复杂方案都要拿它当标尺。from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, f1_score model_rf RandomForestClassifier( n_estimators200, max_depthNone, min_samples_leaf2, n_jobs-1, random_state42, ) model_rf.fit(X_train, y_train) y_pred model_rf.predict(X_val) print(验证集准确率: {:.4f}.format(accuracy_score(y_val, y_pred))) print(验证集 Macro-F1: {:.4f}.format(f1_score(y_val, y_pred, averagemacro)))n_estimators200是速度和精度的中间值太多收益递减还拖时间min_samples_leaf2让叶子节点至少有两个样本能压一压过拟合n_jobs-1用满所有核。准确率和 Macro-F1 都打印因为很多竞赛用的评分指标不是准确率而是更适合类别不均衡的 F1。把这两个数字记下来它就是整个项目的「地板分」。4.2 GBDT 系是这类竞赛的默认主力如果基线跑完还有时间下一步我会直接换 LightGBM。原因不是 XGBoost 不好而是同样的时间预算下LightGBM 能多试三组参数。这类赛题数据通常是大几百列几千行的规模LightGBM 训练快、内存省、对类别特征和缺失值有原生处理是限时赛里性价比最高的刷分工具。from lightgbm import LGBMClassifier, early_stopping, log_evaluation model_lgb LGBMClassifier( n_estimators300, learning_rate0.05, num_leaves31, max_depth-1, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42, verbose-1, ) model_lgb.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metriclogloss, callbacks[early_stopping(50), log_evaluation(50)], )learning_rate0.05配合n_estimators300靠早停决定真正需要的树数量比默认的 0.1 配 100 棵更稳。num_leaves31是 LightGBM 最常见的初始值超过 64 很容易在中小数据集上过拟合。subsample和colsample_bytree都是 0.8相当于给模型加了一点随机性。reg_alpha和reg_lambda是 L1 和 L2 正则初始给一个小值即可。这里有个容易踩的细节eval_metric要跟赛题评分指标尽量一致。如果评分是 F1就别只用 logloss 判断早停否则模型在验证集上可能停错位置。时间充裕的话把eval_metric换成赛题指标重跑一遍对比两次早停的验证分数。4.3 深度学习什么时候才值得上很多新手一看到「人工智能竞赛」就默认要上神经网络其实这是误解。赛题数据如果是结构化表格GBDT 通常是更好的选择深度模型在几千行表格数据上很难打过调好的树模型还容易在限时赛里因为训练和推理时间翻车。真正该上深度学习的场景只有两种字段里有大量文本且 TF-IDF 特征不够用或者数据本身就是图像、语音这类非结构化数据。文本场景的快速做法是分词后直接用 TF-IDF 加一个线性分类器先拿这个当文本基线如果分数不满意再考虑用预训练模型微调。预训练模型的关键参数是max_length和batch_size分别控制单条样本长度和每批样本数这两个值要配合显卡显存来调。图像场景则用现成的预训练 CNN 抽取特征后接分类头尽量不要从头训练。网络赛的时间约束决定了深度学习是「最后冲刺的手段」不是「第一步的选择」。4.4 预测结果的落盘与提交格式模型选完、验证集分数满意最后一步是把测试集的预测结果写出来。这一步的报错率远超想象最典型的问题是行数不对、索引错位、列名不符合模板要求。我的习惯是永远以提交模板为准先读模板看它要求几列、列名叫什么再按同样的格式输出。submit pd.read_csv(./data/sample_submit.csv) pred_test model_lgb.predict(test) out pd.DataFrame({ id: test[id], label: pred_test, }) out.to_csv(./submit_lgb.csv, indexFalse, encodingutf-8) print(提交文件形状:, out.shape, 模板形状:, submit.shape)不加indexFalse是很多人翻车的重灾区pandas 默认会把行号写成第一列评分脚本一读列数对不上直接报错。输出前打印shape和模板对比一次确认行数一致再交。如果模板里的 id 顺序和测试集文件里的顺序不一样务必用模板的 id 顺序重排不要用模型预测时的原始顺序直接交。5. 赛题数据的常见坑与现场排查现象、原因、解决的五个记录这一章写的是我在这类竞赛数据上真实踩过的坑。每个都按现象、原因、解决三步展开。如果你在复现时遇到类似报错直接对着查。5.1 中文列名和中文内容乱码现象read_csv读出来的列名是一串乱码或者数据内容里的中文全部变成问号提交时模型训练和预测表现正常但分数极低。原因文件是 Excel 导出或者 Windows 记事本另存的 CSV编码是 GBK或者带 BOM 的 UTF-8而pd.read_csv默认用 UTF-8 解码。解决优先用encodingutf-8-sig和encodinggbk依次尝试这俩覆盖了国内竞赛数据九成以上的编码情况。实在不行再用latin1兜底但latin1读进来的中文串在后续特征处理时很可能不是你想要的内容只能保证程序不崩。5.2 训练集和测试集的类别取值不一致现象验证集分数很高提交后线上分数大幅下降再检查发现某些类别特征在测试集里出现了训练集没有见过的取值。原因训练集和测试集是分别从整体数据里抽样出来的某个低频类别恰好只出现在测试集里。建模阶段如果在全量训练集上做LabelEncoder或在pd.get_dummies后直接训练模型没见过测试集的类别组合预测时就只能靠默认路径糊弄。解决所有类别编码器都只用训练集去fit测试集走同一个transform用pd.get_dummies时用columns限定要展开的列之后用reindex补齐训练集和测试集的列差。真实竞赛数据里这类「分布漂移」很常见提前在验证阶段就把它模拟出来比上线后补救值钱得多。5.3 本地分数高但线上分数低现象本地验证集 Macro-F1 到了 0.85提交线上只有 0.72来回调参都拉不回来。原因绝大多数是本地验证切分和线上评分逻辑不一致。如果赛题按时间顺序划分训练集和测试集本地却用了随机切分那模型在本地看到的验证样本分布基本和训练集同源线上测试则是另一个时间段的数据分布天然有偏移。解决先翻题目的说明文档确认评分集是按时间切还是随机切。按时间切就用TimeSeriesSplit或手动按时间字段前 80% 训练、后 20% 验证并把这种切分固化在代码里不要每次跑都重新随机。按随机切就用五折交叉验证的平均分替代单次验证分单次验证的运气成分太大。5.4 提交格式和评分脚本要求不一致现象提交后系统提示「文件行数不符」或「字段缺失」连分都没有。原因输出 CSV 时写了indexTrue或者测试集在特征工程里被dropna删了行导致输出行数和模板不一致还有可能是模板里 id 列名是ID你写的是id。解决在to_csv时固定写indexFalse特征工程阶段不要对整个 DataFrame 做无脑dropna。输出前先做一次形状和列名校验并且用模板的 id 顺序兜底重排永远不要迷信「我眼里看起来一样」。5.5 数据泄露看起来有用的列其实是「后悔药」现象某列特征和标签的关联高到离谱比如训练集里某个数值列只要大于某个阈值标签必为 1模型分数奇高提交后却惨不忍睹。原因赛题数据里可能包含类似身份 ID、报名序号这类列它们不携带语义但恰好和样本的划分顺序、标签生成顺序有相关性模型把「排序规律」当成了「业务规律」来学。解决体检阶段就先删掉 id、序号、行号这类纯标识列。如果非要用做一个严谨的验证把该列打乱后重新训练分数变化超过一定幅度就说明模型在靠它走捷径。走捷径的模型一上线就会在真实分布里现原形这是名副其实的「后悔药」吃下去一时爽事后全是债。6. 用这份赛题数据做沉淀一套可复用的验证套路与交付整理竞赛结束不等于数据作废。我会把整套处理流程按「数据体检 → 特征工程 → 基线模型 → 调参实验 → 提交记录」五个脚本归档每个脚本留下当时的运行结果日志。这样同一个赛题数据一周后重跑能在十分钟内恢复全部上下文下一届比赛换个数据包也能直接复用脚本框架。归档目录长这样project/ ├── 0_data_profile.py ├── 1_feature_engineering.py ├── 2_baseline.py ├── 3_best_model.py ├── 4_make_submit.py └── logs/ ├── data_profile.txt └── experiment_log.csv日志里只记三样东西数据集版本、验证策略、最终分数。调参是玄学但玄学也要有记录否则你永远不知道上一个高分是哪个参数组合跑出来的。我自己的习惯是每个实验一行列名和参数写全跑完模型就追加一行哪怕当时的分数再低也留着低分记录往往比高分更能说明问题。最后补一个被很多人忽略的验证细节提交一次之后把线上分数回填到本地实验日志里用线上分数校准本地的验证策略。如果线上总是比本地低两三个点说明本地验证偏乐观下次切分要把验证集的难度调高一点。这个「线上-本地分差」跟踪几次之后你的验证策略会越来越贴近真实赛场比我见过的大多数调参攻略都来得实在。我现在不管拿到的是哪年哪省的赛题数据第一件事永远是先写好0_data_profile.py让数据先开口说话再谈模型。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网