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.

7 Validación de supuestos del mejor modelo simple

Reviso los supuestos sobre el modelo A (año del modelo), que es el que mejor predice.

par(mfrow = c(2, 2))
plot(mA)

par(mfrow = c(1, 1))

bp_A <- bptest(mA)$p.value                      # varianza constante
sh_A <- shapiro.test(residuals(mA))$p.value     # normalidad
cook_A <- cooks.distance(mA)
umbral_A <- 4 / nrow(datos)
n_cook_A <- sum(cook_A > umbral_A)

plot(cook_A, type = "h", main = "Distancia de Cook (modelo A)", ylab = "Distancia de Cook")
abline(h = umbral_A, col = "red")

  • Linealidad. En el gráfico de residuos contra valores ajustados, una línea roja casi plana indica que una recta describe bien la relación; una curva indicaría que el precio no baja de forma lineal con la edad.
  • Varianza constante. La prueba de Breusch-Pagan da p = 0.025, así que hay evidencia de que la varianza de los errores no es constante: el modelo se equivoca más en unos precios que en otros.
  • Normalidad. Shapiro-Wilk da p = 10^{-24}: los errores se apartan de una normal, sobre todo en los extremos del gráfico Q-Q.
  • Independencia. Cada anuncio es de un vendedor distinto y la base no es una serie de tiempo, por lo que el supuesto es razonable, aunque no se puede comprobar con certeza.
  • Observaciones influyentes. 11 anuncios superan el umbral 4/n de la distancia de Cook. Los revisé uno a uno y no encontré errores evidentes, por eso los mantuve.
  • Multicolinealidad. No aplica con un solo predictor; se evalúa en el Punto 2.

Si la varianza no es constante, el error de predicción es distinto según el precio del carro y los intervalos de predicción dejan de ser confiables. Si los errores no son normales, los p-valores e intervalos son aproximados, aunque la recta estimada cambia poco.

8 Transformación de variables

El precio es positivo y asimétrico, y los errores tienden a crecer con el precio. Por eso pruebo el logaritmo de la respuesta: con él los efectos pasan a ser porcentuales, que es más natural en carros.

mA_log <- lm(log(precio) ~ modelo, data = datos)
summary(mA_log)
## 
## Call:
## lm(formula = log(precio) ~ modelo, data = datos)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.93807 -0.06059  0.00608  0.06482  0.94618 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -1.457e+02  3.940e+00  -36.98   <2e-16 ***
## modelo       8.109e-02  1.955e-03   41.47   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1384 on 296 degrees of freedom
## Multiple R-squared:  0.8532, Adjusted R-squared:  0.8527 
## F-statistic:  1720 on 1 and 296 DF,  p-value: < 2.2e-16
par(mfrow = c(2, 2)); plot(mA_log); par(mfrow = c(1, 1))

# MAE en pesos: la predicción se devuelve a la escala original con exp()
mae_A_log <- sapply(folds, function(idx) {
  fit  <- lm(log(precio) ~ modelo, data = datos[-idx, ])
  pred <- exp(predict(fit, newdata = datos[idx, ]))
  mean(abs(datos$precio[idx] - pred))
})

kable(data.frame(Modelo = c("A original", "A con log(precio)"),
                 `MAE (millones)` = round(c(mean(mae_A), mean(mae_A_log)) / 1e6, 2),
                 check.names = FALSE),
      caption = "Comparación en pesos")
Comparación en pesos
Modelo MAE (millones)
A original 5.62
A con log(precio) 4.80

En el modelo con logaritmo, cada año más nuevo cambia el precio en 8.4 %. El MAE en pesos pasa de 5.62 a 4.8 millones, así que la transformación reduce el error y la adopto para el modelo simple. No comparo el R² de los dos modelos porque están en escalas distintas.

9 Predicción hipotética con el modelo simple

esc_A <- data.frame(modelo = c(2012, 2018))
usa_log_A <- mean(mae_A_log) < mean(mae_A)
if (usa_log_A) {
  pred_A <- exp(predict(mA_log, newdata = esc_A, interval = "prediction", level = 0.95))
} else {
  pred_A <- predict(mA, newdata = esc_A, interval = "prediction", level = 0.95)
}
kable(data.frame(Escenario = c("Mazda 2 modelo 2012", "Mazda 2 modelo 2018"),
                 `Precio estimado (millones)` = round(pred_A[, "fit"] / 1e6, 1),
                 `Límite inferior 95 %` = round(pred_A[, "lwr"] / 1e6, 1),
                 `Límite superior 95 %` = round(pred_A[, "upr"] / 1e6, 1),
                 check.names = FALSE))
Escenario Precio estimado (millones) Límite inferior 95 % Límite superior 95 %
Mazda 2 modelo 2012 37.4 28.4 49.1
Mazda 2 modelo 2018 60.8 46.3 79.9

El intervalo de predicción indica entre qué valores caería el precio anunciado de un carro concreto con ese año. El MAE de validación cruzada del modelo A es 5.62 millones, cerca de 13 % del precio mediano, y mejora 65 % frente a predecir siempre la mediana.

10 Punto 2. Regresión lineal múltiple

10.1 Preparación de la variable región

Hay 20 departamentos y varios tienen entre 1 y 3 anuncios, muy pocos para estimar un coeficiente confiable. Por eso agrupo con una regla fija (no aprendida de los datos): Antioquia, Valle del Cauca, Bogotá y Otros. La categoría de referencia es Antioquia (la más numerosa): cada coeficiente de región se lee como la diferencia frente a un carro igual anunciado en Antioquia.

datos$region <- ifelse(datos$departamento == "Antioquia", "Antioquia",
                ifelse(datos$departamento == "ValledelCauca", "Valle del Cauca",
                ifelse(datos$departamento == "Bogotá", "Bogotá", "Otros")))
datos$region <- relevel(factor(datos$region), ref = "Antioquia")
table(datos$region)
## 
##       Antioquia          Bogotá           Otros Valle del Cauca 
##              81              49             117              51

10.2 Función de validación cruzada

Usa los mismos 10 pliegues del Punto 1. Si se pide usar_step, la selección de variables se hace dentro de cada entrenamiento, nunca con los datos de prueba. Si la respuesta es log(precio), las predicciones se devuelven a pesos con exp() antes de calcular el MAE, para que sea comparable con los demás modelos.

cv_mae <- function(formula, data, folds, usar_step = FALSE, log_y = FALSE) {
  sapply(folds, function(idx) {
    train <- data[-idx, ]
    test  <- data[idx, ]
    fit <- do.call("lm", list(formula = formula, data = train))  # do.call: necesario para que step() funcione dentro de la función
    if (usar_step) fit <- step(fit, trace = 0)
    pred <- predict(fit, newdata = test)
    if (log_y) pred <- exp(pred)
    mean(abs(test$precio - pred))
  })
}

10.3 Dos modelos múltiples

  • Modelo M1: año del modelo + kilometraje + transmisión automática. Reúne los tres predictores del Punto 1 y muestra cuánto mejora combinarlos.
  • Modelo M2: M1 + región (variable categórica, referencia Antioquia). Pregunta si el lugar de publicación aporta información adicional.
f1 <- precio ~ modelo + kilometraje + auto
f2 <- precio ~ modelo + kilometraje + auto + region

m1 <- lm(f1, data = datos)
m2 <- lm(f2, data = datos)
summary(m1)
## 
## Call:
## lm(formula = f1, data = datos)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -36539843  -3961827   -789478   3125135  99377362 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -6.729e+09  5.923e+08 -11.362  < 2e-16 ***
## modelo       3.367e+06  2.930e+05  11.491  < 2e-16 ***
## kilometraje -8.694e+01  2.792e+01  -3.113  0.00203 ** 
## auto         5.092e+06  1.194e+06   4.264 2.71e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 9955000 on 294 degrees of freedom
## Multiple R-squared:  0.7659, Adjusted R-squared:  0.7636 
## F-statistic: 320.7 on 3 and 294 DF,  p-value: < 2.2e-16
summary(m2)
## 
## Call:
## lm(formula = f2, data = datos)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -38793897  -4199898   -534177   3314069  96450526 
## 
## Coefficients:
##                         Estimate Std. Error t value Pr(>|t|)    
## (Intercept)           -6.525e+09  5.975e+08 -10.920  < 2e-16 ***
## modelo                 3.266e+06  2.957e+05  11.047  < 2e-16 ***
## kilometraje           -9.417e+01  2.808e+01  -3.353 0.000904 ***
## auto                   4.614e+06  1.216e+06   3.794 0.000180 ***
## regionBogotá          -2.553e+06  1.809e+06  -1.412 0.159061    
## regionOtros           -1.198e+06  1.467e+06  -0.817 0.414762    
## regionValle del Cauca  2.823e+06  1.788e+06   1.579 0.115435    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 9866000 on 291 degrees of freedom
## Multiple R-squared:  0.7724, Adjusted R-squared:  0.7678 
## F-statistic: 164.6 on 6 and 291 DF,  p-value: < 2.2e-16
mae_m1 <- cv_mae(f1, datos, folds)
mae_m2 <- cv_mae(f2, datos, folds)
kable(data.frame(
  Modelo = c("M1: año + km + transmisión", "M2: M1 + región"),
  `R2 ajustado` = round(c(summary(m1)$adj.r.squared, summary(m2)$adj.r.squared), 3),
  AIC = round(c(AIC(m1), AIC(m2)), 1),
  `MAE (millones)` = round(c(mean(mae_m1), mean(mae_m2)) / 1e6, 2),
  check.names = FALSE), caption = "Modelos múltiples (MAE con validación cruzada)")
Modelos múltiples (MAE con validación cruzada)
Modelo R2 ajustado AIC MAE (millones)
M1: año + km + transmisión 0.764 10455.4 5.36
M2: M1 + región 0.768 10453.0 5.46

Los coeficientes se leen manteniendo lo demás constante. En M1, cada año más nuevo suma 3.366.985 pesos al precio (en el modelo simple eran 4.300.117), cada 1.000 km restan 86.936 pesos (antes 381.396) y un automático cuesta 5.091.845 pesos más que un mecánico (antes 12.913.102). Los coeficientes cambian respecto a los del modelo simple porque año, kilometraje y transmisión están relacionados entre sí; el año y el kilometraje, además, miden casi lo mismo (la edad del carro).

El R² ajustado es 0.764 en M1 y 0.768 en M2; se usa el ajustado porque descuenta las variables que no aportan. De los tres coeficientes de región, 0 son significativos al 5 %. Todos son asociaciones, no efectos causales.

10.4 Selección paso a paso con AIC

El step agrega o quita variables buscando el menor AIC (mejor equilibrio entre ajuste y simplicidad).

m2_step <- step(m2, direction = "both", trace = 0)
m2_step$anova      # pasos dados por el step (vacío si no quitó ni agregó nada)
##   Step Df Deviance Resid. Df   Resid. Dev      AIC
## 1      NA       NA       291 2.832639e+16 9605.272
formula(m2_step)
## precio ~ modelo + kilometraje + auto + region
# Evaluación honesta: el step se repite dentro de cada pliegue de entrenamiento
mae_m2_step <- cv_mae(f2, datos, folds, usar_step = TRUE)
mean(mae_m2_step) / 1e6
## [1] 5.475808

El primer step muestra qué variables entran o salen. En este caso no eliminó ninguna: el AIC indica que quitar variables no mejora el equilibrio entre ajuste y simplicidad. El segundo cálculo repite el step dentro de cada entrenamiento y es el que se usa para comparar modelos.

10.5 Supuestos del modelo múltiple

par(mfrow = c(2, 2))
plot(m2_step)

par(mfrow = c(1, 1))

bptest(m2_step)                     # varianza constante
## 
##  studentized Breusch-Pagan test
## 
## data:  m2_step
## BP = 15.039, df = 6, p-value = 0.01995
shapiro.test(residuals(m2_step))    # normalidad
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(m2_step)
## W = 0.64052, p-value < 2.2e-16
vif(m2_step)                        # multicolinealidad
##                 GVIF Df GVIF^(1/(2*Df))
## modelo      4.498734  1        2.121022
## kilometraje 4.394778  1        2.096373
## auto        1.115636  1        1.056236
## region      1.110441  3        1.017613
dwtest(m2_step)                     # independencia
## 
##  Durbin-Watson test
## 
## data:  m2_step
## DW = 1.4571, p-value = 5.327e-07
## alternative hypothesis: true autocorrelation is greater than 0
cook <- cooks.distance(m2_step)
umbral <- 4 / nrow(datos)
plot(cook, type = "h", main = "Distancia de Cook", ylab = "Distancia de Cook")
abline(h = umbral, col = "red")

sum(cook > umbral)                  # cuántos superan el umbral
## [1] 7
max(cook)                           # el más influyente
## [1] 0.3274549
datos[which(cook > umbral), c("precio", "modelo", "kilometraje", "auto")]
##        precio modelo kilometraje auto
## 29  119900000   2021       29000    1
## 120  28900000   2017       12345    0
## 259 159900000   2022           0    1
## 260 169800000   2019       36026    1
## 271  35000000   2007      148000    0
## 284  25800000   2019      129000    1
## 296 107500000   2021       17909    1

Qué implica cada supuesto si no se cumple:

Supuesto Qué mirar Consecuencia si falla
Linealidad Línea roja de “Residuos vs. ajustados” casi plana Se subestiman o sobrestiman sistemáticamente ciertos tipos de carro
Varianza constante Forma de embudo; p de Breusch-Pagan < 0,05 El error es mayor en carros caros; los intervalos de predicción quedan mal calibrados
Normalidad Puntos del Q-Q lejos de la diagonal; p de Shapiro < 0,05 Los intervalos y p-valores son aproximados; las predicciones puntuales cambian poco
Multicolinealidad VIF mayor a 5 (preocupa más allá de 10) Coeficientes inestables (año y km se “pisan”), pero la predicción casi no se afecta
Independencia Durbin-Watson cerca de 2 Aquí no es serie de tiempo; cada anuncio es de un vendedor distinto
Influyentes Puntos sobre la línea roja de Cook Unos pocos anuncios pueden mover los coeficientes; revisar si son errores o carros especiales

En el modelo elegido por el step, Breusch-Pagan da p = 0.02 (la varianza no es constante) y Shapiro-Wilk da p = 1.4^{-24} (los errores no son normales). El VIF más alto es 4.5, por debajo de 5, pero cercano si año y kilometraje se parecen mucho. Durbin-Watson vale 1.46 y 7 anuncios superan el umbral de Cook.

10.6 Transformación logarítmica de la respuesta

El precio es positivo y asimétrico, y el error crece con el precio. Con log(precio) los efectos pasan a ser porcentuales, lo que tiene sentido en carros: un año más de antigüedad resta un porcentaje del precio, no una cantidad fija de pesos.

f_log <- log(precio) ~ modelo + kilometraje + auto + region
m_log <- lm(f_log, data = datos)
summary(m_log)
## 
## Call:
## lm(formula = f_log, data = datos)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.85581 -0.05965 -0.00004  0.06346  0.89052 
## 
## Coefficients:
##                         Estimate Std. Error t value Pr(>|t|)    
## (Intercept)           -1.143e+02  7.976e+00 -14.325  < 2e-16 ***
## modelo                 6.552e-02  3.947e-03  16.601  < 2e-16 ***
## kilometraje           -1.483e-06  3.749e-07  -3.957 9.55e-05 ***
## auto                   6.047e-02  1.624e-02   3.724 0.000235 ***
## regionBogotá          -4.037e-02  2.414e-02  -1.672 0.095569 .  
## regionOtros           -9.021e-03  1.958e-02  -0.461 0.645360    
## regionValle del Cauca  2.161e-02  2.387e-02   0.905 0.366041    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1317 on 291 degrees of freedom
## Multiple R-squared:  0.8693, Adjusted R-squared:  0.8666 
## F-statistic: 322.5 on 6 and 291 DF,  p-value: < 2.2e-16
m_log_step <- step(m_log, trace = 0)
formula(m_log_step)
## log(precio) ~ modelo + kilometraje + auto
# MAE en PESOS (no en escala log), con step dentro de cada pliegue
mae_log_step <- cv_mae(f_log, datos, folds, usar_step = TRUE, log_y = TRUE)
mean(mae_log_step) / 1e6
## [1] 4.607655
# AIC comparable: se suma el jacobiano 2*sum(log(y)) para pasar a la escala original
aic_log_ajustado <- AIC(m_log) + 2 * sum(log(datos$precio))
c(AIC_original = AIC(m2), AIC_log_ajustado = aic_log_ajustado)
##     AIC_original AIC_log_ajustado 
##         10452.96         10186.50
par(mfrow = c(2, 2)); plot(m_log_step); par(mfrow = c(1, 1))

bptest(m_log_step)
## 
##  studentized Breusch-Pagan test
## 
## data:  m_log_step
## BP = 4.0617, df = 3, p-value = 0.2549
shapiro.test(residuals(m_log_step))
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(m_log_step)
## W = 0.80122, p-value < 2.2e-16

En el modelo con logaritmo, un coeficiente b equivale a un cambio de (exp(b) − 1) × 100 % en el precio:

round((exp(coef(m_log_step)) - 1) * 100, 2)   # % de cambio en el precio por cada unidad de la variable
## (Intercept)      modelo kilometraje        auto 
##     -100.00        6.90        0.00        6.72

El R² del modelo con logaritmo no se compara con el del modelo original porque están en escalas distintas; la comparación válida es el MAE en pesos y el AIC ajustado. La transformación se justifica si el MAE en pesos baja y los residuos mejoran.

10.7 Comparación de todos los modelos y elección del mejor

maes <- c(mean(mediana_mae), mean(mae_A), mean(mae_B), mean(mae_C),
          mean(mae_m1), mean(mae_m2), mean(mae_m2_step), mean(mae_log_step))
tabla_final <- data.frame(
  Modelo = c("Referencia: mediana", "A: año", "B: kilometraje", "C: transmisión",
             "M1: año + km + transmisión", "M2: M1 + región", "M2 con step",
             "M2 con log(precio) + step"),
  `MAE (millones)` = round(maes / 1e6, 2),
  `Mejora vs. mediana (%)` = round((1 - maes / maes[1]) * 100, 1),
  check.names = FALSE)
kable(tabla_final[order(tabla_final$`MAE (millones)`), ], row.names = FALSE,
      caption = "Todos los modelos con los mismos 10 pliegues")
Todos los modelos con los mismos 10 pliegues
Modelo MAE (millones) Mejora vs. mediana (%)
M2 con log(precio) + step 4.61 71.0
M1: año + km + transmisión 5.36 66.3
M2: M1 + región 5.46 65.7
M2 con step 5.48 65.6
A: año 5.62 64.7
B: kilometraje 8.04 49.5
C: transmisión 15.25 4.1
Referencia: mediana 15.91 0.0
ggplot(tabla_final, aes(x = reorder(Modelo, -`MAE (millones)`), y = `MAE (millones)`)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = `MAE (millones)`), hjust = -0.15) +
  coord_flip() + expand_limits(y = max(tabla_final$`MAE (millones)`) * 1.1) +
  labs(title = "Error promedio de cada modelo", x = NULL, y = "MAE (millones de pesos)")

Entre los modelos múltiples elijo el de menor MAE en pesos: el modelo con log(precio). Si un modelo mejora muy poco a otro más simple, conviene el simple por interpretabilidad; aquí lo comparo con el mejor modelo simple (A) en la tabla.

10.8 Dos escenarios hipotéticos con intervalos de predicción del 95 %

Se eligen carros realistas: combinaciones de año y kilometraje coherentes con lo que hay en la base (un carro de 2019 con 35.000 km es normal; uno de 2008 con 5.000 km no lo sería).

usa_log <- mean(mae_log_step) < mean(mae_m2_step)   # se elige el de menor MAE en pesos
mejor <- if (usa_log) m_log_step else m2_step

escenarios <- data.frame(
  modelo = c(2013, 2019),
  kilometraje = c(95000, 35000),
  auto = c(0, 1),
  region = factor(c("Valle del Cauca", "Antioquia"), levels = levels(datos$region)))

pred_esc <- predict(mejor, newdata = escenarios, interval = "prediction", level = 0.95)
if (usa_log) pred_esc <- exp(pred_esc)

kable(data.frame(
  Escenario = c("1: Mazda 2 modelo 2013, 95.000 km, mecánico, Valle del Cauca",
                "2: Mazda 2 modelo 2019, 35.000 km, automático, Antioquia"),
  `Precio estimado (millones)` = round(pred_esc[, "fit"] / 1e6, 1),
  `Límite inferior 95 %` = round(pred_esc[, "lwr"] / 1e6, 1),
  `Límite superior 95 %` = round(pred_esc[, "upr"] / 1e6, 1),
  check.names = FALSE), caption = "Predicciones con intervalo de predicción del 95 %")
Predicciones con intervalo de predicción del 95 %
Escenario Precio estimado (millones) Límite inferior 95 % Límite superior 95 %
1: Mazda 2 modelo 2013, 95.000 km, mecánico, Valle del Cauca 39.3 30.3 51.1
2: Mazda 2 modelo 2019, 35.000 km, automático, Antioquia 68.0 52.4 88.3

El intervalo de predicción indica entre qué valores caería el precio anunciado de un carro concreto con esas características, no el promedio de todos los carros; por eso es más ancho que un intervalo de confianza.

11 Punto 3. Análisis crítico

11.1 ¿Quién usaría el modelo y para qué decisión?

  • Vendedores particulares que van a publicar un Mazda 2 y quieren un precio de partida razonable.
  • Compradores que quieren saber si un anuncio está muy por encima o muy por debajo de lo normal antes de negociar.
  • Compraventas o concesionarios que necesitan una referencia rápida para revisar muchos carros.

En todos los casos el modelo es una referencia para negociar, no un avalúo oficial.

11.2 ¿Qué significa el error de predicción? ¿Es aceptable?

El modelo elegido (con log(precio)) se equivoca en promedio unos 4.6 millones de pesos, cerca del 11 % del precio mediano (42.7 millones). Es 71 % mejor que decir siempre “el precio típico” (la mediana).

Para un carro de 40 millones, equivale a un error de unos 4 o 5 millones. Es aceptable como orientación (saber si un precio es razonable o claramente alto), pero no lo es para fijar un precio de compra exacto: una diferencia de ese tamaño pesa en una negociación. Los intervalos de los escenarios lo confirman: tienen un ancho de 21 y 36 millones.

11.3 ¿Qué pasa si el modelo sobreestima o subestima?

Si el modelo… Vendedor Comprador
Sobreestima (dice más de lo real) Pone un precio muy alto, tarda más en vender o pierde interesados Cree que un anuncio caro es “normal” y paga de más
Subestima (dice menos de lo real) Regala parte del valor del carro Descarta un buen negocio o hace una oferta que el vendedor rechaza

Como el error es mayor en carros caros, el riesgo crece justo en las operaciones de más dinero.

11.4 Limitaciones y cuándo no usarlo

  • Precio anunciado, no precio de venta: el modelo aprende de lo que piden los vendedores, no de lo que se paga después de negociar.
  • Variables que faltan: estado mecánico, accidentes, versión, número de dueños, mantenimientos y accesorios influyen en el precio y no están en la base.
  • Año y kilometraje se parecen entre sí (correlación alta): los coeficientes individuales son inestables y no deben leerse como efectos separados.
  • Supuestos imperfectos: revisa lo que encontraste en varianza y normalidad. Si fallan, los intervalos son aproximados.
  • Muestra pequeña y de un solo portal: 298 anuncios de OLX en un momento dado. Los precios cambian con el tiempo, la inflación y el mercado.
  • No usar para carros fuera del rango de la base (modelos anteriores a 2007, kilometrajes extremos), para otras marcas o modelos, ni para avalúos legales, seguros o créditos.

11.5 ¿Recomiendo aplicarlo? ¿Qué información adicional necesitaría?

Recomendación: usarlo como herramienta de orientación, no como decisor. Sirve para detectar anuncios fuera de lo normal y para tener un punto de partida, siempre acompañado de la inspección del carro y de la negociación.

Información adicional que mejoraría el modelo: precios reales de venta (no solo anunciados), versión y equipamiento, estado mecánico y de carrocería, número de dueños y fecha del anuncio para ajustar por el tiempo.