← Back to list

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

Diagnostico inicial

Miguel Ángel Guerrero Espinoza · 2025-12-08 09:48 · 2 claps · 9.6 min read
#accidentes-de-tránsito #trafico #accident #seguridad #choques
Open on Medium ↗
Wiki topics: CLI · Clinical Medicine

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

Foto de Volodymyr Kozhevnikov en Unsplash

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


메타데이터
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