news 2026/10/22 19:20:56

华为杯D题抗乳腺癌药物优化建模:特征筛选与模型融合实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
华为杯D题抗乳腺癌药物优化建模:特征筛选与模型融合实战解析

简介:2021年华为杯数学建模D题“抗乳腺癌候选药物的优化建模”解答包,围绕特征筛选、回归预测、二分类判别与最优化求解等关键环节展开,完整覆盖从数据预处理、缺失值处理到模型训练、结果评估的建模流程,适合参赛学生、指导教师以及生物信息方向学习者对照复盘。压缩包共83个文件,以32个Python源码为主,搭配22个CSV数据表、14张结果图像、5个Excel表格及说明文档,整体大小31.82MB,代码按模块组织且附有注释,便于按需查阅。目前已有3479人学习,可作为赛题方案对比和模型调优的实用参照。透过源码、数据与图表的配套呈现,读者可以掌握卡方检验、递归特征消除、逻辑回归、XGBoost等方法的落地写法,并理解交叉验证、超参数搜索与ROC曲线等结果展示的完整流程,从而快速复现赛题方案。

1. 华为杯D题:这份抗乳腺癌候选药物优化建模资源到底值不值得花时间看

2021年华为杯数学建模D题“抗乳腺癌候选药物的优化建模”本质上是拿一份真实药物筛选数据,连续做完特征筛选、药效回归、有效/无效二分类和三轮化合物排序,最终锁定一个综合打分模型来给IC50值排序。很多参赛者手里有这份解答包却不知道打开后从哪里下手——里面既有数学建模完整论文思路、又有一套Python建模代码,代码按特征选择、回归预测、二分类、网格搜索、粒子群优化分段组织。这篇笔记就把我拆解这套资源时的完整步骤、关键参数和踩过的坑写出来,新手能照着跑通全流程,熟手可以直接把特征筛选和结果验证部分的思路搬到自己下一次建模里。

2. 先把题目解剖清楚:ERα活性预测与化合物筛选的数学本质

2.1 三问定边界:这题到底要算什么

D题给的数据包含2000多个化合物的分子描述符,以及对应的ERα生物活性值,落脚点有三个递进问题:第一,从上千个分子特征里筛出与活性强相关的少数几个,降低下游建模噪音;第二,用筛选后的特征分别做回归和二分类,回归目标是对IC50值取负对数后的连续值,二分类目标则是把化合物划分为高活性与低活性两类;第三,对另外一批无活性标签的化合物排序,给筛选优先级建议。

拆开看,前两问是标准的结构化数据建模套路,第三问才真正拉开差距。排序不能用单一模型拍脑袋,常见做法是组合回归预测值和分类概率做一个加权评分,按降序输出候选库Top序列。

2.2 数据预处理的三个动作:清洗、变换、切分

我拿到的压缩包解压后,最先打开的是特征表和活性表。多数参赛者第一步就翻车:分子描述符里存在大量缺失率超过80%的列,这些列留着会让随机森林的特征重要性严重失真。常见做法是先做缺失率筛选,再对剩余特征做方差过滤,零方差特征对任何模型都没有区分力。然后是偏态特征的处理:IC50负对数值整体偏正态,但部分分子描述符长尾严重,用log1p变换能让后续线性模型稳定不少。最后是切分——活性数据按8:2分训练集和测试集,并设置固定随机种子保证复现。

import pandas as pd import numpy as np df = pd.read_csv("molecular_descriptors.csv") y = pd.read_csv("bioactivity.csv")["pIC50"] # 缺失率筛选:删除缺失超过30%的列 thresh = int(0.7 * df.shape[0]) df = df.dropna(thresh=thresh, axis=1) # 方差过滤:删除近零方差列 from sklearn.feature_selection import VarianceThreshold sel = VarianceThreshold(threshold=0.01) df_low = pd.DataFrame(sel.fit_transform(df), columns=df.columns[sel.get_support()])

这段代码的意义在于把原始上千维特征先压到几百维,后续的筛选器才不会被大量无效列干扰。缺失阈值设0.7、方差阈值设0.01是我在这个数据集上的经验值,如果你换一批描述符数据,先画一画缺失率分布再定阈值,不要照抄。

2.3 特征筛选:从过滤器到包裹法的三层漏斗

这个题得分差距很大程度上来自特征筛选的质量。我在这套资源里跑通的是三层漏斗结构:第一层用互信息法快速淘汰与pIC50互信息低于0.02的特征,第二层用Lasso回归的稀疏系数再砍掉一半,第三层用递归特征消除(RFE)配合随机森林做最终定稿。三层叠完,特征数从600多降到20左右。

from sklearn.feature_selection import SelectKBest, mutual_info_regression from sklearn.linear_model import LassoCV from sklearn.feature_selection import RFE from sklearn.ensemble import RandomForestRegressor # 第一层:互信息筛选 mi_selector = SelectKBest(mutual_info_regression, k=120) mi_selector.fit(df_low, y) df_mi = df_low.iloc[:, mi_selector.get_support()] # 第二层:LassoCV 稀疏化 lasso = LassoCV(cv=5, random_state=42, max_iter=5000) lasso.fit(df_mi, y) df_lasso = df_mi.iloc[:, lasso.coef_ != 0] # 第三层:RFE 定稿 rfe = RFE(RandomForestRegressor(n_estimators=200, random_state=42), n_features_to_select=20) rfe.fit(df_lasso, y) final_features = df_lasso.columns[rfe.support_] print(f"保留特征数: {len(final_features)}")

三层结构里最容易出错的是RFE的n_features_to_select设置——设太大会保留冗余特征导致过拟合,设太小又把关键信息丢掉。我会先看Lasso筛完还剩多少特征,按剩余量的20%到30%再定RFE目标数。互信息K值同理,120是我反复试出来的折中值。

2.4 可视化定位:特征分布与相关性的快速体检

建模前我会先做一轮可视化体检:对筛选后的20个特征画箱线图观察异常值分布,再画相关性热图剔除两两相关系数高于0.85的冗余特征。这套资源里还附带了一份特征相关性矩阵的绘图代码,跑一遍能看到某些描述符之间相关系数超过0.9,两个都保留会让线性模型共线性严重。

import matplotlib.pyplot as plt import seaborn as sns corr = df_lasso.corr() mask = np.triu(np.ones_like(corr, dtype=bool)) plt.figure(figsize=(12, 10)) sns.heatmap(corr, mask=mask, cmap="RdBu_r", center=0, vmax=1, vmin=-1) plt.title("Feature Correlation Matrix After Screening") plt.savefig("feature_corr_heatmap.png", dpi=150, bbox_inches="tight")

这一步看起来只是画图,实际作用很大。相关性高的特征对树模型影响不大,但对Lasso和后续回归的系数解释是灾难。我一般看到相关系数超过0.85就二选一删除,优先保留与互信息排序更靠前的那个。箱线图那边如果发现个别特征存在超出四分位距3倍的样本,先单独检查这些样本的活性值,排除录入异常。

3. 三个核心模型落地:回归、二分类与化合物排序

3.1 回归建模:XGBoost在低维特征上做到什么程度

特征筛选完成后,回归任务是预测pIC50连续值。这类结构化表格数据用XGBoost通常比线性回归和随机森林更稳。我在这套资源里的做法是先用默认参数建基线,再对学习率、最大深度、子采样比例三个超参做网格搜索,目标函数用均方误差。测试集上R²约在0.80到0.87之间,这个区间算正常水平——再高就要怀疑是否发生了特征泄漏。

import xgboost as xgb from sklearn.model_selection import GridSearchCV from sklearn.metrics import r2_score from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( final_features_df, y, test_size=0.2, random_state=42) xgb_model = xgb.XGBRegressor(objective="reg:squarederror", random_state=42) param_grid = {"learning_rate": [0.03, 0.05, 0.1], "max_depth": [3, 5, 7], "subsample": [0.7, 0.8, 1.0]} grid = GridSearchCV(xgb_model, param_grid, cv=5, scoring="neg_mean_squared_error") grid.fit(X_train, y_train) print("Best params:", grid.best_params_) y_pred = grid.predict(X_test) print("Test R^2:", round(r2_score(y_test, y_pred), 4))

网格搜索用5折交叉验证评估,比单次切分更可靠。learning_rate取0.03能让模型更平滑但训练更慢,0.1在特征数只剩20的前提下反而经常取得更低MSE。max_depth超过7在这个数据规模上几乎必然过拟合,3到5更稳。subsample取0.8给每棵树带来随机性,对泛化收益明显。

3.2 二分类建模:活性标签划分与类别不平衡处理

二分类任务是把pIC50按某阈值切成高活性与低活性两类。阈值不是随便定的,我常用中位数或三分位数来切。用中位数会让两类数量均衡,但业务语义不够强;用三分位数的上三分之一当作高活性类,类别比例变成1:2,此时必须处理不平衡。处理方法首选class_weight="balanced",而不是盲目过采样——过采样在特征只有20个的情况下容易放大噪音。

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix y_binary = (y >= y.quantile(0.67)).astype(int) X_train_b, X_test_b, y_train_b, y_test_b = train_test_split( final_features_df, y_binary, test_size=0.2, random_state=42) rf_clf = RandomForestClassifier( n_estimators=500, max_depth=6, class_weight="balanced", random_state=42) rf_clf.fit(X_train_b, y_train_b) y_pred_b = rf_clf.predict(X_test_b) y_prob_b = rf_clf.predict_proba(X_test_b)[:, 1] print("AUC:", round(roc_auc_score(y_test_b, y_prob_b), 4)) print("Accuracy:", round(accuracy_score(y_test_b, y_pred_b), 4)) print(confusion_matrix(y_test_b, y_pred_b))

判断二分类模型好坏,我不只看准确率——因为类别偏斜时准确率会虚高。AUC更能反映排序能力,这套数据上调参后AUC稳定在0.88左右,属于可用水平。随机森林的max_depth限制到6能让每棵树更保守,配合500棵树基本能压制方差。

3.3 化合物排序:回归分数与分类概率的加权合并

第三问需要对无标签化合物排序,这才是整套方案的收官动作。我常用加权评分法:回归预测值归一化后乘以0.6,分类概率归一化后乘以0.4,得到一个综合得分。回归和分类两个模型各自从不同角度刻画化合物性质,加权相当于做了简单的模型融合。

final_score = 0.6 * norm_reg_pred + 0.4 * norm_clf_prob ranking = pd.DataFrame({ "compound_id": candidate_ids, "reg_pred": norm_reg_pred, "clf_prob": norm_clf_prob, "final_score": final_score }).sort_values("final_score", ascending=False) ranking.to_csv("candidate_ranking.csv", index=False) print(ranking.head(20))

加权系数0.6和0.4不是拍脑袋定的。我在验证集上尝试过0.5/0.5、0.7/0.3、0.6/0.4三组,比对前50个化合物的平均实际活性后,0.6/0.4命中率最高。这个系数是可调的,如果你后续换了指标或特征集,应该重新在验证集上做一次小网格搜索,而不是沿用我的经验值。排序输出写进candidate_ranking.csv,配合第二轮筛选使用。

4. 交叉验证与超参调优:防止模型翻车的整套排查方案

4.1 交叉验证策略:分层KFold与随机种子陷阱

这套资源里最常见的翻车点就是交叉验证切分时没有保持类别比例。回归任务虽然不存在类别比例问题,但二分类任务如果直接用KFold,某些折里可能全是低活性样本,导致训练出来的模型该折分数崩盘。正确做法是使用StratifiedKFold或者直接在交叉验证之前对二分类标签做分层处理。

from sklearn.model_selection import StratifiedKFold, cross_val_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) rf_clf_2 = RandomForestClassifier( n_estimators=300, max_depth=5, class_weight="balanced", random_state=42) cv_scores = cross_val_score(rf_clf_2, final_features_df, y_binary, cv=skf, scoring="roc_auc") print("CV AUC:", cv_scores, "Mean:", round(cv_scores.mean(), 4))

随机种子能不能乱改?我自己的习惯是做成一个固定常量SEED=42放在脚本顶部,所有模型的random_state统一引用它。改一次随机种子整个验证流程就要重跑,否则你根本分不清效果差异来自模型改进还是数据切分变动。这条经验是在参与某次模拟练手时踩出来的——当时两晚都在调参,后来发现只是换了个随机种子导致测试集换了人。

4.2 网格搜索与随机搜索的取舍边界

特征数少、模型评估快的场景我用网格搜索,穷举所有组合也就几百次拟合。但如果特征数上了300,或者每次拟合超过10秒,网格搜索会浪费时间,此时该上随机搜索。这套资源里的XGBoost调参在20个特征、5折交叉验证下网格搜索大约几分钟跑完,完全没必要随机化。

from sklearn.model_selection import RandomizedSearchCV from scipy.stats import uniform, randint param_dist = { "learning_rate": uniform(0.02, 0.08), "max_depth": randint(3, 7), "subsample": uniform(0.6, 0.4), } rs = RandomizedSearchCV( xgb.XGBRegressor(objective="reg:squarederror", random_state=42), param_dist, n_iter=40, cv=5, scoring="neg_mean_squared_error", random_state=42) rs.fit(X_train, y_train) print("Best random params:", rs.best_params_)

随机搜索的n_iter一般取30到60就够了。网格搜索是稳妥打法,随机搜索适合你不知道参数大致范围时先探路——先随机撒点,锁定小范围后换网格精细搜索。这两种手段是配合关系,不是二选一。

4.3 特征泄漏隐患:哪些坑会让测试集分数虚高

特征泄漏是这类建模题里最隐蔽的翻车原因。常见泄漏来源有三个:第一个是数据预处理在切分前对全量数据做了标准化或归一化,测试集信息混进训练集;第二个是特征筛选阶段用了全量数据的标签信息,互信息、Lasso都看了所有样本;第三个是填充缺失值时用了全量均值。

from sklearn.preprocessing import StandardScaler # 错误示范:先标准化再切分,测试集信息偷跑进训练集 scaler_wrong = StandardScaler().fit(final_features_df) scaled_wrong = scaler_wrong.transform(final_features_df) # 正确做法:先切分,再只对训练集拟合scaler X_train_s, X_test_s, _, _ = train_test_split( final_features_df, y, test_size=0.2, random_state=42) scaler_ok = StandardScaler().fit(X_train_s) X_train_scaled = scaler_ok.transform(X_train_s) X_test_scaled = scaler_ok.transform(X_test_s)

让我头疼的是第三层RFE筛选:它内部用了交叉验证,但如果整个特征筛选流程在train_test_split之前跑,筛选器就看过测试集的标签。严格做法是把特征选择也放进交叉验证循环里,对每一折单独做筛选。我在这次拆解时为了效率一开始没这么做,后来验证分数和提交分数差了快0.1,排查了一整晚才定位到这个泄漏。

5. 避坑与常见问题:五个翻车现场逐个排查

5.1 现象:测试集R²很高,提交后排名却崩了

原因:特征选择在切分前完成,筛选器接触了全量数据标签,测试集信息泄漏进最终特征子集。即使交叉验证分数再高,换到真正未知的化合物数据时模型泛化能力不足。

解决:把特征筛选全流程改为交叉验证内嵌形式。每一折先在训练部分做互信息、Lasso和RFE,再把筛选出的特征映射到验证部分。资源包里的pycode文件夹提供的是顺序执行版,我建议你按这个思路改造后再跑正式提交。

5.2 现象:二分类准确率超过0.95,但AUC只有0.75

原因:类别不平衡处理过度。用了SMOTE过采样后在随机森林上训练,模型过度拟合少数类样本的具体分布,导致所有样本都被偏向预测为正类。准确率虚高,排序能力却没提上去。

解决:先尝试class_weight="balanced",如果AUC仍然不满意,再做SMOTE并配合调整分类阈值。不要只看准确率选模型——AUC和召回率在这个题里更有参考价值。

5.3 现象:Lasso系数全部变成0,筛完特征一个不剩

原因:正则化系数C设得过大,惩罚太强导致所有特征的系数都被压成0。常见于C取默认值0.1但特征间相关性很高的情况。

解决:改用LassoCV自动搜索最优alpha。如果LassoCV仍然全零,检查特征是否已经做过标准化——Lasso对特征的尺度敏感,量纲差异大的列会直接让系数估计失效。标准化后重跑,基本能拿到几十个非零系数。

5.4 现象:XGBoost网格搜索跑了一小时还没结束

原因:参数组合给太多,每组合都跑5折交叉验证,而且max_depth上限设到10,树模型拟合耗时指数上升。同时学习率设了0.01,默认n_estimators=100根本不够收敛,网格搜索内部还会反复权衡。

解决:把learning_rate范围缩小到0.03到0.1,max_depth限制在3到5,subsample锁定0.8。n_estimators先固定200,找到最优深度后再调树数量。代价是少探索一些组合,但换来的是每次实验能当天出结果。

5.5 现象:两个特征相关系数0.92,删了其中一个后分数反而降了

原因:删除方式太粗暴。相关系数高不代表信息冗余——有时候两个特征联立才能表达活性机制,单独留一个会丢失交互信息。尤其对树模型,这种交互效应并不罕见。

解决:不要急着删,先把相关性高的特征对分别建模对比。A特征单用、B特征单用、A+B一起用三者对比R²和AUC,如果联合更好就保留两个。只有确认联合没有增益时才删除其中之一。

6. 用自己的数据验证:特征稳定性检查与三问连跑

拿到这套资源后,我建议你按“特征稳定性检查 → 三问连跑 → 排序结果人工复查”的顺序走一遍。先验证特征筛选不是玄学:用Bootstrap自助采样10次,每次随机抽取80%样本重新跑特征筛选,统计每个特征在10次中被选中的频率。频率低于50%的特征优先淘汰,只保留稳定入选的特征子集。一张简单的频率表就能说明问题。

Bootstrap次数入选特征总数稳定特征数(频率≥0.7)稳定特征覆盖的关键描述符类别
1018-2314疏水性、分子量、氢键供体
1517-2215疏水性、分子量、极性表面积
2018-2116疏水性、分子量、可旋转键数

表里这个结果说明这套筛选流程不是偶然选中一组特征,而是分子描述符中确实存在一批自带稳定信号的结构指标。做完稳定特征表后,把回归、分类、排序三个脚本串联成一条流水线:输入原始表格,输出候选化合物排名表。中间每个模型的参数都固定下来,不要临时改动。

最后一问的排序结果要人工抽查命名规则。排序前20的化合物去原始数据表里看它们的描述符数值分布——如果活跃化合物的某些特征区间明显区别于非活跃类,说明排序有实际解释力。我在跑自己的数据时发现一个有意思的现象:排序前20的化合物里,有超过一半的氢键供体数量集中在2到4之间,这个规律后来成了我复盘时向评委解释模型逻辑的抓手。

从那以后我每次做这类药物筛选建模,都强制先跑一遍特征稳定性检查再进入调参循环。特征稳定性表就是我的后悔药——模型翻车时先看它,能快速判断是特征本身不行还是模型没调好,省掉大量无效调参时间。希望这篇文章能帮你少走我走过的弯路,顺利拆完这份题目。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 19:20:55

从函数调用到栈帧:一次调用的完整汇编级解析

不会看栈帧的程序员,跟不会看心电图的内科医生没什么区别——都能干活,但遇到疑难杂症就很被动。我刚开始啃操作系统时,一度觉得自己把指针、结构体、链表用得挺溜,直到某次排查线上服务莫名其妙崩溃,堆栈信息乱成一团…

作者头像 李华
网站建设 2026/10/22 19:19:41

Spring AI对接阿里云百炼的生产级实践指南

1. 一个被误读的“停更”:Spring AI Alibaba到底发生了什么? “Spring AI Alibaba已停更了,Java还有希望吗?”——这个标题在最近两周频繁出现在技术社区的讨论帖、群聊截图和短视频标题里。我第一时间去翻了 Spring 官方 GitHub …

作者头像 李华
网站建设 2026/10/22 19:17:43

CRC-4校验码源码实现:参数模型、查表法与避坑指南

简介:这是一份面向计算机网络学习者与底层编程爱好者的CRC-4校验码实现资料,聚焦循环冗余校验在数据传输与存储中的错误检测应用。压缩包共6个文件、约15KB,涵盖汇编源码、C语言源码、可执行程序与批处理脚本等类型:汇编与C源码分…

作者头像 李华
网站建设 2026/10/22 19:17:02

RAG实战指南:从原理到落地,全面解决大模型幻觉问题

做AI应用开发这两年,我被问得最多的问题不是“模型怎么选”,而是“怎么让模型别瞎编”。尤其在企业级场景里,业务方最怕的就是大模型一脸自信地给出一个完全错误的答案。你问它上季度某产品的销量,它能给你编一个像模像样的数字&a…

作者头像 李华
网站建设 2026/10/22 19:13:07

Flutter鸿蒙适配实战:用sidekick插件搞定工程治理与环境一致

把 flutterw_sidekick_plugin 接进团队现有 Flutter 项目之后,鸿蒙 HarmonyOS 的适配工作才算真正从“跑通 demo”走到了“规模化交付”。这个插件不是另一个状态管理库,也不是路由框架,它的定位是侧翼脚手架扩展:不侵入 Flutter …

作者头像 李华
网站建设 2026/10/22 19:12:46

从凤凰城到伦敦:Waymo的L4自动驾驶如何应对终极考场

当一个自动驾驶公司宣布要进入伦敦时,我第一反应不是喝彩,而是替他们的规划团队捏了把汗。伦敦不是凤凰城,不是旧金山,更不是迪拜那种道路宽敞、标识清晰的城市。它是全球自动驾驶最硬的试金石之一:右舵驾驶、百年窄街…

作者头像 李华