Pregunta: ¿cómo se relaciona el precio de oferta con el área, la zona y el tipo de vivienda? Los datos provienen de anuncios de OLX recolectados mediante web scraping. Son precios anunciados, no precios finales de venta. Las relaciones estimadas son asociaciones: no prueban cuánto valdría una remodelación.
El precio está expresado en millones de pesos y área en m². Coloque
el Excel junto al .Rmd. Si tiene otro nombre o ubicación,
modifique archivo en el encabezado. No reinstale paquetes
que ya funcionan.
archivo <- params$archivo
if (!file.exists(archivo)) stop(
"No se encuentra: ", archivo,
". Coloque el Excel junto al Rmd o cambie params: archivo en el encabezado.")
Datos_Vivienda <- as.data.frame(readxl::read_excel(archivo, sheet = 1))
variables <- c("precio_millon", "Area_contruida", "Zona", "Tipo")
stopifnot(all(variables %in% names(Datos_Vivienda)))
for (v in c("Zona", "Tipo")) {
Datos_Vivienda[[v]] <- trimws(gsub("[[:space:]]+", " ", Datos_Vivienda[[v]]))
}
# Usamos las mismas filas en todos los modelos.
validas <- complete.cases(Datos_Vivienda[variables]) &
is.finite(Datos_Vivienda$precio_millon) &
is.finite(Datos_Vivienda$Area_contruida) &
Datos_Vivienda$precio_millon > 0 & Datos_Vivienda$Area_contruida > 0
tabla(data.frame(Filas = nrow(Datos_Vivienda), Excluidas = sum(!validas),
Repeticiones_exactas = sum(duplicated(Datos_Vivienda))))
| Filas | Excluidas | Repeticiones_exactas |
|---|---|---|
| 8319 | 0 | 57 |
Datos_Vivienda <- Datos_Vivienda[validas, ]
Datos_Vivienda$Zona <- relevel(factor(Datos_Vivienda$Zona), ref = "Zona Centro")
Datos_Vivienda$Tipo <- relevel(factor(Datos_Vivienda$Tipo), ref = "Apartamento")
Datos_Vivienda$area_c <- Datos_Vivienda$Area_contruida - 100
tabla(summary(Datos_Vivienda[c("precio_millon", "Area_contruida")]))
| precio_millon | Area_contruida | |
|---|---|---|
| Min. : 58 | Min. : 30 | |
| 1st Qu.: 220 | 1st Qu.: 80 | |
| Median : 330 | Median : 123 | |
| Mean : 434 | Mean : 175 | |
| 3rd Qu.: 540 | 3rd Qu.: 229 | |
| Max. :1999 | Max. :1745 |
Se conservan las repeticiones para mantener la muestra del ejemplo
original. Conviene verificar si son anuncios duplicados antes de un
análisis definitivo. No convertimos los faltantes en cero.
area_c es el área menos 100: conserva la unidad m² y
facilita interpretar el intercepto para una vivienda de 100 m².
\[ Precio_i=\beta_0+\beta_1 Area_i+\varepsilon_i. \]
mod1 <- lm(precio_millon ~ Area_contruida, data = Datos_Vivienda)
coeficientes(mod1)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 157.48 | 4.136 | 38.07 | 0 |
| Area_contruida | 1.58 | 0.018 | 86.30 | 0 |
plot(precio_millon ~ Area_contruida, data = Datos_Vivienda,
pch = 16, col = adjustcolor("steelblue", .2),
xlab = "Área construida (m²)", ylab = "Precio de oferta (millones)")
abline(mod1, col = "firebrick", lwd = 2)
Interpretación: un m² adicional se asocia con 1.58 millones adicionales en el precio medio anunciado. Por 10 m², la diferencia estimada es diez veces esa cantidad. Todavía no controlamos ubicación, tipo ni otras características.
El intercepto predice el precio con área cero: no tiene una interpretación inmobiliaria útil aquí. Un residuo es precio observado menos predicho. Un residuo positivo no demuestra que una vivienda esté sobrevalorada.
R² = 0.472: proporción de variación del precio explicada dentro de esta muestra. No es el porcentaje de predicciones correctas. El valor p de la pendiente contrasta que sea cero; no mide su importancia práctica.
table(Datos_Vivienda$Zona)
##
## Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## 124 1920 1198 351 4726
mod2 <- lm(precio_millon ~ Zona, data = Datos_Vivienda)
coeficientes(mod2)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 309.69 | 27.78 | 11.149 | 0.000 |
| ZonaZona Norte | 35.91 | 28.66 | 1.253 | 0.210 |
| ZonaZona Oeste | 367.89 | 29.18 | 12.608 | 0.000 |
| ZonaZona Oriente | -81.16 | 32.31 | -2.512 | 0.012 |
| ZonaZona Sur | 116.83 | 28.14 | 4.152 | 0.000 |
tabla(aggregate(precio_millon ~ Zona, data = Datos_Vivienda, FUN = mean))
| Zona | precio_millon |
|---|---|
| Zona Centro | 309.7 |
| Zona Norte | 345.6 |
| Zona Oeste | 677.6 |
| Zona Oriente | 228.5 |
| Zona Sur | 426.5 |
Referencia: Zona Centro. El intercepto es su precio medio, y cada coeficiente es la diferencia entre la zona correspondiente y Centro. Por ejemplo, un coeficiente de −80 significaría 80 millones menos en promedio; es un ejemplo de lectura, no una cifra que deba copiarse como resultado.
Con cinco zonas aparecen cuatro dummies más el intercepto. La zona base no desaparece de los datos: está representada por el intercepto. Estas diferencias todavía mezclan tamaños y tipos de vivienda distintos. Un valor p mayor que 0.05 no demuestra igualdad entre zonas.
Datos_Vivienda$Zona_sur <- relevel(Datos_Vivienda$Zona, ref = "Zona Sur")
mod3 <- lm(precio_millon ~ Zona_sur, data = Datos_Vivienda)
coeficientes(mod3)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 426.52 | 4.499 | 94.795 | 0 |
| Zona_surZona Centro | -116.83 | 28.139 | -4.152 | 0 |
| Zona_surZona Norte | -80.91 | 8.371 | -9.665 | 0 |
| Zona_surZona Oeste | 251.06 | 10.005 | 25.093 | 0 |
| Zona_surZona Oriente | -197.99 | 17.112 | -11.570 | 0 |
max(abs(fitted(mod2) - fitted(mod3)))
## [1] 1.24e-09
Ahora el intercepto es la media de Zona Sur y las diferencias se leen respecto a Sur. Las predicciones y el R² no cambian, solo la comparación de referencia. La diferencia máxima calculada arriba debe ser cero o prácticamente cero.
mod_multiple <- lm(precio_millon ~ area_c + Zona + Tipo, data = Datos_Vivienda)
coeficientes(mod_multiple)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 187.325 | 19.877 | 9.424 | 0.000 |
| area_c | 1.618 | 0.020 | 79.344 | 0.000 |
| ZonaZona Norte | 73.278 | 20.180 | 3.631 | 0.000 |
| ZonaZona Oeste | 339.487 | 20.828 | 16.300 | 0.000 |
| ZonaZona Oriente | -77.769 | 22.597 | -3.442 | 0.001 |
| ZonaZona Sur | 135.729 | 19.806 | 6.853 | 0.000 |
| TipoCasa | -36.951 | 6.231 | -5.930 | 0.000 |
Intuición: antes mezclábamos viviendas de diferentes tamaños; ahora ajustamos esa comparación. El modelo aditivo supone la misma pendiente de área para todas las zonas. «Mantener constante» es un ajuste estadístico, no un experimento.
mod4 <- lm(precio_millon ~ area_c * Zona + Tipo, data = Datos_Vivienda)
coeficientes(mod4)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 253.753 | 25.145 | 10.091 | 0.000 |
| area_c | 0.970 | 0.172 | 5.626 | 0.000 |
| ZonaZona Norte | 24.826 | 25.492 | 0.974 | 0.330 |
| ZonaZona Oeste | 219.108 | 26.293 | 8.333 | 0.000 |
| ZonaZona Oriente | -22.735 | 28.020 | -0.811 | 0.417 |
| ZonaZona Sur | 62.992 | 25.173 | 2.502 | 0.012 |
| TipoCasa | -43.748 | 6.088 | -7.186 | 0.000 |
| area_c:ZonaZona Norte | 0.396 | 0.176 | 2.253 | 0.024 |
| area_c:ZonaZona Oeste | 1.218 | 0.179 | 6.798 | 0.000 |
| area_c:ZonaZona Oriente | -0.607 | 0.186 | -3.256 | 0.001 |
| area_c:ZonaZona Sur | 0.772 | 0.173 | 4.454 | 0.000 |
anova(mod_multiple, mod4)
| Res.Df | RSS | Df | Sum of Sq | F | Pr(>F) |
|---|---|---|---|---|---|
| 8312 | 388898979 | NA | NA | NA | NA |
| 8308 | 364971467 | 4 | 23927512 | 136.2 | 0 |
area_c * Zona incluye área + zona + productos
área×zona. La prueba F compara el modelo de pendiente común con
el de pendientes distintas, bajo los supuestos clásicos, incluida
varianza constante.
area_c es la pendiente en Centro.area_c:Zona... es cuánto cambia esa pendiente respecto
a Centro. Para obtener la pendiente de otra zona hay que
sumar ambos coeficientes.area_c = 0.Corrección del ejemplo inicial:
Zona:Area_contruida sin los efectos principales permite
pendientes por zona, pero impone un intercepto común.
Ese intercepto sí se interpreta: es el precio predicho a área cero para
todas las zonas, aunque no resulte útil en la práctica. No es
equivalente a Zona * Area.
# La diferencia entre 101 y 100 m² da la pendiente por m² de cada zona.
perfil <- data.frame(area_c = 0,
Zona = factor(levels(Datos_Vivienda$Zona), levels = levels(Datos_Vivienda$Zona)),
Tipo = factor("Apartamento", levels = levels(Datos_Vivienda$Tipo)))
perfil_101 <- perfil
perfil_101$area_c <- 1
tabla(data.frame(Zona = perfil$Zona,
Precio_a_100m2 = predict(mod4, perfil),
Pendiente_por_m2 = predict(mod4, perfil_101) - predict(mod4, perfil)))
| Zona | Precio_a_100m2 | Pendiente_por_m2 |
|---|---|---|
| Zona Centro | 253.8 | 0.970 |
| Zona Norte | 278.6 | 1.366 |
| Zona Oeste | 472.9 | 2.188 |
| Zona Oriente | 231.0 | 0.363 |
| Zona Sur | 316.7 | 1.742 |
Pregunta de clase: si una zona tiene mayor pendiente, ¿podemos afirmar que ampliar allí una vivienda causa más valorización? No: observamos viviendas distintas, con posibles diferencias de calidad, antigüedad y otras variables omitidas. Además, compruebe que el perfil de 100 m² sea plausible en cada zona.
Conservamos zona y tipo en los tres modelos para estudiar la forma de
la relación con área sin retirar esos controles. log() es
el logaritmo natural. No se aplica a cero, números negativos o códigos
de categorías.
mod5 <- lm(precio_millon ~ log(Area_contruida) + Zona + Tipo, data = Datos_Vivienda)
mod6 <- lm(log(precio_millon) ~ area_c + Zona + Tipo, data = Datos_Vivienda)
mod7 <- lm(log(precio_millon) ~ log(Area_contruida) + Zona + Tipo, data = Datos_Vivienda)
coeficientes(mod5)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | -1725.89 | 26.699 | -64.642 | 0.000 |
| log(Area_contruida) | 419.96 | 4.452 | 94.337 | 0.000 |
| ZonaZona Norte | 100.16 | 18.579 | 5.391 | 0.000 |
| ZonaZona Oeste | 274.38 | 19.252 | 14.252 | 0.000 |
| ZonaZona Oriente | -41.85 | 20.823 | -2.010 | 0.044 |
| ZonaZona Sur | 155.62 | 18.234 | 8.535 | 0.000 |
| TipoCasa | -147.07 | 6.289 | -23.385 | 0.000 |
coeficientes(mod6)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 5.281 | 0.041 | 130.141 | 0 |
| area_c | 0.003 | 0.000 | 70.781 | 0 |
| ZonaZona Norte | 0.150 | 0.041 | 3.632 | 0 |
| ZonaZona Oeste | 0.771 | 0.043 | 18.131 | 0 |
| ZonaZona Oriente | -0.304 | 0.046 | -6.585 | 0 |
| ZonaZona Sur | 0.309 | 0.040 | 7.645 | 0 |
| TipoCasa | 0.078 | 0.013 | 6.143 | 0 |
coeficientes(mod7)
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 1.192 | 0.046 | 26.131 | 0 |
| log(Area_contruida) | 0.903 | 0.008 | 118.814 | 0 |
| ZonaZona Norte | 0.184 | 0.032 | 5.813 | 0 |
| ZonaZona Oeste | 0.567 | 0.033 | 17.243 | 0 |
| ZonaZona Oriente | -0.224 | 0.036 | -6.299 | 0 |
| ZonaZona Sur | 0.326 | 0.031 | 10.457 | 0 |
| TipoCasa | -0.253 | 0.011 | -23.551 | 0 |
| Modelo | Interpretación de la pendiente del área, manteniendo zona y tipo |
|---|---|
| Lin–log, mod5 | 1% más de área: aproximadamente beta/100 millones más de precio |
| Log–lin, mod6 | Un m² adicional: cambio exacto de 100[exp(beta)−1]% en precio |
| Log–log, mod7 | 1% más de área: aproximadamente beta% de cambio en precio; beta es la elasticidad |
b5 <- coef(mod5)["log(Area_contruida)"]
b6 <- coef(mod6)["area_c"]
b7 <- coef(mod7)["log(Area_contruida)"]
tabla(data.frame(
Comparacion = c("mod5: 10% más de área", "mod6: 10 m² más", "mod7: 10% más de área"),
Cambio = c(b5 * log(1.10), 100 * expm1(10 * b6), 100 * (1.10^b7 - 1)),
Unidad = c("Millones", "%", "%")))
| Comparacion | Cambio | Unidad |
|---|---|---|
| mod5: 10% más de área | 40.027 | Millones |
| mod6: 10 m² más | 2.991 | % |
| mod7: 10% más de área | 8.991 | % |
Corrección importante: en lin–log el cambio exacto
ante 10% más de área es beta * log(1.10), no
beta * 0.10. Si beta fuera 360.20, serían 34.33
millones. No es una relación de proporcionalidad entre precio y
área: duplicar el área implica sumar beta * log(2)
millones, no duplicar el precio.
En modelos con log(precio), una dummy se interpreta
como 100 * (exp(coeficiente) − 1)% frente a su referencia.
Por ejemplo, 0.20 equivale a 22.14%, no exactamente 20%. Estas lecturas
describen la media geométrica condicional; también la mediana si el
error logarítmico tiene mediana cero. Para predecir la media aritmética
en millones hace falta tratar la retransformación.
No compare directamente R² de precio con R² de log(precio): son escalas distintas. Una transformación no garantiza por sí sola mejor ajuste o supuestos válidos.
op <- par(mfrow = c(2, 2))
plot(mod4, which = c(1, 2, 3, 5))
par(op)
| Gráfico | Qué revisar | Qué sugeriría un problema |
|---|---|---|
| Residuos vs ajustados | Errores alrededor de cero | Curvatura: forma funcional insuficiente |
| Q–Q | Cercanía a la recta, especialmente en colas | Colas alejadas: normalidad dudosa |
| Scale–Location | Dispersión aproximadamente estable | Embudo: posible heterocedasticidad |
| Residuos vs leverage | Perfiles extremos y distancia de Cook | Casos que pueden influir mucho en los coeficientes |
Describa lo que vea al ejecutar, sin asumir de
antemano que se cumplen los supuestos. Cook menor que 1 no garantiza
ausencia de influencia. Revise casos marcados y su contexto antes de
eliminarlos. Repita los gráficos para mod7 si prefiere el
modelo logarítmico.
if (requireNamespace("lmtest", quietly = TRUE) &&
requireNamespace("sandwich", quietly = TRUE)) {
print(lmtest::bptest(mod4))
print(lmtest::coeftest(mod4, vcov. = sandwich::vcovHC(mod4, type = "HC3")))
print(lmtest::waldtest(mod_multiple, mod4,
vcov = function(m) sandwich::vcovHC(m, type = "HC3"), test = "F"))
} else {
cat("Para las pruebas opcionales se requieren lmtest y sandwich. Los gráficos sí están disponibles.")
}
##
## studentized Breusch-Pagan test
##
## data: mod4
## BP = 2129, df = 10, p-value <2e-16
##
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 253.753 16.307 15.56 < 2e-16 ***
## area_c 0.970 0.156 6.22 5.2e-10 ***
## ZonaZona Norte 24.826 16.505 1.50 0.13257
## ZonaZona Oeste 219.108 20.240 10.83 < 2e-16 ***
## ZonaZona Oriente -22.735 19.994 -1.14 0.25555
## ZonaZona Sur 62.992 16.262 3.87 0.00011 ***
## TipoCasa -43.748 9.042 -4.84 1.3e-06 ***
## area_c:ZonaZona Norte 0.396 0.176 2.25 0.02448 *
## area_c:ZonaZona Oeste 1.218 0.211 5.78 7.6e-09 ***
## area_c:ZonaZona Oriente -0.607 0.219 -2.77 0.00558 **
## area_c:ZonaZona Sur 0.772 0.164 4.72 2.4e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Wald test
##
## Model 1: precio_millon ~ area_c + Zona + Tipo
## Model 2: precio_millon ~ area_c * Zona + Tipo
## Res.Df Df F Pr(>F)
## 1 8312
## 2 8308 4 27.9 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Breusch–Pagan contrasta varianza constante. HC3 cambia los errores estándar, no los coeficientes; la última prueba evalúa conjuntamente las interacciones con esa corrección. Son herramientas de inferencia aproximada, no una reparación de variables omitidas ni de dependencia entre viviendas del mismo barrio.
Tres límites: un valor p grande no demuestra un supuesto; un R² de 0.59 no basta para llamar «bueno» al modelo; precios o áreas no necesitan ser normales para estimar una regresión. La independencia y la representatividad también dependen de cómo se obtuvieron los anuncios.
# Perfil observado, usado aquí para ilustrar predict(), no para medir desempeño.
nueva <- Datos_Vivienda[1, , drop = FALSE]
tabla(nueva[c("Area_contruida", "Zona", "Tipo")])
| Area_contruida | Zona | Tipo |
|---|---|---|
| 237 | Zona Sur | Casa |
predict(mod4, newdata = nueva, interval = "prediction")
## fit lwr upr
## 1 511.7 100.7 922.6
El intervalo de predicción contempla la variación de una oferta individual. Un intervalo de confianza para el precio medio sería más estrecho. Estos intervalos clásicos dependen de normalidad y homocedasticidad de los errores; HC3 no los vuelve robustos automáticamente. Para evaluar capacidad predictiva, reserve datos que no se usen al ajustar y compare errores en millones.
Tarea breve: filtre por Valle del Lili, compare
precio ~ área con
log(precio) ~ log(área) + Tipo y agregue una interacción
área×tipo. Interprete una pendiente, una dummy y la interacción. Revise
residuos y explique cuál modelo elegiría. Verifique antes que ambos
tipos tengan observaciones suficientes; no incluya barrio ni otras
categorías que queden constantes tras filtrar.
Idea para recordar: simple = una asociación; múltiple = una asociación ajustada; interacción = una pendiente que cambia; logaritmos = otra escala para interpretar. Ninguno convierte automáticamente una asociación en causalidad.
Nota de preparación: se revisó la estructura del código y la base adjunta. No se compiló aquí porque el entorno de revisión no dispone de R. Las tablas e interpretaciones numéricas se actualizan al compilar en RStudio.