Skip to content

Commit

Permalink
Add Bar and Pie Chart visualization and include charts example (fix I…
Browse files Browse the repository at this point in the history
…ssue projectmesa#490) (projectmesa#594)

* Added first version of pie chart and put it into the forest_fire example

* Added Bar charts and created a new "charts" example

Added bar charts that can either visualize model-level fields or
agent-level fields. The syntax for adding these to models are identical
to exisiting syntax to add line charts to a model.

Added a new "charts" example based off the existing bank_reserves
model to better provide examples of each available chart in mesa.
(model-level bar, agent-level bar, pie, and line.)

* Improved handling of negative values in bar chart; cleanup

Negative values now appear as bars below the X axis.

Cleaned up intially very messy D3.js code. (Now it's just the
normal amount of wild for D3)

* Bundled the legend and chart itself together in a div.

* Fix testing errors

* fixed charts example

For some reason, there was a mismatch in variable names between the
server.py file and the model.py file. This commmit fixes that mismatch.
  • Loading branch information
James Hovet authored and Corvince committed Feb 7, 2019
1 parent 5b5a730 commit f390fd5
Show file tree
Hide file tree
Showing 13 changed files with 900 additions and 2 deletions.
40 changes: 40 additions & 0 deletions examples/charts/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Mesa Charts Example

## Summary

A modified version of the "bank_reserves" example made to provide examples of mesa's charting tools.

The chart types included in this example are:
- Line Charts for time-series data of multiple model parameters
- Pie Charts for model parameters
- Bar charts for both model and agent-level parameters

## Installation

To install the dependencies use pip and the requirements.txt in this directory. e.g.

```
$ pip install -r requirements.txt
```

## Interactive Model Run

To run the model interactively, use `mesa runserver` in this directory:

```
$ mesa runserver
```

Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/), select the model parameters, press Reset, then Start.

## Files

* ``bank_reserves/random_walker.py``: This defines a class that inherits from the Mesa Agent class. The main purpose is to provide a method for agents to move randomly one cell at a time.
* ``bank_reserves/agents.py``: Defines the People and Bank classes.
* ``bank_reserves/model.py``: Defines the Bank Reserves model and the DataCollector functions.
* ``bank_reserves/server.py``: Sets up the interactive visualization server.
* ``run.py``: Launches a model visualization server.

## Further Reading

See the "bank_reserves" model for more information.
180 changes: 180 additions & 0 deletions examples/charts/charts/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""
The following code was adapted from the Bank Reserves model included in Netlogo
Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves
Accessed on: November 2, 2017
Author of NetLogo code:
Wilensky, U. (1998). NetLogo Bank Reserves model.
http://ccl.northwestern.edu/netlogo/models/BankReserves.
Center for Connected Learning and Computer-Based Modeling,
Northwestern University, Evanston, IL.
"""

from mesa import Agent
from charts.random_walk import RandomWalker


class Bank(Agent):
def __init__(self, unique_id, model, reserve_percent=50):
# initialize the parent class with required parameters
super().__init__(unique_id, model)
# for tracking total value of loans outstanding
self.bank_loans = 0
"""percent of deposits the bank must keep in reserves - this is a
UserSettableParameter in server.py"""
self.reserve_percent = reserve_percent
# for tracking total value of deposits
self.deposits = 0
# total amount of deposits in reserve
self.reserves = ((self.reserve_percent / 100) * self.deposits)
# amount the bank is currently able to loan
self.bank_to_loan = 0

"""update the bank's reserves and amount it can loan;
this is called every time a person balances their books
see below for Person.balance_books()"""
def bank_balance(self):
self.reserves = ((self.reserve_percent / 100) * self.deposits)
self.bank_to_loan = (self.deposits - (self.reserves + self.bank_loans))


# subclass of RandomWalker, which is subclass to Mesa Agent
class Person(RandomWalker):
def __init__(self, unique_id, pos, model, moore, bank, rich_threshold):
# init parent class with required parameters
super().__init__(unique_id, pos, model, moore=moore)
# the amount each person has in savings
self.savings = 0
# total loan amount person has outstanding
self.loans = 0
"""start everyone off with a random amount in their wallet from 1 to a
user settable rich threshold amount"""
self.wallet = self.random.randint(1, rich_threshold + 1)
# savings minus loans, see balance_books() below
self.wealth = 0
# person to trade with, see do_business() below
self.customer = 0
# person's bank, set at __init__, all people have the same bank in this model
self.bank = bank

def do_business(self):
"""check if person has any savings, any money in wallet, or if the
bank can loan them any money"""
if self.savings > 0 or self.wallet > 0 or self.bank.bank_to_loan > 0:
# create list of people at my location (includes self)
my_cell = self.model.grid.get_cell_list_contents([self.pos])
# check if other people are at my location
if len(my_cell) > 1:
# set customer to self for while loop condition
customer = self
while customer == self:
"""select a random person from the people at my location
to trade with"""
customer = self.random.choice(my_cell)
# 50% chance of trading with customer
if self.random.randint(0, 1) == 0:
# 50% chance of trading $5
if self.random.randint(0, 1) == 0:
# give customer $5 from my wallet (may result in negative wallet)
customer.wallet += 5
self.wallet -= 5
# 50% chance of trading $2
else:
# give customer $2 from my wallet (may result in negative wallet)
customer.wallet += 2
self.wallet -= 2

def balance_books(self):
# check if wallet is negative from trading with customer
if self.wallet < 0:
# if negative money in wallet, check if my savings can cover the balance
if self.savings >= (self.wallet * -1):
"""if my savings can cover the balance, withdraw enough
money from my savings so that my wallet has a 0 balance"""
self.withdraw_from_savings(self.wallet * -1)
# if my savings cannot cover the negative balance of my wallet
else:
# check if i have any savings
if self.savings > 0:
"""if i have savings, withdraw all of it to reduce my
negative balance in my wallet"""
self.withdraw_from_savings(self.savings)
# record how much money the bank can loan out right now
temp_loan = self.bank.bank_to_loan
"""check if the bank can loan enough money to cover the
remaining negative balance in my wallet"""
if temp_loan >= (self.wallet * -1):
"""if the bank can loan me enough money to cover
the remaining negative balance in my wallet, take out a
loan for the remaining negative balance"""
self.take_out_loan(self.wallet * -1)
else:
"""if the bank cannot loan enough money to cover the negative
balance of my wallet, then take out a loan for the
total amount the bank can loan right now"""
self.take_out_loan(temp_loan)
else:
"""if i have money in my wallet from trading with customer, deposit
it to my savings in the bank"""
self.deposit_to_savings(self.wallet)
# check if i have any outstanding loans, and if i have savings
if self.loans > 0 and self.savings > 0:
# check if my savings can cover my outstanding loans
if self.savings >= self.loans:
# payoff my loans with my savings
self.withdraw_from_savings(self.loans)
self.repay_a_loan(self.loans)
# if my savings won't cover my loans
else:
# pay off part of my loans with my savings
self.withdraw_from_savings(self.savings)
self.repay_a_loan(self.wallet)
# calculate my wealth
self.wealth = (self.savings - self.loans)

# part of balance_books()
def deposit_to_savings(self, amount):
# take money from my wallet and put it in savings
self.wallet -= amount
self.savings += amount
# increase bank deposits
self.bank.deposits += amount

# part of balance_books()
def withdraw_from_savings(self, amount):
# put money in my wallet from savings
self.wallet += amount
self.savings -= amount
# decrease bank deposits
self.bank.deposits -= amount

# part of balance_books()
def repay_a_loan(self, amount):
# take money from my wallet to pay off all or part of a loan
self.loans -= amount
self.wallet -= amount
# increase the amount the bank can loan right now
self.bank.bank_to_loan += amount
# decrease the bank's outstanding loans
self.bank.bank_loans -= amount

# part of balance_books()
def take_out_loan(self, amount):
"""borrow from the bank to put money in my wallet, and increase my
outstanding loans"""
self.loans += amount
self.wallet += amount
# decresae the amount the bank can loan right now
self.bank.bank_to_loan -= amount
# increase the bank's outstanding loans
self.bank.bank_loans += amount

# step is called for each agent in model.BankReservesModel.schedule.step()
def step(self):
# move to a cell in my Moore neighborhood
self.random_move()
# trade
self.do_business()
# deposit money or take out a loan
self.balance_books()
# updat the bank's reserves and the amount it can loan right now
self.bank.bank_balance()
138 changes: 138 additions & 0 deletions examples/charts/charts/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
The following code was adapted from the Bank Reserves model included in Netlogo
Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves
Accessed on: November 2, 2017
Author of NetLogo code:
Wilensky, U. (1998). NetLogo Bank Reserves model.
http://ccl.northwestern.edu/netlogo/models/BankReserves.
Center for Connected Learning and Computer-Based Modeling,
Northwestern University, Evanston, IL.
"""

from charts.agents import Bank, Person
from mesa import Model
from mesa.space import MultiGrid
from mesa.datacollection import DataCollector
from mesa.time import RandomActivation
import numpy as np


"""
If you want to perform a parameter sweep, call batch_run.py instead of run.py.
For details see batch_run.py in the same directory as run.py.
"""

# Start of datacollector functions


def get_num_rich_agents(model):
""" return number of rich agents"""

rich_agents = [a for a in model.schedule.agents if a.savings > model.rich_threshold]
return len(rich_agents)


def get_num_poor_agents(model):
"""return number of poor agents"""

poor_agents = [a for a in model.schedule.agents if a.loans > 10]
return len(poor_agents)


def get_num_mid_agents(model):
"""return number of middle class agents"""

mid_agents = [a for a in model.schedule.agents if
a.loans < 10 and a.savings < model.rich_threshold]
return len(mid_agents)


def get_total_savings(model):
"""sum of all agents' savings"""

agent_savings = [a.savings for a in model.schedule.agents]
# return the sum of agents' savings
return np.sum(agent_savings)


def get_total_wallets(model):
"""sum of amounts of all agents' wallets"""

agent_wallets = [a.wallet for a in model.schedule.agents]
# return the sum of all agents' wallets
return np.sum(agent_wallets)


def get_total_money(model):
# sum of all agents' wallets
wallet_money = get_total_wallets(model)
# sum of all agents' savings
savings_money = get_total_savings(model)
# return sum of agents' wallets and savings for total money
return wallet_money + savings_money


def get_total_loans(model):
# list of amounts of all agents' loans
agent_loans = [a.loans for a in model.schedule.agents]
# return sum of all agents' loans
return np.sum(agent_loans)


class Charts(Model):

# grid height
grid_h = 20
# grid width
grid_w = 20

"""init parameters "init_people", "rich_threshold", and "reserve_percent"
are all UserSettableParameters"""
def __init__(self, height=grid_h, width=grid_w, init_people=2, rich_threshold=10,
reserve_percent=50,):
self.height = height
self.width = width
self.init_people = init_people
self.schedule = RandomActivation(self)
self.grid = MultiGrid(self.width, self.height, torus=True)
# rich_threshold is the amount of savings a person needs to be considered "rich"
self.rich_threshold = rich_threshold
self.reserve_percent = reserve_percent
# see datacollector functions above
self.datacollector = DataCollector(model_reporters={
"Rich": get_num_rich_agents,
"Poor": get_num_poor_agents,
"Middle Class": get_num_mid_agents,
"Savings": get_total_savings,
"Wallets": get_total_wallets,
"Money": get_total_money,
"Loans": get_total_loans},
agent_reporters={
"Wealth": lambda x: x.wealth})

# create a single bank for the model
self.bank = Bank(1, self, self.reserve_percent)

# create people for the model according to number of people set by user
for i in range(self.init_people):
# set x, y coords randomly within the grid
x = self.random.randrange(self.width)
y = self.random.randrange(self.height)
p = Person(i, (x, y), self, True, self.bank, self.rich_threshold)
# place the Person object on the grid at coordinates (x, y)
self.grid.place_agent(p, (x, y))
# add the Person object to the model schedule
self.schedule.add(p)

self.running = True
self.datacollector.collect(self)

def step(self):
# tell all the agents in the model to run their step function
self.schedule.step()
# collect data
self.datacollector.collect(self)

def run_model(self):
for i in range(self.run_time):
self.step()
Loading

0 comments on commit f390fd5

Please sign in to comment.