Améliorer l’architecture de ses projets
Présentation des principes d’architecture permettant de produire des projets modulaires et maintenables, et d’outils pour faciliter leur adoption.
La structuration d’un projet permet d’immédiatement identifier les éléments de code et les éléments annexes, par exemple les dépendances à gérer, la documentation, etc.
Un certain nombre d’assistants au développement de projets orientés données ont émergé pour gagner en productivité et faciliter le lancement d’un projet (voir ce post très complet sur les extensions VisualStudio
).
L’idée générale est de privilégier une structure de projet bien plus fiable qu’une suite sans structure de scripts ou un Notebook Jupyter (voir ce post de blog sur ce sujet). L’IDE Jupyter
n’est pas le meilleur outil pour le développement de projets Python
; pour des projets intensifs en code il vaut mieux se tourner vers des IDE classiques comme VSCode
.
Structure des projets
La structure du projet suivante rend compliquée la compréhension du projet:
- Quelles sont les données en entrée de chaine ?
- Dans quel ordre les données intermédiaires sont-elles crées ?
- Quel est l’objet des productions graphiques ?
├── report.qmd
├── correlation.png
├── data.csv
├── data2.csv
├── fig1.png
├── figure 2 (copy).png
├── report.pdf
├── partial data.csv
├── script.R
└── script_final.py
Source : eliocamp.github.io
Les principes généraux sont les suivants:
- Organiser son projet en sous-dossiers
- Donner des noms pertinents aux fichiers
- Documenter son projet
- (Faire de son projet un ensemble de modules voire un package)
Adopter une structure de projet
Le principe général d’une structure de projet est le suivant :
- Tous les fichiers nécessaires au projet dans un même dossier ;
- Le dossier à la racine du projet sert de working directory ;
- Utilisation de chemins relatifs plutôt qu’absolus.
Le principe d’une structure de projet est d’adopter une structure arbitraire, mais lisible et cohérente.
├── README.md
├── .gitignore
├── data
│ ├── raw
│ │ ├── data.csv
│ │ └── data2.csv
│ └── derived
│ └── partial data.csv
├── src
| ├── script.py
│ ├── script_final.py
│ └── report.qmd
└── output
├── fig1.png
├── figure 2 (copy).png
├── figure10.png
├── correlation.png
└── report.pdf
Les output sont stockés dans un dossier séparé, de même que les inputs. Idéalement les inputs ne sont même pas stockés avec le code, nous reviendrons sur la distinction entre l’espace de stockage du code et des données plus tard.
Comme Git
est un pré-requis, tout projet présente un fichier .gitignore
(il est très important, surtout quand on manipule des données qui ne devraient pas se retrouver sur Github
ou Gitlab
).
Un projet présente aussi un fichier README.md
à la racine, nous reviendrons dessus.
Un projet qui utilise l’intégration continue présentera également :
- si vous utilisez
Gitlab
, les instructions sont stockées dans le fichiergitlab-ci.yml
- si vous utilisez
Github
, cela se passe dans le dossier.github/workflows
Autodocumenter le projet avec des noms pertinents
Rien qu’en changeant le nom des fichiers, on rend la structure du projet très lisible:
├── README.md
├── .gitignore
├── data
│ ├── raw
│ │ ├── dpe_logement_202103.csv
│ │ └── dpe_logement_202003.csv
│ └── derived
│ └── dpe_logement_merged_preprocessed.csv
├── src
| ├── preprocessing.py
│ ├── generate_plots.py
│ └── report.qmd
└── output
├── histogram_energy_diagnostic.png
├── barplot_consumption_pcs.png
├── correlation_matrix.png
└── report.pdf
Maintenant, le type de données en entrée de chaine est clair, le lien entre les scripts, les données intermédiaires et les output est assez transparent.
Documenter son projet
Le fichier README.md
, situé à la racine du projet, est à la fois la carte d’identité et la vitrine du projet. Sur Github
et Gitlab
, comme il s’agit de l’élément qui s’affiche en accueil, ce fichier fait office de première impression, instant très court qui peut être déterminant sur la valeur évaluée d’un projet.
Idéalement, le README.md
contient :
- Une présentation du contexte et des objectifs du projet
- Une description de son fonctionnement
- Un guide de contribution si le projet accepte des retours dans le cadre d’une démarche open-source
Quelques modèles de README.md
complets, en R
:
Modularité
Dans le chapitre précédent, nous avons recommandé l’utilisation des fonctions. Le regroupement de plusieurs fonctions dans un fichier est appelé un module.
La modularité est un principe fondamental de programmation qui consiste à diviser un programme en plusieurs modules ou scripts indépendants, chacun ayant une fonctionnalité spécifique. Comme indiqué précédemment, la structuration d’un projet sous forme de modules permet de rendre le code plus lisible, plus facile à maintenir et plus réutilisable. Python
fournit un système d’importation flexible et puissant, qui permet de contrôler la portée des variables, les conflits de noms et les dépendances entre les modules1.
Tests unitaires
Les tests unitaires sont des tests automatisés qui vérifient le bon fonctionnement d’une unité de code, comme une fonction ou une méthode. L’objectif est de s’assurer que chaque unité de code fonctionne correctement avant d’être intégrée dans le reste du programme.
Les tests unitaires sont utiles lorsqu’on travaille sur un code de taille conséquente ou lorsqu’on partage son code à d’autres personnes, car ils permettent de s’assurer que les modifications apportées ne créent pas de nouvelles erreurs.
En Python
, on peut utiliser le package unittest
pour écrire des tests unitaires. Voici un exemple tiré de ce site :
# fichier test_str.py
import unittest
class ChaineDeCaractereTest(unittest.TestCase):
def test_reversed(self):
= reversed("abcd")
resultat self.assertEqual("dcba", "".join(resultat))
def test_sorted(self):
= sorted("dbca")
resultat self.assertEqual(['a', 'b', 'c', 'd'], resultat)
def test_upper(self):
= "hello".upper()
resultat self.assertEqual("HELLO", resultat)
def test_erreur
if __name__ == '__main__':
unittest.main()
Pour vérifier que les tests fonctionnent, on execute ce script depuis la ligne de commande:
python3 test_str.py
.----------------------------------------------------------------------
1 test in 0.000s
Ran
OK
Si on écrit des tests unitaires, il est important de les maintenir !
Prendre du temps pour écrire des tests unitaires qui ne sont pas maintenus et donc ne renvoie plus de diagnostics pertinent est du temps perdu.
Template de projet data science
En Python
il existe des modèles de structure de projets : les cookiecutters
. Il s’agit de modèles d’aborescences de fichiers (fichiers Python
mais également tout type de fichiers) proposés par la communauté et téléchargeables comme point de départ d’un projet.
L’idée de cookiecutter
est de proposer des templates que l’on utilise pour initialiser un projet, afin de bâtir à l’avance une structure évolutive. On va s’inspirer de la structure du template datascience développé par la communauté. La syntaxe à utiliser dans ce cas est la suivante :
$ pip install cookiecutter
$ cookiecutter https://github.com/drivendata/cookiecutter-data-science
Le modèle est personnalisable, notamment pour faciliter l’interaction entre un système de stockage distant. L’aborescence générée est assez massive pour permettre une grande diversité de projet. Il n’est souvent pas nécessaire d’avoir toutes les composantes du cookiecutter
.
Structure complète générée par le cookiecutter data science
├── LICENSE ├── Makefile <- Makefile with commands like make data
or make train
├── README.md <- The top-level README for developers using this project. ├── data │ ├── external <- Data from third party sources. │ ├── interim <- Intermediate data that has been transformed. │ ├── processed <- The final, canonical data sets for modeling. │ └── raw <- The original, immutable data dump. │ ├── docs <- A default Sphinx project; see sphinx-doc.org for details │ ├── models <- Trained and serialized models, model predictions, or model summaries │ ├── notebooks <- Jupyter notebooks. Naming convention is a number (for ordering), │ the creator’s initials, and a short -
delimited description, e.g. │ 1.0-jqp-initial-data-exploration
. │ ├── references <- Data dictionaries, manuals, and all other explanatory materials. │ ├── reports <- Generated analysis as HTML, PDF, LaTeX, etc. │ └── figures <- Generated graphics and figures to be used in reporting │ ├── requirements.txt <- The requirements file for reproducing the analysis environment, e.g. │ generated with pip freeze > requirements.txt
│ ├── setup.py <- Make this project pip installable with pip install -e
├── src <- Source code for use in this project. │ ├── init.py <- Makes src a Python module │ │ │ ├── data <- Scripts to download or generate data │ │ └── make_dataset.py │ │ │ ├── features <- Scripts to turn raw data into features for modeling │ │ └── build_features.py │ │ │ ├── models <- Scripts to train models and then use trained models to make │ │ │ predictions │ │ ├── predict_model.py │ │ └── train_model.py │ │ │ └── visualization <- Scripts to create exploratory and results oriented visualizations │ └── visualize.py │ └── tox.ini <- tox file with settings for running tox; see tox.readthedocs.io ```
Transformer son projet en package Python
Le package est la structure aboutie d’un projet Python
autosuffisant. Il s’agit d’une manière formelle de contrôler la reproductibilité d’un projet car:
- le package assure une gestion cohérente des dépendances
- le package offre une certaine structure pour la documentation
- le package facilite la réutilisation du code
- le package permet des économies d’échelle car on peut réutiliser l’un des packages pour un autre projet
- le package facilite le debuggage car il est plus facile d’identifier une erreur quand elle est dans un package
- …
En Python
, le package est une structure peu contraignante si on a adopté les bonnes pratiques de structuration de projet. A partir de la structure modulaire précédemment évoquée, il n’y a qu’un pas vers le package: l’ajout d’un fichier pyproject.toml
qui contrôle la construction du package (voir ici).
Il existe plusieurs outils pour installer un package dans le système à partir d’une structure de fichiers locale[^devtools]. Les deux principaux sont
[^devtools] L’équivalent R
de ces packages est devtools
Le package fait la transition entre un code modulaire et un code portable, concept sur lequel nous reviendrons dans le prochain chapitre.
Vers la séparation du stockage du code et des données et de l’environnement d’exécution
La séparation du stockage du code et des données ainsi que de l’environnement d’exécution est importante pour plusieurs raisons.
Tout d’abord, cela permet de garantir la sécurité et l’intégrité des données. En séparant les données du code, il devient plus difficile pour n’importe qui d’accéder aux informations sensibles stockées dans les données.
En séparant l’environnement d’exécution, il est possible de s’assurer que le code fonctionne de manière cohérente et sans conflit avec d’autres programmes exécutés sur le même système ou n’est pas altéré par des configurations systèmes difficiles à reproduire. Cette séparation facilite également la portabilité et l’adaptation de l’application à différentes plateformes, en permettant de modifier l’environnement d’exécution sans avoir à modifier le code ou les données.
Le prochain chapitre sera consacré à la gestion des dépendances. Il illustrera la manière dont environnement d’exécution et code d’un projet peuvent être reliés afin de créer de la portabilité.
Publication
Quand on débute, on est souvent timide et on désire ne rendre public son code que lorsque celui-ci est propre. C’est une erreur classique:
Comme pour nettoyer un appartement, les petits gestes en continu sont beaucoup plus efficace qu’un grand ménage de printemps. Prendre l’habitude de mettre son code immédiatement sur Github
vous amènera à adopter de bons gestes.
Maintenance
L’objectif des conseils de ce cours est de réduire le coût de la maintenance à long terme en adoptant les structures les plus légères, automatisées et réutilisables.
Les notebooks Jupyter sont très pratiques pour tâtonner et expérimenter. Cependant, ils présentent un certain nombre d’inconvénients à long terme qui peuvent rendre impossible à maintenir le code écrit avec dans un notebook:
- tous les objets (fonctions, classes et données) sont définis et disponibles dans le même fichier. Le moindre changement à une fonction nécessite de retrouver l’emplacement dans le code, écrire et faire tourner à nouveau une ou plusieurs cellules.
- quand on tâtonne, on écrit du code dans des cellules. Dans un cahier, on utiliserait la marge mais cela n’existe pas avec un notebook. On créé donc de nouvelles cellules, pas nécessairement dans l’ordre. Quand il est nécessaire de faire tourner à nouveau le notebook, cela provoque des erreurs difficile à debugger (il est nécessaire de retrouver l’ordre logique du code, ce qui n’est pas évident).
- les notebooks incitent à faire des copier-coller de cellules et modifier marginalement le code plutôt qu’à utiliser des fonctions.
- il est quasi-impossible d’avoir un versioning avec Git des notebooks qui fonctionne. Les notebooks étant, en arrière plan, de gros fichiers JSON, ils ressemblent plus à des données que des codes sources. Git ne parvient pas à identifier les blocs de code qui ont changé
- passage en production des notebooks coûteux alors qu’un script bien fait est beaucoup plus facile à passer en prod (voir suite cours)
- Jupyter manque d’extensions pour mettre en oeuvre les bonnes pratiques (linters, etc.). VSCode au contraire est très bien
- Risques de révélation de données confidentielles puisque les outputs des blocs de code, par exemple les
head
, sont écrits en dur dans le code source.
Globalement, les notebooks sont un bon outil pour tâtonner ou pour faire communiquer. Mais pour maintenir un projet à long terme, il vaut mieux privilégier les scripts. Les recommandations de ce cours visent à rendre le plus léger possible la maintenance à long terme de projets data-science en favorisant la reprise par d’autres (ou par soi dans le futur).
Références
- The Hitchhiker’s Guide to Python
- Tidyverse style guide
- Google style guide
- Cours de Pierre-Antoine Champin
- R Packages par Hadley Wickham and Jenny Bryan
- La documentation collaborative
utilitR
- Project Oriented Workflow
- Un post très complet sur les extensions VisualStudio
- “Coding style, coding etiquette”
Footnotes
A cet égard,
Python
est beaucoup plus fiable queR
. DansR
, si deux scripts utilisent des fonctions dont le nom est identique mais issues de packages différents, il y aura un conflit. EnPython
chaque module sera importé comme un package en soi.↩︎