当前位置:   article > 正文

【数据分析】信用卡用户画像及违约预测逻辑回归模型_逻辑回归 银行信贷

逻辑回归 银行信贷

1、数据源说明

kaggle上比较经典的数据集,来源某银行个人信贷业务,包含客户数据、信用卡数据、交易订单等基本数据,通过这些数据可以了解银行信贷业务及风险防控相关内容。

2、数据库导入及宽表建立

为便于理解及跨软件处理,已将六张表数据表导入mysql,并将账户、卡、客户信息聚合形成宽表(办卡时长和年龄字段为虚构,交易表最后交易日期为1998.12.31,故以1999.1.1为基准日期分别减去发卡日期和生日得到两个字段),E-R图如下:
在这里插入图片描述

--宽表形成逻辑
CREATE table card_client_totle as
SELECT c.card_id,c.issued,TIMESTAMPDIFF(year,c.issued,'1999-01-01') as 'used_duration',c.type,cl.sex,cl.birth_date,TIMESTAMPDIFF(year,cl.birth_date,'1999-01-01') as 'age', a.account_id,dt.A1 as district_id
FROM  card c LEFT JOIN disp d on c.disp_id = d.disp_id
left join clients cl on d.client_id = cl.client_id
LEFT JOIN accounts a on d.account_id = a.account_id
LEFT join district dt on cl.district_id = dt.A1
where d.type = '所有者'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3、tableau可视化

(1)发卡情况总体描述

在这里插入图片描述
由以上仪表盘可知:
1)金卡与普通卡用户的年龄分布比青年卡更均匀,且年龄更大,年龄分布基本符合不同类型卡的目标用户群体;
2)各类卡发卡量均呈逐年增加的趋势,增加率金卡>普通卡>青年卡,增加量普通卡>金卡>青年卡;
3)各类卡持有量普通卡>金卡>青年卡,普通卡持有量总占比达到总量的四分之三;
4)普通卡、青年卡的持卡量性别差异不明显,金卡用户男性>女性。

4、python违约预测模型建立

(1)数据提取

前期建立宽表过程中,发现card表和accounts表数据量并不匹配,即开通账户的可能并没有开通任何一种类型的卡,所以可视化中信用卡分析用到的card表不能再作为建模的基表。且其他表的外键基本上都是account_id,因此重写了宽表,以accounts表为左表,命名为card_client_totle_1。数据提取代码:

import pandas as pd
import sqlalchemy
import cryptography
import numpy as np

engine = sqlalchemy.create_engine("mysql+pymysql://root:数据库密码@localhost:3306/库名",echo=True)
model_sql = '''SELECT
	t.age,
	t.sex,
	d.GDP,
	d.A11 as 'average_salary',
	d.A10 as 'city_rate',
	d.A15 'crime_rate_1995',
	d.a16 'crime_rate_1996',
	d.A12 'unemployment_rate_1995',
	d.A13 'unemployment_rate_1996',
	d.A14 'entrepreneur_counts',
	d.A4 'resident_population',
	od.amount as od_amount,
	l.amount as loan_amount,
	l.duration,
	l.date,
	l.`status`,
	l.account_id	
FROM
	loans l LEFT JOIN `card_client_totle_1` t ON l.account_id = t.account_id
	left JOIN district d ON t.district_id = d.A1
	LEFT JOIN (select ord.account_id,sum(ord.amount) as amount from `order` ord GROUP BY ord.account_id) od on t.account_id = od.account_id;'''

df_main_info = pd.read_sql(model_sql,engine)
df_main_info.head()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

获得结果:
在这里插入图片描述
将宽表与trans表连接,并增加一个索引列。

df_trans = pd.read_sql("select t.account_id,t.date,t.type,t.amount,t.balance from trans t",engine)
df_trans.head()

df_all = df_main_info.merge(df_trans,how='left',on='account_id')
df_all =  df_all.reset_index()
df_all.columns.values[0] = 'New_id'
df_all.info()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

查看表信息:
在这里插入图片描述

(2)数据清洗

由上一步可知1995年的失业率和犯罪率有空值,使用中位数进行填充:

#data cleaning
#fill rate_1995 null with median
df_main_info['unemployment_rate_1995'].fillna(df_all['unemployment_rate_1995'].median(),inplace=True)
df_main_info['crime_rate_1995'].fillna(df_all['crime_rate_1995'].median(),inplace=True)

df_all['unemployment_rate_1995'].fillna(df_all['unemployment_rate_1995'].median(),inplace=True)
df_all['crime_rate_1995'].fillna(df_all['crime_rate_1995'].median(),inplace=True)
#df_all.crime_rate_1995 =df_all.crime_rate_1995.fillna(df_all[7].median(),inplace=True)
#df_all = df_all['unemployment_rate_1995'].fillna(df_all['unemployment_rate_1995'].median())
df_main_info['unemployment_rate_1995'].isnull().any()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

输出为false,即已经没有空值。接下来进行格式转换及去除多余符号。

#日期格式转换,为取近一年数据做准备
df_all['date_x'] = pd.to_datetime(df_all['date_x'])
df_all['date_y'] = pd.to_datetime(df_all['date_y'])
#账户余额、交易额度去掉dollar符,转为数值型
df_all['balance'] = df_all['balance'].map(lambda x:int(''.join(x[1:].split(','))))
df_all['amount'] = df_all['amount'].map(lambda x:int(''.join(x[1:].split(','))))
#df_all.info()
#df_all.isnull().any()
#np.all(np.isfinite(df_all))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

(3)数据预处理:建立衍生指标,筛选模型特征

#取贷款前一年交易数据
import datetime
df_year = df_all[df_all['date_y']<df_all['date_x']][df_all['date_x']<df_all['date_y']+datetime.timedelta(days=365)]
#df_year.iloc[:20,15:]
df_year.info()
  • 1
  • 2
  • 3
  • 4
  • 5
#df_year.sort_values(by=['account_id','date_y'])
#计算每个账户前一年平均账户余额(衡量财富水平)、余额标准差和变异系数(衡量财富稳定情况)
df_year_acct = df_year.groupby('account_id')['balance'].agg(['mean','std'])
df_year_acct.columns = ['avg_balance','stdev_balance']
# # df_year_acct = df_year.groupby('account_id')['balance'].agg([('avg_balance','mean'),('stdev_balance','std')])
df_year_acct['cv_balance'] = df_year_acct[['avg_balance','stdev_balance']].apply(lambda x:x[1]/x[0],axis=1)
df_year_acct.head()
##np.isfinite().count() #682个
# df_year_acct.info()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

计算结果:
在这里插入图片描述
计算收入支出比

#计算平均入账和平均出账比例,对每个账户借贷交易金额汇总
tran_type_dict = {'借':'out','贷':'income'}
df_year['tran_type'] = df_year.type.map(tran_type_dict)
df_year_acct_am = df_year.groupby(['account_id','tran_type'])[['amount']].sum()
#转置表,将入账出账作为字段,方便计算比率
df_year_acct_am_rev = pd.pivot_table(df_year_acct_am,values='amount',index='account_id',columns='tran_type')

df_year_acct_am_rev.fillna(0,inplace=True)#避免分母为0
df_year_acct_am_rev['io_rate'] = df_year_acct_am_rev[['out','income']].apply(lambda x:x[0]/x[1],axis=1)

df_year_acct_am_rev.head()
#np.isnan(df_year_acct_am_rev).any()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在这里插入图片描述

#合并最初以账户id为主表的表,而非带交易明细的表!
df_total = pd.merge(df_main_info,df_year_acct,left_on='account_id',right_index=True,how='left')
df_total = pd.merge(df_total,df_year_acct_am_rev,left_on='account_id',right_index=True,how='left')
df_total.head()
# df_total.groupby(['account_id','type'])['ammount'].sum()

#计算存贷比、贷收比
df_total['loan_amount'] =  df_total['loan_amount'].astype(float)
df_total['ab_rate'] = df_total[['loan_amount','avg_balance']].apply(lambda x:x[0]/x[1],axis=1)
df_total['ai_rate'] = df_total[['loan_amount','income']].apply(lambda x:x[0]/x[1],axis=1)
df_total.info()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
#df_all['balance'] = df_all['balance'].map(lambda x:int(''.join(x[1:].split(','))))
#因出现数据类型报错,将报错字段类型改为float,便于计算增长率
df_total['crime_rate_1995'] =  df_total['crime_rate_1995'].astype(float)
df_total['crime_rate_1996'] = df_total['crime_rate_1996'].astype(float)
df_total['unemployment_rate_1995'] = df_total['unemployment_rate_1995'].astype(float)
df_total['unemployment_rate_1996'] = df_total['unemployment_rate_1996'].astype(float)
df_total['crime_chan'] = df_total[['crime_rate_1995','crime_rate_1996']].apply(lambda x:x[1]-x[0],axis = 1)
df_total['unemployment_chan'] = df_total[['unemployment_rate_1995','unemployment_rate_1996']].apply(lambda x:x[1]-x[0],axis = 1)
#将合同状态编码为数字,A代表合同终止无问题,B代表合同终止贷款未支付,
#C代表合同执行中至今正常,D代表处于违约执行期欠债状态。因此BD为违约账户
state_dic = {'A':0,'B':1,'C':2,'D':1}
df_total['status'] = df_total.status.map(state_dic)
df_total.iloc[:20,15:]
#df_total.info()
df_total.head()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述
绘制相关矩阵,查看各特征值相关性

#相关矩阵
import matplotlib.pyplot as plt
import seaborn as sns
corr = df_inmodel_all[['age','sex','GDP','average_salary','city_rate','entrepreneur_counts','resident_population','crime_chan','unemployment_chan','duration','loan_amount','avg_balance','stdev_balance','cv_balance','io_rate','ab_rate','ai_rate']].corr()
plt.figure(figsize=(12,9))
sns.heatmap(corr,vmax=1,annot=True)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述
根据相关矩阵,说明地区指标相关性较强,取相关性较低的两个指标即可,这里取人均GDP和失业增长率。由于部分特征是相互计算得出,因此要避免重复入模。

#取人均GDP、失业增长率、贷款期限、余额变异系数、消费占比、存贷比
import numpy as np
df_inmodel_all['GDP_avg'] = df_inmodel_all.loc[:,'GDP']/df_inmodel_all.loc[:,'resident_population']
df_inmodel = df_inmodel_all[['sex','age','GDP_avg','unemployment_chan','duration','cv_balance','io_rate','ab_rate','status']]
data_model = df_inmodel[df_inmodel.status !=2] #排除合同还在进行中的账户
#data_model.describe()#查看是否筛选成功
for_predict = df_inmodel[df_inmodel.status==2]
#np.all(np.isfinite(df_inmodel_all))#用来确定参数是否无穷大
#np.isfinite(df_inmodel_all).all()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

(4)模型建立:

X = data_model.drop(columns='status')
y = data_model['status']
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size = 0.2,random_state=123)#划分训练集和测试集
X_train.head()
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(penalty='l2',solver='liblinear')#l1范式存在系数为0的情况,本项目中预测准确率高于l2范式,但一般使用l2
model.fit(X_train,y_train)
y_pred = model.predict(X_test)
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred,y_test)
print(score)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

得出测试集的预测准确度:0.875

#查看各系数
print(model.coef_)
print(model.intercept_)
  • 1
  • 2
  • 3

在这里插入图片描述

绘制ROC曲线图:

import sklearn.metrics as metrics
import matplotlib.pyplot as plt
y_pred_proba = model.predict_proba(X_test)
print(y_pred_proba[:10])
fpr, tpr, th = metrics.roc_curve(y_test,y_pred_proba[:,1])
plt.plot(fpr,tpr)
plt.title('ROC')
plt.xlabel('FPR')
plt.ylabel('tpr')
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述
根据ROC曲线及预测准确度,说明模型效果较好,但在实际业务中,仍需对模型进行调整及迭代。

(5)对合同正在履行的用户(贷款状态为C)进行预测

for_predict_x = for_predict.drop(columns='status')
df_tt_c = df_total[df_total.status==2][['account_id']]#直接写['account_id']一个括号会导致后面报错,即dataframe取列名都要双括号
df_tt_c['pre_result'] = model.predict(for_predict_x)
df_tt_c.head()
  • 1
  • 2
  • 3
  • 4

结果输出:
在这里插入图片描述

5、总结

(1)做完俩月了才来写总结,很多当时遇到调了好久的东西都忘了不少。存在问题的一部分是处理了指标之后没有关联最初的600来条的账户信息而是关联了带交易数据的大宽表,最后好多重复数据,distinct半天才发现不对劲。所以,还是要明白自己研究的是啥啊。
(2)很多基础性的错误总是不可避免。比如,报无穷大的错误,可能是某个得出某个数的分母分子反了以及分母有0;‘A value is trying to be set on a copy of a slice from a DataFrame’这个错误,可能是引用字段名时少加了一个中括号。算是提高了问题排查能力吧。
(3)因为是练习项目,更多是练习了工具的使用,希望以后能有实际场景的分析需求,能够增加一些思维的锻炼和相关的行业知识。

6、参考资料

(1)和鲸社区https://www.heywhale.com/mw/project/5ed9c13fb772f5002d6dc07c/content
(2)CSDN
https://blog.csdn.net/agoldminer/article/details/111460627
(3)电子书《Python大数据分析与机器学习商业案例实战》

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/746541
推荐阅读
相关标签
  

闽ICP备14008679号