Travaux pratiques - Visualisation de graphes : Gephi et Spark¶
Références externes utiles :
Objectif du TP¶
Ce TP met en œuvre des algorithmes de spatialisation vus en cours, avec deux solutions complémentaires :
Gephi : outil interactif de référence, non distribué, pour des graphes de petite taille (ici : Les Misérables, communications VoIP, métro parisien) ;
Spark + GraphFrames → NetworkX/Plotly : pipeline pour des graphes plus volumineux, illustré sur le graphe de co-occurrences MeSH de la base Medline, déjà construit dans le TP sur les graphes.
La comparaison des deux solutions montre que Gephi propose une exploration interactive rapide mais ne passe pas à l’échelle, alors que la chaîne Spark permet de calculer les attributs et la spatialisation sur de grands graphes au prix d’une interface plus programmatique.
Partie 1 : Gephi — prise en main et exploration interactive¶
Gephi n’étant pas facilement utilisable à distance, il est nécessaire de l’installer sur votre ordinateur personnel en suivant le lien indiqué plus haut.
Exemple 1 : Les Misérables¶
D.E. Knuth a construit un graphe des personnages de l’œuvre Les misérables : deux personnages sont reliés s’ils apparaissent dans le même chapitre. C’est le graphe classique de prise en main de Gephi.
Données : gephi-LesMiserables.gexf
Dans l’ordre, il faut :
Importer le fichier GEXF (File → Open). Vérifier le nombre de nœuds et d’arêtes dans le rapport d’importation.
Afficher les étiquettes des nœuds (Show Node Labels dans la barre du bas de l’onglet Overview).
Spatialiser le graphe :
Essayez d’abord Random Layout pour voir un placement aléatoire.
Appliquez ForceAtlas2 (panneau Layout à gauche). Observez la convergence en temps réel. Notez l’effet du paramètre Gravity : sans gravité, les composantes dérivent ; avec gravité forte, le graphe devient trop compact.
Essayez Fruchterman-Reingold. Comparez la qualité visuelle et le temps de convergence avec ForceAtlas2.
Calculer des métriques (panneau Statistics à droite) :
Average Degree apparaît comme attribut de nœud.
Modularity (avec résolution 1.0) détecte les communautés et crée un attribut
modularity_class.Betweenness Centrality crée un attribut
betweenesscentrality.
Encoder les métriques visuellement (panneau Appearance à gauche) :
Taille des nœuds proportionnelle au degré (Nodes → Size → Ranking → Degree, min 5 / max 40).
Couleur des nœuds selon la communauté (Nodes → Color → Partition → modularity_class).
Exporter la visualisation résultante (File → Export → SVG/PDF/PNG).
Question
Combien de communautés distinctes sont détectée par Gephi ? Reconnaissez-vous des communautés qui correspondent aux grandes parties du roman ?
Exemple 2 : Communications VoIP¶
Dans le cadre d’une enquête, un officier de police a récupéré les journaux de communication Skype de deux comptes. Nous pouvons visualiser le réseau de communications pour identifier les acteurs centraux.
Données : gephi-skypeConversations.csv
Ouvrir Gephi, File → Import Spreadsheet. Paramètres : encodage UTF-8, séparateur virgule, import as edges (les colonnes
SourceetTargetsont reconnues automatiquement).Dans le Data Laboratory, observez les données : quelles colonnes sont présentes ? Le graphe est-il orienté ?
Calculez le degré, le diamètre et la modularité.
Encodez le degré par la taille des nœuds, la communauté par la couleur.
Spatialisez avec ForceAtlas (paramètres : répulsion 500, attraction 10, gravité 10, vitesse 10, toutes les options cochées). Observez la séparation des communautés.
Utilisez les filtres (Filters → Topology → Degree Range) pour ne conserver que les nœuds de degré entre 2 et 290 (éliminer les nœuds très périphériques).
Question
Quels nœuds ont la plus forte centralité d’intermédiarité ? Quel est leur rôle dans le réseau de communications (examinez leur position dans la visualisation finale) ?
Exemple 3 : Métro parisien¶
Le fichier GEXF du métro parisien contient les coordonnées géographiques des stations.
Données : gephi-parismetro.gexf
Importez le fichier et observez la visualisation initiale : les stations sont placées selon leurs coordonnées géographiques réelles (Geo Layout). C’est une spatialisation sémantique : la position encode la localisation géographique plutôt que la structure du graphe.
Appliquez ForceAtlas2. Comparez avec la vue géographique : quelle information est gagnée et quelle information est perdue ?
Calculez les centralités et encodez-les visuellement (intermédiarité par la taille, la ligne ou la communauté pr la couleur).
Question
Certaines stations ont-elles une intermédiarité élevée malgré un faible degré ? Quel est leur rôle dans le réseau ? La spatialisation ForceAtlas2 se rapproche de la géographie réelle ou s’en écarte significativement ?
Exercice optionnel
Téléchargez le réseau du métro de Munich ou de Shanghai depuis la page de Matthieu Totet et comparez la structure de ces réseaux avec celui de Paris.
Partie 2 : Pipeline Spark pour la visualisation du graphe Medline¶
Les exemples Gephi ci-dessus portaient sur des graphes de quelques dizaines à centaines de nœuds. Le graphe Medline des co-occurrences MeSH (construit dans le TP graphes) contient ~14 500 nœuds et ~213 000 arêtes. Gephi peut techniquement l’ouvrir, mais l’exploration interactive est lente et la lisibilité de la visualisation brute est médiocre.
Nous souhaitons illustrer ici deux stratégies complémentaires :
A : exporter le graphe de Spark vers Gephi (format GEXF) pour profiter de ForceAtlas2 sur le graphe complet, avec des attributs calculés dans Spark.
B : filtrer dans Spark pour ne conserver qu’un sous-graphe pertinent, ensuite le visualiser dans le notebook même avec NetworkX/Plotly.
Les versions de GraphFrames et de la bibliothèque de lecture XML fonctionnent avec la version 4.x de Pyspark, si ce n’est pas cette version qui est installée alors nous la mettons à jour :
import sys
!{sys.executable} -m pip install "pyspark>=4,<5"
Si la réponse vous indique que la version 4 était déjà installée alors vous pouvez poursuivre. En revanche, si cette instruction vient d’installer la version 4 à la place d’une plus ancienne alors il faut redémarrer le noyau (icône circulaire du menu en haut de la page) avant de poursuivre !
Reprenons l’environnement du TP Medline :
import os, sys
os.environ['PYSPARK_PYTHON'] = sys.executable
os.environ['PYSPARK_DRIVER_PYTHON'] = sys.executable
os.environ['PYSPARK_SUBMIT_ARGS'] = (
'--packages io.graphframes:graphframes-spark4_2.13:0.12.1,com.databricks:spark-xml_2.12:0.18.0 pyspark-shell')
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.config("spark.driver.memory", "4g") \
.getOrCreate()
sc = spark.sparkContext
scala = sc._jvm.scala.util.Properties.versionNumberString()
java = sc._jvm.java.lang.System.getProperty("java.version")
print(f"Spark {sc.version}, Scala {scala}, Java {java}")
assert scala.startswith("2.13"), "Scala 2.13 requis (Spark 4)"
assert int(java.split('.')[0]) >= 17, "Java 17 ou plus requis"
Si vous n’avez pas sauvegardé le graphe du TP précédent, reconstituez-le
rapidement (les données Medline sont supposées présentes dans
tpGraphes/medline_data/) :
import xml.etree.ElementTree as ET
from itertools import combinations
from operator import add
from pyspark.sql.types import StructType, StructField, StringType, IntegerType
from graphframes import GraphFrame
from pyspark.sql import functions as F
def _citations(path):
for _, e in ET.iterparse(path, events=('end',)):
if e.tag == 'MedlineCitation':
yield (0, ET.tostring(e, encoding='unicode'))
e.clear()
def loadMedline(path):
return sc.parallelize([os.path.abspath(path)], 1).flatMap(_citations)
def majorTopics(string):
result = []
elem = ET.fromstring(string)
for d in elem.iter('DescriptorName'):
if d.attrib.get('MajorTopicYN') == 'Y':
result.append(d.text)
return result
paths = [os.path.abspath(f"tpGraphes/medline_data/medsamp2016{l}.xml")
for l in "abcdefgh"]
missing = [p for p in paths if not os.path.exists(p)]
if missing:
raise FileNotFoundError(f"Fichiers Medline manquants : {missing}")
medline_raw = sc.union([loadMedline(p) for p in paths])
medline = medline_raw.map(lambda t: majorTopics(t[1])).cache()
topicCounts = medline.flatMap(lambda l: l).countByValue()
topicPairs = medline.flatMap(lambda l: combinations(sorted(l), 2))
cooccurs = topicPairs.map(lambda p: (p, 1)).reduceByKey(add)
edges_schema = StructType([StructField('src', StringType(), True),
StructField('dst', StringType(), True),
StructField('freq', IntegerType(), True)])
vertices_schema = StructType([StructField('id', StringType(), True),
StructField('hash', StringType(), True)])
edges = spark.createDataFrame(
cooccurs.map(lambda l: (l[0][0], l[0][1], l[1])), schema=edges_schema)
vertices = spark.createDataFrame(
medline.flatMap(lambda l: l).distinct().map(lambda t: (t, str(hash(t)))),
schema=vertices_schema)
topicGraph = GraphFrame(vertices, edges)
print(f"Sommets : {topicGraph.vertices.count()}, Arêtes : {topicGraph.edges.count()}")
Avant de visualiser, on enrichit chaque nœud avec ses attributs analytiques : degré, PageRank, communauté (Label Propagation). Ces calculs sont réalisés de façon distribuée dans Spark.
# Degré
degrees = topicGraph.degrees.withColumnRenamed("degree", "degree")
# PageRank
pr = topicGraph.pageRank(resetProbability=0.15, tol=0.01)
pageranks = pr.vertices.select("id", "pagerank")
# Communautés par Label Propagation
lp = topicGraph.labelPropagation(maxIter=5)
communities = lp.select("id", "label")
# Fréquence de chaque tag dans les articles
freq_df = spark.createDataFrame(
list(topicCounts.items()), schema=["id", "freq_tag"])
# Assemblage de tous les attributs
node_attrs = (topicGraph.vertices
.join(degrees, on="id", how="left")
.join(pageranks, on="id", how="left")
.join(communities, on="id", how="left")
.join(freq_df, on="id", how="left")
.fillna({"degree": 0, "pagerank": 0.0, "freq_tag": 0})
)
node_attrs.cache()
print(f"Nœuds avec attributs : {node_attrs.count()}")
Le format GEXF (Graph Exchange XML Format) est le format natif de Gephi. Il encode nœuds, arêtes et attributs dans un fichier XML. Nous l’obtenons directement à partir des DataFrames Spark en rapatriant les données vers le nœud driver :
# Rapatriement vers le driver (taille raisonnable : ~14 500 nœuds, ~213 000 arêtes)
nodes_pd = node_attrs.toPandas()
edges_pd = topicGraph.edges.toPandas()
print(f"Nœuds collectés : {len(nodes_pd)}")
print(f"Arêtes collectées : {len(edges_pd)}")
Note
Même si ce graphe est grand, il tient facilement en mémoire et toPandas()
peut être employé (les nœuds occupent ~2 Mo, les arêtes ~15 Mo). Pour un
graphe de plusieurs millions de nœuds il faudrait soit appliquer un filtre
préalablement, soit écrire le GEXF directement depuis tous les workers Spark
en parallèle et concaténer les fichiers partitionnés.
def write_gexf(nodes_pd, edges_pd, filepath):
"""
Génère un fichier GEXF à partir de DataFrames Pandas.
Attributs de nœuds exportés : degree, pagerank, label de communauté, freq_tag.
"""
import xml.etree.ElementTree as ET
from xml.dom import minidom
gexf = ET.Element("gexf", {
"xmlns": "http://www.gexf.net/1.2draft",
"version": "1.2"
})
graph = ET.SubElement(gexf, "graph", {
"defaultedgetype": "undirected",
"mode": "static"
})
# Déclaration des attributs de nœuds
attrs = ET.SubElement(graph, "attributes", {"class": "node"})
for aid, aname, atype in [
("0", "degree", "integer"),
("1", "pagerank", "float"),
("2", "community", "integer"),
("3", "freq_tag", "integer"),
]:
ET.SubElement(attrs, "attribute", {"id": aid, "title": aname, "type": atype})
# Nœuds
nodes_el = ET.SubElement(graph, "nodes")
for _, row in nodes_pd.iterrows():
node = ET.SubElement(nodes_el, "node", {
"id": str(row["id"]),
"label": str(row["id"])
})
attvalues = ET.SubElement(node, "attvalues")
ET.SubElement(attvalues, "attvalue", {"for": "0", "value": str(int(row.get("degree", 0)))})
ET.SubElement(attvalues, "attvalue", {"for": "1", "value": str(float(row.get("pagerank", 0.0)))})
ET.SubElement(attvalues, "attvalue", {"for": "2", "value": str(int(row.get("label", 0)))})
ET.SubElement(attvalues, "attvalue", {"for": "3", "value": str(int(row.get("freq_tag", 0)))})
# Arêtes
edges_el = ET.SubElement(graph, "edges")
for i, row in edges_pd.iterrows():
ET.SubElement(edges_el, "edge", {
"id": str(i),
"source": str(row["src"]),
"target": str(row["dst"]),
"weight": str(row.get("freq", 1))
})
# Formatage et écriture
xmlstr = minidom.parseString(ET.tostring(gexf)).toprettyxml(indent=" ")
with open(filepath, "w", encoding="utf-8") as f:
f.write(xmlstr)
print(f"GEXF écrit : {filepath}")
os.makedirs("tpVisualisationGraphes", exist_ok=True)
write_gexf(nodes_pd, edges_pd, "tpVisualisationGraphes/medline_mesh.gexf")
Ouverture dans Gephi :
Transférez
tpVisualisationGraphes/medline_mesh.gexfsur votre ordinateur (ou ouvrez-le directement si Gephi est installé sur la même machine).File → Open, acceptez les paramètres par défaut.
Dans le Data Laboratory, vérifiez que les colonnes
degree,pagerank,communityetfreq_tagsont présentes.Spatialisez avec ForceAtlas2 en mode approximatif (Barnes-Hut). Ce graphe de 14 500 nœuds devrait converger en quelques minutes. Paramètres conseillés : Scaling*=10, *Gravity*=1.0, *LinLog mode activé, Prevent Overlap activé.
Encodages à faire : taille des nœuds proportionnelle à
pagerank(min 1 / max 20), couleur des nœuds parcommunity(partitionnement coloré), épaisseur des arêtes proportionnelle àweight(fréquence de co-occurrence).Appliquez le filtre Degree Range pour cacher les nœuds de degré 1 (tags apparaissant avec un seul autre tag).
Question
Après spatialisation et filtrage combien de « régions » thématiques distinguez-vous ? Les nœuds de fort PageRank (grande taille) sont-ils au centre ou à la périphérie des communautés ?
Question
Comparez la spatialisation obtenue avec ForceAtlas2 (LinLog off) et avec LinLog activé. Lequel met mieux en évidence les communautés ? Lequel préserve mieux les connexions inter-communautaires ?
Pour une exploration plus rapide sans quitter le notebook on travaille sur un sous-graphe filtré : les nœuds de fort degré (en excluant les hubs généraux) et leurs connexions.
# Paramètre : seuil minimal de degré pour conserver un nœud
DEGREE_MIN = 20
DEGREE_MAX = 300 # exclure les hubs trop généraux
ids_ok = (node_attrs
.filter(F.col("degree").between(DEGREE_MIN, DEGREE_MAX))
.select("id"))
ids_set = set(ids_ok.rdd.flatMap(lambda x: x).collect())
print(f"Nœuds conservés : {len(ids_set)}")
# Sous-graphe : on filtre aussi les arêtes
edges_sub = topicGraph.edges.filter(
F.col("src").isin(ids_set) & F.col("dst").isin(ids_set))
nodes_sub = node_attrs.filter(F.col("id").isin(ids_set))
# Rapatriement (taille réduite)
edges_sub_pd = edges_sub.toPandas()
nodes_sub_pd = nodes_sub.toPandas()
print(f"Sous-graphe : {len(nodes_sub_pd)} nœuds, {len(edges_sub_pd)} arêtes")
import networkx as nx
import numpy as np
# Construction du graphe NetworkX
G_sub = nx.Graph()
for _, row in edges_sub_pd.iterrows():
G_sub.add_edge(row["src"], row["dst"], weight=row["freq"])
print(f"NetworkX : nœuds : {G_sub.number_of_nodes()}, "
f"arêtes : {G_sub.number_of_edges()}")
# Spatialisation Fruchterman-Reingold, adaptée à cette taille
# Sous-graphes plus grands : nx.kamada_kawai_layout ou
# spring_layout avec k ajusté
print("Calcul des positions (Fruchterman-Reingold)...")
pos_fr = nx.spring_layout(G_sub, k=0.3, iterations=100, seed=42)
print("Terminé.")
Plotly produit dans le notebook une visualisation interactive : zoom, affichage des attributs au survol.
import plotly.graph_objects as go
# Préparation des données de positionnement
node_ids = list(pos_fr.keys())
node_x = [pos_fr[n][0] for n in node_ids]
node_y = [pos_fr[n][1] for n in node_ids]
# Attributs pour le survol
node_info = nodes_sub_pd.set_index("id")
node_degree = [node_info.loc[n, "degree"] if n in node_info.index else 0
for n in node_ids]
node_pr = [node_info.loc[n, "pagerank"] if n in node_info.index else 0.0
for n in node_ids]
node_comm = [node_info.loc[n, "label"] if n in node_info.index else 0
for n in node_ids]
hover_texts = [
f"<b>{n}</b><br>Degré: {d}<br>PageRank: {p:.4f}<br>Comm.: {c}"
for n, d, p, c in zip(node_ids, node_degree, node_pr, node_comm)
]
# Tracé des arêtes (lignes grises fines)
edge_x, edge_y = [], []
for _, row in edges_sub_pd.iterrows():
if row["src"] in pos_fr and row["dst"] in pos_fr:
x0, y0 = pos_fr[row["src"]]
x1, y1 = pos_fr[row["dst"]]
edge_x += [x0, x1, None]
edge_y += [y0, y1, None]
edge_trace = go.Scatter(
x=edge_x, y=edge_y,
mode="lines",
line=dict(width=0.4, color="#cccccc"),
hoverinfo="none"
)
# Tracé des nœuds (taille = degré, couleur = communauté)
node_trace = go.Scatter(
x=node_x, y=node_y,
mode="markers",
hoverinfo="text",
text=hover_texts,
marker=dict(
size=[max(3, min(20, d * 0.15)) for d in node_degree],
color=node_comm,
colorscale="Turbo",
showscale=True,
colorbar=dict(title="Communauté"),
line=dict(width=0.5, color="#ffffff")
)
)
fig = go.Figure(
data=[edge_trace, node_trace],
layout=go.Layout(
title=f"Sous-graphe Medline MeSH — {len(node_ids)} nœuds "
f"(degré entre {DEGREE_MIN} et {DEGREE_MAX})",
showlegend=False,
hovermode="closest",
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
width=900, height=700,
margin=dict(b=20, l=5, r=5, t=40)
)
)
fig.show()
Question
Survolez quelques nœuds de grande taille (degré élevé). Sont-ils au centre ou à la périphérie de la visualisation ? Les nœuds de même communauté (même couleur) sont-ils spatialement regroupés ?
Question
Comparez cette visualisation Plotly à ce que vous avez obtenu dans Gephi. Quels sont les avantages et inconvénients de chaque approche ?
Une alternative à la visualisation de tous les nœuds est de construire un graphe de méta-nœuds : on remplace chaque communauté par un super-nœud dont la taille est proportionnelle au nombre de membres et on trace les arêtes qui représentent les connexions inter-communautaires.
# Agrégation Spark : pour chaque paire de communautés, somme des fréquences
# des arêtes inter-communautaires
node_with_comm = node_attrs.select("id", F.col("label").alias("comm"))
meta_edges = (
topicGraph.edges
.join(node_with_comm.withColumnRenamed("id", "src")
.withColumnRenamed("comm", "comm_src"), on="src")
.join(node_with_comm.withColumnRenamed("id", "dst")
.withColumnRenamed("comm", "comm_dst"), on="dst")
.filter(F.col("comm_src") != F.col("comm_dst")) # inter-communautaires seulement
.groupBy("comm_src", "comm_dst")
.agg(F.sum("freq").alias("total_freq"),
F.count("*").alias("nb_aretes"))
.filter(F.col("nb_aretes") >= 5) # seuil de lisibilité, à varier
)
meta_nodes = (
node_with_comm
.groupBy("comm")
.agg(F.count("id").alias("taille"))
)
meta_edges_pd = meta_edges.toPandas()
meta_nodes_pd = meta_nodes.toPandas()
print(f"Méta-graphe : {len(meta_nodes_pd)} communautés, "
f"{len(meta_edges_pd)} méta-arêtes")
# Spatialisation et visualisation du méta-graphe
G_meta = nx.Graph()
for _, row in meta_edges_pd.iterrows():
G_meta.add_edge(str(row["comm_src"]), str(row["comm_dst"]),
weight=row["total_freq"])
# Positions : spring layout avec poids inversés (communautés liées → proches)
pos_meta = nx.spring_layout(G_meta, weight="weight", k=1.5,
iterations=200, seed=42)
# Tailles des méta-nœuds
sizes_dict = dict(zip(
meta_nodes_pd["comm"].astype(str),
meta_nodes_pd["taille"]
))
meta_node_ids = list(pos_meta.keys())
meta_x = [pos_meta[n][0] for n in meta_node_ids]
meta_y = [pos_meta[n][1] for n in meta_node_ids]
meta_sizes = [sizes_dict.get(n, 10) for n in meta_node_ids]
meta_hover = [f"Communauté {n}<br>Membres : {sizes_dict.get(n, '?')}"
for n in meta_node_ids]
meta_edge_x, meta_edge_y = [], []
for _, row in meta_edges_pd.iterrows():
s, d = str(row["comm_src"]), str(row["comm_dst"])
if s in pos_meta and d in pos_meta:
meta_edge_x += [pos_meta[s][0], pos_meta[d][0], None]
meta_edge_y += [pos_meta[s][1], pos_meta[d][1], None]
fig_meta = go.Figure(
data=[
go.Scatter(x=meta_edge_x, y=meta_edge_y,
mode="lines",
line=dict(width=0.8, color="#aaaaaa"),
hoverinfo="none"),
go.Scatter(x=meta_x, y=meta_y,
mode="markers+text",
text=[f"C{n}" for n in meta_node_ids],
textposition="top center",
hoverinfo="text",
hovertext=meta_hover,
marker=dict(
size=[max(5, min(50, s * 0.05))
for s in meta_sizes],
color=list(range(len(meta_node_ids))),
colorscale="Turbo",
line=dict(width=1, color="white")
))
],
layout=go.Layout(
title="Méta-graphe des communautés Medline",
showlegend=False,
hovermode="closest",
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
width=800, height=700
)
)
fig_meta.show()
Question
Combien de méta-communautés obtenez-vous ? Certaines sont-elles fortement interconnectées (arêtes épaisses) ?
Exercice
Modifiez le seuil nb_aretes >= 5 et observez l’impact sur la lisibilité
du méta-graphe. Quel seuil vous semble offrir le meilleur compromis entre
complétude et lisibilité ?
Synthèse : critères de choix¶
Ce TP illustre une règle pratique simple pour choisir son outil de visualisation de graphe :
Critère |
Gephi |
NetworkX + Plotly (notebook) |
Méta-graphe agrégé |
|---|---|---|---|
Taille du graphe |
< 50 000 nœuds |
< 5 000 nœuds |
Illimitée (agrégation) |
Qualité de spatialisation |
Très bonne (ForceAtlas2) |
Bonne (FR, Kamada-Kawai) |
Suffisante |
Interactivité |
Très bonne |
Modérée (Plotly) |
Modérée (Plotly) |
Intégration pipeline |
Manuel (import/export) |
Native (Spark → Polars/Pandas) |
Native |
Reproductibilité |
Faible (manuel) |
Totale (code) |
Totale (code) |
Visibilité des détails |
Nœuds individuels |
Nœuds individuels |
Structure de haut niveau |
Conclusion
Employer donc Gephi pour l’exploration initiale et la communication de résultats, la chaîne Spark → notebook pour l’automatisation et les graphes trop grands pour Gephi, et enfin le méta-graphe pour avoir une vue d’ensemble quand le graphe est illisible dans son intégralité.