《Python金融大数据风控建模实战》 第4章 数据清洗与预处理

  • 本章引言
  • Python代码实现及注释



《Python金融大数据风控建模实战》 第4章 数据清洗与预处理

本章引言

数据清洗与预处理是整个评分卡模型开发乃至整个机器学习模型开发中非常重要的部分,通常包括数据集成、数据清洗、探索性数据分析和数据预处理。

  • 数据集成:将多个数据源的数据构成一个统一的数据结构或数据表的过程。如果不同数据源有结构化数据与非结构化数据,数据集成时要统一转换为结构化数据,并存储在数据库或数据表中,以备后续模型开发时使用。
  • 数据清洗:即清除“脏数据”的过程,包括数据源自身与数据集成后产生的数据格式不一致、单位不一致、数据冗余等问题。
  • 探索性数据分析:即通过计算简单的统计量或用绘图的方法快速理解原始数据的形式,进一步了解数据,为特征工程提供参考,如可以观察数据中缺失值的情况、异常值的请款和变量分布等信息。
  • 数据预处理:对缺失值、异常值等“脏数据”进行处理的过程。由于大部分机器学习模型不支持缺失值建模或对异常值敏感,缺失值与异常值会影响模型效果,因此,数据预处理是非常必要的,可以保证模型的准确性。

在评分卡模型中,缺失值是非常重要的特征,往往不需要填补,可直接作为特征进行编码或分箱后再进行编码;异常值也是非常重要的特征,尤其在反欺诈模型中,更不能提前处理异常值,在其他评分卡模型中,如果进行变量分箱,异常值也不需要处理,直接作为参数进行分箱即可。

Python代码实现及注释

# 第4章:数据清洗与预处理

'''
os是Python环境下对文件、文件夹执行操作的一个模块
Python中的time模块主要是用来处理时间和转换时间格式的
datetime模块一般用来对时间进行计算
missingno提供了一个灵活且易于使用的缺少数据可视化工具和实用程序的小型工具集,使你可以快速直观地概述数据集的完整性
'''
import os
import pandas as pd
import numpy as np
import time
import datetime
import missingno as msno
import warnings
warnings.filterwarnings("ignore") #忽略警告

'''
matplotlib.pyplot是一些命令行风格函数的集合,使matplotlib以类似于MATLAB的方式工作。每个pyplot函数对一幅图片(figure)做一些改动:比如创建新图片,在图片创建一个新的作图区域(plotting area),在一个作图区域内画直线,给图添加标签(label)等。
matplotlib.pyplot是有状态的,亦即它会保存当前图片和作图区域的状态,新的作图函数会作用在当前图片的状态基础之上。
'''
import matplotlib.pyplot as plt
import matplotlib

'''
在matplotlib模块载入的时候会调用rc_params,并把得到的配置字典保存到rcParams变量中
'''
matplotlib.rcParams['font.sans-serif']=['SimHei']   # 用黑体显示中文
matplotlib.rcParams['axes.unicode_minus']=False     # 正常显示负号

# 数据读取
def data_read(data_path,file_name):
    
    '''
    csv文件是一种用,和换行符区分数据记录和字段的一种文件结构,可以用excel表格编辑,也可以用记事本编辑,是一种类excel的数据存
    储文件,也可以看成是一种数据库。pandas提供了pd.read_csv()方法可以读取其中的数据并且转换成DataFrame数据帧。python的强大
    之处就在于他可以把不同的数据库类型,比如txt/csv/.xls/.sql转换成统一的DataFrame格式然后进行统一的处理。真是做到了标准化。
    pd.read_csv()函数参数:
    	os.path.join()函数:连接两个或更多的路径名组件
    	sep:如果不指定参数,则会尝试使用逗号分隔。
    	delimiter :定界符,备选分隔符(如果指定该参数,则sep参数失效)
    	delim_whitespace : 指定空格是否作为分隔符使用,等效于设定sep=’\s+’。如果这个参数设定为True那么delimiter 参数失效。
    	header :指定行数用来作为列名,数据开始行数。如果文件中没有列名,则默认为0【第一行数据】,否则设置为None。
	''' 	
    df = pd.read_csv( os.path.join(data_path, file_name), delim_whitespace = True, header = None )
    
    
    # 变量重命名
    columns = ['status_account','duration','credit_history','purpose', 'amount',
               'svaing_account', 'present_emp', 'income_rate', 'personal_status',
               'other_debtors', 'residence_info', 'property', 'age',
               'inst_plans', 'housing', 'num_credits',
               'job', 'dependents', 'telephone', 'foreign_worker', 'target']
    
    '''
    修改列名的两种方式为:
       直接使用df.columns的方式重新命名,不过这种方式需要列出所有列名。
       使用rename方法,注意如果需要原地修改需要带上inplace=True的参数,否则原dataframe列名不会发生改变。
    '''
    df.columns = columns
    
    # 将标签变量由状态1,2转为0,1;0表示好用户,1表示坏用户
    df.target = df.target - 1
    return df
    
# 离散变量与连续变量区分   
def category_continue_separation(df,feature_names):
    categorical_var = []
    numerical_var = []
    
    # 将标签变量从特征名字(feature_name)中删除,因为标签变量'target'是由其他变量决定出的结果
    if 'target' in feature_names:
        feature_names.remove('target')
        
    # 先判断类型,如果是int或float就直接作为连续变量
    
    '''
    pandas 形成的dataframe 数据集,如果有些列是数字,有些列是字母,或者有些是bool型,那么如果我们有时候只需要选取特定数据类型
    的列,那么我们可以使用 ,例如:DataFrame.select_dtypes(include[‘int’], exclude=None)其中 include 包含的是需要获取的列
    的类型,exclude 包含的不需要获取的数据类型
    '''
    numerical_var = list(df[feature_names].select_dtypes(include=['int','float','int32','float32','int64','float64']).columns.values)
    
    categorical_var = [x for x in feature_names if x not in numerical_var]
    return categorical_var,numerical_var
def add_str(x):
    str_1 = ['%',' ','/t','$',';','@']
    str_2 = str_1[np.random.randint( 0,high = len(str_1)-1 )]
    return x+str_2
def add_time(num,style="%Y-%m-%d"):

    # time.mktime 将struct_time格式转回成时间戳
    start_time = time.mktime((2010,1,1,0,0,0,0,0,0) )
    stop_time = time.mktime((2015,1,1,0,0,0,0,0,0) )  
    re_time = []
    for i in range(num):
        rand_time = np.random.randint( start_time,stop_time)
        
        #将时间戳生成时间元组
        date_touple = time.localtime(rand_time)
                  
        # time.strftime()可以用来获得当前时间,可以将时间格式化为字符串等等
        re_time.append(time.strftime(style,date_touple))
    return re_time
    
# 添加冗余样本,在原始数据集中随机抽取指定数量的样本,然后再与原样本合并,构造冗余样本集,代码如下:

'''
可以调用add_row()函数,产生10个冗余样本,得到冗余样本数据集,添加行冗余数据:
df_temp = add_row(df,10)
axis=0表示在行层面进行连接,ignore_index 忽略需要连接的frame本身的index。当原本的index没有特别意义的时候可以使用。
df = pd.concat([df,df_temp],axis=0,ignore_index=True)
df.shape
'''
def add_row(df_temp,num):
    # shape()函数返回的是矩阵的信息——行列数,shape[0]表示行数,shape[1]表示列数
    index_1 = np.random.randint( low = 0,high = df_temp.shape[0]-1,size=num)
    return df_temp.loc[index_1]

if __name__ == '__main__':
    path = 'D:\\code\\chapter4'
    data_path = os.path.join(path ,'data')
    file_name = 'german.csv'
    
    # 读取数据
    df = data_read(data_path,file_name)
    
    # 区分离散变量与连续变量
    feature_names = list(df.columns)
    feature_names.remove('target')
    categorical_var,numerical_var = category_continue_separation(df,feature_names)

# df.describe()

    # 注入“脏数据”,变量status_account随机加入特殊字符
    df.status_account = df.status_account.apply(add_str)
    
    # 添加两列时间格式的数据
    df['apply_time'] = add_time(df.shape[0],"%Y-%m-%d")
    df['job_time'] = add_time(df.shape[0],"%Y/%m/%d %H:%M:%S")
    
    # 添加行冗余数据
    df_temp = add_row(df,10)
    df = pd.concat([df,df_temp],axis=0,ignore_index=True)
    df.shape
    
# 数据清洗

    # 默认值显示5列
    df.head()
    
    # 设置显示多列或全部全是
    pd.set_option('display.max_columns', 10)
    df.head()
    pd.set_option('display.max_columns', None)
    df.head()
    
    # 离散变量先看一下范围
    df.status_account.unique()
    
    # 特殊字符清洗 下面这句将特殊字符替换掉了
    df.status_account = df.status_account.apply(lambda x:x.replace(' ','').replace('%','').
                             replace('/t','').replace('$','').replace('@','').replace(';',''))
    df.status_account.unique()
    
    # 时间格式统一  统一为'%Y-%m-%d格式
    
    # str.split(' ')[0]得到的是第一个' '之前的内容
    df['job_time'] = df['job_time'].apply(lambda x:x.split(' ')[0].replace('/','-'))
    
    # 时间为字符串格式转为时间格式
    df['job_time'] = df['job_time'].apply(lambda x:datetime.datetime.strptime( x, '%Y-%m-%d'))
    df['apply_time'] = df['apply_time'].apply(lambda x:datetime.datetime.strptime( x, '%Y-%m-%d'))

    # 样本去冗余
    '''
    这个drop_duplicate方法是对DataFrame格式的数据,去除特定列下面的重复行。返回DataFrame格式的数据。
    subset : column label or sequence of labels, optional 
    用来指定特定的列,默认所有列
    keep : {‘first’, ‘last’, False}, default ‘first’ 
    删除重复项并保留第一次出现的项
    inplace : boolean, default False 
    是直接在原来数据上修改还是保留一个副本
    '''
    df.drop_duplicates(subset=None,keep='first',inplace=True)
    
    df.shape
    
    # 可以按照订单如冗余
    df['order_id'] = np.random.randint( low = 0,high = df.shape[0]-1,size=df.shape[0])
    df.drop_duplicates(subset=['order_id'],keep='first',inplace=True)
    df.shape
    
    '''
     如果有按列名去重复
     如果需要进行列名去冗余,则可以先将数据集做转置,按列名去重复,然后再转置即可剔除重复的列名,以达到去除列冗余的目的
#    df_1 = df.T
     # df[~df.index.duplicated()]#获取不重复记录行
     # df[df.index.duplicated()]#获取重复记录行
#    df_1 = df_1[~df_1.index.duplicated()]
#    df = df_1.T
	'''
	
    # 探索性分析
    # .describe()函数可以用来观察连续变量的常用统计信息,可以直观地了解每个变量的样本数,以及数值的最大值、最小值、标准差和分位数等信息
    df[numerical_var].describe()
    
    # 添加缺失值
    
    '''
    reset_index()函数中,drop=True: 把原来的索引index列去掉,丢掉;drop=False:保留原来的索引(以前的可能是乱的)
    inplace=True:不创建新的对象,直接对原始对象进行修改;
    inplace=False:对数据进行修改,创建并返回新的对象承载其修改结果。
    '''
    df.reset_index(drop=True,inplace=True)
    var_name = categorical_var+numerical_var
    for i in var_name:
    
        # 添加num个index_1
        num = np.random.randint( low = 0,high = df.shape[0]-1)
        index_1 = np.random.randint( low = 0,high = df.shape[0]-1,size=num)
        index_1 = np.unique(index_1)
        
        # 使得第i个特征中num个特征值为空
        df[i].loc[index_1] = np.nan
        
    # 缺失值绘图
    
    '''
    missingno提供了一个灵活且易于使用的缺失数据可视化和实用程序的小工具集,使您可以快速直观地总结数据集的完整性。
    msno.bar 是列的无效的简单可视化
    '''
    msno.bar(df, labels=True,figsize=(10,6), fontsize=10)
    
    # 对于连续数据绘制箱线图,观察是否有异常值   
    plt.figure(figsize=(10,6))    #设置图形尺寸大小
    for j in range(1,len(numerical_var)+1):
    
        # 因为连续变量只有7个,所以用plt.subplot(2,4,j)
        plt.subplot(2,4,j)
        df_temp = df[numerical_var[j-1]][~df[numerical_var[j-1]].isnull()]
        '''
        plt.boxplot(x,                      # x:指定要绘制箱图的数据
            notch=None,           # notch:是否是凹口的形式展现箱线图,默认非凹口
            sym=None,              # sym:指定异常点的形状,默认为+号显示
            vert=None,              # vert:是否需要将箱线图垂直摆放,默认垂直摆放
            whis=None,             # whis:指定上下须与上下四分位的距离,默认为1.5倍的四分位差
            positions=None,   # positions:指定箱线图的位置,默认为[0,1,2…]
            widths=None,         # widths:指定箱线图的宽度,默认为0.5
            patch_artist=None,        # patch_artist:是否填充箱体的颜色
            meanline=None,             # meanline:是否用线的形式表示均值,默认用点来表示
            showmeans=None,       # showmeans:是否显示均值,默认不显示
            showcaps=None,           # showcaps:是否显示箱线图顶端和末端的两条线,默认显示
            showbox=None,             # showbox:是否显示箱线图的箱体,默认显示
            showfliers=None,          # showfliers:是否显示异常值,默认显示
            boxprops=None,           # boxprops:设置箱体的属性,如边框色,填充色等
            labels=None,                  # labels:为箱线图添加标签,类似于图例的作用
            flierprops=None,          # filerprops:设置异常值的属性,如异常点的形状、大小、填充色等
            medianprops=None,   # medianprops:设置中位数的属性,如线的类型、粗细等
            meanprops=None,       # meanprops:设置均值的属性,如点的大小、颜色等
            capprops=None,           # capprops:设置箱线图顶端和末端线条的属性,如颜色、粗细等
            whiskerprops=None)   # whiskerprops:设置须的属性,如颜色、粗细、线的类型等
        '''
        plt.boxplot( df_temp,
                    notch=False,  #中位线处不设置凹陷
                    widths=0.2,   #设置箱体宽度
                    medianprops={'color':'red'},  #中位线设置为红色
                    boxprops=dict(color="blue"),  #箱体边框设置为蓝色
                     labels=[numerical_var[j-1]],  #设置标签
                    whiskerprops = {'color': "black"}, #设置须的颜色,黑色
                    capprops = {'color': "green"},      #设置箱线图顶端和末端横线的属性,颜色为绿色
                    flierprops={'color':'purple','markeredgecolor':"purple"} #异常值属性,这里没有异常值,所以没表现出来
                   )
    plt.show()
    
    # 查看数据分布
    
    # 连续变量不同类别下的分布
    for i in numerical_var:
#        i = 'duration'

        # 取非缺失值的数据       
        df_temp = df.loc[~df[i].isnull(),[i,'target']]
        df_good = df_temp[df_temp.target == 0]
        df_bad = df_temp[df_temp.target == 1]
        
        # 计算统计量
        
        # round()函数中的2表示取两位小数
        valid = round(df_temp.shape[0]/df.shape[0]*100,2)
        Mean = round(df_temp[i].mean(),2)
        Std = round(df_temp[i].std(),2)
        Max = round(df_temp[i].max(),2)
        Min = round(df_temp[i].min(),2)
        
        # 绘图
        
        '''
        plt.figure(figsize=(6,8)),表示figure 的大小为宽、长(单位为inch)
        plt.subplot(121)表示整个figure分成1行2列,共2个子图,这里子图在第一行第一列
        plt.subplot(122)表示子图在第一行第二列
        '''
        plt.figure(figsize=(10,6))
        
        fontsize_1 = 12
        '''
         n,bins,patches=matplotlib.pyplot.hist(  
                        x, bins=10, range=None, normed=False,   
                        weights=None, cumulative=False, bottom=None,   
                        histtype=u'bar', align=u'mid', orientation=u'vertical',   
                        rwidth=None, log=False, color=None, label=None, stacked=False,   
                        hold=None, **kwargs)  

        参数值:
        hist的参数非常多,但常用的有以下6个,只有第一个是必须的,后面5个可选
		x: 作直方图所要用的数据,必须是一维数组。多维数组可以先进行扁平化再作图
		bins: 直方图的柱数,可选项,默认为10
		normed: 是否将得到的直方图向量归一化。默认为0
		facecolor: 直方图颜色
		edgecolor: 直方图边框颜色
		alpha: 透明度
		histtype: 直方图类型,‘bar’, ‘barstacked’, ‘step’, ‘stepfilled’
		返回值:
		n:直方图向量,是否归一化由参数normed设定。当normed取默认值时,n即为直方图各组内元素的数量(各组频数)
		bins: 返回各个bin的区间范围
		patches:返回每个bin里面包含的数据,是一个list
		'''
        plt.hist(df_good[i],  bins =20, alpha=0.5,label='好样本')
        plt.hist(df_bad[i],  bins =20, alpha=0.5,label='坏样本')
        
        # i表示label,fontsize表示字体大小
        plt.ylabel(i,fontsize=fontsize_1)
        plt.title( 'valid rate='+str(valid)+'%, Mean='+str(Mean) + ', Std='+str(Std)+', Max='+str(Max)+', Min='+str(Min))
        
        # plt.legend()函数能够使得像label这样的东西出现
        plt.legend()
        
        # 保存图片
        file = os.path.join(path,'plot_num', i+'.png')
        plt.savefig(file)
     
        plt.close(1)
        
    # 离散变量不同类别下的分布
    for i in categorical_var:
#        i = 'status_account'

        df_temp = df.loc[~df[i].isnull(),[i,'target']]
        df_bad = df_temp[df_temp.target == 1]
        valid = round(df_temp.shape[0]/df.shape[0]*100,2)
     
        bad_rate = []
        bin_rate = []
        var_name = []
        for j in df[i].unique():

            if pd.isnull(j):
                df_1 = df[df[i].isnull()]
                bad_rate.append(sum(df_1.target)/df_1.shape[0])
                bin_rate.append(df_1.shape[0]/df.shape[0])
                var_name.append('NA')
            else:
                df_1 = df[df[i] == j]
                bad_rate.append(sum(df_1.target)/df_1.shape[0])
                bin_rate.append(df_1.shape[0]/df.shape[0])
                var_name.append(j)
        df_2 = pd.DataFrame({'var_name':var_name,'bin_rate':bin_rate,'bad_rate':bad_rate})
        
        # 绘图
        plt.figure(figsize=(10,6))
        fontsize_1 = 12
        plt.bar(np.arange(1,df_2.shape[0]+1),df_2.bin_rate,0.1,color='black',alpha=0.5, label='占比')
        plt.xticks(np.arange(1,df_2.shape[0]+1), df_2.var_name)
        plt.plot( np.arange(1,df_2.shape[0]+1),df_2.bad_rate,  color='green', alpha=0.5,label='坏样本比率')
        
        plt.ylabel(i,fontsize=fontsize_1)
        plt.title( 'valid rate='+str(valid)+'%')
        plt.legend()
        
        # 保存图片
        file = os.path.join(path,'plot_cat', i+'.png')
        plt.savefig(file)
        plt.close(1)