We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Feature Request: Agent DataCollection Can't Handle Different Attributes in ActivationByType 
**Describe the Feature**
Receive attribute error when running an agent reporter through datacollector when using RandomActivationByType 

**Desired behavior**
I can collect against different attributes based on agent type

**To Reproduce**
Create a model with different agent types and attributes and try to collect against that agent attribute

**Additional context**
Feature would need to update at line 170 of datacollection.py `agent_records = map(get_reports, model.schedule.agents)`


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mesa/datacollection.py`
Content:
```
1 """
2 Mesa Data Collection Module
3 ===========================
4 
5 DataCollector is meant to provide a simple, standard way to collect data
6 generated by a Mesa model. It collects three types of data: model-level data,
7 agent-level data, and tables.
8 
9 A DataCollector is instantiated with two dictionaries of reporter names and
10 associated variable names or functions for each, one for model-level data and
11 one for agent-level data; a third dictionary provides table names and columns.
12 Variable names are converted into functions which retrieve attributes of that
13 name.
14 
15 When the collect() method is called, each model-level function is called, with
16 the model as the argument, and the results associated with the relevant
17 variable. Then the agent-level functions are called on each agent in the model
18 scheduler.
19 
20 Additionally, other objects can write directly to tables by passing in an
21 appropriate dictionary object for a table row.
22 
23 The DataCollector then stores the data it collects in dictionaries:
24     * model_vars maps each reporter to a list of its values
25     * tables maps each table to a dictionary, with each column as a key with a
26       list as its value.
27     * _agent_records maps each model step to a list of each agents id
28       and its values.
29 
30 Finally, DataCollector can create a pandas DataFrame from each collection.
31 
32 The default DataCollector here makes several assumptions:
33     * The model has a schedule object called 'schedule'
34     * The schedule has an agent list called agents
35     * For collecting agent-level variables, agents must have a unique_id
36 """
37 import itertools
38 import types
39 from operator import attrgetter
40 
41 import pandas as pd
42 
43 
44 class DataCollector:
45     """Class for collecting data generated by a Mesa model.
46 
47     A DataCollector is instantiated with dictionaries of names of model- and
48     agent-level variables to collect, associated with attribute names or
49     functions which actually collect them. When the collect(...) method is
50     called, it collects these attributes and executes these functions one by
51     one and stores the results.
52     """
53 
54     def __init__(self, model_reporters=None, agent_reporters=None, tables=None):
55         """Instantiate a DataCollector with lists of model and agent reporters.
56         Both model_reporters and agent_reporters accept a dictionary mapping a
57         variable name to either an attribute name, or a method.
58         For example, if there was only one model-level reporter for number of
59         agents, it might look like:
60             {"agent_count": lambda m: m.schedule.get_agent_count() }
61         If there was only one agent-level reporter (e.g. the agent's energy),
62         it might look like this:
63             {"energy": "energy"}
64         or like this:
65             {"energy": lambda a: a.energy}
66 
67         The tables arg accepts a dictionary mapping names of tables to lists of
68         columns. For example, if we want to allow agents to write their age
69         when they are destroyed (to keep track of lifespans), it might look
70         like:
71             {"Lifespan": ["unique_id", "age"]}
72 
73         Args:
74             model_reporters: Dictionary of reporter names and attributes/funcs
75             agent_reporters: Dictionary of reporter names and attributes/funcs.
76             tables: Dictionary of table names to lists of column names.
77 
78         Notes:
79             If you want to pickle your model you must not use lambda functions.
80             If your model includes a large number of agents, you should *only*
81             use attribute names for the agent reporter, it will be much faster.
82 
83             Model reporters can take four types of arguments:
84             lambda like above:
85             {"agent_count": lambda m: m.schedule.get_agent_count() }
86             method of a class/instance:
87             {"agent_count": self.get_agent_count} # self here is a class instance
88             {"agent_count": Model.get_agent_count} # Model here is a class
89             class attributes of a model
90             {"model_attribute": "model_attribute"}
91             functions with parameters that have placed in a list
92             {"Model_Function":[function, [param_1, param_2]]}
93         """
94         self.model_reporters = {}
95         self.agent_reporters = {}
96 
97         self.model_vars = {}
98         self._agent_records = {}
99         self.tables = {}
100 
101         if model_reporters is not None:
102             for name, reporter in model_reporters.items():
103                 self._new_model_reporter(name, reporter)
104 
105         if agent_reporters is not None:
106             for name, reporter in agent_reporters.items():
107                 self._new_agent_reporter(name, reporter)
108 
109         if tables is not None:
110             for name, columns in tables.items():
111                 self._new_table(name, columns)
112 
113     def _new_model_reporter(self, name, reporter):
114         """Add a new model-level reporter to collect.
115 
116         Args:
117             name: Name of the model-level variable to collect.
118             reporter: Attribute string, or function object that returns the
119                       variable when given a model instance.
120         """
121         self.model_reporters[name] = reporter
122         self.model_vars[name] = []
123 
124     def _new_agent_reporter(self, name, reporter):
125         """Add a new agent-level reporter to collect.
126 
127         Args:
128             name: Name of the agent-level variable to collect.
129             reporter: Attribute string, or function object that returns the
130                       variable when given a model instance.
131         """
132         if type(reporter) is str:
133             attribute_name = reporter
134 
135             def reporter(agent):
136                 return getattr(agent, attribute_name, None)
137 
138             reporter.attribute_name = attribute_name
139         self.agent_reporters[name] = reporter
140 
141     def _new_table(self, table_name, table_columns):
142         """Add a new table that objects can write to.
143 
144         Args:
145             table_name: Name of the new table.
146             table_columns: List of columns to add to the table.
147         """
148         new_table = {column: [] for column in table_columns}
149         self.tables[table_name] = new_table
150 
151     def _record_agents(self, model):
152         """Record agents data in a mapping of functions and agents."""
153         rep_funcs = self.agent_reporters.values()
154         if all(hasattr(rep, "attribute_name") for rep in rep_funcs):
155             prefix = ["model.schedule.steps", "unique_id"]
156             attributes = [func.attribute_name for func in rep_funcs]
157             get_reports = attrgetter(*prefix + attributes)
158         else:
159 
160             def get_reports(agent):
161                 _prefix = (agent.model.schedule.steps, agent.unique_id)
162                 reports = tuple(rep(agent) for rep in rep_funcs)
163                 return _prefix + reports
164 
165         agent_records = map(get_reports, model.schedule.agents)
166         return agent_records
167 
168     def collect(self, model):
169         """Collect all the data for the given model object."""
170         if self.model_reporters:
171             for var, reporter in self.model_reporters.items():
172                 # Check if Lambda operator
173                 if isinstance(reporter, types.LambdaType):
174                     self.model_vars[var].append(reporter(model))
175                 # Check if model attribute
176                 elif isinstance(reporter, str):
177                     self.model_vars[var].append(getattr(model, reporter, None))
178                 # Check if function with arguments
179                 elif isinstance(reporter, list):
180                     self.model_vars[var].append(reporter[0](*reporter[1]))
181                 # TODO: Check if method of a class, as of now it is assumed
182                 # implicitly if the other checks fail.
183                 else:
184                     self.model_vars[var].append(reporter())
185 
186         if self.agent_reporters:
187             agent_records = self._record_agents(model)
188             self._agent_records[model.schedule.steps] = list(agent_records)
189 
190     def add_table_row(self, table_name, row, ignore_missing=False):
191         """Add a row dictionary to a specific table.
192 
193         Args:
194             table_name: Name of the table to append a row to.
195             row: A dictionary of the form {column_name: value...}
196             ignore_missing: If True, fill any missing columns with Nones;
197                             if False, throw an error if any columns are missing
198         """
199         if table_name not in self.tables:
200             raise Exception("Table does not exist.")
201 
202         for column in self.tables[table_name]:
203             if column in row:
204                 self.tables[table_name][column].append(row[column])
205             elif ignore_missing:
206                 self.tables[table_name][column].append(None)
207             else:
208                 raise Exception("Could not insert row with missing column")
209 
210     def get_model_vars_dataframe(self):
211         """Create a pandas DataFrame from the model variables.
212 
213         The DataFrame has one column for each model variable, and the index is
214         (implicitly) the model tick.
215         """
216         # Check if self.model_reporters dictionary is empty, if so raise warning
217         if not self.model_reporters:
218             raise UserWarning(
219                 "No model reporters have been defined in the DataCollector, returning empty DataFrame."
220             )
221 
222         return pd.DataFrame(self.model_vars)
223 
224     def get_agent_vars_dataframe(self):
225         """Create a pandas DataFrame from the agent variables.
226 
227         The DataFrame has one column for each variable, with two additional
228         columns for tick and agent_id.
229         """
230         # Check if self.agent_reporters dictionary is empty, if so raise warning
231         if not self.agent_reporters:
232             raise UserWarning(
233                 "No agent reporters have been defined in the DataCollector, returning empty DataFrame."
234             )
235 
236         all_records = itertools.chain.from_iterable(self._agent_records.values())
237         rep_names = list(self.agent_reporters)
238 
239         df = pd.DataFrame.from_records(
240             data=all_records,
241             columns=["Step", "AgentID", *rep_names],
242             index=["Step", "AgentID"],
243         )
244         return df
245 
246     def get_table_dataframe(self, table_name):
247         """Create a pandas DataFrame from a particular table.
248 
249         Args:
250             table_name: The name of the table to convert.
251         """
252         if table_name not in self.tables:
253             raise Exception("No such table.")
254         return pd.DataFrame(self.tables[table_name])
255
```

Path: `mesa/model.py`
Content:
```
1 """
2 The model class for Mesa framework.
3 
4 Core Objects: Model
5 """
6 # Mypy; for the `|` operator purpose
7 # Remove this __future__ import once the oldest supported Python is 3.10
8 from __future__ import annotations
9 
10 import random
11 
12 # mypy
13 from typing import Any
14 
15 from mesa.datacollection import DataCollector
16 
17 
18 class Model:
19     """Base class for models."""
20 
21     def __new__(cls, *args: Any, **kwargs: Any) -> Any:
22         """Create a new model object and instantiate its RNG automatically."""
23         obj = object.__new__(cls)
24         obj._seed = kwargs.get("seed", None)
25         obj.random = random.Random(obj._seed)
26         return obj
27 
28     def __init__(self, *args: Any, **kwargs: Any) -> None:
29         """Create a new model. Overload this method with the actual code to
30         start the model.
31 
32         Attributes:
33             schedule: schedule object
34             running: a bool indicating if the model should continue running
35         """
36 
37         self.running = True
38         self.schedule = None
39         self.current_id = 0
40 
41     def run_model(self) -> None:
42         """Run the model until the end condition is reached. Overload as
43         needed.
44         """
45         while self.running:
46             self.step()
47 
48     def step(self) -> None:
49         """A single step. Fill in here."""
50 
51     def next_id(self) -> int:
52         """Return the next unique ID for agents, increment current_id"""
53         self.current_id += 1
54         return self.current_id
55 
56     def reset_randomizer(self, seed: int | None = None) -> None:
57         """Reset the model random number generator.
58 
59         Args:
60             seed: A new seed for the RNG; if None, reset using the current seed
61         """
62 
63         if seed is None:
64             seed = self._seed
65         self.random.seed(seed)
66         self._seed = seed
67 
68     def initialize_data_collector(
69         self, model_reporters=None, agent_reporters=None, tables=None
70     ) -> None:
71         if not hasattr(self, "schedule") or self.schedule is None:
72             raise RuntimeError(
73                 "You must initialize the scheduler (self.schedule) before initializing the data collector."
74             )
75         if self.schedule.get_agent_count() == 0:
76             raise RuntimeError(
77                 "You must add agents to the scheduler before initializing the data collector."
78             )
79         self.datacollector = DataCollector(
80             model_reporters=model_reporters,
81             agent_reporters=agent_reporters,
82             tables=tables,
83         )
84         # Collect data for the first time during initialization.
85         self.datacollector.collect(self)
86
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mesa/datacollection.py b/mesa/datacollection.py
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -51,7 +51,13 @@
     one and stores the results.
     """
 
-    def __init__(self, model_reporters=None, agent_reporters=None, tables=None):
+    def __init__(
+        self,
+        model_reporters=None,
+        agent_reporters=None,
+        tables=None,
+        exclude_none_values=False,
+    ):
         """Instantiate a DataCollector with lists of model and agent reporters.
         Both model_reporters and agent_reporters accept a dictionary mapping a
         variable name to either an attribute name, or a method.
@@ -74,6 +80,8 @@
             model_reporters: Dictionary of reporter names and attributes/funcs
             agent_reporters: Dictionary of reporter names and attributes/funcs.
             tables: Dictionary of table names to lists of column names.
+            exclude_none_values: Boolean of whether to drop records which values
+            are None, in the final result.
 
         Notes:
             If you want to pickle your model you must not use lambda functions.
@@ -97,6 +105,7 @@
         self.model_vars = {}
         self._agent_records = {}
         self.tables = {}
+        self.exclude_none_values = exclude_none_values
 
         if model_reporters is not None:
             for name, reporter in model_reporters.items():
@@ -151,7 +160,23 @@
     def _record_agents(self, model):
         """Record agents data in a mapping of functions and agents."""
         rep_funcs = self.agent_reporters.values()
+        if self.exclude_none_values:
+            # Drop records which values are None.
+
+            def get_reports(agent):
+                _prefix = (agent.model.schedule.steps, agent.unique_id)
+                reports = (rep(agent) for rep in rep_funcs)
+                reports_without_none = tuple(r for r in reports if r is not None)
+                if len(reports_without_none) == 0:
+                    return None
+                return _prefix + reports_without_none
+
+            agent_records = (get_reports(agent) for agent in model.schedule.agents)
+            agent_records_without_none = (r for r in agent_records if r is not None)
+            return agent_records_without_none
+
         if all(hasattr(rep, "attribute_name") for rep in rep_funcs):
+            # This branch is for performance optimization purpose.
             prefix = ["model.schedule.steps", "unique_id"]
             attributes = [func.attribute_name for func in rep_funcs]
             get_reports = attrgetter(*prefix + attributes)
diff --git a/mesa/model.py b/mesa/model.py
--- a/mesa/model.py
+++ b/mesa/model.py
@@ -66,7 +66,11 @@
         self._seed = seed
 
     def initialize_data_collector(
-        self, model_reporters=None, agent_reporters=None, tables=None
+        self,
+        model_reporters=None,
+        agent_reporters=None,
+        tables=None,
+        exclude_none_values=False,
     ) -> None:
         if not hasattr(self, "schedule") or self.schedule is None:
             raise RuntimeError(
@@ -80,6 +84,7 @@
             model_reporters=model_reporters,
             agent_reporters=agent_reporters,
             tables=tables,
+            exclude_none_values=exclude_none_values,
         )
         # Collect data for the first time during initialization.
         self.datacollector.collect(self)

{"golden_diff": "diff --git a/mesa/datacollection.py b/mesa/datacollection.py\n--- a/mesa/datacollection.py\n+++ b/mesa/datacollection.py\n@@ -51,7 +51,13 @@\n     one and stores the results.\n     \"\"\"\n \n-    def __init__(self, model_reporters=None, agent_reporters=None, tables=None):\n+    def __init__(\n+        self,\n+        model_reporters=None,\n+        agent_reporters=None,\n+        tables=None,\n+        exclude_none_values=False,\n+    ):\n         \"\"\"Instantiate a DataCollector with lists of model and agent reporters.\n         Both model_reporters and agent_reporters accept a dictionary mapping a\n         variable name to either an attribute name, or a method.\n@@ -74,6 +80,8 @@\n             model_reporters: Dictionary of reporter names and attributes/funcs\n             agent_reporters: Dictionary of reporter names and attributes/funcs.\n             tables: Dictionary of table names to lists of column names.\n+            exclude_none_values: Boolean of whether to drop records which values\n+            are None, in the final result.\n \n         Notes:\n             If you want to pickle your model you must not use lambda functions.\n@@ -97,6 +105,7 @@\n         self.model_vars = {}\n         self._agent_records = {}\n         self.tables = {}\n+        self.exclude_none_values = exclude_none_values\n \n         if model_reporters is not None:\n             for name, reporter in model_reporters.items():\n@@ -151,7 +160,23 @@\n     def _record_agents(self, model):\n         \"\"\"Record agents data in a mapping of functions and agents.\"\"\"\n         rep_funcs = self.agent_reporters.values()\n+        if self.exclude_none_values:\n+            # Drop records which values are None.\n+\n+            def get_reports(agent):\n+                _prefix = (agent.model.schedule.steps, agent.unique_id)\n+                reports = (rep(agent) for rep in rep_funcs)\n+                reports_without_none = tuple(r for r in reports if r is not None)\n+                if len(reports_without_none) == 0:\n+                    return None\n+                return _prefix + reports_without_none\n+\n+            agent_records = (get_reports(agent) for agent in model.schedule.agents)\n+            agent_records_without_none = (r for r in agent_records if r is not None)\n+            return agent_records_without_none\n+\n         if all(hasattr(rep, \"attribute_name\") for rep in rep_funcs):\n+            # This branch is for performance optimization purpose.\n             prefix = [\"model.schedule.steps\", \"unique_id\"]\n             attributes = [func.attribute_name for func in rep_funcs]\n             get_reports = attrgetter(*prefix + attributes)\ndiff --git a/mesa/model.py b/mesa/model.py\n--- a/mesa/model.py\n+++ b/mesa/model.py\n@@ -66,7 +66,11 @@\n         self._seed = seed\n \n     def initialize_data_collector(\n-        self, model_reporters=None, agent_reporters=None, tables=None\n+        self,\n+        model_reporters=None,\n+        agent_reporters=None,\n+        tables=None,\n+        exclude_none_values=False,\n     ) -> None:\n         if not hasattr(self, \"schedule\") or self.schedule is None:\n             raise RuntimeError(\n@@ -80,6 +84,7 @@\n             model_reporters=model_reporters,\n             agent_reporters=agent_reporters,\n             tables=tables,\n+            exclude_none_values=exclude_none_values,\n         )\n         # Collect data for the first time during initialization.\n         self.datacollector.collect(self)\n", "issue": "Feature Request: Agent DataCollection Can't Handle Different Attributes in ActivationByType \n**Describe the Feature**\r\nReceive attribute error when running an agent reporter through datacollector when using RandomActivationByType \r\n\r\n**Desired behavior**\r\nI can collect against different attributes based on agent type\r\n\r\n**To Reproduce**\r\nCreate a model with different agent types and attributes and try to collect against that agent attribute\r\n\r\n**Additional context**\r\nFeature would need to update at line 170 of datacollection.py `agent_records = map(get_reports, model.schedule.agents)`\r\n\n", "before_files": [{"content": "\"\"\"\nMesa Data Collection Module\n===========================\n\nDataCollector is meant to provide a simple, standard way to collect data\ngenerated by a Mesa model. It collects three types of data: model-level data,\nagent-level data, and tables.\n\nA DataCollector is instantiated with two dictionaries of reporter names and\nassociated variable names or functions for each, one for model-level data and\none for agent-level data; a third dictionary provides table names and columns.\nVariable names are converted into functions which retrieve attributes of that\nname.\n\nWhen the collect() method is called, each model-level function is called, with\nthe model as the argument, and the results associated with the relevant\nvariable. Then the agent-level functions are called on each agent in the model\nscheduler.\n\nAdditionally, other objects can write directly to tables by passing in an\nappropriate dictionary object for a table row.\n\nThe DataCollector then stores the data it collects in dictionaries:\n    * model_vars maps each reporter to a list of its values\n    * tables maps each table to a dictionary, with each column as a key with a\n      list as its value.\n    * _agent_records maps each model step to a list of each agents id\n      and its values.\n\nFinally, DataCollector can create a pandas DataFrame from each collection.\n\nThe default DataCollector here makes several assumptions:\n    * The model has a schedule object called 'schedule'\n    * The schedule has an agent list called agents\n    * For collecting agent-level variables, agents must have a unique_id\n\"\"\"\nimport itertools\nimport types\nfrom operator import attrgetter\n\nimport pandas as pd\n\n\nclass DataCollector:\n    \"\"\"Class for collecting data generated by a Mesa model.\n\n    A DataCollector is instantiated with dictionaries of names of model- and\n    agent-level variables to collect, associated with attribute names or\n    functions which actually collect them. When the collect(...) method is\n    called, it collects these attributes and executes these functions one by\n    one and stores the results.\n    \"\"\"\n\n    def __init__(self, model_reporters=None, agent_reporters=None, tables=None):\n        \"\"\"Instantiate a DataCollector with lists of model and agent reporters.\n        Both model_reporters and agent_reporters accept a dictionary mapping a\n        variable name to either an attribute name, or a method.\n        For example, if there was only one model-level reporter for number of\n        agents, it might look like:\n            {\"agent_count\": lambda m: m.schedule.get_agent_count() }\n        If there was only one agent-level reporter (e.g. the agent's energy),\n        it might look like this:\n            {\"energy\": \"energy\"}\n        or like this:\n            {\"energy\": lambda a: a.energy}\n\n        The tables arg accepts a dictionary mapping names of tables to lists of\n        columns. For example, if we want to allow agents to write their age\n        when they are destroyed (to keep track of lifespans), it might look\n        like:\n            {\"Lifespan\": [\"unique_id\", \"age\"]}\n\n        Args:\n            model_reporters: Dictionary of reporter names and attributes/funcs\n            agent_reporters: Dictionary of reporter names and attributes/funcs.\n            tables: Dictionary of table names to lists of column names.\n\n        Notes:\n            If you want to pickle your model you must not use lambda functions.\n            If your model includes a large number of agents, you should *only*\n            use attribute names for the agent reporter, it will be much faster.\n\n            Model reporters can take four types of arguments:\n            lambda like above:\n            {\"agent_count\": lambda m: m.schedule.get_agent_count() }\n            method of a class/instance:\n            {\"agent_count\": self.get_agent_count} # self here is a class instance\n            {\"agent_count\": Model.get_agent_count} # Model here is a class\n            class attributes of a model\n            {\"model_attribute\": \"model_attribute\"}\n            functions with parameters that have placed in a list\n            {\"Model_Function\":[function, [param_1, param_2]]}\n        \"\"\"\n        self.model_reporters = {}\n        self.agent_reporters = {}\n\n        self.model_vars = {}\n        self._agent_records = {}\n        self.tables = {}\n\n        if model_reporters is not None:\n            for name, reporter in model_reporters.items():\n                self._new_model_reporter(name, reporter)\n\n        if agent_reporters is not None:\n            for name, reporter in agent_reporters.items():\n                self._new_agent_reporter(name, reporter)\n\n        if tables is not None:\n            for name, columns in tables.items():\n                self._new_table(name, columns)\n\n    def _new_model_reporter(self, name, reporter):\n        \"\"\"Add a new model-level reporter to collect.\n\n        Args:\n            name: Name of the model-level variable to collect.\n            reporter: Attribute string, or function object that returns the\n                      variable when given a model instance.\n        \"\"\"\n        self.model_reporters[name] = reporter\n        self.model_vars[name] = []\n\n    def _new_agent_reporter(self, name, reporter):\n        \"\"\"Add a new agent-level reporter to collect.\n\n        Args:\n            name: Name of the agent-level variable to collect.\n            reporter: Attribute string, or function object that returns the\n                      variable when given a model instance.\n        \"\"\"\n        if type(reporter) is str:\n            attribute_name = reporter\n\n            def reporter(agent):\n                return getattr(agent, attribute_name, None)\n\n            reporter.attribute_name = attribute_name\n        self.agent_reporters[name] = reporter\n\n    def _new_table(self, table_name, table_columns):\n        \"\"\"Add a new table that objects can write to.\n\n        Args:\n            table_name: Name of the new table.\n            table_columns: List of columns to add to the table.\n        \"\"\"\n        new_table = {column: [] for column in table_columns}\n        self.tables[table_name] = new_table\n\n    def _record_agents(self, model):\n        \"\"\"Record agents data in a mapping of functions and agents.\"\"\"\n        rep_funcs = self.agent_reporters.values()\n        if all(hasattr(rep, \"attribute_name\") for rep in rep_funcs):\n            prefix = [\"model.schedule.steps\", \"unique_id\"]\n            attributes = [func.attribute_name for func in rep_funcs]\n            get_reports = attrgetter(*prefix + attributes)\n        else:\n\n            def get_reports(agent):\n                _prefix = (agent.model.schedule.steps, agent.unique_id)\n                reports = tuple(rep(agent) for rep in rep_funcs)\n                return _prefix + reports\n\n        agent_records = map(get_reports, model.schedule.agents)\n        return agent_records\n\n    def collect(self, model):\n        \"\"\"Collect all the data for the given model object.\"\"\"\n        if self.model_reporters:\n            for var, reporter in self.model_reporters.items():\n                # Check if Lambda operator\n                if isinstance(reporter, types.LambdaType):\n                    self.model_vars[var].append(reporter(model))\n                # Check if model attribute\n                elif isinstance(reporter, str):\n                    self.model_vars[var].append(getattr(model, reporter, None))\n                # Check if function with arguments\n                elif isinstance(reporter, list):\n                    self.model_vars[var].append(reporter[0](*reporter[1]))\n                # TODO: Check if method of a class, as of now it is assumed\n                # implicitly if the other checks fail.\n                else:\n                    self.model_vars[var].append(reporter())\n\n        if self.agent_reporters:\n            agent_records = self._record_agents(model)\n            self._agent_records[model.schedule.steps] = list(agent_records)\n\n    def add_table_row(self, table_name, row, ignore_missing=False):\n        \"\"\"Add a row dictionary to a specific table.\n\n        Args:\n            table_name: Name of the table to append a row to.\n            row: A dictionary of the form {column_name: value...}\n            ignore_missing: If True, fill any missing columns with Nones;\n                            if False, throw an error if any columns are missing\n        \"\"\"\n        if table_name not in self.tables:\n            raise Exception(\"Table does not exist.\")\n\n        for column in self.tables[table_name]:\n            if column in row:\n                self.tables[table_name][column].append(row[column])\n            elif ignore_missing:\n                self.tables[table_name][column].append(None)\n            else:\n                raise Exception(\"Could not insert row with missing column\")\n\n    def get_model_vars_dataframe(self):\n        \"\"\"Create a pandas DataFrame from the model variables.\n\n        The DataFrame has one column for each model variable, and the index is\n        (implicitly) the model tick.\n        \"\"\"\n        # Check if self.model_reporters dictionary is empty, if so raise warning\n        if not self.model_reporters:\n            raise UserWarning(\n                \"No model reporters have been defined in the DataCollector, returning empty DataFrame.\"\n            )\n\n        return pd.DataFrame(self.model_vars)\n\n    def get_agent_vars_dataframe(self):\n        \"\"\"Create a pandas DataFrame from the agent variables.\n\n        The DataFrame has one column for each variable, with two additional\n        columns for tick and agent_id.\n        \"\"\"\n        # Check if self.agent_reporters dictionary is empty, if so raise warning\n        if not self.agent_reporters:\n            raise UserWarning(\n                \"No agent reporters have been defined in the DataCollector, returning empty DataFrame.\"\n            )\n\n        all_records = itertools.chain.from_iterable(self._agent_records.values())\n        rep_names = list(self.agent_reporters)\n\n        df = pd.DataFrame.from_records(\n            data=all_records,\n            columns=[\"Step\", \"AgentID\", *rep_names],\n            index=[\"Step\", \"AgentID\"],\n        )\n        return df\n\n    def get_table_dataframe(self, table_name):\n        \"\"\"Create a pandas DataFrame from a particular table.\n\n        Args:\n            table_name: The name of the table to convert.\n        \"\"\"\n        if table_name not in self.tables:\n            raise Exception(\"No such table.\")\n        return pd.DataFrame(self.tables[table_name])\n", "path": "mesa/datacollection.py"}, {"content": "\"\"\"\nThe model class for Mesa framework.\n\nCore Objects: Model\n\"\"\"\n# Mypy; for the `|` operator purpose\n# Remove this __future__ import once the oldest supported Python is 3.10\nfrom __future__ import annotations\n\nimport random\n\n# mypy\nfrom typing import Any\n\nfrom mesa.datacollection import DataCollector\n\n\nclass Model:\n    \"\"\"Base class for models.\"\"\"\n\n    def __new__(cls, *args: Any, **kwargs: Any) -> Any:\n        \"\"\"Create a new model object and instantiate its RNG automatically.\"\"\"\n        obj = object.__new__(cls)\n        obj._seed = kwargs.get(\"seed\", None)\n        obj.random = random.Random(obj._seed)\n        return obj\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Create a new model. Overload this method with the actual code to\n        start the model.\n\n        Attributes:\n            schedule: schedule object\n            running: a bool indicating if the model should continue running\n        \"\"\"\n\n        self.running = True\n        self.schedule = None\n        self.current_id = 0\n\n    def run_model(self) -> None:\n        \"\"\"Run the model until the end condition is reached. Overload as\n        needed.\n        \"\"\"\n        while self.running:\n            self.step()\n\n    def step(self) -> None:\n        \"\"\"A single step. Fill in here.\"\"\"\n\n    def next_id(self) -> int:\n        \"\"\"Return the next unique ID for agents, increment current_id\"\"\"\n        self.current_id += 1\n        return self.current_id\n\n    def reset_randomizer(self, seed: int | None = None) -> None:\n        \"\"\"Reset the model random number generator.\n\n        Args:\n            seed: A new seed for the RNG; if None, reset using the current seed\n        \"\"\"\n\n        if seed is None:\n            seed = self._seed\n        self.random.seed(seed)\n        self._seed = seed\n\n    def initialize_data_collector(\n        self, model_reporters=None, agent_reporters=None, tables=None\n    ) -> None:\n        if not hasattr(self, \"schedule\") or self.schedule is None:\n            raise RuntimeError(\n                \"You must initialize the scheduler (self.schedule) before initializing the data collector.\"\n            )\n        if self.schedule.get_agent_count() == 0:\n            raise RuntimeError(\n                \"You must add agents to the scheduler before initializing the data collector.\"\n            )\n        self.datacollector = DataCollector(\n            model_reporters=model_reporters,\n            agent_reporters=agent_reporters,\n            tables=tables,\n        )\n        # Collect data for the first time during initialization.\n        self.datacollector.collect(self)\n", "path": "mesa/model.py"}], "after_files": [{"content": "\"\"\"\nMesa Data Collection Module\n===========================\n\nDataCollector is meant to provide a simple, standard way to collect data\ngenerated by a Mesa model. It collects three types of data: model-level data,\nagent-level data, and tables.\n\nA DataCollector is instantiated with two dictionaries of reporter names and\nassociated variable names or functions for each, one for model-level data and\none for agent-level data; a third dictionary provides table names and columns.\nVariable names are converted into functions which retrieve attributes of that\nname.\n\nWhen the collect() method is called, each model-level function is called, with\nthe model as the argument, and the results associated with the relevant\nvariable. Then the agent-level functions are called on each agent in the model\nscheduler.\n\nAdditionally, other objects can write directly to tables by passing in an\nappropriate dictionary object for a table row.\n\nThe DataCollector then stores the data it collects in dictionaries:\n    * model_vars maps each reporter to a list of its values\n    * tables maps each table to a dictionary, with each column as a key with a\n      list as its value.\n    * _agent_records maps each model step to a list of each agents id\n      and its values.\n\nFinally, DataCollector can create a pandas DataFrame from each collection.\n\nThe default DataCollector here makes several assumptions:\n    * The model has a schedule object called 'schedule'\n    * The schedule has an agent list called agents\n    * For collecting agent-level variables, agents must have a unique_id\n\"\"\"\nimport itertools\nimport types\nfrom operator import attrgetter\n\nimport pandas as pd\n\n\nclass DataCollector:\n    \"\"\"Class for collecting data generated by a Mesa model.\n\n    A DataCollector is instantiated with dictionaries of names of model- and\n    agent-level variables to collect, associated with attribute names or\n    functions which actually collect them. When the collect(...) method is\n    called, it collects these attributes and executes these functions one by\n    one and stores the results.\n    \"\"\"\n\n    def __init__(\n        self,\n        model_reporters=None,\n        agent_reporters=None,\n        tables=None,\n        exclude_none_values=False,\n    ):\n        \"\"\"Instantiate a DataCollector with lists of model and agent reporters.\n        Both model_reporters and agent_reporters accept a dictionary mapping a\n        variable name to either an attribute name, or a method.\n        For example, if there was only one model-level reporter for number of\n        agents, it might look like:\n            {\"agent_count\": lambda m: m.schedule.get_agent_count() }\n        If there was only one agent-level reporter (e.g. the agent's energy),\n        it might look like this:\n            {\"energy\": \"energy\"}\n        or like this:\n            {\"energy\": lambda a: a.energy}\n\n        The tables arg accepts a dictionary mapping names of tables to lists of\n        columns. For example, if we want to allow agents to write their age\n        when they are destroyed (to keep track of lifespans), it might look\n        like:\n            {\"Lifespan\": [\"unique_id\", \"age\"]}\n\n        Args:\n            model_reporters: Dictionary of reporter names and attributes/funcs\n            agent_reporters: Dictionary of reporter names and attributes/funcs.\n            tables: Dictionary of table names to lists of column names.\n            exclude_none_values: Boolean of whether to drop records which values\n            are None, in the final result.\n\n        Notes:\n            If you want to pickle your model you must not use lambda functions.\n            If your model includes a large number of agents, you should *only*\n            use attribute names for the agent reporter, it will be much faster.\n\n            Model reporters can take four types of arguments:\n            lambda like above:\n            {\"agent_count\": lambda m: m.schedule.get_agent_count() }\n            method of a class/instance:\n            {\"agent_count\": self.get_agent_count} # self here is a class instance\n            {\"agent_count\": Model.get_agent_count} # Model here is a class\n            class attributes of a model\n            {\"model_attribute\": \"model_attribute\"}\n            functions with parameters that have placed in a list\n            {\"Model_Function\":[function, [param_1, param_2]]}\n        \"\"\"\n        self.model_reporters = {}\n        self.agent_reporters = {}\n\n        self.model_vars = {}\n        self._agent_records = {}\n        self.tables = {}\n        self.exclude_none_values = exclude_none_values\n\n        if model_reporters is not None:\n            for name, reporter in model_reporters.items():\n                self._new_model_reporter(name, reporter)\n\n        if agent_reporters is not None:\n            for name, reporter in agent_reporters.items():\n                self._new_agent_reporter(name, reporter)\n\n        if tables is not None:\n            for name, columns in tables.items():\n                self._new_table(name, columns)\n\n    def _new_model_reporter(self, name, reporter):\n        \"\"\"Add a new model-level reporter to collect.\n\n        Args:\n            name: Name of the model-level variable to collect.\n            reporter: Attribute string, or function object that returns the\n                      variable when given a model instance.\n        \"\"\"\n        self.model_reporters[name] = reporter\n        self.model_vars[name] = []\n\n    def _new_agent_reporter(self, name, reporter):\n        \"\"\"Add a new agent-level reporter to collect.\n\n        Args:\n            name: Name of the agent-level variable to collect.\n            reporter: Attribute string, or function object that returns the\n                      variable when given a model instance.\n        \"\"\"\n        if type(reporter) is str:\n            attribute_name = reporter\n\n            def reporter(agent):\n                return getattr(agent, attribute_name, None)\n\n            reporter.attribute_name = attribute_name\n        self.agent_reporters[name] = reporter\n\n    def _new_table(self, table_name, table_columns):\n        \"\"\"Add a new table that objects can write to.\n\n        Args:\n            table_name: Name of the new table.\n            table_columns: List of columns to add to the table.\n        \"\"\"\n        new_table = {column: [] for column in table_columns}\n        self.tables[table_name] = new_table\n\n    def _record_agents(self, model):\n        \"\"\"Record agents data in a mapping of functions and agents.\"\"\"\n        rep_funcs = self.agent_reporters.values()\n        if self.exclude_none_values:\n            # Drop records which values are None.\n\n            def get_reports(agent):\n                _prefix = (agent.model.schedule.steps, agent.unique_id)\n                reports = (rep(agent) for rep in rep_funcs)\n                reports_without_none = tuple(r for r in reports if r is not None)\n                if len(reports_without_none) == 0:\n                    return None\n                return _prefix + reports_without_none\n\n            agent_records = (get_reports(agent) for agent in model.schedule.agents)\n            agent_records_without_none = (r for r in agent_records if r is not None)\n            return agent_records_without_none\n\n        if all(hasattr(rep, \"attribute_name\") for rep in rep_funcs):\n            # This branch is for performance optimization purpose.\n            prefix = [\"model.schedule.steps\", \"unique_id\"]\n            attributes = [func.attribute_name for func in rep_funcs]\n            get_reports = attrgetter(*prefix + attributes)\n        else:\n\n            def get_reports(agent):\n                _prefix = (agent.model.schedule.steps, agent.unique_id)\n                reports = tuple(rep(agent) for rep in rep_funcs)\n                return _prefix + reports\n\n        agent_records = map(get_reports, model.schedule.agents)\n        return agent_records\n\n    def collect(self, model):\n        \"\"\"Collect all the data for the given model object.\"\"\"\n        if self.model_reporters:\n            for var, reporter in self.model_reporters.items():\n                # Check if Lambda operator\n                if isinstance(reporter, types.LambdaType):\n                    self.model_vars[var].append(reporter(model))\n                # Check if model attribute\n                elif isinstance(reporter, str):\n                    self.model_vars[var].append(getattr(model, reporter, None))\n                # Check if function with arguments\n                elif isinstance(reporter, list):\n                    self.model_vars[var].append(reporter[0](*reporter[1]))\n                # TODO: Check if method of a class, as of now it is assumed\n                # implicitly if the other checks fail.\n                else:\n                    self.model_vars[var].append(reporter())\n\n        if self.agent_reporters:\n            agent_records = self._record_agents(model)\n            self._agent_records[model.schedule.steps] = list(agent_records)\n\n    def add_table_row(self, table_name, row, ignore_missing=False):\n        \"\"\"Add a row dictionary to a specific table.\n\n        Args:\n            table_name: Name of the table to append a row to.\n            row: A dictionary of the form {column_name: value...}\n            ignore_missing: If True, fill any missing columns with Nones;\n                            if False, throw an error if any columns are missing\n        \"\"\"\n        if table_name not in self.tables:\n            raise Exception(\"Table does not exist.\")\n\n        for column in self.tables[table_name]:\n            if column in row:\n                self.tables[table_name][column].append(row[column])\n            elif ignore_missing:\n                self.tables[table_name][column].append(None)\n            else:\n                raise Exception(\"Could not insert row with missing column\")\n\n    def get_model_vars_dataframe(self):\n        \"\"\"Create a pandas DataFrame from the model variables.\n\n        The DataFrame has one column for each model variable, and the index is\n        (implicitly) the model tick.\n        \"\"\"\n        # Check if self.model_reporters dictionary is empty, if so raise warning\n        if not self.model_reporters:\n            raise UserWarning(\n                \"No model reporters have been defined in the DataCollector, returning empty DataFrame.\"\n            )\n\n        return pd.DataFrame(self.model_vars)\n\n    def get_agent_vars_dataframe(self):\n        \"\"\"Create a pandas DataFrame from the agent variables.\n\n        The DataFrame has one column for each variable, with two additional\n        columns for tick and agent_id.\n        \"\"\"\n        # Check if self.agent_reporters dictionary is empty, if so raise warning\n        if not self.agent_reporters:\n            raise UserWarning(\n                \"No agent reporters have been defined in the DataCollector, returning empty DataFrame.\"\n            )\n\n        all_records = itertools.chain.from_iterable(self._agent_records.values())\n        rep_names = list(self.agent_reporters)\n\n        df = pd.DataFrame.from_records(\n            data=all_records,\n            columns=[\"Step\", \"AgentID\", *rep_names],\n            index=[\"Step\", \"AgentID\"],\n        )\n        return df\n\n    def get_table_dataframe(self, table_name):\n        \"\"\"Create a pandas DataFrame from a particular table.\n\n        Args:\n            table_name: The name of the table to convert.\n        \"\"\"\n        if table_name not in self.tables:\n            raise Exception(\"No such table.\")\n        return pd.DataFrame(self.tables[table_name])\n", "path": "mesa/datacollection.py"}, {"content": "\"\"\"\nThe model class for Mesa framework.\n\nCore Objects: Model\n\"\"\"\n# Mypy; for the `|` operator purpose\n# Remove this __future__ import once the oldest supported Python is 3.10\nfrom __future__ import annotations\n\nimport random\n\n# mypy\nfrom typing import Any\n\nfrom mesa.datacollection import DataCollector\n\n\nclass Model:\n    \"\"\"Base class for models.\"\"\"\n\n    def __new__(cls, *args: Any, **kwargs: Any) -> Any:\n        \"\"\"Create a new model object and instantiate its RNG automatically.\"\"\"\n        obj = object.__new__(cls)\n        obj._seed = kwargs.get(\"seed\", None)\n        obj.random = random.Random(obj._seed)\n        return obj\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Create a new model. Overload this method with the actual code to\n        start the model.\n\n        Attributes:\n            schedule: schedule object\n            running: a bool indicating if the model should continue running\n        \"\"\"\n\n        self.running = True\n        self.schedule = None\n        self.current_id = 0\n\n    def run_model(self) -> None:\n        \"\"\"Run the model until the end condition is reached. Overload as\n        needed.\n        \"\"\"\n        while self.running:\n            self.step()\n\n    def step(self) -> None:\n        \"\"\"A single step. Fill in here.\"\"\"\n\n    def next_id(self) -> int:\n        \"\"\"Return the next unique ID for agents, increment current_id\"\"\"\n        self.current_id += 1\n        return self.current_id\n\n    def reset_randomizer(self, seed: int | None = None) -> None:\n        \"\"\"Reset the model random number generator.\n\n        Args:\n            seed: A new seed for the RNG; if None, reset using the current seed\n        \"\"\"\n\n        if seed is None:\n            seed = self._seed\n        self.random.seed(seed)\n        self._seed = seed\n\n    def initialize_data_collector(\n        self,\n        model_reporters=None,\n        agent_reporters=None,\n        tables=None,\n        exclude_none_values=False,\n    ) -> None:\n        if not hasattr(self, \"schedule\") or self.schedule is None:\n            raise RuntimeError(\n                \"You must initialize the scheduler (self.schedule) before initializing the data collector.\"\n            )\n        if self.schedule.get_agent_count() == 0:\n            raise RuntimeError(\n                \"You must add agents to the scheduler before initializing the data collector.\"\n            )\n        self.datacollector = DataCollector(\n            model_reporters=model_reporters,\n            agent_reporters=agent_reporters,\n            tables=tables,\n            exclude_none_values=exclude_none_values,\n        )\n        # Collect data for the first time during initialization.\n        self.datacollector.collect(self)\n", "path": "mesa/model.py"}]}