SW-6b-Python-RDFS

Navigation : Index | << SW-6 C# | SW-7b Python OWL >>

RDFS en Python avec rdflib + owlrl : Schema et Inference

Ce notebook est un sidetrack optionnel qui presente l’equivalent Python des concepts RDFS du notebook SW-6 (dotNetRDF). Vous y decouvrirez comment rdflib (construction de schemas, parsing Turtle, requêtes SPARQL) et owlrl (vrai raisonneur RDFS) cooperent pour reproduire en Python le travail du StaticRdfsReasoner de dotNetRDF.

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Construire un schema RDFS en Python avec rdflib (rdfs:Class, rdfs:subClassOf, rdfs:domain, rdfs:range) 2. Charger un fichier Turtle existant (data/animals.ttl) et explorer sa hiérarchie de classes 3. Appliquer un vrai raisonneur RDFS (owlrl) pour materialiser les triplets inferes 4. Interroger les données enrichies par SPARQL 5. Faire la correspondance entre dotNetRDF (StaticRdfsReasoner) et rdflib + owlrl (DeductiveClosure)

Prerequis

  • SW-6-CSharp-RDFS recommande (pour la comprehension conceptuelle)
  • Python 3.10+
  • Notebooks SW-2b et SW-4b (bases rdflib + SPARQL en Python)

Duree estimee : 40 minutes

Note : owlrl est le raisonneur RDFS/OWL de reference en Python pur (implementation des règles d’entailment W3C RDF 1.1 Semantics). Nous ne reinventerons pas l’inference a la main dans les cellules de production : une cellule de demonstration pedagogique expose le mécanisme (point fixe), puis owlrl prend le relais pour le travail reel.


1. Installation et imports

rdflib est le coeur Python du Web Sémantique (equivalent de dotNetRDF). owlrl est le raisonneur RDFS/OWL-RL qui materialise les triplets inferes (equivalent du StaticRdfsReasoner de dotNetRDF).

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

Import des bibliotheques. Les namespaces W3C (RDF, RDFS, XSD) sont preconfigures dans rdflib, ce qui evite de manipuler les URIs completes a la main.

try:
    from rdflib import Graph, URIRef, Literal, BNode, Namespace
    from rdflib.namespace import RDF, RDFS, XSD
    import owlrl

    LIBS_AVAILABLE = True
    print("Imports rdflib + owlrl prets.")
    print(f"rdflib version : {__import__('rdflib').__version__}")
    print(f"owlrl version  : {owlrl.__version__}")
    print()
    print("Namespaces W3C preconfigures :")
    print(f"  RDF.type       = {RDF.type}")
    print(f"  RDFS.subClassOf= {RDFS.subClassOf}")
    print(f"  RDFS.domain    = {RDFS.domain}")
    print(f"  RDFS.range     = {RDFS.range}")
    print(f"  RDFS.label     = {RDFS.label}")
except ImportError as e:
    LIBS_AVAILABLE = False
    print(f"Bibliotheque manquante : {e}")
    print("Installez avec : pip install rdflib owlrl")
Imports rdflib + owlrl prets.
rdflib version : 7.6.0
owlrl version  : 7.1.4

Namespaces W3C preconfigures :
  RDF.type       = http://www.w3.org/1999/02/22-rdf-syntax-ns#type
  RDFS.subClassOf= http://www.w3.org/2000/01/rdf-schema#subClassOf
  RDFS.domain    = http://www.w3.org/2000/01/rdf-schema#domain
  RDFS.range     = http://www.w3.org/2000/01/rdf-schema#range
  RDFS.label     = http://www.w3.org/2000/01/rdf-schema#label

Interpretation : imports

Symbole rdflib Equivalent dotNetRDF
Graph IGraph / Graph
URIRef IUriNode
Literal ILiteralNode
RDF.type rdf:type (URI complete)
RDFS.subClassOf rdfs:subClassOf
owlrl.DeductiveClosure StaticRdfsReasoner

Points cles : 1. rdflib expose les namespaces W3C comme attributs Python (RDFS.subClassOf au lieu de l’URI complete http://www.w3.org/2000/01/rdf-schema#subClassOf). 2. owlrl est un module separe de rdflib : on l’invoque explicitement quand on veut du raisonnement. C’est une différence philosophique avec dotNetRDF ou le raisonneur est integre.


2. Qu’est-ce que RDFS ?

RDF Schema (RDFS) est une extension de RDF qui fournit un vocabulaire pour decrire la structure des données RDF. Tandis que RDF permet d’exprimer des faits sous forme de triplets (sujet, predicat, objet), RDFS ajoute la capacite de définir :

RDF Schema (RDFS) est standardise par le W3C dans RDF Schema 1.1 (Brickley & Guha, W3C Recommendation 2014). C’est une extension de RDF qui fournit un vocabulaire pour decrire la structure des données RDF – classes, hiérarchies, domaines/portees de proprietes et règles d’inference.

  • Des classes et des hiérarchies de classes
  • Des proprietes avec domaine et portee
  • Des règles d’inference implicites

RDFS dans la pile du Web Sémantique

flowchart TD
    OWL["OWL (SW-7) - Logique descriptive"] --> RDFS["RDFS (SW-6) - Vocabulaires, hierarchies - NOUS SOMMES ICI"]
    RDFS --> SPARQL["SPARQL (SW-4/5) - Interrogation"]
    SPARQL --> RDF["RDF (SW-1/2/3) - Triplets, graphes"]
    RDF --> URI["URI / IRI - Identifiants"]
    style RDFS font-weight:bold

RDF seul vs RDFS

Capacite RDF seul RDF + RDFS
Exprimer des faits (triplets) Oui Oui
Définir des classes Non rdfs:Class
Hiérarchies de classes Non rdfs:subClassOf
Hiérarchies de proprietes Non rdfs:subPropertyOf
Contraindre domaine/portee Non rdfs:domain, rdfs:range
Inference automatique Non Oui (règles RDFS)
Documentation Limite rdfs:label, rdfs:comment

Vocabulaire RDFS complet

Le namespace RDFS est http://www.w3.org/2000/01/rdf-schema#. Voici les termes essentiels :

Terme RDFS Type Description
rdfs:Class Classe Définit une classe (un ensemble de ressources)
rdfs:subClassOf Propriete A est une sous-classe de B (A \(\subseteq\) B)
rdfs:subPropertyOf Propriete P1 est une sous-propriete de P2
rdfs:domain Propriete Le sujet d’un triplet avec P est de type C
rdfs:range Propriete L’objet d’un triplet avec P est de type C
rdfs:label Propriete Etiquette lisible par un humain
rdfs:comment Propriete Description textuelle
rdfs:Resource Classe Superclasse de toutes les ressources
rdfs:Literal Classe Classe des valeurs litterales
rdfs:Datatype Classe Classe des types de données
rdfs:seeAlso Propriete Lien vers une ressource connexe
rdfs:isDefinedBy Propriete Lien vers la definition de la ressource

3. Construire un schema RDFS en code

Construisons une taxonomie animale en Python avec rdflib. Nous allons créer la hiérarchie suivante :

  Animal
  |-- Mammal (Mammifere)
  |   |-- Dog (Chien)
  |   +-- Cat (Chat)
  +-- Bird (Oiseau)
      +-- Parrot (Perroquet)

La même taxonomie, rendue sous forme de graphe : chaque fleche rdfs:subClassOf pointe de la sous-classe vers sa super-classe (sens de lecture « est une sorte de »).

graph BT
    Dog["ex:Dog Chien"]
    Cat["ex:Cat Chat"]
    Parrot["ex:Parrot Perroquet"]
    Mammal["ex:Mammal Mammifere"]
    Bird["ex:Bird Oiseau"]
    Animal["ex:Animal"]
    Dog -->|rdfs:subClassOf| Mammal
    Cat -->|rdfs:subClassOf| Mammal
    Parrot -->|rdfs:subClassOf| Bird
    Mammal -->|rdfs:subClassOf| Animal
    Bird -->|rdfs:subClassOf| Animal
    classDef root fill:#cfe2ff,stroke:#084298,color:#052c65
    classDef mid fill:#d1e7dd,stroke:#0f5132,color:#052e16
    classDef leaf fill:#fff3cd,stroke:#b8860b,color:#5c4400
    class Animal root
    class Mammal,Bird mid
    class Dog,Cat,Parrot leaf

Lecture. Les 5 relations rdfs:subClassOf (Dog/Cat < Mammal, Parrot < Bird, Mammal/Bird < Animal) forment un arbre d’heritage. RDFS garantit que cette relation est transitive : Dog <: Mammal et Mammal <: Animal impliquent Dog <: Animal. C’est cette transitivite que le raisonneur exploite pour typer les instances par toutes leurs classes ancetres.

if LIBS_AVAILABLE:
    # Construire un schema RDFS en Python avec rdflib
    schema = Graph()

    # Namespace personnalise pour notre taxonomie animale
    EX = Namespace("http://example.org/animals#")
    schema.bind("ex", EX)
    schema.bind("rdfs", RDFS)
    schema.bind("rdf", RDF)
    schema.bind("xsd", XSD)

    # --- Definition des classes ---
    animal = EX.Animal
    mammal = EX.Mammal
    bird = EX.Bird
    dog = EX.Dog
    cat = EX.Cat
    parrot = EX.Parrot

    # Declarer les classes comme instances de rdfs:Class
    for cls in (animal, mammal, bird, dog, cat, parrot):
        schema.add((cls, RDF.type, RDFS.Class))

    # Hierarchie de classes (rdfs:subClassOf)
    schema.add((mammal, RDFS.subClassOf, animal))
    schema.add((bird,   RDFS.subClassOf, animal))
    schema.add((dog,    RDFS.subClassOf, mammal))
    schema.add((cat,    RDFS.subClassOf, mammal))
    schema.add((parrot, RDFS.subClassOf, bird))

    # Etiquettes (rdfs:label) en francais
    schema.add((animal, RDFS.label, Literal("Animal", lang="fr")))
    schema.add((mammal, RDFS.label, Literal("Mammifere", lang="fr")))
    schema.add((bird,   RDFS.label, Literal("Oiseau", lang="fr")))
    schema.add((dog,    RDFS.label, Literal("Chien", lang="fr")))
    schema.add((cat,    RDFS.label, Literal("Chat", lang="fr")))
    schema.add((parrot, RDFS.label, Literal("Perroquet", lang="fr")))

    # Commentaire sur la classe racine
    schema.add((animal, RDFS.comment, Literal("Classe racine de tous les animaux", lang="fr")))

    print(f"Schema RDFS cree : {len(schema)} triplets")
    print()
    print("Hierarchie de classes :")
    for s, _, o in schema.triples((None, RDFS.subClassOf, None)):
        child = str(s).split("#")[-1]
        parent = str(o).split("#")[-1]
        print(f"  {child} rdfs:subClassOf {parent}")
else:
    print("Bibliotheques non disponibles : construction du schema ignoree.")
Schema RDFS cree : 18 triplets

Hierarchie de classes :
  Mammal rdfs:subClassOf Animal
  Bird rdfs:subClassOf Animal
  Dog rdfs:subClassOf Mammal
  Cat rdfs:subClassOf Mammal
  Parrot rdfs:subClassOf Bird

Interpretation : Construction du schema

Nous avons créé un schema RDFS complet avec : - 6 classes organisees en hiérarchie - 5 relations rdfs:subClassOf definissant l’arbre taxonomique - 6 etiquettes rdfs:label en francais - 1 commentaire rdfs:comment sur la classe racine

Opération rdflib (Python) dotNetRDF (C#)
Créer un graphe g = Graph() IGraph g = new Graph()
Ajouter un triplet g.add((s, p, o)) g.Assert(new Triple(s, p, o))
Litteral avec langue Literal("Animal", lang="fr") g.CreateLiteralNode("Animal", "fr")
Namespace g.bind("ex", EX) g.NamespaceMap.AddNamespace("ex", uri)

Points cles : 1. Chaque classe est declaree comme instance de rdfs:Class via rdf:type 2. rdfs:subClassOf est transitif : Dog < Mammal < Animal, donc Dog < Animal 3. Les etiquettes multilingues utilisent les tags de langue (lang="fr", lang="en", etc.)

Ajout de proprietes avec domaine et portee

Definissons des proprietes pour notre taxonomie, avec rdfs:domain (type du sujet) et rdfs:range (type de l’objet/valeur).

if LIBS_AVAILABLE:
    # Proprietes avec domaine et portee
    name_prop   = EX.name
    age_prop    = EX.age
    sound_prop  = EX.sound
    canfly_prop = EX.canFly

    # ex:name - propriete avec domaine Animal, portee xsd:string
    schema.add((name_prop,   RDF.type, RDF.Property))
    schema.add((name_prop,   RDFS.domain, animal))
    schema.add((name_prop,   RDFS.range, XSD.string))
    schema.add((name_prop,   RDFS.label, Literal("nom", lang="fr")))

    # ex:age - propriete avec domaine Animal, portee xsd:integer
    schema.add((age_prop,    RDF.type, RDF.Property))
    schema.add((age_prop,    RDFS.domain, animal))
    schema.add((age_prop,    RDFS.range, XSD.integer))
    schema.add((age_prop,    RDFS.label, Literal("age", lang="fr")))

    # ex:sound - propriete avec domaine Animal, portee xsd:string
    schema.add((sound_prop,  RDF.type, RDF.Property))
    schema.add((sound_prop,  RDFS.domain, animal))
    schema.add((sound_prop,  RDFS.range, XSD.string))
    schema.add((sound_prop,  RDFS.label, Literal("cri", lang="fr")))

    # ex:canFly - propriete avec domaine Animal, portee xsd:boolean
    schema.add((canfly_prop, RDF.type, RDF.Property))
    schema.add((canfly_prop, RDFS.domain, animal))
    schema.add((canfly_prop, RDFS.range, XSD.boolean))
    schema.add((canfly_prop, RDFS.label, Literal("peut voler", lang="fr")))

    print(f"Schema enrichi : {len(schema)} triplets")
    print()
    print("Proprietes definies :")
    for p in (name_prop, age_prop, sound_prop, canfly_prop):
        name = str(p).split("#")[-1]
        domain = str(next(schema.objects(p, RDFS.domain))).split("#")[-1]
        rng = str(next(schema.objects(p, RDFS.range))).split("#")[-1]
        print(f"  ex:{name:8s}  domain={domain:8s}  range={rng}")
else:
    print("Bibliotheques non disponibles : proprietes ignorees.")
Schema enrichi : 34 triplets

Proprietes definies :
  ex:name      domain=Animal    range=string
  ex:age       domain=Animal    range=integer
  ex:sound     domain=Animal    range=string
  ex:canFly    domain=Animal    range=boolean

Interpretation : Domaine et portee

Propriete Domaine Portee Signification
ex:name ex:Animal xsd:string Le nom est une chaîne, applicable a tout Animal
ex:age ex:Animal xsd:integer L’age est un entier
ex:sound ex:Animal xsd:string Le cri est une chaîne
ex:canFly ex:Animal xsd:boolean Capacite de vol, vrai/faux

Consequence de rdfs:domain : si on ecrit ex:rex ex:name "Rex", alors un raisonneur RDFS peut inferer que ex:rex rdf:type ex:Animal (car ex:name a pour domaine ex:Animal).

Attention : rdfs:domain et rdfs:range ne sont pas des contraintes de validation (comme en SQL). Ce sont des règles d’inference. Si le domaine est viole, RDFS ne signale pas d’erreur – il infere un type supplementaire.


4. Charger et explorer animals.ttl

Le fichier data/animals.ttl contient la hiérarchie complete avec des instances. Chargeons-le avec le parser Turtle de rdflib et explorons sa structure.

if LIBS_AVAILABLE:
    # Charger le fichier animals.ttl avec rdflib
    animals = Graph()
    animals.parse("data/animals.ttl", format="turtle")

    print(f"Graphe charge : {len(animals)} triplets")
    print()
    print("Namespaces declares :")
    for prefix, ns in animals.namespaces():
        print(f"  {prefix:6s} -> {ns}")
    print()

    # Lister toutes les classes (sujets de type rdfs:Class)
    print("Classes definies :")
    for cls in animals.subjects(RDF.type, RDFS.Class):
        name = str(cls).split("#")[-1]
        print(f"  - {name}")
else:
    print("Bibliotheques non disponibles : chargement ignore.")
Graphe charge : 51 triplets

Namespaces declares :
  brick  -> https://brickschema.org/schema/Brick#
  csvw   -> http://www.w3.org/ns/csvw#
  dc     -> http://purl.org/dc/elements/1.1/
  dcat   -> http://www.w3.org/ns/dcat#
  dcmitype -> http://purl.org/dc/dcmitype/
  dcterms -> http://purl.org/dc/terms/
  dcam   -> http://purl.org/dc/dcam/
  doap   -> http://usefulinc.com/ns/doap#
  foaf   -> http://xmlns.com/foaf/0.1/
  geo    -> http://www.opengis.net/ont/geosparql#
  odrl   -> http://www.w3.org/ns/odrl/2/
  org    -> http://www.w3.org/ns/org#
  prof   -> http://www.w3.org/ns/dx/prof/
  prov   -> http://www.w3.org/ns/prov#
  qb     -> http://purl.org/linked-data/cube#
  schema -> https://schema.org/
  sh     -> http://www.w3.org/ns/shacl#
  skos   -> http://www.w3.org/2004/02/skos/core#
  sosa   -> http://www.w3.org/ns/sosa/
  ssn    -> http://www.w3.org/ns/ssn/
  time   -> http://www.w3.org/2006/time#
  vann   -> http://purl.org/vocab/vann/
  void   -> http://rdfs.org/ns/void#
  wgs    -> https://www.w3.org/2003/01/geo/wgs84_pos#
  owl    -> http://www.w3.org/2002/07/owl#
  rdf    -> http://www.w3.org/1999/02/22-rdf-syntax-ns#
  rdfs   -> http://www.w3.org/2000/01/rdf-schema#
  xsd    -> http://www.w3.org/2001/XMLSchema#
  xml    -> http://www.w3.org/XML/1998/namespace
  ex     -> http://example.org/animals#

Classes definies :
  - Animal
  - Mammal
  - Bird
  - Dog
  - Cat
  - Parrot

Explorons maintenant la hiérarchie de classes et les instances declarees (avant toute inference).

if LIBS_AVAILABLE:
    # Explorer la hierarchie de classes
    print("Hierarchie de classes (rdfs:subClassOf) :")
    for s, _, o in animals.triples((None, RDFS.subClassOf, None)):
        child = str(s).split("#")[-1]
        parent = str(o).split("#")[-1]
        print(f"  {child} --> {parent}")
    print()

    # Lister les instances par classe (uniquement les classes "feuilles")
    print("Instances par classe (avant inference) :")
    for cls_name in ("Dog", "Cat", "Parrot"):
        cls_uri = URIRef(f"http://example.org/animals#{cls_name}")
        instances = sorted(str(s).split("#")[-1] for s in animals.subjects(RDF.type, cls_uri))
        print(f"  {cls_name} : {instances}")
else:
    print("Bibliotheques non disponibles : exploration ignoree.")
Hierarchie de classes (rdfs:subClassOf) :
  Mammal --> Animal
  Bird --> Animal
  Dog --> Mammal
  Cat --> Mammal
  Parrot --> Bird

Instances par classe (avant inference) :
  Dog : ['buddy', 'rex']
  Cat : ['minou']
  Parrot : ['coco']

Interpretation : Structure du fichier animals.ttl

Le fichier Turtle contient :

Élément Quantite Exemples
Classes 6 Animal, Mammal, Bird, Dog, Cat, Parrot
Proprietes 4 name, age, sound, canFly
Instances 4 rex (Dog), minou (Cat), coco (Parrot), buddy (Dog)
Relations subClassOf 5 Dog < Mammal < Animal, etc.

Observation importante : Les instances sont typees avec leur classe la plus spécifique (ex: ex:rex a ex:Dog), mais pas avec les classes parentes. Un raisonneur RDFS deduira que Rex est aussi un Mammal et un Animal.


5. Inference RDFS

L’inference RDFS permet de deduire automatiquement de nouveaux triplets a partir des règles RDFS. Le mécanisme principal est la transitivite de rdfs:subClassOf.

Les règles d’inference RDFS presentees ci-dessous (rdfs2, rdfs3, rdfs5, rdfs9, rdfs11) sont les règles d’entailment formellement specifiees dans RDF 1.1 Semantics (Hayes & Patel-Schneider, W3C Recommendation 2014). Elles definissent quels triplets peuvent etre deduits a partir d’un graphe RDFS.

Règles d’inference RDFS essentielles

Règle Premisses Conclusion
rdfs9 (heritage de type) ?C rdfs:subClassOf ?D et ?X rdf:type ?C ?X rdf:type ?D
rdfs11 (transitivite) ?A rdfs:subClassOf ?B et ?B rdfs:subClassOf ?C ?A rdfs:subClassOf ?C
rdfs5 (sous-propriete) ?P rdfs:subPropertyOf ?Q et ?X ?P ?Y ?X ?Q ?Y
rdfs2 (domaine) ?P rdfs:domain ?C et ?X ?P ?Y ?X rdf:type ?C
rdfs3 (portee) ?P rdfs:range ?C et ?X ?P ?Y ?Y rdf:type ?C

Exemple concret

Avec nos données :

ex:Dog rdfs:subClassOf ex:Mammal .
ex:Mammal rdfs:subClassOf ex:Animal .
ex:rex rdf:type ex:Dog .

L’inference RDFS deduit :

ex:rex rdf:type ex:Mammal .         # Par rdfs9 (Dog < Mammal)
ex:rex rdf:type ex:Animal .         # Par rdfs9 (Mammal < Animal)
ex:Dog rdfs:subClassOf ex:Animal .  # Par rdfs11 (transitivite)

Demonstration : inference manuelle (pedagogique)

Implementons d’abord l’inference manuellement, a but pedagogique uniquement, pour bien comprendre le mécanisme du point fixe. En production, on utilise owlrl (section suivante) qui applique l’ensemble complet des règles W3C.

if LIBS_AVAILABLE:
    # Demonstration pedagogique : implementer manuellement la regle rdfs9
    # (heritage de type par rdfs:subClassOf) avec un point fixe.
    #
    # ATTENTION : cette cellule est pedagogique. Pour du raisonnement reel,
    # utilisez owlrl (cellule suivante) qui couvre l'ensemble des regles W3C.

    data = Graph()
    data.bind("ex", Namespace("http://example.org/animals#"))

    animal_n = URIRef("http://example.org/animals#Animal")
    mammal_n = URIRef("http://example.org/animals#Mammal")
    dog_n    = URIRef("http://example.org/animals#Dog")
    rex      = URIRef("http://example.org/animals#rex")

    # Schema
    data.add((dog_n,    RDFS.subClassOf, mammal_n))
    data.add((mammal_n, RDFS.subClassOf, animal_n))

    # Instance
    data.add((rex, RDF.type, dog_n))

    print("=== AVANT inference ===")
    print(f"Nombre de triplets : {len(data)}")
    print("Types de rex :")
    for _, _, o in data.triples((rex, RDF.type, None)):
        print(f"  rex rdf:type {str(o).split('#')[-1]}")
    print()

    # --- Inference manuelle : regle rdfs9 ---
    # Pour chaque "?X rdf:type ?C" et "?C rdfs:subClassOf ?D", ajouter "?X rdf:type ?D"
    iteration = 0
    while True:
        iteration += 1
        new_triples = []
        # Snapshot des types et sous-classes courantes
        for x, _, c in list(data.triples((None, RDF.type, None))):
            for _, _, d in data.triples((c, RDFS.subClassOf, None)):
                inferred = (x, RDF.type, d)
                if inferred not in data and inferred not in new_triples:
                    new_triples.append(inferred)

        if not new_triples:
            break

        for t in new_triples:
            data.add(t)

        print(f"Iteration {iteration} : {len(new_triples)} triplet(s) infere(s)")
        for s, _, o in new_triples:
            print(f"  + {str(s).split('#')[-1]} rdf:type {str(o).split('#')[-1]}")
    print()

    print("=== APRES inference ===")
    print(f"Nombre de triplets : {len(data)}")
    print("Types de rex :")
    for _, _, o in sorted(data.triples((rex, RDF.type, None)), key=lambda t: str(t[2])):
        print(f"  rex rdf:type {str(o).split('#')[-1]}")
else:
    print("Bibliotheques non disponibles : demonstration ignoree.")
=== AVANT inference ===
Nombre de triplets : 3
Types de rex :
  rex rdf:type Dog

Iteration 1 : 1 triplet(s) infere(s)
  + rex rdf:type Mammal
Iteration 2 : 1 triplet(s) infere(s)
  + rex rdf:type Animal

=== APRES inference ===
Nombre de triplets : 5
Types de rex :
  rex rdf:type Animal
  rex rdf:type Dog
  rex rdf:type Mammal

Interpretation : Inference manuelle

Sortie obtenue : A partir de 3 triplets initiaux, l’inference a deduit 2 triplets supplementaires.

Itération Triplet infere Règle appliquee
1 rex rdf:type Mammal rdfs9 : rex est Dog, Dog < Mammal
2 rex rdf:type Animal rdfs9 : rex est Mammal, Mammal < Animal

Points cles : 1. L’algorithme est un point fixe : on itere jusqu’a ce qu’aucun nouveau triplet ne soit infere 2. Chaque itération propage les types d’un niveau dans la hiérarchie 3. Le nombre d’itérations depend de la profondeur de la hiérarchie de classes

En pratique, on utilise owlrl plutot que cette implementation manuelle. Mais comprendre le mécanisme est essentiel : owlrl applique la même logique de point fixe, mais sur les 51 règles d’entailment W3C complet (rdfs2, rdfs3, rdfs5, rdfs7, rdfs9, rdfs11, etc.), pas seulement rdfs9.

La règle d’inférence rdfs9 (heritage de type par rdfs:subClassOf), rendue sous forme de graphe pour l’instance ex:rex. En trait plein : les 3 triplets assertes ; en tirete ambre : les 2 triplets inferes par transitivite.

graph BT
    rex["ex:rex (instance)"]
    Dog["ex:Dog"]
    Mammal["ex:Mammal"]
    Animal["ex:Animal"]
    rex -->|"rdf:type (asserte)"| Dog
    Dog -->|"rdfs:subClassOf"| Mammal
    Mammal -->|"rdfs:subClassOf"| Animal
    rex -.->|"rdf:type (infere)"| Mammal
    rex -.->|"rdf:type (infere)"| Animal
    classDef inst fill:#cfe2ff,stroke:#084298,color:#052c65
    classDef cls fill:#d1e7dd,stroke:#0f5132,color:#052e16
    class rex inst
    class Dog,Mammal,Animal cls
    linkStyle 3,4 stroke:#b8860b,stroke-width:2px

Lecture. Partant de 3 triplets (rex rdf:type Dog, Dog subClassOf Mammal, Mammal subClassOf Animal), la règle rdfs9 — « si ?x rdf:type ?C et ?C rdfs:subClassOf ?D, alors ?x rdf:type ?D » — deduit 2 triplets supplementaires : rex rdf:type Mammal puis, en reappliquant la règle, rex rdf:type Animal. Le point-fixe est atteint quand plus aucun nouveau triplet n’apparait.

Utilisation du raisonneur RDFS owlrl

owlrl est le raisonneur RDFS/OWL de reference en Python pur. Il implemente l’ensemble des règles d’entailment du W3C (RDF 1.1 Semantics) et materialise les triplets inferes directement dans le graphe. C’est l’equivalent fonctionnel du StaticRdfsReasoner de dotNetRDF.

L’API principale est owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(graph) : on instancie une closure avec un jeu de sémantiques (ici RDFS seul ; owlrl.OWLRL_Semantics pour OWL 2 RL), puis on appelle expand() qui mute le graphe en place.

if LIBS_AVAILABLE:
    # Charger animals.ttl dans un nouveau graphe frais
    animal_graph = Graph()
    animal_graph.parse("data/animals.ttl", format="turtle")

    triplets_before = len(animal_graph)
    print(f"Avant inference : {triplets_before} triplets")

    rex_uri  = URIRef("http://example.org/animals#rex")
    coco_uri = URIRef("http://example.org/animals#coco")

    print("Types de rex avant inference :")
    for _, _, o in sorted(animal_graph.triples((rex_uri, RDF.type, None)), key=lambda t: str(t[2])):
        print(f"  - {str(o).split('#')[-1]}")
    print()

    # === Application du VRAI raisonneur RDFS owlrl ===
    # owlrl.DeductiveClosure.expand() materialise les triplets inferes dans le graphe.
    # C'est l'equivalent Python de `StaticRdfsReasoner.Apply(graph)` en dotNetRDF.
    owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(animal_graph)

    triplets_after = len(animal_graph)
    print(f"Apres inference : {triplets_after} triplets (+{triplets_after - triplets_before} inferes)")
    print()

    print("Types de rex apres inference :")
    for _, _, o in sorted(animal_graph.triples((rex_uri, RDF.type, None)), key=lambda t: str(t[2])):
        if "animals#" in str(o):
            print(f"  - {str(o).split('#')[-1]}")
    print()

    print("Types de coco apres inference :")
    for _, _, o in sorted(animal_graph.triples((coco_uri, RDF.type, None)), key=lambda t: str(t[2])):
        if "animals#" in str(o):
            print(f"  - {str(o).split('#')[-1]}")
else:
    print("Bibliotheques non disponibles : raisonneur ignore.")
Avant inference : 51 triplets
Types de rex avant inference :
  - Dog

Apres inference : 148 triplets (+97 inferes)

Types de rex apres inference :
  - Animal
  - Dog
  - Mammal

Types de coco apres inference :
  - Animal
  - Bird
  - Parrot

Interpretation : Raisonneur RDFS owlrl

Le DeductiveClosure(RDFS_Semantics) a enrichi le graphe automatiquement. Le nombre de triplets inferes est nettement plus grand que dans la demo manuelle car owlrl applique l’ensemble des règles W3C, pas seulement rdfs9 :

Règle W3C Effet materialise
rdfs9 rex rdf:type Mammal, rex rdf:type Animal, etc.
rdfs3 Les objets des proprietes typees xsd:string deviennent des litteraux string
rdfs2 Les sujets de ex:name etc. sont types ex:Animal (même si non declares)
rdfs11 Dog rdfs:subClassOf Animal (transitivite de la hiérarchie)
axiomatique Tout est aussi rdfs:Resource, rdf:type est typing, etc.
Instance Types explicites Types inferes (animaux)
rex Dog Mammal, Animal
minou Cat Mammal, Animal
coco Parrot Bird, Animal
buddy Dog Mammal, Animal

Comparaison : - Sans inference : seul le type le plus spécifique est present - Avec inference : tous les super-types sont materialises dans le graphe

Note technique : owlrl materialise les triplets inferes directement dans le graphe. C’est une approche de materialisation (par opposition au raisonnement a la requête). C’est exactement la même philosophie que StaticRdfsReasoner en dotNetRDF.


6. Interroger les données enrichies par l’inference

Maintenant que le graphe a ete enrichi par owlrl, utilisons SPARQL pour interroger les données inferees. rdflib integre un moteur SPARQL natif : graph.query(sparql_string) retourne un iterable de lignes de résultats.

if LIBS_AVAILABLE:
    # Requete SPARQL : trouver tous les animaux (grace a l'inference)
    # NB : le FILTER restreint ?type aux classes de notre namespace animals# pour
    # exclure le typing axiomatique rdfs:Resource qu'owlrl materialise aussi.
    query1 = """
    PREFIX ex: <http://example.org/animals#>
    PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>

    SELECT ?animal ?name ?type
    WHERE {
        ?animal rdf:type ex:Animal .
        ?animal ex:name ?name .
        ?animal rdf:type ?type .
        FILTER(STRSTARTS(STR(?type), "http://example.org/animals#") && ?type != ex:Animal)
    }
    ORDER BY ?name ?type
    """

    print("Tous les animaux (grace a l'inference rdf:type Animal) :")
    print("-" * 60)
    rows = list(animal_graph.query(query1))
    for row in rows:
        name = str(row.name)
        type_uri = str(row.type)
        type_name = type_uri.split("#")[-1]
        print(f"  {name:10s} (type: {type_name})")
    print(f"\n({len(rows)} lignes de resultat)")
else:
    print("Bibliotheques non disponibles : requete SPARQL ignoree.")
Tous les animaux (grace a l'inference rdf:type Animal) :
------------------------------------------------------------
  Buddy      (type: Dog)
  Buddy      (type: Mammal)
  Coco       (type: Bird)
  Coco       (type: Parrot)
  Minou      (type: Cat)
  Minou      (type: Mammal)
  Rex        (type: Dog)
  Rex        (type: Mammal)

(8 lignes de resultat)

La requête ?animal rdf:type ex:Animal retourne tous les animaux, y compris ceux qui sont seulement types comme Dog, Cat ou Parrot. C’est la puissance de l’inference RDFS : on interroge a un niveau d’abstraction eleve sans avoir a enumerer toutes les sous-classes.

if LIBS_AVAILABLE:
    # Requete 2 : trouver tous les mammiferes (chiens ET chats, par inference)
    query2 = """
    PREFIX ex: <http://example.org/animals#>
    PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>

    SELECT ?animal ?name ?sound
    WHERE {
        ?animal rdf:type ex:Mammal .
        ?animal ex:name ?name .
        OPTIONAL { ?animal ex:sound ?sound . }
    }
    ORDER BY ?name
    """

    print("Tous les mammiferes (Dogs + Cats, grace a l'inference) :")
    print("-" * 60)
    for row in animal_graph.query(query2):
        name = str(row.name)
        sound = str(row.sound) if row.sound is not None else "(inconnu)"
        print(f"  {name:10s} : {sound}")
    print()

    # Requete 3 : compter les instances par classe
    # NB : on nomme les variables ?cls (pas ?class, mot-cle Python) et ?n
    # (pas ?count, qui collide avec ResultRow.count -- la methode de tuple Row).
    query3 = """
    PREFIX ex: <http://example.org/animals#>
    PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

    SELECT ?cls (COUNT(DISTINCT ?instance) AS ?n)
    WHERE {
        ?cls a rdfs:Class .
        ?instance a ?cls .
        FILTER(STRSTARTS(STR(?cls), "http://example.org/animals#"))
    }
    GROUP BY ?cls
    ORDER BY DESC(?n)
    """

    print("Nombre d'instances par classe (avec inference) :")
    print("-" * 50)
    for row in animal_graph.query(query3):
        cls_name = str(row.cls).split("#")[-1]
        n = str(row.n)
        print(f"  {cls_name:10s} : {n} instance(s)")
else:
    print("Bibliotheques non disponibles : requetes SPARQL ignorees.")
Tous les mammiferes (Dogs + Cats, grace a l'inference) :
------------------------------------------------------------
  Buddy      : Woof woof
  Minou      : Miaou
  Rex        : Woof

Nombre d'instances par classe (avec inference) :
--------------------------------------------------
  Animal     : 4 instance(s)
  Mammal     : 3 instance(s)
  Dog        : 2 instance(s)
  Bird       : 1 instance(s)
  Cat        : 1 instance(s)
  Parrot     : 1 instance(s)

Interpretation : Requêtes sur données inferees

Classe Instances directes Instances inferees Total
Animal 0 4 (rex, minou, coco, buddy) 4
Mammal 0 3 (rex, minou, buddy) 3
Bird 0 1 (coco) 1
Dog 2 (rex, buddy) 0 2
Cat 1 (minou) 0 1
Parrot 1 (coco) 0 1

Points cles : 1. La classe Animal n’a aucune instance directe mais 4 par inference 2. Les requêtes SPARQL fonctionnent sur le graphe materialise, incluant les triplets inferes 3. C’est un avantage majeur de RDFS : les données sont flexibles mais interrogeables a différents niveaux d’abstraction

Note : la clause FILTER(STRSTARTS(STR(?class), "http://example.org/animals#")) restreint le comptage aux classes de notre namespace. Sans elle, owlrl a aussi materialise des triplets axiomatiques comme rex rdf:type rdfs:Resource qui gonfleraient les comptes.


7. Comparaison cross-language : dotNetRDF vs rdflib + owlrl

Le notebook SW-6 (C#) et le present notebook (Python) traitent du même domaine (taxonomie animale RDFS) avec des moteurs RDFS reels equivalents. Cette section repertorie les différences fines observees entre les deux stacks – c’est la valeur ajoutée du miroir Python.

Synthese operationnelle

Aspect dotNetRDF (SW-6 C#) rdflib + owlrl (present)
API graphe Graph.Assert(new Triple(s, p, o)) g.add((s, p, o)) – tuple Python
Parsing Turtle TurtleParser().Load(g, "file.ttl") g.parse("file.ttl", format="turtle")
SPARQL LeviathanQueryProcessor + SparqlQueryParser g.query(sparql_string) – one-liner
Raisonneur RDFS StaticRdfsReasoner.Apply(g) (une ligne) owlrl.DeductiveClosure(RDFS_Semantics).expand(g)
Couverture des règles Règles RDFS core (rdfs2/3/5/7/9/11) RDFS Semantics complet (RDF 1.1)
Materialisation Mutations in-place du graphe Mutations in-place du graphe (même philosophie)
Rapport d’exécution Aucun (silencieux) Aucun (silencieux) ; closure.closure_ok disponible

Difficultes rencontrees (et comment les contourner)

Difficulte Solution cote Python
L’API owlrl.RDFSClosure.RDFSClosure(g) n’existe pas dans owlrl 7.x (pourtant citee dans d’anciens tutoriels) Utiliser owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g) – l’API moderne
rdflib Literal vs litteral Turtle 5 (entier non type) rdflib devine XSD.integer au parsing ; pour les assertions en code, utiliser Literal(5, datatype=XSD.integer)
Les prefixes dans la sortie SPARQL Iterer sur row.varname donne des objets rdflib natifs ; formater avec .split("#")[-1]
owlrl ajoute aussi des triplets axiomatics (rdfs:Resource, etc.) Filtrer par namespace STRSTARTS(STR(?x), "http://example.org/animals#") dans SPARQL

Ergonomie comparee

L’API rdflib est plus pythonique (tuples, itérations naturelles, namespaces W3C comme constantes), tandis que dotNetRDF est plus typee (IUriNode vs URIRef, Triple objet vs tuple). Pour du raisonnement, owlrl est plus verbeux a invoquer (3 symboles : DeductiveClosure + RDFS_Semantics + expand) mais couvre davantage de règles W3C que le StaticRdfsReasoner de dotNetRDF qui se limite au coeur RDFS.

Choix pratique : pour un cours Python-first de Web Sémantique, le binome rdflib + owlrl est autonomme et couvre tout le programme RDFS/OWL-RL. Pour un cours .NET, dotNetRDF integre raisonneur et SPARQL dans la même bibliotheque, ce qui evite une dépendance supplementaire.


Exercices

Les trois exercices ci-dessous sont a completer. Le notebook doit pouvoir s’executer de bout en bout même si les exercices restent vides (les stubs utilisent print("Exercice a completer") – jamais d’erreur volontaire).

Exercice 1 : Créer un vocabulaire RDFS pour une bibliotheque

Créez un schema RDFS pour un domaine de bibliotheque avec : - Classes : Publication, Book, Article, Author, Publisher - Hiérarchie : Book et Article sont des sous-classes de Publication - Proprietes : title (domain: Publication, range: string), writtenBy (domain: Publication, range: Author), publishedBy (domain: Book, range: Publisher), year (domain: Publication, range: integer) - Instances : au moins 2 livres et 1 article avec leurs auteurs - Appliquez le raisonneur owlrl et verifiez que les types sont bien inferes (un Book devient aussi une Publication).

Indices : - Inspirez-vous de la section 3 : g.add((EX.Book, RDFS.subClassOf, EX.Publication)) declare la hiérarchie. - Pour les proprietes : g.add((EX.title, RDF.type, RDF.Property)) + g.add((EX.title, RDFS.domain, EX.Publication)) + g.add((EX.title, RDFS.range, XSD.string)). - Pour le raisonnement : owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g) puis verifiez qu’une instance de Book a bien aussi rdf:type Publication.

# Exercice 1 : Vocabulaire RDFS pour une bibliotheque
# TODO etudiant : construire le schema, les instances, appliquer owlrl
#
# Etape 1 : creer le graphe et le namespace EX = Namespace("http://example.org/library#")
# Etape 2 : declarer les 5 classes (Publication, Book, Article, Author, Publisher)
# Etape 3 : declarer la hierarchie (Book < Publication, Article < Publication)
# Etape 4 : declarer les 4 proprietes avec domain/range
# Etape 5 : creer 2 instances de Book et 1 instance de Article
# Etape 6 : appliquer owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g)
# Etape 7 : verifier qu'un livre a bien rdf:type Publication (en plus de Book)

print("Exercice 1 a completer : vocabulaire RDFS pour une bibliotheque")
Exercice 1 a completer : vocabulaire RDFS pour une bibliotheque

Exercice 2 : Requête SPARQL avec inference RDFS

Les sections précédentes ont montre le raisonneur owlrl et les requêtes SPARQL. Reproduisez le pattern complet sur le fichier data/animals.ttl :

Indices : - Chargez data/animals.ttl dans un graphe frais avec g.parse(...). - Appliquez owlrl : owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g). - Ecrivez une requête SPARQL qui retourne tous les animaux dont le cri contient la lettre “o” (indice : FILTER(CONTAINS(LCASE(?sound), "o"))). - Verifiez que le résultat inclut aussi bien rex (Dog) que buddy (Dog) et coco (Parrot) – tous trois typés Animal par inference, même si rex et buddy sont declares explicitement comme Dog dans le fichier source.

# Exercice 2 : Requete SPARQL sur donnees enrichies par owlrl
# TODO etudiant : charger animals.ttl, appliquer owlrl, executer une SPARQL
#
# Etape 1 : g = Graph() ; g.parse("data/animals.ttl", format="turtle")
# Etape 2 : owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g)
# Etape 3 : ecrire une SPARQL qui trouve tous les animaux (rdf:type ex:Animal)
#           dont le cri (ex:sound) contient la lettre "o"
# Etape 4 : afficher les resultats (au moins rex ET buddy, qui sont Animal par inference)

print("Exercice 2 a completer : requete SPARQL sur donnees enrichies par owlrl")
Exercice 2 a completer : requete SPARQL sur donnees enrichies par owlrl

Exercice 3 : Chaîne d’inference profonde (transitivite)

Créez une hiérarchie de classes plus profonde pour tester la transitivite de rdfs:subClassOf : - LivingThing > Animal > Vertebrate > Mammal > Primate > Human - Créez une instance alice rdf:type Human - Affichez les types de alice avant inference - Appliquez owlrl et affichez les types de alice après inference - Verifiez que alice est bien typee par les 6 classes de la hiérarchie (LivingThing, Animal, Vertebrate, Mammal, Primate, Human)

Indices : - Une boucle for i in range(len(chain) - 1): g.add((chain[i+1], RDFS.subClassOf, chain[i])) permet de declarer la chaîne en une ligne. - Après expand(g), parcourez g.triples((alice, RDF.type, None)) et collectez les noms de classes. - owlrl materialise aussi le typing rdfs:Resource ; filtrez par namespace http://example.org/biology# pour ne garder que vos classes.

# Exercice 3 : Chaine d'inference profonde
# TODO etudiant : hierarchie profonde, instance alice, owlrl, verification
#
# Etape 1 : g = Graph() ; BIO = Namespace("http://example.org/biology#")
# Etape 2 : declarer la chaine LivingThing > Animal > Vertebrate > Mammal > Primate > Human
# Etape 3 : creer alice rdf:type Human
# Etape 4 : afficher les types AVANT inference (1 seul : Human)
# Etape 5 : appliquer owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g)
# Etape 6 : afficher les types APRES inference (6 classes attendues)

print("Exercice 3 a completer : chaine d'inference profonde")
Exercice 3 a completer : chaine d'inference profonde

Resume

Vocabulaire RDFS essentiel

Terme Utilisation Exemple
rdfs:Class Declarer une classe ex:Dog rdf:type rdfs:Class
rdfs:subClassOf Hiérarchie de classes ex:Dog rdfs:subClassOf ex:Mammal
rdfs:subPropertyOf Hiérarchie de proprietes ex:father rdfs:subPropertyOf ex:parent
rdfs:domain Type du sujet ex:name rdfs:domain ex:Animal
rdfs:range Type de l’objet/valeur ex:name rdfs:range xsd:string
rdfs:label Etiquette lisible ex:Dog rdfs:label "Chien"@fr
rdfs:comment Description ex:Dog rdfs:comment "Un canide domestique"

Ce que nous avons appris

  1. RDFS enrichit RDF avec un vocabulaire pour decrire la structure des données
  2. Les hiérarchies de classes (rdfs:subClassOf) permettent l’heritage de type
  3. L’inference RDFS deduit automatiquement des triplets implicites (règles rdfs2/3/5/9/11 du W3C)
  4. owlrl.DeductiveClosure(RDFS_Semantics).expand(g) materialise les triplets inferes dans le graphe – equivalent Python du StaticRdfsReasoner de dotNetRDF
  5. SPARQL sur données inferees permet d’interroger a différents niveaux d’abstraction

Equivalences dotNetRDF / rdflib + owlrl

Opération dotNetRDF (SW-6 C#) rdflib + owlrl (present)
Ajouter un triplet g.Assert(new Triple(s, p, o)) g.add((s, p, o))
Litteral avec langue g.CreateLiteralNode("Chien", "fr") Literal("Chien", lang="fr")
Charger Turtle TurtleParser().Load(g, file) g.parse(file, format="turtle")
SPARQL LeviathanQueryProcessor + parser g.query(sparql_string)
Raisonneur RDFS StaticRdfsReasoner.Apply(g) owlrl.DeductiveClosure(RDFS_Semantics).expand(g)

Limites de RDFS

RDFS est volontairement simple. Il ne permet pas de : - Définir des classes disjointes (Dog et Cat ne peuvent pas etre la même instance) - Exprimer des cardinalites (une personne a exactement 2 parents) - Declarer des proprietes inverses (teaches / taughtBy) - Exprimer des classes par union/intersection

Pour ces fonctionnalites, il faut passer a OWL (notebook suivant).

References


Navigation : Index | << SW-6 C# | SW-7b Python OWL >>

Retour au sommet