基本介紹
- 重要性
調參效果有限,特徵工程的好壞決定最終的排名和成績 - 目的
將數據轉換爲能更好地表示潛在問題的特徵
內容介紹(精華)
說明:以下內容中,加粗的部分爲實戰中使用到的方法,有具體的實現代碼,剩餘的相關處理技術後續再補充上。
常見的特徵工程包括:
- 異常處理:
- 通過箱線圖(或3-Sigma)分析刪除異常值
- BOX-COX轉換(處理有偏分佈)
- 長尾截斷
- 特徵歸一化/標準化:
- 標準化(轉換爲標準正態分佈)
- 歸一化(轉換到[0, 1]區間)
- 針對冪律分佈,可以採用公式:
- 數據分桶:
- 等頻分桶
- 等距分桶
- Best-KS分桶(類似利用基尼指數進行二分類)
- 卡方分桶
- 缺失值處理:
- 不處理(針對類似XGBoost等樹模型)
- 刪除(特徵缺失的數據太多,可以考慮刪除)
- 插值補全,包括均值/中位數/衆數/建模預測/多重插補/壓縮感知補全/矩陣補全等
- 分箱,缺失值一個箱
- 特徵構造:
- 構造統計量特徵,報告計數,求和,比例,標準差等
- 時間特徵,包括相對時間和絕對時間,節假日,雙休日等
- 地理信息,包括分箱,分佈編碼等方法
- 非線性變換,包括log/平方/根號等
- 特徵組合,特徵交叉
- 仁者見仁,智者見智
- 特徵篩選
- 過濾式(filter):先對數據進行特徵選擇,然後再訓練學習器,常見的方法有Relief/方差選擇法/相關係數法/卡方檢驗法/互信息法
- 包裹式(wrapper):直接把最終將要使用的學習器的性能作爲特徵子集的評價準則,常見方法有LVM(Las Vegas Wrapper)
- 嵌入式(embedding):結果過濾式和包裹式,學習器訓練過程中自動進行了特徵選擇,常見的有lasso迴歸
- 降維
- PCA/LDA/ICA
代碼示例
導入數據
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
Train_data = pd.read_csv('used_car_train_20200313.csv', sep=' ')
Test_data = pd.read_csv('used_car_testA_20200313.csv', sep=' ')
print(Train_data.shape)
print(Test_data.shape)
刪除異常值
下面爲利用箱線圖剔除異常值的函數
def outliers_proc(data, col_name, scale=3):
"""
用於清洗異常值,默認用 box_plot(scale=3)進行清洗
:param data: 接收 pandas 數據格式
: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)
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]]
print("Delete number is: {}".format(len(index)))
data_n = data_n.drop(index)
data_n.reset_index(drop=True, inplace=True)
print("Now column number is: {}".format(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])
return data_n
實戰中,刪除power特徵的異常數據。
注意:測試集的數據不能刪除
Train_data = outliers_proc(Train_data, 'power', scale=3)
運行結果:
特徵構造
- 測試集和訓練集放到一起,對特徵進行處理。
Train_data['train']=1
Test_data['train']=0
data = pd.concat([Train_data, Test_data], ignore_index=True)
- 時間特徵處理
使用時間data['creatDate'] - data['regDate']
反應汽車使用時間,一般來說價格與使用時間成反比。不過要注意,數據裏有時間出錯的格式,所以我們需要使用errors='coerce'
將無效值強制轉換爲NaN。
data['used_time'] = (pd.to_datetime(data['creatDate'], format='%Y%m%d', errors='coerce') -
pd.to_datetime(data['regDate'], format='%Y%m%d', errors='coerce')).dt.days
data['used_time'].head()
- 缺失值處理,看一下空數據,有 約15k 個樣本的時間是有問題的,我們可以選擇刪除,也可以選擇放着。但是這裏不建議刪除,因爲刪除缺失數據佔總樣本量過大,7.5%。我們可以先放着,因爲如果我們 XGBoost 之類的決策樹,其本身就能處理缺失值,所以可以不用管;
data['used_time'].isnull().sum()
- 地理編碼處理
從郵編中提取城市信息,相當於加入了先驗知識。提取regionCode
的第一位代表不同的城市。
data['city'] = data['regionCode'].apply(lambda x: str(x)[:-3])
data = data
data['city'].head()
- 構造統計量特徵
此處計算某品牌的銷售統計量,也可以計算其他特徵的統計量,這裏要用train 的數據計算統計量。
Train_gb = Train_data.groupby('brand') # 按找brand這一列分組
all_info = {} # 鍵:某一品牌,值:這一品牌車的統計信息
for kind, kind_data in Train_gb: # kind表示具體的一類,kind_data表示這一類中的所有數據
info = {}
kind_data = kind_data[kind_data['price'] > 0] # 挑選出這一類中price>0的
info['brand_amount'] = len(kind_data) # 統計出這一品牌中車的總數
info['brand_price_max'] = kind_data.price.max() # 這一品牌的車種價格最高是多少
info['brand_price_median'] = kind_data.price.median()
info['brand_price_min'] = kind_data.price.min()
info['brand_price_sum'] = kind_data.price.sum()
info['brand_price_std'] = kind_data.price.std()
info['brand_price_average'] = round(kind_data.price.sum() / (len(kind_data)+1), 2)
all_info[kind] = info
# 字典轉變爲DataFrame;還原索引,從新變爲默認的整型索引;重命名類名
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={'index':'brand'})
brand_fe.head()
運行結果示例:
將上述分組統計的數據,合併到原來的數據集中
data = data.merge(brand_fe, how='left', on='brand')
data.head()
數據分桶
- 數據分桶的原因
- 離散後稀疏向量內積乘法運算速度更快,計算結果也方便存儲,容易擴展;
- 離散後的特徵對異常值更具魯棒性,如 age>30 爲 1 否則爲 0,對於年齡爲 200 的也不會對模型造成很大的干擾;
- LR 屬於廣義線性模型,表達能力有限,經過離散化後,每個變量有單獨的權重,這相當於引入了非線性,能夠提升模型的表達能力,加大擬合;
- 離散後特徵可以進行特徵交叉,提升表達能力,由 M+N 個變量編程 M*N 個變量,進一步引入非線形,提升了表達能力;
- 特徵離散後模型更穩定,如用戶年齡區間,不會因爲用戶年齡長了一歲就變化
- LightGBM 在改進 XGBoost 時就增加了數據分桶,增強了模型的泛化性
- 此處使用等距數據分桶
以 power 爲例,這時候我們的缺失值也進桶了。
# bin爲0-300,間距爲10的等差數列,將發動機功率分成30個類別
bin = [i*10 for i in range(31)]
# 用來把一組數據分割成離散的區間
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
# power_bin爲0-29,共30個類
data[['power_bin', 'power']].head()
最後,刪除不需要的數據,axis=1表示刪除一列。
data = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
data.columns
結果:
目前的數據其實已經可以給樹模型使用了,所以我們導出一下。
data.to_csv('data_for_tree.csv', index=0)
特徵構造(LR,NN)
不同模型對數據集的要求不同,所以分開構造
- 變換分佈
Train_data['power'].plot.hist()
- 歸一化
def max_min(x):
return (x - np.min(x)) / (np.max(x) - np.min(x))
data['kilometer'] = max_min(data['kilometer'])
data['brand_amount'] = max_min(data['brand_amount'])
data['brand_price_average'] = max_min(data['brand_price_average'])
data['brand_price_max'] = max_min(data['brand_price_max'])
data['brand_price_median'] = max_min(data['brand_price_median'])
data['brand_price_min'] = max_min(data['brand_price_min'])
data['brand_price_std'] = max_min(data['brand_price_std'])
data['brand_price_sum'] = max_min(data['brand_price_sum'])
- 類別特徵編碼
名義變量轉換成啞元變量,利用pandas實現one hot encode,可參考網址。
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType', 'gearbox',
'notRepairedDamage', 'power_bin'])
data.columns
最後,存儲數據給LR用
data.to_csv('data_for_lr.csv', index=0)
特徵篩選
- 過濾式
相關性分析,從相關性較大的特徵之間,去除一個,可以計算出相關係數,或者看相關性矩陣圖。其中,method
參數的值可以是:
- pearson:來衡量兩個數據集合是否在一條線上面,即針對線性數據的相關係數計算,針對非線性數據便會有誤差。
- kendall:用於反映分類變量相關性的指標,即針對無序序列的相關係數,非正太分佈的數據
- spearman:非線性的,非正太分析的數據的相關係數
print(data['power'].corr(data['price'], method='spearman'))
print(data['kilometer'].corr(data['price'], method='spearman'))
data_numeric = data[['power', 'kilometer', 'brand_amount', 'brand_price_average',
'brand_price_max', 'brand_price_median']]
correlation = data_numeric.corr()
f, ax = plt.subplots(figsize = (7, 7))
plt.title('Correlation of Numeric Features with Price', y=1, size=16)
sns.heatmap(correlation, square=True, vmax=0.8)
- 包裹式
沒有詳細研究,單純記錄下,使用pip install mlxtend
安裝。
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from sklearn.linear_model import LinearRegression
sfs = SFS(LinearRegression(),
k_features=10,
forward=True,
floating=False,
scoring = 'r2',
cv = 0)
x = data.drop(['price'], axis=1)
x = x.fillna(0)
y = data['price']
sfs.fit(x, y)
sfs.k_feature_names_
# 畫出來,可以看到邊際效益
from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs
import matplotlib.pyplot as plt
fig1 = plot_sfs(sfs.get_metric_dict(), kind='std_dev')
plt.grid()
plt.show()
- 嵌入式
大部分情況下都是使用這種方式做特徵篩選! 下一章節補上
經驗總結
- 匿名特徵的處理
(屯着……)
有些比賽的特徵是匿名特徵,這導致我們並不清楚特徵相互直接的關聯性,這時我們就只有單純基於特徵進行處理,比如裝箱,groupby,agg 等這樣一些操作進行一些特徵統計,此外還可以對特徵進行進一步的 log,exp 等變換,或者對多個特徵進行四則運算(如上面我們算出的使用時長),多項式組合等然後進行篩選。由於特性的匿名性其實限制了很多對於特徵的處理,當然有些時候用 NN 去提取一些特徵也會達到意想不到的良好效果。 - 非匿名特徵
深入分析背後的業務邏輯或者說物理原理,從而才能更好的找到 magic。 - 特徵工程和模型結合
當然特徵工程其實是和模型結合在一起的,這就是爲什麼要爲 LR NN 做分桶和特徵歸一化的原因,而對於特徵的處理效果和特徵重要性等往往要通過模型來驗證。
機器學習基礎知識
問題記錄
Q1: 特徵構造中爲什麼要把訓練集和測試集放在一起?
A1:放到一起處理其中的特徵,比如,時間轉換,地理編碼等特徵構造措施。以及後面涉及到的統計量特徵。
Q2:怎麼理解先驗知識?
A2:當你說:這是一幢別墅。你腦子裏面是有“別墅”這個概念的,以及關於別墅的一些屬性,然後你才知道你眼前這個東西叫做“別墅”。前面的“別墅”這個概念就是你對眼前建築的先驗知識。
Q3:地理編碼處理中,提取的部分樣本的城市代碼爲空?【待處理】
A3:有的二手車的regionCode爲3位數,提取出的值爲空,爲空的樣本也可以看作是同一個城市的,但是這裏並沒有處理,當作缺失值的嗎???
Q4:構造統計量特徵中,test的數據是否也需要計算統計量?(很重要)
A4:猜想:需要,訓練好模型後,test數據需要有相應的統計特徵,才能進行預測。
猜想不完全對。
- 先看下train數據的統計特徵構造完後,數據融合的代碼。需要注意的是,此處是將
brand_fe
的信息加入到data
中,而此時的data
包含test和train數據。(其中brand_fe
是train數據brand
特徵的所有統計量)
data = data.merge(brand_fe, how='left', on='brand')
data[['train', 'brand', 'brand_amount']]
- 所以便有了如下的結果,test和train數據中相同
brand
的二手車,有着相同的統計特徵,比如brand_amount、brand_min
等。但是爲什麼test數據的brand
統計特徵的值,要用train數據統計出的值來填充?
- 爲了保證test和train數據的樣本分佈情況相同,更好的適應我們用train數據訓練出的模型。如果我們對test數據的
brand
特徵再進行一次統計,那統計出的特徵值豈不是會隨着test數據的樣本情況或者樣本數量的改變而改變。那爲什麼不對data
數據(即test和train數據)中的brand
特徵進行統計? - 那樣的話,我們就需要用
data
數據來訓練模型,然後再使用額外的測試數據集來測試模型。如果嘗試將data
數據再次切分成test和train數據,以此進行模型訓練和預測的話,應該也可以,test和train數據的brand
相關統計量相同。但是此時用於train的數據的統計特徵與其自身實際特徵不一致,效果可能不好。
Q5:數據分桶的原因沒有看的太懂?
A5:
- 離散後稀疏向量內積乘法運算速度更快,計算結果也方便存儲,容易擴展
- 離散後的特徵對異常值更具魯棒性,如 age>30 爲 1 否則爲 0,對於年齡爲 200 的也不會對模型造成很大的干擾
- 特徵離散後模型更穩定,如用戶年齡區間,不會因爲用戶年齡長了一歲就變化
- 離散化後可以引入特徵交叉,更好的引入非線性。如果年齡分了M個桶,收入分了N個桶,則可以組合出M*N個特徵。
- 分桶之後相當於引入了一個分段函數,利用這個分段函數表達了一些非線性的意義。這也就是常說的離散化之後可以增加非線性的含義。
參考:連續特徵離散化的意義
Q6:LR,NN,LightGBM ,XGBoost 是啥?
A6:
- LR:LinearRegression 線性迴歸
- NN:近鄰 (Nearest Neighbor)
- XGBoost:一種改進後的樹模型,詳細參考網站
- LightGBM:微軟推出了一個新的boosting框架,想要挑戰xgboost的江湖地位,詳細參考網站
Q7:樹模型和LR NN對數據集的要求是什麼?
A7:
- xgb模型:無需提前處理缺失值,在迭代的過程中填補缺失值
- LR模型:對於線性迴歸模型,對y要求正態分佈,類別變量要轉換爲啞變量
Q8:km 的比較正常,應該是已經做過分桶了,怎麼看出來的?
A8:橫軸爲0-14的整數,應該是將汽車行駛里程數,分成了15個桶。
Q9:長尾分佈【待處理】
A9: