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.
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)
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
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.
skim(apto)
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.
# 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.
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.
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
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.
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.
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.
# 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
# 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
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.
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.
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.