Journal Créer une application web avec Jupyter, ipywidgets et voilà

Posté par  . Licence CC By‑SA.
36
4
oct.
2019
Ce journal a été promu en dépêche : Créer une application web avec Jupyter, ipywidgets et voilà.

Sommaire

Cher journal,
tu connais sans doute Jupyter, cet outil de développement tournant dans un navigateur qui est particulièrement en vogue chez les scientifiques et plus généralement dans les domaines liés au traitement des données. Aujourd'hui je vais te parler d'une possibilité offerte par Jupyter qu'il ne me semble pas, sauf erreur de ma part, avoir vu évoquée ici, à savoir le développement d'applications web.

À propos de Jupyter

A titre personnel, et peut-être comme beaucoup des plus anciens (disons 40 ans et plus), j'ai longtemps été très réticent à ce "machin à la mode" ne voyant pas bien ce qu'il pouvait apporter à mon flux de travail habituel basé sur un éditeur de texte et une console, et aussi ne lui trouvais-je que des inconvénients:

  • lancer jupyter-notebook dans une console, basculer sur le navigateur, parcourir l’arborescence, juste pour pouvoir visualiser un fichier ipynb me semble très peu ergonomique. Nous sommes en 2019 et ce truc ne gère pas le double clic. Il y a des solutions pour contourner ce problème, par exemple nteract est une application de bureau basée sur electron qui permet de se passer du navigateur.
  • le fait que les cellules de code puissent être exécutées dans n'importe quel ordre peut amener à des confusions à la lecture des notebooks.
  • le format ipynb (qui est en fait du json contenant plus d'informations que le simple code) est nativement peu compatible avec git : par exemple le simple fait d'exécuter une cellule modifie la numérotation de celle-ci et git détecte une modification. La encore, il y a des solutions pour contourner ça, mais tout de même.
  • tout cela a été mieux présenté par d'autres que moi par exemple ici (diapos de la présentation).

Et bien, cher journal, j'avais tort.

En effet, Jupyter ne vise pas à remplacer notre bonne vieille console, et encore moins notre éditeur de texte favori, mais se place entre les deux. Je paraphraserai cette dépêche en disant que Jupyter est "une console sous stéroïdes". En effet, Jupyter permet d'exécuter des blocs de code sans avoir à écrire / sauvegarder / exécuter un script en bonne et due forme et, à l'instar de la console iPython dont Jupyter dérive directement, touts les objets sont sauvegardés dans un noyau pour pouvoir être réutilisés ailleurs dans le programme sans avoir à tout ré-exécuter. Ceci en fait un excellent outil d'expérimentation et de prototypage de programmes. Par ailleurs, le fait que les notebooks Jupyter contiennent non seulement le code, mais aussi les graphiques et figures produits, et qu'il soit possible d'y adjoindre du texte riche (markdown, html, LaTeX…) les rendent particulièrement intéressants pour l'enseignement et le partage des connaissances (et non pas le partage du code, car comme dit précédemment et comme le font remarquer certains à juste titre, le code est fortement obscurci par le format ipynb). Il est intéressant de noter que Jupyter est régulièrement évoqué au sein du mouvement open science, mouvement qui vise à faciliter la diffusion, non seulement des résultats et connaissances scientifiques, mais aussi des données brutes et des protocoles d'analyses et de traitements de ces données, au sein de la communauté scientifique et auprès du grand public. Voir par exemple ces quelques liens: [1] [2] [3].
Bien évidemment ces notebooks Jupyter sont exportables dans différents types de formats (html, pdf, etc) et peuvent également être aisément mis en ligne. Nbviewer permet, par exemple, de partager des notebooks simplement en passant une URL ou l'adresse d'un dépôt git.

Pour modérer cet enthousiasme débordant il est premièrement bon de rappeler que toutes ces visualisations (y compris sur nbviewer) sont strictement statiques. Il n'est pas possible d'interagir avec ceux-ci et donc de ré-exécuter tout ou partie du notebook. Deuxièmement, c'est bien joli de partager des notebooks, mais quid des lecteurs qui ne maîtrisent pas bien, voire pas du tout, le langage dans lequel lesdits notebooks ont été développés? Ce sont ces points que je me propose d'aborder ici.

PS1 : toutes les bibliothèques présentées ci-dessous sont installables via pip ou conda-forge.
PS2 : Les petits extraits de code donnés ci-dessous son disponibles sur mon github.

Ajouter des composants graphiques avec ipywidgets

Ipywidgets désigne un ensemble de composants graphiques pour le langage python (slider, combo box, boutons, etc.) destinés à rendre les notebooks plus interactifs. En gros il s'agit d'une architecture permettant de lier un objet python (le widget), tournant dans le noyau, à sa représentation JavaScript/HTML/CSS tournant dans le navigateur. Par exemple, afficher un slider qui permet de modifier la variable d'entrée d'une fonction et d'en afficher le résultat s'écrit simplement

from ipywidgets import interact
import ipywidgets as widgets

def f(x):
    return x**2

interact(f, x=10.);

La fonction interact est en fait 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 f. Il est possible d'avoir un contrôle beaucoup plus fin en paramétrant le widget à la main. Le code ci-dessous donne strictement le même résultat :

#définit l'objet slider
mon_slider = widgets.FloatSlider(
    value=10,
    min=-10,
    max=30,
    step=0.1,
    description='x',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout = True
)

#créé un zone de texte pour l'affichage du résultat
resultat = widgets.Output()

#définit l'action à effectuer lorsque le slider est modifié
def maj_resultat(change):
    with resultat:
        resultat.clear_output()
        print(f(change['new']))

#observe le slider
mon_slider.observe(maj_resultat, names='value')

#affiche les widgets
display(mon_slider,resultat)

C'est évidemment beaucoup plus lourd, mais il me semble que cet exemple illustre bien la richesse des potentialités offertes par ipywidgets. La documentation de ipywidgets est tout simplement excellente, et il est possible de maîtriser cette bibliothèque assez rapidement.

Ipywidgets n'est pas seulement une bibliothèque d'objets graphiques. Il s'agit véritablement d'un cadre de développement sur lesquels les développeurs peuvent s'appuyer pour écrire leurs propres widgets. En voici quelques uns:
- ipyvuetify: pour celles et ceux qui trouveraient les widgets de base de ipywidget trop austères, cette bibliothèque apporte à Jupyter les widgets vuetify qui implémentent des composants graphiques obéissants aux spécifications de material design.
- bqplot : un "doit-avoir" absolu pour quiconque s’intéresse à la visualisation en 2D de données. Je donnerai un exemple d'implémentation ci-dessous, mais, en bref, dans bqplot chaque élément d'un graphique (axes, légende, données, graduations, etc.) est en fait un widget avec lequel il est possible d'interagir et de modifier les propriétés programmatiquement.
- ipyvolume : même chose mais pour la visualisation de données en 3D, s'appuyant sur WebGL.
- ipyleaflet : affichage et manipulation de cartes et données géographiques.
- ipywebrtc : permet de diffuser et manipuler du contenu audio ou video depuis à peu près n'importe quelle source (fichier, webcam, etc.)

Afin d'illustrer la compatibilité entre ipywidgets et d'autres bibliothèques (ici bqplot) le code ci-dessous permet d'effectuer les actions suivantes:
- sélectionner une fonction à tracer via un menu déroulant
- la figure est mise à jour en fonction du choix (ipywidgets -> bqplot)
- il est possible de déplacer des points dans la figure
- les coordonnées des points sont affichées dans un champs texte (bqplot -> ipywidgets)

from ipywidgets import interact, fixed
import ipywidgets as widgets
from bqplot import pyplot as plt
import numpy as np
from numpy.random import rand

#génère des abscisses
x = np.arange(0,10,0.1)

#créé une figure y = f(x)
ma_figure = plt.figure(animation_duration = 300)
mon_tracé = plt.scatter(x, x**2, enable_move=True)
plt.xlabel('Axe des x')

#initialise une zone d'affichage de texte
resultat2 = widgets.Output()

#choix de la fonction à tracer -> créé automatiquement un menu déroulant
#modifie le tracé en fonction de la valeur du widget
#il est possible d'utiliser interact par un décorateur
#il est possible de fixer les variables ne devant pas faire l'objet d'un widget
@interact(fonction=['parabole', 'sinus', 'hasard'], x=fixed(x))
def choix_fonction(fonction, x):
    if fonction=='parabole':
        with mon_tracé.hold_sync():
            mon_tracé.x = x
            mon_tracé.y = x**2
            plt.ylabel('x au carré')
    if fonction=='sinus':
        with mon_tracé.hold_sync():
            mon_tracé.x = x
            mon_tracé.y = np.sin(x)
            plt.ylabel('sin(x)')
    if fonction=='hasard':
        with mon_tracé.hold_sync():
            mon_tracé.x = x
            mon_tracé.y = rand(len(x))
            plt.ylabel('Nombres aléatoires')

#fonction qui lit et affiche les coordonnées d'un point déplacé
def affiche(name, value):
    with resultat2:
        resultat2.clear_output()
        print('Le point n° %i a été déplacé en x = %f y = %f'
              %(value['index'], value['point']['x'],value['point']['y']))

#détecte le déplacement d'un point
mon_tracé.on_drag_end(affiche)     

#créé la gui
#il est possible de mixer des widgets créés via interact avec d'autre définis "à la main"
widgets.VBox([ma_figure,resultat2])

Tous ces projets sont relativement jeunes et il peut arriver que la documentation ne soit pas exhaustive (c'est par exemple le cas de bqplot). Dans ce cas, il peut être très intéressant de cloner ou télécharger le dépôt github et d'aller fouiller dans le répertoire 'examples'. Dans le cas de bqplot c'est une véritable mine d'or.

Masquer le code avec Appmode ou Voilà

Maintenant que nous savons comment créer une petite interface graphique, pourquoi ne pas cacher tout ce code afin de ne pas effrayer les débutants ? Appmode est une extension pour Jupyter qui permet très exactement de faire cela : l'extension ajoute un bouton 'Appmode' à l'interface de Jupyter qui, lorsqu'il est cliqué, créé une nouvelle instance du notebook, celui-ci est alors entièrement exécuté et seuls les widgets sont affichés.

Si elle est très efficace, cette extension peut être problématique si le notebook est destiné à être hébergé sur un serveur Jupyter ouvert hébergé par votre entreprise / université / école… En effet le notebook reste entièrement accessible et rien n'interdit l'exécution de code arbitraire. C'est ce problème que solutionne voilà. Ce projet est très jeune puisqu'il n'a été annoncé que cet été dans ce très instructif billet de blog mais il s'avère déjà particulièrement efficace. En bref, lorsque l'URL du notebook est appelée, celui s'exécute intégralement et les cellules de résultats (incluant les widgets) sont converties en une page HTML+JavaScript qui est ensuite présentée à l'utilisateur. En principe, l'utilisateur ne peut plus exécuter de code arbitraire. Pour ceux à qui ça parle (dont je ne fais pas partie), ça repose, entre autres, sur tornado.
La galerie de voilà regorge d'exemples. Par exemple celui-ci où on peut jouer avec une fonction gaussienne: lien. En voyant cet exemple il est utile de rappeler que tout ceci n'est rien d'autre qu'un notebook Jupyter.

Héberger la webapp

Derniére étape de finalisation de notre webapp : la mise en ligne. Comme je l'évoquais plus haut, nbviewer est exclu puisque celui-ci ne permet pas d'interagir avec les notebooks. Si vous avez la chance d'avoir votre propre serveur Jupyter distant (ou d'avoir des admins compétents et sympas), c'est immédiat. Il vous suffit d'activer l'extension voilà:

jupyter serverextension enable voila --sys-prefix

puis de préfixer l'URL du notebook avec 'voila' ; par exemple
http://URL_DU_SERVEUR/NOM_DU_NOTEBOOK.ipynb
devient
http://URL_DU_SERVEUR/voila/NOM_DU_NOTEBOOK.ipynb

Notez qu'il est possible de visualiser le rendu du notebook en local simplement en entrant dans votre terminal
voila notebook.ipynb et le rendu sera visible sur localhost:8866.

Si vous n'avez pas de serveur Jupyter ouvert sur le web, tout n'est pas perdu. Binder permet de venir se brancher sur un dépôt git, puis, à l'aide d'un fichier requirements.txt ou environment.yml listant les dépendances requises, Binder va construire une image Docker du dépôt et votre notebook sera servi via un JupyterHub. Le contenu du fichier environment.yml pour les exemples précédents est :

channels:
  - conda-forge
dependencies:
  - numpy
  - ipywidgets
  - bqplot
  - voila

Finalement le notebook est accessible par un lien de la forme : Binder
Deux remarques utiles à ce stade:
- La création de l'image Docker peut prendre plusieurs minutes.
- Concernant voilà, il existe apparemment un moyen d’atterrir directement sur le rendu html du notebook et non le notebook lui-même en préfixant le nom du notebook avec /voila/render/, mais pour moi cela ne semble pas fonctionner avant la première création de l'image. Il faut donc cliquer sur le bouton 'voilà' dans le notebook pour cacher le code source.

Une solution alternative à mybinder est heroku. La procédure de déploiement est cependant moins aisée (mais les tutoriels sont très bons), et les dépendances considérées comme "obscures" par heroku ne sont pas gérées : par exemple SciPy n'est pas géré, ce qui est handicapant pour des applications techniques ou scientifiques.

Mot de la fin

Pour conclure, et pour illustrer le fait qu’il est possible de créer des applications relativement élaborées, je partage ici un lien vers une application scientifique que j’ai récemment développée et qu’un collègue de notre département TIC a œuvré à mettre en ligne (un grand merci à lui !) : https://radmax.unilim.fr

Pour ceux que ça intéresserait, les sources se trouvent ici : https://github.com/aboulle/RaDMaX-webapp

En bref il s'agit dans le graphique de droite de faire coller la courbe calculée (en rouge) sur des mesures de diffraction des rayons X expérimentales. Les graphes de gauches sont manipulables et le rendu des calculs est donné en temps réel dans le graphe de droite. Pour le contexte, l'objectif final est de déterminer les dommages que subissent des matériaux soumis à des radiations. Ces dommages sont quantifiés par l'évolution en profondeur du taux de déformation et de désordre atomique (graphes de gauche). Le calcul est paramétré par les différents widgets. C'est encore expérimental, la stabilité n'est donc pas garantie ; de plus, il est possible que l'URL change dans les jours à venir.

Merci de m'avoir lu jusqu'ici :-)

  • # une dépêche

    Posté par  (site web personnel) . Évalué à 6.

    Ce merveilleux journal transformé en dépêche de qualitai est déjà en modération !

    "La liberté est à l'homme ce que les ailes sont à l'oiseau" Jean-Pierre Rosnay

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.