简介:这是一份面向计算机相关专业学生的数据挖掘课程设计资源,以Python实现葡萄酒质量分析为完整案例,适合正在准备期末大作业或希望积累实战经验的学习者。项目围绕酒精含量、酸度、密度等变量与葡萄酒质量评定的关系展开,覆盖数据读取、清洗、特征工程、模型训练与结果评估等数据挖掘全流程,可作为分类或回归分析的练习范本。压缩包共16个文件,包含10个csv数据文件、3个py源码文件及3个txt说明文档,整体约595KB,源码经调试后下载即可运行,降低了初学者的上手门槛。目前已有83人学习关注。借助完整数据与可执行代码,读者能直接复现分析流程,理解各变量对质量的影响,并在此基础上调整模型或扩展特征,为课程答辩与后续数据分析工作提供可参考的实践模板。
1. 葡萄酒质量分析项目:一份能跑通的数据挖掘大作业到底长什么样
很多人做数据挖掘大作业时,第一步就卡住了:不是不会写代码,而是不知道一个「完整项目」应该包含哪些东西。葡萄酒质量分析这个题目之所以经典,是因为它同时踩中了数据挖掘课程里最核心的几个考点——数据清洗、特征工程、分类建模、模型评估,而且数据集本身干净、可解释性强,非常适合作为课程项目的载体。但问题也在这里:网上流传的版本大多只有一段建模代码,没有数据说明、没有参数解释、没有踩坑记录,抄下来跑不通,答辩时也讲不清。
这篇文章要讲清楚的是:一个 Python 实现的葡萄酒质量分析项目,从数据加载到模型输出,每一步该怎么做、参数为什么这么设、哪里最容易翻车。适合正在准备数据挖掘大作业的学生,也适合想用这个数据集练手特征工程的从业者。读完你应该能自己搭出一套可复现的流程,而不是复制一段跑不通的代码。
2. 数据加载与探索:先把红白葡萄酒分开看
2.1 数据集结构与你需要先确认的三件事
葡萄酒质量数据集通常以 CSV 形式提供,常见的是红葡萄酒和白葡萄酒两个文件,字段包括固定酸度、挥发性酸度、柠檬酸、残糖、氯化物、游离二氧化硫、总二氧化硫、密度、pH、硫酸盐、酒精含量,以及一个质量评分列。质量评分一般是 0 到 10 的整数,但实际分布集中在 5、6、7 三档,这一点直接决定了后面建模策略。
拿到数据后先确认三件事:第一,红白葡萄酒是否分开存储,如果合并分析,需要加一个颜色标签列;第二,质量列是数值还是字符串,有些版本会写成 "good"/"bad" 这类标签;第三,有没有缺失值,虽然这个数据集通常很干净,但不同来源的文件可能被处理过。
import pandas as pd import numpy as np # 分别加载红白葡萄酒数据,常见文件名是 winequality-red.csv 和 winequality-white.csv red = pd.read_csv('winequality-red.csv', sep=';') white = pd.read_csv('winequality-white.csv', sep=';') # 确认字段名和数据类型 print(red.columns.tolist()) print(red.dtypes) print(red.shape, white.shape) # 检查缺失值 print(red.isnull().sum()) print(white.isnull().sum()) # 查看质量评分分布,这一步决定了后面是回归还是分类 print(red['quality'].value_counts().sort_index()) print(white['quality'].value_counts().sort_index())这段代码的关键在sep=';'。很多 CSV 文件用分号分隔而不是逗号,直接read_csv会读成一整列,这是最常见的翻车点之一。质量分布打印出来后你会发现,红葡萄酒样本量大约 1599 条,白葡萄酒大约 4898 条,评分 5 和 6 占了绝大多数,7 分以上很少。这意味着如果直接做多分类,高分类别样本极少,模型会偏向多数类。
2.2 用描述统计和相关性快速判断哪些特征值得留
数据探索不是走形式,目的是决定后面保留哪些特征、怎么处理。先看描述统计,重点看量纲差异:总二氧化硫的数值可能上百,而 pH 只有 3 左右,如果不做标准化,基于距离的模型会被大数值特征主导。
# 描述统计,关注均值和标准差,判断量纲差异 desc = red.describe().T desc['range'] = desc['max'] - desc['min'] print(desc[['mean', 'std', 'min', 'max', 'range']]) # 相关性分析,看哪些特征和质量评分相关性强 corr = red.corr(numeric_only=True)['quality'].sort_values(ascending=False) print(corr) # 酒精含量和挥发性酸度通常是相关性最强的两个特征 # 可以画散点图确认,但这里先用数值判断从相关性输出里通常能看到:酒精含量与质量正相关,挥发性酸度与质量负相关,这两个特征在多数版本里都排在最前面。密度和残糖的相关性往往较弱,但不代表没用,只是线性关系不明显。这一步的产出是一个特征优先级列表,后面做特征选择时直接参考。
提示:相关性只反映线性关系,葡萄酒数据里有些特征是非线性影响,不要因为相关系数低就直接删掉,后面可以用树模型的特征重要性再验证一次。
3. 特征工程与预处理:标准化、分箱和标签处理的取舍
3.1 标准化该不该做,取决于你选什么模型
标准化不是必选项,它取决于模型类型。逻辑回归、SVM、KNN 这类基于距离或梯度的模型,必须做标准化;决策树、随机森林、XGBoost 这类基于分裂点的模型,标准化不影响结果。很多人不管什么模型都先标准化一遍,结果用树模型时白白增加计算量,还让特征解释变麻烦。
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 特征和标签分开 X = red.drop('quality', axis=1) y = red['quality'] # 先划分训练集和测试集,再做标准化,避免数据泄漏 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 标准化:只在训练集上 fit,测试集用同样的参数 transform scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)这里有两个容易错的地方。第一,fit只能在训练集上做,如果先对全量数据标准化再划分,测试集的信息就泄漏到训练过程里了,评估结果会虚高。第二,stratify=y要加上,因为质量评分分布不均,不加分层可能导致测试集里某个类别样本极少,评估指标波动很大。
3.2 质量评分怎么处理:回归、二分类还是多分类
质量评分是整数,但它本质上是有序的。直接做多分类会忽略顺序信息,做回归又会把 5 和 6 的差异当成连续值处理。常见做法有三种:
第一种,直接做多分类,把 3、4、5、6、7、8 当作独立类别。问题是高分类别样本太少,模型学不好。第二种,做二分类,把评分大于等于 7 的标为好,其余标为一般。这样类别平衡好很多,也是课程项目里最常用的做法。第三种,做回归,预测一个连续分数,再四舍五入。评估时用均方误差,但解释性差一些。
# 二分类方案:质量 >= 7 视为好酒 y_binary = (red['quality'] >= 7).astype(int) print(y_binary.value_counts()) # 如果做多分类,可以合并稀有类别 y_multi = red['quality'].apply(lambda q: q if q >= 5 else 4) print(y_multi.value_counts())我一般会先跑二分类,因为结果稳定、好解释,答辩时也容易讲清楚。如果时间充裕,再补一个多分类做对比,体现工作量。
3.3 特征构造:几个值得试的衍生特征
原始特征之外,可以构造几个有物理意义的衍生特征。比如游离二氧化硫和总二氧化硫的差值,反映结合态二氧化硫的量;酒精和密度的比值,间接反映发酵程度;酸度相关特征可以组合成总酸度。这些特征不一定都有效,但试错成本低,能体现你对数据的理解。
# 构造衍生特征 df = red.copy() df['free_so2_ratio'] = df['free sulfur dioxide'] / (df['total sulfur dioxide'] + 1) df['bound_so2'] = df['total sulfur dioxide'] - df['free sulfur dioxide'] df['alcohol_density'] = df['alcohol'] / df['density'] df['total_acid'] = df['fixed acidity'] + df['volatile acidity'] + df['citric acid'] # 检查新特征和标签的相关性 print(df[['free_so2_ratio', 'bound_so2', 'alcohol_density', 'total_acid']].corrwith(df['quality']))加 1 是为了避免除零,虽然这个数据集里总二氧化硫不会为零,但养成习惯没坏处。构造完新特征后,用corrwith快速看一遍相关性,如果新特征和标签的相关性还不如原始特征,可以考虑不加,避免维度膨胀。
4. 建模与评估:从逻辑回归到集成模型的完整对比
4.1 基线模型:逻辑回归先跑通流程
不管最后用什么模型,先跑一个逻辑回归做基线。它的好处是训练快、结果可解释、系数能直接看特征影响方向。如果逻辑回归的效果已经能接受,后面的复杂模型就是锦上添花;如果逻辑回归效果很差,说明特征或标签处理有问题,先排查再上复杂模型。
from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score # 用标准化后的数据训练逻辑回归 lr = LogisticRegression(max_iter=1000, random_state=42) lr.fit(X_train_scaled, y_train) # 预测和评估 y_pred = lr.predict(X_test_scaled) y_prob = lr.predict_proba(X_test_scaled)[:, 1] print(classification_report(y_test, y_pred)) print('AUC:', roc_auc_score(y_test, y_prob)) print(confusion_matrix(y_test, y_pred))max_iter=1000是因为默认的 100 次迭代在标准化后的数据上有时不收敛,会报收敛警告。AUC 是二分类里比准确率更可靠的指标,因为类别不平衡时准确率会虚高。混淆矩阵能看出模型是把好酒误判成一般,还是一般误判成好酒,这两个错误的代价在实际场景里不一样。
4.2 树模型和集成模型:随机森林与梯度提升的参数怎么设
树模型不需要标准化,可以直接用原始特征。随机森林的关键参数是n_estimators、max_depth和min_samples_split。n_estimators一般设 100 到 500,再多收益递减;max_depth不设的话树会完全生长,容易过拟合,可以设 10 到 20 之间试;min_samples_split控制分裂的最小样本数,设太小会过拟合,设太大会欠拟合。
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier # 随机森林,用原始特征 rf = RandomForestClassifier( n_estimators=300, max_depth=15, min_samples_split=5, min_samples_leaf=2, random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) print('RF AUC:', roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1])) # 梯度提升 gb = GradientBoostingClassifier( n_estimators=200, learning_rate=0.05, max_depth=4, random_state=42 ) gb.fit(X_train, y_train) y_pred_gb = gb.predict(X_test) print('GB AUC:', roc_auc_score(y_test, gb.predict_proba(X_test)[:, 1]))梯度提升的learning_rate和n_estimators要配合调:学习率小就需要更多棵树,学习率大就容易过拟合。0.05 配 200 棵是一个比较稳的起点。max_depth设 4 是因为梯度提升本身对深树敏感,浅树加多轮迭代通常比深树效果好。
4.3 交叉验证和超参数搜索:别只用一次划分的结果
单次训练测试划分的结果波动很大,尤其是样本量不大的时候。用交叉验证能得到更稳定的评估,再用网格搜索找最优参数。注意网格搜索要在训练集内部做交叉验证,不能碰测试集。
from sklearn.model_selection import GridSearchCV, cross_val_score # 先看随机森林的交叉验证表现 cv_scores = cross_val_score(rf, X_train, y_train, cv=5, scoring='roc_auc') print('CV AUC:', cv_scores.mean(), '+/-', cv_scores.std()) # 网格搜索找最优参数 param_grid = { 'n_estimators': [200, 300, 500], 'max_depth': [10, 15, 20], 'min_samples_split': [2, 5, 10] } grid = GridSearchCV( RandomForestClassifier(random_state=42, n_jobs=-1), param_grid, cv=5, scoring='roc_auc', n_jobs=-1 ) grid.fit(X_train, y_train) print('Best params:', grid.best_params_) print('Best CV AUC:', grid.best_score_)n_jobs=-1表示用所有 CPU 核心并行,能显著缩短搜索时间。scoring='roc_auc'是因为类别不平衡时 AUC 比准确率更可靠。搜索完拿到最优参数后,用最优模型在测试集上跑一次最终评估,这个结果才是写进报告里的。
4.4 特征重要性分析:模型告诉你哪些特征真正有用
树模型可以直接输出特征重要性,这是它比逻辑回归更好解释的地方。把重要性排序后,你会发现酒精含量、挥发性酸度、硫酸盐通常排在前列,和相关性分析的结论基本一致,但树模型能捕捉非线性关系,排序可能略有不同。
# 用最优模型的特征重要性 best_rf = grid.best_estimator_ importances = pd.Series(best_rf.feature_importances_, index=X_train.columns) print(importances.sort_values(ascending=False)) # 如果做了特征构造,把新特征也加进去重新训练再对比特征重要性不仅能帮你筛选特征,还能在报告里解释「为什么这个模型有效」。如果某个衍生特征重要性很高,说明你的特征工程有实际价值,这是加分项。
5. 避坑与排查:葡萄酒质量分析里最容易翻车的五个地方
5.1 读取 CSV 时分隔符搞错,整张表变成一列
现象:read_csv之后shape显示只有一列,或者所有字段挤在一起。原因:葡萄酒数据集的 CSV 常用分号分隔,默认逗号解析失败。解决:加sep=';',如果不确定分隔符,先用pd.read_csv('file.csv', nrows=1)看第一行原始内容,或者用sep=None, engine='python'让 pandas 自动推断。
5.2 先标准化再划分数据集,评估结果虚高
现象:测试集 AUC 高得离谱,换一组随机种子后大幅下降。原因:标准化时用了全量数据的均值和方差,测试集信息泄漏到训练过程。解决:先train_test_split,再在训练集上fit标准化器,测试集只做transform。这个顺序不能反。
5.3 质量评分直接做多分类,高分类别全预测错
现象:分类报告里 7 分和 8 分的召回率接近零。原因:高分类别样本太少,模型偏向多数类。解决:改成二分类,或者用class_weight='balanced'让模型关注少数类,也可以用 SMOTE 过采样,但要注意过采样只能在训练集上做。
5.4 逻辑回归不收敛,满屏 ConvergenceWarning
现象:训练时反复提示lbfgs failed to converge。原因:默认迭代次数不够,或者特征量纲差异大。解决:先做标准化,再把max_iter调到 1000 以上。如果还不收敛,检查是否有常数特征或高度共线的特征,删掉再试。
5.5 交叉验证用了测试集,最终评估不可信
现象:交叉验证分数和测试集分数差距很大。原因:在包含测试集的数据上做交叉验证,或者调参时反复用测试集评估。解决:交叉验证只在训练集内部做,测试集只在最终评估时用一次。调参用GridSearchCV的cv参数,它自动在训练集内部分折。
6. 把项目做得能答辩:报告结构、可视化与复现清单
项目能不能拿高分,代码只占一半,另一半是你能不能讲清楚。答辩时老师最常问的三个问题是:你为什么这么处理数据、你为什么选这个模型、你的结果怎么验证。这三个问题对应报告里的数据探索、模型对比和评估指标三部分。
可视化不用多,但要有针对性。质量分布用柱状图,特征相关性用热力图,模型对比用 ROC 曲线叠加,特征重要性用水平条形图。这四张图基本能覆盖答辩需求。ROC 曲线叠加的代码如下:
import matplotlib.pyplot as plt from sklearn.metrics import roc_curve # 三个模型的 ROC 曲线叠加 models = {'Logistic': lr, 'RandomForest': best_rf, 'GradientBoosting': gb} plt.figure(figsize=(8, 6)) for name, model in models.items(): if name == 'Logistic': prob = model.predict_proba(X_test_scaled)[:, 1] else: prob = model.predict_proba(X_test)[:, 1] fpr, tpr, _ = roc_curve(y_test, prob) auc = roc_auc_score(y_test, prob) plt.plot(fpr, tpr, label=f'{name} (AUC={auc:.3f})') plt.plot([0, 1], [0, 1], 'k--', label='Random') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.legend() plt.title('ROC Curve Comparison') plt.savefig('roc_comparison.png', dpi=150) plt.show()这段代码里注意逻辑回归用的是标准化后的测试集,树模型用的是原始测试集,因为它们的预处理需求不同。保存图片时dpi=150保证清晰度,报告里插图不会糊。
复现清单我一般会整理成一张表,放在报告附录里,方便别人照着跑:
| 步骤 | 关键操作 | 常见参数 | 检查点 |
|---|---|---|---|
| 数据加载 | read_csv | sep=';' | shape 和字段名正确 |
| 数据划分 | train_test_split | test_size=0.2, stratify=y | 类别比例一致 |
| 标准化 | StandardScaler | 只在训练集 fit | 无数据泄漏 |
| 基线模型 | LogisticRegression | max_iter=1000 | 无收敛警告 |
| 集成模型 | RandomForestClassifier | n_estimators=300 | 交叉验证稳定 |
| 评估 | roc_auc_score | 测试集只用一次 | 指标可复现 |
最后说一个我自己的习惯:每次跑完实验,把随机种子、参数、评估指标记在一个单独的文本文件里,和代码放在一起。数据挖掘大作业最怕的就是答辩前想复现某个结果却忘了当时参数怎么设的,有了这个记录,随时能翻回去。这个项目本身不难,难的是把每一步做扎实、讲清楚,希望帮到你。
本文还有配套的精品资源,点击获取