đŸ· Wine Scraping

Le breuvage contre-attaque

Guillaume DEVANT et Corentin DUCLOUX

06/01/2024

Le Pourquoi du Comment

  • Le 18 janvier approche, une date en apparence anodine mais trĂšs importante pour nos deux compĂšres.
  • Pour cette journĂ©e festive, nos 2 protagonistes se rendirent sur vinatis.com pour trouver un breuvage.
  • Et c’est Ă  ce moment que l’histoire prend racine


On parle de moi ?

Scraping

“Pour savoir qu’un verre Ă©tait de trop, encore faut-il avoir scrapĂ© son vin !” \(-\) Inconnu

Scraping, Partie I

scraping_functions.py \(\Rightarrow\) Le coeur du scraper

  1. Construit des URL avec query parameters en utilisant le package yarl.
URL_INIT = URL.build(scheme="https", host="vinatis.com")
WHITE = "achat-vin-blanc"
RED = "achat-vin-rouge"
ROSE = "achat-vin-rose"

>>> URL_INIT / WHITE % {"page": 1, "tri": 7}
... URL('https://vinatis.com/achat-vin-blanc?page=1&tri=7')
  1. create_session crĂ©e une session HTML avec un User-Agent et un Proxy alĂ©atoire, pouvant changer entre les requĂȘtes.
  2. PossĂšde un dĂ©corateur @random_waiter(min, max) permettant de gĂ©nĂ©rer un temps d’attente alĂ©atoire entre les deux bornes spĂ©cifiĂ©es entre chaque requĂȘte GET pour Ă©viter d’envoyer trop de requĂȘtes dans un laps de temps rĂ©duit.
  3. create_all_wine_urls permet de crĂ©er l’ensemble des liens href.
  4. export_wine_links permet d’exporter ces liens dans un fichier CSV.

Scraping, Partie II

  1. On va ensuite requĂȘter ces liens href avec create_json et rĂ©cupĂ©rer les pages brutes en HTML.
  2. La fonction scraping du module mystical_soup va permettre d’extraire toutes les informations intĂ©ressantes de la page brute et renvoyer la dataclass Vin sĂ©rialisable en JSON.

Exemple d’un Vin et ses caractĂ©ristiques sĂ©rialisĂ©s en JSON :

{
        "name": "PINOT NOIR 2019 LAS PIZARRAS - ERRAZURIZ",
        "capacity": "0,75 L",
        "price": "94,90 €",
        "price_bundle": null,
        "characteristics": "Vin Rouge / Chili / Central Valley / Aconcagua Valley DO / 13,5 % vol / 100% Pinot noir",
        "note": null,
        "keywords": [
            "Elégance",
            "Finesse",
            "Harmonie"
        ],
        "others": null,
        "picture": "https://www.vinatis.com/67234-detail_default/pinot-noir-2019-las-pizarras-errazuriz.png",
        "classification": null,
        "millesime": "2019",
        "cepage": "100% Pinot noir",
        "gouts": "Rouge Charnu et fruité",
        "par_gouts": "Puissant",
        "oeil": "Robe rubis aux reflets violets.",
        "nez": "Nez complexe sur la griotte, les épices et les champignons (truffe).",
        "bouche": "Bouche fruitée et florale. Tanins structurés, élégants et fins. finale harmonieuse et persistante.",
        "temperature": "8-10°C",
        "service": "En bouteille ou en carafe",
        "conservation_1": "2026",
        "conservation_2": "A boire et Ă  garder",
        "accords_vins": "Apéritif, Entrée, Charcuterie, Viande rouge, Viande blanche, Volaille, Gibier, Champignon, Barbecue, Cuisine du monde, Fromage, Dessert fruité, Dessert chocolaté",
        "accords_reco": "Gigot d'agneau aux herbes de Provence; Tikka massala; Plateau de fromages."
    }

đŸ§č Cleaning

Mais ce JSON brut doit ĂȘtre nettoyĂ© et considĂ©rablement restructurĂ© !

  1. Nous avons choisi d’utiliser polars đŸ» et non pas pandas đŸŒ pour le faire.
  2. Toutes les fonctions de nettoyage sont contenues dans bear_cleaner.py.
  3. La fonction super_pipe permet de chainer toutes les transformations dans un pipeline propre pour structurer notre Dataframe.
  4. Nous obtenons ainsi un Dataframe de taille (4006,40) prĂȘt pour le Machine Learning

Machine Learning

“2024 sera un millĂ©sime français !” \(-\) Emmanuel Macron

Machine Learning - Procédure

  1. Deux variables à prédire : unit_price & type
  2. Utilisation de 6 modĂšles de Machine Learning
  3. ➶ Optimisation des hyperparamĂštres \(\Rightarrow\) models.py
  4. đŸč PrĂ©diction sur les donnĂ©es de test \(\Rightarrow\) prediction.py
  5. đŸ§Ș Utilisation d’un pipeline sklearn
    • Evite le Data Leakage
    • ProcĂ©dure standardisĂ©e pour l’ensemble des modĂšles.

➶ ML : Optimisation

  1. Choix des 21 variables explicatives
  2. Preprocessing : OneHotEncoder(), Imputation NA, MinMaxScaler()
  3. Optimisation des hyperparamĂštres par Cross-Validation
  • Avec optimisation_script.py on optimise les hyperparamĂštres des modĂšles et on rĂ©cupĂšre sous forme de CSV :
    • Les scores de test et d’entrainement
    • Les Ă©carts-type \(\sigma_{\text{test}}\) et \(\sigma_{\text{train}}\)
    • Les hyperparamĂštres optimaux pour chaque modĂšle
ModĂšle,Score Test,Score Entrainement,Ecart-Type Test,Ecart-Type Train,ParamĂštres,Score Test data,Mode
Random Forest,0.934,0.941,0.007,0.007,"{'entrainement__max_depth': 9, 'entrainement__n_estimators': 30, 'imputation__strategy': 'median'}",0.9301745635910225,classification
K Neighbors,0.954,0.965,0.012,0.003,"{'entrainement__n_neighbors': 5, 'imputation__strategy': 'median'}",0.9600997506234414,classification
Réseaux de neurones,0.976,0.997,0.007,0.001,"{'entrainement__hidden_layer_sizes': (100,), 'entrainement__max_iter': 1000, 'entrainement__solver': 'adam', 'imputation__strategy': 'median'}",0.9800498753117207,classification
Boosting,0.975,1.0,0.009,0.0,"{'entrainement__learning_rate': 0.5, 'entrainement__n_estimators': 200, 'imputation__strategy': 'median'}",0.9812967581047382,classification
Ridge,0.979,0.983,0.009,0.002,"{'entrainement__alpha': 0.015625, 'imputation__strategy': 'mean'}",0.9812967581047382,classification
Support Vector,0.981,0.992,0.008,0.002,"{'entrainement__C': 3.281341424030552, 'imputation__strategy': 'median'}",0.9825436408977556,classification

đŸč ML : PrĂ©diction

  • Deux types de prĂ©dictions :
    • Classification sur le type de vin (Vin Rouge / Blanc / RosĂ©)
    • RĂ©gression sur le prix d’une bouteille de vin
  • Avec prediction_script.py on rĂ©alise les prĂ©dictions avec tous les modĂšles
name,type,random_forest,boosting,ridge,knn,mlp,support_vector
LES CARLINES 2021 - MAS HAUT BUIS,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge
LA BARGEMONE ROSE 2022 - COMMANDERIE DE LA BARGEMONE,Vin Rosé,Vin Blanc,Vin Rosé,Vin Rosé,Vin Rosé,Vin Rosé,Vin Rosé
TEMPRANILLO 2021- VEGA DEMARA,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge
CHÂTEAUNEUF DU PAPE - ALCHIMIE 2020 - DOMAINE DES 3 CELLIER,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge,Vin Rouge
  • Pour les 800 vins qui n’ont pas servi dans notre Cross Validation on rĂ©alise une prĂ©diction par chacun de nos 6 modĂšles, le tout stockĂ© dans un fichier CSV !

🔬 Metrics

  • Regression:
    • Erreur moyenne absolue : MAE(\(y\),\(\hat{y}\)) = \(\frac{1}{n}\sum|y_i - \hat{y_i}|\)
    • Erreur quadratique moyenne : MSE(\(y\),\(\hat{y}\)) = \(\frac{1}{n}\sum(y_i - \hat{y_i})^2\)
    • Erreur RĂ©siduelle Maximale : MaxError(\(y\), \(\hat{y}\)) = \(\max\left(|y_i-\hat{y_i}|\right)\)
    • \(R^2\) Score = \(1- \frac{\sum(y_i-\hat{y_i})^2}{\sum(y_i-\bar{y_i})^2}\)
  • Classification:
    • Accuracy Score : AS(\(y\),\(\hat{y}\)) = \(\frac{1}{n}\sum(\hat{y_i} = y_i)\)
    • Precision = \(\frac{\text{true positive}}{\text{true positive + false positive}}\)
    • Recall = \(\frac{\text{true positive}}{\text{true positive + false negative}}\)
    • \(F_1\) Score = \(2 \times \frac{\text{precision } \times \text{ recall}}{\text{precision + recall}}\)

đŸ’» Application

đŸ•” Framework utilisĂ© : streamlit

  • đŸ€· Pourquoi ? FacilitĂ© de mise en oeuvre
    • Base de donnĂ©es
    • Statistiques descriptives (corrĂ©lations, rĂ©partition, etc.)
    • Machine Learning
  • 👹‍🏭 Comment ? Forte flexibilitĂ© \(\rightarrow\) L’utilisateur peut jouer avec les donnĂ©es
    • Sidebar avec de nombreux sĂ©lecteurs

Choix du stockage, Partie I

duckdb : La base de donnĂ©es qui fait “coin coin” 🩆

def db_connector() -> DuckDBPyConnection:
    """Se connecte à la base de données."""
    connection = duckdb.connect(database=":memory:")
    return connection
  • :memory: \(\Rightarrow\) Base de donnĂ©es in-memory
  • La base de donnĂ©es en mĂ©moire stocke les informations directement dans la mĂ©moire vive plutĂŽt que sur un disque.
  • RĂ©duit le temps nĂ©cessaire au stockage et Ă  la consultation des donnĂ©es, et accĂ©lĂšre l’exĂ©cution des requĂȘtes.

Choix du stockage, Partie II

  • 5 tables de rĂ©sultats de Machine Learning sont obtenues grĂące Ă  l’exĂ©cution de ml_trigger qui se charge d’éxĂ©cuter l’ensemble des scripts d’export.

Voici un schĂ©ma du processus d’ingestion des tables :

graph LR;
A("👹‍🔬 pred_classification")-->F;
B("👹‍🔬 pred_regression")-->F;
C("đŸ‘©â€đŸ« result_ml_regression")-->F;
D("đŸ‘©â€đŸ« result_ml_classification")-->F;
E("đŸ•”ïžâ€â™‚ïž importance")-->F[("🩆 In Memory Database")];

style A stroke:#adbac7,stroke-width:3px, fill:white;
style B stroke:#adbac7,stroke-width:3px, fill:white;
style C stroke:#adbac7,stroke-width:3px, fill:white;
style D stroke:#adbac7,stroke-width:3px, fill:white;
style E stroke:#adbac7,stroke-width:3px, fill:white;
style F stroke:#fff100,stroke-width:3px, fill:white;

🚀 DĂ©monstration

Lancement de l’application, 2 MĂ©thodes.

Depuis un terminal :

  • Lancement du shell poetry :
py -m poetry shell
  • Lancement de l’application :
python -m streamlit run "streamlit_app.py"

Depuis le lien de l’application dĂ©ployĂ©e sur le cloud streamlit :

Un code de Deutsche QualitÀt

  • Annotations de type claires
  • Docstrings explicites et soignĂ©es
  • Gestion des dĂ©pendances avec Poetry
  • ModularitĂ©
  • Docker
  • Tests des features de l’application
  • Git pour versionner notre projet
  • Black pour formater notre code
  • Un beau README

Code certifiĂ© conforme par l’Agent Smith\(^*\)

\(^*\) L’Agent Smith tient par ailleurs Ă  prĂ©ciser qu’il n’a reçu aucun pot-de-vin de notre part pour ce diagnostic malgrĂ© son enrichissement personnel fulgurant


Annotations de type

def model_rf(x_train: pd.DataFrame, y_train: pd.Series, mode: str) -> GridSearchCV:
    ...
  • Expliciter au maximum les types d’entrĂ©e et de sortie des fonctions.
  • On peut parler de documentation implicite \(\Rightarrow\) on cherche Ă  Ă©viter Ă  un utilisateur d’utiliser des objets incompatibles avec ce qui a Ă©tĂ© Ă©tabli.

Note

mypy va nous permettre d’effectuer ce contrĂŽle (static type checking), c’est Ă  dire de vĂ©rifier si les valeurs assignĂ©es aux variables, les arguments passĂ©s aux fonctions et les valeurs de retour correspondent aux types attendus.

Docstrings

  • Chaque fonction Ă  interface publique possĂšde une docstring structurĂ©e :
    • Nom de la fonction et description succinte
    • ParamĂštre(s) d’entrĂ©e et paramĂštre(s) de sortie
    • LevĂ©e d’exception (si il y en a)
    • Au minimum un exemple d’utilisation

Exemple avec la fonction model_rf du module models.py :

"""`model_rf`: Effectue une recherche exhaustive (Cross-Validation) des meilleurs paramĂštres
    en utilisant une Random Forest. Les paramÚtres optimisés sont :

    - n_estimators
    - max_depth

    ---------
    `Parameters`
    --------- ::

        x_train (pd.DataFrame): # L'ensemble d'entrainement
        y_train (pd.Series): # La variable à prédire
        mode (str): # regression | classification

    `Raises`
    --------- ::

        ValueError: # Une erreur est levée quand le mode est invalide

    `Returns`
    --------- ::

        GridSearchCV

    `Example(s)`
    ---------

    >>> model_rf(x_train=X_train, y_train=y_train, mode = "regression")
    ... Entrainement du modĂšle : Random Forest
    ... GridSearchCV(estimator=Pipeline(steps=[('imputation', SimpleImputer()),
    ...                                   ('echelle', MinMaxScaler()),
    ...                                   ('entrainement',
    ...                                    RandomForestRegressor())]),
    ...         n_jobs=-1,
    ...         param_grid={'entrainement__max_depth': range(1, 10),
    ...                     'entrainement__n_estimators': range(10, 50, 10),
    ...                     'imputation__strategy': ['mean', 'median',
    ...                                              'most_frequent']},
    ...         return_train_score=True)
    """

đŸ§™â€â™‚ïž Poetry

Gestion des dépendances : poetry simplifie la gestion des dépendances en utilisant un fichier de configuration pyproject.toml. Il permet de spécifier les dépendances directes et les dépendances de développement requises pour le projet.

Environnement Virtuel : venv isolé pour le projet, aidant à maintenir un environnement de développement propre et évitant les conflits entre les versions des packages.

Installation de dĂ©pendances : Facilite l’installation des dĂ©pendances dĂ©finies dans le fichier de configuration en utilisant la commande poetry install.

py -m poetry install

🚱 Modulaire !

Séparation des composants du projet :

├───data
│   â”œâ”€â”€â”€đŸ·vins.json
│   â”œâ”€â”€â”€đŸ’Ÿwine_links.csv
│   └───tables
│       â”œâ”€â”€â”€đŸ’Ÿpred_classification.csv
│       â”œâ”€â”€â”€đŸ’Ÿpred_regression.csv
│       â”œâ”€â”€â”€đŸ’Ÿresult_ml_classification.csv
│       â””â”€â”€â”€đŸ’Ÿresult_ml_regression.csv
│       â””â”€â”€â”€đŸ’Ÿimportance.csv
├───src
│   └───📩modules
│       ├───⚙app
│       │   ├───🐍st_functions.py
│       │   ├───🐍st_plots.py
│       │   ├───🐍st_selectors.py
│       │   ├───🐍st_tables.py
│       │   └───🐍st_tables.py
│       ├───⚙ml_models
│       │   ├───🐍importance_script.py
│       │   ├───🐍models.py
│       │   ├───🐍optimisation_script.py
│       │   ├───🐍prediction_script.py
│       │   └───🐍prediction.py
│       ├───⚙scraping
│       │   ├───🐍mystical_soup.py
│       │   ├───🐍page_scraper.py
│       │   ├───🐍scraping_functions.py
│       │   ├───🐍vin_dataclass.py
│       │   └───🐍wine_scraper.py
│       ├───🐍ml_trigger.py
│       ├───🐍scraping_trigger.py
│       ├───🐍bear_cleaner.py
│       └───🐍utils.py
├───🐳Dockerfile
â”œâ”€â”€â”€đŸ§™â€â™‚ïžpoetry.lock
├───📍pyproject.toml
├───📘README.md
└───🐍streamlit_app.py

🐳 Docker, Partie I

Pourquoi utiliser Docker ?

Isolation : Docker permet d’isoler l’application, ses dĂ©pendances et son environnement d’exĂ©cution dans un conteneur. Cela signifie que l’application s’exĂ©cute avec ses propres ressources et dĂ©pendances sans affecter l’environnement hĂŽte.

PortabilitĂ© : Une fois que l’image Docker est créée, elle peut ĂȘtre exĂ©cutĂ©e sur n’importe quel systĂšme prenant en charge Docker, offrant une portabilitĂ© Ă©levĂ©e.


Comment ? \(\Rightarrow\) Dockerfile

Docker assure la reproductibilitĂ© en permettant Ă  n’importe qui de construire et d’exĂ©cuter le mĂȘme conteneur Ă  partir des spĂ©cifications dĂ©finies dans le Dockerfile.

🐳 Docker, Partie II

  • Contenu du Dockerfile :
FROM python:3.10-slim-buster
WORKDIR /app

COPY pyproject.toml poetry.lock ./

RUN pip install poetry \ 
    && poetry config virtualenvs.create false \
    && poetry install --no-dev --no-interaction --no-ansi

COPY streamlit_app.py .
COPY src ./src
COPY data ./data
COPY img ./img

RUN addgroup --system app \
    && adduser --system --group app

USER app

EXPOSE 8501

HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health

ENTRYPOINT ["python", "-m", "streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]

đŸ—ïž Le conteneur en action !

Il faut tout d’abord s’assurer d’avoir tĂ©lĂ©chargĂ© Docker Desktop avant toute chose.

Une fois installĂ©, l’image est construite en exĂ©cutant la commande suivante dans un terminal :

docker image build . -t "wine_scraping"

Une fois la crĂ©ation de l’image terminĂ©e, on peut consulter la taille de celle-ci avec :

docker images

Ensuite, pour lancer le conteneur Docker avec l’utilisateur app sur le port initial (8501) de streamlit, il suffit de faire :

docker run -u app -p 8501:8501 wine_scraping

🎉 Une fois le conteneur lancĂ©, on le voit apparaitre dans Docker Desktop. Pour accĂ©der Ă  l’application, il faut se rendre sur http://localhost:8501/.

Fin

On ne sait pas pourquoi on a fait tout ça, car nous voulions simplement trouver une bouteille pour fĂȘter notre anniversaire, et on se retrouve avec une application d’analyse de donnĂ©es qui ne nous aide en aucun cas Ă  trouver notre breuvageâ€ŠđŸ˜”

Références