Skip to content

Commit f2f0c99

Browse files
drvinceknightmarcharper
authored andcommitted
Add state to action analysis.
Add two functions to interaction_utils: - `interaction_utils.compute_state_to_action_distribution`: computes list of counter objects of mapping a `(state, action)` to a count. - `interaction_utils.compute_normalised_state_to_action_distribution`: computes list of counter objects of mapping a `(state, action)` to a normalised count (for a given state). There have then been incorporated in `axelrod.result_set` and in particular in the `result_set.summarise()` output. Which now includes the average rates for each strategy in a tournament. So for memory 1 strategies this means we can measure their parameters and for non memory 1 strategies we can infer what the parameters correspond to the data.
1 parent 8d210df commit f2f0c99

File tree

5 files changed

+361
-16
lines changed

5 files changed

+361
-16
lines changed

axelrod/interaction_utils.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,71 @@ def compute_normalised_state_distribution(interactions):
135135
return normalized_count
136136

137137

138+
def compute_state_to_action_distribution(interactions):
139+
"""
140+
Returns the count of each state to action
141+
for a set of interactions.
142+
143+
Parameters
144+
----------
145+
interactions : list of tuples
146+
A list containing the interactions of the match as shown at the top of
147+
this file.
148+
149+
Returns
150+
----------
151+
state_to_C_distributions : List of Counter Object
152+
List of Dictionaries where the keys are the states and actions
153+
"""
154+
if not interactions:
155+
return None
156+
157+
distributions = [Counter([(state, outcome[j]) for state, outcome in zip(interactions,
158+
interactions[1:])])
159+
for j in range(2)]
160+
return distributions
161+
162+
163+
def compute_normalised_state_to_action_distribution(interactions):
164+
"""
165+
Returns the normalised count of each state to action
166+
for a set of interactions.
167+
168+
Parameters
169+
----------
170+
interactions : list of tuples
171+
A list containing the interactions of the match as shown at the top of
172+
this file.
173+
174+
Returns
175+
----------
176+
normalised_state_to_C_distributions : List of Counter Object
177+
List of Dictionaries where the keys are the states and actions and the
178+
values
179+
the proportion of times that state resulted in a cooperation.
180+
"""
181+
if not interactions:
182+
return None
183+
184+
distribution = compute_state_to_action_distribution(interactions)
185+
normalized_distribution = []
186+
for player in range(2):
187+
counter = {}
188+
for state in [('C', 'C'), ('C', 'D'), ('D', 'C'), ('D', 'D')]:
189+
C_count = distribution[player].get((state, 'C'), 0)
190+
D_count = distribution[player].get((state, 'D'), 0)
191+
total = C_count + D_count
192+
if total > 0:
193+
if C_count > 0:
194+
counter[(state, 'C')] = C_count / (C_count + D_count)
195+
if D_count > 0:
196+
counter[(state, 'D')] = D_count / (C_count + D_count)
197+
normalized_distribution.append(Counter(counter))
198+
return normalized_distribution
199+
200+
201+
202+
138203
def sparkline(actions, c_symbol='█', d_symbol=' '):
139204
return ''.join([
140205
c_symbol if play == 'C' else d_symbol for play in actions])

axelrod/result_set.py

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,33 @@ def _build_normalised_state_distribution(self):
358358
norm.append(counters)
359359
return norm
360360

361+
@update_progress_bar
362+
def _build_normalised_state_to_action_distribution(self):
363+
"""
364+
Returns
365+
----------
366+
367+
Normalised state distribution. A list of lists of counter objects:
368+
369+
Dictionary where the keys are the states and the values are a
370+
normalized counts of the number of times that state goes to a given
371+
action.
372+
"""
373+
norm = []
374+
for player in self.state_to_action_distribution:
375+
counters = []
376+
for counter in player:
377+
norm_counter = Counter()
378+
for state in [(C, C), (C, D), (D, C), (D, D)]:
379+
total = counter[(state, C)] + counter[(state, D)]
380+
if total > 0:
381+
for action in [C, D]:
382+
if counter[(state, action)] > 0:
383+
norm_counter[(state, action)] = counter[(state, action)] / total
384+
counters.append(norm_counter)
385+
norm.append(counters)
386+
return norm
387+
361388
def _build_empty_metrics(self, keep_interactions=False):
362389
"""
363390
Creates the various empty metrics ready to be updated as the data is
@@ -385,6 +412,8 @@ def _build_empty_metrics(self, keep_interactions=False):
385412
self.initial_cooperation_count = [0 for player in plist]
386413
self.state_distribution = [[Counter() for opponent in plist]
387414
for player in plist]
415+
self.state_to_action_distribution = [[Counter() for opponent in plist]
416+
for player in plist]
388417
self.good_partner_matrix = [[0 for opponent in plist]
389418
for player in plist]
390419

@@ -567,6 +596,26 @@ def _update_state_distribution(self, p1, p2, counter):
567596
counter[(C, D)], counter[(D, C)] = counter[(D, C)], counter[(C, D)]
568597
self.state_distribution[p2][p1] += counter
569598

599+
def _update_state_to_action_distribution(self, p1, p2, counter_list):
600+
"""
601+
During a read of the data, update the state_distribution attribute
602+
603+
Parameters
604+
----------
605+
606+
p1, p2 : int
607+
The indices of the first and second player
608+
counter_list : list of collections.Counter
609+
A list of counter objects for the states to action of a match
610+
"""
611+
counter = counter_list[0]
612+
self.state_to_action_distribution[p1][p2] += counter
613+
614+
counter = counter_list[1]
615+
for act in [C, D]:
616+
counter[((C, D), act)], counter[((D, C), act)] = counter[((D, C), act)], counter[((C, D), act)]
617+
self.state_to_action_distribution[p2][p1] += counter
618+
570619
def _update_good_partner_matrix(self, p1, p2, cooperations):
571620
"""
572621
During a read of the data, update the good partner matrix attribute
@@ -671,6 +720,7 @@ def _build_score_related_metrics(self, progress_bar=False,
671720
self._update_normalised_cooperation(p1, p2, interaction)
672721

673722
if p1 != p2: # Anything that ignores self interactions
723+
state_to_actions = iu.compute_state_to_action_distribution(interaction)
674724

675725
for player in [p1, p2]:
676726
self.total_interactions[player] += 1
@@ -685,16 +735,19 @@ def _build_score_related_metrics(self, progress_bar=False,
685735
self._update_initial_cooperation_count(p1, p2,
686736
initial_coops)
687737
self._update_state_distribution(p1, p2, state_counter)
738+
self._update_state_to_action_distribution(p1, p2,
739+
state_to_actions)
688740
self._update_good_partner_matrix(p1, p2, cooperations)
689741

690742
if progress_bar:
691-
self.progress_bar = tqdm.tqdm(total=12 + 2 * self.nplayers,
743+
self.progress_bar = tqdm.tqdm(total=13 + 2 * self.nplayers,
692744
desc="Finishing")
693745
self._summarise_normalised_scores()
694746
self._summarise_normalised_cooperation()
695747

696748
self.ranking = self._build_ranking()
697749
self.normalised_state_distribution = self._build_normalised_state_distribution()
750+
self.normalised_state_to_action_distribution = self._build_normalised_state_to_action_distribution()
698751
self.ranked_names = self._build_ranked_names()
699752
self.payoff_matrix = self._build_payoff_matrix()
700753
self.payoff_stddevs = self._build_payoff_stddevs()
@@ -772,7 +825,9 @@ def summarise(self):
772825
self.player = namedtuple("Player", ["Rank", "Name", "Median_score",
773826
"Cooperation_rating", "Wins",
774827
"Initial_C_rate", "CC_rate",
775-
"CD_rate", "DC_rate", "DD_rate"])
828+
"CD_rate", "DC_rate", "DD_rate",
829+
"CC_to_C_rate", "CD_to_C_rate",
830+
"DC_to_C_rate", "DD_to_C_rate"])
776831

777832
states = [(C, C), (C, D), (D, C), (D, D)]
778833
state_prob = []
@@ -787,13 +842,28 @@ def summarise(self):
787842
counts = [0 for c in counts]
788843
state_prob.append(counts)
789844

845+
state_to_C_prob = []
846+
for player in self.normalised_state_to_action_distribution:
847+
rates = []
848+
for state in states:
849+
counts = [counter[(state, 'C')] for counter in player
850+
if counter[(state, 'C')] > 0]
851+
852+
if len(counts) > 0:
853+
rate = mean(counts)
854+
else:
855+
rate = 0
856+
857+
rates.append(rate)
858+
state_to_C_prob.append(rates)
859+
790860
summary_measures = list(zip(self.players, median_scores,
791861
self.cooperating_rating, median_wins,
792862
self.initial_cooperation_rate))
793863

794864
summary_data = []
795865
for rank, i in enumerate(self.ranking):
796-
data = list(summary_measures[i]) + state_prob[i]
866+
data = list(summary_measures[i]) + state_prob[i] + state_to_C_prob[i]
797867
summary_data.append(self.player(rank, *data))
798868

799869
return summary_data
@@ -802,7 +872,8 @@ def write_summary(self, filename):
802872
"""
803873
Write a csv file containing summary data of the results of the form:
804874
805-
"Rank", "Name", "Median-score-per-turn", "Cooperation-rating", "Initial_C_Rate", "Wins", "CC-Rate", "CD-Rate", "DC-Rate", "DD-rate"
875+
"Rank", "Name", "Median-score-per-turn", "Cooperation-rating", "Initial_C_Rate", "Wins", "CC-Rate", "CD-Rate", "DC-Rate", "DD-rate","CC-to-C-Rate", "CD-to-C-Rate", "DC-to-C-Rate", "DD-to-C-rate"
876+
806877
807878
Parameters
808879
----------

axelrod/tests/unit/test_interaction_utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,27 @@ class TestMatch(unittest.TestCase):
2222
Counter({('D', 'C'): 2}),
2323
Counter({('C', 'C'): 1, ('C', 'D'): 1}),
2424
None]
25+
state_to_action_distribution = [[Counter({(('C', 'D'), 'D'): 1}),
26+
Counter({(('C', 'D'), 'C'): 1})],
27+
[Counter({(('D', 'C'), 'D'): 1}),
28+
Counter({(('D', 'C'), 'C'): 1})],
29+
[Counter({(('C', 'C'), 'C'): 1}),
30+
Counter({(('C', 'C'), 'D'): 1})],
31+
None]
32+
2533
normalised_state_distribution = [
2634
Counter({('C', 'D'): 0.5, ('D', 'C'): 0.5}),
2735
Counter({('D', 'C'): 1.0}),
2836
Counter({('C', 'C'): 0.5, ('C', 'D'): 0.5}),
2937
None]
38+
normalised_state_to_action_distribution = [[Counter({(('C', 'D'), 'D'): 1}),
39+
Counter({(('C', 'D'), 'C'): 1})],
40+
[Counter({(('D', 'C'), 'D'): 1}),
41+
Counter({(('D', 'C'), 'C'): 1})],
42+
[Counter({(('C', 'C'), 'C'): 1}),
43+
Counter({(('C', 'C'), 'D'): 1})],
44+
None]
45+
3046
sparklines = [ '█ \n █', ' \n██', '██\n█ ', None ]
3147

3248
def test_compute_scores(self):
@@ -63,6 +79,37 @@ def test_compute_normalised_state_distribution(self):
6379
for inter, dist in zip(self.interactions, self.normalised_state_distribution):
6480
self.assertEqual(dist, iu.compute_normalised_state_distribution(inter))
6581

82+
def test_compute_state_to_action_distribution(self):
83+
for inter, dist in zip(self.interactions,
84+
self.state_to_action_distribution):
85+
self.assertEqual(dist,
86+
iu.compute_state_to_action_distribution(inter))
87+
inter = [(C, D), (D, C), (C, D), (D, C), (D, D), (C, C), (C, D)]
88+
expected_dist =[Counter({(('C', 'C'), 'C'): 1, (('D', 'C'), 'C'): 1,
89+
(('C', 'D'), 'D'): 2, (('D', 'C'), 'D'): 1,
90+
(('D', 'D'), 'C'): 1}),
91+
Counter({(('C', 'C'), 'D'): 1,
92+
(('C', 'D'), 'C'): 2, (('D', 'C'), 'D'): 2,
93+
(('D', 'D'), 'C'): 1})]
94+
95+
self.assertEqual(expected_dist,
96+
iu.compute_state_to_action_distribution(inter))
97+
98+
def test_compute_normalised_state_to_action_distribution(self):
99+
for inter, dist in zip(self.interactions,
100+
self.normalised_state_to_action_distribution):
101+
self.assertEqual(dist,
102+
iu.compute_normalised_state_to_action_distribution(inter))
103+
inter = [(C, D), (D, C), (C, D), (D, C), (D, D), (C, C), (C, D)]
104+
expected_dist =[Counter({(('C', 'C'), 'C'): 1, (('D', 'C'), 'C'): 1 / 2,
105+
(('C', 'D'), 'D'): 1, (('D', 'C'), 'D'): 1 / 2,
106+
(('D', 'D'), 'C'): 1}),
107+
Counter({(('C', 'C'), 'D'): 1,
108+
(('C', 'D'), 'C'): 1, (('D', 'C'), 'D'): 1,
109+
(('D', 'D'), 'C'): 1})]
110+
self.assertEqual(expected_dist,
111+
iu.compute_normalised_state_to_action_distribution(inter))
112+
66113
def test_compute_sparklines(self):
67114
for inter, spark in zip(self.interactions, self.sparklines):
68115
self.assertEqual(spark, iu.compute_sparklines(inter))

0 commit comments

Comments
 (0)