« ArtBot » : différence entre les versions

De Wikipast
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
 
(114 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
{| class="wikitable"
== Description ==
|Langue
Le but de l'[[ArtBot]] est de créer des pages pour des œuvres qui regroupent l'historique de leurs ventes. Il utilise comme base de données les registres de ventes du [https://github.com/thegetty/provenance-index-csv Getty Provenance Index] fourni par le [[Getty Research Institute]]. À partir de ces registres, le bot fait une sélection d'informations pertinentes (titre et auteur de l'œuvre, date de la vente, vendeur, acheteur, etc.) et les regroupe dans une seule base de données. Les pages ''titre (artiste)'' sont ensuite créées et complétées avec la liste des évènements de vente classés par ordre chronologique. Enfin, le bot génère une [[Ventes d'œuvres par année|page de liens]] pour retrouver et lier toutes les pages d'œuvres entre elles.
|'''Français'''
|}


 
Il a été créé par Vincent Philippoz, Michael Richter et Agatha Duranceau.
Le but de l'[[ArtBot]] est de créer des pages pour des œuvres qui regroupent l'historique de leurs ventes. Il utilise comme base de données les registres de ventes du [[Getty Provenance Index]] fourni par le [[Getty Research Institute]] [https://github.com/thegetty/provenance-index-csv].


== Gestion des bases de données ==
== Gestion des bases de données ==
Ligne 12 : Ligne 9 :


=== Récupération des données ===
=== Récupération des données ===
Les fichiers .csv ont directement été récupérés sur GitHub [https://github.com/thegetty/provenance-index-csv], mais ne contiennent pas tous les mêmes catégories, ou les mêmes noms pour les mêmes catégories. Les entrées pertinentes ont été sélectionnées et renommées, puis les fichiers concaténés afin de travailler sur une nouvelle base plus facile à lire par l'[[ArtBot]].
Les fichiers .csv ont directement été récupérés sur GitHub [https://github.com/thegetty/provenance-index-csv], mais ne contiennent pas tous les mêmes catégories, ou les mêmes noms pour les mêmes catégories. Les entrées pertinentes ont été sélectionnées et renommées, puis les fichiers concaténés afin de travailler sur une base unique plus facile à lire par l'[[ArtBot]].
 
Il est possible d'afficher les données de la manière suivante :


L'[[ArtBot]] travaille avec les informations suivantes : Titre de l'œuvre, artiste, nationalité de l'artiste, date de vente, maison de ventes, prix de vente, vendeur, acheteur, et éventuellement type d'œuvre et genre.


[[Fichier:Dataframe.JPG|vignette|redresse= 3|center | Exemple d'informations contenues dans la base de données]]
[[Fichier:Dataframe.JPG|vignette|redresse= 3|center | Exemple d'informations contenues dans la base de données]]




=== Identifiants Wikidata ===
Le jeu de données complet contient 1'288'210 entrées, dont 77.84 % de titres uniques et 15.93 % d'artistes uniques.
 
Pour le type d'œuvre et le genre, des identifiants wikidata sont utilisés :


Type d'œuvre :
=== Dictionnaire Wikidata ===


* [[peinture]] : [https://www.wikidata.org/wiki/Q3305213 Q3305213]
60 genres différents et 82 types d'œuvres ont été identifiés dans la base de données. Cependant, cette dernière contient des informations en anglais, en français, en allemand et en néerlandais. Il est donc nécessaire d'identifier les différents types et genres à l'aide d'une étiquette indépendante du mot utilisé dans la base de données.
* [[sculpture]] : [https://www.wikidata.org/wiki/Q860861 Q860861]
Pour cela, les identifiants [[Wikidata]] [https://www.wikidata.org/wiki/Wikidata:Main_Page] sont utilisés, et un dictionnaire pour associer les bons identifiants aux bonnes catégories a été créé.
* [[photographie]] : [https://www.wikidata.org/wiki/Q125191 Q125191]
* [[émail]] : [https://www.wikidata.org/wiki/Q79496108 Q79496108]
* [[dessin]] : [https://www.wikidata.org/wiki/Q93184 Q93184]
* [[tapisserie]] : [https://www.wikidata.org/wiki/Q184296 Q184296]
* [[broderie]] : [https://www.wikidata.org/wiki/Q28966302 Q28966302]
* [[meuble]] : [https://www.wikidata.org/wiki/Q14745 Q14745]
* [[fresque]] : [https://www.wikidata.org/wiki/Q134194 Q134194]
* [[objet d'arts décoratifs]] : [https://www.wikidata.org/wiki/Q631931 Q631931]
* [[gravure sur pierres précieuses]] : [https://www.wikidata.org/wiki/Q1501187 Q1501187]
* [[médaille]] : [https://www.wikidata.org/wiki/Q131647 Q131647]
* [[mosaïque]] : [https://www.wikidata.org/wiki/Q133067 Q133067]
* [[miniature]]
* [[dentelle]] : [https://www.wikidata.org/wiki/Q231250 Q231250]
* [[marqueterie]] : [https://www.wikidata.org/wiki/Q1049923 Q1049923]
* [[aquarelle]] : [https://www.wikidata.org/wiki/Q18761202 Q18761202]
* [[estampe]] ou [[lithographie]] : [https://www.wikidata.org/wiki/Q11060274 Q11060274]
* [[pastel]] : [https://www.wikidata.org/wiki/Q12043905 Q12043905]
* [[livre]] : [https://www.wikidata.org/wiki/Q571 Q571]
* [[horloge]] ou [[montre]]: [https://www.wikidata.org/wiki/Q376 Q376]
* [[cartes à jouer]] : [https://www.wikidata.org/wiki/Q47883 Q47883]
* [[assiette de cuivre]]
* [[vêtement]] : [https://www.wikidata.org/wiki/Q11460 Q11460]
* [[carte]] :  [https://www.wikidata.org/wiki/Q4006 Q4006]
* [[minéraux]] : [https://www.wikidata.org/wiki/Q7946 Q7946]


Genre :
=== Dictionnaire de traduction ===


* [[paysage]] : [https://www.wikidata.org/wiki/Q191163 Q191163]
Des dictionnaires ont également été créés afin de traduire en français les informations telles que la nationalité de l'artiste et la monnaie utilisée pour la vente. Cependant, les titres d'œuvres n'ont pas été traduits.
* [[portrait]] : [https://www.wikidata.org/wiki/Q134307 Q134307]
* œuvre abstraite : [https://www.wikidata.org/wiki/Q128115 Q128115]
* [[nature morte]] : [https://www.wikidata.org/wiki/Q170571 Q170571]
* représentations d'animaux : [https://www.wikidata.org/wiki/Q16875712 Q16875712]
* scènes historiques : [https://www.wikidata.org/wiki/Q742333 Q742333]


== Fonctionnement ==
== Fonctionnement ==


L'[[ArtBot]] utilise la bibliothèque [[pywikiapi]] pour lire et écrire dans les pages de wikipast.
L'[[ArtBot]] utilise la bibliothèque [[pywikiapi]] [https://www.mediawiki.org/w/api.php] pour lire et écrire dans les pages de [[Wikipast]].


=== Création/modification d'une page ===
=== Création/modification d'une page ===


L'[[ArtBot]] vérifie pour chaque œuvre si une page de type Titre de l'œuvre (Auteur) existe déjà. Si non, il crée la page en question et ajoute la description de l'œuvre et une entrée pour l'évènement de vente dans les sections correspondantes. Si oui, il copie le texte de la page, insère la nouvelle entrée au bon endroit et écrase l'ancienne version de la page.
L'[[ArtBot]] vérifie pour chaque œuvre si une page avec le format: '''Titre de l'œuvre(Auteur)''' existe déjà. Si non, il crée la page en question et ajoute la description de l'œuvre et une entrée pour l'évènement de vente dans les sections correspondantes. Si oui, il copie le texte de la page, insère la nouvelle entrée au bon endroit et écrase l'ancienne version de la page. Les entrées concernant les évènements de vente sont classées par ordre chronologique.
 
La création du titre de la page se fait grâce à l'appel de la fonction <code>sale_title_create</code> qui reçoit comme argument une ligne de la dataframe contenant les informations d'une œuvre et génère un titre de la forme "Nom de l'œuvre (Nom de l'artiste)".


=== Structure des pages ===
=== Structure des pages ===
Les pages d'œuvres générées par l'[[ArtBot]] sont structurées de la manière suivante :


Titre de la page : <nowiki>Titre de l'œuvre (Artiste)</nowiki>
Titre de la page : <nowiki>Titre de l'œuvre (Artiste)</nowiki>
Ligne 77 : Ligne 48 :
Syntaxe pour un évènement de vente :
Syntaxe pour un évènement de vente :


* <nowiki>[[Année.Mois.Jour]] / [[lieu]]. [[Vente]] de [[titre œuvre]] de [[artiste]] (de nationalité [[nationalité]]) par [[vendeur]] à [[acheteur]] au prix de [[prix]] [[monnaie]] par la maison [[maison de ventes]]. [Source] </nowiki>
* <nowiki>[[Année.Mois.Jour]] / [[maison de vente]]. [[Vente de l'oeuvre]] décrite comme [[titre oeuvre]] réalisée par [[artiste]] (nationalité : [[nationalité]]), vendue par [[vendeur]], achetée par [[acheteur]] au prix de prix + currency. [Source] </nowiki>
 
Les hypermots sont récupérés directement dans la dataframe, et sont traduits grâce à des dictionnaires lorsque c'est nécessaire. Si certaines données sont absentes, elles sont ignorées et la proposition correspondante n'est pas écrite.
 
=== Insertion d'une entrée à une page déjà existante ===


Lorsque certaines données sont absentes, elles sont ignorées et la proposition correspondante n'est pas écrite.
Afin d'insérer une nouvelle entrée au bon endroit sur la page d'une œuvre (ordre chronologique), l'[[ArtBot]] fait appel à la fonction <code>sale_entry_create</code> qui reçoit comme argument les informations sur la vente d'une œuvre et les transforme pour les afficher au format désiré décrit dans la [[#Structure des pages|définition de la structure de la page]]. La fonction retourne ensuite la chaîne de caractères complète contenant toutes les informations présentes pour l'œuvre analysée. Les informations manquantes seront bien sûr ignorées (par exemple si elles contiennent des NaN, "unknown" etc.), et si le nom de l'artiste n'est pas précisé, il sera remplacé par la mention "artiste anonyme". La fonction <code>add_one_line</code> insère ensuite la chaîne de caractères sur la page en respectant la chronologie des ventes.


=== Insertion d'une entrée à une page déjà éxistante ===
=== Ajout du type/genre de l'œuvre ===
 
Afin d'ajouter le type et le genre de l'œuvre lorsque ces informations sont disponibles l'[[ArtBot]] fait appel à la fonction <code>sale_info_create</code> qui reçoit comme argument une ligne de la dataframe, c'est à dire les infos relatives à une œuvre unique. Elle recherche ensuite les informations sur le type et le genre de l'œuvre et y associe un mot français et le numéro [[Wikidata]] correspondant. Enfin, la fonction retourne les informations de type et de genre au format correct. C'est de nouveau la fonction <code>add_one_line</code> qui insère la description au début de la page de l'œuvre si elle n'existe pas déjà.


== Création de liens entre les pages ==
== Création de liens entre les pages ==


Afin de mieux référencer les pages crées par l'[[ArtBot]], elles sont toutes liées entre-elles. Pour cela, une page regroupant l'intégralité des années de ventes est créée, et pour chaque année, une page pour chaque date regroupe l'ensemble des ventes effectuées ce jour-là.
Les pages [[Wikipast]] peuvent être créées sans être reliées à aucune autre page du site. Ceci ne pose pas problème à petite échelle, mais lors de la génération de plus d'un million de page, celles-ci peuvent être inaccessible. Pour remédier à cette complication, des pages de liens sont définies. Ces dernières sont structurées de la manière suivante :
 
La page [[Ventes d'œuvres par année]] est crée comme source de l'arborescence de liens. Elle regroupe toutes les années durant lesquelles une vente à été enregistrée. Ensuite, une page est créée pour chacune de ces années ([[Ventes d'œuvres en 1770|Exemple]]). Celles-ci rassemblent toutes les dates (année/mois/jour) de cette année auxquelles une entrée est enregistrée dans la base de données. Le schéma se répète et chaque date possède une page réunissant toutes les ventes homologuées de la journée ([[Ventes d'œuvres le 1770.02.14|Exemple]]).
 
[[Fichier:ventes_oeuvre_1770.png|vignette|redresse= 3|center | Extrait de la page [[Ventes d'œuvres en 1770]]. ]]
 
[[Fichier:ventes_oeuvre_1770_02_14.png|vignette|redresse= 3|center | Extrait de la page [[Ventes d'œuvres le 1770.02.14]].]]
 
Quelques exceptions sont tout de mêmes notables. Heureusement, chaque entrée est associée à une année de vente. Par contre le mois et le jour ne sont pas toujours compris dans la database. Ainsi, deux cas possibles  sont traités : Si l'information du mois est manquante, le jour et le mois sont remplacés par un "-" car le jour sans le mois apporte peu d'informations. Le deuxième cas de figure est celui ou seul le jour est inconnu. Le mois est alors pertinent et seulement le jour est changé pour un "-". De cette manière, même les entrées de la base de données avec une date incomplète seront ajoutés dans l'arborescence de liens.


== Discussion des performances de l'[[ArtBot]] ==
== Discussion des performances de l'[[ArtBot]] ==
Le bot fonctionne bien dans l'ensemble, il a créé 428'069 pages. Cependant, les points suivants sont à améliorer :
=== Titres des œuvre ===
La base de donnée a une grande variété de syntaxes et de langues. Nous n'avons pas choisi de traduire les titre d'œuvres car les appels à Google Translate sont longs et auraient considérablement ralenti l'exécution déjà lente. Quant à elle, la syntaxe pose des problèmes de compatibilité avec Wikipast. En effet, des caractères réservées (<nowiki>\, [, ], ...</nowiki>) apparaissent parfois, ce qui casse des hypermots voire rend impossible la création de la page. Pour résoudre ce problème, il serait possible de filtrer chaînes de caractère afin d'enlever les symboles problématiques. Nous avons choisi de simplement ignorer les entrées posant problème, faute de temps. Cependant, un dernier problème avec les titres est leur longueur ; Certains sont une description complète de l'oeuvre, d'autre incluent le nom de l'artiste, ... Du fait de la grande diversité, un filtrage simple semble impossible et nous avons choisi de simplement utiliser les titres tel quels.
=== Identification des entités uniques ===
Lorsque le titre d'une œuvre et ou d'un artiste est inconnu ou trop vague (comme "Paysage", "Portrait", "Un Christ" etc.), il arrive que l'[[ArtBot]] n'ait pas pris en charge l'exception et considère plusieurs œuvres comme une seule et unique entité. La page [[Untitled_(artiste_anonyme)]] est représentative de ce problème.
=== Vitesse d'exécution ===
Getty propose une base de donnée avec près de 1'300'000 ventes. Bien que traiter un événement soit rapide, le nombre imposant de données conduit inévitablement à un long temps d'exécution. Avec le code développé, nous atteignons approximativement 170 ms par entrée, soit  plus de 2 jours d'exécution au total. Ainsi, nous n'avons pu exporter que 33.23 % de la base de donnée. Pour réduire ce temps, le programme python peut être optimisé et une meilleure connexion à Wikipast serait requise.
== Code complet ==
=== Importation des bibliothèques ===
<pre>
import pandas as pd
!pip install pywikiapi
</pre>
=== Importation et mise en forme de la base de données ===
<pre>
df_SC1 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_1.csv',
          low_memory=False)
df_SC2 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_2.csv',
          low_memory=False)
df_SC3 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_3.csv',
          low_memory=False)
df_SC4 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_4.csv',
          low_memory=False)
df_SC5 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_5.csv',
          low_memory=False)
df_SC6 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_6.csv',
          low_memory=False)
df_SC7 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_7.csv',
          low_memory=False)
df_SC8 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_8.csv',
          low_memory=False)
df_SC9 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_9.csv',
          low_memory=False)
df_SC10 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_10.csv',
          low_memory=False)
df_SC11 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_11.csv',
          low_memory=False)
df_SC12 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_12.csv',
          low_memory=False)
df_SC13 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_13.csv',
          low_memory=False)
df_knoedler = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/knoedler/knoedler.csv',
          low_memory=False)         
# Création de la bases de données des Sales Catalogs
SC_wanted_columns = ["title",
                    "artist_name_1",
                    "lot_sale_year",
                    "lot_sale_month",
                    "lot_sale_day",
                    "auction_house_1",
                    "nationality_1",
                    "price_amount_1",
                    "price_currency_1",
                    "buy_name_1",
                    "sell_name_1",
                    "object_type",
                    "genre"]
df_SC_all = df_SC1.loc[:, SC_wanted_columns]
SALES_CATALOGUE = [df_SC2, df_SC3, df_SC4, df_SC5, df_SC6, df_SC7, df_SC8, df_SC9, df_SC10, df_SC11, df_SC12, df_SC13]
for dataframe in SALES_CATALOGUE :
  df_temp = dataframe.loc[:, SC_wanted_columns]
  df_SC_all = pd.concat([df_SC_all, df_temp])
# Mettre la même convention de noms
df_SC_all = df_SC_all.rename(columns = {"artist_name_1" : "artist_name",
                                        "lot_sale_year" : "sale_year" ,
                                        "lot_sale_month" : "sale_month"  ,
                                        "lot_sale_day": "sale_day",
                                        "buyer_name_1" : "buyer_name",
                                        "seller_name_1" : "seller_name",
                                        "auction_house_1" : "auction_house",
                                        "nationality_1" : "artist_nationality",
                                        "price_amount_1" : "purch_amount",
                                        "price_currency_1" : "purch_currency",
                                        "buy_name_1" : "buyer_name",
                                        "sell_name_1" : "seller_name"})
# Création de la bases de données de Knoedler
K_wanted_columns = ["title",
                    "artist_name_1",
                    "nationality_1",
                    "genre",
                    "object_type",
                    "sale_date_year",
                    "sale_date_month",
                    "sale_date_day",
                    "purch_amount",
                    "purch_currency",
                    "seller_name_1",
                    "buyer_name_1"]
df_K = df_knoedler.loc[:, K_wanted_columns]
df_K = df_K.reindex(columns=list(df_K.columns) + ["auction_house"])
df_K.auction_house = df_K.auction_house.fillna(value='Knoedler')
# Mettre la même convention de noms et le même ordre
df_K = df_K.rename(columns = {"artist_name_1" : "artist_name",
                              "sale_date_year" : "sale_year" ,
                              "sale_date_month" : "sale_month"  ,
                              "sale_date_day": "sale_day",
                              "nationality_1" : "artist_nationality",
                              "buyer_name_1" : "buyer_name",
                              "seller_name_1" : "seller_name"})
columns_list = df_SC_all.columns
df_K = df_K.reindex(columns = columns_list)
# Combinaison des dataframes
df_tot = pd.concat([df_SC_all, df_K])
# Mettre les nombre de la date comme des entiers
df_tot.sale_year = df_tot.sale_year.convert_dtypes(convert_integer = True)
df_tot.sale_month = df_tot.sale_month.convert_dtypes(convert_integer = True)
df_tot.sale_day = df_tot.sale_day.convert_dtypes(convert_integer = True)
</pre>
=== Création de dictionnaires ===
<pre>
# Dictionnaires de traduction
list_nationalities = df_tot.artist_nationality.unique()
trans_nat = ['', 'Flamand', 'Néerlandais ou flamand', 'Italien', 'Néerlandais',
            'Néerlandais', 'Français', '', 'Néerlandais ou flamand ou français',
            'Allemand', 'Espagnol', 'Flamand ou français', 'Belge ou flamand',
            'Flamand ou néerlandais', 'Autrichien', 'Britannique',
            'Flamand ou allemand ou néerlandais', 'Français et italien',
            'Allemand ou suisse', 'Hongrois', 'Autrichien et hongrois',
            'Suisse', 'Néerlandais et italien', 'Allemand ou italien',
            'Flamand ou allemand', 'Allemand et italien', 'Suédois', 'Polonais',
            'Chinois', 'Grec et espagnol', '', 'Belge', 'Néerlandais ou allemand',
            'Français ou néerlandais', 'Flamand', 'Français ou suisse',
            'Belge ou flamand ou néerlandais', 'Italien ou suisse', 'Bohème',
            'Américain', '', 'Britannique et flamand', 'Néerlandais ou français',
            'Belge ou français', '', 'Belge ou flamand et français', '',
            'Flamand et italien', '', 'Danois', 'Autrichien ou allemand', 'Grec',
            'Britannique et allemand', 'Irlandais', 'Britannique ou français',
            'Britannique ou irlandais', 'Tchèque', 'Autrichien ou français',
            'Néerlandais ou néerlandais', 'Néerlandais ou néerlandais',
            'Tchèque ou allemand', '', 'Romain', 'Britannique', 'Arménien',
            'Portugais', 'Français ou allemand', 'Danois ou allemand',
            'Autrichien et flamand', 'Bohème ou allemand', 'Péruvien', 'Russe',
            'Britannique ou néerlandais', 'Mexicain', '', 'Haïtien',
            'Français ou italien', 'Sud-américain', 'Italien ou espagnol',
            'Dalmate', 'Croate ou italien', 'Italien', 'Indien', 'Égyptien',
            'Byzantin', 'Étrusque', 'Néerlandais ou flamand ou allemand',
            'Autrichien et allemand', 'Greuze', 'Britannique ou danois',
            'Canadien', 'Français ou suédois', 'Autrichien ou suisse',
            'Néerlandais', 'Allemand et suisse', 'Français ou espagnol',
            'Gaulois', 'Croate ou istrien', 'Français', 'Flamand', 'Japonais',
            'Allemand ou autrichien', 'Allemand ou suédois', 'Autrichien et bohémien',
            '', 'Autrichien ou italien', '', 'Autrichien ou flamand', 'Allemand ou russe',
            'Danois ou norvégien', 'Danois ou suédois', 'Norvégien', 'Bohémien ou tchèque',
            'Persan', 'Erman', 'Australien', 'Turc', 'Flamand', 'Yougoslave', '',
            'Bulgare', 'Roumain', 'Finnois', '', 'Argentin', 'Scandinave', 'Javanais',
            '', '', 'Néerlandais ou allemand', 'Sud-africain', 'Monégasque',
            'Équatorien', 'Uruguayen', 'Néerlandais et américain']
dict_nat = dict(zip(list_nationalities, trans_nat))
list_currencies = df_tot.purch_currency.unique()
trans_curr = ['fl', 'frs', 'fl', 'frs', 'fl', 'frs', 'frs', '£', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'fl', 'frs', 'frs', 'fl', 'frs', 'frs', 'fl', 'fl', 'fl', '£', '£', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs',
              'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', '£', '£', '', 'gs', 'gs', 'gs', 'gs', '£', 'gs',
              'gs', '£', '£', '£', 'assignats', 'ecus', '£', '£', '£', 'm', 'rt', 'gr', 'sch', 'sch', 'sgr', 'kr', 'th', 'duc.', 'st',
              'danish rigsdaler', 'reichsmark', 'sch', 'chf', '$', 'marks', '$', 'thalers', 'lire', 'chf']
dict_curr = dict(zip(list_currencies, trans_curr))
types_trans = {"miniature" : "miniature",
    "enamel" : "émail", 
    "embroidery" : "broderie", 
    "mosaic" : "mosaïque",
    "painting" : "peinture",
    "drawing" : "dessin",
    "sculpture" : "sculpture",
    "furniture" : "meuble",
    "fresco" : "fresque",
    "tapestry" : "tapisserie" ,
    "decorative arts" : "objet d'arts décoratifs",
    "engraved gemstone" : "gravure sur pierres précieuses",
    "dessin" : "dessin",
    "peinture" : "peinture",
    "émail" : "émail",
    "peinture [?]" : "peinture",
    "médaille" : "médaille",
    "tapisserie" : "tapisserie",
    "mosaique" : "mosaïque",
    "email" : "émail",
    "mosaïque" : "mosaïque",
    "meuble" : "meuble",
    "tapisserie" : "tapisserie",
    "dentelle" : "dentelle",
    "marqueterie" : "marqueterie",
    "peinture ou dessin" : "",
    "broderie" : "broderie",
    "gemälde" : "peinture", 
    "gemalde" : "peinture",
    "zeichnung" : "dessin",
    "skulptur" : "sculpture",
    "miniatur" : "miniature", 
    "gemãlde" : "peinture",
    "skulptur" : "sculpture",
    "graphik" : "",
    "painting [?]" : "peinture",
    "watercolor" : "aquarelle",
    "print" : "",
    "pastel" : "pastel",
    "book" : "livre",
    "clocks" : "horloge",
    "photograph" : "photographie",
    "decorative art" : "objet d'arts décoratifs",
    "playing cards" : "cartes à jouer",
    "copper plate" : "assiette de cuivre",
    "clothing" : "vêtement",
    "maps" : "carte",
    "minerals" : "minéraux"}
genres_trans = {"history" : "scènes historiques",
    "genre" : "",
    "landscape" : "paysage", 
    "portraits" : "portrait",
    "animals" : "représentations d'animaux",
    "still life" : "nature morte",
    "historie" : "scènes historiques",
    "landschaft" : "paysage",
    "stilleben" : "nature morte",
    "tiere" : "représentations d'animaux",
    "porträt" : "portrait",
    "stillleben |t stilleben" : "nature morte",
    "[not identified]" : "",
    "portrait" : "portrait",
    "abstract" : "oeuvre abstraite"}
# Dictionnaires pour lier à Wikidata
types_wiki = {"peinture" : "[https://www.wikidata.org/wiki/Q3305213 Q3305213]",
    "sculpture" : "[https://www.wikidata.org/wiki/Q860861 Q860861]",
    "photographie" : "[https://www.wikidata.org/wiki/Q125191 Q125191]",
    "émail" : "[https://www.wikidata.org/wiki/Q79496108 Q79496108]",
    "dessin" : "[https://www.wikidata.org/wiki/Q93184 Q93184]",
    "tapisserie" : "[https://www.wikidata.org/wiki/Q184296 Q184296]",
    "broderie" : "[https://www.wikidata.org/wiki/Q28966302 Q28966302]",
    "meuble" : "[https://www.wikidata.org/wiki/Q14745 Q14745]",
    "fresque" : "[https://www.wikidata.org/wiki/Q134194 Q134194]",
    "objet d'arts décoratifs" : "[https://www.wikidata.org/wiki/Q631931 Q631931]",
    "gravure sur pierres précieuses" : "[https://www.wikidata.org/wiki/Q1501187 Q1501187]",
    "médaille" : "[https://www.wikidata.org/wiki/Q131647 Q131647]",
    "mosaïque" : "[https://www.wikidata.org/wiki/Q133067 Q133067]",
    "miniature" : "",
    "dentelle" : "[https://www.wikidata.org/wiki/Q231250 Q231250]",
    "marqueterie" : "[https://www.wikidata.org/wiki/Q1049923 Q1049923]",
    "aquarelle" : "[https://www.wikidata.org/wiki/Q18761202 Q18761202]",
    "estampe" : "[https://www.wikidata.org/wiki/Q11060274 Q11060274]",
    "lithographie" : "[https://www.wikidata.org/wiki/Q11060274 Q11060274]",
    "pastel" : "[https://www.wikidata.org/wiki/Q12043905 Q12043905]",
    "livre" : "[https://www.wikidata.org/wiki/Q571 Q571]",
    "horloge" : "[https://www.wikidata.org/wiki/Q376 Q376]",
    "montre": "[https://www.wikidata.org/wiki/Q376 Q376]",
    "cartes à jouer" : "[https://www.wikidata.org/wiki/Q47883 Q47883]",
    "assiette de cuivre" : "",
    "vêtement" : "[https://www.wikidata.org/wiki/Q11460 Q11460]",
    "carte" :  "[https://www.wikidata.org/wiki/Q4006 Q4006]",
    "minéraux" : "[https://www.wikidata.org/wiki/Q7946 Q7946]"}
genres_wiki = {"paysage" : "[https://www.wikidata.org/wiki/Q191163 Q191163]",
    "portrait" : "[https://www.wikidata.org/wiki/Q134307 Q134307]",
    "oeuvre abstraite" : "[https://www.wikidata.org/wiki/Q128115 Q128115]",
    "nature morte" : "[https://www.wikidata.org/wiki/Q170571 Q170571]",
    "représentations d'animaux" : "[https://www.wikidata.org/wiki/Q16875712 Q16875712]",
    "scènes historiques" : "[https://www.wikidata.org/wiki/Q742333 Q742333]"}
</pre>
=== Traduction des nationalités des artistes et monnaies ===
<pre>
def translate_nat(nationnality):
  '''
  Traduis la nationalité en français
  '''
  for word in dict_nat :
    if nationnality == word:
      return dict_nat[word]
  return ""
def translate_curr(currency):
  '''
  Traduis la devise en français
  '''
  for word in dict_curr :
    if currency == word:
      return dict_curr[word]
  return ""
</pre>
=== Génération du texte pour les évènements de vente ===
<pre>
def sale_title_create(line) :
  if (line['artist_name'] == 'Anonymous' or line['artist_name'] == '[unbekannt]' or line['artist_name'] =='Anonyme' or pd.isna(line['artist_name'])) :
    title = (str(line['title']) + " (artiste anonyme)")
  else :
    title = (str(line['title']) + " (" + str(line['artist_name']) + ")")
  return title
def sale_entry_create(line) :
  '''
  Take a line from the cleaned dataframe and prints it according to the
  Wikidata event syntax.
  '''
  if line['sale_day'] < 10:
    day_str = "0" + str(line['sale_day'])
  else:
    day_str = str(line['sale_day'])
  if line['sale_month'] < 10:
    month_str = "0" + str(line['sale_month'])
  else:
    month_str = str(line['sale_month'])
 
  #il y a des dates ou le mois ou le jour ne sont pas connus
  if (line['sale_month'] == 0): #ça ne sert à rien d'avoir le jour si on n'a pas le mois
    date_txt = (f"[[{line['sale_year']}.-.-]]/")
  elif (line['sale_day'] == 0):
    date_txt = (f"[[{line['sale_year']}.{month_str}.-]]/")
  else:
    date_txt = (f"[[{line['sale_year']}.{month_str}.{day_str}]]/")
  if (pd.isna(line['auction_house']) or line['auction_house'] == 'Anonymous'):
    house_txt = (' -. ')
  else :
    house_txt = (f"[[{line['auction_house']} | maison de ventes : {line['auction_house']}]]. ")
  title_txt = (f"[[Ventes d'œuvres par année|Vente de l'œuvre]] décrite comme ''[[" + sale_title_create(line)+f"|{line['title']}]]''")
  if (line['artist_name'] == 'Anonymous' or line['artist_name'] == 'unbekannt' or line['artist_name'] =='Anonyme' or pd.isna(line['artist_name'])) :
    artist_txt = (" réalisée par un.e artiste anonyme")
  else :
    artist_txt = (f" réalisée par [[{line['artist_name']}]]")
  translated_nat = translate_nat(line['artist_nationality'])
  if (pd.isna(line['artist_nationality']) or translated_nat == "") :
    nationality_txt = ""
  else :
    nationality_txt = (f" (nationalité : [[{translated_nat}]])")
  if (pd.isna(line['seller_name'])):
    seller_txt = ('')
  else :
    seller_txt = (f", vendue par [[{line['seller_name']}]]")
  if (pd.isna(line['buyer_name'])):
    buyer_txt = ('')
  else :
    buyer_txt = (f", achetée par [[{line['buyer_name']}]]")
  if (pd.isna(line['purch_amount']) or pd.isna(line['purch_currency'])):
    price_txt = ('')
  else :
    price_txt = (f" au prix de {line['purch_amount']} {translate_curr(line['purch_currency'])}")
  source_txt = (f". [https://github.com/thegetty/provenance-index-csv]")
  concat_txt = '\n* '+date_txt+house_txt+title_txt+artist_txt+seller_txt+buyer_txt+price_txt+source_txt
  return concat_txt
def sale_info_create(line) :
  '''
  Take a line from the cleaned dataframe and prints its type and genre.
  Links it to WikiData
  '''
  # Trouver les correspondance
  type_entry = str(line['object_type']).lower().split("; ")
  genre_entry = str(line['genre']).lower().split("; ")
  type_txt = ""
  # Traduction en français
  for i in range(len(type_entry)):
    for word in types_trans:
      if word == type_entry[i]:
        type_entry[i] = types_trans[word]
        break
  # Associe un lien Wikidata
  for i in range(len(type_entry)):
    for word in types_wiki:
      if word == type_entry[i]:   
        type_txt = type_txt + "Type : " + word.capitalize() + ". (Wikidata : " + types_wiki[word] + ")\n"
        break
  genre_txt = ""
  # Traduction en français
  for i in range(len(genre_entry)):
    for word in genres_trans:
      if word == genre_entry[i]:
        genre_entry[i] = genres_trans[word]
        break
  # Associe un lien Wikidata
  for i in range(len(genre_entry)):
    for word in genres_wiki:
      if word == genre_entry[i]:   
        genre_txt = "Genre : " + word.capitalize() + ". (Wikidata : " + genres_wiki[word] + ")\n"
        break
  return type_txt + genre_txt
</pre>
=== Connexion à Wikipast ===
<pre>
# Define site
from pywikiapi import Site
site = Site('http://wikipast.epfl.ch/wikipast/api.php')
site.no_ssl = True
# Login information
user='VPhilippoz@ArtBot'
passw='8kgee1cofm8l8ks721ju505r0ju50jes'
# Login du bot
site.login(user, passw)
</pre>
=== Lecture d'informations depuis Wikipast ===
<pre>
def page_already_exists(title):
  '''
  Check if a page with the name "page" exists in Wikipast
  Return number of pages with the name (can be used as bool)
  '''
  results = []
  for res in site.query(list='allpages', apprefix=title):
    results.append(res)
  return len(results[0]['allpages'])
def get_wiki_text(page, section=None):
    result = site('parse', page=page, prop=['wikitext'], section=section)
    return result['parse']['wikitext']
def extract_date(event):
    '''
    Extracts the date of an event in the format ['yyyy', 'mm', 'dd']
    Accepted syntaxes : [[yyyy.mm.dd]], [[yyyy.mm]] and [[yyyy]]
    '''
    text = event.split('[[')
    date = ['0001', '01', '01']
    for i in range(len(text)):
        if text[i][0:4].isnumeric() and text[i][5:7].isnumeric() and text[i][8:10].isnumeric(): #format yyyy.mm.dd
            date = text[i][0:10].split('.')
        if text[i][0:4].isnumeric() and text[i][5:7].isnumeric() and text[i][7:9] == ']]': #format yyyy.mm
            date = [text[i][0:4], text[i][5:7] ,'01']
        if text[i][0:4].isnumeric() and text[i][4:6] == ']]': #format yyyy
            date = [text[i][0:4], '01' ,'01']
       
    return date
def add_event_to_page(title, section, event):
    '''
    Add a line at the correct place of the page
    (chronogical order)
    '''
    event_date = extract_date(event)
    event_date = int(event_date[0]+event_date[1]+event_date[2])
   
    text = get_wiki_text(title, section)
    text = text.split('\n')
   
    event_not_added = True
    for i in range(len(text)):
        date = extract_date(text[i])
        date = int(date[0]+date[1]+date[2])
        if (date > event_date) and event_not_added:
            text.insert(i, event)
            event_not_added = False
    if event_not_added:
        text.append(event)
 
    text = '\n'.join(text)
   
    site('edit', title=title,
        section=section,
        text=text,
        token=site.token())
    return text
def get_section_nb(page, section_wanted):
  '''
  Returns index of wanted sections of page
  '''
  sections = site('parse', page=page, prop=['sections'])['parse']['sections']
  if len(sections) >= 1:
    for i in range(len(sections)):
      if sections[i]['anchor'] == section_wanted:
        return int(sections[i]['index'])-1
  # If the section is not found or there is no section, return 0
  return 0
</pre>
=== Création de pages de liens entre tous les évènements de [[Ventes d'œuvres par année | ventes d'œuvres]] ===
<pre>
def create_pages_link(df_to_link, title_tot, title_year, title_day):
    '''
    On crée une page avec toutes les années: de 1657 à 1985
    '''
    linker_page_title = title_tot
    linker_page_contents = ''
    per_year_page_contents = ''
    per_day_page_contents = ''
    years = df_to_link.sale_year.sort_values().unique() #toutes les années ou il y a eu une vente
    for i in range(len(years)-1): #-1 car le sort_values() ajoute un NaN à la fin
        sale_day = df_to_link.loc[df_to_link['sale_year'] == years[i], 'sale_day']
        sale_month = df_to_link.loc[df_to_link['sale_year'] == years[i], 'sale_month']
        #  On crée une variable sale_date avec milliers et centaines pour le mois et
        #  dizaines et unité pour le jour 
        sale_date = sorted((100*sale_month+sale_day).unique())
        per_year_page_title = title_year +f"{years[i]}"
        linker_page_entry = f"* [[" + per_year_page_title + f"|{years[i]}]]"
        #----------------------------------------------event level 1
        linker_page_contents += linker_page_entry + '\n'
        for j in range(len(sale_date)):
            day = sale_date[j] % 100
            month = int((sale_date[j] - day)/100)
            if day < 10:
                day_str = "0" + str(day)
            else:
                day_str = str(day)
            if month < 10:
                month_str = "0" + str(month)
            else:
                month_str = str(month)
            if (month == 0):
                per_day_page_title = title_day + f"{years[i]}.-.-"
                per_year_page_entry = f"* [[" + per_day_page_title + f"|{years[i]}.-.-]]"
            elif (day == 0):
                per_day_page_title = title_day + f"{years[i]}.{month_str}.-"
                per_year_page_entry = f"* [[" + per_day_page_title + f"|{years[i]}.{month_str}.-]]"
            else:
                per_day_page_title = title_day + f"{years[i]}.{month_str}.{day_str}"
                per_year_page_entry = f"* [[" + per_day_page_title + f"|{years[i]}.{month_str}.{day_str}]]"
            #----------------------------------------------event level 2
            per_year_page_contents += per_year_page_entry + "\n"
            sales_of_day = df_to_link[(df_to_link['sale_year'] == (years[i])) & (df_to_link['sale_month'] == (month)) & (df_to_link['sale_day'] == (day))].index
            for k in range(len(sales_of_day)):
                per_day_page_entry = (sale_entry_create(df_to_link.iloc[sales_of_day[k]]))
                #----------------------------------------------event level 3
                per_day_page_contents += per_day_page_entry + "\n"
                #----------------------------------------------page modif level 3
            try:
                site('edit', title=per_day_page_title,
                text=per_day_page_contents,
                token=site.token()) 
            except: #On ségmente l'écriture s'il y a trop d'entrées à la fois (ça dépasse la limite imposée par wikipast)
                length = len(per_day_page_contents)
                print(len(sales_of_day))
                print(per_day_page_title)
                try:
                    site('edit', title=per_day_page_title,
                      text=per_day_page_contents[:length/2],
                      token=site.token())
                    site('edit', title=per_day_page_title,
                      appendtext=per_day_page_contents[length/2:],
                      token=site.token())
                except:
                    try:
                        site('edit', title=per_day_page_title,
                          text=per_day_page_contents[:length/3],
                          token=site.token())
                        site('edit', title=per_day_page_title,
                          appendtext=per_day_page_contents[length/3:2*length/3],
                          token=site.token())
                        site('edit', title=per_day_page_title,
                          text=per_day_page_contents[2*length/3:],
                          token=site.token())
                    except:
                        pass
            per_day_page_contents = ''
        #----------------------------------------------page modif level 2
        site('edit', title=per_year_page_title,
        text=per_year_page_contents,
        token=site.token()) 
        per_year_page_contents = ''
    #----------------------------------------------page modif level 1
    site('edit', title=linker_page_title,
    text=linker_page_contents,
    token=site.token())
    linker_page_contents = ''
    return
# création des pages de lien
page_all_years = "Ventes d'œuvres par année" #--> title tot
page_per_year = "Ventes d'œuvres en " # + année -->title year
page_per_day = "Ventes d'œuvres le " # + année.mois
create_pages_link(df_tot, page_all_years, page_per_year, page_per_day)
</pre>
=== Création et modification des pages pour chaque œuvre ===
<pre>
def add_one_line(line, section) :
  '''
  Creates the new page, adds description and sale event for one sale
  '''
  text = ''
  title = sale_title_create(line)
  if page_already_exists(title):
    text = get_wiki_text(title, section)
  else :
    # create page
    site('edit', title=title,
        text='',
        token=site.token())
  if text.find("Wikidata") == -1 : #si les infos n'ont pas encore été écrites
    info = sale_info_create(line)
    text = info + text
 
  event = sale_entry_create(line)
  event_date = extract_date(event)
  event_date = int(event_date[0]+event_date[1]+event_date[2])
 
  if text.find(event) == -1 :
    event_not_added = True
    text = text.split('\n')
    for i in range(len(text)):
      date = extract_date(text[i]) #si pas de date, an 01 par défaut => toujours plus récent
      date = int(date[0]+date[1]+date[2])
      if (date > event_date) and event_not_added:
          text.insert(i, event)
          event_not_added = False
    if event_not_added:
        text.append(event)
    text = '\n'.join(text)
  site('edit', title=title,
        text=text,
        token=site.token())
section_wanted = 0
error_table = []
for i in range(df_tot.shape[0])
    line = df_tot.iloc[i]
    try :
        add_one_line(line, section_wanted)
    except :
        error_table += [i]
 
print(len(error_table))
</pre>

Dernière version du 1 juin 2021 à 06:43

Description

Le but de l'ArtBot est de créer des pages pour des œuvres qui regroupent l'historique de leurs ventes. Il utilise comme base de données les registres de ventes du Getty Provenance Index fourni par le Getty Research Institute. À partir de ces registres, le bot fait une sélection d'informations pertinentes (titre et auteur de l'œuvre, date de la vente, vendeur, acheteur, etc.) et les regroupe dans une seule base de données. Les pages titre (artiste) sont ensuite créées et complétées avec la liste des évènements de vente classés par ordre chronologique. Enfin, le bot génère une page de liens pour retrouver et lier toutes les pages d'œuvres entre elles.

Il a été créé par Vincent Philippoz, Michael Richter et Agatha Duranceau.

Gestion des bases de données

L'ArtBot utilise la bibliothèque pandas pour les fonctions de gestion de bases de données.

Récupération des données

Les fichiers .csv ont directement été récupérés sur GitHub [1], mais ne contiennent pas tous les mêmes catégories, ou les mêmes noms pour les mêmes catégories. Les entrées pertinentes ont été sélectionnées et renommées, puis les fichiers concaténés afin de travailler sur une base unique plus facile à lire par l'ArtBot.

Il est possible d'afficher les données de la manière suivante :


Exemple d'informations contenues dans la base de données


Le jeu de données complet contient 1'288'210 entrées, dont 77.84 % de titres uniques et 15.93 % d'artistes uniques.

Dictionnaire Wikidata

60 genres différents et 82 types d'œuvres ont été identifiés dans la base de données. Cependant, cette dernière contient des informations en anglais, en français, en allemand et en néerlandais. Il est donc nécessaire d'identifier les différents types et genres à l'aide d'une étiquette indépendante du mot utilisé dans la base de données. Pour cela, les identifiants Wikidata [2] sont utilisés, et un dictionnaire pour associer les bons identifiants aux bonnes catégories a été créé.

Dictionnaire de traduction

Des dictionnaires ont également été créés afin de traduire en français les informations telles que la nationalité de l'artiste et la monnaie utilisée pour la vente. Cependant, les titres d'œuvres n'ont pas été traduits.

Fonctionnement

L'ArtBot utilise la bibliothèque pywikiapi [3] pour lire et écrire dans les pages de Wikipast.

Création/modification d'une page

L'ArtBot vérifie pour chaque œuvre si une page avec le format: Titre de l'œuvre(Auteur) existe déjà. Si non, il crée la page en question et ajoute la description de l'œuvre et une entrée pour l'évènement de vente dans les sections correspondantes. Si oui, il copie le texte de la page, insère la nouvelle entrée au bon endroit et écrase l'ancienne version de la page. Les entrées concernant les évènements de vente sont classées par ordre chronologique.

La création du titre de la page se fait grâce à l'appel de la fonction sale_title_create qui reçoit comme argument une ligne de la dataframe contenant les informations d'une œuvre et génère un titre de la forme "Nom de l'œuvre (Nom de l'artiste)".

Structure des pages

Les pages d'œuvres générées par l'ArtBot sont structurées de la manière suivante :

Titre de la page : Titre de l'œuvre (Artiste)

Description : [[type oeuvre]], [[genre]]

Syntaxe pour un évènement de vente :

  • [[Année.Mois.Jour]] / [[maison de vente]]. [[Vente de l'oeuvre]] décrite comme [[titre oeuvre]] réalisée par [[artiste]] (nationalité : [[nationalité]]), vendue par [[vendeur]], achetée par [[acheteur]] au prix de prix + currency. [Source]

Les hypermots sont récupérés directement dans la dataframe, et sont traduits grâce à des dictionnaires lorsque c'est nécessaire. Si certaines données sont absentes, elles sont ignorées et la proposition correspondante n'est pas écrite.

Insertion d'une entrée à une page déjà existante

Afin d'insérer une nouvelle entrée au bon endroit sur la page d'une œuvre (ordre chronologique), l'ArtBot fait appel à la fonction sale_entry_create qui reçoit comme argument les informations sur la vente d'une œuvre et les transforme pour les afficher au format désiré décrit dans la définition de la structure de la page. La fonction retourne ensuite la chaîne de caractères complète contenant toutes les informations présentes pour l'œuvre analysée. Les informations manquantes seront bien sûr ignorées (par exemple si elles contiennent des NaN, "unknown" etc.), et si le nom de l'artiste n'est pas précisé, il sera remplacé par la mention "artiste anonyme". La fonction add_one_line insère ensuite la chaîne de caractères sur la page en respectant la chronologie des ventes.

Ajout du type/genre de l'œuvre

Afin d'ajouter le type et le genre de l'œuvre lorsque ces informations sont disponibles l'ArtBot fait appel à la fonction sale_info_create qui reçoit comme argument une ligne de la dataframe, c'est à dire les infos relatives à une œuvre unique. Elle recherche ensuite les informations sur le type et le genre de l'œuvre et y associe un mot français et le numéro Wikidata correspondant. Enfin, la fonction retourne les informations de type et de genre au format correct. C'est de nouveau la fonction add_one_line qui insère la description au début de la page de l'œuvre si elle n'existe pas déjà.

Création de liens entre les pages

Les pages Wikipast peuvent être créées sans être reliées à aucune autre page du site. Ceci ne pose pas problème à petite échelle, mais lors de la génération de plus d'un million de page, celles-ci peuvent être inaccessible. Pour remédier à cette complication, des pages de liens sont définies. Ces dernières sont structurées de la manière suivante :

La page Ventes d'œuvres par année est crée comme source de l'arborescence de liens. Elle regroupe toutes les années durant lesquelles une vente à été enregistrée. Ensuite, une page est créée pour chacune de ces années (Exemple). Celles-ci rassemblent toutes les dates (année/mois/jour) de cette année auxquelles une entrée est enregistrée dans la base de données. Le schéma se répète et chaque date possède une page réunissant toutes les ventes homologuées de la journée (Exemple).

Extrait de la page Ventes d'œuvres en 1770.
Extrait de la page Ventes d'œuvres le 1770.02.14.

Quelques exceptions sont tout de mêmes notables. Heureusement, chaque entrée est associée à une année de vente. Par contre le mois et le jour ne sont pas toujours compris dans la database. Ainsi, deux cas possibles sont traités : Si l'information du mois est manquante, le jour et le mois sont remplacés par un "-" car le jour sans le mois apporte peu d'informations. Le deuxième cas de figure est celui ou seul le jour est inconnu. Le mois est alors pertinent et seulement le jour est changé pour un "-". De cette manière, même les entrées de la base de données avec une date incomplète seront ajoutés dans l'arborescence de liens.

Discussion des performances de l'ArtBot

Le bot fonctionne bien dans l'ensemble, il a créé 428'069 pages. Cependant, les points suivants sont à améliorer :

Titres des œuvre

La base de donnée a une grande variété de syntaxes et de langues. Nous n'avons pas choisi de traduire les titre d'œuvres car les appels à Google Translate sont longs et auraient considérablement ralenti l'exécution déjà lente. Quant à elle, la syntaxe pose des problèmes de compatibilité avec Wikipast. En effet, des caractères réservées (\, [, ], ...) apparaissent parfois, ce qui casse des hypermots voire rend impossible la création de la page. Pour résoudre ce problème, il serait possible de filtrer chaînes de caractère afin d'enlever les symboles problématiques. Nous avons choisi de simplement ignorer les entrées posant problème, faute de temps. Cependant, un dernier problème avec les titres est leur longueur ; Certains sont une description complète de l'oeuvre, d'autre incluent le nom de l'artiste, ... Du fait de la grande diversité, un filtrage simple semble impossible et nous avons choisi de simplement utiliser les titres tel quels.

Identification des entités uniques

Lorsque le titre d'une œuvre et ou d'un artiste est inconnu ou trop vague (comme "Paysage", "Portrait", "Un Christ" etc.), il arrive que l'ArtBot n'ait pas pris en charge l'exception et considère plusieurs œuvres comme une seule et unique entité. La page Untitled_(artiste_anonyme) est représentative de ce problème.

Vitesse d'exécution

Getty propose une base de donnée avec près de 1'300'000 ventes. Bien que traiter un événement soit rapide, le nombre imposant de données conduit inévitablement à un long temps d'exécution. Avec le code développé, nous atteignons approximativement 170 ms par entrée, soit plus de 2 jours d'exécution au total. Ainsi, nous n'avons pu exporter que 33.23 % de la base de donnée. Pour réduire ce temps, le programme python peut être optimisé et une meilleure connexion à Wikipast serait requise.

Code complet

Importation des bibliothèques

import pandas as pd
!pip install pywikiapi

Importation et mise en forme de la base de données

df_SC1 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_1.csv',
           low_memory=False)
df_SC2 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_2.csv',
           low_memory=False)
df_SC3 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_3.csv',
           low_memory=False)
df_SC4 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_4.csv',
           low_memory=False)
df_SC5 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_5.csv',
          low_memory=False)
df_SC6 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_6.csv',
          low_memory=False)
df_SC7 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_7.csv',
          low_memory=False)
df_SC8 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_8.csv',
          low_memory=False)
df_SC9 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_9.csv',
          low_memory=False)
df_SC10 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_10.csv',
          low_memory=False)
df_SC11 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_11.csv',
          low_memory=False)
df_SC12 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_12.csv',
          low_memory=False)
df_SC13 = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/sales_catalogs/sales_contents_13.csv',
          low_memory=False)
df_knoedler = pd.read_csv('https://raw.githubusercontent.com/thegetty/provenance-index-csv/master/knoedler/knoedler.csv',
          low_memory=False)          

# Création de la bases de données des Sales Catalogs
SC_wanted_columns = ["title",
                    "artist_name_1",
                    "lot_sale_year", 
                    "lot_sale_month", 
                    "lot_sale_day",
                    "auction_house_1",
                    "nationality_1",
                    "price_amount_1",
                    "price_currency_1",
                    "buy_name_1",
                    "sell_name_1",
                    "object_type",
                    "genre"]

df_SC_all = df_SC1.loc[:, SC_wanted_columns]

SALES_CATALOGUE = [df_SC2, df_SC3, df_SC4, df_SC5, df_SC6, df_SC7, df_SC8, df_SC9, df_SC10, df_SC11, df_SC12, df_SC13]

for dataframe in SALES_CATALOGUE :
  df_temp = dataframe.loc[:, SC_wanted_columns]
  df_SC_all = pd.concat([df_SC_all, df_temp])

# Mettre la même convention de noms
df_SC_all = df_SC_all.rename(columns = {"artist_name_1" : "artist_name",
                                        "lot_sale_year" : "sale_year" , 
                                        "lot_sale_month" : "sale_month"  , 
                                        "lot_sale_day": "sale_day",
                                        "buyer_name_1" : "buyer_name", 
                                        "seller_name_1" : "seller_name",
                                        "auction_house_1" : "auction_house",
                                        "nationality_1" : "artist_nationality",
                                        "price_amount_1" : "purch_amount",
                                        "price_currency_1" : "purch_currency",
                                        "buy_name_1" : "buyer_name",
                                        "sell_name_1" : "seller_name"})

# Création de la bases de données de Knoedler
K_wanted_columns = ["title",
                    "artist_name_1",
                    "nationality_1",
                    "genre",
                    "object_type",
                    "sale_date_year",
                    "sale_date_month",
                    "sale_date_day",
                    "purch_amount",
                    "purch_currency",
                    "seller_name_1",
                    "buyer_name_1"]

df_K = df_knoedler.loc[:, K_wanted_columns]

df_K = df_K.reindex(columns=list(df_K.columns) + ["auction_house"])
df_K.auction_house = df_K.auction_house.fillna(value='Knoedler')

# Mettre la même convention de noms et le même ordre
df_K = df_K.rename(columns = {"artist_name_1" : "artist_name",
                              "sale_date_year" : "sale_year" , 
                              "sale_date_month" : "sale_month"  , 
                              "sale_date_day": "sale_day",
                              "nationality_1" : "artist_nationality", 
                              "buyer_name_1" : "buyer_name", 
                              "seller_name_1" : "seller_name"})

columns_list = df_SC_all.columns
df_K = df_K.reindex(columns = columns_list)

# Combinaison des dataframes
df_tot = pd.concat([df_SC_all, df_K])

# Mettre les nombre de la date comme des entiers
df_tot.sale_year = df_tot.sale_year.convert_dtypes(convert_integer = True) 
df_tot.sale_month = df_tot.sale_month.convert_dtypes(convert_integer = True) 
df_tot.sale_day = df_tot.sale_day.convert_dtypes(convert_integer = True)

Création de dictionnaires

 # Dictionnaires de traduction
list_nationalities = df_tot.artist_nationality.unique()
trans_nat = ['', 'Flamand', 'Néerlandais ou flamand', 'Italien', 'Néerlandais',
             'Néerlandais', 'Français', '', 'Néerlandais ou flamand ou français',
             'Allemand', 'Espagnol', 'Flamand ou français', 'Belge ou flamand',
             'Flamand ou néerlandais', 'Autrichien', 'Britannique', 
             'Flamand ou allemand ou néerlandais', 'Français et italien', 
             'Allemand ou suisse', 'Hongrois', 'Autrichien et hongrois', 
             'Suisse', 'Néerlandais et italien', 'Allemand ou italien', 
             'Flamand ou allemand', 'Allemand et italien', 'Suédois', 'Polonais',
             'Chinois', 'Grec et espagnol', '', 'Belge', 'Néerlandais ou allemand',
             'Français ou néerlandais', 'Flamand', 'Français ou suisse', 
             'Belge ou flamand ou néerlandais', 'Italien ou suisse', 'Bohème', 
             'Américain', '', 'Britannique et flamand', 'Néerlandais ou français',
             'Belge ou français', '', 'Belge ou flamand et français', '', 
             'Flamand et italien', '', 'Danois', 'Autrichien ou allemand', 'Grec',
             'Britannique et allemand', 'Irlandais', 'Britannique ou français',
             'Britannique ou irlandais', 'Tchèque', 'Autrichien ou français', 
             'Néerlandais ou néerlandais', 'Néerlandais ou néerlandais', 
             'Tchèque ou allemand', '', 'Romain', 'Britannique', 'Arménien', 
             'Portugais', 'Français ou allemand', 'Danois ou allemand', 
             'Autrichien et flamand', 'Bohème ou allemand', 'Péruvien', 'Russe',
             'Britannique ou néerlandais', 'Mexicain', '', 'Haïtien', 
             'Français ou italien', 'Sud-américain', 'Italien ou espagnol',
             'Dalmate', 'Croate ou italien', 'Italien', 'Indien', 'Égyptien', 
             'Byzantin', 'Étrusque', 'Néerlandais ou flamand ou allemand', 
             'Autrichien et allemand', 'Greuze', 'Britannique ou danois', 
             'Canadien', 'Français ou suédois', 'Autrichien ou suisse', 
             'Néerlandais', 'Allemand et suisse', 'Français ou espagnol',
             'Gaulois', 'Croate ou istrien', 'Français', 'Flamand', 'Japonais', 
             'Allemand ou autrichien', 'Allemand ou suédois', 'Autrichien et bohémien', 
             '', 'Autrichien ou italien', '', 'Autrichien ou flamand', 'Allemand ou russe', 
             'Danois ou norvégien', 'Danois ou suédois', 'Norvégien', 'Bohémien ou tchèque', 
             'Persan', 'Erman', 'Australien', 'Turc', 'Flamand', 'Yougoslave', '', 
             'Bulgare', 'Roumain', 'Finnois', '', 'Argentin', 'Scandinave', 'Javanais',
             '', '', 'Néerlandais ou allemand', 'Sud-africain', 'Monégasque', 
             'Équatorien', 'Uruguayen', 'Néerlandais et américain']
dict_nat = dict(zip(list_nationalities, trans_nat))

list_currencies = df_tot.purch_currency.unique()
trans_curr = ['fl', 'frs', 'fl', 'frs', 'fl', 'frs', 'frs', '£', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs',
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'fl', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 'frs', 
              'frs', 'frs', 'frs', 'fl', 'frs', 'frs', 'fl', 'frs', 'frs', 'fl', 'fl', 'fl', '£', '£', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 
              'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', 'gs', '£', '£', '', 'gs', 'gs', 'gs', 'gs', '£', 'gs', 
              'gs', '£', '£', '£', 'assignats', 'ecus', '£', '£', '£', 'm', 'rt', 'gr', 'sch', 'sch', 'sgr', 'kr', 'th', 'duc.', 'st', 
              'danish rigsdaler', 'reichsmark', 'sch', 'chf', '$', 'marks', '$', 'thalers', 'lire', 'chf']
dict_curr = dict(zip(list_currencies, trans_curr))

types_trans = {"miniature" : "miniature", 
    "enamel" : "émail",  
    "embroidery" : "broderie",  
    "mosaic" : "mosaïque", 
    "painting" : "peinture", 
    "drawing" : "dessin", 
    "sculpture" : "sculpture", 
    "furniture" : "meuble", 
    "fresco" : "fresque", 
    "tapestry" : "tapisserie" ,
    "decorative arts" : "objet d'arts décoratifs",
    "engraved gemstone" : "gravure sur pierres précieuses", 
    "dessin" : "dessin", 
    "peinture" : "peinture", 
    "émail" : "émail", 
    "peinture [?]" : "peinture", 
    "médaille" : "médaille",
    "tapisserie" : "tapisserie", 
    "mosaique" : "mosaïque", 
    "email" : "émail",
    "mosaïque" : "mosaïque", 
    "meuble" : "meuble",
    "tapisserie" : "tapisserie", 
    "dentelle" : "dentelle", 
    "marqueterie" : "marqueterie", 
    "peinture ou dessin" : "", 
    "broderie" : "broderie", 
    "gemälde" : "peinture",  
    "gemalde" : "peinture", 
    "zeichnung" : "dessin", 
    "skulptur" : "sculpture", 
    "miniatur" : "miniature",  
    "gemãlde" : "peinture",
    "skulptur" : "sculpture", 
    "graphik" : "", 
    "painting [?]" : "peinture",
    "watercolor" : "aquarelle", 
    "print" : "", 
    "pastel" : "pastel", 
    "book" : "livre", 
    "clocks" : "horloge", 
    "photograph" : "photographie",
    "decorative art" : "objet d'arts décoratifs", 
    "playing cards" : "cartes à jouer", 
    "copper plate" : "assiette de cuivre",
    "clothing" : "vêtement", 
    "maps" : "carte", 
    "minerals" : "minéraux"}

genres_trans = {"history" : "scènes historiques", 
    "genre" : "", 
    "landscape" : "paysage",  
    "portraits" : "portrait", 
    "animals" : "représentations d'animaux", 
    "still life" : "nature morte", 
    "historie" : "scènes historiques", 
    "landschaft" : "paysage", 
    "stilleben" : "nature morte", 
    "tiere" : "représentations d'animaux", 
    "porträt" : "portrait", 
    "stillleben |t stilleben" : "nature morte",
    "[not identified]" : "", 
    "portrait" : "portrait", 
    "abstract" : "oeuvre abstraite"}

# Dictionnaires pour lier à Wikidata
types_wiki = {"peinture" : "[https://www.wikidata.org/wiki/Q3305213 Q3305213]",
    "sculpture" : "[https://www.wikidata.org/wiki/Q860861 Q860861]",
    "photographie" : "[https://www.wikidata.org/wiki/Q125191 Q125191]",
    "émail" : "[https://www.wikidata.org/wiki/Q79496108 Q79496108]",
    "dessin" : "[https://www.wikidata.org/wiki/Q93184 Q93184]",
    "tapisserie" : "[https://www.wikidata.org/wiki/Q184296 Q184296]",
    "broderie" : "[https://www.wikidata.org/wiki/Q28966302 Q28966302]",
    "meuble" : "[https://www.wikidata.org/wiki/Q14745 Q14745]",
    "fresque" : "[https://www.wikidata.org/wiki/Q134194 Q134194]",
    "objet d'arts décoratifs" : "[https://www.wikidata.org/wiki/Q631931 Q631931]",
    "gravure sur pierres précieuses" : "[https://www.wikidata.org/wiki/Q1501187 Q1501187]",
    "médaille" : "[https://www.wikidata.org/wiki/Q131647 Q131647]",
    "mosaïque" : "[https://www.wikidata.org/wiki/Q133067 Q133067]",
    "miniature" : "",
    "dentelle" : "[https://www.wikidata.org/wiki/Q231250 Q231250]",
    "marqueterie" : "[https://www.wikidata.org/wiki/Q1049923 Q1049923]",
    "aquarelle" : "[https://www.wikidata.org/wiki/Q18761202 Q18761202]",
    "estampe" : "[https://www.wikidata.org/wiki/Q11060274 Q11060274]",
    "lithographie" : "[https://www.wikidata.org/wiki/Q11060274 Q11060274]",
    "pastel" : "[https://www.wikidata.org/wiki/Q12043905 Q12043905]",
    "livre" : "[https://www.wikidata.org/wiki/Q571 Q571]",
    "horloge" : "[https://www.wikidata.org/wiki/Q376 Q376]",
    "montre": "[https://www.wikidata.org/wiki/Q376 Q376]",
    "cartes à jouer" : "[https://www.wikidata.org/wiki/Q47883 Q47883]",
    "assiette de cuivre" : "",
    "vêtement" : "[https://www.wikidata.org/wiki/Q11460 Q11460]",
    "carte" :  "[https://www.wikidata.org/wiki/Q4006 Q4006]",
    "minéraux" : "[https://www.wikidata.org/wiki/Q7946 Q7946]"}

genres_wiki = {"paysage" : "[https://www.wikidata.org/wiki/Q191163 Q191163]",
    "portrait" : "[https://www.wikidata.org/wiki/Q134307 Q134307]",
    "oeuvre abstraite" : "[https://www.wikidata.org/wiki/Q128115 Q128115]",
    "nature morte" : "[https://www.wikidata.org/wiki/Q170571 Q170571]",
    "représentations d'animaux" : "[https://www.wikidata.org/wiki/Q16875712 Q16875712]",
    "scènes historiques" : "[https://www.wikidata.org/wiki/Q742333 Q742333]"}

Traduction des nationalités des artistes et monnaies

def translate_nat(nationnality):
  '''
  Traduis la nationalité en français
  '''
  for word in dict_nat : 
    if nationnality == word:
      return dict_nat[word]
  return ""

def translate_curr(currency):
  '''
  Traduis la devise en français
  '''
  for word in dict_curr : 
    if currency == word:
      return dict_curr[word]
  return ""

Génération du texte pour les évènements de vente

def sale_title_create(line) :
  if (line['artist_name'] == 'Anonymous' or line['artist_name'] == '[unbekannt]' or line['artist_name'] =='Anonyme' or pd.isna(line['artist_name'])) :
    title = (str(line['title']) + " (artiste anonyme)")
  else :
    title = (str(line['title']) + " (" + str(line['artist_name']) + ")")

  return title

def sale_entry_create(line) :
  '''
  Take a line from the cleaned dataframe and prints it according to the 
  Wikidata event syntax.
  '''

  if line['sale_day'] < 10:
    day_str = "0" + str(line['sale_day'])
  else:
    day_str = str(line['sale_day']) 

  if line['sale_month'] < 10:
    month_str = "0" + str(line['sale_month'])
  else:
    month_str = str(line['sale_month'])
  
  #il y a des dates ou le mois ou le jour ne sont pas connus
  if (line['sale_month'] == 0): #ça ne sert à rien d'avoir le jour si on n'a pas le mois
    date_txt = (f"[[{line['sale_year']}.-.-]]/")
  elif (line['sale_day'] == 0):
    date_txt = (f"[[{line['sale_year']}.{month_str}.-]]/")
  else:
    date_txt = (f"[[{line['sale_year']}.{month_str}.{day_str}]]/")

  if (pd.isna(line['auction_house']) or line['auction_house'] == 'Anonymous'): 
    house_txt = (' -. ')
  else :
    house_txt = (f"[[{line['auction_house']} | maison de ventes : {line['auction_house']}]]. ")

  title_txt = (f"[[Ventes d'œuvres par année|Vente de l'œuvre]] décrite comme ''[[" + sale_title_create(line)+f"|{line['title']}]]''")

  if (line['artist_name'] == 'Anonymous' or line['artist_name'] == 'unbekannt' or line['artist_name'] =='Anonyme' or pd.isna(line['artist_name'])) :
    artist_txt = (" réalisée par un.e artiste anonyme")
  else : 
    artist_txt = (f" réalisée par [[{line['artist_name']}]]")

  translated_nat = translate_nat(line['artist_nationality'])
  if (pd.isna(line['artist_nationality']) or translated_nat == "") :
    nationality_txt = ""
  else : 
    nationality_txt = (f" (nationalité : [[{translated_nat}]])")

  if (pd.isna(line['seller_name'])):
    seller_txt = ('')
  else :
    seller_txt = (f", vendue par [[{line['seller_name']}]]")

  if (pd.isna(line['buyer_name'])):
    buyer_txt = ('')
  else :
    buyer_txt = (f", achetée par [[{line['buyer_name']}]]")

  if (pd.isna(line['purch_amount']) or pd.isna(line['purch_currency'])): 
    price_txt = ('')
  else :
    price_txt = (f" au prix de {line['purch_amount']} {translate_curr(line['purch_currency'])}")

  source_txt = (f". [https://github.com/thegetty/provenance-index-csv]")

  concat_txt = '\n* '+date_txt+house_txt+title_txt+artist_txt+seller_txt+buyer_txt+price_txt+source_txt

  return concat_txt

def sale_info_create(line) :
  '''
  Take a line from the cleaned dataframe and prints its type and genre.
  Links it to WikiData
  '''
  # Trouver les correspondance
  type_entry = str(line['object_type']).lower().split("; ")
  genre_entry = str(line['genre']).lower().split("; ")

  type_txt = ""
  # Traduction en français
  for i in range(len(type_entry)):
    for word in types_trans:
      if word == type_entry[i]:
        type_entry[i] = types_trans[word]
        break
  # Associe un lien Wikidata
  for i in range(len(type_entry)):
    for word in types_wiki:
      if word == type_entry[i]:     
        type_txt = type_txt + "Type : " + word.capitalize() + ". (Wikidata : " + types_wiki[word] + ")\n"
        break

  genre_txt = ""
  # Traduction en français
  for i in range(len(genre_entry)):
    for word in genres_trans:
      if word == genre_entry[i]:
        genre_entry[i] = genres_trans[word]
        break
  # Associe un lien Wikidata
  for i in range(len(genre_entry)):
    for word in genres_wiki:
      if word == genre_entry[i]:     
        genre_txt = "Genre : " + word.capitalize() + ". (Wikidata : " + genres_wiki[word] + ")\n"
        break

  return type_txt + genre_txt

Connexion à Wikipast

# Define site
from pywikiapi import Site
site = Site('http://wikipast.epfl.ch/wikipast/api.php')
site.no_ssl = True

# Login information
user='VPhilippoz@ArtBot'
passw='8kgee1cofm8l8ks721ju505r0ju50jes'

# Login du bot
site.login(user, passw)

Lecture d'informations depuis Wikipast

def page_already_exists(title):
  '''
  Check if a page with the name "page" exists in Wikipast
  Return number of pages with the name (can be used as bool)
  '''
  results = []
  for res in site.query(list='allpages', apprefix=title): 
    results.append(res)
  return len(results[0]['allpages'])

def get_wiki_text(page, section=None):
    result = site('parse', page=page, prop=['wikitext'], section=section)
    return result['parse']['wikitext']

def extract_date(event):
    '''
    Extracts the date of an event in the format ['yyyy', 'mm', 'dd']
    Accepted syntaxes : [[yyyy.mm.dd]], [[yyyy.mm]] and [[yyyy]]
    '''
    text = event.split('[[')
    date = ['0001', '01', '01']
    for i in range(len(text)):
        if text[i][0:4].isnumeric() and text[i][5:7].isnumeric() and text[i][8:10].isnumeric(): #format yyyy.mm.dd
            date = text[i][0:10].split('.')
        if text[i][0:4].isnumeric() and text[i][5:7].isnumeric() and text[i][7:9] == ']]': #format yyyy.mm
            date = [text[i][0:4], text[i][5:7] ,'01']
        if text[i][0:4].isnumeric() and text[i][4:6] == ']]': #format yyyy
            date = [text[i][0:4], '01' ,'01']
        
    return date

def add_event_to_page(title, section, event):
    '''
    Add a line at the correct place of the page 
    (chronogical order)
    '''
    event_date = extract_date(event)
    event_date = int(event_date[0]+event_date[1]+event_date[2])
    
    text = get_wiki_text(title, section)
    text = text.split('\n')
    
    event_not_added = True
    for i in range(len(text)):
        date = extract_date(text[i])
        date = int(date[0]+date[1]+date[2])
        if (date > event_date) and event_not_added:
            text.insert(i, event)
            event_not_added = False
    if event_not_added:
        text.append(event)
   
    text = '\n'.join(text)
    
    site('edit', title=title, 
         section=section, 
         text=text, 
         token=site.token())
    return text

def get_section_nb(page, section_wanted):
  '''
  Returns index of wanted sections of page
  '''
  sections = site('parse', page=page, prop=['sections'])['parse']['sections']
  if len(sections) >= 1:
    for i in range(len(sections)):
      if sections[i]['anchor'] == section_wanted:
        return int(sections[i]['index'])-1
  # If the section is not found or there is no section, return 0
  return 0

Création de pages de liens entre tous les évènements de ventes d'œuvres

def create_pages_link(df_to_link, title_tot, title_year, title_day):
    '''
    On crée une page avec toutes les années: de 1657 à 1985
    '''
    linker_page_title = title_tot
    linker_page_contents = ''
    per_year_page_contents = ''
    per_day_page_contents = ''
 
    years = df_to_link.sale_year.sort_values().unique() #toutes les années ou il y a eu une vente

    for i in range(len(years)-1): #-1 car le sort_values() ajoute un NaN à la fin
        sale_day = df_to_link.loc[df_to_link['sale_year'] == years[i], 'sale_day']
        sale_month = df_to_link.loc[df_to_link['sale_year'] == years[i], 'sale_month']
        #  On crée une variable sale_date avec milliers et centaines pour le mois et 
        #  dizaines et unité pour le jour  
        sale_date = sorted((100*sale_month+sale_day).unique()) 

        per_year_page_title = title_year +f"{years[i]}"
        linker_page_entry = f"* [[" + per_year_page_title + f"|{years[i]}]]"

        #----------------------------------------------event level 1
        linker_page_contents += linker_page_entry + '\n'

        for j in range(len(sale_date)):

            day = sale_date[j] % 100 
            month = int((sale_date[j] - day)/100)

            if day < 10:
                day_str = "0" + str(day)
            else:
                day_str = str(day) 

            if month < 10:
                month_str = "0" + str(month)
            else:
                month_str = str(month)

            if (month == 0):
                per_day_page_title = title_day + f"{years[i]}.-.-"
                per_year_page_entry = f"* [[" + per_day_page_title + f"|{years[i]}.-.-]]"
            elif (day == 0):
                per_day_page_title = title_day + f"{years[i]}.{month_str}.-"
                per_year_page_entry = f"* [[" + per_day_page_title + f"|{years[i]}.{month_str}.-]]"
            else:
                per_day_page_title = title_day + f"{years[i]}.{month_str}.{day_str}"
                per_year_page_entry = f"* [[" + per_day_page_title + f"|{years[i]}.{month_str}.{day_str}]]"

            #----------------------------------------------event level 2
            per_year_page_contents += per_year_page_entry + "\n"
            sales_of_day = df_to_link[(df_to_link['sale_year'] == (years[i])) & (df_to_link['sale_month'] == (month)) & (df_to_link['sale_day'] == (day))].index

            for k in range(len(sales_of_day)):
                per_day_page_entry = (sale_entry_create(df_to_link.iloc[sales_of_day[k]]))

                #----------------------------------------------event level 3
                per_day_page_contents += per_day_page_entry + "\n"

                #----------------------------------------------page modif level 3
            try:
                site('edit', title=per_day_page_title,
                text=per_day_page_contents,
                token=site.token())  
            except: #On ségmente l'écriture s'il y a trop d'entrées à la fois (ça dépasse la limite imposée par wikipast)
                length = len(per_day_page_contents)
                print(len(sales_of_day))
                print(per_day_page_title)
                try:
                    site('edit', title=per_day_page_title,
                      text=per_day_page_contents[:length/2],
                      token=site.token()) 
                    site('edit', title=per_day_page_title,
                      appendtext=per_day_page_contents[length/2:],
                      token=site.token())
                except:
                    try:
                        site('edit', title=per_day_page_title,
                          text=per_day_page_contents[:length/3],
                          token=site.token()) 
                        site('edit', title=per_day_page_title,
                          appendtext=per_day_page_contents[length/3:2*length/3],
                          token=site.token())
                        site('edit', title=per_day_page_title,
                          text=per_day_page_contents[2*length/3:],
                          token=site.token()) 
                    except:
                        pass

            per_day_page_contents = ''
        #----------------------------------------------page modif level 2
        site('edit', title=per_year_page_title,
        text=per_year_page_contents,
        token=site.token())  
        per_year_page_contents = '' 
    #----------------------------------------------page modif level 1
    site('edit', title=linker_page_title,
    text=linker_page_contents,
    token=site.token()) 
    linker_page_contents = ''

    return

# création des pages de lien
page_all_years = "Ventes d'œuvres par année" #--> title tot
page_per_year = "Ventes d'œuvres en " # + année -->title year
page_per_day = "Ventes d'œuvres le " # + année.mois 

create_pages_link(df_tot, page_all_years, page_per_year, page_per_day)

Création et modification des pages pour chaque œuvre

def add_one_line(line, section) :
  '''
  Creates the new page, adds description and sale event for one sale
  '''
  text = ''
  title = sale_title_create(line)
  if page_already_exists(title):
    text = get_wiki_text(title, section)
  else :
    # create page
    site('edit', title=title,
        text='',
        token=site.token())
  if text.find("Wikidata") == -1 : #si les infos n'ont pas encore été écrites
    info = sale_info_create(line)
    text = info + text
  
  event = sale_entry_create(line)
  event_date = extract_date(event)
  event_date = int(event_date[0]+event_date[1]+event_date[2])
  
  if text.find(event) == -1 :
    event_not_added = True
    text = text.split('\n')
    for i in range(len(text)):
      date = extract_date(text[i]) #si pas de date, an 01 par défaut => toujours plus récent
      date = int(date[0]+date[1]+date[2])
      if (date > event_date) and event_not_added:
          text.insert(i, event)
          event_not_added = False
    if event_not_added:
        text.append(event)
    text = '\n'.join(text)
  site('edit', title=title,
        text=text,
        token=site.token())

section_wanted = 0
error_table = []

for i in range(df_tot.shape[0])
    line = df_tot.iloc[i]
    try : 
        add_one_line(line, section_wanted)
    except : 
        error_table += [i]
  
print(len(error_table))