數據挖掘實戰(三):特徵工程-二手車交易價格預測

基本介紹

  1. 重要性
    調參效果有限,特徵工程的好壞決定最終的排名和成績
  2. 目的
    將數據轉換爲能更好地表示潛在問題的特徵

內容介紹(精華)

說明:以下內容中,加粗的部分爲實戰中使用到的方法,有具體的實現代碼,剩餘的相關處理技術後續再補充上。

常見的特徵工程包括:

  1. 異常處理:
    • 通過箱線圖(或3-Sigma)分析刪除異常值
    • BOX-COX轉換(處理有偏分佈)
    • 長尾截斷
  2. 特徵歸一化/標準化:
    • 標準化(轉換爲標準正態分佈)
    • 歸一化(轉換到[0, 1]區間)
    • 針對冪律分佈,可以採用公式:log(1+x1+median)\log \left(\frac{1+x}{1+m e \operatorname{dian}}\right)
  3. 數據分桶:
    • 等頻分桶
    • 等距分桶
    • Best-KS分桶(類似利用基尼指數進行二分類)
    • 卡方分桶
  4. 缺失值處理:
    • 不處理(針對類似XGBoost等樹模型)
    • 刪除(特徵缺失的數據太多,可以考慮刪除)
    • 插值補全,包括均值/中位數/衆數/建模預測/多重插補/壓縮感知補全/矩陣補全等
    • 分箱,缺失值一個箱
  5. 特徵構造:
    • 構造統計量特徵,報告計數,求和,比例,標準差等
    • 時間特徵,包括相對時間和絕對時間,節假日,雙休日等
    • 地理信息,包括分箱,分佈編碼等方法
    • 非線性變換,包括log/平方/根號等
    • 特徵組合,特徵交叉
    • 仁者見仁,智者見智
  6. 特徵篩選
    • 過濾式(filter):先對數據進行特徵選擇,然後再訓練學習器,常見的方法有Relief/方差選擇法/相關係數法/卡方檢驗法/互信息法
    • 包裹式(wrapper):直接把最終將要使用的學習器的性能作爲特徵子集的評價準則,常見方法有LVM(Las Vegas Wrapper)
    • 嵌入式(embedding):結果過濾式和包裹式,學習器訓練過程中自動進行了特徵選擇,常見的有lasso迴歸
  7. 降維
    • 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)

運行結果:

特徵構造

  1. 測試集和訓練集放到一起,對特徵進行處理。
Train_data['train']=1
Test_data['train']=0
data = pd.concat([Train_data, Test_data], ignore_index=True)  
  1. 時間特徵處理
    使用時間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()
  1. 缺失值處理,看一下空數據,有 約15k 個樣本的時間是有問題的,我們可以選擇刪除,也可以選擇放着。但是這裏不建議刪除,因爲刪除缺失數據佔總樣本量過大,7.5%。我們可以先放着,因爲如果我們 XGBoost 之類的決策樹,其本身就能處理缺失值,所以可以不用管;
data['used_time'].isnull().sum()
  1. 地理編碼處理
    從郵編中提取城市信息,相當於加入了先驗知識。提取regionCode的第一位代表不同的城市。
data['city'] = data['regionCode'].apply(lambda x: str(x)[:-3])
data = data
data['city'].head()
  1. 構造統計量特徵
    此處計算某品牌的銷售統計量,也可以計算其他特徵的統計量,這裏要用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()

數據分桶

  1. 數據分桶的原因
    1. 離散後稀疏向量內積乘法運算速度更快,計算結果也方便存儲,容易擴展;
    2. 離散後的特徵對異常值更具魯棒性,如 age>30 爲 1 否則爲 0,對於年齡爲 200 的也不會對模型造成很大的干擾;
    3. LR 屬於廣義線性模型,表達能力有限,經過離散化後,每個變量有單獨的權重,這相當於引入了非線性,能夠提升模型的表達能力,加大擬合;
    4. 離散後特徵可以進行特徵交叉,提升表達能力,由 M+N 個變量編程 M*N 個變量,進一步引入非線形,提升了表達能力;
    5. 特徵離散後模型更穩定,如用戶年齡區間,不會因爲用戶年齡長了一歲就變化
    6. LightGBM 在改進 XGBoost 時就增加了數據分桶,增強了模型的泛化性
  2. 此處使用等距數據分桶
    以 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)

不同模型對數據集的要求不同,所以分開構造

  1. 變換分佈
Train_data['power'].plot.hist()
原數據集不是正態分佈,我們對其取 log,在做歸一化。 ```python data['power'] = np.log(data['power'] + 1) # +1爲了讓取log後的值大於0 data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['price']) - np.min(data['price']))) data['power'].plot.hist() ```
  1. 歸一化
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'])
  1. 類別特徵編碼

名義變量轉換成啞元變量,利用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)

特徵篩選

  1. 過濾式

相關性分析,從相關性較大的特徵之間,去除一個,可以計算出相關係數,或者看相關性矩陣圖。其中,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)
  1. 包裹式

沒有詳細研究,單純記錄下,使用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()
  1. 嵌入式
    大部分情況下都是使用這種方式做特徵篩選! 下一章節補上

經驗總結

  1. 匿名特徵的處理
    (屯着……)
    有些比賽的特徵是匿名特徵,這導致我們並不清楚特徵相互直接的關聯性,這時我們就只有單純基於特徵進行處理,比如裝箱,groupby,agg 等這樣一些操作進行一些特徵統計,此外還可以對特徵進行進一步的 log,exp 等變換,或者對多個特徵進行四則運算(如上面我們算出的使用時長),多項式組合等然後進行篩選。由於特性的匿名性其實限制了很多對於特徵的處理,當然有些時候用 NN 去提取一些特徵也會達到意想不到的良好效果。
  2. 非匿名特徵
    深入分析背後的業務邏輯或者說物理原理,從而才能更好的找到 magic。
  3. 特徵工程和模型結合
    當然特徵工程其實是和模型結合在一起的,這就是爲什麼要爲 LR NN 做分桶和特徵歸一化的原因,而對於特徵的處理效果和特徵重要性等往往要通過模型來驗證。

機器學習基礎知識

問題記錄

Q1: 特徵構造中爲什麼要把訓練集和測試集放在一起?
A1:放到一起處理其中的特徵,比如,時間轉換,地理編碼等特徵構造措施。以及後面涉及到的統計量特徵

Q2:怎麼理解先驗知識?
A2:當你說:這是一幢別墅。你腦子裏面是有“別墅”這個概念的,以及關於別墅的一些屬性,然後你才知道你眼前這個東西叫做“別墅”。前面的“別墅”這個概念就是你對眼前建築的先驗知識。

Q3:地理編碼處理中,提取的部分樣本的城市代碼爲空?【待處理】
A3:有的二手車的regionCode爲3位數,提取出的值爲空,爲空的樣本也可以看作是同一個城市的,但是這裏並沒有處理,當作缺失值的嗎???

Q4:構造統計量特徵中,test的數據是否也需要計算統計量?(很重要)
A4:猜想:需要,訓練好模型後,test數據需要有相應的統計特徵,才能進行預測。

猜想不完全對。

  1. 先看下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']]
  1. 所以便有了如下的結果,test和train數據中相同brand的二手車,有着相同的統計特徵,比如brand_amount、brand_min等。但是爲什麼test數據的brand統計特徵的值,要用train數據統計出的值來填充?
  1. 爲了保證test和train數據的樣本分佈情況相同,更好的適應我們用train數據訓練出的模型。如果我們對test數據的brand特徵再進行一次統計,那統計出的特徵值豈不是會隨着test數據的樣本情況或者樣本數量的改變而改變。那爲什麼不對data數據(即test和train數據)中的brand特徵進行統計?
  2. 那樣的話,我們就需要用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:

  1. xgb模型:無需提前處理缺失值,在迭代的過程中填補缺失值
  2. LR模型:對於線性迴歸模型,對y要求正態分佈,類別變量要轉換爲啞變量

Q8:km 的比較正常,應該是已經做過分桶了,怎麼看出來的?
A8:橫軸爲0-14的整數,應該是將汽車行駛里程數,分成了15個桶。

Q9:長尾分佈【待處理】
A9:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章