1 Problema planteado

Variable respuesta: precio anunciado (en pesos colombianos) de un Mazda 2 publicado en OLX.

Quién lo usaría: una persona que quiere publicar su Mazda 2 o evaluar si el precio de un anuncio es razonable.

Aclaración importante: el dato es el precio anunciado, no el precio efectivo de venta. Los anuncios no permiten conocer el valor real de la transacción, que normalmente se negocia.

2 Datos y calidad de la información

if (!file.exists("base1_mazdacol.xlsx")) {
}
crudo <- as.data.frame( read_excel("C:/Users/nicol/Downloads/base1_mazdacol.xlsx"))
vars <- c("precio", "kilometraje", "transmision", "modelo", "color", "ciudad", "departamento")
crudo <- crudo[, vars]
cat("Registros originales:", nrow(crudo), "\n")
## Registros originales: 314
str(crudo)
## 'data.frame':    314 obs. of  7 variables:
##  $ precio      : num  34000000 26400000 42500000 91000000 35000000 67000000 45000000 31800000 66900000 27500000 ...
##  $ kilometraje : num  140000 112000 102222 1000 134500 ...
##  $ transmision : chr  "Automática" "Automática" "Automática" "Automática" ...
##  $ modelo      : num  2008 2012 2013 2022 2012 ...
##  $ color       : chr  "Gris" "Gris" "Gris" "Gris" ...
##  $ ciudad      : chr  "Medellín" "Medellín" "Itagüí" "Envigado" ...
##  $ departamento: chr  "Antioquia" "Antioquia" "Antioquia" "Antioquia" ...

2.1 Valores faltantes

colSums(is.na(crudo))
##       precio  kilometraje  transmision       modelo        color       ciudad 
##            0            0            0            0            0            0 
## departamento 
##            0

Las variables de interés no tienen valores faltantes, así que no hay que imputar ni eliminar registros por este motivo.

2.2 Duplicados

Un anuncio duplicado es aquel que repite todas sus características (precio, kilometraje, transmisión, modelo, color y ciudad). Mantenerlo contaría dos veces el mismo carro.

vars_dup <- c("precio", "kilometraje", "transmision", "modelo", "color", "ciudad")
n_dup <- sum(duplicated(crudo[, vars_dup]))
cat("Duplicados encontrados:", n_dup, "\n")
## Duplicados encontrados: 1
datos1 <- crudo[!duplicated(crudo[, vars_dup]), ]

2.3 Atípicos y errores evidentes

Antes de aplicar criterios estadísticos, reviso valores que son imposibles o muy improbables para un Mazda 2.

# 1. Precio sospechosamente bajo para un modelo reciente
datos1[datos1$precio < 10e6, c("precio", "modelo", "kilometraje")]
##      precio modelo kilometraje
## 280 6800000   2022       17200
# 2. Modelo anterior al lanzamiento del Mazda 2 (2002)
datos1[datos1$modelo < 2002, c("precio", "modelo", "kilometraje")]
##       precio modelo kilometraje
## 189 29900000   1995      280000
# 3. Carros de modelos anteriores a 2020 con menos de 1.000 km
datos1[datos1$modelo < 2020 & datos1$kilometraje < 1000, c("precio", "modelo", "kilometraje")]
##       precio modelo kilometraje
## 17  36000000   2011         183
## 105 22000000   2008         136
## 168 30000000   2008         174
## 180 36000000   2012          92
## 185 30000000   2008         133
## 196 61000000   2019          54
## 203 28000000   2009           0
## 228 37900000   2013         116
## 236 32900000   2010         114
## 248 66000000   2019          51
## 272 34800000   2011          99
## 298 29000000   2008         129
## 304 63000000   2019          35

Reglas de exclusión y su justificación:

  1. Precio menor a 10 millones: el único caso es un modelo 2022 con 6,8 millones, valor incompatible con el resto del mercado; es casi seguro un error de digitación o un anuncio engañoso.
  2. Modelo anterior a 2002: el Mazda 2 se lanzó en 2002, así que un anuncio de 1995 es un error de registro.
  3. Modelo anterior a 2020 con menos de 1.000 km: un carro con varios años de uso no puede tener un kilometraje tan bajo; probablemente es un dato mal digitado (por ejemplo, 136 en lugar de 136.000).

Los carros nuevos (modelo 2020 o más) con kilometraje cercano a cero sí se conservan, porque son consistentes.

excluir <- datos1$precio < 10e6 |
           datos1$modelo < 2002 |
           (datos1$modelo < 2020 & datos1$kilometraje < 1000)

datos <- datos1[!excluir, ]
datos$transmision <- factor(datos$transmision, levels = c("Mecánica", "Automática"))
datos$auto <- as.integer(datos$transmision == "Automática")
rownames(datos) <- NULL

kable(data.frame(Paso = c("Originales", "Sin duplicados", "Base final"),
                 Registros = c(nrow(crudo), nrow(datos1), nrow(datos))))
Paso Registros
Originales 314
Sin duplicados 313
Base final 298

Con la base final de 298 registros se hace el resto del análisis. Los valores extremos que quedan (carros muy nuevos y caros) son consistentes con el mercado, por eso no se eliminan.

3 Análisis exploratorio

3.1 Estadísticas descriptivas

desc <- t(sapply(datos[, c("precio", "kilometraje", "modelo")], function(x)
  c(Media = mean(x), Mediana = median(x), DE = sd(x), Mínimo = min(x), Máximo = max(x))))
kable(round(desc, 0), caption = "Estadísticas de precio, kilometraje y modelo")
Estadísticas de precio, kilometraje y modelo
Media Mediana DE Mínimo Máximo
precio 51280201 42700000 20472704 22000000 169800000
kilometraje 78020 81378 42735 0 200000
modelo 2015 2015 4 2007 2022
table(datos$transmision)
## 
##   Mecánica Automática 
##        131        167
round(prop.table(table(datos$transmision)) * 100, 1)
## 
##   Mecánica Automática 
##         44         56
sort(table(datos$departamento), decreasing = TRUE)[1:6]
## 
##     Antioquia ValledelCauca        Bogotá        Caldas     Santander 
##            81            51            49            22            20 
##     Atlántico 
##            13

El precio mediano es de 42.7 millones. La media (51.3 millones) es mayor que la mediana, señal de asimetría hacia la derecha por los carros nuevos y caros.

3.2 Gráficos de cada variable

ggplot(datos, aes(x = precio / 1e6)) +
  geom_histogram(bins = 25, fill = "steelblue", color = "white") +
  labs(title = "Distribución del precio anunciado", x = "Precio (millones de pesos)", y = "Anuncios")

ggplot(datos, aes(x = kilometraje / 1000)) +
  geom_histogram(bins = 25, fill = "darkorange", color = "white") +
  labs(title = "Distribución del kilometraje", x = "Kilometraje (miles de km)", y = "Anuncios")

ggplot(datos, aes(x = factor(modelo))) +
  geom_bar(fill = "seagreen") +
  labs(title = "Anuncios por año del modelo", x = "Modelo", y = "Anuncios") +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5))

4 Relación entre el precio y los predictores

4.1 Variables cuantitativas: dispersión y correlación

ggplot(datos, aes(x = modelo, y = precio / 1e6)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE, color = "red") +
  labs(title = "Precio según el año del modelo", x = "Modelo", y = "Precio (millones)")

ggplot(datos, aes(x = kilometraje / 1000, y = precio / 1e6)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE, color = "red") +
  labs(title = "Precio según el kilometraje", x = "Kilometraje (miles de km)", y = "Precio (millones)")

kable(round(cor(datos[, c("precio", "modelo", "kilometraje")]), 2),
      caption = "Matriz de correlación")
Matriz de correlación
precio modelo kilometraje
precio 1.00 0.86 -0.80
modelo 0.86 1.00 -0.87
kilometraje -0.80 -0.87 1.00
  • El precio se asocia positiva y fuertemente con el año (r = 0.86): carros más nuevos cuestan más.
  • El precio se asocia negativa y moderadamente con el kilometraje (r = -0.8): más uso, menor precio.
  • Año y kilometraje están muy correlacionados entre sí (r = -0.87). Esto importa para el Punto 2, porque ambos “cuentan la misma historia” (la edad del carro) y puede generar multicolinealidad.

4.2 Variable categórica binaria: transmisión

ggplot(datos, aes(x = transmision, y = precio / 1e6, fill = transmision)) +
  geom_boxplot(show.legend = FALSE) +
  labs(title = "Precio según la transmisión", x = NULL, y = "Precio (millones)")

tabla_trans <- data.frame(
  Transmisión = levels(datos$transmision),
  n = as.vector(table(datos$transmision)),
  Media = round(tapply(datos$precio, datos$transmision, mean) / 1e6, 1),
  Mediana = round(tapply(datos$precio, datos$transmision, median) / 1e6, 1))
kable(tabla_trans, row.names = FALSE, caption = "Precio por transmisión (millones de pesos)")
Precio por transmisión (millones de pesos)
Transmisión n Media Mediana
Mecánica 131 44 39.0
Automática 167 57 57.9
t.test(precio ~ transmision, data = datos)
## 
##  Welch Two Sample t-test
## 
## data:  precio by transmision
## t = -6.0902, df = 261.09, p-value = 4.015e-09
## alternative hypothesis: true difference in means between group Mecánica and group Automática is not equal to 0
## 95 percent confidence interval:
##  -17088158  -8738047
## sample estimates:
##   mean in group Mecánica mean in group Automática 
##                 44043664                 56956767

Los carros automáticos se anuncian más caros en promedio. Pero esta diferencia puede estar mezclada con la edad: si los automáticos tienden a ser más nuevos, parte de la diferencia viene del año y no de la transmisión. Esto lo revisa el modelo múltiple.

5 Tres modelos de regresión lineal simple

Cada modelo usa un solo predictor:

  • Modelo A: precio según el año del modelo.
  • Modelo B: precio según el kilometraje.
  • Modelo C: precio según la transmisión automática (variable binaria; la categoría de referencia es Mecánica).
mA <- lm(precio ~ modelo, data = datos)
mB <- lm(precio ~ kilometraje, data = datos)
mC <- lm(precio ~ auto, data = datos)
summary(mA)
## 
## Call:
## lm(formula = precio ~ modelo, data = datos)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -42406500  -4705916   -855682   2969493 101593500 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -8.614e+09  2.953e+08  -29.17   <2e-16 ***
## modelo       4.300e+06  1.465e+05   29.34   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 10370000 on 296 degrees of freedom
## Multiple R-squared:  0.7442, Adjusted R-squared:  0.7433 
## F-statistic:   861 on 1 and 296 DF,  p-value: < 2.2e-16
summary(mB)
## 
## Call:
## lm(formula = precio ~ kilometraje, data = datos)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -47428444  -6869878   -977426   5148454 102503390 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  8.104e+07  1.498e+06   54.09   <2e-16 ***
## kilometraje -3.814e+02  1.685e+01  -22.64   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 12410000 on 296 degrees of freedom
## Multiple R-squared:  0.6338, Adjusted R-squared:  0.6326 
## F-statistic: 512.4 on 1 and 296 DF,  p-value: < 2.2e-16
summary(mC)
## 
## Call:
## lm(formula = precio ~ auto, data = datos)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -33456767 -13803491  -2543664  12893836 112843233 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 44043664    1701350  25.887  < 2e-16 ***
## auto        12913102    2272707   5.682 3.19e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 19470000 on 296 degrees of freedom
## Multiple R-squared:  0.09834,    Adjusted R-squared:  0.09529 
## F-statistic: 32.28 on 1 and 296 DF,  p-value: 3.188e-08
extrae <- function(m, nombre) {
  s <- summary(m)
  data.frame(Modelo = nombre,
             Intercepto = round(unname(coef(m)[1])),
             Pendiente = round(unname(coef(m)[2]), 2),
             `p valor` = signif(s$coefficients[2, 4], 3),
             R2 = round(s$r.squared, 3),
             check.names = FALSE)
}
kable(rbind(extrae(mA, "A: modelo"), extrae(mB, "B: kilometraje"), extrae(mC, "C: automática")),
      row.names = FALSE, caption = "Resumen de los modelos simples")
Resumen de los modelos simples
Modelo Intercepto Pendiente p valor R2
A: modelo -8613729345 4300116.8 0 0.744
B: kilometraje 81036774 -381.4 0 0.634
C: automática 44043664 12913102.4 0 0.098

5.1 Interpretación

  • Modelo A. Por cada año más nuevo, el precio anunciado sube en promedio 4.300.117 pesos. El año explica el 74.4 % de la variabilidad del precio. El intercepto no tiene interpretación práctica (sería el precio de un carro del año 0).
  • Modelo B. Cada 1.000 km adicionales se asocian con un precio 381.396 pesos menor. Explica el 63.4 % de la variabilidad.
  • Modelo C. Un carro automático se anuncia en promedio 12.913.102 pesos más caro que uno mecánico, pero explica solo el 9.8 % del precio.
  • Significancia. Los coeficientes de los modelos A y B tienen p-valores muy pequeños y el del modelo C debe leerse en la tabla: hay evidencia estadística de asociación cuando el p-valor es menor a 0,05.
  • Asociación no es causalidad. Que el año o el kilometraje se asocien con el precio no demuestra que cambiar uno de ellos “cause” un cambio de precio. Hay factores no observados (estado del carro, versión, mantenimiento) y la transmisión puede estar reflejando que los automáticos son más nuevos.

6 Comparación con validación cruzada

Se usa validación cruzada de 10 pliegues con pliegues fijos (los mismos se reutilizarán en el Punto 2). La métrica es el MAE (error absoluto medio, en pesos), fácil de interpretar. La referencia sencilla es predecir siempre la mediana del entrenamiento.

set.seed(2026)
# 10 pliegues aleatorios; cada elemento de la lista guarda los índices de prueba
folds <- split(sample(nrow(datos)), rep(1:10, length.out = nrow(datos)))

cv_simple <- function(formula) {
  sapply(folds, function(idx) {
    fit  <- lm(formula, data = datos[-idx, ])
    pred <- predict(fit, newdata = datos[idx, ])
    mean(abs(datos$precio[idx] - pred))
  })
}

mediana_mae <- sapply(folds, function(idx)
  mean(abs(datos$precio[idx] - median(datos$precio[-idx]))))
mae_A <- cv_simple(precio ~ modelo)
mae_B <- cv_simple(precio ~ kilometraje)
mae_C <- cv_simple(precio ~ auto)

comparacion <- data.frame(
  Modelo = c("Mediana (referencia)", "A: año del modelo", "B: kilometraje", "C: transmisión automática"),
  R2 = round(c(NA, summary(mA)$r.squared, summary(mB)$r.squared, summary(mC)$r.squared), 3),
  `MAE (millones)` = round(c(mean(mediana_mae), mean(mae_A), mean(mae_B), mean(mae_C)) / 1e6, 2),
  `Mejora vs. mediana (%)` = round((1 - c(mean(mediana_mae), mean(mae_A), mean(mae_B), mean(mae_C)) /
                                      mean(mediana_mae)) * 100, 1),
  check.names = FALSE)
kable(comparacion, caption = "Desempeño con validación cruzada de 10 pliegues")
Desempeño con validación cruzada de 10 pliegues
Modelo R2 MAE (millones) Mejora vs. mediana (%)
Mediana (referencia) NA 15.91 0.0
A: año del modelo 0.744 5.62 64.7
B: kilometraje 0.634 8.04 49.5
C: transmisión automática 0.098 15.25 4.1
cv_long <- data.frame(
  Modelo = rep(c("Mediana", "A: año", "B: km", "C: automática"), each = length(folds)),
  MAE = c(mediana_mae, mae_A, mae_B, mae_C) / 1e6)
ggplot(cv_long, aes(x = Modelo, y = MAE, fill = Modelo)) +
  geom_boxplot(show.legend = FALSE) +
  labs(title = "MAE por pliegue", y = "MAE (millones de pesos)", x = NULL)

6.1 Mejor modelo simple

El modelo A (año del modelo) es el mejor: tiene el menor MAE (5.62 millones), supera claramente la referencia de la mediana (15.9 millones), su signo es coherente (más nuevo, más caro) y es muy fácil de explicar.

  • Un error medio de 5.62 millones sobre un precio mediano de 42.7 millones equivale a cerca del 13 %: es útil como orientación, pero no es un valor exacto.
  • El modelo B también supera la referencia, pero con mayor error; además comparte información con el año.
  • El modelo C casi no mejora la referencia: la transmisión por sí sola dice poco sobre el precio.