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
| Componente | Tecnología | Propósito |
|---|---|---|
| Scraping (Senado) | curl_cffi + impersonación de huella TLS | Evasión Anti-WAF para portal protegido por Incapsula |
| Scraping (Diputados) | httpx + BeautifulSoup | Scraping del portal de datos abiertos (SITL / INFOPAL) |
| Base de datos | SQLite (WAL mode) | Almacenamiento unificado Popolo-Graph |
| Sistema de build | hatchling | Paquete instalable vía pyproject.toml |
| Análisis — NOMINATE | scipy, numpy, matplotlib | Estimación de puntos ideales (algoritmo W-NOMINATE) |
| Análisis — Redes | networkx (Louvain integrado) | Grafos de co-votación, detección de comunidades |
| Análisis — Poder | numpy, scipy | Shapley-Shubik O(n²W) DP, índices Banzhaf |
| Exportación | JSON (estático) | Datos pre-agregados para visualizaciones |
| Visualizaciones | ECharts 6 (React islands) | Gráficas interactivas en CachorroSpace |
| Logging | Python 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
| Fuente | URL | Cámara | Datos |
|---|---|---|---|
| Cámara de Diputados | datos_abiertos / SITL / INFOPAL | Diputados | Registros de votación, perfiles de legisladores, composición |
| Senado de la República | senado.gob.mx/66/ | Senado | Registros 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_cfficon 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
httpxcon 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ón | Valor | Propósito |
|---|---|---|
journal_mode | WAL | Lecturas concurrentes sin bloquear escrituras |
foreign_keys | ON | Aplicar integridad referencial entre tablas |
busy_timeout | 5000ms | Esperar hasta 5 segundos si la BD está bloqueada |
encoding | UTF-8 | Manejo 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):
| Tabla | Propósito |
|---|---|
area | Divisiones geográficas (estados, distritos, circunscripciones) |
organization | Partidos políticos, bancadas, coaliciones, instituciones |
person | Legisladores y actores políticos |
membership | Relaciones persona-organización con roles y fechas |
post | Cargos legislativos dentro de organizaciones y áreas |
motion | Iniciativas y propuestas legislativas |
vote_event | Instancias específicas de votación (cámara + fecha) |
vote | Votos individuales de legisladores por evento |
count | Conteos agregados de votos por grupo por evento |
Extensiones de redes de poder (más allá del estándar Popolo):
| Tabla | Propósito |
|---|---|
actor_externo | Actores externos (gobernadores, dirigentes, jueces) |
relacion_poder | Relaciones de poder informales (lealtad, presión, alianzas) |
evento_politico | Eventos 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
membershippor persona y por organización - Búsquedas de
vote_eventpor motion y porsource_id(deduplicación) - Consultas de
votepor votante y por evento - Consultas de
countpor evento y por grupo - Consultas de
relacion_poderpor origen, destino y tipo - Filtrado de
personpor 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étrica | Valor |
|---|---|
| Votos individuales | ~3,510,053 |
| Eventos de votación | ~9,437 |
| Personas | ~4,840 |
| Organizaciones | ~20+ |
| Legislaturas | 7 (LX a LXVI, 2006-2027) |
| Tests | 302 passing |
| Scripts de migración | 25 (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