Skip to main content

Maternal-child health

🧠40% 🤖60%

Transmission from mother to child, and the outcomes of a pregnancy (stillbirth, preterm birth, low birth weight, neonatal death), are harder to model than a standard disease: they depend on the timing of pregnancy, on which module acts first in a step, and on how deaths around birth are counted. The ssl.mnch submodule of the Starsim library (import starsim.library as ssl; see Library for what the library is) has worked examples of these patterns. All of them build on pregnancy (ss.Pregnancy, which gives each pregnancy as states and transitions, with conception, delivery, and the unborn child as an agent) and on ss.PrenatalNet, the network between a pregnant woman and her unborn child; both are described on Demographics.

Class Based on Description
ssl.mnch.CongenitalDisease ssl.AgentSIR Congenital outcomes (stillbirth, congenital infection, or none) for children infected before birth
ssl.mnch.NeonatalSepsis ssl.AgentSIR Infects newborns at birth, and kills some within days
ssl.mnch.FetalHealth ss.Module Tracks fetal growth and birth weight (low, very low, and small for gestational age)
ssl.mnch.fetal_infection ss.Connector Links a disease to fetal health: infection in pregnancy brings delivery forward and restricts growth
ssl.mnch.treat_pregnant ss.Delivery Treats infected pregnant women, partly reversing the damage to the fetus

Like the rest of the library, these are illustrative: the probabilities and effect sizes are placeholders, chosen to make the effects easy to see. The examples below use a step of a week, since pregnancy outcomes are timed in weeks, and a high fertility rate, so that a small population gives enough births:

import starsim as ss
import starsim.library as ssl

mnch_pars = dict(start='2000', stop='2005', n_agents=2000, dt=ss.weeks(1))
demographics = lambda: [ss.Pregnancy(fertility_rate=ss.peryear(0.1), burnin=True), ss.Deaths()]
networks = lambda: [ss.PrenatalNet(), ss.RandomNet()]

Congenital outcomes

CongenitalDisease is an SIR that is passed from an infected mother to her unborn child through ss.PrenatalNet. For each child infected before birth, it draws an outcome from birth_outcomes (by default stillbirth, congenital infection, or a normal birth, with probabilities 0.3, 0.4, and 0.3), and schedules it for around the time of delivery. An outcome that kills (a stillbirth or miscarriage) is scheduled one step before delivery, so that it happens before birth; otherwise ss.Pregnancy, which runs earlier in each step, would already have delivered the baby, and the loss would be counted as a live birth followed by a neonatal death. Compared with the same population without transmission (beta=0), the extra stillbirths are those the disease causes (and are no longer counted as births):

def congenital(beta):
    disease = ssl.mnch.CongenitalDisease(beta=beta, init_prev=0.2)
    return ss.Sim(diseases=disease, demographics=demographics(), networks=networks(), **mnch_pars)

for label, beta in {'No transmission': 0, 'Transmission': ss.peryear(20)}.items():
    sim = congenital(beta).run()
    preg = sim.results.pregnancy
    print(label)
    print('  Births:', int(preg.birth.sum()))
    print('  Stillbirths:', int(preg.stillbirth.sum()))
    print('  Congenital infections:', sim.diseases.congenitaldisease.congenital.sum())
Sim: congenitaldisease, as 2,033 agents, 2000-01-01 to 2005-01-01
  Running as agents: n_agents was given
  Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
  Done in 1.6 s
No transmission
  Births: 231
  Stillbirths: 1
  Congenital infections: 0
Sim: congenitaldisease, as 2,033 agents, 2000-01-01 to 2005-01-01
  Running as agents: n_agents was given
  Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
  Done in 1.4 s
Transmission
  Births: 157
  Stillbirths: 75
  Congenital infections: 84

To use this in a disease of your own, build on ssl.mnch.CongenitalDisease, or copy its methods into any ss.AgentDisease:

  • set_congenital(), called instead of set_prognoses() for each child infected before birth (the mother is people.parent), draws and schedules the outcome;
  • step_congenital(), called from step_state(), applies the outcomes that are due;
  • the parameters birth_outcome_keys and birth_outcomes, and a ti_<key> state for each outcome (plus a true/false state for each that doesn’t kill), say what the outcomes are. Outcomes named 'stillborn' or 'miscarriage' are deaths before delivery, and 'neonatal_death' after it.

ssl.HIV (see Library: diseases) instead infects a fetus over ss.PrenatalNet as it infects anyone else.

Neonatal deaths

NeonatalSepsis infects a fraction of newborns at birth (p_at_birth, by default 30%), and kills half of them (p_death) within about a week. It shows that neonatal deaths need no special handling: ss.Pregnancy counts any death of a baby aged 0 to 28 days, from any cause, as a neonatal death. The results are therefore in the pregnancy module, not the disease, and comparing with a sim without sepsis gives the deaths it causes:

scenarios = {
    'No sepsis': ss.Sim(demographics=demographics(), networks=networks(), **mnch_pars),
    'Sepsis':    ss.Sim(diseases=ssl.mnch.NeonatalSepsis(), demographics=demographics(), networks=networks(), **mnch_pars),
}
for label, sim in scenarios.items():
    sim.run()
    print(label)
    print('  Births:', int(sim.results.pregnancy.birth.sum()))
    print('  Neonatal deaths:', int(sim.results.pregnancy.neonatal_death.sum()))
Sim: pregnancy, deaths, prenatalnet, randomnet, as 2,033 agents, 2000-01-01 to 2005-01-01
  Running as agents: n_agents was given
  Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
  Done in 1.1 s
No sepsis
  Births: 231
  Neonatal deaths: 0
Sim: neonatalsepsis, as 2,033 agents, 2000-01-01 to 2005-01-01
  Running as agents: n_agents was given
  Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
  Done in 1.1 s
Sepsis
  Births: 231
  Neonatal deaths: 39

Fetal health

FetalHealth doesn’t depend on any particular disease: it tracks a baseline fetal weight percentile for each pregnancy, and has two levers that other modules can pull: bringing delivery forward (the risk of a preterm birth) and adding growth restriction (the risk of a low birth weight). At delivery, it works out the birth weight from the gestational age, the percentile, and the growth restriction, and classifies newborns as of low birth weight (under 2,500 g), very low birth weight (under 1,500 g), small for gestational age, or small vulnerable newborns (any of these, or preterm). Its results are the numbers of each on each step, and the means and rates over each step’s births (e.g. mean_birth_weight, lbw_rate), which are NaN on a step with no births and are left out of a mean over time (res.mean_birth_weight.resample('year')).

The fetal_infection connector pulls those levers when a pregnant woman is infected: each infection brings delivery forward by about 3 weeks (timing_shift) and restricts growth by 15% (growth_penalty). The treat_pregnant delivery treats infected pregnant women (90% of them, coverage), as ss.treat() would, and reverses 70% of the damage (growth_reversal, timing_reversal). Both are written as code for an SIR written as code, so the disease here is ssl.AgentSIR, named 'sir'. FetalHealth is given in modules, since it is not a disease, network, demographic module, or intervention. Here are the baseline, infection, and infection with treatment from 2001:

def fetal(beta, treat=False):
    treatment = ssl.mnch.treat_pregnant(disease='sir', start=2001) if treat else None
    return ss.Sim(
        diseases      = ssl.AgentSIR(beta=beta, name='sir'),
        demographics  = demographics(),
        connectors    = ssl.mnch.fetal_infection(),
        interventions = treatment,
        modules       = ssl.mnch.FetalHealth(),
        networks      = networks(),
        **mnch_pars,
    )

scenarios = {
    'No infection': fetal(beta=0),
    'Infection, untreated': fetal(beta=ss.peryear(20)),
    'Infection, treated from 2001': fetal(beta=ss.peryear(20), treat=True),
}
msim = ss.parallel(scenarios, workers=1)
for label, sim in zip(scenarios, msim.sims):
    res = sim.results.fetal_health
    births = int(res.birth.sum())
    print(label)
    print('  Births:', births)
    print('  Low birth weight (%):', round(100*res.lbw.sum()/births))
    print('  Small vulnerable newborns (%):', round(100*res.svn.sum()/births))
MultiSim: 3 runs of 3 scenarios (No infection, Infection, untreated, Infection, treated from 2001)
  Done in 4.5 s
No infection
  Births: 231
  Low birth weight (%): 4
  Small vulnerable newborns (%): 15
Infection, untreated
  Births: 233
  Low birth weight (%): 44
  Small vulnerable newborns (%): 69
Infection, treated from 2001
  Births: 232
  Low birth weight (%): 11
  Small vulnerable newborns (%): 33

To adapt this to another disease, build on fetal_infection and replace _apply_damage() with the disease’s own effects: for example, a larger growth penalty for severe infections than for mild ones.

Further reading

  • WHO: Born too soon: the decade of action on preterm birth, and the definitions of preterm and low birth weight used here
  • LiST: the Lives Saved Tool: a widely used model of the effect of maternal and child health interventions on mortality