Introducción

María, una experimentada agente inmobiliaria y fundadora de C&A en Cali, enfrenta el reto de asesorar a una compañía internacional en la compra de dos propiedades para sus empleados. Aunque el mercado inmobiliario en Cali atraviesa un periodo de baja en ventas, María cuenta con más de diez años de experiencia y lidera un equipo de ocho agentes. Este informe ofrece un análisis exhaustivo y la modelación de dos opciones de viviendas: una casa en la zona norte y un apartamento en la zona sur, siguiendo los requisitos de la compañía. Se incluyen recomendaciones, estimaciones y comparaciones de modelos para apoyar a María en la toma de una decisión informada.

Paquetes

Se cargan los paquetes que seran de utilidad

library(devtools)
library(paqueteMODELOS)
library(tidyverse)
library(mice)
library(factoextra)
library(cluster)
library(fpc)
library(psych)
library(corrplot)
library(FactoMineR)
library(gridExtra)
library(pander)
library(ggdendro)
library(plotly)
library(lmtest)
library(car)
library(raster)
library(sf)
library(skimr)
library(reshape2)
library(ggplot2)
library(dplyr)
library(lmtest)
library(car)

Carga de datos

data("vivienda")
head(as.data.frame(vivienda))
##     id         zona piso estrato preciom areaconst parqueaderos banios
## 1 1147 Zona Oriente <NA>       3     250        70            1      3
## 2 1169 Zona Oriente <NA>       3     320       120            1      2
## 3 1350 Zona Oriente <NA>       3     350       220            2      2
## 4 5992     Zona Sur   02       4     400       280            3      5
## 5 1212   Zona Norte   01       5     260        90            1      2
## 6 1724   Zona Norte   01       5     240        87            1      3
##   habitaciones        tipo      barrio  longitud latitud
## 1            6        Casa 20 de julio -76.51168 3.43382
## 2            3        Casa 20 de julio -76.51237 3.43369
## 3            4        Casa 20 de julio -76.51537 3.43566
## 4            3        Casa  3 de julio -76.54000 3.43500
## 5            3 Apartamento       acopi -76.51350 3.45891
## 6            3 Apartamento       acopi -76.51700 3.36971

Filtrando datos

Ahora se aplica un filtro según los requisitos específicos, denominando “apto” a los apartamentos.

# Apartamentos
apto <- vivienda %>% filter(tipo == "Apartamento")
head(as.data.frame(apto))
##     id       zona piso estrato preciom areaconst parqueaderos banios
## 1 1212 Zona Norte   01       5     260        90            1      2
## 2 1724 Zona Norte   01       5     240        87            1      3
## 3 2326 Zona Norte   01       4     220        52            2      2
## 4 4386 Zona Norte   01       5     310       137            2      3
## 5 7497 Zona Norte   02       6     520        98            2      2
## 6 5424 Zona Norte   03       4     320       108            2      3
##   habitaciones        tipo barrio  longitud latitud
## 1            3 Apartamento  acopi -76.51350 3.45891
## 2            3 Apartamento  acopi -76.51700 3.36971
## 3            3 Apartamento  acopi -76.51974 3.42627
## 4            4 Apartamento  acopi -76.53105 3.38296
## 5            2 Apartamento  acopi -76.54999 3.43505
## 6            3 Apartamento  acopi -76.53638 3.40770
names(apto)
##  [1] "id"           "zona"         "piso"         "estrato"      "preciom"     
##  [6] "areaconst"    "parqueaderos" "banios"       "habitaciones" "tipo"        
## [11] "barrio"       "longitud"     "latitud"
table(apto$zona)
## 
##  Zona Centro   Zona Norte   Zona Oeste Zona Oriente     Zona Sur 
##           24         1198         1029           62         2787
table(apto$estrato)
## 
##    3    4    5    6 
##  639 1404 1766 1291
table(apto$habitaciones)
## 
##    0    1    2    3    4    5    6    7    9 
##   21   49  859 3384  714   63    8    1    1

Se observa que la Zona Sur es la que más registros de apartamentos presenta 2787, seguida de la Zona Norte 1198 y Zona Oeste con 1029. Por otra parte, del estrato 4 al 6 es donde se presentan ubicados el mayor número de apartamentos. Finalmente, los apartamentos presentan en su mayoría 3 habitaciones.

Analisis exploratorio

skim(apto)
Data summary
Name apto
Number of rows 5100
Number of columns 13
_______________________
Column type frequency:
character 4
numeric 9
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
zona 0 1.00 8 12 0 5 0
piso 1381 0.73 2 2 0 12 0
tipo 0 1.00 11 11 0 1 0
barrio 0 1.00 4 29 0 289 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
id 0 1.00 4284.03 2449.82 3.00 2179.75 4158.50 6556.25 8317.00 ▆▇▆▆▇
estrato 0 1.00 4.73 0.98 3.00 4.00 5.00 6.00 6.00 ▃▆▁▇▆
preciom 0 1.00 366.94 289.22 58.00 175.00 279.00 430.00 1950.00 ▇▂▁▁▁
areaconst 0 1.00 112.78 69.36 35.00 68.00 90.00 130.00 932.00 ▇▁▁▁▁
parqueaderos 869 0.83 1.57 0.74 1.00 1.00 1.00 2.00 10.00 ▇▁▁▁▁
banios 0 1.00 2.62 1.07 0.00 2.00 2.00 3.00 8.00 ▁▇▂▁▁
habitaciones 0 1.00 2.97 0.68 0.00 3.00 3.00 3.00 9.00 ▁▇▂▁▁
longitud 0 1.00 -76.53 0.02 -76.59 -76.54 -76.53 -76.52 -76.46 ▁▅▇▂▁
latitud 0 1.00 3.42 0.04 3.33 3.38 3.42 3.45 3.50 ▂▇▅▇▅
plot_ly(data = apto, x = ~areaconst, y = ~preciom, type = 'scatter', mode = 'markers',
        marker = list(size = 10, opacity = 0.7, line = list(width = 1, color = 'rgb(0,0,0)')),
        text = ~paste('Área:', areaconst, '<br>Precio:', preciom)) %>%
  layout(title = 'Precio vs Área Construida',
         xaxis = list(title = 'Área Construida'),
         yaxis = list(title = 'Precio'))

Se logra observar en gran proporción que a mayor área de construcción mayor será su precio, con excepción de algunos datos atípicos donde se puede observar que tienen una gran área a un precio bajo.

plot_ly(data = apto, y = ~preciom, x = ~estrato, type = 'box', 
        marker = list(color = 'rgb(7,40,89)')) %>%
  layout(title = 'Distribución del Precio por Estrato',
         xaxis = list(title = 'Estrato'),
         yaxis = list(title = 'Precio'))

Se puede observar que el estrato afecta el valor del precio del apartamento donde vemos que entre más alto sea este mayor será su precio.

plot_ly(data = apto, y = ~preciom, x = as.factor(apto$banios), type = 'box', 
        marker = list(color = 'rgb(219, 64, 82)')) %>%
  layout(title = 'Distribución del Precio por Número de Baños',
         xaxis = list(title = 'Número de Baños'),
         yaxis = list(title = 'Precio'))

Como en el caso anterior se nota que a mayor número de baños el costo del apartamento será mucho mayor como no lo va indicando la mediana por cada número de baños.

plot_ly(data = apto, y = ~preciom, x = as.factor(apto$habitaciones), type = 'box', 
        marker = list(color = 'rgb(204, 204, 204)')) %>%
  layout(title = 'Distribución del Precio por Número de Habitaciones',
         xaxis = list(title = 'Número de Habitaciones'),
         yaxis = list(title = 'Precio'))

Se logra notar que hay un incremento en el precio de los apartamentos cuando el número de habitaciones varia de 2 a 5.

plot_ly(data = apto, y = ~preciom, x = ~zona, type = 'box', 
        marker = list(color = 'rgb(128, 177, 211)')) %>%
  layout(title = 'Distribución del Precio por Zona',
         xaxis = list(title = 'Zona'),
         yaxis = list(title = 'Precio'))

Acá se logra identificar que la Zona Oeste es la que presenta los precios mayores de los apartamentos seguido de la Zona Norte y Zona Sur.

Ajuste modelo

# Convertir variables categóricas a factores
apto$estrato <- as.factor(apto$estrato)

No se hace una eliminación previa de los valores faltantes debido a que la misma función “lm()” omite los valores faltantes en las variables seleccionadas al momento de ajustar el modelo de regresión.

# Ajustar el modelo de regresión lineal múltiple
modelo <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = apto)

# Resumen del modelo
summary(modelo)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = apto)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1635.50   -53.91     0.24    46.02   980.73 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -59.95783   12.43453  -4.822 1.47e-06 ***
## areaconst      1.96798    0.04726  41.644  < 2e-16 ***
## estrato4      25.53268    8.92944   2.859  0.00427 ** 
## estrato5      42.64309    8.77814   4.858 1.23e-06 ***
## estrato6     175.61949   10.25108  17.132  < 2e-16 ***
## habitaciones -37.98467    3.73083 -10.181  < 2e-16 ***
## parqueaderos  82.60882    4.08240  20.235  < 2e-16 ***
## banios        49.66742    3.35468  14.805  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 134.3 on 4223 degrees of freedom
##   (869 observations deleted due to missingness)
## Multiple R-squared:  0.7953, Adjusted R-squared:  0.7949 
## F-statistic:  2344 on 7 and 4223 DF,  p-value: < 2.2e-16
  • El modelo de regresión lineal muestra que todos los coeficientes del modelo son estadísticamente significativos con valores p menores a 0.05.

  • El coeficiente para areaconst, estrato4, estrato5, estrato6, parqueaderos, banios es positivo, lo que indica que un aumento en cualquiera de estas variables, manteniendo las demás constantes, resulta en un aumento en el precio del apartamento. El aumento en el precio se corresponderá con el valor del coeficiente de la variable en cuestión.

  • El coeficiente para habitaciones es -37.98467, lo que indica que un aumento en el número de habitaciones está asociado con una disminución en el precio del apartamento. Lo cual es algo extraño ya que la realidad es que un apartamento entre más habitaciones posee más costoso suele ser.

  • El R cuadrado ajustado del modelo es 0.7949, lo que indica que aproximadamente el 79.49% de la variabilidad en el precio de los apartamentos se puede explicar por las variables incluidas en el modelo.

  • Para la mejora del ajuste del modelo se puede considerar en la posibilidad de transformar variables, incluir términos de interacción, o usar técnicas de selección de variables.

Validación de supuestos

Linealidad

plot(modelo$fitted.values, modelo$residuals, 
     xlab = "Valores Ajustados", ylab = "Residuos", 
     main = "Residuos vs. Valores Ajustados")
abline(h = 0, col = "red")

Se observa que hay un patrón creciente hacia la derecha en forma de embudo lo que indica que problemas de linealidad, ya que los residuos deberían estar distribuidos aleatoriamente alrededor de la línea horizontal en cero.

Independencia

dwtest(modelo)
## 
##  Durbin-Watson test
## 
## data:  modelo
## DW = 1.7189, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0

Para validar la independencia se hará uso del test de Durbin-Watson, un valor del test cercano a 2 sugiere que los residuos están aproximadamente independientes. Valores cercanos a 0 o 4 sugieren autocorrelación. Para este caso tenemos un valor del test de 1.7189 los que nos puede indicar que los residuos son independientes

Homoscedasticidad

std_residuals <- rstandard(modelo)
plot(modelo$fitted.values, std_residuals, 
     xlab = "Valores Ajustados", ylab = "Residuos Estandarizados", 
     main = "Residuos Estandarizados vs. Valores Ajustados")
abline(h = 0, col = "red")

En teoría los residuos estandarizados deben estar distribuidos aleatoriamente. Pero observamos que hay un patrón como de embudo hacia la derecha, podría indicar problemas de homocedasticidad.

Normalidad

qqnorm(modelo$residuals)
qqline(modelo$residuals, col = "red")

shapiro.test(modelo$residuals)
## 
##  Shapiro-Wilk normality test
## 
## data:  modelo$residuals
## W = 0.83994, p-value < 2.2e-16

Los residuos no siguen una línea recta en el gráfico Q-Q. Por otra parte el test de Shapiro-Wilk nos arroja un valor p mucho menor a 0.05, por lo que se rechaza la hipótesis nula de la normalidad de los residuos.

Multicolinealidad

Para validar la multicolinealidad se usara el Factor de Inflación de Varianza (VIF):

vif(modelo)
##                  GVIF Df GVIF^(1/(2*Df))
## areaconst    2.610576  1        1.615728
## estrato      1.843608  3        1.107333
## habitaciones 1.443209  1        1.201336
## parqueaderos 2.155993  1        1.468330
## banios       2.949222  1        1.717330

Un VIF menor a 5 sugiere que no hay problemas serios de multicolinealidad. Para este caso tenemos que todos son menores que 5 lo que indica que no presentan multicolinealidad las variables.

Sugerencias para los supuestos no cumplidos

  • Para la linealidad se puede considerar agregar términos no lineales (cuadráticos, cúbicos) o realizar transformaciones en las variables.
  • Para la homocedasticidad se puede considerar usar transformaciones de la variable dependiente (como logaritmos) o modelos de regresión robusta.
  • Para la normalidad se puede considera usar transformaciones de la variable dependiente o técnicas no paramétricas.

Ajuste modelo con la muestra del 70%

# Establecer una semilla para reproducibilidad
set.seed(321)

# Seleccionar las columnas deseadas
apto <- apto[, c("preciom", "areaconst", "estrato", "habitaciones", "parqueaderos", "banios")]

# Omitir filas con valores faltantes
apto_clean <- na.omit(apto)

# Número total de observaciones
n <- nrow(apto_clean)

# Índices aleatorios para la partición
indices <- sample(seq_len(n), size = 0.7 * n)

# Conjunto de entrenamiento
train_set <- apto_clean[indices, ]

# Ajustar el modelo con el conjunto de entrenamiento
modelo_train <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train_set)

# Resumen del modelo
summary(modelo_train)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train_set)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1751.13   -54.63     0.53    46.69   976.11 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -43.29992   14.83673  -2.918 0.003545 ** 
## areaconst      2.10575    0.05956  35.355  < 2e-16 ***
## estrato4      24.35937   10.60921   2.296 0.021742 *  
## estrato5      39.44185   10.45151   3.774 0.000164 ***
## estrato6     164.77699   12.19183  13.515  < 2e-16 ***
## habitaciones -47.09113    4.54662 -10.357  < 2e-16 ***
## parqueaderos  79.51915    5.02246  15.833  < 2e-16 ***
## banios        51.06095    4.08836  12.489  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 134.1 on 2953 degrees of freedom
## Multiple R-squared:  0.7918, Adjusted R-squared:  0.7913 
## F-statistic:  1604 on 7 and 2953 DF,  p-value: < 2.2e-16
  • El modelo de entrenamiento de regresión lineal muestra que todos los coeficientes del modelo son estadísticamente significativos con valores p menores a 0.05.
  • El R cuadrado ajustado del modelo es 0.7913, lo que indica que aproximadamente el 79.13% de la variabilidad en el precio de los apartamentos se puede explicar por las variables incluidas en el modelo.

Prediciones con la muestra del 30%

# Conjunto de prueba
test_set <- apto_clean[-indices, ]

# Predicciones en el conjunto de prueba
predicciones <- predict(modelo_train, newdata = test_set)
# Valores reales
valores_reales <- test_set$preciom

Métricas

Error Cuadrático Medio (MSE)

mse <- mean((predicciones - valores_reales)^2)
mse
## [1] 18337.33

Se observa que las predicciones están lejos de los precios reales. Esto indica que el modelo está cometiendo errores grandes y no está haciendo un buen trabajo.

Error Absoluto Medio (MAE)

mae <- mean(abs(predicciones - valores_reales))
mae
## [1] 81.50732

Observamos que la diferencia es alta lo que indica que las predicciones están lejos del precio real, el modelo no es tan preciso.

R² para el conjunto de prueba

ss_total <- sum((valores_reales - mean(valores_reales))^2)
ss_residual <- sum((valores_reales - predicciones)^2)
r_squared_test <- 1 - (ss_residual / ss_total)
r_squared_test
## [1] 0.8005481

El R cuadrado para el conjunto de prueba es del 80.05% lo que indica que el modelo no solo se ajusta bien a los datos de entrenamiento, sino que también predice bien en datos no vistos.