基于贝叶斯定理的糖尿病检测系统:手写高斯朴素贝叶斯全链路拆解

发布时间:2026/11/4 7:26:01
基于贝叶斯定理的糖尿病检测系统:手写高斯朴素贝叶斯全链路拆解

简介:这份PDF围绕朴素贝叶斯与高斯概率分布在疾病预测中的应用展开,面向模式识别、机器学习课程作业与毕设阶段的学生,帮助其理解如何把贝叶斯定理落到一个可运行的糖尿病检测流程中。文档依次讲解贝叶斯分类原理、皮马印第安人糖尿病数据集的九项数值指标、按类别汇总均值方差标准差、高斯概率密度计算、概率累乘与最大后验判别,并给出loadCsv、SplitDataset、separateByClass、summarizeByClass、calculateProbability等函数的实现思路与流程图,同时说明90%训练、10%测试的划分方式和准确率评估、图表可视化方法,还交代了i7-8550U、8GB内存、Windows 11与PyCharm的软硬件环境。压缩包共1个PDF文件,约771KB,篇幅紧凑、公式与代码对照清晰,可直接作为课程报告撰写、算法复现和参数调试的参考。该资源已有237人学习,适合希望从零复现贝叶斯分类器并理清特征概率计算细节的读者。

1. 一份课程设计 PDF 拆出来的完整链路:基于贝叶斯定理的糖尿病检测系统到底值不值得跑

有个做模式识别课程设计的朋友把 PDF 发给我,说就几十页,代码全是截图,看着像能跑又不敢动。打开一看,确实是典型的学生交作业式资料:一张流程图、两行公式,后面全是编辑器里截出来的代码,路径里还带着中文字段。但它完整——从 CSV 加载、按类别汇总均值方差、用高斯概率密度算条件概率、连乘取最大、最后算准确率,一条链没断。基于贝叶斯定理的糖尿病检测系统这类题目最怕只剩公式,这份资料把每一步都落成了函数,还额外挂了一条线性回归的可视化分支。适合两类人:想找个能改的最小分类器练手的 Python 新手,和要给课程作业找一个能讲清原理的骨架的人。

2. 项目骨架与技术选型:两个脚本、两套数据源、一条分类主线

拿到这种单文件作业,先别急着跑,先把文件摊开看它到底有几条数据流。这个项目表面上是一个"糖尿病检测系统",实际是两个相互独立的脚本:一个从 CSV 读数据、手写高斯朴素贝叶斯做二分类并输出准确率;另一个调用库里的糖尿病数据集,对 10 个特征挨个做一元线性回归并画图。两者之间没有数据传递,也没有共享模型,只是被同一份报告串在一起。理解这一点很关键,因为它决定了你后面改动时该动哪个文件,以及哪部分出错不会影响另一部分。

2.1 目录与环境:把课程设计拆成最小可运行工程

原始代码把数据集绝对路径写死在函数里,这在别人机器上必挂。我习惯先把工程摊平成下面这种结构,路径全部相对脚本所在目录,跑之前只需要确认 CSV 在那儿。数据集本身是英文列名的逗号分隔文件,8 个生理指标加 1 列 0/1 标签,全部是数值,加载时统一转 float。

文件作用依赖
naive_bayes_diabetes.py手写高斯朴素贝叶斯主流程:加载、拆分、汇总、预测、评估csv、random、math(标准库,零第三方依赖)
regression_plot.py单特征线性回归与 5×2 子图可视化numpy、pandas、matplotlib、scikit-learn
data/diabetes.data.csv8 个生理指标 + 1 个 0/1 标签,纯数值无

安装依赖只需要四条命令,分类脚本其实一条都不用装,这是这类手写实现最大的好处——你把算法逻辑抄到任何一台只有标准库的机器上都能跑,不用赌环境。

# 只跑分类脚本的话,这一条都不用执行 pip install numpy pandas matplotlib scikit-learn # 建目录把数据放进去,路径与代码里保持一致 mkdir -p data && cp diabetes.data.csv data/

提示:先只跑分类脚本,确认算法链路通了,再去装那一堆画图依赖。依赖装完再排查算法问题,出错了你分不清是环境的事还是代码的事。

2.2 主链路:一个样本从 CSV 到 accuracy 的六个环节

整份资料的核心就是一张流程图,翻译成工程语言是六步:载入数据、按比例拆训练测试集、按类别汇总每个特征的均值与标准差、对测试样本算每个类别的条件概率连乘、取概率最大的类别作为预测、与真实标签比对算准确率。这六步在代码里对应六个函数,基本是一一映射,没有多余的封装,也没有中间落盘,跑一次就是一次完整的内存流水线。

第一步loadCsv()把 CSV 读成二维列表,全部转 float;第二步splitDataset()用不放回抽样的方式切出训练集,剩下的自然成为测试集;第三步separateByClass()先按标签分组,summarizeByClass()再对每组按列算统计量;第四步calculateProbability()提供单特征的高斯密度,calculateClassProbabilities()负责连乘;第五步predict()在概率字典里取最大值;第六步getPredictions()批量预测、getAccuracy()算百分比。链条短的好处是每一环都能单独打印验证,坏处是任何一环的假设错了,后面全歪,而且不会报错,只会安静地给你一个偏低的准确率。

2.3 选型:为什么手写高斯朴素贝叶斯而不是直接调 GaussianNB

用sklearn.naive_bayes.GaussianNB三行就能跑完,为什么这份资料要手写?因为课程设计要讲清楚"条件概率连乘"到底乘的是什么。手写版把每个中间量都暴露出来了:某一类下某个特征的均值、标准差、单个特征的概率密度、连乘后的字典。你要改先验、改平滑、改对数域计算,改的都是看得见的几行。

维度手写实现GaussianNB
代码量约 80 行,6 个函数3 行
可解释性中间概率全部可见,能逐样本打印需要看class_prior_、theta_、sigma_属性
数值稳定性直接连乘,特征多时会下溢内部用对数似然,稳定
先验处理默认不乘先验自动按样本比例估计先验
适用场景教学、改算法、验证理解生产、对比基线

实际做法我一般两个都写:手写版用来对照原理,GaussianNB用来当正确答案校验手写版有没有写错。两者在同一份数据、同一个划分下准确率差超过一两个百分点,基本可以断定手写版有 bug,而不是算法本身不行。

3. 数据层拆解:loadCsv、splitDataset 与 summarizeByClass

数据层是这份资料里最容易翻车的地方,因为原始代码把路径、编码、类型转换全挤在一个函数里,一个环节出问题就是一行红色堆栈,看不出根因。拆开看,它其实只做三件事:把文本读成数值矩阵、把矩阵切成两份、把训练集按标签压缩成统计量。这三件事每一件都有值得说的细节。

3.1 loadCsv:编码、路径与字符串转 float

原始写法是csv.reader(open("D:\\...\\diabetes.data.csv", "r", encoding="utf-8")),问题有三个:路径写死、open 没有上下文管理、没有对异常行做任何提示。我改成参数化路径加显式校验,报错的时候能直接告诉你文件在哪、缺在哪。

# -*- coding: utf-8 -*- import csv import os def loadCsv(path="data/diabetes.data.csv"): """按行读取 CSV,把每一列都转成 float,返回二维列表""" if not os.path.exists(path): raise FileNotFoundError("数据集不存在: %s" % os.path.abspath(path)) with open(path, "r", encoding="utf-8", newline="") as f: reader = csv.reader(f) dataset = [row for row in reader if row] # 过滤空行,避免 [float(x) for x in []] 报错 for i in range(len(dataset)): # 标签列一起转 float,后面比较时类型才一致 dataset[i] = [float(x) for x in dataset[i]] return dataset

逻辑说明:csv.reader返回的是迭代器,必须先list()或者列表推导落地才能取长度;with保证文件句柄释放,在反复调用时不会堆积;newline=""在 Windows 上能避免行尾多出一个空行导致float('')崩掉;最后的双重循环做整表类型转换,标签也转成0.0/1.0,这一步决定了后面testSet[x][-1] == predictions[x]能不能比较成功。

参数说明:path默认用相对路径,相对于你执行脚本时的工作目录。如果从别的目录启动,建议改成os.path.join(os.path.dirname(__file__), "data", "diabetes.data.csv"),这样不管在哪执行都能找到。

注意:float(x)抛ValueError往往不是代码问题,而是数据里混了表头行或者空字段。这个报错本身就是数据体检,别急着 try/except 吞掉,先看看是哪一行。

3.2 splitDataset:90/10 随机拆分的写法与它的代价

原始实现用的是"边抽边删":算出训练集条数,然后在副本上随机取一个下标,pop出来塞进训练集,循环到够数为止,剩下的副本就是测试集。这个写法等价于洗牌后切片,但不引入random.shuffle,逻辑上更直观,代价是时间复杂度 O(n) 起步,而且默认没有随机种子,每次跑出来的划分都不一样。

import random def splitDataset(dataset, splitRatio, seed=42): """不放回抽样拆分,splitRatio 是训练集占比""" random.seed(seed) # 固定随机源,保证复现 trainSize = int(len(dataset) * splitRatio) copy = list(dataset) # 拷贝一份,避免 pop 破坏原始数据 trainSet = [] while len(trainSet) < trainSize: index = random.randrange(len(copy)) trainSet.append(copy.pop(index)) # 抽走一条,剩下的天然是测试集 return trainSet, copy

逻辑说明:copy = list(dataset)是浅拷贝,外层列表是新的,内层行对象还是共享的,但因为后续只读不写,安全;random.seed(seed)是这类脚本最该补的一行,没有它你没法回答"上次准确率 74%、这次 71% 到底是我改坏了还是划分变了";while循环的终止条件依赖int()向下取整,当数据集条数小于 10 时trainSize会变成 0,训练集为空,后面summarizeByClass直接返回空字典,predict会给所有样本返回None。

参数说明:splitRatio是训练集占比,正文里说 90% 训练 10% 测试,但原始代码__main__里写的是splitRatio = 0.67,这是个典型的口径不一致,我放到排查章细说。seed建议固定,做对比实验时只改模型不改种子。

3.3 separateByClass 与 summarizeByClass:一次zip(*dataset)的矩阵转置

按类别分组是朴素贝叶斯的第一步,因为所有统计量都是"在某一类内部"算的。原始代码用字典做分组容器,键是标签,值是样本行列表,写法没问题。真正值得讲的是summarize()里的zip(*dataset):这是 Python 里最省事的转置技巧,dataset是 n 行 9 列的二维列表,zip(*dataset)会把它翻成 9 个元组,每个元组是一整列。

import math def separateByClass(dataset): """按最后一列的标签把样本分组""" separated = {} for vector in dataset: label = vector[-1] separated.setdefault(label, []).append(vector) return separated def mean(numbers): return sum(numbers) / float(len(numbers)) def stdev(numbers): """样本标准差,分母是 n-1""" avg = mean(numbers) variance = sum([pow(x - avg, 2) for x in numbers]) / float(len(numbers) - 1) return math.sqrt(variance) def summarize(dataset): """对一组的每一列算 (均值, 标准差)""" summaries = [(mean(attr), stdev(attr)) for attr in zip(*dataset)] del summaries[-1] # 最后一列是标签,不能参与统计 return summaries def summarizeByClass(dataset): separated = separateByClass(dataset) return {label: summarize(rows) for label, rows in separated.items()}

逻辑说明:del summaries[-1]这行是必须的,如果不删,标签列也会被算出一个均值方差,预测时inputVector[i]会越界或者把标签当特征算进去,准确率会莫名其妙地偏高——这是我最常见的一种"看起来能跑但其实错了"的写法。stdev用n-1是样本标准差的无偏估计,用n会让方差偏小,概率密度曲线变尖。

参数说明:separateByClass返回的字典键是0.0和1.0,因为loadCsv已经全转成 float 了,后面predict返回的bestLabel也是 float,getAccuracy里比较时类型一致。如果你改成从别处读整数标签,这里就是第一个不相等的地方。

4. 算法层拆解:高斯密度、连乘、argmax 到准确率与图表

数据层的输出是两份统计量字典:训练集每个类别、每个特征的均值和标准差。算法层要做的就是把一个待预测样本的 8 个特征值代进去,算出它属于各类的概率,取大的那个。这一层代码不多,但公式和实现之间的对应关系密集,几乎每一行都值得停下来看一眼。

4.1 calculateProbability:高斯概率密度函数的一行实现

朴素贝叶斯假设每个特征在给定类别下服从正态分布,所以单特征的条件概率就是高斯概率密度。公式是 F(x)=1/(σ√(2π))·exp(-(x-μ)²/(2σ²)),代码把它拆成指数部分和前面的系数部分,两行写完。

def calculateProbability(x, mean, stdev): """给定特征值、该类别下该特征的均值与标准差,返回概率密度""" if stdev == 0: # 该特征在这一类下完全没波动,退化成常数 return 1.0 if x == mean else 0.0 exponent = math.exp(-(math.pow(x - mean, 2) / (2 * math.pow(stdev, 2)))) return (1 / (math.sqrt(2 * math.pi) * stdev)) * exponent

逻辑说明:exponent那行对应公式里的exp(...),-(x-mean)²/(2σ²)中的负号不能丢,丢了就变成指数爆炸,math.exp直接抛OverflowError;返回值的系数1/(√(2π)·σ)是归一化项,少了它不同 σ 之间的概率不可比,但因为同一类别下所有特征的 σ 范围接近,实测对最终 argmax 影响不大——不过既然要讲原理,这一项不能省。

参数说明:x是待预测样本该特征的值,mean/stdev来自summarize()。stdev == 0的兜底是必须的:某个类别下某个特征所有样本取值完全一样时,stdev为 0,math.pow(0, 2)做分母会抛ZeroDivisionError。用 0 值填补的字段(比如某些指标用 0 表示缺失)最容易触发这一类问题。

4.2 calculateClassProbabilities 与 predict:连乘与最大值

朴素贝叶斯的核心假设是"特征条件独立",所以整个样本属于某类的概率等于各特征条件概率的乘积。代码用一个字典累积,初始值设 1,然后逐个特征乘进去,最后返回字典。

def calculateClassProbabilities(summaries, inputVector): probabilities = {} for classValue, classSummaries in summaries.items(): probabilities[classValue] = 1.0 # 连乘的初始值必须是 1 for i, (mean, stdev) in enumerate(classSummaries): x = inputVector[i] # 第 i 个特征的真实取值 probabilities[classValue] *= calculateProbability(x, mean, stdev) return probabilities def predict(summaries, inputVector): probabilities = calculateClassProbabilities(summaries, inputVector) bestLabel, bestProb = None, -1 # 初始概率设 -1,保证第一个一定进来 for classValue, probability in probabilities.items(): if bestLabel is None or probability > bestProb: bestProb, bestLabel = probability, classValue return bestLabel

逻辑说明:连乘初始值写成 0 是最容易犯的错,乘完永远是 0,predict会给所有样本返回同一类,准确率却不报错,只是难看。predict里没乘类别先验,是因为只做 argmax 时先验通常不改变结果顺序,但如果两类样本量差距大(这份数据里阳性样本本来就少),不乘先验等价于假设各类等概率,会让模型偏向多数类。

参数说明:summaries是summarizeByClass的返回值,inputVector是完整一行,含最后一位标签,但因为summaries里已经del掉了标签列,enumerate的上界是len(classSummaries),正好只遍历 8 个特征,不会碰到标签。这个隐式约定很脆弱,一旦你改了summarize却没同步改这里,就越界了。

下溢风险值得单独提一句:8 个概率密度相乘,每个量级大约 10⁻¹ 到 10⁻²,结果在 10⁻¹⁰ 附近,float 完全够用。但如果特征扩到几十个,乘积会掉到 10⁻³⁰⁰ 以下变成 0.0,两个类别都成 0 时 argmax 就退化成随机。工业实现一律转对数域,加法代替乘法,这是手写版和库版本最大的差距之一。

4.3 getAccuracy:准确率统计与类别不平衡的误导

准确率的定义很直白:预测对的条数除以测试集总数,乘 100 就是百分比。原始代码用一个correct计数器加循环,逻辑没问题,问题在于这个指标本身在糖尿病这种正负样本不平衡的场景下会很乐观。

def getPredictions(summaries, testSet): return [predict(summaries, row) for row in testSet] def getAccuracy(testSet, predictions): correct = 0 for x in range(len(testSet)): if testSet[x][-1] == predictions[x]: # 标签已统一为 float,比较安全 correct += 1 return (correct / float(len(testSet))) * 100.0 if __name__ == "__main__": splitRatio = 0.67 # 训练集占比,与正文里的 90/10 不一致 dataset = loadCsv("data/diabetes.data.csv") trainingSet, testSet = splitDataset(dataset, splitRatio, seed=42) summaries = summarizeByClass(trainingSet) predictions = getPredictions(summaries, testSet) print("Accuracy: %.2f%%" % getAccuracy(testSet, predictions))

逻辑说明:测试集小的时候(按 0.67 拆,几百条数据剩几十条测试),一条样本的差异就是两三个百分点,所以准确率在两个划分之间抖动很正常。我在这种项目里一般至少跑 20 个随机种子取平均,报的是均值±标准差,而不是单次数字,否则读者照着跑出来和你报告的差五六个点,还以为代码坏了。

参数说明:splitRatio在__main__里是唯一需要改的实验变量,比原版多传一个seed。打印格式用%.2f%%是为了避免输出一长串浮点尾数,%要转义成%%。

4.4 回归分支:load_diabetes、reshape 与 5×2 子图

可视化脚本走的是另一条路:不读 CSV,而是从 sklearn 内置数据集加载,返回的是一个字典,三个键data、target、feature_names,10 个特征、442 条样本,target是连续值(一年后病情发展的量化指标),所以这里做的是回归不是分类。原始代码把三个键拆出来拼成 DataFrame,然后循环 10 个特征,每个画一张散点图加一条拟合直线,布局 5 行 2 列。

import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_diabetes from sklearn.linear_model import LinearRegression data_diabetes = load_diabetes() df = pd.DataFrame(data_diabetes["data"], columns=data_diabetes["feature_names"]) target = data_diabetes["target"] plt.figure(figsize=(12, 25)) # 5 行 2 列,画布要够高 for i, col in enumerate(df.columns): X = df.loc[:, col].values.reshape(-1, 1) # 一维列必须转二维,否则 sklearn 报错 model = LinearRegression().fit(X, target) score = model.score(X, target) # 一元回归的 R² ax = plt.subplot(5, 2, i + 1) # 子图编号从 1 开始,不是 0 ax.scatter(X, target, s=6, alpha=0.5) xs = np.linspace(X.min(), X.max(), 100).reshape(-1, 1) ax.plot(xs, model.predict(xs), c="red") # 用 predict 画线,斜率截距自动带符号 ax.set_title(f"{col}: R2={score:.3f}") plt.tight_layout() plt.show()

逻辑说明:reshape(-1, 1)是这段代码里最容易漏的一行,df.loc[:, col].values是一维数组,LinearRegression.fit要求二维特征矩阵,不转就抛Expected 2D array, got 1D array instead;plt.subplot(5, 2, i + 1)的第三个参数是 1 到 10 的编号,从 0 开始会报参数越界;ax.plot(xs, model.predict(xs))比手动算k*x+b更稳,coef_是数组,直接乘一维x在某些 numpy 版本上会有广播警告。

参数说明:figsize=(12, 25)是按 10 张图、每张 5 英寸高估出来的,图少了会留白,多了会挤;s=6控制散点大小,alpha=0.5让重叠点能看出密度。另外注意这里用同一份数据既拟合又打分,R² 是偏乐观的样本内指标,只适合看特征强弱的相对排序,不能当模型泛化能力报告。

读图结论对得上原文:性别那一列几乎是一条水平线,R² 接近 0;年龄有一点弱正相关;BMI 那一列点云明显向上倾斜,R² 在 10 个特征里最高,血压次之。这套图的价值不在精度,而在于让人一眼看出"哪些指标值得进模型、哪些可以扔",做特征筛选时比看相关系数矩阵直观。

5. 常见问题排查:跑不通、准确率异常、图表糊成一团的 5 个坑

这份资料的代码抄下来直接跑,八成会遇到下面几类问题。它们不会让程序崩得很明显,往往是"能跑但结果不对",或者报一个和你写的逻辑八竿子打不着的错。

5.1 加载与路径:文件找不到、类型转换失败

坑一:路径写死 + 中文目录 + 反斜杠转义

现象:FileNotFoundError或者SyntaxWarning: invalid escape sequence。原因:原始代码里路径是形如"D:\某目录\某子目录\diabetes.data.csv"的字符串,Python 会把\某当转义序列处理,运气好只是警告,运气不好路径直接变形;再加上绝对路径绑定在某台机器上,换台电脑必挂。解决:全部改成os.path.join拼相对路径,配合os.path.abspath在报错时打印真实查找位置,一眼能看出是路径错还是文件真没放。

坑二:两套数据源被当成一套在讲

现象:按正文描述,模型输入是 CSV 里 8 个指标 + 1 个标签,做二分类;但可视化脚本加载的是库内置数据集,442 条样本、10 个特征、连续型目标值,两个数据集的样本数、特征数、任务类型全不一样。原因:资料在描述时把分类线和回归线的数据集混着写了,读者会以为图表画的就是分类用的那份数据,结果照着改代码时特征数对不上。解决:明确分开两条链路——分类用 CSV,回归用内置数据集,各自独立;如果要统一,把图表也改成基于 CSV 的标签与特征关系(比如按 BMI 分箱算阳性率画柱状图),而不是拿连续目标值做回归。这一条不搞清楚,后面所有"为什么特征数是 8 但有 10 张图"的疑问都无解。

5.2 数值与准确率:除零、口径不一致、结果不可复现

坑三:stdev分母为 0 导致ZeroDivisionError

现象:跑着跑着在stdev()里抛除零异常,或者准确率异常稳定在某个偏低值。原因:variance的分母是len(numbers) - 1,当某个类别在训练集里只剩一条样本时,分母是 0;另一种情况是该特征在该类别下所有取值相同,variance为 0,math.sqrt(0)没问题,但传到calculateProbability里math.pow(stdev, 2)做分母就炸了。解决:stdev里对len(numbers) < 2返回 0,calculateProbability里对stdev == 0单独走常数分支(前面的代码已经加了这个兜底)。

坑四:正文说 90/10,代码里是 0.67,且没有随机种子

现象:你按文档理解"训练集占九成",跑出来准确率和别人的差好几个点,而且自己每次跑都不一样。原因:__main__里splitRatio = 0.67,和正文描述的 90% 不一致;同时random.randrange没设种子,每次划分不同,测试集只有几十条,一条样本就是两三个百分点。解决:把splitRatio提到配置区并注释清楚,加上random.seed(42),报告结果时至少跑 20 个种子取均值,把"单次准确率"换成"均值±标准差"。

5.3 画图与依赖:reshape 报错、中文乱码、子图编号越界

坑五:一维数组喂给LinearRegression+ 子图编号从 0 开始

现象:ValueError: Expected 2D array, got 1D array instead,或者ValueError: num must be 1 <= num <= 10, not 0。原因:df.loc[:, col].values取出来是一维 Series 转的 ndarray,sklearn 要求二维特征矩阵;plt.subplot(5, 2, i)的第三个参数是 1 基的,enumerate默认从 0 开始。解决:reshape(-1, 1)做形状修正,subplot(5, 2, i + 1)做编号修正。顺带一个隐性问题:图上标题里带中文特征名时,matplotlib 默认字体缺字会显示成方框,加两行plt.rcParams["font.sans-serif"] = ["SimHei"]和plt.rcParams["axes.unicode_minus"] = False就正常了,后者是防止负号变成方块。

6. 二次开发:把课程代码改成能反复验证的工程

这份资料最大的价值是骨架完整,最大的问题是不可验证——改一个参数,你不知道结果是变好了还是运气好。我一般分三步改造。

第一步,把单文件拆成三层:data_loader.py只负责读文件、类型转换、拆分;nb_model.py只放统计量计算和预测;report.py负责跑实验、打印指标、落盘结果。拆完之后data_loader可以被任何模型复用,nb_model可以只靠一份构造出来的假数据做单元测试,不用每次跑全量数据。

第二步,补上先验和交叉验证。先验按训练集里各类样本占比估计,乘进calculateClassProbabilities的初始值;交叉验证用train_test_split+cross_val_score替代手写的单次拆分。同时用GaussianNB跑一遍做对照——这是检验手写实现有没有写错的最快办法,同一份数据同一种划分下,两者准确率应该几乎一致,差得远就说明你的实现里有逻辑错误。

import numpy as np from sklearn.naive_bayes import GaussianNB from sklearn.model_selection import cross_val_score, train_test_split # dataset 是 loadCsv() 的返回值,最后一位是标签 X = np.array([row[:-1] for row in dataset]) y = np.array([row[-1] for row in dataset]) X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) gnb = GaussianNB().fit(X_tr, y_tr) print("hold-out 准确率: %.2f%%" % (gnb.score(X_te, y_te) * 100)) print("5 折交叉验证: %.2f%%" % (cross_val_score(gnb, X, y, cv=5).mean() * 100))

stratify=y保证每折里正负样本比例与整体一致,这一点在糖尿病这种阳性占比不高的数据上很关键,不分层的话某一折可能只有很少的阳性样本,指标会跳得厉害。

第三步,加输出与可解释性。把每个测试样本的两个类别概率、预测值、真实值写进 CSV,跑完直接能筛出误判样本看它们的特征分布;再顺手算一个混淆矩阵,看是漏诊多还是误诊多——在医疗相关的场景里,这两个错误的代价完全不同,光看准确率会掩盖问题。

这一套下来,代码量翻了一倍,但每次改模型都有对照、有分布、有可复现的种子。从那以后我每次拿到这种"能跑但说不清"的作业级代码,都强制先把数据加载和随机性钉死,再谈算法改进——不然调参调到半夜,最后发现是划分变了。希望帮到你。

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