☰
朴素贝叶斯实现收入预测:Python数据挖掘项目实战复盘
2026/10/20 13:07:15 网站建设 项目流程

做数据挖掘项目的时候,我经常被问到:什么算法最适合拿来练手?如果只让我推荐一个,答案不是决策树,也不是逻辑回归,而是朴素贝叶斯分类算法。尤其是“收入预测”这类以离散特征为主、业务含义清晰的任务,朴素贝叶斯不仅实现简单、训练极快,还能在数据量不大时给出相当稳健的效果。

这篇文章是我做“基于朴素贝叶斯分类算法的收入预测: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', header=None, names=column_names, skipinitialspace=True) # 把问号替换成缺失值 df = df.replace('?', np.nan) # 查看缺失 print(df.isnull().sum()) # 填充缺失:分类列用众数 for col in ['workclass', 'occupation', 'native_country']: df[col].fillna(df[col].mode()[0], inplace=True) # 收入字段转为二值标签 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'], q=8, labels=False) df['hours_bin'] = pd.qcut(df['hours_per_week'], q=6, labels=False) # 把分类特征转为整数编码 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_size=0.3, random_state=42) # 离散贝叶斯 cnb = CategoricalNB(alpha=1.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.58,F1约0.63,AUC约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, annot=True, fmt='d', cmap='Blues', 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.85,F1约0.68,AUC约0.88,稍微领先CategoricalNB几个百分点。这符合预期:逻辑回归能自动学习特征权重之间的非线性关系,不对特征独立性做假设。

但朴素贝叶斯有两个不可替代的优势。第一,训练速度极快,一秒钟之内完成,数据量扩大十倍也不怕;第二,模型可解释性极强,每个特征的贡献可以直接分解成对数似然比,业务人员直接能看懂“哪些因素推动了收入预测”。在很多规模不大、特征离散、强解释性优先的落地场景里,朴素贝叶斯依然是值得优先考虑的选择。

6.3 这个项目的扩展方向

如果你的学习目标是把这个项目做得更完整,我有几个建议。一是引入更多特征,比如地区、行业、职业细分,观察维度增加后对预测能力的变化。二是做误差分析,把预测错的样本挑出来看,通常能发现数据录入错误、特征缺失或极端值这类问题。三是把模型封装成一个小型服务,实现输入个人特征、输出收入等级概率的简单接口。

做完这几点,项目就从一个课程练习变成了真正可以讨论上线和迭代的数据产品雏形。

最后分享一条我个人做这类项目的体会:模型复杂度的选择应该由业务需求和数据量来决定,而不是追求最新最花哨的算法。朴素贝叶斯这个“老家伙”在收入预测这类离散特征任务上,训练快、够稳定、解释清楚,依然值得你在技术方案里认真考虑它一次。

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

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

立即咨询