Skip to main content

Technical Blog

Graph Algorithms Your Enterprise Is Not Using: PageRank, Community Detection, and Centrality for Fraud, Churn, and Supply Chain

16 min read
graph-data-sciencegraph-algorithmsneo4jfraud-detectionenterprisepythonsupply-chainchurn

Most companies use graph databases as glorified foreign key stores. They store relationships but never analyze them. Here are five production-ready graph algorithms, each mapped to a concrete business outcome, with Python and Neo4j code you can deploy this week.

You have a graph. You are not using it.

Every enterprise I consult for has relational data. Customers connected to products. Suppliers connected to components. Accounts connected to devices. Employees connected to teams, projects, and clients. The data is there, sitting in PostgreSQL, in your CRM, in your ERP, in three dozen microservices that each own a slice of the picture.

Some of these companies have even loaded that data into a graph database. They write Cypher queries. They draw pretty diagrams for stakeholders. They build a GraphRAG pipeline and call it a day.

But almost none of them are running graph algorithms on that data.

This is the equivalent of owning a Formula 1 car and using it to drive to the grocery store. The graph is there. The engine is there. Nobody is pressing the accelerator.

Graph algorithms are not research curiosities. They are the analytical layer that turns a static knowledge graph into a decision engine. And the gap between companies that run them and companies that do not is measurable in millions of dollars per year.

This post covers five algorithms I deploy in production across fraud detection, churn prediction, supply chain risk, and sales intelligence. Each one maps to a specific business outcome with a specific dollar value. I include the Python and Neo4j GDS code so you can run them on your own data this week.

The five algorithms, mapped to business outcomes

Before we dive into each one, here is the high-level map. If you are a CTO or Head of Data scanning this post, this table tells you where the money is.

AlgorithmWhat it findsBusiness use caseTypical impact
PageRankThe most influential nodes in a networkB2B sales: identify high-influence accounts that drive expansion revenue15 to 30% improvement in lead conversion
Louvain Community DetectionDense clusters of connected nodesFraud: detect coordinated rings of accounts, devices, and merchants50%+ reduction in network fraud losses
Betweenness CentralityNodes that sit on the most shortest paths (bottlenecks)Supply chain: find single points of failure before they breakProactive risk mitigation, avoided disruption costs
Node Similarity (Jaccard)Pairs of nodes with similar connection patternsChurn: enrich prediction models with "customers who look like churners"10 to 20% lift in churn model recall
Shortest Path / DijkstraThe most efficient route between two nodesIT operations: blast radius analysis for system failuresReduced MTTR, faster incident resolution

Let us go through each one.

1. PageRank for sales intelligence

PageRank is the algorithm that made Google work. It assigns an importance score to every node based on the number and quality of its incoming connections. A node is important if important nodes point to it. Simple idea, profound consequences.

In a B2B context, your customer graph is not flat. Some accounts influence others. A large enterprise customer that refers three mid-market companies, who each refer five smaller clients, is not just "one account." It is a network hub whose churn would cascade through your revenue base.

Most CRMs do not capture this. They track accounts as independent rows. Graph algorithms reveal the topology underneath.

The use case

You have a B2B SaaS product. Your graph contains:

  • Account nodes (companies)
  • REFERRED edges (account A referred account B)
  • COLLABORATES_WITH edges (accounts that share integrations, data, or workflows)

Running PageRank on this graph identifies the accounts with the highest network influence. These are the accounts your sales team should prioritize for expansion, your CS team should monitor for churn risk, and your marketing team should feature in case studies.

Neo4j GDS implementation

from neo4j import GraphDatabase
 
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "password")
 
 
def run_pagerank(driver):
    """
    Project a B2B account graph and compute PageRank scores.
    Returns the top 20 most influential accounts.
    """
    with driver.session() as session:
        # Step 1: Project the graph into GDS memory
        session.run("""
            CALL gds.graph.project(
                'account-influence',
                'Account',
                {
                    REFERRED: {orientation: 'NATURAL'},
                    COLLABORATES_WITH: {orientation: 'UNDIRECTED'}
                }
            )
        """)
 
        # Step 2: Run PageRank
        result = session.run("""
            CALL gds.pageRank.stream('account-influence', {
                maxIterations: 50,
                dampingFactor: 0.85
            })
            YIELD nodeId, score
            WITH gds.util.asNode(nodeId) AS account, score
            RETURN account.name AS name,
                   account.arr AS annual_revenue,
                   score AS influence_score
            ORDER BY score DESC
            LIMIT 20
        """)
 
        top_accounts = []
        for record in result:
            top_accounts.append({
                "name": record["name"],
                "arr": record["annual_revenue"],
                "influence_score": round(record["influence_score"], 4),
            })
 
        # Cleanup
        session.run("CALL gds.graph.drop('account-influence')")
 
    return top_accounts
 
 
def main():
    driver = GraphDatabase.driver(URI, auth=AUTH)
    top = run_pagerank(driver)
    for acc in top:
        print(f"{acc['name']:30s}  ARR: ${acc['arr']:>10,.0f}  Influence: {acc['influence_score']}")
    driver.close()
 
 
if __name__ == "__main__":
    main()

What you do with the results

The output is a ranked list of accounts by network influence. In practice, I use this in three ways:

  1. Churn risk weighting. If a high-influence account shows early churn signals (declining usage, support escalation), the business impact is not just their ARR. It is their ARR plus the cascade risk through accounts they influence. Multiply their churn probability by a network impact factor derived from PageRank.

  2. Expansion targeting. High-PageRank accounts are your best candidates for upsell. They are visible in their network, and their adoption of new features signals trust to connected accounts.

  3. Referral program optimization. Instead of treating every customer equally in your referral program, weight incentives by influence score. A referral from a high-PageRank account is worth 5 to 10x more than one from an isolated account.

2. Community detection for fraud rings

Louvain community detection partitions a graph into dense clusters (communities) where nodes within each cluster are more connected to each other than to nodes outside it. It is fast, scalable, and directly maps to one of the most expensive problems in financial services: coordinated fraud.

Why tabular models miss this

I wrote extensively about why XGBoost delivers more ROI than most LLMs for tabular prediction problems. That is still true. But fraud rings are the one domain where feature-based classifiers have a structural blind spot.

An XGBoost model trained on transaction features (amount, merchant, time, device) scores each transaction independently. It catches the obvious outliers: a $10,000 purchase at 3am from a new device. But organized fraud does not look like that. A fraud ring is a set of accounts, devices, and merchants that individually look normal. The signal is in the pattern of connections between them, not in any single data point.

Community detection finds those patterns.

The architecture

from dataclasses import dataclass, field
from neo4j import GraphDatabase
 
 
@dataclass
class FraudCommunity:
    """A detected community in the financial transaction graph."""
    community_id: int
    node_count: int
    internal_edge_density: float
    avg_transaction_velocity: float
    shared_device_ratio: float
    risk_score: float = 0.0
 
    def assess_risk(self) -> str:
        """
        Simple heuristic: communities with high device sharing,
        high internal density, and high velocity are suspicious.
        """
        self.risk_score = (
            0.4 * self.shared_device_ratio
            + 0.3 * min(self.internal_edge_density / 0.5, 1.0)
            + 0.3 * min(self.avg_transaction_velocity / 100, 1.0)
        )
        if self.risk_score > 0.7:
            return "HIGH"
        elif self.risk_score > 0.4:
            return "MEDIUM"
        return "LOW"
 
 
def detect_fraud_communities(driver) -> list[FraudCommunity]:
    """
    Run Louvain community detection on a transaction graph
    and identify suspicious clusters.
    """
    with driver.session() as session:
        # Project the graph: accounts connected by shared devices,
        # shared IPs, and direct transfers
        session.run("""
            CALL gds.graph.project(
                'fraud-network',
                ['Account', 'Device', 'IPAddress'],
                {
                    TRANSACTED_WITH: {orientation: 'UNDIRECTED'},
                    USED_DEVICE: {orientation: 'UNDIRECTED'},
                    LOGGED_FROM: {orientation: 'UNDIRECTED'}
                }
            )
        """)
 
        # Run Louvain
        session.run("""
            CALL gds.louvain.mutate('fraud-network', {
                mutateProperty: 'communityId',
                maxLevels: 10,
                maxIterations: 20
            })
        """)
 
        # Analyze each community
        result = session.run("""
            CALL gds.louvain.stream('fraud-network')
            YIELD nodeId, communityId
            WITH communityId, collect(gds.util.asNode(nodeId)) AS members
            WHERE size(members) >= 3 AND size(members) <= 500
            RETURN communityId,
                   size(members) AS nodeCount
            ORDER BY nodeCount DESC
            LIMIT 50
        """)
 
        communities = []
        for record in result:
            # In production you would compute density, velocity, and
            # device sharing ratios from the underlying data.
            # Simplified here for clarity.
            comm = FraudCommunity(
                community_id=record["communityId"],
                node_count=record["nodeCount"],
                internal_edge_density=0.0,  # compute from subgraph
                avg_transaction_velocity=0.0,
                shared_device_ratio=0.0,
            )
            communities.append(comm)
 
        session.run("CALL gds.graph.drop('fraud-network')")
 
    return communities

The hybrid approach that works

In production, I do not replace the tabular fraud model with a graph model. I combine them. The architecture looks like this:

  1. Graph layer (Neo4j GDS): run Louvain nightly. Flag communities with suspicious density, device sharing, and velocity patterns. Generate per-node features: community_size, community_risk_score, shared_device_count, cross_community_transfer_count.

  2. Tabular layer (XGBoost): train the existing transaction classifier, but add the graph features to the feature set. The model now sees both the transaction-level signal and the network-level signal.

  3. Real-time scoring: every incoming transaction is scored by XGBoost with the pre-computed graph features. Transactions involving accounts in high-risk communities get flagged for review.

This hybrid approach consistently delivers a 30 to 50% reduction in false positives compared to tabular-only models, at the same recall level. The graph features give the model the structural context it needs to distinguish a legitimate burst of activity from coordinated manipulation.

3. Betweenness centrality for supply chain risk

Betweenness centrality measures how often a node appears on the shortest path between other nodes. In plain language: it identifies the bottlenecks, the nodes that, if removed, would break the most connections.

For supply chain analysis, this is the exact question every operations leader should be asking: which supplier, if they went offline tomorrow, would cause the most damage to my production?

The problem with flat supply chain data

Most companies model their supply chain in ERP tables. They know their Tier-1 suppliers. They have contracts, POs, invoices. But they typically have no visibility into Tier 2 or Tier 3. When a disruption hits a raw material supplier three levels deep, the first sign of trouble is a missed delivery six weeks later.

A graph changes this. You model the supply chain as a directed network:

  • Nodes: suppliers, manufacturing plants, warehouses, logistics providers
  • Edges: SUPPLIES, SHIPS_THROUGH, DEPENDS_ON
  • Properties: lead times, contract values, geographic location, historical reliability scores

Betweenness centrality on this graph tells you which nodes carry the most traffic. A small Tier-3 supplier with a high betweenness score is your hidden single point of failure.

Implementation

def find_supply_chain_bottlenecks(driver, top_n: int = 15):
    """
    Identify critical bottleneck suppliers using betweenness centrality.
    """
    with driver.session() as session:
        session.run("""
            CALL gds.graph.project(
                'supply-chain',
                ['Supplier', 'Plant', 'Warehouse'],
                {
                    SUPPLIES: {orientation: 'NATURAL'},
                    SHIPS_THROUGH: {orientation: 'NATURAL'},
                    DEPENDS_ON: {orientation: 'NATURAL'}
                }
            )
        """)
 
        result = session.run("""
            CALL gds.betweenness.stream('supply-chain')
            YIELD nodeId, score
            WITH gds.util.asNode(nodeId) AS entity, score
            RETURN entity.name AS name,
                   entity.type AS entity_type,
                   entity.tier AS supplier_tier,
                   entity.country AS country,
                   score AS betweenness_score
            ORDER BY score DESC
            LIMIT $topN
        """, topN=top_n)
 
        bottlenecks = []
        for record in result:
            bottlenecks.append({
                "name": record["name"],
                "type": record["entity_type"],
                "tier": record["supplier_tier"],
                "country": record["country"],
                "betweenness": round(record["betweenness_score"], 2),
            })
 
        session.run("CALL gds.graph.drop('supply-chain')")
 
    return bottlenecks

What this looks like in practice

I ran this analysis for a mid-market manufacturing client last year. Their procurement team had full confidence in their supply chain. "We have dual-sourced everything at Tier 1," they told me.

The graph told a different story. Two of their "independent" Tier-1 suppliers both depended on the same Tier-3 component manufacturer in a single region. That Tier-3 node had the highest betweenness centrality in the entire graph. If it went down, both Tier-1 supply lines would fail simultaneously.

The fix was not expensive. It was a second-source agreement with a different Tier-3 supplier. The cost was negligible compared to the estimated $4M impact of a simultaneous supply failure.

Without the graph algorithm, this dependency was invisible. It did not exist in any spreadsheet, any ERP report, or any procurement dashboard. It only existed in the topology of relationships between entities.

4. Node similarity for churn prediction enrichment

Node similarity (Jaccard or Overlap coefficient) measures how similar two nodes are based on their shared connections. Two customers who use the same features, contact the same support agents, and belong to the same usage cohort are "similar" in a graph sense, even if their tabular features (contract size, tenure, industry) look different.

Why this matters for churn

I have built churn models in production for years. The standard approach works: take tabular features (tenure, monthly charges, support tickets, usage patterns), train a gradient-boosted classifier, deploy behind an API. It is reliable and cost-effective.

But there is a ceiling. Tabular features capture what each customer is. They do not capture what each customer does in relation to other customers. Node similarity bridges that gap.

The enrichment pattern

def compute_churn_similarity_features(driver) -> list[dict]:
    """
    For each customer, find the most similar customers (by shared product
    usage and support interactions) and compute graph-based churn features.
    """
    with driver.session() as session:
        session.run("""
            CALL gds.graph.project(
                'customer-graph',
                ['Customer', 'Product', 'SupportAgent'],
                {
                    USES: {orientation: 'UNDIRECTED'},
                    CONTACTED: {orientation: 'UNDIRECTED'}
                }
            )
        """)
 
        # Compute Jaccard similarity between all customer pairs
        session.run("""
            CALL gds.nodeSimilarity.mutate('customer-graph', {
                mutateRelationshipType: 'SIMILAR_TO',
                mutateProperty: 'similarity',
                topK: 10,
                similarityCutoff: 0.3
            })
        """)
 
        # For each customer, compute churn enrichment features
        result = session.run("""
            MATCH (c:Customer)
            OPTIONAL MATCH (c)-[s:SIMILAR_TO]->(other:Customer)
            WHERE other.churned = true
            WITH c,
                 count(other) AS similar_churned_count,
                 avg(s.similarity) AS avg_similarity_to_churned,
                 collect(s.similarity) AS similarities
            RETURN c.customer_id AS customer_id,
                   similar_churned_count,
                   coalesce(avg_similarity_to_churned, 0.0) AS avg_similarity_to_churned,
                   CASE WHEN size(similarities) > 0
                        THEN reduce(max = 0.0, s IN similarities | CASE WHEN s > max THEN s ELSE max END)
                        ELSE 0.0
                   END AS max_similarity_to_churned
        """)
 
        features = []
        for record in result:
            features.append({
                "customer_id": record["customer_id"],
                "similar_churned_count": record["similar_churned_count"],
                "avg_similarity_to_churned": round(record["avg_similarity_to_churned"], 4),
                "max_similarity_to_churned": round(record["max_similarity_to_churned"], 4),
            })
 
        session.run("CALL gds.graph.drop('customer-graph')")
 
    return features

Feeding it into XGBoost

The output of this function is a set of per-customer features that you merge into your existing training set:

import pandas as pd
from xgboost import XGBClassifier
 
# Your existing tabular features
df_tabular = pd.read_csv("customer_features.csv")
 
# Graph-enriched features from Neo4j
df_graph = pd.DataFrame(compute_churn_similarity_features(driver))
 
# Merge on customer_id
df = df_tabular.merge(df_graph, on="customer_id", how="left").fillna(0)
 
# Train with the combined feature set
feature_cols = [
    # Tabular features
    "tenure", "monthly_charges", "total_charges",
    "contract_type", "num_support_tickets",
    # Graph features
    "similar_churned_count",
    "avg_similarity_to_churned",
    "max_similarity_to_churned",
]
 
X = df[feature_cols]
y = df["churned"].astype(int)
 
model = XGBClassifier(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.05,
    scale_pos_weight=len(y[y == 0]) / len(y[y == 1]),
    eval_metric="aucpr",
)
model.fit(X, y)

In my experience, adding graph similarity features to a well-tuned tabular churn model lifts recall by 10 to 20 points in the top decile. The model catches customers who look fine on paper but whose "neighborhood" in the customer graph is churning around them. That is a signal no tabular feature can capture.

5. Shortest path for IT blast radius analysis

Shortest path algorithms (BFS, Dijkstra) find the most direct route between two nodes. In IT operations, this translates to a critical question: if system A goes down, what is the fastest path through which the failure reaches system B?

The use case

Modern enterprise infrastructure is a graph of dependencies. Service A calls Service B, which reads from Database C, which replicates to Backup D, which Service E depends on for failover. When something breaks, the blast radius is determined by the topology of these dependencies.

Most incident response teams discover this topology during the incident. They trace logs, read runbooks, ask senior engineers who remember how things are wired. By then, the damage is done.

Running shortest path analysis beforehand gives you a precomputed map of failure propagation. When Service A goes down, you already know that Database C is 2 hops away, Service E is 3 hops away, and the customer-facing API is 4 hops away. You can prioritize your response accordingly.

Implementation

def compute_blast_radius(driver, failed_service: str, max_hops: int = 5) -> list[dict]:
    """
    Given a failed service, compute the blast radius:
    all downstream systems reachable within max_hops,
    ordered by shortest distance (= time to impact).
    """
    with driver.session() as session:
        result = session.run("""
            MATCH (source:Service {name: $serviceName})
            CALL gds.bfs.stream('infra-graph', {
                sourceNode: source,
                maxDepth: $maxHops
            })
            YIELD path
            WITH nodes(path) AS pathNodes, length(path) AS distance
            UNWIND pathNodes AS node
            WITH DISTINCT node, distance
            WHERE node.name <> $serviceName
            RETURN node.name AS service_name,
                   node.tier AS criticality_tier,
                   node.team AS owning_team,
                   distance AS hops_from_failure
            ORDER BY distance ASC
        """, serviceName=failed_service, maxHops=max_hops)
 
        impact = []
        for record in result:
            impact.append({
                "service": record["service_name"],
                "tier": record["criticality_tier"],
                "team": record["owning_team"],
                "hops": record["hops_from_failure"],
            })
 
    return impact

This is not complex code. But having this precomputed and wired into your alerting system means the difference between "we scrambled for 45 minutes figuring out what was affected" and "we knew the blast radius in under 10 seconds and paged the right teams immediately."

The meta-point: graph algorithms are a multiplier, not a replacement

I want to be clear about what I am arguing and what I am not.

I am not arguing that you should replace your XGBoost models with graph algorithms. Gradient-boosted trees on tabular data remain the highest-ROI tool for most enterprise prediction problems. That has not changed.

I am arguing that graph algorithms are the enrichment layer that makes everything else work better. They surface features that no amount of feature engineering on flat tables can produce. They reveal structural patterns (bottlenecks, clusters, influence cascades) that are invisible to row-level analysis. And they cost almost nothing to run once you have the data in a graph.

The pattern is the same for every algorithm in this post:

  1. Model your entities and relationships as a graph. You probably already have the data. It is sitting in foreign keys, join tables, event logs, and API call traces.
  2. Run the algorithm. Neo4j GDS makes this a single Cypher call. The algorithms are well-tested, scalable, and documented.
  3. Extract features or insights. Feed them into your existing ML models, dashboards, or alerting systems.
  4. Measure the impact. Compare precision, recall, cost avoidance, or revenue impact before and after adding graph features.

The companies I work with that follow this pattern consistently find that graph algorithms deliver 230% ROI over three years, with a payback period under eight months. That is not my number. That is what Neo4j's Forrester Total Economic Impact study reports across their enterprise customers.

If your data team has a graph database and is not running algorithms on it, you are leaving money on the table. And if you do not have a graph database yet, the question is not whether you need one. The question is which of these five use cases hits your P&L the hardest.


Graph data science is the analytical layer most enterprises are missing. I build production graph pipelines that find fraud rings, surface supply chain risk, and enrich predictive models with structural features your competitors cannot see. If you want to know what your data's relationships are hiding, let's talk.