Análisis territorial · Bogotá
BOGOTÁ BAJO LA LUPA
La inseguridad no se distribuye de una sola manera: qué revelan las denuncias, el territorio y la percepción ciudadana.
112 UPZ · 67 variables · Denuncias de enero a julio de 2023
Visualización de Datos · Konrad Lorenz · Octubre de 2026
Integrantes:
Camilo Andrés Bonses Ariza
Néstor Fabián Castaño Ramírez
Derli Jibeth Guio Ayala
Daniel Eduardo Salgado Vitola

La pregunta que guía esta historia: ¿qué cambia cuando dejamos de mirar únicamente cuántas denuncias hay y empezamos a comparar tasas, tipos de delito, entorno urbano y percepción ciudadana?

Resumen ejecutivo

112
UPZ ANALIZADAS
162.835
DENUNCIAS (ENE–JUL 2023)
52,4%
SON HURTO A PERSONAS

No existe un único ranking capaz de describir toda la inseguridad. Estos son los cuatro resultados principales:

  • Volumen. La Sabana concentra más denuncias que cualquier otra UPZ (5.441), pero no tiene la tasa más alta.
  • Tasa. Entre las UPZ con al menos 5.000 habitantes, la tasa más alta es la de Zona Industrial (2.359,74 denuncias por cada 10.000 habitantes). En el ranking completo encabeza El Mochuelo, pero con solo 30 habitantes esa cifra no es comparable.
  • Tipo de delito. El hurto a personas y el hurto de celulares suman el 71,4% de los registros.
  • Percepción. La percepción de inseguridad sigue a la pobreza (rho = 0,74) y no a las denuncias: frente a la tasa, la relación incluso es negativa (rho = -0,32). Las denuncias y la percepción miran lugares distintos de la ciudad.

Introducción

La seguridad ciudadana en los entornos urbanos complejos constituye uno de los mayores desafíos para las políticas públicas territoriales. Históricamente, el análisis de la criminalidad se ha centrado primordialmente en los volúmenes absolutos de denuncias, una métrica que suele primar el tamaño poblacional o la concentración de actividades socioeconómicas por encima de la dinámica real del fenómeno.

El presente informe aborda la inseguridad en Bogotá desde una perspectiva multidimensional. A través del examen analítico de 112 Unidades de Planeamiento Zonal (UPZ) y 67 variables sintéticas, la investigación contrasta la frecuencia absoluta de denuncias con las tasas relativas por población, la composición tipológica de los delitos, la infraestructura de iluminación urbana y las dinámicas socioeconómicas. Asimismo, incorpora la lente de la percepción ciudadana capturada por la Encuesta Multipropósito, permitiendo evidenciar cómo las cifras administrativas y la experiencia cotidiana de los habitantes con frecuencia responden a factores territoriales opuestos.

1. Preguntas que organizan el análisis

  1. ¿Dónde se concentra el mayor volumen de denuncias y cómo cambia el panorama al usar tasas por población? (sección 4)
  2. ¿Qué tipos de delito explican la mayor parte de los registros? (sección 5)
  3. ¿La percepción de inseguridad coincide con las denuncias, en casos y en tasas? (sección 6)
  4. ¿Qué relación aparece entre percepción, pobreza y problemas reportados en el entorno? (sección 6)
  5. ¿Cómo se relacionan el alumbrado, los lotes oscuros y la distancia al CAI con las tasas de denuncia? (sección 7)
  6. ¿Qué patrones socioeconómicos acompañan las diferencias territoriales observadas? (sección 8)

La sección 10 responde estas seis preguntas una por una. El análisis combina cinco bloques de información: territorio, población, delitos, infraestructura y Encuesta Multipropósito. El detalle de las variables está en el Anexo B.

2. Antes de contar la historia: qué datos tenemos

La base contiene una fila por UPZ y reúne indicadores que no fueron medidos todos en el mismo año. Los registros delictivos corresponden a enero-julio de 2023; la población se basa en proyecciones de 2023, mientras que la Encuesta Multipropósito y parte de la información de alumbrado son de 2021.

Ficha rápida de la base
Indicador Resultado
UPZ (filas) 112
Variables 67
UPZ con población menor a 5.000 8
UPZ con encuesta agrupada 32
Celdas faltantes en toda la tabla 0

No hay celdas vacías, pero no todos los indicadores son mediciones independientes: en 32 UPZ los datos de la encuesta se reportan en conjunto con zonas vecinas. Además, en las 8 UPZ con menos de 5.000 habitantes, pocos casos pueden producir tasas extremas.

Cómo leer este informe

Tasas. Todas las tasas son denuncias por cada 10.000 habitantes acumuladas en siete meses (enero a julio de 2023). No son tasas anuales.

Correlaciones. El coeficiente rho de Spearman va de -1 a 1. Cerca de 0 indica que no hay relación; cerca de 1, que dos indicadores suben juntos; cerca de -1, que cuando uno sube el otro baja. Lo describimos como muy débil (menos de 0,2), débil, moderado, fuerte o muy fuerte (0,8 o más).

Asociación no es causa. Todas las relaciones de este informe comparan promedios por UPZ. Muestran qué indicadores se mueven juntos, no qué causa qué. Lo decimos aquí una vez para no repetirlo en cada gráfico.

3. Casos frente a tasas: dos mapas distintos de la inseguridad

3.1 ¿Dónde se acumulan las denuncias?

Barras horizontales con las diez UPZ con más denuncias entre enero y julio de 2023. La Sabana encabeza la lista.

HALLAZGO 1 · VOLUMEN

La Sabana encabeza los casos, pero eso no la convierte en la UPZ de mayor riesgo relativo.

Registra 5.441 denuncias para una población de 38.856 habitantes, es decir, una tasa de 1.400,30 por cada 10.000. El conteo sirve para localizar dónde se acumulan registros; para comparar zonas de tamaños distintos hace falta la tasa.

3.2 ¿Qué cambia al comparar tasas?

Al dividir los casos por la población, el ranking cambia por completo. El gráfico muestra las diez tasas más altas de toda la base y las diez más altas entre las UPZ con al menos 5.000 habitantes. Las barras grises son zonas con menos de 5.000 habitantes.

Barras horizontales con las tasas de denuncia más altas por cada 10.000 habitantes. Las UPZ con menos de 5.000 habitantes aparecen en gris; entre las demás, Zona Industrial tiene la tasa más alta.

El caso más extremo es El Mochuelo: 11.333,33 denuncias por cada 10.000 habitantes, con una población registrada de solo 30 personas y 34 casos. La cifra es aritméticamente correcta, pero con un denominador tan pequeño no se puede comparar con el resto. Por eso repetimos la comparación solo con UPZ de al menos 5.000 habitantes. Ese umbral es una decisión analítica para probar la sensibilidad del ranking, no un estándar oficial.

HALLAZGO 2 · COMPARACIÓN JUSTA

Entre las UPZ con al menos 5.000 habitantes, la tasa más alta es la de Zona Industrial: 2.359,74 por cada 10.000 habitantes.

La lista filtrada está dominada por zonas centrales y comerciales (Santa Fe, Los Mártires, Chapinero, Teusaquillo, Puente Aranda), donde mucha gente trabaja o transita aunque no viva allí. Esa población flotante genera denuncias que el denominador de residentes no cuenta.

3.3 La población explica parte del volumen, no toda la historia

Dos diagramas de dispersión con la población de cada UPZ en el eje horizontal. A la izquierda, los casos crecen con la población; a la derecha, la tasa tiende a bajar a medida que aumenta la población.

La asociación entre población y número de casos es moderada y positiva (rho = 0,56); la relación entre población y tasa es moderada y negativa (rho = -0,56). Las UPZ grandes y residenciales, como El Rincón, acumulan muchos casos pero tienen tasas bajas. Las zonas pequeñas o centrales muestran el patrón contrario.

Lo que aprendimos: no hay un único “top de inseguridad”. El ranking por casos muestra dónde se concentran los registros; el ranking por tasas muestra la frecuencia frente a la población residente, y debe leerse con cuidado en zonas pequeñas.

Siguiente pregunta: ¿qué tipo de delitos hay detrás de esas cifras?

4. Tipos de delito: qué compone los registros

Barras horizontales con el número de casos por tipo de delito. El hurto a personas supera ampliamente a las demás categorías.

HALLAZGO 3 · COMPOSICIÓN

El hurto a personas suma 85.398 registros, el 52,4% del total.

Junto con el hurto de celulares llega al 71,4%. Los registros están muy concentrados en delitos contra las personas y sus pertenencias en el espacio público. Eso indica frecuencia, no gravedad: los homicidios son el 0,4% de los casos, y hay delitos que no se denuncian ni se incluyen en la base.

Lo que aprendimos: el robo en la calle es el que define las cifras, y es justamente el tipo de delito que más se concentra en zonas de alto tránsito.

Siguiente pregunta: ¿las personas se sienten más inseguras donde hay más denuncias?

5. Percepción: ¿coincide con las denuncias?

La percepción de inseguridad proviene de la Encuesta Multipropósito; los casos y las tasas provienen de registros administrativos. Son dos lentes distintos sobre el mismo fenómeno. Siguiendo la lección de la sección 3, comparamos la percepción con las dos medidas: el número de casos y la tasa en las UPZ con al menos 5.000 habitantes.

Dos diagramas de dispersión con el porcentaje de personas que perciben inseguridad en el eje vertical. Frente a los casos no hay tendencia; frente a la tasa, la tendencia es ligeramente descendente.

Frente al número de casos, la correlación es muy débil y positiva (rho = 0,06): no hay relación. Frente a la tasa, la relación es débil y negativa (rho = -0,32). Es decir, las UPZ con más denuncias por habitante tienden a reportar algo menos de percepción de inseguridad, no más.

HALLAZGO 4 · DOS LENTES DISTINTOS

Donde más se denuncia no es donde más miedo se siente.

Las denuncias por habitante se concentran en zonas centrales y comerciales; la percepción de inseguridad, como se ve a continuación, es más alta en otro tipo de territorio. Ninguna de las dos medidas es falsa: cada una registra una parte distinta de la experiencia de inseguridad.

5.1 ¿Con qué se relaciona entonces la percepción?

Barras de correlación con la percepción de inseguridad. Pobreza, cercanía a expendios de droga y a basureros tienen correlación positiva; el salario promedio, negativa.

HALLAZGO 5 · EXPERIENCIA COTIDIANA

La percepción de inseguridad es más alta donde hay más pobreza y más problemas visibles en el barrio.

La relación más fuerte es con el ingreso: a mayor salario promedio, menor percepción (-0,79). Le siguen la pobreza (0,74), la cercanía a expendios de droga (0,67) y a basureros (0,61). La cercanía a lotes baldíos o sitios oscuros tiene una relación más débil (0,33). La percepción recoge cómo se vive el entorno cotidiano, algo que el conteo de denuncias no captura.

Lo que aprendimos: la percepción sigue a las condiciones sociales del barrio; las denuncias, a la actividad urbana. Por eso las dos medidas no coinciden.

Siguiente pregunta: ¿qué pasa con la infraestructura? ¿El alumbrado y la presencia policial se relacionan con las tasas?

6. Entorno urbano: alumbrado, lotes oscuros y CAI

Barras de correlación con la tasa de denuncias. Las luminarias por cada 1.000 habitantes tienen correlación fuerte y positiva; medidas por kilómetro cuadrado, casi nula. Los minutos al CAI y los lotes oscuros tienen correlación negativa.

Alumbrado. Las luminarias por cada 1.000 habitantes muestran una relación fuerte y positiva (rho = 0,78) con la tasa. Pero si las mismas luminarias se miden por km², la relación cae a 0,12. La diferencia se explica por el denominador común: las dos medidas se dividen por la población, y las UPZ con pocos residentes tienen a la vez muchas luminarias por habitante y tasas altas (la correlación entre luminarias por 1.000 habitantes y población es -0,70). El porcentaje de luminarias LED no muestra relación (-0,06).

Lotes oscuros y CAI. Las UPZ con más viviendas cerca de lotes oscuros (-0,37) y con más minutos de camino al CAI (-0,34) tienen tasas de denuncia más bajas. Esto no significa que estar lejos del CAI proteja: describe a las zonas periféricas y residenciales, que denuncian menos por habitante que las centrales. Los lotes oscuros, en cambio, sí aparecen asociados a una mayor percepción de inseguridad (sección 5.1). Es otro ejemplo de que denuncias y percepción miran lugares distintos.

HALLAZGO 6 · EVITAR UNA CONCLUSIÓN ENGAÑOSA

No sería correcto concluir que “más luminarias producen más delitos”.

La correlación fuerte desaparece casi por completo cuando el alumbrado se mide por superficie. Para evaluar el alumbrado como factor territorial harían falta datos del mismo periodo y controlar la actividad comercial y la población flotante.

Lo que aprendimos: la infraestructura repite el patrón de la sección 3: las tasas altas están en zonas centrales, bien iluminadas y cerca de los CAI, donde se concentra la actividad.

Siguiente pregunta: ¿se ve el mismo patrón en las condiciones socioeconómicas?

7. Condiciones socioeconómicas: el papel de la centralidad

Barras de correlación con la tasa de denuncias. Salario y años de educación tienen correlación positiva; pobreza y viviendas en estratos 1 y 2, negativa.

HALLAZGO 7 · CENTRALIDAD Y POBLACIÓN FLOTANTE

La tasa de denuncias sube con el ingreso y baja con la pobreza, justo al revés que la percepción.

La asociación más marcada es con el porcentaje de viviendas en estratos 1 y 2 (-0,62), seguida del salario promedio (0,58), los años de educación (0,55) y la pobreza (-0,52). Esto no significa que la pobreza proteja frente al delito. Las zonas centrales y comerciales de ingresos medios y altos reciben a diario a personas que no viven allí, y también pueden tener más disposición o facilidad para denunciar.

Lo que aprendimos: las denuncias por habitante dibujan una ciudad central y de mayores ingresos; la percepción, una ciudad periférica y con más pobreza.

Siguiente paso: reunir las respuestas a las seis preguntas iniciales.

8. Mapa para la toma de decisiones

El mapa reúne en un solo lugar las dos lentes del informe. Pase el cursor sobre una UPZ (o tóquela en el celular) para ver su ficha: población, casos, tasa y su puesto entre las 104 UPZ comparables, percepción de inseguridad, pobreza, delito principal y minutos al CAI. Con el selector de la esquina superior derecha puede cambiar el indicador que colorea el mapa y mostrar u ocultar los límites de las localidades.

La capa inicial, Tipología para decidir, clasifica cada UPZ con al menos 5.000 habitantes según si su tasa y su percepción están por encima o por debajo de la mediana (186,7 denuncias por cada 10.000 habitantes y 66,1% de personas que perciben inseguridad). Así se obtienen cuatro perfiles que piden respuestas distintas:

  • Prioridad integral (20 UPZ): tasa y percepción altas.
  • Foco en denuncias (32 UPZ): muchas denuncias por habitante, pero la gente se siente relativamente segura.
  • Foco en percepción (32 UPZ): pocas denuncias, pero alta sensación de inseguridad.
  • Seguimiento (20 UPZ): ambos indicadores por debajo de la mediana.
HALLAZGO 8 · TERRITORIO

Las denuncias se concentran en el centro; la percepción alta sin denuncias, en el sur

Las tasas bajan a medida que la UPZ se aleja del centro histórico (rho = -0,48 entre la distancia a la Plaza de Bolívar y la tasa), mientras que la percepción casi no depende de esa distancia (-0,16). Las UPZ de prioridad integral están sobre todo en Puente Aranda, Engativá, Kennedy; las de foco en denuncias, en Suba, Teusaquillo, Fontibón; y las de foco en percepción, en Ciudad Bolívar, Bosa, Usme.

La tipología es una herramienta para ordenar prioridades, no un diagnóstico: una UPZ cerca de la mediana puede cambiar de grupo con pequeñas variaciones en los datos.

Tipología de las UPZ para orientar decisiones
Tipología UPZ Qué indica Orientación UPZ incluidas (de mayor a menor tasa)
Prioridad integral 20 Tasa y percepción por encima de la mediana Combinar control del delito y mejoras del entorno Zona Industrial, La Sabana, Puente Aranda, Los Alcázares, Álamos, Restrepo, Las Cruces, Las Ferias, Santa Cecilia, Santa Isabel, Quiroga, Muzú, Fontibón San Pablo, San Rafael, Carvajal, Sosiego, Boyacá Real, Ciudad Montes, Corabastos, Marco Fidel Suárez
Foco en denuncias 32 Tasa alta, percepción baja: muchas denuncias en zonas de tránsito Reforzar el control del hurto en el espacio público Las Nieves, Sagrado Corazón, Chicó Lago, Chapinero, Teusaquillo, Galerías, La Candelaria, Los Andes, Capellanía, Quinta Paredes, La Macarena, Américas, La Alhambra, Usaquén, Granjas de Techo, Modelia, Ciudad Jardín, Doce de Octubre, Ciudad Salitre Occidental, Bavaria, Santa Bárbara, Ciudad Salitre Oriental, San José de Bavaria, La Esmeralda, La Academia, El Prado, San José, El Refugio, Kennedy Central, Minuto de Dios, La Floresta, Castilla
Foco en percepción 32 Tasa baja, percepción alta: el miedo no se refleja en denuncias Mejorar el entorno y la confianza; revisar subregistro Gran Britalia, Patio Bonito, San Francisco, Lucero, Garcés Navas, Tunjuelito, Las Margaritas, 20 de Julio, Bosa Central, El Tesoro, Venecia, El Rincón, Lourdes, Ciudad Usme, Apogeo, Monte Blanco, Comuneros, Engativá, Timiza, Bosa Occidental, Ismael Perdomo, Gran Yomasa, Diana Turbay, Jerusalem, Marruecos, La Gloria, La Flora, San Blas, Tibabuyes, Alfonso López, El Porvenir, Tintal Sur
Seguimiento 20 Tasa y percepción por debajo de la mediana Monitorear la evolución Niza, Fontibón, Bolivia, Country Club, La Uribe, Pardo Rubio, Arborizadora, Casa Blanca Suba, Toberín, Los Cedros, Britalia, Suba, Tintal Norte, Danubio, San Cristóbal Norte, Zona Franca, Calandaima, Los Libertadores, Verbenal, San Isidro - Patios
Menos de 5.000 habitantes 8 Tasa poco estable por el tamaño de la población Revisar caso a caso El Mochuelo, Aeropuerto El Dorado, Parque Simón Bolívar - CAN, Parque Salitre, Jardín Botánico, Paseo de los Libertadores, Guaymaral, Parque Entrenubes

9. Respuestas a las preguntas

PREGUNTA 1 · VOLUMEN Y TASAS

Depende de la medida: una UPZ lidera en casos y otra en tasa

La Sabana acumula más denuncias (5.441). Entre las UPZ con al menos 5.000 habitantes, la tasa más alta es la de Zona Industrial (2.359,74 por cada 10.000). La tasa de El Mochuelo (11.333,33) refleja sobre todo que solo tiene 30 habitantes.

Hallazgos 1 y 2 · sección 4

PREGUNTA 2 · TIPOS DE DELITO

El robo en la calle domina los registros

El hurto a personas aporta el 52,4% de los casos; sumado al hurto de celulares, el 71,4%.

Hallazgo 3 · sección 5

PREGUNTA 3 · PERCEPCIÓN Y DENUNCIAS

No coinciden

Frente a los casos, la correlación es prácticamente nula (0,06); frente a la tasa es negativa (-0,32). Donde más se denuncia por habitante, la percepción tiende a ser algo menor.

Hallazgo 4 · sección 6

PREGUNTA 4 · PERCEPCIÓN Y ENTORNO

La percepción sigue a la pobreza y a los problemas del barrio

Se asocia con la pobreza (0,74), la cercanía a expendios de droga (0,67) y a basureros (0,61), y baja con el salario promedio (-0,79).

Hallazgo 5 · sección 6.1

PREGUNTA 5 · ALUMBRADO, LOTES OSCUROS Y CAI

Las relaciones reflejan centralidad, no efectos de la infraestructura

Las luminarias por habitante se asocian con la tasa (0,78), pero medidas por km² la relación casi desaparece (0,12). Más minutos al CAI (-0,34) y más lotes oscuros (-0,37) acompañan tasas más bajas: describen zonas periféricas que denuncian menos.

Hallazgo 6 · sección 7

PREGUNTA 6 · CONDICIONES SOCIOECONÓMICAS

Las tasas son más altas en zonas de mayores ingresos

La tasa baja con la proporción de estratos 1 y 2 (-0,62) y sube con el salario (0,58). Es el patrón opuesto al de la percepción y apunta a la población flotante de las zonas centrales.

Hallazgo 7 · sección 8

La historia completa tiene dos caras. Los registros administrativos describen una ciudad central, comercial y de mayores ingresos, donde se acumulan las denuncias por habitante. La percepción ciudadana describe una ciudad periférica, con más pobreza y más problemas visibles en el barrio. Que no coincidan no significa que una de las dos esté equivocada: miden aspectos distintos de la inseguridad.

10. Limitaciones y decisiones de lectura

  • Denuncias no equivalen a todos los delitos ocurridos. El subregistro y la disposición a denunciar pueden variar entre territorios, y esa diferencia podría explicar parte del contraste entre zonas centrales y periféricas.
  • Periodo limitado. Los conteos cubren enero-julio de 2023: no son tasas anuales ni describen una tendencia en el tiempo.
  • Fuentes de años distintos. La población, la encuesta y los datos de alumbrado no tienen exactamente el mismo periodo de referencia.
  • Tasas sensibles a poblaciones pequeñas. El caso de El Mochuelo muestra por qué conviene mirar juntos casos, población y tasa.
  • Población residente, no flotante. El denominador cuenta a quienes viven en la UPZ, no a quienes trabajan o transitan por ella.
  • Indicadores con el mismo denominador. Dividir dos variables por la población puede crear correlaciones artificiales, como se vio con las luminarias.
  • Encuesta agrupada. En 32 UPZ los valores de la encuesta se comparten con zonas vecinas y no son independientes.
  • Correlación no implica causalidad. Las asociaciones pueden reflejar variables no incluidas, como usos del suelo, actividad comercial o movilidad.

11. Conclusiones

El análisis de las 112 UPZ muestra que la inseguridad territorial debe leerse desde más de una perspectiva. La Sabana concentra el mayor número de denuncias, Zona Industrial la tasa más alta entre las UPZ comparables, y el ranking completo de tasas exagera la posición de zonas con muy pocos habitantes. Una comparación responsable combina casos, población y tasas, en lugar de depender de una sola cifra.

Los registros están dominados por el robo en la calle: el hurto a personas y de celulares suma el 71,4% de las denuncias. Esas denuncias se concentran en zonas centrales y de mayores ingresos, mientras que la percepción de inseguridad es más alta donde hay más pobreza y más problemas en el entorno. Por eso las dos medidas no coinciden e incluso apuntan en direcciones opuestas. La inseguridad es a la vez una cifra policial y una experiencia territorial.

Las relaciones con el alumbrado, la distancia al CAI y las condiciones socioeconómicas describen ese mismo contraste entre centro y periferia. Merecen investigarse con datos más finos, pero no prueban relaciones de causa y efecto.

La principal conclusión metodológica es sencilla: los datos no cuentan una historia por sí solos; la historia aparece al comparar indicadores, comprobar los resultados y reconocer sus límites.


Anexos

Anexo A. Tablas de detalle

A.1 Diez UPZ con más denuncias registradas
UPZ Localidad Población Casos Tasa por 10.000
La Sabana Los Mártires 38.856 5.441 1.400,30
Chicó Lago Chapinero 38.608 4.985 1.291,18
Las Nieves Santa Fe 17.203 4.033 2.344,36
El Rincón Suba 295.322 3.812 129,08
Los Alcázares Barrios Unidos 44.678 3.495 782,26
Bosa Central Bosa 237.111 3.346 141,12
Las Ferias Engativá 100.848 3.139 311,26
Galerías Teusaquillo 38.425 3.067 798,18
Teusaquillo Teusaquillo 28.881 2.976 1.030,44
Chapinero Chapinero 26.491 2.901 1.095,09
A.2 Diez tasas más altas en el conjunto completo
UPZ Localidad Población Casos Tasa por 10.000
El Mochuelo Ciudad Bolívar 30 34 11.333,33
Aeropuerto El Dorado Fontibón 883 545 6.172,14
Parque Simón Bolívar - CAN Teusaquillo 3.323 958 2.882,94
Zona Industrial Puente Aranda 6.666 1.573 2.359,74
Las Nieves Santa Fe 17.203 4.033 2.344,36
Parque Salitre Barrios Unidos 1.442 233 1.615,81
Sagrado Corazón Santa Fe 11.247 1.707 1.517,74
La Sabana Los Mártires 38.856 5.441 1.400,30
Chicó Lago Chapinero 38.608 4.985 1.291,18
Puente Aranda Puente Aranda 12.482 1.561 1.250,60
A.3 Diez tasas más altas entre UPZ con al menos 5.000 habitantes
UPZ Localidad Población Casos Tasa por 10.000
Zona Industrial Puente Aranda 6.666 1.573 2.359,74
Las Nieves Santa Fe 17.203 4.033 2.344,36
Sagrado Corazón Santa Fe 11.247 1.707 1.517,74
La Sabana Los Mártires 38.856 5.441 1.400,30
Chicó Lago Chapinero 38.608 4.985 1.291,18
Puente Aranda Puente Aranda 12.482 1.561 1.250,60
Chapinero Chapinero 26.491 2.901 1.095,09
Teusaquillo Teusaquillo 28.881 2.976 1.030,44
Galerías Teusaquillo 38.425 3.067 798,18
Los Alcázares Barrios Unidos 44.678 3.495 782,26
A.4 Composición de los casos registrados
Tipo de delito Casos Participación
Hurto a personas 85.398 52,4%
Hurto de celulares 30.906 19,0%
Violencia intrafamiliar 16.053 9,9%
Lesiones personales 9.894 6,1%
Hurto a comercio 6.016 3,7%
Hurto de bicicletas 4.586 2,8%
Hurto a residencias 4.340 2,7%
Hurto de motos 2.863 1,8%
Hurto de automotores 2.180 1,3%
Homicidios 599 0,4%

Anexo B. Variables utilizadas

B.1 Variables principales del análisis
Variable Descripción
upz_nombre, localidad Nombre de la UPZ y de su localidad
poblacion Población proyectada 2023
casos_total, casos_* Denuncias totales y por tipo de delito, enero-julio de 2023
tasa_total Denuncias por cada 10.000 habitantes
luminarias_1000hab, luminarias_km2 Luminarias por cada 1.000 habitantes y por km²
pct_luminarias_led Porcentaje de luminarias con tecnología LED
min_cai_policia Minutos caminando al CAI de policía más cercano
pct_pobreza Porcentaje de personas en pobreza (Encuesta Multipropósito 2021)
salario_promedio Salario promedio (Encuesta Multipropósito 2021)
anios_educacion_15mas Años promedio de educación de personas de 15 años o más
pct_estrato_1_2 Porcentaje de viviendas en estratos 1 y 2
pct_percibe_inseguridad Porcentaje de personas que perciben inseguridad en su barrio
pct_cerca_ollas, pct_cerca_basureros, pct_cerca_lotes_oscuros Porcentaje de viviendas cerca de expendios de droga, basureros o lotes oscuros
em_agrupada Indica si los datos de encuesta se reportan agrupados con UPZ vecinas

Anexo C. Código

Ver el código R del informe
knitr::opts_chunk$set(
  echo=FALSE,
  warning=FALSE,
  message=FALSE,
  fig.align="center",
  fig.width=10,
  fig.height=5.8,
  dpi=130,
  out.width="100%"
)
library(tidyverse)
library(scales)
library(knitr)
azul <- "#0076B6"
oro <- "#FCB712"
navy <- "#172A46"
violeta <- "#7555A6"
rojo <- "#C64E52"
gris <- "#B8C2CC"

# ---- Formatos en español (miles con punto, decimales con coma) ----
fmt_n <- function(x) {
  scales::comma(
    x,
    accuracy=1,
    big.mark=".",
    decimal.mark=","
  )
}
fmt_d <- function(x, digits=2) {
  ifelse(
    is.na(x),
    "No disponible",
    formatC(
      x,
      format="f",
      digits=digits,
      big.mark=".",
      decimal.mark=","
    )
  )
}
fmt_pct <- function(x, digits=1) {
  paste0(fmt_d(x,digits),"%")
}
fmt_rho <- function(x) {
  if (is.na(x)) return("no calculable")
  fuerza <- if (abs(x) < .20) {
    "muy débil"
  } else if (abs(x) < .40) {
    "débil"
  } else if (abs(x) < .60) {
    "moderada"
  } else if (abs(x) < .80) {
    "fuerte"
  } else {
    "muy fuerte"
  }
  sentido <- if (x > 0) {
    "positiva"
  } else if (x < 0) {
    "negativa"
  } else {
    "nula"
  }
  paste0(
    fuerza, " y ", sentido,
    " (rho = ", fmt_d(x,2), ")"
  )
}
cor_spearman <- function(x,y) {
  ok <- is.finite(x) & is.finite(y)
  if (
    sum(ok) < 3 ||
    dplyr::n_distinct(x[ok]) < 2 ||
    dplyr::n_distinct(y[ok]) < 2
  ) {
    return(NA_real_)
  }
  suppressWarnings(
    cor(x[ok],y[ok],method="spearman")
  )
}

# ---- Tema comun de los graficos (SIN LINEAS DE CUADRICULA) ----
tema_informe <- function() {
  theme_minimal(base_size=12) +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      plot.title=element_text(face="bold",color=azul,size=15),
      plot.subtitle=element_text(color="#526174"),
      plot.title.position="plot",
      strip.text=element_text(face="bold",color=navy,hjust=0)
    )
}

# Barras de correlacion: azul = positiva, rojo = negativa, con el valor escrito
grafico_rho <- function(datos, titulo, subtitulo) {
  datos <- datos |>
    mutate(
      indicador=forcats::fct_reorder(indicador,rho),
      signo=ifelse(rho>=0,"Asociación positiva","Asociación negativa"),
      ajuste=ifelse(rho>=0,-0.15,1.15)
    )
  ggplot(datos,aes(x=indicador,y=rho,fill=signo)) +
    geom_col(width=.66) +
    geom_hline(yintercept=0,color=navy,linewidth=.4) +
    geom_text(
      aes(label=fmt_d(rho,2),hjust=ajuste),
      size=3.8,
      color=navy
    ) +
    coord_flip() +
    scale_fill_manual(
      values=c(
        "Asociación positiva"=azul,
        "Asociación negativa"=rojo
      ),
      name=NULL
    ) +
    scale_y_continuous(
      limits=c(-1.1,1.1),
      breaks=seq(-1,1,.5),
      labels=function(x) fmt_d(x,1)
    ) +
    labs(
      title=titulo,
      subtitle=subtitulo,
      x=NULL,
      y="Coeficiente rho de Spearman (de -1 a 1)"
    ) +
    tema_informe() +
    theme(
      legend.position="top",
      legend.justification="left",
      panel.grid = element_blank()
    )
}

# Etiquetas de puntos: usa ggrepel si esta instalado
capa_etiquetas <- function(datos) {
  if (requireNamespace("ggrepel",quietly=TRUE)) {
    ggrepel::geom_text_repel(
      data=datos,
      aes(label=upz_nombre),
      size=3.1,
      color=navy,
      min.segment.length=0,
      box.padding=.4,
      seed=1
    )
  } else {
    geom_text(
      data=datos,
      aes(label=upz_nombre),
      size=3.1,
      color=navy,
      vjust=-.9,
      check_overlap=TRUE
    )
  }
}

# Tendencia robusta (poco sensible a valores extremos)
capa_tendencia <- function() {
  geom_smooth(
    method="loess",
    formula=y ~ x,
    span=.9,
    method.args=list(family="symmetric"),
    se=FALSE,
    color=oro,
    linewidth=1
  )
}

# ---- Lectura de datos ----
candidatos_csv <- c(
  file.path("input","upz_variables.csv"),
  "upz_variables.csv",
  file.path("..","input","upz_variables.csv")
)
ruta_csv <- candidatos_csv[file.exists(candidatos_csv)][1]
if (is.na(ruta_csv)) {
  stop(
    "No encuentro input/upz_variables.csv. ",
    "Guarda el CSV dentro de la carpeta input del proyecto."
  )
}
upz <- readr::read_csv(
  ruta_csv,
  show_col_types=FALSE,
  na=c("", "NA", "NaN")
)
requeridas <- c(
  "upz_cod","upz_nombre","localidad",
  "poblacion","casos_total","tasa_total"
)
faltantes <- setdiff(requeridas,names(upz))
if (length(faltantes)>0) {
  stop(
    "Faltan columnas: ",
    paste(faltantes,collapse=", ")
  )
}

# ---- Nombres de UPZ con tildes y conectores en minuscula ----
correcciones_nombres <- c(
  "\\bDe Los\\b"="de los",
  "\\bDe\\b"="de",
  "\\bCristobal\\b"="Cristóbal",
  "\\bToberin\\b"="Toberín",
  "\\bUsaquen\\b"="Usaquén",
  "\\bBarbara\\b"="Bárbara",
  "\\bJose\\b"="José",
  "\\bRincon\\b"="Rincón",
  "\\bBoyaca\\b"="Boyacá",
  "\\bJardin\\b"="Jardín",
  "\\bMuzu\\b"="Muzú",
  "\\bAmericas\\b"="Américas",
  "\\bSuarez\\b"="Suárez",
  "\\bLopez\\b"="López",
  "\\bGarces\\b"="Garcés",
  "\\bEngativa\\b"="Engativá",
  "\\bFontibon\\b"="Fontibón",
  "\\bCorazon\\b"="Corazón",
  "\\bChico\\b"="Chicó",
  "\\bAlcazares\\b"="Alcázares",
  "\\bGalerias\\b"="Galerías",
  "\\bSimon\\b"="Simón",
  "\\bBolivar\\b"="Bolívar",
  "- Can$"="- CAN",
  "\\bBotanico\\b"="Botánico",
  "\\bCapellania\\b"="Capellanía",
  "\\bAlamos\\b"="Álamos"
)
upz <- upz |>
  mutate(
    across(where(is.character),trimws),
    upz_nombre=stringr::str_replace_all(upz_nombre,correcciones_nombres)
  )

# ---- Nombres legibles para los delitos ----
nombres_delitos <- c(
  hurto_personas="Hurto a personas",
  hurto_celulares="Hurto de celulares",
  violencia_intrafamiliar="Violencia intrafamiliar",
  lesiones="Lesiones personales",
  hurto_comercio="Hurto a comercio",
  hurto_bicicletas="Hurto de bicicletas",
  hurto_residencias="Hurto a residencias",
  hurto_motos="Hurto de motos",
  hurto_automotor="Hurto de automotores",
  homicidios="Homicidios"
)
etiqueta_delito <- function(x) {
  clave <- stringr::str_remove(x,"^casos_")
  ifelse(
    clave %in% names(nombres_delitos),
    nombres_delitos[clave],
    stringr::str_to_sentence(stringr::str_replace_all(clave,"_"," "))
  )
}

# ---- Indicadores principales ----
n_upz <- nrow(upz)
n_variables <- ncol(upz)
total_casos <- sum(upz$casos_total,na.rm=TRUE)
total_hurto_personas <- sum(upz$casos_hurto_personas,na.rm=TRUE)
total_hurto_celulares <- sum(upz$casos_hurto_celulares,na.rm=TRUE)
share_hurto_personas <- 100 * total_hurto_personas / total_casos
share_hurto_pc <- 100 * (total_hurto_personas + total_hurto_celulares) / total_casos
fila_casos <- upz |>
  slice_max(casos_total,n=1,with_ties=FALSE)
fila_tasa <- upz |>
  filter(is.finite(tasa_total),poblacion>0) |>
  slice_max(tasa_total,n=1,with_ties=FALSE)
upz_comparables <- upz |>
  filter(poblacion>=5000,is.finite(tasa_total))
fila_tasa_comp <- upz_comparables |>
  slice_max(tasa_total,n=1,with_ties=FALSE)
top_casos <- upz |>
  arrange(desc(casos_total)) |>
  slice_head(n=10)
top_tasas <- upz |>
  filter(is.finite(tasa_total),poblacion>0) |>
  arrange(desc(tasa_total)) |>
  slice_head(n=10)
top_tasas_comp <- upz_comparables |>
  arrange(desc(tasa_total)) |>
  slice_head(n=10)
crimenes <- grep("^casos_",names(upz),value=TRUE) |>
  setdiff("casos_total")
resumen_delitos <- upz |>
  summarise(across(all_of(crimenes),~sum(.x,na.rm=TRUE))) |>
  pivot_longer(everything(),names_to="delito",values_to="casos") |>
  arrange(desc(casos)) |>
  mutate(
    nombre=etiqueta_delito(delito),
    participacion=100*casos/sum(casos)
  )

# ---- Correlaciones (Spearman) ----
rho_poblacion_casos <- cor_spearman(upz$poblacion,upz$casos_total)
rho_poblacion_tasa <- cor_spearman(upz$poblacion,upz$tasa_total)
rho_casos_percepcion <- cor_spearman(upz$casos_total,upz$pct_percibe_inseguridad)
rho_tasacomp_percepcion <- cor_spearman(
  upz_comparables$tasa_total,
  upz_comparables$pct_percibe_inseguridad
)
rho_pobreza_percepcion <- cor_spearman(upz$pct_pobreza,upz$pct_percibe_inseguridad)
rho_ollas_percepcion <- cor_spearman(upz$pct_cerca_ollas,upz$pct_percibe_inseguridad)
rho_basureros_percepcion <- cor_spearman(upz$pct_cerca_basureros,upz$pct_percibe_inseguridad)
rho_salario_percepcion <- cor_spearman(upz$salario_promedio,upz$pct_percibe_inseguridad)
rho_lotes_percepcion <- cor_spearman(upz$pct_cerca_lotes_oscuros,upz$pct_percibe_inseguridad)
rho_luz_tasa <- cor_spearman(upz$luminarias_1000hab,upz$tasa_total)
rho_luzkm2_tasa <- cor_spearman(upz$luminarias_km2,upz$tasa_total)
rho_luz_poblacion <- cor_spearman(upz$luminarias_1000hab,upz$poblacion)
rho_led_tasa <- cor_spearman(upz$pct_luminarias_led,upz$tasa_total)
rho_lotes_tasa <- cor_spearman(upz$pct_cerca_lotes_oscuros,upz$tasa_total)
rho_cai_tasa <- cor_spearman(upz$min_cai_policia,upz$tasa_total)
rho_pobreza_tasa <- cor_spearman(upz$pct_pobreza,upz$tasa_total)
rho_salario_tasa <- cor_spearman(upz$salario_promedio,upz$tasa_total)
rho_educ_tasa <- cor_spearman(upz$anios_educacion_15mas,upz$tasa_total)
rho_estrato_tasa <- cor_spearman(upz$pct_estrato_1_2,upz$tasa_total)
n_pequenas <- sum(upz$poblacion<5000,na.rm=TRUE)
n_agrupadas <- if ("em_agrupada" %in% names(upz)) {
  sum(upz$em_agrupada %in% TRUE,na.rm=TRUE)
} else {
  NA_integer_
}
# ---- Tipologia para la toma de decisiones ----
mediana_tasa <- median(upz_comparables$tasa_total,na.rm=TRUE)
mediana_perc <- median(upz_comparables$pct_percibe_inseguridad,na.rm=TRUE)
niveles_tipo <- c(
  "Prioridad integral",
  "Foco en denuncias",
  "Foco en percepción",
  "Seguimiento",
  "Menos de 5.000 habitantes"
)
colores_tipo <- setNames(
  c(rojo,oro,violeta,"#8FB8D6","#D9DEE5"),
  niveles_tipo
)
que_indica_tipo <- setNames(
  c(
    "Tasa y percepción por encima de la mediana",
    "Tasa alta, percepción baja: muchas denuncias en zonas de tránsito",
    "Tasa baja, percepción alta: el miedo no se refleja en denuncias",
    "Tasa y percepción por debajo de la mediana",
    "Tasa poco estable por el tamaño de la población"
  ),
  niveles_tipo
)
orientacion_tipo <- setNames(
  c(
    "Combinar control del delito y mejoras del entorno",
    "Reforzar el control del hurto en el espacio público",
    "Mejorar el entorno y la confianza; revisar subregistro",
    "Monitorear la evolución",
    "Revisar caso a caso"
  ),
  niveles_tipo
)
norm_llave <- function(x) {
  d <- stringr::str_extract(as.character(x),"[0-9]+")
  ifelse(is.na(d),NA_character_,as.character(as.integer(d)))
}
mat_delitos <- as.matrix(upz[,crimenes])
idx_delito <- max.col(mat_delitos,ties.method="first")
upz_mapa <- upz |>
  mutate(
    llave_union=norm_llave(upz_cod),
    comparable=poblacion>=5000 & is.finite(tasa_total),
    tipo=case_when(
      !comparable ~ niveles_tipo[5],
      tasa_total>=mediana_tasa & pct_percibe_inseguridad>=mediana_perc ~ niveles_tipo[1],
      tasa_total>=mediana_tasa ~ niveles_tipo[2],
      pct_percibe_inseguridad>=mediana_perc ~ niveles_tipo[3],
      TRUE ~ niveles_tipo[4]
    ),
    tipo=factor(tipo,levels=niveles_tipo),
    puesto_tasa=dplyr::min_rank(dplyr::desc(ifelse(comparable,tasa_total,NA))),
    delito_principal=unname(etiqueta_delito(crimenes[idx_delito])),
    pct_delito_principal=100*mat_delitos[cbind(seq_along(idx_delito),idx_delito)]/casos_total,
    agrupada=if ("em_agrupada" %in% names(upz)) em_agrupada %in% TRUE else FALSE
  )
n_comparables <- sum(upz_mapa$comparable)

mapa_sf <- NULL
localidades_sf <- NULL
mapa_archivo <- NA_character_
leer_shp <- function(archivo, ...) {
  capa <- tryCatch(
    sf::st_read(archivo,quiet=TRUE,...),
    error=function(e) NULL
  )
  if (is.null(capa) || nrow(capa)==0) return(NULL)
  if (is.na(sf::st_crs(capa))) {
    caja <- sf::st_bbox(capa)
    en_grados <- all(abs(caja[c("xmin","xmax")])<=180) &&
      all(abs(caja[c("ymin","ymax")])<=90)
    if (!en_grados) return(NULL)
    capa <- sf::st_set_crs(capa,4326)
  }
  capa |>
    sf::st_make_valid() |>
    sf::st_transform(3116) |>
    sf::st_simplify(preserveTopology=TRUE,dTolerance=15) |>
    sf::st_transform(4326)
}
if (requireNamespace("sf",quietly=TRUE)) {
  carpetas_shp <- c(
    file.path("input","shp"),
    "shp",
    file.path("..","input","shp")
  )
  archivos_shp <- unlist(lapply(
    carpetas_shp[dir.exists(carpetas_shp)],
    list.files,
    pattern="\\.shp$",
    recursive=TRUE,
    full.names=TRUE,
    ignore.case=TRUE
  ))
  es_localidad <- grepl("localidad",basename(archivos_shp),ignore.case=TRUE)
  es_upz <- grepl("upz",basename(archivos_shp),ignore.case=TRUE)
  archivos_upz <- c(
    archivos_shp[es_upz & !es_localidad],
    archivos_shp[!es_upz & !es_localidad]
  )
  posibles <- c(
    "UPZCOD","CODUPZ","CODIGOUPZ","UPZCODIGO",
    "UPLCODIGO","CODIGOUPL","CODUPZ1","UPZ"
  )
  for (archivo in archivos_upz) {
    candidato <- leer_shp(archivo)
    if (is.null(candidato)) next
    nms <- names(candidato)
    nms_norm <- gsub("[^A-Z0-9]","",toupper(nms))
    encontrado <- posibles[posibles %in% nms_norm][1]
    if (is.na(encontrado)) next
    llave <- nms[match(encontrado,nms_norm)]
    candidato$llave_union <- norm_llave(candidato[[llave]])
    candidato <- candidato[,"llave_union"]
    unido <- dplyr::inner_join(candidato,upz_mapa,by="llave_union")
    if (nrow(unido)<10) next
    mapa_sf <- unido
    mapa_archivo <- basename(archivo)
    break
  }
  archivo_loc <- archivos_shp[es_localidad][1]
  if (!is.na(archivo_loc)) {
    localidades_sf <- leer_shp(archivo_loc,options="ENCODING=WINDOWS-1252")
    if (!is.null(localidades_sf)) {
      col_nombre <- names(localidades_sf)[
        toupper(names(localidades_sf)) %in% c("LOCALIDAD","LOCNOMBRE","NOMBRE")
      ][1]
      localidades_sf$nombre_loc <- if (is.na(col_nombre)) NA_character_ else localidades_sf[[col_nombre]]
    }
  }
}
mapa_disponible <- !is.null(mapa_sf)
num <- function(base) {
  if (mapa_disponible || base < 9) base else base - 1L
}
cat(
  "```{=html}\n",
  '<div class="cover">\n',
  '<div class="eyebrow">Análisis territorial · Bogotá</div>\n',
  '<div class="cover-title">BOGOTÁ BAJO LA LUPA</div>\n',
  '<div class="cover-subtitle">La inseguridad no se distribuye de una sola manera: ',
  'qué revelan las denuncias, el territorio y la percepción ciudadana.</div>\n',
  '<div class="meta">',
  fmt_n(n_upz), ' UPZ · ', fmt_n(n_variables),
  ' variables · Denuncias de enero a julio de 2023<br>\n',
  'Visualización de Datos · Konrad Lorenz · Octubre de 2026</div>\n',
  '<div class="integrantes-sec">\n',
  '<strong>Integrantes:</strong>\n',
  '<div class="integrantes-list">\n',
  '<div>Camilo Andrés Bonses Ariza</div>\n',
  '<div>Néstor Fabián Castaño Ramírez</div>\n',
  '<div>Derli Jibeth Guio Ayala</div>\n',
  '<div>Daniel Eduardo Salgado Vitola</div>\n',
  '</div>\n',
  '</div>\n',
  '</div>\n',
  "```\n",
  sep=""
)
item_menu <- function(id, etiqueta, nombre) {
  paste0(
    '<a class="menu-link" href="#', id, '">',
    '<span class="menu-number">', etiqueta, '</span>',
    '<span class="menu-name">', nombre, '</span></a>'
  )
}
n2 <- function(x) sprintf("%02d",x)
items <- c(
  item_menu("resumen-ejecutivo","Inicio","Resumen ejecutivo"),
  item_menu("introduccion","01","Introducción"),
  item_menu("preguntas",paste0(n2(2),"–",n2(3)),"Preguntas y datos"),
  item_menu("casos-y-tasas",n2(4),"Casos frente a tasas"),
  item_menu("tipos-de-delito",n2(5),"Tipos de delito"),
  item_menu("percepcion",n2(6),"Percepción"),
  item_menu("entorno-urbano",n2(7),"Entorno urbano"),
  item_menu("condiciones-socioeconomicas",n2(8),"Condiciones socioeconómicas"),
  if (mapa_disponible) item_menu("mapa-upz",n2(9),"Mapa para decidir"),
  item_menu("respuestas",n2(num(10)),"Respuestas a las preguntas"),
  item_menu("limitaciones",n2(num(11)),"Limitaciones"),
  item_menu("conclusiones",n2(num(12)),"Conclusiones")
)
cat(
  "```{=html}\n",
  '<nav class="report-menu">\n',
  '<div class="menu-title">Explora el informe</div>\n',
  '<p class="menu-subtitle">Una ruta de lectura: del panorama general a los hallazgos y sus límites. ',
  'Los números corresponden a las secciones.</p>\n',
  '<div class="menu-grid">\n',
  paste(items,collapse="\n"),
  '\n</div>\n</nav>\n',
  "```\n",
  sep=""
)
cat(
  "```{=html}\n",
  '<div class="kpi-grid">\n',
  '<div class="kpi"><div class="value">', fmt_n(n_upz),
  '</div><div class="label">UPZ ANALIZADAS</div></div>\n',
  '<div class="kpi"><div class="value">', fmt_n(total_casos),
  '</div><div class="label">DENUNCIAS (ENE–JUL 2023)</div></div>\n',
  '<div class="kpi"><div class="value">', fmt_pct(share_hurto_personas,1),
  '</div><div class="label">SON HURTO A PERSONAS</div></div>\n',
  '</div>\n',
  "```\n",
  sep=""
)
calidad <- tibble(
  indicador=c(
    "UPZ (filas)",
    "Variables",
    "UPZ con población menor a 5.000",
    "UPZ con encuesta agrupada",
    "Celdas faltantes en toda la tabla"
  ),
  resultado=c(
    fmt_n(n_upz),
    fmt_n(n_variables),
    fmt_n(n_pequenas),
    ifelse(is.na(n_agrupadas),"No disponible",fmt_n(n_agrupadas)),
    fmt_n(sum(is.na(upz)))
  )
)
knitr::kable(
  calidad,
  col.names=c("Indicador","Resultado"),
  align=c("l","r"),
  caption="Ficha rápida de la base"
)
ggplot(
  top_casos,
  aes(x = reorder(upz_nombre, -casos_total), y = casos_total)
) +
  geom_col(fill = azul, width = .72) +
  geom_text(
    aes(label = fmt_n(casos_total)),
    vjust = -0.4,
    size = 3.6,
    color = navy
  ) +
  scale_y_continuous(
    labels = fmt_n,
    expand = expansion(mult = c(0, .14))
  ) +
  labs(
    title = paste(fila_casos$upz_nombre, "lidera el volumen de denuncias"),
    subtitle = "Diez UPZ con más casos registrados entre enero y julio de 2023",
    x = NULL,
    y = "Número de denuncias"
  ) +
  tema_informe() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

top_tasas_union <- bind_rows(top_tasas,top_tasas_comp) |>
  distinct(upz_cod,.keep_all=TRUE) |>
  mutate(
    grupo=ifelse(
      poblacion<5000,
      "Menos de 5.000 habitantes",
      "5.000 habitantes o más"
    ),
    etiqueta=paste0(upz_nombre," (",fmt_n(poblacion)," hab.)")
  )
ggplot(
  top_tasas_union,
  aes(x=reorder(etiqueta,tasa_total),y=tasa_total,fill=grupo)
) +
  geom_col(width=.72) +
  coord_flip() +
  geom_text(
    aes(label=fmt_d(tasa_total)),
    hjust=-.12,
    size=3.3,
    color=navy
  ) +
  scale_fill_manual(
    values=c(
      "Menos de 5.000 habitantes"=gris,
      "5.000 habitantes o más"=oro
    ),
    name=NULL
  ) +
  scale_y_continuous(
    labels=fmt_n,
    expand=expansion(mult=c(0,.16))
  ) +
  labs(
    title=paste0(
      "Sin las zonas muy pequeñas, ",
      fila_tasa_comp$upz_nombre,
      " pasa a encabezar las tasas"
    ),
    subtitle="Denuncias por cada 10.000 habitantes, enero a julio de 2023. Entre paréntesis, la población de cada UPZ",
    x=NULL,
    y="Denuncias por cada 10.000 habitantes"
  ) +
  tema_informe() +
  theme(legend.position="top",legend.justification="left")
rel_poblacion <- bind_rows(
  upz |>
    transmute(
      upz_nombre,poblacion,
      valor=casos_total,
      medida=paste0("Casos denunciados (rho = ",fmt_d(rho_poblacion_casos),")")
    ),
  upz |>
    transmute(
      upz_nombre,poblacion,
      valor=tasa_total,
      medida=paste0("Tasa por 10.000 habitantes (rho = ",fmt_d(rho_poblacion_tasa),")")
    )
) |>
  mutate(medida=factor(medida,levels=unique(medida)))
etiquetas_poblacion <- rel_poblacion |>
  group_by(medida) |>
  slice_max(valor,n=4,with_ties=FALSE) |>
  ungroup()
ggplot(rel_poblacion,aes(x=poblacion,y=valor)) +
  geom_point(color=azul,alpha=.7,size=2.2) +
  capa_tendencia() +
  capa_etiquetas(etiquetas_poblacion) +
  facet_wrap(~medida,scales="free_y") +
  scale_x_continuous(labels=fmt_n) +
  scale_y_continuous(labels=fmt_n) +
  labs(
    title="Más población significa más casos, pero no una tasa más alta",
    subtitle="Cada punto es una UPZ. La línea amarilla resume la tendencia",
    x="Población estimada",
    y=NULL
  ) +
  tema_informe()
ggplot(
  resumen_delitos,
  aes(
    x=reorder(nombre,casos),
    y=casos,
    fill=nombre==resumen_delitos$nombre[1]
  )
) +
  geom_col(show.legend=FALSE,width=.72) +
  geom_text(
    aes(label=paste0(fmt_n(casos)," · ",fmt_pct(participacion,1))),
    hjust=-.08,
    size=3.4,
    color=navy
  ) +
  coord_flip() +
  scale_fill_manual(values=c("FALSE"="#9DB8C9","TRUE"=azul)) +
  scale_y_continuous(
    labels=fmt_n,
    expand=expansion(mult=c(0,.22))
  ) +
  labs(
    title="El hurto a personas aporta más de la mitad de los casos registrados",
    subtitle="Casos y participación en el total de denuncias, enero a julio de 2023",
    x=NULL,
    y="Casos denunciados"
  ) +
  tema_informe()
rel_percepcion <- bind_rows(
  upz |>
    transmute(
      upz_nombre,
      x=casos_total,
      y=pct_percibe_inseguridad,
      medida=paste0("Casos denunciados (rho = ",fmt_d(rho_casos_percepcion),")")
    ),
  upz_comparables |>
    transmute(
      upz_nombre,
      x=tasa_total,
      y=pct_percibe_inseguridad,
      medida=paste0("Tasa por 10.000 hab., UPZ con 5.000 o más (rho = ",fmt_d(rho_tasacomp_percepcion),")")
    )
) |>
  mutate(medida=factor(medida,levels=unique(medida)))
ggplot(rel_percepcion,aes(x=x,y=y)) +
  geom_point(color=azul,alpha=.7,size=2.2) +
  capa_tendencia() +
  facet_wrap(~medida,scales="free_x",labeller=label_wrap_gen(46)) +
  scale_x_continuous(labels=fmt_n) +
  scale_y_continuous(labels=function(x) paste0(x,"%")) +
  labs(
    title="La percepción no sube con las denuncias; frente a la tasa, incluso baja",
    subtitle="Cada punto es una UPZ. La línea amarilla resume la tendencia",
    x=NULL,
    y="Personas que perciben inseguridad"
  ) +
  tema_informe()
upz <- read_csv("upz_variables.csv")

mat_cor <- upz |>
  select(
    `Percepción de inseguridad` = pct_percibe_inseguridad,
    `Pobreza`                   = pct_pobreza,
    `Cerca de expendios de droga` = pct_cerca_ollas,
    `Cerca de basureros`        = pct_cerca_basureros,
    `Cerca de lotes baldíos`    = pct_cerca_lotes_oscuros,
    `Salario promedio`          = salario_promedio
  ) |>
  cor(method = "spearman", use = "pairwise.complete.obs")

orden <- colnames(mat_cor)

cor_larga <- mat_cor |>
  as.data.frame() |>
  rownames_to_column("var1") |>
  pivot_longer(-var1, names_to = "var2", values_to = "rho") |>
  mutate(
    var1 = factor(var1, levels = orden),
    var2 = factor(var2, levels = orden)
  ) |>
  filter(as.integer(var1) < as.integer(var2))

ggplot(cor_larga, aes(var2, var1, fill = rho)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(
    aes(label = format(round(rho, 2), decimal.mark = ",")),
    size = 3.8, fontface = "bold"
  ) +
  scale_fill_gradient2(
    low = "#C44E52", mid = "white", high = "#0077B6",
    midpoint = 0, limits = c(-1, 1), name = "rho de\nSpearman"
  ) +
  scale_x_discrete(position = "top", labels = label_wrap(14)) +
  scale_y_discrete(limits = rev, labels = label_wrap(28)) +
  labs(
    title = "Matriz de correlación de Spearman (entorno y percepción)",
    subtitle = "112 UPZ de Bogotá",
    x = NULL, y = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(
    panel.grid  = element_blank(),
    axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0, lineheight = 0.9),
    plot.title  = element_text(face = "bold", color = "#0077B6"),
    plot.title.position = "plot",
    plot.margin = margin(10, 15, 10, 10)
  )
cor_entorno <- tibble(
  indicador=c(
    "Luminarias por 1.000 habitantes",
    "Luminarias por km²",
    "Porcentaje de luminarias LED",
    "Viviendas cerca de lotes oscuros (%)",
    "Minutos caminando al CAI"
  ),
  rho=c(
    rho_luz_tasa,
    rho_luzkm2_tasa,
    rho_led_tasa,
    rho_lotes_tasa,
    rho_cai_tasa
  )
)
grafico_rho(
  cor_entorno,
  "La relación del alumbrado depende de cómo se mida",
  "Correlación de cada indicador con la tasa total de denuncias"
)
cor_socio <- tibble(
  indicador=c(
    "Pobreza (%)",
    "Salario promedio",
    "Años de educación",
    "Viviendas en estratos 1–2 (%)"
  ),
  rho=c(
    rho_pobreza_tasa,
    rho_salario_tasa,
    rho_educ_tasa,
    rho_estrato_tasa
  )
)
grafico_rho(
  cor_socio,
  "Las tasas de denuncia son más altas en las UPZ de mayores ingresos",
  "Correlación de cada indicador con la tasa total de denuncias"
)
conteo_tipo <- table(mapa_sf$tipo)
cat(
  "## 8. Mapa para la toma de decisiones {#mapa-upz}\n\n",
  "El mapa reúne en un solo lugar las dos lentes del informe. ",
  "**Pase el cursor sobre una UPZ** (o tóquela en el celular) para ver su ficha: población, casos, tasa y su puesto entre las ",
  n_comparables, " UPZ comparables, percepción de inseguridad, pobreza, delito principal y minutos al CAI. ",
  "Con el selector de la esquina superior derecha puede cambiar el indicador que colorea el mapa y mostrar u ocultar ",
  "los límites de las localidades.\n\n",
  "La capa inicial, **Tipología para decidir**, clasifica cada UPZ con al menos 5.000 habitantes según si su tasa ",
  "y su percepción están por encima o por debajo de la mediana (", fmt_d(mediana_tasa,1),
  " denuncias por cada 10.000 habitantes y ", fmt_pct(mediana_perc,1), " de personas que perciben inseguridad). ",
  "Así se obtienen cuatro perfiles que piden respuestas distintas:\n\n",
  "- **Prioridad integral** (", conteo_tipo[[niveles_tipo[1]]], " UPZ): tasa y percepción altas.\n",
  "- **Foco en denuncias** (", conteo_tipo[[niveles_tipo[2]]], " UPZ): muchas denuncias por habitante, pero la gente se siente relativamente segura.\n",
  "- **Foco en percepción** (", conteo_tipo[[niveles_tipo[3]]], " UPZ): pocas denuncias, pero alta sensación de inseguridad.\n",
  "- **Seguimiento** (", conteo_tipo[[niveles_tipo[4]]], " UPZ): ambos indicadores por debajo de la mediana.\n\n",
  sep=""
)
fila_html <- function(etiqueta, valor) {
  paste0(
    '<tr><td style="color:#64748b;padding:1px 14px 1px 0">', etiqueta,
    '</td><td style="text-align:right;font-weight:600">', valor, '</td></tr>'
  )
}
datos_mapa <- mapa_sf |>
  mutate(
    color_tipo=unname(colores_tipo[as.character(tipo)]),
    ficha=paste0(
      '<div style="font-size:13px;line-height:1.45;min-width:240px">',
      '<div style="font-size:15px;font-weight:800;color:#172A46">', upz_nombre, '</div>',
      '<div style="color:#64748b;margin-bottom:6px">', localidad, '</div>',
      '<div style="border-left:5px solid ', color_tipo, ';padding-left:8px;margin-bottom:8px">',
      '<div style="font-weight:800">', tipo, '</div>',
      '<div style="color:#526174;font-size:12px">', unname(orientacion_tipo[as.character(tipo)]), '</div></div>',
      '<table>',
      fila_html("Población", fmt_n(poblacion)),
      fila_html("Casos (ene–jul 2023)", fmt_n(casos_total)),
      fila_html("Tasa por 10.000 hab.", fmt_d(tasa_total,1)),
      fila_html(
        "Puesto en tasa",
        ifelse(comparable,paste0(puesto_tasa," de ",n_comparables),"No comparable")
      ),
      fila_html("Percibe inseguridad", fmt_pct(pct_percibe_inseguridad,1)),
      fila_html("Pobreza", fmt_pct(pct_pobreza,1)),
      fila_html(
        "Delito principal",
        paste0(delito_principal," (",fmt_pct(pct_delito_principal,0),")")
      ),
      fila_html("Minutos al CAI", fmt_d(min_cai_policia,0)),
      '</table>',
      ifelse(
        comparable,
        '',
        '<div style="color:#C64E52;margin-top:6px;font-size:12px">Menos de 5.000 habitantes: la tasa es poco estable.</div>'
      ),
      ifelse(
        agrupada,
        '<div style="color:#64748b;margin-top:4px;font-size:12px">Datos de encuesta agrupados con UPZ vecinas.</div>',
        ''
      ),
      '</div>'
    )
  )
if (requireNamespace("leaflet",quietly=TRUE)) {
  cortes_quintil <- function(x) {
    q <- quantile(x,seq(0,1,.2),na.rm=TRUE)
    q[1] <- floor(q[1])
    q[length(q)] <- ceiling(q[length(q)])
    unique(round(q))
  }
  azules <- c("#E6F2F8","#A9D0E8","#5FA8D3","#0076B6","#0B3C6B")
  violetas <- c("#F1ECF7","#CDBDE3","#A68BCB","#7555A6","#3F2A63")
  naranjas <- c("#FDF0E1","#F8C99A","#F09C55","#D9711F","#9A4A0E")
  pal_tipo <- leaflet::colorFactor(unname(colores_tipo),levels=niveles_tipo)
  pal_tasa <- leaflet::colorBin(azules,domain=datos_mapa$tasa_total,bins=cortes_quintil(datos_mapa$tasa_total),na.color="#D9DEE5")
  pal_casos <- leaflet::colorBin(azules,domain=datos_mapa$casos_total,bins=cortes_quintil(datos_mapa$casos_total),na.color="#D9DEE5")
  pal_perc <- leaflet::colorBin(violetas,domain=datos_mapa$pct_percibe_inseguridad,bins=cortes_quintil(datos_mapa$pct_percibe_inseguridad),na.color="#D9DEE5")
  pal_pobreza <- leaflet::colorBin(naranjas,domain=datos_mapa$pct_pobreza,bins=cortes_quintil(datos_mapa$pct_pobreza),na.color="#D9DEE5")
  grupos <- c(
    "Tipología para decidir",
    "Tasa de denuncias",
    "Casos denunciados",
    "Percepción de inseguridad",
    "Pobreza"
  )
  grupo_localidades <- "Límites de localidades"
  grupo_pequenas <- "UPZ con menos de 5.000 hab."
  etiquetas <- lapply(datos_mapa$ficha,htmltools::HTML)
  capa_upz <- function(mapa, colores, grupo) {
    leaflet::addPolygons(
      mapa,
      data=datos_mapa,
      fillColor=colores,
      fillOpacity=.8,
      color="white",
      weight=.8,
      opacity=1,
      group=grupo,
      label=etiquetas,
      popup=datos_mapa$ficha,
      labelOptions=leaflet::labelOptions(
        direction="auto",
        sticky=TRUE,
        style=list(
          "padding"="8px 10px",
          "box-shadow"="0 4px 14px rgba(23,42,70,.18)",
          "border-radius"="8px"
        )
      ),
      highlightOptions=leaflet::highlightOptions(
        weight=3,
        color=navy,
        opacity=1,
        fillOpacity=.92,
        bringToFront=TRUE
      ),
      options=leaflet::pathOptions(pane="upz")
    )
  }
  leyenda <- function(mapa, i, ...) {
    leaflet::addLegend(
      mapa,
      position="bottomright",
      opacity=.9,
      className=paste0("info legend leyenda-",i),
      ...
    )
  }
  mapa <- leaflet::leaflet(
    width="100%",
    height=640,
    options=leaflet::leafletOptions(minZoom=10,zoomSnap=.5)
  ) |>
    leaflet::addMapPane("upz",zIndex=410) |>
    leaflet::addMapPane("limites",zIndex=420) |>
    leaflet::addProviderTiles(leaflet::providers$OpenStreetMap.Mapnik) |>
    capa_upz(pal_tipo(datos_mapa$tipo),grupos[1]) |>
    capa_upz(pal_tasa(datos_mapa$tasa_total),grupos[2]) |>
    capa_upz(pal_casos(datos_mapa$casos_total),grupos[3]) |>
    capa_upz(pal_perc(datos_mapa$pct_percibe_inseguridad),grupos[4]) |>
    capa_upz(pal_pobreza(datos_mapa$pct_pobreza),grupos[5]) |>
    leyenda(
      0,
      colors=unname(colores_tipo),
      labels=paste0(niveles_tipo," (",as.integer(table(datos_mapa$tipo)[niveles_tipo]),")"),
      title="Tipología"
    ) |>
    leyenda(
      1,
      pal=pal_tasa,
      values=datos_mapa$tasa_total,
      title="Tasa por 10.000 hab.<br><small>quintiles</small>",
      labFormat=leaflet::labelFormat(big.mark=".",digits=0)
    ) |>
    leyenda(
      2,
      pal=pal_casos,
      values=datos_mapa$casos_total,
      title="Casos denunciados<br><small>quintiles</small>",
      labFormat=leaflet::labelFormat(big.mark=".",digits=0)
    ) |>
    leyenda(
      3,
      pal=pal_perc,
      values=datos_mapa$pct_percibe_inseguridad,
      title="Percibe inseguridad<br><small>quintiles</small>",
      labFormat=leaflet::labelFormat(suffix="%",digits=0)
    ) |>
    leyenda(
      4,
      pal=pal_pobreza,
      values=datos_mapa$pct_pobreza,
      title="Pobreza<br><small>quintiles</small>",
      labFormat=leaflet::labelFormat(suffix="%",digits=0)
    ) |>
    leaflet::addPolygons(
      data=datos_mapa[!datos_mapa$comparable,],
      fill=FALSE,
      color=navy,
      weight=1.6,
      dashArray="4,4",
      group=grupo_pequenas,
      options=leaflet::pathOptions(pane="limites",clickable=FALSE,pointerEvents="none")
    )
  overlays <- grupo_pequenas
  if (!is.null(localidades_sf)) {
    puntos_loc <- localidades_sf |>
      sf::st_transform(3116) |>
      sf::st_point_on_surface() |>
      sf::st_transform(4326)
    coords_loc <- sf::st_coordinates(puntos_loc)
    mapa <- mapa |>
      leaflet::addPolygons(
        data=localidades_sf,
        fill=FALSE,
        color=navy,
        weight=2,
        opacity=.75,
        group=grupo_localidades,
        options=leaflet::pathOptions(pane="limites",clickable=FALSE,pointerEvents="none")
      ) |>
      leaflet::addLabelOnlyMarkers(
        lng=coords_loc[,1],
        lat=coords_loc[,2],
        label=localidades_sf$nombre_loc,
        group=grupo_localidades,
        labelOptions=leaflet::labelOptions(
          noHide=TRUE,
          textOnly=TRUE,
          direction="center",
          style=list(
            "font-weight"="700",
            "font-size"="11px",
            "color"=navy,
            "text-shadow"="0 0 3px #fff, 0 0 3px #fff"
          )
        )
      )
    overlays <- c(grupo_localidades,overlays)
  }
  mapa <- mapa |>
    leaflet::addLayersControl(
      baseGroups=grupos,
      overlayGroups=overlays,
      position="topright",
      options=leaflet::layersControlOptions(collapsed=FALSE)
    ) |>
    leaflet::hideGroup(grupos[-1]) |>
    leaflet::addScaleBar(
      position="bottomleft",
      options=leaflet::scaleBarOptions(imperial=FALSE)
    )
  htmlwidgets::onRender(
    mapa,
    paste0(
      "function(el, x) {",
      "  var map = this;",
      "  var grupos = ", jsonlite::toJSON(grupos), ";",
      "  function mostrar(nombre) {",
      "    grupos.forEach(function(g, i) {",
      "      el.querySelectorAll('.leyenda-' + i).forEach(function(l) {",
      "        l.style.display = (g === nombre) ? '' : 'none';",
      "      });",
      "    });",
      "  }",
      "  mostrar(grupos[0]);",
      "  map.on('baselayerchange', function(e) { mostrar(e.name); });",
      "}"
    )
  )
} else {
  ggplot(datos_mapa) +
    geom_sf(aes(fill=tipo),color="white",linewidth=.15) +
    scale_fill_manual(values=colores_tipo,drop=FALSE,name=NULL) +
    labs(
      title="Tipología de las UPZ según tasa y percepción",
      subtitle="Instale el paquete leaflet para ver la versión interactiva"
    ) +
    theme_void(base_size=12) +
    theme(
      plot.title=element_text(face="bold",color=azul,size=16),
      legend.position="right"
    )
}
centro_historico <- sf::st_sfc(sf::st_point(c(-74.0760,4.5981)),crs=4326) |>
  sf::st_transform(3116)
puntos_upz <- mapa_sf |>
  sf::st_geometry() |>
  sf::st_transform(3116) |>
  sf::st_point_on_surface()
dist_centro_km <- as.numeric(sf::st_distance(puntos_upz,centro_historico))/1000
rho_dist_tasa <- cor_spearman(
  dist_centro_km[mapa_sf$comparable],
  mapa_sf$tasa_total[mapa_sf$comparable]
)
rho_dist_perc <- cor_spearman(
  dist_centro_km[mapa_sf$comparable],
  mapa_sf$pct_percibe_inseguridad[mapa_sf$comparable]
)
top_localidades <- function(t, n=3) {
  upz_mapa |>
    filter(tipo==t) |>
    count(localidad,sort=TRUE) |>
    slice_head(n=n) |>
    pull(localidad) |>
    paste(collapse=", ")
}
cat(
  "```{=html}\n",
  '<div class="insight"><div class="tag">HALLAZGO 8 · TERRITORIO</div>',
  '<h3>Las denuncias se concentran en el centro; la percepción alta sin denuncias, en el sur</h3>',
  '<p>Las tasas bajan a medida que la UPZ se aleja del centro histórico (rho = ', fmt_d(rho_dist_tasa),
  ' entre la distancia a la Plaza de Bolívar y la tasa), mientras que la percepción casi no depende de esa distancia (',
  fmt_d(rho_dist_perc), '). Las UPZ de <b>prioridad integral</b> están sobre todo en ',
  top_localidades(niveles_tipo[1]), '; las de <b>foco en denuncias</b>, en ',
  top_localidades(niveles_tipo[2]), '; y las de <b>foco en percepción</b>, en ',
  top_localidades(niveles_tipo[3]), '.</p>',
  '<p class="ver">La tipología es una herramienta para ordenar prioridades, no un diagnóstico: ',
  'una UPZ cerca de la mediana puede cambiar de grupo con pequeñas variaciones en los datos.</p></div>\n',
  "```\n\n",
  sep=""
)
resumen_tipo <- upz_mapa |>
  arrange(desc(tasa_total)) |>
  group_by(tipo) |>
  summarise(
    n=n(),
    upz=paste(upz_nombre,collapse=", "),
    .groups="drop"
  ) |>
  mutate(
    que_indica=unname(que_indica_tipo[as.character(tipo)]),
    orientacion=unname(orientacion_tipo[as.character(tipo)])
  )
cat(
  knitr::kable(
    resumen_tipo |> select(tipo,n,que_indica,orientacion,upz),
    col.names=c("Tipología","UPZ","Qué indica","Orientación","UPZ incluidas (de mayor a menor tasa)"),
    align=c("l","r","l","l","l"),
    caption="Tipología de las UPZ para orientar decisiones"
  ),
  sep="\n"
)
tarjeta <- function(tag, titulo, texto, ver) {
  paste0(
    '<div class="insight">',
    '<div class="tag">', tag, '</div>',
    '<h3>', titulo, '</h3>',
    '<p>', texto, '</p>',
    '<p class="ver">', ver, '</p>',
    '</div>'
  )
}
tarjetas <- c(
  tarjeta(
    "PREGUNTA 1 · VOLUMEN Y TASAS",
    "Depende de la medida: una UPZ lidera en casos y otra en tasa",
    paste0(
      fila_casos$upz_nombre, " acumula más denuncias (", fmt_n(fila_casos$casos_total),
      "). Entre las UPZ con al menos 5.000 habitantes, la tasa más alta es la de ",
      fila_tasa_comp$upz_nombre, " (", fmt_d(fila_tasa_comp$tasa_total),
      " por cada 10.000). La tasa de ", fila_tasa$upz_nombre, " (", fmt_d(fila_tasa$tasa_total),
      ") refleja sobre todo que solo tiene ", fmt_n(fila_tasa$poblacion), " habitantes."
    ),
    "Hallazgos 1 y 2 · sección 4"
  ),
  tarjeta(
    "PREGUNTA 2 · TIPOS DE DELITO",
    "El robo en la calle domina los registros",
    paste0(
      "El hurto a personas aporta el ", fmt_pct(share_hurto_personas,1),
      " de los casos; sumado al hurto de celulares, el ", fmt_pct(share_hurto_pc,1), "."
    ),
    "Hallazgo 3 · sección 5"
  ),
  tarjeta(
    "PREGUNTA 3 · PERCEPCIÓN Y DENUNCIAS",
    "No coinciden",
    paste0(
      "Frente a los casos, la correlación es prácticamente nula (", fmt_d(rho_casos_percepcion),
      "); frente a la tasa es negativa (", fmt_d(rho_tasacomp_percepcion),
      "). Donde más se denuncia por habitante, la percepción tiende a ser algo menor."
    ),
    "Hallazgo 4 · sección 6"
  ),
  tarjeta(
    "PREGUNTA 4 · PERCEPCIÓN Y ENTORNO",
    "La percepción sigue a la pobreza y a los problemas del barrio",
    paste0(
      "Se asocia con la pobreza (", fmt_d(rho_pobreza_percepcion),
      "), la cercanía a expendios de droga (", fmt_d(rho_ollas_percepcion),
      ") y a basureros (", fmt_d(rho_basureros_percepcion),
      "), y baja con el salario promedio (", fmt_d(rho_salario_percepcion), ")."
    ),
    "Hallazgo 5 · sección 6.1"
  ),
  tarjeta(
    "PREGUNTA 5 · ALUMBRADO, LOTES OSCUROS Y CAI",
    "Las relaciones reflejan centralidad, no efectos de la infraestructura",
    paste0(
      "Las luminarias por habitante se asocian con la tasa (", fmt_d(rho_luz_tasa),
      "), pero medidas por km² la relación casi desaparece (", fmt_d(rho_luzkm2_tasa),
      "). Más minutos al CAI (", fmt_d(rho_cai_tasa), ") y más lotes oscuros (", fmt_d(rho_lotes_tasa),
      ") acompañan tasas más bajas: describen zonas periféricas que denuncian menos."
    ),
    "Hallazgo 6 · sección 7"
  ),
  tarjeta(
    "PREGUNTA 6 · CONDICIONES SOCIOECONÓMICAS",
    "Las tasas son más altas en zonas de mayores ingresos",
    paste0(
      "La tasa baja con la proporción de estratos 1 y 2 (", fmt_d(rho_estrato_tasa),
      ") y sube con el salario (", fmt_d(rho_salario_tasa),
      "). Es el patrón opuesto al de la percepción y apunta a la población flotante de las zonas centrales."
    ),
    "Hallazgo 7 · sección 8"
  )
)
cat(
  "```{=html}\n",
  paste(tarjetas,collapse="\n"),
  "\n```\n",
  sep=""
)
tabla_ranking <- function(datos, caption) {
  datos |>
    transmute(
      UPZ=upz_nombre,
      Localidad=localidad,
      `Población`=fmt_n(poblacion),
      Casos=fmt_n(casos_total),
      `Tasa por 10.000`=fmt_d(tasa_total)
    ) |>
    knitr::kable(caption=caption,align=c("l","l","r","r","r"))
}
tabla_ranking(top_casos,"A.1 Diez UPZ con más denuncias registradas")
tabla_ranking(top_tasas,"A.2 Diez tasas más altas en el conjunto completo")
tabla_ranking(top_tasas_comp,"A.3 Diez tasas más altas entre UPZ con al menos 5.000 habitantes")
resumen_delitos |>
  transmute(
    `Tipo de delito`=nombre,
    Casos=fmt_n(casos),
    `Participación`=fmt_pct(participacion,1)
  ) |>
  knitr::kable(caption="A.4 Composición de los casos registrados",align=c("l","r","r"))
tibble(
  Variable=c(
    "upz_nombre, localidad","poblacion","casos_total, casos_*","tasa_total",
    "luminarias_1000hab, luminarias_km2","pct_luminarias_led","min_cai_policia",
    "pct_pobreza","salario_promedio","anios_educacion_15mas","pct_estrato_1_2",
    "pct_percibe_inseguridad","pct_cerca_ollas, pct_cerca_basureros, pct_cerca_lotes_oscuros",
    "em_agrupada"
  ),
  `Descripción`=c(
    "Nombre de la UPZ y de su localidad",
    "Población proyectada 2023",
    "Denuncias totales y por tipo de delito, enero-julio de 2023",
    "Denuncias por cada 10.000 habitantes",
    "Luminarias por cada 1.000 habitantes y por km²",
    "Porcentaje de luminarias con tecnología LED",
    "Minutos caminando al CAI de policía más cercano",
    "Porcentaje de personas en pobreza (Encuesta Multipropósito 2021)",
    "Salario promedio (Encuesta Multipropósito 2021)",
    "Años promedio de educación de personas de 15 años o más",
    "Porcentaje de viviendas en estratos 1 y 2",
    "Porcentaje de personas que perciben inseguridad en su barrio",
    "Porcentaje de viviendas cerca de expendios de droga, basureros o lotes oscuros",
    "Indica si los datos de encuesta se reportan agrupados con UPZ vecinas"
  )
) |>
  knitr::kable(caption="B.1 Variables principales del análisis")