Note

You can download this demonstration as a Jupyter Notebook here

Stochastic processes and reproducibility

Random numbers and stochastic processes are essential to many agent-based models. In Python, we can use the pseudo-random number generator from the built-in library random.

Pseudo-random means that this module generates numbers in a sequence that appears random but is actually deterministic, based on an initial seed value. In other words, the generator will produce the same pseudo-random sequence over multiple runs if it is given the same seed at the beginning. We can define this seed to receive reproducible results from a model with stochastic processes.

Generating random numbers

[1]:
import agentpy as ap
import random

To illustrate, let us define a model that generates a list of ten pseudo-random numbers:

[2]:
class RandomModel(ap.Model):

    def setup(self):
        self.random_numbers = [random.randint(0, 9) for _ in range(10)]
        print(f"Model {self.p.n} generated the numbers {self.random_numbers}")

Now if we run this model multiple times, we will get a different series of numbers:

[3]:
for i in range(2):
    parameters = {'steps':0, 'n':i}
    model = RandomModel(parameters)
    results = model.run(display=False)
Model 0 generated the numbers [9, 3, 3, 8, 8, 0, 1, 9, 4, 7]
Model 1 generated the numbers [0, 5, 9, 4, 6, 5, 3, 2, 2, 0]

If we want the results to be reproducible, we can define a parameter seed that will be used automatically at the beginning of Model.run(). Now, we get the same series of numbers:

[4]:
for i in range(2):
    parameters = {'seed':1, 'steps':0, 'n':i}
    model = RandomModel(parameters)
    model.run(display=False)
Model 0 generated the numbers [2, 9, 1, 4, 1, 7, 7, 7, 6, 3]
Model 1 generated the numbers [2, 9, 1, 4, 1, 7, 7, 7, 6, 3]

Using multiple generators

The automatic use of the seed parameter calls the method random.seed(), which affects the default number generator that is created as a hidden instance by the random module. For more advanced applications, we can create seperate generators for each object, using random.Random. We can ensure that the seeds of each object follow a controlled pseudo-random sequence by using also using seperate generator in the main model. Note that we use a different parameter name model_seed to avoid the automatic use of the parameter seed in this case.

[5]:
class RandomAgent2(ap.Agent):

    def setup(self):
        seed = model.seed_generator.getrandbits(128)  # Get seed from model
        self.random = random.Random(seed)  # Create generator for this agent
        self.random_numbers = [self.random.randint(0, 9) for _ in range(10)]
        print(f"{self} generated the numbers {self.random_numbers}")

class RandomModel2(ap.Model):

    def setup(self):
        self.seed_generator = random.Random(self.p.model_seed)
        self.add_agents(2, RandomAgent2)

for i in range(2):
    print(f"Model {i}:")
    parameters = {'model_seed': 1, 'steps': 0}
    model = RandomModel2(parameters)
    results = model.run(display=False)
    print()
Model 0:
RandomAgent2 (Obj 1) generated the numbers [8, 7, 0, 1, 2, 3, 9, 4, 5, 0]
RandomAgent2 (Obj 2) generated the numbers [8, 1, 4, 6, 6, 3, 4, 3, 5, 1]

Model 1:
RandomAgent2 (Obj 1) generated the numbers [8, 7, 0, 1, 2, 3, 9, 4, 5, 0]
RandomAgent2 (Obj 2) generated the numbers [8, 1, 4, 6, 6, 3, 4, 3, 5, 1]

Alternatively, we could also have each agent start from the same seed:

[6]:
class RandomAgent3(ap.Agent):

    def setup(self):
        self.random = random.Random(self.p.agent_seed)
        self.random_numbers = [self.random.randint(0, 9) for _ in range(10)]
        print(f"{self} generated the numbers {self.random_numbers}")

class RandomModel3(ap.Model):

    def setup(self):
        self.add_agents(2, RandomAgent3)

for i in range(2):
    print(f"\nModel {i}:")
    parameters = {'agent_seed': 1, 'steps':0, 'n':i}
    model = RandomModel3(parameters)
    results = model.run(display=False)

Model 0:
RandomAgent3 (Obj 1) generated the numbers [2, 9, 1, 4, 1, 7, 7, 7, 6, 3]
RandomAgent3 (Obj 2) generated the numbers [2, 9, 1, 4, 1, 7, 7, 7, 6, 3]

Model 1:
RandomAgent3 (Obj 1) generated the numbers [2, 9, 1, 4, 1, 7, 7, 7, 6, 3]
RandomAgent3 (Obj 2) generated the numbers [2, 9, 1, 4, 1, 7, 7, 7, 6, 3]

Modeling stochastic processes

This section presents some stochastic operations that are often used in agent-based models. To start, we prepare a generic model with ten agents:

[7]:
model = ap.Model()
agents = model.add_agents(10)
agents
[7]:
AgentList [10 agents]

If we look at the agent’s ids, we see that they have been created in order:

[8]:
agents.id
[8]:
AttrList of attribute 'id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

We can shuffle this list with AgentList.shuffle():

[9]:
agents.shuffle().id
[9]:
AttrList of attribute 'id': [5, 10, 3, 9, 6, 4, 7, 1, 8, 2]

Or create a random subset with AgentList.random():

[10]:
agents.random(5).id
[10]:
AttrList of attribute 'id': [6, 9, 10, 3, 5]

Both AgentList.shuffle() and AgentList.random() can take a custom generator as an argument:

[11]:
for _ in range(2):
    custom_generator = random.Random(1)
    print(agents.random(5, custom_generator).id)
AttrList of attribute 'id': [3, 10, 6, 5, 9]
AttrList of attribute 'id': [3, 10, 6, 5, 9]

Note that the above selection is without repetition, i.e. every agent can only be selected once. Outside these built-in functions of agentpy, there are many other tools that can be used for stochastic processes. For example, we can use the methods random.choices() to make a selection with repetition and probability weights. In the following example, agents with a higher id are more likely to be chosen:

[12]:
choices = random.choices(agents, k=5, weights=agents.id)

If needed, the resulting list from such selections can be converted back into an AgentList:

[13]:
ap.AgentList(choices).id
[13]:
AttrList of attribute 'id': [5, 4, 5, 8, 7]

Further reading