1 Caso CyA Inmobiliaria

Las ventas de bienes raíces en Cali se han visto disminuidas de manera significativa durante el último año, en parte por la coyuntura política y social del país, que ha llevado a las instituciones bancarias a ser más conservadoras en el otorgamiento de crédito hipotecario. Hace dos días, María recibió una carta de una compañía internacional que desea ubicar a dos de sus empleados (con sus respectivas familias) en la ciudad, y solicita asesoría para la compra de dos viviendas con las siguientes características:

Característica Vivienda 1 Vivienda 2
Tipo Casa Apartamento
Área construida (m2) 200 300
Parqueaderos 1 3
Baños 2 3
Habitaciones 4 5
Estrato 4 o 5 5 o 6
Zona Norte Sur
Crédito preaprobado $350 millones $850 millones

el Objetivo es responder la solicitud mediante técnicas de modelación estadística (regresión lineal múltiple), analizando los dos casos y entregando un informe.

2 Base de datos a utilizar

# install.packages("gitcreds")
# gitcreds::gitcreds_delete("https://github.com")
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)

library(paqueteMODELOS)
library(dplyr)
library(ggplot2)
library(plotly)
library(leaflet)
library(corrplot)
library(mice)
library(car)
library(lmtest)
library(broom)
library(modelsummary)
library(knitr)

set.seed(1234)
data("vivienda")
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>

Alizar una revision a la base de datos vivienda encontramos información de oferta de propiedades residenciales en Cali durante los últimos tres meses, con variables como:

ubicación - zona - barrio - coordenadas geográficas

características físicas - area construida - número de baños - habitaciones - parqueaderos - piso

otras variables - estrato - precio en millones

3 Entendimiento y Preparacion de los datos

Antes de modelar, es necesario conocer el conjunto de datos disponible. Para ello se tuvo en cuenta lo siguiente:

  • Cuántos registros y atributos hay, y de qué tipo son.
  • Medidas de centralidad y dispersión para los atributos numéricos.
  • La matriz de correlación de los atributos numéricos, para identificar redundancia.
  • La presencia de datos atípicos.
  • La presencia y magnitud de datos faltantes.
  • El balance/distribución de las variables categóricas.

En esta etapa no se modifican los datos, solo se documentan sus características.

3.1 Entendimiento

3.1.1 Dimensión y tipos de datos

dim(vivienda)
## [1] 8322   13
sapply(vivienda, class)
##           id         zona         piso      estrato      preciom    areaconst 
##    "numeric"  "character"  "character"    "numeric"    "numeric"    "numeric" 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##    "numeric"    "numeric"    "numeric"  "character"  "character"    "numeric" 
##      latitud 
##    "numeric"

La base cuenta con 8.322 registros y 13 variables. Las variables id, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, longitud y latitud son numéricas; zona, piso, tipo y barrio son de tipo texto (categóricas/nominales, aunque piso y estrato tienen un orden implícito).

Para el análisis se definen dos grupos de variables de interés:

num_vars <- c("estrato", "preciom", "areaconst", "parqueaderos", "banios", "habitaciones")
cat_vars <- c("zona", "tipo", "barrio")

3.1.2 Medidas de centralidad y dispersión

summary(vivienda[, num_vars])
##     estrato         preciom         areaconst       parqueaderos   
##  Min.   :3.000   Min.   :  58.0   Min.   :  30.0   Min.   : 1.000  
##  1st Qu.:4.000   1st Qu.: 220.0   1st Qu.:  80.0   1st Qu.: 1.000  
##  Median :5.000   Median : 330.0   Median : 123.0   Median : 2.000  
##  Mean   :4.634   Mean   : 433.9   Mean   : 174.9   Mean   : 1.835  
##  3rd Qu.:5.000   3rd Qu.: 540.0   3rd Qu.: 229.0   3rd Qu.: 2.000  
##  Max.   :6.000   Max.   :1999.0   Max.   :1745.0   Max.   :10.000  
##  NA's   :3       NA's   :2        NA's   :3        NA's   :1605    
##      banios        habitaciones   
##  Min.   : 0.000   Min.   : 0.000  
##  1st Qu.: 2.000   1st Qu.: 3.000  
##  Median : 3.000   Median : 3.000  
##  Mean   : 3.111   Mean   : 3.605  
##  3rd Qu.: 4.000   3rd Qu.: 4.000  
##  Max.   :10.000   Max.   :10.000  
##  NA's   :3        NA's   :3
data.frame(desv_estandar = sapply(vivienda[, num_vars], sd, na.rm = TRUE))
##              desv_estandar
## estrato           1.029222
## preciom         328.647244
## areaconst       142.964126
## parqueaderos      1.124909
## banios            1.428210
## habitaciones      1.459537

El precio (preciom) y el área construida (areaconst) son las variables con mayor dispersión relativa, coherente con un mercado que va desde vivienda económica hasta vivienda de lujo. El estrato se mueve entre 3 y 6

3.1.3 Matriz de correlación

M <- cor(vivienda[, num_vars], use = "pairwise.complete.obs")
round(M, 2)
##              estrato preciom areaconst parqueaderos banios habitaciones
## estrato         1.00    0.61      0.27         0.42   0.42        -0.07
## preciom         0.61    1.00      0.69         0.69   0.67         0.26
## areaconst       0.27    0.69      1.00         0.58   0.65         0.52
## parqueaderos    0.42    0.69      0.58         1.00   0.57         0.28
## banios          0.42    0.67      0.65         0.57   1.00         0.59
## habitaciones   -0.07    0.26      0.52         0.28   0.59         1.00
corrplot(M, method = "color", addCoef.col = "black", type = "upper", tl.col = "black")

El precio se correlaciona de forma moderaday no llega a ser del todo alta con área construida, parqueaderos y baños (0.6-0.7), lo cual es consistente con el mercado y lo qye se esperaria: propiedades más grandes y con más comodidades tienden a ser mas caras. No se observan correlaciones muy altas (>0.9) por lo que no se esperaria redundancia

3.1.4 Datos atípicos

par(mfrow = c(2, 3))
for (v in num_vars) {
  boxplot(vivienda[[v]], main = v, col = "#8FD3D3")
}

par(mfrow = c(1, 1))
data.frame(
  variable = num_vars,
  n_atipicos = sapply(num_vars, function(v) length(boxplot.stats(vivienda[[v]])$out))
)
##                  variable n_atipicos
## estrato           estrato          0
## preciom           preciom        552
## areaconst       areaconst        382
## parqueaderos parqueaderos        567
## banios             banios         72
## habitaciones habitaciones        888

Se identifican datos atípicos en todas las variables, excepto en el estrato que como se dijo antes va de 3 a6. En un mercado inmobiliario real estos valores no necesariamente son errores: corresponden a propiedades de lujo o de gran tamaño, por lo que no se eliminan, aunque se debe tener presente que estos valores extremos pueden afectar los supuestos del modelo de regresión

3.1.5 Datos faltantes

El siguiente grafico nos permite ver por variables la cantidad de registros (a la derecha) que tienen x cantidad (a la izquierda) de variables vacias en rojo, y en la parte inferior (eje X) viene siendo el total de vacias por variable.

colSums(is.na(vivienda))
##           id         zona         piso      estrato      preciom    areaconst 
##            3            3         2638            3            2            3 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##         1605            3            3            3            3            3 
##      latitud 
##            3
md.pattern(vivienda[, c("zona", "piso", "estrato", "preciom", "areaconst",
                         "parqueaderos", "banios", "habitaciones", "tipo", "barrio")],
           rotate.names = TRUE)

##      preciom zona estrato areaconst banios habitaciones tipo barrio
## 4808       1    1       1         1      1            1    1      1
## 1909       1    1       1         1      1            1    1      1
## 876        1    1       1         1      1            1    1      1
## 726        1    1       1         1      1            1    1      1
## 1          1    0       0         0      0            0    0      0
## 2          0    0       0         0      0            0    0      0
##            2    3       3         3      3            3    3      3
##      parqueaderos piso     
## 4808            1    1    0
## 1909            1    0    1
## 876             0    1    1
## 726             0    0    2
## 1               0    0    9
## 2               0    0   10
##              1605 2638 4266

Se observan tres situaciones distintas de datos faltantes:

  1. Registros prácticamente vacíos: 3 filas tienen valores faltantes simultáneos en casi todas las variables (id, zona, estrato, areaconst, banios, habitaciones, tipo, barrio, longitud, latitud). Representan un 0.04% de los datos.
  2. parqueaderos: 1.605 valores faltantes (19.3%)
  3. piso: 2.638 valores faltantes (31.7%), el porcentaje más alto de toda la base.

3.1.6 Distribución de variables categóricas

p_zona <- ggplot(vivienda, aes(x = zona)) + geom_bar(fill = "#0D3B66") +
  theme_minimal() + labs(title = "Oferta por zona", x = NULL, y = "Viviendas") +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))
p_tipo <- ggplot(vivienda, aes(x = tipo)) + geom_bar(fill = "#F4D35E") +
  theme_minimal() + labs(title = "Oferta por tipo", x = NULL, y = "Viviendas")
p_estrato <- ggplot(vivienda, aes(x = factor(estrato))) + geom_bar(fill = "#EE6C4D") +
  theme_minimal() + labs(title = "Oferta por estrato", x = "Estrato", y = "Viviendas")
gridExtra::grid.arrange(p_zona, p_tipo, p_estrato, ncol = 3)

La oferta está fuertemente concentrada en la Zona Sur (más de la mitad de los registros), predominan los apartamentos sobre las casas, y el estrato 5 es el más frecuente. La base no incluye viviendas de estratos 1 y 2, por lo que las conclusiones aplican al segmento medio y alto del mercado, que es justamente el segmento en el que se ubican las dos solicitudes

3.2 Preparación de los datos

De acuerdo con lo observado en la etapa anterior, se define la siguiente secuencia de actividades:

  1. Eliminar los 3 registros prácticamente vacíos
  2. Convertir zona y tipo a factor, lo que es necesario para generar el modelo
  3. Imputar parqueaderos con la mediana, dado que su ausencia no está asociada de forma clara al tipo de vivienda (no se puede asumir que sea homogéneamente “cero parqueaderos”).
  4. Conservar piso sin imputar ya que no es una variable requerida en el modelo de precio de esta actividad por lo que se excluira del análisis cuantitativo
  5. Conservar longitud y latitud: a diferencia de un análisis puramente multivariado (PCA/conglomerados/correspondencia), en esta actividad se requiere ubicar las viviendas en un mapa (filtro por zona, oferta recomendada)
  6. Conservar los datos atípicos identificados (no se eliminan ni se capan), ya que corresponden a un segmento real y relevante del mercado (vivienda de alto valor).
core <- c("zona", "estrato", "preciom", "areaconst", "banios", "habitaciones",
          "tipo", "barrio", "longitud", "latitud")
viv <- vivienda[rowSums(is.na(vivienda[, core])) == 0, ]

viv$zona <- factor(gsub("^Zona ", "", trimws(viv$zona)))
viv$tipo <- factor(viv$tipo)

viv$parqueaderos[is.na(viv$parqueaderos)] <- median(viv$parqueaderos, na.rm = TRUE)

dim(viv)
## [1] 8319   13
colSums(is.na(viv))
##           id         zona         piso      estrato      preciom    areaconst 
##            0            0         2635            0            0            0 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##            0            0            0            0            0            0 
##      latitud 
##            0

La base final de trabajo queda con 8319 registros, prácticamente sin pérdida de información frente a los 8.322 originales, y sin datos faltantes en las variables que se usarán en el filtrado, la exploración y el modelo (piso conserva sus datos faltantes, pero no se usa como predictor).

3.3 Verificación espacial de la variable zona

Antes de filtrar las bases por zona, se calcula el centroide geográfico (promedio de longitud y latitud) de cada zona sobre la totalidad de la base viv. Esto permite, más adelante, verificar si los puntos de cada base filtrada están efectivamente ubicados en su zona declarada o si existen inconsistencias entre la etiqueta de zona y la ubicación geográfica real del predio.

centroides <- viv %>%
  group_by(zona) %>%
  summarise(lon_prom = mean(longitud), lon_sd = sd(longitud),
            lat_prom = mean(latitud), lat_sd = sd(latitud), n = n())
centroides
## # A tibble: 5 × 6
##   zona    lon_prom  lon_sd lat_prom lat_sd     n
##   <fct>      <dbl>   <dbl>    <dbl>  <dbl> <int>
## 1 Centro     -76.5 0.00896     3.44 0.0105   124
## 2 Norte      -76.5 0.0160      3.46 0.0311  1920
## 3 Oeste      -76.5 0.0121      3.44 0.0145  1198
## 4 Oriente    -76.5 0.0160      3.43 0.0208   351
## 5 Sur        -76.5 0.0143      3.39 0.0297  4726

Se observa que cada zona tiene un centroide geográfico claramente distinto y una dispersión (lon_sd, lat_sd) relativamente acotada, lo que sugiere que, en general, las zonas sí corresponden a áreas geográficas diferenciadas de la ciudad. Esta información se usa en las secciones siguientes para contrastar, para cada base filtrada, si sus puntos están más cerca del centroide de su propia zona que del centroide de cualquier otra zona.

4 Solicitud 1: Casa en Zona Norte (crédito preaprobado $350 millones)

Se solicita una casa en la Zona Norte, con área construida de 200 m², 1 parqueadero, 2 baños, 4 habitaciones y estrato 4 o 5, con un crédito preaprobado de $350 millones.

4.1 1. Filtrado de la base de datos (base1)

Se filtra la base de trabajo para incluir únicamente las ofertas de casas de la Zona Norte.

base1 <- viv %>% filter(tipo == "Casa", zona == "Norte")
dim(base1)
## [1] 722  13
head(base1, 3)
## # A tibble: 3 × 13
##      id zona  piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <fct> <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1209 Norte 02          5     320       150            2      4            6
## 2  1592 Norte 02          5     780       380            2      3            3
## 3  4057 Norte 02          6     750       445            2      7            6
## # ℹ 4 more variables: tipo <fct>, barrio <chr>, longitud <dbl>, latitud <dbl>

Se verifica que el filtro haya quedado correctamente aplicado:

table(Tipo = base1$tipo)
## Tipo
## Apartamento        Casa 
##           0         722
table(Zona = base1$zona)
## Zona
##  Centro   Norte   Oeste Oriente     Sur 
##       0     722       0       0       0

La base base1 queda con 722 registros, todos con tipo = "Casa" y zona = "Norte", tal como lo confirman las tablas anteriores.

4.1.1 Ubicación geográfica de base1

norte_c <- centroides %>% filter(zona == "Norte")
otras_c <- centroides %>% filter(zona != "Norte")

base1$dist_norte <- sqrt((base1$longitud - norte_c$lon_prom)^2 + (base1$latitud - norte_c$lat_prom)^2)
base1$dist_otra_zona <- sapply(seq_len(nrow(base1)), function(i) {
  min(sqrt((base1$longitud[i] - otras_c$lon_prom)^2 + (base1$latitud[i] - otras_c$lat_prom)^2))
})
base1$ubicacion <- ifelse(base1$dist_norte <= base1$dist_otra_zona,
                           "Coherente con Zona Norte", "Más cerca de otra zona")
table(base1$ubicacion)
## 
## Coherente con Zona Norte   Más cerca de otra zona 
##                      515                      207
pal1 <- colorFactor(c("#0D3B66", "#EE6C4D"), domain = base1$ubicacion)
leaflet(base1) %>%
  addTiles() %>%
  addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4, stroke = FALSE,
                    fillOpacity = 0.6, color = ~pal1(ubicacion),
                    popup = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M",
                                     "<br>Área: ", areaconst, " m2")) %>%
  addLegend(position = "bottomright", pal = pal1, values = ~ubicacion, title = "Ubicación")

De los 722 registros de base1, 207 (28.7%) están geográficamente más cerca del centroide de otra zona que del centroide de la propia Zona Norte. Esto no significa necesariamente que el filtro esté mal aplicado (el filtro por zona == "Norte" es correcto); lo que evidencia es una inconsistencia entre la etiqueta comercial de zona y la ubicación geográfica real del predio, esto puede llegar a pasar por

  • Los límites administrativos de las zonas no son líneas rectas y se entrelazan con otras como Norte con Oeste u Oriente
  • El dato de zona puede haberse digitado manualmente por el asesor a partir del nombre comercial del sector, mientras que la coordenada proviene de una geolocalización independiente
  • errores de captura en longitud/latitud.

Si bien para el análisis de esta solicitud se conservan todos los registros de base1 se deja como notacion la importancia de revizar la zona de la base segun la longitud/latitud

4.2 2. Análisis exploratorio de datos

Se explora la relación entre el precio (preciom) y el área construida, el estrato, el número de baños, el número de habitaciones y la zona, usando gráficos interactivos del paquete plotly.

4.2.1 Correlación entre las variables numéricas (base1)

vars_mod <- c("preciom", "areaconst", "estrato", "banios", "habitaciones", "parqueaderos")
M1 <- cor(base1[, vars_mod])
round(M1, 2)
##              preciom areaconst estrato banios habitaciones parqueaderos
## preciom         1.00      0.73    0.61   0.52         0.32         0.30
## areaconst       0.73      1.00    0.46   0.46         0.38         0.26
## estrato         0.61      0.46    1.00   0.41         0.11         0.20
## banios          0.52      0.46    0.41   1.00         0.58         0.29
## habitaciones    0.32      0.38    0.11   0.58         1.00         0.19
## parqueaderos    0.30      0.26    0.20   0.29         0.19         1.00
corrplot(M1, method = "color", addCoef.col = "black", type = "upper", tl.col = "black")

Dentro de las casas de la Zona Norte, el precio se correlaciona más fuertemente con areaconst (0.73) y estrato (0.61), de forma moderada con banios (0.52), y de forma más débil con habitaciones (0.32) y parqueaderos (0.3). Esto sugiere que el tamaño de la propiedad y el estrato socioeconómico son los factores más asociados al precio, mientras que el número de habitaciones aporta relativamente poca información adicional una vez se conoce el área construida

4.2.2 Precio vs. área construida, baños y habitaciones (interactivo)

g1 <- ggplot(base1, aes(x = areaconst, y = preciom, color = estrato,
                         text = paste0("Barrio: ", barrio, "<br>Baños: ", banios,
                                        "<br>Habitaciones: ", habitaciones))) +
  geom_point(alpha = 0.5) +
  scale_color_gradient(low = "#F4D35E", high = "#0D3B66") +
  theme_minimal() +
  labs(title = "Precio vs. área construida (color = estrato)", x = "Área construida (m2)",
       y = "Precio (millones)", color = "Estrato")
ggplotly(g1, tooltip = c("x", "y", "color", "text"))
g2 <- ggplot(base1, aes(x = factor(banios), y = preciom)) +
  geom_boxplot(fill = "#8FD3D3") +
  theme_minimal() +
  labs(title = "Precio según número de baños", x = "Baños", y = "Precio (millones)")
ggplotly(g2)
g3 <- ggplot(base1, aes(x = factor(habitaciones), y = preciom)) +
  geom_boxplot(fill = "#EE6C4D") +
  theme_minimal() +
  labs(title = "Precio según número de habitaciones", x = "Habitaciones", y = "Precio (millones)")
ggplotly(g3)

Se confirma visualmente la relación positiva entre precio y área construida , así como una relación positiva pero con más dispersión con baños y habitaciones: a partir de 5-6 habitaciones el precio mediano deja de crecer de forma clara, e incluso algunas casas muy grandes en número de habitaciones no son las más costosas, por lo que el efecto de habitaciones sobre el precio podría no ser tan claro en el modelo de regresión

4.2.3 Precio por zona (contexto)

Dado que base1 ya está en Zona Norte, se construye un gráfico de contexto sobre todas las casas de la base viv (todas las zonas) para dimensionar en qué lugar del mercado se ubica la Zona Norte en términos de precio:

casas <- viv %>% filter(tipo == "Casa")
g4 <- ggplot(casas, aes(x = zona, y = preciom, fill = zona == "Norte")) +
  geom_boxplot() +
  scale_fill_manual(values = c("TRUE" = "#0D3B66", "FALSE" = "#8FD3D3"), guide = "none") +
  theme_minimal() +
  labs(title = "Precio de casas por zona (Zona Norte resaltada)", x = NULL, y = "Precio (millones)")
ggplotly(g4)

La Zona Norte se ubica en un nivel de precios intermedio para casas: por debajo de Zona Oeste y Zona Sur, y por encima de Zona Centro y Zona Oriente. Esto es coherente con lo que se busca: al no incluir zona como variable dentro de base1 (por ser constante), su efecto ya queda controlado por el propio filtro, y el análisis se enfoca en cómo varían área, estrato, baños y habitaciones dentro de esa zona.

4.3 3. Modelo de regresión lineal múltiple

Se estima un modelo de regresión lineal múltiple para el precio en función del área construida, el estrato, el número de habitaciones, el número de parqueaderos y el número de baños:

\[\text{preciom} = \beta_0 + \beta_1\,\text{areaconst} + \beta_2\,\text{estrato} + \beta_3\,\text{habitaciones} + \beta_4\,\text{parqueaderos} + \beta_5\,\text{banios} + \varepsilon\]

m1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
summary(m1)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base1)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -924.94  -77.71  -17.66   45.90 1081.29 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -251.05177   30.11848  -8.335 3.94e-16 ***
## areaconst       0.81090    0.04352  18.634  < 2e-16 ***
## estrato        84.61108    7.17727  11.789  < 2e-16 ***
## habitaciones    0.95948    4.10569   0.234  0.81529    
## parqueaderos   16.55976    5.70396   2.903  0.00381 ** 
## banios         24.57669    5.35583   4.589 5.26e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 158.2 on 716 degrees of freedom
## Multiple R-squared:  0.6548, Adjusted R-squared:  0.6524 
## F-statistic: 271.6 on 5 and 716 DF,  p-value: < 2.2e-16

Interpretacion de los coeficientes:

  • Área construida (0.81, p <2e-16): manteniendo constantes las demás variables, cada m² adicional de área construida incrementa el precio esperado en 0.81 millones de pesos. Es significativo y acorde a lo que se esperaba ya que el área es el principal determinante físico del valor de una vivienda.
  • Estrato (84.6, p <2e-16): cada punto adicional de estrato incrementa el precio esperado en cerca de 85 millones, manteniendo fijas las demas variables. Es significativo.
  • Habitaciones (0.96, p 0.82): no es estadísticamente significativo (p > 0.05). Podria deberse a la relacion que se tiene con el area, ya que si bien se espera que mas habitaciones sinifiquen una mayor area y por ende mayor precio, bien podrian hacer habitaciones mas pequeñas en la misma area y esto no afecta.
  • Parqueaderos (16.56, p 0.0038): cada parqueadero adicional incrementa el precio esperado en cerca de 16.6 millones. Es significativo y se puede tomar como que el parqueadero es un atributo valorado y relativamente escaso
  • Baños (24.58, p 5.3e-06): cada baño adicional incrementa el precio esperado en cerca de 24.6 millones. Es significativo y se puede tomar como una caracteristica de mayor comodidad lo que sube el precio.

Bondad de ajuste: el modelo alcanza un R² de 0.655 (R² ajustado 0.652), es decir, explica cerca del 65% de la variabilidad del precio de las casas de la Zona Norte. Es un ajuste razonable para datos de corte transversal de precios de vivienda, pero deja un 35% de la variabilidad sin explicar, atribuible a factores no incluidos en la base: calidad de los acabados, antigüedad de la construcción, piso (excluido por su alto porcentaje de datos faltantes), vista, cercanía a vías principales o centros comerciales, y el barrio específico dentro de la zona (más de 400 barrios distintos en la base completa).

Para mejorar el modelo podría afinar los datos faltantes del parqueadero o probar transformaciones (por ejemplo logarítmicas en preciom y areaconst, dado que ambas variables son de cola derecha, como se pudo ver en los datos atípicos).

4.4 4. Validación de supuestos del modelo

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

par(mfrow = c(1, 1))

Linealidad: el gráfico de residuales vs. valores ajustados no muestra una curvatura sistemática marcada en el grueso de los datos, aunque se observa mayor dispersión de residuales conforme aumenta el precio ajustado, con varios valores muy alejados de cero en la cola de precios altos (coherente con las viviendas de lujo identificadas como atípicas en el entendimiento de los datos).

Normalidad de los residuales:

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

La prueba de Shapiro-Wilk rechaza la normalidad de los residuales (p < 0.001), lo cual también es visible en el gráfico Q-Q (colas pesadas hacia la derecha). Esto es consistente con la asimetría positiva de preciom documentada desde el entendimiento de los datos: unas pocas viviendas de muy alto valor generan residuales grandes. Con un tamaño de muestra de 722 observaciones, los estimadores puntuales de los coeficientes siguen siendo válidos (insesgados), pero los intervalos de confianza y las pruebas de hipótesis basadas en normalidad deben interpretarse con cautela.

Homocedasticidad (varianza constante de los residuales):

bptest(m1)
## 
##  studentized Breusch-Pagan test
## 
## data:  m1
## BP = 132.24, df = 5, p-value < 2.2e-16

La prueba de Breusch-Pagan rechaza la homocedasticidad (p < 0.001): la varianza del error crece con el nivel de precio, un patrón típico de datos de precios (los errores absolutos de predicción tienden a ser proporcionalmente más grandes en propiedades más costosas).

Multicolinealidad:

vif(m1)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     1.523989     1.433058     1.621630     1.120670     1.918323

Todos los factores de inflación de varianza (VIF) están por debajo de 2, muy lejos del umbral de preocupación (5 o 10), por lo que no hay evidencia de multicolinealidad problemática entre los predictores, a pesar de las correlaciones moderadas observadas en el análisis exploratorio.

Independencia: no se evalúa formalmente autocorrelación (por ejemplo, Durbin-Watson), ya que las observaciones no siguen un orden temporal ni espacial secuencial; corresponden a una muestra transversal de ofertas de vivienda.

Qué se podría hacer para mejorar el cumplimiento de los supuestos Transformar preciom y areaconst a escala logarítmica para reducir la asimetría y estabilizar la varianza; estimar errores estándar robustos a heterocedasticidad; o tratar de forma separada el segmento de vivienda de lujo (posiblemente con un modelo o un componente adicional), en lugar de forzar un único modelo lineal sobre todo el rango de precios.

4.5 5. Predicción del precio para la vivienda solicitada

La vivienda 1 solicitada es una casa de 200 m² de área construida, 1 parqueadero, 2 baños y 4 habitaciones, en estrato 4 o 5. Como el estrato de la solicitud es ambiguo (“4 o 5”), se calcula la predicción para ambos escenarios:

nueva_v1 <- data.frame(
  areaconst = c(200, 200),
  estrato = c(4, 5),
  habitaciones = c(4, 4),
  parqueaderos = c(1, 1),
  banios = c(2, 2)
)
pred_v1 <- predict(m1, newdata = nueva_v1, interval = "prediction")
tabla_pred1 <- cbind(estrato = nueva_v1$estrato, round(pred_v1, 1))
kable(tabla_pred1, col.names = c("Estrato", "Precio estimado", "Límite inferior (95%)", "Límite superior (95%)"))
Estrato Precio estimado Límite inferior (95%) Límite superior (95%)
4 319.1 7.9 630.4
5 403.7 92.0 715.5

Con estrato 4, el precio esperado es de $319 millones, dentro del crédito preaprobado de $350 millones. Con estrato 5, el precio esperado sube a $404 millones, por encima del crédito disponible. Los intervalos de predicción del 95% son amplios (reflejo del R² de 0.65 y de la heterocedasticidad detectada), por lo que estas cifras deben usarse como una guía de orden de magnitud y no como un precio exacto de cierre. Dado el presupuesto de $350 millones, es más realista enfocar la búsqueda en casas de estrato 4 en la Zona Norte con las características solicitadas; una casa de estrato 5 con esa área tiende, en promedio, a superar el presupuesto.

4.6 6. Ofertas potenciales dentro del presupuesto

Se buscan, dentro de base1, ofertas reales con precio menor o igual a $350 millones, estrato 4 o 5, y características lo más cercanas posible a lo solicitado (área 200 m², 1 parqueadero, 2 baños, 4 habitaciones). Para ello se calcula, para cada oferta que cumple el presupuesto, una distancia (estandarizada) de su perfil de características al perfil solicitado, y se seleccionan las más cercanas:

feat <- c("areaconst", "parqueaderos", "banios", "habitaciones")
m_feat <- sapply(feat, function(v) mean(base1[[v]]))
s_feat <- sapply(feat, function(v) sd(base1[[v]]))

objetivo_v1 <- c(areaconst = 200, parqueaderos = 1, banios = 2, habitaciones = 4)

candidatas1 <- base1 %>% filter(preciom <= 350, estrato %in% c(4, 5))
Z1 <- scale(candidatas1[, feat], center = m_feat, scale = s_feat)
z_obj1 <- (objetivo_v1 - m_feat) / s_feat
candidatas1$distancia_perfil <- sqrt(rowSums(sweep(Z1, 2, z_obj1)^2))

ofertas_v1 <- candidatas1 %>%
  arrange(distancia_perfil) %>%
  head(5) %>%
  select(id, barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones,
         longitud, latitud, distancia_perfil)

kable(ofertas_v1 %>% select(-longitud, -latitud), digits = 2,
      col.names = c("ID", "Barrio", "Estrato", "Precio (M)", "Área (m2)",
                    "Parqueaderos", "Baños", "Habitaciones", "Distancia al perfil"))
ID Barrio Estrato Precio (M) Área (m2) Parqueaderos Baños Habitaciones Distancia al perfil
1666 alamos 4 275 120 1 2 4 0.48
3586 la merced 4 330 240 1 2 3 0.60
4779 prados del norte 5 280 140 1 2 3 0.65
1924 vipasa 4 320 264 1 2 3 0.67
94 zona norte 4 265 162 1 3 4 0.69
leaflet(ofertas_v1) %>%
  addTiles() %>%
  addMarkers(lng = ~longitud, lat = ~latitud,
             popup = ~paste0("<b>Barrio:</b> ", barrio,
                              "<br><b>Precio:</b> $", preciom, "M",
                              "<br><b>Área:</b> ", areaconst, " m2",
                              "<br><b>Estrato:</b> ", estrato,
                              "<br><b>Parqueaderos:</b> ", parqueaderos,
                              "<br><b>Baños:</b> ", banios,
                              "<br><b>Habitaciones:</b> ", habitaciones))

Las 5 ofertas anteriores cumplen el presupuesto de $350 millones y se encuentran en la Zona Norte. Se observa un trade-off esperable: las opciones de menor área (alrededor de 120-160 m²) dejan más margen de precio frente al presupuesto, mientras que las que más se acercan a los 200 m² solicitados (por ejemplo, las ofertas en Vipasa y La Merced) usan casi todo el presupuesto disponible. Ninguna de las opciones recomendadas alcanza exactamente el número de habitaciones solicitado (4) junto con el área de 200 m² dentro del presupuesto; esto es consistente con el hallazgo del modelo de que, a este nivel de presupuesto, el mercado ofrece un intercambio entre área y número de habitaciones más que una combinación de ambos en su nivel máximo. Se recomienda a María presentar estas alternativas al cliente, priorizando según si su prioridad es el área construida o el número de habitaciones.

5 Solicitud 2: Apartamento en Zona Sur (crédito preaprobado $850 millones)

La compañía solicita un apartamento en la Zona Sur, con área construida de 300 m², 3 parqueaderos, 3 baños, 5 habitaciones y estrato 5 o 6, con un crédito preaprobado de $850 millones. Se repiten, para esta segunda solicitud, los pasos 1 a 6 aplicados a la primera.

5.1 1. Filtrado de la base de datos (base2)

base2 <- viv %>% filter(tipo == "Apartamento", zona == "Sur")
dim(base2)
## [1] 2787   13
head(base2, 3)
## # A tibble: 3 × 13
##      id zona  piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <fct> <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  5098 Sur   05          4     290        96            1      2            3
## 2   698 Sur   02          3      78        40            1      1            2
## 3  8199 Sur   <NA>        6     875       194            2      5            3
## # ℹ 4 more variables: tipo <fct>, barrio <chr>, longitud <dbl>, latitud <dbl>
table(Tipo = base2$tipo)
## Tipo
## Apartamento        Casa 
##        2787           0
table(Zona = base2$zona)
## Zona
##  Centro   Norte   Oeste Oriente     Sur 
##       0       0       0       0    2787

La base base2 queda con 2787 registros, todos con tipo = "Apartamento" y zona = "Sur".

5.1.1 Ubicación geográfica de base2

sur_c <- centroides %>% filter(zona == "Sur")
otras_c2 <- centroides %>% filter(zona != "Sur")

base2$dist_sur <- sqrt((base2$longitud - sur_c$lon_prom)^2 + (base2$latitud - sur_c$lat_prom)^2)
base2$dist_otra_zona <- sapply(seq_len(nrow(base2)), function(i) {
  min(sqrt((base2$longitud[i] - otras_c2$lon_prom)^2 + (base2$latitud[i] - otras_c2$lat_prom)^2))
})
base2$ubicacion <- ifelse(base2$dist_sur <= base2$dist_otra_zona,
                           "Coherente con Zona Sur", "Más cerca de otra zona")
table(base2$ubicacion)
## 
## Coherente con Zona Sur Más cerca de otra zona 
##                   2294                    493
pal2 <- colorFactor(c("#0D3B66", "#EE6C4D"), domain = base2$ubicacion)
leaflet(base2) %>%
  addTiles() %>%
  addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4, stroke = FALSE,
                    fillOpacity = 0.5, color = ~pal2(ubicacion),
                    popup = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M",
                                     "<br>Área: ", areaconst, " m2")) %>%
  addLegend(position = "bottomright", pal = pal2, values = ~ubicacion, title = "Ubicación")

De los 2787 registros de base2, 493 (17.7%) están geográficamente más cerca del centroide de otra zona que del de la Zona Sur. La proporción es menor que en base1, pero confirma nuevamente que la etiqueta comercial de zona no es perfectamente consistente con la geolocalización, probablemente por las mismas razones discutidas para la Solicitud 1 (límites de zona difusos, digitación manual del campo zona frente a una geolocalización independiente). Se mantiene la base completa para el análisis, apoyándose en la variable zona como criterio de negocio.

5.2 2. Análisis exploratorio de datos

M2 <- cor(base2[, vars_mod])
round(M2, 2)
##              preciom areaconst estrato banios habitaciones parqueaderos
## preciom         1.00      0.76    0.67   0.72         0.33         0.54
## areaconst       0.76      1.00    0.48   0.66         0.43         0.45
## estrato         0.67      0.48    1.00   0.57         0.21         0.30
## banios          0.72      0.66    0.57   1.00         0.51         0.41
## habitaciones    0.33      0.43    0.21   0.51         1.00         0.14
## parqueaderos    0.54      0.45    0.30   0.41         0.14         1.00
corrplot(M2, method = "color", addCoef.col = "black", type = "upper", tl.col = "black")

En los apartamentos de Zona Sur el precio se correlaciona fuertemente con areaconst (0.76), banios (0.72) y estrato (0.67), y de forma moderada con parqueaderos (0.54), mientras que habitaciones (0.33) es la de menor asociación lineal directa con el precio.

h1 <- ggplot(base2, aes(x = areaconst, y = preciom, color = estrato,
                         text = paste0("Barrio: ", barrio, "<br>Baños: ", banios,
                                        "<br>Habitaciones: ", habitaciones))) +
  geom_point(alpha = 0.4) +
  scale_color_gradient(low = "#F4D35E", high = "#0D3B66") +
  theme_minimal() +
  labs(title = "Precio vs. área construida (color = estrato)", x = "Área construida (m2)",
       y = "Precio (millones)", color = "Estrato")
ggplotly(h1, tooltip = c("x", "y", "color", "text"))
h2 <- ggplot(base2, aes(x = factor(banios), y = preciom)) +
  geom_boxplot(fill = "#8FD3D3") +
  theme_minimal() +
  labs(title = "Precio según número de baños", x = "Baños", y = "Precio (millones)")
ggplotly(h2)
h3 <- ggplot(base2, aes(x = factor(habitaciones), y = preciom)) +
  geom_boxplot(fill = "#EE6C4D") +
  theme_minimal() +
  labs(title = "Precio según número de habitaciones", x = "Habitaciones", y = "Precio (millones)")
ggplotly(h3)
aptos <- viv %>% filter(tipo == "Apartamento")
h4 <- ggplot(aptos, aes(x = zona, y = preciom, fill = zona == "Sur")) +
  geom_boxplot() +
  scale_fill_manual(values = c("TRUE" = "#0D3B66", "FALSE" = "#8FD3D3"), guide = "none") +
  theme_minimal() +
  labs(title = "Precio de apartamentos por zona (Zona Sur resaltada)", x = NULL, y = "Precio (millones)")
ggplotly(h4)

El precio crece de forma claracon el área y el número de baños, adicional las habitaciones si parecen tener una relacion directa y creciente con el precio. En cuanto al precio por zona, la Zona Sur se ubica en un nivel de precios bajo-medio para apartamentos, muy por debajo de la Zona Oeste, lo que es consistente con que la Zona Sur concentre el segmento más masivo de la oferta de apartamentos de la ciudad.

5.3 3. Modelo de regresión lineal múltiple

m2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
summary(m2)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1233.80   -45.98    -2.05    42.25   927.76 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -314.61850   13.51614 -23.277  < 2e-16 ***
## areaconst       1.45139    0.04876  29.766  < 2e-16 ***
## estrato        68.93151    2.66560  25.860  < 2e-16 ***
## habitaciones  -16.28835    3.41634  -4.768 1.96e-06 ***
## parqueaderos   51.59374    3.16010  16.327  < 2e-16 ***
## banios         49.11206    3.02770  16.221  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 95.01 on 2781 degrees of freedom
## Multiple R-squared:  0.7544, Adjusted R-squared:  0.754 
## F-statistic:  1709 on 5 and 2781 DF,  p-value: < 2.2e-16

Interpretación de los coeficientes:

  • Área construida (1.45, p <2e-16): cada m² adicional incrementa el precio esperado en 1.45 millones, manteniendo las demás variables constantes. Es significativo y, como en el modelo de casas, es el efecto individual más grande; de hecho, la pendiente es más pronunciada que en base1, lo que sugiere que en los apartamentos de Zona Sur el área construida es un determinante todavía más importante del precio (por unidad de área) que en las casas de Zona Norte.
  • Estrato (68.9, p <2e-16): cada punto adicional de estrato incrementa el precio esperado en cerca de 69 millones.
  • Habitaciones (-16.29, p 2e-06): el coeficiente es negativo y estadísticamente significativo. A igualdad de área, baños, parqueaderos y estrato, cada habitación adicional se asocia con una disminución de cerca de 16.3 millones en el precio esperado. A primera vista parece contraintuitivo, pero es un resultado que puede ser coherente si lo vemos desde el punto de vista de que si el área construida se mantiene fija, tener más habitaciones implica subdividir el mismo espacio en cuartos más pequeños, lo cual el mercado tiende a penalizar (habitaciones pequeñas y poco funcionales) frente a un apartamento con menos habitaciones, pero más amplias.
  • Parqueaderos (51.59, p <2e-16): cada parqueadero adicional incrementa el precio esperado en cerca de 52 millones. Es significativo ya que suele ser un bien más escaso y valorado que en casas
  • Baños (49.11, p <2e-16): cada baño adicional incrementa el precio esperado en cerca de 49 millones.

Bondad de ajuste: el modelo alcanza un R² de 0.754 (R² ajustado 0.754), superior al de base1, explicando cerca del 75% de la variabilidad del precio de los apartamentos de Zona Sur. El mejor ajuste, frente al modelo de casas, es razonable: los apartamentos de una misma zona tienden a ser más homogéneos entre sí (edificios con especificaciones estándar) que las casas, que varían más en lote, número de pisos y remodelaciones. Persiste, sin embargo, un 25% de variabilidad no explicada, atribuible a factores como el piso del apartamento (excluido de la base por datos faltantes, pero puede que unos pisos sean mas apetecidos que otros, el año de construcción, etc. se pueden tomar las mismas estrategias de mejora sugeridas para las casas incluir barrio, recuperar piso, transformar variables.

5.4 4. Validación de supuestos del modelo

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

par(mfrow = c(1, 1))
shapiro.test(residuals(m2))
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(m2)
## W = 0.79864, p-value < 2.2e-16
bptest(m2)
## 
##  studentized Breusch-Pagan test
## 
## data:  m2
## BP = 929.2, df = 5, p-value < 2.2e-16
vif(m2)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.027751     1.555176     1.441342     1.310740     2.470269

Los resultados son cualitativamente similares a los de base1: la prueba de Shapiro-Wilk rechaza la normalidad de los residuales (colas pesadas asociadas a los apartamentos de mayor valor), y la prueba de Breusch-Pagan rechaza la homocedasticidad (la dispersión de los errores crece con el precio). Los VIF son todos inferiores a 2.5, por lo que tampoco hay evidencia de multicolinealidad problemática. Al igual que en base1, no se evalúa formalmente independencia/autocorrelación por tratarse de datos transversales. Por lo anterior posibles sugerencias de mejora son transformación logarítmica, errores estándar robustos a heterocedasticidad, tratamiento diferenciado del segmento de mayor valor

5.5 5. Predicción del precio para la vivienda solicitada

nueva_v2 <- data.frame(
  areaconst = c(300, 300),
  estrato = c(5, 6),
  habitaciones = c(5, 5),
  parqueaderos = c(3, 3),
  banios = c(3, 3)
)
pred_v2 <- predict(m2, newdata = nueva_v2, interval = "prediction")
tabla_pred2 <- cbind(estrato = nueva_v2$estrato, round(pred_v2, 1))
kable(tabla_pred2, col.names = c("Estrato", "Precio estimado", "Límite inferior (95%)", "Límite superior (95%)"))
Estrato Precio estimado Límite inferior (95%) Límite superior (95%)
5 686.1 498.7 873.5
6 755.1 567.6 942.5

Con estrato 5, el precio esperado es de $686 millones; con estrato 6, de $755 millones. Ambos escenarios están dentro del crédito preaprobado de $850 millones, con un margen de cerca de $164 millones (estrato 5) y $95 millones (estrato 6). Sin embargo, el límite superior del intervalo de predicción del 95% para estrato 6 ($942 millones) queda por encima del presupuesto, por lo que si el cliente prioriza el estrato 6 conviene dejar un margen de negociación.

Dado el amplio margen de presupuesto frente al precio esperado, la familia tiene margen para considerar tanto estrato 5 como estrato 6; se recomienda priorizar estrato 5 si no se quiere exceder el presupuesto, o dejar un colchón para negociar algo en estrato 6.

5.6 6. Ofertas potenciales dentro del presupuesto

objetivo_v2 <- c(areaconst = 300, parqueaderos = 3, banios = 3, habitaciones = 5)

m_feat2 <- sapply(feat, function(v) mean(base2[[v]]))
s_feat2 <- sapply(feat, function(v) sd(base2[[v]]))

candidatas2 <- base2 %>% filter(preciom <= 850, estrato %in% c(5, 6))
Z2 <- scale(candidatas2[, feat], center = m_feat2, scale = s_feat2)
z_obj2 <- (objetivo_v2 - m_feat2) / s_feat2
candidatas2$distancia_perfil <- sqrt(rowSums(sweep(Z2, 2, z_obj2)^2))

ofertas_v2 <- candidatas2 %>%
  arrange(distancia_perfil) %>%
  head(5) %>%
  select(id, barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones,
         longitud, latitud, distancia_perfil)

kable(ofertas_v2 %>% select(-longitud, -latitud), digits = 2,
      col.names = c("ID", "Barrio", "Estrato", "Precio (M)", "Área (m2)",
                    "Parqueaderos", "Baños", "Habitaciones", "Distancia al perfil"))
ID Barrio Estrato Precio (M) Área (m2) Parqueaderos Baños Habitaciones Distancia al perfil
6175 capri 5 350 270.00 3 3 4 1.68
2308 San Fernando 5 350 258.00 2 4 5 2.03
4266 el ingenio 6 700 250.00 2 4 5 2.10
8036 seminario 5 530 256.00 3 5 5 2.30
8113 cuarto de legua 5 410 295.55 2 4 4 2.45
leaflet(ofertas_v2) %>%
  addTiles() %>%
  addMarkers(lng = ~longitud, lat = ~latitud,
             popup = ~paste0("<b>Barrio:</b> ", barrio,
                              "<br><b>Precio:</b> $", preciom, "M",
                              "<br><b>Área:</b> ", areaconst, " m2",
                              "<br><b>Estrato:</b> ", estrato,
                              "<br><b>Parqueaderos:</b> ", parqueaderos,
                              "<br><b>Baños:</b> ", banios,
                              "<br><b>Habitaciones:</b> ", habitaciones))

Las 5 ofertas anteriores cumplen el presupuesto de $850 millones y están en la Zona Sur. En este caso el presupuesto es amplio frente al precio típico de la zona, por lo que hay más margen de elección: varias opciones alcanzan o superan los 3 parqueaderos y 3 baños solicitados, aunque ninguna llega a las 5 habitaciones dentro del presupuesto sin sacrificar otras características, en línea con el hallazgo del modelo de que, controlando por área, un número alto de habitaciones no es lo que más valoriza estas propiedades. Se recomienda recomienda entonces revisar entre las opciones o que se quiere priorizar sin pasar el presupuesto, por otra parte con los puntos mostrados en el mapa observamos que algunas de las opciones no parecen estar en la ozna sur, por lo que valdria la pena al igual que en casas, revisar esta etiqueta o si el error esta en la longitud/latitud.

6 Informe ejecutivo: conclusiones y recomendaciones

6.1 Comparación de los dos modelos

modelsummary(list("Casas - Zona Norte" = m1, "Apartamentos - Zona Sur" = m2),
             stars = TRUE, gof_omit = "Log.Lik|F|RMSE")
Casas - Zona Norte Apartamentos - Zona Sur
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) -251.052*** -314.619***
(30.118) (13.516)
areaconst 0.811*** 1.451***
(0.044) (0.049)
estrato 84.611*** 68.932***
(7.177) (2.666)
habitaciones 0.959 -16.288***
(4.106) (3.416)
parqueaderos 16.560** 51.594***
(5.704) (3.160)
banios 24.577*** 49.112***
(5.356) (3.028)
Num.Obs. 722 2787
R2 0.655 0.754
R2 Adj. 0.652 0.754
AIC 9369.4 33300.9
BIC 9401.5 33342.4
  • El modelo de apartamentos en Zona Sur (R² = 0.75) ajusta mejor que el de casas en Zona Norte (R² = 0.65), lo cual es esperable dada la mayor homogeneidad de los apartamentos frente a las casas.
  • En ambos modelos, área construida, estrato, parqueaderos y baños son estadísticamente significativos y con signo positivo, consistente con la lógica del mercado inmobiliario: propiedades más grandes, de mejor estrato y con más comodidades valen más.
  • El número de habitaciones es la única variable con comportamiento distinto entre los dos casos: no es significativa en casas de Zona Norte, y es significativa y negativa en apartamentos de Zona Sur. En ambos casos la explicación es la misma: a igual área construida, más habitaciones implica habitaciones más pequeñas, un atributo que el mercado no premia (y en apartamentos, incluso castiga).
  • En ambos modelos se detectó no normalidad y heterocedasticidad de los residuales, atribuibles a la cola de vivienda de alto valor; y no hay multicolinealidad problemática entre los predictores (VIF < 3 en todos los casos).

6.2 Recomendaciones Finales

  1. Vivienda 1 (Casa, Zona Norte, presupuesto $350 millones): el precio esperado para una casa de 200 m², 1 parqueadero, 2 baños y 4 habitaciones es de $319 millones en estrato 4 (dentro de presupuesto) y de $404 millones en estrato 5 (por encima del presupuesto). Se recomienda orientar la búsqueda hacia estrato 4, o negociar el precio si el cliente insiste en estrato 5. Se presentan 5 ofertas concretas dentro del presupuesto para iniciar la negociación.
  2. Vivienda 2 (Apartamento, Zona Sur, presupuesto $850 millones): el precio esperado para un apartamento de 300 m², 3 parqueaderos, 3 baños y 5 habitaciones es de $686 millones (estrato 5) y $755 millones (estrato 6), ambos dentro del presupuesto, con mayor margen en estrato 5. Se presentan 5 ofertas concretas dentro del presupuesto.
  3. Calidad de datos: se recomienda a C&A mejorar la captura de piso (31.7% de datos faltantes) y parqueaderos (19.3%), y revisar el proceso de asignación de la variable zona, dado varios de los registros de ambas bases (29% en Zona Norte, 18% en Zona Sur) tiene coordenadas geográficas más cercanas a otra zona, lo que puede afectar la confiabilidad de futuros filtros y análisis por zona.
  4. Mejoras futuras al modelo: para reducir el error de predicción y acotar los intervalos de precio, se sugiere incorporar el barrio, recuperar la variable piso, y probar transformaciones logarítmicas de precio y área construida, dado que ambas variables presentan asimetría y heterocedasticidad asociada al segmento de vivienda de alto valor.