Skip to content
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

Add state to action analysis. #870

Merged
merged 4 commits into from
Feb 23, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions axelrod/interaction_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,94 @@ def compute_normalised_state_distribution(interactions):
return normalized_count


def compute_state_to_action_distribution(interactions):
"""
Returns a list (for each player) of counts of each state to action pair
for a set of interactions. A state to action pair is of the form:

((C, D), C)

Implying that from a state of (C, D) (the first player having played C and
the second playing D) the player in question then played C.

The following counter object, implies that the player in question was in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantic: "The following Counter object implies that"

state (C, D) for a total of 12 times, subsequently cooperating 4 times and
defecting 8 times.

Counter({((C, D), C): 4, ((C, D), D): 8})

Parameters
----------
interactions : list of tuples
A list containing the interactions of the match as shown at the top of
this file.

Returns
----------
state_to_C_distributions : List of Counter Object
List of Counter objects where the keys are the states and actions and
the values the counts. The
first/second Counter corresponds to the first/second player.
"""
if not interactions:
return None

distributions = [Counter([(state, outcome[j]) for state, outcome in zip(interactions,
interactions[1:])])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the spacing here more aesthetically pleasing?

for j in range(2)]
return distributions


def compute_normalised_state_to_action_distribution(interactions):
"""
Returns a list (for each player) of normalised counts of each state to action
pair for a set of interactions. A state to action pair is of the form:

((C, D), C)

Implying that from a state of (C, D) (the first player having played C and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantic: lowercase "impying"

the second playing D) the player in question then played C.

The following counter object, implies that the player in question was only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

ever in
state (C, D), subsequently cooperating 1/3 of the time and
defecting 2/3 times.

Counter({((C, D), C): 0.333333, ((C, D), D): 0.66666667})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. A little more explanation of what this is doing might be in order.

Parameters
----------
interactions : list of tuples
A list containing the interactions of the match as shown at the top of
this file.

Returns
----------
normalised_state_to_C_distributions : List of Counter Object
List of Counter objects where the keys are the states and actions and
the values the normalized counts.. The
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double period

first/second Counter corresponds to the first/second player.
"""
if not interactions:
return None

distribution = compute_state_to_action_distribution(interactions)
normalized_distribution = []
for player in range(2):
counter = {}
for state in [('C', 'C'), ('C', 'D'), ('D', 'C'), ('D', 'D')]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C, D as in Actions.C, Actions.D rather than 'C' and 'D'

C_count = distribution[player].get((state, 'C'), 0)
D_count = distribution[player].get((state, 'D'), 0)
total = C_count + D_count
if total > 0:
if C_count > 0:
counter[(state, 'C')] = C_count / (C_count + D_count)
if D_count > 0:
counter[(state, 'D')] = D_count / (C_count + D_count)
normalized_distribution.append(Counter(counter))
return normalized_distribution


def sparkline(actions, c_symbol='█', d_symbol=' '):
return ''.join([
c_symbol if play == 'C' else d_symbol for play in actions])
Expand Down
79 changes: 75 additions & 4 deletions axelrod/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,33 @@ def _build_normalised_state_distribution(self):
norm.append(counters)
return norm

@update_progress_bar
def _build_normalised_state_to_action_distribution(self):
"""
Returns
----------

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return var name / spacing

Normalised state distribution. A list of lists of counter objects:

Dictionary where the keys are the states and the values are a
normalized counts of the number of times that state goes to a given
action.
"""
norm = []
for player in self.state_to_action_distribution:
counters = []
for counter in player:
norm_counter = Counter()
for state in [(C, C), (C, D), (D, C), (D, D)]:
total = counter[(state, C)] + counter[(state, D)]
if total > 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if total == 0 then we add nothing to the list? just checking that is the intention rather than None or an empty dict.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If total == 0 we still add the empty norm_counter so the behaviour is consistant :)

The test cases catch this occurrence :)

for action in [C, D]:
if counter[(state, action)] > 0:
norm_counter[(state, action)] = counter[(state, action)] / total
counters.append(norm_counter)
norm.append(counters)
return norm

def _build_empty_metrics(self, keep_interactions=False):
"""
Creates the various empty metrics ready to be updated as the data is
Expand Down Expand Up @@ -385,6 +412,8 @@ def _build_empty_metrics(self, keep_interactions=False):
self.initial_cooperation_count = [0 for player in plist]
self.state_distribution = [[Counter() for opponent in plist]
for player in plist]
self.state_to_action_distribution = [[Counter() for opponent in plist]
for player in plist]
self.good_partner_matrix = [[0 for opponent in plist]
for player in plist]

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

def _update_state_to_action_distribution(self, p1, p2, counter_list):
"""
During a read of the data, update the state_distribution attribute

Parameters
----------

p1, p2 : int
The indices of the first and second player
counter_list : list of collections.Counter
A list of counter objects for the states to action of a match
"""
counter = counter_list[0]
self.state_to_action_distribution[p1][p2] += counter

counter = counter_list[1]
for act in [C, D]:
counter[((C, D), act)], counter[((D, C), act)] = counter[((D, C), act)], counter[((C, D), act)]
self.state_to_action_distribution[p2][p1] += counter

def _update_good_partner_matrix(self, p1, p2, cooperations):
"""
During a read of the data, update the good partner matrix attribute
Expand Down Expand Up @@ -671,6 +720,7 @@ def _build_score_related_metrics(self, progress_bar=False,
self._update_normalised_cooperation(p1, p2, interaction)

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

for player in [p1, p2]:
self.total_interactions[player] += 1
Expand All @@ -685,16 +735,19 @@ def _build_score_related_metrics(self, progress_bar=False,
self._update_initial_cooperation_count(p1, p2,
initial_coops)
self._update_state_distribution(p1, p2, state_counter)
self._update_state_to_action_distribution(p1, p2,
state_to_actions)
self._update_good_partner_matrix(p1, p2, cooperations)

if progress_bar:
self.progress_bar = tqdm.tqdm(total=12 + 2 * self.nplayers,
self.progress_bar = tqdm.tqdm(total=13 + 2 * self.nplayers,
desc="Finishing")
self._summarise_normalised_scores()
self._summarise_normalised_cooperation()

self.ranking = self._build_ranking()
self.normalised_state_distribution = self._build_normalised_state_distribution()
self.normalised_state_to_action_distribution = self._build_normalised_state_to_action_distribution()
self.ranked_names = self._build_ranked_names()
self.payoff_matrix = self._build_payoff_matrix()
self.payoff_stddevs = self._build_payoff_stddevs()
Expand Down Expand Up @@ -772,7 +825,9 @@ def summarise(self):
self.player = namedtuple("Player", ["Rank", "Name", "Median_score",
"Cooperation_rating", "Wins",
"Initial_C_rate", "CC_rate",
"CD_rate", "DC_rate", "DD_rate"])
"CD_rate", "DC_rate", "DD_rate",
"CC_to_C_rate", "CD_to_C_rate",
"DC_to_C_rate", "DD_to_C_rate"])

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

state_to_C_prob = []
for player in self.normalised_state_to_action_distribution:
rates = []
for state in states:
counts = [counter[(state, 'C')] for counter in player
if counter[(state, 'C')] > 0]

if len(counts) > 0:
rate = mean(counts)
else:
rate = 0

rates.append(rate)
state_to_C_prob.append(rates)

summary_measures = list(zip(self.players, median_scores,
self.cooperating_rating, median_wins,
self.initial_cooperation_rate))

summary_data = []
for rank, i in enumerate(self.ranking):
data = list(summary_measures[i]) + state_prob[i]
data = list(summary_measures[i]) + state_prob[i] + state_to_C_prob[i]
summary_data.append(self.player(rank, *data))

return summary_data
Expand All @@ -802,7 +872,8 @@ def write_summary(self, filename):
"""
Write a csv file containing summary data of the results of the form:

"Rank", "Name", "Median-score-per-turn", "Cooperation-rating", "Initial_C_Rate", "Wins", "CC-Rate", "CD-Rate", "DC-Rate", "DD-rate"
"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"


Parameters
----------
Expand Down
47 changes: 47 additions & 0 deletions axelrod/tests/unit/test_interaction_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,27 @@ class TestMatch(unittest.TestCase):
Counter({('D', 'C'): 2}),
Counter({('C', 'C'): 1, ('C', 'D'): 1}),
None]
state_to_action_distribution = [[Counter({(('C', 'D'), 'D'): 1}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use C and D instead of 'C' and 'D'?

Counter({(('C', 'D'), 'C'): 1})],
[Counter({(('D', 'C'), 'D'): 1}),
Counter({(('D', 'C'), 'C'): 1})],
[Counter({(('C', 'C'), 'C'): 1}),
Counter({(('C', 'C'), 'D'): 1})],
None]

normalised_state_distribution = [
Counter({('C', 'D'): 0.5, ('D', 'C'): 0.5}),
Counter({('D', 'C'): 1.0}),
Counter({('C', 'C'): 0.5, ('C', 'D'): 0.5}),
None]
normalised_state_to_action_distribution = [[Counter({(('C', 'D'), 'D'): 1}),
Counter({(('C', 'D'), 'C'): 1})],
[Counter({(('D', 'C'), 'D'): 1}),
Counter({(('D', 'C'), 'C'): 1})],
[Counter({(('C', 'C'), 'C'): 1}),
Counter({(('C', 'C'), 'D'): 1})],
None]

sparklines = [ '█ \n █', ' \n██', '██\n█ ', None ]

def test_compute_scores(self):
Expand Down Expand Up @@ -63,6 +79,37 @@ def test_compute_normalised_state_distribution(self):
for inter, dist in zip(self.interactions, self.normalised_state_distribution):
self.assertEqual(dist, iu.compute_normalised_state_distribution(inter))

def test_compute_state_to_action_distribution(self):
for inter, dist in zip(self.interactions,
self.state_to_action_distribution):
self.assertEqual(dist,
iu.compute_state_to_action_distribution(inter))
inter = [(C, D), (D, C), (C, D), (D, C), (D, D), (C, C), (C, D)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mixing C and 'C'

expected_dist =[Counter({(('C', 'C'), 'C'): 1, (('D', 'C'), 'C'): 1,
(('C', 'D'), 'D'): 2, (('D', 'C'), 'D'): 1,
(('D', 'D'), 'C'): 1}),
Counter({(('C', 'C'), 'D'): 1,
(('C', 'D'), 'C'): 2, (('D', 'C'), 'D'): 2,
(('D', 'D'), 'C'): 1})]

self.assertEqual(expected_dist,
iu.compute_state_to_action_distribution(inter))

def test_compute_normalised_state_to_action_distribution(self):
for inter, dist in zip(self.interactions,
self.normalised_state_to_action_distribution):
self.assertEqual(dist,
iu.compute_normalised_state_to_action_distribution(inter))
inter = [(C, D), (D, C), (C, D), (D, C), (D, D), (C, C), (C, D)]
expected_dist =[Counter({(('C', 'C'), 'C'): 1, (('D', 'C'), 'C'): 1 / 2,
(('C', 'D'), 'D'): 1, (('D', 'C'), 'D'): 1 / 2,
(('D', 'D'), 'C'): 1}),
Counter({(('C', 'C'), 'D'): 1,
(('C', 'D'), 'C'): 1, (('D', 'C'), 'D'): 1,
(('D', 'D'), 'C'): 1})]
self.assertEqual(expected_dist,
iu.compute_normalised_state_to_action_distribution(inter))

def test_compute_sparklines(self):
for inter, spark in zip(self.interactions, self.sparklines):
self.assertEqual(spark, iu.compute_sparklines(inter))
Expand Down
Loading