简介:一套基于汽车满意度数据集的分析与建模代码,面向机器学习初学者和数据挖掘实践者,用于掌握决策树和多种聚类算法的应用对比。包内共五个Python脚本,分别实现决策树分类、K均值、均值漂移、层次聚类和密度聚类,压缩包仅七KB,轻量易读,便于直接运行和二次修改。已有四百六十六人浏览学习,适合课程设计、毕业设计或算法实战练习。资源覆盖从数据加载、模型训练到结果评估的完整流程,并给出关键结论,决策树准确率约百分之九十五,密度聚类在该数据集上效果不佳,可帮助使用者快速理解各算法的适用场景与调参思路。代码按模型分文件组织,便于对照运行结果比较各算法差异,也可作为算法实验模板进行扩展。 最近整理机器学习练手项目,把经典的汽车满意度数据集 Car Evaluation 翻了出来。它来自 UCI,虽然只有 1728 条记录,特征也全是离散的类别型变量,但特别适合用来同时跑通“有监督”和“无监督”两条主线:决策树做满意度预测,聚类分析做评价模式分群。这篇文章就是我完整记录用 Python 从数据编码、模型训练到结果解读的过程,包含踩过的坑和调参心得。如果你刚学完 sklearn 基础,或者想找一个能练手又不会太烧脑的小项目,这篇应该能给你一份可以直接抄的作业。
1. 数据集理解与前置准备
1.1 数据集字段和业务含义
Car Evaluation 数据集的原始文件可以在 UCI 机器学习仓库下载,文件名一般是 car.data,文件里没有列名,读取时一定要手动指定。数据包含 6 个特征和 1 个目标变量,6 个特征描述了一辆车的购买成本和使用属性,目标变量是整体满意程度。各字段含义如下表。
| 字段 | 可选值 | 业务含义 |
|---|---|---|
| buying | vhigh / high / med / low | 购买价格 |
| maint | vhigh / high / med / low | 维护成本 |
| doors | 2 / 3 / 4 / 5more | 车门数量 |
| persons | 2 / 4 / more | 可载人数 |
| lug_boot | small / med / big | 后备箱大小 |
| safety | low / med / high | 安全等级 |
| class | unacc / acc / good / vgood | 满意度:不可接受 / 可接受 / 好 / 很好 |
满意度等级里 unacc 占了绝大多数,acc、good、vgood 依次减少,是一个典型的类别不平衡数据。所以整个项目不能只看准确率,一定要把混淆矩阵、宏平均 F1 拿出来看,否则容易被虚高的分数骗了。这也是我特意选这个数据集的原因之一:它字段少、语义清楚,非常适合作为学习机器学习基础流程的起点。
1.2 环境准备与依赖说明
我用的 Python 3.10,模型部分主要依赖 scikit-learn,数据操作靠 pandas,画图用 matplotlib 和 seaborn。安装直接在终端执行:
pip install pandas numpy scikit-learn matplotlib seaborn graphvizAnaconda 用户一般自带前四个库,seaborn 没有的话用 pip 装一下就行。graphviz 不仅需要 Python 包,系统层面也要安装 Graphviz 软件,否则导出决策树图片时会报 ExecutableNotFound 错误。如果只是想快速看树形图,用 sklearn 自带的 plot_tree 就够了,不装 graphviz 也能跑。
因为后面会涉及多个随机算法,我会先统一设置随机种子,保证每次运行结果一致。这里要说一句:随机种子不是随便写个 42 就完事,它决定了 KMeans 初始质心、决策树分裂时的随机行为。如果你发现别人的代码跑出来的结果和你不一样,先检查是不是随机种子没对齐。
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns random_state = 42 np.random.seed(random_state)2. 数据加载与特征工程
2.1 读取数据与基本信息
读取 car.data 时最容易踩的坑就是缺少列名。我一开始直接用pd.read_csv('car.data'),结果第一行被当成了表头,后面所有数据都错位。正确做法是指定header=None,再传入列名:
columns = ['buying', 'maint', 'doors', 'persons', 'lug_boot', 'safety', 'class'] df = pd.read_csv('car.data', header=None, names=columns) print("数据集形状:", df.shape) print(df.head()) print(df.isnull().sum()) print(df['class'].value_counts())运行结果能确认两条信息:数据集没有缺失值,形状是 1728 行 7 列。类别分布里 unacc 有 1210 条,acc 有 384 条,good 69 条,vgood 65 条。这样的分布决定了后面的处理重点:少数类 good 和 vgood 很容易被模型忽略,必须靠分层抽样和类别权重来干预。
2.2 类别特征编码:顺序映射还是 One-Hot?
数据里所有特征都是字符串类别,sklearn 的决策树和 KMeans 都不支持直接吃字符串。编码方式有两种主流选择。
第一种是 LabelEncoder 式整体编码,简单但会丢失有序语义。例如把 buying 变成 0、1、2、3 时,模型会误以为等级之间是等距的。第二种是 One-Hot 编码,不会引入顺序假设,但会增加特征维度。对于 Car Evaluation 这种只有 6 个特征的小数据,One-Hot 也可以接受。
我最终选择了手动有序映射,原因是这 6 个特征本身都有明确的高低语义:价格从低到高、安全从低到高。保留这种有序信息,对聚类分析尤为重要,因为 KMeans 计算的是欧氏距离,有序编码能让“低安全”和“高安全”的距离真正拉开,更贴近业务直觉。
def encode_car_data(df): order_maps = { 'buying': {'low': 0, 'med': 1, 'high': 2, 'vhigh': 3}, 'maint': {'low': 0, 'med': 1, 'high': 2, 'vhigh': 3}, 'doors': {'2': 0, '3': 1, '4': 2, '5more': 3}, 'persons': {'2': 0, '4': 1, 'more': 2}, 'lug_boot': {'small': 0, 'med': 1, 'big': 2}, 'safety': {'low': 0, 'med': 1, 'high': 2} } df_encoded = df.copy() for col, mapping in order_maps.items(): df_encoded[col] = df_encoded[col].map(mapping) return df_encoded df_encoded = encode_car_data(df) df_encoded['class'] = df_encoded['class'].map({'unacc': 0, 'acc': 1, 'good': 2, 'vgood': 3})注意,等到后面做决策树特征重要性时,这种映射不会影响树的分裂,因为树模型本身对特征顺序不敏感,但对聚类来说差别很大。如果不想自己做映射,也可以用 sklearn 的 OrdinalEncoder,效果是一样的。另外,建议在编码后检查一下value_counts(),防止原始数据里出现映射字典没覆盖的取值,否则 map 会产生 NaN,后面模型会报错。
3. 决策树实现满意度预测
3.1 为什么选决策树
决策树是一个非常容易解释的监督学习模型,它通过一系列“如果特征 x 小于或大于某个值”的规则,把样本不断划分成更纯的子集。在 Car Evaluation 这种小数据集上,决策树不仅能给出高准确率,还能把判断逻辑用一棵树可视化出来,这是逻辑回归和 SVM 难以直接做到的。sklearn 里的 DecisionTreeClassifier 默认使用 CART 算法,分裂准则默认是基尼系数。基尼系数比熵计算更快,在这类小数据上两者差异很小,所以我直接用默认 gini。
3.2 数据划分、训练与评估
模型训练前先划分训练集和测试集。由于类别不平衡,这里必须用stratify=y做分层抽样,保证训练集和测试集中各个类别的比例一致。同时我在模型里设置了class_weight='balanced',让少数类获得更高权重,避免模型把所有样本都猜成 unacc。
from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import classification_report, confidence_matrix, accuracy_score X = df_encoded.drop('class', axis=1) y = df_encoded['class'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=random_state, stratify=y ) model = DecisionTreeClassifier(random_state=random_state, class_weight='balanced') model.fit(X_train, y_train) y_pred = model.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=['unacc', 'acc', 'good', 'vgood']))我跑出来的准确率在 94% 左右,分类报告里 unacc 的精确率和召回率都很高,但 acc、good、vgood 的召回率波动较大。这是不平衡数据加上样本量少时很常见的现象,需要结合调参和业务需求做取舍,不一定非要追求每个类别都 90% 以上。如果你发现 confusion_matrix 里 good 和 vgood 整行都是 0,不要慌,先去确认 class_weight 是否生效,再看是否需要调整决策阈值。
3.3 决策树可视化与特征重要性
默认的训练树可能很深,直接画出来会非常拥挤。我先用限制深度到 3 的版本来观察树结构,再把特征重要性打出来:
from sklearn.tree import plot_tree plt.figure(figsize=(18, 10)) plot_tree(model, max_depth=3, feature_names=X.columns, class_names=['unacc', 'acc', 'good', 'vgood'], filled=True) plt.show() importance = pd.Series(model.feature_importances_, index=X.columns).sort_values(ascending=False) print(importance)从特征重要性排序来看,safety、buying、persons 通常会出现在前列,maint 和 lug_boot 影响相对弱一些。这也很符合常识:买车的人首先看重安全性和价格,然后是空间是否够坐,最后才是后备箱和维护费用。看树的时候,可以重点观察第一层分叉用了哪个特征,那个特征往往就是全局区分度最强的变量。
3.4 网格搜索调参
决策树需要重点调的参数主要是 max_depth 控制深度,min_samples_leaf 控制叶子节点最少样本数,还有 criterion 选择分裂准则。我用 5 折交叉验证和宏平均 F1 作为评分标准,这样能兼顾少数类:
from sklearn.model_selection import GridSearchCV param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [None, 3, 5, 7, 9], 'min_samples_leaf': [1, 3, 5] } grid = GridSearchCV( DecisionTreeClassifier(random_state=random_state, class_weight='balanced'), param_grid, cv=5, scoring='f1_macro' ) grid.fit(X_train, y_train) print("最佳参数:", grid.best_params_) print("最佳交叉验证得分:", grid.best_score_)用最优参数重新训练后,准确率未必有显著提升,但少数类的召回率通常会变好。这里最该避开的坑是max_depth=None,在数据量小的时候容易长出一棵无限深的树,训练集准确率能到 100%,测试集表现却差很多。交叉验证里的 f1_macro 是比 accuracy 更诚实的指标,尤其在 Car Evaluation 这种少数类占比极低的情况下。
4. 聚类分析汽车满意度分群
4.1 无监督聚类的定位
聚类分析在这个项目里不是用来预测满意度等级的,而是用无监督的方式,探索数据里是否天然存在几类“评价模式”。比如某类车虽然是低价格,但安全性也很低,评价可能普遍很糟糕;另一类车安全性高但价格稍高,评价却不错。这些模式如果只靠人工看 1728 行数据,很难发现,聚类可以把相近的样本归到同一个簇里,再对照真实满意度去验证簇的实际含义。
4.2 KMeans 聚类与最佳 K 值选择
KMeans 是最常用的聚类算法,它把样本分成 K 个簇,使得每个点到所属簇中心的距离平方和最小。在 Car Evaluation 这种低维小样本数据上,KMeans 速度快、结果好解释。选择 K 值我用了两个指标:惯性(inertia)肘部法则和轮廓系数。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score X_cluster = X.copy() inertia = [] silhouette = [] for k in range(2, 11): km = KMeans(n_clusters=k, random_state=random_state, n_init=10) km.fit(X_cluster) inertia.append(km.inertia_) silhouette.append(silhouette_score(X_cluster, km.labels_)) plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) plt.plot(range(2, 11), inertia, marker='o') plt.xlabel('K') plt.ylabel('Inertia') plt.subplot(1, 2, 2) plt.plot(range(2, 11), silhouette, marker='o') plt.xlabel('K') plt.ylabel('Silhouette Score') plt.tight_layout() plt.show()肘部法则看的是下降趋势拐点,轮廓系数则是越大越好。实际运行后,K=3 或 K=4 附近轮廓系数较高,惯性下降也逐渐平稳,我最终选了 4 个簇做后续分析。这里不要机械地选最大轮廓系数的 K,因为 K 越大轮廓系数越不稳定,还要结合业务可解释性。如果某个簇里面的样本特征均值非常接近,说明 K 选大了,簇之间没有明显差异。
4.3 聚类画像与满意度交叉分析
选好 K 后重新拟合,把聚类标签合并回数据框,分组看各特征均值,再和真实满意度做交叉表:
km = KMeans(n_clusters=4, random_state=random_state, n_init=10) cluster_labels = km.fit_predict(X_cluster) df_cluster = df_encoded.copy() df_cluster['cluster'] = cluster_labels print(df_cluster.groupby('cluster')[X.columns].mean()) print(pd.crosstab(df_cluster['cluster'], df['class']))聚类画像通常能清楚区分出几类车:一类是低安全低维护成本,对应质量较差的代步车,满意度几乎都是 unacc;一类是高安全高价格,对应配置较好的车,满意度多在 acc 到 good;中间还有一类是均衡型。这个结果和决策树的特征重要性是互相呼应的。画交叉表的时候,建议把列名改成可读的满意度标签,否则 0、1、2、3 堆在一起很难看出规律。
4.4 用 PCA 降维可视化簇分布
由于原始维度只有 6 个,聚类结果可以直接用 PCA 压到二维平面来观察分布:
from sklearn.decomposition import PCA pca = PCA(n_components=2, random_state=random_state) X_pca = pca.fit_transform(X_cluster) plt.figure(figsize=(10, 7)) scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=cluster_labels, cmap='viridis', alpha=0.6) plt.xlabel('PC1') plt.ylabel('PC2') plt.colorbar(scatter) plt.show()PCA 的可视化只是为了直观感受簇分离程度,不能用来解释业务维度。详细维度解释一定要回到原始特征均值表,否则很容易被降维后的坐标带偏。我在实际跑的时候发现,PC1 和 PC2 大约能保留八成左右的方差信息,所以二维图还是有一定参考性的。
5. 决策树与聚类结果联合解读
5.1 聚类标签作为额外特征
一个有意思的实验是把聚类标签当成新特征放进决策树,看看监督模型是否能从中获益。我不建议直接覆盖原特征,而是把标签加到特征矩阵里做对比:
X_aug = X.copy() X_aug['cluster'] = cluster_labels X_train_aug, X_test_aug, y_train_aug, y_test_aug = train_test_split( X_aug, y, test_size=0.3, random_state=random_state, stratify=y ) model_aug = DecisionTreeClassifier(random_state=random_state, class_weight='balanced') model_aug.fit(X_train_aug, y_train_aug) y_pred_aug = model_aug.predict(X_test_aug) print("加入聚类特征后的准确率:", accuracy_score(y_test_aug, y_pred_aug))我的实验里加入聚类特征后准确率变化不大,甚至略有下降,原因是聚类标签和原始特征高度相关,而决策树本身已经足够捕捉这些模式。但这不代表这个尝试没有意义,它可以帮你理解有监督和无监督模型的信息边界。如果你把聚类标签换成独热编码再加进去,模型可能更平稳一点,但依旧不会有质变。
5.2 特征重要性如何落地成业务结论
结合决策树的特征重要性和聚类画像,可以得到几个明确的视图:安全性和购买价格主导静态评价,维护成本在低端车里会成为压垮骆驼的最后一根稻草;载人数量和后备箱大小在决策树里往往排在后半段,说明舒适性配置不是满意度分级的首要因素。
如果把这个分析套到一个更实际的汽车评价场景中,产品团队可以优先看安全配置的缺失率,再结合聚类里的“高危群体”去定向优化。比如低安全低价格的簇,只要把安全等级从中提到高,满意度就可能从 unacc 跳到 acc,这个跳跃在决策树的可视化分叉里非常直观。再比如,结合聚类均值表你可能会发现某个簇的人均维护成本偏高,那就需要从产品策略上考虑降低维修保养费用,而不是一味加座位数。
6. 常见问题与排查技巧
6.1 数据读取类问题
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 第一行被当表头 | df.columns 出现 vhigh 等 | read_csv 加 header=None,并指定 names |
| 中文列名乱码 | 可视化时中文显示方块 | 设置 plt.rcParams['font.sans-serif']=['SimHei'] |
| 文件编码报错 | UnicodeDecodeError | 读取时指定 encoding='latin1' 或 'utf-8' |
6.2 模型效果类问题
决策树训练集 100%、测试集差,基本就是过拟合,限制 max_depth 和 min_samples_leaf 后有明显改善。分类报告里少数类全为 0,是因为类别不平衡太严重,除了 class_weight,还可以用 imbalanced-learn 的 SMOTE 做重采样,不过在这个数据集上不建议,因为少数类样本太少,合成样本可能引入噪声。
聚类结果不稳定是新手最容易困惑的地方。KMeans 初始点是随机的,所以运行结果可能不同,需要设置 random_state 和 n_init。如果多次运行后簇的归属仍然漂移,可以考虑换成 AgglomerativeClustering 层次聚类,它没有随机初始点问题,但计算复杂度会高一些。还有一点:我用的有序映射对欧氏距离有较强假设,若用 One-Hot 编码再聚类,很可能会得到不同分群,建议两种都试,选业务上更合理的。
6.3 写在最后:一点实操体会
Car Evaluation 这套数据我第一次跑的时候,以为决策树训练完就算结束了,后来把聚类的分群结果和树的可视化图放在一起看,才发现两个模型的结论如此一致:安全性和价格是满意度分级的核心变量,维护成本在低端市场里才是压垮评价的最后一根稻草。这个小项目最值得学习的地方,不是“跑通代码”,而是理解监督学习和无监督学习在同一个数据上的互补视角。后续你可以尝试把随机森林、XGBoost 搬上来做对比,或者用聚类结果做一个简单的推荐规则——比如识别出满意度大概率是 unacc 的样本,提前给出改进建议。这些都是很有意思的扩展方向。
本文还有配套的精品资源,点击获取