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.
- simulatingrisk/__init__.py +1 -0
- simulatingrisk/about_app.md +2 -0
- simulatingrisk/app.py +50 -0
- simulatingrisk/batch_run.py +145 -0
- simulatingrisk/charts/histogram.js +51 -0
- simulatingrisk/charts/histogram.py +72 -0
- simulatingrisk/hawkdove/README.md +106 -0
- simulatingrisk/hawkdove/app.py +90 -0
- simulatingrisk/hawkdove/model.py +391 -0
- simulatingrisk/hawkdove/run.py +31 -0
- simulatingrisk/hawkdove/server.py +189 -0
- simulatingrisk/hawkdovemulti/README.md +92 -0
- simulatingrisk/hawkdovemulti/analysis_utils.py +83 -0
- simulatingrisk/hawkdovemulti/app.py +242 -0
- simulatingrisk/hawkdovemulti/batch_run.py +328 -0
- simulatingrisk/hawkdovemulti/model.py +462 -0
- simulatingrisk/hawkdovemulti/run_simulation.ipynb +55 -0
- simulatingrisk/hawkdovemulti/simrisk_batch.slurm +44 -0
- simulatingrisk/risky_bet/README.md +45 -0
- simulatingrisk/risky_bet/app.py +17 -0
- simulatingrisk/risky_bet/model.py +237 -0
- simulatingrisk/risky_bet/run.py +46 -0
- simulatingrisk/risky_bet/server.py +102 -0
- simulatingrisk/risky_food/README.md +32 -0
- simulatingrisk/risky_food/__init__.py +0 -0
- simulatingrisk/risky_food/app.py +19 -0
- simulatingrisk/risky_food/model.py +250 -0
- simulatingrisk/risky_food/run.py +20 -0
- simulatingrisk/risky_food/server.py +78 -0
- simulatingrisk/stag_hunt/README.md +15 -0
- simulatingrisk/stag_hunt/__init__.py +0 -0
- simulatingrisk/stag_hunt/model.py +123 -0
- simulatingrisk/stag_hunt/run.py +45 -0
- simulatingrisk/utils.py +31 -0
- simulatingrisk-1.0.0.dist-info/METADATA +113 -0
- simulatingrisk-1.0.0.dist-info/RECORD +40 -0
- simulatingrisk-1.0.0.dist-info/WHEEL +5 -0
- simulatingrisk-1.0.0.dist-info/entry_points.txt +2 -0
- simulatingrisk-1.0.0.dist-info/licenses/LICENSE +201 -0
- simulatingrisk-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
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
|