Py module pour la data science : structurer notebooks et pipelines

Un notebook Jupyter qui grossit jusqu’à devenir un script monolithique de plusieurs centaines de cellules, c’est le symptôme d’un projet data science sans architecture de modules. Structurer son code Python en modules réutilisables séparés du notebook change la reproductibilité, la testabilité et la maintenabilité de l’ensemble du pipeline. Nous détaillons ici les choix d’architecture qui font la différence entre un prototype jetable et un projet industrialisable.

Py module et DVC : versionner les pipelines au-delà du notebook

Le notebook reste un outil d’exploration. Dès que le code doit être rejoué, partagé ou déployé, il devient un frein. La bascule vers un module Python structuré (arborescence de packages, fichiers __init__.py, scripts d’étapes) couplé à un outil de versioning de pipelines résout ce problème.

A lire également : Accès au Portail arena aix marseille depuis l'extérieur du réseau académique

DVC (Data Version Control) fonctionne comme un Git pour les données et les modèles. Il permet de versionner jeux de données, artefacts de préprocessing et modèles entraînés, en les liant au dépôt de code tout en stockant les fichiers volumineux sur du stockage objet (S3, MinIO). Le pipeline est alors décrit dans un fichier déclaratif (dvc.yaml) qui référence chaque étape du module Python.

Concrètement, un module data science bien découpé pour DVC ressemble à ceci :

A lire en complément : Trucnet logiciels système OS astuces : solutions pratiques contre les bugs récurrents

  • src/preprocess.py : nettoyage, encodage, feature engineering. Chaque fonction prend un DataFrame pandas ou un chemin de fichier CSV en entrée et renvoie un artefact sérialisé.
  • src/train.py : entraînement du modèle (scikit-learn, XGBoost, etc.), sauvegarde des métriques et du modèle via joblib ou pickle.
  • src/evaluate.py : chargement du modèle, calcul des métriques sur un jeu de test, export des résultats en JSON pour suivi.
  • notebooks/exploration.ipynb : le notebook n’importe que des fonctions depuis src, il ne contient aucune logique métier.

Ce découpage permet de rejouer le pipeline complet en une commande (dvc repro) et de l’intégrer en CI/CD sans toucher au notebook.

L’avantage moins évident de DVC réside dans la traçabilité des expériences. Chaque exécution du pipeline génère un fichier de métriques (metrics.json) qui peut être comparé entre branches Git. La commande dvc metrics diff affiche directement l’écart de performance entre deux versions du modèle. Cette granularité de suivi remplace les tableurs manuels de suivi d’expériences que beaucoup d’équipes maintiennent encore en parallèle de leur code.

Un autre bénéfice concerne la collaboration entre data scientists. Quand un membre de l’équipe modifie une étape de préprocessing, DVC détecte automatiquement quelles étapes en aval doivent être recalculées. Les étapes dont les dépendances n’ont pas changé sont ignorées, ce qui réduit considérablement le temps de calcul sur des jeux de données volumineux. Ce mécanisme de cache repose sur le hachage des fichiers d’entrée et de sortie de chaque étape.

Pour les équipes qui travaillent sur des branches parallèles, DVC propose aussi un mécanisme de stockage partagé. Quand un data scientist pousse ses artefacts sur le remote DVC (un bucket S3 par exemple), ses collègues peuvent récupérer exactement les mêmes fichiers de données et modèles avec dvc pull. Ce fonctionnement supprime les échanges de fichiers par messagerie ou disque partagé qui rendent impossible toute traçabilité.

Ingénieur data debout devant un bureau avec double écran affichant une architecture de pipeline Python et des modules structurés

Architecture d’un module Python pour la data science : arborescence et imports

Nous recommandons de traiter le module Python comme un package installable. Ajouter un pyproject.toml (ou un setup.cfg) à la racine permet d’installer le module en mode éditable (pip install -e .) et d’utiliser des imports absolus dans les notebooks comme dans les scripts.

Arborescence type d’un projet data science

Une structure qui a fait ses preuves :

  • my_project/ : package principal contenant __init__.py, sous-packages data/, features/, models/, utils/.
  • tests/ : tests unitaires avec pytest. Chaque module de my_project/ a son miroir dans tests/.
  • notebooks/ : fichiers .ipynb qui importent depuis my_project sans jamais redéfinir de fonctions localement.
  • dvc.yaml + params.yaml : description du pipeline et hyperparamètres versionnés.
  • pyproject.toml : métadonnées du package, dépendances (pandas, numpy, scikit-learn, matplotlib, seaborn).

L’erreur fréquente est de laisser les fonctions utilitaires dans le notebook puis de les copier-coller dans un script au moment du déploiement. Tout code appelé plus d’une fois appartient au module, pas au notebook.

Gestion des dépendances et environnement

Un fichier requirements.txt ne suffit plus pour garantir la reproductibilité. Les équipes qui passent à pyproject.toml avec des contraintes de version strictes (pandas>=2.0,<3) réduisent les incidents de régression lors des mises à jour de bibliothèques. L'association avec un environnement virtuel (venv ou conda) est le minimum pour isoler les dépendances du système.

Un point souvent négligé concerne les dépendances de développement. Séparer les dépendances de production (pandas, scikit-learn) des outils de développement (pytest, ruff, black) dans des groupes distincts du pyproject.toml évite d'alourdir l'image Docker de déploiement. La section [project.optional-dependencies] avec un groupe dev est la convention la plus répandue pour gérer cette séparation proprement.

Imports absolus contre imports relatifs

Les imports relatifs (from .utils import clean_data) fonctionnent dans un package mais posent problème dans les notebooks, qui ne font pas partie du package. Les imports absolus (from my_project.utils import clean_data) sont préférables parce qu'ils fonctionnent partout : scripts, notebooks, tests, conteneurs Docker.

Pour que les imports absolus fonctionnent dans un notebook, le package doit être installé en mode éditable. Sans cette installation, le notebook ne trouve pas le module et l'utilisateur finit par ajouter des sys.path.append en début de cellule, ce qui casse la portabilité du projet.

La commande pip install -e . crée un lien symbolique entre l'environnement virtuel et le répertoire du projet. Toute modification du code source est immédiatement disponible sans réinstallation. Ce fonctionnement est particulièrement adapté au cycle itératif de la data science, où le code du module évolue en permanence pendant la phase d'exploration.

Pipeline scikit-learn et intégration dans un module Python

Les objets Pipeline de scikit-learn enchaînent transformations et modèle dans un seul objet sérialisable. Quand ce pipeline est défini dans un module (et non dans une cellule de notebook), il devient testable unitairement et versionnable.

Définir le pipeline dans le module permet de le tester, le versionner et le déployer sans notebook. Un fichier my_project/models/pipeline.py exporte une fonction build_pipeline(params: dict) qui retourne un objet sklearn.pipeline.Pipeline. Les hyperparamètres viennent d'un fichier params.yaml lu par DVC ou par un script d'entraînement.

Le notebook d'exploration appelle alors simplement :

from my_project.models.pipeline import build_pipeline

Cette séparation élimine le problème classique des cellules exécutées dans le désordre. Le pipeline est déterministe : mêmes données en entrée, même modèle en sortie.

Un piège fréquent avec les pipelines scikit-learn concerne les transformations personnalisées. Une classe héritant de BaseEstimator et TransformerMixin doit être définie dans le module, pas dans le notebook. Si la classe est définie dans une cellule, la sérialisation avec joblib échoue au moment du chargement dans un autre contexte (script de production, API). Le module garantit que le chemin d'import reste stable entre l'entraînement et l'inférence.

Les ColumnTransformer ajoutent un niveau de complexité supplémentaire. Quand un pipeline applique des transformations différentes aux colonnes numériques et catégorielles, la liste des colonnes doit être définie dans le module (ou lue depuis un fichier de configuration), jamais codée en dur dans le notebook. Une modification de la liste des features dans le notebook sans mise à jour du module provoque un décalage silencieux entre exploration et production.

Pour les projets qui utilisent des transformations lourdes (agrégations temporelles, encodage de variables textuelles avec TF-IDF), encapsuler chaque transformation dans sa propre classe au sein du module facilite le débogage. Un transformateur isolé peut être testé avec un DataFrame minimal, indépendamment du reste du pipeline. Cette granularité rend aussi la documentation du pipeline plus lisible : chaque étape porte un nom explicite qui apparaît dans la représentation textuelle de l'objet Pipeline.

Deux collègues data scientists collaborant autour d'un ordinateur portable affichant la structure de modules Python dans un espace de coworking

Passer du prototype notebook à la mise en production avec FastAPI

Les offres d'emploi récentes en data science mentionnent explicitement la capacité à industrialiser les solutions, du prototype à la mise en production via des API. FastAPI est devenu le standard de fait pour exposer un modèle entraîné via un endpoint HTTP.

Le module Python joue ici un rôle central. Le code de prédiction (chargement du modèle, préprocessing de la requête, inférence) est déjà encapsulé dans le package. L'application FastAPI n'a qu'à importer les fonctions du module :

from my_project.models.pipeline import load_model
from my_project.features.preprocessing import transform_input

Un module bien structuré rend le passage notebook vers API quasi transparent. Aucune réécriture de logique, seulement un point d'entrée HTTP qui appelle les mêmes fonctions que le notebook d'évaluation.

La validation des données entrantes mérite une attention particulière lors de cette transition. Pydantic, intégré nativement à FastAPI, permet de définir des schémas stricts pour les requêtes. En déclarant un modèle Pydantic qui reprend les colonnes attendues par le préprocessing, les erreurs de format sont interceptées avant même d'atteindre le code de prédiction. Ce schéma Pydantic se place dans le module (my_project/api/schemas.py) et sert aussi de documentation automatique via l'interface Swagger générée par FastAPI.

Le déploiement en conteneur Docker bénéficie directement de cette architecture modulaire. Le Dockerfile copie le package, installe les dépendances de production uniquement, et lance le serveur Uvicorn. Le notebook n'est pas embarqué dans l'image, ce qui réduit sa taille et sa surface d'attaque.

Un aspect souvent sous-estimé concerne le rechargement du modèle en production. Charger un fichier joblib à chaque requête dégrade les performances. La pratique recommandée consiste à charger le modèle une seule fois au démarrage de l'application, via un événement lifespan de FastAPI, et à le stocker dans l'état de l'application. Le module fournit alors une fonction load_model(path: str) qui gère le chargement et la vérification de la version du modèle.

La gestion des erreurs en production diffère aussi de celle du notebook. Dans un notebook, une exception affiche un traceback et le data scientist corrige la cellule. En production, l'API doit renvoyer un code HTTP explicite (422 pour une entrée invalide, 500 pour une erreur interne) avec un message structuré. Le module peut fournir des exceptions métier personnalisées (InvalidFeatureError, ModelNotLoadedError) que l'application FastAPI traduit en réponses HTTP via des handlers d'exceptions dédiés.

Tests et qualité de code dans un module data science

Tester du code data science ne se limite pas à vérifier qu'une fonction renvoie le bon type. Les tests les plus utiles portent sur les contrats de données : forme du DataFrame en sortie de préprocessing, plage de valeurs des features après normalisation, cohérence des colonnes entre entraînement et inférence.

Pytest avec des fixtures de données synthétiques couvre la majorité des cas. Chaque fonction du module reçoit un petit DataFrame de test et le résultat est vérifié par assertion. Ce type de test empêche les régressions silencieuses qui passent inaperçues dans un notebook (une cellule modifiée, un résultat différent, personne ne le remarque).

Ajouter un linter (ruff ou flake8) et un formateur (black) dans le pre-commit du dépôt garantit un style homogène. Le code du module reste lisible même quand plusieurs personnes contribuent au même projet d'analyse de données.

Un type de test souvent absent des projets data science concerne la cohérence entre schéma d'entraînement et schéma d'inférence. Une fixture qui compare les colonnes du DataFrame d'entraînement avec celles attendues par le pipeline de prédiction détecte les décalages avant le déploiement. Ce test coûte quelques lignes de code et évite le bug classique où une feature ajoutée en phase d'exploration n'est pas reproduite dans le préprocessing de production.

Les tests de non-régression sur les métriques du modèle complètent le dispositif. Un test qui entraîne le pipeline sur un jeu de données fixe et vérifie que la métrique principale (accuracy, F1, RMSE) reste dans une fourchette définie détecte les modifications involontaires du préprocessing ou des hyperparamètres. Ce type de test s'intègre dans la CI et bloque le merge si la performance chute.

Au-delà des tests unitaires, les tests d'intégration vérifient que les étapes du pipeline s'enchaînent correctement de bout en bout. Un test d'intégration typique prend un petit jeu de données brut, exécute le préprocessing, entraîne le modèle et produit une prédiction. Si l'une des étapes modifie le format de sortie de manière incompatible avec l'étape suivante, ce test échoue immédiatement.

Cette couverture complémentaire est particulièrement utile quand plusieurs personnes travaillent sur des étapes différentes du pipeline en parallèle. Les tests d'intégration servent alors de contrat entre les contributeurs : tant que le test passe, chaque développeur peut modifier sa partie du module sans risque de casser le travail des autres.

Les tests de propriété, moins courants en data science, apportent une couche de robustesse supplémentaire. Avec une bibliothèque comme hypothesis, il est possible de générer automatiquement des DataFrames aléatoires et de vérifier que le préprocessing ne produit jamais de valeurs NaN inattendues ou de colonnes manquantes. Ce type de test révèle des cas limites que les fixtures manuelles ne couvrent pas, comme des colonnes entièrement vides ou des types de données inattendus.

La frontière entre un projet data science amateur et un projet professionnel tient souvent à cette discipline : le notebook explore, le module exécute, les tests vérifient, le pipeline orchestre. Aucun de ces rôles ne devrait empiéter sur les autres.