2025, Dec 09 12:02

Корректный импорт матрицы смежности в graph-tool из pandas

Почему при построении графа из pandas в graph-tool пропадают узлы и рёбра. Разбираем индексацию, форму матрицы SciPy и даём рабочий рецепт на COO.

Когда вы строите матрицу смежности из pandas DataFrame и передаёте её в graph-tool, легко получить визуализацию, в которой «теряются» рёбра или вершины. Типичный симптом — узел, у которого должно быть несколько связей, показан всего с одной, или в данных присутствует индекс вершины, а на графе его нет. Первопричина чаще всего в том, как обрабатываются индексы и размеры при переходе DataFrame → матрица → граф.

Демонстрация проблемы

Данные — это список рёбер из двух столбцов. В pandas матрица смежности делается симметричной, затем преобразуется в разреженную матрицу SciPy и визуализируется в graph-tool. Следующий пример воспроизводит типичный сценарий с ошибкой:

import numpy as np
import scipy
import pandas as pd
from graph_tool.all import *
edges_df = pd.DataFrame({
    'p1': [1, 1, 2, 2, 2, 2, 3, 3, 3, 3],
    'p2': [2, 4, 3, 4, 5, 14, 4, 5, 14, 17]
})
def make_square_adj(tbl, c_left, c_right):
    tbl_ct = pd.crosstab(tbl[c_left], tbl[c_right])
    unified = tbl_ct.columns.union(tbl_ct.index)
    out = tbl_ct.reindex(index=unified, columns=unified, fill_value=0)
    return out
m_ab = make_square_adj(edges_df, 'p1', 'p2')
m_ba = make_square_adj(edges_df, 'p2', 'p1')
sym_df = m_ab + m_ba
arr_dense = sym_df.to_numpy()
G_bad = Graph(scipy.sparse.lil_matrix(arr_dense), directed=False)
graph_draw(G_bad, vertex_text=G_bad.vertex_index)

Что на самом деле идёт не так

Есть два тонких, но критически важных момента. Во‑первых, вершины в graph-tool нумеруются с нуля при импорте матрицы SciPy. Во‑вторых, если вы строите разреженную матрицу, не задавая явно её форму через максимальный идентификатор вершины, внутреннее индексирование охватит только присутствующие координаты или размеры переданного плотного массива — ни то, ни другое не гарантирует совпадение с исходными метками, если они начинаются с 1 или имеют разрывы. В итоге ваши семантические id узлов расходятся с позициями строк/столбцов матрицы, и это выглядит как пропавшие узлы или рёбра на схеме.

Если в списке рёбер встречаются метки вида 1, 2, 3, 5, 14, 17, то матрица по crosstab собирается по множеству уникальных значений, а затем конвертируется в плотный массив с индексами от 0 до len(unique)−1. Передавая его в graph-tool, вы получаете вершины 0..N−1, а не ваши исходные метки. Пробелы в нумерации только усиливают путаницу. Исправление — строить разреженную смежность напрямую в координатном формате (COO) с явной формой, охватывающей максимум id, затем симметризовать её для неориентированного графа и, при необходимости, убрать дубликаты мульти-рёбер.

Решение

Постройте смежность в формате COO прямо из списка рёбер, обеспечьте целочисленные типы, задайте форму как max(label) + 1, симметризуйте сложением с транспонированной матрицей и схлопните дубликаты порогом.

import numpy as np
import scipy
import pandas as pd
from graph_tool.all import *
edge_pairs = pd.DataFrame({
    'p1': [1, 1, 2, 2, 2, 2, 3, 3, 3, 3],
    'p2': [2, 4, 3, 4, 5, 14, 4, 5, 14, 17]
})
def build_coo_undirected(edge_tab, src_col, dst_col):
    src_vals, dst_vals = edge_tab[src_col].values, edge_tab[dst_col].values
    assert pd.api.types.is_integer_dtype(src_vals), "src_col must have integer type"
    assert pd.api.types.is_integer_dtype(dst_vals), "dst_col must have integer type"
    n_vertices = max(np.max(src_vals), np.max(dst_vals)) + 1
    unit_weights = np.ones_like(src_vals)
    coo_mat = scipy.sparse.coo_matrix(
        (unit_weights, (src_vals, dst_vals)), shape=(n_vertices, n_vertices)
    )
    coo_mat = coo_mat + coo_mat.T
    coo_mat = (coo_mat >= 1).astype('int8')
    return coo_mat
adj_coo = build_coo_undirected(edge_pairs, 'p1', 'p2')
G_ok = Graph(adj_coo, directed=False)
print(adj_coo.toarray())
graph_draw(G_ok, vertex_text=G_ok.vertex_index)

Почему это работает

Представление COO повторяет структуру списка рёбер: координаты и значения. Создание напрямую из двух столбцов избегает побочных эффектов переиндексации при использовании плотных промежуточных структур. Явное задание формы матрицы принудительно добавляет пустые строки/столбцы для отсутствующих меток между 0 и максимальным id, что стабилизирует индексацию вершин при импорте графа. Добавление транспонирования гарантирует симметрию для неориентированного графа. Порог по значениям больше или равным единице схлопывает дубликаты из обоих направлений в одиночные рёбра.

Почему это важно держать в уме

Конвейеры работы с графами, которые переходят между pandas, NumPy, SciPy и graph-tool, чувствительны к соглашениям об индексации и выводу формы. Визуально «неправильный» граф часто возникает из‑за безобидного на вид преобразования, которое незаметно перенумеровывает узлы. Если с самого начала правильно задать форму разреженной матрицы и индексацию, это сэкономит часы на поиске артефактов раскладки, которые на деле являются проблемами согласования данных.

Выводы

Передавайте в graph-tool разрежённую матрицу смежности, которая уважает ваши исходные целочисленные метки и явно задаёт форму. Оставляйте идентификаторы узлов целочисленными, собирайте матрицу в формате COO из списка рёбер, симметризуйте её для неориентированных графов и при необходимости удаляйте дубликаты. В этом случае визуализация отразит реальную связность ваших данных, а не побочные эффекты промежуточного переиндексирования.