本日重點:了解特徵工程為何重要、認識特徵工程中常見的方法、如何在 R 中怎麼處理特徵工程

##設定環境
#setwd(dir) #設定working directory的存放位置
# MAC : setwd("/Users/rladiestaipei/R_DragonBall/") 
# Windows : setwd("C://Users/rladiestaipei/Desktop/R_DragonBall/")  

#安裝套件(僅需執行一次)
#install.packages(c("tidyverse","purrr","tidyr","plyr"), dependencies = TRUE)

#load packages
library(tidyverse)
library(purrr)
library(tidyr)
library(plyr)
options(dplyr.print_max=1e9)

Part 1 : 特徵工程為何重要

在說明特徵工程前,不知道大家有沒有注意到,本次活動提供的資料集共有三個分別是
1. data_description.txt : 欄位敘述
2. train.csv : 訓練資料(用來做模型構建)
3. test.csv : 測試資料(用來檢測模型的表現)

不曉得大家是否已經了解什麼是訓練資料、測試資料了?
如果你還沒有什麼概念,那可以先從「人類是怎麼學習辨識貓跟狗的? 」這個問題來下手
Dog and Cat
在你小的時候,你的家人想要教你如何分辨狗跟貓,可能會給你看很多狗跟貓的圖片,並且告訴你哪些是狗哪些是貓,讓你慢慢去認識狗跟貓長什麼樣子,而在你學會辨認狗貓之前所看的這些圖片,就是訓練資料。而測試資料就像是你的家人為了測試你瞭不瞭解,通常會給你一些你沒看過的狗貓圖片,考考你是否學會辨識了。
講這麼多那到底訓練資料、測試資料跟特徵工程有什麼關係呢?那是因為要做出正確的辨識,我們需要要收集一些有用的特徵來輔組我們訓練,以剛剛辨認狗貓的例子,我們可能會從「是不是尖耳朵、嘴巴長短、有沒有鬍子、體型大小、毛色」等等來做辨識,這些就稱為特徵。如果沒有好的特徵,我們就沒辦法有好的訓練資料,可能就無法做出正確的預測。
特徵工程就是我們在收集到很多特徵之後,想辦法再從中找到更多更好的方式,來描述我們想要預測的標的。當我們有更好的方式去描述資料時,通常也能提升模型的表現。從剛剛辨認狗貓的例子,我們可能可以從體型、毛色、嘴巴長短等綜合出一些新的特徵,如體型小、卷毛、嘴巴長、咖啡色 => 貴賓狗。
Identify Dog and Cat

再正式進入特徵工程之前,我們需要將資料讀進來,順便複習一下前兩天學到的東西

  • 複習一下 Day 2 的 Code 訓練資料集中共有 81 個欄位,依照資料特性分割
    • 數值型欄位 : 36 個
    • 類別型欄位 : 43 個
    • 其他欄位 : Id 和 SalePrice
train0 <- read.csv("train.csv", stringsAsFactors = FALSE)

# 分割 Numeric and Character 欄位
num_features <- names(which(sapply(train0, is.numeric)))
cat_features <- names(which(sapply(train0, is.character)))
train_numeric <- train0[, names(train0) %in% num_features]
train_categoric <- train0[, names(train0) %in% cat_features]
# Numerical 欄位 
print(num_features)
 [1] "Id"            "MSSubClass"    "LotFrontage"   "LotArea"       "OverallQual"   "OverallCond"  
 [7] "YearBuilt"     "YearRemodAdd"  "MasVnrArea"    "BsmtFinSF1"    "BsmtFinSF2"    "BsmtUnfSF"    
[13] "TotalBsmtSF"   "X1stFlrSF"     "X2ndFlrSF"     "LowQualFinSF"  "GrLivArea"     "BsmtFullBath" 
[19] "BsmtHalfBath"  "FullBath"      "HalfBath"      "BedroomAbvGr"  "KitchenAbvGr"  "TotRmsAbvGrd" 
[25] "Fireplaces"    "GarageYrBlt"   "GarageCars"    "GarageArea"    "WoodDeckSF"    "OpenPorchSF"  
[31] "EnclosedPorch" "X3SsnPorch"    "ScreenPorch"   "PoolArea"      "MiscVal"       "MoSold"       
[37] "YrSold"        "SalePrice"    
# Categorical 欄位 
print(cat_features)
 [1] "MSZoning"      "Street"        "Alley"         "LotShape"      "LandContour"   "Utilities"    
 [7] "LotConfig"     "LandSlope"     "Neighborhood"  "Condition1"    "Condition2"    "BldgType"     
[13] "HouseStyle"    "RoofStyle"     "RoofMatl"      "Exterior1st"   "Exterior2nd"   "MasVnrType"   
[19] "ExterQual"     "ExterCond"     "Foundation"    "BsmtQual"      "BsmtCond"      "BsmtExposure" 
[25] "BsmtFinType1"  "BsmtFinType2"  "Heating"       "HeatingQC"     "CentralAir"    "Electrical"   
[31] "KitchenQual"   "Functional"    "FireplaceQu"   "GarageType"    "GarageFinish"  "GarageQual"   
[37] "GarageCond"    "PavedDrive"    "PoolQC"        "Fence"         "MiscFeature"   "SaleType"     
[43] "SaleCondition"
  • 複習一下 Day 2 的 Code 有些欄位雖然是使用數字 1, 2, 3,... 做資料呈現,但從欄位定義及資料內容可以很明顯的看出,這些欄位是屬於類別型資料,因此將這部分資料作轉換
    • MSSubClass (住宅類型)
    • OverallQual (材料與完成度評比)
    • OverallCond (綜合狀況評比)
train0$OverallCond <- as.factor(train0$OverallCond)
train0$OverallQual <- as.factor(train0$OverallQual)
train0$MSSubClass <- as.factor(train0$MSSubClass)

Part 2 : 特徵工程中常見的方法

2-1 : 缺失值插補(Missing Value Imputation)

處理缺失值最簡單的做法就是刪掉有缺失資料,但這麼做可能會誤刪掉一些有用的資訊,所以也可以使用一些進階的方法來填補,通常會依照資料特性不同有不同的處理方法。

  • 複習一下 Day 2 的 Code 查看每個欄位的缺失值和比例
missing_values <- sapply(train0, function(x) sum(is.na(x)))

null_count <- data.frame(Column =names(missing_values), 
                        Count = missing_values, 
                        Proportion = missing_values/nrow(train0)) %>%
  filter(Count > 0) %>%
  arrange(-Count)
Column Count Proportion
PoolQC 1453 0.9952055
MiscFeature 1406 0.9630137
Alley 1369 0.9376712
Fence 1179 0.8075342
FireplaceQu 690 0.4726027
LotFrontage 259 0.1773973
GarageType 81 0.0554795
GarageYrBlt 81 0.0554795
GarageFinish 81 0.0554795
GarageQual 81 0.0554795
GarageCond 81 0.0554795
BsmtExposure 38 0.0260274
BsmtFinType2 38 0.0260274
BsmtQual 37 0.0253425
BsmtCond 37 0.0253425
BsmtFinType1 37 0.0253425
MasVnrType 8 0.0054795
MasVnrArea 8 0.0054795
Electrical 1 0.0006849

2-1-1 : 刪掉缺失值資料

直接刪掉那些含有缺失值的 rows 或是不使用含有缺失值的 columns

#刪掉缺失超過 80% 的欄位
train0 <- train0 %>% 
  select(-c(as.vector(null_count$Column[null_count$Proportion>0.8])))

2-1-2 : 數值(Numerical)型資料

  1. 使用特定數值取代
    • 0 : 當欄位中本來就有 0 時,填補後的 0 就會與原始資料混淆
    • -999 : 使用一個正常狀況下不會出現的數值取代,但要小心若選得不好可能會變成異常值
  2. 平均數(Mean)
  3. 中位數(Median) : 與平均數相比,不會被異常值干擾
  4. 其他 : 使用其他有意義的數值取代

發現當車庫、地下室和外觀相關欄位缺失時,可以表示為房子本身就沒有這些設備,所以可以用 0 字串取代

#車庫相關欄位 (GarageCars, GarageArea)
#地下室相關欄位 (BsmtFullBath, BsmtHalfBath, BsmtFinSF1, BsmtFinSF2, BsmtUnfSF, TotalBsmtSF)
#外觀相關欄位 (MasVnrArea)
train0 <- train0 %>%
  replace_na(list(GarageCars = 0, GarageArea = 0, BsmtFullBath = 0, 
                  BsmtHalfBath = 0, BsmtFinSF1 = 0, BsmtFinSF2 = 0, 
                  BsmtUnfSF = 0, TotalBsmtSF = 0, MasVnrArea = 0))

# LotFrontage(前面街道長度) 使用中位數取代
train0$LotFrontage[is.na(train0$LotFrontage)] <- median(train0$LotFrontage, na.rm = T)

# GarageYrBlt(車庫年份) 使用 YearBuilt(房子建造年份) 取代
train0$GarageYrBlt[is.na(train0$GarageYrBlt)] <- train0$YearBuilt[is.na(train0$GarageYrBlt)]

2-1-3 : 類別(Categorical)型資料

  1. 使用特定字串取代
    • None
    • Others
  2. 眾數(Mode)

發現當車庫、地下室和外觀相關欄位缺失時,可以表示為房子本身就沒有這些設備且這幾個欄位的缺失值比較少,所以可以用 None 字串取代

#車庫相關欄位 (GarageFinish , GarageQual, GarageCond, GarageType)
#地下室相關欄位 (BsmtCond, BsmtExposure, BsmtQual, BsmtFinType2, BsmtFinType1)
#其他欄位 (MasVnrType, FireplaceQu)
train0 <- train0 %>%
  replace_na(list(GarageFinish = "None", GarageQual = "None", 
                  GarageCond = "None", GarageType = "None",
                  BsmtExposure = "None", BsmtQual = "None",
                  BsmtCond = "None", BsmtFinType1 = "None",
                  BsmtFinType2 = "None", MasVnrType = "None", FireplaceQu="None"))

# Electrical(電力系統) 使用眾數取代
#建立 Mode Function
Mode <- function(x) {
  u_x <- unique(x)
  u_x[which.max(tabulate(match(x, u_x)))]
}
train0$Electrical[is.na(train0$Electrical)] <- Mode(train0$Electrical)

2-2 : 異常值檢測(Outliers Detection)

  • 檢測異常值的方法,依照資料特性有不同的檢測方法
    • 數值(Numerical)型資料
      • 單一特徵 : Box Plot
      • 兩兩特徵 : Scatter Plot
    • 類別(Categorical)型資料
      • Bar Chart
  • 處理異常值的方法
    • 直接刪除
    • 套用處理缺失值的方法
    • 特徵變換 e.g. log transformation, binning, …

2-3 : 特徵縮放(Feature Scaling)

  • 為什麼要做特徵縮放?
    從我們使用的例子來說,目前有各式各樣的房屋特徵,如房屋面積、前方街道距離、壁爐數量等等,這些資料因為收集的方式不同、單位不同,可能造成資料間範圍有很大的差異。舉例來說壁爐數量可能 1 - 10 以內,而房屋面積可能超過 100 以上。 資料間範圍相差過大,可能會導致模型更偏向於資料範圍較大的那個特徵。解決的辦法就是將各種不同 Scale 的特徵轉換成同樣的 Scale。

  • 常見的方法 :
    • 標準化(Standardization) : 將資料轉換成趨近常態,讓 \(\mu\) 為 0 , \(\sigma\) 為 1 \[X = \frac{ x - \mu}{\sigma}\]
    • 歸一化(Normalization) : 讓數據範圍介在某個範圍之間,如 [0, 1],可以降低單位對資料的影響 \[X = \frac{ x - min(x) }{ max(x) - min(x) }\]

有些模型若放進去的資料沒有做 Scale 調整,模型的預測力會很差,如 SVM、神經網路、K-means 等

#將面積相關的欄位 (GrLivArea, TotalSF, MasVnrArea) 做標準化
train0$GrLivArea_stand <- scale(train0$GrLivArea)
train0$MasVnrArea_stand <- scale(train0$MasVnrArea)

2-4 : 特徵變換(Feature Transformation)

2-4-1 : 數值(Numerical)型資料

  1. Rounding : 有些數據可能會有到小數點後第 n 位的特徵,如果不想要用到這麼精確的數字,就可以嘗試用這個方法
    • round(value * m) ; m = 10, 100, …
    • round(log(value))
  2. Transformation : 可以減少極端值對模型的影響,也可以讓資料分佈更接近常態分佈,而這些好處都可以提升模型預測的表現
    • 取對數(log)
    • 取平方根(square root)
    • 取立方根(cube root) : 負數也適用
  3. Binning : 對連續的數值資料做離散化(discretization)
    • 從我們使用的例子來說
    • 將屋齡來做區分,拆分成 n 段,0-20 年、20-40 年、40-60 年等
    • 將壁爐數量來做處理,當壁爐數量 >0 表示有壁爐 ; 否則無壁爐
    • 為什麼要做離散化?
      1. 離散後的資料,可以克服資料中的缺陷,讓模型更穩定且可以降低 overfitting 的問題
      2. 特徵與目標變數間的關係變得更清楚,有利於對非線性關係的診斷與描述
#將 LotFrontage, SalePrice 做 log transformation
train0$LotFrontage_log <- log(train0$LotFrontage)
train0$SalePrice_log <- log(train0$SalePrice)

#新增一個欄位,判斷房子是否有壁爐
train0$is_Fireplace <- ifelse(train0$Fireplaces>0, "1", "0")
#檢查一下 LotFrontage, SalePrice 做 log transformation 前後比較
train0 %>%
  select(LotFrontage, SalePrice, LotFrontage_log, SalePrice_log) %>% 
  gather() %>% 
  ggplot(aes(value)) +
  facet_wrap(~ key, scales = "free") +
  geom_histogram()

2-4-2 : 類別(Categorical)型資料

  1. Integer Encoding : 把每個類別轉對應到數字
    • 隨機對應到 0, 1, 2, 3, 4 等
    • 依照頻率大小的順序給值
  2. One-hot Encoding
#將地下室等級(BsmtCond)轉換成數值 
#Ex=Excellent,Gd=Good,TA=Typical,Fa=Fair,Po=Poor,None=No Basement 
Qualities <- c('None' = 0, 'Po' = 1, 'Fa' = 2, 'TA' = 3, 'Gd' = 4, 'Ex' = 5)       
train0$BsmtCond <- as.integer( revalue(train0$BsmtCond, Qualities) )

2-5 : 特徵建構(Feature Construction)

  • 從原有的特徵中,創造出新的特徵
#建立一個總共 bathroom 數量
train0$TotalBathrooms <- train0$FullBath+(train0$HalfBath*0.5)+train0$BsmtFullBath+(train0$BsmtHalfBath*0.5)

本日小挑戰

請利用 TotalBsmtSFX1stFlrSFX2ndFlrSF
1. 建立出一個總房屋面積 TotalSF
2. 將 TotalSF 做標準化轉換
[Note] R 中變數的第一個字不能是數字,所以 1stFlrSF -> X1stFlrSF, 2ndFlrSF -> X2ndFlrSF