Repository: projectmesa/mesa
Pull request #576 (2018-08-23 10:26:52)

Faster agent attribute collection
Addresses #575 

This implementation adds a fast-track for collecting agent attributes if all reporters are string-based (no custom or lambda functions). If no string-based reporters are present or for a mixture of reporters it defaults to a only slightly improved version of the current implementation.

I also changed the structure of the class variable agent_vars, to store less data, it specifically stores the agent.unique_id only once and not for every reporter. This breaks code that relies on that variable, although none of the examples do. 

One idea to encourage the use of string-based reporters for agent reporters would be to include a warning if an agent reporter is not string-based and tell users that they could benefit from switching to string-based reporters (or even that support for non string-based reporters will be dropped in a much later version of mesa). 

Hope you like it


Patch:
diff --git a/README.rst b/README.rst
index 73a2cafd..3b0c5337 100644
--- a/README.rst
+++ b/README.rst
@@ -18,7 +18,7 @@ It allows users to quickly create agent-based models using built-in core compone
    :alt: A screenshot of the Schelling Model in Mesa
 
 *Above: A Mesa implementation of the Schelling segregation model,
-being visualized in a browser window and analyzed in a Jupyter
+being visualized in a browser window and analyzed in an IPython
 notebook.*
 
 .. _`Mesa` : https://github.com/projectmesa/mesa/
@@ -30,7 +30,6 @@ Features
 * Modular components
 * Browser-based visualization
 * Built-in tools for analysis
-* Example model library
 
 Using Mesa
 ------------
@@ -47,7 +46,7 @@ You can also use `pip` to install the github version:
 
     $ pip install git+https://github.com/projectmesa/mesa
 
-Take a look at the `examples <https://github.com/projectmesa/mesa/tree/master/examples>`_ folder for sample models demonstrating Mesa features.
+
 
 For more help on using Mesa, check out the following resources:
 
diff --git a/mesa/datacollection.py b/mesa/datacollection.py
index dfa4fa92..18360891 100644
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -36,7 +36,8 @@ The default DataCollector here makes several assumptions:
     * For collecting agent-level variables, agents must have a unique_id
 
 """
-from collections import defaultdict
+import itertools
+from operator import attrgetter
 import pandas as pd
 
 
@@ -83,7 +84,7 @@ class DataCollector:
         self.agent_reporters = {}
 
         self.model_vars = {}
-        self.agent_vars = {}
+        self._agent_records = {}
         self.tables = {}
 
         if model_reporters is not None:
@@ -123,7 +124,6 @@ class DataCollector:
         if type(reporter) is str:
             reporter = self._make_attribute_collector(reporter)
         self.agent_reporters[name] = reporter
-        self.agent_vars[name] = []
 
     def _new_table(self, table_name, table_columns):
         """ Add a new table that objects can write to.
@@ -136,6 +136,21 @@ class DataCollector:
         new_table = {column: [] for column in table_columns}
         self.tables[table_name] = new_table
 
+    def _record_agents(self, model):
+        """ Record agents data in a mapping of functions and agents. """
+        rep_funcs = self.agent_reporters.values()
+        if all([rep.__name__ == 'attr_collector' for rep in rep_funcs]):
+            prefix = ['model.schedule.steps', 'unique_id']
+            attributes = [func.attribute_name for func in rep_funcs]
+            get_reports = attrgetter(*prefix + attributes)
+        else:
+            def get_reports(agent):
+                prefix = (agent.model.schedule.steps, agent.unique_id)
+                reports = tuple(rep(agent) for rep in rep_funcs)
+                return prefix + reports
+        agent_records = map(get_reports, model.schedule.agents)
+        return agent_records
+
     def collect(self, model):
         """ Collect all the data for the given model object. """
         if self.model_reporters:
@@ -143,11 +158,8 @@ class DataCollector:
                 self.model_vars[var].append(reporter(model))
 
         if self.agent_reporters:
-            for var, reporter in self.agent_reporters.items():
-                agent_records = []
-                for agent in model.schedule.agents:
-                    agent_records.append((agent.unique_id, reporter(agent)))
-                self.agent_vars[var].append(agent_records)
+            agent_records = self._record_agents(model)
+            self._agent_records[model.schedule.steps] = list(agent_records)
 
     def add_table_row(self, table_name, row, ignore_missing=False):
         """ Add a row dictionary to a specific table.
@@ -176,6 +188,14 @@ class DataCollector:
         Create a function which collects the value of a named attribute
         '''
 
+        def attribute_name(attr):
+            """Decorator to add attribute name as a function attribute."""
+            def wrapper(func):
+                func.attribute_name = attr
+                return func
+            return wrapper
+
+        @attribute_name(attr)
         def attr_collector(obj):
             return getattr(obj, attr)
 
@@ -197,15 +217,15 @@ class DataCollector:
         columns for tick and agent_id.
 
         """
-        data = defaultdict(dict)
-        for var, records in self.agent_vars.items():
-            for step, entries in enumerate(records):
-                for entry in entries:
-                    agent_id = entry[0]
-                    val = entry[1]
-                    data[(step, agent_id)][var] = val
-        df = pd.DataFrame.from_dict(data, orient="index")
-        df.index.names = ["Step", "AgentID"]
+        all_records = itertools.chain.from_iterable(
+            self._agent_records.values())
+        rep_names = [rep_name for rep_name in self.agent_reporters]
+
+        df = pd.DataFrame.from_records(
+            data=all_records,
+            columns=["Step", "AgentID"] + rep_names,
+            )
+        df = df.set_index(["Step", "AgentID"])
         return df
 
     def get_table_dataframe(self, table_name):
diff --git a/mesa/space.py b/mesa/space.py
index 352b85aa..ee338ea1 100644
--- a/mesa/space.py
+++ b/mesa/space.py
@@ -600,7 +600,7 @@ class ContinuousSpace:
     """ Continuous space where each agent can have an arbitrary position.
 
     Assumes that all agents are point objects, and have a pos property storing
-    their position as an (x, y) tuple. This class uses a numpy array internally
+    their position as an (x, y) tuple. This class uses a MultiGrid internally
     to store agent objects, to speed up neighborhood lookups.
 
     """
@@ -615,6 +615,11 @@ class ContinuousSpace:
             x_min, y_min: (default 0) If provided, set the minimum x and y
                           coordinates for the space. Below them, values loop to
                           the other edge (if torus=True) or raise an exception.
+            grid_width, _height: (default 100) Determine the size of the
+                                 internal storage grid. More cells will slow
+                                 down movement, but speed up neighbor lookup.
+                                 Probably only fiddle with this if one or the
+                                 other is impacting your model's performance.
 
         """
         self.x_min = x_min
diff --git a/mesa/visualization/templates/js/runcontrol.js b/mesa/visualization/templates/js/runcontrol.js
index 7c608737..a27d77e0 100644
--- a/mesa/visualization/templates/js/runcontrol.js
+++ b/mesa/visualization/templates/js/runcontrol.js
@@ -156,9 +156,9 @@ var initGUI = function() {
 
         // Finally render the dropdown and activate choice listeners
         sidebar.append(template.join(''));
-        choiceIdentifiers.forEach(function (id,idx) {
+        choiceIdentifiers.forEach(function (id) {
             $('#' + id).on('click', function () {
-                var value = obj.choices[idx];
+                var value = $(this).text();
                 $('#' + domID).html(value + ' ' + span);
                 onSubmitCallback(param, value);
             });


Test patch:
diff --git a/tests/test_datacollector.py b/tests/test_datacollector.py
index 6d572115..1a6a748a 100644
--- a/tests/test_datacollector.py
+++ b/tests/test_datacollector.py
@@ -83,18 +83,16 @@ class TestDataCollector(unittest.TestCase):
         for element in data_collector.model_vars["model_value"]:
             assert element == 100
 
-    def test_agent_vars(self):
+    def test_agent_records(self):
         '''
         Test agent-level variable collection.
         '''
         data_collector = self.model.datacollector
-        assert len(data_collector.agent_vars["value"]) == 7
-        assert len(data_collector.agent_vars["value2"]) == 7
-        for var in ["value", "value2"]:
-            for step in data_collector.agent_vars[var]:
-                assert len(step) == 10
-                for record in step:
-                    assert len(record) == 2
+        assert len(data_collector._agent_records) == 7
+        for step, records in data_collector._agent_records.items():
+            assert len(records) == 10
+            for values in records:
+                assert len(values) == 4
 
     def test_table_rows(self):
         '''