SW-10-Python-RDFStar

Navigation : << 9-JSONLD | Index | 11-KnowledgeGraphs >>

RDF 1.2 (RDF-Star) : Assertions sur des Assertions

Duree estimee : 45 minutes

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Comprendre la motivation de RDF 1.2 (anciennement RDF-Star) et le problème de la reification classique 2. Utiliser la reification standard (rdf:Statement) pour annoter des assertions avec rdflib 3. Utiliser les graphes nommes (Named Graphs) comme alternative pour le contexte et la provenance 4. Comparer les trois approches : reification classique, graphes nommes, et RDF-Star 5. Appliquer ces patterns a des cas concrets : provenance, confiance, annotations temporelles

Concepts cles

Concept Description
RDF 1.2 Evolution du standard RDF integrant les quoted triples (W3C, 2024)
Quoted Triple Triplet utilise comme sujet ou objet d’un autre triplet : << s p o >>
Reification classique Mécanisme RDF 1.0 pour parler de triplets (4 triplets par assertion)
Graphes nommes Regrouper des triplets dans un contexte identifie par une URI
Provenance Qui a dit quoi ? Source et fiabilite d’une assertion

Prerequis

Note sur le support RDF-Star dans rdflib

La syntaxe RDF-Star (<< s p o >>, quoted triples) est presentee conceptuellement dans ce notebook. Cependant, rdflib 7.x ne supporte pas encore le parsing Turtle-Star ni la classe rdflib.term.Triple. Nous utilisons donc la reification standard et les graphes nommes comme implementations pratiques, tout en montrant comment RDF-Star simplifierait l’ecriture.


Installation des dependances

# Dependances pre-provisionnees (rdflib) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.

Verifions la version de rdflib installee.

La version importe ici plus qu’ailleurs : le support RDF-Star (<< sujet predicat objet >> en Turtle-Star) n’est entre dans rdflib qu’a partir de la generation 6.x, et son degre de maturite (parsing, serialisation, requetage SPARQL-Star) evolue encore. Ce notebook demontre donc d’abord la reification classique rdf:Statement — supportee par toutes les versions et par tous les outils RDF — avant de montrer, dans chaque interpretation, ce que la syntaxe RDF-Star changerait. Verifier la version en tête de notebook, c’est savoir d’avance quelles des variantes montrees sont executables et lesquelles restent de la documentation cible.

Le mécanisme de la cellule suivante est le pattern exact à réutiliser dans vos propres notebooks : import rdflib puis lecture de rdflib.__version__, et surtout un try: from rdflib.term import Triple / except ImportError — c’est la capacité (le type quoted-triple existe-t-il dans cette version ?) qui décide, pas le numéro de version comparé à la main. Un numéro devient obsolète à chaque sortie ; un test de capacité reste juste tant que l’API ne change pas.

Le résultat obtenu ici — rdflib 7.6.0, support natif non disponible — gouverne toute la suite : chaque variante RDF-Star montrée dans ce notebook est une cible (syntaxe de documentation, valide en Turtle-Star dans Jena ou Oxigraph), tandis que la réification classique est ce qui s’exécute réellement sous vos yeux. Gardez cette double lecture en tête : les blocs << ... >> ne sont pas décoratifs, ce sont les versions d’avenir du code qui tourne à côté.

import rdflib

version = rdflib.__version__
major = int(version.split(".")[0])

print(f"rdflib version : {version}")

# Verification du support RDF-Star (Turtle-Star / rdflib.term.Triple)
rdfstar_support = False
try:
    from rdflib.term import Triple as QuotedTriple
    rdfstar_support = True
except ImportError:
    pass

if rdfstar_support:
    print("Support natif RDF-Star (Turtle-Star, QuotedTriple) : disponible.")
else:
    print("Support natif RDF-Star (Turtle-Star, QuotedTriple) : NON disponible.")
    print("Ce notebook utilise la reification standard et les graphes nommes")
    print("comme alternatives fonctionnelles pour enseigner les memes concepts.")
rdflib version : 7.6.0
Support natif RDF-Star (Turtle-Star, QuotedTriple) : NON disponible.
Ce notebook utilise la reification standard et les graphes nommes
comme alternatives fonctionnelles pour enseigner les memes concepts.

1. Le problème : pourquoi parler de triplets ?

RDF permet de representer des faits sous forme de triplets. Mais que faire quand on veut parler d’un triplet lui-même ?

Exemples de besoins courants :

Besoin Exemple Ce qu’on veut exprimer
Provenance Source d’une information “Wikipedia dit que Paris est la capitale de la France”
Confiance Degré de certitude “L’assertion ‘X connait Y’ a un score de confiance de 0.85”
Temporalite Validite dans le temps “Bob travaille chez Acme depuis 2020”
Attribution Qui affirme quoi “Alice affirme que Bob connait Carol”
Annotation Metadata sur un fait “Ce lien a ete decouvert par le crawler v2”

En RDF classique (1.0/1.1), le seul mécanisme disponible est la reification, qui s’avere lourde et peu pratique.

Ces cinq besoins ont un point commun structurel : ils portent tous sur l’acte d’affirmer plutôt que sur le contenu affirmé. « Paris est la capitale » est un fait ; « Wikipédia dit que Paris est la capitale » est un fait sur un fait — et le second ne se range pas dans la même case que le premier. C’est cette catégorie grammaticale manquante (des métadonnées de première classe) que RDF 1.0 n’offre qu’à travers la contorsion de la réification, et que RDF 1.2 introduit comme primitive : parler d’un triplet comme on parle d’une ressource.

En attendant, un test simple pour savoir si votre cas d’usage relève de ce chapitre : pouvez-vous formuler votre besoin en « X dit/dit-que/dit-depuis/quand à propos de tel triplet précis » ? Si la chose affirmée est un triplet (pas une ressource), vous êtes dans l’annotation de triplets — sinon, une propriété ordinaire ou un graphe nommé suffit, et la machinerie qui suit serait surdimensionnée.

1.1 La reification classique : 4 triplets pour 1 assertion

Pour dire “Alice dit que Bob connait Carol” en RDF classique, il faut créer une ressource intermediaire (un rdf:Statement) et decomposer l’assertion en 4 triplets :

# Le triplet original qu'on veut annoter :
#   ex:Bob foaf:knows ex:Carol .

# Reification classique (4 triplets supplementaires) :
ex:statement1 rdf:type rdf:Statement .
ex:statement1 rdf:subject ex:Bob .
ex:statement1 rdf:predicate foaf:knows .
ex:statement1 rdf:object ex:Carol .

# Et enfin l'annotation :
ex:statement1 ex:assertedBy ex:Alice .

Cela represente 5 triplets supplementaires pour annoter un seul fait. De plus, la reification ne garantit pas que le triplet original existe reellement dans le graphe.

Deux précisions de comptabilité avant d’exécuter. D’abord, le bloc Turtle ci-dessus compte 5 triplets supplémentaires (4 de structure + 1 annotation assertedBy) ; la cellule suivante en ajoutera une deuxième annotation (confiance), et la mesure affichera 7 au total — 1 fait + 4 structure + 2 annotations. La formule générale : 1 + 4 + N triplets pour un fait annoté de N manières, et ce même si le fait n’est jamais inséré (le Statement peut décrire un triplet absent — on le verra au fait 4 de la section 4.1).

Ensuite, un piège de vocabulaire : le rdf:Statement n’est pas le triplet qu’il décrit. C’est une ressource à part entière — identifiable, annotable, sérialisable — qui encode les trois composants du triplet dans trois prédicats distincts. Dire « le Statement de Bob-connait-Carol » au lieu de « le triplet Bob-connait-Carol » est le signe qu’on a compris la mécanique : on ne parle pas du fait, on parle de la description de l’éventuel fait. Toute la section qui suit mesure ce que coûte cette indirection.

Voyons cela en pratique avec rdflib : la reification classique est le mecanisme standardise (RDF 1.0) pour parler D’UN triplet. La cellule suivante construit a la main les 4 triplets rdf:subject, rdf:predicate, rdf:object et rdf:type rdf:Statement qui representent l’assertion « Bob connait Carol », puis lui attache une annotation de confiance.

Observez deux choses en executant :

  1. Le fait annoté existe en deux exemplaires independants : le triplet simple Bob connait Carol d’un cote, la structure rdf:Statement de l’autre. Rien ne garantit qu’un consommateur du graphe les reliera — c’est la faille documentee dans l’interpretation qui suit.
  2. Le compte de triplets passe de 1 a 6 pour une seule annotation : c’est ce cout qui motivera la section 2 (fonction utilitaire) puis la comparaison systematique avec RDF-Star.
from rdflib import Graph, URIRef, Literal, Namespace, BNode
from rdflib.namespace import RDF, RDFS, FOAF, XSD

EX = Namespace("http://example.org/")

g_reif = Graph()
g_reif.bind("ex", EX)
g_reif.bind("foaf", FOAF)

# Le fait original
g_reif.add((EX.Bob, FOAF.knows, EX.Carol))

# Réification classique : decomposer le triplet
stmt = EX.statement1
g_reif.add((stmt, RDF.type, RDF.Statement))
g_reif.add((stmt, RDF.subject, EX.Bob))
g_reif.add((stmt, RDF.predicate, FOAF.knows))
g_reif.add((stmt, RDF.object, EX.Carol))

# Annotations : qui affirme ce fait, avec quel degre de confiance ?
g_reif.add((stmt, EX.assertedBy, EX.Alice))
g_reif.add((stmt, EX.confidence, Literal(0.85, datatype=XSD.double)))

print(f"Nombre total de triplets : {len(g_reif)}")
print(f"  - Fait original : 1 triplet")
print(f"  - Reification : 4 triplets")
print(f"  - Annotations : 2 triplets")
print(f"  - Total : 7 triplets pour annoter 1 fait")
print()
print(g_reif.serialize(format="turtle"))
Nombre total de triplets : 7
  - Fait original : 1 triplet
  - Reification : 4 triplets
  - Annotations : 2 triplets
  - Total : 7 triplets pour annoter 1 fait

@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:statement1 a rdf:Statement ;
    ex:assertedBy ex:Alice ;
    ex:confidence 8.5e-01 ;
    rdf:object ex:Carol ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Bob .

ex:Bob foaf:knows ex:Carol .

Interpretation : cout de la reification

Approche Triplets necessaires Lisibilite Garantie d’existence
Fait seul 1 Excellente Oui
Reification classique 1 + 4 + N annotations Faible Non (le Statement est indépendant)
RDF-Star (quoted triple) 1 + N annotations Bonne Configurable

Problemes de la reification classique : 1. Verbeux : 4 triplets par assertion reifiee, explosion combinatoire 2. Deconnecte : le rdf:Statement ne garantit pas que le triplet original existe 3. Requêtes complexes : les requêtes SPARQL pour retrouver les annotations sont longues 4. Pas standard dans la pratique : peu de triple stores optimisent la reification

Lecture des mesures

La sortie de la cellule précédente donne le compte exact : 7 triplets pour un seul fait annoté — 1 triplet de fait (ex:Bob foaf:knows ex:Carol), 4 triplets de structure (le rdf:Statement et ses rdf:subject / rdf:predicate / rdf:object), 2 annotations (assertedBy, confidence). Trois détails de la sérialisation Turtle méritent l’œil :

  • Deux îlots sans pont : le triplet simple et le ex:statement1 coexistent sans aucun lien formel entre eux. Un moteur d’inférence qui parcourt les foaf:knows ne verra jamais ex:statement1 ; une requête sur les Statements ne « voit » pas le fait. Rien dans la sémantique RDF ne les relie — c’est la faille déconnecté du tableau ci-dessus, observable directement dans la sortie.
  • 8.5e-01 : la confiance 0.85 sérialisée en notation scientifique, car le littéral a été créé avec datatype=XSD.double. Le format d’affichage Turtle change, pas la valeur — à retenir quand on compare des sorties sérialisées à du code source.
  • L’ordre des blocs n’est pas garanti : Turtle regroupe par sujet pour compacter, mais rien n’oblige le fait à apparaître près de son Statement. Sur un graphe de milliers de triplets, la « proximité » visuelle ici est un artifact du petit volume.

Arithmétique à l’échelle : annoter un million de faits produit 5 millions de triplets de structure. Un triple store non optimisé pour la réification paiera ce volume deux fois — au stockage, puis à chaque requête d’annotation qui déplie les 4 composants. C’est pourquoi les moteurs sérieux (Jena, Stardog) ont implémenté RDF-Star avant sa standardisation : le besoin est industriel, pas académique.

Une subtilité sémantique enfin : le point 2 du tableau (« le Statement est indépendant ») découle de l’hypothèse ouverte du monde combinée à l’absence de négation en RDF. On ne peut écrire ni « ce Statement correspond à un triplet présent » ni « aucun triplet ne correspond » — le graphe ne peut pas exprimer la cohérence entre fait et mention. RDF 1.2 rend cette liaison configurable (mode par défaut : mention seule), ce qui est précisément le réglage qui manquait.

Point cle : RDF-Star (devenu RDF 1.2) resout ces problemes en permettant d’utiliser un triplet directement comme sujet ou objet d’un autre triplet. En attendant un support complet dans rdflib, nous pouvons utiliser la reification de maniere structuree grace a une fonction utilitaire.


2. RDF 1.2 et les Quoted Triples

Historique

Date Événement
2014 Olaf Hartig propose RDF* (RDF-Star) dans un article academique
2017-2019 Adoption par des triple stores (Blazegraph, Stardog, Jena)
2021 Le W3C créé le RDF-Star Community Group
2023 Le W3C integre RDF-Star dans la specification RDF 1.2
2024 RDF 1.2 devient une W3C Recommendation (Candidate)

Syntaxe des Quoted Triples

Un quoted triple (triplet cite) est un triplet RDF utilise comme sujet ou objet d’un autre triplet. En Turtle-Star, la syntaxe utilise des chevrons doubles << ... >> :

# Syntaxe RDF-Star / Turtle-Star (conceptuelle)
<< ex:Bob foaf:knows ex:Carol >> ex:assertedBy ex:Alice .
<< ex:Bob foaf:knows ex:Carol >> ex:confidence 0.85 .

Cela remplacerait les 7 triplets de la reification classique par seulement 2 triplets (plus le fait original si on veut l’asserter aussi).

Comparaison visuelle

Aspect Reification classique RDF-Star / RDF 1.2
Syntaxe ex:stmt rdf:subject ex:Bob . (4 triplets) << ex:Bob foaf:knows ex:Carol >> (inline)
Triplets par annotation 4 + N N
Requêtes SPARQL JOIN sur Statement Pattern << ?s ?p ?o >>
Support triple stores Universel Croissant (Jena, Blazegraph, Stardog, Oxigraph)
Support rdflib Natif (toutes versions) Pas encore disponible (7.6.0)

Note pratique : rdflib 7.6.0 ne supporte pas encore la syntaxe Turtle-Star (<< ... >>). Dans les sections suivantes, nous implementons les mêmes concepts via la reification standard, en montrant a chaque fois comment RDF-Star simplifierait l’ecriture.

Deux repères pour lire l’historique ci-dessus. L’article d’Olaf Hartig (2014) part d’un constat d’ingénieur — la réification est correcte mais personne ne l’utilise en production à cause de son coût — et propose la syntaxe << ... >> comme sucre sémantique : le triplet cité devient une valeur manipulable, sans ressource intermédiaire. Les triple stores l’adoptent avant le standard (Blazegraph, puis Jena et Stardog) parce que leurs clients industriels — moteurs de recherche, graphes de connaissances — annotent massivement. Le W3C formalise le mouvement en 2021 (Community Group), puis l’intègre à RDF 1.2 en 2023-2024 : le chemin habituel du Web sémantique, la pratique d’abord, la norme ensuite.

Sur la sémantique, un point que le tableau ne montre pas : RDF 1.2 introduit la distinction annotation et citation. La forme << s p o >> q v (annotation) asserte aussi le triplet interne par défaut ; la forme :m rdf:reifies << s p o >> (citation) ne l’asserte pas — elle en parle seulement. C’est exactement la distinction asserté/mentionné que la section 4.1 construira à la main avec le fait 4. Le standard ne fait pas qu’économiser des triplets : il donne un statut explicite à ce que la réification classique laisse implicite.

2.1 Fonction utilitaire de reification

Pour faciliter la creation de reifications et reduire la verbosite du code, definissons une fonction reify() qui encapsule les 4 triplets necessaires.

En RDF-Star, l’equivalent serait simplement :

<< ex:Bob foaf:knows ex:Carol >> ex:assertedBy ex:Alice .

Le contrat de reify() se lit dans sa signature : (graph, subject, predicate, obj, stmt_uri=None). Elle ajoute les 4 triplets de structure au graphe passé et retourne le nœud Statement — à charge de l’appelant d’y attacher ses annotations avec g.add((stmt, ...)). Le paramètre optionnel stmt_uri (une URIRef) permet de forcer une identité stable au lieu du BNode anonyme par défaut : indispensable dès qu’on veut référencer le Statement depuis l’extérieur du graphe, typiquement pour réifier ce Statement (section 3.2).

Sa raison d’être est purement pédagogique et opérationnelle : écrire 4 graph.add(...) à chaque annotation rend le code illisible et error-prone (un composant inversé entre rdf:subject et rdf:object ne produit aucune erreur — juste un graphe faux). La fonction centralise la mécanique normée ; le notebook garde ses lignes de code pour ce qu’il enseigne — les annotations et les requêtes.

from rdflib import Graph, URIRef, Literal, Namespace, BNode
from rdflib.namespace import RDF, FOAF, XSD

EX = Namespace("http://example.org/")


def reify(graph, subject, predicate, obj, stmt_uri=None):
    """Cree une reification d'un triplet et retourne le noeud Statement.

    En RDF-Star, cela serait simplement << subject predicate obj >>.
    Ici, on cree les 4 triplets rdf:Statement equivalents.

    Args:
        graph: le graphe RDF cible
        subject: sujet du triplet a reifier
        predicate: predicat du triplet a reifier
        obj: objet du triplet a reifier
        stmt_uri: URI optionnelle pour le Statement (BNode par defaut)

    Returns:
        Le noeud (URI ou BNode) representant le rdf:Statement
    """
    stmt = stmt_uri if stmt_uri else BNode()
    graph.add((stmt, RDF.type, RDF.Statement))
    graph.add((stmt, RDF.subject, subject))
    graph.add((stmt, RDF.predicate, predicate))
    graph.add((stmt, RDF.object, obj))
    return stmt


# Demonstration : annoter "Bob connait Carol"
g_demo = Graph()
g_demo.bind("ex", EX)
g_demo.bind("foaf", FOAF)

# Le fait original
g_demo.add((EX.Bob, FOAF.knows, EX.Carol))

# Réification + annotations (equivalent RDF-Star : << ex:Bob foaf:knows ex:Carol >>)
stmt = reify(g_demo, EX.Bob, FOAF.knows, EX.Carol)
g_demo.add((stmt, EX.assertedBy, EX.Alice))
g_demo.add((stmt, EX.confidence, Literal(0.85, datatype=XSD.double)))
g_demo.add((stmt, EX.since, Literal("2020-01-15", datatype=XSD.date)))

print(f"Triplets charges : {len(g_demo)}")
print(f"  - Fait original : 1")
print(f"  - Reification (rdf:Statement) : 4")
print(f"  - Annotations : 3")
print(f"  - Total : {len(g_demo)}")
print()
print("=== Serialisation Turtle ===")
print(g_demo.serialize(format="turtle"))
Triplets charges : 8
  - Fait original : 1
  - Reification (rdf:Statement) : 4
  - Annotations : 3
  - Total : 8

=== Serialisation Turtle ===
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Bob foaf:knows ex:Carol .

[] a rdf:Statement ;
    ex:assertedBy ex:Alice ;
    ex:confidence 8.5e-01 ;
    ex:since "2020-01-15"^^xsd:date ;
    rdf:object ex:Carol ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Bob .

Interpretation

La fonction reify() simplifie l’ecriture, mais le cout en triplets reste le même. Comparons :

Approche Triplets pour “Bob connait Carol” Triplets pour 3 annotations Total
Reification classique 1 + 4 (Statement) 3 8
RDF-Star (si supporte) 1 3 4

RDF-Star simplifierait cette ecriture avec :

ex:Bob foaf:knows ex:Carol .
<< ex:Bob foaf:knows ex:Carol >> ex:assertedBy ex:Alice .
<< ex:Bob foaf:knows ex:Carol >> ex:confidence "0.85"^^xsd:double .
<< ex:Bob foaf:knows ex:Carol >> ex:since "2020-01-15"^^xsd:date .

Note technique : Un quoted triple n’est pas necessairement asserte dans le graphe. << ex:Bob foaf:knows ex:Carol >> ex:assertedBy ex:Alice . ne signifie pas automatiquement que Bob connait Carol – seulement qu’Alice le dit. Pour asserter le fait, il faut aussi ajouter ex:Bob foaf:knows ex:Carol . explicitement.

Lecture des mesures

Le compte passe à 8 triplets — 1 fait + 4 de structure + 3 annotations (assertedBy, confidence, since). La fonction reify() n’a rien changé au contrat RDF : elle a encapsulé l’écriture, pas la structure. Deux observations sur la sortie :

  • Le Statement s’affiche [] : la réification a produit un blank node (nœud anonyme), car aucun paramètre stmt_uri n’a été passé. Turtle le rend comme crochet vide puisque personne ne le référence ailleurs. C’est le bon défaut — un nœud anonyme n’a pas d’identité globale, exactement ce qu’il faut pour une annotation locale à un graphe.
  • Le paramètre stmt_uri de la signature prépare la section 3.2 : pour annoter l’annotation (réifier un Statement), il faut pouvoir le désigner — un BNode fonctionne à l’intérieur du graphe, une URI stable devient nécessaire dès que le Statement est cité depuis un autre graphe ou sérialisé en N-Triples où les identifiants de BNode sont précaires.

La comparaison du tableau se vérifie chiffre à chiffre : réification 8 triplets, RDF-Star 4 (1 fait + 3 annotations — les 4 de structure disparaissent, c’est tout le gain). Notez que le delta mesuré (4) est exactement le coût de structure : la bascule future vers rdflib avec support Turtle-Star ne touchera que le corps de reify(), pas ses appels dans la suite du notebook.


3. Construction programmatique avec reification

Voyons comment construire des annotations sur des triplets de maniere programmatique avec la fonction reify().

3.1 Construction programmatique

Cette section attaque le probleme sous l’angle du code : au lieu d’ecrire les 4 triplets de reification a la main (section 1), on encapsule la mecanique dans une fonction reify() qui rend l’operation reutilisable et sure.

L’interet pedagogique est de separer ce qui est contrat RDF (les 4 triplets sont imposes par la norme, aucun outil ne peut les contourner) de ce qui est ergonomie de programmate (l’API avec laquelle on les produit). Quand RDF-Star sera generalise, seule la deuxieme couche changera — la fonction reify() deviendra un simple QuotedTriple(s, p, o) — et le reste du pipeline (annotations, requetes) restera identique. C’est exactement la comparaison que dresse le tableau de l’interpretation suivante.

La cellule suivante exécute le scénario minimal complet : asserter le fait, le réifier, lui attacher trois annotations (assertedBy, confidence, source). Observez dans la sortie la ligne Type du Statement : BNode — la preuve à l’exécution que le nœud produit est anonyme, et la sérialisation Turtle qui rend ce BNode sous la forme compacte [] puisque rien d’autre ne le référence.

Le même scénario reviendra à l’identique dans les cas d’usage (sections 5.1 à 5.3) : seul le vocabulaire d’annotation change — provenance PROV pour l’un, dates de validité pour l’autre, confiance multi-sources pour le troisième. La mécanique reify() + add() est le motif invariant de tout le notebook ; c’est lui qu’il faut savoir écrire de mémoire.

from rdflib import Graph, URIRef, Literal, Namespace, BNode
from rdflib.namespace import RDF, FOAF, XSD

EX = Namespace("http://example.org/")

g_prog = Graph()
g_prog.bind("ex", EX)
g_prog.bind("foaf", FOAF)

# Asserter le fait original
g_prog.add((EX.Bob, FOAF.knows, EX.Carol))

# Reifier et annoter
# En RDF-Star, cela serait : << ex:Bob foaf:knows ex:Carol >> ex:assertedBy ex:Alice .
stmt = reify(g_prog, EX.Bob, FOAF.knows, EX.Carol)
g_prog.add((stmt, EX.assertedBy, EX.Alice))
g_prog.add((stmt, EX.confidence, Literal(0.85, datatype=XSD.double)))
g_prog.add((stmt, EX.source, Literal("Interview 2024-03-15")))

print(f"Graphe construit : {len(g_prog)} triplets")
print(f"  Type du Statement : {type(stmt).__name__}")
print()
print("=== Serialisation Turtle ===")
print(g_prog.serialize(format="turtle"))
Graphe construit : 8 triplets
  Type du Statement : BNode

=== Serialisation Turtle ===
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Bob foaf:knows ex:Carol .

[] a rdf:Statement ;
    ex:assertedBy ex:Alice ;
    ex:confidence 8.5e-01 ;
    ex:source "Interview 2024-03-15" ;
    rdf:object ex:Carol ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Bob .

Interpretation

La construction programmatique utilise reify() pour encapsuler les 4 triplets de reification :

Opération Reification (actuel) RDF-Star (futur)
Créer une reference au triplet reify(g, s, p, o) -> BNode QuotedTriple(s, p, o)
Annoter g.add((stmt, pred, obj)) g.add((qt, pred, obj))
Serialisation Turtle standard Turtle-Star (<< ... >>)

Lecture des mesures

La sortie affiche Type du Statement : BNode — la preuve d’exécution que la fonction produit bien un nœud anonyme par défaut. Le graphe mesure 8 triplets, même topologie qu’à la section 2 : l’ergonomie a changé (une ligne reify(...) au lieu de quatre graph.add(...)), la facture RDF est identique.

Le docstring de reify() fait un travail double : il documente le comportement actuel et la cible RDF-Star (<< subject predicate obj >>). C’est une bonne pratique de migration — quand rdflib publiera le support natif, le docstring devient la spécification de l’implémentation de remplacement, et les assertions du notebook restent vraies.

Pour bien séparer les couches : les 4 triplets sont un contrat de norme (aucun outil RDF ne peut produire une réification conforme avec moins), la signature reify(graph, subject, predicate, obj) est une API d’ergonomie (elle aurait pu s’appeler autrement, prendre un dict, retourner un tuple). Les notebooks suivants (SW-12 GraphRAG en particulier) réutilisent ce motif : encapsuler le verbeux standardisé derrière une fonction courte, pour que le code pédagogique montre la sémantique plutôt que la plomberie.

Avantage de la fonction reify() : elle centralise la creation des 4 triplets de reification, rendant le code plus lisible et moins sujet aux erreurs. Quand rdflib supportera RDF-Star nativement, il suffira de remplacer l’appel a reify() par la creation d’un QuotedTriple.

3.2 Annotations imbriquees (multi-niveaux)

Les annotations de la section precedente portaient sur des faits ; ici ce sont les annotations elles-memes qui sont annotees. Le cas d’usage : « Bob croit que (Carol connait Dave) », puis « Alice rapporte que (Bob croit que…) ». Chaque niveau d’imbrication ajoute un contexte epistemique — qui sait, qui croit, qui rapporte — sans toucher au fait de base.

Ce motif est la reponse RDF a un probleme reel des graphes de connaissances : une information n’a pas seulement une valeur, elle a une chaine de garde. En intelligence artificielle (agents qui citent d’autres agents, journalisme collaboratif, ouï-dire dans un reseau social), savoir qui affirme est aussi important que ce qui est affirme. La cellule suivante construit deux niveaux d’imbrication avec la reification classique — comptez les triplets au passage, l’explosion combinatoire est le prix du sans-RDF-Star.

Pourquoi s’embarrasser de croyances de croyances ? Parce que dans un graphe qui agrège des affirmations, l’information sans chaîne de garde devient indistinguable du fait brut. Trois contextes où l’imbrication n’est pas un cas d’école : les agents IA qui citent d’autres agents (une synthèse doit porter la trace de ses sources, elles-mêmes des affirmations) ; le journalisme collaboratif (un rédacteur rapporte ce qu’un témoin affirme) ; la modération de réseaux (une plateforme qualifie une rumeur rapportée par un utilisateur, sans pour autant l’asserter).

Dans les trois cas, chaque étage d’imbrication ajoute un verbe épistémique — être vrai, être cru, être rapporté — sans toucher au fait de base, qui reste disponible pour qui ne veut que lui. La cellule suivante construit deux étages au-dessus de « Carol connaît Dave » : d’abord la croyance de Bob, puis le rapport d’Alice sur cette croyance. Suivez le compte de triplets au passage : l’explosion est le prix du sans-RDF-Star.

from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import RDF, FOAF, XSD

EX = Namespace("http://example.org/")

g_nested = Graph()
g_nested.bind("ex", EX)
g_nested.bind("foaf", FOAF)

# Niveau 1 : Carol connait Dave (fait de base)
g_nested.add((EX.Carol, FOAF.knows, EX.Dave))

# Niveau 2 : Bob croit ce fait
# En RDF-Star : << ex:Carol foaf:knows ex:Dave >> ex:believedBy ex:Bob .
stmt_level1 = reify(g_nested, EX.Carol, FOAF.knows, EX.Dave)
g_nested.add((stmt_level1, EX.believedBy, EX.Bob))

# Niveau 3 : Alice rapporte la croyance de Bob
# En RDF-Star : << << ... >> ex:believedBy ex:Bob >> ex:reportedBy ex:Alice .
# Avec la réification, on reifie le Statement de niveau 2
stmt_level2 = reify(g_nested, stmt_level1, EX.believedBy, EX.Bob)
g_nested.add((stmt_level2, EX.reportedBy, EX.Alice))
g_nested.add((stmt_level2, EX.reportDate, Literal("2024-06-01", datatype=XSD.date)))

print(f"Triplets charges : {len(g_nested)}")
print(f"  - Fait de base : 1")
print(f"  - Reification niveau 1 : 4 + 1 annotation")
print(f"  - Reification niveau 2 : 4 + 2 annotations")
print(f"  - Total : {len(g_nested)}")
print()
print("=== Serialisation Turtle ===")
print(g_nested.serialize(format="turtle"))
Triplets charges : 12
  - Fait de base : 1
  - Reification niveau 1 : 4 + 1 annotation
  - Reification niveau 2 : 4 + 2 annotations
  - Total : 12

=== Serialisation Turtle ===
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Carol foaf:knows ex:Dave .

[] a rdf:Statement ;
    ex:reportDate "2024-06-01"^^xsd:date ;
    ex:reportedBy ex:Alice ;
    rdf:object ex:Bob ;
    rdf:predicate ex:believedBy ;
    rdf:subject [ a rdf:Statement ;
            ex:believedBy ex:Bob ;
            rdf:object ex:Dave ;
            rdf:predicate foaf:knows ;
            rdf:subject ex:Carol ] .

Interpretation : imbrication

Chaque niveau d’imbrication ajoute du contexte sans modifier le fait de base :

Niveau Reification RDF-Star equivalent Signification
1 Carol foaf:knows Dave Carol foaf:knows Dave Fait asserte
2 stmt1 believedBy Bob << Carol knows Dave >> believedBy Bob Bob croit ce fait
3 stmt2 reportedBy Alice << << ... >> believedBy Bob >> reportedBy Alice Alice rapporte la croyance

Le cout en triplets est eleve avec la reification (12 triplets) contre seulement 4 avec RDF-Star.

Lecture des mesures

Le compte mesuré : 12 triplets — 1 fait de base + (4 + 1) pour le niveau 2 + (4 + 2) pour le niveau 3. La sérialisation Turtle montre la cascade de blank nodes : le rdf:subject du Statement extérieur est le Statement intérieur, rendu inline entre crochets [ a rdf:Statement ; ... ]. Turtle choisit cette forme imbriquée parce que le BNode n’est référencé qu’une fois — c’est déjà la représentation la plus compacte possible, et elle reste difficile à lire : trois niveaux de contexte (qui connaît, qui croit, qui rapporte) sur deux phrases.

Deux enseignements chiffrés :

  • Le coût par niveau est constant (4 triplets), la lisibilité ne l’est pas : chaque palier d’imbrication imbrique un bloc de plus dans la sérialisation. À 3 niveaux, la Turtle devient plus longue que la phrase française équivalente.
  • La requête SPARQL suit la même pente : retrouver « qui rapporte une croyance portant sur Carol » exige de déplier deux Statement imbriqués — un motif à 10+ triplets. C’est l’avertissement pratique du blockquote ci-dessous : au-delà de 2 niveaux, on factorise (propriété dérivée ex:reportedChain, ou graphe nommé par niveau).

L’équivalent RDF-Star du tableau (<< << Carol knows Dave >> believedBy Bob >> reportedBy Alice) tient en 2 triplets : l’imbrication devient un fait de syntaxe, lisible par humain et par parseur. C’est sur les croyances imbriquées — le cas d’usage épistémique — que l’écart entre les deux mécanismes est le plus spectaculaire : 12 contre 4, et surtout une cascade de BNode contre une ligne.

Attention : L’imbrication profonde (>2 niveaux) rend les requêtes SPARQL complexes, que ce soit en reification ou en RDF-Star. En pratique, un seul niveau d’imbrication couvre la majorite des cas d’usage.


4. Interroger les annotations avec SPARQL

Le pendant de SPARQL-Star pour la reification classique utilise des patterns de jointure sur les proprietes rdf:subject, rdf:predicate et rdf:object.

Comparaison des syntaxes

Pattern SPARQL-Star (conceptuel) SPARQL avec reification
Trouver les annotations << ?s ?p ?o >> ?annPred ?annObj . ?stmt rdf:subject ?s ; rdf:predicate ?p ; rdf:object ?o ; ?annPred ?annObj .
Triplet spécifique << ex:Bob foaf:knows ?who >> ?p ?o . ?stmt rdf:subject ex:Bob ; rdf:predicate foaf:knows ; rdf:object ?who .
Annotation spécifique << ?s ?p ?o >> ex:confidence ?c . ?stmt rdf:subject ?s ; rdf:predicate ?p ; rdf:object ?o ; ex:confidence ?c .

La table ci-dessus se lit ligne par ligne comme un dictionnaire de traduction : à gauche la requête SPARQL-Star qu’on veudrait écrire, à droite celle qu’on doit écrire aujourd’hui avec rdflib. La traduction est mécanique — chaque << ?s ?p ?o >> du motif devient une jointure sur les trois prédicats rdf:subject, rdf:predicate, rdf:object d’un ?stmt — mais sa longueur explique à elle seule une part du coût cognitif de la réification : quatre motifs de graphe là où un seul suffirait en SPARQL-Star.

Un point d’attention pour les trois sections qui suivent : la jointure doit être complète. Oublier le motif rdf:predicate ?p dans une requête ramène des Statements partiels qui matchent par accident (deux faits différents peuvent partager sujet et objet). Les requêtes de ce notebook déplient toujours les trois composants — prenez-en l’habitude, c’est la seule garantie de retrouver le triplet décrit.

4.1 Construire un graphe de connaissances annote

Cette section assemble les briques des sections 1-3 dans un cas d’usage complet : un mini reseau social ou chaque relation porte sa confiance, sa source et sa date. C’est le format que produisent reellement les pipelines d’extraction d’information (le notebook SW-12 GraphRAG construit exactement ce genre de graphe a partir d’un LLM).

Quatre relations y sont encodees, dont une (Alice connait Dave, confiance 0.50) qui n’est pas assertee — elle n’existe que comme mention annotée. Cette distinction asserte/mentionnee est la motivation profonde de toute la machinerie de reification : un moteur d’inference ne doit pas traiter une rumeur comme un fait. Les requetes SPARQL des cellules 4.2 et 4.3 exploiteront precisement ces annotations pour trier le grain de l’ivraie.

La construction de la cellule suivante applique le motif de la section 3 à un réseau social complet : quatre personnes typées foaf:Person, quatre relations foaf:knows — chacune réifiée et annotée de confiance, source, date. Le résultat, 37 triplets, est le plus gros graphe du notebook ; sa sérialisation Turtle mérite une lecture attentive car elle matérialise visuellement la distinction asserté/mentionné.

Le chiffre à garder en tête avant d’exécuter : parmi les quatre relations, une seule n’est pas assertée (fait 4 : Alice connaît Dave, confiance 0.50, affirmée par Bob). Les requêtes des sections 4.2 et 4.3 interrogeront les annotations sans distinction — et vous verrez néanmoins la différence émerger dans le graphe lui-même : le triplet simple correspondant au fait 4 n’existe pas. C’est cette asymétrie, invisible dans un Graph plat, qui fait toute la valeur du graphe annoté.

from rdflib import Graph, Namespace, Literal, URIRef, BNode
from rdflib.namespace import RDF, FOAF, XSD

EX = Namespace("http://example.org/")

g_kg = Graph()
g_kg.bind("ex", EX)
g_kg.bind("foaf", FOAF)

# === Personnes ===
for name, uri in [("Alice Dupont", EX.Alice), ("Bob Martin", EX.Bob),
                   ("Carol Laurent", EX.Carol), ("Dave Petit", EX.Dave)]:
    g_kg.add((uri, RDF.type, FOAF.Person))
    g_kg.add((uri, FOAF.name, Literal(name)))

# === Fait 1 : Alice connait Bob (haute confiance, source LinkedIn) ===
# RDF-Star : << ex:Alice foaf:knows ex:Bob >> ex:confidence "0.95"^^xsd:double .
g_kg.add((EX.Alice, FOAF.knows, EX.Bob))
stmt1 = reify(g_kg, EX.Alice, FOAF.knows, EX.Bob)
g_kg.add((stmt1, EX.confidence, Literal(0.95, datatype=XSD.double)))
g_kg.add((stmt1, EX.source, Literal("LinkedIn")))
g_kg.add((stmt1, EX.since, Literal("2019-03-01", datatype=XSD.date)))

# === Fait 2 : Bob connait Carol (confiance moyenne, source Twitter) ===
g_kg.add((EX.Bob, FOAF.knows, EX.Carol))
stmt2 = reify(g_kg, EX.Bob, FOAF.knows, EX.Carol)
g_kg.add((stmt2, EX.confidence, Literal(0.60, datatype=XSD.double)))
g_kg.add((stmt2, EX.source, Literal("Twitter")))
g_kg.add((stmt2, EX.since, Literal("2021-07-15", datatype=XSD.date)))

# === Fait 3 : Carol connait Dave (basse confiance, source rumeur) ===
g_kg.add((EX.Carol, FOAF.knows, EX.Dave))
stmt3 = reify(g_kg, EX.Carol, FOAF.knows, EX.Dave)
g_kg.add((stmt3, EX.confidence, Literal(0.30, datatype=XSD.double)))
g_kg.add((stmt3, EX.source, Literal("Rumeur")))

# === Fait 4 : Alice connait Dave (NON asserte, seulement affirme par Bob) ===
# On ne fait PAS g_kg.add((EX.Alice, FOAF.knows, EX.Dave))
stmt4 = reify(g_kg, EX.Alice, FOAF.knows, EX.Dave)
g_kg.add((stmt4, EX.assertedBy, EX.Bob))
g_kg.add((stmt4, EX.confidence, Literal(0.50, datatype=XSD.double)))

print(f"Graphe de connaissances : {len(g_kg)} triplets")
print()
print(g_kg.serialize(format="turtle"))
Graphe de connaissances : 37 triplets

@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Alice a foaf:Person ;
    foaf:knows ex:Bob ;
    foaf:name "Alice Dupont" .

ex:Carol a foaf:Person ;
    foaf:knows ex:Dave ;
    foaf:name "Carol Laurent" .

ex:Dave a foaf:Person ;
    foaf:name "Dave Petit" .

ex:Bob a foaf:Person ;
    foaf:knows ex:Carol ;
    foaf:name "Bob Martin" .

[] a rdf:Statement ;
    ex:confidence 3e-01 ;
    ex:source "Rumeur" ;
    rdf:object ex:Dave ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Carol .

[] a rdf:Statement ;
    ex:assertedBy ex:Bob ;
    ex:confidence 5e-01 ;
    rdf:object ex:Dave ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Alice .

[] a rdf:Statement ;
    ex:confidence 9.5e-01 ;
    ex:since "2019-03-01"^^xsd:date ;
    ex:source "LinkedIn" ;
    rdf:object ex:Bob ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Alice .

[] a rdf:Statement ;
    ex:confidence 6e-01 ;
    ex:since "2021-07-15"^^xsd:date ;
    ex:source "Twitter" ;
    rdf:object ex:Carol ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Bob .

Interpretation : graphe de connaissances annote

Ce graphe illustre un reseau social avec des annotations de qualite :

Relation Confiance Source Depuis Asserte ?
Alice connait Bob 0.95 LinkedIn 2019-03-01 Oui
Bob connait Carol 0.60 Twitter 2021-07-15 Oui
Carol connait Dave 0.30 Rumeur - Oui
Alice connait Dave 0.50 - - Non (seulement affirme par Bob)

Lecture des mesures — décomposition des 37 triplets

Le compte se vérifie ligne par ligne dans la sortie : 8 triplets d’identité (4 personnes × rdf:type + foaf:name), puis fait 1 : 8 (1 assertion + 4 structure + 3 annotations), fait 2 : 8 (idem), fait 3 : 7 (pas de since), fait 4 : 6 (aucune assertion — 4 structure + 2 annotations). Total : 8 + 8 + 8 + 7 + 6 = 37, à l’identique de ce que len(g_kg) affiche.

L’observation la plus importante du notebook est dans cette sérialisation : parcourez la sortie et cherchez ex:Alice foaf:knows ex:Dave — il n’apparaît nulle part comme triplet. Le fait 4 n’existe que dans les composants du rdf:Statement (lignes rdf:subject ex:Alice ; ... rdf:object ex:Dave du dernier bloc []). Concrètement :

  • un parcours for s, p, o in g_kg sur les foaf:knows ne retourne que 3 relations ;
  • seule une requête qui déplie les Statements « voit » la 4e ;
  • donc assertion et mention ont des audiences différentes dans le même graphe — un moteur d’inférence classique raisonne sur les assertions, un module de vérification raisonne sur les mentions.

C’est la frontière exacte que les pipelines d’extraction d’information doivent gérer : quand un LLM affirme des relations à partir de textes (le notebook SW-12 construit ce genre de graphe), chaque relation extraite est d’abord une mention — la faire passer en assertion exige une politique de confiance, celle que la section 4.3 met en œuvre avec un simple FILTER.

Point cle : Le fait 4 n’est pas asserte dans le graphe (ex:Alice foaf:knows ex:Dave . n’apparait pas). Seul le rdf:Statement et ses annotations existent. Cela signifie que le graphe ne dit pas qu’Alice connait Dave – il dit seulement que Bob le pretend.

En RDF-Star, la même distinction existe : << ex:Alice foaf:knows ex:Dave >> ex:assertedBy ex:Bob . n’asserte pas le fait non plus.

4.2 Requêtes SPARQL sur les reifications

Annoter ne sert a rien si l’on ne peut pas interroger les annotations. Cette cellule montre la premiere requete : retrouver toutes les relations annotees avec leur niveau de confiance.

Le pattern SPARQL a observer : on ne cherche plus des triplets ?s ?p ?o directement, mais des ?stmt rdf:type rdf:Statement dont on deplie ensuite les trois composants rdf:subject, rdf:predicate, rdf:object plus les annotations. La requete est plus verbeuse qu’un motif simple — c’est le cout en lecture de ce que la reification a ajoute en ecriture — mais elle donne acces a une dimension qu’un graphe plat n’a pas : les metadonnees deviennent des donnees de premiere classe, filtrables, aggregables, joignables.

La requête de la cellule suivante introduit deux éléments SPARQL au-delà du motif de réification : les clauses OPTIONAL { ?stmt ex:source ?source . } — qui ramènent l’annotation si elle existe sans exclure le Statement quand elle manque (le fait 3 n’a pas de date, le fait 4 n’a ni source ni date) — et le tri ORDER BY DESC(?confidence) qui ordonne les résultats par fiabilité décroissante : la relation la plus solide en tête, la rumeur en queue.

La colonne Depuis (ex:since) illustre au passage une pratique saine d’annotation : ne stocker une métadonnée que lorsqu’elle a un sens. Les trois faits sourcés portent leur date ; la rumeur n’en a pas — l’absence, rendue visible par le tiret - de l’affichage, est de l’information en soi.

# Requête 1 : Toutes les relations annotées avec confiance
# En SPARQL-Star, on ecrirait : << ?person1 foaf:knows ?person2 >> ex:confidence ?confidence .
# Avec la réification, on joint sur rdf:subject, rdf:predicate, rdf:object

query_all = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

SELECT ?person1 ?person2 ?confidence ?source ?since
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:confidence ?confidence .
    OPTIONAL { ?stmt ex:source ?source . }
    OPTIONAL { ?stmt ex:since ?since . }
}
ORDER BY DESC(?confidence)
"""

results = g_kg.query(query_all)

print("=== Relations annotees (triees par confiance) ===")
print(f"{'Personne 1':<15} {'Personne 2':<15} {'Confiance':>10} {'Source':<12} {'Depuis'}")
print("-" * 70)
for row in results:
    p1 = str(row.person1).split("/")[-1]
    p2 = str(row.person2).split("/")[-1]
    conf = f"{float(row.confidence):.2f}"
    src = str(row.source) if row.source else "-"
    since = str(row.since) if row.since else "-"
    print(f"{p1:<15} {p2:<15} {conf:>10} {src:<12} {since}")
=== Relations annotees (triees par confiance) ===
Personne 1      Personne 2       Confiance Source       Depuis
----------------------------------------------------------------------
Alice           Bob                   0.95 LinkedIn     2019-03-01
Bob             Carol                 0.60 Twitter      2021-07-15
Alice           Dave                  0.50 -            -
Carol           Dave                  0.30 Rumeur       -

4.3 Filtrer par score de confiance

Un cas d’usage important : ne retenir que les relations ayant un score de confiance superieur a un seuil. Cela permet de construire une vue “fiable” du graphe.

Les deux requêtes de la cellule suivante sont des miroirs : même motif de réification, même jointure sur les annotations, seule la clause FILTER change — >= "0.60"^^xsd:double contre < "0.60"^^xsd:double. Notez la forme du littéral : le seuil est écrit avec son datatype (xsd:double) pour que la comparaison typée fonctionne, exactement comme les valeurs de confiance stockées dans le graphe.

Le résultat produit deux vues complémentaires du même graphe : la vue fiable (pour raisonner, recommander) et la vue à vérifier (pour auditer, arbitrer). Aucune des deux n’est « la bonne » — elles servent des consommateurs différents du même stock d’assertions, ce que l’interprétation qui suit détaille seuil par seuil.

# Requête : Relations a haute confiance (>= 0.60)
# En SPARQL-Star : << ?p1 foaf:knows ?p2 >> ex:confidence ?c . FILTER(?c >= 0.60)

query_high = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

SELECT ?person1 ?person2 ?confidence ?source
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:confidence ?confidence .
    FILTER (?confidence >= "0.60"^^xsd:double)
    OPTIONAL { ?stmt ex:source ?source . }
}
ORDER BY DESC(?confidence)
"""

# Requête : Relations a basse confiance (< 0.60)
query_low = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

SELECT ?person1 ?person2 ?confidence ?source
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:confidence ?confidence .
    FILTER (?confidence < "0.60"^^xsd:double)
    OPTIONAL { ?stmt ex:source ?source . }
}
ORDER BY ?confidence
"""

print("=== Relations FIABLES (confiance >= 0.60) ===")
for row in g_kg.query(query_high):
    p1 = str(row.person1).split("/")[-1]
    p2 = str(row.person2).split("/")[-1]
    src = str(row.source) if row.source else "-"
    print(f"  {p1} connait {p2} (confiance={float(row.confidence):.2f}, source={src})")

print()
print("=== Relations INCERTAINES (confiance < 0.60) ===")
for row in g_kg.query(query_low):
    p1 = str(row.person1).split("/")[-1]
    p2 = str(row.person2).split("/")[-1]
    src = str(row.source) if row.source else "-"
    print(f"  {p1} connait {p2} (confiance={float(row.confidence):.2f}, source={src})")
=== Relations FIABLES (confiance >= 0.60) ===
  Alice connait Bob (confiance=0.95, source=LinkedIn)
  Bob connait Carol (confiance=0.60, source=Twitter)

=== Relations INCERTAINES (confiance < 0.60) ===
  Carol connait Dave (confiance=0.30, source=Rumeur)
  Alice connait Dave (confiance=0.50, source=-)

Interpretation : filtrage par confiance

Le filtrage separe les faits fiables des faits incertains :

Catégorie Relations Usage recommande
Haute confiance (>= 0.60) Alice-Bob, Bob-Carol Utiliser pour le raisonnement
Basse confiance (< 0.60) Carol-Dave, Alice-Dave A verifier, ne pas propager

Ce type de filtrage est essentiel dans les graphes de connaissances : on peut construire une vue “fiable” du graphe en excluant les assertions douteuses.

Lecture des mesures

Le partage mesuré est net — 2 relations fiables (Alice-Bob 0.95, Bob-Carol 0.60), 2 incertaines (Carol-Dave 0.30, Alice-Dave 0.50) — mais il ne faut pas le lire comme une propriété du graphe : le seuil 0.60 est un paramètre du consommateur, pas une donnée. Trois seuils, trois vues :

  • à 0.70 (le seuil de recommandation du blockquote), Bob-Carol bascule côté incertain : la vue fiable ne garde qu’Alice-Bob ;
  • à 0.60, la coupure du tableau ci-dessus ;
  • à 0.40, Carol-Dave revient : il ne reste qu’Alice-Dave, la rumeur non sourcée.

Le choix du seuil est un compromis précision/rappel classique : élevé, la vue est conservative (peu de relations, toutes sourcées) ; bas, elle est exhaustive (tout, y compris les ouï-dire). Ce qui rend le compromis pilotable en RDF, c’est que la confiance est une donnée de première classe — la même requête avec un autre littéral dans le FILTER produit l’autre vue, sans reconstruire le graphe.

Notez enfin ce que les tirets des colonnes source/since révèlent : le fait 4 n’a ni source ni date (OPTIONAL non lié), seulement un assertedBy Bob. L’absence d’annotation est elle-même de l’information : « quelqu’un l’affirme, rien de plus » — exactement le profil d’une rumeur, formalisé.

Application concrete : Dans un système de recommandation, on ne recommanderait un contact qu’a partir de relations de confiance >= 0.70.


5. Cas d’usage concrets

Explorons trois cas d’usage representatifs pour l’annotation de triplets.

Les trois cas d’usage choisis balayent les trois axes d’annotation du tableau de la section 1, du plus simple au plus composite : la provenance (qui est la source, avec le vocabulaire standard PROV du W3C), la temporalité (quand le fait est vrai — un historique de carrière complet), et la fusion multi-sources (que faire de trois valeurs contradictoires — le cas le plus riche, qui combine confiance, source et millésime dans la même annotation).

Chaque cas suit le même déroulé : construire un petit graphe réaliste avec reify(), l’interroger en SPARQL standard, puis lire l’interprétation qui suit la sortie. Le motif technique ne change jamais — c’est volontaire : la variété des cas doit faire apparaître ce qui est invariant dans la mécanique, et ce qui varie n’est que le vocabulaire d’annotation.

5.1 Provenance et attribution

Apres la confiance (qualitative), la provenance (d’ou vient le fait ?). Le cas d’usage : deux attributs de Paris — population et superficie — extraits de sources officielles differentes (INSEE pour le recensement, IGN pour la mesure cadastrale), chacune avec sa date et sa methode.

Ce pattern prov:wasDerivedFrom est le socle du W3C PROV, le vocabulaire standard de traçage de provenance. Dans un contexte ou plusieurs sources affirment des valeurs contradictoires pour un meme attribut (le notebook 5.3 y vient), la provenance permet de trancher par autorite plutot que par hasard d’ordre d’insertion. L’interpretation suivante montre la version RDF-Star cote a cote : 4 triplets economises par fait annote, la provenance redevient une annotation d’une ligne.

Le vocabulaire utilisé ici n’est pas inventé : prov:wasDerivedFrom et prov:generatedAtTime viennent du W3C PROV, la recommandation standard de traçage de provenance (PROV-DM, 2013), utilisée telle quelle par les plateformes de données ouvertes. La réification rdf:Statement et PROV se combinent sans friction : le Statement est le support, les prédicats PROV sont les annotations — on retrouve la séparation mécanique / vocabulaire introduite par reify() en section 3.

Le choix des sources dans la cellule suivante est délibéré : l’INSEE pour la population (le recensement est sa compétence), l’IGN pour la superficie (le cadastre est la sienne). Ce ne sont pas deux sources interchangeables mais deux autorités de domaine — la provenance encode cette hiérarchie implicite, que l’interprétation relira à la lumière de la sortie.

from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import RDF, RDFS, XSD

EX = Namespace("http://example.org/")
PROV = Namespace("http://www.w3.org/ns/prov#")
SDO = Namespace("https://schema.org/")

g_prov = Graph()
g_prov.bind("ex", EX)
g_prov.bind("prov", PROV)
g_prov.bind("schema", SDO)

# Fait 1 : Paris a une population de 2,1 millions
pop_value = Literal(2161000, datatype=XSD.integer)
g_prov.add((EX.Paris, SDO.population, pop_value))

# Provenance du fait (réification)
stmt_pop = reify(g_prov, EX.Paris, SDO.population, pop_value)
g_prov.add((stmt_pop, PROV.wasDerivedFrom, EX.INSEE_2023))
g_prov.add((stmt_pop, PROV.generatedAtTime, Literal("2023-12-01", datatype=XSD.date)))
g_prov.add((stmt_pop, EX.method, Literal("Recensement")))
g_prov.add((stmt_pop, EX.accuracy, Literal("haute")))

# Fait 2 : Paris a une superficie de 105.4 km2
area_value = Literal(105.4, datatype=XSD.double)
g_prov.add((EX.Paris, SDO.area, area_value))

stmt_area = reify(g_prov, EX.Paris, SDO.area, area_value)
g_prov.add((stmt_area, PROV.wasDerivedFrom, EX.IGN_2023))
g_prov.add((stmt_area, PROV.generatedAtTime, Literal("2023-06-15", datatype=XSD.date)))
g_prov.add((stmt_area, EX.method, Literal("Mesure cadastrale")))

print(f"Graphe de provenance : {len(g_prov)} triplets")
print()

# Requête : quels faits, avec quelle source ?
query_prov = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX schema: <https://schema.org/>

SELECT ?subject ?property ?value ?source ?date ?method
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?subject ;
          rdf:predicate ?property ;
          rdf:object ?value ;
          prov:wasDerivedFrom ?source .
    OPTIONAL { ?stmt prov:generatedAtTime ?date . }
    OPTIONAL { ?stmt ex:method ?method . }
}
"""

print("=== Faits avec provenance ===")
print(f"{'Sujet':<10} {'Propriete':<20} {'Valeur':<12} {'Source':<15} {'Date':<12} {'Methode'}")
print("-" * 85)
for row in g_prov.query(query_prov):
    subj = str(row.subject).split("/")[-1]
    prop = str(row.property).split("/")[-1]
    src = str(row.source).split("/")[-1]
    date = str(row.date) if row.date else "-"
    method = str(row.method) if row.method else "-"
    print(f"{subj:<10} {prop:<20} {str(row.value):<12} {src:<15} {date:<12} {method}")
Graphe de provenance : 17 triplets

=== Faits avec provenance ===
Sujet      Propriete            Valeur       Source          Date         Methode
-------------------------------------------------------------------------------------
Paris      population           2161000      INSEE_2023      2023-12-01   Recensement
Paris      area                 105.4        IGN_2023        2023-06-15   Mesure cadastrale

Interpretation : provenance

Fait Source Date Méthode
Population de Paris = 2,161,000 INSEE 2023 2023-12-01 Recensement
Superficie de Paris = 105.4 km2 IGN 2023 2023-06-15 Mesure cadastrale

RDF-Star simplifierait cette ecriture en eliminant les 4 triplets de reification par fait :

<< ex:Paris schema:population "2161000"^^xsd:integer >>
    prov:wasDerivedFrom ex:INSEE_2023 ;
    prov:generatedAtTime "2023-12-01"^^xsd:date ;
    ex:method "Recensement" ;
    ex:accuracy "haute" .

En reification classique, il faut 2 x 4 = 8 triplets supplementaires pour les Statements seuls.

Lecture des mesures

Les 17 triplets du graphe se décomposent en 2 faits + 8 de structure (2 × 4) + 7 annotations (4 pour la population : wasDerivedFrom, generatedAtTime, method, accuracy ; 3 pour la superficie). La structure pèse encore près de la moitié du graphe — le thème de coût des sections précédentes ne varie pas.

Le point conceptuel fort de cette sortie est l’autorité de compétence : la population vient de l’INSEE (recensement, publié 2023-12-01), la superficie de l’IGN (mesure cadastrale, 2023-06-15). Chaque fait porte sa source légitime — pas une source unique pour tout le graphe. C’est la thèse du W3C PROV : la provenance est une propriété par assertion, pas par dépôt. Et parce que prov:wasDerivedFrom pointe une URI (ex:INSEE_2023), cette source peut à son tour être décrite ailleurs (date de publication, méthodologie, licence) : la chaîne de provenance s’étend sans jamais toucher aux faits qu’elle documente.

La requête, elle, généralise le motif de la section 4 : jointure sur les trois composants du Statement, puis OPTIONAL sur date et méthode. Les deux lignes du tableau résultat sont exactement les deux faits — la vue « tableau de provenance » qu’un auditeur de données demanderait, produite par une seule requête SPARQL standard.

5.2 Annotations temporelles

La reification est utile pour exprimer la validite temporelle des faits : un emploi, un statut, une relation qui change au fil du temps.

En RDF-Star, cela s’ecrirait :

<< ex:Marie schema:worksFor ex:Sorbonne >> ex:startDate "2020-09-01"^^xsd:date .
<< ex:Marie schema:worksFor ex:Sorbonne >> ex:rôle "Professeure" .

Le cas d’usage de la cellule suivante est l’un des plus fréquents en graphe de connaissances réel : représenter ce qui change — un emploi, une adresse, un statut — sans perdre l’historique. La solution RDF : ne jamais mettre à jour les faits, les dater. Chaque période d’emploi devient une assertion annotée de ex:startDate, ex:endDate (facultative), ex:role, ex:status ; le présent n’est qu’une période dont la fin n’est pas encore connue.

Deux emplois sur les trois de l’exemple ne sont d’ailleurs pas assertés comme triplets simples : seules les versions terminées vivent exclusivement dans leurs Statements. Le triplet Marie worksFor Sorbonne, lui, est asserté — c’est l’état courant. Cette asymétrie encode une politique : le présent s’affirme, le passé se mentionne. Une requête sur les triplets simples donne l’employeur actuel ; une requête sur les Statements donne la carrière complète — deux questions, deux motifs, un seul graphe.

from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import RDF, XSD

EX = Namespace("http://example.org/")
SDO = Namespace("https://schema.org/")

g_temporal = Graph()
g_temporal.bind("ex", EX)
g_temporal.bind("schema", SDO)

# Personnes et organisations
for name, uri, rdf_type in [
    ("Marie", EX.Marie, SDO.Person),
    ("Sorbonne", EX.Sorbonne, SDO.Organization),
    ("CNRS", EX.CNRS, SDO.Organization),
    ("MIT", EX.MIT, SDO.Organization),
]:
    g_temporal.add((uri, RDF.type, rdf_type))
    g_temporal.add((uri, SDO.name, Literal(name)))

# Emploi actuel : Marie -> Sorbonne (asserte)
g_temporal.add((EX.Marie, SDO.worksFor, EX.Sorbonne))
stmt_sorb = reify(g_temporal, EX.Marie, SDO.worksFor, EX.Sorbonne)
g_temporal.add((stmt_sorb, EX.startDate, Literal("2020-09-01", datatype=XSD.date)))
g_temporal.add((stmt_sorb, EX.role, Literal("Professeure")))
g_temporal.add((stmt_sorb, EX.status, Literal("actif")))

# Emploi precedent 1 : Marie -> CNRS (non asserte, termine)
stmt_cnrs = reify(g_temporal, EX.Marie, SDO.worksFor, EX.CNRS)
g_temporal.add((stmt_cnrs, EX.startDate, Literal("2015-01-15", datatype=XSD.date)))
g_temporal.add((stmt_cnrs, EX.endDate, Literal("2020-08-31", datatype=XSD.date)))
g_temporal.add((stmt_cnrs, EX.role, Literal("Chercheuse")))
g_temporal.add((stmt_cnrs, EX.status, Literal("termine")))

# Emploi precedent 2 : Marie -> MIT (non asserte, termine)
stmt_mit = reify(g_temporal, EX.Marie, SDO.worksFor, EX.MIT)
g_temporal.add((stmt_mit, EX.startDate, Literal("2012-09-01", datatype=XSD.date)))
g_temporal.add((stmt_mit, EX.endDate, Literal("2014-12-31", datatype=XSD.date)))
g_temporal.add((stmt_mit, EX.role, Literal("Post-doc")))
g_temporal.add((stmt_mit, EX.status, Literal("termine")))

print(f"Graphe temporel : {len(g_temporal)} triplets")
print()

# Requête : parcours professionnel chronologique
query_career = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX schema: <https://schema.org/>

SELECT ?orgName ?role ?startDate ?endDate ?status
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ex:Marie ;
          rdf:predicate schema:worksFor ;
          rdf:object ?org ;
          ex:role ?role ;
          ex:startDate ?startDate ;
          ex:status ?status .
    OPTIONAL { ?stmt ex:endDate ?endDate . }
    ?org schema:name ?orgName .
}
ORDER BY ?startDate
"""

print("=== Parcours professionnel de Marie ===")
print(f"{'Organisation':<15} {'Role':<15} {'Debut':<12} {'Fin':<12} {'Statut'}")
print("-" * 65)
for row in g_temporal.query(query_career):
    end = str(row.endDate) if row.endDate else "en cours"
    print(f"{str(row.orgName):<15} {str(row.role):<15} {str(row.startDate):<12} {end:<12} {str(row.status)}")
Graphe temporel : 32 triplets

=== Parcours professionnel de Marie ===
Organisation    Role            Debut        Fin          Statut
-----------------------------------------------------------------
MIT             Post-doc        2012-09-01   2014-12-31   termine
CNRS            Chercheuse      2015-01-15   2020-08-31   termine
Sorbonne        Professeure     2020-09-01   en cours     actif

Interpretation : annotations temporelles

Le parcours professionnel est represente de maniere structuree :

Periode Organisation Rôle Statut
2012-2014 MIT Post-doc Termine
2015-2020 CNRS Chercheuse Termine
2020-… Sorbonne Professeure Actif

Lecture des mesures

La requête ORDER BY ?startDate reconstruit la chronologie complète à partir des seules annotations : MIT 2012-09-01 → CNRS 2015-01-15 → Sorbonne 2020-09-01. L’ordre chronologique n’est stocké nulle part — il émerge des dates annotées, et c’est la requête qui le matérialise. Le graphe compte 32 triplets, dont 12 de structure (3 réifications × 4) : l’historique complet d’une carrière tient dans un objet manipulable.

Deux subtilités de lecture sur la sortie :

  • « en cours » n’est pas dans le graphe. C’est le code Python d’affichage qui traduit l’OPTIONAL non lié (row.endDate absent) en texte. Le graphe ne connaît que des intervalles ; la notion d’emploi actuel est une décision du consommateur — en SPARQL, cela s’écrit FILTER(!BOUND(?endDate)). Distinguer ce que le graphe dit de ce que l’affichage ajoute est exactement le réflexe critique à acquérir sur les données annotées.
  • Le graphe est append-only : l’embauche à la Sorbonne n’a pas écrasé l’embauche au CNRS. Les trois emplois coexistent, et ce sont les annotations (startDate, endDate, status) qui les départagent. Sur un sujet où l’information change (employeur, adresse, prix), annoter les versions vaut mieux que mettre à jour en place — on conserve l’historique gratuitement.

On touche ici à la version simplifiée d’un pattern industriel, le bitemporel : validité du fait (quand il est vrai dans le monde, nos startDate/endDate) contre instant d’enregistrement (quand le graphe l’a su — c’est le rôle de prov:generatedAtTime rencontré en 5.1). Les bases de données temporelles dédiées gèrent les deux dimensions nativement ; en RDF, ce sont deux annotations parmi d’autres.

Sans RDF-Star, cet historique necessite soit : - La reification classique (ce que nous faisons ici) : 4 triplets par relation + annotations - Des blank nodes intermediaires (ex: ex:emploi1 ex:employer ex:MIT ; ex:rôle "Post-doc" .) ce qui perd le lien direct Marie -> worksFor -> MIT

RDF-Star simplifierait cette ecriture en annotant directement :

<< ex:Marie schema:worksFor ex:CNRS >> ex:startDate "2015-01-15"^^xsd:date ;
                                        ex:endDate "2020-08-31"^^xsd:date ;
                                        ex:rôle "Chercheuse" ;
                                        ex:status "termine" .

5.3 Fusion multi-sources avec scores de confiance

Dans un graphe de connaissances construit a partir de sources multiples, chaque source peut affirmer des faits contradictoires. La reification permet de conserver toutes les versions et de les departager.

La cellule suivante met la réification au défi qui justifie son existence dans l’industrie : trois sources, trois valeurs différentes pour le même attribut — la population de Lyon selon l’INSEE, Wikipedia et une estimation locale. Un Graph plat ne peut stocker qu’une valeur pour (Lyon, population) ; le triple store écraserait à chaque insertion. Le graphe annoté, lui, conserve les trois affirmations côte à côte, chacune avec sa source, sa confiance et son millésime — puis laisse chaque usage appliquer sa politique de sélection.

C’est le pattern dit de fusion non destructive : rien n’est perdu, tout est qualifié. La requête finale (ORDER BY DESC(?confidence) LIMIT 1) en montre la version la plus simple — le vote par autorité — mais l’interprétation qui suit détaille pourquoi la confiance seule ne suffit pas : les trois valeurs portent des années différentes, et la « contradiction » est en partie un artefact de calendrier.

from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import RDF, XSD

EX = Namespace("http://example.org/")
SDO = Namespace("https://schema.org/")

g_multi = Graph()
g_multi.bind("ex", EX)
g_multi.bind("schema", SDO)

g_multi.add((EX.Lyon, RDF.type, SDO.City))
g_multi.add((EX.Lyon, SDO.name, Literal("Lyon")))

# Source 1 : INSEE (haute confiance)
# RDF-Star : << ex:Lyon schema:population "522250"^^xsd:integer >> ex:source "INSEE" .
pop_insee = Literal(522250, datatype=XSD.integer)
stmt_insee = reify(g_multi, EX.Lyon, SDO.population, pop_insee)
g_multi.add((stmt_insee, EX.source, Literal("INSEE")))
g_multi.add((stmt_insee, EX.confidence, Literal(0.95, datatype=XSD.double)))
g_multi.add((stmt_insee, EX.year, Literal("2023", datatype=XSD.gYear)))

# Source 2 : Wikipedia (confiance moyenne)
pop_wiki = Literal(516092, datatype=XSD.integer)
stmt_wiki = reify(g_multi, EX.Lyon, SDO.population, pop_wiki)
g_multi.add((stmt_wiki, EX.source, Literal("Wikipedia")))
g_multi.add((stmt_wiki, EX.confidence, Literal(0.70, datatype=XSD.double)))
g_multi.add((stmt_wiki, EX.year, Literal("2021", datatype=XSD.gYear)))

# Source 3 : Estimation locale (basse confiance)
pop_mairie = Literal(530000, datatype=XSD.integer)
stmt_mairie = reify(g_multi, EX.Lyon, SDO.population, pop_mairie)
g_multi.add((stmt_mairie, EX.source, Literal("Estimation mairie")))
g_multi.add((stmt_mairie, EX.confidence, Literal(0.40, datatype=XSD.double)))
g_multi.add((stmt_mairie, EX.year, Literal("2024", datatype=XSD.gYear)))

print(f"Graphe multi-sources : {len(g_multi)} triplets")
print()

# Requête : toutes les valeurs avec leurs sources
query_sources = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX schema: <https://schema.org/>

SELECT ?pop ?source ?confidence ?year
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ex:Lyon ;
          rdf:predicate schema:population ;
          rdf:object ?pop ;
          ex:source ?source ;
          ex:confidence ?confidence ;
          ex:year ?year .
}
ORDER BY DESC(?confidence)
"""

print("=== Population de Lyon selon differentes sources ===")
print(f"{'Source':<20} {'Population':>12} {'Confiance':>10} {'Annee':>6}")
print("-" * 52)
for row in g_multi.query(query_sources):
    pop = f"{int(row.pop):,d}"
    conf = f"{float(row.confidence):.2f}"
    print(f"{str(row.source):<20} {pop:>12} {conf:>10} {str(row.year):>6}")

# Valeur la plus fiable
query_best = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX schema: <https://schema.org/>

SELECT ?pop ?source ?confidence
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ex:Lyon ;
          rdf:predicate schema:population ;
          rdf:object ?pop ;
          ex:confidence ?confidence ;
          ex:source ?source .
}
ORDER BY DESC(?confidence)
LIMIT 1
"""

print()
for row in g_multi.query(query_best):
    print(f"Valeur retenue (confiance max) : {int(row.pop):,d} (source: {row.source})")
Graphe multi-sources : 23 triplets

=== Population de Lyon selon differentes sources ===
Source                 Population  Confiance  Annee
----------------------------------------------------
INSEE                     522,250       0.95   2023
Wikipedia                 516,092       0.70   2021
Estimation mairie         530,000       0.40   2024

Valeur retenue (confiance max) : 522,250 (source: INSEE)

Interpretation : fusion multi-sources

Ce pattern est fondamental pour les graphes de connaissances industriels :

  1. Conserver toutes les valeurs : chaque source a sa propre estimation
  2. Annoter la confiance : permettre un filtrage par fiabilite
  3. Tracer la provenance : savoir d’ou vient chaque information
  4. Sélectionner la meilleure : prendre la valeur la plus fiable pour un usage donne
Stratégie de sélection Requête Usage
Plus haute confiance ORDER BY DESC(?confidence) LIMIT 1 Vue par defaut
Plus recente ORDER BY DESC(?year) LIMIT 1 Données a jour
Moyenne ponderee Calcul hors SPARQL Estimation consolidee

Lecture des mesures

Les trois affirmations mesurées : 522 250 (INSEE, confiance 0.95, millésime 2023), 516 092 (Wikipedia, 0.70, 2021), 530 000 (estimation mairie, 0.40, 2024). Le graphe fait 23 triplets ; la requête à LIMIT 1 retient 522 250 — source INSEE.

Avant de parler de contradiction, regarder les millésimes : ils diffèrent (2021, 2023, 2024). Une ville de ce gabarit gagne ou perd quelques milliers d’habitants par an — 522 250 en 2023 et 516 092 en 2021 peuvent être toutes deux exactes, chacune pour son année. La « contradiction » apparente est en partie un artefact de fraîcheur. D’où la leçon centrale de la fusion : la confiance seule ne départage pas, il faut croiser deux axes — fiabilité de la source et date du fait. C’est ce que traduit le tableau des stratégies : la plus fiable pour une référence stable, la plus récente pour un état courant, la moyenne pondérée pour une estimation consolidée (calcul hors SPARQL, dans le langage hôte).

Le choix effectué ici — ORDER BY DESC(?confidence) LIMIT 1, le vote par autorité — privilégie délibérément la robustesse statistique sur la fraîcheur : l’estimation mairie 2024, bien que plus récente, perd avec 0.40. C’est un choix défendable pour une donnée de référence ; il serait contestable pour un tableau de bord temps réel. Le graphe ne tranche pas : il conserve les trois versions annotées, et chaque usage applique sa politique. C’est la définition même d’une fusion non destructive — par opposition au UPSERT d’une base classique qui aurait écrasé les valeurs précédentes dès la deuxième source.


6. Alternative : les Graphes Nommes (Named Graphs)

Les graphes nommes offrent une autre approche pour associer du contexte a des triplets. Au lieu d’annoter un triplet individuel, on place les triplets dans un graphe identifie par une URI, puis on annote cette URI.

Comparaison des approches

Aspect Reification Graphes nommes RDF-Star
Granularite Par triplet Par groupe de triplets Par triplet
Verbosite 4 triplets / annotation 1 URI par contexte Inline
Modèle de données Triplets Quadruplets (quads) Triplets etendus
Support rdflib Oui (toutes versions) Oui (Dataset) Non (7.6.0)
Support SPARQL Standard GRAPH ?g { ... } SPARQL-Star
Cas d’usage ideal Annotation fine Provenance par lot Annotation fine

6.1 Graphes nommes avec rdflib Dataset

rdflib fournit la classe Dataset pour gerer des graphes nommes. Chaque graphe est identifie par une URI et contient un ensemble de triplets.

L’API rdflib utilisée ici : Dataset() (pas Graph()) pour manipuler des quads ; ds.graph(uri) récupère (ou crée) le graphe nommé identifié par l’URI ; ds.default_graph est le graphe sans nom, celui des métadonnées. Les faits vont dans les graphes nommés, les annotations des graphes vont dans le graphe par défaut — la symétrie avec la réification est frappante : là on annotait des Statements, ici on annote des contextes entiers.

La conséquence structurelle : un Dataset sérialise en TriG ou N-Quads (des formats à quadrupeaux), pas en Turtle simple — le format doit porter le quatrième élément. C’est aussi ce qui rend les graphes nommés opérables côté stockage : les moteurs quads indexent le contexte comme une dimension de première classe, et la clause SPARQL GRAPH ?g { ... } l’interroge nativement.

from rdflib import Dataset, URIRef, Literal, Namespace
from rdflib.namespace import RDF, FOAF, XSD

EX = Namespace("http://example.org/")

ds = Dataset()
ds.bind("ex", EX)
ds.bind("foaf", FOAF)

# Créer un graphe nommé pour le contexte "assertion d'Alice"
ctx_alice = URIRef("http://example.org/context/alice-assertion")
g_alice = ds.graph(ctx_alice)

# Les triplets sont dans le graphe nommé
g_alice.add((EX.Bob, FOAF.knows, EX.Carol))
g_alice.add((EX.Bob, FOAF.knows, EX.Dave))

# Les annotations sont dans le graphe par defaut
ds.default_graph.add((ctx_alice, RDF.type, EX.Assertion))
ds.default_graph.add((ctx_alice, EX.assertedBy, EX.Alice))
ds.default_graph.add((ctx_alice, EX.confidence, Literal(0.85, datatype=XSD.double)))
ds.default_graph.add((ctx_alice, EX.date, Literal("2024-03-15", datatype=XSD.date)))

# Second contexte : assertion de Bob
ctx_bob = URIRef("http://example.org/context/bob-assertion")
g_bob = ds.graph(ctx_bob)
g_bob.add((EX.Carol, FOAF.knows, EX.Eve))

ds.default_graph.add((ctx_bob, RDF.type, EX.Assertion))
ds.default_graph.add((ctx_bob, EX.assertedBy, EX.Bob))
ds.default_graph.add((ctx_bob, EX.confidence, Literal(0.60, datatype=XSD.double)))

print("=== Graphe par defaut (metadata) ===")
for s, p, o in sorted(ds.default_graph, key=lambda t: str(t[0])):
    s_short = str(s).replace("http://example.org/", "ex:")
    p_short = str(p).replace("http://example.org/", "ex:").replace(
        "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:")
    o_short = str(o).replace("http://example.org/", "ex:")
    print(f"  {s_short} {p_short} {o_short}")

print()
print("=== Graphe nomme : alice-assertion ===")
for s, p, o in g_alice:
    s_short = str(s).replace("http://example.org/", "ex:")
    p_short = str(p).replace("http://xmlns.com/foaf/0.1/", "foaf:")
    o_short = str(o).replace("http://example.org/", "ex:")
    print(f"  {s_short} {p_short} {o_short}")

print()
print("=== Graphe nomme : bob-assertion ===")
for s, p, o in g_bob:
    s_short = str(s).replace("http://example.org/", "ex:")
    p_short = str(p).replace("http://xmlns.com/foaf/0.1/", "foaf:")
    o_short = str(o).replace("http://example.org/", "ex:")
    print(f"  {s_short} {p_short} {o_short}")
=== Graphe par defaut (metadata) ===
  ex:context/alice-assertion rdf:type ex:Assertion
  ex:context/alice-assertion ex:assertedBy ex:Alice
  ex:context/alice-assertion ex:date 2024-03-15
  ex:context/alice-assertion ex:confidence 0.85
  ex:context/bob-assertion ex:assertedBy ex:Bob
  ex:context/bob-assertion rdf:type ex:Assertion
  ex:context/bob-assertion ex:confidence 0.6

=== Graphe nomme : alice-assertion ===
  ex:Bob foaf:knows ex:Dave
  ex:Bob foaf:knows ex:Carol

=== Graphe nomme : bob-assertion ===
  ex:Carol foaf:knows ex:Eve

Interpretation : graphes nommes

Les graphes nommes regroupent des triplets par contexte :

Graphe Contenu Annotations
ex:context/alice-assertion Bob connait Carol, Bob connait Dave Asserte par Alice, confiance 0.85
ex:context/bob-assertion Carol connait Eve Asserte par Bob, confiance 0.60

Avantages des graphes nommes : - Annotation de groupes de triplets (pas individuel) - Pas de triplets supplementaires par fait annote - Support natif dans tous les triple stores (SPARQL GRAPH)

Limites : - Granularite trop grosse si on veut annoter un seul triplet (il faut un graphe par triplet) - Un triplet ne peut etre que dans un seul graphe nomme a la fois

Lecture des mesures

La sortie sépare physiquement les deux zones : le graphe par défaut porte 7 triplets de métadonnées (type, assertedBy, date, confiance pour les deux contextes), les graphes nommés portent les faits — 2 triplets dans alice-assertion (Bob connaît Carol, Bob connaît Dave), 1 dans bob-assertion (Carol connaît Eve). Faits et annotations ne vivent plus dans le même ensemble : c’est un Dataset de quads (sujet, prédicat, objet, graphe), pas un Graph de triples.

Le contraste de granularité avec la section 4 est instructif : là, chaque relation avait son Statement et sa confiance propre ; ici, les deux relations de alice-assertion partagent une seule annotation (Alice, 0.85, 2024-03-15). Si ces deux relations méritaient des confiances différentes, il faudrait deux graphes nommés — la granularité « par lot » devient un piège dès que le lot n’est pas homogène. La limite listée ci-dessous (« un triplet ne peut être que dans un seul graphe nommé à la fois ») est l’autre face : un fait relevant de deux contextes doit être dupliqué.

Côté interrogation, SPARQL offre GRAPH ?g { ... } pour filtrer par contexte — et contrairement à la réification, les moteurs quads (Jena/Fuseki, Virtuoso) optimisent cette jointure nativement. Le tableau de décision du blockquote se résume en une ligne : annotation par lot, sources par import → graphes nommés ; annotation par fait, confiance, dates → réification aujourd’hui, RDF-Star demain.

Quand utiliser quoi ? : Les graphes nommes conviennent pour la provenance par lot (“tous ces faits viennent de cette source”). La reification (ou RDF-Star) convient pour les annotations fines par triplet.


7. Serialisation et interoperabilite

Formats et support des annotations de triplets

Format Reification Graphes nommes RDF-Star
Turtle Oui (standard) Non Turtle-Star (extension)
TriG Oui Oui (natif) TriG-Star (extension)
N-Quads Oui Oui (natif) Non
JSON-LD Oui Oui (@graph) En discussion
RDF/XML Oui Non Non

Serialisation d’un graphe avec reification

La serialisation est le test de vérité de toute structure RDF : ce qui ne survit pas a un aller-retour texte -> graphe n’existe pas vraiment. Cette cellule ecrit le meme graphe reifie dans plusieurs formats standards (Turtle, N-Triples, RDF/XML, JSON-LD) pour observer comment chacun materialise les blank nodes rdf:Statement.

L’enjeu pratique : deux outils echangent un graphe annote — un extracteur produit du JSON-LD, un triplestore consomme du Turtle. Si la reification est correctement encodee, les annotations traversent ; sinon elles sont silencieusement perdues a la frontiere des formats. Comparez la taille des sorties entre formats au passage : le cout de verbosite de la reification, visible en triplets depuis la section 1, se retrouve amplifie en octets dans les formats verbeux.

Les quatre formats testés couvrent les deux grandes familles d’usage : Turtle (lisible, compact — le format de travail), N-Triples (une ligne par triplet, le format d’échange massif), RDF/XML (le format historique, encore exigé par certains entrepôts), JSON-LD (le format du Web des données dans les APIs JavaScript). Aucun n’a de syntaxe dédiée à la réification — et c’est justement le point : la réification étant du RDF ordinaire, tous les formats la transportent sans extension. RDF-Star, lui, exigera Turtle-Star, TriG-Star, etc. — l’interopérabilité universelle est l’argument qui reste à la réification, support RDF-Star encore partiel.

En exécutant, comparez trois choses : la place du blank node Statement dans chaque format, la longueur totale des sorties, et la façon dont le triplet de fait (la ligne Alice knows Bob) reste toujours séparée de sa réification. L’interprétation qui suit relit les quatre sorties chiffre à chiffre.

from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import RDF, FOAF, XSD

EX = Namespace("http://example.org/")

g_ser = Graph()
g_ser.bind("ex", EX)
g_ser.bind("foaf", FOAF)

# Fait + réification + annotation
g_ser.add((EX.Alice, FOAF.knows, EX.Bob))
stmt = reify(g_ser, EX.Alice, FOAF.knows, EX.Bob)
g_ser.add((stmt, EX.confidence, Literal(0.9, datatype=XSD.double)))

formats_to_test = [
    ("Turtle", "turtle"),
    ("N-Triples", "nt"),
    ("RDF/XML", "xml"),
]

for label, fmt in formats_to_test:
    try:
        output = g_ser.serialize(format=fmt)
        print(f"=== {label} ===")
        print(output.strip())
        print()
    except Exception as e:
        print(f"=== {label} : Erreur ===")
        print(f"  {e}")
        print()

# JSON-LD
try:
    jsonld_out = g_ser.serialize(format="json-ld", indent=2)
    print("=== JSON-LD ===")
    print(jsonld_out.strip())
except Exception as e:
    print(f"=== JSON-LD : {e} ===")
=== Turtle ===
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Alice foaf:knows ex:Bob .

[] a rdf:Statement ;
    ex:confidence 9e-01 ;
    rdf:object ex:Bob ;
    rdf:predicate foaf:knows ;
    rdf:subject ex:Alice .

=== N-Triples ===
<http://example.org/Alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/Bob> .
_:N9ab2df6cd8964ca7b19d7189d1c48042 <http://example.org/confidence> "0.9"^^<http://www.w3.org/2001/XMLSchema#double> .
_:N9ab2df6cd8964ca7b19d7189d1c48042 <http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate> <http://xmlns.com/foaf/0.1/knows> .
_:N9ab2df6cd8964ca7b19d7189d1c48042 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement> .
_:N9ab2df6cd8964ca7b19d7189d1c48042 <http://www.w3.org/1999/02/22-rdf-syntax-ns#subject> <http://example.org/Alice> .
_:N9ab2df6cd8964ca7b19d7189d1c48042 <http://www.w3.org/1999/02/22-rdf-syntax-ns#object> <http://example.org/Bob> .

=== RDF/XML ===
<?xml version="1.0" encoding="utf-8"?>
<rdf:RDF
   xmlns:ex="http://example.org/"
   xmlns:foaf="http://xmlns.com/foaf/0.1/"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
  <rdf:Description rdf:about="http://example.org/Alice">
    <foaf:knows rdf:resource="http://example.org/Bob"/>
  </rdf:Description>
  <rdf:Description rdf:nodeID="N9ab2df6cd8964ca7b19d7189d1c48042">
    <rdf:type rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement"/>
    <rdf:subject rdf:resource="http://example.org/Alice"/>
    <rdf:predicate rdf:resource="http://xmlns.com/foaf/0.1/knows"/>
    <rdf:object rdf:resource="http://example.org/Bob"/>
    <ex:confidence rdf:datatype="http://www.w3.org/2001/XMLSchema#double">0.9</ex:confidence>
  </rdf:Description>
</rdf:RDF>

=== JSON-LD ===
[
  {
    "@id": "http://example.org/Alice",
    "http://xmlns.com/foaf/0.1/knows": [
      {
        "@id": "http://example.org/Bob"
      }
    ]
  },
  {
    "@id": "_:N9ab2df6cd8964ca7b19d7189d1c48042",
    "@type": [
      "http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement"
    ],
    "http://example.org/confidence": [
      {
        "@type": "http://www.w3.org/2001/XMLSchema#double",
        "@value": 0.9
      }
    ],
    "http://www.w3.org/1999/02/22-rdf-syntax-ns#object": [
      {
        "@id": "http://example.org/Bob"
      }
    ],
    "http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate": [
      {
        "@id": "http://xmlns.com/foaf/0.1/knows"
      }
    ],
    "http://www.w3.org/1999/02/22-rdf-syntax-ns#subject": [
      {
        "@id": "http://example.org/Alice"
      }
    ]
  }
]

Interpretation : formats de serialisation

  • Turtle : la reification est representee avec des blank nodes et les proprietes rdf:subject, rdf:predicate, rdf:object
  • N-Triples : chaque triplet est explicite, les blank nodes ont des identifiants generes
  • RDF/XML : la reification est supportee via la syntaxe rdf:Description
  • JSON-LD : la reification est representee comme un objet avec @type: rdf:Statement

Quand le support Turtle-Star sera disponible dans rdflib, la serialisation sera plus compacte :

<< ex:Alice foaf:knows ex:Bob >> ex:confidence "0.9"^^xsd:double .

au lieu des 6 triplets actuels (1 fait + 4 reification + 1 annotation).

Lecture des mesures

Le même blank node — _:N9ab2df6cd8964ca7b19d7189d1c48042 — traverse trois des quatre sorties : identifiant _:... complet en N-Triples, attribut rdf:nodeID en RDF/XML, valeur "@id": "_:N..." en JSON-LD. Turtle seul le compacte en [] parce que le nœud n’y est référencé qu’une fois. La structure survive au changement de format : c’est la preuve d’interopérabilité que cette section cherchait — un extracteur qui émet du JSON-LD et un triple store qui consomme du Turtle peuvent échanger ce graphe annoté sans perte.

La verbosité, elle, paie un second tribut. Mesuré sur la sortie : Turtle reste compact ; N-Triples déploie 6 triplets complets avec URI expansées (chaque ligne ~100 caractères, aucun préfixe) ; RDF/XML emballe chaque triplet dans un élément ; JSON-LD devient le plus volumineux. Le coût de la réification en triplets (section 1) se repaie en octets — le format d’échange multiplie le facteur 4-6 selon le choix.

Le test qui conclut naturellement cette section, à exécuter sur tout pipeline réel d’échange : re-parser chaque sérialisation (Graph().parse(data=..., format=...)) et comparer au graphe source (les blank nodes changent d’identifiant, mais le contenu isomorphe doit matcher). Ce qui survit à l’aller-retour existe ; ce qui ne survit pas n’a jamais existé au sens RDF.


8. Comparaison des trois approches

Recapitulons les trois mécanismes pour annoter des triplets RDF :

Critere Reification classique Graphes nommes RDF-Star / RDF 1.2
Standard RDF 1.0 (2004) SPARQL 1.1 (2013) RDF 1.2 (2024)
Granularite Par triplet Par groupe Par triplet
Cout en triplets 4 par annotation 1 URI par contexte 0 (inline)
Complexite SPARQL Jointures multiples GRAPH clause Pattern << >>
Support rdflib 7.x Complet Complet (Dataset) Non disponible
Support triple stores Universel Universel Croissant
Lisibilite Faible Moyenne Bonne

Recommandations

Situation Approche recommandee
Annotation fine par triplet avec rdflib Reification + fonction reify()
Provenance par lot (“ces 100 faits viennent de cette source”) Graphes nommes
Triple store supportant RDF-Star (Jena, Stardog, Oxigraph) RDF-Star natif
Interoperabilite maximale Reification (universellement supportee)

Pour lire ce tableau de synthèse, remontez le fil des mesures du notebook : 7 triplets pour un fait annoté (section 1), 8 avec la fonction utilitaire (section 2), 12 pour deux niveaux de croyance (section 3.2), 37 pour un réseau social complet (section 4.1), 17 en provenance, 32 en historique de carrière, 23 en fusion multi-sources — la réification facture partout ses 4 triplets de structure par fait. Les graphes nommés (section 6) facturent 1 URI par contexte — imbattable par lot, inapplicable par fait. RDF-Star ne facture rien : l’annotation est inline.

De là, les recommandations du bas de tableau se déduisent plutôt qu’ils ne se décrètent : le choix n’est pas « quel est le meilleur mécanisme » mais « quelle granularité d’annotation mon cas d’usage exige-t-il, et quels outils le consommeront ». Annotation fine consommée par rdflib aujourd’hui → réification (avec reify()). Provenance par import, par fichier, par source → graphes nommés. Consommateur final déjà choisi parmi les stores RDF-Star (Jena, Stardog, Oxigraph) → RDF-Star natif, quitte à garder la réification comme format d’archivage universellement lisible.

Le fil conducteur à retenir au-delà du choix d’outillage : les dimensions d’annotation — qui, quand, combien, jusqu’à quand — sont la compétence transférable. Le mécanisme exact changera (il est en train de changer) ; les questions, elles, restent.


9. Exercices et Exemples guide

Cette section rassemble les travaux dirigés du notebook : trois exemples résolus d’abord (dont les solutions sont créditées à leurs auteurs), trois exercices à compléter ensuite — chaque exercice ayant sa cellule guide immédiatement au-dessus. La correspondance précise entre guides et exercices est donnée dans la cellule d’introduction aux exercices.

Exemple guide 1 : Critiques de film avec reification

Solution proposee par Lilou Mayot (promo 2026)

Voici un exemple complet de critiques de film avec reification : - Critique A : note 4/5, source “Le Monde”, date 2024-01-15 - Critique B : note 3/5, source “Telerama”, date 2024-01-20 - Critique C : note 5/5, source “Cahiers du Cinema”, date 2024-02-01

Le fait principal est ex:Film1 ex:rating ?note avec les annotations de provenance.

Indice : Chaque critique produit un rdf:Statement différent car la note differe.

En RDF-Star, cela s’ecrirait :

<< ex:Film1 ex:rating "4"^^xsd:integer >> ex:reviewer "Critique A" ;
                                           ex:source "Le Monde" ;
                                           ex:date "2024-01-15"^^xsd:date .

La situation traitée est celle de l’agrégation de critiques : trois journalistes, trois notes différentes pour le même film. En RDF plat, impossible d’associer une note à son auteur — ex:Film1 ex:rating 4 et ex:Film1 ex:rating 5 coexistent sans traçabilité. La solution de la cellule suivante : chaque critique asserte son triplet (les notes diffèrent, donc trois triplets distincts coexistent légitimement), et réifie chacun avec reviewer, source et date.

Observez dans la sortie finale le tri ORDER BY DESC(?note) : la requête SPARQL sur les Statements remonte les trois critiques avec leur contexte complet — exactement la vue « revue de presse » qu’aucun parcours de triplets simples ne pourrait produire. C’est le même motif que la section 4.1, transposé du réseau social à la critique culturelle : le pattern est le transférable, le domaine est interchangeable.

# Exemple guide 1 : Critiques de film avec réification
from rdflib import Graph, Namespace, Literal, BNode
from rdflib.namespace import RDF, XSD

EX = Namespace("http://example.org/")

g_ex1 = Graph()
g_ex1.bind("ex", EX)

# Film
g_ex1.add((EX.Film1, RDF.type, EX.Film))
g_ex1.add((EX.Film1, EX.title, Literal("Les Temps Modernes")))

# Critique A : note 4/5
note_a = Literal(4, datatype=XSD.integer)
g_ex1.add((EX.Film1, EX.rating, note_a))
stmt_a = reify(g_ex1, EX.Film1, EX.rating, note_a)
g_ex1.add((stmt_a, EX.reviewer, Literal("Critique A")))
g_ex1.add((stmt_a, EX.source, Literal("Le Monde")))
g_ex1.add((stmt_a, EX.date, Literal("2024-01-15", datatype=XSD.date)))

# Critique B : note 3/5
note_b = Literal(3, datatype=XSD.integer)
g_ex1.add((EX.Film1, EX.rating, note_b))
stmt_b = reify(g_ex1, EX.Film1, EX.rating, note_b)
g_ex1.add((stmt_b, EX.reviewer, Literal("Critique B")))
g_ex1.add((stmt_b, EX.source, Literal("Telerama")))
g_ex1.add((stmt_b, EX.date, Literal("2024-01-20", datatype=XSD.date)))

# Critique C : note 5/5
note_c = Literal(5, datatype=XSD.integer)
g_ex1.add((EX.Film1, EX.rating, note_c))
stmt_c = reify(g_ex1, EX.Film1, EX.rating, note_c)
g_ex1.add((stmt_c, EX.reviewer, Literal("Critique C")))
g_ex1.add((stmt_c, EX.source, Literal("Cahiers du Cinema")))
g_ex1.add((stmt_c, EX.date, Literal("2024-02-01", datatype=XSD.date)))

print("=== Graphe des critiques ===")
print(g_ex1.serialize(format="turtle"))

# Requête SPARQL : lister les critiques
query = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
SELECT ?note ?reviewer ?source ?date
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ex:Film1 ;
          rdf:predicate ex:rating ;
          rdf:object ?note ;
          ex:reviewer ?reviewer ;
          ex:source ?source ;
          ex:date ?date .
}
ORDER BY DESC(?note)
"""

print("\n=== Critiques triees par note ===")
for row in g_ex1.query(query):
    print(f"  {row.reviewer} : {row.note}/5 ({row.source}, {row.date})")
=== Graphe des critiques ===
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Film1 a ex:Film ;
    ex:rating 3,
        4,
        5 ;
    ex:title "Les Temps Modernes" .

[] a rdf:Statement ;
    ex:date "2024-01-20"^^xsd:date ;
    ex:reviewer "Critique B" ;
    ex:source "Telerama" ;
    rdf:object 3 ;
    rdf:predicate ex:rating ;
    rdf:subject ex:Film1 .

[] a rdf:Statement ;
    ex:date "2024-01-15"^^xsd:date ;
    ex:reviewer "Critique A" ;
    ex:source "Le Monde" ;
    rdf:object 4 ;
    rdf:predicate ex:rating ;
    rdf:subject ex:Film1 .

[] a rdf:Statement ;
    ex:date "2024-02-01"^^xsd:date ;
    ex:reviewer "Critique C" ;
    ex:source "Cahiers du Cinema" ;
    rdf:object 5 ;
    rdf:predicate ex:rating ;
    rdf:subject ex:Film1 .



=== Critiques triees par note ===
  Critique C : 5/5 (Cahiers du Cinema, 2024-02-01)
  Critique A : 4/5 (Le Monde, 2024-01-15)
  Critique B : 3/5 (Telerama, 2024-01-20)

Exemple guide 2 : Requeter les reifications avec SPARQL

Solution proposee par Lilou Mayot (promo 2026)

En utilisant le graphe de connaissances annote de la section 4.1 (variable g_kg), voici des requêtes SPARQL pour :

  1. Trouver toutes les relations assertees par une tierce personne (c’est-a-dire qui ont un ex:assertedBy)
  2. Trouver les relations etablies depuis plus de 3 ans (avant 2022-01-01)

Indice : Utilisez le pattern ?stmt a rdf:Statement ; rdf:subject ?s ; rdf:predicate foaf:knows ; rdf:object ?o .

Les deux requêtes de la solution illustrent les deux familles de filtrage sur annotations : la 2a filtre sur une annotation de provenance (ex:assertedBy — qui affirme ?) et ne retourne que les relations rapportées par un tiers, ici l’unique Bob affirme : Alice connait Dave ; la 2b filtre sur une annotation temporelle (FILTER(?since < "2022-01-01"^^xsd:date)) et remonte les relations anciennes — Alice-Bob (2019) et Bob-Carol (2021) passent, les autres non.

Les deux réutilisent le graphe g_kg de la section 4.1 sans le reconstruire : c’est un point de méthode — les annotations n’ont d’intérêt que si les requêtes ultérieures peuvent les exploiter a posteriori, longtemps après la construction.

# Exemple guide 2 : Requêtes SPARQL sur les réifications

# Requête 2a : Relations assertées par une tierce personne
query_2a = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

SELECT ?person1 ?person2 ?asserter
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:assertedBy ?asserter .
}
"""

# Requête 2b : Relations anciennes (avant 2022)
query_2b = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

SELECT ?person1 ?person2 ?since
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:since ?since .
    FILTER(?since < "2022-01-01"^^xsd:date)
}
"""

print("=== 2a : Relations assertees par un tiers ===")
for row in g_kg.query(query_2a):
    p1 = str(row.person1).split("/")[-1]
    p2 = str(row.person2).split("/")[-1]
    asserter = str(row.asserter).split("/")[-1]
    print(f"  {asserter} affirme : {p1} connait {p2}")

print("\n=== 2b : Relations anterieures a 2022 ===")
for row in g_kg.query(query_2b):
    p1 = str(row.person1).split("/")[-1]
    p2 = str(row.person2).split("/")[-1]
    print(f"  {p1} connait {p2} depuis {row.since}")
=== 2a : Relations assertees par un tiers ===
  Bob affirme : Alice connait Dave

=== 2b : Relations anterieures a 2022 ===
  Alice connait Bob depuis 2019-03-01
  Bob connait Carol depuis 2021-07-15

10. Ecosysteme et compatibilite

RDF-Star est de plus en plus adopte par l’ecosysteme du Web Sémantique.

Support dans les triple stores

Triple Store Support RDF-Star SPARQL-Star Notes
Apache Jena (Fuseki) Oui (depuis 4.2) Oui Implementation de reference
Blazegraph Oui (extensions) Oui Support historique
Stardog Oui (depuis 7.4) Oui Support commercial complet
Oxigraph Oui (depuis 0.3) Oui Store Rust performant
GraphDB (Ontotext) Oui (depuis 10.0) Oui Support entreprise
Amazon Neptune Oui (depuis 2023) Oui Service cloud
Virtuoso Partiel Partiel Via extensions

Support dans les bibliotheques

Bibliotheque Langage RDF-Star natif Reification Version min
rdflib Python Non (7.6.0) Oui Toutes
Apache Jena Java Oui Oui 4.2
dotNetRDF C# Partiel Oui 3.x
N3.js JavaScript Oui Oui 1.16
RDF4J Java Oui Oui 4.0

Standardisation W3C

Document Statut (2024)
RDF 1.2 Concepts Candidate Recommendation
RDF 1.2 Turtle Candidate Recommendation
RDF 1.2 N-Triples Candidate Recommendation
SPARQL 1.2 Query Working Draft
RDF 1.2 JSON-LD En discussion

Pour lire ces tableaux : le support triple store conditionne ce que vous pourrez déployer en production (une requête SPARQL-Star sur un store qui ne la parse pas échoue à la première ligne), tandis que le support bibliothèque conditionne ce que vous pouvez construire côté application. L’écart entre les deux colonnes est instructif — les moteurs ont devancé les bibliothèques : Jena et Stardog exécutent du Turtle-Star depuis 2021, alors que rdflib 7.6.0 ne le parse toujours pas. Conséquence pratique pour ce notebook : le même fichier pédagogique est exécutable chez un étudiant (réification) et, demain, nativement compact sur une infrastructure RDF-Star — la sémantique étant stable, seule la syntaxe bascule.

Côté standardisation, la ligne à retenir du dernier tableau est le contraste de maturité : les documents Concepts et Turtle de RDF 1.2 sont au stade Candidate Recommendation (le contenu est stable, les implémenteurs peuvent s’engager), tandis que SPARQL 1.2 reste au stade Working Draft — le requêtage des annotations standardisées est le maillon encore en mouvement. D’où la posture de ce notebook : enseigner les concepts et les patterns avec les outils d’aujourd’hui, en gardant chaque variante RDF-Star posée comme cible documentée plutôt que comme dépendance d’exécution.

Exemple guide 3 : Graphe de confiance avec RDF-Star

Solution proposee par Mustapha Oumeziane et Coraline Carpin (promo 2026)

Construisons un graphe de connaissances representant des affirmations sur les capitales europeennes avec niveaux de confiance. Pour chaque triple sujet-predicat-objet, nous ajoutons en metadata : - :confidence (valeur entre 0.0 et 1.0) - :source (agent ou document d’origine) - :timestamp (date de creation)

Puis ecrivons une requête SPARQL qui selectionne uniquement les affirmations ayant une confiance >= 0.8.

La solution construit le cas d’usage type des graphes de connaissances de faits : cinq affirmations géographiques (France → Paris, …), chacune réifiée avec un triplet de métadonnées confiance / source / timestamp. Les valeurs sont étalées volontairement — de 0.98 (Wikidata) à 0.75 (« Source a verifier ») — pour que le seuil de la requête (>= 0.8) produise une discrimination visible : quatre capitales passent, Berne (0.75) est filtrée.

C’est le même geste que la section 4.3 (filtrage par confiance), généralisé : la confiance y filtrait des relations sociales, ici des faits encyclopédiques. À noter dans la sortie : le graphe fait 40 triplets — 5 faits + 5 réifications (20) + 15 annotations — la facture de structure reste exactement celle prédite par la section 1, à cinq reprises.

# Exemple guide 3 : Graphe de confiance avec RDF-Star
from rdflib import Graph, Namespace, Literal
from rdflib.namespace import RDF, XSD

EX = Namespace("http://example.org/")

g_trust = Graph()
g_trust.bind("ex", EX)

capital_facts = [
    (EX.France, EX.capital, EX.Paris, 0.98, "Wikidata", "2024-03-01"),
    (EX.Allemagne, EX.capital, EX.Berlin, 0.97, "DBpedia", "2024-03-02"),
    (EX.Italie, EX.capital, EX.Rome, 0.96, "Cours geographie", "2024-03-03"),
    (EX.Espagne, EX.capital, EX.Madrid, 0.95, "Wikidata", "2024-03-04"),
    (EX.Suisse, EX.capital, EX.Berne, 0.75, "Source a verifier", "2024-03-05"),
]

for country, predicate, capital, confidence, source, timestamp in capital_facts:
    g_trust.add((country, predicate, capital))
    stmt = reify(g_trust, country, predicate, capital)
    g_trust.add((stmt, EX.confidence, Literal(confidence, datatype=XSD.double)))
    g_trust.add((stmt, EX.source, Literal(source)))
    g_trust.add((stmt, EX.timestamp, Literal(timestamp, datatype=XSD.date)))

print(f"Graphe de confiance : {len(g_trust)} triplets\n")
print(g_trust.serialize(format="turtle"))

query_trusted = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

SELECT ?country ?capital ?confidence ?source ?timestamp
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?country ;
          rdf:predicate ex:capital ;
          rdf:object ?capital ;
          ex:confidence ?confidence ;
          ex:source ?source ;
          ex:timestamp ?timestamp .
    FILTER (?confidence >= "0.8"^^xsd:double)
}
ORDER BY DESC(?confidence)
"""

print("=== Capitales avec confiance >= 0.8 ===")
for row in g_trust.query(query_trusted):
    country = str(row.country).split("/")[-1]
    capital = str(row.capital).split("/")[-1]
    print(f"  {country} -> {capital} (confiance={float(row.confidence):.2f}, source={row.source})")
Graphe de confiance : 40 triplets

@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Allemagne ex:capital ex:Berlin .

ex:Espagne ex:capital ex:Madrid .

ex:France ex:capital ex:Paris .

ex:Italie ex:capital ex:Rome .

ex:Suisse ex:capital ex:Berne .

[] a rdf:Statement ;
    ex:confidence 9.8e-01 ;
    ex:source "Wikidata" ;
    ex:timestamp "2024-03-01"^^xsd:date ;
    rdf:object ex:Paris ;
    rdf:predicate ex:capital ;
    rdf:subject ex:France .

[] a rdf:Statement ;
    ex:confidence 9.5e-01 ;
    ex:source "Wikidata" ;
    ex:timestamp "2024-03-04"^^xsd:date ;
    rdf:object ex:Madrid ;
    rdf:predicate ex:capital ;
    rdf:subject ex:Espagne .

[] a rdf:Statement ;
    ex:confidence 7.5e-01 ;
    ex:source "Source a verifier" ;
    ex:timestamp "2024-03-05"^^xsd:date ;
    rdf:object ex:Berne ;
    rdf:predicate ex:capital ;
    rdf:subject ex:Suisse .

[] a rdf:Statement ;
    ex:confidence 9.7e-01 ;
    ex:source "DBpedia" ;
    ex:timestamp "2024-03-02"^^xsd:date ;
    rdf:object ex:Berlin ;
    rdf:predicate ex:capital ;
    rdf:subject ex:Allemagne .

[] a rdf:Statement ;
    ex:confidence 9.6e-01 ;
    ex:source "Cours geographie" ;
    ex:timestamp "2024-03-03"^^xsd:date ;
    rdf:object ex:Rome ;
    rdf:predicate ex:capital ;
    rdf:subject ex:Italie .


=== Capitales avec confiance >= 0.8 ===
  France -> Paris (confiance=0.98, source=Wikidata)
  Allemagne -> Berlin (confiance=0.97, source=DBpedia)
  Italie -> Rome (confiance=0.96, source=Cours geographie)
  Espagne -> Madrid (confiance=0.95, source=Wikidata)

Resume

Concepts cles

Concept Description
Reification classique Mécanisme RDF 1.0 avec rdf:Statement (4 triplets par assertion, verbeux mais universel)
Graphes nommes Regrouper des triplets dans un contexte identifie par une URI (granularite par lot)
Quoted Triple (RDF-Star) Triplet utilise comme sujet/objet : << s p o >> (RDF 1.2, pas encore dans rdflib)
Provenance Annoter qui a dit quoi, quand, avec quelle source
Confiance Score de fiabilite attache a une assertion
Annotations temporelles Validite dans le temps d’un fait
Fusion multi-sources Conserver et departager les valeurs de sources différentes

Competences acquises

  1. Comprendre les limites de la reification classique et la motivation de RDF-Star
  2. Créer des annotations de triplets avec la reification et la fonction reify()
  3. Utiliser les graphes nommes (Dataset) comme alternative pour le contexte
  4. Interroger les annotations avec SPARQL standard (patterns de reification)
  5. Appliquer ces patterns a des cas concrets (provenance, temporalite, multi-sources)
  6. Comparer reification, graphes nommes et RDF-Star pour choisir l’approche adaptee

Pour aller plus loin

Une façon de réviser ce tableau : pour chaque concept de la colonne de gauche, retrouver l’endroit du notebook où il a été exécuté — la réification classique en section 1 (7 triplets mesurés), reify() en section 2, les graphes nommés en section 6 (Dataset et quads), la provenance en 5.1 (INSEE/IGN), la confiance en 4.1 et 4.3 (le seuil 0.60), la temporalité en 5.2 (le parcours de Marie), la fusion en 5.3 (les trois populations de Lyon). Un concept qu’on ne sait pas relier à une sortie de cellule n’est pas encore acquis.

La ligne « Quoted Triple » du tableau porte la nuance la plus importante à emporter : RDF-Star n’est pas « une meilleure réification », c’est un changement de statut — le triplet devient une valeur du langage. La réification décrit un triplet avec quatre autres ; le quoted triple le cite directement. Toute la différence de coût mesurée dans ce notebook (4+ contre 0 par annotation) vient de là.

Exercices

Les exemples guides 1 a 3 ci-dessus (critiques de film, requetage SPARQL des reifications, graphe de confiance) demontrent les briques ; les exercices 4 a 6 qui suivent reclament de les transposer a des domaines nouveaux :

Exercice Domaine Brique exercee Guide correspondant
4 : Donnees sportives multi-sources Scores de matchs Reification + confiance par source Exemple guide 1
5 : Agregats SPARQL sur scores Statistiques Requetes sur annotations Exemple guide 2
6 : Annotations temporelles historiques Evenements dates Validite temporelle d’un fait Section 5.2

Chaque exercice a sa cellule guidee immédiatement au-dessus : en cas de blocage, etudiez le guide, puis revenez a la version a completer.

La progression n’est pas décorative : chaque exercice reprend une brique démontrée dans le guide de même colonne et la déplace dans un domaine où les valeurs, les sources et les contraintes changent. Transposer, ici, veut dire : identifier ce qui est invariant (le motif reify() + annotations + requête) et ce qui est à réinventer (le vocabulaire du domaine, la politique de seuil, la clé de regroupement).

En cas de blocage, la méthode donnée en fin de cellule vaut pour les trois : lire la solution guide en entier d’abord, la faire tourner, puis revenir à l’énoncé et n’en reprendre que la structure — pas les littéraux. Un exercice « résolu » en copiant les valeurs du guide n’a rien transposé.


Le notebook suivant explore les graphes de connaissances (Knowledge Graphs), en combinant les technologies RDF, SPARQL et les patterns vus dans cette serie.


Navigation : << 9-JSONLD | Index | 11-KnowledgeGraphs >>


Exemples guides (solutions proposees par @Sosolalt)

Les exemples ci-dessous ont ete resolus par @Sosolalt (EPITA-IS, promo 2028). Ils servent de modèle pour comprendre les concepts abordes dans ce notebook.

Exemple guide 4 : Données sportives multi-sources

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

La solution montre le morceau de SPARQL qui manquait au notebook jusqu’ici : l’agrégat COUNT(DISTINCT ?source) combiné à GROUP BY ?match ?score et HAVING (>= 2). C’est ce qui transforme le graphe annoté en système de vérification croisée : match1 (2 sources) et match3 (3 sources) sont confirmés, match2 (une seule source) reste non confirmé — le graphe des 48 triplets répond à une question que ni les faits ni les annotations ne contiennent individuellement.

Notez le DISTINCT dans le comptage : sans lui, deux affirmations de la même source compteraient double. La confirmation exige des sources différentes — subtilité classique des systèmes de vote par corroboration.

# Exercice 4 : Donnees sportives multi-sources
from rdflib import Graph, Namespace, Literal
from rdflib.namespace import RDF, XSD

EX = Namespace("http://example.org/")

g_sport = Graph()
g_sport.bind("ex", EX)
g_sport.bind("rdf", RDF)

# Helper : declare un match (homeTeam / awayTeam) et un score affirme par une source
def add_score_claim(match, home, away, score, source, confidence, date):
    g_sport.add((match, EX.homeTeam, Literal(home)))
    g_sport.add((match, EX.awayTeam, Literal(away)))
    # Le score est l'assertion annotée par source -> réification
    stmt = reify(g_sport, match, EX.score, Literal(score))
    g_sport.add((stmt, EX.source, Literal(source)))
    g_sport.add((stmt, EX.confidence, Literal(confidence, datatype=XSD.double)))
    g_sport.add((stmt, EX.date, Literal(date, datatype=XSD.date)))

# Match 1 : confirme par 2 sources (meme score 3-1)
add_score_claim(EX.match1, "PSG", "OM", "3-1", "L'Equipe", 0.90, "2024-03-10")
add_score_claim(EX.match1, "PSG", "OM", "3-1", "Le Monde", 0.85, "2024-03-10")

# Match 2 : une seule source -> non confirme
add_score_claim(EX.match2, "Lyon", "Nice", "2-2", "L'Equipe", 0.80, "2024-03-11")

# Match 3 : confirme par 3 sources (meme score 0-1)
add_score_claim(EX.match3, "Lille", "Lens", "0-1", "L'Equipe", 0.70, "2024-03-12")
add_score_claim(EX.match3, "Lille", "Lens", "0-1", "Le Monde", 0.75, "2024-03-12")
add_score_claim(EX.match3, "Lille", "Lens", "0-1", "Foot Mercato", 0.60, "2024-03-12")

print(f"Graphe sportif : {len(g_sport)} triplets\n")

# Requête : scores confirmes par au moins 2 sources différentes
query_confirmed = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>

SELECT ?match ?score (COUNT(DISTINCT ?source) AS ?nbSources)
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?match ;
          rdf:predicate ex:score ;
          rdf:object ?score ;
          ex:source ?source .
}
GROUP BY ?match ?score
HAVING (COUNT(DISTINCT ?source) >= 2)
ORDER BY ?match
"""

print("=== Scores confirmes par >= 2 sources ===")
for row in g_sport.query(query_confirmed):
    match = str(row.match).split("/")[-1]
    print(f"  {match} : score {row.score} (confirme par {row.nbSources} sources)")
Graphe sportif : 48 triplets

=== Scores confirmes par >= 2 sources ===
  match1 : score 3-1 (confirme par 2 sources)
  match3 : score 0-1 (confirme par 3 sources)

Exemple guide 5 : Agregats SPARQL sur scores de confiance

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

Les trois requêtes de la solution couvrent le triptyque des agrégats SPARQL sur annotations : AVG(?conf) (moyenne 0.587 sur les 4 relations du graphe 4.1 — vérifiable à la main : (0.95 + 0.60 + 0.30 + 0.50) / 4), ORDER BY ASC(?since) LIMIT 1 pour l’ancienneté (Alice-Bob, 2019), et GROUP BY ?source pour le décompte par provenance (LinkedIn, Twitter, Rumeur : 1 chacune — le fait 4 n’a pas de source, il n’apparaît donc dans aucun groupe).

La leçon de structure : les agrégats s’appliquent aux annotations, pas aux faits — on ne moyenne pas des personnes, on moyenne des confiances. Le Statement est le point d’ancrage qui rend chaque métadonnée adressable en colonne d’agrégation.

# Exercice 5 : Agrégats SPARQL sur scores de confiance (graphe g_kg de la section 4)

# 1. Moyenne des scores de confiance de toutes les relations foaf:knows
query_avg = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

SELECT (AVG(?conf) AS ?moyenne) (COUNT(?stmt) AS ?nbRelations)
WHERE {
    ?stmt a rdf:Statement ;
          rdf:predicate foaf:knows ;
          ex:confidence ?conf .
}
"""

print("=== 1. Moyenne des scores de confiance (foaf:knows) ===")
for row in g_kg.query(query_avg):
    print(f"  Moyenne = {float(row.moyenne):.3f} sur {int(row.nbRelations)} relations")

# 2. Relation la plus ancienne (date ex:since minimale)
query_oldest = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

SELECT ?p1 ?p2 ?since
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?p1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?p2 ;
          ex:since ?since .
}
ORDER BY ASC(?since)
LIMIT 1
"""

print("\n=== 2. Relation la plus ancienne ===")
for row in g_kg.query(query_oldest):
    p1 = str(row.p1).split("/")[-1]
    p2 = str(row.p2).split("/")[-1]
    print(f"  {p1} connait {p2} depuis {row.since}")

# 3. Nombre de relations par source
query_by_source = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

SELECT ?source (COUNT(?stmt) AS ?nb)
WHERE {
    ?stmt a rdf:Statement ;
          rdf:predicate foaf:knows ;
          ex:source ?source .
}
GROUP BY ?source
ORDER BY DESC(?nb)
"""

print("\n=== 3. Nombre de relations par source ===")
for row in g_kg.query(query_by_source):
    print(f"  {str(row.source):<15} : {int(row.nb)} relation(s)")
=== 1. Moyenne des scores de confiance (foaf:knows) ===
  Moyenne = 0.587 sur 4 relations

=== 2. Relation la plus ancienne ===
  Alice connait Bob depuis 2019-03-01

=== 3. Nombre de relations par source ===
  LinkedIn        : 1 relation(s)
  Twitter         : 1 relation(s)
  Rumeur          : 1 relation(s)

Exemple guide 6 : Annotations temporelles sur événements historiques

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

La solution applique le pattern temporel de la section 5.2 à des événements historiques : trois dates de l’histoire de France, chacune assertée comme fait (ex:date) et réifiée avec sa source — le Sacre de Napoléon provient des Archives nationales, la chute du Mur de Wikipédia. La requête filtre par YEAR(?date) > 1800 : deux événements sur trois passent (1804, 1989), la Prise de la Bastille (1789) est exclue.

Le commentaire d’en-tête de la solution vaut citation : rdflib 7.6 ne supportant pas la syntaxe << >>, l’exercice « RDF-Star » se résout en réification — exactement la posture de tout le notebook. Le graphe fait 24 triplets ; notez le double emploi g.add((event, ex.date, ...)) puis reify(g, event, ex.date, ...) : le fait est asserté et annoté, la paire complète du motif asserté-mentionné.

# Exercice 6 : Annotations temporelles sur des événements historiques
# Note : rdflib 7.6 ne supporte pas la syntaxe RDF-Star native (<< s p o >>),
# on utilise donc la réification (fonction reify) comme dans tout le notebook.

from rdflib import Graph, Namespace, Literal, URIRef, BNode
from rdflib.namespace import RDF, XSD

ex = Namespace("http://example.org/")

g = Graph()
g.bind("ex", ex)

# Étape 2 : 3 événements historiques avec annotations temporelles (date + source)
evenements = [
    (ex.priseBastille, "Prise de la Bastille", "1789-07-14", "Encyclopedie Larousse"),
    (ex.sacreNapoleon, "Sacre de Napoleon Ier", "1804-12-02", "Archives nationales"),
    (ex.chuteMurBerlin, "Chute du mur de Berlin", "1989-11-09", "Wikipedia"),
]

for event, label, date, source in evenements:
    g.add((event, RDF.type, ex.EvenementHistorique))
    g.add((event, ex.label, Literal(label)))
    # Le fait date est asserte puis annote (date + source) par réification
    g.add((event, ex.date, Literal(date, datatype=XSD.date)))
    stmt = reify(g, event, ex.date, Literal(date, datatype=XSD.date))
    g.add((stmt, ex.source, Literal(source)))

print(f"Graphe d'evenements : {len(g)} triplets\n")

# Étape 3 : requête SPARQL filtrant les événements postérieurs a l'an 1800
query = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

SELECT ?label ?date ?source
WHERE {
    ?event ex:label ?label ;
           ex:date ?date .
    ?stmt a rdf:Statement ;
          rdf:subject ?event ;
          rdf:predicate ex:date ;
          rdf:object ?date ;
          ex:source ?source .
    FILTER (YEAR(?date) > 1800)
}
ORDER BY ?date
"""

results = g.query(query)

# Étape 4 : afficher les résultats
print("=== Evenements posterieurs a 1800 ===")
for row in results:
    print(f"  {str(row.date)} - {row.label} (source : {row.source})")
Graphe d'evenements : 24 triplets

=== Evenements posterieurs a 1800 ===
  1804-12-02 - Sacre de Napoleon Ier (source : Archives nationales)
  1989-11-09 - Chute du mur de Berlin (source : Wikipedia)

Exercices a completer

Ces exercices sont a realiser par l’etudiant.

Exercice 4 : Annoter des données sportives multi-sources

Créez un graphe RDF representant les scores de 3 matchs de football provenant de sources différentes. Pour chaque match, ajoutez : - ex:homeTeam, ex:awayTeam, ex:score (ex: “3-1”) - Via reification : ex:source (journal), ex:confidence, ex:date

Ecrivez ensuite une requête SPARQL selectionnant uniquement les scores confirmes par au moins 2 sources.

Indice : Chaque source produit un rdf:Statement différent pour le même match. Utilisez GROUP BY et HAVING(COUNT(?source) >= 2).

Méthode conseillée : commencez par écrire un helper à la add_score_claim(match, home, away, score, source, confidence, date) sur le modèle de la section 5.3 — trois appels de fonction par match suffisent ensuite à construire le graphe. Pour la requête, repartez du motif de réification complet (les trois composants plus ex:source), puis ajoutez le groupement GROUP BY ?match ?score — grouper sur le couple évite de fusionner deux scores différents du même match — et la clause HAVING (COUNT(DISTINCT ?source) >= 2).

Piège à éviter : compter les Statements au lieu des sources distinctes — deux affirmations du même journal doivent confirmer moins qu’une seule. Le guide 4 ci-dessus résout exactement ce cas ; comparez votre HAVING au sien une fois votre version écrite.

# Exercice 4 : Donnees sportives multi-sources
from rdflib import Graph, Namespace, Literal
from rdflib.namespace import RDF, XSD

# TODO étudiant : Créez le graphe avec matchs, scores et sources multiples
# Puis écrivez une requête SPARQL pour trouver les scores confirmes par >= 2 sources

print("Exercice a completer")
Exercice a completer

Exercice 5 : Agregats SPARQL sur scores de confiance

En reprenant le graphe g_kg de la section 4, ecrivez des requêtes SPARQL pour : 1. Calculer la moyenne des scores de confiance de toutes les relations foaf:knows 2. Trouver la relation la plus ancienne (date minimale) 3. Compter le nombre de relations par source (regroupement)

Indice : Utilisez AVG(), MIN(), COUNT() et GROUP BY.

Les trois requêtes sont indépendantes — traitez-les dans l’ordre du cahier des charges. La moyenne demande SELECT (AVG(?conf) AS ?moyenne) (COUNT(?stmt) AS ?nb) sur le motif de réification restreint à rdf:predicate foaf:knows ; l’ancienneté se résout avec ORDER BY ASC(?since) LIMIT 1 (inutile de chercher le minimum à la main, le tri + limite le fait) ; le décompte par source est un GROUP BY ?source ORDER BY DESC(?nb).

Vérification croisée conseillée : la moyenne doit tomber entre la confiance minimale et maximale du graphe (bornes 0.30 et 0.95 dans la section 4.1), et la somme des relations par source ne doit pas dépasser le nombre total de relations sourcées — le fait sans source n’entre dans aucun groupe. Ces deux contrôles prennent dix secondes et attrapent la majorité des erreurs de motif.

# Exercice 5 : Agrégats SPARQL sur scores de confiance

# TODO étudiant : Écrivez 3 requêtes SPARQL avec agrégats sur g_kg
# 1. Moyenne des confiances
# 2. Relation la plus ancienne
# 3. Nombre de relations par source

print("Exercice a completer")
Exercice a completer

Exercice 6 : Annotations temporelles sur des événements historiques

Les sections 5.2 et 5.3 ont montre les annotations temporelles et la fusion multi-sources. Créez un graphe RDF-Star qui annote des événements historiques avec des dates et sources.

Objectifs

  1. Construire un graphe avec des événements et leurs annotations temporelles
  2. Utiliser RDF-Star pour attacher date et source a chaque événement
  3. Requête SPARQL pour filtrer les événements après une date donnee

Indices :

  • Reprenez le pattern de la section 5.2 : g.add((s, p, o, context)) pour les triplets annotes
  • <<ex:event1 ex:aLieu "1789-07-14"^^xsd:date>> en Turtle-Star
  • FILTER(YEAR(?date) > 1800) pour le filtrage temporel

Contrairement à l’intitulé, ne cherchez pas la syntaxe Turtle-Star — rdflib 7.6 ne la parse pas (la solution du guide 6 l’explique en tête de cellule). Construisez en réification pure : pour chaque événement, g.add((event, ex.date, Literal(...))) pour le fait, reify(g, event, ex.date, ...) pour la mention, puis g.add((stmt, ex.source, ...)). Le filtre final est un FILTER (YEAR(?date) > 1800) sur la date du fait — YEAR() extrait l’année d’un littéral daté typé.

Simplification assumée par rapport à la section 5.2 : une seule annotation par événement (la source) suffit pour l’énoncé ; ajouter la date de saisie ou une méthode vous entraînerait vers PROV — extension naturelle si vous finissez tôt.

# Exercice 6 : Annotations temporelles sur des événements historiques
# TODO étudiant : créez un graphe RDF-Star d'événements historiques annotés
#
# Étape 1 : importer rdflib et définir les namespaces
#   from rdflib import Graph, Namespace, Literal, URIRef, BNode
#   from rdflib.namespace import XSD
# Étape 2 : créer un Graph et ajouter 3 événements avec annotations temporelles
#   ex = Namespace("http://example.org/")
#   Utiliser RDF-Star pour attacher date et source a chaque triplet événement
# Étape 3 : écrire une requête SPARQL-Star pour filtrer les événements après 1800
# Étape 4 : afficher les résultats

g = None  # TODO étudiant : remplacer par le Graph construit
results = None  # TODO étudiant : remplacer par les résultats de la requête
print("Exercice a completer")
Exercice a completer

Conclusion

Ce parcours a demontre une seule chose sous six angles : parler d’un triplet — l’annoter, le dater, le sourcer, le nuancer — demande une machinerie que le RDF classique n’offre qu’au prix de la reification (4 triplets de structure par fait annote), et que RDF-Star promet d’obtenir nativement.

Concept demontre Avec la reification classique Ce que RDF-Star changera
Annoter un fait (confiance, source, date) 4 + N triplets, existence non garantie 1 + N, assertion configurable
Imbriquer les croyances (Bob croit qu’Alice rapporte…) Cascade de BNode illisible << << ... >> >>
Requeter les metadonnees Motif rdf:Statement a deplier Motif direct sur triplets quotés
Grouper par contexte Graphes nommes Dataset Complementaire (les deux se combinent)

La leçon methodologique independante de l’outillage : un graphe de connaissances utile n’est pas un sac de faits vrais, c’est un sac de faits qualifies — par confiance, provenance, temporalite. Le mecanisme exact (reification aujourd’hui, RDF-Star demain) est un detail d’implementation ; les dimensions d’annotation, elles, sont la competence transferable vers SW-12 (GraphRAG) et tout pipeline d’extraction d’information.

Le fil des mesures du notebook raconte la même histoire que ce tableau : 7 triplets pour un fait annoté, 12 pour une croyance rapportée, 37 pour un réseau social de quatre personnes — la réification facture invariablement ses 4 triplets de structure par fait, quel que soit le domaine. Les graphes nommés (section 6) n’ont facturé que 2 URI de contexte pour le même réseau. RDF-Star, lui, n’aurait rien facturé du tout : sur les trois exemples types (confiance, provenance, temporalité), chaque bloc de code du notebook a son équivalent en une à trois lignes Turtle-Star — posées en regard dans chaque interprétation.

Ce qui doit survivre à la lecture de ce notebook n’est pourtant ni la syntaxe << ... >>, ni le motif rdf:Statement — l’un n’est pas encore exécutable, l’autre est en voie d’obsolescence. Ce sont les quatre questions d’annotation que chaque mécanisme a permises : qui affirme (provenance), à quel degré (confiance), de quand à quand (temporalité), confirmé par qui d’autre (fusion). Les poser avant de choisir l’outillage — et exiger d’un candidat mécanisme qu’il y réponde à un coût mesurable — est la compétence qui restera vraie quand RDF 1.2 sera déployé partout, et après.

Retour au sommet