Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/algorithms/triads.py: 18%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

133 statements  

1# See https://github.com/networkx/networkx/pull/1474 

2# Copyright 2011 Reya Group <http://www.reyagroup.com> 

3# Copyright 2011 Alex Levenson <alex@isnotinvain.com> 

4# Copyright 2011 Diederik van Liere <diederik.vanliere@rotman.utoronto.ca> 

5"""Functions for analyzing triads of a graph.""" 

6 

7from collections import defaultdict 

8from itertools import combinations, permutations 

9 

10import networkx as nx 

11from networkx.utils import not_implemented_for, py_random_state 

12 

13__all__ = [ 

14 "triadic_census", 

15 "is_triad", 

16 "all_triads", 

17 "triads_by_type", 

18 "triad_type", 

19] 

20 

21#: The integer codes representing each type of triad. 

22#: 

23#: Triads that are the same up to symmetry have the same code. 

24TRICODES = ( 

25 1, 

26 2, 

27 2, 

28 3, 

29 2, 

30 4, 

31 6, 

32 8, 

33 2, 

34 6, 

35 5, 

36 7, 

37 3, 

38 8, 

39 7, 

40 11, 

41 2, 

42 6, 

43 4, 

44 8, 

45 5, 

46 9, 

47 9, 

48 13, 

49 6, 

50 10, 

51 9, 

52 14, 

53 7, 

54 14, 

55 12, 

56 15, 

57 2, 

58 5, 

59 6, 

60 7, 

61 6, 

62 9, 

63 10, 

64 14, 

65 4, 

66 9, 

67 9, 

68 12, 

69 8, 

70 13, 

71 14, 

72 15, 

73 3, 

74 7, 

75 8, 

76 11, 

77 7, 

78 12, 

79 14, 

80 15, 

81 8, 

82 14, 

83 13, 

84 15, 

85 11, 

86 15, 

87 15, 

88 16, 

89) 

90 

91#: The names of each type of triad. The order of the elements is 

92#: important: it corresponds to the tricodes given in :data:`TRICODES`. 

93TRIAD_NAMES = ( 

94 "003", 

95 "012", 

96 "102", 

97 "021D", 

98 "021U", 

99 "021C", 

100 "111D", 

101 "111U", 

102 "030T", 

103 "030C", 

104 "201", 

105 "120D", 

106 "120U", 

107 "120C", 

108 "210", 

109 "300", 

110) 

111 

112 

113#: A dictionary mapping triad code to triad name. 

114TRICODE_TO_NAME = {i: TRIAD_NAMES[code - 1] for i, code in enumerate(TRICODES)} 

115 

116 

117def _tricode(G, v, u, w): 

118 """Returns the integer code of the given triad. 

119 

120 This is some fancy magic that comes from Batagelj and Mrvar's paper. It 

121 treats each edge joining a pair of `v`, `u`, and `w` as a bit in 

122 the binary representation of an integer. 

123 

124 """ 

125 combos = ((v, u, 1), (u, v, 2), (v, w, 4), (w, v, 8), (u, w, 16), (w, u, 32)) 

126 return sum(x for u, v, x in combos if v in G[u]) 

127 

128 

129@not_implemented_for("undirected") 

130@nx._dispatchable 

131def triadic_census(G, nodelist=None): 

132 """Determines the triadic census of a directed graph. 

133 

134 The triadic census is a count of how many of the 16 possible types of 

135 triads are present in a directed graph. If a list of nodes is passed, then 

136 only those triads are taken into account which have elements of nodelist in them. 

137 

138 Parameters 

139 ---------- 

140 G : digraph 

141 A NetworkX DiGraph 

142 nodelist : list 

143 List of nodes for which you want to calculate triadic census 

144 

145 Returns 

146 ------- 

147 census : dict 

148 Dictionary with triad type as keys and number of occurrences as values. 

149 

150 Examples 

151 -------- 

152 >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)]) 

153 >>> triadic_census = nx.triadic_census(G) 

154 >>> for key, value in triadic_census.items(): 

155 ... print(f"{key}: {value}") 

156 003: 0 

157 012: 0 

158 102: 0 

159 021D: 0 

160 021U: 0 

161 021C: 0 

162 111D: 0 

163 111U: 0 

164 030T: 2 

165 030C: 2 

166 201: 0 

167 120D: 0 

168 120U: 0 

169 120C: 0 

170 210: 0 

171 300: 0 

172 

173 Notes 

174 ----- 

175 This algorithm has complexity $O(m)$ where $m$ is the number of edges in 

176 the graph. 

177 

178 For undirected graphs, the triadic census can be computed by first converting 

179 the graph into a directed graph using the ``G.to_directed()`` method. 

180 After this conversion, only the triad types 003, 102, 201 and 300 will be 

181 present in the undirected scenario. 

182 

183 Raises 

184 ------ 

185 ValueError 

186 If `nodelist` contains duplicate nodes or nodes not in `G`. 

187 If you want to ignore this you can preprocess with `set(nodelist) & G.nodes` 

188 

189 See also 

190 -------- 

191 triad_graph 

192 

193 References 

194 ---------- 

195 .. [1] Vladimir Batagelj and Andrej Mrvar, A subquadratic triad census 

196 algorithm for large sparse networks with small maximum degree, 

197 University of Ljubljana, 

198 http://vlado.fmf.uni-lj.si/pub/networks/doc/triads/triads.pdf 

199 

200 """ 

201 nodeset = set(G.nbunch_iter(nodelist)) 

202 if nodelist is not None and len(nodelist) != len(nodeset): 

203 raise ValueError("nodelist includes duplicate nodes or nodes not in G") 

204 

205 N = len(G) 

206 Nnot = N - len(nodeset) # can signal special counting for subset of nodes 

207 

208 # create an ordering of nodes with nodeset nodes first 

209 m = {n: i for i, n in enumerate(nodeset)} 

210 if Nnot: 

211 # add non-nodeset nodes later in the ordering 

212 not_nodeset = G.nodes - nodeset 

213 m.update((n, i + N) for i, n in enumerate(not_nodeset)) 

214 

215 # build all_neighbor dicts for easy counting 

216 # After Python 3.8 can leave off these keys(). Speedup also using G._pred 

217 # nbrs = {n: G._pred[n].keys() | G._succ[n].keys() for n in G} 

218 nbrs = {n: G.pred[n].keys() | G.succ[n].keys() for n in G} 

219 dbl_nbrs = {n: G.pred[n].keys() & G.succ[n].keys() for n in G} 

220 

221 if Nnot: 

222 sgl_nbrs = {n: G.pred[n].keys() ^ G.succ[n].keys() for n in not_nodeset} 

223 # find number of edges not incident to nodes in nodeset 

224 sgl = sum(1 for n in not_nodeset for nbr in sgl_nbrs[n] if nbr not in nodeset) 

225 sgl_edges_outside = sgl // 2 

226 dbl = sum(1 for n in not_nodeset for nbr in dbl_nbrs[n] if nbr not in nodeset) 

227 dbl_edges_outside = dbl // 2 

228 

229 # Initialize the count for each triad to be zero. 

230 census = {name: 0 for name in TRIAD_NAMES} 

231 # Main loop over nodes 

232 for v in nodeset: 

233 vnbrs = nbrs[v] 

234 dbl_vnbrs = dbl_nbrs[v] 

235 if Nnot: 

236 # set up counts of edges attached to v. 

237 sgl_unbrs_bdy = sgl_unbrs_out = dbl_unbrs_bdy = dbl_unbrs_out = 0 

238 for u in vnbrs: 

239 if m[u] <= m[v]: 

240 continue 

241 unbrs = nbrs[u] 

242 neighbors = (vnbrs | unbrs) - {u, v} 

243 # Count connected triads. 

244 for w in neighbors: 

245 if m[u] < m[w] or (m[v] < m[w] < m[u] and v not in nbrs[w]): 

246 code = _tricode(G, v, u, w) 

247 census[TRICODE_TO_NAME[code]] += 1 

248 

249 # Use a formula for dyadic triads with edge incident to v 

250 if u in dbl_vnbrs: 

251 census["102"] += N - len(neighbors) - 2 

252 else: 

253 census["012"] += N - len(neighbors) - 2 

254 

255 # Count edges attached to v. Subtract later to get triads with v isolated 

256 # _out are (u,unbr) for unbrs outside boundary of nodeset 

257 # _bdy are (u,unbr) for unbrs on boundary of nodeset (get double counted) 

258 if Nnot and u not in nodeset: 

259 sgl_unbrs = sgl_nbrs[u] 

260 sgl_unbrs_bdy += len(sgl_unbrs & vnbrs - nodeset) 

261 sgl_unbrs_out += len(sgl_unbrs - vnbrs - nodeset) 

262 dbl_unbrs = dbl_nbrs[u] 

263 dbl_unbrs_bdy += len(dbl_unbrs & vnbrs - nodeset) 

264 dbl_unbrs_out += len(dbl_unbrs - vnbrs - nodeset) 

265 # if nodeset == G.nodes, skip this b/c we will find the edge later. 

266 if Nnot: 

267 # Count edges outside nodeset not connected with v (v isolated triads) 

268 census["012"] += sgl_edges_outside - (sgl_unbrs_out + sgl_unbrs_bdy // 2) 

269 census["102"] += dbl_edges_outside - (dbl_unbrs_out + dbl_unbrs_bdy // 2) 

270 

271 # calculate null triads: "003" 

272 # null triads = total number of possible triads - all found triads 

273 total_triangles = (N * (N - 1) * (N - 2)) // 6 

274 triangles_without_nodeset = (Nnot * (Nnot - 1) * (Nnot - 2)) // 6 

275 total_census = total_triangles - triangles_without_nodeset 

276 census["003"] = total_census - sum(census.values()) 

277 

278 return census 

279 

280 

281@nx._dispatchable 

282def is_triad(G): 

283 """Returns True if the graph G is a triad, else False. 

284 

285 Parameters 

286 ---------- 

287 G : graph 

288 A NetworkX Graph 

289 

290 Returns 

291 ------- 

292 istriad : boolean 

293 Whether G is a valid triad 

294 

295 Examples 

296 -------- 

297 >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) 

298 >>> nx.is_triad(G) 

299 True 

300 >>> G.add_edge(0, 1) 

301 >>> nx.is_triad(G) 

302 False 

303 """ 

304 if isinstance(G, nx.Graph): 

305 if G.order() == 3 and nx.is_directed(G): 

306 if not any((n, n) in G.edges() for n in G.nodes()): 

307 return True 

308 return False 

309 

310 

311@not_implemented_for("undirected") 

312@nx._dispatchable(returns_graph=True) 

313def all_triads(G): 

314 """A generator of all possible triads in G. 

315 

316 Parameters 

317 ---------- 

318 G : digraph 

319 A NetworkX DiGraph 

320 

321 Returns 

322 ------- 

323 all_triads : generator of DiGraphs 

324 Generator of triads (order-3 DiGraphs) 

325 

326 Examples 

327 -------- 

328 >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)]) 

329 >>> for triad in nx.all_triads(G): 

330 ... print(triad.edges) 

331 [(1, 2), (2, 3), (3, 1)] 

332 [(1, 2), (4, 1), (4, 2)] 

333 [(3, 1), (3, 4), (4, 1)] 

334 [(2, 3), (3, 4), (4, 2)] 

335 

336 """ 

337 triplets = combinations(G.nodes(), 3) 

338 for triplet in triplets: 

339 yield G.subgraph(triplet).copy() 

340 

341 

342@not_implemented_for("undirected") 

343@nx._dispatchable 

344def triads_by_type(G): 

345 """Returns a list of all triads for each triad type in a directed graph. 

346 There are exactly 16 different types of triads possible. Suppose 1, 2, 3 are three 

347 nodes, they will be classified as a particular triad type if their connections 

348 are as follows: 

349 

350 - 003: 1, 2, 3 

351 - 012: 1 -> 2, 3 

352 - 102: 1 <-> 2, 3 

353 - 021D: 1 <- 2 -> 3 

354 - 021U: 1 -> 2 <- 3 

355 - 021C: 1 -> 2 -> 3 

356 - 111D: 1 <-> 2 <- 3 

357 - 111U: 1 <-> 2 -> 3 

358 - 030T: 1 -> 2 -> 3, 1 -> 3 

359 - 030C: 1 <- 2 <- 3, 1 -> 3 

360 - 201: 1 <-> 2 <-> 3 

361 - 120D: 1 <- 2 -> 3, 1 <-> 3 

362 - 120U: 1 -> 2 <- 3, 1 <-> 3 

363 - 120C: 1 -> 2 -> 3, 1 <-> 3 

364 - 210: 1 -> 2 <-> 3, 1 <-> 3 

365 - 300: 1 <-> 2 <-> 3, 1 <-> 3 

366 

367 Refer to the :doc:`example gallery </auto_examples/graph/plot_triad_types>` 

368 for visual examples of the triad types. 

369 

370 Parameters 

371 ---------- 

372 G : digraph 

373 A NetworkX DiGraph 

374 

375 Returns 

376 ------- 

377 tri_by_type : dict 

378 Dictionary with triad types as keys and lists of triads as values. 

379 

380 Examples 

381 -------- 

382 >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 1), (5, 6), (5, 4), (6, 7)]) 

383 >>> dict = nx.triads_by_type(G) 

384 >>> dict["120C"][0].edges() 

385 OutEdgeView([(1, 2), (1, 3), (2, 3), (3, 1)]) 

386 >>> dict["012"][0].edges() 

387 OutEdgeView([(1, 2)]) 

388 

389 References 

390 ---------- 

391 .. [1] Snijders, T. (2012). "Transitivity and triads." University of 

392 Oxford. 

393 https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf 

394 """ 

395 # num_triads = o * (o - 1) * (o - 2) // 6 

396 # if num_triads > TRIAD_LIMIT: print(WARNING) 

397 all_tri = all_triads(G) 

398 tri_by_type = defaultdict(list) 

399 for triad in all_tri: 

400 name = triad_type(triad) 

401 tri_by_type[name].append(triad) 

402 return tri_by_type 

403 

404 

405@not_implemented_for("undirected") 

406@nx._dispatchable 

407def triad_type(G): 

408 """Returns the sociological triad type for a triad. 

409 

410 Parameters 

411 ---------- 

412 G : digraph 

413 A NetworkX DiGraph with 3 nodes 

414 

415 Returns 

416 ------- 

417 triad_type : str 

418 A string identifying the triad type 

419 

420 Examples 

421 -------- 

422 >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) 

423 >>> nx.triad_type(G) 

424 '030C' 

425 >>> G.add_edge(1, 3) 

426 >>> nx.triad_type(G) 

427 '120C' 

428 

429 Notes 

430 ----- 

431 There can be 6 unique edges in a triad (order-3 DiGraph) (so 2^^6=64 unique 

432 triads given 3 nodes). These 64 triads each display exactly 1 of 16 

433 topologies of triads (topologies can be permuted). These topologies are 

434 identified by the following notation: 

435 

436 {m}{a}{n}{type} (for example: 111D, 210, 102) 

437 

438 Here: 

439 

440 {m} = number of mutual ties (takes 0, 1, 2, 3); a mutual tie is (0,1) 

441 AND (1,0) 

442 {a} = number of asymmetric ties (takes 0, 1, 2, 3); an asymmetric tie 

443 is (0,1) BUT NOT (1,0) or vice versa 

444 {n} = number of null ties (takes 0, 1, 2, 3); a null tie is NEITHER 

445 (0,1) NOR (1,0) 

446 {type} = a letter (takes U, D, C, T) corresponding to up, down, cyclical 

447 and transitive. This is only used for topologies that can have 

448 more than one form (eg: 021D and 021U). 

449 

450 References 

451 ---------- 

452 .. [1] Snijders, T. (2012). "Transitivity and triads." University of 

453 Oxford. 

454 https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf 

455 """ 

456 if not is_triad(G): 

457 raise nx.NetworkXAlgorithmError("G is not a triad (order-3 DiGraph)") 

458 num_edges = len(G.edges()) 

459 if num_edges == 0: 

460 return "003" 

461 elif num_edges == 1: 

462 return "012" 

463 elif num_edges == 2: 

464 e1, e2 = G.edges() 

465 if set(e1) == set(e2): 

466 return "102" 

467 elif e1[0] == e2[0]: 

468 return "021D" 

469 elif e1[1] == e2[1]: 

470 return "021U" 

471 elif e1[1] == e2[0] or e2[1] == e1[0]: 

472 return "021C" 

473 elif num_edges == 3: 

474 for e1, e2, e3 in permutations(G.edges(), 3): 

475 if set(e1) == set(e2): 

476 if e3[0] in e1: 

477 return "111U" 

478 # e3[1] in e1: 

479 return "111D" 

480 elif set(e1).symmetric_difference(set(e2)) == set(e3): 

481 if {e1[0], e2[0], e3[0]} == {e1[0], e2[0], e3[0]} == set(G.nodes()): 

482 return "030C" 

483 # e3 == (e1[0], e2[1]) and e2 == (e1[1], e3[1]): 

484 return "030T" 

485 elif num_edges == 4: 

486 for e1, e2, e3, e4 in permutations(G.edges(), 4): 

487 if set(e1) == set(e2): 

488 # identify pair of symmetric edges (which necessarily exists) 

489 if set(e3) == set(e4): 

490 return "201" 

491 if {e3[0]} == {e4[0]} == set(e3).intersection(set(e4)): 

492 return "120D" 

493 if {e3[1]} == {e4[1]} == set(e3).intersection(set(e4)): 

494 return "120U" 

495 if e3[1] == e4[0]: 

496 return "120C" 

497 elif num_edges == 5: 

498 return "210" 

499 elif num_edges == 6: 

500 return "300"