import folium
import pandas as pd
import geopandas as gpd
import branca
import json
from folium.plugins import TimestampedGeoJsonDataset obtenido del data hub USFQ
- Objetivo: Desarrollar una infografia georeferenciada de las votaciones del Ecuador del año del 2002.
Manejo del json con 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... |
ETL del dataset
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
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()
]Manejo del archivo .json
# 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)
Estilo del Mapa
# 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')GENERACIÓN DE FEATURES PARA EL 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)CREAR MAPA BASE
m = folium.Map(
location=[-0.19899731681836336, -78.4428000494774],
tiles="Cartodb dark_matter", ## estilo del fondo mapa
zoom_start=6
)AÑADIR CAPA TEMPORAL
ts = TimestampedGeoJson(
{
"type": "FeatureCollection",
"features": features,
},
period="P1Y",
duration="P1Y",
add_last_point=True,
auto_play=False,
loop=False
).add_to(m)LEYENDA ( CANDIDATO Y COLOR ) y VISUALIZACION FINAL
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