Nœuds de cluster NetworkX dans une formation circulaire basée sur la couleur des nœuds

Nov 30 2020

J'avais la même question que celle-ci . La solution fonctionne, cependant, je n'arrive pas à espacer les nœuds et à les faire apparaître dans un format circulaire avec mon ensemble de données. J'ai environ 30 nœuds au total qui sont codés par couleur.

Les nœuds de la même couleur se chevauchent au lieu d'être regroupés dans un format circulaire / plus concentrique.

J'ai utilisé le code dans la question ci-dessus et j'ai essayé toutes les valeurs de rayons possibles, mais je n'arrive pas à créer les nœuds du même groupe de couleurs dans un cercle .

Code:

import networkx
import numpy as np
import matplotlib.pyplot as plt

nodesWithGroup = {'A':'#7a8eff', 'B': '#7a8eff', 'C': '#eb2c30', 'D':'#eb2c30', 'E': '#eb2c30', 'F':'#730a15', 'G': '#730a15'}
# Set up graph, adding nodes and edges
G = nx.Graph()
G.add_nodes_from(nodesWithGroup.keys())

# Create a dictionary mapping color to a list of nodes
nodes_by_color = {}
for k, v in nodesWithGroup.items():
    if v not in nodes_by_color:
        nodes_by_color[v] = [k]
    else:
        nodes_by_color[v].append(k)

# Create initial circular layout
pos = nx.circular_layout(RRR)

# Get list of colors
colors2 = list(nodes_by_color.keys())
# clustering
angs = np.linspace(0, 2*np.pi, 1+len(colors))
repos = []
rad = 13
for ea in angs:
    if ea > 0:
        repos.append(np.array([rad*np.cos(ea), rad*np.sin(ea)]))

for color, nodes in nodes_by_color.items():
    posx = colors.index(color)
    for node in nodes:
        pos[node] += repos[posx]


# Plot graph
fig,ax = plt.subplots(figsize=(5, 5))


# node colors
teamX = ['A', 'B']
teamY = ['C', 'D', 'E']
teamZ = ['F', 'G']

for n in G.nodes():
    if n in teamX:
        G.nodes[n]['color'] = '#7a8eff'
    elif n in teamY:
        G.nodes[n]['color'] = '#eb2c30'
    else:
        G.nodes[n]['color'] = '#730a15'


colors = [node[1]['color'] for node in G.nodes(data=True)]

# edges
zorder_edges = 3
zorder_nodes = 4
zorder_node_labels = 5


for edge in G.edges():
    source, target = edge
    rad = 0.15
    node_color_dict = dict(G.nodes(data='color'))
    if node_color_dict[source] == node_color_dict[target]:
        arrowprops=dict(lw=G.edges[(source,target)]['weight'],
                        arrowstyle="-",
                        color='blue',
                        connectionstyle=f"arc3,rad={rad}",
                        linestyle= '-',
                        alpha=0.65, zorder=zorder_edges)
        ax.annotate("",
                    xy=pos[source],
                    xytext=pos[target],
                    arrowprops=arrowprops
                   )
    else:
        arrowprops=dict(lw=G.edges[(source,target)]['weight'],
                        arrowstyle="-",
                        color='purple',
                        connectionstyle=f"arc3,rad={rad}",
                        linestyle= '-',
                        alpha=0.65,  zorder=zorder_edges)
        ax.annotate("",
                    xy=pos[source],
                    xytext=pos[target],
                    arrowprops=arrowprops
                   )

# drawing 
node_labels_dict = nx.draw_networkx_labels(G, pos, font_size=5, font_family="monospace", font_color='white', font_weight='bold')


for color, nodes in nodes_by_color.items():
    nodes_draw = nx.draw_networkx_nodes(G, pos=pos, nodelist=nodes, node_color=color, edgecolors=[(0,0,0,1)])
    nodes_draw.set_zorder(zorder_nodes)
    for node_labels_draw in node_labels_dict.values():
        node_labels_draw.set_zorder(zorder_node_labels)




plt.show()

J'obtiens la sortie suivante:

Sortie souhaitée (comme dans la solution):

Réponses

2 PaulBrodersen Dec 07 2020 at 18:03

Comme @willcrack l'a suggéré, une légère adaptation de cette réponse fonctionne bien.

Vous pouvez ajuster le chevauchement des nœuds en modifiant le ratioparamètre dans partition_layout.

#!/usr/bin/env python

import numpy as np
import matplotlib.pyplot as plt
import networkx as nx


NODE_LAYOUT = nx.circular_layout
COMMUNITY_LAYOUT = nx.circular_layout


def partition_layout(g, partition, ratio=0.3):
    """
    Compute the layout for a modular graph.

    Arguments:
    ----------
    g -- networkx.Graph or networkx.DiGraph instance
        network to plot

    partition -- dict mapping node -> community or None
        Network partition, i.e. a mapping from node ID to a group ID.

    ratio: 0 < float < 1.
        Controls how tightly the nodes are clustered around their partition centroid.
        If 0, all nodes of a partition are at the centroid position.
        if 1, nodes are positioned independently of their partition centroid.

    Returns:
    --------
    pos -- dict mapping int node -> (float x, float y)
        node positions

    """

    pos_communities = _position_communities(g, partition)

    pos_nodes = _position_nodes(g, partition)
    pos_nodes = {k : ratio * v for k, v in pos_nodes.items()}

    # combine positions
    pos = dict()
    for node in g.nodes():
        pos[node] = pos_communities[node] + pos_nodes[node]

    return pos


def _position_communities(g, partition, **kwargs):

    # create a weighted graph, in which each node corresponds to a community,
    # and each edge weight to the number of edges between communities
    between_community_edges = _find_between_community_edges(g, partition)

    communities = set(partition.values())
    hypergraph = nx.DiGraph()
    hypergraph.add_nodes_from(communities)
    for (ci, cj), edges in between_community_edges.items():
        hypergraph.add_edge(ci, cj, weight=len(edges))

    # find layout for communities
    pos_communities = COMMUNITY_LAYOUT(hypergraph, **kwargs)

    # set node positions to position of community
    pos = dict()
    for node, community in partition.items():
        pos[node] = pos_communities[community]

    return pos


def _find_between_community_edges(g, partition):

    edges = dict()

    for (ni, nj) in g.edges():
        ci = partition[ni]
        cj = partition[nj]

        if ci != cj:
            try:
                edges[(ci, cj)] += [(ni, nj)]
            except KeyError:
                edges[(ci, cj)] = [(ni, nj)]

    return edges


def _position_nodes(g, partition, **kwargs):
    """
    Positions nodes within communities.
    """
    communities = dict()
    for node, community in partition.items():
        if community in communities:
            communities[community] += [node]
        else:
            communities[community] = [node]

    pos = dict()
    for community, nodes in communities.items():
        subgraph = g.subgraph(nodes)
        pos_subgraph = NODE_LAYOUT(subgraph, **kwargs)
        pos.update(pos_subgraph)

    return pos


def _layout(networkx_graph):
    edge_list = [edge for edge in networkx_graph.edges]
    node_list = [node for node in networkx_graph.nodes]

    pos = circular_layout(edge_list)

    # NB: some nodes might not be connected and hence will not be in the edge list.
    # Assuming a [0, 0, 1, 1] canvas, we assign random positions on the periphery
    # of the existing node positions.
    # We define the periphery as the region outside the circle that covers all
    # existing node positions.
    xy = list(pos.values())
    centroid = np.mean(xy, axis=0)
    delta = xy - centroid[np.newaxis, :]
    distance = np.sqrt(np.sum(delta**2, axis=1))
    radius = np.max(distance)

    connected_nodes = set(_flatten(edge_list))
    for node in node_list:
        if not (node in connected_nodes):
            pos[node] = _get_random_point_on_a_circle(centroid, radius)

    return pos


def _flatten(nested_list):
    return [item for sublist in nested_list for item in sublist]


def _get_random_point_on_a_circle(origin, radius):
    x0, y0 = origin
    random_angle = 2 * np.pi * np.random.random()
    x = x0 + radius * np.cos(random_angle)
    y = y0 + radius * np.sin(random_angle)
    return np.array([x, y])


def test():

    # create test data
    cliques = 8
    clique_size = 7
    g = nx.connected_caveman_graph(cliques, clique_size)
    partition = {ii : np.int(ii/clique_size) for ii in range(cliques * clique_size)}

    pos = partition_layout(g, partition, ratio=0.2)
    nx.draw(g, pos, node_color=list(partition.values()))
    plt.show()

def test2():

    # create test data
    cliques = 8
    clique_size = 7
    g = nx.connected_caveman_graph(cliques, clique_size)
    partition = {ii : np.int(ii/clique_size) for ii in range(cliques * clique_size)}

    # add additional between-clique edges
    total_nodes = cliques*clique_size
    for ii in range(cliques):
        start = ii*clique_size + int(clique_size/2)
        stop = (ii+cliques/2)*clique_size % total_nodes + int(clique_size/2)
        g.add_edge(start, stop)

    pos = partition_layout(g, partition, ratio=0.2)
    nx.draw(g, pos, node_color=list(partition.values()))
    plt.show()


if __name__ == '__main__':
    test()
    test2()

Addenda

Exemple avec des arêtes inter-cluster supplémentaires comme demandé dans les commentaires: