Code
-r requirements.txt pip install
Ce notebook vise à présenter pas à pas comment créer une application interactive avec Streamlit
reproduisant celle proposée sur myyuka.lab.sspcloud.fr.
Cet exercice est proposé dans le cadre du Funathon
(hackathon non compĂ©titif) organisĂ© en 2023 par lâInsee et le MinistĂšre de lâAgriculture sur le thĂšme âDu champ Ă lâassietteâ. Les autres sujets sont disponibles sur le Github InseeFrLab
.
Pour les personnes bĂ©nĂ©ficiant dâun compte sur lâinfrastructure SSP Cloud
vous pouvez cliquer sur le lien ci-dessous pour lancer un environnement Jupyter
prĂȘt-Ă -lâemploi Il sâagit de lâapproche recommandĂ©e si vous avez un compte sur cette plateforme de lâEtat.
Si vous ne disposez pas dâun tel environnement, il est possible de consulter cette page Ă travers un notebook depuis Google Colab
. NĂ©anmoins, tous les exemples ne seront pas reproductibles puisque, par exemple, certains nĂ©cessitent dâĂȘtre en mesure de lancer une application web depuis Python
, ce qui nâest pas possible sur Google Colab
.
En amont de lâexĂ©cution de ce notebook, il est recommandĂ© dâinstaller lâensemble des packages utilisĂ©s dans ce projet avec la commande suivante :
-r requirements.txt pip install
Lâobjectif de ce projet est dâapprendre Ă utiliser Python
pour créer des applications réactives avec Streamlit
mais aussi de se familiariser à la manipulation de données avec Python
et, au passage, Ă quelques bonnes pratiques utiles pour obtenir des projets plus lisibles et reproductibles.
Pour parvenir Ă cet objectif, il est possible dâemprunter plusieurs voies, plus ou moins guidĂ©es. Celles-ci sont lĂ pour permettre que ce sujet soit rĂ©alisable. Elles sont balisĂ©es de la maniĂšre suivante :
Balisage | Approche | Prérequis de niveau | Objectif pédagogique |
---|---|---|---|
đĄ | ExĂ©cuter les cellules permet dâobtenir le rĂ©sultat attendu | CapacitĂ© Ă installer des packages | DĂ©couvrir de nouveaux packages en suivant le fil conducteur du projet, dĂ©couvrir les scripts Python , se familiariser avec Git |
đą | Des instructions dĂ©taillĂ©es sur la maniĂšre de procĂ©der sont proposĂ©es | ConnaĂźtre quelques manipulations avec Pandas |
Apprendre à utiliser certains packages avec un projet guidé, se familiariser avec les projets Python plus conséquents que les notebooks Jupyter |
đ” | Instructions moins dĂ©taillĂ©es | CapacitĂ© Ă manipuler des donnĂ©es avec Pandas |
Apprendre à modulariser du code pour faciliter sa réutilisation dans une application, découvrir la récupation de données via des API |
đŽ | Peu dâinstructions | ExpĂ©rience en dĂ©veloppement de code Python |
DĂ©couvrir la crĂ©ation dâapplication ou se familiariser avec lâĂ©cosystĂšme DuckDB |
â« | Autonomie | Bonne maĂźtrise de Python et de la ligne de commande ÌLinux |
Sâinitier au dĂ©ploiement dâune application ou Ă lâingĂ©nierie de donnĂ©es |
Le parcours vers la mise en oeuvre dâune application fonctionnelle se fait par Ă©tapes, en sĂ©quençant le projet pour permettre dâavoir un projet lisible, reproductible et modulaire.
Les Ă©tapes ne sont pas forcĂ©ment de difficultĂ© graduelle, il sâagit plutĂŽt de sĂ©quencer de maniĂšre logique le projet pour vous faciliter la prise en main.
Il est donc tout Ă fait possible de passer, selon les parties, dâune voie đą Ă une voie đ” ou bien de tester les codes proposĂ©s dans la voie đĄ dâabord puis, une fois que la logique a Ă©tĂ© comprise, essayer de les faire soit-mĂȘme via la voie đą ou encore essayer via la voie đ”, ne pas y parvenir du fait du caractĂšre plus succinct des instructions et regarder les instructions de la voie đą ou la solution de la voie đĄ.
Il est mĂȘme tout Ă fait possible de sauter une Ă©tape et reprendre Ă partir de la suivante grĂące aux checkpoints proposĂ©s.
Les consignes sont encapsulĂ©es dans des boites dĂ©diĂ©es, afin dâĂȘtre sĂ©parĂ©es des explications gĂ©nĂ©rales.
Par exemple, la boite verte prendra lâaspect suivant:
alors que sur le mĂȘme exercice, si plusieurs voies peuvent emprunter le mĂȘme chemin, on utilisera une dĂ©limitation grise :
La solution associĂ©e, visible pour les personnes sur la voie đĄ, sera :
# Solution pour voie đĄ
print("toto")
Le projet est séquencé de la maniÚre suivante :
Etape | Objectif |
---|---|
Récupération et nettoyage de la base OpenFoodFacts |
Lire des donnĂ©es avec Pandas depuis un site web (đĄ,đą,đ”,đŽ,â«), appliquer des nettoyages de champs textuels (đĄ,đą,đ”,đŽ,â«), catĂ©goriser ces donnĂ©es avec un classifieur automatique (đĄ,đą,đ”,đŽ,â«) voire entrainer un classifieur ad hoc (đŽ,â«), Ă©crire ces donnĂ©es sur un systĂšme de stockage distant (đĄ,đą,đ”,đŽ,â«) |
Faire des statistiques agrĂ©gĂ©es par catĂ©gories | Utiliser Pandas (đĄ,đą,đ”) ou ÌDuckDB (đŽ,â«) pour faire des statistiques par groupe |
Trouver un produit dans OpenFoodFacts Ă partir dâun code barre |
DĂ©tection visuelle dâun code barre (đĄ,đą,đ”, đŽ,â«), rechercher des donnĂ©es avec des critĂšres dâappariement exact comme le code barre via Pandas (đĄ,đą,đ”) ou ÌDuckDB (đŽ,â«) ou via des distances textuelles (đŽ,â«) |
Encapsuler ces Ă©tapes dans une application Streamlit |
Tester une application Streamlit minimale (đĄ,đą,đ”, đŽ,â«), personnaliser celle-ci (đŽ,â« ou đĄ,đą,đ” dĂ©sirant se focaliser sur Streamlit ) |
Mettre en production cette application | DĂ©ployer grĂące Ă des serveurs standardisĂ©s une application Streamlit (đŽ,â«) ou proposer une version sans serveur (â« voulant se familiariser Ă Observable ) |
Le dĂ©veloppement Ă proprement parler de lâapplication est donc assez tardif car un certain nombre dâĂ©tapes prĂ©alables sont nĂ©cessaires pour ne pas avoir une application monolithique (ce qui est une bonne pratique). Si vous nâĂȘtes intĂ©ressĂ©s que par dĂ©velopper une application Streamlit
, vous pouvez directement passer aux Ă©tapes concernĂ©es (Ă partir de la partie 3ïž).
La premiĂšre Ă©tape (1ïžâŁ RĂ©cupĂ©ration et nettoyage de la base OpenFoodFacts
) peut ĂȘtre assez chronophage. Cela est assez reprĂ©sentatif des projets de data science oĂč la majoritĂ© du temps est consacrĂ©e Ă la structuration et la manipulation de donnĂ©es. La deuxiĂšme Ă©tape (2ïž âFaire des statistiques agrĂ©gĂ©es par catĂ©goriesâ) est la moins centrale de ce sujet : si vous manquez de temps vous pouvez la passer et utiliser directement les morceaux de code mis Ă disposition.
Cette page peut ĂȘtre consultĂ©e par diffĂ©rents canaux :
Jupyter
, les solutions de la voie đĄ sont affichĂ©es par dĂ©faut. Elles peuvent ĂȘtre cachĂ©es en faisant View
> Collapse All Code
Notre source de référence sera OpenFoodFacts
, une base contributive sur les produits alimentaires.
OpenFoodFacts
Comme nous allons utiliser fréquemment certains paramÚtres, une bonne pratique consiste à les stocker dans un fichier dédié, au format YAML
et dâimporter celui-ci via Python
. Ceci est expliquĂ© dans ce cours de lâENSAE
Nous proposons de créer le fichier suivant au nom config.yaml
:
URL_OPENFOOD_RAW: "https://static.openfoodfacts.org/data/en.openfoodfacts.org.products.csv.gz"
URL_OPENFOOD: "https://minio.lab.sspcloud.fr/projet-funathon/2023/sujet4/diffusion/openfood.csv.gz"
ENDPOINT_S3: "https://minio.lab.sspcloud.fr"
BUCKET: "projet-funathon"
DESTINATION_DATA_S3: "/2023/sujet4/diffusion"
URL_FASTTEXT_MINIO: "https://minio.lab.sspcloud.fr/projet-funathon/2023/sujet4/diffusion/model_coicop10.bin"
URL_COICOP_LABEL: "https://www.insee.fr/fr/statistiques/fichier/2402696/coicop2016_liste_n5.xls"
â ïž Si vous dĂ©sirez pouvoir reproduire tous les exemples de ce fichier, vous devez changer la variable BUCKET
pour mettre votre nom dâutilisateur sur le SSPCloud
.
Nous allons lire ce fichier avec le package adapté pour transformer ces instructions en variables Python
(stockées dans un dictionnaire),
# Solution pour voie đĄ
import yaml
def import_yaml(filename: str) -> dict:
"""
Importer un fichier YAML
Args:
filename (str): Emplacement du fichier
Returns:
dict: Le fichier YAML sous forme de dictionnaire Python
"""
with open(filename, "r", encoding="utf-8") as stream:
= yaml.safe_load(stream)
config return config
"config.yaml") import_yaml(
Il est recommandĂ© pour la suite de copier-coller la fonction crĂ©Ă©e (ne pas oublier les imports associĂ©s) dans un fichier Ă lâemplacement utils/import_yaml.py
. Cette approche modulaire est une bonne pratique, recommandĂ©e dans ce cours de lâENSAE.
Pour la voie đĄ, ce fichier a dĂ©jĂ Ă©tĂ© crĂ©Ă© pour vous. Le tester de la maniĂšre suivante:
# Solution pour voie đĄ
from utils.import_yaml import import_yaml
= import_yaml("config.yaml") config
OpenFoodFacts
(đĄ,đą,đ”,đŽ,â«)Un export quotidien de la base de donnĂ©es OpenFoodFacts
est fourni au format CSV
. LâURL est le suivant:
"URL_OPENFOOD"] config[
Il est possible dâimporter de plusieurs maniĂšres ce type de fichier avec Python
. Ce quâon propose ici, câest de le faire en deux temps, afin dâavoir un contrĂŽle des options mises en oeuvre lors de lâimport (notamment le format de certaines variables) :
requests
pour tĂ©lĂ©charger le fichier et lâĂ©crire, de maniĂšre intermĂ©diaire, sur le disque local ;pandas
avec quelques options pour importer le fichier puis le manipuler.# Solution pour voie đĄ
from utils.preprocess_openfood import download_openfood, import_openfood
= "openfood.csv.gz")
download_openfood(destination = import_openfood("openfood.csv.gz", usecols = config['variables'])
openfood 'code', 'product_name', 'energy-kcal_100g', 'nutriscore_grade']].sample(5, random_state = 12345) openfood.loc[:, [
Lâobjectif de lâapplication est de proposer pour un produit donnĂ© quelques statistiques descriptives. On propose de se focaliser sur trois scores :
Ces scores ne sont pas systématiquement disponibles sur OpenFoodFacts
mais une part croissante des données présente ces informations (directement renseignées ou imputées).
= ['nutriscore_grade', 'nova_group', 'ecoscore_grade'] indices_synthetiques
Le bloc de code ci-dessous propose dâharmoniser le format de ces scores pour faciliter la reprĂ©sentation graphique ultĂ©rieure.
Comme il ne sâagit pas du coeur du sujet, il est donnĂ© directement Ă tous les parcours. Le code source de cette fonction est disponible dans le module utils.pipeline
:
import pandas as pd
from utils.pipeline import clean_note
= ['nutriscore_grade', 'nova_group', 'ecoscore_grade']
indices_synthetiques
= pd.concat(
openfood.loc[:, indices_synthetiques] "wide") for s in indices_synthetiques],
[clean_note(openfood, s, = 1
axis )
Pour proposer sur notre application quelques statistiques pertinentes sur le produit, nous allons associer chaque ligne dâOpenFoodFacts
Ă un type de produit dans la COICOP
pour pouvoir comparer un produit Ă des produits similaires.
Nous allons ainsi utiliser le nom du produit pour infĂ©rer le type de bien dont il sâagit.
Pour cela, dans les parcours đĄ,đą et đ”, nous allons dâutiliser un classifieur expĂ©rimental proposĂ© sur Github InseeFrLab/predicat
qui a été entrainé sur cette tùche sur un grand volume de données (non spécifiquement alimentaires).
Pour les parcours đŽ et â«, nous proposons Ă©galement dâutiliser ce classifieur. NĂ©anmoins, une voie bis est possible pour entraĂźner soi-mĂȘme un classifieur en utilisant la catĂ©gorisation des donnĂ©es disponible directement dans OpenFoodFacts
. Il est proposĂ© dâutiliser Fasttext
(une librairie spécialisée open-source, développée par Meta
il y a quelques annĂ©es) dans le cadre de la voie đŽ. Les personnes suivant la voie â« sont libres dâutiliser nâimporte quel framework de classification, par exemple un modĂšle disponible sur HuggingFace.
Dans un premier temps, on rĂ©cupĂšre les fonctions permettant dâappliquer sur nos donnĂ©es le mĂȘme preprocessing que celui qui a Ă©tĂ© mis en oeuvre lors de lâentraĂźnement du modĂšle:
# Solution pour voie đĄ et đą
from utils.download_pb import download_pb
"https://raw.githubusercontent.com/InseeFrLab/predicat/master/app/utils_ddc.py", "utils/utils_ddc.py") download_pb(
Pour observer les nettoyages de champs textuels mis en oeuvre, les lignes suivantes peuvent ĂȘtre exĂ©cutĂ©es:
from utils.utils_ddc import replace_values_ean
replace_values_ean
Pour effectuer des remplacements dans des champs textuels, le plus simple est dâutiliser les expressions rĂ©guliĂšres (regex
). Vous pouvez trouver une ressource complĂšte sur le sujet dans ce cours de Python
de lâENSAE.
Deux options sâoffrent Ă nous:
re
et boucler sur les lignesPandas
Nous privilégierons la deuxiÚme approche, plus naturelle quand on utilise des DataFrames
et plus efficace puisquâelle est nativement intĂ©grĂ©e Ă Pandas
.
La syntaxe prend la forme suivante :
=True) data.replace({variable: dict_rules_replacement}, regex
Câest celle qui est implĂ©mentĂ©e dans la fonction ad hoc du script utils/preprocess_openfood.py
. Cette derniĂšre sâutilise de la maniĂšre suivante:
from utils.utils_ddc import replace_values_ean
from utils.preprocess_openfood import clean_column_dataset
= clean_column_dataset(
openfood
openfood, replace_values_ean,"product_name", "preprocessed_labels"
)
Voici quelques cas oĂč notre nettoyage de donnĂ©es a modifiĂ© le nom du produit :
(openfood= ["product_name", "preprocessed_labels"])
.dropna(subset
.loc["product_name"].str.upper() != openfood["preprocessed_labels"],
openfood["product_name", "preprocessed_labels"]
[
] )
On peut remarquer que pour aller plus loin et amĂ©liorer la normalisation des champs, il serait pertinent dâappliquer un certain nombre de nettoyages supplĂ©mentaires, comme le retrait des mots de liaison (stop words). Des exemples de ce type de nettoyages sont prĂ©sents dans le cours de Python
de lâENSAE.
Cela est laissĂ© comme exercice aux voies đŽ et â«.
On peut maintenant se tourner vers la classification Ă proprement parler. Pour celle-ci, on propose dâutiliser un modĂšle qui a Ă©tĂ© entrainĂ© avec la librairie Fasttext
. Voici comment récupérer le modÚle et le tester sur un exemple trÚs basique:
from utils.download_pb import download_pb
import os
import fasttext
if os.path.exists("fasttext_coicop.bin") is False:
download_pb(= config["URL_FASTTEXT_MINIO"],
url = "fasttext_coicop.bin"
fname
)
= fasttext.load_model("fasttext_coicop.bin")
model "RATATOUILLE") model.predict(
Le rĂ©sultat est peu intelligible. En effet, cela demande une bonne connaissance de la COICOP pour savoir de maniĂšre intuitive que cela correspond Ă la catĂ©gorie âAutres plats cuisinĂ©s Ă base de lĂ©gumesâ.
Avant de gĂ©nĂ©raliser le classifieur Ă lâensemble de nos donnĂ©es, on se propose donc de rĂ©cupĂ©rer les noms des COICOP depuis le site insee.fr. Comme cela ne prĂ©sente pas de dĂ©fi majeur, le code est directement proposĂ©, quelle que soit la voie empruntĂ©e:
def import_coicop_labels(url: str) -> pd.DataFrame:
= pd.read_excel(url, skiprows=1)
coicop 'Code'] = coicop['Code'].str.replace("'", "")
coicop[= coicop.rename({"Libellé": "category"}, axis = "columns")
coicop return coicop
= import_coicop_labels(
coicop "https://www.insee.fr/fr/statistiques/fichier/2402696/coicop2016_liste_n5.xls"
)
# Verification de la COICOP rencontrée plus haut
"Code"].str.contains("01.1.7.3.2")] coicop.loc[coicop[
Maintenant nous avons tous les ingrĂ©dients pour gĂ©nĂ©raliser notre approche. Lâapplication en sĂ©rie de prĂ©dictions via Fasttext
Ă©tant un peu fastidieuse et peu Ă©lĂ©gante (elle nĂ©cessite dâĂȘtre Ă lâaise avec les listes Python
) et nâĂ©tant pas le centre de notre sujet, la fonction suivante est fournie pour effectuer cette opĂ©ration :
def model_predict_coicop(data, model, product_column: str = "preprocessed_labels", output_column: str = "coicop"):
= pd.DataFrame(
predictions
{\
output_column: 0] for k in model.predict(
[k[str(libel) for libel in data[product_column]], k = 1
[0]]
)[
})
= predictions[output_column].str.replace(r'__label__', '')
data[output_column] return data
= model_predict_coicop(openfood, model) openfood
predicat
(đĄ,đą,đ”,đŽ,â«)Lâutilisation dâAPI pour accĂ©der Ă des donnĂ©es devient de plus en plus frĂ©quente. Si vous ĂȘtes peu familiers avec les API, vous pouvez consulter ce chapitre du cours de Python
de lâENSAE ou de la documentation utilitR
(langage R
)
Les API peuvent servir Ă faire beaucoup plus que rĂ©cupĂ©rer des donnĂ©es. Elles sont notamment de plus en plus utilisĂ©es pour rĂ©cupĂ©rer des prĂ©dictions dâun modĂšle. La plateforme HuggingFace
est trÚs appréciée pour cela: elle a grandement facilité la réutilisation de modÚles mis en disposition en open source. Cette approche a principalement deux avantages:
DataFrame
.Ici, nous proposons de tester une API mise à disposition de maniÚre expérimentale pour faciliter la réutilisation de notre modÚle de classification dans la nomenclature COICOP.
Cette API sâappelle predicat
et son code source est disponible sur Github
.
Pour les parcours đĄ,đą,đ”, nous suggĂ©rons de se cantonner Ă tester quelques exemples. Pour les parcours đŽ et â« qui voudraient se tester sur les API, nous proposons de gĂ©nĂ©raliser ces appels Ă predicat
pour classifier toutes nos données.
Voici, pour les parcours đĄ,đą,đ”, un exemple dâutilisation:
import requests
def predict_from_api(product_name):
= f"https://api.lab.sspcloud.fr/predicat/label?k=1&q=%27{product_name}%27"
url_api = requests.get(url_api).json()
output_api_predicat = output_api_predicat['coicop'][f"'{product_name}'"][0]['label']
coicop_found return coicop_found
"Ratatouille") predict_from_api(
Pour le parcours đ”, voici un exercice pour tester sur un Ă©chantillon des donnĂ©es de lâOpenFoodFacts
Les grimpeurs des voies đŽ et â« sont encouragĂ©s Ă essayer dâentraĂźner eux-mĂȘmes un modĂšle de classification.
Le fait dâavoir effectuĂ© en amont ce type dâopĂ©ration permettra dâĂ©conomiser du temps par la suite puisquâon sâĂ©vite des calculs Ă la volĂ©e coĂ»teux en performance (rien de pire quâune page web qui rame non ?).
Pour facilement retrouver ces donnĂ©es, on propose de les Ă©crire dans un espace de stockage accessible facilement. Pour cela, nous proposons dâutiliser celui du SSP Cloud
pour les personnes ayant un compte dessus. Pour les personnes nâayant pas de compte sur le SSP Cloud
, vous pouvez passer cette étape et réutiliser le jeu de données que nous proposons pour la suite de ce parcours.
Nous proposons ici dâutiliser le package s3fs
qui est assez pratique pour traiter un espace distant comme on ferait dâun espace de stockage local. Pour en apprendre plus sur le systĂšme de stockage S3
(la technologie utilisée par le SSP Cloud) ou sur le format Parquet
, vous pouvez consulter ce chapitre du cours de Python
de lâENSAE
La premiÚre étape consiste à initialiser la connexion (créer un file system distant, via s3fs.S3FileSystem
, qui pointe vers lâespace de stockage du SSP Cloud). La deuxiĂšme ressemble beaucoup Ă lâĂ©criture dâun fichier en local, il y a seulement une couche dâabstraction supplĂ©mentaire avec fs.open
:
from utils.import_yaml import import_yaml
import s3fs
= import_yaml("config.yaml")
config = f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/openfood.parquet"
DESTINATION_OPENFOOD
# Initialisation de la connexion
= s3fs.S3FileSystem(
fs ={"endpoint_url": config["ENDPOINT_S3"]}
client_kwargs
)
# Ecriture au format parquet sur l'espace de stockage distant
with fs.open(DESTINATION_OPENFOOD, "wb") as file_location:
openfood.to_parquet(file_location)
â ïž Il faut avoir modifiĂ© la valeur de BUCKET
dans le fichier config.yaml
pour que cette commande fonctionne.
Enfin, pour rendre ce fichier accessible Ă votre future application, il est nĂ©cessaire dâĂ©diter la cellule ci-dessous pour remplacer <USERNAME_SSPCLOUD>
par votre nom dâutilisateur sur le SSPCloud
puis dâexĂ©cuter la cellule suivante qui va permettre de rendre ce fichier public. :
# â ïž modifier ci-dessous pour remplacer USERNAME_SSPCLOUD par votre nom d'utilisateur sur le SSPCloud
!mc anonymous set download s3/<USERNAME_SSPCLOUD>/2023/sujet4/diffusion
â ïž Il faut avoir modifiĂ© la valeur de USERNAME_SSPCLOUD
dans la commande pour que cela fonctionne.
Le fichier sera ainsi disponible en téléchargement directement depuis un URL de la forme:
https://minio.lab.sspcloud.fr/
/2023/sujet4/diffusion/openfood.parquet
Cette partie permet de calculer en amont de lâapplication des statistiques descriptives qui pourront ĂȘtre utilisĂ©es par celle-ci.
Il est prĂ©fĂ©rable de minimiser la quantitĂ© de calculs faits Ă la volĂ©e dans le cadre dâune application. Sinon, le risque est une latence embĂȘtante pour lâutilisateur voire un crash du serveur Ă cause de besoins de ressources trop importants.
Cette partie propose ainsi de crĂ©er en avance une base de donnĂ©es synthĂ©tisant le nombre de produits dans une catĂ©gorie donnĂ©e (par exemple les fromages Ă pĂąte crue) qui partagent la mĂȘme note. Cela nous permettra dâafficher des statistiques personnalisĂ©es sur les produits similaires Ă celui quâon scanne.
Sur le plan technique, cette partie propose deux cadres de manipulation de données différents, selon le balisage de la voie:
Pandas
Parquet
grĂące Ă DuckDB
La deuxiĂšme approche permet de mettre en oeuvre des calculs plus efficaces (DuckDB
) est plus rapide mais nĂ©cessite un peu plus dâexpertise sur la manipulation de donnĂ©es, notamment des connaissances en SQL.
Cette partie va fonctionner en trois temps:
OpenFoodFacts
prĂ©cĂ©demment produitesLes Ă©tapes 1 et 2 sont sĂ©parĂ©es conceptuellement pour les parcours đĄ,đą,đ”. Pour les parcours đŽ et â«, lâutilisation de requĂȘtes SQL fait que ces deux Ă©tapes conceptuelles sont intriquĂ©es. Les parcours đĄ,đą,đ” peuvent observer les morceaux de code proposĂ©s dans le cadre đŽ et â«, câest assez instructif. LâĂ©tape 3 (production de graphiques) sera la mĂȘme pour tous les parcours.
Nous proposons dâimporter Ă nouveau nos configurations:
from utils.import_yaml import import_yaml
= import_yaml("config.yaml") config
Les colonnes suivantes nous seront utiles dans cette partie:
= [
indices_synthetiques "nutriscore_grade", "ecoscore_grade", "nova_group"
]= ['product_name', 'code', 'preprocessed_labels', 'coicop'] principales_infos
Voici, Ă nouveau, la configuration pour permettre Ă Python
de communiquer avec lâespace de stockage distant:
import s3fs
= import_yaml("config.yaml")
config = f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/openfood.parquet"
INPUT_OPENFOOD
# Initialisation de la connexion
= s3fs.S3FileSystem(
fs ={"endpoint_url": config["ENDPOINT_S3"]}
client_kwargs )
Pandas
(đĄ,đą,đ”)Il est recommandĂ© pour les parcours đĄ, đą, đ” de travailler avec Pandas
pour construire des statistiques descriptives. Cela se fera en deux Ă©tapes:
OpenFoodFacts
(đĄ, đą et đ”)Il est possible de lire un CSV de plusieurs maniĂšres avec Python
. Lâune dâelle se fait Ă travers le context manager. Le module s3fs
permet dâutiliser ce context manager pour lire un fichier distant, de maniĂšre trĂšs similaire Ă la lecture dâun fichier local.
# Solution pour voie đĄ, đą et đ”
import pandas as pd
# methode 1: pandas
with fs.open(INPUT_OPENFOOD, "rb") as remote_file:
= pd.read_parquet(
openfood
remote_file,= principales_infos + \
columns
indices_synthetiques )
Les donnĂ©es ont ainsi lâaspect suivant:
2) openfood.head(
On dĂ©sire calculer pour chaque classe de produits - par exemple les boissons rafraichissantes - le nombre de produits qui partagent une mĂȘme note pour chaque indicateur de qualitĂ© nutritionnelle ou environnementale.
Nous allons utiliser le DataFrame
suivant pour les calculs de notes:
= openfood.loc[:,["coicop"] + indices_synthetiques] openfood_notes
# Solution pour voie đĄ, đą et đ”
def compute_stats_grades(data, indices_synthetiques):
= (
stats_notes
data"coicop")
.groupby('value_counts' for i in indices_synthetiques})
.agg({i:=['coicop', 'note'])
.reset_index(names
)= pd.melt(stats_notes, id_vars = ['coicop','note'])
stats_notes = stats_notes.dropna().drop_duplicates(subset = ['variable','note','coicop'])
stats_notes 'value'] = stats_notes['value'].astype(int)
stats_notes[
return stats_notes
= compute_stats_grades(openfood_notes, indices_synthetiques) stats_notes
DuckDB
(đŽ et â«)Cette partie propose pour les parcours đŽ et â« de reproduire lâanalyse faite par les parcours đĄ,đą et đ” via Pandas
.
DuckDB
va ĂȘtre utilisĂ© pour lire et agrĂ©ger les donnĂ©es. Pour lire directement depuis un systĂšme de stockage distant, sans prĂ©-tĂ©lĂ©charger les donnĂ©es, vous pouvez utiliser la configuration suivante de DuckDB
:
import duckdb
= duckdb.connect(database=':memory:')
con """
con.execute( INSTALL httpfs;
LOAD httpfs;
SET s3_endpoint='minio.lab.sspcloud.fr'
""")
Et voici un exemple minimal de lecture de données à partir du chemin INPUT_OPENFOOD
défini précédemment.
= con.sql(
duckdb_data f"SELECT product_name, preprocessed_labels, coicop, energy_100g FROM read_parquet('s3://{INPUT_OPENFOOD}') LIMIT 10"
)#conversion en pandas dataframe duckdb_data.df()
Nous proposons de crĂ©er une unique requĂȘte SQL qui, dans une clause SELECT
, pour chaque classe de produit (notre variable de COICOP), compte le nombre de produits qui partagent une mĂȘme note.
# Solution Ă la voie đŽ et â« pour les curieux de la voie đĄ, đą et đ”
def count_one_variable_sql(con, variable, path_within_s3 = "temp.parquet"):
= f"SELECT coicop, {variable} AS note, COUNT({variable}) AS value FROM read_parquet('s3://{path_within_s3}') GROUP BY coicop, {variable}"
query = con.sql(query).df().dropna()
stats_one_variable 'variable'] = variable
stats_one_variable[= stats_one_variable.replace('', 'NONE')
stats_one_variable
return stats_one_variable
= ["nutriscore_grade", "ecoscore_grade", "nova_group"]
grades = [count_one_variable_sql(con, note, INPUT_OPENFOOD) for note in grades]
stats_notes_sql = pd.concat(stats_notes_sql) stats_notes_sql
Ceci nous donne donc le DataFrame
suivant:
2) stats_notes_sql.head(
Ces statistiques descriptives sont Ă Ă©crire dans lâespace de stockage distant pour ne plus avoir Ă les calculer.
def write_stats_to_s3(data, destination):
# Ecriture au format parquet sur l'espace de stockage distant
with fs.open(destination, "wb") as file_location:
data.to_parquet(file_location)
f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/stats_notes_pandas.parquet")
write_stats_to_s3(stats_notes, f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/stats_notes_sql.parquet") write_stats_to_s3(stats_notes_sql,
â ïž Il faut avoir modifiĂ© la valeur de BUCKET
dans le fichier config.yaml
pour que cette commande fonctionne.
On va utiliser Plotly
pour crĂ©er des graphiques et, ultĂ©rieurement, les afficher sur notre page web. Cela permettra dâavoir un peu de rĂ©activitĂ©, câest lâintĂ©rĂȘt de faire un format web plutĂŽt quâune publication figĂ©e comme un PDF
.
Voici un exemple de fonction qui répond aux cahiers des charges ci-dessus:
# Solution pour voie đĄ
import plotly.express as px
import numpy as np
def figure_infos_notes(
= 'nutriscore_grade',
data, variable_note = "01.1.7.3.2", note_produit = "B",
coicop = "Nutriscore"
title
):= data.loc[data['variable'] == variable_note]
example_coicop = example_coicop.loc[example_coicop['coicop']==coicop]
example_coicop 'color'] = np.where(example_coicop['note'] == note_produit, "Note du produit", "Autres produits")
example_coicop[
= px.bar(
fig
example_coicop,='note', y='value', color = "color", template = "simple_white",
x=title,
title={"Note produit": "red", "Autres produits": "royalblue"},
color_discrete_map={
labels"note": "Note",
"value": ""
}
)
fig.update_xaxes(='array',
categoryorder= ['A', 'B', 'C', 'D', 'E'])
categoryarray=False)
fig.update_layout(showlegend="x")
fig.update_layout(hovermode
fig.update_traces(="<br>".join([
hovertemplate"Note %{x}",
f"{variable_note}: " +" %{y} produits"
])
)
return fig
Voici un exemple dâutilisation
from utils.construct_figures import figure_infos_notes
= figure_infos_notes(stats_notes)
fig =800, height=400)
fig.update_layout(width
fig
Tout ce travail prĂ©liminaire nous permettra dâafficher sur notre application des statistiques propres Ă chaque catĂ©gorie.
On propose dâutiliser le jeu de donnĂ©es prĂ©parĂ© prĂ©cedemment
= [
indices_synthetiques "nutriscore_grade", "ecoscore_grade", "nova_group"
]= ['product_name', 'code', 'preprocessed_labels', 'coicop']
principales_infos = principales_infos + indices_synthetiques
liste_colonnes = [f"\"{s}\"" for s in liste_colonnes]
liste_colonnes_sql = ', '.join(liste_colonnes_sql) liste_colonnes_sql
On va aussi utiliser la nomenclature COICOP qui peut ĂȘtre importĂ©e via le code ci-dessous:
from utils.download_pb import import_coicop_labels
= import_coicop_labels(
coicop "https://www.insee.fr/fr/statistiques/fichier/2402696/coicop2016_liste_n5.xls"
)
La premiĂšre brique de notre application consiste Ă repĂ©rer un produit par le scan du code-barre. Nous allons partir pour le moment dâun produit dâexemple, ci-dessous:
= "https://images.openfoodfacts.org/images/products/500/011/260/2791/front_fr.4.400.jpg" url_image
Dans le cadre de notre application, on permettra aux utilisateurs dâuploader la photo dâun produit, ce sera plus fun. En attendant notre application, partir dâun produit standardisĂ© permet dĂ©jĂ de mettre en oeuvre la logique Ă rĂ©-appliquer plus tard.
Pour se simplifier la vie, le plus simple pour repĂ©rer un code-barre est dâutiliser le package pyzbar
. Pour transformer une image en matrice Numpy
(lâobjet attendu par pyzbar
), on peut utiliser le module skimage
de la maniĂšre suivante:
from skimage import io
io.imread(url_image)
GrĂące Ă sklearn.image
, on peut utiliser lâURL dâune page web ou le chemin dâun fichier de maniĂšre indiffĂ©rente pour la valeur de url_image
.
# Solution pour voie đĄ
from pyzbar import pyzbar
def extract_ean(url, verbose=True):
= io.imread(url)
img = pyzbar.decode(img)
decoded_objects if verbose is True:
for obj in decoded_objects:
# draw the barcode
print("detected barcode:", obj)
# print barcode type & data
print("Type:", obj.type)
print("Data:", obj.data)
return decoded_objects
= extract_ean(url_image, verbose = False)
obj
0].data.decode() obj[
On obtient bien un code identifiant notre produit. Il sâagit de lâEAN qui est un identifiant unique, partagĂ© quelque soit le point de vente dâun produit. Il sâagit dâun identifiant prĂ©sent sur tout code-barre, utilisĂ© dans les systĂšmes dâinformation des grandes enseignes mais aussi dans les bases produits qui peuvent ĂȘtre utilisĂ©es de maniĂšre annexe (par exemple lâOpenFoodFacts
).
OpenFoodFacts
(đĄ,đą,đ”,đŽ,â«)Maintenant quâon dispose dâun code-barre (le numĂ©ro EAN), on va trouver le produit dans OpenFoodFacts
Ă partir de ce code-barre.
Cependant, comme il peut arriver quâun produit dispose dâinformations incomplĂštes, il peut ĂȘtre utile de faire non seulement de lâappariement exact (trouver le produit avec le mĂȘme code EAN) mais aussi de lâappariement flou (trouver un produit avec un nom proche de celui quâon veut).
Ceci est un exercice pour les parcours đŽ et â«, les autres voies pouvant prendre cette fonction comme donnĂ©e.
Pour aller plus loin sur cette question des appariements flous, il pourrait ĂȘtre utile dâaller vers ElasticSearch
. Câest nĂ©anmoins un sujet en soi, nous proposons donc aux curieux de consulter cette ressource.
Voici lâEAN dâexemple :
= "5000112602999" ean
Pour avoir un outil performant, on propose dâutiliser DuckDB
pour lire et filtrer les donnĂ©es. Cela sera plus performant que lire, Ă chaque fois que lâutilisateur de notre application upload une image, un gros fichier (2 millions de ligne) pour nâen garder quâune.
Voici la configuration Ă mettre en oeuvre:
import duckdb
= duckdb.connect(database=':memory:')
con """
con.execute( INSTALL httpfs;
LOAD httpfs;
SET s3_endpoint='minio.lab.sspcloud.fr'
""")
= "https://projet-funathon.minio.lab.sspcloud.fr/2023/sujet4/diffusion/openfood.parquet" url_data
Pour commencer, effectuons une requĂȘte SQL pour rĂ©cupĂ©rer le produit correspondant au code-barre quâon a scannĂ©:
Voici la solution:
# Solution pour voie đĄ
def get_product_ean(con, ean, url_data, liste_colonnes_sql):
= con.sql(
openfood_produit f"SELECT {liste_colonnes_sql} FROM read_parquet('{url_data}') WHERE CAST(ltrim(code, '0') AS STRING) = CAST(ltrim({ean}) AS STRING)"
).df()return openfood_produit
On va néanmoins intégrer ceci dans un pipeline plus général:
Voici la fonction qui permet dâimplĂ©menter la deuxiĂšme partie:
# Solution pour voie đĄ
import numpy as np
import pandas as pd
from utils.pipeline import clean_note
def fuzzy_matching_product(openfood_produit, product_name, con, url_data, liste_colonnes_sql, indices_synthetiques):
= con.sql(f"SELECT {liste_colonnes_sql} from read_parquet('{url_data}') WHERE jaro_winkler_similarity('{product_name}',product_name) > 0.9 AND \"energy-kcal_100g\" IS NOT NULL")
out_textual = out_textual.df()
out_textual
= pd.concat(
out_textual_imputed
["code", "product_name", "coicop"]].reset_index(drop = True),
openfood_produit.loc[:, ["NONE","").replace('',np.nan).mode(dropna=True))
pd.DataFrame(out_textual.loc[:, indices_synthetiques].replace(=True, axis=1
], ignore_index
)= ["code", "product_name", "coicop"] + indices_synthetiques
out_textual_imputed.columns
return out_textual_imputed
Voici finalement le pipeline mis en oeuvre par une fonction :
# Solution pour voie đĄ
def find_product_openfood(con, liste_colonnes_sql, url_data, ean):
= con.sql(
openfood_produit f"SELECT {liste_colonnes_sql} FROM read_parquet('{url_data}') WHERE CAST(ltrim(code, '0') AS STRING) = CAST(ltrim({ean}) AS STRING)"
).df()
= openfood_produit["product_name"].iloc[0]
product_name
if openfood_produit['nutriscore_grade'].isin(['NONE','']).iloc[0]:
= fuzzy_matching_product(
openfood_produit
openfood_produit, product_name, con, url_data,
liste_colonnes_sql, indices_synthetiques)= openfood_produit.merge(coicop, left_on = "coicop", right_on = "Code")
openfood_produit
return openfood_produit
Qui peut ĂȘtre finalisĂ© de la maniĂšre suivante:
= find_product_openfood(
openfood_produit
con, liste_colonnes_sql,
url_data, ean
)2) openfood_produit.head(
La derniÚre partie du prototypage consiste à enrober nos fonctions de production de graphiques dans une fonction plus générique.
Pour rappel, lâimport des donnĂ©es se fait de la maniĂšre suivante:
= pd.read_parquet(
stats_notes "https://minio.lab.sspcloud.fr/projet-funathon/2023/sujet4/diffusion/stats_notes_pandas.parquet"
)
Dans notre application, nous allons utiliser cette fonction:
from utils.construct_figures import figure_infos_notes
= 'nutriscore_grade'
variable
def plot_product_info(
data, variable,
stats_notes):
= figure_infos_notes(
fig
stats_notes,= variable,
variable_note = data['coicop'].iloc[0],
coicop = data[variable].iloc[0],
note_produit = variable.split("_")[0].capitalize()
title
)
return fig
= plot_product_info(openfood_produit, variable, stats_notes)
fig =800, height=400)
fig.update_layout(width fig
= plot_product_info(openfood_produit, "ecoscore_grade", stats_notes)
fig =800, height=400)
fig.update_layout(width fig
Cette partie vise Ă assembler les briques prĂ©cĂ©dentes afin de les rendre facilement accessibles Ă un utilisateur final. Pour cela, nous allons construire une application interactive Ă lâaide du framework Streamlit
en Python
.
Lâobjectif est de crĂ©er une application sur le modĂšle de myyuka.lab.sspcloud.fr/. Voici une petite vidĂ©o de dĂ©monstration de lâapplication:
from IPython.display import HTML
"""
HTML( <video width="520" height="240" alt="test" controls>
<source src="https://minio.lab.sspcloud.fr/projet-funathon/2023/sujet4/diffusion/video_out.webm" type="video/mp4">
</video>
""")
Selon le parcours suivi, la construction de cette application sera plus ou moins guidée.
Il est rare dâavoir une application fonctionnelle du premier coup, cela peut demander beaucoup dâessai-erreur pour parvenir Ă ses fins. Il est donc utile de rĂ©guliĂšrement lancer lâapplication pour la tester. Cela se fait en lançant un serveur local, câest-Ă -dire en crĂ©ant une tĂąche qui fonctionne en arriĂšre-plan et qui va crĂ©er une interaction entre un navigateur et du code Python
.
Pour lancer ce serveur web local plusieurs méthodes sont possibles sur le SSP Cloud
, en partant du principe que votre application est stockée dans un fichier app.py
+
dans le menu Ă gauche de Jupyter
et exécuter, dans le bon dossier de travail, streamlit run app.py --server.port 5000 --server.address 0.0.0.0
Jupyter
, il suffit dâexĂ©cuter la cellule suivante:!streamlit run app.py --server.port 5000 --server.address 0.0.0.0
Remarque: si vous nâĂȘtes pas sur le SSP Cloud
, vous pouvez retirer lâoption --server.address 0.0.0.0
.
Il reste Ă accĂ©der au navigateur sur lequel lâapplication a Ă©tĂ© dĂ©ployĂ©e. Sur un poste local, vous ouvririez lâURL localhost:5000
sur votre navigateur. Pour accéder à votre application depuis le SSP Cloud, il va falloir y accéder différemment.
My Services
.README
pour accéder à des informations sur le service Jupyter
ouvert.Il faut ensuite cliquer sur le lien ci-dessous:
Cela va ouvrir un nouvel onglet sur votre navigateur oĂč, cette fois, vous aurez lâapplication. Chaque action que vous effectuerez sur celle-ci dĂ©clenchera une opĂ©ration dans la
ligne de commande que vous avez lancée.
Pour le parcours đĄ, la voie sâarrĂȘte Ă ce niveau. Vous pouvez nĂ©anmoins basculer du cĂŽtĂ© de la voie đą pour apprendre de maniĂšre guidĂ©e Ă crĂ©er votre application Streamlit
.
Pour les parcours đą,đ”,đŽ et â«, vous allez pouvoir crĂ©er vous-mĂȘme lâapplication, de maniĂšre plus ou moins guidĂ©e.
Voici la gradation des niveaux pour crĂ©er lâapplication:
app.py
qui gĂ©nĂšre lâapplicationapp.py
, mettre en oeuvre lâapplication avec des consignes guidĂ©esapp.py
, mettre en oeuvre lâapplication Ă partir dâun cachier des charges dĂ©taillĂ©app.py
, mettre en oeuvre lâapplication uniquement Ă partir de lâexemple sur myyuka.lab.sspcloud.fr/ et de la vidĂ©o prĂ©cĂ©demment prĂ©sentĂ©e. IdĂ©alement, faire en sorte que le contenu du site soit responsive câest-Ă -dire quâil soit bien adaptĂ© Ă la taille de lâĂ©cran.Voici une proposition dâapplication, afin de reproduire en local le contenu de myyuka.lab.sspcloud.fr/.
# Solution pour la voie đą
with open('app.py', 'r') as file:
= file.read()
app_content
print(
app_content )
import streamlit as st
from streamlit_javascript import st_javascript
import cv2
import pandas as pd
import duckdb
from utils.detect_barcode import extract_ean, visualise_barcode
from utils.pipeline import find_product_openfood
from utils.construct_figures import plot_product_info
from utils.utils_app import local_css, label_grade_formatter
from utils.download_pb import import_coicop_labels
# Une personnalisation sympa pour l'onglet
st.set_page_config(page_title="PYuka", page_icon="đ")
# --------------------
# METADATA
indices_synthetiques = [
"nutriscore_grade", "ecoscore_grade", "nova_group"
]
principales_infos = [
'product_name', 'code', 'preprocessed_labels', 'coicop', \
'url', 'image_url'
]
liste_colonnes = principales_infos + indices_synthetiques
liste_colonnes_sql = [f"\"{s}\"" for s in liste_colonnes]
liste_colonnes_sql = ', '.join(liste_colonnes_sql)
con = duckdb.connect(database=':memory:')
con.execute("""
INSTALL httpfs;
LOAD httpfs;
SET s3_endpoint='minio.lab.sspcloud.fr'
""")
# LOAD DATASET
url_data = "https://projet-funathon.minio.lab.sspcloud.fr/2023/sujet4/diffusion/openfood.parquet"
stats_notes = pd.read_parquet("https://minio.lab.sspcloud.fr/projet-funathon/2023/sujet4/diffusion/stats_notes_pandas.parquet")
coicop = import_coicop_labels(
"https://www.insee.fr/fr/statistiques/fichier/2402696/coicop2016_liste_n5.xls"
)
# --------------------
st.title('Mon Yuka đ„ avec Python đ')
# Feuille de style & taille de l'Ă©cran pour adapter l'interface
local_css("style.css")
width = st_javascript(
"window.innerWidth"
)
# --------------------------------
# PARTIE 1: LES INPUTS
if width > 500:
# pour les grands Ă©crans on met une partie Ă gauche
# qui centralise plusieurs type d'input
with st.sidebar:
# choix de la méthode d'upload
input_method = st.radio(
"MĂ©thode d'upload de la photo",
('Photo enregistrée', 'Capture de la webcam'))
if input_method == 'Photo enregistrée':
# file uploader
input_url = st.file_uploader("Uploaded une photo:", accept_multiple_files=False)
else:
# camera uploader
picture = st.camera_input("Take a picture")
input_url = picture
if input_url is not None:
# visualise l'image s'il y a un input
img = visualise_barcode(input_url)
cv2.imwrite('barcode_opencv.jpg', img)
st.image('barcode_opencv.jpg')
# choix des statistiques Ă afficher
options = st.multiselect(
'Quelles statistiques afficher ?',
["nutriscore_grade", "nova_group", "ecoscore_grade"],
["nutriscore_grade", "nova_group", "ecoscore_grade"],
format_func=label_grade_formatter)
else:
# pour les petits Ă©crans (type smartphone)
# le file uploader est au début
input_method = st.radio(
"MĂ©thode d'upload de la photo",
('Photo enregistrée', 'Capture de la webcam'))
if input_method == 'Photo enregistrée':
# file uploader
input_url = st.file_uploader("Uploaded une photo:", accept_multiple_files=False)
else:
# camera uploader
picture = st.camera_input("Take a picture")
input_url = picture
# choix des statistiques Ă afficher
options = st.multiselect(
'Quelles statistiques afficher ?',
["nutriscore_grade", "nova_group", "ecoscore_grade"],
["nutriscore_grade", "nova_group", "ecoscore_grade"],
format_func=label_grade_formatter)
# ----------------------------------------------------------
# PARTIE 2: EXPLOITATION DES INPUTS DANS NOTRE APP
# CHARGEMENT DE LA LIGNE DANS OPENFOODFACTS
@st.cache_data
def load_data(ean):
openfood_data = find_product_openfood(con, liste_colonnes_sql, url_data, ean, coicop)
return openfood_data
if input_url is None:
# Showcase product
st.write('Produit exemple: Coca-Cola')
subset = load_data("5000112602791")
decoded_objects = extract_ean(subset["image_url"].iloc[0])
else:
# decode image
decoded_objects = extract_ean(input_url)
try:
# we manage to read EAN
ean = decoded_objects[0].data.decode("utf-8")
st.markdown(f'đ __EAN dĂ©tectĂ©__: <span style="color:Red">{ean}</span>', unsafe_allow_html=True)
subset = load_data(ean)
# visualise product
st.markdown(f'Consulter ce produit sur le [site `Openfoodfacts`]({subset["url"].iloc[0]})')
st.image(subset["image_url"].iloc[0])
st.dataframe(subset.loc[:, ~subset.columns.str.contains("url")])
# put some statistics
t = f"<div>Statistiques parmi les <span class='highlight blue'>{subset['category'].iloc[0]}<span class='bold'>COICOP</span>"
st.markdown(t, unsafe_allow_html=True)
# images
for var in options:
fig = plot_product_info(subset, var, stats_notes)
st.plotly_chart(fig, height=800, use_container_width=True)
except:
# we don't
st.write('đš ProblĂšme de lecture de la photo, essayez de mieux cibler le code-barre')
st.image("https://i.kym-cdn.com/entries/icons/original/000/025/458/grandma.jpg")
Pour le parcours đą, la voie sâarrĂȘte Ă ce niveau. Vous pouvez nĂ©anmoins basculer du cĂŽtĂ© de la voie đ” pour apprendre de maniĂšre guidĂ©e Ă mettre en production votre travail en dĂ©ployant automatiquement une application.
Pour les parcours đ”,đŽ et â«, vous allez pouvoir dĂ©ployer vous-mĂȘme lâapplication, de maniĂšre plus ou moins guidĂ©e.
Lâapplication construite dans la partie prĂ©cĂ©dente reste pour le moment Ă un niveau local: elle nâest accessible que via lâutilisateur qui lâa dĂ©ployĂ©e et ce sur la machine oĂč elle a Ă©tĂ© dĂ©ployĂ©e. Lâobjectif de cette derniĂšre partie est de dĂ©ployer lâapplication, câest Ă dire de la rendre accessible en continu Ă nâimporte quel utilisateur. Pour cela, on va devoir sâintĂ©resser Ă la technologie des conteneurs, qui est Ă la base des infrastructures de production modernes.
Le fait de lancer ce notebook via un simple lien de lancement nous a permis de commencer Ă travailler directement, sans trop nous soucier de lâenvironnement de dĂ©veloppement dans lequel on se trouvait.
Mais dĂšs lors que lâon souhaite passer de son environnement de dĂ©veloppement Ă un environnement de production, il est nĂ©cessaire de se poser un ensemble de questions pour sâassurer que le projet fonctionne ailleurs que sur sa machine personnelle :
Python
Ă installer pour que le projet fonctionne ?Python
utilisés par le projet et quelles sont leurs versions ?Python
sâinstallent correctement ?La technologie standard pour assurer la portabilitĂ© dâun projet, câest Ă dire de fonctionner sur diffĂ©rents environnements informatiques, est celle des conteneurs. SchĂ©matiquement, il sâagit de boĂźtes virtuelles qui contiennent lâensemble de lâenvironnement (librairies systĂšmes, interprĂ©teur Python
, code applicatif, configurationâŠ) permettant de faire tourner lâapplication, tout en restant lĂ©gĂšres et donc faciles Ă redistribuer. En fait, chaque service lancĂ© sur le SSP Cloud
est un conteneur, et ce notebook tourne donc lui-mĂȘme⊠dans un conteneur !
Lâenjeu de cette partie est donc de dĂ©voiler pas Ă pas la boĂźte noire afin de comprendre dans quel environnement on se trouve, et comment celui-ci va nous permettre de dĂ©ployer notre application.
SSP Cloud
Maintenant que lâimage de notre application est disponible sur le DockerHub
, elle peut Ă prĂ©sent ĂȘtre rĂ©cupĂ©rĂ©e (pull) et dĂ©ployĂ©e sur nâimporte quel environnement. Dans notre cas, on va la dĂ©ployer sur un cluster Kubernetes
, lâinfrastructure sous-jacente du SSP Cloud
. Le fonctionnement de Kubernetes
est assez technique, mais lâon pourra sâabstraire de certaines parties selon le niveau de difficultĂ© choisi.
Votre application est maintenant dĂ©ployĂ©e, vous pouvez partager cette URL avec nâimporte quel utilisateur dans le monde !
Un dernier challenge pour les amateurs de sensations fortes : crĂ©er la mĂȘme application sur un site web statique grĂące au web assembly (par exemple grĂące Ă Observable
et Quarto
) !
Pour avoir un site web statique, lâidentification du code-barre devra ĂȘtre faite en dehors de lâapplication, par exemple par le moyen dâune API