Lodex permet l’enrichissement et le traitement des colonnes du jeu de données à travers l’usage de web service. Cette fonctionnalité peut également être utilisées en “mode avancé” pour créer de nouvelle colonne à partir des colonnes existantes, voici quelques recettes prêtes à l’emploi :
Les fondamentaux de value et self
Les enrichissements ou traitements de données peuvent se faire dans Lodex ou dans un Loader à l’import d’un jeu de données. La syntaxe à employer est légèrement différente selon que l’on écrit des fonctions dans l’un ou dans l’autre.
Dans un Loader, pour créer une nouvelle colonne, on utilisera l’instruction [assign], suivie du nom de la colonne dans “path” puis de la ou des valeurs que l’on souhaite transformer dans “value”.
[assign] path = allKeywords value = get('keywordsAuthor').concat(self.KeywordsMesh)
Dans Lodex en revanche, le nom de la nouvelle colonne est à renseigner dans le nom de l’enrichissement, on assignera systématiquement “value” à “path”, enfin on préfixera toujours le nom du champs à récupérer par “value.”.
[assign] path = value value = get('value.keywordsAuthor')
Le terme “value” désigne ici non pas une valeur, mais le dataset complet ! Ainsi ‘value.keywordsAuthor’ désigne le chemin, à savoir le champ keywordsAuthor contenu dans le dataset (les points caractérisent une imbrication).
Cela signifie que dans le contexte de notre colonne, nous avons importé l’intégralité du dataset par le biais de “get(‘value’)”, que nous avons écrasé par le seul champ “keywordsAuthor”.
C’est pourquoi lorsque l’on souhaite concaténer un autre champ on ne peut plus écrire simplement “value.keywordsMesh”, value ne contenant plus que les mots-clés d’auteurs.
Il faut dans ce cas utiliser “self” (et enlever les quotes par la même occasion).
value = get('value.keywordsAuthor').concat(self.value.KeywordsMesh)
Self est ici une référence constante à l’objet initial, “self.value” permet donc de ré-interroger l’intégralité du dataset tel qu’il est avant la création de la nouvelle colonne en cours.
En conclusion, on peut dire que “value” représente le dataset modifié à un instant donné, alors que “self.value” représente l’objet complet avant toute transformation.
Quelques conventions de nommage
Premièrement, sur le nommage des colonnes, il est plutôt déconseillé d’inclure des caractères spéciaux ou des espaces, cela rendra plus difficile d’accèder à ces champs lors de fonctions complexes ultérieures.
De plus, pour des besoins métiers il est évidemment préférable de nommer les champs en fonction de ce qu’ils contiennent. Mais pour des champs intermédiaires et/ou transformés il est plus lisible d’expliciter les transformations effectuées. Par exemple, authorName dans le 1er cas et authorNameDeburred dans le second pour faire état d’une transformation (usage de la fonction deburr en l’occurence).
Deuxièmement, quelques conseils de nommage des variables ou propriétés que l’on utilisera. Dans de nombreux exemples que l’on peut rencontrer, et qui font appels à des fonctions anonymes symbolisées par “=>”, on voit toutes sortes d’écritures employées pour nommer des variables :
.filter(x => ['France'].includes(x)) .map(v => String(v).trim()) .map((item, i) => `${i+1} - ${item}`) .filter(value=> !value.startsWith("E-mail"))
Le dernier exemple en particulier peut prêter à confusion, puisque value ne désigne ici pas le dataset comme expliqué plus haut, mais est simplement un nommage arbitraire pour désigner les valeurs ou éléments d’un tableau.v, x et item représentent la même chose.
Lorsqu’une fonction a 2 arguments comme map((item, i), le 1er argument est toujours une valeur et le second est sa position dans le tableau. i est donc ici la contraction d’index.
Ainsi, pour une meilleure lisibilité, il est conseillé de nommer ses variables en fonction de ce qu’elles représentent. Par exemple
.filter(country => ['France'].includes(country)) .map(title => String(title).trim()) .map((catégorie, index) => `${index+1} - ${catégorie}`) .filter(address=> !address.startsWith("E-mail"))
Créer une colonne avec une valeur fixe et identique
[assign] path = value value = fix('OK')
Ces instructions créent une colonne où chaque ligne a comme valeur “OK”. On peut également l’écrire de cette façon :
[assign] path = value value = OK
Créer un champ booléen indiquant la présence de certaines valeurs
[assign] path = contientAouB value = get('value).castArray().some(v => ['VAL A', 'VAL B'].includes(v))
Ces instructions créent une colonne (contientAouB) contenant une valeur booléenne indiquant si certaines valeurs (VAL A ou VAL B) sont contenues dans une liste ou un tableau de chaines de caractères (value).
Créer un champ booléen indiquant la présence de certaines valeurs (bis)
[assign] path = contientPresqueMIL value = get('value').castArray().some(v => String(v).search(/mil[aeiuo]+/i) !== -1 )
Ces instructions créent une colonne (contientPresqueMIL) contenant une valeur booléenne indiquant si certaines valeurs contenues dans une liste ou un tableau de chaines de caractères respectent une expression régulière particulière (/mil[aeiou]+/i).
Créer une colonne à partir de la valeur d’un objet d’une autre colonne
[assign] path = value value = get('value.unpaywall.is_oa')
Ces instructions créent une colonne contenant une valeur (is_oa) contenu dans l’objet d’une autre colonne (unpaywall)
Créer une colonne à partir de la valeur modifiée d’une autre colonne
[assign] path = value value = get('value.definition@fr', 'VIDE').toLower().prepend('PREFIX>').append('<SUFIXE')
Ces instructions créent une colonne où chaque ligne contient la valeur de la colonne “definition@fr” mise en minuscule et à laquelle est ajouté un préfixe et un suffixe. Si la colonne “definition@fr” est vide ou n’existe pas, la valeur par défaut sera “VIDE”.
Remplacer les lignes vides ou inexistantes d’une colonne par un texte prédéfini
[assign] path = value value = get('value.definition@en') [swing] test = get('value').isEmpty() [swing/assign] path = value value = fix('non renseigné')
Ces instructions remplacent les lignes vides ou inexistantes par la chaîne “non renseigné”.
On peut également écrire cela de façon plus compacte en utilisant la fonction thru. Fonction qui “passe au travers” des données sans en modifier le format, elle est donc indépendante du format et peut s’appliquer aussi bien aux données primitives, qu’aux tableaux ou aux objets. C’est une façon plus efficace mais aussi plus sécurisée.
[assign] path = value value = get('value.definition@en').thru(definition=>_.isEmpty(definition) ? "non renseigné":definition)
Cette fonction teste si le champ definition@en est vide, si c’est le cas elle retourne “non renseigné”, si non elle retourne la valeur présente. Elle peut aussi être rédigée de façon plus explicite, mais par conséquent plus longue.
value = get('value.definition@en').thru(definition => {if (_.isEmpty(definition)) {return "non renseigné"} else {return definition}})
Modifier le contenu de toutes les lignes d’une colonne sans en créer une nouvelle
[assign] path = value value = get('value.title').capitalize()
Ces instructions transforment toutes les valeurs de la colonne “title” en s’assurant que le premier caractère sera en majuscule et que les suivants seront en minuscule.
IMPORTANT : pour modifier directement une colonne, il est nécessaire de créer un enrichissement ayant le même nom que la colonne à modifier.
Remplacer la colonne URI générée automatiquement par une colonne préexistante
[assign] path = value value = get('value.UT')
Ces instructions utilisent la colonne “UT” pour remplacer la colonne “uri” générée automatiquement.
NOTE : pour modifier directement une uri, il est nécessaire de créer un enrichissement ayant comme nom “uri”.
WARNING : la colonne uri doit impérativement contenir des valeurs distinctes. Si ce n’est pas le cas, la publication et d’autres enrichissements seront impossibles.
Créer une colonne URI avec valeurs distinctes.
[identify] path = value
Ajouter un identifiant pérenne de type ARK
[assign] path = value value = get('value.uri') [expand] size = 10 path=value [expand/URLConnect] url = https://ark-tools.services.inist.fr/v1/67375/stamp?subpublisher=XXX
Ces instructions vont attribuer un identifiant pérenne à chaque ligne. XXX est à remplacer par le code du “subpublisher” approprié et enregistré dans le registre de l’Inist : http://inist-registry.ark.inist.fr/
WARNING : Chaque identifiant ARK créé par ce web service est créé uniquement une seule fois. Si il n’est pas utilisé, l’identifiant est perdu.
ASTUCE : Pour remplacer l’uid attribué automatiquement par Lodex, il suffit de créer un enrichissement nommé uri.
Remplacer des UID par des identifiants ARK
[assign] path = value value = get('value.uri') [swing] test = get('value').startsWith('uid:/') [swing/expand] size = 10 path = value [swing/expand/URLConnect] url = https://ark-tools.services.inist.fr/v1/67375/stamp?subpublisher=XXX
Ces instructions vont attribuer un identifiant pérenne à chaque ligne si et seulement si elle possède un uid comme uri. XXX est à remplacer par le code du “subpublisher” approprié et enregistré dans le registre de l’Inist : http://inist-registry.ark.inist.fr/
WARNING : Chaque identifiant ARK créé par ce web service est créé uniquement une seule fois. Si il n’est pas utilisé, l’identifiant est perdu.
ASTUCE : Pour remplacer l’uid attribué automatiquement par Lodex, il suffit de créer un enrichissement nommé uri.
Créer une colonne avec le préfixe d’un DOI
[assign] path = value value = get('value.DI').split('/').head()
Modifier et/ou corriger certaines valeurs d’une colonne
[assign] path = value value = get('value.Revue').replace('Nature Phys.','Nature Physics.')
NOTE : pour modifier directement une colonne, il est nécessaire de créer un enrichissement ayant comme nom le même nom que la colonne à modifier.
Attention ! L’usage de la fonction replace peut poser plusieurs problèmes. La collision de termes notamment, que l’on peut rapidement rencontrer dès lors que l’on souhaite effectuer plusieurs remplacements.
[assign] path = value value = get('value.species').replace("ant","insect").replace("antelope","mammal")
Ce code à l’air correct en apparence, mais en réalité le terme “mammal” n’apparaîtra jamais, et le terme “antelope” n’existera plus non plus.
Le 1er replace a bien remplacé “ant” par “insect”, mais aussi “antelope” par “insectelope”. Le second replace, qui en plus de faire tourner une nouvelle fois une fonction, ne trouvera donc jamais de motif à remplacer.
En lieu et place de replace, on préférera employer la fonction thru qui, non seulement peut contenir plusieurs conditions, mais est aussi bien plus sécurisée.
value = get('value.species').thru(espèce => espèce ==="ant" ? "insect" : espèce ==="antelope" ? "mammal" : espèce)
Ainsi thru est largement préférable : une seule fonction est utilisée, elle prévient des effets de bords indésirables, elle peut traiter des valeurs conditionnelles et permet d’être bien plus flexible et explicite.
Modifier et/ou corriger certaines valeurs dans un tableau
[assign] path = value value = get('value.affiliations').map(item=>item.replace("shs","Sciences de l'Homme et Société"))
Afin de pouvoir modifier des valeurs au sein d’un tableau sans modifier la structure, il convient d’utiliser ‘replace’ à l’intérieur de la fonction ‘map’. Cette fonction renvoie un tableau après itération sur chacun de ses éléments.
Créer une colonne en s’assurant que toutes les lignes contiennent un tableau (même vide)
[assign] path = value value = get("value.Entités nommées (Unitex).placeName", [])
Ces instructions créent une colonne à partir du sous champ “placeName” de la colonne “Entités nommées (Unitex). S’il n’y a pas de valeur, le tableau sera vide.
Créer une colonne en séparant des champs multivalués (en fonction d’un séparateur) en vue de leur enrichissement
[assign] path = value value = get("value.Keywords").toString().split(';').filter(Boolean)
Ces instructions créent une colonne à partir du champ “Keywords” en découpant chaque ligne en fonction du caractère “;” . Le résultat est un tableau de valeur.
Créer une colonne à partir de la concaténation de deux autres colonnes
[assign] path = value value = fix(self.value.Title, self.value.Abstract).join('>') ; si il y a des espaces dans le nom des champs, on écrira value = fix(self.value["Le champ Title"], self.value["Le champ Abstract"]).join('>')
Ces instructions créent une colonne à partir des champs “Title” & “Abstract” en les concaténant dans une même colonne. Dans cet exemple, les 2 valeurs seront collées avec le caractère “>”
Créer une colonne à partir de la concaténation de deux autres colonnes (bis)
[assign] path = value value = get("value.Title").append(">").append(self.value.Abstract)
Ces instructions créent une colonne à partir des champs “Title” & “Abstract” en les concaténant dans une même colonne. Dans cet exemple, les 2 valeurs seront collées avec le caractère “>”
Créer un objet par défaut pour toutes les lignes qui contiennent la valeur n/a
[assign] path = value value = get("value.loterre") [swing] test = get('value').isEqual('n/a') [swing/replace] path = value.information value = Aucune réponse
Ces instructions créent une colonne à partir du champs “loterre” et remplacent toutes cellules contenant la valeur n/a par un objet :
{ information: "Aucune réponse" }
Récupérer dans une liste d’objets deux valeurs simples
[assign] path = value value = get('value.concepts loterre').map(item => _.pick(item, ['prefLabel@en', 'about']))
Ces instructions créent une colonne à partir d’une liste d’objets en simplifiant chaque objet pour ne garder que 2 champs différents de chaque objet.
Dans cet exemple, seules les propriétés ‘prefLabel@en’ et ‘about’ seront conservées. Les autres champs de l’objet sont ignorés.
Récupérer dans un valeur dans une liste de liste d’objets
[assign]
path = lesAdresses
value = get('auteurs').map('affiliations').flatten().map('address')
Ces instructions créent une colonne des adresses contenues dans l’objet affiliation lui-même étant dans l’objet address. Les champs affiliations et address sont des listes d’objets, on utilisera donc “map” à la place de “get”.
Transformer une liste d’objets en liste de valeurs simples
[assign] path = value value = get("value.Catégories").map((categorie) =>`${categorie.rang}-${categorie.code.value}`))
Ces instructions créent une colonne à partir d’une liste d’objets en créant une chaîne de caractères composée par 2 champs différents de chaque objet.
Dans cet exemple, “item.rang” et “item.code.value” seront collées avec le caractère “-”. Les autres champs de l’objet sont ignorés.
Transformer une liste d’objets en liste de valeurs simples (bis)
[assign] path = value value = get("value.Domaines").map((domaine, i) => `${i+1} - ${domaine.code.value}`)
Ces instructions créent une colonne à partir d’une liste d’objets en créant une chaîne de caractères composée par 2 champs différents de chaque objet.
Contrairement à l’exemple précédent, le rang est calculé automatiquement à partir de l’indice de l’élément dans le tableau.
Filtrer une liste de valeurs avec une autre liste (intersection)
[assign] path = value value = get("value.pays").filter(x => ['ITALIE', 'ALLEMAGNE', 'FRANCE', 'BELGIQUE'].includes(x))
Ces instructions créent une colonne à partir d’une liste de pays en s’assurant qu’ils ont présents dans une liste spécifique.
Dans cet exemple, la liste résultant du traitement contiendra uniquement les pays déclarés dans le liste de 4.
Filtrer une liste de valeurs avec une autre liste (différence)
[assign] path = value value = get("value.pays").pull('ITALIE', 'ALLEMAGNE', 'FRANCE', 'BELGIQUE')
Ces instructions créent une colonne à partir d’une liste de pays en excluant les pays présents dans une liste spécifique.
Dans cet exemple, les 4 pays seront exclut de la liste résultant du traitement. La fonction pull ne prend comme arguments que des valeurs.
Filtrer une liste de valeurs débutant par…
[assign] path = value value = get("value.affiliations").filter(value=> !value.startsWith("E-mail"))
Ces instructions créent une colonne à partir d’une liste d’affiliations en excluant les emails qui se sont glissés dans le tableau de valeurs des affiliations. Contrairement à ‘pull’, la fonction ‘filter’ permet de déclarer des fonctions comme arguments. Ici ‘startsWith’ capture toutes les valeurs commencant par “E-mail’ (si Email est situé autre part dans la chaîne, il ne sera pas filtré) et l’opérateur “!” permet d’inverser le filtre afin de renvoyer tout sauf les valeurs capturées.
Remplacer une valeur en fonction d’un dictionnaire de correspondance
[env] path = dictionary value = fix({\ "deutschland":"germany",\ ... "allemagne":"germany"\ }) [assign] path = value value = get("value.Pays").castArray().map(item => _.get(env("dictionary"), item, item)).uniq()
Ces instructions créent une colonne à partir d’un champ Pays, contenant une ou plusieurs valeurs. Si une valeur est trouvée dans le dictionnaire (nommé dictionary), par exemple “allemagne”, cette valeur sera remplacée par la valeur correspondante dans le dictionnaire, par exemple “germany”.
Remplacer une valeur en fonction d’un dictionnaire de correspondance via un fichier CSV distant
[assign] path = value ; Choix du champ Lodex à rechercher dans le dictionnaire value = get("value.Type de publication") [combine] path = value ; URL vers un fichier CSV accessible via internet primer = https://publication-type.data.istex.fr/api/export/csv ; nom du fichier temporaire local (facultatif, évite de télécharger le fichier plusieurs fois) cacheName = istex-publication-type default = n/a [combine/URLStream] path = false [combine/CSVParse] ; Choix du séparateur : virgule, point-virgule, etc. separator = fix(";") [combine/CSVObject] [combine/replace] path = id ; Choix de la colonne du fichier CSV à mettre en correspondance value = get('titre') path = value value = self()
Ces instructions créent une colonne à partir d’un champ “Type de publication”. Si une valeur est trouvée dans la colonne “titre” du fichier CSV distant (https://publication-type.data.istex.fr/api/export/csv), alors cette valeur sera remplacée par un objet contenant toutes les informations contenues dans la ligne correspondante du fichier CSV.
Remplacer une liste de valeurs en fonction d’un dictionnaire de correspondance via un fichier TSV distant
On souhaite ici remplacer des valeurs à partir d’un fichier composé de deux colonnes, “From” et “To”, disons un code et son intitulé. Mais contrairement à l’exemple précédent, les données envoyées sont une liste sous forme de tableau, il convient donc de mapper chaque valeur du tableau pour obtenir une liste avec tous les résultats.
# Création d'une variable d'environnement qui stocke la date au format ISO [env] path = date value = thru(d => (new Date()).toISOString().split("T")[0]) # Choix du champ Lodex à rechercher dans le dictionnaire [assign] path = value value = get("value.codes") # Sert à traiter individuellement chaque valeur de la liste (chaque code ici) [map] path = value # Création d'une clé temporaire pour stocker la valeur actuelle [map/replace] path = tmpkey value = self() [map/combine] path = tmpkey # URL vers un fichier CSV accessible via internet primer = http://mapping-tables.daf.intra.inist.fr/nomDeMaTable.tsv # Création d'un fichier temporaire local avec la date définie dans [env] (facultatif, évite de télécharger le fichier plusieurs fois) cacheName = env("date").prepend("NomQueVousVoulez") default = n/a [map/combine/URLStream] path = false [map/combine/CSVParse] separator = fix("\t") [map/combine/CSVObject] # Une fois le TSV parsé en objets JSON, la valeur de "From" est transformé en clé et celle de "To" en valeur ({"From": "code1", "To": "Intitulé1"}). Puis on transforme l'objet avec "intitule" comme clé. [map/combine/replace] path = id value = get('From') path = value value = get('To').split().zipObject(["intitule"]).invert() # Enfin on extrait uniquement la vaeur de "intitule" après le mapping [map/exchange] value = get('tmpkey.value.intitule')
Nettoyer une liste de termes (simple)
[assign] path = value value = get('Termes génériques').map(v => String(v).trim())
Ces instructions suppriment les caractères parasites au début et en fin de chaque terme de la liste.
Nettoyer une liste de termes (avancé)
[assign] path = value value = get('Termes génériques').map(v =>_.chain(v).deburr().replace(/[^\w]/, ' ').replace(/\s+/, ' ').trim().lowerCase().value())
Ces instructions appliquent une série de transformation sur l’ensemble des termes d’une liste.
Dans cet exemple deburr supprimera les caractère accentués, replace supprimera tous les caractères non alphanumériques et remplacera tous les espaces doubles par des espaces simples, trim supprimera les espaces en fin et début de mot, et lowerCase mettra tous les mots en minuscules.
Nettoyer une liste de termes de toutes ses valeurs ‘falsy’ et/ou de ses doublons
[assign] path = value value = get('Termes génériques').compact().uniq()
Ces instructions permettent de retirer toutes les valeurs ‘falsy’ dans un tableau de valeurs. ‘compact’ retire les valeurs : false, null, 0, “”, undefined et NaN. La fonction ‘uniq’ conserve la 1ère occurence des valeurs présentent plusieurs fois dans le tableau. [“A”,”B”,null,”A”,”C”] devient [“A”,”B”,”C”].
Inverser des valeurs dans un tableau (simple)
[assign] path=value value=get("value.nomdecolonne").reverse()
Cette instruction inverse les valeurs à l’intérieur d’un tableau. [“A”,”B”,”C”] devient [“C”,”B”,”A”]
Inverser des bouts de chaîne de caractères (avancé)
[assign] path=value value=get("value.nomdecolonne").map(x=>x.split(", ").reverse().join(", "))
Cet enchaînement d’instructions transforme, par itération, des chaînes de caractères contenues dans un tableau.
Dans un tableau de type noms d’auteurs tel que [“Durand, Jacques”,”Dubois, Daniel”] on modifie chaque valeur (map) que l’on transforme en tableau afin d’inverser l’ordre, puis on la restitue en string. Ce qui donne [“Jacques, Durand”,”Daniel, Dubois”].
Créer une colonne avec une valeur JSON
[assign] path = value value = get('value.jsonValue').thru(JSON.parse)
Ces instructions créent une colonne à partir d’une autre colonne (nommée jsonValue) contenant des valeurs formatées en JSON.
Créer une liste d’objets avec des propriétés issues de différentes colonnes
[assign] path = value value = fix({ark:self.value.EnrichIstex.ark,sourceUidChain:self.value.EnrichConditor['business/sourceUidChain']})
Ces instructions créent une colonne contenant une liste d’objets sur la base de valeurs contenues dans 2 colonnes distinctes (EnrichIstex et EnrichConditor ici). La structure objet est créée gràce à fix({}) , les propriétés sont créées avec “ark:” et “sourceUidChain:” (on donne le nom que l’on souhaite) et séparées par une virgule. Les valeurs dynamiques sont récupérés par “self.value” suivi du nom de la colonne, puis du champ en particulier. Cela donne par exemple : {“ark”:”ark:/67375/WNG-NVGLRQV3-C”,”sourceUidChain”:”!hal$hal-03566649!”}
Créer une matrice où les éléments des tableaux sont associés en fonction de leur position
Cette fonction est notament utile pour rassembler des données issues du dataset de base avec des résultats d’enrichissement par exemple.
[assign] path = value value = zip(self.value.TableauA, self.value.TableauB)
La fonction ‘zip’ va ici créer une matrice en associant les deux tableaux suivants : TableauA : [“A”,”B”,”C”,”D”,”E”] TableauB : [1,2,1,9,0]. Tous les éléments possédant le même index dans leur tableau respectif seront alors associés dans un tableau, renvoyant une matrice telle que : [[“A”, 1], [“B”, 2], [“C”, 1], [“D”, 9], [“E”, 0]]
! Attention, il faut s’assurer que les tableaux aient le même nombre d’éléments. Si l’on avait réalisé un dédoublonnage avec ‘uniq’ sur le TableauB (ou ‘compact’ si on a des null, false…), alors le TableauB serait [1,2,9,0] et le zip renverrait donc : [[“A”, 1], [“B”, 2], [“C”, 9], [“D”, 0], [“E”, undefined]]
Réduire une collection (tableau ou objet) à une seule valeur
Nous reprennons ici la collection créée dans “Créer une liste d’objets avec des propriétés issues de différentes colonnes”. Nous voulons savoir si un document est disponible dans Istex, Conditor, les deux bases ou aucune.
[assign] path = value value = fix({ark:self.value.EnrichIstex.ark,sourceUidChain:self.value.EnrichConditor['business/sourceUidChain']}).reduce((result, value, index, collection)=> { if(collection.ark && !collection.sourceUidChain){return "Istex"} if(collection.ark && collection.sourceUidChain){return "Istex & Conditor"} if(!collection.ark && collection.sourceUidChain){return "Conditor"} return result},"Aucune Base")
La fonction ‘reduce’ va ici tester chaque objet selon des conditions et renverra un résultat spécifique en fonction de la condition satisfaite. “Aucune base” est l’accumulateur de départ (result). Si un objet contient une propriété “ark” et aucune propriété “sourceUidChain”, alors “Aucune base” est remplacé par “Istex”. A l’inverse, “Conditor” remplacera “Aucune base”. Si l’objet contient les 2 propriétés à la fois, “Aucune base” sera remplacé par “Istex & Conditor”. Enfin si aucun cas de figure n’est vérifié (donc si l’objet est vide), la fonction renverra l’accumulateur inchangé “Aucune Base”. Dans tous les cas l’objet est réduit à une seule valeur.
Réduire une collection (tableau ou objet) à un tableau de valeurs spécifiques
Un cas plus complexe ici, où nous souhaitons réduire une matrice. Chaque tableau de la matrice débute par un rnsr et est suivi d’un ou plusieurs instituts. Nous souhaitons dans cet exemple réduire le tableaux seuls rnsr relevant de l’institut INSHS
[assign] path=value value=get("value.MatriceRnsrInstituts).reduce((result, item) =>{if (item.slice(1).some(element => element === "INSHS")) {result.push(item[0])}return result},[])
La fonction ‘reduce’ va ici itérer sur chaque tableau de la matrice. L’accumulteur de départ est un tableau vide, déclaré en fin de fonction.’slice’ va décomposer chaque tableau en 2 parties, avec en index 0 le rnsr et en index 1 les instituts. La fonction ‘some’ va parcourir l’index 1 est vérifiera si un ou plusieurs éléments ont pour valeur “INSHS”. Si la condition est vérifiée, le nouveau résultat est le tableau vide de départ dans lequel est injecté le rnsr correspondant (item[0]) par la fonction ‘push’.
Modifier les valeurs de plusieurs champs ou colonnes d’un dataset en une seule fois (dans le loader uniquement)
Pour éviter de nettoyer un jeu de données avant de le mettre dans Lodex, on peut traiter massivement plusieurs champs dans le loader directement.
[exchange] path = value value = self().mapValues((value, key) => ['Titre', 'Editeur','Source','TypeDeDoc'].includes(key) ? _.deburr(_.trim(value)).toUpperCase() : value)
- L’instruction [exchange] prend un objet en entrée et le remplace par l’objet transformé dans “value = “.
- “value = self()” permet de sélectionner l’ensemble du jeu de données.
- “.mapValues((value, key))” permet de transformer les valeurs d’un objet en fonction de leurs clés (les valeurs sont ici des Strings). On spécifie ensuite dans un tableau les clés (ou champs du dataset dans notre cas) devant être transformées.
- “.includes(key) ?” est une fonction de rappel qui vérifie si les clés sont incluses dans le dataset, si oui elle applique les fonctions, sinon elle retourne la valeur originale (” : value”).
- “_.deburr(_.trim(value)).toUpperCase()”. Trois fonctions sont ici appliquées : “deburr” qui enlève les accents, “trim” les espaces superflus et “toUpperCase” pour mettre tous les caractères en majuscule.
- * “_.deburr” et “_.trim” ont besoin d’être préfixées par _ car ce ne sont pas des fonctions JavaScript natives, mais des fonctions Lodash, or nous sommes dans une fonction anonyme (symbolisée par =>), il faut donc repréciser que ce sont des fonctions de la bibliothèque lodash. “toUppercase” est quant à elle une fonction Javascript.
Ce Script permet donc de modifier 4 colonnes ou champs en une seule fois et à travers plusieurs fonctions.
[exchange] path = value value = self().mapValues((value, key) => ['Auteurs', 'Affiliations', 'Keywords'].includes(key) ? _.map(value, item => _.deburr(_.trim(item)).toUpperCase()) : value)
Ce script fait exactement les mêmes transformations mais cette fois sur des tableaux (Array). On rajoute donc un map pour itérer sur chaque valeur de chaque tableau de chaque champ.
Autre exemple : les exports en TSV ou CSV dans lesquels toutes les données sont concaténées à l’intérieur des champs (“auteur1;auteur2;…”) peuvent être transformés en tableaux par :
.mapValues((value, key) => ['Auteurs', 'Affiliations', 'Keywords'].includes(key) ? _.split(value, ';') : value)
Sélectionner certaines valeurs en vue d’un enrichissement
Admettons un corpus de 100 000 notices, seules 50 000 possèdent un DOI (chaîne vide pour les autres). On souhaite effectuer un enrichissement à partir du champ DOI.
En l’état les 50 000 cellules “” seront envoyées au web-service, qui les traitera et renverra toujours “n/a”. Pour éviter ces traitements inutiles et gagner en temps de traitement on peut modifier le script de l’enrichissement.
Il convient d’abord de sélectionner sa colonne, le web service à utiliser puis sauvergarder. Ensuite on clique sur “mode avancé”, puis au-dessus de l’instruction [assign] on ajoute l’instruction suivante :
[remove] test = get('value.DOI').isEqual("")
L’instruction [remove] permet de “retirer des cellules” (ou des lignes entières dans le contexte d’un loader).
On spécifie ensuite ce qu’il faut retirer avec “test” et on spécifie que ce sont des chaînes vides que l’on veut retirer avec la fonction .isEqual(“”).
En fonction des cas rencontrés et en raison de l’inexistence de fonctions Lodash comme isNotEqual() on peut inverser le résultat de “test” en ajoutant à la ligne du dessous
reverse = true
Effectuer des enrichissements ciblés pour compléter des données manquantes
On a effectué un 1er enrichissement sur une colonne (code et intitulé labo) mais qui n’a pas renvoyé 100% de résultats. On obtient donc une colonne avec à la fois des objets et des strings “n/a” pour les correspondances non trouvées.
On souhaite refaire un enrichissement pour compléter le 1er en utilisant une autre source de données (disons le champs « AdressePostale »). Pour ne pas réinterroger toutes les valeurs mais simplement celles qui n’ont pas eu de réponse on peut faire comme suit dans le mode avancé de l’enrichissement avec web service :
[remove] test=get("value.PremierEnrich").isObject() [assign] path = value value = get("value.AdressePostale")
Dans un premier temps nous allons enlever toutes les bonnes réponses obtenues en retirant les objets issus de la colonne du 1er enrichissement.
Ne reste donc plus que les “n/a” que nous remplaçons, pour chaque cellule, par la valeur de la colonne « AdressePostale » correspondante. L’enrichissement va alors se lancer uniquement sur les adresses postales. On peut ensuite concaténer les 2 colonnes enrichies pour avoir une seule et même colonne avec 100% de résultats.
NB : On peut également utiliser ce type de scripts pour lancer des enrichissements sur de nouvelles données ajoutées à un corpus préexistant et déjà enrichi.
Pour aller plus loin
Les recettes Lodex sont composées d’une série d’instructions qui s’enchaînent séquentiellement pour chaque ressource du dataset.
Les instructions ont un nom entre crochet, exemple :
[assign]
Chaque instruction possède ou non plusieurs paramètres. Chaque paramètre a un nom et une valeur, exemple:
[assign] path = doi value = 0.1007/s00300-012-1156-9
la valeur d’un paramètre peut être dynamique, c’est à dire calculé à partir de chaque ressource, ces calculs sont réalisés par un enchaînement de fonctions, une fonction se reconnaît par un nom suivi de parenthèses, exemple:
[assign] path = doi value = get("value.crossrefID")
La liste des instructions utilisables dans lodex et leurs paramètres associés est disponible ici : https://inist-cnrs.github.io/ezs/#/plugin-core
La liste des fonctions utilisables dans les paramètres est disponible ici: https://lodash.com/docs/4.17.15