Datawhale 零基礎入門數據挖掘-Task3 特徵工程
三、 特徵工程目標
Tip:此部分爲零基礎入門數據挖掘的 Task3 特徵工程 部分,帶你來了解各種特徵工程以及分析方法,歡迎大家後續多多交流。
賽題:零基礎入門數據挖掘 - 二手車交易價格預測
地址:https://tianchi.aliyun.com/competition/entrance/231784/introduction?spm=5176.12281957.1004.1.38b02448ausjSX
3.1 特徵工程目標
-
對於特徵進行進一步分析,並對於數據進行處理
-
完成對於特徵工程的分析,並對於數據進行一些圖表或者文字總結並打卡。
3.2 內容介紹
常見的特徵工程包括:
- 異常處理:
- 通過箱線圖(或 3-Sigma)分析刪除異常值;
- BOX-COX 轉換(處理有偏分佈);
- 長尾截斷;
- 特徵歸一化/標準化:
- 標準化(轉換爲標準正態分佈);
- 歸一化(抓換到 [0,1] 區間);
- 針對冪律分佈,可以採用公式:
- 數據分桶:
- 等頻分桶;
- 等距分桶;
- Best-KS 分桶(類似利用基尼指數進行二分類);
- 卡方分桶;
- 缺失值處理:
- 不處理(針對類似 XGBoost 等樹模型);
- 刪除(缺失數據太多);
- 插值補全,包括均值/中位數/衆數/建模預測/多重插補/壓縮感知補全/矩陣補全等;
- 分箱,缺失值一個箱;
- 特徵構造:
- 構造統計量特徵,報告計數、求和、比例、標準差等;
- 時間特徵,包括相對時間和絕對時間,節假日,雙休日等;
- 地理信息,包括分箱,分佈編碼等方法;
- 非線性變換,包括 log/ 平方/ 根號等;
- 特徵組合,特徵交叉;
- 仁者見仁,智者見智。
- 特徵篩選
- 過濾式(filter):先對數據進行特徵選擇,然後在訓練學習器,常見的方法有 Relief/方差選擇發/相關係數法/卡方檢驗法/互信息法;
- 包裹式(wrapper):直接把最終將要使用的學習器的性能作爲特徵子集的評價準則,常見方法有 LVM(Las Vegas Wrapper) ;
- 嵌入式(embedding):結合過濾式和包裹式,學習器訓練過程中自動進行了特徵選擇,常見的有 lasso 迴歸;
- 降維
- PCA/ LDA/ ICA;
- 特徵選擇也是一種降維。
3.3 代碼示例
3.3.0 導入數據
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from operator import itemgetter
%matplotlib inline
train = pd.read_csv('train_fill.csv')
test = pd.read_csv('test_fill.csv')
print(train.shape)
print(test.shape)
C:\Users\94890\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py:3058: DtypeWarning: Columns (3) have mixed types. Specify dtype option on import or set low_memory=False.
interactivity=interactivity, compiler=compiler, result=result)
(150000, 29)
(50000, 28)
train.head()
SaleID | name | regDate | model | brand | bodyType | fuelType | gearbox | power | kilometer | ... | v_5 | v_6 | v_7 | v_8 | v_9 | v_10 | v_11 | v_12 | v_13 | v_14 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 736 | 20040402 | 30 | 6 | 1.0 | 0.0 | 0.0 | 60 | 12.5 | ... | 0.235676 | 0.101988 | 0.129549 | 0.022816 | 0.097462 | -2.881803 | 2.804097 | -2.420821 | 0.795292 | 0.914762 |
1 | 1 | 2262 | 20030301 | 40 | 1 | 2.0 | 0.0 | 0.0 | 0 | 15.0 | ... | 0.264777 | 0.121004 | 0.135731 | 0.026597 | 0.020582 | -4.900482 | 2.096338 | -1.030483 | -1.722674 | 0.245522 |
2 | 2 | 14874 | 20040403 | 115 | 15 | 1.0 | 0.0 | 0.0 | 163 | 12.5 | ... | 0.251410 | 0.114912 | 0.165147 | 0.062173 | 0.027075 | -4.846749 | 1.803559 | 1.565330 | -0.832687 | -0.229963 |
3 | 3 | 71865 | 19960908 | 109 | 10 | 0.0 | 0.0 | 1.0 | 193 | 15.0 | ... | 0.274293 | 0.110300 | 0.121964 | 0.033395 | 0.000000 | -4.509599 | 1.285940 | -0.501868 | -2.438353 | -0.478699 |
4 | 4 | 111080 | 20120103 | 110 | 5 | 1.0 | 0.0 | 0.0 | 68 | 5.0 | ... | 0.228036 | 0.073205 | 0.091880 | 0.078819 | 0.121534 | -1.896240 | 0.910783 | 0.931110 | 2.834518 | 1.923482 |
5 rows × 29 columns
train.columns
Index(['SaleID', 'name', 'regDate', 'model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'power', 'kilometer', 'notRepairedDamage', 'regionCode',
'creatDate', 'price', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6',
'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14'],
dtype='object')
test.columns
Index(['SaleID', 'name', 'regDate', 'model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'power', 'kilometer', 'notRepairedDamage', 'regionCode',
'creatDate', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7',
'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14'],
dtype='object')
3.3.1 刪除異常值
# 這裏我包裝了一個異常值處理的代碼,可以隨便調用。
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)) ## R1=Q3-Q1四分位極差 優點:抗擾、穩健
## 上下截點 一般scale=1.5
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)
print('Original index number is %d' % data.shape[0])
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 index 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 爲例。
# 這裏刪不刪同學可以自行判斷
# 但是要注意 test 的數據不能刪 = = 不能掩耳盜鈴是不是
train = outliers_proc(train, 'power', scale=3)
Original index number is 150000
Delete number is: 963
Now index 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
3.3.2 特徵構造
# 訓練集和測試集放在一起,方便構造特徵
train['train']=1
test['train']=0
data = pd.concat([train, test], ignore_index=True, sort=False)
# 使用時間:data['creatDate'] - data['regDate'],反應汽車使用時間,一般來說價格與使用時間成反比
# 不過要注意,數據裏有時間出錯的格式,所以我們需要 errors='coerce',
#errors='coerce'將強制超出NaT的日期,返回NaT。
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
# 看一下空數據,有 15k 個樣本的時間是有問題的,我們可以選擇刪除,也可以選擇放着。
# 但是這裏不建議刪除,因爲刪除缺失數據佔總樣本量過大,7.5%
# 我們可以先放着,因爲如果我們 XGBoost 之類的決策樹,其本身就能處理缺失值,所以可以不用管;
data['used_time'].isnull().sum()
15072
# 從郵編中提取城市信息,因爲是德國的數據,所以參考德國的郵編,相當於加入了先驗知識
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
data['city'].head()
0 1
1 4
2 2
3
4 6
Name: city, dtype: object
# 計算某品牌的銷售統計量,同學們還可以計算其他特徵的統計量
# 這裏要以 train 的數據計算統計量
train_gb = train.groupby("brand")
all_info = {}
for kind, kind_data in train_gb:
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
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
data = data.merge(brand_fe, how='left', on='brand')
data.head()
SaleID | name | regDate | model | brand | bodyType | fuelType | gearbox | power | kilometer | ... | train | used_time | city | brand_amount | brand_price_max | brand_price_median | brand_price_min | brand_price_sum | brand_price_std | brand_price_average | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 736 | 20040402 | 30 | 6 | 1.0 | 0.0 | 0.0 | 60 | 12.5 | ... | 1 | 4385.0 | 1 | 10193.0 | 35990.0 | 1800.0 | 13.0 | 36457518.0 | 4562.233331 | 3576.37 |
1 | 1 | 2262 | 20030301 | 40 | 1 | 2.0 | 0.0 | 0.0 | 0 | 15.0 | ... | 1 | 4757.0 | 4 | 13656.0 | 84000.0 | 6399.0 | 15.0 | 124044603.0 | 8988.865406 | 9082.86 |
2 | 2 | 14874 | 20040403 | 115 | 15 | 1.0 | 0.0 | 0.0 | 163 | 12.5 | ... | 1 | 4382.0 | 2 | 1458.0 | 45000.0 | 8500.0 | 100.0 | 14373814.0 | 5425.058140 | 9851.83 |
3 | 3 | 71865 | 19960908 | 109 | 10 | 0.0 | 0.0 | 1.0 | 193 | 15.0 | ... | 1 | 7125.0 | 13994.0 | 92900.0 | 5200.0 | 15.0 | 113034210.0 | 8244.695287 | 8076.76 | |
4 | 4 | 111080 | 20120103 | 110 | 5 | 1.0 | 0.0 | 0.0 | 68 | 5.0 | ... | 1 | 1531.0 | 6 | 4662.0 | 31500.0 | 2300.0 | 20.0 | 15414322.0 | 3344.689763 | 3305.67 |
5 rows × 39 columns
# 數據分桶 以 power 爲例
# 這時候我們的缺失值也進桶了,
# 爲什麼要做數據分桶呢,原因有很多,= =
# 1. 離散後稀疏向量內積乘法運算速度更快,計算結果也方便存儲,容易擴展;
# 2. 離散後的特徵對異常值更具魯棒性,如 age>30 爲 1 否則爲 0,對於年齡爲 200 的也不會對模型造成很大的干擾;
# 3. LR 屬於廣義線性模型,表達能力有限,經過離散化後,每個變量有單獨的權重,這相當於引入了非線性,能夠提升模型的表達能力,加大擬合;
# 4. 離散後特徵可以進行特徵交叉,提升表達能力,由 M+N 個變量編程 M*N 個變量,進一步引入非線形,提升了表達能力;
# 5. 特徵離散後模型更穩定,如用戶年齡區間,不會因爲用戶年齡長了一歲就變化
# 當然還有很多原因,LightGBM 在改進 XGBoost 時就增加了數據分桶,增強了模型的泛化性
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
data[['power_bin', 'power']].head(30) # 將power劃分爲31個種類
power_bin | power | |
---|---|---|
0 | 5.0 | 60 |
1 | NaN | 0 |
2 | 16.0 | 163 |
3 | 19.0 | 193 |
4 | 6.0 | 68 |
5 | 10.0 | 109 |
6 | 14.0 | 150 |
7 | 10.0 | 101 |
8 | 17.0 | 179 |
9 | 8.0 | 88 |
10 | 10.0 | 101 |
11 | 7.0 | 75 |
12 | 5.0 | 58 |
13 | 14.0 | 150 |
14 | NaN | 0 |
15 | 12.0 | 128 |
16 | 23.0 | 239 |
17 | 14.0 | 145 |
18 | 4.0 | 45 |
19 | 10.0 | 105 |
20 | 5.0 | 54 |
21 | 11.0 | 116 |
22 | 7.0 | 75 |
23 | 10.0 | 105 |
24 | 5.0 | 54 |
25 | 10.0 | 101 |
26 | 14.0 | 150 |
27 | 14.0 | 143 |
28 | 21.0 | 218 |
29 | 10.0 | 105 |
pd.cut https://blog.csdn.net/missyougoon/article/details/83986511
# 利用好了,就可以刪掉原始數據了
data = data.drop(['creatDate', 'regDate', 'regionCode', 'SaleID'], axis=1)
print(data.shape)
data.columns
(199037, 36)
Index(['name', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'power',
'kilometer', 'notRepairedDamage', 'price', 'v_0', 'v_1', 'v_2', 'v_3',
'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12',
'v_13', 'v_14', 'train', 'used_time', 'city', 'brand_amount',
'brand_price_max', 'brand_price_median', 'brand_price_min',
'brand_price_sum', 'brand_price_std', 'brand_price_average',
'power_bin'],
dtype='object')
# 目前的數據其實已經可以給樹模型使用了,所以我們導出一下
data.to_csv('data_for_tree.csv', index=0)
# 我們可以再構造一份特徵給 LR NN 之類的模型用
# 之所以分開構造是因爲,不同模型對數據集的要求不同
# 我們看下數據分佈:
#data['power'].plot.hist(bins=1)
plt.hist(data['power'], orientation= 'vertical', histtype='bar')
(array([1.99022e+05, 5.00000e+00, 1.00000e+00, 3.00000e+00, 0.00000e+00,
2.00000e+00, 1.00000e+00, 2.00000e+00, 0.00000e+00, 1.00000e+00]),
array([ 0., 2000., 4000., 6000., 8000., 10000., 12000., 14000.,
16000., 18000., 20000.]),
<a list of 10 Patch objects>)
# 我們剛剛已經對 train 進行異常值處理了,但是現在還有這麼奇怪的分佈是因爲 test 中的 power 異常值,
# 所以我們其實剛剛 train 中的 power 異常值不刪爲好,可以用長尾分佈截斷來代替
train['power'].plot.hist()
<matplotlib.axes._subplots.AxesSubplot at 0x18a78926b48>
import scipy.stats as st
fig = plt.figure(3)
ax1 = fig.add_subplot(211)
ax1.set_title('Original Data')
sns.distplot(data['power'], ax=ax1)
ax2 = fig.add_subplot(212)
ax2.set_title('Log Normal')
sns.distplot(data['power'], kde=False, fit=st.lognorm, ax=ax2)
## 可以看出power確實是長尾分佈
<matplotlib.axes._subplots.AxesSubplot at 0x18a57b86c08>
# 我們對其取 log,在做歸一化,一般會服從長尾分佈,可以取 log 再做歸一化,減少精度損失
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()# 將數據映射到[0, 1]
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()
<matplotlib.axes._subplots.AxesSubplot at 0x18a56499888>
長尾分佈:https://www.cnblogs.com/huangshiyu13/p/6217180.html
# km 的比較正常,應該是已經做過分桶了
data['kilometer'].plot.hist()
# 所以我們可以直接做歸一化
data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /
(np.max(data['kilometer']) - np.min(data['kilometer'])))
data['kilometer'].plot.hist()
<matplotlib.axes._subplots.AxesSubplot at 0x18a57c61608>
# 除此之外 還有我們剛剛構造的統計量特徵:
# 'brand_amount', 'brand_price_average', 'brand_price_max',
# 'brand_price_median', 'brand_price_min', 'brand_price_std',
# 'brand_price_sum'
# 這裏不再一一舉例分析了,直接做變換,
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'])))
def fill(x):
if not x:
x = 'MISSING'
return str(x)
data['city'] = data['city'].map(fill)
data['used_time'] = data['used_time'].fillna(data['used_time'].mean())
for i in ['model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'notRepairedDamage', 'city']:
data[i] = data[i].map(str)
data = data.fillna(0)
data.head()
name | model | brand | bodyType | fuelType | gearbox | power | kilometer | notRepairedDamage | price | ... | used_time | city | brand_amount | brand_price_max | brand_price_median | brand_price_min | brand_price_sum | brand_price_std | brand_price_average | power_bin | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 736 | 30.0 | 6 | 1.0 | 0.0 | 0.0 | 0.415091 | 0.827586 | 0.0 | 1850.0 | ... | 4385.0 | 1 | 0.324125 | 0.340786 | 0.032075 | 0.002064 | 0.209684 | 0.207660 | 0.081655 | 5.0 |
1 | 2262 | 40.0 | 1 | 2.0 | 0.0 | 0.0 | 0.000000 | 1.000000 | MISSING | 3600.0 | ... | 4757.0 | 4 | 0.434341 | 0.835230 | 0.205623 | 0.004128 | 0.713985 | 0.437002 | 0.257305 | 0.0 |
2 | 14874 | 115.0 | 15 | 1.0 | 0.0 | 0.0 | 0.514954 | 0.827586 | 0.0 | 6222.0 | ... | 4382.0 | 2 | 0.046117 | 0.433578 | 0.284906 | 0.091847 | 0.082533 | 0.252362 | 0.281834 | 16.0 |
3 | 71865 | 109.0 | 10 | 0.0 | 0.0 | 1.0 | 0.531917 | 1.000000 | 0.0 | 2400.0 | ... | 7125.0 | MISSING | 0.445099 | 0.926889 | 0.160377 | 0.004128 | 0.650591 | 0.398447 | 0.225212 | 19.0 |
4 | 111080 | 110.0 | 5 | 1.0 | 0.0 | 0.0 | 0.427535 | 0.310345 | 0.0 | 5200.0 | ... | 1531.0 | 6 | 0.148090 | 0.294545 | 0.050943 | 0.009288 | 0.088524 | 0.144579 | 0.073020 | 6.0 |
5 rows × 36 columns
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 199037 entries, 0 to 199036
Data columns (total 36 columns):
name 199037 non-null int64
model 199037 non-null object
brand 199037 non-null object
bodyType 199037 non-null object
fuelType 199037 non-null object
gearbox 199037 non-null object
power 199037 non-null float64
kilometer 199037 non-null float64
notRepairedDamage 199037 non-null object
price 199037 non-null float64
v_0 199037 non-null float64
v_1 199037 non-null float64
v_2 199037 non-null float64
v_3 199037 non-null float64
v_4 199037 non-null float64
v_5 199037 non-null float64
v_6 199037 non-null float64
v_7 199037 non-null float64
v_8 199037 non-null float64
v_9 199037 non-null float64
v_10 199037 non-null float64
v_11 199037 non-null float64
v_12 199037 non-null float64
v_13 199037 non-null float64
v_14 199037 non-null float64
train 199037 non-null int64
used_time 199037 non-null float64
city 199037 non-null object
brand_amount 199037 non-null float64
brand_price_max 199037 non-null float64
brand_price_median 199037 non-null float64
brand_price_min 199037 non-null float64
brand_price_sum 199037 non-null float64
brand_price_std 199037 non-null float64
brand_price_average 199037 non-null float64
power_bin 199037 non-null float64
dtypes: float64(27), int64(2), object(7)
memory usage: 56.2+ MB
# 對類別特徵進行 OneEncoder
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'notRepairedDamage', 'power_bin', 'city'])
pd.get_dummies https://blog.csdn.net/maymay_/article/details/80198468
print(data.shape)
data.columns
(199037, 383)
Index(['name', 'power', 'kilometer', 'price', 'v_0', 'v_1', 'v_2', 'v_3',
'v_4', 'v_5',
...
'power_bin_29.0', 'city_1', 'city_2', 'city_3', 'city_4', 'city_5',
'city_6', 'city_7', 'city_8', 'city_MISSING'],
dtype='object', length=383)
# 這份數據可以給 LR 用
data.to_csv('data_for_lr.csv', index=0)
3.3.3 特徵篩選
1) 過濾式
# 相關性分析
print(data['power'].corr(data['price'], method='spearman'))
print(data['kilometer'].corr(data['price'], method='spearman'))
print(data['brand_amount'].corr(data['price'], method='spearman'))
print(data['brand_price_average'].corr(data['price'], method='spearman'))
print(data['brand_price_max'].corr(data['price'], method='spearman'))
print(data['brand_price_median'].corr(data['price'], method='spearman'))
0.31967741112277953
-0.2285011063556878
0.03423284784253153
0.21417438474049755
0.14324994566567756
0.216221606279449
# 當然也可以直接看圖
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)
<matplotlib.axes._subplots.AxesSubplot at 0x18a64271088>
2) 包裹式
!pip install mlxtend
Looking in indexes: http://mirrors.aliyun.com/pypi/simple
Requirement already satisfied: mlxtend in c:\users\94890\anaconda3\lib\site-packages (0.17.2)
Requirement already satisfied: joblib>=0.13.2 in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (0.14.1)
Requirement already satisfied: scikit-learn>=0.20.3 in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (0.21.3)
Requirement already satisfied: matplotlib>=3.0.0 in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (3.2.1)
Requirement already satisfied: setuptools in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (41.4.0)
Requirement already satisfied: pandas>=0.24.2 in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (0.25.3)
Requirement already satisfied: scipy>=1.2.1 in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (1.4.1)
Requirement already satisfied: numpy>=1.16.2 in c:\users\94890\anaconda3\lib\site-packages (from mlxtend) (1.16.5)
Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in c:\users\94890\anaconda3\lib\site-packages (from matplotlib>=3.0.0->mlxtend) (2.4.2)
Requirement already satisfied: cycler>=0.10 in c:\users\94890\anaconda3\lib\site-packages (from matplotlib>=3.0.0->mlxtend) (0.10.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\94890\anaconda3\lib\site-packages (from matplotlib>=3.0.0->mlxtend) (1.1.0)
Requirement already satisfied: python-dateutil>=2.1 in c:\users\94890\anaconda3\lib\site-packages (from matplotlib>=3.0.0->mlxtend) (2.8.0)
Requirement already satisfied: pytz>=2017.2 in c:\users\94890\anaconda3\lib\site-packages (from pandas>=0.24.2->mlxtend) (2019.3)
Requirement already satisfied: six in c:\users\94890\anaconda3\lib\site-packages (from cycler>=0.10->matplotlib>=3.0.0->mlxtend) (1.12.0)
# k_feature 太大會很難跑,沒服務器,所以提前 interrupt 了
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from sklearn.linear_model import LinearRegression
x = data[data['train']==1]
y = x['price']
x = x.drop(['price', 'train'], axis=1)
sfs = SFS(LinearRegression(),
k_features=10,
forward=True,
floating=False,
scoring = 'r2',
cv = 0)
sfs.fit(x, y)
sfs.k_feature_names_
('kilometer',
'v_3',
'v_4',
'v_13',
'v_14',
'used_time',
'brand_price_average',
'model_167.0',
'gearbox_1.0',
'power_bin_24.0')
# 畫出來,可以看到邊際效益
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()
C:\Users\94890\Anaconda3\lib\site-packages\numpy\core\_methods.py:140: RuntimeWarning: Degrees of freedom <= 0 for slice
keepdims=keepdims)
C:\Users\94890\Anaconda3\lib\site-packages\numpy\core\_methods.py:132: RuntimeWarning: invalid value encountered in double_scalars
ret = ret.dtype.type(ret / rcount)
3) 嵌入式
# 下一章介紹,Lasso 迴歸和決策樹可以完成嵌入式特徵選擇
# 大部分情況下都是用嵌入式做特徵篩選
3.4 經驗總結
特徵工程是比賽中最至關重要的的一塊,特別的傳統的比賽,大家的模型可能都差不多,調參帶來的效果增幅是非常有限的,但特徵工程的好壞往往會決定了最終的排名和成績。
特徵工程的主要目的還是在於將數據轉換爲能更好地表示潛在問題的特徵,從而提高機器學習的性能。比如,異常值處理是爲了去除噪聲,填補缺失值可以加入先驗知識等。
特徵構造也屬於特徵工程的一部分,其目的是爲了增強數據的表達。
有些比賽的特徵是匿名特徵,這導致我們並不清楚特徵相互直接的關聯性,這時我們就只有單純基於特徵進行處理,比如裝箱,groupby,agg 等這樣一些操作進行一些特徵統計,此外還可以對特徵進行進一步的 log,exp 等變換,或者對多個特徵進行四則運算(如上面我們算出的使用時長),多項式組合等然後進行篩選。由於特性的匿名性其實限制了很多對於特徵的處理,當然有些時候用 NN 去提取一些特徵也會達到意想不到的良好效果。
對於知道特徵含義(非匿名)的特徵工程,特別是在工業類型比賽中,會基於信號處理,頻域提取,丰度,偏度等構建更爲有實際意義的特徵,這就是結合背景的特徵構建,在推薦系統中也是這樣的,各種類型點擊率統計,各時段統計,加用戶屬性的統計等等,這樣一種特徵構建往往要深入分析背後的業務邏輯或者說物理原理,從而才能更好的找到 magic。
當然特徵工程其實是和模型結合在一起的,這就是爲什麼要爲 LR NN 做分桶和特徵歸一化的原因,而對於特徵的處理效果和特徵重要性等往往要通過模型來驗證。
總的來說,特徵工程是一個入門簡單,但想精通非常難的一件事。
Task 3-特徵工程 END.
— By: 阿澤
PS:復旦大學計算機研究生
知乎:阿澤 https://www.zhihu.com/people/is-aze(主要面向初學者的知識整理)
關於Datawhale:
Datawhale是一個專注於數據科學與AI領域的開源組織,彙集了衆多領域院校和知名企業的優秀學習者,聚合了一羣有開源精神和探索精神的團隊成員。Datawhale 以“for the learner,和學習者一起成長”爲願景,鼓勵真實地展現自我、開放包容、互信互助、敢於試錯和勇於擔當。同時 Datawhale 用開源的理念去探索開源內容、開源學習和開源方案,賦能人才培養,助力人才成長,建立起人與人,人與知識,人與企業和人與未來的聯結。
本次數據挖掘路徑學習,專題知識將在天池分享,詳情可關注Datawhale: