library(kernlab)
library(caret)
data(spam)
set.seed(123)
= createDataPartition(y = spam$type, p = 0.75, list = F)
noTreino = spam[noTreino,]
treino = spam[-noTreino,]
teste # Vamos olhar para a variável capitalAve (média de letras maiúsculas por linha):
hist(treino$capitalAve,
ylab = "Frequência",
xlab = "Média de Letras Maiúsculas por Linha",
main = "Histograma da Média de Letras Maiúsculas por Linha",
col="steelblue", breaks = 4)
9 Pré-processamento
Antes de criarmos um modelo de predição, é importante plotarmos as variáveis do nosso modelo antecipadamente para observarmos se há algum comportamento estranho entre elas. Por exemplo, podemos ter uma variável que assuma frequentemente um único valor (possui muito pouca variabilidade), o que não acrescenta informações relevantes ao modelo, ou uma que possua alguns dados faltantes (NA’s). O que podemos fazer nesses casos, que é o que iremos estudar neste capítulo, é realizar alterações em tais variáveis, afim de melhorar/otimizar a nossa predição/classificação. Essa é a ideia de pré-processar.
9.1 Padronizando os dados
Vamos carregar o banco de dados spam e criar amostras treino e teste.
Podemos notar que muitos elementos estão próximos do 0 e os outros estão muito espalhados. Ou seja, essa variável não está trazendo muita informação para o modelo.
mean(treino$capitalAve)
[1] 4.863991
sd(treino$capitalAve)
[1] 27.80173
Podemos ver que a média é pequena mas o desvio padrão é muito grande.
Para que os algoritmos de machine learning não sejam enganados pelo fato de a variável ser altamente variável, vamos realizar um pré-processamento. Vamos padronizar os dados da variável pela amostra treino pegando cada valor dela e subtraindo pela sua média e dividindo pelo seu desvio padrão.
= treino$capitalAve
treinoCapAve # Padronizando a variável:
= (treino$capitalAve-mean(treinoCapAve))/sd(treinoCapAve)
treinoCapAveP # Média da variável padronizada:
mean(treinoCapAveP)
[1] 9.854945e-18
Agora temos média 0.
# Desvio padrão da variável padronizada:
sd(treinoCapAveP)
[1] 1
E variância 1.
# Vamos olhar para a variável capitalAve (média de letras maiúsculas por linha):
hist(treinoCapAveP, ylab = "Frequência", xlab = "Média de Letras Maiúsculas por Linha",
main = "Histograma da Média de Letras Maiúsculas por Linha",col="steelblue", breaks =4)
Agora vamos aplicar a mesma transformação na amostra teste. Uma coisa a ter em mente é que ao aplicar um algoritmo no conjunto de teste, só podemos usar os parâmetros que estimamos no conjunto de treino. Ou seja, temos que usar a média e o desvio padrão da variável capitalAve do TREINO.
= teste$capitalAve
testeCapAve # Aplicando a transformação:
= (testeCapAve-mean(treinoCapAve))/sd(treinoCapAve)
testeCapAveP # Média da variável transformada do conjunto teste:
mean(testeCapAveP)
[1] 0.04713308
# Desvio Padrão da variável transformada do conjunto teste:
sd(testeCapAveP)
[1] 1.486708
Nesse caso não obtemos média 0 e variância 1, afinal nós utilizamos os parâmetros do treino para a padronização. Mas podemos notar que os valores estão relativamente próximos disso.
9.2 Padronizando os dados com a função PreProcess()
Podemos realizar o pré-processamento utilizando a função preProcess() do caret. Ela realiza vários tipos de padronizações, mas para utilizarmos a mesma (subtrair a média e dividir pelo desvio padrão) utilizamos o método c(“center”,“scale”).
= preProcess(treino, method = c("center","scale"))
padronizacao # O comando acima cria um modelo de padronização. Para ter efeito ele deve ser aplicado nos dados com o
# comando predict().
= predict(padronizacao,treino)$capitalAve
treinoCapAveS # Média da variável padronizada:
mean(treinoCapAveS)
[1] 8.680584e-18
# Desvio padrão da variável padronizada:
sd(treinoCapAveS)
[1] 1
Note que chegamos à mesma média e variância de quando padronizamos sem o preProcess().
Agora vamos aplicar essa padronização no conjunto de teste:
= predict(padronizacao,teste)$capitalAve
testeCapAveS # Note que aplicamos o modelo de padronização criado com a amostra treino.
Observe que também encontramos o mesmo valor da média e desvio padrão de quando padronizamos a variável do conjunto teste anteriormente (sem o preProcess()):
mean(testeCapAveS)
[1] 0.04713308
sd(testeCapAveS)
[1] 1.486708
Repare que também chegamos à mesma média e variância de quando padronizamos sem o preProcess().
9.3 preProcess como argumento da função train()
Também podemos utilizar o preProcess dentro da função train da seguinte forma:
= train(type~., data = treino, preProcess = c("center","scale"),
modelo method = "glm")
A única limitação é que esse método aplica a padronização em todas as variáveis numéricas.
Obs.: Quando for padronizar uma variável da sua base para depois treinar seu algoritmo, lembre-se que colocar a variável padronizada de volta na sua base.
9.4 Tratando NA’s
É muito comum encontrar alguns dados faltantes (NA’s) em uma base de dados. E quando você usa essa base para fazer predições, o algoritmo preditor muitas vezes falha, pois eles são criados para não manipular dados ausentes (na maioria dos casos). O mais recomendado a se fazer é descartar esses dados, principalmente se o número de variáveis for muito pequeno. Porém, em alguns casos, podemos tentar substituir os NA’s da amostra por dados de outros elementos que possuam características parecidas.
Obs: Este é um procedimento que deve ser feito com muito cuidado, apenas em situações de real necessidade.
9.5 Método k-Nearest Neighbors (knn)
O método k-Nearest Neighbors (knn) consiste em procurar os k vizinhos mais próximos do elemento que possui o dado faltante de uma variável de interesse, calculando a média dos valores observados dessa variável dos k vizinhos e imputando esse valor ao elemento.
Vamos utilizar novamente a variável capitalAve do banco de dados spam como exemplo.
library(kernlab)
library(caret)
data(spam)
set.seed(13343)
# Criando amostras treino e teste:
= createDataPartition(y = spam$type, p = 0.75, list = F)
noTreino = spam[noTreino,]
treino = spam[-noTreino,] teste
Originalmente, a variável capitalAve não possui NA’s. Mas para o objetivo de compreendermos como esse método funciona, vamos inserir alguns valores NA’s.
= rbinom(dim(treino)[1], size = 1, p = 0.05)==1 NAs
O que fizemos com a função rbinom() é criar uma amostra de tamanho “dim(treino)[1]” (quantidade de elementos no treino) de uma variável Bernoulli com probabilidade de sucesso = 0,05. Ou seja, o vetor NAs será um vetor do tipo logical, onde será TRUE se o elemento gerado pela rbinom() é “1” (probabilidade de 0,05 de acontecer) e FALSE se é “0” (probabilidade 0,95 de acontecer).
Para preservar os valores originais, vamos criar uma nova coluna de dados no treino chamada capAve, que será uma réplica da variável capitalAve, mas com os NA’s inseridos em alguns valores.
library(dplyr)
# Criando a nova variável capAve com os mesmos valores da capitalAve:
= treino %>% mutate(capAve = capitalAve)
treino
# Inserindo os Na's:
$capAve[NAs] = NA treino
Então, recapitulando: criamos manualmente uma base de dados que possui valores faltantes. Agora podemos aplicar o método KNN para imputar valores aos NA’s, escolhendo essa opção por meio do argumento “method” da função preProcess(). Na vida real, obviamente, não vamos criar dados faltantes, mas é possível que nossas bases tenham esses NA’s e vamos ter que preenchê-los de alguma forma. O padrão da função é utilizar k=5 (número de vizinhos mais próximos igual a cinco).
= preProcess(treino, method = "knnImpute")
imput
# Aplicando o modelo de pré-processamento ao banco de dados treino:
$capAve = predict(imput,treino)$capAve
treino
# Olhando para a variável capAve após o pré-processamento:
head(treino$capAve, n = 20)
[1] -0.046596612 -0.008173931 0.125003949 -0.052792906 -0.052792906
[6] -0.067986558 -0.105588726 -0.083548027 0.122825344 -0.115746121
[11] -0.047388832 -0.093931771 -0.097100652 -0.021245565 0.850451334
[16] -0.115519772 -0.044418006 -0.015445381 -0.120867259 0.001785409
Note que além de ter imputado valores aos NA’s, o comando knnImpute também padronizou os dados.
Obs: O método knnImpute só resolve os NA’s quando os dados faltantes são NUMÉRICOS.
E se quiséssemos aplicar o método de imputar valores aos NA’s em todo o conjunto de dados, e não só em apenas 1 variável? Também podemos fazer isso utilizando a função preProcess().
Vamos utilizar a base de dados “airquality”, já disponível no R, como exemplo.
= airquality
base head(base, n = 15)
Ozone Solar.R Wind Temp Month Day
1 41 190 7.4 67 5 1
2 36 118 8.0 72 5 2
3 12 149 12.6 74 5 3
4 18 313 11.5 62 5 4
5 NA NA 14.3 56 5 5
6 28 NA 14.9 66 5 6
7 23 299 8.6 65 5 7
8 19 99 13.8 59 5 8
9 8 19 20.1 61 5 9
10 NA 194 8.6 69 5 10
11 7 NA 6.9 74 5 11
12 16 256 9.7 69 5 12
13 11 290 9.2 66 5 13
14 14 274 10.9 68 5 14
15 18 65 13.2 58 5 15
Note que essa base possui alguns valores NA’s em algumas variáveis.
# Realizando o método KNN para imputar valores aos NA's:
= preProcess(base, method = "knnImpute")
imput
# Aplicando o modelo em toda a base de dados:
= predict(imput, base)
nova_base
# Vamos olhar para a nova base:
head(nova_base, n = 15)
Ozone Solar.R Wind Temp Month Day
1 -0.03423409 0.045176154 -0.72594816 -1.1497140 -1.407294 -1.6700195
2 -0.18580489 -0.754304874 -0.55563883 -0.6214670 -1.407294 -1.5572102
3 -0.91334473 -0.410083876 0.75006604 -0.4101682 -1.407294 -1.4444009
4 -0.73145977 1.410956244 0.43783226 -1.6779609 -1.407294 -1.3315917
5 -0.81027658 -0.221317522 1.23260914 -2.3118573 -1.407294 -1.2187824
6 -0.42831817 0.007422883 1.40291847 -1.2553634 -1.407294 -1.1059732
7 -0.57988897 1.255501599 -0.38532950 -1.3610128 -1.407294 -0.9931639
8 -0.70114561 -0.965279034 1.09068470 -1.9949091 -1.407294 -0.8803546
9 -1.03460136 -1.853591288 2.87893266 -1.7836103 -1.407294 -0.7675454
10 -0.64051729 0.089591767 -0.38532950 -0.9384152 -1.407294 -0.6547361
11 -1.06491552 0.749163615 -0.86787260 -0.4101682 -1.407294 -0.5419268
12 -0.79208809 0.778033763 -0.07309573 -0.9384152 -1.407294 -0.4291176
13 -0.94365889 1.155566471 -0.21502017 -1.2553634 -1.407294 -0.3163083
14 -0.85271641 0.977904020 0.26752293 -1.0440646 -1.407294 -0.2034991
15 -0.73145977 -1.342811742 0.92037537 -2.1005585 -1.407294 -0.0906898
Note que ela não possui mais NA’s e todas as variáveis foram padronizadas.
9.6 Utilizando Algoritmos de Machine Learning com o Pacote mlr
O pacote mlr fornece vários métodos de imputação para dados faltantes. Alguns desses métodos possuem técnicas padrões como, por exemplo, imputação por uma constante (uma constante fixa, a média, a mediana ou a moda) ou números aleatórios (da distribuição empírica dos dados em consideração ou de uma determinada família de distribuições). Para mais informações sobre como utilizar essas imputações padrões, consulte https://mlr.mlr-org.com/reference/imputations.html.
Entretanto, a principal vantagem desse pacote - que é o que abordaremos nessa seção - é a possibilidade de imputação dos valores faltantes de uma variável por meio de predições de um algoritmo de machine learning, utilizando como base as outras variáveis. Ou seja, além de aceitar valores faltantes de variáveis numéricas para a imputação, ele também aceita de variáveis categóricas.
Podemos observar todos os algoritmos de machine learning possíveis de serem utilizados nesse pacote através da função listLearners().
- Para um problema de imputação de NA’s de variáveis numéricas temos os seguintes métodos:
library(mlr)
::kable(listLearners("regr", properties = "missings")["class"]) knitr
class |
---|
regr.bartMachine |
regr.cforest |
regr.ctree |
regr.cubist |
regr.featureless |
regr.gbm |
regr.h2o.deeplearning |
regr.h2o.gbm |
regr.h2o.glm |
regr.h2o.randomForest |
regr.rpart |
regr.xgboost |
- Para um problema de imputação de NA’s de variáveis categóricas temos os seguintes métodos:
::kable(listLearners("classif", properties = "missings")["class"]) knitr
class |
---|
classif.bartMachine |
classif.boosting |
classif.C50 |
classif.cforest |
classif.ctree |
classif.featureless |
classif.gbm |
classif.h2o.deeplearning |
classif.h2o.gbm |
classif.h2o.glm |
classif.h2o.randomForest |
classif.J48 |
classif.JRip |
classif.naiveBayes |
classif.OneR |
classif.PART |
classif.rpart |
classif.xgboost |
Vamos utilizar o banco de dados “heart” para realizarmos a imputação de dados faltantes categóricos.
library(caret)
library(readr)
library(dplyr)
= read_csv("Heart.csv")
heart
# Verificando se a base "heart" possui valores NA's em alguma variável:
apply(heart, 2, function(x) any(is.na(x)))
X1 Age Sex ChestPain RestBP Chol
FALSE FALSE FALSE FALSE FALSE FALSE
Fbs RestECG MaxHR ExAng Oldpeak Slope
FALSE FALSE FALSE FALSE FALSE FALSE
Ca Thal HeartDisease
FALSE FALSE FALSE
Note que a base não possui dados faltantes. Para fins didáticos, vamos inserir alguns na variável “Thal”.
# Criando um novo banco de dados que possuirá NA's:
= as.data.frame(heart)
new.heart
set.seed(133)
# Criando um vetor do tipo *logical*, onde será TRUE se o elemento gerado pela rbinom() é "1"
# (probabilidade de 0,1 de acontecer):
= rbinom(dim(new.heart)[1], size = 1, p = 0.1)==1
NAs
# Inserindo os NA's na variável Thal:
$Thal[NAs] = NA
new.heart$Thal new.heart
[1] "fixed" "normal" "reversable" "normal" "normal"
[6] "normal" "normal" "normal" "reversable" "reversable"
[11] "fixed" "normal" "fixed" "reversable" "reversable"
[16] "normal" "reversable" "normal" "normal" "normal"
[21] NA "normal" "normal" "reversable" "reversable"
[26] "normal" "normal" "normal" "normal" NA
[31] "normal" "reversable" "normal" "reversable" "normal"
[36] "normal" "reversable" "fixed" "reversable" "normal"
[41] "reversable" "reversable" "normal" "normal" "normal"
[46] "reversable" "normal" "reversable" "normal" "normal"
[51] "normal" "reversable" "normal" "normal" "reversable"
[56] NA "reversable" "reversable" "normal" "normal"
[61] NA "normal" "reversable" "normal" NA
[66] "reversable" "normal" "reversable" "reversable" "normal"
[71] "normal" "reversable" "reversable" "fixed" NA
[76] "normal" "reversable" "normal" "normal" NA
[81] "normal" "normal" "normal" "reversable" "normal"
[86] "normal" "normal" "normal" "normal" "normal"
[91] "reversable" "reversable" "normal" "normal" "reversable"
[96] "reversable" "reversable" "normal" "normal" "normal"
[101] "normal" NA "normal" "reversable" "reversable"
[106] "reversable" "reversable" "reversable" "reversable" "reversable"
[111] "normal" "fixed" "reversable" "reversable" "fixed"
[116] "normal" "normal" "reversable" "reversable" "reversable"
[121] "reversable" "normal" NA "normal" NA
[126] "reversable" "reversable" "normal" "normal" "reversable"
[131] "reversable" "normal" "normal" "normal" "normal"
[136] NA "reversable" "reversable" "normal" "normal"
[141] "reversable" "normal" "reversable" "reversable" "normal"
[146] "reversable" "normal" "normal" NA "reversable"
[151] "normal" "reversable" "reversable" "normal" "normal"
[156] "reversable" "reversable" NA "reversable" "reversable"
[161] NA "normal" "normal" "normal" "reversable"
[166] "normal" NA "normal" "reversable" "reversable"
[171] "normal" "normal" "fixed" "reversable" "reversable"
[176] "fixed" "normal" "normal" "reversable" "reversable"
[181] "normal" "reversable" "normal" "normal" "reversable"
[186] "fixed" "reversable" "reversable" "normal" "reversable"
[191] "normal" "normal" "normal" "normal" "normal"
[196] "normal" "normal" "normal" "normal" NA
[201] "reversable" NA "reversable" NA "reversable"
[206] "normal" "normal" "normal" "reversable" "normal"
[211] "reversable" "normal" "reversable" "normal" "normal"
[216] "normal" "normal" "normal" "normal" "normal"
[221] "reversable" "normal" "normal" "normal" "normal"
[226] "normal" "normal" "normal" "normal" NA
[231] "normal" "normal" "normal" "reversable" "reversable"
[236] "normal" "normal" "normal" "normal" "normal"
[241] "normal" "normal" "normal" "reversable" "normal"
[246] "reversable" "normal" "fixed" "reversable" "reversable"
[251] "normal" "normal" "normal" "normal" "normal"
[256] "normal" "reversable" "normal" "normal" "normal"
[261] "normal" "normal" "fixed" "fixed" "reversable"
[266] "normal" "reversable" "fixed" "reversable" "normal"
[271] "normal" "reversable" "normal" "normal" "normal"
[276] "normal" "reversable" "normal" "reversable" NA
[281] "reversable" "fixed" "fixed" "reversable" "normal"
[286] "reversable" "normal" "fixed" "reversable" "normal"
[291] NA "fixed" NA "reversable" "reversable"
[296] "reversable" "normal"
Agora vamos imputar categorias aos dados faltantes da variável Thal. Iremos fazer isso através da função impute(). O único problema é que possuímos variáveis do tipo character na base de dados, e a função não aceita esta classe nos dados.
str(new.heart)
'data.frame': 297 obs. of 15 variables:
$ X1 : num 1 2 3 4 5 6 7 8 9 10 ...
$ Age : num 63 67 67 37 41 56 62 57 63 53 ...
$ Sex : num 1 1 1 1 0 1 0 0 1 1 ...
$ ChestPain : chr "typical" "asymptomatic" "asymptomatic" "nonanginal" ...
$ RestBP : num 145 160 120 130 130 120 140 120 130 140 ...
$ Chol : num 233 286 229 250 204 236 268 354 254 203 ...
$ Fbs : num 1 0 0 0 0 0 0 0 0 1 ...
$ RestECG : num 2 2 2 0 2 0 2 0 2 2 ...
$ MaxHR : num 150 108 129 187 172 178 160 163 147 155 ...
$ ExAng : num 0 1 1 0 0 0 0 1 0 1 ...
$ Oldpeak : num 2.3 1.5 2.6 3.5 1.4 0.8 3.6 0.6 1.4 3.1 ...
$ Slope : num 3 2 2 3 1 1 3 1 2 3 ...
$ Ca : num 0 3 2 0 0 0 2 0 1 0 ...
$ Thal : chr "fixed" "normal" "reversable" "normal" ...
$ HeartDisease: chr "No" "Yes" "Yes" "No" ...
Vamos transformar essas categorias em fatores.
= mutate_if(new.heart, is.character, as.factor) new.heart
Vamos separar os dados em treino e teste.
set.seed(133)
= caret::createDataPartition(y = new.heart$HeartDisease, p = 0.75,
noTreino list = F)
= new.heart[noTreino,]
treino = new.heart[-noTreino,] teste
Agora vamos imputar os dados no conjunto treino com a função impute().
Para isso passamos como argumento:
A base de dados que possui os valores faltantes;
A variável resposta do modelo, ou seja, a variável de interesse para predição. No nosso exemplo essa variável é a “HeartDisease”, que indica se uma pessoa possui uma doença cardíaca;
Lista contendo o método de imputação para cada coluna do banco de dados. Como apenas temos NA’s na variável “Thal”, a lista só possuirá essa variável, seguida do método de imputação que desejamos para ela. Vamos utilizar o método de árvores de decisão (“rpart”).
= mlr::impute(treino, target = "HeartDisease",
treino cols = list(Thal = imputeLearner("classif.rpart")))
Essa função retorna uma lista de tamanho 2, onde primeiro se encontra a base de dados após a imputação dos valores e em seguida detalhes do método utilizado.
Vamos olhar para a variável após a imputação dos dados:
$data[,"Thal"] treino
[1] normal normal normal normal normal normal
[7] reversable reversable fixed normal fixed reversable
[13] normal normal normal reversable reversable reversable
[19] normal normal normal normal reversable normal
[25] normal reversable reversable reversable normal reversable
[31] reversable normal reversable normal normal normal
[37] normal reversable normal normal reversable reversable
[43] reversable reversable normal normal reversable normal
[49] reversable reversable reversable normal normal reversable
[55] reversable fixed reversable normal reversable normal
[61] normal reversable normal normal normal reversable
[67] normal normal normal normal normal reversable
[73] reversable normal reversable reversable reversable normal
[79] normal normal reversable reversable reversable reversable
[85] reversable reversable fixed normal reversable reversable
[91] normal reversable normal normal reversable reversable
[97] normal normal normal normal normal reversable
[103] reversable normal reversable normal normal reversable
[109] normal normal normal reversable reversable normal
[115] normal reversable reversable normal normal normal
[121] reversable reversable normal reversable reversable normal
[127] normal reversable reversable fixed normal normal
[133] reversable reversable reversable normal normal fixed
[139] reversable reversable normal reversable normal normal
[145] normal normal normal normal reversable reversable
[151] reversable reversable reversable reversable normal normal
[157] reversable reversable normal normal normal normal
[163] normal reversable normal normal normal normal
[169] normal normal normal normal normal normal
[175] normal reversable reversable normal normal normal
[181] normal normal reversable reversable normal fixed
[187] reversable normal normal normal normal normal
[193] reversable normal normal normal fixed fixed
[199] reversable normal reversable fixed reversable normal
[205] reversable normal normal normal normal reversable
[211] normal reversable reversable fixed fixed reversable
[217] normal normal fixed reversable normal normal
[223] normal
Levels: fixed normal reversable
Para implementarmos esse algoritmo no conjunto de dados teste basta utilizarmos a função reimpute() que implementaremos o mesmo método com os mesmos critérios criados no conjuno treino. Basta passar os seguintes argumentos:
A base de dados que possui os valores faltantes;
O mesmo método utilizado no treino.
A função retorna a base de dados com os valores imputados.
= reimpute(teste, treino$desc)
teste $Thal teste
[1] fixed reversable reversable reversable normal normal
[7] normal reversable normal normal fixed normal
[13] normal reversable normal normal normal reversable
[19] normal normal normal normal normal reversable
[25] normal reversable reversable fixed normal reversable
[31] reversable normal reversable reversable normal normal
[37] reversable reversable normal reversable reversable reversable
[43] reversable normal normal fixed normal reversable
[49] normal normal normal normal normal reversable
[55] normal normal normal normal normal normal
[61] normal normal reversable normal normal normal
[67] normal normal reversable normal fixed reversable
[73] reversable reversable
Levels: fixed normal reversable
9.7 Variável Dummy
As variáveis dummies ou variáveis indicadoras são formas de agregar informações qualitativas em modelos estatísticos. Ela atribui 1 se o elemento possui determinada característica, ou 0 caso ele não possua. Esse tipo de transformação é importante para modelos de regressão pois ela torna possível trabalhar com variáveis qualitativas.
Vamos utilizar o banco de dados Wage, do pacote ISLR. Este banco possui informações sobre 3000 trabalhadores do sexo masculino de uma região dos EUA, como por exemplo idade (age), tipo de trabalho (jobclass), salário (wage), entre outras. Nosso objetivo é tentar prever o salário do indivíduo em função das outras variáveis.
library(ISLR)
data(Wage)
head(Wage)
year age maritl race education region
231655 2006 18 1. Never Married 1. White 1. < HS Grad 2. Middle Atlantic
86582 2004 24 1. Never Married 1. White 4. College Grad 2. Middle Atlantic
161300 2003 45 2. Married 1. White 3. Some College 2. Middle Atlantic
155159 2003 43 2. Married 3. Asian 4. College Grad 2. Middle Atlantic
11443 2005 50 4. Divorced 1. White 2. HS Grad 2. Middle Atlantic
376662 2008 54 2. Married 1. White 4. College Grad 2. Middle Atlantic
jobclass health health_ins logwage wage
231655 1. Industrial 1. <=Good 2. No 4.318063 75.04315
86582 2. Information 2. >=Very Good 2. No 4.255273 70.47602
161300 1. Industrial 1. <=Good 1. Yes 4.875061 130.98218
155159 2. Information 2. >=Very Good 1. Yes 5.041393 154.68529
11443 2. Information 1. <=Good 1. Yes 4.318063 75.04315
376662 2. Information 2. >=Very Good 1. Yes 4.845098 127.11574
Vamos olhar para 2 variáveis: jobclass (tipo de trabalho) e health_ins (indica se o trabalhador possui plano de saúde).
library(ggplot2)
%>% ggplot(aes(x=jobclass)) + geom_bar(aes(fill=jobclass)) +
Wage ylab("Frequência") + guides(fill=F) + theme_light() +
ggtitle("Gráfico de Barras para o Tipo de Trabalho")
%>% ggplot(aes(x=health_ins)) + geom_bar(aes(fill=health_ins)) +
Wage ylab("Frequência") + guides(fill=F) + theme_light() +
ggtitle("Gráfico de Barras para o Plano de Saúde")
Vamos transformar essas 2 variáveis em dummies por meio da função dummyVars().
= dummyVars(wage~jobclass+health_ins, data = Wage)
dummies
# Aplicando ao modelo:
= predict(dummies, newdata = Wage)
Dummies
head(Dummies)
jobclass.1. Industrial jobclass.2. Information health_ins.1. Yes
231655 1 0 0
86582 0 1 0
161300 1 0 1
155159 0 1 1
11443 0 1 1
376662 0 1 1
health_ins.2. No
231655 1
86582 1
161300 0
155159 0
11443 0
376662 0
Note que ele transforma cada categoria numa variável dummy. Ou seja, como temos 2 categorias para jobclass, cada uma delas vira uma variável dummy, cujos nomes são “jobclass.1 Industrial” e “jobclass.2 Information”. Então se para um indivíduo temos um “1” na categoria “jobclass=industrial”, isso implica que terá um “0” na categoria “jobclass=information”, pois ou o indivíduo tem um tipo de trabalho, ou tem outro. O mesmo vale para as categorias de plano de saúde.
Vale frisar que se separamos nossa base de dados em Treino e Teste, usar essa técnica de dummyVars vai criar as dummies considerando somente as classes que estão presentes na base de treino. Se na base de teste existirem outras classes possíveis que não estão presentes na base de treino, essas classes precisam ser especificadas na variável da base de treino com a função “factor”. Assim, o algoritmo vai saber que essa classe também precisa de uma variável dummy.
Observe também que esse novo modelo criado é uma matriz:
class(Dummies)
[1] "matrix" "array"
Vamos anexar esse novo objeto aos dados:
= cbind(Wage, Dummies)
Wage_dummy head(Wage_dummy)
year age maritl race education region
231655 2006 18 1. Never Married 1. White 1. < HS Grad 2. Middle Atlantic
86582 2004 24 1. Never Married 1. White 4. College Grad 2. Middle Atlantic
161300 2003 45 2. Married 1. White 3. Some College 2. Middle Atlantic
155159 2003 43 2. Married 3. Asian 4. College Grad 2. Middle Atlantic
11443 2005 50 4. Divorced 1. White 2. HS Grad 2. Middle Atlantic
376662 2008 54 2. Married 1. White 4. College Grad 2. Middle Atlantic
jobclass health health_ins logwage wage
231655 1. Industrial 1. <=Good 2. No 4.318063 75.04315
86582 2. Information 2. >=Very Good 2. No 4.255273 70.47602
161300 1. Industrial 1. <=Good 1. Yes 4.875061 130.98218
155159 2. Information 2. >=Very Good 1. Yes 5.041393 154.68529
11443 2. Information 1. <=Good 1. Yes 4.318063 75.04315
376662 2. Information 2. >=Very Good 1. Yes 4.845098 127.11574
jobclass.1. Industrial jobclass.2. Information health_ins.1. Yes
231655 1 0 0
86582 0 1 0
161300 1 0 1
155159 0 1 1
11443 0 1 1
376662 0 1 1
health_ins.2. No
231655 1
86582 1
161300 0
155159 0
11443 0
376662 0
# Removendo as variáveis categóricas do banco de dados completo (opcional):
= dplyr::select(Wage_dummy, -c(jobclass,health_ins))
Wage_dummy head(Wage_dummy)
year age maritl race education region
231655 2006 18 1. Never Married 1. White 1. < HS Grad 2. Middle Atlantic
86582 2004 24 1. Never Married 1. White 4. College Grad 2. Middle Atlantic
161300 2003 45 2. Married 1. White 3. Some College 2. Middle Atlantic
155159 2003 43 2. Married 3. Asian 4. College Grad 2. Middle Atlantic
11443 2005 50 4. Divorced 1. White 2. HS Grad 2. Middle Atlantic
376662 2008 54 2. Married 1. White 4. College Grad 2. Middle Atlantic
health logwage wage jobclass.1. Industrial
231655 1. <=Good 4.318063 75.04315 1
86582 2. >=Very Good 4.255273 70.47602 0
161300 1. <=Good 4.875061 130.98218 1
155159 2. >=Very Good 5.041393 154.68529 0
11443 1. <=Good 4.318063 75.04315 0
376662 2. >=Very Good 4.845098 127.11574 0
jobclass.2. Information health_ins.1. Yes health_ins.2. No
231655 0 0 1
86582 1 0 1
161300 0 1 0
155159 1 1 0
11443 1 1 0
376662 1 1 0
Como comentado acima, nós temos uma variável dummy para cada categoria. Como tínhamos 2 variáveis qualitativas, então ficamos com 4 variáveis dummies. Porém, para um modelo de regressão, isso não é necessário. Estaríamos inserindo 2 variáveis com colinearidade perfeita no modelo: jobclass=industrial é totalmente correlacionada com jobclass=information, pois o resultado de uma influencia totalmente o da outra (o mesmo vale para as variáveis do plano de saúde). Dessa forma, vamos remover essas variáveis desnecessárias.
= dplyr::select(Wage_dummy, -c("jobclass.2. Information","health_ins.2. No"))
Wage_dummy head(Wage_dummy)
year age maritl race education region
231655 2006 18 1. Never Married 1. White 1. < HS Grad 2. Middle Atlantic
86582 2004 24 1. Never Married 1. White 4. College Grad 2. Middle Atlantic
161300 2003 45 2. Married 1. White 3. Some College 2. Middle Atlantic
155159 2003 43 2. Married 3. Asian 4. College Grad 2. Middle Atlantic
11443 2005 50 4. Divorced 1. White 2. HS Grad 2. Middle Atlantic
376662 2008 54 2. Married 1. White 4. College Grad 2. Middle Atlantic
health logwage wage jobclass.1. Industrial
231655 1. <=Good 4.318063 75.04315 1
86582 2. >=Very Good 4.255273 70.47602 0
161300 1. <=Good 4.875061 130.98218 1
155159 2. >=Very Good 5.041393 154.68529 0
11443 1. <=Good 4.318063 75.04315 0
376662 2. >=Very Good 4.845098 127.11574 0
health_ins.1. Yes
231655 0
86582 0
161300 1
155159 1
11443 1
376662 1
Uma maneira mais simples de fazer isso, sem precisarmos retirar cada variável “na mão”, é utilizar o argumento “fullRank=T” da função dummyVars().
= dummyVars(wage~jobclass+health_ins, data = Wage, fullRank = T)
dummies
# Aplicando ao modelo:
= predict(dummies, newdata = Wage) Dummies
Note que o comando fullRank=T removeu a primeira variável de cada classificação.
# Anexando esse novo objeto aos dados:
= cbind(Wage, Dummies)
Wage_dummy head(Wage_dummy)
year age maritl race education region
231655 2006 18 1. Never Married 1. White 1. < HS Grad 2. Middle Atlantic
86582 2004 24 1. Never Married 1. White 4. College Grad 2. Middle Atlantic
161300 2003 45 2. Married 1. White 3. Some College 2. Middle Atlantic
155159 2003 43 2. Married 3. Asian 4. College Grad 2. Middle Atlantic
11443 2005 50 4. Divorced 1. White 2. HS Grad 2. Middle Atlantic
376662 2008 54 2. Married 1. White 4. College Grad 2. Middle Atlantic
jobclass health health_ins logwage wage
231655 1. Industrial 1. <=Good 2. No 4.318063 75.04315
86582 2. Information 2. >=Very Good 2. No 4.255273 70.47602
161300 1. Industrial 1. <=Good 1. Yes 4.875061 130.98218
155159 2. Information 2. >=Very Good 1. Yes 5.041393 154.68529
11443 2. Information 1. <=Good 1. Yes 4.318063 75.04315
376662 2. Information 2. >=Very Good 1. Yes 4.845098 127.11574
jobclass.2. Information health_ins.2. No
231655 0 1
86582 1 1
161300 0 0
155159 1 0
11443 1 0
376662 1 0
9.8 Variância Zero ou Quase-Zero
Algumas vezes em um conjunto de dados podemos ter uma variável que assuma somente um único valor para todos os indivíduos, ou seja, ela possui variância zero. Ou podemos ter uma com uma frequência muito alta de um único valor, possuindo, assim, variância quase zero. Essas variáveis não auxiliam na predição, pois possuem o mesmo valor em muitos indivíduos, trazendo, assim, pouca informação ao modelo. Nosso objetivo é, então, identificar essas variáveis, chamadas de near zero covariates, para que possamos removê-las do nosso modelo de predição.
Para detectar as near zero covariates, utilizamos a função nearZeroVar() do pacote caret. Vamos verificar se há near zero covariates no banco de dados “forestfires”.
Na função nearZeroVar() passamos primeiro a base de dados a ser analisada, depois o argumento lógico “saveMetrics”, o qual se for “TRUE” retorna todos os detalhes sobre as variáveis da base de dados afim de identificar as near zero covariates. A saída da função fica da seguinte forma:
library(readr)
library(caret)
= read_csv("forestfires.csv")
incendio nearZeroVar(incendio, saveMetrics = T)
freqRatio percentUnique zeroVar nzv
X 1.058140 1.740812 FALSE FALSE
Y 1.624000 1.353965 FALSE FALSE
month 1.069767 2.321083 FALSE FALSE
day 1.117647 1.353965 FALSE FALSE
FFMC 1.000000 20.502901 FALSE FALSE
DMC 1.111111 41.586074 FALSE FALSE
DC 1.111111 42.359768 FALSE FALSE
ISI 1.095238 23.017408 FALSE FALSE
temp 1.000000 37.137331 FALSE FALSE
RH 1.375000 14.506770 FALSE FALSE
wind 1.000000 4.061896 FALSE FALSE
rain 254.500000 1.353965 FALSE TRUE
area 82.333333 48.549323 FALSE FALSE
Note que é retornado uma tabela onde nas linhas se encontram as variáveis da base de dados e as colunas podemos resumir da seguinte forma:
1ª coluna: a Taxa de Frequência de cada variável. Essa taxa é calculada pela razão de frequências do valor mais comum sobre o segundo valor mais comum.
2ª coluna: a Porcentagem de Valores Únicos. Ela é calculada utilizando o número de valores distintos sobre o número de amostras.
3ª coluna: indica se a variável tem variância zero.
4ª coluna: indica se a variável tem variância quase zero.
Podemos observar que a variável “rain” possui variância quase zero, portanto ela é uma near zero covariate.
hist(incendio$rain, main = "Histograma da Variável Rain",
xlab = "Variável Rain", ylab = "Frequência", col = "purple")
Logo, vamos excluir ela da nossa base de dados. O argumento “saveMetrics=FALSE” (default da função) retorna justamente qual(is) variável(is) do bando de dados é(são) near zero covariate .
= nearZeroVar(incendio)
nzv = incendio[,-nzv]
Nova_incendio head(Nova_incendio)
# A tibble: 6 × 12
X Y month day FFMC DMC DC ISI temp RH wind area
<dbl> <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 7 5 mar fri 86.2 26.2 94.3 5.1 8.2 51 6.7 0
2 7 4 oct tue 90.6 35.4 669. 6.7 18 33 0.9 0
3 7 4 oct sat 90.6 43.7 687. 6.7 14.6 33 1.3 0
4 8 6 mar fri 91.7 33.3 77.5 9 8.3 97 4 0
5 8 6 mar sun 89.3 51.3 102. 9.6 11.4 99 1.8 0
6 8 6 aug sun 92.3 85.3 488 14.7 22.2 29 5.4 0
9.9 Análise de Componentes Principais (PCA)
Muitas vezes podemos ter variáveis em excesso no nosso banco de dados, o que torna difícil a manipulação das mesmas. A ideia geral do PCA (Principal Components Analysis) é reduzir a quantidade de variáveis, obtendo combinações interpretáveis delas. O PCA faz isso tranformando um conjunto de observações de variáveis possivelmente correlacionadas num conjunto de valores de variáveis linearmente não correlacionadas, chamadas de componentes principais. Isso é feito utilizando os autovalores e autovetores da matriz de covariâncias da base de dados. O número de componentes principais é, inicialmente, sempre igual ao número de variáveis originais - mas, por conta de suas propriedades matemáticas, é possível selecionar uma quantidade de Componentes Principais muito menor do que a quantidade de variáveis originais da base de dados, enquanto se mantém a mesma (ou muito próximo da mesma) variabilidade original do conjunto de dados.
Efetivamente, se reduz drasticamente a complexidade da base de dados, enquanto se mantém a variabilidade original (não se perde informação).
Para utilizarmos o PCA no nosso modelo, basta colocar o argumento preProcess=“pca” na função train(). Por padrão, são selecionadas componentes que expliquem 95% da variabilidade do conjunto de dados.
Vamos aplicar o método “glm”, com a opção “pca”, no banco de dados spam.
library(caret)
library(kernlab)
data(spam)
# Criando amostras treino/teste.
set.seed(36)
= createDataPartition(spam$type, p=0.75, list=F)
noTreino = spam[noTreino,]
treino = spam[-noTreino,] teste
Agora vamos treinar o nosso modelo com o PCA.
set.seed(887)
= caret::train(type ~ ., method = "glm", preProcess = "pca", data = treino)
modelo
# Aplicando o modelo na amostra TESTE:
= predict(modelo, teste) testePCA
Avaliando nosso modelo com a matriz de confusão:
confusionMatrix(teste$type, testePCA)
Confusion Matrix and Statistics
Reference
Prediction nonspam spam
nonspam 653 44
spam 56 397
Accuracy : 0.913
95% CI : (0.8952, 0.9287)
No Information Rate : 0.6165
P-Value [Acc > NIR] : <2e-16
Kappa : 0.817
Mcnemar's Test P-Value : 0.2713
Sensitivity : 0.9210
Specificity : 0.9002
Pos Pred Value : 0.9369
Neg Pred Value : 0.8764
Prevalence : 0.6165
Detection Rate : 0.5678
Detection Prevalence : 0.6061
Balanced Accuracy : 0.9106
'Positive' Class : nonspam
O modelo obteve uma acurácia de 0,93, o que pode-se considerar uma alta taxa de acerto.
É possível alterar a porcentagem de variância a ser explicada pelos componentes nas opções do train().
Por exemplo, vamos alterar a porcentagem da variância para 60%.
= trainControl(preProcOptions = list(thresh = 0.6))
controle
# Treinando o modelo 2:
set.seed(754)
= caret::train(type ~ ., method = "glm", preProcess = "pca", data = treino, trControl = controle)
modelo2
# Aplicando o modelo 2:
= predict(modelo2, teste) testePCA2
Avaliando o segundo modelo pela matriz de confusão:
confusionMatrix(teste$type,testePCA2)
Confusion Matrix and Statistics
Reference
Prediction nonspam spam
nonspam 647 50
spam 71 382
Accuracy : 0.8948
95% CI : (0.8756, 0.9119)
No Information Rate : 0.6243
P-Value [Acc > NIR] : < 2e-16
Kappa : 0.7778
Mcnemar's Test P-Value : 0.06904
Sensitivity : 0.9011
Specificity : 0.8843
Pos Pred Value : 0.9283
Neg Pred Value : 0.8433
Prevalence : 0.6243
Detection Rate : 0.5626
Detection Prevalence : 0.6061
Balanced Accuracy : 0.8927
'Positive' Class : nonspam
Obtemos uma acurácia de 0,92, o que indica também uma alta taxa de acerto, porém um pouco menor que a do modelo anterior. Note que a sensitividade e a especificidade também diminuíram.
Em geral, são utilizados pontos de corte para a variãncia explicada acima de 0,9.
9.10 PCA fora da função train()
Podemos também realizar o pré-processamento fora da função train(). Primeiramente vamos criar o pré-processamento, utilizando a amostra treino.
= preProcess(treino, method = c("center","scale","pca"), thresh = 0.95) PCA
Obs: pode-se fixar o número de componentes, utilizando o argumento “pcaComp” ao invés de “thresh”.
Agora aplicamos o pré-processamento na amostra treino e realizamos o treinamento, utilizando a amostra treino já pré-processada.
= predict(PCA, treino)
treinoPCA = caret::train(type ~ ., data = treinoPCA, method="glm") modelo
Aplicando o pré-processamento na amostra teste:
= predict(PCA, teste) testePCA
Por último, aplicamos o modelo criado com a amostra treino na amostra teste pré-processada.
= predict(modelo, testePCA)
testeMod
# Avaliando o modelo:
confusionMatrix(testePCA$type, testeMod)
Confusion Matrix and Statistics
Reference
Prediction nonspam spam
nonspam 653 44
spam 56 397
Accuracy : 0.913
95% CI : (0.8952, 0.9287)
No Information Rate : 0.6165
P-Value [Acc > NIR] : <2e-16
Kappa : 0.817
Mcnemar's Test P-Value : 0.2713
Sensitivity : 0.9210
Specificity : 0.9002
Pos Pred Value : 0.9369
Neg Pred Value : 0.8764
Prevalence : 0.6165
Detection Rate : 0.5678
Detection Prevalence : 0.6061
Balanced Accuracy : 0.9106
'Positive' Class : nonspam
9.11 Normalização dos Dados
Normalização de dados consiste em reescalar as variáveis (por exemplo, para um intervalo entre [0, 1] ou padronizando com média zero e desvio padrão unitário). Essa técnica pode reduzir o custo computacional e a complexidade do modelo. Fazer isso traz uma série de propriedades vantajosas quando estamos usando modelos estatísticos, e também para modelos de Aprendizado de Máquinas. Essas propriedades vantajosas se resumem em reduzir custo computacional (pois os números ficam pequenos), reduzir a complexidade dos modelos (pois a simetria conquistada na normalização requer menos parâmetros ou parâmetros menos profundos).
9.11.1 Transformação de Box-Cox
A transformação de Box-Cox é uma transformação feita nos dados (contínuos) para tentar normalizá-los. Considerando \(X_1, X_2, ..., X_n\) as variáveis do conjunto de dados original, essa transformação consiste em encontrar um \(\lambda\) tal que as variáveis transformadas \(Y_1, Y_2, ..., Y_n\) se aproximem de uma distribuição normal com variância constante.
Essa transformação é dada pela seguinte forma: \(Y_i = \frac{X_i^\lambda-1}{\lambda}\), se \(\lambda \neq 0\). O parâmetro \(\lambda\) é estimado utilizando o método de máxima verossimilhança.
O método de Box-Cox é o método mais simples e o mais eficiente computacionalmente. Podemos aplicar a transformação de Box-Cox nos dados através da função preProcess().
Obs: a transformação de Box-Cox só pode ser utilizada com dados positivos.
A transformação de Box-Cox é, por exemplo, muito utilizado no contexto de modelos de Séries Temporais quando a escala da variável resposta é muito grande (muito grande aqui seria já na escala de milhares). A aplicação da transformação é conhecida na literatura por melhorar muito os modelos de Análise de Séries Temporais.
= preProcess(treino, method = "BoxCox") treinoBC
9.11.2 Outras Transformações
9.11.2.1 Transformação de Yeo-Johnson
A transformação de Yeo-Johnson é semelhante à transformação de Box-Cox, porém ela aceita preditores com dados nulos e/ou dados negativos. Também podemos aplicá-la aos dados através da função preProcess().
= preProcess(treino, method = "YeoJohnson") treinoYJ
9.11.2.2 Transformação Exponencial de Manly
O método exponencial de Manly também consiste em estimar um \(\lambda\) tal que as variáveis transformadas se aproximem de uma distribuição normal. Assim como a transformação de Yeo-Johnson, ela também aceita dados positivos, nulos e negativos. Essa transformação é dada pela seguinte forma:
\(Y_i = \frac{e^{X\lambda}-1}{\lambda}\), se $}.
= preProcess(treino, method = "expoTrans") treinoEXP