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:
2026-07-15 11:42:45 +02:00
parent 2692fc6d71
commit a237ad48f1
16 changed files with 30304 additions and 1379 deletions
+22 -2
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+3240
View File
File diff suppressed because it is too large Load Diff
+25386
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -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."
}
+57
View File
@@ -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
View File
@@ -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")
+8 -1
View File
@@ -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"
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+111 -9
View File
@@ -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 })
+12
View File
@@ -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": {
+12
View File
@@ -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": {