Para: María, gerente de C&A (Casas y
Apartamentos)
De: Nicolás Arciniegas Moreno (Asesor Business
Intelligence)
Asunto: Valoración de las dos viviendas solicitadas por
la compañía internacional
Resumen de resultados: La casa solicitada se estima en 326,1 millones en estrato 4 y 412,2 millones en estrato 5; el crédito de 350 millones permite concentrar la búsqueda en el primer escenario y se identificaron 34 ofertas que cumplen todas las condiciones. Para el apartamento del sur, las estimaciones se ubican entre 626,5 y 840 millones según el estrato y la especificación del modelo. El crédito de 850 millones es suficiente, pero solo dos ofertas cumplen todos los requisitos. Se recomienda avanzar con las cinco casas seleccionadas y presentar primero los dos apartamentos que coinciden exactamente con la segunda solicitud.
Resumen de informe ejecutivo: A continuación,
el informe ejecutivo presenta en cuatro páginas los principales
hallazgos del análisis, las estimaciones obtenidas para cada vivienda,
la comparación con los créditos disponibles y las recomendaciones para
la selección de ofertas. Los anexos reúnen el soporte técnico del
estudio, incluyendo la preparación y exploración de los datos, la
estimación e interpretación de los modelos, la validación de sus
supuestos, los análisis de sensibilidad, las predicciones detalladas y
los mapas interactivos de las alternativas recomendadas.
El análisis permite responder las dos solicitudes, pero con niveles de certeza diferentes. Para la casa de la zona norte, el crédito de 350 millones es suficiente si se prioriza el estrato 4. Para el apartamento de la zona sur, el crédito de 850 millones cubre el precio estimado, aunque la oferta que cumple todas las condiciones es muy escasa.
Se ajustó un modelo de regresión lineal múltiple para cada submercado con información de los últimos tres meses: 700 casas del norte y 2.777 apartamentos del sur. Los modelos relacionan el precio con el área construida, el estrato, las habitaciones, los parqueaderos y los baños. Explican el 65 % de la variación del precio en el norte y el 75 % en el sur. La depuración, la comparación de especificaciones y la validación de supuestos se presentan en los anexos 2 y 3.
| Solicitud | Precio estimado (millones) | Crédito | Ofertas que cumplen todo | Recomendación |
|---|---|---|---|---|
| Casa · zona norte | 326.1 en estrato 4; 412.2 en estrato 5 | 350 millones | 34 | Priorizar estrato 4 |
| Apartamento · zona sur | 626.5 a 840 en estratos 5 y 6 | 850 millones | 2 | Mantener crédito y flexibilizar requisitos |
La decisión es clara para la primera solicitud: concentrar la búsqueda en estrato 4 sin descartar ofertas puntuales de estrato 5 que estén dentro del presupuesto. En la segunda, el problema principal no es el crédito sino la disponibilidad. Solo dos apartamentos cumplen simultáneamente el área, el estrato, las habitaciones, los baños, los parqueaderos y el presupuesto.
Para una casa de 200 m², 4 habitaciones, 2 baños y 1 parqueadero, el modelo estima un precio de 326,1 millones en estrato 4 y 412,2 millones en estrato 5. El primer escenario deja un margen cercano a 24 millones; el segundo supera el crédito en unos 62 millones.
La recomendación es utilizar el estrato 4 como criterio principal de búsqueda. Sin embargo, conviene revisar las ofertas de estrato 5 que ya aparecen por debajo del valor esperado. La base contiene 34 casas que cumplen todos los requisitos, incluidas 22 de estrato 5 dentro del presupuesto. Esta diferencia no contradice el modelo: la predicción representa un valor típico, mientras que una oferta concreta puede apartarse de ese promedio.
| Barrio | Estrato | Precio (millones) | Área (m²) | Hab./Baños/Parq. | Margen (millones) |
|---|---|---|---|---|---|
| La Merced | 4 | 230 | 250 | 5 / 3 / 2 | 120 |
| Los Andes | 4 | 260 | 280 | 6 / 4 / 2 | 90 |
| San Vicente | 5 | 340 | 355 | 8 / 5 / 2 | 10 |
| Salomia | 4 | 350 | 350 | 5 / 4 / 1 | 0 |
| Vipasa | 5 | 350 | 346 | 4 / 2 / 1 | 0 |
Las cinco alternativas están ordenadas por precio por metro cuadrado. Entre ellas hay tres casas de estrato 4 y dos de estrato 5. La Merced presenta la mejor relación entre precio y área: 250 m² por 230 millones, con un margen de 120 millones frente al crédito. Los Andes también ofrece una holgura amplia, mientras que Salomia y Vipasa utilizan la totalidad del presupuesto.
Antes de formular una oferta se debe verificar en terreno la antigüedad, el estado de conservación y los acabados. Estas características no aparecen en la base y podrían explicar por qué algunas viviendas se ofrecen por debajo de la estimación. El mapa interactivo y la revisión detallada de la georreferenciación se encuentran en el Anexo 2.
Decisión recomendada: visitar las cinco opciones de la tabla, priorizando las tres de estrato 4. Las dos alternativas de estrato 5 deben mantenerse como oportunidades condicionadas a una inspección física satisfactoria.
Para un apartamento de 300 m², 5 habitaciones, 3 baños y 3 parqueaderos, los modelos estiman un valor de 626,5 a 690 millones en estrato 5 y de 728 a 840 millones en estrato 6. El crédito de 850 millones cubre ambos escenarios según las estimaciones puntuales, aunque en estrato 6 el margen puede reducirse a solo 10 millones.
La principal dificultad es la escasez de comparables. La vivienda solicitada se ubica en el percentil 99 del área construida: únicamente 31 de los 2.777 apartamentos del sur tienen al menos 300 m², y solo dos cumplen todos los requisitos dentro del crédito. Por esta razón se informa un rango y no una cifra única.
| Barrio | Estrato | Precio (millones) | Área (m²) | Hab./Baños/Parq. | Margen (millones) | Cumplimiento |
|---|---|---|---|---|---|---|
| Guadalupe | 5 | 730 | 573 | 5 / 8 / 3 | 120 | Cumple todo |
| Seminario | 5 | 670 | 300 | 6 / 5 / 3 | 180 | Cumple todo |
| El Ingenio | 5 | 650 | 600 | 5 / 4 / 2 | 200 | No cumple: 2 parqueadero(s) |
| Ciudadela Pasoancho | 5 | 650 | 275 | 5 / 5 / 2 | 200 | No cumple: área 275 m² y 2 parqueadero(s) |
| San Fernando | 5 | 350 | 258 | 5 / 4 / 2 | 500 | No cumple: área 258 m² y 2 parqueadero(s) |
Guadalupe y Seminario cumplen todas las condiciones. Las otras tres alternativas permiten visualizar el costo de flexibilizar la solicitud. Si el área mínima baja a 250 m² y se aceptan dos parqueaderos, la oferta aumenta de 2 a 7 apartamentos. Si también se acepta una casa, aparecen 61 opciones en la zona sur que cumplen las demás condiciones.
Decisión recomendada: presentar primero las dos coincidencias exactas. Si ninguna resulta adecuada después de la visita, consultar si la compañía prefiere reducir el área y los parqueaderos o cambiar el tipo de vivienda. El mapa y el análisis completo se encuentran en el Anexo 3.
Los modelos muestran asociaciones distintas en cada submercado. En las casas del norte, subir un estrato se asocia con un aumento promedio de 86 millones. En los apartamentos del sur, cada metro cuadrado adicional se asocia con un aumento de 1,42 millones, frente a 0,78 millones en las casas del norte. El número de baños también presenta una asociación positiva y significativa en ambos modelos.
El efecto de las habitaciones debe interpretarse con cautela. Al mantener constante el área, no es significativo en el norte y es negativo en el sur. Esto puede reflejar una distribución con espacios más pequeños, pero no demuestra que añadir habitaciones reduzca el precio. El parqueadero tiene una asociación positiva en el sur; en el norte, la conclusión cambia según el tratamiento de los datos faltantes.
| Orden | Acción | Propósito |
|---|---|---|
| 1 | Visitar las cinco casas recomendadas y comenzar por las tres de estrato 4. | Atender la solicitud 1 sin ampliar el crédito. |
| 2 | Verificar antigüedad, estado y acabados antes de negociar. | Validar los atributos que no registra la base. |
| 3 | Presentar las dos coincidencias exactas para la solicitud 2. | Confirmar si alguna satisface a la compañía. |
| 4 | Si ninguna es adecuada, acordar qué requisito puede flexibilizarse. | Ampliar la oferta de apartamentos del sur. |
Las cifras deben leerse como una referencia de negociación, no como un avalúo definitivo. La base contiene precios de oferta, carece de información sobre antigüedad y conservación, y presenta datos faltantes en parqueaderos. Además, la segunda valoración depende de pocos comparables. Los modelos identifican asociaciones y no permiten establecer relaciones causales.
Recomendación final: avanzar de inmediato con la búsqueda de la casa del norte y presentar la limitada disponibilidad del apartamento del sur antes de iniciar nuevas visitas. Los mapas interactivos, las pruebas estadísticas, los intervalos y la comparación de modelos quedan documentados en los anexos.
Fin del informe ejecutivo. Las páginas siguientes contienen los anexos técnicos exigidos por el enunciado.
Estimar el valor de mercado de las dos viviendas solicitadas y determinar si el crédito preaprobado alcanza en cada caso.
Los dos inmuebles solicitados pertenecen a submercados distintos (casa en el norte y apartamento en el sur). Un modelo único supondría que el metro cuadrado se valora de igual manera en ambos. Por tanto, se ajusta un modelo independiente para cada submercado, de acuerdo con los filtros definidos en los pasos 1 y 7 del enunciado.
El enunciado plantea siete pasos. La tabla indica qué evidencia responde a cada uno y en qué sección se encuentra, de modo que la trazabilidad con la actividad sea directa.
| Paso | Evidencia | Ubicación |
|---|---|---|
| 1 | Filtro de casas de la zona norte, tres primeros registros, tablas de verificación, mapa y discusión de la ubicación. | Anexo 2, paso 1 |
| 2 | Correlaciones del precio con las variables solicitadas, gráficos interactivos con plotly e interpretación del efecto de la zona. | Anexo 2, paso 2 |
| 3 | Modelo de regresión lineal múltiple, coeficientes, significancia, interpretación contextual, R² y propuestas de mejora. | Anexo 2, paso 3 |
| 4 | Pruebas y gráficos de diagnóstico, interpretación de los supuestos y sugerencias de tratamiento. | Anexo 2, paso 4 |
| 5 | Predicción para la primera solicitud, con estimación puntual e intervalos. | Anexo 2, paso 5 |
| 6 | Selección y análisis de cinco ofertas dentro del crédito, presentadas en un mapa interactivo. | Anexo 2, paso 6 |
| 7 | Repetición de los pasos 1 a 6 para el apartamento de la zona sur y el crédito de 850 millones. | Anexo 3 |
Adicionalmente, entre los pasos 3 y 4 de cada anexo se incluye una comparación de especificaciones alternativas que no exige el enunciado. Sirve para verificar que el modelo lineal solicitado es una elección defendible frente a versiones logarítmicas y frente a un tratamiento categórico del estrato.
Depuración. Se descartan registros sin información en las variables del modelo y valores estructuralmente imposibles (cero habitaciones o cero baños).
Imputación de parqueaderos: un supuesto operativo, no un hecho verificado. El campo falta en el 38,4 % de las casas del norte y en el 14,5 % de los apartamentos del sur. Se imputa con 0 apoyándose en que el valor cero nunca aparece registrado en la base, lo que sugiere que el anunciante omite el campo cuando no hay parqueadero. Esa interpretación no puede comprobarse con los datos disponibles: la omisión también podría deberse a descuido al publicar.
Dado el peso de la ausencia, la decisión se somete a una prueba de sensibilidad que reestima ambos modelos usando solo los casos completos. Los resultados están en el paso 4 de cada anexo, y su lectura obliga a matizar una de las conclusiones sobre las casas del norte.
Atípicos. No se excluyen antes de modelar. La solicitud 2 pide un apartamento de 300 m², y una exclusión previa por Mahalanobis podría retirar precisamente algunos de los apartamentos grandes que sirven como comparables. Por ello, las observaciones atípicas se revisan después del ajuste, mediante los diagnósticos de influencia del modelo.
Variables. Las cinco del enunciado.
zona no entra como predictora porque cada base contiene una
sola zona; su efecto se analiza comparando ambos submercados.
Estrato. Variable ordinal que el modelo del enunciado trata como cuantitativa, lo que supone que todos los saltos valen igual. Ese supuesto se contrasta en la comparación de modelos con un modelo que la incorpora como categórica.
Responde al punto 1 del enunciado.
El filtro se construye en dos etapas: primero la depuración general de la base y luego la selección del submercado.
| Paso | Registros | % de la base original |
|---|---|---|
Base original vivienda
|
8.322 | 100 % |
| Se eliminan registros sin datos en las variables del modelo | 8.319 | 99.96 % |
| Se eliminan habitaciones = 0 y baños = 0 | 8.243 | 99.05 % |
| Se imputan con 0 los 1.555 datos faltantes de parqueaderos | 8.243 | 99.05 % |
| Filtro: tipo = Casa y zona = Zona Norte | 700 | 8.41 % |
La base de trabajo queda con 700 casas. Los tres primeros registros confirman que el filtro operó correctamente:
| zona | tipo | barrio | estrato | preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|---|---|---|---|
| Zona Norte | Casa | acopi | 5 | 320 | 150 | 2 | 4 | 6 |
| Zona Norte | Casa | acopi | 5 | 780 | 380 | 2 | 3 | 3 |
| Zona Norte | Casa | acopi | 6 | 750 | 445 | 0 | 7 | 6 |
Dos tablas de verificación comprueban que no quedó ningún registro ajeno al filtro:
| Tipo | Registros |
|---|---|
| Casa | 700 |
| Zona | Registros |
|---|---|
| Zona Norte | 700 |
Ambas tablas tienen un solo nivel, de modo que la consulta es correcta. La distribución por estrato muestra que el submercado cubre los cuatro estratos, con predominio del 3 y el 5:
| Estrato | Casas | % del total | Precio mediano (millones) |
|---|---|---|---|
| 3 | 229 | 32.7 | 210 |
| 4 | 151 | 21.6 | 380 |
| 5 | 267 | 38.1 | 480 |
| 6 | 53 | 7.6 | 780 |
El mapa muestra las casas de la base sobre el territorio, junto con el resto de la oferta en gris.
El enunciado pide discutir este punto. La respuesta honesta es que no se puede verificar la pertenencia a una zona sin los polígonos oficiales de la división administrativa, que la base no incluye. Lo que sí puede documentarse es que existe solapamiento espacial entre las zonas y evidencia de imprecisión en la geocodificación.
| Zona | Registros | Latitud p05 | Latitud p95 | Longitud p05 | Longitud p95 |
|---|---|---|---|---|---|
| Zona Centro | 121 | 3.433 | 3.453 | -76.54 | -76.51 |
| Zona Norte | 1888 | 3.386 | 3.492 | -76.54 | -76.49 |
| Zona Oeste | 1188 | 3.420 | 3.459 | -76.56 | -76.53 |
| Zona Oriente | 345 | 3.398 | 3.462 | -76.54 | -76.48 |
| Zona Sur | 4701 | 3.345 | 3.450 | -76.55 | -76.51 |
El intervalo central de latitudes de la Zona Norte (3,386 – 3,492) y el de la Zona Sur (3,345 – 3,450) comparten el tramo 3,386 – 3,450, que concentra buena parte de la oferta de ambas. Es decir, las zonas no ocupan franjas separadas en los datos.
La evidencia más clara de imprecisión no depende de ningún límite administrativo: basta mirar cuánto se dispersan las coordenadas dentro de un mismo barrio.
| Barrio | Registros | Extensión (km) |
|---|---|---|
| El Ingenio | 202 | 19.0 |
| Valle Del Lili | 1009 | 19.0 |
| Zona Sur | 74 | 18.7 |
| Acopi | 148 | 18.6 |
| Ciudad Jardin | 537 | 18.5 |
| Pance | 409 | 18.1 |
Un barrio de Cali mide típicamente menos de kilómetro y medio de lado. Sin embargo, de los 87 barrios con 20 o más registros, 51 se extienden más de 10 km y la mediana del conjunto es de 10.7 km. Un solo barrio no puede abarcar esa superficie: las coordenadas no están localizando el inmueble con precisión.
Dos casos ilustran el origen del problema:
Implicación práctica. zona es una
etiqueta comercial declarada por quien publica el anuncio, mientras que
longitud y latitud provienen de geocodificar una dirección o un nombre
de barrio. Son dos fuentes independientes y ambas pueden contener
errores. Para definir el submercado se usa
zona, pues refleja cómo se posiciona el
inmueble en el mercado. Las coordenadas se emplean para visualizar y
localizar ofertas, con la advertencia de que su precisión es
limitada y de que no permiten auditar la etiqueta de zona.
Responde al punto 2 del enunciado.
La matriz de correlaciones cuantifica la relación lineal entre el precio y cada predictora:
| Variable | Correlación con el precio |
|---|---|
| Área construida | 0.730 |
| Estrato | 0.617 |
| Baños | 0.568 |
| Habitaciones | 0.375 |
| Parqueaderos | 0.333 |
El área construida es la variable más asociada al precio (0,730), seguida del estrato (0,617) y del número de baños (0,568). Las habitaciones (0,375) y los parqueaderos (0,333) muestran asociaciones notoriamente más débiles.
El siguiente gráfico interactivo permite explorar la relación entre área y precio, diferenciando por estrato:
Las cuatro rectas son crecientes y aproximadamente paralelas, lo que respalda la especificación aditiva del modelo: a igual área, subir de estrato desplaza el precio hacia arriba sin cambiar la pendiente. También se aprecia que la dispersión aumenta con el área, un primer indicio de heterocedasticidad que se confirmará en el diagnóstico de supuestos.
La progresión del precio por estrato no es uniforme: el salto del estrato 5 al 6 es mucho mayor que los anteriores, lo que anticipa el problema del tratamiento numérico del estrato que se aborda en la comparación de modelos.
Como la base contiene una sola zona, el efecto de esta variable se examina comparando todos los submercados de casas de la ciudad:
| Zona | Casas | Precio mediano (millones) | Área mediana (m²) | Precio/m² (millones) |
|---|---|---|---|---|
| Zona Sur | 1924 | 480 | 247 | 2.17 |
| Zona Oeste | 164 | 680 | 300 | 2.13 |
| Zona Norte | 700 | 390 | 240 | 1.67 |
| Zona Centro | 97 | 300 | 200 | 1.50 |
| Zona Oriente | 284 | 234 | 178 | 1.25 |
La zona importa y bastante: el precio por metro cuadrado de una casa varía de forma sustancial entre zonas. Esto justifica haber ajustado un modelo específico para el norte en lugar de uno común para toda la ciudad.
Responde al punto 3 del enunciado.
Se estima por mínimos cuadrados ordinarios el modelo que indica el enunciado:
\[\text{precio}_i = \beta_0 + \beta_1\text{área}_i + \beta_2\text{estrato}_i + \beta_3\text{habitaciones}_i + \beta_4\text{parqueaderos}_i + \beta_5\text{baños}_i + \varepsilon_i\]
| Término | Coeficiente | Error estándar | Estadístico t | Valor p | |
|---|---|---|---|---|---|
| Intercepto | -251.3563 | 32.3795 | -7.7628 | <0.0001 | *** |
| Área construida | 0.7838 | 0.0464 | 16.8833 | <0.0001 | *** |
| Estrato | 86.1369 | 7.6451 | 11.2670 | <0.0001 | *** |
| Habitaciones | 4.1154 | 4.8261 | 0.8527 | 0.39 | |
| Parqueaderos | -0.8209 | 4.3297 | -0.1896 | 0.85 | |
| Baños | 30.2362 | 5.9136 | 5.1130 | <0.0001 | *** |
| Indicador | Valor |
|---|---|
| R² | 0.654 |
| R² ajustado | 0.6515 |
| Error estándar residual (millones) | 158.1 |
| Estadístico F | 262.3 |
| Valor p del modelo | < 0,0001 |
| Observaciones | 700 |
Tres de las cinco predictoras resultan significativas al 5 %:
¿Son lógicos? Los tres coeficientes significativos son coherentes en signo y magnitud. En el caso de las habitaciones, la estimación no es estadísticamente diferente de cero, por lo que no hay evidencia de que su número modifique el precio esperado al mantener constantes las demás variables. El signo negativo de parqueaderos tampoco debe interpretarse: bajo la imputación con cero, su p = 0,85 indica que es indistinguible de cero. Más adelante se evalúa la sensibilidad de este resultado al tratamiento de los datos faltantes.
El R² de 0.654 indica que las cinco características explican el 65.4 % de la variabilidad del precio de las casas del norte. El 34.6 % restante queda sin explicar, lo cual es esperable: la base no contiene antigüedad de la construcción, estado de conservación, calidad de acabados, orientación, seguridad del sector ni cercanía a servicios, y todas ellas influyen en el precio de una casa.
En términos prácticos, con un error estándar residual de 158 millones, el modelo sirve bien para acotar un rango de negociación pero no para fijar un precio exacto.
Se estiman tres especificaciones alternativas y se comparan con los indicadores de la unidad:
| Modelo | Respuesta | RMSE validación cruzada (millones) | Diferencia frente a M1 |
|---|---|---|---|
| M1 · lineal (enunciado) | precio | 159.7 | 0 |
| M2 · log-lineal | log(precio) | 183.3 | +23.6 |
| M3 · log-log en área | log(precio) | 158.1 | -1.6 |
| M4 · estrato categórico | precio | 157.8 | -1.9 |
Por qué no se comparan los R². Dos de las
especificaciones modelan el logaritmo del precio y dos modelan el
precio. Sus R², AIC y BIC no están en la misma escala y
contrastarlos sería un error: un R² de 0,78 sobre
log(precio) no significa que el modelo prediga mejor el
precio. Por eso la comparación se hace con validación cruzada de
10 pliegues, calculando el error siempre sobre el precio
original y retransformando los modelos logarítmicos con el factor de
Duan.
Los tres modelos en escala de precio quedan prácticamente empatados: entre el mejor y el peor hay 1.9 millones de diferencia sobre un error de unos 160, es decir, alrededor del 1.2 %. No hay un ganador contundente en el norte. El único claramente peor es M2, cuya transformación logarítmica sin transformar el área penaliza la predicción.
M4 (estrato categórico) merece atención aparte, no por su error, similar al de M1, sino por lo que revela su interpretación:
| Término | Sobreprecio (millones) | Error estándar | Valor p |
|---|---|---|---|
| Estrato 4 vs. 3 | 73.9 | 17.83 | <0.0001 |
| Estrato 5 vs. 3 | 136.5 | 17.28 | <0.0001 |
| Estrato 6 vs. 3 | 324.4 | 27.38 | <0.0001 |
Los saltos no son uniformes: pasar de estrato 3 a 4 vale 74 millones, de 3 a 5 vale 136 y de 3 a 6 vale 324. El incremento del estrato 5 al 6 es de 188 millones, más del doble que cualquier salto anterior. El modelo lineal, al imponer un único coeficiente de 86 millones por escalón, subestima el estrato 6 y sobreestima los intermedios.
Al predecir con M3 hay que deshacer el logaritmo, y hacerlo con la exponencial sin más subestima el precio. Se aplica por eso el factor de corrección de Duan, calculado como el promedio de las exponenciales de los residuales; en esta base vale 1.0373.
Modelo seleccionado. M1 se conserva como el modelo solicitado por el enunciado. M3 y M4 se emplean como análisis de sensibilidad, no como sustitutos: en el norte los tres presentan errores de predicción muy similares y no existe un ganador contundente. M4 se incluye porque permite representar el estrato sin imponer incrementos iguales entre categorías; M3, porque reduce la asimetría del precio. La cercanía de sus predicciones aporta confianza al resultado.
Responde al punto 4 del enunciado.
| Supuesto | Prueba | Estadístico | Valor p | Conclusión |
|---|---|---|---|---|
| Normalidad | Shapiro-Wilk | 0.8272 | <0.0001 | Se rechaza normalidad |
| Normalidad | Anderson-Darling | 25.9830 | <0.0001 | Se rechaza normalidad |
| Varianza constante | Breusch-Pagan | 133.9000 | <0.0001 | Hay heterocedasticidad |
| Independencia | Durbin-Watson | 1.6349 | <0.0001 | No interpretable (corte transversal) |
| Atípicos | Residuales |r| > 3 | 16.0000 | No aplica | 2.29 % de la muestra |
| Balanceo | Puntos con h > 2p/n | 62.0000 | No aplica | 8.9 % de la muestra |
| Influencia | Distancia de Cook > 1 | 0.0000 | No aplica | Ninguna observación domina |
Linealidad y forma funcional. El gráfico de residuales y la comparación de especificaciones muestran que la forma lineal es una aproximación razonable, aunque simplifica algunas relaciones. M1, M3 y M4 presentan errores de validación similares, pero M4 confirma que el efecto del estrato no aumenta de manera uniforme. Por tanto, M1 se mantiene como modelo base, con la precaución de no interpretar el estrato como una relación estrictamente lineal.
Normalidad. Se rechaza con ambas pruebas. Las colas se apartan de la recta, sobre todo la superior: hay casas cuyo precio el modelo subestima. Con 700 observaciones el teorema del límite central hace que la distribución de los estimadores se aproxime a la normal, de modo que la no normalidad de los errores compromete sobre todo la exactitud de los intervalos de predicción individual.
Varianza constante. Se rechaza (Breusch-Pagan, p < 0,0001). El gráfico de residuales muestra un patrón de embudo: la dispersión crece con el precio estimado, algo habitual cuando los valores de los inmuebles son muy diferentes. La heterocedasticidad, por sí sola, no sesga los coeficientes de MCO si se mantiene la exogeneidad, pero sí afecta los errores estándar, las pruebas t y los intervalos de confianza. El tamaño de la muestra no corrige este problema.
Independencia. El Durbin-Watson de 1.635 se reporta en la tabla por convención, pero no permite concluir nada aquí: la prueba detecta correlación entre observaciones consecutivas y fue diseñada para series temporales. Estas son ofertas inmobiliarias sin orden temporal, de modo que el orden de las filas es arbitrario y el estadístico solo refleja cómo está ordenada la base.
La forma pertinente de evaluar la independencia en datos georreferenciados es la autocorrelación espacial de los residuales, mediante el índice I de Moran:
| Elemento | Valor |
|---|---|
| I de Moran observado | 0.0317 |
| Valor esperado bajo independencia | -0.0014 |
| Desviación estándar | 0.0057 |
| Valor p | <0.0001 |
El índice observado (0.0317) supera significativamente al esperado bajo independencia (p < 0,0001): los residuales están espacialmente correlacionados. Casas cercanas entre sí tienden a tener errores de predicción parecidos, lo que indica que falta en el modelo una variable de ubicación fina, como el barrio, que las cinco predictoras actuales no capturan. Es la confirmación estadística de la mejora sugerida al interpretar el modelo.
Observaciones extremas. Los 16 residuales mayores a 3 son el 2.29 % de la muestra. Ninguna observación supera una distancia de Cook de 1, así que ningún dato domina la estimación.
Multicolinealidad.
| Variable | VIF | Diagnóstico |
|---|---|---|
| Área construida | 1.673 | Sin problema |
| Estrato | 1.587 | Sin problema |
| Habitaciones | 1.850 | Sin problema |
| Parqueaderos | 1.230 | Sin problema |
| Baños | 2.108 | Sin problema |
Todos los VIF están por debajo de 2,2, muy lejos del umbral de 5. No hay evidencia de multicolinealidad, por lo que esta no explica la falta de significancia de habitaciones y parqueaderos. Sin embargo, el análisis de sensibilidad muestra que el resultado de parqueaderos depende del tratamiento de los datos faltantes.
No se exige corregir. Las medidas apropiadas, en orden de facilidad, serían:
El dato de parqueaderos falta en el 38.4 % de las casas del norte, una proporción alta como para dar por buena la imputación sin comprobarla. Se reestima el modelo usando solo los registros que sí reportan el campo y se comparan ambos resultados.
| Elemento | Imputando 0 | Valor p (imp.) | Casos completos | Valor p (c.c.) |
|---|---|---|---|---|
| Intercepto | -251.360 | <0.0001 | -231.5700 | <0.0001 |
| Área construida | 0.780 | <0.0001 | 0.6600 | <0.0001 |
| Estrato | 86.140 | <0.0001 | 78.3500 | <0.0001 |
| Habitaciones | 4.120 | 0.39 | 6.0700 | 0.3014 |
| Parqueaderos | -0.820 | 0.85 | 24.2700 | <0.0001 |
| Baños | 30.240 | <0.0001 | 22.4100 | 0.0044 |
| Predicción estrato 4 | 326.100 | No aplica | 308.1000 | No aplica |
| Predicción estrato 5 | 412.200 | No aplica | 386.5000 | No aplica |
| Observaciones | 700.000 | No aplica | 431.0000 | No aplica |
| R² | 0.654 | No aplica | 0.6022 | No aplica |
El resultado obliga a matizar una conclusión. Con imputación, el coeficiente de parqueaderos es −0,82 y no significativo; con casos completos pasa a +24,3 millones y altamente significativo. La explicación es directa: al asignar cero a 269 casas cuyo número de parqueaderos simplemente no fue registrado, se introduce ruido que diluye el efecto de la variable. Conviene ser preciso en la dirección contraria también: los casos completos tampoco permiten concluir que esas 269 viviendas tengan parqueadero. Lo único que puede afirmarse es que la conclusión sobre esta variable depende del tratamiento del faltante.
Las predicciones cambian de 326,1 a 308,1 millones en estrato 4 y de 412,2 a 386,5 millones en estrato 5. Ninguna de las dos versiones modifica la conclusión de negocio: el crédito alcanza en estrato 4 y no en estrato 5 bajo ambos supuestos. Sin embargo, la afirmación de que “el parqueadero no aporta valor en las casas del norte” no es sostenible, pues depende del tratamiento del dato faltante.
Se conserva la imputación con cero como especificación principal, por coherencia con el resto del análisis y porque preserva el 100 % de la muestra, dejando constancia de que es un supuesto y no un hecho.
Responde al punto 5 del enunciado.
Las características solicitadas son: casa en la zona norte, 200 m², 4 habitaciones, 2 baños, 1 parqueadero, estrato 4 o 5.
| Variable | Solicitado | Mínimo observado | Máximo observado | Percentil de la solicitud |
|---|---|---|---|---|
| Área construida | 200 | 30 | 1440 | 42 % |
| Parqueaderos | 1 | 0 | 10 | 61 % |
| Baños | 2 | 1 | 10 | 26 % |
| Habitaciones | 4 | 1 | 10 | 58 % |
Las cuatro características caen dentro del rango de la muestra, y el área solicitada se ubica en el percentil 42: es una casa de tamaño corriente para la zona. No hay extrapolación univariada, lo que es condición necesaria pero no suficiente: la precisión de la predicción sigue limitada por la dispersión residual, la heterocedasticidad documentada y el tratamiento de los datos faltantes de parqueaderos.
| Estrato | M1 lineal | IC 95 % de la media | IP 95 % individual | M4 categórico | M3 log-log |
|---|---|---|---|---|---|
| 4 | 326.1 | [307.6 ; 344.5] | [15.1 ; 637] | 325.6 | 322 |
| 5 | 412.2 | [386 ; 438.4] | [100.7 ; 723.7] | 388.2 | 394 |
Interpretación. Para el escenario de estrato 4, el modelo estima un precio esperado de 326.1 millones. El intervalo de confianza del 95 % para el precio medio de las casas con esas características va de 307.6 a 344.5 millones: es el rango donde se ubica el valor típico de mercado. Para el escenario de estrato 5, el precio esperado sube a 412.2 millones.
Los tres modelos coinciden estrechamente en el escenario de estrato 4, con estimaciones entre 322 y 326.1 millones. Esta coincidencia refuerza la confianza en la estimación.
Respuesta a María. Con un crédito de 350 millones, la operación es viable en estrato 4 pero no en estrato 5, donde el valor esperado supera el presupuesto en unos 62 millones. La recomendación es dirigir la búsqueda al estrato 4.
Nótese el contraste entre los dos intervalos: el de la media es estrecho (unos 37 millones) y el de predicción individual muy amplio. No es un defecto del cálculo sino la dispersión detectada en el diagnóstico de supuestos: el modelo predice bien el promedio del mercado, no una vivienda concreta.
Responde al punto 6 del enunciado.
Se seleccionan las ofertas disponibles dentro del presupuesto de 350 millones y en estratos 4 o 5. Primero se exige el cumplimiento de todas las características mínimas y luego se ordenan las ofertas por precio por metro cuadrado.
Criterio de selección. Una oferta entra a la lista solo si cumple simultáneamente todos los requisitos: presupuesto máximo de 350 millones, estrato 4 o 5, al menos 200 m², 4 habitaciones, 2 baños y 1 parqueadero. Entre las que cumplen, el orden lo da el precio por metro cuadrado, que mide la relación entre lo pagado y lo recibido.
| Filtro | Casas |
|---|---|
| Presupuesto (≤ 350 millones) y estrato 4 o 5 | 110 |
| Además, todos los mínimos de área, habitaciones, baños y parqueaderos | 34 |
De las 110 casas que caben en el presupuesto y el estrato, 34 cumplen además todas las características pedidas. De ellas, 22 son de estrato 5 y 12 de estrato 4.
| Barrio | Estrato | Precio (millones) | Área (m²) | Parq. | Baños | Hab. | Precio/m² (millones) | Margen (millones) |
|---|---|---|---|---|---|---|---|---|
| La Merced | 4 | 230 | 250 | 2 | 3 | 5 | 0.92 | 120 |
| Los Andes | 4 | 260 | 280 | 2 | 4 | 6 | 0.93 | 90 |
| San Vicente | 5 | 340 | 355 | 2 | 5 | 8 | 0.96 | 10 |
| Salomia | 4 | 350 | 350 | 1 | 4 | 5 | 1.00 | 0 |
| Vipasa | 5 | 350 | 346 | 1 | 2 | 4 | 1.01 | 0 |
De las 34 casas que cumplen todos los requisitos, estas cinco presentan el menor precio por metro cuadrado. Su ubicación se muestra a continuación:
Discusión. Todas las de la tabla superan lo solicitado en área y varias también en habitaciones y baños.
Un matiz importante sobre el estrato 5. El modelo estima que una casa de estas características en estrato 5 vale unos 412 millones, por encima del crédito. Sin embargo, 22 casas de estrato 5 se ofertan por debajo de los 350 millones cumpliendo todo lo pedido. No hay contradicción: el modelo predice el precio típico de la categoría, mientras que estas ofertas se encuentran por debajo de ese valor esperado. Precisamente por eso deben verificarse en terreno: la diferencia podría estar relacionada con antigüedad, estado de conservación, urgencia de venta u otros atributos que la base no registra.
Recomendación. La Merced y Los Andes por relación precio/área y margen financiero; San Vicente si la compañía prefiere estrato 5 y el mayor tamaño disponible.
Se replica íntegramente el procedimiento anterior sobre el segundo submercado.
Responde al punto 1 del enunciado.
| Paso | Registros |
|---|---|
| Base depurada general | 8.243 |
| Filtro: tipo = Apartamento y zona = Zona Sur | 2.777 |
La base queda con 2.777 apartamentos, cuatro veces el tamaño de la base anterior. Los tres primeros registros:
| zona | tipo | barrio | estrato | preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|---|---|---|---|
| Zona Sur | Apartamento | acopi | 4 | 290 | 96 | 1 | 2 | 3 |
| Zona Sur | Apartamento | aguablanca | 3 | 78 | 40 | 1 | 1 | 2 |
| Zona Sur | Apartamento | aguacatal | 6 | 875 | 194 | 2 | 5 | 3 |
Las tablas de verificación confirman de nuevo que el filtro es correcto:
| Tipo | Registros |
|---|---|
| Apartamento | 2777 |
| Zona | Registros |
|---|---|
| Zona Sur | 2777 |
| Estrato | Apartamentos | % del total | Precio mediano (millones) |
|---|---|---|---|
| 3 | 200 | 7.2 | 128 |
| 4 | 1086 | 39.1 | 188 |
| 5 | 1031 | 37.1 | 280 |
| 6 | 460 | 16.6 | 580 |
A diferencia del norte, aquí la oferta se concentra en los estratos 4 y 5, que juntos reúnen más de tres cuartas partes del total.
Aplica el mismo diagnóstico del anexo anterior, y con más intensidad. Valle del Lili, el barrio con más oferta del sur (1009 registros en toda la base), aparece con una extensión de 19 km entre sus puntos más alejados, cuando el barrio real no llega a dos kilómetros de lado.
| Barrio | Registros | Extensión (km) |
|---|---|---|
| El Ingenio | 202 | 19.0 |
| Valle Del Lili | 1009 | 19.0 |
| Ciudad Jardin | 537 | 18.5 |
| Pance | 409 | 18.1 |
| El Caney | 207 | 13.4 |
| La Hacienda | 165 | 13.3 |
De nuevo, no se afirma que estos inmuebles estén fuera de su
zona, pues eso exigiría los polígonos oficiales. La
geocodificación es demasiado imprecisa para auditar la etiqueta. El
criterio de filtro sigue siendo zona, y las coordenadas se
usan solo para visualizar.
Responde al punto 2 del enunciado.
| Variable | Correlación con el precio |
|---|---|
| Área construida | 0.757 |
| Baños | 0.733 |
| Parqueaderos | 0.682 |
| Estrato | 0.673 |
| Habitaciones | 0.344 |
La estructura de correlaciones difiere de la del norte en un punto relevante: aquí los baños (0,733) y los parqueaderos (0,682) presentan una asociación más fuerte con el precio. En las casas, la correlación de parqueaderos es de 0,333 y, además, su interpretación está limitada por la cantidad de datos faltantes. Esto sugiere que el parqueadero aporta mayor información para diferenciar los precios de los apartamentos del sur.
Las habitaciones (0,344) vuelven a ser la variable menos asociada al precio, replicando el patrón del norte.
El gráfico revela el problema central de esta solicitud: la nube se concentra por debajo de los 150 m² y se vuelve muy escasa a partir de los 250. La solicitud pide 300 m², una zona del gráfico donde apenas hay observaciones sobre las cuales apoyar la estimación.
De nuevo el salto del estrato 5 al 6 es desproporcionado frente a los anteriores.
| Zona | Apartamentos | Precio mediano (millones) | Área mediana (m²) | Precio/m² (millones) |
|---|---|---|---|---|
| Zona Oeste | 1024 | 570.0 | 150 | 3.84 |
| Zona Sur | 2777 | 245.0 | 85 | 2.90 |
| Zona Norte | 1188 | 245.5 | 83 | 2.66 |
| Zona Centro | 24 | 152.5 | 93 | 1.84 |
| Zona Oriente | 61 | 115.0 | 63 | 1.58 |
Responde al punto 3 del enunciado.
| Término | Coeficiente | Error estándar | Estadístico t | Valor p | |
|---|---|---|---|---|---|
| Intercepto | -216.272 | 13.8692 | -15.594 | <0.0001 | *** |
| Área construida | 1.424 | 0.0506 | 28.149 | <0.0001 | *** |
| Estrato | 56.099 | 2.8172 | 19.913 | <0.0001 | *** |
| Habitaciones | -24.170 | 3.5374 | -6.833 | <0.0001 | *** |
| Parqueaderos | 49.482 | 3.1549 | 15.684 | <0.0001 | *** |
| Baños | 50.947 | 3.1500 | 16.174 | <0.0001 | *** |
| Indicador | Valor |
|---|---|
| R² | 0.7521 |
| R² ajustado | 0.7517 |
| Error estándar residual (millones) | 95.1 |
| Estadístico F | 1681.5 |
| Valor p del modelo | < 0,0001 |
| Observaciones | 2777 |
A diferencia del modelo del norte, aquí las cinco predictoras son significativas al 0,1 %:
¿Es lógico el signo negativo? Puede serlo. No significa que las habitaciones reduzcan el valor por sí mismas. A igual superficie, una mayor cantidad de cuartos puede reflejar espacios más pequeños o una distribución asociada con otro segmento del mercado. Es una relación condicional del modelo, no una conclusión causal.
El efecto solo se ve en un modelo múltiple: la correlación simple con el precio es positiva (0,344) porque los apartamentos con más cuartos suelen ser más grandes. Controlada el área, el signo se invierte.
El R² de 0.752 supera al del modelo del norte (0.654). El submercado de apartamentos es más homogéneo y estandarizado que el de casas, de modo que las mismas cinco variables explican mejor su precio. El error estándar residual es de 95 millones, notablemente menor que los 158 del norte.
Las mismas cuatro vías de mejora señaladas para el modelo anterior aplican aquí, con una prioridad distinta: dado que el objetivo es predecir un apartamento de 300 m², lo más valioso sería enriquecer la muestra de apartamentos grandes, hoy insuficiente.
| Modelo | Respuesta | RMSE validación cruzada (millones) | Diferencia frente a M1 |
|---|---|---|---|
| M1 · lineal (enunciado) | precio | 97.8 | 0 |
| M2 · log-lineal | log(precio) | 129.8 | +32 |
| M3 · log-log en área | log(precio) | 82.2 | -15.6 |
| M4 · estrato categórico | precio | 90.8 | -7 |
Aquí el resultado sí es concluyente, a diferencia del norte. M3 (log-log en área) reduce el error de predicción de 97.8 a 82.2 millones, una mejora del 16 % sobre el modelo del enunciado. M4 (estrato categórico) también supera a M1, en 7 millones. El submercado de apartamentos es lo bastante grande y homogéneo como para que las mejoras de especificación se traduzcan en ganancia real de predicción.
| Término | Sobreprecio (millones) | Error estándar | Valor p |
|---|---|---|---|
| Estrato 4 vs. 3 | 17.5 | 6.96 | 0.012 |
| Estrato 5 vs. 3 | 36.9 | 7.28 | <0.0001 |
| Estrato 6 vs. 3 | 198.2 | 9.18 | <0.0001 |
La no linealidad es aún más extrema que en el norte: los estratos 4 y 5 se separan del 3 en apenas 17 y 37 millones, mientras que el 6 lo hace en 198. En apartamentos del sur el estrato casi no discrimina entre 3, 4 y 5; solo el salto al 6 marca una diferencia real. El modelo lineal, que impone 56 millones por escalón, distorsiona esa estructura, y de ahí la discrepancia entre M1 y M4 en la valoración.
Responde al punto 4 del enunciado.
| Supuesto | Prueba | Estadístico | Valor p | Conclusión |
|---|---|---|---|---|
| Normalidad | Shapiro-Wilk | 0.7675 | <0.0001 | Se rechaza normalidad |
| Normalidad | Anderson-Darling | 94.1150 | <0.0001 | Se rechaza normalidad |
| Varianza constante | Breusch-Pagan | 978.7000 | <0.0001 | Hay heterocedasticidad |
| Independencia | Durbin-Watson | 1.5048 | <0.0001 | No interpretable (corte transversal) |
| Atípicos | Residuales |r| > 3 | 35.0000 | No aplica | 1.26 % de la muestra |
| Balanceo | Puntos con h > 2p/n | 183.0000 | No aplica | 6.6 % de la muestra |
| Influencia | Distancia de Cook > 1 | 2.0000 | No aplica | Revisar observaciones |
El diagnóstico reproduce los principales problemas del modelo anterior, pero con mayor intensidad.
Linealidad y forma funcional. El patrón de los residuales, la mejora predictiva de M3 y el comportamiento del estrato en M4 indican que la forma lineal de M1 no representa por completo este submercado. La limitación es más visible en los apartamentos de mayor precio y en el estrato 6. Se recomienda evaluar el logaritmo del precio y tratar el estrato como variable categórica.
Independencia. Vale la misma advertencia: el Durbin-Watson no es interpretable en un corte transversal. La prueba pertinente arroja un I de Moran de 0.0573 frente a un valor esperado de -0.0011 (p < 0,0001), una autocorrelación espacial casi el doble de la observada en el norte. La dependencia geográfica no capturada es aquí más acusada.
Normalidad y varianza constante. Ambos supuestos se rechazan, y el estadístico de Breusch-Pagan (979) es siete veces mayor que en el modelo del norte. El embudo en el gráfico de residuales es muy pronunciado: para apartamentos de precio estimado bajo los residuales se agrupan estrechamente, mientras que por encima de los 600 millones se dispersan de forma considerable. Es justamente el rango donde cae la predicción de la solicitud 2, lo que refuerza la advertencia sobre su confiabilidad.
Observaciones influenciales. A diferencia del modelo anterior, aquí 2 observaciones superan una distancia de Cook de 1 (máximo: 8.12).
| Barrio | Estrato | Precio (millones) | Área (m²) | Parq. | Baños | Hab. | Distancia de Cook | |
|---|---|---|---|---|---|---|---|---|
| 2373 | Valle Del Lili | 5 | 299 | 932 | 1 | 3 | 3 | 8.12 |
| 973 | El Limonar | 5 | 170 | 605 | 1 | 2 | 2 | 1.31 |
| 2559 | Valle Del Lili | 4 | 190 | 50 | 10 | 2 | 4 | 0.33 |
Son apartamentos de gran formato, precisamente el tipo de inmueble que la solicitud 2 describe. Su influencia elevada es consecuencia directa de la escasez de comparables en ese rango: al haber tan pocos, cada uno pesa desproporcionadamente en la pendiente estimada.
| Variable | VIF | Diagnóstico |
|---|---|---|
| Área construida | 2.117 | Sin problema |
| Estrato | 1.725 | Sin problema |
| Habitaciones | 1.438 | Sin problema |
| Parqueaderos | 1.843 | Sin problema |
| Baños | 2.623 | Sin problema |
Tampoco hay evidencia de multicolinealidad: el VIF más alto es 2.62, correspondiente a los baños. Por tanto, el signo negativo de las habitaciones no parece originarse en una relación lineal excesiva entre las predictoras; aun así, debe interpretarse como una asociación condicionada por las demás variables.
Además de las medidas anteriores, este caso requiere ampliar la muestra de apartamentos grandes antes de realizar una valoración definitiva sobre 300 m². Solo 31 apartamentos alcanzan esa superficie. Como análisis complementario podría estimarse un modelo conjunto para viviendas del sur, incorporando el tipo de inmueble y sus interacciones; no obstante, este modelo no sustituiría la valoración específica de apartamentos.
| Elemento | Imputando 0 | Valor p (imp.) | Casos completos | Valor p (c.c.) |
|---|---|---|---|---|
| Intercepto | -216.2700 | <0.0001 | -255.8500 | <0.0001 |
| Área construida | 1.4200 | <0.0001 | 1.2900 | <0.0001 |
| Estrato | 56.1000 | <0.0001 | 60.2700 | <0.0001 |
| Habitaciones | -24.1700 | <0.0001 | -26.5800 | <0.0001 |
| Parqueaderos | 49.4800 | <0.0001 | 75.7300 | <0.0001 |
| Baños | 50.9500 | <0.0001 | 49.9500 | <0.0001 |
| Predicción estrato 5 | 671.9000 | No aplica | 676.9000 | No aplica |
| Predicción estrato 6 | 728.0000 | No aplica | 737.2000 | No aplica |
| Observaciones | 2777.0000 | No aplica | 2375.0000 | No aplica |
| R² | 0.7521 | No aplica | 0.7477 | No aplica |
Aquí el faltante es menor (14.5 %) y el modelo resulta mucho más estable: ningún coeficiente cambia de signo ni de significancia, y las predicciones se mueven menos de 10 millones. La conclusión de negocio se mantiene intacta bajo ambos supuestos. La fragilidad detectada en el norte no se replica en el sur.
Responde al punto 5 del enunciado.
Las características solicitadas son: apartamento en la zona sur, 300 m², 5 habitaciones, 3 baños, 3 parqueaderos, estrato 5 o 6.
| Variable | Solicitado | Mínimo observado | Máximo observado | Percentil de la solicitud |
|---|---|---|---|---|
| Área construida | 300 | 40 | 932 | 99 % |
| Parqueaderos | 3 | 0 | 10 | 99 % |
| Baños | 3 | 1 | 8 | 87 % |
| Habitaciones | 5 | 1 | 6 | 100 % |
Formalmente ningún valor excede el máximo observado, de modo que no hay extrapolación estricta. Pero el área solicitada se ubica en el percentil 99: apenas 31 de los 2.777 apartamentos alcanzan o superan los 300 m². Como advierte el material de la unidad, no basta con verificar que cada valor esté dentro de su rango individual; hay que evaluar si el punto pertenece a la región de observación conjunta:
| Condición | Apartamentos | % de la base |
|---|---|---|
| Área ≥ 300 m² | 31 | 1.12 % |
| Parqueaderos ≥ 3 | 111 | 4 % |
| Habitaciones ≥ 5 | 28 | 1.01 % |
| Baños ≥ 3 | 1025 | 36.91 % |
| Todas las condiciones simultáneamente | 6 | 0.22 % |
Solo 6 apartamentos de toda la zona sur cumplen simultáneamente esas condiciones físicas, antes de aplicar estrato y presupuesto. El producto solicitado es muy escaso en el mercado observado, y esta es la principal limitación del análisis.
| Estrato | M1 lineal | IC 95 % de la media | IP 95 % individual | M4 categórico | M3 log-log |
|---|---|---|---|---|---|
| 5 | 671.9 | [651 ; 692.8] | [484.2 ; 859.6] | 626.5 | 690 |
| 6 | 728.0 | [706.9 ; 749.1] | [540.3 ; 915.7] | 787.7 | 840 |
Interpretación. En el escenario de estrato 5 los tres modelos estiman entre 626.5 y 690 millones. En estrato 6 el rango se amplía a 728 – 840 millones.
La discrepancia entre modelos, de hasta 112 millones en estrato 6, es un resultado que debe reportarse. Su origen es el tratamiento del estrato: como se vio en la comparación de modelos, M1 asigna 56 millones por escalón mientras que M4 reconoce que el salto al estrato 6 vale 198. Cuando los modelos coinciden, la estimación es robusta; cuando divergen, como aquí, la incertidumbre debe comunicarse.
Respuesta a María. El crédito de 850 millones alcanza en los dos escenarios de estrato, pero el margen en estrato 6 es estrecho: entre 10 y 122 millones según el modelo. Más relevante que el precio es la disponibilidad: el inmueble descrito casi no existe en la oferta del sur.
Responde al punto 6 del enunciado.
Criterio de selección. El mismo del anexo anterior, con los requisitos de esta solicitud: 850 millones, estrato 5 o 6, al menos 300 m², 5 habitaciones, 3 baños y 3 parqueaderos.
| Filtro | Apartamentos |
|---|---|
| Presupuesto (≤ 850 millones) y estrato 5 o 6 | 1437 |
| Además, todos los mínimos de la solicitud | 2 |
| Relajando el área a 250 m² y los parqueaderos a 2 | 7 |
El contraste con el norte es contundente: de 1.437 apartamentos que caben en presupuesto y estrato, solo 2 cumplen todas las características. Por eso la tabla combina esos dos con tres alternativas de compromiso, señalando en cada una qué requisito no satisface.
| Barrio | Estrato | Precio (millones) | Área (m²) | Parq. | Baños | Hab. | Precio/m² (millones) | Margen (millones) | Cumplimiento |
|---|---|---|---|---|---|---|---|---|---|
| Guadalupe | 5 | 730 | 573 | 3 | 8 | 5 | 1.27 | 120 | Cumple todo |
| Seminario | 5 | 670 | 300 | 3 | 5 | 6 | 2.23 | 180 | Cumple todo |
| El Ingenio | 5 | 650 | 600 | 2 | 4 | 5 | 1.08 | 200 | No cumple: 2 parqueadero(s) |
| Ciudadela Pasoancho | 5 | 650 | 275 | 2 | 5 | 5 | 2.36 | 200 | No cumple: área 275 m² y 2 parqueadero(s) |
| San Fernando | 5 | 350 | 258 | 2 | 4 | 5 | 1.36 | 500 | No cumple: área 258 m² y 2 parqueadero(s) |
Los dos primeros apartamentos cumplen integralmente la solicitud. Las tres alternativas restantes se incluyen como opciones de compromiso y señalan de forma explícita los requisitos que no satisfacen. Su ubicación se presenta en el siguiente mapa:
Discusión.
Ninguna de las cinco es de estrato 6: en ese estrato la oferta de gran formato dentro del crédito es aún más escasa.
Recomendación. Presentar Seminario y Guadalupe como las opciones que satisfacen íntegramente el requerimiento, y las tres restantes solo si la compañía acepta ceder en área o parqueaderos. La escasez, no el presupuesto, sigue siendo la restricción.
Alternativas para ampliar la búsqueda. El presupuesto no es la principal restricción; lo es la escasez de apartamentos que reúnan todas las características. Se evalúan dos ajustes concretos sobre la oferta disponible:
| Escenario | Apartamentos disponibles | Aumento frente a la solicitud |
|---|---|---|
| Solicitud completa | 2 | Base |
| Solo área mínima de 250 m² | 3 | +1 |
| Solo 2 parqueaderos | 3 | +1 |
| Área mínima de 250 m² y 2 parqueaderos | 7 | +5 |
Flexibilización viable. Reducir solo el área o solo los parqueaderos aumenta las opciones de 2 a 3. En cambio, flexibilizar simultáneamente el área mínima a 250 m² y los parqueaderos a 2 aumenta la oferta a 7 apartamentos. Las cinco opciones de la tabla anterior hacen visible este intercambio.
Cambio de tipología. Si la compañía acepta una casa, existen 61 ofertas en la zona sur que cumplen todas las demás condiciones, incluido el presupuesto. Esta alternativa reduce considerablemente el problema de disponibilidad.
Las casas seleccionadas se ubican en La Merced, Los Andes, San Vicente, Salomia, Vipasa. Los apartamentos y las opciones de compromiso se encuentran en Guadalupe, Seminario, El Ingenio, Ciudadela Pasoancho, San Fernando. Ambos conjuntos corresponden a sectores residenciales de las zonas declaradas en la base.
1. Los dos submercados se comportan de forma distinta y justifican modelos separados. El coeficiente del área es de 1,42 millones por m² para los apartamentos del sur y de 0,78 millones para las casas del norte. Además, la relevancia de los atributos cambia. Bajo la imputación con cero, el parqueadero no resulta significativo en el norte, mientras que en el sur se asocia con un aumento de 49 millones. En el norte, esta conclusión cambia al utilizar casos completos.
2. El área, el estrato y el número de baños son los determinantes principales del precio, y son las tres variables significativas en los dos modelos simultáneamente. Las habitaciones solo resultan significativas en el sur, con signo negativo.
En cuanto a los parqueaderos, la conclusión depende del tratamiento de los datos faltantes: bajo la imputación con cero no son significativos en el norte, pero con casos completos su efecto es positivo (+24,3 millones) y altamente significativo. En el sur son significativos bajo ambos supuestos.
3. El número de habitaciones no presenta una asociación positiva al controlar por el área. En las casas del norte no es significativo y en los apartamentos del sur presenta una asociación negativa de 24 millones por habitación. Este resultado solo aparece en el modelo múltiple, pues la correlación simple es positiva en ambos casos.
4. El estrato no actúa de forma lineal. Tratado como categórico, el salto al estrato 6 vale 188 millones en el norte y 161 en el sur por encima del escalón anterior, muy por encima de lo que supone el modelo lineal. Es la principal fuente de discrepancia entre las predicciones.
5. Ambos modelos incumplen los supuestos de normalidad y varianza constante, con una dispersión que aumenta con el precio. El tamaño muestral reduce la sensibilidad a la falta de normalidad, pero no corrige la heterocedasticidad. Por ello, las pruebas t y los intervalos convencionales deben interpretarse con cautela o recalcularse con errores estándar robustos HC3. La heterocedasticidad, por sí sola, no sesga los coeficientes de MCO si se mantiene la exogeneidad.
6. La confiabilidad de las dos respuestas es muy distinta. En la solicitud 1 los tres modelos coinciden dentro de 4 millones en el escenario de estrato 4, aunque en estrato 5 la discrepancia sube a unos 24 millones. En la solicitud 2, que se ubica en el percentil 99 del área, los modelos difieren hasta en 112 millones en estrato 6.
Todo el código que genera este informe se reúne aquí, en el mismo orden en que se ejecuta, con la etiqueta de cada fragmento.
knitr::opts_chunk$set(
echo = FALSE, warning = FALSE, message = FALSE,
fig.align = "center", fig.width = 9, fig.height = 5.5,
dpi = 110, out.width = "100%"
)
options(scipen = 999, digits = 4)
set.seed(2024)
# Numeración automática de tablas y figuras
.tab_n <- 0
tabnum <- function(texto) { .tab_n <<- .tab_n + 1; paste0("Tabla ", .tab_n, ". ", texto) }
# ---------------------------------------------------------------------
# REQUISITO PREVIO: paqueteMODELOS no está en CRAN.
# install.packages("remotes")
# remotes::install_github("centromagis/paqueteMODELOS", force = TRUE)
# ---------------------------------------------------------------------
library(dplyr) # manipulación
library(tidyr) # reestructuración
library(ggplot2) # gráficos
library(plotly) # gráficos interactivos
library(broom) # salidas ordenadas de los modelos
library(car) # factores de inflación de varianza
library(lmtest) # Breusch-Pagan, Durbin-Watson
library(nortest) # Anderson-Darling
library(leaflet) # mapas interactivos
library(DT) # tablas interactivas
library(knitr) # kable
library(kableExtra) # formato de tablas
library(patchwork) # composición de gráficos
library(stringi) # normalización de texto
library(sf) # límite municipal
library(ape) # I de Moran (autocorrelación espacial)
tema_informe <- theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", size = 13),
plot.subtitle = element_text(color = "grey35"),
legend.position = "bottom",
panel.grid.minor = element_blank())
theme_set(tema_informe)
# Paleta categórica validada: banda de luminosidad, cromatismo, separación para
# daltonismo (ΔE 12,1 peor par) y contraste >= 3:1 contra la superficie.
AZUL <- "#2166ac"; VERDE <- "#158f80"; NARANJA <- "#bf6a11"; ROJO <- "#b2182b"
PAL4 <- c(AZUL, VERDE, NARANJA, ROJO)
# Colores de estado, reservados: acompañan siempre a una etiqueta de texto.
OK_ <- "#1a7f37"; NO_ <- "#b2182b"
GRIS <- "#5c6670"
# Vía principal: paquete del curso. Respaldo: archivo del repositorio.
if (requireNamespace("paqueteMODELOS", quietly = TRUE)) {
library(paqueteMODELOS); data("vivienda")
} else {
url_rda <- paste0("https://raw.githubusercontent.com/centromagis/",
"paqueteMODELOS/main/data/vivienda.rda")
archivo <- tempfile(fileext = ".rda")
download.file(url_rda, archivo, mode = "wb", quiet = TRUE)
load(archivo)
}
viv <- as.data.frame(vivienda)
normalizar_texto <- function(x) {
x <- enc2utf8(as.character(x))
mojibake <- c("\u221a\u00b0" = "a", "\u221a\u00a9" = "e", "\u221a\u2260" = "i",
"\u221a\u00b3" = "o", "\u221a\u222b" = "u", "\u221a\u00b1" = "n")
for (k in names(mojibake)) x <- stri_replace_all_fixed(x, k, mojibake[[k]])
x <- stri_trans_general(x, "Latin-ASCII")
x <- tolower(trimws(x)); x <- gsub("[^a-z0-9 ]", " ", x)
trimws(gsub("\\s+", " ", x))
}
vars_mod <- c("areaconst", "estrato", "habitaciones", "parqueaderos", "banios")
base <- viv %>%
filter(rowSums(is.na(.)) < ncol(.)) %>%
filter(!is.na(preciom), !is.na(areaconst), !is.na(estrato), !is.na(zona),
!is.na(tipo), !is.na(banios), !is.na(habitaciones),
!is.na(longitud), !is.na(latitud)) %>%
filter(habitaciones > 0, banios > 0) %>%
mutate(parq_original = parqueaderos,
parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos),
barrio = normalizar_texto(barrio),
precio_m2 = preciom * 1e6 / areaconst)
# Bases con el faltante de parqueaderos aún sin imputar, para la prueba de sensibilidad
base_cruda <- base %>% mutate(parqueaderos = parq_original)
base1_cruda <- base_cruda %>% filter(tipo == "Casa", zona == "Zona Norte")
base2_cruda <- base_cruda %>% filter(tipo == "Apartamento", zona == "Zona Sur")
base1 <- base %>% filter(tipo == "Casa", zona == "Zona Norte")
base2 <- base %>% filter(tipo == "Apartamento", zona == "Zona Sur")
# Límite municipal de Cali. Se busca en varias rutas posibles; si el archivo
# no aparece en ninguna, los mapas se dibujan igual, sin contorno.
RUTAS_LIMITE <- c(
"Z:/CALI.shp",
"Z:/cali.shp",
"Z:/00_Maestria_DataScience/2do_Semestre/Estadistica/u_1/cali.shp",
"cali.shp", "CALI.shp"
)
ruta_limite <- RUTAS_LIMITE[file.exists(RUTAS_LIMITE)][1]
limite_cali <- NULL
if (!is.na(ruta_limite)) {
limite_cali <- tryCatch({
x <- sf::st_read(ruta_limite, quiet = TRUE)
if (is.na(sf::st_crs(x))) sf::st_crs(x) <- 4326
sf::st_transform(sf::st_zm(x, drop = TRUE), 4326)
}, error = function(e) NULL)
}
hay_limite <- !is.null(limite_cali)
# ---- funciones reutilizables para no duplicar código entre las dos partes ----
titulo_barrio <- function(x) gsub("\\b([a-z])", "\\U\\1", x, perl = TRUE)
tabla_kable <- function(x, cap, ...) {
kable(x, caption = tabnum(cap), ...) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
}
ajustar_modelos <- function(d) {
list(
M1 = lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = d),
M2 = lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = d),
M3 = lm(log(preciom) ~ log(areaconst) + estrato + habitaciones + parqueaderos + banios, data = d),
M4 = lm(preciom ~ areaconst + factor(estrato) + habitaciones + parqueaderos + banios, data = d)
)
}
# Validación cruzada de 10 pliegues. Todas las métricas se calculan sobre el
# PRECIO original: los modelos en logaritmo se retransforman con el factor de
# Duan. Es la única forma de comparar especificaciones con distinta respuesta.
ESPECIFICACIONES <- list(
"M1 · lineal (enunciado)" = list(f = preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, log = FALSE),
"M2 · log-lineal" = list(f = log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, log = TRUE),
"M3 · log-log en área" = list(f = log(preciom) ~ log(areaconst) + estrato + habitaciones + parqueaderos + banios, log = TRUE),
"M4 · estrato categórico" = list(f = preciom ~ areaconst + factor(estrato) + habitaciones + parqueaderos + banios, log = FALSE))
cv_rmse <- function(d, K = 10) {
set.seed(2024); pliegue <- sample(rep(1:K, length.out = nrow(d)))
sapply(ESPECIFICACIONES, function(sp) {
err <- c()
for (k in 1:K) {
m <- lm(sp$f, d[pliegue != k, ])
pr <- predict(m, d[pliegue == k, ])
if (sp$log) pr <- exp(pr) * mean(exp(residuals(m))) # corrección de Duan
err <- c(err, (d$preciom[pliegue == k] - pr)^2)
}
sqrt(mean(err, na.rm = TRUE))
})
}
tabla_comparacion <- function(ms, d) {
cv <- cv_rmse(d)
data.frame(
Modelo = names(ESPECIFICACIONES),
`Respuesta` = c("precio", "log(precio)", "log(precio)", "precio"),
`RMSE validación cruzada (millones)` = round(as.numeric(cv), 1),
`Diferencia frente a M1` = paste0(ifelse(as.numeric(cv) - cv[[1]] > 0, "+", ""),
round(as.numeric(cv) - cv[[1]], 1)),
check.names = FALSE, row.names = NULL)
}
tabla_supuestos <- function(m) {
r <- rstudent(m); n <- length(r); p <- length(coef(m))
sw <- shapiro.test(if (n > 5000) sample(r, 5000) else r)
ad <- ad.test(r); bp <- bptest(m); dw <- dwtest(m)
data.frame(
Supuesto = c("Normalidad", "Normalidad", "Varianza constante",
"Independencia", "Atípicos", "Balanceo", "Influencia"),
Prueba = c("Shapiro-Wilk", "Anderson-Darling", "Breusch-Pagan",
"Durbin-Watson", "Residuales |r| > 3",
"Puntos con h > 2p/n", "Distancia de Cook > 1"),
Estadístico = c(round(sw$statistic, 4), round(ad$statistic, 3),
round(bp$statistic, 1), round(dw$statistic, 4),
sum(abs(r) > 3), sum(hatvalues(m) > 2*p/n),
sum(cooks.distance(m) > 1)),
`Valor p` = c(format.pval(sw$p.value, eps = .0001), format.pval(ad$p.value, eps = .0001),
format.pval(bp$p.value, eps = .0001), format.pval(dw$p.value, eps = .0001),
"No aplica", "No aplica", "No aplica"),
Conclusión = c(ifelse(sw$p.value < .05, "Se rechaza normalidad", "No se rechaza"),
ifelse(ad$p.value < .05, "Se rechaza normalidad", "No se rechaza"),
ifelse(bp$p.value < .05, "Hay heterocedasticidad", "Varianza constante"),
ifelse(dw$p.value < .05, "No interpretable (corte transversal)", "Sin autocorrelación"),
paste0(round(100*mean(abs(r) > 3), 2), " % de la muestra"),
paste0(round(100*mean(hatvalues(m) > 2*p/n), 1), " % de la muestra"),
ifelse(sum(cooks.distance(m) > 1) == 0, "Ninguna observación domina",
"Revisar observaciones")),
check.names = FALSE, row.names = NULL)
}
graficos_residuales <- function(m, titulo) {
d <- data.frame(ajustado = fitted(m), r = rstudent(m))
g1 <- ggplot(d, aes(ajustado, r)) +
geom_point(alpha = .3, size = .8, color = AZUL) +
geom_hline(yintercept = c(-3, 0, 3), linetype = c(2,1,2), color = c(ROJO,"grey40",ROJO)) +
geom_smooth(se = FALSE, color = ROJO, linewidth = .7, method = "loess", formula = y ~ x) +
labs(title = "Residuales frente a valores ajustados", x = "Valor ajustado (millones)",
y = "Residual estudentizado")
g2 <- ggplot(d, aes(sample = r)) + stat_qq(alpha = .3, size = .8, color = AZUL) +
stat_qq_line(color = ROJO) +
labs(title = "Gráfico de probabilidad normal", x = "Cuantiles teóricos", y = "Cuantiles observados")
(g1 + g2) + plot_annotation(title = titulo,
theme = theme(plot.title = element_text(face = "bold", size = 13)))
}
mapa_ofertas <- function(cand, d, titulo_col) {
if (!knitr::is_html_output()) {
p <- ggplot() +
geom_point(data = d, aes(longitud, latitud), color = "grey70",
size = .7, alpha = .35) +
geom_point(data = cand, aes(longitud, latitud), color = ROJO,
size = 2.8) +
geom_text(data = cand,
aes(longitud, latitud,
label = paste0(titulo_barrio(barrio), "\n", preciom, " M")),
size = 2.7, vjust = -.7, check_overlap = TRUE) +
labs(title = "Ubicación de las ofertas sugeridas",
subtitle = "En gris, el resto de la oferta del submercado",
x = "Longitud", y = "Latitud") +
theme(panel.grid.minor = element_blank())
if (hay_limite) {
p <- ggplot() +
geom_sf(data = limite_cali, fill = NA, color = "grey45", linewidth = .5) +
geom_point(data = d, aes(longitud, latitud), color = "grey70",
size = .7, alpha = .35) +
geom_point(data = cand, aes(longitud, latitud), color = ROJO,
size = 2.8) +
geom_text(data = cand,
aes(longitud, latitud,
label = paste0(titulo_barrio(barrio), "\n", preciom, " M")),
size = 2.7, vjust = -.7, check_overlap = TRUE) +
labs(title = "Ubicación de las ofertas sugeridas",
subtitle = "En gris, el resto de la oferta del submercado",
x = "Longitud", y = "Latitud") +
theme(panel.grid.minor = element_blank())
}
return(p)
}
pal <- colorNumeric(c("#2c7bb6", "#ffffbf", "#d7191c"), domain = range(d$preciom))
m <- leaflet() %>%
addProviderTiles(providers$OpenStreetMap.Mapnik) %>%
addCircleMarkers(data = d, ~longitud, ~latitud, radius = 3, stroke = FALSE,
fillOpacity = .25, color = "grey40", group = "Toda la oferta",
popup = ~paste0(barrio, "<br>", preciom, " millones · ", areaconst, " m²")) %>%
addCircleMarkers(data = cand, ~longitud, ~latitud, radius = 9, stroke = TRUE,
weight = 2, color = "black", fillColor = ROJO, fillOpacity = .9,
group = "Ofertas sugeridas",
label = ~paste0(titulo_barrio(barrio), " · ", preciom, " M"),
popup = ~paste0("<b>", titulo_barrio(barrio), "</b><br>",
"<b>Precio:</b> ", preciom, " millones<br>",
"<b>Área:</b> ", areaconst, " m²<br>",
"<b>Estrato:</b> ", estrato, "<br>",
"<b>Hab./Baños/Parq.:</b> ", habitaciones, " / ",
banios, " / ", parqueaderos))
if (hay_limite) m <- m %>% addPolygons(data = limite_cali, fill = FALSE,
color = "#1a1a1a", weight = 2, opacity = .7,
group = "Límite municipal")
m %>% addLayersControl(
overlayGroups = c("Toda la oferta", "Ofertas sugeridas",
if (hay_limite) "Límite municipal"),
options = layersControlOptions(collapsed = FALSE))
}
ms1 <- ajustar_modelos(base1)
ms2 <- ajustar_modelos(base2)
m1 <- ms1$M1; m2 <- ms2$M1 # modelos del enunciado
duan1 <- mean(exp(residuals(ms1$M3)))
duan2 <- mean(exp(residuals(ms2$M3)))
pred_solicitud <- function(ms, duan, nueva) {
do.call(rbind, lapply(seq_len(nrow(nueva)), function(i) {
x <- nueva[i, , drop = FALSE]
p1 <- predict(ms$M1, x, interval = "prediction")
c1 <- predict(ms$M1, x, interval = "confidence")
p4 <- predict(ms$M4, x, interval = "prediction")
p3 <- exp(predict(ms$M3, x, interval = "prediction")) * duan
data.frame(Estrato = x$estrato,
`M1 lineal` = round(p1[1], 1),
`IC 95 % de la media` = paste0("[", round(c1[2],1), " ; ", round(c1[3],1), "]"),
`IP 95 % individual` = paste0("[", round(p1[2],1), " ; ", round(p1[3],1), "]"),
`M4 categórico` = round(p4[1], 1),
`M3 log-log` = round(p3[1], 1),
check.names = FALSE)
}))
}
sol1 <- data.frame(areaconst = 200, parqueaderos = 1, banios = 2, habitaciones = 4, estrato = c(4L, 5L))
sol2 <- data.frame(areaconst = 300, parqueaderos = 3, banios = 3, habitaciones = 5, estrato = c(5L, 6L))
pr1 <- pred_solicitud(ms1, duan1, sol1)
pr2 <- pred_solicitud(ms2, duan2, sol2)
# --- Selección de ofertas -------------------------------------------------
# Criterio: una oferta solo es candidata si CUMPLE todos los requisitos de la
# solicitud (presupuesto, estrato y mínimos de área, habitaciones, baños y
# parqueaderos). Entre las que cumplen se ordena por precio por metro cuadrado,
# es decir, por la mejor relación entre lo que se paga y lo que se recibe.
cumplen <- function(d, credito, estratos, area, hab, ban, parq) {
d %>% filter(preciom <= credito, estrato %in% estratos, areaconst >= area,
habitaciones >= hab, banios >= ban, parqueaderos >= parq) %>%
arrange(precio_m2)
}
cand1 <- cumplen(base1, 350, 4:5, 200, 4, 2, 1)
cand2 <- cumplen(base2, 850, 5:6, 300, 5, 3, 3)
# Solicitud 2: los que cumplen todo son muy pocos, así que se completan con
# alternativas de compromiso, indicando siempre qué requisito no satisfacen.
compromiso2 <- base2 %>%
filter(preciom <= 850, estrato %in% 5:6, areaconst >= 250,
habitaciones >= 5, banios >= 3, parqueaderos >= 2) %>%
anti_join(cand2, by = c("barrio","preciom","areaconst")) %>%
arrange(desc(areaconst))
etiqueta_cumplimiento <- function(d, area, parq) {
falla <- mapply(function(a, p) {
f <- c(if (a < area) paste0("área ", a, " m²"), if (p < parq) paste0(p, " parqueadero(s)"))
if (length(f) == 0) "Cumple todo" else paste("No cumple:", paste(f, collapse = " y "))
}, d$areaconst, d$parqueaderos)
falla
}
top1 <- head(cand1, 5)
top2 <- rbind(cand2, head(compromiso2, 5 - nrow(cand2)))
tabla_ofertas <- function(top, credito, cap, cumplimiento = NULL) {
x <- top %>% transmute(Barrio = titulo_barrio(barrio), Estrato = estrato,
`Precio (millones)` = preciom, `Área (m²)` = areaconst,
Parq. = parqueaderos, Baños = banios, Hab. = habitaciones,
`Precio/m² (millones)` = round(precio_m2/1e6, 2),
`Margen (millones)` = credito - preciom)
if (!is.null(cumplimiento)) x$Cumplimiento <- cumplimiento
tabla_kable(x, cap)
}
# --- diagnóstico de geocodificación: extensión de cada barrio ---
disp_barrio <- base %>% group_by(barrio) %>% filter(n() >= 20) %>%
summarise(n = n(),
km_lat = (max(latitud) - min(latitud)) * 111,
km_lon = (max(longitud) - min(longitud)) * 111 * cos(3.42*pi/180),
.groups = "drop") %>%
mutate(`Extensión (km)` = round(sqrt(km_lat^2 + km_lon^2), 1)) %>%
arrange(desc(`Extensión (km)`))
# --- autocorrelación espacial de los residuales (I de Moran) ---
moran_residuales <- function(d, nmax = 900) {
set.seed(2024)
if (nrow(d) > nmax) d <- d[sample(nrow(d), nmax), ]
m <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = d)
D <- as.matrix(dist(cbind(d$longitud, d$latitud)))
W <- 1/D; diag(W) <- 0; W[is.infinite(W)] <- 0
ape::Moran.I(residuals(m), W)
}
mo1 <- moran_residuales(base1); mo2 <- moran_residuales(base2)
# --- prueba de sensibilidad al supuesto de imputación de parqueaderos ---
sens_imputacion <- function(d_cruda, nueva, etiqueta) {
f <- preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios
d_imp <- d_cruda %>% mutate(parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos))
d_cc <- d_cruda %>% filter(!is.na(parqueaderos))
m_imp <- lm(f, d_imp); m_cc <- lm(f, d_cc)
et <- c("Intercepto","Área construida","Estrato","Habitaciones","Parqueaderos","Baños")
coef_tab <- data.frame(
Elemento = et,
`Imputando 0` = round(coef(m_imp), 2),
`Valor p (imp.)` = format.pval(summary(m_imp)$coefficients[,4], eps = .0001),
`Casos completos` = round(coef(m_cc), 2),
`Valor p (c.c.)` = format.pval(summary(m_cc)$coefficients[,4], eps = .0001),
check.names = FALSE, row.names = NULL)
pred_tab <- data.frame(
Elemento = c(paste0("Predicción estrato ", nueva$estrato), "Observaciones", "R²"),
`Imputando 0` = c(round(predict(m_imp, nueva), 1), nrow(d_imp), round(summary(m_imp)$r.squared, 4)),
`Valor p (imp.)` = "No aplica",
`Casos completos` = c(round(predict(m_cc, nueva), 1), nrow(d_cc), round(summary(m_cc)$r.squared, 4)),
`Valor p (c.c.)` = "No aplica",
check.names = FALSE, row.names = NULL)
rbind(coef_tab, pred_tab)
}
data.frame(
Solicitud = c("Casa · zona norte", "Apartamento · zona sur"),
`Precio estimado (millones)` = c(
paste0(round(pr1[1,2], 1), " en estrato 4; ", round(pr1[2,2], 1), " en estrato 5"),
paste0(round(min(as.numeric(pr2[1,c(2,5,6)])), 1), " a ",
round(max(as.numeric(pr2[2,c(2,5,6)])), 1), " en estratos 5 y 6")
),
Crédito = c("350 millones", "850 millones"),
`Ofertas que cumplen todo` = c(nrow(cand1), nrow(cand2)),
Recomendación = c("Priorizar estrato 4", "Mantener crédito y flexibilizar requisitos"),
check.names = FALSE
) %>%
tabla_kable("Resumen ejecutivo de las dos solicitudes")
comparar <- data.frame(
panel = factor(rep(c("Solicitud 1 · casa en la zona norte",
"Solicitud 2 · apartamento en la zona sur"), each = 2)),
escenario = c("Estrato 4", "Estrato 5", "Estrato 5", "Estrato 6"),
estimado = c(pr1[1,2], pr1[2,2],
round(mean(as.numeric(pr2[1,c(2,5,6)])), 1),
round(mean(as.numeric(pr2[2,c(2,5,6)])), 1)),
credito = c(350, 350, 850, 850))
comparar$alcanza <- ifelse(comparar$estimado <= comparar$credito, "alcanza", "no alcanza")
lineas <- data.frame(panel = levels(comparar$panel), credito = c(350, 850))
holgura <- data.frame(panel = levels(comparar$panel), x = c(560, 1180),
escenario = c("Estrato 4", "Estrato 5"))
ggplot(comparar, aes(estimado, escenario, fill = alcanza)) +
geom_col(width = .5) +
geom_blank(data = holgura, aes(x = x, y = escenario), inherit.aes = FALSE) +
geom_vline(data = lineas, aes(xintercept = credito),
linetype = "22", linewidth = .85, color = GRIS) +
geom_text(aes(label = paste0(round(estimado), " M")),
hjust = 1.18, size = 4, color = "white", fontface = "bold") +
geom_text(aes(label = alcanza, color = alcanza), hjust = -0.14, size = 3.9,
show.legend = FALSE) +
facet_wrap(~ panel, scales = "free", ncol = 2) +
scale_fill_manual(values = c("alcanza" = OK_, "no alcanza" = NO_), guide = "none") +
scale_color_manual(values = c("alcanza" = OK_, "no alcanza" = NO_), guide = "none") +
coord_cartesian(clip = "off") +
labs(title = "Precio estimado frente al crédito preaprobado",
subtitle = "La línea punteada representa el monto disponible",
x = "Millones de pesos", y = NULL) +
theme(strip.text = element_text(face = "bold", size = 10.5),
panel.grid.major.y = element_blank(),
axis.text.y = element_text(size = 11),
plot.margin = margin(8, 12, 4, 6))
top1 %>%
transmute(
Barrio = titulo_barrio(barrio), Estrato = estrato,
`Precio (millones)` = preciom, `Área (m²)` = areaconst,
`Hab./Baños/Parq.` = paste(habitaciones, banios, parqueaderos, sep = " / "),
`Margen (millones)` = 350 - preciom
) %>%
tabla_kable("Cinco ofertas sugeridas para la solicitud 1 · ubicación en el Anexo 2")
ggplot(base2, aes(areaconst)) +
geom_histogram(binwidth = 10, fill = AZUL, alpha = .9) +
geom_vline(xintercept = 300, color = NO_, linewidth = 1) +
annotate("text", x = 315, y = Inf, vjust = 1.8, hjust = 0, size = 3.6, color = NO_,
label = paste0("Solicitado: 300 m²\nSolo ", sum(base2$areaconst >= 300),
" de ", format(nrow(base2), big.mark = ".", decimal.mark = ","),
" apartamentos alcanzan esa área")) +
scale_x_continuous(breaks = seq(0, 900, 100)) +
labs(title = "El apartamento solicitado está en el extremo de la oferta",
subtitle = "Distribución del área construida de los apartamentos de la zona sur",
x = "Área construida (m²)", y = "Número de apartamentos")
top2 %>%
transmute(
Barrio = titulo_barrio(barrio), Estrato = estrato,
`Precio (millones)` = preciom, `Área (m²)` = areaconst,
`Hab./Baños/Parq.` = paste(habitaciones, banios, parqueaderos, sep = " / "),
`Margen (millones)` = 850 - preciom,
Cumplimiento = etiqueta_cumplimiento(top2, 300, 3)
) %>%
tabla_kable("Cinco ofertas sugeridas para la solicitud 2 · ubicación en el Anexo 3")
efectos <- bind_rows(
tidy(ms1$M1) %>% mutate(Zona = "Casas · zona norte"),
tidy(ms2$M1) %>% mutate(Zona = "Apartamentos · zona sur")) %>%
filter(term != "(Intercept)") %>%
mutate(
paso = ifelse(term == "areaconst", 10, 1),
efecto = estimate * paso,
Atributo = c(areaconst = "10 m² más de área", estrato = "Un estrato más",
banios = "Un baño más", habitaciones = "Una habitación más",
parqueaderos = "Un parqueadero más")[term],
Significativo = ifelse(p.value < 0.05, "Significativo", "No significativo"),
Zona = factor(Zona, levels = c("Casas · zona norte", "Apartamentos · zona sur")),
Atributo = factor(Atributo, levels = c("Un estrato más", "Un baño más",
"Un parqueadero más", "10 m² más de área", "Una habitación más")))
ggplot(efectos, aes(efecto, Atributo, fill = Zona, alpha = Significativo)) +
geom_col(position = position_dodge(width = .72), width = .62) +
geom_vline(xintercept = 0, color = "grey40") +
geom_text(aes(label = paste0(ifelse(efecto >= 0, "+", ""), round(efecto, 1))),
position = position_dodge(width = .72),
hjust = ifelse(efectos$efecto >= 0, -.15, 1.15),
size = 3.1, color = "grey20", show.legend = FALSE) +
scale_fill_manual(values = c("Casas · zona norte" = AZUL,
"Apartamentos · zona sur" = NARANJA), name = NULL) +
scale_alpha_manual(values = c("Significativo" = 1,
"No significativo" = .32), guide = "none") +
scale_x_continuous(expand = expansion(mult = c(.12, .16))) +
labs(title = "Cambio estimado del precio ante cada atributo",
subtitle = "Millones de pesos, manteniendo constantes las demás características",
x = "Millones de pesos", y = NULL)
data.frame(
Orden = 1:4,
Acción = c(
"Visitar las cinco casas recomendadas y comenzar por las tres de estrato 4.",
"Verificar antigüedad, estado y acabados antes de negociar.",
"Presentar las dos coincidencias exactas para la solicitud 2.",
"Si ninguna es adecuada, acordar qué requisito puede flexibilizarse."
),
Propósito = c(
"Atender la solicitud 1 sin ampliar el crédito.",
"Validar los atributos que no registra la base.",
"Confirmar si alguna satisface a la compañía.",
"Ampliar la oferta de apartamentos del sur."
),
check.names = FALSE
) %>%
kable(caption = tabnum("Plan de acción recomendado"),
align = c("c", "l", "l")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE) %>%
column_spec(1, width = "8%", bold = TRUE) %>%
column_spec(2, width = "55%") %>%
column_spec(3, width = "37%")
data.frame(
Paso = 1:7,
Evidencia = c(
"Filtro de casas de la zona norte, tres primeros registros, tablas de verificación, mapa y discusión de la ubicación.",
"Correlaciones del precio con las variables solicitadas, gráficos interactivos con plotly e interpretación del efecto de la zona.",
"Modelo de regresión lineal múltiple, coeficientes, significancia, interpretación contextual, R² y propuestas de mejora.",
"Pruebas y gráficos de diagnóstico, interpretación de los supuestos y sugerencias de tratamiento.",
"Predicción para la primera solicitud, con estimación puntual e intervalos.",
"Selección y análisis de cinco ofertas dentro del crédito, presentadas en un mapa interactivo.",
"Repetición de los pasos 1 a 6 para el apartamento de la zona sur y el crédito de 850 millones."
),
Ubicación = c(
"Anexo 2, paso 1", "Anexo 2, paso 2", "Anexo 2, paso 3",
"Anexo 2, paso 4", "Anexo 2, paso 5", "Anexo 2, paso 6",
"Anexo 3"
),
check.names = FALSE
) %>%
kable(caption = tabnum("Correspondencia entre el enunciado y el informe"),
align = c("c", "l", "l")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE) %>%
column_spec(1, width = "7%", bold = TRUE) %>%
column_spec(2, width = "73%") %>%
column_spec(3, width = "20%")
paso_a <- viv %>% filter(rowSums(is.na(.)) < ncol(.)) %>%
filter(!is.na(preciom), !is.na(areaconst), !is.na(estrato), !is.na(zona), !is.na(tipo),
!is.na(banios), !is.na(habitaciones), !is.na(longitud), !is.na(latitud))
n_imput <- sum(is.na(paso_a$parqueaderos[paso_a$habitaciones > 0 & paso_a$banios > 0]))
data.frame(
Paso = c("Base original `vivienda`",
"Se eliminan registros sin datos en las variables del modelo",
"Se eliminan habitaciones = 0 y baños = 0",
paste0("Se imputan con 0 los ",
format(n_imput, big.mark = ".", decimal.mark = ","),
" datos faltantes de parqueaderos"),
"Filtro: tipo = Casa y zona = Zona Norte"),
Registros = c(nrow(viv), nrow(paso_a), nrow(base), nrow(base), nrow(base1)),
`% de la base original` = paste0(round(100*(
c(nrow(viv), nrow(paso_a), nrow(base), nrow(base), nrow(base1)))/nrow(viv), 2), " %"),
check.names = FALSE) %>%
mutate(Registros = format(Registros, big.mark = ".", decimal.mark = ",")) %>%
tabla_kable("Trazabilidad del filtro para la base de casas de la zona norte")
base1 %>% select(zona, tipo, barrio, estrato, preciom, areaconst,
parqueaderos, banios, habitaciones) %>%
head(3) %>%
tabla_kable("Primeros tres registros de la base de casas de la zona norte")
tabla_kable(as.data.frame(table(Tipo = base1$tipo)) %>% rename(Registros = Freq),
"Verificación del filtro por tipo de vivienda")
tabla_kable(as.data.frame(table(Zona = base1$zona)) %>% rename(Registros = Freq),
"Verificación del filtro por zona")
base1 %>% count(Estrato = estrato, name = "Casas") %>%
mutate(`% del total` = round(100 * Casas / sum(Casas), 1),
`Precio mediano (millones)` = sapply(Estrato, function(e) median(base1$preciom[base1$estrato == e]))) %>%
tabla_kable("Composición de la base de casas del norte por estrato")
if (knitr::is_html_output()) {
leaflet() %>%
addProviderTiles(providers$OpenStreetMap.Mapnik) %>%
addCircleMarkers(data = base %>% filter(!(zona == "Zona Norte" & tipo == "Casa")),
~longitud, ~latitud, radius = 2, stroke = FALSE,
fillOpacity = .12, color = "grey50", group = "Resto de la oferta") %>%
addCircleMarkers(data = base1, ~longitud, ~latitud, radius = 4, stroke = FALSE,
fillOpacity = .7, color = AZUL, group = "Casas zona norte",
popup = ~paste0("<b>", barrio, "</b><br>", preciom, " millones · ",
areaconst, " m² · estrato ", estrato)) %>%
{if (hay_limite) addPolygons(., data = limite_cali, fill = FALSE, color = "#1a1a1a",
weight = 2, opacity = .7,
group = "Límite municipal") else .} %>%
addLayersControl(
overlayGroups = c("Resto de la oferta", "Casas zona norte",
if (hay_limite) "Límite municipal"),
options = layersControlOptions(collapsed = FALSE))
} else {
resto_norte <- base %>% filter(!(zona == "Zona Norte" & tipo == "Casa"))
p_mapa_norte <- ggplot() +
geom_point(data = resto_norte, aes(longitud, latitud),
color = "grey75", size = .45, alpha = .25) +
geom_point(data = base1, aes(longitud, latitud),
color = AZUL, size = 1.2, alpha = .65) +
labs(title = "Casas de la zona norte",
subtitle = "En gris, el resto de la oferta registrada",
x = "Longitud", y = "Latitud")
if (hay_limite) {
p_mapa_norte <- p_mapa_norte +
geom_sf(data = limite_cali, fill = NA, color = "grey35",
linewidth = .5, inherit.aes = FALSE)
}
p_mapa_norte
}
base %>%
group_by(Zona = zona) %>%
summarise(Registros = n(),
`Latitud p05` = round(quantile(latitud, .05), 4),
`Latitud p95` = round(quantile(latitud, .95), 4),
`Longitud p05` = round(quantile(longitud, .05), 4),
`Longitud p95` = round(quantile(longitud, .95), 4), .groups = "drop") %>%
tabla_kable("Extensión espacial declarada por cada zona: los rangos se traslapan")
head(disp_barrio, 6) %>%
transmute(Barrio = titulo_barrio(barrio), Registros = n,
`Extensión (km)` = `Extensión (km)`) %>%
tabla_kable("Distancia entre los puntos más alejados de un mismo barrio")
vars_cor <- c("preciom", vars_mod)
M1cor <- cor(base1[, vars_cor])
data.frame(Variable = c("Área construida","Estrato","Habitaciones","Parqueaderos","Baños"),
`Correlación con el precio` = round(M1cor["preciom", vars_mod], 3),
check.names = FALSE, row.names = NULL) %>%
arrange(desc(`Correlación con el precio`)) %>%
tabla_kable("Correlación de cada predictora con el precio · casas de la zona norte")
g <- ggplot(base1, aes(areaconst, preciom, color = factor(estrato),
text = paste0(barrio, "<br>", preciom, " M · ", areaconst, " m²"))) +
geom_point(alpha = .6, size = 1.6) +
geom_smooth(method = "lm", se = FALSE, formula = y ~ x, linewidth = .7) +
scale_color_manual(values = PAL4, name = "Estrato") +
labs(title = "Precio frente a área construida según estrato",
x = "Área construida (m²)", y = "Precio (millones)")
if (knitr::is_html_output()) {
ggplotly(g, tooltip = "text") %>%
layout(legend = list(orientation = "h", y = -0.2))
} else {
g + guides(color = guide_legend(nrow = 1))
}
g2 <- ggplot(base1, aes(factor(estrato), preciom, fill = factor(estrato))) +
geom_boxplot(outlier.alpha = .25) +
scale_fill_manual(values = PAL4, guide = "none") +
labs(title = "Distribución del precio por estrato", x = "Estrato", y = "Precio (millones)")
if (knitr::is_html_output()) ggplotly(g2) else g2
base %>% filter(tipo == "Casa") %>%
group_by(Zona = zona) %>%
summarise(Casas = n(),
`Precio mediano (millones)` = median(preciom),
`Área mediana (m²)` = median(areaconst),
`Precio/m² (millones)` = round(median(precio_m2)/1e6, 2), .groups = "drop") %>%
arrange(desc(`Precio/m² (millones)`)) %>%
tabla_kable("Comparación de los submercados de casas por zona")
tidy(m1) %>%
mutate(term = c("Intercepto","Área construida","Estrato","Habitaciones","Parqueaderos","Baños"),
across(c(estimate, std.error, statistic), ~round(.x, 4)),
p.value = format.pval(p.value, eps = .0001),
Signif = case_when(tidy(m1)$p.value < .001 ~ "***", tidy(m1)$p.value < .01 ~ "**",
tidy(m1)$p.value < .05 ~ "*", TRUE ~ "")) %>%
setNames(c("Término","Coeficiente","Error estándar","Estadístico t","Valor p","")) %>%
tabla_kable("Coeficientes estimados del modelo de regresión · casas de la zona norte")
s <- summary(m1)
data.frame(Indicador = c("R²", "R² ajustado", "Error estándar residual (millones)",
"Estadístico F", "Valor p del modelo", "Observaciones"),
Valor = c(round(s$r.squared, 4), round(s$adj.r.squared, 4),
round(s$sigma, 1), round(s$fstatistic[1], 1), "< 0,0001", nrow(base1))) %>%
tabla_kable("Indicadores de ajuste global del modelo")
tabla_comparacion(ms1, base1) %>%
tabla_kable("Comparación de especificaciones · casas de la zona norte")
cv1 <- cv_rmse(base1)
tidy(ms1$M4) %>% filter(grepl("estrato", term)) %>%
mutate(term = c("Estrato 4 vs. 3","Estrato 5 vs. 3","Estrato 6 vs. 3"),
estimate = round(estimate, 1), std.error = round(std.error, 2),
p.value = format.pval(p.value, eps = .0001)) %>%
select(Término = term, `Sobreprecio (millones)` = estimate,
`Error estándar` = std.error, `Valor p` = p.value) %>%
tabla_kable("Efecto del estrato tratado como variable categórica")
tabla_supuestos(m1) %>%
tabla_kable("Validación de los supuestos del modelo · casas de la zona norte")
graficos_residuales(m1, "Diagnóstico de residuales · casas de la zona norte")
data.frame(Elemento = c("I de Moran observado", "Valor esperado bajo independencia",
"Desviación estándar", "Valor p"),
Valor = c(round(mo1$observed, 4), round(mo1$expected, 4),
round(mo1$sd, 4), format.pval(mo1$p.value, eps = .0001))) %>%
tabla_kable("Autocorrelación espacial de los residuales · casas de la zona norte")
data.frame(Variable = c("Área construida","Estrato","Habitaciones","Parqueaderos","Baños"),
VIF = round(as.numeric(vif(m1)), 3), row.names = NULL) %>%
mutate(Diagnóstico = ifelse(VIF <= 5, "Sin problema", ifelse(VIF <= 10, "Moderada", "Grave"))) %>%
tabla_kable("Factores de inflación de varianza")
sens_imputacion(base1_cruda, sol1, "Casa zona norte") %>%
tabla_kable("Efecto del supuesto de imputación sobre coeficientes y predicciones · casas del norte")
data.frame(Variable = c("Área construida","Parqueaderos","Baños","Habitaciones"),
Solicitado = c(200, 1, 2, 4),
`Mínimo observado` = c(min(base1$areaconst), min(base1$parqueaderos),
min(base1$banios), min(base1$habitaciones)),
`Máximo observado` = c(max(base1$areaconst), max(base1$parqueaderos),
max(base1$banios), max(base1$habitaciones)),
`Percentil de la solicitud` = paste0(
round(100*c(mean(base1$areaconst <= 200), mean(base1$parqueaderos <= 1),
mean(base1$banios <= 2), mean(base1$habitaciones <= 4)), 0), " %"),
check.names = FALSE) %>%
tabla_kable("La solicitud 1 está dentro del rango observado en todas las variables")
pr1 %>% tabla_kable("Precio estimado para la solicitud 1, en millones de pesos")
data.frame(
Filtro = c("Presupuesto (≤ 350 millones) y estrato 4 o 5",
"Además, todos los mínimos de área, habitaciones, baños y parqueaderos"),
Casas = c(sum(base1$preciom <= 350 & base1$estrato %in% 4:5), nrow(cand1))) %>%
tabla_kable("Del presupuesto a las ofertas que realmente cumplen la solicitud")
tabla_ofertas(top1, 350, "Cinco ofertas sugeridas para la solicitud 1")
mapa_ofertas(top1, base1, "Precio")
data.frame(
Paso = c("Base depurada general",
"Filtro: tipo = Apartamento y zona = Zona Sur"),
Registros = format(c(nrow(base), nrow(base2)),
big.mark = ".", decimal.mark = ",")
) %>%
tabla_kable("Trazabilidad del filtro para la base de apartamentos de la zona sur")
base2 %>% select(zona, tipo, barrio, estrato, preciom, areaconst,
parqueaderos, banios, habitaciones) %>%
head(3) %>%
tabla_kable("Primeros tres registros de la base de apartamentos de la zona sur")
tabla_kable(as.data.frame(table(Tipo = base2$tipo)) %>% rename(Registros = Freq),
"Verificación del filtro por tipo de vivienda")
tabla_kable(as.data.frame(table(Zona = base2$zona)) %>% rename(Registros = Freq),
"Verificación del filtro por zona")
base2 %>% count(Estrato = estrato, name = "Apartamentos") %>%
mutate(`% del total` = round(100 * Apartamentos / sum(Apartamentos), 1),
`Precio mediano (millones)` = sapply(Estrato, function(e) median(base2$preciom[base2$estrato == e]))) %>%
tabla_kable("Composición de la base de apartamentos del sur por estrato")
if (knitr::is_html_output()) {
leaflet() %>%
addProviderTiles(providers$OpenStreetMap.Mapnik) %>%
addCircleMarkers(data = base %>% filter(!(zona == "Zona Sur" & tipo == "Apartamento")),
~longitud, ~latitud, radius = 2, stroke = FALSE,
fillOpacity = .12, color = "grey50", group = "Resto de la oferta") %>%
addCircleMarkers(data = base2, ~longitud, ~latitud, radius = 3, stroke = FALSE,
fillOpacity = .55, color = VERDE,
group = "Apartamentos zona sur",
popup = ~paste0("<b>", barrio, "</b><br>", preciom,
" millones · ", areaconst,
" m² · estrato ", estrato)) %>%
{if (hay_limite) addPolygons(., data = limite_cali, fill = FALSE, color = "#1a1a1a",
weight = 2, opacity = .7,
group = "Límite municipal") else .} %>%
addLayersControl(
overlayGroups = c("Resto de la oferta", "Apartamentos zona sur",
if (hay_limite) "Límite municipal"),
options = layersControlOptions(collapsed = FALSE))
} else {
resto_sur <- base %>% filter(!(zona == "Zona Sur" & tipo == "Apartamento"))
p_mapa_sur <- ggplot() +
geom_point(data = resto_sur, aes(longitud, latitud),
color = "grey75", size = .45, alpha = .25) +
geom_point(data = base2, aes(longitud, latitud),
color = VERDE, size = 1, alpha = .5) +
labs(title = "Apartamentos de la zona sur",
subtitle = "En gris, el resto de la oferta registrada",
x = "Longitud", y = "Latitud")
if (hay_limite) {
p_mapa_sur <- p_mapa_sur +
geom_sf(data = limite_cali, fill = NA, color = "grey35",
linewidth = .5, inherit.aes = FALSE)
}
p_mapa_sur
}
disp_barrio %>%
filter(barrio %in% c("valle del lili","ciudad jardin","pance","el ingenio","el caney","la hacienda")) %>%
transmute(Barrio = titulo_barrio(barrio), Registros = n, `Extensión (km)` = `Extensión (km)`) %>%
tabla_kable("Extensión espacial de los principales barrios de la zona sur")
M2cor <- cor(base2[, vars_cor])
data.frame(Variable = c("Área construida","Estrato","Habitaciones","Parqueaderos","Baños"),
`Correlación con el precio` = round(M2cor["preciom", vars_mod], 3),
check.names = FALSE, row.names = NULL) %>%
arrange(desc(`Correlación con el precio`)) %>%
tabla_kable("Correlación de cada predictora con el precio · apartamentos de la zona sur")
g3 <- ggplot(base2, aes(areaconst, preciom, color = factor(estrato),
text = paste0(barrio, "<br>", preciom, " M · ", areaconst, " m²"))) +
geom_point(alpha = .45, size = 1.3) +
geom_smooth(method = "lm", se = FALSE, formula = y ~ x, linewidth = .7) +
scale_color_manual(values = PAL4, name = "Estrato") +
labs(title = "Precio frente a área construida según estrato",
x = "Área construida (m²)", y = "Precio (millones)")
if (knitr::is_html_output()) {
ggplotly(g3, tooltip = "text") %>%
layout(legend = list(orientation = "h", y = -0.2))
} else {
g3 + guides(color = guide_legend(nrow = 1))
}
g4 <- ggplot(base2, aes(factor(estrato), preciom, fill = factor(estrato))) +
geom_boxplot(outlier.alpha = .2) +
scale_fill_manual(values = PAL4, guide = "none") +
labs(title = "Distribución del precio por estrato", x = "Estrato", y = "Precio (millones)")
if (knitr::is_html_output()) ggplotly(g4) else g4
base %>% filter(tipo == "Apartamento") %>%
group_by(Zona = zona) %>%
summarise(Apartamentos = n(),
`Precio mediano (millones)` = median(preciom),
`Área mediana (m²)` = median(areaconst),
`Precio/m² (millones)` = round(median(precio_m2)/1e6, 2), .groups = "drop") %>%
arrange(desc(`Precio/m² (millones)`)) %>%
tabla_kable("Comparación de los submercados de apartamentos por zona")
tidy(m2) %>%
mutate(term = c("Intercepto","Área construida","Estrato","Habitaciones","Parqueaderos","Baños"),
across(c(estimate, std.error, statistic), ~round(.x, 4)),
p.value = format.pval(p.value, eps = .0001),
Signif = case_when(tidy(m2)$p.value < .001 ~ "***", tidy(m2)$p.value < .01 ~ "**",
tidy(m2)$p.value < .05 ~ "*", TRUE ~ "")) %>%
setNames(c("Término","Coeficiente","Error estándar","Estadístico t","Valor p","")) %>%
tabla_kable("Coeficientes estimados del modelo de regresión · apartamentos de la zona sur")
s2 <- summary(m2)
data.frame(Indicador = c("R²", "R² ajustado", "Error estándar residual (millones)",
"Estadístico F", "Valor p del modelo", "Observaciones"),
Valor = c(round(s2$r.squared, 4), round(s2$adj.r.squared, 4),
round(s2$sigma, 1), round(s2$fstatistic[1], 1), "< 0,0001", nrow(base2))) %>%
tabla_kable("Indicadores de ajuste global del modelo")
tabla_comparacion(ms2, base2) %>%
tabla_kable("Comparación de especificaciones · apartamentos de la zona sur")
cv2 <- cv_rmse(base2)
tidy(ms2$M4) %>% filter(grepl("estrato", term)) %>%
mutate(term = c("Estrato 4 vs. 3","Estrato 5 vs. 3","Estrato 6 vs. 3"),
estimate = round(estimate, 1), std.error = round(std.error, 2),
p.value = format.pval(p.value, eps = .0001)) %>%
select(Término = term, `Sobreprecio (millones)` = estimate,
`Error estándar` = std.error, `Valor p` = p.value) %>%
tabla_kable("Efecto del estrato tratado como variable categórica")
tabla_supuestos(m2) %>%
tabla_kable("Validación de los supuestos del modelo · apartamentos de la zona sur")
graficos_residuales(m2, "Diagnóstico de residuales · apartamentos de la zona sur")
idx <- order(cooks.distance(m2), decreasing = TRUE)[1:3]
base2[idx, ] %>%
transmute(Barrio = titulo_barrio(barrio),
Estrato = estrato, `Precio (millones)` = preciom, `Área (m²)` = areaconst,
Parq. = parqueaderos, Baños = banios, Hab. = habitaciones,
`Distancia de Cook` = round(cooks.distance(m2)[idx], 2)) %>%
tabla_kable("Las tres observaciones más influyentes del modelo")
data.frame(Variable = c("Área construida","Estrato","Habitaciones","Parqueaderos","Baños"),
VIF = round(as.numeric(vif(m2)), 3), row.names = NULL) %>%
mutate(Diagnóstico = ifelse(VIF <= 5, "Sin problema", ifelse(VIF <= 10, "Moderada", "Grave"))) %>%
tabla_kable("Factores de inflación de varianza")
sens_imputacion(base2_cruda, sol2, "Apartamento zona sur") %>%
tabla_kable("Efecto del supuesto de imputación sobre coeficientes y predicciones · apartamentos del sur")
data.frame(Variable = c("Área construida","Parqueaderos","Baños","Habitaciones"),
Solicitado = c(300, 3, 3, 5),
`Mínimo observado` = c(min(base2$areaconst), min(base2$parqueaderos),
min(base2$banios), min(base2$habitaciones)),
`Máximo observado` = c(max(base2$areaconst), max(base2$parqueaderos),
max(base2$banios), max(base2$habitaciones)),
`Percentil de la solicitud` = paste0(
round(100*c(mean(base2$areaconst <= 300), mean(base2$parqueaderos <= 3),
mean(base2$banios <= 3), mean(base2$habitaciones <= 5)), 0), " %"),
check.names = FALSE) %>%
tabla_kable("La solicitud 2 se ubica en el borde superior de la distribución")
data.frame(
Condición = c("Área ≥ 300 m²", "Parqueaderos ≥ 3", "Habitaciones ≥ 5", "Baños ≥ 3",
"Todas las condiciones simultáneamente"),
Apartamentos = c(sum(base2$areaconst >= 300), sum(base2$parqueaderos >= 3),
sum(base2$habitaciones >= 5), sum(base2$banios >= 3),
sum(base2$areaconst >= 300 & base2$parqueaderos >= 3 &
base2$habitaciones >= 5 & base2$banios >= 3))
) %>%
mutate(`% de la base` = paste0(round(100*Apartamentos/nrow(base2), 2), " %")) %>%
tabla_kable("Apartamentos del sur que cumplen las condiciones de la solicitud 2")
pr2 %>% tabla_kable("Precio estimado para la solicitud 2, en millones de pesos")
data.frame(
Filtro = c("Presupuesto (≤ 850 millones) y estrato 5 o 6",
"Además, todos los mínimos de la solicitud",
"Relajando el área a 250 m² y los parqueaderos a 2"),
Apartamentos = c(sum(base2$preciom <= 850 & base2$estrato %in% 5:6),
nrow(cand2), nrow(cand2) + nrow(compromiso2))) %>%
tabla_kable("Del presupuesto a las ofertas que cumplen la solicitud 2")
tabla_ofertas(top2, 850, "Cinco ofertas sugeridas para la solicitud 2", etiqueta_cumplimiento(top2, 300, 3))
mapa_ofertas(top2, base2, "Precio")
contar_opciones2 <- function(area_min = 300, parq_min = 3) {
sum(base2$preciom <= 850 & base2$estrato %in% 5:6 &
base2$areaconst >= area_min & base2$habitaciones >= 5 &
base2$banios >= 3 & base2$parqueaderos >= parq_min)
}
casas_sur_cumplen <- base %>%
filter(tipo == "Casa", zona == "Zona Sur", preciom <= 850,
estrato %in% 5:6, areaconst >= 300, habitaciones >= 5,
banios >= 3, parqueaderos >= 3)
opciones2 <- c(contar_opciones2(300, 3), contar_opciones2(250, 3),
contar_opciones2(300, 2), contar_opciones2(250, 2))
data.frame(
Escenario = c("Solicitud completa", "Solo área mínima de 250 m²",
"Solo 2 parqueaderos", "Área mínima de 250 m² y 2 parqueaderos"),
`Apartamentos disponibles` = opciones2,
`Aumento frente a la solicitud` = c("Base", paste0("+", opciones2[-1] - opciones2[1])),
check.names = FALSE) %>%
tabla_kable("Efecto de flexibilizar el área y los parqueaderos")
pal_sol <- colorFactor(c(AZUL, NARANJA), domain = c("Solicitud 1 · casa norte",
"Solicitud 2 · apartamento sur"))
ofertas <- rbind(top1 %>% mutate(grupo = "Solicitud 1 · casa norte", credito = 350),
top2 %>% mutate(grupo = "Solicitud 2 · apartamento sur", credito = 850))
if (knitr::is_html_output()) {
m <- leaflet(ofertas) %>%
addProviderTiles(providers$OpenStreetMap.Mapnik) %>%
setView(lng = -76.53, lat = 3.43, zoom = 12) %>%
addCircleMarkers(~longitud, ~latitud, radius = 10, stroke = TRUE, weight = 2,
color = "white", fillColor = ~pal_sol(grupo), fillOpacity = .95,
label = ~paste0(titulo_barrio(barrio), " · ", preciom, " M"),
labelOptions = labelOptions(permanent = TRUE, direction = "top",
textsize = "11px", opacity = .9),
popup = ~paste0("<b>", titulo_barrio(barrio), "</b><br>",
grupo, "<br><b>Precio:</b> ", preciom,
" millones<br><b>Área:</b> ", areaconst,
" m² · <b>Estrato:</b> ", estrato,
"<br><b>Margen:</b> ", credito - preciom,
" millones")) %>%
addLegend("bottomright", pal = pal_sol, values = ~grupo,
title = "Ofertas", opacity = .95)
if (hay_limite) {
m <- m %>% addPolygons(data = limite_cali, fill = FALSE,
color = "#1a1a1a", weight = 2, opacity = .6)
}
m
} else {
p_ofertas <- ggplot() +
geom_point(data = base, aes(longitud, latitud),
color = "grey80", size = .35, alpha = .22) +
geom_point(data = ofertas, aes(longitud, latitud, color = grupo),
size = 2.8) +
geom_text(data = ofertas,
aes(longitud, latitud,
label = paste0(titulo_barrio(barrio), "\n", preciom, " M"),
color = grupo),
size = 2.5, vjust = -.8, check_overlap = TRUE,
show.legend = FALSE) +
scale_color_manual(values = c("Solicitud 1 · casa norte" = AZUL,
"Solicitud 2 · apartamento sur" = NARANJA),
name = NULL) +
labs(title = "Ubicación conjunta de las diez ofertas propuestas",
subtitle = "La posición se interpreta con la precisión disponible en la base",
x = "Longitud", y = "Latitud") +
theme(legend.position = "bottom")
if (hay_limite) {
p_ofertas <- p_ofertas +
geom_sf(data = limite_cali, fill = NA, color = "grey35",
linewidth = .5, inherit.aes = FALSE)
}
p_ofertas
}
sessionInfo()Versiones de R y de los paquetes empleados, para garantizar la reproducibilidad.
## R version 4.3.1 (2023-06-16 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19045)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] paqueteMODELOS_0.1.0 summarytools_1.1.5 gridExtra_2.3
## [4] GGally_2.4.0 boot_1.3-28.1 ape_5.8
## [7] sf_1.0-19 stringi_1.8.3 patchwork_1.3.2
## [10] kableExtra_1.4.0 knitr_1.45 DT_0.33
## [13] leaflet_2.2.2 nortest_1.0-4 lmtest_0.9-40
## [16] zoo_1.8-12 car_3.1-2 carData_3.0-5
## [19] broom_1.0.5 plotly_4.12.1 ggplot2_4.0.3
## [22] tidyr_1.3.2 dplyr_1.1.4
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 viridisLite_0.4.2 farver_2.1.1
## [4] S7_0.2.2 fastmap_1.1.1 digest_0.6.35
## [7] timechange_0.3.0 lifecycle_1.0.5 ellipsis_0.3.2
## [10] magrittr_2.0.3 compiler_4.3.1 rlang_1.1.3
## [13] sass_0.4.9 tools_4.3.1 utf8_1.2.4
## [16] yaml_2.3.8 data.table_1.15.2 labeling_0.4.3
## [19] htmlwidgets_1.6.4 classInt_0.4-10 plyr_1.8.9
## [22] xml2_1.3.6 RColorBrewer_1.1-3 abind_1.4-5
## [25] KernSmooth_2.23-21 withr_3.0.0 purrr_1.0.2
## [28] grid_4.3.1 fansi_1.0.6 e1071_1.7-14
## [31] scales_1.4.0 MASS_7.3-60 cli_3.6.2
## [34] rmarkdown_2.27 generics_0.1.3 rstudioapi_0.15.0
## [37] reshape2_1.4.4 httr_1.4.7 DBI_1.2.2
## [40] cachem_1.0.8 proxy_0.4-27 pander_0.6.6
## [43] stringr_1.5.1 splines_4.3.1 parallel_4.3.1
## [46] matrixStats_1.5.0 base64enc_0.1-3 vctrs_0.6.5
## [49] Matrix_1.6-5 jsonlite_1.8.9 rapportools_1.2
## [52] systemfonts_1.3.1 magick_2.9.1 crosstalk_1.2.1
## [55] jquerylib_0.1.4 units_0.8-5 glue_1.8.1
## [58] leaflet.providers_2.0.0 ggstats_0.13.0 lubridate_1.9.3
## [61] gtable_0.3.6 tibble_3.2.1 pillar_1.9.0
## [64] htmltools_0.5.7 R6_2.5.1 tcltk_4.3.1
## [67] textshaping_0.4.0 evaluate_1.0.5 lattice_0.21-8
## [70] highr_0.10 backports_1.5.0 bslib_0.6.1
## [73] class_7.3-22 Rcpp_1.0.12 svglite_2.2.2
## [76] nlme_3.1-162 checkmate_2.3.1 mgcv_1.8-42
## [79] xfun_0.42 pkgconfig_2.0.3