锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

Python 银行信用卡客户流失预测

时间:2023-04-03 22:07:00 evr高压直流继电器

1.背景

银行经理对越来越多的客户不再使用信用卡服务感到不安。如果有人能预测哪些客户即将流失,他们会非常感激,因为他们可以主动为客户提供更好的服务,拯救这些即将流失的客户。

2.数据集

该数据集由年龄、工资、婚姻状况、信用卡限额、信用卡类别等1万名客户组成。

然而,只有16%的客户流失,因此很难预测客户是否会流失。

在Python实用宝典后台回复预测客户流失下载此数据和源代码。

3.代码与分析

在开始之前,你必须确保Python和pip已成功安装在计算机上。

(可选1)如果你用Python目的是直接安装数据分析Anaconda,它内置了Python和pip.

(可选2)另外,推荐大家使用VSCode编辑器有很多优点。

本文具有流程性,建议使用 VSCode 的 Jupiter Notebook 扩展,新建一个名字test.ipynb跟着教程一步一步走。

请选择以下任何方式输入命令安装
1. Windows 环境 打开 Cmd (开始-运行-CMD)。
2. MacOS 环境 打开 Terminal (command 空格输入Terminal)。
3. 假如你用的是 VSCode编辑器 或 Pycharm,界面下方可直接使用Terminal.

pipinstallnumpy pipinstallpandas pipinstallplotly pipinstallscikit-learn pipinstallscikit-plot  # 最后需要使用模型预测,安装需要conda # 如果只是想探索性分析数据,可以不导入 imblearn condainstall-c conda-forge imbalanced-learn

3.1 导入所需的模块

这篇文章相对较长,涉及到更多的模块。如果你只是想探索分析数据,你可以不导入 imblearn。

importnumpyasnp# linear algebra importpandasaspd# data processing, CSV file I/O (e.g. pd.read_csv) importmatplotlib.pyplotasplt importseabornassns importplotly.expressasex importplotly.graph_objsasgo importplotly.figure_factoryasff fromplotly.subplotsimportmake_subplots importplotly.offlineaspyo pyo.init_notebook_mode() sns.set_style('darkgrid') fromsklearn.decompositionimportPCA fromsklearn.model_selectionimporttrain_test_split,cross_val_score fromsklearn.ensembleimportRandomForestClassifier,AdaBoostClassifier fromsklearn.svmimportSVC fromsklearn.pipelineimportPipeline fromsklearn.preprocessingimportStandardScaler fromsklearn.metricsimportf1_scoreasf1 fromsklearn.metricsimportconfusion_matrix importscikitplotasskplt  plt.rc('figure',figsize=(18,9)) %pip install imbalanced-learn fromimblearn.over_samplingimportSMOTE

遇到任何 No module named "XXX" 都可以尝试pip install一下。

如果pip install谷歌/百度可以看看别人是怎么解决的。

3.2 加载数据

c_data = pd.read_csv('./BankChurners.csv') c_data = c_data[c_data.columns[:-2]] c_data.head(3)

这里去掉了最后两列简单贝叶斯分类结果。

显示前三行数据, 所有字段都可以看到:

3.3 探索性数据分析

下面看看这20 哪些数据对我们有用?

首先,我想知道数据集中的客户年龄分布:

fig = make_subplots(rows=2, cols=1)  tr1=go.Box(x=c_data['Customer_Age'],name='Age Box Plot',boxmean=True) tr2=go.Histogram(x=c_data['Customer_Age'],name='Age Histogram')  fig.add_trace(tr1,row=1,col=1) fig.add_trace(tr2,row=2,col=1)  fig.update_layout(height=700, width=1200, title_text="Distribution of Customer Ages") fig.show()

可以看出,客户的年龄分布大致遵循正态分布,因此年龄特征可以在正态假设下进一步使用。

同样,我想知道性别分布如何:

ex.pie(c_data,names='Gender',title='Propotion Of Customer Genders')

可以看出,在我们的数据集中,女性的样本比男性多,但差异的百分比并不明显,所以我们可以说性别分布均匀。

每个客户的家庭数量分布如何?

fig = make_subplots(rows=2, cols=1)  tr1=go.Box(x=c_data['Dependent_count'],name='Dependent count Box Plot',boxmean=True) tr2=go.Histogram(x=c_data['Dependent_count'],name='Dependent count Histogram')  fig.add_trace(tr1,row=1,col=1) fig.add_trace(tr2,row=2,col=1)  fig.update_layout(height=700, width=1200, title_text="Distribution of Dependent counts (close family size)") fig.show()

它也大致符合正态分布,稍微右一点,也许后续分析可以使用。

客户的教育水平如何?

ex.pie(c_data,names='Education_Level',title='Propotion Of Education Levels')

假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假大多数教育程度大多数教育程度大多数教育程度大多数教育程度大多数教育程度大多数教育程度大多数教育程度不明程度假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假假(Unknown)客户没有接受过任何教育。我们可以指出,70%以上的客户受过正规教育,其中约35%受过硕士以上教育,45%受过本科以上教育。

他们的婚姻状况如何?

ex.pie(c_data,names='Marital_Status',title='Propotion Of Different Marriage Statuses')

这家银行一半的客户似乎都是已婚人士。有趣的是,另一半的客户几乎都是单身人士,只有7%的客户离婚了。

查看收入分布和卡片类型分布:

ex.pie(c_datanames='Income_Category',title='Propotion Of Different Income Levels')

ex.pie(c_data,names='Card_Category',title='Propotion Of Different Card Categories')

可见大部分人的年收入处于60K美元以下。

在持有的卡片的类型上,蓝卡占了绝大多数。

每月账单数量有没有特征?

fig = make_subplots(rows=2, cols=1)

tr1=go.Box(x=c_data['Months_on_book'],name='Months on book Box Plot',boxmean=True)
tr2=go.Histogram(x=c_data['Months_on_book'],name='Months on book Histogram')

fig.add_trace(tr1,row=1,col=1)
fig.add_trace(tr2,row=2,col=1)

fig.update_layout(height=700, width=1200, title_text="Distribution of months the customer is part of the bank")
fig.show()

可以看到中间的峰值特别高,显然这个指标不是正态分布的。

每位客户持有的银行业务数量有没有特征呢?

fig = make_subplots(rows=2, cols=1)

tr1=go.Box(x=c_data['Total_Relationship_Count'],name='Total no. of products Box Plot',boxmean=True)
tr2=go.Histogram(x=c_data['Total_Relationship_Count'],name='Total no. of products Histogram')

fig.add_trace(tr1,row=1,col=1)
fig.add_trace(tr2,row=2,col=1)

fig.update_layout(height=700, width=1200, title_text="Distribution of Total no. of products held by the customer")
fig.show()

基本上都是均匀分布的,显然这个指标对于我们而言也没太大意义。

用户不活跃月份数量是不是好用的特征?

fig = make_subplots(rows=2, cols=1)

tr1=go.Box(x=c_data['Months_Inactive_12_mon'],name='number of months inactive Box Plot',boxmean=True)
tr2=go.Histogram(x=c_data['Months_Inactive_12_mon'],name='number of months inactive Histogram')

fig.add_trace(tr1,row=1,col=1)
fig.add_trace(tr2,row=2,col=1)

fig.update_layout(height=700, width=1200, title_text="Distribution of the number of months inactive in the last 12 months")
fig.show()

这个似乎有点用处,会不会越不活跃的用户越容易流失呢?我们先往后看。

信用卡额度的分布如何?

fig = make_subplots(rows=2, cols=1)

tr1=go.Box(x=c_data['Credit_Limit'],name='Credit_Limit Box Plot',boxmean=True)
tr2=go.Histogram(x=c_data['Credit_Limit'],name='Credit_Limit Histogram')

fig.add_trace(tr1,row=1,col=1)
fig.add_trace(tr2,row=2,col=1)

fig.update_layout(height=700, width=1200, title_text="Distribution of the Credit Limit")
fig.show()

大部分人的额度都在0到10k之间,这比较正常,暂时看不出和流失有什么关系。

客户总交易额的分布怎么样?

fig = make_subplots(rows=2, cols=1)

tr1=go.Box(x=c_data['Total_Trans_Amt'],name='Total_Trans_Amt Box Plot',boxmean=True)
tr2=go.Histogram(x=c_data['Total_Trans_Amt'],name='Total_Trans_Amt Histogram')

fig.add_trace(tr1,row=1,col=1)
fig.add_trace(tr2,row=2,col=1)

fig.update_layout(height=700, width=1200, title_text="Distribution of the Total Transaction Amount (Last 12 months)")
fig.show()

这个有点意思,总交易额的分布体现出“多组”分布,如果我们根据这个指标将客户聚类为不同的组别,看他们之间的相似性,并作出不同的画线,也许对我们最终的流失分析有一定的意义。

接下来,最重要的流失用户分布

ex.pie(c_data,names='Attrition_Flag',title='Proportion of churn vs not churn customers')

我们可以看到,只有16%的数据样本代表流失客户,在接下来的步骤中,我将使用SMOTE对流失样本进行采样,使其与常规客户的样本大小匹配,以便给后面选择的模型一个更好的机会来捕捉小细节。

3.4 数据预处理

使用SMOTE模型前,需要根据不同的特征对数据进行One Hot编码:

c_data.Attrition_Flag = c_data.Attrition_Flag.replace({'Attrited Customer':1,'Existing Customer':0})
c_data.Gender = c_data.Gender.replace({'F':1,'M':0})
c_data = pd.concat([c_data,pd.get_dummies(c_data['Education_Level']).drop(columns=['Unknown'])],axis=1)
c_data = pd.concat([c_data,pd.get_dummies(c_data['Income_Category']).drop(columns=['Unknown'])],axis=1)
c_data = pd.concat([c_data,pd.get_dummies(c_data['Marital_Status']).drop(columns=['Unknown'])],axis=1)
c_data = pd.concat([c_data,pd.get_dummies(c_data['Card_Category']).drop(columns=['Platinum'])],axis=1)
c_data.drop(columns = ['Education_Level','Income_Category','Marital_Status','Card_Category','CLIENTNUM'],inplace=True)

显示热力图:

sns.heatmap(c_data.corr('pearson'),annot=True)

3.5 SMOTE模型采样

SMOTE模型经常用于解决数据不平衡的问题,它通过添加生成的少数类样本改变不平衡数据集的数据分布,是改善不平衡数据分类模型性能的流行方法之一。

oversample = SMOTE()
X, y = oversample.fit_resample(c_data[c_data.columns[1:]], c_data[c_data.columns[0]])
usampled_df = X.assign(Churn = y)
ohe_data =usampled_df[usampled_df.columns[15:-1]].copy()
usampled_df = usampled_df.drop(columns=usampled_df.columns[15:-1])
sns.heatmap(usampled_df.corr('pearson'),annot=True)

3.6 主成分分析

我们将使用主成分分析来降低单次编码分类变量的维数,从而降低方差。同时使用几个主成分而不是几十个单次编码特征将帮助我构建一个更好的模型。

N_COMPONENTS = 4

pca_model = PCA(n_components = N_COMPONENTS )

pc_matrix = pca_model.fit_transform(ohe_data)

evr = pca_model.explained_variance_ratio_
cumsum_evr = np.cumsum(evr)

ax = sns.lineplot(x=np.arange(0,len(cumsum_evr)),y=cumsum_evr,label='Explained Variance Ratio')
ax.set_title('Explained Variance Ratio Using {} Components'.format(N_COMPONENTS))
ax = sns.lineplot(x=np.arange(0,len(cumsum_evr)),y=evr,label='Explained Variance Of Component X')
ax.set_xticks([i for i in range(0,len(cumsum_evr))])
ax.set_xlabel('Component number #')
ax.set_ylabel('Explained Variance')
plt.show()

usampled_df_with_pcs = pd.concat([usampled_df,pd.DataFrame(pc_matrix,columns=['PC-{}'.format(i) for i in range(0,N_COMPONENTS)])],axis=1)
usampled_df_with_pcs

特征变得越来越明显:

sns.heatmap(usampled_df_with_pcs.corr('pearson'),annot=True)

4.模型选择及测试

选择出以下特征划分训练集并进行训练:

X_features = ['Total_Trans_Ct','PC-3','PC-1','PC-0','PC-2','Total_Ct_Chng_Q4_Q1','Total_Relationship_Count']

X = usampled_df_with_pcs[X_features]
y = usampled_df_with_pcs['Churn']

train_x,test_x,train_y,test_y = train_test_split(X,y,random_state=42)

4.1 交叉验证

分别看看随机森林、AdaBoost和SVM模型三种模型的表现如何:

rf_pipe = Pipeline(steps =[ ('scale',StandardScaler()), ("RF",RandomForestClassifier(random_state=42)) ])
ada_pipe = Pipeline(steps =[ ('scale',StandardScaler()), ("RF",AdaBoostClassifier(random_state=42,learning_rate=0.7)) ])
svm_pipe = Pipeline(steps =[ ('scale',StandardScaler()), ("RF",SVC(random_state=42,kernel='rbf')) ])


f1_cross_val_scores = cross_val_score(rf_pipe,train_x,train_y,cv=5,scoring='f1')
ada_f1_cross_val_scores=cross_val_score(ada_pipe,train_x,train_y,cv=5,scoring='f1')
svm_f1_cross_val_scores=cross_val_score(svm_pipe,train_x,train_y,cv=5,scoring='f1')
plt.subplot(3,1,1)
ax = sns.lineplot(x=range(0,len(f1_cross_val_scores)),y=f1_cross_val_scores)
ax.set_title('Random Forest Cross Val Scores')
ax.set_xticks([i for i in range(0,len(f1_cross_val_scores))])
ax.set_xlabel('Fold Number')
ax.set_ylabel('F1 Score')
plt.show()
plt.subplot(3,1,2)
ax = sns.lineplot(x=range(0,len(ada_f1_cross_val_scores)),y=ada_f1_cross_val_scores)
ax.set_title('Adaboost Cross Val Scores')
ax.set_xticks([i for i in range(0,len(ada_f1_cross_val_scores))])
ax.set_xlabel('Fold Number')
ax.set_ylabel('F1 Score')
plt.show()
plt.subplot(3,1,3)
ax = sns.lineplot(x=range(0,len(svm_f1_cross_val_scores)),y=svm_f1_cross_val_scores)
ax.set_title('SVM Cross Val Scores')
ax.set_xticks([i for i in range(0,len(svm_f1_cross_val_scores))])
ax.set_xlabel('Fold Number')
ax.set_ylabel('F1 Score')
plt.show()

看看三种模型都有什么不同的表现:

看得出来随机森林 F1分数是最高的,达到了0.92。

4.2 模型预测

对测试集进行预测,看看三种模型的效果:

rf_pipe.fit(train_x,train_y)
rf_prediction = rf_pipe.predict(test_x)

ada_pipe.fit(train_x,train_y)
ada_prediction = ada_pipe.predict(test_x)

svm_pipe.fit(train_x,train_y)
svm_prediction = svm_pipe.predict(test_x)

print('F1 Score of Random Forest Model On Test Set - {}'.format(f1(rf_prediction,test_y)))
print('F1 Score of AdaBoost Model On Test Set - {}'.format(f1(ada_prediction,test_y)))
print('F1 Score of SVM Model On Test Set - {}'.format(f1(svm_prediction,test_y)))

4.3 对原始数据(采样前)进行模型预测

接下来对原始数据进行模型预测:

ohe_data =c_data[c_data.columns[16:]].copy()
pc_matrix = pca_model.fit_transform(ohe_data)
original_df_with_pcs = pd.concat([c_data,pd.DataFrame(pc_matrix,columns=['PC-{}'.format(i) for i in range(0,N_COMPONENTS)])],axis=1)

unsampled_data_prediction_RF = rf_pipe.predict(original_df_with_pcs[X_features])
unsampled_data_prediction_ADA = ada_pipe.predict(original_df_with_pcs[X_features])
unsampled_data_prediction_SVM = svm_pipe.predict(original_df_with_pcs[X_features])

效果如下:


F1最高的随机森林模型有0.63分,偏低,这也比较正常,毕竟在这种分布不均的数据集中,查全率是比较难拿到高分数的。

4.4 结果

让我们看看最终在原数据上使用随机森林模型的运行结果:

ax = sns.heatmap(confusion_matrix(unsampled_data_prediction_RF,original_df_with_pcs['Attrition_Flag']),annot=True,cmap='coolwarm',fmt='d')
ax.set_title('Prediction On Original Data With Random Forest Model Confusion Matrix')
ax.set_xticklabels(['Not Churn','Churn'],fontsize=18)
ax.set_yticklabels(['Predicted Not Churn','Predicted Churn'],fontsize=18)

plt.show()

可见,没有流失的客户命中了7709人,未命中791人。

流失客户命中了1130人,未命中497人。

整体而言,是一个比较优秀的模型了。

如果你觉得文章还不错,欢迎关注公众号:Python编程学习圈,或是前往编程学习网,了解更多编程技术知识,还有大量干货学习资料可以领取!

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章