Capítulo 13 Matplotlib orientado a objetos utilizado profesionalmente.

A partir de este punto se utilizará matplotlib sobre un ejemplo de datos real, utilizando buenas prácticas de visualización, lo cual no suele suceder en los manuales y tutoriales al respecto.

La base de datos fue bajada del portal FTP del Instituto Colombiano de Evaluación de la Educación (ICFES) y corresponde a las pruebas de desempeño del año 2019 desarrolladas por los estudiantes que acaban su educación media.

Además de leerla, se ajustan algunas de las variables que se van a utilizar.

import pandas as pd
df = pd.read_csv("C:/Users/HP/Documents/Docencia/Externado/Cursos/Fundamentos programacion/Datos/saber11_2019.csv", sep=";")
df.columns = df.columns.str.lower()
df['estu_inse_individual'] = pd.to_numeric(df['estu_inse_individual'], errors='coerce')
factores = ['estu_genero', 'fami_estratovivienda', 'cole_naturaleza',
            'cole_bilingue', 'desemp_ingles']
for c in factores:
    df[c] = df[c].astype('category')
df['cole_naturaleza'] = df['cole_naturaleza'].cat.rename_categories(['No oficial', 'Oficial'])
df['cole_bilingue'] = df['cole_bilingue'].cat.rename_categories(['No', 'Sí'])
df['fami_estratovivienda'] = df['fami_estratovivienda'].cat.rename_categories(
    ['1', '2', '3', '4', '5', '6', 'Sin estrato']
    )

Si bien es interesante ver la totalidad de la base, se escogerá aleatoriamente un \(10\)% de los datos, para evitar que se demore la renderización de las visualizaciones. Además, cuando el número de puntos es grande, se deben utilizar algunas estrategias que aún no se presentan.

# 10% de filas, muestreo sin reemplazo y reproducible
saber = df.sample(frac=0.10, random_state=20250813)
del df
import numpy as np
from statsmodels.nonparametric.smoothers_lowess import lowess
from matplotlib import pyplot as plt
from matplotlib.ticker import PercentFormatter
import matplotlib.colors as ncolores # nombres de colores
from adjustText import adjust_text
plt.rcParams['figure.facecolor'] = 'white'
plt.rcParams['axes.facecolor'] = 'white'
fig = plt.figure()
ax = fig.add_axes([0, 0, 0.7, 0.7])
_ = ax.scatter(saber.estu_inse_individual, saber.punt_global, 
    s = 0.5, alpha = 0.5, 
    c = "salmon", marker = "s")
_ = ax.set_xlabel('Índice socioeconómico')
_ = ax.set_ylabel('Puntaje global')
_ = ax.set_title("Desempeño en pruebas SABER11")

Para modificar color y forma en función de la variable cole_naturaleza se realizan algunos ajustes:

fig, ax = plt.subplots()
mapa = {'No oficial': ('#FF9C00', '^'),
    'Oficial': ('#4682B4', 'o')}
for cat, (color, marker) in mapa.items():
    m = saber['cole_naturaleza'] == cat
    ax.scatter(
        saber.loc[m, 'estu_inse_individual'],
        saber.loc[m, 'punt_global'],
        color=color, marker=marker,
        s=12, alpha=0.5, linewidths=0,
        label= cat)
ax.set_xlabel('Índice socioeconómico')
ax.set_ylabel('Puntaje global')
ax.set_title("Desempeño en pruebas SABER11")
ax.legend(title='Tipo IE', loc ='lower right',
          bbox_to_anchor = (1.35, 0.4), frameon=True)

fig, ax = plt.subplots()
paleta = {'No oficial': '#4682B4', 'Oficial': '#FF9C00'}
for cat, color in paleta.items():
    m = saber['cole_naturaleza'] == cat
    ax.scatter(
        saber.loc[m, 'estu_inse_individual'],
        saber.loc[m, 'punt_global'],
        s=9, alpha=0.6, linewidths=0, color=color,
        label=cat
    )
ax.set_xlabel('Índice socioeconómico')
ax.set_ylabel('Puntaje global')
ax.set_title("Desempeño en pruebas SABER11")

Para añadir una leyenda otros ajustes:

fig, ax = plt.subplots(figsize=(6,4), layout='constrained')
paleta = {'No oficial': '#4682B4', 'Oficial': '#FF9C00'}
for cat, color in paleta.items():
    m = saber['cole_naturaleza'] == cat
    ax.scatter(
        saber.loc[m, 'estu_inse_individual'],
        saber.loc[m, 'punt_global'],
        s=9, alpha=0.6, linewidths=0, color=color,
        label=cat)
ax.set_xlabel('Índice socioeconómico')
ax.set_ylabel('Puntaje global')
ax.set_title("Desempeño en pruebas SABER11")
ax.legend(title='Tipo IE', loc ='lower right',
          bbox_to_anchor = (1.35, 0.4), frameon=True)

# Para añadir una línea de tendencia
import matplotlib.pyplot as plt
from statsmodels.nonparametric.smoothers_lowess import lowess
fig, ax = plt.subplots(figsize=(6, 4), layout='constrained')
categorias     = ['No oficial', 'Oficial']
paleta         = {'No oficial': '#FF9C00', 'Oficial': '#4682B4'}
paleta_linea   = {'No oficial': '#B46F20', 'Oficial': '#12549B'}
marcador       = {'No oficial': '^',       'Oficial': 'o'}
for cat in categorias:
    m = saber['cole_naturaleza'] == cat
    if not m.any():
        continue
    xy = saber.loc[m, ['estu_inse_individual', 'punt_global']].dropna()
    if xy.empty:
        continue
    x = xy['estu_inse_individual'].to_numpy()
    y = xy['punt_global'].to_numpy()
    ax.scatter(x, y,
               color=paleta[cat],
               marker=marcador[cat],
               s=12, alpha=0.5, linewidths=0,
               label=cat)
    lo = lowess(y, x, frac=0.8, it=1, return_sorted=True)
    ax.plot(lo[:, 0], lo[:, 1],
            color=paleta_linea[cat],
            linewidth=2, zorder=3)
ax.set_xlabel('Índice socioeconómico')
ax.set_ylabel('Puntaje global')
ax.set_title('Desempeño en pruebas SABER11')
ax.grid(True, which='both', alpha=0.25)
ax.legend(title='Tipo IE', loc='lower right', fontsize=7, title_fontsize=8)
fig.text(0.85, 0, 'Fuente: ICFES',
         ha='left', va='center', fontsize=9, style='italic', color='#777777')

Se puede controlar que ejes del axes presentar mediante el argumento spines:

fig, ax = plt.subplots(figsize=(6, 4), layout='constrained')
categorias     = ['No oficial', 'Oficial']
paleta         = {'No oficial': '#FF9C00', 'Oficial': '#4682B4'}
paleta_linea   = {'No oficial': '#B46F20', 'Oficial': '#12549B'}
# marcador       = {'No oficial': '^',       'Oficial': 'o'}
for cat in categorias:
    m = saber['cole_naturaleza'] == cat
    xy = saber.loc[m, ['estu_inse_individual', 'punt_global']].dropna()
    x = xy['estu_inse_individual'].to_numpy()
    y = xy['punt_global'].to_numpy()
    ax.scatter(x, y,
               color=paleta[cat],
               marker=marcador[cat],
               s=12, alpha=0.5, linewidths=0,
               label=cat)
    lo = lowess(y, x, frac=0.8, it=1, return_sorted=True)
    ax.plot(lo[:, 0], lo[:, 1],
            color=paleta_linea[cat],
            linewidth=2, zorder=3)
ax.set_xlabel('Índice socioeconómico')
ax.set_ylabel('Puntaje global')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(False)
ax.legend(title='Tipo IE', loc ='lower right', 
    fontsize = 7, title_fontsize = 8)
ax.set_title("Desempeño en pruebas SABER11 - 2019")
fig.text(0.85, 0, 'Fuente: ICFES', 
    ha='left', va='center', fontsize=9, style='italic', 
    color='#777777')