Given this graph:
import igraph as ig
g=ig.Graph.Erdos_Renyi(10, 0.5, directed=True)
we can find its triad census easily with the triad_census function:
tc = g.triad_census()
If we print 'tc', we get something like:
003 : -2147483648 | 012 : 5 | 102 : 2 | 021D: 4
021U: 6 | 021C: 8 | 111D: 9 | 111U: 14
030T: 5 | 030C: 3 | 201 : 6 | 120D: 9
120U: 5 | 120C: 21 | 210 : 18 | 300 : 4
That is, for every triad type, we have the number of times it was found in the graph.
Unlike "triad census", a "triad list" would give not only the number of times that triad was found, but the participants nodes in every occurrence. As far as I know, the problem here is that "triad listing algorithms" are not necessarily the same ones than "triad census algorithms", the second being less computationally expensive.
I tried by looking at isomorphisms, defining every triad and then searching them in the graph:
# Set up the 16 possible triads
triad_dict=dict()
#003 A,B,C, the empty graph.
triad = ig.Graph(n = 3, directed=True)
triad_dict['003'] = triad
#012 A->B, C, the graph with a single directed edge.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1)])
triad_dict['012'] = triad
#102 A<->B, C, the graph with a mutual connection between two vertices.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(0,2)])
triad_dict['102'] = triad
#021D A<-B->C, the out-star.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(0,2)])
triad_dict['021D'] = triad
#021U A->B<-C, the in-star.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(1,0),
(2,0)])
triad_dict['021U'] = triad
#021C A->B->C, directed line.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(1,2)])
triad_dict['021C'] = triad
#111D A<->B<-C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(0,2)])
triad_dict['111D'] = triad
#111U A<->B->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(0,2),(2,0)])
triad_dict['111U'] = triad
#030T A->B<-C, A->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(2,1),
(0,2)])
triad_dict['030T'] = triad
#030C A<-B<-C, A->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(1,0),
(2,1),
(0,2)])
triad_dict['030C'] = triad
#201 A<->B<->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),(1,0),
(1,2),(2,1)])
triad_dict['201'] = triad
#120D A<-B->C, A<->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(0,2),
(1,2), (2,1)])
triad_dict['120D'] = triad
#120U A->B<-C, A<->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(2,1),
(0,2), (2,0)])
triad_dict['120U'] = triad
#120C A->B->C, A<->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(1,2),
(0,2), (2,0)])
triad_dict['120C'] = triad
#210 A->B<->C, A<->C.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1),
(1,2), (2,1),
(0,2), (2,0)])
triad_dict['210'] = triad
#300 A<->B<->C, A<->C, the complete graph.
triad = ig.Graph(n = 3, directed=True)
triad.add_edges([(0,1), (1,0),
(1,2), (2,1),
(0,2), (2,0)])
triad_dict['300'] = triad
seq = ['300',
'210',
'120C', '120U', '120D', '201',
'030C', '030T','111U','111D',
'021C','021U','021D','102',
'012',
'003']
#Search isomorphisms for every triad
isomorphisms = dict()
for key in seq:
isomorphisms[key] = g.get_subisomorphisms_vf2(triad_dict[key])
However, if we have A<->B<->C, it will be associated to several triads:
A<->B<->C
A->B->C
A<->B->C
etc.
and I only want to consider the more complete triad (the one with more edges) and discard its sub-triads. We could clean the duplicated triads in the isomorphisms dictionary, going from higher order (more edges) to lower order level, for instance:
#Search isomorphisms for every triad
seen = []
for key in seq:
isos = isomorphisms[key]
isos = [i for i in isos if i not in seen]
isomorphisms[key] = isos
seen.extend(isos)
However, this is not an option because the graph represents a set of conversations between nodes. If two different conversations happened:
A->B->C->D (1)
A->B->C (2)
(the graph is multiedged)
the script will delete (1) as it will think that (2) is the subgraph corresponding to (1), but it is not necessarily true as in this case they represent different conversations.