1"""Functions for computing measures of structural holes."""
2
3import networkx as nx
4
5__all__ = ["constraint", "local_constraint", "effective_size"]
6
7
8@nx._dispatchable(edge_attrs="weight")
9def mutual_weight(G, u, v, weight=None):
10 """Returns the sum of the weights of the edge from `u` to `v` and
11 the edge from `v` to `u` in `G`.
12
13 `weight` is the edge data key that represents the edge weight. If
14 the specified key is `None` or is not in the edge data for an edge,
15 that edge is assumed to have weight 1.
16
17 Pre-conditions: `u` and `v` must both be in `G`.
18
19 """
20 try:
21 a_uv = G[u][v].get(weight, 1)
22 except KeyError:
23 a_uv = 0
24 try:
25 a_vu = G[v][u].get(weight, 1)
26 except KeyError:
27 a_vu = 0
28 return a_uv + a_vu
29
30
31@nx._dispatchable(edge_attrs="weight")
32def normalized_mutual_weight(G, u, v, norm=sum, weight=None):
33 """Returns normalized mutual weight of the edges from `u` to `v`
34 with respect to the mutual weights of the neighbors of `u` in `G`.
35
36 `norm` specifies how the normalization factor is computed. It must
37 be a function that takes a single argument and returns a number.
38 The argument will be an iterable of mutual weights
39 of pairs ``(u, w)``, where ``w`` ranges over each (in- and
40 out-)neighbor of ``u``. Commons values for `normalization` are
41 ``sum`` and ``max``.
42
43 `weight` can be ``None`` or a string, if None, all edge weights
44 are considered equal. Otherwise holds the name of the edge
45 attribute used as weight.
46
47 """
48 scale = norm(mutual_weight(G, u, w, weight) for w in set(nx.all_neighbors(G, u)))
49 return 0 if scale == 0 else mutual_weight(G, u, v, weight) / scale
50
51
52@nx._dispatchable(edge_attrs="weight")
53def effective_size(G, nodes=None, weight=None):
54 r"""Returns the effective size of all nodes in the graph ``G``.
55
56 The *effective size* of a node's ego network is based on the concept
57 of redundancy. A person's ego network has redundancy to the extent
58 that her contacts are connected to each other as well. The
59 nonredundant part of a person's relationships is the effective
60 size of her ego network [1]_. Formally, the effective size of a
61 node $u$, denoted $e(u)$, is defined by
62
63 .. math::
64
65 e(u) = \sum_{v \in N(u) \setminus \{u\}}
66 \left(1 - \sum_{w \in N(v)} p_{uw} m_{vw}\right)
67
68 where $N(u)$ is the set of neighbors of $u$ and $p_{uw}$ is the
69 normalized mutual weight of the (directed or undirected) edges
70 joining $u$ and $v$, for each vertex $u$ and $v$ [1]_. And $m_{vw}$
71 is the mutual weight of $v$ and $w$ divided by $v$ highest mutual
72 weight with any of its neighbors. The *mutual weight* of $u$ and $v$
73 is the sum of the weights of edges joining them (edge weights are
74 assumed to be one if the graph is unweighted).
75
76 For the case of unweighted and undirected graphs, Borgatti proposed
77 a simplified formula to compute effective size [2]_
78
79 .. math::
80
81 e(u) = n - \frac{2t}{n}
82
83 where `t` is the number of ties in the ego network (not including
84 ties to ego) and `n` is the number of nodes (excluding ego).
85
86 Parameters
87 ----------
88 G : NetworkX graph
89 The graph containing ``v``. Directed graphs are treated like
90 undirected graphs when computing neighbors of ``v``.
91
92 nodes : container, optional
93 Container of nodes in the graph ``G`` to compute the effective size.
94 If None, the effective size of every node is computed.
95
96 weight : None or string, optional
97 If None, all edge weights are considered equal.
98 Otherwise holds the name of the edge attribute used as weight.
99
100 Returns
101 -------
102 dict
103 Dictionary with nodes as keys and the effective size of the node as values.
104
105 Notes
106 -----
107 Isolated nodes, including nodes which only have self-loop edges, do not
108 have a well-defined effective size::
109
110 >>> G = nx.path_graph(3)
111 >>> G.add_edge(4, 4)
112 >>> nx.effective_size(G)
113 {0: 1.0, 1: 2.0, 2: 1.0, 4: nan}
114
115 Burt also defined the related concept of *efficiency* of a node's ego
116 network, which is its effective size divided by the degree of that
117 node [1]_. So you can easily compute efficiency:
118
119 >>> G = nx.DiGraph()
120 >>> G.add_edges_from([(0, 1), (0, 2), (1, 0), (2, 1)])
121 >>> esize = nx.effective_size(G)
122 >>> efficiency = {n: v / G.degree(n) for n, v in esize.items()}
123
124 See also
125 --------
126 constraint
127
128 References
129 ----------
130 .. [1] Burt, Ronald S.
131 *Structural Holes: The Social Structure of Competition.*
132 Cambridge: Harvard University Press, 1995.
133
134 .. [2] Borgatti, S.
135 "Structural Holes: Unpacking Burt's Redundancy Measures"
136 CONNECTIONS 20(1):35-38.
137 http://www.analytictech.com/connections/v20(1)/holes.htm
138
139 """
140
141 def redundancy(G, u, v, weight=None):
142 nmw = normalized_mutual_weight
143 r = sum(
144 nmw(G, u, w, weight=weight) * nmw(G, v, w, norm=max, weight=weight)
145 for w in set(nx.all_neighbors(G, u))
146 )
147 return 1 - r
148
149 # Check if scipy is available
150 try:
151 # Needed for errstate
152 import numpy as np
153
154 # make sure nx.adjacency_matrix will not raise
155 import scipy as sp
156
157 has_scipy = True
158 except:
159 has_scipy = False
160
161 if nodes is None and has_scipy:
162 # In order to compute constraint of all nodes,
163 # algorithms based on sparse matrices can be much faster
164
165 # Obtain the adjacency matrix
166 P = nx.adjacency_matrix(G, weight=weight)
167
168 # Calculate mutual weights
169 mutual_weights1 = P + P.T
170 mutual_weights2 = mutual_weights1.copy()
171
172 with np.errstate(divide="ignore"):
173 # Mutual_weights1 = Normalize mutual weights by row sums
174 mutual_weights1 /= mutual_weights1.sum(axis=1)[:, np.newaxis]
175
176 # Mutual_weights2 = Normalize mutual weights by row max
177 mutual_weights2 /= mutual_weights2.max(axis=1).toarray()
178
179 # Calculate effective sizes
180 r = 1 - (mutual_weights1 @ mutual_weights2.T).toarray()
181 effective_size = ((mutual_weights1 > 0) * r).sum(axis=1)
182
183 # Special treatment: isolated nodes (ignoring selfloops) marked with "nan"
184 sum_mutual_weights = mutual_weights1.sum(axis=1) - mutual_weights1.diagonal()
185 isolated_nodes = sum_mutual_weights == 0
186 effective_size[isolated_nodes] = float("nan")
187 # Use tolist() to automatically convert numpy scalars -> Python scalars
188 return dict(zip(G, effective_size.tolist()))
189
190 # Results for only requested nodes
191 effective_size = {}
192 if nodes is None:
193 nodes = G
194 # Use Borgatti's simplified formula for unweighted and undirected graphs
195 if not G.is_directed() and weight is None:
196 for v in nodes:
197 # Effective size is not defined for isolated nodes, including nodes
198 # with only self-edges
199 if all(u == v for u in G[v]):
200 effective_size[v] = float("nan")
201 continue
202 E = nx.ego_graph(G, v, center=False, undirected=True)
203 effective_size[v] = len(E) - (2 * E.size()) / len(E)
204 else:
205 for v in nodes:
206 # Effective size is not defined for isolated nodes, including nodes
207 # with only self-edges
208 if all(u == v for u in G[v]):
209 effective_size[v] = float("nan")
210 continue
211 effective_size[v] = sum(
212 redundancy(G, v, u, weight) for u in set(nx.all_neighbors(G, v))
213 )
214 return effective_size
215
216
217@nx._dispatchable(edge_attrs="weight")
218def constraint(G, nodes=None, weight=None):
219 r"""Returns the constraint on all nodes in the graph ``G``.
220
221 The *constraint* is a measure of the extent to which a node *v* is
222 invested in those nodes that are themselves invested in the
223 neighbors of *v*. Formally, the *constraint on v*, denoted `c(v)`,
224 is defined by
225
226 .. math::
227
228 c(v) = \sum_{w \in N(v) \setminus \{v\}} \ell(v, w)
229
230 where $N(v)$ is the subset of the neighbors of `v` that are either
231 predecessors or successors of `v` and $\ell(v, w)$ is the local
232 constraint on `v` with respect to `w` [1]_. For the definition of local
233 constraint, see :func:`local_constraint`.
234
235 Parameters
236 ----------
237 G : NetworkX graph
238 The graph containing ``v``. This can be either directed or undirected.
239
240 nodes : container, optional
241 Container of nodes in the graph ``G`` to compute the constraint. If
242 None, the constraint of every node is computed.
243
244 weight : None or string, optional
245 If None, all edge weights are considered equal.
246 Otherwise holds the name of the edge attribute used as weight.
247
248 Returns
249 -------
250 dict
251 Dictionary with nodes as keys and the constraint on the node as values.
252
253 See also
254 --------
255 local_constraint
256
257 References
258 ----------
259 .. [1] Burt, Ronald S.
260 "Structural holes and good ideas".
261 American Journal of Sociology (110): 349–399.
262
263 """
264
265 # Check if scipy is available
266 try:
267 # Needed for errstate
268 import numpy as np
269
270 # make sure nx.adjacency_matrix will not raise
271 import scipy as sp
272
273 has_scipy = True
274 except:
275 has_scipy = False
276
277 if nodes is None and has_scipy:
278 # In order to compute constraint of all nodes,
279 # algorithms based on sparse matrices can be much faster
280
281 # Obtain the adjacency matrix
282 P = nx.adjacency_matrix(G, weight=weight)
283
284 # Calculate mutual weights
285 mutual_weights = P + P.T
286
287 # Normalize mutual weights by row sums
288 sum_mutual_weights = mutual_weights.sum(axis=1)
289 with np.errstate(divide="ignore"):
290 mutual_weights /= sum_mutual_weights[:, np.newaxis]
291
292 # Calculate local constraints and constraints
293 local_constraints = (mutual_weights + mutual_weights @ mutual_weights) ** 2
294 constraints = ((mutual_weights > 0) * local_constraints).sum(axis=1)
295
296 # Special treatment: isolated nodes marked with "nan"
297 isolated_nodes = sum_mutual_weights - 2 * mutual_weights.diagonal() == 0
298 constraints[isolated_nodes] = float("nan")
299 # Use tolist() to automatically convert numpy scalars -> Python scalars
300 return dict(zip(G, constraints.tolist()))
301
302 # Result for only requested nodes
303 constraint = {}
304 if nodes is None:
305 nodes = G
306 for v in nodes:
307 # Constraint is not defined for isolated nodes
308 if len(G[v]) == 0:
309 constraint[v] = float("nan")
310 continue
311 constraint[v] = sum(
312 local_constraint(G, v, n, weight) for n in set(nx.all_neighbors(G, v))
313 )
314 return constraint
315
316
317@nx._dispatchable(edge_attrs="weight")
318def local_constraint(G, u, v, weight=None):
319 r"""Returns the local constraint on the node ``u`` with respect to
320 the node ``v`` in the graph ``G``.
321
322 Formally, the *local constraint on u with respect to v*, denoted
323 $\ell(u, v)$, is defined by
324
325 .. math::
326
327 \ell(u, v) = \left(p_{uv} + \sum_{w \in N(v)} p_{uw} p_{wv}\right)^2,
328
329 where $N(v)$ is the set of neighbors of $v$ and $p_{uv}$ is the
330 normalized mutual weight of the (directed or undirected) edges
331 joining $u$ and $v$, for each vertex $u$ and $v$ [1]_. The *mutual
332 weight* of $u$ and $v$ is the sum of the weights of edges joining
333 them (edge weights are assumed to be one if the graph is
334 unweighted).
335
336 Parameters
337 ----------
338 G : NetworkX graph
339 The graph containing ``u`` and ``v``. This can be either
340 directed or undirected.
341
342 u : node
343 A node in the graph ``G``.
344
345 v : node
346 A node in the graph ``G``.
347
348 weight : None or string, optional
349 If None, all edge weights are considered equal.
350 Otherwise holds the name of the edge attribute used as weight.
351
352 Returns
353 -------
354 float
355 The constraint of the node ``v`` in the graph ``G``.
356
357 See also
358 --------
359 constraint
360
361 References
362 ----------
363 .. [1] Burt, Ronald S.
364 "Structural holes and good ideas".
365 American Journal of Sociology (110): 349–399.
366
367 """
368 nmw = normalized_mutual_weight
369 direct = nmw(G, u, v, weight=weight)
370 indirect = sum(
371 nmw(G, u, w, weight=weight) * nmw(G, w, v, weight=weight)
372 for w in set(nx.all_neighbors(G, u))
373 )
374 return (direct + indirect) ** 2