mlnlib 0.2.35__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.
- mlnlib/mln.py +1684 -0
- mlnlib/preparation.py +517 -0
- mlnlib-0.2.35.dist-info/LICENSE.md +40 -0
- mlnlib-0.2.35.dist-info/METADATA +126 -0
- mlnlib-0.2.35.dist-info/RECORD +7 -0
- mlnlib-0.2.35.dist-info/WHEEL +5 -0
- mlnlib-0.2.35.dist-info/top_level.txt +1 -0
mlnlib/preparation.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: Eszter Bokanyi, 2023
|
|
3
|
+
E-mail: e.bokanyi@uva.nl
|
|
4
|
+
|
|
5
|
+
This file is created in the context of the POPNET project:
|
|
6
|
+
https://popnet.io
|
|
7
|
+
|
|
8
|
+
This script is able to prepare a multilayer network suitable for reading
|
|
9
|
+
with mln.py from a very general set of CSV files.
|
|
10
|
+
|
|
11
|
+
It accepts an arbitrary number of nodelist and edgelist CSV input files, reads through them,
|
|
12
|
+
and merges the node properties, and converts the edgelists to a scipy.sparse adjacency matrix.
|
|
13
|
+
The conversion happens through a layer.csv file, that contains the different linktypes and their
|
|
14
|
+
binary representation. If no nodelist or layer file is given, these are inferred from the edgelist.
|
|
15
|
+
|
|
16
|
+
Every input CSV is assumed to have a header and no index col.
|
|
17
|
+
|
|
18
|
+
The config parameter colmap is a JSON file or a dict that maps the column names of the input files
|
|
19
|
+
to the column names of the output files. If the map is empty ("none"), the column is dropped.
|
|
20
|
+
|
|
21
|
+
The main node file is used to determine the node set, and the other node files are merged into it.
|
|
22
|
+
|
|
23
|
+
The symmetrize parameters control whether the edgelists are symmetrized. If symmetrize_all is True,
|
|
24
|
+
all layers are symmetrized, thus, the network is undirected.
|
|
25
|
+
Otherwise only the layers listed in symmetrize are symmetrized.
|
|
26
|
+
|
|
27
|
+
The config file is a JSON file with the following structure:
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
"node_conf": {
|
|
31
|
+
"input_folder_prefix": "",
|
|
32
|
+
"files": [],
|
|
33
|
+
"colmap": "",
|
|
34
|
+
"sep": ";",
|
|
35
|
+
"main_file": 0,
|
|
36
|
+
"output": ""
|
|
37
|
+
},
|
|
38
|
+
"edge_conf": {
|
|
39
|
+
"input_folder_prefix": "",
|
|
40
|
+
"files": [],
|
|
41
|
+
"colmap": "",
|
|
42
|
+
"sep": ";",
|
|
43
|
+
"output": ""
|
|
44
|
+
},
|
|
45
|
+
"layer_conf": {
|
|
46
|
+
"raw_file": "",
|
|
47
|
+
"file": "",
|
|
48
|
+
"output": "",
|
|
49
|
+
"symmetrize": [],
|
|
50
|
+
"symmetrize_all": False,
|
|
51
|
+
"raw_sep":",",
|
|
52
|
+
"sep": ",",
|
|
53
|
+
"colors": ""
|
|
54
|
+
},
|
|
55
|
+
"output_folder": ""
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
The resulting files are saved to the output folder, if given, otherwise to the current directory:
|
|
59
|
+
* nodes.csv.gz: node dataframe
|
|
60
|
+
* edges.npz: adjacency matrix
|
|
61
|
+
* layers.csv: layer dataframe
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
import pandas as pd
|
|
65
|
+
from scipy.sparse import csr_matrix, save_npz
|
|
66
|
+
import numpy as np
|
|
67
|
+
|
|
68
|
+
import json
|
|
69
|
+
import gc
|
|
70
|
+
from time import sleep, time
|
|
71
|
+
import sys
|
|
72
|
+
|
|
73
|
+
# check whether we are in repo root, if not, change working directory
|
|
74
|
+
import os
|
|
75
|
+
if os.getcwd().endswith("src"):
|
|
76
|
+
os.chdir("..")
|
|
77
|
+
|
|
78
|
+
print(f"We are currently working in the {os.getcwd()} directory.")
|
|
79
|
+
|
|
80
|
+
class RawCSVtoMLN:
|
|
81
|
+
"""
|
|
82
|
+
This class converts CSV nodelist and edgelist to fast readable input for the MLN class.
|
|
83
|
+
|
|
84
|
+
It accepts an arbitrary number of nodelist and edgelist CSV input files, reads through them,
|
|
85
|
+
and merges the node properties, and converts the edgelists to a scipy.sparse adjacency matrix
|
|
86
|
+
with integer elements that have a binary encoding for the layers.
|
|
87
|
+
|
|
88
|
+
Every input CSV is assumed to have a header and no index col.
|
|
89
|
+
"""
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
node_conf = dict(
|
|
93
|
+
input_folder_prefix = "",
|
|
94
|
+
files = [],
|
|
95
|
+
colmap = "",
|
|
96
|
+
sep = ";",
|
|
97
|
+
main_file = 0,
|
|
98
|
+
output = ""
|
|
99
|
+
),
|
|
100
|
+
edge_conf = dict(
|
|
101
|
+
input_folder_prefix = "",
|
|
102
|
+
files = [],
|
|
103
|
+
colmap = "",
|
|
104
|
+
sep = ";",
|
|
105
|
+
output = ""
|
|
106
|
+
),
|
|
107
|
+
layer_conf = dict(
|
|
108
|
+
raw_file = "",
|
|
109
|
+
file = "",
|
|
110
|
+
output = "",
|
|
111
|
+
symmetrize = [],
|
|
112
|
+
symmetrize_all = False,
|
|
113
|
+
raw_sep=",",
|
|
114
|
+
sep = ",",
|
|
115
|
+
colors = ""
|
|
116
|
+
),
|
|
117
|
+
output_folder = "",
|
|
118
|
+
**kwargs
|
|
119
|
+
):
|
|
120
|
+
|
|
121
|
+
self.node_conf = node_conf
|
|
122
|
+
self.edge_conf = edge_conf
|
|
123
|
+
self.layer_conf = layer_conf
|
|
124
|
+
self.output_folder = output_folder
|
|
125
|
+
self.grouped = False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# check if output folder is empty, if not, add / to end
|
|
129
|
+
if self.output_folder != "":
|
|
130
|
+
if not self.output_folder.endswith("/"):
|
|
131
|
+
self.output_folder += "/"
|
|
132
|
+
|
|
133
|
+
# other kwargs
|
|
134
|
+
for (k, v) in kwargs.items():
|
|
135
|
+
setattr(self, k, v)
|
|
136
|
+
|
|
137
|
+
###########################
|
|
138
|
+
##### LAYERS ###########
|
|
139
|
+
###########################
|
|
140
|
+
|
|
141
|
+
def _open_file(self, read_func, *args, **kwargs):
|
|
142
|
+
success = 0
|
|
143
|
+
iter = 0
|
|
144
|
+
while not success and iter < 5:
|
|
145
|
+
try:
|
|
146
|
+
result = read_func(*args,**kwargs)
|
|
147
|
+
success = 1
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
iter +=1
|
|
150
|
+
if success:
|
|
151
|
+
print(f"\tRead file on attempt no. {iter+1}.")
|
|
152
|
+
else:
|
|
153
|
+
raise FileNotFoundError("Could not read file in 5 attempts.")
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def init_layers(self):
|
|
158
|
+
"""
|
|
159
|
+
Either read layer file, or create rich layer dataframe from bare minimum input.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
# if the layer file is not yet prepared
|
|
163
|
+
if self.layer_conf["file"] == "":
|
|
164
|
+
print("Trying to create enriched layer dataframe...")
|
|
165
|
+
# there should at least be a very basic layer file (e.g. prepared from the edgelists, if nothing else)
|
|
166
|
+
if self.layer_conf["raw_file"]=="":
|
|
167
|
+
self.init_raw_layers_from_edges()
|
|
168
|
+
else:
|
|
169
|
+
print("\tReading raw layer input file...")
|
|
170
|
+
self.layers = self._open_file(
|
|
171
|
+
pd.read_csv,
|
|
172
|
+
self.layer_conf["input_folder_prefix"] + self.layer_conf["raw_file"],
|
|
173
|
+
index_col=None,
|
|
174
|
+
header=0,
|
|
175
|
+
sep=self.layer_conf["raw_sep"]
|
|
176
|
+
)
|
|
177
|
+
# if we want to rename columns, there's either a JSON or a dict
|
|
178
|
+
if self.layer_conf["colmap"] != "":
|
|
179
|
+
print("\tRenaming layer dataframe columns...")
|
|
180
|
+
if type(self.layer_conf["colmap"]) == str:
|
|
181
|
+
self.layer_conf["colmap"] = json.load(open(self.layer_conf["colmap"]))
|
|
182
|
+
print(json.dumps(self.layer_conf["colmap"],indent="\t\t"))
|
|
183
|
+
self.layers.rename(columns = self.layer_conf["colmap"], inplace = True)
|
|
184
|
+
|
|
185
|
+
print("\tAdding binary representation, groups and long labels...")
|
|
186
|
+
if self.grouped:
|
|
187
|
+
grps = dict(zip(self.layers["group_layer"],self.layers["group_label"]))
|
|
188
|
+
self.layers_ungrouped = self.layers
|
|
189
|
+
self.layer_mapping = dict(zip(self.layers_ungrouped["layer"],self.layers_ungrouped["group_layer"]))
|
|
190
|
+
self.layers = pd.DataFrame(np.array([list(grps.keys()),list(grps.values())]).T,columns = ["layer","label"])
|
|
191
|
+
# creating different 2**i numbers for all linktypes for binary encoding
|
|
192
|
+
self.layers["binary"] = self.layers.index.map(lambda i: int(2**i))
|
|
193
|
+
if "group" not in self.layers:
|
|
194
|
+
self.layers["group"] = self.layers["label"]
|
|
195
|
+
# creating long labels for layers for visualizations etc.
|
|
196
|
+
self.layers["label_long"] = self.layers.apply(lambda row: row['group']+": "+row["label"], axis=1)
|
|
197
|
+
# layer_group -> integer representations: summing up all binary numbers corresponding to a certain group
|
|
198
|
+
self.layer_group_binary = self.layers.groupby("group").sum()["binary"].map(int).to_dict()
|
|
199
|
+
|
|
200
|
+
# adding group colors
|
|
201
|
+
if self.layer_conf["colors"]!="":
|
|
202
|
+
print("\tAdding colors...")
|
|
203
|
+
if type(self.layer_conf["colors"]) == str:
|
|
204
|
+
self.layer_conf["colors"] = json.load(open(self.layer_conf["colors"]))
|
|
205
|
+
# print(json.dumps(self.layer_conf["colors"],indent="\t\t"))
|
|
206
|
+
self.layers["color"] = self.layers["group"].map(self.layer_conf["colors"])
|
|
207
|
+
# exporting
|
|
208
|
+
self.layer_conf["file"] = self.output_folder + self.layer_conf["output"]
|
|
209
|
+
self.layers.to_csv(self.layer_conf["file"],index=False,header=True)
|
|
210
|
+
|
|
211
|
+
print("Done.")
|
|
212
|
+
print("Reading " + self.layer_conf["file"] + "...")
|
|
213
|
+
self.layers = pd.read_csv(self.layer_conf["file"], index_col = None, header=0)
|
|
214
|
+
print("Layer dataframe",self.layers.head(),self.layers.columns,end="\n")
|
|
215
|
+
|
|
216
|
+
def init_raw_layers_from_edges(self):
|
|
217
|
+
"""
|
|
218
|
+
This function reads the raw edgelist file and creates a layer dataframe
|
|
219
|
+
from the different linktypes.
|
|
220
|
+
"""
|
|
221
|
+
raise ValueError("To initialize layers from edgelist is not yet done.")
|
|
222
|
+
# getting all layer types
|
|
223
|
+
layers = self.edgelist["layer"].unique()
|
|
224
|
+
# creating layer dataframe
|
|
225
|
+
self.layers = pd.DataFrame({"label": layers, "layer": layers})
|
|
226
|
+
|
|
227
|
+
###########################
|
|
228
|
+
##### NODES ############
|
|
229
|
+
###########################
|
|
230
|
+
|
|
231
|
+
# Loading node attributes files
|
|
232
|
+
# =============================
|
|
233
|
+
def init_nodes(self):
|
|
234
|
+
"""
|
|
235
|
+
Read node dataframe from node_conf["files"]. If node_conf["files"] is empty,
|
|
236
|
+
create a node dataframe from the edgelist.
|
|
237
|
+
"""
|
|
238
|
+
print("Creating merged node attribute file...")
|
|
239
|
+
|
|
240
|
+
if self.node_conf["colmap"] != "":
|
|
241
|
+
if type(self.node_conf["colmap"]) == str:
|
|
242
|
+
self.node_conf["colmap"] = json.load(open(self.node_conf["colmap"]))
|
|
243
|
+
|
|
244
|
+
def get_node_file(p):
|
|
245
|
+
# add input folder prefix from node_conf if given
|
|
246
|
+
if self.node_conf["input_folder_prefix"] != "":
|
|
247
|
+
if not self.node_conf["input_folder_prefix"].endswith("/"):
|
|
248
|
+
self.node_conf["input_folder_prefix"] += "/"
|
|
249
|
+
p = self.node_conf["input_folder_prefix"] + p
|
|
250
|
+
|
|
251
|
+
print(f"Reading file {p}...")
|
|
252
|
+
if p.endswith('gz'):
|
|
253
|
+
node_df = self._open_file(pd.read_csv,p,sep=self.node_conf["sep"],index_col=None,compression='gzip')#,nrows=10)
|
|
254
|
+
elif p.endswith('sav'):
|
|
255
|
+
node_df = self._open_file(pd.read_spss,p,usecols = self.node_conf["colmap"].keys(),convert_categoricals=False)
|
|
256
|
+
else:
|
|
257
|
+
node_df = self._open_file(pd.read_csv,p,sep=self.node_conf["sep"],index_col=None)#,nrows=10)
|
|
258
|
+
if self.node_conf["colmap"] != "":
|
|
259
|
+
for c in node_df.columns:
|
|
260
|
+
if c not in self.node_conf["colmap"]:
|
|
261
|
+
node_df.drop(c,axis=1,inplace=True)
|
|
262
|
+
node_df.rename(columns = self.node_conf["colmap"],inplace=True)
|
|
263
|
+
print(node_df.head())
|
|
264
|
+
node_df.set_index("label",inplace=True)
|
|
265
|
+
return node_df
|
|
266
|
+
|
|
267
|
+
if self.node_conf["files"]!="":
|
|
268
|
+
print("Merging all node files...")
|
|
269
|
+
# TODO: check for joins, we might lose some lines here!
|
|
270
|
+
nodes = pd.concat([get_node_file(f) for f in self.node_conf["files"]], axis=1)
|
|
271
|
+
|
|
272
|
+
# make label a column
|
|
273
|
+
nodes.reset_index(inplace=True)
|
|
274
|
+
if "id" not in nodes.columns:
|
|
275
|
+
# add ids if they don't exist
|
|
276
|
+
nodes.reset_index(inplace=True)
|
|
277
|
+
# renaming columns to human readable and consistency
|
|
278
|
+
nodes.rename(columns={"index":"id"},inplace=True)
|
|
279
|
+
|
|
280
|
+
self.nodes = nodes
|
|
281
|
+
print("Initialized node dataframe.")
|
|
282
|
+
else:
|
|
283
|
+
raise ValueError("To initialize nodelist from edgelist is not yet done.")
|
|
284
|
+
|
|
285
|
+
###########################
|
|
286
|
+
##### EDGES ############
|
|
287
|
+
###########################
|
|
288
|
+
|
|
289
|
+
def init_edges(self):
|
|
290
|
+
# getting id <-> label mappings
|
|
291
|
+
self.nodemap_back = dict(zip(self.nodes["id"], self.nodes['label']))
|
|
292
|
+
self.nodemap = {v:k for k,v in self.nodemap_back.items()}
|
|
293
|
+
|
|
294
|
+
self.N = len(self.nodemap.keys())
|
|
295
|
+
print(f"N is {self.N}")
|
|
296
|
+
self.A = csr_matrix((self.N,self.N), dtype=np.uint64)
|
|
297
|
+
|
|
298
|
+
def adjacency_matrix(self, edgelist, binary, symmetrize=False):
|
|
299
|
+
"""
|
|
300
|
+
This function creates the adjacency matrix representation of a graph
|
|
301
|
+
based on a pandas.DataFrame edgelist. The edgelist should be a plain array.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
|
|
306
|
+
A : scipy.sparse.csr_matrix
|
|
307
|
+
sparse adjacency matrix
|
|
308
|
+
"""
|
|
309
|
+
# remapping labels to integer ids from 0 to N-1 to load into sparse CSR matrix
|
|
310
|
+
i = pd.Series(map(lambda x: self.nodemap.get(x),edgelist["source"]))
|
|
311
|
+
j = pd.Series(map(lambda x: self.nodemap.get(x),edgelist["target"]))
|
|
312
|
+
|
|
313
|
+
in_nodelist = ~pd.isnull(i)&~pd.isnull(j)
|
|
314
|
+
i = i[in_nodelist]
|
|
315
|
+
j = j[in_nodelist]
|
|
316
|
+
|
|
317
|
+
# if creating / enforcing undirected graph
|
|
318
|
+
if symmetrize:
|
|
319
|
+
sym_i = i+j
|
|
320
|
+
sym_j = j+i
|
|
321
|
+
i = sym_i
|
|
322
|
+
del sym_i
|
|
323
|
+
j = sym_j
|
|
324
|
+
del sym_j
|
|
325
|
+
|
|
326
|
+
A = csr_matrix((np.ones(len(i)),(i,j)), shape=(self.N,self.N), dtype = np.uint64)
|
|
327
|
+
if symmetrize:
|
|
328
|
+
A = csr_matrix(A>0, dtype=np.uint64)
|
|
329
|
+
# we multiply to 1/0 matrix by that number so that it corresponds to a certain edgetype
|
|
330
|
+
A *= binary
|
|
331
|
+
|
|
332
|
+
return A
|
|
333
|
+
|
|
334
|
+
def _print_size(self,var,name=""):
|
|
335
|
+
"""
|
|
336
|
+
Prints variable size in GB to allow for memory control.
|
|
337
|
+
"""
|
|
338
|
+
print(f"{name}: {round(sys.getsizeof(var)/1024**3,2)}GB")
|
|
339
|
+
|
|
340
|
+
def read_all_edges(self):
|
|
341
|
+
"""
|
|
342
|
+
This function loads all edgelists for the
|
|
343
|
+
different linktypes of the different layers, and subsequently
|
|
344
|
+
adds 2**i to the adjacency matrix for the edges, where 2**i corresponds
|
|
345
|
+
to the type of the edge from self.layers["binary"].
|
|
346
|
+
In the end, the function saves the scipy.sparse.csr_matrix type
|
|
347
|
+
adjacency matrix to the given location
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
# check if symmetrize_all in layer_conf is str
|
|
351
|
+
if type(self.layer_conf["symmetrize_all"]) == str:
|
|
352
|
+
# if "true" or "false", set to boolean
|
|
353
|
+
if self.layer_conf["symmetrize_all"].lower() == "true":
|
|
354
|
+
self.layer_conf["symmetrize_all"] = True
|
|
355
|
+
elif self.layer_conf["symmetrize_all"].lower() == "false":
|
|
356
|
+
self.layer_conf["symmetrize_all"] = False
|
|
357
|
+
else:
|
|
358
|
+
raise ValueError("symmetrize_all in layer_conf is not a boolean value.")
|
|
359
|
+
|
|
360
|
+
# if symmetrize_all is True, add all layers to symmetrize list
|
|
361
|
+
if self.layer_conf["symmetrize_all"]:
|
|
362
|
+
self.layer_conf["symmetrize"] = self.layers["layer"].tolist()
|
|
363
|
+
|
|
364
|
+
if self.grouped:
|
|
365
|
+
l_list = self.layers_ungrouped["layer"]
|
|
366
|
+
else:
|
|
367
|
+
l_list = self.layers["layer"]
|
|
368
|
+
|
|
369
|
+
# get chunks of the edgelist
|
|
370
|
+
if self.edge_conf["colmap"] != "":
|
|
371
|
+
if type(self.edge_conf["colmap"]) == str:
|
|
372
|
+
self.edge_conf["colmap"] = json.load(open(self.edge_conf["colmap"]))
|
|
373
|
+
|
|
374
|
+
if self.edge_conf["input_folder_prefix"] != "":
|
|
375
|
+
if not self.edge_conf["input_folder_prefix"].endswith("/"):
|
|
376
|
+
self.edge_conf["input_folder_prefix"] += "/"
|
|
377
|
+
for i in range(len(self.edge_conf["files"])):
|
|
378
|
+
self.edge_conf["files"][i] = self.edge_conf["input_folder_prefix"] + self.edge_conf["files"][i]
|
|
379
|
+
|
|
380
|
+
# START HEAVY PART
|
|
381
|
+
chunksize = int(2e8)
|
|
382
|
+
|
|
383
|
+
# read all edge files
|
|
384
|
+
for ef in self.edge_conf["files"]:
|
|
385
|
+
print(f"Reading edge file {ef}...")
|
|
386
|
+
# loading file in chunks
|
|
387
|
+
print("\tLoading file in chunks...")
|
|
388
|
+
tic = time()
|
|
389
|
+
iter = 0
|
|
390
|
+
with self._open_file(pd.read_csv,ef, sep=self.edge_conf["sep"],header=0,chunksize=chunksize) as reader:
|
|
391
|
+
# for all chunks
|
|
392
|
+
for chunk in reader:
|
|
393
|
+
iter += 1
|
|
394
|
+
self._print_size(chunk,"\tSize of current chunk")
|
|
395
|
+
print(f"\tChunk no. {iter}")
|
|
396
|
+
if self.edge_conf["colmap"]!="":
|
|
397
|
+
for k,v in self.edge_conf["colmap"].items():
|
|
398
|
+
if v == None and k in chunk.columns:
|
|
399
|
+
chunk.drop(k,inplace=True,axis=1)
|
|
400
|
+
else:
|
|
401
|
+
chunk.rename(columns = {k:v},inplace=True)
|
|
402
|
+
|
|
403
|
+
# make sure node labels are integers for the mapping to work
|
|
404
|
+
if chunk["source"].dtype!=int:
|
|
405
|
+
chunk["source"] = chunk["source"].map(int)
|
|
406
|
+
if chunk["target"].dtype!=int:
|
|
407
|
+
chunk["target"] = chunk["target"].map(int)
|
|
408
|
+
|
|
409
|
+
# check for layers in chunk and add them to adjacency matrix
|
|
410
|
+
for l in l_list:
|
|
411
|
+
if not self.grouped:
|
|
412
|
+
layer = l
|
|
413
|
+
else:
|
|
414
|
+
layer = self.layer_mapping[l]
|
|
415
|
+
# linktype name
|
|
416
|
+
name = self.layers.set_index("layer").loc[layer]["label_long"]
|
|
417
|
+
binary = self.layers.set_index("layer").loc[layer]["binary"]
|
|
418
|
+
selection = chunk["layer"]==l
|
|
419
|
+
num_edges = (selection).sum()
|
|
420
|
+
# contains values of binary_linktype / 0
|
|
421
|
+
if num_edges>0:
|
|
422
|
+
A = self.adjacency_matrix(chunk[selection], binary, symmetrize=layer in self.layer_conf["symmetrize"])
|
|
423
|
+
print(f"\tAdding {A.nnz} edges to layer {layer}.")
|
|
424
|
+
self.A += A
|
|
425
|
+
print("\tNumber of nonzero edges in full adjacency matrix:",self.A.nnz)
|
|
426
|
+
#temp = dict(zip(zip(*A.nonzero()),A.data))
|
|
427
|
+
#print("temp",temp)
|
|
428
|
+
#self.A_dict = {
|
|
429
|
+
# key: self.A_dict.get(key,0) | temp.get(key,0) for key in set(self.A_dict.keys()) | temp.keys()
|
|
430
|
+
#}
|
|
431
|
+
#print("A_dict",self.A_dict)
|
|
432
|
+
# cleaning memory of large unnecessary objects
|
|
433
|
+
del A#,temp
|
|
434
|
+
toc = time()
|
|
435
|
+
print(f"\tFinished with chunk in {round(toc-tic,1)}s.")
|
|
436
|
+
tic = toc
|
|
437
|
+
|
|
438
|
+
#data = list(self.A_dict.values())
|
|
439
|
+
#i = [elem[0] for elem in self.A_dict.keys()]
|
|
440
|
+
#j = [elem[1] for elem in self.A_dict.keys()]
|
|
441
|
+
#self.A = csr_matrix((data,(i,j)),shape=(self.N,self.N),dtype='int8')
|
|
442
|
+
#del self.A_dict,i,j,data
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def init_all(self):
|
|
446
|
+
"""
|
|
447
|
+
Read all components given in config, and save results to output folder if necessary.
|
|
448
|
+
"""
|
|
449
|
+
self.init_layers()
|
|
450
|
+
print(f"LAYERS: {self.layers.shape[0]}")
|
|
451
|
+
self.init_nodes()
|
|
452
|
+
print(f"NODES: {self.nodes.shape[0]}")
|
|
453
|
+
self.init_edges()
|
|
454
|
+
self.read_all_edges()
|
|
455
|
+
print(f"EDGES: {self.A.nnz}")
|
|
456
|
+
|
|
457
|
+
if "save" in self.__dict__:
|
|
458
|
+
if self.save:
|
|
459
|
+
self.save_all()
|
|
460
|
+
|
|
461
|
+
def save_layer_df(self, output):
|
|
462
|
+
print("Saving layer dataframe...")
|
|
463
|
+
self.layers.to_csv(output,index=False,header=True)
|
|
464
|
+
print("Done.")
|
|
465
|
+
|
|
466
|
+
def save_node_df(self, output):
|
|
467
|
+
print("Saving node dataframe...")
|
|
468
|
+
self.nodes.to_csv(output,index=False,header=True,compression="gzip")
|
|
469
|
+
print("Done.")
|
|
470
|
+
|
|
471
|
+
def save_edge_npz(self, output):
|
|
472
|
+
print("Saving edge adjacency matrix...")
|
|
473
|
+
save_npz(output, self.A)
|
|
474
|
+
print("Done.")
|
|
475
|
+
|
|
476
|
+
def save_all(self, overwrite = False):
|
|
477
|
+
# if overwrite in attributes, set overwrite
|
|
478
|
+
if "overwrite" in self.__dict__:
|
|
479
|
+
overwrite = self.overwrite
|
|
480
|
+
# check if output_folder is given and give warning if not
|
|
481
|
+
if self.output_folder== "":
|
|
482
|
+
print("WARNING: No output folder given, saving to current directory.")
|
|
483
|
+
else:
|
|
484
|
+
# check if output folder has an ending / and add it if not
|
|
485
|
+
if not self.output_folder.endswith("/"):
|
|
486
|
+
self.output_folder += "/"
|
|
487
|
+
# check if output folder exists, if not, create it
|
|
488
|
+
if not os.path.isdir(self.output_folder):
|
|
489
|
+
os.mkdir(self.output_folder)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
print("Writing node, layer and edge files...")
|
|
493
|
+
|
|
494
|
+
if self.layer_conf["output"]=="": self.layer_conf["output"] = "layers.csv"
|
|
495
|
+
# check if the above file exists, if overwrite=False, raise error
|
|
496
|
+
if os.path.isfile(self.output_folder + self.layer_conf["output"]) and not overwrite:
|
|
497
|
+
raise ValueError("Layer file already exists, please specify overwrite=True in config file.")
|
|
498
|
+
print("Writing layer file to " + self.output_folder + self.layer_conf["output"] + "...")
|
|
499
|
+
self.save_layer_df(self.output_folder + self.layer_conf["output"])
|
|
500
|
+
print("Done")
|
|
501
|
+
|
|
502
|
+
if self.node_conf["output"]=="": self.node_conf["output"] = "nodes.csv.gz"
|
|
503
|
+
# check if the above file exists, if overwrite=False, raise error
|
|
504
|
+
if os.path.isfile(self.output_folder + self.node_conf["output"]) and not overwrite:
|
|
505
|
+
raise ValueError("Node file already exists, please specify overwrite=True in config file.")
|
|
506
|
+
print("Writing node file to " + self.output_folder + self.node_conf["output"] + "...")
|
|
507
|
+
self.save_node_df(self.output_folder + self.node_conf["output"])
|
|
508
|
+
print("Done.")
|
|
509
|
+
|
|
510
|
+
if self.edge_conf["output"]=="": self.edge_conf["output"] = "edges.npz"
|
|
511
|
+
# check if the above file exists, if overwrite=False, raise error
|
|
512
|
+
if os.path.isfile(self.output_folder + self.edge_conf["output"]) and not overwrite:
|
|
513
|
+
raise ValueError("Edge file already exists, please specify overwrite=True in config file.")
|
|
514
|
+
print("Writing edge file to " + self.output_folder + self.edge_conf["output"] + "...")
|
|
515
|
+
self.save_edge_npz(self.output_folder + self.edge_conf["output"])
|
|
516
|
+
print("Done.")
|
|
517
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
### MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Eszter Bokányi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
In any academic work, the package should be cited as follows:
|
|
16
|
+
|
|
17
|
+
Bokányi, E., Kazmina, Y., & de Jong, R. (2023). POPNET MultiLayerNetwork Python Class (Version v2.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.10838865
|
|
18
|
+
|
|
19
|
+
The BibTeX entry for the citation is:
|
|
20
|
+
```
|
|
21
|
+
@software{bokanyi2023popnet,
|
|
22
|
+
title = {{{POPNET MultiLayerNetwork Python Class}}},
|
|
23
|
+
author = {Bok\'anyi, Eszter and Kazmina, Yuliia and family=Jong, given=Rachel, prefix=de, useprefix=true},
|
|
24
|
+
date = {2023},
|
|
25
|
+
doi = {10.5281/zenodo.10838865},
|
|
26
|
+
url = {https://zenodo.org/doi/10.5281/zenodo.10838865},
|
|
27
|
+
urldate = {2024-11-11},
|
|
28
|
+
abstract = {Library for working with large multilayer networks by the POPNET group.},
|
|
29
|
+
organization = {Zenodo},
|
|
30
|
+
version = {v2.0}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
35
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
37
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
38
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
39
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
40
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mlnlib
|
|
3
|
+
Version: 0.2.35
|
|
4
|
+
Summary: Memory-efficient library for loading and working with large unweighted multilayer networks.
|
|
5
|
+
Author-email: Eszter Bokányi <e.bokanyi@uva.nl>
|
|
6
|
+
Project-URL: Homepage, https://github.com/popnet-io/popnet_mln
|
|
7
|
+
Project-URL: Issues, https://github.com/popnet-io/popnet_mln/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE.md
|
|
14
|
+
Requires-Dist: pandas >=1.4.4
|
|
15
|
+
Requires-Dist: polars >=0.20.6
|
|
16
|
+
Requires-Dist: numpy >=1.20.3
|
|
17
|
+
Requires-Dist: python-igraph >=0.9.9
|
|
18
|
+
Requires-Dist: networkx >=3.0
|
|
19
|
+
Requires-Dist: scipy >=1.10.1
|
|
20
|
+
|
|
21
|
+
# MLN
|
|
22
|
+
|
|
23
|
+
Library for working with large multilayer networks.
|
|
24
|
+
|
|
25
|
+
Authors:
|
|
26
|
+
* Eszter Bokányi
|
|
27
|
+
* Rachel de Jong
|
|
28
|
+
* Yuliia Kazmina
|
|
29
|
+
|
|
30
|
+
Contact:
|
|
31
|
+
`e.bokanyi@uva.nl`
|
|
32
|
+
|
|
33
|
+
The code is distirbuted under the MIT Licence and should be properly attributed and cited upon reuse - see LICENCE.md for details.
|
|
34
|
+
|
|
35
|
+
## MultiLayeredNetwork class
|
|
36
|
+
|
|
37
|
+
The `MultiLayeredNetwork` class from `src/mln.py` contains methods and attributes to work with a large
|
|
38
|
+
multilayer network using different edge types and layers efficiently.
|
|
39
|
+
|
|
40
|
+
The network has to be unweighted, but can be directed.
|
|
41
|
+
|
|
42
|
+
The network is either loaded from an already saved sparse matrix and a
|
|
43
|
+
node attribute CSV using the following attributes:
|
|
44
|
+
* adjacency_file: scipy.sparse.csr matrix saved as an `npz` file
|
|
45
|
+
* node_attribute_file: CSV to be loaded as a pd.DataFrame, rows in
|
|
46
|
+
order of previous matrix rows should contain a column called
|
|
47
|
+
"label" that is the primary key for node identification
|
|
48
|
+
|
|
49
|
+
Or in-memory objects can also be given to the constructor:
|
|
50
|
+
* adjacency_matrix: scipy.sparse.csr matrix
|
|
51
|
+
* node_attribute_dataframe: pd.DataFrame, rows in order of previous
|
|
52
|
+
matrix rows
|
|
53
|
+
|
|
54
|
+
Pre-stored files with standard names can be called with the shorthand:
|
|
55
|
+
* from_library: str
|
|
56
|
+
Then the npz and csv.gz files will be read from the
|
|
57
|
+
library_path/from_library folder similarly to the first method.
|
|
58
|
+
|
|
59
|
+
After loading these two key elements, class attributes and methods work
|
|
60
|
+
the same.
|
|
61
|
+
|
|
62
|
+
The adjacency matrix self.A is stored in a `scipy.sparse.csr_matrix`
|
|
63
|
+
class, that only saves nonzero elements, and on which scipy csgraph
|
|
64
|
+
algorithms run. People are indexed from 0 to N-1, where N is the total
|
|
65
|
+
number of nodes in this network.
|
|
66
|
+
|
|
67
|
+
Two dictionaries mapping user id (label) to integer node ids (NID) and
|
|
68
|
+
back are created based on the node attribute file in the
|
|
69
|
+
`self.map_label_to_nid` and `self.map_nid_to_label` attributes. These
|
|
70
|
+
mappings only refer to one instance of the class - if you create a
|
|
71
|
+
subgraph (see later) the coding is going to change, and the mapping
|
|
72
|
+
should be found in the new instance that represents the subgraph.
|
|
73
|
+
|
|
74
|
+
The adjacency matrix contains integers that encode linktypes if viewed
|
|
75
|
+
as binary numbers. Each possible linktype is assigned an integer of the
|
|
76
|
+
form 2\**i. For example, if both type i and type j edge is present
|
|
77
|
+
between two people, then the corresponding value in self.A would be
|
|
78
|
+
2\**i+2\**j. It means that we can test for a certain edgetype using
|
|
79
|
+
bitwise AND operation very cheaply. E.g. a certain element of self.A is
|
|
80
|
+
7, then 7=1+2+4 which means that edgetypes 0,1, and 2 are present
|
|
81
|
+
between the two people, and 7&2 = 2 in Python (it behaves like a mask
|
|
82
|
+
111 & 010 = 010).
|
|
83
|
+
|
|
84
|
+
scipy.csr matrices are cheap to slice rowwise, but beware, some
|
|
85
|
+
operation that seem straightforward in numpy might be costly (e.g.
|
|
86
|
+
getting random coordinates after each other or colwise slicing)! If
|
|
87
|
+
something is running too long, consult the scipy reference manual.
|
|
88
|
+
|
|
89
|
+
Node attributes are stored in `self.node_attributes` which is a
|
|
90
|
+
`pandas.DataFrame`. It is possible to store human-readable or longer
|
|
91
|
+
column names in `self.attribute_colnames`, and code tables for used
|
|
92
|
+
variables in `self.attribute_code_table`, keys of that variable are the
|
|
93
|
+
colnames in the values of `self.attribute_colnames`.
|
|
94
|
+
|
|
95
|
+
Example loadings:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
# FROM FILES
|
|
99
|
+
# import custom class for the network from mln
|
|
100
|
+
import MultiLayeredNetwork
|
|
101
|
+
|
|
102
|
+
# read the whole network
|
|
103
|
+
popnet = MultiLayeredNetwork(
|
|
104
|
+
adjacency_file = 'path_to/adjacency.npz',
|
|
105
|
+
node_attribute_file = 'path_to/attributes.csv.gz'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# FROM MEMORY
|
|
109
|
+
# import custom class for the network
|
|
110
|
+
from mln import MultiLayeredNetwork
|
|
111
|
+
|
|
112
|
+
# read the whole network
|
|
113
|
+
popnet = MultiLayeredNetwork(
|
|
114
|
+
adjacency_matrix = A, # NxN scipy.sparse.csr_matrix
|
|
115
|
+
node_attribute_file = df # pd.DataFrame containing N rows in the order of the matrix
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# FROM LIBRARY
|
|
119
|
+
# import custom class for the network
|
|
120
|
+
from mln import MultiLayeredNetwork
|
|
121
|
+
|
|
122
|
+
# read the whole network
|
|
123
|
+
popnet = MultiLayeredNetwork(
|
|
124
|
+
from_library="full"
|
|
125
|
+
)
|
|
126
|
+
```
|