From 782eddcb866b677b3751d6ff92a609412ed10078 Mon Sep 17 00:00:00 2001 From: jschendel Date: Wed, 18 Oct 2017 15:47:32 -0600 Subject: [PATCH] BUG: Fix alignment of list-like objects --- doc/source/whatsnew/v0.21.0.txt | 1 + pandas/core/ops.py | 27 +++++++++++++++------------ pandas/tests/frame/test_operators.py | 20 +++++++++++++++----- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 2b2dd4915b560..18f8858748df5 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -1049,6 +1049,7 @@ Reshaping - Bug in :func:`pivot_table` where the result's columns did not preserve the categorical dtype of ``columns`` when ``dropna`` was ``False`` (:issue:`17842`) - Bug in ``DataFrame.drop_duplicates`` where dropping with non-unique column names raised a ``ValueError`` (:issue:`17836`) - Bug in :func:`unstack` which, when called on a list of levels, would discard the ``fillna`` argument (:issue:`13971`) +- Bug in the alignment of ``range`` objects and other list-likes with ``DataFrame`` leading to operations being performed row-wise instead of column-wise (:issue:`17901`) Numeric ^^^^^^^ diff --git a/pandas/core/ops.py b/pandas/core/ops.py index f0bd2477eec07..b6fc47c04f174 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -36,6 +36,7 @@ from pandas.core.dtypes.cast import maybe_upcast_putmask, find_common_type from pandas.core.dtypes.generic import ( ABCSeries, + ABCDataFrame, ABCIndex, ABCPeriodIndex, ABCDateOffset) @@ -710,7 +711,7 @@ def safe_na_op(lvalues, rvalues): def wrapper(left, right, name=name, na_op=na_op): - if isinstance(right, pd.DataFrame): + if isinstance(right, ABCDataFrame): return NotImplemented left, right = _align_method_SERIES(left, right) @@ -834,7 +835,7 @@ def wrapper(self, other, axis=None): raise ValueError(msg) return self._constructor(na_op(self.values, other.values), index=self.index, name=name) - elif isinstance(other, pd.DataFrame): # pragma: no cover + elif isinstance(other, ABCDataFrame): # pragma: no cover return NotImplemented elif isinstance(other, (np.ndarray, pd.Index)): # do not check length of zerodim array @@ -941,7 +942,7 @@ def wrapper(self, other): return filler(self._constructor(na_op(self.values, other.values), index=self.index, name=name)) - elif isinstance(other, pd.DataFrame): + elif isinstance(other, ABCDataFrame): return NotImplemented else: @@ -1165,10 +1166,7 @@ def to_series(right): right = left._constructor_sliced(right, index=left.columns) return right - if isinstance(right, (list, tuple)): - right = to_series(right) - - elif isinstance(right, np.ndarray) and right.ndim: # skips np scalar + if isinstance(right, np.ndarray): if right.ndim == 1: right = to_series(right) @@ -1182,10 +1180,15 @@ def to_series(right): right = left._constructor(right, index=left.index, columns=left.columns) - else: + elif right.ndim > 2: raise ValueError('Unable to coerce to Series/DataFrame, dim ' 'must be <= 2: {dim}'.format(dim=right.shape)) + elif (is_list_like(right) and + not isinstance(right, (ABCSeries, ABCDataFrame))): + # GH17901 + right = to_series(right) + return right @@ -1252,7 +1255,7 @@ def f(self, other, axis=default_axis, level=None, fill_value=None): other = _align_method_FRAME(self, other, axis) - if isinstance(other, pd.DataFrame): # Another DataFrame + if isinstance(other, ABCDataFrame): # Another DataFrame return self._combine_frame(other, na_op, fill_value, level) elif isinstance(other, ABCSeries): return self._combine_series(other, na_op, fill_value, axis, level) @@ -1300,7 +1303,7 @@ def f(self, other, axis=default_axis, level=None): other = _align_method_FRAME(self, other, axis) - if isinstance(other, pd.DataFrame): # Another DataFrame + if isinstance(other, ABCDataFrame): # Another DataFrame return self._flex_compare_frame(other, na_op, str_rep, level, try_cast=False) @@ -1318,7 +1321,7 @@ def f(self, other, axis=default_axis, level=None): def _comp_method_FRAME(func, name, str_rep, masker=False): @Appender('Wrapper for comparison method {name}'.format(name=name)) def f(self, other): - if isinstance(other, pd.DataFrame): # Another DataFrame + if isinstance(other, ABCDataFrame): # Another DataFrame return self._compare_frame(other, func, str_rep) elif isinstance(other, ABCSeries): return self._combine_series_infer(other, func, try_cast=False) @@ -1411,7 +1414,7 @@ def f(self, other, axis=None): if isinstance(other, self._constructor): return self._compare_constructor(other, na_op, try_cast=False) - elif isinstance(other, (self._constructor_sliced, pd.DataFrame, + elif isinstance(other, (self._constructor_sliced, ABCDataFrame, ABCSeries)): raise Exception("input needs alignment for this object [{object}]" .format(object=self._constructor)) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 10a9853b8a5b4..986ba54314192 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import print_function - +from collections import deque from datetime import datetime import operator @@ -10,7 +10,7 @@ from numpy import nan, random import numpy as np -from pandas.compat import lrange +from pandas.compat import lrange, range from pandas import compat from pandas import (DataFrame, Series, MultiIndex, Timestamp, date_range) @@ -749,6 +749,15 @@ def test_arith_non_pandas_object(self): added = DataFrame(df.values + val3, index=df.index, columns=df.columns) assert_frame_equal(df.add(val3), added) + @pytest.mark.parametrize('values', [[1, 2], (1, 2), np.array([1, 2]), + range(1, 3), deque([1, 2])]) + def test_arith_alignment_non_pandas_object(self, values): + # GH 17901 + df = DataFrame({'A': [1, 1], 'B': [1, 1]}) + expected = DataFrame({'A': [2, 2], 'B': [3, 3]}) + result = df + values + assert_frame_equal(result, expected) + def test_combineFrame(self): frame_copy = self.frame.reindex(self.frame.index[::2]) @@ -1200,8 +1209,8 @@ def test_alignment_non_pandas(self): df = pd.DataFrame(np.random.randn(3, 3), index=index, columns=columns) align = pd.core.ops._align_method_FRAME - - for val in [[1, 2, 3], (1, 2, 3), np.array([1, 2, 3], dtype=np.int64)]: + for val in [[1, 2, 3], (1, 2, 3), np.array([1, 2, 3], dtype=np.int64), + range(1, 4)]: tm.assert_series_equal(align(df, val, 'index'), Series([1, 2, 3], index=df.index)) @@ -1210,7 +1219,8 @@ def test_alignment_non_pandas(self): # length mismatch msg = 'Unable to coerce to Series, length must be 3: given 2' - for val in [[1, 2], (1, 2), np.array([1, 2])]: + for val in [[1, 2], (1, 2), np.array([1, 2]), range(1, 3)]: + with tm.assert_raises_regex(ValueError, msg): align(df, val, 'index')