资讯动态

朴素贝叶斯实现收入预测:Python数据挖掘项目实战复盘

发布时间:2026/10/22 0:06:26 来源:尧图企业网站定制
做数据挖掘项目的时候我经常被问到什么算法最适合拿来练手如果只让我推荐一个答案不是决策树也不是逻辑回归而是朴素贝叶斯分类算法。尤其是“收入预测”这类以离散特征为主、业务含义清晰的任务朴素贝叶斯不仅实现简单、训练极快还能在数据量不大时给出相当稳健的效果。这篇文章是我做“基于朴素贝叶斯分类算法的收入预测Python数据挖掘项目实战”这个项目后的完整复盘。我会从任务定义讲起把朴素贝叶斯的原理用大白话拆开然后带着你一步步走完数据清洗、特征工程、模型训练、效果评估的全流程最后重点说一说真实项目中一定会遇到的几个坑——比如概率下溢、样本不均衡、独立性假设失效。适合刚入门机器学习、想找个完整项目练手的同学也适合做数据分析但想了解分类模型落地细节的读者。1. 收入预测到底在预测什么任务定义与建模思路1.1 “预测收入”其实是二分类问题很多人一看到“收入预测”四个字第一反应是“用回归模型预测一个人一年赚多少钱”。实际上绝大多数公开的经典任务里目标不是预测具体数值而是判断一个人的年收入是否超过某个阈值。我这次项目使用的数据集中阈值设定为5万美元收入超过5万的标记为1低于等于5万的标记为0。所以这本质上是一个二分类问题。为什么通常这么做而不直接预测金额有两个很现实的原因。第一收入的原始数值分布极度偏态绝大部分人的收入集中在较低区间少数高收入者会把分布拉出很长的尾巴这种数据对回归模型很不友好误差会被极端值主导。第二业务上很多时候只需要一个决策边界比如银行决定是否发信用卡、营销系统决定是否推送某个理财产品它们关心的是“这个人值不值得作为高价值客户对待”而不是精确到千元的收入估算。用分类思路来做建模目标变成了“估计某人属于高收入群体的概率”这正好是朴素贝叶斯分类算法的看家本领。它天然输出后验概率既能直接拿来排名也能配合阈值调整做策略控制。1.2 数据字段的第一眼印象项目使用的是某公开的人口普查收入统计记录共有三万多条样本每条样本包含年龄、工作类型、教育程度、婚姻状况、职业、性别、每周工作时长、原生国等特征以及标记是否高收入的目标字段。初看字段有几个点立刻值得注意。第一绝大多数特征是离散型的分类型变量比如工作类型、教育程度、婚姻状况这类特征非常适合朴素贝叶斯。第二有两个连续型特征——年龄和每周工作时长它们需要单独处理这一点我后面会专门展开。第三目标类别的比例需要提前确认这就是后面讲到的样本均衡性检查因为如果正负样本悬殊模型很容易学偏。拿到数据之后我建议不要急着建模。先把数据落地成DataFrame看一眼shape、dtype、describe心里有个基本画像。原始数据往往是脏的缺失值、异常值和看似正常但其实计算口径不一致的字段都会在后续分析里造成不小的麻烦。1.3 评估指标不能只看准确率分类模型的评估是很容易被新手忽略的环节。收入预测这个场景里正样本比例通常只有百分之二十几如果模型把所有人都预测成“低收入”准确率也有大约75%——看起来不难看但模型完全没有用。所以我在项目里定了一组评估指标精确率Precision、召回率Recall、F1分数、ROC曲线下面积AUC以及混淆矩阵。简单解释一下这四个指标的分工。精确率回答的是“被我们预测为高收入的人里有多少是真的高收入”它决定了营销资源的投放准确度召回率回答的是“真正的高收入人群里我们找回了多少”它决定了业务机会的覆盖度F1是两者的调和平均适合在两者都不想偏废时做整体判断AUC则衡量模型把正负样本排序分开的能力不依赖具体阈值。在项目复盘里我一直强调一句话没有唯一的“最好”指标只有最符合业务目标的指标组合。如果要控制预算精确率更重要如果要抢占市场召回率更重要。这个思路在后面调节阈值时会具体用到。2. 朴素贝叶斯为什么“朴素”公式拆解与直觉建立2.1 贝叶斯定理先放倒朴素贝叶斯分类算法的核心是贝叶斯定理公式写成文字就是后验概率 似然 × 先验概率 ÷ 证据放到收入预测任务里我们要算的是已知某人的各项特征他属于高收入群体的概率是多少。用符号写就是P(高收入 | 特征集合)这叫后验概率“特征集合”包括年龄、教育程度、职业等。贝叶斯定理的价值在于直接算P(高收入 | 特征)很困难但如果我们倒过来分别算P(特征 | 高收入)、P(高收入)和P(特征)问题就变得可行。P(高收入)很容易从训练数据里统计出来比如23%的样本是高收入这就是先验概率P(特征 | 高收入)的意思是“已知某人是高收入他具有这组特征的可能性”这个可以在标注数据里统计。用生活里的例子类比会特别直观。假设某种罕见病的发病率是1%对应先验概率P(有病)。某种检测手段在真正有病的人里阳性率是99%这对应似然P(阳性 | 有病)。现在一个人检测结果是阳性他真有病的概率是多少直觉上可能会说99%但用贝叶斯定理算下来远低于这个数因为还要算上健康人群中的假阳性率。收入预测也一样——即使在特征上高度相似高收入群体的基础比例并不高最终判断必须结合先验概率不能只看“特征像不像”。2.2 “朴素”二字牺牲了什么真正让贝叶斯定理变成“朴素”贝叶斯的是一个极其大胆的简化假设给定类别时各个特征之间条件独立。条件独立的意思是假设一个人的收入类别是已知的那么他的职业与他的教育程度之间不存在关联职业与每周工作时长之间也不存在关联。显然现实中这个假设几乎不可能成立——高学历人群更容易从事高薪职业职业类型往往决定了工作时长。但这就是“朴素”这个名字的由来它天真地假设所有特征互不影响。为什么明知不成立还要用因为一旦假设成立联合概率P(特征 | 类别)就可以分解成每个特征单独概率的乘积。这意味着模型不需要大量样本来估计特征组合的联合分布哪怕特征有几十个每个特征只需要单独统计条件概率即可。训练时间复杂度接近线性这对高维离散特征特别友好。而如果不用朴素假设特征组合数量会指数爆炸样本量根本撑不住。这里有一个非常关键的经验独立性假设违背不一定会导致模型效果崩溃关键在于特征之间的相关性是否足够强、以及是否与类别强相关。在多数场景下即使假设确实被违背朴素贝叶斯的排序能力依然可用这被称为“朴素贝叶斯的良序性”。我后面会专门展示一个独立性被违背的实例以及它的实际表现。2.3 拉普拉斯平滑处理零概率的必备手段条件概率统计中有一个非常现实的问题如果某个特征取值在训练集里从未出现过它的概率就是0。概率为0会导致整个乘积变为0后面乘什么都白搭预测结果直接错误。比如某个职业在训练数据中从来没有出现在高收入群体里新样本恰好是这种职业且各项特征都指向高收入模型却会直接输出概率0。解决方法是拉普拉斯平滑也叫加1平滑。在统计每个特征的类别条件计数时给每个可能的特征取值都人为加一个极小值最常见的是加1。这样概率永远不会为0而且当样本量很大时加1的影响几乎可以忽略。sklearn里的各类朴素贝叶斯实现默认都带平滑参数alpha就是这个东西。我在项目里把alpha当作一个可以调节的超参数通过网格搜索选择最优值。后面优化部分会详述。3. 数据预处理与特征工程模型效果的分水岭3.1 缺失值处理先看数量再决定策略拿到原始数据后第一步永远是查看缺失情况。我统计了每个字段的缺失比例发现工作类型、职业、原生国三个字段都存在缺失其中比例都不高只有百分之几。缺失值处理没有万能方案要结合缺失机制和业务逻辑。对于低于5%的缺失常见的策略是删除行或填充众数。我最后选择了按列填充众数原因很实际删除样本会损失数据而众数填充对分类模型的影响通常可控。填充完再用isnull().sum()确认没有遗漏这个过程不要偷懒因为后面训练时会因为编码不一致直接报错。还需要处理一下数据里的问号字符。原始数据里很多字段的缺失值是用“?”表示的如果不处理pandas会把它当成一个正常类别模型会学到一个虚假的规律。我把“?”统一替换成NaN再执行填充流程先替换后填充。3.2 连续特征分箱为什么朴素贝叶斯强烈需要年龄、每周工作时长是连续值。朴素贝叶斯处理连续特征有两种方式一是假设特征服从高斯分布用均值和方差估计概率密度这叫高斯朴素贝叶斯二是先把连续值离散化成若干区间再统计频率这叫分箱。我在项目里踏踏实实做了一个对比实验。用高斯朴素贝叶斯时整体准确率大约82%但AUC只有0.83左右而把年龄分箱成8个区间、把每周工作时长分箱成6个区间再用离散型贝叶斯模型准确率接近84%AUC提到0.87。为什么差距这么明显因为原始收入相关数据中年龄与收入的关系并不是平滑的正态分布——20多岁收入快速上升40岁左右达到平台期50岁以后变化趋缓用一个单峰正态分布去拟合这种关系会损失大量非线性信息。分箱等于让模型自己学一个分段常数函数表达能力明显更强。如果你也想复现分箱边界建议用分位数而不是等距边界。等距边界对于偏态分布的特征容易导致部分箱样本过少分位数边界能保证每个箱子里样本量大致均衡概率估计更稳定。3.3 类别编码与训练集测试集划分类别特征的处理我直接用了pandas的category dtype配合sklearn的LabelEncoder或直接取category的codes。需要注意有些算法对数值大小敏感但对树模型来说类别数值化后的具体数值顺序没有意义。朴素贝叶斯是基于频率统计的类别用什么整数编码都不影响条件概率的计算所以这里用简单标签编码就足够了不需要One-Hot。One-Hot会显著增加特征维度而对朴素贝叶斯没有额外收益反而拖慢训练。然后按7:3划分训练集和测试集70%用于训练得到各类先验和条件概率30%用于评估测试。划分时要设置随机种子保证结果可复现。另外需要确认测试集中正负样本比例和训练集大致一致这样评估指标才有意义。4. 核心代码实现从数据加载到模型评估一条龙4.1 数据集加载与预处理实现我用pandas做数据加载和预处理整个过程可以用下面这段代码概括。注意这里假定你已经把数据文件放到了工作目录。import pandas as pd import numpy as np column_names [age, workclass, fnlwgt, education, education_num, marital_status, occupation, relationship, race, sex, capital_gain, capital_loss, hours_per_week, native_country, income] df pd.read_csv(income_data.csv, headerNone, namescolumn_names, skipinitialspaceTrue) # 把问号替换成缺失值 df df.replace(?, np.nan) # 查看缺失 print(df.isnull().sum()) # 填充缺失分类列用众数 for col in [workclass, occupation, native_country]: df[col].fillna(df[col].mode()[0], inplaceTrue) # 收入字段转为二值标签 df[income_label] df[income].apply(lambda x: 1 if x.strip().startswith(50K) else 0)这段代码做了几件事读数据、重命名列名、把问号变成NaN、查看缺失情况、填充分类特征。最后生成的income_label就是我们要预测的目标变量。4.2 特征工程实现接下来是特征工程。我把年龄和每周工作时长进行分位数分箱然后用pandas的cut函数生成区间标签再编码成整数。其他分类特征直接用category编码。# 分位数分箱 df[age_bin] pd.qcut(df[age], q8, labelsFalse) df[hours_bin] pd.qcut(df[hours_per_week], q6, labelsFalse) # 把分类特征转为整数编码 categorical_cols [workclass, education, marital_status, occupation, relationship, race, sex, native_country] for col in categorical_cols: df[col] df[col].astype(category).cat.codes # 选择模型使用的特征 feature_cols [age_bin, hours_bin] categorical_cols [capital_gain, capital_loss] # 加一个capital总收益特征 df[capital_total] df[capital_gain] - df[capital_loss] feature_cols.append(capital_total) X df[feature_cols].astype(int) y df[income_label].astype(int)这里额外加了capital_gain和capital_loss两个特征它们来自原始数据表示资本利得和资本损失。这两个字段在高收入人群中取值明显更大是区分度很高的特征这也是后续模型能保持较高精确率的重要原因。4.3 模型训练与指标计算模型训练选用sklearn中的CategoricalNB也就是专门为离散特征设计的朴素贝叶斯版本。为了对比我也跑了GaussianNB。核心代码from sklearn.model_selection import train_test_split from sklearn.naive_bayes import CategoricalNB, GaussianNB from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) # 离散贝叶斯 cnb CategoricalNB(alpha1.0) cnb.fit(X_train, y_train) y_pred cnb.predict(X_test) y_prob cnb.predict_proba(X_test)[:, 1] print(准确率:, accuracy_score(y_test, y_pred)) print(精确率:, precision_score(y_test, y_pred)) print(召回率:, recall_score(y_test, y_pred)) print(F1:, f1_score(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_pred)) # 高斯贝叶斯对比 gnb GaussianNB() gnb.fit(X_train, y_train)我用CategoricalNB得到的结果是准确率0.84精确率0.69召回率0.58F1约0.63AUC约0.87。整体来看模型高收入人群的精确率尚可说明模型预测为高收入的那些样本里确实有很大比例是高收入误报率不算高但召回率偏低说明真实的高收入人群中有接近一半没有被找回来。这个结果并不意外。朴素贝叶斯的本质是用特征组合的似然比来做判断而收入的决定因素往往不是孤立特征而是特征之间的交互——比如“高学历高时长管理岗”的组合模式朴素贝叶斯把每个特征单独看天然抓不住这种协同效应。4.4 混淆矩阵可视化的价值除了指标数字我强烈建议画混淆矩阵。数字只能告诉你“预测错了多少”混淆矩阵能告诉你“错在哪个方向”。在这个项目里混淆矩阵清楚地显示了模型倾向把更多人预测为低收入导致右下角的假阴性数量偏高左下角的假阳性相对较少这对应了前面召回率偏低的结论。用matplotlib画一张带标注数字的混淆矩阵热力图发布复盘报告时几乎必备。下面是可视化代码import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(y_test, y_pred) plt.figure(figsize(6, 5)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[Low, High], yticklabels[Low, High]) plt.xlabel(Predicted) plt.ylabel(Actual) plt.title(Confusion Matrix - CategoricalNB) plt.show()5. 踩坑实录真实项目中的三个关键问题5.1 概率下溢连乘导致数值爆炸第一个坑是概率下溢。朴素贝叶斯计算后验概率时要做大量条件概率连乘每个概率都在0到1之间几十个特征乘下来结果会小到Python浮点数都存不住直接变成0。一旦变成0比较大小就失去意义模型对样本的排序能力也随之消失。解决办法是利用对数函数把连乘变成连加。log(P(特征1|类别)) log(P(特征2|类别)) ... 这个操作不改变概率大小的相对顺序却解决了下溢。sklearn的predict_proba内部已经做了处理但是如果你自己手写朴素贝叶斯这个坑一定会遇到。即使是用sklearn我建议输出一次中间概率值肉眼看一下确认没有出现极端的小数。5.2 样本不均衡模型会“偷懒”第二个坑是样本不均衡。收入数据的正样本占比只有24%左右朴素贝叶斯的先验概率会被多数类主导模型天然倾向于预测更多人属于低收入类。这会造成一个很典型的现象召回率低、精确率尚可。在我们的实验里如果把阈值从默认的0.5下调到0.3召回率可以从0.58提升到0.68但精确率会从0.69降到0.61这就是所谓的精确率-召回率权衡。朴素贝叶斯没有直接处理样本不均衡的超参数但它输出了概率所以可以用阈值调节来适应业务策略。如果你希望模型更加关注高收入人群的覆盖就在验证集上画一条PR曲线找到精确率和召回率交叉点把预测阈值往低调。5.3 独立性假设失效真实数据里的“连坐效应”第三个坑最微妙。教育程度与职业几乎肯定不独立每周工作时长与职业也相关。我在项目里专门做了一次验证把原始特征中相关性极强的职业和学历同时删掉再训练模型发现召回率略微下降但整体AUC变化不大。这说明独立性假设的被违背确实会影响模型上限但毕竟模型输出的是“相对概率”特征之间的相关性只要不是完全共线排序能力依然可以保持。不过如果你把场景换成特征之间高度耦合的业务数据比如“用户浏览了A页面且浏览了B页面且点击了广告”这类强交互特征朴素贝叶斯的独立性假设就会造成明显偏差。那种场景下要么改用逻辑回归手动添加交叉特征要么直接用树模型。6. 模型效果从哪里来参数调优与业务落地思考6.1 alpha值的网格搜索CategoricalNB的平滑参数alpha值得调一调。我用GridSearchCV搜索了[0.01, 0.1, 0.5, 1.0, 2.0, 5.0]这几个候选值以F1分数为优化目标发现alpha取1.0和2.0时F1差距非常小alpha取0.01时F1下降明显。这说明什么样本量足够时加1平滑的影响可以忽略但样本量小或特征取值特别多时alpha太大则会过度平滑把真实的条件概率差异抹平。如果以后做小数据集实验记得把alpha调小一点试试。6.2 与逻辑回归的横向对比为了更好地评估朴素贝叶斯在这类任务上的实际水平我把同样的训练测试集交给逻辑回归跑了一遍使用相同的特征和预处理逻辑回归的准确率约0.85F1约0.68AUC约0.88稍微领先CategoricalNB几个百分点。这符合预期逻辑回归能自动学习特征权重之间的非线性关系不对特征独立性做假设。但朴素贝叶斯有两个不可替代的优势。第一训练速度极快一秒钟之内完成数据量扩大十倍也不怕第二模型可解释性极强每个特征的贡献可以直接分解成对数似然比业务人员直接能看懂“哪些因素推动了收入预测”。在很多规模不大、特征离散、强解释性优先的落地场景里朴素贝叶斯依然是值得优先考虑的选择。6.3 这个项目的扩展方向如果你的学习目标是把这个项目做得更完整我有几个建议。一是引入更多特征比如地区、行业、职业细分观察维度增加后对预测能力的变化。二是做误差分析把预测错的样本挑出来看通常能发现数据录入错误、特征缺失或极端值这类问题。三是把模型封装成一个小型服务实现输入个人特征、输出收入等级概率的简单接口。做完这几点项目就从一个课程练习变成了真正可以讨论上线和迭代的数据产品雏形。最后分享一条我个人做这类项目的体会模型复杂度的选择应该由业务需求和数据量来决定而不是追求最新最花哨的算法。朴素贝叶斯这个“老家伙”在收入预测这类离散特征任务上训练快、够稳定、解释清楚依然值得你在技术方案里认真考虑它一次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑