Skip to content

Commit

Permalink
add arithmetic functions
Browse files Browse the repository at this point in the history
  • Loading branch information
philtweir committed Sep 21, 2019
1 parent 0467687 commit 15983db
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 26 deletions.
37 changes: 15 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
# Moneypandas

Moneypandas is a prototype fork of Cyberpandas for currency, using the `money` library.
Moneypandas is a prototype fork of Cyberpandas for currency, using the `money` library. Even this README is shamelessly purloigned, with thanks to Tom Augspurger and the ContinuumIO team.

----

# Cyberpandas

[![Build Status](https://travis-ci.org/ContinuumIO/cyberpandas.svg?branch=master)](https://travis-ci.org/ContinuumIO/cyberpandas)
[![Documentation Status](https://readthedocs.org/projects/cyberpandas/badge/?version=latest)](http://cyberpandas.readthedocs.io/en/latest/?badge=latest)

Cyberpandas provides support for storing IP and MAC address data inside a pandas DataFrame using pandas' [Extension Array Interface](http://pandas-docs.github.io/pandas-docs-travis/extending.html#extension-types)
This package provides support for storing currency data inside a pandas DataFrame using pandas' [Extension Array Interface](http://pandas-docs.github.io/pandas-docs-travis/extending.html#extension-types)

```python
In [1]: from cyberpandas import IPArray
In [1]: from moneypandas import MoneyArray

In [2]: import pandas as pd

In [3]: df = pd.DataFrame({"address": IPArray(['192.168.1.1', '192.168.1.10'])})
In [3]: df = pd.DataFrame({"money": MoneyArray(['1284 EUR', '121 EUR', '€14'])})

In [4]: df
Out[4]:
address
0 192.168.1.1
1 192.168.1.10
money
0 EUR 1,284.00
1 EUR 121.00
2 EUR 14.00
```

See the [documentation](https://cyberpandas.readthedocs.io/en/latest/) for more.

## Installation

With Conda:
For more examples, including summing and converting mixed-currency columns, see the `examples` folder.

conda install -c conda-forge cyberpandas
(note: not yet tested with Conda, only setuptools/pipenv)

Or from PyPI
To efficiently perform operations, aggregation is done per currency first, and then XMoney used to do necessary operations on the output aggregates.

pip install cyberpandas
Currency conversion of a Series only uses XMoney and conversion where currencies mismatch, so converting a column mostly of BBBs, with a few AAAs, should scale according to the number of AAAs.

## TODO

* implement more reduce functions
* testing for arithmetic
22 changes: 22 additions & 0 deletions examples/three_currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from moneypandas import MoneyArray
from money import xrates
import decimal
import pandas as pd


xrates.install('money.exchange.SimpleBackend')
xrates.base = 'USD'
xrates.setrate('EUR', decimal.Decimal('0.9'))
xrates.setrate('GBP', decimal.Decimal('0.8'))

df = pd.DataFrame({"money": MoneyArray(['1284 EUR', '121 EUR', '€14', '£12'], 'USD')})
total = df['money'].sum()
print("Total: ", total)
print("Total (EUR): ", total.to('EUR'))

df['money'] = df['money'].money.to_currency('EUR')
mean = df['money'].mean()
print("Mean: ", mean)

df['money'].money.to_currency('GBP', shallow=False, in_place=True)
print('All converted to GBP', df)
7 changes: 5 additions & 2 deletions moneypandas/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import numpy as np

from pandas.core.arrays import ExtensionArray
from pandas.core.arrays import ExtensionArray, ExtensionScalarOpsMixin


class NumPyBackedExtensionArrayMixin(ExtensionArray):
class NumPyBackedExtensionArrayMixin(ExtensionArray, ExtensionScalarOpsMixin):
@property
def dtype(self):
"""The dtype for this extension array, MoneyType"""
Expand Down Expand Up @@ -69,3 +69,6 @@ def unique(self):
_, indices = np.unique(self.data, return_index=True)
data = self.data.take(np.sort(indices))
return self._from_ndarray(data)

NumPyBackedExtensionArrayMixin._add_arithmetic_ops()
NumPyBackedExtensionArrayMixin._add_comparison_ops()
107 changes: 105 additions & 2 deletions moneypandas/money_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import decimal
import collections

import six
import numpy as np
from pandas.compat.numpy import function as nv
import pandas as pd
from pandas.core import nanops
import money
from pandas.api.extensions import ExtensionDtype

Expand Down Expand Up @@ -124,10 +125,71 @@ def to_decimals(self, money_code=None):
result = decimalize(self.data['va'])
for i, ceq in enumerate(same):
if not ceq:
result[i] = money.XMoney(*self.data[i]).to(money_code)
result[i] = money.XMoney(*self.data[i]).to(money_code).amount

return result

# Operations thanks to pandas.core.arrays.base.numpy_
def _min(self, ndarray, axis=None, out=None, keepdims=False, skipna=True):
nv.validate_min((), dict(out=out, keepdims=keepdims))
return nanops.nanmin(ndarray, axis=axis, skipna=skipna)

def _max(self, ndarray, axis=None, out=None, keepdims=False, skipna=True):
nv.validate_max((), dict(out=out, keepdims=keepdims))
return nanops.nanmax(ndarray, axis=axis, skipna=skipna)

def _sum(
self,
ndarray,
axis=None,
dtype=None,
out=None,
keepdims=False,
initial=None,
skipna=True,
min_count=0,
):
nv.validate_sum(
(), dict(dtype=dtype, out=out, keepdims=keepdims, initial=initial)
)
return nanops.nansum(
ndarray, axis=axis, skipna=skipna, min_count=min_count
)

def _reduce(self, name, skipna=True, **kwargs):
currencies = [cu for cu in np.unique(self.data['cu']) if cu]
totals = {}

if name == 'mean':
meth = getattr(self, '_sum', None)
else:
meth = getattr(self, '_' + name, None)

if meth:
if len(currencies) > 1:
money_code = self.default_money_code if self.default_money_code else currencies[0]
for i, currency in enumerate(currencies):
totals[currency] = money.XMoney(
meth(self.data['va'][self.data['cu'] == currency], skipna=skipna, **kwargs),
currency
)
total = meth(
np.array([subtotal.to(money_code).amount for subtotal in totals.values()]),
skipna=skipna,
**kwargs
)
if name == 'mean':
total = total / len(self.data)
total = money.XMoney(amount=total, currency=money_code)
else:
money_code = currencies[0] if currencies else self.default_money_code
total = money.XMoney(meth(self.data['va'], skipna=skipna, **kwargs), money_code)

return total
else:
msg = "'{}' does not implement reduction '{}'"
raise TypeError(msg.format(type(self).__name__, name))

@classmethod
def from_bytes(cls, bytestring):
r"""Create a MoneyArray from a bytestring.
Expand Down Expand Up @@ -398,6 +460,37 @@ def fmt(x):
def _values_for_factorize(self):
return self.astype(object), (0, '')

def to_currency(self, money_code, shallow=True, in_place=False):
if shallow:
if in_place:
copy = self
else:
copy = self.copy()
copy.default_money_code = money_code
else:
mask = self.isna()
same = (self.data['cu'] == money_code) | mask
decimalize = np.vectorize(decimal.Decimal)

result = self.data
if not in_place:
result = result.copy()

for i, ceq in enumerate(same):
if not ceq:
va = money.XMoney(self.data[i]['va'], self.data[i]['cu']) \
.to(money_code).amount
result[i] = (va, money_code)

if in_place:
self.data = result
copy = self.__class__(
result,
default_money_code=money_code,
dtype=self.dtype
)

return copy

# -----------------------------------------------------------------------------
# Accessor
Expand All @@ -421,6 +514,16 @@ def _validate(obj):
raise AttributeError("Cannot use 'money' accessor on objects of "
"dtype '{}'.".format(obj.dtype))

def to_currency(self, money_code, shallow=True, in_place=True):
return delegated_method(
self._data.to_currency,
self._index,
self._name,
money_code,
shallow,
in_place
)


def is_money_type(obj):
t = getattr(obj, 'dtype', obj)
Expand Down

0 comments on commit 15983db

Please sign in to comment.