---
id: generate_isochrones_by_zone_pipeline_py_pipelines
name: generate_isochrones_by_zone_pipeline
kind: pipeline
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "async def generate_isochrones_by_zone_pipeline(zones, points, centers, base_url, concurrency) -> dict"
description: "Genera isócronas Valhalla por zona geográfica usando el p75 de tiempos de viaje de los puntos en cada zona."
tags: [pipeline, geo, footprint, valhalla, isochrone, zone, p75, geojson]
uses_functions: ["load_geojson_polygons_py_geo", "polygon_bbox_py_geo", "point_in_polygons_bbox_py_geo", "summary_stats_py_datascience", "valhalla_isochrones_async_py_geo"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
example: |
  import asyncio
  result = asyncio.run(generate_isochrones_by_zone_pipeline(
      zones=[{"label": "M-30", "geojson_path": "zones/m30.geojson", "exclude_geojson_path": None}],
      points=[{"lat": 40.41, "lon": -3.70, "seconds": 600.0}],
      centers=[{"lat": 40.42, "lon": -3.69, "id": "centro_a"}],
  ))
  # result["zones"][0]["minutes"] == 10.0, isochrones lista con GeoJSON
tested: true
tests: ["test_generate_isochrones_by_zone_pipeline"]
test_file_path: "python/functions/pipelines/tests/test_generate_isochrones_by_zone_pipeline.py"
file_path: "python/functions/pipelines/generate_isochrones_by_zone_pipeline.py"
params:
  - {name: zones, desc: "Lista de dicts con label, geojson_path y exclude_geojson_path (opcional). Define las zonas geográficas."}
  - {name: points, desc: "Lista de dicts con lat, lon y seconds (tiempo de viaje real medido). Se calcula p75 por zona."}
  - {name: centers, desc: "Lista de dicts con lat, lon e id. Se filtra por zona para generar isócronas."}
  - {name: base_url, desc: "URL base del servidor Valhalla. Default http://localhost:8002."}
  - {name: concurrency, desc: "Número máximo de requests async simultáneos para isócronas. Default 6."}
output: "Dict con 'zones': lista de {label, minutes, n_points, n_centers, isochrones} por zona."
source_repo: "internal:footprint_aurgi"
source_license: "internal-aurgi"
source_file: "internal:composed"
---

## Ejemplo

```python
import asyncio

result = asyncio.run(generate_isochrones_by_zone_pipeline(
    zones=[
        {"label": "M-30", "geojson_path": "zones/m30.geojson", "exclude_geojson_path": None},
        {"label": "M-40", "geojson_path": "zones/m40.geojson", "exclude_geojson_path": "zones/m30.geojson"},
    ],
    points=[
        {"lat": 40.41, "lon": -3.70, "seconds": 600.0},
        {"lat": 40.38, "lon": -3.72, "seconds": 900.0},
    ],
    centers=[
        {"lat": 40.42, "lon": -3.69, "id": "centro_a"},
        {"lat": 40.35, "lon": -3.75, "id": "centro_b"},
    ],
))

for z in result["zones"]:
    print(f"{z['label']}: {z['n_points']} pts, p75={z['minutes']:.1f}min, {z['n_centers']} centros")
```

## Notas

Función async — requiere `asyncio.run(...)` o `await`.
El p75 se calcula sobre `seconds` de los puntos en zona; luego se divide entre 60 para obtener minutos.
Si una zona no tiene puntos con seconds válidos, `minutes=None` y no se generan isócronas.
El polígono de exclusión permite hacer anillos (ej: M-40 excluyendo M-30).
Los centros se asignan a la primera zona que los contiene — no se deduplicam entre zonas.
