Visualización Interactiva

En esta clase buscamos agregar valor a las visualizaciones con interactividad.

Jupyter Widgets

Ya sabemos que no hay mejor manera de describir algo que la forma con que los autores lo hacen, en el respositorio del proyecto nos cuentan lo siguiente:

  • ipywidgets are interactive HTML widgets for Jupyter notebooks and the IPython kernel.

  • Notebooks come alive when interactive widgets are used. Users gain control of their data and can visualize changes in the data.

  • Learning becomes an immersive, fun experience. Researchers can easily see how changing inputs to a model impact the results. We hope you will add ipywidgets to your notebooks, and we’re here to help you get started.

Toda la documentación la puedes encontrar en el siguiente link. En resumen, los widgets son mini-herramientas que brindan interactividad. En esta clase los utilizaremos para visualizaciones, con tal de entregar mayour control y facilitar la exploración.

Para motivar el uso de estos, se hará uso del Atractor de Lorenz.

\[\begin{split} \begin{aligned} \dot{x} & = \sigma(y-x) \\ \dot{y} & = \rho x - y - xz \\ \dot{z} & = -\beta z + xy \end{aligned} \end{split}\]
from ipywidgets import interactive, fixed
import ipywidgets as widgets
from lorenz import solve_lorenz

%matplotlib inline
w=interactive(solve_lorenz,sigma=(0.0,50.0),rho=(0.0,50.0))
w

Para el conjunto de parámetros por defecto observamos trayectorias girando alrededor de dos puntos, llamados atractores.

El objeto devuelto por interactive es de tipo Widget y posee atributos que contienen el resultado actual y los argumentos.

t, x_t = w.result
w.kwargs  ## Cambia el valor de algún widget de w y vuelve a ejecutar esta celda, verás que el valor cambió.
{'sigma': 10.0, 'beta': 2.6666666666666665, 'rho': 28.0}

Instalación

En la documentación oficial se encuentra detallada la forma de instalar ipywidgets.

Para efectos del curso, se instalará en el mismo ambiente virtual que se ha utilizado a lo largo del semestre. Las instrucciones son:

  1. En la terminal correspondiente activa el entorno virtual del curso, es decir, ejecuta conda activate mat281.

  2. Ejecutar en la terminal conda install -c conda-forge ipywidgets, recuerda que mat281 es el nombre del ambiente virtual.

  3. Si has seguido todas las instrucciones ya deberías tener instalado nodejs, puedes verificarlo ejecutando conda list nodejs, ahí debería aparecer la versión instalada.

  4. En la misma terminal (con el ambiente activado, no lo olvides!) ejecuta jupyter labextension install @jupyter-widgets/jupyterlab-manager

from ipywidgets import interact, interactive, fixed, interact_manual

Interact

La función interact crea automáticamente una interfaz de usuario (UI) de control para explorar código y datos interactivatemente.

def f(x):
    return x

interact genera automáticamente la interfaz dde control y luego llama a la función utilizando esos parámetros como argumentos para la función.

interact(f, x=10);

Si el argumento es booleano crea una checkbox en lugar de un slicer.

interact(f, x=True);

Si se le entrega un string interact genera un text box.

interact(f, x='Hi there!');

Otra manera es utilizar interact como un decorator. Esto permite definir una función e interactuar con ella en un solo paso.

Los decoradores se escapan de los contenidos del curso, pero básicamente son funciones tienen como argumento una función y extienden el comportamiento de esta última sin modificarla explícitamente.

@interact(x=True, y=1.0)
def g(x, y):
    return (x, y)

Interactive

Además de ìnteract, IPython proporciona otra función, interactive, que es útil cuando desea reutilizar los widgets que se producen o acceder a los datos vinculados a los controles de la interfaz de usuario.

from IPython.display import display
def f(a, b):
    output = a + b
    display(output)
    return output
w = interactive(f, a=10, b=20)
type(w)
ipywidgets.widgets.interaction.interactive
w.children  # The children of the interactive are two integer-valued sliders and an output widget
(IntSlider(value=10, description='a', max=30, min=-10),
 IntSlider(value=20, description='b', max=60, min=-20),
 Output())

Para ver el widget, basta con utilizar la función display de IPython.

display(w)

Finalmente, un ejemplo con un gráfico:

import matplotlib.pyplot as plt
import numpy as np

def f(m, b):
    plt.figure(2)
    x = np.linspace(-10, 10, num=1000)
    plt.plot(x, m * x + b)
    plt.ylim(-5, 5)
    plt.show()

interactive_plot = interactive(f, m=(-2.0, 2.0), b=(-3, 3, 0.5))
interactive_plot

Si quisieras por ejemplo, que la pendiente m solo sean números enteros y que el coeficiente de posición b solo pueda ser escogido entre 0 y 3 puedes explicitar el tipo de widget para cada argumento.

Más detalle en la documentación! Link

interactive(
    f,
    m=widgets.IntSlider(0, -2, 2),
    b=widgets.Dropdown(options=[0, 3])
)

Altair Interactivo

import altair as alt
from vega_datasets import data

Para que un gráfico en altair sea interactivo basta con agregar al final del chart el método .interactive(). Para agregar mayor interactividad el encoding tooltip es de mucha ayuda.

source = data.cars()

alt.Chart(source).mark_circle(size=60).encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin',
    tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon']
).interactive()

También es posible crear selecciones entrelazadas.

source = data.cars()

brush = alt.selection(type='interval', resolve='global')

base = alt.Chart(source).mark_point().encode(
    y='Miles_per_Gallon',
    color=alt.condition(brush, 'Origin', alt.ColorValue('gray')),
).add_selection(
    brush
).properties(
    width=250,
    height=250
)

base.encode(x='Horsepower') | base.encode(x='Acceleration')

O que la selección repercuta en otro tipo de gráfico.

source = data.cars()

brush = alt.selection(type='interval')

points = alt.Chart(source).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color=alt.condition(brush, 'Origin:N', alt.value('lightgray'))
).add_selection(
    brush
)

bars = alt.Chart(source).mark_bar().encode(
    y='Origin:N',
    color='Origin:N',
    x='count(Origin):Q'
).transform_filter(
    brush
)

points & bars

Podríamos estar horas hablando de esto. Como siempre, más ejemplos en la galería de ejemplos, en particular la Sección de Gráficos Interactivos.

Interactive Scatter Plot and Linked Layered Histogram

import pandas as pd

# generate fake data
source = pd.DataFrame({'gender': ['M']*1000 + ['F']*1000,
               'height':np.concatenate((np.random.normal(69, 7, 1000),
                                       np.random.normal(64, 6, 1000))),
               'weight': np.concatenate((np.random.normal(195.8, 144, 1000),
                                        np.random.normal(167, 100, 1000))),
               'age': np.concatenate((np.random.normal(45, 8, 1000),
                                        np.random.normal(51, 6, 1000)))
        })

selector = alt.selection_single(empty='all', fields=['gender'])

color_scale = alt.Scale(domain=['M', 'F'],
                        range=['#1FC3AA', '#8624F5'])

base = alt.Chart(source).properties(
    width=250,
    height=250
).add_selection(selector)

points = base.mark_point(filled=True, size=200).encode(
    x=alt.X('mean(height):Q',
            scale=alt.Scale(domain=[0,84])),
    y=alt.Y('mean(weight):Q',
            scale=alt.Scale(domain=[0,250])),
    color=alt.condition(selector,
                        'gender:N',
                        alt.value('lightgray'),
                        scale=color_scale),
)

hists = base.mark_bar(opacity=0.5, thickness=100).encode(
    x=alt.X('age',
            bin=alt.Bin(step=5), # step keeps bin size the same
            scale=alt.Scale(domain=[0,100])),
    y=alt.Y('count()',
            stack=None,
            scale=alt.Scale(domain=[0,350])),
    color=alt.Color('gender:N',
                    scale=color_scale)
).transform_filter(
    selector
)


points | hists

Multi-Line Tooltip

np.random.seed(42)
source = pd.DataFrame(np.cumsum(np.random.randn(100, 3), 0).round(2),
                    columns=['A', 'B', 'C'], index=pd.RangeIndex(100, name='x'))
source = source.reset_index().melt('x', var_name='category', value_name='y')

# Create a selection that chooses the nearest point & selects based on x-value
nearest = alt.selection(type='single', nearest=True, on='mouseover',
                        fields=['x'], empty='none')

# The basic line
line = alt.Chart(source).mark_line(interpolate='basis').encode(
    x='x:Q',
    y='y:Q',
    color='category:N'
)

# Transparent selectors across the chart. This is what tells us
# the x-value of the cursor
selectors = alt.Chart(source).mark_point().encode(
    x='x:Q',
    opacity=alt.value(0),
).add_selection(
    nearest
)

# Draw points on the line, and highlight based on selection
points = line.mark_point().encode(
    opacity=alt.condition(nearest, alt.value(1), alt.value(0))
)

# Draw text labels near the points, and highlight based on selection
text = line.mark_text(align='left', dx=5, dy=-5).encode(
    text=alt.condition(nearest, 'y:Q', alt.value(' '))
)

# Draw a rule at the location of the selection
rules = alt.Chart(source).mark_rule(color='gray').encode(
    x='x:Q',
).transform_filter(
    nearest
)

# Put the five layers into a chart and bind the data
alt.layer(
    line, selectors, points, rules, text
).properties(
    width=600, height=300
)