-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdynamic_feature_extraction.py
More file actions
369 lines (286 loc) · 11.7 KB
/
dynamic_feature_extraction.py
File metadata and controls
369 lines (286 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import numpy as np
import os
import csv
from collections import OrderedDict
import networkx as nx
import matplotlib.pyplot as plt
class GraphAnalysis:
'''
class for analyzing dynamic network.
- load network from nodes and edgesf iles
- calculate dynamic feature matrix
- plot degree distribution
- plot network growth over time
'''
def __init__(self, id, fp = "\\output\\", verbose=False):
# whether to log
self.verbose = verbose
self.id = id
self.fp = fp
# graph representation
self.edges = {} # dict edges {node : (set of adjacent nodes)}
self.nodes = OrderedDict() # OrderedDictionary of form {node : time}
filepath_edges = os.path.join(fp, str(id) + "_edges.csv")
filepath_nodes = os.path.join(fp, str(id) +"_nodes.csv")
with open(filepath_nodes, 'r') as f:
self.load_nodes(f)
with open(filepath_edges, 'r') as f:
self.load_edges(f)
'''
get so-called scaled timesteps such that each timestep has the same number of nodes arriving
'''
def get_scaled_timesteps(self, a, up_to):
number_of_nodes_per_step = int(up_to / a) # number of nodes arriving per timestep
time_max = list(self.nodes.items())[up_to-1][1] # total running time
stamps = [] # timestamps indicating arrival of last node for timestep
for i in range(a):
# get the timestamp of the arrival of the last node
succes = False
j = 0
while not succes: # ceiling of i/number_of_nodes_per_step that is in nodes.
try:
s = float(self.nodes[str((i*number_of_nodes_per_step)+j)])
stamps.append(s)
succes = True
except KeyError:
j += 1
stamps.append(time_max)
return stamps
'''
calculates feature matrix from Cai2021
@param a: number of rows; timeinterval
@param b: number of column;
@param save: whether to save the matrix as csv
@param up_to: cutoff node, i.e. if consider only part of the network evolution.
@return a x b matrix of floats.
'''
def calculate_feature_matrix(self, a=3, b=4, fp="", save=False, name=None, up_to=None, scaled_timesteps=True, normalize=True):
if up_to is None:
up_to = len(self.nodes)
assert up_to <= len(self.nodes), "up_to parameter larger than number of nodes"
matrix = np.zeros((a, b))
if scaled_timesteps:
time_bounds = self.get_scaled_timesteps(a, up_to)
else:
time_max = list(self.nodes.items())[up_to-1][1] # total running time
time_bounds = [i*(time_max / len(matrix)) for i in range(len(matrix)+1)]
node_groups = self.set_node_groups(up_to, len(matrix[0]))
# fill the matrix for each row
for i in range(len(matrix)):
matrix[i] = self.get_matrix_row(node_groups, time_bounds[i], time_bounds[i+1])
if self.verbose:
print("all matrix rows are calculated")
if normalize:
self.normalize_matrix(matrix)
if save:
if name is None: # use default name
self.save_matrix_to_csv(matrix, node_groups, fp, "_matrix")
else:
self.save_matrix_to_csv(matrix, node_groups, fp, name)
return matrix
'''
calculate added edges
'''
def get_matrix_row(self, node_groups, t_start, t_end):
new_nodes = []
# find which nodes were added in timestep
for n, t in self.nodes.items(): # TODO optimize this iteration by storing some sort of iterator
t = float(t)
if t >= t_start:
if t <= t_end:
new_nodes.append(n)
else:
break
row = np.zeros(len(node_groups))
# update matrix
for n in new_nodes:
for v in self.edges[n]:
for i, group in node_groups.items():
if v in group:
row[i] += 1
# average out values, the matrix represents the average number of degrees a cell in the node group has received.
for i, group in node_groups.items():
if len(group) != 0:
row[i] = row[i] / len(group)
else:
if self.verbose:
print(" Group size of 0! ")
return row
'''
normalize matrix by dividing every element in matrix by sum over all.
'''
def normalize_matrix(self, matrix):
total = sum(sum(matrix[i]) for i in range(len(matrix)))
for i in range(len(matrix)):
for j in range(len(matrix[i])):
matrix[i][j] = matrix[i][j] / total
if self.verbose:
print("matrix has been normalized")
'''
set the node_groups to self.node_groups (G_j)
'''
def set_node_groups(self, up_to, group_amount):
degree_sequence = {}
# calculate the degree sequence untill the "up_to"
for n in list(self.nodes.keys())[:up_to]:
if n in self.edges:
degree_sequence[n] = len(self.edges[n])
for v in self.edges[n]: # since OrderedDict and PA only connects backwards, all v's already in degree_sequence
if v in degree_sequence:
degree_sequence[v] += 1
else:
degree_sequence[v] = 1
else:
self.edges[n] = set()
degree_sequence[n] = 0
# get a sorted degree-sequence
degrees = list(degree_sequence.values())
degrees.sort()
# calculate the empirical CDF (F_T in Weiting Cai)
empirical_cdf = {}
n = len(degrees)
prev = 0
empirical_cdf[0] = 0
for v in degrees:
if v > prev:
empirical_cdf[v] = empirical_cdf[prev] + 1/n
prev = v
else:
empirical_cdf[prev] += 1/n
assert abs(empirical_cdf[degrees[-1]] - 1) < 0.00001
empirical_cdf[degrees[-1]] = 0.999999999 # last cannot be >= 1 by overflow
node_groups = {i : set() for i in range(group_amount)}
for node, degree in degree_sequence.items():
i = group_amount - 1 - ((empirical_cdf[degree]) // (1/group_amount))
node_groups[i].add(node)
if self.verbose:
print("node_groups are set")
return node_groups
'''
populate self.nodes from file
'''
def load_nodes(self, file, up_to=None):
if not up_to is None:
assert isinstance(up_to, int) and up_to > 0, "up_to should be positive integer"
reader = csv.reader(file)
counter = 0
for node in reader: # node is stored as name_node : timestamp
self.nodes[node[0]] = float(node[1])
counter += 1
if self.verbose:
print(f"loaded all {counter} nodes")
'''
populate self.edges from file.
'''
def load_edges(self, file):
reader = csv.reader(file)
# we add the root manually as it has no originating edges
self.edges['0'] = set()
counter = 0
for edge in reader: # edge in file is stored as node1, node2, timestamp
if edge[0] in self.edges:
self.edges[edge[0]].add(edge[1])
else:
self.edges[edge[0]] = set((edge[1],))
if self.verbose:
counter += 1
if counter % 1_000_000 == 0: # print update every million edges
print(f"++ loaded {counter} edges ")
if self.verbose:
print(f"Loaded edges")
def save_matrix_to_csv(self, matrix, node_groups, fp, post_script):
with open(os.path.join(fp, self.id + post_script + ".csv"), "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([len(group) for group in node_groups.values()])
for row in matrix:
writer.writerow(row)
'''
Set self.G to the networkx.Graph() object representation
'''
def set_graph_representation(self):
self.G = nx.Graph()
for node in self.nodes.keys():
self.G.add_node(node)
for u in self.edges.keys():
for v in self.edges[u]:
self.G.add_edge(u, v)
# remove self loops
if self.G.has_edge('0', '0'):
self.G.remove_edge('0', '0')
'''
gets the networkx.Graph object representation, if it does not exist, creates it.
'''
def get_graph_representation(self):
if hasattr(self, 'G'):
return self.G
else:
self.set_graph_representation()
return self.G
'''
plot degree distribution
'''
def plot_tail_degree_distribution(self, loglog=True, title=None, save=False, show=True):
assert (not save) or not title is None, "need title to save"
assert save or show, "neither saving nor showing, what u want?"
# https://github.com/LourensT/degreedistributions
from DegreeDistributions.DegreeDistributions import DegreeDistribution
distr = DegreeDistribution(self.get_graph_representation() ,tail=True)
plt.scatter(x=distr.keys(), y=distr.values(), color='red')
if loglog:
plt.yscale("log")
plt.xscale("log")
if not title is None:
plt.title(title)
if save:
plt.savefig(self.fp + title+"_degrees.png")
print(f"Saved {title}_degrees.png")
if show:
plt.show()
else:
plt.clf()
'''
plot the size over time. @pre's are checked, so method is reasonably robust.
optional params:
@title: string - title of the plot
@save: bool - if True, save as @title.png
@pre: title != None
@fit_exp: bool - if True, an exponential curve, OLS fit of y = a*exp(bx)
'''
def plot_size_over_time(self, title=None, save=False, fit_exp = False, show = True, log=False):
assert (not save) or not title is None, "need title to save"
assert save or show, "neither saving nor showing, what u want?"
cumulative_growth = {}
prev = 0
cumulative_growth[prev] = 0
for time in self.nodes.values():
if time in cumulative_growth:
cumulative_growth[time] += 1
else:
assert prev <= time, "not ordered; what the fuck"
cumulative_growth[time] = 1 + cumulative_growth[prev]
prev = time
plt.scatter(x=cumulative_growth.keys(), y=cumulative_growth.values())
legend = ["Population size over time"]
# fit the exponential curve y = a*exp(bx)
if fit_exp:
logY = [np.log(i) for i in cumulative_growth.values() if i > 0][10:]
x = [ [i,] for i in cumulative_growth.keys()][10:]
from sklearn.linear_model import LinearRegression
reg = LinearRegression().fit(x, logY)
a = np.exp(reg.intercept_)
b = reg.coef_[0]
x_range = np.linspace(x[9][0], max(cumulative_growth.keys()), num=100)
y_pred = [a*np.exp(b*n) for n in x_range]
plt.plot(x_range, y_pred, color="red")
legend.append(f"fit of y = a*exp(bx) with a = {a}, b = {b}")
if not title is None:
plt.title(title)
if log:
plt.yscale("log")
if save:
plt.savefig(self.fp + title+"_growth.png")
print(f"Saved {title}_growth.png")
if show:
plt.show()
else:
plt.clf()