Skip to content

Commit 2e7f8c1

Browse files
committed
ENH: Support inplace clip (#15388)
1 parent a8a497f commit 2e7f8c1

File tree

2 files changed

+68
-18
lines changed

2 files changed

+68
-18
lines changed

pandas/core/generic.py

+37-14
Original file line numberDiff line numberDiff line change
@@ -4119,8 +4119,7 @@ def isnull(self):
41194119
def notnull(self):
41204120
return notnull(self).__finalize__(self)
41214121

4122-
def _clip_with_scalar(self, lower, upper):
4123-
4122+
def _clip_with_scalar(self, lower, upper, inplace=False):
41244123
if ((lower is not None and np.any(isnull(lower))) or
41254124
(upper is not None and np.any(isnull(upper)))):
41264125
raise ValueError("Cannot use an NA value as a clip threshold")
@@ -4136,10 +4135,16 @@ def _clip_with_scalar(self, lower, upper):
41364135
if np.any(mask):
41374136
result[mask] = np.nan
41384137

4139-
return self._constructor(
4140-
result, **self._construct_axes_dict()).__finalize__(self)
4138+
axes_dict = self._construct_axes_dict()
4139+
result = self._constructor(result, **axes_dict).__finalize__(self)
4140+
4141+
if inplace:
4142+
self._update_inplace(result)
4143+
else:
4144+
return result
41414145

4142-
def clip(self, lower=None, upper=None, axis=None, *args, **kwargs):
4146+
def clip(self, lower=None, upper=None, axis=None, inplace=False,
4147+
*args, **kwargs):
41434148
"""
41444149
Trim values at input threshold(s).
41454150
@@ -4149,6 +4154,9 @@ def clip(self, lower=None, upper=None, axis=None, *args, **kwargs):
41494154
upper : float or array_like, default None
41504155
axis : int or string axis name, optional
41514156
Align object with lower and upper along the given axis.
4157+
inplace : boolean, default False
4158+
Whether to perform the operation in place on the data
4159+
.. versionadded:: 0.21.0
41524160
41534161
Returns
41544162
-------
@@ -4191,6 +4199,8 @@ def clip(self, lower=None, upper=None, axis=None, *args, **kwargs):
41914199
if isinstance(self, ABCPanel):
41924200
raise NotImplementedError("clip is not supported yet for panels")
41934201

4202+
inplace = validate_bool_kwarg(inplace, 'inplace')
4203+
41944204
axis = nv.validate_clip_with_axis(axis, args, kwargs)
41954205

41964206
# GH 2747 (arguments were reversed)
@@ -4201,17 +4211,20 @@ def clip(self, lower=None, upper=None, axis=None, *args, **kwargs):
42014211
# fast-path for scalars
42024212
if ((lower is None or (is_scalar(lower) and is_number(lower))) and
42034213
(upper is None or (is_scalar(upper) and is_number(upper)))):
4204-
return self._clip_with_scalar(lower, upper)
4214+
return self._clip_with_scalar(lower, upper, inplace=inplace)
42054215

42064216
result = self
42074217
if lower is not None:
4208-
result = result.clip_lower(lower, axis)
4218+
result = result.clip_lower(lower, axis, inplace=inplace)
42094219
if upper is not None:
4210-
result = result.clip_upper(upper, axis)
4220+
if inplace:
4221+
result = self
4222+
4223+
result = result.clip_upper(upper, axis, inplace=inplace)
42114224

42124225
return result
42134226

4214-
def clip_upper(self, threshold, axis=None):
4227+
def clip_upper(self, threshold, axis=None, inplace=False):
42154228
"""
42164229
Return copy of input with values above given value(s) truncated.
42174230
@@ -4220,6 +4233,9 @@ def clip_upper(self, threshold, axis=None):
42204233
threshold : float or array_like
42214234
axis : int or string axis name, optional
42224235
Align object with threshold along the given axis.
4236+
inplace : boolean, default False
4237+
Whether to perform the operation in place on the data
4238+
.. versionadded:: 0.21.0
42234239
42244240
See Also
42254241
--------
@@ -4233,12 +4249,14 @@ def clip_upper(self, threshold, axis=None):
42334249
raise ValueError("Cannot use an NA value as a clip threshold")
42344250

42354251
if is_scalar(threshold) and is_number(threshold):
4236-
return self._clip_with_scalar(None, threshold)
4252+
return self._clip_with_scalar(None, threshold, inplace=inplace)
4253+
4254+
inplace = validate_bool_kwarg(inplace, 'inplace')
42374255

42384256
subset = self.le(threshold, axis=axis) | isnull(self)
4239-
return self.where(subset, threshold, axis=axis)
4257+
return self.where(subset, threshold, axis=axis, inplace=inplace)
42404258

4241-
def clip_lower(self, threshold, axis=None):
4259+
def clip_lower(self, threshold, axis=None, inplace=False):
42424260
"""
42434261
Return copy of the input with values below given value(s) truncated.
42444262
@@ -4247,6 +4265,9 @@ def clip_lower(self, threshold, axis=None):
42474265
threshold : float or array_like
42484266
axis : int or string axis name, optional
42494267
Align object with threshold along the given axis.
4268+
inplace : boolean, default False
4269+
Whether to perform the operation in place on the data
4270+
.. versionadded:: 0.21.0
42504271
42514272
See Also
42524273
--------
@@ -4260,10 +4281,12 @@ def clip_lower(self, threshold, axis=None):
42604281
raise ValueError("Cannot use an NA value as a clip threshold")
42614282

42624283
if is_scalar(threshold) and is_number(threshold):
4263-
return self._clip_with_scalar(threshold, None)
4284+
return self._clip_with_scalar(threshold, None, inplace=inplace)
4285+
4286+
inplace = validate_bool_kwarg(inplace, 'inplace')
42644287

42654288
subset = self.ge(threshold, axis=axis) | isnull(self)
4266-
return self.where(subset, threshold, axis=axis)
4289+
return self.where(subset, threshold, axis=axis, inplace=inplace)
42674290

42684291
def groupby(self, by=None, axis=0, level=None, as_index=True, sort=True,
42694292
group_keys=True, squeeze=False, **kwargs):

pandas/tests/frame/test_analytics.py

+31-4
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,7 @@ def test_built_in_round(self):
18071807

18081808
def test_clip(self):
18091809
median = self.frame.median().median()
1810+
original = self.frame.copy()
18101811

18111812
capped = self.frame.clip_upper(median)
18121813
assert not (capped.values > median).any()
@@ -1817,6 +1818,25 @@ def test_clip(self):
18171818
double = self.frame.clip(upper=median, lower=median)
18181819
assert not (double.values != median).any()
18191820

1821+
# Verify that self.frame was not changed inplace
1822+
assert (self.frame.values == original.values).all()
1823+
1824+
def test_inplace_clip(self):
1825+
# GH #15388
1826+
median = self.frame.median().median()
1827+
frame_copy = self.frame.copy()
1828+
1829+
frame_copy.clip_upper(median, inplace=True)
1830+
assert not (frame_copy.values > median).any()
1831+
frame_copy = self.frame.copy()
1832+
1833+
frame_copy.clip_lower(median, inplace=True)
1834+
assert not (frame_copy.values < median).any()
1835+
frame_copy = self.frame.copy()
1836+
1837+
frame_copy.clip(upper=median, lower=median, inplace=True)
1838+
assert not (frame_copy.values != median).any()
1839+
18201840
def test_dataframe_clip(self):
18211841
# GH #2747
18221842
df = DataFrame(np.random.randn(1000, 2))
@@ -1843,18 +1863,25 @@ def test_clip_mixed_numeric(self):
18431863
'B': [1., np.nan, 2.]})
18441864
tm.assert_frame_equal(result, expected, check_like=True)
18451865

1846-
def test_clip_against_series(self):
1866+
@pytest.mark.parametrize("inplace", [True, False])
1867+
def test_clip_against_series(self, inplace):
18471868
# GH #6966
18481869

18491870
df = DataFrame(np.random.randn(1000, 2))
18501871
lb = Series(np.random.randn(1000))
18511872
ub = lb + 1
18521873

1853-
clipped_df = df.clip(lb, ub, axis=0)
1874+
original = df.copy()
1875+
1876+
1877+
clipped_df = df.clip(lb, ub, axis=0, inplace=inplace)
1878+
1879+
if inplace:
1880+
clipped_df = df
18541881

18551882
for i in range(2):
1856-
lb_mask = df.iloc[:, i] <= lb
1857-
ub_mask = df.iloc[:, i] >= ub
1883+
lb_mask = original.iloc[:, i] <= lb
1884+
ub_mask = original.iloc[:, i] >= ub
18581885
mask = ~lb_mask & ~ub_mask
18591886

18601887
result = clipped_df.loc[lb_mask, i]

0 commit comments

Comments
 (0)