Switching sexual-risk behaviour during pregnancy

New in v1.5.6

In many sexual-health models, pregnancy is associated with reduced sexual risk-taking: agents who were sex workers tend to exit FSW status, agents with high-concurrency partnerships tend to drop concurrency, and high-risk-group members tend to transition to lower-risk patterns. The postpartum period often sees those behaviours resume.

STIsim 1.5.6 introduces sti.PregnancyRiskReduction, a plug-in intervention that layers this dynamic on top of any StructuredSexual-style network plus an ss.Pregnancy demographics module. Each parameter is a Bernoulli over the affected pregnant sub-population, so you can configure full, partial, or zero behavioural shift.

This example compares two scenarios — one with the modifier on and one with it off — and shows how it shifts incidence during the pregnancy/postpartum window.

Setup

import numpy as np
import sciris as sc
import starsim as ss
import stisim as sti
import matplotlib.pyplot as plt

sc.options(dpi=110)

Tracking pregnant FSWs

PregnancyRiskReduction doesn’t publish its own per-timestep results. A small analyzer records the number of agents who are simultaneously pregnant and FSW each step. scale=True on the Result ensures the count is multiplied by sim.pars.pop_scale when reading via sim.results — so the trajectory plot is interpretable as a real population count, not an agent count.

class PregnantFswAnalyzer(ss.Analyzer):
    """Record number of pregnant FSWs each timestep (population-scaled)."""

    def init_results(self):
        super().init_results()
        self.define_results(ss.Result('n', dtype=int, scale=True))

    def step(self):
        nw = self.sim.networks.structuredsexual
        preg = self.sim.demographics.pregnancy
        self.results['n'][self.ti] = int((preg.pregnant & nw.fsw).count())

Building the sims

Both scenarios run the same HIV transmission model and same population structure; they differ only in whether PregnancyRiskReduction is included.

sim_kwargs = dict(
    n_agents=10_000, dur=20, start=2005, verbose=-1, rand_seed=1,
    networks=[sti.StructuredSexual(), ss.MaternalNet(), ss.BreastfeedingNet()],
    demographics=[ss.Pregnancy(), ss.Deaths()],
)

def make_sim(modify_pregnancy):
    hiv = sti.HIV(init_prev=0.05, beta_m2f=0.05)
    interventions = [sti.HIVTest(name='hiv_test', test_prob_data=0.3),
                     sti.ART(coverage=0.6)]
    if modify_pregnancy:
        interventions.append(sti.PregnancyRiskReduction())
    label = 'Modifier on' if modify_pregnancy else 'Modifier off'
    return sti.Sim(diseases=[hiv], interventions=interventions,
                   analyzers=[PregnantFswAnalyzer()],
                   label=label, **sim_kwargs)

sims = ss.parallel([make_sim(False), make_sim(True)]).sims
results = {sim.label: sim for sim in sims}
Initializing sim "Modifier off" with 10000 agents
Initializing sim "Modifier on" with 10000 agents

  Running "Modifier off": 2005.01.01 ( 0/241) (0.00 s)  ———————————————————— 0%

  Running "Modifier on": 2005.01.01 ( 0/241) (0.00 s)  ———————————————————— 0%

  Running "Modifier off": 2006.01.01 (12/241) (0.75 s)  •——————————————————— 5%

  Running "Modifier on": 2006.01.01 (12/241) (0.75 s)  •——————————————————— 5%

  Running "Modifier off": 2007.01.01 (24/241) (1.26 s)  ••—————————————————— 10%

  Running "Modifier on": 2007.01.01 (24/241) (1.28 s)  ••—————————————————— 10%

  Running "Modifier off": 2008.01.01 (36/241) (1.78 s)  •••————————————————— 15%

  Running "Modifier on": 2008.01.01 (36/241) (1.80 s)  •••————————————————— 15%

  Running "Modifier off": 2009.01.01 (48/241) (2.31 s)  ••••———————————————— 20%

  Running "Modifier on": 2009.01.01 (48/241) (2.34 s)  ••••———————————————— 20%

  Running "Modifier off": 2010.01.01 (60/241) (2.82 s)  •••••——————————————— 25%

  Running "Modifier on": 2010.01.01 (60/241) (2.86 s)  •••••——————————————— 25%

  Running "Modifier off": 2011.01.01 (72/241) (3.34 s)  ••••••—————————————— 30%

  Running "Modifier on": 2011.01.01 (72/241) (3.40 s)  ••••••—————————————— 30%

  Running "Modifier off": 2012.01.01 (84/241) (3.87 s)  •••••••————————————— 35%

  Running "Modifier on": 2012.01.01 (84/241) (3.92 s)  •••••••————————————— 35%

  Running "Modifier off": 2013.01.01 (96/241) (4.41 s)  ••••••••———————————— 40%

  Running "Modifier on": 2013.01.01 (96/241) (4.47 s)  ••••••••———————————— 40%

  Running "Modifier off": 2014.01.01 (108/241) (4.93 s)  •••••••••——————————— 45%

  Running "Modifier on": 2014.01.01 (108/241) (5.00 s)  •••••••••——————————— 45%

  Running "Modifier off": 2015.01.01 (120/241) (5.45 s)  ••••••••••—————————— 50%

  Running "Modifier on": 2015.01.01 (120/241) (5.54 s)  ••••••••••—————————— 50%

  Running "Modifier off": 2016.01.01 (132/241) (5.99 s)  •••••••••••————————— 55%

  Running "Modifier on": 2016.01.01 (132/241) (6.07 s)  •••••••••••————————— 55%

  Running "Modifier off": 2017.01.01 (144/241) (6.53 s)  ••••••••••••———————— 60%

  Running "Modifier on": 2017.01.01 (144/241) (6.63 s)  ••••••••••••———————— 60%

  Running "Modifier off": 2018.01.01 (156/241) (7.06 s)  •••••••••••••——————— 65%

  Running "Modifier on": 2018.01.01 (156/241) (7.17 s)  •••••••••••••——————— 65%

  Running "Modifier off": 2019.01.01 (168/241) (7.59 s)  ••••••••••••••—————— 70%

  Running "Modifier on": 2019.01.01 (168/241) (7.70 s)  ••••••••••••••—————— 70%

  Running "Modifier off": 2020.01.01 (180/241) (8.11 s)  •••••••••••••••————— 75%

  Running "Modifier on": 2020.01.01 (180/241) (8.26 s)  •••••••••••••••————— 75%

  Running "Modifier off": 2021.01.01 (192/241) (8.67 s)  ••••••••••••••••———— 80%

  Running "Modifier on": 2021.01.01 (192/241) (8.81 s)  ••••••••••••••••———— 80%

  Running "Modifier off": 2022.01.01 (204/241) (9.24 s)  •••••••••••••••••——— 85%

  Running "Modifier on": 2022.01.01 (204/241) (9.40 s)  •••••••••••••••••——— 85%

  Running "Modifier off": 2023.01.01 (216/241) (9.82 s)  ••••••••••••••••••—— 90%

  Running "Modifier on": 2023.01.01 (216/241) (9.99 s)  ••••••••••••••••••—— 90%

  Running "Modifier off": 2024.01.01 (228/241) (10.40 s)  •••••••••••••••••••— 95%

  Running "Modifier on": 2024.01.01 (228/241) (10.59 s)  •••••••••••••••••••— 95%

  Running "Modifier off": 2025.01.01 (240/241) (11.00 s)  •••••••••••••••••••• 100%


  Running "Modifier on": 2025.01.01 (240/241) (11.20 s)  •••••••••••••••••••• 100%

Pregnant FSWs over time

The clearest visual signal: with the modifier off, pregnant FSWs accumulate as the simulation runs (any female agent in her SW window who becomes pregnant remains FSW). With the modifier on, the intervention reversibly toggles paused on those agents and the count stays at zero.

yearvec = sims[0].t.yearvec

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot(yearvec, results['Modifier off'].results.pregnantfswanalyzer.n, lw=2, label='Modifier off')
ax.plot(yearvec, results['Modifier on'].results.pregnantfswanalyzer.n,  lw=2, label='Modifier on')
ax.set_xlabel('Year')
ax.set_ylabel('Pregnant FSWs')
ax.set_title('Pregnant FSWs over time')
ax.legend()
sc.figlayout()
plt.show()

HIV incidence comparison

fig, ax = plt.subplots(figsize=(8, 4.5))
for label, sim in results.items():
    ax.plot(yearvec, sim.results.hiv.new_infections, label=label, lw=2)
ax.set_xlabel('Year')
ax.set_ylabel('New HIV infections per timestep')
ax.set_title('HIV incidence with and without the pregnancy modifier')
ax.legend()
sc.figlayout()
plt.show()

The “modifier on” curve typically sits below the “off” curve because pregnant women — a relatively-high-acquisition-risk subgroup — are shifted out of FSW / high-concurrency partnerships during pregnancy, reducing onward transmission both to them and from them.

What the intervention does

PregnancyRiskReduction runs every timestep and:

  1. Records per-agent high-water marks (was_fsw, was_high_risk, ever_pregnant, default_concurrency) so the original behaviour can be restored after pregnancy and the intervention is a no-op for agents who have never been pregnant.
  2. During pregnancy, samples per-agent Bernoullis to clear FSW status, reduce risk group to default_risk_group, and zero out concurrency.
  3. After pregnancy ends (~pregnant for an agent flagged ever_pregnant), restores the agent’s FSW status, risk group, and concurrency from the high-water mark. No dependency on a breastfeeding state.

The defaults assume 100% behavioural shift; tune each Bernoulli to match locally-observed transitions:

sti.PregnancyRiskReduction(
    fsw_redux=ss.bernoulli(p=0.7),       # 70% of pregnant FSWs exit FSW
    high_risk_redux=ss.bernoulli(p=0.5),  # 50% drop to default risk group
    concurrency_redux=ss.bernoulli(p=0.9),
)

Caveats

  • Restoration fires immediately when an agent’s pregnancy ends. The intervention has no built-in postpartum window — if you want behaviour to stay reduced for some months after birth, wrap or subclass the intervention.
  • The intervention writes directly to network state (nw.fsw, nw.risk_group, nw.concurrency). It assumes those attributes exist on the network identified by network_name (default 'structuredsexual').
  • This is a behavioural shift on top of an otherwise unchanged network. If you want pregnancy to also break specific partnerships (e.g. force a partner separation), do that in a downstream intervention.