机器学习-KNN 心脏病预测案例详解 KNN 心脏病预测案例详解本文档详细讲解ml_tutorial/ch03_knn/4_heart_disease.py涵盖数据提取、缺失值处理、特征分类、独热编码、标准化以及网格搜索调参的完整流程。一、整体流程加载数据 → 清洗缺失值 → 划分特征/标签 → 训练集/测试集切分 → 特征工程(标准化 独热编码 透传) → 网格搜索训练 → 评估二、数据集介绍数据文件ml_tutorial/data/heart_disease.csv共 14 列最后一列是否患有心脏病为标签其余 13 列为特征。列名含义类型年龄患者年龄数值型性别1男0女二元胸痛类型4 种类型(0/1/2/3)类别型静息血压入院时血压数值型胆固醇血清胆固醇数值型空腹血糖120mg/dl 为 1二元静息心电图结果心电图分类类别型最大心率达到的最大心率数值型运动性心绞痛1有0无二元运动后的 ST 下降ST 段下降值数值型峰值 ST 段的斜率斜率分类类别型主血管数量0~3数值型地中海贫血缺陷类型分类类别型是否患有心脏病1患病0健康标签标签三、数据提取与清洗# 1. 加载数据集heart_disease_datapd.read_csv(../data/heart_disease.csv)# 处理缺失值直接删除含空值的行heart_disease_data.dropna(inplaceTrue)heart_disease_data.info()# 查看每列的非空数量和数据类型print(heart_disease_data.head())# 查看前 5 行pd.read_csv把 CSV 读为 DataFrame。dropna(inplaceTrue)删除任何含缺失值的行inplaceTrue表示在原对象上修改。info()用于确认清洗后是否还有缺失值以及各列的数据类型是否符合预期。四、划分特征与标签# 2. 数据集划分Xheart_disease_data.drop(是否患有心脏病,axis1)# 特征删除标签列后剩余全部yheart_disease_data[是否患有心脏病]# 标签单独取出目标列X_train,X_test,y_train,y_testtrain_test_split(X,y,test_size0.3,random_state42)X为特征矩阵y为目标向量。test_size0.330% 数据作测试集70% 作训练集。random_state42固定随机种子保证每次划分结果一致便于复现。五、特征工程核心5.1 为什么要区分特征类型KNN 依赖距离计算默认欧氏距离判断样本相似度。不同类型的特征需要不同处理数值型量纲差异大如年龄 30 vs 胆固醇 300若不统一尺度数值大的特征会主导距离因此需要标准化。类别型整数编码会被算法误认为有序数值。例如胸痛类型1和2的差是 11和3的差是 2算法会误以为类型 1 更接近类型 2而实际上它们只是无序类别。因此需要独热编码消除虚假顺序。二元特征本身就是 0/1已经是合理的数值形式无需处理直接透传。5.2 特征分组numerical_features[年龄,静息血压,胆固醇,最大心率,运动后的ST下降,主血管数量]categorical_features[胸痛类型,静息心电图结果,峰值ST段的斜率,地中海贫血]binary_features[性别,空腹血糖,运动性心绞痛]数据集特征逐一归类如下特征类型说明处理方式年龄数值型连续值标准化静息血压数值型连续值标准化胆固醇数值型连续值标准化最大心率数值型连续值标准化运动后的ST下降数值型连续值标准化主血管数量数值型连续值标准化胸痛类型类别型名义变量4 种分类独热编码静息心电图结果类别型名义变量3 种分类独热编码峰值ST段的斜率类别型有序变量3 种分类独热编码地中海贫血类别型名义变量4 种分类独热编码性别二元特征0/1透传空腹血糖二元特征0/1透传运动性心绞痛二元特征0/1透传名义变量 vs 有序变量名义变量如胸痛类型各类别间无先后大小之分有序变量如峰值ST段的斜率类别本身有顺序含义。本案例为简化处理两者统一用独热编码。若想保留有序变量的顺序信息也可改用OrdinalEncoder按序映射为整数。5.3 独热编码原理以胸痛类型取值 0/1/2/3为例独热编码把 1 列展开为多列每列代表一个类别命中为 1其余为 0原始值类型_0类型_1类型_2类型_30100020010这样任意两个类别之间的距离都相等消除了错误的顺序关系。dropfirst的作用删除第一个类别列避免多重共线性Multicollinearity即哑变量陷阱。多重共线性是指特征之间存在高度线性相关关系的现象。以胸痛类型4 个类别 0/1/2/3为例若直接独热编码会生成 4 个新列此时这 4 列恒满足胸痛类型_0 胸痛类型_1 胸痛类型_2 胸痛类型_3 1也就是说任意一列都可由其余三列线性表示例如类型_0 1 - 类型_1 - 类型_2 - 类型_3存在完全线性相关。这会带来两个问题信息冗余已知其余列全为 0就能推断出被删除的那一类第 4 列不提供新信息。对线性/正则化类模型不友好设计矩阵不满秩参数估计不唯一、不稳定KNN 本身不受影响但保留冗余列会徒增维度。设置dropfirst后删除类型_0用 3 列即可无损表达 4 个类别打破了上面的恒等式消除多重共线性。一般规律n 个类别只需 n-1 个哑变量。5.4 标准化原理StandardScaler对每个数值特征做 z-score 变换z (x - 均值) / 标准差变换后每个特征均值为 0、标准差为 1使所有数值特征处于同一尺度距离计算不再被大量纲特征主导。5.5 ColumnTransformer 组装columnTransformerColumnTransformer(transformers[(num,StandardScaler(),numerical_features),# 数值列 → 标准化(cat,OneHotEncoder(dropfirst),categorical_features),# 类别列 → 独热编码(bin,passthrough,binary_features)# 二元列 → 原样透传])X_traincolumnTransformer.fit_transform(X_train)# 拟合 转换X_testcolumnTransformer.transform(X_test)# 仅转换ColumnTransformer让不同列走不同的处理管道最后横向拼接为一个矩阵。转换后特征列数会因独热编码而增加原 13 列 → 展开后更多列这也是打印X_train.shape前后变化的原因。5.6 为什么训练集用fit_transform、测试集用transformfit_transformfit学参数transform做变换两步合一而transform只做变换、不学参数。二者不能互换核心目的是防止数据泄露data leakage。方法作用对象做什么fit_transform训练集从训练数据学习参数μ、σ、类别集合并立即应用变换transform测试集 / 新数据复用训练集学到的参数只做变换不重新学习具体到本例StandardScalerfit_transform时计算训练集的均值 μ 和标准差 σtransform时用同一套 μ、σ 去标准化测试集即(x - μ_train) / σ_train。OneHotEncoderfit_transform时记录训练集出现过的类别transform时按同一套类别集合编码测试集。为什么测试集不能也fit测试集要模拟未来未知数据。真实上线时新样本是一条条到来的根本没有测试集的整体统计量可算只能套用训练时固定下来的参数。若对测试集单独fit会算出测试集自己的 μ/σ两套标准不一致——同一个原始值在训练和测试里被映射成不同数值模型评估结果会失真通常偏乐观。保证一致性joblib保存的columnTransformer携带的是训练集参数上线后对新数据同样只能transform。一句话训练集fit_transform定标准测试集 / 新数据transform套标准。六、模型训练与网格搜索调参6.1 网格搜索原理网格搜索Grid Search是一种系统化的超参数调优方法通过遍历预定义的超参数组合找到使模型性能最优的参数配置从而避免手动试错、提高效率。网格搜索通常与交叉验证嵌套使用以提高调参的可靠性外层循环遍历参数网格中的每个参数组合。内层循环对每个参数组合使用交叉验证评估模型性能取平均得分作为该组合的最终评分。以本例为例参数网格共 10 × 2 20 组组合每组在 10 折交叉验证下训练 10 次合计训练 200 个模型n_neighbors1, weightsuniform → 10 折 CV → 平均准确率 n_neighbors1, weightsdistance → 10 折 CV → 平均准确率 ...共 20 组最终取平均准确率最高的那组参数作为最佳超参数。6.2 超参数 K 值K 是 KNN 算法的核心超参数n_neighbors即预测时参考最近邻居的数量。预测一个样本时算法找到训练集中距它最近的 K 个点用这 K 个点的类别多数投票决定预测结果K3 → 找最近 3 个邻居 → 2 个患病 1 个健康 → 预测患病 K5 → 找最近 5 个邻居 → 2 个患病 3 个健康 → 预测健康K 值大小对模型的影响K 小如 K1K 大如 K10对训练集拟合极好决策边界更平滑容易过拟合噪声敏感可能欠拟合因此本例用网格搜索遍历 K1~10配合 10 折交叉验证找到泛化能力最好的 K 值而不是手动猜。6.3 十折交叉验证10 折交叉验证10-Fold Cross Validation是把训练集平均分成 10 份轮流用其中 1 份作验证集、其余 9 份作训练集重复 10 次最终取 10 次得分的平均值原始训练集100%均分为 10 份 [1][2][3][4][5][6][7][8][9][10] 第 1 轮[1] 验证 [2~10] 训练 → 得分 a1 第 2 轮[2] 验证 [1,3~10] 训练 → 得分 a2 ... 第 10 轮[10] 验证 [1~9] 训练 → 得分 a10 最终得分 (a1 a2 ... a10) / 10为什么要交叉验证若只划分一次验证集得分可能受划分方式影响运气好/差。10 折让每个样本都当过一次验证数据评估结果更稳定可靠。结合网格搜索每组超参数组合都跑完整的 10 折 CV最后选平均分最高的那组参数避免恰好在某个验证集上表现好的偶然性。6.4 代码实现# 创建 KNN 分类器knnKNeighborsClassifier()# 定义待搜索的超参数网格param_grid{n_neighbors:list(range(1,11)),# K 从 1 到 10weights:[uniform,distance]# 等权 / 距离加权}# 10 折交叉验证的网格搜索grid_search_cvGridSearchCV(estimatorknn,param_gridparam_grid,cv10)grid_search_cv.fit(X_train,y_train)n_neighbors邻居数 K逐一尝试 1~10。weightsuniform所有邻居等权投票distance越近的邻居权重越大。cv1010 折交叉验证每组参数训练评估 10 次取平均结果更稳健。GridSearchCV会遍历所有参数组合10 × 2 20 组自动挑出表现最好的一组。查看结果resultspd.DataFrame(grid_search_cv.cv_results_).to_string()print(results)# 每组参数的详细得分print(grid_search_cv.best_estimator_)# 最佳模型对象print(grid_search_cv.best_params_)# 最佳超参数组合print(grid_search_cv.best_score_)# 交叉验证的最佳平均得分七、最终评估knngrid_search_cv.best_estimator_# 取出网格搜索选出的最佳模型print(knn.score(X_test,y_test))# 在测试集上评估泛化能力用未参与训练的测试集评估衡量模型对新数据的真实预测能力准确率。八、模型持久化代码中已注释供参考importjoblib joblib.dump(valueknn,filenameknn_model)# 保存模型到磁盘knn_loadedjoblib.load(knn_model)# 加载模型y_predknn_loaded.predict(X_test[10:11])# 对新数据预测joblib可将训练好的模型序列化保存避免每次使用都重新训练。九、小结清洗删除缺失行保证数据质量。划分先分特征/标签再分训练/测试固定随机种子。特征工程数值标准化、类别独热编码、二元透传用ColumnTransformer统一编排训练集fit_transform、测试集transform防止数据泄露。调参GridSearchCV 交叉验证自动挑选最优 K 和权重。评估用最佳模型在测试集上验证泛化性能。importpandasaspdfromsklearn.model_selectionimporttrain_test_split,GridSearchCVfromsklearn.composeimportColumnTransformerfromsklearn.preprocessingimportOneHotEncoder,StandardScaler# 独热编码和标准化fromsklearn.neighborsimportKNeighborsClassifierimportjoblib# 1. 加载数据集heart_disease_datapd.read_csv(../data/heart_disease.csv)# 处理缺失值heart_disease_data.dropna(inplaceTrue)heart_disease_data.info()print(heart_disease_data.head())# 2. 数据集划分# 划分特征和标签Xheart_disease_data.drop(是否患有心脏病,axis1)yheart_disease_data[是否患有心脏病]X_train,X_test,y_train,y_testtrain_test_split(X,y,test_size0.3,random_state42)print(X_train.shape,X_test.shape,y_train.shape,y_test.shape)# 3. 特征工程# 数值型特征numerical_features[年龄,静息血压,胆固醇,最大心率,运动后的ST下降,主血管数量]# 类别型特征categorical_features[胸痛类型,静息心电图结果,峰值ST段的斜率,地中海贫血]# 二元特征binary_features[性别,空腹血糖,运动性心绞痛]# 创建一个列转换器columnTransformerColumnTransformer(transformers[(num,StandardScaler(),numerical_features),(cat,OneHotEncoder(dropfirst),categorical_features),(bin,passthrough,binary_features)])# 特征转换X_traincolumnTransformer.fit_transform(X_train)X_testcolumnTransformer.transform(X_test)print(X_train.shape)print(X_test.shape)# # 4. 创建模型# knn KNeighborsClassifier(n_neighbors3)## # 5. 模型训练# knn.fit(X_train, y_train)## # 6. 模型评估计算预测准确率# score knn.score(X_test, y_test)# print(score)# # 7. 保存模型# joblib.dump(valueknn, filenameknn_model)# # 加载模型对新数据进行预测# knn_loaded joblib.load(knn_model)# y_pred knn_loaded.predict(X_test[10:11])# print(f预测类别{y_pred}, 真实类别{y_test[10]})# 创建KNN分类器knnKNeighborsClassifier()# 定义网格搜索参数列表param_grid{n_neighbors:list(range(1,11)),weights:[uniform,distance]}grid_search_cvGridSearchCV(estimatorknn,param_gridparam_grid,cv10)# 模型训练grid_search_cv.fit(X_train,y_train)# 打印模型评估结果resultspd.DataFrame(grid_search_cv.cv_results_).to_string()print(results)# 直接获取最佳模型和最佳得分print(grid_search_cv.best_estimator_)print(grid_search_cv.best_params_)print(grid_search_cv.best_score_)# 使用最佳模型进行测试评估knngrid_search_cv.best_estimator_print(knn.score(X_test,y_test))尚硅谷大模型资料链接https://pan.baidu.com/s/1xN1ah75DlC_osEUV6a1Jyw?pwdyyds