23 votes

Comment utiliser l'argument `pos` dans `networkx` pour créer un graphique de type organigramme (Python 3) ?

J'essaie de créer un graphique de réseau linéaire en utilisant Python (de préférence avec matplotlib y networkx bien qu'il s'intéresse à bokeh ) dont le concept est similaire à celui de l'exemple ci-dessous.

enter image description here

Comment construire ce graphique de manière efficace ( pos ?) en Python en utilisant networkx ? Je veux l'utiliser pour des exemples plus compliqués et j'ai l'impression que coder en dur les positions pour ce simple exemple ne sera pas utile :( . Est-ce que networkx avez-vous une solution à ce problème ?

pos (dictionnaire, facultatif) - Un dictionnaire avec les nœuds comme clés et positions comme valeurs. S'il n'est pas spécifié, un positionnement de mise en page à ressort sera sera calculé. Voir networkx.layout pour les fonctions qui calculent les positions des nœuds. positions.

Je n'ai vu aucun tutoriel sur la façon dont cela peut être réalisé en networkx c'est pourquoi je pense que cette question sera une ressource fiable pour la communauté. J'ai parcouru en détail le networkx tutoriels et rien de tel ne s'y trouve. Les mises en page pour networkx rendrait ce type de réseau impossible à interpréter sans une utilisation minutieuse des pos argument... qui je crois est ma seule option. Aucune des mises en page précalculées sur le site de l https://networkx.github.io/documentation/networkx-1.9/reference/drawing.html La documentation semble bien gérer ce type de structure de réseau.

Exemple simple :

(A) chaque clé extérieure est l'itération dans le graphe en se déplaçant de gauche à droite (par exemple, l'itération 0 représente les échantillons, l'itération 1 a les groupes 1 - 3, de même pour l'itération 2, l'itération 3 a les groupes 1 - 2, etc.) (B) Le dictionnaire interne contient le groupement actuel à cette itération particulière, et les poids des groupes précédents qui fusionnent et qui représentent le groupe actuel (ex. iteration 3 tiene Group 1 y Group 2 et pour iteration 4 tous les iteration 3's Group 2 est entré dans iteration 4's Group 2 mais iteration 3's Group 1 a été scindé. La somme des poids est toujours égale à 1.

Mon code pour les connexions avec les poids pour le graphique ci-dessus :

D_iter_current_previous =    {
        1: {
            "Group 1":{"sample_0":0.5, "sample_1":0.5, "sample_2":0, "sample_3":0, "sample_4":0},
            "Group 2":{"sample_0":0, "sample_1":0, "sample_2":1, "sample_3":0, "sample_4":0},
            "Group 3":{"sample_0":0, "sample_1":0, "sample_2":0, "sample_3":0.5, "sample_4":0.5}
            },
        2: {
            "Group 1":{"Group 1":1, "Group 2":0, "Group 3":0},
            "Group 2":{"Group 1":0, "Group 2":1, "Group 3":0},
            "Group 3":{"Group 1":0, "Group 2":0, "Group 3":1}
            },
        3: {
            "Group 1":{"Group 1":0.25, "Group 2":0, "Group 3":0.75},
            "Group 2":{"Group 1":0.25, "Group 2":0.75, "Group 3":0}
            },
        4: {
            "Group 1":{"Group 1":1, "Group 2":0},
            "Group 2":{"Group 1":0.25, "Group 2":0.75}
            }
        }

C'est ce qui s'est passé quand j'ai fait le graphique en networkx :

import networkx
import matplotlib.pyplot as plt

# Create Directed Graph
G = nx.DiGraph()

# Iterate through all connections
for iter_n, D_current_previous in D_iter_current_previous.items():
    for current_group, D_previous_weights in D_current_previous.items():
        for previous_group, weight in D_previous_weights.items():
            if weight > 0:
                # Define connections using `|__|` as a delimiter for the names
                previous_node = "%d|__|%s"%(iter_n - 1, previous_group)
                current_node = "%d|__|%s"%(iter_n, current_group)
                connection = (previous_node, current_node)
                G.add_edge(*connection, weight=weight)

# Draw Graph with labels and width thickness
nx.draw(G, with_labels=True, width=[G[u][v]['weight'] for u,v in G.edges()])

enter image description here

Note : La seule autre façon à laquelle j'ai pensé pour faire ceci serait dans matplotlib créer un nuage de points où chaque coche représente une itération (5, y compris les échantillons initiaux), puis relier les points les uns aux autres avec des pondérations différentes. Il s'agirait d'un code assez compliqué, surtout si l'on essayait d'aligner les bords des marqueurs avec les connexions... Cependant, je ne suis pas sûr que cette méthode et celle de l'analyse des données soient compatibles. networkx est la meilleure façon de le faire ou s'il existe un outil (par ex. bokeh o plotly ) qui est conçu pour ce type de tracé.

20voto

Paul Brodersen Points 3442

Networkx dispose d'outils de traçage décents pour les données exploratoires. pour l'analyse exploratoire des données, mais ce n'est pas l'outil qui permet de produire des chiffres de qualité pour la publication, pour diverses raisons que je ne souhaite pas aborder ici. J'ai donc réécrit cette partie de la base de code à partir de zéro, et j'ai fait une module de dessin autonome appelé netgraph qui peut être trouvé aquí (comme l'original purement basé sur matplotlib). L'API est très, très similaire et bien documentée, elle ne devrait donc pas être trop difficile de l'adapter à vos besoins.

En partant de là, j'obtiens le résultat suivant :

enter image description here

J'ai choisi la couleur pour indiquer la force du bord, comme vous pouvez le voir.
1) indiquent des valeurs négatives, et
2) mieux distinguer les petites valeurs.
Cependant, vous pouvez également passer une largeur d'arête à netgraph à la place (voir netgraph.draw_edges() ).

L'ordre différent des branches est le résultat de votre structure de données (un dict), qui n'indique aucun ordre inhérent. Vous devriez modifier votre structure de données et la fonction _parse_input() ci-dessous pour résoudre ce problème.

Code :

import itertools
import numpy as np
import matplotlib.pyplot as plt
import netgraph; reload(netgraph)

def plot_layered_network(weight_matrices,
                         distance_between_layers=2,
                         distance_between_nodes=1,
                         layer_labels=None,
                         **kwargs):
    """
    Convenience function to plot layered network.

    Arguments:
    ----------
        weight_matrices: [w1, w2, ..., wn]
            list of weight matrices defining the connectivity between layers;
            each weight matrix is a 2-D ndarray with rows indexing source and columns indexing targets;
            the number of sources has to match the number of targets in the last layer

        distance_between_layers: int

        distance_between_nodes: int

        layer_labels: [str1, str2, ..., strn+1]
            labels of layers

        **kwargs: passed to netgraph.draw()

    Returns:
    --------
        ax: matplotlib axis instance

    """
    nodes_per_layer = _get_nodes_per_layer(weight_matrices)

    node_positions = _get_node_positions(nodes_per_layer,
                                         distance_between_layers,
                                         distance_between_nodes)

    w = _combine_weight_matrices(weight_matrices, nodes_per_layer)

    ax = netgraph.draw(w, node_positions, **kwargs)

    if not layer_labels is None:
        ax.set_xticks(distance_between_layers*np.arange(len(weight_matrices)+1))
        ax.set_xticklabels(layer_labels)
        ax.xaxis.set_ticks_position('bottom')

    return ax

def _get_nodes_per_layer(weight_matrices):
    nodes_per_layer = []
    for w in weight_matrices:
        sources, targets = w.shape
        nodes_per_layer.append(sources)
    nodes_per_layer.append(targets)
    return nodes_per_layer

def _get_node_positions(nodes_per_layer,
                        distance_between_layers,
                        distance_between_nodes):
    x = []
    y = []
    for ii, n in enumerate(nodes_per_layer):
        x.append(distance_between_nodes * np.arange(0., n))
        y.append(ii * distance_between_layers * np.ones((n)))
    x = np.concatenate(x)
    y = np.concatenate(y)
    return np.c_[y,x]

def _combine_weight_matrices(weight_matrices, nodes_per_layer):
    total_nodes = np.sum(nodes_per_layer)
    w = np.full((total_nodes, total_nodes), np.nan, np.float)

    a = 0
    b = nodes_per_layer[0]
    for ii, ww in enumerate(weight_matrices):
        w[a:a+ww.shape[0], b:b+ww.shape[1]] = ww
        a += nodes_per_layer[ii]
        b += nodes_per_layer[ii+1]

    return w

def test():
    w1 = np.random.rand(4,5) #< 0.50
    w2 = np.random.rand(5,6) #< 0.25
    w3 = np.random.rand(6,3) #< 0.75

    import string
    node_labels = dict(zip(range(18), list(string.ascii_lowercase)))

    fig, ax = plt.subplots(1,1)
    plot_layered_network([w1,w2,w3],
                         layer_labels=['start', 'step 1', 'step 2', 'finish'],
                         ax=ax,
                         node_size=20,
                         node_edge_width=2,
                         node_labels=node_labels,
                         edge_width=5,
    )
    plt.show()
    return

def test_example(input_dict):
    weight_matrices, node_labels = _parse_input(input_dict)
    fig, ax = plt.subplots(1,1)
    plot_layered_network(weight_matrices,
                         layer_labels=['', '1', '2', '3', '4'],
                         distance_between_layers=10,
                         distance_between_nodes=8,
                         ax=ax,
                         node_size=300,
                         node_edge_width=10,
                         node_labels=node_labels,
                         edge_width=50,
    )
    plt.show()
    return

def _parse_input(input_dict):
    weight_matrices = []
    node_labels = []

    # initialise sources
    sources = set()
    for v in input_dict[1].values():
        for s in v.keys():
            sources.add(s)
    sources = list(sources)

    for ii in range(len(input_dict)):
        inner_dict = input_dict[ii+1]
        targets = inner_dict.keys()

        w = np.full((len(sources), len(targets)), np.nan, np.float)
        for ii, s in enumerate(sources):
            for jj, t in enumerate(targets):
                try:
                    w[ii,jj] = inner_dict[t][s]
                except KeyError:
                    pass

        weight_matrices.append(w)
        node_labels.append(sources)
        sources = targets

    node_labels.append(targets)
    node_labels = list(itertools.chain.from_iterable(node_labels))
    node_labels = dict(enumerate(node_labels))

    return weight_matrices, node_labels

# --------------------------------------------------------------------------------
# script
# --------------------------------------------------------------------------------

if __name__ == "__main__":

    # test()

    input_dict =   {
        1: {
            "Group 1":{"sample_0":0.5, "sample_1":0.5, "sample_2":0, "sample_3":0, "sample_4":0},
            "Group 2":{"sample_0":0, "sample_1":0, "sample_2":1, "sample_3":0, "sample_4":0},
            "Group 3":{"sample_0":0, "sample_1":0, "sample_2":0, "sample_3":0.5, "sample_4":0.5}
            },
        2: {
            "Group 1":{"Group 1":1, "Group 2":0, "Group 3":0},
            "Group 2":{"Group 1":0, "Group 2":1, "Group 3":0},
            "Group 3":{"Group 1":0, "Group 2":0, "Group 3":1}
            },
        3: {
            "Group 1":{"Group 1":0.25, "Group 2":0, "Group 3":0.75},
            "Group 2":{"Group 1":0.25, "Group 2":0.75, "Group 3":0}
            },
        4: {
            "Group 1":{"Group 1":1, "Group 2":0},
            "Group 2":{"Group 1":0.25, "Group 2":0.75}
            }
        }

    test_example(input_dict)

    pass

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X