Skip to content

Commit

Permalink
Merge 89550ba into 3cefb36
Browse files Browse the repository at this point in the history
  • Loading branch information
jiffyclub committed Nov 25, 2014
2 parents 3cefb36 + 89550ba commit 9ac4eca
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ htmlcov/
.tox/
.coverage
.cache
.idea/
nosetests.xml
coverage.xml

Expand Down
153 changes: 153 additions & 0 deletions urbansim/accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
An Account class for tracking monetary transactions during UrbanSim runs.
"""
from collections import namedtuple

import pandas as pd
import toolz


Transaction = namedtuple('Transaction', ('amount', 'subaccount', 'metadata'))

# column names that are always present in DataFrames of transactions
COLS = ['amount', 'subaccount']


def _column_names_from_metadata(dicts):
"""
Get the unique set of keys from a list of dictionaries.
Parameters
----------
dicts : iterable
Sequence of dictionaries.
Returns
-------
keys : list
Unique set of keys.
"""
return list(toolz.unique(toolz.concat(dicts)))


class Account(object):
"""
Keeps a record of transactions, metadata, and a running balance.
Parameters
----------
name : str
Arbitrary name for this account used in some output.
balance : float, optional
Starting balance for the account.
Attributes
----------
balance : float
Running balance in account.
"""
def __init__(self, name, balance=0):
self.name = name
self.balance = balance
self.transactions = []

def add_transaction(self, amount, subaccount=None, metadata=None):
"""
Add a new transaction to the account.
Parameters
----------
amount : float
Negative for withdrawls, positive for deposits.
subaccount : object, optional
Any indicator of a subaccount to which this transaction applies.
metadata : dict, optional
Any extra metadata to record with the transaction.
(E.g. Info about where the money is coming from or going.)
May not contain keys 'amount' or 'subaccount'.
"""
metadata = metadata or {}
self.transactions.append(Transaction(amount, subaccount, metadata))
self.balance += amount

def add_transactions(self, transactions):
"""
Add a collection of transactions to the account.
Parameters
----------
transactions : iterable
Should be tuples of amount, subaccount, and metadata as would
be passed to `add_transaction`.
"""
for t in transactions:
self.add_transaction(*t)

def total_transactions(self):
"""
Get the sum of all transactions on the account.
Returns
-------
total : float
"""
return sum(t.amount for t in self.transactions)

def total_transactions_by_subacct(self, subaccount):
"""
Get the sum of all transactions for a given subaccount.
Parameters
----------
subaccount : object
Identifier of subaccount.
Returns
-------
total : float
"""
return sum(
t.amount for t in self.transactions if t.subaccount == subaccount)

def all_subaccounts(self):
"""
Returns an iterator of all subaccounts that have a recorded transaction
with the account.
"""
return toolz.unique(t.subaccount for t in self.transactions)

def iter_subaccounts(self):
"""
An iterator over subaccounts yielding subaccount name and
the total of transactions for that subaccount.
"""
for sa in self.all_subaccounts():
yield sa, self.total_transactions_by_subacct(sa)

def to_frame(self):
"""
Return transactions as a pandas DataFrame.
"""
col_names = _column_names_from_metadata(
t.metadata for t in self.transactions)

trow = lambda t: (
toolz.concatv(
(t.amount, t.subaccount),
(t.metadata.get(c) for c in col_names)))
rows = [trow(t) for t in self.transactions]

if len(rows) == 0:
return pd.DataFrame(columns=COLS + col_names)

return pd.DataFrame(rows, columns=COLS + col_names)
4 changes: 3 additions & 1 deletion urbansim/developer/developer.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ def pick(self, form, target_units, parcel_size, ave_unit_size,
# no feasible buldings, might as well bail
return None

if isinstance(form, list):
if form is None:
df = self.feasibility
elif isinstance(form, list):
df = self.keep_form_with_max_profit(form)
else:
df = self.feasibility[form]
Expand Down
Empty file added urbansim/tests/__init__.py
Empty file.
85 changes: 85 additions & 0 deletions urbansim/tests/test_accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import pandas as pd
import pytest
from pandas.util import testing as pdt

from .. import accounts


@pytest.fixture(scope='module')
def acc_name():
return 'test'


@pytest.fixture(scope='module')
def acc_bal():
return 1000


@pytest.fixture
def acc(acc_name, acc_bal):
return accounts.Account(acc_name, acc_bal)


def test_init(acc, acc_name):
assert acc.name == acc_name
assert acc.balance == 1000
assert acc.transactions == []


def test_add_transaction(acc, acc_bal):
amount = -50
subaccount = ('a', 'b', 'c')
metadata = {'for': 'light speed engine'}
acc.add_transaction(amount, subaccount, metadata)

assert len(acc.transactions) == 1
assert acc.balance == acc_bal + amount

t = acc.transactions[-1]
assert isinstance(t, accounts.Transaction)
assert t.amount == amount
assert t.subaccount == subaccount
assert t.metadata == metadata


def test_add_transactions(acc, acc_bal):
t1 = accounts.Transaction(200, ('a', 'b', 'c'), None)
t2 = (-50, None, {'to': 'Acme Corp.'})
t3 = (-100, ('a', 'b', 'c'), 'Acme Corp.')
t4 = (42, None, None)
acc.add_transactions((t1, t2, t3, t4))

assert len(acc.transactions) == 4
assert acc.balance == acc_bal + t1[0] + t2[0] + t3[0] + t4[0]
assert acc.total_transactions() == t1[0] + t2[0] + t3[0] + t4[0]
assert acc.total_transactions_by_subacct(('a', 'b', 'c')) == t1[0] + t3[0]
assert acc.total_transactions_by_subacct(None) == t2[0] + t4[0]

assert list(acc.all_subaccounts()) == [('a', 'b', 'c'), None]

assert list(acc.iter_subaccounts()) == [
(('a', 'b', 'c'), t1[0] + t3[0]),
(None, t2[0] + t4[0])]


def test_column_names_from_metadata():
cnfm = accounts._column_names_from_metadata

assert cnfm([]) == []
assert cnfm([{'a': 1, 'b': 2}]) == ['a', 'b']
assert cnfm([{'a': 1}, {'b': 2}]) == ['a', 'b']
assert cnfm([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]) == ['a', 'b']


def test_to_frame(acc, acc_bal):
t1 = accounts.Transaction(200, ('a', 'b', 'c'), None)
t2 = (-50, None, {'to': 'Acme Corp.'})
acc.add_transactions((t1, t2))

expected = pd.DataFrame(
[[200, ('a', 'b', 'c'), None],
[-50, None, 'Acme Corp.']],
columns=['amount', 'subaccount', 'to'])

df = acc.to_frame()
pdt.assert_frame_equal(df, expected)

0 comments on commit 9ac4eca

Please sign in to comment.