En este informe se analiza la oferta de vivienda usada publicada en OLX para el barrio Pance de Cali. El objetivo es explicar el precio de venta (en millones de pesos) a partir de las características del inmueble, mediante modelos de regresión lineal simple y múltiple, validando los supuestos del modelo, evaluando la pertinencia de transformaciones logarítmicas, seleccionando variables con stepwise y midiendo la capacidad predictiva con el MAE.
Nota sobre el filtro: la base se filtra únicamente por barrio (Pance). No se filtra por tipo de vivienda, por lo que el barrio incluye casas y apartamentos; esto permite usar el tipo de vivienda como variable dummy en la modelación múltiple.
Paso 1. Cargar librerías
# Si alguna librería no está instalada: install.packages("nombre")
library(readxl)
library(dplyr)
library(ggplot2)
library(plotly)
library(knitr)
library(tidyr)
library(leaflet)
library(lmtest) # bptest (Breusch-Pagan)
Paso 2. Cargar la base y filtrar por el barrio Pance
El archivo Datos_Vivienda.xlsx debe estar en la misma
carpeta que este .Rmd.
datos <- read_excel("Datos_Vivienda.xlsx", na = c("", "NA"),
col_types = c("text", "text", "text",
"numeric", "numeric", "numeric",
"numeric", "numeric", "text", "text",
"numeric", "numeric"))
# Identificador único por fila
datos <- data.frame(id = 1:nrow(datos), datos)
# Filtro SOLO por barrio: se ignoran mayúsculas/minúsculas y espacios sobrantes
datos_sub <- datos %>%
filter(tolower(trimws(Barrio)) == "pance")
cat("Registros totales en la base:", nrow(datos), "\n")
## Registros totales en la base: 8319
cat("Registros del barrio Pance:", nrow(datos_sub), "\n")
## Registros del barrio Pance: 412
head(datos_sub)
| id | Zona | piso | Estrato | precio_millon | Area_contruida | parqueaderos | Banos | Habitaciones | Tipo | Barrio | cordenada_longitud | Cordenada_latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Zona Sur | 2 | 6 | 880 | 237 | 2 | 5 | 4 | Casa | pance | -76.46300 | 3.43000 |
| 5 | Zona Sur | 2 | 6 | 1300 | 600 | 4 | 7 | 5 | Casa | pance | -76.46438 | 3.43463 |
| 6 | Zona Sur | 3 | 6 | 513 | 160 | 2 | 4 | 4 | Casa | pance | -76.46438 | 3.43463 |
| 10 | Zona Sur | 6 | 6 | 690 | 150 | 2 | 5 | 4 | Apartamento | pance | -76.46478 | 3.42783 |
| 19 | Zona Sur | 3 | 6 | 550 | 136 | 2 | 4 | 4 | Apartamento | pance | -76.46554 | 3.42933 |
| 33 | Zona Sur | 1 | 6 | 900 | 330 | 2 | 4 | 4 | Casa | pance | -76.46809 | 3.41664 |
Criterio de mínimo 30 observaciones. Para que la regresión sea confiable, el barrio elegido debe tener al menos 30 ofertas. Se cuenta cuántas tiene cada barrio (unificando mayúsculas y espacios):
conteo_barrios <- datos %>%
mutate(Barrio_limpio = tolower(trimws(Barrio))) %>%
count(Barrio_limpio, name = "observaciones") %>%
arrange(desc(observaciones))
head(conteo_barrios, 5)
| Barrio_limpio | observaciones |
|---|---|
| valle del lili | 1009 |
| ciudad jardin | 540 |
| pance | 412 |
| la flora | 368 |
| santa teresita | 263 |
cat("Barrios con 30 o más observaciones:", sum(conteo_barrios$observaciones >= 30), "\n")
## Barrios con 30 o más observaciones: 59
cat("Observaciones de Pance:", conteo_barrios$observaciones[conteo_barrios$Barrio_limpio == "pance"], "\n")
## Observaciones de Pance: 412
Interpretación. Pance es el tercer barrio con más ofertas (412, después de Valle del Lili y Ciudad Jardín), muy por encima del mínimo de 30, y es uno de los 59 barrios que cumplen ese criterio. Por eso es adecuado para estimar los modelos.
str(datos_sub)
## 'data.frame': 412 obs. of 13 variables:
## $ id : int 1 5 6 10 19 33 35 39 44 46 ...
## $ Zona : chr "Zona Sur" "Zona Sur" "Zona Sur" "Zona Sur" ...
## $ piso : chr "2" "2" "3" "6" ...
## $ Estrato : chr "6" "6" "6" "6" ...
## $ precio_millon : num 880 1300 513 690 550 900 570 510 1200 550 ...
## $ Area_contruida : num 237 600 160 150 136 330 137 148 211 143 ...
## $ parqueaderos : num 2 4 2 2 2 2 2 2 2 3 ...
## $ Banos : num 5 7 4 5 4 4 5 3 3 3 ...
## $ Habitaciones : num 4 5 4 4 4 4 4 3 3 3 ...
## $ Tipo : chr "Casa" "Casa" "Casa" "Apartamento" ...
## $ Barrio : chr "pance" "pance" "pance" "pance" ...
## $ cordenada_longitud: num -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ Cordenada_latitud : num 3.43 3.43 3.43 3.43 3.43 ...
table(datos_sub$Tipo)
##
## Apartamento Casa
## 206 206
colSums(is.na(datos_sub))
## id Zona piso Estrato
## 0 0 130 0
## precio_millon Area_contruida parqueaderos Banos
## 0 0 12 0
## Habitaciones Tipo Barrio cordenada_longitud
## 0 0 0 0
## Cordenada_latitud
## 0
Observación sobre datos faltantes: piso tiene 130
valores faltantes (78 casas y 52 apartamentos), por eso no se usa como
variable explicativa. (En la base original esos faltantes venían
escritos como el texto “NA”; por eso la lectura del Excel usa
na = c("", "NA"), para que colSums(is.na())
los cuente bien.) parqueaderos tiene 12 faltantes; en los
modelos que la usan, lm() omite esas filas automáticamente
(de 412 ofertas quedan 400). Además, hay 2 registros con 0 baños y 2 con
0 habitaciones (3 filas en total, una tiene ambos en 0): pueden ser
errores de digitación, y son la razón por la que no se aplica logaritmo
a esas variables.
resumen <- data.frame(
cantidad_ofertas = nrow(datos_sub),
promedio_precio = mean(datos_sub$precio_millon, na.rm = TRUE),
promedio_area = mean(datos_sub$Area_contruida, na.rm = TRUE),
promedio_habitaciones = mean(datos_sub$Habitaciones, na.rm = TRUE),
promedio_banos = mean(datos_sub$Banos, na.rm = TRUE),
promedio_parqueaderos = mean(datos_sub$parqueaderos, na.rm = TRUE)
)
resumen
| cantidad_ofertas | promedio_precio | promedio_area | promedio_habitaciones | promedio_banos | promedio_parqueaderos |
|---|---|---|---|---|---|
| 412 | 884.3883 | 277.6757 | 3.696602 | 4.473301 | 3.01 |
indic_precio <- data.frame(
promedio = mean(datos_sub$precio_millon, na.rm = TRUE),
mediana = median(datos_sub$precio_millon, na.rm = TRUE),
minimo = min(datos_sub$precio_millon, na.rm = TRUE),
maximo = max(datos_sub$precio_millon, na.rm = TRUE),
desv_est = sd(datos_sub$precio_millon, na.rm = TRUE)
)
indic_precio
| promedio | mediana | minimo | maximo | desv_est |
|---|---|---|---|---|
| 884.3883 | 780 | 245 | 1900 | 389.766 |
datos_sub %>%
group_by(Tipo) %>%
summarise(n = n(),
precio_promedio = mean(precio_millon),
precio_mediana = median(precio_millon),
area_promedio = mean(Area_contruida))
| Tipo | n | precio_promedio | precio_mediana | area_promedio |
|---|---|---|---|---|
| Apartamento | 206 | 646.3058 | 600 | 153.3931 |
| Casa | 206 | 1122.4709 | 1125 | 401.9583 |
ggplotly(
ggplot(datos_sub, aes(x = precio_millon)) +
geom_histogram(binwidth = 100, fill = "steelblue", color = "black", alpha = 0.6) +
labs(title = "Figura 1. Histograma del precio de las viviendas en Pance",
x = "Precio (millones)", y = "Frecuencia") +
theme_minimal()
)
Interpretación Figura 1. La distribución del precio es asimétrica a la derecha: la mayoría de las ofertas está por debajo de 900 millones (mediana = 780; Q1 = 590) y hay una cola de inmuebles de lujo que llega a 1.900 millones. Como el promedio (884) es mayor que la mediana (780), esos precios altos “jalan” el promedio hacia arriba. También se nota un segundo grupo de ofertas alrededor de 1.200 a 1.350 millones (casas grandes). Esta asimetría anticipa que una transformación logarítmica del precio podría ayudar a estabilizar la varianza y a acercar los residuos a la normalidad.
ggplotly(
ggplot(datos_sub, aes(x = Area_contruida)) +
geom_histogram(binwidth = 50, fill = "green", color = "black", alpha = 0.6) +
labs(title = "Figura 2. Histograma del área construida en Pance",
x = "Área construida (m²)", y = "Frecuencia") +
theme_minimal()
)
Interpretación Figura 2. El área construida también es asimétrica a la derecha: la mediana es 199 m² (Q1 = 138 y Q3 = 350) y el promedio 278 m², más alto por la cola derecha. Hay 17 viviendas por encima de 650 m² (límite superior del boxplot del área), que son valores atípicos; la más grande tiene 1.500 m².
ggplotly(
ggplot(datos_sub, aes(x = Tipo, y = precio_millon, fill = Tipo)) +
geom_boxplot(outlier.colour = "red", outlier.size = 2) +
labs(title = "Figura 3. Precio por tipo de vivienda en Pance",
x = "Tipo de vivienda", y = "Precio (millones)") +
theme_minimal() + theme(legend.position = "none")
)
Interpretación Figura 3. Las casas son bastante más caras que los apartamentos: mediana de ~1.125 millones frente a 600 (promedio 1.122 vs 646). El 50% central de las casas está entre 850 y 1.350 millones, y el de los apartamentos entre 500 y ~708 millones: las casas, además, son más dispersas. Esta diferencia justifica incluir el tipo de vivienda como variable dummy más adelante.
ggplotly(
ggplot(datos_sub, aes(x = Tipo, y = Area_contruida, fill = Tipo)) +
geom_boxplot(outlier.colour = "red", outlier.size = 2) +
labs(title = "Figura 4. Área construida por tipo de vivienda en Pance",
x = "Tipo de vivienda", y = "Área construida (m²)") +
theme_minimal() + theme(legend.position = "none")
)
Interpretación Figura 4. Las casas son mucho más grandes que los apartamentos (mediana de ~348 m² frente a 138 m²). Los puntos rojos son atípicos dentro de cada tipo: 15 apartamentos (hasta 520 m²) y 14 casas (hasta 1.500 m²). Parte de la diferencia de precio entre casas y apartamentos se explica simplemente por el tamaño.
r_pa <- cor(datos_sub$precio_millon, datos_sub$Area_contruida, use = "complete.obs")
ggplotly(
ggplot(datos_sub, aes(x = Area_contruida, y = precio_millon, color = Tipo)) +
geom_point(alpha = 0.7, size = 2) +
geom_smooth(aes(group = 1), method = "loess", color = "grey30", se = TRUE) +
labs(title = paste0("Figura 5. Precio vs área construida (correlación = ", round(r_pa, 2), ")"),
x = "Área construida (m²)", y = "Precio (millones)") +
theme_minimal()
)
Interpretación Figura 5. La correlación entre precio y área es de 0.7, una relación positiva y fuerte (supera el ±0.5 recomendado): a mayor área, mayor precio. La curva suavizada muestra que la relación no es perfectamente lineal (el precio crece más lento a medida que el área aumenta) y que la dispersión crece con el tamaño, lo que sugiere explorar una transformación logarítmica. Una correlación alta no implica causalidad: la ubicación dentro del barrio, los acabados y la oferta también influyen.
vars_num <- datos_sub %>% select(precio_millon, Area_contruida, parqueaderos, Banos, Habitaciones)
round(cor(vars_num, use = "pairwise.complete.obs"), 2)
## precio_millon Area_contruida parqueaderos Banos Habitaciones
## precio_millon 1.00 0.70 0.63 0.57 0.43
## Area_contruida 0.70 1.00 0.54 0.48 0.42
## parqueaderos 0.63 0.54 1.00 0.45 0.37
## Banos 0.57 0.48 0.45 1.00 0.64
## Habitaciones 0.43 0.42 0.37 0.64 1.00
El precio se relaciona más fuertemente con el área construida (0.70), seguida de parqueaderos (0.63) y baños (0.57); las tres superan el ±0.5 recomendado. Habitaciones (0.43) queda por debajo de ese umbral. Todas las relaciones con el precio son positivas, como se esperaba. Baños y habitaciones están correlacionadas entre sí (0.64), lo que puede causar problemas de multicolinealidad en los modelos múltiples.
leaflet() %>%
addTiles() %>%
addCircleMarkers(lng = datos_sub$cordenada_longitud, lat = datos_sub$Cordenada_latitud,
radius = 2, color = "purple",
label = paste0("ID: ", datos_sub$id,
", Tipo: ", datos_sub$Tipo,
", Precio: ", datos_sub$precio_millon,
", Área: ", datos_sub$Area_contruida))
Para validar cada modelo se usa la siguiente función, que produce los gráficos de diagnóstico y las pruebas formales de los supuestos:
Con un nivel de significancia de 5%, se rechaza H0 si el valor p < 0.05.
validar <- function(modelo, titulo = "") {
op <- par(mfrow = c(2, 2))
plot(modelo) # Residuals vs Fitted, Normal Q-Q, Scale-Location y Residuals vs Leverage
par(op)
res <- data.frame(
Prueba = c("Shapiro-Wilk (normalidad)", "Breusch-Pagan (homocedasticidad)"),
Valor_p = c(shapiro.test(resid(modelo))$p.value,
bptest(modelo)$p.value)
)
res$Decision <- ifelse(res$Valor_p < 0.05, "Se rechaza H0", "No se rechaza H0")
res$Valor_p <- signif(res$Valor_p, 3)
res
}
# Extrae el valor p de una prueba para usarlo en el texto
pval <- function(tabla, i) tabla$Valor_p[i]
decide <- function(p) ifelse(p < 0.05, "se rechaza", "no se rechaza")
# Valor p con formato legible para el texto (ej. 7.24e-14)
fp <- function(tabla, i) formatC(tabla$Valor_p[i], format = "g", digits = 3)
Se plantean tres modelos simples, cada uno con una variable explicativa distinta: área construida, número de parqueaderos y número de baños. Para cada uno se estima el modelo en escala original y luego una versión logarítmica, y se compara cuál cumple mejor los supuestos.
Siguiendo la metodología, antes de estimar se valida que las variables estén relacionadas con el precio (matriz de correlaciones de la sección 2: área 0.70, parqueaderos 0.63 y baños 0.57, todas por encima de ±0.5) y se plantea el signo esperado de cada coeficiente:
| Variable | Signo esperado | Razón |
|---|---|---|
| Área construida | + | más metros, mayor precio |
| Parqueaderos | + | en Pance los parqueaderos son un atributo valorado |
| Baños | + | más comodidad, mayor precio |
| Habitaciones | + | más espacio, mayor precio |
| Casa (dummy) | + | las casas suelen ser más grandes y más caras que los apartamentos |
Después de estimar cada modelo se compara el signo obtenido con el esperado y se verifica la significancia del coeficiente (valor p < 0.05).
ms1 <- lm(precio_millon ~ Area_contruida, data = datos_sub)
summary(ms1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1034.71 -166.49 -59.46 103.04 938.22
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 509.48364 23.19634 21.96 <2e-16 ***
## Area_contruida 1.35015 0.06748 20.01 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 277.6 on 410 degrees of freedom
## Multiple R-squared: 0.4941, Adjusted R-squared: 0.4928
## F-statistic: 400.4 on 1 and 410 DF, p-value: < 2.2e-16
v1 <- validar(ms1, "(MS1)")
v1
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 0 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 0 | Se rechaza H0 |
Interpretación. La ecuación estimada es: precio = 509.48 + 1.35 · área. Por cada metro cuadrado adicional de área construida, el precio esperado aumenta en 1.35 millones de pesos. El R² es 0.494: el área explica aproximadamente ese porcentaje de la variabilidad del precio. El coeficiente es significativo (valor p = 1.22e-62) y su signo positivo coincide con el esperado.
Validación de supuestos. Normalidad: se rechaza H0 (valor p = 7.89e-15); homocedasticidad: se rechaza H0 (valor p = 1.02e-24). En los gráficos, los residuos contra los ajustados no son planos: se abren como un abanico (la dispersión crece con el valor ajustado) y el QQ-plot se separa de la recta en las colas. El modelo en escala original no cumple normalidad ni varianza constante, lo que motiva probar una transformación.
ms1_log <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub)
summary(ms1_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.82274 -0.15856 0.01006 0.13280 0.62845
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.34073 0.10044 33.26 <2e-16 ***
## log(Area_contruida) 0.61698 0.01838 33.57 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2228 on 410 degrees of freedom
## Multiple R-squared: 0.7332, Adjusted R-squared: 0.7326
## F-statistic: 1127 on 1 and 410 DF, p-value: < 2.2e-16
v1l <- validar(ms1_log, "(MS1 log-log)")
v1l
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 0.000824 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 0.000000 | Se rechaza H0 |
Interpretación. En el modelo log-log el coeficiente es una elasticidad: un aumento de 1% en el área construida se asocia con un aumento de aproximadamente 0.617% en el precio. El R² pasa de 0.494 (escala original) a 0.733.
Pertinencia de la transformación. Con el log-log, Breusch-Pagan pasa de p = 1.02e-24 a p = 2.69e-13 y Shapiro-Wilk de p = 7.89e-15 a p = 0.000824: ambos valores p suben (mejoran) y el R² pasa de 0.494 a 0.733, pero las dos pruebas siguen rechazando H0. Conclusión: la transformación sí es pertinente (mejora claramente el ajuste y los supuestos) aunque no los resuelve del todo; falta incluir más variables, lo que se hace en la modelación múltiple. Nota: el R² de modelos con variable respuesta distinta (precio vs log precio) no es directamente comparable; por eso la comparación se apoya en los supuestos y en el error de predicción (MAE, sección 4.6).
Se comparan además las cuatro formas funcionales posibles para el área (lin-lin, log-lin, lin-log y log-log):
ms1_loglin <- lm(log(precio_millon) ~ Area_contruida, data = datos_sub)
ms1_linlog <- lm(precio_millon ~ log(Area_contruida), data = datos_sub)
formas <- list("Lin-lin" = ms1, "Log-lin" = ms1_loglin, "Lin-log" = ms1_linlog, "Log-log" = ms1_log)
data.frame(
Forma = names(formas),
R2 = round(sapply(formas, function(m) summary(m)$r.squared), 3),
p_Shapiro = signif(sapply(formas, function(m) shapiro.test(resid(m))$p.value), 3),
p_BreuschPagan = signif(sapply(formas, function(m) bptest(m)$p.value), 3),
row.names = NULL
)
| Forma | R2 | p_Shapiro | p_BreuschPagan |
|---|---|---|---|
| Lin-lin | 0.494 | 0.00e+00 | 0 |
| Log-lin | 0.491 | 4.41e-05 | 0 |
| Lin-log | 0.674 | 0.00e+00 | 0 |
| Log-log | 0.733 | 8.24e-04 | 0 |
Interpretación. Transformar solo la variable respuesta (log-lin) casi no cambia el ajuste (R² 0.491 vs 0.494). Transformar el área (lin-log) sube el R² a 0.674, y transformar ambas variables (log-log) es la mejor opción: R² = 0.733 y los valores p más altos en Shapiro-Wilk y Breusch-Pagan entre las cuatro formas. Por eso la forma log-log es la elegida.
ms2 <- lm(precio_millon ~ parqueaderos, data = datos_sub)
summary(ms2)
##
## Call:
## lm(formula = precio_millon ~ parqueaderos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -862.69 -190.31 -80.31 151.60 1189.69
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 372.21 34.73 10.72 <2e-16 ***
## parqueaderos 169.05 10.39 16.27 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 302.5 on 398 degrees of freedom
## (12 observations deleted due to missingness)
## Multiple R-squared: 0.3996, Adjusted R-squared: 0.398
## F-statistic: 264.8 on 1 and 398 DF, p-value: < 2.2e-16
v2 <- validar(ms2, "(MS2)")
v2
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 0.00000 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 0.00742 | Se rechaza H0 |
Interpretación. Cada parqueadero adicional se asocia con un aumento promedio de 169.05 millones en el precio. El modelo explica el 40% de la variabilidad del precio. En los supuestos: normalidad se rechaza H0 (p = 7.24e-14), homocedasticidad se rechaza H0 (p = 0.00742). El coeficiente es significativo (valor p = 5.19e-46) y su signo positivo coincide con el esperado.
parqueaderos toma valores desde 1, por lo que se puede
aplicar log a ambas variables (log-log).
ms2_log <- lm(log(precio_millon) ~ log(parqueaderos), data = datos_sub)
summary(ms2_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(parqueaderos), data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.9805 -0.1949 -0.0382 0.1903 1.2209
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 6.02334 0.04176 144.23 <2e-16 ***
## log(parqueaderos) 0.66135 0.03825 17.29 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.3258 on 398 degrees of freedom
## (12 observations deleted due to missingness)
## Multiple R-squared: 0.4289, Adjusted R-squared: 0.4275
## F-statistic: 299 on 1 and 398 DF, p-value: < 2.2e-16
v2l <- validar(ms2_log, "(MS2 log-log)")
v2l
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 2.40e-06 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 9.61e-04 | Se rechaza H0 |
Interpretación. Un aumento de 1% en el número de parqueaderos se asocia con un cambio de aproximadamente 0.661% en el precio (elasticidad). El R² pasa de 0.4 a 0.429. Breusch-Pagan pasa de p = 0.00742 a p = 0.000961 (el valor p baja, es decir, la homocedasticidad no mejora; ambos modelos la rechazan) y Shapiro-Wilk pasa de p = 7.24e-14 a p = 2.36e-06 (mejora, pero sigue rechazando). Conclusión: la transformación no es claramente pertinente para parqueaderos: mejora un poco la normalidad y el R², pero no corrige la varianza no constante. Además, el número de parqueaderos es una variable discreta con pocos valores, por lo que los residuos siempre muestran “escalones”.
ms3 <- lm(precio_millon ~ Banos, data = datos_sub)
summary(ms3)
##
## Call:
## lm(formula = precio_millon ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -613.1 -218.6 -90.0 138.5 1127.7
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 72.32 60.27 1.20 0.231
## Banos 181.54 13.00 13.96 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 321.2 on 410 degrees of freedom
## Multiple R-squared: 0.3223, Adjusted R-squared: 0.3207
## F-statistic: 195 on 1 and 410 DF, p-value: < 2.2e-16
v3 <- validar(ms3, "(MS3)")
v3
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 0.000 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 0.567 | No se rechaza H0 |
Interpretación. Cada baño adicional se asocia con un aumento promedio de 181.54 millones en el precio, con un R² de 0.322. Supuestos: normalidad se rechaza H0 (p = 2.08e-14), homocedasticidad no se rechaza H0 (p = 0.567). El coeficiente es significativo (valor p = 1.57e-36) y su signo positivo coincide con el esperado.
La base tiene registros con 0 baños, y log(0) no está
definido; por eso solo se transforma la variable respuesta (modelo
log-lineal: log(precio) ~ baños).
ms3_log <- lm(log(precio_millon) ~ Banos, data = datos_sub)
summary(ms3_log)
##
## Call:
## lm(formula = log(precio_millon) ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.82930 -0.24398 -0.04321 0.21452 1.38347
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.70660 0.06329 90.17 <2e-16 ***
## Banos 0.22032 0.01365 16.14 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.3374 on 410 degrees of freedom
## Multiple R-squared: 0.3885, Adjusted R-squared: 0.387
## F-statistic: 260.5 on 1 and 410 DF, p-value: < 2.2e-16
v3l <- validar(ms3_log, "(MS3 log-lineal)")
v3l
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 0.000000 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 0.000542 | Se rechaza H0 |
Interpretación. En un modelo log-lineal, cada baño adicional se asocia con un aumento porcentual aproximado de 24.6% en el precio. El R² pasa de 0.322 a 0.388. Pero Breusch-Pagan pasa de p = 0.567 a p = 0.000542: el modelo en escala original sí cumplía homocedasticidad y el log-lineal deja de cumplirla. Shapiro-Wilk mejora (de p = 2.08e-14 a p = 2.8e-08) pero sigue rechazando. Conclusión: para baños la transformación no es pertinente por supuestos: solo mejora el R² y la normalidad parcialmente, y empeora la varianza constante. Si se prioriza cumplir los supuestos, es preferible el modelo en escala original.
data.frame(
Modelo = c("MS1: precio ~ área", "MS1 log-log", "MS2: precio ~ parqueaderos", "MS2 log-log",
"MS3: precio ~ baños", "MS3 log-lineal"),
R2 = round(c(summary(ms1)$r.squared, summary(ms1_log)$r.squared,
summary(ms2)$r.squared, summary(ms2_log)$r.squared,
summary(ms3)$r.squared, summary(ms3_log)$r.squared), 3),
p_Shapiro = c(pval(v1,1), pval(v1l,1), pval(v2,1), pval(v2l,1), pval(v3,1), pval(v3l,1)),
p_BreuschPagan = c(pval(v1,2), pval(v1l,2), pval(v2,2), pval(v2l,2), pval(v3,2), pval(v3l,2))
)
| Modelo | R2 | p_Shapiro | p_BreuschPagan |
|---|---|---|---|
| MS1: precio ~ área | 0.494 | 0.00e+00 | 0.000000 |
| MS1 log-log | 0.733 | 8.24e-04 | 0.000000 |
| MS2: precio ~ parqueaderos | 0.400 | 0.00e+00 | 0.007420 |
| MS2 log-log | 0.429 | 2.40e-06 | 0.000961 |
| MS3: precio ~ baños | 0.322 | 0.00e+00 | 0.567000 |
| MS3 log-lineal | 0.388 | 0.00e+00 | 0.000542 |
Conclusión de la modelación simple. El área construida es la variable que más explica el precio (R² de 0.49 en escala original y 0.73 en log-log), seguida de parqueaderos (0.40) y baños (0.32). La transformación logarítmica es pertinente solo para el área, donde mejora el ajuste y los valores p de los supuestos. Para parqueaderos no corrige la heterocedasticidad y para baños la empeora (el modelo original sí la cumplía). En ningún modelo simple se cumple la normalidad de los residuos (con n ≈ 400 es esperable que Shapiro-Wilk sea estricto). Los modelos simples dejan una parte importante de la variabilidad sin explicar, lo que justifica pasar a modelos múltiples.
Se crea una variable dummy para el tipo de vivienda:
casa = 1 si es casa y 0 si es apartamento
(categoría de referencia).
datos_sub <- datos_sub %>%
mutate(casa = ifelse(Tipo == "Casa", 1, 0))
table(datos_sub$Tipo, datos_sub$casa)
##
## 0 1
## Apartamento 206 0
## Casa 0 206
# Base para la modelación múltiple: casos completos en las variables a usar
datos_mod <- datos_sub %>%
select(id, precio_millon, Area_contruida, Banos, Habitaciones, parqueaderos, casa) %>%
na.omit()
cat("Observaciones usadas en la modelación múltiple:", nrow(datos_mod), "\n")
## Observaciones usadas en la modelación múltiple: 400
precio = β0 + β1·área + β2·baños + β3·habitaciones + β4·parqueaderos + β5·casa + ε
mm1 <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos + casa,
data = datos_mod)
summary(mm1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones +
## parqueaderos + casa, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -803.81 -125.30 -38.79 70.71 943.20
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 120.6271 59.2991 2.034 0.04260 *
## Area_contruida 0.6929 0.0794 8.727 < 2e-16 ***
## Banos 83.5910 14.3011 5.845 1.06e-08 ***
## Habitaciones -20.6435 18.3494 -1.125 0.26126
## parqueaderos 70.9328 10.1308 7.002 1.10e-11 ***
## casa 109.2961 33.6395 3.249 0.00126 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 235.2 on 394 degrees of freedom
## Multiple R-squared: 0.6405, Adjusted R-squared: 0.636
## F-statistic: 140.4 on 5 and 394 DF, p-value: < 2.2e-16
Interpretación (manteniendo constantes las demás variables):
Con un nivel de 5%, son significativos el área, los baños, los
parqueaderos y la dummy casa; habitaciones no lo es. Los
signos coinciden con los esperados, salvo habitaciones (negativo).
vm1 <- validar(mm1, "(MM1)")
vm1
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 0e+00 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 3e-07 | Se rechaza H0 |
Interpretación. Normalidad: se rechaza H0 (p = 9.61e-15). Homocedasticidad: se rechaza H0 (p = 3.35e-07). Es decir, MM1 no cumple normalidad ni varianza constante.
Dada la asimetría del precio y la forma de la relación con el área, se estima el modelo con log(precio) y log(área). Las demás variables (baños, habitaciones, parqueaderos y dummy) se dejan en su escala original, porque baños y habitaciones tienen valores en cero.
log(precio) = β0 + β1·log(área) + β2·baños + β3·habitaciones + β4·parqueaderos + β5·casa + ε
mm2 <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + parqueaderos + casa,
data = datos_mod)
summary(mm2)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos +
## Habitaciones + parqueaderos + casa, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.78503 -0.14089 -0.01421 0.11732 0.65937
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.629274 0.140160 25.894 < 2e-16 ***
## log(Area_contruida) 0.498682 0.031300 15.932 < 2e-16 ***
## Banos 0.080083 0.012979 6.170 1.69e-09 ***
## Habitaciones -0.032145 0.016206 -1.983 0.048 *
## parqueaderos 0.042455 0.009167 4.631 4.94e-06 ***
## casa -0.035660 0.032571 -1.095 0.274
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2074 on 394 degrees of freedom
## Multiple R-squared: 0.771, Adjusted R-squared: 0.7681
## F-statistic: 265.3 on 5 and 394 DF, p-value: < 2.2e-16
Interpretación:
Con un nivel de 5%, son significativos el log del área, los baños y
los parqueaderos; habitaciones apenas lo es (p ≈ 0.048) y con signo
contrario al esperado; la dummy casa no es
significativa.
vm2 <- validar(mm2, "(MM2)")
vm2
| Prueba | Valor_p | Decision | |
|---|---|---|---|
| Shapiro-Wilk (normalidad) | 4.2e-06 | Se rechaza H0 | |
| BP | Breusch-Pagan (homocedasticidad) | 0.0e+00 | Se rechaza H0 |
Interpretación. Normalidad: se rechaza H0 (p = 4.21e-06); homocedasticidad: se rechaza H0 (p = 1.07e-12).
data.frame(
Modelo = c("MM1 (escala original)", "MM2 (log)"),
p_Shapiro = c(pval(vm1, 1), pval(vm2, 1)),
p_BreuschPagan = c(pval(vm1, 2), pval(vm2, 2)),
R2_ajustado = round(c(summary(mm1)$adj.r.squared, summary(mm2)$adj.r.squared), 3)
)
| Modelo | p_Shapiro | p_BreuschPagan | R2_ajustado |
|---|---|---|---|
| MM1 (escala original) | 0.0e+00 | 3e-07 | 0.636 |
| MM2 (log) | 4.2e-06 | 0e+00 | 0.768 |
Interpretación. Al pasar a logaritmos, el R² ajustado sube de 0.636 a 0.768 y la normalidad mejora (Shapiro-Wilk pasa de p = 9.61e-15 a p = 4.21e-06), aunque sigue rechazándose. En cambio, el valor p de Breusch-Pagan baja (de 3.35e-07 a 1.07e-12): con ese criterio la transformación no corrige la varianza no constante, y ambos modelos la incumplen. Conclusión: la transformación es pertinente por el mejor ajuste, la mejor normalidad y el menor error de predicción (MAE, sección 4.6), pero no resuelve la heterocedasticidad.
Se usa step() con criterio AIC y dirección both
(adelante y atrás), partiendo del modelo completo.
cat("===== MM1 (escala original) =====\n")
## ===== MM1 (escala original) =====
mm1_step <- step(mm1, direction = "both", trace = 1)
## Start: AIC=4374.39
## precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos +
## casa
##
## Df Sum of Sq RSS AIC
## - Habitaciones 1 70031 21870320 4373.7
## <none> 21800289 4374.4
## - casa 1 584085 22384374 4383.0
## - Banos 1 1890370 23690660 4405.7
## - parqueaderos 1 2712526 24512816 4419.3
## - Area_contruida 1 4214055 26014344 4443.1
##
## Step: AIC=4373.67
## precio_millon ~ Area_contruida + Banos + parqueaderos + casa
##
## Df Sum of Sq RSS AIC
## <none> 21870320 4373.7
## + Habitaciones 1 70031 21800289 4374.4
## - casa 1 516535 22386856 4381.0
## - Banos 1 2044119 23914439 4407.4
## - parqueaderos 1 2718521 24588841 4418.5
## - Area_contruida 1 4227455 26097775 4442.4
summary(mm1_step)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + parqueaderos +
## casa, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -801.55 -126.06 -43.43 64.26 948.42
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 84.59948 49.92517 1.695 0.09095 .
## Area_contruida 0.69396 0.07942 8.738 < 2e-16 ***
## Banos 75.67502 12.45456 6.076 2.90e-09 ***
## parqueaderos 71.00954 10.13396 7.007 1.06e-11 ***
## casa 98.55709 32.26761 3.054 0.00241 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 235.3 on 395 degrees of freedom
## Multiple R-squared: 0.6394, Adjusted R-squared: 0.6357
## F-statistic: 175.1 on 4 and 395 DF, p-value: < 2.2e-16
cat("===== MM2 (logarítmico) =====\n")
## ===== MM2 (logarítmico) =====
mm2_step <- step(mm2, direction = "both", trace = 1)
## Start: AIC=-1252.7
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones +
## parqueaderos + casa
##
## Df Sum of Sq RSS AIC
## - casa 1 0.0515 16.992 -1253.5
## <none> 16.941 -1252.7
## - Habitaciones 1 0.1692 17.110 -1250.7
## - parqueaderos 1 0.9223 17.863 -1233.5
## - Banos 1 1.6370 18.578 -1217.8
## - log(Area_contruida) 1 10.9139 27.855 -1055.8
##
## Step: AIC=-1253.48
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones +
## parqueaderos
##
## Df Sum of Sq RSS AIC
## <none> 16.992 -1253.5
## + casa 1 0.0515 16.941 -1252.7
## - Habitaciones 1 0.2236 17.216 -1250.2
## - parqueaderos 1 0.8956 17.888 -1234.9
## - Banos 1 1.6469 18.639 -1218.5
## - log(Area_contruida) 1 14.2102 31.202 -1012.4
summary(mm2_step)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos +
## Habitaciones + parqueaderos, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.77249 -0.14816 -0.01079 0.11296 0.67897
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.726836 0.108213 34.440 < 2e-16 ***
## log(Area_contruida) 0.480307 0.026427 18.175 < 2e-16 ***
## Banos 0.080314 0.012980 6.187 1.53e-09 ***
## Habitaciones -0.036051 0.015813 -2.280 0.0231 *
## parqueaderos 0.041728 0.009145 4.563 6.74e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2074 on 395 degrees of freedom
## Multiple R-squared: 0.7703, Adjusted R-squared: 0.768
## F-statistic: 331.2 on 4 and 395 DF, p-value: < 2.2e-16
formula(mm1_step)
## precio_millon ~ Area_contruida + Banos + parqueaderos + casa
formula(mm2_step)
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones +
## parqueaderos
data.frame(
Modelo = c("MM1 completo", "MM1 step", "MM2 completo", "MM2 step"),
AIC = round(c(AIC(mm1), AIC(mm1_step), AIC(mm2), AIC(mm2_step)), 1),
R2_ajustado = round(c(summary(mm1)$adj.r.squared, summary(mm1_step)$adj.r.squared,
summary(mm2)$adj.r.squared, summary(mm2_step)$adj.r.squared), 3)
)
| Modelo | AIC | R2_ajustado |
|---|---|---|
| MM1 completo | 5511.5 | 0.636 |
| MM1 step | 5510.8 | 0.636 |
| MM2 completo | -115.5 | 0.768 |
| MM2 step | -116.3 | 0.768 |
Interpretación. El procedimiento elimina las variables que no mejoran
el AIC. En el modelo en escala original (MM1) conserva Area_contruida,
Banos, parqueaderos, casa: elimina Habitaciones y mantiene
la dummy casa. En el modelo logarítmico (MM2) conserva
log(Area_contruida), Banos, Habitaciones, parqueaderos: elimina la dummy
casa y mantiene Habitaciones. La mejora del
AIC es pequeña en ambos casos (el AIC baja menos de 1 punto), es decir,
los modelos completos y los reducidos son casi equivalentes; el
step sirve sobre todo para simplificar. Los AIC de MM1 y MM2 no
son comparables entre sí porque tienen distinta variable respuesta; sí
lo son los AIC dentro de cada modelo (completo vs. step).
predict)Se predice el precio de una vivienda hipotética en Pance: 250 m² de área construida, 4 baños, 4 habitaciones, 3 parqueaderos, considerando los dos tipos de vivienda.
nuevas <- data.frame(
Area_contruida = c(250, 250),
Banos = c(4, 4),
Habitaciones = c(4, 4),
parqueaderos = c(3, 3),
casa = c(1, 0)
)
nuevas$Tipo <- c("Casa", "Apartamento")
# Modelo 1 (escala original): la predicción ya está en millones
p1 <- predict(mm1_step, newdata = nuevas, interval = "prediction", level = 0.95)
# Modelo 2 (log): se devuelve a millones con exp()
p2 <- predict(mm2_step, newdata = nuevas, interval = "prediction", level = 0.95)
tabla_pred <- data.frame(
Tipo = nuevas$Tipo,
MM1_step_ajuste = round(p1[, "fit"], 1),
MM1_step_inf = round(p1[, "lwr"], 1),
MM1_step_sup = round(p1[, "upr"], 1),
MM2_step_ajuste = round(exp(p2[, "fit"]), 1),
MM2_step_inf = round(exp(p2[, "lwr"]), 1),
MM2_step_sup = round(exp(p2[, "upr"]), 1)
)
tabla_pred
| Tipo | MM1_step_ajuste | MM1_step_inf | MM1_step_sup | MM2_step_ajuste | MM2_step_inf | MM2_step_sup |
|---|---|---|---|---|---|---|
| Casa | 872.4 | 407.6 | 1337.2 | 797.2 | 529.7 | 1199.7 |
| Apartamento | 773.8 | 309.7 | 1237.9 | 797.2 | 529.7 | 1199.7 |
Interpretación. Para una vivienda de 250 m², 4 baños, 4 habitaciones
y 3 parqueaderos: el modelo MM1 step (que conserva la dummy) predice
872.4 millones si es casa y 773.8 millones si es apartamento (la
diferencia es el coeficiente de la dummy), con intervalos de predicción
al 95% de 407.6 a 1337.2 (casa) y de 309.7 a 1237.9 (apartamento). El
modelo MM2 step predice 797.2 millones (intervalo de 529.7 a 1199.7),
igual para casa y apartamento, porque el step eliminó la dummy
de ese modelo y predict ignora la columna
casa. Los intervalos de predicción son amplios (unos 930
millones de ancho en MM1), reflejando que el error típico del modelo es
de unos 235 millones: sirven para el rango de una oferta individual, no
del precio promedio. Los del modelo logarítmico son asimétricos (más
amplios hacia arriba), coherente con la naturaleza del precio. Los
valores centrales (entre ~770 y ~870 millones) son razonables frente a
la mediana del barrio (780 millones).
El error absoluto medio (MAE) es el promedio de los errores en valor absoluto, en las mismas unidades del precio (millones de pesos):
\[MAE = \frac{1}{n}\sum_{i=1}^{n} |y_i - \hat{y}_i|\]
Se calcula dentro de la muestra con los modelos finales del step (la predicción del modelo logarítmico se devuelve a millones con exp()).
mae <- function(real, pred) mean(abs(real - pred))
# (a) Dentro de la muestra (se devuelve el modelo log a millones)
mae_tabla <- data.frame(
Modelo = c("MM1 step (escala original)", "MM2 step (log)"),
MAE_muestra = round(c(
mae(datos_mod$precio_millon, predict(mm1_step, datos_mod)),
mae(datos_mod$precio_millon, exp(predict(mm2_step, datos_mod)))
), 2)
)
mae_tabla
| Modelo | MAE_muestra |
|---|---|
| MM1 step (escala original) | 164.39 |
| MM2 step (log) | 154.96 |
Interpretación del MAE. El MAE indica que, en promedio, la predicción se desvía del precio real en la cantidad de millones de pesos de la tabla: 164.39 millones en MM1 step y 154.96 millones en MM2 step. Frente al precio promedio de las ofertas usadas en los modelos (881 millones), eso es un error de ≈ 18.7% y ≈ 17.6%. El modelo con menor MAE es MM2 step (log), pero la diferencia es pequeña (9.4 millones), así que ambos predicen de forma parecida. La comparación entre MM1 y MM2 es válida porque ambos errores se miden en millones de pesos.
Habitaciones en MM1 y casa en MM2), con
mejoras muy pequeñas del AIC.Limitaciones: son precios de oferta (no de cierre), la muestra proviene de una sola plataforma, hay valores atípicos (viviendas muy grandes) y registros con 0 baños o 0 habitaciones que pueden ser errores de digitación.
piso porque tiene 130 valores faltantes (78
casas y 52 apartamentos).Banos y Habitaciones tienen 2 registros
con valor 0 cada una (3 filas en total), así que a esas variables no se
les aplicó logaritmo (solo al precio y al área).