library(tidyverse) # dplyr, ggplot2, readr, tidyr, forcats
library(sf) # datos espaciales (shapefiles)
library(scales) # formatos de números en ejes y tablas
library(ggrepel) # etiquetas que no se pisan
library(patchwork) # combinar gráficos
library(knitr) # tablas con kable()
library(leaflet) # mapas interactivos (hover y datos por UPZ)
library(htmltools) # para armar el diseño de los mapas en el HTML
# Paleta y tema del curso: un color para lo importante (magenta), otro para el
# contexto (morado / gris) y uno para las líneas de tendencia (turquesa).
morado <- "#351A4F"
magenta <- "#D9006C"
turquesa <- "#168C86"
gris <- "#D9D6DD"
tema_clase <- theme_minimal(base_size = 12) +
theme(
plot.title.position = "plot",
plot.title = element_text(face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(size = 10.5, color = "grey35", hjust = 0),
panel.grid.minor = element_blank(),
plot.caption = element_text(hjust = 0, color = "grey45", size = 8.5),
plot.margin = margin(10, 20, 10, 10)
)
theme_set(tema_clase)
# Funciones auxiliares de formato (separador de miles "." y decimal ",")
f2 <- function(x, d = 2) formatC(x, format = "f", digits = d, decimal.mark = ",")
mil <- function(x) number(x, accuracy = 1, big.mark = ".", decimal.mark = ",")
fmt <- function(x) ifelse(abs(x) >= 1000,
number(x, accuracy = 1, big.mark = ".", decimal.mark = ","),
number(x, accuracy = 0.01, big.mark = ".", decimal.mark = ","))
pval <- function(p) ifelse(p < 0.001, "p < 0,001", paste0("p = ", f2(p, 3)))
Bogotá se organiza en 112 UPZ (Unidades de Planeamiento Zonal) urbanas. Para cada una tenemos cuántos delitos denunciaron entre enero y julio de 2023, y además quién vive allí, cuánto gana, qué tan cerca queda un CAI y qué tan insegura se siente la zona.El analisis del taller está enfocado en los datos de inseguridad y la relación con el territorio y las condiciones de vida de cada sector demostrando que según el delito que miremos (hurto o violencia), el mapa de Bogotá cambia por completo.
① Denuncias vs. habitantes¿Las UPZ con más denuncias son solo las más grandes? (P1) ② ¿CAI lejos, más delito?La pregunta central del equipo: distancia al CAI y tasa de denuncias. (P2) ③ Dos tipos de delitoHurto y violencia cambian con el estrato y el ingreso. (P3-P6) ④ Mapas interactivosPasa el cursor por cada UPZ y mira sus datos. (P7)
| # | Pregunta | Variables principales |
|---|---|---|
| P1 | ¿Las UPZ con más denuncias son las que tienen más delitos por habitante, o solo las más grandes? | casos_total, poblacion,
tasa_total |
| P2 | ¿Hay relación entre la distancia al CAI y la tasa de inseguridad? | min_cai_policia |
| P3 | ¿Cambia el tipo de delito predominante según el estrato o el salario de la zona? | pct_estrato_*, salario_promedio |
| P4 | ¿La percepción de inseguridad coincide con lo que dicen las denuncias? | pct_percibe_inseguridad, tasas |
| P5 | ¿Vivir cerca de “ollas” o bares se asocia con más delitos? | pct_cerca_ollas, pct_cerca_bares,
tasas |
| P6 | ¿La pobreza y el hacinamiento se asocian con algún tipo de delito? | pct_pobreza, personas_por_vivienda,
tasas |
| P7 | ¿Dónde se concentran las denuncias y qué tan lejos queda el CAI de esos lugares? | mapa de tasa_total y min_cai_policia |
Trabajamos con 16 variables de los 3 bloques (más
las llaves upz_cod y upz_nombre). Elegimos
tasas por 10.000 habitantes y no conteos, porque a través de las tasas
podemos tener una visión clara de inseguridad (Pregunta 1) y no tuvimos
en cuenta delitos que consideramos no aportaban claridad como:
bicicletas, automotores, motos, residencias, lesiones) y las variables
de servicios que no influyen con las preguntas (parques, centros
médicos, etc.).
vars <- tribble(
~variable, ~bloque, ~etiqueta, ~por_que,
"personas_por_vivienda", "Vivienda", "Personas por vivienda", "Aproximación al hacinamiento (P6)",
"tasa_homicidios", "Delitos", "Homicidios", "El delito más grave (P3-P6)",
"tasa_hurto_personas", "Delitos", "Hurto a personas", "El delito más frecuente (P2-P6)",
"tasa_hurto_comercio", "Delitos", "Hurto a comercio", "Hurto ligado a zonas de actividad comercial (P2-P3)",
"tasa_hurto_celulares", "Delitos", "Hurto de celulares", "Oportunidad en espacio público (P2-P3)",
"tasa_violencia_intrafamiliar", "Delitos", "Violencia intrafamiliar", "Violencia en el hogar",
"tasa_total", "Delitos", "Tasa total", "Resumen de denuncias (P1, P2, P4, P7)",
"pct_pobreza", "Encuesta Multipropósito", "% en pobreza", "Condición de vida (P6)",
"salario_promedio", "Encuesta Multipropósito", "Salario promedio", "Capacidad económica de la zona (P3)",
"pct_estrato_1_2", "Encuesta Multipropósito", "% viviendas estrato 1-2", "Composición socioeconómica (P3)",
"pct_estrato_3_4", "Encuesta Multipropósito", "% viviendas estrato 3-4", "Composición socioeconómica (P3)",
"pct_estrato_5_6", "Encuesta Multipropósito", "% viviendas estrato 5-6", "Composición socioeconómica (P3)",
"pct_percibe_inseguridad", "Encuesta Multipropósito", "% percibe inseguridad", "Percepción vs. realidad denunciada (P4)",
"pct_cerca_ollas", "Encuesta Multipropósito", "% cerca de ollas", "Entorno (P5)",
"pct_cerca_bares", "Encuesta Multipropósito", "% cerca de bares", "Entorno (P5)",
"min_cai_policia", "Encuesta Multipropósito", "Minutos caminando al CAI", "Presencia policial cercana (P2, P7)"
)
etq <- setNames(vars$etiqueta, vars$variable)
vars_num <- vars$variable
vars %>%
transmute(Variable = paste0("`", variable, "`"), Bloque = bloque, `Para qué la usamos` = por_que) %>%
kable()
| Variable | Bloque | Para qué la usamos |
|---|---|---|
personas_por_vivienda |
Vivienda | Aproximación al hacinamiento (P6) |
tasa_homicidios |
Delitos | El delito más grave (P3-P6) |
tasa_hurto_personas |
Delitos | El delito más frecuente (P2-P6) |
tasa_hurto_comercio |
Delitos | Hurto ligado a zonas de actividad comercial (P2-P3) |
tasa_hurto_celulares |
Delitos | Oportunidad en espacio público (P2-P3) |
tasa_violencia_intrafamiliar |
Delitos | Violencia en el hogar |
tasa_total |
Delitos | Resumen de denuncias (P1, P2, P4, P7) |
pct_pobreza |
Encuesta Multipropósito | Condición de vida (P6) |
salario_promedio |
Encuesta Multipropósito | Capacidad económica de la zona (P3) |
pct_estrato_1_2 |
Encuesta Multipropósito | Composición socioeconómica (P3) |
pct_estrato_3_4 |
Encuesta Multipropósito | Composición socioeconómica (P3) |
pct_estrato_5_6 |
Encuesta Multipropósito | Composición socioeconómica (P3) |
pct_percibe_inseguridad |
Encuesta Multipropósito | Percepción vs. realidad denunciada (P4) |
pct_cerca_ollas |
Encuesta Multipropósito | Entorno (P5) |
pct_cerca_bares |
Encuesta Multipropósito | Entorno (P5) |
min_cai_policia |
Encuesta Multipropósito | Presencia policial cercana (P2, P7) |
# Rutas relativas: el .Rmd debe estar en la misma carpeta que input/
upz <- read_csv("input/upz_variables.csv", show_col_types = FALSE)
upz_shp <- st_read("input/shp/upz_bogota.shp", quiet = TRUE)
loc_shp <- st_read("input/shp/localidades_bogota.shp", quiet = TRUE)
c(filas = nrow(upz), columnas = ncol(upz),
upz_cod_unicos = n_distinct(upz$upz_cod))
## filas columnas upz_cod_unicos
## 112 67 112
# Estructura, faltantes, ceros y rangos de las variables elegidas
calidad <- upz %>%
select(all_of(vars_num)) %>%
pivot_longer(everything(), names_to = "variable", values_to = "v") %>%
group_by(variable) %>%
summarise(Tipo = "numérica", Faltantes = sum(is.na(v)), Ceros = sum(v == 0, na.rm = TRUE),
Mínimo = min(v), Máximo = max(v), .groups = "drop") %>%
mutate(variable = factor(variable, levels = vars_num)) %>%
arrange(variable) %>%
mutate(across(c(Mínimo, Máximo), fmt))
kable(calidad, caption = "Estructura y calidad de las 16 variables elegidas (112 UPZ)")
| variable | Tipo | Faltantes | Ceros | Mínimo | Máximo |
|---|---|---|---|---|---|
| personas_por_vivienda | numérica | 0 | 0 | 1,70 | 5,80 |
| tasa_homicidios | numérica | 0 | 23 | 0,00 | 6,93 |
| tasa_hurto_personas | numérica | 0 | 0 | 15,70 | 6.333 |
| tasa_hurto_comercio | numérica | 0 | 0 | 0,94 | 1.333 |
| tasa_hurto_celulares | numérica | 0 | 0 | 7,29 | 2.667 |
| tasa_violencia_intrafamiliar | numérica | 0 | 2 | 0,00 | 232,06 |
| tasa_total | numérica | 0 | 0 | 64,47 | 11.333 |
| pct_pobreza | numérica | 0 | 0 | 0,07 | 16,56 |
| salario_promedio | numérica | 0 | 0 | 845.688 | 7.977.532 |
| pct_estrato_1_2 | numérica | 0 | 8 | 0,00 | 100,00 |
| pct_estrato_3_4 | numérica | 0 | 6 | 0,00 | 100,00 |
| pct_estrato_5_6 | numérica | 0 | 78 | 0,00 | 98,94 |
| pct_percibe_inseguridad | numérica | 0 | 0 | 17,45 | 92,11 |
| pct_cerca_ollas | numérica | 0 | 0 | 0,31 | 75,60 |
| pct_cerca_bares | numérica | 0 | 0 | 0,64 | 53,91 |
| min_cai_policia | numérica | 0 | 0 | 10,06 | 35,11 |
Encontramos que: no hay valores faltantes, hay ceros solo donde corresponde (UPZ sin homicidios.
Dentro de la rubrica del taller se menciona que hay UPZ con muy pocos habitantes donde la tasa puede llegar a dispararse. Hicimos la siguiente revisión:
upz %>%
arrange(poblacion) %>%
transmute(UPZ = upz_nombre, Localidad = localidad, Población = mil(poblacion),
`Casos (ene-jul 2023)` = mil(casos_total), `Tasa total` = fmt(tasa_total)) %>%
head(12) %>%
kable(caption = "Las 12 UPZ menos pobladas")
| UPZ | Localidad | Población | Casos (ene-jul 2023) | Tasa total |
|---|---|---|---|---|
| El Mochuelo | Ciudad Bolívar | 30 | 34 | 11.333 |
| Aeropuerto El Dorado | Fontibón | 883 | 545 | 6.172 |
| Parque Entrenubes | Usme | 1.169 | 16 | 136,87 |
| Parque Salitre | Barrios Unidos | 1.442 | 233 | 1.616 |
| Paseo De Los Libertadores | Usaquén | 2.690 | 74 | 275,09 |
| Guaymaral | Suba | 2.692 | 45 | 167,16 |
| Jardin Botanico | Engativá | 2.900 | 266 | 917,24 |
| Parque Simon Bolivar - Can | Teusaquillo | 3.323 | 958 | 2.883 |
| La Academia | Suba | 5.260 | 123 | 233,84 |
| Monte Blanco | Ciudad Bolívar | 6.575 | 80 | 121,67 |
| Zona Industrial | Puente Aranda | 6.666 | 1.573 | 2.360 |
| Sagrado Corazon | Santa Fe | 11.247 | 1.707 | 1.518 |
Pudimos observar que UPZ como El Mochuelo y Aeropuerto El Dorado tienen un número bajo de habitantes, sin embargo las tasas son altas lo que permite evidenciar que seguramente no representa la población real ejm: pasajeros, trabajadores, visitantes y zonas industriales.
Decidimos excluir del análisis de tasas las UPZ con menos de 10.000 habitantes. Con ese corte quedan 101 UPZ. Las 11 excluidas seguiran apareciendo en gris en los mapas.
UMBRAL <- 10000
upz <- upz %>%
mutate(
poblacion_baja = poblacion < UMBRAL,
# Estrato dominante: la franja (1-2, 3-4 o 5-6) con mayor % de viviendas
estrato_dom = case_when(
pct_estrato_1_2 >= pct_estrato_3_4 & pct_estrato_1_2 >= pct_estrato_5_6 ~ "Estratos 1-2",
pct_estrato_3_4 >= pct_estrato_5_6 ~ "Estratos 3-4",
TRUE ~ "Estratos 5-6"),
# "Centro ampliado": localidades de alta actividad comercial, de oficinas y de tránsito
# (definición operativa nuestra, para probar la robustez de los resultados)
centro_ampliado = localidad %in% c("Santa Fe", "La Candelaria", "Los Mártires",
"Chapinero", "Teusaquillo", "Puente Aranda")
)
a <- upz %>% filter(!poblacion_baja) # base de análisis: 101 UPZ
excluidas <- upz %>% filter(poblacion_baja) %>% pull(upz_nombre)
mapa_upz <- upz_shp %>% left_join(upz %>% select(-upz_nombre), by = "upz_cod")
nrow(a); excluidas
## [1] 101
## [1] "Paseo De Los Libertadores" "La Academia"
## [3] "Guaymaral" "Parque Entrenubes"
## [5] "El Mochuelo" "Monte Blanco"
## [7] "Parque Salitre" "Parque Simon Bolivar - Can"
## [9] "Jardin Botanico" "Zona Industrial"
## [11] "Aeropuerto El Dorado"
g_pob <- ggplot(upz, aes(poblacion, fill = poblacion_baja)) +
geom_histogram(bins = 30, color = "white") +
geom_vline(xintercept = UMBRAL, color = magenta, linetype = "dashed") +
scale_x_log10(labels = label_comma(big.mark = ".")) +
scale_fill_manual(values = c(`FALSE` = morado, `TRUE` = magenta), guide = "none") +
labs(title = "11 UPZ tienen menos de 10.000 habitantes",
subtitle = "Población por UPZ (escala logarítmica). En magenta, las UPZ excluidas del análisis de tasas",
x = "Población", y = "Número de UPZ")
g_tasa <- ggplot(upz, aes(tasa_total, fill = poblacion_baja)) +
geom_histogram(bins = 30, color = "white") +
scale_x_log10(labels = label_comma(big.mark = ".")) +
scale_fill_manual(values = c(`FALSE` = morado, `TRUE` = magenta), guide = "none") +
labs(title = "La tasa total está muy sesgada a la derecha",
subtitle = "Denuncias por 10.000 hab. (escala log.). Las UPZ excluidas son los valores extremos",
x = "Tasa total (ene-jul 2023)", y = "Número de UPZ")
g_pob + g_tasa
c(media_todas = mean(upz$tasa_total), mediana_todas = median(upz$tasa_total),
media_101 = mean(a$tasa_total), mediana_101 = median(a$tasa_total)) %>% round(1)
## media_todas mediana_todas media_101 mediana_101
## 503.6 192.5 298.9 186.6
La media de la tasa total con las 112 UPZ es 503,64, pero la mediana es solo 192,52; sin las UPZ de población baja la media baja a 298,93. Hay unas pocas UPZ muy altas que arrastran el promedio. Por eso, decidimos que para el analisis usaremos la mediana para resumir, correlacionar y establecer las escalas para graficar tasas.
tabla_resumen <- a %>%
select(all_of(vars_num)) %>%
pivot_longer(everything(), names_to = "variable", values_to = "v") %>%
group_by(variable) %>%
summarise(Media = mean(v), Mediana = median(v), `Desv. est.` = sd(v),
Mín = min(v), Q1 = quantile(v, .25), Q3 = quantile(v, .75), Máx = max(v),
.groups = "drop") %>%
mutate(variable = factor(variable, levels = vars_num)) %>%
arrange(variable) %>%
mutate(Variable = etq[as.character(variable)], .before = 1) %>%
select(-variable) %>%
mutate(across(where(is.numeric), fmt))
kable(tabla_resumen, align = "lrrrrrrr",
caption = "Tabla resumen: estadística descriptiva de las 16 variables (101 UPZ con 10.000 hab. o más)")
| Variable | Media | Mediana | Desv. est. | Mín | Q1 | Q3 | Máx |
|---|---|---|---|---|---|---|---|
| Personas por vivienda | 2,78 | 2,81 | 0,49 | 1,70 | 2,59 | 3,01 | 5,80 |
| Homicidios | 0,90 | 0,56 | 1,13 | 0,00 | 0,24 | 1,14 | 6,25 |
| Hurto a personas | 167,74 | 96,03 | 223,88 | 15,70 | 54,33 | 159,66 | 1.488 |
| Hurto a comercio | 11,80 | 6,93 | 14,97 | 0,94 | 3,75 | 11,83 | 92,43 |
| Hurto de celulares | 60,98 | 34,12 | 85,82 | 7,29 | 19,08 | 59,33 | 619,66 |
| Violencia intrafamiliar | 21,72 | 17,65 | 24,08 | 5,36 | 12,46 | 23,33 | 232,06 |
| Tasa total | 298,93 | 186,56 | 353,67 | 64,47 | 126,39 | 273,23 | 2.344 |
| % en pobreza | 3,99 | 2,70 | 3,83 | 0,07 | 0,99 | 5,79 | 16,56 |
| Salario promedio | 2.553.007 | 1.826.624 | 1.768.638 | 845.688 | 1.229.038 | 3.574.567 | 7.977.532 |
| % viviendas estrato 1-2 | 41,63 | 23,69 | 42,85 | 0,00 | 0,57 | 96,85 | 100,00 |
| % viviendas estrato 3-4 | 50,16 | 66,05 | 41,45 | 0,00 | 2,23 | 92,70 | 100,00 |
| % viviendas estrato 5-6 | 8,10 | 0,00 | 22,95 | 0,00 | 0,00 | 0,22 | 98,94 |
| % percibe inseguridad | 61,29 | 65,97 | 18,67 | 17,45 | 48,52 | 74,30 | 92,11 |
| % cerca de ollas | 21,45 | 20,92 | 15,37 | 0,31 | 10,14 | 30,54 | 75,60 |
| % cerca de bares | 14,54 | 13,00 | 10,71 | 0,64 | 5,82 | 19,48 | 53,91 |
| Minutos caminando al CAI | 18,09 | 17,49 | 4,40 | 10,06 | 15,06 | 20,54 | 35,11 |
Análisis: Entre los delitos la mediana de la tasa de hurto a personas (96,03) es una de las más altas, sine embargo la dispersión es enorme. La percepción de inseguridad es alta en la ciudad (mediana 66,0 %), y el salario promedio de las UPZ va de $845.688 a $7.977.532 mensuales.
# Correlación de Spearman entre dos conjuntos de variables, en formato largo
cor_larga <- function(filas, cols, d = a) {
m <- cor(d[filas], d[cols], method = "spearman")
as_tibble(m, rownames = "fila") %>%
pivot_longer(-fila, names_to = "col", values_to = "rho") %>%
mutate(fila = factor(fila, levels = rev(filas)), col = factor(col, levels = cols))
}
mapa_calor <- function(df, titulo, subtitulo, size = 3) {
ggplot(df, aes(col, fila, fill = rho)) +
geom_tile(color = "white") +
geom_text(aes(label = f2(rho, 2), color = abs(rho) > 0.55), size = size) +
scale_color_manual(values = c(`TRUE` = "white", `FALSE` = "grey20"), guide = "none") +
scale_fill_gradient2(low = turquesa, mid = "white", high = magenta, limits = c(-1, 1),
name = "Spearman") +
scale_x_discrete(labels = etq, position = "top") +
scale_y_discrete(labels = etq) +
labs(title = str_wrap(titulo, 75), subtitle = str_wrap(subtitulo, 95), x = NULL, y = NULL) +
theme(axis.text.x = element_text(angle = 40, hjust = 0), panel.grid = element_blank())
}
mapa_calor(cor_larga(vars_num, vars_num),
"Hurtos e ingreso se mueven juntos; violencia, pobreza y percepción de inseguridad, en el bloque opuesto",
"Correlación de Spearman entre las 16 variables (101 UPZ). Magenta = positiva, turquesa = negativa",
size = 2.5)
Lectura de la matriz: aparecen dos bloques opuestos. Por un lado, los hurtos (personas, comercio, celulares) y la tasa total se mueven con el salario y el estrato 3-4. Por el otro, homicidios y violencia intrafamiliar se mueven con la pobreza, el estrato 1-2, la percepción de inseguridad y la cercanía a ollas (ρ de 0,6 a 0,8 en varios casos).
top_casos <- a %>% slice_max(casos_total, n = 10)
top_tasa <- a %>% slice_max(tasa_total, n = 10)
comunes <- intersect(top_casos$upz_cod, top_tasa$upz_cod)
solo_casos <- setdiff(top_casos$upz_nombre, top_tasa$upz_nombre)
rho_pob_casos <- cor(a$poblacion, a$casos_total, method = "spearman")
barras_top <- function(d, y, titulo, subt, xlab) {
d %>%
mutate(destacada = upz_cod %in% comunes, upz_nombre = fct_reorder(upz_nombre, .data[[y]])) %>%
ggplot(aes(.data[[y]], upz_nombre, fill = destacada)) +
geom_col() +
scale_fill_manual(values = c(`TRUE` = magenta, `FALSE` = gris), guide = "none") +
scale_x_continuous(labels = label_comma(big.mark = "."), expand = expansion(mult = c(0, .08))) +
labs(title = titulo, subtitle = subt, x = xlab, y = NULL)
}
p1a <- barras_top(top_casos, "casos_total", "Por número de denuncias",
"Top 10 UPZ en casos totales", "Denuncias (ene-jul 2023)")
p1b <- barras_top(top_tasa, "tasa_total", "Por denuncias por habitante",
"Top 10 UPZ en tasa total", "Denuncias por 10.000 hab.")
(p1a + p1b) +
plot_annotation(
title = paste0(length(comunes), " de las 10 UPZ con más denuncias también lideran por habitante"),
subtitle = "En magenta, las UPZ que aparecen en ambos rankings",
theme = theme(plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 10.5, color = "grey35")))
a %>%
slice_max(tasa_total, n = 10) %>%
transmute(`#` = row_number(), UPZ = upz_nombre, Localidad = localidad,
Población = mil(poblacion), Casos = mil(casos_total),
`Tasa total` = fmt(tasa_total), `Hurto a personas` = fmt(tasa_hurto_personas),
`Min. al CAI` = f2(min_cai_policia, 1), `% percibe insegur.` = f2(pct_percibe_inseguridad, 1),
`Salario prom. ($)` = mil(salario_promedio)) %>%
kable(align = "rllrrrrrrr",
caption = "Ranking: las 10 UPZ con mayor tasa total de denuncias (101 UPZ con 10.000 hab. o más)")
| # | UPZ | Localidad | Población | Casos | Tasa total | Hurto a personas | Min. al CAI | % percibe insegur. | Salario prom. ($) |
|---|---|---|---|---|---|---|---|---|---|
| 1 | Las Nieves | Santa Fe | 17.203 | 4.033 | 2.344 | 1.488 | 10,1 | 59,9 | 4.390.281 |
| 2 | Sagrado Corazon | Santa Fe | 11.247 | 1.707 | 1.518 | 850,89 | 10,1 | 59,9 | 4.390.281 |
| 3 | La Sabana | Los Mártires | 38.856 | 5.441 | 1.400 | 852,12 | 16,0 | 83,5 | 1.914.420 |
| 4 | Chico Lago | Chapinero | 38.608 | 4.985 | 1.291 | 841,79 | 13,1 | 21,5 | 7.977.532 |
| 5 | Puente Aranda | Puente Aranda | 12.482 | 1.561 | 1.251 | 723,44 | 17,2 | 66,3 | 1.612.726 |
| 6 | Chapinero | Chapinero | 26.491 | 2.901 | 1.095 | 699,86 | 15,6 | 50,3 | 4.981.378 |
| 7 | Teusaquillo | Teusaquillo | 28.881 | 2.976 | 1.030 | 648,18 | 10,4 | 62,9 | 3.675.044 |
| 8 | Galerias | Teusaquillo | 38.425 | 3.067 | 798,18 | 496,55 | 12,8 | 48,5 | 3.574.567 |
| 9 | Los Alcazares | Barrios Unidos | 44.678 | 3.495 | 782,26 | 466,90 | 12,8 | 69,8 | 2.188.284 |
| 10 | La Candelaria | Candelaria | 18.409 | 1.371 | 744,74 | 448,15 | 12,7 | 64,7 | 2.530.327 |
Hallazgo Pegunta 1. El conteo y la tasa cuentan casi la misma historia porque las UPZ más denunciadas no son las más pobladas: la correlación entre población y denuncias es solo moderada (ρ = 0,44). Las que aparecen únicamente en el ranking de casos (El Rincon, Bosa Central, Las Ferias) son muy grandes pero con tasas bajas, es decir, tienen muchas denuncias solo porque tienen mucha gente. En cambio, el top de tasas lo dominan UPZ del centro (Las Nieves, Sagrado Corazón, La Sabana, Chico Lago, Chapinero, Teusaquillo…), con poblaciones mediana.
Esta es la pregunta central de nuestro equipo. Medimos la distancia
como min_cai_policia: minutos promedio caminando
desde la vivienda hasta el CAI más cercano
rho_total <- cor.test(a$min_cai_policia, a$tasa_total, method = "spearman", exact = FALSE)
tasas6 <- c("tasa_homicidios", "tasa_hurto_personas", "tasa_hurto_comercio",
"tasa_hurto_celulares", "tasa_violencia_intrafamiliar", "tasa_total")
rho_cai <- map_dfr(tasas6, function(t) {
r <- cor.test(a$min_cai_policia, a[[t]], method = "spearman", exact = FALSE)
tibble(variable = t, rho = unname(r$estimate), p = r$p.value)
}) %>%
mutate(delito = etq[variable])
rho_cai %>% mutate(rho = f2(rho), p = pval(p)) %>% select(Delito = delito, `ρ de Spearman` = rho, `p-valor` = p) %>%
kable(caption = "Correlación entre minutos al CAI y cada tasa de delito (101 UPZ)")
| Delito | ρ de Spearman | p-valor |
|---|---|---|
| Homicidios | -0,11 | p = 0,268 |
| Hurto a personas | -0,43 | p < 0,001 |
| Hurto a comercio | -0,30 | p = 0,003 |
| Hurto de celulares | -0,41 | p < 0,001 |
| Violencia intrafamiliar | -0,08 | p = 0,398 |
| Tasa total | -0,43 | p < 0,001 |
a <- a %>% mutate(top10_tasa = upz_cod %in% top_tasa$upz_cod)
ggplot(a, aes(min_cai_policia, tasa_total)) +
geom_point(aes(color = top10_tasa, alpha = top10_tasa), size = 2.6) +
geom_smooth(method = "lm", se = FALSE, color = turquesa, linewidth = 1) +
geom_text_repel(data = filter(a, top10_tasa, tasa_total > 1200), aes(label = upz_nombre),
size = 3, color = magenta, min.segment.length = 0, seed = 1) +
scale_y_log10(labels = label_comma(big.mark = ".")) +
scale_color_manual(values = c(`TRUE` = magenta, `FALSE` = morado), guide = "none") +
scale_alpha_manual(values = c(`TRUE` = 1, `FALSE` = .55), guide = "none") +
labs(title = "Donde el CAI queda más lejos, las denuncias por habitante son menores",
subtitle = paste0("Spearman ρ = ", f2(rho_total$estimate), " (", pval(rho_total$p.value),
") · 101 UPZ · magenta = 10 UPZ con mayor tasa total"),
x = "Minutos caminando hasta el CAI más cercano", y = "Denuncias por 10.000 hab. (escala log.)",
caption = "Fuentes: SDSCJ (delitos ene-jul 2023), DANE (Encuesta Multipropósito 2021). La línea turquesa es un ajuste lineal sobre la escala log.")
rho_cai %>%
mutate(sig = p < 0.05, delito = fct_reorder(delito, rho, .desc = TRUE)) %>%
ggplot(aes(rho, delito, fill = sig)) +
geom_col() +
geom_text(aes(label = f2(rho), hjust = if_else(rho < 0, 1.15, -0.15)), size = 3.5) +
geom_vline(xintercept = 0) +
scale_fill_manual(values = c(`TRUE` = magenta, `FALSE` = gris), guide = "none") +
scale_x_continuous(limits = c(-.6, .15)) +
labs(title = "La distancia al CAI solo se asocia con los hurtos, no con la violencia",
subtitle = "Correlación de Spearman entre minutos al CAI y cada tasa. Magenta = estadísticamente distinta de 0 (p < 0,05)",
x = "Correlación (ρ)", y = NULL)
Hallazgo Pregunta 2. La correlación entre minutos al CAI y tasa total es negativa y moderada (ρ = -0,43, p < 0,001): donde el CAI queda más cerca, se denuncian más delitos por habitante.
Dos conclusiones de esta correlación:
tasas5 <- c("tasa_homicidios", "tasa_hurto_personas", "tasa_hurto_celulares",
"tasa_hurto_comercio", "tasa_violencia_intrafamiliar")
por_estrato <- a %>%
group_by(estrato_dom) %>%
summarise(n = n(), across(all_of(tasas5), median), .groups = "drop")
por_estrato %>%
mutate(across(all_of(tasas5), fmt)) %>%
rename(`Estrato dominante` = estrato_dom, UPZ = n,
Homicidios = tasa_homicidios, `Hurto personas` = tasa_hurto_personas,
`Hurto celulares` = tasa_hurto_celulares, `Hurto comercio` = tasa_hurto_comercio,
`Viol. intrafamiliar` = tasa_violencia_intrafamiliar) %>%
kable(align = "lrrrrrr", caption = "Tasa mediana por 10.000 hab., según el estrato dominante de la UPZ")
| Estrato dominante | UPZ | Homicidios | Hurto personas | Hurto celulares | Hurto comercio | Viol. intrafamiliar |
|---|---|---|---|---|---|---|
| Estratos 1-2 | 41 | 1,00 | 51,67 | 17,69 | 3,47 | 22,55 |
| Estratos 3-4 | 53 | 0,39 | 147,09 | 54,16 | 9,98 | 15,53 |
| Estratos 5-6 | 7 | 0,24 | 154,49 | 60,70 | 10,72 | 9,39 |
por_estrato %>%
pivot_longer(all_of(tasas5), names_to = "variable", values_to = "mediana") %>%
mutate(delito = factor(etq[variable], levels = etq[tasas5]),
estrato_lab = str_remove(estrato_dom, "Estratos ")) %>%
group_by(delito) %>% mutate(es_max = mediana == max(mediana)) %>% ungroup() %>%
ggplot(aes(estrato_lab, mediana, fill = es_max)) +
geom_col(width = .7) +
geom_text(aes(label = fmt(mediana)), vjust = -0.4, size = 3.2) +
facet_wrap(~ delito, scales = "free_y", nrow = 1) +
scale_fill_manual(values = c(`TRUE` = magenta, `FALSE` = gris), guide = "none") +
scale_y_continuous(expand = expansion(mult = c(0, .18))) +
labs(title = "Homicidios y violencia intrafamiliar bajan con el estrato; los hurtos suben",
subtitle = "Tasa mediana por 10.000 hab. según el estrato dominante de la UPZ (magenta = grupo con la tasa más alta de ese delito)",
x = "Estrato dominante de la UPZ", y = "Mediana de la tasa",
caption = paste0("Número de UPZ: estratos 1-2 = ", por_estrato$n[1], ", 3-4 = ", por_estrato$n[2], ", 5-6 = ", por_estrato$n[3], " (pocas para sacar conclusiones firmes en 5-6). Cada panel tiene su propia escala vertical."))
mapa_calor(cor_larga(c("salario_promedio", "pct_estrato_1_2", "pct_estrato_3_4", "pct_estrato_5_6"),
c("tasa_homicidios", "tasa_violencia_intrafamiliar", "tasa_hurto_personas",
"tasa_hurto_celulares", "tasa_hurto_comercio")),
"El salario y el estrato separan los delitos en dos grupos opuestos",
"Correlación de Spearman entre nivel socioeconómico y tasas de delito (101 UPZ)", size = 3.4)
Hallazgo Pregunta 3. Sí cambia el tipo de delito. En las UPZ de estratos 1-2 el delito que más pesa es la violencia: su tasa mediana de homicidios es 1,0 frente a 0,2 en estratos 5-6, y la violencia intrafamiliar es 2,4 veces mayor. En estratos 3-6 domina el hurto (a personas, celulares, comercio), con tasas de unas 3 veces las de las UPZ de estratos 1-2. El salario promedio confirma el patrón (ρ positiva con los hurtos, negativa con homicidios y violencia intrafamiliar). Esto no significa que los hogares de estrato alto sean “más delincuentes” ni los de estrato bajo “más violentos”: significa que el tipo de delito que se denuncia varía con el territorio, y que el hurto se denuncia más donde hay más cosas que robar y más hábito de denunciar.
rho_perc <- map_dfr(c("tasa_total", "tasa_hurto_personas", "tasa_homicidios", "tasa_violencia_intrafamiliar"), function(t) {
r <- cor.test(a$pct_percibe_inseguridad, a[[t]], method = "spearman", exact = FALSE)
tibble(variable = t, rho = unname(r$estimate), p = r$p.value)
})
rho_perc %>% mutate(Delito = etq[variable], `ρ de Spearman` = f2(rho), `p-valor` = pval(p)) %>%
select(Delito, `ρ de Spearman`, `p-valor`) %>%
kable(caption = "Correlación entre % que percibe inseguridad en su entorno y cada tasa")
| Delito | ρ de Spearman | p-valor |
|---|---|---|
| Tasa total | -0,31 | p = 0,002 |
| Hurto a personas | -0,39 | p < 0,001 |
| Homicidios | 0,58 | p < 0,001 |
| Violencia intrafamiliar | 0,67 | p < 0,001 |
base_p4 <- ggplot(a, aes(pct_percibe_inseguridad)) +
scale_x_continuous(labels = function(x) paste0(x, " %"))
p4a <- base_p4 +
geom_point(aes(y = tasa_hurto_personas, shape = em_agrupada), color = morado, alpha = .6, size = 2.4) +
geom_smooth(aes(y = tasa_hurto_personas), method = "lm", se = FALSE, color = turquesa) +
scale_y_log10(labels = label_comma(big.mark = ".")) +
scale_shape_manual(values = c(`FALSE` = 16, `TRUE` = 1), guide = "none") +
labs(title = str_wrap("Hurto a personas: la percepción baja donde más se denuncia", 40),
subtitle = paste0("ρ = ", f2(rho_perc$rho[rho_perc$variable == "tasa_hurto_personas"])),
x = "% que percibe inseguridad en su entorno", y = "Hurto a personas por 10.000 hab. (log)")
p4b <- base_p4 +
geom_point(aes(y = tasa_homicidios, shape = em_agrupada), color = magenta, alpha = .65, size = 2.4) +
geom_smooth(aes(y = tasa_homicidios), method = "lm", se = FALSE, color = turquesa) +
scale_shape_manual(values = c(`FALSE` = 16, `TRUE` = 1), guide = "none") +
coord_cartesian(ylim = c(0, NA)) +
labs(title = str_wrap("Homicidios: la percepción sube donde más hay", 40),
subtitle = paste0("ρ = ", f2(rho_perc$rho[rho_perc$variable == "tasa_homicidios"])),
x = "% que percibe inseguridad en su entorno", y = "Homicidios por 10.000 hab.")
p4a + p4b +
plot_annotation(caption = "Círculo vacío = UPZ con valor de encuesta de grupo (em_agrupada). Línea turquesa = ajuste lineal.",
theme = theme(plot.caption = element_text(hjust = 0, color = "grey45", size = 8.5)))
Hallazgo Pregunta 4. La percepción de inseguridad sigue a la violencia, no a los hurtos. Se correlaciona positivamente con homicidios (ρ = 0,58) y violencia intrafamiliar (ρ = 0,67), y negativamente con la tasa total y el hurto a personas. Dicho de otro modo: donde más se denuncian hurtos, la gente dice sentirse menos insegura en su entorno inmediato.
mapa_calor(
cor_larga(c("pct_cerca_ollas", "pct_cerca_bares", "pct_pobreza", "personas_por_vivienda",
"pct_percibe_inseguridad", "min_cai_policia"),
c("tasa_homicidios", "tasa_violencia_intrafamiliar", "tasa_hurto_personas",
"tasa_hurto_celulares", "tasa_hurto_comercio", "tasa_total")),
"Ollas, pobreza y hacinamiento se asocian con violencia; los bares, menos",
"Correlación de Spearman entre condiciones del entorno y tasas de delito (101 UPZ)", size = 3.4)
ggplot(a, aes(pct_pobreza, tasa_homicidios)) +
geom_point(aes(size = poblacion), color = magenta, alpha = .55) +
geom_smooth(method = "lm", se = FALSE, color = turquesa) +
scale_size_area(max_size = 7, labels = label_comma(big.mark = "."), name = "Población") +
scale_x_continuous(labels = function(x) paste0(x, " %")) +
labs(title = "Más pobreza, más homicidios por habitante",
subtitle = paste0("Spearman ρ = ", f2(cor(a$pct_pobreza, a$tasa_homicidios, method = "spearman")),
" · 101 UPZ · el tamaño del punto es la población"),
x = "% de personas en pobreza (Encuesta Multipropósito 2021)", y = "Homicidios por 10.000 hab. (ene-jul 2023)",
caption = "Los homicidios son pocos casos por UPZ: las tasas son ruidosas. Valores de encuesta compartidos en 32 UPZ.")
r_olla_hom <- cor(a$pct_cerca_ollas, a$tasa_homicidios, method = "spearman")
r_olla_vif <- cor(a$pct_cerca_ollas, a$tasa_violencia_intrafamiliar, method = "spearman")
r_olla_hp <- cor(a$pct_cerca_ollas, a$tasa_hurto_personas, method = "spearman")
r_bar_hom <- cor(a$pct_cerca_bares, a$tasa_homicidios, method = "spearman")
r_bar_vif <- cor(a$pct_cerca_bares, a$tasa_violencia_intrafamiliar, method = "spearman")
r_bar_hp <- cor(a$pct_cerca_bares, a$tasa_hurto_personas, method = "spearman")
r_pob_hom <- cor(a$pct_pobreza, a$tasa_homicidios, method = "spearman")
r_pob_vif <- cor(a$pct_pobreza, a$tasa_violencia_intrafamiliar, method = "spearman")
r_hac_hp <- cor(a$personas_por_vivienda, a$tasa_hurto_personas, method = "spearman")
r_hac_hom <- cor(a$personas_por_vivienda, a$tasa_homicidios, method = "spearman")
r_olla_perc <- cor(a$pct_cerca_ollas, a$pct_percibe_inseguridad, method = "spearman")
r_hac_pob <- cor(a$personas_por_vivienda, a$pct_pobreza, method = "spearman")
r_hac_e12 <- cor(a$personas_por_vivienda, a$pct_estrato_1_2, method = "spearman")
Hallazgo Pregunta 5. Las “ollas” se asocian con violencia, no con hurto. El % de viviendas cerca de expendios de droga se correlaciona positivamente con homicidios (ρ = 0,64) y violencia intrafamiliar (ρ = 0,66), y negativamente con el hurto a personas (ρ = -0,41). Los bares muestran una asociación más débil: con violencia (ρ = 0,44 con violencia intrafamiliar y 0,35 con homicidios) y casi ninguna con hurtos (ρ = 0,10). Las ollas también van de la mano de la percepción de inseguridad (ρ = 0,67), lo que es coherente con la pregunta 4: la percepción acompaña a la violencia y a ese problema de entorno.
Hallazgo Pregunta 6. La pobreza y el hacinamiento van con la violencia, y a contramano del hurto. El % en pobreza se asocia con homicidios (ρ = 0,64) y violencia intrafamiliar (ρ = 0,76). Las personas por vivienda, nuestro indicador de hacinamiento, correlacionan con el hurto a personas (ρ = -0,73, fuertemente negativa) y poco con homicidios (ρ = 0,24): las UPZ con viviendas más llenas son también las más pobres (ρ = 0,68 con % en pobreza y 0,71 con % de estrato 1-2), donde se denuncia menos hurto.
Cómo usar los mapas: pasa el cursor sobre cualquier UPZ y se sombrea con borde coral; aparece una ficha con su población, sus tasas, los minutos al CAI, la percepción de inseguridad y el salario. Con la rueda del ratón haces zoom y arrastrando te desplazas.
niveles_q <- c("1 (más baja)", "2", "3", "4", "5 (más alta)")
cortes <- quantile(a$tasa_total, probs = seq(0, 1, .2))
mapa_upz <- mapa_upz %>%
mutate(q_tasa = if_else(poblacion_baja, "Excluida (< 10.000 hab.)",
as.character(cut(tasa_total, breaks = cortes, include.lowest = TRUE, labels = niveles_q))),
q_tasa = factor(q_tasa, levels = c(niveles_q, "Excluida (< 10.000 hab.)")))
pal_q <- c(setNames(colorRampPalette(c("#F1EAF4", morado))(5), niveles_q),
"Excluida (< 10.000 hab.)" = gris)
# Texto que aparece al pasar el cursor sobre cada UPZ (HTML dentro de la etiqueta)
mapa_upz <- mapa_upz %>%
mutate(etiqueta = paste0(
"<div style='font-size:13px;line-height:1.4;min-width:210px'>",
"<b style='font-size:15px;color:#0B2E3A'>", upz_nombre, "</b><br>",
"<span style='color:#168C86'>Localidad: ", localidad, "</span>",
"<hr style='margin:5px 0;border-top:1px solid #ddd'>",
"Población: <b>", mil(poblacion), "</b> hab.<br>",
if_else(poblacion_baja,
"<i style='color:#D9006C'>Excluida del análisis de tasas (< 10.000 hab.)</i><br>", ""),
"Tasa total: <b>", fmt(tasa_total), "</b> por 10.000 hab.<br>",
" · Hurto a personas: ", fmt(tasa_hurto_personas), "<br>",
" · Homicidios: ", fmt(tasa_homicidios), "<br>",
" · Viol. intrafamiliar: ", fmt(tasa_violencia_intrafamiliar), "<br>",
"Minutos al CAI: <b>", f2(min_cai_policia, 1), "</b><br>",
"% percibe inseguridad: ", f2(pct_percibe_inseguridad, 1), " %<br>",
"Salario promedio: $", mil(salario_promedio),
"</div>"))
# Para que el HTML pese menos, simplificamos un poco los contornos (~20 m) solo para los mapas interactivos
sf_use_s2(FALSE)
mapa_upz <- st_simplify(mapa_upz, preserveTopology = TRUE, dTolerance = 0.0002)
loc_shp <- st_simplify(loc_shp, preserveTopology = TRUE, dTolerance = 0.0002)
pal_tasa <- colorFactor(palette = unname(pal_q), levels = names(pal_q))
pal_cai <- colorNumeric(c("#E3F3F2", "#0A4F4B"), domain = mapa_upz$min_cai_policia)
# Opciones comunes: sombrear la UPZ al pasar el cursor y mostrar la etiqueta
resaltar <- highlightOptions(weight = 3.5, color = "#FF6B4A", fillOpacity = 1, bringToFront = TRUE)
estilo_et <- labelOptions(direction = "auto", textsize = "13px",
style = list("padding" = "8px 10px", "border-radius" = "10px",
"box-shadow" = "0 3px 12px rgba(0,0,0,.3)",
"border" = "1px solid #168C86"))
hacer_mapa <- function(relleno, leyenda) {
leaflet(mapa_upz, height = 520, options = leafletOptions(minZoom = 10)) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addPolygons(fillColor = relleno, fillOpacity = .85, color = "white", weight = .8,
label = ~lapply(etiqueta, HTML), labelOptions = estilo_et,
highlightOptions = resaltar) %>%
# bordes de las localidades como referencia (no interactivos)
addPolygons(data = loc_shp, fill = FALSE, color = "#4A4A4A", weight = 1.3, opacity = .75,
options = pathOptions(interactive = FALSE)) %>%
leyenda
}
mapa_tasa <- hacer_mapa(
~pal_tasa(q_tasa),
. %>% addLegend(colors = unname(pal_q), labels = names(pal_q), opacity = .9,
title = "Tasa total<br>(quintiles)", position = "bottomright"))
mapa_cai <- hacer_mapa(
~pal_cai(min_cai_policia),
. %>% addLegend(pal = pal_cai, values = ~min_cai_policia, opacity = .9,
title = "Minutos<br>al CAI", position = "bottomright"))
tags$div(class = "mapas-grid",
tags$div(class = "mapa-card",
tags$h4("Las denuncias por habitante se concentran en el centro"),
tags$p("Tasa total por 10.000 hab. (ene-jul 2023), en quintiles"), mapa_tasa),
tags$div(class = "mapa-card",
tags$h4("Y es también donde el CAI queda más cerca"),
tags$p("Minutos promedio caminando hasta el CAI más cercano (2021)"), mapa_cai))
Tasa total por 10.000 hab. (ene-jul 2023), en quintiles
Minutos promedio caminando hasta el CAI más cercano (2021)
Fuentes: SDSCJ, DANE, Encuesta Multipropósito 2021. Líneas grises = localidades. Gris claro en el mapa de la izquierda = UPZ excluidas por población baja (siguen mostrando sus datos al pasar el cursor).
Hallazgo Pregunta 7. Los mapas confirman la Pregunta 1 y Pregunta 2 en el territorio: las tasas totales más altas se agrupan en una franja que va del centro hacia el norte y el oriente-centro de la ciudad, que es también donde los minutos al CAI son menores. Las UPZ de los bordes (noroccidente y sur) tienen CAI a mayor distancia y tasas totales de denuncia más bajas. Esos mapas ocultan que la violencia (homicidios y violencia intrafamiliar) sigue otra geografía, ligada a pobreza y estratos 1-2 (P3 y P6), que la tasa total no ve porque los hurtos pesan mucho más en el conteo.
Bogotá tiene dos geografías de delito, y según desde donde se mire concluye cosas opuestas.
Es importante que para hace un buen diagnóstico se debería separar el hurto y la violencia para visibilizar el comportamiento integral de la población.
La pregunta “¿qué nos dicen los datos sobre inseguridad y territorio?” tiene una respuesta incómoda para quien quiera un único indicador: depende de qué inseguridad se mida. Las denuncias de hurto, que dominan la tasa total, se concentran en el centro, en UPZ de mayor ingreso y con CAI más cercano.La violencia y la percepción de inseguridad, en cambio, siguen la pobreza, el hacinamiento y las “ollas”, en zonas más periféricas.
Sobre la pregunta de nuestro equipo, la distancia al CAI se relaciona con las denuncias de hurto, pero en sentido inverso al esperado, y no con la violencia; antes de hablar de “efecto policial” haría falta separar presencia policial, actividad económica y subregistro.
## [1] "R version 4.6.1 (2026-06-24 ucrt)"