简介:一份源自个人学习项目的基于Python实现线性回归与聚类分析预测糖尿病的源码与文档汇总,面向计算机专业学生和数据分析学习者,适合用作课程大作业、毕业设计或入门实战,项目难度中等偏易,曾在导师指导下通过评审并获评98分。代码经严格调试,覆盖数据加载、缺失值处理、特征整理、模型训练与测试集评估等完整环节,并基于NumPy、Pandas、Scikit-learn等常用库给出R²、MSE等指标的计算方法,可本地直接运行。压缩包共5个文件,包含2个CSV数据文件、1个Python主程序、1个Markdown说明文档和1个Word版文档,整体大小约406KB,文件划分清晰,便于按模块阅读。目前已有65人学习使用。文档不仅解释了线性回归在连续值预测中的应用,还阐明了聚类分析如何在糖尿病数据中发现潜在分组模式,配合详细代码注释可帮助初学者快速理解从数据预处理到模型评估的完整构建流程。
1. 用Python做糖尿病预测:线性回归与聚类分析的源码流程先说清
用Python做糖尿病预测时,线性回归负责给出可解释的连续预测值,聚类分析负责发现数据里潜在的患者分群——两个算法叠在一起用,常常比单独跑任何一个都提分。同样一份包含血糖、血压、体质指数和血清指标的糖尿病历史数据,直接跑线性回归,R²可能只有0.4;把KMeans聚类结果转成哑变量拼进特征,同一个模型有机会冲到0.55以上。
这里说的“源代码及文档说明”,指的不是一段孤立的算法demo,而是从数据加载、清洗、特征工程、建模到评估的完整流程。它面向的是机器学习入门者,想照着源码把流程跑通;也面向医疗数据分析师,想从同一份数据里挖出患者亚组并解释给业务方听。下面按数据加载、清洗、线性回归、聚类分析到交叉验证的顺序展开,每步都能直接抄进自己的脚本。
2. 数据加载与清洗:从sklearn糖尿病数据集到可建模的特征矩阵
2.1 加载糖尿病数据集:pandas读入与缺失值处理
常见做法是先加载sklearn内置的糖尿病数据集,它包含442个样本和10个预测特征,目标值是疾病进展的量化指标。把原始数据装进pandas的DataFrame后,shape、head、describe、isnull四个动作按顺序过一遍,能在5分钟内确认数据规模、量纲和缺失情况。
import numpy as np import pandas as pd from sklearn.datasets import load_diabetes diabetes = load_diabetes() df = pd.DataFrame(diabetes.data, columns=diabetes.feature_names) df["target"] = diabetes.target print(df.shape) # (442, 11) print(df.head()) # 前5行,检查列名与量纲 print(df.describe()) # 均值、标准差、min/max print(df.isnull().sum()) # 内置数据无缺失,换CSV时重点看这里这段代码把特征矩阵和目标列拼成完整DataFrame。要留意的点是:diabetes.data已经是做过无量纲化的矩阵,每个特征均值接近0、方差接近1,所以describe的绝对值不能直接当作原始医学指标理解。如果换成医院导出的糖尿病病历CSV,字段名、单位、缺失比例都会变得不可控,isnull().sum()这一步必须保留,否则聚类算法在遇到NaN时通常直接报错,线性回归则会在某些实现里静默丢失整行。
如果缺失值比例不高,我一般先用df.dropna()删掉缺失行,简单且不引入额外假设;缺失比例超过10%,再考虑用列均值或中位数填补。填补逻辑要写进文档说明里,因为缺失机制本身也可能是业务信号,比如某家分院常年不测某个血清指标。数据源换成CSV后,建议在加载函数里增加一个列名映射表,把医院系统的英文缩写映射成模型里的统一名,避免后续每次都要改特征列。
2.2 特征标准化与训练测试划分:避免数据泄露的第一步
KMeans依赖欧氏距离,线性回归对量纲敏感,所以标准化是连接清洗与建模的必要环节。很多人图省事,先对整份DataFrame做StandardScaler,再切训练测试集,这一步就是后面评估数字虚高的根源。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df.drop(columns=["target"]) y = df["target"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)train_test_split按8:2比例划分样本,random_state=42只是习惯值,作用是让每次运行得到同一份训练测试划分,这样横向比较算法时,差异来自模型而不是数据划分。标准化的顺序必须是先fit_transform训练集,拿到训练集上的均值与标准差,再transform测试集;测试集的均值方差不参与计算。一旦用全量数据先fit再切分,就属于典型的测试集信息泄漏进训练流程,测试集不再是未知数据,验证出来的R²天然偏乐观。
提示:标准化必须拆成fit_transform和transform两步,测试集的均值方差不能参与计算。这是源码里最值得加注释的一行。
这份数据集只有442条样本,test_size=0.2意味着测试集只有88条,单次划分的结果波动不小。后面放到交叉验证那章,会看到用KFold能把这层随机性消掉。现在先把标准化的Pipeline结构建立起来:scaler负责任何时候都只接触训练数据,这是源码里最值得写清楚的一行注释。
2.3 相关性初探:哪些特征值得进入回归模型
import matplotlib.pyplot as plt corr = df.corr()["target"].sort_values(ascending=False) print(corr) plt.figure(figsize=(8, 8)) plt.imshow(df.corr(), cmap="coolwarm", vmin=-1, vmax=1) plt.colorbar() plt.xticks(range(len(df.columns)), df.columns, rotation=90) plt.yticks(range(len(df.columns)), df.columns) plt.tight_layout() plt.show()corr()默认计算皮尔逊相关系数,输出里bmi、s5、bp这几个特征与target的正相关最明显,绝对值在0.4到0.6之间。热力图的价值在于看特征之间是否高度共线,比如血脂相关的s1、s2经常出现0.8以上的相关度。共线性不会让线性回归无法运行,但会让回归系数对数据扰动极其敏感,换一批样本系数符号都可能反转。
相关性分析只做筛选参考,相关性不高的特征不急着删。sex和s6单独看相关度很弱,但它们与其他特征组合后可能有交互作用;删特征的决策应该交给回归加上正则或行业判断,而不是在清洗阶段靠一张热力图拍板。代码里我会把corr矩阵的计算放到单独的探索脚本里,与建模脚本分开,因为探索过程中的所有结论都不该反向影响训练数据的预处理规则。
2.4 源码结构怎么组织:文档说明先讲数据流向
标题既然带了“源代码及文档说明”,那源码就不应该是一坨几百行的脚本。我常见的组织方式是按数据流向分四个模块:load_data.py负责读数据与列映射,preprocess.py负责清洗与标准化,models.py放线性回归与聚类的训练函数,evaluate.py跑评估指标和画图。主入口main.py按顺序调用,配合一个README写明每一步的输入输出。
| 模块文件 | 职责 | 输入 → 输出 |
|---|---|---|
| load_data.py | 读原始数据与列映射 | CSV/内置数据 → DataFrame |
| preprocess.py | 清洗、标准化、切分 | DataFrame → 训练/测试矩阵 |
| models.py | 线性回归与KMeans训练 | 矩阵 → 模型对象 |
| evaluate.py | 指标、残差图、对比 | 模型+测试集 → 指标与图表 |
这样的好处是复现者能单步执行,改一个环节不需要牵扯其他代码。文档说明里最该写清楚的是四件事:原始数据长什么样、每一步输出什么形状、每个随机种子设在哪、评估指标怎么来的。如果你拿到的源码里面没有这些,按上面这个结构自己重写一遍,比在原有脚本里debug更快。
3. 线性回归建模与评估:fit_intercept、StandardScaler和三个回归指标
3.1 最小二乘原理与sklearn的LinearRegression接口
线性回归假设目标值y和特征x之间存在线性组合关系:y ≈ w0 + w1x1 + ... + wmxm。训练过程就是找一组权重w让残差平方和最小,这也是最小二乘这个名字的由来。sklearn把求解过程封装在LinearRegression里,默认走基于SVD的解法,样本量几千、特征几十这种规模下根本不用关心底层矩阵求逆的数值问题。
from sklearn.linear_model import LinearRegression model = LinearRegression(fit_intercept=True) model.fit(X_train_scaled, y_train) print("截距:", model.intercept_) print("系数:", model.coef_)fit_intercept=True表示让模型单独估计一个截距项,这也是默认值。只有当你确认数据已经中心化、并且理论上截距必然为0时,才改成False;否则把截距去掉,所有特征的系数都会被拉偏来凑那部分基线水平。因为前面用了StandardScaler,这时的coef_可以直接跨特征比较绝对值大小,某个特征系数绝对值越大,说明它对疾病进展指标的边际影响越强。
老代码里常见的normalize=True参数在新版本中被弃用。sklearn希望标准化的职责由StandardScaler或Pipeline单独承担,而不是藏在回归器内部。看到源码中出现normalize=True,直接用StandardScaler替代,数值结果不变,但代码不再依赖被移除的参数名。这个改动放进文档说明里,能帮复现者省掉一堆版本报错。
| 参数 | 常用取值 | 作用 |
|---|---|---|
| fit_intercept | True | 是否拟合截距项 |
| normalize | 已弃用 | 旧版是否标准化特征,新版用StandardScaler替代 |
| copy_X | True | 是否复制特征矩阵,避免原地修改 |
3.2 拟合后先看三个指标:RMSE、MAE、R²
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred = model.predict(X_test_scaled) rmse = mean_squared_error(y_test, y_pred, squared=False) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"RMSE={rmse:.2f} MAE={mae:.2f} R2={r2:.3f}")RMSE把均方误差开根号,单位与目标值一致,它对大误差的惩罚比对小误差重得多;MAE是所有绝对误差的平均,对极端样本更稳健;R²衡量模型解释了多少方差,越接近1越好。在这个数据集上,只用10个原始特征跑线性回归,R²大约在0.45到0.55之间,RMSE在55上下。这个数字不惊艳,但它就是后面所有改进动作的基准线。
三个指标要看组合而不是单个。如果R²高但RMSE也高,说明预测平均意义上过得去,对具体病患的误差仍然很大;如果MAE小但RMSE明显偏大,说明测试集里藏着几个被预测得很离谱的样本。把三个指标同时写进文档,复现者才能判断你说“模型不错”到底指的是哪一方面。
3.3 残差图:线性假设是否成立的直观证据
residuals = y_test - y_pred plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(y=0, color="gray", linestyle="--") plt.xlabel("Predicted Value") plt.ylabel("Residuals") plt.show()线性假设如果成立,残差应该随机散布在0刻度线两侧,没有喇叭形或弯曲弧线。这份数据上最常见的两种异常是:预测值越大,残差越向外张开,说明方差不齐;或者残差整体呈一条弧线,说明漏了非线性结构。无论哪一种,单靠R²都看不出来,残差图比R²更能暴露模型的短板。
这种残差形态是引入聚类的直接理由。全量样本塞进同一条回归直线,很难同时拟合不同人群;如果先按特征相似性把人群分开,再让模型在不同人群里拥有不同的截距甚至斜率,残差的弯曲就能被吸收掉。下一章就用KMeans来做这件事,不需要换掉线性回归,只增加几个稀疏特征列。
3.4 单变量回归与多变量回归的差异及系数解释
如果在源码里只保留bmi一个特征跑单变量线性回归,R²会明显低一截;逐步加入bp、s5之后能到0.45以上。这说明不同特征提供的预测信息是互补的。单变量的好处是系数解释简单,适合给非技术同事讲原始方向;多变量的价值是拟合精度更高,但系数之间会相互影响,个别系数的符号甚至可能跟单变量时相反。
真正要关注的是多变量回归里有没有掺进冗余特征。同一类血清指标放两个进去,会让共线性变高,系数解释不稳。遇到这种情况,我一般看方差膨胀因子VIF,超过10就删掉其中一个。这个筛选过程也要写进文档,因为同一个特征在不同人手里选进模型的结果很可能不一样,写清楚了别人才能复现。线性回归的核心价值从来不只是预测,而是把每个特征的贡献摊开给业务方看,所以系数的稳定性比单个R²的数字更重要。
4. 聚类分析嵌入预测流程:用KMeans找出患者分群并生成哑变量特征
4.1 为什么聚类能帮线性回归:数据异质性与分段建模
KMeans做的是无监督分组:在特征空间里把样本分成K簇,簇内的样本尽量靠近本簇中心,不同簇的中心尽量远离。把分群标签作为一个信息通道引入回归,相当于告诉线性回归不同人群的基线水平不同,模型可以为每个人群分别估计一个截距。这就是分段建模,只是分段边界由无监督算法自动找出来。
糖尿病进展指标受到遗传、生活习惯、并发症等多种因素影响,全量样本本质上来自分布不同的亚群。把所有亚群硬凑进一条回归直线,残差必然留下模式;聚类标签把亚群信息显式交出来,线性回归就能在同一框架内逼近分段效果。这项操作的业务价值在于,分出的簇通常对应临床上的不同病程阶段,医生拿到模型时能直接对应自己的经验分型。
4.2 肘部法与轮廓系数:K值不该拍脑袋定
KMeans必须先确定K,这是源码里最容易被写死的地方。常见做法是跑一个从1到10的循环,记录每个K下的簇内误差平方和SSE,也就是KMeans的inertia_属性。SSE随K增大单调下降,下降速率从某个拐点开始明显变缓,这个拐点就叫肘部,对应的K就是候选值。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse = [] sil_scores = [] for k in range(2, 11): km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X_train_scaled) sse.append(km.inertia_) sil_scores.append(silhouette_score(X_train_scaled, labels)) plt.plot(range(2, 11), sse, marker="o") plt.xlabel("Number of Clusters") plt.ylabel("SSE") plt.show() plt.plot(range(2, 11), sil_scores, marker="s") plt.xlabel("Number of Clusters") plt.ylabel("Silhouette Score") plt.show()KMeans在k=1时不会去划分簇,因此轮廓系数的循环从2开始。轮廓系数衡量簇内紧密度与簇间分离度,取值在-1到1之间,越高越好。SSE曲线不会每次都给一个锐利的拐点,经常是平滑下降;轮廓系数对噪声敏感,给的建议也可能偏高。我的取舍习惯是:两个指标都看,如果SSE肘部在3到5、轮廓系数峰值也在4附近,就优先选4;如果二者冲突,以业务解释为准。
| 参数 | 常用取值 | 作用 |
|---|---|---|
| n_clusters | 2-8 | 簇数量,通过肘部法加轮廓系数确定 |
| n_init | 10 | 用不同初始中心跑的次数,取SSE最小的一次 |
| max_iter | 300 | 单次运行最大迭代次数 |
| random_state | None | 随机种子,固定后结果可复现 |
n_init=10是近几个sklearn版本里普遍推荐的做法,意味着算法会尝试10组不同的初始中心再挑结果最好的那组,这是对抗局部最优的廉价手段。如果数据有几万条,max_iter=300通常够用;如果聚类不收敛或者inertia明显异常,就把它调大到500再试。
4.3 把聚类标签编码成哑变量并回灌回归
K确定后,在训练集上拟合出聚类中心,再把测试集映射到最近的中心。这里有个原则必须写进文档说明:聚类中心只能来自训练集,测试集从未参与聚类拟合,否则又变成数据泄露。
K = 4 km = KMeans(n_clusters=K, n_init=10, random_state=42) train_cluster = km.fit_predict(X_train_scaled) test_cluster = km.predict(X_test_scaled) from sklearn.preprocessing import OneHotEncoder enc = OneHotEncoder(categories="auto", drop="first") train_cluster_oh = enc.fit_transform(train_cluster.reshape(-1, 1)).toarray() test_cluster_oh = enc.transform(test_cluster.reshape(-1, 1)).toarray() X_train_ext = np.hstack([X_train_scaled, train_cluster_oh]) X_test_ext = np.hstack([X_test_scaled, test_cluster_oh])这里用OneHotEncoder而不是直接把聚类标签0、1、2、3当数值特征,原因是聚类标签只是类别代号,数字之间没有大小关系;直接喂给回归,模型会多学一个毫无意义的“标签越大疾病越重”的斜率。drop="first"去掉第一列哑变量,避免与线性回归的截距项产生完全共线。
扩展完特征后再训练一次线性回归:
model_ext = LinearRegression(fit_intercept=True) model_ext.fit(X_train_ext, y_train) y_pred_ext = model_ext.predict(X_test_ext) r2_ext = r2_score(y_test, y_pred_ext) print(f"R2 with cluster features: {r2_ext:.3f}")在这个流程里,加了聚类哑变量后R²通常比纯特征回归高3到8个百分点。提升幅度不会夸张,但只增加了四列0/1特征,投入产出比很划算。如果K选得偏大,比如超过6,提升会明显减弱,因为每个簇里的样本数变少,聚类结果的稳定性和可解释性同步下降。这也是为什么K的选择不能只看SSE曲线,还要看每个簇实际包含多少样本。
4.4 只加分群还是按簇单独建模:复杂度和可解释性的权衡
加了聚类哑变量,本质上是允许不同簇有不同的基线截距,但所有簇共享同一套特征系数。更进一步的做法是K个簇各训练一个线性回归,每个簇内的特征系数完全独立。后者的拟合精度通常会更高,因为不用再受“全量样本共用一个斜率”的限制。但它也有明显代价:每个簇的样本量骤减,系数方差变大,测试集上的泛化反而可能变差。
我在实际项目中会先试加哑变量,看R²提升是否明显;再决定是否按簇拆模型。442条样本、K=4的情况下,一个簇可能只有八九十条,拆开训练每个模型只有六七十条样本,很容易过拟合。样本量过万时按簇拆模型才是划算的。这也是文档说明里应该写清楚的一条决策边界:代码能跑通不等于在当前规模下适用。
5. 避坑清单:糖尿病预测中让线性回归与聚类分析翻车的5个细节
5.1 复现性翻车:同样的代码两次跑结果不同
现象是:拿到同一份源码,第一次跑R²是0.52,第二次变成0.49,而且每次都不一样。原因通常是KMeans或其他带随机性的步骤没有固定随机种子。KMeans默认初始中心是随机从样本里挑的,每次fit得到的簇划分有细微差异;这份数据集只有442条,这种差异会直接传到最后的R²上。
解决方法是给KMeans设置random_state,并把n_init稍微调大。n_init=10意味着算法用10组不同初始中心各跑一遍,选SSE最小的一组作为结果。固定random_state之后,无论跑多少遍,簇划分和最终R²都一致。线性回归本身没有随机性,所以问题基本出在聚类和train_test_split上,这两个地方的random_state都设好就能复现。
5.2 数据泄露:标准化顺序错了,测试集不再是未知数据
现象是验证集上的R²比预期要高,换一批数据立刻打回原形。原因多半是先用StandardScaler对全量数据做了fit,再切分训练测试集。scaler在fit时已经用到了全部样本的均值与标准差,测试集的分布信息被提前暴露,模型在测试集上的表现是虚高的。
解决方法是严格按训练集fit_transform、测试集只transform的顺序来。最简单可靠的办法是把scaler和回归器一起包进Pipeline,Pipeline的fit只接触训练数据,predict时才处理测试集,从结构上杜绝数据泄露。这个坑在文档复现时特别隐蔽,因为它只让指标虚高几个百分点,肉眼很难察觉,一旦上线就露馅。
5.3 聚类标签直接当数值特征用,回归系数解读全乱
现象是:加了聚类特征后R²确实涨了,但模型报告里出现“聚类标签每增加1,疾病进展指标上升12”这种没有临床意义的结论。原因是把KMeans输出的0、1、2、3直接当成一列数值特征喂给了线性回归。聚类标签只表示簇的编号,没有大小和顺序含义,线性回归却把它当连续刻度来拟合。
# 错误做法:聚类标签当数值特征,系数会被解释为“等级每升1级指标变化” X_wrong = np.hstack([X_train_scaled, train_cluster.reshape(-1, 1)])解决办法是改用OneHotEncoder做哑变量编码,并在生成特征时将第一列丢弃以避免共线性。如果源码里用的是pd.get_dummies(cluster_labels),效果是一样的,但默认不会丢第一列,需要手动指定。文档里要写清楚模型最终的特征列表,复现者看到第四列叫cluster_1、cluster_2、cluster_3而不是cluster,就知道这是哑变量而不是数值变量。
5.4 残差U形和K值玄学:两个容易被忽略的模型假设
现象是残差图在预测值较高和较低的两端都是正残差,中间是负残差,呈U形弧线。这说明线性回归漏掉了明显的非线性关系,可能是BMI与目标之间存在阈值效应,也可能是不同人群组内斜率不同。只把聚类哑变量加进去可以缓解,但不根治。
要彻底解决,可以用PolynomialFeatures生成二次项和交叉项,再训练线性回归;或者直接改用能表达非线性的模型。K值选择则是另一处玄学,SSE肘部法给出一个候选区间,轮廓系数给出另一个候选区间,二者经常不重合。统计上最优不一定是业务上最好,医院数据里K=4对应凝血、炎症、代谢、高风险四类人,比K=6更容易被临床接受。我的习惯是统计指标做参考,业务可解释性做最终决定。
5.5 目标列是连续型,评估却拿准确率说事
现象是文档或代码里用accuracy_score来评估线性回归的预测结果,还给出了一个看似不错的准确率。原因是有人把连续预测值按某个阈值转成了二分类标签,比如大于多少就算糖尿病进展。问题在于这个阈值的选取会完全影响准确率,而阈值本身没有在文档里说明,复现者根本无从判断这个准确率合理不合理。
回归任务的评估标准就是RMSE、MAE、R²这三个。如果真的有医学标准把连续指标二值化,那也应该用分类的评估体系,同时把阈值和决策依据写清楚。把回归当回归评,把分类当分类评,这条写进文档说明,能避免团队内部扯皮。
6. 验证与进阶:交叉验证、多项式特征与聚类特征的组合技巧
要验证前面那套带聚类特征的流程稳不稳,先用交叉验证替换单次切分。KFold把数据切成5份,轮流做训练与验证,得到5个R²再取均值,这样能消掉单次train_test_split带来的幸运成分。聚类本身也要在每一折内重新拟合,如果用整体数据先聚类再交叉验证,标签信息又会泄露到验证集。
from sklearn.model_selection import KFold, cross_val_score from sklearn.pipeline import make_pipeline kfold = KFold(n_splits=5, shuffle=True, random_state=42) pipeline = make_pipeline(StandardScaler(), LinearRegression()) scores = cross_val_score(pipeline, X, y, cv=kfold, scoring="r2") print(f"CV R2: {scores.mean():.3f} ± {scores.std():.3f}")这段代码给出R²的均值和标准差,标准差越小说明模型对数据划分越不敏感。做完交叉验证,再回头看单次切分的结果,如果单次R²落在均值加减一个标准差之内,说明之前的结论可信;落在范围之外,说明随机划分主导了结论,需要回头检查随机种子和样本量。另一个实用的进阶是PolynomialFeatures生成二次项后与聚类哑变量一起进模型,通常能再提几个点的R²,代价是特征数量膨胀,需要用VIF或正则去控。
我自己在这个项目上踩过的血泪经验是迷信网格搜索给出的最优K和最高R²,却忽略了每个簇在临床上的含义。后来把聚类分型和医生手写的病历分型对上账,模型才真正被接受。聚类特征的每一步都要能讲出业务含义,模型提升的幅度反而不是第一优先级。希望这些步骤和避坑经验能帮你在自己的糖尿病数据上少走弯路。
本文还有配套的精品资源,点击获取