
简介面向保险产品推荐场景的推荐算法实践资源整合了从数据处理到模型评估的完整脚本流程适合机器学习、数据挖掘初学者及保险科技从业者参考。压缩包共35个文件涵盖Python脚本、数据集与模型相关文件等其中9个py文件覆盖数据预处理、特征选择、建模与评估全流程另有7个csv与6个xlsx提供原始及中间数据便于对照复现。资源包仅3.14MB轻量易用目前已有110人学习下载。通过该资源可了解从客户数据清洗、缺失值填充、独热编码、数值归一化到用户画像构建、模型训练与验证的完整推荐系统搭建思路可以直接在自带数据上运行代码观察各处理步骤对结果的影响适合希望快速上手保险推荐算法并完成动手实践的学习者。1. 保险产品推荐与商品推荐的底层差异在哪如果你把电商推荐那套 Item2Vec 或协同过滤直接搬过来做保险推荐大概率会在上线第一周就收到用户投诉「为什么给我推重疾险我又不是要得病了」。保险产品的推荐场景和电商完全是两码事购买频次极低正常人一年买几次保险、单价高、决策周期长而且涉及合规要求不能像推荐一件 T 恤那样随意。更关键的是保险推荐的用户反馈是极度稀疏的缺样本、缺标签、缺实时行为模型能拿到的有效信号可能只有用户浏览了几个产品页面、填没填过健康告知、以及客服沟通过的历史记录。这也意味着推荐算法在这里的落地方案不是「拼准确率」而是「在稀疏数据下找出有购买倾向的用户并给出能过合规审查的产品组合」。本文会围绕保险产品推荐这个场景讲清楚数据集怎么造、召回排序模型怎么选、以及上线时那些参数和坑。不依赖任何现成的开源推荐算法项目代码和思路都是按工业界最常见的做法来写你照着改就能用在自己的数据集上。2. 保险推荐的数据集怎么造公开数据、自建数据与特征工程2.1 公开数据集与自建数据的选择逻辑在做保险产品推荐时你大概率搜不到一个现成的、干净的「保险推荐数据集」像 MovieLens 那样直接下下来就能训。原因很简单保险数据的隐私等级高用户健康信息、财务信息、保单记录都属于敏感数据公开数据集很难去标识化后放出来。所以常见做法是用公开电商推荐数据集如 MovieLens、Amazon Reviews做算法预演和流程验证再用业务自建数据集来训练最终模型。这里有个很多人都踩过的坑用 MovieLens 调出来的超参数直接搬到保险数据上往往效果很差。因为 MovieLens 是显式反馈评分而保险推荐几乎全是隐式反馈点击、浏览时长、咨询次数。如果你非要用公开数据集来验证我建议至少做一步转换把评分大于等于 4 的行为当作正样本评分低于 4 视为负样本其余交互标记为缺失值这样更接近保险场景的稀疏程度。2.2 自建数据集的核心字段与数据格式设计自建数据集的格式建议直接对齐工业界推荐系统常用的「用户-物品-上下文」三元组结构。下面是一个最简配置下的数据表设计字段名和类型可以直接复用。CREATE TABLE user_profile ( user_id STRING COMMENT 脱敏用户ID, age_group STRING COMMENT 年龄段如 20-30, gender STRING COMMENT 性别, occupation STRING COMMENT 职业类别, income_level STRING COMMENT 收入分层低/中/高, has_insurance INT COMMENT 是否已拥有保险1是0否 ); CREATE TABLE item_profile ( product_id STRING COMMENT 产品ID, product_type STRING COMMENT 产品类型重疾/医疗/寿险/意外/年金, coverage_amount DOUBLE COMMENT 保额, premium DOUBLE COMMENT 保费, coverage_period STRING COMMENT 保障期限, target_tags STRING COMMENT 目标用户标签如 中老年/少儿/带病体 ); CREATE TABLE behavior_log ( user_id STRING, product_id STRING, behavior_type STRING COMMENT 该字段可选值为 view/click/consult/apply, event_time TIMESTAMP, duration_sec INT COMMENT 停留时长秒数 );这个设计的逻辑是保险推荐的信号不在「买了什么」而在「看了多久、问到哪一步」。行为类型按重要程度可以排序为apply consult click view其中apply是投保申请是最强的正样本信号。如果你们业务方没有埋这个字段那推荐效果会大打折扣因为只靠view来学模型分不清用户是随便看看还是真有购买意向。字段设计完还要注意duration_sec的清洗超过 10 分钟的停留大概率是挂机或忘记关页面这种极值要截断。常见处理是做对数变换log1p(duration_sec)让分布更接近正态方便模型学习。2.3 数据质量与负样本构建推荐系统里有一句话正样本决定上限负样本决定下限。保险场景里正样本投保用户极少负样本却多到爆炸。但「用户没投保」不等于「用户不喜欢这个产品」可能是因为产品价格超出预算也可能只是用户当时没有需求。常见做法是把负样本分成两类普通负样本随机采样用户没交互过的产品和困难负样本看了产品详情页但最终没投保的样本。困难负样本能逼模型学到更细的区分边界但引入太多会让模型过于激进容易推荐那些用户最终不会买的产品。经验值是把这两类负样本按 1 比 3 到 1 比 5 混合效果通常比较稳。对于数据量我建议至少具备 3 个月以上的行为日志因为保险用户的决策周期长今天是首次浏览可能下个月才会投保。如果行为日志不足 3 个月模型学到的用户偏好会非常不稳定稍微来一波营销活动就会把推荐结果带偏。数据集处理好之后可以按 8:1:1 切分训练集、验证集和测试集注意这里要按用户切而不是按行为切否则同一个用户出现在训练集和测试集里评估指标会虚高。3. 召回与排序双塔召回加 FM 排序的落地实现3.1 双塔召回模型为什么适合稀疏的保险场景保险产品的量级一般不大一个中型保险公司在线可售的产品可能只有几十到几百个但用户量可能是千万级。这种「用户多、物品少」的形态做全量打分的压力不大排序层直接对全量产品算分也没问题。但召回层依然建议用双塔模型不是为了省算力而是为了做向量化检索把用户和产品都映射成固定维度的向量后续可以做相似度检索也方便给业务方解释「这个用户和这个产品是语义匹配的」。双塔结构的本质是两个独立的 Embedding 层和全连接网络组成塔分别处理用户侧特征和物品侧特征最后用点积算相似度。它的优势在于用户塔和物品塔可以离线分别计算向量线上只做向量检索。下面是一个基于 PyTorch 的最简双塔实现。import torch import torch.nn as nn class UserTower(nn.Module): def __init__(self, num_features, embed_dim, output_dim64): super().__init__() self.fc nn.Sequential( nn.Linear(num_features, 128), nn.ReLU(), nn.Linear(128, output_dim), ) def forward(self, x): return self.fc(x) class ItemTower(nn.Module): def __init__(self, num_features, output_dim64): super().__init__() self.fc nn.Sequential( nn.Linear(num_features, 64), nn.ReLU(), nn.Linear(64, output_dim), ) def forward(self, x): return self.fc(x) class TwoTowerModel(nn.Module): def __init__(self, user_feat_dim, item_feat_dim, embed_dim64): super().__init__() self.user_tower UserTower(user_feat_dim, embed_dim) self.item_tower ItemTower(item_feat_dim) def forward(self, user_feat, item_feat): user_vec self.user_tower(user_feat) item_vec self.item_tower(item_feat) return (user_vec * item_vec).sum(dim-1)训练时用BCEWithLogitsLoss做二分类正样本是「点击且咨询」的用户-产品对负样本从「曝光未点击」里采样。需要留意的是双塔模型不直接输出概率而是要加一个sigmoid再做损失计算代码里forward返回的是 logits。建议训练时加Temperature参数控制点积的数值范围比如把点积结果除以 16避免 logits 过大导致梯度不稳定。3.2 排序层用 FM 模型处理特征交叉召回圈定候选集后排序层要对这些候选产品精确打分。保险推荐里大量特征是类别型职业类型、产品类型、保障期限类别之间的交叉信号很有价值比如「程序员 重疾险」有相关性「高空作业 意外险」更有必然联系。FM因子分解机模型天然适合这种场景它用隐向量交叉的方式替代了人工组合特征的体力活比纯 LR 强又比 DeepFM 好解释。保险推荐对解释性要求高——用户可能直接问客服「我为什么收到这个推荐」——FM 的每个特征权重都可以拆出来讲清楚。下面是一个 FM 的 PyTorch 实现包含一阶线性部分和二阶交叉部分。import torch import torch.nn as nn class FM(nn.Module): def __init__(self, num_features, k8): super().__init__() self.linear nn.Linear(num_features, 1) self.v nn.Parameter(torch.randn(num_features, k) * 0.01) def forward(self, x): # x shape: [batch_size, num_features] linear_part self.linear(x).squeeze(-1) # 二阶交叉0.5 * sum((sum(v_ij * x_i))^2 - sum((v_ij * x_i)^2)) square_of_sum torch.pow(torch.mm(x, self.v), 2).sum(dim-1) sum_of_square torch.mm(x.pow(2), self.v.pow(2)).sum(dim-1) interaction_part 0.5 * (square_of_sum - sum_of_square) return linear_part interaction_part这里k是隐向量维度控制特征交叉的表达能力。k太小小于 4交叉信息学不进去太大大于 32容易过拟合在小数据集上尤其明显。保险数据量级如果是百万以内k8到k16是一个合理的起始范围。注意输入特征要做标准化或归一化FM 对特征尺度敏感数值型特征直接用原始值会导致训练不稳定。对类别特征需要先做 one-hot 编码如果担心维度爆炸可以先做哈希技巧将特征映射到固定维度桶里。3.3 冷启动与规则兜底再好的模型也解决不了冷启动。新用户没有任何行为日志双塔的 Embedding 层学的全是历史行为特征此时双塔输出基本就是随机向量。所以线上部署时一定要加规则兜底。常见方案是新用户前三次请求直接走规则引擎规则按「用户画像里的年龄、职业、收入」匹配产品标签等用户攒够了 5 次有效行为点击或咨询之后再切到模型推荐。这个阈值 5 不是拍脑袋而是统计用户行为数据后的一个折中点小于 5 次行为时模型 AUC 和随机猜测差别不大超过 5 次后模型优势才显著。规则引擎里还需要包含合规硬性条件。比如某些产品只向特定年龄区间开放或者某些产品要求用户完成健康告知才能看到保费测算。这些约束在召回阶段必须硬过滤掉不能靠模型自己学因为模型学的是概率而合规是硬逻辑。具体到工程实现上就是在召回候选集生成前先查询一个过滤白名单filtered_product_set从源头拿掉不合规的产品而不是在模型输出后再做后置强行修改。user_feat build_user_features(user_id) candidate_products get_recommend_pool(user_id) # 已做合规过滤 if user_behavior_count(user_id) 5: ranking_result rule_based_ranking(user_feat, candidate_products) else: item_vecs item_tower(candidate_products) scores torch.sigmoid(two_tower_model(user_feat, item_vecs)) ranking_result sort_by_scores(candidate_products, scores)4. 评估指标与参数调优线上效果比线下 AUC 更重要4.1 离线评估不能只看 AUC还要看覆盖率与多样性很多人做保险推荐评估只看 AUC 数字涨了没这是远远不够的。保险产品推荐的目标不只是「推得准」还要「推得开」——如果把所有用户都推荐同一款爆款医疗险AUC 可能不难看但业务根本没有增量。因为保险产品的利润结构差异大不同产品的佣金率、赔付风险、合作方要求都不同推荐系统需要兼顾业务指标。所以离线阶段一定要同时看 AUC、GAUC按用户分组的 AUC和覆盖率。这里给出一段计算 GAUC 的参考代码它在实践里能比 AUC 更真实地反映「每个用户是否被推荐到了合适的产品」而不是被整体数据里的头部产品带节奏。import numpy as np from sklearn.metrics import roc_auc_score def calc_gauc(user_ids, y_true, y_pred): user_dict {} for uid, label, pred in zip(user_ids, y_true, y_pred): if uid not in user_dict: user_dict[uid] {y: [], pred: []} user_dict[uid][y].append(label) user_dict[uid][pred].append(pred) total_weight 0.0 gauc_sum 0.0 for uid, data in user_dict.items(): y np.array(data[y]) if len(set(y)) 2: continue # 该用户只有单一标签跳过 pred np.array(data[pred]) auc roc_auc_score(y, pred) weight len(y) gauc_sum auc * weight total_weight weight return gauc_sum / total_weight if total_weight 0 else 0.0这段代码的逻辑是对每个用户单独计算 AUC再按用户的行为数量加权平均。如果有用户只有正样本或只有负样本roc_auc_score会直接报错或返回无意义的值所以先跳过。GAUC 相比全局 AUC 最大的价值在于暴露「模型是否偏向给所有用户推同一类产品」的问题。如果 GAUC 比 AUC 低很多比如差 0.05 以上说明模型的排序能力集中在头部产品上对个体用户的分辨力很差。4.2 覆盖率和多样性指标覆盖率定义为有曝光的物品数占总物品数的比例。如果产品池有 80 款产品模型只推荐了其中的 20 款那覆盖率是 25%这个数据在保险场景下几乎不可接受——因为保险公司的合作渠道、销售节奏和考核要求都需要相对均衡地分发产品。多样性可以用推荐列表里产品类型的熵来衡量列表里如果全是医疗险熵为 0需要警惕。多样性计算的参考公式如下def calc_category_entropy(recommend_lists, category_map): total_entropy 0.0 for rec_list in recommend_lists: categories [category_map.get(pid) for pid in rec_list] unique_categories set(categories) entropy 0.0 for cat in unique_categories: p categories.count(cat) / len(categories) entropy - p * np.log(p) total_entropy entropy return total_entropy / len(recommend_lists)类别熵越大说明推荐列表里产品类型的分布越分散。至少要让熵大于 1.0才意味着列表中不止一个主导类别。调参时如果发现多样性太低优先检查召回层是不是只召回了同一种产品子类。4.3 线上 A/B 实验与参数调优速查表线下指标再好最终要用线上 A/B 实验来确认。保险推荐的线上实验周期至少 4 到 6 周因为从看到推荐到完成投保是一个漫长的决策链路做 1 周的实验大概率观察不到显著差异。实验分组要避免用户群体在年龄、收入层上的分布偏差建议按 user_id 哈希分桶而不是按地域或渠道分桶因为保险用户的购买习惯和地域关系不大但和渠道强相关按渠道分桶容易被渠道策略污染。参数调优可以参照下面这张速查表参数推荐起始值调节方向观察指标负采样比例1:4调大则模型更激进调小则更保守精确率、投保转化率FM 隐向量维度 k8调大提升特征交叉纯度过大则过拟合AUC、GAUC双塔输出维度64调大提升表达力但检索耗时增加召回率K行为日志时长参与训练3 个月调长增加样本调短增强时效性GAUC、覆盖率Temperature 系数16调节 logits 数值范围训练稳定性这里着重说下负采样比例。电商推荐里 1:5 甚至 1:10 都常见但保险场景负采样比例尽量不要超过 1:5。因为保险展示位本身有限用户刷到的产品本来就少大量随机负样本会让模型学到「大部分产品对大部分用户都没用」反而抑制召回层的多样性。通常先按 1:3 起步观察精确率和召回率的变化如果投保转化率上去了但曝光量骤降说明负样本量太大了。5. 上线后的三个见效技巧解释性推荐、重排策略与数据回流5.1 给用户一个看得懂的推荐理由保险推荐和内容推荐不同用户看到推荐后的第一反应是「为什么」——为什么给我推年金险我明明是来买医疗险的。没有理由推荐的点击率可能只是带理由推荐的一半。落地做法是维护一个理由模板库每个模板对应一条规则或一个特征组合。例如命中「年龄在 40 岁以上且历史咨询过重疾产品」时显示「根据你的年龄段和保障需求推荐关注这款终身重疾险」。理由生成不需要额外训练模型在排序层打分完成后取出 Top 3 产品的命中特征映射到模板即可。这个模块建议在排序结果出来后直接做线上拼接这样不增加下游响应的计算负担。5.2 重排别把同类型产品一口气推完排序模型输出的是单个用户对单个产品的分数但没有考虑产品之间的冗余度。如果前三名全是百万医疗险用户会觉得推荐没诚意。这里可以加一个 MMR最大边际相关性重排在相关性和多样性之间取平衡。MMR 的核心逻辑是每选一个产品就惩罚掉和已选产品相似度太高的候选项。物品之间的相似度可以用双塔模型中的物品向量直接算余弦相似度无需额外训练。经过 MMR 重排后推荐列表里同一保险大类的产品不会超过两个。重排后要重新检查合规条件避免出现同一保单下互斥的产品被同时推荐。另外线上重排的输出长度尽量控制在 3 到 5 个保险用户一般没有耐心刷大量产品卡片推荐太多反而降低点击意愿。5.3 数据回流的闭环设计模型上线只是起点。保险推荐系统的数据回流比模型本身更重要——用户的曝光记录、点击记录、咨询记录和投保记录要实时或者准实时写回数据仓库用于下一轮的训练集扩充。这个回流过程不能只记录行为还需要记录推荐时的上下文比如当时推荐列表长什么样、用户排在第几位看到了这个产品、当时是早上还是晚上。没有位置信息模型就无法学习「用户是否因为位置靠前才点击」很容易把位置偏差学成偏好偏差。常见做法是给训练样本加上position特征线上预测时统一把position设为 0这样模型学到的就是假设产品出现在第一位时的期望点击率。对于回流数据的更新频率建议训练数据每日更新、模型每日增量训练而不是每周全量重训。保险产品的上下架频率不高模型分布的漂移主要来自用户需求变化和季节性事件——比如年底很多人会集中咨询年金险这种周期性模式只有日级更新的数据才能捕捉到。如果担心日级训练的成本过高可以退一步做周级全量重训加日级增量微调。本文还有配套的精品资源点击获取