Running Infomap¶
How-to
At a glance
Most runs need only a seed, a num_trials count, and perhaps a flow-model
choice such as directed. The rest of Infomap’s options split into flow-model
settings, which define the random walk, and search settings, which control how
hard Infomap looks.
Most runs need only three choices: a seed, how many num_trials to run, and
whether the flow is directed; add two_level if you want a flat partition
instead of the multilevel default. The shortest useful run:
import networkx as nx
import infomap
result = infomap.run(
nx.karate_club_graph(),
seed=123, # reproducible
num_trials=10, # keep the best of 10 restarts
two_level=True, # flat partition; omit for the multilevel default
silent=True, # skip the engine's console log
)
print(result.num_top_modules, result.codelength)
Everything below is when and why to reach for the other options. For the why behind the flow model itself, see Concepts.
Two kinds of options¶
Infomap has dozens of options, but they fall into two groups:
Flow-model options (
directed,markov_time,teleportation_probability) change how the random walk is defined. They encode your beliefs about the system; the wrong choice gives a partition that scores well but means little.Search options (
seed,num_trials,converge) change how hard the search works, not which partition Infomap looks for. They only affect how reliably the stochastic search finds the optimum [Calatayud et al., 2019].
The theory behind them lives in Concepts: Flow and random walks for flow and teleportation, Hierarchy and the multilevel map equation and Reading Infomap through Louvain and Leiden for resolution, and Incomplete data and regularization for regularization.
The options, one task each¶
Each option below comes with a small graph you can run as you read.
Setting up¶
import networkx as nx
import infomap
import random
# ── Helper: build an 8-clique ring (clear, clean structure) ──────────────────
G_ring = nx.ring_of_cliques(8, 5) # 8 cliques × 5 nodes = 40 nodes
# ── Helper: build a weakly-structured graph (noisy, degenerate landscape) ───
def make_weak_structure(n_communities=6, n_per=8, p_in=0.55, p_out=0.08, seed=123):
"""Planted-partition graph with intentionally weak community signal."""
rng = random.Random(seed)
G = nx.Graph()
n = n_communities * n_per
G.add_nodes_from(range(n))
for i in range(n):
for j in range(i + 1, n):
ci, cj = i // n_per, j // n_per
p = p_in if ci == cj else p_out
if rng.random() < p:
G.add_edge(i, j)
return G
G_noisy = make_weak_structure()
print(f"Ring of cliques: {G_ring.number_of_nodes()} nodes, {G_ring.number_of_edges()} edges")
print(f"Weak structure: {G_noisy.number_of_nodes()} nodes, {G_noisy.number_of_edges()} edges")
Ring of cliques: 40 nodes, 88 edges
Weak structure: 48 nodes, 175 edges
seed and reproducibility¶
seed fixes the internal random number generator. With the same seed you get
the same partition on every run, which makes results shareable and publishable.
The default seed=123 is reproducible too, but setting the seed yourself ties
each result to the code that produced it.
# Two runs with the same seed produce the same partition.
for attempt in range(2):
result = infomap.run(G_ring, two_level=True, seed=123, num_trials=10, silent=True)
print(f"Run {attempt + 1}: modules={result.num_top_modules}, L={result.codelength:.4f}")
Run 1: modules=8, L=3.0373
Run 2: modules=8, L=3.0373
num_trials and solution quality¶
A single trial can get trapped in a local minimum. On graphs with clear, distinct communities, like the ring of cliques, this rarely matters because most starting points converge to the same solution. On a noisy or ambiguous graph the landscape has many near-degenerate minima, and a single trial is unreliable.
# On the noisy graph, different seeds land in different local minima.
print("Single trial (num_trials=1), five different seeds:")
for seed in [1, 7, 42, 99, 123]:
result = infomap.run(G_noisy, two_level=True, seed=seed, num_trials=1, silent=True)
print(f" seed={seed}: L={result.codelength:.4f}, modules={result.num_top_modules}")
Single trial (num_trials=1), five different seeds:
seed=1: L=5.4912, modules=4
seed=7: L=5.4326, modules=5
seed=42: L=5.4538, modules=5
seed=99: L=5.4352, modules=5
seed=123: L=5.5043, modules=1
# More trials explore more of the landscape and find a lower, stable minimum.
print("Fixed seed=123, varying num_trials:")
for num_trials in [1, 5, 20]:
result = infomap.run(G_noisy, two_level=True, seed=123, num_trials=num_trials, silent=True)
print(f" num_trials={num_trials:2d}: L={result.codelength:.4f}, modules={result.num_top_modules}")
Fixed seed=123, varying num_trials:
num_trials= 1: L=5.5043, modules=1
num_trials= 5: L=5.4528, modules=5
num_trials=20: L=5.4352, modules=5
Here the single trial with seed=123 lands at a higher codelength (worse
minimum) than num_trials=20 with the same seed: a single trial can settle in a
local trap, and the extra trials find a partition that compresses the random
walk more efficiently. Which single trial gets stuck depends on the seed, so
the gap is not guaranteed for any particular seed. The codelength converges:
once you are reliably in the deepest valley, adding more trials does not change
the answer [Calatayud et al., 2019].
Rule of thumb. Use num_trials=1 for quick exploration. Use
num_trials=10 for most analyses. Use num_trials=20 or more (up to around
50, or converge=True with a higher cap) when you need high confidence in
the global minimum, when you are comparing partitions across networks, or on
networks with weak or diffuse community structure.
converge=True
converge=True treats num_trials as a cap and stops early once the best
codelength has stopped improving. This gives you “as many trials as you need
but no more,” which is useful when you are unsure how many trials your network
requires.
two_level versus multilevel¶
By default Infomap finds a hierarchical partition with as many levels as the
flow supports. Each level is a coarser view of the same structure.
two_level=True forces a single flat partition, which helps when you know your
network is not hierarchical, or when you need a simple cluster assignment for
downstream analysis.
# Build a graph with genuine nested hierarchy:
# 2 top groups → 2 subgroups each → 2 cliques of 5 nodes
# Edge weights shrink at each scale, creating clear nesting.
def hierarchical_graph():
G = nx.Graph()
nid = 0
top_groups = []
for _ in range(2):
subgroups = []
for _ in range(2):
cliques = []
for _ in range(2):
members = list(range(nid, nid + 5))
nid += 5
for i in range(len(members)):
for j in range(i + 1, len(members)):
G.add_edge(members[i], members[j], weight=100.0)
cliques.append(members)
G.add_edge(cliques[0][0], cliques[1][0], weight=10.0)
subgroups.append(cliques)
G.add_edge(subgroups[0][0][0], subgroups[1][0][0], weight=1.0)
top_groups.append(subgroups)
G.add_edge(top_groups[0][0][0][0], top_groups[1][0][0][0], weight=0.1)
return G
G_hier = hierarchical_graph()
for two_level in [False, True]:
result = infomap.run(G_hier, two_level=two_level, seed=123, num_trials=10, silent=True)
m_top = result.modules(depth=1) # top-level groups
m_leaf = result.modules(depth=-1) # finest-level assignments
print(f"two_level={two_level}:")
print(f" levels={result.num_levels}, top modules={result.num_top_modules}")
print(f" distinct groups at depth 1: {len(set(m_top.values()))}")
print(f" distinct groups at finest level: {len(set(m_leaf.values()))}")
print(f" L={result.codelength:.4f}")
two_level=False:
levels=4, top modules=2
distinct groups at depth 1: 2
distinct groups at finest level: 8
L=2.3762
two_level=True:
levels=2, top modules=8
distinct groups at depth 1: 8
distinct groups at finest level: 8
L=2.3848
With two_level=False, the multilevel solution recovers the built-in nesting:
the two main branches at the top and the eight cliques at the finest level,
reached with result.modules(depth=-1) (the cell above prints the exact counts).
The codelength is lower because the deeper code captures the real hierarchy. With
two_level=True, the eight cliques become the top-level modules and the nested
structure is invisible.
Use result.modules(depth=k) to access any level of the hierarchy. The default
result.modules() returns level 1, the coarsest (top modules); pass depth=-1
for the finest (leaf) level.
directed and the flow model¶
When your links represent directed flow (citations, hyperlinks, metabolic
reactions, transit routes), directed=True tells Infomap to use the
directionality to define the random walk. The walker follows links in their
stated direction, and unrecorded teleportation keeps the walk ergodic. Ignoring
direction on the same graph often misses the real community structure, because
it treats every link as symmetric.
# A directed graph: three directed cliques connected by directed bridges.
# The directed structure groups nodes by which clique's flow they participate in.
G_dir = nx.DiGraph()
# Three directed cliques
for offset in [0, 4, 8]:
nodes = list(range(offset, offset + 4))
for u in nodes:
for v in nodes:
if u != v:
G_dir.add_edge(u, v)
# Directed bridges forming an outer loop
G_dir.add_edge(3, 4)
G_dir.add_edge(7, 8)
G_dir.add_edge(11, 0)
print(f"Directed graph: {G_dir.number_of_nodes()} nodes, {G_dir.number_of_edges()} edges")
for directed in [False, True]:
result = infomap.run(G_dir, directed=directed, two_level=True,
seed=123, num_trials=10, silent=True)
print(f"directed={directed}: modules={result.num_top_modules}, L={result.codelength:.4f}")
Directed graph: 12 nodes, 39 edges
directed=False: modules=3, L=2.5174
directed=True: modules=3, L=2.4468
Both settings recover three modules here because the graph is strongly clustered. On noisier real-world directed networks the difference is more pronounced: the directed walk respects asymmetric flow and tends to find tighter modules corresponding to true circulation patterns.
Teleportation probability. The default teleportation_probability=0.15
(the conventional PageRank value) works well for most networks. Because Infomap
uses unrecorded teleportation [Lambiotte and Rosvall, 2012], the partition barely
responds to this value, so the default is rarely worth changing unless your
domain says walkers teleport much more or less often. See
Flow and random walks for what teleportation does to the flow.
markov_time as a resolution dial¶
The map equation operates at the natural timescale of one random-walk step.
markov_time shifts this timescale: values below 1 encode the walk more
frequently (favouring more, smaller modules) and values above 1 encode it less
frequently (favouring fewer, larger modules). This is the principled way to
examine structure at a specific resolution when the data does not have a natural
scale [Kheirkhahzadeh et al., 2016].
# The ring of cliques has 8 genuine cliques. markov_time merges them
# into larger clusters as it increases, giving a resolution sweep.
print("markov_time sweep on a ring of 8 cliques (5 nodes each):")
print(f" graph: {G_ring.number_of_nodes()} nodes, {G_ring.number_of_edges()} edges")
print()
for mt in [0.5, 1.0, 2.0, 4.0, 8.0]:
result = infomap.run(G_ring, two_level=True, markov_time=mt,
seed=123, num_trials=10, silent=True)
print(f" markov_time={mt:.1f}: {result.num_top_modules} modules")
markov_time sweep on a ring of 8 cliques (5 nodes each):
graph: 40 nodes, 88 edges
markov_time=0.5: 8 modules
markov_time=1.0: 8 modules
markov_time=2.0: 8 modules
markov_time=4.0: 4 modules
markov_time=8.0: 3 modules
At markov_time=1.0 (the default) Infomap recovers all 8 cliques. As the
Markov time grows, smaller modules become too expensive to maintain and adjacent
cliques merge. This gives you a natural scale sweep without changing the network.
The multilevel map equation is usually the better first choice [Kawamoto and Rosvall, 2015]: it finds scale automatically and has a less restrictive
resolution limit than the two-level version. Use markov_time when you
specifically want to study structure at a given scale, or when reviewers ask
“what happens at a coarser resolution?”
regularized for sparse data¶
# On the small karate club the regularization prior competes with the data:
# at half strength the three modules survive, and at the default strength
# the prior already outweighs the data and collapses them into one.
G_karate = nx.karate_club_graph()
for label, kwargs in [
("baseline", {}),
("regularized, strength=0.5", {"regularized": True, "regularization_strength": 0.5}),
("regularized (default 1.0)", {"regularized": True}),
]:
result = infomap.run(G_karate, two_level=True, seed=123, num_trials=10, silent=True, **kwargs)
print(f"{label}: modules={result.num_top_modules}, L={result.codelength:.4f}")
baseline: modules=3, L=4.0874
regularized, strength=0.5: modules=3, L=4.7197
regularized (default 1.0): modules=1, L=4.8780
Higher regularization_strength merges more modules. On a network this small
the default strength already merges everything, which is why the option is
meant for large, sparse, or incompletely sampled data rather than well-sampled
toy graphs. For a gradual version of the same sweep, where a planted partition
goes from 14 modules through 7 to 1 as the strength grows, see
Incomplete data and regularization. Use regularization when you have prior
reason to believe that small modules in your result reflect sampling noise
rather than real structure. The Bayesian derivation is in
Smiljanić et al. [2020].
Why trials matter, visualised¶
A picture of the effect: run the noisy graph many times with a single trial each (varying the seed) and compare those codelengths against the best of 20 trials.
import matplotlib.pyplot as plt
from myst_nb import glue
# Codelength from one trial per seed ...
single_L = []
for s in range(1, 13):
single_L.append(
infomap.run(G_noisy, two_level=True, seed=s, num_trials=1, silent=True).codelength
)
# ... versus the best of 20 trials.
best_L = infomap.run(
G_noisy, two_level=True, seed=123, num_trials=20, silent=True
).codelength
fig, ax = plt.subplots(figsize=(6, 3.2))
ax.scatter(range(1, len(single_L) + 1), single_L,
color="0.45", zorder=3, label="single trial (num_trials=1)")
ax.axhline(best_L, color="tab:blue", lw=2, label=f"best of 20 trials = {best_L:.3f}")
ax.set_xlabel("seed")
ax.set_ylabel("codelength (bits/step)")
ax.legend(loc="best", fontsize=8)
fig.tight_layout()
glue("fig-running-and-options", fig, display=False)
plt.close(fig)
Fig. 6 Each grey point is the codelength from a single trial with a different seed. One trial can settle in a worse local minimum; many trials reliably reach the best-of-20 value (blue line).¶
Reusable configuration¶
When you run several networks with the same settings, capture them once as an
Options and pass the instance to each run. Keyword overrides on
the call still take precedence:
from infomap import Options
options = Options(two_level=True, seed=123, num_trials=10, silent=True)
for name, graph in [("ring of cliques", G_ring), ("karate club", G_karate)]:
result = infomap.run(graph, options=options)
print(f"{name}: {result.num_top_modules} modules, L={result.codelength:.4f}")
ring of cliques: 8 modules, L=3.0373
karate club: 3 modules, L=4.0874
Pitfalls¶
seed=0raises. Infomap requiresseed >= 1; use any positive integer.Codelengths are not comparable across
meta_data_ratevalues.result.codelengthreports the combined objective, the topological term plus the weighted attribute term, so it rises with the rate even when the partition does not change (see Networks with metadata).More trials never hurt correctness, only runtime. If repeated runs disagree, raise
num_trials(or passconverge=True) rather than trusting one fit.Sparse or under-sampled data over-splits. Reach for
regularized=True(see Incomplete data and regularization).
API pointers¶
The options in this chapter are keyword arguments to infomap.run() and to
infomap.Infomap (infomap.Network.run() takes them bundled via
its options= argument instead). After a run, the
metrics live on the returned Result:
infomap.Result.codelengthis the map equation value \(L(\mathsf{M}^*)\) for the best partition found across all trials.infomap.Result.num_top_modulescounts the modules at the top level of the hierarchy.infomap.Result.num_levelsis the depth of the module hierarchy.infomap.Result.modules()returns a{node_id: module_id}dict; passdepth=k(1 = coarsest) to access intermediate levels.infomap.Result.one_level_codelengthevaluates \(L\) on a flat single-module partition, a natural baseline.infomap.Result.relative_codelength_savingsis the fractional gain over that baseline, \((L_\text{one} - L^*) / L_\text{one}\).
These are the keyword arguments to infomap.run():
Option |
Type |
Default |
What it controls |
|---|---|---|---|
|
int |
123 |
RNG seed; must be ≥ 1 |
|
int |
1 |
Independent restarts; keep the best |
|
bool |
False |
Stop early when codelength plateaus |
|
bool |
False |
Force a flat (non-hierarchical) partition |
|
bool |
None |
Respect link direction |
|
float |
0.15 |
\(\tau\) in the random-surfer model |
|
bool |
False |
Include teleportation in flow encoding |
|
float |
1.0 |
Resolution scale |
|
bool |
False |
Bayesian regularization for sparse data |
|
float |
1.0 |
How strongly to regularize |
|
bool |
False |
Suppress console output |
For the full set of options as a searchable table, see the
Options reference in the API reference.
Going deeper¶
The options guide notebook lists every option in a searchable table, generated from the installed package so it matches your version. Start here for the exhaustive reference.
The survey (§4) covers method selection and the flow-model justification [Smiljanić et al., 2026].
The quantitative case for multiple trials [Calatayud et al., 2019].
Why unrecorded teleportation produces robust directed partitions [Lambiotte and Rosvall, 2012].
Why the map equation’s resolution limit is less restrictive than modularity’s [Kawamoto and Rosvall, 2015].
The Markov-time mechanism and its efficient resolution sweeps [Kheirkhahzadeh et al., 2016].