1. Introducción

Este documento explica, paso a paso, un script de R que clasifica empresas como “Rentables” o “No Rentables” a partir de su información financiera (ingresos operacionales, activos, pasivos, patrimonio) y de su macrosector.

El script hace tres cosas:

  1. Prepara los datos del archivo Empresas.csv y define la variable que se quiere predecir: una empresa es Rentable si su ganancia es mayor que cero y No Rentable en caso contrario.
  2. Describe los datos con tablas y gráficos que comparan a las empresas rentables con las no rentables.
  3. Construye y compara dos modelos de clasificación, una regresión logística (Logit) y un K-vecinos más cercanos (KNN), usando una parte de los datos para entrenar y otra para evaluar.

Bajo cada bloque de código se explica qué hace. Los resultados (tablas, gráficos y cifras) se generan al ejecutar el documento con el archivo de datos.

2. Metodología

2.1 Carga y preparación de los datos

Librerías. El script usa dplyr (manipular datos), ggplot2 y plotly (gráficos, estáticos e interactivos), class (función knn), caret (partición de datos, escalado y matriz de confusión), stats (funciones estadísticas base), pROC (curvas ROC y AUC) y gridExtra (combinar varios gráficos en un panel).

library(dplyr)
library(ggplot2)
library(plotly)
library(class)
library(caret)
library(stats)
library(pROC)
library(gridExtra)

Lectura del archivo. Se busca Empresas.csv en la misma carpeta de este documento. Si no existe, se detiene con un mensaje. Luego read.csv lo carga como tabla; check.names = FALSE conserva los nombres originales de las columnas (con espacios, paréntesis y tildes).

nombre_csv <- "Empresas.csv"

if (!file.exists(nombre_csv)) {
  stop("No se encontró 'Empresas.csv'. Colócalo en la misma carpeta que este archivo .Rmd.")
}

datos_b <- read.csv(nombre_csv, encoding = "UTF-8", check.names = FALSE)

Creación de variables limpias. Con mutate() se crean columnas nuevas con nombres cortos. En cada columna financiera, gsub("[^0-9.-]", "", ...) elimina cualquier carácter que no sea un dígito, un punto o un signo menos (por ejemplo símbolos de moneda, espacios o comas), y as.numeric() convierte el resultado en número. Macrosector se guarda como texto.

datos_clasificacion <- datos_b %>%
  mutate(
    Ingresos    = as.numeric(gsub("[^0-9.-]", "", `INGRESOS OPERACIONALES`)),
    Activos     = as.numeric(gsub("[^0-9.-]", "", `TOTAL ACTIVOS`)),
    Pasivos     = as.numeric(gsub("[^0-9.-]", "", `TOTAL PASIVOS`)),
    Ganancia    = as.numeric(gsub("[^0-9.-]", "", `GANANCIA (PÉRDIDA)`)),
    Patrimonio  = as.numeric(gsub("[^0-9.-]", "", `TOTAL PATRIMONIO`)),
    Macrosector = as.character(MACROSECTOR)
  )

Variable dependiente binaria (Y). ifelse(Ganancia > 0, "Rentable", "No_Rentable") asigna “Rentable” a las empresas con ganancia positiva y “No_Rentable” al resto (ganancia cero o pérdida). Se convierte en factor con el orden No_Rentable, Rentable; este orden importa porque el modelo logístico estima la probabilidad del segundo nivel, es decir, de ser Rentable. Después select() conserva solo las columnas de interés (se descarta Ganancia, que ya cumplió su función) y na.omit() elimina las filas con algún dato faltante.

datos_clasificacion <- datos_clasificacion %>%
  mutate(
    Rentable = factor(ifelse(Ganancia > 0, "Rentable", "No_Rentable"),
                      levels = c("No_Rentable", "Rentable")),
    Macrosector = as.factor(trimws(Macrosector))
  ) %>%
  select(Rentable, Ingresos, Activos, Pasivos, Patrimonio, Macrosector) %>%
  na.omit()

2.2 Descripción de Variables

Variable en el script Columna original Rol en el análisis
Rentable Se construye a partir de GANANCIA (PÉRDIDA) Variable objetivo (Y): “Rentable” si la ganancia es mayor que 0; “No_Rentable” en caso contrario
Ingresos INGRESOS OPERACIONALES Variable cuantitativa; predictora en Logit y KNN
Activos TOTAL ACTIVOS Variable cuantitativa; predictora en Logit y KNN
Pasivos TOTAL PASIVOS Variable cuantitativa; predictora en Logit y KNN
Patrimonio TOTAL PATRIMONIO Variable cuantitativa; se conserva solo para la exploración y se excluye de los modelos para evitar multicolinealidad e inconvenientes de convergencia (según el propio script)
Macrosector MACROSECTOR Variable categórica (factor); predictora solo en el Logit

2.3 Modelos Utilizados

Regresión logística (Logit). Se ajusta con glm(..., family = binomial(link = "logit")). Usa como predictores Ingresos, Activos, Pasivos (transformados con logaritmo y estandarizados) y Macrosector. El modelo entrega, para cada empresa, una probabilidad de ser Rentable. Esa probabilidad se convierte en clase con un umbral de 0.25: si la probabilidad es mayor que 0.25, la empresa se clasifica como Rentable.

K-vecinos más cercanos (KNN). Se usa la función knn() del paquete class con k = 5. Utiliza solo Ingresos, Activos y Pasivos (transformados y estandarizados). Para clasificar una empresa, busca las 5 empresas de entrenamiento más parecidas (más cercanas en esas tres variables) y le asigna la clase que predomina entre ellas.

Evaluación. Ambos modelos se evalúan con la misma muestra de prueba mediante:

  • la matriz de confusión (confusionMatrix), que cruza la clase predicha con la real y calcula Accuracy, Sensibilidad y Especificidad, tomando “Rentable” como clase positiva;
  • la curva ROC y el AUC (paquete pROC), que resumen qué tan bien separa cada modelo a las empresas rentables de las no rentables.

2.4 Partición y transformación de los datos

Partición 70% / 30%. createDataPartition() separa los datos en entrenamiento (70%) y prueba (30%) manteniendo en ambos una proporción similar de rentables y no rentables. set.seed(123) fija la semilla para que la partición sea reproducible. Después, el factor Macrosector de la muestra de prueba se ajusta a los mismos niveles que el de entrenamiento y se eliminan las filas de prueba cuyo sector no exista en el entrenamiento (el modelo no podría predecirlas). Finalmente se imprime la proporción de clases en el entrenamiento.

set.seed(123)
index <- createDataPartition(datos_clasificacion$Rentable, p = 0.70, list = FALSE)
train_data <- datos_clasificacion[index, ]
test_data  <- datos_clasificacion[-index, ]

test_data$Macrosector <- factor(test_data$Macrosector,
                                levels = levels(train_data$Macrosector))
test_data <- test_data[!is.na(test_data$Macrosector), ]

print(prop.table(table(train_data$Rentable)))
## 
## No_Rentable    Rentable 
##   0.7463215   0.2536785

Transformación logarítmica. Como indica el comentario del script, el logaritmo se usa para suavizar valores atípicos masivos. pmax(0, ...) reemplaza los valores negativos por 0 y log1p() calcula log(1 + x), que sí se puede aplicar cuando el valor es 0. La transformación se aplica por igual a entrenamiento y prueba, y solo a Ingresos, Activos y Pasivos.

vars_modelos <- c("Ingresos", "Activos", "Pasivos")

train_data_log <- train_data
test_data_log  <- test_data

train_data_log[, vars_modelos] <- log1p(pmax(0, train_data[, vars_modelos]))
test_data_log[, vars_modelos]  <- log1p(pmax(0, test_data[, vars_modelos]))

3. Resultados Descriptivos

3.1 Estadísticos Descriptivos (Variables Cuantitativas)

Se calcularon medidas de tendencia central, dispersión, forma y posición para las variables financieras, junto con el coeficiente de variación (CV) y el coeficiente de asimetría.

vars_cuant <- c("Ingresos", "Activos", "Pasivos", "Patrimonio")

# Coeficiente de asimetría (momento de orden 3 estandarizado)
asimetria <- function(x) {
  x <- x[!is.na(x)]
  m3 <- mean((x - mean(x))^3)
  v2 <- mean((x - mean(x))^2)
  if (v2 == 0) return(0)
  m3 / (v2^(1.5))
}

resumen_cuant <- function(x) {
  c(
    n         = length(x),
    Media     = mean(x),
    Desv_Est  = sd(x),
    Minimo    = min(x),
    Q1        = unname(quantile(x, 0.25)),
    Mediana   = median(x),
    Q3        = unname(quantile(x, 0.75)),
    Maximo    = max(x),
    CV_pct    = (sd(x) / mean(x)) * 100,
    Asimetria = asimetria(x)
  )
}

tabla_desc <- t(sapply(datos_clasificacion[, vars_cuant], resumen_cuant))
knitr::kable(round(tabla_desc, 4), format.args = list(big.mark = ","),
             caption = "Estadísticos descriptivos de las variables cuantitativas")
Estadísticos descriptivos de las variables cuantitativas
n Media Desv_Est Minimo Q1 Mediana Q3 Maximo CV_pct Asimetria
Ingresos 50,000 0.1712 1.3652 0.01 0.03 0.05 0.10 144.82 797.4618 67.9668
Activos 50,000 0.2257 2.4167 0.00 0.02 0.04 0.09 216.85 1,070.9487 62.5523
Pasivos 50,000 0.1161 1.4161 0.00 0.01 0.02 0.05 130.54 1,219.8654 68.7106
Patrimonio 50,000 0.1093 1.1213 -3.69 0.01 0.01 0.04 91.03 1,025.7020 46.5930

[Interpretar: comparar media vs. mediana, CV y asimetría. Una media muy superior a la mediana y una asimetría positiva alta indican cola derecha: pocas empresas muy grandes.]

Valores no positivos. Se contaron los valores negativos y los ceros de cada variable, porque determinan cómo se puede aplicar la transformación logarítmica.

tabla_ceros <- data.frame(
  Variable  = vars_cuant,
  Negativos = sapply(vars_cuant, function(v) sum(datos_clasificacion[[v]] < 0)),
  Ceros     = sapply(vars_cuant, function(v) sum(datos_clasificacion[[v]] == 0)),
  Positivos = sapply(vars_cuant, function(v) sum(datos_clasificacion[[v]] > 0)),
  row.names = NULL
)
tabla_ceros$Pct_no_positivos <- round(
  100 * (tabla_ceros$Negativos + tabla_ceros$Ceros) / nrow(datos_clasificacion), 2
)
knitr::kable(tabla_ceros, format.args = list(big.mark = ","),
             caption = "Valores negativos, ceros y positivos por variable")
Valores negativos, ceros y positivos por variable
Variable Negativos Ceros Positivos Pct_no_positivos
Ingresos 0 0 50,000 0.00
Activos 0 1,429 48,571 2.86
Pasivos 0 7,402 42,598 14.80
Patrimonio 780 9,765 39,455 21.09

Estadísticos por grupo de rentabilidad. Se comparan las empresas Rentables y No Rentables.

resumen_grupo <- function(x) {
  c(n = length(x), Media = mean(x), Mediana = median(x),
    Desv_Est = sd(x), Min = min(x), Max = max(x))
}

desc_grupo <- do.call(rbind, lapply(vars_cuant, function(v) {
  m <- t(sapply(split(datos_clasificacion[[v]], datos_clasificacion$Rentable),
                resumen_grupo))
  data.frame(Variable = v, Grupo = rownames(m), m, row.names = NULL)
}))
knitr::kable(desc_grupo, digits = 2, format.args = list(big.mark = ","),
             caption = "Estadísticos descriptivos por rentabilidad")
Estadísticos descriptivos por rentabilidad
Variable Grupo n Media Mediana Desv_Est Min Max
Ingresos No_Rentable 37,317 0.08 0.04 0.36 0.01 25.86
Ingresos Rentable 12,683 0.43 0.12 2.62 0.01 144.82
Activos No_Rentable 37,317 0.08 0.03 0.72 0.00 102.52
Activos Rentable 12,683 0.65 0.13 4.61 0.00 216.85
Pasivos No_Rentable 37,317 0.05 0.01 0.60 0.00 100.20
Pasivos Rentable 12,683 0.30 0.05 2.61 0.00 130.54
Patrimonio No_Rentable 37,317 0.03 0.01 0.26 -3.69 27.58
Patrimonio Rentable 12,683 0.35 0.06 2.16 -1.49 91.03

[Interpretar: las empresas Rentables tienen medianas mayores en ingresos, activos, pasivos y patrimonio?]

Correlaciones.

vars_cuant <- c("Ingresos", "Activos", "Pasivos", "Patrimonio")
matriz_cor <- cor(datos_clasificacion[, vars_cuant], use = "complete.obs")
print(round(matriz_cor, 3))
##            Ingresos Activos Pasivos Patrimonio
## Ingresos      1.000   0.884   0.876      0.799
## Activos       0.884   1.000   0.963      0.940
## Pasivos       0.876   0.963   1.000      0.812
## Patrimonio    0.799   0.940   0.812      1.000
df_cor <- as.data.frame(as.table(matriz_cor))
ggplot(df_cor, aes(Var1, Var2, fill = Freq)) +
  geom_tile() +
  geom_text(aes(label = round(Freq, 2)), color = "white", fontface = "bold") +
  scale_fill_gradient2(low = "#4575B4", mid = "white", high = "#D73027",
                       midpoint = 0.5) +
  theme_minimal() +
  labs(title = "Mapa de Calor de Correlaciones Lineales",
       x = "", y = "", fill = "Correlación")

3.2 Distribución del Diagnóstico (Rentable / No Rentable)

table() cuenta cuántas empresas hay en cada clase de Rentable y prop.table() * 100 convierte esos conteos en porcentajes. Con ambos resultados se arma la tabla df_y (Estado, Frecuencia, Porcentaje). Esta tabla muestra qué tan equilibradas están las dos clases de la variable objetivo.

tabla_y <- table(datos_clasificacion$Rentable)
prop_y  <- prop.table(tabla_y) * 100
df_y    <- data.frame(Estado     = names(tabla_y),
                      Frecuencia = as.numeric(tabla_y),
                      Porcentaje = as.numeric(prop_y))
print(df_y)
##        Estado Frecuencia Porcentaje
## 1 No_Rentable      37317     74.634
## 2    Rentable      12683     25.366

El gráfico de barras muestra la frecuencia de cada clase. ggplotly() lo convierte en un gráfico interactivo (al pasar el cursor se ven los valores).

g1_y <- ggplot(df_y, aes(x = Estado, y = Frecuencia, fill = Estado)) +
  geom_bar(stat = "identity", width = 0.5) +
  theme_minimal() +
  labs(title = "Distribución de Empresas según Rentabilidad (Y)",
       y = "Número de Empresas", x = "Estado")
ggplotly(g1_y)

3.3 Variable Categórica: Macrosector

Se analiza la distribución de las empresas por macrosector y la proporción de empresas rentables dentro de cada uno.

tabla_cont  <- table(datos_clasificacion$Macrosector, datos_clasificacion$Rentable)
prop_sector <- round(prop.table(tabla_cont, margin = 1) * 100, 2)

tabla_sector <- data.frame(
  Macrosector  = rownames(tabla_cont),
  No_Rentable  = as.numeric(tabla_cont[, "No_Rentable"]),
  Rentable     = as.numeric(tabla_cont[, "Rentable"]),
  Pct_Rentable = as.numeric(prop_sector[, "Rentable"]),
  stringsAsFactors = FALSE
) %>% arrange(desc(Pct_Rentable))

knitr::kable(tabla_sector, format.args = list(big.mark = ","),
             caption = "Empresas por macrosector y porcentaje de rentables")
Empresas por macrosector y porcentaje de rentables
Macrosector No_Rentable Rentable Pct_Rentable
MINERO 534 413 43.61
SERVICIOS 10,710 4,979 31.74
MANUFACTURA 7,092 2,941 29.31
AGROPECUARIO 1,576 566 26.42
CONSTRUCCIÓN 2,933 921 23.90
COMERCIO 14,472 2,863 16.52
g_sector <- ggplot(tabla_sector,
                   aes(x = reorder(Macrosector, Pct_Rentable), y = Pct_Rentable)) +
  geom_col(fill = "#377EB8") +
  geom_text(aes(label = paste0(Pct_Rentable, "%")), hjust = -0.1, size = 3.5) +
  coord_flip() +
  ylim(0, max(tabla_sector$Pct_Rentable) * 1.15) +
  theme_minimal() +
  labs(title = "Porcentaje de empresas rentables por macrosector",
       x = NULL, y = "% de empresas rentables")
ggplotly(g_sector)

[Interpretar: qué macrosectores tienen mayor y menor proporción de empresas rentables, y si el sector parece discriminar la rentabilidad.]

3.4 Histogramas de Densidad por Variable

Para Ingresos, Activos y Pasivos se construye el mismo tipo de gráfico, que combina un histograma (geom_histogram, 30 intervalos, en escala de densidad) con una curva de densidad (geom_density), con colores distintos para empresas Rentables y No Rentables. Sirven para ver la forma de la distribución y cuánto se solapan los dos grupos.

Detalles del código:

  • subset(..., variable > 0) deja solo valores positivos, porque el logaritmo no se puede calcular con ceros ni negativos.
  • scale_x_log10() pone el eje X en escala logarítmica, ya que los valores son muy dispersos.
  • scale_fill_manual y scale_color_manual fijan los colores: rojo para No_Rentable y azul para Rentable.

Los tres gráficos se guardan en objetos (h_ing, h_act, h_pas) y se muestran juntos en un panel.

h_ing <- ggplot(subset(datos_clasificacion, Ingresos > 0),
                aes(x = Ingresos, fill = Rentable, color = Rentable)) +
  geom_histogram(aes(y = after_stat(density)), alpha = 0.4,
                 position = "identity", bins = 30) +
  geom_density(alpha = 0.2, linewidth = 1) +
  scale_x_log10() +
  theme_minimal() +
  scale_fill_manual(values = c("#E41A1C", "#377EB8")) +
  scale_color_manual(values = c("#E41A1C", "#377EB8")) +
  labs(title = "Densidad de Ingresos Operacionales",
       x = "Ingresos (Log10)", y = "Densidad")
h_act <- ggplot(subset(datos_clasificacion, Activos > 0),
                aes(x = Activos, fill = Rentable, color = Rentable)) +
  geom_histogram(aes(y = after_stat(density)), alpha = 0.4,
                 position = "identity", bins = 30) +
  geom_density(alpha = 0.2, linewidth = 1) +
  scale_x_log10() +
  theme_minimal() +
  scale_fill_manual(values = c("#E41A1C", "#377EB8")) +
  scale_color_manual(values = c("#E41A1C", "#377EB8")) +
  labs(title = "Densidad de Total Activos",
       x = "Activos (Log10)", y = "Densidad")
h_pas <- ggplot(subset(datos_clasificacion, Pasivos > 0),
                aes(x = Pasivos, fill = Rentable, color = Rentable)) +
  geom_histogram(aes(y = after_stat(density)), alpha = 0.4,
                 position = "identity", bins = 30) +
  geom_density(alpha = 0.2, linewidth = 1) +
  scale_x_log10() +
  theme_minimal() +
  scale_fill_manual(values = c("#E41A1C", "#377EB8")) +
  scale_color_manual(values = c("#E41A1C", "#377EB8")) +
  labs(title = "Densidad de Total Pasivos",
       x = "Pasivos (Log10)", y = "Densidad")

grid.arrange() coloca los tres gráficos en un panel de 2 columnas.

grid.arrange(h_ing, h_act, h_pas, ncol = 2)

3.5 Boxplot

Los diagramas de caja comparan, para cada variable, la distribución de las empresas Rentables y No Rentables (x = Rentable). Igual que antes, se filtran los valores positivos y el eje Y va en escala logarítmica (scale_y_log10()), por la gran dispersión de los valores. Se hacen para Ingresos, Activos y Pasivos, y también para Patrimonio, que el script conserva solo con fines exploratorios. Cada gráfico se hace interactivo con ggplotly().

g2_ing <- ggplot(subset(datos_clasificacion, Ingresos > 0),
                 aes(x = Rentable, y = Ingresos, fill = Rentable)) +
  geom_boxplot(alpha = 0.7) + scale_y_log10() + theme_minimal() +
  labs(title = "Ingresos Operacionales según Rentabilidad",
       y = "Ingresos (Escala Log10)")
ggplotly(g2_ing)
g2_act <- ggplot(subset(datos_clasificacion, Activos > 0),
                 aes(x = Rentable, y = Activos, fill = Rentable)) +
  geom_boxplot(alpha = 0.7) + scale_y_log10() + theme_minimal() +
  labs(title = "Total Activos según Rentabilidad",
       y = "Activos (Escala Log10)")
ggplotly(g2_act)
g2_pas <- ggplot(subset(datos_clasificacion, Pasivos > 0),
                 aes(x = Rentable, y = Pasivos, fill = Rentable)) +
  geom_boxplot(alpha = 0.7) + scale_y_log10() + theme_minimal() +
  labs(title = "Total Pasivos según Rentabilidad",
       y = "Pasivos (Escala Log10)")
ggplotly(g2_pas)
g2_pat <- ggplot(subset(datos_clasificacion, Patrimonio > 0),
                 aes(x = Rentable, y = Patrimonio, fill = Rentable)) +
  geom_boxplot(alpha = 0.7) + scale_y_log10() + theme_minimal() +
  labs(title = "Total Patrimonio según Rentabilidad",
       y = "Patrimonio (Escala Log10)")
ggplotly(g2_pat)

3.6 Análisis de Valores Atípicos

Se aplicó la regla del rango intercuartílico (IQR): se consideran atípicos los valores por debajo de Q1 − 1,5·IQR o por encima de Q3 + 1,5·IQR.

vars_modelos <- c("Ingresos", "Activos", "Pasivos")

contar_atipicos <- function(x) {
  q   <- quantile(x, c(0.25, 0.75))
  iqr <- q[[2]] - q[[1]]
  li  <- q[[1]] - 1.5 * iqr
  ls  <- q[[2]] + 1.5 * iqr
  c(
    n            = length(x),
    Limite_inf   = li,
    Limite_sup   = ls,
    Atipicos_inf = sum(x < li),
    Atipicos_sup = sum(x > ls),
    Pct_atipicos = 100 * mean(x < li | x > ls)
  )
}

tabla_atip_orig <- t(sapply(datos_clasificacion[, vars_cuant], contar_atipicos))
knitr::kable(round(tabla_atip_orig, 4), format.args = list(big.mark = ","),
             caption = "Valores atípicos en la escala original")
Valores atípicos en la escala original
n Limite_inf Limite_sup Atipicos_inf Atipicos_sup Pct_atipicos
Ingresos 50,000 -0.075 0.205 0 6,361 12.722
Activos 50,000 -0.085 0.195 0 6,930 13.860
Pasivos 50,000 -0.050 0.110 0 6,172 12.344
Patrimonio 50,000 -0.035 0.085 271 6,837 14.216
tabla_atip_log <- as.data.frame(t(sapply(vars_cuant, function(v) {
  x <- datos_clasificacion[[v]]
  contar_atipicos(log10(x[x > 0]))
})))
tabla_atip_log$Limite_inf_original <- 10^tabla_atip_log$Limite_inf
tabla_atip_log$Limite_sup_original <- 10^tabla_atip_log$Limite_sup
knitr::kable(round(tabla_atip_log, 4), format.args = list(big.mark = ","),
             caption = "Valores atípicos en escala log10 (solo valores positivos)")
Valores atípicos en escala log10 (solo valores positivos)
n Limite_inf Limite_sup Atipicos_inf Atipicos_sup Pct_atipicos Limite_inf_original Limite_sup_original
Ingresos 50,000 -2.3072 -0.2157 0 2,193 4.3860 0.0049 0.6086
Activos 48,571 -2.7474 0.0485 0 1,332 2.7424 0.0018 1.1180
Pasivos 42,598 -3.1672 -0.0546 0 846 1.9860 0.0007 0.8818
Patrimonio 39,455 -3.0485 -0.2526 0 1,257 3.1859 0.0009 0.5590

Atípicos según grupo de rentabilidad.

atipicos_por_grupo <- do.call(rbind, lapply(vars_cuant, function(v) {
  x   <- datos_clasificacion[[v]]
  g   <- datos_clasificacion$Rentable
  pos <- x > 0
  lx  <- log10(x[pos])
  gg  <- g[pos]

  q   <- quantile(lx, c(0.25, 0.75))
  iqr <- q[[2]] - q[[1]]
  es_atipico <- lx < (q[[1]] - 1.5 * iqr) | lx > (q[[2]] + 1.5 * iqr)

  data.frame(
    Variable = v,
    Grupo    = levels(gg),
    Empresas = as.numeric(table(gg)),
    Atipicos = as.numeric(tapply(es_atipico, gg, sum)),
    stringsAsFactors = FALSE
  )
}))
atipicos_por_grupo$Pct_atipicos <- round(
  100 * atipicos_por_grupo$Atipicos / atipicos_por_grupo$Empresas, 2
)
knitr::kable(atipicos_por_grupo, format.args = list(big.mark = ","),
             caption = "Atípicos por variable y grupo de rentabilidad")
Atípicos por variable y grupo de rentabilidad
Variable Grupo Empresas Atipicos Pct_atipicos
Ingresos No_Rentable 37,317 506 1.36
Ingresos Rentable 12,683 1,687 13.30
Activos No_Rentable 35,890 253 0.70
Activos Rentable 12,681 1,079 8.51
Pasivos No_Rentable 30,587 240 0.78
Pasivos Rentable 12,011 606 5.05
Patrimonio No_Rentable 26,974 172 0.64
Patrimonio Rentable 12,481 1,085 8.69
g_atip <- ggplot(atipicos_por_grupo,
                 aes(x = Variable, y = Pct_atipicos, fill = Grupo)) +
  geom_col(position = "dodge") +
  scale_fill_manual(values = c("#E41A1C", "#377EB8")) +
  theme_minimal() +
  labs(title = "Porcentaje de valores atípicos por variable y grupo",
       x = NULL, y = "% de empresas con valor atípico")
ggplotly(g_atip)

[Interpretar: qué variable tiene más atípicos, en qué grupo se concentran y por qué no se eliminan (son grandes empresas reales).]

Efecto de la transformación logarítmica. Se compara la asimetría y el porcentaje de atípicos antes y después de aplicar log1p(x) = log(1 + x).

efecto_log <- do.call(rbind, lapply(vars_modelos, function(v) {
  x  <- datos_clasificacion[[v]]
  xl <- log1p(pmax(0, x))
  data.frame(
    Variable              = v,
    Asimetria_original    = asimetria(x),
    Asimetria_log1p       = asimetria(xl),
    Asimetria_log10_pos   = asimetria(log10(x[x > 0])),
    Pct_atipicos_original = contar_atipicos(x)[["Pct_atipicos"]],
    Pct_atipicos_log1p    = contar_atipicos(xl)[["Pct_atipicos"]]
  )
}))
knitr::kable(efecto_log, digits = 4,
             caption = "Efecto de la transformación log1p")
Efecto de la transformación log1p
Variable Asimetria_original Asimetria_log1p Asimetria_log10_pos Pct_atipicos_original Pct_atipicos_log1p
Ingresos 67.9668 6.6923 1.2904 12.722 12.176
Activos 62.5523 6.7022 1.1047 13.860 13.316
Pasivos 68.7106 8.6119 1.3489 12.344 12.344

[Interpretar: la asimetría y el % de atípicos bajan tras el logaritmo, lo que justifica usarlo en Logit y KNN.]

4. Resultados del Modelo

En esta sección se ajusta cada modelo con la muestra de entrenamiento (train_data) y se evalúa con la muestra de prueba (test_data), que el modelo no ha visto. Ambos usan las variables ya transformadas con logaritmo en la sección 2.4.

4.1 Modelo KNN

4.1.1 Entrenamiento

Estandarización. preProcess(..., method = c("center", "scale")) calcula, con los datos de entrenamiento, la media y la desviación estándar de cada variable; predict() aplica esa misma transformación a entrenamiento y prueba. Así las tres variables quedan en la misma escala (media 0, desviación 1), algo necesario en KNN porque el método se basa en distancias.

Control de valores faltantes y empates. El script reemplaza por 0 cualquier NA residual en las matrices de KNN y agrega a los datos de entrenamiento un ruido muy pequeño (jitter, con semilla fija) para romper empates exactos de distancia causados por ceros repetidos.

Clases. train_cl guarda la clase real de cada empresa de entrenamiento y real_knn la clase real de las empresas de prueba, ambas como factor con el orden No_Rentable, Rentable.

pp_knn <- preProcess(train_data_log[, vars_modelos], method = c("center", "scale"))

train_knn <- predict(pp_knn, train_data_log[, vars_modelos])
test_knn  <- predict(pp_knn, test_data_log[, vars_modelos])

train_knn[is.na(train_knn)] <- 0
test_knn[is.na(test_knn)]   <- 0

set.seed(123)
train_knn_jitter <- jitter(as.matrix(train_knn), factor = 0.001)

train_cl <- factor(as.character(train_data_log$Rentable),
                   levels = c("No_Rentable", "Rentable"))
real_knn <- factor(as.character(test_data_log$Rentable),
                   levels = c("No_Rentable", "Rentable"))

Ajuste con k = 5. En KNN no hay una fase de entrenamiento separada: knn() recibe los datos de entrenamiento y los de prueba y clasifica cada empresa de prueba según sus 5 vecinos más cercanos. Con prob = TRUE además devuelve la proporción de votos de la clase ganadora. Como esa proporción corresponde a la clase predicha (sea cual sea), el script la convierte en la probabilidad de ser “Rentable”: si la clase predicha es Rentable, usa esa proporción; si no, usa 1 - proporción. Esa probabilidad se necesita después para la curva ROC.

pred_knn <- knn(
  train   = train_knn_jitter,
  test    = test_knn,
  cl      = train_cl,
  k       = 5,
  prob    = TRUE,
  use.all = FALSE
)

prob_gan <- attr(pred_knn, "prob")
prob_knn <- ifelse(pred_knn == "Rentable", prob_gan, 1 - prob_gan)

4.1.2 Evaluación del Modelo

confusionMatrix() compara la clase predicha con la clase real de la muestra de prueba, tomando “Rentable” como clase positiva (positive = "Rentable"). Entrega la tabla de aciertos y errores y estas métricas:

  • Accuracy: proporción de empresas bien clasificadas en total.
  • Sensitivity (sensibilidad): de las empresas realmente Rentables, qué proporción identifica el modelo como Rentables.
  • Specificity (especificidad): de las empresas realmente No Rentables, qué proporción identifica el modelo como No Rentables.
cm_knn <- confusionMatrix(pred_knn, real_knn, positive = "Rentable")
print(cm_knn)
## Confusion Matrix and Statistics
## 
##              Reference
## Prediction    No_Rentable Rentable
##   No_Rentable       10143     1602
##   Rentable           1052     2202
##                                           
##                Accuracy : 0.8231          
##                  95% CI : (0.8169, 0.8291)
##     No Information Rate : 0.7464          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.5092          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.5789          
##             Specificity : 0.9060          
##          Pos Pred Value : 0.6767          
##          Neg Pred Value : 0.8636          
##              Prevalence : 0.2536          
##          Detection Rate : 0.1468          
##    Detection Prevalence : 0.2169          
##       Balanced Accuracy : 0.7424          
##                                           
##        'Positive' Class : Rentable        
## 

4.2 Modelo LOGIT

Estandarización. Igual que en KNN, preProcess() calcula media y desviación estándar con los datos de entrenamiento (ya transformados con logaritmo) y predict() las aplica a entrenamiento y prueba.

pp_logit <- preProcess(train_data_log[, vars_modelos], method = c("center", "scale"))

train_scaled <- predict(pp_logit, train_data_log)
test_scaled  <- predict(pp_logit, test_data_log)

Ajuste del modelo. glm() con family = binomial(link = "logit") ajusta la regresión logística: la variable respuesta es Rentable y los predictores son Ingresos, Activos, Pasivos y Macrosector. En summary() se ven los coeficientes estimados, su error estándar, el estadístico z y el valor p. Los coeficientes están en la escala del logaritmo de las odds de ser Rentable. Como Macrosector es categórica, R la descompone en un coeficiente por cada sector, comparado con un sector de referencia.

modelo_logit <- glm(
  Rentable ~ Ingresos + Activos + Pasivos + Macrosector,
  data   = train_scaled,
  family = binomial(link = "logit")
)

summary(modelo_logit)
## 
## Call:
## glm(formula = Rentable ~ Ingresos + Activos + Pasivos + Macrosector, 
##     family = binomial(link = "logit"), data = train_scaled)
## 
## Coefficients:
##                         Estimate Std. Error z value Pr(>|z|)    
## (Intercept)             -0.90357    0.06702 -13.482  < 2e-16 ***
## Ingresos                 0.88791    0.03364  26.393  < 2e-16 ***
## Activos                  3.92542    0.09466  41.471  < 2e-16 ***
## Pasivos                 -3.44667    0.08481 -40.642  < 2e-16 ***
## MacrosectorCOMERCIO     -0.50208    0.07319  -6.860 6.87e-12 ***
## MacrosectorCONSTRUCCIÓN -0.14219    0.08508  -1.671   0.0947 .  
## MacrosectorMANUFACTURA  -0.16514    0.07398  -2.232   0.0256 *  
## MacrosectorMINERO        0.02665    0.12575   0.212   0.8321    
## MacrosectorSERVICIOS     0.08110    0.07150   1.134   0.2567    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 35478  on 29814  degrees of freedom
## Residual deviance: 28050  on 29806  degrees of freedom
##   (5186 observations deleted due to missingness)
## AIC: 28068
## 
## Number of Fisher Scoring iterations: 6

4.2.1 Evaluación

Predicción en la muestra de prueba. predict(..., type = "response") devuelve, para cada empresa de prueba, la probabilidad de ser Rentable. Luego se aplica el umbral de 0.25: probabilidad mayor que 0.25 se clasifica como “Rentable”, en otro caso “No_Rentable”. real_test guarda la clase real de esas empresas.

Matriz de confusión. table() cruza predicción y realidad, y confusionMatrix() calcula las mismas métricas descritas en 4.1.2.

prob_logit <- predict(modelo_logit, newdata = test_scaled, type = "response")

umbral <- 0.25
pred_logit <- factor(
  ifelse(prob_logit > umbral, "Rentable", "No_Rentable"),
  levels = c("No_Rentable", "Rentable")
)

real_test <- factor(as.character(test_scaled$Rentable),
                    levels = c("No_Rentable", "Rentable"))

print(table(Prediccion = pred_logit, Real = real_test))
##              Real
## Prediccion    No_Rentable Rentable
##   No_Rentable        7716     1085
##   Rentable           1449     2513
cm_logit <- confusionMatrix(pred_logit, real_test, positive = "Rentable")
print(cm_logit)
## Confusion Matrix and Statistics
## 
##              Reference
## Prediction    No_Rentable Rentable
##   No_Rentable        7716     1085
##   Rentable           1449     2513
##                                           
##                Accuracy : 0.8015          
##                  95% CI : (0.7944, 0.8083)
##     No Information Rate : 0.7181          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.5242          
##                                           
##  Mcnemar's Test P-Value : 5.549e-13       
##                                           
##             Sensitivity : 0.6984          
##             Specificity : 0.8419          
##          Pos Pred Value : 0.6343          
##          Neg Pred Value : 0.8767          
##              Prevalence : 0.2819          
##          Detection Rate : 0.1969          
##    Detection Prevalence : 0.3104          
##       Balanced Accuracy : 0.7702          
##                                           
##        'Positive' Class : Rentable        
## 

4.3 Comparación de los Modelos

Curvas ROC y AUC. roc() calcula la curva ROC de cada modelo con la probabilidad de ser Rentable que produjo sobre la muestra de prueba, y auc() calcula el área bajo la curva (AUC). Ambas curvas se dibujan en el mismo gráfico (azul: Logit; rojo: KNN) junto con una diagonal punteada de referencia, y la leyenda incluye el AUC de cada modelo.

roc_logit <- roc(real_test, prob_logit, levels = c("No_Rentable", "Rentable"), direction = "<")
roc_knn   <- roc(real_knn,  prob_knn,   levels = c("No_Rentable", "Rentable"), direction = "<")

auc_logit <- as.numeric(auc(roc_logit))
auc_knn   <- as.numeric(auc(roc_knn))

plot(roc_logit, col = "blue", lwd = 3, legacy.axes = TRUE,
     main = "Curvas ROC Comparativas: Logit vs. KNN")
plot(roc_knn, col = "red", lwd = 3, add = TRUE)
abline(a = 0, b = 1, lty = 2, col = "gray")
legend("bottomright",
       legend = c(paste("Logit (AUC =", round(auc_logit, 3), ")"),
                  paste("KNN (AUC =",   round(auc_knn, 3),   ")")),
       col = c("blue", "red"), lwd = 3)

Tabla comparativa final. Reúne en una sola tabla el Accuracy, la Sensibilidad y la Especificidad (tomados de las matrices de confusión) y el AUC de cada modelo, redondeados a 4 decimales.

tabla_comparativa <- data.frame(
  Modelo      = c("Logit", "KNN"),
  Accuracy    = c(cm_logit$overall["Accuracy"],    cm_knn$overall["Accuracy"]),
  Sensitivity = c(cm_logit$byClass["Sensitivity"], cm_knn$byClass["Sensitivity"]),
  Specificity = c(cm_logit$byClass["Specificity"], cm_knn$byClass["Specificity"]),
  AUC         = c(auc_logit, auc_knn)
)

tabla_comparativa[, -1] <- round(tabla_comparativa[, -1], 4)
rownames(tabla_comparativa) <- NULL
knitr::kable(tabla_comparativa)
Modelo Accuracy Sensitivity Specificity AUC
Logit 0.8015 0.6984 0.8419 0.8478
KNN 0.8231 0.5789 0.9060 0.8364

4.3.1 ¿Cuál es el Mejor Modelo?

Para responder a partir de los resultados de la tabla anterior, el siguiente código identifica, por cada métrica, qué modelo obtuvo el valor más alto (si hay empate, aparecen ambos).

metricas <- c("Accuracy", "Sensitivity", "Specificity", "AUC")

mejor_por_metrica <- data.frame(
  Metrica = metricas,
  Mejor_modelo = sapply(metricas, function(m) {
    paste(tabla_comparativa$Modelo[tabla_comparativa[[m]] == max(tabla_comparativa[[m]])],
          collapse = " y ")
  }),
  Valor = sapply(metricas, function(m) max(tabla_comparativa[[m]]))
)
rownames(mejor_por_metrica) <- NULL
knitr::kable(mejor_por_metrica)
Metrica Mejor_modelo Valor
Accuracy KNN 0.8231
Sensitivity Logit 0.6984
Specificity KNN 0.9060
AUC Logit 0.8478
mejor_auc <- mejor_por_metrica$Mejor_modelo[mejor_por_metrica$Metrica == "AUC"]
auc_mejor <- mejor_por_metrica$Valor[mejor_por_metrica$Metrica == "AUC"]

El script compara los modelos con las curvas ROC y el AUC, que resumen la capacidad de cada uno para separar empresas rentables de no rentables. Con ese criterio, el modelo con mejor desempeño global es Logit, con un AUC de 0.8478.

Hay que tener presente que Accuracy, Sensibilidad y Especificidad dependen de la regla con la que cada modelo asigna la clase: el Logit usa el umbral de 0.25 sobre su probabilidad, mientras que el KNN usa la clase mayoritaria entre sus 5 vecinos. La tabla por métrica muestra en qué aspecto destaca cada modelo.

5. Conclusiones

  • Se trabajó con 50000 empresas con información completa en las variables usadas, de las cuales el 25.37% es Rentable (ganancia mayor que 0).
  • Los datos se dividieron en 35001 empresas para entrenar los modelos y 14999 para evaluarlos.
  • Se compararon dos modelos con la misma muestra de prueba: una regresión logística (con Ingresos, Activos, Pasivos y Macrosector, umbral de 0.25) y un KNN con k = 5 (con Ingresos, Activos y Pasivos). En ambos, las variables numéricas se transformaron con logaritmo y se estandarizaron, y Patrimonio se dejó fuera.
  • Según el AUC, el mejor modelo fue Logit (AUC = 0.8478). El detalle de Accuracy, Sensibilidad y Especificidad de cada modelo está en la tabla comparativa de la sección 4.3.