机器学习案例:保险行业数据分析

2,998 阅读9分钟

一、业务背景

  • 业务环境(宏观、业界、社会)、发展现状、发展趋势、衡量指标
  • 业务目标:针对保险公司的健康险产品用户,制作用户画像,然后进行精准保险营销。

二、案例数据

  • 1、数据来源:美国某保险公司,现有一款新的医疗险准备上市。
  • 2、产品:这是一款针对65岁以上的人群退出的医疗附加险,销售渠道是直邮。
  • 3、商业目的:为该产品做用户画像,找出最具有购买倾向的人群进行保险营销。
  • 4、数据介绍:共76个字段 4.6金融信息 4.8家庭状况

三、案例分析

1、分析流程

步骤操作
观察数据1、样本量和特征个数、数据类型、基本信息;2、统计基本信息、空值数量;3、检查重复值
探索数据及可视化分析1、样本是否平衡?2、用户年龄分布?3、用户年龄与购买商业医疗保险间的关系?4、用户性别与购买保险的关系?5、用户学历与购买保险的关系?6、根据业务理解进行探索分析
空值填充1、哪些特征有空值?2、空值个数和比例?3、空值特点及填充策略?4、如何填充?
变量编码1、删除无效特征;2、不同特征应该如何编码?
数据建模1、切分数据集;2、查看模型基础效果;3、模型调参

2、Python代码实现

(一)读入数据并查看数据
import numpy as np
import pandas as pd
df1 = pd.read_csv('ma_resp_data_temp.csv', header = 0)#读入原始数据
df1.head()#查看数据
df1.tail()#看尾部数据
df1.shape
df1.info() #是否有缺失,数据类型 
df1.dtypes

# 记录最初的数据类型,保存下来,方便后面进行对比
type_first=df1.dtypes
type_first.to_excel('var_type_original.xlsx') #保存中间变量
df1.describe().T#描述性统计信息,非空的数据
# 把这个统计值保存下来,方便后面查看
describe=df1.describe().T
describe.to_excel('describe_var.xlsx')

# 字符型变量的描述性统计
describe2=df1.describe(include=['O']).T
describe2

(二)数据清洗:重复值、无效字段、缺失值
# 查看数据中是否有重复值
df2.duplicated().sum()
# 选出重复的
df2[df2.duplicated()]

# 删除无效字段
# 第一列是用户ID,这一列作为数值存在时没有意义的
# 注意:购买决策之后才能得到的信息也不能作为变量参与建模
df2=df1.drop('KBM_INDV_ID',axis=1)
df2.head()

# 缺失值处理
# 统计一下每一列中有多少个空值
df2.dropna(axis =1).shape #删除有空值的列
df2.shape[1]-df2.dropna(axis =1).shape[1] #总的多少列有缺失
df_null_sum = df2.isnull().sum() #统计各个列中空值的数量
df_null_sum
# 选出缺失数量大于0的列
df_null_sum[df_null_sum>0]

# 将那些不为零的数据过滤出来
NA=df2.isnull().sum()
NA=NA.reset_index()
NA.columns=['Var','NA_count']#修改列名
# 过滤出大于0的数据,drop=True忽略原来的索引
NA=NA[NA.NA_count>0].reset_index(drop=True)
NA

# 计算缺失比例
NA.NA_count / df2.shape[0]
# 发现保留位数太多,改成百分比
(NA.NA_count / df2.shape[0])*100
NA['空值百分比例'] = ((NA.NA_count / df2.shape[0])*100).round(2)
NA

  • 结论: 各个特征中空值所占百分比很低,没有需要因为空值太多而需要删除的列
  • 填充:类别型变量(字符型或离散数值型),如性别,用众数填充。连续型变量(连续数值),如身高,用均值、中位数填充。
把含有空值的列中, 每一列的数据类型统计出来
# lambda函数和map函数作用于NA的Var列,返回数据类型
NA.Var.map(lambda x:df2[x].dtypes)
# 或者:写一个循环依次判断并返回数据类型
temp = []
for feature in NA.Var:
    temp.append(df2[feature].dtypes)
NA['数据类型'] = temp

空值填充策略
分类变量: 对于数据中的分类变量, 我们统一采用众数进行填充, 通常哪个分类水平出现的次数多, 这个出现的概率就是最高的.
数值型变量: 有一些变量虽然看起来是数值,但是已经是经过分箱之后的结果, 所以其实也是分类变量.
例如幸福指数LIVEWELL, 收入所处排名c210cip等
df2.HINSUB.value_counts()#区名字
df2.c210cip.value_counts()#收入所处排名
df2.LIVEWELL.value_counts()#幸福指数
df2.age.value_counts()#年龄

# 决定:除年龄之外的其他特征, 我们采取众数进行填充. 年龄采取均值进行填充
#首先将NA中的列名,去除掉年龄age这个
NA[NA.Var != 'age']
# 另一种填充的lambda写法
NA[NA.Var != 'age'].Var.map(lambda x:df2[x].fillna(df2[x].mode()[0],inplace=True))
# 函数写法填充空缺值:一次性实现,更灵活
def f(x):
    if x== 'age':
        df2[x].fillna(df2[x].mean(),inplace= True)
    else:
        df2[x].fillna(df2[x].mode()[0],inplace= True)
NA.Var.apply(f)
df2.info()#确认填充完毕
df2.to_excel('data_clean.xlsx')#保存清洗后的数据
(三)探索性分析
# 探索性数据分析:可视化数据查看数据分布情况
# 引入画图模块
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
plt.style.use('seaborn')#设置样式
# 支持中文
plt.rcParams['font.sans-serif']=['SimHei']# 支持中文
plt.rcParams['axes.unicode_minus'] = False #正常显示负号
  • 目标变量:平衡?
# 看一下目标变量是否平衡?
df2.resp_flag.sum()/df2.resp_flag.shape[0]
# 查看是否购买比例并绘图https://www.jianshu.com/p/8bb06d3fd21b
plt.figure(1,figsize=(10,3))#inch
sns.countplot(y = 'resp_flag' , data = df2)
plt.show()

  • 购买保险的用户相对较少一些,但相对比较平衡

  • 年龄

#https://www.cnblogs.com/feffery/p/11128113.html
# 绘制年龄分布情况
plt.figure(1 , figsize = (8 , 5))
sns.distplot(df2['age'],bins=40)
plt.xlim(([60,90]))
plt.xlabel('Age')

# 分别绘制两类样本的年龄分布
# 为连续型变量:年龄创建密度图 
sns.kdeplot(df2.age[df2.resp_flag==1], label='1', shade=True)#购买保险
sns.kdeplot(df2.age[df2.resp_flag==0], label='0', shade=True)#没有购买的
plt.xlim(([60,90]))
plt.xlabel('Age')
plt.ylabel('Density')

# 对年龄进行均匀分组,按照10岁一组进行划分,不同年龄的响应率
bins=np.arange(60,110,10)
df2['Age_band']=pd.cut(df2.age,bins)
df2.head()
#每个年龄段里面,响应比例
df2_ageband=df2.groupby(['Age_band','resp_flag'])['Age_band'].count()
df2_ageband
df2.groupby('Age_band')['resp_flag'].mean()
df2_ageband_survived=df2.groupby('Age_band')['resp_flag'].mean()
plt.figure(1 , figsize = (8 , 4))
df2_ageband_survived.plot.bar(title='resp_flag rate by age')
plt.ylabel('resp_rate')
plt.axhline(y=0.4,color='r',linestyle='--') 

  • 医疗覆盖比例
plt.figure(1 , figsize = (8 , 5))
sns.distplot(df2['meda'],bins=40)
plt.xlim(([0,100]))
plt.xlabel('meda')

# 为连续型变量:两类样本的医疗覆盖比例
sns.kdeplot(df2.meda[df2.resp_flag==1], label='1', shade=True)
sns.kdeplot(df2.meda[df2.resp_flag==0], label='0', shade=True)
plt.xlim(([0,100]))
plt.xlabel('meda')
plt.ylabel('Density')

  • 性别
# 查看性别比例
plt.figure(2, figsize = (15, 4),dpi=70)
plt.subplot(1,3,1)
sns.countplot(y = 'GEND' , data = df2)
plt.subplot(1,3,2)
df2.groupby('GEND')['GEND'].count().plot.pie(autopct = '%.2f%%')
plt.subplot(1,3,3)
#不同性别购买数量
sns.countplot(x='GEND', hue='resp_flag', data=df2); #设置 x,y 以及颜色控制的变量,以及画图的数据
plt.xlabel('性别比例')
plt.ylabel('购买数量')
plt.show()

df2[['GEND','resp_flag']].groupby(['GEND']).mean().plot.bar()

  • 学历情况
df2.c210mys.value_counts()#计数
# 查看学历分布情况
plt.figure(1 , figsize = (5 , 5))
sns.countplot(x = 'c210mys' , data = df2)
plt.show()

#查看不同的学历之间购买保险比例
sns.countplot(x='c210mys', hue='resp_flag', data=df2); #设置 x,y 以及颜色控制的变量,以及画图的数据
plt.xlabel('学历')
plt.ylabel('购买数量')
df2[['c210mys','resp_flag']].groupby(['c210mys']).mean().plot.bar()
plt.show()

  • 县级别的影响
sns.countplot(x='N2NCY', hue='resp_flag', data=df2); #设置 x,y 以及颜色控制的变量,以及画图的数据
plt.xlabel('县的大小');
plt.ylabel('购买数量');

df2[['N2NCY','resp_flag']].groupby(['N2NCY']).mean().plot.bar()

(四)变量编码
del df2['STATE_NAME'] #state删除
# 我们依然采用pandas进行数据编码
df2.MOBPLUS.value_counts()#是否通过快递买过东西
df_object = df2.select_dtypes('object')#选择类型为字符型的列
df_object.head()#保存的是原字符型编码
df_object2=df_object.copy()#labelencode编码后的数据
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()                                 #创建LabelEncoder()对象       
for col in df_object2.columns:                                            
    df_object2[col] = le.fit_transform(df_object2[col])        #编码
df_object2.head()
#直接复制一份进行编码
le = LabelEncoder()  
df3=df2.copy()#
for col in df3: # 对每一列进行遍历
    if df3[col].dtypes == 'object': # 如果数据类型是object类型
        df3[col] = le.fit_transform(df3[col])
        print(col,le.classes_)#编码的顺序
df3.head()
df3.info()
df3.to_excel('data_labelencode.xlsx')#保存经过labelencode的数据

(五)特征选择
#分析数据间关系,选择特征
df3.corr()
plt.figure(1 , figsize = (100 , 100))
sns.heatmap(df3.corr(),annot=True)#热图  annot 格子上显示数字
plt.savefig('corr.png')
plt.show()
(六)进行机器学习建模
from sklearn import tree
from sklearn.model_selection  import train_test_split
from sklearn.metrics import classification_report
from sklearn import metrics
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
import matplotlib.pylab as plt
from matplotlib.pylab import rcParams

X = df3.iloc[:, 1:-1]   #特征
y = df3['resp_flag']  #标签
# 将数据集7:3分,70%用来建模,30%用来测试
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3 ,random_state=1)
# 用默认模型试一下
clf = tree.DecisionTreeClassifier()
clf.fit(X_train,y_train)
clf.score(X_test,y_test)#准确率
  • 尝试调参
# 决策树中参数的含义解释如下
clf = tree.DecisionTreeClassifier(
            class_weight=None, #指定样本各类别的的权重,如果样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的"None"
            criterion='gini',#特征选择标准,可以使用"gini"或者"entropy",前者代表基尼系数,后者代表信息增益。一般说使用默认的基尼系数"gini"就可以了,即CART算法。除非你更喜欢类似ID3, C4.5的最优特征选择方法。 
            max_depth=None,#决策树最大深,常用的可以取值10-100之间。主要是限制树的增长
            max_features=None,#划分时考虑的最大特征数
            max_leaf_nodes=None,#最大叶子节点数,这个值限制了决策树的增长
            #min_samples_leaf=3000, #叶子节点最少样本数,用于减枝
            min_samples_split=5000,#内部节点再划分所需最小样本数,这个值限制了子树继续划分的条件
            min_weight_fraction_leaf=0.0, #叶子节点最小的样本权重和。这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝。 默认是0,就是不考虑权重问题。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。
            presort=False, #数据是否预排序
            splitter='best' #特征划分点选择标准,可以使用"best"或者"random"。前者在特征的所有划分点中找出最优的划分点。后者是随机的在部分划分点中找局部最优的划分点。)
  • (1)我们尝试使用最小叶节点样本数量和最小分割样本数量进行调参
# 使用roc_auc分数进行调参分数判断
clf = tree.DecisionTreeClassifier()
param_test={'min_samples_leaf':list(range(1000,6000,100)),'min_samples_split':list(range(4000,6000,100))}
gsearch = GridSearchCV(estimator=clf,param_grid = param_test, scoring='roc_auc', cv=5,verbose=2)
gsearch.fit(X_train,y_train)
gsearch.best_params_ #最优参数

gsearch.best_score_#最优的交叉验证的平均得分
gsearch.score(X_test, y_test)#测试集

  • (2)尝试使用最大深度和最小叶节点数量进行调参
clf = tree.DecisionTreeClassifier()
param_test= {"max_depth": range(3,15),'min_samples_leaf': range(200,3000,100)}
gsearch = GridSearchCV(estimator=clf,param_grid = param_test, scoring='roc_auc',n_jobs=-1, cv=5,verbose=2)
gsearch.fit(X_train,y_train)
#gsearch.grid_scores_, gsearch.best_params_, gsearch.best_score_
gsearch.best_params_

gsearch.best_score_
gsearch.score(X_test, y_test) #AUC的面积

(七)模型评估
y_pred = gsearch.predict(X_test)
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import recall_score

print('accuracy:',accuracy_score(y_test,y_pred)) #准确率
print('precision:',precision_score(y_test,y_pred)) #精确率
print('recall:',recall_score(y_test, y_pred))  #召回率
cm = confusion_matrix(y_test,y_pred,labels=[1,0])  #混淆矩阵
cm

#roc 曲线
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
%matplotlib inline
predict_proba = gsearch.predict_proba(X_test)
false_positive_rate, true_positive_rate, thresholds = roc_curve(y_test, predict_proba[:,1])
roc_auc = auc(false_positive_rate, true_positive_rate)#求面积
plt.title('ROC')
plt.plot(false_positive_rate, true_positive_rate, 'b',label='AUC = %0.3f'% roc_auc)
plt.legend(loc='lower right')
plt.plot([0,1],[0,1],'r--')
plt.ylabel('TPR')
plt.xlabel('FPR')
plt.savefig("ROC曲线.png",dpi=500,bbox_inches='tight')	#保存
plt.show()

(八)决策树可视化
clf = tree.DecisionTreeClassifier(max_depth=9, min_samples_leaf=200)
clf.fit(X_train, y_train)
features= list(X_train.columns) 
dot_data = tree.export_graphviz(clf,
feature_names= features,
class_names=['No Purchase',"Purchase"],  #字符串列表形式,默认为空,指定目标类的名称
filled=True,     #默认False,对节点着色以显示多数类
rounded=True,    #默认为False,若为True,节点框为圆角且用赫维提卡体代替默认的罗马字体
#rotate=True,    #默认为False,若为True,树将长成左右结构,而不是默认的上下结构
impurity=True,   #默认为True,是否为每个节点输出不纯度
leaves_parallel=False,   #默认为False,是否在树的底部画出叶节点
node_ids=True)

import graphviz
import os
os.chdir(graphviz-2.38\release\bin')
graph=graphviz.Source(dot_data)
graph.view()
  • 保存模型
from sklearn.externals import joblib
joblib.dump(gsearch, r'tree.pkl') 
clf = joblib.load('tree.pkl') 
clf.score(X_test, y_test) #AUC的面积
(九)看其他模型性能
  • 使用随机森林尝试建模
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier(n_estimators=200)
rfc.fit(X_train,y_train)
y_pred = rfc.predict(X_test)
print(rfc.score(X_train, y_train))
rfc.score(X_test, y_test)

from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier(n_estimators=1000)
rfc.fit(X_train,y_train)
y_pred = rfc.predict(X_test)
print(rfc.score(X_train, y_train))
rfc.score(X_test, y_test)

from sklearn.ensemble import AdaBoostClassifier
ada = AdaBoostClassifier(n_estimators=500,)
ada.fit(X_train,y_train)
y_pred = ada.predict(X_test)
print(ada.score(X_train, y_train))
ada.score(X_test, y_test)