kaggle实战-银行用户画像及流失预测
本文使用kaggle官网提供的一份银行用户的数据进行相关统计分析、统计分析和流失预测建模。主要内容包含:
简介
原数据地址:https://www.kaggle.com/datasets/sakshigoyal7/credit-card-customers
主要是参考方案:https://www.kaggle.com/code/thomaskonstantin/bank-churn-data-exploration-and-churn-prediction
自己也进行了一些改进,最终F_1 score在3种不同的模型上面都提升了4-5个点左右:
1、原结果:
2、个人结果
导入库
导入一些库,主要是用于数据处理、可视化和建模与评价。
In [2]:
1 | import numpy as np |
数据基本信息
In [4]:
1 | df = pd.read_csv("BankChurners.csv") |
查看数据的一些基本信息:
In [3]:
1 | df.shape # 数据量和列数 |
Out[3]:
1 | (10127, 23) |
In [4]:
1 | pd.value_counts(df.dtypes) # 不同的字段类型 |
Out[4]:
统计不同字段类型的数量:
1 | int64 10 |
In [5]:
描述统计信息的可视化:
1 | df.describe().style.background_gradient(cmap="ocean_r") |
Out[5]:
注意:部分字段截图;描述统计信息只针对数值型字段
删除无关字段
目前是最后两个+第一个字段
字段解释为:
- 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 [7]:
1 | df = df[df.columns[:-2]] |
先删除了一些无关的字段
缺失值情况
In [9]:
1 | ms.bar(df,color="blue") |
根据上面的图形结果显示数据中是没有缺失值的。下面同样可以证明:
1 | df.isnull().sum().sort_values(ascending=False) |
因为是降序排列,第一个取值就是0,也说明是没有缺失值的。
数据EDA-Exploratory Data Analysis
年龄分布
In [11]:
1 | fig = make_subplots(rows=1, cols=2) |
大致上用户年龄的分布是符合正态分布的。
不同教育程度人数占比
In [12]:
1 | fig = px.pie(df, |
不同性别、不同卡种类的用户对比
In [13]:
1 | fig = make_subplots( |
可以看到:整体用户中女性是多余男性的
家属人数-Dependent_count
In [14]:
1 | fig = make_subplots(rows=2, cols=1) |
不同家属人数的统计量基本符合正态分布,呈现轻微的左偏。
不同个人状态
In [15]:
1 | fig = px.pie(df, |
信用卡用户中大部分都是已婚人士;同时单身用户的量也很大,说明也是有一定需求。
不同收入水平对比-Income_Category
In [16]:
1 | number_of_income = df["Income_Category"].value_counts().reset_index() |
1 | fig = px.bar(number_of_income, |
Months_on_book
在银行有交易或者操作记录的时间长短
In [19]:
1 | fig = make_subplots(rows=2, cols=1) |
可以看到数据呈现明显的峰度,计算峰度的值:
In [20]:
1 | print("打印数据的峰度值: ", df["Months_on_book"].kurt()) |
利用小提琴图查看数据的分布:
In [21]:
1 | px.violin(y=df["Months_on_book"]) |
In [22]:
这个数据的分布有点意思:在36-37部分很突出,刚好和柱状图对应,导致小提琴图的中间部分被拉长。
我们观察在两种类型的用户中都是这样的分布:
在这种情况下,我们不能数据看成符合正态分布的特征属性
信用额度-Credit_Limit
In [23]:
1 | fig = px.violin(y=df["Credit_Limit"]) |
女性用户的信用额度普遍大于男性
1 | sns.displot(data=df, x=df["Credit_Limit"], kde=True) |
从分布来看,左偏十分严重,有一定的长尾现象。
最近12个月交易-Total_Trans_Amt
In [26]:
1 | fig = make_subplots(rows=2, cols=1) |
从上面的直方图观察到:用户近12个月的交易额的分布存在多组下的集中分布特性,说明根据这个特征能够将原数据分成不同的组别,对不同的组别进行分析。
现存和流失客户
In [27]:
1 | fig = px.pie(df, |
可以看到两种用户的占比是很不平衡的,后面考虑使用SMOTE采样方法进行处理。
多维度下的现存和流失客户数对比
In [28]:
1 | df.columns |
Out[28]:
1 | Index(['Attrition_Flag', 'Customer_Age', 'Gender', 'Dependent_count', |
In [29]:
1 | df1 = (df.groupby(["Gender","Education_Level","Marital_Status","Income_Category","Attrition_Flag"]) |
1 | fig = px.treemap( |
数据预处理
特征编码
In [31]:
1 | df.Attrition_Flag = df.Attrition_Flag.replace({'Attrited Customer':1,'Existing Customer':0}) |
In [32]:
1 | # 独热码 |
受教育水平使用独热码进行编码,同时删除Unknown的数据信息。下面分类型变量是同样操作:
In [33]:
1 | df = pd.concat([df,pd.get_dummies(df['Income_Category']).drop(columns=['Unknown'])],axis=1) |
把原始的几个字段删除:
In [34]:
1 | df.drop(columns = ['Education_Level','Income_Category', |
计算相关性系数
In [35]:
1 | # 基于plotly实现 |
基于SMOTE采样处理
In [36]:
1 | # 1、切分数据 |
In [37]:
1 | pd.value_counts(y) # 合成前 |
Out[37]:
1 | 0 8500 |
In [38]:
1 | # 2、SMOTE采样 |
In [39]:
1 | # 3、采样后的全部特征 + 标签(Churn) |
Out[39]:
查看合成之后的数据,发现是相同的比例:
In [40]:
1 | pd.value_counts(y) # 合成后 |
Out[40]:
1 | 0 8500 |
数据降维PCA
降维可视化
In [41]:
1 | sm_df.columns |
Out[41]:
1 | Index(['Customer_Age', 'Gender', 'Dependent_count', 'Months_on_book', |
In [42]:
1 | # 独热码生成的字段数据 |
In [43]:
1 | sm_df.shape |
Out[43]:
1 | (17000, 16) |
In [44]:
1 | one_hot_data.shape |
Out[44]:
1 | (17000, 17) |
In [45]:
1 | n = 8 |
为了保留至少80%的主成分信息,通过多次尝试发现:n=8的时候几乎刚好达到要求
降维结果
降维之后的有效字段生成的DataFrame
In [46]:
1 | pca_df = pd.DataFrame(pca_matrix, |
Out[46]:
生成的8个主成分DataFrame:
将sm_df和pca_df进行合并作为后面建模的数据:
In [47]:
1 | df_model = pd.concat([sm_df, pca_df],axis=1) |
解释效果
In [48]:
1 | fig = px.scatter_matrix( |
可以看到我们保留了79.61%的信息。
再次查看相关系数矩阵
In [49]:
1 | # 基于plotly实现 |
建模
取出特征矩阵
In [50]:
1 | df_model_corr = df_model.corr() |
In [51]:
1 | fig = plt.figure(figsize=(16,12)) |
从上面的热力图中我们可以每个特征和Churn(目标变量)的相关性大小;在这里我们将系数的绝对值小于0.2的特征删除掉:
In [53]:
1 | df_model_corr.sort_values("Churn")["Churn"] |
Out[53]:
1 | Total_Trans_Ct -0.535498 |
新特征矩阵
In [54]:
1 | no_use_col = ['Total_Amt_Chng_Q4_Q1', 'P1', 'Dependent_count', |
In [55]:
1 | df_new = df_model.drop(no_use_col, axis=1) |
切割数据
In [56]:
1 | # 新训练集的特征矩阵和目标变量 |
交叉验证
In [57]:
1 | rf = Pipeline(steps =[('scale',StandardScaler()), # 树模型归一化可省略 |
结果对比
In [58]:
1 | length = len(rf_f1_scores) |
In [59]:
1 | fig = make_subplots(rows=3, |
模型预测
使用3种模型进行预测:
In [60]:
1 | rf.fit(X_train,y_train) |
In [61]:
1 | rf_f1 = f1(rf_prediction, y_test) |
3种模型对比
3种模型的F1取值的结果对比:
In [62]:
1 | df_f1_test = pd.DataFrame({"Model":["RF","AdaBoost","SVM"], |
很明显:随机森林仍然是最好的。我们和原方案的结果进行对比:还是提升了4-5个点。