Módulo 2 — Clase 6: Mapas interactivos para política pública con Folium#

Curso: Python y Políticas Públicas
Nivel: Avanzado
Duración estimada: 2 horas


Objetivos de la clase#

  • Entender las diferencias entre visualización geoespacial estática (matplotlib/geopandas) e interactiva (folium)

  • Crear mapas de Argentina con marcadores, coropléticos y capas de calor

  • Combinar múltiples capas en un panel de política pública interactivo

  • Exportar mapas como HTML para reportes y dashboards


1. Introducción a Folium#

¿Por qué Folium?#

En las clases anteriores usamos matplotlib y geopandas para visualizar datos geoespaciales. Estas herramientas producen mapas estáticos — imágenes PNG o SVG que se incrustan en reportes pero no permiten exploración interactiva.

Folium es una librería Python que genera mapas interactivos basados en Leaflet.js. Los mapas resultantes son archivos HTML que funcionan en cualquier navegador sin dependencias adicionales.

Característica

matplotlib / geopandas

folium

Tipo de salida

Imagen estática (PNG/SVG)

HTML interactivo

Zoom / pan

No

Popups en marcadores

No

Capas toggle

No

Tiles de mapa base

No (sin fondo)

Sí (OpenStreetMap, CartoDB, etc.)

Ideal para

Publicaciones, informes impresos

Dashboards, reportes web, presentaciones

¿Cuándo usar cada uno en política pública?#

  • Publicación académica o informe oficial impreso → matplotlib/geopandas

  • Dashboard para funcionarios, mapa en sitio web → folium

  • Análisis exploratorio propio → cualquiera, dependiendo de la comodidad

Versión y compatibilidad#

Esta clase usa folium 0.20.0. La API cambió en versiones recientes: algunas funciones que encontrarán en tutoriales viejos (pre-0.15) ya no funcionan igual.

import folium
import folium.plugins
import pandas as pd
import numpy as np
import requests
import json
import warnings
warnings.filterwarnings('ignore')

print(f'Folium versión: {folium.__version__}')
Folium versión: 0.20.0
import matplotlib as mpl
import matplotlib.pyplot as plt

# --- Paleta de identidad del curso ---
C = ['#2A6496', '#E07B3F', '#3D9970', '#8E5EA2', '#C0A830', '#637A8A']

mpl.rcParams.update({
    'figure.figsize'       : (10, 5),
    'font.size'            : 11,
    'axes.titlesize'       : 12,
    'axes.titleweight'     : 'normal',
    'axes.spines.top'      : False,
    'axes.spines.right'    : False,
    'legend.frameon'       : False,
    'axes.prop_cycle'      : mpl.cycler(color=C),
    'figure.dpi'           : 110,
})

def tit(ax, t, **kw):
    """Título sin negrita, alineado a la izquierda."""
    ax.set_title(t, loc='left', fontweight='normal', **kw)

2. Mapa base de Argentina#

Todo mapa folium empieza con folium.Map(). Los parámetros clave son:

  • location: coordenadas del centro [lat, lon]

  • zoom_start: nivel de zoom inicial (1=mundo, 5=país, 10=ciudad)

  • tiles: proveedor de tiles del mapa base

# Mapa base centrado en Argentina continental
m_base = folium.Map(
    location=[-38.4, -63.6],
    zoom_start=4,
    tiles='CartoDB positron'  # estilo limpio, ideal para datos superpuestos
)

# Mostrar en el notebook (renderiza el HTML inline)
m_base
Make this Notebook Trusted to load map: File -> Trust Notebook
# Tiles disponibles en folium sin token adicional:
tiles_disponibles = [
    'OpenStreetMap',      # Clásico OSM
    'CartoDB positron',   # Gris claro, minimalista
    'CartoDB dark_matter' # Fondo oscuro
]

print('Tiles utilizables directamente (sin API key):')
for t in tiles_disponibles:
    print(f'  - {t}')

print('\nPara tiles con API key (Mapbox, Google Maps), se usa el parámetro tiles con URL template.')
Tiles utilizables directamente (sin API key):
  - OpenStreetMap
  - CartoDB positron
  - CartoDB dark_matter

Para tiles con API key (Mapbox, Google Maps), se usa el parámetro tiles con URL template.

3. Marcadores: capitales provinciales por región#

Vamos a agregar las 24 capitales provinciales como marcadores circulares, coloreados según la región geográfica.

# Datos de las 24 provincias (capital, coordenadas, región, población aproximada)
capitales = [
    # Provincia, Capital, Lat, Lon, Región, Población (miles)
    ('CABA',           'Ciudad de Buenos Aires', -34.60, -58.38, 'GBA',        3075),
    ('Buenos Aires',   'La Plata',               -34.92, -57.95, 'Pampeana',    800),
    ('Córdoba',        'Córdoba',                -31.42, -64.18, 'Pampeana',   1600),
    ('Santa Fe',       'Santa Fe',               -31.63, -60.70, 'Pampeana',    550),
    ('Entre Ríos',     'Paraná',                 -31.73, -60.53, 'Pampeana',    360),
    ('Corrientes',     'Corrientes',             -27.47, -58.83, 'NEA',         380),
    ('Misiones',       'Posadas',                -27.37, -55.90, 'NEA',         370),
    ('Formosa',        'Formosa',                -26.18, -58.17, 'NEA',         200),
    ('Chaco',          'Resistencia',            -27.45, -58.99, 'NEA',         430),
    ('Jujuy',          'San Salvador de Jujuy',  -24.18, -65.30, 'NOA',         320),
    ('Salta',          'Salta',                  -24.79, -65.41, 'NOA',         640),
    ('Tucumán',        'San Miguel de Tucumán',  -26.82, -65.22, 'NOA',         950),
    ('Santiago del Estero', 'Santiago del Estero', -27.78, -64.27, 'NOA',       280),
    ('Catamarca',      'San Fernando del Valle', -28.47, -65.78, 'NOA',         180),
    ('La Rioja',       'La Rioja',               -29.41, -66.86, 'NOA',         210),
    ('San Juan',       'San Juan',               -31.54, -68.54, 'Cuyo',        550),
    ('Mendoza',        'Mendoza',                -32.89, -68.84, 'Cuyo',       1100),
    ('San Luis',       'San Luis',               -33.30, -66.34, 'Cuyo',        250),
    ('La Pampa',       'Santa Rosa',             -36.62, -64.29, 'Pampeana',    140),
    ('Neuquén',        'Neuquén',                -38.95, -68.06, 'Patagonia',   390),
    ('Río Negro',      'Viedma',                 -40.81, -63.00, 'Patagonia',    65),
    ('Chubut',         'Rawson',                 -43.30, -65.10, 'Patagonia',    35),
    ('Santa Cruz',     'Río Gallegos',           -51.62, -69.22, 'Patagonia',   110),
    ('Tierra del Fuego','Ushuaia',               -54.80, -68.30, 'Patagonia',    85),
]

df_capitales = pd.DataFrame(capitales, columns=['provincia', 'capital', 'lat', 'lon', 'region', 'poblacion_miles'])

# Paleta de colores por región
colores_region = {
    'GBA':       '#E84393',  # rosa
    'Pampeana':  '#3A86FF',  # azul
    'NOA':       '#FF9F43',  # naranja
    'NEA':       '#2ECC71',  # verde
    'Cuyo':      '#9B59B6',  # violeta
    'Patagonia': '#1ABC9C',  # turquesa
}

print(df_capitales[['provincia', 'region', 'poblacion_miles']].to_string())
              provincia     region  poblacion_miles
0                  CABA        GBA             3075
1          Buenos Aires   Pampeana              800
2               Córdoba   Pampeana             1600
3              Santa Fe   Pampeana              550
4            Entre Ríos   Pampeana              360
5            Corrientes        NEA              380
6              Misiones        NEA              370
7               Formosa        NEA              200
8                 Chaco        NEA              430
9                 Jujuy        NOA              320
10                Salta        NOA              640
11              Tucumán        NOA              950
12  Santiago del Estero        NOA              280
13            Catamarca        NOA              180
14             La Rioja        NOA              210
15             San Juan       Cuyo              550
16              Mendoza       Cuyo             1100
17             San Luis       Cuyo              250
18             La Pampa   Pampeana              140
19              Neuquén  Patagonia              390
20            Río Negro  Patagonia               65
21               Chubut  Patagonia               35
22           Santa Cruz  Patagonia              110
23     Tierra del Fuego  Patagonia               85
# Crear mapa con marcadores
m_marcadores = folium.Map(
    location=[-38.4, -63.6],
    zoom_start=4,
    tiles='CartoDB positron'
)

# Agregar leyenda de regiones
leyenda_html = '''
<div style="position:fixed; bottom:30px; left:30px; z-index:1000; background:white;
            padding:12px 16px; border-radius:8px; box-shadow:2px 2px 6px rgba(0,0,0,0.3);
            font-family:Arial; font-size:13px;">
<b>Regiones geográficas</b><br>
'''
for region, color in colores_region.items():
    leyenda_html += f'<span style="color:{color};">&#9679;</span> {region}<br>'
leyenda_html += '</div>'
m_marcadores.get_root().html.add_child(folium.Element(leyenda_html))

# Agregar CircleMarker para cada capital
for _, row in df_capitales.iterrows():
    color = colores_region[row['region']]
    
    # Radio proporcional a la población (escala visual)
    radio = 4 + np.log1p(row['poblacion_miles']) * 1.2
    
    # Contenido del popup
    popup_html = f"""
    <div style='font-family:Arial; min-width:160px;'>
        <b style='font-size:14px;'>{row['provincia']}</b><br>
        <span style='color:#555;'>Capital:</span> {row['capital']}<br>
        <span style='color:#555;'>Región:</span> <span style='color:{color};'>{row['region']}</span><br>
        <span style='color:#555;'>Población:</span> {row['poblacion_miles']:,} mil hab.
    </div>
    """
    
    folium.CircleMarker(
        location=[row['lat'], row['lon']],
        radius=radio,
        color=color,
        fill=True,
        fill_color=color,
        fill_opacity=0.75,
        weight=1.5,
        tooltip=f"{row['capital']} ({row['provincia']})",
        popup=folium.Popup(popup_html, max_width=220)
    ).add_to(m_marcadores)

print('Mapa con marcadores creado. Hacé click en un marcador para ver el popup.')
m_marcadores
Mapa con marcadores creado. Hacé click en un marcador para ver el popup.
Make this Notebook Trusted to load map: File -> Trust Notebook

4. Mapa coroplético: tasa de pobreza por provincia#

Un mapa coroplético colorea las áreas geográficas según el valor de una variable cuantitativa. Es el tipo de mapa más usado en política social para mostrar indicadores provinciales o municipales.

Necesitamos dos inputs:

  1. Un GeoJSON con los polígonos de las provincias

  2. Un DataFrame con los datos a mapear, con una columna de identificador que coincida con el GeoJSON

# Tasas de pobreza por provincia (datos simulados, en % de personas)
# Basado aproximadamente en datos EPH 2023
datos_pobreza = {
    'Buenos Aires': 43.5, 'CABA': 16.4, 'Catamarca': 37.2, 'Chaco': 55.8,
    'Chubut': 28.9, 'Córdoba': 35.6, 'Corrientes': 47.3, 'Entre Ríos': 38.4,
    'Formosa': 51.2, 'Jujuy': 46.7, 'La Pampa': 26.1, 'La Rioja': 44.8,
    'Mendoza': 39.1, 'Misiones': 49.3, 'Neuquén': 32.4, 'Río Negro': 34.7,
    'Salta': 50.6, 'San Juan': 41.3, 'San Luis': 36.9,
    'Santa Cruz': 24.3, 'Santa Fe': 37.8, 'Santiago del Estero': 52.1,
    'Tierra del Fuego': 20.5, 'Tucumán': 48.9
}

df_pobreza = pd.DataFrame(list(datos_pobreza.items()), columns=['provincia', 'tasa_pobreza'])
print(df_pobreza.sort_values('tasa_pobreza', ascending=False).to_string(index=False))
          provincia  tasa_pobreza
              Chaco          55.8
Santiago del Estero          52.1
            Formosa          51.2
              Salta          50.6
           Misiones          49.3
            Tucumán          48.9
         Corrientes          47.3
              Jujuy          46.7
           La Rioja          44.8
       Buenos Aires          43.5
           San Juan          41.3
            Mendoza          39.1
         Entre Ríos          38.4
           Santa Fe          37.8
          Catamarca          37.2
           San Luis          36.9
            Córdoba          35.6
          Río Negro          34.7
            Neuquén          32.4
             Chubut          28.9
           La Pampa          26.1
         Santa Cruz          24.3
   Tierra del Fuego          20.5
               CABA          16.4
# Intentar cargar GeoJSON de Argentina desde GitHub
GEOJSON_URL = 'https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/argentina.geojson'

geojson_data = None

try:
    resp = requests.get(GEOJSON_URL, timeout=10)
    resp.raise_for_status()
    geojson_data = resp.json()
    print(f'GeoJSON cargado desde URL. Provincias encontradas: {len(geojson_data["features"])}')
    # Mostrar cómo se llama la propiedad de nombre en el GeoJSON
    sample = geojson_data['features'][0]['properties']
    print(f'Propiedades disponibles en el GeoJSON: {list(sample.keys())}')
except Exception as e:
    print(f'No se pudo cargar el GeoJSON desde URL: {e}')
    print('Usando GeoJSON de fallback con 5 provincias...')
No se pudo cargar el GeoJSON desde URL: 404 Client Error: Not Found for url: https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/argentina.geojson
Usando GeoJSON de fallback con 5 provincias...
# GeoJSON de fallback (polígonos simplificados de 5 provincias)
# Se usa si la URL no está disponible
geojson_fallback = {
    "type": "FeatureCollection",
    "features": [
        {"type": "Feature", "properties": {"name": "Buenos Aires"},
         "geometry": {"type": "Polygon", "coordinates": [[
             [-63.0, -34.0], [-57.0, -34.0], [-57.0, -41.0], [-63.0, -41.0], [-63.0, -34.0]
         ]]}},
        {"type": "Feature", "properties": {"name": "Córdoba"},
         "geometry": {"type": "Polygon", "coordinates": [[
             [-66.0, -29.0], [-62.0, -29.0], [-62.0, -35.0], [-66.0, -35.0], [-66.0, -29.0]
         ]]}},
        {"type": "Feature", "properties": {"name": "Santa Fe"},
         "geometry": {"type": "Polygon", "coordinates": [[
             [-62.0, -28.0], [-59.0, -28.0], [-59.0, -34.0], [-62.0, -34.0], [-62.0, -28.0]
         ]]}},
        {"type": "Feature", "properties": {"name": "Chaco"},
         "geometry": {"type": "Polygon", "coordinates": [[
             [-63.0, -24.0], [-59.0, -24.0], [-59.0, -29.0], [-63.0, -29.0], [-63.0, -24.0]
         ]]}},
        {"type": "Feature", "properties": {"name": "Mendoza"},
         "geometry": {"type": "Polygon", "coordinates": [[
             [-70.5, -32.0], [-67.5, -32.0], [-67.5, -37.5], [-70.5, -37.5], [-70.5, -32.0]
         ]]}}
    ]
}

if geojson_data is None:
    geojson_data = geojson_fallback
    df_pobreza_mapa = df_pobreza[df_pobreza['provincia'].isin(
        [f['properties']['name'] for f in geojson_fallback['features']]
    )].copy()
else:
    df_pobreza_mapa = df_pobreza.copy()

# Detectar el campo de nombre en el GeoJSON
props_keys = list(geojson_data['features'][0]['properties'].keys())
# Buscar el campo que más probablemente contenga el nombre de provincia
nombre_key = 'name'
for k in props_keys:
    if 'name' in k.lower() or 'nombre' in k.lower() or 'provincia' in k.lower():
        nombre_key = k
        break
print(f'Campo de nombre usado para el join: "{nombre_key}"')
Campo de nombre usado para el join: "name"
# Crear mapa coroplético
m_corop = folium.Map(
    location=[-38.4, -63.6],
    zoom_start=4,
    tiles='CartoDB positron'
)

folium.Choropleth(
    geo_data=geojson_data,
    name='Tasa de pobreza (%)',
    data=df_pobreza_mapa,
    columns=['provincia', 'tasa_pobreza'],
    key_on=f'feature.properties.{nombre_key}',
    fill_color='YlOrRd',       # Amarillo -> Naranja -> Rojo (más pobreza = más oscuro)
    fill_opacity=0.75,
    line_opacity=0.3,
    legend_name='Tasa de pobreza (% de personas, EPH 2023)',
    nan_fill_color='lightgray',
    nan_fill_opacity=0.4,
    highlight=True,
).add_to(m_corop)

# Tooltips sobre los polígonos
folium.GeoJson(
    geojson_data,
    style_function=lambda x: {'fillOpacity': 0, 'weight': 0},
    tooltip=folium.GeoJsonTooltip(
        fields=[nombre_key],
        aliases=['Provincia:'],
        style='font-family: Arial; font-size: 13px;'
    )
).add_to(m_corop)

folium.LayerControl().add_to(m_corop)

print('Mapa coroplético creado. Las provincias más oscuras tienen mayor tasa de pobreza.')
m_corop
Mapa coroplético creado. Las provincias más oscuras tienen mayor tasa de pobreza.
Make this Notebook Trusted to load map: File -> Trust Notebook

5. Mapa de calor: concentración de beneficiarios#

El mapa de calor (HeatMap) muestra la densidad de puntos en el espacio. Es útil para visualizar dónde se concentran los beneficiarios de un programa, oficinas de atención, incidentes de pobreza extrema, etc.

En este ejemplo, simulamos 500 beneficiarios de un programa social en el área metropolitana de Buenos Aires.

np.random.seed(99)

# Simular beneficiarios en el AMBA
# Dos núcleos de concentración: zona sur (La Matanza, Lomas) y zona oeste (Moreno, Merlo)
n_benef = 500

# Núcleo 1: conurbano sur (60% de los beneficiarios)
n1 = int(n_benef * 0.60)
lat1 = np.random.normal(-34.75, 0.18, n1)
lon1 = np.random.normal(-58.45, 0.25, n1)

# Núcleo 2: conurbano oeste (40%)
n2 = n_benef - n1
lat2 = np.random.normal(-34.58, 0.15, n2)
lon2 = np.random.normal(-58.70, 0.20, n2)

lats = np.concatenate([lat1, lat2])
lons = np.concatenate([lon1, lon2])

df_benef = pd.DataFrame({'lat': lats, 'lon': lons})
print(f'Beneficiarios simulados: {len(df_benef):,}')
print(df_benef.describe().round(3))
Beneficiarios simulados: 500
           lat      lon
count  500.000  500.000
mean   -34.667  -58.545
std      0.193    0.258
min    -35.304  -59.444
25%    -34.796  -58.725
50%    -34.654  -58.556
75%    -34.533  -58.381
max    -34.107  -57.813
# Crear mapa de calor
m_calor = folium.Map(
    location=[-34.62, -58.55],
    zoom_start=10,
    tiles='CartoDB dark_matter'  # Fondo oscuro resalta mejor el heatmap
)

# HeatMap espera una lista de [lat, lon] o [lat, lon, weight]
heat_data = df_benef[['lat', 'lon']].values.tolist()

folium.plugins.HeatMap(
    heat_data,
    name='Densidad de beneficiarios',
    min_opacity=0.3,
    radius=18,
    blur=12,
    gradient={0.2: 'blue', 0.5: 'lime', 0.8: 'yellow', 1.0: 'red'}
).add_to(m_calor)

# Añadir título
titulo_html = '''
<div style="position:fixed; top:15px; left:50%; transform:translateX(-50%);
            z-index:1000; background:rgba(0,0,0,0.7); color:white;
            padding:8px 16px; border-radius:6px; font-family:Arial; font-size:14px;">
    <b>Concentración de beneficiarios — Programa Social AMBA</b>
</div>
'''
m_calor.get_root().html.add_child(folium.Element(titulo_html))

folium.LayerControl().add_to(m_calor)

print('Mapa de calor creado. Zonas más brillantes = mayor concentración de beneficiarios.')
m_calor
Mapa de calor creado. Zonas más brillantes = mayor concentración de beneficiarios.
Make this Notebook Trusted to load map: File -> Trust Notebook

6. Guardar mapas como HTML#

# Guardar cada mapa como archivo HTML independiente
m_marcadores.save('mapa_capitales.html')
m_corop.save('mapa_coropletico_pobreza.html')
m_calor.save('mapa_calor_beneficiarios.html')

print('Mapas guardados:')
print('  → mapa_capitales.html')
print('  → mapa_coropletico_pobreza.html')
print('  → mapa_calor_beneficiarios.html')
print()
print('Estos archivos HTML son autocontenidos: incluyen todo el JavaScript de Leaflet.')
print('Se pueden compartir por email o subir a un servidor web sin dependencias adicionales.')
Mapas guardados:
  → mapa_capitales.html
  → mapa_coropletico_pobreza.html
  → mapa_calor_beneficiarios.html

Estos archivos HTML son autocontenidos: incluyen todo el JavaScript de Leaflet.
Se pueden compartir por email o subir a un servidor web sin dependencias adicionales.

7. Panel de política pública: mapa con múltiples capas#

En la práctica, los dashboards de política pública requieren mostrar múltiples capas de información que el usuario pueda activar o desactivar según sus necesidades. Folium permite hacer esto con FeatureGroup y LayerControl.

# Mapa base del panel
m_panel = folium.Map(
    location=[-36.0, -63.6],
    zoom_start=4,
    tiles='CartoDB positron'
)

# =====================================================
# CAPA 1: Coroplético de pobreza
# =====================================================
capa_corop = folium.FeatureGroup(name='Tasa de pobreza provincial (%)', show=True)

folium.Choropleth(
    geo_data=geojson_data,
    data=df_pobreza_mapa,
    columns=['provincia', 'tasa_pobreza'],
    key_on=f'feature.properties.{nombre_key}',
    fill_color='YlOrRd',
    fill_opacity=0.65,
    line_opacity=0.3,
    legend_name='Tasa de pobreza (%)',
    nan_fill_color='lightgray',
    highlight=True,
    name='_corop_interno'  # nombre interno para la leyenda de folium
).add_to(m_panel)  # El Choropleth va directo al mapa para que la leyenda aparezca

# =====================================================
# CAPA 2: Marcadores de capitales
# =====================================================
capa_marcadores = folium.FeatureGroup(name='Capitales provinciales', show=True)

for _, row in df_capitales.iterrows():
    color = colores_region[row['region']]
    radio = 4 + np.log1p(row['poblacion_miles']) * 1.0
    
    # Buscar tasa de pobreza para esta provincia
    match = df_pobreza[df_pobreza['provincia'] == row['provincia']]
    tasa_str = f"{match['tasa_pobreza'].values[0]:.1f}%" if len(match) > 0 else 'N/D'
    
    popup_html = f"""
    <div style='font-family:Arial; min-width:180px;'>
        <b style='font-size:14px;'>{row['provincia']}</b><br>
        <span style='color:#555;'>Capital:</span> {row['capital']}<br>
        <span style='color:#555;'>Región:</span> <span style='color:{color}; font-weight:bold;'>{row['region']}</span><br>
        <span style='color:#555;'>Población capital:</span> {row['poblacion_miles']:,} mil<br>
        <span style='color:#555;'>Tasa de pobreza:</span> <span style='color:#e74c3c; font-weight:bold;'>{tasa_str}</span>
    </div>
    """
    
    folium.CircleMarker(
        location=[row['lat'], row['lon']],
        radius=radio,
        color=color,
        fill=True,
        fill_color=color,
        fill_opacity=0.8,
        weight=1.5,
        tooltip=f"{row['capital']} — Pobreza: {tasa_str}",
        popup=folium.Popup(popup_html, max_width=230)
    ).add_to(capa_marcadores)

capa_marcadores.add_to(m_panel)

# =====================================================
# CAPA 3: Mapa de calor AMBA
# =====================================================
capa_calor = folium.FeatureGroup(name='Beneficiarios AMBA (densidad)', show=False)

folium.plugins.HeatMap(
    heat_data,
    min_opacity=0.35,
    radius=16,
    blur=10,
    gradient={0.2: '#ffffb2', 0.4: '#fecc5c', 0.6: '#fd8d3c', 0.8: '#f03b20', 1.0: '#bd0026'}
).add_to(capa_calor)

capa_calor.add_to(m_panel)

# =====================================================
# Control de capas + título del panel
# =====================================================
folium.LayerControl(collapsed=False, position='topright').add_to(m_panel)

titulo_panel = '''
<div style="position:fixed; top:12px; left:55px; z-index:1000;
            background:white; padding:10px 16px; border-radius:8px;
            box-shadow:2px 2px 8px rgba(0,0,0,0.25); font-family:Arial;">
    <b style='font-size:15px; color:#2c3e50;'>Panel de Política Social — Argentina</b><br>
    <span style='font-size:11px; color:#777;'>Fuente: EPH-INDEC 2023 (datos ilustrativos)</span>
</div>
'''
m_panel.get_root().html.add_child(folium.Element(titulo_panel))

# Guardar
m_panel.save('mapa_interactivo.html')
print('Panel guardado como mapa_interactivo.html')
print('Usá el control de capas (arriba a la derecha) para activar/desactivar cada capa.')

m_panel
Panel guardado como mapa_interactivo.html
Usá el control de capas (arriba a la derecha) para activar/desactivar cada capa.
Make this Notebook Trusted to load map: File -> Trust Notebook

Resumen de las capas del panel#

Capa

Tipo

Variable

Visible por defecto

Tasa de pobreza

Coroplético

% personas pobres por provincia

Capitales provinciales

CircleMarker

Población, región, tasa de pobreza

Beneficiarios AMBA

HeatMap

Densidad de puntos

No

Nota de diseño: En dashboards para funcionarios, es recomendable comenzar con pocas capas visibles para no sobrecargar la visualización. El usuario activa las que necesita.


Ejercicios#

Ejercicio 1 — Mapa de oficinas de atención ciudadana#

Simulá y mapeá las oficinas de atención de un programa social ficticio en la Provincia de Buenos Aires.

a) Creá un DataFrame con al menos 30 oficinas. Para cada una, generá aleatoriamente:

  • Nombre del municipio (usá una lista de 10 municipios bonaerenses)

  • Coordenadas dentro de la provincia (lat entre -34 y -41, lon entre -57 y -63)

  • Horario de atención (mañana / tarde / completo)

  • Cantidad de trámites mensuales (número entre 50 y 800)

b) Mapeá las oficinas con CircleMarker:

  • Color según horario de atención

  • Radio proporcional a la cantidad de trámites

  • Popup con todos los datos

c) Identificá visualmente los municipios con mayor demanda y proponé una ubicación para una nueva oficina.

# Tu código aquí

Ejercicio 2 — Coroplético comparativo#

a) Tomá el DataFrame df_pobreza y agregá dos columnas nuevas:

  • desempleo: tasa de desempleo simulada por provincia (entre 5% y 25%)

  • informalidad: tasa de empleo informal simulada (entre 30% y 70%)

b) Creá un mapa con tres capas coropléticas (una por indicador), cada una con su propio folium.Choropleth. Las tres deben ser accesibles desde el LayerControl, pero solo una visible por defecto.

c) ¿Qué ventajas y limitaciones tiene mostrar múltiples indicadores en un mismo panel coroplético?

# Pista: cada Choropleth es una FeatureGroup separada
# Tu código aquí

Ejercicio 3 — Análisis de cobertura territorial#

Un programa de primera infancia tiene 200 centros de atención distribuidos en Argentina. Querés analizar si hay zonas con baja cobertura.

a) Generá aleatoriamente las coordenadas de los 200 centros con distribución no uniforme: el 70% dentro de la región Pampeana y GBA, el 30% restante distribuido en el resto del país. (Pista: generá los dos grupos por separado con np.random.normal con diferentes parámetros y concatenalos.)

b) Creá un mapa que combine:

  • HeatMap de los centros (muestra la densidad actual)

  • Marcadores de las capitales provinciales (para referencia geográfica)

c) Identificá visualmente qué región o provincias tienen menor cobertura relativa a su superficie. ¿Qué datos adicionales necesitarías para cuantificar el déficit de cobertura por habitante?

# Tu código aquí