Leçon 3 — Nœuds, arcs, identité¶
Ce que vous allez apprendre
- Diffuser tout fichier DDI en nœuds et relations depuis Python
- Lire les trois champs de chaque nœud et savoir à quoi ils servent
- Comprendre pourquoi une seule couture sert les trois variantes
Une forme pour trois formats¶
La leçon 2 a prévisualisé trois fichiers très différents avec une seule
commande. Cela fonctionne grâce à une fonction unique en dessous :
iter_graph.
Elle prend un chemin, détermine la variante, et produit la même chose pour
les trois — des morceaux de nœuds et de relations. Tout le reste du package
repose dessus. L'aperçu, l'exportateur RDF, l'écrivain Neo4j, le validateur
SHACL : tous consomment iter_graph et aucun ne sait de quelle variante on
est parti.
graph LR
C[XML Codebook] --> V[iter_graph]
L[XML Lifecycle] --> V
D[XML CDI] --> V
R[RDF] --> V
V --> P[preview]
V --> E[export]
V --> N[Neo4j]
V --> Y[votre code]
Cette dernière flèche est l'objet de cette leçon.
Diffuser un fichier¶
import os
from ddigraph import iter_graph
for chunk in iter_graph(os.environ["FIXTURE"]):
for node in chunk.nodes:
print(node.label, node.identity)
Instrument {'fragment_id': 'urn:ddi:test.org:inst1:1.0'}
Sequence {'fragment_id': 'urn:ddi:test.org:seq1:1.0'}
QuestionConstruct {'fragment_id': 'urn:ddi:test.org:qc1:1.0'}
QuestionItem {'fragment_id': 'urn:ddi:test.org:q1:1.0'}
CodeList {'fragment_id': 'urn:ddi:test.org:cl1:1.0'}
Category {'fragment_id': 'urn:ddi:test.org:cat1:1.0'}
Notez le mot diffuser. iter_graph est un générateur qui produit des
morceaux au fil de l'analyse : la mémoire reste plate que le fichier fasse
6 Ko ou 65 Mo. Il ne détient jamais le graphe entier.
Les trois champs d'un nœud¶
Chaque nœud possède exactement trois choses.
label — de quel type de chose il s'agit. QuestionItem, CodeList.
Cela devient le label Neo4j et la classe RDF.
identity — les champs qui disent lequel c'est. Souvent une seule
clé, mais pas toujours, et c'est important : certains types sont identifiés
par une combinaison de champs, et n'utiliser que le premier fusionnerait
des choses distinctes.
properties — tout le reste. Libellés, textes, agence, version.
import os
from ddigraph import iter_graph
for chunk in iter_graph(os.environ["FIXTURE"]):
for node in chunk.nodes:
if node.label == "QuestionItem":
print("identité ", node.identity)
print("propriétés", sorted(node.properties))
identité {'fragment_id': 'urn:ddi:test.org:q1:1.0'}
propriétés ['agency', 'ddi_id', 'fragment_id', 'label', ...]
identity est un dictionnaire, pas une chaîne, précisément parce qu'il
peut contenir plusieurs clés. Traitez-le comme un tout et vous ne serez pas
pris au dépourvu.
Utilisez l'identité entière
Construire une clé à partir de la première valeur d'identité est le raccourci tentant, et il échoue en silence. Prenez un type de nœud identifié par trois champs : tous ceux qui partagent le premier champ se retrouvent sur une même clé, et leurs propriétés fusionnent. Vous obtenez moins de nœuds qu'au départ, sans la moindre erreur pour vous le signaler.
DDIGenericIdentifiable, dans le fichier Codebook, est identifié par
(dataset_id, element_tag, identifiable_id). Les quatorze partagent un
même dataset_id.
Les relations¶
Une relation a un type et deux extrémités, et chaque extrémité est un nœud :
import os
from ddigraph import iter_graph
for chunk in iter_graph(os.environ["FIXTURE"]):
for edge in chunk.relationships:
print(f"({edge.start.label})-[:{edge.type}]->({edge.end.label})")
(Instrument)-[:HAS_CONSTRUCT]->(Sequence)
(Sequence)-[:HAS_CONSTRUCT]->(QuestionConstruct)
(QuestionConstruct)-[:REFERENCES_QUESTION]->(QuestionItem)
(QuestionItem)-[:USES_CODELIST]->(CodeList)
(CodeList)-[:HAS_CATEGORY]->(Category)
C'est la chaîne de la leçon 2, désormais sous forme d'objets manipulables. Les extrémités portent aussi leur identité, ce qui permet à un consommateur de rattacher un arc à des nœuds vus dans un morceau antérieur.
Pourquoi des morceaux et pas une liste¶
iter_graph produit des objets GraphChunk plutôt qu'une seule liste
plate, et la raison est l'ordonnancement.
Pour DDI-L, l'analyseur travaille en deux phases : tous les nœuds d'abord, puis toutes les relations. Il le doit. Un arc peut désigner un fragment qui apparaît plus loin dans le fichier ; l'analyseur ne peut donc pas le construire avant d'avoir tout vu. Le découpage rend cela visible au lieu de le cacher derrière une liste qu'il faudrait de toute façon matérialiser en entier.
Pour votre code, la conséquence pratique est simple : ne supposez pas que les arcs d'un morceau désignent des nœuds du même morceau. Collectez d'abord les nœuds, puis câblez les arcs. La leçon 6 fait exactement cela.
Exercice¶
Comptez les nœuds et les relations du fichier Codebook, groupés par type,
sans utiliser ddigraph preview.
Solution
import collections
import os
from ddigraph import iter_graph
nodes = collections.Counter()
edges = collections.Counter()
for chunk in iter_graph(os.environ["CODEBOOK_FIXTURE"]):
nodes.update(node.label for node in chunk.nodes)
edges.update(edge.type for edge in chunk.relationships)
print(sum(nodes.values()), "nœuds,", sum(edges.values()), "relations")
for label, count in nodes.most_common(5):
print(f" {label:24} {count}")
C'est, à peu de chose près, ce que fait ddigraph preview. L'aperçu
représente une centaine de lignes au-dessus de iter_graph, et rien
n'y est privilégié : vous disposez du même accès que lui.
Vérifiez-vous¶
- Pourquoi
identityest-il un dictionnaire plutôt qu'une chaîne ? - Pourquoi un arc d'un morceau peut-il désigner un nœud d'un morceau antérieur ?
- Que faudrait-il changer pour que votre code fonctionne sur DDI-CDI au lieu de DDI-L ?
Suivant : Rejoindre le monde extérieur — transformer ces nœuds en RDF que d'autres systèmes comprennent déjà.