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/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
+ ```