El mercado del alquiler residencial constituye un ámbito de especial interés en el análisis económico urbano, debido a su incidencia sobre la accesibilidad a la vivienda y sobre la desigualdad territorial dentro de las ciudades, así como para apoyar a aquellos colectivos vulnerables, con posibilidad de exclusión social.
En este caso concreto, mi relación diaria con una entidad que se dedica al alquiler de inmuebles para las personas con posibilidades de exclusión social a las que se les ofrecen dichos inmuebles dentro de Madrid, por lo que, la realización de este trabajo práctico me ayuda a tener una visión sobre la distribución del mercado en los diferentes distritos.
En la villa de Madrid, la evolución del precio del alquiler ha adquirido una relevancia creciente en los últimos años, tanto por su impacto social como por las diferencias existentes entre distritos, que siempre han existido, pero han acercado la diferencia incluyendo casi la periferia en términos monetarios más cercanos a las zonas que históricamente contaban con alquileres elevados.
El presente trabajo práctico analiza el precio del alquiler de la vivienda en Madrid capital y sus juntas municipales de distrito a partir de la serie estadística 0504030000213 del Banco de Datos del Ayuntamiento de Madrid, correspondiente a la evolución del precio del alquiler en euros por metro cuadrado por distrito entre 2011 y 2024 (aunque en el borrador inicial se pretendía analizar desde el 2019 al 2024, finalmente se obtuvieron los datos desde el 2011 para dar una amplitud mayor a la muestra, ya que solo 6 años no era una muestra representativa).
Esta fuente oficial ofrece una base adecuada para realizar un estudio cuantitativo, reproducible y territorialmente desagregado, a pesar de no contar con las características pormenorizadas de las viviendas.
Desde el punto de vista metodológico, el trabajo combina técnicas de aprendizaje supervisado:
La comparación entre ambos enfoques permitirá evaluar el equilibrio entre capacidad predictiva e interpretabilidad.
Los objetivos del trabajo práctico son los siguientes:
La naturaleza del problema corresponde a una tarea de regresión, ya que la variable objetivo es continua.
En este contexto, se aplicarán dos enfoques distintos:
rpart, method = "anova"), y no un árbol de
clasificación (p. ej. el algoritmo C5.0 aplicado sobre
variables categóricas, como en el ejemplo clásico del conjunto
iris): la variable dependiente, el precio del alquiler por
metro cuadrado, es continua, por lo que el criterio de división en cada
nodo se basa en la reducción de la varianza dentro de cada rama, y no en
medidas de pureza como el índice Gini o la entropía, propias de los
árboles de clasificación.La variable dependiente será el precio del alquiler por metro cuadrado, mientras que las variables explicativas incluirán el año y el distrito.
Para la evaluación de los modelos se emplearán métricas como el MAE, el RMSE y el R².
Asimismo, se utilizará validación cruzada o partición entrenamiento-prueba para asegurar la robustez de los resultados.
Antes del análisis se realizó una depuración de la base de datos para eliminar encabezados informativos, unificar el formato de las variables, tratar los valores no disponibles y corregir inconsistencias en la nomenclatura de los distritos, y se transformará la estructura original a formato largo para facilitar el análisis exploratorio y la estimación de los modelos. Así, la limpieza se centra en dejar la serie lista para trabajar con un formato homogéneo y utilizable en R.
Los puntos más importantes que se limpiaron son:
El Excel contiene además de datos, los títulos, subtítulos, descripciones de la serie y notas al pie. Esas filas no forman parte de la base analítica, por lo que, se eliminaron antes de importar el archivo, aunque al no dar el formato de número a la columna del precio por metro cuadrado, se tuvo que realizar un cambio en Rstudio para que pudiese entenderlos.
Aparecían muchas columnas tipo Unnamed y celdas vacías
en la parte superior del archivo. Esas columnas artefactos del formato
de Excel y no aportaban información al modelo, así que se
descartaron.
Los precios estaban mal separados para la comprensión del lenguaje R
como era el caso de por ejemplo 13,2 o 18,7,
lo que en realidad representa números decimales y no dos campos
distintos. Hay que convertirlos correctamente a formato numérico en R,
teniendo en cuenta que la coma funciona como separador decimal en estos
datos.
El archivo indica que el símbolo .. debe interpretarse
como dato no disponible, y en la tabla también aparecían muchos guiones
- en años o distritos sin observación. Todo eso se
transformó en NA para poder hacer limpieza, análisis
exploratorio y modelización sin errores.
Según la nota del archivo, hasta 2010 solo hay datos para el total de la Ciudad de Madrid, no para todos los distritos. Eso significa que, si el análisis se centra en distritos, deberían excluirse o tratarse aparte los años en los que la información distrital está incompleta, por ello se decide eliminar el año 2010 y comenzar el estudio en el año 2011.
Algunos nombres aparecen con problemas de codificación, por ejemplo,
Chamartn, Tetun, Chamber,
Viclvaro. Por lo que se corrigieron esos valores para que
todos los distritos tuvieran un nombre limpio y uniforme, evitando
errores en gráficos, tablas o modelos.
Los datos aparecen en formato ancho, con los meses como columnas, mientras que para análisis en R suele ser más cómodo convertirlos a formato largo: una fila por combinación de año, distrito y mes. Eso facilita hacer regresión, gráficos temporales y comparaciones entre distritos. Por lo que se convierten los datos a formato largo para que R pueda realizar el análisis más efectivo y eficaz.
No se han eliminado los datos solo porque el precio sea alto o bajo, ya que esos valores pueden ser parte real del comportamiento del mercado. En principio se ha optado por no borrar automáticamente distritos con pocas observaciones; en caso de considerarlo necesario, se justifica metodológicamente, pues si una observación es rara pero válida debe mantenerse.
Para el trabajo práctico, la limpieza mínima y bien justificada se centro en:
.. y - en NA.[1] DATOS_R_LIMPIOS_serie.xlsx
(Corresponde a 01_importar_datos.R)
Antes de modelizar es necesario separar la serie agregada de “Ciudad de Madrid” —que no es una unidad territorial comparable con los distritos— del resto, y dejar el precio en formato numérico y el mes en un orden cronológico explícito (en lugar del orden alfabético que usaría R por defecto).
ruta_datos <- here::here("data", "DATOS_R_LIMPIOS_serie.xlsx")
datos <- read_excel(ruta_datos)
datos <- datos %>%
rename(
anio = `Año`,
distrito = Distrito,
mes = Mes,
precio = `Precio (€/m2)`
) %>%
mutate(precio = as.numeric(gsub(",", ".", as.character(precio))))
str(datos)
## tibble [3,694 × 4] (S3: tbl_df/tbl/data.frame)
## $ anio : num [1:3694] 2011 2011 2011 2011 2011 ...
## $ distrito: chr [1:3694] "Ciudad de Madrid" "Ciudad de Madrid" "Ciudad de Madrid" "Ciudad de Madrid" ...
## $ mes : chr [1:3694] "Enero" "Febrero" "Marzo" "Abril" ...
## $ precio : num [1:3694] 12.7 12.4 12.4 12.4 12.4 12.4 12.4 12.3 12.3 12.3 ...
glimpse(datos)
## Rows: 3,694
## Columns: 4
## $ anio <dbl> 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2…
## $ distrito <chr> "Ciudad de Madrid", "Ciudad de Madrid", "Ciudad de Madrid", "…
## $ mes <chr> "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio…
## $ precio <dbl> 12.7, 12.4, 12.4, 12.4, 12.4, 12.4, 12.4, 12.3, 12.3, 12.3, 1…
datos_ciudad <- datos %>% filter(distrito == "Ciudad de Madrid")
datos_distritos <- datos %>% filter(distrito != "Ciudad de Madrid")
meses_orden <- c("Enero","Febrero","Marzo","Abril","Mayo","Junio",
"Julio","Agosto","Septiembre","Octubre","Noviembre",
"Diciembre")
datos_distritos <- datos_distritos %>%
mutate(
mes = factor(mes, levels = meses_orden, ordered = FALSE),
distrito = factor(distrito)
) %>%
filter(!is.na(precio)) # se excluyen los 5 NA documentados (< 0.2% de los datos)
nrow(datos_distritos)
## [1] 3521
Tras la depuración, la base de trabajo práctica queda con 3521 observaciones distrito-mes-año sobre 21 distritos. No se han eliminado datos por su valor (alto o bajo): solo se han excluido los NA ya documentados, siguiendo el criterio de que una observación rara pero válida debe mantenerse en el análisis en lugar de descartarse automáticamente.
(Corresponde a 02_exploratorio.R)
Antes de descender al detalle por distrito, conviene observar la
forma general de la variable precio. El histograma
siguiente muestra la distribución conjunta de todas las observaciones
distrito-mes-año, sin distinguir todavía por distrito ni por año.
ggplot(datos_distritos, aes(x = precio)) +
geom_histogram(binwidth = 1, fill = "steelblue", color = "white") +
labs(title = "Distribución general del precio del alquiler (2011-2024)",
x = "Precio (€/m2)", y = "Frecuencia") +
theme_minimal()
(Nota de redacción: revisar la figura tras compilar y describir
aquí si se observa cola a la derecha, si aparecen uno o varios
“bultos”/modas, y si esa forma es coherente con la existencia de grupos
de distritos con niveles medios distintos — eso reforzaría la
justificación de incluir distrito como variable explicativa
en los modelos de las secciones 4 y 5.)
descriptivos_distrito <- datos_distritos %>%
group_by(distrito) %>%
summarise(
media = mean(precio, na.rm = TRUE),
sd = sd(precio, na.rm = TRUE),
minimo = min(precio, na.rm = TRUE),
maximo = max(precio, na.rm = TRUE),
n = n(),
.groups = "drop"
) %>%
arrange(desc(media))
knitr::kable(descriptivos_distrito, digits = 2,
caption = "Tabla 1. Estadísticos descriptivos por distrito")
| distrito | media | sd | minimo | maximo | n |
|---|---|---|---|---|---|
| 04. Salamanca | 16.97 | 2.98 | 13.3 | 25.3 | 167 |
| 01. Centro | 16.91 | 2.89 | 13.3 | 25.5 | 168 |
| 07. Chamberí | 16.47 | 2.78 | 13.1 | 24.4 | 168 |
| 05. Chamartín | 14.99 | 2.27 | 12.0 | 21.2 | 168 |
| 03. Retiro | 14.62 | 2.50 | 11.5 | 21.5 | 168 |
| 06. Tetuán | 14.23 | 2.44 | 11.1 | 20.6 | 168 |
| 02. Arganzuela | 13.91 | 2.42 | 10.9 | 20.5 | 168 |
| 09. Moncloa-Aravaca | 13.63 | 2.22 | 10.7 | 19.8 | 168 |
| 15. Ciudad Lineal | 12.34 | 1.95 | 10.0 | 17.4 | 167 |
| 16. Hortaleza | 12.04 | 1.51 | 10.2 | 16.7 | 168 |
| 08. Fuencarral-El Pardo | 11.78 | 1.59 | 9.8 | 16.1 | 168 |
| 21. Barajas | 11.24 | 1.39 | 9.4 | 15.3 | 168 |
| 20. San Blas-Canillejas | 11.11 | 1.55 | 9.0 | 15.2 | 168 |
| 13. Puente de Vallecas | 10.96 | 2.20 | 8.2 | 16.9 | 168 |
| 10. Latina | 10.87 | 2.17 | 8.3 | 17.0 | 168 |
| 11. Carabanchel | 10.82 | 1.96 | 8.3 | 16.2 | 168 |
| 12. Usera | 10.77 | 2.33 | 8.2 | 21.1 | 168 |
| 18. Villa de Vallecas | 10.56 | 1.59 | 8.6 | 15.2 | 168 |
| 14. Moratalaz | 10.32 | 1.77 | 8.0 | 15.1 | 167 |
| 19. Vicálvaro | 9.87 | 1.50 | 7.8 | 14.5 | 166 |
| 17. Villaverde | 9.81 | 2.01 | 7.4 | 15.8 | 166 |
La Tabla 1 confirma una jerarquía territorial clara: los distritos
del centro y del eje norte (encabezados por 04. Salamanca) presentan
tanto la media como la desviación típica más altas, mientras que los
distritos periféricos del sur y el este muestran precios más bajos y, en
general, más homogéneos (menor sd). Esta jerarquía se
mantiene, como se verá, en los coeficientes del modelo de regresión.
La Figura siguiente representa la misma información de la Tabla 1 en formato gráfico, como ranking del precio medio del alquiler por distrito a lo largo de todo el periodo analizado.
ggplot(descriptivos_distrito,
aes(x = reorder(distrito, media), y = media)) +
geom_col(fill = "steelblue") +
geom_text(aes(label = round(media, 1)), hjust = -0.15, size = 3) +
coord_flip() +
scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(title = "Ranking de distritos por precio medio de alquiler",
x = "Distrito", y = "Precio medio (€/m2)") +
theme_minimal()
Los distritos del centro y del eje norte (04. Salamanca a la cabeza) presentan precios medios claramente superiores al resto, mientras que los distritos periféricos del sur y el este muestran los niveles más moderados de toda la serie.
evolucion_anual <- datos_distritos %>%
group_by(anio) %>%
summarise(precio_medio = mean(precio, na.rm = TRUE), .groups = "drop")
ggplot(evolucion_anual, aes(x = anio, y = precio_medio)) +
geom_line(color = "steelblue", linewidth = 1) +
geom_point(color = "steelblue") +
labs(title = "Evolución del precio medio del alquiler en Madrid (2011-2024)",
x = "Año", y = "Precio medio (€/m2)") +
theme_minimal()
La serie del precio medio de la ciudad muestra un patrón de varias fases: una etapa de caída o estancamiento en los primeros años de la serie (efecto de la crisis económica y su prolongación), una subida sostenida en la segunda mitad de la década, un retroceso puntual coincidiendo con la pandemia de COVID-19 en 2020-2021, y un repunte pronunciado en los años más recientes que sitúa el precio en el máximo de toda la serie.
evolucion_distrito <- datos_distritos %>%
group_by(anio, distrito) %>%
summarise(precio_medio = mean(precio, na.rm = TRUE), .groups = "drop")
ggplot(evolucion_distrito, aes(x = anio, y = precio_medio, color = distrito)) +
geom_line(linewidth = 0.6) +
labs(title = "Evolución del precio del alquiler por distrito",
x = "Año", y = "Precio medio (€/m2)", color = "Distrito") +
theme_minimal() +
theme(legend.text = element_text(size = 7))
Al desagregar por distrito se aprecia que todos siguen el mismo patrón general, pero los distritos más caros se disparan con mayor intensidad en el tramo final de la serie que los más baratos: la brecha de precios entre distritos no se ha mantenido constante, sino que se ha ampliado con el tiempo. Un caso singular dentro de este gráfico es el distrito de Puente de Vallecas durante 2020, que se analiza en detalle en el apartado 3.5.
ggplot(datos_distritos, aes(x = reorder(distrito, precio, median), y = precio)) +
geom_boxplot(fill = "steelblue", alpha = 0.6) +
coord_flip() +
labs(title = "Distribución del precio del alquiler por distrito",
x = "Distrito", y = "Precio (€/m2)") +
theme_minimal()
El diagrama de cajas confirma la jerarquía observada en la Tabla 1: los distritos centrales no solo tienen la mediana más alta, sino también las cajas más anchas (mayor dispersión), lo que indica que su precio es más sensible a la localización exacta de la vivienda dentro del distrito y al momento temporal. Los distritos periféricos, en cambio, muestran cajas más estrechas y compactas.
q1 <- quantile(datos_distritos$precio, 0.25, na.rm = TRUE)
q3 <- quantile(datos_distritos$precio, 0.75, na.rm = TRUE)
iqr <- q3 - q1
atipicos <- datos_distritos %>%
filter(precio < q1 - 1.5 * iqr | precio > q3 + 1.5 * iqr)
nrow(atipicos)
## [1] 58
La regla del rango intercuartílico identifica 58 observaciones atípicas (1.65% del total). No se eliminan del análisis: pueden reflejar comportamiento real del mercado y no errores de medición, como ilustra el siguiente caso.
Durante la exploración se detectó un incremento sostenido y atípico en el distrito de Puente de Vallecas durante 2020, con una caída posterior en 2021 — un movimiento mucho más brusco que el de otros distritos de su mismo rango de precio. Antes de aceptar este dato se comprobó su coherencia mes a mes, para descartar que se tratara de un error puntual de un solo registro:
# NOTA: ajusta el nombre exacto del nivel de distrito si difiere en tu factor
# (p. ej. "13. Puente de Vallecas" vs. "Puente de Vallecas").
puente_vallecas_mensual <- datos_distritos %>%
filter(str_detect(distrito, "Puente de Vallecas"), anio %in% 2019:2021) %>%
arrange(anio, match(mes, meses_orden)) %>%
select(anio, mes, precio)
knitr::kable(puente_vallecas_mensual, digits = 2,
caption = "Evolución mensual del precio en Puente de Vallecas (2019-2021).")
| anio | mes | precio |
|---|---|---|
| 2019 | Enero | 11.1 |
| 2019 | Febrero | 11.5 |
| 2019 | Marzo | 11.5 |
| 2019 | Abril | 11.4 |
| 2019 | Mayo | 11.5 |
| 2019 | Junio | 11.9 |
| 2019 | Julio | 12.1 |
| 2019 | Agosto | 12.2 |
| 2019 | Septiembre | 12.1 |
| 2019 | Octubre | 12.2 |
| 2019 | Noviembre | 12.3 |
| 2019 | Diciembre | 12.3 |
| 2020 | Enero | 13.4 |
| 2020 | Febrero | 13.6 |
| 2020 | Marzo | 13.7 |
| 2020 | Abril | 13.7 |
| 2020 | Mayo | 13.8 |
| 2020 | Junio | 13.7 |
| 2020 | Julio | 13.6 |
| 2020 | Agosto | 13.5 |
| 2020 | Septiembre | 13.4 |
| 2020 | Octubre | 13.3 |
| 2020 | Noviembre | 13.1 |
| 2020 | Diciembre | 12.9 |
| 2021 | Enero | 11.7 |
| 2021 | Febrero | 11.7 |
| 2021 | Marzo | 11.7 |
| 2021 | Abril | 11.7 |
| 2021 | Mayo | 11.5 |
| 2021 | Junio | 11.5 |
| 2021 | Julio | 11.6 |
| 2021 | Agosto | 11.7 |
| 2021 | Septiembre | 11.6 |
| 2021 | Octubre | 12.0 |
| 2021 | Noviembre | 12.1 |
| 2021 | Diciembre | 12.0 |
La tabla anterior muestra que, dentro del propio año 2020, el precio se mantiene estable mes a mes, sin saltos raros — el salto ocurre únicamente en la transición de diciembre a enero, tanto al entrar en 2020 como al salir de él. Es decir, todo el año 2020 tiene un nivel elevado y sostenido, no un pico aislado de un mes, lo que descarta que se trate de un error de tecleo o un valor mal codificado.
La interpretación más plausible es un efecto real del mercado: con el turismo y el alquiler de corta duración paralizados por el COVID-19, es posible que parte de la vivienda turística de la zona pasara temporalmente al mercado de alquiler tradicional, alterando la composición de la oferta en ese distrito durante ese año concreto. No es posible distinguir con certeza esta hipótesis de un cambio metodológico puntual de la fuente (Ayuntamiento de Madrid) solo con estos datos.
Decisión adoptada: no se elimina ni se corrige el dato — es un valor real, no un artefacto de limpieza — pero se documenta aquí como una observación singular y como una limitación a tener en cuenta en la interpretación de los resultados de ese distrito concreto.
(Corresponde a 03_modelo_regresion.R)
\[ Precio_{it} = \beta_0 + \beta_1 Distrito_i + \beta_2 Año_t + \beta_3 Mes_t + \varepsilon_{it} \]
Se reserva un 20% de los datos para validación
(createDataPartition, semilla fija para reproducibilidad) y
se ajusta el modelo sobre el 80% restante.
set.seed(123)
indices_train <- createDataPartition(datos_distritos$precio, p = 0.8, list = FALSE)
train_set <- datos_distritos[indices_train, ]
test_set <- datos_distritos[-indices_train, ]
modelo_lm <- lm(precio ~ distrito + anio + mes, data = train_set)
summary(modelo_lm)
##
## Call:
## lm(formula = precio ~ distrito + anio + mes, data = train_set)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.5522 -0.8310 -0.1812 0.6128 5.4078
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -8.907e+02 1.050e+01 -84.786 < 2e-16 ***
## distrito02. Arganzuela -3.125e+00 1.349e-01 -23.168 < 2e-16 ***
## distrito03. Retiro -2.337e+00 1.364e-01 -17.129 < 2e-16 ***
## distrito04. Salamanca -8.414e-02 1.369e-01 -0.614 0.538951
## distrito05. Chamartín -1.974e+00 1.347e-01 -14.658 < 2e-16 ***
## distrito06. Tetuán -2.721e+00 1.340e-01 -20.309 < 2e-16 ***
## distrito07. Chamberí -4.634e-01 1.369e-01 -3.384 0.000724 ***
## distrito08. Fuencarral-El Pardo -5.205e+00 1.359e-01 -38.303 < 2e-16 ***
## distrito09. Moncloa-Aravaca -3.314e+00 1.345e-01 -24.650 < 2e-16 ***
## distrito10. Latina -6.116e+00 1.388e-01 -44.051 < 2e-16 ***
## distrito11. Carabanchel -6.122e+00 1.349e-01 -45.384 < 2e-16 ***
## distrito12. Usera -6.234e+00 1.377e-01 -45.260 < 2e-16 ***
## distrito13. Puente de Vallecas -5.993e+00 1.375e-01 -43.592 < 2e-16 ***
## distrito14. Moratalaz -6.664e+00 1.351e-01 -49.311 < 2e-16 ***
## distrito15. Ciudad Lineal -4.657e+00 1.361e-01 -34.209 < 2e-16 ***
## distrito16. Hortaleza -4.894e+00 1.392e-01 -35.164 < 2e-16 ***
## distrito17. Villaverde -7.176e+00 1.344e-01 -53.375 < 2e-16 ***
## distrito18. Villa de Vallecas -6.357e+00 1.380e-01 -46.052 < 2e-16 ***
## distrito19. Vicálvaro -7.117e+00 1.367e-01 -52.079 < 2e-16 ***
## distrito20. San Blas-Canillejas -5.846e+00 1.361e-01 -42.936 < 2e-16 ***
## distrito21. Barajas -5.739e+00 1.342e-01 -42.761 < 2e-16 ***
## anio 4.497e-01 5.206e-03 86.381 < 2e-16 ***
## mesFebrero 1.171e-01 1.027e-01 1.141 0.254126
## mesMarzo 1.549e-01 1.028e-01 1.506 0.132091
## mesAbril 2.780e-01 1.037e-01 2.680 0.007411 **
## mesMayo 2.775e-01 1.037e-01 2.677 0.007462 **
## mesJunio 3.204e-01 1.035e-01 3.097 0.001973 **
## mesJulio 3.040e-01 1.046e-01 2.906 0.003687 **
## mesAgosto 3.907e-01 1.041e-01 3.754 0.000177 ***
## mesSeptiembre 4.011e-01 1.036e-01 3.874 0.000110 ***
## mesOctubre 4.956e-01 1.033e-01 4.796 1.70e-06 ***
## mesNoviembre 4.397e-01 1.035e-01 4.249 2.22e-05 ***
## mesDiciembre 5.112e-01 1.043e-01 4.901 1.01e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.112 on 2784 degrees of freedom
## Multiple R-squared: 0.8745, Adjusted R-squared: 0.8731
## F-statistic: 606.5 on 32 and 2784 DF, p-value: < 2.2e-16
El coeficiente de anio (0.45 €/m² por año) captura la
tendencia temporal agregada, en línea con la subida observada en el
gráfico de evolución anual. Los coeficientes de distrito reproducen la
jerarquía territorial ya vista en el análisis descriptivo: los distritos
periféricos presentan los coeficientes más negativos respecto a la
categoría de referencia, mientras que los distritos centrales son los
que menos se alejan de ella (o incluso la superan).
par(mfrow = c(2, 2))
plot(modelo_lm)
par(mfrow = c(1, 1))
El diagnóstico de residuos revela dos limitaciones habituales del ajuste lineal en este tipo de datos. En el gráfico de residuos frente a valores ajustados, una forma curva en lugar de una nube de puntos aleatoria sugeriría que la relación entre las variables no es completamente lineal. El gráfico Q-Q, por su parte, permite valorar si hay heterocedasticidad: una desviación de la normalidad concentrada en las colas (precios muy altos o muy bajos) indicaría mayor error de predicción precisamente en esos tramos extremos, algo relevante de cara a interpretar las predicciones del modelo con cautela en los distritos más caros.
test_set <- test_set %>%
mutate(precio_predicho = predict(modelo_lm, newdata = test_set))
ggplot(test_set, aes(x = precio, y = precio_predicho)) +
geom_point(alpha = 0.4, color = "steelblue") +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed") +
labs(title = "Regresión lineal: precio real vs. precio predicho",
x = "Precio real (€/m2)", y = "Precio predicho (€/m2)") +
theme_minimal()
Si la nube de puntos se ajusta bien a la diagonal en el tramo central de precios pero se separa de ella en los extremos, esto sería coherente con la heterocedasticidad detectada en el diagnóstico de residuos: el modelo tendería a sobreestimar los precios más bajos y a subestimar los más altos.
mae_lm <- mae(test_set$precio, test_set$precio_predicho)
rmse_lm <- rmse(test_set$precio, test_set$precio_predicho)
r2_lm <- cor(test_set$precio, test_set$precio_predicho)^2
metricas_lm <- tibble(modelo = "Regresión lineal múltiple",
MAE = mae_lm, RMSE = rmse_lm, R2 = r2_lm)
knitr::kable(metricas_lm, digits = 3)
| modelo | MAE | RMSE | R2 |
|---|---|---|---|
| Regresión lineal múltiple | 0.919 | 1.233 | 0.849 |
Sobre el conjunto de prueba, el modelo lineal alcanza un MAE de 0.919 €/m², un RMSE de 1.233 €/m² y un R² de 0.849, es decir, explica aproximadamente el 84.9% de la variabilidad del precio en datos no vistos durante el entrenamiento.
Un modelo aditivo (distrito + anio) asume que el precio
de todos los distritos crece a la misma velocidad anual, solo que
partiendo de niveles distintos. Para comprobar si esa asunción es
razonable —es decir, si los distritos caros suben más rápido que los
baratos, tal y como sugiere el gráfico de evolución por distrito del
apartado 3.3— se amplía el modelo con un término de interacción
distrito×año.
modelo_lm_int <- lm(precio ~ distrito * anio + mes, data = train_set)
summary(modelo_lm_int)
##
## Call:
## lm(formula = precio ~ distrito * anio + mes, data = train_set)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.2828 -0.7716 -0.1748 0.5664 4.4022
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.294e+03 4.458e+01 -29.034 < 2e-16 ***
## distrito02. Arganzuela 2.709e+02 6.287e+01 4.309 1.70e-05 ***
## distrito03. Retiro 2.242e+02 6.312e+01 3.552 0.000389 ***
## distrito04. Salamanca 8.450e+01 6.357e+01 1.329 0.183877
## distrito05. Chamartín 3.290e+02 6.275e+01 5.243 1.70e-07 ***
## distrito06. Tetuán 2.290e+02 6.212e+01 3.687 0.000232 ***
## distrito07. Chamberí 9.814e+01 6.369e+01 1.541 0.123462
## distrito08. Fuencarral-El Pardo 5.929e+02 6.362e+01 9.319 < 2e-16 ***
## distrito09. Moncloa-Aravaca 3.232e+02 6.284e+01 5.142 2.90e-07 ***
## distrito10. Latina 3.179e+02 6.401e+01 4.967 7.21e-07 ***
## distrito11. Carabanchel 4.937e+02 6.213e+01 7.947 2.77e-15 ***
## distrito12. Usera 3.312e+02 6.412e+01 5.165 2.57e-07 ***
## distrito13. Puente de Vallecas 3.605e+02 6.372e+01 5.658 1.69e-08 ***
## distrito14. Moratalaz 4.902e+02 6.278e+01 7.809 8.14e-15 ***
## distrito15. Ciudad Lineal 4.968e+02 6.460e+01 7.691 2.02e-14 ***
## distrito16. Hortaleza 6.708e+02 6.441e+01 10.414 < 2e-16 ***
## distrito17. Villaverde 4.800e+02 6.291e+01 7.629 3.24e-14 ***
## distrito18. Villa de Vallecas 6.566e+02 6.359e+01 10.326 < 2e-16 ***
## distrito19. Vicálvaro 6.322e+02 6.401e+01 9.877 < 2e-16 ***
## distrito20. San Blas-Canillejas 6.268e+02 6.311e+01 9.933 < 2e-16 ***
## distrito21. Barajas 7.035e+02 6.264e+01 11.230 < 2e-16 ***
## anio 6.499e-01 2.210e-02 29.407 < 2e-16 ***
## mesFebrero 9.741e-02 9.593e-02 1.015 0.310029
## mesMarzo 1.391e-01 9.605e-02 1.448 0.147593
## mesAbril 2.431e-01 9.691e-02 2.508 0.012181 *
## mesMayo 2.834e-01 9.682e-02 2.927 0.003454 **
## mesJunio 3.081e-01 9.667e-02 3.187 0.001455 **
## mesJulio 3.112e-01 9.771e-02 3.185 0.001465 **
## mesAgosto 3.658e-01 9.725e-02 3.761 0.000172 ***
## mesSeptiembre 3.788e-01 9.673e-02 3.916 9.22e-05 ***
## mesOctubre 4.779e-01 9.652e-02 4.951 7.81e-07 ***
## mesNoviembre 4.137e-01 9.665e-02 4.280 1.93e-05 ***
## mesDiciembre 4.826e-01 9.743e-02 4.953 7.74e-07 ***
## distrito02. Arganzuela:anio -1.358e-01 3.116e-02 -4.359 1.35e-05 ***
## distrito03. Retiro:anio -1.123e-01 3.129e-02 -3.589 0.000338 ***
## distrito04. Salamanca:anio -4.194e-02 3.151e-02 -1.331 0.183311
## distrito05. Chamartín:anio -1.641e-01 3.110e-02 -5.275 1.43e-07 ***
## distrito06. Tetuán:anio -1.149e-01 3.079e-02 -3.731 0.000195 ***
## distrito07. Chamberí:anio -4.889e-02 3.157e-02 -1.549 0.121559
## distrito08. Fuencarral-El Pardo:anio -2.964e-01 3.153e-02 -9.401 < 2e-16 ***
## distrito09. Moncloa-Aravaca:anio -1.618e-01 3.115e-02 -5.196 2.19e-07 ***
## distrito10. Latina:anio -1.606e-01 3.173e-02 -5.063 4.40e-07 ***
## distrito11. Carabanchel:anio -2.478e-01 3.080e-02 -8.045 1.27e-15 ***
## distrito12. Usera:anio -1.673e-01 3.178e-02 -5.263 1.53e-07 ***
## distrito13. Puente de Vallecas:anio -1.817e-01 3.159e-02 -5.752 9.79e-09 ***
## distrito14. Moratalaz:anio -2.463e-01 3.112e-02 -7.915 3.54e-15 ***
## distrito15. Ciudad Lineal:anio -2.486e-01 3.202e-02 -7.763 1.16e-14 ***
## distrito16. Hortaleza:anio -3.350e-01 3.193e-02 -10.490 < 2e-16 ***
## distrito17. Villaverde:anio -2.415e-01 3.118e-02 -7.743 1.35e-14 ***
## distrito18. Villa de Vallecas:anio -3.286e-01 3.152e-02 -10.426 < 2e-16 ***
## distrito19. Vicálvaro:anio -3.169e-01 3.173e-02 -9.988 < 2e-16 ***
## distrito20. San Blas-Canillejas:anio -3.136e-01 3.128e-02 -10.026 < 2e-16 ***
## distrito21. Barajas:anio -3.515e-01 3.105e-02 -11.322 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.038 on 2764 degrees of freedom
## Multiple R-squared: 0.8915, Adjusted R-squared: 0.8894
## F-statistic: 436.7 on 52 and 2764 DF, p-value: < 2.2e-16
anova(modelo_lm, modelo_lm_int)
AIC(modelo_lm, modelo_lm_int)
BIC(modelo_lm, modelo_lm_int)
El contraste ANOVA entre ambos modelos, junto con los criterios de información AIC y BIC de la salida anterior, indica si la interacción aporta una mejora de ajuste suficiente para justificar la complejidad adicional (más del doble de coeficientes que el modelo aditivo). Un AIC/BIC menor en el modelo con interacción respalda la hipótesis de que la tendencia temporal difiere por distrito; si la mejora es marginal, el modelo aditivo resulta preferible por su mayor parsimonia e interpretabilidad.
test_set <- test_set %>%
mutate(precio_predicho_int = predict(modelo_lm_int, newdata = test_set))
mae_int <- mae(test_set$precio, test_set$precio_predicho_int)
rmse_int <- rmse(test_set$precio, test_set$precio_predicho_int)
r2_int <- cor(test_set$precio, test_set$precio_predicho_int)^2
metricas_int <- tibble(modelo = "Regresión lineal con interacción distrito×año",
MAE = mae_int, RMSE = rmse_int, R2 = r2_int)
knitr::kable(metricas_int, digits = 3)
| modelo | MAE | RMSE | R2 |
|---|---|---|---|
| Regresión lineal con interacción distrito×año | 0.873 | 1.167 | 0.865 |
chequeo_2024 <- data.frame(
distrito = factor(c("07. Chamberí", "04. Salamanca", "03. Retiro"),
levels = levels(train_set$distrito)),
anio = 2024,
mes = factor("Enero", levels = levels(train_set$mes))
)
real_2024 <- datos_distritos %>%
filter(anio == 2024, mes == "Enero",
distrito %in% c("07. Chamberí","04. Salamanca","03. Retiro")) %>%
select(distrito, precio_real = precio)
chequeo_2024 %>%
mutate(
pred_sin_interaccion = predict(modelo_lm, newdata = chequeo_2024),
pred_con_interaccion = predict(modelo_lm_int, newdata = chequeo_2024)
) %>%
left_join(real_2024, by = "distrito") %>%
knitr::kable(digits = 2)
| distrito | anio | mes | pred_sin_interaccion | pred_con_interaccion | precio_real |
|---|---|---|---|---|---|
| 07. Chamberí | 2024 | Enero | 19.12 | 20.10 | 22.1 |
| 04. Salamanca | 2024 | Enero | 19.50 | 20.54 | 22.7 |
| 03. Retiro | 2024 | Enero | 17.24 | 17.85 | 19.7 |
En términos de error puro, el modelo con interacción pasa de un MAE de 0.919 a 0.873 €/m² (variación de -0.046 €/m²). La tabla anterior complementa esta cifra agregada con una comprobación de sentido común: para tres distritos representativos en enero de 2024, ¿qué modelo se acerca más al precio real observado? Esta clase de contraste puntual ayuda a detectar si una mejora en las métricas globales se traduce también en predicciones razonables caso a caso, o si responde solo a un mejor ajuste medio.
(Corresponde a 04_modelo_arbol.R)
Como alternativa no lineal a la regresión se ajusta un árbol de
regresión (rpart, method = "anova") sobre la
misma partición de entrenamiento y prueba, podado según el parámetro de
complejidad (cp) que minimiza el error de validación
cruzada.
modelo_arbol <- rpart(precio ~ distrito + anio + mes,
data = train_set, method = "anova")
cp_optimo <- modelo_arbol$cptable[which.min(modelo_arbol$cptable[, "xerror"]), "CP"]
modelo_arbol_podado <- prune(modelo_arbol, cp = cp_optimo)
El parámetro de complejidad óptimo seleccionado por validación cruzada fue cp = 0.01.
Antes de representar el árbol gráficamente conviene inspeccionar su
estructura en formato texto: número de nodos, criterio de división en
cada uno, desviación (deviance) y número de observaciones
(n) en cada rama — el equivalente, en árboles, a la tabla
de coeficientes en la regresión.
print(modelo_arbol_podado)
## n= 2817
##
## node), split, n, deviance, yval
## * denotes terminal node
##
## 1) root 2817 27432.53000 12.583460
## 2) distrito=08. Fuencarral-El Pardo,10. Latina,11. Carabanchel,12. Usera,13. Puente de Vallecas,14. Moratalaz,15. Ciudad Lineal,16. Hortaleza,17. Villaverde,18. Villa de Vallecas,19. Vicálvaro,20. San Blas-Canillejas,21. Barajas 1726 6541.87400 10.933660
## 4) anio< 2017.5 871 850.46860 9.501263
## 8) distrito=10. Latina,11. Carabanchel,12. Usera,13. Puente de Vallecas,14. Moratalaz,17. Villaverde,18. Villa de Vallecas,19. Vicálvaro 531 228.43020 8.919397 *
## 9) distrito=08. Fuencarral-El Pardo,15. Ciudad Lineal,16. Hortaleza,20. San Blas-Canillejas,21. Barajas 340 161.48600 10.410000 *
## 5) anio>=2017.5 855 2083.78600 12.392870
## 10) anio< 2022.5 613 511.33640 11.676670 *
## 11) anio>=2022.5 242 461.55810 14.207020 *
## 3) distrito=01. Centro,02. Arganzuela,03. Retiro,04. Salamanca,05. Chamartín,06. Tetuán,07. Chamberí,09. Moncloa-Aravaca 1091 8760.58400 15.193490
## 6) anio< 2017.5 532 1132.50100 13.116170
## 12) distrito=02. Arganzuela,03. Retiro,05. Chamartín,06. Tetuán,09. Moncloa-Aravaca 337 298.44480 12.323440 *
## 13) distrito=01. Centro,04. Salamanca,07. Chamberí 195 256.29260 14.486150 *
## 7) anio>=2017.5 559 3147.50300 17.170480
## 14) anio< 2022.5 403 825.80740 16.165760
## 28) distrito=02. Arganzuela,03. Retiro,05. Chamartín,06. Tetuán,09. Moncloa-Aravaca 260 150.98750 15.275000 *
## 29) distrito=01. Centro,04. Salamanca,07. Chamberí 143 93.43916 17.785310 *
## 15) anio>=2022.5 156 863.92990 19.766030
## 30) distrito=02. Arganzuela,03. Retiro,05. Chamartín,06. Tetuán,09. Moncloa-Aravaca 99 223.89290 18.431310 *
## 31) distrito=01. Centro,04. Salamanca,07. Chamberí 57 157.35580 22.084210 *
El árbol completo, con distrito (21 niveles) como
variable candidata en cada división, tiende a producir nodos cuya regla
de corte es una lista larga de distritos (p. ej. “distrito = 01. Centro,
04. Salamanca, 07. Chamberí, …”), lo que hace el diagrama difícil de
leer. Para las métricas y las predicciones se sigue usando el árbol
completo podado por validación cruzada
(modelo_arbol_podado); para la figura se muestra una
versión simplificada, limitada a una profundidad de 3 niveles, que
conserva las divisiones más importantes (las de mayor reducción de
varianza). Además, en la figura cada división por distrito se etiqueta
solo con el número de distritos que caen a cada lado (p. ej. “distrito
(grupo de 11)”), en lugar de listarlos todos: el listado completo y
exacto de qué distrito corresponde a cada grupo se recoge, de forma
legible, en la salida de texto de rpart.rules() que se
muestra justo después de la figura.
arbol_visual <- rpart(precio ~ distrito + anio + mes, data = train_set,
method = "anova", control = rpart.control(maxdepth = 3))
acortar_distritos <- function(x, labs, digits, varlen, faclen) {
sapply(labs, function(lab) {
if (grepl("^distrito", lab)) {
valores <- strsplit(sub("^distrito\\s*=\\s*", "", lab), ",")[[1]]
paste0("distrito\n(grupo de ", length(valores), ")")
} else {
lab
}
}, USE.NAMES = FALSE)
}
rpart.plot(arbol_visual, digits = 2, fallen.leaves = TRUE,
type = 2, extra = 101, under = FALSE,
box.palette = "Blues", shadow.col = "gray85",
branch.lty = 1, tweak = 1.15, cex = NULL,
split.fun = acortar_distritos)
Como alternativa (o complemento) a la figura, rpart.plot
también permite listar las reglas del árbol completo en formato texto,
sin las limitaciones visuales del diagrama:
rpart.plot::rpart.rules(modelo_arbol_podado, cover = TRUE, roundint = FALSE)
La estructura del árbol permite leer directamente qué variables
segmentan más el precio: cuanto más arriba aparece una variable, mayor
es su capacidad para reducir la heterogeneidad de precios en cada
división. Es habitual que distrito domine las primeras
divisiones —reproduciendo la jerarquía centro/periferia ya vista en el
análisis descriptivo— y que anio aparezca después, marcando
el salto de precios de los años más recientes dentro de cada grupo de
distritos.
test_set <- test_set %>%
mutate(precio_predicho_arbol = predict(modelo_arbol_podado, newdata = test_set))
ggplot(test_set, aes(x = precio, y = precio_predicho_arbol)) +
geom_point(alpha = 0.4, color = "darkgreen") +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed") +
labs(title = "Árbol de decisión: precio real vs. precio predicho",
x = "Precio real (€/m2)", y = "Precio predicho (€/m2)") +
theme_minimal()
Al tratarse de un modelo que predice un valor constante por nodo terminal, los puntos predichos se agrupan en bandas horizontales (una por hoja del árbol), a diferencia de la nube continua del modelo lineal. Lo relevante es si esas bandas siguen la diagonal con más fidelidad que la regresión, especialmente en los precios extremos donde el modelo lineal mostraba más error.
mae_arbol <- mae(test_set$precio, test_set$precio_predicho_arbol)
rmse_arbol <- rmse(test_set$precio, test_set$precio_predicho_arbol)
r2_arbol <- cor(test_set$precio, test_set$precio_predicho_arbol)^2
metricas_arbol <- tibble(modelo = "Árbol de decisión",
MAE = mae_arbol, RMSE = rmse_arbol, R2 = r2_arbol)
knitr::kable(metricas_arbol, digits = 3)
| modelo | MAE | RMSE | R2 |
|---|---|---|---|
| Árbol de decisión | 0.75 | 1.033 | 0.896 |
El árbol alcanza un MAE de 0.75 €/m², un RMSE de 1.033 €/m² y un R² de 0.896 sobre el conjunto de prueba.
comparacion_modelos <- bind_rows(metricas_lm, metricas_int, metricas_arbol)
knitr::kable(comparacion_modelos, digits = 3,
caption = "Tabla 3. Comparación de métricas entre modelos")
| modelo | MAE | RMSE | R2 |
|---|---|---|---|
| Regresión lineal múltiple | 0.919 | 1.233 | 0.849 |
| Regresión lineal con interacción distrito×año | 0.873 | 1.167 | 0.865 |
| Árbol de decisión | 0.750 | 1.033 | 0.896 |
ggplot(comparacion_modelos, aes(x = modelo, y = MAE, fill = modelo)) +
geom_col(width = 0.5) +
labs(title = "Comparación del error (MAE) entre modelos", x = NULL, y = "MAE (€/m2)") +
theme_minimal() +
theme(legend.position = "none")
Comparar el resumen estadístico (summary()) del precio
real con el de las predicciones de cada modelo aporta una evidencia
adicional a las métricas de error: al predecir un valor constante por
nodo terminal, se espera que el árbol genere un rango de valores
predichos más estrecho que el real (mínimo más alto, máximo más bajo),
mientras que la regresión lineal, al ser una función continua, debería
aproximarse más al rango real. Esta evidencia conecta directamente con
la limitación del árbol para extrapolar que se discute en el apartado 7
(predicción a 2027).
summary(test_set$precio)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 7.40 10.30 12.00 12.59 14.40 25.30
summary(test_set$precio_predicho)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 6.921 10.328 12.366 12.587 14.365 20.008
summary(test_set$precio_predicho_arbol)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 8.919 10.410 11.677 12.497 14.207 22.084
De los tres modelos evaluados, Árbol de decisión es el que obtiene el menor error medio (MAE) sobre el conjunto de prueba, con una mejora relativa de aproximadamente el 18.3% respecto a Regresión lineal múltiple. Si el árbol de decisión es el modelo ganador, esto confirma la hipótesis de partida —que un modelo no lineal puede capturar mejor la estructura del precio— con la ventaja adicional de que, gracias a su profundidad limitada, su estructura sigue siendo tan legible como los coeficientes de un modelo lineal. Si en cambio la interacción distrito×año iguala o supera al árbol, el resultado sugiere que la principal fuente de no linealidad en los datos es precisamente esa interacción, y no relaciones más complejas que solo un árbol pudiera capturar.
distritos_todos <- levels(train_set$distrito)
pred_2027 <- data.frame(
distrito = factor(distritos_todos, levels = levels(train_set$distrito)),
anio = 2027,
mes = factor("Enero", levels = levels(train_set$mes))
)
pred_2027$precio_lm <- predict(modelo_lm, newdata = pred_2027)
pred_2027$precio_arbol <- predict(modelo_arbol_podado, newdata = pred_2027)
pred_2027$precio_int <- predict(modelo_lm_int, newdata = pred_2027)
pred_2027_long <- pred_2027 %>%
pivot_longer(cols = c(precio_lm, precio_int, precio_arbol),
names_to = "modelo", values_to = "precio_predicho") %>%
mutate(modelo = recode(modelo,
precio_lm = "Regresión lineal",
precio_int= "Regresión con intercción",
precio_arbol = "Árbol de decisión"))
ggplot(pred_2027_long,
aes(x = reorder(distrito, precio_predicho), y = precio_predicho, fill = modelo)) +
geom_col(position = "dodge") +
coord_flip() +
labs(title = "Precio del alquiler predicho para enero de 2027, por distrito",
x = "Distrito", y = "Precio predicho (€/m2)", fill = "Modelo") +
theme_minimal()
La siguiente tabla recoge los mismos valores del gráfico anterior, ordenados de menor a mayor precio predicho (usando como referencia de orden el precio medio de los tres modelos por distrito):
tabla_pred_2027 <- pred_2027 %>%
mutate(precio_medio_predicho = rowMeans(across(c(precio_lm, precio_int, precio_arbol)))) %>%
arrange(precio_medio_predicho) %>%
transmute(
Distrito = distrito,
`Regresión lineal (€/m2)` = round(precio_lm, 2),
`Regresión con interacción (€/m2)` = round(precio_int, 2),
`Árbol de decisión (€/m2)` = round(precio_arbol, 2),
`Media de los 3 modelos (€/m2)` = round(precio_medio_predicho, 2)
)
knitr::kable(tabla_pred_2027,
caption = "Tabla 4. Precio del alquiler predicho para enero de 2027, por distrito (ordenado de menor a mayor).")
| Distrito | Regresión lineal (€/m2) | Regresión con interacción (€/m2) | Árbol de decisión (€/m2) | Media de los 3 modelos (€/m2) |
|---|---|---|---|---|
| 19. Vicálvaro | 13.81 | 12.73 | 14.21 | 13.58 |
| 17. Villaverde | 13.75 | 13.38 | 14.21 | 13.78 |
| 18. Villa de Vallecas | 14.57 | 13.35 | 14.21 | 14.04 |
| 14. Moratalaz | 14.27 | 13.86 | 14.21 | 14.11 |
| 21. Barajas | 15.19 | 13.78 | 14.21 | 14.39 |
| 20. San Blas-Canillejas | 15.08 | 14.02 | 14.21 | 14.44 |
| 11. Carabanchel | 14.81 | 14.36 | 14.21 | 14.46 |
| 12. Usera | 14.70 | 15.02 | 14.21 | 14.64 |
| 10. Latina | 14.81 | 15.21 | 14.21 | 14.74 |
| 13. Puente de Vallecas | 14.94 | 15.13 | 14.21 | 14.76 |
| 08. Fuencarral-El Pardo | 15.72 | 14.83 | 14.21 | 14.92 |
| 16. Hortaleza | 16.04 | 14.75 | 14.21 | 15.00 |
| 15. Ciudad Lineal | 16.27 | 15.82 | 14.21 | 15.43 |
| 09. Moncloa-Aravaca | 17.62 | 17.99 | 18.43 | 18.01 |
| 02. Arganzuela | 17.80 | 18.43 | 18.43 | 18.22 |
| 06. Tetuán | 18.21 | 19.03 | 18.43 | 18.56 |
| 03. Retiro | 18.59 | 19.46 | 18.43 | 18.83 |
| 05. Chamartín | 18.96 | 19.31 | 18.43 | 18.90 |
| 07. Chamberí | 20.47 | 21.91 | 22.08 | 21.49 |
| 04. Salamanca | 20.85 | 22.36 | 22.08 | 21.76 |
| 01. Centro | 20.93 | 22.87 | 22.08 | 21.96 |
Este ejercicio es puramente ilustrativo y no debe leerse como una previsión fiable: 2027 queda fuera del rango de años observados (extrapolación), y los tres modelos extrapolan de forma muy distinta. La regresión lineal extrapola mecánicamente la recta ajustada, por lo que su predicción para 2027 sigue la misma pendiente que ha tenido durante toda la serie, sin ningún mecanismo que limite el crecimiento. El árbol de decisión, en cambio, no puede extrapolar más allá de su último punto de corte de año: al no existir una hoja específica para 2027, devuelve la misma predicción constante que para el último tramo de años que sí observó (≥ 2023), subestimando probablemente el precio si la tendencia de crecimiento continúa. Cualquier decisión real debería apoyarse en un modelo de series temporales pensado para proyectar hacia adelante, no en una regresión de corte transversal como esta.
El presente trabajo práctico ha aplicado dos enfoques de aprendizaje supervisado —regresión lineal múltiple y árboles de decisión (rpart, method = “anova”)— para modelizar el precio del alquiler por metro cuadrado en los distritos de Madrid entre 2011 y 2024. Los resultados permiten extraer las siguientes conclusiones técnicas:
La variable distrito emerge como el principal determinante del precio en ambos modelos, reproduciendo la jerarquía centro-periferia documentada en la literatura económica urbana: los coeficientes de la regresión lineal y las primeras divisiones del árbol de decisión identifican consistentemente a Salamanca, Chamberí y Retiro como los nodos o categorías con mayor precio medio, mientras que los distritos del sur y el este (Villaverde, Puente de Vallecas, Vicálvaro) ocupan los niveles inferiores, como ha sido hitóricamente dado que los parámetros y calidad del suelos así han dsitribuido la comunidad de Madrid a lo largo de toda la historila de la concida como villa de Madrid. Esta estabilidad sugiere que la localización intrametropolitana captura efectos fijos no observados (accesibilidad a empleos, equipamientos, externalidades de barrio) que no son completamente explicados por la tendencia temporal agregada.
El árbol de decisión podado por validación cruzada (cp = r round(cp_optimo, 4)) alcanza un MAE de r round(mae_arbol, 3) €/m² y un R² de r round(r2_arbol, 3) sobre el conjunto de prueba, superando marginalmente a la regresión lineal aditiva (MAE = r round(mae_lm, 3), R² = r round(r2_lm, 3)). La mejora relativa es del r round(100 * (mae_lm - mae_arbol) / mae_lm, 1)% en términos de error medio absoluto. Sin embargo, el modelo lineal con interacción distrito×año reduce la brecha (MAE = r round(mae_int, 3)), lo que indica que la principal fuente de no linealidad en los datos es la heterogeneidad de las tendencias temporales por distrito, y no relaciones más complejas que requieran particiones recursivas múltiples.
El diagnóstico de residuos del modelo lineal revela dos limitaciones estructurales: (i) una leve curvatura en el gráfico de residuos frente a valores ajustados, compatible con la hipótesis de que la pendiente temporal difiere por distrito (confirmada por el test ANOVA entre modelo aditivo e interactivo); (ii) desviaciones de la normalidad en las colas de la distribución Q-Q, concentradas en los distritos más caros, lo que sugiere heterocedasticidad condicionada al nivel de precio. El árbol de decisión, al predecir un valor constante por nodo terminal, evita la extrapolación lineal en las colas pero introduce un sesgo de subestimación en los máximos observados (el rango de predicciones del árbol es más estrecho que el rango real, como muestra el summary() de la sección 6).
El ejercicio de predicción para enero de 2027 ilustra una limitación fundamental de ambos enfoques cuando se aplican a horizontes temporales no observados: la regresión lineal extrapola mecánicamente la pendiente estimada en el periodo 2011-2024, asumiendo que la tendencia histórica se mantiene indefinidamente; el árbol de decisión, en cambio, no puede extrapolar más allá de su último punto de corte de año (≥ 2023), devolviendo la misma predicción constante para 2024, 2025, 2026 y 2027. Esta asimetría implica que, en presencia de una tendencia alcista sostenida, el árbol subestimará sistemáticamente el precio en horizontes futuros, mientras que la regresión lineal podría sobreestimarlo si la pendiente se atenúa. Para proyecciones fiables sería necesario un modelo de series temporales (p. ej. ARIMA, Prophet) o datos de panel con variables explicativas exógenas (tipos de interés, salario medio, stock de vivienda turística).
La unidad de observación (precio medio por distrito-mes-año) implica una pérdida de información microeconómica: no se controla por características de la vivienda (superficie, antigüedad, planta, ascensor, calificación energética), lo que introduce un sesgo de variable omitida si la composición del parque de viviendas en cada distrito cambia sistemáticamente con el tiempo. Además, el salto atípico de Puente de Vallecas en 2020 —documentado en la sección 3.5— no es replicable con las variables disponibles, lo que limita la validez externa del modelo para distritos con alta exposición al alquiler turístico o a cambios metodológicos de la fuente.
Desde el punto de vista del aprendizaje automático, el trabjo práctico confirma que, en contextos con datos agregados y pocos predictores (año, distrito, mes), la complejidad adicional de un árbol de decisión no aporta una mejora sustancial respecto a una regresión lineal bien especificada (con interacciones relevantes). La interpretabilidad de los coeficientes lineales (efecto marginal de un año adicional, diferencial de precio por distrito) y la facilidad de extrapolar hacen preferible el modelo lineal para análisis de políticas públicas, mientras que el árbol resulta útil como herramienta diagnóstica para detectar interacciones no lineales y segmentar el espacio de predictores sin especificación funcional previa.
Para mejorar la capacidad predictiva y la validez causal del modelo, futuras investigaciones podrían: - Incorporar datos de transacciones individuales (idealnotario, catastro) para controlar por características de la vivienda; - Incluir variables macroeconómicas (IPC, tipos de interés, desempleo) y de oferta (licencias de obra, stock de vivienda turística); - Aplicar modelos de panel con efectos fijos por distrito y errores estándar agrupados por distrito; - Explorar métodos de ensemble (Random Forest, XGBoost) para capturar interacciones de orden superior, sacrificando interpretabilidad por precisión predictiva.
La documentación utilizada versa principalmente en la documentación que el propio departamento de la asignatura nos ha entregado:
Se ha optado por entregar el trabajo práctico en formato HTML colgado en la página que en este momento está leyendo https://rpubs.com/SILGG/1451773, además de aportar el generado por RStudio y sus paquetes en .docx y .pdf
Se incluye la información de la sesión de R por reproducibilidad: paquetes y versiones exactas utilizados para generar este documento.
sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-apple-darwin20
## Running under: macOS Monterey 12.7.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: Europe/Madrid
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] broom_1.0.13 Metrics_0.1.4 rpart.plot_3.1.4 rpart_4.1.27
## [5] caret_7.0-1 lattice_0.22-9 readxl_1.5.0 here_1.0.2
## [9] lubridate_1.9.5 forcats_1.0.1 stringr_1.6.0 dplyr_1.2.1
## [13] purrr_1.2.2 readr_2.2.0 tidyr_1.3.2 tibble_3.3.1
## [17] ggplot2_4.0.3 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 timeDate_4052.112 farver_2.1.2
## [4] S7_0.2.2 fastmap_1.2.0 pROC_1.19.0.1
## [7] digest_0.6.39 timechange_0.4.0 lifecycle_1.0.5
## [10] survival_3.8-6 magrittr_2.0.5 compiler_4.6.1
## [13] rlang_1.3.0 sass_0.4.10 tools_4.6.1
## [16] yaml_2.3.12 data.table_1.18.4 knitr_1.51
## [19] labeling_0.4.3 plyr_1.8.9 RColorBrewer_1.1-3
## [22] withr_3.0.3 nnet_7.3-20 grid_4.6.1
## [25] stats4_4.6.1 future_1.75.0 globals_0.19.1
## [28] scales_1.4.0 iterators_1.0.14 MASS_7.3-65
## [31] cli_3.6.6 rmarkdown_2.31 generics_0.1.4
## [34] otel_0.2.0 rstudioapi_0.19.0 future.apply_1.20.2
## [37] reshape2_1.4.5 tzdb_0.5.0 cachem_1.1.0
## [40] splines_4.6.1 parallel_4.6.1 cellranger_1.1.0
## [43] vctrs_0.7.3 hardhat_1.4.3 Matrix_1.7-5
## [46] jsonlite_2.0.0 hms_1.1.4 listenv_1.0.0
## [49] foreach_1.5.2 gower_1.0.2 jquerylib_0.1.4
## [52] recipes_1.3.3 glue_1.8.1 parallelly_1.48.0
## [55] codetools_0.2-20 stringi_1.8.9 gtable_0.3.6
## [58] pillar_1.11.1 htmltools_0.5.9 ipred_0.9-15
## [61] lava_1.9.2 R6_2.6.1 rprojroot_2.1.1
## [64] evaluate_1.0.5 backports_1.5.1 bslib_0.12.0
## [67] class_7.3-23 Rcpp_1.1.2 nlme_3.1-169
## [70] prodlim_2026.03.11 xfun_0.60 pkgconfig_2.0.3
## [73] ModelMetrics_1.2.2.2