Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/algorithms/triads.py: 21%

146 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

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_triplets", 

17 "all_triads", 

18 "triads_by_type", 

19 "triad_type", 

20 "random_triad", 

21] 

22 

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

24#: 

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

26TRICODES = ( 

27 1, 

28 2, 

29 2, 

30 3, 

31 2, 

32 4, 

33 6, 

34 8, 

35 2, 

36 6, 

37 5, 

38 7, 

39 3, 

40 8, 

41 7, 

42 11, 

43 2, 

44 6, 

45 4, 

46 8, 

47 5, 

48 9, 

49 9, 

50 13, 

51 6, 

52 10, 

53 9, 

54 14, 

55 7, 

56 14, 

57 12, 

58 15, 

59 2, 

60 5, 

61 6, 

62 7, 

63 6, 

64 9, 

65 10, 

66 14, 

67 4, 

68 9, 

69 9, 

70 12, 

71 8, 

72 13, 

73 14, 

74 15, 

75 3, 

76 7, 

77 8, 

78 11, 

79 7, 

80 12, 

81 14, 

82 15, 

83 8, 

84 14, 

85 13, 

86 15, 

87 11, 

88 15, 

89 15, 

90 16, 

91) 

92 

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

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

95TRIAD_NAMES = ( 

96 "003", 

97 "012", 

98 "102", 

99 "021D", 

100 "021U", 

101 "021C", 

102 "111D", 

103 "111U", 

104 "030T", 

105 "030C", 

106 "201", 

107 "120D", 

108 "120U", 

109 "120C", 

110 "210", 

111 "300", 

112) 

113 

114 

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

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

117 

118 

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

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

121 

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

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

124 the binary representation of an integer. 

125 

126 """ 

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

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

129 

130 

131@not_implemented_for("undirected") 

132@nx._dispatch 

133def triadic_census(G, nodelist=None): 

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

135 

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

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

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

139 

140 Parameters 

141 ---------- 

142 G : digraph 

143 A NetworkX DiGraph 

144 nodelist : list 

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

146 

147 Returns 

148 ------- 

149 census : dict 

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

151 

152 Examples 

153 -------- 

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

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

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

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

158 ... 

159 003: 0 

160 012: 0 

161 102: 0 

162 021D: 0 

163 021U: 0 

164 021C: 0 

165 111D: 0 

166 111U: 0 

167 030T: 2 

168 030C: 2 

169 201: 0 

170 120D: 0 

171 120U: 0 

172 120C: 0 

173 210: 0 

174 300: 0 

175 

176 Notes 

177 ----- 

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

179 the graph. 

180 

181 Raises 

182 ------ 

183 ValueError 

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

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

186 

187 See also 

188 -------- 

189 triad_graph 

190 

191 References 

192 ---------- 

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

194 algorithm for large sparse networks with small maximum degree, 

195 University of Ljubljana, 

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

197 

198 """ 

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

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

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

202 

203 N = len(G) 

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

205 

206 # create an ordering of nodes with nodeset nodes first 

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

208 if Nnot: 

209 # add non-nodeset nodes later in the ordering 

210 not_nodeset = G.nodes - nodeset 

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

212 

213 # build all_neighbor dicts for easy counting 

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

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

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

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

218 

219 if Nnot: 

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

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

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

223 sgl_edges_outside = sgl // 2 

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

225 dbl_edges_outside = dbl // 2 

226 

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

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

229 # Main loop over nodes 

230 for v in nodeset: 

231 vnbrs = nbrs[v] 

232 dbl_vnbrs = dbl_nbrs[v] 

233 if Nnot: 

234 # set up counts of edges attached to v. 

235 sgl_unbrs_bdy = sgl_unbrs_out = dbl_unbrs_bdy = dbl_unbrs_out = 0 

236 for u in vnbrs: 

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

238 continue 

239 unbrs = nbrs[u] 

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

241 # Count connected triads. 

242 for w in neighbors: 

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

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

245 census[TRICODE_TO_NAME[code]] += 1 

246 

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

248 if u in dbl_vnbrs: 

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

250 else: 

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

252 

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

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

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

256 if Nnot and u not in nodeset: 

257 sgl_unbrs = sgl_nbrs[u] 

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

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

260 dbl_unbrs = dbl_nbrs[u] 

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

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

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

264 if Nnot: 

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

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

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

268 

269 # calculate null triads: "003" 

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

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

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

273 total_census = total_triangles - triangles_without_nodeset 

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

275 

276 return census 

277 

278 

279@nx._dispatch 

280def is_triad(G): 

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

282 

283 Parameters 

284 ---------- 

285 G : graph 

286 A NetworkX Graph 

287 

288 Returns 

289 ------- 

290 istriad : boolean 

291 Whether G is a valid triad 

292 

293 Examples 

294 -------- 

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

296 >>> nx.is_triad(G) 

297 True 

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

299 >>> nx.is_triad(G) 

300 False 

301 """ 

302 if isinstance(G, nx.Graph): 

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

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

305 return True 

306 return False 

307 

308 

309@not_implemented_for("undirected") 

310@nx._dispatch 

311def all_triplets(G): 

312 """Returns a generator of all possible sets of 3 nodes in a DiGraph. 

313 

314 Parameters 

315 ---------- 

316 G : digraph 

317 A NetworkX DiGraph 

318 

319 Returns 

320 ------- 

321 triplets : generator of 3-tuples 

322 Generator of tuples of 3 nodes 

323 

324 Examples 

325 -------- 

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

327 >>> list(nx.all_triplets(G)) 

328 [(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)] 

329 

330 """ 

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

332 return triplets 

333 

334 

335@not_implemented_for("undirected") 

336@nx._dispatch 

337def all_triads(G): 

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

339 

340 Parameters 

341 ---------- 

342 G : digraph 

343 A NetworkX DiGraph 

344 

345 Returns 

346 ------- 

347 all_triads : generator of DiGraphs 

348 Generator of triads (order-3 DiGraphs) 

349 

350 Examples 

351 -------- 

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

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

354 ... print(triad.edges) 

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

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

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

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

359 

360 """ 

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

362 for triplet in triplets: 

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

364 

365 

366@not_implemented_for("undirected") 

367@nx._dispatch 

368def triads_by_type(G): 

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

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

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

372 are as follows: 

373 

374 - 003: 1, 2, 3 

375 - 012: 1 -> 2, 3 

376 - 102: 1 <-> 2, 3 

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

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

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

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

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

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

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

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

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

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

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

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

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

390 

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

392 for visual examples of the triad types. 

393 

394 Parameters 

395 ---------- 

396 G : digraph 

397 A NetworkX DiGraph 

398 

399 Returns 

400 ------- 

401 tri_by_type : dict 

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

403 

404 Examples 

405 -------- 

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

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

408 >>> dict['120C'][0].edges() 

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

410 >>> dict['012'][0].edges() 

411 OutEdgeView([(1, 2)]) 

412 

413 References 

414 ---------- 

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

416 Oxford. 

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

418 """ 

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

420 # if num_triads > TRIAD_LIMIT: print(WARNING) 

421 all_tri = all_triads(G) 

422 tri_by_type = defaultdict(list) 

423 for triad in all_tri: 

424 name = triad_type(triad) 

425 tri_by_type[name].append(triad) 

426 return tri_by_type 

427 

428 

429@not_implemented_for("undirected") 

430@nx._dispatch 

431def triad_type(G): 

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

433 

434 Parameters 

435 ---------- 

436 G : digraph 

437 A NetworkX DiGraph with 3 nodes 

438 

439 Returns 

440 ------- 

441 triad_type : str 

442 A string identifying the triad type 

443 

444 Examples 

445 -------- 

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

447 >>> nx.triad_type(G) 

448 '030C' 

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

450 >>> nx.triad_type(G) 

451 '120C' 

452 

453 Notes 

454 ----- 

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

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

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

458 identified by the following notation: 

459 

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

461 

462 Here: 

463 

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

465 AND (1,0) 

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

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

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

469 (0,1) NOR (1,0) 

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

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

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

473 

474 References 

475 ---------- 

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

477 Oxford. 

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

479 """ 

480 if not is_triad(G): 

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

482 num_edges = len(G.edges()) 

483 if num_edges == 0: 

484 return "003" 

485 elif num_edges == 1: 

486 return "012" 

487 elif num_edges == 2: 

488 e1, e2 = G.edges() 

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

490 return "102" 

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

492 return "021D" 

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

494 return "021U" 

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

496 return "021C" 

497 elif num_edges == 3: 

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

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

500 if e3[0] in e1: 

501 return "111U" 

502 # e3[1] in e1: 

503 return "111D" 

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

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

506 return "030C" 

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

508 return "030T" 

509 elif num_edges == 4: 

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

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

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

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

514 return "201" 

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

516 return "120D" 

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

518 return "120U" 

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

520 return "120C" 

521 elif num_edges == 5: 

522 return "210" 

523 elif num_edges == 6: 

524 return "300" 

525 

526 

527@not_implemented_for("undirected") 

528@py_random_state(1) 

529@nx._dispatch 

530def random_triad(G, seed=None): 

531 """Returns a random triad from a directed graph. 

532 

533 Parameters 

534 ---------- 

535 G : digraph 

536 A NetworkX DiGraph 

537 seed : integer, random_state, or None (default) 

538 Indicator of random number generation state. 

539 See :ref:`Randomness<randomness>`. 

540 

541 Returns 

542 ------- 

543 G2 : subgraph 

544 A randomly selected triad (order-3 NetworkX DiGraph) 

545 

546 Raises 

547 ------ 

548 NetworkXError 

549 If the input Graph has less than 3 nodes. 

550 

551 Examples 

552 -------- 

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

554 >>> triad = nx.random_triad(G, seed=1) 

555 >>> triad.edges 

556 OutEdgeView([(1, 2)]) 

557 

558 """ 

559 if len(G) < 3: 

560 raise nx.NetworkXError( 

561 f"G needs at least 3 nodes to form a triad; (it has {len(G)} nodes)" 

562 ) 

563 nodes = seed.sample(list(G.nodes()), 3) 

564 G2 = G.subgraph(nodes) 

565 return G2