Leçon 6 — Construire votre pipeline¶
Ce que vous allez apprendre
- Envoyer du DDI vers un magasin dont ce package n'a jamais entendu parler
- Gérer correctement l'ordonnancement en deux phases
- Savoir quelles parties sont fournies et lesquelles sont des exemples
Ce qui est réellement fourni¶
Sachez-le avant d'écrire quoi que ce soit : cela détermine la quantité de travail qui vous attend.
| Cible | Statut |
|---|---|
| Neo4j | Fourni : ddigraph load |
| RDF / SPARQL | Fourni : ddigraph export, ddigraph load out.ttl |
| JSON / CSV | Fourni : ddigraph export |
| NetworkX, pandas, Gremlin | Exemples dans demo/, pas des adaptateurs fournis |
Tout ce qui n'est pas dans les trois premières lignes, vous l'écrivez vous-même — et la leçon 3 vous a déjà montré toute l'interface nécessaire.
Le motif¶
Trois étapes, dont seule celle du milieu est subtile.
- Collecter les nœuds, indexés par identité.
- Collecter les arcs, dont les extrémités renvoient à ces clés.
- Écrire les deux où vous voulez.
La raison de collecter avant d'écrire est l'ordonnancement en deux phases de la leçon 3 : pour DDI-L, tous les nœuds arrivent avant le premier arc, et un arc peut désigner un nœud d'un morceau bien antérieur. Écrire les arcs à leur arrivée voudrait dire chercher des nœuds déjà jetés.
import os
from ddigraph import iter_graph
def node_key(node):
"""Une clé stable tirée de toute l'identité, pas du premier champ."""
return "|".join(f"{k}={v}" for k, v in sorted(node.identity.items()))
nodes, edges = {}, []
for chunk in iter_graph(os.environ["FIXTURE"]):
for node in chunk.nodes:
nodes[node_key(node)] = node
for edge in chunk.relationships:
edges.append((node_key(edge.start), edge.type, node_key(edge.end)))
print(len(nodes), "nœuds,", len(edges), "arcs")
dangling = [e for e in edges if e[0] not in nodes or e[2] not in nodes]
print("extrémités orphelines :", len(dangling))
6 nœuds, 5 arcs
extrémités orphelines : 0
Cette fonction node_key est la partie à recopier. Utiliser
next(iter(node.identity.values())) — le premier champ d'identité —
paraît équivalent et ne l'est pas. Certains types de nœuds sont identifiés
par plusieurs champs ensemble, et ne prendre que le premier fusionne
silencieusement des nœuds distincts en un seul. Rien ne lève d'erreur :
vous vous retrouvez simplement avec moins de nœuds qu'au départ, et leurs
propriétés mélangées.
Une vraie cible¶
NetworkX, en neuf lignes de plus :
import os
import networkx as nx
from ddigraph import iter_graph
def node_key(node):
return "|".join(f"{k}={v}" for k, v in sorted(node.identity.items()))
graph = nx.DiGraph()
for chunk in iter_graph(os.environ["FIXTURE"]):
for node in chunk.nodes:
graph.add_node(node_key(node), node_type=node.label, **node.properties)
for edge in chunk.relationships:
graph.add_edge(node_key(edge.start), node_key(edge.end), type=edge.type)
print(graph.number_of_nodes(), "nœuds,", graph.number_of_edges(), "arcs")
print("acyclique :", nx.is_directed_acyclic_graph(graph))
6 nœuds, 5 arcs
acyclique : True
Notez node_type=, et non label=. Les enregistrements DDI portent leur
propre propriété label : **node.properties en fournit donc déjà une, et
passer en plus label=node.label lève TypeError: got multiple values for
keyword argument 'label'. C'est une ligne facile à écrire et facile à
manquer.
Comme add_node est ici indexé par identité, NetworkX fusionne
naturellement les répétitions : vous pouvez donc sauter l'étape de
collecte. Un magasin sans cette propriété a besoin de la version en deux
passes ci-dessus.
Cela marche aussi sur du RDF¶
read_graph produit les mêmes GraphChunk : tout ce que vous construisez
sur iter_graph lit donc gratuitement Turtle, JSON-LD, N-Triples et
RDF/XML.
import os
import ddigraph
from ddigraph.rdf.reader import read_graph
ddigraph.export(os.environ["FIXTURE"], "survey.ttl", format="turtle")
labels = sorted({node.label for chunk in read_graph("survey.ttl") for node in chunk.nodes})
print(labels)
['Category', 'CodeList', 'Instrument', 'QuestionConstruct', 'QuestionItem', 'Sequence']
Les mêmes libellés que produisait le XML, ressortis du RDF. C'est l'astuce
des deux rdf:type de la leçon 4 qui paie : sans le type de l'espace de
noms projet, on obtiendrait Question là où l'original disait
QuestionItem.
En flux, si nécessaire¶
Les deux exemples ci-dessus gardent tout le graphe en mémoire. Pour un fichier de 65 Mo comptant des dizaines de milliers de nœuds, cela peut convenir — ou non.
Si ce n'est pas le cas, faites ce que fait GraphChunkWriter : écrivez
chaque morceau à son arrivée et laissez le magasin résoudre les
extrémités par identité. En Cypher, c'est MERGE sur l'identité, qui crée
le nœud si l'arc arrive en premier et le retrouve sinon. Tout magasin
disposant d'un upsert sur votre identité peut faire de même.
Exercice¶
Écrivez un pipeline qui indique, pour chaque type de nœud, à quels types de relations il participe. Exécutez-le sur les trois fichiers.
Solution
import collections
import os
from ddigraph import iter_graph
for name in ("FIXTURE", "CODEBOOK_FIXTURE", "CDI_FIXTURE"):
shapes = collections.defaultdict(set)
for chunk in iter_graph(os.environ[name]):
for edge in chunk.relationships:
shapes[edge.start.label].add(f"-{edge.type}->")
shapes[edge.end.label].add(f"<-{edge.type}-")
print(f"--- {name}")
for label in sorted(shapes)[:4]:
print(f" {label}: {', '.join(sorted(shapes[label]))}")
Une seule boucle, trois variantes, aucun branchement selon laquelle.
C'est tout le bénéfice de la vue graphe : vous avez écrit ceci contre
iter_graph et cela fonctionne sur des formats que vous n'avez jamais
regardés.
Ensuite¶
Suivant : Ancrer un modèle dans le graphe — mettre ces métadonnées sous les yeux d'un modèle de langage sans le laisser inventer ce qu'il ne sait pas.
Deux autres lectures utiles :
- L'étude de cas RDF mène une liste de codes de DDI jusqu'aux données liées validées — tout ce cours appliqué à un seul problème réaliste.
- Adaptateurs personnalisés couvre l'interface d'écriture asynchrone, pour alimenter une base plutôt que produire un fichier.
Et si quelque chose ici était faux ou obscur, ouvrez un ticket. Chaque exemple de ces leçons s'exécute en CI : « ça ne marche pas » est donc un bogue qui mérite d'être signalé.