Aunque las base contiene un total de 13 variables, algunas de ellas no son de nuestro interes para el analisis, aún más teniendo en cuenta que filtraremos los datos para la zona norte y el tipo Casa.
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ id : num [1:8322] 1147 1169 1350 5992 1212 ...
## $ zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr [1:8322] NA NA NA "02" ...
## $ estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
## $ barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## $ longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
## - attr(*, "spec")=List of 3
## ..$ cols :List of 13
## .. ..$ id : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ zona : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ piso : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ estrato : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ preciom : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ areaconst : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ parqueaderos: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ banios : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ habitaciones: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ tipo : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ barrio : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ longitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ latitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## ..$ default: list()
## .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
## ..$ delim : chr ";"
## ..- attr(*, "class")= chr "col_spec"
## - attr(*, "problems")=<externalptr>
base1 <- subset(vivienda, zona == "Zona Norte" & tipo == "Casa")
Podemos observar que existe una alta corrrelación entre el precio y el área construida, así como con las variables estrato y Baños.
head(base1)
## # A tibble: 6 × 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 4460 Zona N… 02 4 625 355 3 5 5
## 5 6081 Zona N… 02 5 750 237 2 6 6
## 6 7824 Zona N… 02 4 600 160 1 4 5
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
ggpairs(base1[,3:9], title=" ")
## Warning: Removed 372 rows containing missing values or values outside the scale range
## (`stat_boxplot()`).
## Removed 372 rows containing missing values or values outside the scale range
## (`stat_boxplot()`).
## Removed 372 rows containing missing values or values outside the scale range
## (`stat_boxplot()`).
## Removed 372 rows containing missing values or values outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 96 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 372 rows containing missing values or values outside the scale range
## (`stat_boxplot()`).
## Removed 372 rows containing missing values or values outside the scale range
## (`stat_boxplot()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning in ggally_statistic(data = data, mapping = mapping, na.rm = na.rm, :
## Removed 287 rows containing missing values
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning in ggally_statistic(data = data, mapping = mapping, na.rm = na.rm, :
## Removed 287 rows containing missing values
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning in ggally_statistic(data = data, mapping = mapping, na.rm = na.rm, :
## Removed 287 rows containing missing values
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 287 rows containing non-finite outside the scale range
## (`stat_bin()`).
## Warning: Removed 287 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Removed 287 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Removed 287 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 287 rows containing non-finite outside the scale range
## (`stat_density()`).
## Warning in ggally_statistic(data = data, mapping = mapping, na.rm = na.rm, :
## Removed 287 rows containing missing values
## Warning in ggally_statistic(data = data, mapping = mapping, na.rm = na.rm, :
## Removed 287 rows containing missing values
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 287 rows containing missing values or values outside the scale range
## (`geom_point()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 287 rows containing missing values or values outside the scale range
## (`geom_point()`).
Utilizaremos el método de Winzorización para estandarizar la base, imputando a los faltantes con 1 debido a que es difícil o improbable que existan casas con 0 baños, 0 habitaciones o demás variables que puedan truncar nuestro analisis. Con este método podemos observar que las correlaciones de las variables mencionadas anteriormente aumentan.
columnas_no_numericas <- sapply(base1, function(x) !is.numeric(x))
print(names(base1)[columnas_no_numericas])
## [1] "zona" "piso" "tipo" "barrio"
base1[columnas_no_numericas] <- lapply(base1[columnas_no_numericas], as.numeric)
## Warning in lapply(base1[columnas_no_numericas], as.numeric): NAs introducidos
## por coerción
## Warning in lapply(base1[columnas_no_numericas], as.numeric): NAs introducidos
## por coerción
## Warning in lapply(base1[columnas_no_numericas], as.numeric): NAs introducidos
## por coerción
base1[is.na(base1)] <- 1
base2 <- as.data.frame(lapply(base1, Winsorize))
ggpairs(base2[,3:9], title=" ")
## Mapa
En el mapa aparecen unos puntos por fuera de la zona norte, esto se puede deber a la imputación del barrio por parte del capturador de información, dicho error se puede soluccionar con un método de corrección de georeferenciación con la dirección del inmueble.
datos <- data.frame(
latitud = base2$latitud,
longitud = base2$longitud
)
mapa <- leaflet(data = datos) %>%
addTiles() %>%
addMarkers(~longitud, ~latitud, clusterOptions = markerClusterOptions())
mapa
Verificamos los mínimos y máximos para nuestra base y procedemos con los analísis.
summary(base2)
## id zona piso estrato preciom
## Min. : 206.2 Min. :1 Min. :1.000 Min. :3.000 Min. :145.0
## 1st Qu.: 766.2 1st Qu.:1 1st Qu.:1.000 1st Qu.:3.000 1st Qu.:261.2
## Median :2257.0 Median :1 Median :1.000 Median :4.000 Median :390.0
## Mean :2532.0 Mean :1 Mean :1.468 Mean :4.202 Mean :430.7
## 3rd Qu.:4225.0 3rd Qu.:1 3rd Qu.:2.000 3rd Qu.:5.000 3rd Qu.:550.0
## Max. :6135.1 Max. :1 Max. :3.000 Max. :6.000 Max. :928.5
## areaconst parqueaderos banios habitaciones tipo
## Min. : 75.0 Min. :1.000 Min. :2.000 Min. :3.000 Min. :1
## 1st Qu.:140.0 1st Qu.:1.000 1st Qu.:2.000 1st Qu.:3.000 1st Qu.:1
## Median :240.0 Median :1.000 Median :3.000 Median :4.000 Median :1
## Mean :254.2 Mean :1.633 Mean :3.546 Mean :4.557 Mean :1
## 3rd Qu.:336.8 3rd Qu.:2.000 3rd Qu.:4.000 3rd Qu.:5.000 3rd Qu.:1
## Max. :535.5 Max. :4.000 Max. :6.000 Max. :8.000 Max. :1
## barrio longitud latitud
## Min. :1 Min. :-76.54 Min. :3.386
## 1st Qu.:1 1st Qu.:-76.53 1st Qu.:3.452
## Median :1 Median :-76.52 Median :3.468
## Mean :1 Mean :-76.52 Mean :3.461
## 3rd Qu.:1 3rd Qu.:-76.50 3rd Qu.:3.482
## Max. :1 Max. :-76.49 Max. :3.489
table(base2$zona)
##
## 1
## 722
table(base2$tipo)
##
## 1
## 722
table(base2$barrio)
##
## 1
## 722
Al menos 2/3 de nuestros datos se úbican en los estratos 4, 5 y 6, lo que corresponde con el segmento de altos ingresos que es el objeto de nuestro analísis.
colores_personalizados <- c("#ffbc42", "#0496ff", "#d81159", "#8f2d56")
frecuencia_estrato <- table(base2$estrato)
pie(frecuencia_estrato,
labels = paste(names(frecuencia_estrato), "\n", frecuencia_estrato),
main = "Distribución de Estratos",
col = colores_personalizados)
## Correlaciones interactivas
Como era de esperarse, a mayor precio mayor es el área construida y el estrato, este graáfico puede ser usado por el vendedor para identificar con el cliente su mejor posibilidad dado el recurso disponible, e incluso identificar datos atipicos para compras inmediatas.
scatter_plot <- plot_ly(data = base2, x = ~areaconst, y = ~preciom, mode = "markers",
marker = list(color = ~estrato, colorscale = "Viridis"),
text = ~paste("Estrato: ", estrato, "<br>Precio $", preciom),
hoverinfo = "text") %>%
layout(title = "Relación entre área construida y Precio",
xaxis = list(title = "Área Construida"),
yaxis = list(title = "Precio"))
scatter_plot
## No trace type specified:
## Based on info supplied, a 'scatter' trace seems appropriate.
## Read more about this trace type -> https://plotly.com/r/reference/#scatter
En el gráfico de contornos verificamos el razonamiento anterior y se puede utilizar como presentación en la sala de ventas con la respectiva explicación de los colores.
contour_plot <- plot_ly(data = base2, x = ~areaconst, y = ~preciom, z = ~estrato,
type = "contour",
colorscale = "Viridis",
colorbar = list(title = "Estrato"),
contours = list(coloring = "heatmap"),
hoverinfo = "text",
text = ~paste("Estrato: ", estrato, "<br>Precio $", preciom, "<br>Área Construida: ", areaconst))
contour_plot <- contour_plot %>%
layout(title = "Contorno de Precio y Área Construida según Estrato",
xaxis = list(title = "Área Construida"),
yaxis = list(title = "Precio"))
contour_plot
Así mismo los gráficos de cajas y bigotes pueden servirle al vendedor para identificar los maximos, mínimos y medias de forma rápida e interactiva.
boxplot_precio <- plot_ly(data = base2, y = ~preciom, type = "box", name = "Precio")
boxplot_banios <- plot_ly(data = base2, y = ~banios, type = "box", name = "Baños")
boxplot_habitaciones <- plot_ly(data = base2, y = ~habitaciones, type = "box", name = "Habitaciones")
boxplot_area <- plot_ly(data = base2, y = ~areaconst, type = "box", name = "Área Construida")
boxplot_estrato <- plot_ly(data = base2, y = ~estrato, type = "box", name = "Estrato")
boxplot_precio <- boxplot_precio %>% layout(title = "Boxplot de Precio")
boxplot_banios <- boxplot_banios %>% layout(title = "Boxplot de Baños")
boxplot_habitaciones <- boxplot_habitaciones %>% layout(title = "Boxplot de Habitaciones")
boxplot_area <- boxplot_area %>% layout(title = "Boxplot de Área Construida")
boxplot_estrato <- boxplot_estrato %>% layout(title = "Boxplot de Estrato")
subplot(boxplot_precio, boxplot_banios, boxplot_habitaciones, boxplot_area, boxplot_estrato)
En el ajuste del primer modelo observamos un R2 de 0.73, con al menos 4 variables significativas incluidas el intercepto. En este modelo podemos observar que el cambio en el estrato puede representar un costo adicional de un inmubele en hasta 76.5 millones, un metro más de area construida en 0.86 millones y un baño adicional puede representar hasta 23.4 millones. En los análisis posteriores intentaremos mejorar este modelo.
modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
summary(modelo1)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base2)
##
## Residuals:
## Min 1Q Median 3Q Max
## -320.70 -68.68 -13.17 50.47 483.08
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -195.05217 22.75891 -8.570 < 2e-16 ***
## areaconst 0.85809 0.04338 19.782 < 2e-16 ***
## estrato 76.55798 5.43041 14.098 < 2e-16 ***
## habitaciones -1.19788 3.63388 -0.330 0.742
## parqueaderos 5.06191 4.90856 1.031 0.303
## banios 23.42380 4.65086 5.036 6.01e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 113.5 on 716 degrees of freedom
## Multiple R-squared: 0.7285, Adjusted R-squared: 0.7266
## F-statistic: 384.3 on 5 and 716 DF, p-value: < 2.2e-16
En el modelo 2 podemos observar una leve mejoría de hasta 2% en la capacidad de predicción del modelo usando las variables estadisticamente significativas.Tambien observamos un aumento de los coeficientes.
set.seed(123)
indices_entrenamiento <- createDataPartition(base2$preciom, p = 0.7, list = FALSE)
datos_entrenamiento <- base2[indices_entrenamiento, ]
datos_prueba <- base2[-indices_entrenamiento, ]
modelo2 <- lm(preciom ~ areaconst + estrato + banios, data = datos_entrenamiento)
summary(modelo2)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + banios, data = datos_entrenamiento)
##
## Residuals:
## Min 1Q Median 3Q Max
## -309.36 -69.80 -12.72 50.77 479.21
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -210.00168 22.86463 -9.185 < 2e-16 ***
## areaconst 0.88822 0.04945 17.961 < 2e-16 ***
## estrato 78.63187 6.13426 12.818 < 2e-16 ***
## banios 24.90111 4.58869 5.427 8.94e-08 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 112.5 on 503 degrees of freedom
## Multiple R-squared: 0.7412, Adjusted R-squared: 0.7397
## F-statistic: 480.3 on 3 and 503 DF, p-value: < 2.2e-16
predicciones <- predict(modelo2, newdata = datos_prueba)
head(predicciones)
## 2 6 7 9 10 11
## 595.3828 346.2447 460.4052 217.8106 462.0698 207.1831
summary(predicciones)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 142.3 249.0 443.9 438.1 578.1 886.8
plot(predicciones)
## Predicciones vs Datos reales
En esta gráfica podemos ver como los puntos de predicción se ajustan a la línea de tendencia.
ggplot(datos_prueba, aes(x = preciom, y = predicciones)) +
geom_point(aes(color = "Predicción")) +
geom_point(aes(y = preciom, color = "Real")) +
geom_abline(intercept = 0, slope = 1, color = "blue", linetype = "dashed") +
labs(x = "Precio Real", y = "Predicción") +
ggtitle("Modelo Base vs Predicción") +
theme_minimal() +
scale_color_manual(values = c("blue", "red"),
labels = c("Real", "Predicción")) +
guides(color = guide_legend(title = "Datos"))
base3 <- base2[, !(names(base1) %in% c("id", "longitud", "latitud", "zona", "tipo", "barrio"))]
Mediante el modelo de Bosque aleatorio pasaremos a confirmar que nuestra elección de variables es la mejor posible dado la explicación que puede alcanzar el modelo dadola combinación de distintos árboles. Dicho resultado nos confirma la elección de variables con un 70% de la varianza explicada.
library(randomForest)
## Warning: package 'randomForest' was built under R version 4.3.3
## randomForest 4.7-1.1
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
## The following object is masked from 'package:dplyr':
##
## combine
## The following object is masked from 'package:gridExtra':
##
## combine
## The following object is masked from 'package:ggplot2':
##
## margin
predictores <- base3[, -which(names(base3) == "preciom")]
objetivo <- base3$preciom # Variable 'preciom'
modelo_rf <- randomForest(x = predictores, y = objetivo)
print(modelo_rf)
##
## Call:
## randomForest(x = predictores, y = objetivo)
## Type of random forest: regression
## Number of trees: 500
## No. of variables tried at each split: 2
##
## Mean of squared residuals: 12605.82
## % Var explained: 73.24
importance <- importance(modelo_rf)
varImpPlot(modelo_rf)
print(modelo_rf$err.rate)
## NULL
Para la determinación del precio las variables más importantes son el estrato, el area construida y el número de baños, las demás variables con instrumentales a los principales determinantes del precio.
Los vendedores pueden ofrecer a forma de marketing las demás carécteristicas sin que eso cambie de forma radical el precio.
En el segmento de las casas para altos ingresos las variables habitaciones y otras variables que pudierán ser significativas para familias númerosas no tienen efecto en este segmento. Dicha tesis se podría confirmar con la tasa de fecundidad respecto al nivel de ingreso, estimando de forma inicial un comportamiento decreciente.