kaggle实战-信用卡客户流失预警
带来一篇关于kaggle客户流失预测的数据分析与建模的文章,主要内容:
- 数据基本信息
- 数据EDA分析
- 特征工程和编码
- 基于3大分类模型的建模和预测、评分
- 基于随机搜索和网格搜索调参优化
背景
近年来,不论是传统行业还是互联网行业,都面临着用户流失问题。一般在银行、电话服务公司、互联网公司、保险等公司,经常使用客户流失分析和客户流失率作为他们的关键性业务指标之一。
一般情况下,留住现有客户的成本是远低于获得新客户的成本。因此在这些公司都有自己的客户服务部门来挽回现有即将流失的客户,因为现有客户对公司来说比新客户更具有价值。
记住一点:获客成本高,用户留存很重要
导入库
In [1]:
1 | import numpy as np |
In [2]:
1 | df = pd.read_csv("BankChurners.csv") |
Out[2]:
数据基本信息
1 | df.shape |
结果显示总共是10127行数据,23个字段
In [3]:
1 | # 全部字段 |
Out[3]:
1 | Index(['CLIENTNUM', 'Attrition_Flag', 'Customer_Age', 'Gender', |
字段解释为:
- CLIENTNUM:Client number - Unique identifier for the customer holding the account
- Attrition_Flag:Flag indicative of account closure in next 6 months (between Jan to Jun 2013)
- Customer_Age:Age of the account holder
- Gender:Gender of the account holder
- Dependent_count:Number of people financially dependent on the account holder
- Education_Level:Educational qualification of account holder (ex - high school, college grad etc.)
- Marital_Status:Marital status of account holder (Single, Married, Divorced, Unknown)
- Income_Category:Annual income category of the account holder
- Card_Category:Card type depicting the variants of the cards by value proposition (Blue, Silver and Platinum)
- Months_on_book:Number of months since the account holder opened an an account with the lender
- Total_Relationship_Count:Total number of products held by the customer. Total number of relationships the account holder has with the bank (example - retail bank, mortgage, wealth management etc.)
- Months_Inactive_12_mon:Total number of months inactive in last 12 months
- Contacts_Count_12_mon:Number of Contacts in the last 12 months. No. of times the account holder called to the call center in the past 12 months
- Credit_Limit:Credit limit
- Total_Revolving_Bal:Total amount as revolving balance
- Avg_Open_To_Buy:Open to Buy Credit Line (Average of last 12 months)
- Total_Amt_Chng_Q4_Q1:Change in Transaction Amount (Q4 over Q1)
- Total_Trans_Amt:Total Transaction Amount (Last 12 months)
- Total_Trans_Ct:Total Transaction Count (Last 12 months)
- Total_Ct_Chng_Q4_Q1:Change in Transaction Count (Q4 over Q1)
- Avg_Utilization_Ratio:Average Card 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
In [4]:
1 | df.dtypes |
Out[4]:
通过下面的代码能够统计不同类型下的字段数量:
1 | # 不同字段类型的统计 |
1 | df.describe().style.background_gradient(cmap="ocean_r") # 表格美化输出 |
df数据的描述统计信息美化输出(部分字段)
缺失值
In [7]:
1 | # 每个字段的缺失值统计 |
根据值的降序排列,第一个是0,结果表明数据本身是没有缺失值的**
删除无关字段
In [9]:
1 | no_use = np.arange(21, df.shape[1]) # 最后两个字段 |
Out[9]:
1 | array([21, 22]) |
In [10]:
1 | # 1、删除多个字段 |
In [11]:
CLIENTNUM表示的客户编号的信息,对建模无用直接删除:
1 | # 2、删除单个字段 |
新生成的df的字段(删除了无效字段之后):
In [12]:
1 | df.columns |
Out[12]:
1 | Index(['Attrition_Flag', 'Customer_Age', 'Gender', 'Dependent_count', |
In [13]:
再次查看数据的描述统计信息:
1 | df.describe().style.background_gradient(cmap="ocean_r") |
EDA-Exploratory Data Analysis
基于使用频率和数值特征
In [14]:
取出和用户的数值型字段信息:
1 | # df_frequency = df[["Customer_Age","Total_Trans_Ct","Total_Trans_Amt","Months_Inactive_12_mon","Credit_Limit","Attrition_Flag"]] 效果同下 |
探索在不同的Attrition_Flag下,两两字段之间的关系:
In [15]:
1 | df["Attrition_Flag"].value_counts() |
Out[15]:
1 | Existing Customer 8500 # 现有顾客 |
结果表明:现有顾客为8500,流失客户为1627
In [16]:
1 | # 定义画布大小 |
基于plotly的实现:
1 | for col in ["Customer_Age","Total_Trans_Amt","Months_Inactive_12_mon","Credit_Limit"]: |
上main展示的一个字段和Total_Trans_Ct的关系。下面是基于go.Scatter实现:
1 | # 生成一个副本 |
蓝色:现有客户;黄色:流失客户
我们得到如下的几点结论:
- 图1:用户每年花费的金额越高,越可能留下来(非流失)
- 2-3个月不进行互动,用户流失的可能性较高
- 用户的信用额度越高,留下来的可能性越大
- 从图3中观察到:流失客户的信用卡使用次数大部分低于100次
- 从第4个图中观察到,用户年龄分布不是重要因素
基于用户人口统计信息
用户的人口统计信息主要是包含:用户年龄、性别、受教育程度、状态(单身、已婚等)、收入水平等信息
In [21]:
取出相关的字段进行分析:
1 | df_demographic=df[['Customer_Age', |
不同类型顾客的年龄分布
In [22]:
1 | px.violin(df_demographic, |
从上面的小提琴图看出来,不同类型的用户在年龄上的分布是类似的。
结论:年龄并不是用户是否流失的关键因素
年龄分布
查看整体数据中用户的年龄分布情况:
1 | fig = make_subplots(rows=2, cols=1) |
可以看到年龄基本上是呈现正态分布的,大多数集中在40-55之间。
不同类型下不同性别顾客统计
In [23]:
1 | flag_gender = df.groupby(["Attrition_Flag","Gender"]).size(). ). (columns={0:"number"}) |
Out[23]:
Attrition_Flag | Gender | number | |
---|---|---|---|
0 | Attrited Customer | F | 930 |
1 | Attrited Customer | M | 697 |
2 | Existing Customer | F | 4428 |
3 | Existing Customer | M | 4072 |
In [24]:
1 | fig = px.bar(flag_gender, |
从上面的柱状图中看出来:
- 女性在本次数据中高于男性;在两种不同类型的客户中女性也是高于男性
- 数据不平衡:现有客户和流失客户是不平衡的,大约是8400:1600
交叉表统计分析
基于pandas中交叉表的数据统计分析。解释交叉表很好的文章:https://pbpython.com/pandas-crosstab.html
In [25]:
1 | fig, (ax1,ax2,ax3,ax4) = plt.subplots(ncols=4, figsize=(20,5)) |
我们放大再看:
可以观察到:在两种客户中,不同的教育水平和个人状态的分布是类似的。这个结论也验证了:年龄并不是影响现有或者流失客户的因素。
受教育程度
1 | fig = px.pie(df,names='Education_Level',title='Propotion Of Education Levels') |
对比两种客户数量
In [26]:
1 | churn = df["Attrition_Flag"].value_counts() |
Out[26]:
1 | Existing Customer 8500 |
In [27]:
1 | churn.keys() |
Out[27]:
1 | Index(['Existing Customer', 'Attrited Customer'], dtype='object') |
In [28]:
1 | plt.pie(x=churn, labels=churn.keys(),autopct="%.1f%%") |
上面的饼图表明:
- 现有客户还是占据了绝大部分
- 后面将通过采样的方式使得两种类型的客户数量保持平衡。
相关性
现有数据中的字段涉及到分类型和数值型,采取不同的分析和编码方式
- 数值型变量:使用相关系数Pearson
- 分类型变量:使用Cramer’s V ;克莱姆相关系数,常用于分析双变量之间的关系
参考内容:https://blog.csdn.net/deecheanW/article/details/120474864
1 | # 字符型字段 |
对Attrition_Flag字段执行独热码编码操作:
In [31]:
1 | # 先保留原信息 |
类型编码
In [34]:
1 | from sklearn import preprocessing |
计算克莱姆系数-cramers_V
In [35]:
1 | from scipy.stats import chi2_contingency |
In [36]:
1 | rows = [] |
In [37]:
1 | # 克莱姆系数下的热力图 |
绘制相关的热力图:
1 | mask = np.triu(np.ones_like(cramerv_matrix, dtype=np.bool)) |
1 | # 基于数值型字段的相关系数 |
1 | fig, ax = plt.subplots(ncols=2, figsize=(15,6)) |
小结:从上面右侧的热力图中能看到下面的字段和流失类型客户是无相关的。相关系数的值在正负0.1之间(右图)
- Credit Limit
- Average Open To Buy
- Months On Book
- Age
- Dependent Count
现在我们考虑将上面的字段进行删除:
In [41]:
1 | df_model = df.copy() |
用户标识编码
In [42]:
1 | df_model['Attrition_Flag'] = df_model['Attrition_Flag'].map({'Existing Customer': 1, 'Attrited Customer': 0}) |
剩余字段的独热码:
1 | df_model=pd.get_dummies(df_model) |
建模
切分数据
在之前已经验证过现有客户和流失客户的数量是不均衡的,我们使用SMOTE(Synthetic Minority Oversampling Technique,通过上采样合成少量的数据)采样来平衡数据。
In [50]:
1 | from sklearn.model_selection import train_test_split |
In [51]:
1 | # 特征和目标变量 |
SMOTE采样
In [52]:
1 | sm = SMOTE(sampling_strategy="minority", k_neighbors=20, random_state=42) |
3种模型
In [53]:
1 | # 1、随机森林 |
Out[53]:
1 | RandomForestClassifier() |
一般在使用树模型建模的时候数据不需要归一化。但是在使用支持向量机的时候需要:
In [54]:
1 | # 2、支持向量机 |
Out[54]:
1 | Pipeline(steps=[('standardscaler', StandardScaler()), |
In [55]:
1 | # 3、提升树 |
Out[55]:
1 | GradientBoostingClassifier(learning_rate=1.0, max_depth=1, random_state=42) |
模型预测
In [56]:
1 | y_rf = rf.predict(X_test) |
混淆矩阵
In [57]:
1 | from sklearn.metrics import plot_confusion_matrix |
分类模型得分
In [58]:
1 | # classification_report, recall_score, precision_score, f1_score |
从3种模型的混淆矩阵和分类模型的相关评价指标来看:可以看到随机森林和提升树的结果都是优于支持向量机的
模型调参优化
针对随机森林和提升树模型采用两种不同的调参优化方法:
- 随机森林:随机搜索调参
- 梯度提升树:网格搜索调参
随机搜索调参-随机森林模型
In [59]:
1 | from sklearn.model_selection import RandomizedSearchCV |
设置不同待调参数的取值:
In [60]:
1 | n_estimators = [int(x) for x in np.linspace(start = 200, stop = 2000, num = 10)] |
In [61]:
1 | # 每个tree的最大叶子数 |
Out[61]:
1 | [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, None] |
In [62]:
1 | min_samples_split = [2, 5, 10] |
随机搜索参数
In [64]:
1 | random_grid = {'n_estimators': n_estimators, |
搜索结果如下:
In [65]:
1 | rf_random = RandomizedSearchCV( |
使用搜索参数建模
使用上面搜索之后的参数再次建模:
In [67]:
1 | rf_clf_search= RandomForestClassifier(n_estimators=1400, |
调参后的混淆矩阵:左上角的449变成452,说明分类的更加准确了
网格搜索调参-提升树模型
网格搜索参数
In [68]:
1 | from sklearn.model_selection import GridSearchCV |
Out[68]:
1 | {'n_estimators': range(20, 100, 10)} |
In [69]:
1 | # 实施搜索 |
Out[69]:
1 | {'n_estimators': 90} |
使用搜索参数建模
In [71]:
1 | gb_clf_opt=GradientBoostingClassifier(n_estimators=90, # 搜索到的参数90 |
左上角的分类数目从454提升到456,也有一定的提升,但是效果并不是很明显
总结
本文从一份用户相关的数据出发,从数据预处理、特征工程和编码,到建模分析和调参优化,完成了整个用户流失预警的全流程分析。整体模型的结果准确率达到了95%,召回率也达到了84.2%。肯定还有提升的空间,欢迎一起讨论~