☰
逻辑回归在car-evaluation数据集上的完整建模实战:从编码到阈值调优
2026/10/17 11:14:12 网站建设 项目流程

第一次接触car-evaluation这个数据集的时候,我差点没把它当回事:不到两千行,所有特征都是low / med / high这种字符串,看起来跟机器学习的“高级感”完全不沾边。但就是这样一个入门级数据集,配合逻辑回归这个老牌模型,反而把分类任务里最重要的一堆概念全带出来了——数据不平衡、特征编码、概率阈值、模型评估指标。这篇博文我就用这个组合完整走一遍,讲讲我的建模思路、踩过的坑,以及一套可以直接照抄的实操代码。不管你是刚学 scikit-learn 的新手,还是想快速找回分类建模手感的老手,这个项目都值得花半小时跑一遍。

1. 先认识数据:car-evaluation 不是一个普通的车辆评估表

1.1 六个属性到底在说什么

car-evaluation来自 UCI 数据库,全称是 Car Evaluation Data Set。它记录了人们对一款车辆的综合评估结果,特征是六个维度的信息:

属性含义取值
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
label评估结论unacc, acc, good, vgood

数据集一共 1728 行,六维特征的取值组合基本被枚举了个遍。换句话说,每一条样本都代表一种“车辆配置画像”,最终标签是这辆车是否值得接受。

新手容易忽略的一个关键点:这些特征没有一个是真正的数值型连续变量。doors里的2、3、4看起来像数字,但它实际是离散类别,5more这种取值直接告诉你不能用数学大小去比较。这决定了后续处理必须走编码路线,不能直接丢给模型。

1.2 类别分布和它带来的第一个坑

先看一眼标签分布:

import pandas as pd df = pd.read_csv("car.data", header=None) df.columns = ["buying", "maint", "doors", "persons", "lug_boot", "safety", "label"] print(df["label"].value_counts())

输出大概是:

unacc 1210 acc 384 good 69 vgood 65

四个类别里,unacc(不可接受)占了 70% 左右,而good和vgood加起来才 8%。如果你直接拿原始四个类别去训练一个多分类逻辑回归,模型会把几乎所有样本都预测成多数类unacc,准确率看着有七成,实际上少数类一个都识别不出来。

这个数据集之所以适合做入门项目,恰恰是因为它的“坑”非常典型:真实业务里的分类问题很少是均匀分布的,多数类偏置、少数类被吞掉是家常便饭。car-evaluation把这个问题用一个小数据集暴露得明明白白。

2. 建模思路:把“四选一”改成“行或不行”

2.1 原始标注的业务含义

四档标签不是简单的有序递进,它们背后对应的是不同的决策场景:

  • unacc:这辆车不可接受,直接放弃
  • acc:可接受,属于“还行,能凑合”
  • good:不错,有吸引力
  • vgood:很好,强烈推荐

但在现实里,如果你是一个二手车收购方,或者给客户做购车推荐,真正有意义的判断往往是“这一单要不要做”。把四档压缩成两档,既贴合业务,又能绕开多数类失衡带来的问题。

2.2 二分类标签怎么定义

我试过两种切法:

方案A:unacc视为 0(不推荐),其余acc/good/vgood视为 1(推荐)。

方案B:unacc/acc视为 0,good/vgood视为 1。

表面上看方案B更贴近“好车筛选”,但实际跑下来,正样本只有 134 条(good + vgood),负样本却有 1594 条,逻辑回归几乎学不到什么有效信号。而方案A的正样本是 518 条,负样本 1210 条,比例大约 1 : 2.3,虽然还是偏,但在逻辑回归可接受的范围内。

所以我最后采用方案A:任务定义成“这辆车值不值得推荐”。这也更好向非技术方解释:正类就是“可接受”,负类就是“不可接受”。

3. 逻辑回归在这个数据上是怎么工作的

3.1 sigmoid 和决策边界的一句话解释

逻辑回归的核心不复杂。模型先算一个线性加权和:

z = w1*x1 + w2*x2 + ... + wn*xn + b

然后通过 sigmoid 函数把它压到 0 到 1 之间:

p = 1 / (1 + e^(-z))

这个p就是模型判断样本属于正类的概率。默认情况下,p >= 0.5判成正类,p < 0.5判成负类,0.5就是决策边界。

为什么在这个项目里特别值得提概率?因为当你面对不平衡数据时,默认的 0.5 阈值往往不是最优的。后面实操部分我会演示,通过predict_proba拿到概率后,你可以手动把阈值调到 0.3 或 0.7,配合业务需要换取不同的精确率和召回率。这种灵活度是决策树、SVM 等模型无法直接给你的。

3.2 为什么必须先做特征编码

逻辑回归内部做的是线性加权运算,要求输入是数值。而car-evaluation的六个属性全是字符串,所以必须编码。

这里有两种常见选择:

OrdinalEncoder会给每个类别分配一个整数,比如low=0, med=1, high=2。它的问题是强加了一个并不存在的“等距关系”,把low和high的差距等同于med和high的差距,容易误导模型。

我推荐用OneHotEncoder,把每个取值拆成一个独立的 0/1 维度。比如safety这一列会变成safety_low、safety_med、safety_high三列。这样做的好处是模型不会错误理解类别之间的顺序,坏处是维度会膨胀:六个原始特征,展开后变成4 + 4 + 4 + 3 + 3 + 3 = 21维。

不过 21 维对逻辑回归来说完全小意思,稀疏矩阵存储也毫无压力,所以不用犹豫。

4. 实操全过程:数据加载、编码、训练、评估

4.1 加载数据并确认分布

先把数据读进来,确认列名和分布:

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix df = pd.read_csv("car.data", header=None) df.columns = ["buying", "maint", "doors", "persons", "lug_boot", "safety", "label"] print(df.shape) print(df["label"].value_counts())

这里有第一个小坑:UCI 的原始文件没有表头,pd.read_csv默认会把第一行当成列名,所以必须传header=None,再手动指定列名。我见过不少同学没写header=None,结果发现数据少了一行,列名变成了第一行数据。

4.2 独热编码与标签二值化

X = df.drop(columns=["label"]) y = (df["label"] != "unacc").astype("int") # 正类=1:acc/good/vgood,负类=0:unacc encoder = OneHotEncoder(handle_unknown="ignore") X_encoded = encoder.fit_transform(X) print(X_encoded.shape) # 输出 (1728, 21)

handle_unknown="ignore"的作用是:如果未来新数据里出现了训练时没见过的类别,编码器不会报错,而是直接置为全 0。数据集本身没有未知取值,但这一行对后续把模型部署到新数据上很重要。

4.3 划分训练集和测试集

X_train, X_test, y_train, y_test = train_test_split( X_encoded, y, test_size=0.2, random_state=42, stratify=y )

stratify=y值得单独说一下。如果不加这个参数,随机划分可能在测试集里让某个类别的比例失衡。对于本来就偏斜的数据,这会直接导致测试集指标波动很大。加上stratify后,训练集和测试集都会保持约 70/30 的正负比例。

4.4 训练逻辑回归

clf = LogisticRegression(max_iter=1000, C=1.0) clf.fit(X_train, y_train)

max_iter是新手最容易报错的地方。低版本或默认参数下,如果迭代次数不够,你会看到ConvergenceWarning。这里直接设成 1000,避免训练过程还没收敛就停下来。C=1.0是默认的正则化强度,在 21 维特征的小规模数据上不用动它。

训练完成后看报告:

train_pred = clf.predict(X_test) print(classification_report(y_test, train_pred))

我跑下来的结果大致是:

precision recall f1-score support 0 0.89 0.96 0.93 242 1 0.90 0.76 0.82 104 accuracy 0.89 346

对比一下,如果不做任何处理直接四分类硬跑,逻辑回归也能上 0.7 的准确率,但good和vgood的召回率几乎是 0。二分类方案把正类的 F1 拉到 0.8 以上,这才是真正有用的模型。

4.5 用混淆矩阵拆开看错误

光看准确率会自我麻痹,必须把混淆矩阵打出来:

print(confusion_matrix(y_test, train_pred))

输出类似:

[[232 10] [ 25 79]]

四种格子分别代表:

  • 左上角:实际不可接受,预测不可接受,数量 232,没问题
  • 右上角:实际不可接受,预测可接受,数量 10,也就是说有 10 辆车被“错放行”了
  • 左下角:实际可接受,预测不可接受,数量 25,这是最要命的误判,好车被漏掉
  • 右下角:实际可接受,预测可接受,数量 79,正确

对业务而言,左下角和右上角完全是两种成本。如果你做的是二手车筛选,漏掉一辆好车(左下角)可能损失一单利润,而错收一辆差车(右上角)可能压库存亏更多。到底是压低哪边的错误,取决于业务目标,这也是下一步调阈值的原因。

4.6 调整概率阈值换取不同效果

逻辑回归真正好用的地方在于,你可以绕过predict,直接取得概率:

y_proba = clf.predict_proba(X_test)[:, 1]

然后手动设置阈值:

y_pred_30 = (y_proba >= 0.3).astype("int") y_pred_70 = (y_proba >= 0.7).astype("int")

阈值向下调到 0.3,会放行更多样本,召回率明显上升,但精确率会下降;阈值向上调到 0.7,精确率上升,但可能漏掉不少边缘正样本。这就是精确率和召回率之间最直白的 trade-off。没有绝对正确的阈值,只有适合业务场景的阈值。比如在这个数据集里,如果你希望“宁可多推荐几辆,也别漏掉好车”,0.3 或 0.4 更合理;如果你想“推荐出去的车尽量靠谱”,0.6 左右更稳妥。

5. 常见问题与排错技巧

5.1 类别不平衡导致的“假准确率”

很多人在这个数据集上第一次体会到:准确率 0.7,模型却几乎没有任何实用价值。原因就是全预测成unacc也能拿到 70% 准确率。

这时候必须看分类报告中的精确率、召回率、F1,特别是少数类的召回率。如果模型把所有样本都推给多数类,召回率会惨不忍睹。二分类加阈值调整正是应对这种局面的常规武器。

5.2 四分类直接硬跑为什么效果更差

用LogisticRegression跑四分类并不是不行,sklearn 默认会启用手写 softmax 的多分类模式。但问题是:四个类别重叠度很高,good和vgood之间的边界本来就模糊,线性分类器很难画出足够好的分界面,再加上样本极少,模型很容易把这两个少数类彻底忽略。

把问题简化为二分类,本质上是让模型先回答“能不能接受”,再去处理“能接受到什么程度”这种细粒度问题。后者我个人建议留给规则或者更复杂的模型去处理。

5.3 不收敛的警告怎么处理

报错信息一般是ConvergenceWarning: lbfgs failed to converge。原因很简单:默认max_iter=100,数据维度是稀疏矩阵,迭代不够就停了。直接把max_iter调到 1000 或 2000 就行,训练时间不会增加多少。

在这一步还要注意:如果换成连续数值特征的数据集,比如回归问题里的身高、体重,逻辑回归需要做标准化,否则量纲差异会让模型收敛很慢。而car-evaluation全部是独热编码后的 0/1 特征,天然同尺度,不需要标准化。

5.4 怎么从系数倒推属性重要性

逻辑回归的优势之一是系数可解释。训练完成后:

feature_names = encoder.get_feature_names_out() coef = clf.coef_[0] for name, c in sorted(zip(feature_names, coef), key=lambda x: -abs(x[1])): print(f"{name}: {c:.2f}")

我跑出来比较明显的规律是:safety_high的系数显著为正,safety_low显著为负;persons_more同样正向拉高推荐概率。这符合直觉:安全性高、载人能力强,更容易达到“可接受”标准。

需要提醒的是,系数只能说明“在其他特征不变时”该特征对概率的影响方向,不能当成严格因果关系。尤其当特征间存在相关性时,系数会互相干扰。

一个真实的项目收尾心得

做完这个组合项目,我最大的收获不是记住了某个准确率数字,而是彻底搞懂了“先定义好正样本,再去调模型”有多重要。car-evaluation表面上是四分类,但如果你直接四分类硬跑,大概率陷入多数类偏置;一旦把它压缩成“是否推荐”的二分类,再结合概率阈值调整,模型立刻变得可解释、可落地。后续你可以很自然地把流程扩展成Pipeline封装,或者尝试梯度提升树、随机森林来对比效果。但无论怎么扩展,先想清楚“正样本对业务意味着什么”,永远比调参更值得花时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询