El presente informe desarrolla el caso C&A (Casas y Apartamentos), cuyo propósito es apoyar a la empresa inmobiliaria en la evaluación de dos solicitudes de vivienda mediante técnicas de análisis exploratorio, modelación estadística y regresión lineal múltiple.
La primera solicitud corresponde a una casa ubicada en la Zona Norte de Cali, con un área construida de 200 m², un parqueadero, dos baños, cuatro habitaciones, estrato 4 o 5 y un crédito máximo preaprobado de 350 millones de pesos.
La segunda solicitud corresponde a un apartamento en la Zona Sur de Cali, con un área construida de 300 m², tres parqueaderos, tres baños, cinco habitaciones, estrato 5 o 6 y un crédito máximo preaprobado de 850 millones de pesos.
El análisis incluye filtrado de datos, análisis descriptivo y de correlación, estimación y comparación de modelos de regresión, validación de supuestos, predicción de precios y selección de ofertas potenciales.
paquetes <- c(
"dplyr",
"tidyr",
"stringr",
"ggplot2",
"plotly",
"leaflet",
"broom",
"lmtest",
"car",
"nortest",
"knitr",
"DT"
)
faltantes <- paquetes[!paquetes %in% rownames(installed.packages())]
if(length(faltantes) > 0){
install.packages(faltantes)
}
invisible(lapply(paquetes, library, character.only = TRUE))
if(!requireNamespace("paqueteMODELOS", quietly = TRUE)){
if(!requireNamespace("remotes", quietly = TRUE)){
install.packages("remotes")
}
remotes::install_github(
"centromagis/paqueteMODELOS",
upgrade = "never"
)
}
library(paqueteMODELOS)
# Carga explícita desde utils::data para evitar cualquier función data() enmascarada
utils::data("vivienda", package = "paqueteMODELOS", envir = environment())
if(!exists("vivienda")){
stop("No fue posible cargar el conjunto de datos 'vivienda' desde paqueteMODELOS.")
}
datos <- vivienda
# Validación del esquema requerido por la actividad
columnas_requeridas <- c(
"id", "zona", "piso", "estrato", "preciom", "areaconst",
"parqueaderos", "banios", "habitaciones", "tipo", "barrio",
"longitud", "latitud"
)
columnas_faltantes <- setdiff(columnas_requeridas, names(datos))
if(length(columnas_faltantes) > 0){
stop(
paste0(
"La base no contiene las siguientes variables requeridas: ",
paste(columnas_faltantes, collapse = ", ")
)
)
}
# Conversión defensiva de variables numéricas
variables_numericas <- c(
"estrato", "preciom", "areaconst", "parqueaderos",
"banios", "habitaciones", "longitud", "latitud"
)
datos[variables_numericas] <- lapply(
datos[variables_numericas],
function(x) suppressWarnings(as.numeric(as.character(x)))
)
if(nrow(datos) == 0){
stop("La base 'vivienda' se cargó sin observaciones.")
}
datos <- datos %>%
dplyr::mutate(
zona = stringr::str_squish(as.character(zona)),
tipo = stringr::str_squish(as.character(tipo)),
barrio = stringr::str_squish(as.character(barrio))
)
dim(datos)
## [1] 8322 13
str(datos)
## 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>
head(datos)
## # 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>
La base contiene información relacionada con la localización, características físicas y precio de las viviendas.
faltantes_datos <- data.frame(
Variable = names(datos),
Valores_perdidos = sapply(datos, function(x) sum(is.na(x)))
)
knitr::kable(
faltantes_datos,
caption = "Valores perdidos por variable"
)
| Variable | Valores_perdidos | |
|---|---|---|
| id | id | 3 |
| zona | zona | 3 |
| piso | piso | 2638 |
| estrato | estrato | 3 |
| preciom | preciom | 2 |
| areaconst | areaconst | 3 |
| parqueaderos | parqueaderos | 1605 |
| banios | banios | 3 |
| habitaciones | habitaciones | 3 |
| tipo | tipo | 3 |
| barrio | barrio | 3 |
| longitud | longitud | 3 |
| latitud | latitud | 3 |
# ============================================================
# FUNCIONES GRÁFICAS ROBUSTAS
# ============================================================
validar_plotly <- function(grafico_plotly, grafico_respaldo, contexto){
tryCatch(
{
# Forzar la construcción interna permite detectar incompatibilidades
# antes de que el documento llegue a la etapa de renderizado.
invisible(plotly::plotly_build(grafico_plotly))
grafico_plotly
},
error = function(e){
warning(
paste0(
"No fue posible construir ", contexto,
" con plotly. Se mostrará una versión estática. Detalle: ",
conditionMessage(e)
),
call. = FALSE
)
grafico_respaldo
}
)
}
grafico_precio <- function(base, variable, nombre_x){
if(!variable %in% names(base)){
stop(paste("La variable", variable, "no existe en la base suministrada."))
}
datos_graf <- data.frame(
x_plot = suppressWarnings(as.numeric(as.character(base[[variable]]))),
preciom = suppressWarnings(as.numeric(as.character(base$preciom)))
)
datos_graf <- datos_graf[
is.finite(datos_graf$x_plot) & is.finite(datos_graf$preciom),
,
drop = FALSE
]
if(nrow(datos_graf) == 0){
stop(paste("No hay observaciones válidas para graficar", nombre_x))
}
datos_graf$tooltip <- paste0(
"Precio: ", round(datos_graf$preciom, 2), " millones",
"<br>", nombre_x, ": ", datos_graf$x_plot
)
# Una sola traza: evita incompatibilidades por longitudes diferentes
g_interactivo <- plotly::plot_ly(
data = datos_graf,
x = ~x_plot,
y = ~preciom,
type = "scatter",
mode = "markers",
text = ~tooltip,
hoverinfo = "text"
) %>%
plotly::layout(
xaxis = list(title = nombre_x),
yaxis = list(title = "Precio (millones de pesos)")
)
g_estatico <- ggplot2::ggplot(
datos_graf,
ggplot2::aes(x = x_plot, y = preciom)
) +
ggplot2::geom_point(alpha = 0.5) +
ggplot2::theme_minimal() +
ggplot2::labs(
x = nombre_x,
y = "Precio (millones de pesos)"
)
validar_plotly(
g_interactivo,
g_estatico,
paste("el gráfico de", nombre_x)
)
}
grafico_correlacion <- function(matriz_cor, titulo){
if(!is.matrix(matriz_cor)){
matriz_cor <- as.matrix(matriz_cor)
}
g_interactivo <- plotly::plot_ly(
x = colnames(matriz_cor),
y = rownames(matriz_cor),
z = matriz_cor,
type = "heatmap",
zmin = -1,
zmax = 1
) %>%
plotly::layout(title = titulo)
tabla_cor <- as.data.frame(as.table(matriz_cor))
names(tabla_cor) <- c("Variable_X", "Variable_Y", "Correlacion")
g_estatico <- ggplot2::ggplot(
tabla_cor,
ggplot2::aes(
x = Variable_X,
y = Variable_Y,
fill = Correlacion
)
) +
ggplot2::geom_tile() +
ggplot2::geom_text(
ggplot2::aes(label = round(Correlacion, 2)),
size = 3
) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = titulo,
x = NULL,
y = NULL
)
validar_plotly(
g_interactivo,
g_estatico,
"la matriz de correlaciones"
)
}
seleccionar_ofertas <- function(
base,
modelo,
presupuesto,
area_obj,
estratos_obj,
habitaciones_obj,
parqueaderos_obj,
banios_obj,
n = 5){
candidatas <- base %>%
dplyr::filter(
preciom <= presupuesto,
estrato %in% estratos_obj
) %>%
tidyr::drop_na(
preciom,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios,
latitud,
longitud
)
if(nrow(candidatas) == 0){
candidatas$precio_estimado <- numeric(0)
candidatas$diferencia_modelo <- numeric(0)
candidatas$cumple_estricto <- logical(0)
candidatas$puntuacion <- numeric(0)
return(candidatas)
}
candidatas$precio_estimado <- as.numeric(
predict(modelo, newdata = candidatas)
)
candidatas <- candidatas %>%
dplyr::mutate(
cumple_estricto =
areaconst >= area_obj &
habitaciones >= habitaciones_obj &
parqueaderos >= parqueaderos_obj &
banios >= banios_obj,
diferencia_modelo = precio_estimado - preciom,
puntuacion =
abs(areaconst - area_obj) / area_obj +
abs(habitaciones - habitaciones_obj) / habitaciones_obj +
abs(parqueaderos - parqueaderos_obj) / parqueaderos_obj +
abs(banios - banios_obj) / banios_obj +
0.5 * abs(estrato - mean(estratos_obj))
) %>%
dplyr::arrange(
dplyr::desc(cumple_estricto),
puntuacion,
dplyr::desc(diferencia_modelo)
) %>%
dplyr::slice_head(n = n)
candidatas
}
base1 <- datos %>%
dplyr::filter(
tipo == "Casa",
zona == "Zona Norte"
)
nrow(base1)
## [1] 722
knitr::kable(
head(base1, 3),
caption = "Primeros tres registros de base1"
)
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1209 | Zona Norte | 02 | 5 | 320 | 150 | 2 | 4 | 6 | Casa | acopi | -76.51341 | 3.47968 |
| 1592 | Zona Norte | 02 | 5 | 780 | 380 | 2 | 3 | 3 | Casa | acopi | -76.51674 | 3.48721 |
| 4057 | Zona Norte | 02 | 6 | 750 | 445 | NA | 7 | 6 | Casa | acopi | -76.52950 | 3.38527 |
tabla_tipo1 <- as.data.frame(table(base1$tipo))
tabla_zona1 <- as.data.frame(table(base1$zona))
tabla_estrato1 <- as.data.frame(table(base1$estrato))
knitr::kable(tabla_tipo1, col.names = c("Tipo", "Frecuencia"))
| Tipo | Frecuencia |
|---|---|
| Casa | 722 |
knitr::kable(tabla_zona1, col.names = c("Zona", "Frecuencia"))
| Zona | Frecuencia |
|---|---|
| Zona Norte | 722 |
knitr::kable(tabla_estrato1, col.names = c("Estrato", "Frecuencia"))
| Estrato | Frecuencia |
|---|---|
| 3 | 235 |
| 4 | 161 |
| 5 | 271 |
| 6 | 55 |
base1_mapa <- base1 %>%
dplyr::filter(
!is.na(latitud),
!is.na(longitud),
is.finite(latitud),
is.finite(longitud)
)
leaflet::leaflet(base1_mapa) %>%
leaflet::addTiles() %>%
leaflet::addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 4,
fillOpacity = 0.6,
popup = ~paste0(
"<b>ID:</b> ", id,
"<br><b>Barrio:</b> ", barrio,
"<br><b>Precio:</b> $", preciom, " millones",
"<br><b>Área:</b> ", areaconst, " m²",
"<br><b>Estrato:</b> ", estrato
)
)
El filtro garantiza que todos los registros seleccionados estén clasificados como casas de Zona Norte. Sin embargo, si en el mapa se observan puntos alejados geográficamente, esto puede obedecer a inconsistencias en las coordenadas, errores de digitación, diferencias entre clasificación comercial y límites geográficos, o problemas de georreferenciación.
base1_modelo <- base1 %>%
tidyr::drop_na(
preciom,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios
)
variables1 <- base1_modelo %>%
dplyr::select(
preciom,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios
)
cor1 <- cor(
variables1,
use = "complete.obs",
method = "pearson"
)
round(cor1, 3)
## preciom areaconst estrato habitaciones parqueaderos banios
## preciom 1.000 0.685 0.528 0.365 0.412 0.509
## areaconst 0.685 1.000 0.354 0.421 0.307 0.457
## estrato 0.528 0.354 1.000 0.058 0.261 0.351
## habitaciones 0.365 0.421 0.058 1.000 0.241 0.590
## parqueaderos 0.412 0.307 0.261 0.241 1.000 0.392
## banios 0.509 0.457 0.351 0.590 0.392 1.000
grafico_correlacion(
cor1,
"Matriz de correlaciones - Casas Zona Norte"
)
grafico_precio(base1_modelo, "areaconst", "Área construida (m²)")
grafico_precio(base1_modelo, "estrato", "Estrato")
grafico_precio(base1_modelo, "habitaciones", "Habitaciones")
grafico_precio(base1_modelo, "parqueaderos", "Parqueaderos")
grafico_precio(base1_modelo, "banios", "Baños")
Se debe analizar el sentido y la intensidad de la asociación entre el precio y las características de la vivienda. Una correlación positiva entre área construida y precio indica que las casas más grandes tienden a presentar valores superiores. Asimismo, estrato, baños y parqueaderos pueden mostrar asociaciones positivas con el precio debido a que representan características relacionadas con mayor tamaño y nivel socioeconómico.
modelo1 <- lm(
preciom ~ areaconst +
estrato +
habitaciones +
parqueaderos +
banios,
data = base1_modelo
)
summary(modelo1)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base1_modelo)
##
## Residuals:
## Min 1Q Median 3Q Max
## -784.29 -77.56 -16.03 47.67 978.61
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -238.17090 44.40551 -5.364 1.34e-07 ***
## areaconst 0.67673 0.05281 12.814 < 2e-16 ***
## estrato 80.63495 9.82632 8.206 2.70e-15 ***
## habitaciones 7.64511 5.65873 1.351 0.177
## parqueaderos 24.00598 5.86889 4.090 5.14e-05 ***
## banios 18.89938 7.48800 2.524 0.012 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 155.1 on 429 degrees of freedom
## Multiple R-squared: 0.6041, Adjusted R-squared: 0.5995
## F-statistic: 130.9 on 5 and 429 DF, p-value: < 2.2e-16
coeficientes1 <- broom::tidy(
modelo1,
conf.int = TRUE
) %>%
dplyr::mutate(
Significativo = ifelse(p.value < 0.05, "Sí", "No")
)
knitr::kable(
coeficientes1,
digits = 4,
caption = "Coeficientes del modelo completo - Vivienda 1"
)
| term | estimate | std.error | statistic | p.value | conf.low | conf.high | Significativo |
|---|---|---|---|---|---|---|---|
| (Intercept) | -238.1709 | 44.4055 | -5.3635 | 0.0000 | -325.4503 | -150.8915 | Sí |
| areaconst | 0.6767 | 0.0528 | 12.8140 | 0.0000 | 0.5729 | 0.7805 | Sí |
| estrato | 80.6349 | 9.8263 | 8.2060 | 0.0000 | 61.3212 | 99.9487 | Sí |
| habitaciones | 7.6451 | 5.6587 | 1.3510 | 0.1774 | -3.4772 | 18.7674 | No |
| parqueaderos | 24.0060 | 5.8689 | 4.0904 | 0.0001 | 12.4706 | 35.5413 | Sí |
| banios | 18.8994 | 7.4880 | 2.5240 | 0.0120 | 4.1816 | 33.6171 | Sí |
ajuste1 <- broom::glance(modelo1)
knitr::kable(
ajuste1 %>%
dplyr::select(
r.squared,
adj.r.squared,
sigma,
statistic,
p.value,
AIC,
BIC
),
digits = 4,
caption = "Indicadores de ajuste - Vivienda 1"
)
| r.squared | adj.r.squared | sigma | statistic | p.value | AIC | BIC |
|---|---|---|---|---|---|---|
| 0.6041 | 0.5995 | 155.1149 | 130.919 | 0 | 5630.86 | 5659.387 |
Cada coeficiente representa el cambio promedio esperado en el precio, medido en millones de pesos, cuando la variable correspondiente aumenta en una unidad y las demás variables permanecen constantes.
Los coeficientes con valores p < 0.05 se consideran
estadísticamente significativos al nivel del 5 %.
El coeficiente \(R^2\) indica la proporción de la variabilidad observada en el precio que es explicada conjuntamente por las variables incluidas en el modelo.
modelo1_reducido <- step(
modelo1,
direction = "backward",
trace = 0
)
comparacion1 <- dplyr::bind_rows(
broom::glance(modelo1) %>% dplyr::mutate(Modelo = "Modelo completo"),
broom::glance(modelo1_reducido) %>% dplyr::mutate(Modelo = "Modelo reducido")
) %>%
dplyr::select(
Modelo,
r.squared,
adj.r.squared,
sigma,
AIC,
BIC
)
knitr::kable(
comparacion1,
digits = 3,
caption = "Comparación de modelos - Vivienda 1"
)
| Modelo | r.squared | adj.r.squared | sigma | AIC | BIC |
|---|---|---|---|---|---|
| Modelo completo | 0.604 | 0.599 | 155.115 | 5630.860 | 5659.387 |
| Modelo reducido | 0.602 | 0.599 | 155.264 | 5630.706 | 5655.159 |
modelo1_final <- if(AIC(modelo1_reducido) < AIC(modelo1)){
modelo1_reducido
} else {
modelo1
}
formula(modelo1_final)
## preciom ~ areaconst + estrato + parqueaderos + banios
En la validación de supuestos se utilizan gráficos estadísticos de
ggplot2 en lugar de convertirlos a plotly.
Esto aumenta la estabilidad del documento y evita incompatibilidades
entre versiones de plotly y ggplot2.
# Asegurar que exista el modelo final
if(!exists("modelo1_final")){
modelo1 <- lm(
preciom ~ areaconst +
estrato +
habitaciones +
parqueaderos +
banios,
data = base1_modelo
)
modelo1_reducido <- step(
modelo1,
direction = "backward",
trace = 0
)
modelo1_final <- if(AIC(modelo1_reducido) < AIC(modelo1)){
modelo1_reducido
} else {
modelo1
}
}
diagnostico1 <- data.frame(
Ajustados = fitted(modelo1_final),
Residuos = residuals(modelo1_final),
Residuos_est = rstandard(modelo1_final)
)
# Residuos vs valores ajustados
g_res1 <- ggplot2::ggplot(
diagnostico1,
ggplot2::aes(x = Ajustados, y = Residuos)
) +
ggplot2::geom_point(alpha = 0.45) +
ggplot2::geom_hline(yintercept = 0, linetype = 2) +
ggplot2::geom_smooth(method = "loess", se = FALSE) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "Residuos vs valores ajustados - Vivienda 1",
x = "Valores ajustados",
y = "Residuos"
)
print(g_res1)
# Gráfico Q-Q
g_qq1 <- ggplot2::ggplot(
diagnostico1,
ggplot2::aes(sample = Residuos_est)
) +
ggplot2::stat_qq() +
ggplot2::stat_qq_line() +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "Gráfico Q-Q de residuos - Vivienda 1",
x = "Cuantiles teóricos",
y = "Residuos estandarizados"
)
print(g_qq1)
# Pruebas formales
cat("
Prueba de normalidad Anderson-Darling:
")
##
## Prueba de normalidad Anderson-Darling:
print(nortest::ad.test(residuals(modelo1_final)))
##
## Anderson-Darling normality test
##
## data: residuals(modelo1_final)
## A = 13.321, p-value < 2.2e-16
cat("
Prueba de homocedasticidad Breusch-Pagan:
")
##
## Prueba de homocedasticidad Breusch-Pagan:
print(lmtest::bptest(modelo1_final))
##
## studentized Breusch-Pagan test
##
## data: modelo1_final
## BP = 80.764, df = 4, p-value < 2.2e-16
cat("
Prueba de independencia Durbin-Watson:
")
##
## Prueba de independencia Durbin-Watson:
print(lmtest::dwtest(modelo1_final))
##
## Durbin-Watson test
##
## data: modelo1_final
## DW = 1.7702, p-value = 0.00716
## alternative hypothesis: true autocorrelation is greater than 0
cat("
Factores de inflación de varianza (VIF):
")
##
## Factores de inflación de varianza (VIF):
if(length(attr(terms(modelo1_final), "term.labels")) > 1){
print(car::vif(modelo1_final))
} else {
cat("El modelo final tiene un solo predictor; VIF no aplica.
")
}
## areaconst estrato parqueaderos banios
## 1.358491 1.220649 1.226227 1.437956
# Observaciones influyentes
cooks1 <- cooks.distance(modelo1_final)
limite_cook1 <- 4 / nobs(modelo1_final)
cat(
"
Observaciones potencialmente influyentes según Cook:",
sum(cooks1 > limite_cook1),
"
"
)
##
## Observaciones potencialmente influyentes según Cook: 24
Si se incumplen algunos supuestos, se recomienda evaluar transformación del precio, errores estándar robustos, regresión robusta o especificaciones con variables adicionales.
# Verificación del rango de área para detectar posible extrapolación
rango_area1 <- range(base1_modelo$areaconst, na.rm = TRUE)
cat(
"Rango observado de área en base1:",
round(rango_area1[1], 2), "a", round(rango_area1[2], 2), "m²\n"
)
## Rango observado de área en base1: 30 a 1440 m²
solicitud1 <- data.frame(
areaconst = c(200, 200),
estrato = c(4, 5),
habitaciones = c(4, 4),
parqueaderos = c(1, 1),
banios = c(2, 2)
)
prediccion1 <- tryCatch(
predict(
modelo1_final,
newdata = solicitud1,
interval = "prediction",
level = 0.95
),
error = function(e){
stop(
paste0(
"No fue posible realizar la predicción de la Vivienda 1: ",
conditionMessage(e)
)
)
}
)
resultado_pred1 <- cbind(
solicitud1,
prediccion1
)
knitr::kable(
resultado_pred1,
digits = 2,
caption = "Predicciones de precio - Vivienda 1"
)
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 200 | 4 | 4 | 1 | 2 | 308.66 | 2.51 | 614.80 |
| 200 | 5 | 4 | 1 | 2 | 385.87 | 79.20 | 692.53 |
ofertas1 <- seleccionar_ofertas(
base = base1,
modelo = modelo1_final,
presupuesto = 350,
area_obj = 200,
estratos_obj = c(4, 5),
habitaciones_obj = 4,
parqueaderos_obj = 1,
banios_obj = 2,
n = 5
)
knitr::kable(
ofertas1 %>%
dplyr::select(
id,
barrio,
preciom,
precio_estimado,
diferencia_modelo,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios,
cumple_estricto
),
digits = 2,
caption = "Cinco ofertas potenciales - Vivienda 1"
)
| id | barrio | preciom | precio_estimado | diferencia_modelo | areaconst | estrato | habitaciones | parqueaderos | banios | cumple_estricto |
|---|---|---|---|---|---|---|---|---|---|---|
| 1943 | vipasa | 350 | 487.43 | 137.43 | 346 | 5 | 4 | 1 | 2 | TRUE |
| 1108 | la merced | 330 | 374.54 | 44.54 | 260 | 4 | 4 | 1 | 3 | TRUE |
| 1163 | la merced | 350 | 421.08 | 71.08 | 216 | 5 | 4 | 2 | 2 | TRUE |
| 4267 | el bosque | 335 | 435.55 | 100.55 | 202 | 5 | 5 | 1 | 4 | TRUE |
| 1270 | el bosque | 350 | 412.03 | 62.03 | 203 | 5 | 5 | 2 | 2 | TRUE |
if(nrow(ofertas1) > 0){
leaflet::leaflet(ofertas1) %>%
leaflet::addTiles() %>%
leaflet::addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 7,
fillOpacity = 0.8,
popup = ~paste0(
"<b>ID:</b> ", id,
"<br><b>Barrio:</b> ", barrio,
"<br><b>Precio:</b> $", preciom, " millones",
"<br><b>Predicción:</b> $", round(precio_estimado, 1), " millones",
"<br><b>Área:</b> ", areaconst, " m²",
"<br><b>Estrato:</b> ", estrato,
"<br><b>Habitaciones:</b> ", habitaciones,
"<br><b>Baños:</b> ", banios,
"<br><b>Parqueaderos:</b> ", parqueaderos,
"<br><b>Cumple estrictamente:</b> ", cumple_estricto
)
)
}
base2 <- datos %>%
dplyr::filter(
tipo == "Apartamento",
zona == "Zona Sur"
)
nrow(base2)
## [1] 2787
knitr::kable(
head(base2, 3),
caption = "Primeros tres registros de base2"
)
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5098 | Zona Sur | 05 | 4 | 290 | 96 | 1 | 2 | 3 | Apartamento | acopi | -76.53464 | 3.44987 |
| 698 | Zona Sur | 02 | 3 | 78 | 40 | 1 | 1 | 2 | Apartamento | aguablanca | -76.50100 | 3.40000 |
| 8199 | Zona Sur | NA | 6 | 875 | 194 | 2 | 5 | 3 | Apartamento | aguacatal | -76.55700 | 3.45900 |
tabla_tipo2 <- as.data.frame(table(base2$tipo))
tabla_zona2 <- as.data.frame(table(base2$zona))
tabla_estrato2 <- as.data.frame(table(base2$estrato))
knitr::kable(tabla_tipo2, col.names = c("Tipo", "Frecuencia"))
| Tipo | Frecuencia |
|---|---|
| Apartamento | 2787 |
knitr::kable(tabla_zona2, col.names = c("Zona", "Frecuencia"))
| Zona | Frecuencia |
|---|---|
| Zona Sur | 2787 |
knitr::kable(tabla_estrato2, col.names = c("Estrato", "Frecuencia"))
| Estrato | Frecuencia |
|---|---|
| 3 | 201 |
| 4 | 1091 |
| 5 | 1033 |
| 6 | 462 |
base2_mapa <- base2 %>%
dplyr::filter(
!is.na(latitud),
!is.na(longitud),
is.finite(latitud),
is.finite(longitud)
)
leaflet::leaflet(base2_mapa) %>%
leaflet::addTiles() %>%
leaflet::addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 4,
fillOpacity = 0.5,
popup = ~paste0(
"<b>ID:</b> ", id,
"<br><b>Barrio:</b> ", barrio,
"<br><b>Precio:</b> $", preciom, " millones",
"<br><b>Área:</b> ", areaconst, " m²",
"<br><b>Estrato:</b> ", estrato
)
)
base2_modelo <- base2 %>%
tidyr::drop_na(
preciom,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios
)
variables2 <- base2_modelo %>%
dplyr::select(
preciom,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios
)
cor2 <- cor(
variables2,
use = "complete.obs",
method = "pearson"
)
round(cor2, 3)
## preciom areaconst estrato habitaciones parqueaderos banios
## preciom 1.000 0.741 0.650 0.296 0.693 0.711
## areaconst 0.741 1.000 0.452 0.407 0.578 0.664
## estrato 0.650 0.452 1.000 0.177 0.486 0.535
## habitaciones 0.296 0.407 0.177 1.000 0.237 0.520
## parqueaderos 0.693 0.578 0.486 0.237 1.000 0.556
## banios 0.711 0.664 0.535 0.520 0.556 1.000
grafico_correlacion(
cor2,
"Matriz de correlaciones - Apartamentos Zona Sur"
)
grafico_precio(base2_modelo, "areaconst", "Área construida (m²)")
grafico_precio(base2_modelo, "estrato", "Estrato")
grafico_precio(base2_modelo, "habitaciones", "Habitaciones")
grafico_precio(base2_modelo, "parqueaderos", "Parqueaderos")
grafico_precio(base2_modelo, "banios", "Baños")
modelo2 <- lm(
preciom ~ areaconst +
estrato +
habitaciones +
parqueaderos +
banios,
data = base2_modelo
)
summary(modelo2)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base2_modelo)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1092.02 -42.28 -1.33 40.58 926.56
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -261.62501 15.63220 -16.736 < 2e-16 ***
## areaconst 1.28505 0.05403 23.785 < 2e-16 ***
## estrato 60.89709 3.08408 19.746 < 2e-16 ***
## habitaciones -24.83693 3.89229 -6.381 2.11e-10 ***
## parqueaderos 72.91468 3.95797 18.422 < 2e-16 ***
## banios 50.69675 3.39637 14.927 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 98.02 on 2375 degrees of freedom
## Multiple R-squared: 0.7485, Adjusted R-squared: 0.748
## F-statistic: 1414 on 5 and 2375 DF, p-value: < 2.2e-16
coeficientes2 <- broom::tidy(
modelo2,
conf.int = TRUE
) %>%
dplyr::mutate(
Significativo = ifelse(p.value < 0.05, "Sí", "No")
)
knitr::kable(
coeficientes2,
digits = 4,
caption = "Coeficientes del modelo - Vivienda 2"
)
| term | estimate | std.error | statistic | p.value | conf.low | conf.high | Significativo |
|---|---|---|---|---|---|---|---|
| (Intercept) | -261.6250 | 15.6322 | -16.7363 | 0 | -292.2792 | -230.9708 | Sí |
| areaconst | 1.2850 | 0.0540 | 23.7853 | 0 | 1.1791 | 1.3910 | Sí |
| estrato | 60.8971 | 3.0841 | 19.7457 | 0 | 54.8493 | 66.9448 | Sí |
| habitaciones | -24.8369 | 3.8923 | -6.3811 | 0 | -32.4696 | -17.2043 | Sí |
| parqueaderos | 72.9147 | 3.9580 | 18.4223 | 0 | 65.1533 | 80.6761 | Sí |
| banios | 50.6967 | 3.3964 | 14.9267 | 0 | 44.0366 | 57.3569 | Sí |
modelo2_reducido <- step(
modelo2,
direction = "backward",
trace = 0
)
comparacion2 <- dplyr::bind_rows(
broom::glance(modelo2) %>% dplyr::mutate(Modelo = "Modelo completo"),
broom::glance(modelo2_reducido) %>% dplyr::mutate(Modelo = "Modelo reducido")
) %>%
dplyr::select(
Modelo,
r.squared,
adj.r.squared,
sigma,
AIC,
BIC
)
knitr::kable(
comparacion2,
digits = 3,
caption = "Comparación de modelos - Vivienda 2"
)
| Modelo | r.squared | adj.r.squared | sigma | AIC | BIC |
|---|---|---|---|---|---|
| Modelo completo | 0.749 | 0.748 | 98.019 | 28599.54 | 28639.96 |
| Modelo reducido | 0.749 | 0.748 | 98.019 | 28599.54 | 28639.96 |
modelo2_final <- if(AIC(modelo2_reducido) < AIC(modelo2)){
modelo2_reducido
} else {
modelo2
}
formula(modelo2_final)
## preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios
En la validación de supuestos se utilizan gráficos estadísticos de
ggplot2 en lugar de convertirlos a plotly.
Esto aumenta la estabilidad del documento y evita incompatibilidades
entre versiones de plotly y ggplot2.
# Asegurar que exista el modelo final
if(!exists("modelo2_final")){
modelo2 <- lm(
preciom ~ areaconst +
estrato +
habitaciones +
parqueaderos +
banios,
data = base2_modelo
)
modelo2_reducido <- step(
modelo2,
direction = "backward",
trace = 0
)
modelo2_final <- if(AIC(modelo2_reducido) < AIC(modelo2)){
modelo2_reducido
} else {
modelo2
}
}
diagnostico2 <- data.frame(
Ajustados = fitted(modelo2_final),
Residuos = residuals(modelo2_final),
Residuos_est = rstandard(modelo2_final)
)
# Residuos vs valores ajustados
g_res2 <- ggplot2::ggplot(
diagnostico2,
ggplot2::aes(x = Ajustados, y = Residuos)
) +
ggplot2::geom_point(alpha = 0.45) +
ggplot2::geom_hline(yintercept = 0, linetype = 2) +
ggplot2::geom_smooth(method = "loess", se = FALSE) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "Residuos vs valores ajustados - Vivienda 2",
x = "Valores ajustados",
y = "Residuos"
)
print(g_res2)
# Gráfico Q-Q
g_qq2 <- ggplot2::ggplot(
diagnostico2,
ggplot2::aes(sample = Residuos_est)
) +
ggplot2::stat_qq() +
ggplot2::stat_qq_line() +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "Gráfico Q-Q de residuos - Vivienda 2",
x = "Cuantiles teóricos",
y = "Residuos estandarizados"
)
print(g_qq2)
# Pruebas formales
cat("
Prueba de normalidad Anderson-Darling:
")
##
## Prueba de normalidad Anderson-Darling:
print(nortest::ad.test(residuals(modelo2_final)))
##
## Anderson-Darling normality test
##
## data: residuals(modelo2_final)
## A = 72.413, p-value < 2.2e-16
cat("
Prueba de homocedasticidad Breusch-Pagan:
")
##
## Prueba de homocedasticidad Breusch-Pagan:
print(lmtest::bptest(modelo2_final))
##
## studentized Breusch-Pagan test
##
## data: modelo2_final
## BP = 754.81, df = 5, p-value < 2.2e-16
cat("
Prueba de independencia Durbin-Watson:
")
##
## Prueba de independencia Durbin-Watson:
print(lmtest::dwtest(modelo2_final))
##
## Durbin-Watson test
##
## data: modelo2_final
## DW = 1.5333, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
cat("
Factores de inflación de varianza (VIF):
")
##
## Factores de inflación de varianza (VIF):
if(length(attr(terms(modelo2_final), "term.labels")) > 1){
print(car::vif(modelo2_final))
} else {
cat("El modelo final tiene un solo predictor; VIF no aplica.
")
}
## areaconst estrato habitaciones parqueaderos banios
## 2.066518 1.545162 1.429280 1.737878 2.529494
# Observaciones influyentes
cooks2 <- cooks.distance(modelo2_final)
limite_cook2 <- 4 / nobs(modelo2_final)
cat(
"
Observaciones potencialmente influyentes según Cook:",
sum(cooks2 > limite_cook2),
"
"
)
##
## Observaciones potencialmente influyentes según Cook: 118
Si se incumplen algunos supuestos, se recomienda evaluar transformación del precio, errores estándar robustos, regresión robusta o especificaciones con variables adicionales.
# Verificación del rango de área para detectar posible extrapolación
rango_area2 <- range(base2_modelo$areaconst, na.rm = TRUE)
cat(
"Rango observado de área en base2:",
round(rango_area2[1], 2), "a", round(rango_area2[2], 2), "m²\n"
)
## Rango observado de área en base2: 40 a 932 m²
solicitud2 <- data.frame(
areaconst = c(300, 300),
estrato = c(5, 6),
habitaciones = c(5, 5),
parqueaderos = c(3, 3),
banios = c(3, 3)
)
prediccion2 <- tryCatch(
predict(
modelo2_final,
newdata = solicitud2,
interval = "prediction",
level = 0.95
),
error = function(e){
stop(
paste0(
"No fue posible realizar la predicción de la Vivienda 2: ",
conditionMessage(e)
)
)
}
)
resultado_pred2 <- cbind(
solicitud2,
prediccion2
)
knitr::kable(
resultado_pred2,
digits = 2,
caption = "Predicciones de precio - Vivienda 2"
)
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 300 | 5 | 5 | 3 | 3 | 675.02 | 481.45 | 868.59 |
| 300 | 6 | 5 | 3 | 3 | 735.92 | 542.31 | 929.53 |
ofertas2 <- seleccionar_ofertas(
base = base2,
modelo = modelo2_final,
presupuesto = 850,
area_obj = 300,
estratos_obj = c(5, 6),
habitaciones_obj = 5,
parqueaderos_obj = 3,
banios_obj = 3,
n = 5
)
knitr::kable(
ofertas2 %>%
dplyr::select(
id,
barrio,
preciom,
precio_estimado,
diferencia_modelo,
areaconst,
estrato,
habitaciones,
parqueaderos,
banios,
cumple_estricto
),
digits = 2,
caption = "Cinco ofertas potenciales - Vivienda 2"
)
| id | barrio | preciom | precio_estimado | diferencia_modelo | areaconst | estrato | habitaciones | parqueaderos | banios | cumple_estricto |
|---|---|---|---|---|---|---|---|---|---|---|
| 7512 | seminario | 670 | 751.58 | 81.58 | 300 | 5 | 6 | 3 | 5 | TRUE |
| 7182 | guadalupe | 730 | 1279.33 | 549.33 | 573 | 5 | 5 | 3 | 8 | TRUE |
| 6175 | capri | 350 | 661.31 | 311.31 | 270 | 5 | 4 | 3 | 3 | FALSE |
| 7680 | pampa linda | 450 | 682.29 | 232.29 | 267 | 5 | 3 | 3 | 3 | FALSE |
| 6205 | capri | 350 | 673.30 | 323.30 | 260 | 5 | 3 | 3 | 3 | FALSE |
if(nrow(ofertas2) > 0){
leaflet::leaflet(ofertas2) %>%
leaflet::addTiles() %>%
leaflet::addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 7,
fillOpacity = 0.8,
popup = ~paste0(
"<b>ID:</b> ", id,
"<br><b>Barrio:</b> ", barrio,
"<br><b>Precio:</b> $", preciom, " millones",
"<br><b>Predicción:</b> $", round(precio_estimado, 1), " millones",
"<br><b>Área:</b> ", areaconst, " m²",
"<br><b>Estrato:</b> ", estrato,
"<br><b>Habitaciones:</b> ", habitaciones,
"<br><b>Baños:</b> ", banios,
"<br><b>Parqueaderos:</b> ", parqueaderos,
"<br><b>Cumple estrictamente:</b> ", cumple_estricto
)
)
}
resumen_modelos <- data.frame(
Solicitud = c("Vivienda 1", "Vivienda 2"),
Tipo = c("Casa", "Apartamento"),
Zona = c("Zona Norte", "Zona Sur"),
Presupuesto_millones = c(350, 850),
R2 = c(
summary(modelo1_final)$r.squared,
summary(modelo2_final)$r.squared
),
R2_Ajustado = c(
summary(modelo1_final)$adj.r.squared,
summary(modelo2_final)$adj.r.squared
)
)
knitr::kable(
resumen_modelos,
digits = 3,
caption = "Resumen comparativo de los modelos"
)
| Solicitud | Tipo | Zona | Presupuesto_millones | R2 | R2_Ajustado |
|---|---|---|---|---|---|
| Vivienda 1 | Casa | Zona Norte | 350 | 0.602 | 0.599 |
| Vivienda 2 | Apartamento | Zona Sur | 850 | 0.749 | 0.748 |
El análisis permitió construir modelos de regresión múltiple independientes para las dos solicitudes inmobiliarias. Para cada caso se evaluó la relación entre el precio y las características físicas de las viviendas, se verificó la significancia de los coeficientes, se compararon especificaciones alternativas y se analizaron los principales supuestos del modelo.
Para la primera solicitud, las predicciones deben contrastarse con el presupuesto máximo de 350 millones de pesos y con el cumplimiento de las características solicitadas. Las ofertas seleccionadas constituyen alternativas de interés por su cercanía al perfil buscado y por encontrarse dentro del límite financiero establecido.
Para la segunda solicitud, el mismo procedimiento permite establecer si los apartamentos disponibles y sus precios estimados son compatibles con el crédito máximo preaprobado de 850 millones de pesos.
Las estimaciones obtenidas deben considerarse como apoyo cuantitativo a la decisión y no como una valoración comercial definitiva. Antes de una recomendación final es conveniente complementar el análisis con inspección física, antigüedad del inmueble, estado de conservación, calidad de acabados, costos de administración, seguridad, accesibilidad y condiciones particulares de negociación.
El siguiente bloque registra las versiones de R y de los paquetes utilizados. Esto facilita la trazabilidad del análisis y permite diagnosticar diferencias entre equipos.
sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] paqueteMODELOS_0.1.0 summarytools_1.1.5 gridExtra_2.3
## [4] GGally_2.4.0 boot_1.3-32 DT_0.34.0
## [7] knitr_1.52 nortest_1.0-4 car_3.1-5
## [10] carData_3.0-6 lmtest_0.9-40 zoo_1.8-15
## [13] broom_1.0.12 leaflet_2.2.3 plotly_4.12.1
## [16] ggplot2_4.0.3 stringr_1.6.0 tidyr_1.3.2
## [19] dplyr_1.2.1
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.56 bslib_0.10.0
## [4] htmlwidgets_1.6.4 lattice_0.22-7 vctrs_0.7.1
## [7] tools_4.5.2 crosstalk_1.2.2 generics_0.1.4
## [10] tibble_3.3.1 pkgconfig_2.0.3 Matrix_1.7-4
## [13] data.table_1.18.2.1 checkmate_2.3.4 RColorBrewer_1.1-3
## [16] S7_0.2.1 lifecycle_1.0.5 compiler_4.5.2
## [19] farver_2.1.2 rapportools_1.2 htmltools_0.5.9
## [22] sass_0.4.10 yaml_2.3.12 Formula_1.2-5
## [25] pillar_1.11.1 jquerylib_0.1.4 MASS_7.3-65
## [28] cachem_1.1.0 magick_2.9.1 abind_1.4-8
## [31] nlme_3.1-168 ggstats_0.13.0 tidyselect_1.2.1
## [34] digest_0.6.39 stringi_1.8.7 reshape2_1.4.5
## [37] purrr_1.2.1 pander_0.6.6 labeling_0.4.3
## [40] splines_4.5.2 fastmap_1.2.0 grid_4.5.2
## [43] cli_3.6.5 magrittr_2.0.4 base64enc_0.1-6
## [46] utf8_1.2.6 withr_3.0.2 scales_1.4.0
## [49] backports_1.5.0 lubridate_1.9.5 timechange_0.4.0
## [52] rmarkdown_2.32 httr_1.4.8 matrixStats_1.5.0
## [55] otel_0.2.0 evaluate_1.0.5 tcltk_4.5.2
## [58] viridisLite_0.4.3 mgcv_1.9-3 rlang_1.3.0
## [61] Rcpp_1.1.1 glue_1.8.0 rstudioapi_0.18.0
## [64] jsonlite_2.0.0 plyr_1.8.9 R6_2.6.1