1 Introducción

Colombia cuenta con miles de restaurantes que operan en condiciones muy distintas entre sí. Algunos logran generar altos ingresos a partir de su inversión en infraestructura, personal y servicios, mientras que otros, a pesar de operar en el mismo sector, no alcanzan los mismos resultados. Entender qué factores operativos están detrás de esa diferencia tiene implicaciones directas para quienes toman decisiones en el sector y para las políticas de apoyo a las pequeñas y medianas empresas de alimentos en el país.

Este taller analiza datos de la Encuesta Anual de Servicios del DANE correspondientes al año 2024, que reúne información detallada de 294 restaurantes colombianos. La pregunta que guía el trabajo es concreta: ¿es posible clasificar un restaurante como de altos o bajos ingresos a partir de su estructura de inversión operativa? Variables como los gastos en energía, telecomunicaciones, arrendamiento, mantenimiento y el uso de personal temporal capturan la capacidad y escala de operación de cada negocio, y se espera que esa capacidad sea un buen predictor de sus ingresos totales.

Para responder esa pregunta se aplican dos modelos de clasificación supervisada. El primero es K-Nearest Neighbors, un algoritmo que clasifica cada restaurante según el comportamiento de los más similares a él dentro de los datos de entrenamiento, sin asumir ninguna relación funcional preestablecida entre las variables. El segundo es la Regresión Logística, que estima la probabilidad de que un restaurante tenga altos ingresos en función de sus indicadores operativos, y permite además entender el peso individual de cada variable en esa predicción. Comparar ambos modelos permite identificar cuál captura mejor los patrones de ingresos en el sector restaurantero colombiano.

library(tidyverse)
library(readxl)
library(caret)
library(class)
library(pROC)
library(kableExtra)
library(plotly)

EAS2024 = read_excel("EAS2024.xlsx", sheet = 1)

2 Metodología

2.1 Descripción de variables

La variable que se busca predecir es si un restaurante tiene altos o bajos ingresos totales durante el año 2024. Para construirla se compararon los ingresos de cada restaurante con la mediana del sector: los que la superan se clasifican como de altos ingresos y los que quedan por debajo como de bajos ingresos. Esta forma de construir la variable garantiza un balance perfecto entre las dos categorías y tiene una interpretación económica clara: separa los restaurantes que generan más ingresos que la mitad del sector de los que generan menos.

La lógica detrás de las variables predictoras es que un restaurante invierte más en operación porque opera a mayor escala, y esa mayor escala es precisamente lo que le permite generar más ingresos. En otras palabras, los gastos operativos son una señal de la capacidad del negocio y esa capacidad determina sus ingresos.

Para predecirla se escogieron cinco variables que representan diferentes dimensiones de la operación de un restaurante:

Telecomunicaciones Incluye los gastos en servicios de telefonía, internet y sistemas de comunicación. Un restaurante que invierte más en telecomunicaciones probablemente tiene sistemas de pedidos digitales, plataformas de domicilios y mejor gestión de pagos, lo que amplía sus canales de venta y su capacidad de generar ingresos.

Arrendamiento Corresponde a los gastos en alquiler del local. Un mayor arrendamiento refleja un local más grande, mejor ubicado o con mayor visibilidad, condiciones que permiten atender más clientes y generar mayores ingresos.

Gastos de viaje Corresponde a los gastos en transporte y viáticos del personal durante el año. Para un restaurante esto refleja la logística de abastecimiento, movilización del personal entre sedes o entregas propias. Un restaurante con más gastos de viaje tiene más actividad operativa y mayor alcance geográfico.

Seguros Recoge los gastos del restaurante en pólizas de seguro durante el año, incluyendo seguros contra incendio, robo, responsabilidad civil y protección de equipos. Esta variable refleja el nivel de formalización del negocio, un restaurante más consolidado y con más activos tiende a asegurar más su operación, lo que a su vez está asociado con mayores ingresos.

Sistemas de información Corresponde a los gastos en sistemas de información y software del restaurante, incluyendo licencias, sistemas de gestión de pedidos, facturación electrónica y plataformas digitales. Un restaurante que invierte más en tecnología de gestión tiene mayor capacidad operativa, mejor control de su negocio y más canales de venta, todo lo cual contribuye a generar mayores ingresos.

K-Nearest Neighbors (KNN)

KNN es un algoritmo de clasificación que no asume ninguna distribución específica de los datos. Para clasificar un restaurante nuevo busca los K restaurantes más similares dentro del conjunto de entrenamiento y asigna la categoría más frecuente entre ellos. Es como preguntarle a los negocios más parecidos si tienen altos o bajos ingresos y quedarse con la respuesta más común. Su principal ventaja es la flexibilidad pero es sensible a la escala de las variables, por lo que es necesario estandarizarlas antes de aplicarlo. El valor de K se selecciona automáticamente probando diferentes opciones y eligiendo la que mejor resultado da.

Regresión Logística (Logit)

La regresión logística estima la probabilidad de que un restaurante pertenezca a la categoría de altos ingresos a partir de sus indicadores operativos. A diferencia del KNN no busca negocios similares sino que aprende una relación entre las variables predictoras y la probabilidad de tener altos ingresos. Su resultado siempre es un número entre 0 y 1 interpretable como probabilidad, y si supera un umbral definido el restaurante se clasifica como de altos ingresos. Su principal ventaja es que permite entender el efecto individual de cada variable, lo que hace sus resultados más útiles para la toma de decisiones en el sector.

2.2 Preparación de los datos

datos_modelo <- EAS2024 %>%
  filter(Seccion24 == "I2") %>%
  rename(
    arrendamiento      = ocgar,
    telecomunicaciones = ocgtg,
    gastos_viaje       = gsptot,
    seguros            = ocgse,
    sistemas           = ocgusi,
    ingresos           = insertot
  ) %>%
  select(arrendamiento, telecomunicaciones,
         gastos_viaje, seguros, sistemas, ingresos) %>%
  mutate(across(everything(), as.numeric)) %>%
  filter_all(all_vars(. > 0)) %>%
  drop_na()

# Calculamos la mediana ANTES de normalizar
mediana_ing = median(datos_modelo$ingresos)

# Creamos la variable Y como factor con dos niveles
datos_modelo = datos_modelo %>%
  mutate(
    nivel_ingreso = factor(
      ifelse(ingresos >= mediana_ing, "Alto", "Bajo"),
      levels = c("Bajo", "Alto")
    )
  ) 

# Datos para los graficos
datos_grafico <- datos_modelo

# Quitamos ingresos para modelar
datos_modelo = datos_modelo %>%
  select(-ingresos)


# Normalizamos las variables X usando Z-score
datos_modelo = datos_modelo %>%
  mutate(across(
    c(arrendamiento, telecomunicaciones, gastos_viaje, seguros, sistemas),
    ~ as.numeric(scale(.))
  ))

# Verificamos filas y columnas
dim(datos_modelo)
## [1] 294   6
# Verificamos el balance de la variable Y
table(datos_modelo$nivel_ingreso)
## 
## Bajo Alto 
##  147  147

La base de datos seleccionada del dane, registra diferentes situaciones economica de los sectores laborales de colombia, por este motivo se realizo un proceso de filtrado de datos para asi tener los datos que se decideron modelar, se filtro las empresas tipo restaurante, se renombro para que sea mas facil el analisis, se filtraron las empresas que no dieron infromacion importante como los ingresos obtenidos ya que esto afecta al resultado del estudio, despues se calculo la mediana que es el valor que usaremos como referencia para la fontera de los datos y se revizo que los valores estuvieran de una manera equilibrada para que el analisis sea mas preciso. la tabla queda con 294 restaurantes como muestra y 6 variables.

3 Resultados Descriptivos

3.1 Estadísticas descriptivas

En este bloque se presentan las principales estadísticas de las variables del modelo. Esto permite entender el comportamiento general de los indicadores operativos de los restaurantes colombianos antes de construir los modelos.

# Calculamos las estadísticas descriptivas con los datos sin normalizar
descriptivas = datos_grafico %>%
  select(-nivel_ingreso) %>%
  pivot_longer(everything(),
               names_to  = "Variable",
               values_to = "Valor") %>%
  group_by(Variable) %>%
  summarise(
    Mínimo  = round(min(Valor), 0),
    Q1      = round(quantile(Valor, 0.25), 0),
    Mediana = round(median(Valor), 0),
    Media   = round(mean(Valor), 0),
    Q3      = round(quantile(Valor, 0.75), 0),
    Máximo  = round(max(Valor), 0)
  )

# Tabla kableExtra
descriptivas %>%
  kable(caption = "Estadísticas descriptivas de las variables operativas",
        format.args = list(big.mark = ",")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE)
Estadísticas descriptivas de las variables operativas
Variable Mínimo Q1 Mediana Media Q3 Máximo
arrendamiento 622 221,484 763,903 3,017,994 2,285,800 54,056,042
gastos_viaje 2,086 72,844 163,264 469,397 383,422 7,223,969
ingresos 1,499,127 8,015,278 16,451,685 46,477,258 31,653,858 767,222,791
seguros 50 11,803 33,646 111,138 83,736 3,754,531
sistemas 556 21,469 56,844 230,402 158,628 7,798,544
telecomunicaciones 1,396 67,730 279,118 1,110,613 1,101,791 18,309,571

La tabla muestra que el arrendamiento es la variable con mayor dispersión y valores más altos, con una mediana de 763.903 pesos y un máximo de 54 millones, lo que refleja las grandes diferencias en ubicación y tamaño de los locales entre restaurantes colombianos. La media de 3 millones es muy superior a la mediana, confirmando que hay un grupo pequeño de restaurantes con arriendos muy altos que jalan el promedio hacia arriba.

Las telecomunicaciones tienen la segunda mediana más alta con 279.118 pesos y valores que llegan hasta 18 millones, mostrando que hay restaurantes con inversiones importantes en conectividad y sistemas digitales frente a otros con gastos más modestos.

Los gastos de viaje presentan una mediana de 163.264 pesos con valores entre 2.086 y 7.2 millones, lo que indica una variabilidad considerable en la actividad logística y de movilización del personal entre restaurantes.

Los sistemas de información tienen una mediana de 56.844 pesos pero una media de 230.402, señalando que aunque la mayoría de restaurantes invierte poco en tecnología de gestión, hay un grupo que invierte significativamente más y que como vimos en el modelo Logit, esa inversión está fuertemente asociada con altos ingresos.

Los seguros son la variable con los valores más bajos, con una mediana de 33.646 pesos y un máximo de 3.7 millones. Esto sugiere que aunque la mayoría de restaurantes tiene algún tipo de seguro, las pólizas varían considerablemente en cobertura y costo entre negocios de diferente tamaño y nivel de formalización.

ggplot(datos_grafico, aes(x = ingresos)) +
  geom_histogram(bins = 30, fill = "skyblue", color = "black") +
  scale_x_log10() +
  labs(
    title = "Distribución logarítmica de ingresos",
    x = "Ingresos",
    y = "Frecuencia"
  ) +
  theme_minimal()

En este gráfico de distribución logarítmica podemos apreciar una fuerte asimetría en la que la mayor parte de restaurantes se concentra en rangos intermedios, mientras que una pequeña cantidad de negocios alcanza niveles altos. Este comportamiento refleja una fuerte disperción económica dentro de la muestra analizada.

arrendamientoingreso <- ggplot(datos_grafico, aes(x = nivel_ingreso, y = arrendamiento, fill = nivel_ingreso,
                              text = paste("Nivel:", nivel_ingreso,
                                           "<br>Valor:", round(arrendamiento,2)))) +
  geom_boxplot() +
  labs(
    title = "Arrendamiento según nivel de ingreso",
    x = "Nivel de ingreso",
    y = "Arrendamiento"
  ) +
  theme_minimal()

ggplotly(arrendamientoingreso, tooltip = "text")
sistemasdeinfingresos <- ggplot(datos_grafico,
            aes(x = nivel_ingreso,
                y = sistemas,
                fill = nivel_ingreso,
                text = paste(
                  "Nivel:", nivel_ingreso,
                  "<br>Sistemas:", round(sistemas, 2)
                ))) +
  geom_boxplot() +
  labs(
    title = "Sistemas de información según nivel de ingreso",
    x = "Nivel de ingreso",
    y = "Sistemas de información"
  ) +
  theme_minimal()

ggplotly(sistemasdeinfingresos, tooltip = "text")
boxplot <- ggplot(datos_grafico,
       aes(x = nivel_ingreso,
           y = seguros,
           fill = nivel_ingreso,
           text = paste("Nivel:", nivel_ingreso,
                        "<br>Seguros:", round(seguros, 2)))) +
  geom_boxplot(alpha = 0.7) +
  labs(
    title = "Seguros según nivel de ingresos",
    x = "Nivel de ingreso",
    y = "Seguros"
  ) +
  theme_minimal()

ggplotly(boxplot, tooltip = "text")
gastosviaje <- ggplot(datos_grafico,
            aes(x = nivel_ingreso,
                y = gastos_viaje,
                fill = nivel_ingreso,
                text = paste(
                  "Nivel:", nivel_ingreso,
                  "<br>Gastos viaje:", round(gastos_viaje, 2)
                ))) +
  geom_boxplot() +
  labs(
    title = "Gastos de viaje según nivel de ingreso",
    x = "Nivel de ingreso",
    y = "Gastos de viaje"
  ) +
  theme_minimal()

ggplotly(gastosviaje, tooltip = "text")

El primer boxplot muestra la distribución del gasto en arrendaminetos entre los restaurantes de altos y bajos ingresos, por la mediana de ambos grupos se puede apreciar que los restaurantes con altos ingresos tienden a tener mayores gastos en arrendamiento que el promedio, a su vez en el rango intercuartílico apreciamos una caja mas amplia por parte de los gastos “altos”, reflejando un mayor rango en los gastos de arrendamiento para este grupo.

Podemos apreciar una relación entre el gasto en arriendos y la probabilidad de contar con mayores ingresos, lo cual tiene sentido ya que locales con mejor ubicación, tamaño y personal tienden a requerir un mayor gasto en arrendamiento.

El segundo boxplot compara la distribución de los gastos en sistemas de información. Se observa que los restaurantes de altos ingresos presentan una mayor dispersión y varios valores atípicos elevados, lo que indica que algunos negocios realizan inversiones considerablemente mayores en tecnología y sistemas digitales. Sin embargo, gran parte de los restaurantes mantiene niveles relativamente bajos de inversión en este aspecto, lo que sugiere que la digitalización aún no es una práctica generalizada en el sector, sino más bien una apusta estratégica empleada por ciertos establecimientos.

Por la parte de los seguros, podemos apreciar diferencias importantes. Para los altos ingresos se encuentra una mayor dispersión y valores atípicos, acompañada de valores atípicos, mientras que para los bajos ingresos se puede apreciar una caja mas compacta, lo que nos sugiere que este grupo de negocios destina menos dinero a recursos de protección y pólizas, esto nos sugiere que la mayoria de restaurantes exitosos no necesitan asegurarse de manera intensiva para alcanzar grandes ingresos.

arrendamientoingresos <- ggplot(datos_grafico, aes(
           x = arrendamiento, 
           y = nivel_ingreso, 
           color = nivel_ingreso,
           text = paste(
             "Nivel ingreso:", nivel_ingreso,
             "<br>Arrendamiento:", round(arrendamiento, 2)
           ))) +
  geom_jitter(width = 0, height = 0.1, alpha = 0.7) +
  labs(
    title = "Relación entre arrendamiento y nivel de ingreso",
    x = "Arrendamiento",
    y = "Nivel de ingreso"
  ) +
  theme_minimal()

ggplotly(arrendamientoingresos, tooltip = "text")

Este gráfico presenta una separación parcial entre los restuarantes de ambos grupos a partir de los gastos realizados en arrendamiento, los restaurantes de bajos ingresos se concentran en niveles bajos, presentando poca disperción, al contrario que los restaurantes de altos ingresos los cuales presentan una distribución mucho mas amplia. Como se sugería con el boxplot, este gráfico tambien muestra valores de arrendamiento mas altos para el grupo de ingresos altos, confirmando lo planteado anteriormente de que las inversiones de mayor escala en alquiler suele pertencer a negocios con mayor capacidad económica.

sistemasingresos <- ggplot(datos_grafico, aes(
           x = sistemas, 
           y = nivel_ingreso, 
           color = nivel_ingreso,
           text = paste(
             "Nivel ingreso:", nivel_ingreso,
             "<br>Sistemas:", round(sistemas, 2)
           ))) +
  geom_jitter(width = 0, height = 0.1, alpha = 0.7) +
  labs(
    title = "Relación entre sistemas y nivel de ingresos",
    x = "Sistemas de información",
    y = "Nivel de ingreso"
  ) +
  theme_minimal()

ggplotly(sistemasingresos, tooltip = "text")
arrendiamientoteleco <- ggplot(datos_grafico,
       aes(x = arrendamiento,
           y = telecomunicaciones,
           color = nivel_ingreso,
           text = paste(
             "Nivel ingreso:", nivel_ingreso,
             "<br>Arrendamiento:", round(arrendamiento, 2),
             "<br>Telecom:", round(telecomunicaciones, 2)
           ))) +
  geom_point(alpha = 0.7) +
  labs(
    title = "Relación entre arrendamiento y telecomunicaciones según nivel de ingresos",
    x = "Arrendamiento",
    y = "Telecomunicaciones"
  ) +
  theme_minimal()

ggplotly(arrendiamientoteleco, tooltip = "text")

El gráfico evidencia que los restaurantes de altos ingresos tienden a presentar mayores niveles de gasto en telecomunicaciones en comparación con los de bajos ingresos. Esto sugiere que la inversión en herramientas de comunicación está asociada con una mayor capacidad operativa y comercial. Además, la mayor dispersión observada en el grupo de altos ingresos nos sugiere que algunos restaurantes realizan inversiones superiores en este tipo de servicios, posiblemente como estrategia para ampliar canales de venta, mejorar la atención al cliente y optimizar sus procesos internos, no obstante se pude apreciar una gran cantidad de valores agrupados en los altos ingresos en las zonas mas bajas del gráfico, por lo que estas inversiones no son una norma general para el aumento de ingresos si no mas un movimiento estratégico adoptada por ciertos establecimientos.

3.2 Matriz de correlaciones

# Calculamos la matriz de correlaciones con los datos sin normalizar
cor_matrix <- datos_grafico %>%
  select(-nivel_ingreso) %>%
  cor() %>%
  round(2)

# Convertimos a formato largo para plotly
cor_df <- as.data.frame(cor_matrix) %>%
  rownames_to_column("Variable1") %>%
  pivot_longer(-Variable1,
               names_to  = "Variable2",
               values_to = "Correlacion")

# Mapa de calor interactivo
cor_df %>%
  plot_ly(x = ~Variable2, y = ~Variable1,
          z = ~Correlacion, type = "heatmap",
          colorscale = list(c(0, "#8E44AD"),
                            c(0.5, "white"),
                            c(1, "#E67E22")),
          text  = ~paste("Correlacion:", Correlacion),
          zmin  = -1, zmax = 1) %>%
  layout(title = "Matriz de correlaciones entre variables predictoras",
         xaxis = list(title = ""),
         yaxis = list(title = ""))

La matriz de correlaciones confirma que no hay multicolinealidad problemática entre las variables del modelo. La correlación más alta es de 0.59 entre arrendamiento y telecomunicaciones, que está por debajo del umbral de 0.7 considerado problemático. Las demás correlaciones son moderadas o bajas, lo que garantiza que cada variable aporta información independiente y no redundante al modelo. Esto es una condición importante para que el modelo Logit estime correctamente los coeficientes de cada variable.

4 Resultados del modelo

4.1 Modelo Knn

set.seed(28)

# Particion 75% entrenamiento - 25% prueba
index_entrena <- createDataPartition(y = datos_modelo$nivel_ingreso,
                                     p = 0.75,
                                     list = FALSE)

index_vector <- as.vector(index_entrena)

# Separamos entradas y salidas de entrenamiento y prueba
rest_entrena_input  <- datos_modelo[index_vector, 1:5]
rest_test_input     <- datos_modelo[-index_vector, 1:5]
rest_entrena_output <- datos_modelo[index_vector, 6, drop = TRUE]
rest_test_output    <- datos_modelo[-index_vector, 6, drop = TRUE]

# K del 1 al 50 para encontrar el optimo
k <- 1:50
resultado <- data.frame(k, precision = 0)

for(n in k){
  predicciones_restaurante <- knn(train = rest_entrena_input,
                                  cl    = rest_entrena_output,
                                  test  = rest_test_input,
                                  k     = n)
  resultado$precision[n] <- mean(predicciones_restaurante == rest_test_output)
}

# Tabla de precision por K
resultado %>%
  kable(caption = "Precisión del modelo KNN por valor de K",
        col.names = c("K", "Precisión"),
        digits = 4) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(which.max(resultado$precision),
           bold = TRUE, background = "#D5E8D4")
Precisión del modelo KNN por valor de K
K Precisión
1 0.8611
2 0.8056
3 0.8056
4 0.8194
5 0.8056
6 0.8056
7 0.7917
8 0.7917
9 0.8056
10 0.7778
11 0.8056
12 0.7917
13 0.8056
14 0.7639
15 0.7778
16 0.7639
17 0.7917
18 0.7639
19 0.7778
20 0.7778
21 0.7778
22 0.7778
23 0.7778
24 0.7778
25 0.7778
26 0.7778
27 0.7778
28 0.7639
29 0.7639
30 0.7778
31 0.7639
32 0.7639
33 0.7778
34 0.7500
35 0.7778
36 0.7778
37 0.7639
38 0.7500
39 0.7639
40 0.7500
41 0.7500
42 0.7639
43 0.7500
44 0.7639
45 0.7500
46 0.7361
47 0.7361
48 0.7361
49 0.7361
50 0.7361
# Graficamos la precision por K
resultado %>%
  ggplot() +
  aes(k, precision) +
  geom_line(color = "#3498DB") +
  geom_vline(xintercept = which.max(resultado$precision),
             linetype = "dashed", color = "#E67E22") +
  labs(title = paste("Precisión por K | Mejor K =",
                     which.max(resultado$precision)),
       x = "K", y = "Precisión") +
  theme_minimal()

# Identificamos el mejor K
mejor_k <- resultado$k[which.max(resultado$precision)]
print(paste("Mejor K:", mejor_k))
## [1] "Mejor K: 1"
# Prediccion con el K optimo
predicciones_finales <- knn(train = rest_entrena_input,
                            cl    = rest_entrena_output,
                            test  = rest_test_input,
                            k     = mejor_k)

# Exactitud del modelo
exactitud <- mean(predicciones_finales == rest_test_output)
print(paste("Exactitud del modelo KNN:", round(exactitud, 4)))
## [1] "Exactitud del modelo KNN: 0.8611"
# Matriz de confusion en tabla estética
cm_knn <- confusionMatrix(predicciones_finales,
                          rest_test_output,
                          positive = "Alto")

# Tabla de la matriz de confusion
as.data.frame(cm_knn$table) %>%
  rename(Prediccion = Prediction,
         Referencia = Reference,
         Frecuencia = Freq) %>%
  kable(caption = "Matriz de confusión - Modelo KNN") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")
Matriz de confusión - Modelo KNN
Prediccion Referencia Frecuencia
Bajo Bajo 32
Alto Bajo 4
Bajo Alto 6
Alto Alto 30
# Tabla de métricas
data.frame(
  Métrica = c("Exactitud", "Sensibilidad", "Especificidad",
              "Val. Pred. Positivo", "Val. Pred. Negativo", "Kappa"),
  Valor   = c(round(cm_knn$overall["Accuracy"], 4),
              round(cm_knn$byClass["Sensitivity"], 4),
              round(cm_knn$byClass["Specificity"], 4),
              round(cm_knn$byClass["Pos Pred Value"], 4),
              round(cm_knn$byClass["Neg Pred Value"], 4),
              round(cm_knn$overall["Kappa"], 4))
) %>%
  kable(caption = "Métricas del modelo KNN") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE)
Métricas del modelo KNN
Métrica Valor
Accuracy Exactitud 0.8611
Sensitivity Sensibilidad 0.8333
Specificity Especificidad 0.8889
Pos Pred Value Val. Pred. Positivo 0.8824
Neg Pred Value Val. Pred. Negativo 0.8421
Kappa Kappa 0.7222

Los resultados del modelo KNN muestran una exactitud del 86.11%, lo que significa que de cada 100 restaurantes el modelo clasifica correctamente 86. Este resultado es significativamente mejor que el azar, como lo confirma el valor p menor a 0.05 frente al No Information Rate del 50%.

La sensibilidad del 83.33% indica que de cada 100 restaurantes que realmente tienen altos ingresos, el modelo identifica correctamente 83. La especificidad del 88.89% muestra que el modelo es aún mejor reconociendo los restaurantes de bajos ingresos, acertando en casi 89 de cada 100 casos.

El valor predictivo positivo del 88.24% significa que cuando el modelo predice que un restaurante tiene altos ingresos, acierta casi 9 de cada 10 veces. El valor predictivo negativo del 84.21% indica que cuando predice bajos ingresos, acierta en el 84% de los casos.

El índice Kappa de 0.72 refleja un acuerdo bueno entre las predicciones del modelo y los valores reales, corrigiendo el azar. En conjunto el modelo KNN tiene un desempeño muy sólido para clasificar restaurantes según su nivel de ingresos.

library(ROCR)

# Convertimos las predicciones y valores reales a numérico 
pred_knn = prediction(as.numeric(predicciones_finales),
                       as.numeric(rest_test_output))

# Calculamos la curva ROC con tasa de verdaderos positivos y tasa de falsos positivos
perf_knn = performance(pred_knn, "tpr", "fpr")

# Curva ROC
plot(perf_knn,
     main = "Curva ROC - Modelo KNN",
     col  = "blue")

# AUC
auc_knn = performance(pred_knn, "auc")
print(paste("AUC del modelo KNN:", round(auc_knn@y.values[[1]], 3)))
## [1] "AUC del modelo KNN: 0.861"

La curva ROC del modelo KNN muestra un AUC de 0.861, lo que indica que el modelo tiene una muy buena capacidad para distinguir entre restaurantes de altos y bajos ingresos. Un AUC de 1.0 sería un modelo perfecto y un AUC de 0.5 equivaldría a clasificar al azar, por lo que nuestro valor de 0.861 refleja un modelo con alto poder discriminatorio.

Esto significa que si tomamos al azar un restaurante de altos ingresos y uno de bajos ingresos, el modelo tiene un 86.1% de probabilidad de asignarle una probabilidad más alta de altos ingresos al que realmente la tiene. La curva se aleja notoriamente de la diagonal, confirmando que el modelo aprende patrones reales y no clasifica al azar.

4.2 Modelo Logit

La regresión logística estima la probabilidad de que un restaurante tenga altos ingresos en función de sus indicadores operativos. Se usa la misma partición de datos del modelo KNN para comparar resultados en igualdad de condiciones.

# Particion del KNN
train <- datos_modelo[index_vector, ]
test  <- datos_modelo[-index_vector, ]

# Entrenamos el modelo Logit con las 5 variables predictoras
fit_logit <- glm(nivel_ingreso ~ arrendamiento + telecomunicaciones +
                   gastos_viaje + seguros + sistemas,
                 data   = train,
                 family = binomial())

# Coeficientes en tabla estética
coeficientes <- summary(fit_logit)$coefficients %>%
  as.data.frame() %>%
  rownames_to_column("Variable") %>%
  rename(
    Coeficiente = Estimate,
    Error_Est   = `Std. Error`,
    Valor_Z     = `z value`,
    Valor_P     = `Pr(>|z|)`
  ) %>%
  mutate(
    Coeficiente   = round(Coeficiente, 4),
    Error_Est     = round(Error_Est, 4),
    Valor_Z       = round(Valor_Z, 4),
    Valor_P       = round(Valor_P, 6),
    Significancia = case_when(
      Valor_P < 0.001 ~ "***",
      Valor_P < 0.01  ~ "**",
      Valor_P < 0.05  ~ "*",
      Valor_P < 0.1   ~ ".",
      TRUE            ~ ""
    )
  )

coeficientes %>%
  kable(caption = "Coeficientes del modelo de Regresión Logística",
        align   = c("l","c","c","c","c","c")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(which(coeficientes$Valor_P < 0.05),
           background = "#D5E8D4") %>%
  column_spec(1, bold = TRUE)
Coeficientes del modelo de Regresión Logística
Variable Coeficiente Error_Est Valor_Z Valor_P Significancia
(Intercept) 6.7436 1.1796 5.7168 0.000000 ***
arrendamiento 6.7148 1.9509 3.4420 0.000578 ***
telecomunicaciones 0.0972 0.2981 0.3260 0.744398
gastos_viaje 1.4044 0.7533 1.8644 0.062267 .
seguros 4.0845 1.7280 2.3638 0.018091
sistemas 15.1660 3.4837 4.3534 0.000013 ***
# Probabilidades de altos ingresos para el conjunto de prueba
p_hat <- predict(fit_logit, newdata = test, type = "response")

# Clasificamos con umbral de 0.5
pred_clase <- factor(ifelse(p_hat >= 0.5, "Alto", "Bajo"),
                     levels = c("Bajo", "Alto"))

# Matriz de confusion en tabla estética
cm <- confusionMatrix(pred_clase, test$nivel_ingreso, positive = "Alto")

# Tabla de la matriz de confusion
as.data.frame(cm$table) %>%
  rename(Prediccion = Prediction,
         Referencia = Reference,
         Frecuencia = Freq) %>%
  kable(caption = "Matriz de confusión - Umbral 0.5") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")
Matriz de confusión - Umbral 0.5
Prediccion Referencia Frecuencia
Bajo Bajo 31
Alto Bajo 5
Bajo Alto 4
Alto Alto 32
# Tabla de métricas
data.frame(
  Métrica = c("Exactitud", "Sensibilidad", "Especificidad",
              "Val. Pred. Positivo", "Val. Pred. Negativo", "Kappa"),
  Valor   = c(round(cm$overall["Accuracy"], 4),
              round(cm$byClass["Sensitivity"], 4),
              round(cm$byClass["Specificity"], 4),
              round(cm$byClass["Pos Pred Value"], 4),
              round(cm$byClass["Neg Pred Value"], 4),
              round(cm$overall["Kappa"], 4))
) %>%
  kable(caption = "Métricas del modelo Logit - Umbral 0.5") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE)
Métricas del modelo Logit - Umbral 0.5
Métrica Valor
Accuracy Exactitud 0.8750
Sensitivity Sensibilidad 0.8889
Specificity Especificidad 0.8611
Pos Pred Value Val. Pred. Positivo 0.8649
Neg Pred Value Val. Pred. Negativo 0.8857
Kappa Kappa 0.7500

Los coeficientes del modelo muestran qué variables tienen mayor influencia para predecir si un restaurante tiene altos o bajos ingresos. Los sistemas de información tienen el coeficiente más alto con 15.17 y son altamente significativos, lo que indica que un mayor gasto en tecnología de gestión está fuertemente asociado con altos ingresos. El arrendamiento también es muy significativo con coeficiente de 6.71, confirmando que locales mejor ubicados o más grandes generan más ingresos. Los seguros aportan un coeficiente de 4.08 y son significativos, reflejando que restaurantes más formalizados tienden a tener mayores ingresos. Los gastos de viaje tienen un coeficiente de 1.40 con un valor p de 0.062, que aunque no es significativo al 5% sí lo es al 10%, sugiriendo una relación marginal con los ingresos. Las telecomunicaciones no resultaron estadísticamente significativas con un valor p de 0.74, lo que indica que por sí sola esta variable no determina el nivel de ingresos cuando las demás están presentes.

La reducción en la devianza residual de 307.76 a 147.68 confirma que el modelo en conjunto tiene un buen ajuste sobre los datos de entrenamiento.

Matriz de confusión con umbral 0.5

Con el umbral estándar de 0.5 el modelo logra una exactitud del 87.5%, clasificando correctamente 31 restaurantes de bajos ingresos y 32 de altos ingresos. La sensibilidad del 88.89% indica que el modelo identifica correctamente casi 9 de cada 10 restaurantes de altos ingresos. La especificidad del 86.11% muestra que también reconoce bien los de bajos ingresos. El índice Kappa de 0.75 refleja un acuerdo muy bueno entre las predicciones y los valores reales.

# Calculamos el umbral optimo usando el indice de Youden
roc_o <- roc(response  = test$nivel_ingreso,
             predictor = p_hat,
             levels    = c("Bajo", "Alto"))

thr    <- coords(roc_o, x = "best", best.method = "youden", ret = "threshold")
umbral <- as.numeric(thr[1, 1])

print(paste("Umbral óptimo:", round(umbral, 4)))
## [1] "Umbral óptimo: 0.3846"
# Recalculamos las predicciones con el umbral optimo
pred_clase <- factor(ifelse(p_hat >= umbral, "Alto", "Bajo"),
                     levels = c("Bajo", "Alto"))

# Matriz de confusion con umbral ajustado
cm_opt <- confusionMatrix(pred_clase, test$nivel_ingreso, positive = "Alto")

# Tabla de la matriz de confusion
as.data.frame(cm_opt$table) %>%
  rename(Prediccion = Prediction,
         Referencia = Reference,
         Frecuencia = Freq) %>%
  kable(caption = paste("Matriz de confusión - Umbral óptimo:", round(umbral, 4))) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")
Matriz de confusión - Umbral óptimo: 0.3846
Prediccion Referencia Frecuencia
Bajo Bajo 30
Alto Bajo 6
Bajo Alto 3
Alto Alto 33
# Tabla de métricas con umbral optimo
data.frame(
  Métrica = c("Exactitud", "Sensibilidad", "Especificidad",
              "Val. Pred. Positivo", "Val. Pred. Negativo", "Kappa"),
  Valor   = c(round(cm_opt$overall["Accuracy"], 4),
              round(cm_opt$byClass["Sensitivity"], 4),
              round(cm_opt$byClass["Specificity"], 4),
              round(cm_opt$byClass["Pos Pred Value"], 4),
              round(cm_opt$byClass["Neg Pred Value"], 4),
              round(cm_opt$overall["Kappa"], 4))
) %>%
  kable(caption = "Métricas del modelo Logit - Umbral óptimo") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE)
Métricas del modelo Logit - Umbral óptimo
Métrica Valor
Accuracy Exactitud 0.8750
Sensitivity Sensibilidad 0.9167
Specificity Especificidad 0.8333
Pos Pred Value Val. Pred. Positivo 0.8462
Neg Pred Value Val. Pred. Negativo 0.9091
Kappa Kappa 0.7500

Matriz de confusión con umbral óptimo

Al ajustar el umbral con el índice de Youden la exactitud se mantiene en 87.5% pero la sensibilidad mejora a 91.67%, lo que significa que el modelo ahora identifica correctamente más restaurantes de altos ingresos. Esta mejora viene a costa de una leve reducción en la especificidad que baja a 83.33%. Dependiendo del objetivo del análisis este umbral ajustado puede ser preferible cuando es más importante no dejar pasar restaurantes de altos ingresos que evitar falsos positivos

# Curva ROC del modelo Logit
auc_val = auc(roc_o)

plot(roc_o,
     main = sprintf("ROC Logit | AUC=%.3f | Umbral=%.3f", auc_val, umbral))

La curva ROC del modelo Logit muestra un AUC de 0.936, superior al del modelo KNN que fue de 0.861. Esto indica que el modelo Logit tiene una capacidad de discriminación excelente entre restaurantes de altos y bajos ingresos. Esto significa que si tomamos al azar un restaurante de altos ingresos y uno de bajos ingresos, el modelo tiene un 93.6% de probabilidad de asignarle una probabilidad más alta de altos ingresos al que realmente la tiene. La curva se aleja notoriamente de la diagonal confirmando que el modelo aprende patrones reales y no clasifica al azar.

4.3 Comparación de modelos

En esta sección se comparan los resultados de ambos modelos para determinar cuál clasifica mejor los restaurantes según su nivel de ingresos.

comparacion <- data.frame(
  Metrica = c("Exactitud (Accuracy)",
              "Sensibilidad",
              "Especificidad",
              "Valor Predictivo Positivo",
              "Valor Predictivo Negativo",
              "Kappa",
              "AUC"),
  KNN     = c("86.11%", "83.33%", "88.89%",
              "88.24%", "84.21%", "0.722", "0.861"),
  Logit   = c("87.50%", "91.67%", "83.33%",
              "84.62%", "90.91%", "0.750", "0.936")
)

comparacion %>%
  kable(caption = "Comparación de métricas entre modelos KNN y Logit",
        align   = c("l", "c", "c")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width        = FALSE,
                position          = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(7, bold = TRUE, background = "#D5E8D4") %>%
  column_spec(1, bold = TRUE)
Comparación de métricas entre modelos KNN y Logit
Metrica KNN Logit
Exactitud (Accuracy) 86.11% 87.50%
Sensibilidad 83.33% 91.67%
Especificidad 88.89% 83.33%
Valor Predictivo Positivo 88.24% 84.62%
Valor Predictivo Negativo 84.21% 90.91%
Kappa 0.722 0.750
AUC 0.861 0.936

Los resultados muestran que ambos modelos tienen un desempeño muy similar en exactitud, con el Logit superando al KNN por apenas 1.39 puntos porcentuales. Sin embargo hay diferencias más marcadas en otras métricas.

En sensibilidad el Logit es claramente superior con 91.67% frente al 83.33% del KNN, lo que significa que el Logit identifica correctamente más restaurantes de altos ingresos. Esta es una métrica clave cuando el objetivo es no dejar pasar casos positivos.

En especificidad el KNN supera al Logit con 88.89% frente a 83.33%, lo que indica que el KNN es mejor reconociendo los restaurantes de bajos ingresos. Sin embargo esta diferencia es menos relevante para el objetivo del análisis.

La diferencia más notable está en el AUC: el Logit alcanza 0.936 frente a 0.861 del KNN, lo que indica que el Logit tiene una capacidad de discriminación significativamente mejor entre las dos categorías independientemente del umbral de clasificación.

El índice Kappa también favorece al Logit con 0.750 frente a 0.722 del KNN, confirmando un mejor acuerdo general entre predicciones y valores reales.

# Dataframe con las métricas de ambos modelos
metricas <- data.frame(
  Metrica = rep(c("Exactitud", "Sensibilidad", "Especificidad",
                  "Val. Pred. Positivo", "Val. Pred. Negativo",
                  "Kappa", "AUC"), 2),
  Valor   = c(0.8611, 0.8333, 0.8889, 0.8824, 0.8421, 0.7222, 0.861,
              0.8750, 0.9167, 0.8333, 0.8462, 0.9091, 0.7500, 0.936),
  Modelo  = c(rep("KNN", 7), rep("Logit", 7))
)

# Grafico de barras 
metricas %>%
  plot_ly(x = ~Metrica, y = ~Valor,
          color = ~Modelo,
          colors = c("blue", "#E67E22"),
          type = "bar",
          text = ~paste(round(Valor * 100, 1), "%"),
          textposition = "outside") %>%
  layout(title   = "Comparación de métricas entre modelos KNN y Logit",
         xaxis   = list(title = "Métrica"),
         yaxis   = list(title = "Valor", range = c(0, 1.1)),
         barmode = "group",
         legend  = list(title = list(text = "Modelo")))

5 Conclusiones y Recomendaciones

5.1 Conclusiones

Este trabajo buscaba responder una pregunta concreta: ¿es posible clasificar un restaurante colombiano como de altos o bajos ingresos a partir de su estructura de inversión operativa? Los resultados obtenidos permiten responder que sí, y con un nivel de precisión bastante satisfactorio.

Ambos modelos lograron superar ampliamente el azar. El modelo KNN alcanzó una exactitud del 86.11% y un AUC de 0.861, mientras que el modelo Logit obtuvo una exactitud del 87.50% y un AUC de 0.936. Estos resultados confirman que los gastos operativos de un restaurante sí contienen información útil para predecir su nivel de ingresos, lo que valida el enfoque del trabajo.

De las cinco variables analizadas, los sistemas de información resultaron ser el factor más determinante con el coeficiente más alto en el modelo Logit. Esto sugiere que la digitalización del negocio, entendida como la inversión en software de gestión, facturación electrónica y plataformas digitales, es el elemento que más diferencia a los restaurantes de altos ingresos de los demás. El arrendamiento y los seguros también resultaron altamente significativos, confirmando que la ubicación, el tamaño del local y el nivel de formalización del negocio son factores clave. Las telecomunicaciones en cambio no resultaron significativas individualmente, lo que sugiere que por sí sola esta inversión no determina el nivel de ingresos cuando las demás variables están presentes.

El modelo Logit es el recomendado para este problema por tres razones. Primero tiene el AUC más alto lo que indica mayor capacidad de discriminación general. Segundo tiene mayor sensibilidad, identificando correctamente más restaurantes de altos ingresos. Tercero y más importante, permite interpretar el efecto individual de cada variable, algo que el KNN no puede hacer y que es valioso para entender qué factores operativos impulsan los ingresos de un restaurante.

¿El modelo respondió al objetivo de la investigación?

Sí, el modelo logró responder al objetivo planteado. Se demostró que es posible clasificar restaurantes colombianos como de altos o bajos ingresos a partir de sus gastos operativos con una exactitud superior al 86% en ambos modelos. Sin embargo es importante reconocer una limitación del estudio: las variables utilizadas son gastos que crecen junto con los ingresos porque reflejan el tamaño del negocio, más que causas directas de los ingresos. Un restaurante grande gasta más en arrendamiento y sistemas porque genera más ingresos, no necesariamente porque esos gastos causaron sus ingresos. Esta distinción entre correlación y causalidad debe tenerse en cuenta al interpretar los resultados.

5.2 Recomendaciones

A partir de los hallazgos del modelo se pueden proponer las siguientes recomendaciones para restaurantes que buscan aumentar sus ingresos:

Invertir en sistemas de información es la acción con mayor potencial de impacto según el modelo. Un restaurante que implementa software de gestión de pedidos, facturación electrónica y plataformas de domicilios no solo mejora su eficiencia operativa sino que también amplía sus canales de venta, lo que se traduce directamente en mayores ingresos.

La ubicación sigue siendo un factor crítico. El alto coeficiente del arrendamiento no necesariamente significa que pagar más arriendo genera más ingresos, sino que los restaurantes mejor ubicados, con mayor visibilidad y flujo de clientes, tienden a generar más. Para nuevos negocios la elección del local es una de las decisiones más importantes.

La formalización del negocio, medida a través de los seguros, también está asociada con mayores ingresos. Un restaurante que asegura sus activos y operación refleja un nivel de madurez empresarial que generalmente va acompañado de mejores prácticas de gestión y mayor capacidad de generar ingresos sostenibles.

Para futuras investigaciones se recomienda incluir variables causales más directas como el número de mesas, las reseñas en plataformas digitales, el tiempo de operación o el tipo de cocina, que permitirían construir modelos más explicativos y menos descriptivos del fenómeno.

6 Bibliografía

  • Departamento Administrativo Nacional de Estadística (DANE). (2024). Encuesta Anual de Servicios (EAS) 2024. Recuperado de https://www.dane.gov.co

  • James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning with Applications in R (2nd ed.). Springer.

  • Robin, X., Turck, N., Hainard, A., Tiberti, N., Lisacek, F., Sanchez, J. C., & Müller, M. (2011). pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics, 12(1), 77. https://doi.org/10.1186/1471-2105-12-77

  • Wickham, H., & Grolemund, G. (2017). R for Data Science. O’Reilly Media. https://r4ds.had.co.nz