Carga de 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.
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")
| 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")
| 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
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:
preciom vs areaconst
(rojo claro) presentan una correlación positiva significativa, pues
entre mayor área construida, mayor precio de la viviendapreciom 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.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:
# 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:
Existe una relación directa y positiva entre estrato y precio.
Los estratos más altos no solo tienen precios más altos, sino también más dispersión.
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:
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:
Á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.
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.
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.
Número de parqueaderos (21.4, p<0.002): Cada parqueadero adicional incrementa el precio en promedio 23.0 millones.
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.
Ajuste del modelo: R² = 0.6799 El modelo explica el 65.1% de la variabilidad en el precio.
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
En promedio, el modelo se equivoca en 98 millones de pesos respecto al valor real de una vivienda.
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.
# 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
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:
# 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
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
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
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.
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.
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 :
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"
)
)
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")
| 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")
| 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
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:
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.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.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:
# 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:
Existe una relación directa y positiva entre estrato y precio.
Los estratos más altos no solo tienen precios más altos, sino también más dispersión.
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:
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:
Á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.
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.
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
Número de parqueaderos (92.6, p<0.001): Cada parqueadero adicional incrementa el precio en promedio 86.6 millones.
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.
Ajuste del modelo: R² = 0.7587 El modelo explica el 75.9 % de la variabilidad en el precio.
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
En promedio, el modelo se equivoca en 233 millones de pesos respecto al valor real de una vivienda.
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.
# 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
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:
# 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
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
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
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.
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.
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:
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.