天天看點

資料挖掘-二手車價格預測 Task03:特征工程

一、特征工程理論:

常見的特征工程包括:

資料挖掘-二手車價格預測 Task03:特征工程

一、導入資料

import warnings

warnings.filterwarnings('ignore')

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import seaborn as sns

import missingno as msno

import scipy.stats as st

path = 'D:/data/car/'

Train_data = pd.read_csv(path + 'used_car_train.csv', sep=' ')    

# 檔案以空字元分隔資料,若以','分隔資料,則sep=','或省略(源檔案都将資料存到了一列裡面)

Test_data = pd.read_csv(path + 'used_car_testA.csv', sep=' ')

二、删除異常值(僅适用于樹模型的構造)

在task2中,我們僅僅是憑借觀察删除掉了兩個特征和一部分特征的異常值,在本次任務中,我們給出了一個比較通用的方法來處理異常值:利用箱線圖來去除異常值。我将所有的代碼都做了詳細注釋,希望能幫助到大家了解:

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))   # quantile為四分位函數,其中quantile(0.75)是從小到大排四分之三處的資料大小

        val_low = data_ser.quantile(0.25) - iqr                                 # 設定需要丢棄資料的上下門檻值

        val_up = data_ser.quantile(0.75) + iqr

        rule_low = (data_ser < val_low)                                         # 比val_low更小的值要被丢棄(False和True組成的數組)

        rule_up = (data_ser > val_up)                                           # 比val_up更大的值要被丢棄(False和True組成的數組)

        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]]                  # 存儲需要丢棄的過小過大的值的下标index

    print("Delete number is: {}".format(len(index)))

    data_n = data_n.drop(index)                                                 # 丢棄過小和過大的值

    data_n.reset_index(drop=True, inplace=True)                                 # 資料清洗過後,對資料重新設定連續行索引。

    print("Now row number is: {}".format(data_n.shape[0]))                      # 清洗後該特征剩餘的取值個數(原代碼的column應該是筆誤了)

    index_low = np.arange(data_series.shape[0])[rule[0]]                        # 因為取值過小被清洗掉的資料的index

    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]]                         # 因為取值過大被清洗掉的資料的index

    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

其中第二個print()語句中原文寫的是Now column number is,我認為應該是筆者的筆誤,此處應該是行數(即該特征的有效資料個數)。

其輸出如下所示:

Delete number is: 963

Now row number is: 149037

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個,剩餘的有效取值值個數為149037。

可視化的結果如下圖所示:

其中左圖顯示的是删除異常值之前資料的箱線圖,右圖顯示的是删除異常值之後資料的箱線圖。經過對比可見,删除前的資料中位數,第一四等分點和第三四等分點都處于圖像非常靠下的位置,是以存在非常多異常大的取值,對資料的整體分布産生了較大的影響;删除後的資料中位數,第一四等分點和第三四等分點相對全部的取值取值區間已有較大的區分度,整體分布得到了優化。

三、特征構造(所有模型通用)

本部分我們将應用我們的先驗知識構造有助于預測price的新特征。

1.訓練集與測試集的合并

# 訓練集和測試集放在一起,友善構造特征

Train_data['train']=1

Test_data['train']=0

data = pd.concat([Train_data, Test_data], ignore_index=True)

注意:不熟悉pandas的同學可能會疑問這個ignore_index的作用,下面我們通過控制變量看一下他的作用,當該值取False時,訓練集與測試集合并後的序号如下圖所示:

當該值取True時,合并後的序号如下圖所示:

經過對比可見當該值取False時,合并後的表格将序号依然使用的原表格中的序号,而當該值取True時,序号更新為連續的序号。

2.構造特征之使用時間

因為我們知道其售賣時間和出廠時間,是以兩者相減可以計算出其使用時間。之是以要計算汽車使用時間,是因為一般來說價格與使用時間成反比。

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

3.構造特征之所屬城市

從郵編中可以提取城市特征(4位郵編的第一位,不足4位的郵編認為是異常的)。

data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])

data = data

4.構造特征之品牌

Train_gb = Train_data.groupby("brand")                                # 按照brand來聚合,brand=0的聚合在一起,brand=1的聚合在一起(總共聚合了40類)

all_info = {}

for kind, kind_data in Train_gb:                                      # kind分别從0取到39,kind_data為0-39這40類的具體資料

    info = {}

    kind_data = kind_data[kind_data['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                                              # all_info存儲了這40個聚合類的統計學特征

brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})

data = data.merge(brand_fe, how='left', on='brand')

如上面代碼所示,我們根據brand的值聚合為多種類(本題為40類),也就是一共40個品牌,我們分别計算每個品牌的各種數學統計量,并儲存在字典all_info中,為了将新構造的特征合并到原來的data中,我們将其轉化為和data相同格式的類型,然後将其合并。如下圖所示:新的特征就被加入到了原來的data中了。

5.構造特征之資料分桶

本構造以power特征為例,資料分桶實際上就是資料離散化,其優點如下:

①.離散後稀疏向量内積乘法運算速度更快,計算結果也友善存儲,容易擴充;

②. 離散後的特征對異常值更具魯棒性。如 age>30 為 1 否則為 0,是以異常年齡100也不會對模型造成很大的幹擾;

③. LR (邏輯斯蒂回歸)屬于廣義線性模型,表達能力有限,經過離散化後,每個變量有單獨的權重,這相當于引入了非線性,能夠提升模型的表達能力,加大拟合;

④. 離散後特征可以進行特征交叉,提升表達能力,由 M+N 個變量程式設計 M*N 個變量,進一步引入非線形,提升了表達能力;

⑤. 特征離散後模型更穩定。

其具體實作如下所示:

bin = [i*10 for i in range(31)]                                        # bin 是一個整數數列,标明了我們的cut标準

data['power_bin'] = pd.cut(data['power'], bin, labels=False)           # 按照bin所給的數組切割,左開右閉,是以power等于0或者大于300的值都被認為是nan

以上是将power在(0,300]區間内的值都根據bin的值做一定切分處理(除以10)。其結果如下所示:

        power_bin  power

0             5.0     60

1             NaN      0

2            16.0    163

3            19.0    193

4             6.0     68

...           ...    ...

199995       11.0    116

199996        7.0     75

199997       22.0    224

199998        NaN    334

199999        6.0     68

[200000 rows x 2 columns]

細心的同學可能發現了,power=60處理後是變成了5.0,而不是6.0。這是因為cut()函數預設的切割是左開右閉的。之是以這樣切割是有好處的,因為power=0也是異常值,這樣可以将0處理為Nan。

6.去除無用的特征,導出到檔案

我們前面利用已有特征人為構造了一部分新的特征,是以原來特征已經變成了無用特征,我們需要将其删除,然後導出到檔案作為樹模型的資料使用。

data = data.drop(['creatDate', 'regDate', 'regionCode','seller','offerType'], axis=1)

data.to_csv(path+'data_for_tree.csv',index = 0)

其中seller和offerType為task2中我們尋找到的出現嚴重傾斜的特征,我們一并删除。

四、特征的異常值處理之二+歸一化處理(供LR和NN等模型使用)

1.異常值處理

因為不同模型使用的資料特征是不同的,是以對于樹模型和LR等模型的資料需要分開構造。我們看下之前用箱線圖去除過異常值的特征power的資料分布。

data['power'].plot.hist()

plt.show()

其分布如下圖所示:

可以看到我們雖然用箱型圖去除掉了部分過大過小的異常值,但是仍然有許多過大的異常值,是以我們如果用歸回模型的話,不應該使用箱線圖去除異常值,而應該使用如下所示的長尾分布截斷。

rule = Train_data['power'] > 375

index = np.arange(Train_data.shape[0])[rule]

Train_data_n = Train_data.drop(index)

Train_data_n['power'].plot.hist()

其可視化結果如下所示:

從上圖可見,power特征的數值已經十分接近正态分布了。

2.歸一化處理

因為data中的power特征有過多的異常大值,是以我們之間歸一化勢必會出現問題,是以我們将其取log縮小其差距,然後在歸一化。

min_max_scaler = preprocessing.MinMaxScaler()

data['power'] = np.log(data['power'] +1)                                # +1是為了結果都是正數

data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))

data['power'].plot.hist()

plt.show()

其中取對數的時候+1是為了結果均為正值,否則我們将無法在可視化圖形中表示出來。

其他差異不大的特征不用取對數,直接歸一化即可,不再贅述。

data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /

                        (np.max(data['kilometer']) - np.min(data['kilometer'])))

def max_min(x):

    return (x - np.min(x)) / (np.max(x) - np.min(x))

data['brand_amount'] = ((data['brand_amount'] - np.min(data['brand_amount'])) / 

                        (np.max(data['brand_amount']) - np.min(data['brand_amount'])))

data['brand_price_average'] = ((data['brand_price_average'] - np.min(data['brand_price_average'])) / 

                               (np.max(data['brand_price_average']) - np.min(data['brand_price_average'])))

data['brand_price_max'] = ((data['brand_price_max'] - np.min(data['brand_price_max'])) / 

                           (np.max(data['brand_price_max']) - np.min(data['brand_price_max'])))

data['brand_price_median'] = ((data['brand_price_median'] - np.min(data['brand_price_median'])) /

                              (np.max(data['brand_price_median']) - np.min(data['brand_price_median'])))

data['brand_price_min'] = ((data['brand_price_min'] - np.min(data['brand_price_min'])) / 

                           (np.max(data['brand_price_min']) - np.min(data['brand_price_min'])))

data['brand_price_std'] = ((data['brand_price_std'] - np.min(data['brand_price_std'])) / 

                           (np.max(data['brand_price_std']) - np.min(data['brand_price_std'])))

data['brand_price_sum'] = ((data['brand_price_sum'] - np.min(data['brand_price_sum'])) / 

                           (np.max(data['brand_price_sum']) - np.min(data['brand_price_sum'])))

最後看一下特征的形狀,确認無誤後導出到檔案中供LR模型使用。

print(data.shape)

data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',

                                     'gearbox', 'notRepairedDamage', 'power_bin'])

print(data.shape)

(199037, 37)

(199037, 368)

get_dummies()函數是将編碼方式轉換為One-Hot方式,是以特征數量增加了許多是正常的結果。是以我們将其儲存起來備用。