Hermosillo al límite: estrés urbano y accidentes viales
Diagnostico inicial
Hermosillo al límite: estrés urbano y accidentes viales

Foto de Volodymyr Kozhevnikov en Unsplash
Diagnostico inicial
Salir en carro en Hermosillo se ha convertido, para muchos, en un ejercicio diario de paciencia y estrés: tráfico lento en casi todas las vialidades principales, prisas por llegar al trabajo, presión por dejar y recoger a los hijos en la escuela, y la sensación constante de que “en cualquier esquina puede haber un choque”.
Actualmente, Hermosillo es la segunda ciudad más poblada del noroeste, solo por debajo de Tijuana, y la quinta más grande en el norte del país. Con este ritmo de crecimiento, se prevé que en 2028 la ciudad alcance el millón de habitantes. Surge entonces la pregunta: ¿está Hermosillo preparada para garantizar una movilidad incluyente y segura para toda su población, como ya lo hacen otras ciudades mexicanas? (Sánchez, V. M. V., 2025)
¿Qué acciones tomó el gobierno?
Como parte de las acciones permanentes para fortalecer la seguridad vial y prevenir accidentes, el Gobierno Municipal, a través de la Dirección General de Desarrollo de Infraestructura (DGDI), ha instalado más de 2 mil 200 señales verticales, aplicado más de 370 mil metros lineales de pintura y atendido mil 100 reportes de semaforización en Hermosillo desde enero de 2025. (Proyecto Puente, 2025)
¿Qué problemas se busca resolver?
Durante el desglose del presente trabajo, nos enfocaremos en resolver las preguntas en cuestión:
¿Cómo han evolucionado los accidentes viales en Hermosillo a lo largo del tiempo y cuáles son las ventanas críticas de riesgo donde las autoridades deberían intervenir primero?
Objetivo
Analizar los accidentes viales registrados por INEGI en Hermosillo entre 2018 y 2024 para identificar cómo evolucionan en el tiempo y cuáles son las ventanas de mayor riesgo (por año, mes, día, día de la semana y hora), con el fin de proponer intervenciones focalizadas para autoridades municipales y estatales.
Diseño de la base de datos: Extracción y Limpieza
Para diseñar la base de datos del proyecto partimos del conjunto de datos de accidentes de tránsito del INEGI (ATUS) para el periodo 2018–2024 en México.
Creamos un pipeline de datos automatizado que descarga, descomprime y limpia registros de siniestros viales utilizando Python. Pasaremos de tener varios archivos CSV a un único dataset “Tidy” listo para análisis, manteniendo todo organizado con la aplicación de la estructura ofrecida por la librería CookieCutter Data Science.
# Crear el entorno virtual del proyecto
make create_environment
# Ejecutar el pipeline completo de datos
make pipeline
Todo el pipeline se orquesta con el comando: **make pipeline**
Este comando crea el entorno virtual, instala dependencias y ejecuta los scripts de descarga y limpieza, generando al final un archivo inegi_tidy.csv en la carpeta /data/processed/.
Arquitectura del proyecto
El proyecto utiliza la estructura recomendada por Cookiecutter Data Science, lo que nos permite separar con claridad los datos crudos, los datos procesados y el código:
/data
/raw ← descargas originales (INEGI en CSV)
/processed ← dataset final limpio (inegi_tidy.csv)
/proyecto_ciencia_de_datos
/dataset.py ← scripts de descarga (Extract)
/tidy_data.py (o similar) ← scripts de limpieza (Transform)
De esta forma, cualquier persona puede clonar el repositorio, ejecutar make pipeline y obtener exactamente los mismos archivos originales de INEGI, sin pasos manuales.
Extracción automatizada de los accidentes (INEGI)
En lugar de descargar manualmente los datos desde la página del INEGI, automatizamos la ingesta con un script de Python, integrado en el módulo dataset.py.
from datetime import datetime
import io
import os
import zipfile
import requests
from proyecto_ciencia_de_datos.config import RAW_DATA_DIR
# Carpeta donde se guardan los archivos crudos de INEGI
RAW_DIR_INEGI = RAW_DATA_DIR / "inegi"
RAW_DIR_INEGI.mkdir(parents=True, exist_ok=True)
def descargar_datos_inegi():
"""
Descarga el ZIP oficial de accidentes urbanos/suburbanos de INEGI
y extrae todos los CSV en data/raw/inegi.
"""
url = (
"https://www.inegi.org.mx/contenidos/programas/"
"accidentes/datosabiertos/conjunto_de_datos_atus_anual_csv.zip"
)
print(f"[{datetime.now()}] Descargando datos de INEGI…")
resp = requests.get(url)
resp.raise_for_status()
with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
zf.extractall(RAW_DIR_INEGI)
print(f"✅ Datos de INEGI descomprimidos en: {RAW_DIR_INEGI}")
Transformación a un dataset “tidy” para series de tiempo
Una vez descargados los archivos de INEGI, el siguiente paso es convertir ese conjunto de CSV dispersos en un único dataset coherente y listo para análisis.
Para resolver esto, el script tidy_data.py toma todos los CSV dispersos y los convierte en un único DataFrame limpio. Asimismo, definimos una lista maestra de columnas y el script valida cada archivo dentro del bucle de procesamiento y, si detecta que faltan los encabezados, los inyecta automáticamente antes de integrar todo.
import re
from pathlib import Path
import numpy as np
import pandas as pd
from proyecto_ciencia_de_datos.config import RAW_DATA_DIR, PROCESSED_DATA_DIR
RAW_DIR_INEGI = RAW_DATA_DIR / "inegi"
PROCESSED_DIR = PROCESSED_DATA_DIR
def construir_inegi_tidy():
"""
Lee todos los CSV de INEGI en data/raw/inegi, los concatena
y genera un archivo inegi_tidy.csv en data/processed.
"""
csv_paths = sorted(RAW_DIR_INEGI.glob("*.csv"))
frames = []
for path in csv_paths:
# Leemos el archivo anual
df = pd.read_csv(path, low_memory=False)
# Extraemos el año desde el nombre de archivo (ej. 'atus2018.csv')
match = re.search(r"(\d{4})", path.name)
if match:
df["anio"] = int(match.group(1))
frames.append(df)
inegi_all = pd.concat(frames, ignore_index=True)
# Ejemplo: selección de columnas clave para el diseño de la BD
columnas_clave = [
"ID_ACC", # identificador del accidente (ejemplo)
"FECHA", # fecha del accidente
"HORA", # hora del accidente
"CVE_ENT", # clave de entidad
"CVE_MUN", # clave de municipio
"TIPO_ACC", # tipo de accidente
"CAUSA_ACC", # causa principal
"NUM_HERIDOS", # personas lesionadas
"NUM_MUERTOS", # personas fallecidas
"anio", # año (derivado)
]
# Nos quedamos solo con las columnas que existan en el DataFrame
columnas_existentes = [c for c in columnas_clave if c in inegi_all.columns]
inegi_tidy = inegi_all[columnas_existentes].copy()
# Conversión de fecha y hora a tipos apropiados (si aplica)
if "FECHA" in inegi_tidy.columns:
inegi_tidy["FECHA"] = pd.to_datetime(
inegi_tidy["FECHA"], errors="coerce"
)
# Ejemplo: generar día de la semana (útil para el EDA posterior)
if "FECHA" in inegi_tidy.columns:
inegi_tidy["dia_semana"] = inegi_tidy["FECHA"].dt.day_name()
# Guardamos el resultado
output_path = PROCESSED_DIR / "inegi_tidy.csv"
inegi_tidy.to_csv(output_path, index=False)
print(f"✅ Archivo tidy guardado en: {output_path}")
return inegi_tidy
El resultado se guarda en la carpeta /data/processed/ con el nombre inegi_tidy.csv.
A partir de este archivo inegi_tidy.csv construiremos tanto el análisis exploratorio (EDA) de series de tiempo como la tabla principal de la base de datos accidentes_inegi.
Diseño de la base de datos: Cargar con PostgreSQL
Una vez que contamos con el archivo inegi_tidy.csv en la carpeta /data/processed/, el siguiente paso es cargar este dataset en una base de datos relacional que nos permita hacer consultas flexibles y reproducir fácilmente todas las gráficas y análisis de series de tiempo.
Para este proyecto elegí PostgreSQL como motor de base de datos.
Creación de la tabla "accidentes_inegi"
La tabla **accidentes_inegi almacena cada accidente individual** registrado en la Estadística de Accidentes de Tránsito Terrestre en Zonas Urbanas y Suburbanas (ATUS) de INEGI. Cada fila corresponde a un accidente y las columnas recogen tanto la localización como el momento del evento y su severidad.
-- Tabla detallada con un registro por accidente
CREATE TABLE IF NOT EXISTS accidentes_inegi (
accidente_id BIGSERIAL PRIMARY KEY, -- PK interna
id_acc_inegi BIGINT, -- folio / ID original de INEGI (opcional)
-- Localización
edo SMALLINT NOT NULL, -- clave entidad (Sonora = 26)
mpio SMALLINT NOT NULL, -- clave municipio (Hermosillo = 49)
localidad INTEGER, -- clave de localidad
-- Dimensión temporal
fecha DATE NOT NULL,
hora SMALLINT, -- 0-23
minuto SMALLINT, -- 0-59
-- Tipo y causa del accidente
tipo_accidente SMALLINT, -- TIPACCID (colisión, atropellamiento, etc.)
causa_accidente SMALLINT, -- CAUSAACCI (conductor, peatón, etc.)
responsable_conductor BOOLEAN, -- TRUE si la causa es conductor
-- Severidad (víctimas por tipo)
num_conductores_muertos SMALLINT,
num_conductores_heridos SMALLINT,
num_pasajeros_muertos SMALLINT,
num_pasajeros_heridos SMALLINT,
num_peatones_muertos SMALLINT,
num_peatones_heridos SMALLINT,
num_ciclistas_muertos SMALLINT,
num_ciclistas_heridos SMALLINT,
num_otros_muertos SMALLINT,
num_otros_heridos SMALLINT,
-- Agregados de severidad
total_muertos SMALLINT,
total_heridos SMALLINT,
-- Estimación de daños materiales (si se utiliza)
danos_vehiculos NUMERIC(12,2), -- daños en vehículos
danos_propiedad_estado NUMERIC(12,2), -- daños en propiedad del estado
danos_propiedad_particular NUMERIC(12,2), -- daños en propiedad particular
otros_danos NUMERIC(12,2), -- otros daños
-- Metadatos
creado_en TIMESTAMPTZ DEFAULT now()
);
-- Índices útiles para consultas de series de tiempo y filtrado geográfico
CREATE INDEX IF NOT EXISTS idx_accidentes_inegi_fecha
ON accidentes_inegi (fecha);
CREATE INDEX IF NOT EXISTS idx_accidentes_inegi_edo_mpio_fecha
ON accidentes_inegi (edo, mpio, fecha);
CREATE INDEX IF NOT EXISTS idx_accidentes_inegi_fecha_hora
ON accidentes_inegi (fecha, hora);
Creación de la tabla “accidentes_diarios"
La tabla **accidentes_diarios no almacena accidentes individuales; en su lugar, guarda un resumen por día, entidad y municipio. **Esta tabla se genera a partir de una consulta GROUP BY sobre accidentes_inegi. Es la base para casi todas las gráficas de series de tiempo que se buscan realizar.
-- Tabla agregada por día y municipio
CREATE TABLE IF NOT EXISTS accidentes_diarios (
fecha DATE NOT NULL,
edo SMALLINT NOT NULL,
mpio SMALLINT NOT NULL,
total_accidentes INTEGER NOT NULL,
total_muertos INTEGER,
total_heridos INTEGER,
PRIMARY KEY (fecha, edo, mpio)
);
-- Índice específico para consultas por municipio
CREATE INDEX IF NOT EXISTS idx_accidentes_diarios_mpio_fecha
ON accidentes_diarios (edo, mpio, fecha);
-- Para poblarla a partir de accidentes_inegi
INSERT INTO accidentes_diarios (fecha, edo, mpio, total_accidentes, total_muertos, total_heridos)
SELECT
fecha,
edo,
mpio,
COUNT(*) AS total_accidentes,
COALESCE(SUM(total_muertos), 0) AS total_muertos,
COALESCE(SUM(total_heridos), 0) AS total_heridos
FROM accidentes_inegi
GROUP BY fecha, edo, mpio;
Análisis Exploratorio de Datos (EDA)
Primeramente, necesitamos crear un engine de SQLAlchemy para conectarte a la BD.
from sqlalchemy import create_engine
import pandas as pd
import matplotlib.pyplot as plt
DB_URL = "postgresql+psycopg2://usuario:password@localhost:5432/accidentes_hmo"
engine = create_engine(DB_URL)
Cantidad de accidentes por año en Hermosillo, Sonora
Usamos la tabla agregada accidentes_diarios, sumando por año, para crear la gráfica:
# 1. Accidentes por año en Hermosillo (2018–2024)
query_anual = """
SELECT fecha, total_accidentes
FROM accidentes_diarios
WHERE edo = 26 AND mpio = 49
AND fecha BETWEEN '2018-01-01' AND '2024-12-31'
"""
df_anual = pd.read_sql(query_anual, engine, parse_dates=["fecha"])
df_anual["anio"] = df_anual["fecha"].dt.year
acc_por_anio = (
df_anual
.groupby("anio", as_index=False)["total_accidentes"]
.sum()
.sort_values("anio")
)
plt.figure(figsize=(8, 4))
plt.bar(acc_por_anio["anio"], acc_por_anio["total_accidentes"])
plt.xlabel("Año")
plt.ylabel("Número de accidentes")
plt.title("Accidentes viales por año en Hermosillo (2018–2024)")
plt.xticks(acc_por_anio["anio"])
plt.tight_layout()
plt.show()

El análisis de datos de INEGI (2018–2024) revela una problemática creciente de accidentes viales en Hermosillo. Se registró un alza del 59% en 2021 (vs. 2020)
Cantidad de accidentes por día (serie diaria)
Aquí también nos conviene accidentes_diarios y graficar la serie de tiempo diaria.
# 2. Accidentes por día en Hermosillo (serie diaria)
query_diaria = """
SELECT fecha, total_accidentes
FROM accidentes_diarios
WHERE edo = 26 AND mpio = 49
AND fecha BETWEEN '2018-01-01' AND '2024-12-31'
ORDER BY fecha
"""
df_diaria = pd.read_sql(query_diaria, engine, parse_dates=["fecha"])
plt.figure(figsize=(12, 4))
plt.plot(df_diaria["fecha"], df_diaria["total_accidentes"])
plt.xlabel("Fecha")
plt.ylabel("Número de accidentes")
plt.title("Accidentes viales por día en Hermosillo (2018–2024)")
plt.tight_layout()
plt.show()

El viernes es el día con mayor número de accidentes viales en la ciudad. En términos humanos, esto significa que el cierre de la semana laboral, combinado con el hambre, el cansancio y las prisas de la hora de comida y salida, se refleja directamente en el pavimento.
Cantidad de accidentes por hora
Para esto sí necesitamos la tabla detallada accidentes_inegi, porque ahí está la hora de cada accidente.
# 3. Accidentes por hora del día en Hermosillo (2018–2024)
query_hora = """
SELECT fecha, hora
FROM accidentes_inegi
WHERE edo = 26 AND mpio = 49
AND fecha BETWEEN '2018-01-01' AND '2024-12-31'
AND hora IS NOT NULL
"""
df_hora = pd.read_sql(query_hora, engine, parse_dates=["fecha"])
# Aseguramos que la hora sea tipo entero
df_hora["hora"] = df_hora["hora"].astype(int)
acc_por_hora = (
df_hora
.groupby("hora", as_index=False)
.size()
.rename(columns={"size": "total_accidentes"})
.sort_values("hora")
)
plt.figure(figsize=(10, 4))
plt.bar(acc_por_hora["hora"], acc_por_hora["total_accidentes"])
plt.xlabel("Hora del día")
plt.ylabel("Número de accidentes")
plt.title("Accidentes viales por hora del día en Hermosillo (2018–2024)")
plt.xticks(range(0, 24))
plt.tight_layout()
plt.show()

El análisis horario de los accidentes en Hermosillo revela dos ventanas críticas de riesgo:
- Entre las 9:00–10:00 de la mañana
- Entre las 2:00–4:00 de la tarde
Estas franjas horarias coinciden con las horas de entrada y salida de la mayoría de los trabajos y escuelas.
Accidentes por día del mes en Hermosillo (2018–2024)
Queremos ver cómo se distribuyen a lo largo de los días 1–31 del mes, agregando todos los meses y años del periodo. De nuevo usamos accidentes_diarios:
# 4. Accidentes por día del mes en Hermosillo (2018–2024)
query_dia_mes = """
SELECT fecha, total_accidentes
FROM accidentes_diarios
WHERE edo = 26 AND mpio = 49
AND fecha BETWEEN '2018-01-01' AND '2024-12-31'
"""
df_dia_mes = pd.read_sql(query_dia_mes, engine, parse_dates=["fecha"])
df_dia_mes["dia_mes"] = df_dia_mes["fecha"].dt.day
acc_por_dia_mes = (
df_dia_mes
.groupby("dia_mes", as_index=False)["total_accidentes"]
.sum()
.sort_values("dia_mes")
)
plt.figure(figsize=(10, 4))
plt.bar(acc_por_dia_mes["dia_mes"], acc_por_dia_mes["total_accidentes"])
plt.xlabel("Día del mes")
plt.ylabel("Número de accidentes (2018–2024)")
plt.title("Accidentes viales por día del mes en Hermosillo (2018–2024)")
plt.xticks(range(1, 32))
plt.tight_layout()
plt.show()

A lo largo del año, la cantidad de accidentes por día del mes muestra una tendencia creciente. Al comparar el primer trimestre con el último, se estima un incremento del 30% en el número de accidentes, equivalente a cerca de 1,200 percances adicionales en los últimos meses del año.
Conclusiones
El análisis exploratorio basado en los datos de INEGI para Hermosillo entre 2018 y 2024 muestra una ciudad donde los accidentes viales no son hechos aislados, sino un patrón estructural:
- A nivel anual, la siniestralidad se ha mantenido alta e incluso se ha incrementado respecto a los niveles pre-pandemia, con apenas signos recientes de estabilización.
- A nivel diario, algunos periodos del año concentran una parte desproporcionada de los accidentes, lo que indica ventanas críticas de riesgo ligadas a temporadas festivas, escolares y comerciales.
- A nivel horario, las franjas de mayor movilidad (entrada/salida de trabajo y escuela) son las más peligrosas, lo que sugiere una fuerte relación entre diseño de horarios, infraestructura y comportamiento.
- A nivel de día del mes, se observan patrones vinculados a quincenas y fines de mes, donde el aumento de actividad económica y social se traduce también en más siniestros.
En conjunto, el proyecto confirma que Hermosillo no puede normalizar el peligro, no solo porque haya muchos autos, sino porque la forma en que se combinan infraestructura, horarios, reglas y comportamientos genera un entorno donde el error humano se vuelve inevitable y, demasiadas veces, mortal.
Referencias
- Sánchez, V. M. V. (2025, 27 octubre). Los Números de la Movilidad — Hermosillo ¿como vamos? https://hermosillocomovamos.org/2025/10/27/los-numeros-de-la-movilidad/
- Redacción. (2025, 1 diciembre). Se instalaron más de 2 mil señales viales en Hermosillo durante 2025, 9% de accidentes se debe a falta de altos: Ayuntamiento — Proyecto Puente. Proyecto Puente. https://proyectopuente.com.mx/2025/12/01/se-instalaron-mas-de-2-mil-senales-viales-en-hermosillo-durante-2025-9-de-accidentes-se-debe-a-falta-de-altos-ayuntamiento/t
메타데이터
- post_id
- 429c8ff0f09c
- slug
- hermosillo-al-límite-estrés-urbano-y-accidentes-viales-429c8ff0f09c
- url
- https://medium.com/@mikeguerrero24/hermosillo-al-l%C3%ADmite-estr%C3%A9s-urbano-y-accidentes-viales-429c8ff0f09c
- canonical_url
- https://medium.com/@mikeguerrero24/hermosillo-al-l%C3%ADmite-estr%C3%A9s-urbano-y-accidentes-viales-429c8ff0f09c
- author_url
- https://medium.com/@mikeguerrero24
- status
- ok
- fetched_at
- 2026-06-27 18:20:27