Repository: pandas-dev/pandas

Issue:
DOC: clarify docstring of DataFrame.sub et al 
http://stackoverflow.com/questions/30144170/what-does-pandas-sub-operator-do/30144239#30144238

The docstring of `DataFrame.sub` states `Binary operator sub`, so if you don't directly see the link with "subtraction", this is not very clear



Discussion:

Resolving patch:
diff --git a/pandas/core/ops.py b/pandas/core/ops.py
--- a/pandas/core/ops.py
+++ b/pandas/core/ops.py
@@ -213,7 +213,7 @@ def add_flex_arithmetic_methods(cls, flex_arith_method, radd_func=None,
 
     Parameters
     ----------
-    flex_arith_method : function (optional)
+    flex_arith_method : function
         factory for special arithmetic methods, with op string:
         f(op, name, str_rep, default_axis=None, fill_zeros=None, **eval_kwargs)
     radd_func :  function (optional)
@@ -703,12 +703,35 @@ def _radd_compat(left, right):
 
     return output
 
+_op_descriptions = {'add': {'op': '+', 'desc': 'Addition', 'reversed': False, 'reverse': 'radd'},
+                    'sub': {'op': '-', 'desc': 'Subtraction', 'reversed': False, 'reverse': 'rsub'},
+                    'mul': {'op': '*', 'desc': 'Multiplication', 'reversed': False, 'reverse': 'rmul'},
+                    'mod': {'op': '%', 'desc': 'Modulo', 'reversed': False, 'reverse': 'rmod'},
+                    'pow': {'op': '**', 'desc': 'Exponential power', 'reversed': False, 'reverse': 'rpow'},
+                    'truediv': {'op': '/', 'desc': 'Floating division', 'reversed': False, 'reverse': 'rtruediv'},
+                    'floordiv': {'op': '//', 'desc': 'Integer division', 'reversed': False, 'reverse': 'rfloordiv'}}
+
+_op_names = list(_op_descriptions.keys())
+for k in _op_names:
+    reverse_op = _op_descriptions[k]['reverse']
+    _op_descriptions[reverse_op] = _op_descriptions[k].copy()
+    _op_descriptions[reverse_op]['reversed'] = True
+    _op_descriptions[reverse_op]['reverse'] = k
 
 def _flex_method_SERIES(op, name, str_rep, default_axis=None,
                         fill_zeros=None, **eval_kwargs):
+    op_name = name.replace('__', '')
+    op_desc = _op_descriptions[op_name]
+    if op_desc['reversed']:
+        equiv = 'other ' + op_desc['op'] + ' series'
+    else:
+        equiv = 'series ' + op_desc['op'] + ' other'
+
     doc = """
-    Binary operator %s with support to substitute a fill_value for missing data
-    in one of the inputs
+    %s of series and other, element-wise (binary operator `%s`).
+
+    Equivalent to ``%s``, but with support to substitute a fill_value for
+    missing data in one of the inputs.
 
     Parameters
     ----------
@@ -723,7 +746,11 @@ def _flex_method_SERIES(op, name, str_rep, default_axis=None,
     Returns
     -------
     result : Series
-    """ % name
+
+    See also
+    --------
+    Series.%s
+    """ % (op_desc['desc'], op_name, equiv, op_desc['reverse'])
 
     @Appender(doc)
     def flex_wrapper(self, other, level=None, fill_value=None, axis=0):
@@ -813,7 +840,48 @@ def na_op(x, y):
 
         return result
 
-    @Appender(_arith_doc_FRAME % name)
+    if name in _op_descriptions:
+        op_name = name.replace('__', '')
+        op_desc = _op_descriptions[op_name]
+        if op_desc['reversed']:
+            equiv = 'other ' + op_desc['op'] + ' dataframe'
+        else:
+            equiv = 'dataframe ' + op_desc['op'] + ' other'
+
+        doc = """
+        %s of dataframe and other, element-wise (binary operator `%s`).
+
+        Equivalent to ``%s``, but with support to substitute a fill_value for
+        missing data in one of the inputs.
+
+        Parameters
+        ----------
+        other : Series, DataFrame, or constant
+        axis : {0, 1, 'index', 'columns'}
+            For Series input, axis to match Series index on
+        fill_value : None or float value, default None
+            Fill missing (NaN) values with this value. If both DataFrame locations are
+            missing, the result will be missing
+        level : int or name
+            Broadcast across a level, matching Index values on the
+            passed MultiIndex level
+
+        Notes
+        -----
+        Mismatched indices will be unioned together
+
+        Returns
+        -------
+        result : DataFrame
+
+        See also
+        --------
+        DataFrame.%s
+        """ % (op_desc['desc'], op_name, equiv, op_desc['reverse'])
+    else:
+        doc = _arith_doc_FRAME % name
+
+    @Appender(doc)
     def f(self, other, axis=default_axis, level=None, fill_value=None):
         if isinstance(other, pd.DataFrame):    # Another DataFrame
             return self._combine_frame(other, na_op, fill_value, level)
diff --git a/pandas/core/panel.py b/pandas/core/panel.py
--- a/pandas/core/panel.py
+++ b/pandas/core/panel.py
@@ -28,6 +28,8 @@
 import pandas.core.ops as ops
 import pandas.computation.expressions as expressions
 from pandas import lib
+from pandas.core.ops import _op_descriptions
+
 
 _shared_doc_kwargs = dict(
     axes='items, major_axis, minor_axis',
@@ -1435,7 +1437,7 @@ def _add_aggregate_operations(cls, use_numexpr=True):
 ----------
 other : %s or %s""" % (cls._constructor_sliced.__name__, cls.__name__) + """
 axis : {""" + ', '.join(cls._AXIS_ORDERS) + "}" + """
-Axis to broadcast over
+    Axis to broadcast over
 
 Returns
 -------
@@ -1457,8 +1459,36 @@ def na_op(x, y):
                 result = com._fill_zeros(result, x, y, name, fill_zeros)
                 return result
 
-            @Substitution(name)
-            @Appender(_agg_doc)
+            if name in _op_descriptions:
+                op_name = name.replace('__', '')
+                op_desc = _op_descriptions[op_name]
+                if op_desc['reversed']:
+                    equiv = 'other ' + op_desc['op'] + ' panel'
+                else:
+                    equiv = 'panel ' + op_desc['op'] + ' other'
+
+                _op_doc = """
+                %%s of series and other, element-wise (binary operator `%%s`).
+                Equivalent to ``%%s``.
+
+                Parameters
+                ----------
+                other : %s or %s""" % (cls._constructor_sliced.__name__, cls.__name__) + """
+                axis : {""" + ', '.join(cls._AXIS_ORDERS) + "}" + """
+                    Axis to broadcast over
+
+                Returns
+                -------
+                """ + cls.__name__ + """
+
+                See also
+                --------
+                """ + cls.__name__ + ".%s\n"
+                doc = _op_doc % (op_desc['desc'], op_name, equiv, op_desc['reverse'])
+            else:
+                doc = _agg_doc % name
+
+            @Appender(doc)
             def f(self, other, axis=0):
                 return self._combine(other, na_op, axis=axis)
             f.__name__ = name


Test patch:
diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py
--- a/pandas/tests/test_base.py
+++ b/pandas/tests/test_base.py
@@ -244,6 +244,26 @@ def check_ops_properties(self, props, filter=None, ignore_failures=False):
                     else:
                         self.assertRaises(AttributeError, lambda : getattr(o,op))
 
+    def test_binary_ops_docs(self):
+        from pandas import DataFrame, Panel
+        op_map = {'add': '+',
+                  'sub': '-',
+                  'mul': '*',
+                  'mod': '%',
+                  'pow': '**',
+                  'truediv': '/',
+                  'floordiv': '//'}
+        for op_name in ['add', 'sub', 'mul', 'mod', 'pow', 'truediv', 'floordiv']:
+            for klass in [Series, DataFrame, Panel]:
+                operand1 = klass.__name__.lower()
+                operand2 = 'other'
+                op = op_map[op_name]
+                expected_str = ' '.join([operand1, op, operand2])
+                self.assertTrue(expected_str in getattr(klass, op_name).__doc__)
+
+                # reverse version of the binary ops
+                expected_str = ' '.join([operand2, op, operand1])
+                self.assertTrue(expected_str in getattr(klass, 'r' + op_name).__doc__)
 
 class TestIndexOps(Ops):