Clase 3 — Matplotlib y visualización#

Python y Políticas Públicas


Contenidos#

  1. Buenas prácticas en visualización de datos

  2. Anatomía de un gráfico en matplotlib

  3. Line plots y series temporales

  4. Bar charts

  5. Histogramas

  6. Scatter plots

  7. Heatmaps y matrices de correlación

  8. Subplots: múltiples paneles

  9. Exportar gráficos

  10. Ejercicios

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

# Para que los gráficos aparezcan inline en el notebook
%matplotlib inline

# Estilo general
plt.rcParams['figure.figsize'] = (10, 5)
plt.rcParams['font.size'] = 12
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)

1. Buenas prácticas en visualización de datos#

Un buen gráfico comunica un mensaje claro. Algunas reglas prácticas:

Hacer

Evitar

Título descriptivo que cuente el hallazgo

Títulos genéricos («Gráfico 1»)

Etiquetar ambos ejes con unidades

Ejes sin etiqueta

Usar colores con propósito

Demasiados colores sin sentido

Empezar el eje Y en 0 (para barras)

Truncar el eje Y para exagerar diferencias

Fuente de los datos

Omitir la fuente

Menos es más: eliminar grilla innecesaria

Sobrecargar con elementos decorativos

¿Qué gráfico usar según el tipo de variable?

Tipo de análisis

Gráfico recomendado

Evolución en el tiempo

Line plot

Comparar categorías

Bar chart (horizontal si hay muchas)

Distribución de una variable

Histograma

Relación entre dos variables numéricas

Scatter plot

Correlaciones entre múltiples variables

Heatmap

Composición (partes de un todo)

Pie chart (con moderación)


2. Anatomía de un gráfico en matplotlib#

Matplotlib tiene dos enfoques:

  • pyplot interface (plt.plot()): rápido para gráficos simples.

  • Orientado a objetos (fig, ax = plt.subplots()): recomendado para gráficos más elaborados.

# Anatomía básica de un gráfico (enfoque recomendado)
fig, ax = plt.subplots(figsize=(8, 4))

x = [1, 2, 3, 4, 5]
y = [3, 1, 4, 1, 5]

ax.plot(x, y, marker='o', color='steelblue', linewidth=2)

ax.set_title("Título del gráfico", fontsize=14, loc='left')
ax.set_xlabel("Eje X (unidades)")
ax.set_ylabel("Eje Y (unidades)")
ax.grid(axis='y', alpha=0.3)

# Nota de fuente
fig.text(0.99, 0.01, "Fuente: elaboración propia", ha='right', fontsize=9, color='gray')

plt.tight_layout()
plt.show()
_images/5e974cf23fe9f98f5e3696085b2f9d72432fa727feb5fcfd86c23b04050f6e00.png

3. Line plots y series temporales#

# Evolución de la tasa de pobreza nacional
anios = np.arange(2016, 2024)
pobreza = np.array([30.3, 25.7, 27.3, 35.4, 40.8, 42.0, 36.5, 40.1])

fig, ax = plt.subplots(figsize=(10, 5))

ax.plot(anios, pobreza, marker='o', color='#e74c3c', linewidth=2.5, markersize=8)

# Anotar cada punto
for x, y in zip(anios, pobreza):
    ax.annotate(f"{y:.1f}%", (x, y), textcoords="offset points", xytext=(0, 10), ha='center', fontsize=10)

ax.set_title("Tasa de pobreza urbana en Argentina, 2016–2023", fontsize=14, pad=15, loc='left')
ax.set_xlabel("Año")
ax.set_ylabel("Porcentaje de personas (%)")
ax.set_ylim(20, 50)
ax.set_xticks(anios)
ax.grid(axis='y', alpha=0.3, linestyle='--')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

fig.text(0.99, 0.01, "Fuente: INDEC, EPH", ha='right', fontsize=9, color='gray')
plt.tight_layout()
plt.show()
_images/cdd0cf9a72719b8d294da337a1aad38b0786447993e5f3202c959e2200d54d6b.png

4. Bar charts#

# Gasto público por área (barras verticales)
areas = ["Educación", "Salud", "Seguridad\nSocial", "Deuda", "Defensa", "Otros"]
gasto_pct = [16.2, 12.8, 42.5, 11.3, 3.7, 13.5]
colores = ['#2ecc71' if g == max(gasto_pct) else '#3498db' for g in gasto_pct]

fig, ax = plt.subplots(figsize=(10, 5))

barras = ax.bar(areas, gasto_pct, color=colores, edgecolor='white', linewidth=0.5)

# Etiquetas encima de cada barra
for barra in barras:
    altura = barra.get_height()
    ax.text(barra.get_x() + barra.get_width() / 2., altura + 0.3,
            f'{altura:.1f}%', ha='center', va='bottom', fontsize=10)

ax.set_title("Composición del gasto público nacional, 2023", fontsize=14, loc='left')
ax.set_ylabel("% del gasto total")
ax.set_ylim(0, 55)
ax.grid(axis='y', alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

fig.text(0.99, 0.01, "Fuente: Ministerio de Economía", ha='right', fontsize=9, color='gray')
plt.tight_layout()
plt.show()
_images/3802041a7c813e0ecaa7294f31d37d64d119e6996171bc0ac723a26ffea598ab.png
# Barras horizontales: mejor cuando hay muchas categorías o nombres largos
provincias = ["CABA", "Buenos Aires", "Córdoba", "Santa Fe", "Mendoza",
              "Tucumán", "Salta", "Entre Ríos", "Chaco", "Formosa"]
tasas = [18.5, 40.1, 35.4, 37.1, 32.8, 45.2, 49.3, 38.7, 55.6, 58.2]

# Ordenar de menor a mayor
orden = np.argsort(tasas)
provincias_ord = [provincias[i] for i in orden]
tasas_ord = [tasas[i] for i in orden]

fig, ax = plt.subplots(figsize=(9, 6))
colores = ['#e74c3c' if t > 50 else '#3498db' for t in tasas_ord]

ax.barh(provincias_ord, tasas_ord, color=colores)
ax.axvline(x=np.mean(tasas), color='black', linestyle='--', alpha=0.5, label=f'Media: {np.mean(tasas):.1f}%')

ax.set_title("Tasa de pobreza por provincia, 2023", fontsize=13, loc='left')
ax.set_xlabel("% de personas")
ax.legend(frameon=False)
ax.grid(axis='x', alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.show()
_images/ee69917d623dc025b1dae3c8d31ef6a3fa42f9f95b99a2c5a228aa10dee874a6.png

5. Histogramas#

# Distribución de ingresos (simulada)
np.random.seed(42)
ingresos = np.random.lognormal(mean=10.5, sigma=0.8, size=1000)  # distribución asimétrica

fig, ax = plt.subplots(figsize=(9, 5))

ax.hist(ingresos, bins=40, color='steelblue', edgecolor='white', alpha=0.8)
ax.axvline(np.median(ingresos), color='red', linestyle='--', label=f'Mediana: ${np.median(ingresos):,.0f}')
ax.axvline(np.mean(ingresos), color='orange', linestyle='--', label=f'Media: ${np.mean(ingresos):,.0f}')

ax.set_title("Distribución del ingreso mensual individual", fontsize=13, loc='left')
ax.set_xlabel("Ingreso mensual ($)")
ax.set_ylabel("Frecuencia")
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'${x:,.0f}'))
ax.legend(frameon=False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.show()
print("Nota: la media supera a la mediana → distribución sesgada a la derecha (pocos ingresos muy altos)")
_images/3b3198bcb4becc4e939208f649860944a0e5a82eae168c452294da3034a84605.png
Nota: la media supera a la mediana → distribución sesgada a la derecha (pocos ingresos muy altos)

6. Scatter plots#

# Relación entre gasto en educación y tasa de alfabetización por provincia
np.random.seed(7)
gasto_educ = np.random.uniform(3, 12, 24)  # % del presupuesto provincial
alfabetizacion = 70 + gasto_educ * 2.5 + np.random.normal(0, 3, 24)  # correlación positiva + ruido
poblacion = np.random.uniform(0.1, 5, 24)  # millones (para tamaño de punto)

fig, ax = plt.subplots(figsize=(9, 6))

scatter = ax.scatter(gasto_educ, alfabetizacion,
                     s=poblacion * 100,  # tamaño proporcional a la población
                     alpha=0.7, color='steelblue', edgecolors='white')

# Línea de tendencia
m, b = np.polyfit(gasto_educ, alfabetizacion, 1)
x_line = np.linspace(gasto_educ.min(), gasto_educ.max(), 100)
ax.plot(x_line, m * x_line + b, color='red', linestyle='--', alpha=0.7, label=f'Tendencia')

ax.set_title("Gasto en educación y tasa de alfabetización por provincia", fontsize=13, loc='left')
ax.set_xlabel("Gasto en educación (% del presupuesto provincial)")
ax.set_ylabel("Tasa de alfabetización (%)")
ax.legend(frameon=False)
ax.grid(alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Leyenda para el tamaño
ax.text(0.02, 0.02, "Tamaño del punto = población provincial",
        transform=ax.transAxes, fontsize=9, color='gray')

plt.tight_layout()
plt.show()
_images/f548ad42a9b42c0d93e8f8b50f39cf5a8e8d27a0d20f31292e6997456ebc1b19.png

7. Heatmaps y matrices de correlación#

# Matriz de correlación entre indicadores sociales
np.random.seed(42)
n = 50
pobreza    = np.random.uniform(20, 60, n)
desempleo  = pobreza * 0.3 + np.random.normal(0, 2, n)
educacion  = 100 - pobreza * 0.8 + np.random.normal(0, 5, n)
salud      = 100 - pobreza * 0.5 + np.random.normal(0, 4, n)
hacinamiento = pobreza * 0.4 + np.random.normal(0, 3, n)

data = np.column_stack([pobreza, desempleo, educacion, salud, hacinamiento])
labels = ["Pobreza", "Desempleo", "Educación", "Salud", "Hacinamiento"]

# Calcular correlaciones
corr = np.corrcoef(data.T)

fig, ax = plt.subplots(figsize=(7, 6))

im = ax.imshow(corr, cmap='RdBu_r', vmin=-1, vmax=1)
plt.colorbar(im, ax=ax, label='Correlación de Pearson')

ax.set_xticks(range(len(labels)))
ax.set_yticks(range(len(labels)))
ax.set_xticklabels(labels, rotation=45, ha='right')
ax.set_yticklabels(labels)

# Anotar valores
for i in range(len(labels)):
    for j in range(len(labels)):
        color = 'white' if abs(corr[i, j]) > 0.6 else 'black'
        ax.text(j, i, f"{corr[i, j]:.2f}", ha='center', va='center', fontsize=9, color=color)

ax.set_title("Correlaciones entre indicadores sociales", fontsize=13, pad=15, loc='left')
plt.tight_layout()
plt.show()
_images/7aabfc4fe706069e3bbbc7c77d0bfe11932d690e2220c41fd23a9f5f083e92ba.png

8. Subplots: múltiples paneles#

# Dashboard con 4 indicadores
anios = np.arange(2018, 2024)

fig, axes = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle("Indicadores socioeconómicos, Argentina 2018–2023", fontsize=15, y=1.01)

# Panel 1: Pobreza
axes[0, 0].plot(anios, [27.3, 35.4, 40.8, 37.3, 36.5, 40.1], marker='o', color='#e74c3c')
axes[0, 0].set_title("Tasa de pobreza (%)")
axes[0, 0].set_ylim(20, 50)
axes[0, 0].grid(alpha=0.3)

# Panel 2: Desempleo
axes[0, 1].plot(anios, [9.1, 10.6, 11.5, 10.2, 7.0, 6.9], marker='s', color='#e67e22')
axes[0, 1].set_title("Tasa de desempleo (%)")
axes[0, 1].set_ylim(5, 15)
axes[0, 1].grid(alpha=0.3)

# Panel 3: Inflación
axes[1, 0].bar(anios, [47.6, 53.8, 36.1, 50.9, 94.8, 211.4], color='#9b59b6')
axes[1, 0].set_title("Inflación anual (%)")
axes[1, 0].grid(axis='y', alpha=0.3)

# Panel 4: PBI per cápita (índice base 2018=100)
axes[1, 1].plot(anios, [100, 94.2, 96.8, 99.1, 103.5, 101.2], marker='^', color='#27ae60')
axes[1, 1].axhline(y=100, color='black', linestyle='--', alpha=0.5)
axes[1, 1].set_title("PBI per cápita (índice base 2018)")
axes[1, 1].grid(alpha=0.3)

for ax in axes.flat:
    ax.set_xticks(anios)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.show()
_images/3b4fdb40d6a89162acf2b5030c807530f2fd8b3572530af493edbd827afc2347.png

9. Exportar gráficos#

# Guardar un gráfico con alta resolución
fig, ax = plt.subplots(figsize=(10, 5))
anios = np.arange(2018, 2024)
ax.plot(anios, [27.3, 35.4, 40.8, 37.3, 36.5, 40.1], marker='o', color='#e74c3c', linewidth=2)
ax.set_title("Tasa de pobreza urbana, Argentina 2018–2023", loc='left')
ax.set_ylabel("%")
ax.grid(alpha=0.3)

# Guardar en diferentes formatos
fig.savefig("pobreza_2018_2023.png", dpi=300, bbox_inches='tight')  # para documentos
fig.savefig("pobreza_2018_2023.svg", bbox_inches='tight')           # vectorial (editable)

print("Gráficos guardados: pobreza_2018_2023.png y .svg")
plt.show()
Gráficos guardados: pobreza_2018_2023.png y .svg
_images/5eefa01da039831e40ad83b7d9ec95e10e8de68206c3d9ea27a687c91c436ee2.png

10. Ejercicios#

Ejercicio 1#

Usando los datos de la siguiente tabla, creá un gráfico de barras horizontales que muestre el porcentaje de hogares con acceso a internet por región, ordenado de mayor a menor. Incluí título, etiquetas de ejes y fuente.

regiones = ["CABA", "Pampeana", "Patagonia", "Cuyo", "NOA", "NEA"]
acceso_internet = [82.3, 71.5, 68.9, 62.4, 51.8, 47.2]  # %
# Tu solución aquí

Ejercicio 2#

Creá un subplot con 2 paneles (uno al lado del otro):

  • Izquierda: histograma de la distribución de tasas de mortalidad infantil por provincia (usá np.random.normal(10, 4, 24).clip(2, 25) como datos simulados).

  • Derecha: scatter plot mostrando la relación entre esas tasas de mortalidad y un índice de pobreza simulado (np.random.uniform(20, 60, 24)).

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(1)
mortalidad = np.random.normal(10, 4, 24).clip(2, 25)
pobreza = np.random.uniform(20, 60, 24)

# Tu solución aquí