Skip to content

Commit

Permalink
Calculate & collect rolling percent hawk; detect convergence & stop #21
Browse files Browse the repository at this point in the history
… (#31)

* Calculate & collect rolling percent hawk; detect convergence & stop #21

* Try to avoid empty median errors in q1/q3 tests
  • Loading branch information
rlskoeser authored Oct 10, 2023
1 parent f747758 commit 7214426
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 25 deletions.
42 changes: 25 additions & 17 deletions simulatingrisk/hawkdove/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,46 @@ def plot_hawks(model):

model_df = model.datacollector.get_model_vars_dataframe().reset_index()

# calculate a rolling average for % hawk
model_df["rollingavg_percent_hawk"] = model_df.percent_hawk.rolling(10).mean()

# limit to last N rounds (how many ?)
last_n_rounds = model_df.tail(50)
# determine domain of the chart;
# starting domain 0-50 so it doesn't jump / expand as much
max_index = max(model_df.last_valid_index() or 0, 50)
min_index = max(max_index - 50, 0)

bar_chart = (
alt.Chart(last_n_rounds)
.mark_bar(color="orange")
.encode(
x=alt.X("index", title="Step"),
x=alt.X(
"index", title="Step", scale=alt.Scale(domain=[min_index, max_index])
),
y=alt.Y(
"percent_hawk",
title="Percent who chose hawk",
scale=alt.Scale(domain=[0, 1]),
),
)
)
# graph rolling average as a line over the bar chart
line = (
alt.Chart(last_n_rounds)
.mark_line(color="blue")
.encode(
x=alt.X("index", title="Step"),
y=alt.Y(
"rollingavg_percent_hawk",
title="% hawk (rolling average)",
scale=alt.Scale(domain=[0, 1]),
),
# graph rolling average as a line over the bar chart,
# once we have enough rounds
if model_df.rolling_percent_hawk.any():
line = (
alt.Chart(last_n_rounds)
.mark_line(color="blue")
.encode(
x=alt.X("index", title="Step"),
y=alt.Y(
"rolling_percent_hawk",
title="% hawk (rolling average)",
scale=alt.Scale(domain=[0, 1]),
),
)
)
)
# add the rolling average line on top of the bar chart
bar_chart += line

return solara.FigureAltair(bar_chart + line)
return solara.FigureAltair(bar_chart)


page = JupyterViz(
Expand Down
44 changes: 43 additions & 1 deletion simulatingrisk/hawkdove/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from enum import Enum
from collections import deque
import math
import statistics

import mesa

Expand Down Expand Up @@ -134,7 +136,12 @@ def points_rank(self):
class HawkDoveModel(mesa.Model):
""" """

#: whether the simulation is running
running = True # required for batch run
#: size of deque/fifo for recent values
rolling_window = 30
#: minimum size before calculating rolling average
min_window = 15

def __init__(
self,
Expand All @@ -154,6 +161,10 @@ def __init__(
# distribution of first choice (50/50 by default)
self.hawk_odds = hawk_odds

# create fifos to track recent behavior to detect convergence
self.recent_percent_hawk = deque([], maxlen=self.rolling_window)
self.recent_rolling_percent_hawk = deque([], maxlen=self.rolling_window)

# initialize a single grid (each square inhabited by a single agent);
# configure the grid to wrap around so everyone has neighbors
self.grid = mesa.space.SingleGrid(grid_size, grid_size, True)
Expand All @@ -175,6 +186,7 @@ def __init__(
model_reporters={
"max_agent_points": "max_agent_points",
"percent_hawk": "percent_hawk",
"rolling_percent_hawk": "rolling_percent_hawk",
},
agent_reporters={
"risk_level": "risk_level",
Expand All @@ -189,6 +201,12 @@ def step(self):
"""
self.schedule.step()
self.datacollector.collect(self)
if self.converged:
self.running = False
print(
f"Stopping after {self.schedule.steps} rounds. "
+ f"Final rolling average % hawk: {self.rolling_percent_hawk}"
)

@property
def max_agent_points(self):
Expand All @@ -199,4 +217,28 @@ def max_agent_points(self):
def percent_hawk(self):
# what percent of agents chose hawk?
hawks = [a for a in self.schedule.agents if a.choice == Play.HAWK]
return len(hawks) / self.num_agents
phawk = len(hawks) / self.num_agents
# add to recent values
self.recent_percent_hawk.append(phawk)
return phawk

@property
def rolling_percent_hawk(self):
# make sure we have enough values to check
if len(self.recent_percent_hawk) > self.min_window:
rolling_phawk = statistics.mean(self.recent_percent_hawk)
# add to recent values
self.recent_rolling_percent_hawk.append(rolling_phawk)
return rolling_phawk

@property
def converged(self):
# check if the simulation is stable and should stop running
# calculating based on rolling percent hawk; when this is stable
# within our rolling window, return true
# - currently checking for single value;
# could allow for a small amount variation if necessary
return (
len(self.recent_rolling_percent_hawk) > self.min_window
and len(set(self.recent_rolling_percent_hawk)) == 1
)
15 changes: 8 additions & 7 deletions simulatingrisk/risky_bet/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,16 @@ def risk_max(self):

@property
def risk_q1(self):
if self.agent_risk_levels:
risk_median = self.risk_median
# first quartile is the median of values less than the median
return statistics.median(
[r for r in self.agent_risk_levels if r < risk_median]
)
risk_median = self.risk_median
# first quartile is the median of values less than the median
submedian_values = [r for r in self.agent_risk_levels if r < risk_median]
if submedian_values:
return statistics.median(submedian_values)

@property
def risk_q3(self):
risk_median = self.risk_median
# third quartile is the median of values greater than the median
return statistics.median([r for r in self.agent_risk_levels if r > risk_median])
supermedian_values = [r for r in self.agent_risk_levels if r > risk_median]
if supermedian_values:
return statistics.median(supermedian_values)

0 comments on commit 7214426

Please sign in to comment.