Skip to content

Epidemiological metrics for netdiffuseR - #79

Open
aoliveram wants to merge 42 commits into
masterfrom
issue-78-epidemiological-metrics
Open

Epidemiological metrics for netdiffuseR#79
aoliveram wants to merge 42 commits into
masterfrom
issue-78-epidemiological-metrics

Conversation

@aoliveram

Copy link
Copy Markdown
Member

Refs #78.

This draft PR tracks the implementation of epidemiological analysis capabilities in netdiffuseR, addressing the four gaps identified in the internal gap report:

  • Gap 1 — no support for time-of-disadoption (tod) / reinfection episodes.
  • Gap 2 — missing epi metrics (SAR, survival, peak, generation time, Rₜ).
  • Gap 3 — no transmission-tree representation.
  • Gap 4 — stochastic transmission / adoption (partially covered by the pre-existing stochastic-transmission branch, now integrated here).

This branch starts from master and has already integrated two upstream feature branches:

  • issue-75-epigames-dynamic-attrs — dynamic behavioral attributes for Epigames.
  • stochastic-transmissionmode = "stochastic" for exposure() / rdiffnet().

No new dependencies will be added to DESCRIPTION.

Implementation milestones

#MilestoneFiles touched
M1Infra: \$tod slot, \$transmission slot, validators, coercionsR/diffnet-class.r, R/adjmat.r, new R/transmission.R
M2Gap 4A: pluggable link_fun in exposure()R/stats.R
M3Gap 4B: continuous-weight regression teststests/testthat/
M4Gap 4C: adoption_model = \"logit\" in rdiffnet()R/rdiffnet.r
M5Gap 2 easy: SAR, survival, peak, generation_timenew R/epi_metrics.R
M6Gap 1: hazard_rate(), plot_adopters() under \$todR/stats.R, R/diffnet-methods.r
M7Gap 2 repr_number: repr_number() + plot methodR/epi_metrics.R
M8Epigames parser: histories.csv → \$transmissiondata-raw/
M9Vignette vignettes/epidemiological-analysis.Rmdnew vignette
M10Docs + CI + pkgdowntests/testthat/, .github/workflows/

…ion to 1.25.0, and fix exposure rownames issue
…Issue #75)
- Add data-raw/epigames.R: bundles epigames_hourly + dynamic_attrs_hourly.csv
into epigames list with new $dyn_attrs slot (long format, 201,366 rows)
- Add data-raw/epigamesDiffNet.R: collapses hourly attrs to 15 daily windows,
populates vertex.dyn.attrs with mask/med/quarantine proportions per day
- Regenerate data/epigames.rda and data/epigamesDiffNet.rda
Dynamic attributes (mask, med, quarantine) now visible in print(epigamesDiffNet):
Dynamic attributes: mask, med, quarantine (3)
Validated: exposure(epigamesDiffNet, attrs = 'mask') works with time-varying
data. Correlation with static proxy = 0.88, confirming dynamic attrs
capture additional temporal variation.
- Removed redundant comments and sanity checks for better readability
- Simplified as_diffnet call structure
- Regenerated .rda files to match clean scripts
- new_diffnet() gains a -tod- argument (single-behavior vector) with
validation (element-wise tod > toa, NA where toa is NA, length match).
- When -tod- is supplied, cumadopt is reconstructed from the intervals
[toa, tod - 1] via cumadopt_from_intervals() in R/adjmat.r.
- New $transmission slot plus exported helpers as_transmission_tree()
and get_transmissions() (R/transmission.R) storing the directed
infection forest as a data.frame with columns date, source, target,
source_exposure_date, virus_id, virus. Docs cite Lloyd-Smith et al.
(2005) and White & Pagano (2008).
- Tests cover interval reconstruction, validation errors, coercion,
and the transmission slot round-trip.
No new package dependencies.
@aoliveramaoliveram changed the title Epidemiological metrics for netdiffuseR (#78)Epidemiological metrics for netdiffuseRApr 17, 2026
aoliveramand others added 4 commits April 17, 2026 17:13
Avoid masking epiworldR::get_transmissions() when both packages are
loaded. No behavior change; exported name and docs renamed, tests
updated accordingly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds link_fun + link_pars arguments to exposure(). Supported named
kernels: identity (default), linear (min(beta*w, 1)), sigmoid
(plogis((w-h)/scale)), wells-riley (1 - exp(-beta*w)). Custom user
functions are accepted with either signature function(w) or
function(w, pars). Non-identity kernels force valued = TRUE with a
warning. Default behavior unchanged; 639 pre-existing tests still pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ange weights (M3, #78)
Adds four test_that blocks to tests/testthat/test-stochastic-exposure.R
covering each link kernel under seconds-scale weights, the
degree-denominator fix, and zero-weight self-loop handling. Small code
changes in R/stats.R: (1) warn when stochastic mode sees post-kernel
weights outside [0, 1] -- the sampler silently saturated before;
(2) denominator now counts non-zero-weight neighbours instead of every
stored entry, so link kernels that zero out some edges no longer
inflate the normaliser. No NAMESPACE / Rd changes. 652 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the dual signature for user-supplied link_fun. The helper now
always calls `link_fun(W@x)` with a single argument; parameters are
expected to be baked into the closure. `link_pars` remains relevant
only for the named kernels ("linear", "sigmoid", "wells-riley").
Simpler API, simpler docs, simpler tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aoliveram
aoliveram requested a review from gvegayonApril 20, 2026 15:57
@aoliveramaoliveram self-assigned this Apr 20, 2026
Adds adoption_model = c("threshold", "logit") and adoption_pars
arguments to rdiffnet(). In logit mode, node i adopts at step t with
probability plogis(beta0 + beta_expo * exposure_i), drawn from
runif() each step. Threshold stays the default so fixed-seed calls
that omit the new argument remain bit-identical.
15 new tests cover backward compatibility, parameter validation,
saturation / suppression extremes, and multi-behaviour runs. Full
suite: 665 pass, 0 fail.
…tic/stochastic (M4 follow-up, #78)
Renames the two values accepted by adoption_model in rdiffnet() so
the vocabulary matches the existing exposure.mode (which already
uses deterministic/stochastic). No behavior change:
- adoption_model = c("deterministic", "stochastic"), default
"deterministic" (was "threshold")
- "stochastic" keeps the same plogis(beta0 + beta_expo * exposure)
Bernoulli draw previously called "logit"
- Error message, roxygen doc and man page updated to match
- Test file renamed test-rdiffnet-logit.R -> test-rdiffnet-stochastic.R;
all 15 existing expectations updated to use the new names
Full suite: 665 passed, 0 failed.
@aoliveram

aoliveram commented Apr 22, 2026

Copy link
Copy Markdown
MemberAuthor

The milestones tabla have been updated, following @gvegayon's comments:

#MilestoneStatusNotes
M1$tod vector + $transmission slot, validatorslanded$tod part reformulated in M5; $transmission part migrates to M7
M2Pluggable link_fun in exposure()landedno changes
M3Continuous-weight tests + safety warningslandedno changes
M4adoption_model string APIlandedsuperseded by M6
M5$status array as canonical representation. as_diffnet(graph, toa) intact; as_diffnet(graph, status) added for multi-cycle. tod argument removed from constructor before mergeplannedR/diffnet-class.r, R/adjmat.r
M6adoption_mechanism as function. Exports adoptmech_threshold, adoptmech_logit, adoptmech_probitplannedR/rdiffnet.r, new R/adoption_mechanisms.R
M7diffnet_epi subclass + as_diffnet_epi()plannednew R/diffnet-epi.R
M8Native lineage tracking in rdiffnet() via source_attribution callback with _uniform / _weighted / _earliest; auto-promotes to diffnet_epi + adoption_mechanism extension: pass behavior = q and expo_all` (n × 1 × Q) to enable per-behavior couplingplannedR/rdiffnet.r, new R/source_attribution.R, update R/adoption_mechanisms.R
M9plot_transmission_tree() for diffnet_epi — first goal: unambiguous casesplannednew R/plot_transmission_tree.R
M10SAR, survival, peak, generation_time (diffnet_epi methods)plannednew R/epi_metrics.R
M11hazard_rate() and plot_adopters() status-awareplannedR/stats.R, R/diffnet-methods.r
M12repr_number() + offspring plotplannedR/epi_metrics.R
M13Epigames histories.csv parser → $transmissionplannednew data-raw/ helper
M14Vignette epidemiological-analysis.Rmdplannednew vignettes/

Order of execution: M6 → M5 → M7 → M8 → M9 → M10+.

@aoliveram
aoliveram marked this pull request as ready for review July 3, 2026 20:40
CopilotAI review requested due to automatic review settings July 3, 2026 20:40

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds epidemiological-analysis capabilities to netdiffuseR by extending the core diffnet representation with a canonical multi-cycle state ($status) and an optional transmission-tree layer (diffnet_epi), then building simulation hooks (rdiffnet() + stochastic exposure/adoption/disadoption + lineage tracking) and metrics/tests on top of those primitives.

Changes:

  • Introduces diffnet_epi + transmission-tree attach/reconstruct/access helpers ($transmission, as_diffnet_epi(), as_transmission_tree(), transmission_tree_from_events()).
  • Adds multi-cycle diffusion support via a canonical $status representation and new accessors (toa()/tod() + long-format toa_all()/tod_all()), with new_diffnet() supporting status=.
  • Extends simulation and exposure tooling: stochastic exposure (mode="stochastic"), link kernels (link_fun), pluggable adoption mechanisms, disadoption mechanism factories, and optional source attribution (lineage tracking) in rdiffnet().

Reviewed changes

Copilot reviewed 47 out of 55 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
DESCRIPTIONBumps version and collate order to include new R modules (adoption/disadoption, epi, transmission, accessors).
NEWS.mdNotes new stochastic exposure support.
NAMESPACEExports new epi APIs/metrics and registers new S3 methods.
R/adoption_mechanisms.RAdds pluggable adoption-mechanism kernels (threshold/logit/probit).
R/adjmat.rAdds status-array helpers (status_mat, toa_from_status) and validate_status().
R/data.rUpdates epigamesDiffNet documentation with valued/non-cumulative notes and reconstruction snippet.
R/diffnet-class.rExtends new_diffnet() to accept status= and transmission= and installs canonical $status.
R/diffnet-epi.RImplements diffnet_epi subclass promotion, printing, and reconstruction via attribution.
R/diffnet-indexing.rKeeps $status synchronized with $cumadopt during subsetting.
R/disadoption_mechanisms.RAdds disadoption-mechanism factories (random/bithreshold/logit/probit).
R/random_graph.RKeeps $status synchronized with $cumadopt in graph generators.
R/rdiffnet.rAdds stochastic exposure mode, adoption mechanism plug-in, disadoption-aware status construction, and lineage tracking via source attribution.
R/rewire.rKeeps $status synchronized with $cumadopt during rewiring.
R/source_attribution.RAdds source-attribution kernels + normalization + tree-row assembly helper.
R/stats.RExtends exposure() with stochastic mode + link kernels; updates hazard-rate to use $status.
R/status_accessors.RAdds toa/tod and long-format toa_all/tod_all accessors built on $status.
R/transmission.RAdds transmission-tree schema, attach/access helpers, and post-hoc reconstruction from events.
data-raw/epigames.RAdds hourly dynamic attributes ingestion and bundles into epigames.
data-raw/epigamesDiffNet.RRebuilds epigamesDiffNet as daily, valued, non-cumulative and reconstructs a tree via attribution.
man/adoption_mechanisms.RdDocuments adoption mechanisms.
man/as_transmission_tree.RdDocuments as_transmission_tree().
man/diffnet-class.RdDocuments new new_diffnet() args (status, transmission, transmission_pars).
man/diffnet_epi.RdDocuments diffnet_epi, promotion, and print behavior.
man/disadoption_mechanisms.RdDocuments disadoption mechanisms.
man/epigamesDiffNet.RdDocuments valued/non-cumulative semantics and reconstruction snippet.
man/exposure.RdDocuments mode, link_fun, and link_pars additions.
man/generation_time.RdDocuments generation_time() metric.
man/peak_prevalence.RdDocuments peak_prevalence() / peak_time().
man/rdiffnet.RdDocuments new rdiffnet() args (exposure.mode, mechanisms, source attribution).
man/repr_number.RdDocuments repr_number() metric and plot/print methods.
man/secondary_attack_rate.RdDocuments secondary_attack_rate() metric.
man/source_attribution.RdDocuments source-attribution kernels.
man/status_accessors.RdDocuments status-based accessors.
man/summary.diffnet_epi.RdDocuments summary.diffnet_epi() extension block.
man/survival_curve.RdDocuments survival_curve() metric and print method.
man/transmission_tree.RdDocuments transmission_tree() accessor.
man/transmission_tree_from_events.RdDocuments post-hoc tree reconstruction primitive.
man-roxygen/graph_template.RHardens roxygen template logic for parameter inclusion.
tests/testthat/test-cumulative_adopt_count.RAdds hazard-rate regression for non-monotone status.
tests/testthat/test-diffnet-epi.RAdds promotion/printing/inheritance tests for diffnet_epi.
tests/testthat/test-diffnet-methods.RAdds plot_adopters() regression for non-monotone status.
tests/testthat/test-epi-metrics.RAdds end-to-end tests for peak/survival/SAR/gen-time/R and summary block.
tests/testthat/test-exposure-link-fun.RAdds kernel/link-function tests for exposure.
tests/testthat/test-rdiffnet-cross-coupling.RTests per-behaviour dispatch + cross-state visibility in adoption mechanism.
tests/testthat/test-rdiffnet-disadoption.RTests disadoption mechanisms + composition and regressions.
tests/testthat/test-rdiffnet-source-attribution.RTests source attribution kernels and lineage tracking behavior.
tests/testthat/test-rdiffnet-stochastic.RTests stochastic adoption mechanisms and user plug-ins.
tests/testthat/test-status-slot.RTests canonical $status semantics, constructor behavior, and accessors.
tests/testthat/test-stochastic-exposure.RTests stochastic exposure behavior, weighting, and bounds.
tests/testthat/test-transmission.RTests transmission-tree attach/access/validation and reconstruction agreement.
Files not reviewed (6)
  • man/adoption_mechanisms.Rd: Generated file
  • man/as_transmission_tree.Rd: Generated file
  • man/diffnet-class.Rd: Generated file
  • man/diffnet_epi.Rd: Generated file
  • man/disadoption_mechanisms.Rd: Generated file
  • man/epigamesDiffNet.Rd: Generated file

Comment threadR/adjmat.r
Comment on lines +616 to +617
if (any(!status %in% c(0L, 1L, NA_integer_, 0, 1)))
stop("-status- entries must be 0 or 1.")
Comment threadR/adjmat.r
Comment on lines +631 to +632
if (any(!status[[q]] %in% c(0L, 1L, NA_integer_, 0, 1)))
stop("-status[[", q, "]]- entries must be 0 or 1.")
Comment threadR/transmission.R
Comment on lines +101 to +105
src <- suppressWarnings(as.integer(tree$source))
src_ok <- src[!is.na(src)]
if (length(src_ok) && (any(src_ok < 1L) || any(src_ok > n)))
stop("-tree$source- must be NA or an integer index in 1..", n, ".")

Comment threadR/status_accessors.R
Comment on lines +69 to +74
if (is.null(s)) {
# Defensive — every diffnet built post-status-refactor has a $status
# slot, but keep this safe against any older object that might still
# be in scope.
return(rep(NA_integer_, length(x$toa)))
}

@gvegayongvegayon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK @aoliveram, thanks for all the work, but I am affraid that ~7K new lines of code is too much to do in a single pass of a PR (up to 1K is OK, in my standards). Let's do the following:

  • Prepare a presentation of the changes where you explain the new features and map those back to the code.
  • Coordinate a meeting so you cna present this to Tom and me.
  • After evaluating your presentation, we can think about restructuring the PR and splitting it into smaller chunks.

It is OK to be ambitious, but we need to make smaller contributions at a time; otherwise, the code quality diminishes.

Comment on lines +1 to +7
#' @param graph <%= ifelse(exists("dynamic", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(dynamic))), "A dynamic graph", "Any class of accepted graph format") %> (see \code{\link{netdiffuseR-graphs}}).
#' <%=ifelse(exists("self", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(self))), "@param self Logical scalar. When \\code{TRUE} autolinks (loops, self edges) are allowed (see details).", "") %>
#' <%=ifelse(exists("multiple", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(multiple))), "@param multiple Logical scalar. When \\code{TRUE} allows multiple edges.", "") %>
#' <%=ifelse(exists("valued", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(valued))), "@param valued Logical scalar. When \\code{TRUE} weights will be considered. Otherwise non-zero values will be replaced by ones.", "") %>
#' <%=ifelse(exists("undirected", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(undirected))), "@param undirected Logical scalar. When \\code{TRUE} only the lower triangle of the adjacency matrix will considered (faster).", "") %>
#' <%=ifelse(exists("toa", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(toa))), "@param toa Integer vector of length \\eqn{n} with the times of adoption.", "") %>
#' <%=ifelse(exists("slice", inherits = FALSE) && isTRUE(suppressWarnings(as.logical(slice))), "@param slice Integer scalar. Number of slice to use as baseline for drawing the graph.", "") %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the idea of these changes?

Comment threadR/diffnet-class.r
Comment on lines +359 to +377
#' @param status Optional state representation. Single-behavior: an
#' \eqn{n \times T} integer matrix with \code{1} on the cells where node
#' \eqn{i} is adopted at time \eqn{t} and \code{0} otherwise (need not be
#' monotone — multi-cycle adoption / disadoption is supported). Multi-behavior:
#' a length-\eqn{Q} list of such matrices. When \code{status} is supplied,
#' it becomes the canonical state of the diffnet and \code{toa} is derived
#' from it as the first time each node enters the adopted state. Passing
#' both \code{toa} and \code{status} emits a warning and uses \code{status};
#' the warning reports whether the supplied \code{toa} is consistent with
#' the \code{toa} derived from \code{status}.
#' @param transmission Optional transmission tree (who-infected-whom). Either
#' a \code{data.frame} with the columns documented in
#' \code{\link{as_transmission_tree}}, or a pre-built transmission list with
#' components \code{tree} and \code{pars}. When supplied, the returned
#' object is promoted to the \code{\link{diffnet_epi}} subclass. \code{NULL}
#' (default) returns a plain \code{diffnet}.
#' @param transmission_pars Optional named list stored verbatim in
#' \code{x$transmission$pars}. Only consulted when \code{transmission} is a
#' data.frame.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @param tag should be short. Details go in the details section.

Comment threadR/diffnet-class.r
Comment on lines +615 to +625
# Step 0.2: Resolve t0 / t1 defaults ----------------------------------------
if (is.null(t0))
t0 <- if (!is.null(status)) 1L else min(toa, na.rm = TRUE)
if (is.null(t1)) {
if (!is.null(status)) {
T_ <- if (is.list(status)) ncol(status[[1L]]) else ncol(status)
t1 <- t0 + T_ - 1L
} else {
t1 <- max(toa, na.rm = TRUE)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change should be documented in the description. Moving from using min/max (which is clear to the user) to use NULL without saying what will happen is not good.

Comment threadR/diffnet-class.r

# Step 2.1: Checking class of TOA and coercing if necessary
if (!inherits(toa, "integer")) {
if (!is.integer(toa)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the best practice here? Why the change?

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@aoliveram@gvegayon