Comparendos de Tránsito – Barranquilla 2010
Este informe aplica funciones estadísticas básicas al análisis de la base de datos de comparendos de tránsito de Barranquilla (Atlántico) entre abril y mayo de 2010. Con R se explora, describe y visualiza la información para identificar patrones, tendencias y relaciones en las infracciones registradas: días críticos, género del infractor, tipo de infracción y de vehículo, recaudo y reincidentes.
# ---- LIBRERÍAS USADAS ----
library(tidyverse) # conjunto de paquetes para manipular datos (dplyr, ggplot2, stringr, purrr...)
library(readxl) # permite leer archivos de Excel (.xlsx)
library(plotly) # crea gráficos interactivos y animados
library(scales) # da formato a los números (ejemplo: 1,000,000)
library(knitr) # genera el informe y la función kable() para tablas
# ---- LECTURA DE LA BASE ----
# read_excel() importa el archivo y lo guarda en df_raw (base original, sin modificar)
df_raw <- read_excel("BD_COMPARENDO_ACT_3_24.xlsx")
# head() muestra las primeras 6 filas; [, c(...)] elige solo las columnas principales.
# Sirve para comprobar que el Excel se leyó bien
head(df_raw[, c("EJECUTADO", "GENERO", "NOM.INFRACCION", "VEHICULO", "COSTO DEL COMPARENDO")])# Cuenta cuántos valores vacíos (NA) tenía la base original
nulos_iniciales <- sum(is.na(df_raw))
# El operador %>% (pipe) pasa el resultado de cada línea a la siguiente, como "luego"
df <- df_raw %>%
select(-starts_with("#")) %>% # select(): elimina las columnas auxiliares cuyo nombre empieza con "#"
rename_with(~ str_squish(.x)) %>% # str_squish(): quita espacios sobrantes en los nombres de columnas
rename(SEXO = GENERO, # rename(): cambia los nombres por otros más claros (NUEVO = VIEJO)
INFRACCION = NOM.INFRACCION,
TIPO_VEHICULO = VEHICULO,
MES_NOMBRE = `NOM MES`,
VALOR = `COSTO DEL COMPARENDO`,
FECHA = `FECHA DE COMPARENDO`) %>%
mutate(FECHA = as.Date(FECHA), # mutate(): crea o modifica columnas; as.Date() convierte a formato fecha
across(c(SEXO, INFRACCION, TIPO_VEHICULO, MES_NOMBRE), str_squish), # across(): aplica str_squish a varias columnas
INFRACCION = str_to_sentence(INFRACCION), # primera letra en mayúscula, el resto en minúscula
MES_NOMBRE = str_to_title(MES_NOMBRE)) %>% # Cada Palabra Con Mayúscula Inicial
drop_na(FECHA, VALOR) # drop_na(): elimina las filas sin fecha o sin valor
# complete.cases() marca las filas sin vacíos; su promedio x 100 es el % de datos completos
pct_completo <- round(mean(complete.cases(df)) * 100)Se eliminaron las columnas auxiliares, se estandarizaron los nombres y textos, se corrigieron los tipos de datos (fechas) y se descartaron registros sin fecha o valor.
# Genera las 4 tarjetas de indicadores en HTML.
# sprintf() reemplaza cada %d por un número entero, en orden:
# filas (nrow), columnas (ncol), nulos iniciales y % de datos completos.
# results='asis' hace que R escriba el HTML tal cual, sin formato de código.
# El JavaScript del inicio anima los contadores desde 0 hasta cada valor.
cat(sprintf('<div class="kpis">
<div class="kpi"><b data-n="%d">0</b><span>Total de registros</span></div>
<div class="kpi"><b data-n="%d">0</b><span>Variables</span></div>
<div class="kpi"><b data-n="%d">0</b><span>Valores nulos (iniciales)</span></div>
<div class="kpi"><b data-n="%d" data-s="%%">0</b><span>Datos luego de la limpieza</span></div>
</div>', nrow(df), ncol(df), nulos_iniciales, pct_completo))## [1] "No. MANDAMIENTO DE PAGO" "FECHA MANDAMIENTO DE PAGO"
## [3] "EJECUTADO" "TIPO DE IDENTIFICACION"
## [5] "No. IDENTIFICACION" "SEXO"
## [7] "COD. INFRACCION" "INFRACCION"
## [9] "COMPARENDO" "FECHA"
## [11] "AÑO" "MES"
## [13] "MES_NOMBRE" "DIA"
## [15] "NOM DIA" "PLACA DE VEHICULO"
## [17] "TIPO_VEHICULO" "VALOR"
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## "2010-04-12" "2010-04-27" "2010-05-06" "2010-05-05" "2010-05-16" "2010-05-27"
# FUNCIÓN barra_anim(): dibuja un gráfico de barras animado.
# Una función se escribe una vez y se reutiliza en todos los gráficos de barras.
# Parámetros: d (datos con columnas cat y val), color, horizontal (TRUE/FALSE),
# titulo, ejex y ejey (nombres de los ejes)
barra_anim <- function(d, color, horizontal = FALSE, titulo = "", ejex = "", ejey = "") {
# Si las barras son horizontales, str_wrap() parte los nombres largos en varias
# líneas (cada 28 caracteres) y <br> es el salto de línea en HTML
if (horizontal) {
d <- d %>% mutate(cat = str_replace_all(str_wrap(as.character(cat), 28), "\n", "<br>"))
}
# fct_inorder() mantiene las categorías en el orden en que aparecen
d <- d %>% mutate(cat = fct_inorder(as.character(cat)))
# ANIMACIÓN: map_dfr(1:15, ...) crea 15 copias de los datos (los "cuadros").
# En cada copia la barra mide val * .x / 15, así crece desde casi 0 hasta su valor real
fr <- map_dfr(1:15, ~ mutate(d, frame = .x, yy = val * .x / 15))
# Límite superior del eje: 10 % más que el valor máximo, para que la barra no toque el borde
tope <- max(d$val) * 1.1
# plot_ly() crea el gráfico. frame = ~frame le dice qué columna usar como cuadro de animación.
# Se hace una versión horizontal (orientation = "h") y otra vertical
p <- if (horizontal) {
plot_ly(fr, x = ~yy, y = ~cat, frame = ~frame, type = "bar", orientation = "h",
marker = list(color = color), text = ~comma(val), hoverinfo = "text+y") %>%
layout(xaxis = list(range = c(0, tope), title = ejey), yaxis = list(title = ejex))
} else {
plot_ly(fr, x = ~cat, y = ~yy, frame = ~frame, type = "bar",
marker = list(color = color), text = ~comma(val), hoverinfo = "text+x") %>%
layout(xaxis = list(title = ejex), yaxis = list(range = c(0, tope), title = ejey))
}
# layout(): apariencia (título, márgenes, alto, separación entre barras, colores de fondo,
# tipo de letra y formato de los números de los ejes)
p %>%
layout(title = list(text = paste0("<b>", titulo, "</b>"), x = 0.03,
y = 0.88, yanchor = "top",
font = list(family = "Poppins, sans-serif", size = 18)),
margin = list(t = 100, l = 20, r = 60, b = 80),
height = if (horizontal) 560 else 480, bargap = 0.35,
plot_bgcolor = "#f3f8ff", paper_bgcolor = "#ffffff",
font = list(family = "Poppins, sans-serif", size = 13, color = "#1f3358"),
xaxis = list(tickformat = if (horizontal) "~s" else ",.0f", nticks = 5,
automargin = TRUE, tickfont = list(size = 12)),
yaxis = list(tickformat = ",.0f", automargin = TRUE,
tickfont = list(size = 12))) %>%
animation_opts(frame = 40, transition = 30, redraw = FALSE) %>% # velocidad de la animación (milisegundos)
animation_slider(hide = TRUE) %>% # oculta la barra deslizadora
animation_button(visible = FALSE) %>% # oculta el botón Play
config(displayModeBar = FALSE) # oculta la barra de herramientas de plotly
}# count(FECHA): cuenta cuántos comparendos hubo cada día (columnas FECHA y n)
por_dia <- df %>% count(FECHA)
# Cuadros de la animación: en el cuadro k se dibujan los primeros k días,
# por eso la línea parece avanzar día por día
fr <- map_dfr(seq_len(nrow(por_dia)), ~ mutate(por_dia[1:.x, ], frame = .x))
# Tabla con un solo punto por cuadro (el día actual): ahí van la moto y la etiqueta
ult <- por_dia %>%
mutate(frame = row_number(),
etiqueta = paste0(format(FECHA, "%d/%m"), ": ", n, " comparendos"))
# El gráfico se arma con capas (add_trace):
plot_ly(elementId = "grafico-dia") %>%
# Capa 1: la línea con los datos
add_trace(data = fr, x = ~FECHA, y = ~n, frame = ~frame,
type = "scatter", mode = "lines+markers",
line = list(color = "#0fa3a0"), marker = list(color = "#0fa3a0"),
showlegend = FALSE, hoverinfo = "x+y") %>%
# Capa 2: la moto, que marca el día actual
add_trace(data = ult, x = ~FECHA, y = ~n, frame = ~frame,
type = "scatter", mode = "text",
text = "🏍️", textposition = "middle center",
textfont = list(size = 26),
showlegend = FALSE, hoverinfo = "none") %>%
# Capa 3: la etiqueta con fecha y cantidad, un poco más arriba (n + 8)
add_trace(data = ult, x = ~FECHA, y = ~n + 8, frame = ~frame,
type = "scatter", mode = "text",
text = ~etiqueta, textposition = "top center",
textfont = list(color = "#0d2a6b", size = 11),
showlegend = FALSE, hoverinfo = "none") %>%
# layout(): título, márgenes, colores, fuente y rango de los ejes
layout(title = list(text = "<b>Comparendos por día</b>", x = 0.03,
y = 0.88, yanchor = "top",
font = list(family = "Poppins, sans-serif", size = 18)),
margin = list(t = 100, r = 40, b = 70),
height = 480,
plot_bgcolor = "#f3f8ff", paper_bgcolor = "#ffffff",
font = list(family = "Poppins, sans-serif", size = 13, color = "#1f3358"),
xaxis = list(range = range(por_dia$FECHA), title = "Fecha"),
yaxis = list(range = c(0, max(por_dia$n) * 1.2), title = "Cantidad de comparendos")) %>%
# Velocidad: 250 ms por día, con transición suave de 200 ms
animation_opts(frame = 250, transition = 200, easing = "linear", redraw = FALSE) %>%
animation_slider(hide = TRUE) %>%
config(displayModeBar = FALSE)La variación diaria permite detectar días atípicos, operativos específicos, cambios de clima, festivos o tráfico inusual. El pico máximo fue de 89 comparendos el 01/05/2010. Este patrón ayuda a prever puntos críticos y ajustar los operativos.
# count(SEXO, sort = TRUE): cuenta comparendos por categoría y ordena de mayor a menor.
# rename(cat = SEXO, val = n): la función barra_anim() espera las columnas "cat" y "val"
d <- df %>% count(SEXO, sort = TRUE) %>% rename(cat = SEXO, val = n)
# Se llama a la función con tres colores (uno por barra) y los títulos de los ejes
barra_anim(d, c("#1b3f8f", "#1c7fc4", "#0fa3a0"), titulo = "Comparendos por género / empresa",
ejex = "Género", ejey = "Cantidad")Las mujeres registran la mayor proporción de comparendos (970), seguidas por hombres (683). Las empresas (467) también aportan un volumen considerable, lo que indica la necesidad de fortalecer el control sobre las flotas corporativas.
rec <- df %>%
group_by(INFRACCION) %>% # group_by(): agrupa las filas por tipo de infracción
summarise(Total = sum(VALOR, na.rm = TRUE)) %>% # summarise(): suma el valor de cada grupo (na.rm ignora vacíos)
arrange(Total) %>% # arrange(): ordena de menor a mayor
slice_tail(n = 10) %>% # slice_tail(): deja las 10 mayores (aquí solo hay 4 tipos)
rename(cat = INFRACCION, val = Total) # nombres que espera barra_anim()
# nrow(rec) cuenta las filas, así el título dice "Top 4" y se ajusta solo si cambian los datos.
# horizontal = TRUE porque los nombres de las infracciones son largos
barra_anim(rec, "#1c7fc4", horizontal = TRUE,
titulo = paste0("Top ", nrow(rec), " infracciones con mayor recaudo"),
ejex = "", ejey = "Total recaudado (millones de COP)")La infracción con mayor recaudo es Velocidad superior a la maxima permitida, con 418,695,000 COP, seguida de No detenerse ante una luz roja o amarilla (198,275,000 COP). Las dos primeras concentran el 95 % del recaudo, mientras que transitar en sentido contrario aporta muy poco por su baja ocurrencia o menor detección.
# Cuenta los comparendos por tipo de vehículo (auto o moto) y los ordena de mayor a menor
d <- df %>% count(TIPO_VEHICULO, sort = TRUE) %>% rename(cat = TIPO_VEHICULO, val = n)
barra_anim(d, "#2a4bb5", titulo = "Comparendos por tipo de vehículo", ejex = "Tipo de vehículo", ejey = "Cantidad")Los carros concentran 1,938 comparendos y las motos 182. Los vehículos particulares participan más, posiblemente por su volumen en circulación o mayor exposición a controles automatizados.
# Cuenta los comparendos de abril y mayo. Abril solo tiene datos desde el día 12,
# por eso la diferencia entre meses se debe en parte a que cubre menos días
d <- df %>% count(MES_NOMBRE, sort = TRUE) %>% rename(cat = MES_NOMBRE, val = n)
barra_anim(d, "#0fa3a0", titulo = "Comparendos por mes", ejex = "Mes", ejey = "Cantidad")En mayo se registraron 1,408 comparendos y en abril 712, un incremento de 98 %. Nota: abril solo cubre desde el 12 de ese mes.
top <- df %>%
filter(SEXO != "EMPRESA") %>% # filter(): conserva solo las filas que cumplen la condición (sin empresas)
count(EJECUTADO, sort = TRUE) %>% # cuenta los comparendos de cada persona
slice_head(n = 10) %>% # slice_head(): toma las 10 con más comparendos
arrange(n) %>% # las ordena para que la mayor quede arriba en el gráfico horizontal
rename(cat = EJECUTADO, val = n)
# El título usa nrow(top) para mostrar la cantidad real de personas
barra_anim(top, "#1b3f8f", horizontal = TRUE,
titulo = paste0("Top ", nrow(top), " personas con más comparendos"),
ejex = "", ejey = "Cantidad")Encabeza el listado Liliana Rozo Pinzon con 27 infracciones, lo que evidencia la necesidad de focalizar intervenciones educativas y sancionatorias en los reincidentes.
df %>%
group_by(Infracción = INFRACCION) %>% # agrupa por tipo de infracción
summarise(Comparendos = n(), # n(): cantidad de comparendos del grupo
`Recaudo (COP)` = sum(VALOR), # sum(): recaudo total
`Valor promedio` = mean(VALOR)) %>% # mean(): valor promedio por comparendo
arrange(desc(`Recaudo (COP)`)) %>% # ordena de mayor a menor recaudo
mutate(across(where(is.numeric), ~ comma(round(.x)))) %>% # redondea y pone separador de miles
kable(align = "lrrr") # kable(): presenta el resultado como tabla (l = izquierda, r = derecha)| Infracción | Comparendos | Recaudo (COP) | Valor promedio |
|---|---|---|---|
| Velocidad superior a la maxima permitida | 1,626 | 418,695,000 | 257,500 |
| No detenerse ante una luz roja o amarilla | 385 | 198,275,000 | 515,000 |
| No respetar el paso de peatones | 105 | 27,037,500 | 257,500 |
| Transitar en sentido contrario | 4 | 2,060,000 | 515,000 |
El análisis revela patrones claros: la mayor cantidad de infracciones la cometen mujeres y automóviles, y las infracciones más comunes (exceso de velocidad y semáforo en rojo) son también las de mayor impacto económico. Las empresas generan un volumen relevante, por lo que se requieren estrategias diferenciadas entre personas naturales y jurídicas.
El recaudo representa un impacto económico considerable y un potencial de reinversión en seguridad vial. Identificar reincidentes permite campañas educativas y sanciones más precisas; la gestión de comparendos no solo debe castigar, sino prevenir, orientar y transformar los hábitos de conducción.