Add latent infection class "MobilitySubpopulationInfections" for contact-mixing models
I maintainer di solito rispondono entro 6 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 55/100
Direzione di ricerca
Start in pyrenew/latent by reading SubpopulationInfections and BaseLatentInfectionProcess to learn the latent-process contract, then trace PyrenewBuilder.configure_latent and MultiSignalModel.sample() to see where runtime latent_data and population values must flow. Implement the mixing equations as written, add the runtime validation hook, and wire counts conversion in MultiSignalModel. Done when the required checks pass: identity mobility reproduces SubpopulationInfections, non-column-stochastic matrices are rejected before sampling, and fitting/prediction accept different mobility matrices.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Estimate time-varying $\mathcal{R}(t)$ for each subpopulation when residents of one subpopulation spend part of each day in others. Infections among residents of a region then depend on transmission in the regions they visit and on the infectious visitors their own region receives.
The new class implements Equation 2 of Roy, Clapham, and Mishra (2025),
Incorporating human mobility to enhance epidemic response and estimate real-time reproduction numbers.
This issue states the computation the class performs, the inputs it accepts, and what it does not do. The tests listed at the end check that contract.
Prerequisites
These merge in this order:
- PR #897: time-varying ascertainment. It changes no code in
pyrenew/latent/. - #903, fixed by PR #904: subpopulation count observations are scaled by the subpopulation's population (see Infection counts).
- #902, fixed by PR #906: removes
infection_process. Infection feedback becomes a constructor option ofPopulationInfectionsandSubpopulationInfections. - #905, fixed by PR #907:
log_rt_time_0_rvcan give each subpopulation its own starting $\mathcal{R}(t)$.
Background
SubpopulationInfections gives each subpopulation its own renewal equation.
Subpopulation $\mathcal{R}(t)$ trajectories share a baseline, but infections in one subpopulation do not depend on infections in any other.
MobilitySubpopulationInfections keeps the same $\mathcal{R}(t)$ structure and couples the renewal equations through a fixed mobility matrix,
such as one derived from census commuting data.
The model: Roy et al. Equation 2
For home region $i$ on day $t$:
$$
I_{t,i} = \sum_{j=1}^{n} \frac{m_{ji}, S_{t,i}}{\sum_{l=1}^{n} m_{jl} N_l}; R_{t,j} \left[ \sum_{k=1}^{n} m_{jk} \sum_{\tau=0}^{t-1} I_{\tau,k}, g_{t-\tau} \right]
$$
with $S_{t,i} = N_i - \sum_{s<t} I_{s,i}$, the susceptible residents of region $i$ (Eq 1 applied to each region). This assumes infection confers lasting immunity (Roy et al. section 2.2).
The paper writes the Eq 1 sum from $s = 1$ while its renewal sum starts at $\tau = 0$. The authors' code subtracts every day's infections, including the first, and this class does the same.
- $m_{jk}$: fraction of residents of home region $k$ present at location $j$
- $N_k$: number of residents of region $k$
- $R_{t,j}$: reproduction number at location $j$, where transmission happens
- $g$: generation interval
The paper writes $M$ for the number of regions and $m_{ji}(t)$ for a matrix that may change over time. Here M is the matrix, n is the number of regions, and in this version the matrix is fixed.
Mobility matrix convention
M[j, k] is the fraction of residents of home region k who are present at location j. Columns are home regions (origins), rows are locations (destinations), and each column sums to 1. This is the paper's $m_{jk}$.
A matrix with home regions in its rows (rows sum to 1) must be transposed before use.
Regions must be in the same order in M, subpop_fractions, a vector-valued I0_rv, a vector-valued log_rt_time_0_rv, and every observation's subpop_indices.
The computation, one day at a time
PyRenew's latent infections are per resident: infections[t, k] is the number of new infections among residents of home region k on day t, divided by the number of residents of k. Dividing Eq 2 by $N_i$ and writing $N_k$ = population_size * p[k], where p is subpop_fractions, the total population cancels.
The share of the people present at each location who live in each home region depends only on M and p, so it is computed once, before the daily loop:
share_present = M * p[None, :] / (M @ p)[:, None]
One day of the renewal is:
infectiousness = reversed_gen_int @ history
infectiousness_at_location = share_present @ infectiousness
susceptible = 1 - cumulative
new_infections = susceptible * (M.T @ (rt * infectiousness_at_location))
| Code | Roy et al. Eq 2 |
|---|---|
history |
recent $I_{\tau,k} / N_k$, one row per day of the generation interval |
infectiousness[k] |
$\sum_{\tau} I_{\tau,k}, g_{t-\tau} / N_k$ |
share_present[j, k] |
$m_{jk} N_k / \sum_l m_{jl} N_l$: share of the people present at $j$ who live in $k$ |
infectiousness_at_location[j] |
infectiousness per person present at $j$: the bracketed sum divided by $\sum_l m_{jl} N_l$ |
rt[j] |
$R_{t,j}$ |
M.T @ (...) |
the sum over $j$ weighted by $m_{ji}$ |
cumulative[i] |
$\sum_{s<t} I_{s,i} / N_i$ |
susceptible[i] |
$S_{t,i} / N_i$ |
new_infections[i] |
$I_{t,i} / N_i$ |
The order of operations follows Eq 2:
rtmultiplies at the location, beforeM.T, because $R_{t,j}$ describes transmission where it happens.M.Tassigns infections acquired at each location back to the home region of the person infected.susceptiblemultiplies afterM.T, because $S_{t,i}$ does not depend on $j$: residents of region $i$ lose susceptibility wherever they were infected.- The population present at a location counts all residents present, susceptible or not, as in the denominator of Eq 2.
cumulativecounts every latent infection since the first day of the initialization period, as in $\sum_{s<t}$.
The aggregate is the population-weighted sum, aggregate = p @ new_infections, which is total new infections divided by the total population.
When M is the identity matrix, each region follows Eq 1 on its own. With susceptible depletion turned off, the result is identical to SubpopulationInfections.
Reproduction numbers
Subpopulation $\mathcal{R}(t)$ has the same structure as in SubpopulationInfections:
log_rt_subpop = log_rt_baseline + deviations
where deviations sum to zero across subpopulations on each day. Under mobility, index j of rt_subpop is a location: rt_subpop[t, j] is $R_{t,j}$.
The recorded values (under the latent process name prefix) are those of SubpopulationInfections, plus susceptible_fraction. Under mobility they mean:
| Name | Meaning |
|---|---|
rt_baseline |
geometric mean of rt_subpop across locations |
rt_subpop |
$R_{t,j}$: reproduction number at location j when everyone is susceptible |
rt_subpop_adjusted |
rt_subpop * (share_present @ susceptible_fraction) after the initialization period, and rt_subpop during it: reproduction number at location j after depletion, which is new infections caused per unit of infectiousness present at j; equal to rt_subpop when depletion is off |
infections_aggregate |
aggregate new infections per resident of the jurisdiction |
susceptible_fraction |
susceptible_fraction[t, i] is $S_{t,i} / N_i$, the share of residents of home region i not infected before day t, on the full time axis; recorded with depletion on or off, and used by the renewal only with depletion on |
With susceptible depletion on, rt_subpop is the paper's $R_{t,j}$. With depletion off, rt_subpop absorbs depletion, as in SubpopulationInfections.
Public API
from pyrenew.deterministic import DeterministicVariable
from pyrenew.latent import MobilitySubpopulationInfections
builder.configure_latent(
MobilitySubpopulationInfections,
gen_int_rv=gen_int_rv,
I0_rv=I0_rv,
log_rt_time_0_rv=log_rt_time_0_rv,
baseline_rt_process=baseline_rt_process,
subpop_rt_deviation_process=subpop_rt_deviation_process,
mobility_matrix_rv=DeterministicVariable("mobility_matrix", mobility_matrix),
susceptible_depletion=True,
)
model = builder.build()
model.run(
...,
population_size=population_size,
subpop_fractions=subpop_fractions,
**signal_data,
)
mobility_matrix_rvis required. It is the matrix used on every day. In this version it must be aDeterministicVariablewhose value is an(n, n)numeric array. A data frame must be converted with.to_numpy(), with rows and columns in subpopulation order.susceptible_depletiondefaults toTrue, which is Eq 2 as published.Falsedrops the $S_{t,i}$ term.I0_rvreturns new infections per resident on the first day of the initialization period, the earliest day on the time axis, either one value for all regions or one value per region. It is not prevalence. New infections grow from that value over the initialization period, and every initialization day counts toward depletion.log_rt_time_0_rvreturns the starting log $\mathcal{R}(t)$, either one value for all regions or one value per region (#905). Roy et al. give each region its own starting value.model.run()takes the same arguments as forSubpopulationInfections. The matrix is part of the model definition, not run-time data.- Fitting and forecasting use the same matrix. Prediction with NumPyro's
Predictive, includingmodel.posterior_predictive(), runs the fitted model, so it uses the matrix the model was built with. No run-time argument can replace it. - To model another jurisdiction, build a model with that jurisdiction's matrix.
pyrenew.latentalso exportsBaseSubpopulationInfections(see Class structure) andcompute_infections_from_rt_with_mobility, the renewal function the class uses, next tocompute_infections_from_rt_with_feedback.
Validation
When the latent process is constructed, the matrix must be:
- two-dimensional and square;
- finite and non-negative;
- column-stochastic: each column sums to 1, within
1e-6. If the columns do not but the rows do, the error message says to pass the transpose; - such that every location has at least one positive entry in its row.
A three-dimensional array is rejected: mobility_matrix_rv must be a single (n, n) matrix. A mobility_matrix_rv that is not a DeterministicVariable is rejected.
Before fitting (MultiSignalModel.validate_data, which run() calls):
- the matrix has one row and one column per entry of
subpop_fractions; - every location has a positive population present:
M @ subpop_fractions > 0.
The size check also runs whenever the model is sampled, including prediction with Predictive. The population check does not: under Predictive and during MCMC the model is traced, and checks on values are skipped.
When the latent process is sampled:
- a vector-valued initial infection level has one value per subpopulation;
- a vector-valued starting log $\mathcal{R}(t)$ has one value per subpopulation (#905).
Infection counts
This class depends on the fix for subpopulation count scaling (#903, PR #904). With that fix:
- count observations of subpopulation
kreceiveinfections[:, k] * population_size * subpop_fractions[k]; - measurement observations, such as wastewater concentrations, receive infections per resident;
- aggregate count observations receive
aggregate * population_size, as now.
What this version does not do
- Time-varying mobility. One fixed matrix. A later version is planned to accept a list of dated matrices, each replacing the fixed matrix on its dates: for example, school holidays, or the identity during lockdowns as in Roy et al. The matrices are data and are never estimated. That version would run one fixed-matrix renewal per date range, carrying the state from one range to the next.
- Estimating the matrix. The matrix is data and is never estimated. A random matrix is rejected.
- Scenario projections. This version uses one matrix on every day, so a projection that switches to another matrix from the forecast start (for example a lockdown) is not supported. That needs a matrix that changes on a date, as planned for dated matrices. A counterfactual with another matrix on every day is possible: build a second model with that matrix and pass the fitted model's posterior samples to NumPyro's
Predictive, asPredictive(model2.model, posterior_samples=model1.mcmc.get_samples()).model2.posterior_predictive()does not work, becausemodel2has not been fitted. - Recording the matrix. The matrix is constant, so it is not recorded with every draw. It is available as
model.latent.mobility_matrix_rv.value. - Mobility that responds to the epidemic, for example people travelling less when local transmission is high. That would need its own renewal step, computing each day's matrix from recent infections, and data on mobility: with count data alone, a mobility response trades off against $\mathcal{R}(t)$.
- Infection feedback. This class has no feedback option. In
PopulationInfectionsandSubpopulationInfections(#902), infection feedback is an approximate way to account for susceptible depletion. Here Eq 2 accounts for depletion directly, throughsusceptible_depletion. - Prior immunity. All residents are susceptible at the start of the initialization period, as in Roy et al.
- Coupled initialization. Initialization-period infections grow at a rate computed from each region's own initial $\mathcal{R}(t)$, as in
SubpopulationInfections. Under coupling this can produce a short adjustment over the first generation interval after initialization. - Infection sources. The split of infections into local, incoming, and outgoing sources (Roy et al. Eqs 3 to 5) is not recorded. It can be computed after fitting from
latent_infections_by_subpop,rt_subpop, andsusceptible_fraction, with the matrix,population_size,subpop_fractions, and the generation interval. The generation interval must be fixed, because a sampled one is not recorded. With depletion off, use 1 in place ofsusceptible_fraction. - Matrix orientation conversion. A row-stochastic matrix is rejected, not transposed.
- Other kinds of contact mixing, such as age-structured contact matrices. Those are different models.
Deliberate differences from Roy et al.
- $\mathcal{R}(t)$ structure. Roy et al. give each region an independent random walk on $\log R_{t,i}$ (simulation study) or a covariate model (England case study). This class uses PyRenew's shared baseline plus sum-to-zero deviations, with any
TemporalProcess. As in Roy et al., each region can start at its own $\mathcal{R}(t)$ by givinglog_rt_time_0_rvone value per region (#905). - Initialization. Roy et al. seed a constant number of infections for six days. This class starts from
I0on the first day of the initialization period and grows infections exponentially over that period, asSubpopulationInfectionsdoes. - Overshoot. On a day with extreme $\mathcal{R}(t)$, Eq 2 as written can infect more residents than remain susceptible. This class does not cap that day's infections, so cumulative infections can exceed the number of residents. It floors the susceptible fraction at zero, so infections on later days stay non-negative. At realistic incidence the floor has no effect.
- Observation model. Delays, ascertainment, and noise are PyRenew's observation processes. They are not part of Eq 2.
Class structure
BaseSubpopulationInfections holds what both subpopulation models share: the hierarchical $\mathcal{R}(t)$, initial infections, aggregation, and recorded values. Each subclass defines its renewal step, and its constructor lists its options (#902):
SubpopulationInfections: each subpopulation renews independently, with optional infection feedback. Its constructor, behavior, and recorded values do not change.MobilitySubpopulationInfections: the coupled renewal above, with optional susceptible depletion.
Susceptible depletion is part of the coupled renewal step in Eq 2, so it is a constructor option of MobilitySubpopulationInfections. PopulationInfections and SubpopulationInfections have no depletion option. Independent regions with depletion (Eq 1 for each region) are MobilitySubpopulationInfections with the identity matrix and depletion on. This is the model Roy et al. compare against for regions not linked by mobility.
Required checks
- Eq 2 reference. Infections match a direct transcription of the time loop in the authors' Stan model (
uk_spatial/simulated_data/simulated_fitting_region.stanin mlgh-sg/Spatial_transmission). The comparison uses the Fig 2b mobility matrix, unequal regional $\mathcal{R}(t)$, depletion on, and enough days for substantial depletion, and agrees to floating-point precision. - Identity, no depletion. With the identity matrix and depletion off, every value
SubpopulationInfectionsrecords agrees to floating-point precision (allclose) for the same draws, with one starting log $\mathcal{R}(t)$ or one per region. - Identity, with depletion. With the identity matrix and depletion on, each region follows Eq 1.
- Adjusted Rt. With depletion off,
rt_subpop_adjustedequalsrt_subpop. - Conservation. Total new infections equal the sum over locations of
rt_subpop_adjustedtimes the infectiousness present there. - Aggregate.
aggregateequalsall_subpops @ p. - Running total. Every initialization day counts toward depletion, including days older than the generation interval.
- Susceptible fraction.
susceptible_fractionstarts at 1 and never increases. With depletion on,rt_subpop_adjustedequalsrt_subpop * (share_present @ susceptible_fraction)after the initialization period. - Extreme Rt. Infections stay non-negative and finite, and gradients stay finite.
- Construction checks. Matrices that are non-square, negative, non-finite, row-stochastic but not column-stochastic, three-dimensional, or have an empty row are rejected. So is a non-deterministic
mobility_matrix_rv. SubpopulationInfectionsunchanged. Its tests, as #902 and #905 leave them, pass after the base class is extracted, including those for infection feedback and per-region starting $\mathcal{R}(t)$.- Pre-fit checks. A matrix whose size does not match
subpop_fractions, or a location with no population present, is rejected byvalidate_databefore MCMC starts. - End to end. Prior predictive sampling through
PyrenewBuilderandMultiSignalModelworks. - Counterfactual with another matrix. Samples drawn from a model, passed to
Predictivefor the same model, reproduce its latent infections. Passed toPredictivefor a model built with the identity matrix, they give different latent infections.
- Lingua principale
- Python
- Stelle
- 30
- Fork
- 11
- Merge medio
- 6g 15h
- PR unite (30g)
- 9
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di CDCgov/PyRenew
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
I maintainer di solito rispondono entro 6 giorni
-
Demonstrate how missing values should be specified and how they are handledForse già presa @cdc-mitzimorris l’ha presa 3 giorni fa. Aperta
CDCgov/PyRenew#911 · 1 assegnatario ·
I maintainer di solito rispondono entro 6 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
I maintainer di solito rispondono entro 6 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 52/100
CDCgov/PyRenew#905 · 1 commento ·
I maintainer di solito rispondono entro 6 giorni
-
Make infection feedback a constructor option of the latent classes; remove `infection_process`Apertamodel pyrenew
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
I maintainer di solito rispondono entro 6 giorni
Tutte le issue di CDCgov/PyRenew
Issue simili
-
Edit tool converts CRLF files to LFForse già presa @RaphaelFakhri l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
huggingface/tau#774 ·
I maintainer di solito rispondono entro 1 giorno
-
show ignores the row limit when a column or table name contains "limit"Forse già presa @RaphaelFakhri l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
I maintainer di solito rispondono entro 2 giorni
-
documentation good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
eclipse-score/inc_someip_gateway#356 ·
I maintainer di solito rispondono entro 2 giorni