RAVE SCOUT: análisis de aislamiento de España y visor de localizaciones

Barrido de los 498.528 km² de la península y Baleares sobre malla de 100 m
(EPSG:3035) para localizar sitios alejados de todo con acceso rodado.

- 12,5 M de edificios y 1,4 M de km de viales de OSM, incluidos los países
  vecinos para no inflar el aislamiento en la franja fronteriza
- Espacios protegidos oficiales (Red Natura 2000 + designación nacional) vía
  la Agencia Europea de Medio Ambiente: cubren el 29 % del país frente al 15 %
  que tenía OSM
- Modelo acústico con propagación real; es el filtro que de verdad corta, y
  deja 505 km² en toda España
- 355 localizaciones valoradas de 1 a 5 estrellas en seis criterios
- Visor autónomo con relieve, ortofoto PNOA por localización, relieve 3D y
  mapa deslizante con satélite, catastro y espacios protegidos
This commit is contained in:
sito 2026-08-08 20:29:27 +02:00
commit d4b3b803e0
394 changed files with 7393 additions and 0 deletions

74
src/validate_coverage.py Normal file
View file

@ -0,0 +1,74 @@
"""Control de sesgo: ¿está OSM igual de mapeado en toda España?
Importa porque el modelo mide aislamiento como "distancia al edificio más
cercano". Si una comunidad tiene los edificios a medio mapear, sus celdas
saldrán aisladas por un hueco en los datos y no por estar lejos de nada.
El import del Catastro cubre casi toda España pero no Navarra ni Euskadi, que
tienen catastro propio, así que el sesgo es esperable y hay que cuantificarlo.
Como no usamos datos externos de población, el contraste es interno: viales por
km2 (que en España están mapeados de forma homogénea) frente a edificios por
km2. Si la proporción se desploma en una región, es un agujero de datos.
"""
import pickle
import sys
import numpy as np
import rasterio.features
from shapely.geometry import mapping
sys.path.insert(0, str(__file__.rsplit("/", 1)[0]))
import config as C
STEP_M = 60.0 # espaciado de densificación usado en extract_osm
def main():
with open(C.INTERIM / "area_admin_region.pkl", "rb") as fh:
regs = [r for r in pickle.load(fh) if r["geom"].area > 1e6]
regs.sort(key=lambda r: -r["geom"].area)
rid = rasterio.features.rasterize(
[(mapping(r["geom"]), i + 1) for i, r in enumerate(regs)],
out_shape=(C.HEIGHT, C.WIDTH), transform=C.TRANSFORM,
fill=0, dtype=np.uint8, all_touched=False)
def counts_for(base):
a = np.load(C.INTERIM / f"{base}.npy")
r, c = C.xy_to_rowcol(a[0], a[1])
ok = C.inside(r, c)
ids = rid[r[ok], c[ok]]
return np.bincount(ids, minlength=len(regs) + 1)
b = counts_for("buildings")
v = counts_for("line_minor") + counts_for("line_track")
cells = np.bincount(rid.ravel(), minlength=len(regs) + 1)
print(f"{'comunidad':24s} {'km2':>8s} {'edif':>10s} {'edif/km2':>9s} "
f"{'vial km':>9s} {'edif/vial-km':>13s}")
print("-" * 78)
ratios = []
for i, r in enumerate(regs, start=1):
km2 = cells[i] * 0.01
if km2 < 100:
continue
vial_km = v[i] * STEP_M / 1000.0
ratio = b[i] / vial_km if vial_km else 0
ratios.append((ratio, r["name"]))
print(f"{r['name'][:24]:24s} {km2:8,.0f} {b[i]:10,} {b[i]/km2:9.1f} "
f"{vial_km:9,.0f} {ratio:13.2f}")
med = np.median([x for x, _ in ratios])
print("-" * 78)
print(f"mediana edif/vial-km: {med:.2f}")
print("\nregiones con menos de la mitad de la mediana "
"(sospecha de infra-mapeo de edificios):")
flagged = [n for x, n in ratios if x < 0.5 * med]
for n in flagged:
print(f" - {n}")
if not flagged:
print(" ninguna")
if __name__ == "__main__":
main()