"""Incarceration simulator.
Agent-based model for incarceration dynamics. The simulator models incarceration
as an infectious property that can be passed on along social ties. Infection
probabilities are based on survey data. The simulator is based on Lum, Swarup,
Eubank, and Hawdon (2014). URL: https://doi.org/10.1098/rsif.2014.0409
The simulation happens on top of a fixed set of agents provided by the authors
of the study. The number of agents cannot be varied.
"""
import os
import dataclasses
import random
import gzip
import pickle
import numpy as np
from tqdm.auto import tqdm
[docs]@dataclasses.dataclass
class Config:
# pylint: disable-msg=too-few-public-methods
"""Metaparameters for a single run of simulator.
Attributes
----------
random_seed : int
Controls all randomness of the simulation
min_age : int
Minimum age for incarceration
start_iter : int
Start iteration of simulation
end_iter : int
End iteration of simulation
percent : float
Initial incarceration rate
random_sentence_type : bool
Assign lenient or harsh sentence randomly
random_sentence_bias : float
Probability of picking harsh sentence
mean_sentence_harsh : float
Mean sentence length (months) for harsh assignment
mean_sentence_lenient : float
Mean sentence length (months) for lenient assignment
"""
random_seed: int = 1
min_age: int = 15
start_iter: int = 100
end_iter: int = 200
percent: float = 0.01
harsh_sentence: bool = False
random_sentence_type: bool = False
random_sentence_bias: float = 0.5
consistent_sentence_length: bool = True
mean_sentence_harsh: float = 17.0
mean_sentence_lenient: float = 14.0
def infect(person_sex, relation_type, relation_sex):
"""Generate infection based on relation characteristics.
Parameters
----------
person_sex : string
Sex of person ('m' or 'f')
relation_type : string
Type of social relation ('parent', 'sibling', 'partner', 'child')
relation_sex : string
Sex of related person ('m' or 'f')
Returns
-------
infected : int
1 if infected, 0 otherwise
"""
infection_probability_month = {
"f": {
"parent": {"f": 0.000849988733768181, "m": 0.0112878570024662,},
"sibling": {"f": 0.00801193753900653, "m": 0.0332053842229949,},
"partner": {"*": 0.0043472740358963},
"child": {"*": 0.0169602401420906},
},
"m": {
"parent": {"f": 0.00347339838166261, "m": 0.0113344842544054,},
"sibling": {"f": 0.00436688659218365, "m": 0.0301729987453868,},
"partner": {"*": 0.00078339990345766},
"child": {"*": 0.00634223110220566},
},
"*": {"*": {"*": 1.675041946602729e-05}},
}
probability = infection_probability_month[person_sex][relation_type][relation_sex]
return np.random.binomial(1, probability)
def valid_age(person, i, min_age):
"""Verify age requirement of person in given iteration."""
return person["birth"] <= (i - min_age) and person["death"] >= i
def initialize(config):
"""Load population and generate initial incarceration state."""
random.seed(config.random_seed)
population_pickle = os.path.join(os.path.dirname(__file__), "population.pkl.gz")
popu = pickle.load(gzip.open(population_pickle, "rb"))
alive = []
for num, person in popu.items():
person["num"] = num
person["months_in_prison"] = 0
person["harsh_sentence"] = []
if valid_age(person, config.start_iter, config.min_age):
alive.append(person)
num_infect = round(config.percent * len(alive))
potentials = []
for person in alive:
if config.start_iter - person["birth"] <= 45:
potentials.append(person)
infected = random.sample(potentials, num_infect)
for person in infected:
sentence, harsh_sentence = generate_sentence(person, -1, 0, config)
person["incarcerated"] = sentence
person["harsh_sentence"].append(harsh_sentence)
return popu
def spread_infection(popu, person, itr, month, config):
"""Pass on infection."""
# ensure same infection patterns regardless of sentence intervention
np.random.seed(
hash((config.random_seed, person["num"], itr, month)) % (2 ** 32 - 1)
)
sex = person["sex"]
for sibling in person["siblings"]:
popu[sibling]["infected"] += infect(sex, "sibling", popu[sibling]["sex"])
if person["partner"] >= 0 and person["iter_married"] >= itr:
partner = person["partner"]
popu[partner]["infected"] += infect(popu[partner]["sex"], "partner", "*")
for child in person["children"]:
if valid_age(popu[child], itr, config.min_age):
popu[child]["infected"] += infect(sex, "child", "*")
for friend in person["friends"]:
popu[friend]["infected"] += infect(sex, "sibling", popu[friend]["sex"])
for parent in person["parents"]:
popu[parent]["infected"] += infect(sex, "parent", popu[parent]["sex"])
def generate_sentence(person, itr, month, config):
"""Generate sentence based on harshness setting."""
# generate both sentences to ensure consistent counterfactuals
# offset random seed to avoid interference with infection random seed
np.random.seed(
hash((1, config.random_seed, person["num"], itr, month)) % (2 ** 32 - 1)
)
gamma_parameter = 1.2
lenient_mean = config.mean_sentence_lenient
gamma_sample = np.random.gamma(gamma_parameter, lenient_mean / gamma_parameter)
lenient_sentence = np.random.poisson(gamma_sample)
if config.consistent_sentence_length:
penalty = config.mean_sentence_harsh - config.mean_sentence_lenient
harsh_sentence = lenient_sentence + penalty
else:
harsh_mean = config.mean_sentence_harsh
gamma_sample = np.random.gamma(gamma_parameter, harsh_mean / gamma_parameter)
harsh_sentence = np.random.poisson(gamma_sample)
if config.random_sentence_type:
if np.random.binomial(1, config.random_sentence_bias):
return harsh_sentence, True
return lenient_sentence, False
if config.harsh_sentence:
return harsh_sentence, True
return lenient_sentence, False
def assign_sentence(person, itr, month, config):
"""Assign sentence."""
if valid_age(person, itr, config.min_age):
sentence, harsh_sentence = generate_sentence(person, itr, month, config)
person["incarcerated"] = sentence
person["harsh_sentence"].append(harsh_sentence)
def simulate(config, show_progress=False):
"""Simulate incarceration contagion dynamics.
Parameters
----------
config : Config
Config object specifying simulation parameters.
Returns
-------
dict
Dictionary specifying simulated population of agents.
"""
popu = initialize(config)
agents = popu.values()
def display(range_obj):
if show_progress:
range_obj = tqdm(range_obj)
return range_obj
# these are in years. need to work in terms of months
for itr in display(range(config.start_iter, config.end_iter)):
for month in range(12):
# infection step
for person in agents:
# random infection, not due to contagion
if valid_age(person, itr, config.min_age):
person["infected"] += infect("*", "*", "*")
# infect connected people
if person["incarcerated"] > 0:
person["incarcerated"] -= 1
person["months_in_prison"] += 1
spread_infection(popu, person, itr, month, config)
# sentencing step
for person in agents:
if person["infected"] and not person["incarcerated"]:
assign_sentence(person, itr, month, config)
person["infected"] = 0
return popu