Changelog#
Version 0.3.0 (2026-11-02)#
This release adds hypergraph backbones, the remaining Backbone 3.0 features,
the Pólya and GloSS filters, generalized null models and weight transforms,
and a new temporal module for networks observed over time (conversions
between snapshots, events, and spans, and collapsing them to a static
weighted graph). The public API is now type-annotated and dispatches to
NetworkX backends. It also fixes many correctness problems found in a full
code review that checked every method against its paper, and checks p-values
and backbones against R backbone 3.0.4 and netbone, which they now match
(signed backbones included) except where the documented conventions differ.
Several methods now return different results; see Changes in results
below. Open work is tracked in the Roadmap and in GitHub issues.
Renamed (breaking)
The package is now nx-backbone: install it with
pip install nx-backboneand import it asimport nx_backbone. The repository moved to brianckeegan/nx-backbone, and the documentation to https://www.brianckeegan.com/nx-backbone/. This is a clean break: there is nonetworkx_backbonealias, soimport networkx_backbonefails once the old package is uninstalled. The oldnetworkx-backboneproject on PyPI is not updated.To migrate, replace the distribution and the imports:
pip uninstall networkx-backbone pip install nx-backbone # then, in your code: # import networkx_backbone as nb -> import nx_backbone as nb # from networkx_backbone import ... -> from nx_backbone import ...
Function names, arguments and edge attributes are unchanged by the rename. The BibTeX key that
describe_backbone()writes is nowkeegan_nx_backbone.
Supported versions (breaking)
Requires Python 3.12 or newer and NetworkX 3.4 or newer, matching NetworkX’s own support window. The optional dependency minimums now match NetworkX’s too: NumPy 2.2, SciPy 1.15, Matplotlib 3.10. A CI job tests these minimums exactly.
New features
Statistical and structural methods
New
gloss_filter(), the GloSS filter of Radicchi, Ramasco & Fortunato (2011). It tests each edge weight against a global null model that keeps the topology and draws weights from the network’s own weight distribution, conditioning on both endpoints’ degrees and strengths, and addsgloss_pvalue. The p-values of the paper’s Eq. (4) are computed exactly (integer weights) by direct convolution, stable at any degree; directed graphs use out- and in-degrees and strengths. Les Misérables: 3 edges at alpha = 0.05 (#43).New
polya_filter(): the Pólya urn filter of Marcaccioli & Livan (2019), addingpolya_pvalue. The null model is the beta-binomial urn of Eqs. (1)-(2), tested from both endpoints (out- and in-links for directed graphs). By default the reinforcement parameterais the maximum-likelihood estimate of the paper’s Supplementary Note 5, stored inH.graph["polya_a"]; non-integer weights use the paper’s large-strength approximation, Eq. (6). Les Misérables:a= 0.340 and 12 edges atpolya_pvalue < 0.05(#42).disparity_filter(a=...)generalizes the uniform share null to a symmetric Dirichlet null with concentration1/a, so a share is Beta(1/a, (k-1)/a) and the p-value is a regularized incomplete beta function.a=1(the default) is the original filter, bit for bit and without SciPy;a="ml"fitsaby maximum likelihood and stores it inH.graph["disparity_a"]. With the shares fixed, it is the large-strength limit ofpolya_filter’s beta-binomial null. Les Misérables:a= 0.525 and 19 edges at alpha = 0.05 (9 ata=1) (#55).New
transform_weights()returns a copy of a graph with transformed weights, from a plain callable or any object with a scikit-learn stylefit_transform(scikit-learn is not a dependency). It keeps each node’s neighbor order, rejects non-finite or constant output, warns when a transform reorders weights or creates ties, and records the transform fordescribe_backbone. The new user guide page Weights, Units, and Transforms has a table of which methods are invariant to rescaling and to increasing transforms, each entry checked by a test (#55).unit=onmarginal_likelihood_filter,ecm_filter, andnoise_corrected_filtersets the weight of one count; the models useweight / unit. The defaultunit=1leaves results unchanged. The three scorers now warn (UserWarning) whenweight / unitis not an integer count, naming how they coerce it: MLF rounds counts below 0.5 to 0, the ECM raises counts below 1.5 to 1 (#55).describe_backbonereportsaof the disparity filter (with the fitted value fora="ml"),unit, and transforms recorded bytransform_weights(#55).A calibration test harness (
tests/test_calibration.py) simulates weights from the disparity filter’s null models and checks that endpoint p-values are uniform (#55).ecm_filterandnoise_corrected_filteracceptsigned=True(#25).missing_as_zero=Trueondisparity_filter,lans_filter,marginal_likelihood_filter,ecm_filter,noise_corrected_filterandbackbone_from_weighted(Backbone 3.0). Node pairs without an edge are tested as weight-0 edges and returned withweight0, so a signed backbone can keep pairs that interact significantly less than expected. Degrees, strengths, and fitted multipliers still come from the existing edges only, as in the R package; each scorer’s Notes say how it scores a weight of 0. ARuntimeWarningis raised above one million added pairs (#26).glab_filternow supports directed graphs using the out- and in-involvement of Zhang et al. (2014), Section 2.3 (#12). On a symmetricDiGraphit gives the undirected p-values.doubly_stochastic_filternow also runs the second stage of Slater’s two-stage algorithm: a booleands_keepedge attribute flags the edges added, in descendingds_weightorder, until each connected component is connected (tie levels added together, so the result does not depend on insertion order). Extract it withboolean_filter(H, "ds_keep").ds_weightis unchanged by this stage (#44).doubly_stochastic_filter(smoothing=...)adds a pseudocount to every off-diagonal cell before Sinkhorn-Knopp, so the scaling converges on trees and sparse networks (netboneuses1e-4). The default0leaves the matrix unchanged (#54).local_path_index(weight=...); ah_bridgeattribute fromh_backbone;statistically_validated_cores(max_groups=...).seed=onmodularity_backbone,sparsify, andcompare_graphs.lspar(convention="r")reproduces Rbackbone’slspar. R excludes the endpoints from the Jaccard denominator, uses dense ranks, ignores edges with Jaccard 0, and keepsfloor(d**s)edges per node with no minimum of one. The reference tests show that it matches R exactly on the karate club and Les Misérables at s = 0.3, 0.5 and 0.7 (karate at s = 0.3: 30 edges, against 54 with the default). The default,convention="satuluri", is unchanged (#59).
Bipartite and hypergraph methods
Hypergraph module: the parameter-free MDL hypergraph backbone (Kirkley et al., 2026) with
"edge","node", and"auto"optimizers; statistically validated hypergraphs and cores (SVH/SVC); maximal hyperedges, order filtering, s-components, and intersection graphs.New
local_mdl_hypergraph_backbone(): the local MDL hypergraph backbone of Kirkley et al. (2026, Appendix E). It finds the MDL backbone of each node’s neighborhood separately and combines them with the"union","majority"(default), or"all"rule.HypergraphBackbonegainsn_hyperedges,aggregation,local_backbones, andlocal_description_lengths(#24).Backbone 3.0 parity: multiple-testing correction (
adjust_pvalues(),threshold_filter(mtc=...)), signed backbones (signed=True), and SDSM-EC edge constraints.backbone_from_projection(mtc=...);sdsm(pvalue_method=...);fdsm(burn_in=..., thin=...);bicm(tol=..., max_iter=...).backbone_from_projection(missing_as_zero=False)tests only the agent pairs that co-occur, as Rbackbonedoes by default. Pairs that never co-occur are not counted bymtcand are never retained, even as significantly weak withsigned=True. The default,True, is unchanged and tests every pair, like R’smissing_as_zero = TRUE. The reference tests check both settings against R on Davis Southern Women, where they give different backbones in 13 of 48 settings at alpha = 0.06, 0.6 and 0.8; at 0.05 none differs (#59).hyper_projection(newman=True)/projection="newman"gives Newman’s \(1/(d(z)-1)\) weighting, equal tonx.bipartite.collaboration_weighted_projected_graph. The default hyperbolic weight, \(1/d(z)\), is unchanged and follows Coscia & Rossi (2019). Projection citations are corrected to Coscia & Rossi (2019, arXiv:1906.09081) (#13).
Inputs and interoperability
New
to_graph()converts a NumPy or SciPy sparse adjacency matrix (withnodelistanddirected) or a pandas edge-listDataFrame(source/target/weightcolumns) to a graph with NetworkX’s own converters, refusing an asymmetric matrix for an undirected graph and duplicate edge-list rows.backbone,backbone_from_weightedandbackbone_from_unweightedaccept these inputs directly and give the same backbone as the graph path;nx.to_pandas_edgelistreturns the scored edges as aDataFrame. The individual scorers keep graph-only signatures (#20).Bipartite functions accept NumPy, SciPy sparse, or pandas incidence matrices with
agent_labels/artifact_labels, andagent_nodesis optional (#20, bipartite part).hypergraph_io:hypergraph_to_bipartite, the HIF interchange format, and adapters for xgi, HyperNetX, HypergraphX, and HAT.The hypergraph adapters carry weights:
to_xgi,to_hypernetx,to_hypergraphx, andto_hattakeweights=, and thefrom_*functions takereturn_weights=. New extras[xgi],[hypernetx],[hypergraphx],[hat], and[interop](all four) install the libraries; supported versions are in Installation (#15). Testing against the real libraries found and fixed these adapter bugs:to_xgifailed on tuple node labels (xgi read a two-node hyperedge as a(members, edge_id)pair).to_hatsilently dropped repeated hyperedges (HAT’s defaultcompress=True), andfrom_hatreturned row indices instead of node labels. Labels now round-trip throughH.nodes["Names"].to_hypergraphxsilently merged repeated hyperedges. It still merges them, since HypergraphX has no multi-edges, but now sums their weights, and it explains theTypeErrorraised for mixed-type node labels.The HAT and HypergraphX HIF readers raised
KeyErroronwrite_hifoutput, which omitted thenodesandedgeslists.write_hifnow always writes both.
multigraph_to_weighted(aggregate=...), which can sum, average, or take the max/min of parallel-edge weights.New module
nx_backbone.temporal(first part of #67): temporal networks as snapshots (a dict or list of graphs), events (a multigraph with atimeper edge), or spans (start/end, read as the half-open interval[start, end)).temporal_form()detects the form;snapshots_from_events(),snapshots_from_spans(),events_from_snapshots(),spans_from_snapshots(),events_from_spans(),spans_from_events()andconvert_temporal()convert between them, binning withfreq=/origin=and combining weights withmultigraph_to_weighted(aggregate=...). Snapshots to events or spans and back is lossless (empty snapshots, isolated nodes, and node, edge and graph attributes are kept inM.graph["temporal"]). Numeric times anddatetimetimes withtimedeltasteps need only NetworkX; frequency strings such as"1D"import pandas lazily. See Temporal networks: formats and conversions.flatten_temporal()collapses snapshots, events or spans into one static weighted graph for the static backbone methods: one edge per node pair with the combinedweight(aggregate=sum, count, mean, max or min, or"duration"for spans, the length of the union of a pair’s spans),n_obs,firstandlast(andactive_timefor spans), optionally restricted to a half-openwindow=(t0, t1).from_event_table()andto_event_table()read and write pandas event (or span) tables, keeping duplicate rows as separate events (#70, pandas part).Temporal backbone scores, descriptive (no p-values), for any of the three forms (events and spans are binned with
freq=). Each returns one static graph, the union of the observed edges with score attributes andH.graph["temporal"](method, input form, step times,n_steps,freq,origin, parameters), sothreshold_filterand the other filters apply. They count only an edge’s eligible steps, those in which both endpoints are present: an absent edge is an observed zero there and unknown elsewhere, so a node that joins late is not penalized (#67).observation_fraction():obs_fraction,n_obs,n_eligible,first_obs,last_obs;weight=andmin_weight=count only heavier observations (#68).persistence():max_run,mean_run,n_runsandpersistence, the share of observed steps whose next eligible step is also observed;ineligible="break"lets unknown steps end a run (#68).backbone_stability(): runs any static method (disparity_filter,lans_filter, …) on every step with the same cut and addsstability(n_kept / n_eligible),n_kept,n_eligible,first_kept,last_kept. The cut’s attribute comes from the newdescribe.SCORE_KEYSregistry (#69).weight_trend()(needs numpy):weight_mean,weight_std,weight_cv,weight_slopeper step (andweight_slope_timeper unit of numeric time), with an optional, descriptive slope p-value (SciPy) (#69).
All but
weight_trendwork with NetworkX alone.describe_backbonedescribes them, including the number of steps and the binning. New tutorial: Temporal Backbones; Temporal backbones: presence and eligible steps explains the eligible steps.New
edge_persistence_hmm()(needs numpy): a two-state hidden Markov model of edge activity. Each edge switches between an inactive and an active state (transition probabilitiesa01anda10) and is observed with a false-positive ratealphaand a miss ratebeta, all shared by the edges and fitted by Baum-Welch (EM) pooled over the edges, or fixed withparams=. The observations are, in each eligible step, the edge’s presence (observe="presence"), whether a static method keeps it withbackbone_stability’s per-step cut (observe="keep"), or its p-value (observe="pvalue": uniform when inactive, Beta(a, 1) when active, a modeling choice); ineligible steps carry no evidence. Exact forward-backward and Viterbi give per-edgehmm_posterior(a dict over the eligible steps),hmm_active_fraction,hmm_score,hmm_n_activeandhmm_keep(active in at leastmin_stepssteps); the fitted model is inH.graph["temporal"]["hmm"].alpha < 1 - betais imposed by relabeling the states.hmm_scoreis a persistence-smoothed membership score, not a p-value. The temporal tutorial compares it withpersistence(#73).edge_persistence_hmmrecords two diagnostics inH.graph["temporal"]["hmm"]:n_active_steps, the number of eligible steps the most likely state paths mark active, andunobserved_active_share, the share of them in which the edge was not observed. Because the parameters are pooled over the edges, a mix of persistent and randomly flickering edges can be fitted with a large miss rate and slow switching, which gives the flickering edges more active steps than the persistent ones. A large share flags this. The Notes explain the trade-off and when to fixbetawithparams=. A Beta prior onalphaandbeta(MAP-EM) was evaluated on planted-truth simulations and not adopted. No fitted values change (#112).New
kalman_backbone()(needs numpy and SciPy), a novel adaptation of the Kalman filter and Rauch-Tung-Striebel smoother to backboning (#72). It runs a static significance test (disparity_filterorglab_filter; discrete tests such asnoise_corrected_filterwithdiscrete="raw") on every step, scoring eligible absent edges as weight 0 withmissing_as_zero=True, turns the p-values into probit scores, and smooths each edge’s series with a local-level model (mode="smoother"or the online"filter"; unknown steps and unequal spacing are handled exactly). Per-step p-values use the exact null distribution of the smoothed level; the edge-levelkalman_pvaluetests the largest smoothed z-score over the steps against a null simulated once per missingness pattern (n_sim=,seed=). The process variance comes fromq=,half_life=(in steps) or a pooled maximum likelihood fit, recorded inH.graph["temporal"]["kalman"]. Each edge also getskalman_score,kalman_n_obsand the per-step dictskalman_pvalues,kalman_levelsandkalman_innovations;kalman_snapshots()expands them into per-step graphs. Methods with discrete p-values (LANS, ECM, the marginal likelihood filter, Pólya, GloSS) raise unlessdiscrete="raw"(valid but conservative), because none reports theP(X > x)that a mid-p transform needs. The disparity filter and GLAB give an edge the smaller of two endpoint p-values, which smoothing would turn into many false positives; the defaultsidak=Truecorrects for that, which keeps their smoothed tests valid but conservative. The docstring documents the risks: temporal leakage, autocorrelated confounders, and multiple testing over edges and steps.
Reporting and documentation
New
describe_backbone(): methods-section text and citations for a backbone, in text, Markdown, or BibTeX; the counterpart of Backbone 3.0’snarrative(#23). It is backed by one method-to-BibTeX registry that covers every public scorer, and a test enforces that coverage.New tutorial, “Validating a backbone”: multiple-testing correction,
compare_backbones, consensus, signed backbones, and reporting (#27).New narrative tutorial, “From a hairball to a backbone, end to end”, the “Start here” entry point linked from the home page, the quick start, the tutorials index, and the statistical, filters, measures and describe API pages. A worked case study on Les Misérables and a seeded, simulated collaboration network with planted departments: why a global weight cut fails, scoring versus filtering and the disparity filter’s null model for one node, choosing alpha with Bonferroni and Benjamini-Hochberg corrections, four backbones compared in small multiples with
compare_backbones, robustness to alpha and to the method (consensus_backbone), a signed backbone, anddescribe_backboneoutput. It is a Sphinx-Gallery example, so its nine figures (light and dark variants, in the shared figure style) and every number are computed when the docs are built, and hidden checks fail the build if a claim in the text stops holding. The tutorials index is reorganized into “Start here”, “By network type”, and “Going further” (#89, #96).New narrative tutorial, “Structural and proximity backbones”, executed in the example gallery with eight figures: what spanning trees, the PMFG, the metric and ultrametric backbones, salience, the h-backbone and the doubly stochastic filter guarantee (checked at build time) against the disparity filter; weights as strengths or distances; ties and which methods still depend on input order; proximity scores and sparsifiers for unweighted graphs, including
lspar(convention="r"); and Sinkhorn-Knopp convergence withsmoothing=(#91).Added
CITATION.cff.
Changes in results (bug fixes)
The Les Misérables counts below are edges kept at the documented threshold. Davis counts are significant woman-woman pairs at alpha = 0.05.
doubly_stochastic_filter: when Sinkhorn-Knopp does not converge,ds_weightdepended on the orientation in which each edge was stored, and so on node insertion order (by up to 0.86 on Les Misérables). It is now the symmetric scaling \(\sqrt{D_{uv} D_{vu}}\), which equals the doubly stochastic limit whenever the iteration converges. On Les Misérables (no convergence)ds_keepnow keeps 187 of 254 edges instead of 220 (#54).strongest_n_tieskeeps ties together: each node selects every edge whose weight is at least its n-th largest weight, so a node can select more than n edges. Before, ties at the cutoff were broken by adjacency order, soGandG.copy()could give different backbones (on Les Misérables atn=2they differed in 23 edges). The result now depends only on the weights, as forlspar/local_degree; on an unweighted graph every edge is kept. Les Misérables: 69 -> 108 edges (n=1), 113 -> 146 (n=2), 146 -> 165 (n=3) (#60).global_sparsificationandedge_betweenness_filterbreak ties by the sorted string form of an undirected edge’s endpoints, so the result no longer depends on how each edge is stored (which follows node insertion order). Edge counts are unchanged; on Les Misérables ats=0.5, 5 (global sparsification) and 6 (edge betweenness) of the 127 kept edges swap for other edges tied with them (#60).noise_corrected_filternow implements the published Bayesian noise-corrected backbone (Coscia & Neffke, 2017). Its scores and standard deviations match the authors’ reference code exactly.nc_scoreis the transformed lift, in (-1, 1), with newnc_sdevandnc_pvalueattributes. Keep edges withnc_score > 1.64 * nc_sdevornc_pvalue < 0.05; thresholdingnc_scoreat 2.0 now keeps nothing. The old z-score is available asmethod="zscore". Les Misérables: 98 -> 195 edges (#9); see the next entry for the p-value.noise_corrected_filter:nc_pvalueis now, by default, the exact binomial tail \(P(X \geq n_{ij})\) of the method’s null model, \(X \sim B(n_{..}, n_{i.} n_{.j} / n_{..}^2)\) (the p-value of the paper’s footnote 2), computed with the regularized incomplete beta function so that non-integer counts are not rounded. The previousnc_pvalue, the normal tail ofnc_score / nc_sdev(the paper’sdeltarule read as a z-test), ispvalue_method="delta". Thedeltarule divides the score by a standard deviation evaluated at the observed weight through a transform that flattens as the lift grows, so the ratio has no bound: ongnm_random_graph(30, 40, seed=1)with unit weights all 40 edges had p ≈ 1e-65 (median) and 4 are now below 0.05 (median 0.12). On simulated graphs from the model’s own null (2,000 unit interactions among 50 nodes, 20 seeds), the share ofnc_pvalue < 0.05was 0.24 (undirected) and 0.34 (directed) before and is 0.047 and 0.051 now.nc_scoreandnc_sdevare unchanged. On a directed graph with integer weights the default p-values equalmarginal_likelihood_filter’s; on Les Misérables and the karate club the two keep the same edges.kalman_backbonenow treats the default noise-corrected p-values as discrete (discrete="raw"is needed). Les Misérables: 195 -> 126 edges (unit=0.1: 222 -> 215); karate club: 68 -> 39 (#105).multiple_linkage_analysisnow fits each node’s sorted weights to ideal dominance distributions by default (method="dominance"; Van Nuffel, 2007, after Weaver, 1954) and addsmla_rank. The previous LANS threshold ismethod="lans". The fit is the coefficient of determination over the zero-padded profile, as restated by Van Nuffel, Derudder & Witlox (2010), whose Table 1 the tests reproduce; a newdeviation=keyword also offers"profile"(whole-profile squared deviation, the measure used earlier in this development cycle) and"weaver"(Weaver’s variance over the candidate links). Les Misérables: 109 -> 205 edges ("profile": 223,"weaver": 253) (#10).sparsify(umst=True)now adds the union of all minimum spanning trees (Hamann et al., 2016) instead of one arbitrary spanning forest, so tied scores no longer make the result depend on node order (#32). The union is added wheneverumst=True, not only when filtering split a component.ecm_filter: the solver never converged and every p-value was about 0, so every edge was kept (254 of 254). It now fits the ECM by maximum likelihood and uses the correct geometric tail; it keeps 72 edges and warns if the fit does not converge.marginal_likelihood_filter: now uses Dianati’s null, \(B(T, s_is_j/2T^2)\). The old formula depended on the order nodes were added. Les Misérables: 70 -> 126 edges.glab_filter: now uses per-node shortest-path involvement and \((1-I_{ij})^{(k_i-1)^c}\) (Zhang et al., 2014) instead of global edge betweenness. Les Misérables: 5 -> 56 edges.disparity_filter: the p-value is now \((1 - p_{ij})^{k_i - 1}\) (Serrano et al., 2009). Versions 0.1.0 and 0.2.0 computed \(1 - (k_i - 1)(1 - p_{ij})^{k_i - 2}\), the integrand of the paper’s formula in place of its integral, so almost every edge looked significant. Les Misérables: 247 -> 9 edges. On directed graphs it also tests the target’s incoming links.lans_filteron directed graphs also tests the target’s incoming links (#11, contributed by @bfs2021 in #34).lans_filter(signed=True): the lower tail is now#{weights < w} / k, the mirror image of the upper tail#{weights > w} / k, as in Rbackbone3.0.4. It was the empirical CDF#{weights <= w} / k, which counts the link itself and is never below1 / k, so a link could be significantly weak only at a node of degree above 40 (at alpha = 0.05). The two-sided p-values now equal R’s. The one-tailedlans_pvalueis unchanged. Les Misérables signed backbone at alpha = 0.05: 108 positive / 0 negative -> 108 positive / 86 negative (R: 68 / 126, the difference being 40 ties, whose sign the next entry matches); karate: 45 / 0 -> 45 / 25 (#58).Signed backbones sign an edge whose upper and lower tail p-values are exactly equal
-1, as Rbackbone3.0.4 does (its.retaintakesupper < loweras positive, in both its weighted and bipartite models); it was+1. The rule is the same for every signed scorer (disparity_filter,marginal_likelihood_filter,lans_filter,ecm_filter,noise_corrected_filter,sdsm,fdsm,fixedfill,fixedrow,fixedcoland thebackbone_from_*wrappers). Two-sided p-values do not change. Only LANS ties are significant on the reference data, so only LANS backbones change, and its signed backbones now equal R’s for everymtc: Les Misérables at alpha = 0.05, 108 positive / 86 negative -> 68 / 126; karate, 45 / 25 -> 30 / 40. In every other model a tie needs both tails of at least 0.5 (two-sided p-value 1), so only thesignattribute of untestable or non-significant edges changes, for example ECM self-loops and, under the disparity filter, edges between two degree-1 nodes. Ties are now equal by construction rather than by floating-point rounding:noise_corrected_filtersigns by the statistic, so only a score of exactly 0 is a tie, andfixedrowmakes the two tails of a symmetric hypergeometric null observed at its mean exactly equal and no longer depends on the order of the two agents. Davisfixedrow: all 7 such pairs are now-1(6 were+1) (#59).bicm/sdsm: the BiCM is now actually fitted rather than approximated with Chung-Lu, and p-values use exact Poisson-binomial tails (or RNA). SDSM-EC refits on the free cells. Davis: 3 -> 0 pairs.sdsm(andbackbone_from_projection(method="sdsm")):pvalue_method="auto"now computes exact Poisson-binomial tails whenever that is affordable, instead of switching to the refined normal approximation (RNA) above 200 artifacts. The RNA’s far upper tail is anti-conservative when a pair’s expected co-occurrence is small, as on sparse data. On a 2000 x 500 pure-noise matrix of density 0.01, the smallest p-value goes from 5.7e-37 to 3.9e-05 and Benjamini-Hochberg at 0.05 keeps 1,498 -> 0 pairs. Under SDSM’s own null (600 x 300, density 0.01) the share of p-values below 1e-4 goes from 7.2e-04 to 1.1e-06. When exact tails would be too costly,"auto"keeps them for the pairs of degree classes with the smallest expected co-occurrence, uses the RNA for the rest and warns.pvalue_method="rna"is unchanged and still matches Rbackbone. Davis and other inputs with at most 200 artifacts were already exact; their p-values change only in the last bits (#108).fdsm: the null now preserves degrees (a curveball chain; previously 197 of 200 samples broke the margins), and p-values are(count + 1) / (trials + 1). Davis: about 10 -> 5 pairs.fixedfillandfixedcoluse their exact null distributions.backbone_from_projectionandbackbone()now thresholdsdsmandfdsmatalpha, as they already did for the other models.lspar/sparsifyandlocal_degree: each node ranks its own edges (Satuluri et al., 2011; Lindner et al., 2015), and ties are kept together. Les Misérables: 136 -> 174 (LSpar) and 135 -> 91 (local degree).h_backbonefollows Zhang et al. (2018): unweighted betweenness on the full graph, normalized by N. When h = 0 no edges are kept. Les Misérables: 22 -> 18.high_salience_skeletonsplits credit among tied shortest paths, so saliences sum to n - 1.metric_backboneuses a relative tolerance;ultrametric_backboneno longer crashes on self-loops.Proximity indices exclude self-loops from neighborhoods and use out-degree for directed graphs.
doubly_stochastic_filterwarns when Sinkhorn-Knopp does not converge and no longer accepts a loose convergence check.reachabilitycounts directed reachability correctly;compare_backbonesforwardsweightto every measure that accepts it.adjust_pvalueskeeps NaN as NaN and excludes it from the number of tests (as R’sp.adjustdoes), instead of making it significant.
API conventions (NetworkX compatibility)
Every scorer raises
NetworkXNotImplementedfor multigraphs (usemultigraph_to_weighted()first), andNetworkXErrorfor missing, non-positive, NaN, or infinite weights.Bipartite functions accept generators for
agent_nodes, reject agent sets that are not one side of the bipartition, and raiseAmbiguousSolutionwhen the partition cannot be inferred.Filters keep graph, node, and edge attributes and multigraph keys.
fraction_filterignores unscored elements, andconsensus_backboneworks with node labels of mixed types and returns edges in a deterministic order.Functions that used a hardcoded
seed=42now default toseed=None, following NetworkX. Pass a seed for reproducible results.The graph-in, graph-out functions (statistical, structural, proximity, hybrid, and unweighted methods,
multigraph_to_weighted, and the threshold, fraction, and boolean filters) are dispatchable throughnx._dispatchable: they acceptbackend=and follownx.config.backend_priority, under dispatch names prefixed withbackbone_so they cannot clash with NetworkX’s own (two would have:adamic_adar_indexandresource_allocation_index). A new Backends guide shows how to use backends and records nx-parallel timings for the shortest-path methods. Bipartite, hypergraph, measure, and conversion functions are not dispatchable, since they take matrices or hyperedge lists or return numbers. TheRuntimeWarningofecm_filterandpolya_filternow points at the caller’s line (#19).SDSM-EC constraints that contradict the data (a
requiredpair that is not an edge ofB, aprohibitedpair that is) are now ignored with oneRuntimeWarningper call, pointing at the caller’s line, that counts and lists them, instead of raisingNetworkXError. This applies tosdsm,backbone_from_projectionandbackbone. Pairs naming nodes that are not an(agent, artifact)pair ofBstill raise, and so does a pair listed as both required and prohibited. When no constraint remains (all ignored, or empty lists), the p-values equal those of the unconstrained SDSM exactly. Rbackbone3.0.4 codes constraints as cell values of the incidence matrix, so a constraint there replaces the observed value instead.
Performance
Bipartite projections and null models use sparse incidence matrices and score agent pairs in row blocks, and
backbone_from_projectionbuilds edges only for significant pairs. On a 3000x800 projection this cuts run time from 40-76 s to under 2 s and peak memory from 2.2 GB to 0.23 GB; results are unchanged (#30).Exact SDSM tails truncate each Poisson-binomial convolution at the largest co-occurrence that can be observed (the smaller agent degree, or the observed count with edge constraints), which keeps every probability exact: the mass above the cut is accumulated as it crosses it. Building the tail tables is 8-60x faster (2000 x 2000 at density 0.01: 5.3 s -> 0.18 s; 5000 x 5000 at 0.002: 19.7 s -> 0.35 s) (#108).
Vectorized
sdsm/fixed*/fdsm(sdsmon 400x300: 6.4 s -> 0.36 s); MDL"node"optimizer with a lazy heap (m = 2000: 8.3 s -> 0.47 s); vectorized SVC p-values.ecm_filterfits the ECM over classes of nodes with the same degree and strength (out- and in-degree and strength for directed graphs), which is exact. Pair sums are computed in chunks, and the dense Newton polish is replaced by damped Newton steps (Newton-CG for large problems). On a 2,000-node Barabási-Albert graph the fit drops from about 90 s and 600 MB to under 2 s and 50 MB; 20,000 nodes take about 16 s and 300 MB (about 90 s for a directed version, which has 10,660 classes). The fit now also converges on directed graphs where the old solver stopped short, and nodes whose links all have weight 1 get the exact \(y_i = 0\). P-values on Les Misérables agree with the old solver to about 1e-8 (#21).
Infrastructure and documentation
New narrative tutorial, “Hypergraph backbones: keeping the groups that matter”, a Sphinx-Gallery example executed at build time on the Davis Southern Women events and a synthetic meeting log. It covers what the pairwise projection loses, the MDL backbone’s description-length trade-off and its optimizers, the local MDL aggregation rules, the statistically validated hypergraphs and cores, the structural helpers, and a HIF round trip, with six figures in light and dark variants. It is listed under “By network type” in the tutorials index and linked from the hypergraph API page and the how-to tutorial (#93, #96).
A shared figure style for the docs,
docs/_figures/style.py(docs only, not library API): color tokens checked with a palette validator against the docs theme’s light and dark backgrounds (kept and removed edges, positive and negative edges, a one-hue ramp for scores, eight categorical colors in a fixed order), matplotlib defaults, a cached layout per dataset, a helper that draws a backbone over its original graph with a legend and the counts, and helpers that save light and dark variants and write the reST that shows the one matching the reader’s theme. Sphinx-Gallery examples start from this style (#88, #96).Two narrative guides, executed when the docs are built (#95): Coming from R backbone or netbone maps each R
backbone3.0 and netbone function to nx-backbone, checks the four convention switches against the R fixtures, and reproduces R’s fixed row backbone of the Davis Southern Women p-value by p-value; Scaling up to large networks covers sparse inputs, cost classes with run times measured at build time, the ECM solver’s degree-strength classes, and the committed nx-parallel timings.The method gallery on the API pages is regenerated and redesigned. The images committed for 0.2.0 showed pre-fix results (the ECM image kept nearly every edge, where the fixed ECM keeps 72 of 254). The gallery is now generated on every docs build by
scripts/generate_visualizations.pyinto the gitignoreddocs/_gallery/; no gallery image is committed any more, and the build fails when a public method has no figure. Each figure draws the backbone over its original graph (kept edges in blue, removed edges faint), in one layout per dataset, under the method’s own name, with a legend, light and dark variants, and the edges and nodes kept in the caption and alt text. New figures coverpolya_filter,gloss_filter, a signed backbone (fixedrow(signed=True), negative edges dashed), the MDL hypergraph backbones on Davis Southern Women as a hypergraph, and the temporal scores (includingedge_persistence_hmm) on the temporal tutorial’s snapshots. The graph comparison example now draws the statistical and structural methods as small multiples, with a bar chart of the edges each keeps. The “Generate Visualization Gallery” workflow became a strict docs build on pull requests, and the deploy workflow no longer runs a separate gallery step. Generating the gallery takes about 15 s; with the lighter comparison example, a full docs build takes about 35 s instead of about 27 s (#88).New narrative tutorial, Statistical null models side by side: the seven statistical scorers on Les Misérables, with figures of p-value against weight, backbone overlap (checked against the hierarchy reported by Yassin et al., 2025), the KS distances of each backbone, the disparity filter’s
a=, the count models’unit=,transform_weights, and signed backbones with the R tie convention. It is executed at build time (#90, #96).A narrative tutorial, Bipartite projections and their backbones, executed when the docs are built: two-mode data and why the raw projection overstates ties, the five projection weights, the five null models compared against the FDSM, the FDSM p-value floor and multiple testing, signed backbones, SDSM-EC constraints (including contradictory ones), a check against R
backbonewithmissing_as_zero, and sparse incidence input, with seven figures in light and dark variants. It is listed under “By network type” in the tutorials index (#92, #96).New narrative tutorial, Temporal backbones, from snapshots to smoothing: snapshots, events and spans and binning with
freq=;flatten_temporalby count and by duration, and what a static backbone of the flattened graph misses; observation fraction, persistence and backbone stability on a persistent block among random flicker; the Kalman backbone on a planted rising block (smoother against filter, the edge-level test, the Šidák correction and temporal leakage); the persistence HMM’s posterior against raw presence; and a table for choosing among them. Five figures, executed at build time; it is listed under “By network type” in the tutorials index (#94, #96).The structural and proximity and the hypergraph tutorials no longer embed their figures in the page as base64 data: the figures are written at build time to the gitignored
docs/_static/example_figures/and linked, and load lazily, so a reader fetches only the variant (light or dark) on screen. The three dense network drawings of the structural tutorial are PNG; the charts stay SVG. The pages shrink from 3.9 MB and 1.5 MB of HTML to 0.4 MB and 0.2 MB, both tutorials get a real gallery thumbnail, and the structural tutorial’s captions, which the theme hid in light mode, show again (#96).The public API and the
HypergraphBackbone/ValidatedHypergraphresult classes have type annotations, and the package ships apy.typedmarker (PEP 561), so mypy and pyright check calls into it. Functions whose return type depends on a flag (bicm(return_labels=...),read_hif/from_*withreturn_weights=...,compare_graphs(return_diff=...)) have overloads. The decorators (not_implemented_for,py_random_state, the backend dispatcher) are typed so they keep each signature, and dispatchable functions also acceptbackend=.... NumPy, SciPy, pandas, and Matplotlib types are imported only for type checking, so the core package still needs only NetworkX at runtime. A new CI job runs non-strict mypy with thetypes-networkxstubs; the newtypingextra pins both. The API pages keep plain signatures (autodoc_typehints = "none"); the docstrings still give the types. No behavior changes (#18).The roadmap page opens with a “Roadmap at a glance” section: a stacked bar chart of item status per section, a network map of sections and their items colored by status, and the same counts as a table.
scripts/roadmap_figures.pygenerates them fromdocs/roadmap.rston every docs build, in light and dark variants, so they always match the page (#56).Docstrings are validated by the Sphinx build and by a new
numpydoc lintCI step. The checks are GL06, GL08, SS01, PR01, PR02, PR03, RT01, and EX01, configured in[tool.numpydoc_validation]. numpydoc alone now renders docstrings;sphinx.ext.napoleonwas removed (#17).The Les Misérables benchmark table is computed at docs build time from
benchmarks/benchmarks/les_miserables.py, the same definitions whose counts the tests pin (#28).An asv benchmark suite in
benchmarks/covers the statistical, bipartite, MDL and SVH/SVC hypergraph, and shortest-path methods. It comes with abenchmarkextra and a manually triggered Benchmarks workflow (#22).Cross-implementation reference tests (
tests/test_reference_implementations.py) compare p-values and backbones with Rbackbone3.0.4 and netbone 0.2.3 on Les Misérables, the karate club, and Davis Southern Women, using JSON fixtures written byscripts/make_reference_fixtures.Randscripts/make_netbone_fixtures.py(no R or netbone needed to run them). Disparity, MLF, LANS (one-tailed), BiCM, SDSM (RNA), fixed fill, fixed row,p.adjust, local degree, metric and ultrametric backbones match exactly. SDSM on Davis finds no significant pair at alpha = 0.05 in R either (smallest p-value 0.097 in R, 0.088 here with the exact tail), and FDSM finds the same 5 pairs. Documented differences: signed disparity and LANS lower tails,lsparconventions,globalthreshold strictness,mtccounting non-co-occurring pairs, R’s logistic-regression SDSM-EC fit, and netbone’s noise-corrected total, GLAB and salience tie handling, h-bridge, and MLA. R’sfdsmdefault draws samples with too few curveball trades on Davis, which biases its p-values upward by up to about 0.03 (#14).The
multiple_linkage_analysisdocumentation no longer saysmethod="lans"reproduces netbone: netbone 0.2.3’smlamis a correlation-based fit that neither method reproduces (#14).A decision note (
docs/design/reference-conventions.md) covers the 14 documented convention differences from Rbackboneand netbone. It measures each on Les Misérables, the karate club, and Davis Southern Women and recommends whether to keep ours, match the reference, or add an option. The R fixture gainslsparbackbones for Les Misérables and Davis backbones at alphas where themtcscope matters; every existing fixture value is unchanged. Themultiple_linkage_analysisNotes now compare netbone’s 134 Les Misérables links with the default’s 205 (223 was thedeviation="profile"count), and theglobal_threshold_filterNotes show how to reproduce R’s strict threshold (#59).CI on every push and pull request: ruff lint and format checks, tests and doctests on Python 3.12-3.14, a job pinned to the oldest supported versions, and a NetworkX-only job that checks the optional dependencies stay optional. Test coverage is 97%.
A CI job (and a step in the oldest-versions job) runs the dispatch tests (
tests/test_dispatch.pyandtests/test_api_contract.py, which call every dispatchable function) under NetworkX’s dispatch test harness,NETWORKX_TEST_BACKEND=nx_loopback, which checks that every dispatchable call gives the same result through a backend as without one (#19). The whole suite also passes under the harness.The test suite was condensed from 1,732 to 804 collected tests (with doctests) at unchanged line and branch coverage: sweeps over a registry (every method, style, scorer or dispatchable function) are one test per axis whose failure message names every failing case, and duplicated tests were merged. Doctests and coverage run on one Python version in CI.
An “Interop” workflow (pull requests, weekly, and on demand) installs
.[interop,test]and runspytest -m interop: round trips of hyperedges, weights, and node labels through the real xgi, HyperNetX, HypergraphX, and HAT, and HIF exchange with each library’s own reader and writer. It is a separate, non-required workflow, so an upstream release cannot block unrelated pull requests (#15).A coverage evaluation of the 15 methods compared by Yassin et al. (2025) (
docs/design/yassin-2025-coverage.md). The package covered 13 of them; the two it lacked, the Pólya urn and GloSS filters, were then added (#42, #43). The evaluation also notes where the covered methods behave differently fromnetbone(#29).The PyPI workflow builds from the release tag with its full history and fails unless the wheel and sdist carry the tag’s version; manual runs no longer upload (they had published
0.2.0.post1.dev9). The conda recipe takes its version from the tag instead of a hardcoded 0.1.0. TheDevelopment Statusclassifier is now4 - Beta(#16).New Roadmap; full numpydoc sections for the bipartite, hypergraph, and structural APIs; corrected citations (Van Nuffel, 2007; Lü et al., 2009; Lindner et al., 2015); the docs version is taken from the package metadata.
Version 0.2.0 (2026-02-14)#
Tagged v0.2.0 in git. PyPI has 0.1.0 and a development build
(0.2.0.post1.dev9), but no 0.2.0 release: pyproject.toml at the tag
still set version = "0.1.0", so the release workflow built 0.1.0 again,
and the development build came from a later manual run on main.
New
visualizationmodule:graph_difference(),compare_graphs(), andsave_graph_comparison()draw an original graph and its backbone side by side.A Sphinx-Gallery visual comparison gallery and score-then-filter examples in the documentation.
Version 0.1.0 (Initial Release)#
First public release of networkx-backbone.
Features:
47 backbone extraction functions across 8 modules
Statistical methods: disparity filter, noise-corrected filter, marginal likelihood filter, ECM filter, LANS filter
Structural methods: global threshold, strongest N ties, high salience skeleton, metric backbone, ultrametric backbone, doubly stochastic filter, h-backbone, modularity backbone, planar maximally filtered graph, maximum spanning tree backbone
Proximity methods: neighborhood overlap, Jaccard, Dice, cosine, hub promoted/depressed indices, LHN local index, preferential attachment, Adamic-Adar, resource allocation, graph distance proximity, local path index
Hybrid methods: GLAB filter
Bipartite methods: SDSM, FDSM
Unweighted sparsification: sparsify, LSpar, local degree
Filtering utilities: threshold, fraction, boolean, consensus backbone
Evaluation measures: node/edge/weight fractions, reachability, KS degree, KS weight, compare backbones