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:
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.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.
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()
| 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 |
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:
confusionMatrix), que cruza la clase predicha con la real
y calcula Accuracy, Sensibilidad y Especificidad, tomando “Rentable”
como clase positiva;pROC),
que resumen qué tan bien separa cada modelo a las empresas rentables de
las no rentables.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]))
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")
| 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")
| 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")
| 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")
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)
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")
| 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.]
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)
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)
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")
| 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)")
| 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")
| 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")
| 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.]
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.
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)
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:
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
##
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
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
##
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 |
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.
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.