path = window.location.pathname.replace(".html", ".qmd");
path_modified = (path.includes('en/content')) ? path.replace('en/content', 'content/en') : path
html`${printBadges({fpath: path_modified})}`
Pour essayer les exemples présents dans ce tutoriel :
html`<div>${getConditionalHTML(path, true)}</div>`
function getConditionalHTML(path, print) {
if (print === false) return ``
if (isBetweenSeptAndDec()) {
return md`<i>La correction sera visible prochainement sur cette page. En attendant, la liste des corrections déjà acccessibles est [ici](/content/annexes/corrections.html)</i>`; // Return an empty string if not between the dates
else {
} return html`
<details>
<summary>
Pour ouvrir la version corrigée sous forme de <i>notebook</i>
</summary>
${printBadges({ fpath: path, correction: true })}
</details>
`;
} }
function renderCorrection({ fpath, correction }) {
if (correction) {
return html`${printBadges({ fpath: fpath, correction: true })}`;
else {
} return html`<i>La correction sera visible prochainement sur cette page.</i>`;
} }
Ce chapitre a été écrit avec Milena Suarez-Castillo et présente quelques éléments qui servent de base à un travail en cours sur les inégalités socioéconomiques dans les choix de consommation alimentaire.
1 Introduction
1.1 Réplication de ce chapitre
Ce chapitre est plus exigeant en termes d’infrastructures que les précédents.
Si la première partie de ce chapitre peut être menée avec une
installation standard de Python
, ce n’est pas le cas de la
deuxième qui nécessite un serveur ElasticSearch
. Les utilisateurs du
SSP Cloud pourront répliquer les exemples de ce cours
car cette technologie est disponible (que ce soit pour indexer une base ou
pour requêter une base existante).
⚠️ Ce
chapitre nécessite une version particulière du
package ElasticSearch
pour tenir compte de l’héritage de la version 7 du moteur Elastic
.
Pour cela, faire
!pip install elasticsearch==8.2.0
!pip install unidecode
!pip install rapidfuzz
!pip install xlrd
La première partie de ce tutoriel ne nécessite pas d’architecture particulière et peut ainsi être exécutée en utilisant les packages suivants :
import time
import pandas as pd
Le script functions.py
, disponible sur Github
,
regroupe un certain nombre de fonctions utiles permettant
d’automatiser certaines tâches de nettoyage classiques
en NLP.
Hint
Plusieurs méthodes peuvent être mises en oeuvre pour récupérer
le script d’utilitaires. Vous pouvez trouver en dessous
de cet encadré une méthode qui va chercher la dernière
version sur le dépôt Github
du cours
import requests
= "https://github.com/linogaliana/python-datascientist/raw/main/content/modern-ds/functions.py"
url = requests.get(url, allow_redirects=True)
r
open("functions.py", "wb").write(r.content)
Après l’avoir récupéré (cf. encadré dédié), il convient d’importer les fonctions sous forme de module:
import functions as fc
1.2 Cas d’usage
Ce notebook recense et propose d’appréhender quelques outils utilisés pour le papier présenté aux Journées de Méthodologie Statistiques 2022: Galiana and Suarez-Castillo, “Fuzzy matching on big-data: an illustration with scanner data and crowd-sourced nutritional data” (travail en cours!)
On va partir du cas d’usage suivant :
Combien de calories dans ma recette de cuisine de ce soir? Combien de calories dans mes courses de la semaine?
L’objectif est de reconstituer, à partir de libellés de produits, les caractéristiques nutritionnelles d’une recette. Le problème est que les libellés des tickets de caisse ne sont pas des champs textuels très propres, ils contiennent, par exemple, beaucoup d’abbréviations, toutes n’étant pas évidentes.
Voici par exemple une série de noms de produits qu’on va utiliser par la suite:
= [
ticket "CROISSANTS X6 400G",
"MAQUEREAUX MOUTAR.",
"IGP OC SAUVIGNON B",
"LAIT 1/2 ECRM UHT",
"6 OEUFS FRAIS LOCA",
"ANANAS C2",
"L POMME FUDJI X6 CAL 75/80 1KG ENV",
"PLT MIEL",
"STELLA ARTOIS X6",
"COTES DU LUBERON AIGUEBRUN 75C",
]
A ces produits, s’ajoutent les ingrédients suivants, issus de la recette du velouté de potiron et carottes de Marmiton qui sera notre plat principal :
= [
ingredients "500 g de carottes",
"2 pommes de terre",
"1 gousse d'ail",
"1/2 l de lait",
"1/2 l de bouillon de volaille",
"1 cuillère à soupe de huile d'olive",
"1 kg de potiron",
"1 oignon",
"10 cl de crème liquide (facultatif)",
]
Essayer de récupérer par web scraping cette liste est un bon exercice pour réviser les concepts vus précédemment
On va donc créer une liste de course compilant ces deux listes hétérogènes de noms de produits:
= ticket + ingredients libelles
On part avec cette liste dans notre supermarché virtuel. L’objectif sera de trouver une méthode permettant de passer à l’échelle: automatiser les traitements, effectuer des recherches efficaces, garder une certaine généralité et flexibilité.
Ce chapitre montrera par l’exemple l’intérêt d’Elastic
par rapport à une solution
qui n’utiliserait que du Python
.
2 Données utilisées
2.1 Les bases offrant des informations nutritionnelles
Pour un nombre restreint de produits, on pourrait bien sûr chercher à la main les caractéristiques des produits en utilisant les fonctionalités d’un moteur de recherche:
Cependant, cette approche serait très fastidieuse et nécessiterait de récuperer, à la main, chaque caractéristique pour chaque produit. Ce n’est donc pas envisageable.
Les données disponibles sur Google
viennent de l’USDA,
l’équivalent américain de notre Ministère de l’Agriculture.
Cependant, pour des recettes comportant des noms de produits français, ainsi que
des produits potentiellement transformés, ce n’est pas très pratique d’utiliser
une base de données de produits agricoles en Français. Pour cette raison,
nous proposons d’utiliser les deux bases suivantes,
qui servent de base au travail de
Galiana et Suarez Castillo (2022)
- L’
OpenFoodFacts
database qui est une base
collaborative française de produits alimentaires. Issue d’un projet Data4Good, il s’agit d’une alternative opensource et opendata à la base de données de l’application Yuka. - La table de composition nutritionnelle
Ciqual
produite par l’Anses. Celle-ci propose la composition nutritionnelle moyenne des aliments les plus consommés en France. Il s’agit d’une base de données enrichie par rapport à celle de l’USDA puisqu’elle ne se cantonne pas aux produits agricoles non transformés. Avec cette base, il ne s’agit pas de trouver un produit exact mais essayer de trouver un produit type proche du produit dont on désire connaître les caractéristiques.
2.2 Import
Quelques fonctions utiles sont regroupées dans le script functions.py
et importées dans le notebook.
La base OpenFood
peut être récupérée en ligne
via la fonction fc.import_openfood
. Néanmoins, cette opération nécessitant
un certain temps (les données brutes faisant autour de 2Go), nous proposons une méthode
pour les utilisateurs du SSP Cloud
où une version est disponible sur
l’espace de stockage.
La base Ciqual
, qui plus légère, est récupérée elle directement en ligne
via la fonction fc.import_ciqual
.
# Pour les utilisateurs du SSP Cloud
= fc.import_openfood_s3()
openfood # Pour les utilisateurs hors du SSP Cloud
# openfood = fc.import_openfood()
= fc.import_ciqual() ciqual
openfood.head()
ciqual.head()
3 ElasticSearch ? Mais ce n’est pas du Python ?!
3.1 Qu’est-ce qu’Elastic ?
ElasticSearch
c’est un logiciel qui fournit un moteur de recherche installé sur
un serveur (ou une machine personnelle) qu’il est possible de requêter depuis un client
(une session Python
par exemple).
C’est un moteur de recherche
très performant, puissant et flexible, extrêmement utilisé dans le domaine de la datascience
sur données textuelles.
Un cas d’usage est par exemple de trouver, dans un corpus de grande dimension (plusieurs sites web, livres…), un certain texte en s’autorisant des termes voisins (verbes conjugués, fautes de frappes…).
Un index est une collection de documents dans lesquels on souhaite chercher, préalablement ingérés dans un moteur de recherche les documents sont les établissements. L’indexation consiste à pré-réaliser les traitements des termes des documents pour gagner en efficacité lors de la phase de recherche. L’indexation est faite une fois pour de nombreuses recherches potentielles, pour lesquelles la rapidité de réponse peut être cruciale. Après avoir indexé une base, on effectuera des requêtes qui sont des recherches d’un document dans la base indexé (équivalent de notre web) à partir de termes de recherche normalisés.
Le principe est le même que celui d’un moteur de recherche du web comme Google
.
D’un côté, l’ensemble à parcourir est indexé pour être en
mesure de parcourir de manière efficace l’ensemble du corpus.
De l’autre côté, la phase de recherche permet de retrouver l’élément du corpus le
plus cohérent avec la requête de recherche.
L’indexation consiste, par exemple,
à pré-définir des traitements des termes du corpus pour gagner en efficacité
lors de la phase de recherche. En effet, l’indexation est une opération peu fréquente
par rapport à la recherche. Pour cette dernière, l’efficacité est cruciale (un site web
qui prend plusieurs secondes à interpréter une requête simple ne sera pas utilisé). Mais, pour
l’indexation, ceci est moins crucial.
Les documents sont constitués de variables, les champs (‘fields’), dont le type est spécifié (“text”, “keywoard”, “geo_point”, “numeric”…) à l’indexation.
ElasticSearch
propose une interface graphique nommée Kibana
.
Celle-ci est pratique
pour tester des requêtes et pour superviser le serveur Elastic. Cependant,
pour le passage à l’échelle, notamment pour mettre en lien une base indexée dans
Elastic avec une autre source de données, les API proposées par ElasticSearch
sont beaucoup plus pratiques. Ces API permettent de connecter une session Python
(idem pour R
)
à un serveur Elastic
afin de communiquer avec lui
(échanger des flux via une API REST).
3.2 ElasticSearch
et Python
En Python
, le package officiel est elasticsearch
.
Ce dernier permet de configurer les paramètres pour interagir avec un serveur, indexer
une ou plusieurs bases, envoyer de manière automatisée un ensemble de requêtes
au serveur, récupérer les résultats directement dans une session Python
…
4 Premières limites de la distance de Levenshtein
Pour évaluer la similarité entre deux données textuelles, il est nécessaire de transformer l’information qualitative qu’est le nom du produit en information quantitative qui permettra de rapprocher différents types de produits. Les ordinateurs ont en effet besoin de transformer les informations textuelles en information numérique pour être en mesure de les exploiter.
On appelle distance de Levenshtein entre deux chaînes de caractères le coût minimal (en nombre d’opérations) pour transformer la première en la seconde par:
- substitution
- insertion
- suppression
La distance de Levenshtein est une mesure très utilisée pour comparer la similarité entre deux
chaînes de caractères. Il existe plusieurs packages pour calculer cette dernière.
fuzzywuzzy
est le plus connu mais ce dernier est assez lent (implémentation en pur Python
).
Le package rapidfuzz
, présenté ici, propose les mêmes fonctionalités mais est plus rapide car implémenté
en C++
qui est plus efficace.
Cependant, nous allons le voir, ce package ne nous
offrira pas des performances
assez bonnes pour que nous puissions
passer à l’échelle.
Voici trois exemples pour évaluer le coût de chaque opération:
import rapidfuzz
[
rapidfuzz.distance.Levenshtein.distance("salut", "slut", weights=(1, 1, 1)
# Suppression
),
rapidfuzz.distance.Levenshtein.distance("salut", "saalut", weights=(1, 1, 1)
# Addition
),
rapidfuzz.distance.Levenshtein.distance("salut", "selut", weights=(1, 1, 1)
# Substitution
), ]
4.1 Premier essai: les produits Ciqual
les plus similaires aux produits de la recette
On pourrait écrire une fonction qui prend en argument une liste de libellés d’intérêt et une liste de candidat au match et renvoie le libellé le plus proche. Cependant, le risque est que cet algorithme soit relativement lent s’il n’est pas codé parfaitement.
Il est, à mon avis, plus simple, quand
on est habitué à la logique Pandas
,
de faire un produit cartésien pour obtenir un vecteur mettant en miroir
chaque produit de notre recette avec l’ensembles des produits Ciqual
et ensuite comparer les deux vecteurs pour prendre,
pour chaque produit, le meilleur match.
Les bases étant de taille limitée, le produit cartésien n’est pas problématique. Avec des bases plus conséquentes, une stratégie plus parcimonieuse en mémoire devrait être envisagée.
Pour faire cette opération, on va utiliser la fonction match_product
de
note script d’utilitaires.
= fc.match_product(libelles, ciqual)
dist_leven dist_leven
Cette première étape naïve est décevante à plusieurs égards:
- Certes, on a des matches cohérent (par exemple “Oignon rouge, cru” et “1 oignon”) mais on a plus de couples incohérents ;
- Le temps de calcul peut apparaître faible mais le passage à l’échelle risque d’être compliqué ;
- Les besoins mémoires sont potentiellement importants lors de l’appel à
rapidfuzz.process.extract
ce qui peut bloquer le passage à l’échelle ; - La distance textuelle n’est pas nécessairement la plus pertinente.
On a, en fait, négligé une étape importante: la normalisation (ou nettoyage des textes) présentée dans la partie NLP, notamment:
- harmonisation de la casse, suppression des accents…
- suppressions des mots outils (e.g. ici on va d’abord négliger les quantités pour trouver la nature de l’aliment, en particulier pour
Ciqual
)
Faisons donc en apparence un retour en arrière qui sera néanmoins salvateur pour améliorer la pertinence des liens faits entre nos bases de données.
5 Preprocessing pour améliorer la pertinence des matches
5.1 Objectif
Le preprocessing correspond à l’ensemble des opérations ayant lieu avant l’analyse à proprement parler. Ici, ce preprocessing est intéressant à plusieurs égards:
- Il réduit le bruit dans nos jeux de données (par exemple des mots de liaisons) ;
- Il permet de normaliser et harmoniser les syntaxes dans nos différentes sources.
L’objectif est ainsi de réduire nos noms de produits à la substantifique moelle pour améliorer la pertinence de la recherche.
Pour être pertinent, le preprocessing comporte généralement deux types de
traitements. En premier lieu, ceux qui sont généraux et applicables
à tous types de corpus textuels: retrait des stopwords, de la ponctuation, etc.
les méthodes disponibles dans la partie NLP.
Ensuite, il est nécessaire de mettre en oeuvre des nettoyages plus spécifiques à chaque corpus.
Par exemple dans la source Ciqual
,
la cuisson est souvent renseignée et bruite les appariemments.
5.2 Démarche
Exercice 1: preprocessing
Pour transformer les lettres avec accents en leur équivalent sans accent, la fonction
unidecode
(du package du même nom) est pratique. La tester sur le jeu de donnéesciqual
en créant une nouvelle colonne nomméelibel_clean
La casse différente selon les jeux de données peut être pénalisante pour trouver des produits similaires. Pour éviter ces problèmes, mettre tout en majuscule.
Les informations sur les quantités ou le packaging peuvent apporter du bruit dans notre comparaison. Nous allons retirer ces mots, à travers la liste
['KG','CL','G','L','CRUE?S?', 'PREEMBALLEE?S?']
, qu’on peut considérer comme un dictionnaire de stop-words métier. Pour cela, il convient d’utiliser une expression régulière dans la méthodestr.replace
dePandas
. Avec ceux-ci, on va utiliser la liste des stop-words de la librairienltk
pour retirer les stop-words classiques (_“le”,“la”, etc.). La librairieSpaCy
, plus riche, pourrait être utilisée ; nous laissons cela sous la forme d’exercice supplémentaire.On a encore des signes de ponctuation ou des chiffres qui peuvent poluer la comparaison. Les retirer grâce à la méthode
replace
et une regex[^a-zA-Z]
Enfin, par sécurité, on peut supprimer les espaces multiples. Utiliser la regex
'([ ]{2,})'
pour cela. Observer le résultat final.(Optionnel). Comme exercice supplémentaire, faire la même chose avec les pipelines
SpaCy
.
A l’issue de la question 1, le jeu de données ciqual
devrait
ressembler à celui-ci :
Après avoir mis en majuscule, on se retrouve avec le jeu de données suivant :
Après retrait des stop-words, nos libellés prennent la forme suivante :
La regex pour éliminer les caractères de ponctuation permet ainsi d’obtenir:
Enfin, à l’issue de la question 5, le DataFrame
obtenu est le suivant :
Ces étapes de nettoyage ont ainsi permis de concentrer l’information dans les noms de produits sur ce qui l’identifie vraiment.
5.3 Approche systématique
Pour systématiser cette approche à nos différents DataFrame
, rien de mieux
qu’une fonction. Celle-ci est présente dans le module functions
sous le nom clean_libelle
.
from functions import clean_libelle
Pour résumer l’exercice précédent, cette fonction va :
- Harmoniser la casse et retirer les accents (voir
functions.py
) ; - Retirer tout les caractères qui ne sont pas des lettres (chiffres, ponctuations) ;
- Retirer les caractères isolés.
import nltk
from nltk.corpus import stopwords
"stopwords")
nltk.download(
= ["KG", "CL", "G", "L", "CRUE?S?", "PREEMBALLEE?S?"]
stop_words += [l.upper() for l in stopwords.words("french")]
stop_words
= {r"[^A-Z]": " ", r"\b[A-Z0-9]{1,2}?\b": " "} # replace_regex
Cela permet d’obtenir les bases nettoyées suivantes :
= clean_libelle(
ciqual ="alim_nom_fr", replace_regex=replace_regex, stopWords=stop_words
ciqual, yvar
)10) ciqual.sample(
= clean_libelle(
openfood ="product_name", replace_regex=replace_regex, stopWords=stop_words
openfood, yvar
)10) openfood.sample(
= pd.DataFrame(libelles, columns=["libel"])
courses = clean_libelle(
courses ="libel", replace_regex=replace_regex, stopWords=stop_words
courses, yvar
)10) courses.sample(
Les noms de produits sont déjà plus harmonisés. Voyons voir si cela permet de trouver un match dans l’Openfood database:
= fc.match_product(courses["libel_clean"], openfood, "libel_clean")
dist_leven_openfood 10) dist_leven_openfood.sample(
Pas encore parfait, mais on progresse sur les produits appariés! Concernant le temps de calcul, les quelques secondes nécessaires à ce calcul peuvent apparaître un faible prix à payer. Cependant, il convient de rappeler que le nombre de produits dans l’ensemble de recherche est faible. Cette solution n’est donc pas généralisable.
5.4 Réduire les temps de recherche
Finalement, l’idéal serait de disposer d’un moteur de recherche adapté à notre besoin, contenant les produits candidats, que l’on pourrait interroger, rapide en lecture, capable de classer les échos renvoyés par pertinence, que l’on pourrait requêter de manière flexible.
Par exemple, on pourrait vouloir signaler qu’un écho nous intéresse seulement si la donnée calorique n’est pas manquante. On pourrait même vouloir qu’il effectue pour nous des prétraitements sur les données.
Cela paraît beaucoup demander. Mais c’est exactement ce que fait ElasticSearch
.
6 Indexer une base
A partir de maintenant, commence, à proprement parler, la démonstration Elastic
.
Cette
partie développe les éléments les plus techniques, à savoir l’indexation d’une base.
Tous les utilisateurs d’Elastic
n’ont pas nécessairement à passer par là, ils peuvent
trouver une base déjà indexée, idéalement par un data engineer qui aura optimisé
les traitements.
Les utilisateurs du SSP Cloud, architecture qui repose sur la technologie Kubernetes peuvent répliquer les éléments de la suite du document.
6.1 Créer un cluster Elastic
sur le DataLab
Pour lancer un service Elastic
, il faut cliquer sur ce lien.
Une fois créé, vous pouvez explorer l’interface graphique Kibana
.
Cependant, grâce à l’API Elastic
de Python
, on se passera de celle-ci. Donc, en pratique,
une fois lancé, pas besoin d’ouvrir ce service Elastic
pour continuer à suivre1.
Dans un terminal, vous pouvez aussi vérifier que vous êtes en mesure de dialoguer avec votre cluster Elastic
,
qui est prêt à vous écouter:
kubectl get statefulset
Passer par la ligne de commande serait peu commode pour industrialiser notre
recherche.
Nous allons utiliser la librairie elasticsearch
pour dialoguer avec notre moteur de recherche Elastic.
Les instructions ci-dessous indiquent comment établir la connection.
from elasticsearch import Elasticsearch
= "elasticsearch-master"
HOST
def elastic():
"""Connection avec Elastic sur le data lab"""
= Elasticsearch(
es "host": HOST, "port": 9200, "scheme": "http"}],
[{=True,
http_compress=200,
request_timeout
)return es
= elastic() es
<Elasticsearch([{'host': 'elasticsearch-master', 'port': 9200}])>
Maintenant que la connection est établie, deux étapes nous attendent:
- Indexation Envoyer les documents parmi lesquels on veut chercher des echos pertinents dans notre elastic. Un index est une collection de document. Nous pourrions en créer deux : un pour les produits ciqual, un pour les produits openfood
- Requête Chercher les documents les plus pertinents suivant une recherche textuelle flexible. Nous allons rechercher les libellés de notre recette et de notre liste de course.
6.2 Première indexation
On crée donc nos deux index:
if not es.indices.exists(index="openfood"):
="openfood")
es.indices.create(indexif not es.indices.exists(index="ciqual"):
="ciqual") es.indices.create(index
Pour l’instant, nos index sont vides! Ils contiennent 0 documents.
="openfood") es.count(index
{'count': 0, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}}
Nous allons en rajouter quelques-uns !
es.create(="openfood",
indexid=1,
={
body"product_name": "Tarte noix de coco",
"product_name_clean": "TARTE NOIX COCO",
},
)
es.create(="openfood",
indexid=2,
={"product_name": "Noix de coco", "product_name_clean": "NOIX COCO"},
body
)
es.create(="openfood",
indexid=3,
={"product_name": "Beurre doux", "product_name_clean": "BEURRE DOUX"},
body )
="openfood") es.count(index
{'count': 3, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}}
Dans l’interface graphique Kibana
,
on peut vérifier que l’indexation
a bien eue lieu en allant dans Management > Stack Management
6.3 Première recherche
Faisons notre première recherche: cherchons des noix de pécan!
="openfood", q="noix de pécan") es.search(index
ObjectApiResponse({'took': 116, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 2, 'relation': 'eq'}, 'max_score': 0.9400072, 'hits': [{'_index': 'openfood', '_type': '_doc', '_id': '2', '_score': 0.9400072, '_source': {'product_name': 'Noix de coco', 'product_name_clean': 'NOIX COCO'}}, {'_index': 'openfood', '_type': '_doc', '_id': '1', '_score': 0.8272065, '_source': {'product_name': 'Tarte noix de coco', 'product_name_clean': 'TARTE NOIX COCO'}}]}})
Intéressons nous aux hits
(résultats pertinents, ou echos) : nous en avons 2.
Le score maximal parmi les hits est mentionné dans max_score
et correspond à celui du deuxième document indexé.
Elastic
nous fournit ici un score de pertinence dans notre recherche d’information, et classe ainsi les documents renvoyés.
Ici nous utilisons la configuration par défaut. Mais comment est calculé ce score? Demandons à Elastic de nous expliquer le score du document 2
dans la requête "noix de pécan"
.
="openfood", id=2, q="noix de pécan") es.explain(index
ObjectApiResponse({'_index': 'openfood', '_type': '_doc', '_id': '2', 'matched': True, 'explanation': {'value': 0.9400072, 'description': 'max of:', 'details': [{'value': 0.49917626, 'description': 'sum of:', 'details': [{'value': 0.49917626, 'description': 'weight(product_name_clean:noix in 1) [PerFieldSimilarity], result of:', 'details': [{'value': 0.49917626, 'description': 'score(freq=1.0), computed as boost * idf * tf from:', 'details': [{'value': 2.2, 'description': 'boost', 'details': []}, {'value': 0.47000363, 'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:', 'details': [{'value': 2, 'description': 'n, number of documents containing term', 'details': []}, {'value': 3, 'description': 'N, total number of documents with field', 'details': []}]}, {'value': 0.48275858, 'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:', 'details': [{'value': 1.0, 'description': 'freq, occurrences of term within document', 'details': []}, {'value': 1.2, 'description': 'k1, term saturation parameter', 'details': []}, {'value': 0.75, 'description': 'b, length normalization parameter', 'details': []}, {'value': 2.0, 'description': 'dl, length of field', 'details': []}, {'value': 2.3333333, 'description': 'avgdl, average length of field', 'details': []}]}]}]}]}, {'value': 0.9400072, 'description': 'sum of:', 'details': [{'value': 0.4700036, 'description': 'weight(product_name:noix in 1) [PerFieldSimilarity], result of:', 'details': [{'value': 0.4700036, 'description': 'score(freq=1.0), computed as boost * idf * tf from:', 'details': [{'value': 2.2, 'description': 'boost', 'details': []}, {'value': 0.47000363, 'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:', 'details': [{'value': 2, 'description': 'n, number of documents containing term', 'details': []}, {'value': 3, 'description': 'N, total number of documents with field', 'details': []}]}, {'value': 0.45454544, 'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:', 'details': [{'value': 1.0, 'description': 'freq, occurrences of term within document', 'details': []}, {'value': 1.2, 'description': 'k1, term saturation parameter', 'details': []}, {'value': 0.75, 'description': 'b, length normalization parameter', 'details': []}, {'value': 3.0, 'description': 'dl, length of field', 'details': []}, {'value': 3.0, 'description': 'avgdl, average length of field', 'details': []}]}]}]}, {'value': 0.4700036, 'description': 'weight(product_name:de in 1) [PerFieldSimilarity], result of:', 'details': [{'value': 0.4700036, 'description': 'score(freq=1.0), computed as boost * idf * tf from:', 'details': [{'value': 2.2, 'description': 'boost', 'details': []}, {'value': 0.47000363, 'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:', 'details': [{'value': 2, 'description': 'n, number of documents containing term', 'details': []}, {'value': 3, 'description': 'N, total number of documents with field', 'details': []}]}, {'value': 0.45454544, 'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:', 'details': [{'value': 1.0, 'description': 'freq, occurrences of term within document', 'details': []}, {'value': 1.2, 'description': 'k1, term saturation parameter', 'details': []}, {'value': 0.75, 'description': 'b, length normalization parameter', 'details': []}, {'value': 3.0, 'description': 'dl, length of field', 'details': []}, {'value': 3.0, 'description': 'avgdl, average length of field', 'details': []}]}]}]}]}]}})
Elastic
nous explique donc que le score 0.9400072
est le maximum entre deux sous-scores, 0.4991
et 0.9400072
.
Pour chacun de ces sous-scores, le détail de son calcul est donné. Le premier sous-score n’a accordé un score que par rapport au premier mot (noix), tandis que le second a accordé un score sur la base des deux mots déjà connu dans les documents (“noix” et “de”). Il a ignoré pécan! Jusqu’à présent, ce terme n’est pas connu dans l’index.
La pertinence d’un mot pour notre recherche est construite sur une variante de la TF-IDF
,
considérant qu’un terme est pertinent s’il est souvent présent dans le document (Term Frequency)
alors qu’il est peu fréquent dans les autres document (inverse document frequency).
Ici les notations des documents 1 et 2 sont très proches, la différence est dûe à des IDF plus faibles dans le document 1,
qui est pénalisé pour être légérement plus long.
Bref, tout ça est un peu lourd, mais assez efficace, en tout cas moins rudimentaire que les distances caractères à caractères pour ramener des echos pertinents.
6.4 Limite de cette première indexation
Pour l’instant, Elastic n’a pas l’air de gérer les fautes de frappes! Pas le droit à l’erreur dans la requête:
="openfood", q="TART NOI") es.search(index
ObjectApiResponse({'took': 38, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 0, 'relation': 'eq'}, 'max_score': None, 'hits': []}})
Cela s’explique par la représentation des champs (‘product_name’ par exemple) qu’Elastic
a inféré,
puisque nous n’avons rien spécifié.
La représentation d’une variable conditionne la façon dont les champs sont analysés pour calculer la pertinence.
Par exemple, regardons la représentation du champ product_name
="openfood", fields="product_name") es.indices.get_field_mapping(index
ObjectApiResponse({'openfood': {'mappings': {'product_name': {'full_name': 'product_name', 'mapping': {'product_name': {'type': 'text', 'fields': {'keyword': {'type': 'keyword', 'ignore_above': 256}}}}}}}})
Elastic
a compris qu’il s’agissait d’un champ textuel.
En revanche, le type est keyword
n’autorise pas des analyses approximatives donc
ne permet pas de tenir compte de fautes de frappes.
Pour qu’un echo remonte, un des termes doit matcher exactement. Dommage ! Mais c’est parce qu’on a utilisé le mapping par défaut. En réalité, il est assez simple de préciser un mapping plus riche, autorisant une analyse “fuzzy” ou “flou”.
7 Améliorer l’indexation
On peut spécifier la façon dont l’on souhaite analyser le texte. Par exemple, on peut préciser que l’on souhaite enlever des stopwords, raciniser, analyser les termes via des n-grammes pour rendre la recherche plus robuste aux fautes de frappes…
Ces concepts sont présentés dans la partie NLP. Pour une présentation plus complète, voir la documentation officielle d’Elastic
On propose les analyseurs stockés dans un fichier schema.json
Les n-grammes sont des séquences de n caractères ou plus généralement n éléments qui s’enchaînent séquentiellement. Par exemple, NOI et OIX sont des tri-grammes de caractères dans NOIX.
Comparer les n-grammes composant des libellés peut permettre d’avoir dans des comparaisons à fautes de frappe/abbréviations près. Cela fait aussi plus de comparaisons à opérer ! D’où également, l’intérêt d’Elastic, qui intégre facilement et efficacement ces comparaisons.
On va préciser un peu le schéma de données qu’on souhaite indexer, et aussi préciser comment les différents champs seront analysés.
7.0.1 Une indexation plus adaptée
import json
if es.indices.exists(index="openfood"):
="openfood")
es.indices.delete(index
with open("schema.json") as f:
= json.load(f)
mapping
="openfood", body=mapping) es.indices.create(index
Maintenant, les champs textuels “product_name” et “product_name_clean” vont pouvoir être analysé aussi via leur n-grammes et après racinisation (et l’un n’exclut pas l’autre!)
="openfood", fields="product_name") es.indices.get_field_mapping(index
ObjectApiResponse({'openfood': {'mappings': {'product_name': {'full_name': 'product_name', 'mapping': {'product_name': {'type': 'text', 'fields': {'keyword': {'type': 'keyword', 'ignore_above': 256}, 'ngr': {'type': 'text', 'analyzer': 'ngram_analyzer'}, 'stem': {'type': 'text', 'analyzer': 'stem_analyzer'}}}}}}}})
C’est parti, on envoie toute notre base OpenFood pour pouvoir la requêter !
La fonction suivante (index_elastic
) va vous faire gagner du temps pour indexer
car indexer chaque produit à la main n’est pas très efficace.
Du coup ça prend quelques minutes… mais c’est pour nous en faire gagner ensuite. Cette opération est faite une fois, pour préparer des requêtes potentiellement nombreuses!
fc.index_elastic(=es,
es="openfood",
index_name="schema.json",
setting_file=openfood[
df"product_name", "libel_clean", "energy_100g", "nutriscore_score"]
[
].drop_duplicates(), )
Temps d'exécution total : 125.57 secondes ---
="openfood")["count"] es.count(index
738366
7.1 Nos premières requêtes
Vérifions qu’on recupère quelques tartes aux noix même si l’on fait plein de fautes:
="openfood", q="TART NOI", size=3) es.search(index
ObjectApiResponse({'took': 60, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 10000, 'relation': 'gte'}, 'max_score': 22.837925, 'hits': [{'_index': 'openfood', '_type': '_doc', '_id': '405332', '_score': 22.837925, '_source': {'product_name': 'Tarte noix', 'libel_clean': 'TARTE NOIX', 'energy_100g': 1833.0, 'nutriscore_score': 23.0}}, {'_index': 'openfood', '_type': '_doc', '_id': '1103594', '_score': 22.82367, '_source': {'product_name': 'Tarte aux noix', 'libel_clean': 'TARTE NOIX', 'energy_100g': 4.0, 'nutriscore_score': 4.0}}, {'_index': 'openfood', '_type': '_doc', '_id': '1150755', '_score': 22.82367, '_source': {'product_name': 'Tarte aux noix', 'libel_clean': 'TARTE NOIX', 'energy_100g': 1929.0, 'nutriscore_score': 21.0}}]}})
Si on préfère sous une forme de DataFrame
:
= pd.json_normalize(
df ="openfood", q="TART NOI", size=3)["hits"]["hits"]
es.search(index
)= df.columns.str.replace("_source.", "", regex=False)
df.columns 2) df.head(
_index | _type | _id | _score | product_name | libel_clean | energy_100g | nutriscore_score | |
---|---|---|---|---|---|---|---|---|
0 | openfood | _doc | 405332 | 22.837925 | Tarte noix | TARTE NOIX | 1833.0 | 23.0 |
1 | openfood | _doc | 1103594 | 22.823670 | Tarte aux noix | TARTE NOIX | 4.0 | 4.0 |
2 | openfood | _doc | 1150755 | 22.823670 | Tarte aux noix | TARTE NOIX | 1929.0 | 21.0 |
Pour automatiser l’envoi de requêtes et la récupération du meilleur écho, on peut définir la fonction suivante
def matchElastic(libelles):
= time.time()
start_time = {}
matches for l in libelles:
= es.search(index="openfood", q=l, size=1)
response if len(response["hits"]["hits"]) > 0:
= pd.json_normalize(response["hits"]["hits"])
matches[l] print(80 * "-")
print(f"Temps d'exécution total : {(time.time() - start_time):.2f} secondes ---")
return matches
= matchElastic(courses["libel_clean"])
matches = pd.concat(matches)
matches 3) matches.sample(
_index | _type | _id | _score | _source.product_name | _source.libel_clean | _source.energy_100g | _source.nutriscore_score | ||
---|---|---|---|---|---|---|---|---|---|
GOUSSE AIL | 0 | openfood | _doc | 1982062 | 57.93140 | Gousse d\'ail | GOUSSE AIL | 498.0 | 5.0 |
IGP SAUVIGNON | 0 | openfood | _doc | 1801406 | 96.55756 | vin blanc Sauvignon | VIN BLANC SAUVIGNON | 66.3 | 1.0 |
POTIRON | 0 | openfood | _doc | 1043961 | 75.96385 | Potiron | POTIRON | 172.0 | 0.0 |
Et voilà, on a un outil très rapide de requête ! La pertinence des résultats est encore douteuse. Pour cela, il conviendrait de préciser des requêtes plus sophistiquées!2
= {
req "bool": {
"should": [
"match": {"libel_clean": {"query": "HUILE OLIVE", "boost": 10}}},
{"match": {"libel_clean.ngr": "HUILE OLIVE"}},
{
],"minimum_should_match": 1,
"filter": [{"range": {"nutriscore_score": {"gte": 10, "lte": 20}}}],
} }
= es.search(index="openfood", query=req, size=1)
out "hits"]["hits"]) pd.json_normalize(out[
_index | _type | _id | _score | _source.product_name | _source.libel_clean | _source.energy_100g | _source.nutriscore_score | |
---|---|---|---|---|---|---|---|---|
0 | openfood | _doc | 960041 | 174.27896 | Huile d olive | HUILE OLIVE | 3761.0 | 11.0 |
Qu’a-t-on demandé ici?
- De renvoyer 1 et 1 seul echo ("size":"1"
) et seulement si celui ci a:
+ "should"
: Au moins un ("minimum_should_match":"1"
) des termes des deux champs libel_clean
et libel_clean.ngr
qui matche sur un terme de HUILE OLIVE, l’analyse (la définition du “terme”) étant réalisé soit en tant que text
(“libel_clean”) soit en tant que n-gramme ngr
(“libel_clean.ngr”, une analyse que nous avons spécifié dans le mapping)
+ "filter"
: Le champ float
nutriscore_score
doit être compris entre 10 et 20 (“filter”).
A noter :
- Les clauses (
"should"
+"minimum_should_match":"1"
) peuvent être remplacé par un"must"
. Auquel cas, l’écho doit obligatoirement matcher sur chaque clause. - Préciser dans
"filter"
(plutôt que dans"should"
) une condition signifie que celle-ci ne participe pas au score de pertinence.
On n’a pas encore un appariemment très satisfaisant, en particulier sur les boissons. Comment faire ? La réponse est dans Galiana et Suarez Castillo (2022)
A vous, de calculer le nombre de calories de notre recette de course !
Informations additionnelles
environment files have been tested on.
Python version used:
Package | Version |
---|---|
affine | 2.4.0 |
aiobotocore | 2.15.1 |
aiohappyeyeballs | 2.4.3 |
aiohttp | 3.10.8 |
aioitertools | 0.12.0 |
aiosignal | 1.3.1 |
alembic | 1.13.3 |
altair | 5.4.1 |
aniso8601 | 9.0.1 |
annotated-types | 0.7.0 |
appdirs | 1.4.4 |
archspec | 0.2.3 |
asttokens | 2.4.1 |
attrs | 24.2.0 |
babel | 2.16.0 |
bcrypt | 4.2.0 |
beautifulsoup4 | 4.12.3 |
black | 24.8.0 |
blinker | 1.8.2 |
blis | 0.7.11 |
bokeh | 3.5.2 |
boltons | 24.0.0 |
boto3 | 1.35.23 |
botocore | 1.35.23 |
branca | 0.7.2 |
Brotli | 1.1.0 |
bs4 | 0.0.2 |
cachetools | 5.5.0 |
cartiflette | 0.0.2 |
Cartopy | 0.24.1 |
catalogue | 2.0.10 |
cattrs | 24.1.2 |
certifi | 2024.8.30 |
cffi | 1.17.1 |
charset-normalizer | 3.3.2 |
chromedriver-autoinstaller | 0.6.4 |
click | 8.1.7 |
click-plugins | 1.1.1 |
cligj | 0.7.2 |
cloudpathlib | 0.20.0 |
cloudpickle | 3.0.0 |
colorama | 0.4.6 |
comm | 0.2.2 |
commonmark | 0.9.1 |
conda | 24.9.1 |
conda-libmamba-solver | 24.7.0 |
conda-package-handling | 2.3.0 |
conda_package_streaming | 0.10.0 |
confection | 0.1.5 |
contextily | 1.6.2 |
contourpy | 1.3.0 |
cryptography | 43.0.1 |
cycler | 0.12.1 |
cymem | 2.0.8 |
cytoolz | 1.0.0 |
dask | 2024.9.1 |
dask-expr | 1.1.15 |
databricks-sdk | 0.33.0 |
debugpy | 1.8.6 |
decorator | 5.1.1 |
Deprecated | 1.2.14 |
diskcache | 5.6.3 |
distributed | 2024.9.1 |
distro | 1.9.0 |
docker | 7.1.0 |
duckdb | 0.10.1 |
en-core-web-sm | 3.7.1 |
entrypoints | 0.4 |
et_xmlfile | 2.0.0 |
exceptiongroup | 1.2.2 |
executing | 2.1.0 |
fastexcel | 0.11.6 |
fastjsonschema | 2.20.0 |
fiona | 1.10.1 |
Flask | 3.0.3 |
folium | 0.17.0 |
fontawesomefree | 6.6.0 |
fonttools | 4.54.1 |
fr-core-news-sm | 3.7.0 |
frozendict | 2.4.4 |
frozenlist | 1.4.1 |
fsspec | 2023.12.2 |
funcy | 2.0 |
gensim | 4.3.2 |
geographiclib | 2.0 |
geopandas | 1.0.1 |
geoplot | 0.5.1 |
geopy | 2.4.1 |
gitdb | 4.0.11 |
GitPython | 3.1.43 |
google-auth | 2.35.0 |
graphene | 3.3 |
graphql-core | 3.2.4 |
graphql-relay | 3.2.0 |
graphviz | 0.20.3 |
great-tables | 0.12.0 |
greenlet | 3.1.1 |
gunicorn | 22.0.0 |
h11 | 0.14.0 |
h2 | 4.1.0 |
hpack | 4.0.0 |
htmltools | 0.6.0 |
hyperframe | 6.0.1 |
idna | 3.10 |
imageio | 2.36.0 |
importlib_metadata | 8.5.0 |
importlib_resources | 6.4.5 |
inflate64 | 1.0.0 |
ipykernel | 6.29.5 |
ipython | 8.28.0 |
itsdangerous | 2.2.0 |
jedi | 0.19.1 |
Jinja2 | 3.1.4 |
jmespath | 1.0.1 |
joblib | 1.4.2 |
jsonpatch | 1.33 |
jsonpointer | 3.0.0 |
jsonschema | 4.23.0 |
jsonschema-specifications | 2024.10.1 |
jupyter-cache | 1.0.0 |
jupyter_client | 8.6.3 |
jupyter_core | 5.7.2 |
kaleido | 0.2.1 |
kiwisolver | 1.4.7 |
langcodes | 3.5.0 |
language_data | 1.3.0 |
lazy_loader | 0.4 |
libmambapy | 1.5.9 |
locket | 1.0.0 |
lxml | 5.3.0 |
lz4 | 4.3.3 |
Mako | 1.3.5 |
mamba | 1.5.9 |
mapclassify | 2.8.1 |
marisa-trie | 1.2.1 |
Markdown | 3.6 |
markdown-it-py | 3.0.0 |
MarkupSafe | 2.1.5 |
matplotlib | 3.9.2 |
matplotlib-inline | 0.1.7 |
mdurl | 0.1.2 |
menuinst | 2.1.2 |
mercantile | 1.2.1 |
mizani | 0.11.4 |
mlflow | 2.16.2 |
mlflow-skinny | 2.16.2 |
msgpack | 1.1.0 |
multidict | 6.1.0 |
multivolumefile | 0.2.3 |
munkres | 1.1.4 |
murmurhash | 1.0.10 |
mypy-extensions | 1.0.0 |
narwhals | 1.14.1 |
nbclient | 0.10.0 |
nbformat | 5.10.4 |
nest_asyncio | 1.6.0 |
networkx | 3.3 |
nltk | 3.9.1 |
numexpr | 2.10.1 |
numpy | 1.26.4 |
opencv-python-headless | 4.10.0.84 |
openpyxl | 3.1.5 |
opentelemetry-api | 1.16.0 |
opentelemetry-sdk | 1.16.0 |
opentelemetry-semantic-conventions | 0.37b0 |
outcome | 1.3.0.post0 |
OWSLib | 0.28.1 |
packaging | 24.1 |
pandas | 2.2.3 |
paramiko | 3.5.0 |
parso | 0.8.4 |
partd | 1.4.2 |
pathspec | 0.12.1 |
patsy | 0.5.6 |
Pebble | 5.0.7 |
pexpect | 4.9.0 |
pickleshare | 0.7.5 |
pillow | 10.4.0 |
pip | 24.2 |
platformdirs | 4.3.6 |
plotly | 5.24.1 |
plotnine | 0.13.6 |
pluggy | 1.5.0 |
polars | 1.8.2 |
preshed | 3.0.9 |
prometheus_client | 0.21.0 |
prometheus_flask_exporter | 0.23.1 |
prompt_toolkit | 3.0.48 |
protobuf | 4.25.3 |
psutil | 6.0.0 |
ptyprocess | 0.7.0 |
pure_eval | 0.2.3 |
py7zr | 0.20.8 |
pyarrow | 17.0.0 |
pyarrow-hotfix | 0.6 |
pyasn1 | 0.6.1 |
pyasn1_modules | 0.4.1 |
pybcj | 1.0.2 |
pycosat | 0.6.6 |
pycparser | 2.22 |
pycryptodomex | 3.21.0 |
pydantic | 2.9.2 |
pydantic_core | 2.23.4 |
Pygments | 2.18.0 |
pyLDAvis | 3.4.1 |
PyNaCl | 1.5.0 |
pynsee | 0.1.8 |
pyogrio | 0.10.0 |
pyOpenSSL | 24.2.1 |
pyparsing | 3.1.4 |
pyppmd | 1.1.0 |
pyproj | 3.7.0 |
pyshp | 2.3.1 |
PySocks | 1.7.1 |
python-dateutil | 2.9.0 |
python-dotenv | 1.0.1 |
python-magic | 0.4.27 |
pytz | 2024.1 |
pyu2f | 0.1.5 |
pywaffle | 1.1.1 |
PyYAML | 6.0.2 |
pyzmq | 26.2.0 |
pyzstd | 0.16.2 |
querystring_parser | 1.2.4 |
rasterio | 1.4.2 |
referencing | 0.35.1 |
regex | 2024.9.11 |
requests | 2.32.3 |
requests-cache | 1.2.1 |
retrying | 1.3.4 |
rich | 13.9.4 |
rpds-py | 0.21.0 |
rsa | 4.9 |
Rtree | 1.3.0 |
ruamel.yaml | 0.18.6 |
ruamel.yaml.clib | 0.2.8 |
s3fs | 2023.12.2 |
s3transfer | 0.10.2 |
scikit-image | 0.24.0 |
scikit-learn | 1.5.2 |
scipy | 1.13.0 |
seaborn | 0.13.2 |
selenium | 4.26.1 |
setuptools | 74.1.2 |
shapely | 2.0.6 |
shellingham | 1.5.4 |
six | 1.16.0 |
smart-open | 7.0.5 |
smmap | 5.0.0 |
sniffio | 1.3.1 |
sortedcontainers | 2.4.0 |
soupsieve | 2.5 |
spacy | 3.7.5 |
spacy-legacy | 3.0.12 |
spacy-loggers | 1.0.5 |
SQLAlchemy | 2.0.35 |
sqlparse | 0.5.1 |
srsly | 2.4.8 |
stack-data | 0.6.2 |
statsmodels | 0.14.4 |
tabulate | 0.9.0 |
tblib | 3.0.0 |
tenacity | 9.0.0 |
texttable | 1.7.0 |
thinc | 8.2.5 |
threadpoolctl | 3.5.0 |
tifffile | 2024.9.20 |
toolz | 1.0.0 |
topojson | 1.9 |
tornado | 6.4.1 |
tqdm | 4.66.5 |
traitlets | 5.14.3 |
trio | 0.27.0 |
trio-websocket | 0.11.1 |
truststore | 0.9.2 |
typer | 0.13.1 |
typing_extensions | 4.12.2 |
tzdata | 2024.2 |
Unidecode | 1.3.8 |
url-normalize | 1.4.3 |
urllib3 | 1.26.20 |
wasabi | 1.1.3 |
wcwidth | 0.2.13 |
weasel | 0.4.1 |
webdriver-manager | 4.0.2 |
websocket-client | 1.8.0 |
Werkzeug | 3.0.4 |
wheel | 0.44.0 |
wordcloud | 1.9.3 |
wrapt | 1.16.0 |
wsproto | 1.2.0 |
xgboost | 2.1.1 |
xlrd | 2.0.1 |
xyzservices | 2024.9.0 |
yarl | 1.13.1 |
yellowbrick | 1.5 |
zict | 3.0.0 |
zipp | 3.20.2 |
zstandard | 0.23.0 |
View file history
SHA | Date | Author | Description |
---|---|---|---|
c326488 | 2024-10-10 14:31:57 | Romain Avouac | Various fixes (#565) |
580cba7 | 2024-08-07 18:59:35 | Lino Galiana | Multilingual version as quarto profile (#533) |
005d89b | 2023-12-20 17:23:04 | Lino Galiana | Finalise l’affichage des statistiques Git (#478) |
3fba612 | 2023-12-17 18:16:42 | Lino Galiana | Remove some badges from python (#476) |
4cd44f3 | 2023-12-11 17:37:50 | Antoine Palazzolo | Relecture NLP (#474) |
1f23de2 | 2023-12-01 17:25:36 | Lino Galiana | Stockage des images sur S3 (#466) |
a06a268 | 2023-11-23 18:23:28 | Antoine Palazzolo | 2ème relectures chapitres ML (#457) |
69cf52b | 2023-11-21 16:12:37 | Antoine Palazzolo | [On-going] Suggestions chapitres modélisation (#452) |
889a71b | 2023-11-10 11:40:51 | Antoine Palazzolo | Modification TP 3 (#443) |
df6a66b | 2023-11-05 10:56:31 | Julien PRAMIL | Pb redirection snippet elastic (#445) |
652009d | 2023-10-09 13:56:34 | Lino Galiana | Finalise le cleaning (#430) |
a771183 | 2023-10-09 11:27:45 | Antoine Palazzolo | Relecture TD2 par Antoine (#418) |
154f09e | 2023-09-26 14:59:11 | Antoine Palazzolo | Des typos corrigées par Antoine (#411) |
9977c5d | 2023-08-28 10:43:36 | Lino Galiana | Fix bug path pandas (#397) |
8082302 | 2023-08-25 17:48:36 | Lino Galiana | Mise à jour des scripts de construction des notebooks (#395) |
3bdf3b0 | 2023-08-25 11:23:02 | Lino Galiana | Simplification de la structure 🤓 (#393) |
29ff3f5 | 2023-07-07 14:17:53 | linogaliana | description everywhere |
f21a24d | 2023-07-02 10:58:15 | Lino Galiana | Pipeline Quarto & Pages 🚀 (#365) |
003a625 | 2022-10-28 18:53:05 | Lino Galiana | Rebuild (#311) |
e3a6b2d | 2022-10-28 08:01:32 | Lino Galiana | Proposition utilisation minio pour download openfood (#307) |
044abdb | 2022-10-25 13:57:53 | Lino Galiana | Finalise tuto elastic (#306) |
3e26719 | 2022-10-24 19:09:22 | Lino Galiana | Tutoriel Elastic reprise (#305) |
1f1668a | 2022-10-24 10:02:16 | Lino Galiana | Corrige tutoriel Elastic (#303) |
f10815b | 2022-08-25 16:00:03 | Lino Galiana | Notebooks should now look more beautiful (#260) |
bacb5a0 | 2022-07-04 19:05:20 | Lino Galiana | Enrichir la partie elastic (#241) |
12965ba | 2022-05-25 15:53:27 | Lino Galiana | :launch: Bascule vers quarto (#226) |
2a8809f | 2021-10-27 12:05:34 | Lino Galiana | Simplification des hooks pour gagner en flexibilité et clarté (#166) |
b5615b4 | 2021-09-09 16:52:47 | Lino Galiana | Un petit tuto Elastic (#135) |
8166e22 | 2021-09-09 11:06:20 | Lino Galiana | Wordcloud du site (#136) |
Les références
Galiana, Lino, et Milena Suarez Castillo. 2022. « Fuzzy Matching on Big-Data: An Illustration with Scanner and Crowd-Sourced Nutritional Datasets ». In Proceedings of the 2022 ACM Conference on Information Technology for Social Good, 331‑37. GoodIT ’22. New York, NY, USA: Association for Computing Machinery. https://doi.org/10.1145/3524458.3547244.
Notes de bas de page
Le lancement du service a créé dans votre
NAMESPACE Kubernetes
(l’ensemble de tout vos services) un clusterElastic
. Vous n’avez droit qu’à un cluster par namespace (ou compte d’utilisateur). Votre serviceJupyter
,VSCode
,RStudio
, etc. est associé au même namespace. De même qu’il n’est pas nécessaire de comprendre comment fonctionne le moteur d’une voiture pour conduire, il n’est pas nécessaire de comprendre la manière dont tout ce beau monde dialogue pour pouvoir utiliser leSSP Cloud
.↩︎Vous pouvez aussi explorer les possibilités de requêtes via la doc Elastic et vous entrainer à un écrire avec votre index tout neuf.↩︎
Citation
BibTeX
@book{galiana2023,
author = {Galiana, Lino},
title = {Python pour la data science},
date = {2023},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {fr}
}
Veuillez citer ce travail comme suit :
Galiana, Lino. 2023. Python pour la data science. https://doi.org/10.5281/zenodo.8229676.