Skip to content

Centralize ops kwarg specification #19396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 27, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 107 additions & 84 deletions pandas/core/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ def _gen_fill_zeros(name):
return fill_value


def _get_frame_op_default_axis(name):
"""
Only DataFrame cares about default_axis, specifically:
special methods have default_axis=None and flex methods
have default_axis='columns'.

Parameters
----------
name : str

Returns
-------
default_axis: str or None
"""
if name.replace('__r', '__') in ['__and__', '__or__', '__xor__']:
# bool methods
return 'columns'
elif name.startswith('__'):
# __add__, __mul__, ...
return None
else:
# add, mul, ...
return 'columns'


# -----------------------------------------------------------------------------
# Docstring Generation and Templates

Expand Down Expand Up @@ -281,17 +306,17 @@ def _gen_fill_zeros(name):


_agg_doc_PANEL = """
Wrapper method for {wrp_method}
Wrapper method for {op_name}

Parameters
----------
other : {construct} or {cls_name}
axis : {{{axis_order}}}
other : DataFrame or Panel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this other correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It matches the existing docstring

axis : {{items, major_axis, minor_axis}}
Axis to broadcast over

Returns
-------
{cls_name}
Panel
"""


Expand Down Expand Up @@ -337,14 +362,18 @@ def _make_flex_doc(op_name, typ):
# methods


def _create_methods(arith_method, comp_method, bool_method,
use_numexpr, special=False, default_axis='columns',
have_divmod=False):
def _create_methods(cls, arith_method, comp_method, bool_method,
special=False):
# creates actual methods based upon arithmetic, comp and bool method
# constructors.

# NOTE: Only frame cares about default_axis, specifically: special methods
# have default axis None, whereas flex methods have default axis 'columns'
subtyp = getattr(cls, '_subtyp', '')
use_numexpr = 'sparse' not in subtyp
# numexpr is available for non-sparse classes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments above the statement


have_divmod = issubclass(cls, ABCSeries)
# divmod is available for Series and SparseSeries

# if we're not using numexpr, then don't pass a str_rep
if use_numexpr:
op = lambda x: x
Expand All @@ -360,44 +389,28 @@ def names(x):
else:
names = lambda x: x

# Inframe, all special methods have default_axis=None, flex methods have
# default_axis set to the default (columns)
# yapf: disable
new_methods = dict(
add=arith_method(operator.add, names('add'), op('+'),
default_axis=default_axis),
radd=arith_method(lambda x, y: y + x, names('radd'), op('+'),
default_axis=default_axis),
sub=arith_method(operator.sub, names('sub'), op('-'),
default_axis=default_axis),
mul=arith_method(operator.mul, names('mul'), op('*'),
default_axis=default_axis),
truediv=arith_method(operator.truediv, names('truediv'), op('/'),
default_axis=default_axis),
floordiv=arith_method(operator.floordiv, names('floordiv'), op('//'),
default_axis=default_axis),
add=arith_method(operator.add, names('add'), op('+')),
radd=arith_method(lambda x, y: y + x, names('radd'), op('+')),
sub=arith_method(operator.sub, names('sub'), op('-')),
mul=arith_method(operator.mul, names('mul'), op('*')),
truediv=arith_method(operator.truediv, names('truediv'), op('/')),
floordiv=arith_method(operator.floordiv, names('floordiv'), op('//')),
# Causes a floating point exception in the tests when numexpr enabled,
# so for now no speedup
mod=arith_method(operator.mod, names('mod'), None,
default_axis=default_axis),
pow=arith_method(operator.pow, names('pow'), op('**'),
default_axis=default_axis),
mod=arith_method(operator.mod, names('mod'), None),
pow=arith_method(operator.pow, names('pow'), op('**')),
# not entirely sure why this is necessary, but previously was included
# so it's here to maintain compatibility
rmul=arith_method(operator.mul, names('rmul'), op('*'),
default_axis=default_axis),
rsub=arith_method(lambda x, y: y - x, names('rsub'), op('-'),
default_axis=default_axis),
rmul=arith_method(operator.mul, names('rmul'), op('*')),
rsub=arith_method(lambda x, y: y - x, names('rsub'), op('-')),
rtruediv=arith_method(lambda x, y: operator.truediv(y, x),
names('rtruediv'), op('/'),
default_axis=default_axis),
names('rtruediv'), op('/')),
rfloordiv=arith_method(lambda x, y: operator.floordiv(y, x),
names('rfloordiv'), op('//'),
default_axis=default_axis),
rpow=arith_method(lambda x, y: y**x, names('rpow'), op('**'),
default_axis=default_axis),
rmod=arith_method(lambda x, y: y % x, names('rmod'), op('%'),
default_axis=default_axis))
names('rfloordiv'), op('//')),
rpow=arith_method(lambda x, y: y**x, names('rpow'), op('**')),
rmod=arith_method(lambda x, y: y % x, names('rmod'), op('%')))
# yapf: enable
new_methods['div'] = new_methods['truediv']
new_methods['rdiv'] = new_methods['rtruediv']
Expand Down Expand Up @@ -425,10 +438,7 @@ def names(x):
names('rxor'), op('^'))))
if have_divmod:
# divmod doesn't have an op that is supported by numexpr
new_methods['divmod'] = arith_method(divmod,
names('divmod'),
None,
default_axis=default_axis)
new_methods['divmod'] = arith_method(divmod, names('divmod'), None)

new_methods = {names(k): v for k, v in new_methods.items()}
return new_methods
Expand All @@ -444,8 +454,7 @@ def add_methods(cls, new_methods, force):
# Arithmetic
def add_special_arithmetic_methods(cls, arith_method=None,
comp_method=None, bool_method=None,
use_numexpr=True, force=False,
have_divmod=False):
force=False):
"""
Adds the full suite of special arithmetic methods (``__add__``,
``__sub__``, etc.) to the class.
Expand All @@ -454,27 +463,17 @@ def add_special_arithmetic_methods(cls, arith_method=None,
----------
arith_method : function (optional)
factory for special arithmetic methods, with op string:
f(op, name, str_rep, default_axis=None)
f(op, name, str_rep)
comp_method : function (optional)
factory for rich comparison - signature: f(op, name, str_rep)
bool_method : function (optional)
factory for boolean methods - signature: f(op, name, str_rep)
use_numexpr : bool, default True
whether to accelerate with numexpr, defaults to True
force : bool, default False
if False, checks whether function is defined **on ``cls.__dict__``**
before defining if True, always defines functions on class base
have_divmod : bool, (optional)
should a divmod method be added? this method is special because it
returns a tuple of cls instead of a single element of type cls
"""

# in frame, special methods have default_axis = None, comp methods use
# 'columns'

new_methods = _create_methods(arith_method, comp_method,
bool_method, use_numexpr, default_axis=None,
special=True, have_divmod=have_divmod)
new_methods = _create_methods(cls, arith_method, comp_method, bool_method,
special=True)

# inplace operators (I feel like these should get passed an `inplace=True`
# or just be removed
Expand Down Expand Up @@ -517,28 +516,24 @@ def f(self, other):

def add_flex_arithmetic_methods(cls, flex_arith_method,
flex_comp_method=None, flex_bool_method=None,
use_numexpr=True, force=False):
force=False):
"""
Adds the full suite of flex arithmetic methods (``pow``, ``mul``, ``add``)
to the class.

Parameters
----------
flex_arith_method : function
factory for special arithmetic methods, with op string:
f(op, name, str_rep, default_axis=None)
factory for flex arithmetic methods, with op string:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure "special" here was a copy/paste mistake. Please correct me if I'm wrong.

f(op, name, str_rep)
flex_comp_method : function, optional,
factory for rich comparison - signature: f(op, name, str_rep)
use_numexpr : bool, default True
whether to accelerate with numexpr, defaults to True
force : bool, default False
if False, checks whether function is defined **on ``cls.__dict__``**
before defining if True, always defines functions on class base
"""
# in frame, default axis is 'columns', doesn't matter for series and panel
new_methods = _create_methods(flex_arith_method,
new_methods = _create_methods(cls, flex_arith_method,
flex_comp_method, flex_bool_method,
use_numexpr, default_axis='columns',
special=False)
new_methods.update(dict(multiply=new_methods['mul'],
subtract=new_methods['sub'],
Expand Down Expand Up @@ -597,7 +592,7 @@ def _construct_divmod_result(left, result, index, name, dtype):
)


def _arith_method_SERIES(op, name, str_rep, default_axis=None):
def _arith_method_SERIES(op, name, str_rep):
"""
Wrapper function for Series arithmetic operations, to avoid
code duplication.
Expand Down Expand Up @@ -637,15 +632,9 @@ def safe_na_op(lvalues, rvalues):
with np.errstate(all='ignore'):
return na_op(lvalues, rvalues)
except Exception:
if isinstance(rvalues, ABCSeries):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isinstance(rvalues, ABCSeries) is always False because before calling safe_na_op we do this same check and if True set rvalues = rvalues.values. If reviewers want to call this out-of-scope I'll remove it and make a separate PR.

if is_object_dtype(rvalues):
# if dtype is object, try elementwise op
return libalgos.arrmap_object(rvalues,
lambda x: op(lvalues, x))
else:
if is_object_dtype(lvalues):
return libalgos.arrmap_object(lvalues,
lambda x: op(x, rvalues))
if is_object_dtype(lvalues):
return libalgos.arrmap_object(lvalues,
lambda x: op(x, rvalues))
raise

def wrapper(left, right, name=name, na_op=na_op):
Expand All @@ -671,7 +660,7 @@ def wrapper(left, right, name=name, na_op=na_op):
lvalues = left.values
rvalues = right
if isinstance(rvalues, ABCSeries):
rvalues = getattr(rvalues, 'values', rvalues)
rvalues = rvalues.values

result = safe_na_op(lvalues, rvalues)
return construct_result(left, result,
Expand Down Expand Up @@ -933,7 +922,7 @@ def wrapper(self, other):
return wrapper


def _flex_method_SERIES(op, name, str_rep, default_axis=None):
def _flex_method_SERIES(op, name, str_rep):
doc = _make_flex_doc(name, 'series')

@Appender(doc)
Expand Down Expand Up @@ -964,8 +953,7 @@ def flex_wrapper(self, other, level=None, fill_value=None, axis=0):

series_special_funcs = dict(arith_method=_arith_method_SERIES,
comp_method=_comp_method_SERIES,
bool_method=_bool_method_SERIES,
have_divmod=True)
bool_method=_bool_method_SERIES)


# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -1015,9 +1003,10 @@ def to_series(right):
return right


def _arith_method_FRAME(op, name, str_rep=None, default_axis='columns'):
def _arith_method_FRAME(op, name, str_rep=None):
eval_kwargs = _gen_eval_kwargs(name)
fill_zeros = _gen_fill_zeros(name)
default_axis = _get_frame_op_default_axis(name)

def na_op(x, y):
import pandas.core.computation.expressions as expressions
Expand Down Expand Up @@ -1088,7 +1077,8 @@ def f(self, other, axis=default_axis, level=None, fill_value=None):
return f


def _flex_comp_method_FRAME(op, name, str_rep=None, default_axis='columns'):
def _flex_comp_method_FRAME(op, name, str_rep=None):
default_axis = _get_frame_op_default_axis(name)

def na_op(x, y):
try:
Expand Down Expand Up @@ -1167,8 +1157,7 @@ def f(self, other):
# -----------------------------------------------------------------------------
# Panel

def _arith_method_PANEL(op, name, str_rep=None, default_axis=None):

def _arith_method_PANEL(op, name, str_rep=None):
# work only for scalars
def f(self, other):
if not is_scalar(other):
Expand Down Expand Up @@ -1228,6 +1217,40 @@ def f(self, other, axis=None):
return f


def _flex_method_PANEL(op, name, str_rep=None):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cut/pasted from Panel, with small re-workings to _agg_doc_PANEL.

eval_kwargs = _gen_eval_kwargs(name)
fill_zeros = _gen_fill_zeros(name)

def na_op(x, y):
import pandas.core.computation.expressions as expressions

try:
result = expressions.evaluate(op, str_rep, x, y,
errors='raise',
**eval_kwargs)
except TypeError:
result = op(x, y)

# handles discrepancy between numpy and numexpr on division/mod
# by 0 though, given that these are generally (always?)
# non-scalars, I'm not sure whether it's worth it at the moment
result = missing.fill_zeros(result, x, y, name, fill_zeros)
return result

if name in _op_descriptions:
doc = _make_flex_doc(name, 'panel')
else:
# doc strings substitors
doc = _agg_doc_PANEL.format(op_name=name)

@Appender(doc)
def f(self, other, axis=0):
return self._combine(other, na_op, axis=axis)

f.__name__ = name
return f


panel_special_funcs = dict(arith_method=_arith_method_PANEL,
comp_method=_comp_method_PANEL,
bool_method=_arith_method_PANEL)
Loading