一、业务背景
- 业务环境(宏观、业界、社会)、发展现状、发展趋势、衡量指标
- 业务目标:针对保险公司的健康险产品用户,制作用户画像,然后进行精准保险营销。
二、案例数据
- 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)