Informe ejecutivo

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.

Decisión en una mirada

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.

Tabla 1. Resumen ejecutivo de las dos solicitudes
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.

Solicitud 1 · Casa en la zona norte

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.

Tabla 2. Cinco ofertas sugeridas para la solicitud 1 · ubicación en el Anexo 2
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.

Solicitud 2 · Apartamento en la zona sur

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.

Tabla 3. Cinco ofertas sugeridas para la solicitud 2 · ubicación en el Anexo 3
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.

Lectura del mercado y plan de acción

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.

Tabla 4. Plan de acción recomendado
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.

Anexo 1. Plan de trabajo

Objetivo y estrategia

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.

Pasos requeridos y dónde se resuelve cada uno

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.

Tabla 5. Correspondencia entre el enunciado y el informe
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.

Decisiones metodológicas

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.


Anexo 2. Solicitud 1 · Casas de la zona norte (pasos 1 a 6)

Paso 1. Construcción y verificación de la base

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.

Tabla 6. Trazabilidad del filtro para la base de casas de la zona norte
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:

Tabla 7. Primeros tres registros de la base de casas de la zona norte
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:

Tabla 8. Verificación del filtro por tipo de vivienda
Tipo Registros
Casa 700
Tabla 9. Verificación del filtro por zona
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:

Tabla 10. Composición de la base de casas del norte por estrato
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

Ubicación de los registros

El mapa muestra las casas de la base sobre el territorio, junto con el resto de la oferta en gris.

¿Todos los puntos caen en la zona que declaran?

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.

Tabla 11. Extensión espacial declarada por cada zona: los rangos se traslapan
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.

Tabla 12. Distancia entre los puntos más alejados 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:

  • Acopi aparece con 18.6 km de extensión. Es un corredor industrial del eje Cali–Yumbo, administrativamente fuera del perímetro urbano, y su etiqueta agrupa direcciones muy distantes entre sí.
  • El barrio literalmente llamado “cali” reúne 30 registros. No es un barrio: es el valor por defecto cuando el anuncio no especifica uno, y el geocodificador lo resuelve al centroide de la ciudad.

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.

Paso 2. Exploración de las relaciones entre variables

Responde al punto 2 del enunciado.

La matriz de correlaciones cuantifica la relación lineal entre el precio y cada predictora:

Tabla 13. Correlación de cada predictora con el precio · casas de la zona norte
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.

El efecto de la zona

Como la base contiene una sola zona, el efecto de esta variable se examina comparando todos los submercados de casas de la ciudad:

Tabla 14. Comparación de los submercados de casas por zona
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.

Paso 3. Estimación del modelo e interpretación de coeficientes

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\]

Tabla 15. Coeficientes estimados del modelo de regresión · casas de la zona norte
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 ***
Tabla 16. Indicadores de ajuste global del modelo
Indicador Valor
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

Interpretación de los coeficientes significativos

Tres de las cinco predictoras resultan significativas al 5 %:

  • Área construida (β̂ = 0,784). Cada m² adicional se asocia con un aumento de 0,78 millones en el precio esperado, manteniendo constantes las demás variables. Un incremento de 10 m² representa cerca de 7,8 millones.
  • Estrato (β̂ = 86,1). Subir un estrato se asocia con un aumento aproximado de 86 millones, a igualdad de las demás características. Es el efecto de mayor magnitud en el modelo.
  • Baños (β̂ = 30,2). Cada baño adicional se asocia con un aumento cercano a 30 millones. Este efecto también podría reflejar diferencias en el tamaño, los acabados o la calidad general que la base no registra.

Coeficientes no significativos

  • Habitaciones (β̂ = 4,1; p = 0,39). El coeficiente no es significativo. Al mantener constante el área, no hay evidencia de una asociación adicional entre el número de habitaciones y el precio.
  • Parqueaderos (β̂ = −0,82; p = 0,85). No significativo, y con signo negativo, contrario a lo que dictaría la intuición.

¿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.

Interpretación del R²

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.

¿Qué se podría hacer para mejorarlo?

  1. Incorporar el barrio como categórica. El modelo actual trata igual a La Flora y a Acopi si comparten estrato y área; el mercado no.
  2. Evaluar una transformación logarítmica del precio. Puede reducir la asimetría y la heterocedasticidad; su utilidad predictiva debe determinarse mediante validación cruzada en la escala original del precio.
  3. Tratar el estrato como categórico, para no imponer que todos los saltos valgan lo mismo.
  4. Enriquecer la base con antigüedad, estado y tiempo en el mercado.

Complemento al paso 3. Especificaciones alternativas y selección del modelo

Se estiman tres especificaciones alternativas y se comparan con los indicadores de la unidad:

Tabla 17. Comparación de especificaciones · casas de la zona norte
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:

Tabla 18. Efecto del estrato tratado como variable categórica
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.

Paso 4. Diagnóstico y validación de supuestos

Responde al punto 4 del enunciado.

Tabla 19. Validación de los supuestos del modelo · casas de la zona norte
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 &#124;r&#124; > 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:

Tabla 20. Autocorrelación espacial de los residuales · casas de la zona norte
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.

Tabla 21. Factores de inflación de varianza
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.

Sugerencias correctivas

No se exige corregir. Las medidas apropiadas, en orden de facilidad, serían:

  1. Errores estándar robustos a heterocedasticidad (tipo White o HC3): no cambian los coeficientes pero devuelven validez a las pruebas t y a los intervalos.
  2. Transformar la respuesta a logaritmo. M3 ya lo hace y atenúa a la vez la heterocedasticidad y la no normalidad.
  3. Regresión ponderada (MCP), dando menos peso a las observaciones de mayor varianza.
  4. Incorporar el barrio como categórica, que es lo que sugiere la autocorrelación espacial detectada.

Sensibilidad al supuesto de imputación

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.

Tabla 22. Efecto del supuesto de imputación sobre coeficientes y predicciones · casas del norte
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
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.

Paso 5. Valoración de la vivienda solicitada

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.

Tabla 23. La solicitud 1 está dentro del rango observado en todas las variables
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.

Tabla 24. Precio estimado para la solicitud 1, en millones de pesos
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.

Paso 6. Ofertas disponibles en el mercado

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.

Tabla 25. Del presupuesto a las ofertas que realmente cumplen la solicitud
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.

Tabla 26. Cinco ofertas sugeridas para la solicitud 1
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.

  • La Merced (250 m², E4, 230 M) es la mejor relación de la lista, a 0,92 millones por m², y deja 120 millones de margen.
  • Los Andes (280 m², E4, 260 M) ofrece 6 habitaciones y 4 baños, con 90 millones de margen.
  • San Vicente (355 m², E5, 340 M) es la más grande y la mejor opción de estrato 5: 8 habitaciones y 5 baños al tope del presupuesto.
  • Salomia (350 m², E4, 350 M) y Vipasa (346 m², E5, 350 M) completan la lista con áreas superiores a los 340 m².

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.


Anexo 3. Paso 7 · Solicitud 2 · Apartamentos de la zona sur (repetición de los pasos 1 a 6)

Se replica íntegramente el procedimiento anterior sobre el segundo submercado.

Paso 1. Construcción y verificación de la base

Responde al punto 1 del enunciado.

Tabla 27. Trazabilidad del filtro para la base de apartamentos de la zona sur
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:

Tabla 28. Primeros tres registros de la base de apartamentos de la zona sur
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:

Tabla 29. Verificación del filtro por tipo de vivienda
Tipo Registros
Apartamento 2777
Tabla 30. Verificación del filtro por zona
Zona Registros
Zona Sur 2777
Tabla 31. Composición de la base de apartamentos del sur por estrato
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.

¿Todos los puntos caen en la zona que declaran?

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.

Tabla 32. Extensión espacial de los principales barrios de la zona sur
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.

Paso 2. Exploración de las relaciones entre variables

Responde al punto 2 del enunciado.

Tabla 33. Correlación de cada predictora con el precio · apartamentos de la zona sur
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.

Tabla 34. Comparación de los submercados de apartamentos por zona
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

Paso 3. Estimación del modelo e interpretación de coeficientes

Responde al punto 3 del enunciado.

Tabla 35. Coeficientes estimados del modelo de regresión · apartamentos de la zona sur
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 ***
Tabla 36. Indicadores de ajuste global del modelo
Indicador Valor
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

Interpretación de los coeficientes

A diferencia del modelo del norte, aquí las cinco predictoras son significativas al 0,1 %:

  • Área construida (β̂ = 1,424). Cada m² adicional se asocia con un aumento de 1,42 millones en el precio esperado, casi el doble del coeficiente estimado para las casas del norte (0,78).
  • Estrato (β̂ = 56,1). Cada escalón se asocia con un aumento aproximado de 56 millones, manteniendo constantes las demás variables. Este coeficiente representa un efecto promedio que luego se contrasta tratando el estrato como categoría.
  • Baños (β̂ = 50,9) y parqueaderos (β̂ = 49,5). Cada unidad adicional se asocia con un aumento cercano a 50 millones, manteniendo constantes las demás variables. En las casas del norte, el resultado de parqueaderos depende del tratamiento de los datos faltantes.
  • Habitaciones (β̂ = −24,2; p < 0,0001). A igualdad de área, baños, parqueaderos y estrato, cada habitación adicional se asocia con una reducción aproximada de 24 millones en el precio esperado.

¿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.

Interpretación del R² y ajuste

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.

Complemento al paso 3. Especificaciones alternativas y selección del modelo

Tabla 37. Comparación de especificaciones · apartamentos de la zona sur
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.

Tabla 38. Efecto del estrato tratado como variable categórica
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.

Paso 4. Diagnóstico y validación de supuestos

Responde al punto 4 del enunciado.

Tabla 39. Validación de los supuestos del modelo · apartamentos de la zona sur
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 &#124;r&#124; > 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).

Tabla 40. Las tres observaciones más influyentes del modelo
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.

Tabla 41. Factores de inflación de varianza
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.

Sugerencias correctivas

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.

Sensibilidad al supuesto de imputación

Tabla 42. Efecto del supuesto de imputación sobre coeficientes y predicciones · apartamentos del sur
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
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.

Paso 5. Valoración de la vivienda solicitada

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.

Tabla 43. La solicitud 2 se ubica en el borde superior de la distribución
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:

Tabla 44. Apartamentos del sur que cumplen las condiciones de la solicitud 2
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.

Tabla 45. Precio estimado para la solicitud 2, en millones de pesos
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.

Paso 6. Ofertas disponibles en el mercado

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.

Tabla 46. Del presupuesto a las ofertas que cumplen la solicitud 2
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.

Tabla 47. Cinco ofertas sugeridas para la solicitud 2
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.

  • Guadalupe (573 m², E5, 730 M) y Seminario (300 m², E5, 670 M) son las dos que cumplen todo. Guadalupe casi duplica el área solicitada y ofrece 8 baños; Seminario reproduce el área exacta con 6 habitaciones y deja 180 millones de margen.
  • Las tres restantes se quedan cortas en parqueaderos, en área o en ambos. El Ingenio (600 m², 650 M) es la más llamativa: duplica con creces el área solicitada y a 1,08 millones por m² es la de mejor valor unitario de la lista, pero tiene 2 parqueaderos en lugar de 3. Si ese requisito es negociable, es la opción dominante del grupo.
  • Ciudadela Pasoancho (275 m²) y San Fernando (258 m²) ceden área y parqueaderos, a cambio de márgenes de 200 y 500 millones.

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:

Tabla 48. Efecto de flexibilizar el área y los parqueaderos
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.


Ubicación conjunta de las diez ofertas propuestas

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.

Anexo 4. Conclusiones técnicas detalladas

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.

Limitaciones del estudio

  • Son precios de oferta, no de transacción: léanse como techos de negociación.
  • La base no contiene antigüedad, estado, acabados ni tiempo en el mercado, que explican buena parte de la varianza no capturada.
  • La georreferenciación es imprecisa: la mitad de los barrios con oferta significativa presentan más de 10 km entre sus puntos extremos, una extensión poco compatible con la escala habitual de un barrio.
  • La solicitud 2 se apoya en muy pocos comparables, y por eso se reporta como rango y no como valor puntual.
  • Los métodos son asociativos, no causales.

Anexo 5. Código completo

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()

Anexo 6. Información de la sesión

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