simulatingrisk 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. simulatingrisk/__init__.py +1 -0
  2. simulatingrisk/about_app.md +2 -0
  3. simulatingrisk/app.py +50 -0
  4. simulatingrisk/batch_run.py +145 -0
  5. simulatingrisk/charts/histogram.js +51 -0
  6. simulatingrisk/charts/histogram.py +72 -0
  7. simulatingrisk/hawkdove/README.md +106 -0
  8. simulatingrisk/hawkdove/app.py +90 -0
  9. simulatingrisk/hawkdove/model.py +391 -0
  10. simulatingrisk/hawkdove/run.py +31 -0
  11. simulatingrisk/hawkdove/server.py +189 -0
  12. simulatingrisk/hawkdovemulti/README.md +92 -0
  13. simulatingrisk/hawkdovemulti/analysis_utils.py +83 -0
  14. simulatingrisk/hawkdovemulti/app.py +242 -0
  15. simulatingrisk/hawkdovemulti/batch_run.py +328 -0
  16. simulatingrisk/hawkdovemulti/model.py +462 -0
  17. simulatingrisk/hawkdovemulti/run_simulation.ipynb +55 -0
  18. simulatingrisk/hawkdovemulti/simrisk_batch.slurm +44 -0
  19. simulatingrisk/risky_bet/README.md +45 -0
  20. simulatingrisk/risky_bet/app.py +17 -0
  21. simulatingrisk/risky_bet/model.py +237 -0
  22. simulatingrisk/risky_bet/run.py +46 -0
  23. simulatingrisk/risky_bet/server.py +102 -0
  24. simulatingrisk/risky_food/README.md +32 -0
  25. simulatingrisk/risky_food/__init__.py +0 -0
  26. simulatingrisk/risky_food/app.py +19 -0
  27. simulatingrisk/risky_food/model.py +250 -0
  28. simulatingrisk/risky_food/run.py +20 -0
  29. simulatingrisk/risky_food/server.py +78 -0
  30. simulatingrisk/stag_hunt/README.md +15 -0
  31. simulatingrisk/stag_hunt/__init__.py +0 -0
  32. simulatingrisk/stag_hunt/model.py +123 -0
  33. simulatingrisk/stag_hunt/run.py +45 -0
  34. simulatingrisk/utils.py +31 -0
  35. simulatingrisk-1.0.0.dist-info/METADATA +113 -0
  36. simulatingrisk-1.0.0.dist-info/RECORD +40 -0
  37. simulatingrisk-1.0.0.dist-info/WHEEL +5 -0
  38. simulatingrisk-1.0.0.dist-info/entry_points.txt +2 -0
  39. simulatingrisk-1.0.0.dist-info/licenses/LICENSE +201 -0
  40. simulatingrisk-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,2 @@
1
+
2
+ These simulations are associated with the CDH project [Simulating risk, risking simulations](https://cdh.princeton.edu/projects/simulating-risk/).
simulatingrisk/app.py ADDED
@@ -0,0 +1,50 @@
1
+ import os.path
2
+
3
+ import solara
4
+
5
+ from simulatingrisk.hawkdove.app import page as hawkdove_page
6
+ from simulatingrisk.hawkdovemulti.app import page as hawkdove_multi_page
7
+ from simulatingrisk.risky_bet.app import page as riskybet_page
8
+ from simulatingrisk.risky_food.app import page as riskyfood_page
9
+
10
+
11
+ @solara.component
12
+ def Home():
13
+ # load about markdown file in the same directory
14
+ with open(os.path.join(os.path.dirname(__file__), "about_app.md")) as readmefile:
15
+ return solara.Markdown("\n".join(readmefile.readlines()))
16
+
17
+
18
+ @solara.component
19
+ def hawkdove():
20
+ return hawkdove_page
21
+
22
+
23
+ @solara.component
24
+ def hawkdove_multi():
25
+ return hawkdove_multi_page
26
+
27
+
28
+ @solara.component
29
+ def riskybet():
30
+ return riskybet_page
31
+
32
+
33
+ @solara.component
34
+ def riskyfood():
35
+ return riskyfood_page
36
+
37
+
38
+ routes = [
39
+ solara.Route(path="/", component=Home, label="Simulating Risk"),
40
+ solara.Route(
41
+ path="hawkdove-single", component=hawkdove, label="Hawk/Dove (single r)"
42
+ ),
43
+ solara.Route(
44
+ path="hawkdove-multiple",
45
+ component=hawkdove_multi,
46
+ label="Hawk/Dove (multiple r)",
47
+ ),
48
+ solara.Route(path="riskybet", component=riskybet, label="Risky Bet"),
49
+ solara.Route(path="riskyfood", component=riskyfood, label="Risky Food"),
50
+ ]
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python
2
+
3
+ import argparse
4
+ import csv
5
+ from datetime import datetime
6
+
7
+ from mesa import batch_run
8
+
9
+ from simulatingrisk.hawkdove.model import HawkDoveSingleRiskModel
10
+ from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel
11
+ from simulatingrisk.risky_bet.model import RiskyBetModel
12
+ from simulatingrisk.risky_food.model import RiskyFoodModel
13
+
14
+
15
+ def riskybet_batch_run(args=None):
16
+ results = batch_run(
17
+ RiskyBetModel,
18
+ parameters={
19
+ "grid_size": 30, # [20, 30], # 100],
20
+ # "risk_adjustment": ["adopt", "average"],
21
+ "risk_adjustment": "adopt",
22
+ },
23
+ iterations=5,
24
+ # TODO: vary how often they update strategy
25
+ # every 100, every 1 round?
26
+ max_steps=3000, # at least 1000, maybe more to see where it converges
27
+ # try 10k to see
28
+ # add logic on the model to stop if risk levels converge to 90% in one bin
29
+ number_processes=1, # set None to use all available; set 1 for jupyter
30
+ data_collection_period=1,
31
+ display_progress=True,
32
+ )
33
+ # returns a list of dictionaries from data collection across all runs
34
+ save_results("riskybet", results)
35
+
36
+
37
+ def riskyfood_batch_run(args=None):
38
+ results = batch_run(
39
+ RiskyFoodModel,
40
+ # only parameter to this one currently is number of agents
41
+ parameters={"n": 110, "mode": "types"},
42
+ iterations=5, # this one is faster, could run more iterations
43
+ max_steps=1000,
44
+ number_processes=1, # set None to use all available; set 1 for jupyter
45
+ data_collection_period=1,
46
+ display_progress=True,
47
+ )
48
+ save_results("riskyfood", results)
49
+
50
+
51
+ def hawkdove_singlerisk_batch_run(args):
52
+ # params are:
53
+ # grid_size,
54
+ # include_diagonals=True,
55
+ # agent_risk_level=None,
56
+ # hawk_odds=0.5,
57
+
58
+ params = {
59
+ "grid_size": 20,
60
+ "agent_risk_level": [0, 1, 2, 3, 4, 5, 6, 7, 8],
61
+ }
62
+ iterations = 1
63
+
64
+ results = batch_run(
65
+ HawkDoveSingleRiskModel,
66
+ # when including diagonals, risk levels go from 0 to 8;
67
+ # probably do not need to include the extremes for this analysis
68
+ parameters=params,
69
+ iterations=iterations,
70
+ number_processes=1,
71
+ data_collection_period=1,
72
+ display_progress=True,
73
+ max_steps=200, # converges very quickly, so don't run 1000 times
74
+ )
75
+ # include the mode in the output filename
76
+ save_results("hawkdove_single", results)
77
+
78
+
79
+ def hawkdove_multiplerisk_batch_run(args):
80
+ params = {
81
+ "grid_size": 10,
82
+ "risk_adjustment": "adopt", # run adopt only for now
83
+ }
84
+ iterations = 100
85
+ results = batch_run(
86
+ HawkDoveMultipleRiskModel,
87
+ parameters=params,
88
+ iterations=iterations,
89
+ number_processes=1,
90
+ data_collection_period=1,
91
+ display_progress=True,
92
+ max_steps=250, # converges fairly quickly, don't run 1000 times
93
+ )
94
+ # include the mode in the output filename
95
+ save_results("hawkdove_multiple", results)
96
+
97
+
98
+ def save_results(simulation, results):
99
+ # save as csv for external analysis
100
+ # - use datetime to distinguish this run, but make nicer for filename
101
+ datestr = datetime.today().isoformat().replace(".", "_").replace(":", "")
102
+ output_filename = "%s_%s.csv" % (simulation, datestr)
103
+ print("Saving data collection results to: %s" % output_filename)
104
+ # get field names from last entry, since first entry is for the model
105
+ # and doesn't include agent-level data
106
+ fields = results[-1].keys()
107
+ with open(output_filename, "w", newline="") as output_file:
108
+ dict_writer = csv.DictWriter(output_file, fields)
109
+ dict_writer.writeheader()
110
+ dict_writer.writerows(results)
111
+
112
+ return output_filename
113
+
114
+
115
+ if __name__ == "__main__":
116
+ parser = argparse.ArgumentParser(
117
+ prog="simulatingrisk batch_run",
118
+ description="Run simulations in batch mode and save collected data",
119
+ )
120
+ # use subcommands so we can add model-specific options
121
+ subparsers = parser.add_subparsers(help="Help for model-specific options")
122
+ riskybet_parser = subparsers.add_parser("riskybet")
123
+ riskybet_parser.set_defaults(func=riskybet_batch_run)
124
+ riskyfood_parser = subparsers.add_parser("riskyfood")
125
+ riskyfood_parser.set_defaults(func=riskyfood_batch_run)
126
+ hawkdove_parser = subparsers.add_parser("hawkdove-single")
127
+ # will any subparser arguments be needed in future?
128
+ # hawkdove_parser.add_argument(
129
+ # "-r",
130
+ # "--risk-attitudes",
131
+ # choices=["single", "variable"],
132
+ # help="Mode for initializing agent risk attitudes",
133
+ # )
134
+ hawkdove_parser.set_defaults(func=hawkdove_singlerisk_batch_run)
135
+ hawkdove_multi_parser = subparsers.add_parser("hawkdove-multi")
136
+ hawkdove_multi_parser.set_defaults(func=hawkdove_multiplerisk_batch_run)
137
+
138
+ args = parser.parse_args()
139
+ # run appropriate function based on the selected subcommand
140
+ # if a subcommand is not specified, no function is set
141
+ if hasattr(args, "func"):
142
+ args.func(args)
143
+ else:
144
+ parser.print_help()
145
+ exit(-1)
@@ -0,0 +1,51 @@
1
+ const HistogramModule = function(bins, canvas_width, canvas_height, label) {
2
+ // Create the canvas object:
3
+ const canvas = document.createElement("canvas");
4
+ Object.assign(canvas, {
5
+ width: canvas_width,
6
+ height: canvas_height,
7
+ style: "border:1px dotted",
8
+ });
9
+ // Append it to #elements:
10
+ const elements = document.getElementById("elements");
11
+ elements.appendChild(canvas);
12
+
13
+ // Create the context and the drawing controller:
14
+ const context = canvas.getContext("2d");
15
+
16
+ // Prep the chart properties and series:
17
+ const datasets = [{
18
+ label: label,
19
+ fillColor: "rgba(151,187,205,0.5)",
20
+ strokeColor: "rgba(151,187,205,0.8)",
21
+ highlightFill: "rgba(151,187,205,0.75)",
22
+ highlightStroke: "rgba(151,187,205,1)",
23
+ data: []
24
+ }];
25
+
26
+ // Add a zero value for each bin
27
+ for (var i in bins)
28
+ datasets[0].data.push(0);
29
+
30
+ const data = {
31
+ labels: bins,
32
+ datasets: datasets
33
+ };
34
+
35
+ const options = {
36
+ scaleBeginsAtZero: true
37
+ };
38
+
39
+ // Create the chart object
40
+ let chart = new Chart(context, {type: 'bar', data: data, options: options});
41
+
42
+ this.render = function(data) {
43
+ datasets[0].data = data;
44
+ chart.update();
45
+ };
46
+
47
+ this.reset = function() {
48
+ chart.destroy();
49
+ chart = new Chart(context, {type: 'bar', data: data, options: options});
50
+ };
51
+ };
@@ -0,0 +1,72 @@
1
+ import os
2
+
3
+ import numpy as np
4
+ import solara
5
+ from matplotlib.figure import Figure
6
+
7
+
8
+ from mesa.visualization.ModularVisualization import VisualizationElement, CHART_JS_FILE
9
+
10
+
11
+ # histogram chart from mesa tutorial
12
+ # https://mesa.readthedocs.io/en/stable/tutorials/adv_tutorial_legacy.html
13
+ class HistogramModule(VisualizationElement):
14
+ """histogram plot of agent risk levels; for use with mesa runserver"""
15
+
16
+ package_includes = [CHART_JS_FILE]
17
+ local_includes = ["histogram.js"]
18
+ # javascript is located in the same file as this python file
19
+ local_dir = os.path.dirname(os.path.realpath(__file__))
20
+
21
+ def __init__(self, bins, canvas_height, canvas_width, label, agent_attr):
22
+ self.canvas_height = canvas_height
23
+ self.canvas_width = canvas_width
24
+ self.bins = bins
25
+ self.agent_attr = agent_attr
26
+ new_element = "new HistogramModule({}, {}, {}, {})"
27
+ new_element = new_element.format(
28
+ bins, canvas_width, canvas_height, '"%s"' % label
29
+ )
30
+ self.js_code = "elements.push(" + new_element + ");"
31
+
32
+ def render(self, model):
33
+ agent_values = [
34
+ getattr(agent, self.agent_attr) for agent in model.schedule.agents
35
+ ]
36
+ # generate a histogram of risk levels based on the specified bins
37
+ hist = np.histogram(agent_values, bins=self.bins)[0]
38
+ return [int(x) for x in hist]
39
+
40
+
41
+ class RiskHistogramModule(HistogramModule):
42
+ """special case: risk_level histogram"""
43
+
44
+ def __init__(self, bins, canvas_height, canvas_width, label="risk levels"):
45
+ super().__init__(
46
+ bins, canvas_height, canvas_width, label, agent_attr="risk_level"
47
+ )
48
+
49
+
50
+ # generate bins for histogram, capturing 0-0.5 and 0.95-1.0
51
+ risk_bins = []
52
+ r = 0.05
53
+ while r < 1.05:
54
+ risk_bins.append(round(r, 2))
55
+ r += 0.1
56
+
57
+
58
+ def plot_risk_histogram(model):
59
+ """histogram plot of agent risk levels; for use with jupyterviz/solara"""
60
+
61
+ # adapted from mesa visualiation tutorial
62
+ # https://mesa.readthedocs.io/en/stable/tutorials/visualization_tutorial.html#Building-your-own-visualization-component
63
+
64
+ # Note: per Mesa docs, has to be initialized using this method instead of
65
+ # plt.figure(), for thread safety purpose
66
+ fig = Figure()
67
+ ax = fig.subplots()
68
+ # generate a histogram of current risk levels
69
+ risk_levels = [agent.risk_level for agent in model.schedule.agents]
70
+ ax.hist(risk_levels, bins=risk_bins)
71
+ ax.set_title("risk levels")
72
+ solara.FigureMatplotlib(fig)
@@ -0,0 +1,106 @@
1
+ # Hawk-Dove with risk attitudes
2
+
3
+ Hawk/Dove game with risk attitudes
4
+
5
+ ## Game description
6
+
7
+ This is a variant of the Hawk/Dove Game: https://en.wikipedia.org/wiki/Chicken_(game)
8
+
9
+ | | H | D|
10
+ |-|-|-|
11
+ | H | 0, 0 | 3, 1|
12
+ | D |1, 3| 2, 2|
13
+
14
+ BACKGROUND: An unpublished paper by Simon Blessenohl shows that the equilibrium in this game is different for EU maximizers than for REU maximizers (all with the same risk-attitude), and that REU maximizers do better as a population (basically, play DOVE more often)
15
+
16
+ We want to know: what happens when different people have _different_ risk-attitudes.
17
+ (See also variant simulation [Hawk/Dove game with multiple risk attitudes](../hawkdovemulti/). )
18
+
19
+ GAME: Hawk-Dove with risk-attitudes
20
+
21
+ Players arranged on a lattice [options for both 4 neighbors (AYBD) and 8 neighbors (XYZABCDE)]
22
+
23
+ | | | |
24
+ |-|-|-|
25
+ | X | Y |Z |
26
+ |A | **I** | B |
27
+ | C | D | E |
28
+
29
+ - Payoffs are determined as follows:
30
+ - Look at what each neighbor did, then:
31
+ - If I play HAWK and neighbor plays DOVE: 3
32
+ - If I play DOVE and neighbor plays DOVE: 2
33
+ - If I play DOVE and neighbor plays HAWK: 1
34
+ - If I play HAWK and neighbor plays HAWK: 0
35
+
36
+ Each player on a lattice (grid in Mesa):
37
+ - Has parameter $r$ [from 0 to 9]
38
+ - Let `d` be the number of neighbors who played DOVE during the previous round. If $d >= r$, then play HAWK. Otherwise play DOVE. (Agents who are risk-avoidant only play HAWK if there are a lot of doves around them. More risk-avoidance requires a higher number of doves to get an agent to play HAWK.)
39
+ - The proportion of neighbors who play DOVE corresponds to your probability of encountering a DOVE when playing a randomly-selected neighbor. The intended interpretation is that you maximize REU for this probability of your opponent playing DOVE. Thus, $r$ corresponds to the probability above which playing HAWK maximizes REU.
40
+ - Choice of play for the first round:
41
+ - Who is a HAWK and who is a DOVE is randomly determined; proportion set at the beginning of each simulation. E.g. 30% are HAWKS; if we have 100 players, then each player has a 30% chance of being HAWK;
42
+ - This initial parameter is called HAWK-ODDS; default is 50/50
43
+
44
+
45
+ ## Payoffs and risk attitudes
46
+
47
+ This game has a discrete set of options instead of probability, so instead of defining `r` as a value between 0.0 and 1.0, we use discrete values based on the choices. For the game that includes diagonal neighbors when agents play all neighbors:
48
+
49
+ <table>
50
+ <tr><th>r</th></th><th>0</th><th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th></tr>
51
+ <tr>
52
+ <th>Plays H when:</th>
53
+ <td>always</td>
54
+ <td>$\geq1$ D</td>
55
+ <td>$\geq2$ D</td>
56
+ <td>$\geq3$ D</td>
57
+ <td>$\geq4$ D</td>
58
+ <td>$\geq5$ D</td>
59
+ <td>$\geq6$ D</td>
60
+ <td>$\geq7$ D</td>
61
+ <td>$\geq8$ D</td>
62
+ <td>never</td>
63
+ </tr>
64
+ <tr><td></td>
65
+ <td colspan="4">risk seeking</td>
66
+ <td>EU maximizer<br>(risk neutral)</td>
67
+ <td>EU maximizer<br>(risk neutral)</td>
68
+ <td colspan="4">risk avoidant</td>
69
+ </tr>
70
+ </table>
71
+
72
+
73
+ An REU maximizer will play HAWK when
74
+ ```math
75
+ r(p) > \frac{(D,H)-(H,H)}{(H,D)-(D,D)}
76
+ ```
77
+ In other words, when $r(p) > 0.52$. An EU maximizer, with $r(p) = p$, will play HAWK when $p > 0.52$, e.g., when more than 4 out of 8 neighbors play DOVE. Thus, $r = 4$ corresponds to risk-neutrality (EU maximization), $r < 4$ corresponds to risk-inclination, and $r > 4$ corresponds to risk-avoidance.
78
+
79
+ Payoffs were chosen to avoid the case in which two choices had equal expected utility for some number of neighbors. For example, if the payoff of $(D,D)$ was $(2,2)$, then at $p = 0.5$ (4 of 8 neighbors), then EU maximizers would be indifferent between HAWK and DOVE; in this case, no r-value would correspond to EU maximization, since $r = 4$ strictly prefers DOVE and $r = 3$ strictly prefers HAWK.
80
+
81
+ Another way to visualize the risk attitudes and choices in this game is this table, which shows when agents will play Hawk or Dove based on their risk attitudes (going down on the left side) and the number of neighbors playing Dove (across the top).
82
+
83
+ <table>
84
+ <tr><td colspan="2"></td><th colspan="9"># of neighors playing DOVE</thr></tr>
85
+ <tr><td><th>r</th><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td></tr>
86
+ <tr><td rowspan="4">risk seeking</td><th>0</th><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td></tr>
87
+ <tr><th>1</th><td>D</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td></tr>
88
+ <tr><th>2</th><td>D</td><td>D</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td></tr>
89
+ <tr><th>3</th><td>D</td><td>D</td><td>D</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td></tr>
90
+ <tr><td rowspan="2">neutral</td></td><th>4</th><td>D</td><td>D</td><td>D</td><td>D</td><td>H</td><td>H</td><td>H</td><td>H</td><td>H</td></tr>
91
+ <tr><th>5</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>H</td><td>H</td><td>H</td><td>H</td></tr>
92
+ <tr><td rowspan="4">risk avoidant</td><th>6</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>H</td><td>H</td><td>H</td></tr>
93
+ <tr><th>7</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>H</td><td>H</td></tr>
94
+ <tr><th>8</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>H</td></tr>
95
+ <tr><th>9</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td></tr>
96
+ </table>
97
+
98
+ ## Convergence
99
+
100
+ The model is configured to stop automatically when it has stabilized. Convergence is based on a stable rolling average of the percent of agents in the simulation playing hawk.
101
+
102
+ A rolling average of the percent of agents playing hawk is calculated every round based on the percent for the last **30** rounds. The rolling average is not calculated until after at least **15** rounds.
103
+
104
+ When we have collected the rolling average for at least **15** rounds and the last **30** rolling averages are the same when rounded to 2 percentage points, we consider the simulation converged.
105
+
106
+
@@ -0,0 +1,90 @@
1
+ # solara/jupyterviz app
2
+ from mesa.experimental import JupyterViz
3
+ import pandas as pd
4
+ import altair as alt
5
+ import solara
6
+
7
+
8
+ from simulatingrisk.hawkdove.model import HawkDoveSingleRiskModel
9
+ from simulatingrisk.hawkdove.server import (
10
+ agent_portrayal,
11
+ jupyterviz_params,
12
+ draw_hawkdove_agent_space,
13
+ )
14
+
15
+
16
+ def plot_wealth(model):
17
+ """histogram plot of agent wealth levels across risk levels;
18
+ for use with jupyterviz/solara"""
19
+
20
+ # generate a histogram of points across risk levels
21
+ risk_wealth = [(agent.risk_level, agent.points) for agent in model.schedule.agents]
22
+ df = pd.DataFrame(risk_wealth, columns=["risk_level", "wealth"])
23
+
24
+ chart = (
25
+ alt.Chart(df)
26
+ .mark_bar()
27
+ .encode(y="wealth", x=alt.X("risk_level", title="risk attitude"))
28
+ )
29
+ return solara.FigureAltair(chart)
30
+
31
+
32
+ def plot_hawks(model):
33
+ """plot percent of agents who chose hawk over last several rounds;
34
+ for use with jupyterviz/solara"""
35
+
36
+ model_df = model.datacollector.get_model_vars_dataframe().reset_index()
37
+
38
+ # limit to last N rounds (how many ?)
39
+ last_n_rounds = model_df.tail(50)
40
+ # determine domain of the chart;
41
+ # starting domain 0-50 so it doesn't jump / expand as much
42
+ max_index = max(model_df.last_valid_index() or 0, 50)
43
+ min_index = max(max_index - 50, 0)
44
+
45
+ bar_chart = (
46
+ alt.Chart(last_n_rounds)
47
+ .mark_bar(color="orange")
48
+ .encode(
49
+ x=alt.X(
50
+ "index", title="Step", scale=alt.Scale(domain=[min_index, max_index])
51
+ ),
52
+ y=alt.Y(
53
+ "percent_hawk",
54
+ title="Percent who chose hawk",
55
+ scale=alt.Scale(domain=[0, 1]),
56
+ ),
57
+ )
58
+ )
59
+ # graph rolling average as a line over the bar chart,
60
+ # once we have enough rounds
61
+ if model_df.rolling_percent_hawk.any():
62
+ line = (
63
+ alt.Chart(last_n_rounds)
64
+ .mark_line(color="blue")
65
+ .encode(
66
+ x=alt.X("index", title="Step"),
67
+ y=alt.Y(
68
+ "rolling_percent_hawk",
69
+ title="% hawk (rolling average)",
70
+ scale=alt.Scale(domain=[0, 1]),
71
+ ),
72
+ )
73
+ )
74
+ # add the rolling average line on top of the bar chart
75
+ bar_chart += line
76
+
77
+ return solara.FigureAltair(bar_chart)
78
+
79
+
80
+ page = JupyterViz(
81
+ HawkDoveSingleRiskModel,
82
+ jupyterviz_params,
83
+ measures=[plot_hawks],
84
+ name="Hawk/Dove game with risk attitudes; all agents have the same risk attitude",
85
+ agent_portrayal=agent_portrayal,
86
+ space_drawer=draw_hawkdove_agent_space,
87
+ )
88
+
89
+ # required to render the visualization with Jupyter/Solara
90
+ page