Skip to content

Commit 899576d

Browse files
committed
CLN: Move arithmetic methods into core/generic.py
Adds two classmethods to `NDFrame` that allow easy, correct binding of all the arithmetic operators. Only add if method not already in class' __dict__ Method binding is Py2/3 compatible Handles both flex and special (`__*__`) methods Frame: Refactor frame flex methods to generic Series: refactor rich comparisons to generic Panel: Refactor panel arithmetic methods into generic Note that SparsePanel explicitly opts out because it works differently and falls back to using underlying operations + _fill_zeros. Add subtract and multiply methods to match Panel Add note with question on r* flex methods Add call signature for flex_*_method to docstring Change flexmethod to be accelerated Add basic speedups to panel Simplify naming and use special method check to handle the different names (this does end up causing certain more descriptive names (i.e., "true division") to be lost, but overall I think it's a big improvement and makes everything much more simple). Also sets `default_axis` to be exactly the same as how it was before the refactoring. (Frame flex methods use the default of 'columns', special arithmetic methods use `default_axis=None` and all comp methods use default axis of 'columns')
1 parent 3f27c2f commit 899576d

File tree

5 files changed

+200
-205
lines changed

5 files changed

+200
-205
lines changed

pandas/core/frame.py

+2-78
Original file line numberDiff line numberDiff line change
@@ -835,68 +835,6 @@ def __contains__(self, key):
835835
"""True if DataFrame has this column"""
836836
return key in self.columns
837837

838-
#----------------------------------------------------------------------
839-
# Arithmetic methods
840-
841-
add = _arith_method(operator.add, 'add', '+')
842-
mul = _arith_method(operator.mul, 'multiply', '*')
843-
sub = _arith_method(operator.sub, 'subtract', '-')
844-
if not py3compat.PY3:
845-
div = divide = _arith_method(operator.div, 'divide', '/',
846-
truediv=False, fill_zeros=np.inf, default_axis=None)
847-
else:
848-
div = divide = _arith_method(operator.truediv, 'divide', '/',
849-
truediv=True, fill_zeros=np.inf, default_axis=None)
850-
truediv = _arith_method(operator.truediv, 'true division', '/',
851-
truediv=True, fill_zeros=np.inf, default_axis=None)
852-
floordiv = _arith_method(operator.floordiv, 'floor division', '//',
853-
default_axis=None, fill_zeros=np.inf)
854-
mod = _arith_method(operator.mod, 'mod', default_axis=None, fill_zeros=np.nan)
855-
pow = _arith_method(operator.pow, 'pow', '**')
856-
857-
radd = _arith_method(_radd_compat, 'radd')
858-
rmul = _arith_method(operator.mul, 'rmultiply')
859-
rsub = _arith_method(lambda x, y: y - x, 'rsubtract')
860-
rdiv = _arith_method(lambda x, y: y / x, 'rdivide')
861-
rpow = _arith_method(lambda x, y: y ** x, 'rpow')
862-
863-
__add__ = _arith_method(operator.add, '__add__', '+', default_axis=None)
864-
__sub__ = _arith_method(operator.sub, '__sub__', '-', default_axis=None)
865-
__mul__ = _arith_method(operator.mul, '__mul__', '*', default_axis=None)
866-
__truediv__ = _arith_method(operator.truediv, '__truediv__', '/',
867-
default_axis=None, fill_zeros=np.inf, truediv=True)
868-
__floordiv__ = _arith_method(operator.floordiv, '__floordiv__', '//',
869-
default_axis=None, fill_zeros=np.inf)
870-
__pow__ = _arith_method(operator.pow, '__pow__', '**', default_axis=None)
871-
872-
# Causes a floating point exception in the tests when numexpr enabled, so for now no speedup
873-
# __mod__ = _arith_method(operator.mod, '__mod__', '%', default_axis=None, fill_zeros=np.nan)
874-
__mod__ = _arith_method(operator.mod, '__mod__',
875-
default_axis=None, fill_zeros=np.nan)
876-
877-
__radd__ = _arith_method(_radd_compat, '__radd__', default_axis=None)
878-
__rmul__ = _arith_method(operator.mul, '__rmul__', default_axis=None)
879-
__rsub__ = _arith_method(lambda x, y: y - x, '__rsub__', default_axis=None)
880-
__rtruediv__ = _arith_method(lambda x, y: y / x, '__rtruediv__',
881-
default_axis=None, fill_zeros=np.inf)
882-
__rfloordiv__ = _arith_method(lambda x, y: y // x, '__rfloordiv__',
883-
default_axis=None, fill_zeros=np.inf)
884-
__rpow__ = _arith_method(lambda x, y: y ** x, '__rpow__',
885-
default_axis=None)
886-
__rmod__ = _arith_method(operator.mod, '__rmod__', default_axis=None, fill_zeros=np.nan)
887-
888-
# boolean operators
889-
__and__ = _arith_method(operator.and_, '__and__', '&')
890-
__or__ = _arith_method(operator.or_, '__or__', '|')
891-
__xor__ = _arith_method(operator.xor, '__xor__')
892-
893-
# Python 2 division methods
894-
if not py3compat.PY3:
895-
__div__ = _arith_method(operator.div, '__div__', '/',
896-
default_axis=None, fill_zeros=np.inf, truediv=False)
897-
__rdiv__ = _arith_method(lambda x, y: y / x, '__rdiv__',
898-
default_axis=None, fill_zeros=np.inf)
899-
900838
def __neg__(self):
901839
arr = operator.neg(self.values)
902840
return self._wrap_array(arr, self.axes, copy=False)
@@ -905,21 +843,6 @@ def __invert__(self):
905843
arr = operator.inv(self.values)
906844
return self._wrap_array(arr, self.axes, copy=False)
907845

908-
# Comparison methods
909-
__eq__ = _comp_method(operator.eq, '__eq__', '==')
910-
__ne__ = _comp_method(operator.ne, '__ne__', '!=')
911-
__lt__ = _comp_method(operator.lt, '__lt__', '<' )
912-
__gt__ = _comp_method(operator.gt, '__gt__', '>' )
913-
__le__ = _comp_method(operator.le, '__le__', '<=')
914-
__ge__ = _comp_method(operator.ge, '__ge__', '>=')
915-
916-
eq = _flex_comp_method(operator.eq, 'eq', '==')
917-
ne = _flex_comp_method(operator.ne, 'ne', '!=')
918-
lt = _flex_comp_method(operator.lt, 'lt', '<')
919-
gt = _flex_comp_method(operator.gt, 'gt', '>')
920-
le = _flex_comp_method(operator.le, 'le', '<=')
921-
ge = _flex_comp_method(operator.ge, 'ge', '>=')
922-
923846
def dot(self, other):
924847
"""
925848
Matrix multiplication with DataFrame or Series objects
@@ -6030,7 +5953,8 @@ def boxplot(self, column=None, by=None, ax=None, fontsize=None,
60305953
return ax
60315954
DataFrame.boxplot = boxplot
60325955

6033-
5956+
DataFrame._add_flex_arithmetic_methods(_arith_method, radd_func=_radd_compat, flex_comp_method=_flex_comp_method)
5957+
DataFrame._add_special_arithmetic_methods(_arith_method, radd_func=_radd_compat, comp_method=_comp_method, bool_method=_arith_method)
60345958
if __name__ == '__main__':
60355959
import nose
60365960
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],

pandas/core/generic.py

+148
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# pylint: disable=W0231,E1101
2+
import operator
23

34
import numpy as np
5+
from pandas.core.common import bind_method
46

57
from pandas.core.index import MultiIndex
68
import pandas.core.indexing as indexing
79
from pandas.core.indexing import _maybe_convert_indices
810
from pandas.tseries.index import DatetimeIndex
911
import pandas.core.common as com
1012
import pandas.lib as lib
13+
from pandas.util import py3compat
1114

1215

1316
class PandasError(Exception):
@@ -35,6 +38,76 @@ def __hash__(self):
3538
raise TypeError('{0!r} objects are mutable, thus they cannot be'
3639
' hashed'.format(self.__class__.__name__))
3740

41+
#----------------------------------------------------------------------
42+
# Arithmetic!
43+
@classmethod
44+
def _add_special_arithmetic_methods(cls, arith_method=None, radd_func=None, comp_method=None, bool_method=None,
45+
use_numexpr=True):
46+
"""
47+
Adds the full suite of special arithmetic methods (``__add__``, ``__sub__``, etc.) to the class.
48+
49+
Parameters
50+
----------
51+
flex_arith_method : factory for flex arithmetic methods, with op string:
52+
f(op, name, str_rep, default_axis=None, fill_zeros=None, **eval_kwargs)
53+
radd_func : Possible replacement for ``lambda x, y: y + x`` for compatibility
54+
flex_comp_method : optional, factory for rich comparison - signature: f(op, name, str_rep)
55+
use_numexpr : whether to accelerate with numexpr, defaults to True
56+
"""
57+
radd_func = radd_func or operator.add
58+
# in frame, special methods have default_axis = None, comp methods use 'columns'
59+
new_methods = create_methods(arith_method, radd_func, comp_method, bool_method, use_numexpr, default_axis=None,
60+
special=True)
61+
62+
# inplace operators (I feel like these should get passed an `inplace=True`
63+
# or just be removed
64+
new_methods.update(dict(
65+
__iadd__=new_methods["__add__"],
66+
__isub__=new_methods["__sub__"],
67+
__imul__=new_methods["__mul__"],
68+
__itruediv__=new_methods["__truediv__"],
69+
__ipow__=new_methods["__pow__"]
70+
))
71+
if not py3compat.PY3:
72+
new_methods["__idiv__"] = new_methods["__div__"]
73+
for name, method in new_methods.items():
74+
if name not in cls.__dict__:
75+
bind_method(cls, name, method)
76+
# DELETEME SOON!
77+
else:
78+
print("Not overwriting existing func %r" % name)
79+
80+
@classmethod
81+
def _add_flex_arithmetic_methods(cls, flex_arith_method, radd_func=None, flex_comp_method=None,
82+
flex_bool_method=None, use_numexpr=True):
83+
"""
84+
Adds the full suite of flex arithmetic methods (``pow``, ``mul``, ``add``) to the class.
85+
86+
Parameters
87+
----------
88+
flex_arith_method : factory for flex arithmetic methods, with op string:
89+
f(op, name, str_rep, default_axis=None, fill_zeros=None, **eval_kwargs)
90+
radd_func : Possible replacement for ``lambda x, y: y + x`` for compatibility
91+
flex_comp_method : optional, factory for rich comparison - signature: f(op, name, str_rep)
92+
use_numexpr : whether to accelerate with numexpr, defaults to True
93+
"""
94+
radd_func = radd_func or operator.add
95+
# in frame, default axis is 'columns', doesn't matter for series and panel
96+
new_methods = create_methods(
97+
flex_arith_method, radd_func, flex_comp_method, flex_bool_method,
98+
use_numexpr, default_axis='columns', special=False)
99+
new_methods.update(dict(
100+
multiply=new_methods['mul'],
101+
subtract=new_methods['sub'],
102+
divide=new_methods['div']
103+
))
104+
105+
for name, method in new_methods.items():
106+
if name not in cls.__dict__:
107+
bind_method(cls, name, method)
108+
# DELETEME SOON!
109+
else:
110+
print("Not overwriting existing func %r" % name)
38111

39112
#----------------------------------------------------------------------
40113
# Axis name business
@@ -1158,3 +1231,78 @@ def truncate(self, before=None, after=None, copy=True):
11581231
result = result.copy()
11591232

11601233
return result
1234+
1235+
1236+
1237+
1238+
def create_methods(arith_method, radd_func, comp_method, bool_method, use_numexpr, special=False, default_axis='columns'):
1239+
# NOTE: Only frame cares about default_axis, specifically: special methods have default axis None,
1240+
# whereas flex methods have default axis 'columns'
1241+
# if we're not using numexpr, then don't pass a str_rep
1242+
if use_numexpr:
1243+
op = lambda x: x
1244+
else:
1245+
op = lambda x: None
1246+
if special:
1247+
def names(x):
1248+
if x[-1] == "_":
1249+
return "__%s_" % x
1250+
else:
1251+
return "__%s__" % x
1252+
else:
1253+
names = lambda x: x
1254+
radd_func = radd_func or operator.add
1255+
# Inframe, all special methods have default_axis=None, flex methods have default_axis set to the default (columns)
1256+
new_methods = dict(
1257+
add=arith_method(operator.add, names('add'), op('+'), default_axis=default_axis),
1258+
radd=arith_method(radd_func, names('radd'), op('+'), default_axis=default_axis),
1259+
sub=arith_method(operator.sub, names('sub'), op('-'), default_axis=default_axis),
1260+
mul=arith_method(operator.mul, names('mul'), op('*'), default_axis=default_axis),
1261+
truediv=arith_method(operator.truediv, names('truediv'), op('/'),
1262+
truediv=True, fill_zeros=np.inf, default_axis=default_axis),
1263+
floordiv=arith_method(operator.floordiv, names('floordiv'), op('//'),
1264+
default_axis=default_axis, fill_zeros=np.inf),
1265+
# Causes a floating point exception in the tests when numexpr
1266+
# enabled, so for now no speedup
1267+
mod=arith_method(operator.mod, names('mod'), default_axis=default_axis,
1268+
fill_zeros=np.nan),
1269+
pow=arith_method(operator.pow, names('pow'), op('**'), default_axis=default_axis),
1270+
# not entirely sure why this is necessary, but previously was included
1271+
# so it's here to maintain compatibility
1272+
rmul=arith_method(operator.mul, names('rmul'), default_axis=default_axis),
1273+
rsub=arith_method(lambda x, y: y - x, names('rsub'), default_axis=default_axis),
1274+
rtruediv=arith_method(lambda x, y: operator.truediv(y, x), names('rtruediv'), op('/'),
1275+
truediv=True, fill_zeros=np.inf, default_axis=default_axis),
1276+
rfloordiv=arith_method(lambda x, y: operator.floordiv(y, x), names('rfloordiv'), op('//'),
1277+
default_axis=default_axis, fill_zeros=np.inf),
1278+
rpow=arith_method(lambda x, y: y ** x, names('rpow'), default_axis=default_axis),
1279+
rmod=arith_method(lambda x, y: y % x, names('rmod'), default_axis=default_axis),
1280+
)
1281+
if not py3compat.PY3:
1282+
new_methods["div"] = arith_method(operator.div, names('div'), op('/'),
1283+
truediv=False, fill_zeros=np.inf, default_axis=default_axis)
1284+
new_methods["rdiv"] = arith_method(lambda x, y: operator.div(y, x), names('rdiv'), op('/'),
1285+
truediv=False, fill_zeros=np.inf, default_axis=default_axis)
1286+
else:
1287+
new_methods["div"] = arith_method(operator.truediv, names('div'), op('/'),
1288+
truediv=True, fill_zeros=np.inf, default_axis=default_axis)
1289+
# Comp methods never had a default axis set
1290+
if comp_method:
1291+
new_methods.update(dict(
1292+
eq=comp_method(operator.eq, names('eq'), op('==')),
1293+
ne=comp_method(operator.ne, names('ne'), op('!=')),
1294+
lt=comp_method(operator.lt, names('lt'), op('<')),
1295+
gt=comp_method(operator.gt, names('gt'), op('>')),
1296+
le=comp_method(operator.le, names('le'), op('<=')),
1297+
ge=comp_method(operator.ge, names('ge'), op('>=')),
1298+
))
1299+
if bool_method:
1300+
new_methods.update(dict(
1301+
and_=bool_method(operator.and_, names('and_ [&]'), op('&')),
1302+
or_=bool_method(operator.or_, names('or_ [|]'), op('|')),
1303+
# For some reason ``^`` wasn't used in original.
1304+
xor=bool_method(operator.xor, names('xor [^]'))
1305+
))
1306+
1307+
new_methods = dict((names(k), v) for k, v in new_methods.items())
1308+
return new_methods

pandas/core/panel.py

+18-53
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def f(self, other):
117117
return f
118118

119119

120-
def _comp_method(func, name):
120+
def _comp_method(func, name, str_rep=None):
121121

122122
def na_op(x, y):
123123
try:
@@ -227,26 +227,6 @@ def _construct_axes_dict_for_slice(self, axes=None, **kwargs):
227227
d.update(kwargs)
228228
return d
229229

230-
__add__ = _arith_method(operator.add, '__add__', '+')
231-
__sub__ = _arith_method(operator.sub, '__sub__', '-')
232-
__truediv__ = _arith_method(operator.truediv, '__truediv__', '/', truediv=True, fill_zeros=np.inf)
233-
__floordiv__ = _arith_method(operator.floordiv, '__floordiv__', '//',
234-
fill_zeros=np.inf)
235-
__floordiv__ = _arith_method(operator.floordiv, '__floordiv__')
236-
__mul__ = _arith_method(operator.mul, '__mul__', '*')
237-
__pow__ = _arith_method(operator.pow, '__pow__', '**')
238-
239-
__radd__ = _arith_method(operator.add, '__radd__')
240-
__rmul__ = _arith_method(operator.mul, '__rmul__')
241-
__rsub__ = _arith_method(lambda x, y: y - x, '__rsub__')
242-
__rtruediv__ = _arith_method(lambda x, y: y / x, '__rtruediv__')
243-
__rfloordiv__ = _arith_method(lambda x, y: y // x, '__rfloordiv__')
244-
__rpow__ = _arith_method(lambda x, y: y ** x, '__rpow__')
245-
246-
if not py3compat.PY3:
247-
__div__ = _arith_method(operator.div, '__div__', '/', truediv=False)
248-
__rdiv__ = _arith_method(lambda x, y: y / x, '__rdiv__')
249-
250230
def __init__(self, data=None, items=None, major_axis=None, minor_axis=None,
251231
copy=False, dtype=None):
252232
self._init_data(
@@ -464,21 +444,6 @@ def __neg__(self):
464444
def __invert__(self):
465445
return -1 * self
466446

467-
# Comparison methods
468-
__eq__ = _comp_method(operator.eq, '__eq__')
469-
__ne__ = _comp_method(operator.ne, '__ne__')
470-
__lt__ = _comp_method(operator.lt, '__lt__')
471-
__gt__ = _comp_method(operator.gt, '__gt__')
472-
__le__ = _comp_method(operator.le, '__le__')
473-
__ge__ = _comp_method(operator.ge, '__ge__')
474-
475-
eq = _comp_method(operator.eq, 'eq')
476-
ne = _comp_method(operator.ne, 'ne')
477-
gt = _comp_method(operator.gt, 'gt')
478-
lt = _comp_method(operator.lt, 'lt')
479-
ge = _comp_method(operator.ge, 'ge')
480-
le = _comp_method(operator.le, 'le')
481-
482447
#----------------------------------------------------------------------
483448
# Magic methods
484449

@@ -1627,7 +1592,7 @@ def _extract_axis(self, data, axis=0, intersect=False):
16271592
return _ensure_index(index)
16281593

16291594
@classmethod
1630-
def _add_aggregate_operations(cls):
1595+
def _add_aggregate_operations(cls, use_numexpr=True):
16311596
""" add the operations to the cls; evaluate the doc strings again """
16321597

16331598
# doc strings substitors
@@ -1644,27 +1609,26 @@ def _add_aggregate_operations(cls):
16441609
-------
16451610
""" + cls.__name__ + "\n"
16461611

1647-
def _panel_arith_method(op, name):
1612+
def _panel_arith_method(op, name, str_rep = None, default_axis=None, fill_zeros=None, **eval_kwargs):
1613+
def na_op(x, y):
1614+
try:
1615+
result = expressions.evaluate(op, str_rep, x, y, raise_on_error=True, **eval_kwargs)
1616+
except TypeError:
1617+
result = op(x, y)
1618+
1619+
# handles discrepancy between numpy and numexpr on division/mod by 0
1620+
# though, given that these are generally (always?) non-scalars, I'm
1621+
# not sure whether it's worth it at the moment
1622+
result = com._fill_zeros(result,y,fill_zeros)
1623+
return result
16481624
@Substitution(op)
16491625
@Appender(_agg_doc)
16501626
def f(self, other, axis=0):
1651-
return self._combine(other, op, axis=axis)
1627+
return self._combine(other, na_op, axis=axis)
16521628
f.__name__ = name
16531629
return f
1654-
1655-
cls.add = _panel_arith_method(operator.add, 'add')
1656-
cls.subtract = cls.sub = _panel_arith_method(operator.sub, 'subtract')
1657-
cls.multiply = cls.mul = _panel_arith_method(operator.mul, 'multiply')
1658-
1659-
try:
1660-
cls.divide = cls.div = _panel_arith_method(operator.div, 'divide')
1661-
except AttributeError: # pragma: no cover
1662-
# Python 3
1663-
cls.divide = cls.div = _panel_arith_method(operator.truediv,
1664-
'divide')
1665-
cls.truediv = _panel_arith_method(operator.truediv, 'true division')
1666-
cls.floordiv = _panel_arith_method(operator.floordiv, 'floor division')
1667-
1630+
# add `div`, `mul`, `pow`, etc..
1631+
cls._add_flex_arithmetic_methods(_panel_arith_method, use_numexpr=use_numexpr)
16681632
_agg_doc = """
16691633
Return %(desc)s over requested axis
16701634
@@ -1745,6 +1709,7 @@ def min(self, axis='major', skipna=True):
17451709
return self._reduce(nanops.nanmin, axis=axis, skipna=skipna)
17461710
cls.min = min
17471711

1712+
Panel._add_special_arithmetic_methods(_arith_method, comp_method=_comp_method)
17481713
Panel._add_aggregate_operations()
17491714

17501715
WidePanel = Panel

0 commit comments

Comments
 (0)