We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Issue with data collection of PropertyLayer objects
**Describe the bug**
When using a PropertyLayer class with DataCollector, the data is not captured properly at each model step.  Specifically, for each model step, the grid of values for the PropertyLayer instance are stored only as they appear in the final model step, i.e. all steps store the same array of values.

**Expected behavior**
The DataCollector should store at each step the value of the PropertyLayer at that step as a model variable.  This would allow visualization of the distribution of the variable at each step of the model to examine, for example, the accumulation of pollution in the Sugarscape model.

**To Reproduce**
The toy model below reproduces the issue.  Check the 'Pollution' column of the model_results object to see the problem.  Minimally, one would expect a grid of 0's for the first model step, which is not the case here either.

```
import mesa

class testAgent(mesa.Agent):
    def __init__(self, unique_id, model, pos):
        super().__init__(unique_id, model)
        self.pos = pos
    def pollute(self):
        self.model.grid.properties["pollution"].modify_cell(self.pos, lambda p: p+1)

class testModel(mesa.Model):
    def __init__(self, width = 20, height = 20, initial_population=200):
        super().__init__()
        self.width = width
        self.height = height
        self.initial_population = initial_population
        
        # initiate activation schedule
        self.schedule = mesa.time.RandomActivationByType(self) #activate different type of agents in different order each time
        # initiate mesa grid class
        self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False)
        #add pollution property layer
        pollution_layer = mesa.space.PropertyLayer(name = "pollution", width = self.width, height = self.height, default_value = 0)
        self.grid.add_property_layer(pollution_layer)
        
        self.datacollector = mesa.DataCollector(model_reporters = {"Pollution": lambda m: m.grid.properties["pollution"].data})
        
        agent_id = 0
        for i in range(self.initial_population):
            # get agent position
            x = self.random.randrange(self.width)
            y = self.random.randrange(self.height)
            agent = testAgent(agent_id, self, (x, y))
            # place agent
            self.grid.place_agent(agent, (x, y))
            self.schedule.add(agent)
            agent_id += 1
    
    def randomizeAgents(self):
        agents_shuffle = list(self.schedule.agents_by_type[testAgent].values())
        self.random.shuffle(agents_shuffle)

        return agents_shuffle
    
    def step(self):
        agents_shuffle = self.randomizeAgents()
        for agent in agents_shuffle:
            agent.pollute()
        
        self.datacollector.collect(self)
       
    def run_model(self, step_count=20):
        for i in range(step_count):
            self.step()
            
model = testModel()
model.run_model()
model_results = model.datacollector.get_model_vars_dataframe()
```

**Additional context**
This should be running on the most recent version of Mesa (my version was updated about a week ago).


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

{"golden_diff": "diff --git a/mesa/datacollection.py b/mesa/datacollection.py\n--- a/mesa/datacollection.py\n+++ b/mesa/datacollection.py\n@@ -36,6 +36,7 @@\n import contextlib\n import itertools\n import types\n+from copy import deepcopy\n from functools import partial\n \n with contextlib.suppress(ImportError):\n@@ -197,17 +198,21 @@\n             for var, reporter in self.model_reporters.items():\n                 # Check if lambda or partial function\n                 if isinstance(reporter, types.LambdaType | partial):\n-                    self.model_vars[var].append(reporter(model))\n+                    # Use deepcopy to store a copy of the data,\n+                    # preventing references from being updated across steps.\n+                    self.model_vars[var].append(deepcopy(reporter(model)))\n                 # Check if model attribute\n                 elif isinstance(reporter, str):\n-                    self.model_vars[var].append(getattr(model, reporter, None))\n+                    self.model_vars[var].append(\n+                        deepcopy(getattr(model, reporter, None))\n+                    )\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+                    self.model_vars[var].append(deepcopy(reporter[0](*reporter[1])))\n+                # Assume it's a callable otherwise (e.g., method)\n+                # TODO: Check if method of a class explicitly\n                 else:\n-                    self.model_vars[var].append(reporter())\n+                    self.model_vars[var].append(deepcopy(reporter()))\n \n         if self.agent_reporters:\n             agent_records = self._record_agents(model)\n", "issue": "Issue with data collection of PropertyLayer objects\n**Describe the bug**\r\nWhen using a PropertyLayer class with DataCollector, the data is not captured properly at each model step.  Specifically, for each model step, the grid of values for the PropertyLayer instance are stored only as they appear in the final model step, i.e. all steps store the same array of values.\r\n\r\n**Expected behavior**\r\nThe DataCollector should store at each step the value of the PropertyLayer at that step as a model variable.  This would allow visualization of the distribution of the variable at each step of the model to examine, for example, the accumulation of pollution in the Sugarscape model.\r\n\r\n**To Reproduce**\r\nThe toy model below reproduces the issue.  Check the 'Pollution' column of the model_results object to see the problem.  Minimally, one would expect a grid of 0's for the first model step, which is not the case here either.\r\n\r\n```\r\nimport mesa\r\n\r\nclass testAgent(mesa.Agent):\r\n    def __init__(self, unique_id, model, pos):\r\n        super().__init__(unique_id, model)\r\n        self.pos = pos\r\n    def pollute(self):\r\n        self.model.grid.properties[\"pollution\"].modify_cell(self.pos, lambda p: p+1)\r\n\r\nclass testModel(mesa.Model):\r\n    def __init__(self, width = 20, height = 20, initial_population=200):\r\n        super().__init__()\r\n        self.width = width\r\n        self.height = height\r\n        self.initial_population = initial_population\r\n        \r\n        # initiate activation schedule\r\n        self.schedule = mesa.time.RandomActivationByType(self) #activate different type of agents in different order each time\r\n        # initiate mesa grid class\r\n        self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False)\r\n        #add pollution property layer\r\n        pollution_layer = mesa.space.PropertyLayer(name = \"pollution\", width = self.width, height = self.height, default_value = 0)\r\n        self.grid.add_property_layer(pollution_layer)\r\n        \r\n        self.datacollector = mesa.DataCollector(model_reporters = {\"Pollution\": lambda m: m.grid.properties[\"pollution\"].data})\r\n        \r\n        agent_id = 0\r\n        for i in range(self.initial_population):\r\n            # get agent position\r\n            x = self.random.randrange(self.width)\r\n            y = self.random.randrange(self.height)\r\n            agent = testAgent(agent_id, self, (x, y))\r\n            # place agent\r\n            self.grid.place_agent(agent, (x, y))\r\n            self.schedule.add(agent)\r\n            agent_id += 1\r\n    \r\n    def randomizeAgents(self):\r\n        agents_shuffle = list(self.schedule.agents_by_type[testAgent].values())\r\n        self.random.shuffle(agents_shuffle)\r\n\r\n        return agents_shuffle\r\n    \r\n    def step(self):\r\n        agents_shuffle = self.randomizeAgents()\r\n        for agent in agents_shuffle:\r\n            agent.pollute()\r\n        \r\n        self.datacollector.collect(self)\r\n       \r\n    def run_model(self, step_count=20):\r\n        for i in range(step_count):\r\n            self.step()\r\n            \r\nmodel = testModel()\r\nmodel.run_model()\r\nmodel_results = model.datacollector.get_model_vars_dataframe()\r\n```\r\n\r\n**Additional context**\r\nThis should be running on the most recent version of Mesa (my version was updated about a week ago).\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.\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 an agent list called agents\n    * For collecting agent-level variables, agents must have a unique_id\n\"\"\"\n\nimport contextlib\nimport itertools\nimport types\nfrom functools import partial\n\nwith contextlib.suppress(ImportError):\n    import 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    ):\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, a function, a method of a class/instance,\n        or a function with parameters placed in a list.\n\n        Model reporters can take four types of arguments:\n        1. Lambda function:\n           {\"agent_count\": lambda m: len(m.agents)}\n        2. 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        3. Class attributes of a model:\n           {\"model_attribute\": \"model_attribute\"}\n        4. Functions with parameters that have been placed in a list:\n           {\"Model_Function\": [function, [param_1, param_2]]}\n\n        Agent reporters can similarly take:\n        1. Attribute name (string) referring to agent's attribute:\n           {\"energy\": \"energy\"}\n        2. Lambda function:\n           {\"energy\": lambda a: a.energy}\n        3. Method of an agent class/instance:\n           {\"agent_action\": self.do_action} # self here is an agent class instance\n           {\"agent_action\": Agent.do_action} # Agent here is a class\n        4. Functions with parameters placed in a list:\n           {\"Agent_Function\": [function, [param_1, param_2]]}\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/methods.\n            agent_reporters: Dictionary of reporter names and attributes/funcs/methods.\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, it is recommended to\n              use attribute names for the agent reporter, as it will be faster.\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, function object, method of a class/instance, or\n                      function with parameters placed in a list that returns the\n                      variable when given an agent instance.\n        \"\"\"\n        # Check if the reporter is an attribute string\n        if isinstance(reporter, str):\n            attribute_name = reporter\n\n            def attr_reporter(agent):\n                return getattr(agent, attribute_name, None)\n\n            reporter = attr_reporter\n\n        # Check if the reporter is a function with arguments placed in a list\n        elif isinstance(reporter, list):\n            func, params = reporter[0], reporter[1]\n\n            def func_with_params(agent):\n                return func(agent, *params)\n\n            reporter = func_with_params\n\n        # For other types (like lambda functions, method of a class/instance),\n        # it's already suitable to be used as a reporter directly.\n\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\n        def get_reports(agent):\n            _prefix = (agent.model._steps, agent.unique_id)\n            reports = tuple(rep(agent) for rep in rep_funcs)\n            return _prefix + reports\n\n        agent_records = map(\n            get_reports,\n            model.schedule.agents\n            if hasattr(model, \"schedule\") and model.schedule is not None\n            else model.agents,\n        )\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 or partial function\n                if isinstance(reporter, types.LambdaType | partial):\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._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"}], "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.\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 an agent list called agents\n    * For collecting agent-level variables, agents must have a unique_id\n\"\"\"\n\nimport contextlib\nimport itertools\nimport types\nfrom copy import deepcopy\nfrom functools import partial\n\nwith contextlib.suppress(ImportError):\n    import 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    ):\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, a function, a method of a class/instance,\n        or a function with parameters placed in a list.\n\n        Model reporters can take four types of arguments:\n        1. Lambda function:\n           {\"agent_count\": lambda m: len(m.agents)}\n        2. 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        3. Class attributes of a model:\n           {\"model_attribute\": \"model_attribute\"}\n        4. Functions with parameters that have been placed in a list:\n           {\"Model_Function\": [function, [param_1, param_2]]}\n\n        Agent reporters can similarly take:\n        1. Attribute name (string) referring to agent's attribute:\n           {\"energy\": \"energy\"}\n        2. Lambda function:\n           {\"energy\": lambda a: a.energy}\n        3. Method of an agent class/instance:\n           {\"agent_action\": self.do_action} # self here is an agent class instance\n           {\"agent_action\": Agent.do_action} # Agent here is a class\n        4. Functions with parameters placed in a list:\n           {\"Agent_Function\": [function, [param_1, param_2]]}\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/methods.\n            agent_reporters: Dictionary of reporter names and attributes/funcs/methods.\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, it is recommended to\n              use attribute names for the agent reporter, as it will be faster.\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, function object, method of a class/instance, or\n                      function with parameters placed in a list that returns the\n                      variable when given an agent instance.\n        \"\"\"\n        # Check if the reporter is an attribute string\n        if isinstance(reporter, str):\n            attribute_name = reporter\n\n            def attr_reporter(agent):\n                return getattr(agent, attribute_name, None)\n\n            reporter = attr_reporter\n\n        # Check if the reporter is a function with arguments placed in a list\n        elif isinstance(reporter, list):\n            func, params = reporter[0], reporter[1]\n\n            def func_with_params(agent):\n                return func(agent, *params)\n\n            reporter = func_with_params\n\n        # For other types (like lambda functions, method of a class/instance),\n        # it's already suitable to be used as a reporter directly.\n\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\n        def get_reports(agent):\n            _prefix = (agent.model._steps, agent.unique_id)\n            reports = tuple(rep(agent) for rep in rep_funcs)\n            return _prefix + reports\n\n        agent_records = map(\n            get_reports,\n            model.schedule.agents\n            if hasattr(model, \"schedule\") and model.schedule is not None\n            else model.agents,\n        )\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 or partial function\n                if isinstance(reporter, types.LambdaType | partial):\n                    # Use deepcopy to store a copy of the data,\n                    # preventing references from being updated across steps.\n                    self.model_vars[var].append(deepcopy(reporter(model)))\n                # Check if model attribute\n                elif isinstance(reporter, str):\n                    self.model_vars[var].append(\n                        deepcopy(getattr(model, reporter, None))\n                    )\n                # Check if function with arguments\n                elif isinstance(reporter, list):\n                    self.model_vars[var].append(deepcopy(reporter[0](*reporter[1])))\n                # Assume it's a callable otherwise (e.g., method)\n                # TODO: Check if method of a class explicitly\n                else:\n                    self.model_vars[var].append(deepcopy(reporter()))\n\n        if self.agent_reporters:\n            agent_records = self._record_agents(model)\n            self._agent_records[model._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"}]}