In this project, I explore the relationship between density of formerly incarcerated individuals within a neighborhood and likelihood that prison culture becomes embedded into a neighborhood’s culture ("prisonization"), using an adaptation of Axelrod’s model of cultural dissemination (1997).
Through the rise of mass incarceration over the last thirty years, American communities contain more people than ever before who have experienced incarceration. Many low-income and minority communities, disproportionately affected by tough-on-crime legislation, have particularly high concentrations of people returning home from jails and prisons. Prisons, as total institutions, tend to develop their own subcultures as their members are forced to alter their conceptions of self (Goffman 1961). Whether this prison subculture spreads to communities receiving large numbers of former inmates is unknown.
Econometric approaches for estimating effects of incarceration on neighborhoods are generally not feasible due to multidirectional causality. An agent-based modeling approach to understanding this relationship provides bottom-up insight into understanding how influences at the individual level can cause community-level changes in culture. Through the agent-based model I am developing, I can explore the shape of the relationship between density of previously incarcerated individuals in a neighbhorhood and the likelihood that the overall neighborhood culture changes as a result of cultural traits picked up by individuals during incarceration.
The outcome of interest in this model is the shape of the relationship between initial prisionization levels and equilibrium prisonization levels. A linear or concave relationship between these two levels indicate that there are no compounding effects of high-density incarceration on neighbhorhood culture, whereas a convex relationship indicates that prison culture can spread to the wider community once incarceration reaches some tipping point.
The space is a two-dimensional grid, where the edges wrap around to form a torus.
The agents are individuals that reside on the grid. Each space on the grid contains an agent. Agents all have a vector of cultural traits, of which prisonized is one.
The model contains three classes:
The model consists of five parameters
The model is initialized by creating a gridSize by gridSize grid, and creating agents to populate the grid. Each agent is randomly assigned a vector indicating that agent's cultural features. prisPct percent of agents in the model are randomly designated as having previously been prisonized as a result of incarceration at the start of the model.
To begin a single step of the model, an agent is picked at random. Next, one of that agent’s four neighbors (north, south, east, and west) is randomly chosen. Each of the agent’s features, including prisonization, is compared to those if its neighbor. If the agent has any features in common with its neighbor, the agent “interacts” with its neighbor, and takes on one of the neighbor’s randomly chosen non-shared features (including prisonization) with some probability equal to the index of similarity between the agent and the neighbor.
Steps are repeated until the model reaches equilibrium. This occurs when all agents have either all features or no features in common with all of their neighbors, thus no further cultural contagion is possible. According to the Axelrod model, this should lead to some number of groups that are culturally homogenous.
Once the model reaches equilibrium, I calculate equilibrium proportion of agents who are prisonized.
I choose fixed values for gridSize, numFeatures and numTraits, and then sweep through values between 0 and 1 of prisPct, with increments of 0.1. For each value of prisPct, I run the model 100 times. I then collect the model parameters and the equilibrium proportion of prisonized agents and explore the relationship between initial and equilibrium prisonization levels.
In [3]:
# Import necesary modules
#%matplotlib inline
import random
import time
import matplotlib.pyplot as plt
import numpy as np
import csv
import datetime
# import seaborn; seaborn.set()
# Helper function for getting the current time in seconds
millis = lambda: int(round(time.time()*1000))
'''
Features class:
Static vars:
* traitCounts - an array with length equal to the feature count. Each
element holds an integer representing the number of possible traits
for the corresponding feature.
Object vars:
* curTraits - an array with length equal to the feature count. Each
element holds the current trait value for the corresponding feature.
For the purposes of this model, curTraits[0] represets the binary
trait for the prisionization feature.
Static methods:
* init(traitCounts, prisPct) - sets the feature count, traitCounts,
and initial relative prisionization.
Object methods:
* randomizeTraits - sets random traits for each feature in the object
* setTrait - sets the trait of a selected feature.
'''
class Features(object):
# Initialize the feature count and the trait ranges for those features
@staticmethod
def init(traitCounts):
Features.count = len(traitCounts)
Features.traitCounts = traitCounts
def __init__(self):
# Initialize an empty array with a location for each current trait
self.curTraits = [0 for i in range(Features.count)]
self.randomizeTraits()
self.setTrait(0,0)
def randomizeTraits(self):
for i in range(1, Features.count):
self.curTraits[i] = random.randint(0, self.traitCounts[i]-1)
def setTrait(self, which, val):
self.curTraits[which] = val
'''
Agent class:
Object vars:
* grid - a reference to the grid in which the agent is located
* row - row of the grid in which the agent is located
* col - column of the grid in which the agent is located
* features - the features object
Object methods:
* printTraits - prints to console the current traits for this agent
* influencePossible - returns a boolean value indicating whether the
agent could be influenced by any of its neighbors
* isInfluenced(neighbor) - returns a boolean value indicating whether
a new interaction with a given neighbor causes the agent to be
influenced
* isPrisonized - returns a boolean value indicating whether the agent
is currently prisionized
* similarity(neighbor) - returns a similarity index from 0.0 to 1.0
indicating the agent's cultural similarity to the given neighbor
* differingTraits(neighbor) - returns an array containing values of the
features for which the agent and the given neighbor do not share
the same trait
* inheritTrait(neighbor) - causes the agent to inherit a randomly
selected feature trait from the given neighbor
* executeModel - selects a random neighbor, tests whether the agent
is influenced, and if it is, causes the agent to inherit a trait
from that neighbor
'''
class Agent(object):
def __init__(self, row, col, grid):
self.grid = grid
self.row = row
self.col = col
self.features = Features()
def printTraits(self):
print self.features.curTraits
def influencePossible(self):
# Get all neighbors
r = self.row
c = self.col
neighbors = [grid.getAgent((r+1) % self.grid.size, c), grid.getAgent((r-1) % self.grid.size, c), \
grid.getAgent(r, (c-1) % self.grid.size), grid.getAgent(r, (c+1) % self.grid.size)]
# Influence is possible if similarity to any neighbor is between 0 and 1
for i in range(len(neighbors)):
similarity = self.similarity(neighbors[i])
if similarity > 0 and similarity < 1:
return True
return False
def isInfluenced(self, neighbor):
sim = self.similarity(neighbor)
if sim ==1 or sim ==0:
return False
if sim > random.random():
return True
else:
return False
def isPrisonized(self):
return True if self.features.curTraits[0] == 1 else False
def similarity(self, neighbor):
matchingTraits = 0
for x in range (Features.count):
if self.features.curTraits[x] == neighbor.features.curTraits[x]:
matchingTraits += 1
return float(matchingTraits) / Features.count
def differingTraits(self, neighbor):
diffTraits = []
for x in range (Features.count):
if self.features.curTraits[x] != neighbor.features.curTraits[x]:
diffTraits.append(x)
return diffTraits
def inheritTrait(self, neighbor):
which = random.choice(self.differingTraits(neighbor))
self.features.curTraits[which] = neighbor.features.curTraits[which]
def executeModel(self):
# Pick a neighbor location
# I changed this to use NSEW neighbors, and to wrap around the grid
if random.random() > .5:
row = (self.row + random.choice([1, -1])) % self.grid.size
col = self.col
else:
row = self.row
col = (self.col + random.choice([1, -1])) % self.grid.size
# Retrieve neighbor
neighbor = self.grid.getAgent(row, col)
if self.isInfluenced(neighbor):
self.inheritTrait(neighbor)
'''
Grid class:
Object vars:
* size - the height / width of the grid
* agents - a 2D matrix where each element contains an agent
Object methods:
* getLocationCount - returns the total number of elements in the
agents matrix
* addAgent(row, col) - adds a new agent at the specified matrix location
* getAgent(row, col) - returns the agent object from the specified
matrix location
* getPrisPortion - returns a value from 0.0 to 1.0 representing the
portion of the total grid population that is currently prisionized
* isAtEquilibrium - returns a boolean value indicating whether the
grid object is currently at equilibrium (this occurs when every
agent in the grid either completly shares the culture of all its
neighbors or shares no culture with its neighbors)
'''
class Grid(object):
def __init__(self, size):
self.agents = [[0 for x in range(size)] for x in range(size)]
self.size = size
def getLocationCount(self):
count = self.size * self.size
return count
def addAgent(self, row, col):
self.agents[row][col] = Agent(row, col, self)
def getAgent(self, row, col):
return self.agents[row][col]
def getPrisPortion(self):
prisPop = 0
for x in range(self.size):
for y in range(self.size):
if self.getAgent(x, y).isPrisonized():
prisPop += 1
return float(prisPop) / self.getLocationCount()
def isAtEquilibrium(self):
for x in range(self.size):
for y in range(self.size):
if(self.getAgent(x, y).influencePossible()):
return False
return True
def printSimilarities():
for row in range(grid.size):
for col in range(grid.size):
agent = grid.getAgent(row, col)
print "(" + str(row) + ", " + str(col) + ") " + str(agent.features.curTraits)
print agent.similarity(grid.getAgent(row, (col+1) % grid.size))
print agent.similarity(grid.getAgent((row+1) % grid.size , col))
def printFeatures():
for row in range(grid.size):
for col in range(grid.size):
agent = grid.getAgent(row, col)
print str(row) + ", " + str(col) + " " + str(agent.features.curTraits)
# Parameters
gridSize = 10
numFeatures = 10
numTraits = 8
intervals = 10
loops = 100
# Set up output
with open('pris_output.csv', 'wb') as csvfile:
csvoutput = csv.writer(csvfile, delimiter=',',
quotechar = '|', quoting=csv.QUOTE_MINIMAL)
csvoutput.writerow(["date", "time", "gridSize", "numFeatures", "numTraits", "prisPct", "endingPrisPortion"])
# runHistory = None
# Run the model
stepSize = 100/intervals
for pct in range(0,101,stepSize):
print pct
for loop in range(loops):
prisPct = pct/100.00
traitCounts = [2]
for x in range(numFeatures):
traitCounts.append(numTraits)
'''
Initialize the features class with the number of different traits for each of
the features and the initial relative prisionization rate.
'''
Features.init(traitCounts)
# Initialize the grid size, add agents
grid = Grid(gridSize)
for x in range(grid.size):
for y in range(grid.size):
grid.addAgent(x, y)
# Assign initial prisionization
x = round(prisPct*grid.getLocationCount())
loopCount = 0
while x > 0:
loopCount += 1
row = random.randint(0,grid.size-1)
col = random.randint(0,grid.size-1)
agent = grid.getAgent(row, col)
if agent.isPrisonized() == False:
agent.features.setTrait(0,1)
x -= 1
# Run the model
iteration = 0
running = True
startTime = millis()
while running:
# Select a random agent for this model step, then execute the model
thisAgent = grid.getAgent(random.randint(0, (grid.size - 1)), random.randint(0, (grid.size - 1)))
thisAgent.executeModel()
iteration += 1
# Only check for equilibrium once in a while to save time
if iteration % 10 == 0:
if grid.isAtEquilibrium():
running = False
# Report model results
csvoutput.writerow([datetime.datetime.now().date(), datetime.datetime.now().time(), gridSize, numFeatures, numTraits, prisPct, grid.getPrisPortion()])
The model writes a CSV file that contains, for each model run:
Ending prisonization level means and standard devaiations by starting prisonization level are calculated below:
In [44]:
import numpy as np
import pandas as pd
from pandas import *
data = pd.read_csv('/Users/erinlane/Box Sync/CSCS_530/prisonization_github/pris_output.csv')
df = DataFrame(data, columns = ['prisPct', 'endingPrisPortion'])
# Create table with means and standard deviations
mean_endPris = df.groupby(["prisPct"])["endingPrisPortion"].mean()
std_endPris = df.groupby(["prisPct"])["endingPrisPortion"].std()
endPris = pandas.concat((mean_endPris, std_endPris), axis=1)
endPris.columns = ["mean", "std"]
print endPris
The null hypothesis in the model is that the expected ending level of prisonization is equal to the level of prisonization at the outset. This certainly appears to be the case based on the model output listed above. To formally test this, I import the CSV with model results into Stata, regress endingPrisPortion on prisPct, and test that the coefficient on prisPct = 1.
I fail to reject the null hypothesis, indicating a linear relationship between initial prisonization levels and ending prisonization levels.
. import delimited "C:\Users\erlane\Box Sync\CSCS_530\prisonization_github\pris_output.csv", clear
(7 vars, 1,100 obs)
. regress endingprisportion prispct
Source | SS df MS Number of obs = 1,100
-------------+---------------------------------- F(1, 1098) = 869.78
Model | 121.485091 1 121.485091 Prob > F = 0.0000
Residual | 153.361273 1,098 .13967329 R-squared = 0.4420
-------------+---------------------------------- Adj R-squared = 0.4415
Total | 274.846364 1,099 .250087683 Root MSE = .37373
------------------------------------------------------------------------------
endingpris~n | Coef. Std. Err. t P>|t| [95% Conf. Interval]
-------------+----------------------------------------------------------------
prispct | 1.050909 .0356337 29.49 0.000 .9809914 1.120827
_cons | -.0372727 .0210812 -1.77 0.077 -.0786366 .0040912
------------------------------------------------------------------------------
. test _b[prispct]=1
( 1) prispct = 1
F( 1, 1098) = 2.04
Prob > F = 0.1534
I was surprised to find that the model resulted in a linear relationship between initial prisonization level and the ending prisonization level, as were others familiar with the Axelrod model who came to my CSAAW presentation.
One possible issue with my model is that it only allows for a maximum of two cultural zones at equilibrium. This is because there are only two possible values for prisonization, 0 and 1. With the setup of the Axelrod model, the maximum number of cultural zones at equilibrium is limited by the number of possible values for the cultural feature with the least number of possible values, because at equilibrium, all agents within a zone must be totally idential or totally different from all of its neighbors. Thus, there can only be at maximum one homogenous prisonized zone and one homogenous non-prisonized zone at equilibrium. A next step might be to think of prisonization as having more than two possible outcomes, and then seeing if this makes a difference in the results. It seems unlikely that this will change the overall results, but worth investigating nonetheless.
Another direction I'd like to take is to try using a network setup rather than a grid setup. This will allow me to create agents with different levels of influence over their networks based on the number and strengths of social ties at model initialization.