Predicting Elections
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_roseTransformando 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")