Analyse a Public Goods Game
In this tutorial you will build a complete evolutionary game theory analysis
using ludics: from defining a population, through constructing the Markov
chain, to computing fixation probabilities.
The code blocks use Python's interactive prompt format. Lines starting with
>>> are code you run; lines without are the output.
Installing ludics
$ pip install ludics
The state space
A population of \(N\) players each playing one of \(k\) strategies is represented as an ordered vector \(\mathbf{a} = (a_1, \ldots, a_N)\), where \(a_i \in \{0, 1, \ldots, k-1\}\) is the strategy of player \(i\). The full state space is the set of all \(k^N\) such vectors.
See How states are represented for a detailed explanation of this convention.
>>> import ludics
>>> N = 2
>>> number_of_strategies = 2
>>> ludics.get_state_space(N=N, k=number_of_strategies)
array([[0, 0],
[0, 1],
[1, 0],
[1, 1]])
With \(N=2\) players and \(k=2\) strategies (0 = defect, 1 = cooperate), there are
four states. [0, 0] (all-defect) and [1, 1] (all-cooperate) are absorbing
under most evolutionary dynamics: once the population reaches either state, it
stays there.
The fitness function
The fitness function maps a state to a payoff for each player. Here we use a homogeneous Public Goods Game with multiplication factor \(r = 1.5\) and contribution \(\alpha = 2\). Since \(r < N = 2\), defection is individually rational.
>>> import ludics.fitness_functions
>>> import numpy as np
>>> ludics.fitness_functions.homogeneous_pgg_fitness_function(
... state=np.array([0, 1]),
... alpha=2,
... r=1.5,
... )
array([ 1.5, -0.5])
In state [0, 1], player 0 defects and player 1 cooperates. The defector
earns 1.5 and the cooperator earns -0.5, as expected when \(r < N\).
The evolutionary dynamic
An evolutionary dynamic determines how the population transitions between
states. Here we use the Moran process: fitter players are more likely to
reproduce and pass on their strategy. The selection_intensity parameter
controls how strongly fitness differences influence the outcome (0 = neutral
drift, larger values = stronger selection).
compute_moran_transition_probability returns the probability of moving from
one state to a neighbouring state in a single step:
>>> import ludics
>>> import ludics.fitness_functions
>>> import numpy as np
>>> source = np.array([0, 1])
>>> target = np.array([0, 0])
>>> ludics.compute_moran_transition_probability(
... source=source,
... target=target,
... selection_intensity=0.5,
... fitness_function=ludics.fitness_functions.homogeneous_pgg_fitness_function,
... alpha=2,
... r=1.5,
... )
np.float64(0.35)
From state [0, 1], the probability of moving to [0, 0] in one step is 0.35.
The transition matrix
generate_transition_matrix assembles transition probabilities for the entire
state space into a single matrix \(T\), where \(T_{ij}\) is the probability of
moving from state \(i\) to state \(j\):
>>> import ludics
>>> import ludics.fitness_functions
>>> import numpy as np
>>> state_space = ludics.get_state_space(N=2, k=2)
>>> ludics.generate_transition_matrix(
... state_space=state_space,
... compute_transition_probability=ludics.compute_moran_transition_probability,
... fitness_function=ludics.fitness_functions.homogeneous_pgg_fitness_function,
... selection_intensity=0.5,
... alpha=2,
... r=1.5,
... )
array([[1. , 0. , 0. , 0. ],
[0.35, 0.5 , 0. , 0.15],
[0.35, 0. , 0.5 , 0.15],
[0. , 0. , 0. , 1. ]])
Rows and columns correspond to states [0,0], [0,1], [1,0], [1,1]. The
first and last rows confirm that [0,0] and [1,1] are absorbing.
Fixation probabilities
The fixation probability is the probability that, starting from a mixed state, the population eventually fixes on a particular strategy. It is given by the absorption matrix, whose entry \(B_{ij}\) is the probability of fixing in absorbing state \(j\) when starting from transient state \(i\):
>>> transition_matrix = ludics.generate_transition_matrix(
... state_space=state_space,
... compute_transition_probability=ludics.compute_moran_transition_probability,
... fitness_function=ludics.fitness_functions.homogeneous_pgg_fitness_function,
... selection_intensity=0.5,
... alpha=2,
... r=1.5,
... )
>>> ludics.compute_absorption_matrix(transition_matrix)
array([[0.7, 0.3],
[0.7, 0.3]])
The rows correspond to the two transient states [0,1] and [1,0]; the
columns to [0,0] (all-defect) and [1,1] (all-cooperate). From either mixed
state, the population fixes on defection with probability 0.7 and on
cooperation with probability 0.3, as expected when \(r < N\).
Further reading
For the theory behind these methods, see the bibliography. Nowak (2006) and Hofbauer and Sigmund (1998) both cover evolutionary dynamics and fixation probabilities in depth.