ENPC TD1 : Python (NetworkX) et analyse de réseaux

Dans le cadre d’un enseignement sur les risques à l’Ecole Nationale des Ponts et Chaussées, j’ai élaboré une séquence de cours concernant plus spécifiquement les réseaux techniques. Ce billet propose le résumé d’une séance qui est en lien direct avec les préoccupations de ce blog, puisqu’elle traite de l’analyse de réseaux avec une bibliothèque Python nommée NetworkX. Pour reproduire ce billet, il conviendra bien entendu d’installer Python, puis les bibliothèques : Numpy, Scipy et NetworkX 1.7.

Pour ce billet, nous disposons d’un fichier Python définissant trois listes (ici). Ces listes correspondent à une modélisation simplifiée d’un graphe du réseau autoroutier français. Il convient tout d’abord d’ouvrir ce fichier dans l’IDLE Python, puis d’exécuter ce code. Il est alors possible de construire le graphe du réseau à étudier.

import networkx as nx # import de la bibliothèque networkx
g=nx.Graph() # création du graphe g
g.add_edges_from(arcs) # ajout des arcs de la liste arc au graphe
print(g.nodes()) # consultation des nœuds du graphe g

print(g.edges()) # consultation des arcs du graphe g

Plusieurs indicateurs pouvant nous servir par la suite peuvent être calculés. Pour chaque nœud, on peut ainsi calculer le degré, la centralité intermédiaire ou l’intermédiarité (betweenness), l’éloignement moyen.

deg=nx.degree(g) # calcul des degrés des nœuds du graphe g
bet=nx.betweenness_centrality(g)  # calcul des centralités
aspl=nx.average_shortest_path_length(g)

print(aspl) # consultation des éloignements moyens

Si l’on souhaite afficher le nom des villes suivant leur niveau de centralité intermédiaire, il est possible d’utiliser la bibliothèque Operator :

import operator # import de la bibliothèque operator
tri=sorted(bet.items(), reverse=True, key=operator.itemgetter(1))
for i in range(len(tri)): # boucle affichant le nom des villes

    print(villes[int(tri[i][0])])# en fonction de l’opération de tri

Pour analyser les forces et les faiblesses d’un réseau, il faut le mettre à l’épreuve. Pour cela, il existe un moyen radical : supprimer des éléments du réseau et recalculer des indicateurs calculés au préalable afin de comparer les résultats obtenus. Pour un réseau transport, l’éloignement moyen peut être considéré comme un indicateur pertinent. Afin de conserver un graphe « intact », il peut être pertinent de travailler sur un deuxième graphe créé pour être perturbé.

g2=nx.Graph() # création du graphe g2 créé pour être perturbé
g2.add_edges_from(arcs) # le graphe g2 est au départ identique au graphe g
g2.remove_node(20) # suppression du nœud 20
aspl2=nx.average_shortest_path_length(g2) # calcul sur le graphe perturbé
vul20=aspl2-aspl # comparaison des valeurs obtenues pour un même indicateur

print(vul20) # affichage du résultat correspondant à la perturbation engendrée par la suppression du nœud 20

Dès lors, il est possible de généraliser ce calcul pour l’ensemble des nœuds du réseau étudié :

vul=[] # création du tableau qui contiendra les résultats
for i in range(len(g.nodes())): # boucle de la taille du nombre de nœuds
   g2=nx.Graph() # à chaque itération, création du graphe g2 qui sera perturbé
   g2.add_edges_from(arcs) # le graphe g2 est au départ identique au graphe g
   g2.remove_node(i) # à chaque itération, suppression d’un nœud (le numéro i)
   aspl2=nx.average_shortest_path_length(g2) # calcul sur le graphe perturbé
   dif=aspl2-aspl # à chaque itération on calcule la perturbation engendrée

   vul=vul+[(i,dif)] # ajout dans le tableau de la valeur obtenue pour un nœud supprimé

Néanmoins, à partir du 8ème élément (print(vul)), une erreur se produit. En effet, retirer un ou plusieurs éléments à un réseau n’est pas une opération neutre. En l’occurrence, certains éléments, s’ils sont retirés, peuvent notamment engendrer des ruptures de connexité (des pertes de connectivité). Ici, les ruptures de connexité engendrent l’impossibilité de calculer l’éloignement moyen puisque certaines distances deviennent incalculables (ces distances tendent en fait vers l’infini). Il faut alors créer des exceptions.

vul=[] # création du tableau qui contiendra les résultats
for i in range(len(g.nodes())): # même code que ci-dessus
   if (i!=8 and i!=53): # on rajoute juste deux exceptions
      g2=nx.Graph()
      g2.add_edges_from(arcs)
      g2.remove_node(i)
      aspl2=nx.average_shortest_path_length(g2)
      dif=aspl2-aspl

      vul=vul+[(i,dif)]

Comme précédemment, il est possible de trier les résultats obtenus afin d’identifier les éléments les plus problématiques. Il apparaît alors que ces éléments sont presque identiques à ceux apparaissant comme les plus centraux.

On peut alors chercher à mesurer le degré d’intensité de la relation existant entre la « vulnérabilité » et la « centralité » d’un nœud. Pour cela, il convient de trouver un indicateur de vulnérabilité pouvant se calculer pour tous les nœuds. NetworkX propose un indicateur qui fonctionne comme nous avons procédé, mais en utilisant un indicateur de proximité (closeness) et non d’éloignement, ce qui permet de palier au problème de rupture de connexité (en effet, en s’appuyant sur les inverses des distances, les valeurs tendant vers l’infini tendent vers zéro).

import numpy
vul2=nx.closeness_vitality(g) # calcul de vulnérabilité avec networkx
mvul2=list(vul2.items()) # transforme la bibliothèque en une liste
avul2=numpy.array(mvul2) # transforme cette liste en une array
x=avul2[:,1] # la variable x correspondant à la vulnérabilité des nœuds
mbet=list(bet.items()) # transforme la bibliothèque en une liste
abet=numpy.array(mbet) # on transforme cette liste en une array
y=abet[:,1] # la variable y correspondant à la centralité des nœuds
from scipy import stats
slope, intercept, r_value, p_value, std_err = stats.linregress(x,y)
r2=r_value*r_value # calcul du r2

print(r2) # affichage du r2

A la fin, on obtient un code propre que vous pouvez télécharger ici. S’il existe un lien entre vulnérabilité et centralité, celui-ci est imparfait. On essaiera de le montrer dans un prochain billet relatif à ces enseignements.

Serge Lhomme

Maitre de conférences à l'université Paris Est Créteil - Docteur en géographie de l'université Paris VII spécialité "dynamique des milieux et risques"

More Posts - Website

Analyse de réseau avec QGIS 2.x

Voici un cinquième billet concernant l’analyse de réseau et QGIS (billet 1, billet 2, billet 3, billet 4), car une certaine demande semble apparemment exister. En effet, lorsque que l’on consulte les statistiques du site, les pages relatives à QGIS et à la programmation Python apparaissent parmi les pages les plus visitées (pas autant que les pages relatives à R, mais quand même…). Les plugins présentés dans ces billets sont donc régulièrement téléchargés. En outre, comme quelques mises à jour ont été effectuées (les trois premiers plugins ont notamment été regroupés et complétés), il semble nécessaire de vous proposer un nouveau lien pour le télécharger (cliquez ici pour la télécharger).

Si vous voulez disposer à tout moment de la version la plus récente du plugin, je vous conseille de télécharger la version disponible sur mon site personnel (http://serge.lhomme.pagesperso-orange.fr/dev.html). Pour installer cette extension, il vous faudra tout d’abord dézipper le dossier téléchargé, puis copier le dossier « network_analysis » dans « C:\Program Files\QGIS xxxx\apps\qgis\python\plugins\ ». Lancez QGIS et le tour est joué !

Une seule chose est à vérifier. Lorsque vous dézippez un dossier sous Windows, il vous est proposé de l’envoyer vers un dossier du même nom. Ainsi, votre dossier « network_analysis » est lui-même dans un dossier « network_analysis ». Il faut éviter cette redondance. Ainsi, après avoir copié le dossier, vérifiez bien que votre arborescence est la suivante «C:\Program Files\QGIS xxxx\apps\qgis\python\plugins\network_analysis» et pas «C:\Program Files\QGIS xxxx\apps\qgis\python\plugins\network_analysis\network_analysis».

Pour effectuer les calculs, il est nécessaire de disposer de deux couches d’information géographique : une première couche contenant les nœuds et une deuxième couche contenant les arcs. La couche des nœuds doit impérativement contenir les identifiants des nœuds. Il est conseillé de choisir comme identifiant des valeurs numériques allant de 1 à n (vous pouvez télécharger un exemple ici). De même, la couche des arcs doit posséder un champ « identifiant ». De surcroît, cette couche doit impérativement comporter deux champs qui renvoient aux identifiants des nœuds formant les extrémités des arcs (vous pouvez télécharger un exemple ici).

Le fonctionnement de cette extension est très simple. Premièrement, elle repose sur une décomposition en trois familles d’algorithmes : les algorithmes de base (Degree ; Closeness ; Betweenness ; Eloignement ; Clustering ; Eigenvector ; Square_clustering ; Eccentricity ; Page rank ; Average neighbor degree ; Hub_dependence) ; les algorithmes de vulnérabilité (Allongement moyen ; Closeness vitality ; Dynamique ; Combinatoire) ; un algorithme de détection de communautés (Méthode de Louvain). Vous avez accès à ces familles d’algorithmes via le menu QGIS dans « Extension → Analyse de graphe… ».

qgisv2

Après avoir choisi votre famille d’algorithmes, une boite de dialogue s’ouvre vous demandant de sélectionner votre couche contenant les nœuds et votre couche contenant les arcs. Puis, après avoir appuyé sur OK, une deuxième boite de dialogue s’ouvre permettant de renseigner les principales propriétés de votre graphe. Il est alors possible de sélectionner le champ correspondant aux nœuds de départ, le champ correspondant aux nœuds d’arrivée, d’indiquer si le graphe est orienté ou non et si le réseau est valué ou non. Si le réseau est valué, il vous faudra indiquer le champ correspondant à cette valuation. Enfin, il faut choisir l’algorithme que vous souhaitez appeler et si les résultats doivent être normalisés ou non.

boite

A l’heure actuelle tous les calculs se font sur les nœuds. A l’avenir, les calculs sur les liens seront proposés. Ainsi, une fois que vos choix ont été validés, puis les calculs exécutés, les résultats sont stockés dans la table comportant les nœuds. Grâce à QGIS, vous pouvez alors effectuer des discrétisations ou des caractérisations à partir du champ créé.

A noter que seul le calcul de vulnérabilité combinatoire diffère de ce fonctionnement général avec une boite de dialogue supplémentaire à compléter et des résultats stockés sous la forme d’une image.

Serge Lhomme

Maitre de conférences à l'université Paris Est Créteil - Docteur en géographie de l'université Paris VII spécialité "dynamique des milieux et risques"

More Posts - Website

Analyse (et visualisation) de réseaux avec NetworkX – 1

NetworkX est un module python dédié à l’analyse de réseaux. Déconseillé comme outil d’apprentissage, il pourrait s’avérer utile pour des personnes plus expertes, notamment pour l’analyse des réseaux bipartis où le choix reste limité (y compris dans R, le package bipartite n’intégrant guère de mesures récentes). Avant de découvrir NetworkX, il est indispensable de savoir utiliser Python (écriture et exécution d’un script .py). Il exige des centaines de ressources en ligne sur le sujet et je n’ai pas les compétences permettant d’en recommander une en particulier. Mais j’insiste : soit vous cherchez des mesures non existantes ailleurs (ex. plus courts chemins dans des graphes bipartis), vous avez des notions en programmation et l’investissement peut être rentable ; soit vous vous en sortez très bien avec vos logiciels préférés et, à moins d’animer un blog sur l’analyse de réseaux, l’investissement n’est peut-être pas tout à fait nécessaire… Je précise pour finir que je suis encore très loin de maîtriser l’outil (je n’arrive par exemple pas encore à importer des attributs pour les sommets) et que ce billet est très perfectible. Les phrases suivies d’un * sont à considérer comme de possibles erreurs de ma part…

Comment ça marche ?

Le principe est simple : écrire un script python utilisant les commandes du module NetworkX et l’exécuter. Le graphe analysé peut être créé à l’intérieur même du script ou généré à partir d’une liste de liens, et l’importation de fichiers d’autres formats sont également possibles (GEXF, GraphML, Pajek, JSON etc. voir la liste complète ici). Les résultats et les sorties graphiques peuvent être exportés pour être utilisés dans un environnement logiciel autre (format image ou vectoriel).

Comme souvent, il est possible de générer directement son réseau (sommets, liens et attributs éventuels pour les deux) dans NetworkX. Et comme presque toujours, cette option (longuement développé dans tous les tutoriels disponibles) n’a à peu près aucun intérêt dans la mesure où nos données sont déjà existantes dans un format quelconque… Je vais prendre ici le cas le plus simple – ou en tout cas, l’un des plus répandus – dans la mesure où les données sont sous la forme d’une liste de liens possédant un attribut (intensité). Représenter un sommet isolé oblige à créer une boucle.  NetworkX permet également de lire des fichiers structurés sous la forme :

a b c

b d e  etc. où le premier sommet est l’origine du lien et les sommets suivants ses voisins (fonction networkx.read_adjlist).

J’ai utilisé un format .txt et des espaces comme séparateurs pour créer un fichier liens.txt. Les attributs doivent être indiqués entre accolades. S’il n’y a pas d’attribut pour les liens, n’importe quel séparateur semble utilisable (j’ai testé avec des virgules et des points-virgules) ; par contre, s’il y a un attribut, l’espace semble être le séparateur à utiliser*. Le nom des attributs est entre apostrophes, suivi de :, puis de la valeur. S’il y a plusieurs attributs, les séparer par une virgule comme dans l’exemple ci-dessous.

a b {'weight':2, 'color':'green'}
a c {'weight':3, 'color':'green'}

Par défaut, NetworkX considère que le graphe est non orienté. Le script suivant permet d’importer le fichier, de le visualiser, d’effectuer des mesures standards, de le rendre non-orienté puis d’effectuer une nouvelle série de mesures. Le script et le fichier des liens sont téléchargeables sur github à l’adresse https://github.com/beauguitte/NetworkX_1. Je le reproduis in extenso pour les personnes ne souhaitant pas s’inscrire sur le git en question…

Exemple type

# coding: utf8
import networkx as nx
import matplotlib.pyplot as plt #pour la visualisation

#lecture d'une liste de liens
G=nx.read_edgelist("liens.txt", create_using=nx.DiGraph())

#visualisation
pos=nx.spring_layout(G)               #choix de l'algorithme
nx.draw(G, pos, with_labels=True)
plt.show()
#plt.savefig("edge_colormap.png")     #export dans un format donné

Ce qui donne à peu près ça.

plot_ networkx

#analyse
print "number of nodes : %s" % G.number_of_nodes()
print "number of edges : %s" % G.number_of_edges()
print G.in_degree()
print G.out_degree()

#passage en graphe non orienté
G=G.to_undirected()

print "degree of node a : %s" % G.degree("a") #sommet spécifique
print "degree (undirected) %s" % G.degree()   #ensemble des sommets
print "betweenness centrality %s" % nx.betweenness_centrality(G)
print "degree (normalized) %s" % nx.degree_centrality(G)

#transformer les mesures en attributs
deg = G.degree()
nx.set_node_attributes(G, 'degree', deg)

#visualisation
pos=nx.spring_layout(G)
nx.draw(G, pos, with_labels=True)
plt.show()

#recherche des cliques
cl = list(nx.find_cliques(G))
print "cliques %s" % cl

#plus courts chemins
pathlengths=[]

print("source vertex {target:length, }")
for v in G.nodes():
   spl=nx.single_source_shortest_path_length(G,v)
    print('%s %s' % (v,spl))
    for p in spl.values():
        pathlengths.append(p)
print('')
print("moyenne des plus court chemins %s" % (sum(pathlengths)/len(pathlengths)))

#distribution des plus courts chemins
dist={}
for p in pathlengths:
    if p in dist:
        dist[p]+=1
    else:
        dist[p]=1

print('')
print("length #paths")

print("densité : %s" % nx.density(G))
print("diamètre: %d" % nx.diameter(G))
print("eccentricity: %s" % nx.eccentricity(G))
print("centre : %s" % nx.center(G))
print("péripherie : %s" % nx.periphery(G))

En attendant la suite…

Sur le papier, NetworkX est plutôt séduisant pour l’analyse des graphes bipartites et/ou valués. En pratique, le coût d’entrée pour un programmateur peu avancé est assez élevé. Je compte m’acharner dessus dans les semaines à venir et proposer un billet un peu moins approximatif que celui-ci aussi vite que mes capacités d’apprentissage le permettront.

Et pour aller plus loin

Site : https://networkx.github.ioe/

La documentation disponible en ligne est exclusivement en anglais : voir par exemple la très bonne présentation de Derek Greene à cette adresse : http://mlg.ucd.ie/files/summer/tutorial.pdf