简介:这份资源面向医疗健康数据分析初学者与竞赛参与者,提供一套基于天池大数据竞赛的糖尿病遗传风险预测完整方案,用于早期筛查与风险评估的建模练习。项目以Keras框架搭建神经网络,并引入BoxCox变换改善数据正态性,配合数据可视化分析辅助理解特征分布与模型表现。压缩包共159个文件,约21.09MB,包含34个Python脚本、84张可视化图表、15份PDF文档、8个CSV数据集及ipynb笔记本、模型文件等,覆盖数据处理、训练与结果展示全流程。已有44人学习下载。读者可获取赛题数据预处理脚本、神经网络实现代码、训练好的模型以及数据处理过程说明文档,便于复现实验、对比不同预处理策略,并借助图表快速定位关键特征,适合作为医疗AI入门与竞赛复盘的实践参考。
1. 糖尿病遗传风险预测:从竞赛数据到可落地的筛查工具
糖尿病早期筛查的痛点在于,空腹血糖和糖化血红蛋白往往在胰岛功能已经损失过半时才亮红灯。遗传风险预测的价值,是在生化指标异常之前,用基因位点、家族史和基础体征数据,把高风险人群先筛出来。天池大数据竞赛里这类赛题通常给出结构化的问卷与体检字段,要求参赛者预测个体患病概率。这个方向适合两类人:一类是想用 Keras 跑通一个完整二分类项目、但缺少真实业务数据的算法工程师;另一类是做健康管理或体检系统、需要给用户一个风险分层依据的产品技术负责人。整条链路的核心不是模型多深,而是 BoxCox 变换把偏态特征拉回近似正态、前馈神经网络把非线性交互吃进去、再用可视化把概率翻译成可解释的风险等级。下面按数据、预处理、建模、评估、避坑、进阶六段拆开讲,每一步都能直接抄。
2. 数据到手先别急着喂网络:字段清洗与 BoxCox 变换的实操顺序
2.1 先搞清楚竞赛数据长什么样
天池这类糖尿病预测赛题的数据通常是 CSV,一行一个样本,字段分三类:人口学特征(年龄、性别、BMI)、行为与病史特征(家族史、高血压、吸烟、体力活动)、生化指标(空腹血糖、血脂、胰岛素等)。标签列一般是label或Outcome,取值 0/1。拿到数据第一件事不是model.fit,而是用 pandas 把字段类型、缺失率、取值分布过一遍。
import pandas as pd import numpy as np df = pd.read_csv("diabetes_train.csv") print(df.shape) print(df.dtypes.value_counts()) print(df.isnull().mean().sort_values(ascending=False).head(10)) print(df["label"].value_counts(normalize=True))这段代码做四件事:看样本量和特征数、区分数值列与类别列、按缺失率排序找出需要处理的列、确认正负样本比例。如果正样本占比低于 15%,后面评估就不能只看准确率,必须看 AUC 和召回。缺失率超过 40% 的列,我一般直接删,因为插补引入的噪声比信息还大;缺失率在 5% 到 40% 之间的数值列用中位数填充,类别列用众数填充。
2.2 BoxCox 变换到底解决什么问题
很多生化指标是右偏的,比如胰岛素、甘油三酯,少数人数值极高,直接把均值拉飞。前馈神经网络对输入尺度敏感,偏态分布会让梯度更新偏向极端样本。BoxCox 是一族幂变换,公式是:当 λ≠0 时y = (x^λ - 1) / λ,当 λ=0 时y = log(x)。它通过极大似然估计自动选一个 λ,让变换后的数据尽量接近正态分布。
注意:BoxCox 要求输入严格为正。如果字段里有 0 或负值,先做平移,比如x + 1,或者改用 Yeo-Johnson 变换。我一般只对连续数值列做 BoxCox,类别列和已经是 0/1 的字段不动。
from scipy.stats import boxcox from scipy.special import inv_boxcox continuous_cols = ["BMI", "Glucose", "Insulin", "Triglycerides", "Age"] lambda_dict = {} for col in continuous_cols: x = df[col].values.astype(float) x_shift = x - x.min() + 1.0 # 保证严格为正 x_trans, lam = boxcox(x_shift) df[col + "_boxcox"] = x_trans lambda_dict[col] = (lam, x.min()) print(f"{col}: lambda={lam:.4f}")逻辑说明:x_shift把最小值平移到 1,保证 BoxCox 合法;boxcox返回变换后序列和最优 λ;把 λ 和原始最小值存下来,推理时对新样本做同样变换。参数说明:continuous_cols要按实际字段名改,只放连续变量;λ 接近 0 说明该列接近对数正态,λ 接近 1 说明变换几乎没起作用,可以考虑跳过。
2.3 标准化与训练集/验证集切分的先后关系
BoxCox 之后还要做 z-score 标准化,把均值拉到 0、方差拉到 1。关键顺序是:先切分训练集和验证集,再在训练集上拟合 BoxCox 的 λ 和标准化的均值和方差,然后应用到验证集。如果先对全量数据做变换再切分,验证集的信息就泄漏进训练过程,AUC 会虚高。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler feature_cols = [c for c in df.columns if c.endswith("_boxcox")] + ["Hypertension", "FamilyHistory", "Smoking"] X = df[feature_cols].values y = df["label"].values X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_val = scaler.transform(X_val)stratify=y保证训练集和验证集的正负比例一致,小样本赛题里这一步能避免验证集正样本太少导致指标抖动。random_state=42固定切分,方便复现。标准化器只在训练集上fit,验证集只transform,这是铁律。
3. 用 Keras 搭前馈神经网络:层数、激活、Dropout 与早停的取舍
3.1 网络结构怎么定:从 3 层起步
糖尿病风险预测的输入维度通常几十到一两百,样本量几千到几万,这种规模用 3 到 5 层全连接网络足够。层数再多,参数量上去,小数据集上直接过拟合。我一般第一层 64 个神经元,第二层 32,输出层 1 个神经元配 sigmoid。激活函数隐藏层用 ReLU,输出层用 sigmoid 做二分类概率。
import tensorflow as tf from tensorflow.keras import layers, models, callbacks def build_model(input_dim): model = models.Sequential([ layers.Input(shape=(input_dim,)), layers.Dense(64, activation="relu"), layers.BatchNormalization(), layers.Dropout(0.3), layers.Dense(32, activation="relu"), layers.BatchNormalization(), layers.Dropout(0.2), layers.Dense(1, activation="sigmoid") ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss="binary_crossentropy", metrics=["accuracy", tf.keras.metrics.AUC(name="auc")] ) return model model = build_model(X_train.shape[1]) model.summary()逻辑说明:BatchNormalization放在 Dense 之后、激活之前还是之后有争议,Keras 里 Dense 的activation参数会先算激活,所以这里 BatchNorm 实际作用在激活后的输出上,实践中效果稳定。Dropout(0.3)和Dropout(0.2)是经验值,样本越少 dropout 越大。Adam(1e-3)是默认起点,如果 loss 震荡就降到 5e-4。AUC作为监控指标比 accuracy 更适合不平衡数据。
3.2 训练时的三个必调参数
第一个是batch_size,小数据集用 32 或 64,太大梯度更新次数少,收敛慢。第二个是epochs,配合早停用,设 200 让早停自己决定什么时候停。第三个是class_weight,如果正样本少,给正类更高权重,提升召回。
from sklearn.utils.class_weight import compute_class_weight classes = np.unique(y_train) weights = compute_class_weight("balanced", classes=classes, y=y_train) class_weight = dict(zip(classes, weights)) print(class_weight) early_stop = callbacks.EarlyStopping( monitor="val_auc", patience=15, mode="max", restore_best_weights=True ) reduce_lr = callbacks.ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=8, min_lr=1e-6 ) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=200, batch_size=64, class_weight=class_weight, callbacks=[early_stop, reduce_lr], verbose=1 )compute_class_weight("balanced")按样本比例反比给权重,正样本占 10% 时正类权重约 9 倍。EarlyStopping监控val_auc,patience=15表示连续 15 轮没提升就停,restore_best_weights=True把权重回滚到最优轮,省得手动存模型。ReduceLROnPlateau在验证 loss 停滞时把学习率减半,帮助跳出局部平台。这两个回调一起用,基本不用手动调 epoch。
3.3 训练曲线怎么看:过拟合和欠拟合的区分
history.history里有loss、val_loss、auc、val_auc。如果训练 loss 持续下降、验证 loss 先降后升,是过拟合,加 dropout 或减层数。如果两条 loss 都居高不下,是欠拟合,加层数或加神经元。如果验证 AUC 波动大,多半是验证集太小,考虑交叉验证。
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(history.history["loss"], label="train_loss") axes[0].plot(history.history["val_loss"], label="val_loss") axes[0].set_title("Loss Curve") axes[0].legend() axes[1].plot(history.history["auc"], label="train_auc") axes[1].plot(history.history["val_auc"], label="val_auc") axes[1].set_title("AUC Curve") axes[1].legend() plt.tight_layout() plt.savefig("training_curve.png", dpi=150)这张图是排查训练问题的黑匣子。我习惯每次训练完先看这两条曲线,再决定下一步是调结构还是调数据。保存成 PNG 也方便贴到实验记录里。
4. 评估与可视化:AUC、校准曲线和风险分层的落地写法
4.1 为什么不能只看准确率
糖尿病筛查场景里,漏掉一个高风险患者的代价远大于误报。正样本占比低时,模型全预测为负也能拿到高准确率,但召回为 0。所以评估必须看 AUC、召回率、F1,以及在不同阈值下的表现。
from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix y_prob = model.predict(X_val).ravel() y_pred = (y_prob >= 0.5).astype(int) print("AUC:", roc_auc_score(y_val, y_prob)) print(confusion_matrix(y_val, y_pred)) print(classification_report(y_val, y_pred, digits=4))y_prob是连续概率,y_pred是按 0.5 阈值二值化。roc_auc_score衡量排序能力,不受阈值影响。classification_report给出每类的精确率、召回率和 F1。如果召回偏低,把阈值从 0.5 降到 0.35 左右再试,筛查场景宁可多报也别漏报。
4.2 校准曲线:概率到底准不准
神经网络输出的概率往往偏乐观,预测 0.8 的人实际患病比例可能只有 0.6。校准曲线把预测概率分桶,看每桶里实际正样本比例。如果曲线在对角线下方,说明模型高估风险,需要用 Platt scaling 或 isotonic regression 做后校准。
from sklearn.calibration import calibration_curve prob_true, prob_pred = calibration_curve(y_val, y_prob, n_bins=10) plt.figure(figsize=(6, 6)) plt.plot(prob_pred, prob_true, marker="o", label="Model") plt.plot([0, 1], [0, 1], linestyle="--", label="Perfectly calibrated") plt.xlabel("Predicted probability") plt.ylabel("Fraction of positives") plt.legend() plt.savefig("calibration_curve.png", dpi=150)n_bins=10把概率分成 10 段。曲线越贴近虚线越好。如果偏离明显,在医疗场景里要谨慎直接输出概率,最好只输出风险等级。
4.3 风险分层:把概率翻译成业务能用的标签
体检系统不会直接展示 0.73 这个数字,需要映射成低、中、高三级。按验证集概率分布的分位数切,比如低于 60% 分位为低风险,60% 到 85% 为中风险,高于 85% 为高风险。
low, high = np.percentile(y_prob, [60, 85]) def risk_level(p): if p < low: return "低风险" elif p < high: return "中风险" return "高风险" df_val = pd.DataFrame({"prob": y_prob, "true": y_val}) df_val["risk"] = df_val["prob"].apply(risk_level) print(df_val.groupby("risk")["true"].agg(["mean", "count"]))分位数切点要根据业务容忍度调。如果高风险组实际患病率低于 50%,说明切点太松,往上提。这个表直接给产品看,比 AUC 直观得多。
5. 避坑与排查:BoxCox、Keras 和评估环节的五个血泪教训
5.1 现象:BoxCox 报错「Data must be positive」
原因:字段里有 0 或负值,BoxCox 定义域要求严格为正。解决:先做平移x - x.min() + 1,或者改用PowerTransformer(method="yeo-johnson"),后者支持零和负值。我一般优先 Yeo-Johnson,省得记平移量。
5.2 现象:验证集 AUC 比训练集高很多
原因:数据泄漏。最常见的是先对全量数据做了 BoxCox 和标准化再切分,验证集统计量进了训练。解决:切分放在所有拟合类变换之前,BoxCox 的 λ、标准化的均值方差都只在训练集上算。另一个隐蔽来源是用了未来信息字段,比如「是否已确诊」这类标签泄漏列,必须删。
5.3 现象:Keras 训练 loss 不下降,accuracy 卡在 0.5
原因:输入没有标准化,或者标签列混进了特征。解决:检查X_train的均值和方差是否接近 0 和 1;检查feature_cols里有没有把label或id带进去。另一个常见原因是学习率太大,Adam 默认 1e-3 对某些数据偏大,降到 1e-4 试。
5.4 现象:模型预测概率全是 0 或全是 1
原因:正负样本极度不平衡,模型学会了全预测多数类。解决:加class_weight,或者用 focal loss 替代 binary crossentropy。focal loss 降低易分样本的权重,让模型关注难分样本。Keras 里可以自定义 loss 函数实现。
5.5 现象:早停后验证 AUC 仍然波动大
原因:验证集太小,或者切分时没有 stratify。解决:改用 5 折交叉验证,每折都做早停,取平均 AUC。交叉验证还能顺带估计模型方差,比单次切分可靠得多。代价是训练时间翻 5 倍,小数据集上值得。
6. 进阶技巧:用交叉验证集成和阈值搜索把 AUC 再抬一截
单次切分的验证 AUC 有随机性,想稳,用 5 折交叉验证训练 5 个模型,推理时取平均概率。这个做法在竞赛里几乎是标配,代价是训练时间线性增长,但小数据集上完全可接受。
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) oof_prob = np.zeros(len(X)) test_prob = np.zeros(len(X_val)) for fold, (tr_idx, va_idx) in enumerate(skf.split(X, y)): X_tr, X_va = X[tr_idx], X[va_idx] y_tr, y_va = y[tr_idx], y[va_idx] scaler = StandardScaler() X_tr = scaler.fit_transform(X_tr) X_va = scaler.transform(X_va) X_val_s = scaler.transform(X_val) m = build_model(X_tr.shape[1]) es = callbacks.EarlyStopping(monitor="val_auc", patience=15, mode="max", restore_best_weights=True) m.fit(X_tr, y_tr, validation_data=(X_va, y_va), epochs=200, batch_size=64, class_weight=class_weight, callbacks=[es], verbose=0) oof_prob[va_idx] = m.predict(X_va).ravel() test_prob += m.predict(X_val_s).ravel() / 5 print("OOF AUC:", roc_auc_score(y, oof_prob)) print("Ensemble AUC:", roc_auc_score(y_val, test_prob))oof_prob是 out-of-fold 预测,每个样本的预测都来自没在训练中见过它的模型,用它评估最干净。test_prob是 5 个模型对验证集的平均预测,集成后方差降低,AUC 通常比单模型高 1 到 3 个点。
阈值搜索是另一个提分点。默认 0.5 不一定最优,按业务目标扫一遍阈值,找 F1 或召回最大的点。
thresholds = np.arange(0.2, 0.8, 0.02) best_f1, best_thr = 0, 0.5 for thr in thresholds: pred = (test_prob >= thr).astype(int) f1 = f1_score(y_val, pred) if f1 > best_f1: best_f1, best_thr = f1, thr print(f"Best threshold={best_thr:.2f}, F1={best_f1:.4f}")扫阈值的逻辑很直接:遍历 0.2 到 0.8,每步算 F1,记录最优。筛查场景如果更看重召回,把目标函数换成recall_score。这个阈值要固化到推理代码里,别每次上线再猜。
最后说个习惯:每次实验把 λ 值、网络结构、学习率、AUC、最优阈值记到一张表里,跑上十几轮后回头看,哪些参数真正有用一目了然。我早期不做记录,调了两周发现最优配置和第一版差不多,纯属白干。希望帮到你。
本文还有配套的精品资源,点击获取