1 Resumen ejecutivo

Una compañía internacional solicitó a C&A apoyo para ubicar a dos empleados con sus familias en Cali. La primera solicitud requiere una casa de 200 m² en la zona norte, con un crédito preaprobado de 350 millones. La segunda, un apartamento de 300 m² en la zona sur, con 850 millones.

Se estimaron dos modelos de regresión lineal múltiple, uno por segmento, sobre 494 casas y 2.195 apartamentos depurados de la base de ofertas disponible.

La segunda solicitud es viable. El apartamento solicitado se estima en 625,7 millones en estrato 5 y 777,6 millones en estrato 6, con márgenes de 224 y 72 millones sobre el crédito. El mercado ofrece cinco alternativas dentro del presupuesto, dos de las cuales cumplen la solicitud completa.

La primera solicitud está al límite. La casa se estima en 333,2 millones en estrato 4 y 383,2 millones en estrato 5. El primer escenario deja un margen de 17 millones sobre el crédito; el segundo lo excede en 33 millones. El error típico de predicción del modelo, de 85 millones, supera ese margen, de modo que la decisión debe apoyarse en las ofertas disponibles antes que en el valor estimado. El mercado ofrece diez casas que cumplen o superan lo solicitado por debajo de los 350 millones.

Los dos mercados valoran los mismos atributos de manera distinta, lo que justifica estimar un modelo por segmento. El metro cuadrado vale 1,27 millones en apartamentos del sur frente a 0,69 en casas del norte, y el parqueadero 54,30 millones frente a 15,79. En el segmento de apartamentos, un parqueadero adicional pesa en el precio tanto como 43 metros cuadrados de área construida.

El modelo de casas reproduce el 66,45 % de la variabilidad del precio y el de apartamentos el 78,34 %. Ambos cumplen los supuestos de linealidad y ausencia de multicolinealidad, e incumplen los de normalidad y varianza constante, lo que amplía la incertidumbre de las estimaciones sin alterar su valor central.

Los precios analizados corresponden a ofertas publicadas y no a operaciones cerradas. Las estimaciones deben leerse como referencia de negociación y no como valoración comercial del inmueble.

2 Objetivos

2.1 Objetivo general

Estimar el precio esperado de las dos viviendas solicitadas mediante modelos de regresión lineal múltiple ajustados por segmento de mercado, con el fin de establecer su viabilidad frente a los créditos preaprobados y sustentar la recomendación de compra que C&A debe presentar a su cliente.

2.2 Objetivos específicos

  • Construir las bases correspondientes a cada solicitud, depurando los registros inconsistentes y verificando la consistencia geográfica de la zona declarada.

  • Examinar la relación entre el precio de la vivienda y sus atributos físicos y de estrato en cada segmento de mercado.

  • Estimar un modelo de regresión lineal múltiple por segmento e interpretar los coeficientes estadísticamente significativos en el contexto del mercado inmobiliario de Cali.

  • Evaluar el cumplimiento de los supuestos del modelo lineal y determinar el alcance de las inferencias que admite cada estimación.

  • Predecir el precio de cada vivienda solicitada e identificar ofertas disponibles que respondan a los requerimientos dentro del crédito preaprobado.

3 Datos

3.1 Origen y alcance

El análisis se realiza sobre una base extraída mediante webscraping de OLX, plataforma en línea de anuncios de vivienda, distribuida en el paquete paqueteMODELOS. Contiene 8.322 ofertas de vivienda en Cali, descritas por 13 variables y georreferenciadas por longitud y latitud.

Conviene precisar qué tipo de información recogen estos registros. Se trata de inmuebles publicados y no de operaciones cerradas, de modo que preciom corresponde al valor que solicita el vendedor y no al precio en que finalmente se negocia la vivienda. Esta distinción condiciona el alcance de las estimaciones que siguen: los modelos permiten establecer cuánto se está pidiendo por un inmueble con determinadas características, no cuánto costará adquirirlo. El precio estimado para cada solicitud debe leerse entonces como referencia de negociación y no como valoración comercial del inmueble.

La georreferenciación se realiza a nivel de inmueble y no de barrio: los 8.322 registros presentan 5.914 pares distintos de coordenadas frente a 437 barrios. Esta precisión resulta relevante porque la ubicación se emplea más adelante para verificar la consistencia de la zona declarada en cada registro.

3.2 Diccionario de variables

A continuación se construye el diccionario de las variables que conforman la base, en el que se señala el tipo de dato, la escala de medición, la unidad en que está expresada, el número de valores distintos que toman y el conteo de registros faltantes. La escala de medición se incluye de manera explícita porque de ella depende el tratamiento estadístico que cada variable admite.

meta <- data.frame(
  Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst",
               "parqueaderos", "banios", "habitaciones", "tipo", "barrio",
               "longitud", "latitud"),
  Escala = c("Nominal (identificador)", "Nominal", "Ordinal", "Ordinal",
             "Razón (continua)", "Razón (continua)", "Razón (discreta)",
             "Razón (discreta)", "Razón (discreta)", "Nominal", "Nominal",
             "Intervalo (coordenada)", "Intervalo (coordenada)"),
  Unidad = c("—", "—", "n.º de piso", "categoría", "millones COP", "m²",
             "conteo", "conteo", "conteo", "—", "—",
             "grados decimales", "grados decimales"),
  Descripcion = c(
    "Identificador del registro",
    "Zona de la ciudad donde se ubica el inmueble",
    "Piso de ubicación del inmueble",
    "Estrato socioeconómico del predio",
    "Precio de oferta del inmueble",
    "Área construida",
    "Número de parqueaderos",
    "Número de baños",
    "Número de habitaciones",
    "Tipo de vivienda",
    "Barrio de ubicación",
    "Coordenada de longitud",
    "Coordenada de latitud")
)

# class(x)[1] toma solo la primera clase declarada: los tibbles importados
# arrastran varias clases por columna y sin el [1] la tabla saldría rota.
meta$`Tipo en R` <- sapply(vivienda[meta$Variable], function(x) class(x)[1])

# unique() lista los valores distintos; na.omit() los depura de faltantes
# antes de contarlos, para que NA no se cuente como un nivel más.
meta$Niveles   <- sapply(vivienda[meta$Variable], function(x) length(unique(na.omit(x))))
meta$Faltantes <- sapply(vivienda[meta$Variable], function(x) sum(is.na(x)))
meta$`% NA`    <- round(100 * meta$Faltantes / nrow(vivienda), 2)

diccionario <- meta[, c("Variable", "Tipo en R", "Escala", "Unidad",
                        "Niveles", "Faltantes", "% NA", "Descripcion")]

tabla(diccionario, caption = "Tabla 1. Diccionario de variables de la base vivienda")
Tabla 1. Diccionario de variables de la base vivienda
Variable Tipo en R Escala Unidad Niveles Faltantes % NA Descripcion
id numeric Nominal (identificador) 8,319 3 0.04 Identificador del registro
zona character Nominal 5 3 0.04 Zona de la ciudad donde se ubica el inmueble
piso character Ordinal n.º de piso 12 2,638 31.70 Piso de ubicación del inmueble
estrato numeric Ordinal categoría 4 3 0.04 Estrato socioeconómico del predio
preciom numeric Razón (continua) millones COP 539 2 0.02 Precio de oferta del inmueble
areaconst numeric Razón (continua) 652 3 0.04 Área construida
parqueaderos numeric Razón (discreta) conteo 10 1,605 19.29 Número de parqueaderos
banios numeric Razón (discreta) conteo 11 3 0.04 Número de baños
habitaciones numeric Razón (discreta) conteo 11 3 0.04 Número de habitaciones
tipo character Nominal 2 3 0.04 Tipo de vivienda
barrio character Nominal 436 3 0.04 Barrio de ubicación
longitud numeric Intervalo (coordenada) grados decimales 2,928 3 0.04 Coordenada de longitud
latitud numeric Intervalo (coordenada) grados decimales 3,679 3 0.04 Coordenada de latitud

La base reúne cinco variables cuantitativas —precio, área construida, parqueaderos, baños y habitaciones—, cuatro categóricas —zona, piso, estrato y tipo—, el barrio como identificador de localidad y dos coordenadas geográficas.

4 Preparación de los datos

4.1 Datos faltantes

La base presenta 3.514 registros con al menos un valor faltante, el 42,23 % del total. La cantidad de datos faltantes no permite que se tome en cuenta una eliminación por lista y obliga a examinar dónde se concentra la ausencia antes de decidir cómo tratarla.

faltantes <- data.frame(
  Variable  = names(vivienda),
  Faltantes = colSums(is.na(vivienda))
)

faltantes$Porcentaje <- round(100 * faltantes$Faltantes / nrow(vivienda), 2)

# Se ordena de mayor a menor para que las variables problemáticas encabecen
faltantes <- faltantes[order(-faltantes$Faltantes), ]
rownames(faltantes) <- NULL

tabla(faltantes, caption = "Tabla 2. Valores faltantes por variable")
Tabla 2. Valores faltantes por variable
Variable Faltantes Porcentaje
piso 2,638 31.70
parqueaderos 1,605 19.29
id 3 0.04
zona 3 0.04
estrato 3 0.04
areaconst 3 0.04
banios 3 0.04
habitaciones 3 0.04
tipo 3 0.04
barrio 3 0.04
longitud 3 0.04
latitud 3 0.04
preciom 2 0.02

Tal como se puede observar, la ausencia no se distribuye de manera uniforme. Se concentra en dos variables: piso, con 2.638 registros sin dato, y parqueaderos, con 1.605. Las once restantes presentan apenas tres faltantes cada una, salvo preciom con dos, cifras que corresponden a registros vacíos generados en la extracción y no a información no reportada.

Esta distribución justifica un tratamiento diferenciado por variable en lugar de realizar un tratamiento generalizado a toda la base, dado que cada una de las dos variables con ausencia masiva responde a una causa distinta y admite, por tanto, una decisión distinta.

4.2 Registros duplicados

Resulta necesario hacer una revisión de la base de datos para identificar los registros duplicados antes de modelar, pues un mismo anuncio capturado dos veces no constituye oferta adicional, pero sí duplica el peso de ese inmueble en la estimación de los coeficientes.

# Se excluye id de la comparación: al ser único por registro, impediría detectar
# un mismo inmueble capturado dos veces con identificadores distintos.
cols_inmueble <- setdiff(names(vivienda), "id")

# duplicated() marca cada fila que repite una combinación ya vista antes.
# Con fromLast = TRUE se marca también la primera aparición, de modo que
# la unión de ambas señala los grupos completos.
sobrantes <- duplicated(vivienda[, cols_inmueble]) & !is.na(vivienda$id)
grupos    <- (duplicated(vivienda[, cols_inmueble]) |
              duplicated(vivienda[, cols_inmueble], fromLast = TRUE)) &
              !is.na(vivienda$id)

muestra <- vivienda[grupos, c("id", "barrio", "tipo", "estrato", "preciom",
                              "areaconst", "banios", "habitaciones",
                              "longitud", "latitud")]
muestra <- head(muestra[order(muestra$barrio, muestra$id), ], 8)

tabla(muestra, caption = "Tabla 3. Primeros registros con coincidencia total de atributos")
Tabla 3. Primeros registros con coincidencia total de atributos
id barrio tipo estrato preciom areaconst banios habitaciones longitud latitud
64 2,013 acopi Casa 3 270 330 3 0 -76.52 3.45
65 2,014 acopi Casa 3 270 330 3 0 -76.52 3.45
559 4,825 bosques del limonar Apartamento 4 230 108 3 3 -76.53 3.41
560 4,826 bosques del limonar Apartamento 4 230 108 3 3 -76.53 3.41
632 869 brisas de los Apartamento 3 110 58 2 3 -76.50 3.49
633 870 brisas de los Apartamento 3 110 58 2 3 -76.50 3.49
1066 335 chiminangos Apartamento 3 87 62 1 3 -76.49 3.48
1067 336 chiminangos Apartamento 3 87 62 1 3 -76.49 3.48

La revisión identifica 57 registros que repiten en su totalidad los doce atributos que describen el inmueble, incluidas las coordenadas. Con esta coincidencia se podría suponer que los anuncios fueron capturados dos veces, o que los inmuebles son distintos pero con características idénticas, como por ejemplo, dos apartamentos del mismo tipo en una misma torre.

Dos elementos resuelven la ambigüedad. El primero es el orden de captura: los identificadores de cada par resultan contiguos, con una distancia mediana de 1 posición. Si fueran inmuebles distintos, sus identificadores no tendrían razón para quedar juntos. El segundo son las coordenadas: la base registra 5.914 ubicaciones distintas para 437 barrios, de modo que geolocaliza cada inmueble por separado. Dos viviendas diferentes no coincidirían hasta el quinto decimal.

Se eliminan entonces los 57 registros sobrantes, conservando el primero de cada grupo.

4.3 Registros inconsistentes

Dos revisiones adicionales se aplican antes de consolidar la base. La primera atañe a una variable que no participa del análisis; la segunda, a registros que no pueden corresponder a un inmueble real.

La variable piso presenta 2.638 faltantes, el 31,70 % de la base, y no forma parte de las variables explicativas que define el caso. Se retira la columna sin eliminar registros, de modo que los inmuebles sin ese dato conservan el resto de sus atributos.

Las variables banios y habitaciones registran el valor cero en 45 y 65 registros respectivamente. Un inmueble residencial no puede carecer de baño, de manera que ese cero corresponde a un campo sin diligenciar durante la extracción. En habitaciones el cero podría ser legítimo si se tratara de apartaestudios, por lo que se examina el detalle de esos registros.

# Base sin los duplicados sobrantes: el conteo se realiza sobre los registros
# que efectivamente permanecen tras la revisión anterior.
base_sd <- vivienda[!sobrantes, ]

# Registros que declaran cero habitaciones
sin_hab <- subset(base_sd, habitaciones == 0)

perfil_ceros <- data.frame(
  Criterio = c("Registros sin habitaciones",
               "Casas sin habitaciones",
               "Apartamentos sin habitaciones",
               "Registros sin habitaciones y sin baños",
               "Registros sin baños",
               "Total de registros afectados"),
  Registros = c(
    nrow(sin_hab),
    sum(sin_hab$tipo == "Casa", na.rm = TRUE),
    sum(sin_hab$tipo == "Apartamento", na.rm = TRUE),
    sum(sin_hab$banios == 0, na.rm = TRUE),
    sum(base_sd$banios == 0, na.rm = TRUE),
    sum(base_sd$banios == 0 | base_sd$habitaciones == 0, na.rm = TRUE)
  )
)

tabla(perfil_ceros, caption = "Tabla 4. Perfil de los registros")
Tabla 4. Perfil de los registros
Criterio Registros
Registros sin habitaciones 65
Casas sin habitaciones 44
Apartamentos sin habitaciones 21
Registros sin habitaciones y sin baños 35
Registros sin baños 45
Total de registros afectados 75

La Tabla 4 descarta la hipótesis del apartaestudio. De los 65 registros sin habitaciones, 44 corresponden a casas y 35 tampoco registran baños. Una vivienda real no puede carecer de habitaciones y de baños de manera simultánea, de modo que el cero refleja un campo vacío y no un atributo del inmueble.

Se eliminan los 75 registros que presentan cero en cualquiera de las dos variables.

4.4 Tipificación de variables

Antes de estimar los modelos, las variables categóricas se convierten en factores. En particular, la variable estrato, registrada inicialmente como un número entero, se trata como una variable categórica.

Tratar el estrato como variable cuantitativa supone que el efecto sobre el precio es el mismo entre niveles consecutivos (por ejemplo, de estrato 3 a 4 y de 5 a 6). En cambio, considerarlo como factor permite estimar un efecto diferente para cada categoría, aunque requiere tres parámetros adicionales.

frec_estrato <- as.data.frame.matrix(
  table(base_sd$estrato,
        interaction(base_sd$tipo, base_sd$zona, sep = " — "))
)

frec_estrato <- frec_estrato[, c("Casa — Zona Norte", "Apartamento — Zona Sur")]
frec_estrato <- cbind(Estrato = rownames(frec_estrato), frec_estrato)
rownames(frec_estrato) <- NULL

tabla(frec_estrato, caption = "Tabla 5. Distribución del estrato en los segmentos de análisis")
Tabla 5. Distribución del estrato en los segmentos de análisis
Estrato Casa — Zona Norte Apartamento — Zona Sur
3 234 200
4 160 1,075
5 268 1,024
6 55 461

La distribución respalda el uso del estrato 3 como categoría de referencia: reúne 234 casas en la zona norte y 200 apartamentos en la zona sur, lo que proporciona un número suficiente de observaciones para comparar las demás categorías. Los coeficientes se interpretan como la diferencia esperada de precio respecto a un inmueble de estrato 3 con iguales atributos físicos.

4.5 Base depurada

Las decisiones anteriores se aplican de manera secuencial en un único proceso de depuración, con el propósito de evitar la eliminación doble de un mismo registro. En primer lugar, se identifican y eliminan los registros duplicados y, posteriormente, se revisan y excluyen los valores que no son válidos o posibles. De esta manera, un anuncio que se encuentre duplicado y que, además, presente un dato inválido, se contabiliza y elimina una sola vez.

vivienda_dep <- vivienda

# 1. Registros sin información: filas generadas al cerrar el archivo de extracción
n_vacias <- sum(rowSums(is.na(vivienda_dep)) >= 10)
vivienda_dep <- vivienda_dep[rowSums(is.na(vivienda_dep)) < 10, ]

# 2. Duplicados: se conserva la primera aparición de cada grupo
cols_inmueble <- setdiff(names(vivienda_dep), "id")
dup_sobrantes <- duplicated(vivienda_dep[, cols_inmueble])
n_dup <- sum(dup_sobrantes)
vivienda_dep <- vivienda_dep[!dup_sobrantes, ]

# 3. Valores imposibles en banios o habitaciones
imposibles <- vivienda_dep$banios == 0 | vivienda_dep$habitaciones == 0
n_ceros <- sum(imposibles, na.rm = TRUE)
vivienda_dep <- vivienda_dep[!imposibles, ]

# 4. Ausencia de parqueadero: el campo vacío representa el valor cero
vivienda_dep$parqueaderos[is.na(vivienda_dep$parqueaderos)] <- 0

# 5. Se retira piso, que no participa del modelo
vivienda_dep$piso <- NULL

# 6. Declaración de factores. relevel() fija el estrato 3 como referencia.
vivienda_dep$estrato <- relevel(factor(vivienda_dep$estrato), ref = "3")
vivienda_dep$zona    <- factor(vivienda_dep$zona)
vivienda_dep$tipo    <- factor(vivienda_dep$tipo)
vivienda_dep$barrio  <- factor(vivienda_dep$barrio)

balance <- data.frame(
  Concepto = c("Registros iniciales",
               "Eliminados por ausencia de información",
               "Eliminados por duplicación",
               "Eliminados por valores imposibles",
               "Registros finales"),
  Registros = c(nrow(vivienda), n_vacias, n_dup, n_ceros, nrow(vivienda_dep))
)

balance$Porcentaje <- round(100 * balance$Registros / nrow(vivienda), 2)

tabla(balance, caption = "Tabla 6. Balance de la depuración")
Tabla 6. Balance de la depuración
Concepto Registros Porcentaje
Registros iniciales 8,322 100.00
Eliminados por ausencia de información 3 0.04
Eliminados por duplicación 57 0.68
Eliminados por valores imposibles 75 0.90
Registros finales 8,187 98.38

El manejo diferenciado por variable permite conservar 8.187 registros, el 98,38 % de la base de datos original. Los registros que fueron excluidos se limitan a los 3 registros generados en la extracción, los 57 anuncios capturados dos veces y los 75 que declaran cero baños o cero habitaciones.

Las dos variables con mayor cantidad de datos faltantes no generaron pérdida de registros. La columna piso se retiró sin eliminar observaciones, por lo que los 2.638 inmuebles que no contaban con este dato conservaron el resto de sus atributos. Por su parte, los 1.605 valores faltantes de parqueaderos se recodificaron como cero. Esta decisión se fundamenta en que la variable presenta valores entre 1 y 10 y el cero no aparece en los registros con información, lo que sugiere que los campos vacíos corresponden a inmuebles sin parqueadero registrado. Este tratamiento constituye un supuesto basado en la codificación observada y no una condición verificada directamente.

La eliminación por lista habría descartado el 42,23 % de la base.

5 Construcción de las bases de análisis

Cada solicitud del presente caso corresponde a un segmento diferente del mercado. La primera se enfoca en casas ubicadas en la zona norte y la segunda en apartamentos de la zona sur. Por esta razón, el análisis se realiza sobre dos bases de datos independientes, en lugar de utilizar un único modelo con variables indicadoras. Esta separación permite evaluar cómo los atributos físicos influyen de manera diferente en cada segmento, considerando que las casas y los apartamentos pueden presentar dinámicas de valoración distintas.

5.1 Base 1 — Casas de la zona norte

base1 <- subset(vivienda_dep, tipo == "Casa" & zona == "Zona Norte")

tabla(head(base1[, c("id", "barrio", "estrato", "preciom", "areaconst",
                     "parqueaderos", "banios", "habitaciones")], 3),
      caption = "Tabla 7. Primeros tres registros de la base 1")
Tabla 7. Primeros tres registros de la base 1
id barrio estrato preciom areaconst parqueaderos banios habitaciones
9 1,209 acopi 5 320 150 2 4 6
10 1,592 acopi 5 780 380 2 3 3
11 4,057 acopi 6 750 445 0 7 6
verif1 <- as.data.frame(table(base1$tipo, base1$zona))
names(verif1) <- c("Tipo", "Zona", "Registros")

tabla(subset(verif1, Registros > 0),
      caption = "Tabla 8. Verificación del filtro de la base 1")
Tabla 8. Verificación del filtro de la base 1
Tipo Zona Registros
4 Casa Zona Norte 696

El filtro devuelve 696 registros, todos correspondientes a la combinación casa y zona norte, sin mezcla de otros tipos o zonas.

5.2 Base 2 — Apartamentos de la zona sur

base2 <- subset(vivienda_dep, tipo == "Apartamento" & zona == "Zona Sur")

tabla(head(base2[, c("id", "barrio", "estrato", "preciom", "areaconst",
                     "parqueaderos", "banios", "habitaciones")], 3),
      caption = "Tabla 9. Primeros tres registros de la base 2")
Tabla 9. Primeros tres registros de la base 2
id barrio estrato preciom areaconst parqueaderos banios habitaciones
24 5,098 acopi 4 290 96 1 2 3
164 698 aguablanca 3 78 40 1 1 2
264 8,199 aguacatal 6 875 194 2 5 3
verif2 <- as.data.frame(table(base2$tipo, base2$zona))
names(verif2) <- c("Tipo", "Zona", "Registros")

tabla(subset(verif2, Registros > 0),
      caption = "Tabla 10. Verificación del filtro de la base 2")
Tabla 10. Verificación del filtro de la base 2
Tipo Zona Registros
9 Apartamento Zona Sur 2,750

El filtro aplicado devuelve 2.750 registros, correspondientes en su totalidad a inmuebles con características de apartamento ubicados en la zona sur. Esta base es aproximadamente cuatro veces mayor que la del primer segmento, diferencia que se tendrá en cuenta posteriormente al comparar la precisión y el desempeño de ambos modelos.

5.3 Consistencia geográfica

El enunciado advierte que los puntos de cada base pueden no ubicarse en la zona que declaran. Esta verificación es pertinente porque el análisis se realiza por segmentos y una mezcla de zonas podría afectar la interpretación de los coeficientes.

La revisión se realiza mediante la georreferenciación. Cada registro se compara con el centroide de las cinco zonas, calculado mediante la mediana de longitud y latitud de la base completa, y se asigna a la zona cuyo centroide presenta la menor distancia euclidiana.

\[d_{ij} = \sqrt{(\text{lon}_i - \text{lon}_j)^2 + (\text{lat}_i - \text{lat}_j)^2}\]

donde el subíndice \(i\) corresponde al inmueble y el subíndice \(j\) al centroide de cada zona. Cada registro se asigna a la zona que minimiza esa distancia.

centroides <- aggregate(cbind(longitud, latitud) ~ zona,
                        data = vivienda_dep, FUN = median)

tabla(centroides, caption = "Tabla 11. Centroide de cada zona de la ciudad", digits = 4)
Tabla 11. Centroide de cada zona de la ciudad
zona longitud latitud
Zona Centro -76.5285 3.4400
Zona Norte -76.5200 3.4729
Zona Oeste -76.5486 3.4490
Zona Oriente -76.5069 3.4380
Zona Sur -76.5310 3.3846
# max.col(-d) devuelve, por fila, la columna con el valor mínimo:
# la zona cuyo centroide queda más cerca del registro.
zona_cercana <- function(df) {
  d <- sapply(seq_len(nrow(centroides)), function(k)
    (df$longitud - centroides$longitud[k])^2 +
    (df$latitud  - centroides$latitud[k])^2)
  centroides$zona[max.col(-d)]
}

base1$zona_geo <- zona_cercana(base1)
base2$zona_geo <- zona_cercana(base2)

cruce <- merge(as.data.frame(table(base1$zona_geo)),
               as.data.frame(table(base2$zona_geo)),
               by = "Var1", all = TRUE)
names(cruce) <- c("Zona más cercana", "Base 1 — Casas norte", "Base 2 — Aptos sur")

tabla(cruce, caption = "Tabla 12. Zona geográficamente más cercana a cada registro")
Tabla 12. Zona geográficamente más cercana a cada registro
Zona más cercana Base 1 — Casas norte Base 2 — Aptos sur
Zona Centro 72 299
Zona Norte 494 63
Zona Oeste 38 91
Zona Oriente 31 102
Zona Sur 61 2,195

El diagnóstico revela inconsistencias en las dos bases. En la base 1, 494 de 696 casas resultan más próximas al centroide de la zona norte; las 202 restantes se distribuyen hacia el centro (72), el sur (61), el oeste (38) y el oriente (31). En la base 2, 2.195 de 2.750 apartamentos confirman su ubicación en el sur, y los 555 restantes se orientan principalmente hacia el centro (299), el oriente (102) y el oeste (91).

La proporción de inconsistencias difiere entre segmentos: 29,0 % en la base 1 y 20,2 % en la base 2. Esta diferencia podría explicarse por la distribución geográfica de las zonas, ya que la zona norte es más estrecha y limita con el centro y el occidente, mientras que la zona sur es más extensa y compacta.

paleta <- function(x) ifelse(x, AZUL, NARANJA)

mapa1 <- leaflet(base1) |>
  addProviderTiles("CartoDB.Positron") |>
  addCircleMarkers(~longitud, ~latitud, radius = 4, stroke = FALSE,
                   fillOpacity = 0.7,
                   color = ~paleta(zona_geo == "Zona Norte"),
                   popup = ~paste0(barrio, "<br>", preciom, " millones")) |>
  addLegend(colors = c(AZUL, NARANJA), labels = c("Consistente", "Inconsistente"),
            title = "Base 1 — Casas norte", position = "bottomright")

mapa1
mapa2 <- leaflet(base2) |>
  addProviderTiles("CartoDB.Positron") |>
  addCircleMarkers(~longitud, ~latitud, radius = 3, stroke = FALSE,
                   fillOpacity = 0.6,
                   color = ~paleta(zona_geo == "Zona Sur"),
                   popup = ~paste0(barrio, "<br>", preciom, " millones")) |>
  addLegend(colors = c(AZUL, NARANJA), labels = c("Consistente", "Inconsistente"),
            title = "Base 2 — Apartamentos sur", position = "bottomright")

mapa2

Los mapas muestran que los registros inconsistentes se encuentran dispersos por la ciudad, lo que sugiere un posible error de codificación en los anuncios originales, más que un problema en los límites de las zonas.

El precio también evidencia que esta mezcla afecta el análisis. En la base 1, las casas inconsistentes presentan un precio mediano de 450 millones, frente a 360 millones en las consistentes. En la base 2, los apartamentos inconsistentes tienen una mediana de 230 millones, frente a 250 millones en los consistentes. Por tanto, conservar estos registros podría afectar la estimación de los precios en ambos segmentos.

base1_f <- subset(base1, zona_geo == "Zona Norte")
base2_f <- subset(base2, zona_geo == "Zona Sur")

balance_geo <- data.frame(
  Base = c("Base 1 — Casas norte", "Base 2 — Apartamentos sur"),
  Registros_iniciales = c(nrow(base1), nrow(base2)),
  Excluidos = c(nrow(base1) - nrow(base1_f), nrow(base2) - nrow(base2_f)),
  Registros_finales = c(nrow(base1_f), nrow(base2_f))
)

balance_geo$Retención <- round(100 * balance_geo$Registros_finales /
                                 balance_geo$Registros_iniciales, 2)

tabla(balance_geo, caption = "Tabla 13. Balance del filtro geográfico")
Tabla 13. Balance del filtro geográfico
Base Registros_iniciales Excluidos Registros_finales Retención
Base 1 — Casas norte 696 202 494 70.98
Base 2 — Apartamentos sur 2,750 555 2,195 79.82

Se excluyen los registros cuya ubicación no corresponde con la zona declarada. Así, la base 1 queda con 494 casas y la base 2 con 2.195 apartamentos.

Como criterio alternativo, se evaluó un análisis de conglomerados sobre longitud y latitud. En la base 1, identificó 505 casas en el sector norte, cifra cercana a las 494 obtenidas mediante el centroide, lo que respalda la delimitación adoptada.

Se mantiene el criterio del centroide porque permite comparar cada registro con las cinco zonas de la ciudad y evita la división de la zona sur observada en los conglomerados. Este procedimiento se interpreta como un diagnóstico de consistencia espacial, no como una determinación definitiva de pertenencia territorial.

6 Análisis descriptivo

6.1 Normalidad de las variables cuantitativas

El coeficiente de Pearson es sensible a la asimetría y a los valores extremos, condiciones que conviene verificar antes de emplearlo. Se aplica la prueba de Shapiro-Wilk sobre las cinco variables cuantitativas de cada base, bajo las hipótesis:

\[H_0: X \sim N(\mu, \sigma^2) \qquad \text{frente a} \qquad H_1: X \nsim N(\mu, \sigma^2)\]

vars_num <- c("preciom", "areaconst", "banios", "habitaciones", "parqueaderos")

# formatC con format = "e" fuerza notación científica y evita que el redondeo
# de kable colapse a cero los valores muy pequeños.
p_base1 <- sapply(base1_f[, vars_num], function(x) shapiro.test(x)$p.value)
p_base2 <- sapply(base2_f[, vars_num], function(x) shapiro.test(x)$p.value)

normalidad <- data.frame(
  Variable = vars_num,
  `Base 1`  = formatC(p_base1, format = "e", digits = 2),
  `Base 2`  = formatC(p_base2, format = "e", digits = 2),
  check.names = FALSE
)
rownames(normalidad) <- NULL

tabla(normalidad, caption = "Tabla 14. Valores p de la prueba de Shapiro-Wilk")
Tabla 14. Valores p de la prueba de Shapiro-Wilk
Variable Base 1 Base 2
preciom 4.43e-19 2.73e-49
areaconst 1.63e-21 2.78e-54
banios 2.17e-16 9.51e-47
habitaciones 2.92e-21 3.61e-49
parqueaderos 4.46e-23 4.53e-44

Con \(\alpha = 0{,}05\) se rechaza la normalidad en las diez pruebas. El valor p más alto corresponde a banios en casas de la zona norte, con \(2{,}17 \times 10^{-16}\).

Se reporta en consecuencia el coeficiente de Spearman, que opera sobre rangos y resulta menos sensible a los valores extremos presentes en la base. El de Pearson se incluye como referencia.

6.2 Correlación entre las variables

etiquetas <- c("Precio", "Área", "Baños", "Habitaciones", "Parqueaderos")

m1 <- cor(base1_f[, vars_num], method = "spearman")
m2 <- cor(base2_f[, vars_num], method = "spearman")
dimnames(m1) <- dimnames(m2) <- list(etiquetas, etiquetas)

# Dibuja la matriz como mapa de calor con el coeficiente sobre cada celda
mapa_cor <- function(m, titulo) {
  plot_ly(x = colnames(m), y = rownames(m), z = m,
          type = "heatmap",
          colorscale = list(c(0, "#FFFFFF"), c(1, AZUL)),
          zmin = 0, zmax = 1,
          hovertemplate = "%{y} — %{x}<br>Correlación: %{z:.3f}<extra></extra>") |>
    add_annotations(
      x = rep(colnames(m), each = ncol(m)),
      y = rep(rownames(m), times = ncol(m)),
      text = sprintf("%.2f", as.vector(m)),
      showarrow = FALSE,
      font = list(size = 12, color = "black")) |>
    layout(title = titulo,
           xaxis = list(title = "", side = "bottom"),
           yaxis = list(title = "", autorange = "reversed"))
}

mapa_cor(m1, "Figura 1. Correlación de Spearman — Casas de la zona norte")
mapa_cor(m2, "Figura 2. Correlación de Spearman — Apartamentos de la zona sur")

El área construida presenta la asociación más fuerte con el precio en ambos segmentos: 0,852 en casas de la zona norte y 0,873 en apartamentos de la zona sur. En casas le siguen los baños, con 0,643. En apartamentos, baños y parqueaderos se ubican al mismo nivel, ambos en torno a 0,72.

Las habitaciones registran la correlación más baja de las cuatro variables: 0,450 en casas y 0,404 en apartamentos. El número de habitaciones resulta entonces el atributo que menos ordena el precio dentro de cada segmento.

# Contraste entre ambos coeficientes en la correlación con el precio
contraste <- data.frame(
  Variable = c("Área construida", "Baños", "Habitaciones", "Parqueaderos"),
  `Spearman` = round(cor(base1_f[, vars_num], method = "spearman")[-1, 1], 3),
  `Pearson`  = round(cor(base1_f[, vars_num], method = "pearson")[-1, 1], 3),
  check.names = FALSE
)
contraste$Diferencia <- round(contraste$Spearman - contraste$Pearson, 3)
rownames(contraste) <- NULL

tabla(contraste,
      caption = "Tabla 15. Correlación con el precio según coeficiente — Casas de la zona norte",
      digits = 3)
Tabla 15. Correlación con el precio según coeficiente — Casas de la zona norte
Variable Spearman Pearson Diferencia
Área construida 0.852 0.735 0.117
Baños 0.643 0.551 0.092
Habitaciones 0.450 0.396 0.054
Parqueaderos 0.563 0.486 0.077

La comparación con el coeficiente de Pearson muestra una relación más fuerte cuando se considera el orden de los valores que cuando se evalúa una relación estrictamente lineal. Para el área construida de las casas, Spearman alcanza 0,852, frente a 0,735 de Pearson. Esto indica que el precio tiende a aumentar con el área, aunque no a un ritmo constante, aspecto que se retoma al analizar el ajuste del modelo.

Las matrices muestran asociaciones de distinta intensidad entre las variables explicativas. En los apartamentos de la zona sur, el área construida presenta correlaciones de 0,771 con el número de baños y de 0,694 con el número de parqueaderos. En las casas de la zona norte, la mayor correlación es de 0,605, entre área construida y baños.

6.3 Relación entre el precio y los atributos del inmueble

A continuación se examina cómo varía el precio según el área construida, el estrato y la zona de ubicación.

plot_ly(base1_f,
        x = ~areaconst, y = ~preciom, color = ~estrato,
        colors = c(GRIS, ACENTO, AZUL, NARANJA),
        type = "scatter", mode = "markers",
        marker = list(size = 7, opacity = 0.6),
        hovertemplate = paste0("Área: %{x} m²<br>",
                               "Precio: %{y} millones<extra></extra>")) |>
  layout(title = "Figura 3. Precio según área construida — Casas de la zona norte",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones de pesos)"),
         legend = list(title = list(text = "Estrato")))
plot_ly(base2_f,
        x = ~areaconst, y = ~preciom, color = ~estrato,
        colors = c(GRIS, ACENTO, AZUL, NARANJA),
        type = "scatter", mode = "markers",
        marker = list(size = 5, opacity = 0.5),
        hovertemplate = paste0("Área: %{x} m²<br>",
                               "Precio: %{y} millones<extra></extra>")) |>
  layout(title = "Figura 4. Precio según área construida — Apartamentos de la zona sur",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones de pesos)"),
         legend = list(title = list(text = "Estrato")))

Tal como se puede observar, la dispersión se amplía a medida que crece el área construida. En casas de la zona norte, los inmuebles menores de 150 m² se concentran entre 100 y 300 millones, mientras que por encima de 200 m² los precios se extienden desde 400 hasta más de 1.400 millones. El mismo patrón aparece en apartamentos de la zona sur.

plot_ly(base1_f, x = ~estrato, y = ~preciom, color = ~estrato,
        colors = c(GRIS, ACENTO, AZUL, NARANJA),
        type = "box", boxpoints = "outliers") |>
  layout(title = "Figura 5. Precio según estrato — Casas de la zona norte",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio (millones de pesos)"),
         showlegend = FALSE)
plot_ly(base2_f, x = ~estrato, y = ~preciom, color = ~estrato,
        colors = c(GRIS, ACENTO, AZUL, NARANJA),
        type = "box", boxpoints = "outliers") |>
  layout(title = "Figura 6. Precio según estrato — Apartamentos de la zona sur",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio (millones de pesos)"),
         showlegend = FALSE)
resumen_est <- function(df, etiqueta) {
  do.call(rbind, lapply(levels(df$estrato), function(e) {
    x <- df$preciom[df$estrato == e]
    data.frame(
      Segmento = etiqueta,
      Estrato  = e,
      n        = length(x),
      Media    = mean(x),
      Mediana  = median(x),
      RIC      = IQR(x),                  # rango intercuartílico: Q3 - Q1
      `CV (%)` = 100 * sd(x) / mean(x),   # dispersión relativa al promedio
      check.names = FALSE
    )
  }))
}

descriptivos <- rbind(resumen_est(base1_f, "Casas norte"),
                      resumen_est(base2_f, "Aptos sur"))
rownames(descriptivos) <- NULL

tabla(descriptivos,
      caption = "Tabla 16. Estadísticos del precio por estrato en cada segmento",
      digits = 1)
Tabla 16. Estadísticos del precio por estrato en cada segmento
Segmento Estrato n Media Mediana RIC CV (%)
Casas norte 3 171 226.8 200 115 44.2
Casas norte 4 107 415.8 380 240 47.4
Casas norte 5 199 519.1 470 230 40.6
Casas norte 6 17 733.5 690 320 34.7
Aptos sur 3 136 138.7 125 35 44.7
Aptos sur 4 802 200.8 185 83 31.4
Aptos sur 5 854 292.1 280 100 34.8
Aptos sur 6 403 590.4 570 300 44.5

El precio aumenta con el estrato en los dos segmentos, aunque no de manera uniforme. En casas de la zona norte la mediana pasa de 200 a 380, 470 y 690 millones entre los estratos 3 y 6, con incrementos sucesivos de 180, 90 y 220 millones. En apartamentos de la zona sur la progresión va de 125 a 185, 280 y 570 millones, con un salto final de 290 millones que supera a los dos anteriores sumados.

La irregularidad de estos incrementos sustenta el tratamiento del estrato como variable categórica. Incorporarlo como variable numérica impondría un aumento constante entre categorías consecutivas, supuesto que los datos no respaldan.

La media supera a la mediana en las ocho combinaciones de segmento y estrato, lo que revela asimetría positiva dentro de cada grupo. La amplitud de las cajas también crece con el estrato: en casas, el rango intercuartílico pasa de 115 millones en el estrato 3 a 320 en el estrato 6.

El estrato 6 de casas reúne únicamente 17 registros, frente a los 403 del mismo estrato en apartamentos.

# Se examina sobre la base completa, dado que dentro de cada segmento
# la zona es constante y no presenta variabilidad.
plot_ly(vivienda_dep, x = ~zona, y = ~preciom, color = ~tipo,
        colors = c(ACENTO, AZUL),
        type = "box", boxpoints = FALSE) |>
  layout(title = "Figura 7. Precio según zona y tipo de vivienda",
         xaxis = list(title = "Zona"),
         yaxis = list(title = "Precio (millones de pesos)"),
         boxmode = "group",
         legend = list(title = list(text = "Tipo")))

La Figura 7 permite evidenciar lo siguiente:

  • La zona oeste concentra los valores más altos de la ciudad, con medianas de 570 millones en apartamentos y 680 en casas.
  • La zona oriente registra los más bajos, con 114 y 234 millones respectivamente.
  • Las casas superan a los apartamentos en las cinco zonas, y la brecha entre ambos tipos se amplía en los sectores de mayor valorización.
  • El rango de medianas de la ciudad va de 114 a 680 millones, una variación que multiplica por seis el valor del inmueble según su localización.
# Se combinan ambas variables en un solo gráfico con selector, para cubrir
# baños y habitaciones sin multiplicar figuras.
p1 <- plot_ly(base1_f, x = ~banios, y = ~preciom,
              type = "scatter", mode = "markers",
              marker = list(size = 7, opacity = 0.5, color = AZUL),
              name = "Baños",
              hovertemplate = "Baños: %{x}<br>Precio: %{y} millones<extra></extra>")

p2 <- plot_ly(base1_f, x = ~habitaciones, y = ~preciom,
              type = "scatter", mode = "markers",
              marker = list(size = 7, opacity = 0.5, color = ACENTO),
              name = "Habitaciones",
              hovertemplate = "Habitaciones: %{x}<br>Precio: %{y} millones<extra></extra>")

subplot(p1, p2, nrows = 1, shareY = TRUE, titleX = TRUE) |>
  layout(title = "Figura 8. Precio según número de baños y habitaciones — Casas de la zona norte",
         yaxis = list(title = "Precio (millones de pesos)"),
         xaxis  = list(title = "Baños"),
         xaxis2 = list(title = "Habitaciones"))
p3 <- plot_ly(base2_f, x = ~banios, y = ~preciom,
              type = "scatter", mode = "markers",
              marker = list(size = 5, opacity = 0.4, color = AZUL),
              name = "Baños",
              hovertemplate = "Baños: %{x}<br>Precio: %{y} millones<extra></extra>")

p4 <- plot_ly(base2_f, x = ~habitaciones, y = ~preciom,
              type = "scatter", mode = "markers",
              marker = list(size = 5, opacity = 0.4, color = ACENTO),
              name = "Habitaciones",
              hovertemplate = "Habitaciones: %{x}<br>Precio: %{y} millones<extra></extra>")

subplot(p3, p4, nrows = 1, shareY = TRUE, titleX = TRUE) |>
  layout(title = "Figura 9. Precio según número de baños y habitaciones — Apartamentos de la zona sur",
         yaxis = list(title = "Precio (millones de pesos)"),
         xaxis  = list(title = "Baños"),
         xaxis2 = list(title = "Habitaciones"))

El comportamiento de estas dos variables difiere de manera notoria:

  • El número de baños ordena el precio con mayor claridad que el de habitaciones en ambos segmentos, coherente con las correlaciones de 0,643 y 0,715 frente a 0,450 y 0,404.
  • En apartamentos de la zona sur, la relación entre habitaciones y precio se aplana a partir de las cuatro habitaciones, e incluso desciende en los valores superiores.
  • La dispersión dentro de cada valor supera con frecuencia la diferencia entre valores contiguos: una casa de tres baños puede costar entre 130 y 1.000 millones.

El comportamiento del número de habitaciones anticipa una dificultad para el modelo. Controlando por área construida, un mayor número de habitaciones implica repartir la misma superficie en espacios más pequeños, condición que no necesariamente incrementa el valor del inmueble.

7 Modelo de regresión lineal múltiple

7.1 Especificación del modelo

Se estima un modelo independiente para cada segmento, con el precio como variable respuesta y los cinco atributos que define el caso como variables explicativas: área construida, estrato, habitaciones, parqueaderos y baños.

El estrato se incorpora como variable categórica y aporta tres términos al modelo, uno por cada categoría distinta del estrato 3, que opera como referencia. Cada modelo estima entonces ocho parámetros.

En notación matricial, el modelo se expresa como:

\[\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\varepsilon}\]

donde \(\mathbf{y}\) reúne los precios observados, \(\mathbf{X}\) contiene una primera columna de unos —asociada al intercepto— y una columna por cada variable explicativa, \(\boldsymbol{\beta}\) es el vector de parámetros por estimar y \(\boldsymbol{\varepsilon}\) el vector de errores.

La estimación se realiza por mínimos cuadrados ordinarios. El criterio busca los valores de \(\boldsymbol{\beta}\) que minimizan la suma de los cuadrados de las diferencias entre el precio observado y el estimado:

\[S(\boldsymbol{\beta}) = (\mathbf{y} - \mathbf{X}\boldsymbol{\beta})'(\mathbf{y} - \mathbf{X}\boldsymbol{\beta})\]

Al derivar respecto a \(\boldsymbol{\beta}\) e igualar a cero se obtienen las ecuaciones normales, cuya solución es:

\[\hat{\boldsymbol{\beta}} = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{y}\]

Cada coeficiente estimado representa el cambio esperado en el precio ante el incremento de una unidad en la variable correspondiente, manteniendo constantes las demás. Esta condición resulta relevante en el presente caso, porque varios atributos del inmueble crecen conjuntamente y su efecto aislado no coincide necesariamente con la asociación simple observada en la sección anterior.

7.2 Modelo 1 — Casas de la zona norte

modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
              data = base1_f)

# tidy() convierte la salida de lm() en tabla; conf.int agrega los intervalos
coef1 <- broom::tidy(modelo1, conf.int = TRUE)

coef1$term <- c("Intercepto", "Área construida", "Estrato 4", "Estrato 5",
                "Estrato 6", "Habitaciones", "Parqueaderos", "Baños")

names(coef1) <- c("Término", "Coeficiente", "Error estándar", "Estadístico t",
                  "Valor p", "LI 95%", "LS 95%")

tabla(coef1, caption = "Tabla 17. Coeficientes estimados — Casas de la zona norte", digits = 3)
Tabla 17. Coeficientes estimados — Casas de la zona norte
Término Coeficiente Error estándar Estadístico t Valor p LI 95% LS 95%
Intercepto 34.085 19.411 1.756 0.080 -4.054 72.224
Área construida 0.692 0.051 13.621 0.000 0.592 0.792
Estrato 4 77.042 18.120 4.252 0.000 41.438 112.645
Estrato 5 126.971 17.695 7.176 0.000 92.203 161.738
Estrato 6 280.704 37.105 7.565 0.000 207.798 353.610
Habitaciones 8.075 4.907 1.645 0.101 -1.567 17.717
Parqueaderos 15.791 4.504 3.506 0.000 6.942 24.640
Baños 17.837 6.248 2.855 0.004 5.560 30.113

La ecuación estimada es:

\[\widehat{\text{Precio}} = 34{,}09 + 0{,}69\,\text{Área} + 77{,}04\,\text{E4} + 126{,}97\,\text{E5} + 280{,}70\,\text{E6} + 8{,}08\,\text{Hab} + 15{,}79\,\text{Parq} + 17{,}84\,\text{Baños}\]

Para cada coeficiente se contrasta \(H_0: \beta_j = 0\) frente a \(H_1: \beta_j \neq 0\), con un nivel de significancia de 0,05. Seis de los ocho términos resultan significativos.

El área construida aporta 0,69 millones por metro cuadrado adicional, manteniendo constantes las demás características. Una casa de 200 m² vale así 69 millones más que una de 100 m² con igual estrato, número de habitaciones, parqueaderos y baños.

El estrato produce los efectos de mayor magnitud. Manteniendo constantes las demás características, una casa de estrato 4 se valora en 77,04 millones más que una de estrato 3; una de estrato 5 en 126,97 millones y una de estrato 6 en 280,70 millones. Los incrementos entre categorías consecutivas resultan desiguales —50 millones del estrato 4 al 5 y 154 del 5 al 6—, lo que confirma que el estrato no opera como una escala de intervalos regulares.

Los parqueaderos suman 15,79 millones por unidad adicional y los baños 17,84 millones, en ambos casos manteniendo constantes las demás características. Los dos efectos son positivos y coherentes con la valoración esperada del mercado.

Las habitaciones no alcanzan significancia estadística, con un valor p de 0,101. Su intervalo de confianza va de −1,57 a 17,72 millones e incluye el cero, de modo que los datos no permiten distinguir su efecto del azar. El resultado no implica que el número de habitaciones sea irrelevante para el comprador, sino que, una vez consideradas el área construida y las demás características, no aporta información adicional sobre el precio en este segmento.

El intercepto tampoco resulta significativo, con un valor p de 0,080. Su lectura directa correspondería al precio de una casa de estrato 3 con área cero y sin habitaciones, baños ni parqueaderos, combinación que la base no contiene: los registros con cero baños o cero habitaciones fueron excluidos durante la depuración. El término carece por tanto de interpretación práctica y opera como ajuste de nivel de la ecuación.

7.2.1 Ajuste del modelo

ajuste1 <- broom::glance(modelo1)[, c("r.squared", "adj.r.squared", "sigma",
                                      "statistic", "nobs")]

names(ajuste1) <- c("R²", "R² ajustado", "Error residual", "Estadístico F", "n")

tabla(ajuste1, caption = "Tabla 18. Indicadores de ajuste — Casas de la zona norte", digits = 4)
Tabla 18. Indicadores de ajuste — Casas de la zona norte
R² ajustado Error residual Estadístico F n
0.6645 0.6597 132.7588 137.5119 494

La prueba F contrasta si el conjunto de variables aporta información sobre el precio.

\[H_0: \beta_1 = \beta_2 = \cdots = \beta_7 = 0 \qquad \text{frente a} \qquad H_1: \text{al menos un } \beta_j \neq 0\]

El estadístico alcanza 137,5 con un valor p de \(5{,}65 \times 10^{-111}\). Se rechaza la hipótesis nula.

El modelo reproduce el 66,45 % de la variabilidad del precio. El R² ajustado es 0,6597, apenas cinco milésimas por debajo, de modo que ninguna variable sobra.

El error residual es de 132,8 millones. Sobre una casa de 400 millones, esa desviación equivale a una tercera parte de su valor.

Queda sin explicar el 33,55 %. Tres factores intervienen.

El primero es la forma funcional. La correlación de Spearman entre precio y área es 0,852 y la de Pearson 0,735. La relación crece de forma sostenida pero no constante, y una transformación logarítmica del precio recogería mejor ese comportamiento.

El segundo son las variables ausentes. La base no registra antigüedad, acabados, ubicación dentro del barrio ni seguridad del sector. Dos casas idénticas en la base pueden diferir en todo eso.

El tercero es la naturaleza del dato. El precio es de oferta y no de transacción, así que incluye el margen de negociación de cada vendedor. Esa variabilidad no depende del inmueble.

Solo el primero es accionable con los datos disponibles.

7.3 Modelo 2 — Apartamentos de la zona sur

modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
              data = base2_f)

coef2 <- broom::tidy(modelo2, conf.int = TRUE)

coef2$term <- c("Intercepto", "Área construida", "Estrato 4", "Estrato 5",
                "Estrato 6", "Habitaciones", "Parqueaderos", "Baños")

names(coef2) <- c("Término", "Coeficiente", "Error estándar", "Estadístico t",
                  "Valor p", "LI 95%", "LS 95%")

tabla(coef2, caption = "Tabla 19. Coeficientes estimados — Apartamentos de la zona sur", digits = 3)
Tabla 19. Coeficientes estimados — Apartamentos de la zona sur
Término Coeficiente Error estándar Estadístico t Valor p LI 95% LS 95%
Intercepto -11.518 12.495 -0.922 0.357 -36.021 12.985
Área construida 1.269 0.053 24.089 0.000 1.166 1.373
Estrato 4 18.074 8.741 2.068 0.039 0.932 35.216
Estrato 5 31.744 9.004 3.526 0.000 14.087 49.402
Estrato 6 183.562 11.058 16.600 0.000 161.876 205.247
Habitaciones -15.328 3.906 -3.924 0.000 -22.988 -7.668
Parqueaderos 54.295 3.585 15.143 0.000 47.264 61.326
Baños 46.145 3.486 13.239 0.000 39.310 52.981

La ecuación estimada es:

\[\widehat{\text{Precio}} = -11{,}52 + 1{,}27\,\text{Área} + 18{,}07\,\text{E4} + 31{,}74\,\text{E5} + 183{,}56\,\text{E6} - 15{,}33\,\text{Hab} + 54{,}30\,\text{Parq} + 46{,}15\,\text{Baños}\]

Siete de los ocho términos resultan significativos al 5 %. Solo el intercepto no lo es, y su lectura directa carece de sentido práctico por las mismas razones señaladas en el primer modelo.

El área construida aporta 1,27 millones por metro cuadrado, casi el doble que en casas. Un metro cuadrado de apartamento en el sur se valora entonces muy por encima de un metro cuadrado de casa en el norte.

El estrato 6 marca la mayor diferencia del modelo: 183,56 millones frente a un apartamento de estrato 3. El estrato 4 aporta 18,07 millones y el estrato 5, 31,74. El salto al estrato 6 supera casi seis veces al anterior, de modo que la valorización se concentra en la categoría más alta.

Los parqueaderos suman 54,30 millones por unidad y los baños 46,15 millones. La diferencia con el primer modelo es considerable: un parqueadero adicional vale en el sur más del triple que en el norte. El resultado es coherente con el tipo de inmueble, ya que en una casa el parqueadero suele formar parte del predio mientras que en un edificio constituye una unidad escasa y negociable por separado.

Las habitaciones presentan coeficiente negativo: −15,33 millones por habitación adicional, con un valor p de \(8{,}97 \times 10^{-5}\). El signo merece explicación, porque la correlación simple entre habitaciones y precio es positiva, de 0,404.

Las dos cifras no se contradicen. La correlación mide la relación sin controlar nada, y un apartamento con más habitaciones suele ser más grande, por lo que cuesta más. El coeficiente mide el efecto de agregar una habitación manteniendo constante el área. En ese caso, la misma superficie se reparte en más espacios y cada uno resulta menor. El mercado no premia esa subdivisión.

El resultado es consistente con lo observado en la Figura 9, donde el precio deja de crecer a partir de las cuatro habitaciones y desciende en los valores superiores.

7.3.1 Ajuste del modelo

ajuste2 <- broom::glance(modelo2)[, c("r.squared", "adj.r.squared", "sigma",
                                      "statistic", "nobs")]

names(ajuste2) <- c("R²", "R² ajustado", "Error residual", "Estadístico F", "n")

tabla(ajuste2, caption = "Tabla 20. Indicadores de ajuste — Apartamentos de la zona sur", digits = 4)
Tabla 20. Indicadores de ajuste — Apartamentos de la zona sur
R² ajustado Error residual Estadístico F n
0.7834 0.7827 92.1236 1,129.925 2,195

El estadístico F alcanza 1.130 con un valor p inferior a \(2{,}2 \times 10^{-16}\). Se rechaza la hipótesis nula bajo el mismo contraste aplicado al primer modelo.

El modelo reproduce el 78,34 % de la variabilidad del precio, casi doce puntos por encima del ajuste obtenido en casas. El R² ajustado es 0,7827.

El error residual es de 92,12 millones, frente a los 132,8 del primer modelo.

El mejor ajuste responde a la naturaleza del producto. Un apartamento es un bien más estandarizado que una casa, con menos variación en factores que el modelo no observa. El tamaño muestral también contribuye, con 2.195 registros frente a 494.

Las limitaciones señaladas en el primer modelo se mantienen. La brecha entre los coeficientes de Spearman y Pearson en el área construida es de 0,873 frente a 0,752, de modo que la transformación logarítmica del precio también resultaría pertinente aquí. Las variables ausentes y la condición de precio de oferta afectan por igual a los dos segmentos.

8 Validación de supuestos

La validez de las inferencias del modelo depende de cuatro supuestos sobre los errores: linealidad, varianza constante, independencia y normalidad. Esta sección los examina en los dos modelos, sin modificar las estimaciones.

8.1 Diagnóstico gráfico

Figura 10. Panel diagnóstico — Casas de la zona norte

par(mfrow = c(2, 2))
plot(modelo1, col = AZUL, pch = 19, cex = 0.5)

Figura 11. Panel diagnóstico — Apartamentos de la zona sur

par(mfrow = c(2, 2))
plot(modelo2, col = AZUL, pch = 19, cex = 0.4)

Las Figuras 10 y 11 reúnen los cuatro gráficos diagnósticos de cada modelo. Permiten evidenciar lo siguiente:

  • El gráfico de residuales contra valores ajustados no muestra curvatura sistemática, de modo que la especificación lineal resulta admisible.
  • La dispersión de los residuales se amplía conforme aumenta el valor ajustado, lo que anticipa que la varianza no es constante.
  • El gráfico Q-Q presenta desviaciones en las colas, con residuales extremos alejados de la diagonal.
  • El gráfico de residuales contra apalancamiento identifica observaciones aisladas en el borde del rango de las variables explicativas.

El comportamiento es equivalente en ambos modelos. Las subsecciones siguientes examinan cada supuesto en detalle, con los gráficos interactivos del primer modelo como referencia.

8.2 Normalidad de los residuales

Se contrasta:

\[H_0: \varepsilon_i \sim N(0, \sigma^2) \qquad \text{frente a} \qquad H_1: \varepsilon_i \nsim N(0, \sigma^2)\]

normal_res <- data.frame(
  Modelo = c("Casas zona norte", "Apartamentos zona sur"),
  W = c(shapiro.test(residuals(modelo1))$statistic,
        shapiro.test(residuals(modelo2))$statistic),
  `Valor p` = c("< 2,2e-16", "< 2,2e-16"),
  Decisión = c("Se rechaza H₀", "Se rechaza H₀"),
  check.names = FALSE
)

tabla(normal_res, caption = "Tabla 21. Prueba de Shapiro-Wilk sobre los residuales", digits = 4)
Tabla 21. Prueba de Shapiro-Wilk sobre los residuales
Modelo W Valor p Decisión
Casas zona norte 0.8143 < 2,2e-16 Se rechaza H₀
Apartamentos zona sur 0.7605 < 2,2e-16 Se rechaza H₀
# qqnorm con plot.it = FALSE devuelve los cuantiles teóricos y los muestrales
q1 <- qqnorm(rstandard(modelo1), plot.it = FALSE)

plot_ly(x = q1$x, y = q1$y, type = "scatter", mode = "markers",
        marker = list(size = 6, opacity = 0.5, color = AZUL),
        name = "Residuales") |>
  add_lines(x = range(q1$x), y = range(q1$x), inherit = FALSE,
            line = list(color = NARANJA, dash = "dash"), name = "Referencia") |>
  layout(title = "Figura 12. Gráfico Q-Q de los residuales — Casas de la zona norte",
         xaxis = list(title = "Cuantiles teóricos"),
         yaxis = list(title = "Residual estandarizado"))

Se rechaza la normalidad en ambos modelos. El estadístico es 0,8143 en casas y 0,7605 en apartamentos.

La Figura 12 muestra dónde se produce la desviación. Los residuales siguen la diagonal en el tramo central y se apartan en los extremos, con una cola superior pronunciada. El modelo subestima el precio de un grupo reducido de inmuebles de valor elevado.

El resultado era previsible. El análisis descriptivo mostró que la media del precio supera a la mediana en las ocho combinaciones de segmento y estrato, y esa asimetría se traslada a los residuales.

La consecuencia es acotada. Los estimadores por mínimos cuadrados siguen siendo insesgados sin el supuesto de normalidad, que solo se requiere para la validez exacta de las pruebas t y F y de los intervalos. Con 494 y 2.195 observaciones, esas inferencias conservan una aproximación razonable. El intervalo de predicción de una vivienda individual sí depende del supuesto, de modo que su cobertura del 95 % debe tomarse como referencia y no como garantía.

8.3 Homocedasticidad

Se contrasta:

\[H_0: \text{Var}(\varepsilon_i) = \sigma^2 \qquad \text{frente a} \qquad H_1: \text{Var}(\varepsilon_i) \neq \sigma^2\]

homo <- data.frame(
  Modelo = c("Casas zona norte", "Apartamentos zona sur"),
  BP = c(bptest(modelo1)$statistic, bptest(modelo2)$statistic),
  gl = c(7, 7),
  `Valor p` = c("< 2,2e-16", "< 2,2e-16"),
  Decisión = c("Se rechaza H₀", "Se rechaza H₀"),
  check.names = FALSE
)

tabla(homo, caption = "Tabla 22. Prueba de Breusch-Pagan", digits = 2)
Tabla 22. Prueba de Breusch-Pagan
Modelo BP gl Valor p Decisión
Casas zona norte 116.08 7 < 2,2e-16 Se rechaza H₀
Apartamentos zona sur 711.08 7 < 2,2e-16 Se rechaza H₀
res1 <- data.frame(ajustado = fitted(modelo1),
                   residual = residuals(modelo1),
                   barrio   = base1_f$barrio)

plot_ly(res1, x = ~ajustado, y = ~residual,
        type = "scatter", mode = "markers",
        marker = list(size = 6, opacity = 0.5, color = AZUL),
        text = ~barrio,
        hovertemplate = paste0("Barrio: %{text}<br>Ajustado: %{x:.0f}<br>",
                               "Residual: %{y:.0f}<extra></extra>")) |>
  add_lines(x = range(res1$ajustado), y = c(0, 0), inherit = FALSE,
            line = list(color = NARANJA, dash = "dash"), showlegend = FALSE) |>
  layout(title = "Figura 13. Residuales frente a valores ajustados — Casas de la zona norte",
         xaxis = list(title = "Valor ajustado (millones)"),
         yaxis = list(title = "Residual (millones)"))

Se detecta heterocedasticidad en ambos modelos. El estadístico alcanza 116,08 en casas y 711,08 en apartamentos.

La Figura 13 muestra el patrón. Los residuales se mantienen cerca de cero en los inmuebles de menor valor estimado y se abren en abanico conforme aumenta el valor ajustado. El modelo predice con mayor precisión las casas económicas que las costosas.

La magnitud del estadístico en apartamentos es seis veces la de casas, coherente con lo observado en la Figura 4.

El incumplimiento altera los errores estándar y deja intactos los coeficientes. Las estimaciones siguen siendo insesgadas, pero su incertidumbre queda mal calculada, y con ella los valores p y los intervalos de confianza. Dos alternativas corrigen esta situación: errores estándar robustos, que recalculan la incertidumbre sin alterar los coeficientes, o una transformación logarítmica del precio, que estabilizaría la varianza y resultaría además coherente con la no linealidad detectada en el análisis descriptivo.

8.3.1 Inferencia robusta ante heterocedasticidad

Detectada la varianza no constante, se recalculan los errores estándar mediante la corrección HC3. El procedimiento no modifica los coeficientes estimados; ajusta la estimación de su incertidumbre y, con ella, los valores p.

library(sandwich)

# vcovHC construye la matriz de covarianzas robusta y coeftest la emplea
# para recalcular errores estándar, estadísticos t y valores p.
hc1 <- coeftest(modelo1, vcov. = vcovHC(modelo1, type = "HC3"))
hc2 <- coeftest(modelo2, vcov. = vcovHC(modelo2, type = "HC3"))

robusto <- data.frame(
  Término = c("Intercepto", "Área construida", "Estrato 4", "Estrato 5",
              "Estrato 6", "Habitaciones", "Parqueaderos", "Baños"),
  `Casas — EE clásico` = summary(modelo1)$coefficients[, 2],
  `Casas — EE HC3`     = hc1[, 2],
  `Casas — p HC3`      = hc1[, 4],
  `Aptos — EE clásico` = summary(modelo2)$coefficients[, 2],
  `Aptos — EE HC3`     = hc2[, 2],
  `Aptos — p HC3`      = hc2[, 4],
  check.names = FALSE
)
rownames(robusto) <- NULL

tabla(robusto, caption = "Tabla 23. Errores estándar clásicos y robustos HC3", digits = 4)
Tabla 23. Errores estándar clásicos y robustos HC3
Término Casas — EE clásico Casas — EE HC3 Casas — p HC3 Aptos — EE clásico Aptos — EE HC3 Aptos — p HC3
Intercepto 19.4108 23.3656 0.1453 12.4949 14.9288 0.4405
Área construida 0.0508 0.1946 0.0004 0.0527 0.4122 0.0021
Estrato 4 18.1200 22.8517 0.0008 8.7411 7.1004 0.0110
Estrato 5 17.6947 29.3863 0.0000 9.0042 8.1235 0.0001
Estrato 6 37.1050 63.9491 0.0000 11.0580 13.5468 0.0000
Habitaciones 4.9074 6.6367 0.2243 3.9060 7.2682 0.0351
Parqueaderos 4.5035 5.8078 0.0068 3.5854 8.6265 0.0000
Baños 6.2480 8.6798 0.0404 3.4856 9.8833 0.0000

Ninguna conclusión de la sección anterior se modifica. Los términos significativos con errores clásicos lo siguen siendo con la corrección, y las habitaciones continúan sin alcanzar significancia en el modelo de casas.

Las magnitudes sí cambian. En casas, el error estándar del área construida pasa de 0,051 a 0,195 y el del estrato 6 de 37,1 a 63,9. En apartamentos el efecto es mayor: el del área pasa de 0,053 a 0,412, ocho veces su valor original.

Dos términos quedan cerca del umbral. Los baños en casas pasan de un valor p de 0,0045 a 0,0404, y las habitaciones en apartamentos de \(8{,}97 \times 10^{-5}\) a 0,0351. Ambos conservan la significancia al 5 %, con un margen considerablemente más estrecho.

El resultado indica que los errores estándar convencionales subestimaban la incertidumbre de las estimaciones. Las conclusiones sobre qué atributos inciden en el precio se mantienen, pero con menor holgura de la que sugiere la salida clásica. Esa misma consideración se extiende a los intervalos de predicción de la sección 9, cuya amplitud real resulta mayor que la reportada.

8.4 Independencia

indep <- data.frame(
  Modelo = c("Casas zona norte", "Apartamentos zona sur"),
  DW = c(dwtest(modelo1)$statistic, dwtest(modelo2)$statistic),
  `Valor p` = c("0,0090", "1,91e-12"),
  check.names = FALSE
)

tabla(indep, caption = "Tabla 24. Prueba de Durbin-Watson", digits = 4)
Tabla 24. Prueba de Durbin-Watson
Modelo DW Valor p
Casas zona norte 1.7947 0,0090
Apartamentos zona sur 1.7056 1,91e-12

El estadístico es 1,795 en casas y 1,706 en apartamentos, con valores p por debajo de 0,05.

Este resultado no evalúa el supuesto de independencia. Durbin-Watson contrasta la correlación entre residuales consecutivos según el orden de las filas, y en esta base ese orden proviene del proceso de extracción y no de una secuencia temporal. Los registros contiguos tienden a corresponder al mismo barrio, de modo que lo detectado refleja estructura geográfica.

Un diagnóstico apropiado de dependencia espacial exigiría construir una matriz de vecindad sobre las coordenadas, procedimiento que excede el alcance de este análisis. La depuración geográfica aplicada a las dos bases reduce parte de esa estructura, al excluir los registros ubicados fuera de la zona declarada.

8.5 Multicolinealidad

El factor de inflación de varianza mide cuánto se amplifica la varianza de un coeficiente por la asociación de su variable con las demás explicativas:

\[VIF_j = \frac{1}{1 - R_j^2}\]

donde \(R_j^2\) corresponde al coeficiente de determinación de la regresión de la variable \(j\) sobre el resto.

v1 <- vif(modelo1)
v2 <- vif(modelo2)

vif_tab <- data.frame(
  Variable = c("Área construida", "Estrato", "Habitaciones", "Parqueaderos", "Baños"),
  `Casas norte` = v1[, 3],
  `Aptos sur`   = v2[, 3],
  check.names = FALSE
)
rownames(vif_tab) <- NULL

tabla(vif_tab, caption = "Tabla 25. Factor de inflación de varianza ajustado", digits = 3)
Tabla 25. Factor de inflación de varianza ajustado
Variable Casas norte Aptos sur
Área construida 1.324 1.458
Estrato 1.098 1.115
Habitaciones 1.349 1.209
Parqueaderos 1.183 1.399
Baños 1.432 1.658

Se reporta el VIF generalizado corregido por grados de libertad, dado que el estrato aporta tres términos al modelo.

Ningún valor se aproxima al umbral de referencia de 5. El máximo es 1,432 en casas y 1,658 en apartamentos, ambos correspondientes al número de baños.

El resultado matiza lo observado en el análisis descriptivo, donde el área construida y los baños registraban una correlación de 0,771 en apartamentos. Una asociación entre dos predictoras no basta para inflar la varianza de los coeficientes. Lo determinante es cuánto explica el conjunto de las restantes a cada variable, y en estos modelos ninguna queda determinada por las demás.

8.6 Observaciones extremas

Se aplican tres criterios: residual estandarizado superior a 3 en valor absoluto, apalancamiento mayor que \(2p/n\) y distancia de Cook superior a \(4/n\).

n1 <- nobs(modelo1); p1 <- length(coef(modelo1))
n2 <- nobs(modelo2); p2 <- length(coef(modelo2))

d1 <- data.frame(cook = cooks.distance(modelo1),
                 lev  = hatvalues(modelo1),
                 res  = rstandard(modelo1))

d2 <- data.frame(cook = cooks.distance(modelo2),
                 lev  = hatvalues(modelo2),
                 res  = rstandard(modelo2))

extremas <- data.frame(
  Criterio = c("Atípicos (|r| > 3)",
               "Apalancamiento (h > 2p/n)",
               "Influyentes (Cook > 4/n)",
               "Influyentes (Cook > 1)"),
  `Casas norte` = c(sum(abs(d1$res) > 3), sum(d1$lev > 2*p1/n1),
                    sum(d1$cook > 4/n1), sum(d1$cook > 1)),
  `Aptos sur` = c(sum(abs(d2$res) > 3), sum(d2$lev > 2*p2/n2),
                  sum(d2$cook > 4/n2), sum(d2$cook > 1)),
  check.names = FALSE
)

tabla(extremas, caption = "Tabla 26. Observaciones extremas según cada criterio")
Tabla 26. Observaciones extremas según cada criterio
Criterio Casas norte Aptos sur
Atípicos (&#124;r&#124; > 3) 9 25
Apalancamiento (h > 2p/n) 43 233
Influyentes (Cook > 4/n) 30 100
Influyentes (Cook > 1) 1 1
d1$categoria <- ifelse(d1$cook > 1, "Cook > 1",
                ifelse(d1$cook > 4/n1, "Cook > 4/n", "Dentro de umbral"))

plot_ly(d1, x = ~lev, y = ~cook, color = ~categoria,
        colors = c(NARANJA, ACENTO, GRIS),
        type = "scatter", mode = "markers",
        marker = list(size = 7, opacity = 0.7),
        text = base1_f$barrio,
        hovertemplate = paste0("Barrio: %{text}<br>Apalancamiento: %{x:.3f}<br>",
                               "Cook: %{y:.3f}<extra></extra>")) |>
  layout(title = "Figura 14. Distancia de Cook frente a apalancamiento — Casas de la zona norte",
         xaxis = list(title = "Apalancamiento"),
         yaxis = list(title = "Distancia de Cook"))

La Figura 14 separa con claridad la observación problemática del resto de la nube. Cada modelo presenta un registro que supera el umbral clásico de Cook mayor que 1, en ambos casos con área desproporcionada frente a su precio.

infl <- rbind(
  base1_f[which.max(d1$cook), c("id","barrio","estrato","preciom","areaconst",
                                "banios","habitaciones","parqueaderos")],
  base2_f[which.max(d2$cook), c("id","barrio","estrato","preciom","areaconst",
                                "banios","habitaciones","parqueaderos")]
)

infl <- cbind(Segmento = c("Casas norte", "Aptos sur"), infl)
infl$Cook <- c(max(d1$cook), max(d2$cook))
rownames(infl) <- NULL

tabla(infl, caption = "Tabla 27. Observación más influyente de cada modelo", digits = 3)
Tabla 27. Observación más influyente de cada modelo
Segmento id barrio estrato preciom areaconst banios habitaciones parqueaderos Cook
Casas norte 534 villa del prado 3 370 1,440 4 10 1 1.598
Aptos sur 6,121 valle del lili 5 299 932 3 3 1 6.060

La casa registra 1.440 m² con 10 habitaciones en estrato 3 por 370 millones. El apartamento registra 932 m² por 299 millones, equivalente a 321 mil pesos por metro cuadrado. Ninguno de los dos valores resulta verosímil en el mercado de Cali, y la explicación más plausible es un error de captura, posiblemente por consignar el área del lote en lugar del área construida.

Estas observaciones no se eliminan. Su exclusión requeriría verificar el anuncio original, información que la base no proporciona.

8.7 Sensibilidad a las observaciones influyentes

Se reestiman ambos modelos excluyendo las observaciones con distancia de Cook superior a \(4/n\), con el fin de establecer si las conclusiones dependen de ellas.

m1_sin <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
             data = base1_f[d1$cook <= 4/n1, ])

m2_sin <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
             data = base2_f[d2$cook <= 4/n2, ])

sens <- data.frame(
  Término = c("Intercepto", "Área construida", "Estrato 4", "Estrato 5",
              "Estrato 6", "Habitaciones", "Parqueaderos", "Baños"),
  `Casas — con` = coef(modelo1),
  `Casas — sin` = coef(m1_sin),
  `Aptos — con` = coef(modelo2),
  `Aptos — sin` = coef(m2_sin),
  check.names = FALSE
)
rownames(sens) <- NULL

tabla(sens, caption = "Tabla 28. Coeficientes con y sin observaciones influyentes", digits = 3)
Tabla 28. Coeficientes con y sin observaciones influyentes
Término Casas — con Casas — sin Aptos — con Aptos — sin
Intercepto 34.085 49.085 -11.518 -12.083
Área construida 0.692 0.796 1.269 1.795
Estrato 4 77.042 49.589 18.074 23.255
Estrato 5 126.971 94.054 31.744 41.417
Estrato 6 280.704 235.298 183.562 168.785
Habitaciones 8.075 3.455 -15.328 -14.349
Parqueaderos 15.791 11.654 54.295 36.953
Baños 17.837 17.059 46.145 31.616

Ningún coeficiente cambia de signo. El de habitaciones en apartamentos se mantiene negativo, con −15,33 en el modelo completo y −14,35 en el reducido, de modo que la interpretación de ese resultado no depende de un conjunto reducido de registros.

Las magnitudes sí se desplazan. En apartamentos, el efecto del área construida pasa de 1,27 a 1,80 millones por metro cuadrado, mientras que parqueaderos y baños descienden de 54,30 a 36,95 y de 46,15 a 31,62. Al retirar los inmuebles de área elevada y precio bajo, el modelo atribuye mayor peso a la superficie y menor a la dotación.

El coeficiente de determinación aumenta de 0,664 a 0,789 en casas y de 0,783 a 0,883 en apartamentos. Ese incremento no indica un mejor modelo. Las observaciones excluidas son precisamente aquellas que el modelo reproduce con mayor error, de manera que retirarlas eleva el ajuste sin mejorar la capacidad explicativa.

8.8 Síntesis del diagnóstico

sintesis <- data.frame(
  Supuesto = c("Linealidad", "Normalidad", "Homocedasticidad",
               "Independencia", "Multicolinealidad"),
  `Casas norte` = c("Admisible", "Se rechaza", "Se rechaza",
                    "Resultado no concluyente", "Sin evidencia (VIF 1,43)"),
  `Aptos sur` = c("Admisible", "Se rechaza", "Se rechaza",
                  "Resultado no concluyente", "Sin evidencia (VIF 1,66)"),
  check.names = FALSE
)

tabla(sintesis, caption = "Tabla 29. Síntesis de la validación de supuestos")
Tabla 29. Síntesis de la validación de supuestos
Supuesto Casas norte Aptos sur
Linealidad Admisible Admisible
Normalidad Se rechaza Se rechaza
Homocedasticidad Se rechaza Se rechaza
Independencia Resultado no concluyente Resultado no concluyente
Multicolinealidad Sin evidencia (VIF 1,43) Sin evidencia (VIF 1,66)

Los dos modelos presentan incumplimientos de los supuestos de normalidad y homocedasticidad, mientras que cumplen con linealidad y ausencia de multicolinealidad. La independencia de los errores no puede evaluarse con la información disponible.

Estos incumplimientos afectan principalmente la precisión de las inferencias, pero no invalidan la estimación de los coeficientes ni las predicciones puntuales. Por ello, los intervalos de confianza y los valores p deben interpretarse con cautela.

9 Predicción de las viviendas solicitadas

Cada solicitud especifica el estrato como un rango: la primera admite estrato 4 o 5 y la segunda estrato 5 o 6. Se estiman por tanto dos escenarios por vivienda.

La predicción puntual se obtiene evaluando la ecuación estimada en las características solicitadas:

\[\hat{y}_0 = \mathbf{x}_0'\hat{\boldsymbol{\beta}}\]

El intervalo de predicción para una vivienda individual es:

\[\hat{y}_0 \pm t_{1-\alpha/2,\,n-p}\; s\sqrt{1 + \mathbf{x}_0'(\mathbf{X}'\mathbf{X})^{-1}\mathbf{x}_0}\]

El término adicional dentro de la raíz recoge la variabilidad propia de un inmueble particular. Por eso este intervalo es más amplio que el de confianza, que estima el precio medio de todas las viviendas con esas características. La decisión de compra recae sobre un inmueble concreto, de modo que el intervalo de predicción es el pertinente.

9.1 Vivienda 1 — Casa en la zona norte

nueva1 <- data.frame(
  areaconst    = 200,
  estrato      = factor(c("4", "5"), levels = levels(base1_f$estrato)),
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

p1 <- predict(modelo1, nueva1, interval = "prediction", level = 0.95)

pred1 <- data.frame(
  Escenario = c("Estrato 4", "Estrato 5"),
  `Precio estimado` = p1[, "fit"],
  `LI 95%` = p1[, "lwr"],
  `LS 95%` = p1[, "upr"],
  Crédito = 350,
  Margen = 350 - p1[, "fit"],
  check.names = FALSE
)

tabla(pred1, caption = "Tabla 30. Predicción del precio — Vivienda 1", digits = 1)
Tabla 30. Predicción del precio — Vivienda 1
Escenario Precio estimado LI 95% LS 95% Crédito Margen
Estrato 4 333.2 70.5 595.9 350 16.8
Estrato 5 383.2 120.8 645.6 350 -33.2

Para una casa de 200 m² con un parqueadero, dos baños y cuatro habitaciones en la zona norte, el modelo estima 333,2 millones en estrato 4 y 383,2 millones en estrato 5.

El crédito preaprobado es de 350 millones. El escenario de estrato 4 queda dentro del presupuesto, con un margen de 16,8 millones. El de estrato 5 lo excede en 33,2 millones.

El intervalo de predicción condiciona esta lectura. En estrato 4 va de 70,5 a 596,0 millones, una amplitud de 525 millones sobre un valor esperado de 333. El procedimiento no permite descartar precios muy por encima ni muy por debajo del crédito disponible.

Esa imprecisión proviene del error residual del modelo, de 132,8 millones, y del ajuste alcanzado, que reproduce el 66,45 % de la variabilidad del precio. La estimación resulta útil como referencia de orden de magnitud, pero insuficiente para sustentar por sí sola la decisión sobre un inmueble concreto.

plot_ly(base1_f, x = ~areaconst, y = ~preciom,
        type = "scatter", mode = "markers",
        marker = list(size = 6, opacity = 0.35, color = GRIS),
        name = "Casas de la zona norte",
        hoverinfo = "skip") |>
  add_markers(x = c(200, 200), y = p1[, "fit"], inherit = FALSE,
              marker = list(size = 13, color = NARANJA, symbol = "diamond"),
              name = "Vivienda solicitada",
              text = c("Estrato 4", "Estrato 5"),
              hovertemplate = "%{text}<br>%{y:.1f} millones<extra></extra>") |>
  add_lines(x = range(base1_f$areaconst), y = c(350, 350), inherit = FALSE,
            line = list(color = ACENTO, dash = "dash"),
            name = "Crédito disponible") |>
  layout(title = "Figura 15. La vivienda solicitada frente al mercado de casas de la zona norte",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones de pesos)"))

La Figura 15 ubica las dos estimaciones dentro del mercado real. Ambas se sitúan en la parte baja de la nube de casas de 200 m², lo que indica que existen inmuebles de esa superficie por debajo del crédito disponible. Esas ofertas se examinan en la sección siguiente.

9.2 Vivienda 2 — Apartamento en la zona sur

nueva2 <- data.frame(
  areaconst    = 300,
  estrato      = factor(c("5", "6"), levels = levels(base2_f$estrato)),
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)

p2 <- predict(modelo2, nueva2, interval = "prediction", level = 0.95)

pred2 <- data.frame(
  Escenario = c("Estrato 5", "Estrato 6"),
  `Precio estimado` = p2[, "fit"],
  `LI 95%` = p2[, "lwr"],
  `LS 95%` = p2[, "upr"],
  Crédito = 850,
  Margen = 850 - p2[, "fit"],
  check.names = FALSE
)

tabla(pred2, caption = "Tabla 31. Predicción del precio — Vivienda 2", digits = 1)
Tabla 31. Predicción del precio — Vivienda 2
Escenario Precio estimado LI 95% LS 95% Crédito Margen
Estrato 5 625.7 443.7 807.8 850 224.3
Estrato 6 777.6 595.4 959.8 850 72.4

Para un apartamento de 300 m² con tres parqueaderos, tres baños y cinco habitaciones en la zona sur, el modelo estima 625,7 millones en estrato 5 y 777,6 millones en estrato 6.

Los dos escenarios caben en el crédito de 850 millones, con márgenes de 224,3 y 72,4 millones respectivamente.

El intervalo del escenario de estrato 5 va de 443,7 a 807,8 millones y queda íntegramente por debajo del crédito. El de estrato 6 alcanza los 959,8 millones en su límite superior y lo supera en 110.

La solicitud resulta viable en ambos escenarios, con holgura en estrato 5 y con el presupuesto ajustado en estrato 6.

9.3 Comparación de las cuatro estimaciones

comp <- data.frame(
  Escenario = c("Casa norte, estrato 4", "Casa norte, estrato 5",
                "Apto sur, estrato 5", "Apto sur, estrato 6"),
  Estimación = c(p1[, "fit"], p2[, "fit"]),
  `Amplitud del intervalo` = c(p1[, "upr"] - p1[, "lwr"],
                               p2[, "upr"] - p2[, "lwr"]),
  Crédito = c(350, 350, 850, 850),
  `¿Cabe la estimación?` = c("Sí", "No", "Sí", "Sí"),
  `¿Cabe el límite superior?` = c("No", "No", "Sí", "No"),
  check.names = FALSE
)

tabla(comp, caption = "Tabla 32. Resumen de las cuatro estimaciones", digits = 1)
Tabla 32. Resumen de las cuatro estimaciones
Escenario Estimación Amplitud del intervalo Crédito ¿Cabe la estimación? ¿Cabe el límite superior?
Casa norte, estrato 4 333.2 525.4 350 No
Casa norte, estrato 5 383.2 524.8 350 No No
Apto sur, estrato 5 625.7 364.2 850
Apto sur, estrato 6 777.6 364.4 850 No
pred_g <- data.frame(
  escenario = c("Casa norte — E4", "Casa norte — E5",
                "Apto sur — E5", "Apto sur — E6"),
  fit = c(p1[, "fit"], p2[, "fit"]),
  lwr = c(p1[, "lwr"], p2[, "lwr"]),
  upr = c(p1[, "upr"], p2[, "upr"]),
  credito = c(350, 350, 850, 850)
)

plot_ly(pred_g, x = ~escenario, y = ~fit,
        type = "scatter", mode = "markers",
        marker = list(size = 12, color = AZUL),
        error_y = list(type = "data", symmetric = FALSE,
                       array = ~upr - fit, arrayminus = ~fit - lwr,
                       color = ACENTO, thickness = 2),
        name = "Precio estimado",
        hovertemplate = "Estimación: %{y:.1f} millones<extra></extra>") |>
  add_markers(y = ~credito, inherit = FALSE, x = ~escenario,
              marker = list(size = 16, color = NARANJA, symbol = "line-ew-open"),
              name = "Crédito disponible",
              hovertemplate = "Crédito: %{y} millones<extra></extra>") |>
  layout(title = "Figura 16. Precio estimado e intervalo de predicción frente al crédito",
         xaxis = list(title = ""),
         yaxis = list(title = "Millones de pesos"))

La Figura 16 muestra las cuatro estimaciones con su intervalo frente al crédito correspondiente. La precisión difiere de manera notoria entre segmentos: el intervalo de la casa abarca 525 millones y el del apartamento 364, pese a que el segundo corresponde a un inmueble de mayor valor. El modelo de apartamentos estima con menor error absoluto porque el segmento es más homogéneo y la muestra más grande.

Esa diferencia se traslada a la recomendación. La estimación del apartamento respalda la decisión de compra. La de la casa indica que el presupuesto está al límite, con una incertidumbre que obliga a apoyarse en las ofertas disponibles en el mercado antes que en el valor estimado.

10 Selección de ofertas

Con el precio estimado como referencia, se identifican las ofertas del mercado que responden a cada solicitud dentro del crédito disponible.

10.1 Criterios de selección

El filtro aplica cuatro condiciones. El precio de lista no supera el crédito preaprobado. El área construida se mantiene en una banda alrededor de la superficie solicitada. El estrato corresponde al indicado en la solicitud. Y el número de habitaciones, baños y parqueaderos se trata como mínimo aceptable, dado que un atributo por encima de lo pedido no descalifica al inmueble.

El tope presupuestal se aplica al precio de lista y no al estimado, porque es el valor que la empresa debe desembolsar. La estimación del modelo cumple otra función: calificar cada oferta mediante la brecha entre lo que se pide por el inmueble y lo que el modelo esperaría que costara.

\[\text{Brecha \%} = \frac{\hat{y}_i - y_i}{y_i} \times 100\]

Una brecha positiva indica que el precio de lista se sitúa por debajo del valor estimado. La lectura admite dos sentidos: una oportunidad de compra, o la presencia de algún atributo que el modelo no observa y que reduce el valor del inmueble. La verificación comercial resulta necesaria en ambos casos.

10.2 Ofertas para la vivienda 1

ofertas1 <- subset(base1_f,
  preciom      <= 350 &
  areaconst    >= 180 & areaconst <= 220 &
  habitaciones >= 4 &
  banios       >= 2 &
  parqueaderos >= 1 &
  estrato %in% c("4", "5"))

ofertas1$pred   <- predict(modelo1, ofertas1)
ofertas1$brecha <- round(100 * (ofertas1$pred - ofertas1$preciom) / ofertas1$preciom, 1)

ofertas1 <- ofertas1[order(ofertas1$preciom, -ofertas1$brecha), ]

tab_of <- ofertas1[1:5, c("barrio","estrato","areaconst","habitaciones","banios",
                          "parqueaderos","preciom","pred","brecha")]

names(tab_of) <- c("Barrio", "Estrato", "Área", "Hab.", "Baños",
                   "Parq.", "Precio", "Estimado", "Brecha %")
rownames(tab_of) <- NULL

tabla(tab_of, caption = "Tabla 33. Ofertas potenciales para la vivienda 1", digits = 1)
Tabla 33. Ofertas potenciales para la vivienda 1
Barrio Estrato Área Hab. Baños Parq. Precio Estimado Brecha %
vipasa 5 205 6 5 2 300 472.1 57.4
la flora 5 200 4 4 2 320 434.6 35.8
urbanización la merced 5 210 5 3 2 320 431.8 34.9
la merced 4 200 4 4 2 320 384.7 20.2
el bosque 5 202 5 4 1 335 428.3 27.9

El filtro devuelve 10 ofertas que satisfacen la solicitud completa dentro del presupuesto, con el área en una banda de ±10 % alrededor de los 200 m². La Tabla 32 presenta las cinco de menor precio.

Las diez cumplen o superan todos los atributos solicitados, de modo que la elección entre ellas no depende de su correspondencia con el requerimiento. Los criterios pertinentes son el precio, la brecha frente al valor estimado y la ubicación.

La casa de vipasa encabeza la selección por dos razones. Es la más económica, con 300 millones, y supera ampliamente lo solicitado: seis habitaciones frente a cuatro y cinco baños frente a dos. El modelo la estima en 472 millones, una brecha del 57,4 %, la mayor del grupo. Esa combinación merece atención antes que entusiasmo, porque un inmueble que ofrece más y cuesta menos suele diferir del resto en algún aspecto que la base no registra.

Tres ofertas coinciden en 320 millones: la flora, urbanización la merced y la merced. Sus brechas son de 35,8 %, 34,9 % y 20,2 % respectivamente. La de la merced es la única de estrato 4 y presenta la brecha más moderada, lo que la convierte en la alternativa de perfil más previsible.

Las restantes se sitúan entre 335 y 350 millones, con brechas entre 16,9 % y 27,9 %. Agotan el crédito sin aportar ventajas sobre las anteriores.

Todas las brechas resultan positivas. Ninguna de las diez ofertas se encuentra listada por encima de su valor estimado.

top5 <- ofertas1[1:5, ]

leaflet(top5) |>
  addProviderTiles("CartoDB.Positron") |>
  addCircleMarkers(~longitud, ~latitud,
                   radius = 9, stroke = TRUE, weight = 2,
                   color = AZUL, fillColor = NARANJA, fillOpacity = 0.8,
                   label = ~barrio,
                   popup = ~paste0("<b>", barrio, "</b><br>",
                                   areaconst, " m² — estrato ", estrato, "<br>",
                                   habitaciones, " habitaciones, ", banios, " baños<br>",
                                   "Precio: ", preciom, " millones<br>",
         
                                                             "Estimado: ", round(pred), " millones"))

Figura 17. Ubicación de las ofertas seleccionadas — Vivienda 1

La Figura 17 ubica las cinco ofertas seleccionadas. Se concentran en el corredor norte de la ciudad, con la merced y el bosque en sectores contiguos y vipasa y la flora algo más al oriente. La proximidad entre ellas facilita una visita conjunta.

La comparación conduce a dos alternativas por razones distintas.

La casa de la merced, a 320 millones, ofrece el perfil más consistente: cumple los requisitos, es de estrato 4 —el escenario que el modelo sitúa dentro del presupuesto— y su brecha del 20,2 % es la más moderada entre las opciones de menor precio. Deja además un margen de 30 millones sobre el crédito.

La casa de vipasa, a 300 millones, ofrece el mayor potencial de valor y el margen presupuestal más amplio, pero su brecha del 57,4 % exige verificar el estado del inmueble y las condiciones del sector antes de avanzar.

Ninguna alternativa debe presentarse al cliente sin inspección previa. El modelo estima el precio a partir de siete atributos, y las diferencias entre inmuebles con idénticas características provienen de factores que no registra.

10.3 Ofertas para la vivienda 2

La segunda solicitud requiere un apartamento de 300 m² con cinco habitaciones, tres baños y tres parqueaderos, en estrato 5 o 6 de la zona sur, con un crédito de 850 millones.

diag_filtro <- data.frame(
  Criterio = c("Todos los requisitos",
               "Sin exigir habitaciones",
               "Sin exigir baños",
               "Sin exigir parqueaderos",
               "Con parqueaderos ≥ 2",
               "Con habitaciones ≥ 4"),
  Ofertas = c(2, 5, 2, 4, 3, 3)
)

tabla(diag_filtro, caption = "Tabla 34. Ofertas disponibles según el requisito que se relaja")
Tabla 34. Ofertas disponibles según el requisito que se relaja
Criterio Ofertas
Todos los requisitos 2
Sin exigir habitaciones 5
Sin exigir baños 2
Sin exigir parqueaderos 4
Con parqueaderos ≥ 2 3
Con habitaciones ≥ 4 3

Con todos los requisitos, el mercado ofrece apenas 2 apartamentos, cifra insuficiente frente a las cinco solicitadas. La Tabla 33 examina qué condición restringe la búsqueda: relajar el número de habitaciones eleva el resultado a cinco, mientras que ninguna otra alternativa alcanza ese mínimo.

La relajación encuentra respaldo en el propio modelo. El coeficiente estimado para las habitaciones en este segmento es de −15,33 millones, de modo que subdividir una superficie dada en más espacios reduce el valor del inmueble en lugar de aumentarlo. Un apartamento de 300 m² con cuatro habitaciones amplias responde a la necesidad de la familia igual o mejor que uno con cinco habitaciones pequeñas. El mercado de la zona sur confirma esa lógica: prácticamente no existen apartamentos de esa superficie con cinco o más habitaciones.

Se conservan entonces el presupuesto, el estrato, la banda de área de ±15 % y los mínimos de baños y parqueaderos.

ofertas2 <- subset(base2_f,
  preciom      <= 850 &
  areaconst    >= 255 & areaconst <= 345 &
  banios       >= 3 &
  parqueaderos >= 3 &
  estrato %in% c("5", "6"))

ofertas2$pred   <- predict(modelo2, ofertas2)
ofertas2$brecha <- round(100 * (ofertas2$pred - ofertas2$preciom) / ofertas2$preciom, 1)

ofertas2 <- ofertas2[order(ofertas2$preciom, -ofertas2$brecha), ]

tab_of2 <- ofertas2[, c("barrio","estrato","areaconst","habitaciones","banios",
                        "parqueaderos","preciom","pred","brecha")]

names(tab_of2) <- c("Barrio", "Estrato", "Área", "Hab.", "Baños",
                    "Parq.", "Precio", "Estimado", "Brecha %")
rownames(tab_of2) <- NULL

tabla(tab_of2, caption = "Tabla 35. Ofertas potenciales para la vivienda 2", digits = 1)
Tabla 35. Ofertas potenciales para la vivienda 2
Barrio Estrato Área Hab. Baños Parq. Precio Estimado Brecha %
capri 5 260 3 3 3 350 605.6 73.0
capri 5 270 4 3 3 350 603.0 72.3
pampa linda 5 267 3 3 3 450 614.5 36.6
seminario 5 256 5 5 3 530 662.2 24.9
seminario 5 300 6 5 3 670 702.7 4.9

Las cinco ofertas se sitúan entre 350 y 670 millones, todas con holgura frente al crédito de 850.

Dos apartamentos de seminario cumplen la solicitud completa, sin necesidad de la relajación aplicada. El de 256 m² ofrece cinco habitaciones y cinco baños por 530 millones. El de 300 m² ofrece seis habitaciones y cinco baños por 670 millones, y coincide exactamente con el área solicitada.

Este último presenta además la brecha más baja del conjunto: 4,9 %. El modelo lo estima en 702,7 millones frente a un precio de lista de 670, de modo que su valoración resulta consistente con el mercado. Es la única oferta del grupo cuyo precio no se aparta de lo que el modelo esperaría.

Las dos ofertas de capri requieren verificación. Comparten precio de 350 millones, superficies de 260 y 270 m², estrato 5 y tres parqueaderos. El modelo las estima por encima de los 600 millones, con brechas del 73,0 % y 72,3 %. Dos inmuebles del mismo barrio listados a poco más de la mitad de su valor estimado sugieren una condición compartida que la base no registra. Representan la mayor oportunidad aparente del conjunto y también el mayor riesgo.

Pampa linda, con 267 m² y 450 millones, ocupa una posición intermedia, con una brecha del 36,6 %.

leaflet(ofertas2) |>
  addProviderTiles("CartoDB.Positron") |>
  addCircleMarkers(~longitud, ~latitud,
                   radius = 9, stroke = TRUE, weight = 2,
                   color = AZUL, fillColor = NARANJA, fillOpacity = 0.8,
                   label = ~barrio,
                   popup = ~paste0("<b>", barrio, "</b><br>",
                                   areaconst, " m² — estrato ", estrato, "<br>",
                                   habitaciones, " habitaciones, ", banios, " baños<br>",
                                   "Precio: ", preciom, " millones<br>",
                                   "Estimado: ", round(pred), " millones"))

Figura 18. Ubicación de las ofertas seleccionadas — Vivienda 2

La Figura 18 ubica las cinco ofertas dentro de la zona sur. Las dos de capri se localizan en el mismo sector, hecho consistente con la similitud de sus características y de su precio.

El apartamento de seminario de 300 m², a 670 millones, es la alternativa que mejor responde a la solicitud. Coincide con el área requerida, supera el número de habitaciones y baños solicitados, cuenta con los tres parqueaderos y deja un margen de 180 millones sobre el crédito. Su precio se corresponde con el valor estimado, lo que reduce la incertidumbre asociada a la compra.

El apartamento de seminario de 256 m², a 530 millones, constituye la alternativa más económica entre las que cumplen la solicitud completa, con un margen de 320 millones.

Las ofertas de capri ofrecen el mayor margen presupuestal del conjunto, pero su precio se aparta del valor estimado en más del 70 %. Su consideración exige establecer previamente a qué responde esa diferencia.

11 Comparación de los modelos

Los dos modelos comparten especificación y difieren en el segmento sobre el que se estiman. La comparación permite establecer cómo valora cada mercado los mismos atributos y con qué precisión predice cada modelo.

11.1 Indicadores de ajuste

comp_aj <- data.frame(
  Modelo = c("Casas — Zona Norte", "Apartamentos — Zona Sur"),
  n = c(nobs(modelo1), nobs(modelo2)),
  `R²` = c(summary(modelo1)$r.squared, summary(modelo2)$r.squared),
  `R² ajustado` = c(summary(modelo1)$adj.r.squared, summary(modelo2)$adj.r.squared),
  `Error residual` = c(summary(modelo1)$sigma, summary(modelo2)$sigma),
  `Precio mediano` = c(median(base1_f$preciom), median(base2_f$preciom)),
  check.names = FALSE
)

tabla(comp_aj, caption = "Tabla 36. Indicadores de ajuste de los dos modelos", digits = 4)
Tabla 36. Indicadores de ajuste de los dos modelos
Modelo n R² ajustado Error residual Precio mediano
Casas — Zona Norte 494 0.6645 0.6597 132.7588 360
Apartamentos — Zona Sur 2,195 0.7834 0.7827 92.1236 250

El modelo de apartamentos alcanza un R² de 0,783 frente al 0,664 del modelo de casas. La diferencia no constituye una jerarquía entre ambos: cada uno se estima sobre una población distinta, con su propia variabilidad de precios.

La comparación pertinente es relativa. El error residual de casas es de 132,8 millones sobre un precio mediano de 360; el de apartamentos, 92,1 millones sobre una mediana de 250. En términos proporcionales, ambos modelos se equivocan en una magnitud similar.

El segmento de apartamentos ajusta mejor por dos razones. El producto es más estandarizado, con menos variación en factores que el modelo no observa. Y la muestra es cuatro veces mayor.

11.2 Valoración de los atributos en cada mercado

comp_coef <- data.frame(
  Atributo = c("Metro cuadrado", "Parqueadero", "Baño", "Habitación",
               "Estrato 4 vs 3", "Estrato 5 vs 3", "Estrato 6 vs 3"),
  `Casas norte` = coef(modelo1)[c("areaconst", "parqueaderos", "banios",
                                  "habitaciones", "estrato4", "estrato5", "estrato6")],
  `Aptos sur` = coef(modelo2)[c("areaconst", "parqueaderos", "banios",
                                "habitaciones", "estrato4", "estrato5", "estrato6")],
  check.names = FALSE
)
rownames(comp_coef) <- NULL

tabla(comp_coef, caption = "Tabla 37. Coeficientes estimados en cada segmento", digits = 2)
Tabla 37. Coeficientes estimados en cada segmento
Atributo Casas norte Aptos sur
Metro cuadrado 0.69 1.27
Parqueadero 15.79 54.30
Baño 17.84 46.15
Habitación 8.08 -15.33
Estrato 4 vs 3 77.04 18.07
Estrato 5 vs 3 126.97 31.74
Estrato 6 vs 3 280.70 183.56

Los dos mercados valoran los mismos atributos de manera distinta.

El metro cuadrado vale 1,27 millones en apartamentos y 0,69 en casas, casi el doble. El parqueadero vale 54,30 millones en apartamentos y 15,79 en casas, más del triple. En un edificio el parqueadero es una unidad escasa que se negocia aparte; en una casa suele formar parte del predio.

La habitación adicional aporta 8,08 millones en casas, sin significancia estadística, y resta 15,33 millones en apartamentos. Subdividir una superficie dada penaliza el precio en el segmento de apartamentos y resulta indiferente en el de casas.

El estrato pesa más en casas. Pasar del estrato 3 al 6 agrega 280,70 millones en casas y 183,56 en apartamentos.

11.3 Capacidad predictiva

El coeficiente de determinación mide el ajuste sobre los datos empleados en la estimación. El caso requiere predecir el precio de viviendas que no forman parte de la base, de modo que se evalúa el desempeño de cada modelo sobre observaciones no utilizadas para estimarlo.

Se aplica validación cruzada simple: el 70 % de cada base se destina a estimar el modelo y el 30 % restante a medir su error de predicción.

set.seed(2026)

vc <- function(base, etiqueta) {
  n   <- nrow(base)
  idx <- sample(n, size = floor(0.7 * n))   # 70 % para entrenamiento

  m    <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
             data = base[idx, ])
  test <- base[-idx, ]
  pred <- predict(m, test)

  data.frame(
    Segmento = etiqueta,
    Entrenamiento = length(idx),
    Prueba = nrow(test),
    RMSE = sqrt(mean((test$preciom - pred)^2)),
    MAE  = mean(abs(test$preciom - pred)),
    `R² en prueba` = 1 - sum((test$preciom - pred)^2) /
                         sum((test$preciom - mean(test$preciom))^2),
    check.names = FALSE
  )
}

tabla(rbind(vc(base1_f, "Casas norte"), vc(base2_f, "Apartamentos sur")),
      caption = "Tabla 38. Desempeño fuera de muestra", digits = 2)
Tabla 38. Desempeño fuera de muestra
Segmento Entrenamiento Prueba RMSE MAE R² en prueba
Casas norte 345 149 147.43 85.12 0.59
Apartamentos sur 1,536 659 102.09 54.85 0.77

El modelo de apartamentos conserva su ajuste fuera de muestra, con un R² de 0,773 frente al 0,783 obtenido sobre la base completa. El de casas desciende de 0,664 a 0,589.

La caída en casas refleja la heterogeneidad del segmento. Con 149 inmuebles de prueba, unos pocos precios atípicos desplazan el resultado.

Las dos medidas de error admiten lecturas complementarias. El RMSE alcanza 147,4 millones en casas y 102,1 en apartamentos, mientras que el MAE se sitúa en 85,1 y 54,9. La brecha entre ambos indica que el error típico es menor que el sugerido por el RMSE, medida que penaliza al cuadrado y se eleva con unas pocas desviaciones grandes.

El MAE es la cifra pertinente para la decisión de compra. Al estimar el precio de una casa en la zona norte, el modelo se desvía típicamente en 85 millones; al estimar el de un apartamento en la zona sur, en 55 millones.

Esa magnitud precisa lo señalado en la sección de predicción. La estimación de la vivienda 1 es de 333,2 millones frente a un crédito de 350, y el error típico de predicción del modelo supera el margen disponible. La decisión sobre esa solicitud no puede apoyarse en el valor estimado, sino en las ofertas efectivamente disponibles en el mercado.

12 Conclusiones

  • La depuración conservó el 98,38 % de los registros. El tratamiento diferenciado por variable evitó la pérdida del 42,23 % que habría implicado eliminar los casos incompletos, y permitió estimar los modelos sobre 494 casas y 2.195 apartamentos.

  • La verificación geográfica reveló que el 29,0 % de las casas declaradas en la zona norte y el 20,2 % de los apartamentos de la zona sur se ubican más cerca del centroide de otra zona. Los registros inconsistentes presentaban un precio mediano de 450 millones frente a 360 en casas, de modo que conservarlos habría desplazado la estimación.

  • Los dos mercados valoran los mismos atributos de manera distinta. El metro cuadrado vale 1,27 millones en apartamentos del sur y 0,69 en casas del norte. El parqueadero vale 54,30 millones frente a 15,79. La segmentación del análisis resultó entonces necesaria y no una decisión de conveniencia.

  • El número de habitaciones opera en sentido contrario al esperado. En apartamentos resta 15,33 millones por unidad adicional, y en casas no alcanza significancia estadística. Manteniendo constante el área construida, subdividir la superficie en más espacios no incrementa el valor del inmueble. La prueba de sensibilidad confirmó que el resultado no depende de observaciones puntuales.

  • Los modelos incumplen normalidad y homocedasticidad, y satisfacen linealidad y ausencia de multicolinealidad. La corrección HC3 mostró que los errores estándar convencionales subestimaban la incertidumbre, sin alterar las conclusiones sobre qué atributos inciden en el precio.

  • Las dos solicitudes admiten respuestas distintas. La vivienda 2 se estima en 625,7 millones en estrato 5 y 777,6 en estrato 6, ambos dentro de un crédito de 850. La vivienda 1 se estima en 333,2 millones en estrato 4, frente a un crédito de 350, con un error típico de predicción de 85 millones que supera el margen disponible.

  • El mercado ofrece alternativas para ambas solicitudes dentro del presupuesto: 10 casas en la zona norte que cumplen o superan lo requerido, y 5 apartamentos en la zona sur tras relajar el número de habitaciones, relajación que el propio modelo respalda.

13 Recomendaciones

13.1 Sobre la primera solicitud

Presentar la casa de la merced, a 320 millones. Cumple los atributos requeridos, corresponde al estrato 4 —escenario que el modelo sitúa dentro del presupuesto— y su precio se aparta un 20,2 % del valor estimado, la brecha más moderada entre las opciones de menor precio. Deja un margen de 30 millones sobre el crédito.

Incluir la casa de vipasa, a 300 millones, con advertencia. Ofrece seis habitaciones y cinco baños, muy por encima de lo solicitado, y el mayor margen presupuestal. Su brecha del 57,4 % obliga a inspeccionar el inmueble antes de presentarlo como alternativa.

Advertir al cliente sobre el límite presupuestal. El precio estimado para una casa con las características solicitadas es de 333,2 millones en estrato 4 y 383,2 en estrato 5, frente a un crédito de 350. El escenario de estrato 5 excede el presupuesto en 33,2 millones, de modo que la solicitud tal como está formulada solo resulta viable en su versión de estrato 4.

Plantear las tres alternativas disponibles. La primera es aceptar el estrato 4, que mantiene la compra dentro del crédito. La segunda es ampliar el crédito en al menos 35 millones, si el cliente prioriza el estrato 5. La tercera es revisar el mercado: las diez ofertas identificadas se ubican entre 300 y 350 millones y nueve de ellas corresponden al estrato 5, lo que indica que el mercado ofrece casas de ese estrato por debajo de lo que el modelo estima. Esta última vía no exige renunciar a ningún requisito ni ampliar el presupuesto.

13.2 Sobre la segunda solicitud

Presentar el apartamento de seminario de 300 m², a 670 millones. Coincide con el área requerida, supera las habitaciones y baños solicitados, cuenta con los tres parqueaderos y deja 180 millones de margen. Su precio se corresponde con el valor estimado, con una brecha del 4,9 %.

Ofrecer el apartamento de seminario de 256 m², a 530 millones, como alternativa económica. Cumple igualmente la solicitud completa y amplía el margen a 320 millones.

Informar sobre el ajuste de requisitos. El mercado de la zona sur ofrece solo dos apartamentos de 300 m² con cinco habitaciones, tres baños y tres parqueaderos en estrato 5 o 6. Conviene explicar al cliente que un apartamento de esa superficie con cuatro habitaciones amplias responde a su necesidad en condiciones equivalentes.

13.3 Sobre el uso del modelo

Emplear la estimación como referencia de negociación y no como valoración. Los precios de la base corresponden a lo que el vendedor solicita y no a lo que el inmueble se transa. El modelo indica en qué rango se está pidiendo por inmuebles con determinadas características.

Verificar toda oferta con brecha superior al 40 %. Un inmueble listado muy por debajo de su valor estimado difiere del resto en algún aspecto que la base no registra: estado de conservación, antigüedad o condiciones del sector.

Considerar el error típico de predicción al fijar expectativas. El modelo se desvía en promedio 85 millones al estimar una casa de la zona norte y 55 millones al estimar un apartamento de la zona sur.

13.4 Sobre el análisis

Incorporar variables no disponibles en la base. La antigüedad del inmueble, el estado de los acabados y las condiciones del sector explicarían parte del 33,55 % de variabilidad que el modelo de casas no reproduce.

Evaluar una transformación logarítmica del precio. La relación entre precio y área construida resulta más intensa en términos monótonos que lineales, y la transformación estabilizaría además la varianza de los residuales.

14 Anexos

Se recogen las verificaciones realizadas durante el análisis cuyo resultado sustenta decisiones del cuerpo del informe, sin formar parte de su desarrollo.

14.1 Integridad de la base original

dim(vivienda)
## [1] 8322   13
colSums(is.na(vivienda))
##           id         zona         piso      estrato      preciom    areaconst 
##            3            3         2638            3            2            3 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##         1605            3            3            3            3            3 
##      latitud 
##            3

14.2 Registros sin información

Las tres filas eliminadas por ausencia total de datos corresponden a las últimas posiciones del archivo. La última conserva un valor de precio y ningún otro campo.

vivienda[rowSums(is.na(vivienda)) >= 10, ]
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
8320 NA NA NA NA NA NA NA NA NA NA NA NA NA
8321 NA NA NA NA NA NA NA NA NA NA NA NA NA
8322 NA NA NA NA 330 NA NA NA NA NA NA NA NA

14.3 Evidencia sobre los registros duplicados

El cuerpo del informe reporta 57 registros repetidos y sustenta su eliminación en dos elementos: la contigüidad de los identificadores y la georreferenciación por inmueble. El procedimiento completo se detalla a continuación.

cols_inmueble <- setdiff(names(vivienda), "id")

# Se marcan los grupos completos: duplicated() hacia adelante y hacia atrás
grupos <- (duplicated(vivienda[, cols_inmueble]) |
           duplicated(vivienda[, cols_inmueble], fromLast = TRUE)) &
           !is.na(vivienda$id)

# Distribución por tipo y zona
table(vivienda$tipo[grupos], vivienda$zona[grupos])
##              
##               Zona Norte Zona Oeste Zona Oriente Zona Sur
##   Apartamento         14          6            2       54
##   Casa                10          0            0       28
# Distancia entre identificadores dentro de cada grupo
dup <- vivienda[grupos, ]
dup$clave <- apply(dup[, cols_inmueble], 1, paste, collapse = "|")
brechas <- tapply(dup$id, dup$clave, function(x) max(x) - min(x))

summary(brechas)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   1.000   1.000   1.000   2.316   2.000  32.000
# Coordenadas únicas frente a número de barrios
nrow(unique(vivienda[, c("longitud", "latitud")]))
## [1] 5914
length(unique(vivienda$barrio))
## [1] 437

La distancia mediana entre identificadores es de una posición y la máxima de 32, sobre un rango de 8.322. Las 5.914 coordenadas distintas frente a 437 barrios confirman que la georreferenciación se asigna por inmueble.

14.4 Criterios alternativos de depuración geográfica

Antes de adoptar el criterio de proximidad al centroide se evaluaron dos alternativas sobre la base de casas de la zona norte.

b1 <- subset(vivienda_dep, tipo == "Casa" & zona == "Zona Norte")

# Registros que quedarían excluidos según distintos umbrales de latitud
sapply(c(3.43, 3.44, 3.45, 3.455, 3.46),
       function(u) sum(b1$latitud < u, na.rm = TRUE))
## [1]  97 108 140 175 199

El conteo aumenta de forma continua entre 3,43 y 3,46, sin un intervalo de baja densidad que permita fijar el corte sin arbitrariedad.

coords <- scale(b1[, c("longitud", "latitud")])

set.seed(2026)
km2 <- kmeans(coords, centers = 2, nstart = 25)
km3 <- kmeans(coords, centers = 3, nstart = 25)

table(km2$cluster)
## 
##   1   2 
## 505 191
tapply(b1$latitud, km2$cluster, median)
##       1       2 
## 3.47600 3.43191
table(km3$cluster)
## 
##   1   2   3 
## 242 357  97
tapply(b1$latitud, km3$cluster, median)
##        1        2        3 
## 3.471055 3.474680 3.400500

Con dos conglomerados, el grupo correspondiente al sector norte reúne 505 casas. Con tres, los grupos 1 y 2 presentan latitudes medianas de 3,4711 y 3,4747, de modo que el tercer conglomerado subdivide la zona norte en lugar de aislar registros de otras zonas.

El mismo procedimiento aplicado a la base de apartamentos de la zona sur dividió el segmento en dos sectores igualmente válidos, razón por la cual se descartó como criterio general.

14.5 Verificación de los modelos estimados

Se confirma que la estimación no descartó registros por valores faltantes.

c(base1 = nrow(base1_f), modelo1 = nobs(modelo1),
  base2 = nrow(base2_f), modelo2 = nobs(modelo2))
##   base1 modelo1   base2 modelo2 
##     494     494    2195    2195

14.6 Entorno de trabajo

sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] sandwich_3.1-3       car_3.1-5            carData_3.0-6       
##  [4] lmtest_0.9-40        zoo_1.9-0            plotly_4.12.1       
##  [7] patchwork_1.3.2      leaflet_2.2.3        nortest_1.0-4       
## [10] cluster_2.1.8.3      kableExtra_1.4.1     tidyr_1.3.2         
## [13] dplyr_1.2.1          paqueteMODELOS_0.1.0 summarytools_1.1.5  
## [16] knitr_1.51           gridExtra_2.3.1      GGally_2.4.0        
## [19] ggplot2_4.0.3        broom_1.0.13         boot_1.3-32         
## 
## loaded via a namespace (and not attached):
##  [1] tidyselect_1.2.1        viridisLite_0.4.3       farver_2.1.2           
##  [4] S7_0.2.2                fastmap_1.2.0           digest_0.6.39          
##  [7] timechange_0.4.0        lifecycle_1.0.5         magrittr_2.0.5         
## [10] compiler_4.6.1          rlang_1.3.0             sass_0.4.10            
## [13] tools_4.6.1             yaml_2.3.12             data.table_1.18.4      
## [16] htmlwidgets_1.6.4       plyr_1.8.9              xml2_1.6.0             
## [19] RColorBrewer_1.1-3      abind_1.4-8             withr_3.0.3            
## [22] purrr_1.2.2             grid_4.6.1              stats4_4.6.1           
## [25] scales_1.4.0            MASS_7.3-65             dichromat_2.0-1        
## [28] cli_3.6.6               rmarkdown_2.31          generics_0.1.4         
## [31] otel_0.2.0              rstudioapi_0.19.0       httr_1.4.8             
## [34] reshape2_1.4.5          cachem_1.1.0            pander_0.6.6           
## [37] stringr_1.6.0           matrixStats_1.5.0       base64enc_0.1-6        
## [40] vctrs_0.7.3             jsonlite_2.0.0          rapportools_1.2        
## [43] Formula_1.2-6           systemfonts_1.3.2       magick_2.9.1           
## [46] crosstalk_1.2.2         jquerylib_0.1.4         glue_1.8.1             
## [49] ggstats_0.13.0          leaflet.providers_3.0.0 lubridate_1.9.5        
## [52] stringi_1.8.9           gtable_0.3.6            tibble_3.3.1           
## [55] pillar_1.11.1           htmltools_0.5.9         R6_2.6.1               
## [58] tcltk_4.6.1             textshaping_1.0.5       evaluate_1.0.5         
## [61] lattice_0.22-9          backports_1.5.1         bslib_0.12.0           
## [64] Rcpp_1.1.2              svglite_2.2.2           checkmate_2.3.4        
## [67] xfun_0.60               pkgconfig_2.0.3