Gerar bons modelos de treinamento: pré-processamento de dados

A qualidade dos dados e a quantidade de informação útil que contém são fatores chaves que determinam o bem que pode aprender um modelo de aprendizado de máquina. Portanto, é imprescindível ter certeza de examinar e pré-processar um conjunto de dados antes de inserí-lo em um algoritmo de aprendizado.

Trataremos, técnicas fundamentais para o pré-processamento dos dados, que nos ajudarão a gerar bons modelos de aprendizado de máquina.

Os objetivos que abordaremos nesta fase de pré-processamento são:

  1. Remover variáveis que não ajudam na predição:

    • Variância zero ou quase zero.
    • Variáveis com alta correlação.
    • Variáveis com dependência linear.
  2. Transformar as variáveis quantitativas

    • Padronização.
    • Normalização.
  3. Remover ou imputar dados faltantes.

  4. Codificar variáveis qualitativas e reduzir a dimensionalidade.

    • Variáveis dummy.
    • Análise de Componentes Principais (ACP).

Variância zero ou quase-zero

Objetivo: Remover variáveis com um único valor (variância zero) ou com uma frequência muito alta de um único valor (variância quase zero).

  • Para identificar variáveis preditoras com variância quase-zero, utilizamos a função nearZeroVar() do pacote caret. A função nearZeroVar() calcula as seguintes duas métricas:

    • A razão do valor mais frequente e o segundo valor mais frequente (razão de frequência, do inglês “frequency ratio”) e
    • O percentual de valores únicos definido como o número de valores únicos dividido pelo total de amostras \(\times 100\).

Critério de decisão nzv :

\[\text{Razão de Frequência} = \frac{\text{Frequência do valor mais comum}}{\text{Frequência do segundo valor mais comum}} > \text{freqCut}\] e

\[\text{Percentual de Valores Únicos} = \left( \frac{\text{número de valores distintos }}{\text{tamanho da amostra}}\right) \times 100 < \text{uniqueCut}.\] Então, a variável é classificada como nvz.

A ideia é que valores altos da razão de frequências indicam desbalanceamento extremo e valores muito baixos do percentual de valores únicos indicam falta de variabilidade.

Os pontos de corte por defeito da função nearZeroVar() são: freqCut = 95/5 e uniqueCut = 10.

Para ilustração das técnicas de pré-processamento usaremos o wdbc dataset.

# Carregando librarias
library(tidyverse)
library(caret)
# Link para o banco de dados
url <- "https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data"

# Lendo o banco de dados diretamente do link 
wdbc <- read_csv(url, col_names = FALSE)

# Removendo a coluna de ID
wdbc <- wdbc[,-1]

# Adicionando nomes das colunas (são 31 no total)
features <- c("Diagnosis",
              paste0(rep(c("radius", "texture", "perimeter", "area", "smoothness",
                           "compactness", "concavity", "concave_points", "symmetry", "fractal_dimension"), 3),
                     "_", rep(c("mean", "se", "worst"), each = 10)))


colnames(wdbc) <- features

# Substituir zeros por 0.001 para evitar problemas com log
wdbc[wdbc == 0] <- 0.001

# Transformando para log as variáveis preditoras
wdbc_log <- data.frame(Diagnosis = as.factor(wdbc$Diagnosis),
                       apply(wdbc[,-1],2,log))

glimpse(wdbc_log)
## Rows: 569
## Columns: 31
## $ Diagnosis               <fct> M, M, M, M, M, M, M, M, M, M, M, M, M, M, M, M…
## $ radius_mean             <dbl> 2.889816, 3.023834, 2.980111, 2.435366, 3.0101…
## $ texture_mean            <dbl> 2.339881, 2.877512, 3.056357, 3.014554, 2.6630…
## $ perimeter_mean          <dbl> 4.810557, 4.889597, 4.867534, 4.351310, 4.9060…
## $ area_mean               <dbl> 6.908755, 7.189922, 7.092574, 5.956096, 7.1678…
## $ smoothness_mean         <dbl> -2.133687, -2.468168, -2.210918, -1.948413, -2…
## $ compactness_mean        <dbl> -1.281574, -2.542875, -1.833207, -1.259133, -2…
## $ concavity_mean          <dbl> -1.203640, -2.442997, -1.622523, -1.421300, -1…
## $ concave_points_mean     <dbl> -1.916643, -2.656834, -2.056507, -2.251892, -2…
## $ symmetry_mean           <dbl> -1.419231, -1.708154, -1.575520, -1.348228, -1…
## $ fractal_dimension_mean  <dbl> -2.541985, -2.870510, -2.813577, -2.328518, -2…
## $ radius_se               <dbl> 0.09075436, -0.60972557, -0.29356602, -0.70198…
## $ texture_se              <dbl> -0.099488899, -0.309382499, -0.239654103, 0.14…
## $ perimeter_se            <dbl> 2.1504823, 1.2231870, 1.5227901, 1.2369239, 1.…
## $ area_se                 <dbl> 5.033049, 4.305146, 4.543614, 3.304319, 4.5479…
## $ smoothness_se           <dbl> -5.051614, -5.254300, -5.091303, -4.698383, -4…
## $ compactness_se          <dbl> -3.015119, -4.336671, -3.217377, -2.595883, -3…
## $ concavity_se            <dbl> -2.923784, -3.984594, -3.261783, -2.871570, -2…
## $ concave_points_se       <dbl> -4.143325, -4.312501, -3.883436, -3.980837, -3…
## $ symmetry_se             <dbl> -3.505558, -4.276586, -3.794240, -2.819596, -4…
## $ fractal_dimension_se    <dbl> -5.084336, -5.645891, -5.388023, -4.687683, -5…
## $ radius_worst            <dbl> 3.233961, 3.218476, 3.159975, 2.702032, 3.1152…
## $ texture_worst           <dbl> 2.852439, 3.153163, 3.239854, 3.277145, 2.8136…
## $ perimeter_worst         <dbl> 5.218191, 5.067646, 5.027165, 4.593806, 5.0251…
## $ area_worst              <dbl> 7.610358, 7.578657, 7.443664, 6.341593, 7.3620…
## $ smoothness_worst        <dbl> -1.818925, -2.089088, -1.935168, -1.561601, -1…
## $ compactness_worst       <dbl> -0.40706639, -1.67878799, -0.85684327, -0.1435…
## $ concavity_worst         <dbl> -0.33981783, -1.42047181, -0.79761920, -0.3755…
## $ concave_points_worst    <dbl> -1.326517, -1.682009, -1.414694, -1.356736, -1…
## $ symmetry_worst          <dbl> -0.7763114, -1.2909842, -1.0180466, -0.4097744…
## $ fractal_dimension_worst <dbl> -2.129472, -2.418894, -2.435203, -1.754464, -2…
# Examinando preditoras com variância zero ou quase-zero
X <- wdbc_log[,-1]
nearZeroVar(X, saveMetrics = TRUE)
##                         freqRatio percentUnique zeroVar   nzv
## radius_mean              1.333333      80.14060   FALSE FALSE
## texture_mean             1.000000      84.18278   FALSE FALSE
## perimeter_mean           1.000000      91.73989   FALSE FALSE
## area_mean                1.500000      94.72759   FALSE FALSE
## smoothness_mean          1.250000      83.30404   FALSE FALSE
## compactness_mean         1.000000      94.37610   FALSE FALSE
## concavity_mean           4.333333      94.37610   FALSE FALSE
## concave_points_mean      4.333333      95.25483   FALSE FALSE
## symmetry_mean            1.000000      75.92267   FALSE FALSE
## fractal_dimension_mean   1.000000      87.69772   FALSE FALSE
## radius_se                1.000000      94.90334   FALSE FALSE
## texture_se               1.000000      91.21265   FALSE FALSE
## perimeter_se             2.000000      93.67311   FALSE FALSE
## area_se                  1.000000      92.79438   FALSE FALSE
## smoothness_se            1.000000      96.13357   FALSE FALSE
## compactness_se           1.000000      95.07909   FALSE FALSE
## concavity_se             6.500000      93.67311   FALSE FALSE
## concave_points_se        4.333333      89.10369   FALSE FALSE
## symmetry_se              1.333333      87.52197   FALSE FALSE
## fractal_dimension_se     1.000000      95.78207   FALSE FALSE
## radius_worst             1.250000      80.31634   FALSE FALSE
## texture_worst            1.000000      89.80668   FALSE FALSE
## perimeter_worst          1.000000      90.33392   FALSE FALSE
## area_worst               1.000000      95.60633   FALSE FALSE
## smoothness_worst         1.000000      72.23199   FALSE FALSE
## compactness_worst        1.000000      92.97012   FALSE FALSE
## concavity_worst          4.333333      94.72759   FALSE FALSE
## concave_points_worst     4.333333      86.46749   FALSE FALSE
## symmetry_worst           1.000000      87.87346   FALSE FALSE
## fractal_dimension_worst  1.500000      94.02460   FALSE FALSE

Neste caso, não temos nenhuma variável com variância zero o quase-zero, então não há necessidade de remoção.

Para alterar os pontos de corte podemos usar a função nearZeroVar() com as opções freqCut e uniqueCut como segue:

# "nonsense example" alterar freqCut = 1 e uniqueCut = 85
nearZeroVar(X, 
            saveMetrics = TRUE,
            names = TRUE, 
            freqCut = 1, 
            uniqueCut = 85)
##                         freqRatio percentUnique zeroVar   nzv
## radius_mean              1.333333      80.14060   FALSE  TRUE
## texture_mean             1.000000      84.18278   FALSE FALSE
## perimeter_mean           1.000000      91.73989   FALSE FALSE
## area_mean                1.500000      94.72759   FALSE FALSE
## smoothness_mean          1.250000      83.30404   FALSE  TRUE
## compactness_mean         1.000000      94.37610   FALSE FALSE
## concavity_mean           4.333333      94.37610   FALSE FALSE
## concave_points_mean      4.333333      95.25483   FALSE FALSE
## symmetry_mean            1.000000      75.92267   FALSE FALSE
## fractal_dimension_mean   1.000000      87.69772   FALSE FALSE
## radius_se                1.000000      94.90334   FALSE FALSE
## texture_se               1.000000      91.21265   FALSE FALSE
## perimeter_se             2.000000      93.67311   FALSE FALSE
## area_se                  1.000000      92.79438   FALSE FALSE
## smoothness_se            1.000000      96.13357   FALSE FALSE
## compactness_se           1.000000      95.07909   FALSE FALSE
## concavity_se             6.500000      93.67311   FALSE FALSE
## concave_points_se        4.333333      89.10369   FALSE FALSE
## symmetry_se              1.333333      87.52197   FALSE FALSE
## fractal_dimension_se     1.000000      95.78207   FALSE FALSE
## radius_worst             1.250000      80.31634   FALSE  TRUE
## texture_worst            1.000000      89.80668   FALSE FALSE
## perimeter_worst          1.000000      90.33392   FALSE FALSE
## area_worst               1.000000      95.60633   FALSE FALSE
## smoothness_worst         1.000000      72.23199   FALSE FALSE
## compactness_worst        1.000000      92.97012   FALSE FALSE
## concavity_worst          4.333333      94.72759   FALSE FALSE
## concave_points_worst     4.333333      86.46749   FALSE FALSE
## symmetry_worst           1.000000      87.87346   FALSE FALSE
## fractal_dimension_worst  1.500000      94.02460   FALSE FALSE

Se quisessemos remover as variáveis “nzv” podemos proceder como segue:

# ATENÇÃO:
# caso SOMENTE usado para ilustrar o 
# funcionamento da função nearZeroVar com
# limiares alterados

nzv <- nearZeroVar(X, 
            saveMetrics = FALSE,
            freqCut = 1, 
            uniqueCut = 85)
nzv
## [1]  1  5 21
X_sem_nvc <- X[, -nzv]
glimpse(X_sem_nvc)
## Rows: 569
## Columns: 27
## $ texture_mean            <dbl> 2.339881, 2.877512, 3.056357, 3.014554, 2.6630…
## $ perimeter_mean          <dbl> 4.810557, 4.889597, 4.867534, 4.351310, 4.9060…
## $ area_mean               <dbl> 6.908755, 7.189922, 7.092574, 5.956096, 7.1678…
## $ compactness_mean        <dbl> -1.281574, -2.542875, -1.833207, -1.259133, -2…
## $ concavity_mean          <dbl> -1.203640, -2.442997, -1.622523, -1.421300, -1…
## $ concave_points_mean     <dbl> -1.916643, -2.656834, -2.056507, -2.251892, -2…
## $ symmetry_mean           <dbl> -1.419231, -1.708154, -1.575520, -1.348228, -1…
## $ fractal_dimension_mean  <dbl> -2.541985, -2.870510, -2.813577, -2.328518, -2…
## $ radius_se               <dbl> 0.09075436, -0.60972557, -0.29356602, -0.70198…
## $ texture_se              <dbl> -0.099488899, -0.309382499, -0.239654103, 0.14…
## $ perimeter_se            <dbl> 2.1504823, 1.2231870, 1.5227901, 1.2369239, 1.…
## $ area_se                 <dbl> 5.033049, 4.305146, 4.543614, 3.304319, 4.5479…
## $ smoothness_se           <dbl> -5.051614, -5.254300, -5.091303, -4.698383, -4…
## $ compactness_se          <dbl> -3.015119, -4.336671, -3.217377, -2.595883, -3…
## $ concavity_se            <dbl> -2.923784, -3.984594, -3.261783, -2.871570, -2…
## $ concave_points_se       <dbl> -4.143325, -4.312501, -3.883436, -3.980837, -3…
## $ symmetry_se             <dbl> -3.505558, -4.276586, -3.794240, -2.819596, -4…
## $ fractal_dimension_se    <dbl> -5.084336, -5.645891, -5.388023, -4.687683, -5…
## $ texture_worst           <dbl> 2.852439, 3.153163, 3.239854, 3.277145, 2.8136…
## $ perimeter_worst         <dbl> 5.218191, 5.067646, 5.027165, 4.593806, 5.0251…
## $ area_worst              <dbl> 7.610358, 7.578657, 7.443664, 6.341593, 7.3620…
## $ smoothness_worst        <dbl> -1.818925, -2.089088, -1.935168, -1.561601, -1…
## $ compactness_worst       <dbl> -0.40706639, -1.67878799, -0.85684327, -0.1435…
## $ concavity_worst         <dbl> -0.33981783, -1.42047181, -0.79761920, -0.3755…
## $ concave_points_worst    <dbl> -1.326517, -1.682009, -1.414694, -1.356736, -1…
## $ symmetry_worst          <dbl> -0.7763114, -1.2909842, -1.0180466, -0.4097744…
## $ fractal_dimension_worst <dbl> -2.129472, -2.418894, -2.435203, -1.754464, -2…

Alta correlação

Objetivo: Remover variáveis preditoras com alta correlação.

Para avaliarmos a presença de variáveis preditoras com alta correlação podemos calcular a matriz de correlações \(R = (r_{ij})\), em que \(r_{ij} = Cor(X_i,X_j)\) é o coeficiente de correlação amostral entre \(X_i\) e \(X_j\).

# Matriz de correlação das preditoras
R_X <- cor(X)
summary(R_X[lower.tri(R_X)])
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
## -0.3645  0.1649  0.3635  0.3711  0.5808  0.9996
# Procurando por correlações quase-perfeitas
highCorr <- sum(abs(R_X[lower.tri(R_X)]) > 0.999)
highCorr
## [1] 2
findCorrelation(R_X, cutoff = 0.999, names = TRUE)
## [1] "radius_worst" "radius_mean"

Podemos observar que existem duas variáveis que estão quase-perfeitamente correlacionadas, a saber: radius_worst e radius_mean.

Vamos observar o efeito de remover preditoras com correlações absolutas acima de 75%.

# Preditoras altamente correlacionadas
highlyCorrPred <- findCorrelation(R_X, cutoff = 0.75)

# Eliminando da base as variáveis altamente correlacionadas
X2 <- X[,-highlyCorrPred]
colnames(X2)
##  [1] "area_mean"              "symmetry_mean"          "fractal_dimension_mean"
##  [4] "radius_se"              "texture_se"             "smoothness_se"         
##  [7] "symmetry_se"            "fractal_dimension_se"   "texture_worst"         
## [10] "smoothness_worst"       "symmetry_worst"
# Calculando matriz de correlações para os dados filtrados (sem pred. com alta correlação)
R_X2 <- cor(X2) 

# Medidas resumo das correlações  
summary(R_X2[lower.tri(R_X2)])
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
## -0.36448  0.07744  0.24420  0.22407  0.40629  0.71856

Pré-processamento dentro da função train()

set.seed(12345)
index_train <- createDataPartition(y = wdbc_log$Diagnosis, p = 0.8)[[1]]
training <- wdbc_log[index_train,]
testing <- wdbc_log[-index_train,]

ctrl <- trainControl(preProcOptions = list(cutoff = 0.75))

model_svm <- train(Diagnosis ~ .,
                   data = training,
                   method = "svmLinear",
                   trControl = ctrl,
                   preProcess = c("nzv","corr"))

# Preditoras removidas com alta correlação
model_svm$preProcess$method$remove
##  [1] "compactness_mean"        "concave_points_mean"    
##  [3] "concavity_mean"          "perimeter_worst"        
##  [5] "compactness_worst"       "radius_worst"           
##  [7] "concavity_worst"         "area_worst"             
##  [9] "concave_points_worst"    "perimeter_mean"         
## [11] "area_se"                 "compactness_se"         
## [13] "concavity_se"            "radius_mean"            
## [15] "perimeter_se"            "smoothness_mean"        
## [17] "fractal_dimension_worst" "texture_mean"
# Aplicamos na amostra teste
predictions <- predict(model_svm,testing)
# Avaliando o desempenho
confusionMatrix(predictions, testing$Diagnosis, positive = "M")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  B  M
##          B 68  1
##          M  3 41
##                                           
##                Accuracy : 0.9646          
##                  95% CI : (0.9118, 0.9903)
##     No Information Rate : 0.6283          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9249          
##                                           
##  Mcnemar's Test P-Value : 0.6171          
##                                           
##             Sensitivity : 0.9762          
##             Specificity : 0.9577          
##          Pos Pred Value : 0.9318          
##          Neg Pred Value : 0.9855          
##              Prevalence : 0.3717          
##          Detection Rate : 0.3628          
##    Detection Prevalence : 0.3894          
##       Balanced Accuracy : 0.9670          
##                                           
##        'Positive' Class : M               
## 

Dependência Linear

Objetivo: Remover variáveis preditoras linearmente dependentes.

Para ilustração considere a seguinte matriz de delineamento de um experimento com posto incompleto.

# Matriz de delineamento de posto incompleto
m.Design <- matrix(0, nrow=6, ncol=6)
m.Design[,1] <- c(1, 1, 1, 1, 1, 1)
m.Design[,2] <- c(1, 1, 1, 0, 0, 0)
m.Design[,3] <- c(0, 0, 0, 1, 1, 1)
m.Design[,4] <- c(1, 0, 0, 1, 0, 0)
m.Design[,5] <- c(0, 1, 0, 0, 1, 0)
m.Design[,6] <- c(0, 0, 1, 0, 0, 1)
m.Design
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,]    1    1    0    1    0    0
## [2,]    1    1    0    0    1    0
## [3,]    1    1    0    0    0    1
## [4,]    1    0    1    1    0    0
## [5,]    1    0    1    0    1    0
## [6,]    1    0    1    0    0    1

Se denotamos por \(\mathbf{c}^{(j)}\) a \(j\)-ésima coluna da matriz m.Design, então

\[\begin{align}\mathbf{c}^{(3)} &= \mathbf{c}^{(1)} - \mathbf{c}^{(2)}\\ \mathbf{c}^{(6)} &= \mathbf{c}^{(1)} - \mathbf{c}^{(4)} - \mathbf{c}^{(5)}, \end{align}\]

ou seja a terceira e última coluna da matriz m.Design são combinações lineares das outras. Portanto, podem ser removidas. Para identificar as combinações lineares dentro de uma matriz podemos usar a função findLinearCombos como segue:

comboInfo <- findLinearCombos(m.Design)
comboInfo
## $linearCombos
## $linearCombos[[1]]
## [1] 3 1 2
## 
## $linearCombos[[2]]
## [1] 6 1 4 5
## 
## 
## $remove
## [1] 3 6

A matriz de delineamento com as colunas removidas pode ser obtiva por meio de:

m.Design[,-comboInfo$remove]
##      [,1] [,2] [,3] [,4]
## [1,]    1    1    1    0
## [2,]    1    1    0    1
## [3,]    1    1    0    0
## [4,]    1    0    1    0
## [5,]    1    0    0    1
## [6,]    1    0    0    0

Uso da função preProcess() para o pré-processamento

O pré-processamento dos dados pode ser feito:

  • usando as funções próprias: nearZeroVar(), findCorrelation(), etc.

  • dentro da função train().

  • usando a função caret::preProcess()

Veremos a continução como usar a função caret::preProcess()

# Criando um pré-processamento para os dados de treino
preProcValues <- preProcess(training, method = c("nzv","corr"), cutoff = 0.75)

# Aplicando o pré-processamento aos dados de treino
trainTrandformed <- predict(preProcValues, training)

# Aplicando o pré-processamento aos dados de teste
testTrandformed <- predict(preProcValues, testing)

Observações

  • O mesmo pré-processamento aplicado aos dados de treinamenteo deve ser aplicado aos dados de teste.

  • O uso da função caret::preProcess() permite realizar pré-processamento mais customizados para certos algoritmos de aprendizado.