Análise de rotatividade de clientes para uma empresa de telecomunicações.
Esse é um projeto de classificação.
Disponível também em meu github.
O conjunto de dados utilizado está disponível neste link.
A EasyNet é uma empresa de telecomunicações que oferece serviços de telefonia, internet e TV por assinatura. Para esse segmento, saber e prever a taxa de cancelamento de serviços é bastante útil, pois há uma grande concorrência nesse mercado.
Atualmente, a taxa de rotatividade de clientes da empresa é de aproximadamente 25%. Visando diminuir esse número, a EasyNet resolveu implementar um projeto para identificar potenciais cancelamentos com antecedência, e assim, promover ações direcionadas para esses clientes a fim de mantê-los na empresa.
Durante a reunião de alinhamento do projeto, foi estabelecido que a equipe de dados ficará responsável por identificar os clientes mais propensos a abandonar os serviços da empresa e a área de Marketing deverá criar ofertas e planos customizados para esses clientes. Portanto, nesse projeto, nosso objetivo é criar um modelo capaz de responder a seguinte pergunta:
Quais clientes estão mais propensos a abandonar os serviços da EasyNet?
Em relação ao conjunto de dados, as seguintes informações foram disponibilizadas:
Além disso, também foi disponibilizado o dicionário de dados:
customerID
: identificação do cliente.gender
: gênero do cliente.SeniorCitizen
: cliente da terceira idade: Partner
: cliente casado:Dependents
: cliente possui dependentes:tenure
: tempo de contrato do cliente (em meses).PhoneService
: serviço telefônico:MultipleLines
: múltiplas linhas telefônicas:InternetService
: tipo de provedor de internet: OnlineSecurity
: serviço de segurança online:OnlineBackup
: serviço de backup online:DeviceProtection
: proteção de dispositivo:TechSupport
: suporte técnico:StreamingTV
: serviços de streaming (TV):StreamingMovies
: serviços de streaming (filmes):Contract
: tipo do contrato:PaperlessBilling
: fatura eletrônica:PaymentMethod
: método de pagamento:MonthlyCharges
: valor pago mensal.TotalCharges
: valor total já pago pelo cliente.Churn
: cancelamento do serviço:Como estratégia para a solução do projeto, definimos as seguintes etapas:
Vamos iniciar o projeto carregando as bibliotecas e o conjunto de dados.
# Filtragem das mensagens de avisos.
options(warn = -1)
options(dplyr.summarise.inform = FALSE)
# Carregando as bibliotecas.
library(pacman)
pacman::p_load(caret,
cowplot,
dplyr,
ggcorrplot,
ggplot2,
klaR,
plotROC,
plyr,
ROSE,
stringr)
# Configurações do notebook.
# Plotagens.
options(repr.plot.width = 10,
repr.plot.height = 6,
scipen = 999)
# Estilo dos gráficos.
theme_set(theme_minimal())
Nessa etapa, nosso objetivo é realizar uma análise geral no dataset a fim de tratar possíveis inconsistências nos dados.
# Carregando o conjunto de dados.
df <- read.csv('data/Telco-Customer-Churn.csv')
Criar uma cópia do dataset é uma boa prática para não perdermos o conteúdo original durante a manipulação dos dados.
# Cópia do dataset.
df1 <- df
# Dimensão do dataframe.
dim(df1)
O conjunto de dados possui 7043 registros e 21 variáveis.
# Visualizando o dataframe.
head(df1)
customerID | gender | SeniorCitizen | Partner | Dependents | tenure | PhoneService | MultipleLines | InternetService | OnlineSecurity | ... | DeviceProtection | TechSupport | StreamingTV | StreamingMovies | Contract | PaperlessBilling | PaymentMethod | MonthlyCharges | TotalCharges | Churn |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7590-VHVEG | Female | 0 | Yes | No | 1 | No | No phone service | DSL | No | ... | No | No | No | No | Month-to-month | Yes | Electronic check | 29.85 | 29.85 | No |
5575-GNVDE | Male | 0 | No | No | 34 | Yes | No | DSL | Yes | ... | Yes | No | No | No | One year | No | Mailed check | 56.95 | 1889.50 | No |
3668-QPYBK | Male | 0 | No | No | 2 | Yes | No | DSL | Yes | ... | No | No | No | No | Month-to-month | Yes | Mailed check | 53.85 | 108.15 | Yes |
7795-CFOCW | Male | 0 | No | No | 45 | No | No phone service | DSL | Yes | ... | Yes | Yes | No | No | One year | No | Bank transfer (automatic) | 42.30 | 1840.75 | No |
9237-HQITU | Female | 0 | No | No | 2 | Yes | No | Fiber optic | No | ... | No | No | No | No | Month-to-month | Yes | Electronic check | 70.70 | 151.65 | Yes |
9305-CDSKC | Female | 0 | No | No | 8 | Yes | Yes | Fiber optic | No | ... | Yes | No | Yes | Yes | Month-to-month | Yes | Electronic check | 99.65 | 820.50 | Yes |
# Informações do dataframe.
str(df1)
'data.frame': 7043 obs. of 21 variables: $ customerID : Factor w/ 7043 levels "0002-ORFBO","0003-MKNFE",..: 5376 3963 2565 5536 6512 6552 1003 4771 5605 4535 ... $ gender : Factor w/ 2 levels "Female","Male": 1 2 2 2 1 1 2 1 1 2 ... $ SeniorCitizen : int 0 0 0 0 0 0 0 0 0 0 ... $ Partner : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 1 1 2 1 ... $ Dependents : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 2 1 1 2 ... $ tenure : int 1 34 2 45 2 8 22 10 28 62 ... $ PhoneService : Factor w/ 2 levels "No","Yes": 1 2 2 1 2 2 2 1 2 2 ... $ MultipleLines : Factor w/ 3 levels "No","No phone service",..: 2 1 1 2 1 3 3 2 3 1 ... $ InternetService : Factor w/ 3 levels "DSL","Fiber optic",..: 1 1 1 1 2 2 2 1 2 1 ... $ OnlineSecurity : Factor w/ 3 levels "No","No internet service",..: 1 3 3 3 1 1 1 3 1 3 ... $ OnlineBackup : Factor w/ 3 levels "No","No internet service",..: 3 1 3 1 1 1 3 1 1 3 ... $ DeviceProtection: Factor w/ 3 levels "No","No internet service",..: 1 3 1 3 1 3 1 1 3 1 ... $ TechSupport : Factor w/ 3 levels "No","No internet service",..: 1 1 1 3 1 1 1 1 3 1 ... $ StreamingTV : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 3 1 3 1 ... $ StreamingMovies : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 1 1 3 1 ... $ Contract : Factor w/ 3 levels "Month-to-month",..: 1 2 1 2 1 1 1 1 1 2 ... $ PaperlessBilling: Factor w/ 2 levels "No","Yes": 2 1 2 1 2 2 2 1 2 1 ... $ PaymentMethod : Factor w/ 4 levels "Bank transfer (automatic)",..: 3 4 4 1 3 3 2 4 3 1 ... $ MonthlyCharges : num 29.9 57 53.9 42.3 70.7 ... $ TotalCharges : num 29.9 1889.5 108.2 1840.8 151.7 ... $ Churn : Factor w/ 2 levels "No","Yes": 1 1 2 1 2 2 1 1 2 1 ...
SeniorCitizen
foi carregada com o tipo incorreto.# Convertendo a variável.
df1$SeniorCitizen <- as.factor(df1$SeniorCitizen)
# Verificando registros ausentes.
colSums(is.na(df1))
TotalCharges
possui 11 registros ausentes.# Removendo registros ausentes.
df1 <- df1[complete.cases(df1), ]
# Verificando registros duplicados.
table(duplicated(df1))
FALSE 7032
# Verificando registros únicos.
sapply(df1, function(x) {length(unique(x))})
Iremos substituir os valores No phone service e No internet service presentes nas variáveis categóricas pelo valor No.
# Substituindo os valores "No phone service".
df1$MultipleLines <- as.factor(mapvalues(df1$MultipleLines, from = c('No phone service'), to = c('No')))
# Substituindo os valores "No internet service".
# Índices das variáveis.
cols_recode <- c(10:15)
# Tratando os registros das variáveis.
for(i in 1: ncol(df1[, cols_recode])){
df1[, cols_recode][,i] <- as.factor(mapvalues(df1[, cols_recode][,i], from = c('No internet service'), to = c('No')))}
Realizamos uma análise geral nos dados tratando as principais inconsistências observadas, agora, partiremos para uma análise mais detalhada.
Verificaremos a taxa de rotatividade baseado nas características dos clientes e nos serviços prestados pela EasyNet.
# Churn por características dos clientes.
plot_grid(ggplot(df1, aes(x = gender, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = SeniorCitizen, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = Partner, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = Dependents, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%') +
scale_x_discrete(labels = function(x) str_wrap(x, width = 10)), align = 'h')
# Churn por informações das contas dos clientes.
plot_grid(ggplot(df1, aes(x = Contract, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = PaperlessBilling, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = PaymentMethod, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%') +
scale_x_discrete(labels = function(x) str_wrap(x, width = 10)), align = 'h')
# Churn por serviços da empresa.
plot_grid(ggplot(df1, aes(x = PhoneService, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = MultipleLines, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = InternetService, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = OnlineSecurity, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = OnlineBackup, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = DeviceProtection, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = TechSupport, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = StreamingTV, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%'),
ggplot(df1, aes(x = StreamingMovies, fill = Churn)) + geom_bar(position = 'fill') + labs(y = '%') +
scale_x_discrete(labels = function(x) str_wrap(x, width = 10)), align = 'h')
# Churn total de clientes.
df1 %>%
group_by(Churn) %>%
dplyr::summarise(Number = n()) %>%
mutate(Porcentagem = prop.table(Number) * 100) %>%
ggplot(aes(Churn, Porcentagem)) +
geom_col(aes(fill = Churn), show.legend = FALSE) +
labs(title = 'Churn de Clientes', y = NULL) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(aes(label = sprintf('%.2f%%', Porcentagem)), vjust = -0.5, size = 4)
# Churn por tempo de contrato e pagamentos realizados.
plot_grid(ggplot(df1, aes(x = tenure, fill = Churn)) + geom_histogram(bins = 30),
ggplot(df1, aes(x = MonthlyCharges, fill = Churn)) + geom_histogram(bins = 30),
ggplot(df1, aes(x = TotalCharges, fill = Churn)) + geom_histogram(bins = 30))
# Pagamento mensal por provedor de internet.
ggplot(df1, aes(x = MonthlyCharges, fill = InternetService)) + geom_histogram(bins = 30) + labs(x = 'Pagamento Mensal')
A etapa de extração de variáveis será constituída na criação da variável tenure_group
seguindo as regras abaixo:
# Criando a função.
group_tenure <- function(tenure){
if (tenure >= 0 & tenure <= 12){
return(1)
}else if(tenure > 12 & tenure <= 24){
return(2)
}else if (tenure > 24 & tenure <= 48){
return(3)
}else if (tenure > 48 & tenure <= 60){
return(4)
}else if (tenure > 60){
return(5)
}}
# Criando nova variável.
df1$tenure_group <- sapply(df1$tenure, group_tenure)
# Convertendo a variável.
df1$tenure_group <- as.factor(df1$tenure_group)
# Level's da variável.
levels(df1$tenure_group)
# Contagem de registros.
table(df1$tenure_group)
1 2 3 4 5 2175 1024 1594 832 1407
# Churn por grupos de clientes.
df1 %>%
ggplot(aes(x = tenure_group, fill = Churn)) +
geom_bar(position = 'fill') +
labs(x = 'Grupos de Clientes',
y = '%',
title = 'Churn por Grupos de Clientes') +
theme(plot.title = element_text(hjust = 0.5))
Essa etapa será constituída na análise de correlação e na exclusão de variáveis categóricas que não fornecerão informações úteis para prever o resultado.
# Análise de correlação.
corr <- round(cor(df1[, c('tenure', 'MonthlyCharges', 'TotalCharges')]), 1)
# Plotagem.
ggcorrplot(corr,
hc.order = TRUE,
type = 'lower',
outline.col = 'white',
ggtheme = ggplot2::theme_gray,
lab = TRUE)
colors = c('#6D9EC1', 'white', '#E46726')
Por conta da alta correlação da variável TotalCharges
com as demais variáveis numéricas, a mesma será desconsiderada.
Em relação as variáveis categóricas, vimos durante a análise exploratória que todas elas possuem uma grande diferença em termos percentuais das classes, exceto as variáveis gender
, PhoneService
e MultipleLines
. Sendo assim, essas variáveis também serão desconsideradas.
Por fim, as variáveis customerID
que se refere a identificação dos clientes e a tenure
que está representada em outra variável também serão descartadas.
# Seleção de variáveis.
df2 <- subset(df1, select = -c(customerID, gender, tenure, PhoneService, MultipleLines, TotalCharges))
# Visualizando o dataframe.
head(df2)
SeniorCitizen | Partner | Dependents | InternetService | OnlineSecurity | OnlineBackup | DeviceProtection | TechSupport | StreamingTV | StreamingMovies | Contract | PaperlessBilling | PaymentMethod | MonthlyCharges | Churn | tenure_group |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Yes | No | DSL | No | Yes | No | No | No | No | Month-to-month | Yes | Electronic check | 29.85 | No | 1 |
0 | No | No | DSL | Yes | No | Yes | No | No | No | One year | No | Mailed check | 56.95 | No | 3 |
0 | No | No | DSL | Yes | Yes | No | No | No | No | Month-to-month | Yes | Mailed check | 53.85 | Yes | 1 |
0 | No | No | DSL | Yes | No | Yes | Yes | No | No | One year | No | Bank transfer (automatic) | 42.30 | No | 3 |
0 | No | No | Fiber optic | No | No | No | No | No | No | Month-to-month | Yes | Electronic check | 70.70 | Yes | 1 |
0 | No | No | Fiber optic | No | No | Yes | No | Yes | Yes | Month-to-month | Yes | Electronic check | 99.65 | Yes | 1 |
# Divindo os dados.
set.seed(42)
ind <- createDataPartition(df2$Churn, p = .70, list = F)
# Conjuntos de treino e teste.
train <- df2[ind, ]
test <- df2[-ind, ]
O balanceamento de classes é aplicado apenas aos dados de treino.
Vejamos as características do conjunto de treino antes da aplicação.
# Dimensão do conjunto de treino.
dim(train)
# Proporção da variável target.
prop.table(table(train$Churn))
No Yes 0.7341592 0.2658408
# Balanceando as classes.
train <- ovun.sample(Churn ~ ., data = train, method = 'over')$data
# Dimensão do conjunto de treino.
dim(train)
# Proporção da variável target.
prop.table(table(train$Churn))
No Yes 0.4988959 0.5011041
Para as variáveis multiclasse, aplicaremos a transformação One Hot Enconding.
# Variáveis multiclasse.
# One Hot Enconding.
dummies_model <- dummyVars(Churn ~ InternetService + Contract + PaymentMethod, data = train)
# Aplicando a transformação.
trainData <- data.frame(predict(dummies_model, newdata = train))
# Adicionando as demais variáveis.
trainData <- cbind(trainData, train)
# Removendo as variáveis transformadas.
trainData <- subset(trainData, select = -c(InternetService, Contract, PaymentMethod))
Para as variáveis binárias, substituiremos os valores No e Yes por 0 e 1.
# Variáveis binárias.
# Índices das variáveis.
cols_recode <- c(12:20)
# Tratando os registros das variáveis.
for(i in 1: ncol(trainData[, cols_recode])){
trainData[, cols_recode][,i] <- as.factor(mapvalues(trainData[, cols_recode][,i], from = c('No', 'Yes'), to = c('0', '1')))}
Aplicaremos as mesmas transformações realizadas no conjunto de treino ao conjunto de teste.
# Variáveis multiclasse.
# One Hot Enconding.
testData <- data.frame(predict(dummies_model, test))
# Adicionando as demais variáveis.
testData <- cbind(testData, test)
# Removendo as variáveis transformadas.
testData <- subset(testData, select = -c(InternetService, Contract, PaymentMethod))
# Variáveis binárias.
# Índices das variáveis.
cols_recode <- c(12:20)
# Tratando os registros das variáveis.
for(i in 1: ncol(testData[, cols_recode])){
testData[, cols_recode][,i] <- as.factor(mapvalues(testData[, cols_recode][,i], from = c('No', 'Yes'), to = c('0', '1')))}
Essa etapa será constituída na normalização da variável MonthlyCharges
.
# Conjunto de treino.
range_model <- preProcess(trainData, method = 'range')
# Aplicando a transformação.
trainData <- data.frame(predict(range_model, newdata = trainData))
# Conjunto de teste.
testData <- predict(range_model, newdata = testData)
Vejamos o resultado final após os processamentos realizados.
# Conjunto de treino.
str(trainData)
'data.frame': 7246 obs. of 23 variables: $ InternetService.DSL : num 1 1 1 0 1 1 0 0 0 0 ... $ InternetService.Fiber.optic : num 0 0 0 1 0 0 0 1 1 0 ... $ InternetService.No : num 0 0 0 0 0 0 1 0 0 1 ... $ Contract.Month.to.month : num 1 0 0 1 0 1 0 0 1 0 ... $ Contract.One.year : num 0 1 1 0 1 0 0 1 0 1 ... $ Contract.Two.year : num 0 0 0 0 0 0 1 0 0 0 ... $ PaymentMethod.Bank.transfer..automatic.: num 0 0 1 0 1 0 0 0 0 0 ... $ PaymentMethod.Credit.card..automatic. : num 0 0 0 1 0 0 1 1 0 0 ... $ PaymentMethod.Electronic.check : num 1 0 0 0 0 0 0 0 1 0 ... $ PaymentMethod.Mailed.check : num 0 1 0 0 0 1 0 0 0 1 ... $ SeniorCitizen : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ... $ Partner : Factor w/ 2 levels "0","1": 2 1 1 1 1 2 1 2 1 1 ... $ Dependents : Factor w/ 2 levels "0","1": 1 1 1 2 2 2 1 1 1 1 ... $ OnlineSecurity : Factor w/ 2 levels "0","1": 1 2 2 1 2 2 1 1 2 1 ... $ OnlineBackup : Factor w/ 2 levels "0","1": 2 1 1 2 2 1 1 1 1 1 ... $ DeviceProtection : Factor w/ 2 levels "0","1": 1 2 2 1 1 1 1 2 2 1 ... $ TechSupport : Factor w/ 2 levels "0","1": 1 1 2 1 1 1 1 1 2 1 ... $ StreamingTV : Factor w/ 2 levels "0","1": 1 1 1 2 1 1 1 2 2 1 ... $ StreamingMovies : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 2 2 1 ... $ PaperlessBilling : Factor w/ 2 levels "0","1": 2 1 1 2 1 2 1 1 2 1 ... $ MonthlyCharges : num 0.116 0.385 0.24 0.706 0.377 ... $ Churn : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 1 1 1 1 ... $ tenure_group : Factor w/ 5 levels "1","2","3","4",..: 1 3 3 2 5 2 2 4 3 4 ...
# Conjunto de teste.
str(testData)
'data.frame': 2108 obs. of 23 variables: $ InternetService.DSL : num 0 1 0 0 0 1 0 1 0 0 ... $ InternetService.Fiber.optic : num 1 0 1 1 1 0 0 0 1 1 ... $ InternetService.No : num 0 0 0 0 0 0 1 0 0 0 ... $ Contract.Month.to.month : num 1 1 0 1 1 0 1 1 1 1 ... $ Contract.One.year : num 0 0 0 0 0 1 0 0 0 0 ... $ Contract.Two.year : num 0 0 1 0 0 0 0 0 0 0 ... $ PaymentMethod.Bank.transfer..automatic.: num 0 0 0 0 0 0 1 1 0 0 ... $ PaymentMethod.Credit.card..automatic. : num 0 0 1 0 1 0 0 0 0 0 ... $ PaymentMethod.Electronic.check : num 1 0 0 1 0 0 0 0 1 1 ... $ PaymentMethod.Mailed.check : num 0 1 0 0 0 1 0 0 0 0 ... $ SeniorCitizen : Factor w/ 2 levels "0","1": 1 1 1 1 2 1 1 2 1 1 ... $ Partner : Factor w/ 2 levels "0","1": 1 1 2 1 2 2 1 1 1 1 ... $ Dependents : Factor w/ 2 levels "0","1": 1 1 2 1 1 2 1 1 1 1 ... $ OnlineSecurity : Factor w/ 2 levels "0","1": 1 2 2 1 1 2 1 1 1 1 ... $ OnlineBackup : Factor w/ 2 levels "0","1": 1 1 2 2 1 2 1 1 1 2 ... $ DeviceProtection : Factor w/ 2 levels "0","1": 2 1 2 2 2 2 1 1 1 2 ... $ TechSupport : Factor w/ 2 levels "0","1": 1 1 2 1 1 2 1 1 1 1 ... $ StreamingTV : Factor w/ 2 levels "0","1": 2 1 2 1 2 1 1 1 1 2 ... $ StreamingMovies : Factor w/ 2 levels "0","1": 2 1 2 2 2 1 1 1 1 2 ... $ PaperlessBilling : Factor w/ 2 levels "0","1": 2 1 1 2 2 1 1 1 2 2 ... $ MonthlyCharges : num 0.811 0.115 0.946 0.715 0.769 ... $ Churn : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 1 1 2 2 ... $ tenure_group : Factor w/ 5 levels "1","2","3","4",..: 1 1 5 2 1 3 1 1 1 3 ...
Iremos utilizar os dados de treino para treinar e avaliar o desempenho de alguns algoritmos de classificação, são eles:
# Configurações da validação cruzada.
control <- trainControl(method = 'cv', number = 5)
# Treinando os modelos.
# Logistic Regression.
modelGLM <- train(Churn ~ ., data = trainData, method = 'glm', trControl = control)
# Naive Bayes.
modelNB <- train(Churn ~ ., data = trainData, method = 'nb', trControl = control)
# Decision Tree.
modelDT <- train(Churn ~ ., data = trainData, method = 'ctree', trControl = control)
# Random Forest.
modelRF <- train(Churn ~ ., data = trainData, method = 'rf', trControl = control)
# Obtendo os resultados.
results <- resamples(list(GLM = modelGLM, NB = modelNB, DT = modelDT, RF = modelRF))
summary(results)
bwplot(results)
dotplot(results)
Call: summary.resamples(object = results) Models: GLM, NB, DT, RF Number of resamples: 5 Accuracy Min. 1st Qu. Median Mean 3rd Qu. Max. NA's GLM 0.7413793 0.7494824 0.7681159 0.7631827 0.7763975 0.7805383 0 NB 0.7280883 0.7425811 0.7460317 0.7462024 0.7508627 0.7634483 0 DT 0.7556936 0.7653554 0.7681159 0.7686990 0.7758621 0.7784679 0 RF 0.8689655 0.8743961 0.8785369 0.8768987 0.8799172 0.8826777 0 Kappa Min. 1st Qu. Median Mean 3rd Qu. Max. NA's GLM 0.4826149 0.4988810 0.5361875 0.5262759 0.5527190 0.5609769 0 NB 0.4560127 0.4850584 0.4918958 0.4922731 0.5016477 0.5267507 0 DT 0.5112901 0.5306069 0.5361132 0.5373030 0.5516508 0.5568541 0 RF 0.7378353 0.7487136 0.7570034 0.7537160 0.7597510 0.7652766 0
Baseado na acurácia e na métrica Kappa (teste de concordância), podemos concluir:
Vejamos como o modelo treinado se sairá com os dados de teste. Para mais detalhes sobre esse algoritmo acesse este link.
# Previsões do modelo.
# Previsões com dados de treino.
rf_train <- predict(modelRF, trainData)
# Previsões com dados de teste.
rf_test <- predict(modelRF, testData)
# Desempenho com dados de treino.
confusionMatrix(rf_train, trainData$Churn, positive = 'Yes', mode = 'everything')
Confusion Matrix and Statistics Reference Prediction No Yes No 3472 10 Yes 143 3621 Accuracy : 0.9789 95% CI : (0.9753, 0.9821) No Information Rate : 0.5011 P-Value [Acc > NIR] : < 0.00000000000000022 Kappa : 0.9578 Mcnemar's Test P-Value : < 0.00000000000000022 Sensitivity : 0.9972 Specificity : 0.9604 Pos Pred Value : 0.9620 Neg Pred Value : 0.9971 Precision : 0.9620 Recall : 0.9972 F1 : 0.9793 Prevalence : 0.5011 Detection Rate : 0.4997 Detection Prevalence : 0.5195 Balanced Accuracy : 0.9788 'Positive' Class : Yes
# Desempenho com dados de teste.
confusionMatrix(rf_test, test$Churn, positive = 'Yes', mode = 'everything')
Confusion Matrix and Statistics Reference Prediction No Yes No 1277 212 Yes 271 348 Accuracy : 0.7709 95% CI : (0.7523, 0.7887) No Information Rate : 0.7343 P-Value [Acc > NIR] : 0.00006471 Kappa : 0.4318 Mcnemar's Test P-Value : 0.008313 Sensitivity : 0.6214 Specificity : 0.8249 Pos Pred Value : 0.5622 Neg Pred Value : 0.8576 Precision : 0.5622 Recall : 0.6214 F1 : 0.5903 Prevalence : 0.2657 Detection Rate : 0.1651 Detection Prevalence : 0.2936 Balanced Accuracy : 0.7232 'Positive' Class : Yes
Apesar do bom desempenho, observamos que esse modelo está sobreajustado.
Para saber como esse algoritmo funciona, acesse esse link em meu blog de estudos.
# Previsões do modelo.
# Previsões com dados de treino.
glm_train <- predict(modelGLM, trainData)
# Previsões com dados de teste.
glm_test <- predict(modelGLM, testData)
# Desempenho com dados de treino.
confusionMatrix(glm_train, trainData$Churn, positive = 'Yes', mode = 'everything')
Confusion Matrix and Statistics Reference Prediction No Yes No 2613 702 Yes 1002 2929 Accuracy : 0.7648 95% CI : (0.7549, 0.7746) No Information Rate : 0.5011 P-Value [Acc > NIR] : < 0.00000000000000022 Kappa : 0.5296 Mcnemar's Test P-Value : 0.0000000000004379 Sensitivity : 0.8067 Specificity : 0.7228 Pos Pred Value : 0.7451 Neg Pred Value : 0.7882 Precision : 0.7451 Recall : 0.8067 F1 : 0.7747 Prevalence : 0.5011 Detection Rate : 0.4042 Detection Prevalence : 0.5425 Balanced Accuracy : 0.7647 'Positive' Class : Yes
# Desempenho com dados de teste.
confusionMatrix(glm_test, test$Churn, positive = 'Yes', mode = 'everything')
Confusion Matrix and Statistics Reference Prediction No Yes No 1138 103 Yes 410 457 Accuracy : 0.7566 95% CI : (0.7377, 0.7748) No Information Rate : 0.7343 P-Value [Acc > NIR] : 0.01044 Kappa : 0.4691 Mcnemar's Test P-Value : < 0.0000000000000002 Sensitivity : 0.8161 Specificity : 0.7351 Pos Pred Value : 0.5271 Neg Pred Value : 0.9170 Precision : 0.5271 Recall : 0.8161 F1 : 0.6405 Prevalence : 0.2657 Detection Rate : 0.2168 Detection Prevalence : 0.4113 Balanced Accuracy : 0.7756 'Positive' Class : Yes
Caso desejado, podemos treinar um novo modelo filtrando apenas as melhores variáveis.
# Variáveis mais importantes.
varImp <- varImp(modelGLM)
plot(varImp, main = 'Melhores Variáveis GLM')
Realizaremos o mesmo procedimento abordado com os outros algoritmos.
# Previsões do modelo.
# Previsões com dados de treino.
dt_train <- predict(modelDT, trainData)
# Previsões com dados de teste.
dt_test <- predict(modelDT, testData)
# Desempenho com dados de treino.
confusionMatrix(dt_train, trainData$Churn, positive = 'Yes', mode = 'everything')
Confusion Matrix and Statistics Reference Prediction No Yes No 2869 535 Yes 746 3096 Accuracy : 0.8232 95% CI : (0.8142, 0.8319) No Information Rate : 0.5011 P-Value [Acc > NIR] : < 0.00000000000000022 Kappa : 0.6464 Mcnemar's Test P-Value : 0.000000004427 Sensitivity : 0.8527 Specificity : 0.7936 Pos Pred Value : 0.8058 Neg Pred Value : 0.8428 Precision : 0.8058 Recall : 0.8527 F1 : 0.8286 Prevalence : 0.5011 Detection Rate : 0.4273 Detection Prevalence : 0.5302 Balanced Accuracy : 0.8231 'Positive' Class : Yes
# Desempenho com dados de teste.
confusionMatrix(dt_test, test$Churn, positive = 'Yes', mode = 'everything')
Confusion Matrix and Statistics Reference Prediction No Yes No 1165 159 Yes 383 401 Accuracy : 0.7429 95% CI : (0.7237, 0.7614) No Information Rate : 0.7343 P-Value [Acc > NIR] : 0.1943 Kappa : 0.4156 Mcnemar's Test P-Value : <0.0000000000000002 Sensitivity : 0.7161 Specificity : 0.7526 Pos Pred Value : 0.5115 Neg Pred Value : 0.8799 Precision : 0.5115 Recall : 0.7161 F1 : 0.5967 Prevalence : 0.2657 Detection Rate : 0.1902 Detection Prevalence : 0.3719 Balanced Accuracy : 0.7343 'Positive' Class : Yes
Assim como na Random Forest, esse modelo também apresentou sobreajuste nos dados.
Com isso, finalizamos a etapa de modelagem preditiva.
Para comparar os modelos utilizaremos a Curva ROC e a AUC que são ferramentas para medir o desempenho de modelos de classificação binária.
Abaixo segue uma breve explicação do que cada uma representa:
# Previsões em probabilidades.
dt_pb <- predict(modelDT, testData, type = 'prob')[,2]
glm_pb <- predict(modelGLM, testData, type = 'prob')[,2]
rf_pb <- predict(modelRF, testData, type = 'prob')[,2]
# Concatenando os dados.
roc.data <- cbind(testData, dt_pb, glm_pb, rf_pb)
head(roc.data[, c('Churn', 'dt_pb', 'glm_pb', 'rf_pb')])
Churn | dt_pb | glm_pb | rf_pb | |
---|---|---|---|---|
6 | Yes | 0.98076923 | 0.91767440 | 0.978 |
8 | No | 0.61764706 | 0.55198729 | 0.912 |
16 | No | 0.03478261 | 0.06004441 | 0.086 |
20 | No | 0.66666667 | 0.77068851 | 0.422 |
32 | No | 0.75789474 | 0.90399770 | 0.822 |
33 | No | 0.07692308 | 0.06820508 | 0.126 |
# Preparando os dados.
roc.long <- melt_roc(roc.data, d = 'Churn', m = c('dt_pb', 'glm_pb', 'rf_pb'))
# ROC AUC.
rocplot <- ggplot(roc.long, aes(d = ifelse(D == 'Yes', 1, 0), m = M, color = name)) +
geom_roc(n.cuts = 0) +
style_roc(xlab = 'Falsos Positivos',
ylab = 'Verdadeiros Positivos') +
labs(title = 'Curva ROC', color = 'Legenda') +
theme(plot.title = element_text(hjust = 0.5))
# Plotagem.
rocplot +
geom_abline(size = 0.5, color = 'grey30') +
annotate('text', x = .75, y = .35, label = paste('AUC DT =', round(calc_auc(rocplot)$AUC[1],3))) +
annotate('text', x = .75, y = .28, label = paste('AUC GLM =', round(calc_auc(rocplot)$AUC[2],3))) +
annotate('text', x = .75, y = .21, label = paste('AUC RF =', round(calc_auc(rocplot)$AUC[3],3)))
Os modelos treinados com a Random Forest e a Regressão Logística foram os que apresentaram os melhores resultados. Para a entrega final desse primeiro ciclo, optamos pelo modelo com a Regressão Logística, pois esse obteve um resultado semelhante ao primeiro, e está mais generalizável. Abaixo segue as métricas do modelo com os dados de teste:
Para um primeiro ciclo do projeto, esses resultados são satisfatórios, porém, podemos perceber que há uma dificuldade do modelo em prever a classe positiva, e isso, deverá ser considerado para um próximo ciclo. Nesse caso, algumas tarefas que poderiam ajudar são:
Além disso, também poderia ser realizado:
Implementando essa primeira versão do modelo, a EasyNet conseguirá identificar em média 52,7% dos clientes que abandonarão os serviços da empresa.