Moran Process

The strategies in the library can be pitted against one another in the Moran process, a population process simulating natural selection.

The process works as follows. Given an initial population of players, the population is iterated in rounds consisting of:

  • matches played between each pair of players, with the cumulative total scores recorded
  • a player is chosen to reproduce proportional to the player’s score in the round
  • a player is chosen at random to be replaced

The process proceeds in rounds until the population consists of a single player type. That type is declared the winner. To run an instance of the process with the library, proceed as follows:

>>> import axelrod as axl
>>> players = [axl.Cooperator(), axl.Defector(),
...               axl.TitForTat(), axl.Grudger()]
>>> mp = axl.MoranProcess(players)
>>> populations = mp.play()
>>> mp.winning_strategy_name   
Defector

You can access some attributes of the process, such as the number of rounds:

>>> len(mp)  
6

The sequence of populations:

>>> import pprint
>>> pprint.pprint(populations)  
[Counter({'Defector': 1, 'Cooperator': 1, 'Grudger': 1, 'Tit For Tat': 1}),
Counter({'Defector': 1, 'Cooperator': 1, 'Grudger': 1, 'Tit For Tat': 1}),
Counter({'Defector': 2, 'Cooperator': 1, 'Grudger': 1}),
Counter({'Defector': 3, 'Grudger': 1}),
Counter({'Defector': 3, 'Grudger': 1}),
Counter({'Defector': 4})]

The scores in each round:

>>> for row in mp.score_history: 
...     print([round(element, 1) for element in row])
[[6.0, 7.08, 6.99, 6.99],
[6.0, 7.08, 6.99, 6.99],
[3.0, 7.04, 7.04, 4.98],
[3.04, 3.04, 3.04, 2.97],
[3.04, 3.04, 3.04, 2.97]]

The MoranProcess class also accepts an argument for a mutation rate. Nonzero mutation changes the Markov process so that it no longer has absorbing states, and will iterate forever. To prevent this, iterate with a loop (or function like takewhile from itertools):

>>> import axelrod as axl
>>> axl.seed(4) # for reproducible example
>>> players = [axl.Cooperator(), axl.Defector(),
...               axl.TitForTat(), axl.Grudger()]
>>> mp = axl.MoranProcess(players, mutation_rate=0.1)
>>> for _ in mp:
...     if len(mp.population_distribution()) == 1:
...         break
>>> mp.population_distribution()
Counter({'Cooperator': 4})

Moran Process on Graphs

The library also provides a graph-based Moran process [Shakarian2013] with MoranProcessGraph. To use this class you must supply at least one Axelrod.graph.Graph object, which can be initialized with just a list of edges:

edges = [(source_1, target1), (source2, target2), ...]

The nodes can be any hashable object (integers, strings, etc.). For example:

>>> from axelrod.graph import Graph
>>> edges = [(0, 1), (1, 2), (2, 3), (3, 1)]
>>> graph = Graph(edges)

Graphs are undirected by default. Various intermediates such as the list of neighbors are cached for efficiency by the graph object.

A Moran process can be invoked with one or two graphs. The first graph, the interaction graph, dictates how players are matched up in the scoring phase. Each player plays a match with each neighbor. The second graph dictates how players replace another during reproduction. When an individual is selected to reproduce, it replaces one of its neighbors in the reproduction graph. If only one graph is supplied to the process, the two graphs are assumed to be the same.

To create a graph-based Moran process, use a graph as follows:

>>> import axelrod
>>> from axelrod import Cooperator, Defector, MoranProcessGraph
>>> from axelrod.graph import Graph
>>> axelrod.seed(40)
>>> edges = [(0, 1), (1, 2), (2, 3), (3, 1)]
>>> graph = Graph(edges)
>>> players = [Cooperator(), Cooperator(), Cooperator(), Defector()]
>>> mp = MoranProcessGraph(players, interaction_graph=graph)
>>> results = mp.play()
>>> mp.population_distribution()
Counter({'Cooperator': 4})

You can supply the reproduction_graph as a keyword argument. The standard Moran process is equivalent to using a complete graph for both graphs.