Problematica:
Las empresas de telecomunicaciones tienen problemas con la fuga de sus clientes a la competencia, es por esto, que la empresa busca implementar un modelo de clasificacion que les permita identificar a aquellos clientes que se van a fugar, de manera que puedan gestionar con algun mecanismo de retencion/fidelizacion de aquellos clientes. La empresa nos proporciono un conjunto de datos de datos churn-analysis.csv con informacion que nos puede ser de utilidad para identificar patrones de comportamiento de clientes que se van a fugar.
La descripcion de las columnas de la base de datos son:
state: Region del usuario.
area.code: Codigo de area.
phone.number: Numero telefonico.
international.plan: Plan internacional (yes o no).
voice.mail.plan: Plan con correo de voz (yes o no).
number.vmail.messages: Cantidad de mensajes virtuales posee.
total.day.minutes: Cantidad de minutos diarios.
total.day.calls: Cantidad de llamadas diarias.
total.day.charge: Cantidad del costo diario.
total.eve.minutes: Cantidad de minutos en la tarde.
total.eve.calls: Cantidad de llamadas en la tarde.
total.eve.charge: Cantidad de costo en la tarde.
total.night.minutes: Cantidad de minutos en la noche.
total.night.calls: Cantidad de llamadas en la noche.
total.night.charge: Cantidad de costo en la noche.
total.intl.minutes: Cantidad de minutos internacionales.
total.intl.calls: Cantidad de llamadas internacionales.
total.intl.charge: Cantidad de costo internacionales.
customer.service.calls: Cantidad de llamados a la mesa de ayuda.
churn: Fuga del cliente (True o False).
El objetivo de este análisis fue desarrollar un modelo predictivo capaz de identificar a los clientes en riesgo de abandonar una empresa de telecomunicaciones. La metodología seguida para alcanzar este objetivo se dividió en varias fases clave, que se detallan a continuación:
Comprensión del Problema: Se inició con una comprensión clara del problema empresarial y los objetivos del análisis. Esto incluyó entender la importancia de predecir la fuga de clientes y cómo esto puede impactar en la rentabilidad y sustentabilidad de la empresa.
Preparación de Datos: Los datos fueron cargados y examinados para entender su estructura y contenido. Esto incluyó la identificación de las variables disponibles y sus tipos, así como una inspección inicial de posibles valores faltantes o atípicos.
Análisis Exploratorio de Datos (AED): Se llevó a cabo un AED para resumir las principales características del conjunto de datos mediante resúmenes estadísticos y visualizaciones, con el fin de captar tendencias, patrones y relaciones entre las variables.
Preprocesamiento de Datos: Se aplicaron técnicas de preprocesamiento como la conversión de variables categóricas en factores, la normalización de variables numéricas y la imputación de valores faltantes donde fue necesario.
Evaluación de Datos Atípicos: Se implementó una estrategia para manejar datos atípicos, con el fin de minimizar su impacto en el modelo.
Construcción y Selección del Modelo: Se construyeron varios modelos de clasificación, incluyendo Regresión Logística, Árboles de Decisión, Random Forest y Máquinas de Vectores de Soporte (SVM). Luego, se seleccionó el mejor modelo basado en su precisión y capacidad predictiva, utilizando la métrica AUC como referencia principal.
Validación del Modelo: Se validó el modelo utilizando un conjunto de datos de prueba para evaluar su rendimiento en datos no vistos anteriormente. Se calculó la precisión y el AUC para cuantificar la capacidad predictiva del modelo.
Interpretación y Conclusión: Se interpretaron los resultados en el contexto del problema empresarial y se formularon recomendaciones basadas en las conclusiones obtenidas del análisis.
Cada fase del proyecto fue documentada detalladamente para asegurar la transparencia y reproducibilidad del análisis. Además, se adoptó un enfoque iterativo para la mejora continua del modelo, considerando los comentarios y los resultados obtenidos en cada etapa del proceso.
# Leer los datos
df <- read.csv("churn-analysis.csv", header = TRUE, sep = ";")
#### 1- Análisis Exploratorio ####
# Estructura de los datos
print("Estructura de los datos:")
## [1] "Estructura de los datos:"
str(df)
## 'data.frame': 3333 obs. of 20 variables:
## $ state : chr "KS" "OH" "NJ" "OH" ...
## $ area.code : int 415 415 415 408 415 510 510 415 408 415 ...
## $ phone.number : chr "382-4657" "371-7191" "358-1921" "375-9999" ...
## $ international.plan : chr "no" "no" "no" "yes" ...
## $ voice.mail.plan : chr "yes" "yes" "no" "no" ...
## $ number.vmail.messages : int 25 26 0 0 0 0 24 0 0 37 ...
## $ total.day.minutes : num 265 162 243 299 167 ...
## $ total.day.calls : int 110 123 114 71 113 98 88 79 97 84 ...
## $ total.day.charge : num 45.1 27.5 41.4 50.9 28.3 ...
## $ total.eve.minutes : num 197.4 195.5 121.2 61.9 148.3 ...
## $ total.eve.calls : int 99 103 110 88 122 101 108 94 80 111 ...
## $ total.eve.charge : num 16.78 16.62 10.3 5.26 12.61 ...
## $ total.night.minutes : num 245 254 163 197 187 ...
## $ total.night.calls : int 91 103 104 89 121 118 118 96 90 97 ...
## $ total.night.charge : num 11.01 11.45 7.32 8.86 8.41 ...
## $ total.intl.minutes : num 10 13.7 12.2 6.6 10.1 6.3 7.5 7.1 8.7 11.2 ...
## $ total.intl.calls : int 3 3 5 7 3 6 7 6 4 5 ...
## $ total.intl.charge : num 2.7 3.7 3.29 1.78 2.73 1.7 2.03 1.92 2.35 3.02 ...
## $ customer.service.calls: int 1 1 0 2 3 0 3 0 1 0 ...
## $ churn : chr "False" "False" "False" "False" ...
# Resumen estadístico
print("Resumen estadístico:")
## [1] "Resumen estadístico:"
summary(df)
## state area.code phone.number international.plan
## Length:3333 Min. :408.0 Length:3333 Length:3333
## Class :character 1st Qu.:408.0 Class :character Class :character
## Mode :character Median :415.0 Mode :character Mode :character
## Mean :437.2
## 3rd Qu.:510.0
## Max. :510.0
## voice.mail.plan number.vmail.messages total.day.minutes total.day.calls
## Length:3333 Min. : 0.000 Min. : 0.0 Min. : 0.0
## Class :character 1st Qu.: 0.000 1st Qu.:143.7 1st Qu.: 87.0
## Mode :character Median : 0.000 Median :179.4 Median :101.0
## Mean : 8.099 Mean :179.8 Mean :100.4
## 3rd Qu.:20.000 3rd Qu.:216.4 3rd Qu.:114.0
## Max. :51.000 Max. :350.8 Max. :165.0
## total.day.charge total.eve.minutes total.eve.calls total.eve.charge
## Min. : 0.00 Min. : 0.0 Min. : 0.0 Min. : 0.00
## 1st Qu.:24.43 1st Qu.:166.6 1st Qu.: 87.0 1st Qu.:14.16
## Median :30.50 Median :201.4 Median :100.0 Median :17.12
## Mean :30.56 Mean :201.0 Mean :100.1 Mean :17.08
## 3rd Qu.:36.79 3rd Qu.:235.3 3rd Qu.:114.0 3rd Qu.:20.00
## Max. :59.64 Max. :363.7 Max. :170.0 Max. :30.91
## total.night.minutes total.night.calls total.night.charge total.intl.minutes
## Min. : 23.2 Min. : 33.0 Min. : 1.040 Min. : 0.00
## 1st Qu.:167.0 1st Qu.: 87.0 1st Qu.: 7.520 1st Qu.: 8.50
## Median :201.2 Median :100.0 Median : 9.050 Median :10.30
## Mean :200.9 Mean :100.1 Mean : 9.039 Mean :10.24
## 3rd Qu.:235.3 3rd Qu.:113.0 3rd Qu.:10.590 3rd Qu.:12.10
## Max. :395.0 Max. :175.0 Max. :17.770 Max. :20.00
## total.intl.calls total.intl.charge customer.service.calls churn
## Min. : 0.000 Min. :0.000 Min. :0.000 Length:3333
## 1st Qu.: 3.000 1st Qu.:2.300 1st Qu.:1.000 Class :character
## Median : 4.000 Median :2.780 Median :1.000 Mode :character
## Mean : 4.479 Mean :2.765 Mean :1.563
## 3rd Qu.: 6.000 3rd Qu.:3.270 3rd Qu.:2.000
## Max. :20.000 Max. :5.400 Max. :9.000
# Verificar valores faltantes
print("Valores faltantes por columna:")
## [1] "Valores faltantes por columna:"
colSums(is.na(df))
## state area.code phone.number
## 0 0 0
## international.plan voice.mail.plan number.vmail.messages
## 0 0 0
## total.day.minutes total.day.calls total.day.charge
## 0 0 0
## total.eve.minutes total.eve.calls total.eve.charge
## 0 0 0
## total.night.minutes total.night.calls total.night.charge
## 0 0 0
## total.intl.minutes total.intl.calls total.intl.charge
## 0 0 0
## customer.service.calls churn
## 0 0
# Visualización básica
ggplot(df, aes(x = churn)) +
geom_bar() +
labs(title = "Distribución de Churn", x = "Churn", y = "Frecuencia")
##### Análisis de Correlaciones ####
# Convertir primero las variables character a factor
df <- df %>% mutate(across(where(is.character), as.factor))
# Luego convertir todas las variables categóricas (ahora factors) a numéricas
df_numeric <- df %>% mutate(across(where(is.factor), as.numeric))
# Calcular la matriz de correlación
correlaciones <- cor(df_numeric, use = "complete.obs")
# Visualizar la matriz de correlación
corrplot(correlaciones, method = "circle")
##### AIdentificación de Variables con Baja Correlación con 'churn' ####
# Establecer un umbral de correlación (por ejemplo, 0.05)
umbral <- 0.05
# Obtener correlaciones de todas las variables con 'churn'
correlaciones_churn <- correlaciones["churn", ]
# Identificar variables con correlación absoluta menor que el umbral
variables_baja_corr <- names(correlaciones_churn[abs(correlaciones_churn) < umbral])
# Mostrar variables con baja correlación con 'churn'
print(variables_baja_corr)
## [1] "state" "area.code" "phone.number"
## [4] "total.day.calls" "total.eve.calls" "total.night.minutes"
## [7] "total.night.calls" "total.night.charge"
##### Exclusión de Variables con Baja Correlación #####
# Excluir variables con baja correlación de df
df_reducido <- df %>% select(-all_of(variables_baja_corr))
# Ver la estructura del nuevo DataFrame
str(df_reducido)
## 'data.frame': 3333 obs. of 12 variables:
## $ international.plan : Factor w/ 2 levels "no","yes": 1 1 1 2 2 2 1 2 1 2 ...
## $ voice.mail.plan : Factor w/ 2 levels "no","yes": 2 2 1 1 1 1 2 1 1 2 ...
## $ number.vmail.messages : int 25 26 0 0 0 0 24 0 0 37 ...
## $ total.day.minutes : num 265 162 243 299 167 ...
## $ total.day.charge : num 45.1 27.5 41.4 50.9 28.3 ...
## $ total.eve.minutes : num 197.4 195.5 121.2 61.9 148.3 ...
## $ total.eve.charge : num 16.78 16.62 10.3 5.26 12.61 ...
## $ total.intl.minutes : num 10 13.7 12.2 6.6 10.1 6.3 7.5 7.1 8.7 11.2 ...
## $ total.intl.calls : int 3 3 5 7 3 6 7 6 4 5 ...
## $ total.intl.charge : num 2.7 3.7 3.29 1.78 2.73 1.7 2.03 1.92 2.35 3.02 ...
## $ customer.service.calls: int 1 1 0 2 3 0 3 0 1 0 ...
## $ churn : Factor w/ 2 levels "False","True": 1 1 1 1 1 1 1 1 1 1 ...
print("Valores faltantes en df_reducido:")
## [1] "Valores faltantes en df_reducido:"
colSums(is.na(df_reducido))
## international.plan voice.mail.plan number.vmail.messages
## 0 0 0
## total.day.minutes total.day.charge total.eve.minutes
## 0 0 0
## total.eve.charge total.intl.minutes total.intl.calls
## 0 0 0
## total.intl.charge customer.service.calls churn
## 0 0 0
# identificación de datos atípicos para 'total.day.minutes'
boxplot(df_reducido$total.day.minutes, main = "Boxplot - Total Day Minutes")
# Función para identificar outliers basada en el IQR
find_outliers <- function(data) {
Q1 <- quantile(data, 0.25)
Q3 <- quantile(data, 0.75)
IQR <- Q3 - Q1
return(data < (Q1 - 1.5 * IQR) | data > (Q3 + 1.5 * IQR))
}
# Aplicar la función a cada columna numérica y obtener un data frame de outliers
outliers <- df_reducido %>%
select_if(is.numeric) %>%
map_df(~find_outliers(.))
# Ver un sumario de los outliers por columna
summary_outliers <- colSums(outliers)
print(summary_outliers)
## number.vmail.messages total.day.minutes total.day.charge
## 1 25 25
## total.eve.minutes total.eve.charge total.intl.minutes
## 24 24 46
## total.intl.calls total.intl.charge customer.service.calls
## 78 49 267
# Eliminar filas con outliers
df_sin_outliers <- df_reducido[!rowSums(outliers), ]
#df_sin_outliers
#### 3- Modelo propuesto #####
# Dividir los datos en conjuntos de entrenamiento y prueba
set.seed(123)
indices <- createDataPartition(df_sin_outliers$churn, p = 0.8, list = FALSE)
train_data <- df_sin_outliers[indices,]
test_data <- df_sin_outliers[-indices,]
# Preparar la receta de preprocesamiento
receta <- recipe(churn ~ ., data = train_data) %>%
step_dummy(all_nominal(), -all_outcomes()) %>%
step_center(all_predictors(), -all_outcomes()) %>%
step_scale(all_predictors(), -all_outcomes())
# Aplicar la receta de preprocesamiento
train_data_prep <- prep(receta, training = train_data)
train_data_processed <- bake(train_data_prep, new_data = train_data)
test_data_processed <- bake(train_data_prep, new_data = test_data)
# Entrenar varios modelos y evaluarlos
modelos <- list()
# Modelo de Regresión Logística
modelos$log_reg <- train(churn ~ ., data = train_data_processed, method = "glm", family = "binomial")
# Árbol de Decisión
modelos$arbol_dec <- train(churn ~ ., data = train_data_processed, method = "rpart")
# Random Forest
modelos$random_forest <- train(churn ~ ., data = train_data_processed, method = "rf")
# SVM (Support Vector Machine)
modelos$svm <- train(churn ~ ., data = train_data_processed, method = "svmRadial")
# Evaluar modelos
resultados <- lapply(modelos, function(modelo) {
predicciones <- predict(modelo, newdata = test_data_processed)
prob_predicciones <- predict(modelo, newdata = test_data_processed, type = "prob")[,2]
accuracy <- mean(predicciones == test_data_processed$churn)
list(accuracy = accuracy)
})
## Warning in method$prob(modelFit = modelFit, newdata = newdata, submodels =
## param): kernlab class probability calculations failed; returning NAs
resultados
## $log_reg
## $log_reg$accuracy
## [1] 0.9119171
##
##
## $arbol_dec
## $arbol_dec$accuracy
## [1] 0.9360967
##
##
## $random_forest
## $random_forest$accuracy
## [1] 0.955095
##
##
## $svm
## $svm$accuracy
## [1] 0.9360967
##### modelo propuesto -- random_forest-- #####
#random_forest
#[1] 0.955095
#### 4- ramdom_forest ####
# Evaluar el modelo Random Forest y calcular Accuracy y AUC
modelo_rf <- modelos$random_forest
predicciones_rf <- predict(modelo_rf, newdata = test_data_processed)
prob_predicciones_rf <- predict(modelo_rf, newdata = test_data_processed, type = "prob")[,2]
# Asegúrate de que churn está en los mismos niveles y orden que las predicciones
test_data_churn_factor <- factor(test_data_processed$churn, levels = levels(predicciones_rf))
# Calcula la precisión
accuracy_rf <- mean(predicciones_rf == test_data_churn_factor)
# Calcula el AUC
if (is.numeric(prob_predicciones_rf)) {
roc_curve_rf <- roc(response = test_data_churn_factor, predictor = prob_predicciones_rf)
auc_rf <- auc(roc_curve_rf)
} else {
warning("Las probabilidades predichas no son numéricas para Random Forest.")
auc_rf <- NA
}
## Setting levels: control = False, case = True
## Setting direction: controls < cases
# Imprimir los resultados para Random Forest
print(paste("Accuracy para Random Forest:", accuracy_rf))
## [1] "Accuracy para Random Forest: 0.955094991364421"
print(paste("AUC para Random Forest:", auc_rf))
## [1] "AUC para Random Forest: 0.840454657315124"
# Opcional: Visualizar la curva ROC
plot(roc_curve_rf, main = "Curva ROC - Random Forest")
El análisis de la fuga de clientes realizado mediante el modelo de Random Forest ha proporcionado resultados prometedores. La precisión del modelo, un impresionante 95.5%, junto con un AUC de 0.840, indica una fuerte capacidad predictiva. Estas métricas sugieren que el modelo es bastante confiable en la identificación correcta de los clientes que podrían abandonar la empresa.
La elevada precisión indica que el modelo acertó en la gran mayoría de las predicciones realizadas sobre el conjunto de prueba. Por otro lado, el AUC cercano a 1 demuestra que el modelo tiene una alta capacidad para distinguir entre los clientes que se fugan y los que no. Esto es crucial para la empresa, ya que le permite enfocar sus esfuerzos de retención y fidelización de manera más efectiva, destinando recursos de forma eficiente a aquellos clientes identificados como de alto riesgo de fuga.
Cabe mencionar que, aunque el modelo funciona bien con los datos actuales, es importante continuar con la validación y ajuste del modelo a medida que se disponga de nuevos datos. El comportamiento del cliente puede cambiar con el tiempo, y el modelo puede necesitar ajustes para mantener su precisión.
En conclusión, el modelo de Random Forest se muestra como una herramienta valiosa para la toma de decisiones estratégicas en la retención de clientes. Implementar este modelo podría traducirse en una mejora significativa en la gestión de la relación con los clientes y, en última instancia, en la rentabilidad de la empresa.