Skip to content

Commit 63326dc

Browse files
rhtdanielxu05
authored andcommitted
Implement El Farol model
Co-authored-by: Daniel Xu <tx29@uw.edu>
1 parent 605e093 commit 63326dc

File tree

7 files changed

+311
-0
lines changed

7 files changed

+311
-0
lines changed

examples/el_farol/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# El Farol
2+
3+
This folder contains an implementation of El Farol restaurant model. Agents (restaurant customers) decide whether to go to the restaurant or not based on their memory and reward from previous trials. Implications from the model have been used to explain how individual decision-making affects overall performance and fluctuation.
4+
5+
The implementation is based on Fogel 1999 (in particular the calculation of the prediction), which is a refinement over Arthur 1994.
6+
7+
## How to Run
8+
9+
Launch the model: You can run the model and perform analysis in el_farol.ipynb.
10+
You can test the model itself by running `pytest tests.py`.
11+
12+
## Files
13+
* [el_farol.ipynb](el_farol.ipynb): Run the model and visualization in a Jupyter notebook
14+
* [el_farol/model.py](el_farol/model.py): Core model file.
15+
* [el_farol/agents.py](el_farol/agents.py): The agent class.
16+
* [tests.py](tests.py): Tests to ensure the model is consistent with Arthur 1994, Fogel 1996.
17+
18+
## Further Reading
19+
20+
1. W. Brian Arthur Inductive Reasoning and Bounded Rationality (1994) https://www.jstor.org/stable/2117868
21+
1. D.B. Fogel, K. Chellapilla, P.J. Angeline Inductive reasoning and bounded rationality reconsidered (1999)
22+
1. NetLogo implementation of the El Farol bar problem https://ccl.northwestern.edu/netlogo/models/ElFarol

examples/el_farol/el_farol.ipynb

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import matplotlib.pyplot as plt\n",
10+
"import numpy as np\n",
11+
"import seaborn as sns\n",
12+
"\n",
13+
"from el_farol.model import ElFarolBar"
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"metadata": {},
20+
"outputs": [],
21+
"source": [
22+
"memory_sizes = [5, 10, 20]\n",
23+
"crowd_threshold = 60\n",
24+
"models = [\n",
25+
" ElFarolBar(N=100, crowd_threshold=crowd_threshold, memory_size=m)\n",
26+
" for m in memory_sizes\n",
27+
"]\n",
28+
"for model in models:\n",
29+
" for i in range(100):\n",
30+
" model.step()"
31+
]
32+
},
33+
{
34+
"cell_type": "code",
35+
"execution_count": null,
36+
"metadata": {},
37+
"outputs": [],
38+
"source": [
39+
"# You should observe that the attendance converges to 60.\n",
40+
"_, axs = plt.subplots(1, 3, figsize=(10, 3))\n",
41+
"for idx, model in enumerate(models):\n",
42+
" ax = axs[idx]\n",
43+
" plt.sca(ax)\n",
44+
" df = model.datacollector.get_model_vars_dataframe()\n",
45+
" sns.lineplot(data=df, x=df.index, y=\"Customers\", ax=ax)\n",
46+
" ax.set(\n",
47+
" xlabel=\"Step\",\n",
48+
" ylabel=\"Attendance\",\n",
49+
" title=f\"Memory size = {memory_sizes[idx]}\",\n",
50+
" ylim=(20, 80),\n",
51+
" )\n",
52+
" plt.axhline(crowd_threshold, color=\"tab:red\")\n",
53+
" plt.tight_layout()"
54+
]
55+
},
56+
{
57+
"cell_type": "code",
58+
"execution_count": null,
59+
"metadata": {},
60+
"outputs": [],
61+
"source": [
62+
"for idx, memory_size in enumerate(memory_sizes):\n",
63+
" model = models[idx]\n",
64+
" df = model.datacollector.get_agent_vars_dataframe()\n",
65+
" sns.lineplot(\n",
66+
" x=df.index.levels[0],\n",
67+
" y=df.Utility.groupby(\"Step\").mean(),\n",
68+
" label=str(memory_size),\n",
69+
" )\n",
70+
"plt.legend(title=\"Memory size\");"
71+
]
72+
},
73+
{
74+
"cell_type": "code",
75+
"execution_count": null,
76+
"metadata": {},
77+
"outputs": [],
78+
"source": [
79+
"# Decisions made on across trials\n",
80+
"fix, axs = plt.subplots(1, 3, figsize=(12, 4))\n",
81+
"for idx, memory_size in enumerate(memory_sizes):\n",
82+
" plt.sca(axs[idx])\n",
83+
" df = models[idx].datacollector.get_agent_vars_dataframe()\n",
84+
" df.reset_index(inplace=True)\n",
85+
" ax = sns.heatmap(df.pivot(index=\"AgentID\", columns=\"Step\", values=\"Attendance\"))\n",
86+
" ax.set(title=f\"Memory size = {memory_size}\")\n",
87+
" plt.tight_layout()"
88+
]
89+
},
90+
{
91+
"cell_type": "code",
92+
"execution_count": null,
93+
"metadata": {},
94+
"outputs": [],
95+
"source": [
96+
"# Next, we experiment with varying the number of strategies\n",
97+
"num_strategies_list = [5, 10, 20]\n",
98+
"crowd_threshold = 60\n",
99+
"models = [\n",
100+
" ElFarolBar(N=100, crowd_threshold=crowd_threshold, num_strategies=ns)\n",
101+
" for ns in num_strategies_list\n",
102+
"]\n",
103+
"for model in models:\n",
104+
" for i in range(100):\n",
105+
" model.step()"
106+
]
107+
},
108+
{
109+
"cell_type": "code",
110+
"execution_count": null,
111+
"metadata": {},
112+
"outputs": [],
113+
"source": [
114+
"# Attendance of the bar based on the number of strategies\n",
115+
"_, axs = plt.subplots(1, 3, figsize=(10, 3))\n",
116+
"for idx, num_strategies in enumerate(num_strategies_list):\n",
117+
" model = models[idx]\n",
118+
" ax = axs[idx]\n",
119+
" plt.sca(ax)\n",
120+
" df = model.datacollector.get_model_vars_dataframe()\n",
121+
" sns.lineplot(data=df, x=df.index, y=\"Customers\", ax=ax)\n",
122+
" ax.set(\n",
123+
" xlabel=\"Trial\",\n",
124+
" ylabel=\"Attendance\",\n",
125+
" title=f\"Number of Strategies = {num_strategies}\",\n",
126+
" ylim=(20, 80),\n",
127+
" )\n",
128+
" plt.axhline(crowd_threshold, color=\"tab:red\")\n",
129+
" plt.tight_layout()"
130+
]
131+
}
132+
],
133+
"metadata": {
134+
"interpreter": {
135+
"hash": "18b8a6ab22c23ac88fce14986952a46f0d293914064547c699eac09fb58cfe0f"
136+
},
137+
"kernelspec": {
138+
"display_name": "Python 3 (ipykernel)",
139+
"language": "python",
140+
"name": "python3"
141+
},
142+
"language_info": {
143+
"codemirror_mode": {
144+
"name": "ipython",
145+
"version": 3
146+
},
147+
"file_extension": ".py",
148+
"mimetype": "text/x-python",
149+
"name": "python",
150+
"nbconvert_exporter": "python",
151+
"pygments_lexer": "ipython3",
152+
"version": "3.11.6"
153+
}
154+
},
155+
"nbformat": 4,
156+
"nbformat_minor": 4
157+
}

examples/el_farol/el_farol/__init__.py

Whitespace-only changes.

examples/el_farol/el_farol/agents.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import mesa
2+
import numpy as np
3+
4+
5+
class BarCustomer(mesa.Agent):
6+
def __init__(self, unique_id, model, memory_size, crowd_threshold, num_strategies):
7+
super().__init__(unique_id, model)
8+
# Random values from -1.0 to 1.0
9+
self.strategies = np.random.rand(num_strategies, memory_size + 1) * 2 - 1
10+
self.best_strategy = self.strategies[0]
11+
self.attend = False
12+
self.memory_size = memory_size
13+
self.crowd_threshold = crowd_threshold
14+
self.utility = 0
15+
self.update_strategies()
16+
17+
def step(self):
18+
prediction = self.predict_attendance(
19+
self.best_strategy, self.model.history[-self.memory_size :]
20+
)
21+
if prediction <= self.crowd_threshold:
22+
self.attend = True
23+
self.model.attendance += 1
24+
else:
25+
self.attend = False
26+
27+
def update_strategies(self):
28+
# Pick the best strategy based on new history window
29+
best_score = float("inf")
30+
for strategy in self.strategies:
31+
score = 0
32+
for week in range(self.memory_size):
33+
last = week + self.memory_size
34+
prediction = self.predict_attendance(
35+
strategy, self.model.history[week:last]
36+
)
37+
score += abs(self.model.history[last] - prediction)
38+
if score <= best_score:
39+
best_score = score
40+
self.best_strategy = strategy
41+
should_attend = self.model.history[-1] <= self.crowd_threshold
42+
if should_attend != self.attend:
43+
self.utility -= 1
44+
else:
45+
self.utility += 1
46+
47+
def predict_attendance(self, strategy, subhistory):
48+
# This is extracted from the source code of the model in
49+
# https://ccl.northwestern.edu/netlogo/models/ElFarol.
50+
# This reports an agent's prediction of the current attendance
51+
# using a particular strategy and portion of the attendance history.
52+
# More specifically, the strategy is then described by the formula
53+
# p(t) = x(t - 1) * a(t - 1) + x(t - 2) * a(t - 2) +..
54+
# ... + x(t - memory_size) * a(t - memory_size) + c * 100,
55+
# where p(t) is the prediction at time t, x(t) is the attendance of the
56+
# bar at time t, a(t) is the weight for time t, c is a constant, and
57+
# MEMORY-SIZE is an external parameter.
58+
59+
# The first element of the strategy is the constant, c, in the
60+
# prediction formula. one can think of it as the the agent's prediction
61+
# of the bar's attendance in the absence of any other data then we
62+
# multiply each week in the history by its respective weight.
63+
return strategy[0] * 100 + np.dot(strategy[1:], subhistory)

examples/el_farol/el_farol/model.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import mesa
2+
import numpy as np
3+
4+
from .agents import BarCustomer
5+
6+
7+
class ElFarolBar(mesa.Model):
8+
def __init__(
9+
self,
10+
crowd_threshold=60,
11+
num_strategies=10,
12+
memory_size=10,
13+
width=100,
14+
height=100,
15+
N=100,
16+
):
17+
self.running = True
18+
self.num_agents = N
19+
self.schedule = mesa.time.RandomActivation(self)
20+
21+
# Initialize the previous attendance randomly so the agents have a history
22+
# to work with from the start.
23+
# The history is twice the memory, because we need at least a memory
24+
# worth of history for each point in memory to test how well the
25+
# strategies would have worked.
26+
self.history = np.random.randint(0, 100, size=memory_size * 2).tolist()
27+
self.attendance = self.history[-1]
28+
for i in range(self.num_agents):
29+
a = BarCustomer(i, self, memory_size, crowd_threshold, num_strategies)
30+
self.schedule.add(a)
31+
self.datacollector = mesa.DataCollector(
32+
model_reporters={"Customers": "attendance"},
33+
agent_reporters={"Utility": "utility", "Attendance": "attend"},
34+
)
35+
36+
def step(self):
37+
self.datacollector.collect(self)
38+
self.attendance = 0
39+
self.schedule.step()
40+
# We ensure that the length of history is constant
41+
# after each step.
42+
self.history.pop(0)
43+
self.history.append(self.attendance)
44+
for agent in self.schedule.agents:
45+
agent.update_strategies()

examples/el_farol/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
jupyter
2+
matplotlib
3+
mesa
4+
numpy
5+
seaborn

examples/el_farol/tests.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import numpy as np
2+
from el_farol.model import ElFarolBar
3+
4+
np.random.seed(1)
5+
crowd_threshold = 60
6+
7+
8+
def test_convergence():
9+
# Testing that the attendance converges to crowd_threshold
10+
attendances = []
11+
for _ in range(10):
12+
model = ElFarolBar(N=100, crowd_threshold=crowd_threshold, memory_size=10)
13+
for _ in range(100):
14+
model.step()
15+
attendances.append(model.attendance)
16+
mean = np.mean(attendances)
17+
standard_deviation = np.std(attendances)
18+
deviation = abs(mean - crowd_threshold)
19+
assert deviation < standard_deviation

0 commit comments

Comments
 (0)