diff --git a/README.md b/README.md index 9c29af37..81ac755d 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ pip install -e . ## Usage +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa-frames/blob/main/docs/general/user-guide/2_introductory-tutorial.ipynb) + **Note:** mesa-frames is currently in its early stages of development. As such, the usage patterns and API are subject to change. Breaking changes may be introduced. Reports of feedback and issues are encouraged. You can find the API documentation [here](https://projectmesa.github.io/mesa-frames/api). diff --git a/docs/general/user-guide/2_introductory-tutorial.ipynb b/docs/general/user-guide/2_introductory-tutorial.ipynb new file mode 100644 index 00000000..9d8e64fa --- /dev/null +++ b/docs/general/user-guide/2_introductory-tutorial.ipynb @@ -0,0 +1,441 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7ee055b2", + "metadata": {}, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa-frames/blob/main/docs/general/user-guide/2_introductory-tutorial.ipynb)" + ] + }, + { + "cell_type": "markdown", + "id": "8bd0381e", + "metadata": {}, + "source": [ + "## Installation (if running in Colab)\n", + "\n", + "Run the following cell to install `mesa-frames` if you are using Google Colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df4d8623", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install mesa-frames mesa" + ] + }, + { + "cell_type": "markdown", + "id": "11515dfc", + "metadata": {}, + "source": [ + " # Introductory Tutorial: Boltzmann Wealth Model with mesa-frames 💰🚀\n", + "\n", + "In this tutorial, we'll implement the Boltzmann Wealth Model using mesa-frames. This model simulates the distribution of wealth among agents, where agents randomly give money to each other.\n", + "\n", + "## Setting Up the Model 🏗️\n", + "\n", + "First, let's import the necessary modules and set up our model class:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fc0ee981", + "metadata": {}, + "outputs": [], + "source": [ + "from mesa_frames import ModelDF, AgentSetPolars\n", + "\n", + "\n", + "class MoneyModelDF(ModelDF):\n", + " def __init__(self, N: int, agents_cls):\n", + " super().__init__()\n", + " self.n_agents = N\n", + " self.agents += agents_cls(N, self)\n", + "\n", + " def step(self):\n", + " # Executes the step method for every agentset in self.agents\n", + " self.agents.do(\"step\")\n", + "\n", + " def run_model(self, n):\n", + " for _ in range(n):\n", + " self.step()" + ] + }, + { + "cell_type": "markdown", + "id": "00e092c4", + "metadata": {}, + "source": [ + "## Implementing the AgentSet 👥\n", + "\n", + "Now, let's implement our `MoneyAgentSet` using polars backends." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2bac0126", + "metadata": {}, + "outputs": [], + "source": [ + "import polars as pl\n", + "\n", + "\n", + "class MoneyAgentPolars(AgentSetPolars):\n", + " def __init__(self, n: int, model: ModelDF):\n", + " super().__init__(model)\n", + " self += pl.DataFrame(\n", + " {\"unique_id\": pl.arange(n, eager=True), \"wealth\": pl.ones(n, eager=True)}\n", + " )\n", + "\n", + " def step(self) -> None:\n", + " self.do(\"give_money\")\n", + "\n", + " def give_money(self):\n", + " self.select(self.wealth > 0)\n", + " other_agents = self.agents.sample(\n", + " n=len(self.active_agents), with_replacement=True\n", + " )\n", + " self[\"active\", \"wealth\"] -= 1\n", + " new_wealth = other_agents.group_by(\"unique_id\").len()\n", + " self[new_wealth[\"unique_id\"], \"wealth\"] += new_wealth[\"len\"]" + ] + }, + { + "cell_type": "markdown", + "id": "3b141016", + "metadata": {}, + "source": [ + "\n", + "## Running the Model ▶️\n", + "\n", + "Now that we have our model and agent set defined, let's run a simulation:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "65da4e6f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "shape: (9, 2)\n", + "┌────────────┬──────────┐\n", + "│ statistic ┆ wealth │\n", + "│ --- ┆ --- │\n", + "│ str ┆ f64 │\n", + "╞════════════╪══════════╡\n", + "│ count ┆ 1000.0 │\n", + "│ null_count ┆ 0.0 │\n", + "│ mean ┆ 1.0 │\n", + "│ std ┆ 1.171056 │\n", + "│ min ┆ 0.0 │\n", + "│ 25% ┆ 0.0 │\n", + "│ 50% ┆ 1.0 │\n", + "│ 75% ┆ 2.0 │\n", + "│ max ┆ 9.0 │\n", + "└────────────┴──────────┘\n" + ] + } + ], + "source": [ + "# Choose either MoneyAgentPandas or MoneyAgentPolars\n", + "agent_class = MoneyAgentPolars\n", + "\n", + "# Create and run the model\n", + "model = MoneyModelDF(1000, agent_class)\n", + "model.run_model(100)\n", + "\n", + "wealth_dist = list(model.agents.agents.values())[0]\n", + "# Print the final wealth distribution\n", + "print(wealth_dist.select(pl.col(\"wealth\")).describe())" + ] + }, + { + "cell_type": "markdown", + "id": "812da73b", + "metadata": {}, + "source": [ + "\n", + "This output shows the statistical summary of the wealth distribution after 100 steps of the simulation with 1000 agents.\n", + "\n", + "## Performance Comparison 🏎️💨\n", + "\n", + "One of the key advantages of mesa-frames is its performance with large numbers of agents. Let's compare the performance of mesa and polars:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "fbdb540810924de8", + "metadata": {}, + "outputs": [], + "source": [ + "class MoneyAgentPolarsConcise(AgentSetPolars):\n", + " def __init__(self, n: int, model: ModelDF):\n", + " super().__init__(model)\n", + " ## Adding the agents to the agent set\n", + " # 1. Changing the agents attribute directly (not recommended, if other agents were added before, they will be lost)\n", + " \"\"\"self.agents = pl.DataFrame(\n", + " {\"unique_id\": pl.arange(n, eager=True), \"wealth\": pl.ones(n, eager=True)}\n", + " )\"\"\"\n", + " # 2. Adding the dataframe with add\n", + " \"\"\"self.add(\n", + " pl.DataFrame(\n", + " {\n", + " \"unique_id\": pl.arange(n, eager=True),\n", + " \"wealth\": pl.ones(n, eager=True),\n", + " }\n", + " )\n", + " )\"\"\"\n", + " # 3. Adding the dataframe with __iadd__\n", + " self += pl.DataFrame(\n", + " {\"unique_id\": pl.arange(n, eager=True), \"wealth\": pl.ones(n, eager=True)}\n", + " )\n", + "\n", + " def step(self) -> None:\n", + " # The give_money method is called\n", + " # self.give_money()\n", + " self.do(\"give_money\")\n", + "\n", + " def give_money(self):\n", + " ## Active agents are changed to wealthy agents\n", + " # 1. Using the __getitem__ method\n", + " # self.select(self[\"wealth\"] > 0)\n", + " # 2. Using the fallback __getattr__ method\n", + " self.select(self.wealth > 0)\n", + "\n", + " # Receiving agents are sampled (only native expressions currently supported)\n", + " other_agents = self.agents.sample(\n", + " n=len(self.active_agents), with_replacement=True\n", + " )\n", + "\n", + " # Wealth of wealthy is decreased by 1\n", + " # 1. Using the __setitem__ method with self.active_agents mask\n", + " # self[self.active_agents, \"wealth\"] -= 1\n", + " # 2. Using the __setitem__ method with \"active\" mask\n", + " self[\"active\", \"wealth\"] -= 1\n", + "\n", + " # Compute the income of the other agents (only native expressions currently supported)\n", + " new_wealth = other_agents.group_by(\"unique_id\").len()\n", + "\n", + " # Add the income to the other agents\n", + " # 1. Using the set method\n", + " \"\"\"self.set(\n", + " attr_names=\"wealth\",\n", + " values=pl.col(\"wealth\") + new_wealth[\"len\"],\n", + " mask=new_wealth,\n", + " )\"\"\"\n", + "\n", + " # 2. Using the __setitem__ method\n", + " self[new_wealth, \"wealth\"] += new_wealth[\"len\"]\n", + "\n", + "\n", + "class MoneyAgentPolarsNative(AgentSetPolars):\n", + " def __init__(self, n: int, model: ModelDF):\n", + " super().__init__(model)\n", + " self += pl.DataFrame(\n", + " {\"unique_id\": pl.arange(n, eager=True), \"wealth\": pl.ones(n, eager=True)}\n", + " )\n", + "\n", + " def step(self) -> None:\n", + " self.do(\"give_money\")\n", + "\n", + " def give_money(self):\n", + " ## Active agents are changed to wealthy agents\n", + " self.select(pl.col(\"wealth\") > 0)\n", + "\n", + " other_agents = self.agents.sample(\n", + " n=len(self.active_agents), with_replacement=True\n", + " )\n", + "\n", + " # Wealth of wealthy is decreased by 1\n", + " self.agents = self.agents.with_columns(\n", + " wealth=pl.when(pl.col(\"unique_id\").is_in(self.active_agents[\"unique_id\"]))\n", + " .then(pl.col(\"wealth\") - 1)\n", + " .otherwise(pl.col(\"wealth\"))\n", + " )\n", + "\n", + " new_wealth = other_agents.group_by(\"unique_id\").len()\n", + "\n", + " # Add the income to the other agents\n", + " self.agents = (\n", + " self.agents.join(new_wealth, on=\"unique_id\", how=\"left\")\n", + " .fill_null(0)\n", + " .with_columns(wealth=pl.col(\"wealth\") + pl.col(\"len\"))\n", + " .drop(\"len\")\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "496196d999f18634", + "metadata": {}, + "source": [ + "Add Mesa implementation of MoneyAgent and MoneyModel classes to test Mesa performance" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "9dbe761af964af5b", + "metadata": {}, + "outputs": [], + "source": [ + "import mesa\n", + "import importlib.metadata\n", + "from packaging import version\n", + "\n", + "\n", + "class MoneyAgent(mesa.Agent):\n", + " \"\"\"An agent with fixed initial wealth.\"\"\"\n", + "\n", + " def __init__(self, model):\n", + " # Pass the parameters to the parent class.\n", + " super().__init__(model)\n", + "\n", + " # Create the agent's variable and set the initial values.\n", + " self.wealth = 1\n", + "\n", + " def step(self):\n", + " # Verify agent has some wealth\n", + " if self.wealth > 0:\n", + " other_agent: MoneyAgent = self.model.random.choice(self.model.agents)\n", + " if other_agent is not None:\n", + " other_agent.wealth += 1\n", + " self.wealth -= 1\n", + "\n", + "\n", + "class MoneyModel(mesa.Model):\n", + " \"\"\"A model with some number of agents.\"\"\"\n", + "\n", + " def __init__(self, N: int):\n", + " super().__init__()\n", + " self.num_agents = N\n", + " for i in range(N):\n", + " self.agents.add(MoneyAgent(self))\n", + "\n", + " def step(self):\n", + " \"\"\"Advance the model by one step.\"\"\"\n", + " self.agents.shuffle_do(\"step\")\n", + "\n", + " def run_model(self, n_steps) -> None:\n", + " for _ in range(n_steps):\n", + " self.step()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "2d864cd3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Execution times:\n", + "---------------\n", + "mesa:\n", + " Number of agents: 100, Time: 0.06 seconds\n", + " Number of agents: 1001, Time: 3.56 seconds\n", + " Number of agents: 2000, Time: 12.81 seconds\n", + "---------------\n", + "---------------\n", + "mesa-frames (pl concise):\n", + " Number of agents: 100, Time: 0.20 seconds\n", + " Number of agents: 1001, Time: 0.21 seconds\n", + " Number of agents: 2000, Time: 0.23 seconds\n", + "---------------\n", + "---------------\n", + "mesa-frames (pl native):\n", + " Number of agents: 100, Time: 0.11 seconds\n", + " Number of agents: 1001, Time: 0.12 seconds\n", + " Number of agents: 2000, Time: 0.13 seconds\n", + "---------------\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "\n", + "def run_simulation(model: MoneyModel | MoneyModelDF, n_steps: int):\n", + " start_time = time.time()\n", + " model.run_model(n_steps)\n", + " end_time = time.time()\n", + " return end_time - start_time\n", + "\n", + "\n", + "# Compare mesa and mesa-frames implementations\n", + "n_agents_list = [10**2, 10**3 + 1, 2 * 10**3]\n", + "n_steps = 100\n", + "print(\"Execution times:\")\n", + "for implementation in [\n", + " \"mesa\",\n", + " \"mesa-frames (pl concise)\",\n", + " \"mesa-frames (pl native)\",\n", + "]:\n", + " print(f\"---------------\\n{implementation}:\")\n", + " for n_agents in n_agents_list:\n", + " if implementation == \"mesa\":\n", + " ntime = run_simulation(MoneyModel(n_agents), n_steps)\n", + " elif implementation == \"mesa-frames (pl concise)\":\n", + " ntime = run_simulation(\n", + " MoneyModelDF(n_agents, MoneyAgentPolarsConcise), n_steps\n", + " )\n", + " elif implementation == \"mesa-frames (pl native)\":\n", + " ntime = run_simulation(\n", + " MoneyModelDF(n_agents, MoneyAgentPolarsNative), n_steps\n", + " )\n", + "\n", + " print(f\" Number of agents: {n_agents}, Time: {ntime:.2f} seconds\")\n", + " print(\"---------------\")" + ] + }, + { + "cell_type": "markdown", + "id": "6dfc6d34", + "metadata": {}, + "source": [ + "\n", + "## Conclusion 🎉\n", + "\n", + "- All mesa-frames implementations significantly outperform the original mesa implementation. 🏆\n", + "- The native implementation for Polars shows better performance than their concise counterparts. 💪\n", + "- The Polars native implementation shows the most impressive speed-up, ranging from 10.86x to 17.60x faster than mesa! 🚀🚀🚀\n", + "- The performance advantage of mesa-frames becomes more pronounced as the number of agents increases. 📈" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/general/user-guide/2_introductory-tutorial.md b/docs/general/user-guide/2_introductory-tutorial.md deleted file mode 100644 index aa782f83..00000000 --- a/docs/general/user-guide/2_introductory-tutorial.md +++ /dev/null @@ -1,167 +0,0 @@ -# Introductory Tutorial: Boltzmann Wealth Model with mesa-frames 💰🚀 - -In this tutorial, we'll implement the Boltzmann Wealth Model using mesa-frames. This model simulates the distribution of wealth among agents, where agents randomly give money to each other. - -## Setting Up the Model 🏗️ - -First, let's import the necessary modules and set up our model class: - -```python -from mesa_frames import ModelDF, AgentSetPolars - -class MoneyModelDF(ModelDF): - def __init__(self, N: int, agents_cls): - super().__init__() - self.n_agents = N - self.agents += agents_cls(N, self) - - def step(self): - self.agents.do("step") - - def run_model(self, n): - for _ in range(n): - self.step() -``` - -This `MoneyModelDF` class will work for Polars implementations. - -## Implementing the AgentSet 👥 - -Now, let's implement our `MoneyAgentSet` using Polars backends. You can switch between the two implementations: - -=== "Polars 🐻‍❄️" - - ```python - import polars as pl - - class MoneyAgentPolars(AgentSetPolars): - def __init__(self, n: int, model: ModelDF): - super().__init__(model) - self += pl.DataFrame( - {"unique_id": pl.arange(n, eager=True), "wealth": pl.ones(n, eager=True)} - ) - - def step(self) -> None: - self.do("give_money") - - def give_money(self): - self.select(self.wealth > 0) - other_agents = self.agents.sample(n=len(self.active_agents), with_replacement=True) - self["active", "wealth"] -= 1 - new_wealth = other_agents.group_by("unique_id").len() - self[new_wealth["unique_id"], "wealth"] += new_wealth["len"] - ``` - -## Running the Model ▶️ - -Now that we have our model and agent set defined, let's run a simulation: - -```python - -agent_class = MoneyAgentPolars - -# Create and run the model -model = MoneyModelDF(1000, agent_class) -model.run_model(100) - -# Print the final wealth distribution -print(model.agents["wealth"].describe()) -``` - -Output: - -```python -count 1000.000000 -mean 1.000000 -std 1.414214 -min 0.000000 -25% 0.000000 -50% 1.000000 -75% 1.000000 -max 13.000000 -Name: wealth, dtype: float64 -``` - -This output shows the statistical summary of the wealth distribution after 100 steps of the simulation with 1000 agents. - -## Performance Comparison 🏎️💨 - -One of the key advantages of mesa-frames is its performance with large numbers of agents. Let's compare the performance of our pandas and Polars implementations: - -```python -import time - -def run_simulation(model_class, n_agents, n_steps): - start_time = time.time() - model = model_class(n_agents) - model.run_model(n_steps) - end_time = time.time() - return end_time - start_time - -# Compare mesa and mesa-frames implementations -n_agents_list = [100000, 300000, 500000, 700000] -n_steps = 100 - -print("Execution times:") -for implementation in ["mesa", "mesa-frames (pl concise)", "mesa-frames (pl native)", "mesa-frames (pd concise)", "mesa-frames (pd native)"]: - print(f"---------------\n{implementation}:") - for n_agents in n_agents_list: - if implementation == "mesa": - time = run_simulation(MoneyModel, n_agents, n_steps) - elif implementation == "mesa-frames (pl concise)": - time = run_simulation(lambda n: MoneyModelDF(n, MoneyAgentPolarsConcise), n_agents, n_steps) - elif implementation == "mesa-frames (pl native)": - time = run_simulation(lambda n: MoneyModelDF(n, MoneyAgentPolarsNative), n_agents, n_steps) - - print(f" Number of agents: {n_agents}, Time: {time:.2f} seconds") - print("---------------") -``` - -Example output: - -```python ---------------- -mesa: - Number of agents: 100000, Time: 3.80 seconds - Number of agents: 300000, Time: 14.96 seconds - Number of agents: 500000, Time: 26.88 seconds - Number of agents: 700000, Time: 40.34 seconds ---------------- ---------------- -mesa-frames (pl concise): - Number of agents: 100000, Time: 0.76 seconds - Number of agents: 300000, Time: 2.01 seconds - Number of agents: 500000, Time: 4.77 seconds - Number of agents: 700000, Time: 7.26 seconds ---------------- ---------------- -mesa-frames (pl native): - Number of agents: 100000, Time: 0.35 seconds - Number of agents: 300000, Time: 0.85 seconds - Number of agents: 500000, Time: 1.55 seconds - Number of agents: 700000, Time: 2.61 seconds ---------------- -``` - -Speed-up over mesa: 🚀 - -```python -mesa-frames (pl concise): - Number of agents: 100000, Speed-up: 5.00x 💨 - Number of agents: 300000, Speed-up: 7.44x 💨 - Number of agents: 500000, Speed-up: 5.63x 💨 - Number of agents: 700000, Speed-up: 5.56x 💨 ---------------- -mesa-frames (pl native): - Number of agents: 100000, Speed-up: 10.86x 💨 - Number of agents: 300000, Speed-up: 17.60x 💨 - Number of agents: 500000, Speed-up: 17.34x 💨 - Number of agents: 700000, Speed-up: 15.46x 💨 -``` - -## Conclusion 🎉 - -- All mesa-frames implementations significantly outperform the original mesa implementation. 🏆 -- The native implementation for Polars shows better performance than their concise counterparts. 💪 -- The Polars native implementation shows the most impressive speed-up, ranging from 10.86x to 17.60x faster than mesa! 🚀🚀🚀 -- The performance advantage of mesa-frames becomes more pronounced as the number of agents increases. 📈 diff --git a/mkdocs.yml b/mkdocs.yml index e7eb915b..394e33c6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,7 +44,8 @@ theme: # Plugins plugins: - search - - mkdocs-jupyter + - mkdocs-jupyter: + execute: true # Ensures the notebooks run and generate output - git-revision-date-localized: enable_creation_date: true - minify: @@ -109,7 +110,7 @@ nav: - User Guide: - Getting Started: user-guide/0_getting-started.md - Classes: user-guide/1_classes.md - - Introductory Tutorial: user-guide/2_introductory-tutorial.md + - Introductory Tutorial: user-guide/2_introductory-tutorial.ipynb - Advanced Tutorial: user-guide/3_advanced-tutorial.md - Benchmarks: user-guide/4_benchmarks.md - API Reference: api/index.html