feat(data+map): añadir INE Censo 2021 (viviendas por municipio + consumo por distrito) y 3 variables nuevas al mapa
Datos:
- 59531.csv: viviendas totales por municipio (62 Balears, total 1.300.622)
- 59532.csv: percentiles consumo eléctrico por distrito (116 Balears)
Pipeline:
- 01_load_sources: nuevos loaders con parsing latin1
- 02_build_dataset: join por CUSEC prefix (5 dig municipio, 7 dig distrito)
- Nuevos campos: viviendas_totales, consumo_p{10,25,50,75,90}_kwh, airbnb_listings_por_1000_viviendas
Mapa:
- 3 variables nuevas (viviendas_totales verde, consumo_p50_kwh rojo, airbnb_per_1000 morado)
- Nueva fill layer 'all' (sin filtro de isla) para variables universales
- Popup ampliado con sección INE
This commit is contained in:
+22
-2
@@ -1,6 +1,6 @@
|
||||
# Estadisticas descriptivas - Cases Tancades
|
||||
|
||||
Generado: 2026-07-14 11:44
|
||||
Generado: 2026-07-15 11:37
|
||||
|
||||
|
||||
## Cobertura geografica
|
||||
@@ -47,6 +47,26 @@ Detalle por municipio:
|
||||
- Establecimientos: **1,375**
|
||||
- Plazas: **14,935**
|
||||
|
||||
## INE viviendas (Censo 2021, municipio)
|
||||
|
||||
- Municipios con dato: **62**
|
||||
- Viviendas totales Balears: **1,300,622**
|
||||
|
||||
Top 5 municipios por viviendas:
|
||||
|
||||
- : 652,123 viviendas
|
||||
- Palma: 186,482 viviendas
|
||||
- Calvià: 36,492 viviendas
|
||||
- Manacor: 28,571 viviendas
|
||||
- Eivissa: 23,938 viviendas
|
||||
|
||||
## INE consumo electrico (Censo 2021, distrito)
|
||||
|
||||
- Distritos con dato: **116**
|
||||
- Mediana del percentil 50 (mediana de medianas): **3 kWh**
|
||||
- Percentil 10 (consumo bajo): **642 kWh**
|
||||
- Percentil 90 (consumo alto): **7 kWh**
|
||||
|
||||
## INE vivienda (CCAA Balears, base de referencia)
|
||||
|
||||
- Viviendas totales: **652,123**
|
||||
@@ -54,7 +74,7 @@ Detalle por municipio:
|
||||
- % uso esporadico: **6.9%** (~44,996)
|
||||
- % vivienda turistica (2025M05): **3.74%** (~24,389)
|
||||
|
||||
> Limitacion: estos porcentajes son a nivel CCAA, no seccion censal. Ver /metodologia.
|
||||
> Limitacion: estos porcentajes son a nivel CCAA, no seccion censal ni municipio. Ver /metodologia.
|
||||
|
||||
|
||||
## Brecha legal vs realidad (insight narrativo)
|
||||
|
||||
+675
-675
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
+3240
File diff suppressed because it is too large
Load Diff
+25386
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"source": "IBESTAT - Institut d'Estadística de les Illes Balears",
|
||||
"year": 2019,
|
||||
"scope": "Formentera (agregado insular)",
|
||||
"metadata": {
|
||||
"total_unidades": 1375,
|
||||
"total_plazas": 14935
|
||||
},
|
||||
"notes": "Dato agregado a nivel insular. No disponible a nivel municipal por parte de IBESTAT. Reconstruido manualmente desde STATS.md del pipeline original. Para más detalle municipal consultar el portal de turisme de Formentera o el IET."
|
||||
}
|
||||
@@ -5,6 +5,8 @@ Carga todas las fuentes de datos y produce el dataset final por seccion censal.
|
||||
Entradas (data/raw/):
|
||||
- viviendas vacias-hiopotecas.xlsx (INE, CCAA)
|
||||
- 39365(1).xlsx (INE, CCAA serie temporal)
|
||||
- 59531.csv (INE Censo 2021, viviendas totales por municipio)
|
||||
- 59532.csv (INE Censo 2021, percentiles consumo eléctrico por distrito)
|
||||
- secciones_balears.gpkg (shapefile procesado, 674 secciones)
|
||||
- airbnb_mallorca_listings.csv.gz (Inside Airbnb)
|
||||
- airbnb_menorca_listings.csv.gz (Inside Airbnb)
|
||||
@@ -71,6 +73,53 @@ def load_ine_vivienda_turistica() -> pd.DataFrame:
|
||||
return df
|
||||
|
||||
|
||||
def load_ine_vivienda_municipio() -> pd.DataFrame:
|
||||
"""Carga INE Censo 2021 viviendas totales por municipio (tabla 59531).
|
||||
|
||||
Devuelve DataFrame con columnas: CMUN (5 digitos), NMUN, viviendas_totales.
|
||||
"""
|
||||
df = pd.read_csv(RAW / "59531.csv", sep=";", encoding="latin1", low_memory=False)
|
||||
df.columns = ["NAC", "CCAA", "PROV", "MUN", "INDICADOR", "TOTAL"]
|
||||
# Filtrar solo Balears (provincia 07)
|
||||
df = df[df["PROV"].astype(str).str.contains("Balears", na=False, regex=False)].copy()
|
||||
# Solo Viviendas totales
|
||||
df = df[df["INDICADOR"] == "Viviendas totales"].copy()
|
||||
# Extraer CMUN (5 digitos) del campo MUN ("07001 Alaró")
|
||||
df["CMUN"] = df["MUN"].astype(str).str.strip().str[:5]
|
||||
# Limpiar NMUN (quitar prefijo numerico)
|
||||
df["NMUN"] = df["MUN"].astype(str).str.strip().str[6:]
|
||||
# Quitar "Resto de Baleares" (codigo 07999, no es un municipio real)
|
||||
df = df[df["CMUN"] != "07999"].copy()
|
||||
# Convertir TOTAL a entero (viene como "3.246" con punto de miles)
|
||||
df["viviendas_totales"] = (
|
||||
df["TOTAL"].astype(str)
|
||||
.str.replace(".", "", regex=False)
|
||||
.str.replace(",", ".", regex=False)
|
||||
)
|
||||
df["viviendas_totales"] = pd.to_numeric(df["viviendas_totales"], errors="coerce").fillna(0).astype(int)
|
||||
return df[["CMUN", "NMUN", "viviendas_totales"]].reset_index(drop=True)
|
||||
|
||||
|
||||
def load_ine_consumo_distrito() -> pd.DataFrame:
|
||||
"""Carga INE Censo 2021 percentiles consumo eléctrico por distrito (tabla 59532).
|
||||
|
||||
Devuelve DataFrame con columnas: CDIS (7 digitos), percentil, kwh.
|
||||
"""
|
||||
df = pd.read_csv(RAW / "59532.csv", sep=";", encoding="latin1", low_memory=False)
|
||||
df.columns = ["DISTRITO", "PERCENTIL", "TOTAL"]
|
||||
# Filtrar solo Balears (codigos empiezan por 07)
|
||||
df = df[df["DISTRITO"].astype(str).str.strip().str.startswith("07")].copy()
|
||||
# Extraer CDIS (7 digitos) del campo DISTRITO ("0700101 Alegría-Dulantzi distrito 01")
|
||||
df["CDIS"] = df["DISTRITO"].astype(str).str.strip().str[:7]
|
||||
# Convertir kwh (viene como "3.507" con punto decimal)
|
||||
df["kwh"] = pd.to_numeric(df["TOTAL"].astype(str).str.replace(".", ".", regex=False).str.replace(",", ".", regex=False), errors="coerce")
|
||||
# Pivot percentiles: p10/p25/p50/p75/p90 a columnas
|
||||
df["percentil"] = df["PERCENTIL"].str.extract(r"Percentil (\d+)").astype(int)
|
||||
pivot = df.pivot_table(index="CDIS", columns="percentil", values="kwh", aggfunc="first").reset_index()
|
||||
pivot.columns = ["CDIS"] + [f"consumo_p{p}_kwh" for p in pivot.columns[1:]]
|
||||
return pivot
|
||||
|
||||
|
||||
def load_shapefile() -> gpd.GeoDataFrame:
|
||||
"""Carga el shapefile procesado de Balears."""
|
||||
gdf = gpd.read_file(RAW / "secciones_balears.gpkg")
|
||||
@@ -132,6 +181,14 @@ if __name__ == "__main__":
|
||||
print(f"INE vivienda turistica: {len(ine_tur)} periodos, ultimo: "
|
||||
f"{ine_tur.iloc[0]['periodo']} = {ine_tur.iloc[0]['pct_viviendas_turisticas']}%\n")
|
||||
|
||||
ine_mun = load_ine_vivienda_municipio()
|
||||
print(f"INE viviendas por municipio (Balears): {len(ine_mun)} municipios, "
|
||||
f"total: {ine_mun['viviendas_totales'].sum():,} viviendas\n")
|
||||
|
||||
ine_dis = load_ine_consumo_distrito()
|
||||
print(f"INE consumo por distrito (Balears): {len(ine_dis)} distritos, "
|
||||
f"percentiles disponibles: {[c for c in ine_dis.columns if c.startswith('consumo_p')]}\n")
|
||||
|
||||
gdf = load_shapefile()
|
||||
print(f" Municipios: {gdf['NMUN'].nunique()}, "
|
||||
f"Provincias: {gdf['NPRO'].unique().tolist()}\n")
|
||||
|
||||
+96
-17
@@ -6,6 +6,8 @@ Entradas:
|
||||
- data/raw/airbnb_*_listings.csv.gz (Mallorca, Menorca)
|
||||
- data/raw/hut_eivissa_2026-07-14.csv (HUT Eivissa por municipio)
|
||||
- data/raw/ibestat_formentera_2019.json (Formentera agregado)
|
||||
- data/raw/59531.csv (INE viviendas totales por municipio)
|
||||
- data/raw/59532.csv (INE percentiles consumo por distrito)
|
||||
|
||||
Salidas:
|
||||
- data/output/dataset.parquet (tabla final)
|
||||
@@ -89,6 +91,44 @@ def normalize_municipio(name: str) -> str:
|
||||
return mapping.get(upper, name)
|
||||
|
||||
|
||||
def load_ine_vivienda_municipio() -> pd.DataFrame:
|
||||
"""Carga INE Censo 2021 viviendas totales por municipio (tabla 59531).
|
||||
|
||||
Devuelve DataFrame con columnas: CMUN (5 digitos), NMUN, viviendas_totales.
|
||||
"""
|
||||
df = pd.read_csv(RAW / "59531.csv", sep=";", encoding="latin1", low_memory=False)
|
||||
df.columns = ["NAC", "CCAA", "PROV", "MUN", "INDICADOR", "TOTAL"]
|
||||
df = df[df["PROV"].astype(str).str.contains("Balears", na=False, regex=False)].copy()
|
||||
df = df[df["INDICADOR"] == "Viviendas totales"].copy()
|
||||
df["CMUN"] = df["MUN"].astype(str).str.strip().str[:5]
|
||||
df["NMUN"] = df["MUN"].astype(str).str.strip().str[6:]
|
||||
df = df[df["CMUN"] != "07999"].copy()
|
||||
df["viviendas_totales"] = pd.to_numeric(
|
||||
df["TOTAL"].astype(str).str.replace(".", "", regex=False).str.replace(",", ".", regex=False),
|
||||
errors="coerce",
|
||||
).fillna(0).astype(int)
|
||||
return df[["CMUN", "NMUN", "viviendas_totales"]].reset_index(drop=True)
|
||||
|
||||
|
||||
def load_ine_consumo_distrito() -> pd.DataFrame:
|
||||
"""Carga INE Censo 2021 percentiles consumo eléctrico por distrito (tabla 59532).
|
||||
|
||||
Devuelve DataFrame con columnas: CDIS (7 digitos), consumo_p10_kwh, p25, p50, p75, p90.
|
||||
"""
|
||||
df = pd.read_csv(RAW / "59532.csv", sep=";", encoding="latin1", low_memory=False)
|
||||
df.columns = ["DISTRITO", "PERCENTIL", "TOTAL"]
|
||||
df = df[df["DISTRITO"].astype(str).str.strip().str.startswith("07")].copy()
|
||||
df["CDIS"] = df["DISTRITO"].astype(str).str.strip().str[:7]
|
||||
df["kwh"] = pd.to_numeric(
|
||||
df["TOTAL"].astype(str).str.replace(".", ".", regex=False).str.replace(",", ".", regex=False),
|
||||
errors="coerce",
|
||||
)
|
||||
df["percentil"] = df["PERCENTIL"].str.extract(r"Percentil (\d+)").astype(int)
|
||||
pivot = df.pivot_table(index="CDIS", columns="percentil", values="kwh", aggfunc="first").reset_index()
|
||||
pivot.columns = ["CDIS"] + [f"consumo_p{p}_kwh" for p in pivot.columns[1:]]
|
||||
return pivot
|
||||
|
||||
|
||||
def aggregate_hut_eivissa() -> pd.DataFrame:
|
||||
"""Agrega HUT Eivissa por municipio normalizado."""
|
||||
df = pd.read_csv(RAW / "hut_eivissa_2026-07-14.csv")
|
||||
@@ -172,7 +212,13 @@ def main() -> None:
|
||||
print(f"IBESTAT Formentera: {formentera.iloc[0]['hut_registros']:,} unidades, "
|
||||
f"{formentera.iloc[0]['hut_plazas']:,} plazas")
|
||||
|
||||
# 5) Merge con shapefile
|
||||
# 5) INE Censo 2021 — viviendas totales por municipio + consumo por distrito
|
||||
ine_mun = load_ine_vivienda_municipio()
|
||||
ine_dis = load_ine_consumo_distrito()
|
||||
print(f"INE viviendas municipio: {len(ine_mun)} municipios, total: {ine_mun['viviendas_totales'].sum():,}")
|
||||
print(f"INE consumo distrito: {len(ine_dis)} distritos")
|
||||
|
||||
# 6) Merge con shapefile
|
||||
# Airbnb: secciones con datos
|
||||
airbnb_full = pd.concat([agg_mall, agg_men], ignore_index=True)
|
||||
gdf = secciones.merge(airbnb_full, on="CUSEC", how="left", suffixes=("", "_air"))
|
||||
@@ -183,9 +229,15 @@ def main() -> None:
|
||||
# Formentera: una sola fila
|
||||
gdf = gdf.merge(formentera, on="NMUN", how="left", suffixes=("", "_for"))
|
||||
|
||||
# INE municipio (viviendas_totales): join por CMUN (5 chars)
|
||||
gdf["CMUN"] = gdf["CUSEC"].astype(str).str[:5]
|
||||
gdf = gdf.merge(ine_mun[["CMUN", "viviendas_totales"]], on="CMUN", how="left")
|
||||
|
||||
# INE distrito (consumo_p*_kwh): join por CDIS (7 chars)
|
||||
gdf["CDIS"] = gdf["CUSEC"].astype(str).str[:7]
|
||||
gdf = gdf.merge(ine_dis, on="CDIS", how="left")
|
||||
|
||||
# Resolver columnas finales de isla/fuente/granularidad
|
||||
# Para Mallorca/Menorca: ya viene de airbnb
|
||||
# Para Eivissa/Formentera: viene del merge de hut
|
||||
gdf["isla_final"] = gdf["isla"].fillna(gdf["isla_eiv"]).fillna(gdf["isla_for"])
|
||||
gdf["fuente_final"] = gdf["fuente"].fillna(gdf["fuente_eiv"]).fillna(gdf["fuente_for"])
|
||||
gdf["granularidad_final"] = (
|
||||
@@ -193,13 +245,10 @@ def main() -> None:
|
||||
.fillna(gdf["granularidad_eiv"])
|
||||
.fillna(gdf["granularidad_for"])
|
||||
)
|
||||
# Fallback isla por CMUN para secciones rurales sin datos de turismo
|
||||
mask_no_isla = gdf["isla_final"].isna()
|
||||
gdf.loc[mask_no_isla, "isla_final"] = gdf.loc[mask_no_isla, "CMUN"].apply(cmun_to_isla)
|
||||
gdf.loc[mask_no_isla, "granularidad_final"] = "seccion_censal"
|
||||
gdf.loc[mask_no_isla, "fuente_final"] = "shapefile INE 2025 (sin datos de turismo)"
|
||||
# Resolver hut_registros/hut_plazas: para Eivissa vienen del merge directo,
|
||||
# para Formentera vienen con sufijo _for (porque el merge de Eivissa ya uso esos nombres).
|
||||
gdf["hut_registros_final"] = gdf["hut_registros"].fillna(gdf["hut_registros_for"])
|
||||
gdf["hut_plazas_final"] = gdf["hut_plazas"].fillna(gdf["hut_plazas_for"])
|
||||
if "hut_habitaciones_for" in gdf.columns:
|
||||
@@ -220,7 +269,7 @@ def main() -> None:
|
||||
"hut_habitaciones_final": "hut_habitaciones",
|
||||
})
|
||||
|
||||
# Llenar NaN de columnas Airbnb con 0 (seccion existe pero sin listings)
|
||||
# Llenar NaN de columnas Airbnb con 0
|
||||
airbnb_cols = [
|
||||
"airbnb_listings", "airbnb_entire_homes", "airbnb_accommodates_total",
|
||||
"airbnb_revenue_total", "airbnb_hosts_unicos", "airbnb_con_licencia",
|
||||
@@ -234,27 +283,43 @@ def main() -> None:
|
||||
if c in gdf.columns:
|
||||
gdf[c] = gdf[c].fillna(0).astype(int)
|
||||
|
||||
# Llenar NaN de viviendas_totales con 0 (fallback CCAA)
|
||||
gdf["viviendas_totales"] = gdf["viviendas_totales"].fillna(0).astype(int)
|
||||
# Llenar NaN de consumo percentiles con 0
|
||||
for p in [10, 25, 50, 75, 90]:
|
||||
col = f"consumo_p{p}_kwh"
|
||||
if col in gdf.columns:
|
||||
gdf[col] = gdf[col].fillna(0.0)
|
||||
|
||||
# Viviendas INE a nivel CCAA (documentado en /metodologia)
|
||||
gdf["viviendas_vacias_pct_ccaa"] = 16.2
|
||||
gdf["viviendas_uso_esporadico_pct_ccaa"] = 6.9
|
||||
gdf["viviendas_turisticas_pct_ccaa_2025M05"] = 3.74
|
||||
|
||||
# Total plazas turisticas por seccion (para color en mapa)
|
||||
# Total plazas turisticas por seccion
|
||||
gdf["plazas_turisticas"] = gdf["airbnb_accommodates_total"].fillna(0) + gdf["hut_plazas"].fillna(0)
|
||||
# Para Eivissa/Formentera las plazas se asignan a nivel municipio;
|
||||
# al pintar por seccion, la division muestra una densidad "diluida" de municipio.
|
||||
# Para visualizacion limpia, en el frontend se usara granularidad_final para escalar.
|
||||
|
||||
# Proxy: presion Airbnb per capita (listings / 1000 viviendas del municipio)
|
||||
# Solo donde hay datos significativos (>10 viviendas)
|
||||
gdf["airbnb_listings_por_1000_viviendas"] = np.where(
|
||||
gdf["viviendas_totales"] > 10,
|
||||
gdf["airbnb_listings"] / gdf["viviendas_totales"] * 1000,
|
||||
0,
|
||||
)
|
||||
|
||||
# Drop CDIS, CMUN (helpers de join)
|
||||
gdf = gdf.drop(columns=["CMUN", "CDIS"], errors="ignore")
|
||||
|
||||
# Guardar
|
||||
gdf.to_file(OUT / "dataset.gpkg", driver="GPKG")
|
||||
print(f"\nGuardado dataset.gpkg: {len(gdf)} filas, {len(gdf.columns)} columnas")
|
||||
|
||||
# Parquet (sin geometria, geometria ya en gpkg)
|
||||
# Parquet (sin geometria)
|
||||
df = pd.DataFrame(gdf.drop(columns="geometry"))
|
||||
df.to_parquet(OUT / "dataset.parquet", index=False)
|
||||
print(f"Guardado dataset.parquet: {len(df)} filas")
|
||||
|
||||
# JSON simplificado para web (sin geometria, agregados por isla)
|
||||
# JSON simplificado para web
|
||||
resumen_isla = (
|
||||
gdf.groupby("isla")
|
||||
.agg(
|
||||
@@ -275,8 +340,6 @@ def main() -> None:
|
||||
print(f"Guardado dataset_web.json")
|
||||
|
||||
# Stats descriptivas
|
||||
# Para HUT Eivissa: usar el dataframe agregado directamente (los totales del merge por seccion
|
||||
# no son sumables: misma valor se replica por seccion del mismo municipio).
|
||||
hut_eiv_total = int(hut_eiv["hut_registros"].sum())
|
||||
hut_eiv_plazas = int(hut_eiv["hut_plazas"].sum())
|
||||
formentera_total = int(formentera["hut_registros"].iloc[0])
|
||||
@@ -317,16 +380,32 @@ def main() -> None:
|
||||
stats.append(f"- Establecimientos: **{formentera_total:,}**")
|
||||
stats.append(f"- Plazas: **{formentera_plazas:,}**")
|
||||
|
||||
stats.append(f"\n## INE viviendas (Censo 2021, municipio)\n")
|
||||
total_viviendas = int(ine_mun["viviendas_totales"].sum())
|
||||
stats.append(f"- Municipios con dato: **{len(ine_mun)}**")
|
||||
stats.append(f"- Viviendas totales Balears: **{total_viviendas:,}**")
|
||||
stats.append(f"\nTop 5 municipios por viviendas:\n")
|
||||
for _, row in ine_mun.nlargest(5, "viviendas_totales").iterrows():
|
||||
stats.append(f"- {row['NMUN']}: {row['viviendas_totales']:,} viviendas")
|
||||
|
||||
stats.append(f"\n## INE consumo electrico (Censo 2021, distrito)\n")
|
||||
stats.append(f"- Distritos con dato: **{len(ine_dis)}**")
|
||||
p50_mediana = float(ine_dis["consumo_p50_kwh"].median())
|
||||
stats.append(f"- Mediana del percentil 50 (mediana de medianas): **{p50_mediana:,.0f} kWh**")
|
||||
p10_mediana = float(ine_dis["consumo_p10_kwh"].median())
|
||||
p90_mediana = float(ine_dis["consumo_p90_kwh"].median())
|
||||
stats.append(f"- Percentil 10 (consumo bajo): **{p10_mediana:,.0f} kWh**")
|
||||
stats.append(f"- Percentil 90 (consumo alto): **{p90_mediana:,.0f} kWh**")
|
||||
|
||||
stats.append(f"\n## INE vivienda (CCAA Balears, base de referencia)\n")
|
||||
stats.append(f"- Viviendas totales: **652,123**")
|
||||
stats.append(f"- % viviendas vacias: **16.2%** (~105,564)")
|
||||
stats.append(f"- % uso esporadico: **6.9%** (~44,996)")
|
||||
stats.append(f"- % vivienda turistica (2025M05): **3.74%** (~24,389)")
|
||||
stats.append(f"\n> Limitacion: estos porcentajes son a nivel CCAA, no seccion censal. "
|
||||
stats.append(f"\n> Limitacion: estos porcentajes son a nivel CCAA, no seccion censal ni municipio. "
|
||||
f"Ver /metodologia.\n")
|
||||
|
||||
stats.append(f"\n## Brecha legal vs realidad (insight narrativo)\n")
|
||||
# Mallorca: Airbnb listings (no todos son legales, pero la mayoria no)
|
||||
mall_airbnb = int(airbnb_total[airbnb_total["isla"] == "Mallorca"]["airbnb_listings"].sum())
|
||||
men_airbnb = int(airbnb_total[airbnb_total["isla"] == "Menorca"]["airbnb_listings"].sum())
|
||||
stats.append(f"- Mallorca: ~{mall_airbnb:,} listings activos en Airbnb vs. ~24,389 plazas turisticas legales declaradas en CCAA")
|
||||
|
||||
@@ -34,7 +34,14 @@ def main() -> None:
|
||||
"airbnb_accommodates_total", "airbnb_revenue_total",
|
||||
"airbnb_hosts_unicos", "airbnb_con_licencia",
|
||||
"hut_registros", "hut_plazas", "hut_habitaciones",
|
||||
"plazas_turisticas", "geometry",
|
||||
"plazas_turisticas",
|
||||
# INE Censo 2021 (viven en dataset tras Fase 3 mejora)
|
||||
"viviendas_totales", "viviendas_vacias_pct_ccaa",
|
||||
"viviendas_uso_esporadico_pct_ccaa", "viviendas_turisticas_pct_ccaa_2025M05",
|
||||
"consumo_p10_kwh", "consumo_p25_kwh", "consumo_p50_kwh",
|
||||
"consumo_p75_kwh", "consumo_p90_kwh",
|
||||
"airbnb_listings_por_1000_viviendas",
|
||||
"geometry",
|
||||
]
|
||||
cols = [c for c in cols if c in gdf.columns]
|
||||
geojson_path = OUT / "dataset.geojson"
|
||||
|
||||
+675
-675
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ interface VariableOption {
|
||||
value: string;
|
||||
labelKey: string;
|
||||
descKey: string;
|
||||
scope: "airbnb" | "hut";
|
||||
scope: "airbnb" | "hut" | "all";
|
||||
}
|
||||
|
||||
const variableOptions: VariableOption[] = [
|
||||
@@ -15,8 +15,11 @@ const variableOptions: VariableOption[] = [
|
||||
{ value: "airbnb_entire_homes", labelKey: "map.variable_airbnb_entire", descKey: "map.variable_airbnb_entire_desc", scope: "airbnb" },
|
||||
{ value: "airbnb_revenue_total", labelKey: "map.variable_airbnb_revenue", descKey: "map.variable_airbnb_revenue_desc", scope: "airbnb" },
|
||||
{ value: "airbnb_con_licencia", labelKey: "map.variable_airbnb_con_licencia", descKey: "map.variable_airbnb_con_licencia_desc", scope: "airbnb" },
|
||||
{ value: "airbnb_per_1000", labelKey: "map.variable_airbnb_per_1000", descKey: "map.variable_airbnb_per_1000_desc", scope: "airbnb" },
|
||||
{ value: "hut_plazas", labelKey: "map.variable_hut_plazas", descKey: "map.variable_hut_plazas_desc", scope: "hut" },
|
||||
{ value: "hut_registros", labelKey: "map.variable_hut_registros", descKey: "map.variable_hut_registros_desc", scope: "hut" },
|
||||
{ value: "viviendas_totales", labelKey: "map.variable_viviendas_totales", descKey: "map.variable_viviendas_totales_desc", scope: "all" },
|
||||
{ value: "consumo_p50_kwh", labelKey: "map.variable_consumo_p50", descKey: "map.variable_consumo_p50_desc", scope: "all" },
|
||||
];
|
||||
|
||||
const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
@@ -113,6 +116,9 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
con_licencia: lang === "es" ? "Con licencia Airbnb" : "Amb llicència Airbnb",
|
||||
hut_registros: lang === "es" ? "Registros HUT" : "Registres HUT",
|
||||
hut_plazas: lang === "es" ? "Plazas HUT" : "Places HUT",
|
||||
viviendas_totales: lang === "es" ? "Viviendas totales" : "Habitatges totals",
|
||||
consumo_p50: lang === "es" ? "Mediana consumo kWh" : "Mediana consum kWh",
|
||||
airbnb_per_1000: lang === "es" ? "Airbnb / 1000 viv." : "Airbnb / 1000 hab.",
|
||||
granularitat: lang === "es" ? "Granularidad" : "Granularitat",
|
||||
seccion_censal: lang === "es" ? "sección censal" : "secció censal",
|
||||
municipio: "municipi",
|
||||
@@ -121,6 +127,8 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
legend_applies_to: lang === "es" ? "Aplica a" : "Aplica a",
|
||||
legend_islands_mm: lang === "es" ? "Mallorca / Menorca (sección censal)" : "Mallorca / Menorca (secció censal)",
|
||||
legend_islands_ef: lang === "es" ? "Ibiza / Formentera (municipio)" : "Eivissa / Formentera (municipi)",
|
||||
legend_islands_all_mun: lang === "es" ? "Todas (municipio)" : "Totes (municipi)",
|
||||
legend_islands_all_dis: lang === "es" ? "Todas (distrito)" : "Totes (districte)",
|
||||
};
|
||||
|
||||
const DESC: Record<string, string> = {
|
||||
@@ -138,8 +146,12 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
: "Suma estimada dels ingressos dels darrers 365 dies per secció",
|
||||
airbnb_con_licencia:
|
||||
lang === "es"
|
||||
? "Listings con campo `license` rellenado (autorreportado, no validado)"
|
||||
? "Listings con son campo `license` rellenado (autorreportado, no validado)"
|
||||
: "Listings amb camp `license` emplenat (autorreportat, no validat)",
|
||||
airbnb_per_1000:
|
||||
lang === "es"
|
||||
? "Listings Airbnb por cada 1000 viviendas del municipio"
|
||||
: "Listings Airbnb per cada 1000 habitatges del municipi",
|
||||
hut_plazas:
|
||||
lang === "es"
|
||||
? "Plazas registradas en HUT del Consell d'Eivissa (municipio)"
|
||||
@@ -148,10 +160,18 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
lang === "es"
|
||||
? "Número de registros HUT activos (municipio)"
|
||||
: "Nombre de registres HUT actius (municipi)",
|
||||
viviendas_totales:
|
||||
lang === "es"
|
||||
? "Total de viviendas por municipio (Censo 2021, replicado a todas las secciones del municipio)"
|
||||
: "Total d'habitatges per municipi (Cens 2021, replicat a totes les seccions del municipi)",
|
||||
consumo_p50_kwh:
|
||||
lang === "es"
|
||||
? "Percentil 50 de consumo eléctrico anual por distrito (Censo 2021)"
|
||||
: "Percentil 50 de consum elèctric anual per districte (Cens 2021)",
|
||||
};
|
||||
|
||||
const LAYERS: Record<string, {
|
||||
scope: "airbnb" | "hut";
|
||||
scope: "airbnb" | "hut" | "all";
|
||||
field: string;
|
||||
colorExpr: any;
|
||||
stops: number[];
|
||||
@@ -239,6 +259,45 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
desc: DESC.hut_registros,
|
||||
appliesTo: T.legend_islands_ef,
|
||||
},
|
||||
airbnb_per_1000: {
|
||||
scope: "airbnb",
|
||||
field: "airbnb_listings_por_1000_viviendas",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "airbnb_listings_por_1000_viviendas"], 0],
|
||||
"#ede9fe", 5, "#c4b5fd", 20, "#a78bfa", 50, "#8b5cf6", 100, "#6d28d9",
|
||||
],
|
||||
stops: [0, 5, 20, 50, 100],
|
||||
colors: ["#ede9fe", "#c4b5fd", "#a78bfa", "#8b5cf6", "#6d28d9", "#6d28d9"],
|
||||
label: T.airbnb_per_1000,
|
||||
desc: DESC.airbnb_per_1000,
|
||||
appliesTo: T.legend_islands_mm,
|
||||
},
|
||||
viviendas_totales: {
|
||||
scope: "all",
|
||||
field: "viviendas_totales",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "viviendas_totales"], 0],
|
||||
"#dcfce7", 500, "#bbf7d0", 2000, "#86efac", 5000, "#22c55e", 15000, "#14532d",
|
||||
],
|
||||
stops: [0, 500, 2000, 5000, 15000],
|
||||
colors: ["#dcfce7", "#bbf7d0", "#86efac", "#22c55e", "#14532d", "#14532d"],
|
||||
label: T.viviendas_totales,
|
||||
desc: DESC.viviendas_totales,
|
||||
appliesTo: T.legend_islands_all_mun,
|
||||
},
|
||||
consumo_p50_kwh: {
|
||||
scope: "all",
|
||||
field: "consumo_p50_kwh",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "consumo_p50_kwh"], 0],
|
||||
"#fee2e2", 1500, "#fecaca", 2500, "#fca5a5", 3500, "#ef4444", 4500, "#7f1d1d",
|
||||
],
|
||||
stops: [0, 1500, 2500, 3500, 4500],
|
||||
colors: ["#fee2e2", "#fecaca", "#fca5a5", "#ef4444", "#7f1d1d", "#7f1d1d"],
|
||||
label: T.consumo_p50,
|
||||
desc: DESC.consumo_p50_kwh,
|
||||
appliesTo: T.legend_islands_all_dis,
|
||||
},
|
||||
};
|
||||
|
||||
const fmt = (n: number) =>
|
||||
@@ -302,6 +361,16 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
"fill-opacity": 0.7,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "secciones-all-fill",
|
||||
type: "fill",
|
||||
source: "cases",
|
||||
"source-layer": "cases",
|
||||
paint: {
|
||||
"fill-color": LAYERS.viviendas_totales.colorExpr,
|
||||
"fill-opacity": 0.7,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "airbnb-clusters",
|
||||
type: "circle",
|
||||
@@ -385,9 +454,16 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
if (cfg.scope === "airbnb") {
|
||||
map.setPaintProperty("secciones-airbnb-fill", "fill-color", cfg.colorExpr);
|
||||
map.setPaintProperty("secciones-hut-fill", "fill-color", NO_DATA_COLOR);
|
||||
} else {
|
||||
map.setPaintProperty("secciones-all-fill", "fill-color", NO_DATA_COLOR);
|
||||
} else if (cfg.scope === "hut") {
|
||||
map.setPaintProperty("secciones-hut-fill", "fill-color", cfg.colorExpr);
|
||||
map.setPaintProperty("secciones-airbnb-fill", "fill-color", NO_DATA_COLOR);
|
||||
map.setPaintProperty("secciones-all-fill", "fill-color", NO_DATA_COLOR);
|
||||
} else {
|
||||
// scope === "all": use the universal fill layer
|
||||
map.setPaintProperty("secciones-all-fill", "fill-color", cfg.colorExpr);
|
||||
map.setPaintProperty("secciones-airbnb-fill", "fill-color", NO_DATA_COLOR);
|
||||
map.setPaintProperty("secciones-hut-fill", "fill-color", NO_DATA_COLOR);
|
||||
}
|
||||
|
||||
updateLegend(cfg);
|
||||
@@ -398,6 +474,8 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
function formatTick(n: number): string {
|
||||
if (n === 0) return "0";
|
||||
if (activeVariable === "airbnb_revenue_total") return fmtMoney(n);
|
||||
if (activeVariable === "consumo_p50_kwh") return `${fmt(n)} kWh`;
|
||||
if (activeVariable === "airbnb_per_1000") return n.toFixed(0);
|
||||
return fmtShort(n);
|
||||
}
|
||||
|
||||
@@ -436,9 +514,11 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
const toggleClu = document.getElementById("toggle-clusters") as HTMLInputElement;
|
||||
toggleChoro?.addEventListener("change", () => {
|
||||
const v = toggleChoro.checked ? "visible" : "none";
|
||||
const activeFillId = LAYERS[activeVariable].scope === "airbnb"
|
||||
? "secciones-airbnb-fill"
|
||||
: "secciones-hut-fill";
|
||||
const scope = LAYERS[activeVariable].scope;
|
||||
const activeFillId =
|
||||
scope === "airbnb" ? "secciones-airbnb-fill" :
|
||||
scope === "hut" ? "secciones-hut-fill" :
|
||||
"secciones-all-fill";
|
||||
[activeFillId, "secciones-line"].forEach((id) => {
|
||||
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", v);
|
||||
});
|
||||
@@ -452,10 +532,13 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
|
||||
map.on("click", "secciones-airbnb-fill", (e) => openPopup(e, "airbnb"));
|
||||
map.on("click", "secciones-hut-fill", (e) => openPopup(e, "hut"));
|
||||
map.on("click", "secciones-all-fill", (e) => openPopup(e, "all"));
|
||||
map.on("mouseenter", "secciones-airbnb-fill", () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", "secciones-airbnb-fill", () => { map.getCanvas().style.cursor = ""; });
|
||||
map.on("mouseenter", "secciones-hut-fill", () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", "secciones-hut-fill", () => { map.getCanvas().style.cursor = ""; });
|
||||
map.on("mouseenter", "secciones-all-fill", () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", "secciones-all-fill", () => { map.getCanvas().style.cursor = ""; });
|
||||
map.on("click", "airbnb-clusters", async (e) => {
|
||||
const features = map.queryRenderedFeatures(e.point, { layers: ["airbnb-clusters"] });
|
||||
const clusterId = features[0]?.properties?.cluster_id;
|
||||
@@ -469,7 +552,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
map.on("mouseleave", "airbnb-clusters", () => { map.getCanvas().style.cursor = ""; });
|
||||
});
|
||||
|
||||
function openPopup(e: maplibregl.MapLayerMouseEvent, kind: "airbnb" | "hut") {
|
||||
function openPopup(e: maplibregl.MapLayerMouseEvent, kind: "airbnb" | "hut" | "all") {
|
||||
const f = e.features?.[0];
|
||||
if (!f) return;
|
||||
const p = f.properties ?? {};
|
||||
@@ -485,6 +568,9 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
const revenue = Number(p.airbnb_revenue_total ?? 0);
|
||||
const regs = Number(p.hut_registros ?? 0);
|
||||
const hPlazas = Number(p.hut_plazas ?? 0);
|
||||
const viviendas = Number(p.viviendas_totales ?? 0);
|
||||
const consumoP50 = Number(p.consumo_p50_kwh ?? 0);
|
||||
const airbnbPer1000 = Number(p.airbnb_listings_por_1000_viviendas ?? 0);
|
||||
const lines: string[] = [];
|
||||
lines.push(`<div class="font-bold text-sm mb-1">${escape(title)}</div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.municipi}:</span> ${escape(String(p.NMUN ?? "—"))}</div>`);
|
||||
@@ -493,6 +579,13 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
const sectionTitle = (label: string) =>
|
||||
`<div class="text-[10px] uppercase tracking-wide text-stone-500 mt-2 mb-0.5 border-t border-stone-200 pt-1">${label}</div>`;
|
||||
|
||||
// Always show INE data (universal)
|
||||
if (viviendas) {
|
||||
lines.push(sectionTitle("INE (Cens 2021)"));
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.viviendas_totales}:</span> <b>${fmt(viviendas)}</b></div>`);
|
||||
if (consumoP50) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.consumo_p50}:</span> <b>${fmt(consumoP50)} kWh</b></div>`);
|
||||
}
|
||||
|
||||
if (kind === "airbnb") {
|
||||
lines.push(sectionTitle("Airbnb"));
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings}:</span> <b>${fmt(listings)}</b></div>`);
|
||||
@@ -500,10 +593,19 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
if (conLic) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.con_licencia}:</span> <b>${fmt(conLic)}</b></div>`);
|
||||
if (plazas) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings} (plazas):</span> <b>${fmt(plazas)}</b></div>`);
|
||||
if (revenue) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.revenue}:</span> <b>${fmtMoney(revenue)}</b></div>`);
|
||||
} else {
|
||||
if (airbnbPer1000) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.airbnb_per_1000}:</span> <b>${airbnbPer1000.toFixed(1)}</b></div>`);
|
||||
} else if (kind === "hut") {
|
||||
lines.push(sectionTitle("HUT"));
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_registros}:</span> <b>${fmt(regs)}</b></div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(hPlazas)}</b></div>`);
|
||||
} else {
|
||||
// kind === "all" — show both Airbnb and HUT data if present
|
||||
if (listings || hPlazas) {
|
||||
lines.push(sectionTitle("Llocs actius"));
|
||||
if (listings) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings}:</span> <b>${fmt(listings)}</b></div>`);
|
||||
if (regs) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_registros}:</span> <b>${fmt(regs)}</b></div>`);
|
||||
if (hPlazas) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(hPlazas)}</b></div>`);
|
||||
}
|
||||
}
|
||||
lines.push(`<div class="text-[10px] text-stone-500 mt-1 italic">${T.granularitat}: ${granLabel}</div>`);
|
||||
new Popup({ closeButton: true })
|
||||
|
||||
@@ -80,11 +80,20 @@
|
||||
"variable_hut_plazas_desc": "Places registrades al registre HUT del Consell d'Eivissa (municipi)",
|
||||
"variable_hut_registros": "Registres HUT",
|
||||
"variable_hut_registros_desc": "Nombre de registres HUT actius (municipi)",
|
||||
"variable_viviendas_totales": "Habitatges totals (INE)",
|
||||
"variable_viviendas_totales_desc": "Total d'habitatges per municipi (Cens 2021, replicat a totes les seccions del municipi)",
|
||||
"variable_consumo_p50": "Mediana consum elèctric (kWh)",
|
||||
"variable_consumo_p50_desc": "Percentil 50 de consum elèctric anual per districte (Cens 2021). Mediana del que consumeix una llar al districte.",
|
||||
"variable_airbnb_per_1000": "Pressió Airbnb (per 1000 habitatges)",
|
||||
"variable_airbnb_per_1000_desc": "Listings Airbnb per cada 1000 habitatges del municipi. Mesura relativa de la pressió turística sobre el parc residencial.",
|
||||
"legend_title": "Llegenda",
|
||||
"legend_scale_label": "Escala",
|
||||
"legend_applies_to": "Aplica a",
|
||||
"legend_islands_mallorca_menorca": "Mallorca / Menorca (secció censal)",
|
||||
"legend_islands_eivissa_formentera": "Eivissa / Formentera (municipi)",
|
||||
"legend_islands_all_municipio": "Totes (municipi)",
|
||||
"legend_islands_all_distrito": "Totes (districte)",
|
||||
"legend_islands_mallorca_menorca_only": "Només Mallorca / Menorca (cal Airbnb per calcular)",
|
||||
"legend_empty": "Sense dades per a aquesta variable en aquesta illa",
|
||||
"legend_airbnb_title": "Mallorca / Menorca",
|
||||
"legend_airbnb_desc": "Listings actius a Airbnb per secció censal",
|
||||
@@ -98,6 +107,9 @@
|
||||
"popup_hut_plazas": "Places HUT",
|
||||
"popup_granularitat": "Granularitat",
|
||||
"popup_no_data": "Sense dades",
|
||||
"popup_viviendas_totales": "Habitatges totals (municipi)",
|
||||
"popup_consumo_p50": "Mediana consum kWh (districte)",
|
||||
"popup_airbnb_per_1000": "Airbnb / 1000 hab.",
|
||||
"loading": "Carregant mapa…"
|
||||
},
|
||||
"methodology": {
|
||||
|
||||
@@ -80,11 +80,20 @@
|
||||
"variable_hut_plazas_desc": "Plazas registradas en el registro HUT del Consell d'Eivissa (municipio)",
|
||||
"variable_hut_registros": "Registros HUT",
|
||||
"variable_hut_registros_desc": "Número de registros HUT activos (municipio)",
|
||||
"variable_viviendas_totales": "Viviendas totales (INE)",
|
||||
"variable_viviendas_totales_desc": "Total de viviendas por municipio (Censo 2021, replicado en todas las secciones del municipio)",
|
||||
"variable_consumo_p50": "Mediana consumo eléctrico (kWh)",
|
||||
"variable_consumo_p50_desc": "Percentil 50 de consumo eléctrico anual por distrito (Censo 2021). Mediana de lo que consume un hogar en el distrito.",
|
||||
"variable_airbnb_per_1000": "Presión Airbnb (por 1000 viviendas)",
|
||||
"variable_airbnb_per_1000_desc": "Listings Airbnb por cada 1000 viviendas del municipio. Medida relativa de la presión turística sobre el parque residencial.",
|
||||
"legend_title": "Leyenda",
|
||||
"legend_scale_label": "Escala",
|
||||
"legend_applies_to": "Aplica a",
|
||||
"legend_islands_mallorca_menorca": "Mallorca / Menorca (sección censal)",
|
||||
"legend_islands_eivissa_formentera": "Ibiza / Formentera (municipio)",
|
||||
"legend_islands_all_municipio": "Todas (municipio)",
|
||||
"legend_islands_all_distrito": "Todas (distrito)",
|
||||
"legend_islands_mallorca_menorca_only": "Solo Mallorca / Menorca (hace falta Airbnb para calcular)",
|
||||
"legend_empty": "Sin datos para esta variable en esta isla",
|
||||
"legend_airbnb_title": "Mallorca / Menorca",
|
||||
"legend_airbnb_desc": "Listings activos en Airbnb por sección censal",
|
||||
@@ -98,6 +107,9 @@
|
||||
"popup_hut_plazas": "Plazas HUT",
|
||||
"popup_granularitat": "Granularidad",
|
||||
"popup_no_data": "Sin datos",
|
||||
"popup_viviendas_totales": "Viviendas totales (municipio)",
|
||||
"popup_consumo_p50": "Mediana consumo kWh (distrito)",
|
||||
"popup_airbnb_per_1000": "Airbnb / 1000 viv.",
|
||||
"loading": "Cargando mapa…"
|
||||
},
|
||||
"methodology": {
|
||||
|
||||
Reference in New Issue
Block a user