NetworkX vs. igraph for Large Indoor Routing Graphs

Choosing the graph library underneath an indoor router is a decision that only bites at scale, which is why it belongs in the Fallback Routing Architectures reference: a single building routes fine on anything, but a campus graph with hundreds of thousands of edges makes the NetworkX-versus-igraph question the difference between a five-millisecond route and a stalled kiosk.

What the Choice Actually Trades

Both libraries model the same thing — a weighted graph you run shortest paths over — but they sit at opposite ends of the ergonomics-versus-performance axis, and the right pick depends entirely on where your graph falls on the size curve.

NetworkX is pure Python. A node is any hashable object, an edge is a dictionary of attributes, and the whole graph is nested dicts. That makes it a joy to author against: node IDs are your own string keys like "L2-room-0184", attributes are read and written inline, and the API reads like the domain. The cost is that every node and edge is a Python object with per-object memory overhead and interpreter-speed traversal, so a graph of a few hundred thousand edges consumes gigabytes and runs Dijkstra in tenths of a second rather than milliseconds.

NetworkX and igraph on the properties that decide an indoor routing deployment A two-column comparison over six dimensions. NetworkX is pure Python built on dictionaries, accepts any hashable object as a node identity, stores a dictionary of attributes per node and edge, mutates cheaply and incrementally, installs from pip with no build step, and fits the shapely and geopandas idioms most indoor pipelines already use. igraph has a C core behind Python bindings, requires integer node indices that you must map yourself, stores attributes as parallel vectors, is expensive to mutate structurally, needs a C build or a wheel, and has sparser GIS-facing examples. Same algorithms, very different data model Dimension NetworkX igraph Implementation pure Python dicts C core, Python bindings Node identity ● any hashable object △ integer index, map it yourself Attribute access ● dict per node/edge △ parallel attribute vectors Mutation cost ● cheap, incremental △ rebuild-ish for structure Install ● pip, pure Python △ C build or wheel Ecosystem fit ● shapely/geopandas idioms sparser GIS examples Both solve the same shortest paths; they differ in what a large graph costs to hold.

The node-identity row causes the most rework. Indoor graphs key nodes by stable feature ids; igraph wants dense integers, so an id-to-index map becomes a permanent part of the codebase and a permanent source of off-by-one bugs.

igraph wraps a C core. Vertices are integers 0..n-1, edges are stored in compact C arrays, and shortest-path search runs entirely in compiled code. At campus scale it is roughly an order of magnitude faster and several times smaller in memory. The cost is the integer-index model: igraph does not route on your string IDs, so you maintain a bidirectional mapping between your domain node IDs and igraph’s dense integer indices, and every attribute access crosses the Python/C boundary. The API is less forgiving, and a stale index mapping is a class of bug NetworkX simply does not have.

The practical rule: author and validate on NetworkX for its ergonomics, and switch the hot routing path to igraph once the graph outgrows single-building scale. The topology is identical either way — only the container changes.

Minimal Working Example

The example builds the same tiny four-node routing graph in both libraries and runs one shortest path, so the index-model difference is visible side by side. Note that NetworkX returns your string IDs directly, while igraph returns integer indices you must map back.

import logging
from typing import Optional

import networkx as nx
import igraph as ig

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger(__name__)

EDGES: list[tuple[str, str, float]] = [
    ("lobby", "hall_a", 5.0), ("hall_a", "clinic", 12.0),
    ("lobby", "hall_b", 4.0), ("hall_b", "clinic", 18.0),
]


def route_networkx(src: str, dst: str) -> list[str]:
    """Shortest path on NetworkX: node IDs are your own strings, returned directly."""
    g = nx.Graph()
    for u, v, w in EDGES:
        g.add_edge(u, v, weight=w)
    try:
        return nx.dijkstra_path(g, src, dst, weight="weight")
    except nx.NodeNotFound:
        logger.error("networkx: %s or %s not in graph", src, dst)
        return []


def route_igraph(src: str, dst: str) -> list[str]:
    """Shortest path on igraph: vertices are integers, so map string IDs to indices and back."""
    names = sorted({n for e in EDGES for n in e[:2]})
    idx = {name: i for i, name in enumerate(names)}           # domain id -> integer vertex
    g = ig.Graph(n=len(names))
    g.add_edges([(idx[u], idx[v]) for u, v, _ in EDGES])
    g.es["weight"] = [w for *_, w in EDGES]
    result = g.get_shortest_paths(idx[src], to=idx[dst], weights="weight", output="vpath")
    if not result or not result[0]:
        logger.warning("igraph: no path %s -> %s", src, dst)
        return []
    return [names[i] for i in result[0]]                      # integer vertices -> domain ids


logger.info("networkx: %s", route_networkx("lobby", "clinic"))
logger.info("igraph:   %s", route_igraph("lobby", "clinic"))

Both print ['lobby', 'hall_a', 'clinic']. The nine extra lines in route_igraph are the whole story: the idx map and the closing [names[i] ...] translation are permanent overhead you carry forever with igraph, in exchange for compiled-speed search once the graph is large.

NetworkX vs. igraph Reference

Figures are indicative orders of magnitude for a connected campus routing graph on commodity hardware; benchmark your own topology before committing.

Single-source solve time against graph size for both libraries Two curves of single-source shortest-path solve time against graph size, from one thousand to two hundred and fifty thousand nodes. NetworkX rises roughly linearly from 0.9 milliseconds to 447 milliseconds. igraph rises from 0.2 to 41 milliseconds, consistently about ten times faster. A typical campus graph of 42,000 nodes is marked, where NetworkX takes about 72 milliseconds and igraph about 7. Ten times faster — and both fast enough until they are not 0 200 400 50000 100000 150000 200000 250000 nodes in the routing graph single-source solve (ms) NetworkX shortest_path igraph get_shortest_paths typical campus: 42k nodes

A 10x gap that usually does not matter. At campus scale both answer inside a typical 100 ms budget; igraph earns its integration cost when a single process serves many concurrent solves, or above roughly 150k nodes.

Dimension NetworkX igraph
Core implementation Pure Python (nested dicts) C library with a Python wrapper
Vertex identity Any hashable (your string IDs) Dense integers 0..n-1; you map IDs yourself
Shortest-path speed (100k+ edges) Slower; interpreter-bound, tenths of a second ~5-20× faster; compiled, low single-digit ms
Memory per node/edge High; every element is a Python object Low; compact C arrays, several times smaller
Build / load time Slower; per-edge Python calls Faster bulk add_edges; slower one-at-a-time
Attribute handling Inline dicts, ergonomic, cheap to read Parallel attribute arrays; boundary-crossing cost
API ergonomics Rich, Pythonic, huge algorithm set Terser, index-centric, steeper learning curve
Campus-scale suitability Fine to ~10k nodes; strains past ~100k edges Preferred for large multi-building campuses
Best fit Authoring, validation, single buildings The hot routing path at scale

Common Errors & Fixes

igraph returns a path of unfamiliar integers, or IndexError on lookup. You forgot that igraph routes on vertex indices, not your node IDs, and either used a string where an integer was required or indexed your name list with the wrong array. Keep one canonical, stable name -> index map built once when the graph is constructed, and translate at both ends — never rebuild the map per query or the indices can shift:

Choosing a graph library by graph size and how often the topology changes One decision fanning to four outcomes. Under fifty thousand nodes, NetworkX fits the latency budget and keeps the shapely idioms. When the topology is edited many times a minute — corridor closures, lift outages — NetworkX wins because incremental mutation is cheap. Above roughly one hundred and fifty thousand nodes with a read-mostly graph, igraph is worth the integration cost: rebuild nightly and solve all day. When one process serves many concurrent solves, igraph wins because its C core releases the GIL. Two of the four branches are about writes, not reads Which graph library for this routing service? answer by graph size and mutation rate, not by benchmark < 50k nodes NetworkX fits the budget keeps the idioms edits per minute NetworkX incremental mutation is cheap > 150k, read-mostly igraph rebuild nightly solve all day many concurrent solves igraph C core releases the GIL

Mutation rate decides more often than size. An indoor graph that absorbs live closures is being edited constantly, and igraph's structural rebuild cost eats the solve-time advantage it was chosen for.

# WRONG: passing a domain string straight into igraph.
# g.get_shortest_paths("lobby", to="clinic")   # -> error or wrong vertex
# RIGHT: translate through the canonical map at both boundaries.
vpath = g.get_shortest_paths(idx["lobby"], to=idx["clinic"], weights="weight")[0]
route = [names[i] for i in vpath]

NetworkX process memory balloons past 100k edges. Every node object, edge, and attribute dict carries Python overhead, so a large campus graph exhausts RAM long before it is slow. Do not paper over it by trimming attributes; move the hot path to igraph, or keep NetworkX only for the build-and-validate stage and export a compact edge list into igraph for serving. Rebuild the igraph copy only when the topology_hash changes so the two never disagree.

Attribute lookups dominate igraph runtime. Reading g.vs[i]["floor_level"] inside a tight per-edge loop crosses the Python/C boundary on every access and can erase igraph’s speed advantage. Pull attributes out into a plain Python list or NumPy array once (levels = g.vs["floor_level"]) and index that in the loop, so the boundary is crossed once rather than per iteration.

Integration Point

The library choice is not cosmetic — it sets the performance envelope of the whole routing engine and therefore of the Fallback Routing Architectures that wrap it. A fallback pipeline that copies the graph per request (graph.copy() for a vertical-transition reweight) is cheap on a compact igraph and expensive on a heavyweight NetworkX object, which changes how aggressively you can afford to reweight. The same tension appears in Accessible & Multi-Profile Indoor Routing: running N profile weight functions over one shared graph is only affordable if that graph is compact at campus scale, which pushes the serving path toward igraph while authoring stays on NetworkX. A pragmatic split — validate on NetworkX, serve on igraph, keyed by topology_hash — is compatible with the graceful-degradation design because both copies derive from one authored topology.

Frequently Asked Questions

At what graph size should I switch from NetworkX to igraph?

There is no universal threshold, but a useful heuristic is node count and query rate: below about ten thousand nodes with occasional routing, NetworkX is comfortable and its ergonomics are worth keeping. Past roughly a hundred thousand edges, or when you route many times per second on an edge kiosk, NetworkX’s per-object overhead and interpreter-speed traversal start to show as latency and memory pressure, and igraph’s compiled core pays for its clunkier API. Benchmark your own topology at your real query rate before committing — the crossover depends heavily on how dense your graph is and how often you reweight.

Can I keep NetworkX for authoring and igraph only for serving?

Yes, and it is the pattern most large deployments converge on. Author, mutate, and validate the graph in NetworkX where the API is pleasant, then export a compact edge list and attribute arrays into igraph for the hot routing path, rebuilding the igraph copy only when the topology_hash changes. Keep one canonical node-ID-to-index map so both representations agree on identity, and treat the NetworkX graph as the source of truth and the igraph copy as a derived, disposable serving index.

Does igraph support the weight-function style used for accessibility profiles?

Not the same way. NetworkX accepts a Python callable w(u, v, attrs) and can therefore exclude an edge by returning None at query time. igraph expects a precomputed weight vector over its edges, so you materialise one weight array per profile — set excluded edges to a very large value or, better, delete them from a per-profile subgraph — rather than deciding lazily during traversal. The result is identical, but the exclusion moves from the search loop into a build step that you cache per profile.

This page is part of the Fallback Routing Architectures guide within the Indoor Mapping Architecture & Standards reference.