Как использовать аргумент `pos` в` networkx` для создания графика в стиле блок-схем? (Python 3)

Я пытаюсь создать линейный сетевой граф, используя Python (желательно с matplotlib и networkx, хотя он будет интересоваться bokeh), похожим по понятию ниже.

введите описание изображения здесь

Как этот график графика может быть построен эффективно (pos?) в Python с помощью networkx? Я хочу использовать это для более сложных примеров, поэтому я чувствую, что жесткое кодирование позиций для этого простой пример не будет полезен:(. Имеет ли networkx решение этого?

pos (словарь, необязательный) - словарь с узлами как ключи и позиции как значения. Если не указано, расположение макета spring будет вычисляться. См. Networkx.layout для функций, которые вычисляют nodeпозиции.

Я не видел ни одного учебника о том, как это можно достичь в networkx, поэтому я считаю, что этот вопрос станет надежным ресурсом для сообщества. Я подробно рассмотрел networkx учебники, и ничего подобного не существует. Макеты для networkx сделали бы этот тип сети невозможным для интерпретации без тщательного использования аргумента pos... который, я считаю, является моим единственным вариантом. Ни один из предварительно вычисленных макетов в документации https://networkx.github.io/documentation/networkx-1.9/reference/drawing.html, похоже, хорошо справляется с этим типом структуры сети.

Простой пример:

(A) каждый внешний ключ - это итерация в графе, перемещающемся слева направо (например, итерация 0 представляет образцы, итерация 1 имеет группы 1 - 3, то же самое с итерацией 2, итерация 3 имеет группы 1 - 2 и т.д..). (B) Внутренний словарь содержит текущую группировку на этой конкретной итерации, а веса для слияния предыдущих групп, которые представляют текущую группу (например, iteration 3 имеет Group 1 и Group 2, а для iteration 4 все iteration 3's > Group 2 перешел в iteration 4's Group 2, но iteration 3's Group 1 был разделен. Весы всегда суммируются до 1.

Мой код для соединений w/weight для вышеприведенного графика:

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}
            }
        }

Это то, что произошло, когда я сделал график в 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()])

введите описание изображения здесь

Примечание. Единственный другой способ, который я мог бы сделать, заключался бы в matplotlib создании графика рассеяния с каждым тиком, представляющим итерацию (5, включая исходные образцы), а затем подключение точек друг к другу с разными весами, Это был бы довольно грязный код, особенно пытающийся выровнять края маркеров с соединениями... Однако я не уверен, что это и networkx - лучший способ сделать это, или если есть инструмент (например, bokeh или plotly), который предназначен для такого типа построения.

Ответы

Ответ 1

Networkx имеет достойные объекты для составления разведочных данных анализ, это не инструмент, чтобы сделать показатели качества публикации, по разным причинам, что я не хочу вдаваться сюда. Я отсюда переписал эту часть базы кода с нуля и сделал автономный модуль рисования, называемый netgraph, который можно найти здесь (например, оригинал, основанный только на matplotlib). API очень, очень похоже и хорошо документирован, поэтому не должно быть слишком трудно формовать в ваших целях.

Основываясь на этом, я получаю следующий результат:

введите описание изображения здесь

Я выбрал цвет, чтобы обозначить силу края, как вы можете 1) указать отрицательные значения, и 2) лучше отличить небольшие значения.
Однако вы также можете передать ширину ребра в netgraph (см. netgraph.draw_edges()).

Разный порядок ветвей является результатом вашей структуры данных (a dict), которая не указывает на неотъемлемый порядок. Вам нужно будет изменить структуру данных и функцию _parse_input() ниже, чтобы исправить эту проблему.

код:

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