Sys.setlocale("LC_ALL", "Spanish_Colombia.UTF-8")
## [1] "LC_COLLATE=Spanish_Colombia.utf8;LC_CTYPE=Spanish_Colombia.utf8;LC_MONETARY=Spanish_Colombia.utf8;LC_NUMERIC=C;LC_TIME=Spanish_Colombia.utf8"

1 1. Introducción

En este informe se desarrolla un análisis de regresión lineal múltiple para apoyar la toma de decisiones de la empresa C&A en la selección de dos viviendas para una compañía internacional. El objetivo es construir modelos que permitan explicar y predecir el precio de viviendas a partir de características como el área construida, estrato, número de habitaciones, parqueaderos y baños.

Además del modelado, se realiza un proceso de revisión de calidad de datos, que incluye inspección de valores faltantes, imputación cuando sea necesaria y detección de valores atípicos. Esto es importante porque la calidad de los datos influye directamente en la estabilidad y capacidad predictiva del modelo.

Finalmente, con base en las predicciones obtenidas, se identifican ofertas potenciales que se ajusten a las restricciones de cada solicitud y se veran en mapas.

2 2. Objetivos del análisis

2.1 2.1 Objetivo general

Estimar y validar modelos de regresión lineal múltiple para predecir el precio de viviendas y recomendar ofertas potenciales para dos solicitudes específicas del caso C&A.

2.2 2.2 Objetivos específicos

  1. Filtrar la base de datos de acuerdo con las características de cada solicitud.
  2. Realizar un análisis exploratorio de datos con apoyo de visualizaciones interactivas.
  3. Revisar la calidad de los datos mediante búsqueda de faltantes y valores atípicos.
  4. Estimar modelos de regresión lineal múltiple.
  5. Validar supuestos y medir el rendimiento predictivo del modelo.
  6. Generar predicciones para las viviendas solicitadas.
  7. Identificar ofertas potenciales dentro del presupuesto y mostrarlas en mapa.

3 3. Carga de paquetes y datos

# Paquetes 
library(paqueteMODELOS)
library(dplyr)
library(ggplot2)
library(plotly)
library(leaflet)
library(caret)
library(tidyr)
library(knitr)
library(kableExtra)
library(lmtest)
library(car)
#library(MASS)
data("vivienda")

# Revisar estructura general
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id          : num [1:8322] 1147 1169 1350 5992 1212 ...
##  $ zona        : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
##  $ piso        : chr [1:8322] NA NA NA "02" ...
##  $ estrato     : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
##  $ preciom     : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
##  $ areaconst   : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
##  $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
##  $ banios      : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
##  $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
##  $ tipo        : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
##  $ barrio      : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
##  $ longitud    : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
##  $ latitud     : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
##  - attr(*, "spec")=List of 3
##   ..$ cols   :List of 13
##   .. ..$ id          : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ zona        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ piso        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ estrato     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ preciom     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ areaconst   : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ parqueaderos: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ banios      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ habitaciones: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ tipo        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ barrio      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ longitud    : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ latitud     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   ..$ default: list()
##   .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
##   ..$ delim  : chr ";"
##   ..- attr(*, "class")= chr "col_spec"
##  - attr(*, "problems")=<externalptr>
dim(vivienda)
## [1] 8322   13

4 4. Diccionario breve de variables

Las variables principales usadas en este trabajo son:

  • preciom: precio de la vivienda en millones de pesos.
  • areaconst: área construida.
  • estrato: nivel socioeconómico.
  • parqueaderos: número de parqueaderos.
  • banios: número de baños.
  • habitaciones: número de habitaciones.
  • tipo: tipo de vivienda.
  • zona: zona de ubicación.
  • latitud y longitud: coordenadas geográficas.

5 5. Funciones auxiliares

#   Moda 
moda <- function(x) {
  ux <- unique(na.omit(x))
  ux[which.max(tabulate(match(x, ux)))]
}

# Imputaciones

imputar_datos <- function(df) {
  df2 <- df
  
  for (col in names(df2)) {
    if (is.numeric(df2[[col]])) {
      if (any(is.na(df2[[col]]))) {
        df2[[col]][is.na(df2[[col]])] <- median(df2[[col]], na.rm = TRUE)
      }
    } else {
      if (any(is.na(df2[[col]]))) {
        df2[[col]][is.na(df2[[col]])] <- moda(df2[[col]])
      }
    }
  }
  
  return(df2)
}

#IQR
marcar_outliers_iqr <- function(x) {
  q1 <- quantile(x, 0.25, na.rm = TRUE)
  q3 <- quantile(x, 0.75, na.rm = TRUE)
  iqr <- q3 - q1
  li <- q1 - 1.5 * iqr
  ls <- q3 + 1.5 * iqr
  return(ifelse(x < li | x > ls, 1, 0))
}

# MMetricias
metricas_modelo <- function(real, pred) {
  data.frame(
    RMSE = RMSE(pred, real),
    MAE  = MAE(pred, real),
    R2   = R2(pred, real)
  )
}

6 6. Metodología general

Se trabajará con dos subconjuntos de la base:

  • Caso 1: casas en zona norte.
  • Caso 2: apartamentos en zona sur.

Para cada caso se seguirá esta secuencia:

  1. Filtro y comprobación del subconjunto.
  2. Revisión de calidad de datos.
  3. Imputación de faltantes si existen.
  4. Detección de valores atípicos.
  5. Análisis exploratorio con gráficos interactivos.
  6. Separación en entrenamiento y prueba.
  7. Estimación del modelo de regresión lineal múltiple.
  8. Comparación entre un modelo base y uno sin outliers.
  9. Validación de supuestos.
  10. Predicción de la vivienda solicitada.
  11. Selección de ofertas potenciales y visualización en mapa.

7 7. Caso 1: Vivienda 1 (Casa en zona norte)

7.1 7.1 Filtro de la base

base1 <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte")

dim(base1)
## [1] 722  13
head(base1, 3) %>%
  kable(caption = "Primeros 3 registros de base1") %>%
  kable_styling(full_width = FALSE)
Primeros 3 registros de base1
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
1209 Zona Norte 02 5 320 150 2 4 6 Casa acopi -76.51341 3.47968
1592 Zona Norte 02 5 780 380 2 3 3 Casa acopi -76.51674 3.48721
4057 Zona Norte 02 6 750 445 NA 7 6 Casa acopi -76.52950 3.38527

7.1.1 Tablas de comprobación del filtro

table(base1$tipo)
## 
## Casa 
##  722
table(base1$zona)
## 
## Zona Norte 
##        722

7.2 7.2 Mapa

base1_mapa <- base1 %>%
  filter(!is.na(latitud), !is.na(longitud))

leaflet(base1_mapa) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 4,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio (millones):</b> ", preciom,
      "<br><b>Área:</b> ", areaconst,
      "<br><b>Estrato:</b> ", estrato
    )
  )

El mapa permite verificar si las coordenadas realmente se concentran en la zona norte. Hay datos fuera de la zona, esto podría deberse a errores de digitación o problemas en la georreferenciacion.

7.2.1 Chequeo de coordenadas externas

base1_mapa %>%
  arrange(latitud) %>%
   dplyr::select(barrio, latitud, longitud) %>%
  head(10)
## # A tibble: 10 × 3
##    barrio    latitud longitud
##    <chr>       <dbl>    <dbl>
##  1 juanamb√∫    3.33    -76.5
##  2 Cali         3.34    -76.5
##  3 Cali         3.34    -76.6
##  4 acopi        3.35    -76.5
##  5 Cali         3.35    -76.5
##  6 Cali         3.35    -76.5
##  7 acopi        3.35    -76.5
##  8 acopi        3.35    -76.5
##  9 acopi        3.36    -76.5
## 10 acopi        3.36    -76.5
base1 %>%
  arrange(desc(latitud)) %>%
   dplyr::select(barrio, latitud, longitud) %>%
  head(10)
## # A tibble: 10 × 3
##    barrio          latitud longitud
##    <chr>             <dbl>    <dbl>
##  1 alameda del río    3.50    -76.5
##  2 floralia           3.49    -76.5
##  3 brisas de los      3.49    -76.5
##  4 brisas de los      3.49    -76.5
##  5 brisas de los      3.49    -76.5
##  6 floralia           3.49    -76.5
##  7 menga              3.49    -76.5
##  8 brisas de los      3.49    -76.5
##  9 zona norte         3.49    -76.5
## 10 brisas de los      3.49    -76.5

7.2.1.1 Ajuste de coordenadas fuera de la zona “norte”

q1_lat <- quantile(base1$latitud, 0.25, na.rm = TRUE)
q3_lat <- quantile(base1$latitud, 0.75, na.rm = TRUE)
iqr_lat <- q3_lat - q1_lat

li_lat <- q1_lat - 1.5 * iqr_lat
ls_lat <- q3_lat + 1.5 * iqr_lat

q1_lon <- quantile(base1$longitud, 0.25, na.rm = TRUE)
q3_lon <- quantile(base1$longitud, 0.75, na.rm = TRUE)
iqr_lon <- q3_lon - q1_lon

li_lon <- q1_lon - 1.5 * iqr_lon
ls_lon <- q3_lon + 1.5 * iqr_lon

base1_geo <- base1 %>%
  dplyr::filter(
    latitud >= li_lat,
    latitud <= ls_lat,
    longitud >= li_lon,
    longitud <= ls_lon
  )

Se hace un ajuste a las coordenadas para omitir aquellos datos que no pertenecen al norte

leaflet(base1_geo) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 4,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom
    )
  )

Ahora si tenemos un mapa con toda la distribucion lo mas cercana a la realidad de la zona norte

7.3 7.3 Revisión de calidad de datos

7.3.1 Valores faltantes

na_base1 <- sapply(base1, function(x) sum(is.na(x)))
na_base1_df <- data.frame(variable = names(na_base1), faltantes = as.numeric(na_base1))

na_base1_df %>%
  kable(caption = "Valores faltantes en base1") %>%
  kable_styling(full_width = FALSE)
Valores faltantes en base1
variable faltantes
id 0
zona 0
piso 372
estrato 0
preciom 0
areaconst 0
parqueaderos 287
banios 0
habitaciones 0
tipo 0
barrio 0
longitud 0
latitud 0

7.3.2 Explicación de imputación

En un análisis de regresión, los valores faltantes pueden reducir el tamaño de la muestra o generar errores si no se tratan.

  • variables numéricas( Piso y Parqueaderos ) : mediana

Se usa la mediana porque es la forma mas robusta de trabajar con numericos extremos

base1_imp <- imputar_datos(base1)

na_base1_despues <- sapply(base1_imp, function(x) sum(is.na(x)))
data.frame(variable = names(na_base1_despues),
           faltantes_despues = as.numeric(na_base1_despues)) %>%
  kable(caption = "Valores faltantes después de imputación en base1") %>%
  kable_styling(full_width = FALSE)
Valores faltantes después de imputación en base1
variable faltantes_despues
id 0
zona 0
piso 0
estrato 0
preciom 0
areaconst 0
parqueaderos 0
banios 0
habitaciones 0
tipo 0
barrio 0
longitud 0
latitud 0

7.4 7.4 Búsqueda de outliers

En este punto no se eliminan automáticamente los valores atípicos. Primero se identifican para evaluar si podrían afectar demasiado el modelo. Esto es útil porque en vivienda sí pueden existir precios altos o áreas grandes reales, y no todo outlier es un error pueden ser casas premium

vars_num1 <- c("preciom", "areaconst", "estrato", "parqueaderos", "banios", "habitaciones")

outliers_base1 <- base1_imp %>%
  mutate(
    out_preciom      = marcar_outliers_iqr(preciom),
    out_areaconst    = marcar_outliers_iqr(areaconst),
    out_estrato      = marcar_outliers_iqr(estrato),
    out_parqueaderos = marcar_outliers_iqr(parqueaderos),
    out_banios       = marcar_outliers_iqr(banios),
    out_habitaciones = marcar_outliers_iqr(habitaciones)
  ) %>%
  mutate(
    total_outliers = out_preciom + out_areaconst + out_estrato +
      out_parqueaderos + out_banios + out_habitaciones
  )

resumen_outliers1 <- data.frame(
  variable = c("preciom", "areaconst", "estrato", "parqueaderos", "banios", "habitaciones"),
  cantidad_outliers = c(
    sum(outliers_base1$out_preciom),
    sum(outliers_base1$out_areaconst),
    sum(outliers_base1$out_estrato),
    sum(outliers_base1$out_parqueaderos),
    sum(outliers_base1$out_banios),
    sum(outliers_base1$out_habitaciones)
  )
)

resumen_outliers1 %>%
  kable(caption = "Cantidad de outliers detectados en base1") %>%
  kable_styling(full_width = FALSE)
Cantidad de outliers detectados en base1
variable cantidad_outliers
preciom 31
areaconst 26
estrato 0
parqueaderos 277
banios 14
habitaciones 27

7.4.1 Base alternativa sin registros extremadamente atípicos

Dado que hay varios outliers no sabemos si estos casos se dan por que son viviendas de lujo o son errores en la data, se va a crear dos bases para entender si hay diferencia entre usar la data con o sin atipicos

base1_clean <- outliers_base1 %>%
  filter(total_outliers == 0)

dim(base1_imp)
## [1] 722  13
dim(base1_clean)
## [1] 412  20

7.5 7.5 Análisis exploratorio de datos

El objetivo aquí es estudiar cómo cambia el precio con las variables explicativas. Esto ayuda a entender si el uso de una regresión lineal tiene sentido.

7.5.1 Precio vs área construida

plot_ly(
  data = base1_imp,
  x = ~areaconst,
  y = ~preciom,
  type = "scatter",
  mode = "markers",
  color = ~as.factor(estrato),
  text = ~paste("Barrio:", barrio,
                "<br>Habitaciones:", habitaciones,
                "<br>Baños:", banios),
  hoverinfo = "text"
) %>%
  layout(
    title = "Caso 1: Precio vs Área construida",
    xaxis = list(title = "Área construida"),
    yaxis = list(title = "Precio (millones)")
  )

El grafico de dispersion entre areaa construida y el precio de la vivienda, muestra una relacion positiva entre ambas variables, las viviendas de mayor area construida tienden a tener precios mas altos.

Se observa que los estratos socioeconomicos mas altos se asocian con precios mas altos, lo que sugiere que esa variable tambien influye en la valorizacion de las propiedades

Se ven algunos valores extremos que corresponden a viviendas de gran tamaño o alto precio, lo cual podrian ser del segmento de lujo

7.5.2 Precio vs número de baños

plot_ly(
  data = base1_imp,
  x = ~banios,
  y = ~preciom,
  type = "box",
  color = ~as.factor(banios)
) %>%
  layout(
    title = "Caso 1: Precio por número de baños",
    xaxis = list(title = "Baños"),
    yaxis = list(title = "Precio (millones)")
  )

Las viviendas con mayor numero de baños tienden a presentar precios mas altos, lo cual es normal en el mercado inmobiliario. Se observa una dispersion de precios en viviendas con mayor numero de baños, lo que puede estar mostrando propiedades de alto valor o con caracteristicas muy especificas

7.5.3 Precio vs número de habitaciones

plot_ly(
  data = base1_imp,
  x = ~habitaciones,
  y = ~preciom,
  type = "box",
  color = ~as.factor(habitaciones)
) %>%
  layout(
    title = "Caso 1: Precio por número de habitaciones",
    xaxis = list(title = "Habitaciones"),
    yaxis = list(title = "Precio (millones)")
  )

Las viviendas con mayor numero de habitaciones tienden a presentar precios mas altos, lo cual es consistente con el comportamiento del mercado, adicionalmente se ve que aumentar el numero de habitaciones aumenta la dispersion de los precios,lo que puede indicar presencias de casas de mayor tamaño o propiedades de alto valor

7.5.4 Precio vs parqueaderos

plot_ly(
  data = base1_imp,
  x = ~parqueaderos,
  y = ~preciom,
  type = "box",
  color = ~as.factor(parqueaderos)
) %>%
  layout(
    title = "Caso 1: Precio por número de parqueaderos",
    xaxis = list(title = "Parqueaderos"),
    yaxis = list(title = "Precio (millones)")
  )

Las viviendas con mayor numero de parqueaderos presentan precios mas altos, lo cual es consistente a el comportamiento esperado

Se observa una dispersion de precios mas alto para las viviendas con un numero de parqueaderos altos, lo cual podria reflejar la presencia de propiedades grandes o de alto valor.

7.5.5 Correlaciones numéricas

cor_base1 <- base1_imp %>%
  select(preciom, areaconst, estrato, parqueaderos, banios, habitaciones) %>%
  cor(use = "complete.obs")

cor_base1 %>%
  kable(caption = "Matriz de correlación - Caso 1") %>%
  kable_styling(full_width = FALSE)
Matriz de correlación - Caso 1
preciom areaconst estrato parqueaderos banios habitaciones
preciom 1.0000000 0.7313480 0.6123503 0.3033762 0.5233357 0.3227096
areaconst 0.7313480 1.0000000 0.4573818 0.2586839 0.4628152 0.3753323
estrato 0.6123503 0.4573818 1.0000000 0.2039056 0.4083039 0.1073141
parqueaderos 0.3033762 0.2586839 0.2039056 1.0000000 0.2922145 0.1928669
banios 0.5233357 0.4628152 0.4083039 0.2922145 1.0000000 0.5755314
habitaciones 0.3227096 0.3753323 0.1073141 0.1928669 0.5755314 1.0000000

La matriz de correlacion muestra que la variable area construida presenta la relacion mas fuerte con el precio de la vivienda ( 0.73), seguida por el estrato (0.61 ) y el numero de baños (0.52). Estas variables parecen ser los principales factores asociados al valor de las casas en este conjunto de datos

Las variables de habitaciones y parqueaderos presentan correlaciones positivas, pero en menor magnitud, lo que sugiere que si bien influyen en el precio no lo hacen en tan gran medida

Se oibservan correlaciones moderadas entre algunas variables explicativas, como los baños y habitaciones, lo que era esperable por las caracteristicas estructurales de las viviendas, segun lo que vemos no vemos problemas severos de multicolinealidad

7.6 7.6 Separación en entrenamiento y prueba

Vamos a dividir la base en entrenamiento prueba ( 80 - 20% siendo 20% el test)

set.seed(123)

idx_train1 <- createDataPartition(base1_imp$preciom, p = 0.8, list = FALSE)

train1 <- base1_imp[idx_train1, ]
test1  <- base1_imp[-idx_train1, ]

dim(train1)
## [1] 579  13
dim(test1)
## [1] 143  13

Tambien tenemos el modelo de la base limpia ( quitando los outliers)

set.seed(123)

idx_train1c <- createDataPartition(base1_clean$preciom, p = 0.8, list = FALSE)

train1_clean <- base1_clean[idx_train1c, ]
test1_clean  <- base1_clean[-idx_train1c, ]

dim(train1_clean)
## [1] 332  20
dim(test1_clean)
## [1] 80 20

7.7 7.7 Estimación del modelo

Modelo Base

modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones  + banios,
              data = train1)

summary(modelo1)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + banios, 
##     data = train1)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -944.52  -80.99  -14.45   52.15  912.51 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -259.06556   33.00747  -7.849 2.07e-14 ***
## areaconst       0.81608    0.04803  16.991  < 2e-16 ***
## estrato        92.15564    7.94172  11.604  < 2e-16 ***
## habitaciones    1.71397    4.39893   0.390    0.697    
## banios         26.20489    5.74957   4.558 6.32e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 156.5 on 574 degrees of freedom
## Multiple R-squared:  0.6648, Adjusted R-squared:  0.6625 
## F-statistic: 284.6 on 4 and 574 DF,  p-value: < 2.2e-16

Segun el modelo, variables como area, estrato, baños y parqueaderos tienen una relacion significativa con el precio de la vivienda. En particular el area construida presenta el mayor impacto sobre el precio, lo cual es consistente con la realidad del mercado

Por otro lado la variable de numero de habitacion no resulto estadisticamente significativa dentro del modelo, lo que sugiere que el efecto lo arrastran otras variables como el area

El modelo presenta un coeficiente de determinacion R2 de 0.6678 lo que indica que el 66% de la variabilidad del precio puede ser explicada por las variables del modelo

Aun asi volvemos a correr el modelo, esta vez sin habitaciones para entender si mejoran las metricas

modelo2 <- lm(preciom ~ areaconst + estrato + parqueaderos + banios,
              data = train1)

summary(modelo2)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + parqueaderos + banios, 
##     data = train1)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -910.07  -78.19  -16.20   49.00  917.04 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -268.9739    29.9957  -8.967  < 2e-16 ***
## areaconst       0.8064     0.0469  17.196  < 2e-16 ***
## estrato        90.2130     7.6673  11.766  < 2e-16 ***
## parqueaderos   14.5714     6.3818   2.283   0.0228 *  
## banios         25.6583     5.0538   5.077 5.19e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 155.8 on 574 degrees of freedom
## Multiple R-squared:  0.6677, Adjusted R-squared:  0.6654 
## F-statistic: 288.4 on 4 and 574 DF,  p-value: < 2.2e-16

A partir de este segundo modelo, las variables como area, estrato, parqueaderos y baños , siguen siendo significativas El modelo presenta un R2 de 0.6677 lo cual quiere decir que si bien quitamos una variable, no mejoro ni empeoro el desempeño del modelo, lo que confirma que esta variable es explicada por otras variables dentro del modelo

Modelo sin los Outliers

modelo1_clean <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
                    data = train1_clean)

summary(modelo1_clean)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train1_clean)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -238.27  -60.63  -11.11   38.81  519.00 
## 
## Coefficients: (1 not defined because of singularities)
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -186.05928   28.97631  -6.421 4.76e-10 ***
## areaconst       0.75477    0.06102  12.369  < 2e-16 ***
## estrato        85.15785    7.12759  11.948  < 2e-16 ***
## habitaciones   -9.66851    4.45548  -2.170   0.0307 *  
## parqueaderos         NA         NA      NA       NA    
## banios         29.58859    5.70785   5.184 3.81e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 105 on 327 degrees of freedom
## Multiple R-squared:  0.725,  Adjusted R-squared:  0.7217 
## F-statistic: 215.6 on 4 and 327 DF,  p-value: < 2.2e-16

En este tercer modelo, se utilizo una version del dataset quitando los valores atipicos Los resultados muestran una mejora en el ajuste del modelo, evidenciada por el aumento del R2 de 0.66 a 0.72 Tambien el error residual del modelo se redujo significativamente, lo que nos quiere decir que los datos atipicos estaban influyendo de manera nevativa en el modelo

# modelo1_clean2 <- lm(preciom ~ areaconst + estrato +  parqueaderos + banios,
#                     data = train1_clean)
# 
# summary(modelo1_clean2)
#Dio el mismo resultado, entonces no vale la pena dejarlo

7.8 7.8 Comparación de desempeño

pred1_base  <- predict(modelo1, newdata = test1)
pred1_clean <- predict(modelo1_clean, newdata = test1_clean)

met_base1  <- metricas_modelo(test1$preciom, pred1_base) %>% mutate(modelo = "Base")
met_clean1 <- metricas_modelo(test1_clean$preciom, pred1_clean) %>% mutate(modelo = "Sin outliers")

bind_rows(met_base1, met_clean1) %>%
  select(modelo, everything()) %>%
  kable(caption = "Comparación de métricas - Caso 1") %>%
  kable_styling(full_width = FALSE)
Comparación de métricas - Caso 1
modelo RMSE MAE R2
Base 169.27104 102.48831 0.5929570
Sin outliers 98.21813 77.60659 0.7201693

Para evaluar el desempeño del modelo, se comparan dos versiones : un modelo base utilizando todos los datos y un modelo estimado despues de eliminar los atipicos. Los resultados muestran que el modelo sin outliers presentan un mejor desempeño en todas las metricas.

En particular el RMSE se reduce de 167 a 98 , el MAE se reduce de 100 a 77 y el R2 de 0.60 a 0.72 lo que indica que el modelo sin outliers explica una mayor proporcion de la variabilidad del precio de las viviendas

Esto sugiere que eliminar los outliers mejora en gran medida el modelo

modelo1_final <- modelo1_clean
nombre_modelo1_final <- "Modelo sin outliers"

summary(modelo1_final)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train1_clean)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -238.27  -60.63  -11.11   38.81  519.00 
## 
## Coefficients: (1 not defined because of singularities)
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -186.05928   28.97631  -6.421 4.76e-10 ***
## areaconst       0.75477    0.06102  12.369  < 2e-16 ***
## estrato        85.15785    7.12759  11.948  < 2e-16 ***
## habitaciones   -9.66851    4.45548  -2.170   0.0307 *  
## parqueaderos         NA         NA      NA       NA    
## banios         29.58859    5.70785   5.184 3.81e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 105 on 327 degrees of freedom
## Multiple R-squared:  0.725,  Adjusted R-squared:  0.7217 
## F-statistic: 215.6 on 4 and 327 DF,  p-value: < 2.2e-16

7.9 7.9 Interpretación del modelo

El modelo de regresion lineal multiple muestra que variables como area construida, estrato y numero de baños tienen una relacion positiva y significativa con el precio de las viviendas. Esto indica que en general , las viviendas con mayor area, ubicadas en estratos altos y con mas baños tienden a tener precios mas elevados

El modelo presenta un R2 de 0.725 lo que significa que aproximadamente el 72,5% de la variacion del precio de las viviendas puede ser explicadas por las variables incluidas en el modelo. Este resultado indica que el modelo tiene una buena capacidad para explicar el precio.

7.10 7.10 Validación de supuestos

par(mfrow = c(2,2))
plot(modelo1_final)

par(mfrow = c(1,1))

En el grafico Residuals vs Fitted se observa dispersion relativamente aleatoria de los residuos, esto sugiere que la relacion entre variables es aproximadamente lineal. El grafico Q-Q Residuals muestra que la mayoria de los puntos siguen la linea teorica, lo que indica que los residuos presentan un comportamiento cercano a la noramlidad ( Aunque hay algunas desviaciones en los extremos) El grafico Scale-Location se observa una dispersion moderada de los residuos, lo cual sugiere que la varianza es relativamente constante El grafico Residuals vs Leverage, nos ayuda a ver observaciones influyentes, aunque no se observan puntos extremadamente dominantes que afecten en gran medida el modelo

En general los resultados sugieren que el modelo cumple razonablemente los supuestos basicos de la regresion lineal

7.11 7.11 Predicción de la vivienda 1

La solicitud 1 corresponde a una casa en zona norte con:

  • área construida = 200
  • parqueaderos = 1
  • baños = 2
  • habitaciones = 4
  • estrato = 4 o 5
  • presupuesto máximo = 350 millones

Dado que el estrato puede ser 4 o 5, se realizan dos predicciones.

nueva_viv1_e4 <- data.frame(
  areaconst = 200,
  estrato = 4,
  habitaciones = 4,
  parqueaderos = 1,
  banios = 2
)

nueva_viv1_e5 <- data.frame(
  areaconst = 200,
  estrato = 5,
  habitaciones = 4,
  parqueaderos = 1,
  banios = 2
)

pred_viv1_e4 <- predict(modelo1_final, newdata = nueva_viv1_e4, interval = "prediction")
pred_viv1_e5 <- predict(modelo1_final, newdata = nueva_viv1_e5, interval = "prediction")

pred_viv1_e4
##        fit      lwr      upr
## 1 326.0293 118.6429 533.4157
pred_viv1_e5
##        fit      lwr      upr
## 1 411.1871 203.1059 619.2684

Se generan dos escenarios por que se permite estrato 4 o 5 por ende los resultados quedan

Estrato 4: Precio estimado 326 Millones, entre 118 y 533 Millones Estrato 5; Precio estimado 411 Millones, entre 203 y 619 Millones

7.12 7.12 Búsqueda de ofertas potenciales para vivienda 1

Aquí se buscan viviendas reales dentro de la base que se parezcan a la solicitud y respeten el presupuesto.

ofertas1 <- base1_imp %>%
  filter(
    preciom <= 350,
    estrato %in% c(4,5),
    habitaciones >= 4,
    banios >= 2,
    parqueaderos >= 1
  ) %>%
  mutate(
    dif_area = abs(areaconst - 200),
    dif_precio_ref = abs(preciom - as.numeric(pred_viv1_e4[1]))
  ) %>%
  arrange(dif_area, dif_precio_ref) %>%
  slice(1:5)

ofertas1 %>%
  select(barrio, zona, tipo, preciom, areaconst, estrato, habitaciones, banios, parqueaderos, latitud, longitud) %>%
  kable(caption = "Top 5 ofertas potenciales - Vivienda 1") %>%
  kable_styling(full_width = FALSE)
Top 5 ofertas potenciales - Vivienda 1
barrio zona tipo preciom areaconst estrato habitaciones banios parqueaderos latitud longitud
la flora Zona Norte Casa 320 200 5 4 4 2 3.48893 -76.51524
la merced Zona Norte Casa 320 200 4 4 4 2 3.48029 -76.51156
el bosque Zona Norte Casa 350 200 5 4 3 3 3.48503 -76.53010
el bosque Zona Norte Casa 335 202 5 5 4 1 3.48399 -76.53044
vipasa Zona Norte Casa 340 203 5 4 3 2 3.48257 -76.51803

Las condiciones utilizadas corresponden a valores mínimos deseados para la vivienda. Por lo tanto, las ofertas encontradas pueden presentar características superiores, como un mayor número de baños o parqueaderos, lo cual representa una mejora respecto a los requerimientos iniciales.

7.12.1 Mapa de ofertas potenciales

ofertas1_mapa <- ofertas1 %>%
  filter(!is.na(latitud), !is.na(longitud))

leaflet(ofertas1_mapa) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 6,
    color = "blue",
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom, " millones",
      "<br><b>Área:</b> ", areaconst,
      "<br><b>Estrato:</b> ", estrato,
      "<br><b>Habitaciones:</b> ", habitaciones,
      "<br><b>Baños:</b> ", banios,
      "<br><b>Parqueaderos:</b> ", parqueaderos
    )
  )

7.12.2 Discusión sugerida

Estas ofertas se seleccionan por cercanía a las condiciones solicitadas y por cumplir el presupuesto máximo de 350 millones. La recomendación final debe considerar no solo el precio estimado, sino también la similitud con el área, estrato y número de habitaciones.

Los resultados muestran que existen varias viviendas con características similares en barrios como La Flora, La Merced, El Bosque y Vipasa, con precios entre 320 y 350 millones y áreas cercanas a 200 m². Esto sugiere que el valor estimado por el modelo es consistente con los precios observados en el mercado para viviendas con características similares.

8 8. Caso 2: Vivienda 2 (Apartamento en zona sur)

8.1 8.1 Filtro de la base

Para el segundo caso se deben considerar solo apartamentos en zona sur.

base2 <- vivienda %>%
  filter(tipo == "Apartamento", zona == "Zona Sur")

dim(base2)
## [1] 2787   13

8.1.1 Primeros 3 registros

head(base2, 3) %>%
  kable(caption = "Primeros 3 registros de base2") %>%
  kable_styling(full_width = FALSE)
Primeros 3 registros de base2
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
5098 Zona Sur 05 4 290 96 1 2 3 Apartamento acopi -76.53464 3.44987
698 Zona Sur 02 3 78 40 1 1 2 Apartamento aguablanca -76.50100 3.40000
8199 Zona Sur NA 6 875 194 2 5 3 Apartamento aguacatal -76.55700 3.45900

8.1.2 Tablas de comprobación del filtro

table(base2$tipo)
## 
## Apartamento 
##        2787
table(base2$zona)
## 
## Zona Sur 
##     2787

8.2 8.2 Mapa de las observaciones filtradas

base2_mapa <- base2 %>%
  filter(!is.na(latitud), !is.na(longitud))

leaflet(base2_mapa) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 4,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio (millones):</b> ", preciom,
      "<br><b>Área:</b> ", areaconst,
      "<br><b>Estrato:</b> ", estrato
    )
  )

8.2.1 Ajuste de apartamentos que no estan

q1_lat <- quantile(base2$latitud, 0.25, na.rm = TRUE)
q3_lat <- quantile(base2$latitud, 0.75, na.rm = TRUE)
iqr_lat <- q3_lat - q1_lat

li_lat <- q1_lat - 1.5 * iqr_lat
ls_lat <- q3_lat + 1.5 * iqr_lat

q1_lon <- quantile(base2$longitud, 0.25, na.rm = TRUE)
q3_lon <- quantile(base2$longitud, 0.75, na.rm = TRUE)
iqr_lon <- q3_lon - q1_lon

li_lon <- q1_lon - 1.5 * iqr_lon
ls_lon <- q3_lon + 1.5 * iqr_lon

base2_geo <- base2 %>%
  dplyr::filter(
    latitud >= li_lat,
    latitud <= ls_lat,
    longitud >= li_lon,
    longitud <= ls_lon
  )
leaflet(base2_geo) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 4,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom
    )
  )

Ahora si tenemos un mapa con toda la distribucion lo mas cercana a la realidad de la zona sur ( Aun quedan datos que pueden estar fuera del rango, pueden ser datos mal georeferenciados o mal clasificados )

8.3 8.3 Calidad de datos

8.3.1 Valores faltantes

na_base2 <- sapply(base2, function(x) sum(is.na(x)))
data.frame(variable = names(na_base2), faltantes = as.numeric(na_base2)) %>%
  kable(caption = "Valores faltantes en base2") %>%
  kable_styling(full_width = FALSE)
Valores faltantes en base2
variable faltantes
id 0
zona 0
piso 622
estrato 0
preciom 0
areaconst 0
parqueaderos 406
banios 0
habitaciones 0
tipo 0
barrio 0
longitud 0
latitud 0

8.3.2 Explicación de imputación

En un análisis de regresión, los valores faltantes pueden reducir el tamaño de la muestra o generar errores si no se tratan. Se hara uso de lo siguiente:

  • variables numéricas( Piso y Parqueaderos ) : mediana

La mediana es útil porque es robusta frente a valores extremos.

8.3.3 Imputación

base2_imp <- imputar_datos(base2)

na_base2_despues <- sapply(base2_imp, function(x) sum(is.na(x)))
data.frame(variable = names(na_base2_despues),
           faltantes_despues = as.numeric(na_base2_despues)) %>%
  kable(caption = "Valores faltantes después de imputación en base2") %>%
  kable_styling(full_width = FALSE)
Valores faltantes después de imputación en base2
variable faltantes_despues
id 0
zona 0
piso 0
estrato 0
preciom 0
areaconst 0
parqueaderos 0
banios 0
habitaciones 0
tipo 0
barrio 0
longitud 0
latitud 0

8.4 8.4 Búsqueda de outliers

outliers_base2 <- base2_imp %>%
  mutate(
    out_preciom      = marcar_outliers_iqr(preciom),
    out_areaconst    = marcar_outliers_iqr(areaconst),
    out_estrato      = marcar_outliers_iqr(estrato),
    out_parqueaderos = marcar_outliers_iqr(parqueaderos),
    out_banios       = marcar_outliers_iqr(banios),
    out_habitaciones = marcar_outliers_iqr(habitaciones)
  ) %>%
  mutate(
    total_outliers = out_preciom + out_areaconst + out_estrato +
      out_parqueaderos + out_banios + out_habitaciones
  )

resumen_outliers2 <- data.frame(
  variable = c("preciom", "areaconst", "estrato", "parqueaderos", "banios", "habitaciones"),
  cantidad_outliers = c(
    sum(outliers_base2$out_preciom),
    sum(outliers_base2$out_areaconst),
    sum(outliers_base2$out_estrato),
    sum(outliers_base2$out_parqueaderos),
    sum(outliers_base2$out_banios),
    sum(outliers_base2$out_habitaciones)
  )
)

resumen_outliers2 %>%
  kable(caption = "Cantidad de outliers detectados en base2") %>%
  kable_styling(full_width = FALSE)
Cantidad de outliers detectados en base2
variable cantidad_outliers
preciom 263
areaconst 177
estrato 0
parqueaderos 33
banios 141
habitaciones 885

8.4.1 Base alternativa sin registros extremadamente atípicos

Dado que hay varios outliers no sabemos si estos casos se dan por que son viviendas de lujo o son errores en la data, se va a crear dos bases para entender si hay diferencia entre usar la data con o sin atipicos

base2_clean <- outliers_base2 %>%
  filter(total_outliers == 0)

dim(base2_imp)
## [1] 2787   13
dim(base2_clean)
## [1] 1722   20

8.5 8.5 Análisis exploratorio

8.5.1 Precio vs área construida

plot_ly(
  data = base2_imp,
  x = ~areaconst,
  y = ~preciom,
  type = "scatter",
  mode = "markers",
  color = ~as.factor(estrato),
  text = ~paste("Barrio:", barrio,
                "<br>Habitaciones:", habitaciones,
                "<br>Banos:", banios),
  hoverinfo = "text"
) %>%
  layout(
    title = "Caso 2: Precio vs Área construida",
    xaxis = list(title = "Área construida"),
    yaxis = list(title = "Precio (millones)")
  )

El grafico de dispersion entre areaa construida y el precio de los apartamentos, muestra una relacion positiva entre ambas variables, las viviendas de mayor area construida tienden a tener precios mas altos.

Se observa que los estratos socioeconomicos mas altos ( estrato 5 y 6 ) se asocian con precios mas altos, lo que sugiere que esa variable tambien influye en la valorizacion de las propiedades

Se ven algunos valores extremos que corresponden a viviendas de gran tamaño o alto precio, lo cual podrian ser del segmento de lujo

8.5.2 Precio vs baños

plot_ly(
  data = base2_imp,
  x = ~banios,
  y = ~preciom,
  type = "box",
  color = ~as.factor(banios)
) %>%
  layout(
    title = "Caso 2: Precio por número de baños",
    xaxis = list(title = "Baños"),
    yaxis = list(title = "Precio (millones)")
  )

El gráfico muestra que, en general, a mayor número de baños, mayor precio del apartamento. Las viviendas con 5 o 6 baños presentan los valores más altos, mientras que aquellas con 1 o 2 baños se concentran en precios menores. Esto sugiere que el número de baños está asociado positivamente con el valor de los apartamentos

8.5.3 Precio vs habitaciones

plot_ly(
  data = base2_imp,
  x = ~habitaciones,
  y = ~preciom,
  type = "box",
  color = ~as.factor(habitaciones)
) %>%
  layout(
    title = "Caso 2: Precio por número de habitaciones",
    xaxis = list(title = "Habitaciones"),
    yaxis = list(title = "Precio (millones)")
  )

Las viviendas con mayor numero de habitaciones tienden a presentar precios mas altos, lo cual es consistente con el comportamiento del mercado, adicionalmente se ve que aumentar el numero de habitaciones aumenta la dispersion de los precios,lo que puede indicar presencias de apartamentos de mayor tamaño o propiedades de alto valor

8.5.4 Correlaciones numéricas

cor_base2 <- base2_imp %>%
  select(preciom, areaconst, estrato, parqueaderos, banios, habitaciones) %>%
  cor(use = "complete.obs")

cor_base2 %>%
  kable(caption = "Matriz de correlación - Caso 2") %>%
  kable_styling(full_width = FALSE)
Matriz de correlación - Caso 2
preciom areaconst estrato parqueaderos banios habitaciones
preciom 1.0000000 0.7579955 0.6727067 0.6967313 0.7196705 0.3317538
areaconst 0.7579955 1.0000000 0.4815593 0.5787405 0.6618179 0.4339608
estrato 0.6727067 0.4815593 1.0000000 0.4983633 0.5686171 0.2125953
parqueaderos 0.6967313 0.5787405 0.4983633 1.0000000 0.5633010 0.2517695
banios 0.7196705 0.6618179 0.5686171 0.5633010 1.0000000 0.5149227
habitaciones 0.3317538 0.4339608 0.2125953 0.2517695 0.5149227 1.0000000

La matriz de correlación muestra que el precio de la vivienda tiene una relación positiva principalmente con el área construida (0.76), seguida por el número de baños (0.72), parqueaderos (0.70) y estrato (0.67). Esto indica que estas variables influyen de manera importante en el valor de los apartamentos. En contraste, el número de habitaciones presenta una correlación más baja (0.33)

8.6 8.6 Separación en entrenamiento y prueba

Vamos a dividir la base en entrenamiento prueba ( 80 - 20% siendo 20% el test)

set.seed(123)

idx_train2 <- createDataPartition(base2_imp$preciom, p = 0.8, list = FALSE)

train2 <- base2_imp[idx_train2, ]
test2  <- base2_imp[-idx_train2, ]

dim(train2)
## [1] 2231   13
dim(test2)
## [1] 556  13

Tambien tenemos el modelo de la base limpia ( quitando los outliers)

set.seed(123)

idx_train2c <- createDataPartition(base2_clean$preciom, p = 0.8, list = FALSE)

train2_clean <- base2_clean[idx_train2c, ]
test2_clean  <- base2_clean[-idx_train2c, ]

dim(train2_clean)
## [1] 1379   20
dim(test2_clean)
## [1] 343  20

8.7 8.7 Estimación del modelo

8.7.1 Modelo base

modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
              data = train2)

summary(modelo2)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1185.54   -38.92    -2.76    37.82   919.34 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -266.62819   14.49194 -18.398  < 2e-16 ***
## areaconst       1.39711    0.05587  25.007  < 2e-16 ***
## estrato        59.71370    3.02394  19.747  < 2e-16 ***
## habitaciones  -19.31148    3.71704  -5.195 2.23e-07 ***
## parqueaderos   68.32691    4.02302  16.984  < 2e-16 ***
## banios         46.70035    3.42887  13.620  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 93.62 on 2225 degrees of freedom
## Multiple R-squared:   0.76,  Adjusted R-squared:  0.7594 
## F-statistic:  1409 on 5 and 2225 DF,  p-value: < 2.2e-16

Segun el modelo, variables como area, estrato, baños y parqueaderos tienen una relacion significativa con el precio de la vivienda. En particular el area construida presenta el mayor impacto sobre el precio, lo cual es consistente con la realidad del mercado

Por otro lado la variable de numero de habitaciones no resulto estadisticamente significativa dentro del modelo

El modelo presenta un coeficiente de determinacion R2 de 0.76 lo que indica que el 76% de la variabilidad del precio puede ser explicada por las variables del modelo

8.7.2 Modelo sin outliers

modelo2_clean <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
                    data = train2_clean)

summary(modelo2_clean)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train2_clean)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -215.850  -25.170   -0.099   27.016  159.128 
## 
## Coefficients: (1 not defined because of singularities)
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -160.87761    7.42135 -21.678  < 2e-16 ***
## areaconst       2.06429    0.08188  25.211  < 2e-16 ***
## estrato        39.42774    1.97312  19.982  < 2e-16 ***
## habitaciones         NA         NA      NA       NA    
## parqueaderos   28.80915    3.56230   8.087 1.33e-15 ***
## banios          9.96042    2.83560   3.513 0.000458 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 45.26 on 1374 degrees of freedom
## Multiple R-squared:  0.7503, Adjusted R-squared:  0.7496 
## F-statistic:  1032 on 4 and 1374 DF,  p-value: < 2.2e-16

En este segundo modelo, se utilizo una version del dataset quitando los valores atipicos Los resultados muestran una desmejoria en el ajuste del modelo, evidenciada por la disminucion del R2 de 0.76 a 0.75

Elerror residual del modelo se redujo significativamente, lo que nos quiere decir que los datos atipicos estaban influyendo de manera negativa en el modelo

8.8 8.8 Comparación de desempeño

pred2_base  <- predict(modelo2, newdata = test2)
pred2_clean <- predict(modelo2_clean, newdata = test2_clean)

met_base2  <- metricas_modelo(test2$preciom, pred2_base) %>% mutate(modelo = "Base")
met_clean2 <- metricas_modelo(test2_clean$preciom, pred2_clean) %>% mutate(modelo = "Sin outliers")

bind_rows(met_base2, met_clean2) %>%
  select(modelo, everything()) %>%
  kable(caption = "Comparación de métricas - Caso 2") %>%
  kable_styling(full_width = FALSE)
Comparación de métricas - Caso 2
modelo RMSE MAE R2
Base 90.61755 55.89612 0.7826907
Sin outliers 50.63154 37.01886 0.6929699

La comparación de métricas muestra que el modelo sin outliers reduce considerablemente el error, pasando de un RMSE de 90.62 a 50.63 y de un MAE de 55.89 a 37.02, lo que indica predicciones más precisas.

Aunque el R2 disminuye ligeramente de 0.78 a 0.69, el modelo sin valores atípicos presenta un mejor desempeño en términos de error, sugiriendo que los outliers estaban afectando la estabilidad de las estimaciones.

8.8.1 Selección del modelo final

modelo2_final <- modelo2_clean
nombre_modelo2_final <- "Modelo sin outliers"

summary(modelo2_final)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train2_clean)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -215.850  -25.170   -0.099   27.016  159.128 
## 
## Coefficients: (1 not defined because of singularities)
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -160.87761    7.42135 -21.678  < 2e-16 ***
## areaconst       2.06429    0.08188  25.211  < 2e-16 ***
## estrato        39.42774    1.97312  19.982  < 2e-16 ***
## habitaciones         NA         NA      NA       NA    
## parqueaderos   28.80915    3.56230   8.087 1.33e-15 ***
## banios          9.96042    2.83560   3.513 0.000458 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 45.26 on 1374 degrees of freedom
## Multiple R-squared:  0.7503, Adjusted R-squared:  0.7496 
## F-statistic:  1032 on 4 and 1374 DF,  p-value: < 2.2e-16

Se elige el modelo sin outliers dado que este modelo tiene mejores estadisticas aun cuando sacrificamos un poco del R2

8.9 8.9 Validación de supuestos

par(mfrow = c(2,2))
plot(modelo2_final)

par(mfrow = c(1,1))

Los gráficos de validación muestran que los residuos se distribuyen alrededor de cero, lo que sugiere que el modelo captura razonablemente la relación entre las variables. Sin embargo, el gráfico Q-Q indica algunas desviaciones en los extremos, lo que sugiere que la normalidad de los residuos no se cumple completamente. Además, el gráfico Scale-Location muestra cierta variación en la dispersión, lo que podría indicar heterocedasticidad. En general, el modelo es útil para predicción, aunque podrían aplicarse transformaciones o métodos robustos para mejorar el cumplimiento de los supuestos.

8.10 8.10 Predicción de la vivienda 2

La solicitud 2 corresponde a un apartamento en zona sur con:

  • área construida = 300
  • parqueaderos = 3
  • baños = 3
  • habitaciones = 5
  • estrato = 5 o 6
  • presupuesto máximo = 850 millones

Se generan dos predicciones, una para estrato 5 y otra para estrato 6.

nueva_viv2_e5 <- data.frame(
  areaconst = 300,
  estrato = 5,
  habitaciones = 5,
  parqueaderos = 3,
  banios = 3
)

nueva_viv2_e6 <- data.frame(
  areaconst = 300,
  estrato = 6,
  habitaciones = 5,
  parqueaderos = 3,
  banios = 3
)

pred_viv2_e5 <- predict(modelo2_final, newdata = nueva_viv2_e5, interval = "prediction")
pred_viv2_e6 <- predict(modelo2_final, newdata = nueva_viv2_e6, interval = "prediction")

pred_viv2_e5
##        fit      lwr      upr
## 1 771.8578 678.3122 865.4035
pred_viv2_e6
##        fit      lwr      upr
## 1 811.2856 718.0928 904.4783

Se generan dos escenarios por que se permite estrato 5 o 6 por ende los resultados quedan

Estrato 5: Precio estimado 771 Millones, entre 678 y 865 Millones Estrato 6; Precio estimado 811 Millones, entre 718 y 908 Millones

8.11 8.11 Búsqueda de ofertas potenciales para vivienda 2

ofertas2 <- base2_imp %>%
  filter(
    preciom <= 850,
    estrato %in% c(5,6),
    habitaciones >= 5,
    banios >= 3,
    parqueaderos >= 3
  ) %>%
  mutate(
    dif_area = abs(areaconst - 300),
    dif_precio_ref = abs(preciom - as.numeric(pred_viv2_e5[1]))
  ) %>%
  arrange(dif_area, dif_precio_ref) %>%
  slice(1:5)

ofertas2 %>%
  select(barrio, zona, tipo, preciom, areaconst, estrato, habitaciones, banios, parqueaderos, latitud, longitud) %>%
  kable(caption = "Top 5 ofertas potenciales - Vivienda 2") %>%
  kable_styling(full_width = FALSE)
Top 5 ofertas potenciales - Vivienda 2
barrio zona tipo preciom areaconst estrato habitaciones banios parqueaderos latitud longitud
seminario Zona Sur Apartamento 670 300 5 6 5 3 3.40900 -76.55000
seminario Zona Sur Apartamento 530 256 5 5 5 3 3.40748 -76.55408
guadalupe Zona Sur Apartamento 730 573 5 5 8 3 3.40800 -76.54800

Para esta solicitud unicamente se encontraron 3 apartamentos, los cuales cumplen con las caracteristicas y se encuentran dentro del presupuesto maximo de 850 Millones, los barrios serian Seminario y Guadalupe, en la zona Sur, con caracteristicas muy similares. Estos apartamentos representan alternativas viables segun la estimacion del modelo y las restricciones del credito disponible

8.11.1 Mapa de ofertas potenciales

ofertas2_mapa <- ofertas2 %>%
  filter(!is.na(latitud), !is.na(longitud))

leaflet(ofertas2_mapa) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 6,
    color = "blue",
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom, " millones",
      "<br><b>Área:</b> ", areaconst,
      "<br><b>Estrato:</b> ", estrato,
      "<br><b>Habitaciones:</b> ", habitaciones,
      "<br><b>Baños:</b> ", banios,
      "<br><b>Parqueaderos:</b> ", parqueaderos
    )
  )

Validando la ubicacion geografica vemos que los 3 apartamentos estan relativamente cerca los unos de los otros, por lo que representan alternativas viables que cumplen con las características solicitadas y el presupuesto disponible.

9 9. Comparación general de modelos

comparacion_general <- bind_rows(
  met_base1  %>% mutate(caso = "Caso 1"),
  met_clean1 %>% mutate(caso = "Caso 1"),
  met_base2  %>% mutate(caso = "Caso 2"),
  met_clean2 %>% mutate(caso = "Caso 2")
)

comparacion_general %>%
  select(caso, modelo, RMSE, MAE, R2) %>%
  kable(caption = "Comparación general de métricas") %>%
  kable_styling(full_width = FALSE)
Comparación general de métricas
caso modelo RMSE MAE R2
Caso 1 Base 169.27104 102.48831 0.5929570
Caso 1 Sin outliers 98.21813 77.60659 0.7201693
Caso 2 Base 90.61755 55.89612 0.7826907
Caso 2 Sin outliers 50.63154 37.01886 0.6929699

La comparación general de métricas muestra que los modelos sin outliers presentan menores errores de predicción (RMSE y MAE) en ambos casos, lo que indica estimaciones más precisas. Aunque en el Caso 2 el R2 disminuye ligeramente, la reducción en los errores sugiere que eliminar valores atípicos mejora la estabilidad del modelo. En general, los modelos ajustados sin outliers ofrecen mejor desempeño para la predicción de precios de vivienda.

10 10. Conclusiones

A partir del análisis realizado se evidenció que variables como el área construida, el estrato, el número de baños y los parqueaderos presentan una relación significativa con el precio de la vivienda. En particular, el área construida mostró la mayor influencia, lo cual es consistente con el comportamiento del mercado inmobiliario. Por otro lado las habitaciones fueron la variable con menor reelevancia lo que quiere decir que otras variables ya la explicaban.

El análisis exploratorio permitió identificar la presencia de valores atípicos, los cuales afectaban el ajuste del modelo. Al remover estos valores se observó una reducción en los errores de predicción (RMSE y MAE), lo que indica una mejora en la estabilidad del modelo, aunque en algunos casos el coeficiente R2 disminuyó ligeramente.

Finalmente, el modelo permitió estimar precios para las viviendas solicitadas e identificar ofertas reales dentro del presupuesto disponible, lo que demuestra la utilidad del análisis para apoyar procesos de toma de decisiones en el mercado inmobiliario.

11 11. Recomendaciones

Para futuros análisis se recomienda incluir más variables que puedan influir en el precio, como la antigüedad del inmueble, características del barrio o cercanía a servicios y transporte.

También sería útil explorar otros modelos predictivos que puedan capturar relaciones más complejas entre las variables, con el fin de mejorar la precisión de las estimaciones.

Por último, se recomienda continuar realizando procesos de limpieza y validación de datos, ya que la presencia de valores atípicos puede afectar significativamente el desempeño de los modelos.