最近看了一本《Python金融大數據風控建模實戰:基於機器學習》(機械工業出版社)這本書,看了其中第4章:數據清洗和預處理的內容,了解了代碼,覺得寫的不錯,所以分享給大家。
1. 數據集
德國信貸數據集。官網地址 http://archive.ics.uci.edu/ml/datasets/Statlog+%28German+Credit+Data%29 。
該數據集包含 1000條樣本數據,其中有20個標簽變量('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')和1個結果變量('target')。
數據集以及說明我已經上傳到網盤中:
鏈接:https://pan.baidu.com/s/1yWqbUHsP8wky0_kiOE1mEQ
提取碼:6666
2. 代碼思路
數據清洗和預處理的一般流程:
(1)數據集成:將多個數據源的數據構成一個統一的數據結構或數據表的過程。
(2)數據清洗:即清除“臟數據”的過程。數據清洗包括特殊字符清洗、數據格式轉換、數據概念統一、數據類型轉換和樣本去冗余等。
(3)探索性數據分析(Exploratory Data Analysis,EDA)又稱為描述性統計分析,是一種通過計算統計量、數據可視化等方法,快速了解原始數據的結構與規律的一種數據分析方法。
(4)數據預處理:對缺失值、異常值等“臟數據”進行處理的過程。
3. python代碼詳解(代碼來自該書第四章內容),代碼已經寫了詳細注釋,該代碼相對簡單,只要認真走一遍就能看懂~
1 import os 2 import pandas as pd 3 import numpy as np 4 import time 5 import datetime 6 # missingno是缺失值可視化工具庫
7 import missingno as msno 8 import warnings 9 warnings.filterwarnings("ignore") # 忽略警告
10 import matplotlib.pyplot as plt 11 import matplotlib 12 matplotlib.rcParams['font.sans-serif'] = ['SimHei'] # 用黑體顯示中文
13 matplotlib.rcParams['axes.unicode_minus'] = False # 正常顯示負號
14
15
16 # 數據讀取
17 def data_read(data_path, file_name): 18
19 data_path_whole = os.path.join(data_path, file_name) 20 # header=None表示第一行不是列名
21 # delim_whitespace表示是否用空格為分隔符,如果為True,那么其他字符如,就不會被認為是分隔符
22 df = pd.read_csv(data_path_whole, delim_whitespace=True, header=None) 23 # 變量重命名
24 columns = ['status_account', 'duration', 'credit_history', 'purpose', 'amount', 'svaing_account', 'present_emp', 'income_rate', 'personal_status', 25 'other_debtors', 'residence_info', 'property', 'age', 'inst_plans','housing', 'num_credits', 'job', 'dependents', 'telephone', 'foreign_worker', 'target'] 26 # 添加列標題
27 df.columns = columns 28 # 將標簽變量由狀態1,2轉為0,1; 0表示好用戶,1表示壞用戶
29 df.target = df.target - 1
30
31 return df 32
33
34 # 離散變量與連續變量區分
35 def category_continue_separation(df, feature_names): 36 categorical_var = [] 37 numerical_var = [] 38 # 移除 "target" 標簽
39 if 'target' in feature_names: 40 feature_names.remove('target') 41 # 先判斷類型,如果是int或float就直接作為連續變量
42 # DataFrame的select_dtypes方法是返回具有指定數據類型的列
43 numerical_var = list(df[feature_names].select_dtypes(include=['int', 'float', 'int32', 'float32', 'int64', 'float64']).columns.values) 44 # 獲取剩下的標簽
45 categorical_var = [x for x in feature_names if x not in numerical_var] 46
47 return categorical_var, numerical_var 48
49
50 # 字符串末位隨機添加符號
51 def add_str(x): 52 str_1 = ['%', ' ', '/t', '$', ';', '@'] 53 str_2 = str_1[np.random.randint(0, high=len(str_1) - 1)] 54 return x + str_2 55
56
57 # 生成一列時間戳數據
58 def add_time(num, style="%Y-%m-%d"): 59 # time.mktime(t) 函數接收struct_time對象作為參數,返回用秒數來表示時間的浮點數。
60 # 它的參數t是 結構化的時間或者完整的9位元組元素
61 start_time = time.mktime((2010, 1, 1, 0, 0, 0, 0, 0, 0)) 62 stop_time = time.mktime((2015, 1, 1, 0, 0, 0, 0, 0, 0)) 63 re_time = [] 64 for i in range(num): 65 rand_time = np.random.randint(start_time, stop_time) 66 # 將時間戳生成時間元組
67 # Python time localtime() 作用是格式化時間戳為本地的時間
68 date_touple = time.localtime(rand_time) 69 # Python time strftime() 函數接收以時間元組,並返回以可讀字符串表示的當地時間,格式由參數 format 決定。
70 re_time.append(time.strftime(style, date_touple)) 71 return re_time 72
73
74 # 獲取行值
75 def add_row(df_temp, num): 76 # 生成num個隨機值
77 index_1 = np.random.randint(low=0, high=df_temp.shape[0] - 1, size=num) 78 # loc為Selection by Label函數,即為按標簽取數據,這里是獲取數值標簽的數據,這里的數值標簽是行,所以就是獲取這些行的值
79 return df_temp.loc[index_1] 80
81
82 if __name__ == '__main__': 83 # 獲取當前文件路徑
84 path_current = os.getcwd() 85 data_path = os.path.join(path_current, 'data') 86 file_name = 'german.csv'
87
88 # 讀取數據
89 df = data_read(data_path, file_name) 90
91 # 區分離散變量與連續變量
92 feature_names = list(df.columns) 93 feature_names.remove('target') 94 categorical_var, numerical_var = category_continue_separation(df, feature_names) 95 # describe()函數就是返回數據結構的統計變量。其目的在於觀察這一系列數據的范圍、大小、波動趨勢等等,為后面的模型選擇打下基礎。
96 # 統計值變量說明: count:數量統計,此列共有多少有效值; unipue:不同的值有多少個; std:標准差; min:最小值
97 # 25%:四分之一分位數; 50%:二分之一分位數; 75%:四分之三分位數; max:最大值; mean:均值
98 print(df.describe()) 99
100 # ------------------------------- 數據清洗 --------------------------------- #
101 # 注入“臟數據”
102 # 變量status_account 隨機加入特殊字符
103 # apply函數主要用於對DataFrame中的行或者列進行特定的函數計算。
104 df.status_account = df.status_account.apply(add_str) 105 # 添加兩列時間格式的數據
106 df['apply_time'] = add_time(df.shape[0], "%Y-%m-%d") 107 df['job_time'] = add_time(df.shape[0], "%Y/%m/%d %H:%M:%S") 108 # 添加行冗余數據,即復制了幾行的數據
109 df_temp = add_row(df, 10) 110 # pandas.concat()通常用來連接DataFrame對象
111 df = pd.concat([df, df_temp], axis=0, ignore_index=True) 112
113 # Pandas讀取數據之后使用pandas的head()函數的時候,來觀察一下讀取的數據。head()函數只能讀取前五行數據
114 print(df.head()) 115
116 # 設置顯示的最大列數,None表示列都要顯示
117 pd.set_option('display.max_columns', 10) 118 print("---------------------------------------------") 119 print(df.head()) 120 pd.set_option('display.max_columns', None) 121 print("---------------------------------------------") 122 print(df.head()) 123
124 # ------------------ 特殊字符清洗 -------------- #
125 # 離散變量先看一下范圍
126 print("特殊字符清洗之前:", df.status_account.unique()) 127 df.status_account = df.status_account.apply( 128 lambda x: x.replace(' ', '').replace('%', '').replace('/t', '').replace('$', '').replace('@', '').replace(';', '')) 129 print("特殊字符清洗之后:", df.status_account.unique()) 130
131 # ----------------- 時間格式統一 ----------------- #
132 # 統一為'%Y-%m-%d格式
133 df['job_time'] = df['job_time'].apply(lambda x: x.split(' ')[0].replace('/', '-')) 134 # 時間為字符串格式轉為時間格式
135 df['job_time'] = df['job_time'].apply(lambda x: datetime.datetime.strptime(x, '%Y-%m-%d')) 136 df['apply_time'] = df['apply_time'].apply(lambda x: datetime.datetime.strptime(x, '%Y-%m-%d')) 137
138 # 樣本去冗余
139 print("樣本去冗余之前的形狀 df.shape = ", df.shape) 140 # pandas中的drop_duplicates()函數是根據指定的字段對數據集進行去重處理
141 # 參數:subset根據指定的列名進行去重,默認整個數據集
142 # keep 可選{‘first’, ‘last’, False},默認first,即默認保留第一次出現的重復值,並刪去其他重復的數據,False是指刪去所有重復數據。
143 # inplace 是否對數據集本身進行修改,默認False布爾值參數,默認為 False 表示刪除重復項后返回一個副本,若為 Ture 則表示直接在原數據上刪除重復項。
144 df.drop_duplicates(subset=None, keep='first', inplace=True) 145 print("樣本去冗余之后的形狀 df.shape = ", df.shape) 146
147 # 假設訂單id出現重復則去除冗余實例
148 print("按照訂單id去除冗余之前 df.shape = ", df.shape) 149 df['order_id'] = np.random.randint(low=0, high=df.shape[0] - 1, size=df.shape[0]) 150 df.drop_duplicates(subset=['order_id'], keep='first', inplace=True) 151 print("按照訂單id去除冗余之后 df.shape = ", df.shape) 152
153 # 探索性分析
154 print(df[numerical_var].describe()) 155
156 # --------------------- 添加缺失值 ------------------- #
157 # 數據清洗時,會將帶某行刪除,此時DataFrame或Series類型的數據不再是連續的索引,可以使用reset_index()重置索引。
158 # 在獲得新的index,原來的index變成數據列,保留下來。不想保留原來的index,使用參數 drop=True
159 df.reset_index(drop=True, inplace=True) 160 print(df) 161 var_name = categorical_var + numerical_var 162 for i in var_name: 163 num = np.random.randint(low=0, high=df.shape[0] - 1) 164 # 從num中隨機生成num個數
165 index_1 = np.random.randint(low=0, high=df.shape[0] - 1, size=num) 166 print("=============================================") 167 print("去重之前的 len(index_1) = ", len(index_1)) 168 # 對數據進行去重
169 index_1 = np.unique(index_1) 170 print("去重之后的 len(index_1) = ", len(index_1)) 171 df[i].loc[index_1] = np.nan 172
173 # 缺失值繪圖
174 # 數據分析之前首先要保證數據集的質量,missingno庫提供了一個靈活易用的可視化工具來觀察數據缺失情況
175 # msno.bar是按列對空值進行簡單可視化
176 msno.bar(df, labels=True, figsize=(10, 6), fontsize=10) 177
178 # !!!! 注意,繪制以下三種圖都需要現剔除缺失值再繪制箱線圖
179 # 對於連續數據繪制箱線圖,觀察是否有異常值
180 plt.figure(figsize=(10, 6)) # 設置圖形尺寸大小
181 for j in range(1, len(numerical_var) + 1): 182 plt.subplot(2, 4, j) 183 df_temp = df[numerical_var[j - 1]][~df[numerical_var[j - 1]].isnull()] 184 # 繪制箱線圖函數
185 # 箱線圖在使用場景上最常見的是用於質量管理、人事測評、探索性數據分析等統計分析活動。
186 # (1)識別異常值。異常值是指數據中遠離其他大部分值的數據。
187 # (2)判斷偏態。偏態是指與正態分布相對,非對稱分布的偏斜狀態。若平均數大於眾數,則為右偏態(正偏態);若平均數小於眾數,則為左偏態(負偏態)。
188 # (3)評估數據集中程度。箱線圖的寬度一定程度反映了數據的波動程度。因為箱線圖包含中間50%的數據,若它越扁,則說明數據較為集中;若它越寬,則說明數據較為分散。
189 # 箱圖是一中用於統計數據分布的統計圖,也可以粗略地看出數據是否具有對稱性,分布的分散程度等信息。箱圖中的信息含義如下:
190 # 最下方的橫線表示最小值, 最上方的橫線表示最大值, 黑色空心圓圈表示異常值, 黑色實心圓圈表示極端值, 箱子由下四分位數、中值以及上四分位數組成
191 plt.boxplot( 192 df_temp, 193 notch=False, # 中位線處不設置凹陷
194 widths=0.2, # 設置箱體寬度
195 medianprops={'color': 'red'}, # 中位線設置為紅色
196 boxprops=dict(color="blue"), # 箱體邊框設置為藍色
197 labels=[numerical_var[j - 1]], # 設置標簽
198 whiskerprops={'color': "black"}, # 設置須的顏色,黑色
199 capprops={'color': "green"}, # 設置箱線圖頂端和末端橫線的屬性,顏色為綠色
200 flierprops={'color': 'purple', 'markeredgecolor': "purple"} # 異常值屬性,這里沒有異常值,所以沒表現出來
201 ) 202 plt.show() 203
204 # ------------------------------- 查看數據分布 ------------------------------ #
205 # -------- 連續變量不同類別下的分布 ---------- #
206 for i in numerical_var: 207 # i = 'duration'
208 # 取非缺失值的數據
209 # isnull()函數,它可以用來判斷數據是否缺失值,這里是獲取沒有缺失值的數據
210 df_temp = df.loc[~df[i].isnull(), [i, 'target']] 211 df_good = df_temp[df_temp.target == 0] 212 df_bad = df_temp[df_temp.target == 1] 213 # 計算統計量
214 valid = round(df_temp.shape[0] / df.shape[0] * 100, 2) 215 Mean = round(df_temp[i].mean(), 2) 216 Std = round(df_temp[i].std(), 2) 217 Max = round(df_temp[i].max(), 2) 218 Min = round(df_temp[i].min(), 2) 219 # 繪圖
220 plt.figure(figsize=(10, 6)) 221 fontsize_1 = 12
222 # 繪制直方圖函數,其中 bins: 直方圖的柱數,即要分的組數,默認為10;alpha: 透明度;
223 plt.hist(df_good[i], bins=20, alpha=0.5, label='好樣本') 224 plt.hist(df_bad[i], bins=20, alpha=0.5, label='壞樣本') 225 plt.ylabel(i, fontsize=fontsize_1) 226 plt.title('valid rate=' + str(valid) + '%, Mean=' + str(Mean) +', Std=' + str(Std) + ', Max=' + str(Max) + ', Min=' + str(Min)) 227 plt.legend() 228 # 保存圖片
229 file = os.path.join(path_current, 'plot_num', i + '.png') 230 plt.savefig(file) 231 plt.close(1) 232
233 # --------- 離散變量不同類別下的分布 --------- #
234 for i in categorical_var: 235 # i = 'status_account'
236 # 獲取非缺失值數據
237 df_temp = df.loc[~df[i].isnull(), [i, 'target']] 238 df_bad = df_temp[df_temp.target == 1] 239 valid = round(df_temp.shape[0] / df.shape[0] * 100, 2) 240
241 bad_rate = [] 242 bin_rate = [] 243 var_name = [] 244 for j in df[i].unique(): 245
246 if pd.isnull(j): 247 df_1 = df[df[i].isnull()] 248 bad_rate.append(sum(df_1.target) / df_1.shape[0]) 249 bin_rate.append(df_1.shape[0] / df.shape[0]) 250 var_name.append('NA') 251 else: 252 df_1 = df[df[i] == j] 253 bad_rate.append(sum(df_1.target) / df_1.shape[0]) 254 bin_rate.append(df_1.shape[0] / df.shape[0]) 255 var_name.append(j) 256 df_2 = pd.DataFrame({'var_name': var_name, 'bin_rate': bin_rate, 'bad_rate': bad_rate}) 257 # 繪圖
258 plt.figure(figsize=(10, 6)) 259 fontsize_1 = 12
260 # 繪制柱狀圖
261 plt.bar(np.arange(1, df_2.shape[0] + 1), df_2.bin_rate, 0.1, color='black', alpha=0.5, label='占比') 262 plt.xticks(np.arange(1, df_2.shape[0] + 1), df_2.var_name) 263 # 繪制折線圖
264 plt.plot(np.arange(1, df_2.shape[0] + 1), df_2.bad_rate, color='green', alpha=0.5, label='壞樣本比率') 265
266 plt.ylabel(i, fontsize=fontsize_1) 267 plt.title('valid rate=' + str(valid) + '%') 268 plt.legend() 269 # 保存圖片
270 file = os.path.join(path_current, 'plot_cat', i + '.png') 271 plt.savefig(file) 272 plt.close(1)