拖延症又犯了,最後一天才寫博客,。,不想給自己找啥理由了000。
特徵工程介紹
我理解的特徵工程是一種更深層的數據分析,爲特定的數據做處理,深挖其中的信息,以便後續模型得到更好的效果。
本文是參考天池二手車預測比賽的教程——特徵工程做的筆記,task3 中提到了很多特徵處理的方法,主要介紹了其中的五種方法:
- 異常數據處理
- 特徵構造
- 數據分桶
- 特徵歸一化
- 特徵篩選
首先,導入需要的包,並載入數據:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from operator import itemgetter
path = './data/'
Train_data = pd.read_csv(path+'used_car_train_20200313.csv', sep=' ')
Test_data = pd.read_csv(path+'used_car_testA_20200313.csv', sep=' ')
接下來就是各種特徵處理的方法了。
異常數據處理
這部分教程中使用的是箱線圖來處理,箱線圖的理解可以參考 箱線圖的理解
總的來講,就是根據四分位線,設置一個上邊界線和下邊界線。上下邊界線分別是由上四分位數和下四分位數加減“箱子”的高度得到的。這裏的箱子高度是指特定倍數的四分位數差,教程裏設置的是三倍四分位數差。
數值超出上下邊界線的數據被劃分爲異常數據。對於異常數據,我們可以選擇截斷或是直接刪除的方式來處理,教程中採用的是後者。
對 power 特徵進行數據處理,由於其中得異常值共九百多個,數量較多,因此,截斷的方式更好。我對代碼做了點更改,改成了截斷的方式。代碼如下(默認已經導入需要的包):
def outliers_proc(data, col_name, scale=3):
"""
用於清洗異常值,默認用 box_plot(scale=3)進行清洗
:param data: 接收 pandas.DataFrame 數據格式
:param col_name: pandas 列名(單列)
:param scale: 尺度
:return: 返回清洗後的數據
"""
def box_plot_outliers(data_ser, box_scale):
"""
利用箱線圖找出異常值
:param data_ser: 接收 pandas.Series 數據格式
:param box_scale: 箱線圖尺度,
:return:
"""
iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25)) # 計算箱子的高度
val_low = data_ser.quantile(0.25) - iqr # 計算下邊界
val_up = data_ser.quantile(0.75) + iqr # 計算上邊界
rule_low = data_ser < val_low # type = <class 'pandas.core.series.Series'>
rule_up = data_ser > val_up # 是一個布爾類型的一維數組
return (rule_low, rule_up), (val_low, val_up)
data_n = data.copy()
data_series = data_n[col_name] # 取出該特徵的所有數據
rule, value = box_plot_outliers(data_series, box_scale=scale) # 找出異常值
index = np.arange(data_series.shape[0])[rule[0] | rule[1]] # 取出所有爲 True 的元素下標,即異常值的元素下標(第一次遇到這種操作,神奇)
print(f"abnormal data number is: {len(index)}")
# data_n = data_n.drop(index) # 這裏不做刪除操作,做截斷操作
# data_n.reset_index(drop=True, inplace=True) # 重置索引,刪除異常值時需要將索引重置,drop=True 表示不保留以前的索引
for i in range(2):
row = np.arange(data_series.shape[0])[rule[i]] # 取出異常值的下標
data_n.loc[row, [col_name]] = value[i]
# print(data_n.loc[row, [col_name]], value[i])
print(f"change {'low' if i == 0 else 'high'} data number is : {len(row)}")
##########---------- 到這裏數據就已經截斷完了,下面是數據的展示 ----------##########
print(f"Now column number is: {data_n.shape[0]}")
index_low = np.arange(data_series.shape[0])[rule[0]]
outliers = data_series.iloc[index_low]
print("Description of data less than the lower bound is:")
print(pd.Series(outliers).describe())
index_up = np.arange(data_series.shape[0])[rule[1]]
outliers = data_series.iloc[index_up]
print("Description of data larger than the upper bound is:")
print(pd.Series(outliers).describe())
fig, ax = plt.subplots(1, 2, figsize=(10, 7))
sns.boxplot(y=data[col_name], data=data, palette="Set1", ax=ax[0])
sns.boxplot(y=data_n[col_name], data=data_n, palette="Set1", ax=ax[1])
plt.show()
return data_n
Train_data = outliers_proc(Train_data, 'power', scale=3)
運行結果如下:
abnormal data number is: 963
change low data number is : 0
change high data number is : 963
Now column number is: 150000
Description of data less than the lower bound is:
count 0.0
mean NaN
std NaN
min NaN
25% NaN
50% NaN
75% NaN
max NaN
Name: power, dtype: float64
Description of data larger than the upper bound is:
count 963.000000
mean 846.836968
std 1929.418081
min 376.000000
25% 400.000000
50% 436.000000
75% 514.000000
max 19312.000000
Name: power, dtype: float64
可以看到,一共 963 個異常數據,而且異常類型全都是超出上邊界線。
特徵構造
教程裏在特徵構造方面,主要是新構造了一些特徵,構造一些認爲對模型有幫助的特徵。
第一個新特徵是使用時間:data[‘creatDate’] - data[‘regDate’],反應汽車使用時間,一般來說價格與使用時間成反比。這裏的 creatDate
是廣告發布時間;regDate
是汽車註冊時間。二者相減,就是汽車的使用時間。由於時間數據有的格式解析有問題,在前天晚上的直播中,作者也解釋了這個問題,因此,會有部分非法數據。一共有 15101 個非法數據,佔總樣本量過大,7.5%,教程裏不建議刪除。
第二個新特徵是城市信息,作者根據郵編信息,提取出郵編內的城市信息。(直播中,作者解釋了爲什麼知道這是德國的郵編格式)
最後就是計算某品牌的銷售統計量,教程作者根據訓練集中的數據信息,進行了一系列的統計計算。
特徵如果使用完了,認爲沒有什麼更多的信息了,就可以將其刪除,降低一下維度。
數據分桶
數據分桶可以簡單地理解成將數據離散化,其優點可以總結成如下三點:
- 稀疏向量,計算速度更快。
- 離散後的特徵更穩定,魯棒性更強,增強了對異常數據的抗干擾性。
- 離散化可以引入非線性,類似於神經網的激活函數。
教程中對 power
特徵進行了分桶操作:
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
print(data[['power_bin', 'power']].head())
運行結果:
power_bin power
0 5.0 60
1 NaN 0
2 16.0 163
3 19.0 193
4 6.0 68
特徵歸一化
特徵歸一化還是對剛纔的 power
特徵做的操作,可以先不對其進行異常值截斷,先查看其特徵分佈:
Train_data['power'].plot.hist()
plt.show()
運行結果:
可以看到,特徵很符合長尾分佈的特點。長尾分佈就是尾巴很長的分佈,這種分佈的尾巴處數據很少,如果數據出現不準的情況,會造成較大影響,畢竟尾巴處可參考的數據很少。
可以先對特徵做 log
操作,然後再做歸一化操作。
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
data['power'] = np.log(data['power'] + 1)
data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))
data['power'].plot.hist()
plt.show()
注意這裏的 log
中 +1
的操作,目的是使計算後的數據值都大於 0。運行結果如下:
特徵篩選
特徵篩選有三種方法:(1)過濾式(2)包裹式(3)嵌入式
詳細介紹請參考:特徵選擇/篩選方法總結
過濾式(Filter)
過濾式的思想就是給每個特徵“打分”,相當於給特徵賦予權值,權值越大,特徵越重要。主要的“打分”方法有三種:
- 信息增益
- 卡方檢驗
- 相關係數
教程中使用的是相關係數進行打分,計算每個特徵與 price
的相關係數。由於負相關也是一種強相關,因此打分時,需要將相關係數取絕對值。
教程中畫了相關係數的熱力圖,可以很直觀地感受到各特徵之間的相關性強弱。
包裹式(Wrapper)
包裹式的主要思想是選擇不同的特徵子集,對不同的特徵子集組合進行打分。將特徵選擇的問題轉換成優化問題,尋找分值較高的特徵子集。使用要使用的分類器作爲一個評價函數,對特徵子集進行打分。
教程裏使用的 mlxtend.feature_selection
下的 SequentialFeatureSelector
方法進行特徵選擇,其中評價函數使用 LinearRegression
。
嵌入式(Embedded)
在模型既定的情況下學習出對提高模型準確性最好的特徵。也就是在確定模型的過程中,挑選出那些對模型的訓練有重要意義的特徵。
這個教程中沒有給出案例,以後會有,留待下次記錄。
最後
整個教程讓人對特徵有了一個更深的認識,通過教程,直接和間接地瞭解到了很多知識,很贊。有點遺憾的是,最近沒有花太多時間在這上面,僅僅只是達到了一個入門的狀態。不過每天都在進步的感覺還是很有成就感的。