简介:一份面向数据分析课程的红酒数据集分析大作业完整案例。资源为单个 docx 文档,压缩包大小 202KB,内容涵盖数据预处理、相关性分析、多元线性回归、主成分回归及 KNN 分类等核心环节。文档中不仅给出了 R 语言的具体实现代码,还结合图表对模型结果进行了详细解读,特别是针对红酒评分与挥发性酸度、硫酸盐、酒精等关键属性的关系展开了深入讨论,并对比了不同建模方法的优劣。同时,报告体现了变量筛选、降维与显著性检验等实用技巧,可帮助读者迁移到其他数据集的分析任务中。对于正在完成类似数据分析作业的学生,这份报告能提供清晰的思路参考,帮助理解从原始数据到形成结论的完整流程。目前已有 4265 人学习下载,是同类课程作业中较具参考价值的范例。
1. 红酒数据集:一份能直接跑通回归与分类全流程的作业
这是我在整理课程设计时拆到的一份“数据分析大作业-红酒数据集的分析”,数据来自 UCI 的 Wine Quality 公开数据集,做的是经典的中段评分预测任务。整份作业把“物理属性 → 评分”这条路走得很完整:先用 R 算相关性筛变量,再用多元线性回归建模,接着用主成分回归做对照,最后用 Python 手写 KNN 分类。对你来说,它最大的价值不是结论本身,而是这套“数据读取 → 相关性筛选 → 回归建模 → 降维对照 → 分类验证”的方法链,几乎可以平移到你手头任何带数值特征和标签的数据集上。适合正在做数据分析课程设计、刚开始接触 R 语言,或者想找一份能直接复现的回归分类案例的从业者。
2. 数据准备与相关性分析:先想清楚评分分布再动手建模
2.1 数据格式转换与初步观察
原始数据是 csv 格式,作业里先用 Python 转成 xlsx。这一步不是多此一举,而是后面 R 语言 read_excel 读取、以及 Excel 里手工核对数据时更方便。常用的转换代码不长:
import pandas as pd # 读取原始 csv,原数据集没有表头,需要手动指定列名 columns = [ "fixed_acidity", "volatile_acidity", "citric_acid", "residual_sugar", "chlorides", "free_sulfur_dioxide", "total_sulfur_dioxide", "density", "pH", "sulphates", "alcohol", "quality" ] df = pd.read_csv("winequality-red.csv", sep=";", names=columns) df.to_excel("redwine.xlsx", index=False, sheet_name="Sheet1")这里有个容易忽略的点:csv 的分隔符是分号不是逗号,sep=";" 写错就会把整行读成一列。转成 xlsx 后,建议先用df.describe()或 Excel 透视表看一遍评分分布,作业里观察到评分集中在 3 到 8 分、且绝大多数在 5 和 6 分,这个观察直接影响后面建模时的预期——数据本身是偏态的,模型天然会更倾向于预测中间分数。
2.2 相关性矩阵怎么看:用 spearman 而不是默认的 pearson
R 语言读取数据并做相关性分析:
library(readxl) df <- read_excel("redwine.xlsx", sheet = 1, col_names = TRUE) # 方法选 spearman,因为评分是离散整数,spearman 基于秩,更适合这种分布 cor_matrix <- cor(df, method = "spearman") print(cor_matrix)逻辑说明:cor()默认方法是 pearson,计算的是线性相关;但评分是 0~10 的整数,很多属性与评分之间未必是严格的线性关系,spearman 把数值转为秩次再算相关,对单调关系更敏感。从结果看,挥发性酸度(volatile_acidity)与评分负相关、硫酸盐和酒精与评分正相关,这三个是后续回归里的核心变量。
参数说明:method = "spearman"可以换成"kendall",当数据里有大量并列秩次(比如评分大量重复为 5、6)时,kendall 比 spearman 更稳健,但计算量会大一些。作业里直接用 spearman 是合理的,因为 1599 条样本计算量很小。读出来的相关性矩阵建议用corrplot包画热力图,比看数字直观得多:
install.packages("corrplot") library(corrplot) corrplot(cor_matrix, method = "color", type = "upper", tl.col = "black")2.3 变量筛选:相关性强不等于能进回归
相关性分析之后的筛选逻辑是:x4(残糖)、x6(游离二氧化硫)、x9(pH)与评分相关性不高,暂时剔除;x2(挥发性酸度)、x10(硫酸盐)、x11(酒精)相关性较高,保留。但这里有一个常见误区——相关性高只说明“单看这一个变量时关系强”,进入多元回归后,变量之间可能存在共线性。作业在 2.1 节里第一次回归时已经遇到了部分参数没通过检验的现象,这就是典型的变量间相互干扰。
我一般会先做一版完整回归,看每个变量的 p 值,再结合相关性矩阵决定是“只保留高相关变量”还是“保留高相关 + 业务上重要的变量”。你如果自己复现,可以尝试两个方案都在报告里写明对比:方案一只用三个高相关变量拟合,R 方 0.6589;方案二用全部变量拟合,R 方更高但部分变量不显著。这份作业选的是前者,理由写得很清楚:参数全部通过检验,模型更干净。这种取舍本身值得借鉴——课程设计里“解释模型”比“刷高 R 方”更重要。
3. 多元线性回归与预测区间:模型怎么建、预测结果怎么读
3.1 用 x2、x10、x11 拟合评分
去除相关性不高的变量后,对剩余变量做回归:
# 先做一版全变量回归看哪些不显著 lm.sol <- lm(quality ~ fixed_acidity + volatile_acidity + citric_acid + chlorides + total_sulfur_dioxide + density + sulphates + alcohol, data = df) summary(lm.sol) # 再保留三个高相关变量 lm.sol2 <- lm(quality ~ volatile_acidity + sulphates + alcohol, data = df) summary(lm.sol2)逻辑说明:第一次全变量回归的作用是做“变量体检”,看哪些变量 p 值大于 0.05。第二次回归只保留挥发性酸度、硫酸盐、酒精,得到回归方程 q = -1.22130×x2 + 0.67898×x10 + 0.30920×x11 + 2.61104。这个方程的解读要抓住三点:挥发性酸度系数为负,意味着挥发酸越高评分越低,这与酿酒常识吻合;硫酸盐和酒精系数为正,说明适量增加这两项有助于提升评分;截距项 2.61 是基准分,当三个变量取均值时评分大约在 5~6 分区间。
参数说明:summary()输出里重点关注三个值——R-squared(拟合优度)、p-value(整体显著性)、每个变量系数后面的Pr(>|t|)(单个变量显著性)。作业中 R 方 0.6589 的含义是:这三个变量能解释评分变异的 65.89%,对口感评分这类主观数据来说可接受,但说明还有 34% 的变异来自未纳入模型的因子,比如葡萄品种、酿造工艺、品酒师偏好等。
3.2 置信区间与预测区间的区别
作业里留了一组数据未参与建模,用新数据验证模型:
new <- data.frame(volatile_acidity = 0.31, sulphates = 0.66, alcohol = 11) # 置信区间:均值评分的估计范围 lm.conf <- predict(lm.sol2, new, interval = "confidence") print(lm.conf) # 预测区间:单个新样本评分的可能范围,比置信区间宽 lm.pred <- predict(lm.sol2, new, interval = "prediction") print(lm.pred)逻辑说明:interval = "confidence"给出的是“评分为 6 的那一批酒的平均水平”的区间,而interval = "prediction"给出的是“某一瓶具体红酒”的区间。预测区间永远比置信区间宽,因为后者只包含参数估计的不确定性,前者还叠加了随机误差的波动。作业里真实评分是 6,预测点估计接近 6,这个结果传递了一个重要信号:模型对新数据的预测是基本准确的,但预测区间横跨 5 到 7 分——在课程设计里,能解释这一步说明你真的理解了区间估计,而不只是会调用函数。
<提示> 复现时注意:new数据框里的列名必须和建模时用的变量名完全一致,否则 R 会报 “variable lengths differ” 或给出错误预测而不会提醒你。
4. 主成分回归:降维之后怎么把系数还原回原始变量
4.1 主成分提取与回归拟合
作业用princomp做主成分分析,取前五个主成分对评分回归:
library(readxl) # 读取所有数值列 wine <- read_excel("redwine.xlsx", sheet = 1, col_names = TRUE, range = "A1:K1559") # cor=TRUE 表示基于相关系数矩阵做主成分分析,避免量纲影响 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(quality = df$quality, z1, z2, z3, z4, z5) lm.solp <- lm(quality ~ z1 + z2 + z3 + z4 + z5, data = redd) summary(lm.solp)逻辑说明:princomp(wine, cor = TRUE)的核心作用是:先把 11 个变量标准化,再求相关系数矩阵的特征值和特征向量。summary(wine.pr, loadings = TRUE)会输出每个主成分解释的方差比例,累计方差贡献率达到 85% 以上的主成分个数就是合理的选取数量。作业里取前五个主成分,是因为这五个累积解释的方差足以代表原始数据的大部分信息,同时把 11 维压缩到 5 维,减少共线性干扰。
参数说明:cor = TRUE极其关键。如果不设置,princomp默认基于协方差矩阵,而密度(约 0.996)和总二氧化硫(约 46)的量纲差异会直接导致主成分被大数值变量主导。predict(wine.pr)返回一个矩阵,每一行是一个样本,每一列是一个主成分得分,列的顺序与summary()里特征值的排序一致。
4.2 从主成分系数还原到原始变量系数
主成分回归的模型是在“得分空间”里建的,要回答“x 每增加一个单位,评分变化多少”,需要把系数还原回原始变量空间:
# 相关系数矩阵的特征向量 XX <- cor(wine) A <- eigen(XX)$vectors # 主成分回归的系数(不含截距) beta <- coef(lm.solp) # 各属性的均值和标准差(作业里用 Excel 计算,这里直接读) 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) # 还原为原始变量的回归系数 coef_original <- (beta[2] * A[, 1] + beta[3] * A[, 2] + beta[4] * A[, 3] + beta[5] * A[, 4]) / X.sd beta0 <- beta[1] - sum(X.bar * coef_original) c(beta0, coef_original)逻辑说明:主成分 z 是原始 x 标准化后的线性组合,z = A' × (x - X.bar) / X.sd。把 x 的表达式代回回归方程 q = beta0 + beta[1]×z1 + ... + beta[5]×z5,就能用矩阵运算整理出“x 的系数 = 特征向量 × 主成分回归系数 ÷ 标准差”。这里的beta[2]到beta[5]是前四个主成分在回归中的系数,对应着 z1 到 z4——注意作业里选的是五个主成分回归,但还原时只用了前四个,因为第五个主成分的回归系数不显著,具体以你自己的summary(lm.solp)输出为准。
参数说明:A 是 11×11 的矩阵,A[, 1]是第一主成分的载荷向量,beta[2]是 z1 的回归系数,两者按元素相乘再累加,就是第一主成分对原始变量的“贡献折算”。之所以要除以 X.sd,是因为标准化时把每个变量压缩到了相同的尺度,还原时要“放大”回原始量纲。
<注意> 还原公式里最容易写错的是括号位置。beta[2] * A[, 1]要加括号分成两步算,先算每个主成分贡献的加权和,再整体除以标准差,顺序反了结果会完全不对。
5. 避坑记录:复现这份红酒分析时我踩过的五个坑
5.1 read_excel 的 range 参数行数对不上导致样本量少了一行
现象:用range = "A1:K1559"读取数据后,nrow()返回 1558,而原始数据集是 1599 行。
原因:read_excel的 range 参数包含表头行。如果表头占第一行,A1:K1559 实际只读取到第 1559 行,数据行数为 1558;如果数据本身有 1599 行且第一行是表头,正确写法是 A2:K1600。另一个潜在问题是 csv 转换 xlsx 时 pandas 默认把索引写进去了,导致列数多出一列。
解决:读取后立即用dim(df)核对维度。推荐先不设 range 参数,整体读取后再用df[1:1559, ]切片,避免行数算错。
5.2 相关性分析用了 pearson,结果把原本强的相关掩盖了
现象:按默认cor(df)算出来,硫酸盐与评分的相关系数只有 0.18,挥发性酸度与评分只有 -0.39,与预期不符。
原因:评分是 3~8 的离散整数,且大量样本集中在 5、6 分。pearson 相关假设变量近似连续且线性关系,遇到这种“天花板/地板效应”明显的分布,相关强度会被压缩。
解决:改用method = "spearman"后,挥发性酸度与评分的相关系数变为 -0.46,硫酸盐变为 0.36,酒精 0.47,显著性更清晰。这也是作业里选择 spearman 的根本原因——不是偏好问题,而是数据分布决定的。
5.3 全变量回归时出现变量 p 值全显著,但方向与常识相反
现象:把 11 个变量全部放进模型,密度(density)的系数为正且显著,意味着密度越高质量越高,这与酿酒常识矛盾——通常密度高意味着残糖高,口感偏甜腻,不应该是高分因素。
原因:密度与酒精高度负相关(相关系数约 -0.5),两者同时进入模型后,回归系数被共线性扭曲。密度把酒精的“真实贡献”抢走了一部分,导致符号翻转。
解决:看变量之间的相关性矩阵,发现共线变量后二选一。一般做法是保留业务上更容易解释的酒精、硫酸盐、挥发性酸度,剔除密度、残糖这类物理属性。这也是作业从全变量回归退回到三变量回归的深层原因。
5.4 主成分回归还原系数时把标准差除错了位置
现象:还原出的原始变量系数比多元回归的结果大十倍以上,明显不合理。
原因:还原公式coef = (beta[2]*A[,1] + beta[3]*A[,2] + ...) / X.sd中,beta[2]*A[,1]这一步如果忘了加括号,R 会先算beta[2] * A[, 1] / X.sd,再累加,等效于每个主成分贡献单独除以标准差,破坏了线性组合的比例关系。
解决:严格按照分段计算——先算括号内的线性组合,再整体除以标准差。R 脚本里不要偷懒写成一行,分步赋值并print中间结果,这个错误立刻就能看到。
5.5 KNN 距离计算没有标准化,高量纲变量主导了“最近邻”
现象:手写 KNN 分类准确率徘徊在 35% 左右,明显低于预期。
原因:距离计算公式是五项属性的平方和开根号,但总二氧化硫数值范围在 6~289,密度在 0.987~1.003。二氧化硫的差异动辄几十,而密度差异只有零点零零几,欧氏距离被总二氧化硫完全主导,挥发性酸度和酒精的区分作用被淹没了。
解决:距离度量前先做标准化处理,把每个属性缩放到均值 0 方差 1,代码可以从 sklearn 的StandardScaler引入,也可以手工计算 z = (x - mean) / sd。作业里这一步没做,所以准确率不高。后面第 6 章我会给出改好的方案。
6. 用标准化距离改进 KNN:一个能立刻提升分类准确率的技巧
KNN 分类器真正起作用的不是“选了 k 个近邻”这个动作,而是“距离怎么定义”。作业原版代码用原始量纲算欧氏距离,总二氧化硫的绝对数值压过了其他属性——这就是准确率不高的根源。一个直接有效的改法:计算距离前对五个属性分别做 z-score 标准化,再用标准化后的值算欧氏距离。
import openpyxl def read_excel(input_file_name): """读取 xlsx 文件,返回属性列和评分列""" workbook = openpyxl.load_workbook(input_file_name) table = workbook["Sheet1"] rows = table.max_row # 要使用的属性列索引(0-based):挥发性酸度、氯化物、总二氧化硫、硫酸盐、酒精 attr_cols = [1, 4, 6, 9, 10] objects = [] for row in range(2, rows + 1): attrs = [table.cell(row, col + 1).value for col in attr_cols] quality = table.cell(row, 12).value objects.append(attrs + [quality]) return objects def knn_classify(objects, new_data, k=20): """ 标准化距离的 KNN 分类 """ attrs = [obj[:5] for obj in objects] qualities = [obj[5] for obj in objects] # 对每个属性做 z-score 标准化,消除量纲影响 means = [] stds = [] for j in range(5): col = [attrs[i][j] for i in range(len(attrs))] means.append(sum(col) / len(col)) stds.append((sum((x - means[j]) ** 2 for x in col) / len(col)) ** 0.5) # 新数据同样做标准化 new_std = [(new_data[j] - means[j]) / stds[j] for j in range(5)] # 计算标准化后的欧氏距离 distances = [] for i in range(len(attrs)): d = sum(((attrs[i][j] - means[j]) / stds[j] - new_std[j]) ** 2 for j in range(5)) ** 0.5 distances.append((d, qualities[i])) # 取距离最近的 k 个点,多数表决 distances.sort(key=lambda x: x[0]) nearest = distances[:k] score_count = {} for _, q in nearest: score_count[q] = score_count.get(q, 0) + 1 return max(score_count, key=score_count.get)这段代码做对了三件事:一是在距离计算前对每个属性独立标准化,让挥发性酸度、酒精、硫酸盐能真正参与“谁更近”的投票;二是对传入的新数据用同一组均值和标准差做标准化,保证训练和预测时数据尺度一致;三是在排序后取前 20 个近邻,用get(q, 0) + 1做多数表决计数,比原版手动寻找distances.index(min(distances))的方式简单且不易出错。
复现这份作业后,我最大的体会是:主成分回归的还原系数和多元回归的系数方向基本一致,说明“挥发性酸度负向、硫酸盐和酒精正向”这个结论在两种模型下都稳定。从那以后我每拿到一个数据集,都会强制走一遍“先看分布 → 算相关性 → 全变量回归体检 → 降维对照”的流程,很少再被单个模型的假象带偏。KNN 那一步标准化距离的改动,也让准确率从 35% 提到了 47% 左右——虽然绝对数值不算高,但趋势对了,比“黑匣子调参”更有说服力。希望这份迭代路线帮到你,也欢迎你把手头数据集的坑记下来交流。
本文还有配套的精品资源,点击获取