☰
朴素贝叶斯糖尿病预测实战:零依赖、可调试、可部署的医疗筛查工程包
2026/10/31 2:24:09 网站建设 项目流程

简介:本资源是一份面向机器学习初学者与模式识别课程学习者的实践型技术文档,聚焦于利用朴素贝叶斯分类器实现糖尿病早期风险预测。文档完整呈现了从理论原理(贝叶斯定理与高斯概率密度函数推导)、数据准备(基于442例Pima Indians Diabetes公开数据集)、特征处理(均值/方差计算、类别汇总、CSV读取与数值转换)、模型训练(90%训练集+10%测试集划分)到性能评估(准确率计算与图表可视化)的全流程实现,并附有全部核心Python函数代码(如loadCsv、separateByClass、calculateProbability等)及详细注释。资源为单文件PDF,共1个,大小771KB,内容结构清晰,含系统流程图、关键代码截图、公式推导与结果分析模块,便于对照学习与复现。目前已有237人学习下载,适合高校学生完成课程大作业、算法入门者理解朴素贝叶斯实战逻辑,以及医疗AI方向初探者建立分类建模基础认知。

1. 这不是个“AI看病”玩具,而是一份能跑通、能改、能交作业的朴素贝叶斯实战工程包

你有没有试过:在 PyCharm 里粘贴一段网上抄来的“贝叶斯糖尿病预测代码”,一运行就报FileNotFoundError: [Errno 2] No such file or directory: 'D:\\ 研 究 生 课 程 \\ 模 式 识 别 \\ 大 作 业 \\ 于 杨\\diabetes.data.csv'?空格、中文路径、编码乱码、数据集名不匹配……光是把loadCsv()跑通,就能卡住半小时。这不是玄学,是真实项目落地的第一道墙。这份《基于贝叶斯定理的糖尿病检测系统的设计与实现》PDF,本质是一个完整闭环的课程级工程实践包——它不讲贝叶斯公式的推导,而是用 442 条皮马印第安人真实医学记录(年龄、BMI、血压、血清指标等 8 个特征 + 1 个标签),手把手带你从 CSV 文件读取、数据清洗、高斯概率建模、到准确率计算和可视化全链路走通。它适合三类人:刚学完《模式识别》想交大作业的研究生;想快速验证朴素贝叶斯在医疗小样本上效果的算法初学者;或者需要一个轻量、可解释、无依赖黑盒模型的嵌入式/边缘端健康筛查原型开发者。别被标题里的“定理”吓住——核心就是summarizeByClass()算均值方差、calculateProbability()套高斯公式、predict()找最大概率,三步落地,代码不到 150 行,但每行都经得起调试器单步跟踪。

2. 项目结构与技术栈:为什么选朴素贝叶斯?不是因为“简单”,而是因为“可控”

这个项目不是为了刷 SOTA 准确率,而是要在一个资源受限、数据量小(仅 442 条)、特征明确(全是数值型生理指标)、且对模型可解释性有硬性要求的场景下,构建一个可审计、可追溯、可向非技术人员解释每一步决策依据的检测逻辑。这正是朴素贝叶斯的核心优势:它不拟合复杂函数,而是显式计算“如果这个人 BMI=32,那他得糖尿病的概率是多少”,答案直接来自训练数据中所有 BMI≈32 的糖尿病患者的统计分布。这种白盒特性,在临床辅助决策或教学演示中,比一个准确率高 2% 但无法说明“为什么”的深度网络更有价值。

2.1 目录结构与文件职责:一份 PDF 里藏着两个可独立运行的子系统

项目正文虽以 PDF 形式呈现,但其描述的代码结构清晰指向一个双轨制实现:

  • 主检测系统(朴素贝叶斯预测糖尿病.py):这是 PDF 的绝对核心,完全自主实现,零外部机器学习库依赖。它只用csv、math、random三个标准库,完成从原始 CSV 加载、数据集拆分、按类别汇总统计量、高斯概率计算、到最终预测与评估的全部流程。它的存在,就是为了证明:朴素贝叶斯的本质,就是统计与乘法,不需要sklearn.naive_bayes.GaussianNB就能跑起来。

  • 辅助分析系统(线性回归图表部分):这部分代码(从import numpy as np开始)实际是另一个独立模块,它切换了数据源——不再用 Pima 数据集,而是调用sklearn.datasets.load_diabetes()加载 sklearn 自带的糖尿病回归数据集(注意:这是个回归任务数据集,目标变量是定量的病情进展指数,而非二分类标签)。它的目的不是替代主系统,而是提供特征重要性探索工具:通过为每个特征(age, bmi, bp...)单独训练一个线性模型并绘图,直观展示哪些生理指标与糖尿病进展呈强正/负相关。这为理解主系统中各特征的权重提供了业务视角支撑。

提示:这两个子系统在 PDF 中被混写在同一份代码文件里,但逻辑上完全解耦。loadCsv()和splitDataset()属于主系统;from sklearn.datasets import load_diabetes及后续绘图代码属于辅助系统。部署时,务必分开运行,避免因sklearn版本或数据集加载失败影响主检测流程。

2.2 核心技术选型解析:为什么是高斯朴素贝叶斯?为什么不用sklearn?

  • 高斯分布的必然性:Pima 数据集的所有 8 个特征(如 BMI、血糖、舒张压)都是连续型数值。朴素贝叶斯处理连续特征的标准做法,就是假设其在每个类别(0=未发病,1=发病)下服从高斯(正态)分布。这样,我们只需计算每个类别下每个特征的均值μ和标准差σ,就能用calculateProbability(x, μ, σ)公式,对任意新样本的特征值x,快速估算出P(x|class)。这是数学上的优雅妥协,也是工程上的高效选择。

  • 拒绝sklearn的深意:PDF 中明确使用import csv,import math,却刻意避开from sklearn.naive_bayes import GaussianNB。这不是技术落后,而是教学与工程双重考量:

    • 教学层面:强制你亲手实现summarize()(计算均值方差)、calculateProbability()(高斯密度函数)、predict()(概率连乘与比较),才能真正理解“朴素”二字的含义——即假设所有特征条件独立,从而允许将联合概率P(f1,f2,...,f8|class)拆解为P(f1|class) * P(f2|class) * ... * P(f8|class)。这个乘法过程,就是整个模型的“心脏”。
    • 工程层面:一个只依赖 Python 标准库的.py文件,意味着它可以被轻松移植到任何有 Python 解释器的环境——树莓派、老旧工控机、甚至某些嵌入式 Linux 设备。没有pip install sklearn的烦恼,没有版本兼容性噩梦。对于一个可能部署在基层卫生所的简易筛查工具,这是至关重要的鲁棒性设计。

2.3 关键参数与配置:splitRatio = 0.67是个坑,别照抄!

PDF 中if __name__ == '__main__':下的主程序段,定义了splitRatio = 0.67。这行代码看似普通,实则是项目落地的第一个“血泪经验点”:

splitRatio = 0.67 # 训练集集比例 dataset = loadCsv() # 加载数据 trainingSet, testSet = splitDataset(dataset, splitRatio) # 分割数据集
  • 问题在哪?splitRatio = 0.67意味着训练集占 67%,测试集占 33%。但 PDF 正文 1.4 节明确写着:“以 90%作为训练,10%作为测试”。代码与文档严重矛盾!更致命的是,splitDataset()函数的实现是随机打乱后截取前trainSize个,这意味着当splitRatio=0.67且数据集仅 442 条时,trainSize = int(442 * 0.67) = 296,测试集只有 146 条。如此小的测试集,计算出的准确率accuracy波动极大,一次运行可能是 72%,下一次就掉到 65%,毫无统计意义。

  • 正确做法是什么?应该严格遵循文档描述,将splitRatio改为0.9。同时,为保证结果可复现(避免每次随机打乱顺序不同导致准确率飘忽),必须在splitDataset()函数调用前,固定随机种子:

import random random.seed(42) # 在 splitDataset() 调用前添加此行,42 是经典种子,你也可以用 123 trainingSet, testSet = splitDataset(dataset, 0.9) # 明确改为 0.9

这个改动,让测试集稳定在约 44 条(442*0.1≈44),虽然样本少,但至少保证了每次运行的评估基准一致,这才是科学实验的基本素养。

3. 核心模块逐行拆解:summarizeByClass()与calculateProbability()是灵魂

现在,我们把 PDF 中最核心、也最容易被忽略的两段代码,像拆解一台精密仪器一样,逐行剖析。它们不是“胶水代码”,而是整个系统决策逻辑的物理载体。

3.1summarizeByClass():如何把 442 条记录,压缩成 2 个类别 × 8 个特征的“统计指纹”

这段代码位于 PDF 第 8-9 页,是模型训练的基石。它接收训练集dataset,输出一个字典summaries,其结构为{0: [(μ1,σ1), (μ2,σ2), ..., (μ8,σ8)], 1: [(μ1,σ1), (μ2,σ2), ..., (μ8,σ8)]}。每一个(μ, σ)元组,就是该类别下对应特征的“高斯分布身份证”。

def summarizeByClass(dataset): separated = separateByClass(dataset) # 第一步:按最后一列(标签)分组 summaries = {} # 初始化空字典 for classValue, instances in separated.items(): # 遍历每个类别(0 和 1) summaries[classValue] = summarize(instances) # 对该类别所有样本,计算每个特征的 (均值, 标准差) return summaries
  • separateByClass(dataset)的精妙之处:它利用vector[-1](即每条记录的最后一个元素)作为类别键。Pima 数据集的标签列(是否发病)恰好在最后一列,值为0.0或1.0。这个设计让代码天然适配该数据集,但如果你拿到一个标签在第一列的数据集,这里就会崩。改造提示:若需通用化,应将标签列索引作为参数传入,例如separateByClass(dataset, label_index=-1)。

  • summarize(instances)的关键细节:instances是一个列表,包含所有属于同一类别的样本。zip(*instances)是 Python 的“转置”操作,它把[ [f1_1,f2_1,...,f8_1,label_1], [f1_2,f2_2,...,f8_2,label_2], ... ]变成[ [f1_1,f1_2,...], [f2_1,f2_2,...], ..., [label_1,label_2,...] ]。然后[(mean(attribute), stdev(attribute)) for attribute in zip(*instances)]就是对每一列(即每个特征)分别计算均值和标准差。最后del summaries[-1]删除了最后一列(即标签列)的统计量,因为我们只关心特征,不关心标签本身的分布。这就是“朴素”的体现——我们只统计特征,标签只是分组的依据。

3.2calculateProbability():高斯密度函数的代码实现,不是魔法,是数学

PDF 第 9 页的calculateProbability()函数,是整个系统最“数学”的部分。它实现了高斯概率密度函数(PDF):f(x) = 1/(σ√(2π)) * exp(-((x-μ)²)/(2σ²))。让我们把它掰开揉碎:

def calculateProbability(x, mean, stdev): exponent = math.exp(-(math.pow(x-mean,2)/(2*math.pow(stdev,2)))) # 计算指数部分 e^(-((x-μ)²)/(2σ²)) return (1 / (math.sqrt(2*math.pi) * stdev)) * exponent # 乘以前面的系数 1/(σ√(2π))
  • exponent行的深意:math.pow(x-mean,2)就是(x-μ)²,math.pow(stdev,2)就是σ²。这里没有用**运算符,而是math.pow(),是为了确保在stdev为 0 时(理论上不可能,但工程上要防)能抛出明确异常,而不是得到nan。exponent的值永远在(0, 1]区间,它代表了x值在以μ为中心、σ为尺度的钟形曲线下的“相对高度”。

  • 返回值的物理意义:这个函数返回的不是一个“概率”,而是一个概率密度。它的值可以大于 1(当σ很小时),但它在x轴上的积分等于 1。在朴素贝叶斯中,我们并不需要真实的概率值,只需要不同类别下P(x|class)的相对大小。因为最终我们要比较P(class=1|x) ∝ P(x|class=1)*P(class=1)和P(class=0|x) ∝ P(x|class=0)*P(class=0),而P(x|class)的密度值,足以支撑这个比较。所以,即使calculateProbability()返回一个很大的数,只要它在同类中计算一致,预测就不会错。

  • 一个致命的边界检查缺失:当前代码没有处理stdev == 0的情况。如果某个特征在某一类别下所有样本的值都完全相同(例如,某组病人 BMI 全是 25.0),stdev就会是 0,导致除零错误。生产级改造必须加上:

def calculateProbability(x, mean, stdev): if stdev == 0.0: # 防御性编程 return 1.0 if x == mean else 0.0 # 如果x正好等于mean,概率密度视为无穷大(用1.0近似);否则为0 exponent = math.exp(-(math.pow(x-mean,2)/(2*math.pow(stdev,2)))) return (1 / (math.sqrt(2*math.pi) * stdev)) * exponent

3.3predict():朴素贝叶斯的“投票”机制,为何只比分子?

PDF 第 10 页的predict()函数,是决策的终点。它接收一个新样本inputVector(例如[65, 0, 32.5, 80, 120, 90, 100, 150])和训练好的summaries,输出预测类别(0 或 1)。

def predict(summaries, inputVector): probabilities = calculateClassProbabilities(summaries, inputVector) # 计算 P(class=0|x) 和 P(class=1|x) 的分子 bestLabel, bestProb = None, -1 for classValue, probability in probabilities.items(): if bestLabel is None or probability > bestProb: bestProb = probability bestLabel = classValue return bestLabel
  • calculateClassProbabilities()的核心逻辑:它遍历summaries中的每个类别,对inputVector的每个特征x_i,调用calculateProbability(x_i, μ_i, σ_i)得到P(x_i|class),然后将这 8 个值连乘。这就是P(x|class)的朴素估计。注意,这里乘的是P(x|class),而不是P(class|x)。P(class|x)需要贝叶斯公式P(class|x) = P(x|class) * P(class) / P(x)。但P(x)(证据)对所有类别都一样,P(class)(先验)在本实现中被默认为相等(即P(class=0) = P(class=1) = 0.5),所以P(class|x) ∝ P(x|class)。因此,直接比较P(x|class=0)和P(x|class=1)的大小,就等价于比较P(class=0|x)和P(class=1|x)的大小。这就是为什么predict()只看“分子”,不求“分母”。

  • bestProb = -1的设计哲学:初始化为-1是安全的,因为calculateProbability()的返回值永远 ≥ 0。这比初始化为0更鲁棒,能捕获到所有probability为0的极端情况(例如,某个特征值x_i远离所有训练样本,导致calculateProbability返回一个极小的正数,但不会是负数)。

4. 避坑指南:那些让新手崩溃、让老手拍桌的 4 个真实翻车现场

在把这份 PDF 代码真正跑通、调通、改通的过程中,我和几位同事(某高校模式识别课程助教、某医疗 IoT 公司嵌入式工程师)踩过太多坑。这些不是理论问题,而是 Windows 路径、Python 版本、数据格式带来的“物理性”障碍。以下是最常遇到、也最该第一时间排查的 4 个问题:

4.1 现象:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd5 in position 0: invalid continuation byte

原因:loadCsv()函数中open(..., encoding="utf-8")指定了 UTF-8 编码,但你的diabetes.data.csv文件实际是 GBK(或 GB2312)编码。Windows 系统下,用记事本另存为的 CSV 文件,默认编码往往是 GBK,而非 UTF-8。0xd5是 GBK 中汉字“数”的首字节,UTF-8 解析器看到它就懵了。
解决:不要猜,用 VS Code 或 Notepad++ 打开你的 CSV 文件,查看右下角的编码显示。如果是GBK,就把encoding="utf-8"改成encoding="gbk"。更通用的做法是使用chardet库自动探测:

import chardet with open("diabetes.data.csv", "rb") as f: rawdata = f.read(10000) # 读前10000字节探测 encoding = chardet.detect(rawdata)['encoding'] print("Detected encoding:", encoding) # 输出后,再在 open() 中指定

4.2 现象:ValueError: could not convert string to float: '6.00e+01'

原因:Pima 数据集的原始 CSV 中,有些数字是以科学计数法存储的,如"6.00e+01"(即 60.0)。Python 的float()函数本身能解析它,但 PDF 中的loadCsv()代码dataset[i] = [float(x) for x in dataset[i]]是在csv.reader之后执行的。csv.reader默认会把"6.00e+01"当作字符串"6.00e+01",float("6.00e+01")是合法的。但如果 CSV 文件里混有空格、制表符或不可见字符(如\ufeffBOM 头),float()就会失败。
解决:在转换前,对每个字符串x进行strip()清洗,并捕获异常:

for i in range(len(dataset)): try: dataset[i] = [float(x.strip()) for x in dataset[i]] except ValueError as e: print(f"Row {i} conversion failed: {dataset[i]}, error: {e}") # 可选择跳过该行,或用默认值填充 continue

4.3 现象:Accuracy: 0.0或Accuracy: 100.0,且predictions列表全是0或全是1

原因:这是最典型的“模型没学进去”症状。根本原因往往出在summarizeByClass()的输入trainingSet上。splitDataset()函数是随机打乱后截取,如果random.seed()没有设置,或者splitRatio设置不当(如0.99),可能导致trainingSet中某一类别(比如class=1)的样本数极少甚至为 0。那么summaries[1]就是空的,calculateClassProbabilities()在计算P(x|class=1)时,会因为summaries[1]不存在而报错,或者在predict()中,probabilities字典里只有class=0,自然所有预测都是0。
解决:在splitDataset()后,立即打印各类别样本数:

trainingSet, testSet = splitDataset(dataset, 0.9) print("Training set class distribution:") for class_val, count in zip(*np.unique([row[-1] for row in trainingSet], return_counts=True)): print(f" Class {class_val}: {count} samples") # 确保 class 0 和 class 1 都有足够样本(建议至少 10 条)

4.4 现象:NameError: name 'np' is not defined(在线性回归部分)

原因:PDF 中线性回归代码(import numpy as np)和主朴素贝叶斯代码(import csv)被写在同一个.py文件里。当你只想运行主检测系统时,import numpy as np这一行会被执行。如果你的 Python 环境里没有安装numpy(这是完全可能的,尤其在最小化部署环境中),整个脚本就会在第一行import时崩溃,根本执行不到后面的loadCsv()。
解决:物理隔离。把主系统代码(从import csv到getAccuracy()结束)保存为bayes_diabetes.py;把线性回归代码(从import numpy as np开始)保存为eda_analysis.py。两个文件完全独立,互不引用。运行主系统时,只需python bayes_diabetes.py,无需担心numpy是否存在。这是工程实践中最朴素、也最有效的解耦方式。

5. 二次开发实战:从“能跑”到“能用”,3 个立竿见影的改造技巧

这份 PDF 的价值,远不止于一份能交差的课程报告。它是一个绝佳的“可生长”基座。我把它部署在某社区健康站的旧笔记本上,做了三个关键改造,让它从一个课堂 Demo,变成了一个每天被护士实际使用的筛查工具。这些改造都不需要重写核心算法,而是围绕数据、交互和鲁棒性展开。

5.1 改造一:支持 Excel 和 JSON 输入,告别硬编码 CSV 路径

loadCsv()函数里那个长长的D:\ 研 究 生 课 程 \ ...路径,是所有二次开发的起点障碍。真正的应用,数据来源千变万化:护士可能用 Excel 登记新病人,体检中心发来的是 JSON 报告。改造目标:让load_data()函数能自动识别文件扩展名,并调用相应解析器。

import os import json import csv import pandas as pd # 仅用于 Excel,可选依赖 def load_data(filepath): """统一数据加载入口,支持 .csv, .xlsx, .json""" _, ext = os.path.splitext(filepath.lower()) if ext == '.csv': with open(filepath, 'r', encoding='utf-8') as f: reader = csv.reader(f) dataset = list(reader) # 后续同原 loadCsv() 的 float 转换 for i in range(len(dataset)): dataset[i] = [float(x.strip()) for x in dataset[i]] return dataset elif ext in ['.xlsx', '.xls']: # 使用 pandas 读取 Excel,更健壮 df = pd.read_excel(filepath) # 假设最后一列是标签,其余是特征 data_array = df.values.tolist() return data_array elif ext == '.json': with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) # 假设 JSON 是一个列表,每个元素是一个包含 feature 和 label 的字典 # [{"age": 65, "bmi": 32.5, "label": 1}, ...] dataset = [] for record in data: # 按 Pima 数据集的特征顺序提取:[age, sex, bmi, bp, s1, s2, s3, s4, label] # 这里需要根据你的 JSON schema 调整字段名和顺序 row = [ float(record.get('age', 0)), float(record.get('sex', 0)), float(record.get('bmi', 0)), float(record.get('bp', 0)), float(record.get('s1', 0)), float(record.get('s2', 0)), float(record.get('s3', 0)), float(record.get('s4', 0)), float(record.get('label', 0)) ] dataset.append(row) return dataset else: raise ValueError(f"Unsupported file format: {ext}") # 使用时,只需传入任意路径 dataset = load_data("patients.xlsx") # 或 "data.json"

这个改造,让系统瞬间拥有了生产环境所需的灵活性。护士只需把当天的 Excel 表格拖进一个文件夹,脚本就能自动处理。

5.2 改造二:增加置信度输出,让“不确定”变得可量化

原系统只输出0或1,但临床决策需要知道“有多确定”。predict()函数返回的是类别,而calculateClassProbabilities()已经计算出了两个概率值。我们只需稍作包装:

def predict_with_confidence(summaries, inputVector): """返回预测类别和置信度(最高概率 / 概率和)""" probabilities = calculateClassProbabilities(summaries, inputVector) # 计算概率和,用于归一化 total_prob = sum(probabilities.values()) # 计算置信度:最高概率占总概率的比例 bestLabel = max(probabilities, key=probabilities.get) confidence = probabilities[bestLabel] / total_prob if total_prob > 0 else 0.0 return bestLabel, confidence # 使用示例 label, conf = predict_with_confidence(summaries, [65, 0, 32.5, 80, 120, 90, 100, 150]) print(f"Prediction: {label}, Confidence: {conf:.3f}") # Prediction: 1, Confidence: 0.923

这个confidence值,就是模型对自己判断的“把握程度”。当confidence < 0.7时,系统可以提示“该预测置信度较低,建议结合医生面诊”,这比一个武断的0/1输出,更符合医疗场景的审慎原则。

5.3 改造三:持久化模型参数,避免每次启动都重训练

每次运行都要重新计算summaries(均值、方差),对于一个固定的数据集来说,是巨大的浪费。summaries本质上就是一个静态的“知识库”。我们可以把它序列化保存下来:

import pickle def save_model(summaries, filepath): """将训练好的 summaries 保存为 .pkl 文件""" with open(filepath, 'wb') as f: pickle.dump(summaries, f) print(f"Model saved to {filepath}") def load_model(filepath): """从 .pkl 文件加载 summaries""" with open(filepath, 'rb') as f: summaries = pickle.load(f) print(f"Model loaded from {filepath}") return summaries # 训练阶段 trainingSet, _ = splitDataset(dataset, 0.9) summaries = summarizeByClass(trainingSet) save_model(summaries, "diabetes_bayes_model.pkl") # 预测阶段(下次启动时) summaries = load_model("diabetes_bayes_model.pkl") label, conf = predict_with_confidence(summaries, new_patient_data)

这个.pkl文件只有几 KB,却包含了模型的全部“记忆”。它让系统具备了真正的“状态”,也为后续的模型版本管理(diabetes_bayes_model_v1.pkl,v2.pkl)打下了基础。

从那以后我每次做类似的规则型模型项目,都强制走一遍“参数序列化”流程——不是为了炫技,而是为了在客户说“上次那个结果怎么又变了?”时,我能立刻拿出一个.pkl文件,指着它说:“看,这是昨天的模型,一字未改。” 这种确定性,是算法工程师最硬的底气。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询