ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用伯努利朴素贝叶斯预测房车险购买:从特征二元化到营销名单

用伯努利朴素贝叶斯预测房车险购买:从特征二元化到营销名单 简介房车险客户购买预测是机器学习中典型的二分类问题这份资源提供了一套基于Python与伯努利朴素贝叶斯的完整实战项目适合数据分析初学者和从事保险客户分析的人员使用。项目从客户数据读取开始逐步完成缺失值处理、特征编码、模型训练与交叉验证并结合准确率、F1等指标评估效果训练脚本可直接运行预测模块支持对新客户进行判断。压缩包共14个文件整体仅1.51MB内含5个Python源码、5个数据集与字段说明文本、1个Jupyter可视化分析笔记、1个训练好的模型文件和1张特征相关图结构简洁、便于快速定位。目前已有834人学习资源通过伯努利NB模型演示了特征独立性假设在实际业务中的应用可视化图表能帮助理解数据分布训练好的pkl模型可即时调用是一份适合从理论走向实践的二分类建模参考。1. 用伯努利朴素贝叶斯预测房车险购买一个值得先跑通的最小闭环保险营销的痛不在话术而在名单。给全量客户群发房车险推广单次触达成本高、转化率低真正要做的是在销售动作开始前先回答一个问题这个人会不会买。Python 实现基于伯努利朴素贝叶斯预测客户购买房车险正是这类场景里最轻量的起步方案一份客户数据集配几十行源码把「买 / 不买」压缩成二元分类问题再用概率输出给营销名单排序。它不追求模型复杂度而是用朴素贝叶斯的概率逻辑先跑通「数据 → 特征 → 预测 → 名单」闭环适合刚接触机器学习分类的从业者也适合想快速验证保险营销数据价值的团队。我下面讲的这套做法会直接落到能跑的 Python 代码和参数上你照着改字段名就能复现。2. 伯努利朴素贝叶斯为什么适合「买不买」模型原理与数据格式准备2.1 伯努利分布与朴素贝叶斯的结合点二元特征假设朴素贝叶斯家族里有高斯、多项式、伯努利三兄弟各自的适用前提完全不同。高斯朴素贝叶斯假设连续特征服从正态分布多项式朴素贝叶斯假设特征近似离散词频而伯努利朴素贝叶斯做了一个更严格的假设每个特征只有 0/1 两种取值并且特征之间条件独立。它的预测逻辑可以写成后验概率形式P(购买|特征组合) 正比于 P(购买) 乘以每个特征在购买条件下的伯努利概率。分母对所有类别求和后归一化最终得到的就是 predict_proba 输出的那个分数。放在「客户是否会购买房车险」这个场景里二元特征恰好和业务口径对得上是否投保过交强险、是否收到过营销短信、是否在官网留过联系方式、是否发生过理赔、车辆是否属于房车品类这些字段本质就是“是/否”。即使原始数据里存的是连续量比如客户年龄、年收入也可以先按业务阈值切成“高龄与否”“高收入与否”再交给伯努利模型。这样一来模型要学的 p(x_i|y) 就退化成两张简单的频次表买的人里某个特征是 1 的比例不买的人里这个特征是 1 的比例。模型复杂度低意味着训练极快数据量小也扛得住这是它在这个场景里的核心价值。这个模型的输出也贴合业务场景。predict_proba 给的是 P(购买|特征组合)而不是一个干巴巴的 0/1。营销负责人可以把概率当作“购买倾向分”按分数降序圈名单而不是让模型替你做最终决定。换句话说伯努利朴素贝叶斯在这里不是用来追求超高准确率的而是用来在大池子里快速捞出一小撮高倾向客户这决定了后面我们看指标的方式也决定了它对特征要求的苛刻程度特征必须先变成“是否”语义模型才接得住。2.2 把数据集整理成模型能吃的形态字段清洗与目标变量定义标题里提到的“源码数据集”通常不是直接能训练的形态。以常见实践为例数据集侧面一般是这种结构每一行是一个客户列包含人口学属性、历史保单、渠道互动记录和购买标签。由于不能假设原始 CSV 一定整洁第一步永远是加载后查空缺、查类型、查目标分布。import pandas as pd df pd.read_csv(caravan_insurance.csv, encodingutf-8) print(df.shape) print(df.dtypes) print(df.isnull().sum().sort_values(ascendingFalse).head(10)) # 目标变量是否购买房车险原始数据里的字段名可能是 CARAVAN / purchase # 把 yes/no 或 Y/N 统一成整数便于 sklearn 识别 df[purchase] df[purchase].map({yes: 1, no: 0}).fillna(0).astype(int) print(df[purchase].value_counts(normalizeTrue))这段代码干了两件事首先是体检确认有多少行、每个字段的类型有没有解析错位、哪些字段缺失严重其次是收拾标签把字符串型的目标变量映射成 0/1。如果原始列里缺失值太多比如超过 40%我一般直接删掉这列而不是硬补因为伯努利模型本身不擅长处理缺失补出来的 0/1 反而会把噪声带进去。目标变量定义要慎重别把“曾经购买过”和“有意向购买”混成一类。如果数据里有单独的“咨询过房车险”字段那是很好的特征不是目标。购买标签应以保单成交为准否则模型学到的分布会失真上线后翻车的概率很高。我见过有人把“点击过广告”当标签训练出来模型分数虚高一跑外呼名单就现原形。特征列里常有一些取值混乱的字段比如保险类型写成“全险”“comprehensive”“Comprehensive”三种形式直接独热会产生三个列。处理时要先归一化取值再生成 0/1 标记# 原始数据常见陷阱字段名不一致、取值混乱 df[has_full_coverage] df[coverage_type].apply( lambda x: 1 if str(x).strip().lower() in (comprehensive, 全险, c) else 0 )这段转换把字符串取值统一成小写后匹配命中就置 1。逻辑很朴素但能避免同一个业务含义被独热拆成多列稀释模型权重。保险数据里这种“同义不同形”的字段非常多处理时宁可慢一点也别让模型把噪声当规律学进去。2.3 标签分布体检与特征列选择原则房车险购买率在真实业务里通常很低5% 上下很正常。这个数字直接影响后面的评估方式和阈值设计拿到数据后第一件事就是确认它。value_counts(normalizeTrue) 输出的结果是 0.95 和 0.05就应该立刻意识到全预测“不买”也有 95% 准确率模型必须围绕那 5% 来评价。特征选择遵循一个朴素原则只保留在销售流程开始前就能拿到的字段。比如客户年龄、行业、历史投保年份数、前一年是否有过理赔、是否通过邮件渠道回应过。像“是否访问过产品页”这类后验字段训练时很漂亮部署时拿不到属于典型的泄露特征务必排除。一个有效的判断方法是问自己如果明天要为一个今天刚注册的客户打分这个字段的值能从数据库里查出来吗查不出来就删。我一般会把特征分成三组客户社会属性、历史保单行为、营销触达响应。社会属性刻画“这个人像不像房车险客群”保单行为刻画“他是不是我们的长期优质客户”触达响应刻画“他对保险营销是否敏感”。三组各取几个代表字段比单押一组字段稳定得多。3. 特征二元化与模型训练从 CSV 到可运行的 Python 源码3.1 连续特征怎么转 0/1阈值切分的细节与代码伯努利模型不吃连续值这是新手最容易踩的硬门槛。很多人在网上找到源码把年龄、收入原样传给 BernoulliNB结果模型要么报错要么预测结果毫无区分度因为 BernoulliNB 的 binarize 参数默认值只对非负值生效直接喂连续值会把大部分实数值变成 1信息全丢。常见做法是先对连续字段做阈值切分切分依据有两类一类是业务口径比如“年龄大于 45 岁视为高龄客户”因为房车险的主力客群是退休前后的家庭用户另一类是统计口径比如用中位数或分位数保证切出来的两组尽量均衡。# 连续特征二元化示例年龄、年收入、历史保单年限 import numpy as np df[senior_flag] (df[age] 45).astype(int) df[high_income_flag] (df[income] df[income].median()).astype(int) df[long_tenure_flag] (df[policy_years] 3).astype(int) # 多分类名义变量也要转成独热再合并别直接当数值用 df pd.get_dummies(df, columns[occupation], prefixocc, dtypeint)这里的三个新特征含义senior_flag 抓房车险的目标客群年龄特征high_income_flag 用中位数做切点避免极值把均值拉偏long_tenure_flag 表达“老客户比新客户更容易加购新品”。编码方式全是 astype(int)布尔值转 0/1干净利落。如果是名义变量比如职业、居住地区那就别用阈值直接独热编码。独热之后每个取值都变成独立的 0/1 列伯努利模型处理起来很自然。需要注意的是独热列数可能很多训练前先确认这些列不会让矩阵变成几万维否则 2000 行数据扛几百个特征过拟合风险非常大。出现频次极低的取值可以预先归并成“其他”减少特征面。3.2 用 sklearn 的 BernoulliNB 训练房车险预测模型核心代码与参数特征准备好之后训练代码很短但参数不能盲抄。下面是可运行的构造方式语义很明确每个样本的每个特征要么出现要么没出现不要把它们当作计数。核心代码如下from sklearn.model_selection import train_test_split from sklearn.naive_bayes import BernoulliNB from sklearn.metrics import confusion_matrix feature_cols [ senior_flag, high_income_flag, long_tenure_flag, email_contacted_flag, prior_claim_flag, ] [c for c in df.columns if c.startswith(occ_)] X df[feature_cols].values.astype(int) y df[purchase].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, random_state42, stratifyy ) model BernoulliNB(alpha1.0, binarize0.0, fit_priorTrue) model.fit(X_train, y_train) y_pred model.predict(X_test) print(confusion_matrix(y_test, y_pred))这里必须解释三个参数alpha、binarize、fit_prior。alpha 是拉普拉斯平滑系数默认 1.0当训练集中某个特征在某个类别下从未出现时避免概率算成 0 吞掉其他信息一般保持默认即可binarize 设为 0.0 表示“大于 0 的值一律置 1”因为我们已经做了特征二元化再传 0.0 可以保证模型内部不会乱动编码fit_prior 是是否从训练数据里学先验概率默认 True当购买比例明显失衡时也可以手动传 class_prior 覆盖后面专门讲。数据量小的话stratify 分层抽样很有必要。房车险购买率低不分层的话测试集可能一个正样本都没有混淆矩阵直接缺一块。random_state 固定一个数字不仅为了复现更重要的是让你调参时能对比同一批测试集上的效果。每次改动特征或参数只要随机种子不变指标变化就可以归因到你的改动而不是随机波动。3.3 数据集分割与类别不平衡处理购买率只有 5% 的场景直接训练会出现一个尴尬现象模型预测几乎所有样本为“不买”准确率却有 95% 以上。这个准确率是虚的因为营销人员要的是从名单里捞出那 5% 的人而不是复述“大部分人不会买”这个事实。解决类别不平衡伯努利朴素贝叶斯特有的手段是改先验。模型默认按训练集的类别比例算 P(购买) 和 P(不买)如果真实业务里购买率就是 5%那默认是对的但如果数据采集时正样本被刻意抽样放大了比如为了做营销活动把过去的购买客户全部拉进来比例失真就要手动纠正。# 训练集购买率可能被抽样扭曲按业务真实先验覆盖 real_prior 0.05 # 全量客户中实际购买房车险的比例 model_prior BernoulliNB(alpha1.0, binarize0.0, fit_priorFalse, class_prior[1 - real_prior, real_prior]) model_prior.fit(X_train, y_train)fit_priorFalse 时模型不再从训练集统计类别频率直接用 class_prior 给的两项做预测。这个技巧在做跨时间测试时特别有用比如用 2023 年数据训练、2024 年数据验证两年的购买率可能差好几个百分点直接套用训练集先验会系统性低估。改完先验后predict_proba 输出的分数量级会变化名单排序也会重新洗牌值得专门盯一下。注意class_prior 的列表顺序要跟着 classes_ 走通常 classes_[0] 是 0不买classes_[1] 是 1购买。写反了会得到完全镜像的错误概率等于把名单发给了反向客户。训练完打印 model.classes_ 确认一下就放心了。4. 模型评估与阈值调整不只盯准确率4.1 混淆矩阵与召回的解读方式训练完第一版模型大多数人第一反应是打印 accuracy看到 93% 就觉得完事。但在购买率 5% 的数据集上93% 的准确率可能意味着正样本一个都没捞着。评估模型服务于营销名单时我更建议同时看四个数准确率、精确率、召回率、F1并且按“购买”这个少数类去读。from sklearn.metrics import classification_report print(classification_report(y_test, y_pred, target_names[不买, 购买]))实际会看到类似这样的输出模型在“不买”类上召回 99%在“购买”类上召回 0%。这就是典型的“名单全救不活”的症状原因通常有两个阈值不当或者特征区分度不足。此时不要急着改模型先跑一次 predict_proba把购买类的概率降序排列看看前 100 个客户里到底有多少个真正买了房车险。如果 100 个里能命中 15 个说明模型学到的排序能力是有的只是默认阈值 0.5 太苛刻需要往下调。另一个容易忽略的角度是基线对比。全量客户购买率若为 5%随机抽 100 人预期命中 5 人模型名单命中 12 人就是基线的 2.4 倍这个倍数才是业务方能直观理解的指标。我在给业务方汇报时从不念精确率和召回率只讲一句话这份名单的命中率是随机名单的两倍还是三倍。4.2 调整 predict_proba 阈值来改变营销名单朴素贝叶斯输出的概率绝对值偏大或偏小是常事因为特征独立性假设在真实保险数据里几乎不成立所以概率分数只能看相对排序不能当作真实概率。这也决定了阈值调整要基于名单目标来设定而不是盯着 0.5。import numpy as np proba model.predict_proba(X_test)[:, 1] # 购买类的概率 # 按分数排序取前 10% 客户作为营销名单 threshold np.quantile(proba, 0.90) marketing_list proba threshold print(名单人数:, marketing_list.sum()) print(名单命中购买人数:, y_test[marketing_list].sum()) print(名单命中率:, y_test[marketing_list].sum() / max(marketing_list.sum(), 1))threshold 的取法可以完全绕开模型输出的概率绝对值用分位数把“分数最高的 10%”圈成第一批外呼名单。好处是名单规模和营销预算直接挂钩预算够就取前 20%不够就取前 5%。这里的关键是不要拿全部客户的平均购买率和名单内的命中率比而是拿名单内命中率和随机抽样的 5% 基线比命中率翻倍就算模型有效。我习惯把评估报告和阈值实验串成一个函数重跑每次改特征或调参后输出“前 5% / 前 10% / 前 20%”三档名单的命中率。这个表比任何单一指标都能说服业务方它直接回答“给我这批名单这批里到底有多少人真会买”。调阈值时如果发现前 5% 名单命中率反而低于前 10%说明分数尾部噪声大需要回去看特征质量而不是继续切阈值。提示不要用 train_test_split 的随机拆分做过拟合判断。跨时间验证更接近真实场景把前几个月的当训练集后一个月的当测试集这样评估出的命中率你敢直接报给业务。5. 避坑伯努利朴素贝叶斯做房车险预测的五个常见问题5.1 现象预测结果全是“不买”购买类召回率为 0训练完成后打印分类报告购买类的召回率为 0%整个预测输出没有一个 1。原因很简单训练集里购买样本占比太低模型学到的先验 P(购买)≈0.05而特征对购买类的似然又不够强后验概率整体压不过不买类默认阈值 0.5 下全被判为负样本。解决路径分两步。第一步确认特征真的有意义用交叉验证看一下购买类的 AUC 分数如果 AUC 在 0.5 附近说明特征是摆设得回去做特征工程如果 AUC 有 0.7 以上说明模型排序能力存在就执行第二步调整决策阈值把阈值从 0.5 降到 0.2 或更低用名单坐定策略而不是硬性分类。这一步在所有朴素贝叶斯保险场景里都是标配操作不丢人。5.2 现象连续特征直接丢进 BernoulliNB报错或者输出毫无区分度部分网上的源码示例用 binarize0.5 直接吃连续特征看起来没报错结果所有连续值较大的样本都变成 1模型把年龄、收入信息全当成“都大于 0”区分度丧失。伯努利模型命名已经说明问题每个特征只回答“有没有”不回答“有多少”。解决方法是先人工切分把连续字段按业务或分位数转成 0/1如果实在想偷懒就给 BernoulliNB 传 binarize0.0让它把大于 0 的离散为一个状态但这只对本来就在 0/n 附近分布的特征有效年龄这种平滑分布照样失效。正确的姿势永远是先 explore再 binary别指望模型替你圆场。5.3 现象同一个数据集换用 MultinomialNB 后分数完全相反MultinomialNB 假设特征服从多项式分布适合单词计数、次数统计这类离散频数而伯努利适合布尔指示量。如果特征里有“理赔次数”“接触次数”这种计数型字段MultinomialNB 可能会表现更好但两者混用是概念性错误。伯努利模型的概率公式里某一个特征为 1 与特征值大小没有关系一旦计数特征没有被二元化模型会把数值大小当成出现概率的一部分去计算排序结果自然诡异。解决方法是先想清楚特征矩阵是什么语义全是“是否”用 BernoulliNB有“几次”也有“是否”的把“几次”字段先二元化再统一进伯努利或者干脆改用适合混合分布的逻辑回归。别在同一个模型里混两种语义这是选型问题不是调参问题。5.4 现象训练集上效果惊艳测试集上一败涂地房车险数据集特征列数多、样本量少很容易出现高维稀疏导致的过拟合。伯努利模型虽然结构简单但加了大量独热列后同样会记住训练样本。症状是训练集精确率很高测试集精确率掉一半以上。解决方法是压缩特征面。先看每个独热列的出现频次出现次数小于 5 的取值直接归并为“其他”类型再看特征之间相关性比如“是否高龄”和“是否退休年龄”高度重叠留一个即可。特征做减法对朴素贝叶斯的收益往往大于换模型因为模型本身不擅长做特征筛选垃圾进垃圾出在这里表现得比很多模型都明显。5.5 现象新预测数据转特征时报错特征数量不一致训练时用的特征列里包含独热编码产生的多个 occ_ 列等到实际预测线上新数据时如果新数据集里职业取值比训练集少get_dummies 产生的列数就比训练时少模型直接报维度不匹配。这类错误在把模型封装成 Python 脚本、批量跑新名单时特别常见。解决方法是先保存一份完整的特征列清单预测前用 reindex 把新数据框对齐到训练特征列上缺失列补 0。代码如下def prepare_features(raw_df, feature_cols): df raw_df.copy() df[senior_flag] (df[age] 45).astype(int) df[high_income_flag] (df[income] df[income].median()).astype(int) df pd.get_dummies(df, columns[occupation], prefixocc, dtypeint) df df.reindex(columnsfeature_cols, fill_value0) return df[feature_cols]这里还有个隐含问题中位数、分位数这些切分参数是训练集里算出来的预测时一定要沿用训练阶段的切点不能在 predict 时用新数据的 median 重新算。否则同一个客户在不同月份的分数会漂移上线后行为完全不可控。提示把训练阶段计算出的阈值、列清单、class_prior 全部保存成 JSON 或 pkl预测阶段只加载这些元数据不在预测脚本里重新计算。6. 从预测到落地用概率分数圈营销名单的进阶玩法模型跑通之后价值不在一份 sklearn 报告里而在每天生成的那份外呼名单里。进阶玩法是把概率分数直接对接营销流程每天早晨从数据库拉新增客户跑一遍特征转换和 predict_proba按分数倒序输出 Excel交给电销团队。名单前面可以加一列“推荐强度”把概率分数分档位比如前 5% 标记为“A 类”5% 到 20% 标记为“B 类”低于 20% 不进入当期营销池。这里要特别注意一个我踩过不止一次的坑朴素贝叶斯概率分数会随客户群结构变化而漂移。某个月进了大量年轻客户他们本身购买倾向低top 10% 的分数门槛会跟着波动导致同样分数的客户在不同月份被划入不同档位。我的习惯是每个月用当月数据跑一次名单分位数动态定档而不是写死一个概率阈值。另一个值得投入的验证方法是分层抽样回访。从模型挑出的 A 类名单里随机抽 100 人做外呼试点同时从 B 类里抽 100 人对比两组的实际购买转化率。只要 A 类转化率明显高于 B 类模型就真正兑现了价值这个测试结果比任何离线精度指标都更能让业务方认可。伯努利朴素贝叶斯的优势之一就是训练快、更新成本低一个月重新训练一次完全可行配合滚动验证能持续监控名单质量。我把这套流程跑了三个季度后最深切的体会是真正难的不是那几十行 Python 源码而是每次数据格式变化后都要重新守住的稳定性约定——阈值不用新数据重算、特征列不擅自增加、先验不随手改动。把这三条约定写进代码注释里比试图理解每个概率数字背后的玄学更值钱。希望这些经验能帮到你。本文还有配套的精品资源点击获取
返回列表