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.
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" ...
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.
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]), ]
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:
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.
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")
| 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.
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))
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")
| precio | modelo | kilometraje | |
|---|---|---|---|
| precio | 1.00 | 0.86 | -0.80 |
| modelo | 0.86 | 1.00 | -0.87 |
| kilometraje | -0.80 | -0.87 | 1.00 |
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)")
| 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.
Cada modelo usa un solo predictor:
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")
| 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 |
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")
| 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)
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.