Datos y pregunta de la clase

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².

Regresión simple: una sola característica

Área construida

\[ 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.

Una variable cualitativa: zona

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.

Cambiar la referencia

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.

Regresión múltiple: comparar viviendas más parecidas

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
  • Área: diferencia por un m² adicional, manteniendo zona y tipo constantes.
  • Zona: diferencia frente a Centro, comparando viviendas de igual área y tipo.
  • TipoCasa: diferencia casa−apartamento, comparando igual área y zona.
  • Intercepto: precio estimado de un apartamento de 100 m² en Centro.

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.

Interacción: ¿el área tiene la misma pendiente en todas las zonas?

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.
  • Las dummies de zona comparan precios a 100 m², porque allí 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.

Transformaciones: cambiar unidades de interpretación

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.

Diagnóstico: revisar antes de concluir

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.

Predicción y cierre de la clase

# 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.