Skip to content

Commit 2930f1e

Browse files
ENH: implement fill_value for df.add(other=Series) pandas-dev#13488
1 parent c20528e commit 2930f1e

File tree

4 files changed

+131
-28
lines changed

4 files changed

+131
-28
lines changed

doc/source/whatsnew/v1.0.2.rst

+10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ including other versions of pandas.
1010

1111
.. ---------------------------------------------------------------------------
1212
13+
.. _whatsnew_102.enhancements:
14+
15+
Enhancements
16+
~~~~~~~~~~~~
17+
18+
- :meth:`DataFrame.add` now accepts a ``fill_value`` not equal to ``None`` when ``other`` parameter equals :class:`Series`.
19+
Same enhancement also available with other binary operators: :meth:`~DataFrame.sub`, :meth:`~DataFrame.mul`, :meth:`~DataFrame.div`, :meth:`~DataFrame.truediv`, :meth:`~DataFrame.floordiv`, :meth:`~DataFrame.mod`, :meth:`~DataFrame.pow`. (:issue:`13488`)
20+
21+
.. ---------------------------------------------------------------------------
22+
1323
.. _whatsnew_102.regressions:
1424

1525
Fixed regressions

pandas/core/ops/__init__.py

+34-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
from pandas._typing import ArrayLike, Level
1515
from pandas.util._decorators import Appender
1616

17-
from pandas.core.dtypes.common import is_list_like, is_timedelta64_dtype
17+
from pandas.core.dtypes.common import (
18+
is_bool,
19+
is_list_like,
20+
is_number,
21+
is_timedelta64_dtype,
22+
)
1823
from pandas.core.dtypes.generic import ABCDataFrame, ABCIndexClass, ABCSeries
1924
from pandas.core.dtypes.missing import isna
2025

@@ -341,7 +346,11 @@ def fill_binop(left, right, fill_value):
341346
left = left.copy()
342347
left[left_mask & mask] = fill_value
343348

344-
if right_mask.any():
349+
if is_bool(right_mask):
350+
if right_mask:
351+
right = left._constructor(right, index=left.index)
352+
right[right_mask & mask] = fill_value
353+
elif right_mask.any():
345354
# Avoid making a copy if we can
346355
right = right.copy()
347356
right[right_mask & mask] = fill_value
@@ -585,7 +594,7 @@ def flex_wrapper(self, other, level=None, fill_value=None, axis=0):
585594
# DataFrame
586595

587596

588-
def _combine_series_frame(left, right, func, axis: int):
597+
def _combine_series_frame(left, right, func, axis: int, fill_value=None):
589598
"""
590599
Apply binary operator `func` to self, other using alignment and fill
591600
conventions determined by the axis argument.
@@ -596,16 +605,29 @@ def _combine_series_frame(left, right, func, axis: int):
596605
right : Series
597606
func : binary operator
598607
axis : {0, 1}
608+
fill_value : numeric, optional
599609
600610
Returns
601611
-------
602612
result : DataFrame
603613
"""
614+
if fill_value is None:
615+
_arith_op = func
616+
617+
else:
618+
619+
def _arith_op(left, right):
620+
left, right = fill_binop(left, right, fill_value)
621+
return func(left, right)
622+
604623
# We assume that self.align(other, ...) has already been called
605624
if axis == 0:
606-
new_data = left._combine_match_index(right, func)
625+
if fill_value is not None:
626+
new_data = dispatch_to_series(left, right, _arith_op, axis=0)
627+
else:
628+
new_data = left._combine_match_index(right, _arith_op)
607629
else:
608-
new_data = dispatch_to_series(left, right, func, axis="columns")
630+
new_data = dispatch_to_series(left, right, _arith_op, axis="columns")
609631

610632
return left._construct_result(new_data)
611633

@@ -771,6 +793,12 @@ def f(self, other, axis=default_axis, level=None, fill_value=None):
771793
if _should_reindex_frame_op(self, other, axis, default_axis, fill_value, level):
772794
return _frame_arith_method_with_reindex(self, other, op)
773795

796+
if not is_number(fill_value) and fill_value is not None:
797+
raise TypeError(
798+
"fill_value must be numeric or None. "
799+
f"Got {type(fill_value).__name__}"
800+
)
801+
774802
self, other = _align_method_FRAME(self, other, axis, flex=True, level=level)
775803

776804
if isinstance(other, ABCDataFrame):
@@ -787,11 +815,8 @@ def f(self, other, axis=default_axis, level=None, fill_value=None):
787815
pass_op = op if axis in [0, "columns", None] else na_op
788816
pass_op = pass_op if not is_logical else op
789817

790-
if fill_value is not None:
791-
raise NotImplementedError(f"fill_value {fill_value} not supported.")
792-
793818
axis = self._get_axis_number(axis) if axis is not None else 1
794-
return _combine_series_frame(self, other, pass_op, axis=axis)
819+
return _combine_series_frame(self, other, pass_op, axis, fill_value)
795820
else:
796821
# in this case we always have `np.ndim(other) == 0`
797822
if fill_value is not None:

pandas/core/ops/docstrings.py

+30
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,36 @@ def _make_flex_doc(op_name, typ):
478478
triangle 4 181
479479
rectangle 5 361
480480
481+
Add Series by axis when values are missing
482+
483+
>>> a = pd.Series([2, 4], index=['circle', 'triangle'])
484+
485+
>>> df.add(a, axis=0)
486+
angles degrees
487+
circle 2.0 362.0
488+
rectangle NaN NaN
489+
triangle 7.0 184.0
490+
491+
>>> df.add(a, axis=0, fill_value=1)
492+
angles degrees
493+
circle 2.0 362.0
494+
rectangle 5.0 361.0
495+
triangle 7.0 184.0
496+
497+
>>> b = pd.Series([3, 6, 9], index=["angles", "degrees", "scale"])
498+
499+
>>> df.add(b)
500+
angles degrees scale
501+
circle 3 366 NaN
502+
triangle 6 186 NaN
503+
rectangle 7 366 NaN
504+
505+
>>> df.add(b, fill_value=1)
506+
angles degrees scale
507+
circle 3 366 10.0
508+
triangle 6 186 10.0
509+
rectangle 7 366 10.0
510+
481511
Divide by constant with reverse version.
482512
483513
>>> df.div(10)

pandas/tests/frame/test_arithmetic.py

+57-19
Original file line numberDiff line numberDiff line change
@@ -453,12 +453,6 @@ def test_arith_flex_frame_corner(self, float_frame):
453453
result = float_frame[:0].add(float_frame)
454454
tm.assert_frame_equal(result, float_frame * np.nan)
455455

456-
with pytest.raises(NotImplementedError, match="fill_value"):
457-
float_frame.add(float_frame.iloc[0], fill_value=3)
458-
459-
with pytest.raises(NotImplementedError, match="fill_value"):
460-
float_frame.add(float_frame.iloc[0], axis="index", fill_value=3)
461-
462456
def test_arith_flex_series(self, simple_frame):
463457
df = simple_frame
464458

@@ -490,19 +484,6 @@ def test_arith_flex_series(self, simple_frame):
490484
result = df.div(df[0], axis="index")
491485
tm.assert_frame_equal(result, expected)
492486

493-
def test_arith_flex_zero_len_raises(self):
494-
# GH 19522 passing fill_value to frame flex arith methods should
495-
# raise even in the zero-length special cases
496-
ser_len0 = pd.Series([], dtype=object)
497-
df_len0 = pd.DataFrame(columns=["A", "B"])
498-
df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"])
499-
500-
with pytest.raises(NotImplementedError, match="fill_value"):
501-
df.add(ser_len0, fill_value="E")
502-
503-
with pytest.raises(NotImplementedError, match="fill_value"):
504-
df_len0.sub(df["A"], axis=None, fill_value=3)
505-
506487

507488
class TestFrameArithmetic:
508489
def test_td64_op_nat_casting(self):
@@ -774,6 +755,63 @@ def test_frame_single_columns_object_sum_axis_1():
774755
tm.assert_series_equal(result, expected)
775756

776757

758+
@pytest.fixture
759+
def simple_frame_with_na():
760+
df = pd.DataFrame(
761+
[[np.nan, 2.0, 3.0], [4.0, np.nan, 6.0], [7.0, 8.0, 9.0]],
762+
index=["a", "b", "c"],
763+
columns=np.arange(3),
764+
)
765+
return df
766+
767+
768+
@pytest.mark.parametrize(
769+
"axis, series, expected",
770+
[
771+
(
772+
0,
773+
pd.Series([1.0, np.nan, 3.0, 4.0], index=["a", "b", "c", "d"]),
774+
pd.DataFrame(
775+
[
776+
[2.0, 3.0, 4.0],
777+
[5.0, np.nan, 7.0],
778+
[10.0, 11.0, 12.0],
779+
[5.0, 5.0, 5.0],
780+
],
781+
columns=np.arange(3),
782+
index=["a", "b", "c", "d"],
783+
),
784+
),
785+
(
786+
"columns",
787+
pd.Series([np.nan, 2.0, np.nan, 4.0], index=np.arange(4)),
788+
pd.DataFrame(
789+
[[np.nan, 4.0, 4.0, 5.0], [5.0, 3.0, 7.0, 5.0], [8.0, 10.0, 10.0, 5.0]],
790+
index=["a", "b", "c"],
791+
columns=np.arange(4),
792+
),
793+
),
794+
],
795+
)
796+
def test_add_series_to_frame_with_fill(simple_frame_with_na, axis, series, expected):
797+
# Check missing values correctly populated with fill-value when
798+
# adding series to frame, GH#13488.
799+
df = simple_frame_with_na
800+
result = df.add(other=series, axis=axis, fill_value=1)
801+
expected = expected
802+
tm.assert_frame_equal(result, expected)
803+
804+
805+
def test_df_add_with_non_numeric_fill(simple_frame):
806+
# Check non-numeric fill-value raises when adding series to frame, GH#13488.
807+
# Test replaces non-numeric check in removed test_arith_flex_zero_len_raises.
808+
df = simple_frame
809+
ser = pd.Series([1.0, np.nan, 3.0], index=["a", "b", "c"])
810+
811+
with pytest.raises(TypeError, match="fill_value"):
812+
df.add(ser, fill_value="E")
813+
814+
777815
# -------------------------------------------------------------------
778816
# Unsorted
779817
# These arithmetic tests were previously in other files, eventually

0 commit comments

Comments
 (0)