From a0d2d95658ed5f14b7f8527c3b1d85f74ff13d5f Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 29 Jan 2020 21:27:55 -0600 Subject: [PATCH 01/18] Add test --- pandas/tests/arrays/test_integer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py index 63e3c946df912..2237db25d7a28 100644 --- a/pandas/tests/arrays/test_integer.py +++ b/pandas/tests/arrays/test_integer.py @@ -1074,6 +1074,16 @@ def test_cut(bins, right, include_lowest): tm.assert_categorical_equal(result, expected) +@pytest.mark.parametrize("q", [2, 10, 50]) +def test_qcut(q): + arr = pd.array(np.arange(100), dtype="Int64") + + result = pd.qcut(arr, q) + expected = pd.qcut(arr.astype(int), q) + + tm.assert_categorical_equal(result, expected) + + # TODO(jreback) - these need testing / are broken # shift From fdfb2e964d737605db343207e1e339e31015184c Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 29 Jan 2020 21:28:11 -0600 Subject: [PATCH 02/18] Cast BooleanArray to bool --- pandas/core/reshape/tile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 00a7645d0c7a5..678f523ae65ac 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -394,7 +394,10 @@ def _bins_to_cuts( ids = ensure_int64(bins.searchsorted(x, side=side)) if include_lowest: - ids[x == bins[0]] = 1 + min_mask = x == bins[0] + if is_extension_array_dtype(min_mask): + min_mask = min_mask.fillna(False).astype(bool) + ids[min_mask] = 1 na_mask = isna(x) | (ids == len(bins)) | (ids == 0) has_nas = na_mask.any() From a35c7a6388fbce95fd2e76251cbf043b2da490eb Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 29 Jan 2020 21:31:50 -0600 Subject: [PATCH 03/18] Update 1.1 whatsnew --- doc/source/whatsnew/v1.1.0.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 920919755dc23..9c34f366a140a 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -200,9 +200,7 @@ Sparse ExtensionArray ^^^^^^^^^^^^^^ -- -- - +- Bug where :meth:`qcut` would raise when passed a nullable integer. (:issue:``) Other ^^^^^ From e2d1a460bceba6872b7eab10cd7f8860858d2835 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 29 Jan 2020 22:07:22 -0600 Subject: [PATCH 04/18] Update test --- pandas/tests/arrays/test_integer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py index 2237db25d7a28..e023dccbc0e13 100644 --- a/pandas/tests/arrays/test_integer.py +++ b/pandas/tests/arrays/test_integer.py @@ -1074,12 +1074,13 @@ def test_cut(bins, right, include_lowest): tm.assert_categorical_equal(result, expected) -@pytest.mark.parametrize("q", [2, 10, 50]) -def test_qcut(q): - arr = pd.array(np.arange(100), dtype="Int64") +@pytest.mark.parametrize("q", [2, 5, 10]) +def test_qcut_nullable_integer(q, any_nullable_int_dtype): + arr = pd.array(np.random.randint(1, 100, 100), dtype=any_nullable_int_dtype) + arr[::2] = pd.NA result = pd.qcut(arr, q) - expected = pd.qcut(arr.astype(int), q) + expected = pd.qcut(arr.astype(float), q) tm.assert_categorical_equal(result, expected) From bee26b1128dce3924914c5c09f625f28092ecdc0 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 29 Jan 2020 22:07:44 -0600 Subject: [PATCH 05/18] Use place holder for NA values --- pandas/core/reshape/tile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 678f523ae65ac..6e25895a0e44b 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -391,7 +391,7 @@ def _bins_to_cuts( bins = unique_bins side = "left" if right else "right" - ids = ensure_int64(bins.searchsorted(x, side=side)) + ids = ensure_int64(bins.searchsorted(np.where(~isna(x), x, -1), side=side)) if include_lowest: min_mask = x == bins[0] From 516dbe3ab33b6a16c831046c9c1c62a9c2e0486f Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 29 Jan 2020 23:03:45 -0600 Subject: [PATCH 06/18] Issue number --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 9c34f366a140a..8c1ec988afb87 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -200,7 +200,7 @@ Sparse ExtensionArray ^^^^^^^^^^^^^^ -- Bug where :meth:`qcut` would raise when passed a nullable integer. (:issue:``) +- Bug where :meth:`qcut` would raise when passed a nullable integer. (:issue:`31389`) Other ^^^^^ From 3f09385947d25e427a58a2449b1b418660f3fa85 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 31 Jan 2020 08:34:49 -0600 Subject: [PATCH 07/18] Cast to numpy --- pandas/core/reshape/tile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 6e25895a0e44b..2c5b794a5c357 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -340,6 +340,9 @@ def qcut( x = _preprocess_for_cut(x) x, dtype = _coerce_to_type(x) + if is_extension_array_dtype(x.dtype) and is_integer_dtype(x.dtype): + x = x.to_numpy(dtype=np.float64, na_value=np.nan) + if is_integer(q): quantiles = np.linspace(0, 1, q + 1) else: @@ -391,7 +394,7 @@ def _bins_to_cuts( bins = unique_bins side = "left" if right else "right" - ids = ensure_int64(bins.searchsorted(np.where(~isna(x), x, -1), side=side)) + ids = ensure_int64(bins.searchsorted(x, side=side)) if include_lowest: min_mask = x == bins[0] From 55793813dfd9095c6a59be44d875c07156a5be73 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 31 Jan 2020 08:40:41 -0600 Subject: [PATCH 08/18] Move note --- doc/source/whatsnew/v1.0.1.rst | 1 + doc/source/whatsnew/v1.1.0.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.0.1.rst b/doc/source/whatsnew/v1.0.1.rst index ff8433c7cafd9..a23a18e8a67d8 100644 --- a/doc/source/whatsnew/v1.0.1.rst +++ b/doc/source/whatsnew/v1.0.1.rst @@ -118,6 +118,7 @@ ExtensionArray ^^^^^^^^^^^^^^ - Bug in dtype being lost in ``__invert__`` (``~`` operator) for extension-array backed ``Series`` and ``DataFrame`` (:issue:`23087`) +- Bug where :meth:`qcut` would raise when passed a nullable integer. (:issue:`31389`) - diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index cd6a9f29f7cc5..dc023f011c2c4 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -202,7 +202,7 @@ Sparse ExtensionArray ^^^^^^^^^^^^^^ -- Bug where :meth:`qcut` would raise when passed a nullable integer. (:issue:`31389`) +- Other ^^^^^ From 93ef631ff35be13eb010d1150c0f2c5303683d01 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 31 Jan 2020 08:42:23 -0600 Subject: [PATCH 09/18] Fixup --- doc/source/whatsnew/v1.1.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index dc023f011c2c4..e07a8fa0469f4 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -203,6 +203,8 @@ ExtensionArray ^^^^^^^^^^^^^^ - +- + Other ^^^^^ From f0990b1f1f07abb972dbbbee4b03328b7a2e4e51 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 31 Jan 2020 08:44:04 -0600 Subject: [PATCH 10/18] Don't check for BooleanArray --- pandas/core/reshape/tile.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 2c5b794a5c357..3c22e55f7c3d8 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -397,10 +397,7 @@ def _bins_to_cuts( ids = ensure_int64(bins.searchsorted(x, side=side)) if include_lowest: - min_mask = x == bins[0] - if is_extension_array_dtype(min_mask): - min_mask = min_mask.fillna(False).astype(bool) - ids[min_mask] = 1 + ids[x == bins[0]] = 1 na_mask = isna(x) | (ids == len(bins)) | (ids == 0) has_nas = na_mask.any() From 99e60805e5a87c47927a1ed9e280c686b3a6ec87 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 31 Jan 2020 10:34:35 -0600 Subject: [PATCH 11/18] Make test data deterministic Don't want this to raise with "unlucky" test data that doesn't have high enough cardinality --- pandas/tests/arrays/test_integer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py index e023dccbc0e13..804f71f171586 100644 --- a/pandas/tests/arrays/test_integer.py +++ b/pandas/tests/arrays/test_integer.py @@ -1076,7 +1076,7 @@ def test_cut(bins, right, include_lowest): @pytest.mark.parametrize("q", [2, 5, 10]) def test_qcut_nullable_integer(q, any_nullable_int_dtype): - arr = pd.array(np.random.randint(1, 100, 100), dtype=any_nullable_int_dtype) + arr = pd.array(np.arange(100), dtype=any_nullable_int_dtype) arr[::2] = pd.NA result = pd.qcut(arr, q) From 5694ab6494307d08b64e0f29282ee7bf04085944 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 1 Feb 2020 09:24:23 -0600 Subject: [PATCH 12/18] Update test scripts --- pandas/tests/arrays/test_integer.py | 20 +++++++++++++------- pandas/tests/reshape/test_qcut.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py index 804f71f171586..cc81ae4504dd8 100644 --- a/pandas/tests/arrays/test_integer.py +++ b/pandas/tests/arrays/test_integer.py @@ -1074,15 +1074,21 @@ def test_cut(bins, right, include_lowest): tm.assert_categorical_equal(result, expected) -@pytest.mark.parametrize("q", [2, 5, 10]) -def test_qcut_nullable_integer(q, any_nullable_int_dtype): - arr = pd.array(np.arange(100), dtype=any_nullable_int_dtype) - arr[::2] = pd.NA +def test_array_setitem_nullable_boolean_mask(): + # GH 31446 + ser = pd.Series([1, 2], dtype="Int64") + result = ser.where(ser > 1) + expected = pd.Series([pd.NA, 2], dtype="Int64") + tm.assert_series_equal(result, expected) - result = pd.qcut(arr, q) - expected = pd.qcut(arr.astype(float), q) - tm.assert_categorical_equal(result, expected) +def test_array_setitem(): + # GH 31446 + arr = pd.Series([1, 2], dtype="Int64").array + arr[arr > 1] = 1 + + expected = pd.array([1, 1], dtype="Int64") + tm.assert_extension_array_equal(arr, expected) # TODO(jreback) - these need testing / are broken diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py index 95406a5ebf4f7..7e3c36ddb56d7 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -4,6 +4,7 @@ import pytest from pandas import ( + NA, Categorical, DatetimeIndex, Interval, @@ -12,6 +13,7 @@ Series, TimedeltaIndex, Timestamp, + array, cut, date_range, isna, @@ -286,3 +288,14 @@ def test_qcut_bool_coercion_to_int(bins, box, compare): expected = qcut(data_expected, bins, duplicates="drop") result = qcut(data_result, bins, duplicates="drop") compare(result, expected) + + +@pytest.mark.parametrize("q", [2, 5, 10]) +def test_qcut_nullable_integer(q, any_nullable_int_dtype): + arr = array(np.arange(100), dtype=any_nullable_int_dtype) + arr[::2] = NA + + result = qcut(arr, q) + expected = qcut(arr.astype(float), q) + + tm.assert_categorical_equal(result, expected) From 1bf4cee796571990042904c3730e71bbac561f02 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 1 Feb 2020 09:35:39 -0600 Subject: [PATCH 13/18] Move test_cut for nullable int --- pandas/tests/arrays/test_integer.py | 13 ------------- pandas/tests/reshape/test_cut.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py index cc81ae4504dd8..7a0c9300a43a2 100644 --- a/pandas/tests/arrays/test_integer.py +++ b/pandas/tests/arrays/test_integer.py @@ -1061,19 +1061,6 @@ def test_value_counts_na(): tm.assert_series_equal(result, expected) -@pytest.mark.parametrize("bins", [3, [0, 5, 15]]) -@pytest.mark.parametrize("right", [True, False]) -@pytest.mark.parametrize("include_lowest", [True, False]) -def test_cut(bins, right, include_lowest): - a = np.random.randint(0, 10, size=50).astype(object) - a[::2] = np.nan - result = pd.cut( - pd.array(a, dtype="Int64"), bins, right=right, include_lowest=include_lowest - ) - expected = pd.cut(a, bins, right=right, include_lowest=include_lowest) - tm.assert_categorical_equal(result, expected) - - def test_array_setitem_nullable_boolean_mask(): # GH 31446 ser = pd.Series([1, 2], dtype="Int64") diff --git a/pandas/tests/reshape/test_cut.py b/pandas/tests/reshape/test_cut.py index 13b6f05ed304a..af01cf1aede13 100644 --- a/pandas/tests/reshape/test_cut.py +++ b/pandas/tests/reshape/test_cut.py @@ -612,3 +612,16 @@ def test_cut_incorrect_labels(labels): msg = "Bin labels must either be False, None or passed in as a list-like argument" with pytest.raises(ValueError, match=msg): cut(values, 4, labels=labels) + + +@pytest.mark.parametrize("bins", [3, [0, 5, 15]]) +@pytest.mark.parametrize("right", [True, False]) +@pytest.mark.parametrize("include_lowest", [True, False]) +def test_cut_nullable_integer(bins, right, include_lowest): + a = np.random.randint(0, 10, size=50).astype(object) + a[::2] = np.nan + result = cut( + pd.array(a, dtype="Int64"), bins, right=right, include_lowest=include_lowest + ) + expected = cut(a, bins, right=right, include_lowest=include_lowest) + tm.assert_categorical_equal(result, expected) From 4eb93131543ec0548ec818fa78e440df4bcccdb0 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 1 Feb 2020 11:13:31 -0600 Subject: [PATCH 14/18] Just import pandas --- pandas/tests/groupby/aggregate/test_timegrouper.py | 0 pandas/tests/reshape/test_qcut.py | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 pandas/tests/groupby/aggregate/test_timegrouper.py diff --git a/pandas/tests/groupby/aggregate/test_timegrouper.py b/pandas/tests/groupby/aggregate/test_timegrouper.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py index 7e3c36ddb56d7..c436ab5d90578 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -3,8 +3,8 @@ import numpy as np import pytest +import pandas as pd from pandas import ( - NA, Categorical, DatetimeIndex, Interval, @@ -13,7 +13,6 @@ Series, TimedeltaIndex, Timestamp, - array, cut, date_range, isna, @@ -292,8 +291,8 @@ def test_qcut_bool_coercion_to_int(bins, box, compare): @pytest.mark.parametrize("q", [2, 5, 10]) def test_qcut_nullable_integer(q, any_nullable_int_dtype): - arr = array(np.arange(100), dtype=any_nullable_int_dtype) - arr[::2] = NA + arr = pd.array(np.arange(100), dtype=any_nullable_int_dtype) + arr[::2] = pd.NA result = qcut(arr, q) expected = qcut(arr.astype(float), q) From 6767ea42346d2aa5c5d678b4b7ef79fbd1599f72 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sun, 2 Feb 2020 13:07:33 -0600 Subject: [PATCH 15/18] Cast to float instead of object in test --- pandas/tests/reshape/test_cut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/reshape/test_cut.py b/pandas/tests/reshape/test_cut.py index af01cf1aede13..830e786fd1c6d 100644 --- a/pandas/tests/reshape/test_cut.py +++ b/pandas/tests/reshape/test_cut.py @@ -618,7 +618,7 @@ def test_cut_incorrect_labels(labels): @pytest.mark.parametrize("right", [True, False]) @pytest.mark.parametrize("include_lowest", [True, False]) def test_cut_nullable_integer(bins, right, include_lowest): - a = np.random.randint(0, 10, size=50).astype(object) + a = np.random.randint(0, 10, size=50).astype(float) a[::2] = np.nan result = cut( pd.array(a, dtype="Int64"), bins, right=right, include_lowest=include_lowest From fc0a0baba0948a1cfc9c3a980718c2fa983bbc83 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sun, 2 Feb 2020 13:08:21 -0600 Subject: [PATCH 16/18] Move conversion into _coerce_to_type --- pandas/core/reshape/tile.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 3c22e55f7c3d8..9a3693b1b3e7e 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -203,16 +203,13 @@ def cut( # NOTE: this binning code is changed a bit from histogram for var(x) == 0 # for handling the cut for datetime and timedelta objects + # Also to support cut(IntegerArray) we convert to float dtype + # Will properly support in the future. + # https://github.com/pandas-dev/pandas/pull/31290 original = x x = _preprocess_for_cut(x) x, dtype = _coerce_to_type(x) - # To support cut(IntegerArray), we convert to object dtype with NaN - # Will properly support in the future. - # https://github.com/pandas-dev/pandas/pull/31290 - if is_extension_array_dtype(x.dtype) and is_integer_dtype(x.dtype): - x = x.to_numpy(dtype=object, na_value=np.nan) - if not np.iterable(bins): if is_scalar(bins) and bins < 1: raise ValueError("`bins` should be a positive integer.") @@ -340,9 +337,6 @@ def qcut( x = _preprocess_for_cut(x) x, dtype = _coerce_to_type(x) - if is_extension_array_dtype(x.dtype) and is_integer_dtype(x.dtype): - x = x.to_numpy(dtype=np.float64, na_value=np.nan) - if is_integer(q): quantiles = np.linspace(0, 1, q + 1) else: @@ -437,7 +431,7 @@ def _bins_to_cuts( def _coerce_to_type(x): """ - if the passed data is of datetime/timedelta or bool type, + if the passed data is of datetime/timedelta, bool or nullable int type, this method converts it to numeric so that cut or qcut method can handle it """ @@ -454,6 +448,8 @@ def _coerce_to_type(x): elif is_bool_dtype(x): # GH 20303 x = x.astype(np.int64) + elif is_extension_array_dtype(x) and is_integer_dtype(x): + x = x.to_numpy(dtype=np.float64, na_value=np.nan) if dtype is not None: # GH 19768: force NaT to NaN during integer conversion From b67abadb2e22950efcb2198a54a6af37375bd76d Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sun, 2 Feb 2020 13:47:02 -0600 Subject: [PATCH 17/18] Move comments --- pandas/core/reshape/tile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 9a3693b1b3e7e..a18b45a077be0 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -202,10 +202,6 @@ def cut( """ # NOTE: this binning code is changed a bit from histogram for var(x) == 0 - # for handling the cut for datetime and timedelta objects - # Also to support cut(IntegerArray) we convert to float dtype - # Will properly support in the future. - # https://github.com/pandas-dev/pandas/pull/31290 original = x x = _preprocess_for_cut(x) x, dtype = _coerce_to_type(x) @@ -448,6 +444,10 @@ def _coerce_to_type(x): elif is_bool_dtype(x): # GH 20303 x = x.astype(np.int64) + # To support cut and qcut for IntegerArray we convert to float dtype. + # Will properly support in the future. + # https://github.com/pandas-dev/pandas/pull/31290 + # https://github.com/pandas-dev/pandas/issues/31389 elif is_extension_array_dtype(x) and is_integer_dtype(x): x = x.to_numpy(dtype=np.float64, na_value=np.nan) From e5c2598220ed917c1a639c4e51644359192bd7ef Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sun, 2 Feb 2020 15:29:27 -0600 Subject: [PATCH 18/18] Remove empty file --- pandas/tests/groupby/aggregate/test_timegrouper.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pandas/tests/groupby/aggregate/test_timegrouper.py diff --git a/pandas/tests/groupby/aggregate/test_timegrouper.py b/pandas/tests/groupby/aggregate/test_timegrouper.py deleted file mode 100644 index e69de29bb2d1d..0000000000000