ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用伯努利朴素贝叶斯预测房车险购买:Python源码与工程实践

用伯努利朴素贝叶斯预测房车险购买:Python源码与工程实践 简介面向数据分析和机器学习入门者这份资源提供了基于Python与伯努利朴素贝叶斯预测客户是否购买房车险的完整源码与配套数据集属于典型的监督学习二分类项目。压缩包共14个文件以Python源码和txt数据文件为主另含ipynb可视化分析笔记、已训练好的pkl模型、png图表及说明文档可覆盖从数据读取、预处理、特征工程到模型训练、评估与预测的完整流程。包体仅1.51MB轻量且结构清晰便于按需查阅和复现。项目重点演示了Pandas数据操作、Scikit-learn中BernoulliNB的使用以及准确率、召回率、交叉验证等模型评估手段可帮助读者理解保险客户行为预测的典型建模套路。已有834人浏览学习对于希望快速积累一个完整机器学习实战案例的学习者来说具有不错的参考价值。1. 用伯努利朴素贝叶斯预测房车险购买一个冷门但稳当的选择很多人做“预测客户是否会购买房车险”时第一反应是逻辑回归或XGBoost。我在实际保险营销名单落地里反而经常先跑一版伯努利朴素贝叶斯它把每个客户特征压成“有/没有”的0/1信号对稀疏、离散、全是行为开关的数据特别友好。这个标题里的关键组合——Python、源码、数据集、伯努利朴素贝叶斯——说明你要的不是理论推演而是能直接导入房车险客户数据、跑出概率名单的完整实现。适合谁营销预算有限、需要给运营一个可解释名单的保险数据岗以及想做分类模型入门对照的学习者。下面按我的工程习惯把特征选型、参数设置和上线前必踩的坑一次讲清。2. 为什么是伯努利朴素贝叶斯而不是多项式或高斯房车险特征建模时的取舍2.1 从保险公司角度看“购买房车险”是一个特殊的二分类问题房车险的购买行为有一个明显特点决策链长、触发信号杂、样本中购买用户占比很低。用贝叶斯模型做这件事我们关注的是给定客户特征向量 X后验概率 P(y1|X) 是否超过活动投放阈值。伯努利朴素贝叶斯为每个特征单独估计“这个信号出现时购买的条件概率”再把它们连乘并归一化。这在数学上很朴素但当业务特征恰好是一堆“0/1开关”时它比高斯朴素贝叶斯少了很多分布假设也比逻辑回归更容易解释每个信号的作用方向。把客户特征全部变成“是/否”等于把原始表格压缩成轻量信号矩阵。对房车险这类低频高客单险种运营系统里能拿到的有效变量主要是是否拥有房车、是否已有其他险种、是否点击过车险落地页、是否致电客服、是否发生过理赔。这些字段天然是0/1直接建模比强行做正态假设合理得多。我在落地时一般会先只保留这些二值列跑一版baseline后续再逐步加入连续变量二分后的派生特征。很多人会问朴素贝叶斯的“朴素”假设也就是特征条件独立在保险数据里明显不成立为什么还敢用我的理解是营销名单更看重排序而不是绝对值。特征之间有相关时后验概率会被放大或压缩但同一套特征下相对高低通常仍然能保留。也就是说你给运营的0.83分不代表真实购买率但它能告诉运营“这批人比那批人更值得外呼”。这也是伯努利朴素贝叶斯在早期风控和营销模型里能生存下来的原因。2.2 伯努利分布和多项式分布的区别什么时候必须用伯努利很多人把伯努利朴素贝叶斯和多项式朴素贝叶斯混在一起。它们的差别不在朴素贝叶斯本身而在似然函数的定义。多项式朴素贝叶斯统计的是某个特征值出现的“次数”适合文本词频这种可多次出现的计数伯努利朴素贝叶斯统计的是“是否出现”同一特征重复出现再多次也只记一次。房车险预测里客户访问官网10次和访问1次对购买意向的区分度通常没有10倍差距。如果直接把“访问次数”喂给多项式模型模型会把这个特征的重要性放大很多倍反而压制其他行为信号。所以当数据集中出现次数型变量时我的处理顺序是先看业务逻辑把次数转成阈值型二值特征。“最近30天访问官网大于等于3次”记1否则记0再交给伯努利模型。这样做的另一个好处是避开长尾分布访问次数可能到几十次不转二值时模型会严重受高频客户影响。对应地高斯朴素贝叶斯在保险营销场景里也不常用年龄、收入等连续变量往往偏态分布直接用高斯拟合容易出现某个方差极小的特征单方面主导后验概率。模型特征假设典型场景房车险预测中的用法伯努利朴素贝叶斯0/1是否出现用户行为开关、渠道触达标记是否点击、是否已投保、是否致电多项式朴素贝叶斯整数次数词频、计数访问次数先转阈值后用不直接用原始次数高斯朴素贝叶斯连续正态身高、分数等年龄转分箱二值后可用但不推荐直接喂连续值这里有一个容易误判的地方伯努利模型用binarize参数把输入转成0/1不代表它可以接受任意连续特征。它内部只是拿一个阈值硬切切完之后原始差值信息全丢。所以“先分箱再二值化”和“直接设binarize”两者都能做但前者能让你控制阈值依据后者只是省一行代码。对保险这种业务规则明确的场景我倾向于把阈值写在预处理脚本里而不是塞进模型参数里。2.3 数据集怎么准备一份能直接喂给BernoulliNB的客户表我在实际项目中用的房车险客户数据集通常由三块拼起来客户基础信息、保单历史、近90天营销触点记录。基础信息包括年龄、婚姻状况、是否拥有房车、是否拥有私家车、所在区域保单历史包括是否已有车险、是否有理赔记录、是否临期触点记录包括是否打开营销短信、是否访问投保页面、是否拨打客服热线。目标列是“投放期内是否成交房车险”一般定义为在指定活动窗口内完成购买。整理时最常用的做法是把所有预测特征先清洗成0/1CSV结构大致是has_rv, has_car, has_rv_insurance, is_married, has_claim, clicked_landing_page, called_agent, buy_rv_insurance读取时可以直接用pandas但有一个容易忽略的点读进来要确认列是int还是bool。有的数据源导出CSV会把布尔列写成True/FalseBernoulliNB能接收布尔值但一旦列里出现NaNfit时会报错或静默产生错误概率。我一般会加一个强制转换import pandas as pd df pd.read_csv(rv_insurance_customers.csv) feature_cols [ has_rv, has_car, has_rv_insurance, is_married, has_claim, clicked_landing_page, called_agent ] df[feature_cols] df[feature_cols].astype(int) # True/False - 1/0 X df[feature_cols] y df[buy_rv_insurance].astype(int)这里的逻辑是astype(int)可以统一把True/False以及0.0/1.0这种浮点格式转成0/1。如果某列被读成字符串“1”“0”会直接抛错这反而是一种早期数据质量检查。注意不要在astype之前盲目fillna(0)因为“没有理赔记录”和“理赔记录为0”不是一回事。缺失在保险数据里有时代表“未留痕”是单独一类业务信号最好在源表里显式区分空值和0。如果实在要填充我建议增加一列is_claim_known而不是用0掩盖缺失。数据集的正负样本比例也要留意。房车险购买率经常只有2%到5%如果训练集里正样本只有几十条伯努利模型的先验估计会很不稳。我的做法是先按月份切分历史数据用前几个月的客户做训练用后一个月的客户做验证而不是在一个小活动里随机抽样后直接训练。这样代码文件也能分开preprocess.py负责清洗和二值化train.py负责训练和调参predict.py负责生成营销名单。数据集单独放在data目录下避免把几千行CSV和源码搅在一起后续换城市、换月份数据时只需要改数据路径。3. 用Python跑通房车险预测源码从最小命令到参数调优3.1 最小可运行代码加载、切分、训练、评估先给一份能直接跑通的最小子集。实际使用时把模拟数据部分替换成自己的CSV即可。下面代码用numpy生成2000条与房车险营销场景匹配的二值特征并构造一个可学习的购买概率然后训练伯努利朴素贝叶斯并输出评估指标。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.naive_bayes import BernoulliNB from sklearn.metrics import classification_report, roc_auc_score np.random.seed(42) n 2000 data pd.DataFrame({ has_car: np.random.choice([0, 1], n), has_house: np.random.choice([0, 1], n), has_rv: np.random.choice([0, 1], n), has_rv_insurance: np.random.choice([0, 1], n), clicked_landing_page: np.random.choice([0, 1], n), called_agent: np.random.choice([0, 1], n), is_married: np.random.choice([0, 1], n), recent_claim: np.random.choice([0, 1], n), }) # 购买概率和特征相关制造可学习的信号 prob 0.05 0.3 * data[has_rv] 0.2 * data[has_car] \ 0.1 * data[clicked_landing_page] data[buy_rv_insurance] (np.random.rand(n) prob).astype(int) X data.drop(buy_rv_insurance, axis1) y data[buy_rv_insurance] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) model BernoulliNB(alpha1.0, binarize0.0, fit_priorTrue) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]))这段代码的逻辑是先用训练集拟合模型再用测试集predict得到0/1标签同时用predict_proba取第二列作为P(y1)。注意因为模拟数据里大部分客户不购买只看classification_report容易误判建议重点看roc_auc_score。AUC不关心绝对概率只关心正样本的概率是否排在负样本前面对不平衡数据更可靠。参数设置上alpha1.0是默认的拉普拉斯平滑能防止某个特征在训练集中只出现过一种类别导致条件概率为0。binarize0.0表示输入已经是0/1任何大于0的值会被置为1如果你的特征里有[0,1]区间浮点概率应该把binarize改成0.5。fit_priorTrue的意思是用训练集里正负样本占比作为先验这一点在购买率很低的保险数据里尤其重要。3.2 三个必调参数alpha、binarize、fit_priorBernoulliNB的调参重点不是网络结构或树深度而是三个细节参数alpha、binarize、fit_prior。alpha控制平滑强度默认1.0太小容易过拟合太大会把所有条件概率往均值方向拉。对房车险这类稀疏数据我一般先试0.1、0.3、0.5、1.0很少超过2.0。 bin表示输入已是0/1超过0记1。如果你的特征列不是严格0/1binarize的取值直接影响模型效果0.0和0.5结果差异很大。fit_prior决定是否用训练集的类别分布做先验。如果购买率只有3%fit_priorTrue会天然压低所有客户的预测概率但不会破坏排序。反过来如果你已经通过业务经验知道真实转化率可以直接传class_prior[0.93, 0.07]去覆盖训练集比例。调参可以使用网格搜索把特征二值化和模型参数一起搜from sklearn.pipeline import make_pipeline from sklearn.preprocessing import Binarizer from sklearn.model_selection import GridSearchCV pipe make_pipeline(Binarizer(), BernoulliNB()) param_grid { binarizer__threshold: [0.0, 0.5], bernoullinb__alpha: [0.01, 0.1, 0.5, 1.0], bernoullinb__fit_prior: [True, False], } gs GridSearchCV(pipe, param_grid, scoringroc_auc, cv5) gs.fit(X_train, y_train) print(gs.best_params_)这里用make_pipeline把Binarizer和BernoulliNB串起来是为了保证网格搜索能同步调“阈值”和“模型参数”。如果你的特征已经在预处理阶段转成0/1不建议再用Binarizer因为多一层无意义的变换会拖慢搜索速度。scoring选roc_auc而不是accuracy原因前面说过房车险购买率太低准确率会被“全部预测不购买”刷得很高但营销名单毫无价值。3.3 特征预处理连续变量怎么安全地变成0/1信号年龄、收入、访问次数这类连续变量不能直接进BernoulliNB。常见做法是先转成有业务含义的派生列。比如年龄可以用业务经验拆出“是否超过35岁”收入按城市中位数拆出“是否高于当地中位数”访问次数拆出“是否大于3次”。二值化的阈值必须在训练集上计算再应用到测试集否则会引入未来信息。from sklearn.model_selection import train_test_split train, test train_test_split( df, test_size0.3, random_state42 ) # 在训练集上计算阈值 age_threshold train[age].median() income_threshold train[annual_income].median() train[age_above_median] (train[age] age_threshold).astype(int) test[age_above_median] (test[age] age_threshold).astype(int) train[income_above_median] (train[annual_income] income_threshold).astype(int) test[income_above_median] (test[annual_income] income_threshold).astype(int)这段代码的关键在于如果对全量数据集计算阈值再随机切分训练和测试测试集的信息就泄漏到了训练集里AUC会被高估。实际上线后新客户数据用的是历史训练集算好的阈值而不是新客户自己的中位数。另一个细节是连续变量缺失值要先填充否则大于比较会返回False。我一般先用中位数填充再生成二值列但会把“是否缺失”单独保持为另一个开关特征给模型一个记录异常的机会。4. 源码落地最常见的五个坑从数据泄漏到概率黑匣子4.1 数据侧的三个常见翻车点泄漏、类别失衡、二值化阈值选错第一个坑是特征泄漏。现象训练AUC 0.98测试AUC 0.99看起来非常好但上线外呼后高潜组转化率和随机名单没区别。原因特征里混进了“是否填过投保单”这类只有在成交之后才会产生的数据。比如你把“是否点击过投保页”作为特征但点击行为发生的时间晚于成交时间这就是典型的利用未来事件预测过去。解决按时间切分验证确保每个特征都可被采集在预测时刻之前尤其注意活动期间新建的埋点变量。第二个坑是类别不平衡导致模型输出全是低概率。现象classification_report里0类精确率很高1类召回率为0AUC却还有0.7。原因训练集购买率不足5%模型学到的先验P(y1)极低默认0.5阈值把所有客户都判成不购买。解决放弃predict()只用predict_proba再按业务成本选决策阈值比如取0.2作为外呼线。简单写法是pred_proba model.predict_proba(X_test)[:, 1] threshold 0.2 y_pred_custom (pred_proba threshold).astype(int)这里阈值0.2不是调出来的是根据外呼成本和单均转化利润估算的。如果一次外呼成本50元成交后毛利500元那么保本转化率是10%模型给出的概率超过10%的客户都值得外呼。不要盲目用0.5房车险这类不平衡业务里0.5根本不适用。第三个坑是二值化阈值选错。现象所有样本的预测概率几乎一样区分度极差。原因有些字段是0到10的次数或者0到1的分数binarize默认设为0.0导致所有非零特征都被置为1。比如访问次数0到10binarize0.0让1次和10次都变成1特征彻底失去区分力。解决连续或计数变量先在预处理中按分位数或业务经验切分再进模型或者调binarize到中位数。我一般把这类变量在pipeline里单独设置阈值而不是全部交给同一个binarize。4.2 模型侧的两个黑匣子概率漂移与独立性假设第四个坑是预测概率集中在0和1两端看起来“很有信心”但排序不稳定。现象模型给出的0.98客户实际购买率只有30%0.99客户只有35%概率差距不大但名字排序每个月都变。原因alpha太小加上特征之间高度相关朴素贝叶斯的独立性假设被放大多个强相关特征连乘后把概率推到极端。解决先把alpha调大比如0.5到1.0然后用Cramer‘s V检查特征两两相关性把相关性大于0.7的合并。下面是一个简单的特征相关性检查片段import pandas as pd from scipy.stats import chi2_contingency def cramers_v(x, y): ct pd.crosstab(x, y) chi2 chi2_contingency(ct)[0] n ct.sum().sum() return (chi2 / (n * (min(ct.shape) - 1))) ** 0.5 cols [has_rv, has_car, has_rv_insurance, clicked_landing_page] corr_matrix pd.DataFrame([[cramers_v(df[a], df[b]) for b in cols] for a in cols], indexcols, columnscols) print(corr_matrix)这段代码的作用是计算二值特征之间的Cramer’s V取值0到1越接近1代表关联越强。发现“has_rv”和“has_rv_insurance”相关度极高时可以考虑只保留一个或者合并成“已拥车且已投保”的组合列。注意这只是一个排查工具不是模型本身。第五个坑是概率值没有业务校准意义。现象模型输出0.6你认为有60%客户会买但事后统计实际只有20%。原因伯努利朴素贝叶斯在特征不独立时后验概率会系统性偏移它不是天然校准的概率。解决不要把predict_proba直接当成真实概率先做校准或者只看排序分组。这个和营销名单关系很大我在第五章单独展开。5. 概率校准与名单导出让预测结果落到营销动作如果只做模型训练伯努利朴素贝叶斯能给你一个不错的AUC排序。但真正让运营愿意用的是一份有“概率分数”和“层级分组”的名单。我一般会用CalibratedClassifierCV对原始概率做校准让分数更接近真实转化率然后按业务阈值分低潜、中潜、高潜三组导出。from sklearn.calibration import CalibratedClassifierCV cal_model CalibratedClassifierCV(model, methodisotonic, cv5) cal_model.fit(X_train, y_train) cal_proba cal_model.predict_proba(X_test)[:, 1]这里methodisotonic适合样本量较大、曲线形状灵活的情况样本量小时改用sigmoid更稳。校准之后0.6基本能对应接近60%的成交率运营就可以直接用它算外呼成本。名单导出时我坚持给运营概率本身而不是0/1标签result X_test.copy() result[prob] cal_proba result[segment] pd.cut( result[prob], bins[0, 0.2, 0.5, 1.0], labels[低潜, 中潜, 高潜] ) result.to_csv(rv_insurance_campaign_list.csv, indexFalse)运营拿到名单后会优先打高潜组并对中潜组做短信触达低潜暂时不进量。这样做比“预测会买/不会买”更符合保险销售的节奏。我早期在这个环节吃过亏只报告AUC和精确率忽略了概率校准结果高潜组名单转化率不如随机抽样差点让项目被砍。后来把重点挪到“分组名单的稳定性”上——每个月用滚动时间窗口重新训练观察高潜组人数和实际转化率有没有跟着季节波动。伯努利朴素贝叶斯本身不复杂真正决定项目成败的是数据切分、阈值设定和后处理校准。希望这套做法能帮你少走一段弯路。本文还有配套的精品资源点击获取
返回列表