Introducción

En el contexto actual de los mercados inmobiliarios, comprender los factores que influyen en el precio de las viviendas es crucial tanto para los compradores como para los vendedores, así como para los profesionales del sector inmobiliario. La capacidad de predecir el valor de una propiedad con precisión puede proporcionar una ventaja significativa en la toma de decisiones, la planificación y la inversión.

Este trabajo tiene como objetivo desarrollar un modelo de regresión lineal múltiple para predecir el precio de viviendas en un conjunto de datos específico que se centra en apartamentos

Preparación de datos

Para el presente trabajo, se realizará un estudio con un conjunto de datos reducido de viviendas, el cual consiste únicamente en apartamentos.

data("vivienda")
vivienda <- vivienda %>% filter(tipo == "Apartamento")
head(vivienda)
## # A tibble: 6 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1212 Zona N… 01          5     260        90            1      2            3
## 2  1724 Zona N… 01          5     240        87            1      3            3
## 3  2326 Zona N… 01          4     220        52            2      2            3
## 4  4386 Zona N… 01          5     310       137            2      3            4
## 5  7497 Zona N… 02          6     520        98            2      2            2
## 6  5424 Zona N… 03          4     320       108            2      3            3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

Entendiendo los datos que pertenecen a este conjunto, se procede a realizar algunas configuraciones básicas del mismo.

vivienda$tipo =tolower(vivienda$tipo)
vivienda$barrio =tolower(vivienda$barrio)
vivienda$piso <- as.numeric(vivienda$piso)
vivienda$zona    <- as.factor(vivienda$zona)
vivienda$estrato <- as.factor(vivienda$estrato)
vivienda$tipo    <- as.factor(vivienda$tipo)

mapeo <- c("á" = "a", "é" = "e", "í" = "i", "ó" = "o", "ú" = "u", "ü" = "u")
vivienda$barrio = chartr(paste(names(mapeo), collapse = ""), paste(mapeo, collapse = ""), vivienda$barrio)
rm(mapeo)
vivienda$barrio <- gsub("√∫", "u", vivienda$barrio)
vivienda$barrio <- gsub("é", "e", vivienda$barrio)
vivienda$barrio <- gsub("ó", "o", vivienda$barrio)
vivienda$barrio <- gsub("ò", "a", vivienda$barrio)
vivienda$barrio <- gsub("í", "i", vivienda$barrio)
vivienda$barrio  <- as.factor(vivienda$barrio)

summary(vivienda)
##        id                 zona           piso        estrato     preciom      
##  Min.   :   3   Zona Centro :  24   Min.   : 1.000   3: 639   Min.   :  58.0  
##  1st Qu.:2180   Zona Norte  :1198   1st Qu.: 2.000   4:1404   1st Qu.: 175.0  
##  Median :4158   Zona Oeste  :1029   Median : 4.000   5:1766   Median : 279.0  
##  Mean   :4284   Zona Oriente:  62   Mean   : 4.634   6:1291   Mean   : 366.9  
##  3rd Qu.:6556   Zona Sur    :2787   3rd Qu.: 6.000            3rd Qu.: 430.0  
##  Max.   :8317                       Max.   :12.000            Max.   :1950.0  
##                                     NA's   :1381                              
##    areaconst      parqueaderos        banios       habitaciones  
##  Min.   : 35.0   Min.   : 1.000   Min.   :0.000   Min.   :0.000  
##  1st Qu.: 68.0   1st Qu.: 1.000   1st Qu.:2.000   1st Qu.:3.000  
##  Median : 90.0   Median : 1.000   Median :2.000   Median :3.000  
##  Mean   :112.8   Mean   : 1.568   Mean   :2.617   Mean   :2.971  
##  3rd Qu.:130.0   3rd Qu.: 2.000   3rd Qu.:3.000   3rd Qu.:3.000  
##  Max.   :932.0   Max.   :10.000   Max.   :8.000   Max.   :9.000  
##                  NA's   :869                                     
##           tipo                 barrio        longitud         latitud     
##  apartamento:5100   valle del lili: 841   Min.   :-76.59   Min.   :3.334  
##                     la flora      : 268   1st Qu.:-76.54   1st Qu.:3.380  
##                     santa teresita: 251   Median :-76.53   Median :3.419  
##                     ciudad jardin : 230   Mean   :-76.53   Mean   :3.419  
##                     pance         : 206   3rd Qu.:-76.52   3rd Qu.:3.453  
##                     normandia     : 155   Max.   :-76.46   Max.   :3.498  
##                     (Other)       :3149

Posteriormente, se realiza una verificación de los datos faltantes para su posterior imputación.

nulos_por_fila <- apply(vivienda, 1, function(x) sum(is.na(x)))
nulos_df <- data.frame(fila = 1:nrow(vivienda), nulos = nulos_por_fila)
nulos_df <- nulos_df[order(-nulos_df$nulos),]
top_5_nulos <- head(nulos_df, 5)
print(top_5_nulos)
##    fila nulos
## 16   16     2
## 17   17     2
## 18   18     2
## 19   19     2
## 20   20     2

En este punto, se procede a verificar la cantidad de datos faltantes actuales a través de la siguiente gráfica.

rm(top_5_nulos)
captured_output <- capture.output({
  md.pattern(vivienda[, 1:13], plot = TRUE, rotate.names = TRUE)
})

Luego, se realiza la imputación de la variable “parqueaderos” mediante un código que determina la moda de los parqueaderos por zonas y asigna la cantidad en función del precio de la vivienda. La hipótesis es que las viviendas con parqueadero deben tener un precio mayor en comparación con las que no lo tienen.

vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
vivienda$NAPARQUEA = ifelse(is.na(vivienda$parqueaderos),1,0)
vivienda$costo_metro_m = vivienda$preciom / vivienda$areaconst


# Calcular la moda y la cantidad de valores faltantes en parquea
df_resultado_estrato_parquea <- vivienda %>% 
  group_by(barrio, tipo,estrato) %>%
  reframe(nv_parqueo = sum(NAPARQUEA, na.rm = TRUE),  
          N_MODE  = Mode(parqueaderos, na.rm =TRUE))

df_resultado_estrato_parquea <- df_resultado_estrato_parquea %>% 
  rename(barrio1=barrio,
         tipo1=tipo,
         estrato1=estrato)

df_resultado_estrato_parquea$N_MODE[is.na(df_resultado_estrato_parquea$N_MODE)] <- 0
df_resultado_estrato_parquea$COSTO_METRO_CUADRADO <- NA
m<-max(df_resultado_estrato_parquea$N_MODE)
for(a in 1:m){
  for (i in 1:nrow(df_resultado_estrato_parquea)) {
    filtros <- filter(vivienda,
                      barrio == df_resultado_estrato_parquea[[i, "barrio1"]]
                      ,tipo == df_resultado_estrato_parquea[[i, "tipo1"]]
                      ,estrato == df_resultado_estrato_parquea[[i, "estrato1"]]
                      ,parqueaderos == df_resultado_estrato_parquea[[i, "N_MODE"]]
    )
    if (nrow(filtros) > 0) {
      filtros$costo_metro<-filtros$preciom / filtros$areaconst
      val=sd(filtros$costo_metro)/mean(filtros$costo_metro)
      #print(paste("entro", val))
      costo_medio  <-ifelse(val>0.20,median(filtros$costo_metro, na.rm=TRUE),mean(filtros$costo_metro, na.rm=TRUE)) 
      df_resultado_estrato_parquea[i, "COSTO_METRO_CUADRADO"] <- costo_medio
    }
  }
  df_resultado_estrato_parquea<- filter(df_resultado_estrato_parquea,
                                        !is.na(COSTO_METRO_CUADRADO))
  vivienda$NAPARQUEA = ifelse(is.na(vivienda$parqueaderos),1,0)
  for (i in 1:nrow(df_resultado_estrato_parquea)) {
    vivienda$parqueaderos <- ifelse(vivienda$barrio == df_resultado_estrato_parquea[[i, "barrio1"]] &
                                           vivienda$tipo == df_resultado_estrato_parquea[[i, "tipo1"]] &
                                           vivienda$estrato == df_resultado_estrato_parquea[[i, "estrato1"]] &
                                           vivienda$costo_metro_m >= df_resultado_estrato_parquea[[i, "COSTO_METRO_CUADRADO"]] &
                                           vivienda$NAPARQUEA == 1,
                                         df_resultado_estrato_parquea[[i, "N_MODE"]],
                                         vivienda$parqueaderos)
  }
  df_resultado_estrato_parquea$N_MODE = ifelse(df_resultado_estrato_parquea$N_MODE == 0, 0 ,df_resultado_estrato_parquea$N_MODE-1)
}



df_resultado_estrato_parquea <- vivienda %>% 
  group_by(zona, tipo,estrato) %>%
  reframe(nv_parqueo = sum(NAPARQUEA, na.rm = TRUE),  
          N_MODE  = Mode(parqueaderos, na.rm =TRUE))  
df_resultado_estrato_parquea <- df_resultado_estrato_parquea %>% 
  rename(zona1=zona,
         tipo1=tipo,
         estrato1=estrato)
df_resultado_estrato_parquea$N_MODE[is.na(df_resultado_estrato_parquea$N_MODE)] <- 0
df_resultado_estrato_parquea$COSTO_METRO_CUADRADO <- NA
m<-max(df_resultado_estrato_parquea$N_MODE)
for(a in 1:m){
  for (i in 1:nrow(df_resultado_estrato_parquea)) {
    filtros <- filter(vivienda,
                      zona == df_resultado_estrato_parquea[[i, "zona1"]]
                      ,tipo == df_resultado_estrato_parquea[[i, "tipo1"]]
                      ,estrato == df_resultado_estrato_parquea[[i, "estrato1"]]
                      ,parqueaderos == df_resultado_estrato_parquea[[i, "N_MODE"]]
    )
    if (nrow(filtros) > 0) {
      filtros$costo_metro<-filtros$preciom / filtros$areaconst
      val=sd(filtros$costo_metro)/mean(filtros$costo_metro)
      #print(paste("entro", val))
      costo_medio  <-ifelse(val>0.20,median(filtros$costo_metro, na.rm=TRUE),mean(filtros$costo_metro, na.rm=TRUE)) 
      df_resultado_estrato_parquea[i, "COSTO_METRO_CUADRADO"] <- costo_medio
    }
  }
  df_resultado_estrato_parquea<- filter(df_resultado_estrato_parquea,
                                        !is.na(COSTO_METRO_CUADRADO))
  vivienda$NAPARQUEA = ifelse(is.na(vivienda$parqueaderos),1,0)
  for (i in 1:nrow(df_resultado_estrato_parquea)) {
    vivienda$parqueaderos <- ifelse(vivienda$zona == df_resultado_estrato_parquea[[i, "zona1"]] &
                                           vivienda$tipo == df_resultado_estrato_parquea[[i, "tipo1"]] &
                                           vivienda$estrato == df_resultado_estrato_parquea[[i, "estrato1"]] &
                                           vivienda$costo_metro_m >= df_resultado_estrato_parquea[[i, "COSTO_METRO_CUADRADO"]] &
                                           vivienda$NAPARQUEA == 1,
                                         df_resultado_estrato_parquea[[i, "N_MODE"]],
                                         vivienda$parqueaderos)
  }
  
  df_resultado_estrato_parquea$N_MODE = ifelse(df_resultado_estrato_parquea$N_MODE == 0, 0 ,df_resultado_estrato_parquea$N_MODE-1)
}

##asigno 0 a los demas 
vivienda$parqueaderos <- ifelse(is.na(vivienda$parqueaderos),0,vivienda$parqueaderos)
rm(df_resultado_estrato_parquea,a,costo_medio,i,m,val,filtros)


captured_output <- capture.output({
  md.pattern(vivienda[, 1:13], plot = TRUE, rotate.names = TRUE)
})

En este punto, se han imputado todos los datos de la variable “parqueadero”; sin embargo, aún se observan valores nulos en la variable “piso”. Por lo tanto, se realiza un proceso similar al utilizado para la imputación de parqueaderos, identificando las zonas por latitud y longitud. Se asume que, en dos apartamentos ubicados en el mismo edificio, los pisos superiores tienen un costo mayor.

## tratamiento de piso 
# Calcular la moda y la cantidad de valores faltantes en piso
#rm(df_resultado_piso)
vivienda$todos = 1
filtro<-filter(vivienda,tipo == "apartamento")
df_resultado_piso <- filtro %>% 
  group_by(latitud, longitud, estrato) %>%
  summarise(nv_piso = sum(NAPISO),
            num_total = sum(todos))
## `summarise()` has grouped output by 'latitud', 'longitud'. You can override
## using the `.groups` argument.
df_resultado_piso$DIF <- df_resultado_piso$nv_piso - df_resultado_piso$num_total
df_resultado_piso <- filter(df_resultado_piso,DIF<0)
df_resultado_piso <- filter(df_resultado_piso,nv_piso!=0)
#print(sum(df_resultado_piso$nv_piso))
for(a in 1:nrow(df_resultado_piso)){
  filtro<- filter(vivienda,
                  latitud == df_resultado_piso[[a,"latitud"]] &
                    longitud == df_resultado_piso[[a,"longitud"]] &
                    tipo == "apartamento" &
                    estrato == df_resultado_piso[[a,"estrato"]]
  )
  listadepisos=sort(unique(filtro$piso))
  elmax=max(listadepisos)
  for(i in listadepisos){
    #print(paste("iter ", a," piso ",i))
    filtroa<-filter(vivienda,
                    piso==i &
                      latitud == df_resultado_piso[[a,"latitud"]] &
                      longitud == df_resultado_piso[[a,"longitud"]] &
                      tipo == "apartamento" &
                      estrato == df_resultado_piso[[a,"estrato"]])
    if(nrow(filtroa)>0){
      val = sd(filtroa$costo_metro_m)/mean(filtroa$costo_metro_m)
      cost= ifelse(val>0.20 | is.na(val) ,median(filtroa$costo_metro_m),mean(filtroa$costo_metro_m))
      
      vivienda$piso <- ifelse(vivienda$costo_metro_m<=cost &
                                          vivienda$NAPISO == 1 &
                                          vivienda$latitud == df_resultado_piso[[a,"latitud"]] &
                                          vivienda$longitud == df_resultado_piso[[a,"longitud"]] &
                                          vivienda$tipo == "apartamento" &
                                          vivienda$estrato == df_resultado_piso[[a,"estrato"]]
                                        ,
                                        ifelse(i==1,1,i-1),vivienda$piso) 
      vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
      #print(sum(vivienda$NAPISO))
      if(i==elmax){
        #print(paste("elmax ",elmax))
        vivienda$piso <-ifelse(vivienda$costo_metro_m>cost &
                                           vivienda$NAPISO == 1 &
                                           i==elmax &
                                           vivienda$latitud == df_resultado_piso[[a,"latitud"]] &
                                           vivienda$longitud == df_resultado_piso[[a,"longitud"]] &
                                           vivienda$tipo == "apartamento" &
                                           vivienda$estrato == df_resultado_piso[[a,"estrato"]]
                                         ,
                                         i+1,vivienda$piso)
        vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
        #print(sum(vivienda$NAPISO))
      }
    }
  }
}


vivienda$todos = 1
filtro<-filter(vivienda,tipo == "apartamento")
df_resultado_piso <- filtro %>% 
  group_by(barrio, estrato) %>%
  summarise(nv_piso = sum(NAPISO),
            num_total = sum(todos))
## `summarise()` has grouped output by 'barrio'. You can override using the
## `.groups` argument.
df_resultado_piso$DIF <- df_resultado_piso$nv_piso - df_resultado_piso$num_total
df_resultado_piso <- filter(df_resultado_piso,DIF<0)
df_resultado_piso <- filter(df_resultado_piso,nv_piso!=0)
#print(sum(df_resultado_piso$nv_piso))
for(a in 1:nrow(df_resultado_piso)){
  filtro<- filter(vivienda,
                  barrio == df_resultado_piso[[a,"barrio"]] &
                    tipo == "apartamento" &
                    estrato == df_resultado_piso[[a,"estrato"]]
  )
  listadepisos=sort(unique(filtro$piso))
  elmax=max(listadepisos)
  for(i in listadepisos){
    #print(paste("iter ", a," piso ",i))
    filtroa<-filter(vivienda,
                    piso==i &
                      barrio == df_resultado_piso[[a,"barrio"]] &
                      tipo == "apartamento" &
                      estrato == df_resultado_piso[[a,"estrato"]])
    if(nrow(filtroa)>0){
      val = sd(filtroa$costo_metro_m)/mean(filtroa$costo_metro_m)
      cost= ifelse(val>0.20 | is.na(val) ,median(filtroa$costo_metro_m),mean(filtroa$costo_metro_m))
      
      vivienda$piso <- ifelse(vivienda$costo_metro_m<=cost &
                                          vivienda$NAPISO == 1 &
                                          vivienda$barrio == df_resultado_piso[[a,"barrio"]] &
                                          vivienda$tipo == "apartamento" &
                                          vivienda$estrato == df_resultado_piso[[a,"estrato"]]
                                        ,
                                        ifelse(i==1,1,i-1),vivienda$piso) 
      #print("aca1")
      vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
      #print(sum(vivienda$NAPISO))
      if(i==elmax){
        #print(paste("elmax ",elmax))
        vivienda$piso <-ifelse(vivienda$costo_metro_m>cost &
                                           vivienda$NAPISO == 1 &
                                           i==elmax &
                                           vivienda$barrio == df_resultado_piso[[a,"barrio"]] &
                                           vivienda$tipo == "apartamento" &
                                           vivienda$estrato == df_resultado_piso[[a,"estrato"]]
                                         ,
                                         i+1,vivienda$piso)
        vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
        #print(sum(vivienda$NAPISO))
      }
    }
  }
}


rm(filtro,filtroa,df_resultado_piso,cost,elmax,elmin,listadepisos,a,i,val)
## Warning in rm(filtro, filtroa, df_resultado_piso, cost, elmax, elmin,
## listadepisos, : objeto 'elmin' no encontrado
vivienda$piso<-ifelse(is.na(vivienda$piso),1,vivienda$piso)
vivienda <- subset(vivienda, select = -c(NAPISO,NAPARQUEA,costo_metro_m,todos))

captured_output <- capture.output({
  md.pattern(vivienda[, 1:13], plot = TRUE, rotate.names = TRUE)
})

En este punto, se ha completado la imputación de los datos y la base de datos está lista para realizar los respectivos análisis.

Modelo de regresión lineal multiple

Con el objetivo de predecir el precio de las viviendas, se utiliza un modelo de regresión lineal múltiple. Para ello, primero se debe realizar un análisis bivariado para observar las relaciones entre las variables del conjunto de datos, seguido del planteamiento del modelo y la validación de sus supuestos. Finalmente, se realiza una validación cruzada para revisar la eficiencia del modelo.

Análisis Bivariado

Para comenzar, se estudian las relaciones entre las variables numéricas utilizando la correlación de Spearman.

rm(nulos_df)
numeric_var <- vivienda %>% select(piso, preciom, areaconst, parqueaderos, banios, habitaciones) 
cor_matrix_spearman <- cor(numeric_var, method = "spearman", use = "complete.obs")
print(cor_matrix_spearman)  
##                      piso   preciom  areaconst parqueaderos    banios
## piso          1.000000000 0.1880468 0.08720664    0.1542436 0.1223430
## preciom       0.188046799 1.0000000 0.88957067    0.7664321 0.7716120
## areaconst     0.087206636 0.8895707 1.00000000    0.7015856 0.7995163
## parqueaderos  0.154243630 0.7664321 0.70158564    1.0000000 0.6502720
## banios        0.122343044 0.7716120 0.79951634    0.6502720 1.0000000
## habitaciones -0.002210114 0.3258252 0.46750971    0.2965316 0.4887431
##              habitaciones
## piso         -0.002210114
## preciom       0.325825237
## areaconst     0.467509708
## parqueaderos  0.296531617
## banios        0.488743079
## habitaciones  1.000000000
ggpairs(numeric_var, title=" ")

Se observa que no existe una alta correlación entre las variables predictoras.

A continuación, se realiza un gráfico de dispersión que muestra la relación entre el precio de la vivienda y el área construida. Los colores de los puntos representan el estrato.

p1 <- ggplot(vivienda, aes(x = areaconst, y = preciom, color = factor(estrato))) +
  geom_point(alpha = 0.7) +
  labs(title = "Precio vs Área Construida",
       x = "Área Construida",
       y = "Precio",
       color = "Estrato")

ggplotly(p1)

En este gráfico se observa un comportamiento similar entre los estratos 3 y 4.

Posteriormente, se realizan gráficos de cajas para relacionar distintas variables con el precio.

p2 <- ggplot(vivienda, aes(x = factor(estrato), y = preciom, fill = factor(estrato))) +
  geom_boxplot() +
  labs(title = "Distribución del Precio por Estrato",
       x = "Estrato",
       y = "Precio",
       fill = "Estrato")
ggplotly(p2)

De esta gráfica se puede inferir que la relación entre estrato y precio es más fuerte en los estratos 5 y 6, mientras que los estratos 3 y 4 no muestran una diferencia significativa entre ellos.

p3 <- ggplot(vivienda, aes(x = factor(habitaciones), y = preciom, fill = factor(habitaciones))) +
  geom_boxplot() +
  labs(title = "Distribución del Precio por Número de Habitaciones",
       x = "Número de Habitaciones",
       y = "Precio",
       fill = "Número de Habitaciones")
ggplotly(p3)

Al observar la relación entre el precio y el número de habitaciones, no se detecta una clara tendencia, principalmente debido a la presencia de valores atípicos en el precio.

p4 <- ggplot(vivienda, aes(x = factor(banios), y = preciom, color = factor(banios))) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "Precio vs Número de Baños",
       x = "Número de Baños",
       y = "Precio",
       color = "Baños")
ggplotly(p4)

En el caso de los baños, se distingue claramente una relación directamente proporcional entre la cantidad de baños y el precio de la vivienda, con una excepción para aquellas viviendas que reportan 0 baños.

p5 <- ggplot(vivienda, aes(x = factor(parqueaderos), y = preciom, color = factor(parqueaderos))) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "Precio vs Número de Parqueaderos",
       x = "Número de Parqueaderos",
       y = "Precio",
       color = "Parqueaderos")
ggplotly(p5)

En cuanto al número de parqueaderos, se observa nuevamente una tendencia lineal, aunque esta tendencia no se mantiene a partir de los 6 parqueaderos.

p5 <- ggplot(vivienda, aes(x = factor(zona), y = preciom, color = factor(zona))) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "Precio vs Zona",
       x = "Zona",
       y = "Precio",
       color = "Zona")
ggplotly(p5)

Finalmente, al analizar la relación con las zonas, se aprecia que los rangos de precios coinciden en varias zonas, aunque la Zona Oeste destaca por tener precios más elevados que las demás.

Con esta información y observando que no existe una alta correlación entre las variables del modelo, se procede a utilizarlas todas en el planteamiento del mismo.

Modelo Lineal Múltiple

A continuación, se realiza el modelo de regresión lineal múltiple, utilizando las variables categóricas como factores, lo que permite realizar una codificación creando variables dicotómicas, donde el propio modelo establece el valor de referencia.

##### modelo sin ajustes 
modelo <- lm(preciom ~ areaconst + factor(estrato) + habitaciones + parqueaderos + banios + factor(zona), data = vivienda)
summary(modelo)
## 
## Call:
## lm(formula = preciom ~ areaconst + factor(estrato) + habitaciones + 
##     parqueaderos + banios + factor(zona), data = vivienda)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1605.06   -48.56     1.70    43.20   950.70 
## 
## Coefficients:
##                           Estimate Std. Error t value Pr(>|t|)    
## (Intercept)              -71.30498   26.85398  -2.655 0.007949 ** 
## areaconst                  1.93755    0.04045  47.896  < 2e-16 ***
## factor(estrato)4          13.04814    6.62160   1.971 0.048830 *  
## factor(estrato)5          24.39810    6.65972   3.664 0.000251 ***
## factor(estrato)6         145.43353    8.52943  17.051  < 2e-16 ***
## habitaciones             -29.59047    3.11535  -9.498  < 2e-16 ***
## parqueaderos              69.00524    3.02873  22.784  < 2e-16 ***
## banios                    44.80714    2.85292  15.706  < 2e-16 ***
## factor(zona)Zona Norte    43.05528   25.83334   1.667 0.095645 .  
## factor(zona)Zona Oeste    97.09698   26.17314   3.710 0.000210 ***
## factor(zona)Zona Oriente  -0.33626   30.11727  -0.011 0.991092    
## factor(zona)Zona Sur      30.51200   25.79874   1.183 0.236986    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 124.9 on 5088 degrees of freedom
## Multiple R-squared:  0.8138, Adjusted R-squared:  0.8134 
## F-statistic:  2022 on 11 and 5088 DF,  p-value: < 2.2e-16

En este punto, se describe cada uno de los coeficientes obtenidos en el modelo de regresión lineal múltiple, su nivel de significancia y, por lo tanto, su relevancia para el modelo:

  • Intercepto (BO): Su valor es de -71.30 millones, el intercepto representa el valor de la vivienda cuando las demas variables se igualan a 0, Aunque su valor no tiene un significado práctico en este contexto, sigue siendo estadísticamente significativo con un P value de 0.0079.

  • Área construida (B1): Este coeficiente muestra que por cada metro cuadrado adicional en el área construida, el precio de la vivienda aumenta en 1.94 millones. Este coeficiente es altamente significativo con un p value <2e-16, lo que sugiere que el área construida es una variable muy relevante en la predicción del precio.

  • Estrato 4 (B2): Las viviendas de estrato 4 tienen un precio promedio 13.05 millones mayor en comparación con las viviendas de estrato 3 (considerado como referencia). Aunque el coeficiente es significativo, su valor es relativamente bajo en comparación con otros estratos el pa value de este estrato es de 0.0488.

  • Estrato 5 (B3): Las viviendas de estrato 5 tienen un precio promedio 24.40 millones mayor en comparación con las viviendas de estrato 3. Este coeficiente es estadísticamente significativo con un o value de 0.00025 y muestra una diferencia notable en los precios respecto a los estratos bajos.

  • Estrato 6 (B4): Las viviendas de estrato 6 tienen un precio promedio 145.43 millones mayor en comparación con las viviendas de estrato 3. Este coeficiente es altamente significativo con un p value < 2e-16, lo que resalta el impacto del estrato en el precio de la vivienda.

  • Número de habitaciones (B5): Este coeficiente tiene un valor negativo -29.59 millones, lo que indica que un mayor número de habitaciones está asociado con una disminución en el precio de la vivienda. Aunque es estadísticamente significativo (p value < 2e-16), esta relación negativa es contraria a la intuición y podría ser un indicativo de multicolinealidad o algún problema en el modelo.

  • Número de parqueaderos (B6): Cada parqueadero adicional está asociado con un incremento de 69.01 millones en el precio de la vivienda. Este coeficiente es muy significativo con un p value < 2e-16, lo que sugiere que los parqueaderos son una característica importante en la valoración de la vivienda en Cali.

  • Número de baños (B7): Cada baño adicional aumenta el precio de la vivienda en 44.81 millones. Este coeficiente también es muy significativo con un p value < 2e-16, indicando que los baños son una característica clave en la determinación del precio de las viviendas en Cali.

  • Zona Norte (B8): Las viviendas en la Zona Norte tienen un precio promedio 43.06 millones mayor en comparación con la zona de referencia (Zona Centro). Este coeficiente es poco significativo con un pvalue de 0.0956, lo que sugiere que la ubicación en esta zona podría tener un impacto en el precio, aunque no es tan fuerte como en otras zonas.

  • Zona Oeste (B9): Las viviendas en la Zona Oeste tienen un precio promedio 97.10 millones mayor en comparación con la zona de referencia (Zona Centro). Este coeficiente es altamente significativo con un p value de 0.00021, lo que muestra que la ubicación en esta zona tiene un gran impacto en el precio.

  • Zona Oriente (B10): El precio de las viviendas en la Zona Oriente no presenta una diferencia significativa en comparación con la zona de referencia, con un valor de -0.34 millones. Este coeficiente no es estadísticamente significativo con un valor p de 0.991, lo que puede deberse a la proporción de viviendas ubicadas en esta zona.

 viviendas_por_zona <- vivienda %>%
     group_by(zona) %>%
     summarize(total_viviendas = n())

# Mostrar el resultado
print(viviendas_por_zona)
## # A tibble: 5 × 2
##   zona         total_viviendas
##   <fct>                  <int>
## 1 Zona Centro               24
## 2 Zona Norte              1198
## 3 Zona Oeste              1029
## 4 Zona Oriente              62
## 5 Zona Sur                2787
  • Zona Sur (B11): Las viviendas en la Zona Sur tienen un precio promedio 30.51 millones mayor en comparación con la zona de referencia (Zona Centro). Este coeficiente es poco significativo con un valor p de 0.237, lo que indica que vivir en esta zona no tiene un gran impacto en el precio de las viviendas.

Dado que el coeficiente asociado al número de habitaciones es negativo, lo cual podría indicar la presencia de multicolinealidad, se procede a verificar esta situación. Al observar que todos los valores del VIF (Factor de Inflación de la Varianza) son menores a 5, se descarta la presencia significativa de multicolinealidad en el modelo.

vif(modelo)
##                     GVIF Df GVIF^(1/(2*Df))
## areaconst       2.572362  1        1.603859
## factor(estrato) 2.719155  3        1.181424
## habitaciones    1.448997  1        1.203743
## parqueaderos    2.056435  1        1.434028
## banios          3.037143  1        1.742740
## factor(zona)    1.754589  4        1.072808

Finalmente, se realiza la validación de los supuestos del modelo.

Validación de los supuestos

## no son lineales 
# Gráfico de residuos estandarizados vs. valores ajustados
plot(modelo$fitted.values, rstandard(modelo))
abline(h = 0, col = "red")

###varianza constante 
##la varianza de los errores no es constante.
lmtest::bptest(modelo)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo
## BP = 1440, df = 11, p-value < 2.2e-16
##normalidad 
##los errores del modelo no siguen una distribución normal.
qqnorm(residuals(modelo))
qqline(residuals(modelo), col = "red")

residuals_subset <- sample(modelo$residuals, size = 5000)
shapiro.test(residuals_subset)
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals_subset
## W = 0.82498, p-value < 2.2e-16
#existe autocorrelación positiva en los residuos del modelo.
lmtest::dwtest(modelo)
## 
##  Durbin-Watson test
## 
## data:  modelo
## DW = 1.7422, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0

Los análisis y gráficos presentados anteriormente indican que el modelo no cumple con varios de los supuestos fundamentales. El gráfico de residuos estandarizados frente a valores ajustados muestra una forma de cono, lo que sugiere la presencia de heterocedasticidad. Además, el test de Breusch-Pagan arroja un valor p menor a 0.05, confirmando que la varianza no es constante. Tanto el gráfico QQ plot como el test de Shapiro-Wilk revelan que los residuos no siguen una distribución normal. Por último, el test de Durbin-Watson evidencia la existencia de autocorrelación en los errores.

Para corregir los incumplimientos de estos supuestos, se sugieren las siguientes acciones:

  • Normalidad de los errores: Se pueden emplear modelos de regresión robustos o pruebas no paramétricas que no dependen del supuesto de normalidad de los residuos.

  • Heterocedasticidad: Se recomienda aplicar modelos de regresión ponderada, donde se asignen menores pesos a las observaciones con mayor varianza.

  • Autocorrelación: Para abordar la autocorrelación en los errores, se podrían utilizar métodos como los mínimos cuadrados generalizados o modelos de series temporales.

Proceso de Validación Cruzada

Finalmente, para poner a prueba el modelo, se procede a realizar un proceso de validación cruzada con el modelo ajustado. Para ello, se realiza una partición de los datos en un 70% para el entrenamiento y un 30% para la prueba.

entrenamientoin <- createDataPartition(vivienda$preciom, p = 0.7, list = FALSE)
dataentrenamiento <- vivienda[entrenamientoin, ]
dataprueba <- vivienda[-entrenamientoin, ]
modelo_entre <- lm(preciom ~ areaconst + factor(estrato) + habitaciones + parqueaderos + banios + factor(zona), data = dataentrenamiento)
summary(modelo_entre)
## 
## Call:
## lm(formula = preciom ~ areaconst + factor(estrato) + habitaciones + 
##     parqueaderos + banios + factor(zona), data = dataentrenamiento)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1601.74   -50.31     2.64    44.46   923.54 
## 
## Coefficients:
##                          Estimate Std. Error t value Pr(>|t|)    
## (Intercept)               -65.040     32.822  -1.982  0.04760 *  
## areaconst                   1.933      0.049  39.455  < 2e-16 ***
## factor(estrato)4            9.856      8.050   1.224  0.22093    
## factor(estrato)5           22.057      8.106   2.721  0.00654 ** 
## factor(estrato)6          149.695     10.341  14.476  < 2e-16 ***
## habitaciones              -35.585      3.850  -9.244  < 2e-16 ***
## parqueaderos               68.784      3.606  19.075  < 2e-16 ***
## banios                     47.447      3.438  13.800  < 2e-16 ***
## factor(zona)Zona Norte     53.571     31.905   1.679  0.09323 .  
## factor(zona)Zona Oeste     97.835     32.315   3.028  0.00248 ** 
## factor(zona)Zona Oriente    8.806     36.574   0.241  0.80976    
## factor(zona)Zona Sur       37.531     31.898   1.177  0.23944    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 125.8 on 3560 degrees of freedom
## Multiple R-squared:  0.8106, Adjusted R-squared:   0.81 
## F-statistic:  1385 on 11 and 3560 DF,  p-value: < 2.2e-16

En este análisis, se observan resultados similares a los mostrados en el planteamiento del modelo inicial. Ahora se utiliza el modelo y el conjunto de prueba para realizar las estimaciones, las cuales se almacenan en la variable predic1. Con esta información, se procede a calcular algunas métricas de desempeño.

predic <- predict(modelo_entre, newdata = dataprueba)

# Calcular el Error Cuadrático Medio (RMSE)
rmse <- sqrt(mean((predic - dataprueba$preciom)^2))
# Calcular el Error Absoluto Medio (MAE)
mae <- mean(abs(predic - dataprueba$preciom))
# Calcular el coeficiente de determinación R2

rss <- sum((predic - dataprueba$preciom)^2)  # Residual Sum of Squares
tss <- sum((dataprueba$preciom - mean(dataprueba$preciom))^2)  # Total Sum of Squares
r2 <- 1 - (rss / tss)

# Mostrar los resultados
cat("RMSE: ", rmse, "\n")
## RMSE:  123.194
cat("MAE: ", mae, "\n")
## MAE:  74.73336
cat("R2: ", r2, "\n")
## R2:  0.8204521

El valor del RMSE sugiere que el precio de las viviendas puede variar hasta 123.19 millones. Es importante tener en cuenta que esta medida penaliza en mayor medida los errores grandes debido al cuadrado de las diferencias. Algunos precios de las viviendas parecen ser extremadamente altos, lo que podría contribuir a esta gran variación. Considerando las medidas de tendencia central presentadas al inicio del documento, con una media de 366.9 millones y una mediana de 279 millones, esto implica que la variación puede generar errores del 33.57% al 44.15%.

En cuanto al MAE, que penaliza menos los errores grandes, el valor de la vivienda muestra variaciones de hasta 74.73 millones. Nuevamente, teniendo en cuenta las medidas de tendencia central, esto varía desde el 20.36% hasta el 26.78%.

Finalmente, en relación con el conjunto de prueba, el valor del R² indica que el modelo explica el 82.04% de la variabilidad en el precio de las viviendas.

Conclusiones

  1. Desempeño del Modelo: El modelo de regresión lineal múltiple tiene un buen desempeño, con un R² del 82.04%, pero aún puede mejorarse.

  2. Importancia de las Variables: El área construida, el estrato socioeconómico, y el número de parqueaderos son los principales factores que afectan el precio, con el área y los parqueaderos elevando el precio, mientras que los estratos y zonas también juegan un papel importante.

  3. Problemas con los Supuestos: Se identificaron violaciones a los supuestos del modelo, como heterocedasticidad y autocorrelación, sugiriendo la necesidad de ajustes o técnicas alternativas.

  4. Validación Cruzada: El modelo se generaliza bien a datos nuevos, pero las métricas de error (RMSE y MAE) indican que hay margen para mejorar la precisión de las predicciones.

  5. Recomendaciones: Se sugiere explorar transformaciones de variables, utilizar modelos robustos, y ajustar el modelo eliminando variables no significativas.