Pandas, Plotly et Jupyter : De l’analyse de données à l’application en ligne (2/3)
Temps de lecture estimé 10 minutes.
Dans un article précédent nous vous proposions une analyse de données à l’aide de la bibliothèque Pandas. Nous y avions construit une série de graphiques simples pour réaliser cette analyse. Dans cet épisode, nous allons aborder les widgets qui vont nous permettre de rendre ces graphiques dynamiques.
Il est conseillé d’avoir lu l’article précédent qui détaille la structure des données utilisées.
Qu’est-ce qu’un widget ?
Dans un calepin jupyter, le code peut facilement être édité et rejoué. Il est donc assez simple d’effectuer des changements. Il est toutefois possible que les utilisateurs finaux de l’application ne sachent pas programmer ou simplement qu’on préfère avoir un moyen simple d'interagir (sans avoir à relire le code Python et à le modifier). Dans de tels cas, les widgets constituent une bonne solution.
Les widgets sont des objets qui sont rendus dynamiquement dans les calepins Jupyter, et avec lesquels il est possible d’interagir.
La bibliothèque de base pour construire ces widgets est ipywidgets
.
Dans l’exemple ci-dessous, la bibliothèque est importée puis un curseur glissant est construit.
>>> import ipywidgets as ipw
>>> ipw.IntSlider(min=0, max=20, step=2)
À l’exécution de la cellule Jupyter, le widget est affiché.
La connexion entre le widget affiché dans la page Web et l’objet python a été automatiquement définie. Cela signifie que si l’objet python est modifié, le rendu du widget est modifié et vice-versa. Dans le cas présent, l’attribut value
du widget vaut 6
.
À titre d’exemple, on peut construire un curseur glissant comme ceci :
>>> slider = ipw.IntSlider(min=0, max=20, step=2)
>>> slider
puis modifier dynamiquement la valeur de cet objet. Le rendu sera alors mis à jour.
>>> from time import sleep
>>> for i in range(0, 22, 2):
... sleep(1)
... slider.value = i
Les widgets deviennent très intéressants dès lors que l’on associe des fonctions python à des évènements. Dans l’exemple ci-dessous, nous avons défini deux widgets de type “curseur glissant” et un widget d’affichage. Nous voulons afficher dans ce dernier widget la somme des deux curseurs.
On construit un widget de type Bouton, et on associe le clic sur ce bouton à l’appel de la fonction compute_add
qui somme les valeurs des deux curseurs et met à jour l’affichage.
>>> from IPython.display import clear_output
>>> sld1 = ipw.IntSlider(min=0, max=20)
>>> sld2 = ipw.IntSlider(min=0, max=20)
>>>
>>> out = ipw.Output()
>>> with out:
... print("0 + 0 = 0")
...
>>> def compute_add(evt):
... with out:
... clear_output()
... res = sld1.value + sld2.value
... print(f"{sld1.value} + {sld2.value} = {res}")
...
>>> btn = ipw.Button(description="Sum")
>>> btn.on_click(compute_add)
>>> ipw.HBox([ipw.VBox([sld1, sld2, btn]), out])
Le rendu est alors le suivant :
Utiliser un widget pour sélectionner les données à afficher
Dans l’épisode précédent, nous avions écrit une fonction pour charger toutes les données des licenciés inscrits dans les fédérations sportives pour les années 2012 à 2019. La fonction est la suivante :
>>> from pathlib import Path
>>> import pandas as pd
>>> DATA_DIR = Path().resolve() / "data"
>>> def load_data():
... year_dfs = []
... for year in range(2012, 2019):
... fname = f"sport_license_holders_{year}.csv"
... yr_df = pd.read_csv(
... DATA_DIR / fname,
... dtype={"dep_code": str},
... index_col=["dep_code", "dep_name", "fed_code", "fed_name", "gender", "age"],
... )
... yr_df.rename(columns={"lic_holders": str(year)}, inplace=True)
... year_dfs.append(yr_df)
... data = pd.concat(year_dfs, axis=1)
... return data
...
>>> d = load_data()
Le DataFrame
résultant contient plus de 1.6 millions de lignes et 7 colonnes. Nous pouvons maintenant écrire une fonction très simple qui affiche l’évolution du nombre de licenciés de 2012 à 2019 pour les fédérations qui sont données en paramètre.
>>> pd.options.plotting.backend = "plotly" # Choose Plotly as the plotting back-end
>>> def plot_license_holders_evolution_by_sport(data, fed_codes):
... data_sports = data.groupby(level=["fed_code", "fed_name"]).sum()
... sel_data_sports = data_sports.loc[list(fed_codes)]
... sel_data_sports = sel_data_sports.droplevel(0)
... sel_data_sports.index.name = "Federations"
... fig = sel_data_sports.transpose().plot(title="Sport license holders")
... fig.update_layout(xaxis_title="year", yaxis_title="number of license holders")
... return fig
...
>>> plot_license_holders_evolution_by_sport(d, [109, 115, 242, 117])
Nous souhaitons utiliser un widget proposant de sélectionner une ou plusieurs disciplines, puis afficher le graphique correspondant lorsque la sélection est validée.
La première chose que nous réalisons est un dictionnaire contenant en clef le nom des fédérations sportives et en valeur leur numéro associé. Ce dictionnaire pourra être fourni à un widget de type SelectMultiple
.
Nous utilisons le code suivant pour obtenir le dictionnaire de correspondance :
>>> def extract_federation_names_codes(data):
... codes = data.index.get_level_values(
... "fed_code"
... ) # Extract all the values from the level ``fed_codes`` of the index
... names = data.index.get_level_values(
... "fed_name"
... ) # Extract all the values from the level ``fed_names`` of the index
... dic = {name: code for code, name in zip(codes, names)}
... return dic
...
Et finalement, la fonction suivante permet de construire l’interface souhaitée :
>>> from IPython.display import display
>>> def build_gui(data):
... fed_values = extract_federation_names_codes(data)
... fed_wdg = ipw.SelectMultiple(
... options=fed_values, description="Sport federations", rows=20
... )
... plt_btn = ipw.Button(description="Plot")
... out_wdg = ipw.Output()
... # Define the hook function that will be called each time the button is clicked
... def refresh_plot(evt):
... fed_codes = fed_wdg.value
... with out_wdg:
... clear_output()
... display(plot_license_holders_evolution_by_sport(data, fed_codes))
...
... plt_btn.on_click(refresh_plot)
... gui_wdg = ipw.HBox([ipw.VBox([fed_wdg, plt_btn]), out_wdg])
... return gui_wdg
...
>>> build_gui(d)
Nous venons ainsi de faire une fonction qui construit une interface utilisateur, composée d’un widget permettant de faire une sélection multiple. Lorsque la sélection est validée, la fonction d’affichage du graphique est rappelée, mettant ainsi le composant à jour. Le développement de cette interface utilisateur est bien plus simple que ce que nous aurions eu à faire avec d'autres solutions comme Qt, Tkinter ou même Flask + Javascript.
On voit que cela permet à tous les utilisateurs de faire leur propre analyse sans avoir à changer une seule ligne de code.
Dans le prochain épisode, nous présenterons Voila qui permet de transformer un calepin Jupyter en une petite application Web, utilisable sans aucune connaissance de Python. Nous utiliserons également jupyter-flex pour obtenir une jolie application Web dotée de bulles d’aides, d’onglets et d’un menu latéral.