analyzers

analyzers

Common analyzers for STI analyses

Classes

Name Description
DebutAge Analyze the proportion of agents who have sexually debuted by age.
NetworkDegree Analyze lifetime partner count distributions in a StructuredSexual network.
PartnershipFormationAnalyzer Track partnership formation per network, gender, and age bin.
RelationshipDurations Analyze relationship durations in a StructuredSexual network.
TimeBetweenRelationships Analyzes the time between relationships in a structuredsexual network.
art_coverage Track ART coverage (number and proportion) by sex and age bin.
coinfection_stats Generates stats for the coinfection of two diseases.
partner_age_diff Analyze age differences between sexual partners.
result_grouper Base analyzer providing conditional probability utilities for grouped results.
sw_stats Track new infections and transmissions among sex workers and their clients.

DebutAge

analyzers.DebutAge(bins=None, cohort_starts=None, *args, **kwargs)

Analyze the proportion of agents who have sexually debuted by age.

Tracks the share of agents who are sexually active (past their debut age) at each single-year age bin, disaggregated by sex and birth cohort. Useful for validating debut age distributions against survey data such as DHS.

Parameters

Name Type Description Default
bins array Age bins to evaluate, e.g. np.arange(12, 31). Defaults to ages 12-30. None
cohort_starts array Birth-year cohort start years. Defaults to all cohorts that fit within the simulation timespan. None

Methods

Name Description
plot Plot the proportion of active agents by cohort and debut age
plot
analyzers.DebutAge.plot()

Plot the proportion of active agents by cohort and debut age

NetworkDegree

analyzers.NetworkDegree(
    year=None,
    bins=None,
    relationship_types=None,
    *args,
    **kwargs,
)

Analyze lifetime partner count distributions in a StructuredSexual network.

At a specified year, records the number of lifetime partners per agent, disaggregated by sex and relationship type (stable, casual, one-time, sex work). Results are binned into a histogram for plotting and comparison with survey data.

Parameters

Name Type Description Default
year float Calendar year at which to record partner counts. Defaults to the last year of the simulation. None
bins array Bin edges for the partner count histogram. Defaults to [0, 1, ..., 20, 100]. None
relationship_types list Relationship types to track, e.g. ['stable', 'casual']. Use 'partners' for combined stable + casual counts. None

Methods

Name Description
init_pre Initialize the analyzer
init_results Add results for n_rships, separated for males and females
plot Plot histograms and stats by sex and relationship type
step record lifetime_partners for the user-specified year
init_pre
analyzers.NetworkDegree.init_pre(sim, **kwargs)

Initialize the analyzer

init_results
analyzers.NetworkDegree.init_results()

Add results for n_rships, separated for males and females Optionally disaggregate for risk level / age?

plot
analyzers.NetworkDegree.plot()

Plot histograms and stats by sex and relationship type

step
analyzers.NetworkDegree.step()

record lifetime_partners for the user-specified year

PartnershipFormationAnalyzer

analyzers.PartnershipFormationAnalyzer(
    age_bins=None,
    networks=None,
    *args,
    **kwargs,
)

Track partnership formation per network, gender, and age bin.

Works with any analyzer that subclasses stisim BaseNetwork (that does not have class attribute records_all_expirations set to False on purpose. This attribute (when False) explicitly tells PartnershipFormationAnalyzer the network is not configured to work with it (yet)).

Stores one row per distinct edge with its active interval, rather than re-snapshotting the active edge set every timestep. Formation is detected at the step where ti_formed == ti; the end of the interval comes from the network’s expired_this_loop (populated by _on_edge_dissolution when record_expired=True), so step() only touches the per-step flows (newly-formed and newly-expired edges), never the active stock. Each partner’s sex and age-at-formation are read from the edge/people directly, so the analyzer makes no p1=male/p2=female assumption and works for any gender combination (male-female, male-male).

Recorded state lives in self.results['relationships'][nw]: a list of one mutable row per distinct edge::

[p1, p2, ti_formed, ti_expired, age_p1, age_p2, female_p1, female_p2]

ti_expired is the timestep at which the edge was observed in expired_this_loop (so its active interval is the half-open [ti_formed, ti_expired)), or None while the edge is still active at the end of the run. Getters map None -> final_ti + 1 so an ongoing edge tests active through the last timestep.

.. warning:: Single-edge-per-pair-per-timestep assumption. Edges are keyed by (p1, p2, ti_formed) (see _indicies_of_rels_in_table_for_nw). If a network ever forms more than one edge between the same pair of agents at the same ti, those edges collide on this key and expiry data is corrupted: only the last-formed colliding edge remains reachable for ti_expired recording, so the others keep ti_expired = None (wrongly treated as active through run end) and their expiry events are dropped. Formation counts are unaffected (every edge is still appended as its own row), so only the active-interval results (get_unique_partners_active_per_agent and any duration computed from ti_expired) are affected. The bundled networks do not do this; the assumption holds as long as a pair forms at most one edge per network per timestep (the no-concurrent-duplicate-edge invariant noted in BaseNetwork.add_pairs). Forming a second edge between the same pair at a different ti is fine (distinct key).

Duration semantics. ti_expired is defined as one past the last timestep the edge was present and transmission-capable during the disease transmission step (step 9 of the 16-step integration loop). Any duration reported or computed from this analyzer is therefore::

duration = ti_expired - ti_formed
         = number of active transmission steps the relationship existed

where an “active transmission step” means the edge was present in the network with both partners alive at step 9 (i.e. structurally available to transmit) – not that a transmission event actually fired (that further depends on beta, acts, condom use, and infection status). This count is consistent regardless of when a relationship formed, when it dissolved, or why it dissolved:

  • duration expiry at R: active {F..R-1} -> duration = R - F;
  • partner death/removal at T: the dying agent’s alive flag flips at step 10 (after step 9), so the edge is still active at T -> active {F..T}, ti_expired = T+1, duration = (T+1) - F;
  • formed and ended-by-death in the same timestep T: active {T} -> duration = 1 (never a zero/empty interval);
  • still active at run end: active {F..final_ti} -> duration = (final_ti+1) - F (right-censored at the run boundary).

Note this realized duration can be shorter than the network’s drawn dur parameter (the intended duration) when a partner dies or leaves early; the analyzer records the realized count, not the drawn one.

Example (self.results layout, network ‘mfnetwork’)::

ana.results['relationships']['mfnetwork'][0]
# -> [9, 17, 0, 4, 27.23, 19.62, False, True]
#    p1=9, p2=17, ti_formed=0, ti_expired=4 (active ti 0..3),
#    ages at formation, female flags. ti_expired is None if still active
#    at run end.

Reporting is via getters (all return {network_name: result}; female selects the subject sex):

  • get_n_partnerships_formed(female, age_bins=None, window_months=None){nw: {age_bin: array}}. With window_months=None the inner array is the full per-timestep series (length n_ti); with an int it is a single trailing-window sum (length 1).
  • get_n_partnerships_formed_per_agent(female, window_months=None){nw: array} of non-unique formation counts per agent over the window.
  • get_unique_partners_active_per_agent(female, window_months=None){nw: array} of unique partners per agent on edges active during the window (half-open interval test).

Parameters

Name Type Description Default
age_bins list[str] Age bin strings parsable by ss.parse_age_range (e.g. ['0-5', '5-10', ..., '70-75']). Bins may overlap; a partner is counted in every bin containing its age. Defaults to five-year bins from '0-5' through '70-75'. None
networks list[str] | None Network names to track. None (default) tracks every stisim BaseNetwork-derived sexual network in the sim. Named missing networks raise ValueError; named networks that are not BaseNetwork-derived warn and are skipped. A network that cannot record all edge expirations (records_all_expirations is False, e.g. MSMScaleFreeNetwork) is skipped with a warning, since its active-interval results would be incomplete. The analyzer requires BaseNetwork because it reads BaseNetwork-only edge meta (ti_formed, age_p1, age_p2) and the expired_this_loop buffer, and sets record_expired = True on each tracked network. None

Methods

Name Description
finalize Resolve still-buffered relationships that expired at the end of the simulation.
get_n_partnerships_formed Total count of partnerships formed (non-unique pairings), per network and age bin.
get_n_partnerships_formed_per_agent Partnerships formed (non-unique) per agent over a trailing window, for agents alive at simulation end.
get_unique_partners_active_per_agent Unique partner counts per agent over a trailing window, for agents alive at simulation end.
finalize
analyzers.PartnershipFormationAnalyzer.finalize()

Resolve still-buffered relationships that expired at the end of the simulation.

After the loop, the only edges left in a network’s self.expired_this_loop are those removed by death at the final step 15 — i.e. after the last step() read. They were active through the disease phase of the final ti (step 9), so they get ti_expired = final_ti + 1. Relationships/edges still active at sim end were never buffered for removal recording, so they keep ti_expired = None (the right-censored marker; getter methods treat None as still-active relationships at simulation end).

get_n_partnerships_formed
analyzers.PartnershipFormationAnalyzer.get_n_partnerships_formed(
    female=False,
    age_bins=None,
    window_months=None,
)

Total count of partnerships formed (non-unique pairings), per network and age bin.

When window_months is not used (None), one timeseries of partnership formation counts will be returned per age bin. When window_months is an int, N, it will return single-element lists of partnership formation counts during the final N months of the simulation, one single-element list per age bin.

Parameters
Name Type Description Default
female bool Subject sex (True=female, False=male). False
age_bins list[str] | None Bins to report; None (default) uses the analyzer’s construction bins. A partner is counted in every bin containing its age at formation (overlapping bins each count it); a partner whose age is outside every bin is not counted. None
window_months int | None If None (default), the inner array is the full per-timestep series (length n_ti, indexed by ti). If an int, the inner array is a single trailing-window sum (length 1). None
Returns
Name Type Description
dict {network_name: {age_bin: ndarray}}. The inner array is
length n_ti when window_months is None, else length 1.

Example (per timestep, whole run)::

ana.get_n_partnerships_formed(female=False, age_bins=['15-50', '40-50'])
# -> {'mfnetwork': {'15-50': array([0, 2, 0, 0, 4]),   # by timestep
#                   '40-50': array([0, 0, 1, 0, 0])}}

Example (single trailing-window sum)::

ana.get_n_partnerships_formed(female=False, age_bins=['15-50', '40-50'],
                              window_months=12)
# -> {'mfnetwork': {'15-50': array([6]),   # sum over last 12 steps
#                   '40-50': array([1])}}
get_n_partnerships_formed_per_agent
analyzers.PartnershipFormationAnalyzer.get_n_partnerships_formed_per_agent(
    female=False,
    window_months=None,
)

Partnerships formed (non-unique) per agent over a trailing window, for agents alive at simulation end.

Equivalent to asking living agents, “How many partnerships have you formed over the last X months?”

Parameters
Name Type Description Default
female bool Subject sex. False
window_months int | None Trailing ti window length for partnership counting, None for whole simulation. None
Returns
Name Type Description
dict {network_name: array} indexed like the subject-sex
surviving-agent uids (ana._final_uids['f'\|'m']).

Example (whole run)::

ana.get_n_partnerships_formed_per_agent(female=False)
# -> {'mfnetwork': array([0, 0, 1, 0, 4, ...])}
#    aligned to ana._final_uids['m']; e.g. that male formed 4
#    partnerships over the run (non-unique: a re-formed pair counts twice).
get_unique_partners_active_per_agent
analyzers.PartnershipFormationAnalyzer.get_unique_partners_active_per_agent(
    female=False,
    window_months=None,
)

Unique partner counts per agent over a trailing window, for agents alive at simulation end.

Equivalent to asking living agents, “How many unique partners have you had over the last X months?”

An edge counts as active in the trailing window iff it had not expired before the window opened, i.e. ti_expired > win_start (its half-open active interval [ti_formed, ti_expired) overlaps the window). The window always ends at the final timestep, so no upper bound on ti_formed is needed; an ongoing edge carries ti_expired = final_ti + 1 and therefore always counts. For window_months=None the window is the whole run (win_start = 0). Sex-general: the partner is the other endpoint regardless of its sex, so this works for same-sex networks too. Dedups by partner uid (a network is assumed not to hold concurrent duplicate edges between the same pair, so unique partners == unique active partnerships).

Parameters
Name Type Description Default
female bool Subject sex. False
window_months int | None Trailing ti window length for partner counting, None for whole simulation. None
Returns
Name Type Description
dict {network_name: array} indexed like the subject-sex
surviving-agent uids.

Example (trailing 12 timesteps)::

ana.get_unique_partners_active_per_agent(female=False, window_months=12)
# -> {'mfnetwork': array([1, 0, 2, ...])}
#    aligned to ana._final_uids['m']; e.g. the male at index 2 had 2
#    distinct partners across edges active during the last 12 steps.

RelationshipDurations

analyzers.RelationshipDurations(*args, **kwargs)

Analyze relationship durations in a StructuredSexual network.

Records the mean and median duration of all relationships (stable, casual, etc.) at each timestep, disaggregated by sex. Durations are extracted from the network’s relationship_durs tracking dict.

Methods

Name Description
get_relationship_durations Returns the durations of all relationships, separated by sex.
get_relationship_durations
analyzers.RelationshipDurations.get_relationship_durations()

Returns the durations of all relationships, separated by sex.

If include_current is False, return the duration of only relationships that have ended

Returns
Name Type Description
female_durations list of durations of relationships
male_durations list of durations of relationships

TimeBetweenRelationships

analyzers.TimeBetweenRelationships(relationship_type='stable', *args, **kwargs)

Analyzes the time between relationships in a structuredsexual network. Each timestep, for each debuted agent, check if they are in a relationship of the provided type. If not, increment the counter Otherwise, reset the counter to 0 and append the counter to the list of times between relationships for that agent.

Methods

Name Description
step For each debuted agent, check if they are in a relationship.
step
analyzers.TimeBetweenRelationships.step()

For each debuted agent, check if they are in a relationship. If they are not, increment the time since last relationship by 1. If they are and time since last relationship is greater than 0, append the time to the list of times between relationships.

art_coverage

analyzers.art_coverage(age_bins=None, *args, **kwargs)

Track ART coverage (number and proportion) by sex and age bin.

Results are stored as time series per stratum, accessible via: analyzer.results[‘n_art_f_15_25’] # Women 15-25 on ART (count) analyzer.results[‘p_art_m_25_35’] # Men 25-35 on ART (proportion of infected)

Parameters

Name Type Description Default
age_bins list age bin edges, e.g. [15, 25, 35, 45, 65]. Default: [15, 25, 35, 45, 65]. Bins are half-open intervals: [lo, hi), i.e. lo <= age < hi. None

Methods

Name Description
plot Plot ART coverage over time.
plot
analyzers.art_coverage.plot(by_age=True)

Plot ART coverage over time.

Creates a 2-panel figure: aggregate coverage (left) and by age/sex (right). If by_age=False, only plots aggregate.

Example::

sim.run()
sim.analyzers.art_coverage.plot()

coinfection_stats

analyzers.coinfection_stats(
    disease1,
    disease2,
    disease1_infected_state_name='infected',
    disease2_infected_state_name='infected',
    age_limits=None,
    denom=None,
    *args,
    **kwargs,
)

Generates stats for the coinfection of two diseases. This is useful for looking at the coinfection of HIV and syphilis, for example.

Parameters

Name Type Description Default
disease1 str | ss.Disease name of the first disease required
disease2 str | ss.Disease name of the second disease required
disease1_infected_state_name str name of the infected state for disease1 (default: ‘infected’) 'infected'
disease2_infected_state_name str name of the infected state for disease2 (default: ‘infected’) 'infected'
age_limits list list of two integers that define the age limits for the denominator. None
denom function function that returns a boolean array of the denominator, usually the relevant population. default: lambda self: (self.sim.people.age >= 15) & (self.sim.people.age < 50) None
*args, **kwargs optional, passed to ss.Analyzer constructor required

partner_age_diff

analyzers.partner_age_diff(
    year=2000,
    age_bins=['teens', 'young', 'adult'],
    network='structuredsexual',
    *args,
    **kwargs,
)

Analyze age differences between sexual partners.

Records the mean, median, and standard deviation of male-female age differences (male age minus female age) at each timestep. At a specified year, stores full age-difference distributions disaggregated by female age group for detailed plotting.

Parameters

Name Type Description Default
year float Calendar year at which to store detailed age-difference distributions. Defaults to 2000. 2000
age_bins list Female age group names matching the network’s f_age_group_bins keys. Defaults to ['teens', 'young', 'adult']. ['teens', 'young', 'adult']
network str Name of the network to analyze. Defaults to 'structuredsexual'. 'structuredsexual'

Methods

Name Description
init_results Initialize the results for the age differences.
plot Plot histograms of the age differences between partners.
step Record the age differences between partners in the specified year.
init_results
analyzers.partner_age_diff.init_results()

Initialize the results for the age differences.

plot
analyzers.partner_age_diff.plot()

Plot histograms of the age differences between partners.

step
analyzers.partner_age_diff.step()

Record the age differences between partners in the specified year.

result_grouper

analyzers.result_grouper()

Base analyzer providing conditional probability utilities for grouped results.

Provides a cond_prob static method that computes the proportion of a numerator group within a denominator group. Intended as a base class for analyzers that compute stratified prevalence or infection statistics.

sw_stats

analyzers.sw_stats(diseases=None, *args, **kwargs)

Track new infections and transmissions among sex workers and their clients.

At each timestep, records the number and share of new infections and transmissions attributable to female sex workers (FSW), clients, and non-sex-worker populations, disaggregated by disease.

Parameters

Name Type Description Default
diseases list List of disease names (str) to track, e.g. ['hiv', 'syphilis']. None