AI in Finance and Fraud

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.

On this page 5
  1. Why it exists
  2. How it works
  3. A real example you have seen
  4. Remember this
  5. What to learn next

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

Developer — Code and libraries.

Setup

bash
pip install networkx

Minimal 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.

fraud_ring_demo.py
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)")
Output
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) >= 5 check 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 networkx find the connection between fake_0 and fake_9 even 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

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 node v after k rounds of message passing
  • N(v) — the set of v's graph neighbours
  • AGGREGATE — 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