资讯动态

朴素贝叶斯实现糖尿病检测:从贝叶斯定理到Python工程实践

发布时间:2026/10/13 13:30:56 来源:尧图企业网站定制
简介糖尿病早期筛查是健康管理的重要环节这份 PDF 完整记录了一个基于贝叶斯定理构建糖尿病检测系统的设计过程适合机器学习初学者、医学数据建模人员以及模式识别课程设计参考。文档以皮马印第安人糖尿病数据集为基础交代了朴素贝叶斯分类原理、数据加载与拆分、均值/方差/标准差计算、高斯概率密度估计及分类预测实现并展示了如何评估准确率并用图表呈现发病参数关系。系统基于 Python PyCharm 实现流程图中数据划分、汇总统计、概率预测、精度评估几个环节均有明确说明代码段和结果分析完整保留读者可对照复现。资源为单份 PDF 文档压缩包大小 771KB轻量易读已有 234 人学习下载适合快速搭建糖尿病风险预测实验也可作为课程报告或答辩底稿。1. 贝叶斯定理做糖尿病检测指标正常不等于没事这笔账得靠条件概率算「空腹血糖 5.8 mmol/L体检单上没标红但结合 55 岁、BMI 29、直系亲属有糖尿病史这三个条件医生大概率会建议你去查糖耐量。」基于贝叶斯定理的糖尿病检测系统做的就是同一件事把医生脑子里那个「先按人群比例估一个风险再看指标逐项修正」的判断过程落成一套可复现、可解释、几百条样本就能跑通的代码。它不需要显卡不需要海量数据输出的是「患病概率」而不是一个冷冰冰的分类标签适合做医学信息方向的课程设计与毕业论文课题也适合在体检初筛、健康管理小程序里当一个可解释的风险提示模块。这套方案真正的门槛不在数学而在数据清洗、离散化和阈值选择那几个容易翻车的细节上。2. 贝叶斯公式在糖尿病检测里怎么落地先验、似然、后验的工程对应2.1 公式拆解后验概率 似然 × 先验 ÷ 证据朴素贝叶斯解决的根本问题是已知一个人有一组体检指标他患糖尿病的概率是多少。数学上写下来就是P(患病 | 指标组合) P(指标组合 | 患病) × P(患病) ÷ P(指标组合)这一项一项落到工程上对应关系非常直接。「先验」P(患病) 是数据集中确诊患者占总量的比例比如一份筛查数据里患病率是 12%那先验就是 0.12。「似然」P(指标组合 | 患病) 是在确诊人群里出现某一种指标组合的比例。「证据」P(指标组合) 是所有人里出现这种组合的比例它在分类时只起归一化作用通常可以不算。难点在于「指标组合」可能有很多种数据量根本不够统计每一种组合的频率。朴素贝叶斯的狠招是加了一条假设在已知患病与否的条件下各特征之间相互独立。于是联合概率被拆成单个条件概率的乘积P(指标组合 | 患病) P(空腹血糖偏高 | 患病) × P(肥胖 | 患病) × P(年龄偏大 | 患病) × …这样一来每种指标只需要统计一张一维的条件概率表数据需求瞬间从指数级降到线性级。工程实现上为了数值稳定一般对等式两边取对数把连乘变成连加避免几百个小概率相乘直接下溢成 0。这是我建议所有实现里都采用的做法后面代码部分会展示。2.2 选型理由为什么朴素贝叶斯适合小样本医疗筛查糖尿病检测系统在真实落地时通常拿到的数据只有几百到几千条标签还是人工核对的。这种规模下深度学习模型基本没有优势。把朴素贝叶斯和几类常见分类器放在一张表里对比选型理由就很清楚了维度朴素贝叶斯逻辑回归决策树SVM小样本表现好几百条可稳定中等需要调正则易过拟合剪枝参数多中等核函数选择依赖经验可解释性高概率表直接可读系数可解释规则可读差核映射是黑匣子缺失值容忍高计算时可跳过单特征低需要先填充中低需要先填充训练成本O(n)一次统计迭代求解O(n log n)二次以上复杂度调参负担几乎为零正则化强度深度、剪枝、叶子大小C、核函数、gamma我一般会建议选朴素贝叶斯核心理由是它的「下限高」不需要精细调参只要特征离散化做得合理基线效果就能达到可用的筛查水平。特别适合做初筛——先把大部分人正确区分开再交给后续更重的模型或人工复核。它也有明确的适用边界。如果手里的数据到了几千条且特征之间强相关同样的数据喂给梯度提升树往往能拿到更高的 AUC但可解释性和训练成本会同步上升。朴素贝叶斯不是「最强」的模型而是「最稳」的模型这恰恰是医疗初筛场景最看重的东西。2.3 数据组织与标签定义先验概率不是拍脑袋训练数据怎么组织直接决定先验和似然有没有意义。首先要明确标签口径患病组必须是确诊患者对照组应该排除「糖尿病前期」这类说不清的状况。常见做法是去掉糖尿病前期样本只保留确诊和完全正常两类先做干净的二分类。如果实在想保留把糖尿病前期单独作为第三类也可以但模型复杂度会上来筛查场景不推荐。切分训练集和验证集时有一条硬规矩按「人」切不能按「记录」切。同一个人的多次就诊记录如果既进了训练集又进了验证集模型等于见过答案验证分数会虚高得离谱。数据量小的时候我一般用留一法或 Stratified 交叉验证每折按人分组。先验概率直接从训练集统计而不是引用某篇文献的数字因为不同地区、不同体检人群的患病率差异很大。3. 特征工程决定上限分箱、缺失值与独立性假设的工程处理3.1 连续指标离散化临床切点优先等频分箱兜底朴素贝叶斯处理连续变量有两条路一是高斯朴素贝叶斯假设每个特征在类别内服从正态分布二是把连续变量离散化成类别特征后统计频率。实际体检数据里血糖、胰岛素、BMI 几乎都是偏态分布高斯假设经常不成立所以我默认走离散化这条路。离散化怎么做决定了模型上限。这里给出一个可直接参考的分箱方案import pandas as pd df pd.read_csv(diabetes_screen.csv) # 空腹血糖按临床常用切点分箱单位 mmol/L glucose_bins [-float(inf), 5.6, 6.1, 7.0, float(inf)] glucose_labels [正常, 空腹血糖受损, 偏高, 疑似糖尿病] df[glucose_bin] pd.cut( df[fasting_glucose], binsglucose_bins, labelsglucose_labels, rightFalse ) # BMI 按亚洲人群常用标准分箱 bmi_bins [-float(inf), 18.5, 24.0, 28.0, float(inf)] bmi_labels [偏瘦, 正常, 超重, 肥胖] df[bmi_bin] pd.cut(df[bmi], binsbmi_bins, labelsbmi_labels, rightFalse) # 年龄没有统一临床切点用等频分箱 df[age_bin] pd.qcut(df[age], q4, labels[青年, 中年, 中老年, 老年])这里有一个细节值得多说一句rightFalse表示区间左闭右开避免同一个数值被分进两个箱。血糖切点 5.6、6.1、7.0 是临床判断空腹血糖受损和疑似糖尿病的常用边界这类先验知识直接编码进分箱比让数据自己找切点更稳定。BMI 的四个档位同理。为什么年龄不用等宽分箱而用等频因为年龄的分布相对均匀等宽分箱还可以但遇到像胰岛素这种长尾分布等宽分箱会出现某个箱几乎没有样本的情况条件概率表直接塌陷。等频分箱能保证每箱样本量差不多概率估计更稳。这是分箱策略里性价比最高的兜底方案。3.2 缺失值处理三选一剔除、填充、还是「缺失」单独成档体检数据几乎没有干净的。血糖可能没查胰岛素可能漏检血压值可能没录。缺失值处理有一条顺序先看缺失比例再看缺失原因最后决定策略。# 第一步缺失率超过 30% 的特征直接丢弃 missing_ratio df.isna().mean() drop_cols missing_ratio[missing_ratio 0.3].index df.drop(columnsdrop_cols, inplaceTrue) # 第二步对缺失率中等的特征用训练集中位数填充同时生成“是否缺失”标志列 train_med df_train[insulin].median() df_train[insulin_missing] df_train[insulin].isna().astype(int) df_test[insulin_missing] df_test[insulin].isna().astype(int) df_train[insulin] df_train[insulin].fillna(train_med) df_test[insulin] df_test[insulin].fillna(train_med)有两个容易踩的坑。第一填充用的中位数只能从训练集里算全量数据的统计量会泄漏到验证集里这在交叉验证中会直接抬高分数。第二对胰岛素这类临床上「没查」和「查了偏低」含义完全不同的指标单独加一列insulin_missing作为特征比单纯填充更有信息量——病人为什么只查了血糖没查胰岛素这件事本身可能和病情有关系。第三种做法是「缺失单独成档」。比如舒张压缺失我不填充而是把缺失值作为一个单独的类别「未测量」放进离散化后的特征里。这样做的代价是特征取值变多、概率表变稀疏但小样本下往往比用均值填充更稳妥因为它没有引入「虚假的中间值」。具体用哪种我一般以交叉验证结果为准。3.3 独立性假设破了怎么办相关性特征要合并或剔除朴素贝叶斯的命门是条件独立假设。糖尿病检测里最典型的问题就是空腹血糖、餐后 2h 血糖、糖化血红蛋白这三个特征都反映血糖状态彼此强相关。如果全部放进模型血糖这个信息被重复加权了三次后验概率会被推到严重偏离真实水平的极端值。工程上有两种常见处理。第一种是去掉冗余特征两两相关性超过 0.7 的一组特征只保留一个通常保留信息最全、缺失最少的那个。第二种是合成特征把强相关的几个指标合成一个「代谢风险评分」比如用空腹血糖和糖化血红蛋白偏离正常范围的程度做加权平均再作为一个新特征分箱。我一般会先把临床知识梳理一遍血糖类特征保留一个血压类保留收缩压体型保留 BMI再加上年龄和家族史。用chi2或互信息做二次筛选但很少让统计量单独做决定。独立假设虽然理论上不成立工程上只要不把重复信息当多份用朴素贝叶斯在这个场景下依旧能给出可用的排序结果。4. 用 Python 实现糖尿病检测模型从概率表到判别函数4.1 训练阶段统计先验与条件概率表训练的核心工作就是统计两张表先验概率表和一个嵌套的条件概率表。下面是一个不依赖机器学习库的手写实现逻辑清晰也方便后续加平滑和自定义逻辑。import pandas as pd import numpy as np class NaiveBayesDetector: def __init__(self, alpha1.0): self.alpha alpha self.prior {} self.cond_prob {} self.classes_ None def fit(self, X: pd.DataFrame, y: pd.Series): self.classes_ list(np.sort(y.unique())) n_total len(y) # 先验概率各类样本占比 for c in self.classes_: self.prior[c] (y c).sum() / n_total # 条件概率表P(特征取值 | 类别) for col in X.columns: self.cond_prob[col] {} categories X[col].dropna().unique() for c in self.classes_: sub X.loc[y c, col] counts sub.value_counts() total sub.shape[0] self.cond_prob[col][c] { v: (counts.get(v, 0) self.alpha) / (total self.alpha * len(categories)) for v in categories } return self这段代码的fit只做一件事按类别统计每个特征取值的出现频率。先验概率就是单纯的样本占比条件概率表的每一项都是「在患病组里空腹血糖属于『偏高』的样本占患病组总数的比例」。alpha是拉普拉斯平滑系数默认 1.0作用会在下一节专门讲。dropna().unique()用来固定类别集合确保验证集里出现训练集没见过的取值时能走兜底逻辑。有一个容易被忽略的点条件概率的分子用counts.get(v, 0) self.alpha分母用total self.alpha * len(categories)。这样所有类别的概率之和仍然等于 1数学上是自洽的。4.2 拉普拉斯平滑与零概率公式里不能出现除以零为什么必须做平滑假设训练集里 55 岁以上的患病组样本数量太少恰好没有「空腹血糖偏高」这个组合那 P(血糖偏高 | 患病) 就等于 0。后验概率是连乘的任何一项为 0 都会把整个结果压成 0等于一票否决了所有其他证据。这在医学上完全说不通——某个年龄段的样本里恰好没人血糖偏高不代表这个年龄段的人不会血糖偏高。平滑系数alpha的取值影响也值得展开。默认 1.0 是最常见的做法等价于给每个取值「预置」了一个假样本。数据量只有几百条时alpha在 0.1 到 1.0 之间通常都能稳定工作。设得太小零概率问题压不住设得太大真实频率被稀释模型会偏向均匀分布区分度下降。我在调参时一般让它随样本量走样本越少平滑越要保守一点。4.3 预测阶段从对数概率到风险判定训练完成后预测阶段把先验和所有条件概率取对数求和再做归一化。完整代码是这样的def predict_proba(self, X: pd.DataFrame) - np.ndarray: proba_list [] for _, row in X.iterrows(): log_scores {} for c in self.classes_: log_scores[c] np.log(self.prior[c]) for col in X.columns: val row[col] table self.cond_prob[col][c] if val in table: log_scores[c] np.log(table[val]) else: log_scores[c] np.log(self.alpha / (len(table) self.alpha * len(table))) # 减最大值再指数防止下溢 max_log max(log_scores.values()) exp_scores {c: np.exp(s - max_log) for c, s in log_scores.items()} total sum(exp_scores.values()) proba_list.append([exp_scores[c] / total for c in self.classes_]) return np.array(proba_list) def predict(self, X: pd.DataFrame, threshold0.5) - np.ndarray: proba self.predict_proba(X) return (proba[:, 1] threshold).astype(int)max_log这一步是很多初写者容易漏掉的如果后验概率极小取完对数可能是负几千直接np.exp会下溢成 0。先减去最大值再指数数值上完全等价但能保住精度。threshold参数默认 0.5但在糖尿病筛查场景里这个默认值往往不合适第六章会专门讲怎么调它。如果不想手写用现成库也有对应方案。sklearn 里的CategoricalNB就是为离散特征设计的用法和效果与上面的手写版基本一致。我之所以建议至少手写一遍是因为概率表可以直接打印出来让人复核——医生和导师想看的往往不是准确率而是「肥胖 血糖偏高的人模型算出来有多大概率患病」这个表能直接回答。5. 糖尿病检测模型避坑指南五个让结果翻车的常见问题5.1 准确率虚高全猜健康也能拿到 88% 的正确率现象模型在测试集上准确率 0.91看起来非常理想但把检测结果拿去人工复核糖尿病患者漏掉了将近一半。原因这类筛查数据里健康人群通常占 85% 以上。模型只要全部预测为「健康」准确率就有 0.85 以上。准确率被多数类绑架完全不能反映模型对患病组的识别能力。解决换成混淆矩阵、召回率、特异度来评估。具体到项目里重点看两个指标召回率患病的人里被找出来的比例和特异度健康的人里被正确放行的比例。如果召回率明显偏低先验概率 P(患病) 会被多数类带偏需要把判别阈值往下调或者训练时对少数类加权。5.2 分箱边界一变结果就翻车现象上一版用「血糖 5.6 / 5.6-7.0 / ≥ 7.0」三档分箱交叉验证 AUC 0.84换成等距五档后AUC 掉到 0.77完全说不清是模型变强了还是变弱了。原因等距分箱破坏了血糖与患病风险之间的非线性关系。糖尿病风险在 7.0 附近存在明显的阈值效应等距切分会把 6.9 和 7.1 强行分到同一箱把 7.1 和 8.9 分到不同箱信息被切碎了。等频分箱也有类似问题它会为了样本量均匀而把切点放到 6.5 这种没有临床意义的位置。解决优先采用临床标准切点比如血糖的 5.6、6.1、7.0BMI 的 18.5、24、28。这些边界来自医学共识不随数据集波动。如果没有临床标准再退而求其次用等频分箱并且把分箱逻辑封装成一个独立函数在训练集上拟合边界、验证集上只做映射避免数据泄漏。5.3 连续特征直接进模型条件概率全部变成 0现象把空腹血糖、年龄、BMI 的原始数值直接喂进 MultinomialNB跑出来所有样本的患病概率都是 0 或 nan。原因MultinomialNB 的输入被解释为「计数」连续值对它没有意义如果换 GaussianNB血糖的长尾分布又会让均值估计被极端值拉偏概率密度估计失真。解决先离散化再做分类这是这个场景里最省事的路径。血糖、BMI、年龄全部转成有序类别特征后再用 CategoricalNB 或手写的概率表进行训练。如果要保留连续信息宁可换逻辑回归或梯度提升树也不要硬套高斯朴素贝叶斯。5.4 数据泄漏同一个人既在训练集又在验证集现象训练集 AUC 0.92验证集 AUC 只有 0.61差距大得不像同一份数据。反复调参后依旧如此。原因切分时按行而不是按人。某个人做了三次体检三条记录有两条进了训练集、一条进了验证集模型记住了「这个人」而不是学到了「什么样的人风险高」。换一批人做验证性能立刻打回原形。解决以人为粒度分组切分。先对每个人的 ID 做 groupby再用 GroupShuffleSplit 或手动按人抽样保证同一人的所有记录只出现在一侧。这一步要在特征工程之前就做好填充均值、分箱边界等所有统计量都只从训练集里计算。5.5 概率过于自信后验不是真实风险现象模型给某个患者打出 0.97 的患病概率但临床复核显示他只有轻度的血糖异常医生和患者都被这个数字吓到了。原因朴素贝叶斯在特征实际相关时会把重复信息当成多份独立证据累乘导致后验概率向 0 和 1 两端极化。模型输出的概率适合做「排序」不适合直接当作真实发生的概率来看待。解决做概率校准。常见做法是在验证集上用 Platt scaling 或等温回归把原始后验映射成校准概率再对外展示。更稳妥的做法是只把原始后验当风险分数用对外输出「低危 / 中危 / 高危」三档彻底避开对精确小数的不合理解读。6. 阈值选择与概率校准系统上线前值得养成的两个习惯筛查系统真正给人用的时候默认 0.5 这个阈值几乎总是错的。健康人群基数大、患病先验低直接比较后验和 0.5 会让大量真实患者被漏掉。正确做法是在验证集上画 ROC 曲线用约登指数找「敏感度 特异度 - 1」最大的那个点用它作为判定阈值。def best_threshold(y_true, proba, step0.01): best_t, best_j 0.5, 0 thresholds np.arange(0.1, 0.9, step) for t in thresholds: pred (proba t).astype(int) tp ((pred 1) (y_true 1)).sum() fn ((pred 0) (y_true 1)).sum() fp ((pred 1) (y_true 0)).sum() tn ((pred 0) (y_true 0)).sum() sensitivity tp / (tp fn) specificity tn / (tn fp) youden sensitivity specificity - 1 if youden best_j: best_j, best_t youden, t return best_t约登指数的思路是同时最大化「该抓的抓得住」和「该放的放得开」。如果系统定位是初筛后续还有医生复核我会刻意把阈值再往下调一点宁可多检出假阳性也要降低漏检。这个取舍要写进设计文档里不能等上线后让使用者自己猜。第二个习惯是概率校准。模型输出的后验在特征相关时偏自信对外展示前先做一次 Platt 校准把分数映射到更贴近真实频率的区间。如果系统只输出「低中高」三档风险这点也可以省但内部评估时仍然要分档统计每档的真实患病率验证分档有意义。我最初做类似系统时直接套默认 0.5 阈值上线结果漏掉的几例全是空腹血糖刚过临界、但年龄和家族史风险很高的人。后来把「先找最佳阈值再校准概率最后给档位」变成固定习惯系统才真正从「能算」变成「敢用」。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑