I've developed a class used for some epidemic simulations I'm doing. Individuals are 'S' (susceptible), 'I' (infected), or 'R' (recovered). These are standard abbreviations in the research community. I assume some calculations are done in advance to determine who will be infected (and recover) when. There will be many different ways I do this calculation depending on the population studied.
Some questions I have:
Should I worry that I'm passing in some dicts that could later be edited by the user? What's the best way to prevent this?
Any advice on how to make this clean? I'm moderately familiar with Python, but it's self-taught, and this is the first time I'm seriously working with classes.
import scipy
import pylab as py
from collections import Counter
class SIREpidemic(object):
"""
This will have the basic commands we want for any variety of SIR epidemic.
When an epidemic is initialized, we will have already calculated the time of
infection and recovery for each individual. These will be passed in as dicts.
Each individual, if infected will have individual.inftime as the time of infection
and individual.rectime as time of recovery.
"""
def __init__(self, infectionTime, recoveryTime, N):
'''Process the infectionTime and RecoveryTime to determine who is susceptible
when. infectionTime[individual] gives time of infection of individual.
recoveryTime[individual] gives recovery time of individual (the keys for both
lists must match). N is the total population size (may include individuals
that are never infected.'''
statusTypes = ['S', 'I', 'R']
self._timeSeries = {status:scipy.array([]) for status in statusTypes}
self._timeSeries['t'] = scipy.array([])
self.N = N
self._infectionTime = infectionTime
self._recoveryTime = recoveryTime
infTimes = [infectionTime[individual] for individual in infectionTime.keys()]
recTimes = [recoveryTime[individual] for individual in infectionTime.keys()]
infTimeCounter = Counter(infTimes)
recTimeCounter = Counter(recTimes)
self._timeSeries['t'] = scipy.array(sorted(set(infTimes+recTimes)))
for status in statusTypes:
self._timeSeries[status] = 0*self._timeSeries['t'] #initializing to be the right size
datum = {'S':1, 'I':0, 'R': 0}
for index, time in enumerate(self._timeSeries['t']):
incidence = infTimeCounter[time]
recoveries = recTimeCounter[time]
datum['S'] -= float(incidence)/self.N
datum['I'] += float(incidence - recoveries)/self.N
datum['R'] += float(recoveries)/self.N
for status in statusTypes:
self._timeSeries[status][index]=datum[status]
def infTime(self, individual):
return self._infectionTime.get(individual,None)
def recTime(self, individual):
return self._recoveryTime.get(individual,None)
def t(self):
return self._timeSeries['t']
def S(self):
return self._timeSeries['S']
def I(self):
return self._timeSeries['I']
def R(self):
return self._timeSeries['R']
def size(self):
return self._timeSeries['R'][-1]
def initial_size(self):
return 1-self._timeseries['S'][0]
def plot(self,x=None, y=None, fid = None): #x and y are either 'S', 'I', 'R', 'cumulative', or 't'.
'''if no arguments, plot S,I, and R vs t. if just one argument, then plot that versus t. if two arguments, plot second versus first. Need to add a collection of arguments to pass to plot.'''
if y == None and x == None:
self.plot('t', 'S')
self.plot('t', 'I')
self.plot('t', 'R')
else:
if y == None:
y = x
x = 't'
elif x == None: #but y was something else
x = 't'
if fid != None:
py.figure(fid)
if x == 't':
py.plot(self._timeSeries[x],self._timeSeries[y], label = r'$'+y+'$')
else:
py.plot(self._timeSeries[x],self._timeSeries[y], label = r'$'+y+'$ versus $'+x+'$')