Skip to main content

strictnull

Build the control before you compare.

A graph only looks special against the right control. Most papers compare a network against an Erdős–Rényi graph with the same number of nodes and edges. That control cannot tell wiring from degree: a heavy-tailed degree distribution on its own produces clustering, short paths, reciprocity, and communities. strictnull puts the second control next to the first, verifies both, and tells you how much of the "structure" was the degree sequence.

pip install git+https://github.com/tsurutanmen/strictnull

A PyPI release is planned; until then install from GitHub as above. Requires numpy and igraph. networkx is optional (for Graph.from_networkx).

Thirty seconds

$ strictnull tests/data/toy.csv --n-null 20

Graph(directed, weighted, 60 nodes, 400 edges)
controls: 20 draws each, strict null rewired 73.4% of edges

statistic              real          weak null        strict null   z(weak)  z(strict)  by degree
-------------------------------------------------------------------------------------------------
reciprocity          0.1250    0.1150+-0.0251      0.1153+-0.0171        +0.4       +0.6         2%
clustering           0.2835    0.2154+-0.0075      0.2817+-0.0091        +9.0       +0.2        97%
avg_path             1.8113    1.8391+-0.0098      1.8178+-0.0075        -2.8       -0.9        77%
assortativity       -0.1818   -0.0292+-0.0311     -0.1695+-0.0172        -4.9       -0.7        92%

warning: strict null rewired only 73% of edges; raise swaps_per_edge or accept that this graph has little room to rewire

Against the weak control this toy graph is nine standard deviations more clustered than chance. Against the strict control it is 0.2. The degree sequence explains 97% of the gap. Nothing about the wiring is special here; the paper that reported z = 9 would have been reporting its degree distribution.

The three numbers

For each statistic the report gives:

column meaning
weak null mean and sd over draws of an Erdős–Rényi graph with the same node and edge count (weights shuffled). The control most papers use.
strict null mean and sd over draws of a degree-preserving rewiring: every node keeps its exact in-degree and out-degree, everything else is randomised.
z(weak), z(strict) distance of the real value from each control, in control standard deviations.
by degree (strict − weak) / (real − weak): the share of the weak-control gap that the degree sequence alone reproduces. Near 100% means the structure is the degree distribution.

Python

import strictnull as sn

g = sn.Graph.from_csv("edges.csv", directed=True)      # header: source,target[,weight]
g = sn.Graph.from_edges(edges, weights, directed=True)  # (E, 2) int array
g = sn.Graph.from_adjacency(A, directed=True)
g = sn.Graph.from_networkx(G)

rep = sn.compare(g, stats=["clustering", "reciprocity", "avg_path", "assortativity"], n_null=20, seed=0)
print(rep)
rep.to_json("report.json")

for row in rep.rows:
    print(row.stat, row.z_strict, row.fraction_explained_by_degree)

Built-in statistics: reciprocity, clustering, avg_clustering, avg_path, assortativity, n_triangles, max_core, modularity_leiden, mean_weight. Add your own:

@sn.register("rich_club_100")
def rich_club_100(graph):
    ...
    return value

rep = sn.compare(g, stats=["rich_club_100", "clustering"])
# or pass the callable directly: sn.compare(g, stats=[rich_club_100])

Swapping the graph inside a model

When the claim is not about a statistic but about a model that uses the graph (a connectome as a fixed layer, a knowledge graph as a prior), draw nulls and retrain:

for i, h in enumerate(sn.ensemble(g, n=5, seed=0)):        # independent degree-preserving draws
    sn.verify(g, h, strict=True)                            # raises if a degree moved
    A_null = h.g.get_adjacency(attribute="weight")          # or h.edges(), h.weights()
    acc = train_and_evaluate(A_null)

Compare the real graph's outcome against the mean and spread over draws, not against one draw.

What the tool refuses or flags, and why

  • One draw is not a control. compare raises on n_null < 2.
  • Every null is verified before use. Exact degree match, no self-loops, no multi-edges, weight multiset preserved, and the fraction of edges actually rewired. Below 80% rewired, the report warns.
  • Invariant statistics are flagged. If a statistic is identical, to the last digit, on the graph and on every null, the control never touched what it measures. That is not "no effect"; it is "nothing was tested". The row is marked INVARIANT and a warning explains which control it is invariant under. Functions of the degree sequence (max degree, degree variance) are invariant under the strict null. Functions of the edge count or weight multiset are invariant under both.

Worked example: a connectome

The larval Drosophila connectome released by Winding et al. (2023): 2,952 neurons, 110,140 directed edges. Ten draws per control, 25 seconds.

statistic              real          weak null        strict null   z(weak)  z(strict)  by degree
reciprocity          0.2569    0.0126+-0.0004      0.0250+-0.0006      +677.1     +380.7         5%
clustering           0.2350    0.0252+-0.0000      0.0537+-0.0001     +5103.3    +1469.6        14%
avg_path             2.7466    2.1262+-0.0002      2.2596+-0.0008     +2840.9     +631.7        22%
assortativity        0.2365   -0.0012+-0.0020     -0.0138+-0.0021      +116.1     +119.7        -5%

Here the wiring is real: the degree sequence explains only 5–22% of each gap and the strict-control z stays in the hundreds. That is the case where the strict control changes nothing about the conclusion, and it still had to be run to know that.

The same connectome, used as a fixed recurrent layer in an MNIST classifier, gave the opposite result: swapping the layer for a strict null moved accuracy by +0.002, within seed noise. Structure exists, and the task does not use it. The two findings, and the code that produced them, are in the Tsuruta Lab research notes.

Claude Code skill

skill/strictnull/SKILL.md teaches Claude Code when to reach for this tool and how to write up the result. Install it by copying the folder:

cp -r skill/strictnull ~/.claude/skills/strictnull

After that, a sentence like "this network is more clustered than random" in a session triggers the comparison against both controls and a write-up that names the control.

Method

The strict null is the directed configuration model conditioned on simple graphs, sampled by double-edge swaps (Maslov & Sneppen 2002), swaps_per_edge swaps per edge (default 20). For undirected graphs the same procedure preserves each node's degree. The weak null samples uniformly among simple graphs with the given edge count. See Fosdick, Larremore, Nishimura & Ugander (2018), Configuring random graph models with fixed degree sequences, SIAM Review 60(2), for the space of choices this tool does not cover (multigraphs, self-loops, stub matching).

License

MIT.

Metadata

Release files for strictnull 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for strictnull 0.1.0
File Size Uploaded
strictnull-0.1.0.tar.gz 18.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for strictnull 0.1.0
File Interpreter ABI Platform
strictnull-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.2 kB

Release files / strictnull-0.1.0.tar.gz

Download URL strictnull-0.1.0.tar.gz
Size 18.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d7b050bba03c67dafdf7146acbb7b1239cfdb54d753824bc2b1ea4547832f232
BLAKE2b-256 checksum
How to use checksums
e736b7f73f77e9bb7b1b02c0c5605b7ef8e3b33e27909500ecfcef080ec2737f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release files / strictnull-0.1.0-py3-none-any.whl

Download URL strictnull-0.1.0-py3-none-any.whl
Size 15.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6723c1808b395f53f0b2b1b3787ea8fddc478f13fc032930c7dec33290cc7948
BLAKE2b-256 checksum
How to use checksums
7e42d3aa64698623cb3e1c60fabdd13f5dc9f1b34cec164a80397d15fc00336a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page