Repository: pandas-dev/pandas

Issue:
BUG/API: Series arithmetic ops inconsistently hold names
Based on `Index` behavior, `Series` arithmetic should reset name when names are different?

```
import pandas as pd
s1 = pd.Series([1, 2, 3], name='a')
s2 = pd.Series([3, 4, 5], name='b')

(s1 + s2).name
# None

s1.add(s2).name
# a
```



Discussion:
This looks like just need to pass `name` to the constructor https://github.com/pydata/pandas/blob/master/pandas/core/series.py#L1510

The fix is in ops.py - https://github.com/pydata/pandas/blob/master/pandas/core/ops.py#L509 - I added a check for `if name is None` and used the name from left if this is True. This however breaks an explicit test - https://github.com/pydata/pandas/blob/master/pandas/tests/test_series.py#L266. If the names don't match, neither is selected. Should I update this test as part of the fix as well?

@vikramls I don't think we want to change that behavior (if names are different, the resulting name should be `None`). So I guess this needs a different fix.

@shoyer - The test-case checks the same scenario that this bug is addressed to. Does this mean that the bug is not valid?

from test_series.py:

``` python
    result = self.ts + cp # self.ts and cp are both Series objects
    self.assertIsNone(result.name)
```

Series.add is executed via the `_binop` method wheres Series + Series is executed via the added `__add__` method.

@vikramls To clarify, I think this behavior is correct:

```
(s1 + s2).name
# None
```

and this behavior is the bug:

```
s1.add(s2).name
# a
```

@shoyer, thanks for the clarification. In this case the bug is triggered by the call to `__finalize__` which copies attributes from `other` to the newly created object. One fix is to remove the call to `__finalize__` in `_binop` but it doesn't seem right to special-case `_binop` in this manner.


Resolving patch:
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -1508,7 +1508,12 @@ def _binop(self, other, func, level=None, fill_value=None):
 
         result = func(this_vals, other_vals)
         name = _maybe_match_name(self, other)
-        return self._constructor(result, index=new_index).__finalize__(self)
+        result = self._constructor(result, index=new_index, name=name)
+        result = result.__finalize__(self)
+        if name is None:
+            # When name is None, __finalize__ overwrites current name
+            result.name = None
+        return result
 
     def combine(self, other, func, fill_value=nan):
         """


Test patch:
diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py
--- a/pandas/tests/test_series.py
+++ b/pandas/tests/test_series.py
@@ -264,10 +264,11 @@ def test_tab_completion(self):
         self.assertTrue('dt' not in dir(s))
 
     def test_binop_maybe_preserve_name(self):
-
         # names match, preserve
         result = self.ts * self.ts
         self.assertEqual(result.name, self.ts.name)
+        result = self.ts.mul(self.ts)
+        self.assertEqual(result.name, self.ts.name)
 
         result = self.ts * self.ts[:-2]
         self.assertEqual(result.name, self.ts.name)
@@ -277,6 +278,22 @@ def test_binop_maybe_preserve_name(self):
         cp.name = 'something else'
         result = self.ts + cp
         self.assertIsNone(result.name)
+        result = self.ts.add(cp)
+        self.assertIsNone(result.name)
+
+        ops = ['add', 'sub', 'mul', 'div', 'truediv', 'floordiv', 'mod', 'pow']
+        ops = ops + ['r' + op for op in ops]
+        for op in ops:
+            # names match, preserve
+            s = self.ts.copy()
+            result = getattr(s, op)(s)
+            self.assertEqual(result.name, self.ts.name)
+
+            # names don't match, don't preserve
+            cp = self.ts.copy()
+            cp.name = 'changed'
+            result = getattr(s, op)(cp)
+            self.assertIsNone(result.name)
 
     def test_combine_first_name(self):
         result = self.ts.combine_first(self.ts[:5])