Caso C&A

Carga de Base de Datos

Base de datos

Se crea copia de la base de datos como buena práctica y su respectiva visualización

data = vivienda
head(data)
## # A tibble: 6 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1147 Zona O… <NA>        3     250        70            1      3            6
## 2  1169 Zona O… <NA>        3     320       120            1      2            3
## 3  1350 Zona O… <NA>        3     350       220            2      2            4
## 4  5992 Zona S… 02          4     400       280            3      5            3
## 5  1212 Zona N… 01          5     260        90            1      2            3
## 6  1724 Zona N… 01          5     240        87            1      3            3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# Ver tamaño del dataset

nrow(data)        # Número de filas
## [1] 8322
ncol(data)        # Número de columnas
## [1] 13

El dataset original cuenta con 8322 registros con 13 columnas.

Dado que el primer punto es realizar un filTrado por zona, para ello primero se rectifican cuales son los valores únicos para las columnas zona y tipo, previo a realizar el filtrado.

# Ver solo los valores únicos
unique(data$zona)
## [1] "Zona Oriente" "Zona Sur"     "Zona Norte"   "Zona Oeste"   "Zona Centro" 
## [6] NA
# Ver solo los valores únicos
unique(data$tipo)
## [1] "Casa"        "Apartamento" NA

Adicional, se revisa los valores faltantes, sin embargo al tratarse solo de 3 registros para las variables zona y tipo, se toma la decisión de no realizar ningun tratamiento estadístico para estás ya que, 6 registros representan el 0,07%.

# Se revisar valores faltantes
colSums(is.na(data))
##           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

Una vez rectificado que la BD no cuenta con categorias mal escritas o repetidas, se procede a realizar el filtro.

Caso 1 - Vivienda 1

Respuesta 1 - Vivienda 1 (casa)

Filtro en la base de datos que incluya las ofertas de: casas, de la zona norte de la ciudad.

base1_c = data

# Filtrar solo Casas en Zona Norte
base1_c <- data %>%
  filter(tipo == "Casa", zona == "Zona Norte")

# Ver dimensiones y primeras filas
dim(base1_c)
## [1] 722  13
head(base1_c, 3)
## # A tibble: 3 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1209 Zona N… 02          5     320       150            2      4            6
## 2  1592 Zona N… 02          5     780       380            2      3            3
## 3  4057 Zona N… 02          6     750       445           NA      7            6
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
head(data, 3) 
## # A tibble: 3 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1147 Zona O… <NA>        3     250        70            1      3            6
## 2  1169 Zona O… <NA>        3     320       120            1      2            3
## 3  1350 Zona O… <NA>        3     350       220            2      2            4
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# Guardar la base filtrada
# write.csv(base1, "base1_casas_zona_norte.csv", row.names = FALSE)

Se verifica que el filtrado de las varibales tipo y zona se haya llevado a cabo correctamente comparando con el dataset original.

# Tabla comparativa para 'tipo'
tabla_tipo <- data.frame(
  Tipo = union(unique(data$tipo), unique(base1_c$tipo)),
  Original = as.numeric(table(data$tipo)[union(unique(data$tipo), unique(base1_c$tipo))]),
  Filtrada = as.numeric(table(base1_c$tipo)[union(unique(data$tipo), unique(base1_c$tipo))])
)

# Tabla comparativa para 'zona'
tabla_zona <- data.frame(
  Zona = union(unique(data$zona), unique(base1_c$zona)),
  Original = as.numeric(table(data$zona)[union(unique(data$zona), unique(base1_c$zona))]),
  Filtrada = as.numeric(table(base1_c$zona)[union(unique(data$zona), unique(base1_c$zona))])
)

# Muestra las tablas
kable(tabla_tipo, caption = "Tabla 1. Comparación de distribución de 'tipo' entre base original y filtrada")
Tabla 1. Comparación de distribución de ‘tipo’ entre base original y filtrada
Tipo Original Filtrada
Casa 3219 722
Apartamento 5100 NA
NA NA NA
kable(tabla_zona, caption = "Tabla 2. Comparación de distribución de 'zona' entre base original y filtrada")
Tabla 2. Comparación de distribución de ‘zona’ entre base original y filtrada
Zona Original Filtrada
Zona Oriente 351 NA
Zona Sur 4726 NA
Zona Norte 1920 722
Zona Oeste 1198 NA
Zona Centro 124 NA
NA NA NA

Tambien, se mira si el dataset filtrado tiene valores repetidos, en donde no se encontraron registros duplicados.

# Cuántas filas duplicadas hay en todo el dataset
sum(duplicated(base1_c))
## [1] 0
# Visualización de las filas duplicadas
base1_c[duplicated(base1_c), ]
## # A tibble: 0 × 13
## # ℹ 13 variables: id <dbl>, zona <chr>, piso <chr>, estrato <dbl>,
## #   preciom <dbl>, areaconst <dbl>, parqueaderos <dbl>, banios <dbl>,
## #   habitaciones <dbl>, tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

Se realiza la ubicación de las viviendas del filtro en un mapa.

# 1. Base original
oferta_original <- data.frame(
  lat = data$latitud,
  long = data$longitud,
  tipo = data$tipo,
  zona = data$zona
)

# 2. Base filtrada (Casas - Zona Norte)
oferta_filtrada <- data.frame(
  lat = base1_c$latitud,
  long = base1_c$longitud,
  tipo = base1_c$tipo,
  zona = base1_c$zona
)

# 3. Crea mapa con ambas bases
map <- leaflet() %>%
  addTiles() %>%
  # Puntos de la base original en azul
  addCircleMarkers(
    data = oferta_original,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "blue",
    radius = 4,
    opacity = 0.6,
    group = "Base Original"
  ) %>%
  # Puntos de la base filtrada en rojo
  addCircleMarkers(
    data = oferta_filtrada,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "red",
    radius = 5,
    opacity = 0.7,
    group = "Base Filtrada"
  ) %>%
  addLayersControl(
    overlayGroups = c("Base Original", "Base Filtrada"),
    options = layersControlOptions(collapsed = FALSE)
  )
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored
map

Como se puede observar en el mapa, hay gran cantidad de casas que estan por fuera de la zona norte de la ciudad de Cali, es decir, existen datos inconsistentes (la columna zona no coincide con la localización real de latitud/longitud). Esto se pudo presentar por errores en la variable zona (ej. registro marcado como “Zona Norte” pero geográficamente está en otra zona).

Debido a la problematica anterior, se toma la desición de realizar la respectiva corrección previo a continuar con el siguiente apartado.

Para llevar a cabo lo anterior se define una longitud y latitud aproximados para la zona norte y se realiza la corrección. Para ello se determinó cual era el estadístico de las variables latitud y longitud y se determino la zona norte a criterio personal (visual).

base2_c = base1_c


# Se define lat/long aproximados para Zona Norte
base2_c <- base1_c %>%
  filter(latitud > 3.43 & latitud < 3.55 & 
         longitud > -76.55 & longitud < -76.45)

# Se rectifica la cantidad eliminada
nrow(base1_c)          # cantidad filtrada
## [1] 722
nrow(base2_c)          # cantidad corregida
## [1] 613

Se vuelve a cargar el mapa con las correciones en la longitud y latitud con respecto a la zona.

# 1. Base original
oferta_original <- data.frame(
  lat = data$latitud,
  long = data$longitud,
  tipo = data$tipo,
  zona = data$zona
)

# 2. Base filtrada (Casas - Zona Norte)
oferta_filtrada <- data.frame(
  lat = base1_c$latitud,
  long = base1_c$longitud,
  tipo = base1_c$tipo,
  zona = base1_c$zona
)

# 3. Base corregida
oferta_corregida <- data.frame(
  lat = base2_c$latitud,
  long = base2_c$longitud,
  tipo = base2_c$tipo,
  zona = base2_c$zona
)


# 4. Crea mapa con las bases
map <- leaflet() %>%
  addTiles() %>%
  # Puntos de la base en amarillo (original)
  addCircleMarkers(
    data = oferta_original,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "yellow",
    radius = 5,
    opacity = 0.7,
    group = "Oferta original"
  ) %>%
  # Puntos de la base en rojo (filtrada)
  addCircleMarkers(
    data = oferta_filtrada,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "red",
    radius = 5,
    opacity = 0.7,
    group = "Oferta filtrada"
  ) %>%
  # Puntos de la base en azul (corregida)
  addCircleMarkers(
    data = oferta_corregida,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "blue",
    radius = 4,
    opacity = 0.6,
    group = "Oferta corregida"
  ) %>%

    addLayersControl(
    overlayGroups = c("Oferta corregida", "Oferta filtrada", "Oferta original"),
    options = layersControlOptions(collapsed = FALSE)
  )
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored
map

Respuesta 2 - Vivienda 1 (casa)

Con las variables seleccionadas propuestas en la actividad: área construida, estrato, número de cuartos, número de parqueaderos y número de baños se crea una copia del filtro.

base3_c = base2_c

# Selección de variables 
base3_c <- base2_c %>%
  select(preciom, areaconst, estrato, banios, habitaciones, zona, parqueaderos, longitud, latitud)

Posterior, se busca valores faltantes en el dataset

# Se revisar valores faltantes
colSums(is.na(base3_c))
##      preciom    areaconst      estrato       banios habitaciones         zona 
##            0            0            0            0            0            0 
## parqueaderos     longitud      latitud 
##          215            0            0

En estos se encontró que, solo la variable parqueaderos presenta valores faltantes. Para corregir este hallazgo se decide imputar con la mediana

# Se imputa 'parqueaderos' con la mediana


base3_c <- base3_c %>%
  mutate(parqueaderos = ifelse(is.na(parqueaderos),
                               median(parqueaderos, na.rm = TRUE),
                               parqueaderos))
# Se verifica nuevamente los datos faltantes
colSums(is.na(base3_c))
##      preciom    areaconst      estrato       banios habitaciones         zona 
##            0            0            0            0            0            0 
## parqueaderos     longitud      latitud 
##            0            0            0

Una vez limpio el filtro, se mira la correlación de las variables.

# Matriz de correlación entre variables numéricas
corr_matrix <- cor(base3_c %>% select(-zona), use = "complete.obs")

# Gráfico de correlación
plot_ly(
  x = colnames(corr_matrix),
  y = rownames(corr_matrix),
  z = corr_matrix,
  type = "heatmap",
  colors = colorRamp(c("blue", "white", "red"))
) %>%
  layout(title = "Matriz de Correlación entre variables numéricas")

De acuerdo con la matriz de correlación entre la variable respuesta (precio de la casa) en función del área construida, estrato,numero de baños, numero de habitaciones, parqueadero y zona donde se ubica la vivienda, se concluye que:

  1. preciom vs areaconst (rojo claro) presentan una correlación positiva significativa, pues entre mayor área construida, mayor precio de la vivienda
  2. preciom vs estrato y``banios (ligeramente positivo, tono rosado) correlación positiva moderada, indicando que el precio aumenta con el estrato socioeconómico y la cantidad de baños, aunque no tan fuerte como con el área. Esto refleja que la ubicación/estrato influye, pero no tanto como el tamaño.
  3. preciom vs habitaciones y parqueaderos (casi neutro, colores claros) relación débil. Aunque intuitivamente más baños/habitaciones deberían aumentar el precio, aquí el efecto es menor comparado con área y estrato. Lo anterior se puede deber a que probablemente porque el número de cuartos está muy ligado al tamaño de la casa (ya reflejado en areaconst).
# Precio vs Área construida 
plot_ly(base3_c, x = ~areaconst, y = ~preciom,
        type = "scatter", mode = "markers",
        color = ~estrato, size = ~habitaciones,
        text = ~paste("Baños:", banios, "<br>Habitaciones:", habitaciones, "<br>Zona:", zona)) %>%
  layout(title = "Precio vs Área construida",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones)"))
## Warning: `line.width` does not currently support multiple values.

Continuando con el gráfico de precio vs área teniendo en cuenta el estrato socieconómico se tiene que:

  1. La nube de puntos muestra una tendencia ascendente, es decir, a mayor área construida, mayor precio de la vivienda, uan relacion directamente proporcional, la cual coincide con la correlación fuerte que ya vimos en la matriz.
  2. Se cuenta con variabilidad del precio en áreas similares. Como se puede observar para un mismo rango de área (ej. 200–400 m²), hay precios que varían bastante (unos de 200 millones y otros de 1.000 millones). Eso nos indica que otros factores además del área (estrato, ubicación, calidad de materiales) influyen mucho en el precio.
  3. De acuerdo con los estratos entre mas altos (amarillo/verde ~6), los precios son mayores incluso con áreas moderadas. Esto confirma que el estrato incrementa el precio independiente del área.
  4. Hay puntos alejados de la tendencia general, por ejemplo una vivienda de casi 1500 m² con precio bajo (~500 millones), parece subvalorada o error en datos. Continuando, viviendas de área < 200 m² con precios cercanos a 1000–1500 millones, lo anterior se puede presentar dado que se tratan de apartamentos de lujo en estratos altos. Estos fuera de tendencia pueden distorsionar el modelo, conviene revisarlos.
# Precio según Estrato
plot_ly(base3_c, x = ~factor(estrato), y = ~preciom,
        type = "box", color = ~factor(estrato)) %>%
  layout(title = "Distribución del Precio según Estrato",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio (millones)"))

Ahora, para el gráfico de distribución de precion acorde con el estrato se tiene que:

  1. Existe una relación directa y positiva entre estrato y precio.

  2. Los estratos más altos no solo tienen precios más altos, sino también más dispersión.

  3. Hay outliers muy relevantes que deben revisarse: pueden ser errores de registro o propiedades atípicas (ej. mansiones de lujo).

# Precio según Zona
plot_ly(base3_c, x = ~zona, y = ~preciom,
        type = "box", color = ~zona) %>%
  layout(title = "Distribución del Precio por Zona",
         xaxis = list(title = "Zona"),
         yaxis = list(title = "Precio (millones)"))

Por último, en el gráfico de distribución del precio por zona se tiene que:

  1. La mayoría de las viviendas están en un rango de 250–500 millones, pero hay una proporción importante de propiedades de lujo que superan 1000 millones.
  2. La alta cantidad de outliers refleja un mercado inmobiliario heterogéneo en la Zona Norte, es decir, puede que se traten de viviendas lujosas y por ellos explicaria los precio tan elevados que se presentan en esta zona.

Respuesta 3 - Vivienda 1 (casa)

Una vez limpio el dataset, se realiza la división en el set de entrenamiento y prueba.

base4_c = base3_c

set.seed(123)  # Semilla para reproducibilidad

# 1. División de los datos (70% entrenamiento, 30% prueba)
split <- sample.split(base4_c$preciom, SplitRatio = 0.7)

train_data <- subset(base4_c, split == TRUE)
test_data  <- subset(base4_c, split == FALSE)

# 2. Verificar tamaños
nrow(train_data)  # número de observaciones entrenamiento
## [1] 446
nrow(test_data)   # número de observaciones prueba
## [1] 167
modelo_C <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train_data)
summary(modelo_C)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train_data)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -747.85  -70.93  -18.53   41.23 1017.54 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -248.36056   37.06000  -6.702 6.33e-11 ***
## areaconst       0.88053    0.05735  15.353  < 2e-16 ***
## estrato        69.20206    9.02952   7.664 1.16e-13 ***
## habitaciones   16.09903    5.48774   2.934  0.00353 ** 
## parqueaderos   21.42888    7.00744   3.058  0.00236 ** 
## banios         11.97091    6.98645   1.713  0.08733 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 152.1 on 440 degrees of freedom
## Multiple R-squared:  0.6799, Adjusted R-squared:  0.6763 
## F-statistic:   187 on 5 and 440 DF,  p-value: < 2.2e-16

A partir del mode lo de regresión lineal múltiple realizadas sobre las variables área construida, estrato, número de cuartos, número de parqueaderos y número de baños, se puede deducir que:

  1. Área construida (0.88, p<0.001): Por cada metro cuadrado adicional, el precio aumenta en 0.88 millones aprox. Es estadísticamente significativo y tiene sentido, pues a mayor área construida, mayor precio.

  2. Estrato (69.2, p<0.001): Por cada nivel adicional de estrato, el precio aumenta en promedio 69.2 millones. Lo anterior es oligos dado que, estratos más altos se asocian con mayor valorización de inmuebles.

  3. Número de habitaciones (16.1, p=0.004): El efecto no es estadísticamente significativo. Aunque se esperaría que más habitaciones aumentaran el precio, el resultado sugiere que su efecto ya está capturado por el área construida. Es decir, más habitaciones no implican necesariamente mayor precio si el área total no crece proporcionalmente.

  4. Número de parqueaderos (21.4, p<0.002): Cada parqueadero adicional incrementa el precio en promedio 23.0 millones.

  5. Número de baños (12.0, p<0.087): Cada baño adicional aumenta el precio en promedio 20.48 millones. Es estadísticamente significativo y lógico: más baños se asocian a mayor confort y, por tanto, mayor valorización.

  6. Ajuste del modelo: R² = 0.6799 El modelo explica el 65.1% de la variabilidad en el precio.

  7. R² ajustado = 0.6763. Corrige por el número de variables y muestra que el ajuste sigue siendo bueno.

En conclusión, el modelo actual explica el 68% de la variabilidad del precio, sin embargo, aún queda un 32% sin explicar, lo que indica que hay factores importantes por fuera. Este modelo podrias mejorar si se incorporan variables de ubicación, características físicas más detalladas y calidad del inmueble, además se podrian considerar transformaciones logarítmicas. Para finalizar, se puede llegar a considerar modelos más avanzados como Random Forest o XGBoost.

Para finalizar, se evalúa el modelo creado

# Predicciones sobre test
pred_test <- predict(modelo_C, newdata = test_data)

# Comparar con valores reales
resultados <- data.frame(
  Real = test_data$preciom,
  Predicho = pred_test
)

# Calcular métricas
rmse_val <- rmse(resultados$Real, resultados$Predicho)
mae_val  <- mae(resultados$Real, resultados$Predicho)

cat("RMSE:", rmse_val, "\nMAE:", mae_val)
## RMSE: 161.9142 
## MAE: 98.06947
  • MAE (Mean Absolute Error = 98.07)

En promedio, el modelo se equivoca en 98 millones de pesos respecto al valor real de una vivienda.

  • RMSE (Root Mean Squared Error = 161.91)

El error cuadrático medio penaliza más fuertemente los errores grandes.

Un RMSE de 161 millones indica que, cuando el modelo se equivoca mucho, esos errores son considerables.

  • Conclusión práctica

    El modelo tiene un nivel de error alto para predicciones individuales.

    Para tomar decisiones de inversión (como en el caso de la empresa con crédito preaprobado de 350 millones), debes tener cuidado: el modelo puede subestimar o sobreestimar el precio real en un rango amplio.

Respuesta 4 - Vivienda 1 (casa)

# Ajustar el modelo
modelo_C <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train_data)


# 1. Linealidad e independencia
par(mfrow = c(2,2))
plot(modelo_C)   

Ahora, se realizará la validación de supuestos del modelo e interprete los resultados. Primero, se discutirá sobre la linealidad e independencia, de los gráficos se puede concluir que:

Residuals vs Fitted (Residuos vs valores ajustados):

  • Función: linealidad y homocedasticidad (varianza constante de los errores). Los residuos se concentran alrededor de cero, pero se nota cierta dispersión mayor en valores altos (cola derecha).

  • Significado: Podría haber heterocedasticidad (la varianza de los errores aumenta con el precio). Esto afecta la eficiencia de los estimadores.

Normal Q-Q

  • Interpretación: si los residuos siguen una distribución normal. La mayoría de puntos sigue la línea diagonal, aunque en las colas (extremos) se apartan. Sin embargo, hay cierta no normalidad en los extremos (outliers o valores atípicos).

Scale-Location (√estandarized residuals vs Fitted)

  • Función: homocedasticidad (igual dispersión). Se ve una ligera pendiente ascendente (los residuos aumentan a mayor fitted value), lo cual refuerza la idea de heterocedasticidad.

  • Sugerencia: prueba transformación logarítmica de la variable dependiente.

Residuals vs Leverage (residuos estandarizados vs apalancamiento)

  • Función: observaciones influyentes (puntos que afectan de forma desproporcionada el modelo).

  • Observaciones: Hay algunas observaciones identificadas que podrían ser influyentes y estén afectando mucho los coeficientes del modelo.

  • Sugerencia: revisar estas observaciones y decidir si mantenerlas (casos reales) o tratarlas como atípicos.

Conclusión General

El modelo cumple parcialmente los supuestos, pero se observan:

  1. Heterocedasticidad (varianza no constante).
  2. Cierta no normalidad en los extremos.
  3. Observaciones influyentes que pueden estar afectando resultados.
# 2. Homocedasticidad

# Prueba de Breusch-Pagan
bptest(modelo_C)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo_C
## BP = 60.416, df = 5, p-value = 9.972e-12

Breusch-Pagan test (Heterocedasticidad)

  • Hipótesis nula (H₀): Los residuos tienen varianza constante (homocedasticidad).

  • Hipótesis alternativa (H₁): Los residuos presentan heterocedasticidad.


De acuerdo con los resultados del valor P, el cual dio como resultados menor que 0.05, se rechaza H₀ y por tanto, el modelo presenta heterocedasticidad, que afecta la eficiencia de los estimadores. Adicional, los residuos no son normales, puede estar asociado a outliers o colas pesadas lo cual se visualizó en los gráficos anteriores.

Se recomienda aplicar transformaciones y/o robustez estadística para mejorar la validez de los resultados.

# 3. Normalidad de los residuos

# Histograma y Q-Q plot
hist(residuals(modelo_C), main="Histograma de residuos", xlab="Residuos")

qqnorm(residuals(modelo_C))
qqline(residuals(modelo_C), col="red")

# Prueba de Shapiro-Wilk (para normalidad)
shapiro.test(residuals(modelo_C))  
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(modelo_C)
## W = 0.80017, p-value < 2.2e-16

Test de Shapiro-Wilk

Evalúa si los datos (en este caso, los residuos del modelo) siguen una distribución normal.

  • Hipótesis nula (H0): los residuos siguen una distribución normal.

  • Hipótesis alternativa (H1): los residuos no siguen una distribución normal.

Como el p-value es extremadamente pequeño (< 0.05), se rechaza la hipótesis nula.
Esto significa que los residuos no son normales, el supuesto de normalidad del modelo de regresión no se cumple.

Continuando, el gráfico confirma lo que indicó el test de Shapiro-Wilk: los residuos no siguen una distribución normal. Esto sugiere que el modelo podría estar afectado por valores atípicos o que la variable dependiente no es adecuada sin transformación.

# 4. Multicolinealidad

vif(modelo_C)   # Variance Inflation Factor
##    areaconst      estrato habitaciones parqueaderos       banios 
##     1.692667     1.463118     1.920150     1.147300     2.189137

Valores reportados:

  • areaconst = 1.69

  • estrato = 1.46

  • habitaciones = 1.92

  • parqueaderos = 1.15

  • banios = 2.19

Ahora, teniendo en cuenta el Factor de Inflación de la Varianza (VIF), el cual es un indicador que se usa en regresión lineal para detectar multicolinealidad entre las variables explicativas.

  • VIF ≈ 1 → Sin colinealidad.

  • VIF entre 1 y 5 → Colinealidad baja/moderada (aceptable).

  • VIF > 10 → Colinealidad severa.

Se obtuvieron valores entre 1.1 y 2.2, no hay problemas de multicolinealidad en el modelo.

# 5. Autocorrelación de errores

# Test de Durbin-Watson
dwtest(modelo_C)
## 
##  Durbin-Watson test
## 
## data:  modelo_C
## DW = 1.5962, p-value = 7.538e-06
## alternative hypothesis: true autocorrelation is greater than 0

Test de Durbin-Watson

Se utiliza en regresión lineal para detectar autocorrelación de primer orden en los residuos del modelo.

El estadístico toma valores entre 0 y 4:

  • DW ≈ 2 → No hay autocorrelación (lo ideal).

  • DW < 2 → Autocorrelación positiva (los errores tienden a seguir el mismo signo).

  • DW > 2 → Autocorrelación negativa (los errores tienden a alternar signos).

En este caso, DW ≈ 1.5962, lo que sugiere ligera autocorrelación positiva.

  • Hipótesis nula (H₀): No existe autocorrelación entre los residuos.

  • Hipótesis alternativa (H₁): Existe autocorrelación positiva entre los residuos.

Como el p-valor = es menor a 0.01 < 0.05, se rechaza H₀, confirmando autocorrelación positiva de los residuos.

Respuesta 5 - Vivienda 1 (casa)

A continuación se va a predecir el precio de la vivienda con las características de la primera solicitud.

# Crear el dataframe de la vivienda
vivienda1 <- data.frame(
  areaconst    = 200,
  estrato      = c(4, 5),   # dos opciones
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

# Predecir con tu modelo (ejemplo: modelo_C)
pred_vivienda1 <- predict(
  modelo_C, 
  newdata = vivienda1, 
  interval = "prediction", 
  level = 0.95
)

# Combinar resultados
resultado <- cbind(vivienda1, pred_vivienda1)
print(resultado)
##   areaconst estrato habitaciones parqueaderos banios      fit      lwr      upr
## 1       200       4            4            1      2 314.3202 14.46388 614.1765
## 2       200       5            4            1      2 383.5222 82.83220 684.2123

fit → es el valor predicho del precio (en millones).

  • Para estrato 4 ≈ 314.3 millones

  • Para estrato 5 ≈ 383.5 millones

lwr y upr → intervalo de predicción al 95%.

  • Estrato 4: el precio podría estar entre 14.5 y 614.2 millones.

  • Estrato 5: el precio podría estar entre 82.8 y 684.2 millones.

Comparación con el crédito (350 millones):

  • Estrato 4: el valor estimado (314.3) está por debajo del crédito aprobado, sí alcanzaría.

  • Estrato 5: el valor estimado (383.5) está ligeramente por encima del crédito aprobado, pero como el intervalo cubre valores menores a 350, existe riesgo de que no alcance dependiendo de la vivienda que se seleccione.

Respuesta 6 - Vivienda 1 (casa)

Ahora, en base a los requerimiento del cliente 1 se busca en la base de datos limpia que contenia todos los datos, las viviendas que cumplan con los requisitos.

Para ello se tiene en cuenta el número de habitaciones, área construida, parqueadero, baños, estrato, de estas variables se prioriza que el estrato sea preferiblemente estrato 5, que el área de la vivienda se encuentre entre los rango 180 - 230 metros y el el credito pre-aprobado.

data_caso1 = base4_c

base_pred <- data_caso1 %>%
  mutate(pred = preciom)

# Ranking de priorización

rango_inferior_area <- 180
rango_superior_area <- 230
credito_max <- 350

candidatas <- base_pred %>%
  filter(
    areaconst >= rango_inferior_area,
    areaconst <= rango_superior_area,
    parqueaderos >= 1,
    banios >= 2,
    habitaciones >= 4,
    estrato %in% c(4,5),
    pred <= credito_max
  )

candidatas_rank <- candidatas %>%
  mutate(
    gap_area   = abs(areaconst - 200),
    ahorro     = pmax(0, credito_max - pred),
    pref_estr5 = ifelse(estrato == 5, 1, 0),
    score      = rescale(-gap_area) + rescale(ahorro) + 1 * pref_estr5
  ) %>%
  arrange(desc(score))

# Selección de las 5 mejores

top5 <- candidatas_rank %>% slice_head(n = 5)

top5
## # A tibble: 5 × 14
##   preciom areaconst estrato banios habitaciones zona       parqueaderos longitud
##     <dbl>     <dbl>   <dbl>  <dbl>        <dbl> <chr>             <dbl>    <dbl>
## 1     300       205       5      5            6 Zona Norte            2    -76.5
## 2     320       200       5      4            4 Zona Norte            2    -76.5
## 3     335       202       5      4            5 Zona Norte            1    -76.5
## 4     320       210       5      3            5 Zona Norte            2    -76.5
## 5     340       203       5      3            4 Zona Norte            2    -76.5
## # ℹ 6 more variables: latitud <dbl>, pred <dbl>, gap_area <dbl>, ahorro <dbl>,
## #   pref_estr5 <dbl>, score <dbl>

De acuerdo con las características deseadas por el cliente, se encontraron este top 5 de viviendas que cumple con las especificaciones, adicional se tiene que :

  1. Se priorizó que el estrato fuera 5
  2. Varias de estas viviendas tienen más de 200 metros.
  3. Presentan un precio menor al presupuesto, es decir, el cliente tendria un ahorro.
  4. Varias de estas viviendas tienen más de un parqueadero, es decir, el cliente tendria uno de sobra a su solicitud.
  5. Todas estas viviendas tienen más 2 baños.
  6. Varias de estas viviendas tienen más 4 habitaciones.

A continuación se muestra la ubicación geográfica del top 5 de viviendas

# Ubicación geográfica de las viviendas top 5

leaflet(top5) %>%
  addTiles() %>%
  addMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste0(
      "Área: ", areaconst, " m²<br>",
      "Estrato: ", estrato, "<br>",
      "Habitaciones: ", habitaciones, "<br>",
      "Baños: ", banios, "<br>",
      "Parqueaderos: ", parqueaderos, "<br>",
      "Precio estimado: ", round(pred, 1), " millones"
    )
  )

Caso 2 - Vivienda 2

Respuesta 1 - Vivienda 2 (apartamento)

Filtro en la base de datos que incluya las ofertas de: apartamento, de la zona sur de la ciudad.

base1_a = data

# Filtrar solo Casas en Zona Norte
base1_a <- data %>%
  filter(tipo == "Apartamento", zona == "Zona Sur")

# Ver dimensiones y primeras filas
dim(base1_a)
## [1] 2787   13
head(base1_a, 3)
## # A tibble: 3 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  5098 Zona S… 05          4     290        96            1      2            3
## 2   698 Zona S… 02          3      78        40            1      1            2
## 3  8199 Zona S… <NA>        6     875       194            2      5            3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
head(data, 3) 
## # A tibble: 3 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1147 Zona O… <NA>        3     250        70            1      3            6
## 2  1169 Zona O… <NA>        3     320       120            1      2            3
## 3  1350 Zona O… <NA>        3     350       220            2      2            4
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# Guardar la base filtrada
#write.csv(base1_a, "base1_aparta_zona_sur.csv", row.names = FALSE)

Se verifica que el filtrado de las varibales tipo y zona se haya llevado a cabo correctamente comparando con el dataset original.

# Tabla comparativa para 'tipo'
tabla_aparta <- data.frame(
  Tipo = union(unique(data$tipo), unique(base1_a$tipo)),
  Original = as.numeric(table(data$tipo)[union(unique(data$tipo), unique(base1_a$tipo))]),
  Filtrada = as.numeric(table(base1_a$tipo)[union(unique(data$tipo), unique(base1_a$tipo))])
)

# Tabla comparativa para 'zona'
tabla_zona <- data.frame(
  Zona = union(unique(data$zona), unique(base1_a$zona)),
  Original = as.numeric(table(data$zona)[union(unique(data$zona), unique(base1_a$zona))]),
  Filtrada = as.numeric(table(base1_a$zona)[union(unique(data$zona), unique(base1_a$zona))])
)

# Muestra las tablas
kable(tabla_aparta, caption = "Tabla 3. Comparación de distribución de 'tipo' entre base original y filtrada")
Tabla 3. Comparación de distribución de ‘tipo’ entre base original y filtrada
Tipo Original Filtrada
Casa 3219 NA
Apartamento 5100 2787
NA NA NA
kable(tabla_zona, caption = "Tabla 4. Comparación de distribución de 'zona' entre base original y filtrada")
Tabla 4. Comparación de distribución de ‘zona’ entre base original y filtrada
Zona Original Filtrada
Zona Oriente 351 NA
Zona Sur 4726 2787
Zona Norte 1920 NA
Zona Oeste 1198 NA
Zona Centro 124 NA
NA NA NA

Tambien, se mira si el dataset filtrado tiene valores repetidos, en donde no se encontraron registros duplicados.

# Cuántas filas duplicadas hay en todo el dataset
sum(duplicated(base1_a))
## [1] 0
# Visualización de las filas duplicadas
base1_a[duplicated(base1_a), ]
## # A tibble: 0 × 13
## # ℹ 13 variables: id <dbl>, zona <chr>, piso <chr>, estrato <dbl>,
## #   preciom <dbl>, areaconst <dbl>, parqueaderos <dbl>, banios <dbl>,
## #   habitaciones <dbl>, tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

Se realiza la ubicación de las viviendas del filtro en un mapa.

# 1. Base original
oferta_original <- data.frame(
  lat = data$latitud,
  long = data$longitud,
  tipo = data$tipo,
  zona = data$zona
)

# 2. Base filtrada (Apartamento - Zona Sur)
oferta_filtrada_apart <- data.frame(
  lat = base1_a$latitud,
  long = base1_a$longitud,
  tipo = base1_a$tipo,
  zona = base1_a$zona
)

# 3. Crea mapa con ambas bases
map_apart <- leaflet() %>%
  addTiles() %>%
  # Puntos de la base original en azul
  addCircleMarkers(
    data = oferta_original,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "blue",
    radius = 4,
    opacity = 0.6,
    group = "Base Original"
  ) %>%
  # Puntos de la base filtrada en rojo
  addCircleMarkers(
    data = oferta_filtrada_apart,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "red",
    radius = 5,
    opacity = 0.7,
    group = "Base Filtrada"
  ) %>%
  addLayersControl(
    overlayGroups = c("Base Original", "Base Filtrada"),
    options = layersControlOptions(collapsed = FALSE)
  )
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored
map_apart

Como se puede observar en el mapa, hay gran cantidad de apartamentos que estan por fuera de la zona sur de la ciudad de Cali, es decir, existen datos inconsistentes (la columna zona no coincide con la localización real de latitud/longitud). Esto se pudo presentar por errores en la variable zona (ej. registro marcado como “Zona Sur” pero geográficamente está en otra zona).

Debido a la problematica anterior, se toma la desición de realizar la respectiva corrección previo a continuar con el siguiente apartado.

Para llevar a cabo lo anterior se define una longitud y latitud aproximados para la zona sur y se realiza la corrección. Para ello se determinó cual era el estadístico de las variables latitud y longitud y se determino la zona sur a criterio personal (visual).

base2_a = base1_a


# Se define lat/long aproximados para Zona Norte
base2_a <- base1_a %>%
  filter(latitud > 3.334 & latitud < 3.41 & 
         longitud > -76.56 & longitud < -76.46)

# Se rectifica la cantidad eliminada
nrow(base1_a)          # cantidad filtrada
## [1] 2787
nrow(base2_a)          # cantidad corregida
## [1] 2179
# 1. Base original
oferta_original <- data.frame(
  lat = data$latitud,
  long = data$longitud,
  tipo = data$tipo,
  zona = data$zona
)

# 2. Base filtrada (Apartamentos - Zona Sur)
oferta_filtrada_apart <- data.frame(
  lat = base1_a$latitud,
  long = base1_a$longitud,
  tipo = base1_a$tipo,
  zona = base1_a$zona
)

# 3. Base corregida
oferta_corregida_apart <- data.frame(
  lat = base2_a$latitud,
  long = base2_a$longitud,
  tipo = base2_a$tipo,
  zona = base2_a$zona
)


# 4. Crea mapa con las bases
map_apart <- leaflet() %>%
  addTiles() %>%
  # Puntos de la base en amarillo (original)
  addCircleMarkers(
    data = oferta_original,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "yellow",
    radius = 5,
    opacity = 0.7,
    group = "Oferta original"
  ) %>%
  # Puntos de la base en rojo (filtrada)
  addCircleMarkers(
    data = oferta_filtrada_apart,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "red",
    radius = 5,
    opacity = 0.7,
    group = "Oferta filtrada"
  ) %>%
  # Puntos de la base en azul (corregida)
  addCircleMarkers(
    data = oferta_corregida_apart,
    lng = ~long,
    lat = ~lat,
    popup = ~paste("Tipo:", tipo, "<br>Zona:", zona),
    color = "blue",
    radius = 4,
    opacity = 0.6,
    group = "Oferta corregida"
  ) %>%

    addLayersControl(
    overlayGroups = c("Oferta corregida", "Oferta filtrada", "Oferta original"),
    options = layersControlOptions(collapsed = FALSE)
  )
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored
map_apart

Respuesta 2 - Vivienda 2 (apartamento)

Con las variables seleccionadas propuestas en la actividad: área construida, estrato, número de cuartos, número de parqueaderos y número de baños se crea una copia del filtro.

base3_a = base2_a

# Selección de variables 
base3_a <- base2_a %>%
  select(preciom, areaconst, estrato, banios, habitaciones, zona, parqueaderos, longitud, latitud)

Posterior, se busca valores faltantes en el dataset

# Se revisar valores faltantes
colSums(is.na(base3_a))
##      preciom    areaconst      estrato       banios habitaciones         zona 
##            0            0            0            0            0            0 
## parqueaderos     longitud      latitud 
##          289            0            0

En estos se encontró que, solo la variable parqueaderos presenta valores faltantes. Para corregir este hallazgo se decide imputar con la mediana, por se menos sensible a los atípicos.

# Se imputa 'parqueaderos' con la mediana


base3_a <- base3_a %>%
  mutate(parqueaderos = ifelse(is.na(parqueaderos),
                               median(parqueaderos, na.rm = TRUE),
                               parqueaderos))
# Se revisar valores faltantes
colSums(is.na(base3_a))
##      preciom    areaconst      estrato       banios habitaciones         zona 
##            0            0            0            0            0            0 
## parqueaderos     longitud      latitud 
##            0            0            0

Una vez limpio el filtro, se mira la correlación de las variables.

# Matriz de correlación entre variables numéricas
corr_matrix_apart <- cor(base3_a %>% select(-zona), use = "complete.obs")

# Gráfico de correlación
plot_ly(
  x = colnames(corr_matrix_apart),
  y = rownames(corr_matrix_apart),
  z = corr_matrix,
  type = "heatmap",
  colors = colorRamp(c("blue", "white", "red"))
) %>%
  layout(title = "Matriz de Correlación entre variables numéricas")

De acuerdo con la matriz de correlación entre la variable respuesta (precio del apartamento) en función del área construida, estrato,numero de baños, numero de habitaciones, parqueadero y zona donde se ubica la vivienda, se concluye que:

  1. preciom vs areaconst, banios parqueaderos (rojo claro) presentan una correlación positiva significativa, pues entre mayor área construida, esta tendra la posibilidad de tener mas baños y parqueadero, lo que aumenta el precio de la vivienda, ya que traduce amayor área.
  2. preciom vs estrato (rojo claro) correlación positiva moderada, indicando que el precio aumenta con el estrato socioeconómico, aunque no tan fuerte como con el área. Esto refleja que la ubicación/estrato influye, pero no tanto como el tamaño.
  3. preciomvs habitaciones (casi neutro, colores claros) relación débil. Aunque intuitivamente más habitaciones deberían aumentar el precio, aquí el efecto es menor comparado con área y estrato. Lo anterior se puede deber a que probablemente porque el número de cuartos está muy ligado al tamaño de la casa (ya reflejado en areaconst).
# Precio vs Área construida 
plot_ly(base3_a, x = ~areaconst, y = ~preciom,
        type = "scatter", mode = "markers",
        color = ~estrato, size = ~habitaciones,
        text = ~paste("Baños:", banios, "<br>Habitaciones:", habitaciones, "<br>Zona:", zona)) %>%
  layout(title = "Precio vs Área construida",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones)"))
## Warning: `line.width` does not currently support multiple values.

Continuando con el gráfico de precio vs área teniendo en cuenta el estrato socieconómico se tiene que:

  1. La nube de puntos muestra una tendencia ascendente, es decir, a mayor área construida, mayor precio de la vivienda, uan relacion directamente proporcional, la cual coincide con la correlación fuerte que ya vimos en la matriz.
  2. Se cuenta con variabilidad del precio en áreas similares. Como se puede observar para un mismo rango de área (ej. 40–120 m²), hay precios que varían bastante (unos de 100 millones y otros de 400 millones). Eso nos indica que otros factores además del área (estrato, ubicación, calidad de materiales) influyen mucho en el precio.
  3. De acuerdo con los estratos entre mas altos (amarillo/verde ~6), los precios son mayores incluso con áreas moderadas. Esto confirma que el estrato incrementa el precio independiente del área.
  4. Hay puntos alejados de la tendencia general, por ejemplo una vivienda de casi 930 m² con precio bajo (~300 millones), parece subvalorada o error en datos. Continuando, viviendas de área entre 200 m² - 400 m² con precios cercanos a 1500–1800 millones, lo anterior se puede presentar dado que se tratan de apartamentos de lujo en estratos altos. Estos fuera de tendencia pueden distorsionar el modelo, conviene revisarlos.
# Precio según Estrato
plot_ly(base3_a, x = ~factor(estrato), y = ~preciom,
        type = "box", color = ~factor(estrato)) %>%
  layout(title = "Distribución del Precio según Estrato",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio (millones)"))

Ahora, para el gráfico de distribución de precion acorde con el estrato se tiene que:

  1. Existe una relación directa y positiva entre estrato y precio.

  2. Los estratos más altos no solo tienen precios más altos, sino también más dispersión.

  3. Hay outliers muy relevantes que deben revisarse: pueden ser errores de registro o propiedades atípicas (ej. apartamentos de lujo).

# Precio según Zona
plot_ly(base3_a, x = ~zona, y = ~preciom,
        type = "box", color = ~zona) %>%
  layout(title = "Distribución del Precio por Zona",
         xaxis = list(title = "Zona"),
         yaxis = list(title = "Precio (millones)"))

Por último, en el gráfico de distribución del precio por zona, en este caso Sur, se tiene que:

  1. La mayoría de las viviendas están en un rango de 180–340 millones, pero hay una proporción importante de propiedades de lujo que superan 580 millones.
  2. La alta cantidad de outliers refleja un mercado inmobiliario heterogéneo en la Zona Sur, es decir, puede que se traten de apartamentos lujosos y por ellos explicaria los precio tan elevados que se presentan en esta zona.

Respuesta 3 - Vivienda 2 (apartamento)

Una vez limpio el dataset, se realiza la división en el set de entrenamiento y prueba.

base4_a = base3_a

set.seed(456)  # Semilla para reproducibilidad

# 1. División de los datos (70% entrenamiento, 30% prueba)
split_a <- sample.split(base4_a$preciom, SplitRatio = 0.7)

train_data_a <- subset(base4_a, split_a == TRUE)
test_data_a  <- subset(base4_a, split_a == FALSE)

# 2. Verificar tamaños
nrow(train_data_a)  # número de observaciones entrenamiento
## [1] 1548
nrow(test_data_a)   # número de observaciones prueba
## [1] 631
modelo_a <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train_data_a)
summary(modelo_a)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train_data_a)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -900.22  -40.96   -3.85   39.67  937.65 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -294.24893   18.46754 -15.933   <2e-16 ***
## areaconst       1.06295    0.06279  16.928   <2e-16 ***
## estrato        58.11411    3.85540  15.073   <2e-16 ***
## habitaciones  -12.26659    4.76183  -2.576   0.0101 *  
## parqueaderos   92.64674    5.07036  18.272   <2e-16 ***
## banios         52.12901    4.25115  12.262   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 97.93 on 1542 degrees of freedom
## Multiple R-squared:  0.7587, Adjusted R-squared:  0.7579 
## F-statistic: 969.8 on 5 and 1542 DF,  p-value: < 2.2e-16

A partir del mode lo de regresión lineal múltiple realizadas sobre las variables área construida, estrato, número de cuartos, número de parqueaderos y número de baños, se puede deducir que:

  1. Área construida (1.06, p<0.001): Por cada metro cuadrado adicional, el precio aumenta en 1.06 millones aprox. Es estadísticamente significativo y tiene sentido, pues a mayor área construida, mayor precio.

  2. Estrato (58.1, p<0.001): Por cada nivel adicional de estrato, el precio aumenta en promedio 55.1 millones. Lo anterior es oligos dado que, estratos más altos se asocian con mayor valorización de inmuebles.

  3. Número de habitaciones (-12.3, p=0.010): Sorprendentemente, más habitaciones disminuyen el precio en promedio. Puede reflejar que casas con muchas habitaciones no necesariamente son más costosas si el área no aumenta

  4. Número de parqueaderos (92.6, p<0.001): Cada parqueadero adicional incrementa el precio en promedio 86.6 millones.

  5. Número de baños (52.1, p<0.001): Cada baño adicional aumenta el precio en promedio 50.1 millones. Es estadísticamente significativo y lógico: más baños se asocian a mayor confort y, por tanto, mayor valorización.

  6. Ajuste del modelo: R² = 0.7587 El modelo explica el 75.9 % de la variabilidad en el precio.

  7. R² ajustado = 0.7579. Corrige por el número de variables y muestra que el ajuste sigue siendo bueno.

En conclusión, el modelo actual explica el 76% de la variabilidad del precio, sin embargo, aún queda un 24% sin explicar, lo que indica que hay factores importantes por fuera. Este modelo podrias mejorar si se incorporan variables de ubicación, características físicas más detalladas y calidad del inmueble, además se podrian considerar transformaciones logarítmicas.

Para finalizar, se evalúa el modelo creado.

# Predicciones sobre test
pred_test_a <- predict(modelo_a, newdata = test_data_a)

# Comparar con valores reales
resultados_a <- data.frame(
  Real = test_data_a$preciom,
  Predicho = pred_test_a
)


rmse_val_a <- rmse(resultados$Real, resultados_a$Predicho)
## Warning in actual - predicted: longer object length is not a multiple of
## shorter object length
mae_val_a  <- mae(resultados$Real, resultados_a$Predicho)
## Warning in actual - predicted: longer object length is not a multiple of
## shorter object length
cat("RMSE:", rmse_val_a, "\nMAE:", mae_val_a)
## RMSE: 314.2086 
## MAE: 232.9427
  • MAE (Mean Absolute Error = 232.94)

En promedio, el modelo se equivoca en 233 millones de pesos respecto al valor real de una vivienda.

  • RMSE (Root Mean Squared Error = 314.21)

El error cuadrático medio penaliza más fuertemente los errores grandes.

Un RMSE de 314 millones indica que, cuando el modelo se equivoca mucho, esos errores son considerables.

  • Conclusión práctica

    El modelo tiene un nivel de error alto para predicciones individuales.

Respuesta 4 - Vivienda 2 (apartamento)

# Ajustar el modelo
modelo_a <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train_data_a)


# 1. Linealidad e independencia
par(mfrow = c(2,2))
plot(modelo_a) 

Ahora, se realizará la validación de supuestos del modelo e interprete los resultados. Primero, se discutirá sobre la linealidad e independencia, de los gráficos se puede concluir que:

Residuals vs Fitted (Residuos vs valores ajustados):

  • Función: linealidad y homocedasticidad (varianza constante de los errores). Los residuos se concentran alrededor de cero, pero se nota cierta dispersión mayor en valores altos (cola derecha).

  • Significado: Podría haber heterocedasticidad (la varianza de los errores aumenta con el precio). Esto afecta la eficiencia de los estimadores.

Normal Q-Q

  • Interpretación: los residuos siguen una distribución normal. La mayoría de puntos sigue la línea diagonal, aunque en las colas (extremos) se apartan. Sin embargo, hay cierta no normalidad en los extremos (outliers o valores atípicos).

Scale-Location (√estandarized residuals vs Fitted)

  • Función: homocedasticidad (igual dispersión). Se ve una ligera pendiente ascendente (los residuos aumentan a mayor fitted value), lo cual refuerza la idea de heterocedasticidad.

  • Sugerencia: prueba transformación logarítmica de la variable dependiente.

Residuals vs Leverage (residuos estandarizados vs apalancamiento)

  • Función: observaciones influyentes (puntos que afectan de forma desproporcionada el modelo).

  • Observaciones: Hay algunas observaciones identificadas que podrían ser influyentes y estén afectando mucho los coeficientes del modelo.

  • Sugerencia: revisar estas observaciones y decidir si mantenerlas (casos reales) o tratarlas como atípicos.

Conclusión General

El modelo cumple parcialmente los supuestos, pero se observan:

  1. Heterocedasticidad (varianza no constante).
  2. Cierta no normalidad en los extremos.
  3. Observaciones influyentes que pueden estar afectando resultados.
# 2. Homocedasticidad

# Prueba de Breusch-Pagan
bptest(modelo_a)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo_a
## BP = 511.28, df = 5, p-value < 2.2e-16

Breusch-Pagan test (Heterocedasticidad)

  • Hipótesis nula (H₀): Los residuos tienen varianza constante (homocedasticidad).

  • Hipótesis alternativa (H₁): Los residuos presentan heterocedasticidad.


De acuerdo con los resultados del valor P, el cual dio como resultados menor que 0.05, se rechaza H₀ y por tanto, el modelo presenta heterocedasticidad, que afecta la eficiencia de los estimadores. Adicional, los residuos no son normales, puede estar asociado a outliers o colas pesadas lo cual se visualizó en los gráficos anteriores.

Se recomienda aplicar transformaciones y/o robustez estadística para mejorar la validez de los resultados.

# 3. Normalidad de los residuos

# Histograma y Q-Q plot
hist(residuals(modelo_a), main="Histograma de residuos", xlab="Residuos")

qqnorm(residuals(modelo_a))
qqline(residuals(modelo_a), col="red")

# Prueba de Shapiro-Wilk (para normalidad)
shapiro.test(residuals(modelo_a))  
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(modelo_a)
## W = 0.77432, p-value < 2.2e-16

Test de Shapiro-Wilk

Evalúa si los datos (en este caso, los residuos del modelo) siguen una distribución normal.

  • Hipótesis nula (H0): los residuos siguen una distribución normal.

  • Hipótesis alternativa (H1): los residuos no siguen una distribución normal.

Como el p-value es extremadamente pequeño (< 0.05), se rechaza la hipótesis nula.
Esto significa que los residuos no son normales, el supuesto de normalidad del modelo de regresión no se cumple.

Continuando, el gráfico confirma lo que indicó el test de Shapiro-Wilk: los residuos no siguen una distribución normal. Esto sugiere que el modelo podría estar afectado por valores atípicos o que la variable dependiente no es adecuada sin transformación.

# 4. Multicolinealidad

vif(modelo_a)   # Variance Inflation Factor
##    areaconst      estrato habitaciones parqueaderos       banios 
##     1.965309     1.657391     1.418911     1.746312     2.500250

Valores reportados:

  • areaconst = 1.97

  • estrato = 1.66

  • habitaciones = 1.42

  • parqueaderos = 1.75

  • banios = 2.5

Ahora, teniendo en cuenta el Factor de Inflación de la Varianza (VIF), el cual es un indicador que se usa en regresión lineal para detectar multicolinealidad entre las variables explicativas.

  • VIF ≈ 1 → Sin colinealidad.

  • VIF entre 1 y 5 → Colinealidad baja/moderada (aceptable).

  • VIF > 10 → Colinealidad severa.

Se obtuvieron valores entre 1.4 y 2.5, se tiene Colinealidad baja/moderada (aceptable).

# 5. Autocorrelación de errores

# Test de Durbin-Watson
dwtest(modelo_a)
## 
##  Durbin-Watson test
## 
## data:  modelo_a
## DW = 1.6515, p-value = 2.774e-12
## alternative hypothesis: true autocorrelation is greater than 0

Test de Durbin-Watson

Se utiliza en regresión lineal para detectar autocorrelación de primer orden en los residuos del modelo.

El estadístico toma valores entre 0 y 4:

  • DW ≈ 2 → No hay autocorrelación (lo ideal).

  • DW < 2 → Autocorrelación positiva (los errores tienden a seguir el mismo signo).

  • DW > 2 → Autocorrelación negativa (los errores tienden a alternar signos).

En este caso, DW ≈ 1.6515, lo que sugiere ligera autocorrelación positiva.

  • Hipótesis nula (H₀): No existe autocorrelación entre los residuos.

  • Hipótesis alternativa (H₁): Existe autocorrelación positiva entre los residuos.

Como el p-valor = es menor a 0.01 < 0.05, se rechaza H₀, confirmando autocorrelación positiva de los residuos.

Respuesta 5 - Vivienda 2 (apartamento)

A continuación se va a predecir el precio de la vivienda con las características de la segunda solicitud.

# Crear el dataframe de la vivienda
vivienda2 <- data.frame(
  areaconst    = 300,
  estrato      = c(5, 6),   # dos opciones
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)

# Predecir con tu modelo (ejemplo: modelo_C)
pred_vivienda2 <- predict(
  modelo_a, 
  newdata = vivienda2, 
  interval = "prediction", 
  level = 0.95
)

# Combinar resultados
resultado2 <- cbind(vivienda2, pred_vivienda2)
print(resultado2)
##   areaconst estrato habitaciones parqueaderos banios      fit      lwr      upr
## 1       300       5            5            3      3 688.1998 494.0775 882.3221
## 2       300       6            5            3      3 746.3139 552.1908 940.4371

fit → es el valor predicho del precio (en millones).

  • Para estrato 5 ≈ 688.2 millones

  • Para estrato 6 ≈ 746.3 millones

lwr y upr → intervalo de predicción al 95%.

  • Estrato 5: el precio podría estar entre 494.1 y 882.3 millones.

  • Estrato 6: el precio podría estar entre 552.2 y 940.4 millones.

Comparación con el crédito (850 millones):

  • Estrato 5: el valor estimado (688.2) está por debajo del crédito aprobado, sí alcanzaría.

  • Estrato 6: el valor estimado (746.3) stá por debajo del crédito aprobado, sí alcanzaría, pero como el intervalo cubre valores menores a 850, existe riesgo de que no alcance dependiendo de la vivienda que se seleccione.

Respuesta 6 - Vivienda 2 (apartamento)

Ahora, en base a los requerimiento del cliente 2 se busca en la base de datos limpia que contenia todos los datos, los apartamentos que cumplan con los requisitos.

Para ello se tiene en cuenta el número de habitaciones, área construida, parqueadero, baños, estrato, de estas variables se prioriza que el estrato sea preferiblemente estrato 6, que el área de la vivienda se encuentre entre los rango 280 - 600 metros y el el credito pre-aprobado. Para este caso se tuvo que aumentar de manera considerable el área dada la limitada opcon de apartamentos que cumplieran con los requisitos solicitados.

data_caso2 = base4_a

base_pred2 <- data_caso2 %>%
  mutate(pred = preciom)

# Ranking de priorización

rango_inferior_area <- 280
rango_superior_area <- 600
credito_max <- 850

candidatas2 <- base_pred2 %>%
  filter(
    areaconst >= rango_inferior_area,
    areaconst <= rango_superior_area,
    parqueaderos >= 3,
    banios >= 3,
    habitaciones >= 5,
    estrato %in% c(5,6),
    pred <= credito_max
  )

candidatas_rank2 <- candidatas2 %>%
  mutate(
    gap_area   = abs(areaconst - 300),
    ahorro     = pmax(0, credito_max - pred),
    pref_estr6 = ifelse(estrato == 6, 1, 0),
    score      = rescale(-gap_area) + rescale(ahorro) + 1 * pref_estr6
  ) %>%
  arrange(desc(score))

# Selección de las 5 mejores

top5_apart <- candidatas_rank2 %>% slice_head(n = 5)

top5_apart
## # A tibble: 2 × 14
##   preciom areaconst estrato banios habitaciones zona     parqueaderos longitud
##     <dbl>     <dbl>   <dbl>  <dbl>        <dbl> <chr>           <dbl>    <dbl>
## 1     670       300       5      5            6 Zona Sur            3    -76.6
## 2     730       573       5      8            5 Zona Sur            3    -76.5
## # ℹ 6 more variables: latitud <dbl>, pred <dbl>, gap_area <dbl>, ahorro <dbl>,
## #   pref_estr6 <dbl>, score <dbl>

De acuerdo con las características deseadas por el cliente, no se logra encontrar como mínimo 5 viviendas que cumplan con las especificaciones, solo se lograron filtrar dos y solo una de ellas cumple con lo mas cercano a los requerimientos dle cliente:

  1. Se priorizó que el estrato fuera 6, sin embargo, ningun apartamento cumplía con las especificaciones, por tanto las opciones se limitaron al estrato 5.
  2. Solo uno de los dos apartamentos tienen los 300 metros, el segundo que puede ser una opción se pasa de lo solicitado por 273 metros, ya es decisión del cliente si lo quiere ver.
  3. Presentan un precio menor al presupuesto, es decir, el cliente tendria un ahorro en ambas.
  4. Ambos apartamentos tienen más 3 baños.
  5. La primera vivienda cuenta con más de 5 habitaciones y la segunda cumple con lo mínimo, es decir, 5 habitaciones.

A continuación se muestra la ubicación geográfica de los dos apartamentos.

# Ubicación geográfica de las viviendas top 5

leaflet(top5_apart) %>%
  addTiles() %>%
  addMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste0(
      "Área: ", areaconst, " m²<br>",
      "Estrato: ", estrato, "<br>",
      "Habitaciones: ", habitaciones, "<br>",
      "Baños: ", banios, "<br>",
      "Parqueaderos: ", parqueaderos, "<br>",
      "Precio estimado: ", round(pred, 1), " millones"
    )
  )

Dadas las circusntancias para este segundo caso, se hablaría con el cliente para modificar las especificaciones de búsqueda de su apartamento, lo principal sería aumentra el presupuesto, pues si quiere un apartamento sin modificar sus requerimientos en el numero de habitaciones, parqueadero, baños, área construída y el estrato la limitante es su presupuesto, pues existen opciones que cumplen pero estan por encima de su valoración monetaria.

La segunda opción que le brindaria que se ajuste a su presupuesto es, dismimuir ya sea el número de parqueadero o habitaciones requeridas.