Este documento no pretende convertir el proyecto en una lista de comandos. La lógica será siempre:
\[ \text{pregunta} \rightarrow \text{decisión estadística} \rightarrow \text{evidencia} \rightarrow \text{interpretación} \rightarrow \text{implicación} \]
La base fue construida para que existan comportamientos reales, valores extremos válidos, datos faltantes y algunas inconsistencias. Por tanto, un valor raro no se elimina automáticamente. Primero se investiga.
Clave docente. La solución usa resultados calculados directamente sobre la base. Los textos con código en línea se actualizarán al tejer el documento, evitando escribir a mano cifras que puedan cambiar si se genera otra versión de la base.
Con más de diez millones de registros conviene trabajar con
data.table. Si se dispone del archivo .rds,
esa será la ruta recomendada.
ruta_rds <- "Business_Data_Lab.rds"
ruta_csv <- "Business_Data_Lab.csv"
if (file.exists(ruta_rds)) {
ventas <- readRDS(ruta_rds)
setDT(ventas)
} else if (file.exists(ruta_csv)) {
ventas <- fread(ruta_csv)
} else {
stop("Ubique Business_Data_Lab.rds o Business_Data_Lab.csv en la carpeta de trabajo.")
}
dim(ventas)
## [1] 1438729 27
La primera comprobación no es calcular la media: es preguntar qué tenemos.
str(ventas)
## Classes 'data.table' and 'data.frame': 1438729 obs. of 27 variables:
## $ fila : int 1 2 3 4 5 6 7 8 9 10 ...
## $ id_transaccion : chr "TX000664995" "TX000717907" "TX000215251" "TX000264893" ...
## $ fecha : Date, format: "2026-05-25" "2025-04-17" ...
## $ anio : int 2026 2025 2025 2023 2024 2023 2024 2025 2024 2025 ...
## $ mes : int 5 4 1 8 1 11 7 11 6 3 ...
## $ trimestre : chr "T2" "T2" "T1" "T3" ...
## $ dia_semana : chr "lunes" "jueves" "domingo" "martes" ...
## $ fin_semana : chr "No" "No" "Sí" "No" ...
## $ id_cliente : chr "CL0224227" "CL0474181" "CL0814355" "CL0346415" ...
## $ tipo_cliente : chr "Persona" "Persona" "Persona" "Persona" ...
## $ ciudad : chr "Medellín" "Cali" "Bogotá" "Cali" ...
## $ region : chr "Andina" "Occidente" "Centro" "Occidente" ...
## $ canal : chr "Página web" "Marketplace" "Página web" "App" ...
## $ categoria : chr "Hogar" "Hogar" "Accesorios" "Deportes" ...
## $ producto : chr "Mesa auxiliar" "Lámpara" "Morral" "Colchoneta" ...
## $ unidades : int 2 2 1 4 8 3 4 7 2 5 ...
## $ precio_unitario : num 228800 84100 77900 76400 249700 ...
## $ descuento : num 0 0.3 0.1 0.15 0 0.4 0.1 0.05 0 0.3 ...
## $ precio_final : num 228800 58870 70110 64940 249700 ...
## $ ingreso : num 503400 117700 61700 259800 1757900 ...
## $ costo : num 319200 102090 34600 166000 1144700 ...
## $ utilidad : num 184200 15610 27100 93800 613200 ...
## $ margen_pct : num 36.6 13.3 43.9 36.1 34.9 ...
## $ tiempo_entrega_dias: int 2 10 5 2 5 5 0 5 13 5 ...
## $ satisfaccion : num NA 3.9 NA 4.9 4.6 4.3 NA 5 3.9 4.9 ...
## $ devolucion : chr "No" "Sí" "No" "No" ...
## $ estado_operacion : chr "Completada" "Completada" "Completada" "Completada" ...
## - attr(*, ".internal.selfref")=<externalptr>
head(ventas, 5)
tail(ventas, 5)
La base contiene 1.438.729 registros y 27 variables. El periodo observado va desde 01/01/2023 hasta 30/06/2026.
Cada fila representa un registro de una operación o
transacción comercial. El identificador
id_transaccion intenta identificar esa operación, aunque
posteriormente comprobaremos si el identificador es siempre único.
La unidad de observación es una operación comercial registrada.
No es el cliente. Un mismo cliente puede aparecer varias veces porque puede realizar varias compras.
Si el objetivo es describir el comportamiento del negocio durante el periodo contenido en la base, la población de interés puede formularse como:
Todas las operaciones registradas por la empresa entre la fecha inicial y final contenidas en el sistema analizado.
Esto es distinto de afirmar que la base representa todas las ventas posibles de la empresa en el futuro.
diccionario <- data.table(
variable = c(
"fila", "id_transaccion", "fecha", "anio", "mes", "trimestre",
"dia_semana", "fin_semana", "id_cliente", "tipo_cliente", "ciudad",
"region", "canal", "categoria", "producto", "unidades", "precio_unitario",
"descuento", "precio_final", "ingreso", "costo", "utilidad", "margen_pct",
"tiempo_entrega_dias", "satisfaccion", "devolucion", "estado_operacion"
),
naturaleza = c(
"Identificador", "Cualitativa", "Temporal", "Cuantitativa", "Cuantitativa", "Cualitativa",
"Cualitativa", "Cualitativa", "Cualitativa", "Cualitativa", "Cualitativa",
"Cualitativa", "Cualitativa", "Cualitativa", "Cualitativa", "Cuantitativa", "Cuantitativa",
"Cuantitativa", "Cuantitativa", "Cuantitativa", "Cuantitativa", "Cuantitativa", "Cuantitativa",
"Cuantitativa", "Cuantitativa ordinal", "Cualitativa", "Cualitativa"
),
subtipo = c(
"Discreto", "Nominal", "Fecha", "Discreto", "Discreto", "Nominal",
"Nominal", "Nominal", "Nominal", "Nominal", "Nominal",
"Nominal", "Nominal", "Nominal", "Nominal", "Discreto", "Continua",
"Continua", "Continua", "Continua", "Continua", "Continua", "Continua",
"Discreta", "Discreta/ordinal", "Nominal", "Nominal"
),
escala = c(
"Nominal", "Nominal", "Intervalo temporal", "Intervalo", "Ordinal/temporal", "Ordinal",
"Nominal", "Nominal", "Nominal", "Nominal", "Nominal",
"Nominal", "Nominal", "Nominal", "Nominal", "Razón", "Razón",
"Razón*", "Razón", "Razón", "Razón", "Razón**", "Razón***",
"Razón", "Ordinal", "Nominal", "Nominal"
),
unidad = c(
"registro", "código", "día", "año", "mes", "trimestre",
"categoría", "categoría", "código", "categoría", "categoría",
"categoría", "categoría", "categoría", "categoría", "unidades", "COP/unidad",
"proporción", "COP/unidad", "COP", "COP", "COP", "%",
"días", "puntos (1–5)", "categoría", "categoría"
)
)
kable(diccionario, align = "lllll")
| variable | naturaleza | subtipo | escala | unidad |
|---|---|---|---|---|
| fila | Identificador | Discreto | Nominal | registro |
| id_transaccion | Cualitativa | Nominal | Nominal | código |
| fecha | Temporal | Fecha | Intervalo temporal | día |
| anio | Cuantitativa | Discreto | Intervalo | año |
| mes | Cuantitativa | Discreto | Ordinal/temporal | mes |
| trimestre | Cualitativa | Nominal | Ordinal | trimestre |
| dia_semana | Cualitativa | Nominal | Nominal | categoría |
| fin_semana | Cualitativa | Nominal | Nominal | categoría |
| id_cliente | Cualitativa | Nominal | Nominal | código |
| tipo_cliente | Cualitativa | Nominal | Nominal | categoría |
| ciudad | Cualitativa | Nominal | Nominal | categoría |
| region | Cualitativa | Nominal | Nominal | categoría |
| canal | Cualitativa | Nominal | Nominal | categoría |
| categoria | Cualitativa | Nominal | Nominal | categoría |
| producto | Cualitativa | Nominal | Nominal | categoría |
| unidades | Cuantitativa | Discreto | Razón | unidades |
| precio_unitario | Cuantitativa | Continua | Razón | COP/unidad |
| descuento | Cuantitativa | Continua | Razón* | proporción |
| precio_final | Cuantitativa | Continua | Razón | COP/unidad |
| ingreso | Cuantitativa | Continua | Razón | COP |
| costo | Cuantitativa | Continua | Razón | COP |
| utilidad | Cuantitativa | Continua | Razón** | COP |
| margen_pct | Cuantitativa | Continua | Razón*** | % |
| tiempo_entrega_dias | Cuantitativa | Discreta | Razón | días |
| satisfaccion | Cuantitativa ordinal | Discreta/ordinal | Ordinal | puntos (1–5) |
| devolucion | Cualitativa | Nominal | Nominal | categoría |
| estado_operacion | Cualitativa | Nominal | Nominal | categoría |
* El descuento se expresa como proporción. Un 0,20
representa 20 %.
** La utilidad puede ser negativa. Conceptualmente sigue
siendo una magnitud monetaria con cero significativo, aunque esto obliga
a tener cuidado con cocientes.
*** El margen porcentual es una razón derivada.
Compararlo es útil, pero no debe confundirse con una variable monetaria
aditiva.
Punto conceptual importante. Que anio
esté guardado como número no significa que tenga sentido promediar años.
El tipo de almacenamiento de R y la naturaleza estadística de una
variable no son la misma cosa.
faltantes <- data.table(
variable = names(ventas),
n_faltantes = vapply(ventas, function(x) sum(is.na(x)), numeric(1))
)
faltantes[, porcentaje := n_faltantes / nrow(ventas)]
faltantes <- faltantes[n_faltantes > 0][order(-n_faltantes)]
faltantes[, .(
variable,
n_faltantes = fmt_n(n_faltantes),
porcentaje = fmt_p(porcentaje, 2)
)] |> kable()
| variable | n_faltantes | porcentaje |
|---|---|---|
| satisfaccion | 297.453 | 20,67% |
| tiempo_entrega_dias | 8.632 | 0,60% |
La pregunta correcta no es únicamente “¿cuántos NA hay?”, sino “¿faltan de la misma manera en todos los grupos?”.
na_satisfaccion_canal <- ventas[, .(
operaciones = .N,
sin_satisfaccion = sum(is.na(satisfaccion)),
porcentaje_sin_satisfaccion = mean(is.na(satisfaccion))
), by = canal][order(-porcentaje_sin_satisfaccion)]
na_satisfaccion_canal[, .(
canal,
operaciones = fmt_n(operaciones),
sin_satisfaccion = fmt_n(sin_satisfaccion),
porcentaje = fmt_p(porcentaje_sin_satisfaccion, 1)
)] |> kable()
| canal | operaciones | sin_satisfaccion | porcentaje |
|---|---|---|---|
| Tienda física | 489.517 | 254.907 | 52,1% |
| Página web | 387.791 | 17.552 | 4,5% |
| Marketplace | 172.274 | 7.690 | 4,5% |
| App | 288.641 | 12.853 | 4,5% |
| Venta telefónica | 100.506 | 4.451 | 4,4% |
Interpretación. Si el porcentaje de satisfacción faltante cambia fuertemente por canal, no es razonable asumir de entrada que los faltantes son completamente aleatorios. En consecuencia, comparar promedios de satisfacción sin mencionar la cobertura de respuesta puede producir una lectura incompleta.
ids_repetidos <- ventas[, .N, by = id_transaccion][N > 1]
data.table(
ids_repetidos = nrow(ids_repetidos),
registros_involucrados = sum(ids_repetidos$N),
exceso_sobre_un_id = sum(ids_repetidos$N - 1L)
) |> kable()
| ids_repetidos | registros_involucrados | exceso_sobre_un_id |
|---|---|---|
| 1248 | 2496 | 1248 |
Encontrar un ID repetido no autoriza a ejecutar
unique(ventas) sin mirar. Dos filas con el mismo
identificador podrían diferir en otros campos. Primero se
inspeccionan.
ventas[id_transaccion %chin% head(ids_repetidos$id_transaccion, 5)][order(id_transaccion, fila)]
Separaremos tres conceptos:
reglas <- ventas[, .(
descuento_fuera_rango = sum(descuento < 0 | descuento > 0.50, na.rm = TRUE),
entrega_negativa = sum(tiempo_entrega_dias < 0, na.rm = TRUE),
satisfaccion_fuera_escala = sum(satisfaccion < 1 | satisfaccion > 5, na.rm = TRUE),
precio_muy_bajo = sum(precio_unitario < 5000, na.rm = TRUE),
entrega_mayor_30 = sum(tiempo_entrega_dias > 30, na.rm = TRUE)
)]
t(reglas)
## [,1]
## descuento_fuera_rango 310
## entrega_negativa 164
## satisfaccion_fuera_escala 215
## precio_muy_bajo 650
## entrega_mayor_30 303
Los dos últimos criterios son banderas de revisión, no pruebas de error. Un precio bajo o una entrega de 45 días pueden ser posibles en el mundo real. En cambio, una satisfacción de 10 en una escala 1–5 contradice la definición de la variable.
Hay variables que deberían guardar relaciones lógicas. Por ejemplo,
ciudad y región; también precio_final debería estar
relacionado con precio y descuento.
ventas[, .N, by = .(ciudad, region)][order(ciudad, -N)] |> head(30) |> kable()
| ciudad | region | N |
|---|---|---|
| Barranquila | Centro | 37 |
| Barranquila | Andina | 24 |
| Barranquila | Occidente | 19 |
| Barranquila | Caribe | 16 |
| Barranquila | Oriente | 7 |
| Barranquilla | Caribe | 129256 |
| Bogota | Andina | 31 |
| Bogota | Centro | 21 |
| Bogota | Caribe | 17 |
| Bogota | Occidente | 16 |
| Bogota | Oriente | 9 |
| Bogotá | Centro | 432324 |
| Bucaramanga | Oriente | 100824 |
| Cali | Occidente | 187132 |
| Cali | Centro | 26 |
| Cali | Andina | 22 |
| Cali | Caribe | 15 |
| Cali | Oriente | 11 |
| Cali | Occidente | 9 |
| Cartagena | Caribe | 101038 |
| Ibagué | Occidente | 71698 |
| MEDELLIN | Centro | 38 |
| MEDELLIN | Andina | 22 |
| MEDELLIN | Caribe | 20 |
| MEDELLIN | Occidente | 13 |
| MEDELLIN | Oriente | 8 |
| Manizales | Andina | 57622 |
| Medellin | Centro | 37 |
| Medellin | Andina | 28 |
| Medellin | Occidente | 21 |
ventas[, precio_final_teorico := precio_unitario * (1 - descuento)]
ventas[, diferencia_precio := precio_final - precio_final_teorico]
resumen_consistencia_precio <- ventas[, .(
n = .N,
diferencia_mediana = median(abs(diferencia_precio), na.rm = TRUE),
p99_diferencia = quantile(abs(diferencia_precio), .99, na.rm = TRUE),
casos_diferencia_mayor_1000 = sum(abs(diferencia_precio) > 1000, na.rm = TRUE)
)]
resumen_consistencia_precio |> kable()
| n | diferencia_mediana | p99_diferencia | casos_diferencia_mayor_1000 |
|---|---|---|---|
| 1438729 | 0 | 0 | 1380 |
ventas[, c("precio_final_teorico", "diferencia_precio") := NULL]
Lectura empresarial. Cuando una variable derivada no coincide con sus componentes, no basta con corregirla silenciosamente: habría que conocer cuál campo es la fuente oficial. En este laboratorio lo documentamos como una inconsistencia potencial.
Para una primera radiografía del negocio seleccionamos:
ingreso: tamaño monetario de la operación;utilidad y margen_pct: resultado
económico, no solo volumen;unidades: tamaño físico de la operación;descuento: intensidad promocional;canal, categoria, ciudad,
tipo_cliente: segmentación;tiempo_entrega_dias: desempeño operativo;satisfaccion y devolucion: experiencia
posterior a la operación;estado_operacion: define si la transacción puede
interpretarse como venta completada.No todas las preguntas necesitan exactamente las mismas filas. Para estudiar el desempeño comercial principal usaremos operaciones completadas y conservaremos aparte las banderas de calidad.
ventas[, flag_imposible :=
(!is.na(descuento) & (descuento < 0 | descuento > 0.50)) |
(!is.na(tiempo_entrega_dias) & tiempo_entrega_dias < 0) |
(!is.na(satisfaccion) & (satisfaccion < 1 | satisfaccion > 5))
]
ventas[, flag_sospechoso :=
(!is.na(precio_unitario) & precio_unitario < 5000) |
(!is.na(tiempo_entrega_dias) & tiempo_entrega_dias > 30)
]
ventas[, flag_analitica := estado_operacion == "Completada" & flag_imposible == FALSE]
n_completadas <- ventas[estado_operacion == "Completada", .N]
n_analitica <- ventas[flag_analitica == TRUE, .N]
data.table(
conjunto = c("Base original", "Completadas", "Completadas sin inconsistencias imposibles"),
registros = c(nrow(ventas), n_completadas, n_analitica),
porcentaje_base = c(1, n_completadas/nrow(ventas), n_analitica/nrow(ventas))
)[, .(
conjunto,
registros = fmt_n(registros),
porcentaje_base = fmt_p(porcentaje_base, 2)
)] |> kable()
| conjunto | registros | porcentaje_base |
|---|---|---|
| Base original | 1.438.729 | 100,00% |
| Completadas | 1.388.183 | 96,49% |
| Completadas sin inconsistencias imposibles | 1.387.525 | 96,44% |
No eliminamos los casos flag_sospechoso de forma
automática. Precisamente queremos conservar la posibilidad de que una
operación rara sea real. Sí excluimos de la base principal los valores
que contradicen las reglas explícitas de medición.
El ingreso suele presentar cola derecha: muchas operaciones relativamente pequeñas y pocas de gran tamaño. Con más de diez millones de filas, para visualizar no necesitamos dibujar diez millones de puntos; una muestra grande es suficiente para el gráfico, mientras que los estadísticos se calculan sobre toda la base.
res_ingreso <- ventas[flag_analitica == TRUE, .(
n = .N,
media = mean(ingreso, na.rm = TRUE),
mediana = median(ingreso, na.rm = TRUE),
desviacion = sd(ingreso, na.rm = TRUE),
minimo = min(ingreso, na.rm = TRUE),
q1 = quantile(ingreso, .25, na.rm = TRUE),
q3 = quantile(ingreso, .75, na.rm = TRUE),
p90 = quantile(ingreso, .90, na.rm = TRUE),
p95 = quantile(ingreso, .95, na.rm = TRUE),
p99 = quantile(ingreso, .99, na.rm = TRUE),
maximo = max(ingreso, na.rm = TRUE)
)]
res_ingreso[, ric := q3 - q1]
res_ingreso[, cv := desviacion / media]
# Moda del valor observado. Como el ingreso está redondeado, puede haber
# muchos empates; por eso su interpretación es secundaria frente a la mediana.
tabla_moda_ingreso <- ventas[flag_analitica == TRUE, .N, by = ingreso][order(-N)]
moda_ingreso <- tabla_moda_ingreso$ingreso[1]
frecuencia_moda <- tabla_moda_ingreso$N[1]
res_ingreso[, .(
n = fmt_n(n),
media = fmt_money(media),
mediana = fmt_money(mediana),
q1 = fmt_money(q1),
q3 = fmt_money(q3),
RIC = fmt_money(ric),
desviacion = fmt_money(desviacion),
CV = fmt_p(cv, 1),
P95 = fmt_money(p95),
P99 = fmt_money(p99),
maximo = fmt_money(maximo)
)] |> kable()
| n | media | mediana | q1 | q3 | RIC | desviacion | CV | P95 | P99 | maximo |
|---|---|---|---|---|---|---|---|---|---|---|
| 1.387.525 | $1.312.190 | $567.000 | $299.200 | $1.113.000 | $813.800 | $6.602.285 | 503,2% | $3.126.700 | $8.725.780 | $749.038.400 |
El valor modal observado es $410.400 y aparece 272 veces. En una variable monetaria casi continua, la moda depende mucho del nivel de redondeo; por eso suele ser menos informativa que la mediana o los cuartiles para describir una operación típica.
set.seed(2608)
idx_analitica <- which(ventas$flag_analitica)
idx_plot <- sample(idx_analitica, min(length(idx_analitica), 250000L))
muestra_plot <- ventas[idx_plot]
p99_ingreso <- res_ingreso$p99
ggplot(muestra_plot[ingreso <= p99_ingreso], aes(ingreso)) +
geom_histogram(bins = 60, fill = azul, color = "white", linewidth = 0.2) +
scale_x_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
labs(
title = "Distribución del ingreso por operación",
subtitle = "Visualización hasta el percentil 99; el 1 % superior se excluye solo del gráfico",
x = "Ingreso por operación (COP)", y = "Frecuencia"
) +
theme_minimal(base_size = 12)
No se eliminaron los valores superiores al P99 de los cálculos: únicamente se acotó el gráfico para poder ver la masa principal.
razon_media_mediana <- res_ingreso$media / res_ingreso$mediana
data.table(
medida = c("Media", "Mediana", "Media / mediana"),
valor = c(fmt_money(res_ingreso$media), fmt_money(res_ingreso$mediana), fmt_num(razon_media_mediana, 2))
) |> kable()
| medida | valor |
|---|---|
| Media | $1.312.190 |
| Mediana | $567.000 |
| Media / mediana | 2,31 |
Si la media resulta claramente mayor que la mediana, existe evidencia descriptiva compatible con una cola derecha: unas pocas operaciones grandes empujan la media hacia arriba.
Conclusión recomendada. Para comunicar la “operación típica”, la mediana es más defendible que la media cuando la distribución está fuertemente sesgada. La media sigue siendo relevante para planeación financiera agregada; no se reemplaza, se interpreta con otra finalidad.
li <- res_ingreso$q1 - 1.5 * res_ingreso$ric
ls <- res_ingreso$q3 + 1.5 * res_ingreso$ric
res_outliers <- ventas[flag_analitica == TRUE, .(
inferiores = sum(ingreso < li, na.rm = TRUE),
superiores = sum(ingreso > ls, na.rm = TRUE),
proporcion_superiores = mean(ingreso > ls, na.rm = TRUE)
)]
data.table(
concepto = c("Q1", "Q3", "RIC", "Límite inferior", "Límite superior", "% sobre límite superior"),
valor = c(fmt_money(res_ingreso$q1), fmt_money(res_ingreso$q3), fmt_money(res_ingreso$ric),
fmt_money(li), fmt_money(ls), fmt_p(res_outliers$proporcion_superiores, 2))
) |> kable()
| concepto | valor |
|---|---|
| Q1 | $299.200 |
| Q3 | $1.113.000 |
| RIC | \(813.800 | |Límite inferior |\)-921.500 |
| Límite superior | $2.333.700 |
| % sobre límite superior | 8,33% |
La regla de Tukey identifica observaciones alejadas del cuerpo central. No demuestra que sean errores. En un negocio con clientes empresariales y mayoristas, una compra grande puede ser precisamente una transacción valiosa.
ventas[flag_analitica == TRUE][order(-ingreso), .(
id_transaccion, fecha, tipo_cliente, ciudad, canal, categoria,
unidades, ingreso, utilidad, margen_pct
)][1:15] |> kable()
| id_transaccion | fecha | tipo_cliente | ciudad | canal | categoria | unidades | ingreso | utilidad | margen_pct |
|---|---|---|---|---|---|---|---|---|---|
| TX000847695 | 2024-03-23 | Empresa | Cali | Tienda física | Tecnología | 292 | 749038400 | 188826900 | 25.21 |
| TX001221185 | 2025-05-25 | Empresa | Villavicencio | App | Tecnología | 316 | 651339200 | 112589500 | 17.29 |
| TX000729995 | 2025-05-06 | Empresa | Bogotá | Tienda física | Tecnología | 348 | 625528300 | 167959200 | 26.85 |
| TX001215981 | 2025-05-19 | Empresa | Medellín | App | Tecnología | 351 | 586905300 | 96565200 | 16.45 |
| TX000384968 | 2023-08-21 | Empresa | Medellín | Marketplace | Tecnología | 409 | 571013100 | 122544400 | 21.46 |
| TX001183559 | 2024-03-16 | Empresa | Barranquilla | Tienda física | Tecnología | 393 | 538889500 | 148486900 | 27.55 |
| TX001256018 | 2024-07-15 | Empresa | Bogotá | Tienda física | Oficina | 440 | 497904000 | 144764300 | 29.07 |
| TX000581214 | 2025-06-15 | Empresa | Bogotá | Página web | Tecnología | 434 | 497719900 | 88257900 | 17.73 |
| TX000507195 | 2025-10-14 | Empresa | Bogotá | App | Tecnología | 417 | 496480200 | 154977700 | 31.22 |
| TX000202173 | 2026-02-21 | Empresa | Barranquilla | App | Tecnología | 300 | 494304000 | 126529800 | 25.60 |
| TX000614510 | 2025-12-26 | Empresa | Barranquilla | Tienda física | Tecnología | 404 | 487771400 | 69170700 | 14.18 |
| TX001070891 | 2026-04-01 | Empresa | Bogotá | Página web | Tecnología | 404 | 486383700 | 125589500 | 25.82 |
| TX000051253 | 2025-06-26 | Empresa | Bogotá | Página web | Tecnología | 356 | 483464000 | 101910800 | 21.08 |
| TX000651814 | 2025-05-19 | Empresa | Ibagué | App | Tecnología | 402 | 478983000 | 152980000 | 31.94 |
| TX001373749 | 2024-02-13 | Empresa | Cartagena | Marketplace | Tecnología | 446 | 471903700 | 142453500 | 30.19 |
Calculamos manualmente los momentos estandarizados para no añadir otro paquete.
momento_forma <- function(x) {
x <- x[is.finite(x)]
m <- mean(x)
s <- sd(x)
c(
asimetria = mean((x - m)^3) / s^3,
curtosis_exceso = mean((x - m)^4) / s^4 - 3
)
}
# Para evitar crear vectores auxiliares gigantes, una muestra amplia basta
# para describir la forma; media/mediana/cuartiles anteriores sí usan la base completa.
set.seed(2608)
idx_forma <- sample(idx_analitica, min(length(idx_analitica), 1000000L))
forma_muestra <- ventas[idx_forma, ingreso]
forma <- momento_forma(forma_muestra)
data.table(
medida = c("Asimetría", "Exceso de curtosis"),
valor = fmt_num(unname(forma), 2)
) |> kable()
| medida | valor |
|---|---|
| Asimetría | 29,17 |
| Exceso de curtosis | 1375,47 |
Una asimetría positiva confirma cola derecha. Una curtosis elevada indica mayor peso de valores alejados del centro que en una distribución normal. Ninguna de estas medidas, por sí sola, decide que los datos estén “mal”.
Buscaremos dos segmentos con ingresos medios relativamente parecidos, pero distinta dispersión. En vez de elegirlos a ojo, podemos pedirle a R que examine todas las ciudades.
res_ciudad <- ventas[flag_analitica == TRUE, .(
n = .N,
media = mean(ingreso, na.rm = TRUE),
mediana = median(ingreso, na.rm = TRUE),
sd = sd(ingreso, na.rm = TRUE),
q1 = quantile(ingreso, .25, na.rm = TRUE),
q3 = quantile(ingreso, .75, na.rm = TRUE),
p95 = quantile(ingreso, .95, na.rm = TRUE)
), by = ciudad]
res_ciudad[, `:=`(
ric = q3 - q1,
cv = sd / media
)]
res_ciudad[order(media), .(
ciudad, n = fmt_n(n), media = fmt_money(media), mediana = fmt_money(mediana),
sd = fmt_money(sd), CV = fmt_p(cv, 1), RIC = fmt_money(ric)
)] |> kable()
| ciudad | n | media | mediana | sd | CV | RIC |
|---|---|---|---|---|---|---|
| bogota | 101 | $ 735.301 | \(516.000 |\) 686.342 | 93,3% | \(646.000 | |Medellin |103 |\) 938.197 | \(573.500 |\) 1.091.181 |
| Cali | 78 | $1.035.896 | \(657.200 |\) 1.460.068 | 140,9% | $794.925 | |
| MEDELLIN | 99 | $1.205.097 | \(499.400 |\) 5.130.259 | 425,7% | $705.950 | |
| Ibagué | 69.173 | $1.226.077 | \(539.900 |\) 5.903.130 | 481,5% | $771.600 | |
| Villavicencio | 55.484 | $1.244.142 | \(534.950 |\) 6.545.201 | 526,1% | $764.400 | |
| Pereira | 69.165 | $1.248.170 | \(534.700 |\) 6.344.506 | 508,3% | $763.900 | |
| Manizales | 55.505 | $1.249.565 | \(489.500 |\) 6.119.759 | 489,8% | $777.800 | |
| Bucaramanga | 97.203 | $1.259.383 | \(551.000 |\) 6.279.612 | 498,6% | $782.900 | |
| Cali | 180.448 | $1.275.325 | \(560.500 |\) 6.298.317 | 493,9% | $802.600 | |
| Medellín | 220.897 | $1.318.239 | \(579.000 |\) 6.610.988 | 501,5% | $826.800 | |
| Barranquilla | 124.728 | $1.344.734 | \(572.450 |\) 7.183.550 | 534,2% | $820.125 | |
| Bogotá | 416.917 | $1.358.259 | \(587.500 |\) 6.757.560 | 497,5% | $839.600 | |
| Cartagena | 97.433 | $1.358.585 | \(580.100 |\) 6.870.137 | 505,7% | $826.700 | |
| Bogota | 92 | $2.689.708 | $705.150 | $12.720.633 | 472,9% | $848.825 |
| Barranquila | 99 | $4.347.236 | $645.900 | $30.607.440 | 704,1% | $758.800 |
Como la base contiene algunas etiquetas de ciudad inconsistentes, primero conviene distinguir comparaciones sustantivas de errores de escritura. Para ilustrar el fenómeno sembrado en la base, contrastaremos Pereira y Manizales.
comp_pm <- res_ciudad[ciudad %chin% c("Pereira", "Manizales")]
comp_pm[, .(
ciudad,
n = fmt_n(n),
media = fmt_money(media),
mediana = fmt_money(mediana),
desviacion = fmt_money(sd),
CV = fmt_p(cv, 1),
RIC = fmt_money(ric),
P95 = fmt_money(p95)
)] |> kable()
| ciudad | n | media | mediana | desviacion | CV | RIC | P95 |
|---|---|---|---|---|---|---|---|
| Manizales | 55.505 | $1.249.565 | $489.500 | $6.119.759 | 489,8% | $777.800 | $3.159.880 |
| Pereira | 69.165 | $1.248.170 | $534.700 | $6.344.506 | 508,3% | $763.900 | $2.942.460 |
ggplot(muestra_plot[ciudad %chin% c("Pereira", "Manizales")],
aes(x = ciudad, y = ingreso, fill = ciudad)) +
geom_boxplot(outlier.alpha = 0.10, show.legend = FALSE) +
coord_cartesian(ylim = c(0, p99_ingreso)) +
scale_fill_manual(values = c("Manizales" = "#7EA6D8", "Pereira" = "#2E75B6")) +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
labs(title = "Mismo promedio no significa mismo comportamiento",
subtitle = "Comparación de dispersión entre Pereira y Manizales",
x = NULL, y = "Ingreso por operación (COP)") +
theme_minimal(base_size = 12)
Interpretación. Una ciudad puede tener un ingreso medio atractivo y, sin embargo, mostrar menor estabilidad. La gerencia no debería usar únicamente el promedio para caracterizar la operación territorial.
segmentos_categoria <- ventas[flag_analitica == TRUE, .(
operaciones = .N,
ingreso_total = sum(ingreso, na.rm = TRUE),
utilidad_total = sum(utilidad, na.rm = TRUE),
ingreso_medio = mean(ingreso, na.rm = TRUE),
mediana_ingreso = median(ingreso, na.rm = TRUE),
sd_ingreso = sd(ingreso, na.rm = TRUE)
), by = categoria]
segmentos_categoria[, `:=`(
participacion_operaciones = operaciones / sum(operaciones),
participacion_ingreso = ingreso_total / sum(ingreso_total),
margen_sobre_ingreso = utilidad_total / ingreso_total,
cv_ingreso = sd_ingreso / ingreso_medio
)]
segmentos_categoria[order(-ingreso_total), .(
categoria,
operaciones = fmt_n(operaciones),
`% operaciones` = fmt_p(participacion_operaciones, 1),
ingreso_total = fmt_money(ingreso_total),
`% ingreso` = fmt_p(participacion_ingreso, 1),
ingreso_medio = fmt_money(ingreso_medio),
mediana = fmt_money(mediana_ingreso),
CV = fmt_p(cv_ingreso, 1),
margen = fmt_p(margen_sobre_ingreso, 1)
)] |> kable()
| categoria | operaciones | % operaciones | ingreso_total | % ingreso | ingreso_medio | mediana | CV | margen |
|---|---|---|---|---|---|---|---|---|
| Tecnología | 277.265 | 20,0% | $696.967.050.100 | 38,3% | $2.513.722 | $1.224.400 | 446,2% | 16,5% |
| Oficina | 194.339 | 14,0% | $273.191.209.400 | 15,0% | \(1.405.746 |\) 708.400 | 436,7% | 28,3% | |
| Electrodomésticos | 139.307 | 10,0% | $261.974.056.600 | 14,4% | \(1.880.552 |\) 965.300 | 433,2% | 24,1% | |
| Hogar | 250.112 | 18,0% | \(233.025.021.900 |12,8% |\) 931.683 | $ 481.500 | 422,4% | 29,8% | ||
| Cuidado personal | 166.252 | 12,0% | \(132.792.273.900 |7,3% |\) 798.741 | $ 417.850 | 424,1% | 32,6% | ||
| Accesorios | 207.744 | 15,0% | \(118.549.533.400 |6,5% |\) 570.652 | $ 290.100 | 432,8% | 38,0% | ||
| Deportes | 152.506 | 11,0% | \(104.196.976.700 |5,7% |\) 683.232 | $ 354.000 | 424,4% | 33,4% |
Esta tabla permite separar cuatro preguntas que suelen confundirse:
El ganador no tiene por qué ser el mismo en las cuatro.
plot_cat <- copy(segmentos_categoria)
ggplot(plot_cat, aes(x = ingreso_total, y = margen_sobre_ingreso, label = categoria)) +
geom_point(size = 4, color = azul) +
geom_text(vjust = -0.8, size = 3.6, check_overlap = TRUE) +
scale_x_continuous(labels = label_number(big.mark = ".", decimal.mark = ",", scale_cut = cut_short_scale())) +
scale_y_continuous(labels = label_percent(accuracy = 1)) +
labs(
title = "Volumen de ingresos y margen no son la misma cosa",
x = "Ingreso total", y = "Utilidad / ingreso"
) +
theme_minimal(base_size = 12)
Lectura gerencial. Un segmento puede liderar en facturación y no hacerlo en margen. Decidir únicamente con base en ventas puede premiar volumen que deja poco resultado económico.
res_canal <- ventas[flag_analitica == TRUE, .(
operaciones = .N,
ingreso_total = sum(ingreso, na.rm = TRUE),
ingreso_medio = mean(ingreso, na.rm = TRUE),
utilidad_total = sum(utilidad, na.rm = TRUE),
margen = sum(utilidad, na.rm = TRUE) / sum(ingreso, na.rm = TRUE),
devolucion_pct = mean(devolucion == "Sí", na.rm = TRUE),
entrega_mediana = median(tiempo_entrega_dias, na.rm = TRUE),
satisfaccion_media = mean(satisfaccion, na.rm = TRUE),
cobertura_satisfaccion = mean(!is.na(satisfaccion))
), by = canal][order(-ingreso_total)]
res_canal[, .(
canal,
operaciones = fmt_n(operaciones),
ingreso_total = fmt_money(ingreso_total),
ingreso_medio = fmt_money(ingreso_medio),
margen = fmt_p(margen, 1),
devoluciones = fmt_p(devolucion_pct, 1),
mediana_entrega_dias = fmt_num(entrega_mediana, 1),
satisfaccion_media = fmt_num(satisfaccion_media, 2),
cobertura_satisfaccion = fmt_p(cobertura_satisfaccion, 1)
)] |> kable()
| canal | operaciones | ingreso_total | ingreso_medio | margen | devoluciones | mediana_entrega_dias | satisfaccion_media | cobertura_satisfaccion |
|---|---|---|---|---|---|---|---|---|
| Tienda física | 472.147 | $625.362.310.600 | $1.324.508 | 25,3% | 3,1% | 0,0 | 4,58 | 47,9% |
| Página web | 374.040 | $487.093.600.400 | $1.302.250 | 25,3% | 4,1% | 4,0 | 4,40 | 95,5% |
| App | 278.271 | $366.176.524.700 | $1.315.899 | 25,3% | 3,8% | 4,0 | 4,46 | 95,5% |
| Marketplace | 166.071 | $214.470.941.100 | $1.291.441 | 19,6% | 5,0% | 7,0 | 4,14 | 95,5% |
| Venta telefónica | 96.996 | $127.592.745.200 | $1.315.443 | 25,2% | 3,3% | 5,0 | 4,31 | 95,6% |
Aquí la satisfacción se reporta junto con su cobertura. Comparar 4,3 contra 4,4 sin advertir que un canal responde mucho menos sería una interpretación floja.
ventas[, tramo_descuento := cut(
descuento,
breaks = c(-Inf, 0, .10, .20, .30, Inf),
labels = c("0%", ">0–10%", ">10–20%", ">20–30%", ">30%"),
right = TRUE
)]
res_desc <- ventas[flag_analitica == TRUE, .(
operaciones = .N,
unidades_media = mean(unidades, na.rm = TRUE),
ingreso_medio = mean(ingreso, na.rm = TRUE),
utilidad_media = mean(utilidad, na.rm = TRUE),
margen = sum(utilidad, na.rm = TRUE) / sum(ingreso, na.rm = TRUE),
devolucion = mean(devolucion == "Sí", na.rm = TRUE)
), by = tramo_descuento]
res_desc[, .(
tramo_descuento,
operaciones = fmt_n(operaciones),
unidades_media = fmt_num(unidades_media, 2),
ingreso_medio = fmt_money(ingreso_medio),
utilidad_media = fmt_money(utilidad_media),
margen = fmt_p(margen, 1),
devoluciones = fmt_p(devolucion, 1)
)] |> kable()
| tramo_descuento | operaciones | unidades_media | ingreso_medio | utilidad_media | margen | devoluciones |
|---|---|---|---|---|---|---|
| 0% | 360.718 | 5,12 | $1.312.744 | $334.297 | 25,5% | 3,7% |
| >20–30% | 171.044 | 7,17 | $1.342.800 | $302.291 | 22,5% | 3,7% |
| >0–10% | 471.940 | 5,53 | $1.320.388 | $331.344 | 25,1% | 3,8% |
| >10–20% | 338.336 | 5,85 | $1.260.771 | $314.958 | 25,0% | 3,8% |
| >30% | 45.487 | 9,54 | $1.490.091 | $288.129 | 19,3% | 4,1% |
Si los tramos de descuento alto muestran más unidades pero menor margen, aparece un hallazgo clásico de negocios: mover más producto no implica crear más valor por peso vendido.
No debemos afirmar causalidad. Los datos describen una asociación: otros factores —cliente, categoría, canal, temporada— también cambian entre operaciones.
En esta sección formulamos tres preguntas empresariales propias. La exigencia es que cada una conduzca a una decisión estadística concreta.
canal, tiempo_entrega_dias,
satisfaccion y devolucion.
res_entrega <- ventas[flag_analitica == TRUE & !is.na(tiempo_entrega_dias), .(
operaciones = .N,
entrega_mediana = median(tiempo_entrega_dias),
satisfaccion_media = mean(satisfaccion, na.rm = TRUE),
cobertura_satisfaccion = mean(!is.na(satisfaccion)),
devolucion_pct = mean(devolucion == "Sí", na.rm = TRUE)
), by = canal][order(entrega_mediana)]
res_entrega[, .(
canal,
operaciones = fmt_n(operaciones),
entrega_mediana,
satisfaccion_media = fmt_num(satisfaccion_media, 2),
cobertura_satisfaccion = fmt_p(cobertura_satisfaccion, 1),
devoluciones = fmt_p(devolucion_pct, 1)
)] |> kable()
| canal | operaciones | entrega_mediana | satisfaccion_media | cobertura_satisfaccion | devoluciones |
|---|---|---|---|---|---|
| Tienda física | 472.147 | 0 | 4,58 | 47,9% | 3,1% |
| Página web | 370.609 | 4 | 4,40 | 95,5% | 4,1% |
| App | 275.781 | 4 | 4,46 | 95,5% | 3,8% |
| Venta telefónica | 96.173 | 5 | 4,31 | 95,6% | 3,3% |
| Marketplace | 164.487 | 7 | 4,14 | 95,5% | 5,0% |
Comparamos posición típica de la entrega con dos indicadores de experiencia. Si los canales más lentos también exhiben satisfacción menor y/o más devoluciones, existe una señal descriptiva coherente para revisar la operación logística.
No podemos concluir, solo con esta tabla, que demorar un día adicional cause una devolución. El análisis es descriptivo.
categoria, ingreso, utilidad y
margen_pct.
med_ingreso_cat <- median(segmentos_categoria$ingreso_total)
med_margen_cat <- median(segmentos_categoria$margen_sobre_ingreso)
segmentos_categoria[, cuadrante := fcase(
ingreso_total >= med_ingreso_cat & margen_sobre_ingreso >= med_margen_cat, "Alto ingreso / Alto margen",
ingreso_total >= med_ingreso_cat & margen_sobre_ingreso < med_margen_cat, "Alto ingreso / Bajo margen",
ingreso_total < med_ingreso_cat & margen_sobre_ingreso >= med_margen_cat, "Bajo ingreso / Alto margen",
default = "Bajo ingreso / Bajo margen"
)]
segmentos_categoria[order(-ingreso_total), .(
categoria,
ingreso_total = fmt_money(ingreso_total),
margen = fmt_p(margen_sobre_ingreso, 1),
cuadrante
)] |> kable()
| categoria | ingreso_total | margen | cuadrante |
|---|---|---|---|
| Tecnología | $696.967.050.100 | 16,5% | Alto ingreso / Bajo margen |
| Oficina | $273.191.209.400 | 28,3% | Alto ingreso / Bajo margen |
| Electrodomésticos | $261.974.056.600 | 24,1% | Alto ingreso / Bajo margen |
| Hogar | $233.025.021.900 | 29,8% | Alto ingreso / Alto margen |
| Cuidado personal | $132.792.273.900 | 32,6% | Bajo ingreso / Alto margen |
| Accesorios | $118.549.533.400 | 38,0% | Bajo ingreso / Alto margen |
| Deportes | $104.196.976.700 | 33,4% | Bajo ingreso / Alto margen |
La matriz evita llamar “mejor categoría” a la que simplemente factura más. Una categoría en “alto ingreso / bajo margen” merece una pregunta distinta a una de “bajo ingreso / alto margen”.
Usaremos un umbral relativo al propio negocio: P99 del ingreso. Esto identifica el 1 % superior sin afirmar que sea un error.
umbral_top1 <- quantile(ventas[flag_analitica == TRUE, ingreso], .99, na.rm = TRUE)
top1 <- ventas[flag_analitica == TRUE & ingreso >= umbral_top1]
res_top1_cliente <- top1[, .N, by = tipo_cliente][, participacion := N / sum(N)][order(-N)]
res_top1_cliente[, .(
tipo_cliente,
operaciones_top1 = fmt_n(N),
participacion = fmt_p(participacion, 1)
)] |> kable()
| tipo_cliente | operaciones_top1 | participacion |
|---|---|---|
| Empresa | 9.421 | 67,9% |
| Cliente mayorista | 2.830 | 20,4% |
| Persona | 1.625 | 11,7% |
top1[, .N, by = .(tipo_cliente, canal)][order(-N)] |> head(15) |> kable()
| tipo_cliente | canal | N |
|---|---|---|
| Empresa | Tienda física | 3207 |
| Empresa | Página web | 2509 |
| Empresa | App | 1908 |
| Empresa | Marketplace | 1120 |
| Cliente mayorista | Tienda física | 1010 |
| Cliente mayorista | Página web | 737 |
| Empresa | Venta telefónica | 677 |
| Cliente mayorista | App | 589 |
| Persona | Tienda física | 545 |
| Persona | Página web | 450 |
| Persona | App | 334 |
| Cliente mayorista | Marketplace | 301 |
| Cliente mayorista | Venta telefónica | 193 |
| Persona | Marketplace | 182 |
| Persona | Venta telefónica | 114 |
Si las operaciones extremas se concentran en empresas o mayoristas, eso apoya la idea de que muchos valores atípicos representan compras reales de gran escala. Eliminarlos de manera automática habría borrado precisamente un comportamiento comercial relevante.
La selección final no debe hacerse por cantidad de tablas, sino por relevancia. A continuación se propone una forma de redactar tres hallazgos; las cifras se completan dinámicamente con la base.
La media del ingreso por operación es $1.312.190 y la mediana es $567.000. El percentil 99 alcanza $8.725.780.
La separación entre media y mediana, junto con una cola superior extensa, muestra una distribución asimétrica. Un grupo pequeño de operaciones grandes eleva el promedio.
Para describir una compra “típica” conviene comunicar mediana y cuartiles; para presupuestos agregados sigue siendo necesario estudiar ingresos totales y promedio. Son preguntas diferentes.
La categoría con mayor ingreso total es Tecnología, con $696.967.050.100 y margen agregado de 16,5%. La categoría con mayor margen agregado es Accesorios, con 38,0%.
Los indicadores de escala y rentabilidad ordenan los segmentos de forma diferente.
Las decisiones de portafolio no deberían evaluarse exclusivamente por facturación; deben considerar cuánto resultado queda después de costos.
El canal con mayor mediana de entrega es Marketplace (7,0 días), con devolución de 5,0% y satisfacción media de 4,14. La cobertura de satisfacción en ese canal es 95,5%.
Entrega, devolución y satisfacción ofrecen dimensiones complementarias de la experiencia. Además, la tasa de respuesta de satisfacción cambia la confianza descriptiva con la que debe leerse el promedio.
Antes de comparar canales solo por satisfacción, conviene revisar cobertura de la encuesta y desempeño logístico.
Una presentación ejecutiva podría cerrarse así:
La operación típica es bastante menor que lo que sugiere el promedio cuando existen compras corporativas muy grandes. Conviene separar indicadores de operación típica de indicadores de volumen total.
Los líderes en facturación no necesariamente son los líderes en rentabilidad. La gerencia debería revisar conjuntamente ingreso, utilidad y margen antes de priorizar categorías o canales.
La experiencia del cliente está asociada con condiciones operativas distintas entre canales, pero la satisfacción tiene cobertura desigual. La siguiente decisión debería ser revisar logística, devoluciones y mecanismo de captura de satisfacción de manera integrada.
Estas conclusiones son descriptivas. Expresiones como “el descuento causa más ventas” o “la demora causa devoluciones” exceden lo que este análisis permite afirmar.
Resolver bien este proyecto implica más que ejecutar
mean() y sd():
Esta sección funciona como lista de control para el profesor. No es necesario entregarla a los estudiantes.
checklist <- data.table(
fenomeno = c(
"IDs de transacción repetidos",
"Satisfacción faltante y desigual por canal",
"Satisfacción fuera de escala",
"Tiempos de entrega negativos",
"Descuentos fuera del rango operativo",
"Etiquetas inconsistentes de ciudad",
"Operaciones extremas plausibles",
"Media de ingreso afectada por cola derecha",
"Diferencias de dispersión entre ciudades",
"Diferencia entre ingreso y rentabilidad",
"Descuentos altos asociados a volumen y presión de margen"
),
idea_clave = c(
"No deduplicar sin investigar",
"NA no necesariamente aleatorio",
"Inconsistencia por definición",
"Inconsistencia por definición",
"Validar según regla de negocio",
"Limpiar etiquetas sin ocultar el hallazgo de calidad",
"Atípico no equivale a error",
"Mediana y RIC son esenciales",
"Mismo centro puede ocultar distinta estabilidad",
"Facturación y margen responden preguntas distintas",
"Asociación descriptiva, no causalidad"
)
)
kable(checklist)
| fenomeno | idea_clave |
|---|---|
| IDs de transacción repetidos | No deduplicar sin investigar |
| Satisfacción faltante y desigual por canal | NA no necesariamente aleatorio |
| Satisfacción fuera de escala | Inconsistencia por definición |
| Tiempos de entrega negativos | Inconsistencia por definición |
| Descuentos fuera del rango operativo | Validar según regla de negocio |
| Etiquetas inconsistentes de ciudad | Limpiar etiquetas sin ocultar el hallazgo de calidad |
| Operaciones extremas plausibles | Atípico no equivale a error |
| Media de ingreso afectada por cola derecha | Mediana y RIC son esenciales |
| Diferencias de dispersión entre ciudades | Mismo centro puede ocultar distinta estabilidad |
| Diferencia entre ingreso y rentabilidad | Facturación y margen responden preguntas distintas |
| Descuentos altos asociados a volumen y presión de margen | Asociación descriptiva, no causalidad |
La solución mantiene una sola tabla grande, ventas, y
usa flag_analitica e índices para evitar copias completas
innecesarias. Esto es importante con más de diez millones de
registros.
sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.50 scales_1.4.0 ggplot2_4.0.0 data.table_1.17.8
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.6.5 cli_3.6.5 rlang_1.1.6 xfun_0.52
## [5] generics_0.1.4 S7_0.2.0 jsonlite_2.0.0 labeling_0.4.3
## [9] glue_1.8.0 htmltools_0.5.8.1 sass_0.4.10 rmarkdown_2.29
## [13] grid_4.5.1 tibble_3.3.0 evaluate_1.0.4 jquerylib_0.1.4
## [17] fastmap_1.2.0 yaml_2.3.10 lifecycle_1.0.4 compiler_4.5.1
## [21] dplyr_1.1.4 RColorBrewer_1.1-3 pkgconfig_2.0.3 rstudioapi_0.17.1
## [25] farver_2.1.2 digest_0.6.37 R6_2.6.1 tidyselect_1.2.1
## [29] pillar_1.11.0 magrittr_2.0.3 bslib_0.9.0 withr_3.0.2
## [33] tools_4.5.1 gtable_0.3.6 cachem_1.1.0