-
Notifications
You must be signed in to change notification settings - Fork 282
Moran process #534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Moran process #534
Changes from all commits
5269133
9a3494d
9de867e
fc831f3
1146f3a
5b28eec
b487bde
9a950d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from collections import Counter | ||
| import random | ||
|
|
||
| import numpy as np | ||
|
|
||
| from .deterministic_cache import DeterministicCache | ||
| from .match import Match, is_stochastic | ||
| from .player import Player | ||
| from .random_ import randrange | ||
|
|
||
|
|
||
| def fitness_proportionate_selection(scores): | ||
| """Randomly selects an individual proportionally to score. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| scores: Any sequence of real numbers | ||
|
|
||
| Returns | ||
| ------- | ||
| An index of the above list selected at random proportionally to the list | ||
| element divided by the total. | ||
| """ | ||
| csums = np.cumsum(scores) | ||
| total = csums[-1] | ||
| r = random.random() * total | ||
|
|
||
| for i, x in enumerate(csums): | ||
| if x >= r: | ||
| return i | ||
|
|
||
| class MoranProcess(object): | ||
| def __init__(self, players, turns=100, noise=0): | ||
| self.turns = turns | ||
| self.noise = noise | ||
| self.players = list(players) # initial population | ||
| self.winning_strategy_name = None | ||
| self.populations = [] | ||
| self.populations.append(self.population_distribution()) | ||
| self.score_history = [] | ||
| self.num_players = len(self.players) | ||
|
|
||
| @property | ||
| def _stochastic(self): | ||
| """ | ||
| A boolean to show whether a match between two players would be | ||
| stochastic | ||
| """ | ||
| return is_stochastic(self.players, self.noise) | ||
|
|
||
| def __next__(self): | ||
| """Iterate the population: | ||
| - play the round's matches | ||
| - chooses a player proportionally to fitness (total score) to reproduce | ||
| - choose a player at random to be replaced | ||
| - update the population | ||
| """ | ||
| # Check the exit condition, that all players are of the same type. | ||
| population = self.populations[-1] | ||
| classes = set(p.__class__ for p in self.players) | ||
| if len(classes) == 1: | ||
| self.winning_strategy_name = str(self.players[0]) | ||
| raise StopIteration | ||
| scores = self._play_next_round() | ||
| # Update the population | ||
| # Fitness proportionate selection | ||
| j = fitness_proportionate_selection(scores) | ||
| # Randomly remove a strategy | ||
| i = randrange(0, len(self.players)) | ||
| # Replace player i with clone of player j | ||
| self.players[i] = self.players[j].clone() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this going to be ok? Can we have a test with strategies we've had issues with before? In the same way we have a test for tournaments with strategies
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about that -- I was hoping you could work some hypothesis magic :). I think we should be ok as far as strategy pairings go since I'm using the match class. So if they pass their clone tests and match pairings they should be fine here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sure thing: I'll send you a PR (probably in time for your tomorrow morning).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| self.populations.append(self.population_distribution()) | ||
|
|
||
| def _play_next_round(self): | ||
| """Plays the next round of the process. Every player is paired up | ||
| against every other player and the total scores are recorded.""" | ||
| N = self.num_players | ||
| scores = [0] * N | ||
| for i in range(N): | ||
| for j in range(i + 1, N): | ||
| player1 = self.players[i] | ||
| player2 = self.players[j] | ||
| player1.reset() | ||
| player2.reset() | ||
| match = Match((player1, player2), self.turns, noise=self.noise) | ||
| match.play() | ||
| match_scores = np.sum(match.scores(), axis=0) / float(self.turns) | ||
| scores[i] += match_scores[0] | ||
| scores[j] += match_scores[1] | ||
| self.score_history.append(scores) | ||
| return scores | ||
|
|
||
| def population_distribution(self): | ||
| """Returns the population distribution of the last iteration.""" | ||
| player_names = [str(player) for player in self.players] | ||
| counter = Counter(player_names) | ||
| return counter | ||
|
|
||
| next = __next__ # Python 2 | ||
|
|
||
| def __iter__(self): | ||
| return self | ||
|
|
||
| def reset(self): | ||
| """Reset the process to replay.""" | ||
| self.winning_strategy_name = None | ||
| self.populations = [self.populations[0]] | ||
| self.score_history = [] | ||
|
|
||
| def play(self): | ||
| """Play the process out to completion.""" | ||
| while True: | ||
| try: | ||
| self.__next__() | ||
| except StopIteration: | ||
| break | ||
|
|
||
| def __len__(self): | ||
| return len(self.populations) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # -*- coding: utf-8 -*- | ||
| import random | ||
| import unittest | ||
|
|
||
| import axelrod | ||
| from axelrod import MoranProcess | ||
| from axelrod.moran import fitness_proportionate_selection | ||
|
|
||
| from hypothesis import given, example, settings | ||
| from hypothesis.strategies import integers, lists, sampled_from, random_module, floats | ||
|
|
||
|
|
||
| class TestMoranProcess(unittest.TestCase): | ||
|
|
||
| def test_fps(self): | ||
| self.assertEqual(fitness_proportionate_selection([0, 0, 1]), 2) | ||
| random.seed(1) | ||
| self.assertEqual(fitness_proportionate_selection([1, 1, 1]), 0) | ||
| self.assertEqual(fitness_proportionate_selection([1, 1, 1]), 2) | ||
|
|
||
| def test_stochastic(self): | ||
| p1, p2 = axelrod.Cooperator(), axelrod.Cooperator() | ||
| mp = MoranProcess((p1, p2)) | ||
| self.assertFalse(mp._stochastic) | ||
| p1, p2 = axelrod.Cooperator(), axelrod.Cooperator() | ||
| mp = MoranProcess((p1, p2), noise=0.05) | ||
| self.assertTrue(mp._stochastic) | ||
| p1, p2 = axelrod.Cooperator(), axelrod.Random() | ||
| mp = MoranProcess((p1, p2)) | ||
| self.assertTrue(mp._stochastic) | ||
|
|
||
| def test_exit_condition(self): | ||
| p1, p2 = axelrod.Cooperator(), axelrod.Cooperator() | ||
| mp = MoranProcess((p1, p2)) | ||
| mp.play() | ||
| self.assertEqual(len(mp), 1) | ||
|
|
||
| def test_two_players(self): | ||
| p1, p2 = axelrod.Cooperator(), axelrod.Defector() | ||
| random.seed(5) | ||
| mp = MoranProcess((p1, p2)) | ||
| mp.play() | ||
| self.assertEqual(len(mp), 5) | ||
| self.assertEqual(mp.winning_strategy_name, str(p2)) | ||
|
|
||
| def test_three_players(self): | ||
| players = [axelrod.Cooperator(), axelrod.Cooperator(), | ||
| axelrod.Defector()] | ||
| random.seed(5) | ||
| mp = MoranProcess(players) | ||
| mp.play() | ||
| self.assertEqual(len(mp), 7) | ||
| self.assertEqual(mp.winning_strategy_name, str(axelrod.Defector())) | ||
|
|
||
| def test_four_players(self): | ||
| players = [axelrod.Cooperator() for _ in range(3)] | ||
| players.append(axelrod.Defector()) | ||
| random.seed(10) | ||
| mp = MoranProcess(players) | ||
| mp.play() | ||
| self.assertEqual(len(mp), 9) | ||
| self.assertEqual(mp.winning_strategy_name, str(axelrod.Defector())) | ||
|
|
||
| @given(strategies=lists(sampled_from(axelrod.strategies), | ||
| min_size=2, # Errors are returned if less than 2 strategies | ||
| max_size=5, unique=True), | ||
| rm=random_module()) | ||
| @settings(max_examples=5, timeout=0) # Very low number of examples | ||
|
|
||
| # Two specific examples relating to cloning of strategies | ||
| @example(strategies=[axelrod.BackStabber, axelrod.MindReader], | ||
| rm=random.seed(0)) | ||
| @example(strategies=[axelrod.ThueMorse, axelrod.MindReader], | ||
| rm=random.seed(0)) | ||
| def test_property_players(self, strategies, rm): | ||
| """Hypothesis test that randomly checks players""" | ||
| players = [s() for s in strategies] | ||
| mp = MoranProcess(players) | ||
| mp.play() | ||
| self.assertIn(mp.winning_strategy_name, [str(p) for p in players]) | ||
|
|
||
| def test_reset(self): | ||
| p1, p2 = axelrod.Cooperator(), axelrod.Defector() | ||
| random.seed(8) | ||
| mp = MoranProcess((p1, p2)) | ||
| mp.play() | ||
| self.assertEqual(len(mp), 4) | ||
| self.assertEqual(len(mp.score_history), 3) | ||
| mp.reset() | ||
| self.assertEqual(len(mp), 1) | ||
| self.assertEqual(mp.winning_strategy_name, None) | ||
| self.assertEqual(mp.score_history, []) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| Moran Process | ||
| ============= | ||
|
|
||
| The strategies in the library can be pitted against one another in the | ||
| [Moran process](https://en.wikipedia.org/wiki/Moran_process), a population | ||
| process simulating natural selection. Given the evolutionary basis of the Moran | ||
| process it can be compared to the :ref:`ecological-variant`. | ||
| While that variant was used by Axelrod in his original works, the Moran process | ||
| is now much more widely studied in the literature. | ||
|
|
||
| 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 recored | ||
| - 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) | ||
| >>> mp.play() | ||
| >>> mp.winning_strategy_name # doctest: +SKIP | ||
| Defector | ||
|
|
||
| You can access some attributes of the process, such as the number of rounds:: | ||
|
|
||
| >>> len(mp) # doctest: +SKIP | ||
| 6 | ||
|
|
||
| The sequence of populations:: | ||
|
|
||
| >>> import pprint | ||
| >>> pprint.pprint(mp.populations) # doctest: +SKIP | ||
| [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: # doctest: +SKIP | ||
| ... print([round(element, 1) for element in row]) | ||
| [[6.0, 7.0800000000000001, 6.9900000000000002, 6.9900000000000002], | ||
| [6.0, 7.0800000000000001, 6.9900000000000002, 6.9900000000000002], | ||
| [3.0, 7.04, 7.04, 4.9800000000000004], | ||
| [3.04, 3.04, 3.04, 2.9699999999999998], | ||
| [3.04, 3.04, 3.04, 2.9699999999999998]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can be done simpler with
numpy.random.choiceas in https://github.com/Axelrod-Python/Axelrod/blob/master/axelrod/strategy_transformers.py#L296http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.random.choice.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm.. I'm not sure which is more simple.
scoreswill have to be normalized, something like:Technically scores could be all zeros in my implementation, or even some negative -- that shouldn't happen for IPD but it could for another game matrix. So we'd need a check or try/except block for that, and it might be better as is.Edit: I'm not sure this argument makes sense in this context, people usually prevent nonnegative values by exponentiating or some other such transformation. As a purely mathematical function it's fine to have negative values but not for this application.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is. Leave it.