Através de uma amostra de emails, será utilizado um modelo de regressão logística para classificar se um e-mail é spam ou não. A ideia é treinar um modelo através de um conjunto de observações e por fim, submeter um dataset para teste onde o modelo classificará os emails. O dataset utilizado possui 3921 observações (emails) e 21 colunas (variáveis). As variáveis envolvidas são:
Inicialmente, foi investigado quais variáveis são mais relevantes para explicar se um email é spam ou não. Através da análise de histogramas, foi identificado o comportamento das variáveis onde algumas não mostraram nenhuma variação relevante para ambos os casos (spam ou não-spam). Essas variáveis foram descartadas do modelo, sendo utilizadas apenas aquelas que possuiram algum tipo de variação relevante.
ggplot(data=spams, aes(x=from)) + geom_histogram(binwidth=0.2) + theme_bw() +
ggtitle("Histograma para emails SPAM")
ggplot(data=nao_spams, aes(x=from)) + geom_histogram(bindwidth=0.2) + theme_bw() +
ggtitle("Histograma para emails NÃO SPAM")
Como pode ser observado, para ambas as classes (spam e não-spam), a variável from não sofre consideráveis variações nos conjuntos, fazendo com que esta não fosse utilizada no modelo. As variáveis que seguem o mesmo comportamento também foram descartadas.
ggplot(data=spams, aes(x=format)) + geom_histogram(binwidth=0.2) + theme_bw() +
ggtitle("Histograma para emails SPAM")
ggplot(data=nao_spams, aes(x=format)) + geom_histogram(binwidth=0.2) + theme_bw() +
ggtitle("Histograma para emails NÃO SPAM")
Já no caso acima, a variável format possui consideráveis variações para ambas as classes, fazendo com que esta fosse incluída no treino do modelo. Variáveis que tiveram um comportamento similar também foram incluídas.
Após vários testes e análises de histogramas, as variáveis escolhidas para treinar o modelo foram: format, re_subj e number.
setTrain <- email[1:((2/3)*nrow(email)),]
setTest <- email[((2/3)*nrow(email)+1):nrow(email),]
#Montagem dos dados de treino utilizando metade de emails spam e
#outra metade não spam para o treino, afim de balancear os dados utilizados no treino.
train_spam <- setTrain %>% filter(spam == 1)
train_no_spam <- setTrain %>% filter(spam == 0)
train_no_spam <- train_no_spam[sample(nrow(train_spam)),]
train_sample <- rbind(train_spam, train_no_spam)
train_sample <- train_sample[sample(nrow(train_sample)),]
Geração de um modelo de regressão logística a partir dos dados de treino.
mod <- glm(spam ~ format + re_subj + number, data=train_sample, family="binomial")
summary(mod)
##
## Call:
## glm(formula = spam ~ format + re_subj + number, family = "binomial",
## data = train_sample)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.1755 -0.9474 0.1038 0.9883 2.6804
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 1.1210 0.3258 3.441 0.000579 ***
## format -1.0310 0.2332 -4.421 9.84e-06 ***
## re_subj -2.9959 0.5451 -5.496 3.87e-08 ***
## numbernone 1.1469 0.3999 2.868 0.004132 **
## numbersmall -0.6584 0.3055 -2.155 0.031131 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 657.10 on 473 degrees of freedom
## Residual deviance: 486.67 on 469 degrees of freedom
## AIC: 496.67
##
## Number of Fisher Scoring iterations: 5
O sumário mostra a significância das variáveis para o modelo de acordo com om p_valor computado. É possível observar que as variáveis format e re_subj possuem alguma significância para o modelo. Para a variável number, em relação a classe big, a classe none possui uma significância média, enquanto que a classe small possui baixa significância.
Realização da predição para os dados de teste.
predictions <- predict(mod, type = "response", newdata=setTest) > .8
true_spams <- setTest$spam == 1
res <- table(predictions, true_spams)
res
## true_spams
## predictions FALSE TRUE
## FALSE 1105 94
## TRUE 72 36
Através da tabela acima, é possível verificar o número de acertos e erros do modelo na predição. O modelo acerta muito mais quando um email não é spam (o que já era esperado, pois a amostra possui mais de 10 vezes emails que não são spam, fazendo com que esta classe seja a mais comum em ocorrências). Já na identificação de emails spam, o modelo erra quase o dobro do que acertou (36 acertos e 72 erros). Isso se deve ao fato de que o modelo não possui variáveis suficientes que expliquem mais precisamente sobre um email ser spam ou não.
A seguir, um gráfico de mosaico ajuda a visualizar os acertos e erros do modelo.
require(vcd)
mosaic(table(predictions, true_spams))
erro <- sum((predictions != true_spams)) / NROW(predictions)
erro
## [1] 0.1270084
A taxa de erro das predições parece ser baixa (em torno de 12%), mostrando que o modelo acertou muito em suas predições. Entretanto, por conta do número de emails não-spam ser bem superior ao de spams nos dados de teste, o modelo acerta muito ao classificar os emails não-spam. Caso os dados de teste fossem mais balanceados (50% spams e 50% não-spams), talvez a taxa de erro encontrada fosse maior, pois o modelo errou muito mais ao classificar um email como spam, ou seja, houveram muitos falsos positivos.
O exemplo a seguir ilustra essa ideia:
spam_test <- setTest %>% filter(spam == 1)
not_spam_test <- setTest %>% filter(spam == 0)
not_spam_test <- not_spam_test[sample(nrow(spam_test)),]
spam_test <- rbind(spam_test, not_spam_test)
spam_test <- spam_test[sample(nrow(spam_test)),]
predictions_test <- predict(mod, type = "response", newdata=spam_test) > .8
true_spams_test <- spam_test$spam == 1
res_test <- table(predictions_test, true_spams_test)
res_test
## true_spams_test
## predictions_test FALSE TRUE
## FALSE 122 94
## TRUE 8 36
erro_test <- sum((predictions_test != true_spams_test)) / NROW(predictions_test)
erro_test
## [1] 0.3923077
Neste caso, foi utilizado o mesmo modelo treinado anteriormente. A diferença aparece nos dados de teste, onde o conjunto utilizado possui a mesma quantidade de emails spam e não-spam (124 cada, totalizando 248 observações). Uma ponto que pode ser relevante é o de que este dataset de teste é bem menor do que o utilizado anteriormente. Ao analisar a tabela, é possível observar que desta vez a predição acertou muito mais ao classificar emails como spam. Por outro lado, muitos emails que são spam foram classificados como não-spam (falsos negativos), acarretando um maior número de erros e consequentemente, uma maior taxa de erros. Vale lembrar que ao realizar a predição para o exemplo acima, além de ter sido utilizado o mesmo modelo treinado anteriormente, foi utilizado o mesmo valor mínimo de probabilidade para considerar uma observação como spam na predição (0.8), que indica que o modelo só classifica um email como spam caso tenha muita certeza (mais de 0.8). Sendo assim, é possível alterar esse fator para que os resultados obtidos na predição sejam melhorados ou piorados. Também é notório o aumento na taxa de erro, chegando perto de 40%.
A seguir, foi feito o cálculo de predições alterando o valor da probabilidade que indica a partir de qual valor uma observação será classificada como TRUE (é spam) ou FALSE (não é spam). Os valores utilizados são números decimais entre 0 e 1, com uma precisão de duas casas decimais após a vírgula. Em seguida, através do cálculo do erro, foi utilizada a taxa de acerto (1 - erro) para ajudar a identificar o quão boa foi a predição. A variável score se mostrou fundamental pois é através dela que é possível, além de também medir o quanto o modelo acertou, quais as faixas de valores podem ser utilizadas sem que a predição considere todos os emails como não-spam.
pred_probs <- data.frame(prob = seq(0,1, 0.01), erro=NA, acerto=NA, score=NA)
for (i in 1:100){
prediction <- predict(mod, type = "response", newdata=setTest) > pred_probs$prob[i]
true_spams <- setTest$spam == 1
res = table(prediction, true_spams)
pred_probs$score[i] = res[1] + res[2]*(-3) + res[3]*(-1) + res[4]*(5)
pred_probs$erro[i] = round(sum((prediction != true_spams)) / NROW(prediction), digits = 2)
pred_probs$acerto[i] = 1 - pred_probs$erro[i]
}
pred_probs <- pred_probs %>% filter(score != "NA")
pred_probs <- pred_probs %>% filter(score != "NA")
ggplot(data=pred_probs, aes(x=prob, y=score)) + geom_line() +
ggtitle("Scores das Predições") + theme_bw()
O gráfico acima mostra os scores das predições para o modelo treinado inicialmente, quando alterado o valor mínimo de probabilidade para o modelo classificar um email como spam ou não. É possível observar que, para algumas faixas de valores, o score se mantém constante, chegando ao seu valor máximo em pouco mais de 80%. Valores acima de 0.82 e abaixo de 0.05 foram descartados pois, ao calcular a predição para estas faixas, o modelo considerava que todos os emails não eram spam e assim alcançava uma maior taxa de acerto (pois existem bem menos observações que são classificadas como spam nos dados de teste). Com isso, a predição não identificaria nenhum spam. O uso da variável score ajuda, além de identificar o quão bom está o modelo, a eliminar os casos onde a predição julga todas as observações como não sendo spams, na tentativa de obter a taxa mínima de erro.
Por fim, o melhor valor encontrado para se utilizar neste caso, foi o de 0.82, valor este indicado pelo maior score registrado que chega próximo ao valor 1000, indicando que é a predição que mais acerta nos resultados.