Makina Blog
Augmenter l'intéractivité de vos notebooks Jupyter
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.
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 :
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.
a) 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
b) 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'
)
c) 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
d) 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)
e) 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],
)
2) 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é.
a) 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'
b) 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')
3) 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.
a) 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')
b) 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')
c) 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],
)
d) 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.
Formations associées
Formations IA / Data Science
Formation Jupyter Notebook
Nantes - Toulouse - Paris ou distanciel A la demande
Voir la formationFormations IA / Data Science
Formation Mise en place de projets Deep Learning avec Keras
Toulouse Du 19 au 21 mars 2025
Voir la formationActualités en lien
Fabriquer une couche raster à partir d'une géométrie avec PostGIS
Cet article présente une procédure de manipulation de données SIG avec PostGIS afin de fabriquer une couche raster de densité.
La formation Python éligible au CPF est enfin arrivée
Makina Corpus propose un nouvelle formation Python éligible au CPF. Grâce à cette certification, cette formation peut être entièrement financée par votre compte Compte Personnel de Formation.
CANARI Europe, un service climatique innovant pour adapter l'agriculture européenne
Après un lancement réussi de CANARI l'application de projections climatiques dédiée à l'agriculture en France, CANARI s’étend à toute L’Europe et au nord du Maghreb.