web-dev-qa-db-fra.com

Peut-on obtenir des graphes hiérarchiques de networkx avec Python 3?

J'essaie d'afficher un graphe arborescent de ma hiérarchie de classe à l'aide denetworkx.je l'ai tout tracé correctement, et il affiche fine . Mais en tant que graphique circulaire avec des bords croisés, il s’agit d’une hiérarchie pure, et il me semble que je devrais pouvoir l’afficher sous forme d’arbre.

J'ai beaucoup cherché dans Google, et chaque solution proposée implique l'utilisation depygraphviz... mais PyGraphviz ne fonctionne pas avec Python 3 (documentation du site pygraphviz) .

Quelqu'un a-t-il pu obtenir un graphique en arborescence dans Python 3?

14
NickDanger66

edit (19 janv. 2019) J'ai mis à jour le code pour qu'il soit plus robuste: il fonctionne désormais sans modification pour les graphes dirigés et non dirigés, il n'est plus nécessaire que l'utilisateur spécifie la racine et il vérifie que le graphe est un arbre avant son exécution (sans le test, il aurait une récursion infinie - voir la réponse de user2479115 pour une manière de gérer les non-arbres).

edit (27 août 2018) Si vous voulez créer un tracé avec les noeuds apparaissant comme des anneaux autour du noeud racine, le code en bas à droite montre une modification simple pour ce faire

edit (17 sept. 2017) Je crois que le problème de pygraphviz qu'avait OP devait être résolu maintenant. Donc, pygraphviz est probablement une meilleure solution que celle que j'ai ci-dessous.


Voici un programme récursif simple pour définir les positions. La récurrence a lieu dans _hierarchy_pos, appelé par hierarchy_pos. Le rôle principal de hierarcy_pos consiste à effectuer quelques tests pour s'assurer que le graphique est approprié avant d'entrer la récursivité:

import networkx as nx
import random


def hierarchy_pos(G, root=None, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5):

    '''
    From Joel's answer at https://stackoverflow.com/a/29597209/2966723 

    If the graph is a tree this will return the positions to plot this in a 
    hierarchical layout.

    G: the graph (must be a tree)

    root: the root node of current branch 
    - if the tree is directed and this is not given, the root will be found and used
    - if the tree is directed and this is given, then the positions will be just for the descendants of this node.
    - if the tree is undirected and not given, then a random choice will be used.

    width: horizontal space allocated for this branch - avoids overlap with other branches

    vert_gap: gap between levels of hierarchy

    vert_loc: vertical location of root

    xcenter: horizontal location of root
    '''
    if not nx.is_tree(G):
        raise TypeError('cannot use hierarchy_pos on a graph that is not a tree')

    if root is None:
        if isinstance(G, nx.DiGraph):
            root = next(iter(nx.topological_sort(G)))  #allows back compatibility with nx version 1.11
        else:
            root = random.choice(list(G.nodes))

    def _hierarchy_pos(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5, pos = None, parent = None):
        '''
        see hierarchy_pos docstring for most arguments

        pos: a dict saying where all nodes go if they have been assigned
        parent: parent of this branch. - only affects it if non-directed

        '''

        if pos is None:
            pos = {root:(xcenter,vert_loc)}
        else:
            pos[root] = (xcenter, vert_loc)
        children = list(G.neighbors(root))
        if not isinstance(G, nx.DiGraph) and parent is not None:
            children.remove(parent)  
        if len(children)!=0:
            dx = width/len(children) 
            nextx = xcenter - width/2 - dx/2
            for child in children:
                nextx += dx
                pos = _hierarchy_pos(G,child, width = dx, vert_gap = vert_gap, 
                                    vert_loc = vert_loc-vert_gap, xcenter=nextx,
                                    pos=pos, parent = root)
        return pos


    return _hierarchy_pos(G, root, width, vert_gap, vert_loc, xcenter)

et un exemple d'utilisation:

import matplotlib.pyplot as plt
import networkx as nx
G=nx.Graph()
G.add_edges_from([(1,2), (1,3), (1,4), (2,5), (2,6), (2,7), (3,8), (3,9), (4,10),
                  (5,11), (5,12), (6,13)])
pos = hierarchy_pos(G,1)    
nx.draw(G, pos=pos, with_labels=True)
plt.savefig('hierarchy.png')

enter image description here

Idéalement, cela devrait redimensionner la séparation horizontale en fonction de la largeur des éléments situés en dessous. Je ne tente pas cela maintenant.

Expansion radiale

Disons que vous voulez que l'intrigue ressemble à ceci:

 enter image description here

Voici le code pour cela:

pos = hierarchy_pos(G, 0, width = 2*math.pi, xcenter=0)
new_pos = {u:(r*math.cos(theta),r*math.sin(theta)) for u, (theta, r) in pos.items()}
nx.draw(G, pos=new_pos, node_size = 50)
nx.draw_networkx_nodes(G, pos=new_pos, nodelist = [0], node_color = 'blue', node_size = 200)

edit - merci à Deepak Saini pour avoir signalé une erreur qui apparaissait auparavant dans les graphes orientés

25
Joel

Voici une solution pour les grands arbres. C'est une modification de l'approche récursive de Joel qui espace uniformément les nœuds à chaque niveau.

def hierarchy_pos(G, root, levels=None, width=1., height=1.):
    '''If there is a cycle that is reachable from root, then this will see infinite recursion.
       G: the graph
       root: the root node
       levels: a dictionary
               key: level number (starting from 0)
               value: number of nodes in this level
       width: horizontal space allocated for drawing
       height: vertical space allocated for drawing'''
    TOTAL = "total"
    CURRENT = "current"
    def make_levels(levels, node=root, currentLevel=0, parent=None):
        """Compute the number of nodes for each level
        """
        if not currentLevel in levels:
            levels[currentLevel] = {TOTAL : 0, CURRENT : 0}
        levels[currentLevel][TOTAL] += 1
        neighbors = G.neighbors(node)
        for neighbor in neighbors:
            if not neighbor == parent:
                levels =  make_levels(levels, neighbor, currentLevel + 1, node)
        return levels

    def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0):
        dx = 1/levels[currentLevel][TOTAL]
        left = dx/2
        pos[node] = ((left + dx*levels[currentLevel][CURRENT])*width, vert_loc)
        levels[currentLevel][CURRENT] += 1
        neighbors = G.neighbors(node)
        for neighbor in neighbors:
            if not neighbor == parent:
                pos = make_pos(pos, neighbor, currentLevel + 1, node, vert_loc-vert_gap)
        return pos
    if levels is None:
        levels = make_levels({})
    else:
        levels = {l:{TOTAL: levels[l], CURRENT:0} for l in levels}
    vert_gap = height / (max([l for l in levels])+1)
    return make_pos({})

L'exemple de Joel ressemblera à ceci:  enter image description here

Et ceci est un graphique plus complexe (rendu en utilisant plotly):  enter image description here

8
burubum

J'ai légèrement modifié pour qu'il ne soit pas recurse à l'infini. 

import networkx as nx

def hierarchy_pos(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5 ):
    '''If there is a cycle that is reachable from root, then result will not be a hierarchy.

       G: the graph
       root: the root node of current branch
       width: horizontal space allocated for this branch - avoids overlap with other branches
       vert_gap: gap between levels of hierarchy
       vert_loc: vertical location of root
       xcenter: horizontal location of root
    '''

    def h_recur(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5, 
                  pos = None, parent = None, parsed = [] ):
        if(root not in parsed):
            parsed.append(root)
            if pos == None:
                pos = {root:(xcenter,vert_loc)}
            else:
                pos[root] = (xcenter, vert_loc)
            neighbors = G.neighbors(root)
            if parent != None:
                neighbors.remove(parent)
            if len(neighbors)!=0:
                dx = width/len(neighbors) 
                nextx = xcenter - width/2 - dx/2
                for neighbor in neighbors:
                    nextx += dx
                    pos = h_recur(G,neighbor, width = dx, vert_gap = vert_gap, 
                                        vert_loc = vert_loc-vert_gap, xcenter=nextx, pos=pos, 
                                        parent = root, parsed = parsed)
        return pos

    return h_recur(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5)
7
user2479115

Le moyen le plus simple d'obtenir un bel affichage graphique sous Python 2 ou 3 sans PyGraphviz est d'utiliser PyDot ( https://pypi.python.org/pypi/pydot ). Alors que PyGraphviz fournit une interface à l'ensemble de Graphviz, PyDot ne fournit qu'une interface à l'outil Point de Graphviz, qui est le seul dont vous avez besoin si vous recherchez un graphique/arbre hiérarchique. Si vous souhaitez créer votre graphique dans NetworkX plutôt que dans PyDot, vous pouvez utiliser NetworkX pour exporter un graphique PyDot, comme suit:

import networkx as nx

g=nx.DiGraph()
g.add_edges_from([(1,2), (1,3), (1,4), (2,5), (2,6), (2,7), (3,8), (3,9),
                  (4,10), (5,11), (5,12), (6,13)])
p=nx.drawing.nx_pydot.to_pydot(g)
p.write_png('example.png')

Notez que Graphviz et PyDot doivent être installés pour que ce qui précède fonctionne correctement. 

 enter image description here

Avertissement: des problèmes ont été rencontrés lors de l’utilisation de PyDot pour dessiner des graphiques avec des dictionnaires d’attributs de noeud exportés à partir de NetworkX. Parfois, les dictionnaires semblent être exportés avec des guillemets manquants dans les chaînes, ce qui provoque le blocage de la méthode write. Cela peut être évité en laissant de côté les dictionnaires.

5
Westcroft_to_Apse

Pour un graphe orienté, Puisque voisins (x) n'incluent que les successeurs (x), vous devez donc supprimer les lignes:

if parent != None:
        neighbors.remove(parent)

En outre, une meilleure option serait la suivante:

pos=nx.graphviz_layout(G,prog='dot')
1
Deepak Saini