INTRODUCCIÓN

El mercado inmobiliario urbano presenta una dinámica compleja influenciada por múltiples factores, entre ellos las características físicas de las propiedades, su ubicación geográfica y el contexto socioeconómico de las zonas en las que se encuentran. Comprender estas interacciones es clave para identificar patrones de oferta y orientar la toma de decisiones tanto de inversionistas como de entidades del sector.

or otra parte el mercado inmobiliario de Cali atraviesa un periodo de desaceleración. Sin embargo, la reciente solicitud de una empresa internacional para la compra de dos viviendas de alta gama representa una oportunidad estratégica para C&A. Este informe tiene como objetivo analizar las características de cada propiedad solicitada y proporcionar recomendaciones basadas en modelos de estimación de precios para orientar la negociación y la toma de decisiones.

library(paqueteMODELOS)
library(knitr)
library(kableExtra)

# Cargar datos
datos <- paqueteMODELOS::vivienda


# Crear tabla descriptiva
Tabla_descriptiva <- data.frame(
  Variable = names(datos),
  Descripción = c(
    "Identificador único de la vivienda",
    "Zona de la ciudad",
    "Piso en el que se encuentra la vivienda",
    "Estrato socioeconómico",
    "Precio (millones de pesos)",
    "Área construida (m²)",
    "Número de parqueaderos",
    "Número de baños",
    "Número de habitaciones",
    "Tipo de vivienda",
    "Barrio",
    "Coordenada de longitud",
    "Coordenada de latitud"
  ),
  Tipo = c(
    "Categórica",
    "Categórica",
    "Categórica",
    "Categórica",
    "Numérica",
    "Numérica",
    "Numérica",
    "Numérica",
    "Numérica",
    "Categórica",
    "Categórica",
    "Ubicación",
    "Ubicación"
  )
)

# Mostrar tabla con estilo
Tabla_descriptiva %>%
  kable("html", caption = "Tabla descriptiva de variables") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2FF") %>% # Encabezado azul
  row_spec(1:nrow(Tabla_descriptiva), background = ifelse(1:nrow(Tabla_descriptiva) %% 2 == 0, "#f7f7f7", "white"))
Tabla descriptiva de variables
Variable Descripción Tipo
id Identificador único de la vivienda Categórica
zona Zona de la ciudad Categórica
piso Piso en el que se encuentra la vivienda Categórica
estrato Estrato socioeconómico Categórica
preciom Precio (millones de pesos) Numérica
areaconst Área construida (m²) Numérica
parqueaderos Número de parqueaderos Numérica
banios Número de baños Numérica
habitaciones Número de habitaciones Numérica
tipo Tipo de vivienda Categórica
barrio Barrio Categórica
longitud Coordenada de longitud Ubicación
latitud Coordenada de latitud Ubicación
# Función para normalizar variables categóricas, incluyendo la eliminación de tildes
normalizar_columnas_base <- function(df, columnas) {
  for (col in columnas) {
    # Convertir a minúsculas, eliminar espacios en blanco y tildes
    df[[col]] <- tolower(trimws(df[[col]]))
    df[[col]] <- gsub("á", "a", df[[col]])
    df[[col]] <- gsub("é", "e", df[[col]])
    df[[col]] <- gsub("í", "i", df[[col]])
    df[[col]] <- gsub("ó", "o", df[[col]])
    df[[col]] <- gsub("ú", "u", df[[col]])
    df[[col]] <- gsub("ñ", "n", df[[col]])
  }
  return(df)
}

# Aplicar la función a las columnas seleccionadas
datos2 <- normalizar_columnas_base(datos,c("id","zona","estrato","tipo","barrio"))

CASAS ZONA NORTE DE LA CIUDAD DE CALI

Se ha creado una nueva base de datos que contiene exclusivamente la información de las viviendas de tipo Casa ubicadas en la Zona Norte de la ciudad. De un total de 8321 de registros de la base de datos original viviendas, se han identificado 722 que cumplen con ambas características, lo que representa un 8.7% del total de la base de datos. Este subconjunto de datos será utilizado para el análisis posterior.

library(dplyr)

Base1<-datos2%>%
 filter(tipo=="casa",zona=="zona norte")

head(Base1, 3)
## # A tibble: 3 × 13
##   id    zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <chr> <chr>   <chr> <chr>     <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1 1209  zona n… 02    5           320       150            2      4            6
## 2 1592  zona n… 02    5           780       380            2      3            3
## 3 4057  zona n… 02    6           750       445           NA      7            6
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# tab: frecuencias en el filtrado (Base1)
tab <- with(Base1, table(zona, tipo))

# denominador: total de la base original
# si tu base original se llama 'datos' (como antes):
denom <- nrow(datos)
# (alternativa) denom <- nrow(paqueteMODELOS::vivienda)

# formatear cada celda como: n (xx.x%)
fmt <- function(n) sprintf("%d (%.1f%%)", n, 100 * n / denom)

cell_mat <- matrix(
  fmt(as.vector(tab)),
  nrow = nrow(tab), ncol = ncol(tab),
  dimnames = dimnames(tab)
)

# totales (también vs base original)
rs <- rowSums(tab)
cs <- colSums(tab)
n_tot <- sum(tab)

out <- cbind(cell_mat, Total = fmt(rs))
out <- rbind(out, Total = c(fmt(cs), fmt(n_tot)))

knitr::kable(out, caption = "Cruzada zona × tipo: n (porcentaje vs base original)")
Cruzada zona × tipo: n (porcentaje vs base original)
casa Total
zona norte 722 (8.7%) 722 (8.7%)
Total 722 (8.7%) 722 (8.7%)

Análisis del Mapa

Se evidencia que en el mapa se grafican ubicaciones de viviendas tipo casa no precisamente en el norte de la ciudad. Esto puede ser causado al momento de la recolección de datos, ya sea porque asignaron de forma incorrecta la zona o posiblemente un error de tipeo en las coordenadas.

# Mapa interactivo de Cali con las casas de Base1
library(leaflet)

# Asegura que las coordenadas sean numéricas
Base1$latitud  <- as.numeric(Base1$latitud)
Base1$longitud <- as.numeric(Base1$longitud)

# Bounding box para encuadrar los puntos (fallback a centro de Cali si faltan NAs)
bb <- with(na.omit(Base1), list(
  xmin = min(longitud, na.rm = TRUE),
  xmax = max(longitud, na.rm = TRUE),
  ymin = min(latitud,  na.rm = TRUE),
  ymax = max(latitud,  na.rm = TRUE)
))

mapa <- leaflet(Base1) |>
  addProviderTiles(leaflet::providers$CartoDB.Positron) |>
  # Centro de Cali por si el bbox no sirve (valores NA o solo 1 punto)
  setView(lng = -76.5320, lat = 3.4516, zoom = 12) |>
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 5, stroke = FALSE, fillOpacity = 0.7,
    popup = ~paste0(
      "<b>", if ("tipo" %in% names(Base1)) tipo else "Inmueble", "</b><br>",
      if ("barrio" %in% names(Base1)) paste0(barrio, "<br>") else "",
      if ("zona"   %in% names(Base1)) paste0(zona, "<br>")   else ""
    )
  ) |>
  addScaleBar(position = "bottomleft")

# Si hay al menos 2 puntos distintos, ajusta a su extensión
if (is.finite(bb$xmin) && is.finite(bb$xmax) && is.finite(bb$ymin) && is.finite(bb$ymax) &&
    bb$xmin < bb$xmax && bb$ymin < bb$ymax) {
  mapa <- mapa |> fitBounds(lng1 = bb$xmin, lat1 = bb$ymin, lng2 = bb$xmax, lat2 = bb$ymax)
}

mapa

Se encuentra de inconsistencias en la base de datos filtrada,a continuacion se presenta la siguiente lista de barrios que, si bien aparecen en el filtro inicial como parte de la Zona Norte, geográficamente se encuentran en otras áreas de la ciudad

barrios <- sort(unique(trimws(na.omit(Base1$barrio))))
cols <- 3
pad  <- (cols - length(barrios) %% cols) %% cols
m    <- matrix(c(barrios, rep("", pad)), ncol = cols, byrow = TRUE)
colnames(m) <- paste0("Barrio ", 1:cols)

knitr::kable(m, caption = "Barrios (distribuidos en columnas)")
Barrios (distribuidos en columnas)
Barrio 1 Barrio 2 Barrio 3
acopi alameda del rio alamos
atanasio girardot barranquilla barrio tranquilo y
base aérea berlin brisas de los
brisas del guabito cali calibella
calima calimio norte cambulos
centenario chapinero chipichape
ciudad los alamos colinas del bosque cristales
el bosque el cedro el gran limonar
el guabito el sena el trébol
evaristo garcia flora industrial floralia
gaitan granada jorge eliecer gaitan
juanamb√∫ la base la campina
la esmeralda la flora la floresta
la merced la rivera la rivera i
la rivera ii la riviera la villa del
las acacias las américas las ceibas
las delicias las granjas los andes
los guaduales los guayacanes manzanares
menga metropolitano del norte nueva tequendama
oasis de comfandi occidente pacara
parque residencial el paseo de los paso del comercio
poblado campestre popular portada de comfandi
portales de comfandi porvenir prados del norte
quintas de salomia rozo la torre salomia
san luis san vicente santa barbara
santa monica santa monica norte santa monica residencial
santander tejares de san torres de comfandi
union de vivienda urbanizacion barranquilla urbanizacion la flora
urbanizacion la merced urbanizacion la nueva valle del lili
versalles villa colombia villa de veracruz
villa del prado villa del sol villas de veracruz
vipasa zona norte zona oriente

Nuevo Filtro por Coordenadas

Para propósitos prácticos y de análisis, se realizará un nuevo filtro para incluir solo las viviendas que están realmente ubicadas en la zona norte de la ciudad de Cali. Este filtro se hará a través de las coordenadas de latitud y longitud.

Base1a<-datos2%>%
 filter(latitud>=3.458,longitud>=-76.54,tipo=="casa")
# Mapa interactivo de Cali con las casas de Base1
library(leaflet)

# Asegura que las coordenadas sean numéricas
Base1a$latitud  <- as.numeric(Base1a$latitud)
Base1a$longitud <- as.numeric(Base1a$longitud)

# Bounding box para encuadrar los puntos (fallback a centro de Cali si faltan NAs)
bb <- with(na.omit(Base1a), list(
  xmin = min(longitud, na.rm = TRUE),
  xmax = max(longitud, na.rm = TRUE),
  ymin = min(latitud,  na.rm = TRUE),
  ymax = max(latitud,  na.rm = TRUE)
))

mapa <- leaflet(Base1a) |>
  addProviderTiles(leaflet::providers$CartoDB.Positron) |>
  # Centro de Cali por si el bbox no sirve (valores NA o solo 1 punto)
  setView(lng = -76.5320, lat = 3.4516, zoom = 12) |>
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 5, stroke = FALSE, fillOpacity = 0.7,
    popup = ~paste0(
      "<b>", if ("tipo" %in% names(Base1a)) tipo else "Inmueble", "</b><br>",
      if ("barrio" %in% names(Base1a)) paste0(barrio, "<br>") else "",
      if ("zona"   %in% names(Base1a)) paste0(zona, "<br>")   else ""
    )
  ) |>
  addScaleBar(position = "bottomleft")

# Si hay al menos 2 puntos distintos, ajusta a su extensión
if (is.finite(bb$xmin) && is.finite(bb$xmax) && is.finite(bb$ymin) && is.finite(bb$ymax) &&
    bb$xmin < bb$xmax && bb$ymin < bb$ymax) {
  mapa <- mapa |> fitBounds(lng1 = bb$xmin, lat1 = bb$ymin, lng2 = bb$xmax, lat2 = bb$ymax)
}

mapa

ANALISIS EXPLORTORIO

library(summarytools)

datos_filtrados <- Base1a[, !names(Base1a) %in% c("id", "latitud", "longitud")]


print(descr(datos_filtrados), method = "render")
## Non-numerical variable(s) ignored: zona, piso, estrato, tipo, barrio

Descriptive Statistics

datos_filtrados

N: 581
areaconst banios habitaciones parqueaderos preciom
Mean 251.05 3.46 4.55 2.19 417.29
Std.Dev 166.25 1.46 1.75 1.46 259.53
Min 30.00 0.00 0.00 1.00 85.00
Q1 130.00 2.00 3.00 1.00 235.00
Median 228.00 3.00 4.00 2.00 360.00
Q3 320.00 4.00 5.00 3.00 520.00
Max 1500.00 10.00 10.00 10.00 1800.00
MAD 145.29 1.48 1.48 1.48 207.56
IQR 190.00 2.00 2.00 2.00 285.00
CV 0.66 0.42 0.38 0.67 0.62
Skewness 2.41 0.70 0.96 1.91 2.00
SE.Skewness 0.10 0.10 0.10 0.12 0.10
Kurtosis 11.77 0.95 1.25 4.57 6.00
N.Valid 581 581 581 386 581
N 581 581 581 581 581
Pct.Valid 100.00 100.00 100.00 66.44 100.00

Generated by summarytools 1.1.1 (R version 4.4.2)
2025-09-03

1. Identificar datos duplicados

duplicados <- Base1a[duplicated(Base1a), ]
sum(duplicated(Base1a))
## [1] 0
Base1a <- unique(Base1a)

2. Identificar datos faltante

blancos <- is.na(Base1a)

# Contar el número de celdas vacías (NA) por columna
columna_blancos <- colSums(blancos)

# Ver la cantidad de celdas vacías por columna
#print(columna_blancos)


tabla_blancos <- data.frame(
  Variable   = names(columna_blancos),
  NA_n       = as.integer(columna_blancos),
  Porcentaje = round(columna_blancos / nrow(Base1a) * 100, 2),
  row.names  = NULL
)


tabla_blancos %>%
  kable("html", caption = "Celdas NA por columna") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Celdas NA por columna
Variable NA_n Porcentaje
id 0 0.00
zona 0 0.00
piso 248 42.69
estrato 0 0.00
preciom 0 0.00
areaconst 0 0.00
parqueaderos 195 33.56
banios 0 0.00
habitaciones 0 0.00
tipo 0 0.00
barrio 0 0.00
longitud 0 0.00
latitud 0 0.00

Debido a que se ha encontrado una concentración de datos faltantes en dos variables, se procederá a realizar una imputación utilizando el método de la moda.

# Copia de trabajo
datos_imp <- Base1a

# (opcional) columnas a excluir de la imputación
excluir <- c("id")  # agrega "latitud","longitud", etc. si no quieres imputarlas
cols <- setdiff(names(datos_imp), excluir)

# Función de moda (sirve para numéricas, factor y character)
moda <- function(x) {
  y <- x
  if (is.character(y)) y[y == ""] <- NA  # trata vacíos como NA (opcional)
  y <- y[!is.na(y)]
  if (!length(y)) return(NA)
  ux <- unique(y)
  ux[which.max(tabulate(match(y, ux)))]
}

# Imputación con moda por columna (simple y directa)
for (v in cols) {
  m <- moda(datos_imp[[v]])
  if (is.na(m)) next
  if (is.factor(datos_imp[[v]])) {
    # asegura que la moda exista como nivel
    lv <- levels(datos_imp[[v]])
    if (!(as.character(m) %in% lv)) levels(datos_imp[[v]]) <- c(lv, as.character(m))
    datos_imp[[v]][is.na(datos_imp[[v]])] <- as.character(m)
  } else {
    datos_imp[[v]][is.na(datos_imp[[v]])] <- m
  }
}


tabla_na <- data.frame(
  Variable   = names(datos_imp),
  NA_n       = colSums(is.na(datos_imp)),
  Porcentaje = round(colMeans(is.na(datos_imp)) * 100, 2),
  row.names  = NULL
)


tabla_na %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Variable NA_n Porcentaje
id 0 0
zona 0 0
piso 0 0
estrato 0 0
preciom 0 0
areaconst 0 0
parqueaderos 0 0
banios 0 0
habitaciones 0 0
tipo 0 0
barrio 0 0
longitud 0 0
latitud 0 0

3. Identificar Valores atipicos

# 1) Excluir columnas no numéricas
vars_excluir <- c("id","zona","tipo","barrio","latitud","longitud")
datos_num <- datos_imp[, setdiff(names(datos_imp), vars_excluir), drop = FALSE]
datos_num <- datos_num[, sapply(datos_num, is.numeric), drop = FALSE]

# 2) Transformación log estable (log1p = log(1+x))
datos_log <- as.data.frame(lapply(datos_num, log1p))

# 3) Outliers por IQR, devolviendo índices (mejor para localizar filas)
detectar_idx_iqr <- function(x){
  x <- x[!is.na(x)]
  if (length(x) < 2) return(integer(0))
  Q1 <- quantile(x, .25); Q3 <- quantile(x, .75); I <- Q3 - Q1
  which(x < (Q1 - 1.5*I) | x > (Q3 + 1.5*I))
}

idx_por_var <- lapply(datos_log, detectar_idx_iqr)
resumen <- data.frame(
  Variable = names(idx_por_var),
  Valores_Atipicos = sapply(idx_por_var, length),
  row.names = NULL
)




#Conteo de válidos por variable (en datos_log)
n_valid <- sapply(datos_log, function(x) sum(!is.na(x)))

# Conteo de outliers (ya lo tienes en idx_por_var)
n_out <- sapply(idx_por_var, length)

# Porcentaje de outliers
pct_out <- ifelse(n_valid > 0, round(100 * n_out / n_valid, 2), NA_real_)

# Tabla final
tabla_out <- data.frame(
  Variable   = names(n_out),
  N_validos  = as.integer(n_valid[names(n_out)]),
  N_outliers = as.integer(n_out),
  Porcentaje = pct_out,
  row.names  = NULL
)

# Ordenar por mayor porcentaje (opcional)
tabla_out <- tabla_out[order(-tabla_out$Porcentaje), ]

tabla_out %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Variable N_validos N_outliers Porcentaje
3 parqueaderos 581 28 4.82
5 habitaciones 581 7 1.20
4 banios 581 6 1.03
2 areaconst 581 3 0.52
1 preciom 581 1 0.17
vars <- c("preciom","areaconst","parqueaderos","banios","habitaciones")
vars <- intersect(vars, names(datos_imp))

# Conversión segura a numérico
to_num <- function(x) if (is.numeric(x)) x else suppressWarnings(as.numeric(as.character(x)))
X <- setNames(lapply(datos_imp[vars], to_num), vars)
# X: data.frame/lista con variables numéricas
nmv  <- names(X)
n    <- length(nmv)

# Layout automático
nc <- ceiling(sqrt(n)); nr <- ceiling(n / nc)

op <- par(mfrow = c(nr, nc), mar = c(4, 4, 2, 1), bg = "white")

# Paleta disponible
cols <- grDevices::hcl.colors(n, palette = "Set2")

i <- 0
for (nm in nmv) {
  i <- i + 1
  x <- X[[nm]]
  x <- x[is.finite(x) & x > -1]  # log1p válido

  if (length(x)) {
    # Fondo con rejilla
    plot(NA, xlab = "", ylab = "log(1 + x)", xlim = c(0.5, 1.5),
         ylim = range(log1p(x), na.rm = TRUE), axes = FALSE, main = nm)
    grid(nx = NA, ny = NULL, col = gray(0.9), lty = 3)
    axis(2); box(bty = "n")

    # Boxplot con color
    boxplot(log1p(x), add = TRUE,
            col     = adjustcolor(cols[i], 0.85),
            border  = adjustcolor(cols[i], 0.9),
            notch   = TRUE, boxwex = 0.6,
            medcol  = "white", medlwd = 2,
            whisklty = 1, outpch = 16,
            outcol  = adjustcolor(cols[i], 0.85),
            outbg   = adjustcolor(cols[i], 0.55),
            axes = FALSE, names = "")
  } else {
    plot.new(); title(main = paste0(nm, " (sin datos válidos para log1p)"))
  }
}

par(op)

Se optó por remover los valores atípicos en las variables parqueaderos, baños, área construida y precio, ya que en conjunto representan menos del 5 % del total de registros. Además, para los tipos de vivienda analizados, resulta poco coherente que existan valores tan elevados en el número de baños y parqueaderos.

# === Eliminar outliers (IQR en log1p) excepto la variable 'habitaciones' ===

# 1) Variables numéricas a evaluar (excluye id/zona/tipo/barrio/lat/long y 'habitaciones')
vars_excluir <- c("id","zona","tipo","barrio","latitud","longitud")
num_cols <- names(datos_imp)[sapply(datos_imp, is.numeric)]
vars_filtrar <- setdiff(intersect(num_cols, setdiff(names(datos_imp), vars_excluir)), "habitaciones")

# 2) Máscara de outliers en log1p alineada a filas
is_outlier_log1p <- function(x, k = 1.5){
  m  <- rep(FALSE, length(x))
  ok <- is.finite(x) & (x > -1)       # log1p definido para x > -1
  if (sum(ok) < 2) return(m)
  lx <- log1p(x[ok])
  Q1 <- quantile(lx, .25); Q3 <- quantile(lx, .75); I <- Q3 - Q1
  m_ok <- (lx < Q1 - k*I) | (lx > Q3 + k*I)
  m[ok] <- m_ok
  m
}

if (length(vars_filtrar) == 0) {
  message("No hay variables para filtrar (aparte de 'habitaciones').")
  datos_sin_out <- datos_imp
} else {
  masks <- lapply(vars_filtrar, function(v) is_outlier_log1p(datos_imp[[v]]))
  mask_any <- Reduce("|", masks)         # outlier en cualquiera de las variables
  cat("Filas a eliminar:", sum(mask_any), "de", nrow(datos_imp), "\n")
  datos_sin_out <- datos_imp[!mask_any, ]
}
## Filas a eliminar: 38 de 581

ANALISIS DE CORRELACIÓN

library(dplyr); library(plotly)

df <- datos_sin_out

# -- Dispersión interactiva: Precio vs Área (color por estrato) ---
p_area <- plot_ly(
  df, x = ~areaconst, y = ~preciom,
  color = ~as.factor(estrato),
  type = "scatter", mode = "markers",
  text = ~paste0("<br>Barrio: ", barrios,
                 "<br>Estrato: ", estrato,
                 "<br>Baños: ", banios,
                 "<br>Habitaciones: ", habitaciones),
  hoverinfo = "text+x+y"
) |>
  layout(title = "Precio vs Área (color: Estrato)",
         xaxis = list(title = "Área construida"),
         yaxis = list(title = "Precio"))
p_area

El análisis de la correlación entre el precio y el área construida, con el estrato como factor diferenciador, revela una relación positiva y clara: a medida que el área construida aumenta, el precio de la vivienda también lo hace, formando una diagonal ascendente en la nube de puntos. Se observa una prima significativa por estrato, ya que para un mismo metraje, las viviendas en estratos más altos (5 y 6) tienden a tener precios superiores a las de estratos 3 y 4, indicando que este factor agrega un valor independiente del área.

library(dplyr); library(plotly)

df <- datos_sin_out %>% filter(areaconst > 0, preciom > 0)

p_area <- plot_ly(
  df, x = ~areaconst, y = ~preciom,
  color = ~as.factor(estrato),
  type = "scatter", mode = "markers",
  text = ~paste0("<br>Barrio: ", barrios,
                 "<br>Estrato: ", estrato,
                 "<br>Baños: ", banios,
                 "<br>Habitaciones: ", habitaciones),
  hoverinfo = "text+x+y"
) %>%
  layout(
    title = "Precio vs Área (color: Estrato) — ejes log10",
    xaxis = list(title = "Área construida (log10)", type = "log"),
    yaxis = list(title = "Precio (log10)",           type = "log")
  )

p_area

En esta grafica se realiza análisis a una escala logarítmica para confirmar la existencia de una relación lineal entre los datos.

# -- Boxplots interactivos: Precio por Estrato y por Zona ---
p_estrato <- plot_ly(df, x = ~as.factor(estrato), y = ~preciom,
                     type = "box", boxpoints = "outliers") |>
  layout(title = "Precio por estrato",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio"))
p_estrato

Respecto al estrato, se observa una clara tendencia ascendente: la mediana de los precios aumenta consistentemente a medida que el estrato sube del 3 al 6, lo que confirma una asociación positiva y directa entre el estrato socioeconómico y el precio de la vivienda. Al cruzar estos datos con el gráfico de dispersión, se evidencian valores atípicos con precios inusualmente altos en los estratos 3, 5 y 6. Estos puntos no son atípicos debido a errores en los datos, sino que representan viviendas con un área construida significativamente mayor a la esperada para sus respectivos estratos.

num_df <- df |>
  select(preciom, areaconst, estrato, banios, habitaciones) |>
  mutate(across(everything(), as.numeric)) |>
  na.omit()

cm <- cor(num_df, use = "complete.obs", method = "spearman")

plot_ly(x = colnames(cm), y = rownames(cm), z = cm, type = "heatmap") |>
  layout(title = "Correlaciones (Spearman): precio y predictores")

El mapa de calor de correlaciones (Spearman) confirma estas observaciones. La relación más fuerte y positiva se da entre el precio y el área construida, indicando que a mayor metraje, mayor es el precio. La correlación entre el precio y el estrato es moderada a alta y también positiva, lo que subraya la influencia del estrato en el valor de la propiedad, independientemente de su área. Asimismo, la relación entre el precio y el número de baños es moderada, mientras que la del precio y el número de habitaciones resulta ser la más débil entre las variables analizadas.

MODELO DE REGRESION

m_lin <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
            data = df )
summary(m_lin)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -672.31  -56.65  -14.94   37.29  929.10 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   33.81258   18.64787   1.813   0.0704 .  
## areaconst      0.85821    0.05235  16.395  < 2e-16 ***
## estrato4      70.20860   16.48783   4.258 2.43e-05 ***
## estrato5     109.73976   16.07940   6.825 2.39e-11 ***
## estrato6     307.42093   31.65216   9.712  < 2e-16 ***
## habitaciones   4.45781    4.53561   0.983   0.3261    
## parqueaderos  12.79676    7.56011   1.693   0.0911 .  
## banios        13.54556    5.92898   2.285   0.0227 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 129.1 on 535 degrees of freedom
## Multiple R-squared:  0.6867, Adjusted R-squared:  0.6826 
## F-statistic: 167.5 on 7 and 535 DF,  p-value: < 2.2e-16

INTERPRETACIÓN DE COEFICIENTES

Intercepto = 33.8 (p = 0.07) → no significativo. No tiene interpretación práctica (precio cuando todas las x son 0).

areaconst = 0.858 (SE 0.052; p < 2e-16) → significativo. Cada +1 m² se asocia con +0.858 millones (≈ $858 mil) en el precio, ceteris paribus. IC95% ≈ [0.756, 0.961].

estrato 4 = +70.2 M (p = 2.4e-05) → significativo. Frente a estrato 3, +70 millones manteniendo lo demás constante. IC95% ≈ [37.8, 102.6].

estrato 5 = +109.7 M (p = 2.3e-11) → significativo. IC95% ≈ [78.2, 141.3].

estrato 6 = +307.4 M (p < 2e-16) → significativo y grande. IC95% ≈ [245.4, 369.5].

habitaciones = +4.46 M (p = 0.326) → no significativo. IC95% ≈ [-4.43, 13.35]. Probable colinealidad con el área: cuando ya controlas por m², “tener más cuartos” no añade información adicional de forma clara.

parqueaderos = +12.8 M (p = 0.091) → marginal (10%). IC95% ≈ [-2.0, 27.6]. Señal positiva lógica, pero evidencia débil al 5%.

baños = +13.55 M (p = 0.022) → significativo. IC95% ≈ [1.92, 25.17]. Un baño extra incrementa el precio incluso controlando por área.

El área y el estrato son los motores principales del precio (más m² y mejores condiciones socio–urbanas ⇒ mayor valor). Baños también agrega valor; parqueadero apunta en la dirección esperada pero con significancia marginal; habitaciones pierde relevancia al ya controlar por m² (tamaño y distribución están correlacionados).

INTERPRETACIÓN DE R2

El modelo explica ~68.7% de la variación observada en el precio entre viviendas.

Aun así, ~31% de la variación queda sin explicar: es normal en datos inmobiliarios porque faltan atributos finos (ubicación exacta, estado/edad, acabados, lote, amenities, etc.)

¿Cómo mejorarlo?

  1. Agregar variables de ubicación como el barrio
  2. pasar la variable precio y area a una base logaritmica (Log10) pero para este caso de la oferta no estan aclarando un barrio especifico
m_log <- lm(log10(preciom) ~ log10(areaconst) + estrato + habitaciones + parqueaderos + banios,
            data = df )
summary(m_log)
## 
## Call:
## lm(formula = log10(preciom) ~ log10(areaconst) + estrato + habitaciones + 
##     parqueaderos + banios, data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.28789 -0.06730 -0.00805  0.06316  0.44423 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      1.205927   0.049853  24.190  < 2e-16 ***
## log10(areaconst) 0.484631   0.026841  18.056  < 2e-16 ***
## estrato4         0.105553   0.014387   7.337 8.16e-13 ***
## estrato5         0.150568   0.014390  10.464  < 2e-16 ***
## estrato6         0.265298   0.027477   9.655  < 2e-16 ***
## habitaciones     0.005084   0.003902   1.303   0.1932    
## parqueaderos     0.013411   0.006393   2.098   0.0364 *  
## banios           0.022390   0.005106   4.385 1.40e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1103 on 535 degrees of freedom
## Multiple R-squared:  0.7879, Adjusted R-squared:  0.7851 
## F-statistic: 283.9 on 7 and 535 DF,  p-value: < 2.2e-16

Conclusiones sobre la mejora del modelo logarítmico

El cambio a una escala logarítmica ha mejorado significativamente el modelo lineal en pesos. En primer lugar, se ha observado un mayor poder explicativo. El coeficiente de determinación (R2) subió de 0.687 a 0.788, y el R2 ajustado pasó de 0.683 a 0.785, lo que representa un aumento de aproximadamente 10 puntos porcentuales. Esto demuestra que el modelo en escala logarítmica capta una mayor proporción de la variación en el precio de las viviendas.

Además, el error es más interpretable y estable. El error residual estándar se redujo de 129.1 millones (en la escala original) a 0.1103 (en la escala logarítmica). En términos relativos, esto equivale a un error típico de ≈±29% sobre el precio, lo cual es mucho más intuitivo que el valor original. Los residuos en la escala logarítmica también muestran menos heterocedasticidad (una varianza más constante), lo que hace que el modelo sea más robusto.

VALIDACIÓN DE SUPUESTOS

# Instala los paquetes si no los tienes:
# install.packages("ggplot2")
# install.packages("plotly")
# install.packages("dplyr")

# Carga las librerías necesarias
library(ggplot2)
library(plotly)
library(dplyr)

# Supongamos que ya tienes el modelo m_log ajustado
# m_log <- lm(log10(precio) ~ areaconst + estrato + num_habitaciones + num_banos, data = df)

# Extrae los datos y calcula los valores para los gráficos de diagnóstico
modelo_df <- data.frame(
  fitted_values = fitted(m_log),
  residuals = resid(m_log),
  # Se utiliza rstandard() para obtener los residuales estandarizados,
  # que son necesarios para el gráfico de Escala-Ubicación.
  sqrt_abs_residuals = sqrt(abs(rstandard(m_log))),
  leverage = hatvalues(m_log),
  cooks_distance = cooks.distance(m_log)
)

# ----------------------------------------------------
# 1. Gráfico de Residuales vs. Valores Ajustados
# ----------------------------------------------------
# Propósito: Evaluar la linealidad y la varianza constante (homocedasticidad)
# Si los puntos forman una banda horizontal sin patrón, el modelo es adecuado.

p1 <- ggplot(modelo_df, aes(x = fitted_values, y = residuals)) +
  geom_point(alpha = 0.5, color = "#2c7bb6") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) +
  geom_smooth(method = "loess", se = FALSE, color = "#fdae61", size = 1) +
  labs(
    title = "Residuales vs. Valores Ajustados",
    x = "Valores Ajustados (log10)",
    y = "Residuales"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p1)
# ----------------------------------------------------
# 2. Gráfico Q-Q Normal
# ----------------------------------------------------
# Propósito: Verificar si los residuales siguen una distribución normal.
# Los puntos deben seguir la línea diagonal.

p2 <- ggplot(modelo_df, aes(sample = residuals)) +
  stat_qq(color = "#2c7bb6") +
  stat_qq_line(color = "red", linetype = "dashed", size = 1) +
  labs(
    title = "Gráfico Q-Q Normal",
    x = "Cuantiles Teóricos",
    y = "Cuantiles de los Residuales"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p2)
# ----------------------------------------------------
# 3. Gráfico de Escala-Ubicación
# ----------------------------------------------------
# Propósito: Evaluar la homocedasticidad (varianza constante de los residuales).
# Los puntos deben formar una banda horizontal sin patrón.

p3 <- ggplot(modelo_df, aes(x = fitted_values, y = sqrt_abs_residuals)) +
  geom_point(alpha = 0.5, color = "#2c7bb6") +
  geom_smooth(method = "loess", se = FALSE, color = "#fdae61", size = 1) +
  labs(
    title = "Gráfico de Escala-Ubicación",
    x = "Valores Ajustados (log10)",
    y = "Raíz Cuadrada de los Residuales Estandarizados"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p3)
# ----------------------------------------------------
# 4. Gráfico de Residuales vs. Leverage
# ----------------------------------------------------
# Propósito: Identificar puntos influyentes que pueden afectar los coeficientes del modelo.
# Los puntos con alta leverage (alto en el eje x) y/o altos residuales (alto en el eje y) son de interés.

p4 <- ggplot(modelo_df, aes(x = leverage, y = residuals, text = paste("Cook's D:", round(cooks_distance, 4)))) +
  geom_point(aes(color = cooks_distance), alpha = 0.7) +
  scale_color_viridis_c(name = "Distancia de Cook") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  labs(
    title = "Residuales vs. Leverage",
    x = "Leverage",
    y = "Residuales"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p4, tooltip = "text")

1. Gráfico de Residuales vs. Valores Ajustados

Este gráfico nos ayuda a validar la suposición de linealidad y homocedasticidad. La línea de tendencia naranja sigue de cerca la línea roja discontinua, que representa el cero. Esto indica que no hay un patrón claro en los residuales, lo que apoya la suposición de linealidad. La dispersión de los puntos es relativamente constante a lo largo del eje x, lo que sugiere que el modelo ha corregido en gran medida el problema de heterocedasticidad que se observaba en el modelo lineal.

2. Gráfico Q-Q Normal

Este gráfico evalúa la suposición de normalidad en los residuales. La mayoría de los puntos se alinean con la línea de referencia diagonal, lo que indica que los residuales siguen una distribución normal. Sin embargo, se observa una ligera desviación en los extremos (las “colas” de la distribución), lo que es común en datos reales y no suele ser un problema grave para el modelo.

3. Gráfico de Escala-Ubicación

Este gráfico es otra forma de verificar la homocedasticidad (varianza constante). La línea de tendencia naranja es relativamente plana, lo que confirma que la dispersión de los residuales estandarizados es bastante uniforme a lo largo de los valores ajustados.

4. Gráfico de Residuales vs. Leverage

Este gráfico es crucial para identificar puntos influyentes. Los puntos con una alta Distancia de Cook (indicados por un color más claro) son aquellos que podrían estar afectando desproporcionadamente los coeficientes de tu modelo. Aunque hay algunos puntos con un leverage moderado, la mayoría tiene una Distancia de Cook baja. Esto significa que, aunque hay algunos valores con potencial de influencia, no hay un impacto significativo que obligue a removerlos o a reconsiderar el modelo.

PRONOSTICO

Para una casa en la zona norte con:

area de 200 mt2

1 parqueadero

2 baños

4 habitaciones

Estrato 5

El precio es de: 366 millones

newdata2 <- data.frame(
  areaconst    = 200,   # m²
  estrato      = "5",
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)




pred_log10_2   <- predict(m_log, newdata = newdata2)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log2_mill <- 10^pred_log10_2 * smear


pred_log2_mill
##        1 
## 366.9063

Para una casa en la zona norte de Cali con:

area de 200 mt2

1 parqueadero

2 baños

4 habitaciones

Estrato 4

El precio es de: 330 millones

newdata3 <- data.frame(
  areaconst    = 200,   # m²
  estrato      = "4",
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)




pred_log10_3   <- predict(m_log, newdata = newdata3)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log3_mill <- 10^pred_log10_3 * smear


pred_log3_mill
##       1 
## 330.781

APARTAMENTO ZONA NORTE DE LA CIUDAD DE CALI

library(dplyr)

Base2a<-datos2%>%
 filter(latitud<=3.41,longitud>=-76.54,tipo=="casa")
# Mapa interactivo de Cali con las casas de Base1
library(leaflet)

# Asegura que las coordenadas sean numéricas
Base2a$latitud  <- as.numeric(Base2a$latitud)
Base2a$longitud <- as.numeric(Base2a$longitud)

# Bounding box para encuadrar los puntos (fallback a centro de Cali si faltan NAs)
bb <- with(na.omit(Base2a), list(
  xmin = min(longitud, na.rm = TRUE),
  xmax = max(longitud, na.rm = TRUE),
  ymin = min(latitud,  na.rm = TRUE),
  ymax = max(latitud,  na.rm = TRUE)
))

mapa <- leaflet(Base2a) |>
  addProviderTiles(leaflet::providers$CartoDB.Positron) |>
  # Centro de Cali por si el bbox no sirve (valores NA o solo 1 punto)
  setView(lng = -76.5320, lat = 3.4516, zoom = 12) |>
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 5, stroke = FALSE, fillOpacity = 0.7,
    popup = ~paste0(
      "<b>", if ("tipo" %in% names(Base2a)) tipo else "Inmueble", "</b><br>",
      if ("barrio" %in% names(Base2a)) paste0(barrio, "<br>") else "",
      if ("zona"   %in% names(Base2a)) paste0(zona, "<br>")   else ""
    )
  ) |>
  addScaleBar(position = "bottomleft")

# Si hay al menos 2 puntos distintos, ajusta a su extensión
if (is.finite(bb$xmin) && is.finite(bb$xmax) && is.finite(bb$ymin) && is.finite(bb$ymax) &&
    bb$xmin < bb$xmax && bb$ymin < bb$ymax) {
  mapa <- mapa |> fitBounds(lng1 = bb$xmin, lat1 = bb$ymin, lng2 = bb$xmax, lat2 = bb$ymax)
}

mapa

ANALISIS EXPLORATORIO

library(summarytools)

datos_filtrados2 <- Base2a[, !names(Base2a) %in% c("id", "latitud", "longitud")]


print(descr(datos_filtrados2), method = "render")
## Non-numerical variable(s) ignored: zona, piso, estrato, tipo, barrio

Descriptive Statistics

datos_filtrados2

N: 1124
areaconst banios habitaciones parqueaderos preciom
Mean 268.91 4.13 4.22 2.43 597.00
Std.Dev 180.62 1.39 1.32 1.57 376.83
Min 50.00 0.00 0.00 1.00 80.00
Q1 150.00 3.00 3.00 1.00 330.00
Median 230.00 4.00 4.00 2.00 460.00
Q3 332.00 5.00 5.00 3.00 790.00
Max 1600.00 10.00 10.00 10.00 1900.00
MAD 133.43 1.48 1.48 1.48 281.69
IQR 182.00 2.00 2.00 2.00 460.00
CV 0.67 0.34 0.31 0.64 0.63
Skewness 2.36 0.52 0.82 1.71 1.32
SE.Skewness 0.07 0.07 0.07 0.08 0.07
Kurtosis 9.57 0.80 2.31 3.70 1.26
N.Valid 1124 1124 1124 991 1124
N 1124 1124 1124 1124 1124
Pct.Valid 100.00 100.00 100.00 88.17 100.00

Generated by summarytools 1.1.1 (R version 4.4.2)
2025-09-03

1. Identificar datos duplicados

duplicados2 <- Base2a[duplicated(Base2a), ]
sum(duplicated(Base2a))
## [1] 0

2. Identificar datos faltantes

blancos2 <- is.na(Base2a)

# Contar el número de celdas vacías (NA) por columna
columna_blancos2 <- colSums(blancos2)

# Ver la cantidad de celdas vacías por columna
#print(columna_blancos)


tabla_blancos2 <- data.frame(
  Variable   = names(columna_blancos2),
  NA_n       = as.integer(columna_blancos2),
  Porcentaje = round(columna_blancos2 / nrow(Base2a) * 100, 2),
  row.names  = NULL
)


tabla_blancos2 %>%
  kable("html", caption = "Celdas NA por columna") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Celdas NA por columna
Variable NA_n Porcentaje
id 0 0.00
zona 0 0.00
piso 394 35.05
estrato 0 0.00
preciom 0 0.00
areaconst 0 0.00
parqueaderos 133 11.83
banios 0 0.00
habitaciones 0 0.00
tipo 0 0.00
barrio 0 0.00
longitud 0 0.00
latitud 0 0.00

Debido a que se ha encontrado una concentración de datos faltantes en dos variables, se procederá a realizar una imputación utilizando el método de la moda.

# Copia de trabajo
datos_imp2 <- Base2a

# (opcional) columnas a excluir de la imputación
excluir <- c("id")  # agrega "latitud","longitud", etc. si no quieres imputarlas
cols <- setdiff(names(datos_imp2), excluir)

# Función de moda (sirve para numéricas, factor y character)
moda <- function(x) {
  y <- x
  if (is.character(y)) y[y == ""] <- NA  # trata vacíos como NA (opcional)
  y <- y[!is.na(y)]
  if (!length(y)) return(NA)
  ux <- unique(y)
  ux[which.max(tabulate(match(y, ux)))]
}

# Imputación con moda por columna (simple y directa)
for (v in cols) {
  m <- moda(datos_imp2[[v]])
  if (is.na(m)) next
  if (is.factor(datos_imp[[v]])) {
    # asegura que la moda exista como nivel
    lv <- levels(datos_imp[[v]])
    if (!(as.character(m) %in% lv)) levels(datos_imp2[[v]]) <- c(lv, as.character(m))
    datos_imp2[[v]][is.na(datos_imp2[[v]])] <- as.character(m)
  } else {
    datos_imp2[[v]][is.na(datos_imp2[[v]])] <- m
  }
}


tabla_na <- data.frame(
  Variable   = names(datos_imp2),
  NA_n       = colSums(is.na(datos_imp2)),
  Porcentaje = round(colMeans(is.na(datos_imp2)) * 100, 2),
  row.names  = NULL
)


tabla_na %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Variable NA_n Porcentaje
id 0 0
zona 0 0
piso 0 0
estrato 0 0
preciom 0 0
areaconst 0 0
parqueaderos 0 0
banios 0 0
habitaciones 0 0
tipo 0 0
barrio 0 0
longitud 0 0
latitud 0 0

3. Identificar Valores atipicos

# 1) Excluir columnas no numéricas
vars_excluir <- c("id","zona","tipo","barrio","latitud","longitud")
datos_num <- datos_imp2[, setdiff(names(datos_imp2), vars_excluir), drop = FALSE]
datos_num <- datos_num[, sapply(datos_num, is.numeric), drop = FALSE]

# 2) Transformación log estable (log1p = log(1+x))
datos_log <- as.data.frame(lapply(datos_num, log1p))

# 3) Outliers por IQR, devolviendo índices (mejor para localizar filas)
detectar_idx_iqr <- function(x){
  x <- x[!is.na(x)]
  if (length(x) < 2) return(integer(0))
  Q1 <- quantile(x, .25); Q3 <- quantile(x, .75); I <- Q3 - Q1
  which(x < (Q1 - 1.5*I) | x > (Q3 + 1.5*I))
}

idx_por_var <- lapply(datos_log, detectar_idx_iqr)
resumen <- data.frame(
  Variable = names(idx_por_var),
  Valores_Atipicos = sapply(idx_por_var, length),
  row.names = NULL
)




#Conteo de válidos por variable (en datos_log)
n_valid <- sapply(datos_log, function(x) sum(!is.na(x)))

# Conteo de outliers (ya lo tienes en idx_por_var)
n_out <- sapply(idx_por_var, length)

# Porcentaje de outliers
pct_out <- ifelse(n_valid > 0, round(100 * n_out / n_valid, 2), NA_real_)

# Tabla final
tabla_out <- data.frame(
  Variable   = names(n_out),
  N_validos  = as.integer(n_valid[names(n_out)]),
  N_outliers = as.integer(n_out),
  Porcentaje = pct_out,
  row.names  = NULL
)

# Ordenar por mayor porcentaje (opcional)
tabla_out <- tabla_out[order(-tabla_out$Porcentaje), ]

tabla_out %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Variable N_validos N_outliers Porcentaje
4 banios 1124 15 1.33
5 habitaciones 1124 11 0.98
2 areaconst 1124 6 0.53
1 preciom 1124 2 0.18
3 parqueaderos 1124 0 0.00
vars <- c("preciom","areaconst","parqueaderos","banios","habitaciones")
vars <- intersect(vars, names(datos_imp2))

# Conversión segura a numérico
to_num <- function(x) if (is.numeric(x)) x else suppressWarnings(as.numeric(as.character(x)))
X <- setNames(lapply(datos_imp2[vars], to_num), vars)




# X: data.frame/lista con variables numéricas
nmv  <- names(X)
n    <- length(nmv)

# Layout automático
nc <- ceiling(sqrt(n)); nr <- ceiling(n / nc)

op <- par(mfrow = c(nr, nc), mar = c(4, 4, 2, 1), bg = "white")

# Paleta disponible
cols <- grDevices::hcl.colors(n, palette = "Set2")

i <- 0
for (nm in nmv) {
  i <- i + 1
  x <- X[[nm]]
  x <- x[is.finite(x) & x > -1]  # log1p válido

  if (length(x)) {
    # Fondo con rejilla
    plot(NA, xlab = "", ylab = "log(1 + x)", xlim = c(0.5, 1.5),
         ylim = range(log1p(x), na.rm = TRUE), axes = FALSE, main = nm)
    grid(nx = NA, ny = NULL, col = gray(0.9), lty = 3)
    axis(2); box(bty = "n")

    # Boxplot con color
    boxplot(log1p(x), add = TRUE,
            col     = adjustcolor(cols[i], 0.85),
            border  = adjustcolor(cols[i], 0.9),
            notch   = TRUE, boxwex = 0.6,
            medcol  = "white", medlwd = 2,
            whisklty = 1, outpch = 16,
            outcol  = adjustcolor(cols[i], 0.85),
            outbg   = adjustcolor(cols[i], 0.55),
            axes = FALSE, names = "")
  } else {
    plot.new(); title(main = paste0(nm, " (sin datos válidos para log1p)"))
  }
}

par(op)

Se optó por remover los valores atípicos en las variables parqueaderos, baños, área construida y precio, ya que en conjunto representan menos del 3 % del total de registros. Además, para los tipos de vivienda analizados, resulta poco coherente que existan valores tan elevados en el número de baños y parqueaderos.

# === Eliminar outliers (IQR en log1p) excepto la variable 'habitaciones' ===

# 1) Variables numéricas a evaluar (excluye id/zona/tipo/barrio/lat/long y 'habitaciones')
vars_excluir <- c("id","zona","tipo","barrio","latitud","longitud")
num_cols <- names(datos_imp2)[sapply(datos_imp2, is.numeric)]
vars_filtrar <- setdiff(intersect(num_cols, setdiff(names(datos_imp2), vars_excluir)), "habitaciones")

# 2) Máscara de outliers en log1p alineada a filas
is_outlier_log1p <- function(x, k = 1.5){
  m  <- rep(FALSE, length(x))
  ok <- is.finite(x) & (x > -1)       # log1p definido para x > -1
  if (sum(ok) < 2) return(m)
  lx <- log1p(x[ok])
  Q1 <- quantile(lx, .25); Q3 <- quantile(lx, .75); I <- Q3 - Q1
  m_ok <- (lx < Q1 - k*I) | (lx > Q3 + k*I)
  m[ok] <- m_ok
  m
}

if (length(vars_filtrar) == 0) {
  message("No hay variables para filtrar (aparte de 'habitaciones').")
  datos_sin_out2 <- datos_imp2
} else {
  masks <- lapply(vars_filtrar, function(v) is_outlier_log1p(datos_imp2[[v]]))
  mask_any <- Reduce("|", masks)         # outlier en cualquiera de las variables
  cat("Filas a eliminar:", sum(mask_any), "de", nrow(datos_imp2), "\n")
  datos_sin_out2 <- datos_imp2[!mask_any, ]
}
## Filas a eliminar: 22 de 1124

ANALISIS DE CORRELACIÓN

library(dplyr); library(plotly)

df2 <- datos_sin_out2

# -- Dispersión interactiva: Precio vs Área (color por estrato) ---
p_area <- plot_ly(
  df2, x = ~areaconst, y = ~preciom,
  color = ~as.factor(estrato),
  type = "scatter", mode = "markers",
  text = ~paste0("<br>Barrio: ", barrios,
                 "<br>Estrato: ", estrato,
                 "<br>Baños: ", banios,
                 "<br>Habitaciones: ", habitaciones),
  hoverinfo = "text+x+y"
) |>
  layout(title = "Precio vs Área (color: Estrato)",
         xaxis = list(title = "Área construida"),
         yaxis = list(title = "Precio"))
p_area
library(dplyr); library(plotly)

df2 <- datos_sin_out2 %>% filter(areaconst > 0, preciom > 0)

p_area <- plot_ly(
  df2, x = ~areaconst, y = ~preciom,
  color = ~as.factor(estrato),
  type = "scatter", mode = "markers",
  text = ~paste0("<br>Barrio: ", barrios,
                 "<br>Estrato: ", estrato,
                 "<br>Baños: ", banios,
                 "<br>Habitaciones: ", habitaciones),
  hoverinfo = "text+x+y"
) %>%
  layout(
    title = "Precio vs Área (color: Estrato) — ejes log10",
    xaxis = list(title = "Área construida (log10)", type = "log"),
    yaxis = list(title = "Precio (log10)",           type = "log")
  )

p_area
# -- Boxplots interactivos: Precio por Estrato y por Zona ---
p_estrato <- plot_ly(df2, x = ~as.factor(estrato), y = ~preciom,
                     type = "box", boxpoints = "outliers") |>
  layout(title = "Precio por estrato",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio"))
p_estrato
num_df2 <- df2 |>
  select(preciom, areaconst, estrato, banios, habitaciones) |>
  mutate(across(everything(), as.numeric)) |>
  na.omit()

cm <- cor(num_df2, use = "complete.obs", method = "spearman")

plot_ly(x = colnames(cm), y = rownames(cm), z = cm, type = "heatmap") |>
  layout(title = "Correlaciones (Spearman): precio y predictores")

MODELO DE REGRESION

m_lin2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
            data = df2 )
summary(m_lin2)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = df2)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -741.75  -91.22  -17.52   52.29  938.67 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -59.77088   33.28666  -1.796   0.0728 .  
## areaconst      0.75741    0.04984  15.197  < 2e-16 ***
## estrato4      88.12043   26.93371   3.272   0.0011 ** 
## estrato5     154.48193   27.41244   5.635 2.22e-08 ***
## estrato6     404.64417   29.67588  13.635  < 2e-16 ***
## habitaciones  -8.64408    5.49599  -1.573   0.1161    
## parqueaderos  54.11170    5.27605  10.256  < 2e-16 ***
## banios        36.16974    6.39079   5.660 1.94e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 192.2 on 1094 degrees of freedom
## Multiple R-squared:  0.7309, Adjusted R-squared:  0.7292 
## F-statistic: 424.5 on 7 and 1094 DF,  p-value: < 2.2e-16

INTERPRETACIÓN DE COEFICIENTES

Intercepto = −59.8 (SE 33.29; p = 0.073) → no significativo. No tiene interpretación práctica (precio cuando todas las x son 0). IC95% ≈ [−125.0, 5.5].

areaconst = 0.757 (SE 0.050; p < 2e−16) → significativo. Cada +1 m² se asocia con +0.757 millones (≈ $757 mil) en el precio, ceteris paribus. IC95% ≈ [0.66, 0.86].

estrato 4 = +88.1 M (SE 26.93; p = 0.0011) → significativo. Frente a estrato 3, +88 millones manteniendo lo demás constante. IC95% ≈ [35.3, 140.9].

estrato 5 = +154.5 M (SE 27.41; p = 2.2e−08) → significativo. IC95% ≈ [100.8, 208.2].

estrato 6 = +404.6 M (SE 29.68; p < 2e−16) → significativo y grande. IC95% ≈ [346.5, 462.8].

habitaciones = −8.64 M (SE 5.50; p = 0.116) → no significativo. IC95% ≈ [−19.4, 2.1]. Probable colinealidad con área: cuando ya controlas por m², “más cuartos” no añade valor adicional claro.

parqueaderos = +54.1 M (SE 5.28; p < 2e−16) → significativo. Un cupo extra de parqueadero se asocia con +54 millones. IC95% ≈ [43.8, 64.4].

baños = +36.2 M (SE 6.39; p = 1.9e−08) → significativo. Un baño adicional se asocia con +36 millones. IC95% ≈ [23.7, 48.7].

En conjunto, los resultados confirman que el tamaño y la localización socioeconómica son los principales determinantes del precio: cada m² adicional se asocia, en promedio, con +0,757 millones de pesos, y pertenecer a estratos más altos implica primas sustanciales frente al estrato 3 (+88 M en estrato 4, +155 M en estrato 5 y +405 M en estrato 6, ceteris paribus). Entre los atributos internos, parqueadero (+54 M por cupo) y baños (+36 M por unidad) agregan valor de forma estadísticamente significativa, mientras que el número de habitaciones no muestra efecto adicional una vez controlado el metraje—coherente con su colinealidad con el tamaño. El intercepto no es de interés práctico. En términos de gestión, para ajustar o comparar ofertas conviene priorizar área construida, estrato, parqueaderos y baños; las “habitaciones” influyen principalmente a través de cómo se distribuye el área.

INTERPRETACIÓN DE R2

El modelo explica ≈ 73.1% de la variación del precio entre viviendas (R² ajustado ≈ 72.9%, casi igual ⇒ poco sobreajuste).

Aun así, ≈ 26.9% de la variación queda sin explicar: habitual en inmobiliario por falta de atributos finos (ubicación exacta/barrio, edad y estado, acabados, tamaño del lote, amenities, etc.).

¿Cómo mejorarlo?

1- Agregar variables de ubicación como el barrio

2- pasar las variables precio y area a una base logaritmica (Log10) pero para este caso de la oferta no estan aclarando un barrio especifico

m_log2 <- lm(log10(preciom) ~ log10(areaconst) + estrato + habitaciones + parqueaderos + banios,
            data = df2 )
summary(m_log2)
## 
## Call:
## lm(formula = log10(preciom) ~ log10(areaconst) + estrato + habitaciones + 
##     parqueaderos + banios, data = df2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.43451 -0.06872 -0.01034  0.05849  0.52610 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       1.350933   0.035990  37.536  < 2e-16 ***
## log10(areaconst)  0.405119   0.017972  22.541  < 2e-16 ***
## estrato4          0.162683   0.014614  11.132  < 2e-16 ***
## estrato5          0.246234   0.014903  16.523  < 2e-16 ***
## estrato6          0.392256   0.016232  24.166  < 2e-16 ***
## habitaciones     -0.003213   0.003007  -1.068    0.286    
## parqueaderos      0.025038   0.002811   8.908  < 2e-16 ***
## banios            0.022633   0.003470   6.522 1.06e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.104 on 1094 degrees of freedom
## Multiple R-squared:  0.8292, Adjusted R-squared:  0.8281 
## F-statistic: 758.9 on 7 and 1094 DF,  p-value: < 2.2e-16

VALIDACIÓN DE SUPUESTOS

# Instala los paquetes si no los tienes:
# install.packages("ggplot2")
# install.packages("plotly")
# install.packages("dplyr")

# Carga las librerías necesarias
library(ggplot2)
library(plotly)
library(dplyr)

# Supongamos que ya tienes el modelo m_log ajustado
# m_log <- lm(log10(precio) ~ areaconst + estrato + num_habitaciones + num_banos, data = df)

# Extrae los datos y calcula los valores para los gráficos de diagnóstico
modelo_df <- data.frame(
  fitted_values = fitted(m_log2),
  residuals = resid(m_log2),
  # Se utiliza rstandard() para obtener los residuales estandarizados,
  # que son necesarios para el gráfico de Escala-Ubicación.
  sqrt_abs_residuals = sqrt(abs(rstandard(m_log2))),
  leverage = hatvalues(m_log2),
  cooks_distance = cooks.distance(m_log2)
)

# ----------------------------------------------------
# 1. Gráfico de Residuales vs. Valores Ajustados
# ----------------------------------------------------
# Propósito: Evaluar la linealidad y la varianza constante (homocedasticidad)
# Si los puntos forman una banda horizontal sin patrón, el modelo es adecuado.

p1 <- ggplot(modelo_df, aes(x = fitted_values, y = residuals)) +
  geom_point(alpha = 0.5, color = "#2c7bb6") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) +
  geom_smooth(method = "loess", se = FALSE, color = "#fdae61", size = 1) +
  labs(
    title = "Residuales vs. Valores Ajustados",
    x = "Valores Ajustados (log10)",
    y = "Residuales"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p1)
## `geom_smooth()` using formula = 'y ~ x'
# ----------------------------------------------------
# 2. Gráfico Q-Q Normal
# ----------------------------------------------------
# Propósito: Verificar si los residuales siguen una distribución normal.
# Los puntos deben seguir la línea diagonal.

p2 <- ggplot(modelo_df, aes(sample = residuals)) +
  stat_qq(color = "#2c7bb6") +
  stat_qq_line(color = "red", linetype = "dashed", size = 1) +
  labs(
    title = "Gráfico Q-Q Normal",
    x = "Cuantiles Teóricos",
    y = "Cuantiles de los Residuales"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p2)
# ----------------------------------------------------
# 3. Gráfico de Escala-Ubicación
# ----------------------------------------------------
# Propósito: Evaluar la homocedasticidad (varianza constante de los residuales).
# Los puntos deben formar una banda horizontal sin patrón.

p3 <- ggplot(modelo_df, aes(x = fitted_values, y = sqrt_abs_residuals)) +
  geom_point(alpha = 0.5, color = "#2c7bb6") +
  geom_smooth(method = "loess", se = FALSE, color = "#fdae61", size = 1) +
  labs(
    title = "Gráfico de Escala-Ubicación",
    x = "Valores Ajustados (log10)",
    y = "Raíz Cuadrada de los Residuales Estandarizados"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p3)
## `geom_smooth()` using formula = 'y ~ x'
# ----------------------------------------------------
# 4. Gráfico de Residuales vs. Leverage
# ----------------------------------------------------
# Propósito: Identificar puntos influyentes que pueden afectar los coeficientes del modelo.
# Los puntos con alta leverage (alto en el eje x) y/o altos residuales (alto en el eje y) son de interés.

p4 <- ggplot(modelo_df, aes(x = leverage, y = residuals, text = paste("Cook's D:", round(cooks_distance, 4)))) +
  geom_point(aes(color = cooks_distance), alpha = 0.7) +
  scale_color_viridis_c(name = "Distancia de Cook") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  labs(
    title = "Residuales vs. Leverage",
    x = "Leverage",
    y = "Residuales"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

# Convierte el gráfico de ggplot2 en interactivo con plotly
ggplotly(p4, tooltip = "text")

PRONOSTICO

Para una casa con: area de 300 mt2 3 parqueadero 3 baños 5 habitaciones Estrato 5 en la zona norte

El precio es de: 550 millones

newdata3 <- data.frame(
  areaconst    = 300,   # m²
  estrato      = "5",
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)




pred_log10_2a   <- predict(m_log2, newdata = newdata3)
smear          <- mean(10^residuals(m_log2), na.rm = TRUE)
pred_log2a_mill <- 10^pred_log10_2a * smear


pred_log2a_mill
##        1 
## 550.2154

Para una casa con: area de 300 mt2 3 parqueadero 3 baños 5 habitaciones Estrato 6 en la zona norte

El precio es de: 770 millones

newdata4 <- data.frame(
  areaconst    = 300,   # m²
  estrato      = "6",
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)




pred_log10_3a   <- predict(m_log2, newdata = newdata4)
smear          <- mean(10^residuals(m_log2), na.rm = TRUE)
pred_log3a_mill <- 10^pred_log10_3a * smear


pred_log3a_mill
##        1 
## 770.1133

OFERTAS

Hemos identificado cuatro ofertas que se alinean con su presupuesto y requisitos para la vivienda en la zona norte.

La primera propuesta, y la más completa, es una casa de 200 m² en estrato 4, con cuatro habitaciones, un parqueadero y dos baños, por un valor de $330 millones. Esta opción satisface todas las especificaciones incluso se podria aumenta a 5 habitaciones y 2 parqueaderos.

Si la condición de estrato 5 es una prioridad para usted, será necesario ajustar algunos de los atributos de la vivienda. Dependiendo de sus preferencias, las compensaciones podrían ser:

Disminuir el área construida si el tamaño es flexible.

Reducir el número de baños y/o habitaciones si el área es un requisito fijo. Por ejemplo, podríamos ofrecer una casa de igual área, pero con menos baños, y en un caso extremo, con solo dos habitaciones y sin parqueadero.

# Data frame para graficar

oferta <- data.frame(
  areaconst    = 200,   # m²
  estrato      = "4",
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

pred_log10_3   <- predict(m_log, newdata = oferta)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log3_mill <- 10^pred_log10_3 * smear



df <- data.frame(
  Variable = c("Área (m²)", "Estrato", "Habitaciones", "Parqueaderos", "Baños", "Precio (MM)"),
  Valor    = c(oferta$areaconst,
               as.numeric(oferta$estrato),
               oferta$habitaciones,
               oferta$parqueaderos,
               oferta$banios,
               round(pred_log3_mill, 2))
)


# --- Gráfico con formato mejorado ---
ggplot(df, aes(x = Variable, y = Valor, fill = Variable)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = round(Valor, 2)),
            vjust = -0.4, size = 4, fontface = "bold", color = "black") +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Variables del inmueble y precio estimado",
    y = "Valor",
    x = ""
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 20, hjust = 1),
    panel.grid.minor = element_blank()
  )

# Data frame para graficar

oferta <- data.frame(
  areaconst    = 200,   # m²
  estrato      = "4",
  habitaciones = 5,
  parqueaderos = 2,
  banios       = 2
)

pred_log10_3   <- predict(m_log, newdata = oferta)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log3_mill <- 10^pred_log10_3 * smear



df <- data.frame(
  Variable = c("Área (m²)", "Estrato", "Habitaciones", "Parqueaderos", "Baños", "Precio (MM)"),
  Valor    = c(oferta$areaconst,
               as.numeric(oferta$estrato),
               oferta$habitaciones,
               oferta$parqueaderos,
               oferta$banios,
               round(pred_log3_mill, 2))
)


# --- Gráfico con formato mejorado ---
ggplot(df, aes(x = Variable, y = Valor, fill = Variable)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = round(Valor, 2)),
            vjust = -0.4, size = 4, fontface = "bold", color = "black") +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Variables del inmueble y precio estimado",
    y = "Valor",
    x = ""
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 20, hjust = 1),
    panel.grid.minor = element_blank()
  )

# Data frame para graficar

oferta <- data.frame(
  areaconst    = 180,   # m²
  estrato      = "5",
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

pred_log10_3   <- predict(m_log, newdata = oferta)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log3_mill <- 10^pred_log10_3 * smear




df <- data.frame(
  Variable = c("Área (m²)", "Estrato", "Habitaciones", "Parqueaderos", "Baños", "Precio (MM)"),
  Valor    = c(oferta$areaconst,
               as.numeric(oferta$estrato),
               oferta$habitaciones,
               oferta$parqueaderos,
               oferta$banios,
               round(pred_log3_mill, 2))
)


# --- Gráfico con formato mejorado ---
ggplot(df, aes(x = Variable, y = Valor, fill = Variable)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = round(Valor, 2)),
            vjust = -0.4, size = 4, fontface = "bold", color = "black") +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Variables del inmueble y precio estimado",
    y = "Valor",
    x = ""
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 20, hjust = 1),
    panel.grid.minor = element_blank()
  )

# Data frame para graficar

oferta <- data.frame(
  areaconst    = 200,   # m²
  estrato      = "5",
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 1
)

pred_log10_3   <- predict(m_log, newdata = oferta)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log3_mill <- 10^pred_log10_3 * smear




df <- data.frame(
  Variable = c("Área (m²)", "Estrato", "Habitaciones", "Parqueaderos", "Baños", "Precio (MM)"),
  Valor    = c(oferta$areaconst,
               as.numeric(oferta$estrato),
               oferta$habitaciones,
               oferta$parqueaderos,
               oferta$banios,
               round(pred_log3_mill, 2))
)


# --- Gráfico con formato mejorado ---
ggplot(df, aes(x = Variable, y = Valor, fill = Variable)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = round(Valor, 2)),
            vjust = -0.4, size = 4, fontface = "bold", color = "black") +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Variables del inmueble y precio estimado",
    y = "Valor",
    x = ""
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 20, hjust = 1),
    panel.grid.minor = element_blank()
  )

# Data frame para graficar

oferta <- data.frame(
  areaconst    = 200,   # m²
  estrato      = "5",
  habitaciones = 2,
  parqueaderos = 0,
  banios       = 2
)

pred_log10_3   <- predict(m_log, newdata = oferta)
smear          <- mean(10^residuals(m_log), na.rm = TRUE)
pred_log3_mill <- 10^pred_log10_3 * smear



df <- data.frame(
  Variable = c("Área (m²)", "Estrato", "Habitaciones", "Parqueaderos", "Baños", "Precio (MM)"),
  Valor    = c(oferta$areaconst,
               as.numeric(oferta$estrato),
               oferta$habitaciones,
               oferta$parqueaderos,
               oferta$banios,
               round(pred_log3_mill, 2))
)


# --- Gráfico con formato mejorado ---
ggplot(df, aes(x = Variable, y = Valor, fill = Variable)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = round(Valor, 2)),
            vjust = -0.4, size = 4, fontface = "bold", color = "black") +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Variables del inmueble y precio estimado",
    y = "Valor",
    x = ""
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 20, hjust = 1),
    panel.grid.minor = element_blank()
  )

Para el caso del apartamento, el presupuesto del cliente es suficiente para cubrir las opciones disponibles y cumplir con sus requerimientos.