Ir al contenido

Arquitectura

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-2026). El dataset cubre aproximadamente 3.5 millones de votos individuales, 8,000 eventos de votación y 3,800 legisladores.

┌─────────────────────────────────────────────────────────────────────┐
│ 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 │ │ (networkx, │ │
│ │ │ │ │ │ python-louvain) │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────┬──────────────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌───────────┴──────────────┐ │
│ │ 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 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
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
Análisis — NOMINATEscipy, numpy, matplotlibEstimación de puntos ideales (algoritmo W-NOMINATE)
Análisis — Redesnetworkx, python-louvainGrafos de co-votación, detección de comunidades
ExportaciónJSON (estático)Datos pre-agregados para visualizaciones
VisualizacionesECharts 6 (React islands)Gráficas interactivas en CachorroSpace
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

El pipeline procesa datos en cuatro etapas:

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.

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

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

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)
observatorio-congreso/
├── db/
│ ├── schema.sql # Esquema Popolo-Graph (12 tablas)
│ ├── senado_schema.sql # Extensiones de esquema para Senado
│ ├── init_db.py # Inicialización de BD + datos semilla
│ ├── helpers.py # Funciones auxiliares SQLite
│ ├── id_generator.py # Generación de IDs legibles (P01, O01...)
│ ├── constants.py # Mapeos de legislaturas y constantes
│ ├── migrations/ # Migraciones de esquema y correcciones
│ └── congreso.db # Base de datos SQLite (~3.5M votos)
├── diputados/
│ └── scraper/
│ ├── client.py # Cliente HTTP httpx con caché
│ ├── config.py # Configuración del scraper
│ ├── pipeline.py # Pipeline principal de scraping
│ ├── loader.py # Loader SQLite (dedup vía source_id)
│ ├── models.py # Modelos de datos
│ ├── legislatura.py # Lógica de rangos de legislatura
│ └── 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
├── senado/
│ └── scrapers/
│ ├── shared/
│ │ ├── client.py # Cliente Anti-WAF (curl_cffi)
│ │ ├── config.py # Configuración del scraper
│ │ └── models.py # Modelos de datos compartidos
│ ├── votaciones/
│ │ ├── __main__.py # Entry point CLI
│ │ ├── cli.py # Interfaz de línea de comandos
│ │ ├── transformers.py # Normalización de datos
│ │ ├── congreso_loader.py # Loader SQLite
│ │ └── parsers/
│ │ └── lxvi_portal.py # Parser del portal LXVI
│ └── perfiles/
│ ├── __main__.py # Entry point CLI
│ ├── scraper.py # Scraper de perfiles
│ └── parsers/
│ └── perfil_parser.py # Parser de perfil de senador
├── analysis/
│ ├── nominate.py # Implementación W-NOMINATE
│ ├── covotacion.py # Matriz y grafo de co-votación
│ ├── covotacion_dinamica.py # Co-votación dinámica (ventanas de tiempo)
│ ├── comunidades.py # Detección de comunidades Louvain
│ ├── centralidad.py # Centralidad de grado y betweenness
│ ├── poder_partidos.py # Índices Shapley-Shubik y Banzhaf
│ ├── poder_empirico.py # Poder empírico desde votos reales
│ ├── run_analysis.py # Ejecutar todos los análisis
│ ├── run_nominate.py # Ejecutar solo NOMINATE
│ ├── run_covotacion_dinamica.py # Ejecutar co-votación dinámica
│ ├── visualizacion.py # Exportaciones de visualización general
│ ├── visualizacion_nominate.py # Datos para gráficas NOMINATE
│ ├── visualizacion_dinamica.py # Datos para co-votación dinámica
│ ├── visualizacion_poder.py # Datos para índices de poder
│ ├── visualizacion_articulo.py # Visualizaciones específicas por artículo
│ ├── scripts/
│ │ └── export_observatorio_json.py # CSV → JSON para ECharts
│ ├── analisis-diputados/ # Salidas de análisis específicos Diputados
│ ├── analisis-senado/ # Salidas de análisis específicos Senado
│ └── analisis-bicameral/ # Salidas de análisis bicameral
├── utils/
│ ├── db_utils.py # Funciones utilitarias de base de datos
│ ├── text_utils.py # Utilidades de normalización de texto
│ └── tests/
│ └── test_text_utils.py # Tests de utilidades de texto
├── cache/ # Caché de respuestas HTTP
├── logs/ # Logs de scrapers
├── pyproject.toml # Dependencias del proyecto (uv)
└── scrape_diputados_all.sh # Script de scraping batch

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;
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 Popolo-Graph contiene 12 tablas, organizadas 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

El esquema incluye índices sobre 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)

Los triggers de validación de fechas aseguran que end_date >= start_date en las tablas person y membership tanto para inserciones como para actualizaciones. Estos se ejecutan a nivel de SQLite para prevenir corrupción de datos independientemente de qué loader escriba los datos.