grand-graph 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grand/__init__.py +83 -0
- grand/backends/__init__.py +29 -0
- grand/backends/_dataframe.py +498 -0
- grand/backends/_dynamodb.py +532 -0
- grand/backends/_gremlin.py +331 -0
- grand/backends/_igraph.py +273 -0
- grand/backends/_networkit.py +308 -0
- grand/backends/_networkx.py +215 -0
- grand/backends/_sqlbackend.py +701 -0
- grand/backends/backend.py +437 -0
- grand/backends/metadatastore.py +64 -0
- grand/backends/test_backends.py +487 -0
- grand/backends/test_cached_backend.py +83 -0
- grand/backends/test_metadatastore.py +56 -0
- grand/dialects/__init__.py +370 -0
- grand/dialects/test_dialect.py +207 -0
- grand/test_graph.py +16 -0
- grand_graph-0.7.0.dist-info/METADATA +22 -0
- grand_graph-0.7.0.dist-info/RECORD +21 -0
- grand_graph-0.7.0.dist-info/WHEEL +4 -0
- grand_graph-0.7.0.dist-info/licenses/LICENSE +174 -0
grand/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grand graphs package.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from .backends import Backend, NetworkXBackend
|
|
8
|
+
from .dialects import NetworkXDialect, IGraphDialect, NetworkitDialect
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_DEFAULT_BACKEND = NetworkXBackend
|
|
12
|
+
|
|
13
|
+
__version__ = "0.7.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Graph:
|
|
17
|
+
"""
|
|
18
|
+
A grand.Graph enables you to manipulate a graph using multiple dialects.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
nx: NetworkXDialect
|
|
23
|
+
networkit: NetworkitDialect
|
|
24
|
+
igraph: IGraphDialect
|
|
25
|
+
|
|
26
|
+
def __init__(self, backend: Optional[Backend] = None, **backend_kwargs: dict):
|
|
27
|
+
"""
|
|
28
|
+
Create a new grand.Graph.
|
|
29
|
+
|
|
30
|
+
The only positional argument is the backend to use. All other arguments
|
|
31
|
+
are passed to the backend's constructor, if a type is provided.
|
|
32
|
+
Otherwise, kwargs are ignored.
|
|
33
|
+
|
|
34
|
+
Arguments:
|
|
35
|
+
backend (Backend): The backend to use. If none is provided, will
|
|
36
|
+
default to _DEFAULT_BACKEND.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
self.backend = backend or _DEFAULT_BACKEND
|
|
40
|
+
|
|
41
|
+
# If you passed a class instead of an instance, instantiate it with
|
|
42
|
+
# kwargs from the constructor:
|
|
43
|
+
if isinstance(self.backend, type):
|
|
44
|
+
self.backend = self.backend(**backend_kwargs)
|
|
45
|
+
|
|
46
|
+
# Attach dialects:
|
|
47
|
+
self.attach_dialect("nx", NetworkXDialect)
|
|
48
|
+
self.attach_dialect("igraph", IGraphDialect)
|
|
49
|
+
self.attach_dialect("networkit", NetworkitDialect)
|
|
50
|
+
|
|
51
|
+
def attach_dialect(self, name: str, dialect: type):
|
|
52
|
+
"""
|
|
53
|
+
Attach a dialect to the graph.
|
|
54
|
+
|
|
55
|
+
Arguments:
|
|
56
|
+
name (str): The name of the dialect.
|
|
57
|
+
dialect (type): The dialect class to attach.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
setattr(self, name, dialect(self))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DiGraph(Graph):
|
|
64
|
+
"""
|
|
65
|
+
A grand.DiGraph enables you to manipulate a directed graph. This is a
|
|
66
|
+
convenience class that inherits from grand.Graph.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, backend: Optional[Backend] = None, **backend_kwargs: dict):
|
|
71
|
+
"""
|
|
72
|
+
Create a new grand.DiGraph.
|
|
73
|
+
|
|
74
|
+
The only positional argument is the backend to use. All other arguments
|
|
75
|
+
are passed to the backend's constructor, if a type is provided.
|
|
76
|
+
Otherwise, kwargs are ignored.
|
|
77
|
+
|
|
78
|
+
Arguments:
|
|
79
|
+
backend (Backend): The backend to use. If none is provided, will
|
|
80
|
+
default to _DEFAULT_BACKEND.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(backend, **{**backend_kwargs, "directed": True})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .backend import Backend, CachedBackend, InMemoryCachedBackend
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from ._dynamodb import DynamoDBBackend
|
|
5
|
+
except ImportError:
|
|
6
|
+
pass
|
|
7
|
+
from ._networkx import NetworkXBackend
|
|
8
|
+
from ._dataframe import DataFrameBackend
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from ._sqlbackend import SQLBackend
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from ._networkit import NetworkitBackend
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Backend",
|
|
22
|
+
"CachedBackend",
|
|
23
|
+
"InMemoryCachedBackend",
|
|
24
|
+
"NetworkXBackend",
|
|
25
|
+
"DataFrameBackend",
|
|
26
|
+
"DynamoDBBackend",
|
|
27
|
+
"SQLBackend",
|
|
28
|
+
"NetworkitBackend",
|
|
29
|
+
]
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
from typing import Hashable, Generator
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from .backend import Backend
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataFrameBackend(Backend):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
directed: bool = False,
|
|
13
|
+
edge_df: pd.DataFrame = None,
|
|
14
|
+
node_df: pd.DataFrame = None,
|
|
15
|
+
edge_df_source_column: str = "Source",
|
|
16
|
+
edge_df_target_column: str = "Target",
|
|
17
|
+
node_df_id_column: str = "id",
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Create a new DataFrame backend.
|
|
21
|
+
|
|
22
|
+
You must pass an edgelist. A nodelist is optional.
|
|
23
|
+
|
|
24
|
+
Arguments:
|
|
25
|
+
edge_df (pd.DataFrame): An edgelist dataframe with one edge per row
|
|
26
|
+
directed (bool: False): Whether the graph is directed
|
|
27
|
+
node_df (pd.DataFrame): A node metadata lookup
|
|
28
|
+
edge_df_source_column (str): The name of the column in `edge_df` to
|
|
29
|
+
use as the source of edges
|
|
30
|
+
edge_df_target_column (str): The name of the column in `edge_df` to
|
|
31
|
+
use as the target of edges
|
|
32
|
+
node_df_id_column (str): The name of the column in `node_df` to
|
|
33
|
+
use as the node ID
|
|
34
|
+
"""
|
|
35
|
+
self._directed = directed
|
|
36
|
+
self._edge_df = (
|
|
37
|
+
edge_df
|
|
38
|
+
if edge_df is not None
|
|
39
|
+
else pd.DataFrame(columns=[edge_df_source_column, edge_df_target_column])
|
|
40
|
+
)
|
|
41
|
+
self._node_df = node_df if node_df is not None else None
|
|
42
|
+
self._edge_df_source_column = edge_df_source_column
|
|
43
|
+
self._edge_df_target_column = edge_df_target_column
|
|
44
|
+
self._node_df_id_column = node_df_id_column
|
|
45
|
+
|
|
46
|
+
def is_directed(self) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Return True if the backend graph is directed.
|
|
49
|
+
|
|
50
|
+
Arguments:
|
|
51
|
+
None
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
bool: True if the backend graph is directed.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
return self._directed
|
|
58
|
+
|
|
59
|
+
def teardown(self, yes_i_am_sure: bool = False):
|
|
60
|
+
"""
|
|
61
|
+
Tear down this graph, deleting all evidence it once was here.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
def add_node(self, node_name: Hashable, metadata: dict) -> Hashable:
|
|
67
|
+
"""
|
|
68
|
+
Add a new node to the graph.
|
|
69
|
+
|
|
70
|
+
Insert a new document into the nodes table.
|
|
71
|
+
|
|
72
|
+
Arguments:
|
|
73
|
+
node_name (Hashable): The ID of the node
|
|
74
|
+
metadata (dict: None): An optional dictionary of metadata
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Hashable: The ID of this node, as inserted
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# Add a new row to the nodes table:
|
|
82
|
+
if self._node_df is None:
|
|
83
|
+
self._node_df = pd.DataFrame(
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
self._node_df_id_column: node_name,
|
|
87
|
+
**metadata,
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
columns=[
|
|
91
|
+
self._node_df_id_column,
|
|
92
|
+
*metadata.keys(),
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
self._node_df.set_index(self._node_df_id_column, inplace=True)
|
|
96
|
+
else:
|
|
97
|
+
if self.has_node(node_name):
|
|
98
|
+
existing_metadata = self.get_node_by_id(node_name)
|
|
99
|
+
existing_metadata.update(metadata)
|
|
100
|
+
for k, v in existing_metadata.items():
|
|
101
|
+
self._node_df.at[node_name, k] = v
|
|
102
|
+
else:
|
|
103
|
+
# Insert a new row:
|
|
104
|
+
self._node_df = pd.concat(
|
|
105
|
+
[self._node_df, pd.DataFrame([{node_name: metadata}]).T]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return node_name
|
|
109
|
+
|
|
110
|
+
def all_nodes_as_iterable(self, include_metadata: bool = False):
|
|
111
|
+
"""
|
|
112
|
+
Get a generator of all of the nodes in this graph.
|
|
113
|
+
|
|
114
|
+
Arguments:
|
|
115
|
+
include_metadata (bool: False): Whether to include node metadata in
|
|
116
|
+
the response
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Generator: A generator of all nodes (arbitrary sort)
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
if self._node_df is not None:
|
|
123
|
+
return [
|
|
124
|
+
(
|
|
125
|
+
(
|
|
126
|
+
node_id,
|
|
127
|
+
row.to_dict(),
|
|
128
|
+
)
|
|
129
|
+
if include_metadata
|
|
130
|
+
else node_id
|
|
131
|
+
)
|
|
132
|
+
for node_id, row in self._node_df.iterrows()
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
return [
|
|
137
|
+
(node_id, {}) if include_metadata else node_id
|
|
138
|
+
for node_id in self._edge_df[self._edge_df_source_column]
|
|
139
|
+
] + [
|
|
140
|
+
(node_id, {}) if include_metadata else node_id
|
|
141
|
+
for node_id in self._edge_df[self._edge_df_target_column]
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
def has_node(self, u: Hashable) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Return true if the node exists in the graph.
|
|
147
|
+
|
|
148
|
+
Arguments:
|
|
149
|
+
u (Hashable): The ID of the node to check
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
bool: True if the node exists
|
|
153
|
+
"""
|
|
154
|
+
if self._node_df is not None:
|
|
155
|
+
return u in self._node_df.index
|
|
156
|
+
|
|
157
|
+
return u in (self._edge_df[self._edge_df_source_column]) or u in (
|
|
158
|
+
self._edge_df[self._edge_df_target_column]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
|
|
162
|
+
"""
|
|
163
|
+
Add a new edge to the graph between two nodes.
|
|
164
|
+
|
|
165
|
+
If the graph is directed, this edge will start (source) at the `u` node
|
|
166
|
+
and end (target) at the `v` node.
|
|
167
|
+
|
|
168
|
+
Arguments:
|
|
169
|
+
u (Hashable): The source node ID
|
|
170
|
+
v (Hashable): The target node ID
|
|
171
|
+
metadata (dict): Optional metadata to associate with the edge
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Hashable: The edge ID, as inserted.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
if not self.has_node(u):
|
|
179
|
+
self.add_node(u, {})
|
|
180
|
+
if not self.has_node(v):
|
|
181
|
+
self.add_node(v, {})
|
|
182
|
+
|
|
183
|
+
if self._has_edge(u, v):
|
|
184
|
+
# Update the existing edge:
|
|
185
|
+
for k, m in metadata.items():
|
|
186
|
+
if self._directed:
|
|
187
|
+
self._edge_df.loc[
|
|
188
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
189
|
+
& (self._edge_df[self._edge_df_target_column] == v),
|
|
190
|
+
k,
|
|
191
|
+
] = m
|
|
192
|
+
else:
|
|
193
|
+
# Check for the edge in both directions:
|
|
194
|
+
self._edge_df.loc[
|
|
195
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
196
|
+
& (self._edge_df[self._edge_df_target_column] == v),
|
|
197
|
+
k,
|
|
198
|
+
] = m
|
|
199
|
+
self._edge_df.loc[
|
|
200
|
+
(self._edge_df[self._edge_df_source_column] == v)
|
|
201
|
+
& (self._edge_df[self._edge_df_target_column] == u),
|
|
202
|
+
k,
|
|
203
|
+
] = m
|
|
204
|
+
else:
|
|
205
|
+
row = {
|
|
206
|
+
self._edge_df_source_column: u,
|
|
207
|
+
self._edge_df_target_column: v,
|
|
208
|
+
**metadata,
|
|
209
|
+
}
|
|
210
|
+
self._edge_df.loc[len(self._edge_df)] = None
|
|
211
|
+
for k, m in row.items():
|
|
212
|
+
self._edge_df.loc[len(self._edge_df) - 1, k] = m
|
|
213
|
+
return (u, v)
|
|
214
|
+
|
|
215
|
+
def _has_edge(self, u: Hashable, v: Hashable) -> bool:
|
|
216
|
+
"""
|
|
217
|
+
Return true if the edge exists in the graph.
|
|
218
|
+
|
|
219
|
+
Arguments:
|
|
220
|
+
u (Hashable): The source node ID
|
|
221
|
+
v (Hashable): The target node ID
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
bool: True if the edge exists
|
|
225
|
+
"""
|
|
226
|
+
if self._directed:
|
|
227
|
+
return (
|
|
228
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
229
|
+
& (self._edge_df[self._edge_df_target_column] == v)
|
|
230
|
+
).any()
|
|
231
|
+
else:
|
|
232
|
+
return (
|
|
233
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
234
|
+
& (self._edge_df[self._edge_df_target_column] == v)
|
|
235
|
+
).any() or (
|
|
236
|
+
(self._edge_df[self._edge_df_source_column] == v)
|
|
237
|
+
& (self._edge_df[self._edge_df_target_column] == u)
|
|
238
|
+
).any()
|
|
239
|
+
|
|
240
|
+
def all_edges_as_iterable(self, include_metadata: bool = False) -> Generator:
|
|
241
|
+
"""
|
|
242
|
+
Get a list of all edges in this graph, arbitrary sort.
|
|
243
|
+
|
|
244
|
+
Arguments:
|
|
245
|
+
include_metadata (bool: False): Whether to include edge metadata
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Generator: A generator of all edges (arbitrary sort)
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
for _, row in self._edge_df.iterrows():
|
|
252
|
+
if include_metadata:
|
|
253
|
+
yield (
|
|
254
|
+
row[self._edge_df_source_column],
|
|
255
|
+
row[self._edge_df_target_column],
|
|
256
|
+
dict(row),
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
yield (
|
|
260
|
+
row[self._edge_df_source_column],
|
|
261
|
+
row[self._edge_df_target_column],
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def get_node_by_id(self, node_name: Hashable):
|
|
265
|
+
"""
|
|
266
|
+
Return the data associated with a node.
|
|
267
|
+
|
|
268
|
+
Arguments:
|
|
269
|
+
node_name (Hashable): The node ID to look up
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
dict: The metadata associated with this node
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
if self._node_df is not None:
|
|
276
|
+
res = (self._node_df.loc[node_name]).to_dict()
|
|
277
|
+
return res.get(0, res)
|
|
278
|
+
|
|
279
|
+
return {}
|
|
280
|
+
|
|
281
|
+
def get_edge_by_id(self, u: Hashable, v: Hashable):
|
|
282
|
+
"""
|
|
283
|
+
Get an edge by its source and target IDs.
|
|
284
|
+
|
|
285
|
+
Arguments:
|
|
286
|
+
u (Hashable): The source node ID
|
|
287
|
+
v (Hashable): The target node ID
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
dict: Metadata associated with this edge
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
if self._directed:
|
|
294
|
+
result = self._edge_df[
|
|
295
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
296
|
+
& (self._edge_df[self._edge_df_target_column] == v)
|
|
297
|
+
]
|
|
298
|
+
if len(result):
|
|
299
|
+
return self._edge_as_dict(result.iloc[0])
|
|
300
|
+
|
|
301
|
+
else:
|
|
302
|
+
left = self._edge_df[
|
|
303
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
304
|
+
& (self._edge_df[self._edge_df_target_column] == v)
|
|
305
|
+
]
|
|
306
|
+
if len(left):
|
|
307
|
+
return self._edge_as_dict(left.iloc[0])
|
|
308
|
+
right = self._edge_df[
|
|
309
|
+
(self._edge_df[self._edge_df_source_column] == v)
|
|
310
|
+
& (self._edge_df[self._edge_df_target_column] == u)
|
|
311
|
+
]
|
|
312
|
+
if len(right):
|
|
313
|
+
return self._edge_as_dict(right.iloc[0])
|
|
314
|
+
|
|
315
|
+
def get_node_neighbors(self, u: Hashable, include_metadata: bool = False):
|
|
316
|
+
"""
|
|
317
|
+
Get a generator of all downstream nodes from this node.
|
|
318
|
+
|
|
319
|
+
Arguments:
|
|
320
|
+
u (Hashable): The source node ID
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Generator
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
if include_metadata:
|
|
328
|
+
if self._directed:
|
|
329
|
+
return {
|
|
330
|
+
(r[self._edge_df_target_column]): self._edge_as_dict(r)
|
|
331
|
+
for _, r in self._edge_df[
|
|
332
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
333
|
+
].iterrows()
|
|
334
|
+
}
|
|
335
|
+
else:
|
|
336
|
+
return {
|
|
337
|
+
(
|
|
338
|
+
r[self._edge_df_source_column]
|
|
339
|
+
if r[self._edge_df_source_column] != u
|
|
340
|
+
else r[self._edge_df_target_column]
|
|
341
|
+
): self._edge_as_dict(r)
|
|
342
|
+
for _, r in self._edge_df[
|
|
343
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
344
|
+
| (self._edge_df[self._edge_df_target_column] == u)
|
|
345
|
+
].iterrows()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if self._directed:
|
|
349
|
+
return iter(
|
|
350
|
+
[
|
|
351
|
+
row[self._edge_df_target_column]
|
|
352
|
+
for _, row in self._edge_df[
|
|
353
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
354
|
+
].iterrows()
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
return iter(
|
|
359
|
+
[
|
|
360
|
+
(
|
|
361
|
+
row[self._edge_df_source_column]
|
|
362
|
+
if row[self._edge_df_source_column] != u
|
|
363
|
+
else row[self._edge_df_target_column]
|
|
364
|
+
)
|
|
365
|
+
for _, row in self._edge_df[
|
|
366
|
+
(self._edge_df[self._edge_df_source_column] == u)
|
|
367
|
+
| (self._edge_df[self._edge_df_target_column] == u)
|
|
368
|
+
].iterrows()
|
|
369
|
+
]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _edge_as_dict(self, row):
|
|
373
|
+
"""
|
|
374
|
+
Convert an edge row to a dictionary.
|
|
375
|
+
|
|
376
|
+
Arguments:
|
|
377
|
+
row (pandas.Series): The edge row
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
dict: The edge metadata
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
r = row.to_dict()
|
|
384
|
+
r.pop(self._edge_df_source_column)
|
|
385
|
+
r.pop(self._edge_df_target_column)
|
|
386
|
+
return r
|
|
387
|
+
|
|
388
|
+
def get_node_predecessors(self, u: Hashable, include_metadata: bool = False):
|
|
389
|
+
"""
|
|
390
|
+
Get a generator of all upstream nodes from this node.
|
|
391
|
+
|
|
392
|
+
Arguments:
|
|
393
|
+
u (Hashable): The source node ID
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Generator
|
|
397
|
+
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
if include_metadata:
|
|
401
|
+
if self._directed:
|
|
402
|
+
return {
|
|
403
|
+
(
|
|
404
|
+
r[self._edge_df_target_column]
|
|
405
|
+
if r[self._edge_df_target_column] != u
|
|
406
|
+
else r[self._edge_df_source_column]
|
|
407
|
+
): self._edge_as_dict(r)
|
|
408
|
+
for _, r in self._edge_df[
|
|
409
|
+
(self._edge_df[self._edge_df_target_column] == u)
|
|
410
|
+
].iterrows()
|
|
411
|
+
}
|
|
412
|
+
else:
|
|
413
|
+
return {
|
|
414
|
+
(
|
|
415
|
+
r[self._edge_df_target_column]
|
|
416
|
+
if r[self._edge_df_target_column] != u
|
|
417
|
+
else r[self._edge_df_source_column]
|
|
418
|
+
): self._edge_as_dict(r)
|
|
419
|
+
for _, r in self._edge_df[
|
|
420
|
+
(self._edge_df[self._edge_df_target_column] == u)
|
|
421
|
+
| (self._edge_df[self._edge_df_source_column] == u)
|
|
422
|
+
].iterrows()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if self._directed:
|
|
426
|
+
return iter(
|
|
427
|
+
[
|
|
428
|
+
row[self._edge_df_source_column]
|
|
429
|
+
for _, row in self._edge_df[
|
|
430
|
+
(self._edge_df[self._edge_df_target_column] == u)
|
|
431
|
+
].iterrows()
|
|
432
|
+
]
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
return iter(
|
|
436
|
+
[
|
|
437
|
+
(
|
|
438
|
+
row[self._edge_df_source_column]
|
|
439
|
+
if row[self._edge_df_target_column] != u
|
|
440
|
+
else row[self._edge_df_target_column]
|
|
441
|
+
)
|
|
442
|
+
for _, row in self._edge_df[
|
|
443
|
+
(self._edge_df[self._edge_df_target_column] == u)
|
|
444
|
+
| (self._edge_df[self._edge_df_source_column] == u)
|
|
445
|
+
].iterrows()
|
|
446
|
+
]
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def get_node_count(self) -> int:
|
|
450
|
+
"""
|
|
451
|
+
Get an integer count of the number of nodes in this graph.
|
|
452
|
+
|
|
453
|
+
Arguments:
|
|
454
|
+
None
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
int: The count of nodes
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
if self._node_df is not None:
|
|
461
|
+
return len(self._node_df)
|
|
462
|
+
# Return number of unique sources intersected with number of unique targets
|
|
463
|
+
return len(
|
|
464
|
+
set(self._edge_df[self._edge_df_source_column]).intersection(
|
|
465
|
+
set(self._edge_df[self._edge_df_target_column])
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def get_edge_count(self) -> int:
|
|
470
|
+
"""
|
|
471
|
+
Get an integer count of the number of edges in this graph.
|
|
472
|
+
|
|
473
|
+
Arguments:
|
|
474
|
+
None
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
int: The count of edges
|
|
478
|
+
|
|
479
|
+
"""
|
|
480
|
+
return len(self._edge_df)
|
|
481
|
+
|
|
482
|
+
def ingest_from_edgelist_dataframe(
|
|
483
|
+
self, edgelist: pd.DataFrame, source_column: str, target_column: str
|
|
484
|
+
) -> dict:
|
|
485
|
+
"""
|
|
486
|
+
Ingest an edgelist from a Pandas DataFrame.
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
# Produce edge list:
|
|
490
|
+
|
|
491
|
+
edge_tic = time.time()
|
|
492
|
+
self._edge_df = edgelist
|
|
493
|
+
self._edge_df_source_column = source_column
|
|
494
|
+
self._edge_df_target_column = target_column
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
"edge_duration": time.time() - edge_tic,
|
|
498
|
+
}
|