Accueil / Blog / Métier / 2019 / Augmenter l'intéractivité de vos notebooks Jupyter

Augmenter l'intéractivité de vos notebooks Jupyter

Par Daphné Lercier — publié 18/10/2019, édité le 21/10/2019
Dans cet article, nous allons rapidement présenter cet outil que peut être le notebook Jupyter et surtout ensuite, parler des contrôles (ipywidgets) que nous pouvons utiliser pour rendre les sorties interactives, allant jusqu'à créer de petites interfaces utilisateurs ou tableaux de bord.
Augmenter l'intéractivité de vos notebooks Jupyter

Nous vous parlions des notebooks Jupyter dès 2016 dans nos retours sur la PyConf. Depuis, ils ont beaucoup évolués, se sont enrichis et peuvent aussi bien servir à l'expérimentation, au prototypage, à la documentation, à la présentation de résultats ou encore à l'enseignement.

Aujourd'hui, vous les retrouverez dans nos formations et sur notre dépôt GitHub de tutoriels.

Les notebooks Jupyter

Les notebooks du projet Jupyter sont des documents interactifs qui peuvent comporter du code exécutable, des équations, des visualisations, du texte, des images.

Ils se consultent et s'utilisent à l'aide d'un simple navigateur web (si vous êtes de celles ou ceux qui préfèrent avoir une application bureautique, le projet nteract répondra sûrement à vos attentes). Ils se composent de cellules, qui peuvent être des cellules de texte (au format Markdown, HTML, LaTeX, etc) ou des cellules de code.

Grâce à ses cellules de code, un notebook peut être vu comme un long script découpé en morceaux que l’on peut exécuter à la demande. Ceci peut être particulièrement utile pour des projets expérimentaux où l'on teste des idées les unes à la suite des autres, ou encore pour construire un tutoriel détaillé. Toutes les variables sont sauvegardées dans un noyau pour pouvoir être réutilisés à tout moment dans le notebook. Python n'est pas le seul langage supporté, plus de quarante langages peuvent être utilisés dans ces notebooks (dont R, Julia, Scala, bash, perl) - une liste est disponible sur le wiki du projet.

Chacune des cellules de code peut produire une sortie numérique, textuelle ou graphique. Généralement toutes ces sorties sont statiques. Une bibliothèque a été conçue pour obtenir des sorties interactives dans les notebooks Jupyter, il s'agit d'ipywidgets, et c'est ce dont nous allons parler dans la suite de cet article.

Obtenir des sorties interactives

La bibliothèque ipywidgets rassemble l'ensemble des contrôles et composants graphiques (menus déroulants, boutons, sélecteurs, calendrier, etc) qui peuvent être utilisés dans des notebooks Jupyter avec le langage Python.

Installation de la bibliothèque ipywidgets

La bibliothèque ipywidgets peut être installée via l'outil pip (et dans ce cas, il sera nécessaire de l'activer avant d'ouvrir vos notebooks) :

pip install ipywidgets
jupyter nbextension enable --py widgetsnbextension

ou directement via l'installateur conda de la distribution Anaconda :

conda install -c conda-forge ipywidgets

Interact, la fonction "couteau-suisse" pour débuter

La façon la plus simple de commencer avec les widgets Ipython, et de créer une sortie interactive, est d'utiliser la fonction interact.

Pour cela, vous configurez une fonction qui fait quelque chose d'intéressant, vous spécifiez la plage de choix de paramètres et vous appelez interact qui génère à l'avance les résultats et affiche un (ou des) curseur(s) javascript qui vous permet(tent) d'interagir avec ces derniers.

  • Avec une variable en entrée

Un premier exemple très simple :

from ipywidgets import interact

def ma_fonction_interessante(x):
   return x

interact(ma_fonction_interessante, x=10)

En fonction de la valeur par défaut donné à la variable x, le type de contrôle affiché va varier.

Variable d'entrée Contrôle affiché
Un booléen Une case à cocher
Une chaîne de caractères Une zone de texte
Une valeur entière ou un tuple d'entiers : (min, max) ou (min, max, step) Un curseur pour la sélection d'un entier
Une valeur réelle ou un tuple de réels : (min, max) ou (min, max, step) Un curseur pour la sélection d'un flottant
Une liste ou un dictionnaire Une liste déroulante

Vous pouvez facilement le tester en changeant le type de la variable x dans l'exemple précédent.

  • Avec plusieurs variables en entrée

La fonction contrôlée par interact peut avoir plusieurs variables d'entrée. Dans ce cas, chacune de ces variables pourra être sélectionnée par un contrôle dédié.

Voici un exemple pour modifier les paramètres d'une représentation graphique:

import numpy as np
import matplotlib.pyplot as plt

def signal_plot(amplitude, color):
   # Create a figure
   fig, ax = plt.subplots(figsize=(5, 4))
   # Add a grid
   ax.grid(color='#EEEEEE', linewidth=2, linestyle='solid')
   # Define the x range
   x = np.linspace(0, 10, 1000)
   # Plot the sinusoid
   ax.plot(x, amplitude * np.sin(x), color=color, lw=5, alpha=0.6)
   # Define the x and y limits
   ax.set_xlim(0, 10)
   ax.set_ylim(-1.1, 1.1)

interact(signal_plot,
            amplitude=(0, 1.0, 0.1),
            color=['blue', 'green', 'red'])

La fonction interact est un raccourci vers un ensemble de widgets graphiques avec des choix faits par défaut selon le type d’objet (int, float, bool, list, etc) passé à la fonction associée. Il est possible d’avoir un contrôle beaucoup plus fin en paramétrant le widget à la main.

Organiser les contrôles sans interact

Pour paramétrer des widgets "à la main", nous allons laisser de côté la fonction interact et définir explicitement chacun des contrôles que nous voulons, ainsi que chacune de leurs interactions.

Pour avoir un exemple complet, nous allons créer une petite interface utilisateur qui permettra la visualisation de marches aléatoires. L'idée est de générer un trajet d'un nombre aléatoire de pas dans un intervalle choisi par l'utilisateur, et de permettre le changement de couleur ou de style du tracé.

Voici l'aperçu de ce à quoi nous voulons arriver :

randomwalk_ui.png

Cette interface se compose d'un générateur de nombre aléatoire, d'un cadre central pour l'affichage graphique, et d'un panneau pour la configuration des options graphiques.

  1. Créer un générateur de nombre aléatoire

L'idée ici est de combiner plusieurs contrôles afin de permettre à l'utilisateur de choisir un intervalle dans lequel un nombre sera tiré aléatoirement. Nous avons donc besoin :

  • d'un sélecteur permettant de choisir un intervalles de nombres entiers ;
  • d'un bouton ;
  • et d'une zone de texte pour afficher le résultat.

L'ensemble des contrôles disponibles dans la bibliothèque ipywidgets sont listés et documentés ici.

  1. Initialisation d'un sélecteur d'entiers

Pour le moment, rien de compliqué. Nous initialisons seulement le contrôle dont nous avons besoin, à savoir un IntRangeSlider.

# Create a slider to select a range
my_range = widgets.IntRangeSlider(
  description='Intervalle choisi :',
  min=0,
  max=10000,
  value=(1000,5000), #Default value
  style={'description_width': 'initial'},
  orientation='vertical'
)

L'option de style utilisée ici permet de s'assurer que le titre du widget ne sera pas rogné à l'affichage. D'autres options de style peuvent être ajoutées, notamment en ajoutant un objet Layout à votre widget (voir la documentation à ce sujet).

L'intervalle choisi est accessible sous forme d'un tuple par l'attribut value.

my_range.value
  1. Création d'un bouton

Nous faisons de même pour initialiser le bouton de notre interface.

# Create a button
my_button = widgets.Button(
   description='Générer',
   button_style='', # 'success', 'info', 'warning', 'danger' or ''
   tooltip='Générer un nombre aléatoire'
)
  1. Création d'une zone de texte

Nous ajoutons maintenant une zone de texte pour afficher le résultat.

my_text = widgets.IntText(
   description = 'Résultat :',
   disabled = True,
   style={'description_width': 'initial'},
)

Le contenu de cette zone de texte est aussi accessible par l'attribut value (pour le renseigner ou le lire).

my_text.value
  1. Communication des contrôles entre eux

Nous voulons désormais que nos trois contrôles communiquent entre eux : au clic sur le bouton, un nombre doit être tiré dans l'intervalle du sélecteur et affiché dans le champ de résultat.

Pour cela, les boutons de la bibliothèque ipywidgets possèdent une méthode on_click permettant de gérer les événements qui doivent avoir lieu au clic. Cette méthode prend en paramètre le nom de la fonction à exécuter.

def on_button_clicked(event):
   # Get the selected range
   my_min = my_range.value[0]
   my_max = my_range.value[1]
   # If a correct range is selected
   if(my_min < my_max):
       # Get a random int in this range
       my_nb = np.random.randint(my_min, my_max)
       # Display this number
       print(my_nb)
       # Update the button style
       my_button.button_style = 'success'
       my_button.icon = 'check'
   else:
       # Update the button style
       my_button.button_style = 'danger'
       my_button.icon = ''

# Define the 'on_click' event
my_button.on_click(on_button_clicked)
  1. Encapsulation des contrôles

Ces trois contrôles peuvent désormais être assemblés dans une boîte commune (Box, VBox pour un empilement vertical ou HBox pour un empilement horizontal) qui servira à construire l'interface finale.

my_LVBox = widgets.VBox(
   [my_range, my_button, my_text],
)
  1. Afficher le graphique d'une marche aléatoire à partir du nombre généré

Le résultat que nous obtenons grâce à notre générateur de nombres aléatoires va désormais nous servir au tracé d'une marche aléatoire.

Tout d'abord, voici la fonction de marche aléatoire que nous utiliserons :

def get_random_walk(n):
   """
   This function creates two array containing x and y coordinates of the random walk.
   :param n : number of steps
   """
   #creating two arrays for containing x and y coordinates
   #of size equals to the number of size and filled up with 0's
   x = np.zeros(n)
   y = np.zeros(n)

   # filling the coordinates with random variables
   for i in range(1, n):
       val = random.randint(1, 4)
       if val == 1:
           x[i] = x[i - 1] + 1
           y[i] = y[i - 1]
       elif val == 2:
           x[i] = x[i - 1] - 1
           y[i] = y[i - 1]
       elif val == 3:
           x[i] = x[i - 1]
           y[i] = y[i - 1] + 1
       else:
           x[i] = x[i - 1]
           y[i] = y[i - 1] - 1

   return x,y

Cette fonction prend en entrée un nombre de pas et retourne deux tableaux contenant les coordonnées x et y du tracé.

  1. bqplot, un "graphique widget"

Pour afficher le tracé, nous utilisons la bibliothèque bqplot. Dans notre exemple, elle va nous permettre d’interagir avec le graphique (changer le tracé, modifier la couleur, etc) sans avoir à recharger à chaque fois toute la figure (ce que Matplotlib nous obligerait à faire).

# Initialize the random walk with 0 steps
walk_x, walk_y = get_random_walk(0)

# Use linear scales
sc_x = bq.LinearScale()
sc_y = bq.LinearScale()

# Create the line with the coordinates of the random walk
walk = bq.Lines(x=walk_x, y=walk_y, scales={'x': sc_x,'y': sc_y}, opacities=[0.6])

# Define axis
ax_x = bq.Axis(scale=sc_x)
ax_y = bq.Axis(scale=sc_y, orientation='vertical')

# Create a figure
fig = bq.Figure(marks=[walk], axes=[ax_x, ax_y],
             fig_margin=dict(top=20, bottom=20, left=20, right=20))

# Fix the figure size
fig.layout.height = '450px'
fig.layout.width = '450px'
  1. Lier le générateur de nombre aléatoire à l'affichage graphique

Maintenant, grâce à bqplot, nous pouvons facilement redessiner le tracé à chaque fois qu'un nouveau nombre aléatoire est généré.

Pour cela, nous utilisons la méthode observe de notre zone de texte qui permet d'appeler une fonction à chaque fois que sa valeur change.

def on_value_change(change):
   """
   Update the random walk when a new number is generated.
   """
   # Random number
   n = my_text.value
   # Calculate a new random walk
   wx, wy = get_random_walk(n)
   # Update the plot
   walk.x = wx
   walk.y = wy

my_text.observe(on_value_change,'value')
  1. Rendre configurable des options graphiques

Dernière partie de notre interface, nous allons ajouter quelques contrôles pour pouvoir changer facilement la couleur ou/et le style du tracé. Ce panneau de gauche est construit de la même façon que le générateur de nombre aléatoire.

  1. Changer la couleur du tracé
my_color = widgets.Dropdown(
  options=['blue', 'green', 'red'],
  value='blue',
  description='Couleur:',
  disabled=False,
  style={'description_width': 'initial'},
)

def on_color_change(change):
   """
   Change the plot color
   """
   walk.colors = [my_color.value]

my_color.observe(on_color_change, 'value')
  1. Changer le style du tracé
my_line = widgets.Dropdown(
   options=['solid', 'dashed', 'dotted', 'dash_dotted'],
   value='solid',
   description='Style des lignes:',
   disabled=False,
   style={'description_width': 'initial'}
)

def on_line_change(change):
   """
   Change the line style
   """
   walk.line_style = my_line.value

my_line.observe(on_line_change, 'value')
  1. Ajouter une image et encapsuler les contrôles
# Open the file containing our image and read it
with open('pedestrians-1209316_1920.jpg','rb') as my_file:
     img = my_file.read()

# Create an Image widget to display it in the UI
my_img = widgets.Image(
   value=img,
   format='jpg'
)

my_RVBox = widgets.VBox(
   children=[my_img, my_color, my_line],
)
  1. Construire l'interface

Il ne reste plus maintenant qu'à assembler les différents blocs dans un AppLayout : le générateur de nombre aléatoire, la figure, et les options graphiques.

widgets.AppLayout(
   header=None,
   left_sidebar=my_LVBox,
   center=fig,
   right_sidebar=my_RVBox,
   footer=None,
   align_items="center",
   width='85%'
)

Et voilà notre interface est complète !

Conclusion

J'espère que cet exemple vous aura donné envie d'utiliser des ipywidgets dans vos notebooks Jupyter. Ils peuvent être particulièrement utiles pour explorer plus facilement vos données ou rendre vos présentations plus interactives.

Vous trouverez beaucoup d'autres exemples sur Github, par exemple comme ici pour tester différents modèles de régression. Ainsi que d'autres widgets en dehors de la bibliothèque ipywidgets, comme sur le site de Jupyter.

Les notebooks Jupyter sont des outils très riches, nous vous en reparlerons sûrement prochainement !

Cet article est disponible sous forme de notebook Jupyter (évidemment) sur le GitHub de Makina Corpus.

Si ce sujet vous intéresse, n'hésitez pas à nous contacter ou à suivre l'une de nos prochaines formations Jupyter Notebook.

ABONNEZ-VOUS À LA NEWSLETTER !