C&A necesita ubicar vivienda para dos empleados de un cliente corporativo: una casa en el norte de Cali (200 m², 4 habitaciones, 2 baños, 1 parqueadero, estrato 4 o 5, crédito preaprobado de $350 millones) y un apartamento en el sur (300 m², 5 habitaciones, 3 baños, 3 parqueaderos, estrato 5 o 6, crédito de $850 millones). Este informe estima un precio de referencia justo para cada perfil y propone propiedades concretas dentro del inventario disponible.
Se depuró una base de 8322 transacciones de vivienda en Cali hasta dejar 8,215 registros útiles. Como el precio se comporta de forma distinta según la zona y el tipo de inmueble, se construyeron dos modelos de regresión lineal múltiple independientes en lugar de uno solo: uno para casas del norte (n = 698) y otro para apartamentos del sur (n = 2,776).
En ambos modelos, el área construida y el estrato son los factores que más explican el precio. Las diferencias interesantes aparecen en el resto: en apartamentos del sur, más parqueaderos y más baños suben el precio de forma clara, pero más habitaciones —a igual área— lo baja, señal de que el mercado valora más los espacios amplios que la cantidad de cuartos. En casas del norte, en cambio, ni las habitaciones ni los parqueaderos resultan predictores confiables una vez se controla por área, estrato y baños. El modelo de casas explica 67% de la variación del precio; el de apartamentos, 76%.
Para elegir entre un modelo simple, el modelo completo y una versión logarítmica del precio, el criterio decisivo fue el error de predicción en pesos (no el R²): el modelo completo en escala original predice mejor en la práctica y es más fácil de interpretar, así que es el que se usa en todo el informe.
| Solicitud | Escenario | Precio estimado | Intervalo de predicción (95%) | Presupuesto |
|---|---|---|---|---|
| 1. Casa, zona norte | Estrato 4 | $328 M | $27 M – $630 M | $350 M |
| 1. Casa, zona norte | Estrato 5 | $408 M | $106 M – $711 M | $350 M |
| 2. Apartamento, zona sur | Estrato 5 | $690 M | $505 M – $875 M | $850 M |
| 2. Apartamento, zona sur | Estrato 6 | $746 M | $561 M – $931 M | $850 M |
La recomendación es clara en ambos casos. Para la casa del norte, solo el estrato 4 cabe dentro del presupuesto; el estrato 5 lo excede en cerca de $58 M. Para el apartamento del sur, ambos estratos son viables, con margen cómodo de presupuesto — suficiente para inclinarse por estrato 6 si el cliente prioriza ubicación y acabados. Se identificaron 110 y 1,436 propiedades reales dentro de presupuesto para cada solicitud, respectivamente, de las cuales se seleccionan al menos cinco por caso más adelante en el informe.
C&A es una agencia inmobiliaria en Cali que apoya a una empresa internacional en la búsqueda de vivienda para sus empleados. Cuenta con un histórico de transacciones (zona, tipo, estrato, área, habitaciones, baños, parqueaderos, barrio, coordenadas y precio), pero no con una forma sistemática de convertir ese histórico en un precio de referencia y una lista corta de alternativas para un perfil concreto.
Para cada una de las dos solicitudes, C&A necesita responder dos preguntas: ¿cuál es un precio razonable para el inmueble solicitado, dado el crédito preaprobado del cliente?, y ¿qué propiedades del inventario disponible se ajustan mejor a ese perfil y ese presupuesto?
El objetivo estadístico de este informe es estimar, mediante regresión lineal múltiple, la relación entre el precio de la vivienda y sus características físicas —área, estrato, habitaciones, baños y parqueaderos— de forma específica para cada zona y tipo de inmueble, y usar esa relación para predecir precios y evaluar alternativas.
La base vivienda, distribuida en el paquete
paqueteMODELOS del curso, contiene 8322 registros de
transacciones de vivienda en Cali.
glimpse(vivienda)
## Rows: 8,322
## Columns: 13
## $ id <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
## $ estrato <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
## $ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
vars_num <- c("preciom", "areaconst", "estrato", "habitaciones", "banios", "parqueaderos")
data.frame(
Variable = vars_num,
`% NA` = sapply(vars_num, function(v) round(100 * mean(is.na(vivienda[[v]])), 1)),
Mínimo = sapply(vars_num, function(v) min(vivienda[[v]], na.rm = TRUE)),
Media = sapply(vars_num, function(v) round(mean(vivienda[[v]], na.rm = TRUE), 1)),
Máximo = sapply(vars_num, function(v) max(vivienda[[v]], na.rm = TRUE)),
check.names = FALSE
) %>% tabla()
| Variable | % NA | Mínimo | Media | Máximo | |
|---|---|---|---|---|---|
| preciom | preciom | 0.0 | 58 | 433.9 | 1,999 |
| areaconst | areaconst | 0.0 | 30 | 174.9 | 1,745 |
| estrato | estrato | 0.0 | 3 | 4.6 | 6 |
| habitaciones | habitaciones | 0.0 | 0 | 3.6 | 10 |
| banios | banios | 0.0 | 0 | 3.1 | 10 |
| parqueaderos | parqueaderos | 19.3 | 1 | 1.8 | 10 |
Antes de modelar, revisamos qué tan confiable es esta base. Encontramos seis problemas concretos:
parqueaderos falta en 19% de los
registros, y piso en 32%. El resto de variables casi no
tiene datos faltantes.habitaciones = 0 o
banios = 0 en 66 y 45 casos — implausible para una
vivienda habitable.barrio con formato inconsistente
(mayúsculas y acentos mal codificados). No afecta el modelo porque
barrio no es una variable explicativa, pero limita un
análisis de precio por barrio sin limpieza adicional.Con esto, tomamos cinco decisiones de depuración:
zona, tipo,
estrato, preciom, areaconst,
habitaciones, banios).parqueaderos = 0 cuando falta el
dato. Los valores que sí están registrados van de 1 a 10 —
nunca 0 — lo que sugiere que un anuncio sin parqueadero simplemente no
diligencia el campo, en vez de escribir “0”. Es un supuesto razonable
pero no verificado; se preservan así 1,605 registros que de otro modo se
perderían.piso del modelo. Con un tercio
de datos faltantes y sin estar entre las variables que pide el caso, no
compensa el riesgo de sesgo por imputación.n0 <- nrow(vivienda)
vivienda_limpia <- vivienda %>%
filter(!is.na(zona), !is.na(tipo), !is.na(estrato), !is.na(preciom),
!is.na(areaconst), !is.na(habitaciones), !is.na(banios)) %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos)) %>%
filter(habitaciones > 0, banios > 0, areaconst > 0, preciom > 0) %>%
mutate(precio_m2 = preciom * 1e6 / areaconst) %>%
filter(precio_m2 >= 300000, precio_m2 <= 6500000)
n1 <- nrow(vivienda_limpia)
cat("Registros iniciales:", n0, "| Tras depuración:", n1,
"| Removido:", round(100*(n0-n1)/n0, 2), "%\n")
## Registros iniciales: 8322 | Tras depuración: 8215 | Removido: 1.29 %
La pérdida neta es de 1.29%: de 8322 registros quedan 8215, una base que conserva la representatividad del mercado.
De ahí se filtran los dos subconjuntos que usará cada solicitud:
casas_norte <- vivienda_limpia %>% filter(zona == "Zona Norte", tipo == "Casa")
aptos_sur <- vivienda_limpia %>% filter(zona == "Zona Sur", tipo == "Apartamento")
cat("Casas, zona norte:", nrow(casas_norte), "| Apartamentos, zona sur:", nrow(aptos_sur), "\n")
## Casas, zona norte: 698 | Apartamentos, zona sur: 2776
Ambos tamaños de muestra (698 y 2776) son más que suficientes para una regresión con cinco predictores.
p1 <- ggplot(casas_norte, aes(x = preciom)) +
geom_histogram(bins = 30, fill = "#2C5F8A", color = "white") +
labs(title = "Casas — Zona Norte", x = "Precio (millones $)", y = "Frecuencia") +
theme_minimal()
p2 <- ggplot(aptos_sur, aes(x = preciom)) +
geom_histogram(bins = 30, fill = "#B2472B", color = "white") +
labs(title = "Apartamentos — Zona Sur", x = "Precio (millones $)", y = "Frecuencia") +
theme_minimal()
gridExtra::grid.arrange(p1, p2, ncol = 2)
bind_rows(
casas_norte %>% summarise(Segmento = "Casas Zona Norte", n = n(), Media = mean(preciom),
Mediana = median(preciom), `Desv. estándar` = sd(preciom),
Mín. = min(preciom), Máx. = max(preciom)),
aptos_sur %>% summarise(Segmento = "Apartamentos Zona Sur", n = n(), Media = mean(preciom),
Mediana = median(preciom), `Desv. estándar` = sd(preciom),
Mín. = min(preciom), Máx. = max(preciom))
) %>% tabla()
| Segmento | n | Media | Mediana | Desv. estándar | Mín. | Máx. |
|---|---|---|---|---|---|---|
| Casas Zona Norte | 698 | 443.31 | 390 | 268.13 | 89 | 1,940 |
| Apartamentos Zona Sur | 2,776 | 296.97 | 245 | 190.89 | 75 | 1,750 |
En los dos segmentos el precio está sesgado a la derecha: la mayoría de las propiedades se agrupa en precios bajos-medios y una cola de inmuebles de alto valor estira la distribución. Esta asimetría es justamente la razón por la que, más adelante, evaluamos una transformación logarítmica del precio como alternativa de modelamiento.
mk_scatter <- function(df, var, titulo, color_pt) {
ggplot(df, aes(x = .data[[var]], y = preciom)) +
geom_point(alpha = 0.25, color = color_pt) +
geom_smooth(method = "lm", se = FALSE, color = "#333333") +
labs(title = titulo, x = var, y = "Precio (millones $)") +
theme_minimal(base_size = 10)
}
gridExtra::grid.arrange(
mk_scatter(casas_norte, "areaconst", "Área — Casas Norte", "#2C5F8A"),
mk_scatter(casas_norte, "estrato", "Estrato — Casas Norte", "#2C5F8A"),
mk_scatter(casas_norte, "habitaciones", "Habitaciones — Casas Norte", "#2C5F8A"),
mk_scatter(casas_norte, "banios", "Baños — Casas Norte", "#2C5F8A"),
ncol = 2
)
gridExtra::grid.arrange(
mk_scatter(aptos_sur, "areaconst", "Área — Aptos. Sur", "#B2472B"),
mk_scatter(aptos_sur, "estrato", "Estrato — Aptos. Sur", "#B2472B"),
mk_scatter(aptos_sur, "habitaciones", "Habitaciones — Aptos. Sur", "#B2472B"),
mk_scatter(aptos_sur, "banios", "Baños — Aptos. Sur", "#B2472B"),
ncol = 2
)
En ambos segmentos, el área construida es la relación más fuerte y clara con el precio: a más metros cuadrados, precios sistemáticamente más altos. El estrato también marca una tendencia positiva evidente. Habitaciones y baños muestran relaciones más ruidosas, con bastante dispersión alrededor de la línea de tendencia — una primera señal de que su aporte, una vez controladas las demás variables, podría ser más débil de lo que sugiere el gráfico. Esto se confirma (y en el caso de las habitaciones, se matiza de forma interesante) en la sección de modelo.
cor_norte <- round(cor(casas_norte[, c("preciom","areaconst","estrato","habitaciones","banios","parqueaderos")]), 2)
cor_sur <- round(cor(aptos_sur[, c("preciom","areaconst","estrato","habitaciones","banios","parqueaderos")]), 2)
plot_ly(z = cor_norte, x = colnames(cor_norte), y = rownames(cor_norte),
type = "heatmap", colors = colorRamp(c("#B2472B", "white", "#2C5F8A")),
zmin = -1, zmax = 1) %>%
layout(title = "Correlaciones — Casas Zona Norte")
plot_ly(z = cor_sur, x = colnames(cor_sur), y = rownames(cor_sur),
type = "heatmap", colors = colorRamp(c("#B2472B", "white", "#2C5F8A")),
zmin = -1, zmax = 1) %>%
layout(title = "Correlaciones — Apartamentos Zona Sur")
El área es la variable más correlacionada con el precio en los dos segmentos (0.76 y 0.77), seguida del estrato (0.62 y 0.67) y los baños (0.57 y 0.73); las habitaciones son, en ambos casos, la de menor correlación (0.38 y 0.34). Entre los propios predictores, la correlación más alta es la de baños con habitaciones (0.61 y 0.52) — esperable, porque las viviendas más grandes tienden a tener más de las dos cosas. Esa correlación no es lo bastante alta como para anticipar un problema de multicolinealidad, pero lo confirmamos formalmente con el VIF una vez estimado el modelo.
El modelo conceptual, igual para los dos segmentos, es:
\[\text{preciom} = \beta_0 + \beta_1\,\text{areaconst} + \beta_2\,\text{estrato} + \beta_3\,\text{habitaciones} + \beta_4\,\text{parqueaderos} + \beta_5\,\text{banios} + \varepsilon\]
preciom es el precio en millones de pesos; cada \(\beta_i\) es el cambio esperado en el
precio por unidad adicional de esa variable, manteniendo las
demás constantes; \(\beta_0\)
es el intercepto y \(\varepsilon\) el
error no explicado. Los coeficientes se estiman por mínimos cuadrados
ordinarios, el método que encuentra los valores de \(\beta\) que minimizan la suma de los
residuales al cuadrado.
Como el precio se comporta distinto según la zona y el tipo de inmueble —ya visible en la exploración anterior—, ajustamos un modelo independiente por segmento en vez de forzar uno solo sobre toda la base. Usar el modelo de casas para predecir apartamentos, o viceversa, extrapolaría relaciones estimadas en un mercado distinto; la justificación completa de esta decisión se retoma en la comparación de modelos, más adelante.
modelo_norte <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = casas_norte)
modelo_sur <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = aptos_sur)
bind_rows(
tidy(modelo_norte, conf.int = TRUE) %>% mutate(Segmento = "Casas — Norte"),
tidy(modelo_sur, conf.int = TRUE) %>% mutate(Segmento = "Apartamentos — Sur")
) %>%
mutate(term = ifelse(term == "(Intercept)", "Intercepto", term),
p.value = fmt_p(p.value)) %>%
select(Segmento, Variable = term, Estimación = estimate, `Error estándar` = std.error,
`valor p` = p.value, `IC 95% inf.` = conf.low, `IC 95% sup.` = conf.high) %>%
tabla(digits = 2)
| Segmento | Variable | Estimación | Error estándar | valor p | IC 95% inf. | IC 95% sup. |
|---|---|---|---|---|---|---|
| Casas — Norte | Intercepto | -242.18 | 31.55 | < 0.001 | -304.12 | -180.24 |
| Casas — Norte | areaconst | 0.88 | 0.05 | < 0.001 | 0.79 | 0.98 |
| Casas — Norte | estrato | 80.13 | 7.49 | < 0.001 | 65.43 | 94.83 |
| Casas — Norte | habitaciones | 5.83 | 4.69 | 0.214 | -3.38 | 15.04 |
| Casas — Norte | parqueaderos | -2.22 | 4.21 | 0.597 | -10.48 | 6.04 |
| Casas — Norte | banios | 26.07 | 5.77 | < 0.001 | 14.75 | 37.40 |
| Apartamentos — Sur | Intercepto | -209.57 | 13.67 | < 0.001 | -236.37 | -182.77 |
| Apartamentos — Sur | areaconst | 1.56 | 0.05 | < 0.001 | 1.46 | 1.67 |
| Apartamentos — Sur | estrato | 55.83 | 2.77 | < 0.001 | 50.39 | 61.26 |
| Apartamentos — Sur | habitaciones | -26.65 | 3.49 | < 0.001 | -33.49 | -19.80 |
| Apartamentos — Sur | parqueaderos | 47.27 | 3.11 | < 0.001 | 41.16 | 53.37 |
| Apartamentos — Sur | banios | 47.43 | 3.12 | < 0.001 | 41.31 | 53.55 |
El área y el estrato encabezan los dos modelos, con signo positivo y alta significancia: cada m² adicional suma en promedio $0.88 M en casas del norte y $1.56 M en apartamentos del sur; cada estrato adicional suma $80 M y $56 M, respectivamente (siempre manteniendo las demás variables fijas).
El resto de variables es donde los dos segmentos se separan. En casas del norte, ni las habitaciones (p = 0.21) ni los parqueaderos (p = 0.60) son estadísticamente significativos una vez controlados área, estrato y baños — el modelo no logra distinguir su efecto del ruido. En apartamentos del sur, en cambio, parqueaderos y baños sí suman de forma clara y significativa (+$47 M cada uno), y las habitaciones muestran un efecto negativo y significativo (-$27 M por habitación adicional, p < 0.001): a igual área, más habitaciones implica cuartos más pequeños, y en este segmento el mercado paga menos por eso. Vale la pena tenerlo presente para la solicitud 2, que pide 5 habitaciones en 300 m² — un promedio generoso de 60 m² por habitación, lejos del escenario de cuartos diminutos que penaliza el coeficiente, pero un factor a vigilar al comparar alternativas.
bind_rows(
glance(modelo_norte) %>% mutate(Segmento = "Casas — Norte"),
glance(modelo_sur) %>% mutate(Segmento = "Apartamentos — Sur")
) %>%
mutate(p.value = fmt_p(p.value)) %>%
select(Segmento, n = nobs, R2 = r.squared, `R2 ajustado` = adj.r.squared,
`Error residual ($M)` = sigma, `Estadístico F` = statistic, `valor p` = p.value) %>%
tabla(digits = 3)
| Segmento | n | R2 | R2 ajustado | Error residual ($M) | Estadístico F | valor p |
|---|---|---|---|---|---|---|
| Casas — Norte | 698 | 0.675 | 0.673 | 153.327 | 287.905 | < 0.001 |
| Apartamentos — Sur | 2,776 | 0.760 | 0.760 | 93.599 | 1,754.509 | < 0.001 |
El modelo de casas del norte explica 67% de la variación del precio; el de apartamentos del sur, 76% — un ajuste notablemente mejor, en parte por la muestra más grande (2,776 vs. 698) y en parte porque en apartamentos las características físicas capturan una porción mayor del precio. Ambos modelos son globalmente significativos (p < 0.001). Un R² de 67-76% es útil para orientar una decisión de negocio, pero está lejos de 100%: ni acabados, ni antigüedad, ni ubicación exacta dentro de la zona están en la base, y esas variables sí influyen en el precio real de cada propiedad.
data.frame(
Variable = names(vif(modelo_norte)),
`Casas Norte` = round(vif(modelo_norte), 2),
`Apartamentos Sur` = round(vif(modelo_sur), 2),
check.names = FALSE
) %>% tabla()
| Variable | Casas Norte | Apartamentos Sur | |
|---|---|---|---|
| areaconst | areaconst | 1.73 | 2.22 |
| estrato | estrato | 1.61 | 1.73 |
| habitaciones | habitaciones | 1.82 | 1.45 |
| parqueaderos | parqueaderos | 1.23 | 1.85 |
| banios | banios | 2.13 | 2.66 |
Todos los VIF están por debajo de 2.7, muy lejos del umbral de preocupación habitual (5-10). Los predictores aportan información suficientemente independiente entre sí en los dos modelos, así que no hay razón para remover ninguno por redundancia.
Antes de confiar en las predicciones, revisamos si los residuales del modelo se comportan como asume la regresión lineal: relación lineal con los predictores, independencia entre observaciones, varianza constante (homocedasticidad) y distribución normal, además de identificar observaciones que puedan estar dominando el ajuste.
Casas — Zona Norte
par(mfrow = c(2,2))
plot(modelo_norte, main = "")
Apartamentos — Zona Sur
par(mfrow = c(2,2))
plot(modelo_sur, main = "")
sh_n <- shapiro.test(residuals(modelo_norte)); sh_s <- shapiro.test(residuals(modelo_sur))
bp_n <- bptest(modelo_norte); bp_s <- bptest(modelo_sur)
dw_n <- dwtest(modelo_norte); dw_s <- dwtest(modelo_sur)
cd_n <- cooks.distance(modelo_norte); cd_s <- cooks.distance(modelo_sur)
data.frame(
Supuesto = c("Normalidad (Shapiro-Wilk)", "Homocedasticidad (Breusch-Pagan)",
"Independencia (Durbin-Watson)", "Obs. influyentes (Cook's D > 4/n)"),
`Casas Norte` = c(
sprintf("W = %.3f, p %s", sh_n$statistic, fmt_p(sh_n$p.value)),
sprintf("BP = %.1f, p %s", bp_n$statistic, fmt_p(bp_n$p.value)),
sprintf("DW = %.2f, p %s", dw_n$statistic, fmt_p(dw_n$p.value)),
sprintf("%d de %d (%.1f%%)", sum(cd_n > 4/length(cd_n)), length(cd_n), 100*mean(cd_n > 4/length(cd_n)))
),
`Apartamentos Sur` = c(
sprintf("W = %.3f, p %s", sh_s$statistic, fmt_p(sh_s$p.value)),
sprintf("BP = %.1f, p %s", bp_s$statistic, fmt_p(bp_s$p.value)),
sprintf("DW = %.2f, p %s", dw_s$statistic, fmt_p(dw_s$p.value)),
sprintf("%d de %d (%.1f%%)", sum(cd_s > 4/length(cd_s)), length(cd_s), 100*mean(cd_s > 4/length(cd_s)))
),
check.names = FALSE
) %>% tabla()
| Supuesto | Casas Norte | Apartamentos Sur |
|---|---|---|
| Normalidad (Shapiro-Wilk) | W = 0.851, p < 0.001 | W = 0.786, p < 0.001 |
| Homocedasticidad (Breusch-Pagan) | BP = 105.8, p < 0.001 | BP = 944.6, p < 0.001 |
| Independencia (Durbin-Watson) | DW = 1.64, p < 0.001 | DW = 1.51, p < 0.001 |
| Obs. influyentes (Cook’s D > 4/n) | 51 de 698 (7.3%) | 153 de 2776 (5.5%) |
Linealidad. El panel de residuales vs. ajustados no muestra curvatura sistemática en ninguno de los dos modelos, aunque la dispersión crece con el precio — más sobre esto en homocedasticidad. No hay evidencia fuerte de una relación no lineal que el modelo esté ignorando.
Independencia. Los datos son de corte transversal, no una serie temporal, así que la independencia depende del diseño muestral más que de un orden en el tiempo. Aun así, Durbin-Watson se aleja de 2 en los dos modelos (1.64 y 1.51, p < 0.001), lo que probablemente refleja que registros consecutivos en la base comparten barrio o lote de captura, no una dependencia temporal real. No lo interpretamos como una violación grave, pero sí como motivo para tratar los errores estándar con cautela — ver limitaciones.
Homocedasticidad. Breusch-Pagan es significativo en ambos modelos, más marcado en apartamentos del sur (BP = 945 vs. 106), consistente con su rango de precios más amplio ($75 M a $1,900 M). Hay heterocedasticidad: los residuales se dispersan más en las viviendas de precio alto. En la práctica, esto significa que los intervalos de predicción son menos precisos para las propiedades más caras de cada segmento — algo a tener presente al leer los intervalos de la siguiente sección.
Normalidad. Shapiro-Wilk rechaza la normalidad en los dos modelos, y los histogramas y gráficos Q-Q (paneles superior derecho e inferior izquierdo, arriba) muestran colas más pesadas que una normal — causadas por un número pequeño de propiedades de precio muy alto en cada segmento. Esto no invalida los coeficientes estimados, que siguen siendo insesgados, pero sí reduce la exactitud de los intervalos de predicción, que deben leerse como aproximados.
Observaciones influyentes. Bajo el criterio de referencia 4/n, 51 casas del norte (7.3%) y 153 apartamentos del sur (5.5%) destacan como influyentes — una proporción esperable con este criterio conservador. Se concentran en los extremos de precio de cada segmento y no hay evidencia de que sean errores de captura, así que se conservan: eliminarlas sesgaría el modelo hacia el precio “típico” y perdería justo la información sobre el extremo alto del mercado que puede interesarle al cliente.
Para cada segmento comparamos tres especificaciones — un modelo simple (solo área), el modelo completo del caso, y una versión con el precio en escala logarítmica, para atender el sesgo visto en la exploración — usando una partición 80/20 (entrenamiento/validación) que da una medida de error honesta fuera de muestra.
comparar_modelos <- function(df) {
idx <- sample(seq_len(nrow(df)), size = floor(0.8 * nrow(df)))
train <- df[idx, ]; test <- df[-idx, ]
m1 <- lm(preciom ~ areaconst, data = train)
m2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train)
m3 <- lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train)
rmse <- function(mod, log = FALSE) {
pred <- predict(mod, newdata = test)
if (log) pred <- exp(pred)
sqrt(mean((test$preciom - pred)^2, na.rm = TRUE))
}
data.frame(
Modelo = c("M1: solo área", "M2: completo (nivel)", "M3: completo (log)"),
`R2 ajustado` = c(summary(m1)$adj.r.squared, summary(m2)$adj.r.squared, summary(m3)$adj.r.squared),
AIC = c(AIC(m1), AIC(m2), AIC(m3)),
BIC = c(BIC(m1), BIC(m2), BIC(m3)),
`RMSE validación ($M)` = c(rmse(m1), rmse(m2), rmse(m3, log = TRUE)),
check.names = FALSE
)
}
comp_norte <- comparar_modelos(casas_norte) %>% mutate(Segmento = "Casas Norte", .before = 1)
comp_sur <- comparar_modelos(aptos_sur) %>% mutate(Segmento = "Apartamentos Sur", .before = 1)
bind_rows(comp_norte, comp_sur) %>% tabla(digits = 2)
| Segmento | Modelo | R2 ajustado | AIC | BIC | RMSE validación ($M) |
|---|---|---|---|---|---|
| Casas Norte | M1: solo área | 0.59 | 7,343.43 | 7,356.40 | 174.45 |
| Casas Norte | M2: completo (nivel) | 0.69 | 7,194.26 | 7,224.53 | 161.42 |
| Casas Norte | M3: completo (log) | 0.77 | 148.32 | 178.59 | 178.01 |
| Apartamentos Sur | M1: solo área | 0.58 | 27,646.61 | 27,663.72 | 116.82 |
| Apartamentos Sur | M2: completo (nivel) | 0.75 | 26,449.72 | 26,489.65 | 94.87 |
| Apartamentos Sur | M3: completo (log) | 0.81 | -376.27 | -336.33 | 115.99 |
Agregar estrato, habitaciones, parqueaderos y baños (M2) mejora
sustancialmente sobre el modelo de solo área (M1) en R², AIC y BIC, en
los dos segmentos — no es una mejora marginal. La comparación con M3
(log) requiere más cuidado: su AIC y BIC no son
comparables con los de M1/M2 porque modela
log(preciom), una variable respuesta en otra escala;
comparar directamente esos valores llevaría a una conclusión equivocada.
El criterio común entre escalas es el RMSE de validación, siempre en
pesos — y ahí M2 gana en los dos segmentos (161 vs. 178 en casas; 95
vs. 116 en apartamentos). M3 ajusta mejor su propia escala logarítmica,
pero al deshacer la transformación para volver a pesos, predice peor en
la práctica.
Por eso el modelo seleccionado es M2, en escala original,
para los dos segmentos: es el que mejor predice en la unidad
que le importa al negocio (millones de pesos) y sus coeficientes se leen
directamente en esa unidad, sin pasos adicionales de interpretación. Los
modelos modelo_norte y modelo_sur usados en el
resto del informe se reajustan sobre el 100% de los datos de cada
segmento — práctica estándar una vez elegida la especificación, para
maximizar la precisión de las estimaciones finales.
Antes de ver los números: una estimación puntual es el precio esperado más probable para una vivienda con exactamente el perfil solicitado. Un intervalo de predicción (95%) es el rango donde razonablemente caería el precio de una vivienda individual con ese perfil — más ancho que un intervalo de confianza para el promedio, porque suma la incertidumbre del modelo a la variabilidad natural entre viviendas parecidas. Dada la heterocedasticidad documentada arriba, estos intervalos son una referencia amplia, no un límite exacto.
nuevo1_e4 <- data.frame(areaconst = 200, estrato = 4, habitaciones = 4, parqueaderos = 1, banios = 2)
nuevo1_e5 <- data.frame(areaconst = 200, estrato = 5, habitaciones = 4, parqueaderos = 1, banios = 2)
nuevo2_e5 <- data.frame(areaconst = 300, estrato = 5, habitaciones = 5, parqueaderos = 3, banios = 3)
nuevo2_e6 <- data.frame(areaconst = 300, estrato = 6, habitaciones = 5, parqueaderos = 3, banios = 3)
pred1_e4 <- predict(modelo_norte, newdata = nuevo1_e4, interval = "prediction", level = 0.95)
pred1_e5 <- predict(modelo_norte, newdata = nuevo1_e5, interval = "prediction", level = 0.95)
pred2_e5 <- predict(modelo_sur, newdata = nuevo2_e5, interval = "prediction", level = 0.95)
pred2_e6 <- predict(modelo_sur, newdata = nuevo2_e6, interval = "prediction", level = 0.95)
preds <- data.frame(
Solicitud = c("1. Casa, Zona Norte", "1. Casa, Zona Norte", "2. Apto., Zona Sur", "2. Apto., Zona Sur"),
Escenario = c("Estrato 4", "Estrato 5", "Estrato 5", "Estrato 6"),
`Precio estimado` = c(pred1_e4[1], pred1_e5[1], pred2_e5[1], pred2_e6[1]),
`IC 95% inf.` = c(pred1_e4[2], pred1_e5[2], pred2_e5[2], pred2_e6[2]),
`IC 95% sup.` = c(pred1_e4[3], pred1_e5[3], pred2_e5[3], pred2_e6[3]),
Presupuesto = c(350, 350, 850, 850),
check.names = FALSE
)
preds %>% tabla(digits = 1)
| Solicitud | Escenario | Precio estimado | IC 95% inf. | IC 95% sup. | Presupuesto |
|---|---|---|---|---|---|
|
Estrato 4 | 328.2 | 26.7 | 629.8 | 350 |
|
Estrato 5 | 408.4 | 106.3 | 710.5 | 350 |
|
Estrato 5 | 689.9 | 505.2 | 874.6 | 850 |
|
Estrato 6 | 745.7 | 561.0 | 930.5 | 850 |
Casa, zona norte. En estrato 4, el precio esperado ($328 M) cabe dentro del crédito preaprobado, con margen de $22 M. En estrato 5, el precio esperado ($408 M) excede el presupuesto en $58 M — viable solo si el cliente aporta recursos adicionales o negocia el precio a la baja.
Apartamento, zona sur. Los dos escenarios quedan por debajo del presupuesto de $850 M, con margen de $160 M (estrato 5) y $104 M (estrato 6). Ambos son financieramente viables; la diferencia entre estratos ($56 M en promedio) es, simplemente, el efecto del estrato manteniendo todo lo demás constante.
Para cada solicitud filtramos las propiedades del segmento correspondiente que cumplen el estrato pedido y cuestan dentro del presupuesto. Sobre ese grupo calculamos un índice de ajuste al perfil (0 a 100) que penaliza la distancia relativa al área, las habitaciones, los baños y los parqueaderos solicitados, dando el doble de peso al área por ser la variable más asociada al precio:
\[\text{índice} = 100 - 100\times\big(0.4\,d_{\text{área}} + 0.2\,d_{\text{habitaciones}} + 0.2\,d_{\text{baños}} + 0.2\,d_{\text{parqueaderos}}\big)\]
donde cada \(d\) es la diferencia
absoluta frente al valor solicitado, como proporción de ese valor. Junto
al precio observado (preciom) mostramos el precio que el
modelo esperaría para esas características
(precio_estimado): cuando el observado es menor al
estimado, la propiedad está relativamente bien valorada frente al resto
del mercado.
target1 <- list(areaconst = 200, parqueaderos = 1, banios = 2, habitaciones = 4)
presupuesto1 <- 350
cand1 <- casas_norte %>% filter(estrato %in% c(4, 5), preciom <= presupuesto1)
cand1$precio_estimado <- predict(modelo_norte, newdata = cand1)
cand1 <- cand1 %>%
mutate(
d_area = abs(areaconst - target1$areaconst) / target1$areaconst,
d_hab = abs(habitaciones - target1$habitaciones) / target1$habitaciones,
d_ban = abs(banios - target1$banios) / target1$banios,
d_parq = abs(parqueaderos - target1$parqueaderos) / max(target1$parqueaderos, 1),
indice = 100 - 100 * (0.4*d_area + 0.2*d_hab + 0.2*d_ban + 0.2*d_parq)
) %>%
arrange(desc(indice))
cat("Propiedades dentro de presupuesto y estrato:", nrow(cand1), "\n")
## Propiedades dentro de presupuesto y estrato: 110
top1 <- cand1 %>%
transmute(id, barrio = tools::toTitleCase(trimws(barrio)), preciom, areaconst, habitaciones,
banios, parqueaderos, estrato, precio_estimado = round(precio_estimado,1),
indice = round(indice,1)) %>%
head(6)
top1 %>% tabla()
| id | barrio | preciom | areaconst | habitaciones | banios | parqueaderos | estrato | precio_estimado | indice |
|---|---|---|---|---|---|---|---|---|---|
| 3,586 | La Merced | 330 | 240 | 3 | 2 | 1 | 4 | 357.8 | 87.0 |
| 1,666 | Alamos | 275 | 120 | 4 | 2 | 1 | 4 | 257.6 | 84.0 |
| 1,352 | La Flora | 350 | 190 | 3 | 3 | 1 | 5 | 419.8 | 83.0 |
| 4,779 | Prados Del Norte | 280 | 140 | 3 | 2 | 1 | 5 | 349.5 | 83.0 |
| 94 | Zona Norte | 265 | 162 | 4 | 3 | 1 | 4 | 320.8 | 82.4 |
| 1,924 | Vipasa | 320 | 264 | 3 | 2 | 1 | 4 | 379.0 | 82.2 |
pal1 <- colorFactor(c("#999999", "#B2472B"), domain = c("otras", "top"))
cand1_map <- cand1 %>% mutate(grupo = ifelse(id %in% top1$id, "top", "otras"))
leaflet(cand1_map) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(~longitud, ~latitud, radius = ~ifelse(grupo=="top", 8, 4),
color = ~pal1(grupo), stroke = FALSE, fillOpacity = 0.8,
popup = ~paste0("<b>", tools::toTitleCase(trimws(barrio)), "</b><br>",
"Precio: $", format(preciom, big.mark=","), " M<br>",
"Área: ", areaconst, " m² | Hab: ", habitaciones,
" | Baños: ", banios, " | Parq: ", parqueaderos)) %>%
addLegend(position = "bottomright", colors = c("#B2472B","#999999"),
labels = c("Top 6 recomendadas","Otras dentro de presupuesto"))
target2 <- list(areaconst = 300, parqueaderos = 3, banios = 3, habitaciones = 5)
presupuesto2 <- 850
cand2 <- aptos_sur %>% filter(estrato %in% c(5, 6), preciom <= presupuesto2)
cand2$precio_estimado <- predict(modelo_sur, newdata = cand2)
cand2 <- cand2 %>%
mutate(
d_area = abs(areaconst - target2$areaconst) / target2$areaconst,
d_hab = abs(habitaciones - target2$habitaciones) / target2$habitaciones,
d_ban = abs(banios - target2$banios) / target2$banios,
d_parq = abs(parqueaderos - target2$parqueaderos) / max(target2$parqueaderos, 1),
indice = 100 - 100 * (0.4*d_area + 0.2*d_hab + 0.2*d_ban + 0.2*d_parq)
) %>%
arrange(desc(indice))
cat("Propiedades dentro de presupuesto y estrato:", nrow(cand2), "\n")
## Propiedades dentro de presupuesto y estrato: 1436
top2 <- cand2 %>%
transmute(id, barrio = tools::toTitleCase(trimws(barrio)), preciom, areaconst, habitaciones,
banios, parqueaderos, estrato, precio_estimado = round(precio_estimado,1),
indice = round(indice,1)) %>%
head(6)
top2 %>% tabla()
| id | barrio | preciom | areaconst | habitaciones | banios | parqueaderos | estrato | precio_estimado | indice |
|---|---|---|---|---|---|---|---|---|---|
| 6,175 | Capri | 350 | 270.00 | 4 | 3 | 3 | 5 | 669.6 | 92.0 |
| 7,680 | Pampa Linda | 450 | 267.00 | 3 | 3 | 3 | 5 | 691.6 | 87.6 |
| 6,205 | Capri | 350 | 260.00 | 3 | 3 | 3 | 5 | 680.6 | 86.7 |
| 7,512 | Seminario | 670 | 300.00 | 6 | 5 | 3 | 5 | 758.1 | 82.7 |
| 5,423 | Ciudad Jardín | 695 | 227.00 | 3 | 3 | 3 | 6 | 684.8 | 82.3 |
| 8,113 | Cuarto De Legua | 410 | 295.55 | 4 | 4 | 2 | 5 | 709.8 | 82.1 |
pal2 <- colorFactor(c("#999999", "#2C5F8A"), domain = c("otras", "top"))
cand2_map <- cand2 %>% mutate(grupo = ifelse(id %in% top2$id, "top", "otras"))
leaflet(cand2_map) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(~longitud, ~latitud, radius = ~ifelse(grupo=="top", 8, 4),
color = ~pal2(grupo), stroke = FALSE, fillOpacity = 0.8,
popup = ~paste0("<b>", tools::toTitleCase(trimws(barrio)), "</b><br>",
"Precio: $", format(preciom, big.mark=","), " M<br>",
"Área: ", areaconst, " m² | Hab: ", habitaciones,
" | Baños: ", banios, " | Parq: ", parqueaderos)) %>%
addLegend(position = "bottomright", colors = c("#2C5F8A","#999999"),
labels = c("Top 6 recomendadas","Otras dentro de presupuesto"))
En los dos casos, todas las propiedades listadas cumplen presupuesto y estrato por diseño del filtro; el índice solo ordena por cercanía al perfil solicitado. Son el punto de partida natural para la negociación — en particular las que tienen precio observado por debajo de su precio estimado, la mejor relación precio/valor del grupo.
Casa, zona norte. Buscar en estrato 4: el precio esperado ($328 M) cabe en el crédito preaprobado; estrato 5 lo excede en $58 M y solo es viable con recursos adicionales o negociación. Las propiedades de la sección anterior son el punto de partida.
Apartamento, zona sur. Ambos estratos son viables financieramente, con margen amplio. Se recomienda estrato 6 si el cliente prioriza ubicación y acabados — el presupuesto lo permite —, o estrato 5 si prefiere maximizar el margen de negociación. Como más habitaciones a igual área castigan el precio en este segmento, conviene priorizar entre las alternativas las de mejor relación área/habitación, no solo el conteo de cuartos.
En conjunto, área construida y estrato son las variables que más pesan en el precio en los dos segmentos. Parqueaderos y baños importan en apartamentos del sur pero no en casas del norte; habitaciones nunca es, por sí sola, un buen predictor, y en apartamentos del sur su efecto es incluso negativo a igualdad de área.
El principal riesgo a tener presente: los intervalos de predicción son amplios — reflejo de la heterocedasticidad y de un R² lejos de 100% — así que el precio estimado debe usarse como referencia de negociación, no como precio de cierre.
parqueaderos. Imputar
los NA como 0 es razonable pero no está confirmado con la fuente
original; si fuera incorrecto para una fracción relevante de casos, el
coeficiente de parqueaderos podría estar sesgado.barrio. Persisten
inconsistencias de formato que no afectan el modelo pero sí limitarían
un análisis de precio por barrio específico.Ninguna de estas limitaciones invalida las conclusiones del informe; delimitan la precisión con la que deben usarse las estimaciones para negociar y seleccionar propiedades.
Código reproducible de principio a fin (ya ejecutado en las secciones anteriores; aquí se presenta consolidado, sin volver a ejecutarse).
# ---- Librerías ----
library(dplyr); library(ggplot2); library(car); library(lmtest)
library(broom); library(kableExtra); library(plotly); library(leaflet)
library(paqueteMODELOS)
data("vivienda")
# ---- Depuración ----
vivienda_limpia <- vivienda %>%
filter(!is.na(zona), !is.na(tipo), !is.na(estrato), !is.na(preciom),
!is.na(areaconst), !is.na(habitaciones), !is.na(banios)) %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos)) %>%
filter(habitaciones > 0, banios > 0, areaconst > 0, preciom > 0) %>%
mutate(precio_m2 = preciom * 1e6 / areaconst) %>%
filter(precio_m2 >= 300000, precio_m2 <= 6500000)
casas_norte <- vivienda_limpia %>% filter(zona == "Zona Norte", tipo == "Casa")
aptos_sur <- vivienda_limpia %>% filter(zona == "Zona Sur", tipo == "Apartamento")
# ---- Modelos finales ----
modelo_norte <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = casas_norte)
modelo_sur <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = aptos_sur)
# ---- Diagnósticos ----
vif(modelo_norte); vif(modelo_sur)
bptest(modelo_norte); bptest(modelo_sur)
dwtest(modelo_norte); dwtest(modelo_sur)
shapiro.test(residuals(modelo_norte)); shapiro.test(residuals(modelo_sur))
# ---- Predicción ----
predict(modelo_norte, newdata = data.frame(areaconst=200, estrato=4, habitaciones=4,
parqueaderos=1, banios=2),
interval = "prediction", level = 0.95)
predict(modelo_sur, newdata = data.frame(areaconst=300, estrato=5, habitaciones=5,
parqueaderos=3, banios=3),
interval = "prediction", level = 0.95)
summary(modelo_norte)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = casas_norte)
##
## Residuals:
## Min 1Q Median 3Q Max
## -740.41 -76.11 -16.19 47.58 1034.55
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -242.18007 31.54533 -7.677 5.55e-14 ***
## areaconst 0.88334 0.04757 18.571 < 2e-16 ***
## estrato 80.12981 7.48814 10.701 < 2e-16 ***
## habitaciones 5.82813 4.69051 1.243 0.214
## parqueaderos -2.22186 4.20572 -0.528 0.597
## banios 26.07379 5.76958 4.519 7.30e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 153.3 on 692 degrees of freedom
## Multiple R-squared: 0.6754, Adjusted R-squared: 0.673
## F-statistic: 287.9 on 5 and 692 DF, p-value: < 2.2e-16
summary(modelo_sur)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = aptos_sur)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1338.70 -42.54 -1.45 36.28 919.19
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -209.57018 13.66564 -15.336 < 2e-16 ***
## areaconst 1.56492 0.05191 30.147 < 2e-16 ***
## estrato 55.82860 2.77237 20.138 < 2e-16 ***
## habitaciones -26.64897 3.49052 -7.635 3.1e-14 ***
## parqueaderos 47.26581 3.11313 15.183 < 2e-16 ***
## banios 47.43374 3.12130 15.197 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 93.6 on 2770 degrees of freedom
## Multiple R-squared: 0.76, Adjusted R-squared: 0.7596
## F-statistic: 1755 on 5 and 2770 DF, p-value: < 2.2e-16
sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 10 x64 (build 19045)
##
## 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 knitr_1.51
## [4] gridExtra_2.3 GGally_2.4.0 boot_1.3-32
## [7] leaflet_2.2.3 plotly_4.12.1 kableExtra_1.4.0
## [10] broom_1.0.12 lmtest_0.9-40 zoo_1.8-15
## [13] car_3.1-5 carData_3.0-6 ggplot2_4.0.2
## [16] dplyr_1.2.0
##
## 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 leaflet.providers_3.0.0
## [7] vctrs_0.7.1 tools_4.5.2 crosstalk_1.2.2
## [10] generics_0.1.4 tibble_3.3.1 pkgconfig_2.0.3
## [13] Matrix_1.7-4 data.table_1.18.2.1 checkmate_2.3.4
## [16] RColorBrewer_1.1-3 S7_0.2.1 lifecycle_1.0.5
## [19] compiler_4.5.2 farver_2.1.2 stringr_1.6.0
## [22] rapportools_1.2 textshaping_1.0.4 htmltools_0.5.9
## [25] sass_0.4.10 yaml_2.3.12 Formula_1.2-5
## [28] pillar_1.11.1 jquerylib_0.1.4 tidyr_1.3.2
## [31] MASS_7.3-65 cachem_1.1.0 magick_2.9.0
## [34] abind_1.4-8 nlme_3.1-168 ggstats_0.13.0
## [37] tidyselect_1.2.1 digest_0.6.39 stringi_1.8.7
## [40] reshape2_1.4.5 pander_0.6.6 purrr_1.2.1
## [43] splines_4.5.2 labeling_0.4.3 fastmap_1.2.0
## [46] grid_4.5.2 cli_3.6.6 magrittr_2.0.4
## [49] base64enc_0.1-6 withr_3.0.2 scales_1.4.0
## [52] backports_1.5.0 timechange_0.4.0 lubridate_1.9.5
## [55] rmarkdown_2.31 httr_1.4.9 matrixStats_1.5.0
## [58] otel_0.2.0 evaluate_1.0.5 tcltk_4.5.2
## [61] viridisLite_0.4.3 mgcv_1.9-3 rlang_1.3.0
## [64] Rcpp_1.1.1 glue_1.8.0 xml2_1.5.2
## [67] svglite_2.2.2 rstudioapi_0.18.0 jsonlite_2.0.0
## [70] plyr_1.8.9 R6_2.6.1 systemfonts_1.3.1