尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

伯努利朴素贝叶斯实战:Python实现房车险购买预测

伯努利朴素贝叶斯实战:Python实现房车险购买预测 简介本资源是一套面向数据分析初学者与机器学习实践者的Python实战项目聚焦客户行为预测这一典型商业场景使用伯努利朴素贝叶斯算法解决房车险购买意愿的二分类问题。资源包含完整可运行代码、清洗后的结构化数据集及模型持久化文件覆盖从数据加载、Pandas预处理、特征二值化、模型训练到评估与预测的全流程适合用于课程设计、竞赛备赛或企业销售线索建模参考。压缩包共14个文件1.51MB含5个核心Python脚本train.py/test.py/tic.py等、5个文本数据文件含训练集、测试集、标签与字段说明、1个Jupyter可视化分析笔记、1个模型.pkl文件、1张散点矩阵图及1份README说明文档结构清晰、模块解耦便于逐环节调试与复现。目前已有833人学习下载读者可直接运行源码获得完整预测流程、理解伯努利NB在离散特征场景下的应用逻辑并掌握保险行业客户画像建模的关键实践要点。 接到保险业务方一个需求手上有一批客户的画像数据想预判哪些人更可能在近期购买房车险。数据就是常规的营销库字段很杂但又有大量二值属性比如是否拥有私人房产是否拥有第二辆车有没有孩子这类0/1变量。这类场景用Python做模型预测最合适的选择之一就是伯努利朴素贝叶斯——它对二值特征有天然的适配性而且训练快、解释起来也不费劲。我这次把完整的源码、数据集处理流程和踩过的坑都整理出来了新手可以照着复现有基础的朋友也能换到其他险种上直接用。我先把项目拆成六块讲业务背景、数据预处理、算法原理、完整源码、问题排查、个人经验。每块之间都有依赖关系建议按顺序看。1. 项目概述与业务场景分析1.1 房车险保单预测的产品逻辑房车险和普通车险最大的区别在于它不是刚需险种。交强险是法律强制要求的车损险、三者险基本也是车主标配但房车险意味着客户有一辆房车并且愿意额外掏一笔钱来保障这辆非日常通勤车辆。这个购买决策链条很长客户可能犹豫几个月甚至一年。所以保险业务方想做这件事的核心诉求是在有限的电销人力和渠道预算下优先触达那些最有可能购买的客户。如果能用历史客户数据训练一个分类模型给每个潜在客户输出一个购买概率然后按照概率从高到低排序分配销售线索就能显著提高转化率。具体到建模目标这就是一个经典的二分类监督学习任务。标签就是某个客户在指定时间窗口内是否购买了房车险特征就是客户的基本信息、历史投保记录、家庭属性等。模型输出的结果不是简单的买/不买而是P(买|客户特征)这个概率值方便业务方自定义决策阈值。1.2 为什么选伯努利朴素贝叶斯而不是逻辑回归很多人一上来就会问这种分类问题为什么不直接上逻辑回归或者XGBoost我当时的考虑是这样的。先看数据形态。这个数据集里有大量二值变量比如是否拥有卡车是否持有长期人寿保单是否有高收入职业。逻辑回归当然也能处理二值变量但它在特征共线性严重、稀疏矩阵占比高的时候迭代收敛速度和稳定性都会受影响。而伯努利朴素贝叶斯对二值特征的处理是直接的概率统计不需要梯度下降也不存在收敛问题。再看业务需求。保险营销场景对模型解释性要求很高销售主管要能看懂为什么这个客户分值高你不能给他丢一个黑盒。伯努利朴素贝叶斯算出来的每个特征条件概率都是可解释的——有房车的客户中36%购买了房车险没有房车的客户中只有5%购买了。这种解释方式业务方能直接理解。最后是成本。这个项目不需要秒级实时打分也不需要超高精度业务方要的是可落地、可快速迭代的营销线索打分模型。朴素贝叶斯训练一次只要几秒钟预测百万级客户也很快后续换数据重训几乎没有成本。所以我最终选型就是sklearn里的BernoulliNB配合标准化和二值化预处理。2. 数据集解析与预处理2.1 数据集字段与业务含义我这次用的是经典的房车险营销数据集共5822条客户记录每条记录包含86个属性字段。标签字段是购买房车险取值只有0和1其中购买客户约348户未购买客户约5474户购买比例大概6%。数据集的字段主要分成几个业务维度。第一类是客户人口统计学属性年龄、婚姻状况、职业类型、教育程度、收入等级第二类是家庭结构属性是否有孩子、家庭人口数、居住在哪个区域类型第三类是历史保单属性是否已有船险、是否已有机动车辆险、是否有人寿保单、是否拥有私人房产第四类是营销渠道属性客户是通过哪种渠道进来的是代理推荐、线上注册还是老客户转介绍。这些字段里有很大比例是天然的0/1变量。比如是否有私人房产1表示有0表示没有。还有一部分是分类变量比如职业类型有12个类别收入等级有9个等级。这类多类别变量不能直接丢给伯努利模型需要进一步处理。2.2 特征二值化处理的核心思路伯努利朴素贝叶斯最核心的假设是每个特征只有两种取值状态0和1。所以拿到原始数据后最关键的一步就是把所有特征统一转成二值形式。这个步骤有两种场景处理逻辑不一样。第一种是天然二值变量直接原样保留比如是否拥有房产是否有船险是否有第二辆车。这类字段无需任何处理但要做缺失值检查。如果发现缺失用0填充还是用众数填充要看业务含义。比如是否有船险缺失八成是客户没有购买过船险系统默认没记录填0是合理的但婚姻状况缺失就不能这么做。第二种是多分类变量和连续变量需要转换。多分类变量用one-hot编码后每个类别维度就变成了0/1特征。连续变量比如年龄、年收入处理方式有两种选择一是先做分箱离散化比如收入分成低收入/中收入/高收入三个桶再one-hot二是直接调用BernoulliNB里的binarize参数模型会自己找一个阈值把连续值切分成0和1。我这次采取的是先分箱再二值化的策略。比如年龄字段我先切分成小于25岁25到40岁40到55岁55岁以上四个区间然后one-hot成四个二值特征。为什么不直接binarize因为单个阈值会丢掉太多信息分箱之后再one-hot能在保留一定区分度的同时满足伯努利模型的输入要求。import pandas as pd import numpy as np df pd.read_csv(caravan_insurance.csv) # 年龄段分箱 df[age_group] pd.cut( df[AGE], bins[0, 25, 40, 55, 100], labels[age_lt25, age_25_40, age_40_55, age_gt55] ) # one-hot编码 age_dummies pd.get_dummies(df[age_group], prefixage) df pd.concat([df, age_dummies], axis1) df df.drop([AGE, age_group], axis1)2.3 标签分布与数据质量检查在动手训练之前我习惯先做三件事看标签分布、查缺失值、查重复行。标签分布这块这个数据集里正样本只有6%典型的类别不平衡。如果不做任何处理直接训练模型会倾向于把所有样本都预测成不购买因为这样准确率也有94%。所以后面训练之前必须考虑采样策略或者调整评估指标不能只盯着准确率看。缺失值方面86个字段逐个检查不现实我直接用代码统计缺失比例超过30%的字段直接丢弃低于30%的字段按业务规则填充。重复行检查很容易被忽略。营销数据集经常有重复导入问题同一个客户出现两次甚至三次会导致模型过拟合到重复样本上。我用客户ID字段做去重保留一条记录。# 缺失值检查 missing_ratio df.isnull().mean() drop_cols missing_ratio[missing_ratio 0.3].index.tolist() df df.drop(columnsdrop_cols) # 重复值检查 df df.drop_duplicates(subsetCUSTOMER_ID, keepfirst)3. 伯努利朴素贝叶斯原理解读3.1 贝叶斯公式与朴素假设朴素贝叶斯的核心公式其实就一行P(购买|特征) P(特征|购买) * P(购买) / P(特征)用人话说就是我们知道在已购买客户里拥有房产的人占比是多少现在来了一个新客户他拥有房产我们就反推他购买的概率有多高。这里的朴素两个字是关键。它假设所有特征之间是相互独立的——也就是说拥有房产和有孩子这两个事件互不影响。这个假设在现实中几乎是不可能成立的有孩子的人更倾向买房买房的更可能有车特征之间一定有相关性。但有意思的是即便这个假设很强硬朴素贝叶斯在很多实际分类任务中依然表现不错尤其是在特征维度高、数据量不是特别大的时候。伯努利朴素贝叶斯在此基础上进一步限定每个特征必须是二值的。它统计的是P(特征1|购买)和P(特征0|购买)而不是连续取值下的条件概率密度。所以在计算的时候它只关心这个特征出现或者不出现不关心次数、不关心大小。3.2 三种朴素贝叶斯模型的适用边界很多人分不清伯努利朴素贝叶斯、多项式朴素贝叶斯和高斯朴素贝叶斯我简单说下三者的区别。高斯朴素贝叶斯假设特征服从正态分布适合连续型特征。比如预测客户年收入区间特征是收入金额这就能用高斯模型。多项式朴素贝叶斯适合特征是计数的场景比如文本分类里一个词在一篇文章中出现了几次。它统计的是频率不是简单的是否出现。伯努利朴素贝叶斯则对应是否出现的二值场景。比如文本分类中只关心某个词出现没出现不关心出现几次再比如保险营销数据里关心客户有没有某个属性不关心这个属性量化成多少。选型的时候判断标准很简单在文本场景如果词典大小固定且认为单词出现两次和出现一次对类别判断没有区别就选伯努利如果认为出现次数有意义就选多项式。在结构化营销数据场景只要特征已经处理成0/1就默认选伯努利。3.3 拉普拉斯平滑解决零概率问题伯努利朴素贝叶斯有一个经典bug如果某个特征在训练集的某个类别下从未出现过它的条件概率就是0。概率是0意味着什么意味着连乘计算结果整个变成0其他特征再强也救不回来。举个例子如果训练数据里所有购买房车险的客户都没有在读研究生这个属性那么来了一个新客户他是研究生在读模型计算P(购买|研究生在读)时这一项的贡献是0整个后验概率就是0。这显然不合理营销数据里某个小众特征在正样本中恰好没出现是很常见的。解决办法是拉普拉斯平滑也叫做加1平滑。在计算分子时加一个平滑参数alpha分母加上alpha乘以特征取值数。sklearn的BernoulliNB默认alpha1.0就是为了防止这种零概率。我在实战中把alpha从1.0调低到0.5效果反而更好。原因是这个数据集特征维度高过大的平滑值会让概率值趋向均匀化削弱了真正有区分度的特征的影响。alpha本质上是一个可调的超参数建议用网格搜索调一下不要一直用默认值。4. 源码实现与核心流程拆解4.1 项目结构与依赖环境整个项目文件结构很简单适合直接复用rv_insurance_prediction/ ├── data/ │ ├── caravan_insurance.csv ├── src/ │ ├── preprocess.py │ ├── train.py │ └── predict.py ├── models/ │ └── bernoulli_nb.pkl └── requirements.txt依赖方面用到的Python库不多核心是scikit-learn、pandas、numpy评估可视化用到了matplotlib和seaborn。Python版本我用的3.9向下兼容3.7以上没问题。pip install scikit-learn pandas numpy matplotlib seaborn需要说明的是源码里我没有用任何深度学习框架。这个任务用朴素贝叶斯就够没有必要引入过重的依赖。如果后续要换XGBoost或者LightGBM单独加包就行数据预处理代码可以完全复用。4.2 数据预处理模块预处理模块是整个项目的基石承载了数据加载、清洗、特征工程三大功能。我直接看核心代码。def load_and_preprocess(filepath): df pd.read_csv(filepath) # 分离标签 X df.drop(columns[PURCHASE, CUSTOMER_ID]) y df[PURCHASE] # 只保留二值特征 binary_cols [col for col in X.columns if set(X[col].unique()) {0, 1}] X_binary X[binary_cols].astype(int) # 多分类变量one-hot cat_cols [MOSTYPE, MGEMLEEF, MOSHOOFD] X_cat pd.get_dummies(X[cat_cols], prefixcat_cols, drop_firstFalse) # 合并特征 X_final pd.concat([X_binary, X_cat], axis1).reset_index(dropTrue) return X_final, y有一个细节值得注意在过滤二值特征的时候我用set(X[col].unique()) {0, 1}判断这样既能捕获0/1类型也能捕获0.0/1.0这种浮点型二值列。如果直接用dtype int64判断某些列虽然取值只有0和1但列类型是float就会被漏掉。另外one-hot之后的列要做列名统一保证训练和预测时特征顺序一致。这个问题在新数据上线时特别容易出现后面我会专门讲。4.3 模型训练与预测模块训练模块的代码不长但每一行都有讲究。from sklearn.model_selection import train_test_split from sklearn.naive_bayes import BernoulliNB from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score X_train, X_test, y_train, y_test train_test_split( X_final, y, test_size0.3, random_state42, stratifyy ) model BernoulliNB(alpha0.5, binarizeNone, fit_priorTrue) model.fit(X_train, y_train) y_pred model.predict(X_test) y_proba model.predict_proba(X_test)[:, 1] print(Accuracy:, accuracy_score(y_test, y_pred)) print(Precision:, precision_score(y_test, y_pred)) print(Recall:, recall_score(y_test, y_pred)) print(F1:, f1_score(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_proba))这里几个参数我展开说一下。alpha0.5是我调参后的结果前面已经解释过平滑参数的作用。binarizeNone是因为我在预处理阶段已经把所有特征都转成0/1了模型不需要再自动做二值化。假如你跳过预处理直接喂原始数据可以把binarize设成0.0模型会自动把大于0的值映射为1但这要求特征本身分布比较规整才行。fit_priorTrue表示模型会根据训练数据自动计算先验概率也就是P(购买)和P(不购买)。如果你知道业务上有更强的先验信息比如大盘转化率已知是8%也可以用class_prior[0.92, 0.08]手动指定但多数情况下让模型自己学就行。测试集切分这里用了stratifyy这是分类任务必备的操作。分层采样保证训练集和测试集中正负样本比例一致避免出现测试集里正样本一个都没有的尴尬情况。4.4 评估指标与结果解读我用上面代码跑出来的结果比较理想指标数值准确率0.87精确率0.10召回率0.28F1值0.15AUC0.72只看准确率0.87会觉得模型不错但精确率0.10和召回率0.28才是真正的试金石。因为正样本占比只有6%哪怕全部预测为负样本准确率也有94%。这个模型实际能捞回28%的潜在购买客户而且在捞回的客户里每10个人有1个真的买了房车险。这个表现对于营销线索打分场景来说是能用的。这里的核心逻辑是业务方不需要模型直接替销售做决定而是让模型输出一个概率排名销售把精力集中在前20%的客户身上。在这个目标下AUC0.72意味着排序能力是有效的远超随机猜测的0.5。如果是想进一步提升召回率可以降低预测阈值。predict方法默认以0.5为阈值但我用predict_proba拿到了概率值后可以根据业务承载力把阈值下调到0.2甚至0.1换取更高召回。5. 常见问题与排查实录5.1 特征维度不一致导致维度错误这是我在部署阶段踩过最大的坑。线下训练的时候一切正常但把模型打包给业务方用新数据预测时直接报错说特征数量不一致。原因很简单新数据的分类变量分布和训练集不完全一致。比如训练数据里职业类型有12个类别one-hot后生成12列新数据里某种职业完全没有出现one-hot之后只生成了11列。模型是按照12列训练的预测时拿到11列维度对不上。解决办法是手动固定列名。在训练完成后把特征列名列表保存下来预测时对数据做reindex。# 训练完保存列名 feature_cols list(X_final.columns) with open(models/feature_cols.json, w) as f: json.dump(feature_cols, f) # 预测时对齐列 X_new X_new.reindex(columnsfeature_cols, fill_value0)reindex会自动补全缺失的列新数据里多的列会被自动舍弃缺失的列填0。这样就保证了训练和预测的输入维度永远一致。5.2 类别不平衡下的指标失真我在初期版本里没有处理类别不平衡直接拿6%正样本的数据训练。结果模型把所有客户都预测成不购买准确率依然有94%看起来很不错实际完全不能用。处理这个问题我试过两种方案效果都还OK。第一种是采样法用imblearn库对训练集做SMOTE过采样把正样本复制生成到和负样本差不多数量。但SMOTE对二值特征生成的样本是连续值需要再做一次二值化操作上有点绕。第二种更简单直接在训练时用class_weight给少数类加权。不过BernoulliNB没有直接的class_weight参数我通过手动修改class_prior来实现类似效果。实际落地时我用的还是阈值调整法因为这个项目的核心是排序而不是硬分类。模型输出概率后我不依赖默认阈值而是选一个让业务方满意的召回率对应的阈值点。比如我需要找潜客取概率最高20%的客户直接按proba降序取前20%就行完全不需要纠结阈值。5.3 特征选择对效果的影响刚开始我把86个字段全塞进模型训练是能训练但模型会把很多噪声特征也纳入计算。比如客户是否有某种特定型号的手机这类和购买房车险毫无关系的字段在朴素贝叶斯里会贡献无意义的概率项拉低模型表现。我做了一次基于卡方检验的特征筛选把和标签相关性不显著的特征剔除。sklearn里SelectKBest加chi2可以直接做这件事。from sklearn.feature_selection import SelectKBest, chi2 selector SelectKBest(chi2, k50) X_selected selector.fit_transform(X_final, y)筛选到50个特征之后AUC从0.65提升到0.72。提升的原因不是模型变强了而是噪声变少了。特征筛选这个步骤在朴素贝叶斯里比在树模型里更重要因为树模型本身有特征选择能力朴素贝叶斯没有。5.4 调参与数据规模经验这个项目的训练数据只有五千多条属于小样本场景。这种规模下朴素贝叶斯的优势很明显不容易过拟合对异常值不敏感计算开销小。如果换一个更大的数据集比如几十万条客户记录朴素贝叶斯依然能跑得动但优势会缩小。数据量大之后树模型往往能学到更多非线性关系。我的建议是数据量小于两万优先试朴素贝叶斯和逻辑回归数据量更大再考虑集成学习。alpha这个参数的调法我习惯用网格搜索配5折交叉验证候选值取[0.01, 0.1, 0.5, 1.0, 2.0, 5.0]。大部分结构化数据场景最优值集中在0.1到1.0之间。6. 后续扩展与实用建议6.1 快速迁移到其他险种的改造方法房车险的数据处理流程完全可以复用到其他非刚需险种。我后来用同一套代码改过宠物险和意外险的预测发现只要改三个地方就行。第一标签字段换成目标险种的购买记录。第二特征工程里把业务相关的特本文还有配套的精品资源点击获取
返回列表