web-dev-qa-db-ja.com

pythonの無向グラフでトライアドの国勢調査を効率的に計算する方法

_triad census_に対して次のように_undirected network_を計算しています。

_import networkx as nx
G = nx.Graph()
G.add_edges_from(
    [('A', 'B'), ('A', 'C'), ('D', 'B'), ('E', 'C'), ('E', 'F'),
     ('B', 'H'), ('B', 'G'), ('B', 'F'), ('C', 'G')])

from itertools import combinations
#print(len(list(combinations(G.nodes, 3))))

triad_class = {}
for nodes in combinations(G.nodes, 3):
    n_edges = G.subgraph(nodes).number_of_edges()
    triad_class.setdefault(n_edges, []).append(nodes)
print(triad_class)
_

小規模なネットワークで正常に動作します。ただし、現在は約4000〜8000ノードの大きなネットワークがあります。 1000ノードのネットワークで既存のコードを実行しようとすると、実行に数日かかります。これを行うより効率的な方法はありますか?

私の現在のネットワークはほとんどスパースです。つまり、ノード間の接続はほとんどありません。その場合、接続されていないノードを残して、最初に計算を行ってから、接続されていないノードを出力に追加できますか?

また、すべての組み合わせを計算することなく、おおよその回答を得ることができてうれしいです。

トライアドの国勢調査の例:

トライアドの国勢調査では、トライアド(3ノード)を次の図に示す4つのカテゴリに分割しています。

Four classes of triad census

たとえば、以下のネットワークを考えてみましょう。

enter image description here

4つのクラスのトライアドの国勢調査は次のとおりです。

_{3: [('A', 'B', 'C')], 
2: [('A', 'B', 'D'), ('B', 'C', 'D'), ('B', 'D', 'E')], 
1: [('A', 'B', 'E'), ('A', 'B', 'F'), ('A', 'B', 'G'), ('A', 'C', 'D'), ('A', 'C', 'E'), ('A', 'C', 'F'), ('A', 'C', 'G'), ('A', 'D', 'E'), ('A', 'F', 'G'), ('B', 'C', 'E'), ('B', 'C', 'F'), ('B', 'C', 'G'), ('B', 'D', 'F'), ('B', 'D', 'G'), ('B', 'F', 'G'), ('C', 'D', 'E'), ('C', 'F', 'G'), ('D', 'E', 'F'), ('D', 'E', 'G'), ('D', 'F', 'G'), ('E', 'F', 'G')], 
0: [('A', 'D', 'F'), ('A', 'D', 'G'), ('A', 'E', 'F'), ('A', 'E', 'G'), ('B', 'E', 'F'), ('B', 'E', 'G'), ('C', 'D', 'F'), ('C', 'D', 'G'), ('C', 'E', 'F'), ('C', 'E', 'G')]}
_

必要に応じて詳細をお知らせいたします。

編集:

回答で提案されているように、#print(len(list(combinations(G.nodes, 3))))行にコメントを付けることで、_memory error_を解決できました。しかし、私のプログラムはまだ遅く、1000ノードのネットワークでも実行に数日かかります。私はこれをpythonで行うより効率的な方法を探しています。

networkxに限らず、他のライブラリや言語を使用して回答を受け入れることもできます。

いつものように、必要に応じて詳細を提供させていただきます。

16
EmJ

考え方は簡単です。グラフで直接作業する代わりに、隣接行列を使用します。これはもっと効率的だと思いました、そして私は正しかったようです。

Adjacency matrix for example

隣接行列では、1は2つのノード間にエッジがあることを示します。たとえば、最初の行は「AとBの間にリンクがあり、Cがある」と読むことができます。

そこから私はあなたの4つのタイプを見て、次のものを見つけました:

  • タイプ3の場合、N1とN2、N1とN3、およびN2とN3の間にエッジが必要です。隣接行列では、各行(各行はノードとその接続を表し、これはN1です)を調べて、接続先のノード(N2になる)を見つけることでこれを見つけることができます。次に、N2の行で、接続されているすべてのノード(これはN3です)をチェックし、N1の行に正のエントリがあるノードを保持します。この例は「A、B、C」で、AはBに接続しています。BはCに接続しており、AもCに接続しています。

  • タイプ2の場合、タイプ3とほぼ同じように機能します。ただし、N1の行でN3列の0を見つけたい場合を除きます。この例は「A、B、D」です。 AにはBへの接続があり、BにはD列に1がありますが、Aにはありません。

  • タイプ1の場合、N2の行を見て、N1行とN2行の両方が0であるすべての列を見つけます。

  • 最後に、タイプ0の場合、エントリが0であるN1行のすべての列を確認し、それらの行を確認して、0があるすべての列を見つけます。

このコードはあなたのために働くはずです。 1000ノードの場合、(i7-8565U CPUを搭載したマシンで)約7分かかりましたが、それでも比較的低速ですが、現在ソリューションを実行するのにかかる数日とはかけ離れています。結果を確認できるように、写真の例を含めました。あなたのコードは、ところであなたが以下に示す例とは異なるグラフを生成します。コード内のサンプルグラフと隣接行列は、どちらも含まれている画像を参照しています。

1000ノードの例では networkx.generators.random_graphs.fast_gnp_random_graph を使用しています。 1000はノード数、0.1はエッジ作成の確率、シードは一貫性のためです。グラフがスパースであるとおっしゃっていたので、エッジ作成の確率を設定しました。

networkx.linalg.graphmatrix.adjacency_matrix : "純粋なPython隣接行列表現が必要な場合は、辞書の辞書形式を返すnetworkx.convert.to_dict_of_dictsを試してくださいそれは疎行列として扱うことができます。」

辞書構造にはM辞書(=行)があり、最大M辞書がネストされています。ネストされた辞書は空であるため、その中にキーが存在するかどうかを確認することは、上記の1または0を確認することと同じです。

import time

import networkx as nx


def triads(m):
    out = {0: set(), 1: set(), 2: set(), 3: set()}
    nodes = list(m.keys())
    for i, (n1, row) in enumerate(m.items()):
        print(f"--> Row {i + 1} of {len(m.items())} <--")
        # get all the connected nodes = existing keys
        for n2 in row.keys():
            # iterate over row of connected node
            for n3 in m[n2]:
                # n1 exists in this row, all 3 nodes are connected to each other = type 3
                if n3 in row:
                    if len({n1, n2, n3}) == 3:
                        t = Tuple(sorted((n1, n2, n3)))
                        out[3].add(t)
                # n2 is connected to n1 and n3 but not n1 to n3 = type 2
                else:
                    if len({n1, n2, n3}) == 3:
                        t = Tuple(sorted((n1, n2, n3)))
                        out[2].add(t)
            # n1 and n2 are connected, get all nodes not connected to either = type 1
            for n3 in nodes:
                if n3 not in row and n3 not in m[n2]:
                    if len({n1, n2, n3}) == 3:
                        t = Tuple(sorted((n1, n2, n3)))
                        out[1].add(t)
        for j, n2 in enumerate(nodes):
            if n2 not in row:
                # n2 not connected to n1
                for n3 in nodes[j+1:]:
                    if n3 not in row and n3 not in m[n2]:
                        # n3 is not connected to n1 or n2 = type 0
                        if len({n1, n2, n3}) == 3:
                            t = Tuple(sorted((n1, n2, n3)))
                            out[0].add(t)
    return out


if __name__ == "__main__":
    g = nx.Graph()
    g.add_edges_from(
        [("E", "D"), ("G", "F"), ("D", "B"), ("B", "A"), ("B", "C"), ("A", "C")]
    )
    _m = nx.convert.to_dict_of_dicts(g)
    _out = triads(_m)
    print(_out)

    start = time.time()
    g = nx.generators.fast_gnp_random_graph(1000, 0.1, seed=42)
    _m = nx.convert.to_dict_of_dicts(g)
    _out = triads(_m)
    end = time.time() - start
    print(end)
5
Lomtrur

数字を確認してみましょう。 nを頂点の数、eエッジの数とします。

0トライアドはO(n ^ 3)にあります

1トライアドはO(e * n)にあります

2 + 3トライアドはO(e)にあります

2 + 3トライアドを取得するには:

For every node a:
   For every neighbor of a b:
      For every neighbor of b c:
        if a and c are connected, [a b c] is a 3 triad
        else [a b c] is a 2 triad
   remove a from list of nodes (to avoid duplicate triads)

次のステップは、目標が何であるかによって異なります。 1と0のトライアドの数だけが必要な場合は、これで十分です。

#(1 triads) = e * (n -2) - #(2 triads) - #(3 triads)

#(0 triads) = {n \choose 3} - #(3 triads) - #(2 triads) - #(1 triads)

説明:

1トライアドはすべて接続ノード+ 1非接続ノードなので、接続ノードの数+ 1つの他のノードを計算することによって数を取得し、他のノードが接続されているケース(2および3トライアド)を差し引きます

0トライアドは、ノードのすべての組み合わせから他のトライアドを差し引いたものです。

トライアドを実際にリストする必要がある場合、何をしようとも、0トライアドをリストすることはO(n ^ 3)であり、グラフが大きくなるとあなたを殺してしまうので、ほとんど運がありません。

上記の2 + 3トライアドのアルゴリズムはO(e * max(#neighbors))にあり、他の部分はノードとエッジをカウントするためにO(e + n)にあります。 0トライアドを明示的にリストする必要があるO(n ^ 3)よりはるかに優れています。 1つのトライアドのリストは、O(e * n)でも実行できます。

5
kutschkem
import networkx as nx
from time import sleep
from itertools import combinations


G = nx.Graph()
arr=[]
for i in range(1000):
    arr.append(str(i))

for i,j in combinations(arr, 2):
    G.add_edges_from([(i,j)])

#print(len(list(combinations(G.nodes, 3))))
triad_class = [[],[],[],[]]

for nodes in combinations(G.subgraph(arr).nodes, 3):
            n_edges = G.subgraph(nodes).number_of_edges()
            triad_class[n_edges].append(nodes)


print(triad_class)

辞書は指数関数的に増加し、より多くの時間がかかるため、リストを使用すると辞書よりも高速に挿入されると思います。

2
Jainil Patel
  1. すべての組み合わせをリストに変換しようとすると、おそらくプログラムがクラッシュします:print(len(list(combinations(G.nodes, 3))))combinationsは少量のメモリを消費するイテレータを返すため、絶対に実行しないでください。ただし、リストはギガバイトのメモリを簡単に消費する可能性があります。

  2. スパースグラフがある場合、 接続されたコンポーネント でトライアドを見つけるのがより合理的です:nx.connected_components(G)

  3. Networkxには triads サブモジュールがありますが、あなたに合わないようです。 networkx.algorithms.triadsコードを変更して、カウントではなくトライアドを返すようにしました。あなたはそれを見つけることができます ここ 。 DiGraphsを使用することに注意してください。無向グラフで使用する場合は、最初に有向グラフに変換する必要があります。

2
vurmux