跳到主要内容Python 机器学习:基于逻辑回归和决策树的寿险续保预测 | 极客日志PythonAI算法
Python 机器学习:基于逻辑回归和决策树的寿险续保预测
本项目利用 Python 机器学习技术构建寿险续保预测模型。通过逻辑回归和决策树算法,分析人口统计、保险特征等数据。结果显示模型准确率达 92%,AUC 约 0.97。关键影响因素包括收入水平、婚姻状况、年龄及职业。业务上建议针对高价值客户(中高收入、已婚)优化服务,对年轻未婚或老年单身群体制定差异化策略以提升续保率。
字节跳动268 浏览 1. 项目概述
本项目旨在通过数据分析和机器学习技术,深入挖掘营销保险续保的关键因素,构建续保预测模型,帮助保险公司预测用户是否会续保,探索影响用户续保行为的关键因素,识别高价值用户、可能流失的用户,从而采取针对性的营销服务,减少客户流失,提升留存率。
2. 数据集介绍
数据集包含 1000 条记录,包含以下主要特征:
- 人口统计特征:年龄、性别、出生地区、收入水平、教育程度、职业、婚姻状况、家庭成员数量
- 保险相关特征:保单类型、保单期限、保费金额、保单开始日期、保单结束日期、理赔历史(是否理赔,'Yes'或'No')
- 目标变量:renewal(是否续保,'Yes'或'No')
3. 数据探索性分析(EDA)
3.1 客户特征与续保
![图片]
3.2 保单特征与续保
3.3 保单地区续保率
![图片]
3.4 数据相关性分析
数值型特征相关性分析
从相关系数中可以发现,年龄、家庭成员数、保单金额三个特征之间具有正相关性。
![图片]
分类型特征和续保相关性分析
收入水平、婚姻状态、教育水平、理赔历史、性别等特征,和是否续保都具有显著相关性。
| Variable | Chi2 | P_value | Significant |
|---|
| income_level | 255.2955 | 3.66E-56 | TRUE |
| marital_status | 125.3968 | 5.89E-28 | TRUE |
| education_level | 18.05732 | 0.000428 | TRUE |
| claim_history | 11.72501 | 0.000617 | TRUE |
| gender | 9.345081 | 0.002236 | TRUE |
4. 逻辑回归模型分析 - 预测续保用户
使用逻辑回归模型对寿险客户是否会续保进行了预测分析,再基于模型系数解释不同客户特征对续保决策的影响。
4.1 模型性能指标
分类报告与混淆矩阵
![图片]
- 准确率: 92.00% - 预测的续保或不续保的结果,有 92% 的预测准确
- 精确率: 94% - 预测的续保用户中,有 94% 用户实际产生续保行为
- 召回率: 95% - 实际的续保用户,有 95% 能通过模型预测
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
PR 曲线展示召回率和准确率的关系,更加关注正类的识别质量,对类别不平衡数据更加敏感。
- AUC: 0.9712
- PR 曲线解释:当选择 0.5 作为阈值时,每预测出的 100 个续保用户有 93 个会续保,能识别出 95% 的真正续保用户
4.2 模型系数分析 - 哪些是影响用户续保决策的特征
选择对续保或不续保影响权重最大的 TOP20 特征,特征系数情况如下,权重为正数表示该因素和续保正相关,权重为负数表示该因素和续保负相关。
- 收入水平:中等(5.66)和高收入(4.61)用户续保意愿显著较高,而低收入(-9.66)强烈抑制续保。
- 婚姻状况:已婚用户(3.82)续保意愿强,离异用户(-3.73)明显更低。
- 职业与地区:医生(2.77)、律师(2.12)、工程师(1.91)等职业续保率高;上海(2.56)、江苏(2.50)等经济发达地区用户更倾向续保。
- 保单特征:长期保障型产品如'平安盛世金越终身寿险'(2.13)及 1 年期保单(2.08)续保意愿强;家庭成员多(1.89)也有正向影响。
4.3 业务启示
收入中、高等级,已婚、特定职业用户群体更倾向于续保,说明这部分用户具有较高忠诚度。
- 客户分层管理:将这些用户划分为'高价值客户',优先进行客户关系维护;
- 定向营销:为这些用户定制专属服务或产品升级方案(如家庭保障计划、附加健康服务),以提升客户满意度和续保率;
- 提升客户粘性:通过定期沟通,如生日关怀、年度保障回顾、专属客户经理服务等方式,增强客户归属感。
特定地区和特定职业的用户表现出更强的续保倾向,可能和地区文化、经济水平、保险意识有关。
- 区域策略差异化:在续保率高的地区,可考虑加大市场投入,推出本地化产品或服务;
- 职业专属产品:针对续保率高的职业群体,设计契合其职业风险的产品,如高风险职业险种;
- 市场调研:进一步分析这些地区或职业的共性,挖掘其潜在需求,为产品创新提供依据。
购买特定产品的客户更倾向于续保,说明产品设计或内容与其需求高度匹配。
- 产品分类管理:对高续保产品进行分类整理,分析其共同特征(如保障范围、价格机构、服务体验),作为其他产品的优化参考;
- 产品捆绑策略:以高续保产品为核心,设计产品组合包或升级套餐,引导客户选择高价值组合;
- 用户反馈机制:收集购买该类产品客户的反馈,了解其满意度与改进点,持续优化产品设计。
4.4 结论
通过模型分析,识别了高续保倾向和高流失风险的客户特征,基于这些特征制定更具针对性的业务策略,业务发展可以围绕高价值客户维护、产品与市场精细适配、数据驱动客户管理等方向展开,从而提升客户满意度、增强用户粘性,最终提高整体业务的续保率与盈利能力。
5. 决策树模型分析 - 预测续保用户
使用决策树模型对寿险客户是否会续保进行预测,决策树能够自动发现特征间的复杂交互作用,生成的规则易于理解和验证,适合用于续保预测场景。
5.1 模型性能指标
- 准确率: 92.00% - 预测的续保或不续保的结果,有 92% 的预测准确
- 精确率: 96% - 预测的续保用户中,有 96% 用户实际产生续保行为
- 召回率: 94% - 实际的续保用户,有 94% 能通过模型预测
PR 曲线展示召回率和准确率的关系,更加关注正类的识别质量,对类别不平衡数据更加敏感。
- AUC: 0.976
- PR 曲线解释:当选择 0.5 作为阈值时,每预测出的 100 个续保用户有 94 个会续保,能识别出 94% 的真正续保用户
5.2 决策树分析 - 重要特征
- 年龄(39%)
- 家庭成员数量(15%)
- 婚姻状态(11%)
- 教育水平(9%)
- 职业(8%)
5.3 决策树规则
决策树的结构和决策路径通过可视化图形展示,从图中可以看到决策树的根节点,将客户分层 3 个区间,然后根据不同年龄区间的特定重要性进一步划分,建立了以下主要决策路径:
-
年轻用户(年龄≤29.5)54% 不续保
-
中年用户(29.5≤年龄≤60.5)99% 续保
- 设计师以外的职业 99% 续保
- 职业为设计师 86% 续保
-
老年用户(年龄>60.5)68% 续保
- 单身或未婚、离异 59% 不续保
- 已婚用户 90% 续保
5.4 业务启示
- 年轻用户:未婚证续保率非常低,而已婚人群续保率较高,可以针对未婚的年轻用户设计更具吸引力的续保激励策略,比如折扣优惠或捆绑增值服务
- 中年用户:续保率达到 99%,说明这一年龄段其他的影响较小,建议加强对这些用户的客户关系维护,减少流失率
- 老年客户:单身或离异的用户不续保率较高,而已婚用户续保率高,公司可以考虑为单身或离异老年人设计定制化保险解决方案或提供更灵活的套餐以提升续保率
- 家庭成员数以较高的特征重要性,表面家庭规模对续保行为有重要影响,公司可以针对家庭成员较多的客户提供更灵活的组合保险产品,强调家庭整体保障,从而提升客户续保意愿
- 单身或离异的青年和老年客户群体续保率较低,可能与家庭责任和保险意识较弱有关
- 教育水平以 8.7% 的重要性进入决策树特征排序,说明教育背景对客户续保有一定影响,可以在客户教育方面加强,比如提供保险产品知识、理赔流程等内容,帮助客户更好地了解保险价值,从而提升续保率
5.5 结论
决策树模型解释了客户续保率与年龄、婚姻状态、职业、产品等特征的关联性,提示保险公司应根据这些特征的差异性设计策略,优化客户体验和服务,同时重点加强对年轻未婚用户、单身退休用户等低续保率人群的关注。通过个性化沟通和定制化产品,提升客户续保率和满意度。
6. 项目流程
6.1 数据探索性分析(EDA)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
df_data = pd.read_excel("policy_data.xlsx")
print(df_data.info())
df_data.isnull().sum()
fig,ax = plt.subplots(2,3,figsize=(20,8))
plt.subplots_adjust(left=0.05,right=0.95,bottom=0.05,top=0.95,hspace=0.5,wspace=0.3)
sns.histplot(data=df_data,x='age',hue='renewal',kde=True,element='step',multiple='layer',ax=ax[0,0])
ax[0,0].set_title("年龄与续保")
sns.countplot(data=df_data,x='renewal',hue='gender',width=0.4,ax=ax[0,1])
ax[0,1].set_title("性别与续保")
relate_income_renewal = df_data.groupby(['income_level','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_income_renewal,x='income_level',y='count',hue='renewal',width=0.6,ax=ax[0,2])
ax[0,2].set_title("收入水平与续保")
relate_occupation_renewal = df_data.groupby(['occupation','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_occupation_renewal,x='count',y='occupation',hue='renewal',width=0.6,ax=ax[1,0])
ax[1,0].set_title("职业与续保")
relate_edu_renewal = df_data.groupby(['education_level','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_edu_renewal,x='education_level',y='count',hue='renewal',width=0.6,ax=ax[1,1])
ax[1,1].set_title("教育水平与续保")
relate_marital_renewal = df_data.groupby(['marital_status','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_marital_renewal,x='marital_status',y='count',hue='renewal',width=0.6,ax=ax[1,2])
ax[1,2].set_title("婚姻状况与续保")
plt.savefig("客户特征与续保.png")
fig,ax = plt.subplots(2,2,figsize=(12,6))
plt.subplots_adjust(left=0.05,right=0.95,bottom=0.05,top=0.95,hspace=0.5,wspace=0.3)
relate_policy_renewal = df_data.groupby(['policy_type','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_policy_renewal,x='count',y='policy_type',hue='renewal',width=0.6,ax=ax[0,0])
ax[0,0].set_title("产品与续保")
relate_term_renewal = df_data.groupby(['policy_term','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_term_renewal,y='count',x='policy_term',hue='renewal',width=0.6,ax=ax[0,1])
ax[0,1].set_title("保单年限与续保")
sns.histplot(data=df_data,x='premium_amount',hue='renewal',element='step',multiple='layer',kde=True,ax=ax[1,0])
ax[1,0].set_title("保费金额与续保")
relate_claim_renewal = df_data.groupby(['claim_history','renewal']).size().reset_index(name='count')
sns.barplot(data=relate_claim_renewal,x='claim_history',y='count',hue='renewal',width=0.3,ax=ax[1,1])
ax[1,1].set_title("理赔历史与续保")
plt.savefig('保单特征与续保.png')
gdf = gpd.read_file('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json').to_crs('EPSG:4573')
gdf['area'] = gdf.area/1e6/1e4
gdf = gdf[:-1]
data_rr = pd.DataFrame(df_data.groupby(['insurance_region'])['renewal'].apply(lambda x:(x=='Yes').sum()/x.count()))
gdf = gdf.join(data_rr,on='name')
fig,ax = plt.subplots(figsize=(15,15))
fontdict = {'family':'SimHei', 'size':8, 'color': "black",'weight': 'bold'}
gdf.plot(ax=ax,column='renewal',cmap='coolwarm',legend=True,legend_kwds={'label': "续保率", 'shrink':0.5})
for index in gdf.index:
x = gdf.iloc[index].geometry.centroid.x
y = gdf.iloc[index].geometry.centroid.y
t = f"{gdf.iloc[index]['name']}:{gdf.iloc[index]['renewal']*100:.0f}%"
ax.text(x,y,t,ha='center',va='center',fontdict=fontdict)
ax.axis('off')
ax.set_title('保单地区续保率')
plt.savefig('保单地区续保率.png')
numeric_df = df_data.select_dtypes(include=['int64','float64'])
correlation = numeric_df.corr()
sns.heatmap(data=correlation,annot=True,cmap='coolwarm',linewidths=.5)
plt.savefig('correlation.png')
var_with_renewal = ['gender', 'marital_status', 'claim_history', 'income_level', 'education_level']
results = []
for var in var_with_renewal:
contingency_table = pd.crosstab(df_data[var], df_data['renewal'])
chi2, p_value, dof, expected = chi2_contingency(contingency_table)
results.append({
'Variable': var,
'Chi2': chi2,
'P_value': p_value,
'Significant': p_value < 0.05
})
results_df = pd.DataFrame(results).sort_values('P_value')
display(results_df)
6.2 数据预处理
le = LabelEncoder().fit(df_data.renewal)
df_data["renewal_encode"] = le.transform(df_data["renewal"])
class_to_index = {target:index for index, target in enumerate(le.classes_)}
print(class_to_index)
X = df_data.drop(["policy_id","policy_end_date","renewal","renewal_encode"], axis=1)
y = df_data["renewal_encode"].astype('int')
X.policy_start_date = X.policy_start_date.dt.year.astype(str)
numeric_features = X.select_dtypes(exclude=["object"]).columns.to_list()
categorical_features = X.select_dtypes(include=["object"]).columns.to_list()
print(f"数值变量:{numeric_features}")
print(f"分类变量:{categorical_features}")
6.3 逻辑回归模型预测续保
transformers = [('num', StandardScaler(), numeric_features), ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)]
preprocessor = ColumnTransformer(transformers=transformers)
pipeline = Pipeline([('preprocessor',preprocessor),('classifier',LogisticRegression(random_state=42))])
param_grid = [{'classifier__C':[0.001,0.01,0.1,1.0,10,100], 'classifier__penalty':['l1','l2']}]
grid_search = GridSearchCV(estimator=pipeline, param_grid=param_grid, cv=5, verbose=1)
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,random_state=42,stratify=y)
grid_search.fit(X_train,y_train)
print(f"最佳参数:{grid_search.best_params_}")
print(f"最佳交叉验证分数:{grid_search.best_score_}")
def get_feature_names_from_pipeline(grid_search):
feature_names = grid_search.best_estimator_.named_steps['preprocessor'].get_feature_names_out()
coefficients = grid_search.best_estimator_.named_steps['classifier'].coef_[0]
df = pd.DataFrame({'feature':feature_names,'coefficient':coefficients})
df = df.sort_values(by='coefficient',key=abs,ascending=False)
return df
df_coef = get_feature_names_from_pipeline(grid_search)
df_coef.to_excel('coefficient.xlsx')
df_coef_top = df_coef.head(20)
plt.figure(figsize = (10,5))
ax = sns.barplot(data=df_coef_top,y='feature',x='coefficient',palette=['red' if x<0 else 'blue' for x in df_coef_top['coefficient']])
plt.axvline(x=0,color='black',linestyle='-',alpha=0.3)
for i,value in enumerate(df_coef_top['coefficient']):
ax.text(value,i,f'{value:.2f}',ha='left' if value>0 else 'right',va='center',)
plt.title(f"逻辑回归特征系数(权重 Top 20)")
plt.xlabel('Feature')
plt.ylabel('coefficient')
plt.tight_layout()
plt.savefig("coef_logistic.png")
plt.show()
best_model = grid_search.best_estimator_
pred_logic = best_model.predict(X_test)
pred_proba_logic = best_model.predict_proba(X_test)[:,1]
print(f"详细分类报告 - 逻辑回归模型:\n{classification_report(y_test,pred_logic)}")
cm_logic = confusion_matrix(y_test,pred_logic,labels=[0,1])
def plot_confusion_matrix(cm):
TN,FP,FN,TP = cm.ravel()
recall = TP / (TP + FN) if (TP + FN) > 0 else 0
precision = TP / (TP + FP) if (TP + FP) > 0 else 0
plt.figure(figsize = (4.5,4))
sns.heatmap(cm,fmt='d',annot=True,cmap='Blues',xticklabels=["不续保","续保"],yticklabels=["不续保","续保"])
plt.title(f"recall:{recall:.3f}\nprecision:{precision:.3f}")
plt.xlabel('Predict label')
plt.ylabel('True label')
plt.tight_layout()
plt.savefig("confusion_matrix_logistic.png")
plt.show()
print("混淆矩阵 - 逻辑回归预测:")
plot_confusion_matrix(cm_logic)
precision_lg,recall_lg,thresholds_lg = precision_recall_curve(y_test, pred_proba_logic)
pr_auc_lg = auc(recall_lg, precision_lg)
plt.figure(figsize = (5,5))
plt.plot(recall_lg,precision_lg,label=f'PR Curve (AUC={pr_auc_lg:.3f})')
close_idx = np.argmin(np.abs(thresholds_lg-0.5))
close_recall_lg = recall_lg[close_idx]
close_precision_lg = precision_lg[close_idx]
plt.plot(close_recall_lg,close_precision_lg,marker='o',markersize=8,fillstyle='none', label=f'Threshold=0.5 recall={close_recall_lg:.3f} precision={close_precision_lg:.3f}')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall curve')
plt.legend()
plt.savefig("precision_recall_logistic.png")
plt.show()
6.4 决策树模型预测续保
transformers = [('num', 'passthrough', numeric_features), ('cat', OrdinalEncoder(), categorical_features)]
preprocessor = ColumnTransformer(transformers=transformers)
pipeline = Pipeline([('preprocessor',preprocessor),('classifier',DecisionTreeClassifier(random_state=42))])
param_grid = [{'classifier__max_depth':[1,3,5,7]}]
grid_search = GridSearchCV(estimator=pipeline, param_grid=param_grid, cv=5, verbose=1)
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,random_state=42,stratify=y)
grid_search.fit(X_train,y_train)
print(f"最佳参数:{grid_search.best_params_}")
print(f"最佳交叉验证分数:{grid_search.best_score_}")
def get_feature_names_from_pipeline(grid_search):
feature_names = grid_search.best_estimator_.named_steps['preprocessor'].get_feature_names_out()
importance = grid_search.best_estimator_.named_steps['classifier'].feature_importances_
df = pd.DataFrame({'feature':feature_names,'importance':importance})
df = df.sort_values(by='importance',ascending=False)
return df
df_importance = get_feature_names_from_pipeline(grid_search)
plt.figure(figsize = (8,5))
ax = sns.barplot(data=df_importance[:15],x="importance",y="feature")
for i,imp in enumerate(df_importance[:15]['importance']):
plt.text(imp,i,f'{imp:.2f}',ha='left',va='center')
plt.title('Feature Importance')
plt.tight_layout()
plt.savefig('feature_importance.png')
plt.show()
cat_feature_select = ['marital_status','education_level','occupation']
X_select = X[numeric_features+cat_feature_select]
X_select_train,X_select_text,y_select_train,y_select_test = train_test_split(X_select,y,test_size=0.2,random_state=42,stratify=y)
transformers = [('num', 'passthrough', numeric_features), ('cat', OneHotEncoder(), cat_feature_select)]
preprocessor = ColumnTransformer(transformers=transformers)
pipeline = Pipeline([('preprocessor',preprocessor),('classifier',DecisionTreeClassifier(random_state=42))])
param_grid = [{'classifier__max_depth':[1,3,5,7]}]
grid_search_tree = GridSearchCV(estimator=pipeline, param_grid=param_grid, cv=5, verbose=1)
grid_search_tree.fit(X_select_train,y_select_train)
print(f"最佳参数:{grid_search_tree.best_params_}")
print(f"最佳交叉验证分数:{grid_search_tree.best_score_}")
best_model_tree = grid_search_tree.best_estimator_
feature_names_select = best_model_tree.named_steps['preprocessor'].get_feature_names_out().tolist()
export_graphviz(best_model_tree.named_steps['classifier'],out_file='tree.dot',class_names=['不续保','续保'], feature_names=feature_names_select, impurity=False,filled=True)
plt.figure(figsize = (10,5))
plot_tree(best_model_tree.named_steps['classifier'], feature_names=feature_names_select, class_names=['不续保','续保'], max_depth=3, filled=True, rounded=True, precision=2, fontsize=8, proportion=True)
plt.title('决策树模型')
plt.tight_layout()
plt.savefig('decision_tree.png')
plt.show()
best_model_tree = grid_search.best_estimator_
pred_dt = best_model_tree.predict(X_test)
pred_proba_dt = best_model_tree.predict_proba(X_test)[:,1]
print(f"详细分类报告 - 决策树模型:\n{classification_report(y_test,pred_dt)}")
cm_dt = confusion_matrix(y_test,pred_dt,labels=[0,1])
def plot_confusion_matrix(cm):
TN,FP,FN,TP = cm.ravel()
recall = TP / (TP + FN) if (TP + FN) > 0 else 0
precision = TP / (TP + FP) if (TP + FP) > 0 else 0
plt.figure(figsize = (4.5,4))
sns.heatmap(cm,fmt='d',annot=True,cmap='Blues',xticklabels=["不续保","续保"],yticklabels=["不续保","续保"])
plt.title(f"recall:{recall:.3f}\nprecision:{precision:.3f}")
plt.xlabel('Predict label')
plt.ylabel('True label')
plt.tight_layout()
plt.savefig("confusion_matrix_decision_tree.png")
plt.show()
print("混淆矩阵 - 决策树预测:")
plot_confusion_matrix(cm_dt)
precision_dt,recall_dt,thresholds_dt = precision_recall_curve(y_test, pred_proba_dt)
pr_auc_lg = auc(recall_dt, precision_dt)
plt.figure(figsize = (5,5))
plt.plot(recall_dt,precision_dt,label=f'PR Curve (AUC={pr_auc_lg:.3f})')
close_idx = np.argmin(np.abs(thresholds_dt-0.5))
close_recall_lg = recall_dt[close_idx]
close_precision_lg = precision_dt[close_idx]
plt.plot(close_recall_lg,close_precision_lg,marker='o',markersize=8,fillstyle='none', label=f'Threshold=0.5 recall={close_recall_lg:.3f} precision={close_precision_lg:.3f}')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall curve')
plt.legend()
plt.savefig("precision_recall_decision_tree.png")
plt.show()