La gentrificación es contagiosa. Cuando un barrio se pone de moda, el de al
lado sube justo después: los que no pueden pagar el primero se mudan al vecino, y
el efecto se propaga por el mapa como una mancha de aceite. Un modelo que puntúe
cada barrio de forma aislada nunca capturará esto. La solución tiene dos piezas:
un clasificador fuerte y una propagación sobre el grafo de vecindad.
Pieza 1: XGBoost de 3 clases
El clasificador base es un XGBoost (600 árboles,
max_depth=6, learning_rate=0.04) que predice la
probabilidad de que un barrio tenga revalorización Baja / Media /
Alta a partir de 18 features socioeconómicas: precio/m²,
tendencia a 1 y 3 años, inversión en infraestructura, accesibilidad de transporte,
licencias de obra nueva y de rehabilitación, renta media, densidad, vacancia
comercial, edad de los edificios, etc.
En lugar de quedarme con la clase más probable, convierto las tres
probabilidades en un score continuo 0-100, que se propaga mucho
mejor que una etiqueta discreta:
proba = model.predict_proba(X) # [P(baja), P(media), P(alta)] score = (proba[:, 2] * 1.0 + proba[:, 1] * 0.5) * 100 # 0-100 Pieza 2: message-passing sobre el grafo de barrios
Aquí es donde entra el "efecto contagio". Construyo un grafo donde cada barrio
se conecta con sus vecinos geográficos y aplico 2 rondas de
message-passing: cada barrio mezcla su propio score con la media de sus
vecinos. Es el mismo principio que una Graph Neural Network, con una diferencia
que conviene declarar con honestidad: los pesos son fijos, no
aprendidos. Es una difusión/suavizado sobre el grafo, no una GNN
entrenada con backpropagation:
def message_passing(score, neighbors): # Ronda 1: 65% score propio + 35% media de vecinos s1 = {i: 0.65 * score[i] + 0.35 * mean(score[j] for j in neighbors[i]) for i in score} # Ronda 2: 72% + 28% (converge, suaviza sin aplanar) s2 = {i: 0.72 * s1[i] + 0.28 * mean(s1[j] for j in neighbors[i]) for i in s1} return s2 El resultado: un barrio con score medio pero rodeado de barrios
al alza sube; uno alto pero aislado se modera. Justo la dinámica espacial que un
modelo tabular puro no ve.
Datos: sintéticos, pero calibrados (y lo digo)
Punto de honestidad importante: el dataset son 4.000 barrios
sintéticos, no un registro real de transacciones. Ahora bien, no son
números al azar: están calibrados con el Índice de Precios de Vivienda del
INE (2015-2024) para que las distribuciones y correlaciones se parezcan a
las de España. Sirve para demostrar la arquitectura y el razonamiento; no para
decidir dónde comprar tu casa.
Resultados honestos
MétricaValor
F1-score (macro)**0.61** MCC0.41 CV F1 (5-fold)0.59 ± 0.02 Con 3 clases equilibradas, el azar da un F1 de 0.33; el modelo llega a 0.61, un
28% por encima del azar. No es magia —predecir revalorización es
genuinamente difícil— pero la varianza baja en validación cruzada (±0.02) dice que
generaliza y no está sobreajustado. Prefiero un 0.61 honesto y estable que un 0.9
sospechoso.
Qué mejoraría
Lo siguiente sería sustituir el message-passing de pesos fijos por una
GNN de verdad (GraphSAGE o GAT) que aprenda cuánto pesa cada
vecino según sus features, y alimentarla con datos reales de portales
inmobiliarios en lugar de sintéticos.
Publicado originalmente en adrianmoreno-dev.com.