1. INTRODUCCIÓN

1.1. Contextualización de la investigación

El análisis de datos constituye una herramienta importante para estudiar el comportamiento de los clientes y evaluar la capacidad de diferentes métodos estadísticos y computacionales para realizar predicciones. En el sector financiero, las campañas de marketing telefónico permiten ofrecer productos como los depósitos bancarios a plazo fijo, cuya contratación puede variar según las características de los clientes y las condiciones en las que se realizan los contactos.

Para esta investigación se utiliza la base de datos Bank Marketing, publicada en el repositorio de la Universidad de California en Irvine (UCI), que contiene información sobre campañas de marketing telefónico realizadas por una institución bancaria portuguesa. Para el análisis se utiliza el archivo bank.csv, correspondiente a la versión de la base que contiene 4.521 observaciones y 17 variables. El objetivo es estudiar si la información disponible sobre cada cliente y los contactos previos permite distinguir entre quienes contrataron el depósito y quienes no lo hicieron.

El estudio se enmarca en el aprendizaje supervisado y tiene como variable dependiente y, que registra si el cliente contrató (yes) o no contrató (no) un depósito bancario a plazo fijo. Para analizar esta variable, se implementan dos métodos de clasificación: K-Nearest Neighbors (KNN), que clasifica las observaciones a partir de la proximidad entre ellas, y regresión logística (Logit), que permite estimar la probabilidad de que un cliente contrate el producto financiero.

Finalmente, se comparará el desempeño de ambos modelos mediante métricas como la exactitud (accuracy), la sensibilidad, la precisión y el área bajo la curva ROC (AUC), con el propósito de evaluar su capacidad de clasificación e identificar sus diferencias de desempeño.

1.2. Pregunta de investigación

¿En qué medida los modelos (KNN) y (Logit) permiten predecir la contratación de un depósito bancario a plazo fijo a partir de las características de los clientes y las condiciones de contacto?

1.3. Objetivo general

Evaluar y comparar el desempeño de los modelos K-Nearest Neighbors (KNN) y regresión logística (Logit) en la predicción de la contratación de un depósito bancario a plazo fijo, utilizando las variables seleccionadas de las 4.521 observaciones del archivo bank.csv y métricas de evaluación como la exactitud (accuracy), la sensibilidad, la precisión y el área bajo la curva ROC (AUC).

2. METODOLOGÍA

2.1 Datos y limpieza

Para el desarrollo de esta investigación se utilizó la base de datos Bank Marketing, publicada en el repositorio de la Universidad de California en Irvine (UCI). Esta contiene información de clientes de una institución bancaria portuguesa y de campañas de marketing telefónico orientadas a promover la contratación de depósitos bancarios a plazo fijo. Debido al tamaño de la base de datos original, se realizó una reducción del número de observaciones para trabajar con 4.521 registros y 17 variables. Posteriormente, se revisó la calidad de los datos y se preparó la información para construir los modelos de clasificación KNN y regresión logística. Finalmente, los datos se dividieron en conjuntos de entrenamiento y prueba, con el propósito de evaluar la capacidad de los modelos para predecir si un cliente contrata o no el depósito bancario.

A continuación, se realizó una revisión de la calidad de los datos para identificar valores faltantes, registros duplicados, rangos de edad y posibles valores extremos.

bank <- read.csv2("bank.csv", stringsAsFactors = TRUE)   # separador ";"
bank$y   <- factor(bank$y, levels = c("no", "yes"))

calidad <- data.frame(
  Revisión = c("Clientes (filas)", "Variables (columnas)", "Valores faltantes (NA)",
               "Filas duplicadas", "Edad mínima y máxima", "Clientes con saldo negativo"),
  Resultado = c(nrow(bank), ncol(bank), sum(is.na(bank)), sum(duplicated(bank)),
                paste(min(bank$age), "a", max(bank$age)), sum(bank$balance < 0))
)
tabla_morada(calidad, caption = "Tabla 1. Revisión de calidad de la base")
Tabla 1. Revisión de calidad de la base
Revisión Resultado
Clientes (filas) 4521
Variables (columnas) 17
Valores faltantes (NA) 0
Filas duplicadas 0
Edad mínima y máxima 19 a 87
Clientes con saldo negativo 366

La base de datos no presenta valores faltantes ni registros duplicados, y las edades se encuentran dentro de un rango plausible. Los saldos negativos de la variable balance se interpretan como posibles cuentas sobregiradas, por lo que no se consideran necesariamente errores de captura. En la variable poutcome, la categoría unknown, que representa el 82,0 % de los clientes, indica que el resultado de una campaña de marketing anterior no está registrado o no se conoce. Esta categoría no significa necesariamente que el cliente no haya participado en una campaña previa; por esta razón, se conservó como una categoría independiente.

Por otra parte, las variables balance (con un valor máximo de 71.188 €) y campaign (con un máximo de 50 contactos durante la campaña actual) presentan valores extremos. Estos registros se conservaron porque pueden corresponder a situaciones reales y no se identificaron, por ese solo hecho, como errores de captura. Sin embargo, estos valores pueden influir en el comportamiento de los modelos. En particular, KNN es sensible a las diferencias de escala entre variables, por lo que se estandarizaron las variables numéricas antes de ajustar el modelo. Esta estandarización ayuda a que las diferencias de escala no dominen el cálculo de distancias, aunque no elimina la influencia de los valores extremos.

2.2 Variables

vars_x <- c("age", "balance", "campaign", "default", "housing", "loan", "poutcome")
numv   <- c("age", "balance", "campaign")

variables <- data.frame(
  Variable = c("y", vars_x),
  Descripción = c("Contrató el depósito a plazo fijo", "Edad en años", "Saldo promedio anual (€)",
                  "Contactos realizados en la campaña actual", "Incumplimiento crediticio",
                  "Crédito hipotecario", "Préstamo personal", "Resultado de la campaña anterior"),
  Tipo = c("Binaria (Y)", "Cuantitativa", "Cuantitativa", "Cuantitativa",
           "Binaria", "Binaria", "Binaria", "Categórica (4 niveles)"),
  Justificación = c("Resultado que se quiere predecir.",
                    "Las necesidades de ahorro cambian con la etapa de vida.",
                    "Aproxima la capacidad de ahorro del cliente.",
                    "Mide la insistencia comercial, que puede generar rechazo.",
                    "Señal de riesgo financiero.",
                    "Compromiso financiero que reduce el flujo disponible para ahorrar.",
                    "Compromiso financiero que reduce el flujo disponible para ahorrar.",
                    "La respuesta a una oferta anterior anticipa la respuesta a la actual.")
)
tabla_morada(variables, caption = " Tabla 2. Variables del modelo")
Tabla 2. Variables del modelo
Variable Descripción Tipo Justificación
y Contrató el depósito a plazo fijo Binaria (Y) Resultado que se quiere predecir.
age Edad en años Cuantitativa Las necesidades de ahorro cambian con la etapa de vida.
balance Saldo promedio anual (€) Cuantitativa Aproxima la capacidad de ahorro del cliente.
campaign Contactos realizados en la campaña actual Cuantitativa Mide la insistencia comercial, que puede generar rechazo.
default Incumplimiento crediticio Binaria Señal de riesgo financiero.
housing Crédito hipotecario Binaria Compromiso financiero que reduce el flujo disponible para ahorrar.
loan Préstamo personal Binaria Compromiso financiero que reduce el flujo disponible para ahorrar.
poutcome Resultado de la campaña anterior Categórica (4 niveles) La respuesta a una oferta anterior anticipa la respuesta a la actual.

Se excluyó duration (duración de la llamada): solo se conoce después de contactar al cliente y determina casi por completo el resultado, así que incluirla produciría una filtración de información (data leakage) y un modelo inútil para decidir a quién llamar (Moro et al., 2014).

2.2.1 Justificación de variables

Para la selección de las variables del modelo, se tuvieron en cuenta características personales, financieras y comerciales de los clientes que podrían estar relacionadas con la contratación de un depósito bancario a plazo fijo. Estas variables se eligieron considerando su utilidad para comprender el comportamiento de los clientes y cumplir con el objetivo de comparar el desempeño de los modelos de clasificación KNN y regresión logística. A continuación, se presenta la justificación de cada una de las variables seleccionadas.

-La variable dependiente y corresponde a la contratación del depósito bancario a plazo fijo y se seleccionó porque representa el resultado que se desea predecir mediante los modelos de clasificación.

-La variable age representa la edad del cliente en años. Se eligió porque las necesidades financieras y los hábitos de ahorro pueden variar según la etapa de vida de cada persona.

-La variable balance corresponde al saldo bancario promedio anual del cliente. Se incluyó porque permite analizar si la cantidad de dinero disponible en su cuenta está relacionada con la decisión de contratar un depósito a plazo fijo.

-La variable campaign indica el número de contactos realizados al cliente durante la campaña de marketing actual. Se seleccionó porque permite evaluar si la frecuencia de los contactos comerciales está relacionada con la probabilidad de contratar el depósito.

-La variable default indica si el cliente presenta incumplimiento crediticio. Se incluyó porque esta información puede ayudar a analizar si los antecedentes financieros del cliente están asociados con su decisión de contratar el producto bancario.

-La variable housing señala si el cliente tiene un crédito hipotecario. Se eligió porque permite estudiar si tener este tipo de compromiso financiero está relacionado con la contratación de un depósito a plazo fijo.

-La variable loan indica si el cliente tiene un préstamo personal. Se incluyó porque las obligaciones financieras del cliente podrían estar relacionadas con sus decisiones de ahorro e inversión.

Finalmente, la variable poutcome representa el resultado de la campaña de marketing anterior. Se seleccionó porque conocer si el cliente tuvo una respuesta exitosa, fallida, diferente o desconocida en campañas previas puede aportar información útil para predecir su respuesta a la campaña actual.

En conjunto, estas variables permiten analizar diferentes características de los clientes e identificar patrones que ayuden a predecir la contratación del depósito bancario, comparando la capacidad de clasificación de los modelos KNN y regresión logística.

2.3 Partición y preprocesamiento

La base se dividió de forma estratificada en 70 % para entrenamiento y 30 % para prueba, de modo que ambos conjuntos conserven aproximadamente la misma proporción de clientes que contratan. El conjunto de entrenamiento se utilizó para ajustar los modelos y seleccionar k y los umbrales; el conjunto de prueba se reservó para la evaluación final y no se utilizó para ajustar los modelos. Las variables categóricas se representaron mediante variables dummy y las cuantitativas se estandarizaron porque KNN calcula distancias y es sensible a la escala. En la evaluación final, la media y la desviación estándar se calcularon con el entrenamiento. Durante la validación cruzada, esos parámetros se recalcularon dentro de cada pliegue usando solo su parte de entrenamiento, para evitar que los datos de validación influyeran en el preprocesamiento.

set.seed(123)


idx   <- unlist(lapply(split(seq_len(nrow(bank)), bank $y),
                       function(i) sample(i, round(0.7 * length(i)))))
train <- bank[idx, ]
test  <- bank[-idx, ]

X_raw_tr <- model.matrix(y ~ ., data = train[, c("y", vars_x)])[, -1]
X_raw_te <- model.matrix(y ~ ., data = test[, c("y", vars_x)])[, -1]

mu <- colMeans(X_raw_tr[, numv, drop = FALSE])
s  <- apply(X_raw_tr[, numv, drop = FALSE], 2, sd)

escalar <- function(m, centro = mu, desv = s) {
  m[, numv] <- scale(m[, numv, drop = FALSE], center = centro, scale = desv)
  m
}

X_tr <- escalar(X_raw_tr)
X_te <- escalar(X_raw_te)

prev_train <- mean(train $y == "yes")

2.4 Modelos y métricas

KNN asigna a cada cliente la clase más frecuente entre sus k vecinos más cercanos (distancia euclidiana). El valor de k se eligió por validación cruzada de 5 particiones sobre el conjunto de entrenamiento, maximizando el AUC.

Logit modela la probabilidad de contratar mediante

\[\ln\left(\frac{p}{1-p}\right)=\beta_0+\beta_1x_1+\dots+\beta_kx_k,\]

y sus coeficientes se interpretan mediante odds ratios (\(e^{\beta}\)): el factor por el que se multiplican las odds de contratar cuando la variable aumenta una unidad (o cambia de categoría), manteniendo constantes las demás variables.

Umbral de clasificación. Un cliente se clasifica como yes si su probabilidad estimada supera un umbral. Se evaluaron dos: el estándar de 0,5 y el umbral de Youden (Youden, 1950), que maximiza sensibilidad + especificidad − 1. Para evitar favorecer a un modelo, los umbrales de ambos modelos se calcularon con predicciones fuera de muestra obtenidas mediante la misma validación cruzada de cinco particiones dentro del conjunto de entrenamiento. El conjunto de prueba se reservó para la evaluación final.

Métricas. Se reportan exactitud, sensibilidad (proporción de contratantes detectados), especificidad, precisión (proporción de aciertos entre los clasificados como yes), F1 y AUC.

3. RESULTADOS DESCRIPTIVOS

3.1 Variable objetivo

tab_y <- bank %>% count(y) %>% mutate(porcentaje = round(100 * n / sum(n), 1))

p <- ggplot(tab_y, aes(y, n, fill = y,
                       text = paste0("y = ", y, "<br>Clientes: ", n, "<br>", porcentaje, " %"))) +
  geom_col(width = 0.6, show.legend = FALSE) +
  scale_fill_manual(values = c(no = lila, yes = morado)) +
  labs(title = "Figura 1. Contratación del depósito a plazo fijo", x = "¿Contrató el depósito?", y = "Clientes")

# Forzamos la fuente directamente en la conversión interactiva
ggplotly(p, tooltip = "text") %>% 
  layout(
    font = list(family = "Times New Roman"),
    title = list(font = list(family = "Times New Roman")),
    hoverlabel = list(font = list(family = "Times New Roman"))
  )

De los 4,521 clientes, 521 (11.5 %) contrataron el depósito y 4000 (88.5 %) no. La base está fuertemente desbalanceada, y esto condiciona todo el análisis: un clasificador que respondiera “no” a todos los clientes alcanzaría una exactitud de 88.5 % sin identificar a un solo contratante. Por eso la exactitud no sirve como único criterio y se complementa con sensibilidad, precisión y AUC. El desbalance también explica que el umbral de 0,5 sea inadecuado: como pocos clientes superan una probabilidad de 0,5, los modelos casi nunca predicen yes, y de ahí el uso del umbral de Youden. La tasa de contratación de 11.5 % es además la referencia para medir cuánto mejora un modelo la selección de clientes frente a llamar al azar.

3.2 Variables cuantitativas

desc <- bank %>%
  select(all_of(numv)) %>%
  pivot_longer(everything(), names_to = "Variable", values_to = "value") %>%
  group_by(Variable) %>%
  summarise(
    Media = mean(value, na.rm = TRUE),
    Mediana = median(value, na.rm = TRUE),
    `Desv. est.` = sd(value, na.rm = TRUE),
    Mínimo = min(value, na.rm = TRUE),
    Q1 = quantile(value, 0.25, na.rm = TRUE),
    Q3 = quantile(value, 0.75, na.rm = TRUE),
    Máximo = max(value, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(across(where(is.numeric), ~ round(.x, 1)))

tabla_morada(desc, caption = "Tabla 3. Estadísticas descriptivas de las variables cuantitativas")
Tabla 3. Estadísticas descriptivas de las variables cuantitativas
Variable Media Mediana Desv. est. Mínimo Q1 Q3 Máximo
age 41.2 39 10.6 19 33 49 87
balance 1422.7 444 3009.6 -3313 69 1480 71188
campaign 2.8 2 3.1 1 1 3 50
datos_box <- bank %>%
  select(y, all_of(numv)) %>%
  pivot_longer(cols = -y, names_to = "variable", values_to = "valor") %>%
  group_by(variable) %>%
  filter(
    valor >= quantile(valor, 0.01, na.rm = TRUE),
    valor <= quantile(valor, 0.99, na.rm = TRUE)
  ) %>%
  ungroup()

p <- ggplot(datos_box, aes(x = y, y = valor, fill = y)) +
  geom_boxplot(outlier.alpha = 0.25, outlier.color = morado, show.legend = FALSE) +
  scale_fill_manual(values = c("no" = lila, "yes" = morado_medio)) +
  facet_wrap(~ variable, scales = "free_y") +
  labs(
    title = "Figura 2. Variables cuantitativas según contratación",
    subtitle = "Se muestran los valores entre los percentiles 1 y 99",
    x = "¿Contrató el depósito?",
    y = NULL
  ) +
  theme_minimal(base_family = "Times New Roman") +
  theme(
    plot.title = element_text(face = "bold"),
    plot.subtitle = element_text(size = 9),
    strip.text = element_text(face = "bold")
  )

ggplotly(p) %>%
  layout(font = list(family = "Times New Roman"))

La edad tiene una media de 41.2 años y una mediana de 39 años. Su desviación estándar es de 10.6 años. En el gráfico se puede comparar la distribución de la edad entre los clientes que contrataron el depósito y los que no. Si las cajas se superponen bastante, significa que esta variable no permite diferenciar claramente a los dos grupos por sí sola.

El saldo bancario (balance) tiene una media de 1.422,7 € y una mediana de 444 €. La diferencia entre estos valores puede indicar que algunos clientes tienen saldos bastante superiores al de la mayoría. Su desviación estándar es de 3.009,6 €, lo que permite observar la dispersión de los datos. En el diagrama de cajas se puede revisar si hay diferencias entre los clientes que contrataron y los que no.

Por su parte, campaign registra el número de contactos realizados durante la campaña. La media es de 2.8 contactos, mientras que la mediana es de 2. El máximo registrado es de 50 contactos. Estos resultados permiten observar si algunos clientes recibieron muchas más llamadas que otros y comparar el número de contactos según el resultado de la campaña.

En general, los gráficos permiten explorar las diferencias entre los dos grupos, pero no son suficientes para determinar qué variables predicen mejor la contratación. Para observar con más detalle estas diferencias, se comparan las tasas de contratación por grupos de edad y número de contactos.

tasa_edad <- bank %>%
  mutate(
    nivel = cut(
      age,
      breaks = c(-Inf, 30, 40, 50, 60, Inf),
      labels = c("30 o menos", "31-40", "41-50", "51-60", "Más de 60")
    )
  ) %>%
  group_by(nivel) %>%
  summarise(
    n = n(),
    tasa = mean(y == "yes", na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(variable = "Grupo de edad")

tasa_camp <- bank %>%
  mutate(
    nivel = cut(
      campaign,
      breaks = c(0, 1, 2, 3, 5, Inf),
      labels = c("1", "2", "3", "4-5", "6 o más")
    )
  ) %>%
  group_by(nivel) %>%
  summarise(
    n = n(),
    tasa = mean(y == "yes", na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(variable = "Contactos en la campaña")

p <- bind_rows(tasa_edad, tasa_camp) %>%
  ggplot(aes(
    x = nivel,
    y = tasa,
    text = paste0(
      "Grupo: ", nivel,
      "<br>Clientes: ", n,
      "<br>Tasa de contratación: ", pct(tasa)
    )
  )) +
  geom_col(fill = morado, width = 0.65) +
  facet_wrap(~ variable, scales = "free_x") +
  scale_y_continuous(labels = scales::percent) +
  labs(
    title = "Figura 3. Tasa de contratación por grupo de edad y número de contactos",
    x = NULL,
    y = "Tasa de contratación"
  ) +
  theme_minimal(base_family = "Times New Roman")

ggplotly(p, tooltip = "text") %>%
  layout(font = list(family = "Times New Roman"))

En los grupos de edad, la tasa de contratación para los clientes de 30 años o menos es de 14.2 %, mientras que para los mayores de 60 años es de 37.8 %. En este último grupo hay 127 clientes, de los cuales 48 contrataron el depósito. Esta comparación permite identificar posibles diferencias en las tasas de contratación según la edad. Sin embargo, también es importante considerar la cantidad de clientes que conforman cada grupo.

En cuanto al número de contactos, la tasa de contratación para los clientes que recibieron una sola llamada es de 13.8 %, mientras que para aquellos que recibieron seis o más contactos es de 7.2 %. Los grupos intermedios permiten observar cómo varía la tasa de contratación a medida que aumenta el número de llamadas. No obstante, estos resultados no permiten afirmar que el número de contactos sea la causa de una mayor o menor contratación, ya que también pueden influir otras características de los clientes.

En conjunto, estos resultados descriptivos permiten comprender mejor las características de los clientes y explorar posibles diferencias relacionadas con la contratación del depósito. En la siguiente sección se construirán y compararán los modelos KNN y de regresión logística para evaluar su capacidad de clasificar correctamente a los clientes.

3.3 Variables categóricas

tasa_cat <- bank %>%
  select(y, default, housing, loan, poutcome) %>%
  pivot_longer(-y, names_to = "variable", values_to = "nivel") %>%
  group_by(variable, nivel) %>%
  summarise(n = n(), tasa = mean(y == "yes"), .groups = "drop")

p <- ggplot(tasa_cat, aes(nivel, tasa,
                          text = paste0(variable, " = ", nivel, "<br>Clientes: ", n,
                                        "<br>Tasa de contratación: ", pct(tasa)))) +
  geom_col(fill = morado, width = 0.65) +
  facet_wrap(~ variable, scales = "free_x", nrow = 1) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Figura 4. Tasa de contratación por categoría", x = NULL, y = "Tasa de contratación")

ggplotly(p) %>% 
  layout(font = list(family = "Times New Roman")) %>% 
  style(font = list(family = "Times New Roman"))

La variable poutcome parece ser una de las más informativas para explicar la contratación del depósito. Los clientes que obtuvieron un resultado exitoso en una campaña anterior presentan una tasa de contratación de 64.3 %, frente a 9.1 % entre aquellos cuyo resultado anterior es desconocido. Aunque la categoría success comprende solo 129 clientes (2.9 % de la base), representa 15.9 % de todas las contrataciones. Por su parte, los clientes con un resultado anterior desfavorable (failure) presentan una tasa de contratación de 12.9 %. Estas diferencias sugieren que el resultado de campañas anteriores puede estar asociado con la contratación actual. Sin embargo, es necesario evaluar esta relación mediante los modelos de clasificación, considerando simultáneamente las demás variables.

Las variables housing y loan también presentan diferencias en las tasas de contratación. Entre los clientes sin crédito hipotecario, la tasa es de 15.3 %, mientras que entre quienes tienen este tipo de crédito es de 8.6 %. De manera similar, la tasa de contratación es de 12.5 % para los clientes sin préstamo personal y de 6.2 % para quienes sí lo tienen. Estas diferencias muestran una posible asociación entre las obligaciones crediticias y la contratación del depósito. No obstante, el análisis descriptivo no permite concluir que los créditos sean la causa de las diferencias observadas ni establecer si estas se mantienen al considerar otras características de los clientes.

En cuanto a la variable default, solo 76 clientes presentan la categoría de incumplimiento. Su tasa de contratación es de 11.8 %. Debido a la baja cantidad de observaciones en esta categoría, las comparaciones deben interpretarse con cautela, pues las estimaciones pueden ser poco precisas. La importancia de esta variable se evaluará posteriormente a través de los modelos de clasificación y sus métricas de desempeño.

4. RESULTADOS DE LOS MODELOS

4.1 KNN: selección de k

p_yes_knn <- function(Xa, ya, Xb, k) {
  cl <- knn(Xa, Xb, cl = ya, k = k, prob = TRUE)
  pr <- attr(cl, "prob")
  ifelse(cl == "yes", pr, 1 - pr)
}
auc_y <- function(y, p) as.numeric(auc(roc(y, p, levels = c("no", "yes"), direction = "<", quiet = TRUE)))

set.seed(123)
# Asignar folds dentro de cada clase para mantener el desbalance en cada partición.
folds <- integer(nrow(train))
for (clase in levels(train$y)) {
  id_clase <- which(train$y == clase)
  folds[id_clase] <- sample(rep(1:5, length.out = length(id_clase)))
}
ks <- seq(5, 151, by = 2)

cv_auc <- sapply(ks, function(k) {
  mean(sapply(1:5, function(f) {
    tr <- folds != f
    va <- !tr
    # La estandarización se estima solo con el pliegue de entrenamiento.
    centro_f <- colMeans(X_raw_tr[tr, numv, drop = FALSE])
    desv_f   <- apply(X_raw_tr[tr, numv, drop = FALSE], 2, sd)
    xtr_f <- escalar(X_raw_tr[tr, , drop = FALSE], centro_f, desv_f)
    xva_f <- escalar(X_raw_tr[va, , drop = FALSE], centro_f, desv_f)
    auc_y(train$y[va], p_yes_knn(xtr_f, train$y[tr], xva_f, k))
  }))
})
k_opt <- ks[which.max(cv_auc)]

p <- ggplot(data.frame(k = ks, AUC = cv_auc), aes(k, AUC)) +
  geom_line(color = morado, linewidth = 1) +
  geom_point(color = morado_medio, size = 1.6) +
  geom_point(data = data.frame(k = k_opt, AUC = max(cv_auc)), color = morado_oscuro, size = 4) +
  labs(title = paste0("Figura 5. AUC en validación cruzada según k (óptimo: k = ", k_opt, ")"), x = "Número de vecinos (k)")

ggplotly(p) %>% 
  layout(font = list(family = "Times New Roman")) %>% 
  style(font = list(family = "Times New Roman"))

El AUC medio máximo en la validación cruzada es 0.663 y se alcanza con k = 149. Para valores pequeños de k, la clasificación depende de un vecindario reducido; al aumentar k, la predicción se basa en un conjunto más amplio de clientes. La curva permite observar cómo cambia la capacidad de discriminación según el número de vecinos. El valor elegido corresponde al que obtuvo el mayor AUC medio entre los valores evaluados; esta selección se realizó únicamente con el conjunto de entrenamiento.

4.2 Logit

library(car)
logit <- glm(y ~ ., data = train[, c("y", vars_x)], family = binomial)


vif_logit <- vif(logit)
print(vif_logit)
##              GVIF Df GVIF^(1/(2*Df))
## age      1.053039  1        1.026177
## balance  1.016028  1        1.007982
## campaign 1.017676  1        1.008800
## default  1.018566  1        1.009240
## housing  1.069894  1        1.034357
## loan     1.014475  1        1.007212
## poutcome 1.049329  3        1.008058
coefs <- summary(logit)$coefficients
ic    <- exp(confint.default(logit))

tab_logit <- data.frame(
  variable = rownames(coefs),
  coef     = coefs[, 1],
  OR       = exp(coefs[, 1]),
  IC95_inf = ic[, 1],
  IC95_sup = ic[, 2],
  p_num    = coefs[, 4],
  row.names = NULL
)
tab_mostrar <- tab_logit %>%
  mutate(p_valor = ifelse(p_num < 0.001, "<0.001", sprintf("%.3f", p_num)),
         Significativa = ifelse(p_num < 0.05, "Sí", "No")) %>%
  mutate(across(c(coef, OR, IC95_inf, IC95_sup), ~ round(.x, 3))) %>%
  select(Variable = variable, Coeficiente = coef, OR, `IC 95 % inf.` = IC95_inf,
         `IC 95 % sup.` = IC95_sup, `p-valor` = p_valor, Significativa)
tabla_morada(tab_mostrar, caption = "Tabla 4. Coeficientes y odds ratios del modelo Logit (conjunto de entrenamiento)")
Tabla 4. Coeficientes y odds ratios del modelo Logit (conjunto de entrenamiento)
Variable Coeficiente OR IC 95 % inf. IC 95 % sup. p-valor Significativa
(Intercept) -1.914 0.147 0.080 0.272 <0.001 Sí
age 0.010 1.010 0.999 1.020 0.064 No
balance 0.000 1.000 1.000 1.000 0.860 No
campaign -0.073 0.929 0.879 0.983 0.010 Sí
defaultyes 0.533 1.703 0.745 3.893 0.207 No
housingyes -0.535 0.585 0.462 0.742 <0.001 Sí
loanyes -0.588 0.556 0.378 0.817 0.003 Sí
poutcomeother 0.638 1.893 1.093 3.277 0.023 Sí
poutcomesuccess 2.350 10.487 6.011 18.296 <0.001 Sí
poutcomeunknown -0.226 0.798 0.553 1.150 0.226 No
bosque <- tab_logit %>%
  filter(variable != "(Intercept)") %>%
  mutate(variable = reorder(variable, OR))

p <- ggplot(bosque, aes(y = variable,
                        text = paste0(variable, "<br>OR = ", round(OR, 2),
                                      "<br>IC 95 %: ", round(IC95_inf, 2), " a ", round(IC95_sup, 2)))) +
  geom_vline(xintercept = 1, linetype = "dashed", color = "grey50") +
  geom_segment(aes(x = IC95_inf, xend = IC95_sup, yend = variable), color = morado_medio, linewidth = 1) +
  geom_point(aes(x = OR), color = morado_oscuro, size = 3) +
  scale_x_log10() +
  labs(title = "Figura 6. Odds ratios con intervalo de confianza del 95 % (escala logarítmica)",
       x = "Odds ratio (1 = sin efecto)", y = NULL) +
  theme_minimal(base_family = "Times New Roman")

ggplotly(p, tooltip = "text") %>% 
  layout(font = list(family = "Times New Roman"))

Con un nivel de significancia del 5 %, las variables estadísticamente significativas son: campaign, housingyes, loanyes, poutcomeother, poutcomesuccess. Por su parte, las variables que no presentan significancia estadística son: age, balance, defaultyes, poutcomeunknown. En el gráfico de odds ratios, se considera que una asociación es estadísticamente significativa cuando el intervalo de confianza del 95 % no incluye el valor OR = 1.

El resultado más destacado corresponde a poutcomesuccess. En comparación con la categoría de referencia del modelo, los clientes cuyo resultado en una campaña anterior fue exitoso presentan unas odds de contratación 10.49 veces las de los clientes de la categoría de referencia, manteniendo constantes las demás variables. Por otra parte, tener un crédito hipotecario (housingyes, OR = 0.59) o un préstamo personal (loanyes, OR = 0.56) se asocia con menores odds de contratación. Asimismo, cada contacto adicional durante la campaña actual (campaign, OR = 0.93) se asocia con un cambio en las odds de contratación, cuya dirección y magnitud se reflejan en su odds ratio. Estas asociaciones no deben interpretarse automáticamente como relaciones causales.

La interpretación de age, balance y default debe considerar sus respectivos odds ratios, intervalos de confianza y valores p. Si alguna de estas variables no resulta estadísticamente significativa al nivel del 5 %, no existe evidencia suficiente para afirmar que su asociación con la contratación sea distinta de cero dentro del modelo estimado. En particular, la relación entre la edad y la contratación podría no estar representada completamente por un término lineal. Aunque el análisis descriptivo de la sección 3.2 permite identificar diferencias entre grupos etarios, sería necesario evaluar términos no lineales o categorías de edad para determinar si esta relación mejora la capacidad explicativa del modelo.

4.3 Comparación en el conjunto de prueba

p_knn   <- p_yes_knn(X_tr, train$y, X_te, k_opt)
p_logit <- predict(logit, newdata = test, type = "response")


umbral_youden <- function(y, p) {
  r <- roc(y, p, levels = c("no", "yes"), direction = "<", quiet = TRUE)
  as.numeric(coords(r, "best", best.method = "youden", ret = "threshold"))[1]
}
oof_knn   <- numeric(nrow(train))
oof_logit <- numeric(nrow(train))
for (f in 1:5) {
  tr <- folds != f
  va <- !tr

  
  centro_f <- colMeans(X_raw_tr[tr, numv, drop = FALSE])
  desv_f   <- apply(X_raw_tr[tr, numv, drop = FALSE], 2, sd)
  xtr_f <- escalar(X_raw_tr[tr, , drop = FALSE], centro_f, desv_f)
  xva_f <- escalar(X_raw_tr[va, , drop = FALSE], centro_f, desv_f)
  oof_knn[va] <- p_yes_knn(xtr_f, train$y[tr], xva_f, k_opt)

 
  logit_cv <- glm(y ~ ., data = train[tr, c("y", vars_x), drop = FALSE],
                  family = binomial)
  oof_logit[va] <- predict(logit_cv, newdata = train[va, , drop = FALSE],
                           type = "response")
}
thr_knn   <- umbral_youden(train$y, oof_knn)
thr_logit <- umbral_youden(train$y, oof_logit)

metricas <- function(modelo, criterio, p, umbral) {
  pred <- ifelse(p >= umbral, "yes", "no")
  tp <- sum(pred == "yes" & test$y == "yes"); fn <- sum(pred == "no"  & test$y == "yes")
  fp <- sum(pred == "yes" & test$y == "no");  tn <- sum(pred == "no"  & test$y == "no")
  prec <- ifelse(tp + fp == 0, NA, tp / (tp + fp))
  sens <- tp / (tp + fn)
  data.frame(Modelo = modelo, Criterio = criterio, Umbral = umbral,
             Accuracy = (tp + tn) / (tp + tn + fp + fn),
             Sensibilidad = sens, Especificidad = tn / (tn + fp), Precision = prec,
             F1 = ifelse(is.na(prec) || (prec + sens) == 0, 0, 2 * prec * sens / (prec + sens)),
             AUC = auc_y(test$y, p), TP = tp, FN = fn, FP = fp, TN = tn)
}

res <- bind_rows(metricas("KNN",   "Umbral 0.5",    p_knn,   0.5),
                 metricas("Logit", "Umbral 0.5",    p_logit, 0.5),
                 metricas("KNN",   "Umbral Youden", p_knn,   thr_knn),
                 metricas("Logit", "Umbral Youden", p_logit, thr_logit))

v <- function(m, c, col) res[res$Modelo == m & res$Criterio == c, col]
auc_knn <- v("KNN", "Umbral 0.5", "AUC");  auc_logit <- v("Logit", "Umbral 0.5", "AUC")
prev_test <- mean(test$y == "yes")

res %>%
  select(-c(TP, FN, FP, TN)) %>%
  mutate(across(where(is.numeric), ~ round(.x, 3))) %>%
  tabla_morada(caption = "Tabla 5. Métricas en el conjunto de prueba")
Tabla 5. Métricas en el conjunto de prueba
Modelo Criterio Umbral Accuracy Sensibilidad Especificidad Precision F1 AUC
KNN Umbral 0.5 0.500 0.885 0.000 1.000 NA 0.000 0.703
Logit Umbral 0.5 0.500 0.896 0.179 0.989 0.683 0.284 0.687
KNN Umbral Youden 0.114 0.706 0.526 0.730 0.202 0.292 0.703
Logit Umbral Youden 0.106 0.641 0.667 0.637 0.193 0.299 0.687

Con el umbral estándar de 0,5, ambos modelos presentan limitaciones para identificar a los clientes que contrataron el depósito. La sensibilidad es de 0.0 % para KNN y de 17.9 % para Logit. Aunque la exactitud (accuracy) se aproxima al 89 % en ambos modelos, este resultado debe interpretarse con cautela debido al predominio de la clase no en el conjunto de datos. Por tanto, una exactitud elevada no garantiza que el modelo identifique adecuadamente a los clientes que contrataron el producto.

Al utilizar el umbral de Youden, la sensibilidad aumenta a 52.6 % para KNN y a 66.7 % para Logit. Esto significa que KNN identifica aproximadamente la mitad de los clientes que contrataron el depósito, mientras que Logit detecta cerca de dos tercios. Este aumento en la detección se acompaña de una reducción de la exactitud, que pasa a 70.6 % en KNN y a 64.1 % en Logit. Asimismo, disminuye la especificidad, porque los modelos clasifican como posibles contratantes a más clientes que finalmente no contrataron. En consecuencia, la selección del umbral implica un equilibrio entre identificar a más clientes interesados y evitar falsas alarmas.

cmy <- res %>% filter(Criterio == "Umbral Youden")
cm <- bind_rows(lapply(seq_len(nrow(cmy)), function(i)
  data.frame(Modelo = cmy$Modelo[i],
             Real = c("yes", "yes", "no", "no"), Predicho = c("yes", "no", "yes", "no"),
             n = c(cmy$TP[i], cmy$FN[i], cmy$FP[i], cmy$TN[i])))) %>%
  mutate(Real = factor(Real, levels = c("no", "yes")), Predicho = factor(Predicho, levels = c("no", "yes")))

p <- ggplot(cm, aes(Predicho, Real, fill = n)) +
  geom_tile(aes(text = paste0("Modelo: ", Modelo, "<br>Real: ", Real, "<br>Predicho: ", Predicho,
                              "<br>Clientes: ", n)), color = "white", linewidth = 1.5) +
  geom_text(aes(label = n), color = "white", size = 6, fontface = "bold", family = "Times New Roman") +
  scale_fill_gradient(low = morado_medio, high = "#311B92", guide = "none") +
  facet_wrap(~ Modelo) +
  labs(title = "Figura 7. Matrices de confusión con umbral de Youden (conjunto de prueba)",
       x = "Clase predicha", y = "Clase real") +
  theme_minimal(base_family = "Times New Roman")

ggplotly(p, tooltip = "text") %>% 
  layout(font = list(family = "Times New Roman")) %>% 
  style(font = list(family = "Times New Roman"))

La matriz de confusión permite observar este comportamiento con mayor claridad. Con el umbral de Youden, KNN identifica 82 de los 156 clientes que contrataron el depósito en el conjunto de prueba, mientras que Logit identifica 104 de ellos. Sin embargo, ambos modelos generan una cantidad considerable de falsos positivos: 324 para KNN y 435 para Logit. Esto se refleja en precisiones de 20.2 % y 19.3 %, respectivamente. En términos prácticos, por cada contratante identificado, se clasifican aproximadamente 5 clientes como posibles contratantes con KNN y 5 con Logit. Frente a la tasa de contratación del conjunto de prueba, de 11.5 %, esto representa un lift de 1.8 veces para KNN y de 1.7 veces para Logit. Por tanto, los modelos ayudan a priorizar clientes, aunque todavía producen numerosos falsos positivos.

roc_knn   <- roc(test$y, p_knn,   levels = c("no", "yes"), direction = "<", quiet = TRUE)
roc_logit <- roc(test$y, p_logit, levels = c("no", "yes"), direction = "<", quiet = TRUE)
et_knn   <- sprintf("KNN (AUC = %.3f)", auc_knn)
et_logit <- sprintf("Logit (AUC = %.3f)", auc_logit)

roc_df <- bind_rows(
  data.frame(Modelo = et_knn,   FPR = 1 - roc_knn$specificities,   TPR = roc_knn$sensitivities),
  data.frame(Modelo = et_logit, FPR = 1 - roc_logit$specificities, TPR = roc_logit$sensitivities))

p <- ggplot(roc_df, aes(FPR, TPR, color = Modelo, linetype = Modelo)) +
  geom_line(linewidth = 1) +
  geom_abline(linetype = "dotted", color = "grey50") +
  scale_color_manual(values = setNames(c(morado_medio, morado_oscuro), c(et_knn, et_logit))) +
  scale_linetype_manual(values = setNames(c("dashed", "solid"), c(et_knn, et_logit))) +
  labs(title = "Figura 8. Curvas ROC en el conjunto de prueba", x = "1 − Especificidad", y = "Sensibilidad")

ggplotly(p) %>% 
  layout(font = list(family = "Times New Roman")) %>% 
  style(font = list(family = "Times New Roman"))

Las curvas ROC se encuentran por encima de la diagonal de referencia, correspondiente a un AUC de 0,5. El AUC es de 0.703 para KNN y de 0.687 para Logit. Estos resultados indican una capacidad de discriminación limitada, aunque superior a la esperada por azar. KNN obtiene el mayor AUC, con una diferencia absoluta de 0.015. Sin embargo, esta diferencia, calculada sobre una sola partición de los datos, no basta para afirmar que un modelo sea estadísticamente superior al otro. El AUC mide la capacidad del modelo para asignar puntuaciones de probabilidad más altas a los clientes que contrataron que a los que no contrataron. En este caso, ambos modelos distinguen las dos clases, pero todavía tienen dificultades para separarlas con precisión. A diferencia de la exactitud, la sensibilidad y la precisión, el AUC no cambia al modificar el umbral de clasificación, porque se calcula a partir de las probabilidades estimadas.

Modelo recomendado. La elección del modelo depende del objetivo principal de la clasificación. Si se busca identificar a la mayor cantidad posible de clientes que podrían contratar el depósito, se recomienda Logit con el umbral de Youden, ya que alcanza una sensibilidad de 66.7 % y un F1 de 0.299, ligeramente superior al 0.292 obtenido por KNN. Además, la regresión logística permite interpretar la asociación entre las variables explicativas y la probabilidad de contratación. Por otro lado, KNN presenta un AUC de 0.703, superior al 0.687 de Logit, y obtiene mejores valores de exactitud y precisión con el umbral de Youden. Por ello, KNN también constituye una alternativa válida si se prioriza la discriminación global o se busca una mayor exactitud. En conclusión, se prefiere Logit cuando la prioridad es detectar más contratantes y contar con un modelo interpretable, reconociendo que ninguno de los dos modelos presenta una precisión alta. Antes de implementar cualquiera de ellos en una campaña real, sería conveniente evaluar su estabilidad con otras particiones de los datos y considerar los costos de los falsos positivos y los beneficios de las contrataciones.

5. CONCLUSIONES

La comparación entre los modelos K-Nearest Neighbors (KNN) y regresión logística (Logit) permitió identificar diferencias en su capacidad para predecir la contratación de depósitos bancarios a plazo fijo. Los resultados mostraron que KNN obtuvo un AUC de 0,703, superior al 0,687 de Logit, mientras que este último alcanzó una sensibilidad del 66,7 % y un F1 de 0,299 con el umbral de Youden, frente al 52,6 % y 0,292 de KNN, respectivamente. Estos resultados indican que KNN presentó una mayor capacidad de discriminación global en el conjunto de prueba, mientras que Logit logró identificar una mayor proporción de clientes contratantes. Por tanto, ninguno de los modelos fue superior en todos los criterios evaluados y su utilidad depende del objetivo de clasificación.

Desde la perspectiva comercial, los modelos mostraron potencial para apoyar la selección de clientes en las campañas de marketing telefónico. Con el umbral de Youden, la precisión fue del 20,2 % para KNN y del 19,3 % para Logit, valores superiores a la tasa general de contratación del 11,5 %. Sin embargo, ambos modelos generaron numerosos falsos positivos, lo que significa que una proporción considerable de los clientes identificados como posibles contratantes no adquirió finalmente el producto. Adicionalmente, el modelo Logit identificó asociaciones estadísticamente significativas entre la contratación y variables como el resultado exitoso de campañas anteriores, el número de contactos y la tenencia de créditos hipotecarios o préstamos personales. Estos hallazgos aportan información para comprender el comportamiento de los clientes, aunque no permiten establecer relaciones causales.

En relación con el objetivo de la investigación, se logró construir y comparar los dos modelos de clasificación mediante métricas de exactitud, sensibilidad, precisión, F1 y AUC. La evaluación permitió reconocer sus fortalezas y limitaciones, así como comprobar que el umbral de clasificación influye considerablemente en la detección de contratantes, especialmente cuando existe un desequilibrio importante entre las clases. No obstante, aunque ambos modelos mostraron una capacidad de discriminación superior a la esperada por azar, sus niveles de precisión evidencian que todavía presentan dificultades para identificar de manera acertada a los clientes que contratarán el depósito. En consecuencia, el objetivo de comparación se cumplió, pero los resultados no son suficientes para recomendar una implementación comercial sin evaluaciones adicionales.

Entre las principales limitaciones del estudio se encuentran el desequilibrio de la variable objetivo, que solo registra un 11,5 % de contrataciones, y el uso de una única partición de entrenamiento y prueba para la evaluación final. Estas condiciones pueden influir en las métricas obtenidas y limitan la generalización de los resultados. Asimismo, la diferencia entre los valores AUC de KNN y Logit no permite afirmar que exista una superioridad estadísticamente significativa de uno sobre el otro. Por ello, los resultados deben interpretarse como una evaluación inicial de su desempeño y no como una garantía de predicción para futuras campañas.

6. RECOMENDACIONES

A partir de los resultados obtenidos, se recomienda considerar la regresión logística con el umbral de Youden cuando la prioridad sea identificar una mayor proporción de clientes que podrían contratar el depósito, debido a su mayor sensibilidad y a su F1 ligeramente superior. Por otra parte, KNN constituye una alternativa relevante cuando se prioriza la discriminación global, dado que obtuvo el mayor AUC. La elección definitiva debería considerar los objetivos de la campaña, los costos de contactar a clientes que no contratan y las oportunidades perdidas cuando no se identifica a un posible contratante. En este sentido, los modelos deberían utilizarse como herramientas de apoyo para priorizar contactos y no como mecanismos que garanticen la contratación.

También se recomienda profundizar en la evaluación de los modelos mediante diferentes particiones de los datos o validación cruzada externa, con el fin de comprobar la estabilidad de las métricas y determinar si las diferencias observadas se mantienen en otras muestras. Asimismo, sería conveniente explorar otras variables disponibles en la base de datos, estudiar posibles relaciones no lineales entre las características de los clientes y la contratación, y evaluar técnicas apropiadas para abordar el desequilibrio de las clases. Estas acciones permitirían examinar si es posible mejorar la capacidad de identificación de los clientes contratantes.

Finalmente, antes de utilizar los modelos en una campaña real, se recomienda evaluar la calibración de las probabilidades estimadas y seleccionar los umbrales de clasificación teniendo en cuenta los costos y beneficios comerciales. Aunque el umbral de Youden ofrece un criterio estadístico para equilibrar sensibilidad y especificidad, no necesariamente maximiza la rentabilidad de la campaña. Incorporar información sobre el costo de cada contacto y el beneficio esperado de las contrataciones permitiría establecer qué modelo y umbral resultan más útiles para la toma de decisiones y contribuiría a mejorar la eficiencia de las estrategias de marketing bancario.

7. BIBLIOGRAFÍA

  • Moro, S., Cortez, P., & Rita, P. (2014). A data-driven approach to predict the success of bank telemarketing. Decision Support Systems, 62, 22–31.
  • Dua, D., & Graff, C. (2019). UCI Machine Learning Repository: Bank Marketing Data Set. University of California, Irvine. https://archive.ics.uci.edu/dataset/222/bank+marketing
  • James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning with Applications in R (2.ª ed.). Springer.
  • Hosmer, D. W., Lemeshow, S., & Sturdivant, R. X. (2013). Applied Logistic Regression (3.ª ed.). Wiley.
  • Youden, W. J. (1950). Index for rating diagnostic tests. Cancer, 3(1), 32–35.
  • Robin, X., Turck, N., Hainard, A., et al. (2011). pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics, 12, 77.
  • Venables, W. N., & Ripley, B. D. (2002). Modern Applied Statistics with S (4.ª ed.). Springer.
  • Sievert, C. (2020). Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC.
  • Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer.