Incendios urbanos en Los Rosales — cuaderno integrado
Ocurrencia histórica, susceptibilidad de lo construido, capacidad de respuesta y sensibilidad de los pesos
Resumen¶
El incendio estructural es la amenaza más cotidiana de un barrio consolidado, pero Bogotá no tiene un mapa oficial de amenaza a escala de manzana Unidad Administrativa Especial Cuerpo Oficial de Bomberos de Bogotá, 2022. Este cuaderno construye un índice compuesto relativo de 0 a 1 por manzana. El índice se arma con tres ideas: dónde han ocurrido incendios, qué tan susceptible es lo construido y qué tan rápida puede ser la respuesta. Después prueba cuánto cambia el orden de las manzanas si cambian los pesos.
Resultados clave (ejecución del 2026-10-27 con datos reales):
Hay 29 incidentes estructurales georreferenciados alrededor del barrio, 4 estaciones de bomberos cercanas y 162 hidrantes de la EAAB en el área con búfer.
El tiempo de viaje por la red vial desde la estación más cercana va de 3 a 8 minutos, con una media de 5.
Las siete variables del índice tienen datos. Cinco de las 57 manzanas (00830405, 00830431, 00830531, 00830523 y 00830517) quedan entre el 20 % más alto en al menos el 92 % de las simulaciones de pesos.
Las páginas de detalle a, b, c y d explican cada paso con más extensión.
1. Marco¶
1.1 Cadena de análisis¶
Variables. Siete por manzana, todas orientadas «más alto = más riesgo»: vetustez, uso de alto riesgo, altura, densidad construida, densidad de kernel de incidentes, tiempo de respuesta y distancia al hidrante.
Normalización. Cada variable se lleva a 0–1 por mínimo y máximo dentro del barrio.
Índice. Suma ponderada con los pesos de
config/parametros.yaml. Si falta una variable, los pesos de las demás se renormalizan.Sensibilidad. Miles de combinaciones de pesos alrededor de los pesos base (Dirichlet) miden la estabilidad del orden.
El incendio urbano no depende del clima de forma directa, así que aquí no hay escenarios CMIP6. La incertidumbre principal es metodológica: los pesos son un juicio.
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 espacial, indices
from sidro.config import parametros
from sidro.fetch import obtener_capa
p = parametros()["incendio_urbano"]
incidentes = intentar(obtener_capa, "incidentes_incendio_urbano",
mensaje="No hay registro de incidentes: solicítelo a UAECOB y guárdelo en data/user/incidentes_uaecob.csv "
"(columnas longitud, latitud, fecha, tipo_incidente). Sin él, el índice omite la ocurrencia histórica")3. Amenaza y capacidad de respuesta¶
3.1 Ocurrencia histórica¶
if incidentes is not None and len(incidentes):
celdas = process.grilla(25)
celdas["densidad"] = indices.densidad_kernel(incidentes, celdas, p["radio_kernel_m"])
print(f"{len(incidentes)} incidentes en el área con búfer; radio del kernel: {p['radio_kernel_m']} m")
m = viz.mapa_base()
viz.capa(m, celdas, "densidad", nombre="Densidad de incidentes (eventos/km²)", cmap="OrRd")
viz.capa(m, incidentes, nombre="Incidentes", color="#222", marker_kwds={"radius": 2})
viz.cerrar(m)
elif incidentes is not None:
display(Markdown(":::{note}\nEl registro no tiene incidentes dentro del área con búfer.\n:::"))
m = None
else:
m = None
m29 incidentes en el área con búfer; radio del kernel: 300 m
Lectura. Los 29 incidentes vienen de un CSV de 2019 a agosto de 2020 y de la capa REST desde 2024, recortados al búfer del área anterior. Faltan 2021–2023 y los años sin coordenadas. Con tan pocos eventos, el mapa de densidad es muy sensible al radio de 300 m: indica concentración, no frecuencia.
3.2 Estaciones, tiempo de respuesta e hidrantes¶
est = intentar(obtener_capa, "estaciones_bomberos",
mensaje="No se pudieron obtener las estaciones de bomberos de OpenStreetMap")
if est is not None:
print(f"{len(est)} estación(es) en el área con búfer ampliado")
if len(est):
cols = [c for c in ("name", "operator") if c in est.columns]
display(est[cols].reset_index(drop=True) if cols else est.head())4 estación(es) en el área con búfer ampliado
if est is not None and len(est):
grilla = process.grilla(50)
grilla["minutos"] = espacial.tiempo_respuesta_min(grilla, est)
grilla["rango_min"] = pd.cut(grilla["minutos"], [0, 3, 5, 8, 12, np.inf],
labels=["< 3", "3–5", "5–8", "8–12", "> 12"]).astype("string")
display(grilla["minutos"].describe().round(1).to_frame("minutos"))
m = viz.mapa_base()
viz.capa(m, grilla, "rango_min", nombre="Tiempo de respuesta (min)", categorica=True, cmap="YlOrRd")
viz.capa(m, est, nombre="Estaciones", color="#0b3d91", marker_kwds={"radius": 6})
viz.cerrar(m)
else:
m = None
mhid = intentar(obtener_capa, "hidrantes", mensaje="No se pudieron obtener los hidrantes")
if hid is not None:
print(f"{len(hid)} hidrantes en el área con búfer")
if len(hid):
m = viz.mapa_base()
viz.capa(m, hid, nombre="Hidrantes", color="#1b998b", marker_kwds={"radius": 3})
viz.cerrar(m)
else:
m = None
else:
m = None
m162 hidrantes en el área con búfer
Lectura. Todo el barrio queda entre 3 y 8 minutos de viaje de la estación más cercana. Es un tiempo de viaje por la red vial de OpenStreetMap a velocidad fija: no incluye despacho, salida, tráfico ni acceso al inmueble. Los 162 hidrantes vienen del servicio abierto de la EAAB, que es un inventario más completo que el de OpenStreetMap.
4. Índice de riesgo por manzana¶
import numpy as np
import pandas as pd
from IPython.display import Markdown, display
from sidro import exposicion, process, viz
from sidro.analisis import indices, urbano
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")
incidentes = intentar(obtener_capa, "incidentes_incendio_urbano", mensaje="Sin registro de incidentes: se omite la ocurrencia histórica")
estaciones = intentar(obtener_capa, "estaciones_bomberos", mensaje="Sin estaciones: se omite el tiempo de respuesta")
hidrantes = intentar(obtener_capa, "hidrantes", mensaje="Sin hidrantes: se omite la distancia al hidrante")if edif is not None:
unidades = process.unidades_analisis()
var = urbano.variables_unidad(edif, unidades, incidentes, estaciones, hidrantes)
pesos = parametros()["incendio_urbano"]["pesos"]
indice = indices.incendio_urbano(var)
usados = indice.attrs.get("pesos_usados", {}) # los atributos no sobreviven a la asignación a una columna
var["indice_incendio_urbano"] = indice
print(f"Unidades: {len(var)} (origen: {unidades.attrs.get('origen')})")
display(pd.DataFrame({"peso base": pd.Series(pesos), "peso usado": pd.Series(usados),
"unidades con dato": var[list(pesos)].notna().sum()}).fillna(0).round(2))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 edif is not None:
agg = unidades[["id_unidad", "geometry"]].join(var.drop(columns="id_unidad"))
agg["vulnerabilidad_social"] = indices.vulnerabilidad_social(unidades)
top = agg.drop(columns="geometry").sort_values("indice_incendio_urbano", ascending=False)
display(top.head(10).round(2))if edif is not None:
m = viz.mapa_base()
viz.capa(m, agg, "indice_incendio_urbano", nombre="Índice de incendio urbano", cmap="YlOrRd",
tooltip=["id_unidad", "indice_incendio_urbano", "vetustez", "altura", "tiempo_respuesta"])
viz.cerrar(m)
else:
m = None
m4.1 Qué mueve el índice¶
import matplotlib.pyplot as plt
if edif is not None:
aporte = pd.DataFrame({c: process.normalizar(agg[c]).fillna(0) * w for c, w in usados.items()})
fig, ax = plt.subplots(figsize=(7, 3.6))
orden_u = agg["indice_incendio_urbano"].sort_values(ascending=False).head(15).index
aporte.loc[orden_u].plot.barh(stacked=True, ax=ax, width=0.85)
ax.invert_yaxis(), ax.set_xlabel("Contribución al índice"), ax.set_yticklabels(agg.loc[orden_u, "id_unidad"])
ax.legend(fontsize=7, loc="lower right")
fig
Lectura. La 00830405 encabeza el índice por su vetustez (unos 48 años de media) y por los incidentes cercanos. La 00830431 combina incidentes cercanos con la mayor distancia a un hidrante (unos 90 m), y la 00830517 tiene la mayor densidad de incidentes del barrio. La descomposición indica qué tipo de intervención orientar en cada una: prevención, inspección, hidrantes o tiempos de respuesta. El índice es relativo: ordena manzanas, no es una probabilidad.
4.2 Riesgo × vulnerabilidad social¶
if edif is not None:
biv = indices.bivariado(agg["indice_incendio_urbano"], agg["vulnerabilidad_social"])
agg = agg.join(biv)
m = viz.mapa_base()
viz.capa_bivariada(m, agg)
viz.cerrar(m)
else:
m = None
mLeyenda: índice de incendio urbano en el eje vertical, vulnerabilidad social en el horizontal.
if edif is not None:
process.guardar(agg[["id_unidad", "geometry", "indice_incendio_urbano", "vulnerabilidad_social"]]
.rename(columns={"indice_incendio_urbano": "riesgo"}), "riesgo_incendio_urbano")
print("Guardado:", (rutas().processed / "riesgo_incendio_urbano.parquet").relative_to(rutas().raiz))Guardado: data/processed/riesgo_incendio_urbano.parquet
5. Sensibilidad de los pesos¶
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from IPython.display import display
from sidro import exposicion, process
from sidro.analisis import indices, urbano
from sidro.config import parametros
from sidro.fetch import obtener_capa
edif = intentar(exposicion.edificaciones, mensaje="No se pudo construir la exposición de edificaciones")
inc = intentar(obtener_capa, "incidentes_incendio_urbano", mensaje="Sin incidentes: se omite la ocurrencia histórica")
est = intentar(obtener_capa, "estaciones_bomberos", mensaje="Sin estaciones: se omite el tiempo de respuesta")
hid = intentar(obtener_capa, "hidrantes", mensaje="Sin hidrantes: se omite la distancia al hidrante")
sens = None
if edif is not None:
unidades = process.unidades_analisis()
var = urbano.variables_unidad(edif, unidades, inc, est, hid)
pesos = parametros()["incendio_urbano"]["pesos"]
base = indices.incendio_urbano(var)
sens = indices.sensibilidad_pesos(var, pesos)
sens.insert(0, "id_unidad", var["id_unidad"])
sens["indice_base"] = base
sens["ancho_rango"] = sens["rango_p95"] - sens["rango_p05"]
display(sens.sort_values("indice_base", ascending=False).head(10).round(2))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'}])
if sens is not None:
top = sens.sort_values("indice_base", ascending=False).head(15)
fig, ax = plt.subplots(figsize=(7, 4))
y = np.arange(len(top))
ax.hlines(y, top.rango_p05, top.rango_p95, color="#bc6c25")
ax.plot(top.rango_mediano, y, "o", color="#8c2d04")
ax.set_yticks(y, top.id_unidad), ax.invert_yaxis(), ax.invert_xaxis()
ax.set_xlabel("Puesto en el ranking (1 = más alto), mediana e intervalo del 90 %")
ax.grid(alpha=0.3)
fig
if sens is not None:
k = max(1, round(0.2 * len(var)))
prob = urbano.probabilidad_top_k(var, pesos, k)
sens["prob_top"] = prob.values
resumen = sens.sort_values("prob_top", ascending=False).head(10)[["id_unidad", "indice_base", "prob_top"]]
print(f"Fracción de simulaciones en que la unidad queda entre el 20 % más alto ({k} unidades):")
display(resumen.round(2))
robustas = int((sens.prob_top >= 0.9).sum())
print(f"{robustas} unidades están entre las más altas en al menos 90 % de las simulaciones")Fracción de simulaciones en que la unidad queda entre el 20 % más alto (11 unidades):
5 unidades están entre las más altas en al menos 90 % de las simulaciones
Lectura. La manzana 00830405 es la primera en todas las simulaciones y la 00830431 queda en el 20 % más alto en el 99 % de ellas. Con la 00830531, la 00830523 y la 00830517 (92–96 %) son las candidatas más sólidas para priorizar. Las que siguen (00830407, 00830516 y 00830518, con 80–85 %) dependen más de la ponderación.
if sens is not None:
from sidro import export
export.guardar_resumen("incendio_urbano_sensibilidad",
{"pesos_base": pesos, "unidades": sens.drop(columns="id_unidad").assign(
id_unidad=sens["id_unidad"]).to_dict("records")})6. Tabla resumen para el informe ejecutivo¶
| Indicador | Valor (ejecución 2026-10-25) |
|---|---|
| Incidentes estructurales en el área con búfer | 29 (2019–2020 y desde 2024) |
| Estaciones de bomberos consideradas | 4 |
| Tiempo de viaje por la red vial (mín. – media – máx.) | 3,0 – 5,2 – 8,0 min |
| Hidrantes (EAAB, área con búfer) | 162 |
| Manzana con mayor índice | 00830405 (0,57) |
| Manzanas robustas (≥ 90 % de simulaciones en el 20 % más alto) | 5 de 57 |
7. Conclusiones¶
Cinco manzanas (00830405, 00830431, 00830531, 00830523 y 00830517) salen entre las de mayor riesgo con casi cualquier juicio razonable de pesos. Son el punto de partida para inspecciones y prevención.
La ocurrencia histórica separa a las manzanas más altas, pero se apoya en solo 29 eventos: completar la serie de incidentes puede cambiar el orden.
7.1 Limitaciones¶
El índice es relativo y no es una probabilidad. Sin un registro completo de incidentes, la ocurrencia histórica es una muestra parcial.
El tiempo de respuesta usa la red vial de OpenStreetMap a una velocidad fija, sin tráfico ni pendiente.
No se observan instalaciones eléctricas y de gas, almacenamiento de materiales ni conductas.
La sensibilidad solo cubre los pesos, no los errores en los datos. El resultado es indicativo y no sustituye los conceptos de Bomberos.
7.2 Próximos pasos¶
Solicitar a UAECOB los incidentes 2016–hoy con coordenadas.
Actualizar los incidentes al nuevo búfer cuando responda el portal de datos abiertos de Bogotá.
Calibrar los pesos con la ocurrencia histórica cuando la serie esté completa.
- Unidad Administrativa Especial Cuerpo Oficial de Bomberos de Bogotá. (2022). Caracterización de escenarios de riesgo por incendio estructural por localidad, 2014–2021. https://www.bomberosbogota.gov.co/sites/default/files/Documentacion/Prevencion/EscenariosRiesgo/