stisim 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stisim/__init__.py +25 -0
- stisim/analyzers.py +518 -0
- stisim/calibration.py +271 -0
- stisim/connectors/__init__.py +2 -0
- stisim/connectors/gud_syph.py +34 -0
- stisim/connectors/hiv_sti.py +160 -0
- stisim/data/__init__.py +2 -0
- stisim/data/downloaders.py +275 -0
- stisim/data/loaders.py +71 -0
- stisim/data/test_downloaders.py +6 -0
- stisim/demographics.py +215 -0
- stisim/diseases/__init__.py +10 -0
- stisim/diseases/bv.py +738 -0
- stisim/diseases/chlamydia.py +143 -0
- stisim/diseases/gonorrhea.py +71 -0
- stisim/diseases/gud.py +137 -0
- stisim/diseases/hiv.py +579 -0
- stisim/diseases/sti.py +615 -0
- stisim/diseases/syphilis.py +567 -0
- stisim/diseases/trichomoniasis.py +69 -0
- stisim/interventions/__init__.py +7 -0
- stisim/interventions/base_interventions.py +667 -0
- stisim/interventions/bv_interventions.py +205 -0
- stisim/interventions/gonorrhea_interventions.py +103 -0
- stisim/interventions/hiv_interventions.py +298 -0
- stisim/interventions/syphilis_interventions.py +314 -0
- stisim/networks.py +773 -0
- stisim/parameters.py +116 -0
- stisim/sim.py +366 -0
- stisim/utils.py +392 -0
- stisim/version.py +9 -0
- stisim-1.4.0.dist-info/METADATA +48 -0
- stisim-1.4.0.dist-info/RECORD +36 -0
- stisim-1.4.0.dist-info/WHEEL +5 -0
- stisim-1.4.0.dist-info/licenses/LICENSE +21 -0
- stisim-1.4.0.dist-info/top_level.txt +1 -0
stisim/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .version import __version__, __versiondate__, __license__
|
|
2
|
+
|
|
3
|
+
from .calibration import *
|
|
4
|
+
from .connectors import *
|
|
5
|
+
from .diseases import *
|
|
6
|
+
from .demographics import *
|
|
7
|
+
from .interventions import *
|
|
8
|
+
from .networks import *
|
|
9
|
+
from .utils import *
|
|
10
|
+
from .analyzers import *
|
|
11
|
+
from .parameters import *
|
|
12
|
+
from .sim import *
|
|
13
|
+
|
|
14
|
+
# Assign the root folder
|
|
15
|
+
import sciris as sc
|
|
16
|
+
root = sc.thispath(__file__).parent
|
|
17
|
+
data = root/'data'
|
|
18
|
+
|
|
19
|
+
# Import the version and print the license
|
|
20
|
+
print(__license__)
|
|
21
|
+
|
|
22
|
+
# Double-check key requirements -- should match setup.py
|
|
23
|
+
sc.require(['starsim>=3.0.0', 'sciris>=3.1.6', 'pandas>=2.0.0', 'scipy', 'numba', 'networkx'], message=f'The following dependencies for STIsim {__version__} were not met: <MISSING>.')
|
|
24
|
+
del sc # Don't keep this in the module
|
|
25
|
+
|
stisim/analyzers.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common analyzers for STI analyses
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# %% Imports and settings
|
|
6
|
+
import numpy as np
|
|
7
|
+
import sciris as sc
|
|
8
|
+
import starsim as ss
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
import stisim as sti
|
|
12
|
+
import pylab as pl
|
|
13
|
+
|
|
14
|
+
__all__ = ["result_grouper", "coinfection_stats", "sw_stats", "RelationshipDurations", "NetworkDegree", "DebutAge", "partner_age_diff"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class result_grouper(ss.Analyzer):
|
|
18
|
+
@staticmethod
|
|
19
|
+
def cond_prob(numerator, denominator):
|
|
20
|
+
numer = len((denominator & numerator).uids)
|
|
21
|
+
denom = len(denominator.uids)
|
|
22
|
+
out = sc.safedivide(numer, denom)
|
|
23
|
+
return out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class coinfection_stats(result_grouper):
|
|
27
|
+
"""
|
|
28
|
+
Generates stats for the coinfection of two diseases.
|
|
29
|
+
This is useful for looking at the coinfection of HIV and syphilis, for example.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
disease1 (str | ss.Disease): name of the first disease
|
|
33
|
+
disease2 (str | ss.Disease): name of the second disease
|
|
34
|
+
disease1_infected_state_name (str): name of the infected state for disease1 (default: 'infected')
|
|
35
|
+
disease2_infected_state_name (str): name of the infected state for disease2 (default: 'infected')
|
|
36
|
+
age_limits (list): list of two integers that define the age limits for the denominator.
|
|
37
|
+
denom (function): function that returns a boolean array of the denominator, usually the relevant population.
|
|
38
|
+
default: lambda self: (self.sim.people.age >= 15) & (self.sim.people.age < 50)
|
|
39
|
+
*args, **kwargs : optional, passed to ss.Analyzer constructor
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, disease1, disease2, disease1_infected_state_name='infected', disease2_infected_state_name='infected',
|
|
42
|
+
age_limits=None, denom=None, *args, **kwargs):
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
self.name = 'coinfection_stats'
|
|
45
|
+
if disease1 is None or disease2 is None:
|
|
46
|
+
raise ValueError('Coinfection stats requires exactly 2 diseases')
|
|
47
|
+
|
|
48
|
+
self.disease1 = disease1
|
|
49
|
+
self.disease2 = disease2
|
|
50
|
+
|
|
51
|
+
# if the diseases are objects, get their names and store them instead of the objects
|
|
52
|
+
if isinstance(self.disease1, ss.Disease):
|
|
53
|
+
self.disease1 = self.disease1.name
|
|
54
|
+
if isinstance(self.disease2, ss.Disease):
|
|
55
|
+
self.disease2 = self.disease2.name
|
|
56
|
+
|
|
57
|
+
self.disease1_infected_state_name = disease1_infected_state_name
|
|
58
|
+
self.disease2_infected_state_name = disease2_infected_state_name
|
|
59
|
+
self.age_limits = age_limits or [15, 50]
|
|
60
|
+
default_denom = lambda self: (self.sim.people.age >= self.age_limits[0]) & (self.sim.people.age < self.age_limits[0])
|
|
61
|
+
self.denom = denom or default_denom
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
def init_results(self):
|
|
66
|
+
super().init_results()
|
|
67
|
+
results = [
|
|
68
|
+
ss.Result(f'{self.disease1}_prev_no_{self.disease2}', dtype=float, scale=False),
|
|
69
|
+
ss.Result(f'{self.disease1}_prev_has_{self.disease2}', dtype=float, scale=False),
|
|
70
|
+
ss.Result(f'{self.disease1}_prev_no_{self.disease2}_f', dtype=float, scale=False),
|
|
71
|
+
ss.Result(f'{self.disease1}_prev_has_{self.disease2}_f', dtype=float, scale=False),
|
|
72
|
+
ss.Result(f'{self.disease1}_prev_no_{self.disease2}_m', dtype=float, scale=False),
|
|
73
|
+
ss.Result(f'{self.disease1}_prev_has_{self.disease2}_m', dtype=float, scale=False),
|
|
74
|
+
]
|
|
75
|
+
self.define_results(*results)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
def step(self):
|
|
79
|
+
sim = self.sim
|
|
80
|
+
ti = self.ti
|
|
81
|
+
disease1name = self.disease1
|
|
82
|
+
disease2name = self.disease2
|
|
83
|
+
disease1obj = getattr(self.sim.diseases, self.disease1)
|
|
84
|
+
disease2obj = getattr(self.sim.diseases, self.disease2)
|
|
85
|
+
|
|
86
|
+
ppl = sim.people
|
|
87
|
+
|
|
88
|
+
denom = self.denom(self)
|
|
89
|
+
has_disease2 = getattr(disease2obj, self.disease2_infected_state_name) # Adults with HIV
|
|
90
|
+
has_disease1 = getattr(disease1obj, self.disease1_infected_state_name) # Adults with syphilis
|
|
91
|
+
|
|
92
|
+
has_disease1_f = denom & has_disease1 & ppl.female # Women with dis1
|
|
93
|
+
has_disease2_m = denom & has_disease1 & ppl.male # Men with dis1
|
|
94
|
+
has_disease2_f = denom & has_disease2 & ppl.female # Women with dis2
|
|
95
|
+
has_disease2_m = denom & has_disease2 & ppl.male # Men with dis2
|
|
96
|
+
no_disease2 = denom & ~has_disease2 # Adults without dis2
|
|
97
|
+
no_disease2_f = no_disease2 & ppl.female # Women without dis2
|
|
98
|
+
no_disease2_m = no_disease2 & ppl.male # Men without dis2
|
|
99
|
+
|
|
100
|
+
self.results[f'{disease1name}_prev_no_{disease2name}'][ti] = self.cond_prob(has_disease1, no_disease2)
|
|
101
|
+
self.results[f'{disease1name}_prev_has_{disease2name}'][ti] = self.cond_prob(has_disease1, has_disease2)
|
|
102
|
+
self.results[f'{disease1name}_prev_no_{disease2name}_f'][ti] = self.cond_prob(has_disease1_f, no_disease2_f)
|
|
103
|
+
self.results[f'{disease1name}_prev_has_{disease2name}_f'][ti] = self.cond_prob(has_disease1_f, has_disease2_f)
|
|
104
|
+
self.results[f'{disease1name}_prev_no_{disease2name}_m'][ti] = self.cond_prob(has_disease2_m, no_disease2_m)
|
|
105
|
+
self.results[f'{disease1name}_prev_has_{disease2name}_m'][ti] = self.cond_prob(has_disease2_m, has_disease2_m)
|
|
106
|
+
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class sw_stats(result_grouper):
|
|
111
|
+
def __init__(self, diseases=None, *args, **kwargs):
|
|
112
|
+
super().__init__(*args, **kwargs)
|
|
113
|
+
self.name = 'sw_stats'
|
|
114
|
+
self.diseases = diseases
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
def init_results(self):
|
|
118
|
+
results = sc.autolist()
|
|
119
|
+
for d in self.diseases:
|
|
120
|
+
results += [
|
|
121
|
+
ss.Result('share_new_infections_fsw_'+d, scale=False, summarize_by='mean'),
|
|
122
|
+
ss.Result('share_new_infections_client_'+d,scale=False, summarize_by='mean'),
|
|
123
|
+
ss.Result('new_infections_fsw_'+d, dtype=int),
|
|
124
|
+
ss.Result('new_infections_client_'+d, dtype=int),
|
|
125
|
+
ss.Result('new_infections_non_fsw_'+d, dtype=int),
|
|
126
|
+
ss.Result('new_infections_non_client_'+d, dtype=int),
|
|
127
|
+
ss.Result('new_transmissions_fsw_'+d, dtype=int),
|
|
128
|
+
ss.Result('new_transmissions_client_'+d, dtype=int),
|
|
129
|
+
ss.Result('new_transmissions_non_fsw_'+d, dtype=int),
|
|
130
|
+
ss.Result('new_transmissions_non_client_'+d, dtype=int),
|
|
131
|
+
]
|
|
132
|
+
self.define_results(*results)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
def step(self):
|
|
136
|
+
sim = self.sim
|
|
137
|
+
ti = self.ti
|
|
138
|
+
|
|
139
|
+
if ti > 0:
|
|
140
|
+
|
|
141
|
+
for d in self.diseases:
|
|
142
|
+
dis = sim.diseases[d]
|
|
143
|
+
nw = sim.networks.structuredsexual
|
|
144
|
+
adult = sim.people.age > 0
|
|
145
|
+
fsw = nw.fsw & adult
|
|
146
|
+
client = nw.client & adult
|
|
147
|
+
non_fsw = sim.people.female & ~nw.fsw & adult
|
|
148
|
+
non_client = sim.people.male & ~nw.client & adult
|
|
149
|
+
newly_infected = (dis.ti_exposed == ti) & adult
|
|
150
|
+
new_trans = dis.ti_transmitted_sex == ti
|
|
151
|
+
total_acq = len(newly_infected.uids)
|
|
152
|
+
|
|
153
|
+
newly_transmitting_fsw = (dis.ti_transmitted_sex == ti) & fsw
|
|
154
|
+
newly_transmitting_clients = (dis.ti_transmitted_sex == ti) & client
|
|
155
|
+
newly_transmitting_non_fsw = (dis.ti_transmitted_sex == ti) & non_fsw
|
|
156
|
+
newly_transmitting_non_client = (dis.ti_transmitted_sex == ti) & non_client
|
|
157
|
+
|
|
158
|
+
new_transmissions_fsw = dis.new_transmissions_sex[newly_transmitting_fsw]
|
|
159
|
+
new_transmissions_client = dis.new_transmissions_sex[newly_transmitting_clients]
|
|
160
|
+
new_transmissions_non_fsw = dis.new_transmissions_sex[newly_transmitting_non_fsw]
|
|
161
|
+
new_transmissions_non_client = dis.new_transmissions_sex[newly_transmitting_non_client]
|
|
162
|
+
|
|
163
|
+
self.results['share_new_infections_fsw_'+d][ti] = self.cond_prob(fsw, newly_infected)
|
|
164
|
+
self.results['share_new_infections_client_'+d][ti] = self.cond_prob(client, newly_infected)
|
|
165
|
+
|
|
166
|
+
self.results['new_infections_fsw_'+d][ti] = len((fsw & newly_infected).uids)
|
|
167
|
+
self.results['new_infections_client_'+d][ti] = len((client & newly_infected).uids)
|
|
168
|
+
self.results['new_infections_non_fsw_'+d][ti] = len((non_fsw & newly_infected).uids)
|
|
169
|
+
self.results['new_infections_non_client_'+d][ti] = len((non_client & newly_infected).uids)
|
|
170
|
+
|
|
171
|
+
self.results['new_transmissions_fsw_'+d][ti] = sum(new_transmissions_fsw)
|
|
172
|
+
self.results['new_transmissions_client_'+d][ti] = sum(new_transmissions_client)
|
|
173
|
+
self.results['new_transmissions_non_fsw_'+d][ti] = sum(new_transmissions_non_fsw)
|
|
174
|
+
self.results['new_transmissions_non_client_'+d][ti] = sum(new_transmissions_non_client)
|
|
175
|
+
|
|
176
|
+
total_trans = sum(new_transmissions_fsw) + sum(new_transmissions_client) + sum(new_transmissions_non_fsw) + sum(new_transmissions_non_client)
|
|
177
|
+
if total_trans != len(newly_infected.uids):
|
|
178
|
+
errormsg = f'Infections acquired should equal number transmitted: {total_acq} vs {total_trans}'
|
|
179
|
+
raise ValueError(errormsg)
|
|
180
|
+
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class RelationshipDurations(ss.Analyzer):
|
|
185
|
+
"""
|
|
186
|
+
Analyzes the durations of relationships in a structuredsexual network.
|
|
187
|
+
"""
|
|
188
|
+
def __init__(self, *args, **kwargs):
|
|
189
|
+
super().__init__(*args, **kwargs)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
def init_results(self):
|
|
193
|
+
self.define_results(
|
|
194
|
+
ss.Result('mean_duration', dtype=float, scale=False),
|
|
195
|
+
ss.Result('median_duration', dtype=float, scale=False),
|
|
196
|
+
)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
def step(self):
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
def update_results(self):
|
|
203
|
+
sim = self.sim
|
|
204
|
+
ti = self.ti
|
|
205
|
+
nw = sim.networks.structuredsexual
|
|
206
|
+
rel_durations = self.get_relationship_durations()
|
|
207
|
+
self.results['mean_duration'][ti] = np.mean(rel_durations)
|
|
208
|
+
self.results['median_duration'][ti] = np.median(rel_durations)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
def plot(self):
|
|
212
|
+
sim = self.sim
|
|
213
|
+
ti = self.ti
|
|
214
|
+
female_relationship_durs, male_relationship_durs = self.get_relationship_durations()
|
|
215
|
+
all_durations = female_relationship_durs + male_relationship_durs
|
|
216
|
+
pl.figure(1)
|
|
217
|
+
pl.hist(all_durations, bins=(max(all_durations) - min(all_durations)))
|
|
218
|
+
pl.xlabel('Relationship Duration')
|
|
219
|
+
pl.ylabel('Frequency')
|
|
220
|
+
pl.title('Distribution of Relationship Durations')
|
|
221
|
+
|
|
222
|
+
pl.figure(2)
|
|
223
|
+
pl.hist(female_relationship_durs, bins=(max(all_durations) - min(all_durations)))
|
|
224
|
+
pl.xlabel('Female Relationship Duration')
|
|
225
|
+
pl.ylabel('Frequency')
|
|
226
|
+
pl.title('Distribution of Female Relationship Durations')
|
|
227
|
+
|
|
228
|
+
pl.figure(3)
|
|
229
|
+
pl.hist(male_relationship_durs, bins=(max(all_durations) - min(all_durations)))
|
|
230
|
+
pl.xlabel('Male Relationship Duration')
|
|
231
|
+
pl.ylabel('Frequency')
|
|
232
|
+
pl.title('Distribution of Male Relationship Durations')
|
|
233
|
+
pl.show()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_relationship_durations(self):
|
|
237
|
+
"""
|
|
238
|
+
Returns the durations of all relationships, separated by sex.
|
|
239
|
+
|
|
240
|
+
If include_current is False, return the duration of only relationships that have ended
|
|
241
|
+
|
|
242
|
+
returns:
|
|
243
|
+
female_durations: list of durations of relationships
|
|
244
|
+
male_durations: list of durations of relationships
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
# Get the current duration of all relationships
|
|
248
|
+
male_durations = []
|
|
249
|
+
female_durations = []
|
|
250
|
+
for pair, relationships in self.sim.networks.structuredsexual.relationship_durs.items():
|
|
251
|
+
durs = [relationship['dur'] for relationship in relationships]
|
|
252
|
+
|
|
253
|
+
# assign the durations to male and female lists
|
|
254
|
+
for uid in pair:
|
|
255
|
+
if self.sim.people.female[uid]:
|
|
256
|
+
female_durations.extend(durs)
|
|
257
|
+
else:
|
|
258
|
+
male_durations.extend(durs)
|
|
259
|
+
|
|
260
|
+
return female_durations, male_durations
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class NetworkDegree(ss.Analyzer):
|
|
264
|
+
def __init__(self, year=None, bins=None, relationship_types=None, *args, **kwargs):
|
|
265
|
+
super().__init__(*args, **kwargs)
|
|
266
|
+
self.year = year
|
|
267
|
+
|
|
268
|
+
if bins is None:
|
|
269
|
+
bins = np.concatenate([np.arange(21),[100]])
|
|
270
|
+
self.bins = bins
|
|
271
|
+
|
|
272
|
+
if relationship_types is None:
|
|
273
|
+
relationship_types = ['stable', 'casual'] # Other options are 'partners' (stable+casual), 'onetime', 'sw'
|
|
274
|
+
|
|
275
|
+
self.relationship_types = []
|
|
276
|
+
if 'partners' in relationship_types:
|
|
277
|
+
relationship_types.remove('partners')
|
|
278
|
+
self.relationship_types.append('lifetime_partners')
|
|
279
|
+
[self.relationship_types.append(f'lifetime_{relationship_type}_partners') for relationship_type in relationship_types]
|
|
280
|
+
|
|
281
|
+
for relationship_type in self.relationship_types:
|
|
282
|
+
setattr(self, f'{relationship_type}_f', [])
|
|
283
|
+
setattr(self, f'{relationship_type}_m', [])
|
|
284
|
+
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
def init_results(self):
|
|
288
|
+
"""
|
|
289
|
+
Add results for `n_rships`, separated for males and females
|
|
290
|
+
Optionally disaggregate for risk level / age?
|
|
291
|
+
"""
|
|
292
|
+
super().init_results()
|
|
293
|
+
for relationship_type in self.relationship_types:
|
|
294
|
+
self.results += [
|
|
295
|
+
ss.Result(f'{relationship_type}_f', dtype=int, scale=False, shape=len(self.bins)),
|
|
296
|
+
ss.Result(f'{relationship_type}_m', dtype=int, scale=False, shape=len(self.bins)),
|
|
297
|
+
]
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
def init_pre(self, sim, **kwargs):
|
|
301
|
+
"""
|
|
302
|
+
Initialize the analyzer
|
|
303
|
+
"""
|
|
304
|
+
super().init_pre(sim, **kwargs)
|
|
305
|
+
self.year = sim.t.yearvec[-1]
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def step(self):
|
|
310
|
+
"""
|
|
311
|
+
record lifetime_partners for the user-specified year
|
|
312
|
+
"""
|
|
313
|
+
if self.sim.t.yearvec[self.ti] == self.year:
|
|
314
|
+
for relationship_type in self.relationship_types:
|
|
315
|
+
# Get the number of partners, disaggregated by sex. We can't use a Result object for this because we
|
|
316
|
+
# don't know how many agents there will be at any given time step. We can use Results for the binned
|
|
317
|
+
# counts.
|
|
318
|
+
|
|
319
|
+
female_partners = getattr(self.sim.networks.structuredsexual, relationship_type)[self.sim.people.female]
|
|
320
|
+
male_partners = getattr(self.sim.networks.structuredsexual, relationship_type)[self.sim.people.male]
|
|
321
|
+
|
|
322
|
+
getattr(self, f'{relationship_type}_f').extend(female_partners)
|
|
323
|
+
getattr(self, f'{relationship_type}_m').extend(male_partners)
|
|
324
|
+
|
|
325
|
+
# bin the data by number of partners
|
|
326
|
+
female_counts, female_bins = np.histogram(female_partners, bins=self.bins)
|
|
327
|
+
male_counts, male_bins = np.histogram(male_partners, bins=self.bins)
|
|
328
|
+
|
|
329
|
+
for i, female_count, male_count in zip(range(len(self.bins)), female_counts, male_counts):
|
|
330
|
+
self.results[f'{relationship_type}_f'][i] = female_count
|
|
331
|
+
self.results[f'{relationship_type}_m'][i] = male_count
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
def plot(self):
|
|
335
|
+
"""
|
|
336
|
+
Plot histograms and stats by sex and relationship type
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
for relationship_type in self.relationship_types:
|
|
340
|
+
fig, axes = pl.subplots(1, 2, figsize=(9, 5), layout="tight")
|
|
341
|
+
axes = axes.flatten()
|
|
342
|
+
for ai, sex in enumerate(['f', 'm']):
|
|
343
|
+
counts = self.results[f'{relationship_type}_{sex}'].values
|
|
344
|
+
bins=self.bins
|
|
345
|
+
|
|
346
|
+
total = sum(counts)
|
|
347
|
+
counts = counts / total
|
|
348
|
+
counts[-2] = counts[-2:].sum()
|
|
349
|
+
counts = counts[:-1]
|
|
350
|
+
|
|
351
|
+
axes[ai].bar(bins[:-1], counts)
|
|
352
|
+
axes[ai].set_xlabel(f'Number of {relationship_type}')
|
|
353
|
+
axes[ai].set_title(f'Distribution of partners, {sex}')
|
|
354
|
+
axes[ai].set_ylim([0, 1])
|
|
355
|
+
|
|
356
|
+
sex_counts = np.array(getattr(self, f'{relationship_type}_{sex}'))
|
|
357
|
+
stats = f"Mean: {np.mean(sex_counts):.1f}\n"
|
|
358
|
+
stats += f"Median: {np.median(sex_counts):.1f}\n"
|
|
359
|
+
stats += f"Std: {np.std(sex_counts):.1f}\n"
|
|
360
|
+
stats += f"%>20: {np.count_nonzero(sex_counts >= 20) / total * 100:.2f}\n"
|
|
361
|
+
axes[ai].text(15, 0.5, stats)
|
|
362
|
+
|
|
363
|
+
pl.show()
|
|
364
|
+
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class partner_age_diff(ss.Analyzer):
|
|
369
|
+
def __init__(self, year=2000, age_bins=['teens', 'young', 'adult'], network='structuredsexual', *args, **kwargs):
|
|
370
|
+
super().__init__(*args, **kwargs)
|
|
371
|
+
self.year = year
|
|
372
|
+
self.network = network
|
|
373
|
+
self.age_diffs = {}
|
|
374
|
+
self.age_bins = age_bins
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
def init_results(self):
|
|
378
|
+
"""
|
|
379
|
+
Initialize the results for the age differences.
|
|
380
|
+
"""
|
|
381
|
+
self.define_results(
|
|
382
|
+
ss.Result('age_diff_mean', dtype=float, scale=False),
|
|
383
|
+
ss.Result('age_diff_median', dtype=float, scale=False),
|
|
384
|
+
ss.Result('age_diff_std', dtype=float, scale=False),
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
def step(self):
|
|
389
|
+
"""
|
|
390
|
+
Record the age differences between partners in the specified year.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
net = self.sim.networks[self.network]
|
|
394
|
+
relationships = net.edges.dur > 1
|
|
395
|
+
p1 = net.p1[relationships]
|
|
396
|
+
p2 = net.p2[relationships]
|
|
397
|
+
|
|
398
|
+
age_diffs = (self.sim.people.age[p1] - self.sim.people.age[p2])
|
|
399
|
+
|
|
400
|
+
f_ages = self.sim.people.age[p2]
|
|
401
|
+
|
|
402
|
+
# bin the female ages by the bins used in the structured sexual network
|
|
403
|
+
# age_bins = sorted([bin[0] for bin in self.sim.networks.structuredsexual.pars.f_age_group_bins.values()])
|
|
404
|
+
age_bin_limits = [net.pars.f_age_group_bins[bin][0] for bin in self.age_bins]
|
|
405
|
+
age_bin_indices = np.digitize(f_ages, age_bin_limits) - 1
|
|
406
|
+
|
|
407
|
+
self.results['age_diff_mean'][self.ti] = np.mean(age_diffs)
|
|
408
|
+
self.results['age_diff_median'][self.ti] = np.median(age_diffs)
|
|
409
|
+
self.results['age_diff_std'][self.ti] = np.std(age_diffs)
|
|
410
|
+
|
|
411
|
+
if self.sim.t.yearvec[self.ti] == self.year:
|
|
412
|
+
for bin in self.age_bins:
|
|
413
|
+
self.age_diffs[bin] = age_diffs[age_bin_indices == self.age_bins.index(bin)]
|
|
414
|
+
|
|
415
|
+
def plot(self):
|
|
416
|
+
"""
|
|
417
|
+
Plot histograms of the age differences between partners.
|
|
418
|
+
"""
|
|
419
|
+
if len(self.age_diffs) > 0:
|
|
420
|
+
pl.figure(figsize=(8, 5))
|
|
421
|
+
pl.hist(list(self.age_diffs.values()), label=list(self.age_diffs.keys()), bins=30, edgecolor='black', alpha=0.7)
|
|
422
|
+
pl.legend()
|
|
423
|
+
pl.xlabel('Age Difference (years)')
|
|
424
|
+
pl.ylabel('Frequency')
|
|
425
|
+
pl.title(f'Age Differences Between Partners in {self.year} (Male Age - Female Age)')
|
|
426
|
+
pl.grid(True)
|
|
427
|
+
pl.show()
|
|
428
|
+
else:
|
|
429
|
+
print("No age differences recorded for the specified year.")
|
|
430
|
+
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class DebutAge(ss.Analyzer):
|
|
435
|
+
"""
|
|
436
|
+
Analyzes the debut age of relationships in a structuredsexual network.
|
|
437
|
+
"""
|
|
438
|
+
def __init__(self, bins=None, cohort_starts=None, *args, **kwargs):
|
|
439
|
+
super().__init__(*args, **kwargs)
|
|
440
|
+
|
|
441
|
+
self.bins = bins or np.arange(12, 31, 1)
|
|
442
|
+
self.binspan = self.bins[-1] - self.bins[0]
|
|
443
|
+
self.cohort_starts = cohort_starts
|
|
444
|
+
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
def init_pre(self, sim, force=False):
|
|
448
|
+
if self.cohort_starts is None:
|
|
449
|
+
first_cohort = sim.t.start.years
|
|
450
|
+
last_cohort = sim.t.stop.years - self.binspan
|
|
451
|
+
self.cohort_starts = sc.inclusiverange(first_cohort, last_cohort)
|
|
452
|
+
self.cohort_ends = self.cohort_starts + self.binspan
|
|
453
|
+
self.n_cohorts = len(self.cohort_starts)
|
|
454
|
+
self.cohort_years = np.array([sc.inclusiverange(i, i + self.binspan) for i in self.cohort_starts])
|
|
455
|
+
|
|
456
|
+
self.prop_active_f = np.zeros((self.n_cohorts, self.binspan + 1))
|
|
457
|
+
self.prop_active_m = np.zeros((self.n_cohorts, self.binspan + 1))
|
|
458
|
+
super().init_pre(sim, force=force)
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
def init_results(self):
|
|
462
|
+
super().init_results()
|
|
463
|
+
self.define_results(
|
|
464
|
+
ss.Result('prop_active_f', dtype=float, scale=False, shape=len(self.cohort_starts)),
|
|
465
|
+
ss.Result('prop_active_m', dtype=float, scale=False, shape=len(self.cohort_starts)),
|
|
466
|
+
)
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
def step(self):
|
|
470
|
+
|
|
471
|
+
sim = self.sim
|
|
472
|
+
ppl = sim.people
|
|
473
|
+
if sim.t.yearvec[sim.ti] in self.cohort_years:
|
|
474
|
+
cohort_inds, bin_inds = sc.findinds(self.cohort_years, sim.t.yearvec[sim.ti])
|
|
475
|
+
for ci, cohort_ind in enumerate(cohort_inds):
|
|
476
|
+
bin_ind = bin_inds[ci]
|
|
477
|
+
bin = self.bins[bin_ind]
|
|
478
|
+
|
|
479
|
+
# all females cohort:
|
|
480
|
+
conditions_f = ppl.female * ppl.alive * (ppl.age >= (bin - 1)) * (
|
|
481
|
+
ppl.age < bin)
|
|
482
|
+
cohort_f_count = sum(conditions_f)
|
|
483
|
+
# all active females in cohort:
|
|
484
|
+
num_conditions_f = conditions_f * sim.networks.structuredsexual.over_debut
|
|
485
|
+
debut_f_count = sum(num_conditions_f)
|
|
486
|
+
|
|
487
|
+
self.prop_active_f[cohort_ind, bin_ind] = (debut_f_count) / (cohort_f_count) if cohort_f_count > 0 else 0
|
|
488
|
+
|
|
489
|
+
# all males cohort:
|
|
490
|
+
conditions_m = ~sim.people.female * sim.people.alive * (sim.people.age >= (bin - 1)) * (
|
|
491
|
+
sim.people.age < bin)
|
|
492
|
+
cohort_m_count = sum(conditions_m)
|
|
493
|
+
# all active males in cohort:
|
|
494
|
+
num_conditions_m = conditions_m * sim.networks.structuredsexual.over_debut
|
|
495
|
+
debut_m_count = sum(num_conditions_m)
|
|
496
|
+
self.prop_active_m[cohort_ind, bin_ind] = (debut_m_count) / (cohort_m_count) if cohort_m_count > 0 else 0
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
def plot(self):
|
|
500
|
+
"""
|
|
501
|
+
Plot the proportion of active agents by cohort and debut age
|
|
502
|
+
"""
|
|
503
|
+
pl.figure(1)
|
|
504
|
+
for row in self.prop_active_f:
|
|
505
|
+
pl.plot(self.bins, row)
|
|
506
|
+
pl.xlabel('Age')
|
|
507
|
+
pl.ylabel('Share')
|
|
508
|
+
pl.title('Proportion of females who are sexually active')
|
|
509
|
+
pl.show()
|
|
510
|
+
|
|
511
|
+
pl.figure(2)
|
|
512
|
+
for row in self.prop_active_m:
|
|
513
|
+
pl.plot(self.bins, row)
|
|
514
|
+
pl.xlabel('Age')
|
|
515
|
+
pl.ylabel('Share')
|
|
516
|
+
pl.title('Proportion of males who are sexually active')
|
|
517
|
+
pl.show()
|
|
518
|
+
|