INTRODUCCIÓN Y MOTIVACIÓN

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:

  1. Por un lado, se emplea la regresión lineal múltiple como modelo base, dada su interpretabilidad.
  2. Por otro, se incorpora un modelo de árboles de decisión, que permite capturar posibles relaciones no lineales y patrones más complejos en los datos.

La comparación entre ambos enfoques permitirá evaluar el equilibrio entre capacidad predictiva e interpretabilidad.

OBJETIVOS DEL TRABAJO PRÁCTICO

Los objetivos del trabajo práctico son los siguientes:

  1. Analizar la evolución del precio del alquiler en Madrid entre 2011 y 2024.
  2. Estudiar las diferencias entre distritos y su comportamiento temporal.
  3. Construir un modelo de regresión lineal múltiple para explicar el precio del alquiler.
  4. Implementar un modelo alternativo basado en árboles de decisión.
  5. Comparar el rendimiento predictivo de ambos modelos.
  6. Evaluar el ajuste mediante métricas de error y medidas de bondad de ajuste.
  7. Valorar las limitaciones de cada enfoque en relación con la información disponible.

DESCRIPCIÓN Y JUSTIFICACIÓN DE LAS TÉCNICAS UTILIZADAS Y METODOLOGÍA

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:

  1. Regresión lineal múltiple: permite modelizar el precio del alquiler en función del tiempo y del distrito, facilitando la interpretación de los coeficientes y el efecto de cada variable.
  2. Árboles de decisión: permiten segmentar el espacio de variables explicativas y capturar relaciones no lineales sin necesidad de especificación funcional previa. En este trabajo se emplea específicamente un árbol de regresión (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.

DESAROLLO DEL TRABAJO PRÁCTICO, ANÁLISIS Y EVALUACIÓN DE LOS RESULTADOS OBTENIDOS

1. Limpieza de datos previa a su inclusión en Rstudio

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:

1.1. Filas de encabezado y texto auxiliar

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.

1.2. Columnas vacías o sin nombre útil

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.

1.3. Formato de los valores numéricos

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.

1.4. Valores no disponibles

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.

1.5. Períodos sin datos para ciertos distritos

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.

1.6. Consistencia de nombres de distritos

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.

1.7. Estructura de la tabla

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.

1.8. Lo que no se limpió

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:

  • Eliminar encabezados, subtítulos y notas.
  • Convertir .. y - en NA.
  • Cambiar los precios a formato numérico.
  • Corregir los nombres de distritos.
  • Reestructurar los datos a formato largo.
  • Filtrar los periodos sin información distrital para comparar distritos posteriormente.
  • Comprobar valores perdidos y decidir si se imputan o se excluyen.

1.9. Fuente final

[1] DATOS_R_LIMPIOS_serie.xlsx

2. Importación y verificación de los datos

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

3. ANÁLISIS EXPLORATORIO DE LOS DATOS

(Corresponde a 02_exploratorio.R)

3.1. Distribución general del precio

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

3.2. Estadísticos descriptivos por distrito

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

3.3. Evolución temporal del precio

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.

3.4. Comparación entre distritos

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.

3.5. Valores atípicos (regla del IQR)

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.

3.5.1. El caso de Puente de Vallecas en 2020

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

4. MODELO DE REGRESIÓN LINEAL MÚLTIPLE

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

4.1. Extensión interacción distrito/año

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.

5. ÁRBOL DE DECISIÓN

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

6. COMPARACIÓN DE LOS MODELOS UTILIZADOS

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

7. PREDICCIÓN PARA EL AÑO 2027 ILUSTRATIVO DEL OBJETIVO DEL TRABAJO PRÁCTICO

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

8. CONCLUSIÓN

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:

8.1. Jerarquía territorial y estructura del modelo

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.

8.2. Rendimiento predictivo y no linealidad

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.

8.3. Patrón de residuos y heterocedasticidad

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

8.4. Extrapolación fuera del soporte de los datos

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

8.5. Limitaciones de los datos y validez externa

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.

8.6. Implicaciones metodológicas

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.

8.7. Líneas de ampliación

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.

9. BIBLIOGRAFÍA

La documentación utilizada versa principalmente en la documentación que el propio departamento de la asignatura nos ha entregado:

10. INFORMACIÓN DE LA SESIÓN Y CÓDIGO EMPLEADO

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