预测信用卡流失客户

kaggle数据地址:https://www.kaggle.com/sakshigoyal7/credit-card-customers

导入数据

#导入模块
import pandas as pd 
import numpy as np 
import  matplotlib.pyplot as plt
import seaborn as sns

BankChurners = pd.read_csv('D:\python_home\预测客户流失\bankchurners\BankChurners.csv')

简单的数据查看

BankChurners.columns

'''
Index(['CLIENTNUM', 'Attrition_Flag', 'Customer_Age', 'Gender',
       'Dependent_count', 'Education_Level', 'Marital_Status',
       'Income_Category', 'Card_Category', 'Months_on_book',
       'Total_Relationship_Count', 'Months_Inactive_12_mon',
       'Contacts_Count_12_mon', 'Credit_Limit', 'Total_Revolving_Bal',
       'Avg_Open_To_Buy', 'Total_Amt_Chng_Q4_Q1', 'Total_Trans_Amt',
       'Total_Trans_Ct', 'Total_Ct_Chng_Q4_Q1', 'Avg_Utilization_Ratio',
       'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
       'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2'],
      dtype='object')
'''

拿到百度翻译了一下

'''
'CLIENTNUM','消耗标志','客户年龄','性别',

“受抚养人数量”、“受教育程度”、“婚姻状况”,

'收入类别','卡片类别','账面上的月份',

'总关系数','月数'u不活跃'u 12个月',

“联系人数量12个月”,“信用额度”,“总周转余额”,

'平均开盘价到买入价','总金额',

'总交易量','第4季度总交易量','平均利用率',

'天真的_Bayes_分类器_消耗_标志_卡片_类别_联系人_Count_12 _mon_依赖性_Count_u教育程度_u个月_不活跃_12 _mon_1',

'天真的_Bayes_分类器_消耗_标志_卡片_类别_联系人_Count_12 _mon_依赖性_Count_受教育程度_u月_不活跃_12_mon_2'],

dtype='object'
'''

简单看一下缺失情况

#木有空值,但是不代表没有其他表达类型的空值,比如说'-'
BankChurners.isnull().sum()

我们按照数据类型分一下字段

#数据特征
numeric_features = BankChurners.select_dtypes(include=[np.number])
print(numeric_features.columns)
#类别特征
categorical_features = BankChurners.select_dtypes(include=[np.object])
print(categorical_features.columns)
Index(['CLIENTNUM', 'Customer_Age', 'Dependent_count', 'Months_on_book',
       'Total_Relationship_Count', 'Months_Inactive_12_mon',
       'Contacts_Count_12_mon', 'Credit_Limit', 'Total_Revolving_Bal',
       'Avg_Open_To_Buy', 'Total_Amt_Chng_Q4_Q1', 'Total_Trans_Amt',
       'Total_Trans_Ct', 'Total_Ct_Chng_Q4_Q1', 'Avg_Utilization_Ratio',
       'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
       'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2'],
      dtype='object')
Index(['Attrition_Flag', 'Gender', 'Education_Level', 'Marital_Status',
       'Income_Category', 'Card_Category'],
      dtype='object')

看看类别型变量的唯一值

for i in list(categorical_features.columns):
    print('*************************')
    print('{0}+的唯一值如下:{1}'.format(i,BankChurners[i].nunique()))
    print(BankChurners[i].unique())
*************************
Attrition_Flag+的唯一值如下:2
['Existing Customer' 'Attrited Customer']
*************************
Gender+的唯一值如下:2
['M' 'F']
*************************
Education_Level+的唯一值如下:7
['High School' 'Graduate' 'Uneducated' 'Unknown' 'College' 'Post-Graduate'
 'Doctorate']
*************************
Marital_Status+的唯一值如下:4
['Married' 'Single' 'Unknown' 'Divorced']
*************************
Income_Category+的唯一值如下:6
['$60K - $80K' 'Less than $40K' '$80K - $120K' '$40K - $60K' '$120K +'
 'Unknown']
*************************
Card_Category+的唯一值如下:4
['Blue' 'Gold' 'Silver' 'Platinum'] 

看来没有乱七八糟的空值

我们在看一下数值型变量的分布

plt.figure(figsize=(20,50))
col = list(numeric_features.columns)
for i in range(1,len(numeric_features.columns)+1):
    ax=plt.subplot(9,2,i) 
    sns.kdeplot(BankChurners[col[i-1]],bw=1.5)
    plt.xlabel(col[i-1])
plt.show()
    

 我们看上面变量,第一个变量CLIENTNUM 其实就是客户ID,我们可以不用理会

仔细看上面的图片,我们就会发现,其实有些字段其实是类别比较少的数值型变量,但是还是写代码看好,眼睛有时也会出卖自己

for i in list(numeric_features.columns):
    print('{0}+的唯一值如下:{1}'.format(i,BankChurners[i].nunique()))
CLIENTNUM+的唯一值如下:10127
Customer_Age+的唯一值如下:45
Dependent_count+的唯一值如下:6
Months_on_book+的唯一值如下:44
Total_Relationship_Count+的唯一值如下:6
Months_Inactive_12_mon+的唯一值如下:7
Contacts_Count_12_mon+的唯一值如下:7
Credit_Limit+的唯一值如下:6205
Total_Revolving_Bal+的唯一值如下:1974
Avg_Open_To_Buy+的唯一值如下:6813
Total_Amt_Chng_Q4_Q1+的唯一值如下:1158
Total_Trans_Amt+的唯一值如下:5033
Total_Trans_Ct+的唯一值如下:126
Total_Ct_Chng_Q4_Q1+的唯一值如下:830
Avg_Utilization_Ratio+的唯一值如下:964
Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1+的唯一值如下:1704
Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2+的唯一值如下:640

我们把它归为一类

num_cate_col = [  'Dependent_count', 
       'Total_Relationship_Count', 'Months_Inactive_12_mon',
       'Contacts_Count_12_mon', 
       ]

剩下的归为一类

numeric_col = [ x for x in list(numeric_features.columns) if x not in num_cate_col]
numeric_col.remove('CLIENTNUM')
numeric_col
'''
['Customer_Age',
 'Months_on_book',
 'Credit_Limit',
 'Total_Revolving_Bal',
 'Avg_Open_To_Buy',
 'Total_Amt_Chng_Q4_Q1',
 'Total_Trans_Amt',
 'Total_Trans_Ct',
 'Total_Ct_Chng_Q4_Q1',
 'Avg_Utilization_Ratio',
 'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
 'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2']
'''

我们在看看类别比较少的数值型变量

for i in num_cate_col:
    print('*************************')
    print('{0}+的唯一值如下:{1}'.format(i,BankChurners[i].nunique()))
    print(BankChurners[i].unique())
*************************
Dependent_count+的唯一值如下:6
[3 5 4 2 0 1]
*************************
Total_Relationship_Count+的唯一值如下:6
[5 6 4 3 2 1]
*************************
Months_Inactive_12_mon+的唯一值如下:7
[1 4 2 3 6 0 5]
*************************
Contacts_Count_12_mon+的唯一值如下:7
[3 2 0 1 4 5 6]

看看y值Attrition_Flag

BankChurners['Attrition_Flag'].value_counts().plot.pie(explode=[0,0.1],autopct='%1.1f%%')

 比例为16%,还好

画类别变量的直方图以及逾期概率图

cate_cold = list(categorical_features.columns)+num_cate_col
for i in cate_cold:
    tmp = pd.crosstab(BankChurners[i], BankChurners['Attrition_Flag'])
    tmp['总人数'] = tmp.sum(axis=1)
    tmp['流失率'] = tmp['Attrited Customer']/tmp['总人数']
    fig, ax1 = plt.subplots()
    ax1.bar(tmp.index,tmp['总人数'],color='green')
    ax2 = ax1.twinx() 
    ax2.plot(tmp.index,tmp['流失率'],color='red')
    plt.show()

 

 

 

 构建特征

2021.1.15来补充一下建模的部分

具体过程就不写了,直接附上代码,以及结果

# -*- coding: utf-8 -*-
"""
Created on Fri Jan 15 09:18:46 2021

@author: Administrator
"""

import pandas as pd 
import numpy as np 
import  matplotlib.pyplot as plt
import seaborn as sns
import pycard as pc

#%%导入数据
BankChurners = pd.read_csv('D:\python_home\预测客户流失\bankchurners\BankChurners.csv')

BankChurners.isnull().sum()

#%%区分数值型变量和类别型变量
num_col = ['Customer_Age',
 'Months_on_book',
 'Credit_Limit',
 'Total_Revolving_Bal',
 'Avg_Open_To_Buy',
 'Total_Amt_Chng_Q4_Q1',
 'Total_Trans_Amt',
 'Total_Trans_Ct',
 'Total_Ct_Chng_Q4_Q1',
 'Avg_Utilization_Ratio',
 'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
 'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2']


cate_col = [ i for i in list(BankChurners.columns) if i not in num_col]
cate_col.remove('CLIENTNUM')
cate_col
'''
['Attrition_Flag',
 'Gender',
 'Dependent_count',
 'Education_Level',
 'Marital_Status',
 'Income_Category',
 'Card_Category',
 'Total_Relationship_Count',
 'Months_Inactive_12_mon',
 'Contacts_Count_12_mon']
'''

#%%cate_col.unqion()
for i in cate_col:
    print(i,BankChurners[i].unique())
'''
Attrition_Flag ['Existing Customer' 'Attrited Customer']
Gender ['M' 'F']
Dependent_count [3 5 4 2 0 1]
Education_Level ['High School' 'Graduate' 'Uneducated' 'Unknown' 'College' 'Post-Graduate'
 'Doctorate']
Marital_Status ['Married' 'Single' 'Unknown' 'Divorced']
Income_Category ['$60K - $80K' 'Less than $40K' '$80K - $120K' '$40K - $60K' '$120K +'
 'Unknown']
Card_Category ['Blue' 'Gold' 'Silver' 'Platinum']
Total_Relationship_Count [5 6 4 3 2 1]
Months_Inactive_12_mon [1 4 2 3 6 0 5]
Contacts_Count_12_mon [3 2 0 1 4 5 6]
'''
    
#%%处理目标变量,赋值0和1 流失就是1 
BankChurners.Attrition_Flag = BankChurners.Attrition_Flag.map({'Existing Customer':0,'Attrited Customer':1})


#%%类别变量的iv计算
cate_iv_woedf = pc.WoeDf()
clf = pc.NumBin()
for i in cate_col[1:]:
    cate_iv_woedf.append(pc.cross_woe(BankChurners[i] ,BankChurners.Attrition_Flag))
cate_iv_woedf.to_excel('tmp11')


#尝试使用二者组合变量,发现并没有作用
BankChurners['Gender_Dependent_count'] = BankChurners.apply(lambda x :x.Gender+ str(x.Dependent_count),axis=1)
pc.cross_woe(BankChurners.Gender_Dependent_count,BankChurners.Attrition_Flag)
BankChurners['Education_Level_Income_Category'] = BankChurners.apply(lambda x :x.Education_Level+ str(x.Income_Category),axis=1)
pc.cross_woe(BankChurners.Education_Level_Income_Category,BankChurners.Attrition_Flag)
BankChurners.pop('Gender_Dependent_count')
BankChurners.pop('Education_Level_Income_Category')

#%%数值变量的iv值计算
num_iv_woedf = pc.WoeDf()
clf = pc.NumBin()
for i in num_col:
    clf.fit(BankChurners[i] ,BankChurners.Attrition_Flag)
    clf.generate_transform_fun()
    num_iv_woedf.append(clf.woe_df_)
num_iv_woedf.to_excel('tmp12')

#%%简单处理一下变量
#Total_Relationship_Count,Months_Inactive_12_mon,Contacts_Count_12_mon,类别型变量就不需要管了,保留原来的分区即可


#Credit_Limit
from numpy import *
BankChurners['Credit_Limit_bin'] = pd.cut(BankChurners.Credit_Limit,bins=[-inf, 1438.65, 1638.5, 1900.5, 2477.5, 3398.5, 5061.5, inf])
BankChurners['Total_Revolving_Bal_bin'] = pd.cut(BankChurners.Total_Revolving_Bal,bins=[-inf, 66.0, 581.5, 979.5, 2512.5, inf])
BankChurners['Avg_Open_To_Buy_bin'] = pd.cut(BankChurners.Avg_Open_To_Buy,bins=[-inf, 447.5, 1038.5, 1437.0, 1944.5, 2229.5,  inf])
BankChurners['Total_Amt_Chng_Q4_Q1_bin'] = pd.cut(BankChurners.Total_Amt_Chng_Q4_Q1,bins=[-inf,  0.3685, 0.4355, 0.5045, 0.5325, 1.0625, inf])
BankChurners['Total_Trans_Amt_bin'] = pd.cut(BankChurners.Total_Trans_Amt,bins=[-inf, 1001.0, 2010.5, 2729.5, 2932.5, 3152.0, 5365.0, 11093.0, inf])
BankChurners['Total_Trans_Ct_bin'] = pd.cut(BankChurners.Total_Trans_Ct,bins=[-inf, 20.5, 37.5, 47.5, 54.5, 57.5, 64.5, 78.5, inf])
BankChurners['Total_Ct_Chng_Q4_Q1_bin'] = pd.cut(BankChurners.Total_Ct_Chng_Q4_Q1,bins=[-inf, 0.4075,  0.4875, 0.504, 0.6015, 0.6565, inf])
BankChurners['Avg_Utilization_Ratio_bin'] = pd.cut(BankChurners.Avg_Utilization_Ratio,bins=[-inf,  0.0205, 0.4505, 0.7985,  inf])
BankChurners['Naive_Bayes1_bin'] = pd.cut(BankChurners.Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1,
                                         bins=[-inf, 0.4736, inf])
BankChurners['Naive_Bayes2_bin'] = pd.cut(BankChurners.Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2,
                                         bins=[-inf, 0.5264, inf])


iv_col = [i for i in ['Total_Relationship_Count','Months_Inactive_12_mon','Contacts_Count_12_mon',
          ] + list(BankChurners.columns)[-10:]]

cate_iv_woedf = pc.WoeDf()
clf = pc.NumBin()
for i in iv_col:
    cate_iv_woedf.append(pc.cross_woe(BankChurners[i] ,BankChurners.Attrition_Flag))
cate_iv_woedf.to_excel('tmp11')

BankChurners.Contacts_Count_12_mon[BankChurners.Contacts_Count_12_mon ==6] =5

#%%解决方案如下:
#解决方案:后面这两个就不入模型了,作为规则即可,不然会导致过拟合,上面的出现了无穷的变量要合并区间
iv_col = [i for i in ['Total_Relationship_Count','Months_Inactive_12_mon','Contacts_Count_12_mon',
          ] + list(BankChurners.columns)[-10:-2]]

cate_iv_woedf = pc.WoeDf()
clf = pc.NumBin()
for i in iv_col:
    cate_iv_woedf.append(pc.cross_woe(BankChurners[i] ,BankChurners.Attrition_Flag))
cate_iv_woedf.to_excel('tmp11')

#%%woe转换
pc.obj_info(cate_iv_woedf)

cate_iv_woedf.bin2woe(BankChurners,iv_col)

model_col = [i for i in ['CLIENTNUM', 'Attrition_Flag']+list(BankChurners.columns)[-11:]]

#%%建模
import pandas as pd
import matplotlib.pyplot as plt #导入图像库
import matplotlib
import seaborn as sns
import statsmodels.api as sm
from sklearn.metrics import roc_curve, auc
from sklearn.model_selection import train_test_split

X = BankChurners[list(BankChurners.columns)[-11:]]
Y = BankChurners['Attrition_Flag']


x_train,x_test,y_train,y_test=train_test_split(X,Y,test_size=0.3,random_state=0)

#(10127, 44)

X1=sm.add_constant(x_train)   #在X前加上一列常数1,方便做带截距项的回归
logit=sm.Logit(y_train.astype(float),X1.astype(float))
result=logit.fit()
result.summary()
result.params



X3 = sm.add_constant(x_test)
resu = result.predict(X3.astype(float))
fpr, tpr, threshold = roc_curve(y_test, resu)
rocauc = auc(fpr, tpr)
plt.plot(fpr, tpr, 'b', label='AUC = %0.2f' % rocauc)
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('真正率')
plt.xlabel('假正率')
plt.show()

模型的参数如下:

const -1.662204
Total_Relationship_C_woe -2.307529
Months_Inactive_12_woe -1.050666
Contacts_Count_12_woe -0.742126
Credit_Limit_woe -0.633916
Total_Revolving_Bal_woe -0.914706
Avg_Open_To_Buy_woe -0.120073
Total_Amt_Chng_Q4_Q1_woe -0.845637
Total_Trans_Amt_woe -0.924220
Total_Trans_Ct_woe -0.601850
Total_Ct_Chng_Q4_Q1_woe -0.493193
Avg_Utilization_Ratio_woe -0.265479

测试集的auc图片如下:

0.9651589169400466

 看看训练集的auc

resu_1 = result.predict(X1.astype(float))
fpr, tpr, threshold = roc_curve(y_train, resu_1)
rocauc = auc(fpr, tpr)
plt.plot(fpr, tpr, 'b', label='AUC = %0.2f' % rocauc)
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('真正率')
plt.xlabel('假正率')
plt.show()

0.9683602097734862

 放一下入模变量的iv值

 

 

 

 

 

 

 

 

 

 

 

 总结:

1.变量的逾期率不单调,没有关系,但是分箱时每个箱子不能太小,太小需要合并,最好挑选iv值比较好(2%以上)的入魔,使用woe去训练模型

2.关于iv值为无穷时候的做法:

IVi无论等于负无穷还是正无穷,都是没有意义的。
由上述问题我们可以看到,使用IV其实有一个缺点,就是不能自动处理变量的分组中出现响应比例为0或100%的情况。那么,遇到响应比例为0或者100%的情况,我们应该怎么做呢?建议如下:
(1)如果可能,直接把这个分组做成一个规则,作为模型的前置条件或补充条件;
(2)重新对变量进行离散化或分组,使每个分组的响应比例都不为0且不为100%,尤其是当一个分组个体数很小时(比如小于100个),强烈建议这样做,因为本身把一个分组个体数弄得很小就不是太合理。
(3)如果上面两种方法都无法使用,建议人工把该分组的响应数和非响应的数量进行一定的调整。如果响应数原本为0,可以人工调整响应数为1,如果非响应数原本为0,可以人工调整非响应数为1.

本次也出现了两种iv值为无穷的情况

 

 我的处理方法是:

解决方案:后面这两个就不入模型了,作为规则即可,不然可能导致过拟合,上面的出现了无穷的变量要合并区间

 还有一些个人信息的特征,比如年龄,婚姻等等,由于iv值很低就没有放进去,但是还是值得探讨一下,不过本次的模型的效果很好,就不做太复杂的模型了

但是还有一个疑惑是:

Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1
Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2

最后两个变量实在是太厉害了,不知道是不是别人训练好的y?

需要查资料看看

最后在官网找到了答案

 

原文地址:https://www.cnblogs.com/cgmcoding/p/14120510.html