Incendios forestales en la interfaz de Los Rosales — cuaderno integrado
Vegetación combustible, pendiente, FWI histórico y futuro, y exposición por franjas de interfaz
Resumen¶
Los Rosales está pegado a los Cerros Orientales. En esa interfaz urbano-forestal un incendio de la cobertura vegetal puede alcanzar las viviendas. Este cuaderno combina dos enfoques. El espacial mide la distancia de cada edificación a la vegetación combustible, agravada por la pendiente. El meteorológico sigue el flujo «Wildfire (FWI)» del manual CLIMAAX CLIMAAX consortium, 2026: calcula el índice meteorológico de incendios (FWI) Van Wagner, 1987 con xclim Bourgault et al., 2023 y su cambio bajo SSP2-4.5 y SSP5-8.5.
Resultados clave (ejecución del 2026-10-27 con datos reales):
Hay 356 ha de vegetación combustible (árboles y arbustos) en el área con búfer, el 42 % de las celdas. El 18 % del área tiene pendiente mayor de 30°.
El FWI de Bogotá es bajo: el máximo anual promedia 13,6 y ningún año supera el umbral alto de 21,3.
Con SSP5-8.5 al final del siglo (Tmax +4,6 °C, lluvia −14 %) aparecen unos 3 días al año con FWI alto. Hoy no hay ninguno.
Las páginas de detalle a, b, c y d explican cada paso con más extensión.
1. Marco¶
1.1 La amenaza y el barrio¶
El riesgo tiene dos componentes. El primero es la meteorología que hace arder la vegetación: temporadas secas, calor, viento y baja humedad. El segundo es la cercanía y la pendiente, que determinan cuántas edificaciones y personas quedan al alcance del fuego. El POT de Bogotá zonifica la amenaza en suelo urbano, pero a escala 1:25.000 Alcaldía Mayor de Bogotá, 2021, muy gruesa para un barrio. Por eso aquí se usa como contexto y la interfaz se deriva de la cobertura del suelo.
1.2 Cadena de análisis¶
Combustible. Las clases 10 y 20 de ESA WorldCover Zanaga et al., 2022 se vectorizan.
Interfaz. Se mide la distancia de cada edificación a la vegetación y se clasifica por franjas.
Pendiente. Una ladera de más de 30° se marca como agravante.
FWI. Se cuentan los días por año sobre 21,3, el umbral «alto» de EFFIS.
Futuro. Se aplican a la serie diaria los deltas de cada modelo de NEX-GDDP-CMIP6 Thrasher et al., 2022 y se recalcula el FWI.
2. Configuración del entorno¶
import numpy as np
import pandas as pd
from IPython.display import Markdown, display
from sidro import process, viz
from sidro.analisis import clima, espacial, masa
from sidro.aoi import bbox_wgs84
from sidro.config import parametros
from sidro.fetch import obtener_capa
pot = intentar(obtener_capa, "amenaza_incendio_forestal",
mensaje="La capa de amenaza por incendios forestales del POT no está disponible")3. Amenaza¶
3.1 Zonificación oficial del POT¶
if pot is not None and len(pot):
pot = masa.preparar(pot) # traduce los códigos numéricos del POT (1 Baja, 2 Media, 3 Alta ...)
m = viz.mapa_base()
viz.capa(m, pot, "categoria", nombre="Amenaza POT", categorica=True, cmap="OrRd")
viz.cerrar(m)
elif pot is not None:
display(Markdown("La capa del POT no tiene polígonos en el área de interés: la interfaz urbano-forestal del barrio "
"se evalúa con la distancia a la vegetación de [3.2.c](c_riesgo.md)."))
m = None
else:
m = None
m3.2 Vegetación combustible¶
ruta_cob = intentar(obtener_capa, "cobertura_suelo", mensaje="No se pudo obtener la cobertura del suelo")
if ruta_cob is not None:
cob = process.abrir_raster(ruta_cob)
clases = parametros()["incendio_forestal"]["clases_bosque_worldcover"]
bosque = espacial.poligonos_de_clase(cob, clases)
area_ha = bosque.area.sum() / 1e4
print(f"Vegetación combustible en el área con búfer: {area_ha:,.1f} ha "
f"({100 * np.isin(cob.values, clases).sum() / np.isfinite(cob.values).sum():.0f} % de las celdas)")
m = viz.mapa_base()
viz.capa(m, bosque, nombre="Vegetación combustible", color="#2d6a4f")
viz.cerrar(m)
else:
bosque, m = None, None
mWarning 1: DeprecationWarning: 'Memory' driver is deprecated since GDAL 3.11. Use 'MEM' onwards. Further messages of this type will be suppressed.
Vegetación combustible en el área con búfer: 355.7 ha (42 % de las celdas)
3.3 Pendiente¶
ruta_dem = intentar(obtener_capa, "dem", mensaje="No se pudo obtener el modelo digital de elevación")
if ruta_dem is not None:
dem = process.abrir_raster(ruta_dem)
pend = process.pendiente_grados(dem)
crit = parametros()["incendio_forestal"]["pendiente_critica_grados"]
v = pend.values[np.isfinite(pend.values)]
display(pd.Series({
"pendiente media (°)": v.mean(),
"pendiente máxima (°)": v.max(),
f"área con pendiente > {crit}° (%)": 100 * (v > crit).mean(),
}).round(1).to_frame("área con búfer"))Lectura. La vegetación combustible cubre el 42 % del área con búfer. Eso incluye los cerros, pero también el arbolado urbano, que WorldCover clasifica como árboles. Casi una quinta parte del área supera 30° de pendiente, sobre todo en la ladera oriental.
3.4 FWI histórico¶
import matplotlib.pyplot as plt
clima_df = intentar(obtener_capa, "clima_observado_diario", mensaje="No se pudo obtener la serie climática diaria")
if clima_df is not None:
lat = (bbox_wgs84()[1] + bbox_wgs84()[3]) / 2
fwi = intentar(clima.fwi_diario, clima_df, lat, mensaje="No se pudo calcular el FWI (faltan variables en la serie)")
else:
fwi = NoneWARNING pint.util: Redefining 'percent' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining '%' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'year' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'yr' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'C' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'd' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'h' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'degrees_north' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'degrees_east' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining 'degrees' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
WARNING pint.util: Redefining '[speed]' (<class 'pint.delegates.txt_defparser.plain.DerivedDimensionDefinition'>)
if fwi is not None:
umbral = parametros()["incendio_forestal"]["fwi_umbral_alto"]
dias = clima.dias_fwi_alto(fwi)
resumen = pd.DataFrame({"FWI medio": fwi.groupby(fwi.index.year).mean(),
"FWI máximo": fwi.groupby(fwi.index.year).max(), "días FWI alto": dias})
display(resumen.describe().loc[["mean", "min", "max"]].round(1))
fig, ax = plt.subplots(1, 2, figsize=(10, 3.4))
mensual = fwi.groupby(fwi.index.month).mean()
ax[0].bar(mensual.index, mensual.values, color="#bc6c25")
ax[0].set_xlabel("Mes"), ax[0].set_ylabel("FWI medio"), ax[0].set_title("Ciclo anual")
ax[1].plot(resumen.index, resumen["FWI máximo"], color="#bc6c25")
ax[1].axhline(umbral, ls="--", color="#555")
ax[1].set_xlabel("Año"), ax[1].set_ylabel("FWI máximo anual"), ax[1].set_title(f"Umbral alto = {umbral}")
fig.tight_layout()
fig
Lectura. El régimen bimodal de lluvias concentra el FWI en diciembre–marzo y julio–agosto. El máximo anual llegó a 21,1 en el peor año de la serie, justo por debajo del umbral. El conteo de días «altos» es nulo por el clima húmedo de la ciudad, no por un error. Para decisiones locales conviene calibrar un umbral propio con percentiles de la serie.
4. Exposición y riesgo¶
La medida es de exposición: cuántos elementos están cerca de la vegetación. No hay una función de vulnerabilidad aceptada para vivienda urbana frente a incendios de cobertura, y en ese caso CCDR-tools recomienda reportar la exposición Amadio, 2024.
import numpy as np
import pandas as pd
from IPython.display import Markdown, display
from sidro import exposicion, process, viz
from sidro.analisis import espacial, indices
from sidro.config import parametros, rutas
from sidro.fetch import obtener_capa
edif = intentar(exposicion.edificaciones, mensaje="No se pudo construir la exposición de edificaciones")
ruta_cob = intentar(obtener_capa, "cobertura_suelo", mensaje="No se pudo obtener la cobertura del suelo")
ruta_dem = intentar(obtener_capa, "dem", mensaje="No se pudo obtener el modelo digital de elevación")
bosque = None
if ruta_cob is not None:
clases = parametros()["incendio_forestal"]["clases_bosque_worldcover"]
bosque = espacial.poligonos_de_clase(process.abrir_raster(ruta_cob), clases)
listo = edif is not None and bosque is not None and len(bosque) > 0
if edif is not None and not listo:
display(Markdown(":::{warning}\nSin vegetación combustible no se puede calcular la interfaz.\n:::"))4.1 Franjas de interfaz¶
if listo:
exp = espacial.franjas_interfaz(edif, bosque)
orden = [f"{a}–{b} m" for a, b in zip(parametros()["incendio_forestal"]["franjas_interfaz_m"][:-1],
parametros()["incendio_forestal"]["franjas_interfaz_m"][1:])]
orden.append(f"> {parametros()['incendio_forestal']['franjas_interfaz_m'][-1]} m")
if ruta_dem is not None:
pend = process.pendiente_grados(process.abrir_raster(ruta_dem))
exp["pendiente_deg"] = process.muestrear(pend, exp, "pendiente_deg")
crit = parametros()["incendio_forestal"]["pendiente_critica_grados"]
exp["pendiente_critica"] = exp["pendiente_deg"] > crit
else:
exp["pendiente_deg"], exp["pendiente_critica"] = np.nan, False
tabla = (exp.groupby("franja_interfaz")
.agg(edificaciones=("id_edif", "count"), personas=("ocupantes_est", "sum"),
valor_millones_cop=("valor_cop", lambda s: s.sum() / 1e6),
en_pendiente_critica=("pendiente_critica", "sum"))
.reindex(orden).fillna(0))
tabla.loc["Total"] = tabla.sum()
display(tabla.round(0).astype(int))if listo:
m = viz.mapa_base()
viz.capa(m, bosque, nombre="Vegetación combustible", color="#2d6a4f")
viz.capa(m, exp, "franja_interfaz", nombre="Franja de interfaz", categorica=True, cmap="YlOrRd_r",
tooltip=["franja_interfaz", "dist_bosque_m", "pisos", "pendiente_deg"])
viz.cerrar(m)
else:
m = None
mLectura. El 24 % de las edificaciones queda a menos de 30 m de vegetación, pero en buena parte es arbolado urbano. Lea esa franja como sensibilidad. Las 248 edificaciones en pendiente crítica, casi todas en la franja de 0–30 m, son la exposición más clara a un incendio que suba por la ladera. Las personas son residentes estimados desde el área habitacional (110 m² brutos por residente): unos 54.000 en el búfer, de los que 26.000 viven a menos de 30 m de vegetación. Dentro del barrio, prácticamente todos sus 13.700 residentes viven a menos de 300 m.
4.2 Agregación por manzana y vulnerabilidad social¶
La medida de riesgo por unidad son las personas estimadas a menos de 300 m de la vegetación.
if listo:
exp["cerca"] = exp["dist_bosque_m"] <= 300
exp["personas_cerca"] = exp["ocupantes_est"].where(exp["cerca"], 0.0)
exp["edif_cerca"] = exp["cerca"].astype(int)
unidades = process.unidades_analisis()
agg = process.agregar(exp, unidades, {
"n_edificaciones": ("id_edif", "count"),
"edificaciones_cerca": ("edif_cerca", "sum"),
"personas_cerca": ("personas_cerca", "sum"),
"dist_media_bosque_m": ("dist_bosque_m", "mean"),
"pendiente_media_deg": ("pendiente_deg", "mean"),
})
agg["vulnerabilidad_social"] = indices.vulnerabilidad_social(agg)
print(f"Unidades: {len(agg)} (origen: {unidades.attrs.get('origen')})")
display(agg.drop(columns="geometry").sort_values("personas_cerca", ascending=False).head(10).round(1))WARNING sidro.fetch: manzanas_censo: falló nivel nacional/usuario -> Archivo de usuario no encontrado: /Users/mauricio/Documents/Data-science-local/SIDRo/data/user/dane_mgn_manzanas_cnpv2018.gpkg
WARNING sidro.process: Unidades: manzanas_censo no disponible (No se pudo obtener 'manzanas_censo'. Intentos: [{'nivel': 'nacional', 'tipo': 'usuario', 'error': 'Archivo de usuario no encontrado: /Users/mauricio/Documents/Data-science-local/SIDRo/data/user/dane_mgn_manzanas_cnpv2018.gpkg'}])
Unidades: 57 (origen: estratificacion)
if listo:
biv = indices.bivariado(agg["personas_cerca"].fillna(0), agg["vulnerabilidad_social"])
agg = agg.join(biv)
m = viz.mapa_base()
viz.capa_bivariada(m, agg)
viz.cerrar(m)
else:
m = None
mLeyenda: exposición a incendio forestal en el eje vertical, vulnerabilidad social en el horizontal.
if listo:
process.guardar(agg[["id_unidad", "geometry", "personas_cerca", "edificaciones_cerca", "vulnerabilidad_social"]]
.rename(columns={"personas_cerca": "riesgo"}), "riesgo_incendio_forestal")
print("Guardado:", (rutas().processed / "riesgo_incendio_forestal.parquet").relative_to(rutas().raiz))Guardado: data/processed/riesgo_incendio_forestal.parquet
5. Escenarios climáticos¶
El clima futuro cambia la propensión meteorológica a incendios, no la distancia a la vegetación. A la serie observada se le aplica el cambio medio de cada modelo: aditivo en Tmax, multiplicativo en lluvia, humedad y viento Riahi et al., 2017O'Neill et al., 2017.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from IPython.display import display
from sidro.analisis import clima
from sidro.aoi import bbox_wgs84
from sidro.config import aoi_cfg, parametros
from sidro.fetch import obtener_capa
MODELOS = None # p. ej. ["ACCESS-CM2", "EC-Earth3"] para una prueba rápida
VARIABLES = ["tasmax", "pr", "hurs", "sfcWind"]
cfg = aoi_cfg()["clima"]
serie = intentar(obtener_capa, "clima_observado_diario", mensaje="No se pudo obtener la serie climática diaria")
lat = (bbox_wgs84()[1] + bbox_wgs84()[3]) / 2
fwi_base = None
if serie is not None:
fwi_base = intentar(clima.fwi_diario, serie, lat, mensaje="No se pudo calcular el FWI base")deltas = None
if fwi_base is not None:
deltas = intentar(clima.deltas_tabla, VARIABLES, cfg["escenarios"], cfg["horizontes"], MODELOS,
cache="deltas_incendio_forestal",
mensaje="No se pudieron obtener las proyecciones CMIP6 (requiere acceso a Planetary Computer)")
if deltas is not None:
display(deltas.groupby(["variable", "escenario", "horizonte"])["delta"].median().unstack("variable").round(3))resultados = None
if deltas is not None:
umbral = parametros()["incendio_forestal"]["fwi_umbral_alto"]
base = clima.dias_fwi_alto(fwi_base, umbral).mean()
filas = []
for (esc, hor), g in deltas.groupby(["escenario", "horizonte"]):
dias = []
for modelo, gm in g.groupby("modelo"):
d = gm.set_index("variable")["delta"].to_dict()
fut = clima.fwi_diario(clima.aplicar_deltas_meteo(serie, d), lat)
dias.append(clima.dias_fwi_alto(fut, umbral).mean())
filas.append({"escenario": esc, "horizonte": hor, "modelos": len(dias),
**{f"p{q}": np.percentile(dias, q) for q in cfg["percentiles_ensamble"]}})
resultados = pd.DataFrame(filas)
resultados["base_dias_por_anio"] = base
display(resultados.round(2))if resultados is not None:
fig, ax = plt.subplots(figsize=(6, 3.4))
etiquetas = [f"{r.escenario}\n{r.horizonte}" for r in resultados.itertuples()]
x = np.arange(len(resultados))
mediana = resultados[f"p{cfg['percentiles_ensamble'][1]}"].values
bajo = mediana - resultados[f"p{cfg['percentiles_ensamble'][0]}"].values
alto = resultados[f"p{cfg['percentiles_ensamble'][2]}"].values - mediana
ax.bar(x, mediana, yerr=[bajo, alto], color="#bc6c25", capsize=4)
ax.axhline(resultados.base_dias_por_anio.iloc[0], ls="--", color="#555", label="base observada")
ax.set_xticks(x, etiquetas, fontsize=8), ax.set_ylabel("Días con FWI alto por año")
ax.legend()
fig
Lectura. Todos los modelos proyectan más calor y menos lluvia. Hasta mediados de siglo los días con FWI alto siguen siendo excepcionales (menos de 1 al año en la mediana). Con SSP5-8.5 en 2081–2100 la mediana sube a unos 3 días al año. El método delta no cambia la longitud de las rachas secas, que son clave para el FWI. Por eso el resultado ordena escenarios, pero no predice el número de días críticos.
if resultados is not None:
from sidro import export
export.guardar_resumen("incendio_forestal_escenarios", {"escenarios": resultados.to_dict("records")})6. Tabla resumen para el informe ejecutivo¶
| Indicador | Valor (ejecución 2026-10-25) |
|---|---|
| Vegetación combustible en el área con búfer | 355,7 ha (42 % de las celdas) |
| Área con pendiente > 30° | 18,0 % |
| FWI máximo anual (media / máximo de la serie) | 13,6 / 21,1 |
| Días con FWI ≥ 21,3, clima actual | 0 por año |
| Edificaciones a menos de 30 m / 100 m de vegetación | 3.964 / 9.380 |
| Edificaciones en pendiente crítica | 248 |
| Días con FWI alto, SSP5-8.5 2081–2100 (p10–p50–p90) | 1,6 – 3,1 – 3,2 por año |
7. Conclusiones¶
Hoy el clima de Bogotá limita la propensión meteorológica a incendios. La exposición del barrio viene sobre todo de su cercanía a los cerros y de la pendiente.
Con altas emisiones, el FWI alto deja de ser inexistente a final de siglo. La señal es pequeña en valor absoluto, pero clara en dirección.
Las edificaciones en pendiente crítica cerca de la vegetación son las primeras candidatas a franjas de protección y manejo del combustible.
7.1 Limitaciones¶
WorldCover cuenta el arbolado urbano como combustible. La franja de 0–30 m sobrestima la exposición real.
El umbral de 21,3 no está calibrado para el páramo y el bosque altoandino. El FWI se calcula con variables diarias, no de mediodía.
La resolución de ERA5 (~25 km para lluvia y viento) y de NEX-GDDP (~25 km) no resuelve los cerros.
No se modela la propagación del fuego ni el humo. El resultado es indicativo y no reemplaza la zonificación del POT ni los conceptos de IDIGER.
7.2 Próximos pasos¶
Limitar la vegetación a la cobertura de los cerros o a polígonos de tamaño mínimo, para separar el arbolado urbano.
Calibrar un umbral local de FWI con percentiles de la serie.
Añadir la cobertura CORINE del IDEAM como contexto nacional de vegetación.
- CLIMAAX consortium. (2026). CLIMAAX Climate Risk Assessment Handbook. Zenodo. 10.5281/zenodo.18186357
- Van Wagner, C. E. (1987). Development and structure of the Canadian Forest Fire Weather Index System (Forestry Technical Report No. 35). Canadian Forestry Service.
- Bourgault, P., Huard, D., Smith, T. J., & others. (2023). xclim: xarray-based climate data analytics. Journal of Open Source Software, 8(85), 5415. 10.21105/joss.05415
- Alcaldía Mayor de Bogotá. (2021). Decreto 555 de 2021, por el cual se adopta la revisión general del Plan de Ordenamiento Territorial de Bogotá D.C. https://services7.arcgis.com/lsxbLWF2l19Rmhqj/ArcGIS/rest/services/POT_Bogota_Decreto_555_2021/FeatureServer
- Zanaga, D., Van De Kerchove, R., Daems, D., & others. (2022). ESA WorldCover 10 m 2021 v200. 10.5281/zenodo.7254221
- Thrasher, B., Wang, W., Michaelis, A., Melton, F., Lee, T., & Nemani, R. (2022). NASA Global Daily Downscaled Projections, CMIP6. Scientific Data, 9, 262. 10.1038/s41597-022-01393-4
- Amadio, M. (2024). CCDR tools data and methodology. Global Facility for Disaster Reduction and Recovery (GFDRR), World Bank. https://gfdrr.github.io/CCDR-tools
- Riahi, K., van Vuuren, D. P., Kriegler, E., & others. (2017). The Shared Socioeconomic Pathways and their energy, land use, and greenhouse gas emissions implications: An overview. Global Environmental Change, 42, 153–168. 10.1016/j.gloenvcha.2016.05.009
- O’Neill, B. C., Kriegler, E., Ebi, K. L., & others. (2017). The roads ahead: Narratives for shared socioeconomic pathways describing world futures in the 21st century. Global Environmental Change, 42, 169–180. 10.1016/j.gloenvcha.2015.01.004