Este tutorial ensina a utilizar técnicas de transformação e extração de características para melhorar a performance de modelos de predição. Todos os códigos executados estão disponíveis em meu GitHub (não esquece de dar uma estrelinha se você gostar!).

Introdução

Eu resumiria feature engineering em uma só frase: É o que hoje difere o cientista de dados do computador!

É muito comum escutarmos que o trabalho do cientista de dados é 80% manipulação, limpeza e formatação de dados e somente 20% é ajuste de modelos. Mas não devemos tratar isso como algo pejorativo, mas sim como algo necessário! Hoje temos vários exemplos de bibliotecas que ajustam modelos automaticamente: H2O AutoML, TPOT, Auto-Sklearn.

O que cabe ao cientista de dados é a empatia com os dados, algo que não é possível de automatizar. Cabe a ele procurar entender o valor de cada variável, como ela pode ser cruzada com outras informações para gerar mais valor, como ela pode ser formatada para expressar melhor o problema, explicar a existência de valores faltantes, entender o surgimento de valores extremos, se aprofundar tecnicamente no processo que rege cada uma das variáveis.

Feature Engineering é o processo de criação de novas features com o objetivo de enriquecer o conjunto de informações que temos disponíveis a respeito de um problema que queremos resolver. Existem várias formas de gerar novas features, das quais destaco:

  • Dominar muito bem o problema em questão. Essa é sem dúvidas a melhor forma de gerar novas informações e insights, utilizando conhecimentos explícitos e tácitos sobre o problema. Se você trabalha com dados de saúde, torne-se um especialista em saúde!
  • Usar técnicas de transformação de variáveis para gerar uma nova feature a partir de uma feature que se tem em mãos.
  • Usar técnicas de extração de features para gerar uma ou mais features a partir de uma ou mais features que se tem em mãos.

As 3 variáveis mais ricas

São elas:

  • Data
  • Latitude
  • Longitude

Saber quando e onde algo aconteceu! De posse dessas variáveis temos infinitas possibilidades e podemos gerar os mais ricos e variados insights a respeito do problema que temos em mãos.

Exemplos de informações que podemos extrair:

  • Mês,
  • Dia da semana,
  • Período do dia,
  • Bairro,
  • Cidade,
  • Estado,
  • País,
  • Clima/tempo (via API),
  • Altitude do local (API Elevation do Google),
  • Tipos de locais ao redor (API Places do Google)

E por quê gerar novas features?

Para enxergar o que as features orignais não nos permite ver!

Biblitecas Utilizadas

Neste tutorial utilizaremos a linguguem R na versão 3.3.1 e algumas das bibliotecas mais famosas de manipulação, visualização e modelagem de dados. Para checar as versões das bibliotecas utilizadas neste tutorial, acesse o arquivo requirements.txt no repositório do GitHub.

## Load required packages
library(magrittr)    ## For the pipe operator %>% 
library(purrr)       ## For functional programming
library(tidyr)       ## For data cleaning and formating
library(dplyr)       ## For data manipulation
library(ggplot2)     ## For data visualization
library(reshape2)    ## For data reshaping before plot
library(hrbrthemes)  ## For beautiful graph themes
library(plotly)      ## For interactive graphs
library(htmltools)   ## For showing html graphs
library(matrixStats) ## For fast row-wise and column-wise matrix operations
library(dbscan)      ## For the DBSCAN clustering algorithm
library(MASS)        ## For the LDA algorithm
library(h2o)         ## For scaled and fast ml algorithms
library(Rtsne)       ## For the t-SNE algorithm
library(fastknn)     ## For feature extraction with KNN

Exemplo com Dados Sintéticos

Vamos utilizar um exemplo clássico que consiste em um problema de classificação contendo 2 círculos concêntricos.

## Load toy data
load("../data/concentric_circles.rda")
glimpse(concentric.circles)
## Observations: 480
## Variables: 3
## $ x     (dbl) -0.89862678, 0.77766641, 0.25897271, -0.53312726, 0.3426...
## $ y     (dbl) -0.1301759, -0.1940032, -0.7962246, 0.6258241, -0.715352...
## $ class (fctr) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...

Vamos primeiramente visualizar os dados:

## Plot toy data
g <- ggplot(concentric.circles, aes(x, y, shape = class, color = class)) +
   geom_point(alpha = 1, size = 1.5) + 
   scale_shape_manual(name = "Class", values = c(4, 3)) +
   scale_color_manual(name = "Class", values = c('#0C4B8E', '#BF382A')) +
   guides(shape = guide_legend(barwidth = 0.5, barheight = 7)) +
   coord_fixed() +
   labs(x = expression(x[1]), y = expression(x[2])) +
   theme_ipsum(axis_title_size = 12)
plot(g)

À primeira vista, é um problema de separação não-linear, ou seja, um classificador linear não é capaz de resolver o problema. Vamos treinar um modelo GLM Logístico para tentar gerar uma superficie de decisão que possa discernir as duas classes:

## Create training data
dt.train <- concentric.circles

## Create test data
n <- 200   
x <- rep(seq(-1, 1, length = n), times = n)
y <- rep(seq(-1, 1, length = n), each = n)
dt.test <- data_frame(x = x, y = y)

## Train GLM model
glm.model <- glm(data = dt.train, formula = class ~ x + y, family = "binomial")
yhat <- predict(glm.model, dt.test, type = "response")

## Plot decision boundary for test data
g <- data_frame(x1 = x, x2 = y, y = yhat, z = ifelse(y >= 0.5, 1, 0)) %>% 
   ggplot() + 
   geom_tile(aes_string("x1", "x2", fill = "y"), color = NA, size = 0, alpha = 0.8) +
   scale_fill_distiller(name = "Prob +", palette = "Spectral", limits = c(0.38, 0.62)) +
   geom_point(data = dt.train, aes_string("x", "y", shape = "class"), 
              alpha = 1, size = 1.5, color = "black") + 
   geom_contour(aes_string("x1", "x2", z = "z"), color = 'red', alpha = 0.6, 
                size = 0.5, bins = 1) +
   scale_shape_manual(name = "Class", values = c(4, 3)) +
   guides(fill = guide_colorbar(barwidth = 0.5, barheight = 7),
          shape = guide_legend(barwidth = 0.5, barheight = 7)) +
   coord_fixed() +
   labs(x = expression(x[1]), y = expression(x[2])) +
   theme_ipsum(axis_title_size = 12)
plot(g)

Como esperado, o modelo GLM não foi capaz de separar as duas classes. Poderíamos utilizar um classificador não-linear mais complexo para resolver o problema. No entanto, podemos gerar features que possibilitem nosso modelo linear a aprender o problema. Para o problema em questão apenas uma feature é suficiente:

\[ z = x^2 + y^2 \]

## Create a third feature
dt.train <- dt.train %>% 
   mutate(z = x^2 + y^2)
dt.test <- dt.test %>% 
   mutate(z = x^2 + y^2)

## Plot 3d space
p <- dt.train %>%  
   plot_ly(x = ~x, y = ~y, z = ~z, color = ~class, colors = c('#0C4B8E', '#BF382A'), 
           symbol = ~class, symbols = c("x", "cross")) %>%
   add_markers() %>%
   layout(scene = list(
      xaxis = list(title = 'X1'), yaxis = list(title = 'X2'), 
      zaxis = list(title = 'X3 = X1² + X2²')
   ), autosize = TRUE)
tagList(p)

Agora existe um separador linear (no espaço 3D é um plano) que distingue as duas classes:

## Plot 3d surface boundary
p <- plot_ly() %>% 
   add_trace(type = "surface", x = seq(-1, 1, length = n), y = seq(-1, 1, length = n), 
             z = matrix(0.5, ncol = n, nrow = n), colors = c('gray20', 'gray80'), 
             color = c(0, 0.5, 1), showlegend = FALSE, name = "Decision Boundary", 
             surfacecolor = matrix(seq(0, 1, length = 200), ncol = n, nrow = n), 
             opacity = 0.8) %>% 
   add_trace(type = "scatter3d", mode = "markers", x = dt.train$x[dt.train$class==0], 
             y = dt.train$y[dt.train$class==0], z = dt.train$z[dt.train$class==0], 
             marker = list(color = "#0C4B8E", symbol = "x"), name = "0") %>%
   add_trace(type = "scatter3d", mode = "markers", x = dt.train$x[dt.train$class==1], 
             y = dt.train$y[dt.train$class==1], z = dt.train$z[dt.train$class==1], 
             marker = list(color = "#BF382A", symbol = "cross"), name = "1") %>% 
   layout(scene = list(
      xaxis = list(title = 'X1'), yaxis = list(title = 'X2'), 
      zaxis = list(title = 'X3 = X1² + X2²')
   ))
tagList(p)