InfluenceDiffusion 0.0.3__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.
- InfluenceDiffusion/Graph.py +478 -0
- InfluenceDiffusion/Trace.py +431 -0
- InfluenceDiffusion/__init__.py +4 -0
- InfluenceDiffusion/estimation_models/BaseWeightEstimator.py +436 -0
- InfluenceDiffusion/estimation_models/EMEstimation.py +236 -0
- InfluenceDiffusion/estimation_models/OptimEstimation.py +291 -0
- InfluenceDiffusion/estimation_models/__init__.py +3 -0
- InfluenceDiffusion/influence_models.py +614 -0
- InfluenceDiffusion/utils.py +29 -0
- InfluenceDiffusion-0.0.3.dist-info/METADATA +21 -0
- InfluenceDiffusion-0.0.3.dist-info/RECORD +13 -0
- InfluenceDiffusion-0.0.3.dist-info/WHEEL +5 -0
- InfluenceDiffusion-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from scipy import stats
|
|
4
|
+
from scipy.stats._distn_infrastructure import rv_frozen
|
|
5
|
+
from typing import Dict, Set, Union, List
|
|
6
|
+
from joblib import Parallel, delayed
|
|
7
|
+
from Graph import Graph
|
|
8
|
+
from Trace import Trace, Traces
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InfluenceModel:
|
|
12
|
+
"""Base class for influence models on a graph.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
g : Graph
|
|
17
|
+
The graph on which the influence model operates.
|
|
18
|
+
check_init : bool, optional
|
|
19
|
+
Whether to check the parameters upon initialization (default is True).
|
|
20
|
+
random_state : int, optional
|
|
21
|
+
Seed for random number generation (default is None).
|
|
22
|
+
n_jobs : int, optional
|
|
23
|
+
Number of parallel jobs for simulations (default is None).
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
g : Graph
|
|
28
|
+
The graph on which the influence model operates.
|
|
29
|
+
vertices : List[int]
|
|
30
|
+
The list of vertices in the graph.
|
|
31
|
+
random_state : int
|
|
32
|
+
Seed for random number generation.
|
|
33
|
+
n_jobs : int
|
|
34
|
+
Number of parallel jobs for simulations.
|
|
35
|
+
seed_set : Set[int]
|
|
36
|
+
Set of initial active nodes.
|
|
37
|
+
_cur_influence_nodes : Set[int]
|
|
38
|
+
Nodes currently influencing others.
|
|
39
|
+
all_influenced_nodes : Set[int]
|
|
40
|
+
All nodes that have been influenced.
|
|
41
|
+
_propagation_trace : List[Set[int]]
|
|
42
|
+
Trace of the propagation process.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, g: Graph, check_init: bool = True, random_state: int = None, n_jobs: int = None) -> None:
|
|
46
|
+
self.g = g
|
|
47
|
+
self.vertices = list(self.g.get_vertices())
|
|
48
|
+
self.random_state = random_state
|
|
49
|
+
self.n_jobs = n_jobs
|
|
50
|
+
if check_init:
|
|
51
|
+
self.check_param_init_correctness()
|
|
52
|
+
|
|
53
|
+
def check_param_init_correctness(self) -> None:
|
|
54
|
+
"""Check the correctness of the model's parameters.
|
|
55
|
+
|
|
56
|
+
Raises
|
|
57
|
+
------
|
|
58
|
+
NotImplementedError
|
|
59
|
+
This method should be implemented in derived classes.
|
|
60
|
+
"""
|
|
61
|
+
raise NotImplementedError()
|
|
62
|
+
|
|
63
|
+
def _init_simulation_rvs(self, simulation_rvs=None) -> None:
|
|
64
|
+
"""Initialize simulation random variables.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
simulation_rvs : np.ndarray, optional
|
|
69
|
+
Predefined random variables for simulation (default is None).
|
|
70
|
+
|
|
71
|
+
Raises
|
|
72
|
+
------
|
|
73
|
+
NotImplementedError
|
|
74
|
+
This method should be implemented in derived classes.
|
|
75
|
+
"""
|
|
76
|
+
raise NotImplementedError()
|
|
77
|
+
|
|
78
|
+
def _generate_simulation_rvs(self, n_runs: int = 1) -> List:
|
|
79
|
+
"""Generate random variables for simulation.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
n_runs : int, optional
|
|
84
|
+
Number of simulation runs (default is 1).
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
List
|
|
89
|
+
Generated random variables for the simulation.
|
|
90
|
+
|
|
91
|
+
Raises
|
|
92
|
+
------
|
|
93
|
+
NotImplementedError
|
|
94
|
+
This method should be implemented in derived classes.
|
|
95
|
+
"""
|
|
96
|
+
raise NotImplementedError()
|
|
97
|
+
|
|
98
|
+
def _pre_simulation_init(self, seed_set: Set[int], simulation_rvs=None) -> None:
|
|
99
|
+
"""Prepare for simulation by initializing parameters.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
seed_set : Set[int]
|
|
104
|
+
Set of initial active nodes.
|
|
105
|
+
simulation_rvs : np.ndarray, optional
|
|
106
|
+
Predefined random variables for simulation (default is None).
|
|
107
|
+
|
|
108
|
+
Raises
|
|
109
|
+
------
|
|
110
|
+
AssertionError
|
|
111
|
+
If the seed set is empty.
|
|
112
|
+
"""
|
|
113
|
+
assert len(seed_set) > 0, "Seed set should contain at least one vertex"
|
|
114
|
+
self.seed_set = set(seed_set)
|
|
115
|
+
self._cur_influence_nodes = deepcopy(self.seed_set)
|
|
116
|
+
self.all_influenced_nodes = deepcopy(self.seed_set)
|
|
117
|
+
self._propagation_trace: List[Set[int]] = [self.seed_set]
|
|
118
|
+
self._init_simulation_rvs(simulation_rvs)
|
|
119
|
+
|
|
120
|
+
def _make_edge_influence_attempt(self, v: int, v_adj: int) -> bool:
|
|
121
|
+
"""Attempt to influence adjacent vertex.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
v : int
|
|
126
|
+
The current influencing vertex.
|
|
127
|
+
v_adj : int
|
|
128
|
+
The adjacent vertex to be influenced.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
bool
|
|
133
|
+
True if the adjacent vertex is influenced, otherwise False.
|
|
134
|
+
|
|
135
|
+
Raises
|
|
136
|
+
------
|
|
137
|
+
NotImplementedError
|
|
138
|
+
This method should be implemented in derived classes.
|
|
139
|
+
"""
|
|
140
|
+
raise NotImplementedError()
|
|
141
|
+
|
|
142
|
+
def _simulate_trace(self, out_trace_type: bool = True) -> Union[Trace, List[Set[int]]]:
|
|
143
|
+
"""Simulate the influence spread.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
out_trace_type : bool, optional
|
|
148
|
+
Whether to return a Trace object (default is True).
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
Union[Trace, List[Set[int]]]
|
|
153
|
+
The trace of influenced nodes or a Trace object.
|
|
154
|
+
"""
|
|
155
|
+
while self._cur_influence_nodes:
|
|
156
|
+
self._make_step()
|
|
157
|
+
return Trace(self.g, tuple(self._propagation_trace)) if out_trace_type else self._propagation_trace
|
|
158
|
+
|
|
159
|
+
def sample_trace(self, seed_set: Set[int],
|
|
160
|
+
out_trace_type: bool = True,
|
|
161
|
+
simulation_rvs=None) -> Union[Trace, List[Set[int]]]:
|
|
162
|
+
"""Sample a single trace from the model.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
seed_set : Set[int]
|
|
167
|
+
Set of initial active nodes.
|
|
168
|
+
out_trace_type : bool, optional
|
|
169
|
+
Whether to return a Trace object (default is True).
|
|
170
|
+
simulation_rvs : np.ndarray, optional
|
|
171
|
+
Predefined random variables for simulation (default is None).
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
Union[Trace, List[Set[int]]]
|
|
176
|
+
The sampled trace of influenced nodes or a Trace object.
|
|
177
|
+
"""
|
|
178
|
+
self._pre_simulation_init(seed_set, simulation_rvs=simulation_rvs)
|
|
179
|
+
return self._simulate_trace(out_trace_type=out_trace_type)
|
|
180
|
+
|
|
181
|
+
def make_simulation(self, seed_set: Set[int], simulation_rvs=None) -> np.ndarray:
|
|
182
|
+
"""Run a simulation and return the propagation trace.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
seed_set : Set[int]
|
|
187
|
+
Set of initial active nodes.
|
|
188
|
+
simulation_rvs : np.ndarray, optional
|
|
189
|
+
Predefined random variables for simulation (default is None).
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
np.ndarray
|
|
194
|
+
The propagation trace as an array.
|
|
195
|
+
"""
|
|
196
|
+
self._pre_simulation_init(seed_set, simulation_rvs=simulation_rvs)
|
|
197
|
+
return self._simulate_trace()
|
|
198
|
+
|
|
199
|
+
def sample_traces(self, n_traces: int = 100,
|
|
200
|
+
seed_size_range: List[int] = None,
|
|
201
|
+
out_trace_type: bool = True) -> Union[Traces, List[List[Set[int]]]]:
|
|
202
|
+
"""Sample multiple traces.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
n_traces : int, optional
|
|
207
|
+
Number of traces to sample (default is 100).
|
|
208
|
+
seed_size_range : List[int], optional
|
|
209
|
+
Range of seed sizes to sample from (default is None).
|
|
210
|
+
out_trace_type : bool, optional
|
|
211
|
+
Whether to return a Traces object (default is True).
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
Union[Traces, List[List[Set[int]]]]
|
|
216
|
+
The sampled traces or a Traces object.
|
|
217
|
+
"""
|
|
218
|
+
seed_sets = self._sample_seeds(n_seeds=n_traces, seed_size_range=seed_size_range)
|
|
219
|
+
return self.sample_traces_from_seeds(seed_sets, out_trace_type=out_trace_type)
|
|
220
|
+
|
|
221
|
+
def sample_traces_from_seeds(self, seed_sets: List[Set[int]], out_trace_type: bool = True) -> Union[Traces, List[List[Set[int]]]]:
|
|
222
|
+
"""Sample traces from a list of seed sets.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
seed_sets : List[Set[int]]
|
|
227
|
+
List of seed sets for sampling.
|
|
228
|
+
out_trace_type : bool, optional
|
|
229
|
+
Whether to return a Traces object (default is True).
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
Union[Traces, List[List[Set[int]]]]
|
|
234
|
+
The sampled traces or a Traces object.
|
|
235
|
+
"""
|
|
236
|
+
simulation_rvs_over_runs = self._generate_simulation_rvs(n_runs=len(seed_sets))
|
|
237
|
+
traces = Parallel(n_jobs=self.n_jobs)(
|
|
238
|
+
delayed(lambda seed_set, simulation_rvs:
|
|
239
|
+
self.sample_trace(seed_set=seed_set, out_trace_type=False, simulation_rvs=simulation_rvs))
|
|
240
|
+
(seed_set, thresholds)
|
|
241
|
+
for seed_set, thresholds in zip(seed_sets, simulation_rvs_over_runs)
|
|
242
|
+
)
|
|
243
|
+
return Traces(self.g, traces) if out_trace_type else traces
|
|
244
|
+
|
|
245
|
+
def _make_step(self) -> None:
|
|
246
|
+
"""Make a simulation step by activating new nodes.
|
|
247
|
+
|
|
248
|
+
This method updates the current influence nodes and the propagation trace.
|
|
249
|
+
"""
|
|
250
|
+
new_influence_nodes = set()
|
|
251
|
+
for v in self._cur_influence_nodes:
|
|
252
|
+
for v_adj in self.g.get_children(v).difference(self.all_influenced_nodes):
|
|
253
|
+
influence_res = self._make_edge_influence_attempt(v, v_adj)
|
|
254
|
+
if influence_res:
|
|
255
|
+
new_influence_nodes.add(v_adj)
|
|
256
|
+
|
|
257
|
+
self._cur_influence_nodes = new_influence_nodes
|
|
258
|
+
self.all_influenced_nodes.update(new_influence_nodes)
|
|
259
|
+
|
|
260
|
+
# Append to the propagation trace if there are new nodes influenced
|
|
261
|
+
if new_influence_nodes:
|
|
262
|
+
self._propagation_trace.append(new_influence_nodes)
|
|
263
|
+
|
|
264
|
+
def _sample_seeds(self, n_seeds: int, seed_size_range: List[int] = None) -> List[Set[int]]:
|
|
265
|
+
"""Sample seed sets from the graph.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
n_seeds : int
|
|
270
|
+
Number of seed sets to sample.
|
|
271
|
+
seed_size_range : List[int], optional
|
|
272
|
+
Range of seed sizes to sample from (default is None).
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
List[Set[int]]
|
|
277
|
+
List of sampled seed sets.
|
|
278
|
+
|
|
279
|
+
Raises
|
|
280
|
+
------
|
|
281
|
+
AssertionError
|
|
282
|
+
If values in seed_size_range are out of bounds.
|
|
283
|
+
"""
|
|
284
|
+
if seed_size_range is None:
|
|
285
|
+
seed_size_range = range(1, self.g.count_vertices() + 1)
|
|
286
|
+
else:
|
|
287
|
+
assert max(seed_size_range) <= self.g.count_vertices() and min(seed_size_range) > 0, \
|
|
288
|
+
"Values of `seed_size_range should be between 1 and the number of vertices in the graph"
|
|
289
|
+
|
|
290
|
+
if self.random_state:
|
|
291
|
+
np.random.seed(self.random_state)
|
|
292
|
+
seed_sizes = np.random.choice(seed_size_range, size=n_seeds, replace=True)
|
|
293
|
+
seed_sets = [set(np.random.choice(self.vertices, size=size, replace=False)) for size in seed_sizes]
|
|
294
|
+
return seed_sets
|
|
295
|
+
|
|
296
|
+
def estimate_spread(self, seed_set: Set[int], n_runs: int = 100, with_std: bool = False) -> Union[float, tuple]:
|
|
297
|
+
"""Estimate the spread of influence from a seed set.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
seed_set : Set[int]
|
|
302
|
+
Set of initial active nodes.
|
|
303
|
+
n_runs : int, optional
|
|
304
|
+
Number of simulation runs (default is 100).
|
|
305
|
+
with_std : bool, optional
|
|
306
|
+
Whether to return standard deviation along with the mean (default is False).
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
Union[float, tuple]
|
|
311
|
+
Mean spread, or a tuple of (mean spread, standard deviation) if with_std is True.
|
|
312
|
+
"""
|
|
313
|
+
traces = self.sample_traces_from_seeds([seed_set] * n_runs, out_trace_type=True)
|
|
314
|
+
spread_over_runs = [len(trace.get_all_activated_vertices()) for trace in traces]
|
|
315
|
+
mean_spread, std_spread = np.mean(spread_over_runs), np.std(spread_over_runs)
|
|
316
|
+
return (mean_spread, std_spread) if with_std else mean_spread
|
|
317
|
+
|
|
318
|
+
def find_optimal_seed_greedily(self, seed_size: int, n_runs_per_node: int = 100) -> List[int]:
|
|
319
|
+
"""Find the optimal seed set using a greedy approach.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
seed_size : int
|
|
324
|
+
Desired size of the seed set.
|
|
325
|
+
n_runs_per_node : int, optional
|
|
326
|
+
Number of runs to estimate the spread for each candidate node (default is 100).
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
List[int]
|
|
331
|
+
List of vertices in the optimal seed set.
|
|
332
|
+
"""
|
|
333
|
+
seed_set = []
|
|
334
|
+
unseen_seeds = list(range(len(self.vertices)))
|
|
335
|
+
|
|
336
|
+
for _ in range(seed_size):
|
|
337
|
+
spread_over_seeds = [
|
|
338
|
+
self.estimate_spread(seed_set=set(seed_set + [vertex]), n_runs=n_runs_per_node)
|
|
339
|
+
for vertex in unseen_seeds
|
|
340
|
+
]
|
|
341
|
+
best_node_idx = np.argmax(spread_over_seeds)
|
|
342
|
+
best_node = unseen_seeds[best_node_idx]
|
|
343
|
+
seed_set.append(best_node)
|
|
344
|
+
unseen_seeds.pop(best_node_idx)
|
|
345
|
+
return seed_set
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class LTM(InfluenceModel):
|
|
349
|
+
"""Linear Threshold Model (LTM) for influence spread.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
g : Graph
|
|
354
|
+
The graph on which the LTM operates.
|
|
355
|
+
threshold_generator : rv_frozen, optional
|
|
356
|
+
Distribution for generating thresholds (default is uniform distribution).
|
|
357
|
+
check_init : bool, optional
|
|
358
|
+
Whether to check the parameters upon initialization (default is True).
|
|
359
|
+
random_state : int, optional
|
|
360
|
+
Seed for random number generation (default is None).
|
|
361
|
+
n_jobs : int, optional
|
|
362
|
+
Number of parallel jobs for simulations (default is None).
|
|
363
|
+
|
|
364
|
+
Attributes
|
|
365
|
+
----------
|
|
366
|
+
vertex_2_threshold : Dict[int, float]
|
|
367
|
+
Mapping of vertices to their corresponding thresholds.
|
|
368
|
+
vertex_2_influence_counter : Dict[int, float]
|
|
369
|
+
Mapping of vertices to their current influence counter.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
def __init__(self, g: Graph, threshold_generator=stats.uniform(0, 1),
|
|
373
|
+
check_init: bool = True, random_state: int = None, n_jobs: int = None) -> None:
|
|
374
|
+
self.threshold_generator = threshold_generator
|
|
375
|
+
super().__init__(g, check_init=check_init, random_state=random_state, n_jobs=n_jobs)
|
|
376
|
+
|
|
377
|
+
def _generate_simulation_rvs(self, n_runs: int = 1) -> np.ndarray:
|
|
378
|
+
"""Generate random thresholds for simulation.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
n_runs : int, optional
|
|
383
|
+
Number of simulation runs (default is 1).
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
np.ndarray
|
|
388
|
+
Generated thresholds for each vertex.
|
|
389
|
+
"""
|
|
390
|
+
thresholds = self.threshold_generator.rvs(size=(n_runs, len(self.vertices)),
|
|
391
|
+
random_state=self.random_state)
|
|
392
|
+
return thresholds[0] if n_runs == 1 else thresholds
|
|
393
|
+
|
|
394
|
+
def _init_simulation_rvs(self, simulation_rvs=None) -> None:
|
|
395
|
+
"""Initialize thresholds for vertices.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
simulation_rvs : np.ndarray, optional
|
|
400
|
+
Predefined thresholds for vertices (default is None).
|
|
401
|
+
|
|
402
|
+
Raises
|
|
403
|
+
------
|
|
404
|
+
AssertionError
|
|
405
|
+
If the length of simulation_rvs does not match the number of vertices.
|
|
406
|
+
"""
|
|
407
|
+
if simulation_rvs is None:
|
|
408
|
+
simulation_rvs = self._generate_simulation_rvs()
|
|
409
|
+
else:
|
|
410
|
+
assert len(simulation_rvs) == len(self.vertices), \
|
|
411
|
+
"Length of simulation_rvs should match the number of nodes in the graph for LTM"
|
|
412
|
+
self.vertex_2_threshold = dict(zip(self.vertices, simulation_rvs))
|
|
413
|
+
|
|
414
|
+
def _pre_simulation_init(self, seed_set: Set[int], simulation_rvs=None) -> None:
|
|
415
|
+
"""Prepare for simulation by initializing parameters specific to LTM.
|
|
416
|
+
|
|
417
|
+
Parameters
|
|
418
|
+
----------
|
|
419
|
+
seed_set : Set[int]
|
|
420
|
+
Set of initial active nodes.
|
|
421
|
+
simulation_rvs : np.ndarray, optional
|
|
422
|
+
Predefined thresholds for vertices (default is None).
|
|
423
|
+
"""
|
|
424
|
+
super()._pre_simulation_init(seed_set, simulation_rvs=simulation_rvs)
|
|
425
|
+
self.vertex_2_influence_counter = {vertex: self.vertex_2_threshold[vertex] if vertex in seed_set else 0.0
|
|
426
|
+
for vertex in self.vertices}
|
|
427
|
+
|
|
428
|
+
def _make_edge_influence_attempt(self, v: int, v_adj: int) -> bool:
|
|
429
|
+
"""Attempt to influence an adjacent vertex based on its threshold and received influence.
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
v : int
|
|
434
|
+
The current influencing vertex.
|
|
435
|
+
v_adj : int
|
|
436
|
+
The adjacent vertex to be influenced.
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
bool
|
|
441
|
+
True if the adjacent vertex is influenced, otherwise False.
|
|
442
|
+
"""
|
|
443
|
+
self.vertex_2_influence_counter[v_adj] += self.g.get_edge_weight((v, v_adj))
|
|
444
|
+
return self.vertex_2_threshold[v_adj] <= self.vertex_2_influence_counter[v_adj]
|
|
445
|
+
|
|
446
|
+
def check_param_init_correctness(self, eps: float = 1e-6) -> None:
|
|
447
|
+
"""Validate parameters of the LTM model.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
eps : float, optional
|
|
452
|
+
Tolerance for validating indegree constraints (default is 1e-6).
|
|
453
|
+
|
|
454
|
+
Raises
|
|
455
|
+
------
|
|
456
|
+
AssertionError
|
|
457
|
+
If the parameters are not valid.
|
|
458
|
+
"""
|
|
459
|
+
assert isinstance(self.threshold_generator, rv_frozen), "Only scipy distributions are allowed"
|
|
460
|
+
dist_min, dist_max = self.threshold_generator.support()
|
|
461
|
+
indegrees = np.array([self.g.get_indegree(vertex, weighted=True) for vertex in self.g.get_vertices()])
|
|
462
|
+
assert np.all((indegrees >= dist_min - eps) & (indegrees <= dist_max + eps))
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class GLTM(LTM):
|
|
466
|
+
"""General Linear Threshold Model (GLTM).
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
g : Graph
|
|
471
|
+
The graph on which the GLTM operates.
|
|
472
|
+
threshold_distribs : Dict[int, rv_frozen]
|
|
473
|
+
Mapping of vertices to their corresponding threshold distributions.
|
|
474
|
+
check_init : bool, optional
|
|
475
|
+
Whether to check the parameters upon initialization (default is True).
|
|
476
|
+
random_state : int, optional
|
|
477
|
+
Seed for random number generation (default is None).
|
|
478
|
+
n_jobs : int, optional
|
|
479
|
+
Number of parallel jobs for simulations (default is None).
|
|
480
|
+
|
|
481
|
+
Attributes
|
|
482
|
+
----------
|
|
483
|
+
threshold_distribs : Dict[int, rv_frozen]
|
|
484
|
+
Mapping of vertices to their corresponding threshold distributions.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
def __init__(self, g: Graph, threshold_distribs: Dict[int, rv_frozen],
|
|
488
|
+
check_init: bool = True, random_state: int = None, n_jobs: int = None) -> None:
|
|
489
|
+
self.threshold_distribs = threshold_distribs
|
|
490
|
+
super().__init__(g, check_init=check_init, random_state=random_state, n_jobs=n_jobs)
|
|
491
|
+
|
|
492
|
+
def _generate_simulation_rvs(self, n_runs: int = 1) -> np.ndarray:
|
|
493
|
+
"""Generate random thresholds for each vertex using their specific distributions.
|
|
494
|
+
|
|
495
|
+
Parameters
|
|
496
|
+
----------
|
|
497
|
+
n_runs : int, optional
|
|
498
|
+
Number of simulation runs (default is 1).
|
|
499
|
+
|
|
500
|
+
Returns
|
|
501
|
+
-------
|
|
502
|
+
np.ndarray
|
|
503
|
+
Generated thresholds for each vertex.
|
|
504
|
+
"""
|
|
505
|
+
if self.random_state:
|
|
506
|
+
np.random.seed(self.random_state)
|
|
507
|
+
thresholds = np.random.rand(n_runs, len(self.vertices))
|
|
508
|
+
for idx, vertex in enumerate(self.vertices):
|
|
509
|
+
inv_cdf = self.threshold_distribs[vertex].ppf
|
|
510
|
+
thresholds[:, idx] = inv_cdf(thresholds[:, idx])
|
|
511
|
+
return thresholds[0] if n_runs == 1 else thresholds
|
|
512
|
+
|
|
513
|
+
def check_param_init_correctness(self, eps: float = 1e-6) -> None:
|
|
514
|
+
"""Validate parameters of the GLT model.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
eps : float, optional
|
|
519
|
+
Tolerance for validating indegree constraints (default is 1e-6).
|
|
520
|
+
|
|
521
|
+
Raises
|
|
522
|
+
------
|
|
523
|
+
AssertionError
|
|
524
|
+
If the parameters are not valid.
|
|
525
|
+
"""
|
|
526
|
+
assert isinstance(self.threshold_distribs, dict), \
|
|
527
|
+
"Threshold distributions should be a dict with vertices as keys and rv_frozen objects as values"
|
|
528
|
+
indegrees = self.g.get_indegrees_dict(weighted=True)
|
|
529
|
+
for vertex, threshold_dist in self.threshold_distribs.items():
|
|
530
|
+
assert isinstance(threshold_dist, rv_frozen), "Only scipy distributions are allowed"
|
|
531
|
+
dist_min, dist_max = threshold_dist.support()
|
|
532
|
+
assert (indegrees[vertex] >= dist_min - eps) and (indegrees[vertex] <= dist_max + eps), \
|
|
533
|
+
f"Indegree of vertex {vertex} is out of the distribution support range."
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class ICM(InfluenceModel):
|
|
537
|
+
"""Independent Cascade Model (ICM) for influence spread.
|
|
538
|
+
|
|
539
|
+
Parameters
|
|
540
|
+
----------
|
|
541
|
+
g : Graph
|
|
542
|
+
The graph on which the ICM operates.
|
|
543
|
+
check_init : bool, optional
|
|
544
|
+
Whether to check the parameters upon initialization (default is True).
|
|
545
|
+
random_state : int, optional
|
|
546
|
+
Seed for random number generation (default is None).
|
|
547
|
+
n_jobs : int, optional
|
|
548
|
+
Number of parallel jobs for simulations (default is None).
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
def check_param_init_correctness(self) -> None:
|
|
552
|
+
"""Check if the graph edge weights are valid for the ICM.
|
|
553
|
+
|
|
554
|
+
Raises
|
|
555
|
+
------
|
|
556
|
+
AssertionError
|
|
557
|
+
If any edge weight is not in the range [0, 1].
|
|
558
|
+
"""
|
|
559
|
+
assert np.all((0 <= self.g.weights) & (self.g.weights <= 1))
|
|
560
|
+
|
|
561
|
+
def _make_edge_influence_attempt(self, v: int, v_adj: int) -> bool:
|
|
562
|
+
"""Attempt to influence an adjacent vertex based on edge activation.
|
|
563
|
+
|
|
564
|
+
Parameters
|
|
565
|
+
----------
|
|
566
|
+
v : int
|
|
567
|
+
The current influencing vertex.
|
|
568
|
+
v_adj : int
|
|
569
|
+
The adjacent vertex to be influenced.
|
|
570
|
+
|
|
571
|
+
Returns
|
|
572
|
+
-------
|
|
573
|
+
bool
|
|
574
|
+
True if the adjacent vertex is influenced, otherwise False.
|
|
575
|
+
"""
|
|
576
|
+
return self.edge_activations[self.g.get_edge_index((v, v_adj))]
|
|
577
|
+
|
|
578
|
+
def _generate_simulation_rvs(self, n_runs: int = 1) -> List:
|
|
579
|
+
"""Generate random activations for edges.
|
|
580
|
+
|
|
581
|
+
Parameters
|
|
582
|
+
----------
|
|
583
|
+
n_runs : int, optional
|
|
584
|
+
Number of simulation runs (default is 1).
|
|
585
|
+
|
|
586
|
+
Returns
|
|
587
|
+
-------
|
|
588
|
+
List[np.ndarray]
|
|
589
|
+
Random activation results for each edge.
|
|
590
|
+
"""
|
|
591
|
+
if self.random_state:
|
|
592
|
+
np.random.seed(self.random_state)
|
|
593
|
+
edge_activations = np.random.rand(n_runs, self.g.count_edges()) <= self.g.weights
|
|
594
|
+
return edge_activations[0] if n_runs == 1 else edge_activations
|
|
595
|
+
|
|
596
|
+
def _init_simulation_rvs(self, simulation_rvs=None) -> None:
|
|
597
|
+
"""Initialize edge activations for the ICM.
|
|
598
|
+
|
|
599
|
+
Parameters
|
|
600
|
+
----------
|
|
601
|
+
simulation_rvs : np.ndarray, optional
|
|
602
|
+
Predefined edge activations (default is None).
|
|
603
|
+
|
|
604
|
+
Raises
|
|
605
|
+
------
|
|
606
|
+
AssertionError
|
|
607
|
+
If the length of simulation_rvs does not match the number of edges.
|
|
608
|
+
"""
|
|
609
|
+
if simulation_rvs is None:
|
|
610
|
+
simulation_rvs = self._generate_simulation_rvs()
|
|
611
|
+
else:
|
|
612
|
+
assert len(simulation_rvs) == self.g.count_edges(), \
|
|
613
|
+
"Length of edge_activations must match the number of edges in the graph"
|
|
614
|
+
self.edge_activations = simulation_rvs
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Iterable, Set, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def multiple_union(set_list: Iterable[Set]):
|
|
6
|
+
final_set = set()
|
|
7
|
+
for cur_set in set_list:
|
|
8
|
+
final_set = final_set.union(cur_set)
|
|
9
|
+
return final_set
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def invert_non_zeros(array):
|
|
13
|
+
out = np.array(array, dtype=float)
|
|
14
|
+
non_zero_mask = array != 0
|
|
15
|
+
out[non_zero_mask] = 1. / out[non_zero_mask]
|
|
16
|
+
return out
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def random_vector_inside_simplex(dim, ub=1):
|
|
20
|
+
U = np.random.uniform(low=0, high=ub, size=dim)
|
|
21
|
+
U_sorted = np.sort(U)
|
|
22
|
+
U_sorted = np.concatenate(([0], U_sorted))
|
|
23
|
+
x = np.diff(U_sorted)
|
|
24
|
+
return x
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def random_vector_on_simplex(dim, ub=1):
|
|
28
|
+
X = random_vector_inside_simplex(dim=dim, ub=1)
|
|
29
|
+
return X / np.sum(X) * ub
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: InfluenceDiffusion
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: InfluenceDiffusion package
|
|
5
|
+
Author: Alexander Kagan
|
|
6
|
+
Author-email: <amkagan@umich.edu>
|
|
7
|
+
Keywords: python,Influence Maximization,Network diffusion models,General Linear Threshold model,Social Networks,Independent Cascade model
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Programming Language :: Python :: 2
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: networkx
|
|
17
|
+
Requires-Dist: typing
|
|
18
|
+
Requires-Dist: joblib
|
|
19
|
+
Requires-Dist: os
|
|
20
|
+
|
|
21
|
+
in this package, we implement popular network diffusion models and methods for their estimation.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
InfluenceDiffusion/Graph.py,sha256=KivMsqW8Zl1Yp6I-Z32JTfOsuf3vPgqysTt0DCnmWww,15159
|
|
2
|
+
InfluenceDiffusion/Trace.py,sha256=LoOimi8tB9eItHI0VaTQk6qnc-BixzWWHdrjaHJHo4o,15272
|
|
3
|
+
InfluenceDiffusion/__init__.py,sha256=uSesPBwxMshakiwcev7rtwWUoEdVVV62NcPzOEeIzQU,91
|
|
4
|
+
InfluenceDiffusion/influence_models.py,sha256=-AUiJ3VSY8-Ra69lswfKAzMI2Lhoxa9GmQS6_RmldcE,22472
|
|
5
|
+
InfluenceDiffusion/utils.py,sha256=KysTdA99OGqZbcMQXpNxFi-rBfkjLdUSwtE56FV19TI,723
|
|
6
|
+
InfluenceDiffusion/estimation_models/BaseWeightEstimator.py,sha256=WpikOb5q74QWe2LyRWHHK5Bhp3sEx_TGBw-xWXgDWMI,18140
|
|
7
|
+
InfluenceDiffusion/estimation_models/EMEstimation.py,sha256=OEUcqzOxmMyqKp0IIsC0CXcGeTbKrkYxuZwo0R9qRt8,9871
|
|
8
|
+
InfluenceDiffusion/estimation_models/OptimEstimation.py,sha256=LvyKOPlB5GvfUyM7s8xghhwo7cSQ-dCVxlL5UImX0wI,10707
|
|
9
|
+
InfluenceDiffusion/estimation_models/__init__.py,sha256=Ow7q6gMnlRMp7jAaI3d-GMRukz7RWVmL-lSaVs1DpV8,91
|
|
10
|
+
InfluenceDiffusion-0.0.3.dist-info/METADATA,sha256=JTXgMGDsHrKT8T2Sph2iMxJRJF5MHKx0FXp6hqRbH9U,805
|
|
11
|
+
InfluenceDiffusion-0.0.3.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
12
|
+
InfluenceDiffusion-0.0.3.dist-info/top_level.txt,sha256=Fad_YlZYnVYGG5UerSx2REIoxBrEjVKJKleHguMf8rw,19
|
|
13
|
+
InfluenceDiffusion-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
InfluenceDiffusion
|