Billets

Retours sur la PyConFR 2023

16/03/2023 by Frank Bessou

500 mots - 3 minutes

La PyConFR est le rendez-vous immanquable de la communauté Python en France. Hébergé cette année par l'Université de Bordeaux, cet évènement a rassemblé des développeurs, novices et expérimentés, pendant quatre jours autour de sprints, conférence et ateliers. Après presque trois ans d'attente, nous étions contents de pouvoir enfin retrouver la communauté Python.

Sprint ReservoirPy

Les jeudi et vendredi 15 et 16 étaient consacrés aux sprints, ces ateliers qui rassemblent plusieurs personnes pour faire avancer des projets choisis au préalable.

Nous avons pu contribuer à reservoirpy, une bibliothèque de Reservoir Computing développée à l'INRIA. Nous avons travaillé sur la publication automatique via l'intégration continue (GitHub Actions en l'occurrence) de nouvelles versions de la bibliothèque sur Pypi et sur un entrepôt Anaconda.

Pour en savoir plus sur le Reservoir Computing, vous pouvez regarder cette vidéo d'introduction captée à Dataquitaine en février 2022).

Conférences

Le programme des conférences était très riche et nous avons apprécié la diversité des thématiques (généralistes, web, science des données, devops, ...). Nous n'avons pas pu aller voir toutes les conférences, mais voici un échantillon de celles qui ont particulièrement retenu notre attention.

NucliaDB, une base de données pour le machine learning et les données non-structurées

Éric Bréhault (Nuclia) a présenté NucliaDB qui est une base de données vectorielle, c'est-à-dire qu'elle permet d'associer des données à des vecteurs situés dans un espace ayant de nombreuses dimensions. Adaptée à un usage en machine learning, cette base de données propose une API permettant d'indexer des données non structurées, de faire des recherches sémantiques, etc.

À la découverte de Polars (ou pourquoi vous pourriez quitter pandas)

Cette présentation de Olivier Hervieu nous a fait découvrir une alternative à Pandas pour le traitement de données tabulaires, nommée Polars. Cette bibliothèque est utilisable en Rust et en Python. Nous avons retenu sa capacité à charger des données de manière paresseuse à partir de fichiers.

Python moderne et fonctionnel pour des logiciels robustes

La présentation de Guillaume Desforges (Tweag) a mis en avant les avantages de la programmation fonctionnelle et son applicabilité au langage Python. Elle s'est terminée par une présentation de l'architecture en oignon appliquée à une application Flask.

Psycopg, troisième du nom

Durant cette conférence, Denis Laxalde (Dalibo) a présenté l'historique de la bibliothèque Psycopg. Nous avons également pu découvrir le protocole de communication utilisé pour parler avec un cluster PostgreSQL. Enfin, nous avons observé comment Psycopg s'appuie sur la bibliothèque libpq pour proposer une API haut niveau aux développeurs Python. La version 3 de Psycopg apporte de nombreuses améliorations dont le support de async/await, le support du mode pipeline ou encore le typage statique. Nous sommes fiers à Logilab d'avoir contribué à son financement.

Conclusion

Les PyConFR sont toujours un grand moment partagé avec la communauté Python. Que ce soit pendant les sprints ou entre les conférences, nous avons eu l'opportunité de rencontrer des développeurs de tous horizons et d'échanger avec eux sur des problématiques communes. Rendez-vous dans un an pour la prochaine édition et le 16 mars dans nos locaux parisiens pour un Afpyro.


AFPYRo du 16 mars 2023

27/02/2023 by Simon Chabot

Les AFPYRo reprennent et le prochain aura lieu dans nos locaux parisiens entre Denfert Rochereau et la Place d'Italie !

Un AFPYRo est un événement organisé par l’AFPy − Association Francophone Python − pour regrouper des personnes souhaitant discuter du langage de programmation Python dans un cadre convivial. Après une ou deux présentations (vous pouvez proposer la vôtre), nous échangerons autour de quelques pizzas.

Le prochain AFPYRo sera donc à Logilab, au 104 Boulevard Auguste Blanqui 75013 Paris, le 16 mars 2023 de 19h à 21h et nous offrirons les pizzas.

N’hésitez pas à vous inscrire et à passer nous voir !


Demandez le programme ! Le calendrier 2023 des formations est en ligne.

19/01/2023 by Charlotte Cazals

Logilab a publié son calendrier de formations pour le premier semestre 2023. Ces formations commenceront à partir du mois de mars avec un programme varié et modulable.

Cette année, nous proposons des formations sur Toulouse en plus de nos incontournables formations à Paris. Par ailleurs, quelques-unes de nos formations (qui s'y prêtent bien) se dérouleront en ligne afin d'éviter des déplacements et des heures de trajet inutiles. Grâce à nos outils de visio-conférence, vous pourrez profiter de sessions d'exercices avec un suivi aussi soigné que pour nos formations en présentiel.

Cette année, quelques nouveautés ont été ajoutées au catalogue afin de couvrir au mieux les besoins de formation que nous avons identifiés. Par exemple, la formation "Exploiter le Web des données avec Python (2 jours)" qui s'adresse à des experts et des techniciens dans le domaine de la publication de données ouvertes.

Spécialiste de Python en France depuis 2000, nous proposons toujours un large choix de formations sur ce langage de programmation et ses bibliothèques. Nous proposons également des formations sur :

Nous limitons volontairement le spectre de nos formations pour ne proposer que les sujets que nous pratiquons au quotitien. Assister à une formation Logilab, c'est donc la garantie d'apprendre avec des professionnels compétents maîtrisant parfaitement le sujet qu'ils enseignent. Nos formations sont toujours ajustées en fonction des stagiaires présents. Pour toute demande spécifique, n'hésitez pas à contacter notre service dédié.

Logilab est déclarée comme organisme de formation depuis sa création, est référencée dans Data-Dock, et a été certifiée Qualiopi au titre des actions de formations. Les formations que nous proposons peuvent donc être financées par vos OPCO.


Le meetup Python, c'est ce soir !

03/10/2017

Membre toujours actif de la communauté Python, Logilab soutient le meetup Python Nantes sur luigi et behave qui aura lieu ce soir, à 19h au site de Voyages-Sncf.com situé au sud de la Gare SNCF, au 5ème étage du bâtiment Jalais dont l'entrée principale se trouve au 34 rue du Pré Gauchet.

image

Entrée gratuite, mais inscription obligatoire.

INSCRIVEZ-VOUS !


Pandas, Plotly et Jupyter : De l'analyse de données à l'application en ligne (3/3)

28/07/2022 by Pierre Choffé

Temps de lecture estimé 3 minutes.

Dans les articles précédents nous avons utilisé Pandas pour analyser un jeu de données, et avons créé des graphiques interactifs avec un calepin Jupyter. Cet article conclut la série en montrant comment utiliser Voilà et Jupyter-flex pour créer une application Web à partir d'un tel calepin.

Comme nous l'avons vu dans les deux premiers articles de cette série, l'utilisation de calepins Jupyter améliore le flux de travail des chercheurs et scientifiques depuis la phase exploratoire jusqu'à la communication des résultats. Pour un public non spécialisé, la présentation du code dans les calepins peut avoir un aspect rebutant, c'est pourquoi nous allons maintenant examiner Voilà et Jupyter-flex, qui permettent de créer facilement des tableaux de bord à partir des calepins.

Voilà

Voilà est un outil très simple qui permet de transformer nos calepins en applications web ou en tableaux de bord.

Voilà cache le code et affiche seulement le texte, les widgets et les résultats des calculs réalisés par le code, y compris les graphiques. Il se lance avec la commande voila <mon-calepin.ipynb>.

Si l'on reprend nos exemples précédents, cela donne :

Et si l'on reprend nos tableaux :

Le document reste interactif et on peut jouer avec, tout comme on le ferait avec un calepin, mais sans intervenir sur le code.

Jupyter Flex

Jupyter-flex quant à lui permet de créer des tableaux de bord HTML basés sur des calepins Jupyter. Il suffit pour cela d'ajouter un tag body dans la ou les cellules que l'on souhaite afficher dans le tableau de bord, puis de lancer la commande jupyter nbconvert --to flex <mon-calepin.ipynb> --execute qui exécutera toutes les cellules du calepin et retournera un joli tableau de bord composé avec les cellules taguées.

À noter : si nous utilisons la commande précédente, nous obtenons une version statique, ce qui peut être le comportement souhaité pour un tableau de bord. Si nous lançons le calepin avec Voilà, nous avons un tableau de bord dynamique.

Chez Logilab, nous utilisons Jupyter-flex pour notre tableau de bord interne. Couplé à la CI, il est mis à jour régulièrement.

Nous avons utilisé Jupyter et Jupyter-flex dans le cadre de projets clients, notamment le projet Resourcecode pour l'IFREMER (Institut Français de Recherche pour l'Exploitation de la Mer) et ses partenaires.

Les outils créés à cette occasion sont visibles dans la page ResourceCodeTools et le code disponible dans l'entrepôt GitLab de l'Ifremer.

Comme on le voit dans cette illustration tirée du projet Resourcecode, on peut ajouter un menu latéral, des onglets ou une infobulle d’information. Jupyter-flex propose une architecture Cards -> Section : une Card (encart) contient une ou plusieurs cellules (code ou markdown) taguées (body, source, footer, ou même help pour afficher un modal) et une Section est constituée d'un ou plusieurs encarts que l'on affiche en colonnes ou en rangées. Jupyter-flex utilise Material UI qui s'appuie sur Grid qui est basé sur CSS Flexbox.

La documentation de Jupyter-flex est claire et comporte bien d'autres informations et options intéressantes, notamment pour la mise en page ou pour l'association Voilà et Jupyter-flex.

Note : à la date où nous écrivons, Jupyter-flex n'est pas compatible avec les dernières versions de Voilà ce qui devrait être rapidement résolu.


Pandas, Plotly et Jupyter : De l’analyse de données à l’application en ligne (2/3)

27/06/2022 by Simon Chabot

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.


jupyterlab-friendly-traceback

24/05/2022 by Olivier Giorgis

Temps de lecture ~1 min (100 mots)

Dans le cadre de ses formations, Logilab à créé jupyterlab-friendly-traceback, une extension JupyterLab qui permet d'utiliser friendly-traceback de façon interactive dans les calepins Jupyter.

Le but du module Friendly-traceback est de remplacer les messages d'erreurs standards de Python par des messages plus complets et plus faciles à comprendre. Ce module permet, entre autre, d'expliquer ce qui a provoqué la levée d'une exception dans un programme.

Les informations données par Friendly-traceback ont une grande valeur pédagogique et permettent aux développeurs Python débutants, voir confirmés, de mieux comprendre les erreurs présentes dans leur code.

Pour utiliser l'extension jupyterlab-friendly-traceback, il suffit de la pip-installer dans votre environnement de la façon suivante:

$> pip install jupyterlab-friendly-traceback

Il est ensuite possible d'activer et de désactiver l'extension JupyterLab en cliquant sur un bouton qui apparaît dans la barre d'outils du calepin Jupyter.


Typage du paquet RQL

18/02/2022

Temps de lecture = 4 min (~700 mots)

Contexte

Le projet RQL est l'implémentation d'un parser pour un langage de requête du même nom permettant d'interroger une base de données qui a été créée avec un schéma YAMS. Ce langage de requête est au coeur de CubicWeb.

Le cadriciel CubicWeb est très largement utilisé dans nos projets à Logilab, et donc nous continuons à maintenir CubicWeb et ses dépendances en le faisant évoluer suivant nos besoins. Parfois ce besoin concerne le langage d'interrogation RQL lui-même. Nous aimerions par exemple ajouter les chemins de propriétés qui existent en SPARQL (voir SPARQL property paths) ou encore la possibilité d'avoir des propriétés calculées dans les attributs de projection.

Mise en oeuvre

Pour faciliter ces évolutions, nous avons décidé de profiter des progrès récents de Python et d'enrichir la base code avec des annotations de type et de nous appuyer sur MyPy pour valider nos remaniements.

Le projet de typage de RQL a été un projet de longue haleine. Nous pensions que quelques semaines suffiraient mais il a été nécessaire d'y passer plusieurs mois pour arriver à un résultat satisfaisant. Typer l'ensemble d'un projet nécessite de comprendre son fonctionnement global, ce qui peut très vite être chronophage, surtout quand les pratiques de développement ont bien évolué.

Au lieu de s'attaquer au monolithe d'un seul coup, nous avons commencé par typer les modules séparémment les uns des autres, en ajoutant des commentaires #type: ignore aux endroits ne pouvant pas encore être typés, et sans forcémment essayer de détailler les interactions entre les différents modules. Les # type: ignore ont ensuite peu a peu disparu.

Problèmes rencontrés

Le typage aura permis de déceler des soucis de conception du projet RQL et de voir les limites du typage en Python.

Principe de Substitution de Liskov

Ce principe dit qu'une sous-classe doit pouvoir être utilisée là où une classe parente est attendue. Celui-ci n'est pas toujours respecté dans RQL. Par exemple, la méthode copy de la classe Insert ne prend pas d'argument alors que la même méthode sur la classe BaseNode en prend un. Cette différence de signature pourrait causer des problèmes dans du code client.

Le problème a été signalé par mypy:

rql/stmts.py:1283: error: Signature of "copy" incompatible with supertype "BaseNode"  [override]
rql/stmts.py:1283: note:          def copy(self, stmt: Optional[Statement] = ...) -> BaseNode
rql/stmts.py:1283: note:          def copy(self) -> Insert
Found 3 errors in 1 file (checked 1 source file)

Mixins difficilement typables

L'implémentation de l'arbre syntaxique qui a été choisie utilise beaucoup de mixins. Ces mixins ne sont pas typables de manière élégante.

Prenons par exemple le mixin OperatorExpressionMixin suivant:

class OperatorExpressionMixin:

    ...

    def is_equivalent(self: Self, other: Any) -> bool:
        if not Node.is_equivalent(self, other):
            return False
        return self.operator == other.operator

    ...

Il ne s'applique que sur des classes qui héritent de BaseNode et qui ont un attribut "operator" mais ce type ne peut pas être exprimé, car on aurait besoin de l'intersection de deux types, dont un classe, ce qui n'existe pas en Python (https://github.com/python/typing/issues/213).

En Typescript par exemple on aurait écrit:

type Self = BaseNode & {operator: string}

Covariance/Contravariance/...

Les types génériques, List par exemple, sont définis comme acceptant des paramètres de type. Lorsqu'on déclare ces paramètres de type (en utilisant TypeVar), il faut être attentif à choisir la variance appropriée, ce qui n'est pas trivial quand on vient de langages où ce n'est pas nécessaire (ni Typescript, ni C++, ni Java n'y font référence).

Conclusion

Nous avons publié une version 0.38 de RQL qui contient l'ajout des types et ne casse pas l'API utilisée. Ceci va nous aider à ajouter de nouvelles fonctionnalités et à remanier le code pour le simplifier. L'introduction du typage nous a également permis de déceler du code buggé ou jamais utilisé et de mieux documenter le code de RQL.

Merci à Patrick pour le temps qu'il a consacré à ce sujet important. Vous pouvez consulter son article de blog sur ce sujet ici


Pandas, Plotly et Jupyter : De l'analyse de données à l'application en ligne (1/3)

04/02/2022 by Simon Chabot

Temps de lecture estimé 10 minutes.

Nous proposons une série de quelques articles où nous allons utiliser la bibliothèque Pandas pour analyser les licences sportives en France. En chemin, nous réaliserons une interface utilisateur avec des widgets.

Cette série sera découpée en trois articles. Dans le premier, nous allons explorer le jeu de données à notre disposition en utilisant la bibliothèque Pandas. Dans le second, nous introduirons Jupyter et les ipywidgets qui nous permettront de faire une interface utilisateur. Nous terminerons la série en présentant Voilà ainsi que le thème jupyter-flex.

Pandas, jupyter, ipywidgets, voilà ? De quoi parle-t-on ?

  • Pandas est une bibliothèque Python très connue, qui permet d’analyser et d’étudier des jeux de données. Elle est conçue pour traiter des jeux de données tabulaires (ceux pouvant être lus par un tableur). Les données peuvent être de différents types (nombres, dates, chaînes de caractères, etc). Pandas est, comme nous le verrons, très efficace. Les fonctions coûteuses de Pandas sont généralement écrites en C, et Python est utilisé pour manipuler et appeler ces fonctions.
  • Jupyter est une plateforme, utilisable dans un navigateur web qui permet d’exécuter des calepins (notebooks). Un calepin est un fichier qui combine des cellules de différents types : du code exécutable, du texte, des visualisations, etc.
  • Les Ipywidgets sont des éléments graphiques interactifs que l’on peut ajouter à des calepins Jupyter. Ils vont nous permettre de proposer aux utilisateurs de choisir un fichier, choisir un élément dans une liste, cliquer sur un bouton, etc. Chacune des actions de l’utilisateur peut être associée à une fonction Python, et donc rendre le calepin interactif.
  • Voilà est une application qui permet d’exécuter des calepins, mais sans afficher le code source − qui est visible par défaut dans Jupyter. L’énorme intérêt à cela est qu’un calepin Jupyter devient alors une application web à part entière, utilisable dans le navigateur, et seuls les éléments indispensables à son utilisation sont visibles.

Après cette petite phase de présentation, découvrons les données que nous allons manipuler aujourd’hui.

Présentation des données

Dans cette série d’articles nous utilisons des données issues de https://data.gouv.fr. Il s’agit du nombre de licences sportives, par sexe, par catégorie d’âges, par municipalité pour les années 2012 à 2018. Les données brutes peuvent être téléchargées ici.

Nous avons réalisé une opération de nettoyage sur ces données, afin de nous assurer d’avoir une structure cohérente pour chaque année. Nous avons également remplacé les municipalités par leur département, ce qui permet d’alléger les données à manipuler. Au final, nous obtenons six fichiers csv, un par année, dont la structure est la suivante :

dep_code,dep_name,fed_code,fed_name,gender,age,lic_holders
01,Ain,101,Fédération Française d'athlétisme,F,00-04,0
01,Ain,101,Fédération Française d'athlétisme,F,05-09,75
01,Ain,101,Fédération Française d'athlétisme,F,10-14,251
01,Ain,101,Fédération Française d'athlétisme,F,15-19,130
01,Ain,101,Fédération Française d'athlétisme,F,20-29,39
01,Ain,101,Fédération Française d'athlétisme,F,30-44,105
01,Ain,101,Fédération Française d'athlétisme,F,45-59,105
01,Ain,101,Fédération Française d'athlétisme,F,60-74,23
01,Ain,101,Fédération Française d'athlétisme,F,75+,0
01,Ain,101,Fédération Française d'athlétisme,M,00-04,0
01,Ain,101,Fédération Française d'athlétisme,M,05-09,106
01,Ain,101,Fédération Française d'athlétisme,M,10-14,278
[…]
Nom de colonne Description
dep_code Code (unique) du département
dep_name Nom du département
fed_code Code (unique) de la fédération sportive
fed_name Nom de la fédération sportive
gender Genre (peut être M ou F)
age La tranche d’âge considérée (peut être 00-04, 05-09, 10-14, 15-19, 20-29, 30-44, 44-59, 60-74, 75+)
lic_holders Le nombre de licenciés dans le département, enregistrés dans cette fédération, de ce genre et de cette tranche d’âge.

Chargement de données pour une année

Pandas offre un nombre important de fonctions permettant de charger des données depuis différents formats: CSV, Excel, tableaux HTML, JSON, bases SQL, HDF5, etc. Nous allons utiliser la fonction read_csv. Cette fonction utilise les éléments de la première ligne comme noms de colonnes. Pandas essaie également de détecter les types de colonnes à utiliser (nombre, date, chaîne de caractères) en se basant sur les premiers éléments lus. Nous spécifions donc à Pandas que la colonne dep_code est de type str, pour prendre en compte les départements Corse (2A et 2B), sans quoi Pandas émettra un avertissement.

from pathlib import Path
import pandas as pd

DATA_DIR = Path().resolve() / "data"  # en supposant que les données sont dans le dossier data

d2012 = pd.read_csv(
    DATA_DIR / "sport_license_holders_2012.csv", dtype={"dep_code": str}
)

Nous obtenons alors la DataFrame suivante :

Premières analyses

À partir de là, nous pouvons commencer à étudier le jeu de données. Par exemple, en demandant le nom de chaque fédération :

>>> d2012["fed_name"].unique()
array(["Fédération Française d'athlétisme",
       "Fédération Française des sociétés d'aviron",
       'Fédération Française de badminton',
       'Fédération Française de basketball',
       'Fédération Française de boxe',
       'Fédération Française de canoë-kayak',
       'Fédération Française de cyclisme',
       "Fédération Française d'équitation",
       "Fédération Française d'escrime",
       […],
       'Fédération française de pentathlon moderne',
       'Fédération Française de javelot tir sur cible',
       'Fédération Flying Disc France', 'Fédération Française Maccabi',
       'Fédération Française de la course camarguaise',
       'Fédération Française de la course landaise',
       'Fédération Française de ballon au poing'], dtype=object)

Nous pouvons facilement compter le nombre total, toutes catégories confondues, de licenciés :

>>> d2012["lic_holders"].sum()
12356101

Une des forces de Pandas réside dans la possibilité de créer des filtres, ou des groupes simplement. Par exemple, pour compter le nombre de licenciés hommes, nous pouvons créer un masque (True si le genre est M et False sinon), puis appliquer ce masque à notre DataFrame.

>>> mask_male = d2012["gender"] == "M"
>>> d2012[mask_male]["lic_holders"].sum()
7806235

Ainsi, en 2012, il y avait 7 806 235 licenciés masculins de sport en France.

Combien y a-t-il de licenciés, en 2012, par tranche d’âge ? Pour répondre à cette question, nous utilisons la méthode groupby de Pandas, en donnant le nom de la colonne sur laquelle nous souhaitons faire le groupe :

>>> d2012.groupby("age")["lic_holders"].sum()

Cette méthode permet de constituer des groupes, selon une clé (généralement le nom d’une ou plusieurs colonnes), puis d’appliquer sur chaque groupe partageant la même clé une fonction d’agrégation. Dans notre exemple, la clé de chaque groupe est l’âge, et la fonction d’agrégation la somme sur la colonne lic_holders.

Nous pouvons effectuer le même type de calcul, mais en groupant cette fois-ci sur le genre et l’âge, ce qui donne le résultat suivant :

>>> d2012.groupby(["gender", "age"])["lic_holders"].sum()

Les deux résultats que nous venons d’obtenir sont ce qu’on appelle des Series. C’est-à-dire, des DataFrames mais constituées d’une seule colonne. On observe que les groupes sont directement constitués par l’index. Dans le cas d’un groupby() avec une seule colonne, nous avons un index simple et dans le cas où plusieurs colonnes sont utilisées, nous obtenons ce qu’on appelle un index multiple ou un index hiérarchique. Nous allons étudier cela un peu plus en profondeur dans la suite.

Créer un index sur mesure

Dans la DataFrame que nous avons chargée, de très nombreuses données sont répétées et ne sont utilisées que pour définir des groupes (dep_code, dep_name, gender, age etc). Nous allons mettre toutes ces données dans l’index de la DataFrame. Cela permet d’avoir dans l’index les données de chaque groupe, et dans la DataFrame les données desdits groupes (ici le nombre de licenciés sportifs).

Pour ce faire, nous utilisons la méthode set_index :

>>> d2012.set_index(
   ["dep_code", "dep_name", "fed_code", "fed_name", "gender", "age"], inplace=True
)
>>> d2012

Nous avons ainsi une DataFrame à une seule colonne, et avec un index à six niveaux. Nous pouvons toujours grouper par genre et par âge, en utilisant le mot-clé level, indiquant qu’il faut grouper en utilisant l’index :

>>> d2012.groupby(level=["gender", "age"]).sum()

Dans quels départements la course camarguaise est-elle pratiquée ?

La course camarguaise est un sport traditionnel dans lequel les participants tentent d'attraper des attributs primés fixés au frontal et aux cornes d'un bœuf. Pour savoir dans quels départements ce sport est le plus pratiqué, nous allons :

  1. Filtrer sur l’index pour n’avoir que les enregistrements correspondant à ce sport (le code de la fédération est 215) ;
  2. Grouper par code et nom de département, et compter le nombre de licenciés ;
  3. Afficher les groupes triés par ordre décroissant de licenciés.
>>> d2012_camarg = d2012.xs(
    215, level="fed_code"
)  # Only keep the rows with index equal to 215 at level ``fed_code``
>>> d2012_camarg_depts = d2012_camarg.groupby(
    ["dep_code", "dep_name"]
).sum()  # Group the data by department (only keep departments with non-null values)
>>> d2012_camarg_depts.sort_values(
    by="lic_holders", ascending=False
)  # Sort the data in decreasing order

Sans trop de surprise, on observe que c’est le Gard (où est la Camargue), les Bouches-du-Rhône, l’Hérault et le Vaucluse (départements qui entourent le Gard) qui ont le plus de licenciés dans ce sport.

Quels sont les sports les plus pratiqués par les femmes ?

Nous allons :

  1. Sélectionner les enregistrements correspondant à gender = 'F' ;
  2. Grouper par fédération et compter le nombre de licenciées ;
  3. Afficher les dix sports avec le plus de licenciées.
>>> d2012_females_top_10 = d2012.xs("F", level="gender")
    .groupby(["fed_code", "fed_name"])
    .sum()
    .nlargest(10, "lic_holders")
>>> d2012_females_top_10

Pandas permet également de faire des graphiques. Par défaut c’est la bibliothèque matplotlib qui est utilisée. Nous pouvons par exemple utiliser un diagramme en bâtons pour afficher le top 10 des sports pratiqués par les femmes :

>>> d2012_females_top_10.plot(
    kind="bar",
    legend=False,
    xlabel="Sport federation",
    ylabel="Number of license holders",
    color="#CC0066",
    title="Female sport license holders in 2012 (top 10)",
)

Charger les données pour toutes les années

Dans la section précédente, nous avons chargé uniquement les données de l’année 2012. Mais nous avons bien plus de données que cela. Nous allons donc charger chaque fichier, puis renommer la colonne lic_holders en fonction de l’année en cours. Nous aurons ainsi une DataFrame, avec en colonne le nombre de licenciés par année, et en index les différents groupes.

Nous allons faire une liste years_dfs qui va contenir toutes les DataFrames, une par année, puis nous allons simplement les concaténer. Cela donne donc :

>>> years_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)
>>>

On concatène toutes les DataFrames, en fonction de l’index (axis=1) :

>>> data = pd.concat(years_df, axis=1)
>>> data

Nous avons ainsi une DataFrame avec plus de 1.6 million de lignes, et 7 colonnes.

On peut maintenant afficher, par exemple, les 10 sports les plus pratiqués en fonction des années :

>>> data_sport = data.groupby(level=["fed_code", "fed_name"]).sum()
>>> data_sport.nlargest(10, "2012")

Nous avons ainsi le nombre de licenciés, par fédération et par année pour les 10 plus grosses fédérations de 2012. Le tri est effectué par rapport aux données de 2012.

On notera qu’en 2018 il y a 0 licencié de Karaté. Cela est probablement une erreur dans les données, ce qui peut arriver.

Tracer l'évolution du nombre de licenciés avec Plotly

Nous pouvons maintenant suivre l’évolution du nombre de licenciés dans certaines disciplines. Nous sélectionnons les sports dont le code de fédération est 109, 115, 242, 117.

>>> sel_data_sports = data_sports.loc[
...    [109, 115, 242, 117]
... ] # Select the rows whose value at the first level of the index (``fed_code``)
... # is one of the list items
>>> # Drop the first level of the index (``fed_code``)
>>> sel_data_sports = sel_data_sports.droplevel(0)
>>> # Will be used as the title of the legend
>>> sel_data_sports.index.name = "Federations"
>>> sel_data_sports.transpose().plot(
...    title="Sport license holders", xlabel="year", ylabel="number of license holders"
...)  # Transpose to have the years as the index (will be the X axis)

Comme nous le disions, par défaut Pandas utilise la bibliothèque matplotlib pour générer les graphiques. La figure produite ici est statique, elle peut facilement être insérée dans un rapport par exemple, mais cela présente des difficultés lors de la phase d’exploration.

Depuis quelque temps maintenant, Pandas est compatible avec plusieurs bibliothèques de visualisation. Il y a notamment Plotly, qui permet de faire des graphiques interactifs utilisables dans le navigateur web.

Pour utiliser Plotly, il est nécessaire de changer la bibliothèque utilisée par défaut.

# Choose Plotly as the plotting back-end
# this has to be done only once, usually at the begining of the code
>>> pd.options.plotting.backend = "plotly"

Une fois Plotly configurée, nous pouvons retracer le graphique, comme précédemment :

>>> fig = sel_data_sports.transpose().plot(title="Sport license holders")
>>> fig.update_layout(xaxis_title="year", yaxis_title="number of license holders")
>>> fig

Dans un environnement Jupyter, la figure produite est celle-ci, et il est possible de sélectionner/désélectionner les courbes à afficher :

Quelle est la prochaine étape ?

Nous avons dans ce premier article, chargé avec Pandas des données textuelles au format CSV. Nous avons vu comment et pourquoi utiliser un index multiple. Cela nous a permis de calculer quelques statistiques simples sur des groupes d’individus. Nous avons également produit des visualisations avec matplotlib et avec Plotly.

Dans le prochain article, nous utiliserons des widgets Jupyter pour manipuler dynamiquement les données à afficher sur les graphiques.