InfluenceDiffusion 0.0.12__tar.gz → 0.0.14__tar.gz
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-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/Graph.py +29 -104
- influencediffusion-0.0.14/InfluenceDiffusion/Inference.py +114 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/Trace.py +4 -4
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/__init__.py +2 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/BaseWeightEstimator.py +9 -10
- influencediffusion-0.0.14/InfluenceDiffusion/estimation_models/CDFEstimation.py +73 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/OptimEstimation.py +89 -13
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/influence_models.py +6 -4
- influencediffusion-0.0.14/InfluenceDiffusion/plot_utils.py +91 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/utils.py +15 -2
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/PKG-INFO +39 -23
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/SOURCES.txt +3 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/requires.txt +1 -1
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/PKG-INFO +39 -23
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/README.md +27 -20
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/setup.py +2 -2
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/EMEstimation.py +0 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/__init__.py +0 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/weight_samplers.py +0 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/dependency_links.txt +0 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/top_level.txt +0 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/LICENSE +0 -0
- {influencediffusion-0.0.12 → influencediffusion-0.0.14}/setup.cfg +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
1
|
import numpy as np
|
|
3
2
|
from typing import List, Tuple, Union, Dict
|
|
4
3
|
import networkx as nx
|
|
@@ -7,67 +6,41 @@ __all__ = ["Graph"]
|
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class Graph(nx.DiGraph):
|
|
10
|
-
def __init__(self,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Parameters
|
|
17
|
-
----------
|
|
18
|
-
edge_list : List[Tuple[int, int]]
|
|
19
|
-
List of tuples (source, sink) representing edges of the graph.
|
|
20
|
-
directed : bool, optional
|
|
21
|
-
Flag to indicate whether the graph is directed (default is True).
|
|
22
|
-
weights : Union[None, List[float]], optional
|
|
23
|
-
Optional weights for each edge. If None, all edges are assigned a weight of 1.
|
|
24
|
-
"""
|
|
25
|
-
self.directed = directed
|
|
26
|
-
self.edge_array = np.array(edge_list, dtype=int)
|
|
27
|
-
|
|
28
|
-
if not self.directed:
|
|
29
|
-
reverse_edge_array = self.edge_array[:, [1, 0]]
|
|
30
|
-
self.edge_array = np.concatenate([self.edge_array, reverse_edge_array])
|
|
31
|
-
|
|
32
|
-
super().__init__(self.edge_array.tolist())
|
|
33
|
-
|
|
34
|
-
if weights is not None:
|
|
35
|
-
assert len(weights) == len(edge_list), "number of edges is different from number of weights"
|
|
9
|
+
def __init__(self, incoming_graph_data=None, weights: Union[List, np.array] = None, **attr):
|
|
10
|
+
super().__init__(incoming_graph_data, **attr)
|
|
11
|
+
self.edge_array = np.array(self.edges)
|
|
12
|
+
if weights is None:
|
|
13
|
+
self.weights = np.ones(len(self.edges))
|
|
36
14
|
else:
|
|
37
|
-
weights
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
assert len(weights) == self.count_edges(), "number of weights different from the number of edges"
|
|
16
|
+
self.weights = np.array(weights)
|
|
17
|
+
nx.set_edge_attributes(self, dict(zip(self.edges, self.weights)), "weight")
|
|
40
18
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
19
|
+
def add_edge(self, u_of_edge, v_of_edge, **attr):
|
|
20
|
+
super().add_edge(u_of_edge, v_of_edge, **attr)
|
|
21
|
+
self.edge_array = np.array(self.edges)
|
|
22
|
+
self.weights = self._get_edge_weight_attributes()
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
If None, re-indexes vertices to [0, 1, ..., |V|-1].
|
|
24
|
+
def add_edges_from(self, ebunch_to_add, **attr):
|
|
25
|
+
super().add_edges_from(ebunch_to_add, **attr)
|
|
26
|
+
self.edge_array = np.array(self.edges)
|
|
27
|
+
self.weights = self._get_edge_weight_attributes()
|
|
50
28
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"""
|
|
56
|
-
if old_2_new_vertex_name_dict is None:
|
|
57
|
-
old_vertex_names = sorted(list(self.get_vertices()))
|
|
58
|
-
old_2_new_vertex_name_dict = {name: idx for idx, name in enumerate(old_vertex_names)}
|
|
29
|
+
def add_weighted_edges_from(self, ebunch_to_add, weight='weight', **attr):
|
|
30
|
+
super().add_weighted_edges_from(ebunch_to_add, weight=weight, **attr)
|
|
31
|
+
self.edge_array = np.array(self.edges)
|
|
32
|
+
self.weights = self._get_edge_weight_attributes()
|
|
59
33
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return self
|
|
34
|
+
def _get_edge_weight_attributes(self):
|
|
35
|
+
return np.array([edge[2] for edge in self.edges.data("weight", default=1)])
|
|
63
36
|
|
|
64
|
-
def set_weights(self, weights: Union[
|
|
37
|
+
def set_weights(self, weights: Union[List, np.array]) -> "Graph":
|
|
65
38
|
"""
|
|
66
39
|
Set weights for the edges in the graph.
|
|
67
40
|
|
|
68
41
|
Parameters
|
|
69
42
|
----------
|
|
70
|
-
weights : Union[
|
|
43
|
+
weights : Union[list, np.array]
|
|
71
44
|
Array of weights for the edges.
|
|
72
45
|
|
|
73
46
|
Returns
|
|
@@ -81,7 +54,8 @@ class Graph(nx.DiGraph):
|
|
|
81
54
|
If the number of weights does not match the number of edges.
|
|
82
55
|
"""
|
|
83
56
|
assert len(weights) == self.count_edges(), "number of weights different from the number of edges"
|
|
84
|
-
self.
|
|
57
|
+
nx.set_edge_attributes(self, dict(zip(self.edges, weights)), 'weight')
|
|
58
|
+
self.weights = self._get_edge_weight_attributes()
|
|
85
59
|
return self
|
|
86
60
|
|
|
87
61
|
def get_edges(self, as_array: bool = False) -> Union[np.array, List[Tuple[int, int]]]:
|
|
@@ -100,7 +74,7 @@ class Graph(nx.DiGraph):
|
|
|
100
74
|
"""
|
|
101
75
|
if as_array:
|
|
102
76
|
return self.edge_array
|
|
103
|
-
return
|
|
77
|
+
return list(self.edges)
|
|
104
78
|
|
|
105
79
|
def get_vertices(self) -> set:
|
|
106
80
|
"""
|
|
@@ -144,7 +118,7 @@ class Graph(nx.DiGraph):
|
|
|
144
118
|
int
|
|
145
119
|
The total number of edges.
|
|
146
120
|
"""
|
|
147
|
-
return len(self.
|
|
121
|
+
return len(self.edges)
|
|
148
122
|
|
|
149
123
|
def count_vertices(self) -> int:
|
|
150
124
|
"""
|
|
@@ -155,7 +129,7 @@ class Graph(nx.DiGraph):
|
|
|
155
129
|
int
|
|
156
130
|
The total number of vertices.
|
|
157
131
|
"""
|
|
158
|
-
return len(self.
|
|
132
|
+
return len(self.nodes)
|
|
159
133
|
|
|
160
134
|
def get_children_mask(self, vertex: int) -> np.array:
|
|
161
135
|
"""
|
|
@@ -392,38 +366,6 @@ class Graph(nx.DiGraph):
|
|
|
392
366
|
"""
|
|
393
367
|
return np.mean(self.get_all_indegrees(weighted))
|
|
394
368
|
|
|
395
|
-
def get_edge_index(self, edge: Tuple[int, int]) -> int:
|
|
396
|
-
"""
|
|
397
|
-
Get the index of a specified edge.
|
|
398
|
-
|
|
399
|
-
Parameters
|
|
400
|
-
----------
|
|
401
|
-
edge : Tuple[int, int]
|
|
402
|
-
The edge for which to find the index.
|
|
403
|
-
|
|
404
|
-
Returns
|
|
405
|
-
-------
|
|
406
|
-
int
|
|
407
|
-
The index of the edge in the graph.
|
|
408
|
-
"""
|
|
409
|
-
return self.edge_2_index[tuple(edge)]
|
|
410
|
-
|
|
411
|
-
def get_edge_weight(self, edge: Tuple[int, int]) -> float:
|
|
412
|
-
"""
|
|
413
|
-
Get the weight of a specified edge.
|
|
414
|
-
|
|
415
|
-
Parameters
|
|
416
|
-
----------
|
|
417
|
-
edge : Tuple[int, int]
|
|
418
|
-
The edge for which to retrieve the weight.
|
|
419
|
-
|
|
420
|
-
Returns
|
|
421
|
-
-------
|
|
422
|
-
float
|
|
423
|
-
The weight of the edge.
|
|
424
|
-
"""
|
|
425
|
-
return self.weights[self.get_edge_index(edge)]
|
|
426
|
-
|
|
427
369
|
def get_edges_mask_from_set_to_vertex(self, vertex_set: set, vertex: int) -> np.array:
|
|
428
370
|
"""
|
|
429
371
|
Create a mask for edges leading from a set of vertices to a specified vertex.
|
|
@@ -445,23 +387,6 @@ class Graph(nx.DiGraph):
|
|
|
445
387
|
parent_edges_mask[parent_edges_mask] = [(parent in vertex_set) for parent in parent_vertices]
|
|
446
388
|
return parent_edges_mask
|
|
447
389
|
|
|
448
|
-
def get_adj_matrix(self) -> np.array:
|
|
449
|
-
"""
|
|
450
|
-
Generate the adjacency matrix of the graph.
|
|
451
|
-
|
|
452
|
-
Returns
|
|
453
|
-
-------
|
|
454
|
-
np.array
|
|
455
|
-
The adjacency matrix as a numpy array.
|
|
456
|
-
"""
|
|
457
|
-
vertices = list(self.get_vertices())
|
|
458
|
-
n_vertex = len(vertices)
|
|
459
|
-
adj_matrix = pd.DataFrame(np.zeros((n_vertex, n_vertex)), columns=vertices, index=vertices)
|
|
460
|
-
for (v1, v2), weight in zip(self.edge_array, self.weights):
|
|
461
|
-
adj_matrix.at[v1, v2] = weight
|
|
462
|
-
|
|
463
|
-
return adj_matrix
|
|
464
|
-
|
|
465
390
|
def is_edge_in_graph(self, edge: Tuple[int, int]) -> bool:
|
|
466
391
|
"""
|
|
467
392
|
Check if a specified edge exists in the graph.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
import jax.numpy as jnp
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Dict, Callable
|
|
5
|
+
from functools import partial
|
|
6
|
+
from scipy.stats import norm
|
|
7
|
+
|
|
8
|
+
from .estimation_models.OptimEstimation import GLTWeightEstimator
|
|
9
|
+
from .utils import make_jax_cdf
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GLTInferenceModule:
|
|
13
|
+
def __init__(self, estimator: GLTWeightEstimator, vertex_2_jax_cdf: Dict[int, Callable] = None):
|
|
14
|
+
self.estimator = estimator
|
|
15
|
+
if vertex_2_jax_cdf is None:
|
|
16
|
+
self.vertex_2_jax_cdf = {vertex: make_jax_cdf(distrib)
|
|
17
|
+
for vertex, distrib in self.estimator.vertex_2_distrib.items()}
|
|
18
|
+
else:
|
|
19
|
+
self.vertex_2_jax_cdf = vertex_2_jax_cdf
|
|
20
|
+
self.vertex_2_parent_weight_cov = {}
|
|
21
|
+
|
|
22
|
+
def _get_vertex_activ_status_and_masks(self, vertex: int):
|
|
23
|
+
n_parents = self.estimator.graph.get_indegree(vertex)
|
|
24
|
+
if vertex in self.estimator._vertex_2_active_parent_mask_t:
|
|
25
|
+
t_active_masks = self.estimator._vertex_2_active_parent_mask_t[vertex]
|
|
26
|
+
tm1_active_masks = self.estimator._vertex_2_active_parent_mask_tm1[vertex]
|
|
27
|
+
else:
|
|
28
|
+
t_active_masks = jnp.empty(shape=(0, n_parents))
|
|
29
|
+
tm1_active_masks = jnp.empty(shape=(0, n_parents))
|
|
30
|
+
|
|
31
|
+
if vertex in self.estimator._failed_vertices_masks:
|
|
32
|
+
t_failed_masks = self.estimator._failed_vertices_masks[vertex]
|
|
33
|
+
tm1_failed_masks = jnp.zeros_like(t_failed_masks)
|
|
34
|
+
else:
|
|
35
|
+
t_failed_masks = jnp.empty(shape=(0, n_parents))
|
|
36
|
+
tm1_failed_masks = jnp.empty(shape=(0, n_parents))
|
|
37
|
+
ys = jnp.array([True] * len(t_active_masks) + [False] * len(t_failed_masks), dtype=bool)
|
|
38
|
+
masks_t = jnp.vstack([t_active_masks, t_failed_masks], dtype=jnp.float32)
|
|
39
|
+
masks_tm1 = jnp.vstack([tm1_active_masks, tm1_failed_masks], dtype=jnp.float32)
|
|
40
|
+
return ys, masks_t, masks_tm1
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _vertex_ll(parent_weights, cdf, ys, masks_t, masks_tm1, eps=1e-6):
|
|
44
|
+
F_t = cdf(masks_t @ parent_weights)
|
|
45
|
+
F_tm1 = cdf(masks_tm1 @ parent_weights)
|
|
46
|
+
F_tm1 = jnp.where(F_tm1 < eps, 0, F_tm1)
|
|
47
|
+
F_t = jnp.where(F_t > 1. - eps, 1., F_t)
|
|
48
|
+
prob = jnp.clip(F_t - F_tm1, eps, 1. - eps)
|
|
49
|
+
return jnp.sum(jnp.log(jnp.where(ys, prob, 1. - prob)))
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _vertex_conditional_activ_prob(weights, cdf, mask_t, mask_tm1, eps=1e-6):
|
|
53
|
+
F_t = cdf(mask_t @ weights)
|
|
54
|
+
F_tm1 = cdf(mask_tm1 @ weights)
|
|
55
|
+
F_t = jnp.clip(F_t, eps, 1. - eps)
|
|
56
|
+
F_tm1 = jnp.clip(F_tm1, eps, 1. - eps)
|
|
57
|
+
return 1. - (1. - F_t) / (1. - F_tm1)
|
|
58
|
+
|
|
59
|
+
def compute_vertex_conditional_activ_probs(self, vertex: int, masks_t, masks_tm1):
|
|
60
|
+
cdf = self.vertex_2_jax_cdf[vertex]
|
|
61
|
+
parent_mask = self.estimator.graph.get_parents_mask(vertex)
|
|
62
|
+
weights = jnp.array(self.estimator.weights_[parent_mask], dtype=jnp.float32)
|
|
63
|
+
return self._vertex_conditional_activ_prob(weights=weights, cdf=cdf, mask_t=masks_t, mask_tm1=masks_tm1)
|
|
64
|
+
|
|
65
|
+
def compute_vertex_parent_weight_cov(self, vertex, fisher_eps=1e-6):
|
|
66
|
+
if vertex in self.vertex_2_parent_weight_cov:
|
|
67
|
+
return self.vertex_2_parent_weight_cov[vertex]
|
|
68
|
+
cdf = self.vertex_2_jax_cdf[vertex]
|
|
69
|
+
parent_mask = self.estimator.graph.get_parents_mask(vertex)
|
|
70
|
+
weights = jnp.array(self.estimator.weights_[parent_mask], dtype=jnp.float32)
|
|
71
|
+
ys, masks_t, masks_tm1 = self._get_vertex_activ_status_and_masks(vertex=vertex)
|
|
72
|
+
hessian_fun = jax.hessian(partial(self._vertex_ll, cdf=cdf, ys=ys,
|
|
73
|
+
masks_t=masks_t, masks_tm1=masks_tm1))
|
|
74
|
+
fisher_info = -hessian_fun(weights)
|
|
75
|
+
cov = jnp.linalg.inv(fisher_info + fisher_eps * jnp.eye(len(fisher_info)))
|
|
76
|
+
self.vertex_2_parent_weight_cov[vertex] = cov
|
|
77
|
+
return cov
|
|
78
|
+
|
|
79
|
+
def compute_vertex_2_parent_weight_cov_dict(self):
|
|
80
|
+
return {vertex: self.compute_vertex_parent_weight_cov(vertex)
|
|
81
|
+
for vertex in self.estimator.informative_vertices}
|
|
82
|
+
|
|
83
|
+
def compute_parent_weight_conf_ints(self, vertex: int, alpha: float = 0.05):
|
|
84
|
+
weights = self.estimator.weights_[self.estimator.graph.get_parents_mask(vertex)]
|
|
85
|
+
lb, ub = self.estimator.vertex_2_distrib[vertex].support()
|
|
86
|
+
cov = self.compute_vertex_parent_weight_cov(vertex=vertex)
|
|
87
|
+
stds = jnp.sqrt(jnp.diag(cov))
|
|
88
|
+
quantile = norm.ppf(1 - alpha / 2)
|
|
89
|
+
return jnp.stack([jnp.clip(weights - quantile * stds, lb, None),
|
|
90
|
+
jnp.clip(weights + quantile * stds, None, ub)]).T
|
|
91
|
+
|
|
92
|
+
def compute_all_weight_conf_ints(self, alpha=0.05):
|
|
93
|
+
conf_ints = np.empty(shape=(self.estimator.graph.count_edges(), 2), dtype=np.float32)
|
|
94
|
+
for vertex in self.estimator.informative_vertices:
|
|
95
|
+
parent_conf_ints = self.compute_parent_weight_conf_ints(vertex=vertex, alpha=alpha)
|
|
96
|
+
mask = self.estimator.graph.get_parents_mask(vertex)
|
|
97
|
+
conf_ints[mask] = parent_conf_ints
|
|
98
|
+
return conf_ints
|
|
99
|
+
|
|
100
|
+
def compute_vertex_activation_prob_conf_ints(self, vertex: int, masks_t, masks_tm1, alpha=0.05):
|
|
101
|
+
cdf = self.vertex_2_jax_cdf[vertex]
|
|
102
|
+
weights = self.estimator.weights_[self.estimator.graph.get_parents_mask(vertex)]
|
|
103
|
+
cov = self.compute_vertex_parent_weight_cov(vertex=vertex)
|
|
104
|
+
quantile = norm.ppf(1 - alpha / 2)
|
|
105
|
+
conf_ints = []
|
|
106
|
+
for mask_t, mask_tm1 in zip(masks_t, masks_tm1):
|
|
107
|
+
prob, prob_grad = jax.value_and_grad(partial(self._vertex_conditional_activ_prob,
|
|
108
|
+
cdf=cdf, mask_t=mask_t, mask_tm1=mask_tm1))(weights)
|
|
109
|
+
prob_grad = prob_grad.reshape((-1, 1))
|
|
110
|
+
std = jnp.sqrt(prob_grad.T @ cov @ prob_grad)
|
|
111
|
+
conf_ints.append([jnp.clip(prob - quantile * std, 0., None).item(),
|
|
112
|
+
jnp.clip(prob + quantile * std, None, 1.).item()])
|
|
113
|
+
|
|
114
|
+
return np.stack(conf_ints)
|
|
@@ -78,10 +78,10 @@ class Trace(tuple):
|
|
|
78
78
|
if check_feasibility:
|
|
79
79
|
self.make_feasibility_check()
|
|
80
80
|
|
|
81
|
-
self.__cumulative_trace
|
|
82
|
-
self.__failed_vertices
|
|
83
|
-
self.__activated_vertices
|
|
84
|
-
self.__activation_time_dict
|
|
81
|
+
self.__cumulative_trace = None
|
|
82
|
+
self.__failed_vertices = None
|
|
83
|
+
self.__activated_vertices = None
|
|
84
|
+
self.__activation_time_dict = None
|
|
85
85
|
|
|
86
86
|
def make_feasibility_check(self) -> None:
|
|
87
87
|
"""Check if each vertex has at least one active parent at each time step."""
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from .Graph import Graph
|
|
2
2
|
from .influence_models import InfluenceModel, ICM, LTM, GLTM
|
|
3
3
|
from .Trace import Trace, Traces, PseudoTraces
|
|
4
|
+
from .Inference import GLTInferenceModule
|
|
4
5
|
from .utils import invert_non_zeros, multiple_union, random_vector_inside_simplex, random_vector_on_simplex
|
|
6
|
+
from plot_utils import plot_with_conf_intervals, plot_hist_with_normal_fit
|
|
5
7
|
from .weight_samplers import make_weighted_cascade_weights, make_random_weights_with_fixed_indeg, \
|
|
6
8
|
make_random_weights_with_indeg_constraint
|
|
@@ -2,14 +2,13 @@ from typing import List, Union, Dict
|
|
|
2
2
|
import os
|
|
3
3
|
import pickle
|
|
4
4
|
import numpy as np
|
|
5
|
-
from tqdm import tqdm
|
|
6
5
|
from joblib import Parallel, delayed
|
|
7
6
|
from scipy.stats import uniform
|
|
8
7
|
from scipy.stats._distn_infrastructure import rv_frozen
|
|
9
8
|
|
|
10
9
|
from ..Trace import Traces, PseudoTraces
|
|
11
10
|
from ..Graph import Graph
|
|
12
|
-
from ..utils import multiple_union
|
|
11
|
+
from ..utils import multiple_union, random_vector_inside_simplex
|
|
13
12
|
|
|
14
13
|
__all__ = ["BaseWeightEstimator", "BaseGLTWEightEstimator", "BaseICWEightEstimator"]
|
|
15
14
|
|
|
@@ -144,13 +143,13 @@ class BaseWeightEstimator:
|
|
|
144
143
|
vertex_2_active_parent_mask_t = {vertex: [] for vertex in self.informative_vertices}
|
|
145
144
|
failed_vertices_masks = {vertex: [] for vertex in self.informative_vertices}
|
|
146
145
|
|
|
147
|
-
for vertex, vertex_pseudo_traces in
|
|
146
|
+
for vertex, vertex_pseudo_traces in traces.items():
|
|
148
147
|
for vertices_tm1, new_vertices_t in vertex_pseudo_traces:
|
|
149
148
|
parents = self.graph.get_parents(vertex, out_type=np.array)
|
|
150
|
-
active_parents_mask_tm1 = np.
|
|
149
|
+
active_parents_mask_tm1 = np.isin(parents, np.array(list(vertices_tm1)))
|
|
151
150
|
if new_vertices_t:
|
|
152
151
|
vertices_t = vertices_tm1 | new_vertices_t
|
|
153
|
-
active_parents_mask_t = np.
|
|
152
|
+
active_parents_mask_t = np.isin(parents, np.array(list(vertices_t)))
|
|
154
153
|
vertex_2_active_parent_mask_tm1[vertex].append(active_parents_mask_tm1)
|
|
155
154
|
vertex_2_active_parent_mask_t[vertex].append(active_parents_mask_t)
|
|
156
155
|
else:
|
|
@@ -362,10 +361,12 @@ class BaseGLTWEightEstimator(BaseWeightEstimator):
|
|
|
362
361
|
"""
|
|
363
362
|
|
|
364
363
|
def __init__(self, graph: Graph, n_jobs: int = None,
|
|
365
|
-
vertex_2_distrib: Dict[int, rv_frozen] = None):
|
|
364
|
+
vertex_2_distrib: Union[Dict[int, rv_frozen], rv_frozen] = None):
|
|
366
365
|
super().__init__(graph, n_jobs)
|
|
367
366
|
if vertex_2_distrib is None:
|
|
368
367
|
vertex_2_distrib = {v: uniform(0, 1) for v in graph.get_vertices()}
|
|
368
|
+
elif isinstance(vertex_2_distrib, rv_frozen):
|
|
369
|
+
vertex_2_distrib = {v: vertex_2_distrib for v in graph.get_vertices()}
|
|
369
370
|
self.vertex_2_distrib = vertex_2_distrib
|
|
370
371
|
|
|
371
372
|
def _generate_random_weights(self) -> np.ndarray:
|
|
@@ -381,10 +382,8 @@ class BaseGLTWEightEstimator(BaseWeightEstimator):
|
|
|
381
382
|
parent_mask = self.graph.get_parents_mask(vertex)
|
|
382
383
|
num_parents = self.graph.get_indegree(vertex, weighted=False)
|
|
383
384
|
support_ub = self.vertex_2_distrib[vertex].support()[1]
|
|
384
|
-
if support_ub == np.inf
|
|
385
|
-
|
|
386
|
-
else:
|
|
387
|
-
weights[parent_mask] = np.random.rand(num_parents) * support_ub / num_parents
|
|
385
|
+
support_ub = 1 if support_ub == np.inf else support_ub
|
|
386
|
+
weights[parent_mask] = random_vector_inside_simplex(num_parents, ub=support_ub)
|
|
388
387
|
return weights
|
|
389
388
|
|
|
390
389
|
def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray], eps: float = 1e-6) -> None:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import List, Tuple, Any
|
|
2
|
+
from scipy.stats._distn_infrastructure import rv_continuous
|
|
3
|
+
from scipy.interpolate import interp1d
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CensoredCDFEstimator(rv_continuous):
|
|
8
|
+
def __init__(self, support: Tuple[float, float] = (-np.inf, np.inf),
|
|
9
|
+
momtype=1,
|
|
10
|
+
a=None,
|
|
11
|
+
b=None,
|
|
12
|
+
xtol=1e-14,
|
|
13
|
+
badvalue=None,
|
|
14
|
+
name=None,
|
|
15
|
+
longname=None,
|
|
16
|
+
shapes=None,
|
|
17
|
+
extradoc=None,
|
|
18
|
+
seed=None):
|
|
19
|
+
|
|
20
|
+
super().__init__(momtype=momtype, a=a, b=b, xtol=xtol, badvalue=badvalue,
|
|
21
|
+
name=name, longname=longname, shapes=shapes, seed=seed)
|
|
22
|
+
self.support_ = support
|
|
23
|
+
|
|
24
|
+
def fit(self, intervals: List[Tuple[float, float]],
|
|
25
|
+
max_iter=50, tol=1e-4, verbose=False, verbose_interval=1):
|
|
26
|
+
|
|
27
|
+
self.qp_ints_ = self._extract_disjoint_intervals(intervals)
|
|
28
|
+
|
|
29
|
+
alphas = np.fromfunction(
|
|
30
|
+
np.vectorize(lambda i, j: self._if_sub_interval(intervals[i], self.qp_ints_[j])),
|
|
31
|
+
shape=(len(intervals), len(self.qp_ints_)), dtype=int)
|
|
32
|
+
|
|
33
|
+
self.qp_probs_ = np.ones(len(self.qp_ints_)) / len(self.qp_ints_)
|
|
34
|
+
|
|
35
|
+
for iteration in range(max_iter):
|
|
36
|
+
cur_probs = self.qp_probs_.copy()
|
|
37
|
+
ms = (alphas * cur_probs) / (alphas @ cur_probs).reshape(-1, 1)
|
|
38
|
+
self.qp_probs_ = ms.sum(0) / ms.sum()
|
|
39
|
+
diff = np.linalg.norm(cur_probs - self.qp_probs_)
|
|
40
|
+
if verbose and iteration % verbose_interval == 0:
|
|
41
|
+
print(f"Iteration: {iteration}, Probs diff l2-norm: {round(diff, int(2 - np.log10(tol)))}")
|
|
42
|
+
if diff < tol:
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if len(self.qp_ints_[:, 0]) >= 2:
|
|
46
|
+
self._cdf_interpolator = interp1d(self.qp_ints_[:, 0], np.cumsum(self.qp_probs_),
|
|
47
|
+
bounds_error=False, fill_value=(0.0, 1.0))
|
|
48
|
+
else:
|
|
49
|
+
self._cdf_interpolator = lambda x: np.clip(x, 0, 1)
|
|
50
|
+
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def _cdf(self, x: Any, *args, **kwargs):
|
|
54
|
+
return self._cdf_interpolator(x)
|
|
55
|
+
|
|
56
|
+
def support(self):
|
|
57
|
+
return self.support_
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _if_sub_interval(interval1: Tuple[float, float], interval2: Tuple[float, float]):
|
|
61
|
+
return (interval1[0] <= interval2[0]) and (interval1[1] >= interval2[1])
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _extract_disjoint_intervals(intervals: List[Tuple[float, float]]):
|
|
65
|
+
assert len(intervals) > 0, "At least one interval should be provided"
|
|
66
|
+
assert all(interval[0] <= interval[1] for interval in intervals)
|
|
67
|
+
lefts, rights = zip(*intervals)
|
|
68
|
+
sort_endpoints = sorted([(left, "L") for left in lefts] + [(right, "R") for right in rights])
|
|
69
|
+
disjoint_intervals = []
|
|
70
|
+
for (ep, next_ep) in zip(sort_endpoints[:-1], sort_endpoints[1:]):
|
|
71
|
+
if ep[1] == "L" and next_ep[1] == "R":
|
|
72
|
+
disjoint_intervals.append((ep[0], next_ep[0]))
|
|
73
|
+
return np.array(disjoint_intervals)
|
|
@@ -2,22 +2,17 @@ import numpy as np
|
|
|
2
2
|
from typing import List, Union, Dict, Tuple
|
|
3
3
|
from scipy.optimize import LinearConstraint, minimize
|
|
4
4
|
from scipy.stats._distn_infrastructure import rv_frozen
|
|
5
|
+
from copy import copy
|
|
5
6
|
|
|
6
7
|
from ..Graph import Graph
|
|
7
8
|
from ..Trace import Traces, PseudoTraces
|
|
8
9
|
from .BaseWeightEstimator import BaseGLTWEightEstimator
|
|
10
|
+
from .CDFEstimation import CensoredCDFEstimator
|
|
9
11
|
|
|
10
|
-
__all__ = ["GLTGridSearchEstimator", "GLTWeightEstimator"]
|
|
12
|
+
__all__ = ["GLTGridSearchEstimator", "GLTWeightEstimator", "GLTWeightDistribEstimator"]
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class GLTWeightEstimator(BaseGLTWEightEstimator):
|
|
14
|
-
"""GLTW Weight Estimator for modeling influence in networks.
|
|
15
|
-
|
|
16
|
-
Attributes
|
|
17
|
-
----------
|
|
18
|
-
vertex_2_distrib : Dict[int, rv_frozen]
|
|
19
|
-
Distribution mapping for vertices.
|
|
20
|
-
"""
|
|
21
16
|
|
|
22
17
|
def _compute_failed_vertex_ll(self, weights: np.ndarray, vertex: int) -> float:
|
|
23
18
|
"""Compute the log-likelihood for failed vertices.
|
|
@@ -99,8 +94,8 @@ class GLTWeightEstimator(BaseGLTWEightEstimator):
|
|
|
99
94
|
"""
|
|
100
95
|
return np.sum([
|
|
101
96
|
self._compute_vertex_nll(weights[self.graph.get_parents_mask(vertex)], vertex)
|
|
102
|
-
for vertex in self.informative_vertices
|
|
103
|
-
|
|
97
|
+
for vertex in self.informative_vertices]
|
|
98
|
+
)
|
|
104
99
|
|
|
105
100
|
def _compute_normalized_vertex_nll(self, weights: np.ndarray, vertex: int) -> float:
|
|
106
101
|
"""Compute the normalized negative log-likelihood for a vertex.
|
|
@@ -149,7 +144,8 @@ class GLTWeightEstimator(BaseGLTWEightEstimator):
|
|
|
149
144
|
for vertex in self.informative_vertices
|
|
150
145
|
}
|
|
151
146
|
|
|
152
|
-
def _optimize_vertex_parent_params(self, vertex: int, optimization_kwargs: Dict = None) ->
|
|
147
|
+
def _optimize_vertex_parent_params(self, vertex: int, optimization_kwargs: Dict = None) -> \
|
|
148
|
+
Tuple[np.ndarray, rv_frozen]:
|
|
153
149
|
"""Optimize parameters for a specific vertex.
|
|
154
150
|
|
|
155
151
|
Parameters
|
|
@@ -173,7 +169,8 @@ class GLTWeightEstimator(BaseGLTWEightEstimator):
|
|
|
173
169
|
)
|
|
174
170
|
return optimizer_output.x, self.vertex_2_distrib[vertex]
|
|
175
171
|
|
|
176
|
-
def _set_informative_vertices_parent_params(self, informative_vertices_params: List[Tuple[np.ndarray, rv_frozen]])
|
|
172
|
+
def _set_informative_vertices_parent_params(self, informative_vertices_params: List[Tuple[np.ndarray, rv_frozen]]) \
|
|
173
|
+
-> None:
|
|
177
174
|
"""Set the optimized parameters for informative vertices.
|
|
178
175
|
|
|
179
176
|
Parameters
|
|
@@ -257,7 +254,8 @@ class GLTGridSearchEstimator(GLTWeightEstimator):
|
|
|
257
254
|
for vertex_distribs in self.vertex_2_distrib_grid.values()
|
|
258
255
|
]), "All elements of the grid should be `rv_frozen`."
|
|
259
256
|
|
|
260
|
-
def _optimize_vertex_parent_params(self, vertex: int, optimization_kwargs: Dict = None) ->
|
|
257
|
+
def _optimize_vertex_parent_params(self, vertex: int, optimization_kwargs: Dict = None) -> \
|
|
258
|
+
Tuple[np.ndarray, rv_frozen]:
|
|
261
259
|
"""Optimize parameters for a specific vertex using grid search.
|
|
262
260
|
|
|
263
261
|
Parameters
|
|
@@ -292,3 +290,81 @@ class GLTGridSearchEstimator(GLTWeightEstimator):
|
|
|
292
290
|
best_nll = nll
|
|
293
291
|
|
|
294
292
|
return best_weights, best_distrib
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class GLTWeightDistribEstimator(GLTWeightEstimator):
|
|
296
|
+
"""
|
|
297
|
+
Estimator of weights and vertex threshold distributions under the GLT diffusion model
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, graph: Graph,
|
|
301
|
+
threshold_support: Tuple[float, float] = (0, np.inf),
|
|
302
|
+
n_jobs: int = 1) -> None:
|
|
303
|
+
|
|
304
|
+
"""Initialize the GLTWeightDistribEstimator.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
graph : Graph
|
|
309
|
+
The graph structure.
|
|
310
|
+
n_jobs : int, optional
|
|
311
|
+
Number of jobs for parallel processing.
|
|
312
|
+
"""
|
|
313
|
+
super().__init__(graph=graph, vertex_2_distrib=None, n_jobs=n_jobs)
|
|
314
|
+
self.threshold_support = threshold_support
|
|
315
|
+
|
|
316
|
+
def _construct_vertex_threshold_observed_intervals(self, vertex: int, parent_weights: np.array):
|
|
317
|
+
intervals = []
|
|
318
|
+
for mask_tm1, mask_t in zip(self._vertex_2_active_parent_mask_tm1[vertex],
|
|
319
|
+
self._vertex_2_active_parent_mask_t[vertex]):
|
|
320
|
+
lb = parent_weights[mask_tm1].sum()
|
|
321
|
+
ub = parent_weights[mask_t].sum()
|
|
322
|
+
intervals.append((lb, ub))
|
|
323
|
+
|
|
324
|
+
for failed_mask in self._failed_vertices_masks[vertex]:
|
|
325
|
+
lb = parent_weights[failed_mask].sum()
|
|
326
|
+
intervals.append((lb, np.inf))
|
|
327
|
+
return intervals
|
|
328
|
+
|
|
329
|
+
def _optimize_vertex_parent_params(self, vertex: int,
|
|
330
|
+
max_alternate_iter=2, tol=1e-5,
|
|
331
|
+
verbose=False,
|
|
332
|
+
optimization_kwargs: Dict = None) -> Tuple[np.ndarray, rv_frozen]:
|
|
333
|
+
"""Optimize parameters for a specific vertex by iteratively estimating the weights and vertex threshold CDF.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
vertex : int
|
|
338
|
+
The vertex to optimize.
|
|
339
|
+
max_alternate_iter: int
|
|
340
|
+
Max number of weight & CDF estimation steps
|
|
341
|
+
verbose: bool
|
|
342
|
+
If verbose the NLL after iteration.
|
|
343
|
+
optimization_kwargs : Dict, optional
|
|
344
|
+
Additional optimization parameters.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
Tuple[np.ndarray, rv_frozen]
|
|
349
|
+
Weights and vertex threshold empirical CDF.
|
|
350
|
+
"""
|
|
351
|
+
parent_weights = self.weights_[self.graph.get_parents_mask(vertex)].copy()
|
|
352
|
+
for iteration in range(max_alternate_iter):
|
|
353
|
+
|
|
354
|
+
cdf_fit_intervals = self._construct_vertex_threshold_observed_intervals(vertex, parent_weights)
|
|
355
|
+
distrib = CensoredCDFEstimator(support=self.threshold_support)
|
|
356
|
+
self.vertex_2_distrib[vertex] = copy(distrib.fit(intervals=cdf_fit_intervals))
|
|
357
|
+
optimizer_output = minimize(
|
|
358
|
+
lambda weights: self._compute_normalized_vertex_nll(weights, vertex=vertex),
|
|
359
|
+
x0=parent_weights,
|
|
360
|
+
method='SLSQP',
|
|
361
|
+
constraints=self._weight_constraints[vertex],
|
|
362
|
+
options=optimization_kwargs)
|
|
363
|
+
diff = np.linalg.norm(optimizer_output.x - parent_weights)
|
|
364
|
+
parent_weights = optimizer_output.x.copy()
|
|
365
|
+
if verbose:
|
|
366
|
+
print(f"Alternating Iteration: {iteration}, l2-norm wights diff: {diff}")
|
|
367
|
+
if diff < tol:
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
return parent_weights, self.vertex_2_distrib[vertex]
|
{influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/influence_models.py
RENAMED
|
@@ -48,6 +48,7 @@ class InfluenceModel:
|
|
|
48
48
|
self.vertices = list(self.g.get_vertices())
|
|
49
49
|
self.random_state = random_state
|
|
50
50
|
self.n_jobs = n_jobs
|
|
51
|
+
self._edge_2_index = {edge: idx for idx, edge in enumerate(g.edges)}
|
|
51
52
|
if check_init:
|
|
52
53
|
self.check_param_init_correctness()
|
|
53
54
|
|
|
@@ -179,7 +180,7 @@ class InfluenceModel:
|
|
|
179
180
|
self._pre_simulation_init(seed_set, simulation_rvs=simulation_rvs)
|
|
180
181
|
return self._simulate_trace(out_trace_type=out_trace_type)
|
|
181
182
|
|
|
182
|
-
def make_simulation(self, seed_set: Set[int], simulation_rvs=None) ->
|
|
183
|
+
def make_simulation(self, seed_set: Set[int], simulation_rvs=None) -> Union[Trace, List[Set[int]]]:
|
|
183
184
|
"""Run a simulation and return the propagation trace.
|
|
184
185
|
|
|
185
186
|
Parameters
|
|
@@ -219,7 +220,8 @@ class InfluenceModel:
|
|
|
219
220
|
seed_sets = self._sample_seeds(n_seeds=n_traces, seed_size_range=seed_size_range)
|
|
220
221
|
return self.sample_traces_from_seeds(seed_sets, out_trace_type=out_trace_type)
|
|
221
222
|
|
|
222
|
-
def sample_traces_from_seeds(self, seed_sets: List[Set[int]],
|
|
223
|
+
def sample_traces_from_seeds(self, seed_sets: List[Set[int]],
|
|
224
|
+
out_trace_type: bool = True) -> Union[Traces, List[List[Set[int]]]]:
|
|
223
225
|
"""Sample traces from a list of seed sets.
|
|
224
226
|
|
|
225
227
|
Parameters
|
|
@@ -441,7 +443,7 @@ class LTM(InfluenceModel):
|
|
|
441
443
|
bool
|
|
442
444
|
True if the adjacent vertex is influenced, otherwise False.
|
|
443
445
|
"""
|
|
444
|
-
self.vertex_2_influence_counter[v_adj] += self.g.
|
|
446
|
+
self.vertex_2_influence_counter[v_adj] += self.g.get_edge_data(v, v_adj)["weight"]
|
|
445
447
|
return self.vertex_2_threshold[v_adj] <= self.vertex_2_influence_counter[v_adj]
|
|
446
448
|
|
|
447
449
|
def check_param_init_correctness(self, eps: float = 1e-6) -> None:
|
|
@@ -574,7 +576,7 @@ class ICM(InfluenceModel):
|
|
|
574
576
|
bool
|
|
575
577
|
True if the adjacent vertex is influenced, otherwise False.
|
|
576
578
|
"""
|
|
577
|
-
return self.edge_activations[self.
|
|
579
|
+
return self.edge_activations[self._edge_2_index[(v, v_adj)]]
|
|
578
580
|
|
|
579
581
|
def _generate_simulation_rvs(self, n_runs: int = 1) -> List:
|
|
580
582
|
"""Generate random activations for edges.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def plot_with_conf_intervals(x_true, x_pred, conf_intervals=None,
|
|
6
|
+
fontsize=12, figsize=(10, 8), color="blue", ax=None,
|
|
7
|
+
xlab="True activation probability",
|
|
8
|
+
ylab="Predicted activation probability"):
|
|
9
|
+
"""
|
|
10
|
+
Plot a scatter plot of `x_true` vs `x_pred` with confidence intervals as a filled area
|
|
11
|
+
and a diagonal line y=x.
|
|
12
|
+
|
|
13
|
+
:param x_true: Array-like, true values.
|
|
14
|
+
:param x_pred: Array-like, predicted values.
|
|
15
|
+
:param conf_intervals: 2D array of shape (2, len(x_pred)),
|
|
16
|
+
containing the lower and upper bounds of the confidence intervals.
|
|
17
|
+
If None, no confidence intervals are plotted.
|
|
18
|
+
:param fontsize: int, font size for axis labels. Default is 12.
|
|
19
|
+
:param figsize: tuple, size of the figure (width, height) in inches. Default is (10, 8).
|
|
20
|
+
:param color: str or tuple, color of the scatter points and confidence interval area. Default is "blue".
|
|
21
|
+
:param ax: matplotlib.axes.Axes, optional existing axes to plot on.
|
|
22
|
+
If None, a new figure and axes are created.
|
|
23
|
+
:param xlab: str, label for the x-axis. Default is "True activation probability".
|
|
24
|
+
:param ylab: str, label for the y-axis. Default is "Predicted activation probability".
|
|
25
|
+
"""
|
|
26
|
+
assert len(x_pred) == len(x_true), "x_pred and x_true must have the same length"
|
|
27
|
+
assert conf_intervals.shape[1] == len(x_pred), "conf_intervals must have the same second dim as x_pred"
|
|
28
|
+
assert conf_intervals.shape[0] == 2, "conf_intervals must have two rows for lower and upper bounds"
|
|
29
|
+
|
|
30
|
+
# Sort by x_true so fill_between works correctly
|
|
31
|
+
sort_idx = np.argsort(x_true)
|
|
32
|
+
x_true_sorted = x_true[sort_idx]
|
|
33
|
+
x_pred_sorted = x_pred[sort_idx]
|
|
34
|
+
|
|
35
|
+
if ax is None:
|
|
36
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
37
|
+
|
|
38
|
+
# Scatter plot
|
|
39
|
+
ax.scatter(x_true_sorted, x_pred_sorted, color=color)
|
|
40
|
+
|
|
41
|
+
# Filled confidence intervals
|
|
42
|
+
if conf_intervals is not None:
|
|
43
|
+
conf_lower_sorted = conf_intervals[0, sort_idx]
|
|
44
|
+
conf_upper_sorted = conf_intervals[1, sort_idx]
|
|
45
|
+
ax.fill_between(x_true_sorted, conf_lower_sorted, conf_upper_sorted,
|
|
46
|
+
color=color, alpha=0.2)
|
|
47
|
+
|
|
48
|
+
# Diagonal line y=x
|
|
49
|
+
min_x, max_x = np.min(x_true), np.max(x_true)
|
|
50
|
+
min_y, max_y = np.min(x_pred), np.max(x_pred)
|
|
51
|
+
ax.plot([min_x, max_x], [min_y, max_y], linestyle='--', color="black")
|
|
52
|
+
|
|
53
|
+
# Labels
|
|
54
|
+
ax.set_xlabel(xlab, fontsize=fontsize)
|
|
55
|
+
ax.set_ylabel(ylab, fontsize=fontsize)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def plot_hist_with_normal_fit(sample, true_value, true_std=None, n_bins=20):
|
|
59
|
+
"""
|
|
60
|
+
Plot a histogram of a sample with a fitted normal curve and a vertical line at the true value.
|
|
61
|
+
|
|
62
|
+
:param sample: Array-like, sample data points
|
|
63
|
+
:param true_value: Float, the true value
|
|
64
|
+
:param true_std: Float, the true std
|
|
65
|
+
:param n_bins: Int, the number of histogram bins
|
|
66
|
+
"""
|
|
67
|
+
from scipy.stats import norm
|
|
68
|
+
|
|
69
|
+
# Plot the histogram
|
|
70
|
+
plt.hist(sample, bins=n_bins, density=True, alpha=0.6, color='g', edgecolor='black')
|
|
71
|
+
|
|
72
|
+
xmin, xmax = plt.xlim()
|
|
73
|
+
x = np.linspace(xmin, xmax, 100)
|
|
74
|
+
mean, std = norm.fit(sample)
|
|
75
|
+
|
|
76
|
+
# Plot the fitted normal curve
|
|
77
|
+
p_fit = norm.pdf(x, loc=mean, scale=std)
|
|
78
|
+
plt.plot(x, p_fit, 'b', linewidth=2, label="Fitted Gaussian")
|
|
79
|
+
|
|
80
|
+
# Create the normal distribution's PDF
|
|
81
|
+
if true_std is not None:
|
|
82
|
+
p = norm.pdf(x, loc=true_value, scale=true_std)
|
|
83
|
+
plt.plot(x, p, 'r', linewidth=2, label="Theoretical Gaussian")
|
|
84
|
+
|
|
85
|
+
# Plot the vertical line at the true value
|
|
86
|
+
plt.axvline(true_value, color='black', linestyle='--', linewidth=1.5, label="True value")
|
|
87
|
+
|
|
88
|
+
# Add labels and title
|
|
89
|
+
plt.xlabel('Value')
|
|
90
|
+
plt.ylabel('Density')
|
|
91
|
+
plt.legend()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from typing import Iterable, Set
|
|
3
|
+
from scipy.stats._distn_infrastructure import rv_frozen
|
|
4
|
+
import jax.numpy as jnp
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
def multiple_union(set_list: Iterable[Set]):
|
|
@@ -16,7 +18,7 @@ def invert_non_zeros(array):
|
|
|
16
18
|
return out
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
def random_vector_inside_simplex(dim, ub=1):
|
|
21
|
+
def random_vector_inside_simplex(dim: int, ub: float = 1.):
|
|
20
22
|
U = np.random.uniform(low=0, high=ub, size=dim)
|
|
21
23
|
U_sorted = np.sort(U)
|
|
22
24
|
U_sorted = np.concatenate(([0], U_sorted))
|
|
@@ -24,7 +26,18 @@ def random_vector_inside_simplex(dim, ub=1):
|
|
|
24
26
|
return x
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def random_vector_on_simplex(dim, ub=1):
|
|
29
|
+
def random_vector_on_simplex(dim: int, ub: float = 1.):
|
|
28
30
|
X = random_vector_inside_simplex(dim=dim, ub=1)
|
|
29
31
|
return X / np.sum(X) * ub
|
|
30
32
|
|
|
33
|
+
|
|
34
|
+
def make_jax_cdf(distrib: rv_frozen):
|
|
35
|
+
name = distrib.dist.name
|
|
36
|
+
args = distrib.args
|
|
37
|
+
kwargs = distrib.kwds
|
|
38
|
+
local_dic = {}
|
|
39
|
+
exec(f"jax_distrib=jax.scipy.stats.{name}", None, local_dic)
|
|
40
|
+
jax_distrib = local_dic["jax_distrib"]
|
|
41
|
+
if name == "expon":
|
|
42
|
+
return lambda x: 1. - jnp.exp(-x)
|
|
43
|
+
return lambda x: jax_distrib.cdf(x, *args, **kwargs)
|
{influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: InfluenceDiffusion
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.14
|
|
4
4
|
Summary: InfluenceDiffusion package
|
|
5
5
|
Author: Alexander Kagan
|
|
6
6
|
Author-email: <amkagan@umich.edu>
|
|
@@ -13,18 +13,27 @@ Classifier: Operating System :: MacOS :: MacOS X
|
|
|
13
13
|
Classifier: Operating System :: Microsoft :: Windows
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: numpy<2
|
|
17
17
|
Requires-Dist: scipy
|
|
18
18
|
Requires-Dist: networkx
|
|
19
19
|
Requires-Dist: typing
|
|
20
20
|
Requires-Dist: joblib
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: keywords
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: summary
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
# InfluenceDiffusion
|
|
24
33
|
|
|
25
34
|
InfluenceDiffusion is a Python library that provides instruments for working with influence diffusion models on graphs. In particular, it contains implementations of
|
|
26
|
-
- Popular diffusion models such as Independent Cascade, (General) Linear Threshold, etc.
|
|
27
|
-
- Methods for estimating parameters of these models
|
|
35
|
+
- Popular diffusion models such as Independent Cascade, (General) Linear Threshold, etc.
|
|
36
|
+
- Methods for estimating parameters of these models and constructing the corresponding confidence intervals.
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
## Installation
|
|
@@ -40,36 +49,43 @@ pip install InfluenceDiffusion
|
|
|
40
49
|
```python
|
|
41
50
|
# Imports
|
|
42
51
|
import matplotlib.pyplot as plt
|
|
43
|
-
from networkx import
|
|
52
|
+
from networkx import connected_watts_strogatz_graph
|
|
53
|
+
from scipy.stats import beta
|
|
44
54
|
|
|
45
55
|
from InfluenceDiffusion.Graph import Graph # class inheriting from nx.DiGraph
|
|
46
|
-
from InfluenceDiffusion.
|
|
47
|
-
from InfluenceDiffusion.
|
|
56
|
+
from InfluenceDiffusion.Inference import GLTInferenceModule
|
|
57
|
+
from InfluenceDiffusion.influence_models import LTM
|
|
58
|
+
from InfluenceDiffusion.estimation_models.OptimEstimation import GLTWeightEstimator
|
|
48
59
|
from InfluenceDiffusion.weight_samplers import make_random_weights_with_indeg_constraint
|
|
60
|
+
from InfluenceDiffusion.plot_utils import plot_with_conf_intervals
|
|
49
61
|
|
|
50
|
-
# Sample an Erdos-Renyi graph
|
|
51
|
-
g_nx = erdos_renyi_graph(50, p=0.1, directed=True)
|
|
52
|
-
g = Graph(edge_list=g_nx.edges)
|
|
53
62
|
|
|
54
|
-
#
|
|
55
|
-
|
|
63
|
+
# Sample a connected Watts-Strogatz graph
|
|
64
|
+
random_state = 1
|
|
65
|
+
g = Graph(connected_watts_strogatz_graph(n=100, k=5, p=0.2, seed=random_state))
|
|
66
|
+
|
|
67
|
+
# Set ground-truth GLT model edge weights (in-degree of each node is at most 1)
|
|
68
|
+
weights = make_random_weights_with_indeg_constraint(g, indeg_ub=1, random_state=random_state)
|
|
56
69
|
g.set_weights(weights)
|
|
57
70
|
|
|
58
|
-
# Sample traces from
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
# Sample traces from the Beta(2, 1)-GLT model on this graph
|
|
72
|
+
threhsold_distrib = beta(2, 1)
|
|
73
|
+
gltm = LTM(g, threshold_generator=threhsold_distrib, random_state=random_state)
|
|
74
|
+
traces = gltm.sample_traces(1000)
|
|
61
75
|
|
|
62
76
|
# Estimate the weights using the traces
|
|
63
|
-
|
|
64
|
-
pred_weights =
|
|
77
|
+
gltm_estimator = GLTWeightEstimator(g, threhsold_distrib)
|
|
78
|
+
pred_weights = gltm_estimator.fit(traces)
|
|
79
|
+
|
|
80
|
+
# Compute 95% confidence intervals
|
|
81
|
+
glt_inferencer = GLTInferenceModule(gltm_estimator)
|
|
82
|
+
conf_ints = glt_inferencer.compute_all_weight_conf_ints(alpha=0.05)
|
|
65
83
|
|
|
66
84
|
# Compare with the ground-truth weights
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
plt.xlabel("True weights")
|
|
70
|
-
plt.ylabel("Predicted weights")
|
|
71
|
-
plt.show()
|
|
85
|
+
plot_with_conf_intervals(weights, pred_weights, conf_ints,
|
|
86
|
+
xlab="True weights", ylab="Predicted weights")
|
|
72
87
|
```
|
|
88
|
+

|
|
73
89
|
|
|
74
90
|
## License
|
|
75
91
|
|
{influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/SOURCES.txt
RENAMED
|
@@ -3,9 +3,11 @@ README.md
|
|
|
3
3
|
setup.cfg
|
|
4
4
|
setup.py
|
|
5
5
|
InfluenceDiffusion/Graph.py
|
|
6
|
+
InfluenceDiffusion/Inference.py
|
|
6
7
|
InfluenceDiffusion/Trace.py
|
|
7
8
|
InfluenceDiffusion/__init__.py
|
|
8
9
|
InfluenceDiffusion/influence_models.py
|
|
10
|
+
InfluenceDiffusion/plot_utils.py
|
|
9
11
|
InfluenceDiffusion/utils.py
|
|
10
12
|
InfluenceDiffusion/weight_samplers.py
|
|
11
13
|
InfluenceDiffusion.egg-info/PKG-INFO
|
|
@@ -14,6 +16,7 @@ InfluenceDiffusion.egg-info/dependency_links.txt
|
|
|
14
16
|
InfluenceDiffusion.egg-info/requires.txt
|
|
15
17
|
InfluenceDiffusion.egg-info/top_level.txt
|
|
16
18
|
InfluenceDiffusion/estimation_models/BaseWeightEstimator.py
|
|
19
|
+
InfluenceDiffusion/estimation_models/CDFEstimation.py
|
|
17
20
|
InfluenceDiffusion/estimation_models/EMEstimation.py
|
|
18
21
|
InfluenceDiffusion/estimation_models/OptimEstimation.py
|
|
19
22
|
InfluenceDiffusion/estimation_models/__init__.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: InfluenceDiffusion
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.14
|
|
4
4
|
Summary: InfluenceDiffusion package
|
|
5
5
|
Author: Alexander Kagan
|
|
6
6
|
Author-email: <amkagan@umich.edu>
|
|
@@ -13,18 +13,27 @@ Classifier: Operating System :: MacOS :: MacOS X
|
|
|
13
13
|
Classifier: Operating System :: Microsoft :: Windows
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: numpy<2
|
|
17
17
|
Requires-Dist: scipy
|
|
18
18
|
Requires-Dist: networkx
|
|
19
19
|
Requires-Dist: typing
|
|
20
20
|
Requires-Dist: joblib
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: keywords
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: summary
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
# InfluenceDiffusion
|
|
24
33
|
|
|
25
34
|
InfluenceDiffusion is a Python library that provides instruments for working with influence diffusion models on graphs. In particular, it contains implementations of
|
|
26
|
-
- Popular diffusion models such as Independent Cascade, (General) Linear Threshold, etc.
|
|
27
|
-
- Methods for estimating parameters of these models
|
|
35
|
+
- Popular diffusion models such as Independent Cascade, (General) Linear Threshold, etc.
|
|
36
|
+
- Methods for estimating parameters of these models and constructing the corresponding confidence intervals.
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
## Installation
|
|
@@ -40,36 +49,43 @@ pip install InfluenceDiffusion
|
|
|
40
49
|
```python
|
|
41
50
|
# Imports
|
|
42
51
|
import matplotlib.pyplot as plt
|
|
43
|
-
from networkx import
|
|
52
|
+
from networkx import connected_watts_strogatz_graph
|
|
53
|
+
from scipy.stats import beta
|
|
44
54
|
|
|
45
55
|
from InfluenceDiffusion.Graph import Graph # class inheriting from nx.DiGraph
|
|
46
|
-
from InfluenceDiffusion.
|
|
47
|
-
from InfluenceDiffusion.
|
|
56
|
+
from InfluenceDiffusion.Inference import GLTInferenceModule
|
|
57
|
+
from InfluenceDiffusion.influence_models import LTM
|
|
58
|
+
from InfluenceDiffusion.estimation_models.OptimEstimation import GLTWeightEstimator
|
|
48
59
|
from InfluenceDiffusion.weight_samplers import make_random_weights_with_indeg_constraint
|
|
60
|
+
from InfluenceDiffusion.plot_utils import plot_with_conf_intervals
|
|
49
61
|
|
|
50
|
-
# Sample an Erdos-Renyi graph
|
|
51
|
-
g_nx = erdos_renyi_graph(50, p=0.1, directed=True)
|
|
52
|
-
g = Graph(edge_list=g_nx.edges)
|
|
53
62
|
|
|
54
|
-
#
|
|
55
|
-
|
|
63
|
+
# Sample a connected Watts-Strogatz graph
|
|
64
|
+
random_state = 1
|
|
65
|
+
g = Graph(connected_watts_strogatz_graph(n=100, k=5, p=0.2, seed=random_state))
|
|
66
|
+
|
|
67
|
+
# Set ground-truth GLT model edge weights (in-degree of each node is at most 1)
|
|
68
|
+
weights = make_random_weights_with_indeg_constraint(g, indeg_ub=1, random_state=random_state)
|
|
56
69
|
g.set_weights(weights)
|
|
57
70
|
|
|
58
|
-
# Sample traces from
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
# Sample traces from the Beta(2, 1)-GLT model on this graph
|
|
72
|
+
threhsold_distrib = beta(2, 1)
|
|
73
|
+
gltm = LTM(g, threshold_generator=threhsold_distrib, random_state=random_state)
|
|
74
|
+
traces = gltm.sample_traces(1000)
|
|
61
75
|
|
|
62
76
|
# Estimate the weights using the traces
|
|
63
|
-
|
|
64
|
-
pred_weights =
|
|
77
|
+
gltm_estimator = GLTWeightEstimator(g, threhsold_distrib)
|
|
78
|
+
pred_weights = gltm_estimator.fit(traces)
|
|
79
|
+
|
|
80
|
+
# Compute 95% confidence intervals
|
|
81
|
+
glt_inferencer = GLTInferenceModule(gltm_estimator)
|
|
82
|
+
conf_ints = glt_inferencer.compute_all_weight_conf_ints(alpha=0.05)
|
|
65
83
|
|
|
66
84
|
# Compare with the ground-truth weights
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
plt.xlabel("True weights")
|
|
70
|
-
plt.ylabel("Predicted weights")
|
|
71
|
-
plt.show()
|
|
85
|
+
plot_with_conf_intervals(weights, pred_weights, conf_ints,
|
|
86
|
+
xlab="True weights", ylab="Predicted weights")
|
|
72
87
|
```
|
|
88
|
+

|
|
73
89
|
|
|
74
90
|
## License
|
|
75
91
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# InfluenceDiffusion
|
|
3
3
|
|
|
4
4
|
InfluenceDiffusion is a Python library that provides instruments for working with influence diffusion models on graphs. In particular, it contains implementations of
|
|
5
|
-
- Popular diffusion models such as Independent Cascade, (General) Linear Threshold, etc.
|
|
6
|
-
- Methods for estimating parameters of these models
|
|
5
|
+
- Popular diffusion models such as Independent Cascade, (General) Linear Threshold, etc.
|
|
6
|
+
- Methods for estimating parameters of these models and constructing the corresponding confidence intervals.
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
@@ -19,36 +19,43 @@ pip install InfluenceDiffusion
|
|
|
19
19
|
```python
|
|
20
20
|
# Imports
|
|
21
21
|
import matplotlib.pyplot as plt
|
|
22
|
-
from networkx import
|
|
22
|
+
from networkx import connected_watts_strogatz_graph
|
|
23
|
+
from scipy.stats import beta
|
|
23
24
|
|
|
24
25
|
from InfluenceDiffusion.Graph import Graph # class inheriting from nx.DiGraph
|
|
25
|
-
from InfluenceDiffusion.
|
|
26
|
-
from InfluenceDiffusion.
|
|
26
|
+
from InfluenceDiffusion.Inference import GLTInferenceModule
|
|
27
|
+
from InfluenceDiffusion.influence_models import LTM
|
|
28
|
+
from InfluenceDiffusion.estimation_models.OptimEstimation import GLTWeightEstimator
|
|
27
29
|
from InfluenceDiffusion.weight_samplers import make_random_weights_with_indeg_constraint
|
|
30
|
+
from InfluenceDiffusion.plot_utils import plot_with_conf_intervals
|
|
28
31
|
|
|
29
|
-
# Sample an Erdos-Renyi graph
|
|
30
|
-
g_nx = erdos_renyi_graph(50, p=0.1, directed=True)
|
|
31
|
-
g = Graph(edge_list=g_nx.edges)
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
|
|
33
|
+
# Sample a connected Watts-Strogatz graph
|
|
34
|
+
random_state = 1
|
|
35
|
+
g = Graph(connected_watts_strogatz_graph(n=100, k=5, p=0.2, seed=random_state))
|
|
36
|
+
|
|
37
|
+
# Set ground-truth GLT model edge weights (in-degree of each node is at most 1)
|
|
38
|
+
weights = make_random_weights_with_indeg_constraint(g, indeg_ub=1, random_state=random_state)
|
|
35
39
|
g.set_weights(weights)
|
|
36
40
|
|
|
37
|
-
# Sample traces from
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
# Sample traces from the Beta(2, 1)-GLT model on this graph
|
|
42
|
+
threhsold_distrib = beta(2, 1)
|
|
43
|
+
gltm = LTM(g, threshold_generator=threhsold_distrib, random_state=random_state)
|
|
44
|
+
traces = gltm.sample_traces(1000)
|
|
40
45
|
|
|
41
46
|
# Estimate the weights using the traces
|
|
42
|
-
|
|
43
|
-
pred_weights =
|
|
47
|
+
gltm_estimator = GLTWeightEstimator(g, threhsold_distrib)
|
|
48
|
+
pred_weights = gltm_estimator.fit(traces)
|
|
49
|
+
|
|
50
|
+
# Compute 95% confidence intervals
|
|
51
|
+
glt_inferencer = GLTInferenceModule(gltm_estimator)
|
|
52
|
+
conf_ints = glt_inferencer.compute_all_weight_conf_ints(alpha=0.05)
|
|
44
53
|
|
|
45
54
|
# Compare with the ground-truth weights
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
plt.xlabel("True weights")
|
|
49
|
-
plt.ylabel("Predicted weights")
|
|
50
|
-
plt.show()
|
|
55
|
+
plot_with_conf_intervals(weights, pred_weights, conf_ints,
|
|
56
|
+
xlab="True weights", ylab="Predicted weights")
|
|
51
57
|
```
|
|
58
|
+

|
|
52
59
|
|
|
53
60
|
## License
|
|
54
61
|
|
|
@@ -3,7 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
this_directory = Path(__file__).parent
|
|
4
4
|
LONG_DESCRIPTION = (this_directory / "README.md").read_text()
|
|
5
5
|
|
|
6
|
-
VERSION = '0.0.
|
|
6
|
+
VERSION = '0.0.14'
|
|
7
7
|
DESCRIPTION = 'InfluenceDiffusion package'
|
|
8
8
|
|
|
9
9
|
setup(
|
|
@@ -15,7 +15,7 @@ setup(
|
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
16
16
|
long_description=LONG_DESCRIPTION,
|
|
17
17
|
packages=find_packages(),
|
|
18
|
-
install_requires=["numpy",
|
|
18
|
+
install_requires=["numpy<2",
|
|
19
19
|
"scipy",
|
|
20
20
|
"networkx",
|
|
21
21
|
"typing",
|
|
File without changes
|
|
File without changes
|
{influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/weight_samplers.py
RENAMED
|
File without changes
|
|
File without changes
|
{influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|