第一次接触car-evaluation这个数据集的时候我差点没把它当回事不到两千行所有特征都是low / med / high这种字符串看起来跟机器学习的“高级感”完全不沾边。但就是这样一个入门级数据集配合逻辑回归这个老牌模型反而把分类任务里最重要的一堆概念全带出来了——数据不平衡、特征编码、概率阈值、模型评估指标。这篇博文我就用这个组合完整走一遍讲讲我的建模思路、踩过的坑以及一套可以直接照抄的实操代码。不管你是刚学 scikit-learn 的新手还是想快速找回分类建模手感的老手这个项目都值得花半小时跑一遍。1. 先认识数据car-evaluation 不是一个普通的车辆评估表1.1 六个属性到底在说什么car-evaluation来自 UCI 数据库全称是 Car Evaluation Data Set。它记录了人们对一款车辆的综合评估结果特征是六个维度的信息属性含义取值buying购买价格vhigh, high, med, lowmaint维护费用vhigh, high, med, lowdoors车门数量2, 3, 4, 5morepersons可载人数2, 4, morelug_boot后备箱大小small, med, bigsafety安全性low, med, highlabel评估结论unacc, acc, good, vgood数据集一共 1728 行六维特征的取值组合基本被枚举了个遍。换句话说每一条样本都代表一种“车辆配置画像”最终标签是这辆车是否值得接受。新手容易忽略的一个关键点这些特征没有一个是真正的数值型连续变量。doors里的2、3、4看起来像数字但它实际是离散类别5more这种取值直接告诉你不能用数学大小去比较。这决定了后续处理必须走编码路线不能直接丢给模型。1.2 类别分布和它带来的第一个坑先看一眼标签分布import pandas as pd df pd.read_csv(car.data, headerNone) 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 二分类标签怎么定义我试过两种切法方案Aunacc视为 0不推荐其余acc/good/vgood视为 1推荐。方案Bunacc/acc视为 0good/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会给每个类别分配一个整数比如low0, med1, high2。它的问题是强加了一个并不存在的“等距关系”把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, headerNone) df.columns [buying, maint, doors, persons, lug_boot, safety, label] print(df.shape) print(df[label].value_counts())这里有第一个小坑UCI 的原始文件没有表头pd.read_csv默认会把第一行当成列名所以必须传headerNone再手动指定列名。我见过不少同学没写headerNone结果发现数据少了一行列名变成了第一行数据。4.2 独热编码与标签二值化X df.drop(columns[label]) y (df[label] ! unacc).astype(int) # 正类1acc/good/vgood负类0unacc encoder OneHotEncoder(handle_unknownignore) X_encoded encoder.fit_transform(X) print(X_encoded.shape) # 输出 (1728, 21)handle_unknownignore的作用是如果未来新数据里出现了训练时没见过的类别编码器不会报错而是直接置为全 0。数据集本身没有未知取值但这一行对后续把模型部署到新数据上很重要。4.3 划分训练集和测试集X_train, X_test, y_train, y_test train_test_split( X_encoded, y, test_size0.2, random_state42, stratifyy )stratifyy值得单独说一下。如果不加这个参数随机划分可能在测试集里让某个类别的比例失衡。对于本来就偏斜的数据这会直接导致测试集指标波动很大。加上stratify后训练集和测试集都会保持约 70/30 的正负比例。4.4 训练逻辑回归clf LogisticRegression(max_iter1000, C1.0) clf.fit(X_train, y_train)max_iter是新手最容易报错的地方。低版本或默认参数下如果迭代次数不够你会看到ConvergenceWarning。这里直接设成 1000避免训练过程还没收敛就停下来。C1.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_iter100数据维度是稀疏矩阵迭代不够就停了。直接把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), keylambda x: -abs(x[1])): print(f{name}: {c:.2f})我跑出来比较明显的规律是safety_high的系数显著为正safety_low显著为负persons_more同样正向拉高推荐概率。这符合直觉安全性高、载人能力强更容易达到“可接受”标准。需要提醒的是系数只能说明“在其他特征不变时”该特征对概率的影响方向不能当成严格因果关系。尤其当特征间存在相关性时系数会互相干扰。一个真实的项目收尾心得做完这个组合项目我最大的收获不是记住了某个准确率数字而是彻底搞懂了“先定义好正样本再去调模型”有多重要。car-evaluation表面上是四分类但如果你直接四分类硬跑大概率陷入多数类偏置一旦把它压缩成“是否推荐”的二分类再结合概率阈值调整模型立刻变得可解释、可落地。后续你可以很自然地把流程扩展成Pipeline封装或者尝试梯度提升树、随机森林来对比效果。但无论怎么扩展先想清楚“正样本对业务意味着什么”永远比调参更值得花时间。