-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
242 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ htmlcov/ | |
.tox/ | ||
.coverage | ||
.cache | ||
.idea/ | ||
nosetests.xml | ||
coverage.xml | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |