简介:面向数据分析课程大作业与统计建模入门练习,这份资料以红酒数据集为例,完整走通从数据清洗、相关性分析到回归与分类的建模流程,适合正在完成课程作业、需要一份可对照报告范例的高校学生与数据分析初学者参考。压缩包共 1 个文件,为 docx 文档,约 202KB,正文涵盖背景介绍、变量说明、相关性分析、多元线性回归、主成分回归与 KNN 分类等模块,并配有 R 语言代码、运行结果截图与图表,便于按章节复现实验。资源已有 4271 人学习下载,说明其作为作业参考的实用性得到较多认可。读者可从中获得一份结构完整的分析报告框架,理解残糖、游离二氧化硫、pH 值与评分相关性较弱而挥发性酸度、硫酸盐、酒精影响更明显的结论,学习如何借相关系数降维、解读回归系数与 R 方、用主成分回归处理多重共线性,并借鉴 KNN 分类思路与图表排版方式,快速形成自己的分析思路与写作提纲。
1. 红酒评分分析项目定位:从 11 个理化指标到 3 个关键变量
拿到一份redwine.xlsx,很多人第一反应是直接lm(q ~ .)跑回归,结果摘要里一堆不显著参数,R 方也不高。这个红酒数据集分析项目反着来:先用 Spearman 相关矩阵筛变量,再用三变量多元线性回归解释评分,接着用主成分回归处理共线性,最后用 KNN 做评分分类。它适合刚做完数据分析大作业、想把 R 与 Python 混编流程跑通的开发者;也适合想看清“相关不等于因果”在真实表格里怎么落地的人。项目不追求高精度模型,重点是把数据读取、字段含义、回归诊断和分类评估串成一条可复现的链路。评分集中在 5 分和 6 分,这一点决定了后面很多参数不能照搬教科书。
2. 项目结构与数据流:csv 转 xlsx 到 R/Python 双线分析
这个项目的工程味道不重,但数据流很清楚:原始数据以 csv 存放,先用 Python 转成 xlsx,R 侧负责相关分析、多元线性回归和主成分回归,Python 侧负责用 openpyxl 读表并手写 KNN。两条线共用同一份redwine.xlsx,所以表名、列顺序、表头这三件事一旦不一致,后面所有结果都会对不上。我一般会先把文件角色固定下来,再谈模型。
2.1 目录结构与两条分析线
常见做法是把文件按“数据、R 脚本、Python 脚本、输出”分开,避免在根目录里反复覆盖同名文件。下面这份清单是按项目正文里的动作反推出来的,文件名可以按自己的习惯改,但角色最好别混。
| 文件 | 作用 | 入口 |
|---|---|---|
redwine.csv | 原始数据,逗号分隔 | 转换脚本输入 |
redwine.xlsx | R 与 Python 共用的分析表,Sheet1 | 所有脚本读取 |
cor.R | 读取 xlsx、Spearman 相关矩阵、变量间相关性 | Rscript 或 RStudio |
regression.R | 多元线性回归、三变量精简模型、预测区间 | 依赖redwine.xlsx |
pcr.R | princomp 主成分分析、主成分回归、系数还原 | 依赖redwine.xlsx |
knn.py | openpyxl 读表、距离计算、K 近邻投票 | 依赖redwine.xlsx |
R 侧的分析顺序是“先看单变量与评分的关系,再看变量之间的共线性,最后才进模型”。Python 侧不参与统计建模,只做分类验证,所以它读表时只取五项距离特征和评分标签,不需要把 11 列全部搬进去。这种拆法的好处是:R 里改模型不会影响 KNN 的输入列,Python 里调 K 值也不用重跑回归。
2.2 字段表和评分分布:先看清 q 再建模
项目正文里列了 11 个物理属性,评分范围 0 到 10,实际数据落在 3 到 8,绝大多数集中在 5 和 6。这个分布很关键:回归的 R 方不会太高,KNN 的准确率也容易被多数类带偏。先把字段代号和含义对齐,后面看summary()和混淆矩阵才不会迷路。
| 代号 | 字段含义 | 在项目里的角色 |
|---|---|---|
| x1 | 非挥发性酸度 | 全变量回归候选,与 pH 有共线信号 |
| x2 | 挥发性酸度 | 与评分相关性较高,精简模型核心变量 |
| x3 | 柠檬酸 | 全变量回归候选 |
| x4 | 残糖 | 与评分相关性不高,先降维 |
| x5 | 氯化物 | 全变量回归候选,KNN 距离特征之一 |
| x6 | 游离二氧化硫 | 与评分相关性不高,先降维 |
| x7 | 总二氧化硫 | 全变量回归候选,KNN 距离特征之一 |
| x8 | 密度 | 与酒精有相关性,全变量回归候选 |
| x9 | pH 值 | 与评分相关性不高,先降维;与非挥发性酸相关高 |
| x10 | 硫酸盐 | 与评分相关性较高,精简模型核心变量 |
| x11 | 酒精 | 与评分相关性较高,精简模型核心变量 |
| q | 评分 | 回归因变量、KNN 标签 |
正文里提到“去除相关性不高的变量”时去掉了 x4、x6、x9,精简模型只留 x2、x10、x11。这里不要理解成“这三个变量永远没用”,它只是在这份数据和这次线性假设下先放一边。评分是有序离散值,5 和 6 占了大头,直接用普通最小二乘去拟合,残差分布不会太漂亮,后面看主成分回归和 KNN 结果时要带着这个前提。
2.3 环境准备与 csv 转 xlsx 的第一步
R 侧只需要readxl读 xlsx,相关、回归、主成分都用基础包;Python 侧只需要openpyxl,KNN 不依赖 sklearn,直接手写距离和投票。下面两条命令装依赖,不要把环境搞得太重,后面复现时反而容易出错。
# R 侧安装 readxl,只用于读取 xlsx Rscript -e 'install.packages("readxl")' # Python 侧安装 openpyxl,KNN 用标准库手写 python -m pip install openpyxlRscript -e适合在终端里一次性装包,python -m pip能避免 pip 指向错解释器。装完后先别急着跑模型,先把 csv 转成 xlsx,并确认 Sheet 名称是Sheet1,因为后面 R 的sheet = 1和 Python 的wb["Sheet1"]都依赖这个约定。
import csv from openpyxl import Workbook src = "redwine.csv" dst = "redwine.xlsx" wb = Workbook() ws = wb.active ws.title = "Sheet1" # 后面 R 和 Python 都按这个表名读 with open(src, newline="", encoding="utf-8") as f: reader = csv.reader(f) for row in reader: ws.append(row) wb.save(dst) print("rows:", ws.max_row, "cols:", ws.max_column)这段脚本按原样保留表头,ws.append(row)会从第一行开始写,所以 xlsx 的第一行是列名,数据从第二行开始。encoding="utf-8"在 Windows 上如果遇到乱码,换成gbk再跑一次;ws.max_row和ws.max_column打印出来是为了和 R 侧read_excel后的dim(df)对一下,行数、列数不一致就说明转换时丢行或多了空行。常见做法是转换完立刻用 R 读一次:
library(readxl) df <- read_excel("redwine.xlsx", sheet = 1, col_names = TRUE) dim(df) head(df)sheet = 1表示读第一个工作表,col_names = TRUE表示第一行当列名。dim(df)是行数和列数,head(df)看前六行。如果列名变成...1、...2这种,说明表头没被识别,回头检查 csv 第一行是不是空行。这个环节看起来琐碎,但后面相关矩阵列名对不上、KNN 取错列,基本都是这里留下的坑。
3. 相关性分析与降维:spearman 相关系数怎么读、怎么删变量
相关分析在这个项目里不是走过场,它决定了后面回归放哪些变量。正文用 R 的cor(df, method = "spearman")一次性算出矩阵,再从矩阵里看 x4、x6、x9 与评分相关性不高,x2、x10、x11 与评分相关性较高。这里要注意:cor()默认是 Pearson,项目显式写了method = "spearman",原因是评分是有序等级,Spearman 基于秩,对异常值和单调非线性更稳。
3.1 读取 xlsx 与 cor 函数
先跑通读取和相关系数矩阵,不要一上来就写回归公式。下面这段是项目正文里的核心代码,我补了use = "complete.obs"和一行按相关性绝对值排序,方便定位变量。
install.packages("readxl") library(readxl) df <- read_excel("redwine.xlsx", sheet = 1, col_names = TRUE) # Spearman 相关矩阵,complete.obs 表示含缺失值的行不参与该对变量计算 cor_mat <- cor(df, method = "spearman", use = "complete.obs") round(cor_mat, 3) # 单独看各变量与评分 q 的相关性,按绝对值从大到小排 q_cor <- cor_mat[, "q"] sort(abs(q_cor), decreasing = TRUE)method = "spearman"先把每列转成秩再算 Pearson,所以它对“评分 5 和 6 之间差距不一定等于 6 和 7 之间差距”这种情况更宽容。use = "complete.obs"是常见做法,避免缺失值让整行被静默丢掉。round(cor_mat, 3)只是让矩阵好读,不改变计算。cor_mat[, "q"]取的是评分列与其他变量的相关系数,sort(abs(q_cor))按绝对值排序,能快速看出哪些变量和评分关系弱。
跑完先看两件事:一是评分列 q 与其他变量的相关系数符号和大小,二是变量之间有没有明显高相关。正文的结论是 x4 残糖、x6 游离二氧化硫、x9 pH 与评分相关性不高,可以暂时忽略;x2 挥发性酸度、x10 硫酸盐、x11 酒精与评分相关性较高。这个结论是后面“三变量精简模型”的依据,但不要把它当成固定规则,换一份数据就要重新看矩阵。
3.2 变量取舍:x4/x6/x9 先放一边的逻辑
降维不是“相关性低就永久删除”,而是“在这轮线性模型里先不投入自由度”。x4 残糖和 x6 游离二氧化硫与评分相关性不高,放进全变量回归容易增加不显著参数;x9 pH 与评分关系弱,但它和非挥发性酸 x1 之间有较强的共线信号,如果同时保留 x1 和 x9,回归系数会变得不稳定。项目正文里选择先去掉这三个变量,再对剩余变量做回归,这是课程作业里比较稳妥的做法。
实际写代码时,我一般不会直接手写删除,而是先把候选变量列出来,再决定模型公式。下面这段只做筛选展示,不替代业务判断:
# 查看与评分相关性绝对值最低的几个变量 q_cor <- cor_mat[, "q"] low_vars <- names(sort(abs(q_cor)))[1:4] low_vars # 全变量回归候选:去掉 x4、x6、x9 lm.sol <- lm(q ~ x1 + x2 + x3 + x5 + x7 + x8 + x10 + x11, data = df) summary(lm.sol)names(sort(abs(q_cor)))[1:4]只是把相关性最弱的几个变量名打印出来,方便对照,不代表一定要删。lm.sol的公式里没有 x4、x6、x9,是因为正文在相关分析后决定先降维。summary(lm.sol)里重点看Estimate、Pr(>|t|)和Multiple R-squared:参数不显著说明在当前线性设定下贡献不稳定,R 方不高说明评分还有大量未被这 11 个理化指标解释的部分。这个判断要结合后面的精简模型一起看,不要只凭一次summary()下结论。
提示:相关性低不等于变量没用,可能是非线性关系,也可能是评分本身噪声大。课程作业里可以先按相关性降维,但二次开发时最好保留原始列,方便换模型回查。
3.3 共线性预判:酸-pH、酒精-密度
正文还提到变量之间的相关性:pH 与非挥发性酸相关性很高,酒精含量和密度也有一定相关性。这两组关系在回归里要特别注意,因为自变量之间高度相关会让系数估计变得敏感,今天多一行样本,明天系数符号可能就翻了。下面这段取子矩阵看共线信号,列名按 x1、x9、x8、x11 取,实际数据列名不一致时改成自己的列名。
# 只看非挥发性酸 x1、pH x9、密度 x8、酒精 x11 之间的相关 sub_mat <- cor_mat[c("x1", "x9", "x8", "x11"), c("x1", "x9", "x8", "x11")] round(sub_mat, 3)cor_mat是上一步算好的 Spearman 矩阵,取子矩阵不会重新计算,只是把关心的行列切出来。round(sub_mat, 3)看绝对值,如果 x1 和 x9 的相关系数绝对值接近 1,说明它们携带的信息高度重叠;x8 和 x11 同理。遇到这种情况,全变量回归里保留两者会让summary()的标准误变大,精简模型时只留一个更符合业务解释的变量。项目正文选择在三变量模型里保留 x11 酒精,把 x1、x9、x8 放到全变量回归里观察,这个顺序是合理的:先看全变量,再收缩到核心变量,最后用主成分回归处理剩余共线。
4. 回归建模:从多元线性回归到主成分回归的两次拟合
这个项目的回归分两层:第一层是普通多元线性回归,先去掉相关性不高的变量,再收缩到 x2、x10、x11 三个核心变量;第二层是主成分回归,用princomp(cor = TRUE)取前五个主成分,再把主成分系数还原回原始变量。两层模型解决的不是同一个问题:前者要解释“哪些指标影响评分”,后者要缓解共线性、提高数值稳定性。正文里三变量模型 R 方为 0.6589,说明拟合程度还不够高,这个数字要记住,后面看主成分回归时不要期待它突然变成 0.9。
4.1 全变量回归与参数显著性
先跑去掉 x4、x6、x9 后的全变量回归,看看哪些参数通过检验。正文给的是lm(q~x1+x2+x3+x5+x7+x8+x10+x11, data=df),然后summary(lm.sol)。这段代码的作用不是追求高 R 方,而是看剩余变量里哪些还有独立贡献。
# 去掉相关性不高的 x4、x6、x9 后做全变量回归 lm.sol <- lm(q ~ x1 + x2 + x3 + x5 + x7 + x8 + x10 + x11, data = df) summary(lm.sol) # 提取系数和 p 值,按 p 值排序看哪些参数没通过检验 coef_tab <- summary(lm.sol)$coefficients coef_tab[order(coef_tab[, 4]), ]lm()的第一个参数是公式,q是因变量,右边是自变量,data = df指定数据框。summary(lm.sol)输出里Estimate是系数,Std. Error是标准误,t value是 t 统计量,Pr(>|t|)是 p 值。coef_tab[order(coef_tab[, 4]), ]按 p 值从小到大排,能快速看到哪些参数显著、哪些不显著。正文的结论是“有部分参数没有通过检验”,这很正常:全变量模型里存在共线和冗余变量,参数显著性会被稀释。不要把不显著变量直接判死刑,先看它和核心变量的相关性,再决定是删还是换。
4.2 三变量精简模型与预测区间
正文接着只保留 x2 挥发性酸度、x10 硫酸盐、x11 酒精,重新拟合。这个模型参数全部通过检验,但 R 方只有 0.6589。R 方不高不代表模型不能用,它说明评分还受其他因素影响,或者评分本身主观性较强。代码里还留了一组数据做预测:x2=0.31、x10=0.66、x11=11,实际评分为 6,预测结果比较接近。
# 只保留相关性较高的三个变量 lm.sol2 <- lm(q ~ x2 + x10 + x11, data = df) summary(lm.sol2) # 对新样本做预测,同时给置信区间和预测区间 new <- data.frame(x2 = 0.31, x10 = 0.66, x11 = 11) predict(lm.sol2, new, interval = "confidence") predict(lm.sol2, new, interval = "prediction")lm.sol2是精简模型,summary()里看三个变量的系数符号和 p 值。正文给出的拟合式是q = -1.22130*x2 + 0.67898*x10 + 0.30920*x11 + 2.61104,意思是挥发性酸度越高评分越低,硫酸盐和酒精越高评分越高。new是数据框,列名必须和模型里的自变量名完全一致,否则predict()会报“变量长度不一致”或找不到变量。interval = "confidence"给的是平均响应的置信区间,interval = "prediction"给的是单个新样本的预测区间,后者一定比前者宽,因为多了个体误差。实际写报告时两个都报,别只报点预测。
注意:
predict()的new里不要多放模型没用的列,也不要少放模型用到的列。列名对不上是这段代码最常见的翻车点。
4.3 主成分回归:princomp 取前五个主成分
三变量模型虽然参数显著,但 R 方不够高,而且只用了三个变量,信息损失大。正文于是对全部 11 个变量做主成分分析,用相关阵cor = TRUE,取前五个主成分,再对评分做回归。这里读数据的范围写成A1:K1559,注意表头占一行,样本从第二行开始,别把表头当样本算进去。
library(readxl) # 读取全部 11 个属性,范围按实际文件调整 wine <- read_xlsx("redwine.xlsx", sheet = 1, col_names = TRUE, range = "A1:K1559") # 用相关阵做主成分,等价于先标准化再 PCA wine.pr <- princomp(wine, cor = TRUE) summary(wine.pr, loadings = TRUE) # 计算每个样本的主成分得分,取前五个 pre <- predict(wine.pr) z1 <- pre[, 1] z2 <- pre[, 2] z3 <- pre[, 3] z4 <- pre[, 4] z5 <- pre[, 5] redd <- data.frame(q = df$q, z1, z2, z3, z4, z5) lm.solp <- lm(q ~ z1 + z2 + z3 + z4 + z5, data = redd) summary(lm.solp)princomp(wine, cor = TRUE)里的cor = TRUE表示用相关矩阵而不是协方差矩阵,等价于先把每个变量标准化,避免量纲大的变量(比如总二氧化硫)主导主成分。summary(wine.pr, loadings = TRUE)看每个主成分的方差贡献率和载荷,正文取前五个主成分,是因为前五个能捕捉大部分变化趋势。predict(wine.pr)返回每个样本在各个主成分上的得分,pre[, 1]到pre[, 5]取前五列。redd把评分 q 和五个主成分拼成一个数据框,lm.solp对主成分做回归。注意df$q的长度要和pre的行数一致,不一致通常是 xlsx 读取范围或缺失值处理不同导致的。
4.4 主成分系数还原到原始变量
主成分回归的系数是“主成分空间”的系数,不能直接解释成“酒精每增加 1 单位评分增加多少”。正文最后一步是把主成分系数还原回原始变量:用相关阵的特征向量A、原始变量的均值X.bar和标准差X.sd,把beta换算成原始尺度。这里有一个容易忽略的点:模型里用了 z1 到 z5,还原时就要把beta[2]到beta[6]都乘上对应的特征向量,正文原代码只累加到beta[5]*A[,4],和模型里的 z5 不一致,复现时要么模型只取四个主成分,要么补上第五项。
XX <- cor(wine) A <- eigen(XX)$vectors beta <- coef(lm.solp) # 均值和标准差来自对原始变量的统计,顺序必须和 wine 的列顺序一致 X.bar <- c(8.3623, 0.5273, 0.2719, 2.5371, 0.0878, 15.7054, 46.4037, 0.9968, 3.309, 0.6583, 10.4114) X.sd <- c(1.7415, 0.1791, 0.1947, 1.4103, 0.0471, 10.4627, 32.9041, 0.00189, 0.154, 0.1695, 1.0657) # 补上 beta[6] * A[,5],和 lm.solp 里的 z5 对齐 coef_raw <- (beta[2] * A[, 1] + beta[3] * A[, 2] + beta[4] * A[, 3] + beta[5] * A[, 4] + beta[6] * A[, 5]) / X.sd beta0 <- beta[1] - sum(X.bar * coef_raw) c(beta0, coef_raw)XX <- cor(wine)是原始变量的相关阵,eigen(XX)$vectors取特征向量,列顺序对应主成分顺序。beta <- coef(lm.solp)的第一个元素是截距,后面依次是 z1 到 z5 的系数。X.bar和X.sd是原始变量的均值和标准差,顺序必须和wine的列顺序一致,否则还原出来的系数会错位。coef_raw先算原始尺度下的斜率,beta0再用截距减去均值项,得到原始尺度下的常数项。正文代入一组主成分得分后预测值和实际 6 有偏差,这很正常:主成分回归牺牲了部分解释性,换来了对共线的稳定性,不能拿它和三变量模型的点预测直接比高低。
5. KNN 分类与常见问题排查:五项距离、20 近邻和准确度上不去的原因
KNN 部分是这个项目里最容易“看起来能跑、结果对不上”的模块。正文选择 x2 挥发性酸度、x5 氯化物、x7 总二氧化硫、x10 硫酸盐、x11 酒精五项作为距离度量,计算新样本与已知样本的平方和再开方,取最近的 20 个点投票。但正文代码里for i in range(30)又写了 30,和“20 个点”不一致;read_excel的行号也从range(1, rows)配cell(row + 1, ...),很容易跳过第一行或越界。下面先给一版可运行的修正代码,再把踩过的坑逐条拆开。
5.1 数据读取:openpyxl 只取五列加标签
原始代码用openpyxl.load_workbook读Sheet1,按列索引筛出需要的列。openpyxl 的行列都从 1 开始,表头在第 1 行,数据从第 2 行开始,所以循环要从 2 到max_row,不要再用row + 1二次偏移。下面这版把特征列和标签列提成常量,换表时只改常量。
from openpyxl import load_workbook FEATURE_COLS = [2, 5, 7, 10, 11] # x2,x5,x7,x10,x11,openpyxl 列号从 1 开始 LABEL_COL = 12 # q 评分列 def read_excel(path): wb = load_workbook(path, data_only=True) ws = wb["Sheet1"] objects = [] for row in range(2, ws.max_row + 1): # 跳过第 1 行表头 item = [ws.cell(row=row, column=c).value for c in FEATURE_COLS] label = ws.cell(row=row, column=LABEL_COL).value if label is not None: objects.append(item + [label]) return objectsload_workbook(path, data_only=True)读的是单元格计算值,避免公式没刷新时读到None。ws["Sheet1"]要求工作簿里确实有这个名字,如果转换时改了表名,这里要同步改。range(2, ws.max_row + 1)从第二行到最后一行,ws.cell(row=row, column=c)的 row 和 column 都是 1 基,不需要再加一。if label is not None过滤掉空标签行,防止后面投票时出现None类别。item + [label]把五个特征和标签拼成一条样本,最后一位是标签。
5.2 距离计算与投票:knn.py 的三个函数
距离函数只做欧氏距离,投票函数用Counter取出现次数最多的标签。正文原代码用字典手动计数,但只打印了第一个值,没有返回最大票数,这里改成返回(预测标签, 票数明细),方便调试。
from collections import Counter def distances(nowa, objects): out = [] for obj in objects: # obj 前五项是特征,最后一项是标签,不参与距离 dist = sum((obj[j] - nowa[j]) ** 2 for j in range(len(nowa))) ** 0.5 out.append(dist) return out def classify(nowa, objects, k=20): dists = distances(nowa, objects) idx = sorted(range(len(dists)), key=lambda i: dists[i])[:k] votes = Counter(objects[i][-1] for i in idx) return votes.most_common(1)[0][0], votes if __name__ == "__main__": objects = read_excel("redwine.xlsx") test = [0.31, 0.08, 30.0, 0.66, 11.0] # 按 x2,x5,x7,x10,x11 顺序 label, votes = classify(test, objects, k=20) print("pred:", label, "votes:", votes)distances()里range(len(nowa))只遍历新样本的特征长度,obj[j]取已知样本对应特征,** 2是平方,** 0.5是开方。classify()的k=20是近邻数,sorted(...)[:k]按距离升序取前 k 个索引,Counter统计标签出现次数,most_common(1)取票数最多的类别。test的顺序必须和FEATURE_COLS一致,否则距离算的是错位的特征。正文说“选择距离最近的 20 个点”,代码里却写了 30,这里统一成 20,复现时不要两处混用。
5.3 五条踩坑记录:现象、原因、解决
现象一:读出来的样本少一行,或者最后一行是None。
原因:range(1, rows)从 0 基开始,cell(row + 1, ...)又加了一次 1,第一行数据被跳过,最后一行可能越界。
解决:循环直接从第 2 行到max_row,cell用当前 row,不再二次偏移;读完后打印len(objects)和ws.max_row - 1对一下。
现象二:KNN 准确率和正文对不上。
原因:正文文字写 k=20,代码里range(30)写了 30;投票函数只打印第一个类别,没有真正取最大票数。
解决:把 k 提成常量K = 20,距离排序后取[:K],投票用Counter.most_common(1)返回最大票数类别;每次跑完打印票数明细,确认不是只看了第一个近邻。
现象三:模型总预测 5 或 6,准确率看起来还行。
原因:评分集中在 5 和 6,类别不平衡,多数类投票天然占优。
解决:不要只看整体准确率,分标签看召回率和混淆矩阵;做训练/测试划分时按评分分层抽样,保证每个评分段都有样本。如果 3、4、7、8 分样本太少,分类报告里要单独说明,不要拿一个总数掩盖问题。
现象四:距离被酒精或总二氧化硫主导。
原因:五项特征量纲不同,酒精在 10 左右,挥发性酸度在 0.3 到 0.6,氯化物更小,欧氏距离会被数值大的特征吃掉。
解决:做 KNN 前先标准化,常见做法是 z-score 或 min-max;标准化参数只能用训练集计算,再应用到测试集,避免数据泄漏。正文原代码没有标准化,这是准确度不高的原因之一。
现象五:用全量数据找近邻再评估,准确率虚高。
原因:测试样本本身就在训练集里,最近邻距离为 0,投票结果必然包含自己。
解决:先留出测试集,再在训练集里找近邻;课程作业至少做一次 7:3 留出,二次开发可以上交叉验证。每次评估前检查测试集标签有没有参与距离计算,objects里只放训练样本。
6. 二次开发:把课程作业改成可复用分析脚本
这份作业的代码是“按报告顺序写下来的”,跑一遍没问题,但换一份数据或改一个参数就要翻好几个文件。二次开发我一般先做三件事:把 R 侧的相关、回归、主成分拆成函数;把 KNN 的列号、K 值、表名抽成配置;把每次运行的样本数、缺失值、R 方、KNN 准确率记到日志里。这样改模型时不用碰读表逻辑,换数据时也不用全局搜索列号。
| 改造点 | 做法 | 验证方式 |
|---|---|---|
| R 脚本函数化 | read_wine()、cor_filter()、fit_lm()、fit_pcr()各自只做一件事 | 同一份 xlsx 跑两次,系数和 R 方一致 |
| KNN 参数配置化 | 表名、特征列、标签列、K 值放进CONFIG | 改 K 值不用改函数体 |
| 评估留出法 | 按评分分层抽 30% 做测试集 | 输出混淆矩阵和每个评分的召回率 |
| 运行日志 | 记录行数、缺失值数、R 方、KNN 准确率 | 换数据后能对比前后差异 |
配置可以写成下面这样,Python 侧和 R 侧各读各的,但字段顺序保持一致:
CONFIG = { "file": "redwine.xlsx", "sheet": "Sheet1", "feature_cols": [2, 5, 7, 10, 11], # x2,x5,x7,x10,x11 "label_col": 12, "k": 20, "test_ratio": 0.3, }feature_cols和label_col是 openpyxl 的 1 基列号,换表时只改这里;k和test_ratio是实验参数,跑对比时不用动函数。R 侧我习惯把X.bar、X.sd也写进配置或单独保存,主成分还原时按列名匹配,而不是靠手抄 11 个数字。验证方法很朴素:同一份数据跑两次,回归系数、R 方、KNN 混淆矩阵完全一致才算复现成功;如果第二次结果变了,先查随机种子和训练测试划分,再查读表范围。
从那以后我每次拿到评分型数据集,都先画评分分布再跑相关,强制留一份不参与训练的样本,主成分还原时逐项对 beta 下标。这套习惯不高级,但能少很多“代码跑通、结论对不上”的返工。希望帮到你。
本文还有配套的精品资源,点击获取