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.
Files changed (23) hide show
  1. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/Graph.py +29 -104
  2. influencediffusion-0.0.14/InfluenceDiffusion/Inference.py +114 -0
  3. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/Trace.py +4 -4
  4. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/__init__.py +2 -0
  5. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/BaseWeightEstimator.py +9 -10
  6. influencediffusion-0.0.14/InfluenceDiffusion/estimation_models/CDFEstimation.py +73 -0
  7. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/OptimEstimation.py +89 -13
  8. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/influence_models.py +6 -4
  9. influencediffusion-0.0.14/InfluenceDiffusion/plot_utils.py +91 -0
  10. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/utils.py +15 -2
  11. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/PKG-INFO +39 -23
  12. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/SOURCES.txt +3 -0
  13. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/requires.txt +1 -1
  14. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/PKG-INFO +39 -23
  15. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/README.md +27 -20
  16. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/setup.py +2 -2
  17. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/EMEstimation.py +0 -0
  18. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/estimation_models/__init__.py +0 -0
  19. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion/weight_samplers.py +0 -0
  20. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/dependency_links.txt +0 -0
  21. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/InfluenceDiffusion.egg-info/top_level.txt +0 -0
  22. {influencediffusion-0.0.12 → influencediffusion-0.0.14}/LICENSE +0 -0
  23. {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, edge_list: List[Tuple[int, int]],
11
- directed: bool = True,
12
- weights: Union[np.array, List[float]] = None):
13
- """
14
- Initialize a directed or undirected graph from an edge list.
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 = np.ones(len(edge_list))
38
- self.weights = np.array(weights) if directed else np.hstack([weights] * 2)
39
- self.edge_2_index = {tuple(edge): idx for idx, edge in enumerate(self.edge_array)}
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 rename_graph_vertices(self, old_2_new_vertex_name_dict: Union[None, Dict] = None) -> "Graph":
42
- """
43
- Rename graph vertices according to the provided mapping or re-index them.
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
- Parameters
46
- ----------
47
- old_2_new_vertex_name_dict : Union[None, Dict], optional
48
- Mapping of old vertex names to new names.
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
- Returns
52
- -------
53
- Graph
54
- The updated graph instance with renamed vertices.
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
- self.edge_array[:, 0] = list(map(lambda v: old_2_new_vertex_name_dict[v], self.edge_array[:, 0]))
61
- self.edge_array[:, 1] = list(map(lambda v: old_2_new_vertex_name_dict[v], self.edge_array[:, 1]))
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[str, np.array]) -> "Graph":
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[str, np.array]
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.weights = np.array(weights)
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 [tuple(edge) for edge in self.edge_array]
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.edge_array)
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.get_vertices())
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: Tuple[Set] = None
82
- self.__failed_vertices: Set = None
83
- self.__activated_vertices: Set = None
84
- self.__activation_time_dict: Dict[int, int] = None
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 tqdm(traces.items()):
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.in1d(parents, np.array(list(vertices_tm1)))
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.in1d(parents, np.array(list(vertices_t)))
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
- weights[parent_mask] = np.random.exponential(num_parents)
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) -> Tuple[np.ndarray, rv_frozen]:
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]]) -> None:
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) -> Tuple[np.ndarray, rv_frozen]:
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]
@@ -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) -> np.ndarray:
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]], out_trace_type: bool = True) -> Union[Traces, List[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.get_edge_weight((v, v_adj))
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.g.get_edge_index((v, v_adj))]
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)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: InfluenceDiffusion
3
- Version: 0.0.12
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 erdos_renyi_graph
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.influence_models import LTM
47
- from InfluenceDiffusion.estimation_models.EMEstimation import LTWeightEstimatorEM
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
- # Set ground-truth LT model edge weights (in-degree of each node is at most 1)
55
- weights = make_random_weights_with_indeg_constraint(g, indeg_ub=1)
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 an LT model on this graph
59
- ltm = LTM(g)
60
- traces = ltm.sample_traces(1000)
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
- ltm_estimator = LTWeightEstimatorEM(g)
64
- pred_weights = ltm_estimator.fit(traces)
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
- plt.scatter(weights, pred_weights)
68
- plt.plot([0, 1], [0, 1], linestyle='--', c='black')
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
+ ![](images/weight_estimation.jpg)
73
89
 
74
90
  ## License
75
91
 
@@ -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,4 +1,4 @@
1
- numpy
1
+ numpy<2
2
2
  scipy
3
3
  networkx
4
4
  typing
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: InfluenceDiffusion
3
- Version: 0.0.12
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 erdos_renyi_graph
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.influence_models import LTM
47
- from InfluenceDiffusion.estimation_models.EMEstimation import LTWeightEstimatorEM
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
- # Set ground-truth LT model edge weights (in-degree of each node is at most 1)
55
- weights = make_random_weights_with_indeg_constraint(g, indeg_ub=1)
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 an LT model on this graph
59
- ltm = LTM(g)
60
- traces = ltm.sample_traces(1000)
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
- ltm_estimator = LTWeightEstimatorEM(g)
64
- pred_weights = ltm_estimator.fit(traces)
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
- plt.scatter(weights, pred_weights)
68
- plt.plot([0, 1], [0, 1], linestyle='--', c='black')
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
+ ![](images/weight_estimation.jpg)
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 erdos_renyi_graph
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.influence_models import LTM
26
- from InfluenceDiffusion.estimation_models.EMEstimation import LTWeightEstimatorEM
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
- # Set ground-truth LT model edge weights (in-degree of each node is at most 1)
34
- weights = make_random_weights_with_indeg_constraint(g, indeg_ub=1)
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 an LT model on this graph
38
- ltm = LTM(g)
39
- traces = ltm.sample_traces(1000)
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
- ltm_estimator = LTWeightEstimatorEM(g)
43
- pred_weights = ltm_estimator.fit(traces)
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
- plt.scatter(weights, pred_weights)
47
- plt.plot([0, 1], [0, 1], linestyle='--', c='black')
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
+ ![](images/weight_estimation.jpg)
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.12'
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",