Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unstable Vital Dynamics #695

Closed
5 tasks
daniel-klein opened this issue Oct 11, 2024 · 10 comments
Closed
5 tasks

Unstable Vital Dynamics #695

daniel-klein opened this issue Oct 11, 2024 · 10 comments
Milestone

Comments

@daniel-klein
Copy link
Contributor

Consider a simulation with the following demographics

dems = [
    ss.Births(pars=dict(birth_rate=20)),
    ss.Deaths(pars=dict(death_rate=20))
]

One might expect a stable population. However, the result is a dramatically declining population.
image

This result is for two reasons:

  1. Births takes the floor when deterministically adding agents.
    n_new = int(np.floor(sim.people.alive.count() * scaled_birth_prob))

  2. Module order matters. Because Births comes before Deaths in the list of dems, agents are added to the population (resulting in a larger population), and then Deaths applies the death rate to all agents, including the new agents. A 1% increase followed by a 1% decrease results in a net decline.

Switching the order of demographic module updates (and changing the Births floor to something like sc.randround) resolves the issue. Why? Because Deaths simply schedules agents for death via ti_death without actually removing them from the population. The Births module then adds agents considering the same denominator as used for deaths.
image

We cannot expect users to know they have to put Deaths before Births to get a stable population.

Next Steps:

  • Change Births away from floor. It could apply a probability per-agent
  • Consider a combined BirthDeath module that would act on the same denominator.
  • Consider only adding births (including from Pregnancy) at the end of the time step, although that will likely create other problems.
  • Consider excluding agents born on the current time step from the death rate (although doing so could have other unintended consequences as we know postnatal mortality is typically quite high.)
  • Consider adjusting the death rate by the birth rate, if Deaths follows Births.

Thanks to Deven for raising this issue in the context of SEIRD in TBsim.

@cliffckerr
Copy link
Contributor

We had discussed this issue previously here:
#303

I agree with the logic that newborns have a nonzero mortality risk, so this makes sense. It's like how the population replacement rate is closer to 2.1 than 2.0 births per woman -- nonintuitive, but correct.

I'm also not sure I'd call the drop "dramatic": take a look at this:

import starsim as ss
import matplotlib.pyplot as plt

dems = [
    ss.Births(pars=dict(birth_rate=20)),
    ss.Deaths(pars=dict(death_rate=20))
]

sim = ss.Sim(demographics=dems, dur=30)
sim.run()

res = sim.results
plt.plot(res.timevec, res.n_alive)
plt.ylim(bottom=0, top=10_000)
plt.show()

image

@vgdeven
Copy link

vgdeven commented Oct 11, 2024

Just curious, why not have a separation of labor between - calculation/drawing of transitions (eg. n_births, n_deaths etc.) in one step and then application of transitions (eg., ΔState = n_births-n_deaths) in other? That way all transitions will based on the same denominator?

@cliffckerr
Copy link
Contributor

Good question! Two main reasons:

  1. Since different modules can have different timesteps, there isn't a single point at which the deltas can be collected, and another point at which they can be applied. (Births and deaths could even use different timesteps, although that would probably not be a wise choice :))
  2. The model logic is intended so that under "standard" conditions, things can take effect immediately. For example, on a single timestep, a person can be tested, test positive, be scheduled for treatment, receive treatment, and then be less likely to infect someone else (or die etc). If we collected all the deltas and then modified the states after, then each step would introduce a 1-step delay (e.g. if they are tested on day t, they couldn't start treatment until day t+1, and then this wouldn't impact their probability of death/infectiousness until day t+2).

@daniel-klein
Copy link
Contributor Author

Agree the drop is not dramatic, but will still come as a surprise to users.

@daniel-klein
Copy link
Contributor Author

We should Births away from the np.floor. It could look more like a simple version of Pregnancy and could even link newborns and mothers - just without bothering with the pre/post-natal periods nor vertical network edges. I suppose one could configure Pregnancy in this way, but we could just add a shim layer to help the user if not fully refactoring as BasePregnancy and Pregnancy.

@daniel-klein
Copy link
Contributor Author

We could also change example scripts to place Deaths before Births and describe why this change matters in a tutorial on demographics.

@cliffckerr cliffckerr added this to the Version 2.2 milestone Oct 30, 2024
@cliffckerr cliffckerr modified the milestones: Version 2.2, Version 2.1.1 Nov 7, 2024
@cliffckerr
Copy link
Contributor

I had a look at this and I think something else is going on. Partly, we wouldn't expect them to match anyway, since 1.1*0.9 != 1.0. The order shouldn't matter, but does:

s1 = ss.Sim(label='Births→Deaths', demographics=[ss.Births(birth_rate=20), ss.Deaths(death_rate=20)])
s2 = ss.Sim(label='Deaths→Births', demographics=[ss.Deaths(death_rate=20), ss.Births(birth_rate=20)])
msim = ss.parallel(s1, s2)
msim.plot()

image

@daniel-klein
Copy link
Contributor Author

The floor in Births magnifies the difference resulting from the order. Surprising to see the difference in variance though. In my exploration, I replaced the floor with a round and that did help the drift. But I didn't look at variance.

@cliffckerr
Copy link
Contributor

This is with floor removed, which is why I think it's something else happening.

@cliffckerr
Copy link
Contributor

Closed by #749

cliffckerr added a commit that referenced this issue Nov 18, 2024
Addressing stochasticity in Births related to dicussion in Unstable Vital Dynamics #695
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants