☰
决策树实战:从客户数据清洗到购车意向预测与营销名单落地
2026/10/21 6:28:09 网站建设 项目流程

做数据分析这几年,我接过不少类似的活儿:客户档案躺在数据库里,业务方想筛选出“最近三个月可能要买车”的人。很多人一听就想上神经网络、上大模型,其实完全没必要。决策树是这里面性价比最高的起点,训练快、能解释、还不用归一化数据,最关键的是它输出的规则能直接翻译成业务动作。这篇文章,我用一份模拟的客户购车意向数据,从数据清洗到参数调优,再到把模型规则落成营销名单,完整走一遍流程。无论你是刚接触机器学习的数据新人,还是想用模型辅助业务判断的运营同学,都可以照着复现。

1. 项目整体设计与思路拆解

1.1 业务场景与目标拆解

先交代场景。某车企准备给一款新车型做推广,销售团队手里有一批历史客户档案,字段无非是年龄、收入、贷款情况、家庭人口、通勤距离、保养记录这些。业务卡在一个问题上:未来三个月,哪些客户有购车意向?这个场景本质上是一个二分类问题:给每个客户打一个“有意向/无意向”的标签,最好还能输出一个概率值。

我习惯在动手前把业务目标拆成两个东西:一个是排序,一个是分群。排序意味着销售可以从概率最高的人开始打电话,分群则意味着团队能针对不同意向概率的客户设计不同话术。纯靠人工经验去做这件事,问题在于判断标准不稳定,换一个人就有不同的标准;决策树模型的价值,就是把“什么特征的客户更可能买车”这个经验固化成一棵看得见的树。

1.2 为什么选决策树作为第一个模型

选型这件事,我踩过不少坑。早年我也喜欢一上来就上复杂模型,结果调参调得头大,业务方还听不懂,最后模型再好也落不了地。后来我总结出一个原则:起步模型越简单越好,决策树刚好符合。

它有三个别人替代不了的优势。第一,可解释性极强。树的每个分裂节点都是一个“收入大于多少”“是否有贷款”这样的判断,业务人员一眼就能看懂逻辑,不需要把他当算法工程师培养。第二,它对特征尺度完全不敏感。收入从几万到几十万、通勤距离从几公里到几十公里,决策树只看排序,不需要做标准化,省掉一大步预处理工作。第三,它能自动捕捉非线性关系和特征交互。比如“高收入且通勤距离远”这类组合规律,特征工程里不写它也会被自动找出来。

当然,这不意味着决策树是终局方案。它方差大,数据稍微改一改树结构就可能变,所以它更适合做“第一个能跑通、能跟业务对齐的基线模型”。等基线跑通,后面换随机森林、梯度提升树时,你手里已经有了一份可对照的成绩单。

1.3 从0到1的总体路线图

从零开始做这个项目,我一般按下述顺序推进:数据准备与清洗 → 特征编码 → 模型训练 → 参数调优 → 评估与可视化 → 业务规则输出。时间分配上,数据清洗和特征工程通常占六成以上时间,模型训练反而是最快的部分。很多人以为建模难,实际上难点全在数据上。

新手最容易犯的错,是拿到数据就直接丢给模型训练,结果出了结果也不知道怎么解释。正确做法是先花时间理解每列数据的含义、分布和缺失情况,想清楚这些特征在业务上代表什么,再动手写代码。后面每一步,我都会有对应的实操说明。

2. 数据准备与特征工程

2.1 设计一份可复现的模拟数据集

既然是从0到1,我们得先有一份能动手的数据。我用脚本生成了一份3200条的模拟客户档案,字段如下:

字段名含义示例值
age_group年龄段20-30, 31-40, 41-50, 50+
income年收入(万元)8 ~ 80
loan_status是否有贷款0 = 无,1 = 有
family_size家庭人数1 ~ 6
commute_distance通勤距离(公里)1 ~ 50
maintain_count近半年保养次数0 ~ 6
driving_years驾驶经验(年)0 ~ 30
target三个月内是否有购车意向0 = 否,1 = 是

生成数据不是凭空乱造,要让分布符合业务直觉。比如收入较高、通勤距离较远、最近有保养记录的客户,购车意向比例要适当拉高;完全没有驾驶经验的人,意向大概率偏低。3200条里设定约30%的客户target为1,这样既接近真实销售场景,也能让正负样本不至于极度失衡。

2.2 数据清洗与缺失值处理

数据拿到手,先别急着建模。我一般按三步做检查:查重复、查异常、查缺失。

重复好理解,同一客户出现两次会导致模型“记住”这个人,影响泛化。异常值需要结合实际判断:年收入出现负数或超过200万,在普通家用车销售场景里属于录入错误,宁可删除也不要让极端值拖拽树的切分。缺失值则要注意,scikit-learn的决策树实现不允许特征列存在NaN,所以必须处理。如果缺失比例很低(比如小于5%),直接删除对应行;如果缺失集中在某个特征且比例不低,可以用中位数填充。

这里有个细节我特别想提醒:决策树本身对缺失值的容忍度较高,但那是针对其他语言实现而言的,scikit-learn需要你先清洗干净。不要因为觉得树模型“稳健”就跳过这一步,真实数据里脏数据永远是第一位的问题。

2.3 类别特征编码与连续特征处理

特征处理上,决策树比线性模型省心得多,但也没到“什么都不用做”的程度。age_group这个年龄段特征是文本形式,我习惯按顺序映射成0、1、2、3,因为“31-40”和“41-50”之间天然有前后顺序;loan_status这种二值变量,直接保持0/1即可。如果遇到城市、品牌这类无序类别,可以考虑OneHot编码,不过一旦类别数很多,树会倾向于利用高基数的分类特征,反而影响可解释性。

还有一条原则:连续特征不要提前离散化。很多人习惯把收入分成“低收入/中等收入/高收入”再喂给模型,这是我在项目里反复劝阻的做法。决策树每次分裂时会自己搜索最优切分点,如果提前切好,等于把模型的能力限制死了。你只需要把原始数值原样给模型,它自己会找到“收入大于27.5万”之类的规律。

相关代码可以这样组织:

import pandas as pd df = pd.read_csv('car_intention.csv') # 年龄组分段编码 age_map = {'20-30': 0, '31-40': 1, '41-50': 2, '50+': 3} df['age_group'] = df['age_group'].map(age_map) # 基础清洗:删除重复值、过滤异常收入 df = df.drop_duplicates() df = df[(df['income'] > 0) & (df['income'] <= 200)] # 缺失值用中位数填充 df = df.fillna(df.median(numeric_only=True)) X = df.drop(['target'], axis=1) y = df['target']

这段代码后面会直接接到模型训练里。特征少、逻辑清晰时,DataFrame本身就够用,没必要一开始就套复杂的Pipeline。

3. 决策树核心原理与参数选择

3.1 从信息熵到信息增益:一个手算例子

决策树分类的原理,通俗讲就是“每次选一个特征,把数据分成两堆,让两堆各自的纯度都尽量高”。纯度用什么衡量?信息熵。假设一个数据集S里有20条记录,10个有意向、10个无意向,那么:

H(S) = -0.5 * log2(0.5) - 0.5 * log2(0.5) = 1

如果按“年收入是否大于15万”这个条件拆分:右侧收入大于15万的有6人,其中5人有意向、1人无意向;左侧收入小于等于15万的有14人,其中5人有意向、9人无意向。分别计算两堆的熵:

右侧:H = -5/6 * log2(5/6) - 1/6 * log2(1/6) ≈ 0.65
左侧:H = -5/14 * log2(5/14) - 9/14 * log2(9/14) ≈ 0.94

以样本占比为权重,得到条件熵:6/20 * 0.65 + 14/20 * 0.94 ≈ 0.853。信息增益就是划分前的熵减去划分后的加权熵:1 - 0.853 ≈ 0.147。

这个数字的意义是什么?它表示我们用“收入是否大于15万”这个规则拆分之后,不确定性下降了0.147。树分裂时,会在所有特征的所有切分点里,找那个让信息增益最大或加权熵最小的方案。增益越大,说明这条规则越能把“有意向”和“无意向”的人分开。

3.2 基尼不纯度与分裂标准

和信息熵并列的另一个常用指标是基尼不纯度,公式为Gini(S) = 1 - Σ(p_k)^2。二分类里基尼不纯度的最大值是0.5,代表两类样本各占一半,最混乱;最小值是0,代表全是同一类,最纯净。

还用上面的20条数据举例,初始基尼是0.5。按“收入是否大于15万”划分后,两堆的基尼分别是:

右侧:1 - (5/6)^2 - (1/6)^2 ≈ 0.2778
左侧:1 - (5/14)^2 - (9/14)^2 ≈ 0.4592

加权基尼 = 6/20 * 0.2778 + 14/20 * 0.4592 ≈ 0.4048,基尼增益约0.0952。

scikit-learn的DecisionTreeClassifier默认criterion='gini',也支持'entropy'。我在真实项目里两种都试过,效果差异很小,所以不必在这上面纠结。选一个固定下来,把时间留给剪枝参数更划算。

3.3 剪枝与关键参数调优

决策树最经典的毛病是过拟合。不设限制的树会生长到每个叶子只剩一两个样本,训练集效果接近满分,一到新数据就明显下滑。解决办法是剪枝,核心参数有四个:

参数作用常用范围
max_depth限制树的最大深度3 ~ 9
min_samples_split内部节点继续分裂所需最少样本数2 ~ 20
min_samples_leaf叶节点最少样本数5 ~ 20
ccp_alpha成本复杂度剪枝系数0 ~ 0.05

我有几条调参心得。第一,不要一上来就GridSearchCV全参数穷举,那会非常慢而且容易过拟合验证集。先手动试max_depth=3、5、7、9,观察训练集和测试集AUC的变化;如果训练集AUC远高于测试集,说明太深了,往回缩。第二,min_samples_leaf比max_depth更稳定,我习惯将min_samples_leaf固定在10附近,让每个叶子至少代表一小群人,这样落到业务上不至于出现“只适配了一个人”的规则。第三,如果类别不平衡,先把class_weight设为'balanced',再调结构参数。

剪枝还有一种正规做法叫成本复杂度剪枝,通过ccp_alpha控制。实际操作中,我用cost_complexity_pruning_path算出不同alpha对应的树的不纯度,找不纯度出现明显拐点的位置,选那个alpha值。这个技巧适合数据量较大的情况,数据小的时候直接限制深度就够了。

4. 模型训练与评估全流程

4.1 训练集/测试集划分与交叉验证

建模前必须划出独立的测试集,否则评估结果乐观得没有意义。我一般按70/30比例划分,关键点是用stratify参数保持正负样本比例一致。如果全量数据里30%是意向客户,那么训练集和测试集里也各保持30%左右,这样评估出来的指标才可信。

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y )

只做一次划分还不够稳。决策树对数据扰动敏感,同一份测试集上可能运气好、也可能运气差。我推荐配合StratifiedKFold做5折交叉验证来选参数,这也比我手动瞎试参数高效得多。

from sklearn.model_selection import GridSearchCV, StratifiedKFold param_grid = { 'max_depth': [3, 5, 7, 9], 'min_samples_leaf': [5, 10, 20], 'criterion': ['gini', 'entropy'] } grid = GridSearchCV( DecisionTreeClassifier(class_weight='balanced', random_state=42), param_grid, cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42), scoring='roc_auc' ) grid.fit(X_train, y_train) print(grid.best_params_)

GridSearchCV会返回一组在交叉验证中最优的超参数,但我也会留意它选出的参数是否贴近业务直觉。比如如果最优max_depth=9,我会继续观察这个树的可视化结果,看叶子是否足够多、规则是否碎片化,再决定是否手动收窄。

4.2 完整建模代码与评估指标

选好参数后,训练最终模型并输出评估指标:

from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import classification_report, roc_auc_score model = DecisionTreeClassifier(**grid.best_params_, class_weight='balanced', random_state=42) model.fit(X_train, y_train) pred = model.predict(X_test) proba = model.predict_proba(X_test)[:, 1] print(classification_report(y_test, pred)) print('AUC:', roc_auc_score(y_test, proba))

输出里通常包含precision、recall、F1这几个指标。这里我特别提醒一个业务侧的判断逻辑:如果销售团队的外呼资源有限,误打电话会消耗人力成本,这时候precision更重要;如果公司想尽可能不错过潜在客户,宁可多打几个电话,那么recall要优先。购车意向场景里,我一般优先看recall和F1,因为漏掉一个高意向客户比多打一个无效电话损失更大。AUC则用来判断模型整体的排序能力,AUC越高,说明高意向客户越容易被排在名单前面。

4.3 决策树可视化与业务规则转换

训练完模型后,真正让业务方信服的不是准确率数字,而是把树画出来。scikit-learn自带plot_tree,一行代码就能输出可视化:

from sklearn.tree import plot_tree import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(14, 8)) plot_tree(model, feature_names=X.columns.tolist(), class_names=['否', '是'], filled=True, ax=ax) plt.show()

画出来的树,根节点会显示最重要的分裂特征。比如实际项目中,根节点可能是“income <= 27.5”,左边是高收入客户子集,右边是低收入客户子集。再往下走,某个分支可能继续看“loan_status == 0”和“commute_distance > 20”,走到一个叶子节点,里面有意向客户占比78%。

这一步就是业务规则转换的关键环节。我会把树上几条典型路径翻译成销售团队能直接用的语言:年收入高于27.5万、无贷款、通勤距离大于20公里的人,优先邀约到店;收入较低且最近半年没有保养记录的客户,暂缓联系,放到养客池里做长期培育。这样一来,模型就从“黑盒打分”变成了“可执行的客户分群策略”。

4.4 类别不平衡与业务阈值调整

真实销售数据里,真正购车的人永远是少数。如果正样本只占5%,模型只要全部预测成“不买”,准确率就有95%,但这毫无价值。处理思路有两个方向:一个是在训练时设置class_weight='balanced',让树更重视少数类;另一个是在预测阶段调整阈值。

当我看到AUC不错但recall偏低时,会先看预测概率的分布,再手动把阈值从默认的0.5往下调。比如只把概率大于0.4的客户标记为意向,但要接受误报增加。具体写法:

proba = model.predict_proba(X_test)[:, 1] threshold = 0.4 y_pred_adjusted = (proba >= threshold).astype(int)

阈值怎么定最合理?我习惯结合业务成本算:给销售一个“高意向名单”,每多一个客户就多一份外呼成本,所以阈值不能太低;但阈值太高又会漏掉真正想买的人。实际操作时,我会按概率分桶统计,比如概率0.3到0.4、0.4到0.5、0.5以上分别覆盖多少人、命中率多少,再用一个简单的利润公式倒推最优阈值。这个思路比盲目追求0.5更有说服力。

5. 常见问题与避坑指南

5.1 过拟合怎么判断与处理

训练集AUC很高、测试集AUC明显下滑,基本可以判定过拟合了。决策树不设限地生长,会试图记住每个样本的细节,而不是学习普遍规律。解决手段有三个:限制max_depth、提高min_samples_leaf、设置ccp_alpha剪枝。

我最初做决策树时踩过一个坑:为了让训练集好看,把max_depth调到20,测试集结果惨不忍睹。后来经验是,先用min_samples_leaf限制叶子最少样本数,再配合max_depth限制整体深度。实际操作中叶子样本数10是个不错的起点,数据量大时可以加大到20甚至50,效果往往比单纯限制深度更稳定。

5.2 特征重要性不要照单全收

scikit-learn的feature_importances_是个很直观的指标,但它基于不纯度下降来统计,有一个已知缺陷:高分类型别的特征和数值特征容易被高估。比如在客户数据里,“城市等级”如果有几十个取值,树可能在很多节点都尝试拿它切分,累计重要性虚高。

我在项目里遇到这种情况,会额外用permutation importance去交叉验证。它的原理是把某个特征随机打乱,看模型性能下降多少;下降越多说明这个特征越重要。这个指标规避了树结构的偏差,更适合用来给业务方解释“到底哪些字段是关键”。如果两者结论打架,以permutation importance为准,特征重要性排序也更稳健。

from sklearn.inspection import permutation_importance result = permutation_importance(model, X_test, y_test, n_repeats=30, random_state=42) print(result.importances_mean)

5.3 什么时候从单棵树换到随机森林

单棵决策树的缺点是方差大,数据稍微变化,树结构就可能完全不同。如果你发现换一个随机种子后,测试集指标波动明显,说明该考虑随机森林了。随机森林通过对多棵树取平均,大幅降低方差,通常能比单棵树高出几个百分点AUC,同时保留一定可解释性。

我的建议是:先用单棵决策树做业务对齐和规则梳理,确认好特征口径、目标定义和评估指标;再把基线树替换成随机森林冲高指标。顺序不能反,因为一旦直接上随机森林,业务方看到“几百棵树投票”的说法,就很难再把规则翻译回行动了。从技术演进角度看,梯度提升树一般能在随机森林基础上再提升一点效果,但调参成本也上来了,我通常是在随机森林不够用的时候再引入。

5.4 业务落地环节的典型坑

症状可能原因解决思路
模型指标好,销售不爱用规则不直观,名单没有优先级用可解释规则输出分群表
线下表现不如测试集数据分布变化,比如新能源补贴政策调整定期重训,监控特征分布漂移
正样本太少,模型全部预测“不买”类别不平衡且未处理设置class_weight='balanced'并调阈值
高收入客户被误判为无意向类别特征过多或特征重要性被扭曲用permutation importance排查,做特征筛选
测试集概率集中在0.5附近特征区分度不足补充外部特征,或换更强模型

每一个坑背后都对应真实业务场景。对我来说,模型能力只是占项目三分之一,剩下三分之二是数据理解和业务沟通。这些“非技术”问题,反而最影响最终效果。

我个人在实际项目里的体会是:决策树模型最大的价值不是拿到最高的准确率,而是让团队第一次建立起“预测客户行为”的信心。有一次我把树的规则打印出来贴在销售办公室,一个老销售看完后说“这跟我平时判断客户的方法差不多,但模型比我想得更细”,那一刻我就知道这个模型落地了。如果你也想做购车意向预测,照着这篇文章先跑通一棵树,再逐步替换更强模型即可。中间要点就一个:保持简单,让每一次判断都有迹可循。

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

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

立即咨询