简介:一套面向数据挖掘课程设计、期末大作业及入门实战的葡萄酒质量分析项目资料,基于Python完整实现了从数据读取、清洗、分析到可视化、建模评估的全流程,适合计算机专业学生用来直接参考或二次开发。压缩包共16个文件,含10个csv数据文件(酒精含量、酸度、密度等指标及质量评分)、3个py源码文件(主程序、分析脚本、辅助工具)和3个txt说明文档,整体仅595KB,结构紧凑。已有84人学习/下载,该项目代码经过调试可直接运行,降低上手门槛。通过逐行阅读源码与对照数据,学习者可以系统掌握Pandas数据处理、特征选择、分类或回归模型训练以及结果评价等要点,同时也能快速生成一份组织完整、可视化的课程作业或答辩展示成果。
1. 葡萄酒质量分析为什么是数据挖掘作业的黄金选题
数据挖掘大作业最怕的不是模型分数低,而是选题本身是个坑:数据集要么几十万行跑不动,要么脏到清洗占掉三分之二时间。相比之下,葡萄酒质量分析是这类作业里公认性价比最高的方向——数据规模刚好卡在“单机跑得动、代码秒出结果”的量级,字段含义直白,质量分数自带业务解释力,用 Python 做数据挖掘的标准流程(读取、清洗、特征工程、建模、评估)能在一套数据上完整走一遍。这篇笔记就围绕这个选题,按数据挖掘项目的实际推进顺序,讲清楚每一步怎么做、参数怎么定、哪些地方容易翻车,让新手能照着复现,也让熟手看到边界和细节。
2. 拿到数据先别建模:数据体检与红白葡萄酒的差异处理
2.1 字段含义梳理:11个理化指标和一个质量分数
这份经典数据集分红酒和白酒两个文件,字段结构一致,共有 12 列。前 11 列是葡萄酒的理化检测指标,最后一列quality是品酒师给出的质量打分(3 到 8 分)。表 1 列出字段含义,便于后面做特征分析时对照。
| 字段名 | 含义 | 对建模的典型作用 |
|---|---|---|
| fixed acidity | 固定酸度(酒石酸为主) | 酸度类特征,影响口感 |
| volatile acidity | 挥发性酸度(醋酸为主) | 过高会产生醋味,与质量负相关 |
| citric acid | 柠檬酸含量 | 与固定酸度配合,起清新作用 |
| residual sugar | 残糖含量 | 甜型酒与干型酒的区分 |
| chlorides | 氯化物(盐分)含量 | 过高可能是不良工艺 |
| free sulfur dioxide | 游离二氧化硫 | 防腐指标,有阈值限制 |
| total sulfur dioxide | 总二氧化硫 | 游离加结合态之和 |
| density | 密度 | 与糖分和酒精含量高度相关 |
| pH | pH 值 | 酸度强弱的直接度量 |
| sulphates | 硫酸盐含量 | 与发酵工艺相关 |
| alcohol | 酒精体积百分比 | 通常与质量正相关 |
| quality | 质量分数(3–8) | 目标变量 |
红葡萄酒 1599 条记录,白葡萄酒 4898 条,两者在残糖、氯化物、二氧化硫等指标的分布上差异明显。常见做法是分开建模或至少单独做统计分析,我的习惯是先把两个文件都读进内存,体检后对比分布差异,再决定后续建模策略。
2.2 用 Pandas 快速体检:描述性统计与缺失值检查
拿到数据的第一步不是直接建模,而是先跑describe()看整体分布,顺便确认有没有缺失值。分隔符是分号而不是逗号,read_csv里要指定sep=';',这是新手最容易卡住的地方。
import pandas as pd # 读取红酒和白酒数据,注意分隔符是分号 red = pd.read_csv('winequality-red.csv', sep=';') white = pd.read_csv('winequality-white.csv', sep=';') print(red.shape, white.shape) # 描述性统计转置后查看,连同缺失值计数一起输出 stats = red.describe().T stats['missing'] = red.isnull().sum() print(stats)这段代码里describe().T会把统计指标转成行,方便逐字段查看均值、标准差、最小值、四分位数。isnull().sum()返回每列缺失值数量,这里输出全为 0——这个数据集很干净,不需要做缺失值填充。如果换成其他脏数据,看到非零缺失值时,先判断缺失机制:随机缺失可以直接删除或均值填充,非随机缺失要慎重,不能无脑删行。
参数上要注意一个细节:describe()默认只统计数值列。这份数据恰好全是数值,如果以后做其他项目遇到文本特征,需要先用select_dtypes过滤,再单独对文本字段做频次统计。
2.3 分布与相关性速览:一张热力图定建模基调
数据体检的第二部分是可视化。质量分数是离散的 3 到 8 分,用sns.countplot看分布;理化指标之间则用相关性热力图,快速定位强相关特征对。
import matplotlib.pyplot as plt import seaborn as sns # 质量分数分布对比 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) sns.countplot(x='quality', data=red, ax=axes[0], color='crimson') axes[0].set_title('Red Wine Quality Distribution') sns.countplot(x='quality', data=white, ax=axes[1], color='gold') axes[1].set_title('White Wine Quality Distribution') plt.tight_layout() plt.show() # 相关性热力图(以红酒为例) corr = red.corr() plt.figure(figsize=(10, 8)) sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdYlBu_r', linewidths=0.5, cbar_kws={'label': 'Pearson Correlation'}) plt.title('Red Wine Feature Correlation Heatmap') plt.show()这段代码用了两个 Seaborn 函数:countplot画离散变量频数,heatmap画相关矩阵。annot=True会在格子里显示相关系数数值,fmt='.2f'保留两位小数。
运行时注意两点:第一,两个子图的配色是故意区分的,方便在报告中直接对比;第二,相关系数超过 0.7 的特征对(比如 density 和 residual sugar、alcohol 之间)要特别留意,它们会带来多重共线性问题。
红白葡萄酒的质量分布形态不同,白葡萄酒 6 分的样本占比更高,红葡萄酒 5 分和 6 分双峰。这种分布差异会直接影响后续目标变量切分策略和模型评估方式,下一章详细展开。
3. 特征工程决定分数上限:理化指标到质量线索的三步转换
3.1 目标变量重编码:二分类还是多分类
原始质量分数是 3 到 8 的整数,直接做回归预测,输出会是连续的,不好解释;直接做多分类,类别间有天然的序数关系,普通分类器又忽略了这个约束。大作业里最常见且稳妥的做法是转成二分类:6.5 分以上算“优质”,否则算“普通”,这样业务含义清晰,模型评估也直观。
def label_quality(score): """质量分数 >= 7 视为优质,否则视为普通""" if score >= 7: return 1 else: return 0 red['good'] = red['quality'].apply(label_quality) white['good'] = white['quality'].apply(label_quality) # 查看正负样本比例 print(red['good'].value_counts(normalize=True)) print(white['good'].value_counts(normalize=True))切分阈值定在 7 而不是 6 或 8,是因为 6 分占了样本的大头,如果把 6 分划到优质类,绝大多数样本都成了正样本,模型学不到区分度;切在 8 分则正样本太少,类别不均衡会很严重。用normalize=True直接看比例,如果优质酒占比不到 15%,后面就要考虑用分层采样或类别权重处理不均衡。
如果想让作业更有层次,也可以做三分级(低/中/高),但多分类会引入类别间顺序关系的问题,普通模型不擅长利用这种序数信息。我的建议是:主体用二分类,报告里可提一句“多分类作为扩展实验”,体现出思考深度就够了。
3.2 标准化与离群值处理:不同特征差异化处理
葡萄酒理化指标的量纲差异很大:酒精含量在 8 到 15 之间,二氧化硫总量动辄几十到几百,pH 值只有 3 到 4。如果不做标准化,逻辑回归这类依赖特征尺度的模型会把注意力全放在数值大的特征上。常用做法是StandardScaler,让每个特征均值归 0、方差归 1。
需要强调的是标准化要在切分训练集和测试集之后做,具体原因放在第五章避坑里详细讲,这里是正确顺序:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler features = red.drop(['quality', 'good'], axis=1) target = red['good'] # stratify=y 让切分前后正负样本比例保持一致 X_train, X_test, y_train, y_test = train_test_split( features, target, test_size=0.2, random_state=42, stratify=target ) # 先 fit 训练集,再 transform 测试集 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) print(X_train_scaled.shape, X_test_scaled.shape)这里stratify=target是处理类别不均衡最基础的手段:如果不加,切分可能让训练集里优质样本比例失真;加了之后,训练集和测试集的正负比例都与原始数据一致。
离群值方面,先看describe()的最大值和 75% 分位。如果某个指标的最大值远高于 75% 分位,大概率存在离群样本。处理方式有两种:一是用 IQR 法(四分位距)界定离群边界再截断,二是保留但依赖树模型对离群值的鲁棒性。逻辑回归对离群值敏感,随机森林不敏感,所以先跑逻辑回归时截断离群值,跑随机森林时保留,在报告中形成对比。
3.3 特征筛选:方差阈值与相关性去重
数据只有 11 个特征,不算高维,但特征筛选仍然值得做。一是去掉方差过低的特征,它们几乎没有区分能力;二是处理高相关特征对,避免冗余信息干扰系数解释。常见的做法是先用VarianceThreshold过滤低方差特征,再看相关性矩阵手动去重。
from sklearn.feature_selection import VarianceThreshold # 对标准化后的特征做低方差过滤 selector = VarianceThreshold(threshold=0.05) X_train_selected = selector.fit_transform(X_train_scaled) X_test_selected = selector.transform(X_test_scaled) # 查看被保留的特征索引 print('保留的特征索引:', selector.get_support(indices=True))阈值 0.05 的含义是:特征方差低于 0.05 的会被移除。这里用的是标准化后的数据,方差等于 1 是平均水平,0.05 是非常低的区分度阈值。实际调试时,如果保留的特征数太少,把阈值调低;如果目的是精简模型,调高一些。
相关性去重更依赖业务判断。比如 density 与 residual sugar、alcohol 的相关系数都在 0.6 以上,可以把 density 删掉,保留后两者,因为残糖和酒精有明确业务含义,密度只是衍生指标。这样处理后,模型输入从 11 维降到 9 维左右,解释性更强,还避开了共线性对逻辑回归系数的干扰。
4. 模型训练与调参:逻辑回归到随机森林的对比实验设计
4.1 基线模型:用逻辑回归跑通完整流程
建模的第一个模型建议用逻辑回归,原因很简单:训练速度快、结果可解释、对线性关系能直接给出系数。跑通整个流程,再上随机森林做非线性提升。
from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix # 初始化逻辑回归,max_iter 调高避免收敛警告 lr = LogisticRegression(max_iter=1000, random_state=42) lr.fit(X_train_scaled, y_train) y_pred = lr.predict(X_test_scaled) y_proba = lr.predict_proba(X_test_scaled)[:, 1] print(classification_report(y_test, y_pred))max_iter=1000是必须调的参数。逻辑回归默认 100 次迭代,标准化后的高维数据可能不收敛,触发 ConvergenceWarning;调到 1000 能规避。predict_proba输出每个样本属于优质酒的概率,后面画 ROC 曲线或者调阈值时会用到。
classification_report输出精确率、召回率、F1 值。此处正负样本不均衡,只看 accuracy 毫无意义——模型一直预测“普通类”就能拿到 85% 以上的准确率,但召回率是 0,必须综合看 F1 和混淆矩阵。
4.2 随机森林与网格搜索:三个必调参数
随机森林是这种量级数据集上最稳的模型,不需要复杂调参就能超过逻辑回归。但“不用调参”是指默认参数可用,想逼近效果上限还是得做网格搜索。三个关键参数是n_estimators、max_depth、min_samples_split。
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [None, 10, 15, 20], 'min_samples_split': [2, 5, 10] } rf = RandomForestClassifier(random_state=42, n_jobs=-1) grid = GridSearchCV(rf, param_grid, cv=5, scoring='f1', n_jobs=-1) grid.fit(X_train_scaled, y_train) print('最优参数:', grid.best_params_) print('最优 F1:', grid.best_score_) best_rf = grid.best_estimator_ y_pred_rf = best_rf.predict(X_test_scaled)n_jobs=-1让网格搜索并行跑满所有 CPU 核心,数据量不大,几分钟能出结果。scoring='f1'这里特意选 F1 而非 accuracy,理由和上一节一致:类别不均衡场景下 F1 更能反映模型质量。
参数说明:n_estimators是决策树数量,从 100 加到 300 收益递减,起步用 100 够了;max_depth=None表示树不限制深度,这在小数据集上容易过拟合,网格搜索会在 None 和有限深度之间权衡;min_samples_split控制内部节点再划分所需的最小样本数,调大可以抑制过拟合。这三组参数组合共 36 种,5 折交叉验证就是 180 次训练,在这个数据集上完全跑得动。
4.3 混淆矩阵与 ROC 曲线:评估指标的选择逻辑
大作业里只输出一行accuracy_score是典型扣分项。正确的做法是输出混淆矩阵、精度/召回率、ROC 曲线,并且解释每个指标的含义。
from sklearn.metrics import roc_curve, auc cm = confusion_matrix(y_test, y_pred_rf) print('混淆矩阵:\n', cm) # ROC 曲线数据 fpr, tpr, thresholds = roc_curve(y_test, y_proba_rf) roc_auc = auc(fpr, tpr) print(f'AUC = {roc_auc:.3f}')混淆矩阵的四象限能直接看出错误类型:把普通酒误判为优质酒(假阳性)和把优质酒误判为普通酒(假阴性),业务代价不同。品酒检测场景里,假阳性的代价是让劣质酒流入市场,假阴性的代价是浪费优质酒,两者都不是零成本,所以要权衡阈值。
ROC 曲线的横轴是假阳性率,纵轴是真阳性率,AUC 越接近 1 越好。如果 AUC 低于 0.8,说明特征对质量的解释力不够,回到特征工程找问题;如果高于 0.9,要警惕是不是发生了数据泄漏,检查特征里有没有混入目标相关信息。
5. 葡萄酒质量分析的避坑指南:五个让模型翻车的隐蔽错误
5.1 数据泄漏:标准化放在切分之前
现象:训练集交叉验证分数很高,但测试集分数突然掉一截;或者发现 AUC 跑到 0.99 这种不正常的数值。
原因:在train_test_split之前对整个数据集做fit_transform,缩放器会“看见”测试集的均值和方差,等于把测试集信息提前透露给了模型。这不是推导过程泄漏,但对模型性能评估是致命的—相当于一个学生提前看了一部分考卷答案,模拟考分数虚高,真考就崩。
解决:严格分两步。先用训练集fit_transform,再用同一缩放器对测试集只做transform。常见做法是把数据切分、缩放封装进sklearn.pipeline,让 pipeline 在每一折交叉验证里自动用训练折拟合缩放器,测试折只被转换。这样流程上杜绝了泄漏的可能。
5.2 类别不均衡:模型只会说“普通酒”
现象:分类报告里“普通类”精确率很高,召回率也很高;“优质类”召回率不到 0.2。整个模型变成了一个复读机——永远输出多数类。
原因:普通酒占比 85% 以上,模型发现全猜“普通”也有 85% 的准确率,于是偷懒了。分类器默认优化的是准确率,不均衡数据下准确率是骗人的。
解决:三件事分层做。第一,切分时用stratify保证训练集和测试集的比例一致;第二,模型层面给少数类加权重,随机森林里设class_weight='balanced',让损失函数对少数类分类错误的惩罚更大;第三,评估时看 F1 和 AUC,不看准确率。实在还不够,可以用imblearn库的 SMOTE 过采样合成少数类样本,但要注意只对训练集做,测试集必须保持原始分布。
5.3 过拟合:随机森林的 max_depth 不是越大越好
现象:训练集上 F1 接近 0.95,测试集只有 0.6 到 0.7;网格搜索结果里max_depth=None且min_samples_split=2时分数最高,但你深知这不对劲。
原因:max_depth=None让每棵树长到叶子节点只剩一个样本,训练集完全被记住,泛化能力自然差。小数据集上这种表现尤其明显,因为树的深度上限由样本量决定,样本太少时树容易把噪声也学进去。
解决:网格搜索时把max_depth和min_samples_split的候选值范围拉开,引导模型选择正则化更强的组合。我的经验是max_depth从 5 到 15 之间搜,min_samples_split从 5 到 20 之间搜,min_samples_leaf也值得加入。最终选出来的模型可能训练分数略降,但测试分数更稳,这才是泛化能力。
5.4 可视化翻车:离散变量画错了图
现象:报告里的质量分数分布图是一条条竖线,看不出分布形态;画图时 x 轴标签挤成一团,密密麻麻看不清。
原因:把离散的质量分数当成连续变量画了直方图,或者没处理横坐标密度问题。plt.hist默认会对整数数据分箱,分箱边界在整数之间导致图形错乱。
解决:离散变量用countplot而不是直方图;坐标轴刻度密度过高时,用plt.xticks(rotation=45)旋转标签,或者手动指定plt.xticks(np.unique(data))只显示出现的整数分数。如果你用的数据中出现了横坐标太密集的问题,多半是把连续变量也画成了 bar 图,此时回到histplot加bins参数控制分箱数。画图前先问自己:这个变量是能取小数还是只能取整数,答案决定用哪种图。
5.5 结果不可复现:随机种子与版本锁定的教训
现象:换一台机器跑同一份代码,ROC 曲线数值不同;同一个人在同一个环境里跑两次,交叉验证结果都不一样。
原因:随机森林和交叉验证都有随机过程。train_test_split和RandomForestClassifier不设随机种子,每次切分和采样都不一样;GridSearchCV的交叉验证也带随机性。这是让作业报告被质疑数据造假的最常见原因。
解决:在所有可能引入随机性的地方显式设置random_state=42,把 42 定义成一个常量放代码顶部,不要散落各个函数里写死。交叉验证的cv用KFold(n_splits=5, shuffle=True, random_state=42)替代cv=5,因为在GridSearchCV里,cv=5默认用不置乱的折切分,结果稳定但不够随机;加shuffle=True后更需要随机种子来锚定。环境层面,在报告开头注明 Python 版本和关键库版本(sklearn、pandas、numpy),因为 sklearn 不同版本间随机森林和逻辑回归的默认行为有差异,版本不锁定,别人的复现就是玄学。
6. 让大作业从“能跑”到“高分”:报告结构与可视化技巧
报告得分点不在模型分数,而在讲清楚“为什么”。一份高分的数据挖掘报告,结构上至少要有这几块:问题定义(葡萄酒质量预测的业务价值)、数据理解(字段含义和分布特点)、数据准备(目标重编码、标准化、特征选择)、建模实验(基线到优化模型的演进)、评估与结论(业务建议和局限)。每一块配合可视化和代码结果,避免大段贴代码却不解释。
可视化上,几个容易出效果的小技巧。第一,报告里所有图表配色统一,用同一套色系,推荐 Seaborn 的官方主题,别一图一个色号;第二,特征重要性排序图(来自随机森林的feature_importances_)能直观展示“酒精含量是预测质量最重要的指标”,这比放一个大段文字说明有力得多;第三,模型对比用表格形式呈现,列出逻辑回归和随机森林在准确率、F1、AUC 三个指标上的数值,一眼看出模型演进的效果。这可以画一个简版的表格:
| 模型 | 准确率 | F1 | AUC |
|---|---|---|---|
| 逻辑回归 | 0.87 | 0.56 | 0.89 |
| 随机森林(调参前) | 0.90 | 0.62 | 0.92 |
| 随机森林(网格搜索后) | 0.91 | 0.66 | 0.93 |
数值是示意,实际以你的运行结果为准。图表要有标题、坐标轴标签、图例,这些细节是阅卷老师最看重的。
我自己的教训是:第一个版本的报告写了整整五页代码,每段代码后面没有一句话解释,老师直接批注“这不是报告,是脚本合集”。后来血的教训总结出一条习惯——每张图、每段代码后面,用两三句话解释“从结果里看到了什么、这个发现对后续步骤有什么影响”,把报告写成一个决策链,而不是代码流水账。这个习惯让我后来的每一个数据分析项目都少走了很多弯路,也希望帮到你。
本文还有配套的精品资源,点击获取