R
Informe de Análisis de Datos

Comparendos de Tránsito – Barranquilla 2010

📅 Fecha: 27 de octubre de 2026
👤 Autor: Eduardo Pasion
📄 Curso: Programación para Analítica de Datos – SENA
🚗 🚓

Introducción

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.

Carga de datos

# ---- 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")])

Limpieza y preparación

# 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.

Análisis exploratorio

# 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))
0Total de registros
0Variables
0Valores nulos (iniciales)
0Datos luego de la limpieza
colnames(df)          # lista los nombres de las columnas ya limpias
##  [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"
summary(df$FECHA)     # fecha mínima, máxima y mediana: muestra el periodo que cubre la base
##         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"

Visualización de datos

# 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
}

Comparendos por día

# 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.

Comparendos por género

# 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.

Recaudo total por tipo de infracción

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.

Comparendos por tipo de vehículo

# 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.

Comparendos por mes

# 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 de personas con más comparendos

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.

Tabla de resultados

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

Conclusiones

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.

Referencias

  • Secretaría de Tránsito de Barranquilla. Base de datos de comparendos 2010.
  • Wickham, H. et al. tidyverse, plotly, readxl, knitr (paquetes de R).