Mon Yuka đŸ„• avec Python 🐍

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 Onyxia 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 Open In 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 :

Code
pip install -r requirements.txt

Objectif et approche pédagogique

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 :

Code
# Solution pour voie 🟡
print("toto")

Etapes du projet

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.

Remarques

Cette page peut ĂȘtre consultĂ©e par diffĂ©rents canaux :

  • Sur un site web, les codes faisant office de solution sont, par dĂ©fauts, cachĂ©s. Cela peut ĂȘtre pratique de consulter cette page si vous ĂȘtes sur un parcours de couleur diffĂ©rente que le jaune et ne voulez pas voir la solution sans le vouloir ;
  • Sur un notebook Jupyter, les solutions de la voie 🟡 sont affichĂ©es par dĂ©faut. Elles peuvent ĂȘtre cachĂ©es en faisant View > Collapse All Code

Sources et packages utilisés

Notre source de référence sera OpenFoodFacts, une base contributive sur les produits alimentaires.

1ïžâƒŁ RĂ©cupĂ©ration des donnĂ©es OpenFoodFacts

1.1. PrĂ©liminaire (🟡,🟱,đŸ””,🔮,⚫)

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

Code
# 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:
        config = yaml.safe_load(stream)
        return config

import_yaml("config.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:

Code
# Solution pour voie 🟡
from utils.import_yaml import import_yaml
config = import_yaml("config.yaml")

1.2. TĂ©lĂ©charger et nettoyer la base OpenFoodFacts (🟡,🟱,đŸ””,🔮,⚫)

Un export quotidien de la base de donnĂ©es OpenFoodFacts est fourni au format CSV. L’URL est le suivant:

Code
config["URL_OPENFOOD"]

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

  • Utiliser requests pour tĂ©lĂ©charger le fichier et l’écrire, de maniĂšre intermĂ©diaire, sur le disque local ;
  • Utiliser pandas avec quelques options pour importer le fichier puis le manipuler.
Code
# Solution pour voie 🟡
from utils.preprocess_openfood import download_openfood, import_openfood
download_openfood(destination = "openfood.csv.gz")
openfood = import_openfood("openfood.csv.gz", usecols = config['variables'])
openfood.loc[:, ['code', 'product_name', 'energy-kcal_100g', 'nutriscore_grade']].sample(5, random_state = 12345)

L’objectif de l’application est de proposer pour un produit donnĂ© quelques statistiques descriptives. On propose de se focaliser sur trois scores :

  • Le nutriscore ;
  • Le score Nova indiquant le degrĂ© de transformation d’un produit ;
  • L’écoscore, une mesure de l’empreinte carbone d’un produit ;

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

Code
indices_synthetiques = ['nutriscore_grade', 'nova_group', 'ecoscore_grade']

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:

Code
import pandas as pd
from utils.pipeline import clean_note

indices_synthetiques = ['nutriscore_grade', 'nova_group', 'ecoscore_grade']

openfood.loc[:, indices_synthetiques] = pd.concat(
        [clean_note(openfood, s, "wide") for s in indices_synthetiques],
        axis = 1
    )

1.3. Classification automatique dans une nomenclature de produits (🟡,🟱,đŸ””,🔮,⚫)

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:

Code
# Solution pour voie 🟡 et 🟱
from utils.download_pb import download_pb
download_pb("https://raw.githubusercontent.com/InseeFrLab/predicat/master/app/utils_ddc.py", "utils/utils_ddc.py")

Pour observer les nettoyages de champs textuels mis en oeuvre, les lignes suivantes peuvent ĂȘtre exĂ©cutĂ©es:

Code
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:

  • Utiliser le package re et boucler sur les lignes
  • Utiliser les fonctionnalitĂ©s trĂšs pratiques de Pandas

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 :

data.replace({variable: dict_rules_replacement}, regex=True)

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:

Code
from utils.utils_ddc import replace_values_ean
from utils.preprocess_openfood import clean_column_dataset
openfood = clean_column_dataset(
        openfood, replace_values_ean,
        "product_name", "preprocessed_labels"
)

Voici quelques cas oĂč notre nettoyage de donnĂ©es a modifiĂ© le nom du produit :

Code
(openfood
    .dropna(subset = ["product_name", "preprocessed_labels"])
    .loc[
        openfood["product_name"].str.upper() != openfood["preprocessed_labels"],
        ["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:

Code
from utils.download_pb import download_pb
import os
import fasttext

if os.path.exists("fasttext_coicop.bin") is False:
    download_pb(
        url = config["URL_FASTTEXT_MINIO"],
        fname = "fasttext_coicop.bin"
    )


model = fasttext.load_model("fasttext_coicop.bin")
model.predict("RATATOUILLE")

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:

Code
def import_coicop_labels(url: str) -> pd.DataFrame:
    coicop = pd.read_excel(url, skiprows=1)
    coicop['Code'] = coicop['Code'].str.replace("'", "")
    coicop = coicop.rename({"Libellé": "category"}, axis = "columns")
    return coicop
    
coicop = import_coicop_labels(
    "https://www.insee.fr/fr/statistiques/fichier/2402696/coicop2016_liste_n5.xls"
)

# Verification de la COICOP rencontrée plus haut
coicop.loc[coicop["Code"].str.contains("01.1.7.3.2")]

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 :

Code
def model_predict_coicop(data, model, product_column: str = "preprocessed_labels", output_column: str = "coicop"):
    predictions = pd.DataFrame(
        {
        output_column: \
            [k[0] for k in model.predict(
                [str(libel) for libel in data[product_column]], k = 1
                )[0]]
        })

    data[output_column] = predictions[output_column].str.replace(r'__label__', '')
    return data

openfood = model_predict_coicop(openfood, model)

1.3.bis Version alternative via l’API 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:

  • Elle permet d’appliquer sur les donnĂ©es fournies en entrĂ©e exactement les mĂȘmes prĂ©-traitement que sur les donnĂ©es d’entrainement. Ceci renforce la fiabilitĂ© des prĂ©dictions.
  • Elle facilite le travail des data scientists ou statisticiens car ils ne sont plus obligĂ©s de mettre en place des fonctions compliquĂ©es pour passer les prĂ©dictions dans une colonne de 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:

Code
import requests

def predict_from_api(product_name):
    url_api = f"https://api.lab.sspcloud.fr/predicat/label?k=1&q=%27{product_name}%27"
    output_api_predicat = requests.get(url_api).json()
    coicop_found = output_api_predicat['coicop'][f"'{product_name}'"][0]['label']
    return coicop_found

predict_from_api("Ratatouille")

Pour le parcours đŸ””, voici un exercice pour tester sur un Ă©chantillon des donnĂ©es de l’OpenFoodFacts

1.3.ter Entrainer son propre classifieur (🔮,⚫)

Les grimpeurs des voies 🔮 et ⚫ sont encouragĂ©s Ă  essayer d’entraĂźner eux-mĂȘmes un modĂšle de classification.

1.4. Ecriture de la base sur l’espace de stockage distant

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:

Code
from utils.import_yaml import import_yaml
import s3fs

config = import_yaml("config.yaml")
DESTINATION_OPENFOOD = f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/openfood.parquet"

# Initialisation de la connexion
fs = s3fs.S3FileSystem(
    client_kwargs={"endpoint_url": config["ENDPOINT_S3"]}
)

# 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. :

Code
# ⚠ 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

2ïžâƒŁ Faire des statistiques agrĂ©gĂ©es par catĂ©gories

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.

2.1. PrĂ©liminaires (🟡,🟱,đŸ””,🔮,⚫)

Sur le plan technique, cette partie propose deux cadres de manipulation de données différents, selon le balisage de la voie:

  • 🟡,🟱,đŸ””: utilisation de Pandas
  • 🔮,⚫: requĂȘtes SQL directement sur le fichier 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:

  1. Lecture des données OpenFoodFacts précédemment produites
  2. Construction de statistiques descriptives standardisées
  3. Construction de graphiques Ă  partir de ces statistiques descriptives

Les Ă©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:

Code
from utils.import_yaml import import_yaml
config = import_yaml("config.yaml")

Les colonnes suivantes nous seront utiles dans cette partie:

Code
indices_synthetiques = [
    "nutriscore_grade", "ecoscore_grade", "nova_group"
]
principales_infos = ['product_name', 'code', 'preprocessed_labels', 'coicop']

Voici, à nouveau, la configuration pour permettre à Python de communiquer avec l’espace de stockage distant:

Code
import s3fs

config = import_yaml("config.yaml")
INPUT_OPENFOOD = f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/openfood.parquet"

# Initialisation de la connexion
fs = s3fs.S3FileSystem(
    client_kwargs={"endpoint_url": config["ENDPOINT_S3"]}
)

2.2. Import des donnĂ©es depuis l’espace de stockage distant avec Pandas (🟡,🟱,đŸ””)

Il est recommandĂ© pour les parcours 🟡, 🟱, đŸ”” de travailler avec Pandas pour construire des statistiques descriptives. Cela se fera en deux Ă©tapes:

  • Import des donnĂ©es directement depuis l’espace de stockage, sans Ă©criture intermĂ©diaire sur le disque local, puis nettoyage de celles-ci ;
  • Construction de fonctions standardisĂ©es pour la production de statistiques descriptives.

Import et nettoyage des donnĂ©es 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.

Code
# Solution pour voie 🟡, 🟱 et đŸ””
import pandas as pd

# methode 1: pandas
with fs.open(INPUT_OPENFOOD, "rb") as remote_file:
    openfood = pd.read_parquet(
        remote_file,
        columns = principales_infos + \
        indices_synthetiques
    )

Les donnĂ©es ont ainsi l’aspect suivant:

Code
openfood.head(2)

2.3. Statistiques descriptives (🟡, 🟱 et đŸ””)

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:

Code
openfood_notes = openfood.loc[:,["coicop"] + indices_synthetiques]
Code
# Solution pour voie 🟡, 🟱 et đŸ””
def compute_stats_grades(data, indices_synthetiques):
    stats_notes = (
        data
        .groupby("coicop")
        .agg({i:'value_counts' for i in indices_synthetiques})
        .reset_index(names=['coicop', 'note'])
    )
    stats_notes = 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)
  
    return stats_notes

stats_notes = compute_stats_grades(openfood_notes, indices_synthetiques)

2.4. Import et traitement des donnĂ©es avec 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:

Code
import duckdb
con = duckdb.connect(database=':memory:')
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.

Code
duckdb_data = con.sql(
    f"SELECT product_name, preprocessed_labels, coicop, energy_100g FROM read_parquet('s3://{INPUT_OPENFOOD}') LIMIT 10"
)
duckdb_data.df() #conversion en pandas dataframe

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.

Code
# Solution Ă  la voie 🔮 et ⚫ pour les curieux de la voie 🟡, 🟱 et đŸ””
def count_one_variable_sql(con, variable, path_within_s3 = "temp.parquet"):
    query = f"SELECT coicop, {variable} AS note, COUNT({variable}) AS value FROM read_parquet('s3://{path_within_s3}') GROUP BY coicop, {variable}"
    stats_one_variable = con.sql(query).df().dropna()
    stats_one_variable['variable'] = variable
    stats_one_variable = stats_one_variable.replace('', 'NONE')

    return stats_one_variable

grades = ["nutriscore_grade", "ecoscore_grade", "nova_group"]
stats_notes_sql = [count_one_variable_sql(con, note, INPUT_OPENFOOD) for note in grades]
stats_notes_sql = pd.concat(stats_notes_sql)

Ceci nous donne donc le DataFrame suivant:

Code
stats_notes_sql.head(2)

2.5. Sauvegarde dans l’espace de stockage distant (🟡,🟱,đŸ””,🔮,⚫)

Ces statistiques descriptives sont Ă  Ă©crire dans l’espace de stockage distant pour ne plus avoir Ă  les calculer.

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

write_stats_to_s3(stats_notes, f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/stats_notes_pandas.parquet")
write_stats_to_s3(stats_notes_sql, f"{config['BUCKET']}{config['DESTINATION_DATA_S3']}/stats_notes_sql.parquet")

⚠ Il faut avoir modifiĂ© la valeur de BUCKET dans le fichier config.yaml pour que cette commande fonctionne.

2.6. CrĂ©ation d’un modĂšle de graphiques (🟡,🟱,đŸ””,🔮,⚫)

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:

Code
# Solution pour voie 🟡

import plotly.express as px
import numpy as np

def figure_infos_notes(
    data, variable_note = 'nutriscore_grade',
    coicop = "01.1.7.3.2", note_produit = "B",
    title = "Nutriscore"
):
    example_coicop = 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")

    fig = px.bar(
        example_coicop,
        x='note', y='value', color = "color", template = "simple_white",
        title=title,
        color_discrete_map={"Note produit": "red", "Autres produits": "royalblue"},
        labels={
            "note": "Note",
            "value": ""
        }
    )
    fig.update_xaxes(
        categoryorder='array',
        categoryarray= ['A', 'B', 'C', 'D', 'E'])
    fig.update_layout(showlegend=False)
    fig.update_layout(hovermode="x")
    fig.update_traces(
        hovertemplate="<br>".join([
            "Note %{x}",
            f"{variable_note}: " +" %{y} produits"
        ])
    )

    return fig

Voici un exemple d’utilisation

Code
from utils.construct_figures import figure_infos_notes
fig = figure_infos_notes(stats_notes)
fig.update_layout(width=800, height=400)

fig

3ïžâƒŁ Comparer un produit Ă  un groupe similaire

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

Code
indices_synthetiques = [
    "nutriscore_grade", "ecoscore_grade", "nova_group"
]
principales_infos = ['product_name', 'code', 'preprocessed_labels', 'coicop']
liste_colonnes = principales_infos + indices_synthetiques
liste_colonnes_sql = [f"\"{s}\"" for s in liste_colonnes]
liste_colonnes_sql = ', '.join(liste_colonnes_sql)

On va aussi utiliser la nomenclature COICOP qui peut ĂȘtre importĂ©e via le code ci-dessous:

Code
from utils.download_pb import import_coicop_labels
coicop = import_coicop_labels(
    "https://www.insee.fr/fr/statistiques/fichier/2402696/coicop2016_liste_n5.xls"
)

3.1. DĂ©tection de code barre (🟡,🟱,đŸ””,🔮,⚫)

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:

Code
url_image = "https://images.openfoodfacts.org/images/products/500/011/260/2791/front_fr.4.400.jpg"

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:

Code
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.

Code
# Solution pour voie 🟡
from pyzbar import pyzbar

def extract_ean(url, verbose=True):
    img = io.imread(url)
    decoded_objects = pyzbar.decode(img)
    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

obj = extract_ean(url_image, verbose = False)

obj[0].data.decode()

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

3.2. Association d’un code barre Ă  un produit d’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 :

Code
ean = "5000112602999"

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:

Code
import duckdb
con = duckdb.connect(database=':memory:')
con.execute("""
    INSTALL httpfs;
    LOAD httpfs;
    SET s3_endpoint='minio.lab.sspcloud.fr'
""")

url_data = "https://projet-funathon.minio.lab.sspcloud.fr/2023/sujet4/diffusion/openfood.parquet"

Pour commencer, effectuons une requĂȘte SQL pour rĂ©cupĂ©rer le produit correspondant au code-barre qu’on a scannĂ©:

Voici la solution:

Code
# Solution pour voie 🟡
def get_product_ean(con, ean, url_data, liste_colonnes_sql):
    openfood_produit = con.sql(
            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:

  1. On cherche le produit Ă  partir du code barre
  2. Si les infos sont manquantes, on récupÚre les produits dont le nom ressemble par distance de Jaro-Winkler.

Voici la fonction qui permet d’implĂ©menter la deuxiĂšme partie:

Code
# 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):
    out_textual = 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_imputed = pd.concat(
        [
            openfood_produit.loc[:, ["code", "product_name", "coicop"]].reset_index(drop = True),
            pd.DataFrame(out_textual.loc[:, indices_synthetiques].replace("NONE","").replace('',np.nan).mode(dropna=True))
        ], ignore_index=True, axis=1
    )
    out_textual_imputed.columns = ["code", "product_name", "coicop"] + indices_synthetiques
    
    return out_textual_imputed

Voici finalement le pipeline mis en oeuvre par une fonction :

Code
# Solution pour voie 🟡

def find_product_openfood(con, liste_colonnes_sql, url_data, ean):
    openfood_produit = con.sql(
        f"SELECT {liste_colonnes_sql} FROM read_parquet('{url_data}') WHERE CAST(ltrim(code, '0') AS STRING) = CAST(ltrim({ean}) AS STRING)"
    ).df()
    
    product_name = openfood_produit["product_name"].iloc[0]
    
    if openfood_produit['nutriscore_grade'].isin(['NONE','']).iloc[0]:
        openfood_produit = fuzzy_matching_product(
            openfood_produit, product_name, con, url_data,
            liste_colonnes_sql, indices_synthetiques)
        openfood_produit = openfood_produit.merge(coicop, left_on = "coicop", right_on = "Code")

    return openfood_produit

Qui peut ĂȘtre finalisĂ© de la maniĂšre suivante:

Code
openfood_produit = find_product_openfood(
    con, liste_colonnes_sql,
    url_data, ean
)
openfood_produit.head(2)

Production automatique d’un graphique (🟡,🟱,đŸ””,🔮,⚫)

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:

Code
stats_notes = pd.read_parquet(
    "https://minio.lab.sspcloud.fr/projet-funathon/2023/sujet4/diffusion/stats_notes_pandas.parquet"
)

Dans notre application, nous allons utiliser cette fonction:

Code
from utils.construct_figures import figure_infos_notes

variable = 'nutriscore_grade'

def plot_product_info(
    data, variable,
    stats_notes):

    fig = figure_infos_notes(
        stats_notes,
        variable_note = variable,
        coicop = data['coicop'].iloc[0],
        note_produit = data[variable].iloc[0],
        title = variable.split("_")[0].capitalize()
    )

    return fig
Code
fig = plot_product_info(openfood_produit, variable, stats_notes)
fig.update_layout(width=800, height=400)
fig
Code
fig = plot_product_info(openfood_produit, "ecoscore_grade", stats_notes)
fig.update_layout(width=800, height=400)
fig

4ïžâƒŁ Construire une application interactive

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:

Code
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.

4.1. Lancer l’application pour la tester (🟡,🟱,đŸ””,🔮,⚫)

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

  • Pour les personnes familiĂšres de la ligne de commande, vous pouvez en lancer une (en cliquant sur + 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
  • Pour les personnes dĂ©sirant lancer la commande depuis Jupyter, il suffit d’exĂ©cuter la cellule suivante:
Code
!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.

  1. Il convient d’ouvrir un nouvel onglet sur votre navigateur web pour retourner sur votre espace SSPCloud: datalab.sspcloud.fr/my-services. Si vous ĂȘtes sur une autre page, vous pouvez cliquer Ă  gauche sur My Services.
  2. Ensuite, il faut cliquer sur le bouton 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.

4.2. CrĂ©er l’application dans un serveur temporaire (🟱,đŸ””,🔮,⚫)

Voici la gradation des niveaux pour crĂ©er l’application:

  • 🟱: Lire et comprendre le contenu du fichier app.py qui gĂ©nĂšre l’application
  • đŸ””: AprĂšs avoir supprimĂ© le fichier d’exemple app.py, mettre en oeuvre l’application avec des consignes guidĂ©es
  • 🔮: AprĂšs avoir supprimĂ© le fichier d’exemple app.py, mettre en oeuvre l’application Ă  partir d’un cachier des charges dĂ©taillĂ©
  • ⚫: AprĂšs avoir supprimĂ© le fichier d’exemple 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/.

Code
# Solution pour la voie 🟱
with open('app.py', 'r') as file:
    app_content = file.read()

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

4.3. En marche vers la mise en production (🟱,đŸ””,🔮,⚫)

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.

5ïžâƒŁ DĂ©ploiement de l’application interactive

5.1. PrĂ©liminaires (đŸ””,🔮,⚫)

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 :

  • quelle est la version de Python Ă  installer pour que le projet fonctionne ?
  • quels sont les packages Python utilisĂ©s par le projet et quelles sont leurs versions ?
  • quelles sont les Ă©ventuelles librairies systĂšmes, i.e. dĂ©pendantes du systĂšme d’exploitation installĂ©, nĂ©cessaires pour que les packages 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.

5.2. Conteneurisation de l’application (đŸ””,🔮,⚫)

5.3. DĂ©ploiement sur le 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 !

Bonus: le parcours 🟣

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