从0到1建立一张评分卡之数据预处理

从 0 到 1 建立一张评分卡

  之前看了很多评分卡建模方面的课程和文章,对评分卡的建立大致有一些了解。但是由于实际工作中没有接触过,也没有看到过比较舒服的代码,所以对评分卡这块一直有点不踏实。所幸最近在 Gayhub 上发现一个特别好用的半自动化建模的库,试了一遍发现堪称神器,比之前看的 scorecardpy、toad 用上去还要方便。对于评分卡这块,如果有大佬有相关的经验,欢迎找我,很想学习一下工作中是如何将评分卡落地的。
  下面以网上找的一个数据集为例,介绍一下如何从 0 到 1 建立一张评分卡。

数据读取

  首先,读取数据,看下数据的大致情况。

folderOfData = './'

allData = pd.read_csv(folderOfData + 'application.csv', header=0, encoding='latin1')
allData['term'] = allData['term'].apply(lambda x: int(x.replace(' months', '')))

# 处理标签:Fully Paid是正常用户;Charged Off是违约用户
allData['y'] = allData['loan_status'].map(lambda x: int(x == 'Charged Off'))

allData1 = allData.loc[allData.term == 36]
trainData, testData = train_test_split(allData1, test_size=0.4)
  • term 应该表示期数,将其中的 months 替换掉
  • 生成 y 变量,charged off 表示违约。
  • 由于存在不同的贷款期限(term),申请评分卡模型评估的违约概率必须要在统一的期限中,且不宜太长,所以选取 term = 36months 的样本

  除去 ID 和 label 变量外一共有 24 个变量,看一下变量类型。

数据清洗

# 将带%的百分比变为浮点数
trainData['int_rate_clean'] = trainData['int_rate'].map(lambda x: float(x.replace('%', '')) / 100)
def CareerYear(x):
    # 对工作年限进行转换
    x = str(x)
    if x.find('nan') > -1:
        return -1
    elif x.find("10+") > -1:  # 将"10+years"转换成 11
        return 11
    elif x.find('< 1') > -1:  # 将"< 1 year"转换成 0
        return 0
    else:
        return int(re.sub("\D", "", x))  # 其余数据,去掉"years"并转换成整数

# 将工作年限进行转化,否则影响排序
trainData['emp_length_clean'] = trainData['emp_length'].map(CareerYear)
def DescExisting(x):
    # 将desc变量转换成有记录和无记录两种
    if type(x).__name__ == 'float':
        return 'no desc'
    else:
        return 'desc'

# 将desc的缺失作为一种状态,非缺失作为另一种状态
trainData['desc_clean'] = trainData['desc'].map(DescExisting)

  这里用到了__name__这个 python 的魔法方法。

def ConvertDateStr(x):
    mth_dict = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10,
                'Nov': 11, 'Dec': 12}
    if str(x) == 'nan':
        return datetime.datetime.fromtimestamp(time.mktime(time.strptime('9900-1', '%Y-%m')))
        # time.mktime 不能读取1970年之前的日期
    else:
        yr = int(x[4:6])
        if yr <= 17:
            yr = 2000 + yr
        else:
            yr = 1900 + yr
        mth = mth_dict[x[:3]]
        return datetime.datetime(yr, mth, 1)

# 处理日期。earliest_cr_line的格式不统一,需要统一格式且转换成python的日期
trainData['app_date_clean'] = trainData['issue_d'].map(lambda x: ConvertDateStr(x))
trainData['earliest_cr_line_clean'] = trainData['earliest_cr_line'].map(lambda x: ConvertDateStr(x))

  这里的时间转换操作比较复杂。

def MakeupMissing(x):
    if np.isnan(x):
        return -1
    else:
        return x

# 处理mths_since_last_delinq。原始值中有0,用-1代替缺失
trainData['mths_since_last_delinq_clean'] = trainData['mths_since_last_delinq'].map(lambda x: MakeupMissing(x))

trainData['mths_since_last_record_clean'] = trainData['mths_since_last_record'].map(lambda x: MakeupMissing(x))

trainData['pub_rec_bankruptcies_clean'] = trainData['pub_rec_bankruptcies'].map(lambda x: MakeupMissing(x))

  下面衍生两个变量。

# 考虑申请额度与收入的占比
trainData['limit_income'] = trainData.apply(lambda x: x.loan_amnt / x.annual_inc, axis=1)

# 考虑earliest_cr_line到申请日期的跨度,以月份记
trainData['earliest_cr_to_app'] = trainData.apply(lambda x: MonthGap(x.earliest_cr_line_clean, x.app_date_clean),axis=1)

  下面利用之前提到的半自动化库进行数据的数据处理。

  首先看一下处理完之后的数据的缺失情况。先看变量的缺失情况。

# 每个变量缺失率的计算
def missing_cal(df):
    """
    df :数据集
    return:每个变量的缺失率
    """
    missing_series = df.isnull().sum()/df.shape[0]
    missing_df = pd.DataFrame(missing_series).reset_index()
    missing_df = missing_df.rename(columns={'index':'col', 0:'missing_pct'})
    missing_df = missing_df.sort_values('missing_pct',ascending=False).reset_index(drop=True)
    return missing_df
missing_cal(trainData)

  可以看到 mths_since_last_record、mths_since_last_delinq、desc 缺失值较多。对缺失率超过一定阈值的变量进行剔除,这里取 0.3,将上述三个变量剔除。

扫描二维码关注公众号,回复: 11236546 查看本文章
# 缺失值剔除(单个变量)
def missing_delete_var(df,threshold=None):
    """
    df:数据集
    threshold:缺失率删除的阈值
    return :删除缺失后的数据集
    """
    df2 = df.copy()
    missing_df = missing_cal(df)
    missing_col_num = missing_df[missing_df.missing_pct>=threshold].shape[0]
    missing_col = list(missing_df[missing_df.missing_pct>=threshold].col)
    df2 = df2.drop(missing_col,axis=1)
    print('缺失率超过{}的变量个数为{}'.format(threshold,missing_col_num))
    return df2
trainData=missing_delete_var(trainData,threshold=0.3)

  然后看下样本的缺失情况。

# 单个样本的缺失分布
def plot_missing_user(df,plt_size=None):
    """
    df: 数据集
    plt_size: 图纸的尺寸

    return :缺失分布图(折线图形式)
    """
    missing_series = df.isnull().sum(axis=1)
    list_missing_num  = sorted(list(missing_series.values))
    plt.figure(figsize=plt_size)
    plt.rcParams['font.sans-serif']=['Microsoft YaHei']
    plt.rcParams['axes.unicode_minus'] = False
    plt.plot(range(df.shape[0]),list_missing_num)
    plt.ylabel('缺失变量个数')
    plt.xlabel('samples')
    return plt.show()
plot_missing_user(trainData,plt_size=None)

  上图是那 3 个缺失变量剔除之前每个样本的缺失情况图。可以看到中间一大段样本缺失变量个数为 2 个,缺失变量最多的个数为 3 个,仅一小部分样本没有缺失变量。将 3 个缺失率较高的变量进行剔除之后的样本缺失情况如下:

  可以看到仅有少数样本有 1 个缺失变量。这里为了展示代码,对有 1 个缺失变量的样本进行剔除。

# 缺失值剔除(单个样本)
def missing_delete_user(df,threshold=None):
    """
    df:数据集
    threshold:缺失个数删除的阈值

    return :删除缺失后的数据集
    """
    df2 = df.copy()
    missing_series = df.isnull().sum(axis=1)
    missing_list = list(missing_series)
    missing_index_list = []
    for i,j in enumerate(missing_list):
        if j>=threshold:
            missing_index_list.append(i)
    df2 = df2[~(df2.index.isin(missing_index_list))]
    print('缺失变量个数在{}以上的用户数有{}个'.format(threshold,len(missing_index_list)))
    return df2

  这里有个坑,就是这个函数的 df 的 index 必须是从 0 开始的,因为 trainData 是经过切分之后的,index 被打乱了,导致用这个函数时剔除的样本一直不对,需要用 trainData.reset_index(drop=True),被这个坑绊住了大半天。
后面还有对缺失变量进行填充,由于这里已经剔除完,样本集不存在缺失值,所以无需进行缺失值的填充。但是还是介绍一下缺失值填充的方法。
类别型变量

  • 用众数进行填充
  • 单独当作一个类别
def fillna_cate_var(df,col_list,fill_type=None):
    """
    df:数据集
    col_list:变量list集合
    fill_type: 填充方式:众数/当做一个类别

    return :填充后的数据集
    """
    df2 = df.copy()
    for col in col_list:
        if fill_type=='class':
            df2[col] = df2[col].fillna('unknown')
        if fill_type=='mode':
            df2[col] = df2[col].fillna(df2[col].mode()[0])
    return df2

数值型变量

  • 缺失率 5%以下:中位数
  • 缺失率 5%-15%:随机森林填充
  • 缺失率 15%以上:当作一个类别
def fillna_num_var(df,col_list,fill_type=None,filled_df=None):
    """
    df:数据集
    col_list:变量list集合
    fill_type:填充方式:中位数/随机森林/当做一个类别
    filled_df :已填充好的数据集,当填充方式为随机森林时 使用

    return:已填充好的数据集
    """
    df2 = df.copy()
    for col in col_list:
        if fill_type=='median':
            df2[col] = df2[col].fillna(df2[col].median())
        if fill_type=='class':
            df2[col] = df2[col].fillna(-999)
        if fill_type=='rf':
            rf_df = pd.concat([df2[col],filled_df],axis=1)
            known = rf_df[rf_df[col].notnull()]
            unknown = rf_df[rf_df[col].isnull()]
            x_train = known.drop([col],axis=1)
            y_train = known[col]
            x_pre = unknown.drop([col],axis=1)
            rf = RandomForestRegressor(random_state=0)
            rf.fit(x_train,y_train)
            y_pre = rf.predict(x_pre)
            df2.loc[df2[col].isnull(),col] = y_pre
    return df2

  此外还有常变量处理和分类变量降基处理。常变量处理就是处理方差过小的变量,而分类变量降基是将一些占比较小的类别进行合并。附上代码。

# 常变量/同值化处理
def const_delete(df,col_list,threshold=None):
    """
    df:数据集
    col_list:变量list集合
    threshold:同值化处理的阈值

    return :处理后的数据集
    """
    df2 = df.copy()
    const_col = []
    for col in col_list:
        const_pct = df2[col].value_counts().iloc[0]/df2[df2[col].notnull()].shape[0]
        if const_pct>=threshold:
            const_col.append(col)
    df2 = df2.drop(const_col,axis=1)
    print('常变量/同值化处理的变量个数为{}'.format(len(const_col)))
    return df2
# 分类型变量的降基处理
def descending_cate(df,col_list,threshold=None):
    """
    df: 数据集
    col_list:变量list集合
    threshold:降基处理的阈值

    return :处理后的数据集
    """
    df2 = df.copy()
    for col in col_list:
        value_series = df[col].value_counts()/df[df[col].notnull()].shape[0]
        small_value = []
        for value_name,value_pct in zip(value_series.index,value_series.values):
            if value_pct<=threshold:
                small_value.append(value_name)
        df2.loc[df2[col].isin(small_value),col]='other'
    return df2

【作者】:Labryant
【原创公众号】:风控猎人
【简介】:某创业公司策略分析师,积极上进,努力提升。乾坤未定,你我都是黑马。
【转载说明】:转载请说明出处,谢谢合作!~

猜你喜欢

转载自blog.csdn.net/lc434699300/article/details/105261387