A fraud investigator may see chargebacks across several customers without spotting what connects them. Representing each customer and merchant as a node turns transaction rows into a network where a shared merchant becomes visible. section 7.4.1
A node, also called a vertex, represents one entity in a graph. In financial fraud analysis, nodes can represent customers, accounts, merchants, IP addresses, devices, or contact details. A transaction can also be a node when its attributes or links to multiple parties warrant that representation. Edges represent connections between nodes; properties hold additional information, such as a customer's risk score or a transaction's timestamp. section 9.2.1
To construct a multi-entity fraud graph from transaction data:
- Choose entity columns, such as user ID, IP address, device ID, phone number, and credit card number. Add each distinct entity as a typed node. section 9.3.2
- Connect entities that appear in the same transaction. An edge weight can count how many transactions link a pair. section 9.3.2
- Check node and edge counts before looking for densely connected communities or shared identifiers. These patterns can expose coordinated activity that isolated transaction checks miss. section 9.3.2 section 9.1
Finding a shared point of compromise
Customers who later filed chargebacks had previously purchased from the same merchant. With customers and merchants as nodes and purchases as edges, that common merchant is easier to identify than it is in separate chargeback records. The connection supports an investigation hypothesis; it does not, by itself, establish that the merchant was compromised. section 7.4.1
Figure 7.5 A conceptual visualization of transactions involving users who filed chargebacks. This simplified graph highlights the ability to quickly identify commonly shared merchants among users. The numbers on the edges represent the transaction dates (YY.MM.DD format). The solid lines indicate normal transactions, while the dashed lines represent transactions that were later disputed as chargebacks. While this example remains intentionally minimal, such graph-based relationships become even more valuable in complex real-world scenarios where detecting shared connections can reveal fraud rings or coordinated behavior.
The transaction graph shows customer and merchant nodes, dated purchase edges, and which transactions were later disputed. section 7.4.1
Listing 9.2 Building the graph from transaction data with parallel edge preparation
if transactions_df is not None:
G = nx.Graph() #1
node_types_and_columns = {
'user': 'user_id', 'ip': 'ip', 'device': 'device_id',
'phone': 'phone_number', 'card': 'credit_card_number'
}
print("Preparing nodes for the graph...")
for node_type, col_name in node_types_and_columns.items():
for entity_id in transactions_df[col_name].unique():
G.add_node(f"{node_type}_{entity_id}", type=node_type)
print(f"Initial nodes added. Total nodes: {G.number_of_nodes()}")
edges_to_add_with_attributes = defaultdict(lambda: {'weight': 0, 'transactions': []})
def process_transaction_row(index_row_tuple):
index, row = index_row_tuple
transaction_entities_in_row = []
for node_type_key, col_name_val in node_types_and_columns.items():
transaction_entities_in_row.append(f"{node_type_key}_{row[col_name_val]}")
row_edges = []
for node1, node2 in combinations(sorted(transaction_entities_in_row), 2):
row_edges.append(((node1, node2), row['transaction_id']))
return row_edges
print("\nProcessing transactions in parallel to prepare edge data...")
prepared_edges_nested_list = []
with ThreadPoolExecutor(max_workers=8) as executor: # Using 8 worker threads
futures = [executor.submit(process_transaction_row, item) for item in transactions_df.iterrows()]
count = 0
for future in as_completed(futures):
prepared_edges_nested_list.extend(future.result())
count += 1
if count % 1000 == 0 and count > 0: # Print progress every 1000 transactions
print(f"Processed {count} transactions for edge preparation...")
print(f"All {len(transactions_df)} transactions processed for edge preparation.")
print("\nAggregating edge data and adding edges to the graph...")
for (node1, node2), transaction_id in prepared_edges_nested_list:
edges_to_add_with_attributes[(node1, node2)]['weight'] += 1
edges_to_add_with_attributes[(node1, node2)]['transactions'].append(transaction_id)
for (node1, node2), attributes in edges_to_add_with_attributes.items():
G.add_edge(node1, node2, weight=attributes['weight'], transactions=attributes['transactions'])
print("\nGraph construction completed.")
print(f"Number of nodes in the graph: {G.number_of_nodes()}")
print(f"Number of edges in the graph: {G.number_of_edges()}")
else:
print("transactions_df not loaded. Skipping graph construction.")
G = None
The NetworkX example adds typed nodes from unique entity IDs, then prepares edges from entities appearing together in transactions. section 9.3.2


















