import folium
import pandas as pd
import geopandas as gpd
import branca
import json
from folium.plugins import TimestampedGeoJsonThe dataset was obtained from the USFQ Data Hub and from the following source:
Mendoza, K., Medina, P., & Astudillo, P. (2025).
Ecuadorian Presidential Elections (2002–2023): Canton-Level Results for First and Second Rounds.
Harvard Dataverse. https://doi.org/10.7910/DVN/A6ZP75
Objective:
Develop a georeferenced infographic that visualizes the voting results of the 2002 Ecuadorian presidential elections in Ecuador.
Handling JSON with GeoPandas
df_ecu = gpd.read_file("ecuador_provincias.json")
df_ecu["name"] = df_ecu["name"].str.upper()
df_ecu.head()| source | id | name | geometry | |
|---|---|---|---|---|
| 0 | https://simplemaps.com | ECE | ESMERALDAS | MULTIPOLYGON (((-78.47812 1.18712, -78.47831 1... |
| 1 | https://simplemaps.com | ECC | CARCHI | POLYGON ((-78.44211 0.87194, -78.44714 0.87437... |
| 2 | https://simplemaps.com | ECU | SUCUMBIOS | POLYGON ((-77.81408 0.34566, -77.79186 0.37099... |
| 3 | https://simplemaps.com | ECD | ORELLANA | POLYGON ((-77.29941 -0.05855, -77.28401 -0.047... |
| 4 | https://simplemaps.com | ECY | PASTAZA | POLYGON ((-77.05165 -1.01038, -77.07198 -1.033... |
Dataset ETL Process
df = pd.read_csv("presidentes_votacion_cantonal_formato_angosto.csv")
df.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 77462 entries, 0 to 77461
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ANIO 77462 non-null int64
1 VUELTA 77462 non-null int64
2 PROVINCIA_CODIGO 77462 non-null object
3 PROVINCIA_NOMBRE 77462 non-null object
4 CANTON_CODIGO 77462 non-null object
5 CANTON_NOMBRE 77462 non-null object
6 CANDIDATO_NOMBRE 77462 non-null object
7 AGRUPACION 77462 non-null object
8 VOTOS 77462 non-null int64
dtypes: int64(3), object(6)
memory usage: 5.3+ MB
# agrupacion para obtener valos por provincia
df_gr = df.groupby(["ANIO", "VUELTA", "PROVINCIA_NOMBRE",
"CANDIDATO_NOMBRE","PROVINCIA_CODIGO"])["VOTOS"].sum().reset_index()
# solo datos de Ecuador continental
df_gr = df_gr[~df_gr["PROVINCIA_NOMBRE"].isin(['EUROPA, ASIA, '
'OCEANIA', 'NORTE-AMERICA', 'LATAM, CARIBE, AFRICA'])]
# agrupacion por voto mayoritario en provinica
df_max = df_gr.loc[
df_gr.groupby(['PROVINCIA_NOMBRE','ANIO','VUELTA'])['VOTOS'].idxmax()
]Working with the .json File
# Load Ecuador's GeoJSON data
with open("ecuador_provincias.json", "r", encoding="utf-8") as f:
geo_json_data = json.load(f)
df_max.PROVINCIA_NOMBRE.unique()array(['AZUAY', 'BOLIVAR', 'CANAR', 'CARCHI', 'CHIMBORAZO', 'COTOPAXI', 'EL ORO', 'ESMERALDAS', 'GALAPAGOS',
'GUAYAS', 'IMBABURA', 'LOJA', 'LOS RIOS', 'MANABI', 'MORONA SANTIAGO', 'NAPO', 'ORELLANA', 'PASTAZA',
'PICHINCHA', 'SANTA ELENA', 'STO DGO TSACHILAS', 'SUCUMBIOS', 'TUNGURAHUA', 'ZAMORA CHINCHIPE'],
dtype=object)
# Nombre de la provincias
nombresGjson = sorted([x['properties']["name"].upper() for x in geo_json_data['features']])
# nombres a cambiar CAÑAR ,'SANTO DOMINGO DE LOS TSÁCHILAS' cambio en el dataset
df_max["PROVINCIA_NOMBRE"] = df_max["PROVINCIA_NOMBRE"].replace({'CANAR':'CAÑAR',
'STO DGO TSACHILAS':'SANTO DOMINGO DE LOS TSÁCHILAS'})
df_max["PROVINCIA_NOMBRE"].unique()array(['AZUAY', 'BOLIVAR', 'CAÑAR', 'CARCHI', 'CHIMBORAZO', 'COTOPAXI', 'EL ORO', 'ESMERALDAS', 'GALAPAGOS',
'GUAYAS', 'IMBABURA', 'LOJA', 'LOS RIOS', 'MANABI', 'MORONA SANTIAGO', 'NAPO', 'ORELLANA', 'PASTAZA',
'PICHINCHA', 'SANTA ELENA', 'SANTO DOMINGO DE LOS TSÁCHILAS', 'SUCUMBIOS', 'TUNGURAHUA',
'ZAMORA CHINCHIPE'], dtype=object)
Map Style
# COLORES POR CANDIDATO
mapa_colores ={'ALVARO NOBOA':"#980BEF", 'JACOBO BUCARAM':"#1070B9", 'LUCIO GUTIERREZ':"#E01212",
'RODRIGO BORJA':"#FEB00A", 'XAVIER NEIRA':"#F6FE69",'RAFAEL CORREA':"#07F93F",
'LENIN MORENO':"#07F93F",'YAKU PEREZ':"#F9D107",'GUILLERMO LASSO':"#1070B9",
'LUISA GONZALEZ':"#34FFF8",'DANIEL NOBOA':"#980BEF", 'NULOS':"#FFFFFF",
'FERNANDO VILLAVICENCIO':"#96B0FF",'XAVIER HERVAS':"#F9D107",
'ANDRES ARAUZ':"#07F93F"
}
# aplicar color al dataset
df_max['COLOR'] = df_max['CANDIDATO_NOMBRE'].apply(lambda x:mapa_colores.get(x))
df_max.head()| ANIO | VUELTA | PROVINCIA_NOMBRE | CANDIDATO_NOMBRE | PROVINCIA_CODIGO | VOTOS | COLOR | |
|---|---|---|---|---|---|---|---|
| 10 | 2002 | 1 | AZUAY | RODRIGO BORJA | EC01 | 157246 | #FEB00A |
| 290 | 2002 | 2 | AZUAY | LUCIO GUTIERREZ | EC01 | 371564 | #E01212 |
| 397 | 2006 | 1 | AZUAY | RAFAEL CORREA | EC01 | 166524 | #07F93F |
| 765 | 2006 | 2 | AZUAY | RAFAEL CORREA | EC01 | 429830 | #07F93F |
| 879 | 2009 | 1 | AZUAY | RAFAEL CORREA | EC01 | 418082 | #07F93F |
def generar_start_end(row, col_anio='ANIO', col_vuelta='VUELTA'):
"""
Devuelve start y end como timestamps (ms) según:
- Vuelta 1: del 1 de enero al 1 de julio
- Vuelta 2: del 2 de julio al 31 diciembre
"""
anio = int(row[col_anio])
vuelta = int(row[col_vuelta])
if vuelta == 1:
start = pd.Timestamp(f"{anio}-01-01 00:00:00")
end = pd.Timestamp(f"{anio}-07-01 00:00:00")
else: # vuelta 2
start = pd.Timestamp(f"{anio}-07-02 00:00:00")
end = pd.Timestamp(f"{anio}-12-31 23:59:59")
# convertir a timestamp en milisegundos
return {
'start': float(start.value // 10**6),
'end': float(end.value // 10**6)
}df_max['start_end'] = df_max.apply(lambda x :
generar_start_end(x, col_anio='ANIO', col_vuelta='VUELTA'),
axis=1
)df_merged = df_max.merge(df_ecu[['name', 'geometry']],
left_on='PROVINCIA_NOMBRE',
right_on='name',
how='inner')
# Convertir en GeoDataFrame
gdf_final = gpd.GeoDataFrame(df_merged, geometry='geometry')Feature Generation for the Timeline
features = []
for _, row in gdf_final.iterrows():
start_ts = int(row['start_end']['start']) // 1000 # segundos UNIX
start_date = pd.to_datetime(start_ts, unit='s').strftime("%Y-%m-%dT%H:%M:%S")
feature = {
"type": "Feature",
"geometry": row["geometry"].__geo_interface__,
"properties": {
"time": start_date,
"style": {
"color": "black",
"weight": 1,
"opacity": 1,
"fillColor": row["COLOR"],
"fillOpacity": 0.8
},
"tooltip": f"Provincia: {row['PROVINCIA_NOMBRE']} - {row['CANDIDATO_NOMBRE']}",
"popup": (
f"<b>Provincia:</b> {row['PROVINCIA_NOMBRE']}<br>"
f"<b>Candidato:</b> {row['CANDIDATO_NOMBRE']}<br>"
f"<b>Votos:</b> {row['VOTOS']}<br>"
f"<b>Año:</b> {row['ANIO']} - Vuelta {row['VUELTA']}"
)
}
}
features.append(feature)Base Map
m = folium.Map(
location=[-0.19899731681836336, -78.4428000494774],
tiles="Cartodb dark_matter", ## estilo del fondo mapa
zoom_start=6
)Adding a Temporal Layer
ts = TimestampedGeoJson(
{
"type": "FeatureCollection",
"features": features,
},
period="P1Y",
duration="P1Y",
add_last_point=True,
auto_play=False,
loop=False
).add_to(m)Legend (Candidate and Color) and Final Visualization
items_html = ""
for candidato, color in mapa_colores.items():
items_html += f'''
<i class="fa fa-square" style="color:{color};"></i>
{candidato}<br>'''
legend_html = f"""
{{% macro html(this, kwargs) %}}
<div style="
position: fixed;
bottom: 50px; left: 50px;
width: 250px;
border:2px solid grey;
z-index:9999;
font-size:14px;
background-color:white;
opacity: 0.9;
padding: 10px;
border-radius: 5px;">
<b>🗳️ Candidato ganador</b><br>
{items_html}
</div>
{{% endmacro %}}
"""
legend = branca.element.MacroElement()
legend._template = branca.element.Template(legend_html)
m.get_root().add_child(legend)
m.save("VotEcuador_Timeline_Legend.html")
mMake this Notebook Trusted to load map: File -> Trust Notebook