Actualizado en septiembre de 2026: corregí cuántos territorios cubre el mapa, qué incluye el 41,5% de abstención y dónde están sus focos, cuánto pesan Bogotá, Medellín y Cali, y el nombre del mapa de burbujas, que antes llamaba cartograma. Además, dejé como hipótesis las causas que había dado por hechas.
Los mapas electorales que circularon después de la votación del 31 de mayo responden una sola pregunta: ¿quién va ganando?
Y esa, casi siempre, es la menos interesante.
Son mapas departamentales: promedian y esconden. Dentro del país, el preconteo de la Registraduría trae resultados de 1.122 municipios y territorios equivalentes, y el mapa departamental los aplana en un solo color por departamento. La pregunta que a mí me obsesiona es otra: ¿qué cuenta el territorio cuando bajas al nivel municipal y le aplicas análisis espacial en serio?
Así que construí un visor electoral para responderla. Lo que quiero compartirte aquí es lo que aprendí en el camino, porque si trabajas con datos del territorio, casi todo esto te sirve mañana.
Recorrido en video por la historia del visor (38 s, sin audio).El promedio nacional miente por omisión
A escala departamental, una capital que arrasa y un municipio a una hora de camino donde casi nadie votó terminan pintados del mismo color. Dentro de un departamento conviven municipios con realidades opuestas. Por eso leí la elección territorio por territorio.
Primero, las fuentes: el preconteo de la Registraduría Nacional (votos, censo, blancos y nulos por municipio). El visor lo dibuja sobre la geometría oficial del MGN 2024 del DANE, simplificada con topología para aligerarla sin romper las fronteras compartidas entre vecinos; los estadísticos espaciales, en cambio, los calculé sobre la geometría original de la Registraduría, sin simplificar. Ninguna de las dos geometrías trae a Nuevo Belén de Bajirá, en Chocó, que el preconteo sí reporta con 5.196 votantes. Así que el mapa y el análisis cubren 1.121 territorios, que el MGN clasifica en 1.102 municipios, 18 áreas no municipalizadas y la isla de San Andrés.
La ingesta y la normalización las hice en Python sobre un modelo estrella (territorio, candidato, resultado): esa estructura separa un análisis que vuelves a correr cuando llegan datos nuevos de un Excel que nadie vuelve a entender en tres semanas.
Bajar de departamento a municipio cambia la historia que cuenta el mapa. El problema tiene nombre, MAUP: cambias la unidad de agregación y cambia el resultado.
Mapa a escala departamental: grandes bloques de color que promedian regiones enteras.
El mismo dato a escala municipal: una pieza por territorio, donde afloran los contrastes internos. El color es el candidato ganador; la intensidad, su respaldo sobre el padrón.
Lo que hizo la IA y lo que decidí yo
No tecleé esto línea por línea en un fin de semana. Me apoyé en asistentes de IA con skills especializadas para el trabajo pesado: andamiar el pipeline de datos, generar y depurar las expresiones de Arcade, resolver el error de lectura de Parquet que cuento más abajo, y bosquejar análisis que luego validé contra los datos del preconteo.
Lo importante es lo que la IA no hizo: decidir por mí. Saber qué pregunta hacerle al territorio, qué modelo tiene sentido y cuándo desconfiar de un resultado demasiado bonito sigue siendo trabajo de ingeniero. La IA me devolvió el tiempo para concentrarme justo en eso.
En geoespacial, ese es el uso que me ha rendido: que la IA haga la plomería.
Un solo archivo para el notebook, la nube y el navegador
La capa de análisis vive en un GeoParquet de ~5,5 MB: columnar, comprimido, con la geometría en WKB y metadatos estándar. El formato permite leer por rangos de bytes y pedir solo el pedazo que hace falta. En este visor igual lo traigo entero, porque necesito todos los polígonos a la vez.
Lo mejor es la interoperabilidad: el mismo archivo lo abren DuckDB, GeoPandas, pyarrow y el navegador, sin una sola conversión. ArcGIS Maps SDK 5.0 lo lee directo en el cliente con ParquetLayer, sin servidor de mapas detrás. Para dibujarlo, eso sí, paso las entidades a un FeatureLayer en memoria: la beta de ParquetLayer no dibujaba DotDensity sobre polígonos grandes. Cero tiles propios y cero servicio publicado para los datos: lo único que llega de un servidor de mapas es el mapa base de Esri.
El detalle que te ahorra horas: el lector beta de
ParquetLayer(versión 5.0.19) pide un rango de bytes conend == filesize, un byte más allá del final del archivo. El servidor de desarrollo de Vite recorta ese rango y el lector rechaza la respuesta conFailed to fetch range. Servido desde almacenamiento de objetos (object storage; en mi caso, Cloudflare R2), carga sin problema. En local fallaba misteriosamente, en producción funcionaba. Si te topas con eso, sírvelo desde object storage y sigue.
Arquitectura: GeoParquet en almacenamiento de objetos (Cloudflare R2) → ParquetLayer → entidades en el cliente, sin servidor de mapas en el medio.
La forma del mapa es un argumento
El mismo dato cambia de sentido según cómo lo dibujes. Elige mal la forma y el mapa engaña, aunque cada número esté bien.
La coropleta (colorear municipios) es útil pero esconde una trampa: mide área. Los municipios enormes y despoblados dominan la pantalla y parecen pesar más votos de los que tienen. El mapa de burbujas (símbolos proporcionales) lo corrige: cada municipio pasa a ser un círculo del tamaño de sus votantes, y la geografía del voto se reordena por completo. Bogotá, Medellín y Cali, apenas tres municipios, suman más de un cuarto de los votantes, mientras los municipios más extensos y despoblados casi desaparecen. Y un mapa bivariado (ganador × intensidad del respaldo) separa a quien ganó arrasando de quien ganó raspando.
Todo con renderers (las reglas de simbología) nativos del SDK: ClassBreaks y UniqueValue para coropletas y categorías, DotDensity (1 punto = 2.500 votos, solo para los cuatro candidatos que pasaron del 2%) y visual variables de tamaño para las burbujas.
Antes de elegir un renderer, pregúntate qué estás midiendo. Si tu variable habla de personas, una coropleta por área te va a despistar.
Mapa de burbujas: cada municipio es un círculo del tamaño de sus votantes. Bogotá, Medellín y Cali suman más de un cuarto de ellos; compáralo con el mapa municipal de arriba, donde manda el área.
Deja que la estadística te contradiga
Un mapa bonito muestra qué se ve. El análisis espacial dice qué significa y, sobre todo, si lo que ves es un patrón o simple ruido. Lo apliqué sobre la abstención: en 824 territorios, casi tres de cada cuatro, superó al candidato más votado.
¿La abstención se parece a la de sus vecinos, o está regada al azar? El Moran's I dio 0,748: está fuertemente agrupada en el espacio. ¿Dónde están los focos significativos? El estadístico Getis-Ord Gi* los ubica con un 95% o un 99% de confianza. Si los cuentas por territorio, los calientes, de abstención alta, se concentran en el Caribe, el Chocó, buena parte de Antioquia y la Amazonía. Los fríos, de abstención baja, se agrupan en el altiplano cundiboyacense y Santander.
Pero el momento que más me enseñó fue otro. Lo intuitivo era explicar la abstención con variables internas del municipio: su tamaño, qué tan dividido está el voto. Lo modelé con una regresión y me dio un R² de 0,02. Casi nada. Y sin embargo, los residuos seguían agrupados en el espacio: el modelo se estaba perdiendo algo geográfico. Un modelo de rezago espacial, que incorpora la abstención de los municipios vecinos, reprodujo buena parte de la variación: pseudo-R² de 0,78, con ρ = 0,89. Ese pseudo-R² no es la misma medida que el R² de la regresión, así que no los compares uno a uno; aun así, un ρ tan alto confirma que el patrón es geográfico.
Dicho sin estadística: el tamaño del municipio y qué tan dividido está su voto casi no explican la abstención; dónde está el municipio se asocia mucho más con ella. Por qué pasa, estos datos no lo dicen. Las sospechas obvias son las distancias largas, los puestos de votación lejanos y las huellas del conflicto, que tienen que ver con qué tan fácil es, físicamente, llegar a la urna. Pero ninguna de esas variables entró al modelo: probarlas sería el siguiente paso.
Si te llevas una sola idea de este post, que sea esta: cuando tu modelo no explica nada pero los residuos están agrupados, no es que "no haya señal": es que la señal es espacial. (Herramientas: geopandas, pyarrow, libpysal/esda, spreg y topojson.)
Focos de abstención (Gi*): rojo y naranja, abstención alta significativa al 99% y al 95%; azul oscuro y azul claro, abstención baja al 99% y al 95%; gris, sin patrón significativo. El recuadro de texto de la captura conserva mi lectura anterior, que ponía los focos calientes en el Pacífico y la Amazonía; la corregida es la del texto.
Un popup también puede contar una historia
Al tocar un municipio en el visor aparece el top-5 de candidatos de ese lugar, cada barra pintada con el color de su candidatura. Dos piezas lo hacen posible. La primera es Arcade, un lenguaje de expresiones que corre en el cliente y me deja clasificar, calcular y formatear al vuelo sin volver a pedir datos. La segunda son los popups con CustomContent, que reemplazan el popup genérico por contenido a medida. Le sumé hitTest + highlight para resaltar el municipio bajo el cursor, y un toque de bloom para la estética firefly del mapa de burbujas: los municipios brillan como luciérnagas sobre el fondo oscuro.
El popup es el rincón más desaprovechado de un mapa web. Con Arcade y CustomContent cabe una minihistoria por cada municipio.
Seis actos en menos de dos minutos
Tener los datos y los análisis no basta: un tablero los muestra todos juntos y te deja escoger por dónde empezar; una historia te da el orden. Por eso convertí todo en un scrollytelling (un relato que avanza a medida que bajas por la página) de seis actos, que se lee en menos de dos minutos aunque nunca hayas tocado un mapa. Arranca con los dos candidatos que concentraron el respaldo entre los trece que se presentaron. Sigue con el área que distorsiona la lectura del voto, con la abstención y su geografía y con un mosaico de regiones que se inclinan distinto. Termina en los municipios más parejos, donde la elección se define por poco.
No mentir con los datos es una decisión
Tres decisiones que tomé a conciencia, y que valen para cualquier proyecto de datos:
- Distinguir las cifras. El 73,5% es el porcentaje de territorios donde la abstención superó al candidato más votado (824 de 1.121). La abstención de los 1.121 territorios del mapa, sin el voto del exterior, fue del 41,5% del censo electoral; el total nacional del preconteo, que sí incluye el exterior, da 42,11%. Son medidas distintas. El panel del visor, que se ve en tres de las capturas de este post, todavía rotula el 41,5% como "abstención nacional real": léelo como la abstención de esos 1.121 territorios.
- Área ≠ personas. Muestro la coropleta y el mapa de burbujas, a propósito: para que veas el sesgo y su corrección.
- Transparencia editorial. La clasificación por bloques (izquierda / centro / derecha) es una asignación editorial, porque el preconteo de la Registraduría no trae el nombre del partido ni la ideología; el visor lo advierte en cada popup y muestra el reparto por bloque de cada municipio. Esto es análisis descriptivo: te muestro qué pasó, con las cartas sobre la mesa, y el por qué queda abierto.
En 824 de los 1.121 territorios, la abstención superó al candidato más votado. Ese dato no sale en un mapa pintado por departamento.
Explora el proyecto
- Visor completo → el mapa municipio a municipio, con la historia en seis actos, los rankings y las fichas por municipio.
Cuéntame: ¿qué te muestra a ti el mapa de tu región que el promedio nacional esconde?