Qué hace este bloque: Configura cómo se mostrarán el código, las advertencias y las tablas; no estima ningún modelo. Este bloque se ejecuta, pero no se muestra en el HTML.

Paso 1. Conocer los datos y formular la pregunta

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.

Interpretamos precio 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.

Qué hace este bloque: Lee el Excel, revisa las columnas necesarias, conserva una muestra común y fija las referencias de las categorías. La primera tabla permite saber si se excluyeron filas.

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",
               "Banos", "Habitaciones", "Estrato")
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$Estrato <- relevel(factor(Datos_Vivienda$Estrato), ref = "3")
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².

Paso 2. Estimar un modelo simple y aprender a leer summary()

Área construida

\[ Precio_i=\beta_0+\beta_1 Area_i+\varepsilon_i. \]

Qué hace este bloque: Ajusta una recta entre precio y área, muestra su resumen y dibuja esa recta sobre los datos. Empiece por la pendiente de Area_contruida.

mod1 <- lm(precio_millon ~ Area_contruida, data = Datos_Vivienda)
summary(mod1)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2659.9  -120.8   -47.5    67.3  1330.1 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    157.4764     4.1364    38.1   <2e-16 ***
## Area_contruida   1.5802     0.0183    86.3   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 239 on 8317 degrees of freedom
## Multiple R-squared:  0.472,  Adjusted R-squared:  0.472 
## F-statistic: 7.45e+03 on 1 and 8317 DF,  p-value: <2e-16
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.

¿En qué orden leemos summary()?

  1. Estimate: lea primero el signo, el tamaño y las unidades de la pendiente.
  2. Std. Error: indica la incertidumbre de esa estimación.
  3. Pr(>|t|): contrasta si el coeficiente es cero bajo los supuestos del modelo. Las estrellas no indican importancia económica ni causalidad.
  4. Residual standard error: dispersión residual en unidades de la respuesta. En este modelo son millones; en un modelo de log(precio) serán unidades log.
  5. Multiple R-squared y Adjusted R-squared: ajuste interno y ajuste penalizado por el número de parámetros. No son medidas de acierto fuera de muestra.
  6. F-statistic: contrasta que todas las pendientes sean cero simultáneamente.

Ejemplo de lectura completa: «Por un m² adicional, el precio anunciado difiere en 1.58 millones en promedio. Su error estándar es 0.018. El modelo explica 47.2% de la variación muestral».

Antes de avanzar: mire la nube de puntos. Si área por sí sola deja diferencias grandes, piense qué otra característica de la vivienda podría explicar parte de ellas. No agregue variables solamente para aumentar R².

Paso 3. Incorporar categorías y entender las dummies

Qué hace este bloque: Cuenta las ofertas por zona y estima un modelo de medias por grupo. Compare el intercepto y las dummies con la tabla de promedios.

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)
summary(mod2)
## 
## Call:
## lm(formula = precio_millon ~ Zona, data = Datos_Vivienda)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -592.6 -200.6  -85.6   93.5 1594.4 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         309.7       27.8   11.15  < 2e-16 ***
## ZonaZona Norte       35.9       28.7    1.25    0.210    
## ZonaZona Oeste      367.9       29.2   12.61  < 2e-16 ***
## ZonaZona Oriente    -81.2       32.3   -2.51    0.012 *  
## ZonaZona Sur        116.8       28.1    4.15  3.3e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 309 on 8314 degrees of freedom
## Multiple R-squared:  0.115,  Adjusted R-squared:  0.114 
## F-statistic:  269 on 4 and 8314 DF,  p-value: <2e-16
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

Qué hace este bloque: Cambia únicamente la zona de referencia a Sur, vuelve a estimar y comprueba que las predicciones siguen siendo iguales.

Datos_Vivienda$Zona_sur <- relevel(Datos_Vivienda$Zona, ref = "Zona Sur")
mod3 <- lm(precio_millon ~ Zona_sur, data = Datos_Vivienda)
summary(mod3)
## 
## Call:
## lm(formula = precio_millon ~ Zona_sur, data = Datos_Vivienda)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -592.6 -200.6  -85.6   93.5 1594.4 
## 
## Coefficients:
##                      Estimate Std. Error t value Pr(>|t|)    
## (Intercept)            426.52       4.50   94.80  < 2e-16 ***
## Zona_surZona Centro   -116.82      28.14   -4.15  3.3e-05 ***
## Zona_surZona Norte     -80.91       8.37   -9.67  < 2e-16 ***
## Zona_surZona Oeste     251.06      10.01   25.09  < 2e-16 ***
## Zona_surZona Oriente  -197.99      17.11  -11.57  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 309 on 8314 degrees of freedom
## Multiple R-squared:  0.115,  Adjusted R-squared:  0.114 
## F-statistic:  269 on 4 and 8314 DF,  p-value: <2e-16
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.

Paso 4. Construir modelos múltiples de forma gradual

La idea: agregar grupos de variables con una razón concreta. Así podemos ver qué cambia al comparar viviendas más parecidas. Los modelos siguientes usan las mismas filas, seleccionadas al importar.

4.1. Área y zona: ajustar por ubicación general

Qué hace este bloque: Agrega zona a la explicación del precio y centra el área en 100 m². Lea ahora la pendiente del área como una asociación ajustada por zona.

mod_area_zona <- lm(precio_millon ~ area_c + Zona, data = Datos_Vivienda)
summary(mod_area_zona)
## 
## Call:
## lm(formula = precio_millon ~ area_c + Zona, data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2377.3  -102.7   -37.5    63.3  1246.4 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      164.0994    19.5277    8.40  < 2e-16 ***
## area_c             1.5482     0.0167   92.84  < 2e-16 ***
## ZonaZona Norte    86.8810    20.0908    4.32  1.5e-05 ***
## ZonaZona Oeste   364.2379    20.4472   17.81  < 2e-16 ***
## ZonaZona Oriente -78.5136    22.6433   -3.47  0.00053 ***
## ZonaZona Sur     148.9189    19.7213    7.55  4.8e-14 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 217 on 8313 degrees of freedom
## Multiple R-squared:  0.565,  Adjusted R-squared:  0.565 
## F-statistic: 2.16e+03 on 5 and 8313 DF,  p-value: <2e-16

¿Por qué agregar zona? Dos viviendas con la misma área pueden estar en mercados distintos. La pendiente del área ahora se interpreta manteniendo fija la zona. Cada dummy compara zonas a igual área. El modelo todavía supone una pendiente común para todas ellas.

Qué hace este bloque: Extrae la pendiente del área de dos modelos para observar cuánto cambia al controlar por ubicación. No es una prueba estadística de esa diferencia.

c(Simple = coef(mod1)["Area_contruida"],
  Ajustando_por_zona = coef(mod_area_zona)["area_c"])
##     Simple.Area_contruida Ajustando_por_zona.area_c 
##                     1.580                     1.548

Pregunta: ¿cambió la pendiente? Un cambio no es una contradicción: cambió la comparación. Centrar el área en 100 solo cambia el intercepto; las diferencias de pendiente entre estos modelos provienen del ajuste por zona.

4.2. Agregar tipo: casas y apartamentos

Qué hace este bloque: Usa update() para añadir tipo sin reescribir toda la fórmula. Busque TipoCasa y observe si cambia la pendiente del área.

mod_area_zona_tipo <- update(mod_area_zona, . ~ . + Tipo)
summary(mod_area_zona_tipo)
## 
## Call:
## lm(formula = precio_millon ~ area_c + Zona + Tipo, data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2479.3  -103.9   -38.8    65.4  1257.9 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      187.3254    19.8773    9.42  < 2e-16 ***
## area_c             1.6181     0.0204   79.34  < 2e-16 ***
## ZonaZona Norte    73.2780    20.1804    3.63  0.00028 ***
## ZonaZona Oeste   339.4868    20.8278   16.30  < 2e-16 ***
## ZonaZona Oriente -77.7691    22.5972   -3.44  0.00058 ***
## ZonaZona Sur     135.7291    19.8062    6.85  7.8e-12 ***
## TipoCasa         -36.9507     6.2309   -5.93  3.1e-09 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 216 on 8312 degrees of freedom
## Multiple R-squared:  0.567,  Adjusted R-squared:  0.567 
## F-statistic: 1.82e+03 on 6 and 8312 DF,  p-value: <2e-16

update() conserva el modelo y agrega el término indicado. TipoCasa compara casa−apartamento con igual área y zona. El coeficiente estimado es -36.95 millones: si es negativo, las casas tienen menor precio ajustado; si es positivo, mayor. Esto puede diferir de comparar los promedios sin ajustar.

4.3. Agregar distribución interna y estrato

Qué hace este bloque: Agrega baños, habitaciones y estrato. summary() muestra las estimaciones; confint() agrega intervalos clásicos al 95% para apreciar su incertidumbre.

mod_multiple <- update(mod_area_zona_tipo, . ~ . + Banos + Habitaciones + Estrato)
summary(mod_multiple)
## 
## Call:
## lm(formula = precio_millon ~ area_c + Zona + Tipo + Banos + Habitaciones + 
##     Estrato, data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1664.5   -71.6    -9.1    50.0  1170.3 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      137.2702    16.7302    8.20  2.7e-16 ***
## area_c             1.0369     0.0191   54.28  < 2e-16 ***
## ZonaZona Norte   -16.6274    16.1301   -1.03  0.30265    
## ZonaZona Oeste    70.1350    17.0482    4.11  3.9e-05 ***
## ZonaZona Oriente -61.8704    17.7163   -3.49  0.00048 ***
## ZonaZona Sur     -12.3725    16.1334   -0.77  0.44317    
## TipoCasa          -2.9091     5.2547   -0.55  0.57985    
## Banos             62.5271     2.1644   28.89  < 2e-16 ***
## Habitaciones     -23.1554     1.8757  -12.34  < 2e-16 ***
## Estrato4          28.3694     6.8079    4.17  3.1e-05 ***
## Estrato5          88.9216     6.7238   13.22  < 2e-16 ***
## Estrato6         317.0343     8.2783   38.30  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 169 on 8307 degrees of freedom
## Multiple R-squared:  0.735,  Adjusted R-squared:  0.734 
## F-statistic: 2.09e+03 on 11 and 8307 DF,  p-value: <2e-16
confint(mod_multiple)
##                     2.5 %  97.5 %
## (Intercept)      104.4747 170.066
## area_c             0.9995   1.074
## ZonaZona Norte   -48.2464  14.992
## ZonaZona Oeste    36.7164 103.554
## ZonaZona Oriente -96.5986 -27.142
## ZonaZona Sur     -43.9980  19.253
## TipoCasa         -13.2097   7.391
## Banos             58.2844  66.770
## Habitaciones     -26.8323 -19.479
## Estrato4          15.0242  41.715
## Estrato5          75.7414 102.102
## Estrato6         300.8069 333.262

¿Por qué incluirlas? Baños y habitaciones describen la distribución interna; estrato permite diferencias entre categorías que la zona amplia no necesariamente captura. Se trata estrato como factor: no se impone el mismo salto de 3 a 4 que de 5 a 6. La categoría base es 3.

  • Área: diferencia por un m² adicional, manteniendo las demás variables fijas.
  • Baños: diferencia por un baño adicional con igual área, habitaciones, zona, tipo y estrato. No equivale al efecto causal de construir un baño.
  • Habitaciones: diferencia por una habitación adicional a igual área; más habitaciones pueden implicar espacios más pequeños.
  • Estrato6: diferencia estrato 6−estrato 3, con los otros atributos constantes.
  • Intercepto: apartamento de 100 m² en Centro, estrato 3, con cero baños y habitaciones. Ese perfil puede no ser útil: no todo intercepto tiene interés práctico.

Lectura numérica: en el modelo completo, la diferencia por un m² es 1.037 millones; por un baño adicional, 62.527 millones. Son asociaciones parciales. Si un coeficiente sorprende, revise la comparación y la redundancia entre características antes de declararlo un error.

4.4. ¿Qué aportó cada paso?

Qué hace este bloque: Compara el aporte conjunto de los términos añadidos en cada etapa mediante pruebas F para modelos anidados.

anova(mod1, mod_area_zona, mod_area_zona_tipo, mod_multiple)
Res.Df RSS Df Sum of Sq F Pr(>F)
8317 474009760 NA NA NA NA
8313 390544380 4 83465380 727.16 0
8312 388898979 1 1645401 57.34 0
8307 238376382 5 150522597 1049.09 0

Cada fila después de la primera pregunta si los términos añadidos mejoran el ajuste frente al modelo anterior. La prueba F clásica supone varianza constante. mod1 y la versión centrada describen la misma recta, por eso el cambio de escala del intercepto no impide la comparación. No use esta prueba si cambian las filas, la respuesta o si los modelos no son anidados.

Decisión docente: no eliminar una variable solo porque su p sea mayor que 0.05. Considere su papel, su incertidumbre y el aporte conjunto de categorías. Área, baños y habitaciones pueden compartir información y producir coeficientes imprecisos.

Paso 5. Probar una interacción con una pregunta concreta

Qué hace este bloque: Agrega los productos área×zona al modelo completo y lo compara con el modelo aditivo. Ahora las pendientes pueden variar entre zonas.

mod4 <- update(mod_multiple, . ~ . + area_c:Zona)
summary(mod4)
## 
## Call:
## lm(formula = precio_millon ~ area_c + Zona + Tipo + Banos + Habitaciones + 
##     Estrato + area_c:Zona, data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1049.1   -66.3   -10.0    48.2  1167.9 
## 
## Coefficients:
##                         Estimate Std. Error t value Pr(>|t|)    
## (Intercept)              167.777     20.390    8.23  < 2e-16 ***
## area_c                     0.666      0.136    4.91  9.3e-07 ***
## ZonaZona Norte           -42.451     20.135   -2.11   0.0350 *  
## ZonaZona Oeste           -27.421     21.099   -1.30   0.1938    
## ZonaZona Oriente         -20.493     21.954   -0.93   0.3506    
## ZonaZona Sur             -52.350     20.151   -2.60   0.0094 ** 
## TipoCasa                  -5.044      5.146   -0.98   0.3270    
## Banos                     58.259      2.106   27.67  < 2e-16 ***
## Habitaciones             -19.579      1.827  -10.72  < 2e-16 ***
## Estrato4                  37.437      6.684    5.60  2.2e-08 ***
## Estrato5                  98.310      6.606   14.88  < 2e-16 ***
## Estrato6                 323.970      8.080   40.09  < 2e-16 ***
## area_c:ZonaZona Norte      0.193      0.138    1.40   0.1605    
## area_c:ZonaZona Oeste      1.026      0.140    7.31  3.0e-13 ***
## area_c:ZonaZona Oriente   -0.456      0.146   -3.12   0.0018 ** 
## area_c:ZonaZona Sur        0.413      0.136    3.03   0.0024 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 164 on 8303 degrees of freedom
## Multiple R-squared:  0.751,  Adjusted R-squared:  0.751 
## F-statistic: 1.67e+03 on 15 and 8303 DF,  p-value: <2e-16
anova(mod_multiple, mod4)
Res.Df RSS Df Sum of Sq F Pr(>F)
8307 238376382 NA NA NA NA
8303 223452551 4 14923831 138.6 0

La hipótesis es que un m² adicional no tiene la misma asociación con el precio en todas las zonas. 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.

Qué hace este bloque: Predice para perfiles que difieren solo en un m². La resta permite leer directamente la pendiente de cada zona sin sumar coeficientes a mano.

# 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)),
  Banos = 2, Habitaciones = 3,
  Estrato = factor("3", levels = levels(Datos_Vivienda$Estrato)))
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 225.6 0.666
Zona Norte 183.1 0.859
Zona Oeste 198.1 1.693
Zona Oriente 205.1 0.211
Zona Sur 173.2 1.079

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, el perfil ilustrativo tiene 100 m², dos baños, tres habitaciones y estrato 3: compruebe que sea plausible en cada zona. La pendiente estimada es constante dentro de cada zona en esta especificación.

Paso 6. Probar transformaciones: por qué y cómo

Antes de transformar, observe el modelo aditivo. Una transformación responde a una hipótesis sobre la forma de la relación, no a la búsqueda de estrellas.

Qué hace este bloque: Dibuja residuos frente a predicciones del modelo aditivo. Busque curvatura o dispersión creciente para motivar qué alternativa explorar.

plot(mod_multiple, which = 1)

Lo que sospechamos Candidato Qué cambia
La diferencia por m² disminuye a medida que crece la vivienda Lin–log El área entra en log; precio sigue en millones
La dispersión crece con el precio y tiene sentido pensar en proporciones Log–lin Precio en log, área en niveles
Cambios porcentuales de área se relacionan con cambios porcentuales de precio Log–log Precio y área en log
Hay curvatura, pero interesa mantener ambos en niveles Cuadrático Se agrega el área al cuadrado

Estas son motivaciones, no diagnósticos automáticos. Conserve los mismos controles para aislar qué cambia al transformar el área o el precio. Usamos mod_multiple como base; la interacción por zona se evaluó por separado y se puede combinar después si hay una razón para hacerlo.

6.1. Lin–log: diferencias en millones ante cambios porcentuales del área

Qué hace este bloque: Sustituye el área centrada por su logaritmo y mantiene los demás controles. La respuesta continúa en millones.

mod5 <- update(mod_multiple, . ~ . - area_c + log(Area_contruida))
summary(mod5)
## 
## Call:
## lm(formula = precio_millon ~ Zona + Tipo + Banos + Habitaciones + 
##     Estrato + log(Area_contruida), data = Datos_Vivienda)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -752.4  -94.1    2.0   57.6 1140.7 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         -1120.38      25.98  -43.13  < 2e-16 ***
## ZonaZona Norte         30.21      16.12    1.87   0.0609 .  
## ZonaZona Oeste         89.43      17.02    5.26  1.5e-07 ***
## ZonaZona Oriente      -41.08      17.69   -2.32   0.0202 *  
## ZonaZona Sur           43.19      16.15    2.67   0.0075 ** 
## TipoCasa              -63.34       5.75  -11.02  < 2e-16 ***
## Banos                  52.05       2.22   23.42  < 2e-16 ***
## Habitaciones          -29.13       1.89  -15.40  < 2e-16 ***
## Estrato4                5.24       6.83    0.77   0.4431    
## Estrato5               34.95       6.88    5.08  3.9e-07 ***
## Estrato6              245.91       8.59   28.62  < 2e-16 ***
## log(Area_contruida)   285.93       5.23   54.64  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 169 on 8307 degrees of freedom
## Multiple R-squared:  0.736,  Adjusted R-squared:  0.735 
## F-statistic: 2.1e+03 on 11 and 8307 DF,  p-value: <2e-16

Un aumento del 1% de área se asocia aproximadamente con beta/100 millones. La pendiente por m² es beta/área: si beta es positivo, el mismo m² adicional tiene una asociación menor cuanto mayor sea la vivienda.

En estos datos: 10% más de área se asocia con 27.25 millones de diferencia, manteniendo los controles fijos. Se usa beta * log(1.10), no beta * 0.10. Duplicar área implica sumar beta * log(2), no duplicar precio.

6.2. Log–lin: diferencias porcentuales por un m² adicional

Qué hace este bloque: Transforma solo la respuesta a log(precio), conservando el área en niveles y los mismos controles.

mod6 <- update(mod_multiple, log(precio_millon) ~ .)
summary(mod6)
## 
## Call:
## lm(formula = log(precio_millon) ~ area_c + Zona + Tipo + Banos + 
##     Habitaciones + Estrato, data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.0229 -0.1960 -0.0121  0.1810  1.7730 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       4.968549   0.028896  171.95  < 2e-16 ***
## area_c            0.001411   0.000033   42.76  < 2e-16 ***
## ZonaZona Norte   -0.190606   0.027859   -6.84  8.4e-12 ***
## ZonaZona Oeste    0.013010   0.029445    0.44     0.66    
## ZonaZona Oriente -0.246463   0.030599   -8.05  9.1e-16 ***
## ZonaZona Sur     -0.206131   0.027865   -7.40  1.5e-13 ***
## TipoCasa          0.155393   0.009076   17.12  < 2e-16 ***
## Banos             0.125165   0.003738   33.48  < 2e-16 ***
## Habitaciones     -0.004783   0.003240   -1.48     0.14    
## Estrato4          0.311549   0.011758   26.50  < 2e-16 ***
## Estrato5          0.584345   0.011613   50.32  < 2e-16 ***
## Estrato6          0.965367   0.014298   67.52  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.293 on 8307 degrees of freedom
## Multiple R-squared:  0.81,   Adjusted R-squared:  0.809 
## F-statistic: 3.21e+03 on 11 and 8307 DF,  p-value: <2e-16

Un m² adicional se asocia con un cambio exacto de 100[exp(beta)−1]%. Por 10 m² use 100 * (exp(10 * beta) - 1), que aquí da 1.42%.

Por qué probarlo: un error de 50 millones puede ser muy distinto en una oferta de 100 y en una de 1.000 millones. Modelar log(precio) enfatiza diferencias relativas y comprime los valores altos. Puede estabilizar la dispersión, pero hay que comprobarlo en los residuos del nuevo modelo.

6.3. Log–log: elasticidad del precio respecto al área

Qué hace este bloque: Transforma también la respuesta del modelo lin–log. Ahora el coeficiente de log(área) se interpreta como elasticidad.

mod7 <- update(mod5, log(precio_millon) ~ .)
summary(mod7)
## 
## Call:
## lm(formula = log(precio_millon) ~ Zona + Tipo + Banos + Habitaciones + 
##     Estrato + log(Area_contruida), data = Datos_Vivienda)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.5445 -0.1590 -0.0088  0.1563  1.4556 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          2.57860    0.03893   66.23  < 2e-16 ***
## ZonaZona Norte      -0.10376    0.02415   -4.30  1.8e-05 ***
## ZonaZona Oeste       0.04329    0.02550    1.70  0.08966 .  
## ZonaZona Oriente    -0.20494    0.02651   -7.73  1.2e-14 ***
## ZonaZona Sur        -0.09120    0.02420   -3.77  0.00017 ***
## TipoCasa            -0.03011    0.00861   -3.50  0.00047 ***
## Banos                0.08337    0.00333   25.03  < 2e-16 ***
## Habitaciones        -0.02572    0.00283   -9.08  < 2e-16 ***
## Estrato4             0.25452    0.01024   24.87  < 2e-16 ***
## Estrato5             0.45448    0.01031   44.06  < 2e-16 ***
## Estrato6             0.77456    0.01288   60.15  < 2e-16 ***
## log(Area_contruida)  0.56565    0.00784   72.13  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.253 on 8307 degrees of freedom
## Multiple R-squared:  0.857,  Adjusted R-squared:  0.857 
## F-statistic: 4.53e+03 on 11 and 8307 DF,  p-value: <2e-16

La pendiente del área es una elasticidad. Un aumento del 1% del área se asocia aproximadamente con 0.566% de cambio del precio, manteniendo las demás variables constantes. El cambio exacto ante 10% más de área es 100 * (1.10^beta - 1).

Dummies en modelos logarítmicos: TipoCasa se interpreta como 100 * (exp(coeficiente) - 1)% respecto al apartamento. En mod7 la diferencia ajustada es -2.97%. Una dummy pasa de 0 a 1: no aumenta «1%». Un coeficiente hipotético de 0.20 equivale a 22.14%.

Estas interpretaciones de log(precio) describen la media geométrica condicional; también la mediana si el error logarítmico tiene mediana cero. Para la media aritmética en millones se necesita tratar la retransformación. No aplique log a zona, tipo o códigos de estrato. Baños y habitaciones se dejan en niveles porque pueden contener ceros; sumar uno cambiaría la interpretación.

6.4. Cuadrático: una curva sin cambiar la respuesta a logaritmos

Qué hace este bloque: Añade el cuadrado del área centrada para permitir curvatura y contrasta si ese término aporta al modelo aditivo.

mod8 <- update(mod_multiple, . ~ . + I(area_c^2))
summary(mod8)
## 
## Call:
## lm(formula = precio_millon ~ area_c + Zona + Tipo + Banos + Habitaciones + 
##     Estrato + I(area_c^2), data = Datos_Vivienda)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -836.9  -73.9   -3.3   52.2 1107.3 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       1.85e+02   1.63e+01   11.36  < 2e-16 ***
## area_c            1.73e+00   3.41e-02   50.78  < 2e-16 ***
## ZonaZona Norte   -6.45e+00   1.56e+01   -0.41   0.6792    
## ZonaZona Oeste    6.78e+01   1.65e+01    4.12  3.9e-05 ***
## ZonaZona Oriente -5.01e+01   1.71e+01   -2.93   0.0034 ** 
## ZonaZona Sur      5.05e+00   1.56e+01    0.32   0.7461    
## TipoCasa         -4.65e+01   5.39e+00   -8.63  < 2e-16 ***
## Banos             5.13e+01   2.14e+00   23.97  < 2e-16 ***
## Habitaciones     -2.85e+01   1.83e+00  -15.61  < 2e-16 ***
## Estrato4          1.79e+01   6.59e+00    2.71   0.0067 ** 
## Estrato5          6.81e+01   6.55e+00   10.39  < 2e-16 ***
## Estrato6          2.84e+02   8.12e+00   34.92  < 2e-16 ***
## I(area_c^2)      -9.13e-04   3.77e-05  -24.23  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 164 on 8306 degrees of freedom
## Multiple R-squared:  0.752,  Adjusted R-squared:  0.752 
## F-statistic: 2.1e+03 on 12 and 8306 DF,  p-value: <2e-16
anova(mod_multiple, mod8)
Res.Df RSS Df Sum of Sq F Pr(>F)
8307 238376382 NA NA NA NA
8306 222634658 1 15741724 587.3 0

I() pide el cuadrado aritmético. La pendiente por m² es beta_area + 2 * beta_cuadrado * (área - 100). A 100 m² vale beta_area; a 200 m² vale beta_area + 200 * beta_cuadrado. Un coeficiente cuadrático negativo indica pendiente decreciente, no necesariamente precio decreciente en todo el rango. Evite extrapolar esa curva a tamaños no observados.

6.5. ¿Cómo elegir entre las transformaciones?

Qué hace este bloque: Resume R² ajustado y AIC en dos tablas separadas según la respuesta. Las salidas completas de cada modelo siguen disponibles en sus summary().

comparar <- function(lista) {
  data.frame(Modelo = names(lista),
    R2_ajustado = sapply(lista, function(m) summary(m)$adj.r.squared),
    AIC = sapply(lista, AIC), row.names = NULL)
}
tabla(comparar(list(Aditivo = mod_multiple, Interaccion = mod4,
                   Lin_log = mod5, Cuadratico = mod8)))
Modelo R2_ajustado AIC
Aditivo 0.734 109013
Interaccion 0.751 108483
Lin_log 0.735 108984
Cuadratico 0.752 108446
tabla(comparar(list(Log_lin = mod6, Log_log = mod7)))
Modelo R2_ajustado AIC
Log_lin 0.809 3173.8
Log_log 0.857 782.9

Compare dentro de cada tabla: misma respuesta y mismas filas. Mayor R² ajustado y menor AIC pueden orientar, pero no bastan. No compare directamente R² ni AIC de precio con los de log(precio); para AIC entre escalas haría falta corregir la transformación de la verosimilitud.

Para elegir, siga tres preguntas: ¿la interpretación tiene sentido?, ¿mejoraron los patrones de residuos?, ¿predice mejor en datos reservados? Esta comparación interna no sustituye la tercera pregunta. No elija automáticamente el modelo más complejo ni afirme que la transformación «arregló» los supuestos.

Paso 7. Volver a revisar los supuestos

Qué hace este bloque: Organiza cuatro gráficos para revisar el modelo aditivo en niveles y luego restaura la disposición gráfica original.

op <- par(mfrow = c(2, 2))
plot(mod_multiple, which = c(1, 2, 3, 5))

par(op)

Qué hace este bloque: Repite los mismos cuatro gráficos para el modelo log–log. Compare la forma de los patrones con los del bloque anterior.

op <- par(mfrow = c(2, 2))
plot(mod7, which = c(1, 2, 3, 5))

par(op)

Compare los patrones del modelo aditivo en niveles y del log–log. Los residuos están en unidades diferentes: interesa su estructura, no cuál eje tiene números más pequeños.

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. Si prefiere otro candidato, repita el diagnóstico para ese modelo.

Qué hace este bloque: Si están disponibles los paquetes, evalúa heterocedasticidad, muestra inferencia robusta HC3 del log–log y contrasta las interacciones con HC3.

if (requireNamespace("lmtest", quietly = TRUE) &&
    requireNamespace("sandwich", quietly = TRUE)) {
  print(lmtest::bptest(mod_multiple))
  print(lmtest::bptest(mod7))
  print(lmtest::coeftest(mod7, vcov. = sandwich::vcovHC(mod7, 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:  mod_multiple
## BP = 1768, df = 11, p-value <2e-16
## 
## 
##  studentized Breusch-Pagan test
## 
## data:  mod7
## BP = 477, df = 11, p-value <2e-16
## 
## 
## t test of coefficients:
## 
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          2.57860    0.05157   50.00  < 2e-16 ***
## ZonaZona Norte      -0.10376    0.02710   -3.83  0.00013 ***
## ZonaZona Oeste       0.04329    0.02914    1.49  0.13751    
## ZonaZona Oriente    -0.20494    0.03051   -6.72  2.0e-11 ***
## ZonaZona Sur        -0.09120    0.02743   -3.32  0.00089 ***
## TipoCasa            -0.03011    0.00996   -3.02  0.00250 ** 
## Banos                0.08337    0.00414   20.12  < 2e-16 ***
## Habitaciones        -0.02572    0.00374   -6.87  6.8e-12 ***
## Estrato4             0.25452    0.01059   24.02  < 2e-16 ***
## Estrato5             0.45448    0.01086   41.84  < 2e-16 ***
## Estrato6             0.77456    0.01480   52.33  < 2e-16 ***
## log(Area_contruida)  0.56565    0.01182   47.86  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Wald test
## 
## Model 1: precio_millon ~ area_c + Zona + Tipo + Banos + Habitaciones + 
##     Estrato
## Model 2: precio_millon ~ area_c + Zona + Tipo + Banos + Habitaciones + 
##     Estrato + area_c:Zona
##   Res.Df Df    F Pr(>F)    
## 1   8307                   
## 2   8303  4 28.8 <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.

Extensión opcional: ¿step by step es lo mismo que stepwise?

No. Lo anterior fue una construcción gradual guiada por preguntas. step() automatiza una búsqueda según AIC. Puede servir para explorar, pero puede elegir resultados inestables y no sabe qué variables necesita la pregunta.

Qué hace este bloque: Ejecuta, solo si usted lo activa, una búsqueda hacia atrás por AIC dentro de la escala log. Conserva el área y muestra el resumen del modelo seleccionado.

# Ejecutar solo como extensión: selección hacia atrás en la misma escala log.
# Conservamos el área por ser la variable central de la pregunta.
mod_step <- step(mod7, direction = "backward",
  scope = list(lower = ~ log(Area_contruida), upper = formula(mod7)), trace = 1)
summary(mod_step)

La salida muestra qué términos se retiran y cómo cambia AIC. No busque con este procedimiento entre precio y log(precio), ni interprete sus valores p como si el modelo se hubiera definido antes de ver los datos. Evalúe el resultado con nuevos datos y compruebe que siga respondiendo la pregunta.

Paso 8. Predecir y comunicar una conclusión

Qué hace este bloque: Toma un perfil observado, calcula una predicción con intervalo en niveles y una estimación de la media en millones desde el modelo log–log.

# 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(mod_multiple, newdata = nueva, interval = "prediction")
##     fit   lwr  upr
## 1 801.1 468.9 1133
# Media en millones desde el modelo log–log: corrección de smearing.
eta <- predict(mod7, newdata = nueva)
exp(eta) * mean(exp(resid(mod7)))
##     1 
## 789.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 independencia, 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. La última línea aproxima la media desde log(precio), usando smearing; un factor global puede fallar si la distribución del error logarítmico cambia con el perfil.

Guion de cierre: «Empecé con área; agregué variables para comparar perfiles más parecidos; probé si la pendiente variaba por zona; exploré transformaciones por la forma y dispersión de la relación. Prefiero ___ porque , aunque queda la limitación . El R² por sí solo no responde esas preguntas».

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