Skip to content

Commit 38386a2

Browse files
authored
changed semantics of Graph.m for undirected networks (#286)
1 parent b1ef49d commit 38386a2

5 files changed

Lines changed: 31 additions & 37 deletions

File tree

src/pathpyG/algorithms/generative_models.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def erdos_renyi_gnm(n: int, m: int, mapping: IndexMap | None = None,
7373
7474
Args:
7575
n: the number of nodes of the graph
76-
m: the number of random edges to be generated
76+
m: the number of random directed or undirected edges to be generated
7777
mapping: optional given mapping of n nodes to node IDs. If this is not given a mapping is created
7878
self_loops: whether or not to allow self-loops (v,v) to be generated
7979
multi_edges: whether or not multiple identical edges are allowed
@@ -126,12 +126,8 @@ def erdos_renyi_gnm_randomize(graph: Graph, self_loops: bool = False, multi_edge
126126
r = pp.algorithms.generative_models.G_nm_randomize(g)
127127
```
128128
"""
129-
if graph.is_undirected():
130-
m = int(graph.m / 2)
131-
else:
132-
m = graph.m
133129
return erdos_renyi_gnm(
134-
graph.n, m, directed=graph.is_directed(),
130+
graph.n, graph.m, directed=graph.is_directed(),
135131
self_loops=self_loops,
136132
multi_edges=multi_edges,
137133
mapping=graph.mapping
@@ -183,31 +179,28 @@ def erdos_renyi_gnp_randomize(graph: Graph, self_loops: bool = False) -> Graph:
183179
The number of nodes, expected number of edges, edge directedness and node uids of the
184180
generated graph match the corresponding values of the graph given as parameter.
185181
"""
186-
if graph.is_directed():
187-
m = graph.m
188-
else:
189-
m = int(graph.m / 2)
190182
M = max_edges(graph.n, directed=graph.is_directed(), self_loops=self_loops)
191-
p = m / M
183+
p = graph.m / M
192184
return erdos_renyi_gnp(n=graph.n, p=p, directed=graph.is_directed(),
193185
self_loops=self_loops, mapping=graph.mapping)
194186

195187

196188
def erdos_renyi_gnp_likelihood(p: float, graph: Graph) -> float:
197189
"""Calculate the likelihood of parameter p for a G(n,p) model and a given graph"""
198-
assert graph.is_directed is False
199-
return p**graph.n * (1 - p) ** (scipy.special.binom(graph.n, 2) - graph.m / 2)
190+
assert graph.is_directed() is False
191+
return p**graph.n * (1 - p) ** (scipy.special.binom(graph.n, 2) - graph.m)
200192

201193

202194
def erdos_renyi_gnp_log_likelihood(p: float, graph: Graph) -> float:
203195
"""Calculate the log-likelihood of parameter p for a G(n,p) model and a given graph"""
204-
return (graph.m / 2) * _np.log10(p) + (scipy.special.binom(graph.n, 2) - (graph.m / 2)) * _np.log10(1 - p)
196+
assert graph.is_directed() is False
197+
return graph.m * _np.log10(p) + (scipy.special.binom(graph.n, 2) - (graph.m)) * _np.log10(1 - p)
205198

206199

207200
def erdos_renyi_gnp_mle(graph: Graph) -> float:
208201
"""Calculate the maximum likelihood estimate of parameter p for a G(n,p) model and a given undirected graph"""
209202
assert graph.is_directed() is False
210-
return (graph.m / 2) / scipy.special.binom(graph.n, 2)
203+
return graph.m / scipy.special.binom(graph.n, 2)
211204

212205

213206
def watts_strogatz(

src/pathpyG/core/graph.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,12 +601,19 @@ def m(self) -> int:
601601
"""
602602
Return number of edges.
603603
604-
Returns the number of edges in the graph. For an undirected graph, the number of directed edges is returned.
604+
Returns the number of edges in the graph. For an undirected graph, the number of
605+
undirected edges (accounting for self-loops) is returned, i.e. in an undirected
606+
graph the directed edges (a,b) and (b,a) will be counted only once.
605607
606608
Returns:
607609
int: number of edges in the graph
608610
"""
609-
return self.data.num_edges # type: ignore
611+
if self.is_directed():
612+
return self.data.num_edges # type: ignore
613+
else:
614+
num_self_loops = (self.data.edge_index[0] == self.data.edge_index[1]).sum().item()
615+
num_edges_wo_self_loops = self.data.edge_index.size(1) - int(num_self_loops)
616+
return int(num_edges_wo_self_loops/2 + num_self_loops) # type: ignore
610617

611618
@property
612619
def order(self) -> int:
@@ -741,7 +748,7 @@ def __str__(self) -> str:
741748
from pprint import pformat
742749

743750
if self.is_undirected():
744-
s = "Undirected graph with {0} nodes and {1} (directed) edges\n".format(self.n, self.m)
751+
s = "Undirected graph with {0} nodes and {1} edges\n".format(self.n, self.m)
745752
else:
746753
s = "Directed graph with {0} nodes and {1} edges\n".format(self.n, self.m)
747754

src/pathpyG/statistics/node_similarities.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,8 @@ def katz_index(graph: Graph, v, w, beta) -> float:
6969
def LeichtHolmeNewman_index(graph: Graph, v, w, alpha) -> float:
7070
A = graph.sparse_adj_matrix()
7171
ev = _sp.sparse.linalg.eigs(A, which="LM", k=2, return_eigenvectors=False)
72-
if graph.is_directed():
73-
m = graph.m
74-
else:
75-
m = graph.m / 2
7672
eigenvalues_sorted = _np.sort(_np.absolute(ev))
73+
m = graph.m
7774
lambda_1 = eigenvalues_sorted[1]
7875
D = _sp.sparse.diags(degree_sequence(graph)).tocsc()
7976
I = _sp.sparse.identity(graph.n).tocsc()

tests/algorithms/test_generative_models.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,32 @@ def test_erdos_renyi_gnm():
3434
# test undirected graph w/o multi-edges, w/o self-loops and without IndexMapping
3535
n = 100
3636
m = 200
37-
m_1 = erdos_renyi_gnm(n=n, m=m)
37+
m_1 = erdos_renyi_gnm(n=n, m=m, directed=False, self_loops=False)
3838
assert m_1.n == n
39-
# 400 directed edges for undirected graph
40-
assert m_1.m == 2 * m
39+
assert m_1.m == m
4140
# no multiple edges
4241
assert len(set([(v, w) for v, w in m_1.edges])) == len([(v, w) for v, w in m_1.edges])
4342
assert m_1.is_directed() is False
4443

4544
# test undirected graph w/o multi-edges, w/o self-loops and with custom IDs
4645
m_2 = erdos_renyi_gnm(n=n, m=m, mapping=IndexMap([str(i) for i in range(n)]))
4746
assert m_2.n == n
48-
# 400 directed edges for undirected graph
49-
assert m_2.m == 2 * m
47+
assert m_2.m == m
5048
# no multiple edges
5149
assert len(set([(v, w) for v, w in m_2.edges])) == len([(v, w) for v, w in m_2.edges])
5250
assert m_2.is_directed() is False
5351

5452
# test directed graph w/o multi-edges, w/o self-loops
5553
m_3 = erdos_renyi_gnm(n=n, m=m, directed=True)
5654
assert m_3.n == n
57-
# 200 directed edges
5855
assert m_3.m == m
5956
assert len(set([(v, w) for v, w in m_3.edges])) == len([(v, w) for v, w in m_3.edges])
6057
assert m_3.is_directed() is True
6158

6259
# test undirected graph w/o multi-edges and with self-loops
6360
m_4 = erdos_renyi_gnm(n=n, m=m, self_loops=True)
6461
assert m_4.n == n
65-
# since self-loops only exist in one direction we have 2 * m - n <= M <= 2 * m
66-
assert m_4.m >= 2 * m - n and m_4.m <= 2 * m
62+
assert m_4.m == m
6763
assert len(set([(v, w) for v, w in m_4.edges])) == len([(v, w) for v, w in m_4.edges])
6864
assert m_4.is_directed() is False
6965

@@ -123,11 +119,12 @@ def test_erdos_renyi_gnm_randomize():
123119
n = 100
124120
m = 200
125121

126-
g = erdos_renyi_gnp(n, m)
127-
g_r = erdos_renyi_gnm_randomize(g)
122+
g = erdos_renyi_gnm(n, m, directed=False, self_loops=False)
123+
g_r = erdos_renyi_gnm_randomize(g, self_loops=False)
128124
assert g_r.n == g.n
125+
assert g_r.is_directed() == g.is_directed()
129126
assert g_r.mapping == g.mapping
130-
assert g_r.m == g_r.m
127+
assert g_r.m == g.m
131128

132129

133130
def test_erdos_renyi_gnp_randomize():
@@ -179,15 +176,15 @@ def test_generate_degree_sequence():
179176

180177
def test_watts_strogatz_simple():
181178
g = watts_strogatz(5, 1, 0.0)
182-
assert g.m == 10
179+
assert g.m == 5
183180
assert (
184181
to_numpy(g.data.edge_index) == np.array([[0, 0, 1, 1, 2, 2, 3, 3, 4, 4], [1, 4, 0, 2, 1, 3, 2, 4, 0, 3]])
185182
).all()
186183

187184
torch.manual_seed(1)
188185
g = watts_strogatz(5, 1, 0.5, allow_duplicate_edges=False, allow_self_loops=False)
189186
print(g.data.edge_index)
190-
assert g.m == 10
187+
assert g.m == 5
191188
assert g.n == 5
192189
assert g.has_self_loops() is False
193190
assert g.is_directed() is False
@@ -256,7 +253,7 @@ def test_stochastic_block_model():
256253
g = stochastic_block_model(M, z, IndexMap(list('abcdefghi')))
257254

258255
assert g.n == 9
259-
assert g.m == 18
256+
assert g.m == 9
260257
assert g.is_undirected()
261258
_, labels = connected_components(g)
262259
assert Counter(labels) == {0: 3, 1: 3, 2: 3}

tests/io/test_pandas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def test_df_to_graph():
200200
)
201201
g = df_to_graph(df_graph_with_string_attr, is_undirected=True)
202202
assert g.n == 3
203-
assert g.m == 6
203+
assert g.m == 3
204204

205205

206206
def test_add_node_attributes_by_name(simple_graph):

0 commit comments

Comments
 (0)