前言:在之前的网络分析中,大网络的中介中心性和接近中心性计算是一个困扰我挺久的问题,最近貌似找到了一些解决方法,在这里进行分享。

1.现有的计算方法存在的问题

之前基本上是通过python中的networkx进行的,以接近中心性为例,我们看一下networkx提供的代码。

if G.is_directed():
        G = G.reverse()  # create a reversed graph view

    if distance is not None:
        # use Dijkstra's algorithm with specified attribute as edge weight
        path_length = functools.partial(
            nx.single_source_dijkstra_path_length, weight=distance
        )
    else:
        path_length = nx.single_source_shortest_path_length

    if u is None:
        nodes = G.nodes
    else:
        nodes = [u]
    closeness_dict = {}
    for n in nodes:
        sp = path_length(G, n)
        totsp = sum(sp.values())
        len_G = len(G)
        _closeness_centrality = 0.0
        if totsp > 0.0 and len_G > 1:
            _closeness_centrality = (len(sp) - 1.0) / totsp
            # normalize to number of nodes-1 in connected part
            if wf_improved:
                s = (len(sp) - 1.0) / (len_G - 1)
                _closeness_centrality *= s
        closeness_dict[n] = _closeness_centrality
    if u is not None:
        return closeness_dict[u]
    return closeness_dict

这段代码优雅简洁,但是对于大网络而言,这种循环的计算方式会很慢。
我们再看一下其中核心的代码 _single_source_shortest_path_length

def _single_shortest_path_length(adj, firstlevel, cutoff):
    """Yields (node, level) in a breadth first search

    Shortest Path Length helper function
    Parameters
    ----------
        adj : dict
            Adjacency dict or view
        firstlevel : dict
            starting nodes, e.g. {source: 1} or {target: 1}
        cutoff : int or float
            level at which we stop the process
    """
    seen = {}  # level (number of hops) when seen in BFS
    level = 0  # the current level
    nextlevel = set(firstlevel)  # set of nodes to check at next level
    n = len(adj)
    while nextlevel and cutoff >= level:
        thislevel = nextlevel  # advance to next level
        nextlevel = set()  # and start a new set (fringe)
        found = []
        for v in thislevel:
            if v not in seen:
                seen[v] = level  # set the level of vertex v
                found.append(v)
                yield (v, level)
        if len(seen) == n:
            return
        for v in found:
            nextlevel.update(adj[v])
        level += 1
    del seen

这里的对所有的节点进行了一轮判断,计算与当前节点的距离。

2.修改方案

很直接的,我们基于_single_source_shortest_path_length实施多进程。
其实在构建最短距离矩阵的时候是有更有效的方法的,即弗洛伊德法,networkx也有现成的方法: nx.algorithms.floyd_warshall_numpy(graph),但是这需要构建一个巨大的格式为np.float64邻接矩阵,然后内存就爆了。这可能也是networkx自己也没有用弗洛伊德法计算度中心性的的原因,虽然慢,至少循环可以算出来。

这里贴出代码。

import networkx as nx
import multiprocessing as mp
from functools import partial
import numpy as np


def get_dis_single(graph_sub, k, node):
    print(node)
    path = nx.single_source_shortest_path_length(graph_sub, node)
    sun_length = np.sum([p[1] for p in path.items()])
    return node, k / sun_length


def get_clossness(graph):
    N = graph.number_of_nodes()
    result = []
    # 计算距离
    for c in nx.connected_components(graph):
        # 对于每一个连通体育分别计算
        graph_sub = graph.subgraph(c)
        node_list_sub = list(graph_sub.nodes)
        n = graph_sub.number_of_nodes()
        k = (n - 1) * (n - 1) / (N - 1)

        pool = mp.Pool()
        func = partial(get_dis_single, graph_sub, k)
        result.extend(pool.map(func, node_list_sub))
        pool.close()

    return dict(result)

if __name__ == '__main__':
    with open('../data/test_1000.json', 'r') as f:
        link_list = json.load(f)
    graph_test = nx.Graph()
    graph_test.add_edges_from(link_list)
    clossness = get_clossness(graph_test)

需要注意的是,在计算中介中心性和接近中心性的时候需要对不同的联通体分别计算,一方面这符合优化后的接近中心性的计算方法,另一方面这会让计算single_source_shortest_path_length的速度更快,从而提高整个多进程计算的速度。

3.修改方案升级

思路

这个方案其实是有优化空间的。

我们看这样一个图:

接近度中心性python 代码_算法


图中A的度为1,

A到B、C、D、E三个节点的距离为1,2,3,2

B到B、C、D、E三个节点的距离为0,1,2,1

A到图中其他节点的距离等于B到图中其他节点距离加1,A“继承”了B的所有的最短距离的信息。

所以可以先将所有度为1的节点移除,对移除节点后的网络进行最短距离计算,再补充节点为1的度。

这里说明两个问题:

  1. 这个对多进程是非常友好的,因为遍历的节点变少了。而且这一效果对与大网络尤其有效,现实的大网络的度分布往往是一个长尾分布,度为1的节点占比非常大。
  2. 我想过通过递归的方式来处理,事实上当我们的网络是树的时候确实可以通过递归的方式解决,而且应该很高效,但是真实的网络存在环,这导致递归多次之后的结果可能是一个难以处理的复杂的环(找不到度为1的点)。而且正如上一条所说,对于长尾分布的网络处理度为1的节点收益是非常大的(去除度为1的节点,网路会小很多),但是在此基础上处理度为2度为3的节点,收益会指数级下降(网络变小的速度呈指数级下降)。

在实际操作中需要注意对两类特殊的联通体分别进行处理。

接近度中心性python 代码_json_02

  1. 如上图中的(1)所示,移除度为1的节点后联通体就消失了。
  2. 如上图中的(2)所示,移除度为1的节点后会有度为0的节点。

这里贴出代码。

import networkx as nx
import multiprocessing as mp
from functools import partial
import numpy as np


def get_neighbor_unweighted(link_list):
    node2neighbor = defaultdict(set)
    for s, t in link_list:
        node2neighbor[s].add(t)
        node2neighbor[t].add(s)
    return node2neighbor

def remove_degree1(graph_sub, node2neighbor):
    # remove_degree1
    degree = nx.degree(graph_sub)
    node_degree1 = [node for node, deg in degree if deg == 1]
    graph_sub.remove_nodes_from(node_degree1)
    node_degree12neighbor = {node: list(node2neighbor[node])[0] for node in node_degree1}
    return graph_sub, node_degree12neighbor


def get_length_single(graph_sub, node_degree12neighbor, k, node):
    path = nx.single_source_shortest_path_length(graph_sub, node)
    for node_degree1, neighbor in node_degree12neighbor.items():
        path[node_degree1] = path[neighbor] + 1
    sun_length = np.sum([p[1] for p in path.items()])

    return node, k / sun_length


def get_clossness(graph, node2neighbor):
    N = graph.number_of_nodes()
    result = dict()
    # 计算距离
    for c in nx.connected_components(graph):
        # 对于每一个连通体育分别计算
        graph_sub = graph.subgraph(c).copy()
        n = graph_sub.number_of_nodes()
        if n == 2:
            # 网络中只有两个节点
            node_list_sub = list(graph_sub.nodes)
            result[node_list_sub[0]] = 1 / (N - 1)
            result[node_list_sub[1]] = 1 / (N - 1)
        else:
            # 网络中有多个节点
            graph_sub_mini, node_degree12neighbor = remove_degree1(graph_sub, node2neighbor)
            num_graph_sub_mini = graph_sub_mini.number_of_nodes()
            node_list_sub_mini = list(graph_sub_mini.nodes)

            if num_graph_sub_mini == 1:
                # 星形网络
                # 中心点
                node = node_list_sub_mini[0]
                # (n - 1) * (n - 1) / (N - 1) / (n - 1) = (n - 1) / (N - 1)
                result[node] = (n - 1) / (N - 1)
                # 外围点
                for node in node_degree12neighbor:
                    # (n - 1) * (n - 1) / (N - 1) / ((n - 2) * 2 + 1)
                    result[node] = (n - 1) * (n - 1) / (N - 1) / (2 * n - 3)

            else:
                # 一般网络
                k = (n - 1) * (n - 1) / (N - 1)
                pool = mp.Pool()
                func = partial(get_length_single, graph_sub_mini, node_degree12neighbor, k)
                result_ = pool.map(func, node_list_sub_mini)
                pool.close()
                pool.join()
                result_ = dict(result_)
                for node_degree1, neighbor in node_degree12neighbor.items():
                    result_[node_degree1] = k / (k / result_[neighbor] + n - 2)
                result.update(result_)

    return result

if __name__ == '__main__':
    with open('../data/test_1000.json', 'r') as f:
        link_list = json.load(f)
    graph_test = nx.Graph()
    graph_test.add_edges_from(link_list)
	node2neighbor = get_neighbor_unweighted(link_list)
    clossness = get_clossness(graph_test)

4.实验比较

我们通过生成随机网络来比较(每组实验测三次求平均)

num_node

num_link

1000

1000

2000

2000

5000

5000

10000

10000

networkx

0.76s

2.83s

17.88s

72.85s

mp_1

2.04s

4.82s

16.12s

50.17s

mp_2

0.20s

0.53s

1.95s

5.82s

我们可以看到mp_1前期是慢于networkx原始方法的,在节点连接规模达到5000时,两种方案的速度基本持平,节点连接规模达到10000时mp_1快于原始方法。

mp_2相较于mp_1和原始方法有很大的提升。

结语:
构建了一个多进程的接近中心性计算方法,通过500次随机网络的测试,与原始方法结果一致。
注意:本方法只用于无权无向网络,有权有向网络可以在此基础上拓展。