Antes de empezar

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.

1. Cargar y reconocer la base

1.1 Lectura eficiente

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.

1.2 ¿Qué representa cada fila?

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.

Unidad de observación

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.

Población de interés

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.

2. Fase 1 — Conozca los datos

2.1 Diccionario y clasificación de variables

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.

2.2 Datos faltantes

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.

2.3 ¿El identificador de transacción es único?

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)]

2.4 Reglas de validación: imposible, sospechoso y atípico

Separaremos tres conceptos:

  • Imposible/inconsistente por definición: por ejemplo, satisfacción fuera de 1–5 o tiempo de entrega negativo.
  • Sospechoso: muy raro, pero requiere contexto antes de declararlo error.
  • Atípico válido: observación extrema que sí puede representar una operación real, como una compra corporativa grande.
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.

2.5 Consistencia entre variables relacionadas

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.

2.6 ¿Qué variables son especialmente importantes?

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.

3. Definir la base analítica antes de calcular

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.

4. Fase 2 — Radiografía del negocio

4.1 ¿Cómo se distribuye el ingreso por operació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.

Interpretación de las medidas

  • Media: ingreso promedio por operación; es sensible a compras excepcionalmente grandes.
  • Mediana: deja 50 % de las operaciones por debajo y 50 % por encima; suele describir mejor una operación típica cuando hay asimetría fuerte.
  • Desviación estándar: distancia típica respecto de la media, en pesos.
  • RIC: amplitud del 50 % central; es robusta ante extremos.
  • CV: dispersión relativa respecto de la media. Es útil para comparar grupos con escalas distintas siempre que la media sea positiva y tenga sentido.
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.

4.2 ¿Media y mediana cuentan la misma historia?

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.

4.3 El 50 % central y la regla de 1,5 RIC

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

4.4 Forma: asimetría y curtosis

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”.

5. Fase 2.2 — Mire más allá del promedio

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.

6. Fase 3 — ¿Todos los segmentos se comportan igual?

6.1 ¿Qué segmentos concentran las operaciones?

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:

  1. ¿Quién tiene más operaciones?
  2. ¿Quién genera más ingresos totales?
  3. ¿Quién tiene operaciones típicas de mayor valor?
  4. ¿Quién convierte mejor el ingreso en utilidad?

El ganador no tiene por qué ser el mismo en las cuatro.

6.2 Ventas altas no equivalen a rentabilidad alta

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.

6.3 Comparación por canal

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.

6.4 ¿Descuentos altos siempre son mejores?

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.

7. Fase 4 — Investigue el negocio

En esta sección formulamos tres preguntas empresariales propias. La exigencia es que cada una conduzca a una decisión estadística concreta.

7.1 Pregunta 1 — ¿Los canales con entregas más lentas muestran peor experiencia?

Variables

canal, tiempo_entrega_dias, satisfaccion y devolucion.

Análisis

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%

Interpretación

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.

Lo que NO podemos decir

No podemos concluir, solo con esta tabla, que demorar un día adicional cause una devolución. El análisis es descriptivo.

7.2 Pregunta 2 — ¿Qué categorías combinan escala con rentabilidad?

Variables

categoria, ingreso, utilidad y margen_pct.

Análisis

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

Interpretación

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”.

7.3 Pregunta 3 — ¿Dónde se concentran las operaciones excepcionalmente grandes?

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

Interpretación

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.

8. Fase 5 — De resultados a hallazgos

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.

Hallazgo 1 — La operación típica no está bien representada por el promedio

Evidencia

La media del ingreso por operación es $1.312.190 y la mediana es $567.000. El percentil 99 alcanza $8.725.780.

Interpretación

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.

Implicación para el negocio

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.

Hallazgo 2 — Facturar más no significa necesariamente ser más rentable

Evidencia

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%.

Interpretación

Los indicadores de escala y rentabilidad ordenan los segmentos de forma diferente.

Implicación para el negocio

Las decisiones de portafolio no deberían evaluarse exclusivamente por facturación; deben considerar cuánto resultado queda después de costos.

Hallazgo 3 — La calidad de la experiencia debe evaluarse junto con la logística y la cobertura del dato

Evidencia

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%.

Interpretación

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.

Implicación para el negocio

Antes de comparar canales solo por satisfacción, conviene revisar cobertura de la encuesta y desempeño logístico.

9. Cinco minutos con la gerencia

Una presentación ejecutiva podría cerrarse así:

  1. 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.

  2. 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.

  3. 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.

10. Qué aprendimos metodológicamente

Resolver bien este proyecto implica más que ejecutar mean() y sd():

  • definir la unidad de observación antes de analizar;
  • distinguir tipo de variable de tipo de almacenamiento;
  • estudiar la estructura de los faltantes;
  • diferenciar valores imposibles, sospechosos y extremos válidos;
  • elegir la población analítica adecuada para cada pregunta;
  • preferir medidas robustas cuando la distribución lo exige;
  • comparar dispersión además de promedios;
  • separar volumen, rentabilidad y experiencia;
  • no confundir asociación descriptiva con causalidad;
  • traducir resultados estadísticos en preguntas de negocio.

11. Anexo docente — Fenómenos que conviene comprobar

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

12. Limpieza de objetos auxiliares (opcional)

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