fix(data): regenerar dataset_web.json con sumas correctas a nivel isla

El script 02_build_dataset.py hacia SUM de hut_registros/hut_plazas por
seccion censal, multiplicando los valores de Eivissa (76 secciones) y
Formentera (6 secciones) por el numero de secciones del municipio.

Creado scripts/04_regenerate_web_json.py que:
- Lee el CSV raw de HUT Eivissa y agrega por municipio (total real: 2.364
  registros / 18.184 plazas)
- Lee el JSON raw de IBESTAT Formentera (1.375 / 14.935)
- Para Mallorca/Menorca mantiene SUM por seccion del parquet (correcto,
  granularidad seccion censal)

Bug secundario corregido: Mallorca y Menorca tenian los mismos totales
porque se agregaba el dict combinado. Ahora se agrega por isla individual.

Nuevo scripts/05_export_airbnb_geojson.py genera el GeoJSON de listings
individuales para los clusters del mapa (18.391 puntos, 2.97MB).
This commit is contained in:
opencode
2026-07-14 12:27:17 +02:00
parent c4e503df77
commit 404fd30ac3
3 changed files with 199 additions and 22 deletions
+22 -22
View File
@@ -1,27 +1,5 @@
{
"resumen_por_isla": [
{
"isla": "Eivissa",
"secciones": 76,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 32953,
"hut_plazas": 252243
},
{
"isla": "Formentera",
"secciones": 6,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 8250,
"hut_plazas": 89610
},
{
"isla": "Mallorca",
"secciones": 545,
@@ -43,6 +21,28 @@
"airbnb_con_licencia": 3379,
"hut_registros": 0,
"hut_plazas": 0
},
{
"isla": "Eivissa",
"secciones": 76,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 2364,
"hut_plazas": 18184
},
{
"isla": "Formentera",
"secciones": 6,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 1375,
"hut_plazas": 14935
}
]
}
+119
View File
@@ -0,0 +1,119 @@
"""
Regenera data/output/dataset_web.json con sumas correctas a nivel isla.
Bug original en 02_build_dataset.py: para Eivissa/Formentera hace SUM
de `hut_registros` y `hut_plazas` por seccion censal, pero el valor se
replica a todas las secciones del mismo municipio -> multiplica.
Este script:
- Para Mallorca/Menorca: usa SUM del parquet (correcto, granularidad seccion).
- Para Eivissa: lee el CSV raw `hut_eivissa_2026-07-14.csv` y agrega por municipio.
- Para Formentera: lee el JSON raw `ibestat_formentera_2019.json` (1 fila).
Salida: data/output/dataset_web.json con la misma estructura que antes.
"""
from pathlib import Path
import json
import pandas as pd
ROOT = Path(__file__).resolve().parent.parent
RAW = ROOT / "data" / "raw"
OUT = ROOT / "data" / "output"
def huts_eivissa_total() -> dict:
"""Total HUT Eivissa a nivel isla (sin duplicar por seccion)."""
df = pd.read_csv(RAW / "hut_eivissa_2026-07-14.csv")
df = df[df["Municipi"] != "NUEVO BOLSA DE PLAZAS"].copy()
df["plazas"] = pd.to_numeric(df["Total Places"], errors="coerce").fillna(0).astype(int)
return {
"registros": int(len(df)),
"plazas": int(df["plazas"].sum()),
}
def formentera_total() -> dict:
"""Total Formentera a nivel isla (1 municipio)."""
with open(RAW / "ibestat_formentera_2019.json", "r", encoding="utf-8") as f:
data = json.load(f)
return {
"registros": int(data["metadata"]["total_unidades"]),
"plazas": int(data["metadata"]["total_plazas"]),
}
def airbnb_por_isla() -> dict:
"""Total Airbnb por isla desde el parquet (SUM por seccion es correcto)."""
df = pd.read_parquet(OUT / "dataset.parquet")
mall = df[df["isla"] == "Mallorca"]
men = df[df["isla"] == "Menorca"]
return {
"Mallorca": {
"listings": int(mall["airbnb_listings"].sum()),
"entire_homes": int(mall["airbnb_entire_homes"].sum()),
"revenue_total": int(mall["airbnb_revenue_total"].sum()),
"hosts_unicos": int(mall["airbnb_hosts_unicos"].sum()),
"con_licencia": int(mall["airbnb_con_licencia"].sum()),
},
"Menorca": {
"listings": int(men["airbnb_listings"].sum()),
"entire_homes": int(men["airbnb_entire_homes"].sum()),
"revenue_total": int(men["airbnb_revenue_total"].sum()),
"hosts_unicos": int(men["airbnb_hosts_unicos"].sum()),
"con_licencia": int(men["airbnb_con_licencia"].sum()),
},
}
def main() -> None:
print("=== Regenerando dataset_web.json ===\n")
df = pd.read_parquet(OUT / "dataset.parquet")
eiv = huts_eivissa_total()
fmr = formentera_total()
ab = airbnb_por_isla()
print(f"HUT Eivissa: {eiv['registros']:,} registros, {eiv['plazas']:,} plazas")
print(f"IBESTAT Formentera: {fmr['registros']:,} unidades, {fmr['plazas']:,} plazas")
print(f"Airbnb Mallorca: {ab['Mallorca']['listings']:,} listings, "
f"{ab['Mallorca']['entire_homes']:,} entire home/apt")
print(f"Airbnb Menorca: {ab['Menorca']['listings']:,} listings, "
f"{ab['Menorca']['entire_homes']:,} entire home/apt")
resumen = []
isla_meta = df.groupby("isla").size().to_dict()
for isla in ["Mallorca", "Menorca", "Eivissa", "Formentera"]:
record = {
"isla": isla,
"secciones": int(isla_meta.get(isla, 0)),
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 0,
"hut_plazas": 0,
}
if isla in ("Mallorca", "Menorca"):
record["airbnb_listings"] = ab[isla]["listings"]
record["airbnb_entire_homes"] = ab[isla]["entire_homes"]
record["airbnb_revenue_total"] = ab[isla]["revenue_total"]
record["airbnb_hosts_unicos"] = ab[isla]["hosts_unicos"]
record["airbnb_con_licencia"] = ab[isla]["con_licencia"]
if isla == "Eivissa":
record["hut_registros"] = eiv["registros"]
record["hut_plazas"] = eiv["plazas"]
if isla == "Formentera":
record["hut_registros"] = fmr["registros"]
record["hut_plazas"] = fmr["plazas"]
resumen.append(record)
out = {"resumen_por_isla": resumen}
with open(OUT / "dataset_web.json", "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"\nGuardado {OUT / 'dataset_web.json'}")
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+58
View File
@@ -0,0 +1,58 @@
"""
Genera web/public/data/airbnb_listings.geojson a partir de los CSVs raw
de Inside Airbnb Mallorca y Menorca.
Output: FeatureCollection con ~18K puntos. Solo geometria + 2 props
(isla, room_type) para mantener el fichero ligero (~700KB).
"""
from pathlib import Path
import json
import pandas as pd
ROOT = Path(__file__).resolve().parent.parent
RAW = ROOT / "data" / "raw"
OUT = ROOT / "web" / "public" / "data"
OUT.mkdir(parents=True, exist_ok=True)
def load_isla(name: str) -> pd.DataFrame:
df = pd.read_csv(RAW / f"airbnb_{name.lower()}_listings.csv.gz", low_memory=False)
df = df[(df["availability_365"] > 0) | (df["room_type"] == "Entire home/apt")]
df = df.dropna(subset=["latitude", "longitude"])
df["isla"] = name
return df[["id", "latitude", "longitude", "room_type", "isla"]]
def main() -> None:
mall = load_isla("mallorca")
men = load_isla("menorca")
df = pd.concat([mall, men], ignore_index=True)
print(f"Total puntos: {len(df):,}")
features = []
for _, row in df.iterrows():
features.append({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [float(row["longitude"]), float(row["latitude"])],
},
"properties": {
"id": str(row["id"]),
"isla": row["isla"],
"room_type": row["room_type"],
},
})
out = {"type": "FeatureCollection", "features": features}
out_path = OUT / "airbnb_listings.geojson"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, separators=(",", ":"))
size_mb = out_path.stat().st_size / (1024 * 1024)
print(f"Guardado {out_path} ({size_mb:.2f} MB)")
if __name__ == "__main__":
main()