INTRODUCCIÓN

El mercado inmobiliario urbano es complejo y depende de factores físicos, geográficos y socioeconómicos. En Cali atraviesa una desaceleración, pero la solicitud de compra de dos viviendas de alta gama por parte de una empresa internacional abre una oportunidad estratégica para C&A. Este informe analiza dichas propiedades y ofrece recomendaciones basadas en modelos de estimación de precios para apoyar la negociación y la toma de decisiones..

library(tidymodels)
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"
  )
)


Tabla_descriptiva %>%
  kable("html", caption = "Tabla descriptiva de variables") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"),
                full_width = FALSE,
                position = "center") 
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 construyó una base de datos específica con la información de las viviendas clasificadas como casas localizadas en la Zona Norte de la ciudad. De los 8.321 registros que conforman la base original de viviendas, se seleccionaron 722 que cumplen simultáneamente con estos criterios, equivalentes al 8,7% del total. Este conjunto reducido servirá como base 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>

Se detectaron inconsistencias en la base de datos filtrada. A continuación, se muestra una lista de barrios que, aunque fueron incluidos en el filtro inicial como pertenecientes a la Zona Norte, en realidad se ubican geográficamente en otras zonas 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.8,
    color = "red", fillColor = "red",   # 🔴 Puntos rojos
    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)
## 
## Adjuntando el paquete: 'summarytools'
## The following object is masked from 'package:tibble':
## 
##     view
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.4 (R version 4.5.1)
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"
  ) 
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

Dado que se identificó una concentración de valores ausentes en dos variables, se optará por aplicar un proceso de imputación empleando la moda como método de reemplazo.

# 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"
  ) 
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"
  ) 
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
# =========================
# Preparación de datos (tu parte)
# =========================
vars <- c("preciom","areaconst","parqueaderos","banios","habitaciones")
vars <- intersect(vars, names(datos_imp))

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)

# =========================
# Función para graficar en distintos estilos
# =========================
suppressPackageStartupMessages({
  library(dplyr)
  library(tidyr)
  library(ggplot2)
})

prep_long <- function(X) {
  as.data.frame(X) |>
    dplyr::select(where(is.numeric)) |>
    tidyr::pivot_longer(everything(), names_to="Variable", values_to="x") |>
    dplyr::filter(is.finite(x), x > -1) |>
    dplyr::mutate(lx = log1p(x))
}

plot_vars <- function(X, style = c("box", "violin", "hist", "density", "ridge"),
                      ncol = 3, palette = "Set2", log_label = "log(1 + x)") {
  style <- match.arg(style)
  df <- prep_long(X)

  # Comun: tema y escalas
  base_theme <- theme_minimal(base_size = 12) +
    theme(
      strip.text = element_text(face = "bold"),
      panel.grid.minor = element_blank()
    )

  if (style == "box") {
    p <- ggplot(df, aes(x = "", y = lx, fill = Variable)) +
      geom_boxplot(width = 0.6, notch = TRUE, outlier.shape = 16, outlier.alpha = 0.45, color = "grey30") +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Distribución por variable (Boxplot, log1p)", x = NULL, y = log_label) +
      base_theme +
      theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())

  } else if (style == "violin") {
    p <- ggplot(df, aes(x = "", y = lx, fill = Variable)) +
      geom_violin(trim = TRUE, alpha = 0.9, color = NA) +
      stat_summary(fun = median, geom = "point", size = 2, color = "grey10") +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Distribución por variable (Violín, log1p)", x = NULL, y = log_label) +
      base_theme +
      theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())

  } else if (style == "hist") {
    p <- ggplot(df, aes(x = lx, fill = Variable)) +
      geom_histogram(bins = 30, alpha = 0.9, color = "white") +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Histograma por variable (log1p)", x = log_label, y = "Frecuencia") +
      base_theme

  } else if (style == "density") {
    p <- ggplot(df, aes(x = lx, fill = Variable)) +
      geom_density(alpha = 0.8) +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Densidad por variable (log1p)", x = log_label, y = "Densidad") +
      base_theme

  } else if (style == "ridge") {
    if (!requireNamespace("ggridges", quietly = TRUE)) {
      warning("Instala 'ggridges' para usar style='ridge'. Mostrando densidad por defecto.")
      return(plot_vars(X, style = "density", ncol = ncol, palette = palette, log_label = log_label))
    }
    p <- ggplot(df, aes(x = lx, y = Variable, fill = Variable)) +
      ggridges::geom_density_ridges(scale = 1.2, alpha = 0.9, color = NA) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Ridgelines por variable (log1p)", x = log_label, y = NULL) +
      base_theme
  }

  return(p)
}

# =========================
# Ejemplos de uso (elige uno)
# =========================
p1 <- plot_vars(X, style = "box",    ncol = 3)  # Boxplots facetados
p2 <- plot_vars(X, style = "violin", ncol = 3)  # Violines + mediana
p3 <- plot_vars(X, style = "hist",   ncol = 3)  # Histogramas
p4 <- plot_vars(X, style = "density",ncol = 3)  # Densidades
# p5 <- plot_vars(X, style = "ridge")            # Requiere ggridges

print(p1)  # o p2/p3/p4
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?

Los valores atípicos de las variables ‘parqueaderos’, ‘baños’, ‘área construida’ y ‘precio’ fueron removidos del conjunto de datos. Esta decisión se basa en que estos casos representan menos del 5 % del total de registros. Se consideró que la presencia de valores excesivamente altos para baños y parqueaderos no es consistente con el tipo de vivienda bajo estudio.

# === 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)
## 
## Adjuntando el paquete: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout
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),
  colors = "Set2",                    # paleta distinta
  type = "scatter", mode = "markers",
  marker = list(size = 9,             # tamaño puntos
                symbol = "circle",    # forma
                line = list(width = 1, color = "black")), # borde negro
  text = ~paste0(
    "<b>Barrio:</b> ", barrios,
    "<br><b>Estrato:</b> ", estrato,
    "<br><b>Baños:</b> ", banios,
    "<br><b>Habitaciones:</b> ", habitaciones
  ),
  hoverinfo = "text+x+y"
) |>
  layout(
    title = list(text = "Precio vs Área construida", x = 0.5),  # título centrado
    xaxis = list(title = "Área construida (m²)",
                 zeroline = FALSE,
                 gridcolor = "lightgrey"),
    yaxis = list(title = "Precio (millones)",
                 zeroline = FALSE,
                 gridcolor = "lightgrey"),
    plot_bgcolor  = "white",   # fondo blanco del panel
    paper_bgcolor = "white",   # fondo general
    legend = list(title = list(text = "<b>Estrato</b>"))
  )

p_area

La correlación entre el precio y el área construida es positiva y evidente, lo que se refleja en la nube de puntos con una clara línea de tendencia ascendente. El estrato social actúa como un factor diferenciador importante: a igual área construida, los inmuebles en estratos altos (5 y 6) tienen un precio considerablemente mayor que los de estratos medios (3 y 4). Esto sugiere que el estrato añade un valor adicional al precio, independientemente del tamaño de la propiedad.

# -- Boxplots interactivos: Precio por Estrato ---
p_estrato <- plot_ly(
  df,
  x = ~as.factor(estrato),
  y = ~preciom,
  color = ~as.factor(estrato),
  colors = "Set2",
  type = "box",
  boxpoints = "all",     # muestra todos los puntos además del boxplot
  jitter = 0.4,          # dispersión de puntos
  pointpos = -1.8,       # posición de los puntos respecto a la caja
  marker = list(size = 5, opacity = 0.6),
  line = list(width = 1)
) |>
  layout(
    title = list(text = "Distribución de precios por estrato", x = 0.5),
    xaxis = list(title = "Estrato"),
    yaxis = list(title = "Precio (millones)", gridcolor = "lightgrey"),
    plot_bgcolor  = "white",
    paper_bgcolor = "white",
    showlegend = FALSE
  )

p_estrato

Al analizar la relación por estrato, se evidencia una tendencia ascendente clara: la mediana de los precios crece de forma consistente del estrato 3 al 6. Esto valida una asociación positiva y directa entre el nivel socioeconómico y el precio de la vivienda. Se identificaron valores extremos en los estratos 3, 5 y 6; estos puntos anómalos no son errores de datos, sino que corresponden a propiedades con un área construida excepcionalmente grande para sus respectivos estratos.

library(plotly)
library(dplyr)

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

# Heatmap interactivo con anotaciones
p_corr <- plot_ly(
  x = colnames(cm),
  y = rownames(cm),
  z = cm,
  type = "heatmap",
  colorscale = "RdBu",       # paleta rojo-azul
  reversescale = TRUE,
  zmin = -1, zmax = 1,
  text = round(cm, 2),       # valores como texto
  hovertemplate = paste(
    "<b>Var1:</b> %{y}<br>",
    "<b>Var2:</b> %{x}<br>",
    "<b>Correlación:</b> %{z:.2f}<extra></extra>"
  )
) |>
  layout(
    title = list(text = "Correlaciones (Spearman): precio y predictores", x = 0.5),
    xaxis = list(title = "", side = "top"),
    yaxis = list(title = "", autorange = "reversed"),
    plot_bgcolor = "white",
    paper_bgcolor = "white"
  )

p_corr

El mapa de calor de correlaciones (Spearman) valida los resultados previamente obtenidos. Se identifica la relación más fuerte y positiva entre el precio y el área construida. El estrato social también demuestra una correlación positiva y relevante con el precio, confirmando su influencia en el valor del inmueble más allá de su tamaño. Por otro lado, la correlación entre el precio y el número de baños es moderada, mientras que la del precio y el número de habitaciones es la más baja de todas las variables estudiadas.

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

El análisis de regresión revela que el área construida y el estrato social son los principales impulsores del precio de la vivienda.

Área construida: Por cada metro cuadrado adicional, el precio aumenta en aproximadamente $858 mil (0.858 millones). Esta relación es altamente significativa, demostrando que el tamaño es un factor fundamental en la valoración de una propiedad.

Estrato social: El estrato es un factor determinante del precio. Comparado con el estrato 3, las viviendas en el estrato 4 son $70.2 millones más costosas, mientras que las de estrato 5 valen $109.7 millones más. El mayor incremento se observa en el estrato 6, donde el precio es $307.4 millones superior, lo que demuestra un valor significativamente mayor.

Baños y parqueaderos: El número de baños también es un factor significativo, ya que un baño extra se asocia a un aumento de $13.55 millones en el precio. Por su parte, la presencia de parqueaderos muestra una relación positiva, pero con una evidencia estadística más débil (significancia marginal).

Habitaciones: El número de habitaciones no es un factor significativo para el precio. Esto se debe probablemente a que está altamente correlacionado con el área construida; es decir, una vez que se considera el tamaño de la vivienda, el número de cuartos no agrega nueva información relevante.

En resumen, los hallazgos confirman que las propiedades con mayor área y ubicadas en estratos socioeconómicos más altos tienen un valor significativamente superior.

INTERPRETACIÓN DE R2

El modelo demuestra una capacidad explicativa notable, capturando cerca del 68.7% de la varianza en el precio de las propiedades. No obstante, un 31% de la variabilidad permanece sin ser explicada. Este fenómeno es típico en el sector inmobiliario debido a la ausencia de variables de granularidad fina en la base de datos, como la ubicación precisa, el estado de conservación, los acabados y las comodidades ofrecidas.

¿Cómo mejorarlo?

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

La aplicación de una transformación logarítmica al modelo lineal ha generado mejoras significativas. El poder explicativo del modelo se incrementó notablemente, evidenciado por un aumento de 10 puntos porcentuales en el R² (de 0.687 a 0.788). Este cambio indica que el modelo ahora captura una mayor proporción de la variabilidad en los precios. Asimismo, la transformación ha hecho el error más interpretable y robusto. El error residual estándar se redujo drásticamente de 129.1 millones a 0.1103. En términos prácticos, esto significa que el error típico es de aproximadamente ±29% del precio de la vivienda, y la varianza de los residuos es más constante, lo que fortalece la validez del modelo.

VALIDACIÓN DE SUPUESTOS

m <- m_log  # o m_lin

par(mfrow = c(2,2))
plot(m, which = 1, main = "Residuos vs Ajustados"); abline(h = 0, lty = 2)
plot(m, which = 2, main = "Q–Q de residuos")
plot(m, which = 3, main = "Scale–Location")
plot(m, which = 5, main = "Residuos vs Leverage (Cook)")

par(mfrow = c(1,1))

El análisis de los gráficos de diagnóstico confirma que el modelo ha mejorado significativamente tras la transformación logarítmica.

Gráfico de Residuales vs. Valores Ajustados: Este gráfico muestra que los residuos son aleatorios y no siguen un patrón, lo cual confirma la linealidad del modelo. La dispersión de los puntos es uniforme, lo que indica que se ha corregido el problema de heterocedasticidad (varianza no constante), haciendo que el modelo sea más fiable.

Gráfico Q-Q Normal: La mayoría de los puntos se alinean con la línea diagonal, lo que demuestra que los residuos tienen una distribución aproximadamente normal. Aunque hay algunas desviaciones en los extremos, esto es común en datos reales y no compromete la validez del modelo.

Gráfico de Escala-Ubicación: Este gráfico refuerza la conclusión del primer gráfico, mostrando una línea de tendencia plana que confirma la homocedasticidad, es decir, que la varianza de los residuos es constante a lo largo de los valores ajustados.

Gráfico de Residuales vs. Leverage: Este gráfico ayuda a identificar puntos influyentes. La mayoría de los puntos tienen una Distancia de Cook baja, lo que indica que, a pesar de que hay algunos valores con potencial de influencia, no hay puntos extremos que afecten desproporcionadamente los coeficientes del modelo. Por lo tanto, no es necesario eliminar ningún dato para mejorar la precisión.

PRONOSTICO

Para una casa con: area de 200 mt2 1 parqueadero 2 baños 4 habitaciones Estrato 5 en la zona norte

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

OFERTAS

se identifica cuatro ofertas que se ajustan a su presupuesto y requisitos para una vivienda en la zona norte.

La opción más completa es una casa de 200 m² con cuatro habitaciones, un parqueadero, dos baños y estrato 4, por un valor de $330 millones. Esta propuesta no solo cumple con todas las especificaciones, sino que también ofrece la posibilidad de ampliarse a cinco habitaciones y dos parqueaderos.

En caso de que su prioridad sea vivir en un estrato 5, deberá considerar algunos ajustes. Las posibles compensaciones son:

Reducir el área construida si el tamaño es negociable.

Disminuir la cantidad de baños o habitaciones si el área es un requisito fijo. Por ejemplo, podríamos ofrecer una casa del mismo tamaño, pero con menos baños o, 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))
)


ggplot(df, aes(x = Variable, y = Valor, group = 1)) +
  geom_line(color = "steelblue", size = 1.2) +
  geom_point(color = "red", size = 4) +
  geom_text(aes(label = round(Valor, 2)), vjust = -0.8, size = 4) +
  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))
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

# 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))
)
ggplot(df, aes(x = Variable, y = Valor, group = 1)) +
  geom_line(color = "steelblue", size = 1.2) +
  geom_point(color = "red", size = 4) +
  geom_text(aes(label = round(Valor, 2)), vjust = -0.8, size = 4) +
  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))

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

ggplot(df, aes(x = Variable, y = Valor, group = 1)) +
  geom_line(color = "steelblue", size = 1.2) +
  geom_point(color = "red", size = 4) +
  geom_text(aes(label = round(Valor, 2)), vjust = -0.8, size = 4) +
  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))

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

ggplot(df, aes(x = Variable, y = Valor, group = 1)) +
  geom_line(color = "steelblue", size = 1.2) +
  geom_point(color = "red", size = 4) +
  geom_text(aes(label = round(Valor, 2)), vjust = -0.8, size = 4) +
  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))

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

ggplot(df, aes(x = Variable, y = Valor, group = 1)) +
  geom_line(color = "steelblue", size = 1.2) +
  geom_point(color = "red", size = 4) +
  geom_text(aes(label = round(Valor, 2)), vjust = -0.8, size = 4) +
  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))

APARTAMENTO ZONA SUR 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 Base2
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,
    color = "red",         # borde rojo
    fillColor = "red",     # relleno rojo
    fillOpacity = 0.8,     # un poco más intenso
    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.4 (R version 4.5.1)
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"
  )
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"
  ) 
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"
  ) 
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
# =========================
# Preparación de datos (tu parte)
# =========================
vars <- c("preciom","areaconst","parqueaderos","banios","habitaciones")
vars <- intersect(vars, names(datos_imp2))

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)

# =========================
# Función para graficar en distintos estilos
# =========================
suppressPackageStartupMessages({
  library(dplyr)
  library(tidyr)
  library(ggplot2)
})

prep_long <- function(X) {
  as.data.frame(X) |>
    dplyr::select(where(is.numeric)) |>
    tidyr::pivot_longer(everything(), names_to="Variable", values_to="x") |>
    dplyr::filter(is.finite(x), x > -1) |>
    dplyr::mutate(lx = log1p(x))
}

plot_vars <- function(X, style = c("box", "violin", "hist", "density", "ridge"),
                      ncol = 3, palette = "Set2", log_label = "log(1 + x)") {
  style <- match.arg(style)
  df <- prep_long(X)

  # Comun: tema y escalas
  base_theme <- theme_minimal(base_size = 12) +
    theme(
      strip.text = element_text(face = "bold"),
      panel.grid.minor = element_blank()
    )

  if (style == "box") {
    p <- ggplot(df, aes(x = "", y = lx, fill = Variable)) +
      geom_boxplot(width = 0.6, notch = TRUE, outlier.shape = 16, outlier.alpha = 0.45, color = "grey30") +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Distribución por variable (Boxplot, log1p)", x = NULL, y = log_label) +
      base_theme +
      theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())

  } else if (style == "violin") {
    p <- ggplot(df, aes(x = "", y = lx, fill = Variable)) +
      geom_violin(trim = TRUE, alpha = 0.9, color = NA) +
      stat_summary(fun = median, geom = "point", size = 2, color = "grey10") +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Distribución por variable (Violín, log1p)", x = NULL, y = log_label) +
      base_theme +
      theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())

  } else if (style == "hist") {
    p <- ggplot(df, aes(x = lx, fill = Variable)) +
      geom_histogram(bins = 30, alpha = 0.9, color = "white") +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Histograma por variable (log1p)", x = log_label, y = "Frecuencia") +
      base_theme

  } else if (style == "density") {
    p <- ggplot(df, aes(x = lx, fill = Variable)) +
      geom_density(alpha = 0.8) +
      facet_wrap(~ Variable, scales = "free_y", ncol = ncol) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Densidad por variable (log1p)", x = log_label, y = "Densidad") +
      base_theme

  } else if (style == "ridge") {
    if (!requireNamespace("ggridges", quietly = TRUE)) {
      warning("Instala 'ggridges' para usar style='ridge'. Mostrando densidad por defecto.")
      return(plot_vars(X, style = "density", ncol = ncol, palette = palette, log_label = log_label))
    }
    p <- ggplot(df, aes(x = lx, y = Variable, fill = Variable)) +
      ggridges::geom_density_ridges(scale = 1.2, alpha = 0.9, color = NA) +
      scale_fill_brewer(palette = palette, guide = "none") +
      labs(title = "Ridgelines por variable (log1p)", x = log_label, y = NULL) +
      base_theme
  }

  return(p)
}

# =========================
# Ejemplos de uso (elige uno)
# =========================
p1 <- plot_vars(X, style = "box",    ncol = 3)  # Boxplots facetados
p2 <- plot_vars(X, style = "violin", ncol = 3)  # Violines + mediana
p3 <- plot_vars(X, style = "hist",   ncol = 3)  # Histogramas
p4 <- plot_vars(X, style = "density",ncol = 3)  # Densidades
# p5 <- plot_vars(X, style = "ridge")            # Requiere ggridges

print(p1)  # o p2/p3/p4

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),
  colors = "Set2",                # paleta de colores suaves
  type = "scatter", mode = "markers",
  marker = list(
    size = 9,                     # tamaño de puntos
    symbol = "circle",            # forma
    line = list(width = 1, color = "black") # borde negro
  ),
  text = ~paste0(
    "<b>Barrio:</b> ", barrios,
    "<br><b>Estrato:</b> ", estrato,
    "<br><b>Baños:</b> ", banios,
    "<br><b>Habitaciones:</b> ", habitaciones
  ),
  hoverinfo = "text+x+y"
) |>
  layout(
    title = list(text = "Precio vs Área construida", x = 0.5),
    xaxis = list(title = "Área construida (m²)",
                 zeroline = FALSE,
                 gridcolor = "lightgrey"),
    yaxis = list(title = "Precio (millones)",
                 zeroline = FALSE,
                 gridcolor = "lightgrey"),
    plot_bgcolor  = "white",
    paper_bgcolor = "white",
    legend = list(title = list(text = "<b>Estrato</b>"))
  )

p_area
library(dplyr); library(plotly)

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

# Heatmap con nuevo formato
p_corr <- plot_ly(
  x = colnames(cm),
  y = rownames(cm),
  z = cm,
  type = "heatmap",
  colorscale = "RdBu",        # rojo-azul
  reversescale = TRUE,        # rojo = negativo, azul = positivo
  zmin = -1, zmax = 1,        # escala fija
  text = round(cm, 2),
  hovertemplate = paste(
    "<b>Var1:</b> %{y}<br>",
    "<b>Var2:</b> %{x}<br>",
    "<b>Correlación:</b> %{z:.2f}<extra></extra>"
  )
) |>
  layout(
    title = list(text = "Correlaciones (Spearman): Precio y predictores", x = 0.5),
    xaxis = list(title = "", side = "top"),
    yaxis = list(title = "", autorange = "reversed"),
    plot_bgcolor  = "white",
    paper_bgcolor = "white"
  )

p_corr

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

Factores con Mayor Impacto en el Precio Área Construida: Por cada metro cuadrado adicional, el precio de una vivienda aumenta en $757.000, un factor altamente significativo.

Estrato: El estrato socioeconómico tiene un impacto sustancial en el precio. Comparadas con las viviendas del estrato 3, las del estrato 4 son $88 millones más caras, las del estrato 5 valen $155 millones más, y las del estrato 6 tienen un valor superior de $405 millones.

Parqueaderos y Baños: El número de parqueaderos y baños también influye de manera significativa en el precio. Cada cupo de parqueadero se asocia con un aumento de $54 millones, mientras que cada baño adicional incrementa el precio en $36 millones.

Factores con Menor Impacto Habitaciones: El número de habitaciones no afecta el precio de manera independiente una vez que se considera el área construida. Esto se debe a que el tamaño de la vivienda y el número de habitaciones suelen estar relacionados, por lo que el área ya explica la mayor parte del valor.

Intercepto: El intercepto no es estadísticamente significativo y no tiene una interpretación práctica en este contexto.

En resumen, al analizar o comparar propiedades, es crucial dar prioridad a factores como el área construida, el estrato, los parqueaderos y los baños, ya que son los que más influyen en el precio final.

INTERPRETACIÓN DE R2

El modelo es muy efectivo, logrando explicar aproximadamente el 73.1% de la variación en el precio de las viviendas. Aunque es un resultado sólido, el 26.9% restante no se puede explicar con los datos que tenemos. Esto es común en los análisis inmobiliarios, ya que el modelo no incluye detalles más específicos como la ubicación exacta, la edad de la propiedad, la calidad de los acabados o las comodidades que ofrece.

¿Cómo mejorarlo?

1- 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

m2 <- m_log2  # o m_lin

par(mfrow = c(2,2))
plot(m2, which = 1, main = "Residuos vs Ajustados"); abline(h = 0, lty = 2)
plot(m2, which = 2, main = "Q–Q de residuos")
plot(m2, which = 3, main = "Scale–Location")
plot(m2, which = 5, main = "Residuos vs Leverage (Cook)")

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