diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 87214c3758d5c..4b3a7aa44e8ab 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -48,20 +48,14 @@ from pandas._libs.tslibs.ccalendar import ( weekday_to_int, ) from pandas.util._exceptions import find_stack_level - from pandas._libs.tslibs.ccalendar cimport ( MONTH_TO_CAL_NUM, dayofweek, get_days_in_month, - get_firstbday, - get_lastbday, ) from pandas._libs.tslibs.conversion cimport localize_pydatetime from pandas._libs.tslibs.dtypes cimport ( - c_OFFSET_RENAMED_FREQSTR, - c_OFFSET_TO_PERIOD_FREQSTR, c_PERIOD_AND_OFFSET_DEPR_FREQSTR, - c_PERIOD_TO_OFFSET_FREQSTR, periods_per_day, ) from pandas._libs.tslibs.nattype cimport ( @@ -5132,7 +5126,7 @@ def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str: f"\'{name}\' is deprecated and will be removed " f"in a future version, please use " f"\'{c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)}\'" - f" instead.", + f"instead.", FutureWarning, stacklevel=find_stack_level(), ) @@ -5144,599 +5138,5 @@ def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str: if _name in c_PERIOD_AND_OFFSET_DEPR_FREQSTR.values(): warnings.warn( f"\'{name}\' is deprecated and will be removed " - f"in a future version, please use " - f"\'{_name}\'" - f" instead.", - FutureWarning, - stacklevel=find_stack_level(), - ) - return _name - - return name - - -def _validate_to_offset_alias(alias: str, is_period: bool) -> None: - if not is_period: - if alias.upper() in c_OFFSET_RENAMED_FREQSTR: - raise ValueError( - f"\'{alias}\' is no longer supported for offsets. Please " - f"use \'{c_OFFSET_RENAMED_FREQSTR.get(alias.upper())}\' " - f"instead." + f"in a future version, please use '{_name}'" ) - if (alias.upper() != alias and - alias.lower() not in {"s", "ms", "us", "ns"} and - alias.upper().split("-")[0].endswith(("S", "E"))): - raise ValueError(INVALID_FREQ_ERR_MSG.format(alias)) - if ( - is_period and - alias in c_OFFSET_TO_PERIOD_FREQSTR and - alias != c_OFFSET_TO_PERIOD_FREQSTR[alias] - ): - alias_msg = c_OFFSET_TO_PERIOD_FREQSTR.get(alias) - raise ValueError( - f"for Period, please use \'{alias_msg}\' " - f"instead of \'{alias}\'" - ) - - -# TODO: better name? -def _get_offset(name: str) -> BaseOffset: - """ - Return DateOffset object associated with rule name. - - Examples - -------- - _get_offset('EOM') --> BMonthEnd(1) - """ - if name not in _offset_map: - try: - split = name.split("-") - klass = prefix_mapping[split[0]] - # handles case where there's no suffix (and will TypeError if too - # many '-') - offset = klass._from_name(*split[1:]) - except (ValueError, TypeError, KeyError) as err: - # bad prefix or suffix - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{name}, failed to parse with error message: {repr(err)}") - ) - # cache - _offset_map[name] = offset - - return _offset_map[name] - - -cpdef to_offset(freq, bint is_period=False): - """ - Return DateOffset object from string or datetime.timedelta object. - - Parameters - ---------- - freq : str, datetime.timedelta, BaseOffset or None - The frequency represented. - is_period : bool, default False - Convert string denoting period frequency to corresponding offsets - frequency if is_period=True. - - Returns - ------- - BaseOffset subclass or None - - Raises - ------ - ValueError - If freq is an invalid frequency - - See Also - -------- - BaseOffset : Standard kind of date increment used for a date range. - - Examples - -------- - >>> from pandas.tseries.frequencies import to_offset - >>> to_offset("5min") - <5 * Minutes> - - >>> to_offset("1D1h") - <25 * Hours> - - >>> to_offset("2W") - <2 * Weeks: weekday=6> - - >>> to_offset("2B") - <2 * BusinessDays> - - >>> to_offset(pd.Timedelta(days=1)) - - - >>> to_offset(pd.offsets.Hour()) - - - Passing the parameter ``is_period`` equal to True, you can use a string - denoting period frequency: - - >>> freq = to_offset(freq="ME", is_period=False) - >>> freq.rule_code - 'ME' - - >>> freq = to_offset(freq="M", is_period=True) - >>> freq.rule_code - 'ME' - """ - if freq is None: - return None - - if isinstance(freq, tuple): - raise TypeError( - f"to_offset does not support tuples {freq}, pass as a string instead" - ) - - if isinstance(freq, BaseOffset): - result = freq - - elif PyDelta_Check(freq): - result = delta_to_tick(freq) - - elif isinstance(freq, str): - result = None - stride_sign = None - - try: - split = opattern.split(freq) - if split[-1] != "" and not split[-1].isspace(): - # the last element must be blank - raise ValueError("last element must be blank") - - tups = zip(split[0::4], split[1::4], split[2::4]) - for n, (sep, stride, name) in enumerate(tups): - name = _warn_about_deprecated_aliases(name, is_period) - _validate_to_offset_alias(name, is_period) - if is_period: - if name.upper() in c_PERIOD_TO_OFFSET_FREQSTR: - if name.upper() != name: - raise ValueError( - f"\'{name}\' is no longer supported, " - f"please use \'{name.upper()}\' instead.", - ) - name = c_PERIOD_TO_OFFSET_FREQSTR[name.upper()] - name = _lite_rule_alias.get(name, name) - - if sep != "" and not sep.isspace(): - raise ValueError("separator must be spaces") - if stride_sign is None: - stride_sign = -1 if stride.startswith("-") else 1 - if not stride: - stride = 1 - - if name in {"D", "h", "min", "s", "ms", "us", "ns"}: - # For these prefixes, we have something like "3h" or - # "2.5min", so we can construct a Timedelta with the - # matching unit and get our offset from delta_to_tick - td = Timedelta(1, unit=name) - off = delta_to_tick(td) - offset = off * float(stride) - if n != 0: - # If n==0, then stride_sign is already incorporated - # into the offset - offset *= stride_sign - else: - stride = int(stride) - offset = _get_offset(name) - offset = offset * int(np.fabs(stride) * stride_sign) - - if result is None: - result = offset - else: - result = result + offset - except (ValueError, TypeError) as err: - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{freq}, failed to parse with error message: {repr(err)}") - ) from err - else: - result = None - - if result is None: - raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) - - try: - has_period_dtype_code = hasattr(result, "_period_dtype_code") - except ValueError: - has_period_dtype_code = False - - if is_period and not has_period_dtype_code: - if isinstance(freq, str): - raise ValueError(f"{result.name} is not supported as period frequency") - else: - raise ValueError(f"{freq} is not supported as period frequency") - - return result - - -# ---------------------------------------------------------------------- -# RelativeDelta Arithmetic - -cdef datetime _shift_day(datetime other, int days): - """ - Increment the datetime `other` by the given number of days, retaining - the time-portion of the datetime. For tz-naive datetimes this is - equivalent to adding a timedelta. For tz-aware datetimes it is similar to - dateutil's relativedelta.__add__, but handles pytz tzinfo objects. - - Parameters - ---------- - other : datetime or Timestamp - days : int - - Returns - ------- - shifted: datetime or Timestamp - """ - if other.tzinfo is None: - return other + timedelta(days=days) - - tz = other.tzinfo - naive = other.replace(tzinfo=None) - shifted = naive + timedelta(days=days) - return localize_pydatetime(shifted, tz) - - -cdef int year_add_months(npy_datetimestruct dts, int months) noexcept nogil: - """ - New year number after shifting npy_datetimestruct number of months. - """ - return dts.year + (dts.month + months - 1) // 12 - - -cdef int month_add_months(npy_datetimestruct dts, int months) noexcept nogil: - """ - New month number after shifting npy_datetimestruct - number of months. - """ - cdef: - int new_month = (dts.month + months) % 12 - return 12 if new_month == 0 else new_month - - -@cython.wraparound(False) -@cython.boundscheck(False) -cdef ndarray shift_quarters( - ndarray dtindex, - int quarters, - int q1start_month, - str day_opt, - int modby=3, - NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns, -): - """ - Given an int64 array representing nanosecond timestamps, shift all elements - by the specified number of quarters using DateOffset semantics. - - Parameters - ---------- - dtindex : int64_t[:] timestamps for input dates - quarters : int number of quarters to shift - q1start_month : int month in which Q1 begins by convention - day_opt : {'start', 'end', 'business_start', 'business_end'} - modby : int (3 for quarters, 12 for years) - reso : NPY_DATETIMEUNIT, default NPY_FR_ns - - Returns - ------- - out : ndarray[int64_t] - """ - if day_opt not in ["start", "end", "business_start", "business_end"]: - raise ValueError("day must be None, 'start', 'end', " - "'business_start', or 'business_end'") - - cdef: - Py_ssize_t count = dtindex.size - ndarray out = cnp.PyArray_EMPTY(dtindex.ndim, dtindex.shape, cnp.NPY_INT64, 0) - Py_ssize_t i - int64_t val, res_val - int months_since, n - npy_datetimestruct dts - cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, dtindex) - - with nogil: - for i in range(count): - # Analogous to: val = dtindex[i] - val = (cnp.PyArray_MultiIter_DATA(mi, 1))[0] - - if val == NPY_NAT: - res_val = NPY_NAT - else: - pandas_datetime_to_datetimestruct(val, reso, &dts) - n = quarters - - months_since = (dts.month - q1start_month) % modby - n = _roll_qtrday(&dts, n, months_since, day_opt) - - dts.year = year_add_months(dts, modby * n - months_since) - dts.month = month_add_months(dts, modby * n - months_since) - dts.day = get_day_of_month(&dts, day_opt) - - res_val = npy_datetimestruct_to_datetime(reso, &dts) - - # Analogous to: out[i] = res_val - (cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val - - cnp.PyArray_MultiIter_NEXT(mi) - - return out - - -@cython.wraparound(False) -@cython.boundscheck(False) -def shift_months( - ndarray dtindex, # int64_t, arbitrary ndim - int months, - str day_opt=None, - NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns, -): - """ - Given an int64-based datetime index, shift all elements - specified number of months using DateOffset semantics - - day_opt: {None, 'start', 'end', 'business_start', 'business_end'} - * None: day of month - * 'start' 1st day of month - * 'end' last day of month - """ - cdef: - Py_ssize_t i - npy_datetimestruct dts - int count = dtindex.size - ndarray out = cnp.PyArray_EMPTY(dtindex.ndim, dtindex.shape, cnp.NPY_INT64, 0) - int months_to_roll - int64_t val, res_val - - cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, dtindex) - - if day_opt is not None and day_opt not in { - "start", "end", "business_start", "business_end" - }: - raise ValueError("day must be None, 'start', 'end', " - "'business_start', or 'business_end'") - - if day_opt is None: - # TODO: can we combine this with the non-None case? - with nogil: - for i in range(count): - # Analogous to: val = i8other[i] - val = (cnp.PyArray_MultiIter_DATA(mi, 1))[0] - - if val == NPY_NAT: - res_val = NPY_NAT - else: - pandas_datetime_to_datetimestruct(val, reso, &dts) - dts.year = year_add_months(dts, months) - dts.month = month_add_months(dts, months) - - dts.day = min(dts.day, get_days_in_month(dts.year, dts.month)) - res_val = npy_datetimestruct_to_datetime(reso, &dts) - - # Analogous to: out[i] = res_val - (cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val - - cnp.PyArray_MultiIter_NEXT(mi) - - else: - with nogil: - for i in range(count): - - # Analogous to: val = i8other[i] - val = (cnp.PyArray_MultiIter_DATA(mi, 1))[0] - - if val == NPY_NAT: - res_val = NPY_NAT - else: - pandas_datetime_to_datetimestruct(val, reso, &dts) - months_to_roll = months - - months_to_roll = _roll_qtrday(&dts, months_to_roll, 0, day_opt) - - dts.year = year_add_months(dts, months_to_roll) - dts.month = month_add_months(dts, months_to_roll) - dts.day = get_day_of_month(&dts, day_opt) - - res_val = npy_datetimestruct_to_datetime(reso, &dts) - - # Analogous to: out[i] = res_val - (cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val - - cnp.PyArray_MultiIter_NEXT(mi) - - return out - - -def shift_month(stamp: datetime, months: int, day_opt: object = None) -> datetime: - """ - Given a datetime (or Timestamp) `stamp`, an integer `months` and an - option `day_opt`, return a new datetimelike that many months later, - with day determined by `day_opt` using relativedelta semantics. - - Scalar analogue of shift_months. - - Parameters - ---------- - stamp : datetime or Timestamp - months : int - day_opt : None, 'start', 'end', 'business_start', 'business_end', or int - None: returned datetimelike has the same day as the input, or the - last day of the month if the new month is too short - 'start': returned datetimelike has day=1 - 'end': returned datetimelike has day on the last day of the month - 'business_start': returned datetimelike has day on the first - business day of the month - 'business_end': returned datetimelike has day on the last - business day of the month - int: returned datetimelike has day equal to day_opt - - Returns - ------- - shifted : datetime or Timestamp (same as input `stamp`) - """ - cdef: - int year, month, day - int days_in_month, dy - - dy = (stamp.month + months) // 12 - month = (stamp.month + months) % 12 - - if month == 0: - month = 12 - dy -= 1 - year = stamp.year + dy - - if day_opt is None: - days_in_month = get_days_in_month(year, month) - day = min(stamp.day, days_in_month) - elif day_opt == "start": - day = 1 - elif day_opt == "end": - day = get_days_in_month(year, month) - elif day_opt == "business_start": - # first business day of month - day = get_firstbday(year, month) - elif day_opt == "business_end": - # last business day of month - day = get_lastbday(year, month) - elif is_integer_object(day_opt): - days_in_month = get_days_in_month(year, month) - day = min(day_opt, days_in_month) - else: - raise ValueError(day_opt) - return stamp.replace(year=year, month=month, day=day) - - -cdef int get_day_of_month(npy_datetimestruct* dts, str day_opt) noexcept nogil: - """ - Find the day in `other`'s month that satisfies a DateOffset's is_on_offset - policy, as described by the `day_opt` argument. - - Parameters - ---------- - dts : npy_datetimestruct* - day_opt : {'start', 'end', 'business_start', 'business_end'} - 'start': returns 1 - 'end': returns last day of the month - 'business_start': returns the first business day of the month - 'business_end': returns the last business day of the month - - Returns - ------- - day_of_month : int - - Examples - ------- - >>> other = datetime(2017, 11, 14) - >>> get_day_of_month(other, 'start') - 1 - >>> get_day_of_month(other, 'end') - 30 - - Notes - ----- - Caller is responsible for ensuring one of the four accepted day_opt values - is passed. - """ - - if day_opt == "start": - return 1 - elif day_opt == "end": - return get_days_in_month(dts.year, dts.month) - elif day_opt == "business_start": - # first business day of month - return get_firstbday(dts.year, dts.month) - else: - # i.e. day_opt == "business_end": - # last business day of month - return get_lastbday(dts.year, dts.month) - - -cpdef int roll_convention(int other, int n, int compare) noexcept nogil: - """ - Possibly increment or decrement the number of periods to shift - based on rollforward/rollbackward conventions. - - Parameters - ---------- - other : int, generally the day component of a datetime - n : number of periods to increment, before adjusting for rolling - compare : int, generally the day component of a datetime, in the same - month as the datetime form which `other` was taken. - - Returns - ------- - n : int number of periods to increment - """ - if n > 0 and other < compare: - n -= 1 - elif n <= 0 and other > compare: - # as if rolled forward already - n += 1 - return n - - -def roll_qtrday(other: datetime, n: int, month: int, - day_opt: str, modby: int) -> int: - """ - Possibly increment or decrement the number of periods to shift - based on rollforward/rollbackward conventions. - - Parameters - ---------- - other : datetime or Timestamp - n : number of periods to increment, before adjusting for rolling - month : int reference month giving the first month of the year - day_opt : {'start', 'end', 'business_start', 'business_end'} - The convention to use in finding the day in a given month against - which to compare for rollforward/rollbackward decisions. - modby : int 3 for quarters, 12 for years - - Returns - ------- - n : int number of periods to increment - - See Also - -------- - get_day_of_month : Find the day in a month provided an offset. - """ - cdef: - int months_since - npy_datetimestruct dts - - if day_opt not in ["start", "end", "business_start", "business_end"]: - raise ValueError(day_opt) - - pydate_to_dtstruct(other, &dts) - - if modby == 12: - # We care about the month-of-year, not month-of-quarter, so skip mod - months_since = other.month - month - else: - months_since = other.month % modby - month % modby - - return _roll_qtrday(&dts, n, months_since, day_opt) - - -cdef int _roll_qtrday(npy_datetimestruct* dts, - int n, - int months_since, - str day_opt) except? -1 nogil: - """ - See roll_qtrday.__doc__ - """ - - if n > 0: - if months_since < 0 or (months_since == 0 and - dts.day < get_day_of_month(dts, day_opt)): - # pretend to roll back if on same month but - # before compare_day - n -= 1 - else: - if months_since > 0 or (months_since == 0 and - dts.day > get_day_of_month(dts, day_opt)): - # make sure to roll forward, so negate - n += 1 - return n diff --git a/pandas/tests/frame/methods/test_clip.py b/pandas/tests/frame/methods/test_clip.py index 62a97271d208b..f69e3c3c0ddcb 100644 --- a/pandas/tests/frame/methods/test_clip.py +++ b/pandas/tests/frame/methods/test_clip.py @@ -1,10 +1,7 @@ import numpy as np import pytest -from pandas import ( - DataFrame, - Series, -) +import pandas as pd import pandas._testing as tm @@ -30,7 +27,7 @@ def test_inplace_clip(self, float_frame): def test_dataframe_clip(self): # GH#2747 - df = DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) + df = pd.DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) for lb, ub in [(-1, 1), (1, -1)]: clipped_df = df.clip(lb, ub) @@ -46,12 +43,12 @@ def test_dataframe_clip(self): def test_clip_mixed_numeric(self): # clip on mixed integer or floats # GH#24162, clipping now preserves numeric types per column - df = DataFrame({"A": [1, 2, 3], "B": [1.0, np.nan, 3.0]}) + df = pd.DataFrame({"A": [1, 2, 3], "B": [1.0, np.nan, 3.0]}) result = df.clip(1, 2) - expected = DataFrame({"A": [1, 2, 2], "B": [1.0, np.nan, 2.0]}) + expected = pd.DataFrame({"A": [1, 2, 2], "B": [1.0, np.nan, 2.0]}) tm.assert_frame_equal(result, expected) - df = DataFrame([[1, 2, 3.4], [3, 4, 5.6]], columns=["foo", "bar", "baz"]) + df = pd.DataFrame([[1, 2, 3.4], [3, 4, 5.6]], columns=["foo", "bar", "baz"]) expected = df.dtypes result = df.clip(upper=3).dtypes tm.assert_series_equal(result, expected) @@ -60,8 +57,8 @@ def test_clip_mixed_numeric(self): def test_clip_against_series(self, inplace): # GH#6966 - df = DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) - lb = Series(np.random.default_rng(2).standard_normal(1000)) + df = pd.DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) + lb = pd.Series(np.random.default_rng(2).standard_normal(1000)) ub = lb + 1 original = df.copy() @@ -98,21 +95,21 @@ def test_clip_against_list_like(self, inplace, lower, axis, res): # GH#15390 arr = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) - original = DataFrame( + original = pd.DataFrame( arr, columns=["one", "two", "three"], index=["a", "b", "c"] ) result = original.clip(lower=lower, upper=[5, 6, 7], axis=axis, inplace=inplace) - expected = DataFrame(res, columns=original.columns, index=original.index) + expected = pd.DataFrame(res, columns=original.columns, index=original.index) if inplace: result = original tm.assert_frame_equal(result, expected, check_exact=True) @pytest.mark.parametrize("axis", [0, 1, None]) def test_clip_against_frame(self, axis): - df = DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) - lb = DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) + df = pd.DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) + lb = pd.DataFrame(np.random.default_rng(2).standard_normal((1000, 2))) ub = lb + 1 clipped_df = df.clip(lb, ub, axis=axis) @@ -127,15 +124,15 @@ def test_clip_against_frame(self, axis): def test_clip_against_unordered_columns(self): # GH#20911 - df1 = DataFrame( + df1 = pd.DataFrame( np.random.default_rng(2).standard_normal((1000, 4)), columns=["A", "B", "C", "D"], ) - df2 = DataFrame( + df2 = pd.DataFrame( np.random.default_rng(2).standard_normal((1000, 4)), columns=["D", "A", "B", "C"], ) - df3 = DataFrame(df2.values - 1, columns=["B", "D", "C", "A"]) + df3 = pd.DataFrame(df2.values - 1, columns=["B", "D", "C", "A"]) result_upper = df1.clip(lower=0, upper=df2) expected_upper = df1.clip(lower=0, upper=df2[df1.columns]) result_lower = df1.clip(lower=df3, upper=3) @@ -153,12 +150,12 @@ def test_clip_with_na_args(self, float_frame): tm.assert_frame_equal(float_frame.clip(upper=np.nan, lower=np.nan), float_frame) # GH#19992 and adjusted in GH#40420 - df = DataFrame({"col_0": [1, 2, 3], "col_1": [4, 5, 6], "col_2": [7, 8, 9]}) + df = pd.DataFrame({"col_0": [1, 2, 3], "col_1": [4, 5, 6], "col_2": [7, 8, 9]}) result = df.clip(lower=[4, 5, np.nan], axis=0) - expected = DataFrame( + expected = pd.DataFrame( { - "col_0": Series([4, 5, 3], dtype="float"), + "col_0": pd.Series([4, 5, 3], dtype="float"), "col_1": [4, 5, 6], "col_2": [7, 8, 9], } @@ -166,35 +163,41 @@ def test_clip_with_na_args(self, float_frame): tm.assert_frame_equal(result, expected) result = df.clip(lower=[4, 5, np.nan], axis=1) - expected = DataFrame( + expected = pd.DataFrame( {"col_0": [4, 4, 4], "col_1": [5, 5, 6], "col_2": [7, 8, 9]} ) tm.assert_frame_equal(result, expected) # GH#40420 data = {"col_0": [9, -3, 0, -1, 5], "col_1": [-2, -7, 6, 8, -5]} - df = DataFrame(data) - t = Series([2, -4, np.nan, 6, 3]) + df = pd.DataFrame(data) + t = pd.Series([2, -4, np.nan, 6, 3]) result = df.clip(lower=t, axis=0) - expected = DataFrame( + expected = pd.DataFrame( {"col_0": [9, -3, 0, 6, 5], "col_1": [2, -4, 6, 8, 3]}, dtype="float" ) tm.assert_frame_equal(result, expected) def test_clip_int_data_with_float_bound(self): # GH51472 - df = DataFrame({"a": [1, 2, 3]}) + df = pd.DataFrame({"a": [1, 2, 3]}) result = df.clip(lower=1.5) - expected = DataFrame({"a": [1.5, 2.0, 3.0]}) + expected = pd.DataFrame({"a": [1.5, 2.0, 3.0]}) tm.assert_frame_equal(result, expected) def test_clip_with_list_bound(self): # GH#54817 - df = DataFrame([1, 5]) - expected = DataFrame([3, 5]) + df = pd.DataFrame([1, 5]) + expected = pd.DataFrame([3, 5]) result = df.clip([3]) tm.assert_frame_equal(result, expected) - expected = DataFrame([1, 3]) + expected = pd.DataFrame([1, 3]) result = df.clip(upper=[3]) tm.assert_frame_equal(result, expected) + + def test_clip_lower_greater_than_upper(self): + df = pd.DataFrame({"A": [1, 2, 3]}) + result = df.clip(lower=5, upper=3) + expected = pd.DataFrame({"A": [3, 3, 3]}) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/test_clip.py b/pandas/tests/frame/test_clip.py new file mode 100644 index 0000000000000..734162a42d7e8 --- /dev/null +++ b/pandas/tests/frame/test_clip.py @@ -0,0 +1,7 @@ +import pandas as pd + +def test_clip_lower_greater_than_upper(): + df = pd.DataFrame({'A': [1, 5, 10]}) + result = df.clip(lower=10, upper=5) + expected = pd.DataFrame({'A': [5, 5, 10]}) + pd.testing.assert_frame_equal(result, expected)