Repository navigation
Edge weights, node metrics, subnetwork extraction, null models - #8
Merged
Merged
Conversation
None of these was visible by reading the code; each was concealed either by a plausible-looking number or by a return value nothing asserted. - robustness_analysis() called set.seed(seed) unconditionally, so the documented default seed = NULL ran set.seed(NULL) and re-seeded from the clock. `set.seed(42); robustness_analysis(g)` was not reproducible and the caller's RNG stream was destroyed as a side effect. Now only seeds when asked. - calculate_roles() returned plot/roles_definitions/result with no `graph` and no `method`, breaking the shared return vocabulary. It now annotates the graph with module/role_z/role_p/role, keyed by vertex name, and reports the thresholds actually applied. test-return-contract.R now lists it by name. - The five participation-coefficient boundaries were written out three times -- classifier, roles_definitions table, plot bands -- and had drifted. The R5/R6 boundary was 0.30 in the classifier but 0.25 in the other two, so the shaded "provincial hub" region disagreed with the classification it illustrated and the table documented a threshold the code did not use. They now come from one vector in R/utils-roles.R, overridable via `thresholds`. The R2/R3 boundary is corrected from 0.60 to the published 0.62. - compare_networks() reported one number twice: edge_overlap was the identical expression to jaccard_similarity. It is now the overlap coefficient, which divides by the smaller edge set and is the informative companion to Jaccard when two networks differ greatly in size. - compare_networks() crashed on an edgeless graph, where compute_ccdf() returns zero rows and recycling a scalar into a zero-row frame is an error. Found by a test written for the defect above. compute_ccdf() now returns an empty table for degenerate input, matching summarize_graph_metrics(), which reports NaN rather than refusing. - compare_networks() used cat() where message() was wanted. find_modules()$module_table and robustness_analysis()$summary gain `result` aliases so the return vocabulary has no exceptions left; the old names are kept and documented as deprecated. 425 -> 481 expectations. R CMD check --as-cran unchanged at 2 NOTEs, both unavoidable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
netkit had three different, undocumented weight semantics at once. Measured on a 60-node sample_gnp with a confidence score attached as E(g)$weight: summarize_graph_metrics()$Diameter 3.11 vs 6.00 unweighted -> cost summarize_graph_metrics()$Avg_path 1.39 vs 2.88 -> cost find_hubs()$result$betweenness changes, r = 0.68 -> cost summarize_graph_metrics()$Modularity 0.47 vs 0.38 -> strength network_diffusion() bit-identical -> ignored igraph reads the `weight` attribute implicitly and gives it opposite meanings in different functions, while netkit's own matrix code built the adjacency without `attr` and dropped it entirely. So attaching a confidence score -- the most common edge annotation in the domain netkit targets -- inverted every path-based metric, was read correctly by community detection, and vanished from diffusion. R/utils-weights.R adds as_netkit_weights(), the companion to as_netkit_graph(): it resolves `weights` (NULL, an attribute name, or a numeric vector), validates it, and derives *both* a strength and a cost vector so each metric receives the one it needs. `weight_type` declares which kind the input is. Threaded through summarize_graph_metrics(), prepare_diffusion(), network_diffusion(), network_diffusion_with_pvalues(), greedy_seed_selection(), find_hubs(), find_bottlenecks(), calculate_roles(), find_modules(), robustness_analysis(), compare_networks() and plot_CCDF(). Notable decisions: - `weights = NULL` ignores weights even when the graph has the attribute, and warns that it is doing so. Explicit beats inherited-by-accident, and the warning means the behaviour is never silent again. This differs from igraph. - cluster_edge_betweenness() is given costs, not strengths: it works by removing high-betweenness edges, so the strength vector would invert its notion of which edges are central. Every other algorithm gets strengths. - collapse_to_undirected() carries weights as edge attributes through as_undirected(mode = "collapse"), which renumbers edges. The default edge.attr.comb would have dropped them silently. - calculate_roles() pairs each incident edge with its own far endpoint rather than zipping incident() against neighbors(); those are sorted by edge id and vertex id respectively and need not correspond. - plot_CCDF() gets a separate continuous code path for strengths, since the integer-degree tabulation cannot represent them. Also fixed while in here: - network_diffusion_with_pvalues() never called as_netkit_graph(), so the data.frame edge list it documents failed with igraph's own error. It was the one export missing from test-input-contract.R. - Same unconditional set.seed(seed) as robustness_analysis() had. - n_seeds was counted before intersecting with the graph's vertices, so any absent seed made every permuted set larger than the real one, inflating the null and biasing every p-value. - RWR's convergence threshold is now relative to sum(abs(f0)) rather than absolute, and the unbounded `repeat` has an iteration cap. Both matter now that `seed_weights` lets callers start from values of any scale. New: network_diffusion(seed_weights =) to diffuse from a continuous signal rather than set membership, and robustness_analysis(removal_strategy = "strength"). 482 -> 558 expectations; one warning, the pre-existing `future` build artifact. R CMD check --as-cran unchanged at 2 NOTEs, both unavoidable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rics() netkit described graphs but not their vertices: there was no single call for per-node centrality, so a ranking had to be stitched together from individual igraph calls. node_metrics() computes eleven metrics -- degree, strength, betweenness, closeness, harmonic, eigenvector, pagerank, coreness, local clustering, Burt's constraint, eccentricity -- and returns the standard plot/result/graph/method shape. Because `graph` carries every metric as a vertex attribute, this also makes robustness_analysis()'s documented "any numeric vertex attribute" removal strategy usable without extra steps: g <- node_metrics(g)$graph robustness_analysis(g, removal_strategy = "pagerank") Design notes: - `closeness` is excluded from the defaults. igraph averages distance over *reachable* vertices only, so on a fragmented graph a node in a small isolated component scores higher than an equally well-placed node in the giant component -- ranking by closeness puts the periphery on top. `harmonic` has no such failure mode because unreachable pairs contribute 1/Inf = 0 rather than being dropped from the denominator. Both behaviours are pinned by a test on the 5-clique + 4-clique fixture. - The default plot is a Spearman correlation heatmap, not a ranking. The usual mistake with a table like this is to read the metrics as independent evidence when betweenness, eigenvector centrality and PageRank often correlate with degree above 0.9; the heatmap shows that before the ranking is interpreted. - `max_nodes` (default 5000, matching summarize_graph_metrics()) skips the O(VE) metrics on large graphs, warns, and returns them as NA rather than omitting the columns -- so the column set never depends on the graph's size. - `degree` is always the unweighted count and `strength` the weighted sum, so neither column changes meaning with the arguments. Metrics are pinned to hand-computed values on the 5-node ring (betweenness 1, harmonic 3, eccentricity 2, clustering 0, pagerank 1/5) and to properties elsewhere: degree equals the adjacency row sums, coreness never exceeds degree, pagerank sums to 1, normalization preserves ranking, and unit weights reproduce the unweighted result. 558 -> 621 expectations. R CMD check --as-cran unchanged at 2 NOTEs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The most common real task in netkit's target domain -- "here is my gene list, show me how these are connected" -- had no entry point. Five strategies: - "induced": the plain induced subgraph, the baseline to compare against. - "neighbors": the order-step neighbourhood, with `max_degree` to exclude promiscuous hubs. Without that cap, first-neighbour expansion on a hub-dominated network returns most of the network. - "shortest_paths": the union of all shortest paths between every seed pair. - "steiner": an approximate minimum Steiner tree (Kou-Markowsky-Berman: metric closure, MST of the closure, path expansion, leaf pruning), within 2 - 2/|terminals| of optimal. - "diffusion": reuse the existing kernel, keep the top_n scoring vertices. Returns result/graph/method; no `plot` element, since like plot_Net(), find_modules() and highlight_nodes() it renders through base plot.igraph. Two things found by testing rather than by reading: - The Steiner helper originally returned only the vertex *names* of the tree it built, and the caller then took the induced subgraph on them -- which put back exactly the cycle-closing edges the spanning-tree step had removed. A method named after a tree was returning a subgraph with cycles (8 edges on 7 nodes). The helper now hands back the tree itself. The first version of the tree test passed only because the induced subgraph happened to be a tree on that one fixture; it now runs across three. - The unit-weight identity property does not hold for the two path-based methods, and should not be asserted. igraph uses breadth-first search when no weights are given and Dijkstra when they are, and the two break ties between equal-cost paths differently, so declaring all-equal weights can yield a different near-minimal tree. The test now asserts what is actually well defined -- that the pairwise distances agree, that both results are trees spanning the terminals, and that their costs are within the method's stated approximation bound of each other -- and the behaviour is documented rather than papered over. 621 -> 693 expectations. R CMD check --as-cran unchanged at 2 NOTEs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…null
netkit could describe a graph but not say whether any of it was surprising. A
modularity of 0.42 means nothing on its own -- degree-preserving random graphs
routinely reach 0.3 -- and the only statistical statement the package made was a
KS test on two degree vectors.
- null_model(): ensembles under "rewire" (degree-preserving double-edge swaps,
the default), "configuration" (sample_degseq, falling back from "vl" to
"configuration.simple" with a warning where no connected realisation exists),
or "erdos_renyi" (the deliberate weak contrast). Weights are permuted across
the null's edges so the distribution is preserved and the placement is not.
- metric_significance(): composes directly with summarize_graph_metrics(), so no
new metric definitions. Reports observed, null mean and sd, z, two-sided
empirical p and the null's 2.5/97.5 percentiles. p uses (r+1)/(n+1) and so is
never 0; the method string states the resulting floor, because 20 nulls cannot
produce evidence at p < 0.05 however large the effect. Metrics the null holds
fixed (Nodes, Edges, Density) are excluded from automatic selection, since
their z is undefined, but are reported as NA if requested explicitly.
- small_worldness(): sigma = (C/C_rand)/(L/L_rand). omega is deliberately NOT
shipped: it needs a lattice reference, igraph has no degree-preserving
latticisation, and an approximate one would make omega quietly dependent on
how well that approximation worked.
- network_diffusion_with_pvalues(null = "degree_matched"), now the default.
Diffusion scores are strongly degree-dependent, so a uniform seed permutation
compares a real seed set -- usually enriched for well-connected vertices --
against a null of mostly poorly connected ones, and reports the degree
difference as significance. On a 150-node scale-free graph seeded with its five
largest hubs, the uniform null calls 157 nodes significant at p < 0.05 and the
matched null 134. This changes every p-value the function produces; the package
is unreleased, so this is the moment to pick the better default.
Getting the matching right took three attempts, and the first two are recorded
in the code because both looked correct:
quantile bins of degree value -- collapse on a heavy-tailed distribution.
Most vertices of a scale-free graph have degree 1, so most quantiles
coincide, unique() folds them together, and the surviving top bin spans
degree 3 to the largest hub. Mean seed degree 11.2 -> null 5.9 (uniform 1.7).
quantile bins of degree rank -- equal-sized by construction but still wide.
Ten bins over 200 vertices is twenty per bin, degrees 1..17 inside the top
one. No better: 5.2.
nearest-degree matching -- each seed replaced from the match_pool candidates
closest to it in degree. Per-seed median absolute degree difference 0.
The residual set-level gap is real and is now warned about rather than assumed
away: the six highest-degree vertices of a scale-free graph have no comparable
vertices to be permuted with, so no permutation null can fully correct for
degree there. That is a property of the graph, not of the sampling.
Two of my own bugs, both caught by tests rather than by reading:
- match_pool = Inf was documented as reducing "exactly" to the uniform null but
consumed the RNG differently, so it was only equivalent in distribution. It now
returns the uniform sampler outright.
- shuffle_weights = FALSE left model = "rewire" inheriting the observed `weight`
attribute bound to edges that had since been rewired -- neither the observed
weighting nor a declared permutation of it. The nulls are now unambiguously
unweighted.
693 -> 815 expectations. 21 executable examples in ~1.1s.
R CMD check --as-cran unchanged at 2 NOTEs, both unavoidable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Version 0.0.1.9000 -> 0.1.0.9000. These five commits add five exports and make three behaviour changes, so a patch bump would misrepresent them. - DESCRIPTION Description expanded to cover the new capabilities, with the Kou-Markowsky-Berman DOI alongside the existing Guimera-Amaral one. - NEWS.md gains a real entry with "Breaking changes" first, since the weight default, the diffusion null, the robustness RNG fix and the edge_overlap redefinition all change results for existing code. - _pkgdown.yml had no `reference:` section, so all 21 exports landed in one ungrouped list. Now grouped into nine topics. - British spellings normalized to match the declared Language: en-US, and inst/WORDLIST extended from 62 to 100 entries. spell_check_package() is clean. One self-inflicted bug from the spelling pass, caught by the suite: the search-and-replace rewrote `@importFrom dplyr ... summarise ...` to `summarize` while the protected call site kept the correct spelling, so the import vanished and robustness_analysis() failed with "could not find function summarise" in 8 tests. The tag is restored. 815 expectations, 0 failures, 1 warning (the pre-existing `future` build artifact). R CMD check --as-cran: 2 NOTEs, both unavoidable. Build tarball carries no dev/, .claude, README.html or cran-comments.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The weight contract is the one place netkit's behaviour is genuinely subtle, and a parameter description cannot carry it: the point is that the same numbers mean opposite things depending on a declaration the caller makes, and that is only convincing worked through. The first draft of the central comparison was wrong in a way worth recording. It put the two readings' diameters side by side and called the difference an order of magnitude -- but one diameter is measured in units of confidence and the other in units of 1/confidence, so the comparison was meaningless, and the article would have been committing the error it exists to warn about. It now compares the share of a crossing path that the bridge accounts for, which is unit-free: 5% read as a strength, 83% read as a cost, from the identical input. R CMD check --as-cran still 2 NOTEs; both vignettes rebuild. spell check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README and the tutorial vignette both still described the 0.0.1 feature set,
so none of the five new exports was discoverable from the front door.
- README.Rmd gains a "What it does" summary covering all nine topic areas and a
short section on the edge-weight contract, plus a worked chaining example.
Regenerated README.md from it (never hand-edited).
- introduction.Rmd gains three new sections: node-level metrics (3.2), null
models and metric significance (3.3), and subnetwork extraction (4). The
subsequent sections are renumbered. It renders in ~5s.
- Fixed the pre-existing \VignetteIndexEntry{introduction} / YAML title
mismatch that rmarkdown warned about on every render.
Two things caught by the check rather than by reading:
- The first README draft used `g |> node_metrics() |> _$graph` for the chaining
example. The `_` placeholder needs R >= 4.2 and extraction from it R >= 4.3,
while DESCRIPTION declares R >= 4.1. Replaced with plain reassignment.
- Linking to the not-yet-published pkgdown article page made
`R CMD check --as-cran` report a 404 under "possibly invalid URLs". The link
is removed with a note to restore it; pkgdown::build_site() cannot run in this
sandbox (it needs to write to ~/Library/Caches), so docs/ remains stale and
is the one item left outstanding.
815 expectations, 0 failures. R CMD check --as-cran: 2 NOTEs, both unavoidable.
spell check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caught by spell_check_package() after the previous commit; Language is en-US. 'pkgdown' added to inst/WORDLIST. Spell check is clean and R CMD check --as-cran is unchanged at 2 NOTEs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerated from the current sources, so docs/ is no longer stale. Covers the five new reference pages (node_metrics, extract_subnetwork, null_model, metric_significance, small_worldness) plus the netkit-weights topic, the new weighted-networks article, the nine reference groups from _pkgdown.yml, the rewritten index from README.Rmd, the NEWS entry, and version 0.1.0.9000. articles/introduction.html picked up the three new tutorial sections automatically, since it is generated from vignettes/introduction.Rmd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The link was removed last session because the page did not exist yet and R CMD check --as-cran reported it under "possibly invalid URLs". The page now exists in docs/, so the link is correct again -- but it only resolves once these commits reach origin/main, since GitHub Pages serves this site from main/docs. Until that push lands, `R CMD check --as-cran` reports the 404 as a line inside the existing "CRAN incoming feasibility" NOTE. The NOTE count stays at 2; the line clears on deployment. 815 expectations, 0 failures. Spell check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
agallinat
added a commit
that referenced
this pull request
Oct 5, 2026
PR #8 merged feat/defect-sweep server-side; the only commit it brings that we did not already have is 08cb673, which replaced the "Test environments" TODO in cran-comments.md with the five GitHub Actions platforms, all of which have now passed. Rewriting that file for 0.1.0 reinstated the TODO, so this merge takes their side -- the TODO existed precisely until a CI run existed, and one does. Also drops the paragraph explaining the "Version contains large components" note. That note was a consequence of the .9000 development suffix; DESCRIPTION is now 0.1.0 and --as-cran no longer reports it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See NEWS.md. Three behaviour changes: weights ignored unless requested, degree-matched diffusion null by default, robustness_analysis() no
longer re-seeds the RNG.