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

Stopping PrEP usage #216

Open
wants to merge 14 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/hivpy/column_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@
FIRST_CAB_START_DATE = "first_cab_start_date" # None | date: start date of first ever injectable Cab PrEP usage
FIRST_LEN_START_DATE = "first_len_start_date" # None | date: start date of first ever injectable Len PrEP usage
FIRST_VR_START_DATE = "first_vr_start_date" # None | date: start date of first ever vaginal ring PrEP usage
LAST_PREP_START_DATE = "last_prep_start_date" # None | date: start date of most recent PrEP usage
LAST_PREP_STOP_DATE = "last_prep_stop_date" # None | date: stop date of most recent PrEP usage
LAST_PREP_START_DATE = "last_prep_start_date" # None | date: start date of most recent PrEP usage
PREP_JUST_STARTED = "prep_just_started" # Bool: True if PrEP usage began this time step
LAST_PREP_USE_DATE = "last_prep_use_date" # None | date: most recent date of PrEP usage
LAST_PREP_STOP_DATE = "last_prep_stop_date" # None | date: stop date of most recent PrEP usage
PREP_PAUSED = "prep_paused" # Bool: True if PrEP usage is paused this time step due to ineligibility
CONT_ON_PREP = "cont_on_prep" # None | timedelta: total length of continuous usage of current PrEP based on user intention (breaks due to ineligibility do not count against continuity)
CONT_ACTIVE_ON_PREP = "cont_active_on_prep" # None | timedelta: actual total length of continuous usage of current PrEP
CUMULATIVE_PREP_ORAL = "cumulative_prep_oral" # None | timedelta: total length of cumulative oral PrEP usage
Expand Down
11 changes: 6 additions & 5 deletions src/hivpy/data/prep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ prob_suspect_risk_prep: 0.5
# prep preference beta
prep_oral_pref_beta:
Value: [1.1, 1.3, 1.5]
prep_willing_threshold: 0.2
# effect of low vl on prep willingness
vl_prevalence_prep_threshold:
Value: [0.005, 0.01]

# starting prep
# prep-related testing
rate_test_onprep_any: 1
prep_willing_threshold: 0.2
prob_test_prep_start:
Value: [0.25, 0.50, 0.75]

# starting prep
prob_base_prep_start:
Value: [0.10, 0.30]
prob_prep_restart:
Value: [0.05, 0.10, 0.20]
prob_base_prep_stop:
Value: [0.05, 0.15, 0.30]
prob_base_prep_stop_nonuniform:
Value: [0.05, 0.15, 0.30]
Probability: [0.8, 0.1, 0.1]
prob_prep_restart:
Value: [0.05, 0.10, 0.20]
70 changes: 61 additions & 9 deletions src/hivpy/prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ def __init__(self, **kwargs):
self.prep_cab_pref_beta = self.prep_oral_pref_beta + 0.3
self.prep_len_pref_beta = self.prep_cab_pref_beta
self.prep_vr_pref_beta = self.prep_oral_pref_beta - 0.1
self.prep_willing_threshold = self.p_data.prep_willing_threshold

self.vl_prevalence_affects_prep = rng.choice([True, False], p=[1/3, 2/3])
self.vl_prevalence_prep_threshold = self.p_data.vl_prevalence_prep_threshold.sample()

self.rate_test_onprep_any = self.p_data.rate_test_onprep_any
self.prep_willing_threshold = self.p_data.prep_willing_threshold
self.prob_test_prep_start = self.p_data.prob_test_prep_start.sample()

# probability of starting prep in people who are eligible, willing,
# and tested for HIV according to base rate of testing
self.prob_base_prep_start = self.p_data.prob_base_prep_start.sample()
Expand All @@ -60,12 +61,12 @@ def __init__(self, **kwargs):
self.prob_cab_prep_start = self.prob_base_prep_start
self.prob_len_prep_start = self.prob_base_prep_start
self.prob_vr_prep_start = self.prob_base_prep_start
self.prob_prep_restart = self.p_data.prob_prep_restart.sample()
# FIXME: stop probabilities dependent on time step length
self.prob_oral_prep_stop = self.p_data.prob_base_prep_stop.sample()
self.prob_cab_prep_stop = self.p_data.prob_base_prep_stop.sample()
self.prob_len_prep_stop = self.prob_cab_prep_stop
self.prob_vr_prep_stop = self.p_data.prob_base_prep_stop_nonuniform.sample()
self.prob_prep_restart = self.p_data.prob_prep_restart.sample()

def init_prep_variables(self, pop: Population):
pop.init_variable(col.PREP_ORAL_PREF, 0)
Expand Down Expand Up @@ -95,8 +96,10 @@ def init_prep_variables(self, pop: Population):
pop.init_variable(col.FIRST_LEN_START_DATE, None)
pop.init_variable(col.FIRST_VR_START_DATE, None)
pop.init_variable(col.LAST_PREP_START_DATE, None)
pop.init_variable(col.LAST_PREP_STOP_DATE, None)
pop.init_variable(col.PREP_JUST_STARTED, False)
pop.init_variable(col.LAST_PREP_USE_DATE, None)
pop.init_variable(col.LAST_PREP_STOP_DATE, None)
pop.init_variable(col.PREP_PAUSED, False)
pop.init_variable(col.CONT_ON_PREP, timedelta(months=0))
pop.init_variable(col.CONT_ACTIVE_ON_PREP, timedelta(months=0))
pop.init_variable(col.CUMULATIVE_PREP_ORAL, timedelta(months=0))
Expand Down Expand Up @@ -191,6 +194,20 @@ def get_presumed_hiv_neg_pop(self, pop: Population):

return pop.apply_bool_mask(mask, false_neg_pop)

def get_prep_cont_choice_pop(self, pop: Population):
"""
Return the sub-population that have reached a point in their PrEP usage
where they can choose whether to stop or switch. For people on oral
or vaginal ring PrEP this happens every time step, but injectable PrEP
takes some time to wear off (~3 months for cab and ~6 months for len).
"""
return pop.get_sub_pop(OR(COND(col.PREP_TYPE, op.eq, PrEPType.Oral),
COND(col.PREP_TYPE, op.eq, PrEPType.VaginalRing),
AND(COND(col.PREP_TYPE, op.eq, PrEPType.Cabotegravir),
COND(col.LAST_PREP_USE_DATE, op.le, pop.date - timedelta(months=3))),
AND(COND(col.PREP_TYPE, op.eq, PrEPType.Lenacapavir),
COND(col.LAST_PREP_USE_DATE, op.le, pop.date - timedelta(months=6)))))

def prep_preference(self, pop: Population):
"""
Determine PrEP preferences for all PrEP types.
Expand Down Expand Up @@ -529,6 +546,7 @@ def tested_start_prep(self, pop: Population, prep_eligible_pop, prep_type,
pop.set_present_variable(col.CONT_ACTIVE_ON_PREP, time_step, starting_prep_pop)
# increment cumulative use
self.set_all_prep_cumulative(pop, starting_prep_pop, time_step)
pop.set_present_variable(col.LAST_PREP_USE_DATE, pop.date, starting_prep_pop)

def general_start_prep(self, pop: Population, prep_eligible_pop, time_step):
"""
Expand All @@ -555,6 +573,7 @@ def general_start_prep(self, pop: Population, prep_eligible_pop, time_step):
pop.set_present_variable(col.CONT_ACTIVE_ON_PREP, time_step, starting_prep_pop)
# increment cumulative use
self.set_all_prep_cumulative(pop, starting_prep_pop, time_step)
pop.set_present_variable(col.LAST_PREP_USE_DATE, pop.date, starting_prep_pop)

def set_all_prep_start_dates(self, pop: Population, starting_prep_pop):
"""
Expand Down Expand Up @@ -658,11 +677,15 @@ def continue_prep(self, pop: Population, time_step):
OR(COND(col.LAST_TEST_DATE, op.ne, pop.date),
AND(COND(col.LAST_TEST_DATE, op.eq, pop.date),
COND(col.HIV_DIAGNOSED, op.eq, False)))))
# people who can choose to stop or switch this time step
prep_choice_pop = pop.get_sub_pop_intersection(eligible, self.get_prep_cont_choice_pop(pop))

if len(eligible) > 0:
# continuous prep outcomes
prep_types = pop.transform_group([col.PREP_TYPE, col.FAVOURED_PREP_TYPE],
self.calc_current_prep, sub_pop=eligible, dropna=True)
self.calc_current_prep, sub_pop=prep_choice_pop, dropna=True)
# get all current prep types and replace with new outcomes where necessary
prep_types = pop.get_variable(col.PREP_TYPE, eligible).combine(prep_types, lambda _, y: y)
mmcleod89 marked this conversation as resolved.
Show resolved Hide resolved
# find various sub-populations
# people who are continuing current prep
continuing_prep_mask = pop.get_variable(col.PREP_TYPE, eligible) == prep_types
Expand Down Expand Up @@ -694,6 +717,7 @@ def continue_prep(self, pop: Population, time_step):
if len(using_prep_pop) > 0:
# increment cumulative use
self.set_all_prep_cumulative(pop, using_prep_pop, time_step)
pop.set_present_variable(col.LAST_PREP_USE_DATE, pop.date, using_prep_pop)

if len(stopping_prep_pop) > 0:
# stop continuous use
Expand Down Expand Up @@ -734,8 +758,8 @@ def restart_prep(self, pop: Population, time_step):

if len(eligible) > 0:
# starting prep outcomes
prep_types = pop.transform_group([col.FAVOURED_PREP_TYPE], self.calc_restarting_prep,
sub_pop=eligible, dropna=True)
prep_types = pop.transform_group([col.FAVOURED_PREP_TYPE, col.PREP_PAUSED],
self.calc_restarting_prep, sub_pop=eligible, dropna=True)
# people who are restarting prep
restarting_prep_pop = pop.apply_bool_mask(prep_types.notnull(), eligible)

Expand All @@ -750,21 +774,47 @@ def restart_prep(self, pop: Population, time_step):
pop.set_present_variable(col.CONT_ACTIVE_ON_PREP, time_step, restarting_prep_pop)
# increment cumulative use
self.set_all_prep_cumulative(pop, restarting_prep_pop, time_step)
pop.set_present_variable(col.LAST_PREP_USE_DATE, pop.date, restarting_prep_pop)
# unset stop date
pop.set_present_variable(col.LAST_PREP_STOP_DATE, None, restarting_prep_pop)
# unpause prep
pop.set_present_variable(col.PREP_PAUSED, False, restarting_prep_pop)

def calc_restarting_prep(self, favoured_prep, size):
def calc_restarting_prep(self, favoured_prep, prep_paused, size):
"""
Returns PrEP types for people restarting PrEP.
Individual preferences and availability are taken into account.
"""
# outcomes
r = rng.uniform(size=size)
restarting = r < self.prob_prep_restart
# those who stopped due to ineligibility are guaranteed to restart
if prep_paused:
restarting = r < 1
else:
restarting = r < self.prob_prep_restart
prep = [favoured_prep if r else None for r in restarting]

return prep

def stop_prep(self, pop: Population):
"""
Update PrEP usage for people stopping PrEP due to lack of eligibility or positive test.
"""
# people who are using prep and are now ineligible
ineligible = pop.get_sub_pop(AND(OR(COND(col.HIV_DIAGNOSED, op.eq, True),
COND(col.PREP_ELIGIBLE, op.eq, False)),
COND(col.EVER_PREP, op.eq, True),
COND(col.LAST_PREP_STOP_DATE, op.eq, None),
COND(col.LAST_TEST_DATE, op.eq, pop.date)))

if len(ineligible) > 0:
# reset active continuous use
pop.set_present_variable(col.CONT_ACTIVE_ON_PREP, timedelta(months=0), ineligible)
# set stop date
pop.set_present_variable(col.LAST_PREP_STOP_DATE, pop.date, ineligible)
# pause prep
pop.set_present_variable(col.PREP_PAUSED, True, ineligible)

def prep_usage(self, pop: Population, time_step):
"""
Update PrEP usage for people starting, continuing, switching, restarting, and stopping PrEP.
Expand All @@ -775,3 +825,5 @@ def prep_usage(self, pop: Population, time_step):
self.continue_prep(pop, time_step)
# restarting prep
self.restart_prep(pop, time_step)
# stopping prep
self.stop_prep(pop)
6 changes: 3 additions & 3 deletions src/hivpy/prep_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ def __init__(self, filename):
self.prob_suspect_risk_prep = self.data["prob_suspect_risk_prep"]

self.prep_oral_pref_beta = self._get_discrete_dist("prep_oral_pref_beta")
self.prep_willing_threshold = self.data["prep_willing_threshold"]
self.vl_prevalence_prep_threshold = self._get_discrete_dist("vl_prevalence_prep_threshold")

self.rate_test_onprep_any = self.data["rate_test_onprep_any"]
self.prep_willing_threshold = self.data["prep_willing_threshold"]
self.prob_test_prep_start = self._get_discrete_dist("prob_test_prep_start")

self.prob_base_prep_start = self._get_discrete_dist("prob_base_prep_start")
self.prob_prep_restart = self._get_discrete_dist("prob_prep_restart")
self.prob_base_prep_stop = self._get_discrete_dist("prob_base_prep_stop")
self.prob_base_prep_stop_nonuniform = self._get_discrete_dist("prob_base_prep_stop_nonuniform")
self.prob_prep_restart = self._get_discrete_dist("prob_prep_restart")

except KeyError as ke:
print(ke.args)
Expand Down
35 changes: 34 additions & 1 deletion src/tests/test_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ def test_restarting_prep():
pop.data[col.PREP_ELIGIBLE] = True
pop.data[col.EVER_PREP] = True
pop.data[col.LAST_PREP_STOP_DATE] = pop.date - time_step
pop.data[col.PREP_PAUSED] = False
pop.data[col.PREP_JUST_STARTED] = False
pop.data[col.CONT_ON_PREP] = timedelta(months=0)
pop.data[col.CONT_ACTIVE_ON_PREP] = timedelta(months=0)
Expand All @@ -858,7 +859,7 @@ def test_restarting_prep():
pop.prep.prob_prep_restart = 0.5

pop.prep.restart_prep(pop, time_step)
# expecting 90% of people to restart prep
# expecting 50% of people to restart prep
no_on_prep = sum(pop.data[col.LAST_PREP_STOP_DATE].isnull())
mean = N * pop.prep.prob_prep_restart
stdev = sqrt(mean * (1 - pop.prep.prob_prep_restart))
Expand All @@ -868,3 +869,35 @@ def test_restarting_prep():
(pop.data[col.LAST_PREP_STOP_DATE].isnull()))
assert all((pop.data[col.CONT_ACTIVE_ON_PREP] == time_step) ==
(pop.data[col.LAST_PREP_STOP_DATE].isnull()))

pop.data[col.LAST_PREP_STOP_DATE] = pop.date - time_step
pop.data[col.PREP_PAUSED] = True
pop.prep.restart_prep(pop, time_step)
# expecting everyone to restart prep
assert sum(pop.data[col.LAST_PREP_STOP_DATE].isnull()) == N


def test_stopping_prep():
N = 100
pop = Population(size=N, start_date=date(5000, 1, 1))
pop.prep.date_prep_intro = [date(2000), date(3000), date(4000), date(5000)]
pop.data[col.HIV_DIAGNOSED] = False
pop.data[col.PREP_ELIGIBLE] = False
pop.data[col.EVER_PREP] = True
pop.data[col.LAST_PREP_STOP_DATE] = None
pop.data[col.PREP_PAUSED] = False
pop.data[col.LAST_TEST_DATE] = pop.date

pop.prep.stop_prep(pop)
# expecting everyone to stop prep due to ineligibility
assert sum(pop.data[col.LAST_PREP_STOP_DATE] == pop.date) == N
assert sum(pop.data[col.PREP_PAUSED]) == N

pop.data[col.HIV_DIAGNOSED] = True
pop.data[col.PREP_ELIGIBLE] = True
pop.data[col.LAST_PREP_STOP_DATE] = None
pop.data[col.PREP_PAUSED] = False
pop.prep.stop_prep(pop)
# expecting everyone to stop prep due to positive diagnosis
assert sum(pop.data[col.LAST_PREP_STOP_DATE] == pop.date) == N
assert sum(pop.data[col.PREP_PAUSED]) == N
Loading