Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 9f07c64

Browse files
feat: GraphDiffAnalyzer — compare two graphs structurally
Adds GraphDiffAnalyzer that compares two JUNG graphs and reports: - Added/removed/common nodes and edges - Jaccard similarity for both nodes and edges - Degree change detection for shared nodes - Graph edit distance (total additions + removals) Useful for tracking network evolution across snapshots, comparing observed vs predicted structures, and detecting topology changes in communication/IMEI graphs. Includes full test suite (GraphDiffAnalyzerTest). Usage: GraphDiffAnalyzer analyzer = new GraphDiffAnalyzer(graphA, graphB); DiffResult diff = analyzer.computeDiff(); System.out.println(diff.getSummary());
1 parent e6505c0 commit 9f07c64

3 files changed

Lines changed: 1193 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
5+
import java.util.*;
6+
7+
/**
8+
* Compares two graphs and computes structural differences: added, removed,
9+
* and common nodes and edges, plus similarity metrics. Useful for analyzing
10+
* how a network evolves over time or comparing alternative network models.
11+
*
12+
* <h3>Use Cases</h3>
13+
* <ul>
14+
* <li>Track network evolution across snapshots (before/after)</li>
15+
* <li>Compare observed vs. predicted network structures</li>
16+
* <li>Detect topology changes in communication networks</li>
17+
* <li>Measure similarity between two social/IMEI graphs</li>
18+
* </ul>
19+
*
20+
* <h3>Metrics</h3>
21+
* <ul>
22+
* <li><b>Jaccard Similarity (nodes):</b> |intersection| / |union|</li>
23+
* <li><b>Jaccard Similarity (edges):</b> same, based on endpoint pairs</li>
24+
* <li><b>Edit Distance:</b> total additions + removals to transform A into B</li>
25+
* </ul>
26+
*
27+
* @author zalenix
28+
*/
29+
public class GraphDiffAnalyzer {
30+
31+
private final Graph<String, edge> graphA;
32+
private final Graph<String, edge> graphB;
33+
34+
/**
35+
* Create a diff analyzer comparing graphA (baseline) to graphB (target).
36+
*
37+
* @param graphA the baseline graph
38+
* @param graphB the target graph to compare against
39+
* @throws IllegalArgumentException if either graph is null
40+
*/
41+
public GraphDiffAnalyzer(Graph<String, edge> graphA, Graph<String, edge> graphB) {
42+
if (graphA == null || graphB == null) {
43+
throw new IllegalArgumentException("Both graphs must not be null");
44+
}
45+
this.graphA = graphA;
46+
this.graphB = graphB;
47+
}
48+
49+
// ── Result class ────────────────────────────────────────────
50+
51+
/**
52+
* Holds the complete diff result between two graphs.
53+
*/
54+
public static class DiffResult {
55+
private final Set<String> addedNodes;
56+
private final Set<String> removedNodes;
57+
private final Set<String> commonNodes;
58+
private final List<EdgeDiff> addedEdges;
59+
private final List<EdgeDiff> removedEdges;
60+
private final List<EdgeDiff> commonEdges;
61+
private final double nodeJaccard;
62+
private final double edgeJaccard;
63+
64+
public DiffResult(Set<String> addedNodes, Set<String> removedNodes,
65+
Set<String> commonNodes, List<EdgeDiff> addedEdges,
66+
List<EdgeDiff> removedEdges, List<EdgeDiff> commonEdges,
67+
double nodeJaccard, double edgeJaccard) {
68+
this.addedNodes = Collections.unmodifiableSet(addedNodes);
69+
this.removedNodes = Collections.unmodifiableSet(removedNodes);
70+
this.commonNodes = Collections.unmodifiableSet(commonNodes);
71+
this.addedEdges = Collections.unmodifiableList(addedEdges);
72+
this.removedEdges = Collections.unmodifiableList(removedEdges);
73+
this.commonEdges = Collections.unmodifiableList(commonEdges);
74+
this.nodeJaccard = nodeJaccard;
75+
this.edgeJaccard = edgeJaccard;
76+
}
77+
78+
/** Nodes present in B but not in A. */
79+
public Set<String> getAddedNodes() { return addedNodes; }
80+
81+
/** Nodes present in A but not in B. */
82+
public Set<String> getRemovedNodes() { return removedNodes; }
83+
84+
/** Nodes present in both A and B. */
85+
public Set<String> getCommonNodes() { return commonNodes; }
86+
87+
/** Edges present in B but not in A. */
88+
public List<EdgeDiff> getAddedEdges() { return addedEdges; }
89+
90+
/** Edges present in A but not in B. */
91+
public List<EdgeDiff> getRemovedEdges() { return removedEdges; }
92+
93+
/** Edges present in both A and B. */
94+
public List<EdgeDiff> getCommonEdges() { return commonEdges; }
95+
96+
/** Jaccard similarity of node sets: |A∩B| / |A∪B|. */
97+
public double getNodeJaccard() { return nodeJaccard; }
98+
99+
/** Jaccard similarity of edge sets: |A∩B| / |A∪B|. */
100+
public double getEdgeJaccard() { return edgeJaccard; }
101+
102+
/** True if the two graphs are structurally identical. */
103+
public boolean isIdentical() {
104+
return addedNodes.isEmpty() && removedNodes.isEmpty()
105+
&& addedEdges.isEmpty() && removedEdges.isEmpty();
106+
}
107+
108+
/**
109+
* Summary string describing the diff.
110+
*/
111+
public String getSummary() {
112+
StringBuilder sb = new StringBuilder();
113+
sb.append("=== Graph Diff Summary ===\n");
114+
sb.append(String.format("Nodes: %d added, %d removed, %d common (Jaccard: %.4f)%n",
115+
addedNodes.size(), removedNodes.size(), commonNodes.size(), nodeJaccard));
116+
sb.append(String.format("Edges: %d added, %d removed, %d common (Jaccard: %.4f)%n",
117+
addedEdges.size(), removedEdges.size(), commonEdges.size(), edgeJaccard));
118+
if (isIdentical()) {
119+
sb.append("Graphs are structurally identical.\n");
120+
}
121+
return sb.toString();
122+
}
123+
}
124+
125+
/**
126+
* Represents an edge for diff purposes (endpoint pair, normalized order
127+
* for undirected comparison).
128+
*/
129+
public static class EdgeDiff {
130+
private final String vertex1;
131+
private final String vertex2;
132+
private final String edgeKey;
133+
134+
public EdgeDiff(String v1, String v2) {
135+
if (v1.compareTo(v2) <= 0) {
136+
this.vertex1 = v1;
137+
this.vertex2 = v2;
138+
} else {
139+
this.vertex1 = v2;
140+
this.vertex2 = v1;
141+
}
142+
this.edgeKey = this.vertex1 + "-" + this.vertex2;
143+
}
144+
145+
public String getVertex1() { return vertex1; }
146+
public String getVertex2() { return vertex2; }
147+
public String getEdgeKey() { return edgeKey; }
148+
149+
@Override
150+
public boolean equals(Object o) {
151+
if (this == o) return true;
152+
if (o == null || getClass() != o.getClass()) return false;
153+
EdgeDiff that = (EdgeDiff) o;
154+
return edgeKey.equals(that.edgeKey);
155+
}
156+
157+
@Override
158+
public int hashCode() {
159+
return edgeKey.hashCode();
160+
}
161+
162+
@Override
163+
public String toString() {
164+
return vertex1 + " <-> " + vertex2;
165+
}
166+
}
167+
168+
// ── Analysis ────────────────────────────────────────────────
169+
170+
/**
171+
* Compute the full diff between graphA and graphB.
172+
*
173+
* @return a DiffResult with all differences and similarity scores
174+
*/
175+
public DiffResult computeDiff() {
176+
Set<String> nodesA = new HashSet<>(graphA.getVertices());
177+
Set<String> nodesB = new HashSet<>(graphB.getVertices());
178+
179+
Set<String> addedNodes = new TreeSet<>(nodesB);
180+
addedNodes.removeAll(nodesA);
181+
182+
Set<String> removedNodes = new TreeSet<>(nodesA);
183+
removedNodes.removeAll(nodesB);
184+
185+
Set<String> commonNodes = new TreeSet<>(nodesA);
186+
commonNodes.retainAll(nodesB);
187+
188+
Set<EdgeDiff> edgesA = extractEdges(graphA);
189+
Set<EdgeDiff> edgesB = extractEdges(graphB);
190+
191+
Set<EdgeDiff> addedEdgeSet = new HashSet<>(edgesB);
192+
addedEdgeSet.removeAll(edgesA);
193+
194+
Set<EdgeDiff> removedEdgeSet = new HashSet<>(edgesA);
195+
removedEdgeSet.removeAll(edgesB);
196+
197+
Set<EdgeDiff> commonEdgeSet = new HashSet<>(edgesA);
198+
commonEdgeSet.retainAll(edgesB);
199+
200+
double nodeJaccard = jaccard(nodesA.size(), nodesB.size(), commonNodes.size());
201+
double edgeJaccard = jaccard(edgesA.size(), edgesB.size(), commonEdgeSet.size());
202+
203+
return new DiffResult(
204+
addedNodes, removedNodes, commonNodes,
205+
new ArrayList<>(addedEdgeSet),
206+
new ArrayList<>(removedEdgeSet),
207+
new ArrayList<>(commonEdgeSet),
208+
nodeJaccard, edgeJaccard
209+
);
210+
}
211+
212+
/**
213+
* Find nodes that exist in both graphs but whose degree changed.
214+
*
215+
* @return map of node to [degreeInA, degreeInB] for changed nodes
216+
*/
217+
public Map<String, int[]> findDegreeChanges() {
218+
Set<String> common = new HashSet<>(graphA.getVertices());
219+
common.retainAll(graphB.getVertices());
220+
221+
Map<String, int[]> changes = new TreeMap<>();
222+
for (String node : common) {
223+
int degA = graphA.degree(node);
224+
int degB = graphB.degree(node);
225+
if (degA != degB) {
226+
changes.put(node, new int[]{degA, degB});
227+
}
228+
}
229+
return changes;
230+
}
231+
232+
/**
233+
* Compute the edit distance: total node/edge additions + removals
234+
* needed to transform A into B.
235+
*
236+
* @return the graph edit distance
237+
*/
238+
public int computeEditDistance() {
239+
DiffResult diff = computeDiff();
240+
return diff.getAddedNodes().size() + diff.getRemovedNodes().size()
241+
+ diff.getAddedEdges().size() + diff.getRemovedEdges().size();
242+
}
243+
244+
// ── Helpers ─────────────────────────────────────────────────
245+
246+
private Set<EdgeDiff> extractEdges(Graph<String, edge> g) {
247+
Set<EdgeDiff> edges = new HashSet<>();
248+
for (edge e : g.getEdges()) {
249+
Collection<String> endpoints = g.getEndpoints(e);
250+
if (endpoints != null && endpoints.size() == 2) {
251+
Iterator<String> it = endpoints.iterator();
252+
edges.add(new EdgeDiff(it.next(), it.next()));
253+
}
254+
}
255+
return edges;
256+
}
257+
258+
private double jaccard(int sizeA, int sizeB, int intersection) {
259+
int union = sizeA + sizeB - intersection;
260+
if (union == 0) return 1.0;
261+
return (double) intersection / union;
262+
}
263+
}

0 commit comments

Comments
 (0)