triads.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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. from collections import defaultdict
  7. from itertools import combinations, permutations
  8. import networkx as nx
  9. from networkx.utils import not_implemented_for, py_random_state
  10. __all__ = [
  11. "triadic_census",
  12. "is_triad",
  13. "all_triads",
  14. "triads_by_type",
  15. "triad_type",
  16. ]
  17. #: The integer codes representing each type of triad.
  18. #:
  19. #: Triads that are the same up to symmetry have the same code.
  20. TRICODES = (
  21. 1,
  22. 2,
  23. 2,
  24. 3,
  25. 2,
  26. 4,
  27. 6,
  28. 8,
  29. 2,
  30. 6,
  31. 5,
  32. 7,
  33. 3,
  34. 8,
  35. 7,
  36. 11,
  37. 2,
  38. 6,
  39. 4,
  40. 8,
  41. 5,
  42. 9,
  43. 9,
  44. 13,
  45. 6,
  46. 10,
  47. 9,
  48. 14,
  49. 7,
  50. 14,
  51. 12,
  52. 15,
  53. 2,
  54. 5,
  55. 6,
  56. 7,
  57. 6,
  58. 9,
  59. 10,
  60. 14,
  61. 4,
  62. 9,
  63. 9,
  64. 12,
  65. 8,
  66. 13,
  67. 14,
  68. 15,
  69. 3,
  70. 7,
  71. 8,
  72. 11,
  73. 7,
  74. 12,
  75. 14,
  76. 15,
  77. 8,
  78. 14,
  79. 13,
  80. 15,
  81. 11,
  82. 15,
  83. 15,
  84. 16,
  85. )
  86. #: The names of each type of triad. The order of the elements is
  87. #: important: it corresponds to the tricodes given in :data:`TRICODES`.
  88. TRIAD_NAMES = (
  89. "003",
  90. "012",
  91. "102",
  92. "021D",
  93. "021U",
  94. "021C",
  95. "111D",
  96. "111U",
  97. "030T",
  98. "030C",
  99. "201",
  100. "120D",
  101. "120U",
  102. "120C",
  103. "210",
  104. "300",
  105. )
  106. #: A dictionary mapping triad code to triad name.
  107. TRICODE_TO_NAME = {i: TRIAD_NAMES[code - 1] for i, code in enumerate(TRICODES)}
  108. def _tricode(G, v, u, w):
  109. """Returns the integer code of the given triad.
  110. This is some fancy magic that comes from Batagelj and Mrvar's paper. It
  111. treats each edge joining a pair of `v`, `u`, and `w` as a bit in
  112. the binary representation of an integer.
  113. """
  114. combos = ((v, u, 1), (u, v, 2), (v, w, 4), (w, v, 8), (u, w, 16), (w, u, 32))
  115. return sum(x for u, v, x in combos if v in G[u])
  116. @not_implemented_for("undirected")
  117. @nx._dispatchable
  118. def triadic_census(G, nodelist=None):
  119. """Determines the triadic census of a directed graph.
  120. The triadic census is a count of how many of the 16 possible types of
  121. triads are present in a directed graph. If a list of nodes is passed, then
  122. only those triads are taken into account which have elements of nodelist in them.
  123. Parameters
  124. ----------
  125. G : digraph
  126. A NetworkX DiGraph
  127. nodelist : list
  128. List of nodes for which you want to calculate triadic census
  129. Returns
  130. -------
  131. census : dict
  132. Dictionary with triad type as keys and number of occurrences as values.
  133. Examples
  134. --------
  135. >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)])
  136. >>> triadic_census = nx.triadic_census(G)
  137. >>> for key, value in triadic_census.items():
  138. ... print(f"{key}: {value}")
  139. 003: 0
  140. 012: 0
  141. 102: 0
  142. 021D: 0
  143. 021U: 0
  144. 021C: 0
  145. 111D: 0
  146. 111U: 0
  147. 030T: 2
  148. 030C: 2
  149. 201: 0
  150. 120D: 0
  151. 120U: 0
  152. 120C: 0
  153. 210: 0
  154. 300: 0
  155. Notes
  156. -----
  157. This algorithm has complexity $O(m)$ where $m$ is the number of edges in
  158. the graph.
  159. For undirected graphs, the triadic census can be computed by first converting
  160. the graph into a directed graph using the ``G.to_directed()`` method.
  161. After this conversion, only the triad types 003, 102, 201 and 300 will be
  162. present in the undirected scenario.
  163. Raises
  164. ------
  165. ValueError
  166. If `nodelist` contains duplicate nodes or nodes not in `G`.
  167. If you want to ignore this you can preprocess with `set(nodelist) & G.nodes`
  168. See also
  169. --------
  170. triad_graph
  171. References
  172. ----------
  173. .. [1] Vladimir Batagelj and Andrej Mrvar, A subquadratic triad census
  174. algorithm for large sparse networks with small maximum degree,
  175. University of Ljubljana,
  176. http://vlado.fmf.uni-lj.si/pub/networks/doc/triads/triads.pdf
  177. """
  178. nodeset = set(G.nbunch_iter(nodelist))
  179. if nodelist is not None and len(nodelist) != len(nodeset):
  180. raise ValueError("nodelist includes duplicate nodes or nodes not in G")
  181. N = len(G)
  182. Nnot = N - len(nodeset) # can signal special counting for subset of nodes
  183. # create an ordering of nodes with nodeset nodes first
  184. m = {n: i for i, n in enumerate(nodeset)}
  185. if Nnot:
  186. # add non-nodeset nodes later in the ordering
  187. not_nodeset = G.nodes - nodeset
  188. m.update((n, i + N) for i, n in enumerate(not_nodeset))
  189. # build all_neighbor dicts for easy counting
  190. # After Python 3.8 can leave off these keys(). Speedup also using G._pred
  191. # nbrs = {n: G._pred[n].keys() | G._succ[n].keys() for n in G}
  192. nbrs = {n: G.pred[n].keys() | G.succ[n].keys() for n in G}
  193. dbl_nbrs = {n: G.pred[n].keys() & G.succ[n].keys() for n in G}
  194. if Nnot:
  195. sgl_nbrs = {n: G.pred[n].keys() ^ G.succ[n].keys() for n in not_nodeset}
  196. # find number of edges not incident to nodes in nodeset
  197. sgl = sum(1 for n in not_nodeset for nbr in sgl_nbrs[n] if nbr not in nodeset)
  198. sgl_edges_outside = sgl // 2
  199. dbl = sum(1 for n in not_nodeset for nbr in dbl_nbrs[n] if nbr not in nodeset)
  200. dbl_edges_outside = dbl // 2
  201. # Initialize the count for each triad to be zero.
  202. census = {name: 0 for name in TRIAD_NAMES}
  203. # Main loop over nodes
  204. for v in nodeset:
  205. vnbrs = nbrs[v]
  206. dbl_vnbrs = dbl_nbrs[v]
  207. if Nnot:
  208. # set up counts of edges attached to v.
  209. sgl_unbrs_bdy = sgl_unbrs_out = dbl_unbrs_bdy = dbl_unbrs_out = 0
  210. for u in vnbrs:
  211. if m[u] <= m[v]:
  212. continue
  213. unbrs = nbrs[u]
  214. neighbors = (vnbrs | unbrs) - {u, v}
  215. # Count connected triads.
  216. for w in neighbors:
  217. if m[u] < m[w] or (m[v] < m[w] < m[u] and v not in nbrs[w]):
  218. code = _tricode(G, v, u, w)
  219. census[TRICODE_TO_NAME[code]] += 1
  220. # Use a formula for dyadic triads with edge incident to v
  221. if u in dbl_vnbrs:
  222. census["102"] += N - len(neighbors) - 2
  223. else:
  224. census["012"] += N - len(neighbors) - 2
  225. # Count edges attached to v. Subtract later to get triads with v isolated
  226. # _out are (u,unbr) for unbrs outside boundary of nodeset
  227. # _bdy are (u,unbr) for unbrs on boundary of nodeset (get double counted)
  228. if Nnot and u not in nodeset:
  229. sgl_unbrs = sgl_nbrs[u]
  230. sgl_unbrs_bdy += len(sgl_unbrs & vnbrs - nodeset)
  231. sgl_unbrs_out += len(sgl_unbrs - vnbrs - nodeset)
  232. dbl_unbrs = dbl_nbrs[u]
  233. dbl_unbrs_bdy += len(dbl_unbrs & vnbrs - nodeset)
  234. dbl_unbrs_out += len(dbl_unbrs - vnbrs - nodeset)
  235. # if nodeset == G.nodes, skip this b/c we will find the edge later.
  236. if Nnot:
  237. # Count edges outside nodeset not connected with v (v isolated triads)
  238. census["012"] += sgl_edges_outside - (sgl_unbrs_out + sgl_unbrs_bdy // 2)
  239. census["102"] += dbl_edges_outside - (dbl_unbrs_out + dbl_unbrs_bdy // 2)
  240. # calculate null triads: "003"
  241. # null triads = total number of possible triads - all found triads
  242. total_triangles = (N * (N - 1) * (N - 2)) // 6
  243. triangles_without_nodeset = (Nnot * (Nnot - 1) * (Nnot - 2)) // 6
  244. total_census = total_triangles - triangles_without_nodeset
  245. census["003"] = total_census - sum(census.values())
  246. return census
  247. @nx._dispatchable
  248. def is_triad(G):
  249. """Returns True if the graph G is a triad, else False.
  250. Parameters
  251. ----------
  252. G : graph
  253. A NetworkX Graph
  254. Returns
  255. -------
  256. istriad : boolean
  257. Whether G is a valid triad
  258. Examples
  259. --------
  260. >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
  261. >>> nx.is_triad(G)
  262. True
  263. >>> G.add_edge(0, 1)
  264. >>> nx.is_triad(G)
  265. False
  266. """
  267. if isinstance(G, nx.Graph):
  268. if G.order() == 3 and nx.is_directed(G):
  269. if not any((n, n) in G.edges() for n in G.nodes()):
  270. return True
  271. return False
  272. @not_implemented_for("undirected")
  273. @nx._dispatchable(returns_graph=True)
  274. def all_triads(G):
  275. """A generator of all possible triads in G.
  276. Parameters
  277. ----------
  278. G : digraph
  279. A NetworkX DiGraph
  280. Returns
  281. -------
  282. all_triads : generator of DiGraphs
  283. Generator of triads (order-3 DiGraphs)
  284. Examples
  285. --------
  286. >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)])
  287. >>> for triad in nx.all_triads(G):
  288. ... print(triad.edges)
  289. [(1, 2), (2, 3), (3, 1)]
  290. [(1, 2), (4, 1), (4, 2)]
  291. [(3, 1), (3, 4), (4, 1)]
  292. [(2, 3), (3, 4), (4, 2)]
  293. """
  294. triplets = combinations(G.nodes(), 3)
  295. for triplet in triplets:
  296. yield G.subgraph(triplet).copy()
  297. @not_implemented_for("undirected")
  298. @nx._dispatchable
  299. def triads_by_type(G):
  300. """Returns a list of all triads for each triad type in a directed graph.
  301. There are exactly 16 different types of triads possible. Suppose 1, 2, 3 are three
  302. nodes, they will be classified as a particular triad type if their connections
  303. are as follows:
  304. - 003: 1, 2, 3
  305. - 012: 1 -> 2, 3
  306. - 102: 1 <-> 2, 3
  307. - 021D: 1 <- 2 -> 3
  308. - 021U: 1 -> 2 <- 3
  309. - 021C: 1 -> 2 -> 3
  310. - 111D: 1 <-> 2 <- 3
  311. - 111U: 1 <-> 2 -> 3
  312. - 030T: 1 -> 2 -> 3, 1 -> 3
  313. - 030C: 1 <- 2 <- 3, 1 -> 3
  314. - 201: 1 <-> 2 <-> 3
  315. - 120D: 1 <- 2 -> 3, 1 <-> 3
  316. - 120U: 1 -> 2 <- 3, 1 <-> 3
  317. - 120C: 1 -> 2 -> 3, 1 <-> 3
  318. - 210: 1 -> 2 <-> 3, 1 <-> 3
  319. - 300: 1 <-> 2 <-> 3, 1 <-> 3
  320. Refer to the :doc:`example gallery </auto_examples/graph/plot_triad_types>`
  321. for visual examples of the triad types.
  322. Parameters
  323. ----------
  324. G : digraph
  325. A NetworkX DiGraph
  326. Returns
  327. -------
  328. tri_by_type : dict
  329. Dictionary with triad types as keys and lists of triads as values.
  330. Examples
  331. --------
  332. >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 1), (5, 6), (5, 4), (6, 7)])
  333. >>> dict = nx.triads_by_type(G)
  334. >>> dict["120C"][0].edges()
  335. OutEdgeView([(1, 2), (1, 3), (2, 3), (3, 1)])
  336. >>> dict["012"][0].edges()
  337. OutEdgeView([(1, 2)])
  338. References
  339. ----------
  340. .. [1] Snijders, T. (2012). "Transitivity and triads." University of
  341. Oxford.
  342. https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf
  343. """
  344. # num_triads = o * (o - 1) * (o - 2) // 6
  345. # if num_triads > TRIAD_LIMIT: print(WARNING)
  346. all_tri = all_triads(G)
  347. tri_by_type = defaultdict(list)
  348. for triad in all_tri:
  349. name = triad_type(triad)
  350. tri_by_type[name].append(triad)
  351. return tri_by_type
  352. @not_implemented_for("undirected")
  353. @nx._dispatchable
  354. def triad_type(G):
  355. """Returns the sociological triad type for a triad.
  356. Parameters
  357. ----------
  358. G : digraph
  359. A NetworkX DiGraph with 3 nodes
  360. Returns
  361. -------
  362. triad_type : str
  363. A string identifying the triad type
  364. Examples
  365. --------
  366. >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
  367. >>> nx.triad_type(G)
  368. '030C'
  369. >>> G.add_edge(1, 3)
  370. >>> nx.triad_type(G)
  371. '120C'
  372. Notes
  373. -----
  374. There can be 6 unique edges in a triad (order-3 DiGraph) (so 2^^6=64 unique
  375. triads given 3 nodes). These 64 triads each display exactly 1 of 16
  376. topologies of triads (topologies can be permuted). These topologies are
  377. identified by the following notation:
  378. {m}{a}{n}{type} (for example: 111D, 210, 102)
  379. Here:
  380. {m} = number of mutual ties (takes 0, 1, 2, 3); a mutual tie is (0,1)
  381. AND (1,0)
  382. {a} = number of asymmetric ties (takes 0, 1, 2, 3); an asymmetric tie
  383. is (0,1) BUT NOT (1,0) or vice versa
  384. {n} = number of null ties (takes 0, 1, 2, 3); a null tie is NEITHER
  385. (0,1) NOR (1,0)
  386. {type} = a letter (takes U, D, C, T) corresponding to up, down, cyclical
  387. and transitive. This is only used for topologies that can have
  388. more than one form (eg: 021D and 021U).
  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. if not is_triad(G):
  396. raise nx.NetworkXAlgorithmError("G is not a triad (order-3 DiGraph)")
  397. num_edges = len(G.edges())
  398. if num_edges == 0:
  399. return "003"
  400. elif num_edges == 1:
  401. return "012"
  402. elif num_edges == 2:
  403. e1, e2 = G.edges()
  404. if set(e1) == set(e2):
  405. return "102"
  406. elif e1[0] == e2[0]:
  407. return "021D"
  408. elif e1[1] == e2[1]:
  409. return "021U"
  410. elif e1[1] == e2[0] or e2[1] == e1[0]:
  411. return "021C"
  412. elif num_edges == 3:
  413. for e1, e2, e3 in permutations(G.edges(), 3):
  414. if set(e1) == set(e2):
  415. if e3[0] in e1:
  416. return "111U"
  417. # e3[1] in e1:
  418. return "111D"
  419. elif set(e1).symmetric_difference(set(e2)) == set(e3):
  420. if {e1[0], e2[0], e3[0]} == {e1[0], e2[0], e3[0]} == set(G.nodes()):
  421. return "030C"
  422. # e3 == (e1[0], e2[1]) and e2 == (e1[1], e3[1]):
  423. return "030T"
  424. elif num_edges == 4:
  425. for e1, e2, e3, e4 in permutations(G.edges(), 4):
  426. if set(e1) == set(e2):
  427. # identify pair of symmetric edges (which necessarily exists)
  428. if set(e3) == set(e4):
  429. return "201"
  430. if {e3[0]} == {e4[0]} == set(e3).intersection(set(e4)):
  431. return "120D"
  432. if {e3[1]} == {e4[1]} == set(e3).intersection(set(e4)):
  433. return "120U"
  434. if e3[1] == e4[0]:
  435. return "120C"
  436. elif num_edges == 5:
  437. return "210"
  438. elif num_edges == 6:
  439. return "300"