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)
../_images/e9d5176f9534f02246e2c9f4e3d6d4d72b10f17949522a551a5743c639a7fb83.png

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=0 raises. Infomap requires seed >= 1; use any positive integer.

  • Codelengths are not comparable across meta_data_rate values. result.codelength reports 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 pass converge=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:

These are the keyword arguments to infomap.run():

Option

Type

Default

What it controls

seed

int

123

RNG seed; must be ≥ 1

num_trials

int

1

Independent restarts; keep the best

converge

bool

False

Stop early when codelength plateaus

two_level

bool

False

Force a flat (non-hierarchical) partition

directed

bool

None

Respect link direction

teleportation_probability

float

0.15

\(\tau\) in the random-surfer model

recorded_teleportation

bool

False

Include teleportation in flow encoding

markov_time

float

1.0

Resolution scale

regularized

bool

False

Bayesian regularization for sparse data

regularization_strength

float

1.0

How strongly to regularize

silent

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