diff --git a/examples/UltimatumModel/run.py b/examples/UltimatumModel/run.py new file mode 100644 index 000000000..aad54d92f --- /dev/null +++ b/examples/UltimatumModel/run.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +from ultimatum_game.model import UltimatumModel + +# Setup and run +model = UltimatumModel(n=1000) +for i in range(1000): + model.step() + +# Retrieve data +data = model.datacollector.get_model_vars_dataframe() + +# Visualization +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) +data["Accept Rate"].plot(ax=axes[0, 0], title="Accept Rate %") +data["Avg Offer"].plot(ax=axes[0, 1], title="Average Offer") +data["Proposer Avg"].plot(ax=axes[1, 0], title="Proposer Earnings") +data["Responder Avg"].plot(ax=axes[1, 1], title="Responder Earnings") + +plt.tight_layout() +plt.show() diff --git a/examples/UltimatumModel/ultimatum_game/Readme.md b/examples/UltimatumModel/ultimatum_game/Readme.md new file mode 100644 index 000000000..e8f0bf457 --- /dev/null +++ b/examples/UltimatumModel/ultimatum_game/Readme.md @@ -0,0 +1,37 @@ +# Iterated Ultimatum Game with Several Agents + +## Summary + +The Ultimatum Game is an experimental economics game in which two players interact to decide how to divide a sum of money. This model consists of agents that pair up, one randomly taking the role of proposer and the other the role of responder. + +The total stake in this model is 100 units. The proposer chooses a positive integer between 0 and 99 as the offer to pay the responder. The responder has two options: + +1. Accept: The offer is recognized as fair, and the money is divided according to the offer. + +2. Reject: The offer is recognized as unfair; it is rejected, and neither party receives any amount. + +Behavioral studies on this game have shown that, contrary to classic economic theory, responders do not always accept every offer greater than zero. + +In this model, initial offers and thresholds start randomly. However, after each step, agents learn based on whether the interaction was successful. Each offer is recorded in the players' memory, increasing the probability of choosing that strategy in the future. The higher the benefit of a specific offer or threshold, the higher its probability weight. In each new step, agents are randomly re-paired and roles are reassigned. + +## How to Run + +To run the model and see the results, ensure you have mesa, numpy, and matplotlib installed, then execute: + +``` + $ python run.py +``` + +## Files + +* ``agents.py``: Contains the ``UltimatumAgent`` class and the learning logic. + +* ``model.py``: Contains the ``UltimatumModel`` class. The model takes a positive integer ``n`` as an argument to determine the number of agents. + +* ``run.py``: The script used to run the simulation for a specified number of steps and visualize the data. + +## Further Reading + +This model is adapted from: + +* Camerer, Colin F. 2003. Behavioral Game Theory: Experiments in Strategic Interaction. Princeton, NJ: Princeton University Press. \ No newline at end of file diff --git a/examples/UltimatumModel/ultimatum_game/__init__.py b/examples/UltimatumModel/ultimatum_game/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/UltimatumModel/ultimatum_game/agents.py b/examples/UltimatumModel/ultimatum_game/agents.py new file mode 100644 index 000000000..208624565 --- /dev/null +++ b/examples/UltimatumModel/ultimatum_game/agents.py @@ -0,0 +1,12 @@ +import mesa +import numpy as np + + +class UltimatumAgent(mesa.Agent): + def __init__(self, model, unique_id): + super().__init__(model) + self.wealth = 0.0 + self.offer_memory = np.zeros(len(model.offers)) + self.offer_chosen = 0 + self.thresh_memory = np.zeros(len(model.thresholds)) + self.thresh_chosen = 0 diff --git a/examples/UltimatumModel/ultimatum_game/model.py b/examples/UltimatumModel/ultimatum_game/model.py new file mode 100644 index 000000000..140965b79 --- /dev/null +++ b/examples/UltimatumModel/ultimatum_game/model.py @@ -0,0 +1,116 @@ +import mesa +import numpy as np + +from ultimatum_game.agents import UltimatumAgent + + +class UltimatumModel(mesa.Model): + def __init__(self, n, stake=100): + super().__init__() + self.num_agents = n + self.stake = stake + self.grid = mesa.space.MultiGrid(10, 10, torus=True) + self.offers = list(range(self.stake)) + self.thresholds = list(range(self.stake)) + self.learning_rate = 0.2 + self.decay_rate = 0.95 + # Data collection + self.datacollector = mesa.DataCollector( + model_reporters={ + "Accept Rate": "current_accept_rate", + "Avg Offer": "current_avg_offer", + "Proposer Avg": "current_proposer_avg", + "Responder Avg": "current_responder_avg", + } + ) + self.current_accept_rate = 0 + self.current_avg_offer = 0 + self.current_proposer_avg = 0 + self.current_responder_avg = 0 + + # Create Agents + for i in range(self.num_agents): + agent = UltimatumAgent(self, i) + # Random position + x = self.random.randrange(self.grid.width) + y = self.random.randrange(self.grid.height) + self.grid.place_agent(agent, (x, y)) + + def step(self): + self.agents.shuffle_do("step") + agents = [a for a in self.agents] + np.random.shuffle(agents) + pairs = [(agents[i], agents[i + 1]) for i in range(0, len(agents), 2)] + + # Stats trackers + offers_made = [] + accept_count = 0 + total_proposer_pay = 0 + total_responder_pay = 0 + + # Game Settings + for first_agent, second_agent in pairs: + if np.random.random() < 0.5: + proposer, responder = first_agent, second_agent + else: + proposer, responder = second_agent, first_agent + + proposer_offer = ( + proposer.offer_chosen + if np.sum(proposer.offer_memory) > 0 + else np.random.randint(0, len(proposer.offer_memory)) + ) + if np.sum(proposer.offer_memory) > 0: + exp_mem = np.exp(proposer.offer_memory / 5.0) + probs = exp_mem / exp_mem.sum() + proposer_offer = np.random.choice(len(proposer.offer_memory), p=probs) + offer = self.offers[proposer_offer] + proposer.offer_chosen = proposer_offer + + responder_thresh = ( + responder.thresh_chosen + if np.sum(responder.thresh_memory) > 0 + else np.random.randint(0, len(responder.thresh_memory)) + ) + if np.sum(responder.thresh_memory) > 0: + exp_mem = np.exp(responder.thresh_memory / 5.0) + probs = exp_mem / exp_mem.sum() + responder_thresh = np.random.choice( + len(responder.thresh_memory), p=probs + ) + threshold = self.thresholds[responder_thresh] + responder.thresh_chosen = responder_thresh + + # Play the game + if offer >= threshold: + p_pay = self.stake - offer + r_pay = offer + accept_count += 1 + else: + p_pay = 0 + r_pay = 0 + + # Learn + proposer.offer_memory[proposer_offer] += self.learning_rate * p_pay + responder.thresh_memory[responder_thresh] += self.learning_rate * r_pay + + # Decay memories + proposer.offer_memory *= self.decay_rate + responder.thresh_memory *= self.decay_rate + + # Update wealth + proposer.wealth += p_pay + responder.wealth += r_pay + + # Stats + offers_made.append(offer) + total_proposer_pay += p_pay + total_responder_pay += r_pay + + # Model stats + self.current_accept_rate = (accept_count / len(pairs)) * 100 + self.current_avg_offer = np.mean(offers_made) + self.current_proposer_avg = total_proposer_pay / len(pairs) + self.current_responder_avg = total_responder_pay / len(pairs) + + self.datacollector.collect(self)