Finding fraud rings with graphs
A fraud ring looks completely normal one account at a time — the tell only appears when you draw the connections between accounts and look at the shape.
- 9 min read
- 3 reading levels
- Published
Read these first
On this page 5
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
A fraud ring is a group of accounts that look fine alone, but suspicious once you see how they connect.
Think about a school project submission. Ten unrelated-looking students each submit reasonable work. Nothing alone raises a flag. Then someone notices all ten forms list the exact same guardian phone number. One shared phone number could be a coincidence. Ten forms sharing it is not a coincidence. It is a pattern only visible once you connect the dots across submissions. It is invisible within any single one.
Organised fraud works the same way, at bank scale.
Why it exists
The fraud model from the previous lesson looks at one transaction, or one account, at a time. That catches a lot. It misses coordinated fraud rings almost by design. A ring is built specifically to make each account look ordinary.
A typical ring opens many accounts using stolen or fake identities. It reuses a small number of real resources: one phone number, one device, one delivery address, one payout account. Individually, account #47 looks like a normal new customer. The tell is that account #47 shares a phone number with accounts #12, #83 and #91. None of them have ever contacted each other.
A graph is the natural way to represent this. Accounts and shared attributes become dots, called nodes. Lines, called edges, connect two dots wherever they share an attribute. Fraud rings show up as an unusually dense tangle of connections in a graph that is otherwise sparse.
How it works
Ordinary customers: A fraud ring:
cust_1 --- cust_2 fake_1 fake_2 fake_3 ... fake_10
(shared family phone) \ | | /
\ | | /
cust_3 cust_4 cust_5 device_X -- device_Y
(no shared attributes) (10 accounts, only 2 real devices
behind all of them)Most real customers form small, isolated pairs or stand completely alone. A ring stands out as an unusually large, densely connected cluster.
A real example you have seen
Banks and telecom companies routinely find rings of accounts. Each account has a slightly different fake name, but the exact same delivery address or SIM card. This pattern is invisible if you only ever check one application at a time. It becomes obvious the moment someone draws the connections.
Remember this
- A fraud ring is designed to look normal one account at a time. The pattern only appears in how accounts connect to each other.
- Shared attributes — device, phone, address, payout account — become the edges of a graph built specifically to catch this.
- This is a different tool from the per-transaction model in the previous lesson, and real systems run both.
- A dense cluster is a lead for a human investigator, not an automatic verdict. Freezing a real account needs a person to check it first.
What to learn next
- Anti-money-laundering monitoring — a related problem, tracking money moving between accounts over time.
- How a real fraud system is built — the per-transaction half of a complete system.
- HDBSCAN — a general clustering technique that shares the "find the dense group" idea.
Developer — Code and libraries.
Setup
pip install networkxMinimal runnable code
We build a small graph of accounts and the devices they used to sign up. Ordinary customers barely connect to anyone. A fraud ring stands out immediately once you look at connected components.
import networkx as nx
G = nx.Graph()
# Ordinary customers: at most a device shared within one family, nothing more
G.add_edges_from([("cust_1", "cust_2"), ("cust_3", "cust_4"), ("cust_5", "cust_6")])
# Accounts with no shared attributes with anyone at all
G.add_nodes_from([f"cust_{i}" for i in range(7, 15)])
# A fraud ring: 10 fake accounts funnelled through only 2 shared devices
ring_accounts = [f"fake_{i}" for i in range(10)]
shared_devices = ["device_X", "device_Y"]
for acct in ring_accounts:
for device in shared_devices:
G.add_edge(acct, device)
print(f"total accounts + devices in the graph: {G.number_of_nodes()}")
print(f"total connected components: {nx.number_connected_components(G)}")
print()
for component in sorted(nx.connected_components(G), key=len, reverse=True):
accounts = sorted(n for n in component if n.startswith(("cust", "fake")))
devices = sorted(n for n in component if n.startswith("device"))
if len(accounts) >= 5:
print(f"SUSPICIOUS: {len(accounts)} accounts sharing only {len(devices)} device(s)")
print(f" accounts: {accounts}")
else:
print(f"normal: {accounts} (no ring pattern)")total accounts + devices in the graph: 26 total connected components: 12 SUSPICIOUS: 10 accounts sharing only 2 device(s) accounts: ['fake_0', 'fake_1', 'fake_2', 'fake_3', 'fake_4', 'fake_5', 'fake_6', 'fake_7', 'fake_8', 'fake_9'] normal: ['cust_1', 'cust_2'] (no ring pattern) normal: ['cust_3', 'cust_4'] (no ring pattern) normal: ['cust_5', 'cust_6'] (no ring pattern) normal: ['cust_7'] (no ring pattern) normal: ['cust_8'] (no ring pattern) normal: ['cust_9'] (no ring pattern) normal: ['cust_10'] (no ring pattern) normal: ['cust_11'] (no ring pattern) normal: ['cust_12'] (no ring pattern) normal: ['cust_13'] (no ring pattern) normal: ['cust_14'] (no ring pattern)
What actually happened
nx.connected_components(G) splits the entire graph into groups of nodes that can reach each other by following edges. Ordinary customers form tiny groups of one or two, because they share nothing with strangers.
The 10 fake accounts, despite being fabricated with different names, all connect to the same two devices — so the graph algorithm sees them as one single component of size 10, instantly separable from every real customer's tiny, isolated cluster. Nobody wrote a rule that says "10 is suspicious." The structure itself is the signal.
G.add_edges_from(...)adds both nodes and the edge in one call — a convenience worth knowing when building graphs from lists of pairs.- The
if len(accounts) >= 5check here is a placeholder threshold, chosen to make the demo readable. A real system would tune this against labelled historical rings, not pick it by eye. - Devices are graph nodes too, exactly like accounts — this is what lets
networkxfind the connection betweenfake_0andfake_9even though they never directly reference each other.
Common mistakes
Only looking at direct pairs, not full components. Two accounts sharing a device is common and often innocent (a family). The suspicious signal is usually a large connected structure, not any single shared attribute.
Ignoring legitimate reasons for sharing. A university's public Wi-Fi IP address, or a popular co-working space address, can connect hundreds of genuine accounts. Real systems weight or exclude known "hub" attributes like these — otherwise every dorm and office produces a false positive ring.
Rebuilding the whole graph from scratch on every transaction. A production fraud graph updates incrementally as new accounts and shared attributes arrive; recomputing components over the entire historical graph on every event does not scale.
Treating a large component as automatic proof of fraud. A dense cluster is a strong signal to investigate, not a verdict. Human review of flagged rings before any account action remains standard practice.
Try it yourself
Add a thirteenth "normal" customer, cust_15, who happens to share device_X with only one fake account — perhaps a coincidence, perhaps a public library computer. Re-run the component search and see how cust_15 gets pulled into the suspicious component, which is exactly the false-positive risk raised in "common mistakes" above.
What to learn next
- Anti-money-laundering monitoring — a graph problem over money movement instead of shared attributes.
- HDBSCAN — density-based clustering, the general technique this lesson's connected-components trick is a simple cousin of.
- Model risk management — governance for a system that can flag real customers as suspicious.
Researcher — Mathematics and papers.
Beyond connected components
Plain connected components are a useful first pass but miss rings that are sparsely, rather than densely, connected to shared resources, and are easily evaded by using one fresh device per small sub-group. Production systems typically layer on:
- Community detection (e.g. Louvain modularity maximisation, Blondel et al., 2008) to find densely-intra-connected, sparsely-inter-connected subgroups within a much larger connected graph, rather than requiring full disconnection from the rest of the network.
- Graph centrality measures (degree, betweenness, PageRank) to rank individual nodes by how structurally unusual their position is, rather than only classifying whole components.
- Graph neural networks (GNNs), which learn node embeddings that combine an account's own features with the aggregated features of its neighbours, typically via message passing:
h_v^(k) = UPDATE( h_v^(k-1), AGGREGATE( { h_u^(k-1) : u in N(v) } ) )h_v^(k)— the embedding of nodevafterkrounds of message passingN(v)— the set ofv's graph neighboursAGGREGATE— a permutation-invariant pooling function (sum, mean, or an attention-weighted combination, as in GraphSAGE or GAT)UPDATE— typically a small neural network combining the node's previous state with the aggregated neighbour signal
After k rounds, h_v^(k) encodes information from v's k-hop neighbourhood, letting a downstream classifier use both an account's own features and the shape of its surroundings.
Why GNNs specifically for fraud
Fraud rings are engineered to defeat feature-based classifiers by making each account's own features look ordinary. A GNN's inductive bias — that a node's label depends partly on its neighbourhood, not only its own features — directly targets this evasion strategy. Wang et al. (2019, at Alibaba) and Liu et al. (2021, DGraph, JD.com) report production fraud-detection deployments using heterogeneous GNNs over transaction and relationship graphs at billion-node scale.
Cost
Graphs at this scale are typically stored and queried in a dedicated graph database or graph-processing framework (Neo4j, TigerGraph, or a Spark GraphX / DGL pipeline), not recomputed from scratch per request. Full-graph GNN training does not fit in memory at production scale, so neighbourhood sampling (GraphSAGE-style, sampling a fixed number of neighbours per node per layer) is standard to bound both memory and per-node inference cost, at the price of some approximation error versus using the full neighbourhood.
Key references
- Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E. (2008). Fast Unfolding of Communities in Large Networks. Journal of Statistical Mechanics — the Louvain method.
- Hamilton, W., Ying, R., & Leskovec, J. (2017). Inductive Representation Learning on Large Graphs (GraphSAGE). NeurIPS.
- Wang, D., et al. (2019). A Semi-Supervised Graph Attentive Network for Financial Fraud Detection. ICDM.
- Liu, X., et al. (2021). DGraph: A Large-Scale Financial Dataset for Graph Anomaly Detection. NeurIPS Datasets and Benchmarks.
Current state
GNN-based fraud detection is now standard practice at large payment and e-commerce platforms, but public benchmarks lag industry practice substantially, because real fraud graphs and labels are commercially sensitive and rarely released — DGraph is a rare public exception. Published GNN fraud results should be read with the awareness that most real deployments cannot be independently verified against the datasets that motivated them.
What to learn next
- HDBSCAN — density-based clustering, conceptually related to community detection above.
- Anti-money-laundering monitoring — where graph structure represents money flow rather than shared attributes.
- Feature importance done right — relevant once graph-derived features feed into a downstream classifier.