Skip to content

Commit a58bc6a

Browse files
committed
Merge pull request #5022 from jtratner/arithmetic-refactor2
CLN/ENH: Provide full suite of arithmetic (and flex) methods to all NDFrame objects.
2 parents e3201ff + 9a1b2b4 commit a58bc6a

19 files changed

+1563
-1084
lines changed

doc/source/api.rst

+58-8
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,30 @@ Binary operator functions
275275
:toctree: generated/
276276

277277
Series.add
278-
Series.div
279-
Series.mul
280278
Series.sub
279+
Series.mul
280+
Series.div
281+
Series.truediv
282+
Series.floordiv
283+
Series.mod
284+
Series.pow
285+
Series.radd
286+
Series.rsub
287+
Series.rmul
288+
Series.rdiv
289+
Series.rtruediv
290+
Series.rfloordiv
291+
Series.rmod
292+
Series.rpow
281293
Series.combine
282294
Series.combine_first
283295
Series.round
296+
Series.lt
297+
Series.gt
298+
Series.le
299+
Series.ge
300+
Series.ne
301+
Series.eq
284302

285303
Function application, GroupBy
286304
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -480,13 +498,27 @@ Binary operator functions
480498
:toctree: generated/
481499

482500
DataFrame.add
483-
DataFrame.div
484-
DataFrame.mul
485501
DataFrame.sub
502+
DataFrame.mul
503+
DataFrame.div
504+
DataFrame.truediv
505+
DataFrame.floordiv
506+
DataFrame.mod
507+
DataFrame.pow
486508
DataFrame.radd
487-
DataFrame.rdiv
488-
DataFrame.rmul
489509
DataFrame.rsub
510+
DataFrame.rmul
511+
DataFrame.rdiv
512+
DataFrame.rtruediv
513+
DataFrame.rfloordiv
514+
DataFrame.rmod
515+
DataFrame.rpow
516+
DataFrame.lt
517+
DataFrame.gt
518+
DataFrame.le
519+
DataFrame.ge
520+
DataFrame.ne
521+
DataFrame.eq
490522
DataFrame.combine
491523
DataFrame.combineAdd
492524
DataFrame.combine_first
@@ -710,9 +742,27 @@ Binary operator functions
710742
:toctree: generated/
711743

712744
Panel.add
713-
Panel.div
714-
Panel.mul
715745
Panel.sub
746+
Panel.mul
747+
Panel.div
748+
Panel.truediv
749+
Panel.floordiv
750+
Panel.mod
751+
Panel.pow
752+
Panel.radd
753+
Panel.rsub
754+
Panel.rmul
755+
Panel.rdiv
756+
Panel.rtruediv
757+
Panel.rfloordiv
758+
Panel.rmod
759+
Panel.rpow
760+
Panel.lt
761+
Panel.gt
762+
Panel.le
763+
Panel.ge
764+
Panel.ne
765+
Panel.eq
716766

717767
Function application, GroupBy
718768
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

doc/source/release.rst

+4
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ API Changes
263263
- Begin removing methods that don't make sense on ``GroupBy`` objects
264264
(:issue:`4887`).
265265
- Remove deprecated ``read_clipboard/to_clipboard/ExcelFile/ExcelWriter`` from ``pandas.io.parsers`` (:issue:`3717`)
266+
- All non-Index NDFrames (``Series``, ``DataFrame``, ``Panel``, ``Panel4D``,
267+
``SparsePanel``, etc.), now support the entire set of arithmetic operators
268+
and arithmetic flex methods (add, sub, mul, etc.). ``SparsePanel`` does not
269+
support ``pow`` or ``mod`` with non-scalars. (:issue:`3765`)
266270

267271
Internal Refactoring
268272
~~~~~~~~~~~~~~~~~~~~

doc/source/v0.13.0.txt

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ API changes
6868
df1 and df2
6969
s1 and s2
7070

71+
- All non-Index NDFrames (``Series``, ``DataFrame``, ``Panel``, ``Panel4D``,
72+
``SparsePanel``, etc.), now support the entire set of arithmetic operators
73+
and arithmetic flex methods (add, sub, mul, etc.). ``SparsePanel`` does not
74+
support ``pow`` or ``mod`` with non-scalars. (:issue:`3765`)
75+
7176

7277
Prior Version Deprecations/Changes
7378
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

pandas/computation/expressions.py

+37-13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
except ImportError: # pragma: no cover
1616
_NUMEXPR_INSTALLED = False
1717

18+
_TEST_MODE = None
19+
_TEST_RESULT = None
1820
_USE_NUMEXPR = _NUMEXPR_INSTALLED
1921
_evaluate = None
2022
_where = None
@@ -55,9 +57,10 @@ def set_numexpr_threads(n=None):
5557

5658
def _evaluate_standard(op, op_str, a, b, raise_on_error=True, **eval_kwargs):
5759
""" standard evaluation """
60+
if _TEST_MODE:
61+
_store_test_result(False)
5862
return op(a, b)
5963

60-
6164
def _can_use_numexpr(op, op_str, a, b, dtype_check):
6265
""" return a boolean if we WILL be using numexpr """
6366
if op_str is not None:
@@ -88,11 +91,8 @@ def _evaluate_numexpr(op, op_str, a, b, raise_on_error=False, **eval_kwargs):
8891

8992
if _can_use_numexpr(op, op_str, a, b, 'evaluate'):
9093
try:
91-
a_value, b_value = a, b
92-
if hasattr(a_value, 'values'):
93-
a_value = a_value.values
94-
if hasattr(b_value, 'values'):
95-
b_value = b_value.values
94+
a_value = getattr(a, "values", a)
95+
b_value = getattr(b, "values", b)
9696
result = ne.evaluate('a_value %s b_value' % op_str,
9797
local_dict={'a_value': a_value,
9898
'b_value': b_value},
@@ -104,6 +104,9 @@ def _evaluate_numexpr(op, op_str, a, b, raise_on_error=False, **eval_kwargs):
104104
if raise_on_error:
105105
raise
106106

107+
if _TEST_MODE:
108+
_store_test_result(result is not None)
109+
107110
if result is None:
108111
result = _evaluate_standard(op, op_str, a, b, raise_on_error)
109112

@@ -119,13 +122,9 @@ def _where_numexpr(cond, a, b, raise_on_error=False):
119122
if _can_use_numexpr(None, 'where', a, b, 'where'):
120123

121124
try:
122-
cond_value, a_value, b_value = cond, a, b
123-
if hasattr(cond_value, 'values'):
124-
cond_value = cond_value.values
125-
if hasattr(a_value, 'values'):
126-
a_value = a_value.values
127-
if hasattr(b_value, 'values'):
128-
b_value = b_value.values
125+
cond_value = getattr(cond, 'values', cond)
126+
a_value = getattr(a, 'values', a)
127+
b_value = getattr(b, 'values', b)
129128
result = ne.evaluate('where(cond_value, a_value, b_value)',
130129
local_dict={'cond_value': cond_value,
131130
'a_value': a_value,
@@ -189,3 +188,28 @@ def where(cond, a, b, raise_on_error=False, use_numexpr=True):
189188
if use_numexpr:
190189
return _where(cond, a, b, raise_on_error=raise_on_error)
191190
return _where_standard(cond, a, b, raise_on_error=raise_on_error)
191+
192+
193+
def set_test_mode(v = True):
194+
"""
195+
Keeps track of whether numexpr was used. Stores an additional ``True`` for
196+
every successful use of evaluate with numexpr since the last
197+
``get_test_result``
198+
"""
199+
global _TEST_MODE, _TEST_RESULT
200+
_TEST_MODE = v
201+
_TEST_RESULT = []
202+
203+
204+
def _store_test_result(used_numexpr):
205+
global _TEST_RESULT
206+
if used_numexpr:
207+
_TEST_RESULT.append(used_numexpr)
208+
209+
210+
def get_test_result():
211+
"""get test result and reset test_results"""
212+
global _TEST_RESULT
213+
res = _TEST_RESULT
214+
_TEST_RESULT = []
215+
return res

pandas/computation/tests/test_eval.py

+9-30
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import unittest
44
import functools
5-
import numbers
65
from itertools import product
7-
import ast
86

97
import nose
108
from nose.tools import assert_raises, assert_true, assert_false, assert_equal
@@ -250,12 +248,6 @@ def check_complex_cmp_op(self, lhs, cmp1, rhs, binop, cmp2):
250248
not np.isscalar(rhs_new) and binop in skip_these):
251249
with tm.assertRaises(TypeError):
252250
_eval_single_bin(lhs_new, binop, rhs_new, self.engine)
253-
elif _bool_and_frame(lhs_new, rhs_new):
254-
with tm.assertRaises(TypeError):
255-
_eval_single_bin(lhs_new, binop, rhs_new, self.engine)
256-
with tm.assertRaises(TypeError):
257-
pd.eval('lhs_new & rhs_new'.format(binop),
258-
engine=self.engine, parser=self.parser)
259251
else:
260252
expected = _eval_single_bin(lhs_new, binop, rhs_new, self.engine)
261253
result = pd.eval(ex, engine=self.engine, parser=self.parser)
@@ -301,28 +293,15 @@ def check_operands(left, right, cmp_op):
301293
rhs_new = check_operands(mid, rhs, cmp2)
302294

303295
if lhs_new is not None and rhs_new is not None:
304-
# these are not compatible operands
305-
if isinstance(lhs_new, Series) and isinstance(rhs_new, DataFrame):
306-
self.assertRaises(TypeError, _eval_single_bin, lhs_new, '&',
307-
rhs_new, self.engine)
308-
elif (_bool_and_frame(lhs_new, rhs_new)):
309-
self.assertRaises(TypeError, _eval_single_bin, lhs_new, '&',
310-
rhs_new, self.engine)
311-
elif _series_and_2d_ndarray(lhs_new, rhs_new):
312-
# TODO: once #4319 is fixed add this test back in
313-
#self.assertRaises(Exception, _eval_single_bin, lhs_new, '&',
314-
#rhs_new, self.engine)
315-
pass
316-
else:
317-
ex1 = 'lhs {0} mid {1} rhs'.format(cmp1, cmp2)
318-
ex2 = 'lhs {0} mid and mid {1} rhs'.format(cmp1, cmp2)
319-
ex3 = '(lhs {0} mid) & (mid {1} rhs)'.format(cmp1, cmp2)
320-
expected = _eval_single_bin(lhs_new, '&', rhs_new, self.engine)
321-
322-
for ex in (ex1, ex2, ex3):
323-
result = pd.eval(ex, engine=self.engine,
324-
parser=self.parser)
325-
assert_array_equal(result, expected)
296+
ex1 = 'lhs {0} mid {1} rhs'.format(cmp1, cmp2)
297+
ex2 = 'lhs {0} mid and mid {1} rhs'.format(cmp1, cmp2)
298+
ex3 = '(lhs {0} mid) & (mid {1} rhs)'.format(cmp1, cmp2)
299+
expected = _eval_single_bin(lhs_new, '&', rhs_new, self.engine)
300+
301+
for ex in (ex1, ex2, ex3):
302+
result = pd.eval(ex, engine=self.engine,
303+
parser=self.parser)
304+
assert_array_equal(result, expected)
326305

327306
@skip_incompatible_operand
328307
def check_simple_cmp_op(self, lhs, cmp1, rhs):

pandas/core/common.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import codecs
99
import csv
1010
import sys
11+
import types
1112

1213
from datetime import timedelta
1314

@@ -27,6 +28,7 @@
2728
from pandas.core.config import get_option
2829
from pandas.core import array as pa
2930

31+
3032
class PandasError(Exception):
3133
pass
3234

@@ -74,6 +76,31 @@ def __instancecheck__(cls, inst):
7476

7577
ABCGeneric = _ABCGeneric("ABCGeneric", tuple(), {})
7678

79+
80+
def bind_method(cls, name, func):
81+
"""Bind a method to class, python 2 and python 3 compatible.
82+
83+
Parameters
84+
----------
85+
86+
cls : type
87+
class to receive bound method
88+
name : basestring
89+
name of method on class instance
90+
func : function
91+
function to be bound as method
92+
93+
94+
Returns
95+
-------
96+
None
97+
"""
98+
# only python 2 has bound/unbound method issue
99+
if not compat.PY3:
100+
setattr(cls, name, types.MethodType(func, None, cls))
101+
else:
102+
setattr(cls, name, func)
103+
77104
def isnull(obj):
78105
"""Detect missing values (NaN in numeric arrays, None/NaN in object arrays)
79106
@@ -360,10 +387,10 @@ def _take_2d_multi_generic(arr, indexer, out, fill_value, mask_info):
360387
if col_needs:
361388
out[:, col_mask] = fill_value
362389
for i in range(len(row_idx)):
363-
u = row_idx[i]
390+
u_ = row_idx[i]
364391
for j in range(len(col_idx)):
365392
v = col_idx[j]
366-
out[i, j] = arr[u, v]
393+
out[i, j] = arr[u_, v]
367394

368395

369396
def _take_nd_generic(arr, indexer, out, axis, fill_value, mask_info):
@@ -2348,3 +2375,10 @@ def save(obj, path): # TODO remove in 0.13
23482375
warnings.warn("save is deprecated, use obj.to_pickle", FutureWarning)
23492376
from pandas.io.pickle import to_pickle
23502377
return to_pickle(obj, path)
2378+
2379+
2380+
def _maybe_match_name(a, b):
2381+
name = None
2382+
if a.name == b.name:
2383+
name = a.name
2384+
return name

0 commit comments

Comments
 (0)