Application

Une application fil rouge pour illustrer l’intérêt d’appliquer graduellement les bonnes pratiques dans une optique de mise en production d’une application de data science.

Dérouler les slides ci-dessous ou cliquer ici pour afficher les slides en plein écran.

Introduction

L’objectif de cette mise en application est d’illustrer les différentes étapes qui séparent la phase de développement d’un projet de celle de la mise en production. Elle permettra de mettre en pratique les différents concepts présentés tout au long du cours.

L’objectif pédagogique principal de cette application est d’adopter un point de vue pragmatique en choisissant des outils et des méthodes de travail qui permettent de réaliser des objectifs ambitieux de valorisation de données. Python sera le trait d’union entre les différentes technologies ou infrastructures que nous utiliserons.

Cette application est un tutoriel pas à pas pour avoir un projet reproductible et disponible sous plusieurs livrables. Toutes les étapes ne sont pas indispensables à tous les projets de data science et il existe des outils alternatifs à ceux présentés. Néanmoins, les outils présentés ont l’avantage d’être très bien intégrés à Python, bien configurés si vous utilisez le SSPCloud comme nous le recommandons, tout en étant agnostiques sur le reste des outils que vous utilisez ; de sorte à ne pas être bloquants si on remplace l’une des briques logicielles par une autre.

Nous nous plaçons dans une situation initiale correspondant à la fin de la phase de développement d’un projet de data science. On a un notebook un peu monolithique, qui réalise les étapes classiques d’un pipeline de machine learning :

  • Import de données ;
  • Statistiques descriptives et visualisations ;
  • Feature engineering ;
  • Entraînement d’un modèle ;
  • Evaluation du modèle.

Objectif

L’objectif est d’améliorer le projet de manière incrémentale jusqu’à pouvoir le mettre en production, en le valorisant sous une forme adaptée et en adoptant une méthode de travail fluidifiant les évolutions futures.

La Figure 1 montre que notre point de départ initial, à savoir un notebook, mélange tout. Ceci rend très complexe la mise à jour de notre modèle ou l’exploitation de notre modèle sur de nouvelles données, ce qui est pourtant la raison d’être du machine learning qui est pensé pour l’extrapolation. Si on vous demande de valoriser votre modèle sur de nouvelles données, vous risquez de devoir refaire tourner tout votre notebook, avec le risque de ne pas retrouver les mêmes résultats que dans la version précédente.

La Figure 2 illustre l’horizon auquel nous aboutirons à la fin de cette application. Nous désynchronisons les étapes d’entraînement et de prédiction, en identifiant mieux les pré-requis de chacunes et en adoptant des briques technologiques adaptées à celles-ci. Les noms présents sur cette figure sont encore obscurs, c’est normal, mais ils vous deviendrons familiers si vous adoptez une infrastructure et une méthode de travail à l’état de l’art.

Figure 1: Illustration de notre point de départ
Figure 2: Illustration de l’horizon vers lequel on se dirige
Important

Il est important de bien lire les consignes et d’y aller progressivement. Certaines étapes peuvent être rapides, d’autres plus fastidieuses ; certaines être assez guidées, d’autres vous laisser plus de liberté. Si vous n’effectuez pas une étape, vous risquez de ne pas pouvoir passer à l’étape suivante qui en dépend.

Bien que l’exercice soit applicable sur toute configuration bien faite, nous recommandons de privilégier l’utilisation du SSP Cloud, où tous les outils nécessaires sont pré-installés et pré-configurés. Le service VSCode ne sera en effet que le point d’entrée pour l’utilisation d’outils plus exigeants sur le plan de l’infrastructure: Argo, MLFLow, etc.

Ce que cette application ne couvre pas (pour le moment)

A l’heure actuelle, cette application se concentre sur la mise en oeuvre fiable de l’entraînement de modèles de machine learning. Comme vous pouvez le voir, quand on part d’aussi loin qu’un projet monolithique dans un notebook, c’est un travail conséquent d’en arriver à un pipeline pensé pour la production. Cette application vise à vous sensibiliser au fait qu’avoir la Figure 2 en tête et adopter une organisation de travail et faire des choix techniques adéquats, vous fera économiser des dizaines voire centaines d’heures lorsque votre modèle aura vocation à passer en production.

A l’heure actuelle, cette application ne se concentre que sur une partie du cycle de vie d’un projet data ; il y a déjà fort à faire. Nous nous concentrons sur l’entraînement et la mise à disposition d’un modèle à des fins opérationnelles. C’est la première partie du cycle de vie d’un modèle. Dans une approche MLOps, il faut également penser la maintenance de ce modèle et les enjeux que représentent l’arrivée continue de nouvelles données, ou le besoin d’en collecter de nouvelles à travers des annotations, sur la qualité prédictive d’un modèle. Toute entreprise qui ne pense pas cet après est vouée à se faire doubler par un nouveau venu. Une prochaine version de cette application permettra certainement d’illustrer certains des enjeux afférants à la vie en production d’un modèle (supervision, annotations…) sur notre cas d’usage.

Il convient aussi de noter que nous ne faisons que parcourir la surface des sujets que nous évoquons. Ce cours, déjà dense, deviendrait indigeste si nous devions présenter chaque outil dans le détail. Nous laissons donc les curieux approfondir chacun des outils que nous présentons pour découvrir comment en tirer le maximum (et si vous avez l’impression que nous oublions des éléments cruciaux, les issues et pull requests sont bienvenues).

Comment gérer les checkpoints ?

Pour simplifier la reprise en cours de ce fil rouge, nous proposons un système de checkpoints qui s’appuient sur des tags Git. Ces tags figent le projet tel qu’il est à l’issue d’un exercice donné.

Si vous faites évoluer votre projet de manière expérimentale mais désirez tout de même utiliser à un moment ces checkpoints, il va falloir faire quelques acrobaties Git. Pour cela, nous mettons à disposition un script qui permet de sauvegarder votre avancée dans un tag donné (au cas où, à un moment, vous vouliez revenir dessus) et écraser la branche main avec le tag en question. Par exemple, si vous désirez reprendre après l’exercice 9, vous devrez faire tourner le code dans cette boite :

Checkpoint d'exemple
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli92
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli9
3
Nettoyer derrière nous

Celui-ci sauvegarde votre avancée dans un tag nommé dev_before_appli9, le pousse sur votre dépôt Github puis force votre branche à adopter l’état du tag appli9.

Partie 0 : initialisation du projet

Nous allons prendre comme point de départ un projet livré exclusivement avec un notebook, à la manière d’un challenge Kaggle. Vous pourrez ainsi voir à quel point ce type de livrable est très loin d’être satisfaisant si on veut que le projet soit réutilisable.

Application préliminaire 1: forker le dépôt d’exemple

Les premières étapes consistent à mettre en place son environnement de travail sur Github:

  • Générer un jeton d’accès (token) sur GitHub afin de permettre l’authentification en ligne de commande à votre compte. La procédure est décrite ici. Vous ne voyez ce jeton qu’une fois, ne fermez pas la page de suite.

  • Mettez de côté ce jeton en l’enregistrant dans un gestionnaire de mot de passe ou dans l’espace “Mon compte” du SSP Cloud.

  • Forker le dépôt Github : https://github.com/ensae-reproductibilite/application en faisant attention à une option :

    • Décocher la case “Copy the main branch only” afin de copier également les tags Git qui nous permettront de faire les checkpoint.
Ce que vous devriez voir sur la page de création du fork.

Nous recommandons d’utiliser, tout au long de ce projet, l’environnement de développement VSCode. En plus d’être très bien construit, les nombreuses extensions disponibles rendent celui-ci adaptable à tous nos besoins. Comme il s’agit de l’outil sur lequel vous passerez votre quotidien, n’hésitez pas à personnaliser celui-ci grâce aux nombreuses ressources disponibles en ligne1.

Application préliminaire 2: mettre en place son environnement de travail

Il est maintenant possible de ce lancer dans la création de l’environnement de travail:

  • Ouvrir un service VSCode sur le SSP Cloud. Vous pouvez aller dans la page My Services et cliquer sur New service. Sinon, vous pouvez initialiser la création du service en cliquant directement ici. Modifier les options suivantes:
  • Clôner votre dépôt Github en utilisant le terminal depuis Visual Studio (Terminal > New Terminal) et en passant directement le token dans l’URL selon cette structure:
terminal
$ git clone https://$TOKEN@github.com/$USERNAME/application.git

$TOKEN et $USERNAME sont à remplacer, respectivement, par le jeton que vous avez généré précédemment et votre nom d’utilisateur.

Le script d’initialisation proposé

Ce script initialise quelques extensions intéressantes pour le développement de projets utilisant Python ou des fichiers textes type Markdown ou YAML: diagnostic, mise en forme automatisée, etc.

#!/bin/bash

# Define the configuration directory for VS Code
VSCODE_CONFIG_DIR="$HOME/.local/share/code-server/User"

# Create the configuration directory if necessary
mkdir -p "$VSCODE_CONFIG_DIR"

# User settings file
SETTINGS_FILE="$VSCODE_CONFIG_DIR/settings.json"


code-server --install-extension yzhang.markdown-all-in-one
code-server --install-extension oderwat.indent-rainbow
code-server --install-extension tamasfe.even-better-toml
code-server --install-extension aaron-bond.better-comments
code-server --install-extension github.vscode-github-actions


# Replace default flake8 linter with project-preconfigured ruff
code-server --uninstall-extension ms-python.flake8
code-server --install-extension charliermarsh.ruff


jq '. + {
    "workbench.colorTheme": "Default Dark Modern",  # Set the theme

    "editor.rulers": [80, 100, 120],  # Add specific vertical rulers
    "files.trimTrailingWhitespace": true,  # Automatically trim trailing whitespace
    "files.insertFinalNewline": true,  # Ensure files end with a newline

    "flake8.args": [
        "--max-line-length=100"  # Max line length for Python linting
    ]



}' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"

Si, dans quelques jours, vous désirez relancer un service avec cette configuration, vous pouvez cliquer sur ce lien:

Onyxia

En cliquant sur l’onglet Git, vous pouvez renseigner directement votre URL de la forme https://github.com/username/depot.git: cela clônera votre dépôt dans le service et injectera le token pour vous économiser l’authentification à venir lors de la phase de push.

Partie 1 : qualité du script

Cette première partie vise à rendre le projet conforme aux bonnes pratiques présentées dans le cours.

Elle fait intervenir les notions suivantes :

Le plan de la partie est le suivant :

  1. S’assurer que le script fonctionne ;
  2. Nettoyer le code des scories formelles avec un linter et un formatter ;
  3. Paramétrisation du script ;
  4. Utilisation de fonctions.

Étape 1 : s’assurer que le script s’exécute correctement

On va partir du fichier notebook.py qui reprend le contenu du notebook2 mais dans un script classique. Le travail de nettoyage en sera facilité.

La première étape est simple, mais souvent oubliée : vérifier que le code fonctionne correctement. Pour cela, nous recommandons de faire un aller-retour entre le script ouvert dans VSCode et un terminal pour le lancer.

Application 1: corriger les erreurs
  • Ouvrir dans VSCode le script titanic.py ;
  • Exécuter le script en ligne de commande (python titanic.py)3 pour détecter les erreurs ;
  • Corriger les deux erreurs qui empêchent la bonne exécution ;
  • Vérifier le fonctionnement du script en utilisant la ligne de commande:
terminal
$ python titanic.py

Le code devrait afficher des sorties.

Aide sur les erreurs rencontrées

La première erreur rencontrée est une alerte FileNotFoundError, la seconde est liée à un package.

Il est maintenant temps de commit les changements effectués avec Git4 :

terminal
$ git add titanic.py
$ git commit -m "Corrige l'erreur qui empêchait l'exécution"
$ git push
Checkpoint post appli1
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli12
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli1
3
Nettoyer derrière nous

Étape 2: utiliser un linter puis un formatter

On va maintenant améliorer la qualité de notre code en appliquant les standards communautaires. Pour cela, on va utiliser le linter classique PyLint et le formatter Black. Si vous désirez un outil deux en un, il est possible d’utiliser Ruff en complément ou substitut.

Ce nettoyage automatique du code permettra, au passage, de restructurer notre script de manière plus naturelle.

Important

PyLint, Black et Ruff sont des packages Python qui s’utilisent principalement en ligne de commande.

Si vous avez une erreur qui suggère que votre terminal ne connait pas PyLint, Black, ou Ruff, n’oubliez pas d’exécuter la commande pip install pylint, pip install black ou pip install ruff.

Le linter PyLint renvoie alors une série d’irrégularités, en précisant à chaque fois la ligne de l’erreur et le message d’erreur associé (ex : mauvaise identation). Il renvoie finalement une note sur 10, qui estime la qualité du code à l’aune des standards communautaires évoqués dans la partie Qualité du code.

Application 2: rendre lisible le script
  • Diagnostiquer et évaluer la qualité de titanic.py avec PyLint. Regarder la note obtenue.
  • Utiliser black titanic.py --diff --color pour observer les changements de forme que va induire l’utilisation du formatter Black. Cette étape n’applique pas les modifications, elle ne fait que vous les montrer.
  • Appliquer le formatter Black
  • Réutiliser PyLint pour diagnostiquer l’amélioration de la qualité du script et le travail qui reste à faire.
  • Comme la majorité du travail restant est à consacrer aux imports:
    • Mettre tous les imports ensemble en début de script
    • Retirer les imports redondants en s’aidant des diagnostics de votre éditeur
    • Réordonner les imports si PyLint vous indique de le faire
    • Corriger les dernières fautes formelles suggérées par PyLint
  • Délimiter des parties dans votre code pour rendre sa structure plus lisible. Si des parties vous semblent être dans le désordre, vous pouvez réordonner le script (mais n’oubliez pas de le tester)
Checkpoint post appli2
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli22
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli2
3
Nettoyer derrière nous

Le code est maintenant lisible, il obtient à ce stade une note formelle proche de 10. Mais il n’est pas encore totalement intelligible ou fiable. Il y a notamment quelques redondances de code auxquelles nous allons nous attaquer par la suite. Néanmoins, avant cela, occupons-nous de mieux gérer certains paramètres du script: jetons d’API et chemin des fichiers.

Étape 3: gestion des paramètres

Reprendre à partir d'ici
Si vous n'avez plus de VSCode actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:
Onyxia

Et ensuite, après avoir clôné le dépôt

terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli22
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli2
3
Nettoyer derrière nous

L’exécution du code et les résultats obtenus dépendent de certains paramètres définis dans le code. L’étude de résultats alternatifs, en jouant sur des variantes des (hyper)paramètres, est à ce stade compliquée car il est nécessaire de parcourir le code pour trouver ces paramètres. De plus, certains paramètres personnels comme des jetons d’API ou des mots de passe n’ont pas vocation à être présents dans le code.

Il est plus judicieux de considérer ces paramètres comme des variables d’entrée du script. Cela peut être fait de deux manières:

  1. Avec des arguments optionnels appelés depuis la ligne de commande (Application 3a). Cela peut être pratique pour mettre en oeuvre des tests automatisés mais n’est pas forcément pertinent pour toutes les variables. Nous allons montrer cet usage avec le nombre d’arbres de notre random forest ;
  2. En utilisant un fichier de configuration dont les valeurs sont importées dans le script principal (Application 3b).
Un exemple de définition d’un argument pour l’utilisation en ligne de commande
prenom.py
import argparse
parser = argparse.ArgumentParser(description="Qui êtes-vous?")
parser.add_argument(
    "--prenom", type=str, default="Toto", help="Un prénom à afficher"
)
args = parser.parse_args()
print(args.prenom)

Exemples d’utilisations en ligne de commande

terminal
$ python prenom.py
$ python prenom.py --prenom "Zinedine"
Application 3a: Paramétrisation du script
  1. En s’inspirant de l’exemple ci-dessus 👆️, créer une variable n_trees qui peut éventuellement être paramétrée en ligne de commande et dont la valeur par défaut est 20 ;
  2. Tester cette paramétrisation en ligne de commande avec la valeur par défaut puis 2, 10 et 50 arbres.

L’exercice suivant permet de mettre en application le fait de paramétriser un script en utilisant des variables définies dans un fichier YAML.

Application 3b: La configuration dans un fichier dédié
  1. Installer le package python-dotenv que nous allons utiliser pour charger notre jeton d’API à partir d’une variable d’environnement.
  2. A partir de l’exemple de la documentation, utiliser la fonction load_dotenv pour charger dans Python nos variables d’environnement à partir d’un fichier (vous pouvez le créer mais ne pas le remplir encore avec les valeurs voulues, ce sera fait ensuite)
  3. Créer la variable et vérifier la sortie de Python en faisant tourner titanic.py en ligne de commande
titanic.py
jeton_api = os.environ.get("JETON_API", "")

if jeton_api.startswith("$"):
    print("API token has been configured properly")
else:
    print("API token has not been configured")
  1. Maintenant introduire la valeur voulue pour le jeton d’API dans le fichier d’environnement lu par dotenv
  2. S’il n’existe pas déjà, créer un fichier .gitignore (cf. Chapitre Git). Ajouter dans ce fichier .env car il ne faut pas committer ce fichier. Au passage ajouter __pycache__/ au .gitignore5, cela évitera d’avoir à le faire ultérieurement ;
  3. Créer un fichier README.md où vous indiquez qu’il faut créer un fichier .env pour pouvoir utiliser l’API.
Checkpoint post appli3
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli32
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli3
3
Nettoyer derrière nous

Étape 4 : Privilégier la programmation fonctionnelle

Nous allons mettre en fonctions les parties importantes de l’analyse. Ceci facilitera l’étape ultérieure de modularisation de notre projet. Comme cela est évoqué dans les éléments magistraux de ce cours, l’utilisation de fonctions va rendre notre code plus concis, plus traçable, mieux documenté.

Cet exercice étant chronophage, il n’est pas obligatoire de le réaliser en entier. L’important est de comprendre la démarche et d’adopter fréquemment une approche fonctionnelle6. Pour obtenir une chaine entièrement fonctionnalisée, vous pouvez reprendre le checkpoint.

Pour commencer, cet exercice fait un petit pas de côté pour faire comprendre la manière dont les pipelines scikit sont un outil au service des bonnes pratiques.

Application 4 (optionnelle): pourquoi utiliser un pipeline Scikit ?
  • Le pipeline Scikit d’estimation et d’évaluation vous a été donné tel quel. Regardez, ci-dessous, le code équivalent sans utiliser de pipeline Scikit:

Le code équivalent sans pipeline

from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix

import pandas as pd
import numpy as np

# Définition des variables
numeric_features = ["Age", "Fare"]
categorical_features = ["Embarked", "Sex"]

# PREPROCESSING ----------------------------

# Handling missing values for numerical features
num_imputer = SimpleImputer(strategy="median")
X_train[numeric_features] = num_imputer.fit_transform(X_train[numeric_features])
X_test[numeric_features] = num_imputer.transform(X_test[numeric_features])

# Scaling numerical features
scaler = MinMaxScaler()
X_train[numeric_features] = scaler.fit_transform(X_train[numeric_features])
X_test[numeric_features] = scaler.transform(X_test[numeric_features])

# Handling missing values for categorical features
cat_imputer = SimpleImputer(strategy="most_frequent")
X_train[categorical_features] = cat_imputer.fit_transform(X_train[categorical_features])
X_test[categorical_features] = cat_imputer.transform(X_test[categorical_features])

# One-hot encoding categorical features
encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
X_train_encoded = encoder.fit_transform(X_train[categorical_features])
X_test_encoded = encoder.transform(X_test[categorical_features])

# Convert encoded features into a DataFrame
X_train_encoded = pd.DataFrame(X_train_encoded, columns=encoder.get_feature_names_out(categorical_features), index=X_train.index)
X_test_encoded = pd.DataFrame(X_test_encoded, columns=encoder.get_feature_names_out(categorical_features), index=X_test.index)

# Drop original categorical columns and concatenate encoded ones
X_train = X_train.drop(columns=categorical_features).join(X_train_encoded)
X_test = X_test.drop(columns=categorical_features).join(X_test_encoded)

# MODEL TRAINING ----------------------------

# Defining the model
model = RandomForestClassifier(n_estimators=n_trees)

# Fitting the model
model.fit(X_train, y_train)

# EVALUATION ----------------------------

# Scoring
rdmf_score = model.score(X_test, y_test)
print(f"{rdmf_score:.1%} de bonnes réponses sur les données de test pour validation")

# Confusion matrix
print(20 * "-")
print("matrice de confusion")
print(confusion_matrix(y_test, model.predict(X_test)))
  • Voyez-vous l’intérêt de l’approche par pipeline en termes de lisibilité, évolutivité et fiabilité ?

  • Créer un notebook qui servira de brouillon. Y introduire le code suivant:

Le code à copier-coller dans un notebook

import pandas as pd
from sklearn.preprocessing import OneHotEncoder, MinMaxScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer

train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
X_train, y_train = train.drop("Survived", axis="columns"), train["Survived"]
X_test, y_test = test.drop("Survived", axis="columns"), train["Survived"]

MAX_DEPTH = None
MAX_FEATURES = "sqrt"
n_trees=20

numeric_features = ["Age", "Fare"]
categorical_features = ["Embarked", "Sex"]

# Variables numériques
numeric_transformer = Pipeline(
    steps=[
        ("imputer", SimpleImputer(strategy="median")),
        ("scaler", MinMaxScaler()),
    ]
)

# Variables catégorielles
categorical_transformer = Pipeline(
    steps=[
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("onehot", OneHotEncoder()),
    ]
)

# Preprocessing
preprocessor = ColumnTransformer(
    transformers=[
        ("Preprocessing numerical", numeric_transformer, numeric_features),
        (
            "Preprocessing categorical",
            categorical_transformer,
            categorical_features,
        ),
    ]
)

# Pipeline
pipe = Pipeline(
    [
        ("preprocessor", preprocessor),
        ("classifier", RandomForestClassifier(
            n_estimators=n_trees,
            max_depth=MAX_DEPTH,
            max_features=MAX_FEATURES
        )),
    ]
)

pipe.fit(X_train, y_train)
  • Afficher ce pipeline dans une cellule de votre notebook. Cela vous aide-t-il mieux à comprendre les différentes étapes du pipeline de modélisation ?

  • Comment pouvez-vous accéder aux étapes de preprocessing ?

  • Comment pouvez-vous faire pour appliquer le pipeline de preprocessing des variables numériques (et uniquement celui-ci) à ce DataFrame ?

Le DataFrame à créer pour appliquer un bout de notre pipeline

import numpy as np

new_data = {
    "Age": [22, np.nan, 35, 28, np.nan],
    "Fare": [7.25, 8.05, np.nan, 13.00, 15.50]
}

new_data = pd.DataFrame(new_data)
  • Normalement ce code ne devrait pas prendre plus d’une demie-douzaine de lignes. Sans pipeline le code équivalent, beaucoup plus verbeux et moins fiable, ressemble à celui-ci
Le code équivalent, sans pipeline
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler

# Définition des nouvelles données
new_data = pd.DataFrame({
    "Age": [25, np.nan, 40, 33, np.nan],
    "Fare": [10.50, 7.85, np.nan, 22.00, 12.75]
})

# Définition des transformations (même que dans le pipeline)
num_imputer = SimpleImputer(strategy="median")
scaler = MinMaxScaler()

# Apprentissage des transformations sur X_train (assumant que vous l'avez déjà)
X_train_numeric = X_train[["Age", "Fare"]]  # Supposons que X_train existe
num_imputer.fit(X_train_numeric)
scaler.fit(num_imputer.transform(X_train_numeric))

# Transformation des nouvelles données
new_data_imputed = num_imputer.transform(new_data)
new_data_scaled = scaler.transform(new_data_imputed)

# Création du DataFrame final
new_data_preprocessed = pd.DataFrame(
    new_data_scaled,
    columns=["Age_scaled", "Fare_scaled"]  # Générer des noms de colonnes adaptés
)

# Affichage du DataFrame
print(new_data_preprocessed)
  • Imaginons que vous ayez déjà des données préprocessées:

Créer des données préprocessées

import numpy as np
import pandas as pd

new_data = pd.DataFrame({
    "Age": [25, np.nan, 40, 33, np.nan],
    "Fare": [10.50, 7.85, np.nan, 22.00, 12.75],
    "Embarked": ["S", "C", np.nan, "Q", "S"],
    "Sex": ["male", "female", "male", np.nan, "female"]
})
new_y = np.random.randint(0, 2, size=len(new_data))

preprocessed_data = pd.DataFrame(
    pipe[:-1].transform(new_data),
    columns = preprocessor_numeric.get_feature_names_out()
)
preprocessed_data
  • Déterminer le score en prédiction sur ces données

Maintenant, revenons à notre chaine de production et appliquons des fonctions pour la rendre plus lisible, plus fiable et plus modulaire.

Application 4: adoption des standards de programmation fonctionnelle

Cette application peut être chronophage, vous pouvez aller plus ou moins loin dans la fonctionalisation de votre script en fonction du temps dont vous disposez.

  • Créer une fonction générique pour réduire la redondance de code dans l’étape d’exploration des données où on utilise split ;
  • Créer une fonction qui réalise le split train/test en fonction d’un paramètre représentant la proportion de l’échantillon de test et d’arguments optionnels sur les chemins d’écriture des deux échantillons en csv.
  • Créer une fonction qui intègre les différentes étapes du pipeline (preprocessing et définition du modèle). Cette fonction prend en paramètre le nombre d’arbres (argument obligatoire) et des arguments optionnels supplémentaires (les colonnes sur lesquelles s’appliquent les différentes étapes du pipeline, max_depth et max_features).
  • Créer une fonction d’évaluation renvoyant le score obtenu et la matrice de confusion, à l’issue d’une estimation (mais cette estimation est faite en amont de la fonction, pas au sein de celle-ci)
  • Déplacer toutes les fonctions ensemble, en début de script. Si besoin, ajouter des paramètres à votre fichier d’environnement pour créer de nouvelles variables comme les chemins des données.
Important

Le fait d’appliquer des fonctions a déjà amélioré la fiabilité du processus en réduisant le nombre d’erreurs de copier-coller. Néanmoins, pour vraiment fiabiliser le processus, il faudrait utiliser un pipeline de transformations de données.

Ceci n’est pas encore au programme du cours mais le sera dans une prochaine version.

Checkpoint post appli4
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli42
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli4
3
Nettoyer derrière nous

Cela ne se remarque pas encore vraiment car nous avons de nombreuses définitions de fonctions mais notre chaine de production est beaucoup plus concise (le script fait environ 300 lignes dont 250 de définitions de fonctions génériques). Cette auto-discipline facilitera grandement les étapes ultérieures. Cela aurait été néanmoins beaucoup moins coûteux en temps d’adopter ces bons gestes de manière plus précoce.

Partie 2 : adoption d’une structure modulaire

Dans la partie précédente, on a appliqué de manière incrémentale de nombreuses bonnes pratiques vues tout au long du cours. Ce faisant, on s’est déjà considérablement rapprochés d’un possible partage du code : celui-ci est lisible et intelligible. Le code est proprement versionné sur un dépôt GitHub.

Cependant, le projet est encore perfectible: il est encore difficile de rentrer dedans si on ne sait pas exactement ce qu’on recherche. L’objectif de cette partie est d’isoler les différentes étapes de notre pipeline. Outre le gain de clarté pour notre projet, nous économiserons beaucoup de peines pour la mise en production ultérieure de notre modèle.

Etat du pipeline avant la modularisation

Dans cette partie nous allons continuer les améliorations incrémentales de notre projet avec les étapes suivantes:

  1. Modularisation du code Python pour séparer les différentes étapes de notre pipeline ;
  2. Adopter une structure standardisée pour notre projet afin d’autodocumenter l’organisation de celui-ci ;
  3. Documenter les packages indispensables à l’exécution du code ;
  4. Stocker les données dans un environnement adéquat afin de continuer la démarche de séparer conceptuellement les données du code en de la configuration.

Étape 1 : modularisation

Nous allons profiter de la modularisation pour adopter une structure applicative pour notre code. Celui-ci n’étant en effet plus lancé que depuis la ligne de commande, on peut considérer qu’on construit une application générique où un script principal (main.py) encapsule des éléments issus d’autres scripts Python.

Application 5: modularisation
  • Déplacer les fonctions dans une série de fichiers dédiés:
    • import_data.py: fonctions d’import et d’exploration de données
    • build_features.py: fonctions regroupant la définition des échantillons d’apprentissage et de test ainsi que le pipeline
    • train_evaluate.py: fonctions d’évaluation du modèle
  • Spécifier les dépendances (i.e. les packages à importer) dans les modules pour que ceux-ci puissent s’exécuter indépendamment ;
  • Renommer titanic.py en main.py pour suivre la convention de nommage des projets Python ;
  • Importer les fonctions nécessaires à partir des modules.
  • Vérifier que tout fonctionne bien en exécutant le script main à partir de la ligne de commande :
terminal
$ python main.py
Checkpoint post appli5
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli52
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli5
3
Nettoyer derrière nous

Étape 2 : adopter une architecture standardisée de projet

On dispose maintenant d’une application Python fonctionnelle. Néanmoins, le projet est certes plus fiable mais sa structuration laisse à désirer et il serait difficile de rentrer à nouveau dans le projet dans quelques temps.

Etat actuel du projet 🙈
├── .gitignore
├── data.csv
├── train.csv
├── test.csv
├── README.md
├── config.yaml
├── import_data.py
├── build_features.py
├── train_evaluate.py
├── titanic.ipynb
└── main.py

Comme cela est expliqué dans la partie Structure des projets, on va adopter une structure certes arbitraire mais qui va faciliter l’autodocumentation de notre projet. De plus, une telle structure va faciliter des évolutions optionnelles comme la packagisation du projet. Passer d’une structure modulaire bien faite à un package est quasi-immédiat en Python.

On va donc modifier l’architecture de notre projet pour la rendre plus standardisée. Pour cela, on va s’inspirer des structures cookiecutter qui génèrent des templates de projet. En l’occurrence notre source d’inspiration sera le template datascience issu d’un effort communautaire.

Note

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. La syntaxe à utiliser dans ce cas est la suivante :

terminal
$ pip install cookiecutter
$ cookiecutter https://github.com/drivendata/cookiecutter-data-science

Ici, on a déjà un projet, on va donc faire les choses dans l’autre sens : on va s’inspirer de la structure proposée afin de réorganiser celle de notre projet selon les standards communautaires.

En s’inspirant du cookiecutter data science on va adopter la structure suivante:

Structure recommandée
application
├── main.py
├── README.md
├── data
│   ├── raw
│   │   └── data.csv
│   └── derived
│       ├── test.csv
│       └── train.csv
├── configuration
│   └── config.yaml
├── notebooks
│   └── titanic.ipynb
└── src
    ├── data
    │   └── import_data.py
    ├── pipeline
    │   └── build_pipeline.py
    └── models
        └── train_evaluate.py
Application 6: adopter une structure lisible
  • (optionnel) Analyser et comprendre la structure de projet proposée par le template ;
  • Modifier l’arborescence du projet selon le modèle ;
  • Mettre à jour l’import des dépendances, le fichier de configuration et main.py avec les nouveaux chemins ;
Checkpoint post appli6
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli62
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli6
3
Nettoyer derrière nous

Étape 3: mieux tracer notre chaine de production

Indiquer l’environnement minimal de reproductibilité

Le script main.py nécessite un certain nombre de packages pour être fonctionnel. Chez vous les packages nécessaires sont bien sûr installés mais êtes-vous assuré que c’est le cas chez la personne qui testera votre code ?

Afin de favoriser la portabilité du projet, il est d’usage de “fixer l’environnement”, c’est-à-dire d’indiquer dans un fichier toutes les dépendances utilisées ainsi que leurs version. Nous proposons de créer un fichier requirements.txt minimal, sur lequel nous reviendrons dans la partie consacrée aux environnements reproductibles.

Le fichier requirements.txt est conventionnellement localisé à la racine du projet. Ici on ne va pas fixer les versions, on raffinera ce fichier ultérieurement.

Application 7a: création du requirements.txt
  • Créer un fichier requirements.txt avec la liste des packages nécessaires
  • Ajouter une indication dans README.md sur l’installation des packages grâce au fichier requirements.txt

Tracer notre chaîne

Quand votre projet passera en production, vous aurez un accès limité à celui-ci. Il est donc important de faire remonter, par le biais du logging des informations critiques sur votre projet qui vous permettront de savoir où il en est (si vous avez accès à la console où il tourne) ou là où il s’est arrêté.

L’utilisation de print montre rapidement ses limites pour cela. Les informations enregistrées ne persistent pas après la session et sont quelques peu rudimentaires.

Pour faire du logging, la librairie consacrée depuis longtemps en Python est… logging. On va néanmoins ici proposer d’utiliser loguru qui est un peu plus simple à configurer (l’instanciation du logger est plus aisée) et plus agréable grâce à ses messages en couleurs qui permettent de visuellement trier les informations.

Application 7b: remontée de messages par logging
  1. Installer loguru et l’ajouter au requirements.txt
  2. En s’aidant du README du projet sur Github, remplacer nos print par différents types de messages (info, success, etc.).
  3. Tester l’exécution du script en ligne de commande et observer vos sorties
  4. Mettre à jour le logger pour enregistrer dans un fichier de log. Ajouter celui-ci au .gitignore puis tester en ligne de commande votre script. Ouvrir le fichier en question, refaites tourner le script et regardez son évolutoin.
  5. Il est possible avec loguru de capturer les erreurs des fonctions grâce au système de cache décrit ici. Introduire une erreur dans une des fonctions (par exemple dans split_train_test) avec un code du type raise ValueError("Problème ici")
Checkpoint post appli7
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli72
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli7
3
Nettoyer derrière nous

Étape 4 : stocker les données de manière externe

Pour cette partie, il faut avoir un service VSCode dont les jetons d’authentification à S3 sont valides. Pour cela, si vous êtes sur le SSPCloud, le plus simple est de recréer un nouveau service avec le bouton suivant

Onyxia

et remplir l’onglet Git comme ça votre VSCode sera pré à l’emploi (cf. application 0).

Une fois que vous avez un VSCode fonctionnel, il est possible de reprendre cette application fil rouge depuis le checkpoint précédent.

Reprendre à partir d'ici
Si vous n'avez plus de VSCode actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:
Onyxia

Et ensuite, après avoir clôné le dépôt

terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli72
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli7
3
Nettoyer derrière nous

Enfin, il vous suffira d’ouvrir un terminal et faire pip install -r requirements.txt && python main.py pour pouvoir démarrer l’application.

L’étape précédente nous a permis d’isoler la configuration. Nous avons conceptuellement isolé les données du code lors des applications précédentes. Cependant, nous n’avons pas été au bout du chemin car le stockage des données reste conjoint à celui du code. Nous allons maintenant dissocier ces deux éléments.

Pour mettre en oeuvre cette étape, il peut être utile de comprendre un peu comme fonctionne le SSP Cloud. Vous devrez suivre la documentation du SSP Cloud pour la réaliser. Une aide-mémoire est également disponible dans le cours de 2e année de l’ENSAE Python pour la data science.

L’objectif de cette application est de montrer comment utiliser le format Parquet dans une chaîne production ; un objectif somme toute modeste.

Si vous voulez aller plus loin dans la découverte du format Parquet, vous pouvez consulter cette ressource R très similaire à ce cours (oui elle est faite par les mêmes auteurs…) et essayer de faire les exercices avec votre librairie Python de prédilection (PyArrow ou DuckDB)

Le chapitre sur la structure des projets développe l’idée qu’il est recommandé de converger vers un modèle où environnements d’exécution, de stockage du code et des données sont conceptuellement séparés. Ce haut niveau d’exigence est un gain de temps important lors de la mise en production car au cours de cette dernière, le projet est amené à être exécuté sur une infrastructure informatique dédiée qu’il est bon d’anticiper. Schématiquement, nous visons la structure de projet suivante:

A l’heure actuelle, les données sont stockées dans le dépôt. C’est une mauvaise pratique. En premier lieu, Git n’est techniquement pas bien adapté au stockage de données. Ici ce n’est pas très grave car il ne s’agit pas de données volumineuses et ces dernières ne sont pas modifiées au cours de notre chaine de traitement.

La raison principale est que les données traitées par les data scientists sont généralement soumises à des clauses de confidentialités (RGPD, secret statistique…). Mettre ces données sous contrôle de version c’est prendre le risque de les divulguer à un public non habilité. Il est donc recommandé de privilégier des outils techniques adaptés au stockage de données.

L’idéal, dans notre cas, est d’utiliser une solution de stockage externe. On va utiliser pour cela MinIO, la solution de stockage de type S3 offerte par le SSP Cloud. Cela nous permettra de supprimer les données de Github tout en maintenant la reproductibilité de notre projet 7.

Plus concrètement, nous allons adopter le pipeline suivant pour notre projet:

Le scénario type est que nous avons une source brute, reçue sous forme de CSV, dont on ne peut changer le format. Il aurait été idéal d’avoir un format plus adapté au traitement de données pour ce fichier mais ce n’était pas de notre ressort. Notre chaine va aller chercher ce fichier, travailler dessus jusqu’à valoriser celui-ci sous la forme de notre matrice de confusion. Si on imagine que notre chaine prend un certain temps, il n’est pas inutile d’écrire des données intermédiaires. Pour faire cela, puisque nous avons la main, autant choisir un format adapté, à savoir le format Parquet.

Application 8a: ajout de données sur le système de stockage S3

Pour commencer, à partir de la ligne de commande, utiliser l’utilitaire MinIO pour copier les données data/raw/data.csv vers votre bucket personnel. Les données intermédiaires peuvent être laissées en local mais doivent être ajoutées au .gitignore.

Indice

Structure à adopter:

terminal
$ mc cp data/raw/data.csv s3/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/data.csv
en modifiant $BUCKET_PERSONNEL, l’emplacement de votre bucket personnel

Pour se simplifier la vie, dans les prochaines applications, on va utiliser des URL de téléchargement des fichiers (comme si ceux-ci étaient sur n’importe quel espace de stockage) plutôt que d’utiliser une librairie S3 compatible comme boto3 ou s3fs.

Néanmoins, il est utile de les utiliser une fois pour comprendre la logique. Pour aller plus loin sur ces librairies, vous pouvez consulter cette page du cours de 2A de Python pour la data science.

Pour commencer, on va lister les fichiers se trouvant dans un bucket. En ligne de commande, sur notre poste local, on ferait ls (cf. Linux 101). Cela ne va pas beaucoup différer avec les librairies cloud native:

Dans un notebook, copier-coller ce code, le modifier et exécuter:

import s3fs

fs = s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})

1MY_BUCKET = "mon_nom_utilisateur_sspcloud"
2CHEMIN = "ensae-reproductibilite/data/raw"
fs.ls(f"s3://{MY_BUCKET}/{CHEMIN}")
1
Changer avec le bucket
2
Changer en fonction du chemin voulu

Dans un terminal, copier-coller ligne à ligne ce code, le modifier et exécuter:

import s3fs

1MY_BUCKET="mon_nom_utilisateur_sspcloud"
2CHEMIN = "ensae-reproductibilite/data/raw"
mc ls s3/${MY_BUCKET}/${CHEMIN}
1
Changer avec le bucket
2
Changer en fonction du chemin voulu

On va maintenant lire directement une donnée stockée sur S3. Pour illustrer le fait que cela change peu notre code d’être sur un système cloud avec les librairies adaptées, on va lire directement un fichier CSV stocké sur le SSPCloud, sans passer par un fichier en local.

Application 8b: ajout de données sur le système de stockage S3
1MY_BUCKET = "mon_nom_utilisateur_sspcloud"
2CHEMIN = "ensae-reproductibilite/data/raw"
1
Changer avec le bucket
2
Changer en fonction du chemin voulu
import s3fs
import pandas as pd

fs = s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})

with fs.open(f"s3://{MY_BUCKET}/{CHEMIN}") as f:
    df = pd.read_csv(f)

df
import s3fs
from pyarrow import csv

fs = s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})

with fs.open(f"s3://{MY_BUCKET}/{CHEMIN}") as f:
    df = csv.read_csv(f)

df
import os
import duckdb

con = duckdb.connect(database=":memory:")

con.execute(
    f"""
CREATE SECRET secret (
    TYPE S3,
    KEY_ID '{os.environ["AWS_ACCESS_KEY_ID"]}',
    SECRET '{os.environ["AWS_SECRET_ACCESS_KEY"]}',
    ENDPOINT 'minio.lab.sspcloud.fr',
    SESSION_TOKEN '{os.environ["AWS_SESSION_TOKEN"]}',
    REGION 'us-east-1',
    URL_STYLE 'path',
    SCOPE 's3://{MY_BUCKET}/'
);
"""
)

query_definition = f"SELECT * FROM read_csv('s3://{MY_BUCKET}/{CHEMIN}')"
con.sql(query_definition)
import s3fs

fs = s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})

MY_BUCKET = "mon_nom_utilisateur_sspcloud"
path_data = "data/python-ENSAE/dvf.parquet"
pd.read_csv(f"s3://${MY_BUCKET}/${path_data}", filesystem=fs) #remplacer par read_parquet
Application 8c: privilégier le format Parquet dans notre chaîne

Dans main.py, remplacer le format csv initialement prévu par un format parquet:

data_train_path = os.environ.get("train_path", "data/derived/train.parquet")
data_test_path = os.environ.get("test_path", "data/derived/test.parquet")

Et modifier l’écriture des données pour utiliser to_parquet plutôt que to_csv pour écrire les fichiers intermédiaires:

pd.concat([X_train, y_train], axis = 1).to_parquet(data_train_path)
pd.concat([X_test, y_test], axis = 1).to_parquet(data_test_path)
Application 8d: partage de données sur le système de stockage S3

Par défaut, le contenu de votre bucket est privé, seul vous y avez accès. Pour pouvoir lire votre donnée, vos applications externes devront utiliser des jetons vous identifiant. Ici, comme nous utilisons une donnée publique, vous pouvez rendre accessible celle-ci à tous en lecture. Dans le jargon S3, cela signifie donner un accès anonyme à votre donnée.

Le modèle de commande à utiliser dans le terminal est le suivant:

terminal
$ mc anonymous set download s3/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/

en modifiant $BUCKET_PERSONNEL et le chemin en son sein (/ensae-reproductibilite/data/raw/). Les URL de téléchargement seront de la forme https://minio.lab.sspcloud.fr/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/data.csv

  • Remplacer la définition de data_path pour utiliser, par défaut, directement l’URL dans l’import. Modifier, si cela est pertinent, aussi votre fichier .env.
1URL_RAW = ""
data_path = os.environ.get("data_path", URL_RAW)
1
Modifier avec URL_RAW un lien de la forme "https://minio.lab.sspcloud.fr/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/data.csv"
  • Ajouter le dossier data/ au .gitignore ainsi que les fichiers *.parquet

  • Supprimer le dossier data de votre projet et faites git rm --cached -r data

  • Vérifier le bon fonctionnement de votre application.

Maintenant qu’on a arrangé la structure de notre projet, c’est l’occasion de supprimer le code qui n’est plus nécessaire au bon fonctionnement de notre projet (cela réduit la charge de maintenance8).

Pour vous aider, vous pouvez utiliser vulture de manière itérative pour vous assister dans le nettoyage de votre code.

terminal
$ pip install vulture
$ vulture .
Exemple de sortie
terminal
$ vulture .
src/data/import_data.py:3: unused function 'split_and_count' (60% confidence)
src/pipeline/build_pipeline.py:12: unused function 'split_train_test' (60% confidence)
Checkpoint post appli8
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli82
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli8
3
Nettoyer derrière nous

Partie 2bis: packagisation de son projet (optionnel)

Cette série d’actions n’est pas forcément pertinente pour tous les projets. Elle fait un peu la transition entre la modularité et la portabilité.

Étape 1 : proposer des tests unitaires (optionnel)

Notre code comporte un certain nombre de fonctions génériques. On peut vouloir tester leur usage sur des données standardisées, différentes de celles du Titanic.

Même si la notion de tests unitaires prend plus de sens dans un package, nous pouvons proposer dans le projet des exemples d’utilisation de la fonction, ceci peut être pédagogique.

Nous allons utiliser unittest pour effectuer des tests unitaires. Cette approche nécessite quelques notions de programmation orientée objet ou une bonne discussion avec ChatGPT.

Application 9: test unitaire (optionnel)

Dans le dossier tests/, créer avec l’aide de ChatGPT ou de Copilot un test pour la fonction split_and_count.

  • Effectuer le test unitaire en ligne de commande avec unittest (python -m unittest tests/test_split.py). Corriger le test unitaire en cas d’erreur.
  • Si le temps le permet, proposer des variantes ou d’autres tests.
Checkpoint post appli9
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli92
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli9
3
Nettoyer derrière nous

Note

Lorsqu’on effectue des tests unitaires, on cherche généralement à tester le plus de lignes possibles de son code. On parle de taux de couverture (coverage rate) pour désigner la statistique mesurant cela.

Cela peut s’effectuer de la manière suivante avec le package coverage:

terminal
$ coverage run -m unittest tests/test_create_variable_title.py
$ coverage report -m
Name                                  Stmts   Miss  Cover   Missing
-------------------------------------------------------------------
src/features/build_features.py           34     21    38%   35-36, 48-58, 71-74, 85-89, 99-101, 111-113
tests/test_create_variable_title.py      21      1    95%   54
-------------------------------------------------------------------
TOTAL                                    55     22    60%

Le taux de couverture est souvent mis en avant par les gros projets comme indicateur de leur qualité. Il existe d’ailleurs des badges Github dédiés.

Étape 2 : transformer son projet en package (optionnel)

Notre projet est modulaire, ce qui le rend assez simple à transformer en package, en s’inspirant de la structure du cookiecutter adapté, issu de cet ouvrage.

On va créer un package nommé titanicml qui encapsule tout notre code et qui sera appelé par notre script main.py. La structure attendue est la suivante:

Structure visée
ensae-reproductibilite-application
├── docs                                    ┐
│   ├── main.py                             │
│   └── notebooks                           │ Package documentation and examples
│       └── titanic.ipynb                   │
├── configuration                           ┐ Configuration (pas à partager avec Git)
│   └── config.yaml                         ┘
├── README.md
├── pyproject.toml                          ┐
├── requirements.txt                        │
├── titanicml                               │
│   ├── __init__.py                         │ Package source code, metadata
│   ├── data                                │ and build instructions
│   │   ├── import_data.py                  │
│   │   └── test_create_variable_title.py   │
│   ├── features                            │
│   │   └── build_features.py               │
│   └── models                              │
│       └── train_evaluate.py               ┘
└── tests                                   ┐
    └── test_create_variable_title.py       ┘ Package tests
Rappel: structure actuelle
ensae-reproductibilite-application
├── notebooks
│   └── titanic.ipynb
├── configuration
│   └── config.yaml
├── main.py
├── README.md
├── requirements.txt
└── src
    ├── data
    │   ├── import_data.py
    │   └── test_create_variable_title.py
    ├── features
    │   └── build_features.py
    └── models
        └── train_evaluate.py

Il existe plusieurs frameworks pour construire un package. Nous allons privilégier Poetry à Setuptools.

Note

Pour créer la structure minimale d’un package, le plus simple est d’utiliser le cookiecutter adapté, issu de cet ouvrage.

Comme on a déjà une structure très modulaire, on va plutôt recréer cette structure dans notre projet déjà existant. En fait, il ne manque qu’un fichier essentiel, le principal distinguant un projet classique d’un package : pyproject.toml.

terminal
$ cookiecutter https://github.com/py-pkgs/py-pkgs-cookiecutter.git
Dérouler pour voir les choix possibles
author_name [Monty Python]: Daffy Duck
package_name [mypkg]: titanicml
package_short_description []: Impressive Titanic survival analysis
package_version [0.1.0]:
python_version [3.9]:
Select open_source_license:
1 - MIT
2 - Apache License 2.0
3 - GNU General Public License v3.0
4 - Creative Commons Attribution 4.0
5 - BSD 3-Clause
6 - Proprietary
7 - None
Choose from 1, 2, 3, 4, 5, 6 [1]:
Select include_github_actions:
1 - no
2 - ci
3 - ci+cd
Choose from 1, 2, 3 [1]:
Application 10: packagisation (optionnel)
  • Renommer le dossier titanicml pour respecter la nouvelle arborescence ;
  • Créer un fichier pyproject.toml sur cette base ;
pyproject.toml
[tool.poetry]
name = "titanicml"
version = "0.0.1"
description = "Awesome Machine Learning project"
authors = ["Daffy Duck <daffy.duck@fauxmail.fr>", "Mickey Mouse"]
license = "MIT"
readme = "README.md"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
log_cli = true
log_cli_level = "WARNING"
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
  • Créer le dossier docs et mettre les fichiers indiqués dedans
  • Dans titanicml/, créer un fichier __init__.py9
__init__.py
from .data.import_data import (
    split_and_count
)
from .pipeline.build_pipeline import (
    split_train_test,
    create_pipeline
)
from .models.train_evaluate import (
    evaluate_model
)
__all__ = [
    "split_and_count",
    "split_train_test",
    "create_pipeline",
    "evaluate_model"
]
  • Installer le package en local avec pip install -e .
  • Modifier le contenu de docs/main.py pour importer les fonctions de notre package titanicml et tester en ligne de commande notre fichier main.py
Checkpoint post appli10
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli102
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli10
3
Nettoyer derrière nous

Partie 3 : construction d’un projet portable et reproductible

Reprendre à partir d'ici
Si vous n'avez plus de VSCode actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:
Onyxia

Et ensuite, après avoir clôné le dépôt

terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli82
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli8
3
Nettoyer derrière nous

Dans la partie précédente, on a appliqué de manière incrémentale de nombreuses bonnes pratiques vues dans les chapitres Qualité du code et Structure des projets tout au long du cours.

Ce faisant, on s’est déjà considérablement rapprochés d’une possible mise en production : le code est lisible, la structure du projet est normalisée et évolutive, et le code est proprement versionné sur un dépôt GitHub .

Illustration de l’état actuel du projet

A présent, nous avons une version du projet qui est largement partageable. Du moins en théorie, car la pratique est souvent plus compliquée : il y a fort à parier que si vous essayez d’exécuter votre projet sur un autre environnement (typiquement, votre ordinateur personnel), les choses ne se passent pas du tout comme attendu. Cela signifie qu’en l’état, le projet n’est pas portable : il n’est pas possible, sans modifications coûteuses, de l’exécuter dans un environnement différent de celui dans lequel il a été développé.

Dans cette troisème partie de notre travail vers la mise en production, nous allons voir comment normaliser l’environnement d’exécution afin de produire un projet portable. Autrement dit, nous n’allons plus nous contenter de modularité mais allons rechercher la portabilité. On sera alors tout proche de pouvoir mettre le projet en production.

On progressera dans l’échelle de la reproductibilité de la manière suivante:

  1. Environnements virtuels ;
  2. Créer un script shell qui permet, depuis un environnement minimal, de construire l’application de A à Z ;
  3. Images et conteneurs Docker.

Nous allons repartir de l’application 8, c’est-à-dire d’un projet modulaire mais qui n’est pas, à strictement parler, un package (objet des applications optionnelles suivantes 9 et 10).

Pour se replacer dans l’état du projet à ce niveau, il est possible d’utiliser le tag ad hoc.

terminal
$ git stash
$ git checkout appli8

Étape 1 : un environnement pour rendre le projet portable

Pour qu’un projet soit portable, il doit remplir deux conditions:

  • Ne pas nécessiter de dépendance qui ne soient pas renseignées quelque part ;
  • Ne pas proposer des dépendances inutiles, qui ne sont pas utilisées dans le cadre du projet.

Le prochain exercice vise à mettre ceci en oeuvre. Comme expliqué dans le chapitre portabilité, le choix du gestionnaire d’environnement est laissé libre. Il est recommandé de privilégier venv si vous découvrez la problématique de la portabilité.

L’approche la plus légère est l’environnement virtuel. Nous avons en fait implicitement déjà commencé à aller vers cette direction en créant un fichier requirements.txt.

Application 11a: environnement virtuel venv
  1. Exécuter pip freeze en ligne de commande et observer la (très) longue liste de package
  2. Créer l’environnement virtuel titanic en s’inspirant de la documentation officielle10 ou du chapitre dédié
  3. Utiliser ls pour observer et comprendre le contenu du dossier titanic/bin installé
  4. Activer l’environnement et vérifier l’installation de Python maintenant utilisée par votre machine
  5. Vérifier directement depuis la ligne de commande que Python exécute bien une commande11 avec:
terminal
$ python -c "print('Hello')"
  1. Faire la même chose mais avec import pandas as pd
  2. Installer les packages à partir du requirements.txt. Tester à nouveau import pandas as pd pour comprendre la différence.
  3. Exécuter pip freeze et comprendre la différence avec la situation précédente.
  4. Vérifier que le script main.py fonctionne bien. Sinon ajouter les packages manquants dans le requirements.txt et reprendre de manière itérative à partir de la question 7.
  5. Ajouter le dossier titanic/ au .gitignore pour ne pas ajouter ce dossier à Git.
Aide pour la question 4

Après l’activation, vous pouvez vérifier quel python est utilisé de cette manière

terminal
(titanic) $ which python
Checkpoint post appli11a
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli11a2
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli11a
3
Nettoyer derrière nous

Les environnements conda sont plus lourds à mettre en oeuvre que les environnements virtuels mais peuvent permettre un contrôle plus formel des dépendances.

Application 11b: environnement conda
  1. Exécuter conda env export en ligne de commande et observer la (très) longue liste de package
  2. Créer un environnement titanic avec conda create
  3. Activer l’environnement et vérifier l’installation de Python maintenant utilisée par votre machine
  4. Vérifier directement depuis la ligne de commande que Python exécute bien une commande12 avec:
terminal
$ python -c "print('Hello')"
  1. Faire la même chose mais avec import pandas as pd
  2. Installer les packages qu’on avait listé dans le requirements.txt précédemment. Ne pas faire un pip install -r requirements.txt afin de privilégier conda install
  3. Exécuter à nouveau conda env export et comprendre la différence avec la situation précédente13.
  4. Vérifier que le script main.py fonctionne bien. Sinon installer les packages manquants et reprndre de manière itérative à partir de la question 7.
  5. Quand main.py fonctionne, faire conda env export > environment.yml pour figer l’environnement de travail.
Checkpoint post appli11b
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli11b2
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli11b
3
Nettoyer derrière nous

Étape 2: construire l’environnement de notre application via un script shell

Les environnements virtuels permettent de mieux spécifier les dépendances de notre projet, mais ne permettent pas de garantir une portabilité optimale. Pour cela, il faut recourir à la technologie des conteneurs. L’idée est de construire une machine, en partant d’une base quasi-vierge, qui permette de construire étape par étape l’environnement nécessaire au bon fonctionnement de notre projet. C’est le principe des conteneurs Docker .

Leur méthode de construction étant un peu difficile à prendre en main au début, nous allons passer par une étape intermédiaire afin de bien comprendre le processus de production.

  • Nous allons d’abord créer un script shell, c’est à dire une suite de commandes Linux permettant de construire l’environnement à partir d’une machine vierge ;
  • Nous transformerons celui-ci en Dockerfile dans un deuxième temps. C’est l’objet de l’étape suivante.
Application 12a : créer un fichier d’installation de A à Z
  1. Créer un service ubuntu sur le SSP Cloud
  2. Ouvrir un terminal
  3. Cloner le dépôt
  4. Se placer dans le dossier du projet avec cd
  5. Se placer au niveau du checkpoint 11a avec git checkout appli11a
  6. Via l’explorateur de fichiers, créer le fichier install.sh à la racine du projet avec le contenu suivant:
Script à créer sous le nom install.sh
install.sh
#!/bin/bash
# Install Python
apt-get -y update
apt-get install -y python3-pip python3-venv
# Create empty virtual environment
python3 -m venv titanic
source titanic/bin/activate
# Install project dependencies
pip install -r requirements.txt
  1. Changer les permissions sur le script pour le rendre exécutable
terminal
$ chmod +x install.sh
  1. Exécuter le script depuis la ligne de commande avec des droits de super-utilisateur (nécessaires pour installer des packages via apt)
terminal
$ sudo ./install.sh
  1. Vérifier que le script main.py fonctionne correctement dans l’environnement virtuel créé
terminal
$ source titanic/bin/activate
$ python3 main.py
Checkpoint post appli12a
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli12a2
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli12a
3
Nettoyer derrière nous

Application 12b : créer un fichier d’installation de A à Z
  1. Créer un service ubuntu sur le SSP Cloud
  2. Ouvrir un terminal
  3. Cloner le dépôt
  4. Se placer dans le dossier du projet avec cd
  5. Se placer au niveau du checkpoint 11b avec git checkout appli11b
  6. Via l’explorateur de fichiers, créer le fichier install.sh à la racine du projet avec le contenu suivant:
Script à créer sous le nom install.sh
install.sh
apt-get -y update && apt-get -y install wget
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \
    bash Miniconda3-latest-Linux-x86_64.sh -b -p /miniconda && \
    rm -f Miniconda3-latest-Linux-x86_64.sh
PATH="/miniconda/bin:${PATH}"
# Create environment
conda create -n titanic pandas PyYAML scikit-learn -c conda-forge
conda activate titanic
PATH="/miniconda/envs/titanic/bin:${PATH}"
python main.py
  1. Changer les permissions sur le script pour le rendre exécutable
terminal
$ chmod +x install.sh
  1. Exécuter le script depuis la ligne de commande avec des droits de super-utilisateur (nécessaires pour installer des packages via apt)
terminal
$ sudo ./install.sh
  1. Vérifier que le script main.py fonctionne correctement dans l’environnement virtuel créé
terminal
$ conda activate titanic
$ python3 main.py
Checkpoint post appli12b
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli12b2
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli12b
3
Nettoyer derrière nous

Étape 3: conteneuriser l’application avec Docker

Note

Cette application nécessite l’accès à une version interactive de Docker. Il n’y a pas beaucoup d’instances en ligne disponibles.

Nous proposons deux solutions:

Sinon, elle peut être réalisée en essai-erreur par le biais des services d’intégration continue de Github ou Gitlab . Néanmoins, nous présenterons l’utilisation de ces services plus tard, dans la prochaine partie.

Maintenant qu’on sait que ce script préparatoire fonctionne, on va le transformer en Dockerfile pour anticiper la mise en production. Comme la syntaxe Docker est légèrement différente de la syntaxe Linux classique (voir le chapitre portabilité), il va être nécessaire de changer quelques instructions mais ceci sera très léger.

On va tester le Dockerfile dans un environnement bac à sable pour ensuite pouvoir plus facilement automatiser la construction de l’image Docker.

Application 13: création de l’image Docker

Se placer dans un environnement avec Docker, par exemple Play with Docker

Création du Dockerfile

  • Dans le terminal Linux, cloner votre dépôt Github
  • Repartir de la dernière version à disposition. Par exemple, si vous avez privilégié l’environnement virtuel venv, ce sera:
terminal
1$ git stash
$ git checkout appli12a
1
Pour annuler les modifications depuis le dernier commit
  • Créer via la ligne de commande un fichier texte vierge nommé Dockerfile (la majuscule au début du mot est importante)
Commande pour créer un Dockerfile vierge depuis la ligne de commande
terminal
$ touch Dockerfile
  • Ouvrir ce fichier via un éditeur de texte et copier le contenu suivant dedans:
Premier Dockerfile
terminal
FROM ubuntu:22.04
WORKDIR ${HOME}/titanic
# Install Python
RUN apt-get -y update && \
    apt-get install -y python3-pip
# Install project dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["python3", "main.py"]

Construire (build) l’image

  • Utiliser docker build pour créer une image avec le tag my-python-app
terminal
$ docker build . -t my-python-app
  • Vérifier les images dont vous disposez. Vous devriez avoir un résultat proche de celui-ci :
terminal
$ docker images
REPOSITORY      TAG       IMAGE ID       CREATED              SIZE
my-python-app   latest    188957e16594   About a minute ago   879MB

Tester l’image: découverte du cache

L’étape de build a fonctionné: une image a été construite.

Mais fait-elle effectivement ce que l’on attend d’elle ?

Pour le savoir, il faut passer à l’étape suivante, l’étape de run.

terminal
$ docker run -it my-python-app
python3: can't open file '/~/titanic/main.py': [Errno 2] No such file or directory

Le message d’erreur est clair : Docker ne sait pas où trouver le fichier main.py. D’ailleurs, il ne connait pas non plus les autres fichiers de notre application qui sont nécessaires pour faire tourner le code, par exemple le dossier src.

  • Avant l’étape CMD, copier les fichiers nécessaires sur l’image afin que l’application dispose de tous les éléments nécessaires pour être en mesure de fonctionner.
Nouveau Dockerfile
terminal
FROM ubuntu:22.04
WORKDIR ${HOME}/titanic
# Install Python
RUN apt-get -y update && \
    apt-get install -y python3-pip
# Install project dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py .
COPY src ./src
CMD ["python3", "main.py"]
  • Refaire tourner l’étape de build

  • Refaire tourner l’étape de run. A ce stade, la matrice de confusion doit fonctionner 🎉. Vous avez créé votre première application reproductible !

Note

Ici, le cache permet d’économiser beaucoup de temps. Par besoin de refaire tourner toutes les étapes, Docker agit de manière intelligente en faisant tourner uniquement les étapes qui ont changé.

Checkpoint post appli13
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli132
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli13
3
Nettoyer derrière nous

Partie 4 : automatisation avec l’intégration continue

Imaginez que vous êtes au restaurant et qu’on ne vous serve pas le plat mais seulement la recette et que, de plus, on vous demande de préparer le plat chez vous avec les ingrédients dans votre frigo. Vous seriez quelque peu déçu. En revanche, si vous avez goûté au plat, que vous êtes un réel cordon bleu et qu’on vous donne la recette pour refaire ce plat ultérieurement, peut-être que vous appréciriez plus.

Cette analogie illustre l’enjeu de définir le public cible et ses attentes afin de fournir un livrable adapté. Une image Docker est un livrable qui n’est pas forcément intéressant pour tous les publics. Certains préféreront avoir un plat bien préparé qu’une recette ; certains apprécieront avoir une image Docker mais d’autres ne seront pas en mesure de construire celle-ci ou ne sauront pas la faire fonctionner. Une image Docker est plus souvent un moyen pour faciliter la mise en service d’une production qu’une fin en soi.

Nous allons donc proposer plusieurs types de livrables plus classiques par la suite. Ceux-ci correspondront mieux aux attendus des publics utilisateurs de services construits à partir de techniques de data science. Docker est néanmoins un passage obligé car l’ensemble des types de livrables que nous allons explorer reposent sur la standardisation permise par les conteneurs.

Cette approche nous permettra de quitter le domaine de l’artisanat pour s’approcher d’une industrialisation de la mise à disposition de notre projet. Ceci va notamment nous amener à mettre en oeuvre l’approche pragmatique du DevOps qui consiste à intégrer dès la phase de développement d’un projet les contraintes liées à sa mise à disposition au public cible (cette approche est détaillée plus amplement dans le chapitre sur la mise en production).

L’automatisation et la mise à disposition automatisée de nos productions sera faite progressivement, au cours des prochaines parties. Tous les projets n’ont pas vocation à aller aussi loin dans ce domaine. L’opportunité doit être comparée aux coûts humains et financiers de leur mise en oeuvre et de leur cycle de vie. Avant de faire une production en série de nos modèles, nous allons déjà commencer par automatiser quelques tests de conformité de notre code. On va ici utiliser l’intégration continue pour deux objectifs distincts:

  • la mise à disposition de l’image Docker ;
  • la mise en place de tests automatisés de la qualité du code sur le modèle de notre linter précédent.

Nous allons utiliser Github Actions pour cela. Il s’agit de serveurs standardisés mis à disposition gratuitement par Github . Gitlab , l’autre principal acteur du domaine, propose des services similaires. L’implémentation est légèrement différente mais les principes sont identiques.

Reprendre à partir d'ici
Si vous n'avez plus de VSCode actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:
Onyxia

Et ensuite, après avoir clôné le dépôt

terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli132
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli13
3
Nettoyer derrière nous

Étape 1: mise en place de tests automatisés

Avant d’essayer de mettre en oeuvre la création de notre image Docker de manière automatisée, nous allons présenter la logique de l’intégration continue en testant de manière automatisée notre script main.py.

Pour cela, nous allons partir de la structure proposée dans l’action officielle. La documentation associée est ici. Des éléments succincts de présentation de la logique déclarative des actions Github sont disponibles dans le chapitre sur la mise en production. Néanmoins, la meilleure école pour comprendre le fonctionnement de celles-ci est de parcourir la documentation du service et d’observer les actions Github mises en oeuvre par vos projets favoris, celles-ci seront fort instructives !

Application 14: premier script d’intégration continue

A partir de l’exemple présent dans la documentation officielle de Github , on a déjà une base de départ qui peut être modifiée. Les questions suivantes permettront d’automatiser les tests et le diagnostic qualité de notre code14

  1. Créer un fichier .github/workflows/test.yaml avec le contenu de l’exemple de la documentation
  2. Avec l’aide de la documentation, introduire une étape d’installation des dépendances. Utiliser le fichier requirements.txt pour installer les dépendances.
  3. Utiliser pylint pour vérifier la qualité du code. Ajouter l’argument --fail-under=6 pour renvoyer une erreur en cas de note trop basse15
  4. Utiliser une étape appelant notre application en ligne de commande (python main.py) pour tester que la matrice de confusion s’affiche bien.
  5. Créer un secret stockant une valeur du JETON_API. Ne le faites pas commencer par un “$” comme ça vous pourrez regarder la log ultérieurement
  6. Aller voir votre test automatisé dans l’onglet Actions de votre dépôt sur Github
  7. (optionnel): Créer un artefact à partir du fichier de log que vous créez dans main.py
Checkpoint post appli14
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli142
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli14
3
Nettoyer derrière nous

Maintenant, nous pouvons observer que l’onglet Actions s’est enrichi. Chaque commit va entraîner une série d’actions automatisées.

Si l’une des étapes échoue, ou si la note de notre projet est mauvaise, nous aurons une croix rouge (et nous recevrons un mail). On pourra ainsi détecter, en développant son projet, les moments où on dégrade la qualité du script afin de la rétablir immédiatemment.

Étape 2: Automatisation de la livraison de l’image Docker

Maintenant, nous allons automatiser la mise à disposition de notre image sur DockerHub (le lieu de partage des images Docker). Cela facilitera sa réutilisation mais aussi des valorisations ultérieures.

Là encore, nous allons utiliser une série d’actions pré-configurées.

Pour que Github puisse s’authentifier auprès de DockerHub, il va falloir d’abord interfacer les deux plateformes. Pour cela, nous allons utiliser un jeton (token) DockerHub que nous allons mettre dans un espace sécurisé associé à votre dépôt Github.

Application 15a: configuration
  • Se rendre sur https://hub.docker.com/ et créer un compte. Il est recommandé d’associer ce compte à votre compte Github.
  • Créer un dépôt public application
  • Aller dans les paramètres de votre compte et cliquer, à gauche, sur Security
  • Créer un jeton personnel d’accès, ne fermez pas l’onglet en question, vous ne pouvez voir sa valeur qu’une fois.
  • Dans le dépôt Github de votre projet, cliquer sur l’onglet Settings et cliquer, à gauche, sur Secrets and variables puis dans le menu déroulant en dessous sur Actions. Sur la page qui s’affiche, aller dans la section Repository secrets
  • Créer un jeton DOCKERHUB_TOKEN à partir du jeton que vous aviez créé sur Dockerhub. Valider
  • Créer un deuxième secret nommé DOCKERHUB_USERNAME ayant comme valeur le nom d’utilisateur que vous avez créé sur Dockerhub
Etape optionnelle supplémentaire si on met en production un site web
  • Dans le dépôt Github de votre projet, cliquer sur l’onglet Settings et cliquer, à gauche, sur Actions. Donner les droits d’écriture à vos actions sur le dépôt du projet (ce sera nécessaire pour Github Pages)

A ce stade, nous avons donné les moyens à Github de s’authentifier avec notre identité sur Dockerhub. Il nous reste à mettre en oeuvre l’action en s’inspirant de la documentation officielle. On ne va modifier que trois éléments dans ce fichier. Effectuer les actions suivantes:

Application 15b: automatisation de l’image Docker
  • En s’inspirant de ce template, créer le fichier .github/workflows/prod.yml qui va build et push l’image sur le DockerHub. Il va être nécessaire de changer légèrement ce modèle :
    • Retirer la condition restrictive sur les commits pour lesquels sont lancés cette automatisation. Pour cela, remplacer le contenu de on de sorte à avoir on: push: branches: - main - dev
    • Changer le tag à la fin pour mettre username/application:latestusername est le nom d’utilisateur sur DockerHub;
    • Optionnel: changer le nom de l’action
  • Faire un commit et un push de ces fichiers

Comme on est fier de notre travail, on va afficher ça avec un badge sur le README (partie optionnelle).

  • Se rendre dans l’onglet Actions et cliquer sur une des actions listées.
  • En haut à droite, cliquer sur ...
  • Sélectionner Create status badge
  • Récupérer le code Markdown proposé
  • Copier dans votre README.md le code markdown proposé
Créer le badge

Maintenant, il nous reste à tester notre application dans l’espace bac à sable ou en local, si Docker est installé.

Application 15b (partie optionnelle): Tester l’application
  • Se rendre sur l’environnement bac à sable Play with Docker ou dans votre environnement Docker de prédilection.
  • Récupérer et lancer l’image :
terminal
$ docker run -it username/application:latest

🎉 La matrice de confusion doit s’afficher ! Vous avez grandement facilité la réutilisation de votre image.

Checkpoint post appli15
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli152
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli15
3
Nettoyer derrière nous

Partie 5: expérimenter en local des valorisations puis automatiser leur production

Nous avons automatisé les étapes intermédiaires de notre projet. Néanmoins nous n’avons pas encore réfléchi à la valorisation à mettre en oeuvre pour notre projet. On va supposer que notre projet s’adresse à des data scientists mais aussi à une audience moins technique. Pour ces premiers, nous pourrions nous contenter de valorisations techniques, comme des API, mais pour ces derniers il est conseillé de privilégier des formats plus user friendly.

Afin de faire le parallèle avec les parcours possibles pour l’évaluation, nous allons proposer deux valorisations16:

  • Une API facilitant la réutilisation du modèle en “production” ;
  • Un site web statique exploitant cette API pour exposer les prédictions à une audience moins technique.

La solution que nous allons proposer pour les sites statiques, Quarto associé à Github Pages, peut être utilisée dans le cadre des parcours “rapport reproductible” ou “dashboard / application interactive”.

Pour ce dernier parcours, d’autres approches techniques sont néanmoins possibles, comme Streamlit. Celles-ci sont plus exigeantes sur le plan technique puisqu’elles nécessitent de mettre en production sur des serveurs conteuneurisés (comme la mise en production de l’API) là où le site statique ne nécessite qu’un serveur web, mis à disposition gratuitement par Github.

La distinction principale entre ces deux approches est qu’elles s’appuient sur des serveurs différents. Un site statique repose sur un serveur web là où Streamlit s’appuie sur serveur classique en backend. La différence principale entre ces deux types de serveurs réside principalement dans leur fonction et leur utilisation:

  • Un serveur web est spécifiquement conçu pour stocker, traiter et livrer des pages web aux clients. Cela inclut des fichiers HTML, CSS, JavaScript, images, etc. Les serveurs web écoutent les requêtes HTTP/HTTPS provenant des navigateurs des utilisateurs et y répondent en envoyant les données demandées.
  • Un serveur backend classique est conçu pour effectuer des opérations en réponse à un front, en l’occurrence une page web. Dans le contexte d’une application Streamlit, il s’agit d’un serveur avec l’environnement Python ad hoc pour exécuter le code nécessaire à répondre à toute action d’un utilisateur de l’appliacation.

Étape 1: développer une API en local

Le premier livrable devenu classique dans un projet impliquant du machine learning est la mise à disposition d’un modèle par le biais d’une API (voir chapitre sur la mise en production). Le framework FastAPI va permettre de rapidement transformer notre application Python en une API fonctionnelle.

Reprendre à partir d'ici
Si vous n'avez plus de VSCode actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:
Onyxia

Et ensuite, après avoir clôné le dépôt

terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli152
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli15
3
Nettoyer derrière nous

Application 16: Mise à disposition sous forme d’API locale
  • Installer fastAPI et uvicorn puis les ajouter au requirements.txt
  • Renommer le fichier main.py en train.py. Dans ce script, ajouter une sauvegarde du modèle après l’avoir entraîné, sous le format joblib.
  • Faire tourner
terminal
$ python train.py

pour enregistrer en local votre modèle de production.

  • Modifier les appels à main.py dans votre Dockerfile et vos actions Github sous peine d’essuyer des échecs lors de vos actions Github après le prochain push.

  • Ajouter model.joblib au .gitignore car Git n’est pas fait pour ce type de fichiers.

Nous allons maintenant passer au développement de l’API. Comme découvrir FastAPI n’est pas l’objet de cet enseignement, nous donnons directement le modèle pour créer l’API. Si vous désirez tester de vous-mêmes, vous pouvez créer votre fichier sans vous référer à l’exemple

  • Créer le fichier api.py permettant d’initialiser l’API:
Fichier api.py
src/models/train_evaluation.py
"""A simple API to expose our trained RandomForest model for Tutanic survival."""
from fastapi import FastAPI
from joblib import load

import pandas as pd

model = load('model.joblib')

app = FastAPI(
    title="Prédiction de survie sur le Titanic",
    description=
    "Application de prédiction de survie sur le Titanic 🚢 <br>Une version par API pour faciliter la réutilisation du modèle 🚀" +\
        "<br><br><img src=\"https://media.vogue.fr/photos/5faac06d39c5194ff9752ec9/1:1/w_2404,h_2404,c_limit/076_CHL_126884.jpg\" width=\"200\">"
    )


@app.get("/", tags=["Welcome"])
def show_welcome_page():
    """
    Show welcome page with model name and version.
    """

    return {
        "Message": "API de prédiction de survie sur le Titanic",
        "Model_name": 'Titanic ML',
        "Model_version": "0.1",
    }


@app.get("/predict", tags=["Predict"])
async def predict(
    sex: str = "female",
    age: float = 29.0,
    fare: float = 16.5,
    embarked: str = "S"
) -> str:
    """
    """

    df = pd.DataFrame(
        {
            "Sex": [sex],
            "Age": [age],
            "Fare": [fare],
            "Embarked": [embarked],
        }
    )

    prediction = "Survived 🎉" if int(model.predict(df)) == 1 else "Dead ⚰️"

    return prediction
  • Déployer en local l’API avec la commande
terminal
$ uvicorn api:app --reload --host "0.0.0.0" --port 5000
  • A partir du README du service, se rendre sur l’URL de déploiement, ajouter /docs/ à celui-ci et observer la documentation de l’API
  • Se servir de la documentation pour tester les requêtes /predict
  • Récupérer l’URL d’une des requêtes proposées. La tester dans le navigateur et depuis Python avec requests :
import request
requests.get(url).json()
  • Une fois que vous avez testé, vous pouvez tuer l’application en faisant CTRL+C. Retester votre bout de code Python et comprendre l’origine du problème.
Checkpoint post appli16
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli162
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli16
3
Nettoyer derrière nous

Étape 2: déployer l’API de manière manuelle

Reprendre à partir d'ici
Si vous n'avez plus de VSCode actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:
Onyxia

Et ensuite, après avoir clôné le dépôt

terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli162
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli16
3
Nettoyer derrière nous

A ce stade, nous avons déployé l’API seulement localement, dans le cadre d’un terminal qui tourne en arrière-plan. C’est une mise en production manuelle, pas franchement pérenne. Ce mode de déploiement est très pratique pour la phase de développement, afin de s’assurer que l’API fonctionne comme attendu. Pour pérenniser la mise en production, on va éliminer l’aspect artisanal de celle-ci.

Il est temps de passer à l’étape de déploiement, qui permettra à notre API d’être accessible via une URL sur le web et d’avoir un serveur, en arrière plan, qui effectuera les opérations pour répondre à une requête. Pour se faire, on va utiliser les possibilités offertes par Kubernetes, sur lequel est basé le SSP Cloud.

Application 17: Dockeriser l’API (intégration continue)
  • Pour rendre la structure du projet plus lisible, déplacer api.py -> api/main.py

  • Créer un script api/run.sh à la racine du projet qui lance le script train.py puis déploie localement l’API

Fichier run.sh
api/run.sh
#/bin/bash
python3 train.py
uvicorn api.main:app --reload --host "0.0.0.0" --port 5000
  • Donner au script api/run.sh des permissions d’exécution : chmod +x api/run.sh

  • Ajouter COPY api ./api pour avoir les fichiers nécessaires au lancement dans l’API dans l’image

  • Modifier COPY train.py . pour tenir compte du nouveau nom du fichier

  • Changer l’instruction CMD du Dockerfile pour exécuter le script api/run.sh au lancement du conteneur (CMD ["bash", "-c", "./api/run.sh"])

  • Mettre à jour votre requirements.txt pour tenir compte des nouveaux packages utilisés

  • Commit et push les changements

  • Une fois le CI terminé, récupérer la nouvelle image dans votre environnement de test de Docker et vérifier que l’API se déploie correctement

Checkpoint post appli17
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli172
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli17
3
Nettoyer derrière nous

Nous avons préparé la mise à disposition de notre API mais à l’heure actuelle elle n’est pas disponible de manière aisée car il est nécessaire de lancer manuellement une image Docker pour pouvoir y accéder. Ce type de travail est la spécialité de Kubernetes que nous allons utiliser pour gérer la mise à disposition de notre API.

Application 18b: Mettre à disposition l’API (déploiement manuel)

Cette partie nécessite d’avoir à disposition une infrastructure cloud.

  • Créer un dossier deployment à la racine du projet qui va contenir les fichiers de configuration nécessaires pour déployer sur un cluster Kubernetes

  • En vous inspirant de la documentation, y ajouter un premier fichier deployment.yaml qui va spécifier la configuration du Pod à lancer sur le cluster

Fichier deployment/deployment.yaml
deployment/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanic-deployment
  labels:
    app: titanic
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanic
  template:
    metadata:
      labels:
        app: titanic
    spec:
      containers:
      - name: titanic
1        image:
        ports:
        - containerPort: 5000
1
Mettre ici l’image Docker utilisée, sous la forme username/image:latest
  • En vous inspirant de la documentation, y ajouter un second fichier service.yaml qui va créer une ressource Service permettant de donner une identité fixe au Pod précédemment créé au sein du cluster
Fichier deployment/service.yaml
deployment/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: titanic-service
spec:
  selector:
    app: titanic
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000
  • En vous inspirant de la documentation, y ajouter un troisième fichier ingress.yaml qui va créer une ressource Ingress permettant d’exposer le service via une URL en dehors du cluster
Fichier deployment/ingress.yaml
deployment/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: titanic-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    # Enable CORS by adding these annotations
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, DELETE, PATCH, OPTIONS"
    nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
    nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
    nginx.ingress.kubernetes.io/cors-allow-origin: "*"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
1    -
  rules:
2  - host:
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: titanic-service
            port:
              number: 80
1
Mettez l’URL auquel vous voulez exposer votre service. Sur le modèle de titanic.kub.sspcloud.fr (mais ne tentez pas celui-là, il est déjà pris 😃)
2
Mettre ce même URL ici aussi
  • Appliquer ces fichiers de configuration sur le cluster : kubectl apply -f deployment/

  • Si tout a correctement fonctionné, vous devriez pouvoir accéder depuis votre navigateur à l’API à l’URL spécifiée dans le fichier deployment/ingress.yaml. Par exemple https://toto.kub.sspcloud.fr/ si vous avez mis celui-ci plus tôt (et https://toto.kub.sspcloud.fr/docs pour la documentation).

Checkpoint post appli18
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli182
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli18
3
Nettoyer derrière nous

Notre API est accessible sans problème depuis Python ou notre navigateur.

En revanche, si on désire utiliser JavaScript pour créer une application interactive il est indispensable de mettre les lignes un peu obscure sur le CORS dans le fichier ingress.yaml.

Comme c’est un point technique qui ne concerne pas les compétences liées à ce cours, nous avons donné directement les lignes correspondantes dans ce fichier.

On peut remarquer quelques voies d’amélioration de notre approche qui seront ultérieurement traitées:

  • L’entraînement du modèle est ré-effectué à chaque lancement d’un nouveau conteneur. On relance donc autant de fois un entraînement qu’on déploie de conteneurs pour répondre à nos utilisateurs. Ce sera l’objet de la partie MLOps de fiabiliser et optimiser cette partie du pipeline.
  • il est nécessaire de (re)lancer manuellement kubectl apply -f deployment/ à chaque changement de notre code. Autrement dit, lors de cette application, on a amélioré la fiabilité du lancement de notre API mais un lancement manuel est encore indispensable. Comme dans le reste de ce cours, on va essayer d’éviter un geste manuel pouvant être source d’erreur en privilégiant l’automatisation et l’archivage dans des scripts. C’est l’objet de la prochaine étape.

Etape 3: automatiser le déploiement (déploiement en continu)

Clarification sur la branche de travail et les tags

A partir de maintenant, il est nécessaire de clarifier la branche principale sur laquelle nous travaillons. De manière traditionnelle, on utilise la branche main. Si vous avez changé de branche, vous pouvez continuer 1/ continuer mais en tenir compte dans les exemples ultérieurs ou 2/ fusionner celle-ci à main.

Si vous avez utilisé un tag pour sauter une ou plusieurs étapes, il va être nécessaire de se placer sur une branche car vous êtes en head detached. Pour cela, après avoir committé les fichiers que vous désirez garder

terminal
1$ git branch -D dev
2$ git push origin -d dev
3$ git checkout -b dev
4$ git push --set-upstream origin dev
1
Supprime la branche dev locale (si elle existe).
2
Supprime la branche dev remote (si elle existe).
3
Crée une nouvelle branche dev locale et on se place sur cette branche.
4
Pousse la branche dev et active la synchronisation entre la branche locale et la branche remote.

Qu’est-ce qui peut déclencher une évolution nécessitant de mettre à jour l’ensemble de notre processus de production ?

Regardons à nouveau notre pipeline:

Les inputs de notre pipeline sont donc:

  • La configuration. Ici, on peut considérer que notre .env de configuration ou les secrets renseignés à Github relèvent de cette catégorie ;
  • Les données. Nos données sont statiques et n’ont pas vocation à évoluer. Si c’était le cas, il faudrait en tenir compte dans notre automatisation[^versionning-data]. ;
  • Le code. C’est l’élément principal qui évolue chez nous. Idéalement, on veut automatiser le processus au maximum en faisant en sorte qu’à chaque mise à jour de notre code (un push sur Github), les étapes ultérieures (production de l’image Docker, etc.) se lancent. Néanmoins, on veut aussi éviter qu’une erreur puisse donner lieu à une mise en production non-fonctionnelle, on va donc maintenir une action manuelle minimale comme garde-fou.
Et le versionning des données ?

Ici, nous nous plaçons dans le cas simple où les données brutes reçues sont figées. Ce qui peut changer est la manière dont on constitue nos échantillons train/test. Il sera donc utile de logguer les données en question par le biais de MLFlow. Mais il n’est pas nécessaire de versionner les données brutes.

Si celles-ci évoluaient, il pourrait être utile de versionner les données, à la manière dont on le fait pour le code. Git n’est pas l’outil approprié pour cela. Parmi les outils populaires de versionning de données, bien intégrés avec S3, il y a sur le SSPCloud lakefs.

Pour automatiser au maximum la mise en production, on va utiliser un nouvel outil : ArgoCD. Ainsi, au lieu de devoir appliquer manuellement la commande kubectl apply à chaque modification des fichiers de déploiement (présents dans le dossier kubernetes/), c’est l’opérateur ArgoCD, déployé sur le cluster, qui va détecter les changements de configuration du déploiement et les appliquer automatiquement.

C’est l’approche dite GitOps : le dépôt Git du déploiement fait office de source de vérité unique de l’état voulu de l’application, tout changement sur ce dernier doit donc se répercuter immédiatement sur le déploiement effectif.

Application 19a: Automatiser la mise à disposition de l’API (déploiement continu)
  • Lancer un service ArgoCD sur le SSPCloud depuis la page Mes services (catalogue Automation). Laisser les configurations par défaut.
  • Sur GitHub, créer un dépôt application-deployment qui va servir de dépôt GitOps, c’est à dire un dépôt qui spécifie le paramétrage du déploiement de votre application.
  • Ajouter un dossier deployment à votre dépôt GitOps, dans lequel on mettra les trois fichiers de déploiement qui permettent de déployer notre application sur Kubernetes (deployment.yaml, service.yaml, ingress.yaml).
  • A la racine de votre dépôt GitOps, créez un fichier application.yml avec le contenu suivant, en prenant bien soin de modifier les lignes surlignées avec les informations pertinentes :
application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: ensae-mlops
spec:
  project: default
  source:
1    repoURL: https://github.com/<your_github_username>/application-deployment.git
2    targetRevision: main
3    path: deployment
  destination:
    server: https://kubernetes.default.svc
4    namespace: user-<your_sspcloud_username>
  syncPolicy:
    automated:
      selfHeal: true
1
L’URL de votre dépôt Github faisant office de dépôt GitOps.
2
La branche à partir de laquelle vous déployez.
3
Le nom du dossier contenant vos fichiers de déploiement Kubernetes.
4
Votre namespace Kubernetes. Sur le SSPCloud, cela prend la forme user-${username}.
  • Pousser sur Github le dépôt GitOps.
  • Dans ArgoCD, cliquez sur New App puis Edit as a YAML. Copiez-collez le contenu de application.yml et cliquez sur Create.
  • Observez dans l’interface d’ArgoCD le déploiement progressif des ressources nécessaires à votre application sur le cluster. Joli non ?
  • Vérifiez que votre API est bien déployée en utilisant l’URL définie dans le fichier ingress.yml.
  • Supprimer du code applicatif le dossier deployment puisque c’est maintenant votre dépôt de déploiement qui le contrôle.
  • Indiquer dans le README.md que le déploiement de votre application (dont vous pouvez mettre l’URL dans le README) est contrôlé par un autre dépôt.

Si cela a fonctionné, vous devriez maintenant voir votre application dans votre tableau de bord ArgoCD:

Checkpoint post appli19a
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli19a2
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli19a
3
Nettoyer derrière nous

A présent, nous avons tous les outils à notre disposition pour construire un vrai pipeline de CI/CD, automatisé de bout en bout. Il va nous suffire pour cela de mettre à bout les composants :

  • dans la partie 4 de l’application, nous avons construit un pipeline de CI : on a donc seulement à faire un commit sur le dépôt de l’application pour lancer l’étape de build et de mise à disposition de la nouvelle image sur le DockerHub ;

  • dans l’application précédente, nous avons construit un pipeline de CD : ArgoCD suit en permanence l’état du dépôt GitOps, tout commit sur ce dernier lancera donc automatiquement un redéploiement de l’application.

Il y a donc un élément qui fait la liaison entre ces deux pipelines et qui nous sert de garde-fou en cas d’erreur : la version de l’application.

Application 19b : Mettre à jour la version en production

Jusqu’à maintenant, on a utilisé le tag latest pour définir la version de notre application. En pratique, lorsqu’on passe de la phase de développement à celle de production, on a plutôt envie de versionner proprement les versions de l’application afin de savoir ce qui est déployé. On va pour cela utiliser les tags avec Git, qui vont se propager au nommage de l’image Docker.

  • Modifier le fichier de CI prod.yml pour assurer la propagation des tags.

Fichier .github/workflows/prod.yml

.github/workflows/prod.yml
name: Construction image Docker

on:
  push:
    branches:
      - main
      - dev
    tags:
      - 'v*.*.*'

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      -
        name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
1          images: linogaliana/application

      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
1
Modifier ici !
  • Dans le dépôt de l’application, mettre à jour le code dans api/main.py pour changer un élément de l’interface de votre documentation. Par exemple, mettre en gras un titre.
app = FastAPI(
    title="Démonstration du modèle de prédiction de survie sur le Titanic",
    description=
    "<b>Application de prédiction de survie sur le Titanic</b> 🚢 <br>Une version par API pour faciliter la réutilisation du modèle 🚀" +\
        "<br><br><img src=\"https://media.vogue.fr/photos/5faac06d39c5194ff9752ec9/1:1/w_2404,h_2404,c_limit/076_CHL_126884.jpg\" width=\"200\">"
    )
  • Commit et push les changements.

  • Tagger le commit effectué précédemment et push le nouveau tag :

terminal
$ git tag v0.0.1
$ git push --tags
  • Vérifier sur le dépôt GitHub de l’application que ce commit lance bien un pipeline de CI associé au tag v1.0.0. Une fois terminé, vérifier sur le DockerHub que le tag v0.0.1 existe bien parmi les tags disponibles de l’image.

La partie CI a correctement fonctionné. Intéressons-nous à présent à la partie CD.

  • Sur le dépôt GitOps, mettre à jour la version de l’image à déployer en production dans le fichier deployment/deployment.yaml

Fichier deployment/deployment.yaml

deployment/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanic-deployment
  labels:
    app: titanic
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanic
  template:
    metadata:
      labels:
        app: titanic
    spec:
      containers:
      - name: titanic
1        image: linogaliana/application:v0.0.1
        ports:
        - containerPort: 5000
1
Remplacer ici par le dépôt applicatif adéquat
  • Après avoir committé et pushé, observer dans ArgoCD le statut de votre application. Normalement, l’opérateur devrait avoir automatiquement identifié le changement, et mettre à jour le déploiement pour en tenir compte.

  • Vérifier que l’API a bien été mise à jour.
Checkpoint post appli19b
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli19b2
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli19b
3
Nettoyer derrière nous

Etape 4: construire un site web

terminal
$ git checkout appli19
$ git checkout -b dev
$ git push origin dev

On va proposer un nouveau livrable pour parler à un public plus large. Pour faire ce site web, on va utiliser Quarto et déployer sur Github Pages.

Application 20: Création d’un site web pour valoriser le projet
terminal
$ quarto create project website mysite
  • Faire remonter d’un niveau _quarto.yml
  • Supprimer about.qmd, déplacer index.qmd vers la racine de notre projet.
  • Remplacer le contenu de index.qmd par celui-ci et retirer about.qmd des fichiers à compiler.
  • Déplacer styles.css à la racine du projet
  • Mettre à jour le .gitignore avec les instructions suivantes
/.quarto/
*.html
*_files
_site/
  • En ligne de commande, faire quarto preview (ajouter les arguments --port 5000 --host 0.0.0.0 si vous passez par le SSPCloud)
  • Observer le site web généré en local

Enfin, on va construire et déployer automatiquement ce site web grâce au combo Github Actions et Github Pages:

  • Créer une branche gh-pages à partir des lignes suivantes
git checkout --orphan gh-pages
git reset --hard # make sure all changes are committed before running this!
git commit --allow-empty -m "Initialising gh-pages branch"
git push origin gh-pages
  • Revenir à votre branche
  • Créer un fichier .github/workflows/website.yaml avec le contenu de ce fichier
  • Modifier le README pour indiquer l’URL de votre site web et de votre API
Checkpoint post appli20
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli202
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli20
3
Nettoyer derrière nous

Partie 6: adopter une approche MLOps pour améliorer notre modèle

terminal
$ git checkout appli20
$ git checkout -b dev
$ git push origin dev

Maintenant que nous avons tout préparé pour mettre à disposition rapidement un modèle, nous pouvons revenir en arrière pour améliorer ce modèle. Pour cela, nous allons mettre en oeuvre une validation croisée.

Le problème que nous allons rencontrer va être que nous voudrions facilement tracer les évolutions de notre modèle, la qualité prédictive de celui-ci dans différentes situations. Il s’agira d’à nouveau mettre en place du logging mais, cette fois, de suivre la qualité du modèle et pas seulement s’il fonctionne. L’outil MLFlow va répondre à ce problème et va, au passage, fluidifier la mise à disposition du modèle de production, c’est-à-dire de celui qu’on désire mettre à disposition du public.

Revenir sur le code d’entraînement du modèle pour faire de la validation croisée

Pour pouvoir faire ceci, il va falloir changer un tout petit peu notre code applicatif dans sa phase d’entraînement.

Application 21 (optionnelle): restructuration de la chaîne
  1. Faire les modifications suivantes pour restructurer notre pipeline afin de mieux distinguer les étapes d’estimation et d’évaluation
Modification de train.py pour faire une grid search
train.py
"""
Prediction de la survie d'un individu sur le Titanic
"""

import os
from dotenv import load_dotenv
import argparse
from loguru import logger

import pathlib
from joblib import dump
import pandas as pd
from sklearn.model_selection import GridSearchCV

from src.pipeline.build_pipeline import split_train_test, create_pipeline
from src.models.train_evaluate import evaluate_model


# ENVIRONMENT CONFIGURATION ---------------------------

logger.add("recording.log", rotation="500 MB")
load_dotenv()

parser = argparse.ArgumentParser(description="Paramètres du random forest")
parser.add_argument(
    "--n_trees", type=int, default=20, help="Nombre d'arbres"
)
args = parser.parse_args()

URL_RAW = "https://minio.lab.sspcloud.fr/lgaliana/ensae-reproductibilite/data/raw/data.csv"

n_trees = args.n_trees
jeton_api = os.environ.get("JETON_API", "")
data_path = os.environ.get("data_path", URL_RAW)
data_train_path = os.environ.get("train_path", "data/derived/train.parquet")
data_test_path = os.environ.get("test_path", "data/derived/test.parquet")
MAX_DEPTH = None
MAX_FEATURES = "sqrt"

if jeton_api.startswith("$"):
    logger.info("API token has been configured properly")
else:
    logger.warning("API token has not been configured")


# IMPORT ET STRUCTURATION DONNEES --------------------------------

p = pathlib.Path("data/derived/")
p.mkdir(parents=True, exist_ok=True)

TrainingData = pd.read_csv(data_path)

X_train, X_test, y_train, y_test = split_train_test(
    TrainingData, test_size=0.1,
    train_path=data_train_path,
    test_path=data_test_path
)


# PIPELINE ----------------------------


# Create the pipeline
pipe = create_pipeline(
    n_trees, max_depth=MAX_DEPTH, max_features=MAX_FEATURES
)


param_grid = {
    "classifier__n_estimators": [10, 20, 50],
    "classifier__max_leaf_nodes": [5, 10, 50],
}


pipe_cross_validation = GridSearchCV(
    pipe,
    param_grid=param_grid,
    scoring=["accuracy", "precision", "recall", "f1"],
    refit="f1",
    cv=5,
    n_jobs=5,
    verbose=1,
)
pipe = pipe_cross_validation.best_estimator_

# ESTIMATION ET EVALUATION ----------------------

pipe.fit(X_train, y_train)

dump(pipe, 'model.joblib')


# Evaluate the model
score, matrix = evaluate_model(pipe, X_test, y_test)

logger.success(f"{score:.1%} de bonnes réponses sur les données de test pour validation")
logger.debug(20 * "-")
logger.info("Matrice de confusion")
logger.debug(matrix)
  1. Dans le code de l’API (api/main.py), changer la version du modèle mis en oeuvre en “0.2”
  2. Après avoir committé cette nouvelle version du code applicatif, tagguer ce dépôt avec le tag v0.0.2
  3. Modifier deployment/deployment.yaml dans le code GitOps pour utiliser ce tag.
Checkpoint post appli21
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli212
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli21
3
Nettoyer derrière nous

Garder une trace des entraînements de notre modèle grâce au register de MLFlow

terminal
$ git stash
$ git checkout appli21

Enregistrer nos premiers entraînements

Application 22 : archiver nos entraînements avec MLFlow
  1. Lancer MLFlow depuis l’onflet Mes services du SSPCloud. Attendre que le service soit bien lancé. Cela créera un service dont l’URL est de la forme https://user-{username}.user.lab.sspcloud.fr. Ce service MLFlow communiquera avec les VSCode que vous ouvrirez ultérieurement à partir de cet URL ainsi qu’avec le système de stockage S317.

  2. Regarder la page Experiments. Elle ne contient que Default à ce stade, c’est normal.

  1. Une fois le service MLFlow fonctionnel, lancer un nouveau VSCode pour bénéficier de la connexion automatique entre les services interactifs du SSPCloud et les services d’automatisation comme MLFlow.

  2. Clôner votre projet, vous situer sur la branche de travail.

  3. Dans la section de passage des paramètres de notre ligne de commande, introduire ce morceau de code:

parser = argparse.ArgumentParser(description="Paramètres du random forest")
parser.add_argument(
    "--n_trees", type=int, default=20, help="Nombre d'arbres"
)
parser.add_argument(
    "--experiment_name", type=str, default="titanicml", help="MLFlow experiment name"
)
args = parser.parse_args()
  1. Faire tourner train.py en ligne de commande puis retourner sur l’UI de MLFlow et observer la différence, à gauche.

  2. A la fin du script train.py, ajouter le code suivant

Code à ajouter
fin de train.py
# LOGGING IN MLFLOW -----------------

input_data_mlflow = mlflow.data.from_pandas(
    TrainingData, source=data_path, name="Raw dataset"
)
training_data_mlflow = mlflow.data.from_pandas(
    pd.concat([X_train, y_train], axis=1), source=data_path, name="Training data"
)


with mlflow.start_run():

    # Log datasets
    mlflow.log_input(input_data_mlflow, context="raw")
    mlflow.log_input(training_data_mlflow, context="raw")

    # Log parameters
    mlflow.log_param("n_trees", n_trees)
    mlflow.log_param("max_depth", MAX_DEPTH)
    mlflow.log_param("max_features", MAX_FEATURES)

    # Log best hyperparameters from GridSearchCV
    best_params = pipe_cross_validation.best_params_
    for param, value in best_params.items():
        mlflow.log_param(param, value)

    # Log metrics
    mlflow.log_metric("accuracy", score)

    # Log confusion matrix as an artifact
    matrix_path = "confusion_matrix.txt"
    with open(matrix_path, "w") as f:
        f.write(str(matrix))
    mlflow.log_artifact(matrix_path)

    # Log model
    mlflow.sklearn.log_model(pipe, "model")
  1. Ajouter mlruns/* dans .gitignore

  2. Tester train.py en ligne de commande

  3. Observer l’évolution de la page Experiments. Cliquer sur un des run. Observer toutes les métadonnées archivées (hyperparamètres, métriques d’évaluation, requirements.txt dont MLFlow a fait l’inférence, etc.)

  4. Observer le code proposé par MLFlow pour récupérer le run en question. Tester celui-ci dans un notebook sur le fichier intermédiaire de test au format Parquet

  5. En ligne de commande, faites tourner pour une autre valeur de n_trees. Retourner à la liste des runs en cliquant à nouveau sur “titanicml” dans les expérimentations

  6. Dans l’onglet Table, sélectionner plusieurs expérimentations, cliquer sur Columns et ajouter la statistique d’accuracy. Ajuster la taille des colonnes pour la voir et classer les modèles par score décroissants

  7. Cliquer sur Compare après en avoir sélectionné plusieurs. Afficher un scatterplot des performances en fonction du nombre d’estimateurs. Conclure.

Checkpoint post appli22
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli222
rm -f update.sh
1
Récupérer le script de checkpoint
2
Avancer à l’état à l’issue de l’application appli22
3
Nettoyer derrière nous

Cette appplication illustre l’un des premiers apports de MLFlow: on garde une trace de nos expérimentations: le modèle est archivé avec les paramètres et des métriques de performance. On peut donc retrouver de plusieurs manières un modèle qui nous avait tapé dans l’oeil.

Néanmoins, persistent un certain nombre de voies d’amélioration dans notre pipeline.

  • On entraîne le modèle en local, de manière séquentielle, et en lançant nous-mêmes le script train.py.
  • Pis encore, à l’heure actuelle, cette étape d’estimation n’est pas séparée de la mise à disposition du modèle par le biais de notre API. On archive des modèles mais on les utilise pas ultérieurement.

Les prochaines applications permettront d’améliorer ceci.

Consommation d’un modèle archivé sur MLFlow

A l’heure actuelle, notre pipeline est linéaire:

Ceci nous gêne pour faire évoluer notre modèle: on ne dissocie pas ce qui relève de l’entraînement du modèle de son utilisation. Un pipeline plus cyclique permettra de mieux dissocier l’expérimentation de la production:

Application 23 : passer en production un modèle avec MLFlow
  1. Si vous avez entraîné plusieurs modèles avec des n_trees différents, utiliser l’interface de MLFlow pour sélectionner le “meilleur”. Cliquer sur le modèle en question et faire l’action “Register Model”. L’enregistrer comme le modèle de “production”

  2. Rendez-vous sur l’onglet Models et observez cet entrepôt de modèles. Cliquez sur le modèle de production. Vous pourrez par ce biais suivre ses différentes versions.

  3. Ouvrir un notebook temporaire et observer le résultat.

import mlflow
import pandas as pd

model_name = "production"
model_version = "latest"

# Load the model from the Model Registry
model_uri = f"models:/{model_name}/{model_version}"
logged_model = mlflow.sklearn.load_model(model_uri)


# GENERATE PREDICTION DATA ---------------------

def create_data(
    sex: str = "female",
    age: float = 29.0,
    fare: float = 16.5,
    embarked: str = "S",
) -> str:
    """
    """

    df = pd.DataFrame(
        {
            "Sex": [sex],
            "Age": [age],
            "Fare": [fare],
            "Embarked": [embarked],
        }
    )

    return df


data = pd.concat(
    [create_data(age=40), create_data(sex="male")]
)

# PREDICTION ---------------------

logged_model.predict(pd.DataFrame(data))
  1. On va adapter le code applicatif de notre API pour tenir compte de ce modèle de production.
"""A simple API to expose our trained RandomForest model for Tutanic survival."""
from fastapi import FastAPI
import mlflow

import pandas as pd

# Preload model -------------------

model_name = "production"
model_version = "latest"

# Load the model from the Model Registry
model_uri = f"models:/{model_name}/{model_version}"
model = mlflow.sklearn.load_model(model_uri)

# Define app -------------------------


app = FastAPI(
    title="Prédiction de survie sur le Titanic",
    description=
    "Application de prédiction de survie sur le Titanic 🚢 <br>Une version par API pour faciliter la réutilisation du modèle 🚀" +\
        "<br><br><img src=\"https://media.vogue.fr/photos/5faac06d39c5194ff9752ec9/1:1/w_2404,h_2404,c_limit/076_CHL_126884.jpg\" width=\"200\">"
    )


@app.get("/", tags=["Welcome"])
def show_welcome_page():
    """
    Show welcome page with model name and version.
    """

    return {
        "Message": "API de prédiction de survie sur le Titanic",
        "Model_name": 'Titanic ML',
        "Model_version": "0.3",
    }


@app.get("/predict", tags=["Predict"])
async def predict(
    sex: str = "female",
    age: float = 29.0,
    fare: float = 16.5,
    embarked: str = "S"
) -> str:
    """
    """

    df = pd.DataFrame(
        {
            "Sex": [sex],
            "Age": [age],
            "Fare": [fare],
            "Embarked": [embarked],
        }
    )

    prediction = "Survived 🎉" if int(model.predict(df)) == 1 else "Dead ⚰️"

    return prediction

Les changements principaux de ce code sont:

  • on va chercher le modèle de production
  • on met à jour la version de notre API pour signaler à nos clients que celle-ci a évolué
  1. On va retirer l’entraînement de la séquence d’opération du api/run.sh. En supprimant la ligne relative à l’entraînement du modèle, vous devriez avoir
#/bin/bash
uvicorn api.main:app --reload --host "0.0.0.0" --port 5000

Mettons en production cette nouvelle version. Cela implique de faire les gestes suivants:

  1. Publier un tag v0.0.3 pour le code applicatif

  2. Mettre à jour notre manifeste dans le dépôt GitOps. En premier lieu, il faut changer la version de référence pour utiliser le tag v0.0.3. De plus, il faut déclarer la variable d’environnement MLFLOW_TRACKING_URI qui indique à Python l’entrepôt de modèles où aller chercher celui en production. La bonne pratique est de définir ceci hors du code, dans un fichier de configuration donc, ce qui est l’objet de notre manifeste deployment.yaml. On peut donc changer de cette manière ce fichier:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanic-deployment
  labels:
    app: titanic
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanic
  template:
    metadata:
      labels:
        app: titanic
    spec:
      containers:
      - name: titanic
1        image: linogaliana/application:v0.0.3
        ports:
        - containerPort: 5000
        env:
          - name: MLFLOW_TRACKING_URI
2            value: https://user-${USERNAME}-mlflow.user.lab.sspcloud.fr
        resources:
          limits:
            memory: "2Gi"
            cpu: "1000m"
1
Le tag de notre code applicatif
2
La variable d’environnement à adapter en fonction de l’adresse du dépôt de modèles utilisé

A ce stade, nous avons amélioré la fiabilité de notre application car nous utilisons le meilleur modèle. Néanmoins, nos entraînements sont encore manuels. Là encore il y a des gains à avoir car cela paraît pénible à la longue de devoir systématiquement relancer des entraînements manuellement pour tester des variations de tel ou tel paramètre. Heureusement, nous allons pouvoir automatiser ceci également.

terminal
1$ git stash
$ git checkout appli23
1
Pour annuler les modifications depuis le dernier commit

Industrialiser les entraînements de nos modèles

Pour industrialiser nos entraînements, nous allons créer des processus parallèles indépendants pour chaque combinaison de nos hyperparamètres. Pour cela, l’outil pratique sur le SSPCloud est Argo workflows.

Chaque combinaison d’hyperparamètres sera un processus isolé à l’issue duquel sera loggué le résultat dans MLFlow. Ces entraînements auront lieu en parallèle.

  1. Lancer un service Argo Workflows
  2. Dans le dépôt GitOps, créer un fichier argo-workflow/manifest.yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: titanic-training-workflow-
  namespace: user-lgaliana
spec:
  entrypoint: main
  serviceAccountName: workflow
  arguments:
    parameters:
      # The MLflow tracking server is responsible to log the hyper-parameter and model metrics.
      - name: mlflow-tracking-uri
1        value: https://user-lgaliana-argo-workflows.user.lab.sspcloud.fr
      - name: mlflow-experiment-name
2        value: titanicml
      - name: model-training-conf-list
        value: |
          [
            { "n_trees": 10, "max_features": "log2" },
            { "n_trees": 20, "max_features": "sqrt" },
            { "n_trees": 20, "max_features": "log2" },
            { "n_trees": 50, "max_features": "sqrt" }
          ]
  templates:
    # Entrypoint DAG template
    - name: main
      dag:
        tasks:
          # Task 0: Start pipeline
          - name: start-pipeline
            template: start-pipeline-wt
          # Task 1: Train model with given params
          - name: train-model-with-params
            dependencies: [ start-pipeline ]
            template: run-model-training-wt
            arguments:
              parameters:
                - name: max_features
                  value: "{{item.max_features}}"
                - name: n_trees
                  value: "{{item.n_trees}}"
            # Pass the inputs to the task using "withParam"
            withParam: "{{workflow.parameters.model-training-conf-list}}"

    # Now task container templates are defined
    # Worker template for task 0 : start-pipeline
    - name: start-pipeline-wt
      inputs:
      container:
        image: busybox
        command: [ sh, -c ]
        args: [ "echo Starting pipeline" ]

    # Worker template for task-1 : train model with params
    - name: run-model-training-wt
      inputs:
        parameters:
          - name: n_trees
          - name: max_features
      container:
3        image: linogaliana/application:v0.0.5
        imagePullPolicy: Always
        command: [sh, -c]
        args: [
          "python train.py --n_trees={{inputs.parameters.n_trees}} --max_features={{inputs.parameters.max_features}}"
          ]
        env:
          - name: MLFLOW_TRACKING_URI
            value: "{{workflow.parameters.mlflow-tracking-uri}}"
          - name: MLFLOW_EXPERIMENT_NAME
            value: "{{workflow.parameters.mlflow-experiment-name}}"
          - name: AWS_DEFAULT_REGION
            value: us-east-1
          - name: AWS_S3_ENDPOINT
            value: minio.lab.sspcloud.fr
1
Changer pour votre entrepot de modèle
2
Le nom de l’expérimentation MLFLow dont nous allons avoir besoin (on propose de continuer sur titanicml)
3
Changer l’application ici

Pour aller plus loin

Créer un service label studio pour évaluer la qualité du modèle

Footnotes

  1. Il y a quelques différences entre le VSCode server mis à disposition sur le SSPCloud et la version desktop sur laquelle s’appuient beaucoup de ressources. A quelques extensions prêts (Data Wrangler, Copilot), les différences sont néanmoins minimes.↩︎

  2. L’export dans un script .py a été fait directement depuis VSCode. Comme cela n’est pas vraiment l’objet du cours, nous passons cette étape et fournissons directement le script expurgé du texte intermédiaire. Mais n’oubliez pas que cette démarche, fréquente quand on a démarré sur un notebook et qu’on désire consolider en faisant la transition vers des scripts, nécessite d’être attentif pour ne pas risquer de faire une erreur.↩︎

  3. Il est également possible avec VSCode d’exécuter le script ligne à ligne de manière interactive ligne à ligne (MAJ+ENTER). Néanmoins, cela nécessite de s’assurer que le working directory de votre console interactive est le bon. Celle-ci se lance selon les paramètres préconfigurés de VSCode et les votres ne sont peut-être pas les mêmes que les notres. Vous pouvez changer le working directory dans le script en utilisant le package os mais peut-être allez vous découvrir ultérieurement qu’il y a de meilleures pratiques…↩︎

  4. Essayez de commit vos changements à chaque étape de l’exercice, c’est une bonne habitude à prendre.↩︎

  5. Il est normal d’avoir des dossiers __pycache__ qui traînent en local : ils se créent automatiquement à l’exécution d’un script en Python. Néanmoins, il ne faut pas associer ces fichiers à Git, voilà pourquoi on les ajoute au .gitignore.↩︎

  6. Nous proposons ici d’adopter le principe de la programmation fonctionnelle. Pour encore fiabiliser un processus, il serait possible d’adopter le paradigme de la programmation orientée objet (POO). Celle-ci est plus rebutante et demande plus de temps au développeur. L’arbitrage coût-avantage est négatif pour notre exemple, nous proposons donc de nous en passer. Néanmoins, pour une mise en production réelle d’un modèle, il peut être utle de l’adopter car certains frameworks, à commencer par les pipelines scikit, exigeront certaines classes et méthodes si vous désirez brancher des objets ad hoc à ceux-ci.↩︎

  7. Attention, les données ont été committées au moins une fois. Les supprimer du dépôt ne les efface pas de l’historique. Si cette erreur arrive, le mieux est de supprimer le dépôt en ligne, créer un nouvel historique Git et partir de celui-ci pour des publications ultérieures sur Github. Néanmoins l’idéal serait de ne pas s’exposer à cela. C’est justement l’objet des bonnes pratiques de ce cours: un .gitignore bien construit et une séparation des environnements de stockage du code et des données seront bien plus efficaces pour vous éviter ces problèmes que tout les conseils de vigilance que vous pourrez trouver ailleurs.↩︎

  8. Lorsqu’on développe du code qui finalement ne s’avère plus nécessaire, on a souvent un cas de conscience à le supprimer et on préfère le mettre de côté. Au final, ce syndrôme de Diogène est mauvais pour la pérennité du projet : on se retrouve à devoir maintenir une base de code qui n’est, en pratique, pas utilisée. Ce n’est pas un problème de supprimer un code ; si finalement celui-ci s’avère utile, on peut le retrouver grâce à l’historique Git et les outils de recherche sur Github. Le package vulture est très pratique pour diagnostiquer les morceaux de code inutiles dans un projet.↩︎

  9. Le fichier __init__.py indique à Python que le dossier est un package. Il permet de proposer certaines configurations lors de l’import du package. Il permet également de contrôler les objets exportés (c’est-à-dire mis à disposition de l’utilisateur) par le package par rapport aux objets internes au package. En le laissant vide, nous allons utiliser ce fichier pour importer l’ensemble des fonctions de nos sous-modules. Ce n’est pas la meilleure pratique mais un contrôle plus fin des objets exportés demanderait un investissement qui ne vaut, ici, pas le coût.↩︎

  10. Si vous désirez aussi contrôler la version de Python, ce qui peut être important dans une perspective de portabilité, vous pouvez ajouter une option, par exemple -p python3.10. Néanmoins nous n’allons pas nous embarasser de cette nuance pour la suite car nous pourrons contrôler la version de Python plus finement par le biais de Docker.↩︎

  11. L’option -c passée après la commande python permet d’indiquer à Python que la commande ne se trouve pas dans un fichier mais sera dans le texte qu’on va directement lui fournir.↩︎

  12. L’option -c passée après la commande python permet d’indiquer à Python que la commande ne se trouve pas dans un fichier mais sera dans le texte qu’on va directement lui fournir.↩︎

  13. Pour comparer les deux listes, vous pouvez utiliser la fonctionnalité de split du terminal sur VSCode pour comparer les outputs de conda env export en les mettant en face à face.↩︎

  14. Il est tout à fait normal de ne pas parvenir à créer une action fonctionnelle du premier coup. N’hésitez pas à pusher votre code après chaque question pour vérifier que vous parvenez bien à réaliser chaque étape. Sinon vous risquez de devoir corriger bout par bout un fichier plus conséquent.↩︎

  15. Il existe une approche alternative pour faire des tests réguliers: les hooks Git. Il s’agit de règles qui doivent être satisfaites pour que le fichier puisse être committé. Cela assure que chaque commit remplisse des critères de qualité afin d’éviter le problème de la procrastination.

    La documentation de pylint offre des explications supplémentaires. Ici, nous allons adopter une approche moins ambitieuse en demandant à notre action de faire ce travail d’évaluation de la qualité de notre code↩︎

  16. Vous n’êtes pas obligés pour l’évaluation de mettre en oeuvre les jalons de plusieurs parcours. Néanmoins, vous découvrirez que chaque nouveau pas en avant est moins coûteux que le précédent si vous avez mis en oeuvre les réflexes des bonnes pratiques.↩︎

  17. Par conséquent, MLFLow bénéficie de l’injection automatique des tokens pour pouvoir lire/écrire sur S3. Ces jetons ont la même durée avant expiration que ceux de vos services interactifs VSCode. Il faut donc, par défaut, supprimer et rouvrir un service MLFLow régulièrement. La manière d’éviter cela est de créer des service account sur https://minio-console.lab.sspcloud.fr/ et de les renseigner sur la page.↩︎

Reuse