diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 033f47f0c994d..3dcee72770272 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -929,6 +929,7 @@ Numeric - Bug in arithmetic operations on :class:`Series` not propagating mask when combining masked dtypes and numpy dtypes (:issue:`45810`, :issue:`42630`) - Bug in DataFrame reduction methods (e.g. :meth:`DataFrame.sum`) with object dtype, ``axis=1`` and ``numeric_only=False`` would not be coerced to float (:issue:`49551`) - Bug in :meth:`DataFrame.sem` and :meth:`Series.sem` where an erroneous ``TypeError`` would always raise when using data backed by an :class:`ArrowDtype` (:issue:`49759`) +- Bug in :meth:`Series.__add__` casting to object for list and masked :class:`Series` (:issue:`22962`) Conversion ^^^^^^^^^^ diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index aaa7c706d95bb..77735add89bf7 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -621,6 +621,27 @@ def _arith_method(self, other, op): op_name = op.__name__ omask = None + if ( + not hasattr(other, "dtype") + and is_list_like(other) + and len(other) == len(self) + ): + # Try inferring masked dtype instead of casting to object + inferred_dtype = lib.infer_dtype(other, skipna=True) + if inferred_dtype == "integer": + from pandas.core.arrays import IntegerArray + + other = IntegerArray._from_sequence(other) + elif inferred_dtype in ["floating", "mixed-integer-float"]: + from pandas.core.arrays import FloatingArray + + other = FloatingArray._from_sequence(other) + + elif inferred_dtype in ["boolean"]: + from pandas.core.arrays import BooleanArray + + other = BooleanArray._from_sequence(other) + if isinstance(other, BaseMaskedArray): other, omask = other._data, other._mask diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index 35096540c70d3..a7f73f2e22fca 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -325,6 +325,27 @@ def test_mask_div_propagate_na_for_non_na_dtype(self): result = ser2 / ser1 tm.assert_series_equal(result, expected) + @pytest.mark.parametrize("val, dtype", [(3, "Int64"), (3.5, "Float64")]) + def test_add_list_to_masked_array(self, val, dtype): + # GH#22962 + ser = Series([1, None, 3], dtype="Int64") + result = ser + [1, None, val] + expected = Series([2, None, 3 + val], dtype=dtype) + tm.assert_series_equal(result, expected) + + result = [1, None, val] + ser + tm.assert_series_equal(result, expected) + + def test_add_list_to_masked_array_boolean(self): + # GH#22962 + ser = Series([True, None, False], dtype="boolean") + result = ser + [True, None, True] + expected = Series([True, None, True], dtype="boolean") + tm.assert_series_equal(result, expected) + + result = [True, None, True] + ser + tm.assert_series_equal(result, expected) + # ------------------------------------------------------------------ # Comparisons