Predicting Elections

Neste relatório iremos trabalhar conceitos e métodos de classificação com Machine Learning na tarefa de prever se candidatos serão eleitos ou não.

Dados

Os dados são das eleições de 2006 e 2010 (treino) e 2014 (teste).

library(here)
library(dplyr)
library(readr)

train_raw = read.csv(here::here("data/kaggle/elections/train.csv")) %>% na.omit()
test = read.csv(here::here("data/kaggle/elections/test.csv")) %>% na.omit()
#train %>%
#  glimpse()

Pré-processando

Balanceamento de classes

Vamos comparar a frequência de cada classe (eleito/ não eleito) nos dados de treino. O interesse é identificar se há uma classe que é bem mais recorrente do que outra nos dados de treino.

source("../color_pallet.R")
library(ggplot2)

plot_classes = function(data) {
 classes = data %>% 
  select(situacao) %>%
  group_by(situacao) %>%
  summarise(count=n())

  ylim = 7000 
     
  ggplot(classes, aes(x=situacao, y=count, fill=situacao)) +
    geom_bar(stat = "identity") +
    scale_fill_drsimonj(discrete = TRUE, palette = "hot") +
    scale_y_continuous(limits = c(0, ylim))

}

plot_classes(train_raw)

A partir da visualização acima, fica claro que há um desbalanceamento. As classes aparecem desproporcionalmente nos dados de treino (há quase 7 vezes mais observações classificadas como não eleito do que como eleito).
Um dos efeitos colaterais que este desbalanceamento de classes pode causar no classificador é reduzir sua acurácia, visto que o classificador ficará enviesado.
Para tratar este tipo de problema, há algumas abordagens geralmente utilizadas: 1. Alterar a métrica de avaliação modelo de acurácia para precision e recall.
2. Reamostragem do dataset, sendo possível usar alguma das seguintes técnicas: - ROSE: Random Over-Sampling Examples. - SMOTE: Synthetic Minority Over-Sampling Technique; - Oversampling: criação de observações da classe minoritária; - Undersampling: remoção de algumas observações da classe majoritária;

Vamos testar algumas delas.

ROSE

library(ROSE)

opt_rose = ROSE(situacao ~ ., data  = train_raw)$data
plot_classes(opt_rose)

SMOTE

library(DMwR)

opt_smote = SMOTE(situacao ~ ., data  = train_raw)                         
plot_classes(opt_smote)

Oversampling

require(caret)

opt_oversampling = upSample(x = train_raw[,], 
                            y = train_raw$situacao)  

plot_classes(opt_oversampling)

Undersampling

opt_undersampling = 
  downSample(x = train_raw[, -ncol(train_raw)], 
             y = train_raw$situacao)  %>%
  mutate(situacao = Class)

plot_classes(opt_undersampling)

Iremos utilizar a ténica ROSE para conduzir nosso relatório :)

train = opt_rose

Transformando dados

Eliminando algumas variáveis que não serão relevantes para a classificação.

train = train_raw %>% select(-ano, -sequencial_candidato, -nome, -cargo)
test = test %>% select(-ano, sequencial_candidato, -nome, -cargo)
rm(train_raw)

Vamo também transformar algumas variáveis categóricas em variáveis númericas.

genre_cat_to_id = function(m_genre) {
  return(ifelse(m_genre == "MASCULINO", 0, 1))
}

categoric_to_id = function(data) {
  require(dplyr)
  trasnformed = data %>% 
    mutate(sexo = genre_cat_to_id(sexo),
    grau = as.integer(as.factor(grau)),
    estado_civil = as.integer(as.factor(estado_civil)),
    ocupacao = as.integer(as.factor(ocupacao)),
    partido = as.integer(as.factor(partido)),
    uf = as.integer(as.factor(uf)))
  return(trasnformed)
}

train = train %>% categoric_to_id
test = test %>% categoric_to_id 

Dataset de validação

Iremos retirar um subconjunto dos dados de treino e vamos chamá-lo de dados de validação, que usaremos para avaliar resultados de nossos modelos.

## set seed to make partition reproducible
set.seed(123)

create_train_validate = function(dataframe) {
  assignment = sample(
    1:2, size = nrow(dataframe), 
    prob = c(0.8, 0.2), replace = TRUE)

  smp_train = dataframe[assignment == 1, ]
  smp_valid = dataframe[assignment == 2, ]
  
  return(list(smp_train, smp_valid))
}

splitted = create_train_validate(train)
train = splitted[[1]]
validation_set = splitted[[2]]

Treinando Modelos

Vamos treinar alguns modelos para realizar nossa tarefa de classificação. Em todos os modelos iremos usar cross-validation para encontrar os melhores hiperparâmetros de cada modelo.

formula = as.formula(situacao ~ partido + 
                       media_receita + 
                       media_despesa + 
                       ocupacao +
                       quantidade_doadores + 
                       recursos_proprios + 
                       recursos_de_partido_politico + 
                       quantidade_despesas + 
                       quantidade_fornecedores)

KNN

O modeo KNN (k-nearest neighbors ou k vizinhos mais próximos) é um algoritmo que tem como premissa que, dado um conjunto de observações distribuídas como vetores num plano multidimensional (em que cada dimensão corresponde a uma feature das observações), observações próximas tendem a pertencer à mesma classe. O valor K corresponde justamente ao número de vizinhos próximos a se considerar.

library(caret)

n_neighbors_grid = expand.grid(k = 1:20) 
fit_control_knn = trainControl(method="repeatedcv", repeats = 3)

set.seed(123)
model.knn = 
  train(formula,
        data = train,
        method = "knn",
        trControl = fit_control_knn,
        tuneGrid = n_neighbors_grid,
        tuneLength = 20,
        preProcess = c('scale', 'center'),
        metric = "Accuracy",
        na.action = na.omit)
plot(model.knn, print.thres = 0.5, type="S")

Regressão Logística

Na Regressão Logística, ao inveś de buscar-se predizer um valor diretamente, o algoritmo estima uma probabilidade de uma instância pertencer a uma determinada classe. Se para uma instância essa estimativa for maior que 50%, por exemplo, então o modelo prevê que esta instância pertence a essa classe.

require(caret)

fit_control = trainControl(method = "repeatedcv",
                           number = 10,
                           repeats = 5,
                           classProbs = TRUE)

set.seed(123)
model.logistic = train(formula,
                 data = train,
                 method="glm",
                 family="binomial",
                 trControl = fit_control,
                 tuneLength = 20,
                 preProcess = c("scale", "center"),                 
                 na.action = na.omit)

Árvore de Decisão

Esse modelo parrte da noção fundamental da ciência da computação de “dividir e conquistar”. O objetivo da aprendizagem é descobrir quais perguntas fazer, em que ordem perguntar e qual a resposta a prever depois de fazer perguntas suficientes. A árvore de decisão é assim chamada porque podemos escrever nosso conjunto de perguntas e suposições em um formato de árvore. Podemos mapear essas perguntas para as features do nosso conjunto de dados e as repostas para essas perguntas para os valores dessas features. Em um formato possível de uma árvore, por exemplo, cada nó não terminal tem dois filhos: a criança esquerda especifica o que fazer se a resposta à pergunta for “não” e o filho à direita especifica o que fazer se for “sim”. Grosso modo, uma nova instância a ser classificada percorre um caminho na árvore, desde à raiz, respondendo às perguntas, até chegar a um nó que não tiver filhos, e aí então essa instância será classificada de acordo com o rótulo desse nó.

require(caret)
require(rpart)

mFitControl = trainControl(method = "repeatedcv",
                           number = 10,
                           repeats = 10,
                           classProbs = TRUE,
                           summaryFunction = twoClassSummary)

set.seed(123)
model.tree= train(formula,
                 data = train,
                 method = "rpart",
                 trControl = mFitControl,
                 preProcess = c("scale", "center"), 
                 cp = 0.001,  # parâmetro de complexidade
                 maxdepth = 30,
                 metric = "ROC")

Adaboost

model.ada = train(formula,
                  data = train,
                  trControl = trainControl(method = 'cv', number = 5),
                  preProcess = c("scale", "center"),   
                  method = "adaboost")

Avaliando Modelos

Algumas métricas conhecidas para avaliarmos a eficácia de um modelo são: * Accuracy (acurácia) * Precision * Recall

Essas métricas são definidas em termos de Verdadeiros Positivos (TP), Verdadeiros Negativos (TN), Falsos Positivos (FP) e Falsos Negativos (FN).

Acurácia = (TP + TN) / (TP + TN + FP + FN) Nos diz a proporção de observações corretamente classificadas.

Precision = TP / (TP + FP) Diz respeito a quantas das observaçoes preditas como positivas são realmente positivas.

Recall = TP / (TP + FN) Diz respeito a quantas das observaçoes positivas foram corretamente classificadas.

F-measure = 2 * (Precision * Recall) / (Precision + Recall) O F1 score or F-measure é uma média harmônica das métricas precision e recall, quanto mais próximo de 1 o valor de F1 score melhor e quanto mais próximo de 0 pior.

library(pander)

# function to print metrics
print_metrics = function(model, prediction_set) {
  prediction_set$predction = predict(model, prediction_set)
  
  TP = prediction_set %>% filter(situacao == "eleito", predction == "eleito") %>% nrow()
  TN = prediction_set %>% filter(situacao == "nao_eleito" , predction == "nao_eleito" ) %>% nrow()
  FP = prediction_set %>% filter(situacao == "nao_eleito" , predction == "eleito") %>% nrow() 
  FN = prediction_set %>% filter(situacao == "eleito", predction == "nao_eleito" ) %>% nrow()
  
  accuracy = (TP + TN)/(TP + TN + FP + FN) 
  precision = TP / (TP + FP)
  recall = TP / (TP + FN)
  f_measure = 2 * (precision * recall) / (precision + recall)
  
  df = data.frame("Acurácia" = accuracy, 
                  "Precision" = precision, 
                  "Recall" = recall, 
                  "F Measure" = f_measure)
  pander::pander(df)
}

Vamos avaliar cada modelo treinado segundo essas métricas.
Além de avaliar os modelos usando os dados de treino, iremos também avaliá-los usando o conjunto de dados de validação. Isso é importante porque os modelos estão sujeitos a overfitting em relação aos dados com que foram treinados. Portanto é importante avaliá-los também com um conjunto de dados que não foram usados em treino.

KNN

No treino:

print_metrics(model.knn, train)
Acurácia Precision Recall F.Measure
0.9149 0.7012 0.6244 0.6606

Na validação:

print_metrics(model.knn, validation_set)
Acurácia Precision Recall F.Measure
0.9001 0.7105 0.5047 0.5902

É possível perceber que o modelo obteve melhores resultados na validação. Contudo, o modelo precisaria ser melhor refinado. Por exemplo, somente quase 75% das observações realmente positivas seriam classificadas como positivas.

Regressão Logística

No treino:

print_metrics(model.logistic, train)
Acurácia Precision Recall F.Measure
0.9 0.7128 0.4126 0.5226

Na validação:

print_metrics(model.logistic, validation_set)
Acurácia Precision Recall F.Measure
0.8808 0.6733 0.3178 0.4317

Aqui também a validação obteve melhores resultados. Mas o modelo foi inferior ao anterior (KNN). Portanto, poderia ser melhor refinado.

Ávore de Decisão

No treino:

print_metrics(model.tree, train)
Acurácia Precision Recall F.Measure
0.9003 0.7577 0.3658 0.4934

Na validação:

print_metrics(model.tree, validation_set)
Acurácia Precision Recall F.Measure
0.8842 0.7381 0.2897 0.4161

Note o baixo valor para F Measure e para Recall. Aqui teremos um classificador muito ruim.

Adaboost

Na validação:

print_metrics(model.ada, validation_set)
Acurácia Precision Recall F.Measure
0.9081 0.7317 0.5607 0.6349

No treino:

print_metrics(model.ada, train)
Acurácia Precision Recall F.Measure
1 1 1 1

Esse modelo foi o que obteve o melhor resultado até então, tanto no treino quanto na validação. Note a diferença dos resultados entre treino e validação.

Interpretando modelos

Quais foram as features mais importantes para cada modelo?

A função varImp retornar scores numa escala entre 100 (muita importância) e 0 (pouca importância).

KNN

varImp(model.knn)
## ROC curve variable importance
## 
##                              Importance
## quantidade_fornecedores          100.00
## quantidade_despesas               99.76
## quantidade_doadores               94.41
## media_receita                     90.00
## media_despesa                     66.44
## recursos_proprios                 54.27
## recursos_de_partido_politico      29.08
## ocupacao                          18.26
## partido                            0.00

Regressão Logística

varImp(model.logistic, scale=FALSE)
## glm variable importance
## 
##                              Overall
## quantidade_doadores          15.5382
## media_receita                 9.0414
## media_despesa                 7.1620
## ocupacao                      7.0316
## recursos_proprios             5.7392
## quantidade_fornecedores       4.5098
## partido                       3.2009
## quantidade_despesas           2.2360
## recursos_de_partido_politico  0.9838

Aqui é notável o fato das features terem tido pouca importância para a classificação. Para esse modelo, portanto, poderíamos ter criado novas features baseada nas já existente e investigar se houve melhora nas métricas do modelo (lembrando que este modelo teve uma das piores performances, de acordo com nossas méticas da secção anterior).

Árvore de Decisão

varImp(model.tree)
## rpart variable importance
## 
##                              Overall
## quantidade_fornecedores       100.00
## quantidade_despesas            99.22
## media_receita                  82.10
## quantidade_doadores            79.51
## recursos_de_partido_politico   54.13
## ocupacao                       16.50
## media_despesa                  12.45
## partido                         0.00
## recursos_proprios               0.00

Adaboost

varImp(model.ada)
## ROC curve variable importance
## 
##                              Importance
## quantidade_fornecedores          100.00
## quantidade_despesas               99.76
## quantidade_doadores               94.41
## media_receita                     90.00
## media_despesa                     66.44
## recursos_proprios                 54.27
## recursos_de_partido_politico      29.08
## ocupacao                          18.26
## partido                            0.00

Outros modelos

Random Forest

Para tentar melhorar os resultados, decidi utilizar Random forest, um método bastante utilizado e popular em desafios e na literatura. Neste método, vários modelos diferentes são criado, sendo todos eles porém simples. A ideia é unir diversos classificadores simples para construir uma boa árvore de decisão.

Treino

library(randomForest)
## randomForest 4.6-14
## Type rfNews() to see new features/changes/bug fixes.
## 
## Attaching package: 'randomForest'
## The following object is masked from 'package:ggplot2':
## 
##     margin
## The following object is masked from 'package:dplyr':
## 
##     combine
model.random.forest = randomForest(formula,
                                   data=train,
                                   importance=TRUE,
                                   ntree=2000)

Avaliando modelo

Na validação:

print_metrics(model.random.forest, validation_set)
Acurácia Precision Recall F.Measure
0.9141 0.7485 0.5981 0.6649

No treino:

print_metrics(model.random.forest, train)
Acurácia Precision Recall F.Measure
1 1 1 1

Interpretando ouput

model.random.forest
## 
## Call:
##  randomForest(formula = formula, data = train, importance = TRUE,      ntree = 2000) 
##                Type of random forest: classification
##                      Number of trees: 2000
## No. of variables tried at each split: 3
## 
##         OOB estimate of  error rate: 8.56%
## Confusion matrix:
##            eleito nao_eleito class.error
## eleito        525        287  0.35344828
## nao_eleito    237       5071  0.04464959

Submetendo predição no Kaggle

write_output = function(model, test_set, output_file_name) {
  test_set$predction = predict(model, test_set)
  predictions = test_set %>% 
    select(sequencial_candidato, predction) %>%
    mutate(Id = sequencial_candidato,
           Predicted = predction) %>%
    select(Predicted, Id)
  
  write.csv(predictions, file = output_file_name, row.names = F)
}
write_output(model.knn, test, "knn_predictions.csv")
write_output(model.logistic, test, "logistic_predictions.csv")
write_output(model.tree, test, "tree_predictions.csv")
write_output(model.ada, test, "ada_predictions.csv")

library(caret)
random_forest_pred = predict(model.random.forest, test, type='class')
write_output(model.random.forest, test, "random_forest_predictions.csv")

Italo Batista

28 de novembro de 2018