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

Posté par  . Édité par ZeroHeure, Ysabeau 🧶 🧦, Davy Defaud et Arkem. Modéré par claudex. Licence CC By‑SA.
Étiquettes :
37
4
oct.
2019
Python

Vous connaissez 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.

Sommaire

À propos de Jupyter

À 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, là 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 (diapos de la présentation filmée).

Eh bien, 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 la dépêche Python pour les sciences 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 et exécuter un script en bonne et due forme et, à l’instar de la console iPython dont Jupyter dérive directement, tous 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 enrichi (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 au sein de la communauté scientifique et auprès du grand public, non seulement des résultats et connaissances scientifiques, mais aussi des données brutes et des protocoles d’analyse et de traitement de ces données. Voir par exemple ces quelques liens : [1], [2] et [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 celles‑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.

P.‑S. — Toutes les bibliothèques présentées ci‑dessous sont installables via pip ou conda-forge.
P.‑P.‑S. — Les petits extraits de code donnés ci‑dessous sont 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ée une 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 d’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 lequel les développeurs peuvent s’appuyer pour écrire leurs propres bibliothèques de widgets. En voici quelques‑unes :

  • ipyvuetify, pour celles et ceux qui trouveraient les widgets de base d’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, en 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 vidéo 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 :

  1. sélectionner une fonction à tracer via un menu déroulant ;
  2. la figure est mise à jour en fonction du choix (ipywidgets -> bqplot) ;
  3. il est possible de déplacer des points dans la figure ;
  4. les coordonnées des points sont affichées dans un champ 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ée 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ée automatiquement un menu déroulant
# modifie le tracé en fonction de la valeur du widget
# il est possible d’utiliser interact via 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ée 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])

Toutes ces bibliothèques sont relativement jeunes et il peut arriver que la documentation ne soit pas exhaustive (c’est, par exemple, le cas pour 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 permet de créer une nouvelle instance du notebook, celui‑ci est alors entièrement exécuté et seules les widgets sont affichées.

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 fourni pas votre entreprise, votre université ou votre é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’adresse URL du notebook est appelée, celui‑ci 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, il (ou elle) ne peut plus exécuter de code arbitraire. Pour celles et ceux à qui ça parle (dont je ne fais pas partie), ça repose, entre autres, sur tornado.

La galerie de voilà regorge d’exemples, comme celui‑ci où l’on peut jouer avec une fonction gaussienne. En voyant cet exemple, il est utile de rappeler que tout ceci n’est rien d’autre qu’un notebook Jupyter.

Héberger l’application web

Dernière étape pour finaliser notre application web : 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 administrateurs et des administratrices 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 ». 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, entrez voila notebook.ipynb dans votre terminal 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. Mybinder 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, Mybinder 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 :

  1. la création de l’image Docker peut prendre plusieurs minutes ;
  2. 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.

Heroku est une solution alternative à mybinder. 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 !).

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 gauche 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.

Si ça vous intéresse, voici les sources.

Aller plus loin

  • # Manque de figures

    Posté par  . Évalué à 10.

    C'est vraiment dommage que tu n'aies pas mis des captures d'écran pour voir ce que cela donne, du coup pour que ton journal ait un intérêt, il faut le prendre comme un tutoriel …

  • # Bravo pour l'article

    Posté par  . Évalué à 5.

    Félicitations pour l'article, il est super bien détaillé et ça va sans doute me servir merci bien !

    Un peu dans la même veine, je suis récemment tombé sur https://streamlit.io .

    L'idée c'est grosso modo d'écrire une webapp Python sous forme de script et de pouvoir partager vite fait un prototype. J'ai testé rapidement, ça vaut le coup d'oeil !

    • [^] # Re: Bravo pour l'article

      Posté par  . Évalué à 1.

      Merci pour le partage. Je ne connaissais pas, mais je vais m'empresser de tester ça.

Suivre le flux des commentaires

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