From 90481c2fe9ec08b8f9902131a1c572cc870b737f Mon Sep 17 00:00:00 2001 From: Sara Huston Date: Sat, 8 Nov 2025 23:38:41 -0500 Subject: [PATCH 1/5] Add right argument to pandas.qcut to support left-inclusive binning #62938 --- pandas/core/reshape/tile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index b13da83084e5c..66e798fc868c6 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -294,6 +294,7 @@ def qcut( x, q, labels=None, + right: bool = True, retbins: bool = False, precision: int = 3, duplicates: str = "raise", @@ -378,6 +379,7 @@ def qcut( x_idx, Index(bins), labels=labels, + right=right, precision=precision, include_lowest=True, duplicates=duplicates, From d308111985a4740d3213132790857d21c931e59e Mon Sep 17 00:00:00 2001 From: Sara Huston Date: Sun, 9 Nov 2025 00:13:26 -0500 Subject: [PATCH 2/5] Document new parameter Right for qcut function --- pandas/core/reshape/tile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 66e798fc868c6..152fceae9ed6d 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -324,6 +324,11 @@ def qcut( The precision at which to store and display the bins labels. duplicates : {default 'raise', 'drop'}, optional If bin edges are not unique, raise ValueError or drop non-uniques. + right : bool, default True + Indicates whether `bins` includes the rightmost edge or not. If + ``right == True`` (the default), then the `bins` ``[1, 2, 3, 4]`` + indicate (1,2], (2,3], (3,4]. This argument is ignored when + `bins` is an IntervalIndex. Returns ------- From ebc1b6851032c1f7ce0c2d0c4799bcd8b4f01bee Mon Sep 17 00:00:00 2001 From: Sara Huston Date: Sun, 9 Nov 2025 15:27:58 -0500 Subject: [PATCH 3/5] Updated type annotations, whatsnew doc, and specific tests added and passed --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/reshape/tile.py | 10 ++++----- pandas/tests/reshape/test_qcut.py | 34 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 65982ecdb810c..8f96681abf2f4 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -232,7 +232,7 @@ Other enhancements - Support reading Stata 102-format (Stata 1) dta files (:issue:`58978`) - Support reading Stata 110-format (Stata 7) dta files (:issue:`47176`) - Switched wheel upload to **PyPI Trusted Publishing** (OIDC) for release-tag pushes in ``wheels.yml``. (:issue:`61718`) -- +- :func:`qcut` now accepts ``right`` as optional arguments, as in :meth:`cut` (:issue:`62938`) .. --------------------------------------------------------------------------- .. _whatsnew_300.notable_bug_fixes: diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 152fceae9ed6d..fc3870f2e235f 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -317,6 +317,11 @@ def qcut( Used as labels for the resulting bins. Must be of the same length as the resulting bins. If False, return only integer indicators of the bins. If True, raises an error. + right : bool, default True + Indicates whether `bins` includes the rightmost edge or not. If + ``right == True`` (the default), then the `bins` ``[1, 2, 3, 4]`` + indicate (1,2], (2,3], (3,4]. This argument is ignored when + `bins` is an IntervalIndex. retbins : bool, optional Whether to return the (bins, labels) or not. Can be useful if bins is given as a scalar. @@ -324,11 +329,6 @@ def qcut( The precision at which to store and display the bins labels. duplicates : {default 'raise', 'drop'}, optional If bin edges are not unique, raise ValueError or drop non-uniques. - right : bool, default True - Indicates whether `bins` includes the rightmost edge or not. If - ``right == True`` (the default), then the `bins` ``[1, 2, 3, 4]`` - indicate (1,2], (2,3], (3,4]. This argument is ignored when - `bins` is an IntervalIndex. Returns ------- diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py index b6d45aeab8a7b..368d71f784e28 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -44,6 +44,40 @@ def test_qcut(): tm.assert_categorical_equal(labels, ex_levels) +def test_qcut_right(): + arr = np.random.default_rng(2).standard_normal(1000) + + # We store the bins as Index that have been + # rounded to comparisons are a bit tricky. + labels, _ = qcut(arr, 4, retbins=True, right=True) + ex_bins = np.quantile(arr, [0, 0.25, 0.5, 0.75, 1.0]) + + result = labels.categories.left.values + assert np.allclose(result, ex_bins[:-1], atol=1e-2) + + result = labels.categories.right.values + assert np.allclose(result, ex_bins[1:], atol=1e-2) + + ex_levels = cut(arr, ex_bins, include_lowest=True, right=True) + tm.assert_categorical_equal(labels, ex_levels) + + +def test_qcut_no_right(): + arr = np.random.default_rng(2).standard_normal(1000) + + labels, _ = qcut(arr, 4, retbins=True, right=False) + ex_bins = np.quantile(arr, [0, 0.25, 0.5, 0.75, 1.0]) + + lefts = labels.categories.left.values + assert np.allclose(lefts, ex_bins[:-1], atol=1e-2) + + rights = labels.categories.right.values + assert np.allclose(rights, ex_bins[1:], atol=1e-2) + + ex_levels = cut(arr, ex_bins, include_lowest=True, right=False) + tm.assert_categorical_equal(labels, ex_levels) + + def test_qcut_bounds(): arr = np.random.default_rng(2).standard_normal(1000) From 4fd6088530f8b7b1f20962c6ffa349d3e09c62d3 Mon Sep 17 00:00:00 2001 From: Sara Huston Date: Sun, 9 Nov 2025 19:36:53 -0500 Subject: [PATCH 4/5] fixed linting issues and pre-commit.ci autofix --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/tests/reshape/test_qcut.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 8f96681abf2f4..a1a3833c35cee 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -232,7 +232,7 @@ Other enhancements - Support reading Stata 102-format (Stata 1) dta files (:issue:`58978`) - Support reading Stata 110-format (Stata 7) dta files (:issue:`47176`) - Switched wheel upload to **PyPI Trusted Publishing** (OIDC) for release-tag pushes in ``wheels.yml``. (:issue:`61718`) -- :func:`qcut` now accepts ``right`` as optional arguments, as in :meth:`cut` (:issue:`62938`) +- :func:`qcut` now accepts the ``right`` parameter, consistent with :func:`cut` (:issue:`63053`) .. --------------------------------------------------------------------------- .. _whatsnew_300.notable_bug_fixes: diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py index 368d71f784e28..9e0a61146f68c 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -47,8 +47,6 @@ def test_qcut(): def test_qcut_right(): arr = np.random.default_rng(2).standard_normal(1000) - # We store the bins as Index that have been - # rounded to comparisons are a bit tricky. labels, _ = qcut(arr, 4, retbins=True, right=True) ex_bins = np.quantile(arr, [0, 0.25, 0.5, 0.75, 1.0]) @@ -60,8 +58,8 @@ def test_qcut_right(): ex_levels = cut(arr, ex_bins, include_lowest=True, right=True) tm.assert_categorical_equal(labels, ex_levels) - - + + def test_qcut_no_right(): arr = np.random.default_rng(2).standard_normal(1000) @@ -76,7 +74,7 @@ def test_qcut_no_right(): ex_levels = cut(arr, ex_bins, include_lowest=True, right=False) tm.assert_categorical_equal(labels, ex_levels) - + def test_qcut_bounds(): arr = np.random.default_rng(2).standard_normal(1000) From fa6d86b93da2f92238928340e1822118c798826d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 00:42:48 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/tests/reshape/test_qcut.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index a1a3833c35cee..fa427d0d74bfc 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -202,6 +202,7 @@ Other enhancements - :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`) - :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`) - :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`) +- :func:`qcut` now accepts the ``right`` parameter, consistent with :func:`cut` (:issue:`63053`) - :func:`read_parquet` accepts ``to_pandas_kwargs`` which are forwarded to :meth:`pyarrow.Table.to_pandas` which enables passing additional keywords to customize the conversion to pandas, such as ``maps_as_pydicts`` to read the Parquet map data type as python dictionaries (:issue:`56842`) - :func:`to_numeric` on big integers converts to ``object`` datatype with python integers when not coercing. (:issue:`51295`) - :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`) @@ -232,7 +233,6 @@ Other enhancements - Support reading Stata 102-format (Stata 1) dta files (:issue:`58978`) - Support reading Stata 110-format (Stata 7) dta files (:issue:`47176`) - Switched wheel upload to **PyPI Trusted Publishing** (OIDC) for release-tag pushes in ``wheels.yml``. (:issue:`61718`) -- :func:`qcut` now accepts the ``right`` parameter, consistent with :func:`cut` (:issue:`63053`) .. --------------------------------------------------------------------------- .. _whatsnew_300.notable_bug_fixes: diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py index 9e0a61146f68c..223c5612bfc55 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -68,7 +68,7 @@ def test_qcut_no_right(): lefts = labels.categories.left.values assert np.allclose(lefts, ex_bins[:-1], atol=1e-2) - + rights = labels.categories.right.values assert np.allclose(rights, ex_bins[1:], atol=1e-2)