☰
逻辑回归实战:类别型特征编码与不平衡分类处理全流程
2026/10/17 14:10:39 网站建设 项目流程

我最早接触 Car Evaluation 这个项目,是因为一份实验目录里躺着一个叫car-evaluation/逻辑回归的文件夹。一开始我的反应是:又一个拿公共数据集刷模型的练手题。但真正跑完一段之后,我才意识到这个数据结构非常“刁钻”——六列全是类别型变量,目标四分类里有一类占到了七成,逻辑回归这个线性模型在里面既要对抗数量不均,又要处理低区分度的少数类。这篇就把我从读数据、做编码、训练调参到看混淆矩阵的完整过程写下来,包括那些只在实操中才能碰见的坑,给刚开始用逻辑回归做表格分类的同学当一份参考。

1. 为什么拿"汽车评估"练逻辑回归:这个数据集没那么简单

1.1 一条样本就是一个购车决策:底层结构拆解

Car Evaluation 这个数据集不需要做特征工程就已经很贴近真实场景了,每一条记录代表一辆车的六个属性,最终标记这辆车“可不可接受”。我第一次把这个表展开看的时候,发现它像极了现实中售车顾问评分卡的样子:买价、维护成本、车门数、可载人数、后备箱尺寸、安全性,一个不多一个不少。

具体来说,六个输入特征以及它们的取值范围是这样的:

特征含义候选取值
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

目标变量是四档有序评估:unacc(不可接受)、acc(可接受)、good(好)、vgood(很好)。你可以把每个特征想象成一位评审打出的分项分数,四个目标等级就是把分项分汇总后的最终结论。

这个结构对做逻辑回归特别友好,因为特征之间没有强相关性,每个维度都有明确的业务解释。逻辑回归输出的系数背后,可以直接读出“安全性每提升一档,可接受概率上升多少”这种可解释的结论。相比树模型给出一堆复杂的分叉规则,线性模型在业务汇报场景里更容易讲清楚。

1.2 四个目标等级与明显倾斜的类别分布

数据总量不大,只有 1728 条,没有缺失值,这份“干净”让它看起来人畜无害。但只要你统计一下目标变量的频次,就会发现事情不单纯:

  • unacc(不可接受):约 1210 条,占比 70%
  • acc(可接受):约 384 条,占比 22%
  • good(好):约 69 条,占比 4%
  • vgood(很好):约 65 条,占比 4%

四分类任务,头重脚轻,其中两个“好”类合计不到样本量的 10%。这跟许多真实业务场景很像——比如风控里的违约样本、设备故障样本,都是少数类决定业务价值。如果在训练时不做任何处理,模型只要学会把所有样本都判定成 unacc,准确率就有 70%。这个数字看着还行,但业务上完全没有意义。

所以练习逻辑回归的进阶价值恰恰体现在这里:你不仅要会调用模型,还要会处理不平衡分布、会选择合适的评估指标、会在少数类召回和整体准确率之间做取舍。这些经验放到真实项目里是直接能用的。

1.3 逻辑回归在这个数据集上的先天条件

为什么偏偏用逻辑回归,而不是一上来就堆 XGBoost?这个数据集的六列特征全是类别型,并且每个类别背后的决策机制大概率是规则性、可线性切分的。你可以把它想象成一张打分表:每项达到某个档位就累计一定分数,最终按总分划档。这种结构意味着线性边界已经能解释大部分样本,逻辑回归作为第一个模型非常合适。

我当时给自己定的目标很明确:先让线性模型在这个数据集上达到一个可用的、可解释的基线,再决定要不要升级到更复杂的模型。实际上,跑完一轮就能看到,逻辑回归在判别 unacc 和 acc 上表现很好,真正拉低分数的是 good 和 vgood 两类。这也验证了一件事:很多分类项目的性能上限不在模型复杂度,而在少数类本身的特征区分度。

2. 类别型特征怎么喂进线性模型:编码选型比调参更关键

2.1 为什么不能直接给"低价/中价/高价"标成 0/1/2

第一次跑这类数据的时候,很容易犯一个直觉性错误:把 vhigh、high、med、low 直接映射成 0、1、2、3。这样最省事,模型也能跑起来,但里面埋了一个大坑——这些数字被模型当作有实际意义的“距离”。

买价从 low 到 med 的差距是 1,从 med 到 high 的差距也是 1,从 high 到 vhigh 还是 1。但现实中的价格梯度根本不可能是等距的,高档车和次高档之间的价差,可能远大于中档和低档之间的价差。一旦你这么编码,逻辑回归就会认为它们之间的边际效应是均匀的,系数自然会被带偏。

我当时用一个简单的实验验证了这个陷阱:同一份数据,标签编码方案下训练,和独热编码方案下训练,模型系数和预测结果差异明显。原因在数据量小、特征少的时候会被放大。所以对于没有内在顺序信息、或者顺序间距不可靠的类别型特征,最稳妥的做法就是不排序、不编号,采用独热编码,让模型自己从数据里学习每个档位的影响。

2.2 One-Hot 编码后的真实维度:从 6 列到 21 列

以 Car Evaluation 为例,六个特征的取值数量加总,独热编码后大约会生成 21 到 22 个二值列。数据变成了一张很稀疏的表:每行大部分位置是 0,少数位置是 1。

有一个细节值得注意:逻辑回归对特征量纲和稀疏程度其实是比较宽容的,因为 0/1 编码的量纲天然一致,不会像连续变量那样受到“一列值范围 0 到 1,另一列值范围 0 到 10000”的影响。所以在这个项目中,我没有对独热编码后的特征再做标准化。如果你引入了某些统计特征(比如后续我提到的目标编码),那就需要重新考虑尺度问题。

独热编码之后模型系数变得更容易解释:每个系数对应“该类别取值存在”这件事。比如safety_high=1的系数很大且为正,说明高安全性对“车辆可接受”有强烈正向贡献。这是逻辑回归和树模型最大的不同,树模型给的是分裂路径,线性模型给的是直接的方向和强度。

2.3 目标变量:当成四分类还是保留顺序

Car Evaluation 的目标等级其实是有自然顺序的:unacc < acc < good < vgood。看到这个有序结构,第一反应可能是用某种有序回归模型(Cumulative Link Model)来拟合。但实际工程中,绝大多数实践者会直接把它当作普通多分类问题,用 softmax 逻辑回归或一对多方案解决。

原因有两个。第一,主流机器学习库默认提供的多分类逻辑回归不支持有序假设,手写累积连接函数的成本不低;第二,业务上我们更关心“是否能准确区分好车和差车”,而不是精确预测它在四个等级里的排序位置。直接把目标变量用 LabelEncoder 转成 0-3 的数字,然后当作回归问题处理是绝对错误的,那会让模型学到人造的“数值距离”,把 unacc 和 vgood 之间的差距等同于数字 3 和数字 0 的差距。

我在项目里的做法是:目标变量只做类别索引映射,不参与任何特征计算,模型采用多分类逻辑回归的 softmax 变体,让概率自然分布到四个类上。

3. 一对多还是 softmax:多分类逻辑回归的内部骨架

3.1 OvR:拆成两两胜出的"武斗会"

逻辑回归本质上只能回答“是/否”问题。要让一个二分类器处理四分类任务,最经典的做法是 OvR(One-vs-Rest):训练四个独立分类器,每个分类器负责区分“属于这一类”和“不属于这一类”两种阵营。预测时,把样本分别送入四个分类器,取输出置信度最高的那个作为最终结果。

这个方案在旧版机器学习库中几乎是默认行为,因为四个独立模型可以在小数据下并行训练,速度较快。同时模型的可解释性也更直观:每个类别都有一组系数,你直接看“识别 acc 的分类器”里哪些特征最重要。

但这个方案有一个隐藏缺陷:四个分类器都在“不对称”的数据上训练,比如“识别 good”的分类器,正样本只有 69 条,负样本却有 1659 条,极大不平衡会影响每个子分类器的边界学习。这也是它在少数类表现不佳的根源之一。

3.2 softmax:所有选项一起打分

另一种多分类方案是直接把最后输出层改造为 softmax,模型一次性计算出四个类别的概率,且概率值之和恒为 1。它和逻辑回归的核心思想一脉相承:线性组合加上非线性激活函数。

在 Car Evaluation 项目里我推荐用这种方案,因为它共享了所有类别的特征信息,模型参数规模也小一些。当你是四分类的时候,softmax 让每个样本的真实分布更明确——一个样本的“unacc 概率”升高,必然意味着其他三个类别的概率总和下降,这种竞争机制对类别之间有相似性的任务更有利。

实践中,如果用某个带 liblinear 求解器的库,你会发现它不支持 multinomial/softmax,这时你只能退回到 OvR。当时我的模型就出现了“softmax 比 OvR 的 macro F1 高 2-3 个百分点”的现象,所以优先选择支持多分类的求解器是值得的。

3.3 线性决策边界为什么能拿到八成以上正确率

我最初有些怀疑:一个线性模型面对四分类数据,凭什么能拿接近九成的准确率?跑完代码后我意识到,Car Evaluation 数据本身的决策边界就是接近线性的。你可以把每个特征想象成一根标尺,车辆评分是各标尺读数的加权总和,最终和阈值比较,这天然就是线性分类器擅长的事情。

另外一个辅助因素是独热编码带来的“分而治之”效果。每个取值都被单独记了一列,模型不用自己做特征交互,就能让某个强相关档位获得更高的权重。假如买价是 vhigh,那一列的显著负系数就直接压低了可接受概率,不需要组合其他特征才能表达这条规则。

当然,这不是说逻辑回归永远够用。如果换一个特征之间高度耦合的数据集,线性模型会迅速碰壁。但在 Car Evaluation 这个具体场景里,它先验证了“越简单的模型越适合当基线”这句话。

4. 用代码完整走一遍:从读到评估

4.1 数据读入与编码的一条龙代码

我先把完整流程的代码骨架放出来,再逐段解释为什么这么写。这个项目目录叫car-evaluation/逻辑回归,我习惯把数据处理、建模、评估拆成三个文件,但为了阅读方便,这里合并展示核心部分。

import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split from sklearn.preprocessing import OneHotEncoder, LabelEncoder from sklearn.compose import ColumnTransformer from sklearn.metrics import classification_report, confusion_matrix, f1_score columns = ["buying", "maint", "doors", "persons", "lug_boot", "safety", "label"] df = pd.read_csv("car.data", header=None, names=columns) X = df.drop(columns=["label"]) y = df["label"] # 先对目标变量做索引化映射 label_enc = LabelEncoder() y_encoded = label_enc.fit_transform(y) print(label_enc.classes_) # 类别型特征列 categorical_cols = ["buying", "maint", "doors", "persons", "lug_boot", "safety"] encoder = OneHotEncoder(handle_unknown="ignore", sparse_output=True) preprocessor = ColumnTransformer( transformers=[("cat", encoder, categorical_cols)] ) X_encoded = preprocessor.fit_transform(X) print("编码后特征维度:", X_encoded.shape[1]) # 直接使用多分类 softmax 逻辑回归 model = LogisticRegression( multi_class="multinomial", solver="lbfgs", max_iter=1000, class_weight=None, random_state=42 ) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores_acc = cross_val_score(model, X_encoded, y_encoded, cv=skf, scoring="accuracy") scores_f1 = cross_val_score(model, X_encoded, y_encoded, cv=skf, scoring="f1_macro") print("5折准确率: %.4f (+/- %.4f)" % (scores_acc.mean(), scores_acc.std())) print("5折 macro-F1: %.4f (+/- %.4f)" % (scores_f1.mean(), scores_f1.std()))

有几个点容易踩,单独提醒一下。car.data文件的表头原本是不存在的,所以读文件时必须自己指定header=None和列名,否则第一行会被当成数据表头丢掉。列名里尽量不要用class作为数据列名,否则后面访问属性时容易和 Python 关键字混淆,我干脆用了label。编码器OneHotEncoder使用handle_unknown="ignore"是为了防止训练集和验证集类别不一致时直接报错,属于行业习惯。交叉验证用StratifiedKFold而不是普通的KFold,主要为了让每一折的训练和验证集保持和目标变量同样的四分类比例,这对不平衡数据非常重要。

4.2 训练结果与第一个反直觉发现

用上面的基准配置跑完,得到的准确率大概是 86% 左右,但 macro-F1 却只有 0.55 到 0.60。初看准确率很漂亮,再往里看问题很大。这说明准确率在这个数据集上是一个非常会骗人的指标。

我又单独做了一次 80/20 的数据拆分,打印完整的分类报告,结果如下(数值大致是这个量级,不同随机种子会有小幅浮动):

目标类别样本数PrecisionRecallF1
unacc12100.920.970.94
acc3840.720.480.58
good690.470.030.06
vgood650.800.060.11

可以看到,unacc 的 F1 高达 0.94,acc 勉强能用,good 和 vgood 几乎处于“猜谜”状态。这直接证明了类别不平衡的影响,样本少的两个类别,模型基本没学到有效规律,全被千量级的 unacc 带跑了。

这其实也是我特别想强调的一点:f1_macro比accuracy更能暴露模型好坏。你在跑任何一个表格分类项目时都应该把这两个指标同时打印出来。如果准确率和 macro-F1 差距过大,说明少数类大概率是废掉的,必须处理不平衡问题。

4.3 调整类别权重后的实际效果

既然问题出在类别不平衡,最简单的干预方式是把class_weight调成"balanced"。sklearn 的这个参数会自动根据每个类别的样本量反比设置损失函数里的惩罚权重,样本量少的类别犯错被放得更大。

model_balanced = LogisticRegression( multi_class="multinomial", solver="lbfgs", max_iter=1000, class_weight="balanced", random_state=42 )

调整后的表现是:准确率降到 80% 左右,但 macro-F1 提升到 0.72 左右。再细看混淆矩阵,good 的召回率从 0.03 涨到 0.42,vgood 也从 0.06 涨到 0.28。这个结果就非常有意思:我们牺牲了 6 个百分点的整体准确率,换来了少数类识别能力的大幅上升。

在真实业务里,这个取舍是否值得完全取决于场景。如果任务是一万辆车里找出那几百台“很好”的车作为候选推荐,那么提升 good/vgood 的召回率远比让普通车判定得更准更有价值。反过来,如果只是做大规模初筛,保持较高的 unacc 识别准确率也说得通。所以在实际项目中不要机械追求准确率,而是先回答一个问题:哪一类的判断失误成本最高?

5. 简单模型也有自己的脾气:实测里那些说明书不会写的坑

5.1 max_iter 收敛警告与独热编码的稀疏性

第一次训练时,控制台弹出了ConvergenceWarning: Maximum number of iterations reached,我当时第一反应是调大max_iter。这个操作本身没错,但如果不理解背后的原因,将来换数据集还会卡住。

lbfgs求解器在默认 100 次迭代下收敛不了,通常是因为参数搜索空间较大,或者某个特征范围跨度偏大。在这个项目里,独热编码生成的稀疏矩阵并不会带来量纲灾难,真正拖慢收敛的是多分类 softmax 下参数矩阵比二分类大好几倍。把max_iter从默认的 100 加到 1000,问题立刻消失。

有一点值得注意,有人看到收敛警告后会顺手对稀疏矩阵做StandardScaler标准化,这反而是个坏习惯。标准化会把 0/1 稀疏矩阵变成稠密矩阵,内存翻好几倍,而且逻辑回归对 0/1 编码并不需要缩放,白忙活一场。如果将来你引入连续型特征,应该只用ColumnTransformer对连续列单独做缩放,不要动独热编码那一块。

5.2 交叉验证里最容易忽略的数据泄漏:目标编码的诱惑

我实际跑的时候,还尝试过一种“聪明”做法:按safety分组统计目标变量均值,生成一个新特征,比如“safety 为 high 时 label 为 vgood 的比例”,然后把这个统计特征丢进逻辑回归。理论上这可以给模型补充很多信息,效果也确实立竿见影——准确率直接跳到 95% 以上。

但这个结果是个陷阱。

问题出在统计特征的生成方式:如果我在整个数据集上用groupby计算出均值,再去做交叉验证,那么训练集计算出的均值已经包含了验证集的目标信息,模型在验证集上的高分开属于作弊。这个行为就叫数据泄漏。

正确做法是把统计特征的生成过程放进每一折交叉验证的内部:每一折里只用训练集计算目标均值,然后用这个均值给验证集映射。为了省事,也可以换成专门做类别目标编码的库组件,它会自动处理好交叉验证内部编码。这个小坑我印象很深,因为它在实际项目里非常隐蔽,出现“训练时 99%,上线后崩盘”的诡异现象,八成就是这一类问题。

5.3 如果还想继续往上提,先别急着换模型

很多人跑到 macro-F1 0.72 就想换随机森林或者 XGBoost。我建议先做两件事再决定。

第一,检查有没有业务规则可以直接转化为特征。比如 Car Evaluation 里有大量样本的特征组合本身就是规则的一个分支,把这些组合显式编码为新的二值特征,很多模型都能获得提升。第二,用两阶段建模思路:第一阶段先区分 unacc 和非 unacc,第二阶段在非 unacc 的子集上细分 acc / good / vgood。这样每个阶段的类别比例更均衡,逻辑回归的优势能发挥得更充分。

我当时手动实现了两阶段方案,第二阶段只对 500 多条样本建模,由于类别数从 4 降到 3,且排除掉第一阶段的困难样本干扰,整体 macro-F1 又涨了几个点。这算是一次“不增加模型复杂度但改变任务结构”的收益。

6. 我对这个小项目最后的一点体会

跑完整个car-evaluation/逻辑回归项目,我最大的收获不是学会了调用逻辑回归,而是搞清楚了评估指标、类别失衡和特征编码这三个因素如何联合影响一个模型的可信度。以后不管拿到什么表格分类任务,我都会先打印准确率和 macro-F1 的差距,再看一眼混淆矩阵里损失最大的那个类,最后再回头审视自己的特征工程有没有把某些信息泄漏到验证集里。

这种“先怀疑结果、再审视过程”的习惯,比任何一套固定代码模板都重要。逻辑回归在这个数据集上既给了我们一个优秀的基线,也逼着我们面对所有真实分类任务都会遇到的不平衡问题。如果你想找一个既能快速上手又值得反复琢磨的小项目,把这份数据从头到尾亲手跑一遍,你损失不了多少时间,却能省下未来在更大项目里填坑的不少力气。

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

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

立即咨询