Vista General

El Observatorio del Congreso es una plataforma de análisis cuantitativo del poder legislativo de México (Cámara de Diputados + Senado de la República). Utiliza un esquema unificado Popolo-Graph almacenado en SQLite para modelar legisladores, partidos, votos y redes de poder informales a lo largo de siete legislaturas (LX a LXVI, 2006-2027). El dataset cubre aproximadamente 3.5 millones de votos individuales, 9,437 eventos de votación y 4,840 personas. El código tiene 302 tests passing y corre sobre Python 3.12.

Pipeline

┌─────────────────────────────────────────────────────────────────────┐
│                       RECOLECCIÓN DE DATOS                          │
│                                                                     │
│  ┌──────────────────────┐      ┌──────────────────────────────────┐ │
│  │  Scraper Senado      │      │  Scraper Diputados               │ │
│  │  curl_cffi + TLS     │      │  httpx + BeautifulSoup           │ │
│  │  fingerprint         │      │                                  │ │
│  │  (Anti-WAF:          │      │  SITL / INFOPAL portal abierto   │ │
│  │   bypass Incapsula)  │      │  + API datos.abiertos            │ │
│  └──────────┬───────────┘      └──────────────┬───────────────────┘ │
│             │                                  │                    │
└─────────────┼──────────────────────────────────┼────────────────────┘
              │                                  │
              ▼                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       PARSEO Y CARGA                                │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  Transformers → Loaders (deduplicación vía source_id)        │   │
│  └──────────────────────────────┬───────────────────────────────┘   │
                                │                                   │
└─────────────────────────────────┼───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      CAPA DE ALMACENAMIENTO                         │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  SQLite (WAL mode) — congreso.db                             │   │
│  │  Esquema Popolo-Graph: 12 tablas                             │   │
│  │  area · organization · person · membership · post            │   │
│  │  motion · vote_event · vote · count                          │   │
│  │  actor_externo · relacion_poder · evento_politico            │   │
│  └──────────────────────────────┬───────────────────────────────┘   │
                                │                                   │
└─────────────────────────────────┼───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      CAPA DE ANÁLISIS                               │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
│  │  W-NOMINATE   │  │  Co-votación │  │  Detección de           │  │
│  │  (scipy,      │  │  Matriz      │  │  Comunidades            │  │
│  │   numpy)      │  │  y Grafo     │  │  (nx.community,         │  │
│  │               │  │              │  │   Louvain integrado)    │  │
│  └──────┬───────┘  └──────┬───────┘  └───────────┬──────────────┘  │
│         │                  │                       │                 │
│  ┌──────┴───────┐  ┌──────┴───────┐  ┌───────────┴──────────────┐  │
│  │  Centralidad  │  │  Índices de  │  │  Poder Empírico         │  │
│  │  (grado,      │  │  Poder       │  │  (des coaliciones de    │  │
│  │   betweenness)│  │  (Shapley-   │  │   votación reales)      │  │
│  │               │  │   Shubik,    │  │                          │  │
│  │               │  │   Banzhaf)   │  │                          │  │
│  └──────┬───────┘  └──────┬───────┘  └───────────┬──────────────┘  │
│         │                  │                       │                 │
└─────────┼──────────────────┼───────────────────────┼─────────────────┘
          │                  │                       │
          ▼                  ▼                       ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      CAPA DE EXPORTACIÓN                            │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  Archivos JSON → public/data/observatorio/                   │   │
│  │  Pre-agregados, estáticos, sin cómputo en servidor           │   │
│  └──────────────────────────────┬───────────────────────────────┘   │
                                │                                   │
└─────────────────────────────────┼───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                    CAPA DE VISUALIZACIÓN                             │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  CachorroSpace (Astro + Starlight)                           │   │
│  │  ECharts 6 vía React islands                                 │   │
│  │  Gráficas interactivas: mapas NOMINATE, grafos de co-voto,  │   │
│  │  índices de poder, estructuras comunitarias                  │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Stack Tecnológico

ComponenteTecnologíaPropósito
Scraping (Senado)curl_cffi + impersonación de huella TLSEvasión Anti-WAF para portal protegido por Incapsula
Scraping (Diputados)httpx + BeautifulSoupScraping del portal de datos abiertos (SITL / INFOPAL)
Base de datosSQLite (WAL mode)Almacenamiento unificado Popolo-Graph
Sistema de buildhatchlingPaquete instalable vía pyproject.toml
Análisis — NOMINATEscipy, numpy, matplotlibEstimación de puntos ideales (algoritmo W-NOMINATE)
Análisis — Redesnetworkx (Louvain integrado)Grafos de co-votación, detección de comunidades
Análisis — Podernumpy, scipyShapley-Shubik O(n²W) DP, índices Banzhaf
ExportaciónJSON (estático)Datos pre-agregados para visualizaciones
VisualizacionesECharts 6 (React islands)Gráficas interactivas en CachorroSpace
LoggingPython logging (centralizado)Logging estructurado vía runner_utils.setup_logging()

:::tip Todo el análisis se ejecuta offline contra la base de datos SQLite. No hay cómputo en servidor al momento de visualizar — los JSON de exportación se pre-computan y se sirven como archivos estáticos. :::

Fuentes de Datos

FuenteURLCámaraDatos
Cámara de Diputadosdatos_abiertos / SITL / INFOPALDiputadosRegistros de votación, perfiles de legisladores, composición
Senado de la Repúblicasenado.gob.mx/66/SenadoRegistros de votación, perfiles de senadores, directorio

:::note El portal del Senado está protegido por WAF Incapsula. El scraper usa curl_cffi con impersonate="chrome" para evadir la detección de huella TLS. El portal de Diputados es de acceso abierto y usa peticiones HTTP estándar vía httpx. :::

Flujo de Datos

El pipeline procesa datos en cuatro etapas:

1. Scraping y Parseo

Cada cámara tiene un scraper dedicado con su propio cliente HTTP, parser y módulos transformadores:

  • Senado: una sesión curl_cffi con impersonación TLS recupera las páginas de votación. Los parsers extraen datos de votación del HTML. Los transformers normalizan los datos al formato Popolo-Graph.
  • Diputados: un cliente httpx con caché basada en archivos y rate limiting consulta los sistemas SITL/INFOPAL. Los parsers manejan tanto respuestas XML como HTML.

2. Carga en SQLite

Los datos fluyen a través de loaders que insertan registros en congreso.db con deduplicación vía la columna source_id en la tabla vote_event. El módulo id_generator produce IDs legibles con prefijos (P01, O01, VE01, etc.).

3. Análisis

Los scripts de análisis leen desde SQLite y calculan:

  • W-NOMINATE: estimación de puntos ideales que ubica a los legisladores en un mapa ideológico 2D
  • Matriz de co-votación: tasas de acuerdo por pares entre legisladores, exportadas como grafos ponderados
  • Detección de comunidades: el algoritmo Louvain (vía nx.community) identifica bloques de votación dentro de las redes de co-votación
  • Centralidad: medidas de centralidad de grado y betweenness en grafos de co-votación
  • Índices de poder: Shapley-Shubik y Banzhaf basados en distribuciones de escaños
  • Poder empírico: medido a partir de datos reales de coaliciones de votación, no solo conteo de escaños

4. Exportación y Visualización

El script export_observatorio_json.py lee los CSV de salida del análisis y produce archivos JSON estáticos consumidos por las visualizaciones ECharts 6 embebidas como React islands en CachorroSpace.

analysis/output/*.csv


export_observatorio_json.py


public/data/observatorio/*.json


React ECharts islands (CachorroSpace)

Estructura del Proyecto

observatorio-congreso/
├── pyproject.toml               # build-system hatchling, deps, config ruff

├── scraper_congreso/            # Paquete instalable (pip install -e .)
│   ├── __init__.py
│   ├── diputados/               # Scraper de la Cámara de Diputados
│   │   ├── __init__.py
│   │   ├── __main__.py          # python -m scraper_congreso.diputados
│   │   ├── client.py            # Cliente HTTP httpx con caché SHA256
│   │   ├── config.py            # Legislaturas + mapeos de partidos
│   │   ├── models.py            # Modelos de datos Pydantic
│   │   ├── pipeline.py          # Pipeline principal de scraping
│   │   ├── loader.py            # Loader SQLite (dedup vía source_id)
│   │   ├── legislatura.py       # Lógica de rangos de legislatura
│   │   ├── transformers.py      # Normalización SITL → Popolo-Graph
│   │   └── parsers/
│   │       ├── votaciones.py    # Parser de eventos de votación
│   │       ├── nominal.py       # Parser de votos nominales (roll-call)
│   │       ├── desglose.py      # Parser de desglose de votos
│   │       ├── diputado.py      # Parser de perfil de legislador
│   │       └── composicion.py   # Parser de composición de la Cámara
│   │
│   ├── senadores/               # Scraper del Senado
│   │   ├── __init__.py
│   │   ├── client.py            # Cliente Anti-WAF (curl_cffi, 6 fingerprints)
│   │   ├── config.py            # Configuración del scraper
│   │   ├── models.py            # Modelos de datos compartidos
│   │   ├── votaciones/          # Scraper de registros de votación
│   │   │   ├── __init__.py
│   │   │   ├── __main__.py      # python -m scraper_congreso.senadores.votaciones
│   │   │   ├── cli.py           # Entry point CLI
│   │   │   ├── loader.py        # Loader SQLite
│   │   │   ├── transformers.py  # Normalización de datos
│   │   │   └── parsers/
│   │   │       └── lxvi_portal.py  # Parser del portal /66/ (GET + POST AJAX)
│   │   └── perfiles/            # Scraper de perfiles de senadores
│   │       ├── __init__.py
│   │       ├── __main__.py      # python -m scraper_congreso.senadores.perfiles
│   │       ├── scraper.py       # Lógica del scraper de perfiles
│   │       └── parsers/
│   │           └── perfil_parser.py
│   │
│   └── utils/                   # Utilidades compartidas
│       ├── __init__.py
│       ├── base_loader.py       # BaseLoader (patrones SQLite compartidos)
│       ├── db_helpers.py        # Funciones auxiliares de BD
│       ├── db_utils.py          # Funciones utilitarias de BD
│       ├── id_generator.py      # IDs legibles (P01, O01, VE01...)
│       ├── text_utils.py        # Normalización de texto
│       ├── config.py            # Configuración compartida
│       └── logging_config.py    # Configuración de logging

├── analysis/                    # 28 módulos (~13.8K líneas)
│   ├── constants.py             # PARTY_COLORS, ORG_TO_SHORT, PARTY_ORDER, COLORES_WEB
│   ├── config.py                # 8 parámetros ajustables (umbrales, seeds, IDs)
│   ├── db.py                    # Capa de acceso a datos (get_connection + 5 consultas parametrizadas)
│   ├── runner_utils.py          # Logging compartido, argparse, run_for_cameras
│   ├── nominate.py              # Implementación W-NOMINATE
│   ├── covotacion.py            # Matriz y grafo de co-votación
│   ├── covotacion_dinamica.py   # Co-votación dinámica con ventanas de tiempo (829 líneas)
│   ├── comunidades.py           # Louvain vía nx.community (seed=42)
│   ├── centralidad.py           # Centralidad de grado y betweenness
│   ├── poder_partidos.py        # Shapley-Shubik O(n²W) DP + Banzhaf
│   ├── poder_empirico.py        # Poder empírico desde votos reales
│   ├── evolucion_partidos.py    # Análisis de evolución de partidos
│   ├── efecto_genero.py         # Análisis de efecto de género
│   ├── efecto_curul_tipo.py     # Análisis de efecto por tipo de curul
│   ├── trayectorias.py          # Trayectorias individuales de legisladores
│   ├── visualizacion.py         # Exportaciones de visualización general
│   ├── visualizacion_nominate.py
│   ├── visualizacion_dinamica.py
│   ├── visualizacion_poder.py
│   ├── visualizacion_articulo.py
│   ├── run_analysis.py          # Ejecutar todos los análisis
│   ├── run_nominate.py          # Ejecutar solo NOMINATE
│   ├── run_covotacion_dinamica.py
│   ├── run_evolucion_partidos.py
│   ├── run_efecto_genero.py
│   ├── run_efecto_curul_tipo.py
│   └── run_trayectorias.py

├── db/
│   ├── schema.sql               # Esquema sincronizado (18 índices, 14 FKs, CHECKs corregidos)
│   ├── init_db.py               # PRAGMA FK ON + datos semilla
│   ├── constants.py             # LEGISLATURAS_ORDERED, CAMARA_IDS, mapeos de partidos
│   ├── congreso.db              # Base de datos SQLite (~337MB)
│   ├── migrations/              # 25 migraciones documentadas (todas aplicadas, idempotentes)
│   │   └── README.md            # Documentación de migraciones
│   └── archived/                # Archivos obsoletos (senado_schema.sql, helpers legacy)

├── tests/                       # 302 tests (passing)

├── scripts/
│   ├── mantener.sh              # Script de mantenimiento del proyecto
│   ├── backup_db.sh             # Backup de la base de datos
│   └── clean_cache.sh           # Limpieza de caché

└── cache/                       # Caché de respuestas HTTP

Configuración de la Base de Datos

SQLite se configura para acceso concurrente seguro e integridad de datos:

PRAGMA foreign_keys = ON;
PRAGMA encoding    = "UTF-8";
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;

PRAGMA foreign_keys = ON se ejecuta tanto en db/init_db.py como en analysis/db.py, garantizando integridad referencial independientemente del punto de entrada.

ConfiguraciónValorPropósito
journal_modeWALLecturas concurrentes sin bloquear escrituras
foreign_keysONAplicar integridad referencial entre tablas
busy_timeout5000msEsperar hasta 5 segundos si la BD está bloqueada
encodingUTF-8Manejo correcto de caracteres en español (acentos, ñ)

El esquema define 14 foreign keys con acciones ON DELETE / ON UPDATE explícitas: 3 usan CASCADE (para registros dependientes donde las eliminaciones deben propagarse) y 11 usan RESTRICT (para prevenir referencias huérfanas).

Resumen del Esquema

El esquema Popolo-Graph contiene 12 tablas con 18 índices, 14 foreign keys y 5 restricciones CHECK corregidas. Está organizado en cuatro grupos:

Entidades Popolo centrales (estándar de datos parlamentarios):

TablaPropósito
areaDivisiones geográficas (estados, distritos, circunscripciones)
organizationPartidos políticos, bancadas, coaliciones, instituciones
personLegisladores y actores políticos
membershipRelaciones persona-organización con roles y fechas
postCargos legislativos dentro de organizaciones y áreas
motionIniciativas y propuestas legislativas
vote_eventInstancias específicas de votación (cámara + fecha)
voteVotos individuales de legisladores por evento
countConteos agregados de votos por grupo por evento

Extensiones de redes de poder (más allá del estándar Popolo):

TablaPropósito
actor_externoActores externos (gobernadores, dirigentes, jueces)
relacion_poderRelaciones de poder informales (lealtad, presión, alianzas)
evento_politicoEventos políticos que afectan dinámicas de poder

:::note Todas las tablas usan IDs legibles con prefijos (P01 para persona, O01 para organización, VE01 para evento de votación, etc.). Esto hace que el debugging y las consultas manuales sean significativamente más fáciles que con claves primarias enteras opacas. :::

Mantenimiento del Esquema

El directorio db/migrations/ contiene 25 scripts de migración documentados, todos aplicados e idempotentes. Los archivos de esquema obsoletos (como el anterior senado_schema.sql y scripts helper legacy) se conservan en db/archived/ para referencia.

Índices

El esquema incluye 18 índices que cubren los patrones de consulta más frecuentes:

  • Consultas de membership por persona y por organización
  • Búsquedas de vote_event por motion y por source_id (deduplicación)
  • Consultas de vote por votante y por evento
  • Consultas de count por evento y por grupo
  • Consultas de relacion_poder por origen, destino y tipo
  • Filtrado de person por corriente interna (corriente_interna)

Restricciones de Integridad

Las restricciones CHECK de validación de fechas aseguran que end_date >= start_date en las tablas person y membership tanto para inserciones como para actualizaciones. Estas restricciones aplican integridad de datos a nivel de SQLite independientemente de qué loader escriba los datos.

Volúmenes de Datos

MétricaValor
Votos individuales~3,510,053
Eventos de votación~9,437
Personas~4,840
Organizaciones~20+
Legislaturas7 (LX a LXVI, 2006-2027)
Tests302 passing
Scripts de migración25 (todos aplicados)

Sistema de Build

El proyecto usa pyproject.toml con hatchling como backend de build, lo que permite instalar el scraper como paquete vía pip install -e ..

Entry Points

python -m scraper_congreso.diputados           # Scrapear Diputados
python -m scraper_congreso.senadores.votaciones # Scrapear votos del Senado
python -m scraper_congreso.senadores.perfiles   # Scrapear perfiles del Senado

Dependencias

Core (scraper): curl_cffi, httpx, beautifulsoup4, lxml, pydantic

Dev: pytest, ruff

Análisis: numpy, pandas, scipy, networkx, matplotlib, polars