Analítica de Aprendizaje y Minería de Datos Educacionales

Práctica 2: Diseño e implementación de cuadros de mando (Dashboards).

Autores: Manuel Jesús Gómez Moratilla, José Antonio Ruipérez Valiente y Mariano Albaladejo González

Sesión 3. Diseño e implementación de dashboards con Plotly.¶

En esta sesión guiada vamos a construir, paso a paso, un dashboard interactivo con datos educacionales:
un dataset de rendimiento de estudiantes en exámenes (notas de matemáticas, lectura y escritura, junto con variables de contexto).

¿Qué es un dashboard?¶

Un dashboard (cuadro de mando) es una pantalla que reúne indicadores clave (KPIs) y visualizaciones interactivas para ayudarte a:

  • Monitorizar qué está ocurriendo en un sistema (curso, plataforma, empresa, etc.).
  • Detectar patrones y tendencias rápidamente.
  • Tomar decisiones informadas basadas en datos.
  • Comunicar resultados de forma clara a otras personas (docentes, dirección, estudiantes, etc.).

Características típicas de un buen dashboard:

  1. Claridad: se entiende qué muestra cada gráfico sin necesidad de muchas explicaciones externas.
  2. Relevancia: solo aparecen los indicadores importantes para los objetivos del usuario.
  3. Interactividad: permite filtrar, seleccionar rangos de fechas, elegir subgrupos, etc.
  4. Consistencia visual: mismo estilo de colores, tamaños y tipografías.
  5. Historia / narrativa: ayuda a responder preguntas concretas (por ejemplo:

    ¿Qué estudiantes están en riesgo?
    ¿Qué actividad del curso tiene mayor impacto en el rendimiento?).

🧾 Conjunto de datos (educacional)¶

Usaremos el dataset “Students Performance in Exams” (1000 estudiantes),
que incluye variables como:

  • gender: género del estudiante
  • race/ethnicity: grupo étnico
  • parental level of education: máximo nivel educativo de los padres
  • lunch: tipo de almuerzo (estándar vs subvencionado)
  • test preparation course: si ha realizado un curso de preparación
  • math score, reading score, writing score: notas de 0 a 100

El primer paso es preparar nuestro entorno con la importación de paquetes necesarios:

In [ ]:
import plotly
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

import pandas as pd

# Renderizador recomendado para Colab
import plotly.io as pio
pio.renderers.default = "colab"

print("Entorno listo. Versión de Plotly:", plotly.__version__)
Entorno listo. Versión de Plotly: 5.24.1

Ahora, cargamos nuestro conjunto de datos desde el archivo CSV que tenemos.

In [ ]:
df = pd.read_csv("StudentsPerformance.csv")
df
Out[ ]:
gender race/ethnicity parental level of education lunch test preparation course math score reading score writing score
0 female group B bachelor's degree standard none 72 72 74
1 female group C some college standard completed 69 90 88
2 female group B master's degree standard none 90 95 93
3 male group A associate's degree free/reduced none 47 57 44
4 male group C some college standard none 76 78 75
... ... ... ... ... ... ... ... ...
995 female group E master's degree standard completed 88 99 95
996 male group C high school free/reduced none 62 55 55
997 female group C high school free/reduced completed 59 71 65
998 female group D some college standard completed 68 78 77
999 female group D some college free/reduced none 77 86 86

1000 rows × 8 columns

1. Exploración básica del conjunto de datos.¶

Vamos a realizar una exploración básica del dataset, fijándonos en la estructura y en el tipo de variables que contiene.

In [ ]:
print("Dimensiones del dataset:", df.shape)
print("\nColumnas:\n", list(df.columns))
Dimensiones del dataset: (1000, 8)

Columnas:
 ['gender', 'race/ethnicity', 'parental level of education', 'lunch', 'test preparation course', 'math score', 'reading score', 'writing score']
In [ ]:
print("\nDescripción de las columnas numéricas:")
df.describe()
Descripción de las columnas numéricas:
Out[ ]:
math score reading score writing score
count 1000.00000 1000.000000 1000.000000
mean 66.08900 69.169000 68.054000
std 15.16308 14.600192 15.195657
min 0.00000 17.000000 10.000000
25% 57.00000 59.000000 57.750000
50% 66.00000 70.000000 69.000000
75% 77.00000 79.000000 79.000000
max 100.00000 100.000000 100.000000

Este dataset incluye:

  • Variables categóricas:

    • gender
    • race/ethnicity
    • parental level of education
    • lunch
    • test preparation course
  • Variables numéricas (nuestro foco para el dashboard):

    • math score
    • reading score
    • writing score

Algunas preguntas típicas que podemos hacernos:

  • ¿Cuál es la distribución de notas en cada asignatura?
  • ¿Hay diferencias por género o por nivel educativo de los padres?
  • ¿Influye haber hecho un curso de preparación en las notas?
  • ¿Qué relacion existe entre las notas de matemáticas, lectura y escritura?

Iremos construyendo visualizaciones para ir respondiendo a estas preguntas.

2. Primeros gráficos con Plotly Express¶

Vamos a construir algunos gráficos básicos para entender el rendimiento de los estudiantes:

  1. Distribución de notas (histogramas).
  2. Comparación de medias por grupo (barras).
  3. Relación entre asignaturas (scatter interactivo).

Usaremos plotly.express (px), que permite crear gráficos interactivos con muy poco código.

In [ ]:
# crear un DataFrame con nombres de columnas más manejables
df_edu = df.rename(columns={
    "race/ethnicity": "race_ethnicity",
    "parental level of education": "parent_education",
    "test preparation course": "test_prep",
    "math score": "math_score",
    "reading score": "reading_score",
    "writing score": "writing_score"
})

df_edu.head()
Out[ ]:
gender race_ethnicity parent_education lunch test_prep math_score reading_score writing_score
0 female group B bachelor's degree standard none 72 72 74
1 female group C some college standard completed 69 90 88
2 female group B master's degree standard none 90 95 93
3 male group A associate's degree free/reduced none 47 57 44
4 male group C some college standard none 76 78 75

Distribución de notas¶

Creamos un histograma de la columna math_score:

  • Cada barra representa un rango de notas (bins).
  • La altura de la barra indica cuántos estudiantes caen en ese rango.
  • Con nbins=20 controlamos cuántos “trozos” se usan para dividir el eje X.
  • labels nos permite mostrar un nombre más amigable en el eje.
In [ ]:
# Histograma con las notas de matemáticas
fig_math_hist = px.histogram(
    df_edu,
    x="math_score",
    nbins=20,
    title="Distribución de notas de Matemáticas",
    labels={"math_score": "Nota de Matemáticas"}
)

fig_math_hist

Exportar el dashboard o figuras a HTML¶

Podemos exportar cualquier figura de Plotly a un archivo HTML interactivo:

fig.write_html("nombre_figura.html")
In [ ]:
fig_math_hist.write_html("figura_ejemplo.html")

Distribución de notas separando por género¶

Este gráfico es una extensión del histograma anterior, pero ahora:

  • Usamos color="gender" para dibujar una distribución por cada género.
  • Con barmode="overlay" superponemos las distribuciones.
  • opacity=0.6 hace que las barras sean semitransparentes y podamos ver ambas.
In [ ]:
# Histograma comparando géneros
fig_math_gender = px.histogram(
    df_edu,
    x="math_score",
    color="gender",
    barmode="overlay",
    nbins=20,
    title="Distribución de notas de Matemáticas por género",
    labels={"math_score": "Nota de Matemáticas", "gender": "Género"},
    opacity=0.6
)

fig_math_gender

Media de notas por nivel educativo de los padres¶

Aquí damos un paso hacia el análisis agregado:

  • Agrupamos el dataframe por la columna parent_education con groupby.
  • Para cada grupo calculamos la media de:
    • math_score
    • reading_score
    • writing_score
  • Obtenemos así un nuevo dataframe df_parent_avg donde cada fila es un nivel educativo de los padres y las columnas son las notas medias correspondientes.

Este tipo de tabla es muy típico para construir visualizaciones: no miramos cada estudiante individual, sino patrones por grupo.

In [ ]:
# Calculamos medias por nivel educativo de los padres
df_parent_avg = (
    df_edu
    .groupby("parent_education", as_index=False)[["math_score", "reading_score", "writing_score"]]
    .mean()
)

df_parent_avg
Out[ ]:
parent_education math_score reading_score writing_score
0 associate's degree 67.882883 70.927928 69.896396
1 bachelor's degree 69.389831 73.000000 73.381356
2 high school 62.137755 64.704082 62.448980
3 master's degree 69.745763 75.372881 75.677966
4 some college 67.128319 69.460177 68.840708
5 some high school 63.497207 66.938547 64.888268

Este gráfico de barras muestra, para cada nivel educativo de los padres:

  • La nota media en Matemáticas, Lectura y Escritura.
  • Con y=[...] indicamos que queremos varias series a la vez.
  • Con barmode="group" las barras de cada asignatura aparecen una al lado de la otra.

Objetivo:

  • Permitir comparaciones rápidas del tipo:
    • “Cuando los padres tienen nivel X, ¿qué asignatura tiene mejor media?”
    • “¿Varía mucho el rendimiento según el nivel educativo de los padres?”
In [ ]:
fig_parent_avg = px.bar(
    df_parent_avg,
    x="parent_education",
    y=["math_score", "reading_score", "writing_score"],
    title="Notas medias por nivel educativo de los padres",
    labels={"value": "Nota media", "parent_education": "Nivel educativo de los padres"},
    barmode="group"
)

fig_parent_avg.update_layout(
    xaxis_tickangle=45,
    template="plotly_white"
)

fig_parent_avg

Relación entre asignaturas (Scatter plot)¶

Vamos a crear un scatter para ver la relación entre matemáticas y lectura, usando el color para distinguir el tipo de almuerzo (indicador socioeconómico aproximado), y el símbolo para marcar si ha realizado el curso de preparación.

Ahora usamos un gráfico de dispersión (scatter plot):

  • Cada punto es un estudiante.
  • Eje X: nota en Matemáticas (math_score).
  • Eje Y: nota en Lectura (reading_score).
  • Color: lunch (tipo de almuerzo).
  • Símbolo: test_prep (si ha hecho un curso de preparación o no).
  • hover_data añade información extra al pasar el ratón por encima (género, educación de los padres).

Este tipo de gráfico es muy útil para ver correlaciones o patrones:

  • ¿Los estudiantes que sacan buena nota en Matemáticas suelen sacar buena nota también en Lectura?
  • ¿Hay algún patrón por tipo de almuerzo o por curso de preparación?
In [ ]:
fig_math_read = px.scatter(
    df_edu,
    x="math_score",
    y="reading_score",
    color="lunch",
    symbol="test_prep",
    hover_data=["gender", "parent_education"],
    title="Relación entre Matemáticas y Lectura",
    labels={
        "math_score": "Nota de Matemáticas",
        "reading_score": "Nota de Lectura",
        "lunch": "Tipo de almuerzo",
        "test_prep": "Curso de preparación"
    }
)

fig_math_read.update_layout(template="plotly_white")
fig_math_read

3. Creación de subplots¶

Un dashboard suele mostrar varios gráficos o visualizaciones a la vez.
Vamos a combinar en una misma figura:

  • A la izquierda: media de Matemáticas por género.
  • A la derecha: media de Matemáticas por curso de preparación.

Pasos clave:

  • Agrupamos el dataframe por gender y por test_prep para obtener las medias.
  • Usamos make_subplots(rows=1, cols=2) para crear una figura con dos columnas.
  • Añadimos un go.Bar(...) a cada subplot indicando en qué fila/columna debe ir (row=1, col=1 y row=1, col=2).

Este tipo de figura es habitual en dashboards cuando queremos que el usuario:

  • Compare rápidamente dos factores distintos que afectan a la misma variable (aquí, la nota de Matemáticas).
  • Tenga una visión más compacta sin tener que cambiar de pestaña o gráfico.
In [ ]:
from plotly.subplots import make_subplots

# Medias por género
df_math_gender = df_edu.groupby("gender", as_index=False)["math_score"].mean()

# Medias por curso de preparación
df_math_prep = df_edu.groupby("test_prep", as_index=False)["math_score"].mean()

fig_sub = make_subplots(
    rows=1, cols=2,
    subplot_titles=("Media de Matemáticas por género", "Media de Matemáticas por curso de preparación")
)

fig_sub.add_trace(
    go.Bar(
        x=df_math_gender["gender"],
        y=df_math_gender["math_score"],
        name="Por género"
    ),
    row=1, col=1
)

fig_sub.add_trace(
    go.Bar(
        x=df_math_prep["test_prep"],
        y=df_math_prep["math_score"],
        name="Por curso preparación"
    ),
    row=1, col=2
)

fig_sub.update_layout(
    title_text="Comparación de rendimiento en Matemáticas",
    template="plotly_white",
    showlegend=False
)

fig_sub

3. Interactividad con dropdowns¶

Ahora vamos a construir una figura con un menú desplegable (dropdown)
para que el usuario pueda elegir qué asignatura visualizar:

  • Matemáticas
  • Lectura
  • Escritura

La idea: un histograma cuyo eje X cambia según la asignatura seleccionada.

Este gráfico introduce una idea muy importante en dashboards: usar controles (menús, botones) que cambian lo que se muestra.

¿Qué hace esta celda?

  • Crea una figura vacía fig_dd.
  • Añade un histograma por cada asignatura (math_score, reading_score, writing_score).
    • Solo el primero se marca como visible inicialmente (visible=(i == 0)).
  • Define un menú desplegable (updatemenus) con botones:
    • Cada botón activa uno de los histogramas y oculta los demás.

Ventajas:

  • El usuario puede elegir qué asignatura quiere analizar desde el mismo gráfico.
  • El dashboard es más compacto: en lugar de 3 figuras distintas, tenemos una figura con 3 vistas.
In [ ]:
subjects = ["math_score", "reading_score", "writing_score"]
subject_labels = {
    "math_score": "Matemáticas",
    "reading_score": "Lectura",
    "writing_score": "Escritura"
}

fig_dd = go.Figure()

# Creamos un histograma por cada asignatura, inicialmente visible solo el primero
for i, subj in enumerate(subjects):
    fig_dd.add_trace(
        go.Histogram(
            x=df_edu[subj],
            name=subject_labels[subj],
            visible=(i == 0),
            opacity=0.75
        )
    )

# Botones del dropdown
buttons = []
for i, subj in enumerate(subjects):
    visibility = [False] * len(subjects)
    visibility[i] = True
    buttons.append(
        dict(
            label=subject_labels[subj],
            method="update",
            args=[
                {"visible": visibility},
                {"title": f"Distribución de notas de {subject_labels[subj]}",
                 "xaxis": {"title": "Nota"},
                 "yaxis": {"title": "Frecuencia"}}
            ]
        )
    )

fig_dd.update_layout(
    updatemenus=[
        dict(
            buttons=buttons,
            direction="down",
            showactive=True,
            x=0.5,
            xanchor="center",
            y=1.15,
            yanchor="top"
        )
    ],
    title="Distribución de notas de Matemáticas",
    xaxis_title="Nota",
    yaxis_title="Frecuencia",
    template="plotly_white"
)

fig_dd

4. Diseño y visualización de KPIs orientados a educación¶

En un dashboard para docentes o gestores es útil mostrar indicadores globales como:

  • Nota media de Matemáticas.
  • Nota media de Lectura.
  • Nota media de Escritura.
  • Diferencias de medias entre grupos (por ejemplo, con y sin curso de preparación).

Vamos a definir una función que calcule estos KPIs a partir del DataFrame.

In [ ]:
def compute_kpis_edu(df):
    math_mean = df["math_score"].mean()
    read_mean = df["reading_score"].mean()
    write_mean = df["writing_score"].mean()

    # Diferencia media de Matemáticas entre estudiantes con y sin curso de preparación
    df_prep = df.groupby("test_prep")["math_score"].mean()
    diff_prep = df_prep.max() - df_prep.min()

    return {
        "math_mean": math_mean,
        "read_mean": read_mean,
        "write_mean": write_mean,
        "prep_diff_math": diff_prep,
        "prep_means": df_prep.to_dict()
    }

kpis_edu = compute_kpis_edu(df_edu)
kpis_edu
Out[ ]:
{'math_mean': np.float64(66.089),
 'read_mean': np.float64(69.169),
 'write_mean': np.float64(68.054),
 'prep_diff_math': 5.617649106319291,
 'prep_means': {'completed': 69.69553072625699, 'none': 64.0778816199377}}

Mostrar KPIs en formato legible¶

Representaremos los KPIs como texto formateado (tipo “tarjeta”).
En una implementación más avanzada (Dash/Streamlit) serían cajas con colores, iconos, etc.

Un dashboard no son solo gráficos:
a veces, un buen bloque de texto bien formateado comunica mejor los resultados clave.

En esta celda:

  • Usamos Markdown y display para mostrar los KPIs en un bloque de texto con formato.
  • Insertamos los valores del diccionario kpis dentro de una plantilla de texto usando f"""...""".
  • Mostramos:
    • Las medias de Matemáticas, Lectura y Escritura.
    • Las medias de Matemáticas por grupo de test_prep.
    • La diferencia entre el grupo con mejor y peor media.

Esto se parece al panel de indicadores que suele aparecer en la parte superior de un dashboard.

In [ ]:
from IPython.display import Markdown, display

def show_kpis_edu(kpis):
    text = f"""
### 🎓 Indicadores globales de rendimiento

- **Nota media en Matemáticas:** {kpis['math_mean']:.2f}
- **Nota media en Lectura:** {kpis['read_mean']:.2f}
- **Nota media en Escritura:** {kpis['write_mean']:.2f}

**Efecto del curso de preparación (en Matemáticas):**
- Media por grupo: `{kpis['prep_means']}`
- Diferencia entre el grupo con mejor y peor media: **{kpis['prep_diff_math']:.2f} puntos**
"""
    display(Markdown(text))

show_kpis_edu(kpis_edu)

🎓 Indicadores globales de rendimiento¶

  • Nota media en Matemáticas: 66.09
  • Nota media en Lectura: 69.17
  • Nota media en Escritura: 68.05

Efecto del curso de preparación (en Matemáticas):

  • Media por grupo: {'completed': 69.69553072625699, 'none': 64.0778816199377}
  • Diferencia entre el grupo con mejor y peor media: 5.62 puntos

5. Construcción del mini-dashboard completo¶

Vamos a juntar todo en una especie de dashboard vertical dentro del notebook:

  1. Título y contexto.
  2. KPIs educativos.
  3. Gráfico 1: medias por nivel educativo de los padres.
  4. Gráfico 2: relación Matemáticas–Lectura (con contexto socioeconómico).
  5. Gráfico 3: histograma con dropdown de asignaturas.

La idea es que un docente pueda usarlo para:

  • Detectar patrones generales de rendimiento.
  • Explorar posibles brechas entre grupos.
  • Generar hipótesis sobre el impacto de factores como el nivel educativo de los padres o la preparación previa.
In [ ]:
display(Markdown("# 📊 Dashboard de rendimiento estudiantil"))

# 1) KPIs
show_kpis_edu(kpis_edu)

# 2) Medias por nivel educativo de los padres
fig_parent_avg.show()

# 3) Relación Matemáticas vs Lectura con contexto
fig_math_read.show()

# 4) Histograma interactivo por asignatura
fig_dd.show()

📊 Dashboard de rendimiento estudiantil¶

🎓 Indicadores globales de rendimiento¶

  • Nota media en Matemáticas: 66.09
  • Nota media en Lectura: 69.17
  • Nota media en Escritura: 68.05

Efecto del curso de preparación (en Matemáticas):

  • Media por grupo: {'completed': 69.69553072625699, 'none': 64.0778816199377}
  • Diferencia entre el grupo con mejor y peor media: 5.62 puntos

Sesión 4. Introducción a Dash¶

En este cuaderno vamos a dar el salto desde las visualizaciones con Plotly en un notebook (como en la sesión anterior) a la creación de un dashboard interactivo con Dash.

La idea es que puedas ver, paso a paso, siguiendo el código en python del fichero app_dash_edu.py:

  • Cómo se pasa de un gráfico suelto en Plotly a un gráfico dentro de un dashboard.
  • Qué son los componentes de Dash (dcc.Dropdown, dcc.RadioItems, dcc.Graph, etc.).
  • Cómo se conectan los controles (inputs) con los gráficos (outputs) usando callbacks.

1. Importar librerías y cargar datos¶

Primero importamos las librerías necesarias:

  • pandas: para trabajar con el dataset educacional.
  • plotly.express (px): para crear las figuras, igual que en el cuaderno anterior.
  • dash: para construir la app que contiene el dashboard.
  • Componentes básicos de Dash: dcc (componentes interactivos) y html (contenedores y etiquetas HTML).
  • Input y Output: se usan para definir los callbacks.

Después cargaremos el mismo dataset de rendimiento de estudiantes que usamos en el cuaderno anterior y renombraremos algunas columnas para que sea más cómodo trabajar.

In [ ]:
from dash import Dash, dcc, html, Input, Output
import plotly.express as px
import pandas as pd
# ---------------------------------------------------------
# 1. Carga y preparación de datos
# ---------------------------------------------------------

# Asumimos el mismo dataset que en el notebook
# Si lo tienes en otro sitio, ajusta la ruta:
# - local: "StudentsPerformance.csv"
# - o una URL si lo descargas de internet
df = pd.read_csv("datasets/StudentsPerformance.csv")

# Renombramos columnas para que el código sea más legible
df_edu = df.rename(columns={
    "race/ethnicity": "race_ethnicity",
    "parental level of education": "parent_education",
    "test preparation course": "test_prep",
    "math score": "math_score",
    "reading score": "reading_score",
    "writing score": "writing_score"
})

De Plotly a Dash: ¿qué cambia?¶

En el cuaderno anterior:

  • Creábamos gráficos como px.histogram(...), px.scatter(...), px.bar(...).
  • Cada figura se mostraba de forma independiente en el notebook.

Con Dash:

  • Seguimos creando las figuras con Plotly (igual que antes).
  • Pero además definimos una aplicación web con:
    • Una estructura de página (app.layout) hecha de componentes Dash.
    • Controles interactivos (dropdowns, botones, etc.).
    • Callbacks que dicen: “si el usuario cambia X, actualiza Y de esta forma”.

Es decir: Dash envuelve nuestros gráficos de Plotly para convertirlos en un dashboard completo.

En este cuaderno vamos a explicar cómo construir un dashboard parecido al que ya has visto como script, pero explicando cada pieza para que puedas entender qué hace.

2. Estructura básica de una app Dash (con JupyterDash)¶

La estructura mínima de una app Dash en un notebook es:

from jupyter_dash import JupyterDash
from dash import html, dcc

app = JupyterDash(__name__)

app.layout = html.Div([
    html.H1("Título"),
    dcc.Graph(figure=figura_plotly)
])

app.run()
  • app = JupyterDash(__name__): creamos la aplicación (en Colab usamos JupyterDash para que se vea dentro del notebook).
  • app.layout = ...: definimos qué componentes aparecen en la página.
  • app.run_server(): arrancamos el servidor.

3. Componentes de selección: Dropdown (caja desplegable)¶

En nuestro dashboard queremos que el usuario pueda elegir qué asignatura explorar (Matemáticas, Lectura, Escritura).
Para eso usaremos dcc.Dropdown.

Primero definimos un diccionario para mapear nombres de columna a etiquetas legibles:

In [ ]:
SUBJECT_OPTIONS = {
    "math_score": "Mathematics",
    "reading_score": "Reading",
    "writing_score": "Writing",
}

Ahora, un ejemplo de dcc.Dropdown:

```python dcc.Dropdown( id="subject-dropdown", options=[ {"label": label, "value": col} for col, label in SUBJECT_OPTIONS.items() ], value="math_score", clearable=False )

  • id="subject-dropdown": identificador único del componente. Lo usaremos en los callbacks.
  • options=[...]: lista de opciones; cada opción tiene una etiqueta visible (label) y un valor interno (value).
  • value="math_score": valor por defecto cuando se carga el dashboard.
  • clearable=False: evita que el usuario deje el selector en blanco.

En el layout final colocaremos este Dropdown dentro de un html.Div.

4. Componentes de selección: RadioItems (botones tipo "radio")¶

Para filtrar por género y por curso de preparación usaremos dcc.RadioItems.
Son parecidos a un conjunto de botones donde solo puedes seleccionar una opción.

Ejemplo para el filtro de género:

In [ ]:
gender_selector = dcc.RadioItems(
    id="gender-radio",
    options=[
        {"label": "All", "value": "all"},
        {"label": "Female", "value": "female"},
        {"label": "Male", "value": "male"},
    ],
    value="all",
    inline=True,
)

Y algo similar para el curso de preparación (test_prep):

dcc.RadioItems(
    id="prep-radio",
    options=[
        {"label": "All", "value": "all"},
        {"label": "Completed", "value": "completed"},
        {"label": "None", "value": "none"},
    ],
    value="all",
    inline=True,
)
  • Cada RadioItems tiene un id que luego usaremos en el callback.
  • El value es la opción seleccionada actualmente.
  • Con inline=True hacemos que las opciones se vean en la misma línea. En el dashboard final, estos componentes estarán en la parte superior como filtros.

5. Tarjetas de KPIs: html.Div con estilo¶

Queremos mostrar algunos indicadores clave (KPIs), como:

  • Número de estudiantes filtrados.
  • Nota media en la asignatura seleccionada.
  • Diferencia entre el grupo con mejor y peor resultado en el curso de preparación.

Cada KPI será un html.Div con:

  • Un título (html.H4).
  • Un valor (html.H2).
  • Un texto explicativo (html.P).

Ejemplo de plantilla para una tarjeta:

kpi_example = html.Div(
    [
        html.H4("Nombre del KPI"),
        html.H2("123"),
        html.P("Descripción breve del indicador."),
    ],
    style={
        "border": "1px solid #ddd",
        "borderRadius": "8px",
        "padding": "15px",
        "boxShadow": "0 1px 3px rgba(0,0,0,0.08)",
        "minWidth": "220px",
    },
)

En el dashboard final tendremos tres tarjetas, una para cada KPI principal, que iremos rellenando dentro del callback.

6. Componentes de gráficos: dcc.Graph¶

Los gráficos que ya conoces de Plotly (histogramas, barras, dispersión...) se integran en Dash usando dcc.Graph.

En el cuaderno anterior teníamos, por ejemplo:

  • Un histograma de notas de Matemáticas: px.histogram(df_edu, x="math_score", ...).
  • Un gráfico de barras de notas medias por nivel educativo de los padres.
  • Un diagrama de dispersión Matemáticas vs Lectura.

En Dash, la idea es:

  1. Crear la figura con Plotly (igual que antes), por ejemplo:
fig_hist = px.histogram(df_edu, x="math_score", nbins=20)
  1. Pasar esa figura a un dcc.Graph
dcc.Graph(id="graph-hist", figure=fig_hist)

En nuestro dashboard, sin embargo, la figura no será fija, sino que se construirá dinámicamente dentro del callback según los filtros.

Por eso en el layout solo definimos el contenedor dcc.Graph(id="graph-hist"), y la figura (atributo figure) se actualizará desde el callback.

7. Montar el layout completo del dashboard¶

Ahora sí, creamos la aplicación Dash y definimos el layout completo, combinando:

  • Título y descripción.
  • Filtros (Dropdown + RadioItems).
  • Tarjetas de KPIs (tres html.Div vacíos que rellenaremos con el callback).
  • Tres gráficos (dcc.Graph) vacíos que se actualizarán desde el callback:

    • Histograma de la asignatura seleccionada.
    • Barras: media por nivel educativo de los padres.
    • Dispersión Matemáticas vs Lectura.

Relación con el cuaderno anterior:
Los gráficos que vemos aquí son “versiones Dash” de los que ya creamos con Plotly en la clase anterior:

  • El histograma corresponde al histograma de notas.
  • El gráfico de barras se basa en las medias por parent_education.
  • La dispersión es el mismo tipo de gráfico Matemáticas vs Lectura, pero ahora con filtros.

8. Callbacks: conectar filtros con gráficos¶

Ahora necesitamos decirle a Dash qué debe pasar cuando el usuario cambia un filtro.

Un callback de Dash:

  • Tiene una serie de Input(...): valores que escuchamos (por ejemplo, el valor seleccionado en el dropdown).
  • Tiene una serie de Output(...): propiedades que vamos a actualizar (por ejemplo, la figura de un gráfico o el contenido de una tarjeta).
  • Definimos una función de Python que recibe los inputs y devuelve los outputs.

Los cambios con respecto a la sesión anterior son:

  • Antes hacíamos algo como px.histogram(df_edu, x="math_score", ...).
  • Ahora hacemos lo mismo pero con un dataframe filtrado (df_filtered) y con la asignatura seleccionada (subject_col).
  • Es decir, el callback es el “pegamento” que convierte los parámetros seleccionados en el dashboard en un nuevo gráfico de Plotly.
In [ ]:
@app.callback(
    [
        Output("kpi-num-students", "children"),
        Output("kpi-mean-subject", "children"),
        Output("kpi-diff-prep", "children"),
        Output("graph-hist", "figure"),
        Output("graph-parent", "figure"),
        Output("graph-scatter", "figure"),
    ],
    [
        Input("subject-dropdown", "value"),
        Input("gender-radio", "value"),
        Input("prep-radio", "value"),
    ],
)

def update_dashboard(subject_col, gender_filter, prep_filter):
    # 1. Filtrar datos según los controles
    df_filtered = df_edu.copy()

    if gender_filter != "all":
        df_filtered = df_filtered[df_filtered["gender"] == gender_filter]

    if prep_filter != "all":
        df_filtered = df_filtered[df_filtered["test_prep"] == prep_filter]

    # 2. Calcular KPIs con la función auxiliar
    num_students, subject_mean, diff_prep = compute_kpis(df_filtered, subject_col)
    subject_label = SUBJECT_OPTIONS.get(subject_col, subject_col)

    # 3. Contenido de las tarjetas KPI
    kpi_num = html.Div(
        [
            html.H4("Number of students"),
            html.H2(f"{num_students}"),
            html.P("After applying current filters."),
        ]
    )

    kpi_mean = html.Div(
        [
            html.H4(f"Average score in {subject_label}"),
            html.H2(f"{subject_mean:.2f}"),
            html.P("Mean value for the filtered group."),
        ]
    )

    kpi_prep = html.Div(
        [
            html.H4(f"Gap by test preparation course ({subject_label})"),
            html.H2(f"{diff_prep:.2f}"),
            html.P(
                "Difference between the best and worst group "
                "according to the test preparation course."
            ),
        ]
    )

    # 4. Gráfico 1: histograma (versión Dash del histograma del cuaderno anterior)
    if len(df_filtered) > 0:
        fig_hist = px.histogram(
            df_filtered,
            x=subject_col,
            nbins=20,
            title=f"Distribution of {subject_label} scores",
            labels={subject_col: subject_label, "count": "Number of students"},
        )
        fig_hist.update_layout(template="plotly_white")
    else:
        fig_hist = px.histogram(title="No data for current filters")
        fig_hist.update_layout(template="plotly_white")

    # 5. Gráfico 2: barras de medias por nivel educativo de los padres
    if len(df_filtered) > 0:
        df_parent = (
            df_filtered.groupby("parent_education", as_index=False)[subject_col].mean()
        )

        fig_parent = px.bar(
            df_parent,
            x="parent_education",
            y=subject_col,
            labels={
                "parent_education": "Parents' education level",
                subject_col: f"Average {subject_label} score",
            },
        )
        fig_parent.update_layout(
            template="plotly_white",
            xaxis_tickangle=45,
            margin={"b": 100},
        )
    else:
        fig_parent = px.bar(title="No data for current filters")
        fig_parent.update_layout(template="plotly_white")

    # 6. Gráfico 3: dispersión Matemáticas vs Lectura (versión Dash del scatter anterior)
    if len(df_filtered) > 0:
        fig_scatter = px.scatter(
            df_filtered,
            x="math_score",
            y="reading_score",
            color="lunch",
            symbol="test_prep",
            hover_data=["gender", "parent_education"],
            labels={
                "math_score": "Math score",
                "reading_score": "Reading score",
                "lunch": "Lunch type",
                "test_prep": "Test preparation",
            },
        )
        fig_scatter.update_layout(template="plotly_white")
    else:
        fig_scatter = px.scatter(title="No data for current filters")
        fig_scatter.update_layout(template="plotly_white")

    return kpi_num, kpi_mean, kpi_prep, fig_hist, fig_parent, fig_scatter

El decorador @app.callback(...) registra la función update_dashboard dentro de la app Dash:

Le dice a Dash: “Oye, cuando cambie el valor de subject-dropdown.value, gender-radio.value o prep-radio.value, llama a esta función y usa lo que devuelva para actualizar estos Output”.

Los Inputs:

  • Input("subject-dropdown", "value")
  • Input("gender-radio", "value")
  • Input("prep-radio", "value")

se corresponden con componentes definidos en el layout:

  • dcc.Dropdown(id="subject-dropdown", ...)
  • dcc.RadioItems(id="gender-radio", ...)
  • dcc.RadioItems(id="prep-radio", ...)

Los Outputs:

  • Output("kpi-num-students", "children")
  • Output("graph-hist", "figure"), etc.

se corresponden con otros componentes del layout:

  • html.Div(id="kpi-num-students", ...)
  • dcc.Graph(id="graph-hist"), etc.

Ejercicio para entregar

Añadir un nuevo filtro

  • Crea un filtro adicional (por ejemplo, por tipo de almuerzo lunch) usando otro RadioItems o Dropdown.
  • Aplícalo en el callback filtrando el dataframe antes de generar las figuras.

Añadir un nuevo gráfico

  • Piensa y crea un nuevo gráfico en base a los datos disponibles.
  • Añade un nuevo dcc.Graph en el layout y devuelve su figura desde el callback para mostrarlo en el dashboard.

Para el dashboard final, indica:

  • ¿Qué objetivos cumple este dashboard?
  • ¿Hacía qué usuarios finales puede ir dirigido?
  • ¿Qué frecuencia de actualización tendría este dashboard? ¿Lo definirías como push o pull?
In [ ]: