简介:面向备战2026年江西省研究生数学建模竞赛的团队,这套资源围绕题2电子健康记录数据补全与优化算法,提供从问题拆解到成果输出的完整闭环。论文部分内置特等奖标准模板,摘要、问题重述、模型假设、符号说明、模型建立与求解、灵敏度分析及结论等章节齐备,排版符合官方规范,稍作替换即可作为高质量答卷。代码部分提供Python和MATLAB双版本,模块化覆盖数据清洗、矩阵补全、模型训练与启发式算法寻优,关键代码逐行中文注释,配合一键运行脚本可复现全部图表与结果表。中间处理数据、模型输出参数与最终结论均已整理成高质量表格,涵盖多模型对比与性能评估指标,配以png图表和docx文档便于直接引入论文。资源共117个文件,以20个py源码、32个csv数据表、14个docx文档、28个png图为核心,整体约78.96MB;已有102人学习下载,对于追求效率与稳定产出的参赛队,不失为兼顾思路解析、代码实现和论文撰写的综合参考资源。
1. 2026江西研究生数学建模EHR数据补全:为什么这道题卡住了七成队伍
研究生数学建模里,数据补全类题目一直是"看着简单、拿高分很难"的典型。2026年江西省这道电子健康记录(EHR)题,核心诉求一句话:把医疗记录里缺失的字段补出来,同时保证统计指标和临床合理性都站得住。和华为杯研究生数学建模近两年的风格很像,考的是完整链路——缺失分析、插补建模、参数寻优、结果验证,缺一段都拿不到高分。七成队伍卡在同一个地方:没搞清缺失机制就开始补,均值填、中位数填,后面优化算法做得再花哨也救不回来。
这份资源包含完整代码、建模思路、助攻论文和结果数据,适合第一次打研究生建模、需要跑通全流程参考实现的队伍,也适合想在补全方法和智能优化算法上做出差异化的进阶队伍。这篇笔记从缺失机制讲到参数寻优,参数设置、评价口径和坑点全摊开,照着走一遍就知道每份文件该在哪个环节用上。
2. EHR缺失数据建模:先搞懂缺失机制,再谈补全算法
2.1 三种缺失机制与数学建模题的出题套路
EHR数据缺失不是随机的。医学记录里,血压缺失往往和患者病情轻重有关,化验指标缺失可能因为检测设备或科室没开单子,这直接决定了缺失属于MCAR(完全随机缺失)、MAR(随机缺失)还是MNAR(非随机缺失)。简单说:MCAR是缺失概率和任何变量都无关,像数据传输丢失;MAR是缺失概率和其他已观测变量相关,比如老年患者的某项检查更容易漏做;MNAR是缺失本身和缺失值的大小相关,比如血糖极高的患者反而没测血糖。
建模题里最常考的是MAR,因为MAR场景下用其他特征去预测缺失值是可行的,这正是所有插补算法的理论基础。如果是MNAR,单纯插补会有系统性偏差,因为缺失位置本身携带信息。拿到题目第一步不是写代码,而是先判断缺失机制。一个实用技巧:把样本按某个观测特征分组,比如按年龄段分组,看每组缺失率是否有显著差异。如果差异明显,基本可以判定为MAR,后面用多变量插补就理直气壮。
判断缺失机制还有更定量的做法。卡方检验可以验证某个字段的缺失与否是否和另一个字段的取值有关,显著性水平设在 0.05 以下就认为有关联。论文里不需要写一堆统计术语,但把判断过程和依据写清楚很重要。评委最反感的就是上来就贴代码、完全不解释为什么选这个方法。我的习惯是画一张缺失率热力图加一张分组缺失率对比表,两张图就能把"我理解数据"这件事说透。
2.2 缺失率统计与可视化:第一份必须交付的图表
下面这段代码是固定用的探索流程,产出缺失率表和缺失共现热力图。数据量上万条时跑起来也就几秒钟,但这两张图决定了后面所有方法选择的走向。
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns df = pd.read_csv('ehr_raw.csv', encoding='utf-8') # 缺失率统计:按列计算nan占比,降序排列 missing_rate = df.isnull().mean().sort_values(ascending=False) missing_df = pd.DataFrame({'字段': missing_rate.index, '缺失率': missing_rate.values}) print(missing_df) # 缺失共现矩阵:看哪些字段容易一起缺 miss_mask = df.isnull() co_occur = miss_mask.T @ miss_mask plt.figure(figsize=(10, 8)) sns.heatmap(co_occur, cmap='YlOrRd', cbar=True) plt.title('EHR字段缺失共现热力图') plt.tight_layout() plt.savefig('missing_co_occur.png', dpi=200)逻辑说明:isnull().mean()按列统计缺失比例,输出里缺失率超过 30% 的字段要重点标注,这类字段单靠KNN可能补不动,得走MICE或矩阵补全。miss_mask.T @ miss_mask是布尔矩阵转置相乘,得到的是两两字段同时缺失的样本数,热力图深色位置代表这两个字段的缺失高度相关。这一步能快速判断缺失是否集中在某几个科室或某类检查,直接指导后面选插补策略——缺失高度共现的字段对,必须放在同一个联合模型里补,不能拆开独立处理。
参数说明:热力图cmap='YlOrRd'用黄红渐变,深色代表共现严重;dpi=200保证论文插图清晰。共现阈值可以自己定,一般同时缺失超过样本量 20% 的字段对就要重点关注。这张图建议在论文正文放一张彩色版,附录再放全字段版本。探索结果顺手整理成一张表,四列分别是字段名、缺失率、缺失机制判断、初步处理策略。这张表放在论文第二节,能让评委在三十秒内建立起对你数据理解的信任。资源包里附带了一份已填好的示例表,字段名可以直接替换成自己赛题的列名。
3. 从统计插补到矩阵补全:一套能拿分的补全方法栈
3.1 基线方法:均值插补与KNN插补的代码实现
有了缺失画像,先跑基线。均值、中位数插补属于单变量方法,实现快但会压缩方差、破坏字段间相关关系,评委一眼就能看出只用了初级方法,最多用来做对比实验的最差基线。KNN插补按样本相似度取邻居加权平均,利用了字段间的相关性,是建模赛里性价比最高的起点。下面代码用KNNImputer实现,关键点是量纲处理。
from sklearn.impute import KNNImputer from sklearn.preprocessing import StandardScaler feat_cols = ['age', 'heart_rate', 'systolic_bp', 'glucose', 'cholesterol'] # 标准化:KNN的距离计算对量纲极敏感 scaler = StandardScaler() df_scaled = scaler.fit_transform(df[feat_cols]) # n_neighbors=5是经验起点,weights='distance'让近邻权重更大 imputer = KNNImputer(n_neighbors=5, weights='distance') df_imputed_scaled = imputer.fit_transform(df_scaled) # 逆变换回原始量纲,后续建模都在原始尺度上做 df_imputed = scaler.inverse_transform(df_imputed_scaled) df[feat_cols] = df_imputed逻辑说明:先StandardScaler是因为KNN本质是欧氏距离,收缩压 120 和血糖 5.6 量级差几十倍,不标准化则距离完全被大数值字段主导,小字段的邻居选择形同虚设。weights='distance'比默认的uniform更适合医学数据,相邻样本距离越近相关性越强,这在临床指标里体现得很明显。inverse_transform必须做,否则后续所有建模都在标准化空间里,预测结果没法落地解释。
参数说明:n_neighbors=5是小样本默认起点,样本量上万时可以试 10~20;weights两个选项都跑一遍,实际差异在 2%~5% 之间。这里有个容易被忽略的点:feat_cols里不要混入患者ID、就诊时间这类不参与补全的列,否则KNN会把样本ID差值也算进距离,邻居选择完全跑偏。缺失率超过 50% 的字段,KNN的参考价值已经很低,这类字段优先考虑从业务侧判断是否整列删除。
KNN插补相比均值插补的收益,最直观体现在相关性保持上。用一个简单检验:插补完成后计算字段间的皮尔逊相关系数矩阵,和完整样本的相关系数对比。均值插补会把相关系数向0压缩,KNN能保留大部分相关结构。这个检验在论文里可以作为选择KNN而不是均值插补的定量证据,一句话加一个数字就够。
3.2 MICE与矩阵补全:处理强相关医学指标的进阶方案
如果探索阶段发现字段缺失共现严重,比如甘油三酯和高密度脂蛋白经常一起缺,KNN也救不回来,因为近邻样本里同样的位置也是缺失的。这时候上MICE(多重链式插补)或矩阵补全。MICE的思路是每个缺失列当作目标变量,用其他列轮流建模预测,迭代多轮直到收敛。Scikit-learn 里封装了IterativeImputer,实现成本很低。
from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer # MICE:每列缺失值由其余列轮流补,迭代10轮 mice_imputer = IterativeImputer( max_iter=10, random_state=42, initial_strategy='median', imputation_order='descending' ) df_mice = mice_imputer.fit_transform(df[feat_cols])逻辑说明:imputation_order='descending'表示按缺失率从高到低的顺序逐列填补,先用缺失率低的列做预测源更可靠,这个顺序在EHR数据上比默认ascending效果好不少。initial_strategy='median'是迭代起点,只影响前几轮,最终会收敛到联合分布。max_iter=10对大部分医学数据足够,继续加大轮数收益递减但耗时线性增长,追求稳妥可以设 15。
矩阵补全则是把数据看作低秩矩阵,用奇异值分解或软阈值迭代恢复缺失位置。它在字段间存在复杂线性相关时表现好,但需要调秩参数,秩选大了过拟合噪声,选小了丢失结构。Scikit-learn 没有直接的矩阵补全接口,常见做法是手动做奇异值分解截断。下面这段是截断SVD补全的参考实现。
def svd_impute(X, rank=5, max_iter=50): """截断SVD软阈值补全:交替填充缺失值并做低秩近似""" X_filled = X.copy() missing_mask = np.isnan(X) # 先用列均值初始化缺失位置 col_means = np.nanmean(X, axis=0) for i in np.where(missing_mask.any(axis=1))[0]: X_filled[i, missing_mask[i]] = col_means[missing_mask[i]] for _ in range(max_iter): # 低秩近似:只保留前rank个奇异值 U, s, Vt = np.linalg.svd(X_filled, full_matrices=False) X_approx = U[:, :rank] @ np.diag(s[:rank]) @ Vt[:rank, :] # 只更新缺失位置,已知值不被破坏 X_filled[missing_mask] = X_approx[missing_mask] return X_filled逻辑说明:核心是交替两步,先做低秩近似再回填缺失位,重复迭代到稳定。np.linalg.svd每次迭代都重算,样本量上万时成本不小,建议用sklearn.utils.extmath.randomized_svd替代。rank的选择可以用奇异值谱判断——奇异值从第几个开始跌入平台,就取那个位置附近的秩,别拍脑袋定。
三层方法的定位要清楚:KNN是主战力,MICE是共现缺失时的升级选项,矩阵补全是展示方法视野的加分项。资源包里三套代码独立成文件,结果数据分文件存放,做对照实验非常方便。一个建议:三种方法的结果表和误差指标都保留,论文里放一张三行对比表,再加一段为什么最终选某一种的分析,这段分析往往是评阅时区分度最大的部分。
4. 优化算法调参:把补全误差压到最低的实战配置
4.1 为什么值得上智能优化算法:网格搜索的代价与边界
补全方法不是调好一个参数就完事。KNN的k、weights,MICE的max_iter、imputation_order,矩阵补全的rank,组合起来搜索空间相当大。网格搜索在这个空间里要么太粗漏掉好点,要么太细算到怀疑人生。智能优化算法在低维但非光滑的搜索空间里有天然优势,这也是近几年建模赛里"补全+寻优"组合拳流行起来的原因,和华为杯等赛事里智能优化算法的热度上升是一个趋势。
但要注意边界:补全模型每次评估都要跑一遍插补,单次评估可能几十秒,种群迭代30代、20个个体就是600次评估,算力不够会翻车。我的习惯是先缩小参数范围,只对影响最大的两三个参数做寻优,其他参数固定为经验值。怎么判断影响大小?跑一轮单变量敏感性分析,每个参数取几个候选值看误差波动幅度,波幅大的留下,波幅小的直接固定。
另一个常见误区是拿优化算法硬套所有环节。参数寻优解决的是"给定补全方法后找最优参数",它替代不了方法选择。先定方法栈,再寻优,顺序反了就是拿遗传算法去搜索一个根本不合适的方法空间,纯属浪费时间。这一条在答辩时也经常被评委追问,提前想清楚能少挨很多问。
提示:如果机器只有8G内存、跑一次KNN评估超过30秒,优先把种群降到12、迭代降到20,不要硬撑大种群。
4.2 种群迭代寻优:遗传算法与海星优化算法的落地配置
下面这段代码用遗传算法搜索KNN插补的k和weights两个核心参数,评估误差采用人工掩码法——随机屏蔽10%的已知值,在屏蔽位置上算误差。这是数据补全最通用的验证框架,比直接在缺失位置上评估客观得多。
import numpy as np from sklearn.impute import KNNImputer from sklearn.metrics import mean_absolute_error rng = np.random.default_rng(42) # 人为制造缺失:屏蔽10%已知值作为评估集 mask = rng.random(df_scaled.shape) < 0.1 df_masked = df_scaled.copy() df_masked[mask] = np.nan def evaluate_knn(params): k = int(3 + 17 * params[0]) # k ∈ [3, 20] weights = 'distance' if params[1] > 0.5 else 'uniform' imputer = KNNImputer(n_neighbors=k, weights=weights) imputed = imputer.fit_transform(df_masked) return mean_absolute_error(df_scaled[mask], imputed[mask]) def ga_search(pop_size=16, generations=25): pop = rng.random((pop_size, 2)) best = None for _ in range(generations): scores = np.array([evaluate_knn(ind) for ind in pop]) top_idx = np.argsort(scores)[:pop_size // 2] elites = pop[top_idx] if best is None or scores[top_idx[0]] < best[1]: best = (pop[top_idx[0]].copy(), scores[top_idx[0]]) # 精英保留 + 差分交叉生成下一代 new_pop = elites.copy() while len(new_pop) < pop_size: p1, p2 = elites[rng.choice(len(elites), 2, replace=False)] child = np.clip(p1 + 0.5 * (p2 - p1) + 0.08 * rng.standard_normal(2), 0, 1) new_pop = np.vstack([new_pop, child]) pop = new_pop return best best_param, best_score = ga_search() print(f"最优参数: k={int(3 + 17 * best_param[0])}, " f"weights={'distance' if best_param[1] > 0.5 else 'uniform'}") print(f"最小MAE: {best_score:.4f}")逻辑说明:evaluate_knn先屏蔽再补全、只在屏蔽位置算误差,这是这段代码的灵魂。如果在所有位置算误差,大量真实值会把误差稀释到看不出参数差异。遗传算法部分用精英保留策略,每代取前一半做父代,子代由两个父代差分交叉生成再加小扰动,收敛性和多样性兼顾。
参数说明:pop_size=16和generations=25是算力有限时的折中,总共400次评估,KNN每次秒级,十分钟内能跑完。时间充裕可以加到 30×50。变异步长0.08是调参的关键,太大搜索退化成长尾震荡,太小容易早熟卡在局部最优。掩码比例0.1在 10%~20% 之间取值都可以,比例越高评估越严格但计算量越大。
除了遗传算法,资源包还对比了海星优化算法等新兴元启发式算法的效果。海星优化算法属于比较新的种群算法,低维参数寻优上收敛速度快,迭代曲线前期下降明显,但稳定性比经典遗传算法差。同一份数据跑三次,结果可能差 2%~5%,这在竞赛提交时是致命伤。我的建议很直接:竞赛求稳,最终提交用遗传算法或粒子群,新算法写进论文做对比实验,既展示技术视野又不会在关键时刻翻车。
如果要用粒子群替代遗传算法,核心改动是把个体更新从交叉变异改成速度-位置更新,pbest记录个体历史最优、gbest记录全局最优,速度和位置按经典PSO公式迭代。资源包代码里两种算法都有,切换时只需要替换ga_search函数体,评估函数evaluate_knn完全复用。这也侧面说明评估框架比优化器本身更重要,框架写好了,换算法就是换一层皮的事。
5. 避坑指南:EHR数据补全最容易翻车的五个细节
5.1 缺失值直接填0:最省事也最致命
现象:很多队伍为了先把代码跑通,把缺失值直接fillna(0),结果RMSE看着还行,但补出来的收缩压出现了 0 mmHg 这种临床荒谬值,评阅阶段直接被扣分,甚至被怀疑数据造假。
原因:0 在医学数据里是有效量纲的极端值,不是缺失的合理替代。更重要的是,大量 0 会严重拉偏字段的均值和方差,后续所有基于统计量的特征全部失真。
解决:至少用中位数或KNN起步。任何补全结果都要做取值范围校验,超出临床合理区间的值必须标出并修正。建议在代码里定义一个字段上下限字典,补全后逐列clip并统计越界比例,这个数值要写进论文,是评阅时很加分的细节。
5.2 归一化顺序颠倒:验证集泄漏的隐形坑
现象:先对整个数据集做标准化再划分训练集和验证集,验证误差异常低,一换到测试集上表现就崩盘。
原因:标准化用了全样本的均值和方差,验证集和测试集的信息提前泄漏进了训练过程,这是典型的 data leakage。在EHR这种字段间相关性强的数据上,泄漏的后果比一般表格数据更严重。
解决:先划分再标准化,scaler只fit训练集,验证集和测试集只transform。代码里严格分开,不要图省事一锅端。资源包里的流水线脚本已经按这个顺序写好了,直接对照检查自己的代码。
5.3 分类变量粗暴编码:字符串转数字引发的补全偏差
现象:性别、科室等类别字段直接LabelEncoder编成 0/1/2,插补算法把它们当有序数值处理,距离计算完全失真。
原因:LabelEncoder给类别强加了人为顺序。内科编成 1、外科编成 2、儿科编成 3,KNN就会认为外科距离内科比距离儿科更近,这是没有任何业务依据的。
解决:用OneHotEncoder处理无序类别,数值型插补特征里只保留真正有序的变量。如果类别太多导致维度爆炸,先做频次编码把低频类别合并成"其他",再进OneHot,这在医疗数据里很常用。
5.4 优化算法早熟收敛:种群参数设置失误的典型表现
现象:遗传算法跑到第 5 代就停在同一个值不动了,换随机种子结果一样,明显陷入了局部最优。
原因:种群太小、变异步长太小,基因多样性在前几代就耗尽,后代全是父代的复制品,失去了跳出局部最优的能力。
解决:种群扩到 20 以上,变异步长调到 0.1 左右,或者每隔 5 代强制注入一批随机个体。观察迭代曲线也是好习惯——正常收敛是前期快速下降、后期平缓,如果曲线一开始就是平的,基本就是参数设错了。
5.5 评估指标只看RMSE:忽略业务合理性的代价
现象:RMSE 比所有对比方法都低,但画分布图发现补出来的血糖出现了负值,分布严重偏斜,一看就不合理。
原因:RMSE 对大误差敏感,但完全无法反映插补值是否落在业务合理范围内。医学数据尤其吃这个亏,一个极端值就能把RMSE拉出一个好看的数字,但分布形态是骗不了人的。
解决:RMSE、MAE、R² 三个指标一起看,同时画插补前后分布对比图,验证插补值的分位数是否平滑、有没有异常峰值。越界率这个指标建议单独统计,写进结果表,比任何文字都有说服力。
6. 结果验证与论文呈现:一份能打动评委的交付物长什么样
6.1 交叉验证与指标组合:RMSE之外还要看什么
补全模型评估最怕自欺欺人。我一般做两层验证:第一层是人工掩码,随机屏蔽 10%~20% 已知值,在屏蔽位置算误差,所有对比方法都在同一套掩码下评估,保证公平;第二层是业务校验,对每个字段定义临床合理区间,统计越界值比例。两层都过才算补全合格。
指标上 RMSE、MAE、R² 三件套缺一不可。RMSE 对异常值敏感,MAE 体现平均偏差,R² 说明方差解释能力,评委看到三个指标方向一致才会相信结果。只放一个RMSE的论文,等于告诉评委"我不敢给你看更多维度"。
6.2 论文图表与结果表:把补全效果"讲"出说服力
论文呈现上,我见过太多队伍代码很漂亮、图表一团糟。补全类题目最核心的三张图:缺失热力图、插补前后字段分布对比图、各方法误差对比柱状图。分布对比图尤其关键——插补后的分布应该和真实分布形态接近,峰度、偏度都要对得上。两张分布曲线叠在一张图里,一眼就能看出均值插补和MICE的差别。
结果表建议把基线KNN、MICE、矩阵补全、优化后模型四行放一张表里,字段包括MAE、RMSE、R²、越界率四个指标。越界率这一列是很多队伍漏掉的加分项,也是最容易让评委觉得"这队伍真的懂业务"的细节。资源包里的结果数据文件可以直接生成这三张图和这张表,拿到手改改标题就能用。
做这套资源复盘时我最大的教训是:补全不是埋头调代码,而是先花一天把缺失机制和评估框架定死,后面所有实验才不会白做。从那以后我每次拿到数据补全题,都强制自己先写缺失率表、再跑基线、再谈优化,这个顺序救了我至少两次翻车。这套资源和笔记如果能帮你在2026年的赛场上少走一段弯路,那它就值了,希望帮到你。
本文还有配套的精品资源,点击获取