ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

红酒评分预测:相关性筛选、多元回归与主成分回归实战

红酒评分预测:相关性筛选、多元回归与主成分回归实战

简介:这是一份数据分析课程大作业的完整报告,围绕经典红酒数据集展开“评分影响因素分析+分类建模”的实践,适合高校学生、数据分析初学者参考。报告从红酒的11项物理属性出发,完整呈现了数据清洗(Python转xlsx)、R语言相关性分析、变量筛选、多元线性回归、主成分回归以及KNN分类的流程。通过相关性分析识别出与评分高度相关的挥发性酸度、硫酸盐和酒精,并给出回归方程、置信区间与预测区间;主成分回归部分则展示降维后建模的具体步骤和系数转换方法。压缩包内包含1个docx文档,大小仅202KB,文字、代码与结果图齐全,便于直接查阅。目前已有4265人学习,适合用来对照自己的分析思路,或快速获取一套可复用的红酒数据分析方案。

1. 红酒数据集分析:评分最不看 pH,看的是这三个因素

打开一份红酒数据集,多数人的第一反应是先看 pH、柠檬酸这类"看起来像品质指标"的字段。但相关性分析给出的答案完全相反:真正和评分挂钩的是挥发性酸度、硫酸盐与酒精,pH 的相关系数低到可以直接忽略。这份项目把完整的数据分析流程走了一遍——CSV 转 XLSX、Spearman 相关性筛变量、多元线性回归拟合评分、主成分回归兜底、再用 KNN 做评分分类,R 与 Python 代码都齐。正在写数据分析大作业的人,可以直接替换成本地数据跑通;想弄明白为什么全变量回归会翻车、主成分系数怎么还原成原始变量的,也能在里面找到答案。

2. 数据读取与相关性筛检:从 CSV 到 Spearman 相关性矩阵

2.1 变量构成与格式预处理:CSV 转 XLSX 时容易忽略的细节

这份数据集的核心结构很清晰:11 个理化属性作为自变量,评分(0 到 10 的整数)作为因变量。原始数据以 CSV 格式给出,项目里第一步是用 Python 把它转成 XLSX,再用 R 读取。转换这一步看着简单,但有个细节值得注意——这类红酒数据集的列分隔符不一定是逗号,很多版本用的是分号,因为部分区域的语言环境把逗号保留给了小数点。

import pandas as pd # 注意 sep 参数:如果文件里是分号分隔,这里必须改成 sep=";" df = pd.read_csv("winequality-red.csv", sep=";") df.to_excel("redwine.xlsx", sheet_name="Sheet1", index=False)

转换后打开 Excel 检查两件事:列名是否被正确识别成 x1、x2 这类编号,评分列是否读成了数值而不是文本。我在实际跑这个项目时遇到过评分列带着引号被读成字符串的情况,后续 R 里一旦对这一列做 cor() 就会直接报错。所以转换完之后,用df.dtypes快速确认一下数据类型是值得养成的习惯。

数据读入后,先看评分分布:这份数据里评分范围是 3 到 8 分,绝大多数集中在 5 分和 6 分之间。这个分布特征会在后面反复影响模型表现——线性回归的 R² 上不去、KNN 分类准确率偏低,根源都在这里。

变量名含义在后续分析中的记号
x1非挥发性酸度回归候选变量
x2挥发性酸度最终回归变量
x3柠檬酸回归候选变量
x4残糖相关性筛除
x5氯化物KNN 特征
x6游离二氧化硫相关性筛除
x7总二氧化硫KNN 特征
x8密度回归候选变量
x9pH 值相关性筛除
x10硫酸盐最终回归变量
x11酒精最终回归变量

2.2 用 Spearman 相关性给 11 个属性排序

相关性分析选择 Spearman 而不是 Pearson 是有道理的:评分是离散整数,分布又明显偏向 5 分和 6 分,不满足 Pearson 对正态性和线性关系的假设。Spearman 基于秩计算,对偏态分布更稳。R 里读入 XLSX 后用一行代码就能拿到整个相关性矩阵。

install.packages("readxl") library(readxl) df <- read_excel("redwine.xlsx", sheet = 1, col_names = TRUE) cor(df, method = "spearman")

这里的col_names = TRUE表示首行是列名,sheet = 1读第一个工作表。cor() 要求所有列都是数值型,所以前面检查类型那一步不能省。

相关性矩阵里真正值得关注的结论有两个层面。第一层是与评分的关系:x2(挥发性酸度)、x10(硫酸盐)、x11(酒精)与评分相关性较高,其中 x2 是负相关,x10 和 x11 是正相关;x4(残糖)、x6(游离二氧化硫)、x9(pH 值)与评分相关性弱,可以先剔除做降维。第二层是变量之间的关系:pH 与非挥发性酸度相关性很高,因为数据里非挥发性酸含量较高,直接主导了 pH 的走向;酒精和密度也有一定相关性。这个第二层结论很关键,它预告了后面全变量回归会遇到的多重共线性问题。

筛选后的处理策略如下表:

变量与评分的相关性处理建议
x2 挥发性酸度负相关,较强保留
x10 硫酸盐正相关,较强保留
x11 酒精正相关,较强保留
x4 残糖、x6 游离二氧化硫、x9 pH弱相关剔除

3. 多元线性回归:三个变量拟合评分与预测区间

3.1 全变量回归的翻车现场:为什么参数大面积不显著

相关性筛掉 x4、x6、x9 之后,剩下的 8 个变量全部丢进线性回归,看起来信息更全,但结果并不好:部分参数的显著性检验 p 值超过了 0.05,意味着这些变量对评分的解释贡献在统计上不显著。这一步的教训是——相关性筛选只是第一道过滤,变量之间还彼此相关,把一堆互相纠缠的变量塞进同一个模型,参数估计会被共线性搅浑。

lm.sol <- lm(q ~ x1 + x2 + x3 + x5 + x7 + x8 + x10 + x11, data = df) summary(lm.sol)

看 summary 输出时,我一般先看每个系数的 Pr(>|t|) 列,再回头看整体 F 统计量。如果整体显著但个别系数不显著,多半是共线性在作怪,而不是这些变量真的没用。此时不要急着逐个删变量,回到相关性矩阵看哪些变量扎堆相关,比盲目的逐步回归更有效率。

3.2 三变量回归:公式、系数含义与 R² 的读法

把变量收敛到相关性最强的三个——x2、x10、x11——重新拟合,所有参数都通过了显著性检验,回归方程也很干净:

p = -1.22130 × x2 + 0.67898 × x10 + 0.30920 × x11 + 2.61104

lm.sol2 <- lm(q ~ x2 + x10 + x11, data = df) summary(lm.sol2)

系数的业务含义很直接:挥发性酸度每增加一个单位,评分平均下降约 1.22 分,这是三个变量里影响最大的;硫酸盐每增加一个单位,评分上升约 0.68 分;酒精每增加一个单位,评分上升约 0.31 分。系数大小不能直接跨变量比较重要性,因为三个变量的量纲不同,但方向和量级已经足够说明问题——挥发性酸度的负向作用远超另外两个正向变量。

R² 是 0.6589,坦白说这个值不算高。但要结合数据背景看:评分是品酒师的主观打分,且绝大多数样本挤在 5 分和 6 分之间,模型的解释力天然受限。与其死磕 R²,不如验证预测区间是否覆盖实际值来得实在。

3.3 置信区间与预测区间:预留样本怎么用

项目里留了一组样本没参与建模,专门用来验证:x2=0.31,x10=0.66,x11=11,实际评分是 6。

new <- data.frame(x2 = 0.31, x10 = 0.66, x11 = 11) lm.conf <- predict(lm.sol2, new, interval = "confidence") lm.pred <- predict(lm.sol2, new, interval = "prediction") print(lm.conf) # 置信区间:针对均值估计 print(lm.pred) # 预测区间:针对单个新样本

这两个区间经常被混用。interval = "confidence"给出的是回归均值估计的不确定性区间,适用于"大量相同条件的酒平均能得多少分";interval = "prediction"给出的是单个新样本的波动范围,适用于"这一瓶酒大概能得多少分"。同一个预测值,预测区间明显比置信区间宽,因为它还要容纳个体残差的波动。实际看结果时,别拿置信区间去对标单瓶酒的预测误差,这是新手最容易看错的地方。

4. 主成分回归:把五维主成分还原成原始变量系数

4.1 为什么还要做主成分:共线性与信息保留

三变量回归虽然通过了检验,但代价是丢掉了 x1、x5、x7、x8 这些原始信息。主成分回归的思路完全不同:先把 11 个变量做正交变换,生成互不相关的多个主成分,再用前几个主成分去拟合评分。这样做的好处是能保留大部分变量的变异信息,同时避开共线性对参数估计的干扰。

用princomp(..., cor = TRUE)时,cor = TRUE表示基于相关矩阵做主成分分析,等价于先把所有变量标准化到同一尺度再提取主成分。这一步是必须的——如果直接对原始数据做,密度这种数值在 0.99 左右的变量和酒精这种在 10 左右的变量,会因量纲差异严重扭曲主成分的方向。

wine <- read_excel("redwine.xlsx", sheet = 1, col_names = TRUE) # wine 这里只保留 11 个理化属性列,评分列单独存放 wine.pr <- princomp(wine, cor = TRUE) summary(wine.pr, loadings = TRUE)

sammary(wine.pr, loadings = TRUE)会输出每个主成分的方差贡献率、累积贡献率和载荷向量。项目里取前五个主成分用于后续回归,具体累积贡献率以你自己跑出来的输出为准,不需要硬记一个固定值。

4.2 提取前五个主成分并做回归

前五个主成分从 predict() 里取出来,拼成新数据框,和评分列一起做回归。

pre <- predict(wine.pr) redd <- data.frame( q = df$q, z1 = pre[, 1], z2 = pre[, 2], z3 = pre[, 3], z4 = pre[, 4], z5 = pre[, 5] ) lm.solp <- lm(q ~ z1 + z2 + z3 + z4 + z5, data = redd) summary(lm.solp)

这里有个容易踩的坑:wine 数据框必须只含 11 个理化属性列,如果误把评分列也带进去,主成分会被评分"污染",后续回归的系数方向全变。主成分回归的系数检验基本都能通过,因为主成分两两正交,不再存在共线性问题。但要注意,主成分本身是原始变量的线性组合,解释性比直接回归差——这是它的代价。

4.3 关键技巧:用特征向量反解原始变量系数

主成分回归的系数是在"主成分空间"里的,业务上没法直接用。真正的技术点在最后一步:把主成分系数还原成原始变量的回归系数。做法是取相关矩阵的特征向量,把前几个主成分的回归系数分别与对应特征向量加权求和,再除以原始变量的标准差;截距项用主成分回归截距减去均值加权项。

XX <- cor(wine) A <- eigen(XX)$vectors beta <- coef(lm.solp) X.bar <- apply(wine, 2, mean) # 均值 X.sd <- apply(wine, 2, sd) # 标准差 coef_orig <- (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_orig) c(beta0, coef_orig)

为什么要除以标准差?因为主成分分析是在标准化空间里做的,还原到原始量纲时必须把标准化的缩放比例还原回去。还原后的系数,方向和量级应该与第 3 章三变量回归的结果大体一致——如果方向都反了,说明主成分的符号取向和预期不一致,需要检查特征向量的符号约定。这一步是整份资源里最有复用价值的部分,做完之后主成分回归就不再是"黑匣子"了。

5. 常见问题与避坑:回归和 KNN 的五个实际踩坑记录

5.1 全变量回归参数不显著:先查相关性矩阵再决定留谁

现象:把相关性筛完剩下的 8 个变量全部丢进lm(),summary 输出里好几个系数的 Pr(>|t|) 都大于 0.05,整体模型却显著。

原因:变量之间存在较强的相关关系,比如非挥发性酸度与 pH、酒精与密度。多个高度相关的变量进入同一模型后,彼此争夺解释力,导致单个系数的标准误膨胀,显著性被稀释。

解决:回到相关性矩阵,看变量与变量之间的相关性,而不是只盯变量与评分的关系。先剔除与评分弱相关的变量,再剔除互相强相关的冗余变量。实际项目里通常做两轮筛选,第一轮按相关性筛,第二轮跑一版全变量回归看 VIF(方差膨胀因子),VIF 超过 10 的变量优先排除。

5.2 三变量回归 R² 只有 0.6589:别急着换模型

现象:三个变量建模后参数全部通过检验,但 R² 只有 0.6589,怎么看都觉得"拟合不够高"。

原因:评分是品酒师的主观判断,且数据里 5 分和 6 分占了绝大多数,样本本身方差有限。模型能解释的部分已经解释完了,剩下的残差更多来自人为评分的不确定性,不是变量缺失。

解决:换模型前先做两个动作:第一,用留出的样本跑预测,看预测区间是否覆盖实际评分,预测准比 R² 重要;第二,看残差图,如果残差随机分布在零线附近,说明模型没有系统性偏差。R² 0.66 对主观评分类数据来说,属于"能用但不完美"的水平。追求高 R² 可以加交互项,但代价是过拟合风险,大作业场景里不值得。

5.3 主成分回归预测有偏差:主成分是黑匣子

现象:拿一组新数据代入主成分模型,预测值和实际评分存在偏差,甚至比三变量回归还大。

原因:主成分回归的系数是在正交空间中估计的,前五个主成分只保留了大部分变异信息,并非全部;丢掉的部分里可能包含与评分相关的细节。另外主成分方向不一定与业务含义对齐,预测时任何输入变量的微小异常都会被线性组合放大。

解决:先看 summary 输出里前五个主成分的累积贡献率,如果低于 80%,考虑增加主成分数量;再看还原后的原始变量系数,方向是否和三变量回归一致,如果不一致,检查是否把评分列误带入了主成分分析。主成分回归适合处理"变量多、共线性重"的场景,不适合当作唯一的预测模型。

5.4 KNN 分类准确率上不去:评分分布失衡与 k 值双杀

现象:按项目里的 KNN 实现跑分类,准确率明显偏低,测试结果和实际评分对不上。

原因:两个因素叠加。第一,评分集中在 5 分和 6 分,近邻样本大量来自这两个类别,少数类(3 分、4 分、8 分)几乎被淹没;第二,k 值取 20 甚至更大时,近邻里会混入过多"中间分"样本,把边界样本的投票结果拉向均值。

解决:降 k 值到 10 左右,或者改成距离加权投票——距离越近的样本权重越大,减少远端噪声的影响。更实用的做法是把评分分档,比如 3-5 分算"低分",6 分算"中分",7-8 分算"高分",把回归问题转成三分类问题,准确率会直观很多。这里踩过的坑是,分类目标变成了模糊的分档而非精确分值,业务上要能接受这个折中。

5.5 手工 KNN 的索引越界与重复距离

现象:Python 手写 KNN 时,从 openpyxl 读数据后,分类函数里取objects[i][5]直接报 IndexError;不报错的时候,某些样本会被反复选中,结果明显异常。

原因:读数据时只往列表里放进了 5 个属性值,评分没有放进去,索引 5 自然越界。另一个坑是distances.index(min(distances))这个写法,当多个距离值并列最小时,index() 永远返回第一个最小值的位置,导致同一个样本被多次选中。

解决:读取时把评分作为第 6 个元素一起存进对象;选最近邻时改成sort()按距离排序,再取前 k 个样本投票。openpyxl 的列号从 1 开始,和 x1、x2 的编号不是一回事,写列索引时先看一眼 Excel 表头,避免因为 off-by-one 读错列。

6. 留出验证与 KNN 改进:让结果经得起复核

做完回归和 KNN 之后,最容易被忽略的一步是验证。这个项目里预留了一组样本没参与建模,这个习惯值得保留——它是整个流程里成本最低、说服力最强的验证方式。我自己的固定做法是:建模前先随机切出 10% 的样本锁起来,所有模型跑完后统一拿这组数据做预测,看预测值和真实值的偏差,而不是只用全体数据拟合后自我感觉良好。

from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler X = df[["x2", "x5", "x7", "x10", "x11"]] y = df["q"].apply(lambda v: "low" if v <= 5 else ("mid" if v <= 6 else "high")) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler().fit(X_train) X_train_s = scaler.transform(X_train) X_test_s = scaler.transform(X_test) model = KNeighborsClassifier(n_neighbors=20, weights="distance") model.fit(X_train_s, y_train) print(model.score(X_test_s, y_test))

这段代码里有两个改进点供参考。一个是weights="distance",近邻投票时按距离加权,比等权投票更抗噪;另一个是StandardScaler,对 x2、x5、x7、x10、x11 做标准化,避免硫酸盐数值范围和总二氧化硫差太多导致距离被大数值变量主导。评分分档也要多说一句:5 分以下算 low,6 分算 mid,7 分以上算 high,这个切分不是最优但足够直观,样本量允许的话可以按分布分位数来切。

跑完这组验证再回头看第 3 章的三变量回归和第 4 章的主成分回归,你会发现自己能判断的问题比之前清楚很多:回归模型预测连续分值时更平滑,KNN 分档后准确率更能反映业务上"能不能区分好坏酒"的需求。从那以后我每次做类似的数据分析作业,都会强制走一遍"留出验证 + 两类模型对比"的流程,哪怕课题组没要求也照做,省下的返工时间远多于多写的那几行代码。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进