def degree_sequence(network: Network, weight: Weight = None) -> np.array: """Calculates the degree sequence of a network. Parameters ---------- network : Network The :py:class:`Network` object that contains the network weights : bool If True weighted degrees will be calculated Examples -------- Generate a simple network >>> import pathpy as pp >>> net = pp.Network(directed=False) >>> net.add_edge('a', 'b', weight = 2.1) >>> net.add_edge('a', 'c', weight = 1.0) >>> s = pp.algorithms.statistics.degrees.degree_sequence(net) >>> s np.array([2., 1., 1.]) Return weighted degree sequence >>> s = pp.algorithms.statistics.degrees.degree_sequence(net,weight=True) >>> s array([3.1, 2.1, 1.0]) """ _degrees = np.zeros(network.number_of_nodes(), dtype=float) for v in network.nodes.uids: _degrees[network.nodes.index[v]] = network.degrees(weight=weight)[v] return _degrees
def ER_np_randomize(network: Network, loops: bool = False) -> Network: """Generates a random microstate based on the G(n,p) model. The number of nodes, the expected number of edges, the edge directedness and the node uids of the generated network match the corresponding values of a given network instance. """ n = network.number_of_nodes() m = network.number_of_edges() M = max_edges(n, directed=network.directed, loops=loops) p = m/M return ER_np(n=n, p=p, directed=network.directed, loops=loops, node_uids=list(network.nodes.uids))
def Q_max_modularity(network: Network, cluster_mapping: Dict) -> float: """Computes the maximum theoretically possible Q-modularity for a given network and cluster mapping """ m = network.number_of_edges() qmax: float = 2*m for v in network.nodes.uids: for w in network.nodes.uids: if cluster_mapping[v] == cluster_mapping[w]: qmax -= network.degrees()[v]*network.degrees()[w]/(2*m) return qmax/(2*m)
def Q_modularity(network: Network, cluster_mapping: Dict) -> float: """Computes the Q-modularity of a network for a given cluster mapping """ A = network.adjacency_matrix() m = network.number_of_edges() q = 0.0 for v in network.nodes.uids: for w in network.nodes.uids: if cluster_mapping[v] == cluster_mapping[w]: q += A[network.nodes.index[v], network.nodes.index[w]] - \ network.degrees()[v] * network.degrees()[w]/(2*m) return q/(2*m)
def degree_centrality(network: Network, mode: str = 'degree') -> dict: """Calculates the degree centrality of all nodes. Parameters ---------- network : Network The :py:class:`Network` object that contains the network mode : str Can be chose nas 'degree', 'indegree', or 'outdegree'. Determines whether to calculate undirected/total degrees, indegrees, or degrees Examples -------- Compute degree centrality in a simple network >>> import pathpy as pp >>> net = pp.Network(directed=True) >>> net.add_edge('a', 'x') >>> net.add_edge('x', 'b') >>> c = pp.algorithms.centralities.degree_centrality(net) >>> c['a'] 1 >>> c = pp.algorithms.centralities.degree_centrality(net, mode='indegree') >>> c['a'] 0 """ d: dict = dict() if mode not in set(['degree', 'indegree', 'outdegree']): LOG.error('Mode must be \'degree\', \'indegree\' or \'outdegree\'') raise KeyError for v in network.nodes.keys(): if mode == 'indegree': d[v] = network.indegrees()[v] elif mode == 'outdegree': d[v] = network.outdegrees()[v] else: d[v] = network.degrees()[v] return d
def ER_nm_randomize(network: Network, loops: bool = False, multiedges: bool = False) -> Union[Network, None]: """Generates a random graph whose number of nodes, edges, edge directedness and node uids match the corresponding values of a given network instance. Useful to generate a randomized version of a network. Parameters ---------- network : pathpy.Network Given network used to determine number of nodes, edges, node uids, and edge directedness loops : bool Whether or not the generated network can contain loops. multi_edge : bool Whether or not multiple edges can be added to the same node pair Examples -------- Generate random undirected network with 10 nodes and 25 edges >>> import pathpy as pp >>> n = pp.Network(directed=False) >>> n.add_edge('a', 'b') >>> n.add_edge('b', 'c') >>> n.add_edge('d', 'e') >>> r = pp.generators.ER_nm(n) >>> print(r) Uid: 0x... Type: Network Directed: False Unique nodes: 5 Unique edges: 3 Unique paths: 0 Total paths: 0 >>> print(r.nodes.uids) { 'a', 'b', 'c', 'd', 'e'} """ return ER_nm(network.number_of_nodes(), network.number_of_edges(), directed=network.directed, loops=loops, multiedges=multiedges, node_uids=list(network.nodes.uids))
def local_clustering_coefficient(network: Network, v: str) -> float: """Calculates the local clustering coefficient of a node in a network. The local clustering coefficient of any node with an (out-)degree smaller than two is defined as zero. For all other nodes, it is defined as: cc(c) := 2*k(i)/(d_i(d_i-1)) or cc(c) := k(i)/(d_out_i(d_out_i-1)) in undirected and directed networks respectively. Parameters ---------- network : Network The network in which to calculate the local clustering coefficient node : str The node for which the local clustering coefficient shall be calculated """ lcc: float = 0. d = network.degrees() o = network.outdegrees() if network.directed and o[v] >= 2 or network.directed == False and d[ v] >= 2: k: int = 0 # compute set of closed triads closed = closed_triads(network, v) k = len(closed) if network.directed: return k / (o[v] * (o[v] - 1)) else: return 2 * k / (d[v] * (d[v] - 1)) else: return 0.
def edge_reciprocity(network: Network) -> float: """ """ if network.directed == False: return 1.0 else: reciproc = 0 for e in network.edges: if (e.w, e.v) in network.edges: reciproc += 1.0 return reciproc / network.number_of_edges()
def Molloy_Reed_randomize(network: Network) -> Optional[Network]: """Generates a random realization of a given network based on the observed degree sequence. """ # degrees are listed in order of node indices degrees = network.degree_sequence() # generate node uids in same order node_uids = ['-']*len(degrees) for v in network.nodes.uids: node_uids[network.nodes.index[v]] = v return Molloy_Reed(degrees, node_uids=node_uids)
def from_networkx(graph: Any) -> Network: n = Network(directed=graph.is_directed(), multiedges=graph.is_multigraph()) for v in graph.nodes: n.add_node(v, **graph.nodes[v]) for e in graph.edges: n.add_edge(e[0], e[1], **graph.edges[e]) return n
def degree_assortativity(network: Network, mode: str = 'total', weight: Weight = None) -> float: """Calculates the degree assortativity coefficient of a network. Parameters ---------- network : Network The network in which to calculate the Molloy-Reed fraction """ A = network.adjacency_matrix(weight=weight) m = np.sum(A) d = network.degrees(weight) if network.directed and mode == 'in': d = network.indegrees(weight) elif network.directed and mode == 'out': d = network.outdegrees(weight) elif network.directed and mode == 'total': d = network.degrees(weight) elif not network.directed: m = m / 2. idx = network.nodes.index cov: float = 0. var: float = 0. for i in network.nodes.keys(): for j in network.nodes.keys(): cov += (A[idx[i], idx[j]] - (d[i] * d[j]) / (2 * m)) * d[i] * d[j] if i != j: var -= (d[i] * d[j]) / (2 * m) * d[i] * d[j] else: var += (d[i] - (d[i] * d[j]) / (2 * m)) * d[i] * d[j] return cov / var
def lattice_network(start: Optional[int]=0, stop: Optional[int]=10, dims: Optional[int]=2) -> Network: """ Generates a n-dimensional lattice network with coordinates in each dimension ranging from start (inclusive) to stop (exclusive) """ network = Network(directed=False) for pos in _multi_dim_range(start, stop, dims): network.add_node(Node("".join(str(i)+'-' for i in pos).strip('-'), pos=np.array(pos))) for v in network.nodes: for w in network.nodes: if np.sum(np.abs(v['pos']-w['pos']))==1 and (v.uid, w.uid) not in network.edges: network.add_edge(v, w) return network
def Molloy_Reed(degrees: Union[np.array, Dict[int, float]], multiedge: bool = False, relax: bool=False, node_uids: Optional[list] = None) -> Optional[Network]: """Generate Molloy-Reed graph. Generates a random undirected network with given degree sequence based on the Molloy-Reed algorithm. .. note:: The condition proposed by Erdös and Gallai (1967) is used to test whether the degree sequence is graphic, i.e. whether a network with the given degree sequence exists. Parameters ---------- degrees : list List of integer node degrees. The number of nodes of the generated network corresponds to len(degrees). relax : bool If True, we conceptually allow self-loops and multi-edges, but do not add them to the network This implies that the generated network may not have exactly sum(degrees)/2 links, but it ensures that the algorithm always finishes. node_uids : list Optional list of node uids that will be used. Examples -------- Generate random undirected network with given degree sequence >>> import pathpy as pp >>> random_network = pp.algorithms.random_graphs.Molloy_Reed([1,0]) >>> print(random_network.summary()) ... Network generation fails for non-graphic sequences >>> import pathpy as pp >>> random_network = pp.algorithms.random_graphs.Molloy_Reed([1,0]) >>> print(random_network) None """ # assume that we are given a graphical degree sequence if not is_graphic_Erdos_Gallai(degrees): return None # create empty network with n nodes n = len(degrees) network = Network(directed=False, multiedges=multiedge) if node_uids is None or len(node_uids) != n: LOG.info('No valid node uids given, generating numeric node uids') node_uids = [] for i in range(n): node_uids.append(str(i)) for i in range(n): network.add_node(node_uids[i]) # generate link stubs based on degree sequence stubs = [] for i in range(n): for _ in range(int(degrees[i])): stubs.append(str(node_uids[i])) # connect randomly chosen pairs of link stubs while(len(stubs) > 0): v, w = np.random.choice(stubs, 2, replace=False) if v == w or (multiedge == False and relax == False and network.nodes[w] in network.successors[v]): # remove random edge and add stubs if network.number_of_edges()>0: edge = random.choice(list(network.edges)) stubs.append(edge.v.uid) stubs.append(edge.w.uid) network.remove_edge(edge) else: if not network.nodes[w] in network.successors[v]: network.add_edge(v, w) stubs.remove(v) stubs.remove(w) return network
def Watts_Strogatz(n: int, s: int, p: float = 0.0, loops: bool = False, node_uids: Optional[list] = None) -> Network: """Undirected Watts-Strogatz lattice network Generates an undirected Watts-Strogatz lattice network with lattice dimensionality one. Parameters ---------- n : int The number of nodes in the generated network s : float The number of nearest neighbors that will be connected in the ring lattice p : float The rewiring probability node_uids : list Optional list of node uids that will be used. Examples -------- Generate a Watts-Strogatz network with 100 nodes >>> import pathpy as pp >>> small_world = pp.algorithms.random_graphs.Watts_Strogatz(n=100, s=2, p=0.1) >>> print(small_world.summary()) ... """ network = Network(directed=False) if node_uids is None or len(node_uids) != n: LOG.info('No valid node uids given, generating numeric node uids') node_uids = [] for i in range(n): network.add_node(Node(str(i))) node_uids.append(str(i)) else: for i in range(n): network.add_node(node_uids[i]) # construct a ring lattice (dimension 1) for i in range(n): if loops: x = 0 y = s else: x = 1 y = s+1 for j in range(x, y): v = network.nodes[node_uids[i]] w = network.nodes[node_uids[(i+j) % n]] if (v.uid, w.uid) not in network.edges: network.add_edge(v, w) if p == 0: # nothing to do here return network # Rewire each link with probability p for edge in tqdm(list(network.edges.values()), 'generating WS network'): if np.random.rand() < p: # Delete original link and remember source node v = edge.v.uid network.remove_edge(edge) # Find new random tgt, which is not yet connected to src new_target = None # This loop repeatedly chooses a random target until we find # a target not yet connected to src. Note that this could potentially # result in an infinite loop depending on parameters. while new_target is None: x = node_uids[np.random.randint(n)] if (x != v or loops) and (v, x) not in network.edges: new_target = x network.add_edge(v, new_target) return network
def ER_np(n: int, p: float, directed: bool = False, loops: bool = False, node_uids: Optional[list] = None) -> Network: """(n, p) Erdös-Renyi model Generates a random graph with a fixed number of n nodes and edge probability p based on the Erdös-Renyi model. Parameters ---------- n : int The number of nodes in the generated network p : float The probability with which an edge will be created between each pair of nodes directed : bool Whether a directed network should be generated loops : bool Whether or not the generated network may contain loops. node_uids : list Optional list of node uids that will be used. Examples -------- Generate random undirected network with 10 nodes >>> import pathpy as pp >>> random_graph = pp.algorithms.random_graphs.ER_np(n=10, p=0.03) >>> print(random_graph.summary()) ... """ network = Network(directed=directed) if node_uids is None or len(node_uids) != n: LOG.info('No valid node uids given, generating numeric node uids') node_uids = [] for i in range(n): node_uids.append(str(i)) for i in range(n): network.add_node(node_uids[i]) for s in tqdm(range(n), 'generating G(n,p) network'): if directed: x = n else: x = s+1 for t in range(x): if t == s and not loops: continue if np.random.random_sample() < p: network.add_edge(node_uids[s], node_uids[t]) return network
def train_test_split(network: Network, test_size: Optional[float] = 0.25, train_size: Optional[float] = None, split: Optional[str] = 'node') -> tuple(Network, Network): """Returns a train/test split of a network object. This method is implemented for instances of Network and TemporalNetwork. The train/test split is non-destructive and based on object references, i.e. the function returns new Network instances that contain references to the same node/edge objects. The original network is not affected. Parameters ---------- network: Union[Network, TemporalNetwork] The network or temporal network for which the train/test split shall be performed. test_size: Optional[float] = 0.25 Fraction of the network to include in the test network train_size: Optional[float] = 0.25 Fraction of the network to include in the training network split: Optional['str'] = 'node' Specifies how the train/test split shall be performed. For 'node' a random subset of nodes is selected, while for 'edge' a random subset of edges is selected. Returns ------- Tuple (n1, n2) where n1 is the training network and n2 is the test network Examples -------- >>> n = pp.Network() >>> n.add_edge('a', 'b') >>> n.add_edge('b', 'c') >>> n.add_edge('c', 'd') >>> n.add_edge('d', 'a') >>> train, test = train_test_split(n, test_size=0.25) >>> print(train) >>> print(test) Network with one node Network with three nodes """ test_network = Network(directed=network.directed, multiedges=network.multiedges, uid=network.uid + '_test') train_network = Network(directed=network.directed, multiedges=network.multiedges, uid=network.uid + '_train') if train_size == None: ts = test_size else: ts = 1.0 - train_size if split == 'node': test_nodes = choice([v.uid for v in network.nodes], size=int(ts * network.number_of_nodes()), replace=False) for v in test_nodes: test_network.add_node(network.nodes[v]) train_nodes = [ v.uid for v in network.nodes if v.uid not in test_network.nodes.uids ] for v in train_nodes: train_network.add_node(network.nodes[v]) for e in network.edges: if e.v.uid in test_network.nodes.uids and e.w.uid in test_network.nodes.uids: test_network.add_edge(e) if e.v.uid in train_network.nodes.uids and e.w.uid in train_network.nodes.uids: train_network.add_edge(e) elif split == 'edge': for v in network.nodes: test_network.add_node(v) train_network.add_node(v) test_edges = choice([e.uid for e in network.edges], size=int(ts * network.number_of_edges()), replace=False) for e in test_edges: test_network.add_edge(network.edges[e]) train_edges = [ e.uid for e in network.edges if e.uid not in test_network.edges.uids ] for e in train_edges: train_network.add_edge(network.edges[e]) else: raise NotImplementedError( 'Unsupported split method "{0}" for instance of type Network'. format(split)) return train_network, test_network
def ER_nm(n: int, m: int, directed: bool = False, loops: bool = False, multiedges: bool = False, node_uids: Optional[list] = None) -> Union[Network, None]: """(n, m) Erdös-Renyi model. Generates a random graph with a fixed number of n nodes and m edges based on the Erdös-Renyi model. Parameters ---------- n : int The number of nodes in the generated network m : int The number of randomly generated edges in the network directed : bool Whether a directed network should be generated loops : bool Whether or not the generated network may contain loops. multi_edge : bool Whether or not the same edge can be added multiple times node_uids : list Optional list of node uids that will be used. Examples -------- Generate random undirected network with 10 nodes and 25 edges >>> import pathpy as pp >>> random_graph = pp.algorithms.random_graphs.ER_nm(n=10, m=25) >>> print(random_graph.summary()) ... """ # Check parameter sanity M = max_edges(n, directed=directed, loops=loops, multiedges=multiedges) if m > M: LOG.error( 'Given network type with n nodes can have at most {} edges.'.format(M)) return None network = Network(directed=directed) if node_uids is None or len(node_uids) != n: LOG.info('No valid node uids given, generating numeric node uids') node_uids = [] for i in range(n): node_uids.append(str(i)) for i in range(n): network.add_node(node_uids[i]) edges = 0 while edges < m: v, w = np.random.choice(node_uids, size=2, replace=loops) if multiedges or network.nodes[w] not in network.successors[v]: network.add_edge(v, w) edges += 1 return network