参考资料:
零基础入门金融风控-贷款违约预测

导包

import pandas as pd
import matplotlib.pyplot as plt
# 读取数据
train = pd.read_csv('train.csv')
testA = pd.read_csv('testA.csv')
print('Train data shape:', train.shape)
print('testA data shape:', testA.shape)
train.head()
testA.head()

分类指标评价计算示例

# 混淆矩阵
import numpy as np
from sklearn.metrics import confusion_matrix
"""
          预:反              预:正
真:反    true negatives      false positives
真:正    false negatives     true positives
"""
y_pred = [0, 1, 0, 1]
y_true = [0, 1, 1, 0]
print('混淆矩阵:\n', confusion_matrix(y_true, y_pred))
# accuracy
from sklearn.metrics import accuracy_score
y_pred = [0, 1, 0, 1]
y_true = [0, 1, 1, 0]
print('ACC:', accuracy_score(y_true, y_pred))
# Precision, Recall, F1-score
from sklearn import metrics
y_pred = [0, 1, 0, 1]
y_ture = [0, 1, 1, 0]
print('Precision', metrics.precision_score(y_ture, y_pred))
print('Recall', metrics.recall_score(y_true, y_pred))
print('F1-score', metrics.f1_score(y_true, y_pred))
# P-R曲线
import matplotlib.pyplot as plt
from sklearn.metrics import precision_recall_curve
y_pred = [0, 1, 1, 0, 1, 1, 0, 1, 1, 1]
y_true = [0, 1, 1, 0, 1, 0, 1, 1, 0, 1]
precision , recall, thresholds = precision_recall_curve(y_true, y_pred)
plt.plot(recall, precision)
# ROC曲线
from sklearn.metrics import roc_curve
y_pred = [0, 1, 1, 0, 1, 1, 0, 1, 1, 1]
y_true = [0, 1, 1, 0, 1, 0, 1, 1, 0, 1]
FPR, TPR, thresholds = roc_curve(y_true, y_pred)
plt.title('ROC')
plt.plot(FPR, TPR, 'b') # 蓝实线
plt.plot([0,1], [0,1], 'r--') # 红虚线
plt.ylabel('TPR')
plt.xlabel('FPR')
# AUC
from sklearn.metrics import roc_auc_score
y_true = np.array([0, 0, 1, 1])
y_scores = np.array([0.1, 0.4, 0.35, 0.8])
print('AUC score', roc_auc_score(y_true, y_scores))
# 最大KS值 在实际操作时往往使用ROC曲线配合求出KS值
from sklearn.metrics import roc_curve
y_pred = [0, 1, 1, 0, 1, 1, 0, 1, 1, 1]
y_true = [0, 1, 1, 0, 1, 0, 1, 1, 1, 1]
FPR, TPR, thresholds = roc_curve(y_true, y_pred)
KS = abs(FPR-TPR).max()
print('最大KS值:', KS)

数据分析

"""
1.EDA价值主要在于熟悉了解整个数据集的基本情况(缺失值,异常值),对数据集进行验证是否可以进行接
下来的机器学习或者深度学习建模.
2.了解变量间的相互关系、变量与预测值之间的存在关系。
3.为特征工程做准备
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
import warnings
warnings.filterwarnings('ignore')
data_train = pd.read_csv('./train.csv')
data_test_a = pd.read_csv('./testA.csv')
# 通过os.getcwd()查看当前工作目录
import os
os.getcwd()
# 通过nrows参数读取某几行
data_train_sample = pd.read_csv('./train.csv', nrows=5)
data_train_sample
# 通过设置chunksize参数来控制每次迭代数据的大小
chunker = pd.read_csv('./train.csv', chunksize=5)
for item in chunker:
    print(type(item)) # <class 'pandas.core.frame.DataFrame'>
    print(len(item))  # 5
    display(item)
    break

总体了解

data_test_a.shape
data_train.shape
data_train.columns
# 查看数据类型
data_train.info()
# 查看各个特征(数值类型)的基本统计量
data_train.describe()
# data_train.head(3).append(data_train.tail(3)) # 未来会被弃用
pd.concat([data_train.head(3), data_train.tail(3)]) # 建议使用

查看数据集中特征缺失值,唯一值等

# 查看缺失值
print(f'There are {data_train.isnull().any().sum()} columns in train dataset with missing values.')
# 进一步查看缺失特征中缺失率大于50%的特征
have_null_fea_dict = (data_train.isnull().sum()/len(data_train)).to_dict()
fea_null_moreThanHalf = {}
for key, value in have_null_fea_dict.items():
    if value > 0.5:
        fea_null_moreThanHalf[key] = value
fea_null_moreThanHalf # 没有
# 具体查看缺失特征及缺失率
# nan可视化
missing = data_train.isnull().sum()/len(data_train)
missing = missing[missing > 0]
missing.sort_values(inplace=True)
missing.plot.bar()
"""
了解哪些列存在 “nan”, 并可以把nan的个数打印,主要的目的在于 nan存在的个数是否真的很大,如果很小一般选
择填充,如果使用lgb等树模型可以直接空缺,让树自己去优化,但如果nan存在的过多、可以考虑删掉
"""
# 查看训练集测试集中特征属性只有一值的特征
one_value_fea = [col for col in data_train.columns if data_train[col].nunique() <= 1] # number of unique
oen_value_fea_test = [col for col in data_test_a.columns if data_test_a[col].nunique() <= 1]
print(one_value_fea)
print(oen_value_fea_test)

总结:47列数据中有22列都缺少数据,这在现实世界中很正常。‘policyCode’具有一个唯一值(或全部缺失)。有很多连
续变量和一些分类变量。

查看特征的数值类型有哪些,对象类型有哪些

  1. 特征一般都是由类别型特征和数值型特征组成
  2. 类别型特征有时具有非数值关系,有时也具有数值关系。比如‘grade’中的等级A,B,C等,是否只是单纯的
    分类,还是A优于其他要结合业务判断。
  3. 数值型特征本是可以直接入模的,但往往风控人员要对其做分箱,转化为WOE编码进而做标准评分卡等操作。从模型效果上来看,特征分箱主要是为了降低变量的复杂性,减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定。
# 数值型特征
numerical_fea = list(data_train.select_dtypes(exclude=['object']).columns)
print(numerical_fea)
# 类别型特征
category_fea = list(filter(lambda x: x not in numerical_fea, list(data_train.columns)))
category_fea
data_train.grade
# 数值型变量分析:
# 1. 划分数值型变量中的连续型变量和和离散型变量
# 过滤数值型类别特征:如果特征下面的不同取值小于10种,则认为是离散型数值变量,否则认为是连续型数值变量
def get_numerical_serial_fea(data,feas):
    numerical_serial_fea = []
    numerical_noserial_fea = []
    for fea in feas:
        temp = data[fea].nunique()
        if temp <= 10:
            numerical_noserial_fea.append(fea)
            continue
        numerical_serial_fea.append(fea)
    return numerical_serial_fea, numerical_noserial_fea

numerical_serial_fea, numerical_noserial_fea = get_numerical_serial_fea(data_train,numerical_fea)
print(numerical_serial_fea)
print(numerical_noserial_fea)
# 离散型数值变量分析
data_train['term'].value_counts()
data_train['homeOwnership'].value_counts()
data_train['isDefault'].value_counts()
data_train['initialListStatus'].value_counts()
data_train['applicationType'].value_counts()
data_train['policyCode'].value_counts() # 无用变量,全部一个值
data_train['n11'].value_counts() # 相差悬殊,不用再分析
data_train['n12'].value_counts() # 相差悬殊,不用再分析
# 连续型数值变量分析
# 每个数值特征的分布可视化
f = pd.melt(data_train, value_vars=numerical_serial_fea)
g = sns.FacetGrid(f, col='variable', col_wrap=2, sharex=False, sharey=False) # 结构化多绘图网格,初始化FacetGrid对象
g = g.map(sns.distplot, 'value') # 将sns.distplot应用到每个数据子集
# 参考资料:https://blog.csdn.net/weixin_43618989/article/details/105613021
# https://zhuanlan.zhihu.com/p/484363632
"""
1. 查看某一个数值型变量的分布,查看变量是否符合正态分布,如果不符合正态分布的变量可以log化后再观察下是否符合正态分布。
2. 如果想统一处理一批数据变标准化 必须把这些之前已经正态化的数据提出
"""
# 绘制交易数据(loanAmnt)分布
plt.figure(figsize=(16, 12)) # 画布大小,宽为16,高为12
plt.suptitle('Transaction Values Distribution', fontsize=22) # 标题
plt.subplot(221) # 整个画布分为两行两列,当前子图位于第一个位置
sub_plot_1 = sns.distplot(data_train['loanAmnt']) # 该子图上绘制直方图,默认包含核密度曲线
sub_plot_1.set_title('loanAmnt Distribution', fontsize=18)
sub_plot_1.set_xlabel('')
sub_plot_1.set_ylabel('Probability', fontsize=15)

plt.subplot(222)
sub_plot_2 = sns.distplot(np.log(data_train['loanAmnt']))
sub_plot_2.set_title("loanAmnt (Log) Distribuition", fontsize=18)
sub_plot_2.set_xlabel("")
sub_plot_2.set_ylabel("Probability", fontsize=15)
# 类别型特征分析
category_fea
data_train['grade'].value_counts() # 默认
data_train['subGrade'].value_counts()
data_train['employmentLength'].value_counts()
data_train['issueDate'].value_counts()
data_train['earliesCreditLine'].value_counts()

总结:

  1. 上面我们用value_counts()等函数看了特征属性的分布,但是图表是概括原始信息最便捷的方式。
  2. 数无形时少直觉。
  3. 同一份数据集,在不同的尺度刻画上显示出来的图形反映的规律是不一样的。python将数据转化成图表,但结论是否正确需要由你保证。

变量分布可视化

单一变量分布可视化

plt.figure(figsize=(8, 8))
sns.barplot(data_train['employmentLength'].value_counts(dropna=False)[:20], # .values可加可不加
            data_train['employmentLength'].value_counts(dropna=False).keys()[:20])
plt.show()

根据y值不同可视化x某个特征的分布

# 1.首先查看类别型变量在不同y值上的分布
train_loan_fr = data_train[data_train['isDefault'] == 1] # .loc可加可不加
train_loan_nofr = data_train.loc[data_train['isDefault'] == 0]
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 8))
# 优化:对索引排序
# 欺诈用户的不同等级的数量分布
train_loan_fr.groupby('grade').size().sort_index(ascending=False).plot(kind='barh', ax=ax1, title='Count of grade fraud') 
# 非欺诈用户的不同等级的数量分布
train_loan_nofr.groupby('grade').size().sort_index(ascending=False).plot(kind='barh', ax=ax2, title='Count of grade non-fraud')
# 欺诈用户的就业年限的数量分布
employmentLength_index = train_loan_fr.groupby('employmentLength').size().index
b = np.arange(11)
b[0], b[1], b[10] = b[10], b[0], b[1]
train_loan_fr.groupby('employmentLength').size().reindex(employmentLength_index[b][::-1]).plot(kind='barh', ax=ax3, title='Count of employmentLength fraud')
# 非欺诈用户的就业年限的数量分布
train_loan_nofr.groupby('employmentLength').size().reindex(employmentLength_index[b][::-1]).plot(kind='barh', ax=ax4, title='Count of employmentLength non-fraud')
plt.show()
# 2.查看连续型变量在不同y值上的分布
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
data_train[data_train['isDefault'] == 1]['loanAmnt'].apply(np.log).plot(kind='hist',
          bins = 100,
          title = 'Log Loan Amt - Fraud',
          color = 'r',
          xlim = (-3, 10),
          ax = axes[0])
data_train[data_train['isDefault'] == 0]['loanAmnt'].apply(np.log).plot(kind='hist',
          bins = 100,
          title = 'Log Loan Amt - Not Fraud',
          color = 'b',
          xlim = (-3, 10),
          ax = axes[1])
"""
图一:绘制欺诈用户和非欺诈用户对应的数量,并在图上标注百分比
"""
total = len(data_train)
print(total_amt)
plt.figure(figsize=(12, 5))
plt.subplot(121) # 下面的图画在第一个坐标轴上
plot_tr = sns.countplot(x='isDefault', data=data_train) #
plot_tr.set_title("Fraud Loan Distribution \n 0: good user | 1: bad user", fontsize=14)
plot_tr.set_xlabel("Is fraud by count", fontsize=16)
plot_tr.set_ylabel('Count', fontsize=16)
for p in plot_tr.patches:
    height = p.get_height()
    plot_tr.text(p.get_x()+p.get_width()/2.,
            height,
            '{:1.2f}%'.format(height/total*100),
            ha="center", fontsize=15) # ha是horizontal alignment(水平对齐)的含义


"""
图二:绘制欺诈用户和非欺诈用户对应的贷款金额总数,并在图上标注百分比
"""
total_amt = data_train['loanAmnt'].sum() # 总的loanAmnt
percent_amt = (data_train.groupby(['isDefault'])['loanAmnt'].sum())
percent_amt = percent_amt.reset_index()
plt.subplot(122) # 下面的图画在第二个坐标轴上
plot_tr_2 = sns.barplot(x='isDefault', y='loanAmnt', data=percent_amt)
plot_tr_2.set_title("Total Amount in loanAmnt \n 0: good user | 1: bad user", fontsize=14)
plot_tr_2.set_xlabel("Is fraud by percent", fontsize=16)
plot_tr_2.set_ylabel('Total Loan Amount Scalar', fontsize=16)
for p in plot_tr_2.patches:
    height = p.get_height()
    plot_tr_2.text(p.get_x()+p.get_width()/2.,
            height,
            '{:1.2f}%'.format(height/total_amt * 100),
            ha="center", fontsize=15)

时间格式数据处理及查看

# 训练集:利用datetime类型,计算贷款发放日期距离设定日期的天数,并添加新的列
# issueDate: 贷款发放日期
data_train['issueDate'] = pd.to_datetime(data_train['issueDate'], format='%Y-%m-%d') # 将数据类型转化为datetime类型
startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d') # 设置起始日期,将字符串变成datetime类型
data_train['issueDateDT'] = data_train['issueDate'].apply(lambda x: x-startdate).dt.days # 计算距离起始日期的天数,添加到新的一列中
# 测试集:利用datetime类型,计算贷款发放日期距离设定日期的天数,并添加新的列
data_test_a['issueDate'] = pd.to_datetime(data_train['issueDate'],format='%Y-%m-%d')
startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')
data_test_a['issueDateDT'] = data_test_a['issueDate'].apply(lambda x: x-startdate).dt.days
plt.hist(data_train['issueDateDT'], label='train') # 根据数据绘制直方图
plt.hist(data_test_a['issueDateDT'], label='train')
plt.legend() # 添加图例
plt.title('Distribution of issueDateDT dates');
# train 和 test issueDateDT 日期有重叠 所以使用基于时间的分割进行验证是不明智的

掌握透视图可以让我们更好了解数据

# 透视图 索引可以有多个,“columns(列)”是可选的,聚合函数aggfunc最后是被应用到了变量“values”中你所列举的项目上。
# 透视图 行索引是'grade'(贷款等级),列索引是"issueDateDT"(贷款发放距离初始日期),值是'loanAmnt'(贷款金额)经过np.sum聚合的结果
pivot = pd.pivot_table(data_train, index=['grade'], columns=['issueDateDT'], values=['loanAmnt'], aggfunc=np.sum)
pivot

用pandas_profiling生成数据报告

import pandas_profiling
pfr = pandas_profiling.ProfileReport(data_train)
pfr.to_file('./example.html')

总结

数据探索性分析是我们初步了解数据,熟悉数据为特征工程做准备的阶段,甚至很多时候EDA阶段提取出来的特征可以直接当作规则来用。可见EDA的重要性,这个阶段的主要工作还是借助于各个简单的统计量来对数据整体的了解,分析各个类型变量相互之间的关系,以及用合适的图形可视化出来直观观察。希望本节内容能给初学者带来帮助,更期待各位学习者对其中的不足提出建议。

Task3 特征工程

学习目标

  1. 学习特征预处理、缺失值、异常值处理、数据分桶等特征处理方法
  2. 学习特征交互、编码、选择的相应方法
  3. 完成相应学习打卡任务,两个选做的作业不做强制性要求,供学有余力同学自己探索

内容介绍

  1. 数据预处理:
    a. 缺失值的填充
    b. 时间格式处理
    c. 对象类型特征转换到数值
  2. 异常值处理:
    a. 基于3sigma原则
    b. 基于箱型图
  3. 数据分箱
    a. 固定宽度分箱
    b. 分位数分箱
    • 离散数值型数据分箱
    • 连续数值型数据分箱

c. 卡方分箱(选做作业)
4. 特征交互
a. 特征和特征之间组合
b. 特征和特征之间衍生
c. 其他特征衍生的尝试(选做作业)
5. 特征编码
a. one-hot编码
b. label-encode编码
6. 特征选择
a. 1 Filter
b. 2 Wrapper (RFE)
c. 3 Embedded

代码示例

导入包并读取数据

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
from tqdm import tqdm
from sklearn.preprocessing import LabelEncoder
# from sklearn.feature_selection import SelectKBest
# from sklearn.feature_selection import chi2
# from sklearn.preprocessing import MinMaxScaler
# import xgboost as xgb
# import lightgbm as lgb
# from catboost import CatBOostRegressor
# from sklearn.model_selection import StratifiedKFold, KFold
# from sklearn.metrics import accuracy_score, f1_score, roc_auc_score, log_loss
import warnings
warnings.filterwarnings('ignore')
data_train =pd.read_csv('./train.csv')
data_test_a = pd.read_csv('./testA.csv')

特征预处理

  1. 数据EDA部分我们已经对数据的大概和某些特征分布有了了解,数据预处理部分一般我们要处理一些EDA阶段分析出来的问题,这里介绍了数据缺失值的填充,时间格式特征的转化处理,某些对象类别特征的处理。

在比赛中数据预处理是必不可少的一部分,对于缺失值的填充往往会影响比赛的结果,在比赛中不妨尝试多种填充然后比较结果选择结果最优的一种;
比赛数据相比真实场景的数据相对要“干净”一些,但是还是会有一定的“脏”数据存在,清洗一些异常值往往会获得意想不到的效果。

# 找出数据中的object类型特征和非object类型特征
numerical_fea = list(data_train.select_dtypes(exclude=['object']).columns) # 非object类型特征
category_fea = list(filter(lambda x: x not in numerical_fea,list(data_train.columns))) # object类型特征
# 也可以category_fea = list(data_train.select_dtypes(include=['object']).columns)
label = 'isDefault'
numerical_fea.remove(label) # 剔除label标签

缺失值填充

  1. 把所有缺失值替换为指定的值0
    data_train = data_train.fillna(0)

  2. 纵向用缺失值上面的值替换缺失值
    data_train = data_train.fillna(axis=0,method=‘ffill’)

  3. 纵向用缺失值下面的值替换缺失值,且设置最多只填充两个连续的缺失值
    data_train = data_train.fillna(axis=0,method=‘bfill’,limit=2)

参考资料:https://www.jb51.net/article/255677.htm

# 查看缺失值情况
data_train.isnull().sum()
# 按照平均数填充数值型特征
data_train.fillna(data_train[numerical_fea].median(), inplace=True) # 优化
data_test_a.fillna(data_train[numerical_fea].median(), inplace=True)
# 按照众数填充类别型特征
data_train.fillna(data_train[category_fea].mode().iloc[0], inplace=True) # 勘误
data_test_a.fillna(data_train[category_fea].mode().iloc[0], inplace=True)
data_train.isnull().sum()

时间格式处理

# 利用datetime类型,计算贷款发放日期距离设定日期的天数,并添加新的列
for data in [data_train, data_test_a]:
    data['issueDate'] = pd.to_datetime(data['issueDate'],format='%Y-%m-%d')
    startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')
    #构造时间特征
    data['issueDateDT'] = data['issueDate'].apply(lambda x: x-startdate).dt.days

对象类型特征转换到数值

data_train['employmentLength'].value_counts(dropna=False).sort_index()
def employmentLength_to_int(s):
    if pd.isnull(s):
        return s
    else:
        return np.int(s.split()[0])

for data in [data_train, data_test_a]:
    data['employmentLength'].replace(to_replace='10+ years', value='10 years', inplace=True)
    data['employmentLength'].replace('< 1 year', '0 years', inplace=True)
    data['employmentLength'] = data['employmentLength'].apply(employmentLength_to_int)
data['employmentLength'].value_counts(dropna=False).sort_index()
# 对earliesCreditLine(借款人最早报告的信用额度开立的月份)进行预处理
data_train['earliesCreditLine'].sample(5) # 采五个样本
for data in [data_train, data_test_a]:
    data['earliesCreditLine'] = data['earliesCreditLine'].apply(lambda s: int(s[-4:])) # 截取年份
data_train['earliesCreditLine'].sample(5) # 采五个样本
# cate_features
category_fea

类别特征处理

# 部分类别特征
cate_features = ['grade', 'subGrade', \
                 'employmentTitle', 'homeOwnership', 'verificationStatus', 'purpose', 'postCode', 'regionCode', \
                 'applicationType', 'initialListStatus', 'title', 'policyCode']
for f in cate_features:
    print(f, '类型数:', data[f].nunique())
# 像等级这种类别特征,是有优先级的可以用map映射成数值
for data in [data_train, data_test_a]:
    data['grade'] = data['grade'].map({'A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7})
# 类型数在2之上,又不是高维稀疏的,且纯分类的特征(没有优先级比较),可以用get_dummies转化为one-hot编码
# 参考: https://blog.csdn.net/u010712012/article/details/83002388
# https://zhuanlan.zhihu.com/p/139144355
# 勘误:
data_train = pd.get_dummies(data_train, columns=['homeOwnership', 'verificationStatus', 'purpose', 'regionCode'])
data_test_a = pd.get_dummies(data_test_a, columns=['homeOwnership', 'verificationStatus', 'purpose', 'regionCode'])

异常值处理

  1. 当你发现异常值后,一定要先分清是什么原因导致的异常值,然后再考虑如何处理。首先,如果这一异常值并不代表一种规律性的,而是极其偶然的现象,或者说你并不想研究这种偶然的现象,这时可以将其删除。其次,如果异常值存在且代表了一种真实存在的现象,那就不能随便删除。在现有的欺诈场景中很多时候欺诈数据本身相对于正常数据来说就是异常的,我们要把这些异常点纳入,重新拟合模型,研究其规律。能用监督的用监督模型,不能用的还可以考虑用异常检测的算法来做。
  2. 注意test的数据不能删。

检测异常的方法一:均方差

在统计学中,如果一个数据分布近似正态,那么大约 68% 的数据值会在均值的一个标准差范围内,大约 95% 会在两个标准差范围内,大约 99.7% 会在三个标准差范围内。

def find_outliers_by_3sigma(data,fea):
    data_std = np.std(data[fea])
    data_mean = np.mean(data[fea])
    outliers_cut_off = data_std * 3
    lower_rule = data_mean - outliers_cut_off
    upper_rule = data_mean + outliers_cut_off
    data[fea+'_outliers'] = data[fea].apply(lambda x: '异常值' if x > upper_rule or x < lower_rule else '正常值')
    return data
# data_train = data_train.copy()
for item in ['homeOwnership', 'verificationStatus', 'purpose', 'regionCode']:
    numerical_fea.remove(item)
for fea in numerical_fea:
    data_train = find_outliers_by_3sigma(data_train,fea)
    print(data_train[fea+'_outliers'].value_counts())
    print(data_train.groupby(fea+'_outliers')['isDefault'].sum()) # 当前特则为正常值(异常值)时,欺诈用户的数量
    print('*'*10)
# 删除异常值
for fea in numerical_fea:
    data_train = data_train[data_train[fea+'_outliers']=='正常值'] # 保留正常值对应的行
data_train = data_train.reset_index(drop=True) # 勘误

检测异常的方法二:箱型图

总结一句话:四分位数会将数据分为三个点和四个区间,IQR = Q3 -Q1,下触须=Q1 − 1.5x IQR,上触须=Q3 + 1.5x IQR;

数据分桶

  1. 特征分箱的目的:

    a. 从模型效果上来看,特征分箱主要是为了降低变量的复杂性,减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定。

  2. 数据分桶的对象:

    a. 将连续变量离散化

    b. 将多状态的离散变量合并成少状态

  3. 分箱的原因:

    a. 数据的特征内的值跨度可能比较大,对有监督和无监督中如k-均值聚类它使用欧氏距离作为相似度函数来
    测量数据点之间的相似度。都会造成大吃小的影响,其中一种解决方法是对计数值进行区间量化即数据
    分桶也叫做数据分箱,然后使用量化后的结果。

  4. 分箱的优点:

    a. 处理缺失值:当数据源可能存在缺失值,此时可以把null单独作为一个分箱。

    b. 处理异常值:当数据中存在离群点时,可以把其通过分箱离散化处理,从而提高变量的鲁棒性(抗干扰能力)。例如,age若出现200这种异常值,可分入“age > 60”这个分箱里,排除影响。

    c. 业务解释性:我们习惯于线性判断变量的作用,当x越来越大,y就越来越大。但实际x与y之间经常存在
    着非线性关系,此时可经过WOE变换。

  5. 特别要注意一下分箱的基本原则:

    a. (1)最小分箱占比不低于5%

    b. (2)箱内不能全部是好客户

    c. (3)连续箱单调

"""
1. 固定宽度分箱
当数值横跨多个数量级时,最好按照 10 的幂(或任何常数的幂)来进行分组:0-9、10-99、100-999、1000-9999,等
等。固定宽度分箱非常容易计算,但如果计数值中有比较大的缺口,就会产生很多没有任何数据的空箱子。
"""
# 通过除法映射到间隔均匀的分箱中,每个分箱的取值范围都是loanAmnt/1000
data['loanAmnt_bin1'] = np.floor_divide(data['loanAmnt'], 1000) # 向下取整
# 通过对数函数映射到指数宽度分箱
data['loanAmnt_bin2'] = np.floor(np.log10(data['loanAmnt']))
"""
2.分位数分箱
"""
data['loanAmnt_bin3'] = pd.qcut(data['loanAmnt'], 10, labels=False) # 根据分位数分10箱,且只显示第几箱(不显示箱的范围如(xxx,xxx]))
"""
3.卡方分箱及其他分箱方法的尝试(略)
"""

特征交互

  1. 交互特征的构造非常简单,使用起来却代价不菲。如果线性模型中包含有交互特征对,那它的训练时间和评
    分时间就会从 O(n) 增加到 O( n 2 n^2 n2),其中 n 是单一特征的数量。
# 获得grade, subGrade和isDefault的均值的映射关系,并用均值创建新的列
for col in ['grade', 'subGrade']:
    # temp_dict:获得每个col元素对应的isDefault的均值的映射关系
    # temp_dict = data_train.groupby([col])['isDefault'].agg(['mean']).reset_index().rename(columns={'mean': col + '_target_mean'})
    # temp_dict.index = temp_dict[col].values
    # temp_dict = temp_dict[col + '_target_mean'].to_dict()
    # 上面的代码可优化为
    temp_dict = data_train.groupby([col])['isDefault'].agg('mean').to_dict()
    
    data_train[col + '_target_mean'] = data_train[col].map(temp_dict)
    data_test_a[col + '_target_mean'] = data_test_a[col].map(temp_dict)
    
# 其他衍生变量 mean 和 std
for df in [data_train, data_test_a]:
    for item in ['n0','n1','n2','n3','n4','n5','n6','n7','n8','n9','n10','n11','n12','n13','n14']:
        df['grade_to_mean_' + item] = df['grade'] / df.groupby([item])['grade'].transform('mean')
        df['grade_to_std_' + item] = df['grade'] / df.groupby([item])['grade'].transform('std')

特征编码

labelEncode直接放入树模型中

#label-encode:subGrade,postCode,title
# 高维类别特征需要进行转换
for col in tqdm(['employmentTitle', 'postCode', 'title','subGrade']): # tqdm用于展示进度条
    le = LabelEncoder()
    le.fit(list(data_train[col].astype(str).values) + list(data_test_a[col].astype(str).values))
    data_train[col] = le.transform(list(data_train[col].astype(str).values))
    data_test_a[col] = le.transform(list(data_test_a[col].astype(str).values))
print('Label Encoding 完成')
# 参考资料:
# Python数据预处理中的LabelEncoder与OneHotEncoder: https://blog.csdn.net/quintind/article/details/79850455

逻辑回归等模型要单独增加的特征工程

  1. 对特征做归一化,去除相关性高的特征
  2. 归一化目的是让训练过程更好更快的收敛,避免特征大吃小的问题
  3. 去除相关性是增加模型的可解释性,加快预测过程。
# 举例归一化过程
# 伪代码
# for fea in [要归一化的特征列表]:
#     data[fea] = ((data[fea] - np.min(data[fea])) / (np.max(data[fea]) - np.min(data[fea])))

特征选择

  1. 特征选择技术可以精简掉无用的特征,以降低最终模型的复杂性,它的最终目的是得到一个简约模型,在不降低预测准确率或对预测准确率影响不大的情况下提高计算速度。特征选择不是为了减少训练时间(实际上,一些技术会增加总体训练时间),而是为了减少模型评分时间。

特征选择的方法:

  1. 1 Filter

    a. 方差选择法

    b. 相关系数法(pearson 相关系数)

    c. 卡方检验

    d. 互信息法

  2. 2 Wrapper (RFE)

    a. 递归特征消除法

  3. 3 Embedded

    a. 基于惩罚项的特征选择法

    b. 基于树模型的特征选择

Filter

  1. 基于特征间的关系进行筛选
['a', 'b'] + ['c']
"""
方差选择法
1. 方差选择法中,先要计算各个特征的方差,然后根据设定的阈值,选择方差大于阈值的特征
"""
from sklearn.feature_selection import VarianceThreshold
#其中参数threshold为方差的阈值
VarianceThreshold(threshold=3).fit_transform(train,target_train)
"""
相关系数法
1. Pearson 相关系数
皮尔森相关系数是一种最简单的,可以帮助理解特征和响应变量之间关系的方法,该方法衡量的是变量之间的线性相关性。
结果的取值区间为 [-1,1] , -1 表示完全的负相关, +1表示完全的正相关,0 表示没有线性相关。
"""
from sklearn.feature_selection import SelectKBest
from scipy.stats import pearsonr
#选择K个最好的特征,返回选择特征后的数据
#第一个参数为计算评估特征是否好的函数,该函数输入特征矩阵和目标向量,
#输出二元组(评分,P值)的数组,数组第i项为第i个特征的评分和P值。在此定义为计算相关系数
#参数k为选择的特征个数

SelectKBest(k=5).fit_transform(train,target_train)
"""
卡方检验
1. 经典的卡方检验是用于检验自变量对因变量的相关性。 假设自变量有N种取值,因变量有M种取值,考虑自变
量等于i且因变量等于j的样本频数的观察值与期望的差距。 其统计量如下: χ2=Σ(A−T)2T,其中A为实际值,
T为理论值
2. (注:卡方只能运用在正定矩阵上,否则会报错Input X must be non-negative)
"""
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
#参数k为选择的特征个数
SelectKBest(chi2, k=5).fit_transform(train,target_train)
"""
互信息法
1. 经典的互信息也是评价自变量对因变量的相关性的。 在feature_selection库的SelectKBest类结合最大信息系数
法可以用于选择特征,相关代码如下:
"""
from sklearn.feature_selection import SelectKBest
from minepy import MINE
#由于MINE的设计不是函数式的,定义mic方法将其为函数式的,
#返回一个二元组,二元组的第2项设置成固定的P值0.5
def mic(x, y):
    m = MINE()
    m.compute_score(x, y)
    return (m.mic(), 0.5)
#参数k为选择的特征个数
SelectKBest(lambda X, Y: array(map(lambda x:mic(x, Y), X.T)).T, k=2).fit_transform(train,target_train)

Wrapper(Recursive feature elimination,RFE)

"""
1. 递归特征消除法 递归消除特征法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,
再基于新的特征集进行下一轮训练。 在feature_selection库的RFE类可以用于选择特征,相关代码如下(以逻辑
回归为例):
"""
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
#递归特征消除法,返回特征选择后的数据
#参数estimator为基模型
#参数n_features_to_select为选择的特征个数
RFE(estimator=LogisticRegression(),
n_features_to_select=2).fit_transform(train,target_train)

Embedded

"""
1. 基于惩罚项的特征选择法 使用带惩罚项的基模型,除了筛选出特征外,同时也进行了降维。 在
feature_selection库的SelectFromModel类结合逻辑回归模型可以用于选择特征,相关代码如下:
"""
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
#带L1惩罚项的逻辑回归作为基模型的特征选择
SelectFromModel(LogisticRegression(penalty="l1", C=0.1)).fit_transform(train,target_train)
"""
基于树模型的特征选择 树模型中GBDT也可用来作为基模型进行特征选择。 在feature_selection库的
SelectFromModel类结合GBDT模型可以用于选择特征,相关代码如下:
"""
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import GradientBoostingClassifier
#GBDT作为基模型的特征选择
SelectFromModel(GradientBoostingClassifier()).fit_transform(train,target_train)

数据处理

本数据集中我们删除非入模特征后,并对缺失值填充,然后用计算协方差的方式看一下特征间相关性,然后进行模型训练

# 删除不需要的数据
for data in [data_train, data_test_a]:
    data.drop(['issueDate','id'], axis=1,inplace=True)

# 略

总结

特征工程是机器学习,甚至是深度学习中最为重要的一部分,在实际应用中往往也是所花费时间最多的一步。各
种算法书中对特征工程部分的讲解往往少得可怜,因为特征工程和具体的数据结合的太紧密,很难系统地覆盖所
有场景。本章主要是通过一些常用的方法来做介绍,例如缺失值异常值的处理方法详细对任何数据集来说都是适
用的。但对于分箱等操作本章给出了具体的几种思路,需要读者自己探索。在特征工程中比赛和具体的应用还是
有所不同的,在实际的金融风控评分卡制作过程中,由于强调特征的可解释性,特征分箱尤其重要。学有余力同
学可以自行多尝试,希望大家在本节学习中有所收获。

Task4 建模与调参

学习目标

  1. 学习在金融分控领域常用的机器学习模型

  2. 学习机器学习模型的建模过程与调参流程

  3. 完成相应学习打卡任务

内容介绍

模型相关原理介绍

逻辑回归模型

https://blog.csdn.net/han_xiaoyang/article/details/49123419

决策树模型

https://blog.csdn.net/c406495762/article/details/76262487

GBDT模型

https://zhuanlan.zhihu.com/p/45145899

XGBoost模型

https://blog.csdn.net/wuzhongqiang/article/details/104854890

LightGBM模型

https://blog.csdn.net/wuzhongqiang/article/details/105350579

Catboost模型

https://mp.weixin.qq.com/s/xloTLr5NJBgBspMQtxPoFA

时间序列模型(选学)

RNN:https://zhuanlan.zhihu.com/p/45289691

LSTM:https://zhuanlan.zhihu.com/p/83496936

推荐教材:

《机器学习》 https://book.douban.com/subject/26708119/

《统计学习方法》 https://book.douban.com/subject/10590856/

《面向机器学习的特征工程》 https://book.douban.com/subject/26826639/

《信用评分模型技术与应用》https://book.douban.com/subject/1488075/

《数据化风控》https://book.douban.com/subject/30282558/

模型对比与性能评估

逻辑回归

优缺点见pdf

决策树模型

优缺点见pdf

集成模型集成方法(ensemble method)

模型评估方法

数据集划分总结

  1. 对于数据量充足的时候,通常采用留出法或者k折交叉验证法来进行训练/测试集的划分;
  2. 对于数据集小且难以有效划分训练/测试集时使用自助法;
  3. 对于数据集小且可有效划分的时候最好使用留一法来进行划分,因为这种方法最为准确

模型评估标准

AUC

代码示例

导入相关包和相关设置

import pandas as pd
import numpy as np
import warnings
import os
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
"""
sns 相关设置
"""
# 声明使用 Seaborn 样式
sns.set()
# 有五种seaborn的绘图风格,它们分别是:darkgrid, whitegrid, dark, white, ticks。默认的主题是darkgrid。
sns.set_style("whitegrid")
# 有四个预置的环境,按大小从小到大排列分别为:paper, notebook, talk, poster。其中,notebook是默认的。
sns.set_context('talk')
# 中文字体设置-黑体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 解决保存图像是负号'-'显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
# 解决Seaborn中文显示问题并调整字体大小
sns.set(font='SimHei')

读取数据

# reduce_mem_usage 函数通过调整数据类型,帮助我们减少数据在内存中占用的空间
def reduce_mem_usage(df):
    start_mem = df.memory_usage().sum()/1024/1024
    print('Memory usage of dataframe is {:.2f} MB'.format(start_mem))
    
    for col in df.columns:
        col_type = df[col].dtype
        
        # 非object类型的对象(数值类型对象)
        if col_type != object:
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)
        else:
            df[col] = df[col].astype('category')
            
    end_mem = df.memory_usage().sum()/1024/1024
    print('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
    print('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))
    return df

# 读取数据
train = pd.read_csv('./train.csv')
train = reduce_mem_usage(train)
test = pd.read_csv('./testA.csv')
test = reduce_mem_usage(test)

简单建模

Tips1:金融风控的实际项目多涉及到信用评分,因此需要模型特征具有较好的可解释性,所以目前在实际项目
中多还是以逻辑回归作为基础模型。但是在比赛中以得分高低为准,不需要严谨的可解释性,所以大多基于集
成算法进行建模。

Tips2:因为逻辑回归的算法特性,需要提前对异常值、缺失值数据进行处理【参考task3部分】

Tips3:基于树模型的算法特性,异常值、缺失值处理可以跳过,但是对于业务较为了解的同学也可以自己对缺
失异常值进行处理,效果可能会更优于模型处理的结果。

注:以下建模的源数据参考baseline进行了相应的特征工程,对于异常缺失值未进行相应的处理操作。

# 建模之前的预操作
from sklearn.model_selection import KFold
# 分离数据集,方便进行交叉验证
X_train = train.drop(['id', 'issueDate', 'isDefault'], axis=1)
X_test = test.drop(['id', 'issueDate'], axis=1)
y_train = train.loc[:, 'isDefault']
# 使用Lightgbm进行建模
from sklearn.model_selection import train_test_split
import lightgbm as lgb
# 数据集划分
X_train_split, X_val, y_train_split, y_val = train_test_split(X_train, y_train, test_size=0.2) # 训练集划分为训练集和验证集
train_matrix = lgb.Dataset(X_train_split, label=y_train_split)
valid_matrix = lgb.Dataset(X_val, label=y_val)

params = {
    'boosting_type': 'gbdt', # gbdt
    'objective': 'binary',   # 二分类
    'learning_rate': 0.1,    # 学习率
    'metric': 'auc',         # 评价指标
    'min_child_weight': 1e-3,
    'num_leaves': 31,        # 叶子节点数量
    'max_depth': -1,
    'reg_lambda': 0,         # 目标函数lambda
    'reg_alpha': 0,          # 目标函数alpha
    'feature_fraction': 1,   # 建树的特征选择比例
    'bagging_fraction': 1,   # 建树的样本采样比例
    'bagging_freq': 0,       # k 意味着每 k 次迭代执行bagging
    'seed': 2020,
    'nthread': 8,
#     'silent': True,
    'verbose': -1,           # <0 显示致命的, =0 显示错误 (警告), >0 显示信息
} 

"""使用训练集数据进行模型训练"""
model = lgb.train(params, train_set=train_matrix, valid_sets=valid_matrix, \
                  num_boost_round=20000, verbose_eval=1000, early_stopping_rounds=200) # verbose_eval是打印log的间隔
# 对验证集进行预测
from sklearn import metrics
from sklearn.metrics import roc_auc_score

"""预测并计算roc的相关指标"""
val_pre_lgb = model.predict(X_val, num_iteration=model.best_iteration) # val的预测值
fpr, tpr, threshold = metrics.roc_curve(y_val, val_pre_lgb) # 绘制roc曲线
roc_auc = metrics.auc(fpr, tpr)
print('未调参前lightgbm单模型在验证集上的AUC:{}'.format(roc_auc))
"""画出roc曲线图"""
plt.figure(figsize=(8, 8))
plt.title('Validation ROC')
plt.plot(fpr, tpr, 'b', label = 'Val AUC = %0.4f' % roc_auc)
plt.ylim(0,1)
plt.xlim(0,1)
plt.legend(loc='best')
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
# 画出对角线
plt.plot([0,1],[0,1],'r--')
plt.show()
# 更进一步的,使用5折交叉验证进行模型性能评估
"""使用lightgbm 5折交叉验证进行建模预测"""
cv_scores = []

# 5折交叉验证
folds = 5
seed = 2020
kf = KFold(n_splits=folds, shuffle=True, random_state=seed)

for i, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):
    print('************************************ {} \
    ************************************'.format(str(i+1)))
    X_train_split, y_train_split, X_val, y_val = X_train.iloc[train_index], \
          y_train[train_index], X_train.iloc[valid_index], y_train[valid_index]
    train_matrix = lgb.Dataset(X_train_split, label=y_train_split)
    valid_matrix = lgb.Dataset(X_val, label=y_val)
          
    params = {
    'boosting_type': 'gbdt',
    'objective': 'binary',
    'learning_rate': 0.1,
    'metric': 'auc',
    'min_child_weight': 1e-3,
    'num_leaves': 31,
    'max_depth': -1,
    'reg_lambda': 0,
    'reg_alpha': 0,
    'feature_fraction': 1,
    'bagging_fraction': 1,
    'bagging_freq': 0,
    'seed': 2020,
    'nthread': 8,
    'verbose': -1,
    }
          
    model = lgb.train(params, train_set=train_matrix, num_boost_round=20000, \
                      valid_sets=valid_matrix, verbose_eval=1000, early_stopping_rounds=200)
          
    val_pred = model.predict(X_val, num_iteration=model.best_iteration)
    cv_scores.append(roc_auc_score(y_val, val_pred))
      
print("lgb_scotrainre_list:{}".format(cv_scores))
print("lgb_score_mean:{}".format(np.mean(cv_scores)))
print("lgb_score_std:{}".format(np.std(cv_scores)))

模型调参

  1. 贪心调参

先使用当前对模型影响最大的参数进行调优,达到当前参数下的模型最优化,再使用对模型影响次之的参数进行调优,如此下去,直到所有的参数调整完毕。

这个方法的缺点就是可能会调到局部最优而不是全局最优,但是只需要一步一步的进行参数最优化调试即可,容易理解。

需要注意的是在树模型中参数调整的顺序,也就是各个参数对模型的影响程度,这里列举一下日常调参过程中常用的参数和调参顺序:

  1. ①:max_depth、num_leaves
  2. ②:min_data_in_leaf、min_child_weight
  3. ③:bagging_fraction、 feature_fraction、bagging_freq
  4. ④:reg_lambda、reg_alpha
  5. ⑤:min_split_gain
from sklearn.model_selection import cross_val_score

# 调objective
best_obj = dict()
for obj in objective:
    model = LGBMRegressor(objective=obj)
    """预测并计算roc的相关指标"""
    score = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()
    best_obj[obj] = score
    
# num_leaves
best_leaves = dict()
for leaves in num_leaves:
model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
num_leaves=leaves)
"""预测并计算roc的相关指标"""
score = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()
best_leaves[leaves] = score

# max_depth
best_depth = dict()
for depth in max_depth:
model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
num_leaves=min(best_leaves.items(), key=lambda x:x[1])[0],
max_depth=depth)
"""预测并计算roc的相关指标"""
score = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()
best_depth[depth] = score

"""
可依次将模型的参数通过上面的方式进行调整优化,并且通过可视化观察在每一个最优参数下模型的得分情况
"""
04-13 03:38