Repository: pandas-dev/pandas

Issue:
Labels mismatch when adding series with repeated index values in 0.19rc1
This showed up in the dask.dataframe test suite when testing against the 0.19 release candidate.  I'm unsure if this was an intended change or not:
### Pandas 0.18

``` python
In [1]: import pandas as pd

In [2]: a = pd.Series([1, 2], index=[1, 1])

In [3]: b = pd.Series([10, 10], index=[1, 2])

In [4]: a
Out[4]: 
1    1
1    2
dtype: int64

In [5]: b
Out[5]: 
1    10
2    10
dtype: int64

In [6]: a + b
Out[6]: 
1    11.0
1    12.0
2     NaN
dtype: float64
```
### Pandas 0.19rc1

``` python
In [1]: import pandas as pd

In [2]: a = pd.Series([1, 2], index=[1, 1])

In [3]: b = pd.Series([10, 10], index=[1, 2])

In [4]: a
Out[4]: 
1    1
1    2
dtype: int64

In [5]: b
Out[5]: 
1    10
2    10
dtype: int64

In [6]: a + b
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-f96fb8f649b6> in <module>()
----> 1 a + b

/home/mrocklin/Software/anaconda/lib/python3.5/site-packages/pandas/core/ops.py in wrapper(left, right, name, na_op)
    671             return NotImplemented
    672 
--> 673         left, right = _align_method_SERIES(left, right)
    674 
    675         converted = _Op.get_op(left, right, name, na_op)

/home/mrocklin/Software/anaconda/lib/python3.5/site-packages/pandas/core/ops.py in _align_method_SERIES(left, right, align_asobject)
    613                                                 return_indexers=True)
    614             # if DatetimeIndex have different tz, convert to UTC
--> 615             left.index = index
    616             right.index = index
    617 

/home/mrocklin/Software/anaconda/lib/python3.5/site-packages/pandas/core/generic.py in __setattr__(self, name, value)
   2754         try:
   2755             object.__getattribute__(self, name)
-> 2756             return object.__setattr__(self, name, value)
   2757         except AttributeError:
   2758             pass

pandas/src/properties.pyx in pandas.lib.AxisProperty.__set__ (pandas/lib.c:44336)()

/home/mrocklin/Software/anaconda/lib/python3.5/site-packages/pandas/core/series.py in _set_axis(self, axis, labels, fastpath)
    306         object.__setattr__(self, '_index', labels)
    307         if not fastpath:
--> 308             self._data.set_axis(axis, labels)
    309 
    310     def _set_subtyp(self, is_all_dates):

/home/mrocklin/Software/anaconda/lib/python3.5/site-packages/pandas/core/internals.py in set_axis(self, axis, new_labels)
   2774             raise ValueError('Length mismatch: Expected axis has %d elements, '
   2775                              'new values have %d elements' %
-> 2776                              (old_len, new_len))
   2777 
   2778         self.axes[axis] = new_labels

ValueError: Length mismatch: Expected axis has 3 elements, new values have 5 elements
```



Discussion:
http://pandas-docs.github.io/pandas-docs-travis/whatsnew.html#series-operators-for-different-indexes

this may now not be well defined for duplicates 

Alrighty.  Thanks for the pointer.  

I don't think this change was intentional. The whatsnew also says "Arithmetic operators align both index (no changes)."

@jorisvandenbossche agreed that it wasn't intentional, but does the old behavior make sense under the new equality rules (or the old ones for that matter)?

If we go from the "align both index" description (of course not sure if this is also how it is in the docs or is meant initially), I would expect this yes:

```

In [19]: a_temp, b_temp = a.align(b)

In [20]: a_temp
Out[20]: 
1    1.0
1    2.0
2    NaN
dtype: float64

In [21]: b_temp
Out[21]: 
1    10
1    10
2    10
dtype: int64

In [22]: a_temp + b_temp
Out[22]: 
1    11.0
1    12.0
2     NaN
```

Whoops, yeah obviously that's the expected output.

I wouldn't call it that "obvious" :-), but it does follow from the rules in some way


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -20,6 +20,7 @@
                                  is_numeric_dtype,
                                  is_datetime64_dtype,
                                  is_timedelta64_dtype,
+                                 is_datetime64tz_dtype,
                                  is_list_like,
                                  is_dict_like,
                                  is_re_compilable)
@@ -4438,13 +4439,23 @@ def _align_frame(self, other, join='outer', axis=None, level=None,
             left = left.fillna(axis=fill_axis, method=method, limit=limit)
             right = right.fillna(axis=fill_axis, method=method, limit=limit)
 
+        # if DatetimeIndex have different tz, convert to UTC
+        if is_datetime64tz_dtype(left.index):
+            if left.index.tz != right.index.tz:
+                if join_index is not None:
+                    left.index = join_index
+                    right.index = join_index
+
         return left.__finalize__(self), right.__finalize__(other)
 
     def _align_series(self, other, join='outer', axis=None, level=None,
                       copy=True, fill_value=None, method=None, limit=None,
                       fill_axis=0):
+
+        is_series = isinstance(self, ABCSeries)
+
         # series/series compat, other must always be a Series
-        if isinstance(self, ABCSeries):
+        if is_series:
             if axis:
                 raise ValueError('cannot align series to a series other than '
                                  'axis 0')
@@ -4503,6 +4514,15 @@ def _align_series(self, other, join='outer', axis=None, level=None,
             left = left.fillna(fill_value, method=method, limit=limit,
                                axis=fill_axis)
             right = right.fillna(fill_value, method=method, limit=limit)
+
+        # if DatetimeIndex have different tz, convert to UTC
+        if is_series or (not is_series and axis == 0):
+            if is_datetime64tz_dtype(left.index):
+                if left.index.tz != right.index.tz:
+                    if join_index is not None:
+                        left.index = join_index
+                        right.index = join_index
+
         return left.__finalize__(self), right.__finalize__(other)
 
     def _where(self, cond, other=np.nan, inplace=False, axis=None, level=None,
diff --git a/pandas/core/ops.py b/pandas/core/ops.py
--- a/pandas/core/ops.py
+++ b/pandas/core/ops.py
@@ -622,12 +622,6 @@ def _align_method_SERIES(left, right, align_asobject=False):
 
             left, right = left.align(right, copy=False)
 
-            index, lidx, ridx = left.index.join(right.index, how='outer',
-                                                return_indexers=True)
-            # if DatetimeIndex have different tz, convert to UTC
-            left.index = index
-            right.index = index
-
     return left, right
 
 


Test patch:
diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py
--- a/pandas/tests/series/test_operators.py
+++ b/pandas/tests/series/test_operators.py
@@ -1810,3 +1810,11 @@ def test_dti_tz_convert_to_utc(self):
 
         res = Series([1, 2], index=idx1) + Series([1, 1], index=idx2)
         assert_series_equal(res, Series([np.nan, 3, np.nan], index=base))
+
+    def test_op_duplicate_index(self):
+        # GH14227
+        s1 = Series([1, 2], index=[1, 1])
+        s2 = Series([10, 10], index=[1, 2])
+        result = s1 + s2
+        expected = pd.Series([11, 12, np.nan], index=[1, 1, 2])
+        assert_series_equal(result, expected)
diff --git a/pandas/tseries/tests/test_timezones.py b/pandas/tseries/tests/test_timezones.py
--- a/pandas/tseries/tests/test_timezones.py
+++ b/pandas/tseries/tests/test_timezones.py
@@ -1290,6 +1290,28 @@ def test_align_aware(self):
         self.assertEqual(df1.index.tz, new1.index.tz)
         self.assertEqual(df2.index.tz, new2.index.tz)
 
+        # # different timezones convert to UTC
+
+        # frame
+        df1_central = df1.tz_convert('US/Central')
+        new1, new2 = df1.align(df1_central)
+        self.assertEqual(new1.index.tz, pytz.UTC)
+        self.assertEqual(new2.index.tz, pytz.UTC)
+
+        # series
+        new1, new2 = df1[0].align(df1_central[0])
+        self.assertEqual(new1.index.tz, pytz.UTC)
+        self.assertEqual(new2.index.tz, pytz.UTC)
+
+        # combination
+        new1, new2 = df1.align(df1_central[0], axis=0)
+        self.assertEqual(new1.index.tz, pytz.UTC)
+        self.assertEqual(new2.index.tz, pytz.UTC)
+
+        df1[0].align(df1_central, axis=0)
+        self.assertEqual(new1.index.tz, pytz.UTC)
+        self.assertEqual(new2.index.tz, pytz.UTC)
+
     def test_append_aware(self):
         rng1 = date_range('1/1/2011 01:00', periods=1, freq='H',
                           tz='US/Eastern')