From d48235f5da21a547440f6150990aa42986e88e80 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Sat, 2 Nov 2024 00:54:44 +0800 Subject: [PATCH 001/266] DOC: fix docstring of DataFrame.to_latex, double curly braces to single (#60165) --- pandas/core/generic.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 1759e1ef91d85..756c431022063 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3324,9 +3324,9 @@ def to_latex( r""" Render object to a LaTeX tabular, longtable, or nested table. - Requires ``\usepackage{{booktabs}}``. The output can be copy/pasted + Requires ``\usepackage{booktabs}``. The output can be copy/pasted into a main LaTeX document or read from an external file - with ``\input{{table.tex}}``. + with ``\input{table.tex}``. .. versionchanged:: 2.0.0 Refactored to use the Styler implementation via jinja2 templating. @@ -3344,13 +3344,13 @@ def to_latex( Write row names (index). na_rep : str, default 'NaN' Missing data representation. - formatters : list of functions or dict of {{str: function}}, optional + formatters : list of functions or dict of {str: function}, optional Formatter functions to apply to columns' elements by position or name. The result of each function must be a unicode string. List must be of length equal to the number of columns. float_format : one-parameter function or str, optional, default None Formatter for floating point numbers. For example - ``float_format="%.2f"`` and ``float_format="{{:0.2f}}".format`` will + ``float_format="%.2f"`` and ``float_format="{:0.2f}".format`` will both result in 0.1234 being formatted as 0.12. sparsify : bool, optional Set to False for a DataFrame with a hierarchical index to print @@ -3367,7 +3367,7 @@ def to_latex( columns of numbers, which default to 'r'. longtable : bool, optional Use a longtable environment instead of tabular. Requires - adding a \usepackage{{longtable}} to your LaTeX preamble. + adding a \usepackage{longtable} to your LaTeX preamble. By default, the value will be read from the pandas config module, and set to `True` if the option ``styler.latex.environment`` is `"longtable"`. @@ -3405,7 +3405,7 @@ def to_latex( default value to "r". multirow : bool, default True Use \multirow to enhance MultiIndex rows. Requires adding a - \usepackage{{multirow}} to your LaTeX preamble. Will print + \usepackage{multirow} to your LaTeX preamble. Will print centered labels (instead of top-aligned) across the contained rows, separating groups via clines. The default will be read from the pandas config module, and is set as the option @@ -3416,15 +3416,15 @@ def to_latex( default value to `True`. caption : str or tuple, optional Tuple (full_caption, short_caption), - which results in ``\caption[short_caption]{{full_caption}}``; + which results in ``\caption[short_caption]{full_caption}``; if a single string is passed, no short caption will be set. label : str, optional - The LaTeX label to be placed inside ``\label{{}}`` in the output. - This is used with ``\ref{{}}`` in the main ``.tex`` file. + The LaTeX label to be placed inside ``\label{}`` in the output. + This is used with ``\ref{}`` in the main ``.tex`` file. position : str, optional The LaTeX positional argument for tables, to be placed after - ``\begin{{}}`` in the output. + ``\begin{}`` in the output. Returns ------- From a3f14bfa373c6fb4e3470a1f3bd8fed1657e09e1 Mon Sep 17 00:00:00 2001 From: Abhishek Chaudhari <91185083+AbhishekChaudharii@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:26:00 +0530 Subject: [PATCH 002/266] BUG: Fixes pd.merge issue with columns of dtype numpy.uintc on windows (#60145) * bug fix for numpy.uintc in merge operations on windows Added pytest test case to verify correct behavior with numpy.uintc dtype * Formatting changes after running pre-commit * Added tests for numpy.intc * added whatsnew note * pre-commit automatic changes and also made changes to test_merge.py file to make pandas namespace consistent * removed comment * added the deleted whatsnew note back * better whatsnew note Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/reshape/merge.py | 12 +++++++- pandas/tests/reshape/merge/test_merge.py | 35 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index c61b8f3fb3701..2e64c66812306 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -739,6 +739,7 @@ Reshaping - Bug in :meth:`DataFrame.join` when a :class:`DataFrame` with a :class:`MultiIndex` would raise an ``AssertionError`` when :attr:`MultiIndex.names` contained ``None``. (:issue:`58721`) - Bug in :meth:`DataFrame.merge` where merging on a column containing only ``NaN`` values resulted in an out-of-bounds array access (:issue:`59421`) - Bug in :meth:`DataFrame.unstack` producing incorrect results when ``sort=False`` (:issue:`54987`, :issue:`55516`) +- Bug in :meth:`DataFrame.merge` when merging two :class:`DataFrame` on ``intc`` or ``uintc`` types on Windows (:issue:`60091`, :issue:`58713`) - Bug in :meth:`DataFrame.pivot_table` incorrectly subaggregating results when called without an ``index`` argument (:issue:`58722`) - Bug in :meth:`DataFrame.unstack` producing incorrect results when manipulating empty :class:`DataFrame` with an :class:`ExtentionDtype` (:issue:`59123`) diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py index 07e8fa4841c04..0ca8661ad3b5c 100644 --- a/pandas/core/reshape/merge.py +++ b/pandas/core/reshape/merge.py @@ -123,7 +123,17 @@ # See https://github.com/pandas-dev/pandas/issues/52451 if np.intc is not np.int32: - _factorizers[np.intc] = libhashtable.Int64Factorizer + if np.dtype(np.intc).itemsize == 4: + _factorizers[np.intc] = libhashtable.Int32Factorizer + else: + _factorizers[np.intc] = libhashtable.Int64Factorizer + +if np.uintc is not np.uint32: + if np.dtype(np.uintc).itemsize == 4: + _factorizers[np.uintc] = libhashtable.UInt32Factorizer + else: + _factorizers[np.uintc] = libhashtable.UInt64Factorizer + _known = (np.ndarray, ExtensionArray, Index, ABCSeries) diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py index d4766242b8460..f0abc1afc6ab0 100644 --- a/pandas/tests/reshape/merge/test_merge.py +++ b/pandas/tests/reshape/merge/test_merge.py @@ -1843,6 +1843,41 @@ def test_merge_empty(self, left_empty, how, exp): tm.assert_frame_equal(result, expected) + def test_merge_with_uintc_columns(self): + df1 = DataFrame({"a": ["foo", "bar"], "b": np.array([1, 2], dtype=np.uintc)}) + df2 = DataFrame({"a": ["foo", "baz"], "b": np.array([3, 4], dtype=np.uintc)}) + result = df1.merge(df2, how="outer") + expected = DataFrame( + { + "a": ["bar", "baz", "foo", "foo"], + "b": np.array([2, 4, 1, 3], dtype=np.uintc), + } + ) + tm.assert_frame_equal(result.reset_index(drop=True), expected) + + def test_merge_with_intc_columns(self): + df1 = DataFrame({"a": ["foo", "bar"], "b": np.array([1, 2], dtype=np.intc)}) + df2 = DataFrame({"a": ["foo", "baz"], "b": np.array([3, 4], dtype=np.intc)}) + result = df1.merge(df2, how="outer") + expected = DataFrame( + { + "a": ["bar", "baz", "foo", "foo"], + "b": np.array([2, 4, 1, 3], dtype=np.intc), + } + ) + tm.assert_frame_equal(result.reset_index(drop=True), expected) + + def test_merge_intc_non_monotonic(self): + df = DataFrame({"join_key": Series([0, 2, 1], dtype=np.intc)}) + df_details = DataFrame( + {"join_key": Series([0, 1, 2], dtype=np.intc), "value": ["a", "b", "c"]} + ) + merged = df.merge(df_details, on="join_key", how="left") + expected = DataFrame( + {"join_key": np.array([0, 2, 1], dtype=np.intc), "value": ["a", "c", "b"]} + ) + tm.assert_frame_equal(merged.reset_index(drop=True), expected) + @pytest.fixture def left(): From d11ed2f1193c7a45446a702170b8ca0368bc07d3 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Sat, 2 Nov 2024 00:17:26 +0530 Subject: [PATCH 003/266] DOC: fix SA01,ES01 for pandas.arrays.IntervalArray.left (#60168) --- ci/code_checks.sh | 1 - pandas/core/arrays/interval.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 768e05b16cfe9..adcf48507698b 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -89,7 +89,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.api.types.pandas_dtype PR07,RT03,SA01" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ -i "pandas.arrays.IntegerArray SA01" \ - -i "pandas.arrays.IntervalArray.left SA01" \ -i "pandas.arrays.IntervalArray.length SA01" \ -i "pandas.arrays.IntervalArray.right SA01" \ -i "pandas.arrays.NumpyExtensionArray SA01" \ diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 2ac9c77bef322..c58d03fefedb5 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1233,6 +1233,22 @@ def left(self) -> Index: """ Return the left endpoints of each Interval in the IntervalArray as an Index. + This property provides access to the left endpoints of the intervals + contained within the IntervalArray. This can be useful for analyses where + the starting point of each interval is of interest, such as in histogram + creation, data aggregation, or any scenario requiring the identification + of the beginning of defined ranges. This property returns a ``pandas.Index`` + object containing the midpoint for each interval. + + See Also + -------- + arrays.IntervalArray.right : Return the right endpoints of each Interval in + the IntervalArray as an Index. + arrays.IntervalArray.mid : Return the midpoint of each Interval in the + IntervalArray as an Index. + arrays.IntervalArray.contains : Check elementwise if the Intervals contain + the value. + Examples -------- From 9a015d19514597ad5d1c31f566a0a2bdb17d4cc5 Mon Sep 17 00:00:00 2001 From: Shreyal Gupta <99545557+Ravenin7@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:36:35 +0530 Subject: [PATCH 004/266] DOC: Update contributing docs for Windows build tools instructions (#60170) * DOC: Update Windows build tools instructions for VS Build Tools 2022 * fix trailing whitespaces --- doc/source/development/contributing_environment.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/development/contributing_environment.rst b/doc/source/development/contributing_environment.rst index 1426d3a84a748..98bd4b00d016b 100644 --- a/doc/source/development/contributing_environment.rst +++ b/doc/source/development/contributing_environment.rst @@ -35,6 +35,10 @@ You will need `Build Tools for Visual Studio 2022 scrolling down to "All downloads" -> "Tools for Visual Studio". In the installer, select the "Desktop development with C++" Workloads. + If you encounter an error indicating ``cl.exe`` is not found when building with Meson, + reopen the installer and also select the optional component + **MSVC v142 - VS 2019 C++ x64/x86 build tools** in the right pane for installation. + Alternatively, you can install the necessary components on the commandline using `vs_BuildTools.exe `_ From dc057b4f65b5a2fb5490e65157bc0b36f2638cd1 Mon Sep 17 00:00:00 2001 From: calvin Date: Mon, 4 Nov 2024 01:09:32 -0800 Subject: [PATCH 005/266] DOC: Typo fix in "comparison_with_r (#60177) --- doc/source/getting_started/comparison/comparison_with_r.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/getting_started/comparison/comparison_with_r.rst b/doc/source/getting_started/comparison/comparison_with_r.rst index 25ba237e8caf3..d9d7d916b0238 100644 --- a/doc/source/getting_started/comparison/comparison_with_r.rst +++ b/doc/source/getting_started/comparison/comparison_with_r.rst @@ -405,7 +405,7 @@ In Python, this list would be a list of tuples, so a = list(enumerate(list(range(1, 5)) + [np.NAN])) pd.DataFrame(a) -For more details and examples see :ref:`the Into to Data Structures +For more details and examples see :ref:`the Intro to Data Structures documentation `. meltdf From 988a7c83183912e56db8ec92736d85ece7f4fdc2 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Mon, 4 Nov 2024 10:19:12 +0100 Subject: [PATCH 006/266] TST (string dtype): remove xfails in extension tests + fix categorical/string dispatch (#60134) Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/core/arrays/string_.py | 1 - pandas/tests/extension/base/ops.py | 26 ---------------------- pandas/tests/extension/test_categorical.py | 2 -- pandas/tests/extension/test_numpy.py | 7 ------ 4 files changed, 36 deletions(-) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index 93c678f606fcd..c9e53abc31182 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -915,7 +915,6 @@ def _cmp_method(self, other, op): if not is_array_like(other): other = np.asarray(other) other = other[valid] - other = np.asarray(other) if op.__name__ in ops.ARITHMETIC_BINOPS: result = np.empty_like(self._ndarray, dtype="object") diff --git a/pandas/tests/extension/base/ops.py b/pandas/tests/extension/base/ops.py index 547114ecfddd0..222ff42d45052 100644 --- a/pandas/tests/extension/base/ops.py +++ b/pandas/tests/extension/base/ops.py @@ -5,10 +5,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW - from pandas.core.dtypes.common import is_string_dtype import pandas as pd @@ -134,12 +130,6 @@ class BaseArithmeticOpsTests(BaseOpsUtil): series_array_exc: type[Exception] | None = TypeError divmod_exc: type[Exception] | None = TypeError - # TODO(infer_string) need to remove import of pyarrow - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, - reason="TODO(infer_string)", - strict=False, - ) def test_arith_series_with_scalar(self, data, all_arithmetic_operators): # series & scalar if all_arithmetic_operators == "__rmod__" and is_string_dtype(data.dtype): @@ -149,11 +139,6 @@ def test_arith_series_with_scalar(self, data, all_arithmetic_operators): ser = pd.Series(data) self.check_opname(ser, op_name, ser.iloc[0]) - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, - reason="TODO(infer_string)", - strict=False, - ) def test_arith_frame_with_scalar(self, data, all_arithmetic_operators): # frame & scalar if all_arithmetic_operators == "__rmod__" and is_string_dtype(data.dtype): @@ -163,22 +148,12 @@ def test_arith_frame_with_scalar(self, data, all_arithmetic_operators): df = pd.DataFrame({"A": data}) self.check_opname(df, op_name, data[0]) - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, - reason="TODO(infer_string)", - strict=False, - ) def test_arith_series_with_array(self, data, all_arithmetic_operators): # ndarray & other series op_name = all_arithmetic_operators ser = pd.Series(data) self.check_opname(ser, op_name, pd.Series([ser.iloc[0]] * len(ser))) - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, - reason="TODO(infer_string)", - strict=False, - ) def test_divmod(self, data): ser = pd.Series(data) self._check_divmod_op(ser, divmod, 1) @@ -194,7 +169,6 @@ def test_divmod_series_array(self, data, data_for_twos): other = pd.Series(other) self._check_divmod_op(other, ops.rdivmod, ser) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_add_series_with_extension_array(self, data): # Check adding an ExtensionArray to a Series of the same dtype matches # the behavior of adding the arrays directly and then wrapping in a diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py index c3d4b83f731a3..8f8af607585df 100644 --- a/pandas/tests/extension/test_categorical.py +++ b/pandas/tests/extension/test_categorical.py @@ -140,7 +140,6 @@ def test_map(self, data, na_action): result = data.map(lambda x: x, na_action=na_action) tm.assert_extension_array_equal(result, data) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_arith_frame_with_scalar(self, data, all_arithmetic_operators, request): # frame & scalar op_name = all_arithmetic_operators @@ -152,7 +151,6 @@ def test_arith_frame_with_scalar(self, data, all_arithmetic_operators, request): ) super().test_arith_frame_with_scalar(data, op_name) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_arith_series_with_scalar(self, data, all_arithmetic_operators, request): op_name = all_arithmetic_operators if op_name == "__rmod__": diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py index 1b251a5118681..79cfb736941d6 100644 --- a/pandas/tests/extension/test_numpy.py +++ b/pandas/tests/extension/test_numpy.py @@ -19,8 +19,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.dtypes import NumpyEADtype import pandas as pd @@ -257,7 +255,6 @@ def test_insert_invalid(self, data, invalid_scalar): frame_scalar_exc = None series_array_exc = None - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_divmod(self, data): divmod_exc = None if data.dtype.kind == "O": @@ -265,7 +262,6 @@ def test_divmod(self, data): self.divmod_exc = divmod_exc super().test_divmod(data) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_divmod_series_array(self, data): ser = pd.Series(data) exc = None @@ -274,7 +270,6 @@ def test_divmod_series_array(self, data): self.divmod_exc = exc self._check_divmod_op(ser, divmod, data) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_arith_series_with_scalar(self, data, all_arithmetic_operators, request): opname = all_arithmetic_operators series_scalar_exc = None @@ -288,7 +283,6 @@ def test_arith_series_with_scalar(self, data, all_arithmetic_operators, request) self.series_scalar_exc = series_scalar_exc super().test_arith_series_with_scalar(data, all_arithmetic_operators) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_arith_series_with_array(self, data, all_arithmetic_operators): opname = all_arithmetic_operators series_array_exc = None @@ -297,7 +291,6 @@ def test_arith_series_with_array(self, data, all_arithmetic_operators): self.series_array_exc = series_array_exc super().test_arith_series_with_array(data, all_arithmetic_operators) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_arith_frame_with_scalar(self, data, all_arithmetic_operators, request): opname = all_arithmetic_operators frame_scalar_exc = None From dbeeb1f05bca199b3c1aed979e6ae72074a82243 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 4 Nov 2024 02:38:45 -0800 Subject: [PATCH 007/266] TST (string dtype): un-xfail string tests specific to object dtype (#59433) Co-authored-by: Joris Van den Bossche --- pandas/tests/copy_view/test_interp_fillna.py | 8 ++---- pandas/tests/copy_view/test_replace.py | 3 +-- pandas/tests/test_algos.py | 28 +++++++++++++------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/pandas/tests/copy_view/test_interp_fillna.py b/pandas/tests/copy_view/test_interp_fillna.py index fc57178b897b9..6bcda0ef2c35a 100644 --- a/pandas/tests/copy_view/test_interp_fillna.py +++ b/pandas/tests/copy_view/test_interp_fillna.py @@ -1,10 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW - from pandas import ( NA, DataFrame, @@ -114,18 +110,18 @@ def test_interp_fill_functions_inplace(func, dtype): assert view._mgr._has_no_reference(0) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_interpolate_cannot_with_object_dtype(): df = DataFrame({"a": ["a", np.nan, "c"], "b": 1}) + df["a"] = df["a"].astype(object) msg = "DataFrame cannot interpolate with object dtype" with pytest.raises(TypeError, match=msg): df.interpolate() -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") def test_interpolate_object_convert_no_op(): df = DataFrame({"a": ["a", "b", "c"], "b": 1}) + df["a"] = df["a"].astype(object) arr_a = get_array(df, "a") # Now CoW makes a copy, it should not! diff --git a/pandas/tests/copy_view/test_replace.py b/pandas/tests/copy_view/test_replace.py index a8acd446ff5f5..e57514bffdf1e 100644 --- a/pandas/tests/copy_view/test_replace.py +++ b/pandas/tests/copy_view/test_replace.py @@ -259,10 +259,9 @@ def test_replace_empty_list(): assert not df2._mgr._has_no_reference(0) -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") @pytest.mark.parametrize("value", ["d", None]) def test_replace_object_list_inplace(value): - df = DataFrame({"a": ["a", "b", "c"]}) + df = DataFrame({"a": ["a", "b", "c"]}, dtype=object) arr = get_array(df, "a") df.replace(["c"], value, inplace=True) assert np.shares_memory(arr, get_array(df, "a")) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index dac74a0e32a42..3d1177c23c612 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -4,8 +4,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs import ( algos as libalgos, hashtable as ht, @@ -1684,12 +1682,17 @@ def test_unique_complex_numbers(self, array, expected): class TestHashTable: - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "htable, data", [ - (ht.PyObjectHashTable, [f"foo_{i}" for i in range(1000)]), - (ht.StringHashTable, [f"foo_{i}" for i in range(1000)]), + ( + ht.PyObjectHashTable, + np.array([f"foo_{i}" for i in range(1000)], dtype=object), + ), + ( + ht.StringHashTable, + np.array([f"foo_{i}" for i in range(1000)], dtype=object), + ), (ht.Float64HashTable, np.arange(1000, dtype=np.float64)), (ht.Int64HashTable, np.arange(1000, dtype=np.int64)), (ht.UInt64HashTable, np.arange(1000, dtype=np.uint64)), @@ -1697,7 +1700,7 @@ class TestHashTable: ) def test_hashtable_unique(self, htable, data, writable): # output of maker has guaranteed unique elements - s = Series(data) + s = Series(data, dtype=data.dtype) if htable == ht.Float64HashTable: # add NaN for float column s.loc[500] = np.nan @@ -1724,12 +1727,17 @@ def test_hashtable_unique(self, htable, data, writable): reconstr = result_unique[result_inverse] tm.assert_numpy_array_equal(reconstr, s_duplicated.values) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "htable, data", [ - (ht.PyObjectHashTable, [f"foo_{i}" for i in range(1000)]), - (ht.StringHashTable, [f"foo_{i}" for i in range(1000)]), + ( + ht.PyObjectHashTable, + np.array([f"foo_{i}" for i in range(1000)], dtype=object), + ), + ( + ht.StringHashTable, + np.array([f"foo_{i}" for i in range(1000)], dtype=object), + ), (ht.Float64HashTable, np.arange(1000, dtype=np.float64)), (ht.Int64HashTable, np.arange(1000, dtype=np.int64)), (ht.UInt64HashTable, np.arange(1000, dtype=np.uint64)), @@ -1737,7 +1745,7 @@ def test_hashtable_unique(self, htable, data, writable): ) def test_hashtable_factorize(self, htable, writable, data): # output of maker has guaranteed unique elements - s = Series(data) + s = Series(data, dtype=data.dtype) if htable == ht.Float64HashTable: # add NaN for float column s.loc[500] = np.nan From fc0301d54c92dadb9f1ccfeaf21a5a76ea51d2db Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:03:16 -0800 Subject: [PATCH 008/266] [pre-commit.ci] pre-commit autoupdate (#60185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.7.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.2) - [github.com/asottile/pyupgrade: v3.17.0 → v3.19.0](https://github.com/asottile/pyupgrade/compare/v3.17.0...v3.19.0) - [github.com/pre-commit/mirrors-clang-format: v19.1.1 → v19.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.1...v19.1.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87212309725c7..09912bfb6c349 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ ci: skip: [pyright, mypy] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.7.2 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -74,7 +74,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.0 hooks: - id: pyupgrade args: [--py310-plus] @@ -95,7 +95,7 @@ repos: - id: sphinx-lint args: ["--enable", "all", "--disable", "line-too-long"] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.1 + rev: v19.1.3 hooks: - id: clang-format files: ^pandas/_libs/src|^pandas/_libs/include From 9ec4a9150ef6dbf6da1248b7252141d48203d941 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Mon, 4 Nov 2024 19:11:25 +0100 Subject: [PATCH 009/266] TST (string dtype): fix invalid comparison error message and update test (#60176) --- pandas/core/arrays/arrow/array.py | 2 +- pandas/tests/frame/test_arithmetic.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 53f703b701217..52d7fba8798e6 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -734,7 +734,7 @@ def _cmp_method(self, other, op) -> ArrowExtensionArray: try: result[valid] = op(np_array[valid], other) except TypeError: - result = ops.invalid_comparison(np_array, other, op) + result = ops.invalid_comparison(self, other, op) result = pa.array(result, type=pa.bool_()) result = pc.if_else(valid, result, None) else: diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index e41a3b27e592c..6b61fe8b05219 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -13,8 +13,6 @@ from pandas._config import using_string_dtype -from pandas.compat import HAS_PYARROW - import pandas as pd from pandas import ( DataFrame, @@ -1544,9 +1542,6 @@ def test_comparisons(self, simple_frame, float_frame, func): with pytest.raises(ValueError, match=msg): func(simple_frame, simple_frame[:2]) - @pytest.mark.xfail( - using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)" - ) def test_strings_to_numbers_comparisons_raises(self, compare_operators_no_eq_ne): # GH 11565 df = DataFrame( @@ -1554,7 +1549,12 @@ def test_strings_to_numbers_comparisons_raises(self, compare_operators_no_eq_ne) ) f = getattr(operator, compare_operators_no_eq_ne) - msg = "'[<>]=?' not supported between instances of 'str' and 'int'" + msg = "|".join( + [ + "'[<>]=?' not supported between instances of 'str' and 'int'", + "Invalid comparison between dtype=str and int", + ] + ) with pytest.raises(TypeError, match=msg): f(df, 0) From cbf6e420854e6bfba9d4b8896f879dd24997223f Mon Sep 17 00:00:00 2001 From: Swati Sneha Date: Mon, 4 Nov 2024 23:42:37 +0530 Subject: [PATCH 010/266] DOC: added see also for series.dt.round in series.round (#60187) added see also for series.dt.round in series.round --- pandas/core/series.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/core/series.py b/pandas/core/series.py index fe2bb0b5aa5c3..d83d9715878f8 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2482,6 +2482,7 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Series: -------- numpy.around : Round values of an np.array. DataFrame.round : Round values of a DataFrame. + Series.dt.round : Round values of data to the specified freq. Notes ----- From eacf0326efb709169ebc49f040834670dfe4beb3 Mon Sep 17 00:00:00 2001 From: Sebastian Berg Date: Mon, 4 Nov 2024 21:19:55 +0100 Subject: [PATCH 011/266] BUG: Fix copy semantics in ``__array__`` (#60046) Co-authored-by: Joris Van den Bossche --- doc/source/whatsnew/v2.3.0.rst | 3 ++ pandas/core/arrays/arrow/array.py | 11 ++++- pandas/core/arrays/categorical.py | 24 +++++++---- pandas/core/arrays/datetimelike.py | 7 ++++ pandas/core/arrays/interval.py | 5 +++ pandas/core/arrays/masked.py | 12 +++++- pandas/core/arrays/numpy_.py | 3 ++ pandas/core/arrays/period.py | 15 ++++++- pandas/core/arrays/sparse/array.py | 15 +++++-- pandas/core/generic.py | 13 +++++- pandas/core/indexes/base.py | 6 ++- pandas/core/indexes/multi.py | 9 ++++ pandas/core/internals/construction.py | 2 +- pandas/core/series.py | 13 ++++-- pandas/tests/arrays/sparse/test_array.py | 31 ++++++++++++++ pandas/tests/arrays/test_datetimelike.py | 8 ++++ pandas/tests/base/test_conversion.py | 41 ++++++++++++++++--- pandas/tests/extension/base/interface.py | 21 ++++++++++ pandas/tests/extension/json/array.py | 10 ++++- pandas/tests/indexes/multi/test_conversion.py | 36 ++++++++++++++++ 20 files changed, 255 insertions(+), 30 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index 5d72fabedcee8..90f9f4ed464c6 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -32,6 +32,9 @@ enhancement1 Other enhancements ^^^^^^^^^^^^^^^^^^ +- The semantics for the ``copy`` keyword in ``__array__`` methods (i.e. called + when using ``np.array()`` or ``np.asarray()`` on pandas objects) has been + updated to work correctly with NumPy >= 2 (:issue:`57739`) - The :meth:`~Series.sum` reduction is now implemented for ``StringDtype`` columns (:issue:`59853`) - diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 52d7fba8798e6..b6f1412066574 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -668,7 +668,16 @@ def __array__( self, dtype: NpDtype | None = None, copy: bool | None = None ) -> np.ndarray: """Correctly construct numpy arrays when passed to `np.asarray()`.""" - return self.to_numpy(dtype=dtype) + if copy is False: + # TODO: By using `zero_copy_only` it may be possible to implement this + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + elif copy is None: + # `to_numpy(copy=False)` has the meaning of NumPy `copy=None`. + copy = False + + return self.to_numpy(dtype=dtype, copy=copy) def __invert__(self) -> Self: # This is a bit wise op for integer types diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 7cde4c53cb2f5..99e4cb0545e2d 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -579,11 +579,12 @@ def astype(self, dtype: AstypeArg, copy: bool = True) -> ArrayLike: raise ValueError("Cannot convert float NaN to integer") elif len(self.codes) == 0 or len(self.categories) == 0: - result = np.array( - self, - dtype=dtype, - copy=copy, - ) + # For NumPy 1.x compatibility we cannot use copy=None. And + # `copy=False` has the meaning of `copy=None` here: + if not copy: + result = np.asarray(self, dtype=dtype) + else: + result = np.array(self, dtype=dtype) else: # GH8628 (PERF): astype category codes instead of astyping array @@ -1663,7 +1664,7 @@ def __array__( Specifies the the dtype for the array. copy : bool or None, optional - Unused. + See :func:`numpy.asarray`. Returns ------- @@ -1686,13 +1687,18 @@ def __array__( >>> np.asarray(cat) array(['a', 'b'], dtype=object) """ + if copy is False: + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + ret = take_nd(self.categories._values, self._codes) - if dtype and np.dtype(dtype) != self.categories.dtype: - return np.asarray(ret, dtype) # When we're a Categorical[ExtensionArray], like Interval, # we need to ensure __array__ gets all the way to an # ndarray. - return np.asarray(ret) + + # `take_nd` should already make a copy, so don't force again. + return np.asarray(ret, dtype=dtype) def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): # for binary ops, use our custom dunder methods diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index a25a698856747..9c821bf0d184e 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -359,7 +359,14 @@ def __array__( ) -> np.ndarray: # used for Timedelta/DatetimeArray, overwritten by PeriodArray if is_object_dtype(dtype): + if copy is False: + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) return np.array(list(self), dtype=object) + + if copy is True: + return np.array(self._ndarray, dtype=dtype) return self._ndarray @overload diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index c58d03fefedb5..3e231fb9f8ecb 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1622,6 +1622,11 @@ def __array__( Return the IntervalArray's data as a numpy array of Interval objects (with dtype='object') """ + if copy is False: + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + left = self._left right = self._right mask = self.isna() diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index 92ed690e527c7..349d2ec4d3cc9 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -581,7 +581,17 @@ def __array__( the array interface, return my values We return an object array here to preserve our scalar values """ - return self.to_numpy(dtype=dtype) + if copy is False: + if not self._hasna: + # special case, here we can simply return the underlying data + return np.array(self._data, dtype=dtype, copy=copy) + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + + if copy is None: + copy = False # The NumPy copy=False meaning is different here. + return self.to_numpy(dtype=dtype, copy=copy) _HANDLED_TYPES: tuple[type, ...] diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index aafcd82114b97..9f7238a97d808 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -150,6 +150,9 @@ def dtype(self) -> NumpyEADtype: def __array__( self, dtype: NpDtype | None = None, copy: bool | None = None ) -> np.ndarray: + if copy is not None: + # Note: branch avoids `copy=None` for NumPy 1.x support + return np.array(self._ndarray, dtype=dtype, copy=copy) return np.asarray(self._ndarray, dtype=dtype) def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 7d0ad74f851f0..ae92e17332c76 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -390,8 +390,19 @@ def __array__( self, dtype: NpDtype | None = None, copy: bool | None = None ) -> np.ndarray: if dtype == "i8": - return self.asi8 - elif dtype == bool: + # For NumPy 1.x compatibility we cannot use copy=None. And + # `copy=False` has the meaning of `copy=None` here: + if not copy: + return np.asarray(self.asi8, dtype=dtype) + else: + return np.array(self.asi8, dtype=dtype) + + if copy is False: + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + + if dtype == bool: return ~self._isnan # This will raise TypeError for non-object dtypes diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index 0c76280e7fdb4..a3db7dc1f93e9 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -547,11 +547,20 @@ def from_spmatrix(cls, data: spmatrix) -> Self: def __array__( self, dtype: NpDtype | None = None, copy: bool | None = None ) -> np.ndarray: - fill_value = self.fill_value - if self.sp_index.ngaps == 0: # Compat for na dtype and int values. - return self.sp_values + if copy is True: + return np.array(self.sp_values) + else: + return self.sp_values + + if copy is False: + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + + fill_value = self.fill_value + if dtype is None: # Can NumPy represent this type? # If not, `np.result_type` will raise. We catch that diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 756c431022063..bbd627d4f0d73 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -2015,8 +2015,17 @@ def __array__( self, dtype: npt.DTypeLike | None = None, copy: bool | None = None ) -> np.ndarray: values = self._values - arr = np.asarray(values, dtype=dtype) - if astype_is_view(values.dtype, arr.dtype) and self._mgr.is_single_block: + if copy is None: + # Note: branch avoids `copy=None` for NumPy 1.x support + arr = np.asarray(values, dtype=dtype) + else: + arr = np.array(values, dtype=dtype, copy=copy) + + if ( + copy is not True + and astype_is_view(values.dtype, arr.dtype) + and self._mgr.is_single_block + ): # Check if both conversions can be done without a copy if astype_is_view(self.dtypes.iloc[0], values.dtype) and astype_is_view( values.dtype, arr.dtype diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 749a5fea4d513..cf3d1e6a2ee2d 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -908,7 +908,11 @@ def __array__(self, dtype=None, copy=None) -> np.ndarray: """ The array interface, return my values. """ - return np.asarray(self._data, dtype=dtype) + if copy is None: + # Note, that the if branch exists for NumPy 1.x support + return np.asarray(self._data, dtype=dtype) + + return np.array(self._data, dtype=dtype, copy=copy) def __array_ufunc__(self, ufunc: np.ufunc, method: str_t, *inputs, **kwargs): if any(isinstance(other, (ABCSeries, ABCDataFrame)) for other in inputs): diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index ae9b272af9fe9..e6ce00cb714a4 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -1391,6 +1391,15 @@ def copy( # type: ignore[override] def __array__(self, dtype=None, copy=None) -> np.ndarray: """the array interface, return my values""" + if copy is False: + # self.values is always a newly construct array, so raise. + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + if copy is True: + # explicit np.array call to ensure a copy is made and unique objects + # are returned, because self.values is cached + return np.array(self.values, dtype=dtype) return self.values def view(self, cls=None) -> Self: diff --git a/pandas/core/internals/construction.py b/pandas/core/internals/construction.py index 959e572b2b35b..0812ba5e6def4 100644 --- a/pandas/core/internals/construction.py +++ b/pandas/core/internals/construction.py @@ -258,7 +258,7 @@ def ndarray_to_mgr( # and a subsequent `astype` will not already result in a copy values = np.array(values, copy=True, order="F") else: - values = np.array(values, copy=False) + values = np.asarray(values) values = _ensure_2d(values) else: diff --git a/pandas/core/series.py b/pandas/core/series.py index d83d9715878f8..1d601f36d604a 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -842,7 +842,7 @@ def __array__( the dtype is inferred from the data. copy : bool or None, optional - Unused. + See :func:`numpy.asarray`. Returns ------- @@ -879,8 +879,15 @@ def __array__( dtype='datetime64[ns]') """ values = self._values - arr = np.asarray(values, dtype=dtype) - if astype_is_view(values.dtype, arr.dtype): + if copy is None: + # Note: branch avoids `copy=None` for NumPy 1.x support + arr = np.asarray(values, dtype=dtype) + else: + arr = np.array(values, dtype=dtype, copy=copy) + + if copy is True: + return arr + if copy is False or astype_is_view(values.dtype, arr.dtype): arr = arr.view() arr.flags.writeable = False return arr diff --git a/pandas/tests/arrays/sparse/test_array.py b/pandas/tests/arrays/sparse/test_array.py index c35e8204f3437..1b685100e4931 100644 --- a/pandas/tests/arrays/sparse/test_array.py +++ b/pandas/tests/arrays/sparse/test_array.py @@ -4,6 +4,7 @@ import pytest from pandas._libs.sparse import IntIndex +from pandas.compat.numpy import np_version_gt2 import pandas as pd from pandas import ( @@ -480,3 +481,33 @@ def test_zero_sparse_column(): expected = pd.DataFrame({"A": SparseArray([0, 0]), "B": [1, 3]}, index=[0, 2]) tm.assert_frame_equal(result, expected) + + +def test_array_interface(arr_data, arr): + # https://github.com/pandas-dev/pandas/pull/60046 + result = np.asarray(arr) + tm.assert_numpy_array_equal(result, arr_data) + + # it always gives a copy by default + result_copy1 = np.asarray(arr) + result_copy2 = np.asarray(arr) + assert not np.may_share_memory(result_copy1, result_copy2) + + # or with explicit copy=True + result_copy1 = np.array(arr, copy=True) + result_copy2 = np.array(arr, copy=True) + assert not np.may_share_memory(result_copy1, result_copy2) + + if not np_version_gt2: + # copy=False semantics are only supported in NumPy>=2. + return + + # for sparse arrays, copy=False is never allowed + with pytest.raises(ValueError, match="Unable to avoid copy while creating"): + np.array(arr, copy=False) + + # except when there are actually no sparse filled values + arr2 = SparseArray(np.array([1, 2, 3])) + result_nocopy1 = np.array(arr2, copy=False) + result_nocopy2 = np.array(arr2, copy=False) + assert np.may_share_memory(result_nocopy1, result_nocopy2) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 0c8eefab95464..d1ef29b0bf8a0 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -1152,9 +1152,17 @@ def test_array_interface(self, arr1d): result = np.asarray(arr, dtype=object) tm.assert_numpy_array_equal(result, expected) + # to int64 gives the underlying representation result = np.asarray(arr, dtype="int64") tm.assert_numpy_array_equal(result, arr.asi8) + result2 = np.asarray(arr, dtype="int64") + assert np.may_share_memory(result, result2) + + result_copy1 = np.array(arr, dtype="int64", copy=True) + result_copy2 = np.array(arr, dtype="int64", copy=True) + assert not np.may_share_memory(result_copy1, result_copy2) + # to other dtypes msg = r"float\(\) argument must be a string or a( real)? number, not 'Period'" with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/base/test_conversion.py b/pandas/tests/base/test_conversion.py index d8af7abe83084..888e8628f8664 100644 --- a/pandas/tests/base/test_conversion.py +++ b/pandas/tests/base/test_conversion.py @@ -4,6 +4,7 @@ from pandas._config import using_string_dtype from pandas.compat import HAS_PYARROW +from pandas.compat.numpy import np_version_gt2 from pandas.core.dtypes.dtypes import DatetimeTZDtype @@ -297,24 +298,27 @@ def test_array_multiindex_raises(): @pytest.mark.parametrize( - "arr, expected", + "arr, expected, zero_copy", [ - (np.array([1, 2], dtype=np.int64), np.array([1, 2], dtype=np.int64)), - (pd.Categorical(["a", "b"]), np.array(["a", "b"], dtype=object)), + (np.array([1, 2], dtype=np.int64), np.array([1, 2], dtype=np.int64), True), + (pd.Categorical(["a", "b"]), np.array(["a", "b"], dtype=object), False), ( pd.core.arrays.period_array(["2000", "2001"], freq="D"), np.array([pd.Period("2000", freq="D"), pd.Period("2001", freq="D")]), + False, ), - (pd.array([0, np.nan], dtype="Int64"), np.array([0, np.nan])), + (pd.array([0, np.nan], dtype="Int64"), np.array([0, np.nan]), False), ( IntervalArray.from_breaks([0, 1, 2]), np.array([pd.Interval(0, 1), pd.Interval(1, 2)], dtype=object), + False, ), - (SparseArray([0, 1]), np.array([0, 1], dtype=np.int64)), + (SparseArray([0, 1]), np.array([0, 1], dtype=np.int64), False), # tz-naive datetime ( DatetimeArray._from_sequence(np.array(["2000", "2001"], dtype="M8[ns]")), np.array(["2000", "2001"], dtype="M8[ns]"), + True, ), # tz-aware stays tz`-aware ( @@ -329,6 +333,7 @@ def test_array_multiindex_raises(): Timestamp("2000-01-02", tz="US/Central"), ] ), + False, ), # Timedelta ( @@ -337,6 +342,7 @@ def test_array_multiindex_raises(): dtype=np.dtype("m8[ns]"), ), np.array([0, 3600000000000], dtype="m8[ns]"), + True, ), # GH#26406 tz is preserved in Categorical[dt64tz] ( @@ -347,10 +353,11 @@ def test_array_multiindex_raises(): Timestamp("2016-01-02", tz="US/Pacific"), ] ), + False, ), ], ) -def test_to_numpy(arr, expected, index_or_series_or_array, request): +def test_to_numpy(arr, expected, zero_copy, index_or_series_or_array): box = index_or_series_or_array with tm.assert_produces_warning(None): @@ -362,6 +369,28 @@ def test_to_numpy(arr, expected, index_or_series_or_array, request): result = np.asarray(thing) tm.assert_numpy_array_equal(result, expected) + # Additionally, we check the `copy=` semantics for array/asarray + # (these are implemented by us via `__array__`). + result_cp1 = np.array(thing, copy=True) + result_cp2 = np.array(thing, copy=True) + # When called with `copy=True` NumPy/we should ensure a copy was made + assert not np.may_share_memory(result_cp1, result_cp2) + + if not np_version_gt2: + # copy=False semantics are only supported in NumPy>=2. + return + + if not zero_copy: + with pytest.raises(ValueError, match="Unable to avoid copy while creating"): + # An error is always acceptable for `copy=False` + np.array(thing, copy=False) + + else: + result_nocopy1 = np.array(thing, copy=False) + result_nocopy2 = np.array(thing, copy=False) + # If copy=False was given, these must share the same data + assert np.may_share_memory(result_nocopy1, result_nocopy2) + @pytest.mark.xfail( using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)", strict=False diff --git a/pandas/tests/extension/base/interface.py b/pandas/tests/extension/base/interface.py index 6683c87e2b8fc..79eb64b5a654f 100644 --- a/pandas/tests/extension/base/interface.py +++ b/pandas/tests/extension/base/interface.py @@ -1,6 +1,8 @@ import numpy as np import pytest +from pandas.compat.numpy import np_version_gt2 + from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike from pandas.core.dtypes.common import is_extension_array_dtype from pandas.core.dtypes.dtypes import ExtensionDtype @@ -71,6 +73,25 @@ def test_array_interface(self, data): expected = construct_1d_object_array_from_listlike(list(data)) tm.assert_numpy_array_equal(result, expected) + def test_array_interface_copy(self, data): + result_copy1 = np.array(data, copy=True) + result_copy2 = np.array(data, copy=True) + assert not np.may_share_memory(result_copy1, result_copy2) + + if not np_version_gt2: + # copy=False semantics are only supported in NumPy>=2. + return + + try: + result_nocopy1 = np.array(data, copy=False) + except ValueError: + # An error is always acceptable for `copy=False` + return + + result_nocopy2 = np.array(data, copy=False) + # If copy=False was given and did not raise, these must share the same data + assert np.may_share_memory(result_nocopy1, result_nocopy2) + def test_is_extension_array_dtype(self, data): assert is_extension_array_dtype(data) assert is_extension_array_dtype(data.dtype) diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py index 4fa48023fbc95..a68c8a06e1d18 100644 --- a/pandas/tests/extension/json/array.py +++ b/pandas/tests/extension/json/array.py @@ -148,12 +148,20 @@ def __ne__(self, other): return NotImplemented def __array__(self, dtype=None, copy=None): + if copy is False: + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) + if dtype is None: dtype = object if dtype == object: # on py38 builds it looks like numpy is inferring to a non-1D array return construct_1d_object_array_from_listlike(list(self)) - return np.asarray(self.data, dtype=dtype) + if copy is None: + # Note: branch avoids `copy=None` for NumPy 1.x support + return np.asarray(self.data, dtype=dtype) + return np.asarray(self.data, dtype=dtype, copy=copy) @property def nbytes(self) -> int: diff --git a/pandas/tests/indexes/multi/test_conversion.py b/pandas/tests/indexes/multi/test_conversion.py index f6b10c989326f..347d6b206e3b9 100644 --- a/pandas/tests/indexes/multi/test_conversion.py +++ b/pandas/tests/indexes/multi/test_conversion.py @@ -1,6 +1,8 @@ import numpy as np import pytest +from pandas.compat.numpy import np_version_gt2 + import pandas as pd from pandas import ( DataFrame, @@ -16,6 +18,40 @@ def test_to_numpy(idx): tm.assert_numpy_array_equal(result, exp) +def test_array_interface(idx): + # https://github.com/pandas-dev/pandas/pull/60046 + result = np.asarray(idx) + expected = np.empty((6,), dtype=object) + expected[:] = [ + ("foo", "one"), + ("foo", "two"), + ("bar", "one"), + ("baz", "two"), + ("qux", "one"), + ("qux", "two"), + ] + tm.assert_numpy_array_equal(result, expected) + + # it always gives a copy by default, but the values are cached, so results + # are still sharing memory + result_copy1 = np.asarray(idx) + result_copy2 = np.asarray(idx) + assert np.may_share_memory(result_copy1, result_copy2) + + # with explicit copy=True, then it is an actual copy + result_copy1 = np.array(idx, copy=True) + result_copy2 = np.array(idx, copy=True) + assert not np.may_share_memory(result_copy1, result_copy2) + + if not np_version_gt2: + # copy=False semantics are only supported in NumPy>=2. + return + + # for MultiIndex, copy=False is never allowed + with pytest.raises(ValueError, match="Unable to avoid copy while creating"): + np.array(idx, copy=False) + + def test_to_frame(): tuples = [(1, "one"), (1, "two"), (2, "one"), (2, "two")] From 34387bddffacb158a60a249b08411a8a1fe44455 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 5 Nov 2024 17:23:25 +0100 Subject: [PATCH 012/266] TST (string dtype): avoid hardcoded object dtype for columns in datetime_frame fixture (#60192) --- pandas/tests/frame/conftest.py | 2 +- pandas/tests/frame/indexing/test_indexing.py | 1 - pandas/tests/frame/methods/test_to_csv.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pandas/tests/frame/conftest.py b/pandas/tests/frame/conftest.py index 8da7ac635f293..ea8e2e8ecc194 100644 --- a/pandas/tests/frame/conftest.py +++ b/pandas/tests/frame/conftest.py @@ -18,7 +18,7 @@ def datetime_frame() -> DataFrame: """ return DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 0723c3c70091c..3e8686fd30e44 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -177,7 +177,6 @@ def test_getitem_boolean(self, mixed_float_frame, mixed_int_frame, datetime_fram if bif[c].dtype != bifw[c].dtype: assert bif[c].dtype == df[c].dtype - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_getitem_boolean_casting(self, datetime_frame): # don't upcast if we don't need to df = datetime_frame.copy() diff --git a/pandas/tests/frame/methods/test_to_csv.py b/pandas/tests/frame/methods/test_to_csv.py index adb327e90bb76..23377b7373987 100644 --- a/pandas/tests/frame/methods/test_to_csv.py +++ b/pandas/tests/frame/methods/test_to_csv.py @@ -44,7 +44,6 @@ def test_to_csv_from_csv1(self, temp_file, float_frame): float_frame.to_csv(path, header=False) float_frame.to_csv(path, index=False) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_to_csv_from_csv1_datetime(self, temp_file, datetime_frame): path = str(temp_file) # test roundtrip @@ -549,7 +548,6 @@ def test_to_csv_headers(self, temp_file): assert return_value is None tm.assert_frame_equal(to_df, recons) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_to_csv_multiindex(self, temp_file, float_frame, datetime_frame): frame = float_frame old_index = frame.index From b84e0c81ee424444760a4587d30a7cb8662fa4d9 Mon Sep 17 00:00:00 2001 From: eightyseven Date: Wed, 6 Nov 2024 02:41:39 +0800 Subject: [PATCH 013/266] BUG: Frequency shift on empty DataFrame (#60172) * freq shift * Save local changes before merging --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/frame.py | 2 +- pandas/tests/frame/methods/test_shift.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 2e64c66812306..20efac7e2edb0 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -772,6 +772,7 @@ Other - Bug in :meth:`DataFrame.eval` and :meth:`DataFrame.query` which caused an exception when using NumPy attributes via ``@`` notation, e.g., ``df.eval("@np.floor(a)")``. (:issue:`58041`) - Bug in :meth:`DataFrame.eval` and :meth:`DataFrame.query` which did not allow to use ``tan`` function. (:issue:`55091`) - Bug in :meth:`DataFrame.query` which raised an exception or produced incorrect results when expressions contained backtick-quoted column names containing the hash character ``#``, backticks, or characters that fall outside the ASCII range (U+0001..U+007F). (:issue:`59285`) (:issue:`49633`) +- Bug in :meth:`DataFrame.shift` where passing a ``freq`` on a DataFrame with no columns did not shift the index correctly. (:issue:`60102`) - Bug in :meth:`DataFrame.sort_index` when passing ``axis="columns"`` and ``ignore_index=True`` and ``ascending=False`` not returning a :class:`RangeIndex` columns (:issue:`57293`) - Bug in :meth:`DataFrame.transform` that was returning the wrong order unless the index was monotonically increasing. (:issue:`57069`) - Bug in :meth:`DataFrame.where` where using a non-bool type array in the function would return a ``ValueError`` instead of a ``TypeError`` (:issue:`56330`) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index c4defdb24370f..a3a459796f765 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -5705,7 +5705,7 @@ def shift( "Passing a 'freq' together with a 'fill_value' is not allowed." ) - if self.empty: + if self.empty and freq is None: return self.copy() axis = self._get_axis_number(axis) diff --git a/pandas/tests/frame/methods/test_shift.py b/pandas/tests/frame/methods/test_shift.py index 4e490e9e344ba..a0f96ff111444 100644 --- a/pandas/tests/frame/methods/test_shift.py +++ b/pandas/tests/frame/methods/test_shift.py @@ -747,3 +747,13 @@ def test_shift_axis_one_empty(self): df = DataFrame() result = df.shift(1, axis=1) tm.assert_frame_equal(result, df) + + def test_shift_with_offsets_freq_empty(self): + # GH#60102 + dates = date_range("2020-01-01", periods=3, freq="D") + offset = offsets.Day() + shifted_dates = dates + offset + df = DataFrame(index=dates) + df_shifted = DataFrame(index=shifted_dates) + result = df.shift(freq=offset) + tm.assert_frame_equal(result, df_shifted) From e449b49716203bbf04d20bc867fda7ea6d562ef5 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 5 Nov 2024 19:52:01 +0100 Subject: [PATCH 014/266] TYP/COMPAT: don't use Literal for Series.ndim to avoid tab completion bug in IPython (#60197) --- pandas/core/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/core/base.py b/pandas/core/base.py index 58572aab5b20f..61a7c079d87f8 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -361,8 +361,11 @@ def __len__(self) -> int: # We need this defined here for mypy raise AbstractMethodError(self) + # Temporarily avoid using `-> Literal[1]:` because of an IPython (jedi) bug + # https://github.com/ipython/ipython/issues/14412 + # https://github.com/davidhalter/jedi/issues/1990 @property - def ndim(self) -> Literal[1]: + def ndim(self) -> int: """ Number of dimensions of the underlying data, by definition 1. From 169b00e1eaf06924aee585d8e5469dc284992382 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 5 Nov 2024 19:54:22 +0100 Subject: [PATCH 015/266] BUG: fix inspect usage when pyarrow or jinja2 is not installed (#60196) * BUG: fix inspect usage when pyarrow or jinja2 is not installed * add whatsnew note --- doc/source/whatsnew/v2.3.0.rst | 3 ++- pandas/core/arrays/arrow/accessors.py | 2 +- pandas/core/frame.py | 5 +++++ pandas/tests/frame/test_api.py | 1 - pandas/tests/series/test_api.py | 8 -------- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index 90f9f4ed464c6..922cc0ead7fb0 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -173,7 +173,8 @@ Styler Other ^^^^^ -- +- Fixed usage of ``inspect`` when the optional dependencies ``pyarrow`` or ``jinja2`` + are not installed (:issue:`60196`) - .. --------------------------------------------------------------------------- diff --git a/pandas/core/arrays/arrow/accessors.py b/pandas/core/arrays/arrow/accessors.py index d9a80b699b0bb..230522846d377 100644 --- a/pandas/core/arrays/arrow/accessors.py +++ b/pandas/core/arrays/arrow/accessors.py @@ -46,7 +46,7 @@ def _is_valid_pyarrow_dtype(self, pyarrow_dtype) -> bool: def _validate(self, data) -> None: dtype = data.dtype - if not isinstance(dtype, ArrowDtype): + if pa_version_under10p1 or not isinstance(dtype, ArrowDtype): # Raise AttributeError so that inspect can handle non-struct Series. raise AttributeError(self._validation_msg.format(dtype=dtype)) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index a3a459796f765..b35e2c8497fb7 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -1397,6 +1397,11 @@ def style(self) -> Styler: Please see `Table Visualization <../../user_guide/style.ipynb>`_ for more examples. """ + # Raise AttributeError so that inspect works even if jinja2 is not installed. + has_jinja2 = import_optional_dependency("jinja2", errors="ignore") + if not has_jinja2: + raise AttributeError("The '.style' accessor requires jinja2") + from pandas.io.formats.style import Styler return Styler(self) diff --git a/pandas/tests/frame/test_api.py b/pandas/tests/frame/test_api.py index 3fb994f2e0aff..2b0bf1b0576f9 100644 --- a/pandas/tests/frame/test_api.py +++ b/pandas/tests/frame/test_api.py @@ -376,6 +376,5 @@ def test_constructor_expanddim(self): def test_inspect_getmembers(self): # GH38740 - pytest.importorskip("jinja2") df = DataFrame() inspect.getmembers(df) diff --git a/pandas/tests/series/test_api.py b/pandas/tests/series/test_api.py index 79a55eb357f87..4b369bb0bc869 100644 --- a/pandas/tests/series/test_api.py +++ b/pandas/tests/series/test_api.py @@ -4,10 +4,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW - import pandas as pd from pandas import ( DataFrame, @@ -164,12 +160,8 @@ def test_attrs(self): result = s + 1 assert result.attrs == {"version": 1} - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)" - ) def test_inspect_getmembers(self): # GH38782 - pytest.importorskip("jinja2") ser = Series(dtype=object) inspect.getmembers(ser) From cf52dec71329797b2af84053d091bd7cfc787486 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 5 Nov 2024 19:55:24 +0100 Subject: [PATCH 016/266] BUG (string dtype): fix where() for string dtype with python storage (#60195) --- pandas/core/arrays/string_.py | 6 ++++++ pandas/tests/frame/indexing/test_where.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index c9e53abc31182..f54a5260bd699 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -757,6 +757,12 @@ def _putmask(self, mask: npt.NDArray[np.bool_], value) -> None: # base class implementation that uses __setitem__ ExtensionArray._putmask(self, mask, value) + def _where(self, mask: npt.NDArray[np.bool_], value) -> Self: + # the super() method NDArrayBackedExtensionArray._where uses + # np.putmask which doesn't properly handle None/pd.NA, so using the + # base class implementation that uses __setitem__ + return ExtensionArray._where(self, mask, value) + def isin(self, values: ArrayLike) -> npt.NDArray[np.bool_]: if isinstance(values, BaseStringArray) or ( isinstance(values, ExtensionArray) and is_string_dtype(values.dtype) diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py index 32a827c25c77a..ff66ea491e308 100644 --- a/pandas/tests/frame/indexing/test_where.py +++ b/pandas/tests/frame/indexing/test_where.py @@ -6,8 +6,6 @@ from pandas._config import using_string_dtype -from pandas.compat import HAS_PYARROW - from pandas.core.dtypes.common import is_scalar import pandas as pd @@ -940,9 +938,6 @@ def test_where_nullable_invalid_na(frame_or_series, any_numeric_ea_dtype): obj.mask(mask, null) -@pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)" -) @given(data=OPTIONAL_ONE_OF_ALL) def test_where_inplace_casting(data): # GH 22051 @@ -1023,19 +1018,18 @@ def test_where_producing_ea_cond_for_np_dtype(): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)", strict=False -) @pytest.mark.parametrize( "replacement", [0.001, True, "snake", None, datetime(2022, 5, 4)] ) -def test_where_int_overflow(replacement, using_infer_string, request): +def test_where_int_overflow(replacement, using_infer_string): # GH 31687 df = DataFrame([[1.0, 2e25, "nine"], [np.nan, 0.1, None]]) if using_infer_string and replacement not in (None, "snake"): - request.node.add_marker( - pytest.mark.xfail(reason="Can't set non-string into string column") - ) + with pytest.raises( + TypeError, match="Cannot set non-string value|Scalar must be NA or str" + ): + df.where(pd.notnull(df), replacement) + return result = df.where(pd.notnull(df), replacement) expected = DataFrame([[1.0, 2e25, "nine"], [replacement, 0.1, replacement]]) From bec2dbca274a4f983790d069279a4b3aec184f49 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 5 Nov 2024 19:56:30 +0100 Subject: [PATCH 017/266] TST (string dtype): update all tests in tests/frame/indexing (#60193) --- pandas/tests/frame/indexing/test_coercion.py | 10 +++++--- pandas/tests/frame/indexing/test_indexing.py | 16 ++++-------- pandas/tests/frame/indexing/test_insert.py | 6 ++--- pandas/tests/frame/indexing/test_setitem.py | 26 +++++++++----------- pandas/tests/frame/indexing/test_where.py | 18 +++++++++----- pandas/tests/frame/indexing/test_xs.py | 5 +--- 6 files changed, 38 insertions(+), 43 deletions(-) diff --git a/pandas/tests/frame/indexing/test_coercion.py b/pandas/tests/frame/indexing/test_coercion.py index cb1cbd68ede63..1a454351b7085 100644 --- a/pandas/tests/frame/indexing/test_coercion.py +++ b/pandas/tests/frame/indexing/test_coercion.py @@ -8,8 +8,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -84,14 +82,18 @@ def test_6942(indexer_al): assert df.iloc[0, 0] == t2 -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_26395(indexer_al): # .at case fixed by GH#45121 (best guess) df = DataFrame(index=["A", "B", "C"]) df["D"] = 0 indexer_al(df)["C", "D"] = 2 - expected = DataFrame({"D": [0, 0, 2]}, index=["A", "B", "C"], dtype=np.int64) + expected = DataFrame( + {"D": [0, 0, 2]}, + index=["A", "B", "C"], + columns=pd.Index(["D"], dtype=object), + dtype=np.int64, + ) tm.assert_frame_equal(df, expected) with pytest.raises(TypeError, match="Invalid value"): diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 3e8686fd30e44..eb14f8bdbfb86 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -12,7 +12,6 @@ from pandas._config import using_string_dtype from pandas._libs import iNaT -from pandas.compat import HAS_PYARROW from pandas.errors import InvalidIndexError from pandas.core.dtypes.common import is_integer @@ -505,17 +504,16 @@ def test_setitem_ambig(self, using_infer_string): assert dm[2].dtype == np.object_ @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_setitem_None(self, float_frame, using_infer_string): + def test_setitem_None(self, float_frame): # GH #766 float_frame[None] = float_frame["A"] - key = None if not using_infer_string else np.nan tm.assert_series_equal( float_frame.iloc[:, -1], float_frame["A"], check_names=False ) tm.assert_series_equal( - float_frame.loc[:, key], float_frame["A"], check_names=False + float_frame.loc[:, None], float_frame["A"], check_names=False ) - tm.assert_series_equal(float_frame[key], float_frame["A"], check_names=False) + tm.assert_series_equal(float_frame[None], float_frame["A"], check_names=False) def test_loc_setitem_boolean_mask_allfalse(self): # GH 9596 @@ -1125,7 +1123,6 @@ def test_setitem_with_unaligned_tz_aware_datetime_column(self): df.loc[[0, 1, 2], "dates"] = column[[1, 0, 2]] tm.assert_series_equal(df["dates"], column) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_loc_setitem_datetimelike_with_inference(self): # GH 7592 # assignment of timedeltas with NaT @@ -1144,13 +1141,10 @@ def test_loc_setitem_datetimelike_with_inference(self): result = df.dtypes expected = Series( [np.dtype("timedelta64[ns]")] * 6 + [np.dtype("datetime64[ns]")] * 2, - index=list("ABCDEFGH"), + index=Index(list("ABCDEFGH"), dtype=object), ) tm.assert_series_equal(result, expected) - @pytest.mark.xfail( - using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)" - ) def test_getitem_boolean_indexing_mixed(self): df = DataFrame( { @@ -1192,7 +1186,7 @@ def test_getitem_boolean_indexing_mixed(self): tm.assert_frame_equal(df2, expected) df["foo"] = "test" - msg = "not supported between instances|unorderable types" + msg = "not supported between instances|unorderable types|Invalid comparison" with pytest.raises(TypeError, match=msg): df[df > 0.3] = 1 diff --git a/pandas/tests/frame/indexing/test_insert.py b/pandas/tests/frame/indexing/test_insert.py index 3dd8f7196c594..a1d60eb9626d6 100644 --- a/pandas/tests/frame/indexing/test_insert.py +++ b/pandas/tests/frame/indexing/test_insert.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import PerformanceWarning from pandas import ( @@ -63,7 +61,6 @@ def test_insert_column_bug_4032(self): expected = DataFrame([[1.3, 1, 1.1], [2.3, 2, 2.2]], columns=["c", "a", "b"]) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_insert_with_columns_dups(self): # GH#14291 df = DataFrame() @@ -71,7 +68,8 @@ def test_insert_with_columns_dups(self): df.insert(0, "A", ["d", "e", "f"], allow_duplicates=True) df.insert(0, "A", ["a", "b", "c"], allow_duplicates=True) exp = DataFrame( - [["a", "d", "g"], ["b", "e", "h"], ["c", "f", "i"]], columns=["A", "A", "A"] + [["a", "d", "g"], ["b", "e", "h"], ["c", "f", "i"]], + columns=Index(["A", "A", "A"], dtype=object), ) tm.assert_frame_equal(df, exp) diff --git a/pandas/tests/frame/indexing/test_setitem.py b/pandas/tests/frame/indexing/test_setitem.py index cb971b31c13c4..cfd7e91c4ceab 100644 --- a/pandas/tests/frame/indexing/test_setitem.py +++ b/pandas/tests/frame/indexing/test_setitem.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.base import _registry as ea_registry from pandas.core.dtypes.common import is_object_dtype from pandas.core.dtypes.dtypes import ( @@ -146,13 +144,16 @@ def test_setitem_different_dtype(self): ) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_empty_columns(self): # GH 13522 df = DataFrame(index=["A", "B", "C"]) df["X"] = df.index df["X"] = ["x", "y", "z"] - exp = DataFrame(data={"X": ["x", "y", "z"]}, index=["A", "B", "C"]) + exp = DataFrame( + data={"X": ["x", "y", "z"]}, + index=["A", "B", "C"], + columns=Index(["X"], dtype=object), + ) tm.assert_frame_equal(df, exp) def test_setitem_dt64_index_empty_columns(self): @@ -162,14 +163,15 @@ def test_setitem_dt64_index_empty_columns(self): df["A"] = rng assert df["A"].dtype == np.dtype("M8[ns]") - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_timestamp_empty_columns(self): # GH#19843 df = DataFrame(index=range(3)) df["now"] = Timestamp("20130101", tz="UTC") expected = DataFrame( - [[Timestamp("20130101", tz="UTC")]] * 3, index=range(3), columns=["now"] + [[Timestamp("20130101", tz="UTC")]] * 3, + index=range(3), + columns=Index(["now"], dtype=object), ) tm.assert_frame_equal(df, expected) @@ -202,14 +204,13 @@ def test_setitem_with_unaligned_sparse_value(self): expected = Series(SparseArray([1, 0, 0]), name="new_column") tm.assert_series_equal(df["new_column"], expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_period_preserves_dtype(self): # GH: 26861 data = [Period("2003-12", "D")] result = DataFrame([]) result["a"] = data - expected = DataFrame({"a": data}) + expected = DataFrame({"a": data}, columns=Index(["a"], dtype=object)) tm.assert_frame_equal(result, expected) @@ -672,11 +673,10 @@ def test_setitem_iloc_two_dimensional_generator(self): expected = DataFrame({"a": [1, 2, 3], "b": [4, 1, 1]}) tm.assert_frame_equal(df, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_dtypes_bytes_type_to_object(self): # GH 20734 index = Series(name="id", dtype="S24") - df = DataFrame(index=index) + df = DataFrame(index=index, columns=Index([], dtype="str")) df["a"] = Series(name="a", index=index, dtype=np.uint32) df["b"] = Series(name="b", index=index, dtype="S64") df["c"] = Series(name="c", index=index, dtype="S64") @@ -705,7 +705,6 @@ def test_setitem_ea_dtype_rhs_series(self): expected = DataFrame({"a": [1, 2]}, dtype="Int64") tm.assert_frame_equal(df, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_npmatrix_2d(self): # GH#42376 # for use-case df["x"] = sparse.random((10, 10)).mean(axis=1) @@ -714,7 +713,7 @@ def test_setitem_npmatrix_2d(self): ) a = np.ones((10, 1)) - df = DataFrame(index=np.arange(10)) + df = DataFrame(index=np.arange(10), columns=Index([], dtype="str")) df["np-array"] = a # Instantiation of `np.matrix` gives PendingDeprecationWarning @@ -927,12 +926,11 @@ def test_setitem_with_expansion_categorical_dtype(self): ser.name = "E" tm.assert_series_equal(result2.sort_index(), ser.sort_index()) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_scalars_no_index(self): # GH#16823 / GH#17894 df = DataFrame() df["foo"] = 1 - expected = DataFrame(columns=["foo"]).astype(np.int64) + expected = DataFrame(columns=Index(["foo"], dtype=object)).astype(np.int64) tm.assert_frame_equal(df, expected) def test_setitem_newcol_tuple_key(self, float_frame): diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py index ff66ea491e308..d3040052ea696 100644 --- a/pandas/tests/frame/indexing/test_where.py +++ b/pandas/tests/frame/indexing/test_where.py @@ -48,7 +48,6 @@ def is_ok(s): class TestDataFrameIndexingWhere: - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_where_get(self, where_frame, float_string_frame): def _check_get(df, cond, check_dtypes=True): other1 = _safe_add(df) @@ -66,7 +65,10 @@ def _check_get(df, cond, check_dtypes=True): # check getting df = where_frame if df is float_string_frame: - msg = "'>' not supported between instances of 'str' and 'int'" + msg = ( + "'>' not supported between instances of 'str' and 'int'" + "|Invalid comparison" + ) with pytest.raises(TypeError, match=msg): df > 0 return @@ -99,7 +101,6 @@ def test_where_upcasting(self): tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_where_alignment(self, where_frame, float_string_frame): # aligning def _check_align(df, cond, other, check_dtypes=True): @@ -131,7 +132,10 @@ def _check_align(df, cond, other, check_dtypes=True): df = where_frame if df is float_string_frame: - msg = "'>' not supported between instances of 'str' and 'int'" + msg = ( + "'>' not supported between instances of 'str' and 'int'" + "|Invalid comparison" + ) with pytest.raises(TypeError, match=msg): df > 0 return @@ -174,7 +178,6 @@ def test_where_invalid(self): with pytest.raises(ValueError, match=msg): df.mask(0) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_where_set(self, where_frame, float_string_frame, mixed_int_frame): # where inplace @@ -196,7 +199,10 @@ def _check_set(df, cond, check_dtypes=True): df = where_frame if df is float_string_frame: - msg = "'>' not supported between instances of 'str' and 'int'" + msg = ( + "'>' not supported between instances of 'str' and 'int'" + "|Invalid comparison" + ) with pytest.raises(TypeError, match=msg): df > 0 return diff --git a/pandas/tests/frame/indexing/test_xs.py b/pandas/tests/frame/indexing/test_xs.py index a01b68f1fea2a..54733129b4d47 100644 --- a/pandas/tests/frame/indexing/test_xs.py +++ b/pandas/tests/frame/indexing/test_xs.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, Index, @@ -74,10 +72,9 @@ def test_xs_other(self, float_frame): tm.assert_series_equal(float_frame["A"], float_frame_orig["A"]) assert not (expected == 5).all() - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_xs_corner(self): # pathological mixed-type reordering case - df = DataFrame(index=[0]) + df = DataFrame(index=[0], columns=Index([], dtype="str")) df["A"] = 1.0 df["B"] = "foo" df["C"] = 2.0 From 6631202e8a499a943ed1cbb47033a403725b090b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20C=C3=A1rdenas?= <78029302+miguelcsx@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:00:28 -0500 Subject: [PATCH 018/266] BUG: fix #59950 handle duplicate column names in dataframe queries (#59971) fix: #59950 handle duplicate column names in dataframe queries - Fixed an issue where `Dataframe.query()` would throw an unexpected error - The error was caused by `self.dtypes[k]` - Adjusted the behavior to match the behavior prior to pandas version - Added tests to ensure that `Dataframe.query()` works as expected Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/generic.py | 4 ++-- pandas/tests/frame/test_query_eval.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 20efac7e2edb0..9f90181c50909 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -771,6 +771,7 @@ Other - Bug in :meth:`DataFrame.apply` where passing ``engine="numba"`` ignored ``args`` passed to the applied function (:issue:`58712`) - Bug in :meth:`DataFrame.eval` and :meth:`DataFrame.query` which caused an exception when using NumPy attributes via ``@`` notation, e.g., ``df.eval("@np.floor(a)")``. (:issue:`58041`) - Bug in :meth:`DataFrame.eval` and :meth:`DataFrame.query` which did not allow to use ``tan`` function. (:issue:`55091`) +- Bug in :meth:`DataFrame.query` where using duplicate column names led to a ``TypeError``. (:issue:`59950`) - Bug in :meth:`DataFrame.query` which raised an exception or produced incorrect results when expressions contained backtick-quoted column names containing the hash character ``#``, backticks, or characters that fall outside the ASCII range (U+0001..U+007F). (:issue:`59285`) (:issue:`49633`) - Bug in :meth:`DataFrame.shift` where passing a ``freq`` on a DataFrame with no columns did not shift the index correctly. (:issue:`60102`) - Bug in :meth:`DataFrame.sort_index` when passing ``axis="columns"`` and ``ignore_index=True`` and ``ascending=False`` not returning a :class:`RangeIndex` columns (:issue:`57293`) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index bbd627d4f0d73..a3a6430b51b3b 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -603,9 +603,9 @@ def _get_cleaned_column_resolvers(self) -> dict[Hashable, Series]: dtypes = self.dtypes return { clean_column_name(k): Series( - v, copy=False, index=self.index, name=k, dtype=dtypes[k] + v, copy=False, index=self.index, name=k, dtype=dtype ).__finalize__(self) - for k, v in zip(self.columns, self._iter_column_arrays()) + for k, v, dtype in zip(self.columns, self._iter_column_arrays(), dtypes) if not isinstance(k, int) } diff --git a/pandas/tests/frame/test_query_eval.py b/pandas/tests/frame/test_query_eval.py index a574989860957..ca572b1026526 100644 --- a/pandas/tests/frame/test_query_eval.py +++ b/pandas/tests/frame/test_query_eval.py @@ -159,6 +159,25 @@ def test_query_empty_string(self): with pytest.raises(ValueError, match=msg): df.query("") + def test_query_duplicate_column_name(self, engine, parser): + df = DataFrame( + { + "A": range(3), + "B": range(3), + "C": range(3) + } + ).rename(columns={"B": "A"}) + + res = df.query('C == 1', engine=engine, parser=parser) + + expect = DataFrame( + [[1, 1, 1]], + columns=["A", "A", "C"], + index=[1] + ) + + tm.assert_frame_equal(res, expect) + def test_eval_resolvers_as_list(self): # GH 14095 df = DataFrame( From eea95a304795d5c0e72494a645af417e0449bc9f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:26:40 -0800 Subject: [PATCH 019/266] TST: Skip flaky offset test case on WASM (#60186) * TST: Skip flaky offset test case on WASM * Check tzinfo exists * Check for zoneinfo directly * Undo original change * Try installing tzdata to fix * Revert "Try installing tzdata to fix" This reverts commit 6698cd5de18a9a4cf03dc1ae86ffe8a10461eee5. * Revert "Undo original change" This reverts commit 5bc727258e669050cb0b30e2d652fbd85e86e8da. --- .../tseries/offsets/test_offsets_properties.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tests/tseries/offsets/test_offsets_properties.py b/pandas/tests/tseries/offsets/test_offsets_properties.py index 943434e515828..809d8f87b2c02 100644 --- a/pandas/tests/tseries/offsets/test_offsets_properties.py +++ b/pandas/tests/tseries/offsets/test_offsets_properties.py @@ -8,12 +8,16 @@ tests, or when trying to pin down the bugs exposed by the tests below. """ +import zoneinfo + from hypothesis import ( assume, given, ) import pytest +from pandas.compat import WASM + import pandas as pd from pandas._testing._hypothesis import ( DATETIME_JAN_1_1900_OPTIONAL_TZ, @@ -28,6 +32,15 @@ @given(DATETIME_JAN_1_1900_OPTIONAL_TZ, YQM_OFFSET) def test_on_offset_implementations(dt, offset): assume(not offset.normalize) + # This case is flaky in CI 2024-11-04 + assume( + not ( + WASM + and isinstance(dt.tzinfo, zoneinfo.ZoneInfo) + and dt.tzinfo.key == "Indian/Cocos" + and isinstance(offset, pd.offsets.MonthBegin) + ) + ) # check that the class-specific implementations of is_on_offset match # the general case definition: # (dt + offset) - offset == dt From e49ab80bbd28d5ab03cb796dce96b44f037a0ccf Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:50:38 -0800 Subject: [PATCH 020/266] STY: Fix lint error in test_where.py (#60206) --- pandas/tests/frame/indexing/test_where.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py index d3040052ea696..f399f71a9ce88 100644 --- a/pandas/tests/frame/indexing/test_where.py +++ b/pandas/tests/frame/indexing/test_where.py @@ -4,8 +4,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.common import is_scalar import pandas as pd From 3bcdab2e8e060f92c251b4c88e1c01f1638599be Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Wed, 6 Nov 2024 13:34:27 -0500 Subject: [PATCH 021/266] add warning about setting max_rows/max_columns to 'None' (#60216) --- pandas/core/config_init.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index e4eefb570fd95..20fe8cbab1c9f 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -100,7 +100,10 @@ def use_numba_cb(key: str) -> None: : int If max_rows is exceeded, switch to truncate view. Depending on `large_repr`, objects are either centrally truncated or printed as - a summary view. 'None' value means unlimited. + a summary view. + + 'None' value means unlimited. Beware that printing a large number of rows + could cause your rendering environment (the browser, etc.) to crash. In case python/IPython is running in a terminal and `large_repr` equals 'truncate' this can be set to 0 and pandas will auto-detect @@ -121,7 +124,11 @@ def use_numba_cb(key: str) -> None: : int If max_cols is exceeded, switch to truncate view. Depending on `large_repr`, objects are either centrally truncated or printed as - a summary view. 'None' value means unlimited. + a summary view. + + 'None' value means unlimited. Beware that printing a large number of + columns could cause your rendering environment (the browser, etc.) to + crash. In case python/IPython is running in a terminal and `large_repr` equals 'truncate' this can be set to 0 or None and pandas will auto-detect From 5929ae9f1e1d926b848242d61107075f2a5a363b Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 6 Nov 2024 21:04:00 +0100 Subject: [PATCH 022/266] BUG (string dtype): fix escaping of newline/tab characters in the repr (#60215) * BUG (string dtype): fix escaping of newline/tab characters in the repr * parametrize existing test for all string dtypes * remove xfail --- pandas/core/arrays/string_.py | 11 +++++++++++ pandas/tests/frame/test_repr.py | 3 --- pandas/tests/series/test_formats.py | 14 +++++++------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index f54a5260bd699..2954edd93e343 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -1,5 +1,6 @@ from __future__ import annotations +from functools import partial import operator from typing import ( TYPE_CHECKING, @@ -64,6 +65,8 @@ from pandas.core.indexers import check_array_indexer from pandas.core.missing import isna +from pandas.io.formats import printing + if TYPE_CHECKING: import pyarrow @@ -391,6 +394,14 @@ def _from_scalars(cls, scalars, dtype: DtypeObj) -> Self: raise ValueError return cls._from_sequence(scalars, dtype=dtype) + def _formatter(self, boxed: bool = False): + formatter = partial( + printing.pprint_thing, + escape_chars=("\t", "\r", "\n"), + quote_strings=not boxed, + ) + return formatter + def _str_map( self, f, diff --git a/pandas/tests/frame/test_repr.py b/pandas/tests/frame/test_repr.py index 10cc86385af1b..73628424725e5 100644 --- a/pandas/tests/frame/test_repr.py +++ b/pandas/tests/frame/test_repr.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( NA, Categorical, @@ -176,7 +174,6 @@ def test_repr_mixed_big(self): repr(biggie) - @pytest.mark.xfail(using_string_dtype(), reason="/r in") def test_repr(self): # columns but no index no_index = DataFrame(columns=[0, 1, 3]) diff --git a/pandas/tests/series/test_formats.py b/pandas/tests/series/test_formats.py index ab083d5c58b35..eb81840f6f8f9 100644 --- a/pandas/tests/series/test_formats.py +++ b/pandas/tests/series/test_formats.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( Categorical, @@ -143,11 +141,13 @@ def test_tidy_repr_name_0(self, arg): rep_str = repr(ser) assert "Name: 0" in rep_str - @pytest.mark.xfail( - using_string_dtype(), reason="TODO(infer_string): investigate failure" - ) - def test_newline(self): - ser = Series(["a\n\r\tb"], name="a\n\r\td", index=["a\n\r\tf"]) + def test_newline(self, any_string_dtype): + ser = Series( + ["a\n\r\tb"], + name="a\n\r\td", + index=Index(["a\n\r\tf"], dtype=any_string_dtype), + dtype=any_string_dtype, + ) assert "\t" not in repr(ser) assert "\r" not in repr(ser) assert "a\n" not in repr(ser) From 4b04a2f0043ad04b5546750a8947dfeef68cdb75 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 6 Nov 2024 21:05:43 +0100 Subject: [PATCH 023/266] TST (string dtype): avoid explicit object dtype Index in fixture data (#60217) * TST (string dtype): avoid explicit object dtype Index in fixture data * test updates --- pandas/_testing/__init__.py | 2 ++ pandas/conftest.py | 10 +++++----- pandas/tests/frame/test_reductions.py | 1 - pandas/tests/series/indexing/test_setitem.py | 1 - pandas/tests/series/methods/test_reindex.py | 2 +- pandas/tests/series/methods/test_to_csv.py | 3 --- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index 0be01da1816a2..0a110d69c7a70 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -501,6 +501,8 @@ def shares_memory(left, right) -> bool: if isinstance(left, MultiIndex): return shares_memory(left._codes, right) if isinstance(left, (Index, Series)): + if isinstance(right, (Index, Series)): + return shares_memory(left._values, right._values) return shares_memory(left._values, right) if isinstance(left, NDArrayBackedExtensionArray): diff --git a/pandas/conftest.py b/pandas/conftest.py index 7ad322d050c0f..106518678df6a 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -600,7 +600,7 @@ def multiindex_year_month_day_dataframe_random_data(): """ tdf = DataFrame( np.random.default_rng(2).standard_normal((100, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=100, freq="B"), ) ymd = tdf.groupby([lambda x: x.year, lambda x: x.month, lambda x: x.day]).sum() @@ -787,7 +787,7 @@ def string_series() -> Series: """ return Series( np.arange(30, dtype=np.float64) * 1.1, - index=Index([f"i_{i}" for i in range(30)], dtype=object), + index=Index([f"i_{i}" for i in range(30)]), name="series", ) @@ -798,7 +798,7 @@ def object_series() -> Series: Fixture for Series of dtype object with Index of unique strings """ data = [f"foo_{i}" for i in range(30)] - index = Index([f"bar_{i}" for i in range(30)], dtype=object) + index = Index([f"bar_{i}" for i in range(30)]) return Series(data, index=index, name="objects", dtype=object) @@ -890,8 +890,8 @@ def int_frame() -> DataFrame: """ return DataFrame( np.ones((30, 4), dtype=np.int64), - index=Index([f"foo_{i}" for i in range(30)], dtype=object), - columns=Index(list("ABCD"), dtype=object), + index=Index([f"foo_{i}" for i in range(30)]), + columns=Index(list("ABCD")), ) diff --git a/pandas/tests/frame/test_reductions.py b/pandas/tests/frame/test_reductions.py index 05bb603f5c462..30d02f9b5463d 100644 --- a/pandas/tests/frame/test_reductions.py +++ b/pandas/tests/frame/test_reductions.py @@ -1047,7 +1047,6 @@ def test_sum_bools(self): # ---------------------------------------------------------------------- # Index of max / min - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize("axis", [0, 1]) def test_idxmin(self, float_frame, int_frame, skipna, axis): frame = float_frame diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index 789e3ac752097..d3246f43e991b 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -545,7 +545,6 @@ def test_setitem_with_expansion_type_promotion(self): expected = Series([Timestamp("2016-01-01"), 3.0, "foo"], index=["a", "b", "c"]) tm.assert_series_equal(ser, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_not_contained(self, string_series): # set item that's not contained ser = string_series.copy() diff --git a/pandas/tests/series/methods/test_reindex.py b/pandas/tests/series/methods/test_reindex.py index 068446a5e216b..442d73cadfe47 100644 --- a/pandas/tests/series/methods/test_reindex.py +++ b/pandas/tests/series/methods/test_reindex.py @@ -23,7 +23,7 @@ def test_reindex(datetime_series, string_series): identity = string_series.reindex(string_series.index) - assert np.may_share_memory(string_series.index, identity.index) + assert tm.shares_memory(string_series.index, identity.index) assert identity.index.is_(string_series.index) assert identity.index.identical(string_series.index) diff --git a/pandas/tests/series/methods/test_to_csv.py b/pandas/tests/series/methods/test_to_csv.py index 6eb7c74d2eca0..3e3eb36112680 100644 --- a/pandas/tests/series/methods/test_to_csv.py +++ b/pandas/tests/series/methods/test_to_csv.py @@ -4,8 +4,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import Series import pandas._testing as tm @@ -26,7 +24,6 @@ def read_csv(self, path, **kwargs): return out - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_from_csv(self, datetime_series, string_series, temp_file): # freq doesn't round-trip datetime_series.index = datetime_series.index._with_freq(None) From c15d8236c3bf017971972f5c17d41b027250e750 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 6 Nov 2024 23:06:20 +0100 Subject: [PATCH 024/266] ENH (string dtype): accept string_view in addition to string/large_string for ArrowStringArray input (#60222) --- pandas/core/arrays/string_arrow.py | 7 +++++++ pandas/tests/arrays/string_/test_string_arrow.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pandas/core/arrays/string_arrow.py b/pandas/core/arrays/string_arrow.py index cde39c7f4dc6a..75e36feea2628 100644 --- a/pandas/core/arrays/string_arrow.py +++ b/pandas/core/arrays/string_arrow.py @@ -17,6 +17,7 @@ from pandas.compat import ( pa_version_under10p1, pa_version_under13p0, + pa_version_under16p0, ) from pandas.util._exceptions import find_stack_level @@ -71,6 +72,10 @@ def _chk_pyarrow_available() -> None: raise ImportError(msg) +def _is_string_view(typ): + return not pa_version_under16p0 and pa.types.is_string_view(typ) + + # TODO: Inherit directly from BaseStringArrayMethods. Currently we inherit from # ObjectStringArrayMixin because we want to have the object-dtype based methods as # fallback for the ones that pyarrow doesn't yet support @@ -128,11 +133,13 @@ def __init__(self, values) -> None: _chk_pyarrow_available() if isinstance(values, (pa.Array, pa.ChunkedArray)) and ( pa.types.is_string(values.type) + or _is_string_view(values.type) or ( pa.types.is_dictionary(values.type) and ( pa.types.is_string(values.type.value_type) or pa.types.is_large_string(values.type.value_type) + or _is_string_view(values.type.value_type) ) ) ): diff --git a/pandas/tests/arrays/string_/test_string_arrow.py b/pandas/tests/arrays/string_/test_string_arrow.py index d4363171788d4..e6103da5021bb 100644 --- a/pandas/tests/arrays/string_/test_string_arrow.py +++ b/pandas/tests/arrays/string_/test_string_arrow.py @@ -99,6 +99,20 @@ def test_constructor_valid_string_type_value_dictionary(string_type, chunked): assert pa.types.is_large_string(arr._pa_array.type) +@pytest.mark.parametrize("chunked", [True, False]) +def test_constructor_valid_string_view(chunked): + # requires pyarrow>=18 for casting string_view to string + pa = pytest.importorskip("pyarrow", minversion="18") + + arr = pa.array(["1", "2", "3"], pa.string_view()) + if chunked: + arr = pa.chunked_array(arr) + + arr = ArrowStringArray(arr) + # dictionary type get converted to dense large string array + assert pa.types.is_large_string(arr._pa_array.type) + + def test_constructor_from_list(): # GH#27673 pytest.importorskip("pyarrow") From 909c6da4d3627f95b02d6cc3f482cefbf166e24f Mon Sep 17 00:00:00 2001 From: Jason Mok <106209849+jasonmokk@users.noreply.github.com> Date: Thu, 7 Nov 2024 01:57:32 -0600 Subject: [PATCH 025/266] TST: Add test for `feather` I/O with historical out-of-bounds `datetime` values (#60209) Co-authored-by: Jason Mok Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/tests/io/test_feather.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 9721d045b7b91..8ae2033faab4f 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -1,5 +1,6 @@ """test feather-format compat""" +from datetime import datetime import zoneinfo import numpy as np @@ -247,3 +248,15 @@ def test_string_inference(self, tmp_path): data={"a": ["x", "y"]}, dtype=pd.StringDtype(na_value=np.nan) ) tm.assert_frame_equal(result, expected) + + def test_out_of_bounds_datetime_to_feather(self): + # GH#47832 + df = pd.DataFrame( + { + "date": [ + datetime.fromisoformat("1654-01-01"), + datetime.fromisoformat("1920-01-01"), + ], + } + ) + self.check_round_trip(df) From 7fe140e05349c1251e9e9595da982e63c7cf2154 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 7 Nov 2024 09:57:48 +0100 Subject: [PATCH 026/266] TST: add extra test case for np.array(obj, copy=False) read-only behaviour (#60191) --- pandas/core/generic.py | 6 +++++ pandas/tests/copy_view/test_array.py | 37 ++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index a3a6430b51b3b..35014674565ff 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -2014,6 +2014,12 @@ def empty(self) -> bool: def __array__( self, dtype: npt.DTypeLike | None = None, copy: bool | None = None ) -> np.ndarray: + if copy is False and not self._mgr.is_single_block and not self.empty: + # check this manually, otherwise ._values will already return a copy + # and np.array(values, copy=False) will not raise an error + raise ValueError( + "Unable to avoid copy while creating an array as requested." + ) values = self._values if copy is None: # Note: branch avoids `copy=None` for NumPy 1.x support diff --git a/pandas/tests/copy_view/test_array.py b/pandas/tests/copy_view/test_array.py index bb238d08bd9bd..2b3ef9201d918 100644 --- a/pandas/tests/copy_view/test_array.py +++ b/pandas/tests/copy_view/test_array.py @@ -1,6 +1,8 @@ import numpy as np import pytest +from pandas.compat.numpy import np_version_gt2 + from pandas import ( DataFrame, Series, @@ -15,8 +17,12 @@ @pytest.mark.parametrize( "method", - [lambda ser: ser.values, lambda ser: np.asarray(ser)], - ids=["values", "asarray"], + [ + lambda ser: ser.values, + lambda ser: np.asarray(ser), + lambda ser: np.array(ser, copy=False), + ], + ids=["values", "asarray", "array"], ) def test_series_values(method): ser = Series([1, 2, 3], name="name") @@ -40,8 +46,12 @@ def test_series_values(method): @pytest.mark.parametrize( "method", - [lambda df: df.values, lambda df: np.asarray(df)], - ids=["values", "asarray"], + [ + lambda df: df.values, + lambda df: np.asarray(df), + lambda ser: np.array(ser, copy=False), + ], + ids=["values", "asarray", "array"], ) def test_dataframe_values(method): df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) @@ -82,7 +92,7 @@ def test_series_to_numpy(): ser.iloc[0] = 0 assert ser.values[0] == 0 - # specify copy=False gives a writeable array + # specify copy=True gives a writeable array ser = Series([1, 2, 3], name="name") arr = ser.to_numpy(copy=True) assert not np.shares_memory(arr, get_array(ser, "name")) @@ -130,6 +140,23 @@ def test_dataframe_multiple_numpy_dtypes(): assert not np.shares_memory(arr, get_array(df, "a")) assert arr.flags.writeable is True + if np_version_gt2: + # copy=False semantics are only supported in NumPy>=2. + + with pytest.raises(ValueError, match="Unable to avoid copy while creating"): + arr = np.array(df, copy=False) + + arr = np.array(df, copy=True) + assert arr.flags.writeable is True + + +def test_dataframe_single_block_copy_true(): + # the copy=False/None cases are tested above in test_dataframe_values + df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + arr = np.array(df, copy=True) + assert not np.shares_memory(arr, get_array(df, "a")) + assert arr.flags.writeable is True + def test_values_is_ea(): df = DataFrame({"a": date_range("2012-01-01", periods=3)}) From 0937c95777d44462d67fd5b299d4563984e78332 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 7 Nov 2024 16:03:01 +0100 Subject: [PATCH 027/266] BUG (string dtype): fix qualifier in memory usage info (#60221) --- pandas/core/indexes/base.py | 4 ++- pandas/core/indexes/multi.py | 9 ++++-- pandas/tests/frame/methods/test_info.py | 36 +++++++++++++++--------- pandas/tests/series/methods/test_info.py | 22 +++++++++++---- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index cf3d1e6a2ee2d..d6035c82aaaf8 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -5139,7 +5139,9 @@ def _is_memory_usage_qualified(self) -> bool: """ Return a boolean if we need a qualified .info display. """ - return is_object_dtype(self.dtype) + return is_object_dtype(self.dtype) or ( + is_string_dtype(self.dtype) and self.dtype.storage == "python" # type: ignore[union-attr] + ) def __contains__(self, key: Any) -> bool: """ diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index e6ce00cb714a4..d1c99cb864e57 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -66,6 +66,7 @@ is_list_like, is_object_dtype, is_scalar, + is_string_dtype, pandas_dtype, ) from pandas.core.dtypes.dtypes import ( @@ -1425,10 +1426,12 @@ def dtype(self) -> np.dtype: def _is_memory_usage_qualified(self) -> bool: """return a boolean if we need a qualified .info display""" - def f(level) -> bool: - return "mixed" in level or "string" in level or "unicode" in level + def f(dtype) -> bool: + return is_object_dtype(dtype) or ( + is_string_dtype(dtype) and dtype.storage == "python" + ) - return any(f(level.inferred_type) for level in self.levels) + return any(f(level.dtype) for level in self.levels) # Cannot determine type of "memory_usage" @doc(Index.memory_usage) # type: ignore[has-type] diff --git a/pandas/tests/frame/methods/test_info.py b/pandas/tests/frame/methods/test_info.py index aad43b7a77ac7..74e4383950174 100644 --- a/pandas/tests/frame/methods/test_info.py +++ b/pandas/tests/frame/methods/test_info.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import ( HAS_PYARROW, IS64, @@ -436,18 +434,25 @@ def test_usage_via_getsizeof(): assert abs(diff) < 100 -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") -def test_info_memory_usage_qualified(): +def test_info_memory_usage_qualified(using_infer_string): buf = StringIO() df = DataFrame(1, columns=list("ab"), index=[1, 2, 3]) df.info(buf=buf) assert "+" not in buf.getvalue() buf = StringIO() - df = DataFrame(1, columns=list("ab"), index=list("ABC")) + df = DataFrame(1, columns=list("ab"), index=Index(list("ABC"), dtype=object)) df.info(buf=buf) assert "+" in buf.getvalue() + buf = StringIO() + df = DataFrame(1, columns=list("ab"), index=Index(list("ABC"), dtype="str")) + df.info(buf=buf) + if using_infer_string and HAS_PYARROW: + assert "+" not in buf.getvalue() + else: + assert "+" in buf.getvalue() + buf = StringIO() df = DataFrame( 1, columns=list("ab"), index=MultiIndex.from_product([range(3), range(3)]) @@ -460,7 +465,10 @@ def test_info_memory_usage_qualified(): 1, columns=list("ab"), index=MultiIndex.from_product([range(3), ["foo", "bar"]]) ) df.info(buf=buf) - assert "+" in buf.getvalue() + if using_infer_string and HAS_PYARROW: + assert "+" not in buf.getvalue() + else: + assert "+" in buf.getvalue() def test_info_memory_usage_bug_on_multiindex(): @@ -497,16 +505,15 @@ def test_info_categorical(): df.info(buf=buf) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.xfail(not IS64, reason="GH 36579: fail on 32-bit system") -def test_info_int_columns(): +def test_info_int_columns(using_infer_string): # GH#37245 df = DataFrame({1: [1, 2], 2: [2, 3]}, index=["A", "B"]) buf = StringIO() df.info(show_counts=True, buf=buf) result = buf.getvalue() expected = textwrap.dedent( - """\ + f"""\ Index: 2 entries, A to B Data columns (total 2 columns): @@ -515,19 +522,22 @@ def test_info_int_columns(): 0 1 2 non-null int64 1 2 2 non-null int64 dtypes: int64(2) - memory usage: 48.0+ bytes + memory usage: {'50.0' if using_infer_string and HAS_PYARROW else '48.0+'} bytes """ ) assert result == expected -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") -def test_memory_usage_empty_no_warning(): +def test_memory_usage_empty_no_warning(using_infer_string): # GH#50066 df = DataFrame(index=["a", "b"]) with tm.assert_produces_warning(None): result = df.memory_usage() - expected = Series(16 if IS64 else 8, index=["Index"]) + if using_infer_string and HAS_PYARROW: + value = 18 + else: + value = 16 if IS64 else 8 + expected = Series(value, index=["Index"]) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/methods/test_info.py b/pandas/tests/series/methods/test_info.py index 097976b0a7ac0..e2831fb80b7a0 100644 --- a/pandas/tests/series/methods/test_info.py +++ b/pandas/tests/series/methods/test_info.py @@ -7,10 +7,14 @@ from pandas._config import using_string_dtype -from pandas.compat import PYPY +from pandas.compat import ( + HAS_PYARROW, + PYPY, +) from pandas import ( CategoricalIndex, + Index, MultiIndex, Series, date_range, @@ -41,7 +45,9 @@ def test_info_categorical(): @pytest.mark.parametrize("verbose", [True, False]) -def test_info_series(lexsorted_two_level_string_multiindex, verbose): +def test_info_series( + lexsorted_two_level_string_multiindex, verbose, using_infer_string +): index = lexsorted_two_level_string_multiindex ser = Series(range(len(index)), index=index, name="sth") buf = StringIO() @@ -63,10 +69,11 @@ def test_info_series(lexsorted_two_level_string_multiindex, verbose): 10 non-null int64 """ ) + qualifier = "" if using_infer_string and HAS_PYARROW else "+" expected += textwrap.dedent( f"""\ dtypes: int64(1) - memory usage: {ser.memory_usage()}.0+ bytes + memory usage: {ser.memory_usage()}.0{qualifier} bytes """ ) assert result == expected @@ -142,14 +149,17 @@ def test_info_memory_usage_deep_pypy(): assert s_object.memory_usage(deep=True) == s_object.memory_usage() -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "index, plus", [ ([1, 2, 3], False), - (list("ABC"), True), + (Index(list("ABC"), dtype="str"), not (using_string_dtype() and HAS_PYARROW)), + (Index(list("ABC"), dtype=object), True), (MultiIndex.from_product([range(3), range(3)]), False), - (MultiIndex.from_product([range(3), ["foo", "bar"]]), True), + ( + MultiIndex.from_product([range(3), ["foo", "bar"]]), + not (using_string_dtype() and HAS_PYARROW), + ), ], ) def test_info_memory_usage_qualified(index, plus): From 692ea6f9d4b05187a05f0811d3241211855d6efb Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 7 Nov 2024 16:04:20 +0100 Subject: [PATCH 028/266] ERR (string dtype): harmonize setitem error message for python and pyarrow storage (#60219) --- pandas/core/arrays/arrow/array.py | 4 ++-- pandas/core/arrays/masked.py | 2 +- pandas/core/arrays/string_.py | 12 +++++++++--- pandas/core/arrays/string_arrow.py | 15 ++++++++++++--- pandas/tests/arrays/masked/test_indexing.py | 2 +- pandas/tests/arrays/string_/test_string.py | 17 ++++------------- pandas/tests/frame/indexing/test_indexing.py | 2 +- pandas/tests/frame/indexing/test_where.py | 4 ++-- pandas/tests/indexing/test_loc.py | 2 +- pandas/tests/series/indexing/test_setitem.py | 4 ++-- 10 files changed, 35 insertions(+), 29 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index b6f1412066574..7db10b1cc4a80 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1145,7 +1145,7 @@ def fillna( try: fill_value = self._box_pa(value, pa_type=self._pa_array.type) except pa.ArrowTypeError as err: - msg = f"Invalid value '{value!s}' for dtype {self.dtype}" + msg = f"Invalid value '{value!s}' for dtype '{self.dtype}'" raise TypeError(msg) from err try: @@ -2136,7 +2136,7 @@ def _maybe_convert_setitem_value(self, value): try: value = self._box_pa(value, self._pa_array.type) except pa.ArrowTypeError as err: - msg = f"Invalid value '{value!s}' for dtype {self.dtype}" + msg = f"Invalid value '{value!s}' for dtype '{self.dtype}'" raise TypeError(msg) from err return value diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index 349d2ec4d3cc9..f3a0cc0dccdb3 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -286,7 +286,7 @@ def _validate_setitem_value(self, value): # Note: without the "str" here, the f-string rendering raises in # py38 builds. - raise TypeError(f"Invalid value '{value!s}' for dtype {self.dtype}") + raise TypeError(f"Invalid value '{value!s}' for dtype '{self.dtype}'") def __setitem__(self, key, value) -> None: key = check_array_indexer(self, key) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index 2954edd93e343..01619dab7ce45 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -652,7 +652,8 @@ def _validate_scalar(self, value): return self.dtype.na_value elif not isinstance(value, str): raise TypeError( - f"Cannot set non-string value '{value}' into a string array." + f"Invalid value '{value}' for dtype '{self.dtype}'. Value should be a " + f"string or missing value, got '{type(value).__name__}' instead." ) return value @@ -743,7 +744,9 @@ def __setitem__(self, key, value) -> None: value = self.dtype.na_value elif not isinstance(value, str): raise TypeError( - f"Cannot set non-string value '{value}' into a StringArray." + f"Invalid value '{value}' for dtype '{self.dtype}'. Value should " + f"be a string or missing value, got '{type(value).__name__}' " + "instead." ) else: if not is_array_like(value): @@ -753,7 +756,10 @@ def __setitem__(self, key, value) -> None: # compatible, compatibility with arrow backed strings value = np.asarray(value) if len(value) and not lib.is_string_array(value, skipna=True): - raise TypeError("Must provide strings.") + raise TypeError( + "Invalid value for dtype 'str'. Value should be a " + "string or missing value (or array of those)." + ) mask = isna(value) if mask.any(): diff --git a/pandas/core/arrays/string_arrow.py b/pandas/core/arrays/string_arrow.py index 75e36feea2628..27c1425d11ac6 100644 --- a/pandas/core/arrays/string_arrow.py +++ b/pandas/core/arrays/string_arrow.py @@ -223,7 +223,10 @@ def insert(self, loc: int, item) -> ArrowStringArray: if self.dtype.na_value is np.nan and item is np.nan: item = libmissing.NA if not isinstance(item, str) and item is not libmissing.NA: - raise TypeError("Scalar must be NA or str") + raise TypeError( + f"Invalid value '{item}' for dtype 'str'. Value should be a " + f"string or missing value, got '{type(item).__name__}' instead." + ) return super().insert(loc, item) def _convert_bool_result(self, values, na=lib.no_default, method_name=None): @@ -255,13 +258,19 @@ def _maybe_convert_setitem_value(self, value): if isna(value): value = None elif not isinstance(value, str): - raise TypeError("Scalar must be NA or str") + raise TypeError( + f"Invalid value '{value}' for dtype 'str'. Value should be a " + f"string or missing value, got '{type(value).__name__}' instead." + ) else: value = np.array(value, dtype=object, copy=True) value[isna(value)] = None for v in value: if not (v is None or isinstance(v, str)): - raise TypeError("Must provide strings") + raise TypeError( + "Invalid value for dtype 'str'. Value should be a " + "string or missing value (or array of those)." + ) return super()._maybe_convert_setitem_value(value) def isin(self, values: ArrayLike) -> npt.NDArray[np.bool_]: diff --git a/pandas/tests/arrays/masked/test_indexing.py b/pandas/tests/arrays/masked/test_indexing.py index 37f38a11cbeae..753d562c87ffa 100644 --- a/pandas/tests/arrays/masked/test_indexing.py +++ b/pandas/tests/arrays/masked/test_indexing.py @@ -8,7 +8,7 @@ class TestSetitemValidation: def _check_setitem_invalid(self, arr, invalid): - msg = f"Invalid value '{invalid!s}' for dtype {arr.dtype}" + msg = f"Invalid value '{invalid!s}' for dtype '{arr.dtype}'" msg = re.escape(msg) with pytest.raises(TypeError, match=msg): arr[0] = invalid diff --git a/pandas/tests/arrays/string_/test_string.py b/pandas/tests/arrays/string_/test_string.py index a18161f47039b..a32ac7db4656a 100644 --- a/pandas/tests/arrays/string_/test_string.py +++ b/pandas/tests/arrays/string_/test_string.py @@ -109,14 +109,11 @@ def test_none_to_nan(cls, dtype): def test_setitem_validates(cls, dtype): arr = cls._from_sequence(["a", "b"], dtype=dtype) - if dtype.storage == "python": - msg = "Cannot set non-string value '10' into a StringArray." - else: - msg = "Scalar must be NA or str" + msg = "Invalid value '10' for dtype 'str" with pytest.raises(TypeError, match=msg): arr[0] = 10 - msg = "Must provide strings" + msg = "Invalid value for dtype 'str" with pytest.raises(TypeError, match=msg): arr[:] = np.array([1, 2]) @@ -508,10 +505,7 @@ def test_fillna_args(dtype): expected = pd.array(["a", "b"], dtype=dtype) tm.assert_extension_array_equal(res, expected) - if dtype.storage == "pyarrow": - msg = "Invalid value '1' for dtype str" - else: - msg = "Cannot set non-string value '1' into a StringArray." + msg = "Invalid value '1' for dtype 'str" with pytest.raises(TypeError, match=msg): arr.fillna(value=1) @@ -727,10 +721,7 @@ def test_setitem_scalar_with_mask_validation(dtype): # for other non-string we should also raise an error ser = pd.Series(["a", "b", "c"], dtype=dtype) - if dtype.storage == "python": - msg = "Cannot set non-string value" - else: - msg = "Scalar must be NA or str" + msg = "Invalid value '1' for dtype 'str" with pytest.raises(TypeError, match=msg): ser[mask] = 1 diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index eb14f8bdbfb86..84c01e0be3b6f 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -1274,7 +1274,7 @@ def test_setting_mismatched_na_into_nullable_fails( r"timedelta64\[ns\] cannot be converted to (Floating|Integer)Dtype", r"datetime64\[ns\] cannot be converted to (Floating|Integer)Dtype", "'values' contains non-numeric NA", - r"Invalid value '.*' for dtype (U?Int|Float)\d{1,2}", + r"Invalid value '.*' for dtype '(U?Int|Float)\d{1,2}'", ] ) with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py index f399f71a9ce88..86b39ddd19ec1 100644 --- a/pandas/tests/frame/indexing/test_where.py +++ b/pandas/tests/frame/indexing/test_where.py @@ -931,7 +931,7 @@ def test_where_nullable_invalid_na(frame_or_series, any_numeric_ea_dtype): mask = np.array([True, True, False], ndmin=obj.ndim).T - msg = r"Invalid value '.*' for dtype (U?Int|Float)\d{1,2}" + msg = r"Invalid value '.*' for dtype '(U?Int|Float)\d{1,2}'" for null in tm.NP_NAT_OBJECTS + [pd.NaT]: # NaT is an NA value that we should *not* cast to pd.NA dtype @@ -1030,7 +1030,7 @@ def test_where_int_overflow(replacement, using_infer_string): df = DataFrame([[1.0, 2e25, "nine"], [np.nan, 0.1, None]]) if using_infer_string and replacement not in (None, "snake"): with pytest.raises( - TypeError, match="Cannot set non-string value|Scalar must be NA or str" + TypeError, match=f"Invalid value '{replacement}' for dtype 'str'" ): df.where(pd.notnull(df), replacement) return diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 36b08ee1df790..e0e9d4cfc5ccb 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -1230,7 +1230,7 @@ def test_loc_setitem_str_to_small_float_conversion_type(self, using_infer_string # assigning with loc/iloc attempts to set the values inplace, which # in this case is successful if using_infer_string: - with pytest.raises(TypeError, match="Must provide strings"): + with pytest.raises(TypeError, match="Invalid value"): result.loc[result.index, "A"] = [float(x) for x in col_data] else: result.loc[result.index, "A"] = [float(x) for x in col_data] diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index d3246f43e991b..ed5cb5a8d1237 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -864,7 +864,7 @@ def test_index_where(self, obj, key, expected, raises, val, using_infer_string): mask[key] = True if using_infer_string and obj.dtype == object: - with pytest.raises(TypeError, match="Scalar must"): + with pytest.raises(TypeError, match="Invalid value"): Index(obj).where(~mask, val) else: res = Index(obj).where(~mask, val) @@ -877,7 +877,7 @@ def test_index_putmask(self, obj, key, expected, raises, val, using_infer_string mask[key] = True if using_infer_string and obj.dtype == object: - with pytest.raises(TypeError, match="Scalar must"): + with pytest.raises(TypeError, match="Invalid value"): Index(obj).putmask(mask, val) else: res = Index(obj).putmask(mask, val) From feaa9638a53077218fd9df42dfaa1cd150574bb2 Mon Sep 17 00:00:00 2001 From: Michiel De Muynck Date: Thu, 7 Nov 2024 20:59:52 +0100 Subject: [PATCH 029/266] BUG: Fix to_excel storing decimals as strings instead of numbers (issue #49598) (#60230) * Fix issue 50174 * Add release notes * Use correct github issue number --- doc/source/whatsnew/v2.3.0.rst | 2 +- pandas/io/excel/_base.py | 4 ++++ pandas/tests/io/excel/test_writers.py | 31 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index 922cc0ead7fb0..d57d86f4a1476 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -133,7 +133,7 @@ MultiIndex I/O ^^^ -- +- :meth:`DataFrame.to_excel` was storing decimals as strings instead of numbers (:issue:`49598`) - Period diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py index ef52107c283e9..ced2ad91dba1e 100644 --- a/pandas/io/excel/_base.py +++ b/pandas/io/excel/_base.py @@ -8,6 +8,7 @@ Sequence, ) import datetime +from decimal import Decimal from functools import partial import os from textwrap import fill @@ -43,6 +44,7 @@ from pandas.core.dtypes.common import ( is_bool, + is_decimal, is_file_like, is_float, is_integer, @@ -1348,6 +1350,8 @@ def _value_with_fmt( val = float(val) elif is_bool(val): val = bool(val) + elif is_decimal(val): + val = Decimal(val) elif isinstance(val, datetime.datetime): fmt = self._datetime_format elif isinstance(val, datetime.date): diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index 44266ae9a62a5..81aa0be24bffc 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -3,6 +3,7 @@ datetime, timedelta, ) +from decimal import Decimal from functools import partial from io import BytesIO import os @@ -977,6 +978,36 @@ def test_to_excel_float_format(self, tmp_excel): ) tm.assert_frame_equal(result, expected) + def test_to_excel_datatypes_preserved(self, tmp_excel): + # Test that when writing and reading Excel with dtype=object, + # datatypes are preserved, except Decimals which should be + # stored as floats + + # see gh-49598 + df = DataFrame( + [ + [1.23, "1.23", Decimal("1.23")], + [4.56, "4.56", Decimal("4.56")], + ], + index=["A", "B"], + columns=["X", "Y", "Z"], + ) + df.to_excel(tmp_excel) + + with ExcelFile(tmp_excel) as reader: + result = pd.read_excel(reader, index_col=0, dtype=object) + + expected = DataFrame( + [ + [1.23, "1.23", 1.23], + [4.56, "4.56", 4.56], + ], + index=["A", "B"], + columns=["X", "Y", "Z"], + dtype=object, + ) + tm.assert_frame_equal(result, expected) + def test_to_excel_output_encoding(self, tmp_excel): # Avoid mixed inferred_type. df = DataFrame( From 04432f57145a4681c323f555e69fceb804b4c32e Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Fri, 8 Nov 2024 05:30:24 +0800 Subject: [PATCH 030/266] BUG: fix DataFrame(data=[None, 1], dtype='timedelta64[ns]') raising ValueError (#60081) --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/construction.py | 6 ++++++ pandas/core/dtypes/cast.py | 2 +- pandas/tests/frame/test_constructors.py | 8 ++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 9f90181c50909..0f78f07af4a89 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -613,6 +613,7 @@ Categorical Datetimelike ^^^^^^^^^^^^ - Bug in :attr:`is_year_start` where a DateTimeIndex constructed via a date_range with frequency 'MS' wouldn't have the correct year or quarter start attributes (:issue:`57377`) +- Bug in :class:`DataFrame` raising ``ValueError`` when ``dtype`` is ``timedelta64`` and ``data`` is a list containing ``None`` (:issue:`60064`) - Bug in :class:`Timestamp` constructor failing to raise when ``tz=None`` is explicitly specified in conjunction with timezone-aware ``tzinfo`` or data (:issue:`48688`) - Bug in :func:`date_range` where the last valid timestamp would sometimes not be produced (:issue:`56134`) - Bug in :func:`date_range` where using a negative frequency value would not include all points between the start and end values (:issue:`56147`) diff --git a/pandas/core/construction.py b/pandas/core/construction.py index 1e1292f8ef089..8df4f7e3e08f9 100644 --- a/pandas/core/construction.py +++ b/pandas/core/construction.py @@ -807,6 +807,12 @@ def _try_cast( ) elif dtype.kind in "mM": + if is_ndarray: + arr = cast(np.ndarray, arr) + if arr.ndim == 2 and arr.shape[1] == 1: + # GH#60081: DataFrame Constructor converts 1D data to array of + # shape (N, 1), but maybe_cast_to_datetime assumes 1D input + return maybe_cast_to_datetime(arr[:, 0], dtype).reshape(arr.shape) return maybe_cast_to_datetime(arr, dtype) # GH#15832: Check if we are requesting a numeric dtype and diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 6ba07b1761557..8850b75323d68 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -1205,7 +1205,7 @@ def maybe_infer_to_datetimelike( def maybe_cast_to_datetime( value: np.ndarray | list, dtype: np.dtype -) -> ExtensionArray | np.ndarray: +) -> DatetimeArray | TimedeltaArray | np.ndarray: """ try to cast the array/value to a datetimelike dtype, converting float nan to iNaT diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 0a924aa393be5..3d8213cb3d11a 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -2772,6 +2772,14 @@ def test_construction_datetime_resolution_inference(self, cons): res_dtype2 = tm.get_dtype(obj2) assert res_dtype2 == "M8[us, US/Pacific]", res_dtype2 + def test_construction_nan_value_timedelta64_dtype(self): + # GH#60064 + result = DataFrame([None, 1], dtype="timedelta64[ns]") + expected = DataFrame( + ["NaT", "0 days 00:00:00.000000001"], dtype="timedelta64[ns]" + ) + tm.assert_frame_equal(result, expected) + class TestDataFrameConstructorIndexInference: def test_frame_from_dict_of_series_overlapping_monthly_period_indexes(self): From b5e62ef2dc5dfcc23df5cf8b9dfd65733d9c8886 Mon Sep 17 00:00:00 2001 From: Thomas Dixon <90058210+tev-dixon@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:32:54 -0500 Subject: [PATCH 031/266] BUG: Fix #59429 stacked bar label position (#60211) --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/plotting/_matplotlib/core.py | 2 +- pandas/tests/plotting/frame/test_frame.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 0f78f07af4a89..13e1ddd153ccb 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -711,6 +711,7 @@ Period Plotting ^^^^^^^^ - Bug in :meth:`.DataFrameGroupBy.boxplot` failed when there were multiple groupings (:issue:`14701`) +- Bug in :meth:`DataFrame.plot.bar` with ``stacked=True`` where labels on stacked bars with zero-height segments were incorrectly positioned at the base instead of the label position of the previous segment (:issue:`59429`) - Bug in :meth:`DataFrame.plot.line` raising ``ValueError`` when set both color and a ``dict`` style (:issue:`59461`) - Bug in :meth:`DataFrame.plot` that causes a shift to the right when the frequency multiplier is greater than one. (:issue:`57587`) - Bug in :meth:`Series.plot` with ``kind="pie"`` with :class:`ArrowDtype` (:issue:`59192`) diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 505db4b807cfc..1035150302d2c 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -1960,7 +1960,7 @@ def _make_plot(self, fig: Figure) -> None: ) ax.set_title(label) elif self.stacked: - mask = y > 0 + mask = y >= 0 start = np.where(mask, pos_prior, neg_prior) + self._start_base w = self.bar_width / 2 rect = self._plot( diff --git a/pandas/tests/plotting/frame/test_frame.py b/pandas/tests/plotting/frame/test_frame.py index b39f953da1ee6..087280ed3e01d 100644 --- a/pandas/tests/plotting/frame/test_frame.py +++ b/pandas/tests/plotting/frame/test_frame.py @@ -774,6 +774,16 @@ def test_bar_nan_stacked(self): expected = [0.0, 0.0, 0.0, 10.0, 0.0, 20.0, 15.0, 10.0, 40.0] assert result == expected + def test_bar_stacked_label_position_with_zero_height(self): + # GH 59429 + df = DataFrame({"A": [3, 0, 1], "B": [0, 2, 4], "C": [5, 0, 2]}) + ax = df.plot.bar(stacked=True) + ax.bar_label(ax.containers[-1]) + expected = [8.0, 2.0, 7.0] + result = [text.xy[1] for text in ax.texts] + tm.assert_almost_equal(result, expected) + plt.close("all") + @pytest.mark.parametrize("idx", [Index, pd.CategoricalIndex]) def test_bar_categorical(self, idx): # GH 13019 From f9d2e50a8ca32a8612e9695dbb76d0e3227e8de5 Mon Sep 17 00:00:00 2001 From: YinonHorev <80318264+YinonHorev@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:49:03 +0100 Subject: [PATCH 032/266] ENH: set __module__ for pandas scalars (Timestamp/Timedelta/Period) (#57976) Co-authored-by: Yinon Horev Co-authored-by: Joris Van den Bossche --- pandas/_libs/tslibs/period.pyx | 2 ++ pandas/_libs/tslibs/timedeltas.pyx | 5 +++-- pandas/_libs/tslibs/timestamps.pyx | 5 +++-- pandas/tests/api/test_api.py | 3 +++ pandas/tests/tslibs/test_timezones.py | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index c563ab91c4142..d6d69a49c9539 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -114,6 +114,7 @@ from pandas._libs.tslibs.offsets import ( INVALID_FREQ_ERR_MSG, BDay, ) +from pandas.util._decorators import set_module cdef: enum: @@ -2830,6 +2831,7 @@ cdef class _Period(PeriodMixin): return period_format(self.ordinal, base, fmt) +@set_module("pandas") class Period(_Period): """ Represents a period of time. diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 15b629624bafc..e320aca04683c 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1,6 +1,7 @@ import collections import warnings +from pandas.util._decorators import set_module from pandas.util._exceptions import find_stack_level cimport cython @@ -1854,7 +1855,7 @@ cdef class _Timedelta(timedelta): # Python front end to C extension type _Timedelta # This serves as the box for timedelta64 - +@set_module("pandas") class Timedelta(_Timedelta): """ Represents a duration, the difference between two dates or times. @@ -1916,7 +1917,7 @@ class Timedelta(_Timedelta): -------- Here we initialize Timedelta object with both value and unit - >>> td = pd.Timedelta(1, "d") + >>> td = pd.Timedelta(1, "D") >>> td Timedelta('1 days 00:00:00') diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 34c84d396ad64..1ab34da7ab53f 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -50,6 +50,7 @@ import datetime as dt from pandas._libs.tslibs cimport ccalendar from pandas._libs.tslibs.base cimport ABCTimestamp +from pandas.util._decorators import set_module from pandas.util._exceptions import find_stack_level from pandas._libs.tslibs.conversion cimport ( @@ -1648,7 +1649,7 @@ cdef class _Timestamp(ABCTimestamp): # Python front end to C extension type _Timestamp # This serves as the box for datetime64 - +@set_module("pandas") class Timestamp(_Timestamp): """ Pandas replacement for python datetime.datetime object. @@ -2926,7 +2927,7 @@ timedelta}, default 'raise' -------- >>> ts = pd.Timestamp(1584226800, unit='s', tz='Europe/Stockholm') >>> ts.tz - + zoneinfo.ZoneInfo(key='Europe/Stockholm') """ return self.tzinfo diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index b23876d9280f7..84c6c4df89641 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -400,3 +400,6 @@ def test_util_in_top_level(self): def test_set_module(): assert pd.DataFrame.__module__ == "pandas" + assert pd.Period.__module__ == "pandas" + assert pd.Timestamp.__module__ == "pandas" + assert pd.Timedelta.__module__ == "pandas" diff --git a/pandas/tests/tslibs/test_timezones.py b/pandas/tests/tslibs/test_timezones.py index 8dd7060f21d59..60bbcf08ce8e7 100644 --- a/pandas/tests/tslibs/test_timezones.py +++ b/pandas/tests/tslibs/test_timezones.py @@ -144,7 +144,7 @@ def test_maybe_get_tz_invalid_types(): with pytest.raises(TypeError, match=""): timezones.maybe_get_tz(pytest) - msg = "" + msg = "" with pytest.raises(TypeError, match=msg): timezones.maybe_get_tz(Timestamp("2021-01-01", tz="UTC")) From 3f7bc81ae6839803ecc0da073fe83e9194759550 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 8 Nov 2024 02:04:59 +0100 Subject: [PATCH 033/266] TST (string dtype): resolve xfails in pandas/tests/series (#60233) * TST (string dtype): resolve xfails in pandas/tests/series * a few more * link TODO to issue * fix for non-future mode --- .../series/accessors/test_dt_accessor.py | 4 -- pandas/tests/series/indexing/test_indexing.py | 21 ++++++--- pandas/tests/series/indexing/test_setitem.py | 47 +++++++++++-------- pandas/tests/series/indexing/test_where.py | 17 +++---- pandas/tests/series/methods/test_replace.py | 29 +++++++----- pandas/tests/series/methods/test_unstack.py | 5 +- pandas/tests/series/test_logical_ops.py | 1 + 7 files changed, 68 insertions(+), 56 deletions(-) diff --git a/pandas/tests/series/accessors/test_dt_accessor.py b/pandas/tests/series/accessors/test_dt_accessor.py index 885adb3543b46..2c441a6ed91c1 100644 --- a/pandas/tests/series/accessors/test_dt_accessor.py +++ b/pandas/tests/series/accessors/test_dt_accessor.py @@ -10,8 +10,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs.tslibs.timezones import maybe_get_tz from pandas.core.dtypes.common import ( @@ -556,7 +554,6 @@ def test_strftime(self): ) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_strftime_dt64_days(self): ser = Series(date_range("20130101", periods=5)) ser.iloc[0] = pd.NaT @@ -571,7 +568,6 @@ def test_strftime_dt64_days(self): expected = Index( ["2015/03/01", "2015/03/02", "2015/03/03", "2015/03/04", "2015/03/05"], - dtype=np.object_, ) # dtype may be S10 or U10 depending on python version tm.assert_index_equal(result, expected) diff --git a/pandas/tests/series/indexing/test_indexing.py b/pandas/tests/series/indexing/test_indexing.py index 9f310d8c8ab5f..d3556b644c4bf 100644 --- a/pandas/tests/series/indexing/test_indexing.py +++ b/pandas/tests/series/indexing/test_indexing.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import IndexingError from pandas import ( @@ -251,18 +249,29 @@ def test_slice(string_series, object_series): tm.assert_series_equal(string_series, original) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_timedelta_assignment(): # GH 8209 s = Series([], dtype=object) s.loc["B"] = timedelta(1) - tm.assert_series_equal(s, Series(Timedelta("1 days"), index=["B"])) + expected = Series( + Timedelta("1 days"), dtype="timedelta64[ns]", index=Index(["B"], dtype=object) + ) + tm.assert_series_equal(s, expected) s = s.reindex(s.index.insert(0, "A")) - tm.assert_series_equal(s, Series([np.nan, Timedelta("1 days")], index=["A", "B"])) + expected = Series( + [np.nan, Timedelta("1 days")], + dtype="timedelta64[ns]", + index=Index(["A", "B"], dtype=object), + ) + tm.assert_series_equal(s, expected) s.loc["A"] = timedelta(1) - expected = Series(Timedelta("1 days"), index=["A", "B"]) + expected = Series( + Timedelta("1 days"), + dtype="timedelta64[ns]", + index=Index(["A", "B"], dtype=object), + ) tm.assert_series_equal(s, expected) diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index ed5cb5a8d1237..82c616132456b 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -9,12 +9,7 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import ( - HAS_PYARROW, - WASM, -) +from pandas.compat import WASM from pandas.compat.numpy import np_version_gte1p24 from pandas.errors import IndexingError @@ -32,6 +27,7 @@ NaT, Period, Series, + StringDtype, Timedelta, Timestamp, array, @@ -535,14 +531,16 @@ def test_append_timedelta_does_not_cast(self, td, using_infer_string, request): tm.assert_series_equal(ser, expected) assert isinstance(ser["td"], Timedelta) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_with_expansion_type_promotion(self): # GH#12599 ser = Series(dtype=object) ser["a"] = Timestamp("2016-01-01") ser["b"] = 3.0 ser["c"] = "foo" - expected = Series([Timestamp("2016-01-01"), 3.0, "foo"], index=["a", "b", "c"]) + expected = Series( + [Timestamp("2016-01-01"), 3.0, "foo"], + index=Index(["a", "b", "c"], dtype=object), + ) tm.assert_series_equal(ser, expected) def test_setitem_not_contained(self, string_series): @@ -826,11 +824,6 @@ def test_mask_key(self, obj, key, expected, raises, val, indexer_sli): else: indexer_sli(obj)[mask] = val - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, - reason="TODO(infer_string)", - strict=False, - ) def test_series_where(self, obj, key, expected, raises, val, is_inplace): mask = np.zeros(obj.shape, dtype=bool) mask[key] = True @@ -846,6 +839,11 @@ def test_series_where(self, obj, key, expected, raises, val, is_inplace): obj = obj.copy() arr = obj._values + if raises and obj.dtype == "string": + with pytest.raises(TypeError, match="Invalid value"): + obj.where(~mask, val) + return + res = obj.where(~mask, val) if val is NA and res.dtype == object: @@ -858,12 +856,11 @@ def test_series_where(self, obj, key, expected, raises, val, is_inplace): self._check_inplace(is_inplace, orig, arr, obj) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) - def test_index_where(self, obj, key, expected, raises, val, using_infer_string): + def test_index_where(self, obj, key, expected, raises, val): mask = np.zeros(obj.shape, dtype=bool) mask[key] = True - if using_infer_string and obj.dtype == object: + if raises and obj.dtype == "string": with pytest.raises(TypeError, match="Invalid value"): Index(obj).where(~mask, val) else: @@ -871,12 +868,11 @@ def test_index_where(self, obj, key, expected, raises, val, using_infer_string): expected_idx = Index(expected, dtype=expected.dtype) tm.assert_index_equal(res, expected_idx) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) - def test_index_putmask(self, obj, key, expected, raises, val, using_infer_string): + def test_index_putmask(self, obj, key, expected, raises, val): mask = np.zeros(obj.shape, dtype=bool) mask[key] = True - if using_infer_string and obj.dtype == object: + if raises and obj.dtype == "string": with pytest.raises(TypeError, match="Invalid value"): Index(obj).putmask(mask, val) else: @@ -1372,6 +1368,19 @@ def raises(self): return False +@pytest.mark.parametrize( + "val,exp_dtype,raises", + [ + (1, object, True), + ("e", StringDtype(na_value=np.nan), False), + ], +) +class TestCoercionString(CoercionTest): + @pytest.fixture + def obj(self): + return Series(["a", "b", "c", "d"], dtype=StringDtype(na_value=np.nan)) + + @pytest.mark.parametrize( "val,exp_dtype,raises", [ diff --git a/pandas/tests/series/indexing/test_where.py b/pandas/tests/series/indexing/test_where.py index 053c290999f2f..663ee8ad0ee38 100644 --- a/pandas/tests/series/indexing/test_where.py +++ b/pandas/tests/series/indexing/test_where.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.common import is_integer import pandas as pd @@ -231,7 +229,6 @@ def test_where_ndframe_align(): tm.assert_series_equal(out, expected) -@pytest.mark.xfail(using_string_dtype(), reason="can't set ints into string") def test_where_setitem_invalid(): # GH 2702 # make sure correct exceptions are raised on invalid list assignment @@ -241,7 +238,7 @@ def test_where_setitem_invalid(): "different length than the value" ) # slice - s = Series(list("abc")) + s = Series(list("abc"), dtype=object) with pytest.raises(ValueError, match=msg("slice")): s[0:3] = list(range(27)) @@ -251,18 +248,18 @@ def test_where_setitem_invalid(): tm.assert_series_equal(s.astype(np.int64), expected) # slice with step - s = Series(list("abcdef")) + s = Series(list("abcdef"), dtype=object) with pytest.raises(ValueError, match=msg("slice")): s[0:4:2] = list(range(27)) - s = Series(list("abcdef")) + s = Series(list("abcdef"), dtype=object) s[0:4:2] = list(range(2)) expected = Series([0, "b", 1, "d", "e", "f"]) tm.assert_series_equal(s, expected) # neg slices - s = Series(list("abcdef")) + s = Series(list("abcdef"), dtype=object) with pytest.raises(ValueError, match=msg("slice")): s[:-1] = list(range(27)) @@ -272,18 +269,18 @@ def test_where_setitem_invalid(): tm.assert_series_equal(s, expected) # list - s = Series(list("abc")) + s = Series(list("abc"), dtype=object) with pytest.raises(ValueError, match=msg("list-like")): s[[0, 1, 2]] = list(range(27)) - s = Series(list("abc")) + s = Series(list("abc"), dtype=object) with pytest.raises(ValueError, match=msg("list-like")): s[[0, 1, 2]] = list(range(2)) # scalar - s = Series(list("abc")) + s = Series(list("abc"), dtype=object) s[0] = list(range(10)) expected = Series([list(range(10)), "b", "c"]) tm.assert_series_equal(s, expected) diff --git a/pandas/tests/series/methods/test_replace.py b/pandas/tests/series/methods/test_replace.py index 611fcc114db6c..1ebef333f054a 100644 --- a/pandas/tests/series/methods/test_replace.py +++ b/pandas/tests/series/methods/test_replace.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd import pandas._testing as tm from pandas.core.arrays import IntervalArray @@ -628,15 +626,23 @@ def test_replace_nullable_numeric(self): with pytest.raises(TypeError, match="Invalid value"): ints.replace(1, 9.5) - @pytest.mark.xfail(using_string_dtype(), reason="can't fill 1 in string") @pytest.mark.parametrize("regex", [False, True]) def test_replace_regex_dtype_series(self, regex): # GH-48644 - series = pd.Series(["0"]) + series = pd.Series(["0"], dtype=object) expected = pd.Series([1], dtype=object) result = series.replace(to_replace="0", value=1, regex=regex) tm.assert_series_equal(result, expected) + @pytest.mark.parametrize("regex", [False, True]) + def test_replace_regex_dtype_series_string(self, regex, using_infer_string): + if not using_infer_string: + # then this is object dtype which is already tested above + return + series = pd.Series(["0"], dtype="str") + with pytest.raises(TypeError, match="Invalid value"): + series.replace(to_replace="0", value=1, regex=regex) + def test_replace_different_int_types(self, any_int_numpy_dtype): # GH#45311 labs = pd.Series([1, 1, 1, 0, 0, 2, 2, 2], dtype=any_int_numpy_dtype) @@ -656,21 +662,18 @@ def test_replace_value_none_dtype_numeric(self, val): expected = pd.Series([1, None], dtype=object) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_replace_change_dtype_series(self, using_infer_string): + def test_replace_change_dtype_series(self): # GH#25797 - df = pd.DataFrame.from_dict({"Test": ["0.5", True, "0.6"]}) - warn = FutureWarning if using_infer_string else None - with tm.assert_produces_warning(warn, match="Downcasting"): - df["Test"] = df["Test"].replace([True], [np.nan]) - expected = pd.DataFrame.from_dict({"Test": ["0.5", np.nan, "0.6"]}) + df = pd.DataFrame({"Test": ["0.5", True, "0.6"]}, dtype=object) + df["Test"] = df["Test"].replace([True], [np.nan]) + expected = pd.DataFrame({"Test": ["0.5", np.nan, "0.6"]}, dtype=object) tm.assert_frame_equal(df, expected) - df = pd.DataFrame.from_dict({"Test": ["0.5", None, "0.6"]}) + df = pd.DataFrame({"Test": ["0.5", None, "0.6"]}, dtype=object) df["Test"] = df["Test"].replace([None], [np.nan]) tm.assert_frame_equal(df, expected) - df = pd.DataFrame.from_dict({"Test": ["0.5", None, "0.6"]}) + df = pd.DataFrame({"Test": ["0.5", None, "0.6"]}, dtype=object) df["Test"] = df["Test"].fillna(np.nan) tm.assert_frame_equal(df, expected) diff --git a/pandas/tests/series/methods/test_unstack.py b/pandas/tests/series/methods/test_unstack.py index 8c4f0ff3eaea7..f61e20c43657d 100644 --- a/pandas/tests/series/methods/test_unstack.py +++ b/pandas/tests/series/methods/test_unstack.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -136,11 +134,10 @@ def test_unstack_mixed_type_name_in_multiindex( tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_unstack_multi_index_categorical_values(): df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) mi = df.stack().index.rename(["major", "minor"]) diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py index 8516018e8aa93..8f63819b09238 100644 --- a/pandas/tests/series/test_logical_ops.py +++ b/pandas/tests/series/test_logical_ops.py @@ -413,6 +413,7 @@ def test_logical_ops_label_based(self, using_infer_string): for e in [Series(["z"])]: if using_infer_string: # TODO(infer_string) should this behave differently? + # -> https://github.com/pandas-dev/pandas/issues/60234 with pytest.raises( TypeError, match="not supported for dtype|unsupported operand type" ): From 4cef979494b9838806f205ed575c09bbd4add7bf Mon Sep 17 00:00:00 2001 From: Amir <44722829+Amir-101@users.noreply.github.com> Date: Fri, 8 Nov 2024 08:56:56 +0100 Subject: [PATCH 034/266] ENH: set __module__ for Dtype and Index classes (#59909) Co-authored-by: Amir Co-authored-by: Joris Van den Bossche --- pandas/core/dtypes/dtypes.py | 7 +++++++ pandas/core/indexes/base.py | 2 ++ pandas/core/indexes/category.py | 2 ++ pandas/core/indexes/datetimes.py | 2 ++ pandas/core/indexes/interval.py | 2 ++ pandas/core/indexes/multi.py | 2 ++ pandas/core/indexes/period.py | 2 ++ pandas/core/indexes/range.py | 2 ++ pandas/core/indexes/timedeltas.py | 2 ++ pandas/tests/api/test_api.py | 13 +++++++++++++ 10 files changed, 36 insertions(+) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index bb6610c514375..004a1aab5436e 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -48,6 +48,7 @@ from pandas._libs.tslibs.offsets import BDay from pandas.compat import pa_version_under10p1 from pandas.errors import PerformanceWarning +from pandas.util._decorators import set_module from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.base import ( @@ -155,6 +156,7 @@ class CategoricalDtypeType(type): @register_extension_dtype +@set_module("pandas") class CategoricalDtype(PandasExtensionDtype, ExtensionDtype): """ Type for categorical data with the categories and orderedness. @@ -706,6 +708,7 @@ def index_class(self) -> type_t[CategoricalIndex]: @register_extension_dtype +@set_module("pandas") class DatetimeTZDtype(PandasExtensionDtype): """ An ExtensionDtype for timezone-aware datetime data. @@ -974,6 +977,7 @@ def index_class(self) -> type_t[DatetimeIndex]: @register_extension_dtype +@set_module("pandas") class PeriodDtype(PeriodDtypeBase, PandasExtensionDtype): """ An ExtensionDtype for Period data. @@ -1215,6 +1219,7 @@ def index_class(self) -> type_t[PeriodIndex]: @register_extension_dtype +@set_module("pandas") class IntervalDtype(PandasExtensionDtype): """ An ExtensionDtype for Interval data. @@ -1691,6 +1696,7 @@ def _get_common_dtype(self, dtypes: list[DtypeObj]) -> DtypeObj | None: @register_extension_dtype +@set_module("pandas") class SparseDtype(ExtensionDtype): """ Dtype for data stored in :class:`SparseArray`. @@ -2130,6 +2136,7 @@ def _get_common_dtype(self, dtypes: list[DtypeObj]) -> DtypeObj | None: @register_extension_dtype +@set_module("pandas") class ArrowDtype(StorageExtensionDtype): """ An ExtensionDtype for PyArrow data types. diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index d6035c82aaaf8..4a90b164c89cc 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -71,6 +71,7 @@ Appender, cache_readonly, doc, + set_module, ) from pandas.util._exceptions import ( find_stack_level, @@ -315,6 +316,7 @@ def _new_Index(cls, d): return cls.__new__(cls, **d) +@set_module("pandas") class Index(IndexOpsMixin, PandasObject): """ Immutable sequence used for indexing and alignment. diff --git a/pandas/core/indexes/category.py b/pandas/core/indexes/category.py index 312219eb7b91a..d20a84449fb85 100644 --- a/pandas/core/indexes/category.py +++ b/pandas/core/indexes/category.py @@ -13,6 +13,7 @@ from pandas.util._decorators import ( cache_readonly, doc, + set_module, ) from pandas.core.dtypes.common import is_scalar @@ -76,6 +77,7 @@ Categorical, wrap=True, ) +@set_module("pandas") class CategoricalIndex(NDArrayBackedExtensionIndex): """ Index based on an underlying :class:`Categorical`. diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 536f22d38468d..b3d9c3bc78a66 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -26,6 +26,7 @@ from pandas.util._decorators import ( cache_readonly, doc, + set_module, ) from pandas.core.dtypes.common import is_scalar @@ -126,6 +127,7 @@ def _new_DatetimeIndex(cls, d): + DatetimeArray._bool_ops, DatetimeArray, ) +@set_module("pandas") class DatetimeIndex(DatetimeTimedeltaMixin): """ Immutable ndarray-like of datetime64 data. diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 94717141b30b0..b0b9c5419e2ad 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -32,6 +32,7 @@ from pandas.util._decorators import ( Appender, cache_readonly, + set_module, ) from pandas.util._exceptions import rewrite_exception @@ -202,6 +203,7 @@ def _new_IntervalIndex(cls, d): IntervalArray, ) @inherit_names(["is_non_overlapping_monotonic", "closed"], IntervalArray, cache=True) +@set_module("pandas") class IntervalIndex(ExtensionIndex): _typ = "intervalindex" diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index d1c99cb864e57..36e68465a99d9 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -53,6 +53,7 @@ Appender, cache_readonly, doc, + set_module, ) from pandas.util._exceptions import find_stack_level @@ -195,6 +196,7 @@ def new_meth(self_or_cls, *args, **kwargs): return cast(F, new_meth) +@set_module("pandas") class MultiIndex(Index): """ A multi-level, or hierarchical, index object for pandas objects. diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 377406e24b1d3..0a7a0319bed3a 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -20,6 +20,7 @@ from pandas.util._decorators import ( cache_readonly, doc, + set_module, ) from pandas.core.dtypes.common import is_integer @@ -81,6 +82,7 @@ def _new_PeriodIndex(cls, **d): wrap=True, ) @inherit_names(["is_leap_year"], PeriodArray) +@set_module("pandas") class PeriodIndex(DatetimeIndexOpsMixin): """ Immutable ndarray holding ordinal values indicating regular periods in time. diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index dc96d1c11db74..7eeaab3b0443f 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -27,6 +27,7 @@ from pandas.util._decorators import ( cache_readonly, doc, + set_module, ) from pandas.core.dtypes.base import ExtensionDtype @@ -74,6 +75,7 @@ def min_fitting_element(start: int, step: int, lower_limit: int) -> int: return start + abs(step) * no_steps +@set_module("pandas") class RangeIndex(Index): """ Immutable Index implementing a monotonic integer range. diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 29039ffd0217e..6bbe86816d81f 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -13,6 +13,7 @@ Timedelta, to_offset, ) +from pandas.util._decorators import set_module from pandas.core.dtypes.common import ( is_scalar, @@ -50,6 +51,7 @@ ], TimedeltaArray, ) +@set_module("pandas") class TimedeltaIndex(DatetimeTimedeltaMixin): """ Immutable Index of timedelta64 data. diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 84c6c4df89641..842fa1a151267 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -400,6 +400,19 @@ def test_util_in_top_level(self): def test_set_module(): assert pd.DataFrame.__module__ == "pandas" + assert pd.CategoricalDtype.__module__ == "pandas" + assert pd.PeriodDtype.__module__ == "pandas" + assert pd.IntervalDtype.__module__ == "pandas" + assert pd.SparseDtype.__module__ == "pandas" + assert pd.ArrowDtype.__module__ == "pandas" + assert pd.Index.__module__ == "pandas" + assert pd.CategoricalIndex.__module__ == "pandas" + assert pd.DatetimeIndex.__module__ == "pandas" + assert pd.IntervalIndex.__module__ == "pandas" + assert pd.MultiIndex.__module__ == "pandas" + assert pd.PeriodIndex.__module__ == "pandas" + assert pd.RangeIndex.__module__ == "pandas" + assert pd.TimedeltaIndex.__module__ == "pandas" assert pd.Period.__module__ == "pandas" assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" From e5dd89d4d74d8e2a06256023717880788f2b10ed Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 8 Nov 2024 05:34:20 -0800 Subject: [PATCH 035/266] TST (string dtype): fix groupby xfails with using_infer_string + update error message (#59430) Co-authored-by: Joris Van den Bossche --- pandas/core/arrays/arrow/array.py | 14 +++++ pandas/core/arrays/base.py | 14 +++++ pandas/core/groupby/groupby.py | 4 +- pandas/tests/frame/test_stack_unstack.py | 4 +- pandas/tests/groupby/aggregate/test_cython.py | 4 +- pandas/tests/groupby/methods/test_quantile.py | 9 ++- pandas/tests/groupby/test_groupby.py | 56 ++++++++++++++----- pandas/tests/groupby/test_groupby_subclass.py | 2 +- pandas/tests/groupby/test_numeric_only.py | 20 +++++-- pandas/tests/groupby/test_raises.py | 54 ++++++++++++++++-- pandas/tests/resample/test_resample_api.py | 20 ++++++- pandas/tests/reshape/merge/test_join.py | 4 +- pandas/tests/reshape/test_pivot.py | 8 ++- 13 files changed, 170 insertions(+), 43 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 7db10b1cc4a80..fcc50c5b6b20f 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -2312,6 +2312,20 @@ def _groupby_op( **kwargs, ): if isinstance(self.dtype, StringDtype): + if how in [ + "prod", + "mean", + "median", + "cumsum", + "cumprod", + "std", + "sem", + "var", + "skew", + ]: + raise TypeError( + f"dtype '{self.dtype}' does not support operation '{how}'" + ) return super()._groupby_op( how=how, has_dropped_na=has_dropped_na, diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 5f2c2a7772f78..4835d808f2433 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2608,6 +2608,20 @@ def _groupby_op( # GH#43682 if isinstance(self.dtype, StringDtype): # StringArray + if op.how in [ + "prod", + "mean", + "median", + "cumsum", + "cumprod", + "std", + "sem", + "var", + "skew", + ]: + raise TypeError( + f"dtype '{self.dtype}' does not support operation '{how}'" + ) if op.how not in ["any", "all"]: # Fail early to avoid conversion to object op._get_cython_function(op.kind, op.how, np.dtype(object), False) diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 66db033596872..8f2e5d2ee09d4 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -4162,9 +4162,9 @@ def quantile( starts, ends = lib.generate_slices(splitter._slabels, splitter.ngroups) def pre_processor(vals: ArrayLike) -> tuple[np.ndarray, DtypeObj | None]: - if is_object_dtype(vals.dtype): + if isinstance(vals.dtype, StringDtype) or is_object_dtype(vals.dtype): raise TypeError( - "'quantile' cannot be performed against 'object' dtypes!" + f"dtype '{vals.dtype}' does not support operation 'quantile'" ) inference: DtypeObj | None = None diff --git a/pandas/tests/frame/test_stack_unstack.py b/pandas/tests/frame/test_stack_unstack.py index b4f02b6f81b6f..57c803c23b001 100644 --- a/pandas/tests/frame/test_stack_unstack.py +++ b/pandas/tests/frame/test_stack_unstack.py @@ -2113,7 +2113,7 @@ def test_unstack_period_frame(self): @pytest.mark.filterwarnings( "ignore:The previous implementation of stack is deprecated" ) - def test_stack_multiple_bug(self, future_stack): + def test_stack_multiple_bug(self, future_stack, using_infer_string): # bug when some uniques are not present in the data GH#3170 id_col = ([1] * 3) + ([2] * 3) name = (["a"] * 3) + (["b"] * 3) @@ -2125,6 +2125,8 @@ def test_stack_multiple_bug(self, future_stack): multi.columns.name = "Params" unst = multi.unstack("ID") msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): unst.resample("W-THU").mean() down = unst.resample("W-THU").mean(numeric_only=True) diff --git a/pandas/tests/groupby/aggregate/test_cython.py b/pandas/tests/groupby/aggregate/test_cython.py index d28eb227314c7..b937e7dcc8136 100644 --- a/pandas/tests/groupby/aggregate/test_cython.py +++ b/pandas/tests/groupby/aggregate/test_cython.py @@ -148,11 +148,11 @@ def test_cython_agg_return_dict(): def test_cython_fail_agg(): dr = bdate_range("1/1/2000", periods=50) - ts = Series(["A", "B", "C", "D", "E"] * 10, index=dr) + ts = Series(["A", "B", "C", "D", "E"] * 10, dtype=object, index=dr) grouped = ts.groupby(lambda x: x.month) summed = grouped.sum() - expected = grouped.agg(np.sum) + expected = grouped.agg(np.sum).astype(object) tm.assert_series_equal(summed, expected) diff --git a/pandas/tests/groupby/methods/test_quantile.py b/pandas/tests/groupby/methods/test_quantile.py index 0e31c0698cb1e..4a8ad65200caa 100644 --- a/pandas/tests/groupby/methods/test_quantile.py +++ b/pandas/tests/groupby/methods/test_quantile.py @@ -162,7 +162,8 @@ def test_groupby_quantile_with_arraylike_q_and_int_columns(frame_size, groupby, def test_quantile_raises(): df = DataFrame([["foo", "a"], ["foo", "b"], ["foo", "c"]], columns=["key", "val"]) - with pytest.raises(TypeError, match="cannot be performed against 'object' dtypes"): + msg = "dtype 'object' does not support operation 'quantile'" + with pytest.raises(TypeError, match=msg): df.groupby("key").quantile() @@ -241,7 +242,6 @@ def test_groupby_quantile_nullable_array(values, q): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("q", [0.5, [0.0, 0.5, 1.0]]) @pytest.mark.parametrize("numeric_only", [True, False]) def test_groupby_quantile_raises_on_invalid_dtype(q, numeric_only): @@ -251,9 +251,8 @@ def test_groupby_quantile_raises_on_invalid_dtype(q, numeric_only): expected = df.groupby("a")[["b"]].quantile(q) tm.assert_frame_equal(result, expected) else: - with pytest.raises( - TypeError, match="'quantile' cannot be performed against 'object' dtypes!" - ): + msg = "dtype '.*' does not support operation 'quantile'" + with pytest.raises(TypeError, match=msg): df.groupby("a").quantile(q, numeric_only=numeric_only) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 0d13db79835ba..3305b48a4dcdc 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -425,7 +425,7 @@ def test_frame_multi_key_function_list(): tm.assert_frame_equal(agged, expected) -def test_frame_multi_key_function_list_partial_failure(): +def test_frame_multi_key_function_list_partial_failure(using_infer_string): data = DataFrame( { "A": [ @@ -476,6 +476,8 @@ def test_frame_multi_key_function_list_partial_failure(): grouped = data.groupby(["A", "B"]) funcs = ["mean", "std"] msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): grouped.agg(funcs) @@ -662,9 +664,11 @@ def test_groupby_multi_corner(df): tm.assert_frame_equal(agged, expected) -def test_raises_on_nuisance(df): +def test_raises_on_nuisance(df, using_infer_string): grouped = df.groupby("A") msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): grouped.agg("mean") with pytest.raises(TypeError, match=msg): @@ -699,7 +703,7 @@ def test_keep_nuisance_agg(df, agg_function): ["sum", "mean", "prod", "std", "var", "sem", "median"], ) @pytest.mark.parametrize("numeric_only", [True, False]) -def test_omit_nuisance_agg(df, agg_function, numeric_only): +def test_omit_nuisance_agg(df, agg_function, numeric_only, using_infer_string): # GH 38774, GH 38815 grouped = df.groupby("A") @@ -707,7 +711,10 @@ def test_omit_nuisance_agg(df, agg_function, numeric_only): if agg_function in no_drop_nuisance and not numeric_only: # Added numeric_only as part of GH#46560; these do not drop nuisance # columns when numeric_only is False - if agg_function in ("std", "sem"): + if using_infer_string: + msg = f"dtype 'str' does not support operation '{agg_function}'" + klass = TypeError + elif agg_function in ("std", "sem"): klass = ValueError msg = "could not convert string to float: 'one'" else: @@ -728,16 +735,24 @@ def test_omit_nuisance_agg(df, agg_function, numeric_only): tm.assert_frame_equal(result, expected) -def test_raise_on_nuisance_python_single(df): +def test_raise_on_nuisance_python_single(df, using_infer_string): # GH 38815 grouped = df.groupby("A") - with pytest.raises(ValueError, match="could not convert"): + + err = ValueError + msg = "could not convert" + if using_infer_string: + err = TypeError + msg = "dtype 'str' does not support operation 'skew'" + with pytest.raises(err, match=msg): grouped.skew() -def test_raise_on_nuisance_python_multiple(three_group): +def test_raise_on_nuisance_python_multiple(three_group, using_infer_string): grouped = three_group.groupby(["A", "B"]) msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): grouped.agg("mean") with pytest.raises(TypeError, match=msg): @@ -775,12 +790,16 @@ def test_nonsense_func(): df.groupby(lambda x: x + "foo") -def test_wrap_aggregated_output_multindex(multiindex_dataframe_random_data): +def test_wrap_aggregated_output_multindex( + multiindex_dataframe_random_data, using_infer_string +): df = multiindex_dataframe_random_data.T df["baz", "two"] = "peekaboo" keys = [np.array([0, 0, 1]), np.array([0, 0, 1])] msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): df.groupby(keys).agg("mean") agged = df.drop(columns=("baz", "two")).groupby(keys).agg("mean") @@ -960,8 +979,10 @@ def test_groupby_with_hier_columns(): def test_grouping_ndarray(df): grouped = df.groupby(df["A"].values) + grouped2 = df.groupby(df["A"].rename(None)) + result = grouped.sum() - expected = df.groupby(df["A"].rename(None)).sum() + expected = grouped2.sum() tm.assert_frame_equal(result, expected) @@ -1457,8 +1478,8 @@ def test_no_dummy_key_names(df): result = df.groupby(df["A"].values).sum() assert result.index.name is None - result = df.groupby([df["A"].values, df["B"].values]).sum() - assert result.index.names == (None, None) + result2 = df.groupby([df["A"].values, df["B"].values]).sum() + assert result2.index.names == (None, None) def test_groupby_sort_multiindex_series(): @@ -1761,6 +1782,7 @@ def get_categorical_invalid_expected(): is_per = isinstance(df.dtypes.iloc[0], pd.PeriodDtype) is_dt64 = df.dtypes.iloc[0].kind == "M" is_cat = isinstance(values, Categorical) + is_str = isinstance(df.dtypes.iloc[0], pd.StringDtype) if ( isinstance(values, Categorical) @@ -1785,13 +1807,15 @@ def get_categorical_invalid_expected(): if op in ["prod", "sum", "skew"]: # ops that require more than just ordered-ness - if is_dt64 or is_cat or is_per: + if is_dt64 or is_cat or is_per or (is_str and op != "sum"): # GH#41291 # datetime64 -> prod and sum are invalid if is_dt64: msg = "datetime64 type does not support" elif is_per: msg = "Period type does not support" + elif is_str: + msg = f"dtype 'str' does not support operation '{op}'" else: msg = "category type does not support" if op == "skew": @@ -2714,7 +2738,7 @@ def test_obj_with_exclusions_duplicate_columns(): def test_groupby_numeric_only_std_no_result(numeric_only): # GH 51080 dicts_non_numeric = [{"a": "foo", "b": "bar"}, {"a": "car", "b": "dar"}] - df = DataFrame(dicts_non_numeric) + df = DataFrame(dicts_non_numeric, dtype=object) dfgb = df.groupby("a", as_index=False, sort=False) if numeric_only: @@ -2773,10 +2797,14 @@ def test_grouping_with_categorical_interval_columns(): def test_groupby_sum_on_nan_should_return_nan(bug_var): # GH 24196 df = DataFrame({"A": [bug_var, bug_var, bug_var, np.nan]}) + if isinstance(bug_var, str): + df = df.astype(object) dfgb = df.groupby(lambda x: x) result = dfgb.sum(min_count=1) - expected_df = DataFrame([bug_var, bug_var, bug_var, None], columns=["A"]) + expected_df = DataFrame( + [bug_var, bug_var, bug_var, None], columns=["A"], dtype=df["A"].dtype + ) tm.assert_frame_equal(result, expected_df) diff --git a/pandas/tests/groupby/test_groupby_subclass.py b/pandas/tests/groupby/test_groupby_subclass.py index 0832b67b38098..a1f4627475bab 100644 --- a/pandas/tests/groupby/test_groupby_subclass.py +++ b/pandas/tests/groupby/test_groupby_subclass.py @@ -109,7 +109,7 @@ def test_groupby_resample_preserves_subclass(obj): df = obj( { - "Buyer": "Carl Carl Carl Carl Joe Carl".split(), + "Buyer": Series("Carl Carl Carl Carl Joe Carl".split(), dtype=object), "Quantity": [18, 3, 5, 1, 9, 3], "Date": [ datetime(2013, 9, 1, 13, 0), diff --git a/pandas/tests/groupby/test_numeric_only.py b/pandas/tests/groupby/test_numeric_only.py index 41e00f8121b14..cb4569812f600 100644 --- a/pandas/tests/groupby/test_numeric_only.py +++ b/pandas/tests/groupby/test_numeric_only.py @@ -28,7 +28,8 @@ def df(self): "group": [1, 1, 2], "int": [1, 2, 3], "float": [4.0, 5.0, 6.0], - "string": list("abc"), + "string": Series(["a", "b", "c"], dtype="str"), + "object": Series(["a", "b", "c"], dtype=object), "category_string": Series(list("abc")).astype("category"), "category_int": [7, 8, 9], "datetime": date_range("20130101", periods=3), @@ -40,6 +41,7 @@ def df(self): "int", "float", "string", + "object", "category_string", "category_int", "datetime", @@ -112,6 +114,7 @@ def test_first_last(self, df, method): "int", "float", "string", + "object", "category_string", "category_int", "datetime", @@ -159,7 +162,9 @@ def _check(self, df, method, expected_columns, expected_columns_numeric): # object dtypes for transformations are not implemented in Cython and # have no Python fallback - exception = NotImplementedError if method.startswith("cum") else TypeError + exception = ( + (NotImplementedError, TypeError) if method.startswith("cum") else TypeError + ) if method in ("min", "max", "cummin", "cummax", "cumsum", "cumprod"): # The methods default to numeric_only=False and raise TypeError @@ -170,6 +175,7 @@ def _check(self, df, method, expected_columns, expected_columns_numeric): re.escape(f"agg function failed [how->{method},dtype->object]"), # cumsum/cummin/cummax/cumprod "function is not implemented for this dtype", + f"dtype 'str' does not support operation '{method}'", ] ) with pytest.raises(exception, match=msg): @@ -180,7 +186,7 @@ def _check(self, df, method, expected_columns, expected_columns_numeric): "category type does not support sum operations", re.escape(f"agg function failed [how->{method},dtype->object]"), re.escape(f"agg function failed [how->{method},dtype->string]"), - re.escape(f"agg function failed [how->{method},dtype->str]"), + f"dtype 'str' does not support operation '{method}'", ] ) with pytest.raises(exception, match=msg): @@ -198,7 +204,7 @@ def _check(self, df, method, expected_columns, expected_columns_numeric): f"Cannot perform {method} with non-ordered Categorical", re.escape(f"agg function failed [how->{method},dtype->object]"), re.escape(f"agg function failed [how->{method},dtype->string]"), - re.escape(f"agg function failed [how->{method},dtype->str]"), + f"dtype 'str' does not support operation '{method}'", ] ) with pytest.raises(exception, match=msg): @@ -299,7 +305,9 @@ def test_numeric_only(kernel, has_arg, numeric_only, keys): re.escape(f"agg function failed [how->{kernel},dtype->object]"), ] ) - if kernel == "idxmin": + if kernel == "quantile": + msg = "dtype 'object' does not support operation 'quantile'" + elif kernel == "idxmin": msg = "'<' not supported between instances of 'type' and 'type'" elif kernel == "idxmax": msg = "'>' not supported between instances of 'type' and 'type'" @@ -379,7 +387,7 @@ def test_deprecate_numeric_only_series(dtype, groupby_func, request): # that succeed should not be allowed to fail (without deprecation, at least) if groupby_func in fails_on_numeric_object and dtype is object: if groupby_func == "quantile": - msg = "cannot be performed against 'object' dtypes" + msg = "dtype 'object' does not support operation 'quantile'" else: msg = "is not supported for object dtype" with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/groupby/test_raises.py b/pandas/tests/groupby/test_raises.py index 38b4abfddda1e..1e0a15d0ba796 100644 --- a/pandas/tests/groupby/test_raises.py +++ b/pandas/tests/groupby/test_raises.py @@ -8,8 +8,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( Categorical, DataFrame, @@ -106,10 +104,9 @@ def _call_and_check(klass, msg, how, gb, groupby_func, args, warn_msg=""): gb.transform(groupby_func, *args) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("how", ["method", "agg", "transform"]) def test_groupby_raises_string( - how, by, groupby_series, groupby_func, df_with_string_col + how, by, groupby_series, groupby_func, df_with_string_col, using_infer_string ): df = df_with_string_col args = get_groupby_method_args(groupby_func, df) @@ -169,7 +166,7 @@ def test_groupby_raises_string( TypeError, re.escape("agg function failed [how->prod,dtype->object]"), ), - "quantile": (TypeError, "cannot be performed against 'object' dtypes!"), + "quantile": (TypeError, "dtype 'object' does not support operation 'quantile'"), "rank": (None, ""), "sem": (ValueError, "could not convert string to float"), "shift": (None, ""), @@ -183,6 +180,37 @@ def test_groupby_raises_string( ), }[groupby_func] + if using_infer_string: + if groupby_func in [ + "prod", + "mean", + "median", + "cumsum", + "cumprod", + "std", + "sem", + "var", + "skew", + "quantile", + ]: + msg = f"dtype 'str' does not support operation '{groupby_func}'" + if groupby_func in ["sem", "std", "skew"]: + # The object-dtype raises ValueError when trying to convert to numeric. + klass = TypeError + elif groupby_func == "pct_change" and df["d"].dtype.storage == "pyarrow": + # This doesn't go through EA._groupby_op so the message isn't controlled + # there. + msg = "operation 'truediv' not supported for dtype 'str' with dtype 'str'" + elif groupby_func == "diff" and df["d"].dtype.storage == "pyarrow": + # This doesn't go through EA._groupby_op so the message isn't controlled + # there. + msg = "operation 'sub' not supported for dtype 'str' with dtype 'str'" + + elif groupby_func in ["cummin", "cummax"]: + msg = msg.replace("object", "str") + elif groupby_func == "corrwith": + msg = "Cannot perform reduction 'mean' with string dtype" + if groupby_func == "fillna": kind = "Series" if groupby_series else "DataFrame" warn_msg = f"{kind}GroupBy.fillna is deprecated" @@ -211,7 +239,12 @@ def func(x): @pytest.mark.parametrize("how", ["agg", "transform"]) @pytest.mark.parametrize("groupby_func_np", [np.sum, np.mean]) def test_groupby_raises_string_np( - how, by, groupby_series, groupby_func_np, df_with_string_col + how, + by, + groupby_series, + groupby_func_np, + df_with_string_col, + using_infer_string, ): # GH#50749 df = df_with_string_col @@ -228,6 +261,15 @@ def test_groupby_raises_string_np( "Cannot perform reduction 'mean' with string dtype", ), }[groupby_func_np] + + if using_infer_string: + if groupby_func_np is np.mean: + klass = TypeError + msg = ( + f"Cannot perform reduction '{groupby_func_np.__name__}' " + "with string dtype" + ) + _call_and_check(klass, msg, how, gb, groupby_func_np, ()) diff --git a/pandas/tests/resample/test_resample_api.py b/pandas/tests/resample/test_resample_api.py index a8fb1b392322d..b7b80b5e427ff 100644 --- a/pandas/tests/resample/test_resample_api.py +++ b/pandas/tests/resample/test_resample_api.py @@ -187,7 +187,7 @@ def test_api_compat_before_use(attr): getattr(rs, attr) -def tests_raises_on_nuisance(test_frame): +def tests_raises_on_nuisance(test_frame, using_infer_string): df = test_frame df["D"] = "foo" r = df.resample("h") @@ -197,6 +197,8 @@ def tests_raises_on_nuisance(test_frame): expected = r[["A", "B", "C"]].mean() msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): r.mean() result = r.mean(numeric_only=True) @@ -881,7 +883,9 @@ def test_end_and_end_day_origin( ("sem", lib.no_default, "could not convert string to float"), ], ) -def test_frame_downsample_method(method, numeric_only, expected_data): +def test_frame_downsample_method( + method, numeric_only, expected_data, using_infer_string +): # GH#46442 test if `numeric_only` behave as expected for DataFrameGroupBy index = date_range("2018-01-01", periods=2, freq="D") @@ -898,6 +902,11 @@ def test_frame_downsample_method(method, numeric_only, expected_data): if method in ("var", "mean", "median", "prod"): klass = TypeError msg = re.escape(f"agg function failed [how->{method},dtype->") + if using_infer_string: + msg = f"dtype 'str' does not support operation '{method}'" + elif method in ["sum", "std", "sem"] and using_infer_string: + klass = TypeError + msg = f"dtype 'str' does not support operation '{method}'" else: klass = ValueError msg = expected_data @@ -932,7 +941,9 @@ def test_frame_downsample_method(method, numeric_only, expected_data): ("last", lib.no_default, ["cat_2"]), ], ) -def test_series_downsample_method(method, numeric_only, expected_data): +def test_series_downsample_method( + method, numeric_only, expected_data, using_infer_string +): # GH#46442 test if `numeric_only` behave as expected for SeriesGroupBy index = date_range("2018-01-01", periods=2, freq="D") @@ -948,8 +959,11 @@ def test_series_downsample_method(method, numeric_only, expected_data): func(**kwargs) elif method == "prod": msg = re.escape("agg function failed [how->prod,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'prod'" with pytest.raises(TypeError, match=msg): func(**kwargs) + else: result = func(**kwargs) expected = Series(expected_data, index=expected_index) diff --git a/pandas/tests/reshape/merge/test_join.py b/pandas/tests/reshape/merge/test_join.py index 0f743332acbbe..65bfea0b9beea 100644 --- a/pandas/tests/reshape/merge/test_join.py +++ b/pandas/tests/reshape/merge/test_join.py @@ -620,7 +620,7 @@ def test_join_non_unique_period_index(self): ) tm.assert_frame_equal(result, expected) - def test_mixed_type_join_with_suffix(self): + def test_mixed_type_join_with_suffix(self, using_infer_string): # GH #916 df = DataFrame( np.random.default_rng(2).standard_normal((20, 6)), @@ -631,6 +631,8 @@ def test_mixed_type_join_with_suffix(self): grouped = df.groupby("id") msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): grouped.mean() mn = grouped.mean(numeric_only=True) diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index eccf676b87f89..d8a9acdc561fd 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -935,12 +935,14 @@ def test_margins(self, data): for value_col in table.columns.levels[0]: self._check_output(table[value_col], value_col, data) - def test_no_col(self, data): + def test_no_col(self, data, using_infer_string): # no col # to help with a buglet data.columns = [k * 2 for k in data.columns] msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): data.pivot_table(index=["AA", "BB"], margins=True, aggfunc="mean") table = data.drop(columns="CC").pivot_table( @@ -990,7 +992,7 @@ def test_no_col(self, data): ], ) def test_margin_with_only_columns_defined( - self, columns, aggfunc, values, expected_columns + self, columns, aggfunc, values, expected_columns, using_infer_string ): # GH 31016 df = DataFrame( @@ -1014,6 +1016,8 @@ def test_margin_with_only_columns_defined( ) if aggfunc != "sum": msg = re.escape("agg function failed [how->mean,dtype->") + if using_infer_string: + msg = "dtype 'str' does not support operation 'mean'" with pytest.raises(TypeError, match=msg): df.pivot_table(columns=columns, margins=True, aggfunc=aggfunc) if "B" not in columns: From 9b16b9e6a7149a7206f533be187f0e2ad9d13bbc Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva <91160475+natmokval@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:45:41 +0100 Subject: [PATCH 036/266] DEPR: revert enforcing the deprecation of exposing blocks in core.internals and deprecate with FutureWarning (#58715) --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/internals/__init__.py | 44 ++++++++++++++++++++++++++++++ pandas/io/pytables.py | 3 +- pandas/tests/internals/test_api.py | 26 ++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 13e1ddd153ccb..89bc942cb7250 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -481,7 +481,7 @@ Other Removals - Enforced deprecation of :meth:`Series.interpolate` and :meth:`DataFrame.interpolate` for object-dtype (:issue:`57820`) - Enforced deprecation of :meth:`offsets.Tick.delta`, use ``pd.Timedelta(obj)`` instead (:issue:`55498`) - Enforced deprecation of ``axis=None`` acting the same as ``axis=0`` in the DataFrame reductions ``sum``, ``prod``, ``std``, ``var``, and ``sem``, passing ``axis=None`` will now reduce over both axes; this is particularly the case when doing e.g. ``numpy.sum(df)`` (:issue:`21597`) -- Enforced deprecation of ``core.internals`` members ``Block``, ``ExtensionBlock``, and ``DatetimeTZBlock`` (:issue:`58467`) +- Enforced deprecation of ``core.internals`` member ``DatetimeTZBlock`` (:issue:`58467`) - Enforced deprecation of ``date_parser`` in :func:`read_csv`, :func:`read_table`, :func:`read_fwf`, and :func:`read_excel` in favour of ``date_format`` (:issue:`50601`) - Enforced deprecation of ``keep_date_col`` keyword in :func:`read_csv` (:issue:`55569`) - Enforced deprecation of ``quantile`` keyword in :meth:`.Rolling.quantile` and :meth:`.Expanding.quantile`, renamed to ``q`` instead. (:issue:`52550`) diff --git a/pandas/core/internals/__init__.py b/pandas/core/internals/__init__.py index 45758379e0bd6..5ab70ba38f9c2 100644 --- a/pandas/core/internals/__init__.py +++ b/pandas/core/internals/__init__.py @@ -6,8 +6,52 @@ ) __all__ = [ + "Block", + "ExtensionBlock", "make_block", "BlockManager", "SingleBlockManager", "concatenate_managers", ] + + +def __getattr__(name: str): + # GH#55139 + import warnings + + if name == "create_block_manager_from_blocks": + # GH#33892 + warnings.warn( + f"{name} is deprecated and will be removed in a future version. " + "Use public APIs instead.", + FutureWarning, + # https://github.com/pandas-dev/pandas/pull/55139#pullrequestreview-1720690758 + # on hard-coding stacklevel + stacklevel=2, + ) + from pandas.core.internals.managers import create_block_manager_from_blocks + + return create_block_manager_from_blocks + + if name in [ + "Block", + "ExtensionBlock", + ]: + warnings.warn( + f"{name} is deprecated and will be removed in a future version. " + "Use public APIs instead.", + FutureWarning, + # https://github.com/pandas-dev/pandas/pull/55139#pullrequestreview-1720690758 + # on hard-coding stacklevel + stacklevel=2, + ) + if name == "ExtensionBlock": + from pandas.core.internals.blocks import ExtensionBlock + + return ExtensionBlock + else: + from pandas.core.internals.blocks import Block + + return Block + + raise AttributeError(f"module 'pandas.core.internals' has no attribute '{name}'") diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py index 618254fee9259..7d265bc430125 100644 --- a/pandas/io/pytables.py +++ b/pandas/io/pytables.py @@ -126,8 +126,7 @@ npt, ) - from pandas.core.internals.blocks import Block - + from pandas.core.internals import Block # versioning attribute _version = "0.15.2" diff --git a/pandas/tests/internals/test_api.py b/pandas/tests/internals/test_api.py index 591157bbe87fe..fc222f6987466 100644 --- a/pandas/tests/internals/test_api.py +++ b/pandas/tests/internals/test_api.py @@ -41,6 +41,20 @@ def test_namespace(): assert set(result) == set(expected + modules) +@pytest.mark.parametrize( + "name", + [ + "Block", + "ExtensionBlock", + ], +) +def test_deprecations(name): + # GH#55139 + msg = f"{name} is deprecated.* Use public APIs instead" + with tm.assert_produces_warning(FutureWarning, match=msg): + getattr(internals, name) + + def test_make_block_2d_with_dti(): # GH#41168 dti = pd.date_range("2012", periods=3, tz="UTC") @@ -53,6 +67,18 @@ def test_make_block_2d_with_dti(): assert blk.values.shape == (1, 3) +def test_create_block_manager_from_blocks_deprecated(): + # GH#33892 + # If they must, downstream packages should get this from internals.api, + # not internals. + msg = ( + "create_block_manager_from_blocks is deprecated and will be " + "removed in a future version. Use public APIs instead" + ) + with tm.assert_produces_warning(FutureWarning, match=msg): + internals.create_block_manager_from_blocks + + def test_create_dataframe_from_blocks(float_frame): block = float_frame._mgr.blocks[0] index = float_frame.index.copy() From db38017ba647b000a4f3dec9c6ace51cb7f6ab95 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 8 Nov 2024 17:55:36 +0100 Subject: [PATCH 037/266] TST (string dtype): resolve xfail in arrow interface tests (#60241) --- pandas/tests/frame/test_arrow_interface.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pandas/tests/frame/test_arrow_interface.py b/pandas/tests/frame/test_arrow_interface.py index dc163268f64b9..b36b6b5ffe0cc 100644 --- a/pandas/tests/frame/test_arrow_interface.py +++ b/pandas/tests/frame/test_arrow_interface.py @@ -2,8 +2,6 @@ import pytest -from pandas._config import using_string_dtype - import pandas.util._test_decorators as td import pandas as pd @@ -11,9 +9,8 @@ pa = pytest.importorskip("pyarrow") -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @td.skip_if_no("pyarrow", min_version="14.0") -def test_dataframe_arrow_interface(): +def test_dataframe_arrow_interface(using_infer_string): df = pd.DataFrame({"a": [1, 2, 3], "b": ["a", "b", "c"]}) capsule = df.__arrow_c_stream__() @@ -25,7 +22,8 @@ def test_dataframe_arrow_interface(): ) table = pa.table(df) - expected = pa.table({"a": [1, 2, 3], "b": ["a", "b", "c"]}) + string_type = pa.large_string() if using_infer_string else pa.string() + expected = pa.table({"a": [1, 2, 3], "b": pa.array(["a", "b", "c"], string_type)}) assert table.equals(expected) schema = pa.schema([("a", pa.int8()), ("b", pa.string())]) @@ -34,13 +32,13 @@ def test_dataframe_arrow_interface(): assert table.equals(expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @td.skip_if_no("pyarrow", min_version="15.0") -def test_dataframe_to_arrow(): +def test_dataframe_to_arrow(using_infer_string): df = pd.DataFrame({"a": [1, 2, 3], "b": ["a", "b", "c"]}) table = pa.RecordBatchReader.from_stream(df).read_all() - expected = pa.table({"a": [1, 2, 3], "b": ["a", "b", "c"]}) + string_type = pa.large_string() if using_infer_string else pa.string() + expected = pa.table({"a": [1, 2, 3], "b": pa.array(["a", "b", "c"], string_type)}) assert table.equals(expected) schema = pa.schema([("a", pa.int8()), ("b", pa.string())]) From 754d09163ae08f2b87daa41f2263556dbb809616 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 8 Nov 2024 18:13:03 +0100 Subject: [PATCH 038/266] BUG (string dtype): correctly enable idxmin/max for python-storage strings (#60242) --- pandas/core/arrays/string_.py | 2 +- pandas/tests/frame/test_reductions.py | 5 ----- pandas/tests/reductions/test_reductions.py | 8 -------- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index 01619dab7ce45..de129df2575d3 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -846,7 +846,7 @@ def _reduce( else: return nanops.nanall(self._ndarray, skipna=skipna) - if name in ["min", "max", "sum"]: + if name in ["min", "max", "argmin", "argmax", "sum"]: result = getattr(self, name)(skipna=skipna, axis=axis, **kwargs) if keepdims: return self._from_sequence([result], dtype=self.dtype) diff --git a/pandas/tests/frame/test_reductions.py b/pandas/tests/frame/test_reductions.py index 30d02f9b5463d..fde4dfeed9c55 100644 --- a/pandas/tests/frame/test_reductions.py +++ b/pandas/tests/frame/test_reductions.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import ( IS64, is_platform_windows, @@ -1081,7 +1079,6 @@ def test_idxmin_empty(self, index, skipna, axis): expected = Series(dtype=index.dtype) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("numeric_only", [True, False]) def test_idxmin_numeric_only(self, numeric_only): df = DataFrame({"a": [2, 3, 1], "b": [2, 1, 1], "c": list("xyx")}) @@ -1098,7 +1095,6 @@ def test_idxmin_axis_2(self, float_frame): with pytest.raises(ValueError, match=msg): frame.idxmin(axis=2) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("axis", [0, 1]) def test_idxmax(self, float_frame, int_frame, skipna, axis): frame = float_frame @@ -1132,7 +1128,6 @@ def test_idxmax_empty(self, index, skipna, axis): expected = Series(dtype=index.dtype) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("numeric_only", [True, False]) def test_idxmax_numeric_only(self, numeric_only): df = DataFrame({"a": [2, 3, 1], "b": [2, 1, 1], "c": list("xyx")}) diff --git a/pandas/tests/reductions/test_reductions.py b/pandas/tests/reductions/test_reductions.py index 8153ba66d632b..476978aeab15a 100644 --- a/pandas/tests/reductions/test_reductions.py +++ b/pandas/tests/reductions/test_reductions.py @@ -7,10 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW - import pandas as pd from pandas import ( Categorical, @@ -1206,10 +1202,6 @@ def test_idxminmax_object_dtype(self, using_infer_string): with pytest.raises(TypeError, match=msg): ser3.idxmin(skipna=False) - # TODO(infer_string) implement argmin/max for python string dtype - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)" - ) def test_idxminmax_object_frame(self): # GH#4279 df = DataFrame([["zimm", 2.5], ["biff", 1.0], ["bid", 12.0]]) From 2e3e6946f42770d1a4a3218655a2fe2c9a084e2e Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Sat, 9 Nov 2024 01:18:34 +0530 Subject: [PATCH 039/266] DOC: fix SA01,ES01 for pandas.arrays.IntervalArray.right (#60249) --- ci/code_checks.sh | 1 - pandas/core/arrays/interval.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index adcf48507698b..1c70fcb44f910 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -90,7 +90,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ -i "pandas.arrays.IntegerArray SA01" \ -i "pandas.arrays.IntervalArray.length SA01" \ - -i "pandas.arrays.IntervalArray.right SA01" \ -i "pandas.arrays.NumpyExtensionArray SA01" \ -i "pandas.arrays.SparseArray PR07,SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 3e231fb9f8ecb..f47ef095a8409 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1269,6 +1269,21 @@ def right(self) -> Index: """ Return the right endpoints of each Interval in the IntervalArray as an Index. + This property extracts the right endpoints from each interval contained within + the IntervalArray. This can be helpful in use cases where you need to work + with or compare only the upper bounds of intervals, such as when performing + range-based filtering, determining interval overlaps, or visualizing the end + boundaries of data segments. + + See Also + -------- + arrays.IntervalArray.left : Return the left endpoints of each Interval in + the IntervalArray as an Index. + arrays.IntervalArray.mid : Return the midpoint of each Interval in the + IntervalArray as an Index. + arrays.IntervalArray.contains : Check elementwise if the Intervals contain + the value. + Examples -------- From 7740c4e2c33ba855d36db93d3028164d08e8263c Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Sat, 9 Nov 2024 01:19:12 +0530 Subject: [PATCH 040/266] DOC: fix PR07,SA01,ES01 for pandas.arrays.SparseArray (#60250) --- ci/code_checks.sh | 1 - pandas/core/arrays/sparse/array.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 1c70fcb44f910..093e7a8e26854 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -91,7 +91,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.IntegerArray SA01" \ -i "pandas.arrays.IntervalArray.length SA01" \ -i "pandas.arrays.NumpyExtensionArray SA01" \ - -i "pandas.arrays.SparseArray PR07,SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \ diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index a3db7dc1f93e9..137dbb6e4d139 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -289,12 +289,18 @@ class SparseArray(OpsMixin, PandasObject, ExtensionArray): """ An ExtensionArray for storing sparse data. + SparseArray efficiently stores data with a high frequency of a + specific fill value (e.g., zeros), saving memory by only retaining + non-fill elements and their indices. This class is particularly + useful for large datasets where most values are redundant. + Parameters ---------- data : array-like or scalar A dense array of values to store in the SparseArray. This may contain `fill_value`. sparse_index : SparseIndex, optional + Index indicating the locations of sparse elements. fill_value : scalar, optional Elements in data that are ``fill_value`` are not stored in the SparseArray. For memory savings, this should be the most common value @@ -345,6 +351,10 @@ class SparseArray(OpsMixin, PandasObject, ExtensionArray): ------- None + See Also + -------- + SparseDtype : Dtype for sparse data. + Examples -------- >>> from pandas.arrays import SparseArray From 084b1999cffde35bf9e49e5e5b8a5a0482bf927d Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 8 Nov 2024 22:47:07 +0100 Subject: [PATCH 041/266] TST (string dtype): resolve xfails in pandas/tests/copy_view (#60245) --- pandas/_testing/__init__.py | 28 ++++++----------- pandas/tests/copy_view/test_astype.py | 22 +++++++------- pandas/tests/copy_view/test_functions.py | 1 - pandas/tests/copy_view/test_methods.py | 38 +++++++++++++----------- pandas/tests/copy_view/test_replace.py | 14 +++------ 5 files changed, 46 insertions(+), 57 deletions(-) diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index 0a110d69c7a70..e092d65f08dd4 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -7,7 +7,6 @@ from typing import ( TYPE_CHECKING, ContextManager, - cast, ) import numpy as np @@ -21,8 +20,6 @@ from pandas.compat import pa_version_under10p1 -from pandas.core.dtypes.common import is_string_dtype - import pandas as pd from pandas import ( ArrowDtype, @@ -77,8 +74,8 @@ with_csv_dialect, ) from pandas.core.arrays import ( + ArrowExtensionArray, BaseMaskedArray, - ExtensionArray, NumpyExtensionArray, ) from pandas.core.arrays._mixins import NDArrayBackedExtensionArray @@ -92,7 +89,6 @@ NpDtype, ) - from pandas.core.arrays import ArrowExtensionArray UNSIGNED_INT_NUMPY_DTYPES: list[NpDtype] = ["uint8", "uint16", "uint32", "uint64"] UNSIGNED_INT_EA_DTYPES: list[Dtype] = ["UInt8", "UInt16", "UInt32", "UInt64"] @@ -512,24 +508,18 @@ def shares_memory(left, right) -> bool: if isinstance(left, pd.core.arrays.IntervalArray): return shares_memory(left._left, right) or shares_memory(left._right, right) - if ( - isinstance(left, ExtensionArray) - and is_string_dtype(left.dtype) - and left.dtype.storage == "pyarrow" # type: ignore[attr-defined] - ): - # https://github.com/pandas-dev/pandas/pull/43930#discussion_r736862669 - left = cast("ArrowExtensionArray", left) - if ( - isinstance(right, ExtensionArray) - and is_string_dtype(right.dtype) - and right.dtype.storage == "pyarrow" # type: ignore[attr-defined] - ): - right = cast("ArrowExtensionArray", right) + if isinstance(left, ArrowExtensionArray): + if isinstance(right, ArrowExtensionArray): + # https://github.com/pandas-dev/pandas/pull/43930#discussion_r736862669 left_pa_data = left._pa_array right_pa_data = right._pa_array left_buf1 = left_pa_data.chunk(0).buffers()[1] right_buf1 = right_pa_data.chunk(0).buffers()[1] - return left_buf1 == right_buf1 + return left_buf1.address == right_buf1.address + else: + # if we have one one ArrowExtensionArray and one other array, assume + # they can only share memory if they share the same numpy buffer + return np.shares_memory(left, right) if isinstance(left, BaseMaskedArray) and isinstance(right, BaseMaskedArray): # By convention, we'll say these share memory if they share *either* diff --git a/pandas/tests/copy_view/test_astype.py b/pandas/tests/copy_view/test_astype.py index 80c30f2d0c26e..91f5badeb9728 100644 --- a/pandas/tests/copy_view/test_astype.py +++ b/pandas/tests/copy_view/test_astype.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import HAS_PYARROW from pandas.compat.pyarrow import pa_version_under12p0 @@ -206,7 +204,6 @@ def test_astype_arrow_timestamp(): assert np.shares_memory(get_array(df, "a"), get_array(result, "a")._pa_array) -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") def test_convert_dtypes_infer_objects(): ser = Series(["a", "b", "c"]) ser_orig = ser.copy() @@ -217,20 +214,25 @@ def test_convert_dtypes_infer_objects(): convert_string=False, ) - assert np.shares_memory(get_array(ser), get_array(result)) + assert tm.shares_memory(get_array(ser), get_array(result)) result.iloc[0] = "x" tm.assert_series_equal(ser, ser_orig) -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") -def test_convert_dtypes(): +def test_convert_dtypes(using_infer_string): df = DataFrame({"a": ["a", "b"], "b": [1, 2], "c": [1.5, 2.5], "d": [True, False]}) df_orig = df.copy() df2 = df.convert_dtypes() - assert np.shares_memory(get_array(df2, "a"), get_array(df, "a")) - assert np.shares_memory(get_array(df2, "d"), get_array(df, "d")) - assert np.shares_memory(get_array(df2, "b"), get_array(df, "b")) - assert np.shares_memory(get_array(df2, "c"), get_array(df, "c")) + if using_infer_string and HAS_PYARROW: + # TODO the default nullable string dtype still uses python storage + # this should be changed to pyarrow if installed + assert not tm.shares_memory(get_array(df2, "a"), get_array(df, "a")) + else: + assert tm.shares_memory(get_array(df2, "a"), get_array(df, "a")) + assert tm.shares_memory(get_array(df2, "d"), get_array(df, "d")) + assert tm.shares_memory(get_array(df2, "b"), get_array(df, "b")) + assert tm.shares_memory(get_array(df2, "c"), get_array(df, "c")) df2.iloc[0, 0] = "x" + df2.iloc[0, 1] = 10 tm.assert_frame_equal(df, df_orig) diff --git a/pandas/tests/copy_view/test_functions.py b/pandas/tests/copy_view/test_functions.py index fcdece6077829..32fea794975b6 100644 --- a/pandas/tests/copy_view/test_functions.py +++ b/pandas/tests/copy_view/test_functions.py @@ -153,7 +153,6 @@ def test_concat_copy_keyword(): assert np.shares_memory(get_array(df2, "b"), get_array(result, "b")) -# @pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") @pytest.mark.parametrize( "func", [ diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py index 92e1ba750fae2..250697c91ff13 100644 --- a/pandas/tests/copy_view/test_methods.py +++ b/pandas/tests/copy_view/test_methods.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import HAS_PYARROW import pandas as pd @@ -716,14 +714,18 @@ def test_head_tail(method): tm.assert_frame_equal(df, df_orig) -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") -def test_infer_objects(): - df = DataFrame({"a": [1, 2], "b": "c", "c": 1, "d": "x"}) +def test_infer_objects(using_infer_string): + df = DataFrame( + {"a": [1, 2], "b": Series(["x", "y"], dtype=object), "c": 1, "d": "x"} + ) df_orig = df.copy() df2 = df.infer_objects() assert np.shares_memory(get_array(df2, "a"), get_array(df, "a")) - assert np.shares_memory(get_array(df2, "b"), get_array(df, "b")) + if using_infer_string and HAS_PYARROW: + assert not tm.shares_memory(get_array(df2, "b"), get_array(df, "b")) + else: + assert np.shares_memory(get_array(df2, "b"), get_array(df, "b")) df2.iloc[0, 0] = 0 df2.iloc[0, 1] = "d" @@ -732,19 +734,16 @@ def test_infer_objects(): tm.assert_frame_equal(df, df_orig) -@pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)" -) -def test_infer_objects_no_reference(): +def test_infer_objects_no_reference(using_infer_string): df = DataFrame( { "a": [1, 2], - "b": "c", + "b": Series(["x", "y"], dtype=object), "c": 1, "d": Series( [Timestamp("2019-12-31"), Timestamp("2020-12-31")], dtype="object" ), - "e": "b", + "e": Series(["z", "w"], dtype=object), } ) df = df.infer_objects() @@ -757,8 +756,14 @@ def test_infer_objects_no_reference(): df.iloc[0, 1] = "d" df.iloc[0, 3] = Timestamp("2018-12-31") assert np.shares_memory(arr_a, get_array(df, "a")) - # TODO(CoW): Block splitting causes references here - assert not np.shares_memory(arr_b, get_array(df, "b")) + if using_infer_string and HAS_PYARROW: + # note that the underlying memory of arr_b has been copied anyway + # because of the assignment, but the EA is updated inplace so still + # appears the share memory + assert tm.shares_memory(arr_b, get_array(df, "b")) + else: + # TODO(CoW): Block splitting causes references here + assert not np.shares_memory(arr_b, get_array(df, "b")) assert np.shares_memory(arr_d, get_array(df, "d")) @@ -766,7 +771,7 @@ def test_infer_objects_reference(): df = DataFrame( { "a": [1, 2], - "b": "c", + "b": Series(["x", "y"], dtype=object), "c": 1, "d": Series( [Timestamp("2019-12-31"), Timestamp("2020-12-31")], dtype="object" @@ -904,14 +909,13 @@ def test_sort_values_inplace(obj, kwargs): tm.assert_equal(view, obj_orig) -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") @pytest.mark.parametrize("decimals", [-1, 0, 1]) def test_round(decimals): df = DataFrame({"a": [1, 2], "b": "c"}) df_orig = df.copy() df2 = df.round(decimals=decimals) - assert np.shares_memory(get_array(df2, "b"), get_array(df, "b")) + assert tm.shares_memory(get_array(df2, "b"), get_array(df, "b")) # TODO: Make inplace by using out parameter of ndarray.round? if decimals >= 0: # Ensure lazy copy if no-op diff --git a/pandas/tests/copy_view/test_replace.py b/pandas/tests/copy_view/test_replace.py index e57514bffdf1e..d4838a5e68ab8 100644 --- a/pandas/tests/copy_view/test_replace.py +++ b/pandas/tests/copy_view/test_replace.py @@ -1,10 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW - from pandas import ( Categorical, DataFrame, @@ -13,7 +9,6 @@ from pandas.tests.copy_view.util import get_array -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize( "replace_kwargs", [ @@ -30,14 +25,14 @@ ], ) def test_replace(replace_kwargs): - df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": ["foo", "bar", "baz"]}) + df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [0.1, 0.2, 0.3]}) df_orig = df.copy() df_replaced = df.replace(**replace_kwargs) if (df_replaced["b"] == df["b"]).all(): assert np.shares_memory(get_array(df_replaced, "b"), get_array(df, "b")) - assert np.shares_memory(get_array(df_replaced, "c"), get_array(df, "c")) + assert tm.shares_memory(get_array(df_replaced, "c"), get_array(df, "c")) # mutating squeezed df triggers a copy-on-write for that column/block df_replaced.loc[0, "c"] = -1 @@ -61,18 +56,17 @@ def test_replace_regex_inplace_refs(): tm.assert_frame_equal(view, df_orig) -@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") def test_replace_regex_inplace(): df = DataFrame({"a": ["aaa", "bbb"]}) arr = get_array(df, "a") df.replace(to_replace=r"^a.*$", value="new", inplace=True, regex=True) assert df._mgr._has_no_reference(0) - assert np.shares_memory(arr, get_array(df, "a")) + assert tm.shares_memory(arr, get_array(df, "a")) df_orig = df.copy() df2 = df.replace(to_replace=r"^b.*$", value="new", regex=True) tm.assert_frame_equal(df_orig, df) - assert not np.shares_memory(get_array(df2, "a"), get_array(df, "a")) + assert not tm.shares_memory(get_array(df2, "a"), get_array(df, "a")) def test_replace_regex_inplace_no_op(): From 4fcee0e431135bf6fa97440d4d7e17a96630fe6e Mon Sep 17 00:00:00 2001 From: DL Lim <69715968+dl-lim@users.noreply.github.com> Date: Sat, 9 Nov 2024 23:57:58 +0000 Subject: [PATCH 042/266] Update mypy version from 1.11.2 to 1.13.0 (#60260) --- environment.yml | 2 +- pandas/core/computation/ops.py | 3 +-- pandas/core/dtypes/dtypes.py | 6 ++++-- pandas/core/generic.py | 4 +++- pandas/core/indexes/interval.py | 3 +-- pandas/core/indexing.py | 4 +++- pandas/core/missing.py | 11 ++++------- pandas/core/nanops.py | 10 +++++++--- pandas/io/common.py | 6 +++--- pandas/plotting/_core.py | 4 +++- pandas/plotting/_matplotlib/boxplot.py | 5 +---- pandas/plotting/_matplotlib/hist.py | 5 +---- pandas/tests/io/test_sql.py | 9 ++++++--- requirements-dev.txt | 2 +- 14 files changed, 39 insertions(+), 35 deletions(-) diff --git a/environment.yml b/environment.yml index 5ef5fbe910427..9bf6cf2a92347 100644 --- a/environment.yml +++ b/environment.yml @@ -77,7 +77,7 @@ dependencies: # code checks - flake8=7.1.0 # run in subprocess over docstring examples - - mypy=1.11.2 # pre-commit uses locally installed mypy + - mypy=1.13.0 # pre-commit uses locally installed mypy - tokenize-rt # scripts/check_for_inconsistent_pandas_namespace.py - pre-commit>=4.0.1 diff --git a/pandas/core/computation/ops.py b/pandas/core/computation/ops.py index a1a5f77f8539e..9b26de42e119b 100644 --- a/pandas/core/computation/ops.py +++ b/pandas/core/computation/ops.py @@ -76,8 +76,7 @@ class Term: def __new__(cls, name, env, side=None, encoding=None): klass = Constant if not isinstance(name, str) else cls - # error: Argument 2 for "super" not an instance of argument 1 - supr_new = super(Term, klass).__new__ # type: ignore[misc] + supr_new = super(Term, klass).__new__ return supr_new(klass) is_local: bool diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 004a1aab5436e..96b0aa16940a6 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -2130,9 +2130,11 @@ def _get_common_dtype(self, dtypes: list[DtypeObj]) -> DtypeObj | None: PerformanceWarning, stacklevel=find_stack_level(), ) - np_dtypes = (x.subtype if isinstance(x, SparseDtype) else x for x in dtypes) - return SparseDtype(np_find_common_type(*np_dtypes), fill_value=fill_value) + # error: Argument 1 to "np_find_common_type" has incompatible type + # "*Generator[Any | dtype[Any] | ExtensionDtype, None, None]"; + # expected "dtype[Any]" [arg-type] + return SparseDtype(np_find_common_type(*np_dtypes), fill_value=fill_value) # type: ignore [arg-type] @register_extension_dtype diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 35014674565ff..7c2cc5d33a5db 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -8024,7 +8024,9 @@ def asof(self, where, subset=None): np.nan, index=self.columns, name=where[0] ) - locs = self.index.asof_locs(where, ~(nulls._values)) + # error: Unsupported operand type for + # ~ ("ExtensionArray | ndarray[Any, Any] | Any") + locs = self.index.asof_locs(where, ~nulls._values) # type: ignore[operator] # mask the missing mask = locs == -1 diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index b0b9c5419e2ad..13811c28e6c1e 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -558,8 +558,7 @@ def _maybe_convert_i8(self, key): left = self._maybe_convert_i8(key.left) right = self._maybe_convert_i8(key.right) constructor = Interval if scalar else IntervalIndex.from_arrays - # error: "object" not callable - return constructor(left, right, closed=self.closed) # type: ignore[operator] + return constructor(left, right, closed=self.closed) if scalar: # Timestamp/Timedelta diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 08bd3cde60806..975e7ad135ba7 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -914,7 +914,9 @@ def __setitem__(self, key, value) -> None: indexer = self._get_setitem_indexer(key) self._has_valid_setitem_indexer(key) - iloc = self if self.name == "iloc" else self.obj.iloc + iloc: _iLocIndexer = ( + cast("_iLocIndexer", self) if self.name == "iloc" else self.obj.iloc + ) iloc._setitem_with_indexer(indexer, value, self.name) def _validate_key(self, key, axis: AxisInt) -> None: diff --git a/pandas/core/missing.py b/pandas/core/missing.py index 039d868bccd16..ff2daae002731 100644 --- a/pandas/core/missing.py +++ b/pandas/core/missing.py @@ -413,13 +413,10 @@ def func(yvalues: np.ndarray) -> None: **kwargs, ) - # error: Argument 1 to "apply_along_axis" has incompatible type - # "Callable[[ndarray[Any, Any]], None]"; expected "Callable[..., - # Union[_SupportsArray[dtype[]], Sequence[_SupportsArray - # [dtype[]]], Sequence[Sequence[_SupportsArray[dtype[]]]], - # Sequence[Sequence[Sequence[_SupportsArray[dtype[]]]]], - # Sequence[Sequence[Sequence[Sequence[_SupportsArray[dtype[]]]]]]]]" - np.apply_along_axis(func, axis, data) # type: ignore[arg-type] + # error: No overload variant of "apply_along_axis" matches + # argument types "Callable[[ndarray[Any, Any]], None]", + # "int", "ndarray[Any, Any]" + np.apply_along_axis(func, axis, data) # type: ignore[call-overload] def _index_to_interp_indices(index: Index, method: str) -> np.ndarray: diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py index e775156a6ae2f..d6154e2352c63 100644 --- a/pandas/core/nanops.py +++ b/pandas/core/nanops.py @@ -726,7 +726,9 @@ def nanmean( @bottleneck_switch() -def nanmedian(values, *, axis: AxisInt | None = None, skipna: bool = True, mask=None): +def nanmedian( + values: np.ndarray, *, axis: AxisInt | None = None, skipna: bool = True, mask=None +) -> float | np.ndarray: """ Parameters ---------- @@ -738,7 +740,7 @@ def nanmedian(values, *, axis: AxisInt | None = None, skipna: bool = True, mask= Returns ------- - result : float + result : float | ndarray Unless input is a float array, in which case use the same precision as the input array. @@ -758,7 +760,7 @@ def nanmedian(values, *, axis: AxisInt | None = None, skipna: bool = True, mask= # cases we never need to set NaN to the masked values using_nan_sentinel = values.dtype.kind == "f" and mask is None - def get_median(x, _mask=None): + def get_median(x: np.ndarray, _mask=None): if _mask is None: _mask = notna(x) else: @@ -794,6 +796,8 @@ def get_median(x, _mask=None): notempty = values.size + res: float | np.ndarray + # an array from a frame if values.ndim > 1 and axis is not None: # there's a non-empty array to apply over otherwise numpy raises diff --git a/pandas/io/common.py b/pandas/io/common.py index a76f0cf6dd34d..8da3ca0218983 100644 --- a/pandas/io/common.py +++ b/pandas/io/common.py @@ -910,10 +910,10 @@ def get_handle( or not hasattr(handle, "seekable") ): handle = _IOWrapper(handle) - # error: Argument 1 to "TextIOWrapper" has incompatible type - # "_IOWrapper"; expected "IO[bytes]" + # error: Value of type variable "_BufferT_co" of "TextIOWrapper" cannot + # be "_IOWrapper | BaseBuffer" [type-var] handle = TextIOWrapper( - handle, # type: ignore[arg-type] + handle, # type: ignore[type-var] encoding=ioargs.encoding, errors=errors, newline="", diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index b60392368d944..3fbd4c6f6e26a 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -1038,7 +1038,9 @@ def __call__(self, *args, **kwargs): label_name = label_kw or y data.name = label_name else: - match = is_list_like(label_kw) and len(label_kw) == len(y) + # error: Argument 1 to "len" has incompatible type "Any | bool"; + # expected "Sized" [arg-type] + match = is_list_like(label_kw) and len(label_kw) == len(y) # type: ignore[arg-type] if label_kw and not match: raise ValueError( "label should be list-like and same length as y" diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index 6bb10068bee38..68682344f98ca 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -198,10 +198,7 @@ def _make_plot(self, fig: Figure) -> None: else self.data ) - # error: Argument "data" to "_iter_data" of "MPLPlot" has - # incompatible type "object"; expected "DataFrame | - # dict[Hashable, Series | DataFrame]" - for i, (label, y) in enumerate(self._iter_data(data=data)): # type: ignore[arg-type] + for i, (label, y) in enumerate(self._iter_data(data=data)): ax = self._get_ax(i) kwds = self.kwds.copy() diff --git a/pandas/plotting/_matplotlib/hist.py b/pandas/plotting/_matplotlib/hist.py index 97e510982ab93..1a423ad49c294 100644 --- a/pandas/plotting/_matplotlib/hist.py +++ b/pandas/plotting/_matplotlib/hist.py @@ -137,10 +137,7 @@ def _make_plot(self, fig: Figure) -> None: if self.by is not None else self.data ) - - # error: Argument "data" to "_iter_data" of "MPLPlot" has incompatible - # type "object"; expected "DataFrame | dict[Hashable, Series | DataFrame]" - for i, (label, y) in enumerate(self._iter_data(data=data)): # type: ignore[arg-type] + for i, (label, y) in enumerate(self._iter_data(data=data)): ax = self._get_ax(i) kwds = self.kwds.copy() diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index c28a33069d23f..beca8dea9407d 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -237,14 +237,17 @@ def types_table_metadata(dialect: str): "types", metadata, Column("TextCol", TEXT), - Column("DateCol", date_type), + # error: Cannot infer type argument 1 of "Column" + Column("DateCol", date_type), # type: ignore[misc] Column("IntDateCol", Integer), Column("IntDateOnlyCol", Integer), Column("FloatCol", Float), Column("IntCol", Integer), - Column("BoolCol", bool_type), + # error: Cannot infer type argument 1 of "Column" + Column("BoolCol", bool_type), # type: ignore[misc] Column("IntColWithNull", Integer), - Column("BoolColWithNull", bool_type), + # error: Cannot infer type argument 1 of "Column" + Column("BoolColWithNull", bool_type), # type: ignore[misc] ) return types diff --git a/requirements-dev.txt b/requirements-dev.txt index 00e320e6370ce..69568cf661241 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -54,7 +54,7 @@ moto flask asv>=0.6.1 flake8==7.1.0 -mypy==1.11.2 +mypy==1.13.0 tokenize-rt pre-commit>=4.0.1 gitpython From b4344767e4786149215845e78d44bd6f64a58410 Mon Sep 17 00:00:00 2001 From: DL Lim <69715968+dl-lim@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:49:10 +0000 Subject: [PATCH 043/266] ENH: set `__module__` on `Series` (#60263) --- doc/source/development/contributing_docstring.rst | 4 ++-- pandas/core/indexing.py | 2 +- pandas/core/series.py | 2 ++ pandas/io/formats/info.py | 8 ++++---- pandas/plotting/_core.py | 2 +- pandas/tests/api/test_api.py | 1 + pandas/tests/io/pytables/test_append.py | 2 +- pandas/tests/series/methods/test_argsort.py | 2 +- pandas/tests/series/methods/test_info.py | 4 ++-- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/doc/source/development/contributing_docstring.rst b/doc/source/development/contributing_docstring.rst index e174eea00ca60..59d7957275e15 100644 --- a/doc/source/development/contributing_docstring.rst +++ b/doc/source/development/contributing_docstring.rst @@ -940,7 +940,7 @@ Finally, docstrings can also be appended to with the ``doc`` decorator. In this example, we'll create a parent docstring normally (this is like ``pandas.core.generic.NDFrame``). Then we'll have two children (like -``pandas.core.series.Series`` and ``pandas.DataFrame``). We'll +``pandas.Series`` and ``pandas.DataFrame``). We'll substitute the class names in this docstring. .. code-block:: python @@ -995,5 +995,5 @@ mapping function names to docstrings. Wherever possible, we prefer using ``doc``, since the docstring-writing processes is slightly closer to normal. See ``pandas.core.generic.NDFrame.fillna`` for an example template, and -``pandas.core.series.Series.fillna`` and ``pandas.core.generic.frame.fillna`` +``pandas.Series.fillna`` and ``pandas.core.generic.frame.fillna`` for the filled versions. diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 975e7ad135ba7..0d6d7e68f58a4 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -212,7 +212,7 @@ def iloc(self) -> _iLocIndexer: With a scalar integer. >>> type(df.iloc[0]) - + >>> df.iloc[0] a 1 b 2 diff --git a/pandas/core/series.py b/pandas/core/series.py index 1d601f36d604a..35b576da87ed7 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -50,6 +50,7 @@ Substitution, deprecate_nonkeyword_arguments, doc, + set_module, ) from pandas.util._validators import ( validate_ascending, @@ -229,6 +230,7 @@ # error: Cannot override final attribute "size" (previously declared in base # class "NDFrame") # definition in base class "NDFrame" +@set_module("pandas") class Series(base.IndexOpsMixin, NDFrame): # type: ignore[misc] """ One-dimensional ndarray with axis labels (including time series). diff --git a/pandas/io/formats/info.py b/pandas/io/formats/info.py index 469dcfb76ba0b..b4c6ff8792d52 100644 --- a/pandas/io/formats/info.py +++ b/pandas/io/formats/info.py @@ -165,7 +165,7 @@ >>> text_values = ['alpha', 'beta', 'gamma', 'delta', 'epsilon'] >>> s = pd.Series(text_values, index=int_values) >>> s.info() - + Index: 5 entries, 1 to 5 Series name: None Non-Null Count Dtype @@ -177,7 +177,7 @@ Prints a summary excluding information about its values: >>> s.info(verbose=False) - + Index: 5 entries, 1 to 5 dtypes: object(1) memory usage: 80.0+ bytes @@ -200,7 +200,7 @@ >>> random_strings_array = np.random.choice(['a', 'b', 'c'], 10 ** 6) >>> s = pd.Series(np.random.choice(['a', 'b', 'c'], 10 ** 6)) >>> s.info() - + RangeIndex: 1000000 entries, 0 to 999999 Series name: None Non-Null Count Dtype @@ -210,7 +210,7 @@ memory usage: 7.6+ MB >>> s.info(memory_usage='deep') - + RangeIndex: 1000000 entries, 0 to 999999 Series name: None Non-Null Count Dtype diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 3fbd4c6f6e26a..fbf9009cedc40 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -428,7 +428,7 @@ def hist_frame( >>> boxplot = df.boxplot(column=['Col1', 'Col2'], by='X', ... return_type='axes') >>> type(boxplot) - + If ``return_type`` is `None`, a NumPy array of axes with the same shape as ``layout`` is returned: diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 842fa1a151267..25285a451bb3f 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -416,3 +416,4 @@ def test_set_module(): assert pd.Period.__module__ == "pandas" assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" + assert pd.Series.__module__ == "pandas" diff --git a/pandas/tests/io/pytables/test_append.py b/pandas/tests/io/pytables/test_append.py index d3b4bb0ea6c72..47658c0eb9012 100644 --- a/pandas/tests/io/pytables/test_append.py +++ b/pandas/tests/io/pytables/test_append.py @@ -795,7 +795,7 @@ def test_append_raise(setup_path): # series directly msg = re.escape( "cannot properly create the storer for: " - "[group->df,value->]" + "[group->df,value->]" ) with pytest.raises(TypeError, match=msg): store.append("df", Series(np.arange(10))) diff --git a/pandas/tests/series/methods/test_argsort.py b/pandas/tests/series/methods/test_argsort.py index c1082c06ce307..019efe8683347 100644 --- a/pandas/tests/series/methods/test_argsort.py +++ b/pandas/tests/series/methods/test_argsort.py @@ -66,7 +66,7 @@ def test_argsort_stable(self): tm.assert_series_equal(qindexer.astype(np.intp), Series(qexpected)) msg = ( r"ndarray Expected type , " - r"found instead" + r"found instead" ) with pytest.raises(AssertionError, match=msg): tm.assert_numpy_array_equal(qindexer, mindexer) diff --git a/pandas/tests/series/methods/test_info.py b/pandas/tests/series/methods/test_info.py index e2831fb80b7a0..88f2cf384fc79 100644 --- a/pandas/tests/series/methods/test_info.py +++ b/pandas/tests/series/methods/test_info.py @@ -56,7 +56,7 @@ def test_info_series( expected = textwrap.dedent( """\ - + MultiIndex: 10 entries, ('foo', 'one') to ('qux', 'three') """ ) @@ -87,7 +87,7 @@ def test_info_memory(): memory_bytes = float(s.memory_usage()) expected = textwrap.dedent( f"""\ - + RangeIndex: 2 entries, 0 to 1 Series name: None Non-Null Count Dtype From d3c595e83f442a61cedeb6656bf9f6ee7dc1d2df Mon Sep 17 00:00:00 2001 From: Espoir Murhabazi Date: Mon, 11 Nov 2024 10:51:29 +0000 Subject: [PATCH 044/266] ENH: Set __module__ on StringDtype (#60261) --- pandas/core/arrays/string_.py | 6 +++++- pandas/tests/api/test_api.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index de129df2575d3..9b1f986a7158e 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -28,7 +28,10 @@ pa_version_under10p1, ) from pandas.compat.numpy import function as nv -from pandas.util._decorators import doc +from pandas.util._decorators import ( + doc, + set_module, +) from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.base import ( @@ -86,6 +89,7 @@ from pandas import Series +@set_module("pandas") @register_extension_dtype class StringDtype(StorageExtensionDtype): """ diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 25285a451bb3f..00b0236c236c0 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -405,6 +405,7 @@ def test_set_module(): assert pd.IntervalDtype.__module__ == "pandas" assert pd.SparseDtype.__module__ == "pandas" assert pd.ArrowDtype.__module__ == "pandas" + assert pd.StringDtype.__module__ == "pandas" assert pd.Index.__module__ == "pandas" assert pd.CategoricalIndex.__module__ == "pandas" assert pd.DatetimeIndex.__module__ == "pandas" From ce2570f9a936ea90c819005fb151787ddacd91bf Mon Sep 17 00:00:00 2001 From: Shing Chan Date: Mon, 11 Nov 2024 13:33:48 +0000 Subject: [PATCH 045/266] ENH: set __module__ on `date_range` and `bdate_range` (#60264) --- pandas/core/indexes/datetimes.py | 2 ++ pandas/tests/api/test_api.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index b3d9c3bc78a66..9adbaadbdcdc8 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -816,6 +816,7 @@ def indexer_between_time( return mask.nonzero()[0] +@set_module("pandas") def date_range( start=None, end=None, @@ -1020,6 +1021,7 @@ def date_range( return DatetimeIndex._simple_new(dtarr, name=name) +@set_module("pandas") def bdate_range( start=None, end=None, diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 00b0236c236c0..54e0453ed569b 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -418,3 +418,5 @@ def test_set_module(): assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" assert pd.Series.__module__ == "pandas" + assert pd.date_range.__module__ == "pandas" + assert pd.bdate_range.__module__ == "pandas" From a11fd2e194db1303822462c005363188b4183aad Mon Sep 17 00:00:00 2001 From: ofsouzap Date: Mon, 11 Nov 2024 14:48:24 +0000 Subject: [PATCH 046/266] ENH: set __module__ on NamedAgg / SeriesGroupBy / DataFrameGroupBy (`pandas.core.groupby.generic` classes) (#60268) --- pandas/core/groupby/generic.py | 4 ++++ pandas/tests/api/test_api.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index f076f8d79f104..5ba382bf66bb7 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -32,6 +32,7 @@ Appender, Substitution, doc, + set_module, ) from pandas.util._exceptions import find_stack_level @@ -108,6 +109,7 @@ ScalarResult = TypeVar("ScalarResult") +@set_module("pandas") class NamedAgg(NamedTuple): """ Helper for column specific aggregation with control over output column names. @@ -142,6 +144,7 @@ class NamedAgg(NamedTuple): aggfunc: AggScalar +@set_module("pandas.api.typing") class SeriesGroupBy(GroupBy[Series]): def _wrap_agged_manager(self, mgr: Manager) -> Series: out = self.obj._constructor_from_mgr(mgr, axes=mgr.axes) @@ -1555,6 +1558,7 @@ def unique(self) -> Series: return result +@set_module("pandas.api.typing") class DataFrameGroupBy(GroupBy[DataFrame]): _agg_examples_doc = dedent( """ diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 54e0453ed569b..2a96ef35c981c 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -420,3 +420,6 @@ def test_set_module(): assert pd.Series.__module__ == "pandas" assert pd.date_range.__module__ == "pandas" assert pd.bdate_range.__module__ == "pandas" + assert pd.NamedAgg.__module__ == "pandas" + assert api.typing.SeriesGroupBy.__module__ == "pandas.api.typing" + assert api.typing.DataFrameGroupBy.__module__ == "pandas.api.typing" From 177b952277f451866feb1a67706ec16c1f1bbc06 Mon Sep 17 00:00:00 2001 From: Shing Chan Date: Mon, 11 Nov 2024 14:50:51 +0000 Subject: [PATCH 047/266] ENH: set __module__ on `timedelta_range` (#60267) --- pandas/core/indexes/timedeltas.py | 1 + pandas/tests/api/test_api.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 6bbe86816d81f..fa3de46621643 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -237,6 +237,7 @@ def inferred_type(self) -> str: return "timedelta64" +@set_module("pandas") def timedelta_range( start=None, end=None, diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 2a96ef35c981c..acb4809791394 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -420,6 +420,7 @@ def test_set_module(): assert pd.Series.__module__ == "pandas" assert pd.date_range.__module__ == "pandas" assert pd.bdate_range.__module__ == "pandas" + assert pd.timedelta_range.__module__ == "pandas" assert pd.NamedAgg.__module__ == "pandas" assert api.typing.SeriesGroupBy.__module__ == "pandas.api.typing" assert api.typing.DataFrameGroupBy.__module__ == "pandas.api.typing" From d770a80043ef5000790fb877d8096e85cad11a75 Mon Sep 17 00:00:00 2001 From: Shing Chan Date: Mon, 11 Nov 2024 15:45:19 +0000 Subject: [PATCH 048/266] ENH: set __module__ on `read_*` functions in readers.py (#60265) --- pandas/io/parsers/readers.py | 8 +++++++- pandas/tests/api/test_api.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pandas/io/parsers/readers.py b/pandas/io/parsers/readers.py index ffc2690a5efdf..54877017f76fc 100644 --- a/pandas/io/parsers/readers.py +++ b/pandas/io/parsers/readers.py @@ -32,7 +32,10 @@ AbstractMethodError, ParserWarning, ) -from pandas.util._decorators import Appender +from pandas.util._decorators import ( + Appender, + set_module, +) from pandas.util._exceptions import find_stack_level from pandas.util._validators import check_dtype_backend @@ -771,6 +774,7 @@ def read_csv( % "filepath_or_buffer", ) ) +@set_module("pandas") def read_csv( filepath_or_buffer: FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str], *, @@ -906,6 +910,7 @@ def read_table( % "filepath_or_buffer", ) ) +@set_module("pandas") def read_table( filepath_or_buffer: FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str], *, @@ -1023,6 +1028,7 @@ def read_fwf( ) -> DataFrame: ... +@set_module("pandas") def read_fwf( filepath_or_buffer: FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str], *, diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index acb4809791394..7f738108c25f4 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -417,6 +417,9 @@ def test_set_module(): assert pd.Period.__module__ == "pandas" assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" + assert pd.read_csv.__module__ == "pandas" + assert pd.read_table.__module__ == "pandas" + assert pd.read_fwf.__module__ == "pandas" assert pd.Series.__module__ == "pandas" assert pd.date_range.__module__ == "pandas" assert pd.bdate_range.__module__ == "pandas" From e531732fd934d95463e946f30eeea9fd7270a1d5 Mon Sep 17 00:00:00 2001 From: Shing Chan Date: Mon, 11 Nov 2024 15:46:38 +0000 Subject: [PATCH 049/266] ENH: set __module__ on merge functions in merge.py (#60266) --- pandas/core/reshape/merge.py | 8 +++++++- pandas/tests/api/test_api.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py index 0ca8661ad3b5c..6f9bb8cb24f43 100644 --- a/pandas/core/reshape/merge.py +++ b/pandas/core/reshape/merge.py @@ -39,7 +39,10 @@ npt, ) from pandas.errors import MergeError -from pandas.util._decorators import cache_readonly +from pandas.util._decorators import ( + cache_readonly, + set_module, +) from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.base import ExtensionDtype @@ -138,6 +141,7 @@ _known = (np.ndarray, ExtensionArray, Index, ABCSeries) +@set_module("pandas") def merge( left: DataFrame | Series, right: DataFrame | Series, @@ -502,6 +506,7 @@ def _groupby_and_merge( return result, lby +@set_module("pandas") def merge_ordered( left: DataFrame | Series, right: DataFrame | Series, @@ -645,6 +650,7 @@ def _merger(x, y) -> DataFrame: return result +@set_module("pandas") def merge_asof( left: DataFrame | Series, right: DataFrame | Series, diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 7f738108c25f4..8209ff86c62f1 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -417,6 +417,9 @@ def test_set_module(): assert pd.Period.__module__ == "pandas" assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" + assert pd.merge.__module__ == "pandas" + assert pd.merge_ordered.__module__ == "pandas" + assert pd.merge_asof.__module__ == "pandas" assert pd.read_csv.__module__ == "pandas" assert pd.read_table.__module__ == "pandas" assert pd.read_fwf.__module__ == "pandas" From 156e67e1c6f32d2a794b61604a9f89d411e5e915 Mon Sep 17 00:00:00 2001 From: Maria Ivanova Date: Mon, 11 Nov 2024 16:18:12 +0000 Subject: [PATCH 050/266] Fix docstring timestamps (Issue #59458) (#59688) --------- Co-authored-by: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> --- ci/code_checks.sh | 2 - pandas/_libs/tslibs/timestamps.pxd | 2 +- pandas/_libs/tslibs/timestamps.pyx | 114 ++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 37 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 093e7a8e26854..253c585494910 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -81,10 +81,8 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timedelta.resolution PR02" \ -i "pandas.Timestamp.max PR02" \ -i "pandas.Timestamp.min PR02" \ - -i "pandas.Timestamp.nanosecond GL08" \ -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ - -i "pandas.Timestamp.year GL08" \ -i "pandas.api.types.is_re_compilable PR07,SA01" \ -i "pandas.api.types.pandas_dtype PR07,RT03,SA01" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ diff --git a/pandas/_libs/tslibs/timestamps.pxd b/pandas/_libs/tslibs/timestamps.pxd index bd73c713f6c04..c5ec92fabc7f8 100644 --- a/pandas/_libs/tslibs/timestamps.pxd +++ b/pandas/_libs/tslibs/timestamps.pxd @@ -21,7 +21,7 @@ cdef _Timestamp create_timestamp_from_ts(int64_t value, cdef class _Timestamp(ABCTimestamp): cdef readonly: - int64_t _value, nanosecond, year + int64_t _value, _nanosecond, _year NPY_DATETIMEUNIT _creso cdef bint _get_start_end_field(self, str field, freq) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 1ab34da7ab53f..a3429fc840347 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -162,8 +162,8 @@ cdef _Timestamp create_timestamp_from_ts( dts.sec, dts.us, tz, fold=fold) ts_base._value = value - ts_base.year = dts.year - ts_base.nanosecond = dts.ps // 1000 + ts_base._year = dts.year + ts_base._nanosecond = dts.ps // 1000 ts_base._creso = reso return ts_base @@ -356,9 +356,9 @@ cdef class _Timestamp(ABCTimestamp): # ----------------------------------------------------------------- def __hash__(_Timestamp self): - if self.nanosecond: + if self._nanosecond: return hash(self._value) - if not (1 <= self.year <= 9999): + if not (1 <= self._year <= 9999): # out of bounds for pydatetime return hash(self._value) if self.fold: @@ -376,7 +376,7 @@ cdef class _Timestamp(ABCTimestamp): elif cnp.is_datetime64_object(other): ots = Timestamp(other) elif PyDateTime_Check(other): - if self.nanosecond == 0: + if self._nanosecond == 0: val = self.to_pydatetime() return PyObject_RichCompareBool(val, other, op) @@ -455,7 +455,7 @@ cdef class _Timestamp(ABCTimestamp): if not self._can_compare(other): return NotImplemented - if self.nanosecond == 0: + if self._nanosecond == 0: return PyObject_RichCompareBool(dtval, other, op) # otherwise we have dtval < self @@ -464,9 +464,9 @@ cdef class _Timestamp(ABCTimestamp): if op == Py_EQ: return False if op == Py_LE or op == Py_LT: - return self.year <= other.year + return self._year <= other.year if op == Py_GE or op == Py_GT: - return self.year >= other.year + return self._year >= other.year cdef bint _can_compare(self, datetime other): if self.tzinfo is not None: @@ -607,7 +607,7 @@ cdef class _Timestamp(ABCTimestamp): if own_tz is not None and not is_utc(own_tz): pydatetime_to_dtstruct(self, &dts) - val = npy_datetimestruct_to_datetime(self._creso, &dts) + self.nanosecond + val = npy_datetimestruct_to_datetime(self._creso, &dts) + self._nanosecond else: val = self._value return val @@ -899,7 +899,7 @@ cdef class _Timestamp(ABCTimestamp): >>> ts.is_leap_year True """ - return bool(ccalendar.is_leapyear(self.year)) + return bool(ccalendar.is_leapyear(self._year)) @property def day_of_week(self) -> int: @@ -943,7 +943,7 @@ cdef class _Timestamp(ABCTimestamp): >>> ts.day_of_year 74 """ - return ccalendar.get_day_of_year(self.year, self.month, self.day) + return ccalendar.get_day_of_year(self._year, self.month, self.day) @property def quarter(self) -> int: @@ -1030,6 +1030,29 @@ cdef class _Timestamp(ABCTimestamp): """ return super().fold + @property + def year(self) -> int: + """ + Return the year of the Timestamp. + + Returns + ------- + int + The year of the Timestamp. + + See Also + -------- + Timestamp.month : Return the month of the Timestamp. + Timestamp.day : Return the day of the Timestamp. + + Examples + -------- + >>> ts = pd.Timestamp("2024-08-31 16:16:30") + >>> ts.year + 2024 + """ + return self._year + @property def month(self) -> int: """ @@ -1145,6 +1168,29 @@ cdef class _Timestamp(ABCTimestamp): """ return super().microsecond + @property + def nanosecond(self) -> int: + """ + Return the nanosecond of the Timestamp. + + Returns + ------- + int + The nanosecond of the Timestamp. + + See Also + -------- + Timestamp.second : Return the second of the Timestamp. + Timestamp.microsecond : Return the microsecond of the Timestamp. + + Examples + -------- + >>> ts = pd.Timestamp("2024-08-31 16:16:30.230400015") + >>> ts.nanosecond + 15 + """ + return self._nanosecond + @property def week(self) -> int: """ @@ -1165,7 +1211,7 @@ cdef class _Timestamp(ABCTimestamp): >>> ts.week 11 """ - return ccalendar.get_week_of_year(self.year, self.month, self.day) + return ccalendar.get_week_of_year(self._year, self.month, self.day) @property def days_in_month(self) -> int: @@ -1187,7 +1233,7 @@ cdef class _Timestamp(ABCTimestamp): >>> ts.days_in_month 31 """ - return ccalendar.get_days_in_month(self.year, self.month) + return ccalendar.get_days_in_month(self._year, self.month) # ----------------------------------------------------------------- # Transformation Methods @@ -1261,7 +1307,7 @@ cdef class _Timestamp(ABCTimestamp): The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn'. By default, the fractional part is omitted if self.microsecond == 0 - and self.nanosecond == 0. + and self._nanosecond == 0. If self.tzinfo is not None, the UTC offset is also attached, giving giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn+HH:MM'. @@ -1297,9 +1343,9 @@ cdef class _Timestamp(ABCTimestamp): base_ts = "microseconds" if timespec == "nanoseconds" else timespec base = super(_Timestamp, self).isoformat(sep=sep, timespec=base_ts) # We need to replace the fake year 1970 with our real year - base = f"{self.year:04d}-" + base.split("-", 1)[1] + base = f"{self._year:04d}-" + base.split("-", 1)[1] - if self.nanosecond == 0 and timespec != "nanoseconds": + if self._nanosecond == 0 and timespec != "nanoseconds": return base if self.tzinfo is not None: @@ -1307,11 +1353,11 @@ cdef class _Timestamp(ABCTimestamp): else: base1, base2 = base, "" - if timespec == "nanoseconds" or (timespec == "auto" and self.nanosecond): + if timespec == "nanoseconds" or (timespec == "auto" and self._nanosecond): if self.microsecond or timespec == "nanoseconds": - base1 += f"{self.nanosecond:03d}" + base1 += f"{self._nanosecond:03d}" else: - base1 += f".{self.nanosecond:09d}" + base1 += f".{self._nanosecond:09d}" return base1 + base2 @@ -1345,14 +1391,14 @@ cdef class _Timestamp(ABCTimestamp): def _date_repr(self) -> str: # Ideal here would be self.strftime("%Y-%m-%d"), but # the datetime strftime() methods require year >= 1900 and is slower - return f"{self.year}-{self.month:02d}-{self.day:02d}" + return f"{self._year}-{self.month:02d}-{self.day:02d}" @property def _time_repr(self) -> str: result = f"{self.hour:02d}:{self.minute:02d}:{self.second:02d}" - if self.nanosecond != 0: - result += f".{self.nanosecond + 1000 * self.microsecond:09d}" + if self._nanosecond != 0: + result += f".{self._nanosecond + 1000 * self.microsecond:09d}" elif self.microsecond != 0: result += f".{self.microsecond:06d}" @@ -1516,11 +1562,11 @@ cdef class _Timestamp(ABCTimestamp): >>> pd.NaT.to_pydatetime() NaT """ - if self.nanosecond != 0 and warn: + if self._nanosecond != 0 and warn: warnings.warn("Discarding nonzero nanoseconds in conversion.", UserWarning, stacklevel=find_stack_level()) - return datetime(self.year, self.month, self.day, + return datetime(self._year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, self.tzinfo, fold=self.fold) @@ -1999,7 +2045,7 @@ class Timestamp(_Timestamp): '2020-03-14 15:32:52' """ try: - _dt = datetime(self.year, self.month, self.day, + _dt = datetime(self._year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, self.tzinfo, fold=self.fold) except ValueError as err: @@ -2042,7 +2088,7 @@ class Timestamp(_Timestamp): 'Sun Jan 1 10:00:00 2023' """ try: - _dt = datetime(self.year, self.month, self.day, + _dt = datetime(self._year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, self.tzinfo, fold=self.fold) except ValueError as err: @@ -2082,7 +2128,7 @@ class Timestamp(_Timestamp): datetime.date(2023, 1, 1) """ try: - _dt = dt.date(self.year, self.month, self.day) + _dt = dt.date(self._year, self.month, self.day) except ValueError as err: raise NotImplementedError( "date not yet supported on Timestamps which " @@ -2131,7 +2177,7 @@ class Timestamp(_Timestamp): datetime.IsoCalendarDate(year=2022, week=52, weekday=7) """ try: - _dt = datetime(self.year, self.month, self.day, + _dt = datetime(self._year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, self.tzinfo, fold=self.fold) except ValueError as err: @@ -2273,7 +2319,7 @@ class Timestamp(_Timestamp): tm_hour=10, tm_min=0, tm_sec=0, tm_wday=6, tm_yday=1, tm_isdst=-1) """ try: - _dt = datetime(self.year, self.month, self.day, + _dt = datetime(self._year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, self.tzinfo, fold=self.fold) except ValueError as err: @@ -2334,7 +2380,7 @@ class Timestamp(_Timestamp): 738521 """ try: - _dt = datetime(self.year, self.month, self.day, + _dt = datetime(self._year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, self.tzinfo, fold=self.fold) except ValueError as err: @@ -3223,7 +3269,7 @@ default 'raise' # setup components pandas_datetime_to_datetimestruct(value, self._creso, &dts) - dts.ps = self.nanosecond * 1000 + dts.ps = self._nanosecond * 1000 # replace def validate(k, v): @@ -3313,7 +3359,7 @@ default 'raise' >>> ts.to_julian_date() 2458923.147824074 """ - year = self.year + year = self._year month = self.month day = self.day if month <= 2: @@ -3330,7 +3376,7 @@ default 'raise' self.minute / 60.0 + self.second / 3600.0 + self.microsecond / 3600.0 / 1e+6 + - self.nanosecond / 3600.0 / 1e+9 + self._nanosecond / 3600.0 / 1e+9 ) / 24.0) def isoweekday(self): @@ -3381,7 +3427,7 @@ default 'raise' """ # same as super().weekday(), but that breaks because of how # we have overridden year, see note in create_timestamp_from_ts - return ccalendar.dayofweek(self.year, self.month, self.day) + return ccalendar.dayofweek(self._year, self.month, self.day) # Aliases From cccf1e6704de3fe0455b4aa5b7226d98f51de37d Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 12 Nov 2024 01:11:11 +0530 Subject: [PATCH 051/266] DOC: fix SA01 for pandas.errors.DataError (#60279) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 253c585494910..ba8c3fbb18fe7 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -120,7 +120,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.var SA01" \ -i "pandas.errors.AttributeConflictWarning SA01" \ -i "pandas.errors.ChainedAssignmentError SA01" \ - -i "pandas.errors.DataError SA01" \ -i "pandas.errors.DuplicateLabelError SA01" \ -i "pandas.errors.IntCastingNaNError SA01" \ -i "pandas.errors.InvalidIndexError SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 0aaee1ec177ee..cacbfb49c311f 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -429,6 +429,11 @@ class DataError(Exception): For example, calling ``ohlc`` on a non-numerical column or a function on a rolling window. + See Also + -------- + Series.rolling : Provide rolling window calculations on Series object. + DataFrame.rolling : Provide rolling window calculations on DataFrame object. + Examples -------- >>> ser = pd.Series(["a", "b", "c"]) From f307a0a3615d93c2177f6581133bdb541e12a93c Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Mon, 11 Nov 2024 20:42:28 +0100 Subject: [PATCH 052/266] ENH (string dtype): convert string_view columns to future string dtype instead of object dtype in Parquet/Feather IO (#60235) * ENH (string dtype): convert string_view columns to future string dtype instead of object dtype in Parquet IO * move test to feather * fixup --- pandas/io/_util.py | 9 +++++++-- pandas/tests/io/test_feather.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pandas/io/_util.py b/pandas/io/_util.py index a1c3318f04466..9a8c87a738d4c 100644 --- a/pandas/io/_util.py +++ b/pandas/io/_util.py @@ -4,6 +4,7 @@ import numpy as np +from pandas.compat import pa_version_under18p0 from pandas.compat._optional import import_optional_dependency import pandas as pd @@ -35,7 +36,11 @@ def _arrow_dtype_mapping() -> dict: def arrow_string_types_mapper() -> Callable: pa = import_optional_dependency("pyarrow") - return { + mapping = { pa.string(): pd.StringDtype(na_value=np.nan), pa.large_string(): pd.StringDtype(na_value=np.nan), - }.get + } + if not pa_version_under18p0: + mapping[pa.string_view()] = pd.StringDtype(na_value=np.nan) + + return mapping.get diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 8ae2033faab4f..69354066dd5ef 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -6,6 +6,8 @@ import numpy as np import pytest +from pandas.compat.pyarrow import pa_version_under18p0 + import pandas as pd import pandas._testing as tm @@ -249,6 +251,24 @@ def test_string_inference(self, tmp_path): ) tm.assert_frame_equal(result, expected) + @pytest.mark.skipif(pa_version_under18p0, reason="not supported before 18.0") + def test_string_inference_string_view_type(self, tmp_path): + # GH#54798 + import pyarrow as pa + from pyarrow import feather + + path = tmp_path / "string_view.parquet" + table = pa.table({"a": pa.array([None, "b", "c"], pa.string_view())}) + feather.write_feather(table, path) + + with pd.option_context("future.infer_string", True): + result = read_feather(path) + + expected = pd.DataFrame( + data={"a": [None, "b", "c"]}, dtype=pd.StringDtype(na_value=np.nan) + ) + tm.assert_frame_equal(result, expected) + def test_out_of_bounds_datetime_to_feather(self): # GH#47832 df = pd.DataFrame( From f9f72d144c38c385f97b27455366d19e25137be2 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 12 Nov 2024 02:43:54 +0530 Subject: [PATCH 053/266] DOC: fix SA01,ES01 for groups method of groupby and resampler (#60252) --- ci/code_checks.sh | 3 --- pandas/core/groupby/groupby.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index ba8c3fbb18fe7..fae1a7abba6a8 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -92,14 +92,12 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \ - -i "pandas.core.groupby.DataFrameGroupBy.groups SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.indices SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.nth PR02" \ -i "pandas.core.groupby.DataFrameGroupBy.nunique SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.DataFrameGroupBy.sem SA01" \ -i "pandas.core.groupby.SeriesGroupBy.get_group RT03,SA01" \ - -i "pandas.core.groupby.SeriesGroupBy.groups SA01" \ -i "pandas.core.groupby.SeriesGroupBy.indices SA01" \ -i "pandas.core.groupby.SeriesGroupBy.is_monotonic_decreasing SA01" \ -i "pandas.core.groupby.SeriesGroupBy.is_monotonic_increasing SA01" \ @@ -107,7 +105,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.sem SA01" \ -i "pandas.core.resample.Resampler.get_group RT03,SA01" \ - -i "pandas.core.resample.Resampler.groups SA01" \ -i "pandas.core.resample.Resampler.indices SA01" \ -i "pandas.core.resample.Resampler.max PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.mean SA01" \ diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 8f2e5d2ee09d4..9c30132347111 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -435,6 +435,20 @@ def groups(self) -> dict[Hashable, Index]: """ Dict {group name -> group labels}. + This property provides a dictionary representation of the groupings formed + during a groupby operation, where each key represents a unique group value from + the specified column(s), and each value is a list of index labels + that belong to that group. + + See Also + -------- + core.groupby.DataFrameGroupBy.get_group : Retrieve group from a + ``DataFrameGroupBy`` object with provided name. + core.groupby.SeriesGroupBy.get_group : Retrieve group from a + ``SeriesGroupBy`` object with provided name. + core.resample.Resampler.get_group : Retrieve group from a + ``Resampler`` object with provided name. + Examples -------- From 22df68eb1c3a5ab3b75b8e23531d9181798374fe Mon Sep 17 00:00:00 2001 From: Kevin Amparado <109636487+KevsterAmp@users.noreply.github.com> Date: Tue, 12 Nov 2024 05:16:00 +0800 Subject: [PATCH 054/266] BUG: `read_csv` with chained fsspec TAR file and `compression="infer"` (#60100) --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/common.py | 3 +++ pandas/tests/io/data/tar/test-csv.tar | Bin 0 -> 10240 bytes pandas/tests/io/test_common.py | 14 ++++++++++++++ 4 files changed, 18 insertions(+) create mode 100644 pandas/tests/io/data/tar/test-csv.tar diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 89bc942cb7250..de69166b8c196 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -784,6 +784,7 @@ Other - Bug in :meth:`Series.dt` methods in :class:`ArrowDtype` that were returning incorrect values. (:issue:`57355`) - Bug in :meth:`Series.rank` that doesn't preserve missing values for nullable integers when ``na_option='keep'``. (:issue:`56976`) - Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` inconsistently replacing matching instances when ``regex=True`` and missing values are present. (:issue:`56599`) +- Bug in :meth:`read_csv` where chained fsspec TAR file and ``compression="infer"`` fails with ``tarfile.ReadError`` (:issue:`60028`) - Bug in Dataframe Interchange Protocol implementation was returning incorrect results for data buffers' associated dtype, for string and datetime columns (:issue:`54781`) - Bug in ``Series.list`` methods not preserving the original :class:`Index`. (:issue:`58425`) diff --git a/pandas/io/common.py b/pandas/io/common.py index 8da3ca0218983..e0076eb486976 100644 --- a/pandas/io/common.py +++ b/pandas/io/common.py @@ -584,6 +584,9 @@ def infer_compression( # Infer compression if compression == "infer": # Convert all path types (e.g. pathlib.Path) to strings + if isinstance(filepath_or_buffer, str) and "::" in filepath_or_buffer: + # chained URLs contain :: + filepath_or_buffer = filepath_or_buffer.split("::")[0] filepath_or_buffer = stringify_path(filepath_or_buffer, convert_file_like=True) if not isinstance(filepath_or_buffer, str): # Cannot infer compression of a buffer, assume no compression diff --git a/pandas/tests/io/data/tar/test-csv.tar b/pandas/tests/io/data/tar/test-csv.tar new file mode 100644 index 0000000000000000000000000000000000000000..c3b3091348426791f9bb09e2cbd8196465074c49 GIT binary patch literal 10240 zcmeIy!3u&f9LMpUeTtqy_ur;VBIww$SCCo|(Ir>(_-P`9P?sQ8|uSTYwP;G_K4D=jTp6fjPf;uqPIF$*QWj8^<0)_xwypBC9K6; zZFE^mnfhkF%xy9kgE{|a40TNR^?gi(Hq?ddGVY7K%er~byjSA9Xepd|cWX`0%yGKpQeX`0g&0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL SKmY**5I_I{1Q0;rXMrbBPb;PX literal 0 HcmV?d00001 diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 10e3af601b7ef..4f3f613f71542 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -25,6 +25,7 @@ WASM, is_platform_windows, ) +import pandas.util._test_decorators as td import pandas as pd import pandas._testing as tm @@ -642,6 +643,19 @@ def close(self): handles.created_handles.append(TestError()) +@td.skip_if_no("fsspec", min_version="2023.1.0") +@pytest.mark.parametrize("compression", [None, "infer"]) +def test_read_csv_chained_url_no_error(compression): + # GH 60100 + tar_file_path = "pandas/tests/io/data/tar/test-csv.tar" + chained_file_url = f"tar://test.csv::file://{tar_file_path}" + + result = pd.read_csv(chained_file_url, compression=compression, sep=";") + expected = pd.DataFrame({"1": {0: 3}, "2": {0: 4}}) + + tm.assert_frame_equal(expected, result) + + @pytest.mark.parametrize( "reader", [ From 5f23aced2f97f2ed481deda4eaeeb049d6c7debe Mon Sep 17 00:00:00 2001 From: SiemBerhane <65135505+SiemBerhane@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:41:44 +0000 Subject: [PATCH 055/266] ENH: set __module__ on isna and notna (#60271) --- pandas/core/dtypes/missing.py | 3 +++ pandas/tests/api/test_api.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pandas/core/dtypes/missing.py b/pandas/core/dtypes/missing.py index b9cd6ae2f13e8..f20ca44728664 100644 --- a/pandas/core/dtypes/missing.py +++ b/pandas/core/dtypes/missing.py @@ -19,6 +19,7 @@ NaT, iNaT, ) +from pandas.util._decorators import set_module from pandas.core.dtypes.common import ( DT64NS_DTYPE, @@ -93,6 +94,7 @@ def isna( def isna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: ... +@set_module("pandas") def isna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: """ Detect missing values for an array-like object. @@ -307,6 +309,7 @@ def notna( def notna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: ... +@set_module("pandas") def notna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: """ Detect non-missing values for an array-like object. diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 8209ff86c62f1..75f9958b16286 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -417,6 +417,8 @@ def test_set_module(): assert pd.Period.__module__ == "pandas" assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" + assert pd.isna.__module__ == "pandas" + assert pd.notna.__module__ == "pandas" assert pd.merge.__module__ == "pandas" assert pd.merge_ordered.__module__ == "pandas" assert pd.merge_asof.__module__ == "pandas" From 6bdb32b5ff083ae742ec6f58c8689e77d7e959af Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 12 Nov 2024 19:09:37 +0100 Subject: [PATCH 056/266] CI: Add Windows wheels for the free-threaded build (#60146) Co-authored-by: Thomas Li <47963215+lithomas1@users.noreply.github.com> Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- .gitattributes | 3 ++- .github/workflows/unit-tests.yml | 4 ++-- .github/workflows/wheels.yml | 18 ------------------ MANIFEST.in | 2 ++ pyproject.toml | 15 +++++++-------- scripts/cibw_before_build.sh | 6 +++--- scripts/cibw_before_build_windows.sh | 13 +++++++++++++ scripts/cibw_before_test_windows.sh | 5 +++++ 8 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 scripts/cibw_before_build_windows.sh create mode 100644 scripts/cibw_before_test_windows.sh diff --git a/.gitattributes b/.gitattributes index b3d70ca8b24fb..f77da2339b20f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -85,4 +85,5 @@ pandas/tests/io/parser/data export-ignore # Include cibw script in sdist since it's needed for building wheels scripts/cibw_before_build.sh -export-ignore -scripts/cibw_before_test.sh -export-ignore +scripts/cibw_before_build_windows.sh -export-ignore +scripts/cibw_before_test_windows.sh -export-ignore diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 212ce7441dfab..07fb0c19262a1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -387,8 +387,8 @@ jobs: - name: Build Environment run: | python --version - python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.2.1 meson-python==0.13.1 - python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy cython + python -m pip install --upgrade pip setuptools wheel numpy meson[ninja]==1.2.1 meson-python==0.13.1 + python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython python -m pip install versioneer[toml] python -m pip install python-dateutil pytz tzdata hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0 pytest-cov python -m pip install -ve . --no-build-isolation --no-index --no-deps -Csetup-args="--werror" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4bff9e7e090da..354402c572ade 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -111,10 +111,6 @@ jobs: - buildplat: [ubuntu-22.04, pyodide_wasm32] python: ["cp312", "3.12"] cibw_build_frontend: 'build' - # TODO: Build free-threaded wheels for Windows - exclude: - - buildplat: [windows-2022, win_amd64] - python: ["cp313t", "3.13"] env: IS_PUSH: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} @@ -181,20 +177,6 @@ jobs: shell: bash -el {0} run: for whl in $(ls wheelhouse); do wheel unpack wheelhouse/$whl -d /tmp; done - # Testing on windowsservercore instead of GHA runner to fail on missing DLLs - - name: Test Windows Wheels - if: ${{ matrix.buildplat[1] == 'win_amd64' }} - shell: pwsh - run: | - $TST_CMD = @" - python -m pip install hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0; - python -m pip install `$(Get-Item pandas\wheelhouse\*.whl); - python -c `'import pandas as pd; pd.test(extra_args=[`\"--no-strict-data-files`\", `\"-m not clipboard and not single_cpu and not slow and not network and not db`\"])`'; - "@ - # add rc to the end of the image name if the Python version is unreleased - docker pull python:${{ matrix.python[1] == '3.13' && '3.13-rc' || format('{0}-windowsservercore', matrix.python[1]) }} - docker run --env PANDAS_CI='1' -v ${PWD}:C:\pandas python:${{ matrix.python[1] == '3.13' && '3.13-rc' || format('{0}-windowsservercore', matrix.python[1]) }} powershell -Command $TST_CMD - - uses: actions/upload-artifact@v4 with: name: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }} diff --git a/MANIFEST.in b/MANIFEST.in index a7d7d7eb4e062..c59151f340545 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -65,3 +65,5 @@ graft pandas/_libs/include # Include cibw script in sdist since it's needed for building wheels include scripts/cibw_before_build.sh +include scripts/cibw_before_build_windows.sh +include scripts/cibw_before_test_windows.sh diff --git a/pyproject.toml b/pyproject.toml index 6dfee8f4910db..0c76ecd0b15b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,7 +160,13 @@ free-threaded-support = true before-build = "PACKAGE_DIR={package} bash {package}/scripts/cibw_before_build.sh" [tool.cibuildwheel.windows] -before-build = "pip install delvewheel && bash {package}/scripts/cibw_before_build.sh" +before-build = "pip install delvewheel && bash {package}/scripts/cibw_before_build_windows.sh" +before-test = "bash {package}/scripts/cibw_before_test_windows.sh" +test-command = """ + set PANDAS_CI='1' && \ + python -c "import pandas as pd; \ + pd.test(extra_args=['--no-strict-data-files', '-m not clipboard and not single_cpu and not slow and not network and not db']);" \ + """ repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" [[tool.cibuildwheel.overrides]] @@ -175,13 +181,6 @@ test-command = """ select = "*-musllinux*" before-test = "apk update && apk add musl-locales" -[[tool.cibuildwheel.overrides]] -select = "*-win*" -# We test separately for Windows, since we use -# the windowsservercore docker image to check if any dlls are -# missing from the wheel -test-command = "" - [[tool.cibuildwheel.overrides]] # Don't strip wheels on macOS. # macOS doesn't support stripping wheels with linker diff --git a/scripts/cibw_before_build.sh b/scripts/cibw_before_build.sh index 679b91e3280ec..4cdbf8db0ba89 100644 --- a/scripts/cibw_before_build.sh +++ b/scripts/cibw_before_build.sh @@ -5,8 +5,8 @@ done # TODO: Delete when there's a PyPI Cython release that supports free-threaded Python 3.13. FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" -if [[ $FREE_THREADED_BUILD == "True" ]]; then +if [[ $FREE_THREADED_BUILD == "True" ]]; then python -m pip install -U pip - python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy cython - python -m pip install ninja meson-python versioneer[toml] + python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython + python -m pip install numpy ninja meson-python versioneer[toml] fi diff --git a/scripts/cibw_before_build_windows.sh b/scripts/cibw_before_build_windows.sh new file mode 100644 index 0000000000000..5153ebd691f3b --- /dev/null +++ b/scripts/cibw_before_build_windows.sh @@ -0,0 +1,13 @@ +# Add 3rd party licenses, like numpy does +for file in $PACKAGE_DIR/LICENSES/*; do + cat $file >> $PACKAGE_DIR/LICENSE +done + +# TODO: Delete when there's a PyPI Cython release that supports free-threaded Python 3.13 +# and a NumPy Windows wheel for the free-threaded build on PyPI. +FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" +if [[ $FREE_THREADED_BUILD == "True" ]]; then + python -m pip install -U pip + python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy cython + python -m pip install ninja meson-python versioneer[toml] +fi diff --git a/scripts/cibw_before_test_windows.sh b/scripts/cibw_before_test_windows.sh new file mode 100644 index 0000000000000..dd02bc23dd5a1 --- /dev/null +++ b/scripts/cibw_before_test_windows.sh @@ -0,0 +1,5 @@ +# TODO: Delete when there's a NumPy Windows wheel for the free-threaded build on PyPI. +FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" +if [[ $FREE_THREADED_BUILD == "True" ]]; then + python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy +fi From 2d116df77a2c351f6a5193a46c1e7bf9d6578182 Mon Sep 17 00:00:00 2001 From: Jason Mok <106209849+jasonmokk@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:38:31 -0600 Subject: [PATCH 057/266] TST: Add test for visibility of x label and xtick labels for `plot.hexbin` (#60253) TST: Add test for visibility of x label and xtick labels for plot.hexbin TST: Add test for visibility of x label and xtick labels for plot.hexbin TST: Add test for visibility of x label and xtick labels for plot.hexbin TST: Add test for visibility of x label and xtick labels for plot.hexbin TST: Add test for visibility of x label and xtick labels for plot.hexbin Minimize size of test data Co-authored-by: Jason Mok --- pandas/tests/plotting/frame/test_frame.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandas/tests/plotting/frame/test_frame.py b/pandas/tests/plotting/frame/test_frame.py index 087280ed3e01d..845f369d3090f 100644 --- a/pandas/tests/plotting/frame/test_frame.py +++ b/pandas/tests/plotting/frame/test_frame.py @@ -2589,6 +2589,14 @@ def test_plot_period_index_makes_no_right_shift(self, freq): result = ax.get_lines()[0].get_xdata() assert all(str(result[i]) == str(expected[i]) for i in range(4)) + def test_plot_display_xlabel_and_xticks(self): + # GH#44050 + df = DataFrame(np.random.default_rng(2).random((10, 2)), columns=["a", "b"]) + ax = df.plot.hexbin(x="a", y="b") + + _check_visible([ax.xaxis.get_label()], visible=True) + _check_visible(ax.get_xticklabels(), visible=True) + def _generate_4_axes_via_gridspec(): gs = mpl.gridspec.GridSpec(2, 2) From 938832ba325c6efc1710e002c0d3d4d9b3a6c8ba Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 12 Nov 2024 22:41:46 +0100 Subject: [PATCH 058/266] BUG (string dtype): replace with non-string to fall back to object dtype (#60285) Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v2.3.0.rst | 2 +- pandas/core/arrays/string_.py | 43 ++++++++++++-------- pandas/core/dtypes/cast.py | 7 ++++ pandas/core/internals/blocks.py | 23 ++++++++--- pandas/tests/frame/methods/test_replace.py | 3 -- pandas/tests/series/indexing/test_setitem.py | 18 +++----- pandas/tests/series/methods/test_replace.py | 10 ++--- 7 files changed, 60 insertions(+), 46 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index d57d86f4a1476..da0fcfc2b3f64 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -106,10 +106,10 @@ Conversion Strings ^^^^^^^ - Bug in :meth:`Series.rank` for :class:`StringDtype` with ``storage="pyarrow"`` incorrectly returning integer results in case of ``method="average"`` and raising an error if it would truncate results (:issue:`59768`) +- Bug in :meth:`Series.replace` with :class:`StringDtype` when replacing with a non-string value was not upcasting to ``object`` dtype (:issue:`60282`) - Bug in :meth:`Series.str.replace` when ``n < 0`` for :class:`StringDtype` with ``storage="pyarrow"`` (:issue:`59628`) - Bug in ``ser.str.slice`` with negative ``step`` with :class:`ArrowDtype` and :class:`StringDtype` with ``storage="pyarrow"`` giving incorrect results (:issue:`59710`) - Bug in the ``center`` method on :class:`Series` and :class:`Index` object ``str`` accessors with pyarrow-backed dtype not matching the python behavior in corner cases with an odd number of fill characters (:issue:`54792`) -- Interval ^^^^^^^^ diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index 9b1f986a7158e..3b881cfd2df2f 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -730,20 +730,9 @@ def _values_for_factorize(self) -> tuple[np.ndarray, libmissing.NAType | float]: return arr, self.dtype.na_value - def __setitem__(self, key, value) -> None: - value = extract_array(value, extract_numpy=True) - if isinstance(value, type(self)): - # extract_array doesn't extract NumpyExtensionArray subclasses - value = value._ndarray - - key = check_array_indexer(self, key) - scalar_key = lib.is_scalar(key) - scalar_value = lib.is_scalar(value) - if scalar_key and not scalar_value: - raise ValueError("setting an array element with a sequence.") - - # validate new items - if scalar_value: + def _maybe_convert_setitem_value(self, value): + """Maybe convert value to be pyarrow compatible.""" + if lib.is_scalar(value): if isna(value): value = self.dtype.na_value elif not isinstance(value, str): @@ -753,8 +742,11 @@ def __setitem__(self, key, value) -> None: "instead." ) else: + value = extract_array(value, extract_numpy=True) if not is_array_like(value): value = np.asarray(value, dtype=object) + elif isinstance(value.dtype, type(self.dtype)): + return value else: # cast categories and friends to arrays to see if values are # compatible, compatibility with arrow backed strings @@ -764,11 +756,26 @@ def __setitem__(self, key, value) -> None: "Invalid value for dtype 'str'. Value should be a " "string or missing value (or array of those)." ) + return value - mask = isna(value) - if mask.any(): - value = value.copy() - value[isna(value)] = self.dtype.na_value + def __setitem__(self, key, value) -> None: + value = self._maybe_convert_setitem_value(value) + + key = check_array_indexer(self, key) + scalar_key = lib.is_scalar(key) + scalar_value = lib.is_scalar(value) + if scalar_key and not scalar_value: + raise ValueError("setting an array element with a sequence.") + + if not scalar_value: + if value.dtype == self.dtype: + value = value._ndarray + else: + value = np.asarray(value) + mask = isna(value) + if mask.any(): + value = value.copy() + value[isna(value)] = self.dtype.na_value super().__setitem__(key, value) diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 8850b75323d68..830b84852c704 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -1749,6 +1749,13 @@ def can_hold_element(arr: ArrayLike, element: Any) -> bool: except (ValueError, TypeError): return False + if dtype == "string": + try: + arr._maybe_convert_setitem_value(element) # type: ignore[union-attr] + return True + except (ValueError, TypeError): + return False + # This is technically incorrect, but maintains the behavior of # ExtensionBlock._can_hold_element return True diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index a3ff577966a6d..3264676771d5d 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -77,6 +77,7 @@ ABCNumpyExtensionArray, ABCSeries, ) +from pandas.core.dtypes.inference import is_re from pandas.core.dtypes.missing import ( is_valid_na_for_dtype, isna, @@ -706,7 +707,7 @@ def replace( # bc _can_hold_element is incorrect. return [self.copy(deep=False)] - elif self._can_hold_element(value): + elif self._can_hold_element(value) or (self.dtype == "string" and is_re(value)): # TODO(CoW): Maybe split here as well into columns where mask has True # and rest? blk = self._maybe_copy(inplace) @@ -766,14 +767,24 @@ def _replace_regex( ------- List[Block] """ - if not self._can_hold_element(to_replace): + if not is_re(to_replace) and not self._can_hold_element(to_replace): # i.e. only if self.is_object is True, but could in principle include a # String ExtensionBlock return [self.copy(deep=False)] - rx = re.compile(to_replace) + if is_re(to_replace) and self.dtype not in [object, "string"]: + # only object or string dtype can hold strings, and a regex object + # will only match strings + return [self.copy(deep=False)] - block = self._maybe_copy(inplace) + if not ( + self._can_hold_element(value) or (self.dtype == "string" and is_re(value)) + ): + block = self.astype(np.dtype(object)) + else: + block = self._maybe_copy(inplace) + + rx = re.compile(to_replace) replace_regex(block.values, rx, value, mask) return [block] @@ -793,7 +804,9 @@ def replace_list( # Exclude anything that we know we won't contain pairs = [ - (x, y) for x, y in zip(src_list, dest_list) if self._can_hold_element(x) + (x, y) + for x, y in zip(src_list, dest_list) + if (self._can_hold_element(x) or (self.dtype == "string" and is_re(x))) ] if not len(pairs): return [self.copy(deep=False)] diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py index 6b872bf48d550..73f44bcc6657e 100644 --- a/pandas/tests/frame/methods/test_replace.py +++ b/pandas/tests/frame/methods/test_replace.py @@ -889,7 +889,6 @@ def test_replace_input_formats_listlike(self): with pytest.raises(ValueError, match=msg): df.replace(to_rep, values[1:]) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_replace_input_formats_scalar(self): df = DataFrame( {"A": [np.nan, 0, np.inf], "B": [0, 2, 5], "C": ["", "asdf", "fd"]} @@ -940,7 +939,6 @@ def test_replace_dict_no_regex(self): result = answer.replace(weights) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_replace_series_no_regex(self): answer = Series( { @@ -1176,7 +1174,6 @@ def test_replace_commutative(self, df, to_replace, exp): result = df.replace(to_replace) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") @pytest.mark.parametrize( "replacer", [ diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index 82c616132456b..0d62317893326 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -860,24 +860,16 @@ def test_index_where(self, obj, key, expected, raises, val): mask = np.zeros(obj.shape, dtype=bool) mask[key] = True - if raises and obj.dtype == "string": - with pytest.raises(TypeError, match="Invalid value"): - Index(obj).where(~mask, val) - else: - res = Index(obj).where(~mask, val) - expected_idx = Index(expected, dtype=expected.dtype) - tm.assert_index_equal(res, expected_idx) + res = Index(obj).where(~mask, val) + expected_idx = Index(expected, dtype=expected.dtype) + tm.assert_index_equal(res, expected_idx) def test_index_putmask(self, obj, key, expected, raises, val): mask = np.zeros(obj.shape, dtype=bool) mask[key] = True - if raises and obj.dtype == "string": - with pytest.raises(TypeError, match="Invalid value"): - Index(obj).putmask(mask, val) - else: - res = Index(obj).putmask(mask, val) - tm.assert_index_equal(res, Index(expected, dtype=expected.dtype)) + res = Index(obj).putmask(mask, val) + tm.assert_index_equal(res, Index(expected, dtype=expected.dtype)) @pytest.mark.parametrize( diff --git a/pandas/tests/series/methods/test_replace.py b/pandas/tests/series/methods/test_replace.py index 1ebef333f054a..ecfe3d1b39d31 100644 --- a/pandas/tests/series/methods/test_replace.py +++ b/pandas/tests/series/methods/test_replace.py @@ -635,13 +635,11 @@ def test_replace_regex_dtype_series(self, regex): tm.assert_series_equal(result, expected) @pytest.mark.parametrize("regex", [False, True]) - def test_replace_regex_dtype_series_string(self, regex, using_infer_string): - if not using_infer_string: - # then this is object dtype which is already tested above - return + def test_replace_regex_dtype_series_string(self, regex): series = pd.Series(["0"], dtype="str") - with pytest.raises(TypeError, match="Invalid value"): - series.replace(to_replace="0", value=1, regex=regex) + expected = pd.Series([1], dtype=object) + result = series.replace(to_replace="0", value=1, regex=regex) + tm.assert_series_equal(result, expected) def test_replace_different_int_types(self, any_int_numpy_dtype): # GH#45311 From 73da90c14b124aab05b20422b066794738024a4d Mon Sep 17 00:00:00 2001 From: Anish Karki Date: Wed, 13 Nov 2024 07:46:05 +1000 Subject: [PATCH 059/266] DOC: Additions/updates documentation for df.interpolate() methods #60227 (#60247) * DOC: Improve documentation for df.interpolate() methods #60227 * DOC: Fix rounding in DataFrame.interpolate index method example * Fix: Use index method for linear interpolation with non-sequential index * Refactor: Rename DataFrame variable for non-sequential index * Refactor: Added Example for non-sequential dataframe at the end of docstring * CLN: Remove trailing whitespace in docstring and adding comment * DOC: Revert added non-sequential index example in interpolate() docstring * Update pandas/core/generic.py Co-authored-by: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> * Update pandas/core/generic.py Co-authored-by: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> * Trigger CI * Trigger CI --------- Co-authored-by: anishkarki Co-authored-by: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> --- pandas/core/generic.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 7c2cc5d33a5db..56031f20faa16 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -7668,8 +7668,12 @@ def interpolate( * 'linear': Ignore the index and treat the values as equally spaced. This is the only method supported on MultiIndexes. * 'time': Works on daily and higher resolution data to interpolate - given length of interval. - * 'index', 'values': use the actual numerical values of the index. + given length of interval. This interpolates values based on + time interval between observations. + * 'index': The interpolation uses the numerical values + of the DataFrame's index to linearly calculate missing values. + * 'values': Interpolation based on the numerical values + in the DataFrame, treating them as equally spaced along the index. * 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'barycentric', 'polynomial': Passed to `scipy.interpolate.interp1d`, whereas 'spline' is passed to From 994cab4a10957b066de8d9582a41b77e3697dfd7 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:02:27 -0500 Subject: [PATCH 060/266] BUG: Don't merge Excel cells to a single row with merge_cells=False (#60293) * BUG: Don't merge Excel cells to a single row with merge_cells=False * Cleanup comment * Improve test --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/excel.py | 66 +++++++++------------------ pandas/tests/io/excel/test_writers.py | 46 ++++++++++++++----- 3 files changed, 57 insertions(+), 56 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index de69166b8c196..f2c4f85a50ec3 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -702,6 +702,7 @@ I/O - Bug in :meth:`read_stata` raising ``KeyError`` when input file is stored in big-endian format and contains strL data. (:issue:`58638`) - Bug in :meth:`read_stata` where extreme value integers were incorrectly interpreted as missing for format versions 111 and prior (:issue:`58130`) - Bug in :meth:`read_stata` where the missing code for double was not recognised for format versions 105 and prior (:issue:`58149`) +- Bug in :meth:`to_excel` where :class:`MultiIndex` columns would be merged to a single row when ``merge_cells=False`` is passed (:issue:`60274`) Period ^^^^^^ diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 52b5755558900..6a3e215de3f96 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -48,7 +48,6 @@ CSSWarning, ) from pandas.io.formats.format import get_level_lengths -from pandas.io.formats.printing import pprint_thing if TYPE_CHECKING: from pandas._typing import ( @@ -620,9 +619,8 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: return columns = self.columns - level_strs = columns._format_multi( - sparsify=self.merge_cells in {True, "columns"}, include_names=False - ) + merge_columns = self.merge_cells in {True, "columns"} + level_strs = columns._format_multi(sparsify=merge_columns, include_names=False) level_lengths = get_level_lengths(level_strs) coloffset = 0 lnum = 0 @@ -630,51 +628,34 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: if self.index and isinstance(self.df.index, MultiIndex): coloffset = self.df.index.nlevels - 1 - if self.merge_cells in {True, "columns"}: - # Format multi-index as a merged cells. - for lnum, name in enumerate(columns.names): - yield ExcelCell( - row=lnum, - col=coloffset, - val=name, - style=None, - ) + for lnum, name in enumerate(columns.names): + yield ExcelCell( + row=lnum, + col=coloffset, + val=name, + style=None, + ) - for lnum, (spans, levels, level_codes) in enumerate( - zip(level_lengths, columns.levels, columns.codes) - ): - values = levels.take(level_codes) - for i, span_val in spans.items(): - mergestart, mergeend = None, None - if span_val > 1: - mergestart, mergeend = lnum, coloffset + i + span_val - yield CssExcelCell( - row=lnum, - col=coloffset + i + 1, - val=values[i], - style=None, - css_styles=getattr(self.styler, "ctx_columns", None), - css_row=lnum, - css_col=i, - css_converter=self.style_converter, - mergestart=mergestart, - mergeend=mergeend, - ) - else: - # Format in legacy format with dots to indicate levels. - for i, values in enumerate(zip(*level_strs)): - v = ".".join(map(pprint_thing, values)) + for lnum, (spans, levels, level_codes) in enumerate( + zip(level_lengths, columns.levels, columns.codes) + ): + values = levels.take(level_codes) + for i, span_val in spans.items(): + mergestart, mergeend = None, None + if merge_columns and span_val > 1: + mergestart, mergeend = lnum, coloffset + i + span_val yield CssExcelCell( row=lnum, col=coloffset + i + 1, - val=v, + val=values[i], style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=lnum, css_col=i, css_converter=self.style_converter, + mergestart=mergestart, + mergeend=mergeend, ) - self.rowcounter = lnum def _format_header_regular(self) -> Iterable[ExcelCell]: @@ -798,11 +779,8 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: # MultiIndex columns require an extra row # with index names (blank if None) for - # unambiguous round-trip, unless not merging, - # in which case the names all go on one row Issue #11328 - if isinstance(self.columns, MultiIndex) and ( - self.merge_cells in {True, "columns"} - ): + # unambiguous round-trip, Issue #11328 + if isinstance(self.columns, MultiIndex): self.rowcounter += 1 # if index labels are not empty go ahead and dump diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index 81aa0be24bffc..051aa1f386d92 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -870,27 +870,49 @@ def test_to_excel_multiindex_nan_label(self, merge_cells, tmp_excel): # Test for Issue 11328. If column indices are integers, make # sure they are handled correctly for either setting of # merge_cells - def test_to_excel_multiindex_cols(self, merge_cells, frame, tmp_excel): + def test_to_excel_multiindex_cols(self, merge_cells, tmp_excel): + # GH#11328 + frame = DataFrame( + { + "A": [1, 2, 3], + "B": [4, 5, 6], + "C": [7, 8, 9], + } + ) arrays = np.arange(len(frame.index) * 2, dtype=np.int64).reshape(2, -1) new_index = MultiIndex.from_arrays(arrays, names=["first", "second"]) frame.index = new_index - new_cols_index = MultiIndex.from_tuples([(40, 1), (40, 2), (50, 1), (50, 2)]) + new_cols_index = MultiIndex.from_tuples([(40, 1), (40, 2), (50, 1)]) frame.columns = new_cols_index - header = [0, 1] - if not merge_cells: - header = 0 - - # round trip frame.to_excel(tmp_excel, sheet_name="test1", merge_cells=merge_cells) + + # Check round trip + with ExcelFile(tmp_excel) as reader: + result = pd.read_excel( + reader, sheet_name="test1", header=[0, 1], index_col=[0, 1] + ) + tm.assert_frame_equal(result, frame) + + # GH#60274 + # Check with header/index_col None to determine which cells were merged with ExcelFile(tmp_excel) as reader: - df = pd.read_excel( - reader, sheet_name="test1", header=header, index_col=[0, 1] + result = pd.read_excel( + reader, sheet_name="test1", header=None, index_col=None ) + expected = DataFrame( + { + 0: [np.nan, np.nan, "first", 0, 1, 2], + 1: [np.nan, np.nan, "second", 3, 4, 5], + 2: [40.0, 1.0, np.nan, 1.0, 2.0, 3.0], + 3: [np.nan, 2.0, np.nan, 4.0, 5.0, 6.0], + 4: [50.0, 1.0, np.nan, 7.0, 8.0, 9.0], + } + ) if not merge_cells: - fm = frame.columns._format_multi(sparsify=False, include_names=False) - frame.columns = [".".join(map(str, q)) for q in zip(*fm)] - tm.assert_frame_equal(frame, df) + # MultiIndex column value is repeated + expected.loc[0, 3] = 40.0 + tm.assert_frame_equal(result, expected) def test_to_excel_multiindex_dates(self, merge_cells, tmp_excel): # try multiindex with dates From 61f800d7b69efa632c5f93b4be4b1e4154c698d7 Mon Sep 17 00:00:00 2001 From: ZKaoChi <1953542921@qq.com> Date: Thu, 14 Nov 2024 05:07:34 +0800 Subject: [PATCH 061/266] DOC: Make document merge_cells=columns in to_excel (#60277) * DOC: Make document merge_cells=columns in to_excel * DOC: Make document merge_cells=columns in to_excel * Modify the description --- pandas/core/generic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 56031f20faa16..039bdf9c36ee7 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -2211,8 +2211,9 @@ def to_excel( via the options ``io.excel.xlsx.writer`` or ``io.excel.xlsm.writer``. - merge_cells : bool, default True - Write MultiIndex and Hierarchical Rows as merged cells. + merge_cells : bool or 'columns', default False + If True, write MultiIndex index and columns as merged cells. + If 'columns', merge MultiIndex column cells only. {encoding_parameter} inf_rep : str, default 'inf' Representation for infinity (there is no native representation for From ba4d1cfdda14bf521ff91d6ad432b21095c417fd Mon Sep 17 00:00:00 2001 From: William Ayd Date: Thu, 14 Nov 2024 08:42:13 -0500 Subject: [PATCH 062/266] String dtype: enable in SQL IO + resolve all xfails (#60255) --- pandas/core/dtypes/cast.py | 2 ++ pandas/core/internals/construction.py | 5 +++-- pandas/io/sql.py | 21 +++++++++++++++++++-- pandas/tests/io/test_sql.py | 23 +++++++++++++---------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 830b84852c704..137a49c4487f6 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -1162,6 +1162,7 @@ def convert_dtypes( def maybe_infer_to_datetimelike( value: npt.NDArray[np.object_], + convert_to_nullable_dtype: bool = False, ) -> np.ndarray | DatetimeArray | TimedeltaArray | PeriodArray | IntervalArray: """ we might have a array (or single object) that is datetime like, @@ -1199,6 +1200,7 @@ def maybe_infer_to_datetimelike( # numpy would have done it for us. convert_numeric=False, convert_non_numeric=True, + convert_to_nullable_dtype=convert_to_nullable_dtype, dtype_if_all_nat=np.dtype("M8[s]"), ) diff --git a/pandas/core/internals/construction.py b/pandas/core/internals/construction.py index 0812ba5e6def4..f357a53a10be8 100644 --- a/pandas/core/internals/construction.py +++ b/pandas/core/internals/construction.py @@ -966,8 +966,9 @@ def convert(arr): if dtype is None: if arr.dtype == np.dtype("O"): # i.e. maybe_convert_objects didn't convert - arr = maybe_infer_to_datetimelike(arr) - if dtype_backend != "numpy" and arr.dtype == np.dtype("O"): + convert_to_nullable_dtype = dtype_backend != "numpy" + arr = maybe_infer_to_datetimelike(arr, convert_to_nullable_dtype) + if convert_to_nullable_dtype and arr.dtype == np.dtype("O"): new_dtype = StringDtype() arr_cls = new_dtype.construct_array_type() arr = arr_cls._from_sequence(arr, dtype=new_dtype) diff --git a/pandas/io/sql.py b/pandas/io/sql.py index 9aff5600cf49b..125ca51a456d8 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -45,6 +45,8 @@ from pandas.core.dtypes.common import ( is_dict_like, is_list_like, + is_object_dtype, + is_string_dtype, ) from pandas.core.dtypes.dtypes import ( ArrowDtype, @@ -58,6 +60,7 @@ Series, ) from pandas.core.arrays import ArrowExtensionArray +from pandas.core.arrays.string_ import StringDtype from pandas.core.base import PandasObject import pandas.core.common as com from pandas.core.common import maybe_make_list @@ -1316,7 +1319,12 @@ def _harmonize_columns( elif dtype_backend == "numpy" and col_type is float: # floats support NA, can always convert! self.frame[col_name] = df_col.astype(col_type) - + elif ( + using_string_dtype() + and is_string_dtype(col_type) + and is_object_dtype(self.frame[col_name]) + ): + self.frame[col_name] = df_col.astype(col_type) elif dtype_backend == "numpy" and len(df_col) == df_col.count(): # No NA values, can convert ints and bools if col_type is np.dtype("int64") or col_type is bool: @@ -1403,6 +1411,7 @@ def _get_dtype(self, sqltype): DateTime, Float, Integer, + String, ) if isinstance(sqltype, Float): @@ -1422,6 +1431,10 @@ def _get_dtype(self, sqltype): return date elif isinstance(sqltype, Boolean): return bool + elif isinstance(sqltype, String): + if using_string_dtype(): + return StringDtype(na_value=np.nan) + return object @@ -2205,7 +2218,7 @@ def read_table( elif using_string_dtype(): from pandas.io._util import arrow_string_types_mapper - arrow_string_types_mapper() + mapping = arrow_string_types_mapper() else: mapping = None @@ -2286,6 +2299,10 @@ def read_query( from pandas.io._util import _arrow_dtype_mapping mapping = _arrow_dtype_mapping().get + elif using_string_dtype(): + from pandas.io._util import arrow_string_types_mapper + + mapping = arrow_string_types_mapper() else: mapping = None diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index beca8dea9407d..96d63d3fe25e5 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -60,7 +60,7 @@ pytest.mark.filterwarnings( "ignore:Passing a BlockManager to DataFrame:DeprecationWarning" ), - pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False), + pytest.mark.single_cpu, ] @@ -685,6 +685,7 @@ def postgresql_psycopg2_conn(postgresql_psycopg2_engine): @pytest.fixture def postgresql_adbc_conn(): + pytest.importorskip("pyarrow") pytest.importorskip("adbc_driver_postgresql") from adbc_driver_postgresql import dbapi @@ -817,6 +818,7 @@ def sqlite_conn_types(sqlite_engine_types): @pytest.fixture def sqlite_adbc_conn(): + pytest.importorskip("pyarrow") pytest.importorskip("adbc_driver_sqlite") from adbc_driver_sqlite import dbapi @@ -986,13 +988,13 @@ def test_dataframe_to_sql(conn, test_frame1, request): @pytest.mark.parametrize("conn", all_connectable) def test_dataframe_to_sql_empty(conn, test_frame1, request): - if conn == "postgresql_adbc_conn": + if conn == "postgresql_adbc_conn" and not using_string_dtype(): request.node.add_marker( pytest.mark.xfail( - reason="postgres ADBC driver cannot insert index with null type", - strict=True, + reason="postgres ADBC driver < 1.2 cannot insert index with null type", ) ) + # GH 51086 if conn is sqlite_engine conn = request.getfixturevalue(conn) empty_df = test_frame1.iloc[:0] @@ -3557,7 +3559,8 @@ def test_read_sql_dtype_backend( result = getattr(pd, func)( f"Select * from {table}", conn, dtype_backend=dtype_backend ) - expected = dtype_backend_expected(string_storage, dtype_backend, conn_name) + expected = dtype_backend_expected(string_storage, dtype_backend, conn_name) + tm.assert_frame_equal(result, expected) if "adbc" in conn_name: @@ -3607,7 +3610,7 @@ def test_read_sql_dtype_backend_table( with pd.option_context("mode.string_storage", string_storage): result = getattr(pd, func)(table, conn, dtype_backend=dtype_backend) - expected = dtype_backend_expected(string_storage, dtype_backend, conn_name) + expected = dtype_backend_expected(string_storage, dtype_backend, conn_name) tm.assert_frame_equal(result, expected) if "adbc" in conn_name: @@ -4123,7 +4126,7 @@ def tquery(query, con=None): def test_xsqlite_basic(sqlite_buildin): frame = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) assert sql.to_sql(frame, name="test_table", con=sqlite_buildin, index=False) == 10 @@ -4150,7 +4153,7 @@ def test_xsqlite_basic(sqlite_buildin): def test_xsqlite_write_row_by_row(sqlite_buildin): frame = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) frame.iloc[0, 0] = np.nan @@ -4173,7 +4176,7 @@ def test_xsqlite_write_row_by_row(sqlite_buildin): def test_xsqlite_execute(sqlite_buildin): frame = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) create_sql = sql.get_schema(frame, "test") @@ -4194,7 +4197,7 @@ def test_xsqlite_execute(sqlite_buildin): def test_xsqlite_schema(sqlite_buildin): frame = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) create_sql = sql.get_schema(frame, "test") From c4a20261c337d68dc470fb6fd6a5505e2c7348d0 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 14 Nov 2024 18:11:58 +0100 Subject: [PATCH 063/266] TST (string dtype): resolve all easy xfails in pandas/tests/groupby (#60314) --- pandas/tests/groupby/aggregate/test_aggregate.py | 8 ++------ pandas/tests/groupby/aggregate/test_cython.py | 7 +++---- pandas/tests/groupby/aggregate/test_other.py | 6 ++---- pandas/tests/groupby/methods/test_quantile.py | 5 +---- pandas/tests/groupby/methods/test_size.py | 2 ++ pandas/tests/groupby/test_categorical.py | 9 +++++---- pandas/tests/groupby/test_groupby.py | 9 +++------ pandas/tests/groupby/test_groupby_dropna.py | 5 +---- pandas/tests/groupby/test_grouping.py | 10 ++++------ pandas/tests/groupby/test_pipe.py | 6 +----- pandas/tests/groupby/test_reductions.py | 7 ++----- pandas/tests/groupby/test_timegrouper.py | 2 ++ pandas/tests/groupby/transform/test_transform.py | 7 ++----- 13 files changed, 30 insertions(+), 53 deletions(-) diff --git a/pandas/tests/groupby/aggregate/test_aggregate.py b/pandas/tests/groupby/aggregate/test_aggregate.py index 46c27849356b5..64220f1d3d5b4 100644 --- a/pandas/tests/groupby/aggregate/test_aggregate.py +++ b/pandas/tests/groupby/aggregate/test_aggregate.py @@ -9,8 +9,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import SpecificationError from pandas.core.dtypes.common import is_integer_dtype @@ -296,12 +294,11 @@ def aggfun_1(ser): assert len(result) == 0 -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_wrap_agg_out(three_group): grouped = three_group.groupby(["A", "B"]) def func(ser): - if ser.dtype == object: + if ser.dtype == object or ser.dtype == "string": raise TypeError("Test error message") return ser.sum() @@ -1117,7 +1114,6 @@ def test_lambda_named_agg(func): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_aggregate_mixed_types(): # GH 16916 df = DataFrame( @@ -1129,7 +1125,7 @@ def test_aggregate_mixed_types(): expected = DataFrame( expected_data, index=Index([2, "group 1"], dtype="object", name="grouping"), - columns=Index(["X", "Y", "Z"], dtype="object"), + columns=Index(["X", "Y", "Z"]), ) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/groupby/aggregate/test_cython.py b/pandas/tests/groupby/aggregate/test_cython.py index b937e7dcc8136..a706ea795a0e2 100644 --- a/pandas/tests/groupby/aggregate/test_cython.py +++ b/pandas/tests/groupby/aggregate/test_cython.py @@ -5,8 +5,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.common import ( is_float_dtype, is_integer_dtype, @@ -92,7 +90,6 @@ def test_cython_agg_boolean(): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_cython_agg_nothing_to_agg(): frame = DataFrame( {"a": np.random.default_rng(2).integers(0, 5, 50), "b": ["foo", "bar"] * 25} @@ -108,7 +105,9 @@ def test_cython_agg_nothing_to_agg(): result = frame[["b"]].groupby(frame["a"]).mean(numeric_only=True) expected = DataFrame( - [], index=frame["a"].sort_values().drop_duplicates(), columns=[] + [], + index=frame["a"].sort_values().drop_duplicates(), + columns=Index([], dtype="str"), ) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/groupby/aggregate/test_other.py b/pandas/tests/groupby/aggregate/test_other.py index 835cad0d13078..ce78b58e5d8f4 100644 --- a/pandas/tests/groupby/aggregate/test_other.py +++ b/pandas/tests/groupby/aggregate/test_other.py @@ -8,8 +8,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import SpecificationError import pandas as pd @@ -308,7 +306,6 @@ def test_series_agg_multikey(): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_series_agg_multi_pure_python(): data = DataFrame( { @@ -358,7 +355,8 @@ def test_series_agg_multi_pure_python(): ) def bad(x): - assert len(x.values.base) > 0 + if isinstance(x.values, np.ndarray): + assert len(x.values.base) > 0 return "foo" result = data.groupby(["A", "B"]).agg(bad) diff --git a/pandas/tests/groupby/methods/test_quantile.py b/pandas/tests/groupby/methods/test_quantile.py index 4a8ad65200caa..28cb25b515ed2 100644 --- a/pandas/tests/groupby/methods/test_quantile.py +++ b/pandas/tests/groupby/methods/test_quantile.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -158,11 +156,10 @@ def test_groupby_quantile_with_arraylike_q_and_int_columns(frame_size, groupby, tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_quantile_raises(): df = DataFrame([["foo", "a"], ["foo", "b"], ["foo", "c"]], columns=["key", "val"]) - msg = "dtype 'object' does not support operation 'quantile'" + msg = "dtype '(object|str)' does not support operation 'quantile'" with pytest.raises(TypeError, match=msg): df.groupby("key").quantile() diff --git a/pandas/tests/groupby/methods/test_size.py b/pandas/tests/groupby/methods/test_size.py index 91200f53e36bd..2dc89bc75746f 100644 --- a/pandas/tests/groupby/methods/test_size.py +++ b/pandas/tests/groupby/methods/test_size.py @@ -76,6 +76,8 @@ def test_size_series_masked_type_returns_Int64(dtype): tm.assert_series_equal(result, expected) +# TODO(infer_string) in case the column is object dtype, it should preserve that dtype +# for the result's index @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_size_strings(any_string_dtype): # GH#55627 diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py index 1e86b5401ee09..6d84dae1d25d8 100644 --- a/pandas/tests/groupby/test_categorical.py +++ b/pandas/tests/groupby/test_categorical.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( Categorical, @@ -322,8 +320,7 @@ def test_apply(ordered): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) -def test_observed(observed): +def test_observed(request, using_infer_string, observed): # multiple groupers, don't re-expand the output space # of the grouper # gh-14942 (implement) @@ -331,6 +328,10 @@ def test_observed(observed): # gh-8138 (back-compat) # gh-8869 + if using_infer_string and not observed: + # TODO(infer_string) this fails with filling the string column with 0 + request.applymarker(pytest.mark.xfail(reason="TODO(infer_string)")) + cat1 = Categorical(["a", "a", "b", "b"], categories=["a", "b", "z"], ordered=True) cat2 = Categorical(["c", "d", "c", "d"], categories=["c", "d", "y"], ordered=True) df = DataFrame({"A": cat1, "B": cat2, "values": [1, 2, 3, 4]}) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 3305b48a4dcdc..702bbfef2be3b 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -1281,7 +1281,6 @@ def test_groupby_two_group_keys_all_nan(): assert result == {} -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_groupby_2d_malformed(): d = DataFrame(index=range(2)) d["group"] = ["g1", "g2"] @@ -1290,7 +1289,7 @@ def test_groupby_2d_malformed(): d["label"] = ["l1", "l2"] tmp = d.groupby(["group"]).mean(numeric_only=True) res_values = np.array([[0.0, 1.0], [0.0, 1.0]]) - tm.assert_index_equal(tmp.columns, Index(["zeros", "ones"])) + tm.assert_index_equal(tmp.columns, Index(["zeros", "ones"], dtype=object)) tm.assert_numpy_array_equal(tmp.values, res_values) @@ -2345,7 +2344,6 @@ def test_groupby_all_nan_groups_drop(): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("numeric_only", [True, False]) def test_groupby_empty_multi_column(as_index, numeric_only): # GH 15106 & GH 41998 @@ -2354,7 +2352,7 @@ def test_groupby_empty_multi_column(as_index, numeric_only): result = gb.sum(numeric_only=numeric_only) if as_index: index = MultiIndex([[], []], [[], []], names=["A", "B"]) - columns = ["C"] if not numeric_only else [] + columns = ["C"] if not numeric_only else Index([], dtype="str") else: index = RangeIndex(0) columns = ["A", "B", "C"] if not numeric_only else ["A", "B"] @@ -2362,7 +2360,6 @@ def test_groupby_empty_multi_column(as_index, numeric_only): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_groupby_aggregation_non_numeric_dtype(): # GH #43108 df = DataFrame( @@ -2373,7 +2370,7 @@ def test_groupby_aggregation_non_numeric_dtype(): { "v": [[1, 1], [10, 20]], }, - index=Index(["M", "W"], dtype="object", name="MW"), + index=Index(["M", "W"], name="MW"), ) gb = df.groupby(by=["MW"]) diff --git a/pandas/tests/groupby/test_groupby_dropna.py b/pandas/tests/groupby/test_groupby_dropna.py index d42aa06d6bbfe..060a8b7fd3824 100644 --- a/pandas/tests/groupby/test_groupby_dropna.py +++ b/pandas/tests/groupby/test_groupby_dropna.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat.pyarrow import pa_version_under10p1 from pandas.core.dtypes.missing import na_value_for_dtype @@ -99,7 +97,6 @@ def test_groupby_dropna_multi_index_dataframe_nan_in_two_groups( tm.assert_frame_equal(grouped, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "dropna, idx, outputs", [ @@ -126,7 +123,7 @@ def test_groupby_dropna_normal_index_dataframe(dropna, idx, outputs): df = pd.DataFrame(df_list, columns=["a", "b", "c", "d"]) grouped = df.groupby("a", dropna=dropna).sum() - expected = pd.DataFrame(outputs, index=pd.Index(idx, dtype="object", name="a")) + expected = pd.DataFrame(outputs, index=pd.Index(idx, name="a")) tm.assert_frame_equal(grouped, expected) diff --git a/pandas/tests/groupby/test_grouping.py b/pandas/tests/groupby/test_grouping.py index 6bb2eaf89b5d7..366eb59ee226a 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -10,8 +10,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import SpecificationError import pandas as pd @@ -807,7 +805,6 @@ def test_groupby_empty(self): expected = ["name"] assert result == expected - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_groupby_level_index_value_all_na(self): # issue 20519 df = DataFrame( @@ -817,7 +814,7 @@ def test_groupby_level_index_value_all_na(self): expected = DataFrame( data=[], index=MultiIndex( - levels=[Index(["x"], dtype="object"), Index([], dtype="float64")], + levels=[Index(["x"], dtype="str"), Index([], dtype="float64")], codes=[[], []], names=["A", "B"], ), @@ -981,12 +978,13 @@ def test_groupby_with_empty(self): grouped = series.groupby(grouper) assert next(iter(grouped), None) is None - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_groupby_with_single_column(self): df = DataFrame({"a": list("abssbab")}) tm.assert_frame_equal(df.groupby("a").get_group("a"), df.iloc[[0, 5]]) # GH 13530 - exp = DataFrame(index=Index(["a", "b", "s"], name="a"), columns=[]) + exp = DataFrame( + index=Index(["a", "b", "s"], name="a"), columns=Index([], dtype="str") + ) tm.assert_frame_equal(df.groupby("a").count(), exp) tm.assert_frame_equal(df.groupby("a").sum(), exp) diff --git a/pandas/tests/groupby/test_pipe.py b/pandas/tests/groupby/test_pipe.py index 1044c83e3e56b..ee59a93695bcf 100644 --- a/pandas/tests/groupby/test_pipe.py +++ b/pandas/tests/groupby/test_pipe.py @@ -1,7 +1,4 @@ import numpy as np -import pytest - -from pandas._config import using_string_dtype import pandas as pd from pandas import ( @@ -11,7 +8,6 @@ import pandas._testing as tm -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_pipe(): # Test the pipe method of DataFrameGroupBy. # Issue #17871 @@ -39,7 +35,7 @@ def square(srs): # NDFrame.pipe methods result = df.groupby("A").pipe(f).pipe(square) - index = Index(["bar", "foo"], dtype="object", name="A") + index = Index(["bar", "foo"], name="A") expected = pd.Series([3.749306591013693, 6.717707873081384], name="B", index=index) tm.assert_series_equal(expected, result) diff --git a/pandas/tests/groupby/test_reductions.py b/pandas/tests/groupby/test_reductions.py index a6ea1502103c5..51c7eab2bfa82 100644 --- a/pandas/tests/groupby/test_reductions.py +++ b/pandas/tests/groupby/test_reductions.py @@ -5,8 +5,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs.tslibs import iNaT from pandas.core.dtypes.common import pandas_dtype @@ -470,8 +468,7 @@ def test_max_min_non_numeric(): assert "ss" in result -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") -def test_max_min_object_multiple_columns(): +def test_max_min_object_multiple_columns(using_infer_string): # GH#41111 case where the aggregation is valid for some columns but not # others; we split object blocks column-wise, consistent with # DataFrame._reduce @@ -484,7 +481,7 @@ def test_max_min_object_multiple_columns(): } ) df._consolidate_inplace() # should already be consolidate, but double-check - assert len(df._mgr.blocks) == 2 + assert len(df._mgr.blocks) == 3 if using_infer_string else 2 gb = df.groupby("A") diff --git a/pandas/tests/groupby/test_timegrouper.py b/pandas/tests/groupby/test_timegrouper.py index ee4973cbf18af..a7712d9dc6586 100644 --- a/pandas/tests/groupby/test_timegrouper.py +++ b/pandas/tests/groupby/test_timegrouper.py @@ -76,6 +76,8 @@ def groupby_with_truncated_bingrouper(frame_for_truncated_bingrouper): class TestGroupBy: + # TODO(infer_string) resample sum introduces 0's + # https://github.com/pandas-dev/pandas/issues/60229 @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_groupby_with_timegrouper(self): # GH 4161 diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py index 5b8fa96291c9f..022d3d51ded4e 100644 --- a/pandas/tests/groupby/transform/test_transform.py +++ b/pandas/tests/groupby/transform/test_transform.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs import lib from pandas.core.dtypes.common import ensure_platform_int @@ -1034,20 +1032,19 @@ def test_groupby_transform_with_datetimes(func, values): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_groupby_transform_dtype(): # GH 22243 df = DataFrame({"a": [1], "val": [1.35]}) result = df["val"].transform(lambda x: x.map(lambda y: f"+{y}")) - expected1 = Series(["+1.35"], name="val", dtype="object") + expected1 = Series(["+1.35"], name="val") tm.assert_series_equal(result, expected1) result = df.groupby("a")["val"].transform(lambda x: x.map(lambda y: f"+{y}")) tm.assert_series_equal(result, expected1) result = df.groupby("a")["val"].transform(lambda x: x.map(lambda y: f"+({y})")) - expected2 = Series(["+(1.35)"], name="val", dtype="object") + expected2 = Series(["+(1.35)"], name="val") tm.assert_series_equal(result, expected2) df["val"] = df["val"].astype(object) From 3c25c8c5b77294f267206ced22f85bc0a35d52b6 Mon Sep 17 00:00:00 2001 From: Kevin Amparado <109636487+KevsterAmp@users.noreply.github.com> Date: Fri, 15 Nov 2024 01:16:18 +0800 Subject: [PATCH 064/266] BUG: Disabling pandas option `display.html.use_mathjax` has no effect; add `mathjax_ignore` to fix (#60311) * add bugfix to whatsnew * append "mathjax_ignore" in "tex2jax_ignore" related funcs * add "mathjax_ignore" on existing tests * remove old table_attr --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/html.py | 1 + pandas/io/formats/style_render.py | 6 ++++-- pandas/tests/io/formats/style/test_style.py | 2 ++ pandas/tests/io/formats/test_to_html.py | 2 ++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index f2c4f85a50ec3..7da2f968b900b 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -702,6 +702,7 @@ I/O - Bug in :meth:`read_stata` raising ``KeyError`` when input file is stored in big-endian format and contains strL data. (:issue:`58638`) - Bug in :meth:`read_stata` where extreme value integers were incorrectly interpreted as missing for format versions 111 and prior (:issue:`58130`) - Bug in :meth:`read_stata` where the missing code for double was not recognised for format versions 105 and prior (:issue:`58149`) +- Bug in :meth:`set_option` where setting the pandas option ``display.html.use_mathjax`` to ``False`` has no effect (:issue:`59884`) - Bug in :meth:`to_excel` where :class:`MultiIndex` columns would be merged to a single row when ``merge_cells=False`` is passed (:issue:`60274`) Period diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py index fdea1831d5596..c4884ef4ce4a9 100644 --- a/pandas/io/formats/html.py +++ b/pandas/io/formats/html.py @@ -241,6 +241,7 @@ def _write_table(self, indent: int = 0) -> None: use_mathjax = get_option("display.html.use_mathjax") if not use_mathjax: _classes.append("tex2jax_ignore") + _classes.append("mathjax_ignore") if self.classes is not None: if isinstance(self.classes, str): self.classes = self.classes.split() diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 08d9fd938c873..ecfe3de10c829 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -366,9 +366,11 @@ def _translate( if not get_option("styler.html.mathjax"): table_attr = table_attr or "" if 'class="' in table_attr: - table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ') + table_attr = table_attr.replace( + 'class="', 'class="tex2jax_ignore mathjax_ignore ' + ) else: - table_attr += ' class="tex2jax_ignore"' + table_attr += ' class="tex2jax_ignore mathjax_ignore"' d.update({"table_attributes": table_attr}) if self.tooltips: diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index e9fc2b2d27afd..ff8a1b9f570ab 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -488,9 +488,11 @@ def test_repr_html_ok(self, styler): def test_repr_html_mathjax(self, styler): # gh-19824 / 41395 assert "tex2jax_ignore" not in styler._repr_html_() + assert "mathjax_ignore" not in styler._repr_html_() with option_context("styler.html.mathjax", False): assert "tex2jax_ignore" in styler._repr_html_() + assert "mathjax_ignore" in styler._repr_html_() def test_update_ctx(self, styler): styler._update_ctx(DataFrame({"A": ["color: red", "color: blue"]})) diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py index 8031f67cd0567..b1a437bfdbd8a 100644 --- a/pandas/tests/io/formats/test_to_html.py +++ b/pandas/tests/io/formats/test_to_html.py @@ -934,9 +934,11 @@ def test_repr_html(self, float_frame): def test_repr_html_mathjax(self): df = DataFrame([[1, 2], [3, 4]]) assert "tex2jax_ignore" not in df._repr_html_() + assert "mathjax_ignore" not in df._repr_html_() with option_context("display.html.use_mathjax", False): assert "tex2jax_ignore" in df._repr_html_() + assert "mathjax_ignore" in df._repr_html_() def test_repr_html_wide(self): max_cols = 20 From 9bee2f0536836cfd8e82070e3579a6db3842186c Mon Sep 17 00:00:00 2001 From: U-S-jun <157643778+U-S-jun@users.noreply.github.com> Date: Fri, 15 Nov 2024 02:18:56 +0900 Subject: [PATCH 065/266] DOC: Update from_records docstring to not mention DataFrame as valid input (#60310) * DOC: Update from_records docstring to comply with numpydoc style (GH#60307) * Update pandas/core/frame.py Added "or" before the last item in the enumeration for improved clarity in the from_records docstring. Co-authored-by: Joris Van den Bossche --------- Co-authored-by: Joris Van den Bossche --- pandas/core/frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index b35e2c8497fb7..34eb198b4b4da 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -2115,8 +2115,8 @@ def from_records( """ Convert structured or record ndarray to DataFrame. - Creates a DataFrame object from a structured ndarray, sequence of - tuples or dicts, or DataFrame. + Creates a DataFrame object from a structured ndarray, or sequence of + tuples or dicts. Parameters ---------- From 34c39e9078ea8af12871a92bdcea2058553c9869 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 14 Nov 2024 18:21:17 +0100 Subject: [PATCH 066/266] BUG (string dtype): let fillna with invalid value upcast to object dtype (#60296) * BUG (string dtype): let fillna with invalid value upcast to object dtype * fix fillna limit case + update tests for no longer raising --- pandas/core/internals/blocks.py | 9 +++++---- pandas/tests/frame/indexing/test_where.py | 8 +------- pandas/tests/series/indexing/test_setitem.py | 5 ----- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 3264676771d5d..3c207e8c14b5b 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -108,6 +108,7 @@ PeriodArray, TimedeltaArray, ) +from pandas.core.arrays.string_ import StringDtype from pandas.core.base import PandasObject import pandas.core.common as com from pandas.core.computation import expressions @@ -1336,7 +1337,7 @@ def fillna( return [self.copy(deep=False)] if limit is not None: - mask[mask.cumsum(self.ndim - 1) > limit] = False + mask[mask.cumsum(self.values.ndim - 1) > limit] = False if inplace: nbs = self.putmask(mask.T, value) @@ -1684,7 +1685,7 @@ def where(self, other, cond) -> list[Block]: res_values = arr._where(cond, other).T except (ValueError, TypeError): if self.ndim == 1 or self.shape[0] == 1: - if isinstance(self.dtype, IntervalDtype): + if isinstance(self.dtype, (IntervalDtype, StringDtype)): # TestSetitemFloatIntervalWithIntIntervalValues blk = self.coerce_to_target_dtype(orig_other, raise_on_upcast=False) return blk.where(orig_other, orig_cond) @@ -1854,9 +1855,9 @@ def fillna( limit: int | None = None, inplace: bool = False, ) -> list[Block]: - if isinstance(self.dtype, IntervalDtype): + if isinstance(self.dtype, (IntervalDtype, StringDtype)): # Block.fillna handles coercion (test_fillna_interval) - if limit is not None: + if isinstance(self.dtype, IntervalDtype) and limit is not None: raise ValueError("limit must be None") return super().fillna( value=value, diff --git a/pandas/tests/frame/indexing/test_where.py b/pandas/tests/frame/indexing/test_where.py index 86b39ddd19ec1..d6570fcda2ee8 100644 --- a/pandas/tests/frame/indexing/test_where.py +++ b/pandas/tests/frame/indexing/test_where.py @@ -1025,15 +1025,9 @@ def test_where_producing_ea_cond_for_np_dtype(): @pytest.mark.parametrize( "replacement", [0.001, True, "snake", None, datetime(2022, 5, 4)] ) -def test_where_int_overflow(replacement, using_infer_string): +def test_where_int_overflow(replacement): # GH 31687 df = DataFrame([[1.0, 2e25, "nine"], [np.nan, 0.1, None]]) - if using_infer_string and replacement not in (None, "snake"): - with pytest.raises( - TypeError, match=f"Invalid value '{replacement}' for dtype 'str'" - ): - df.where(pd.notnull(df), replacement) - return result = df.where(pd.notnull(df), replacement) expected = DataFrame([[1.0, 2e25, "nine"], [replacement, 0.1, replacement]]) diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index 0d62317893326..158198239ba75 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -839,11 +839,6 @@ def test_series_where(self, obj, key, expected, raises, val, is_inplace): obj = obj.copy() arr = obj._values - if raises and obj.dtype == "string": - with pytest.raises(TypeError, match="Invalid value"): - obj.where(~mask, val) - return - res = obj.where(~mask, val) if val is NA and res.dtype == object: From 45aa7a55a5a1ef38c5e242488d2419ae84dd874b Mon Sep 17 00:00:00 2001 From: Jason Mok <106209849+jasonmokk@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:22:00 -0600 Subject: [PATCH 067/266] TST: Add test for x-axis labels in subplot with `secondary_y=True` (#60294) * TST: Add test for x-axis labels in subplots with secondary_y=True TST: Add test for x-axis labels in subplots with secondary_y=True TST: Add test for x-axis labels in subplots with secondary_y=True TST: Add test for x-axis labels in subplots with secondary_y=True TST: Add test for x-axis labels in subplots with secondary_y=True * Remove pytest.mark.slow --------- Co-authored-by: Jason Mok --- pandas/tests/plotting/test_series.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 52ca66c218862..9675b936c171e 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -958,3 +958,16 @@ def test_plot_no_warning(self, ts): # TODO(3.0): this can be removed once Period[B] deprecation is enforced with tm.assert_produces_warning(False): _ = ts.plot() + + def test_secondary_y_subplot_axis_labels(self): + # GH#14102 + s1 = Series([5, 7, 6, 8, 7], index=[1, 2, 3, 4, 5]) + s2 = Series([6, 4, 5, 3, 4], index=[1, 2, 3, 4, 5]) + + ax = plt.subplot(2, 1, 1) + s1.plot(ax=ax) + s2.plot(ax=ax, secondary_y=True) + ax2 = plt.subplot(2, 1, 2) + s1.plot(ax=ax2) + assert len(ax.xaxis.get_minor_ticks()) == 0 + assert len(ax.get_xticklabels()) > 0 From 9bc88c79e6fd146a44970309bacc90490fdec590 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Fri, 15 Nov 2024 09:50:23 -0500 Subject: [PATCH 068/266] TST (string dtype): resolve all xfails in JSON IO tests (#60318) --- pandas/tests/io/json/test_json_table_schema.py | 8 +------- pandas/tests/io/json/test_pandas.py | 14 ++++---------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/pandas/tests/io/json/test_json_table_schema.py b/pandas/tests/io/json/test_json_table_schema.py index 7f367ded39863..7936982e4a055 100644 --- a/pandas/tests/io/json/test_json_table_schema.py +++ b/pandas/tests/io/json/test_json_table_schema.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.dtypes import ( CategoricalDtype, DatetimeTZDtype, @@ -27,10 +25,6 @@ set_default_names, ) -pytestmark = pytest.mark.xfail( - using_string_dtype(), reason="TODO(infer_string)", strict=False -) - @pytest.fixture def df_schema(): @@ -127,7 +121,7 @@ def test_multiindex(self, df_schema, using_infer_string): expected["fields"][0] = { "name": "level_0", "type": "any", - "extDtype": "string", + "extDtype": "str", } expected["fields"][3] = {"name": "B", "type": "any", "extDtype": "str"} assert result == expected diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index d3328d1dfcaef..ad9dbf7554a8b 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -84,7 +84,7 @@ def datetime_frame(self): # since that doesn't round-trip, see GH#33711 df = DataFrame( np.random.default_rng(2).standard_normal((30, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=30, freq="B"), ) df.index = df.index._with_freq(None) @@ -184,7 +184,6 @@ def test_roundtrip_simple(self, orient, convert_axes, dtype, float_frame): assert_json_roundtrip_equal(result, expected, orient) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("dtype", [False, np.int64]) @pytest.mark.parametrize("convert_axes", [True, False]) def test_roundtrip_intframe(self, orient, convert_axes, dtype, int_frame): @@ -270,7 +269,6 @@ def test_roundtrip_empty(self, orient, convert_axes): tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("convert_axes", [True, False]) def test_roundtrip_timestamp(self, orient, convert_axes, datetime_frame): # TODO: improve coverage with date_format parameter @@ -698,7 +696,6 @@ def test_series_roundtrip_simple(self, orient, string_series, using_infer_string tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize("dtype", [False, None]) def test_series_roundtrip_object(self, orient, dtype, object_series): data = StringIO(object_series.to_json(orient=orient)) @@ -710,6 +707,9 @@ def test_series_roundtrip_object(self, orient, dtype, object_series): if orient != "split": expected.name = None + if using_string_dtype(): + expected = expected.astype("str") + tm.assert_series_equal(result, expected) def test_series_roundtrip_empty(self, orient): @@ -808,7 +808,6 @@ def test_path(self, float_frame, int_frame, datetime_frame): df.to_json(path) read_json(path) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_axis_dates(self, datetime_series, datetime_frame): # frame json = StringIO(datetime_frame.to_json()) @@ -821,7 +820,6 @@ def test_axis_dates(self, datetime_series, datetime_frame): tm.assert_series_equal(result, datetime_series, check_names=False) assert result.name is None - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_convert_dates(self, datetime_series, datetime_frame): # frame df = datetime_frame @@ -912,7 +910,6 @@ def test_convert_dates_infer(self, infer_word): result = read_json(StringIO(ujson_dumps(data)))[["id", infer_word]] tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize( "date,date_unit", [ @@ -973,7 +970,6 @@ def test_date_format_series_raises(self, datetime_series): with pytest.raises(ValueError, match=msg): ts.to_json(date_format="iso", date_unit="foo") - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_date_unit(self, unit, datetime_frame): df = datetime_frame df["date"] = Timestamp("20130101 20:43:42").as_unit("ns") @@ -1114,7 +1110,6 @@ def test_round_trip_exception(self, datapath): res = res.fillna(np.nan) tm.assert_frame_equal(res, df) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.network @pytest.mark.single_cpu @pytest.mark.parametrize( @@ -1555,7 +1550,6 @@ def test_data_frame_size_after_to_json(self): assert size_before == size_after - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "index", [None, [1, 2], [1.0, 2.0], ["a", "b"], ["1", "2"], ["1.", "2."]] ) From b26b1d2aa4b336a1875ec6a97019148e3af98c18 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 15 Nov 2024 15:54:26 +0100 Subject: [PATCH 069/266] TST (string dtype): resolve xfails in common IO tests (#60320) --- pandas/tests/io/test_clipboard.py | 13 ++++++------ pandas/tests/io/test_common.py | 33 +++++++++++++---------------- pandas/tests/io/test_compression.py | 15 ++++++------- pandas/tests/io/test_gcs.py | 5 ++--- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index 541cc39606047..b5e97314caf03 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import ( PyperclipException, PyperclipWindowsException, @@ -26,10 +24,6 @@ init_qt_clipboard, ) -pytestmark = pytest.mark.xfail( - using_string_dtype(), reason="TODO(infer_string)", strict=False -) - def build_kwargs(sep, excel): kwargs = {} @@ -351,7 +345,7 @@ def test_raw_roundtrip(self, data): @pytest.mark.parametrize("engine", ["c", "python"]) def test_read_clipboard_dtype_backend( - self, clipboard, string_storage, dtype_backend, engine + self, clipboard, string_storage, dtype_backend, engine, using_infer_string ): # GH#50502 if dtype_backend == "pyarrow": @@ -396,6 +390,11 @@ def test_read_clipboard_dtype_backend( ) expected["g"] = ArrowExtensionArray(pa.array([None, None])) + if using_infer_string: + expected.columns = expected.columns.astype( + pd.StringDtype(string_storage, na_value=np.nan) + ) + tm.assert_frame_equal(result, expected) def test_invalid_dtype_backend(self): diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 4f3f613f71542..506276a25dad6 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -140,7 +140,6 @@ def test_bytesiowrapper_returns_correct_bytes(self): assert result == data.encode("utf-8") # Test that pyarrow can handle a file opened with get_handle - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_get_handle_pyarrow_compat(self): pa_csv = pytest.importorskip("pyarrow.csv") @@ -155,6 +154,8 @@ def test_get_handle_pyarrow_compat(self): s = StringIO(data) with icom.get_handle(s, "rb", is_text=False) as handles: df = pa_csv.read_csv(handles.handle).to_pandas() + # TODO will have to update this when pyarrow' to_pandas() is fixed + expected = expected.astype("object") tm.assert_frame_equal(df, expected) assert not s.closed @@ -338,7 +339,6 @@ def test_read_fspath_all(self, reader, module, path, datapath): ("to_stata", {"time_stamp": pd.to_datetime("2019-01-01 00:00")}, "os"), ], ) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_write_fspath_all(self, writer_name, writer_kwargs, module): if writer_name in ["to_latex"]: # uses Styler implementation pytest.importorskip("jinja2") @@ -365,7 +365,7 @@ def test_write_fspath_all(self, writer_name, writer_kwargs, module): expected = f_path.read() assert result == expected - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") + @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) hdf support") def test_write_fspath_hdf5(self): # Same test as write_fspath_all, except HDF5 files aren't # necessarily byte-for-byte identical for a given dataframe, so we'll @@ -438,14 +438,13 @@ def test_unknown_engine(self): with tm.ensure_clean() as path: df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) df.to_csv(path) with pytest.raises(ValueError, match="Unknown engine"): pd.read_csv(path, engine="pyt") - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_binary_mode(self): """ 'encoding' shouldn't be passed to 'open' in binary mode. @@ -455,8 +454,8 @@ def test_binary_mode(self): with tm.ensure_clean() as path: df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) df.to_csv(path, mode="w+b") tm.assert_frame_equal(df, pd.read_csv(path, index_col=0)) @@ -473,8 +472,8 @@ def test_warning_missing_utf_bom(self, encoding, compression_): """ df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) with tm.ensure_clean() as path: with tm.assert_produces_warning(UnicodeWarning, match="byte order mark"): @@ -504,15 +503,14 @@ def test_is_fsspec_url(): assert icom.is_fsspec_url("RFC-3986+compliant.spec://something") -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize("encoding", [None, "utf-8"]) @pytest.mark.parametrize("format", ["csv", "json"]) def test_codecs_encoding(encoding, format): # GH39247 expected = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) with tm.ensure_clean() as path: with codecs.open(path, mode="w", encoding=encoding) as handle: @@ -525,13 +523,12 @@ def test_codecs_encoding(encoding, format): tm.assert_frame_equal(expected, df) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_codecs_get_writer_reader(): # GH39247 expected = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) with tm.ensure_clean() as path: with open(path, "wb") as handle: @@ -556,8 +553,8 @@ def test_explicit_encoding(io_class, mode, msg): # wrong mode is requested expected = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) with io_class() as buffer: with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/io/test_compression.py b/pandas/tests/io/test_compression.py index 5eb202dd5aa24..fd1e9b4fdf211 100644 --- a/pandas/tests/io/test_compression.py +++ b/pandas/tests/io/test_compression.py @@ -12,8 +12,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import is_platform_windows import pandas as pd @@ -139,7 +137,6 @@ def test_compression_warning(compression_only): df.to_csv(handles.handle, compression=compression_only) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_compression_binary(compression_only): """ Binary file handles support compression. @@ -148,8 +145,8 @@ def test_compression_binary(compression_only): """ df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) # with a file @@ -180,8 +177,8 @@ def test_gzip_reproducibility_file_name(): """ df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) compression_options = {"method": "gzip", "mtime": 1} @@ -203,8 +200,8 @@ def test_gzip_reproducibility_file_object(): """ df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ) compression_options = {"method": "gzip", "mtime": 1} diff --git a/pandas/tests/io/test_gcs.py b/pandas/tests/io/test_gcs.py index a9e7b2da03a4d..48580003f6c5e 100644 --- a/pandas/tests/io/test_gcs.py +++ b/pandas/tests/io/test_gcs.py @@ -158,7 +158,6 @@ def assert_equal_zip_safe(result: bytes, expected: bytes, compression: str): assert result == expected -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize("encoding", ["utf-8", "cp1251"]) def test_to_csv_compression_encoding_gcs( gcs_buffer, compression_only, encoding, compression_to_extension @@ -171,8 +170,8 @@ def test_to_csv_compression_encoding_gcs( """ df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=Index(list("ABCD"), dtype=object), - index=Index([f"i-{i}" for i in range(30)], dtype=object), + columns=Index(list("ABCD")), + index=Index([f"i-{i}" for i in range(30)]), ) # reference of compressed and encoded file From fba5f08f048215a6e0a578f8bad7b7f2c9ee8eef Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 15 Nov 2024 16:53:15 +0100 Subject: [PATCH 070/266] TST (string dtype): resolve xfails in pandas/tests/apply + raise TypeError for ArrowArray accumulate (#60312) --- pandas/core/arrays/arrow/array.py | 6 +++++- pandas/tests/apply/test_invalid_arg.py | 30 +++++++++----------------- pandas/tests/apply/test_str.py | 13 ++++++----- pandas/tests/extension/test_arrow.py | 2 +- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index fcc50c5b6b20f..e0c93db0afb07 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1644,7 +1644,11 @@ def _accumulate( else: data_to_accum = data_to_accum.cast(pa.int64()) - result = pyarrow_meth(data_to_accum, skip_nulls=skipna, **kwargs) + try: + result = pyarrow_meth(data_to_accum, skip_nulls=skipna, **kwargs) + except pa.ArrowNotImplementedError as err: + msg = f"operation '{name}' not supported for dtype '{self.dtype}'" + raise TypeError(msg) from err if convert_to_int: result = result.cast(pa_dtype) diff --git a/pandas/tests/apply/test_invalid_arg.py b/pandas/tests/apply/test_invalid_arg.py index e19c21f81b3e1..0503bf9166ec7 100644 --- a/pandas/tests/apply/test_invalid_arg.py +++ b/pandas/tests/apply/test_invalid_arg.py @@ -218,18 +218,12 @@ def transform(row): def test_agg_cython_table_raises_frame(df, func, expected, axis, using_infer_string): # GH 21224 if using_infer_string: - if df.dtypes.iloc[0].storage == "pyarrow": - import pyarrow as pa - - # TODO(infer_string) - # should raise a proper TypeError instead of propagating the pyarrow error - - expected = (expected, pa.lib.ArrowNotImplementedError) - else: - expected = (expected, NotImplementedError) + expected = (expected, NotImplementedError) msg = ( - "can't multiply sequence by non-int of type 'str'|has no kernel|cannot perform" + "can't multiply sequence by non-int of type 'str'" + "|cannot perform cumprod with type str" # NotImplementedError python backend + "|operation 'cumprod' not supported for dtype 'str'" # TypeError pyarrow ) warn = None if isinstance(func, str) else FutureWarning with pytest.raises(expected, match=msg): @@ -259,16 +253,12 @@ def test_agg_cython_table_raises_series(series, func, expected, using_infer_stri if func == "median" or func is np.nanmedian or func is np.median: msg = r"Cannot convert \['a' 'b' 'c'\] to numeric" - if using_infer_string: - if series.dtype.storage == "pyarrow": - import pyarrow as pa - - # TODO(infer_string) - # should raise a proper TypeError instead of propagating the pyarrow error - expected = (expected, pa.lib.ArrowNotImplementedError) - else: - expected = (expected, NotImplementedError) - msg = msg + "|does not support|has no kernel|Cannot perform|cannot perform" + if using_infer_string and func == "cumprod": + expected = (expected, NotImplementedError) + + msg = ( + msg + "|does not support|has no kernel|Cannot perform|cannot perform|operation" + ) warn = None if isinstance(func, str) else FutureWarning with pytest.raises(expected, match=msg): diff --git a/pandas/tests/apply/test_str.py b/pandas/tests/apply/test_str.py index 732652f24e2eb..c52168ae48ca8 100644 --- a/pandas/tests/apply/test_str.py +++ b/pandas/tests/apply/test_str.py @@ -4,8 +4,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import WASM from pandas.core.dtypes.common import is_number @@ -81,7 +79,6 @@ def test_apply_np_transformer(float_frame, op, how): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "series, func, expected", chain( @@ -140,7 +137,6 @@ def test_agg_cython_table_series(series, func, expected): assert result == expected -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "series, func, expected", chain( @@ -163,10 +159,17 @@ def test_agg_cython_table_series(series, func, expected): ), ), ) -def test_agg_cython_table_transform_series(series, func, expected): +def test_agg_cython_table_transform_series(request, series, func, expected): # GH21224 # test transforming functions in # pandas.core.base.SelectionMixin._cython_table (cumprod, cumsum) + if series.dtype == "string" and func == "cumsum": + request.applymarker( + pytest.mark.xfail( + raises=(TypeError, NotImplementedError), + reason="TODO(infer_string) cumsum not yet implemented for string", + ) + ) warn = None if isinstance(func, str) else FutureWarning with tm.assert_produces_warning(warn, match="is currently using Series.*"): result = series.agg(func) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index f0ff11e5fa3f7..9defb97394635 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -441,7 +441,7 @@ def test_accumulate_series(self, data, all_numeric_accumulations, skipna, reques request.applymarker( pytest.mark.xfail( reason=f"{all_numeric_accumulations} not implemented for {pa_type}", - raises=NotImplementedError, + raises=TypeError, ) ) From ee3c18f51b393893ed6e31214c7be2f9427ce0c9 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 15 Nov 2024 17:15:18 +0100 Subject: [PATCH 071/266] TST (string dtype): resolve all xfails in IO parser tests (#60321) --- .../tests/io/parser/common/test_chunksize.py | 13 ++++---- .../io/parser/common/test_file_buffer_url.py | 7 ++--- pandas/tests/io/parser/common/test_index.py | 10 +++--- .../io/parser/dtypes/test_dtypes_basic.py | 4 --- pandas/tests/io/parser/test_c_parser_only.py | 13 ++++---- pandas/tests/io/parser/test_converters.py | 5 +-- pandas/tests/io/parser/test_index_col.py | 5 +-- pandas/tests/io/parser/test_mangle_dupes.py | 10 +++--- pandas/tests/io/parser/test_na_values.py | 31 ++++++++++--------- pandas/tests/io/parser/test_parse_dates.py | 11 ++----- pandas/tests/io/parser/test_upcast.py | 3 -- 11 files changed, 49 insertions(+), 63 deletions(-) diff --git a/pandas/tests/io/parser/common/test_chunksize.py b/pandas/tests/io/parser/common/test_chunksize.py index a6504473fb55f..65ad7273666e5 100644 --- a/pandas/tests/io/parser/common/test_chunksize.py +++ b/pandas/tests/io/parser/common/test_chunksize.py @@ -8,8 +8,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs import parsers as libparsers from pandas.errors import DtypeWarning @@ -231,8 +229,7 @@ def test_chunks_have_consistent_numerical_type(all_parsers, monkeypatch): assert result.a.dtype == float -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) -def test_warn_if_chunks_have_mismatched_type(all_parsers): +def test_warn_if_chunks_have_mismatched_type(all_parsers, using_infer_string): warning_type = None parser = all_parsers size = 10000 @@ -260,8 +257,12 @@ def test_warn_if_chunks_have_mismatched_type(all_parsers): "Specify dtype option on import or set low_memory=False.", buf, ) - - assert df.a.dtype == object + if parser.engine == "c" and parser.low_memory: + assert df.a.dtype == object + elif using_infer_string: + assert df.a.dtype == "str" + else: + assert df.a.dtype == object @pytest.mark.parametrize("iterator", [True, False]) diff --git a/pandas/tests/io/parser/common/test_file_buffer_url.py b/pandas/tests/io/parser/common/test_file_buffer_url.py index d8b8f24abcedd..cef57318195ec 100644 --- a/pandas/tests/io/parser/common/test_file_buffer_url.py +++ b/pandas/tests/io/parser/common/test_file_buffer_url.py @@ -15,8 +15,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import WASM from pandas.errors import ( EmptyDataError, @@ -71,14 +69,13 @@ def test_local_file(all_parsers, csv_dir_path): pytest.skip("Failing on: " + " ".join(platform.uname())) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @xfail_pyarrow # AssertionError: DataFrame.index are different def test_path_path_lib(all_parsers): parser = all_parsers df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=Index(list("ABCD"), dtype=object), - index=Index([f"i-{i}" for i in range(30)], dtype=object), + columns=Index(list("ABCD")), + index=Index([f"i-{i}" for i in range(30)]), ) result = tm.round_trip_pathlib(df.to_csv, lambda p: parser.read_csv(p, index_col=0)) tm.assert_frame_equal(df, result) diff --git a/pandas/tests/io/parser/common/test_index.py b/pandas/tests/io/parser/common/test_index.py index 54b59ac4e25ed..8352cc80f5e62 100644 --- a/pandas/tests/io/parser/common/test_index.py +++ b/pandas/tests/io/parser/common/test_index.py @@ -9,8 +9,6 @@ import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, Index, @@ -88,9 +86,13 @@ def test_pass_names_with_index(all_parsers, data, kwargs, expected): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("index_col", [[0, 1], [1, 0]]) -def test_multi_index_no_level_names(all_parsers, index_col): +def test_multi_index_no_level_names( + request, all_parsers, index_col, using_infer_string +): + if using_infer_string and all_parsers.engine == "pyarrow": + # result should have string columns instead of object dtype + request.applymarker(pytest.mark.xfail(reason="TODO(infer_string)")) data = """index1,index2,A,B,C,D foo,one,2,3,4,5 foo,two,7,8,9,10 diff --git a/pandas/tests/io/parser/dtypes/test_dtypes_basic.py b/pandas/tests/io/parser/dtypes/test_dtypes_basic.py index e02562ac8d93d..75b7cf0d42cb8 100644 --- a/pandas/tests/io/parser/dtypes/test_dtypes_basic.py +++ b/pandas/tests/io/parser/dtypes/test_dtypes_basic.py @@ -9,8 +9,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import ParserWarning import pandas as pd @@ -57,7 +55,6 @@ def test_dtype_all_columns(all_parsers, dtype, check_orig, using_infer_string): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.usefixtures("pyarrow_xfail") def test_dtype_per_column(all_parsers): parser = all_parsers @@ -71,7 +68,6 @@ def test_dtype_per_column(all_parsers): [[1, "2.5"], [2, "3.5"], [3, "4.5"], [4, "5.5"]], columns=["one", "two"] ) expected["one"] = expected["one"].astype(np.float64) - expected["two"] = expected["two"].astype(object) result = parser.read_csv(StringIO(data), dtype={"one": np.float64, 1: str}) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_c_parser_only.py b/pandas/tests/io/parser/test_c_parser_only.py index 9226f265ca2b3..11a30a26f91ef 100644 --- a/pandas/tests/io/parser/test_c_parser_only.py +++ b/pandas/tests/io/parser/test_c_parser_only.py @@ -18,8 +18,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import WASM from pandas.compat.numpy import np_version_gte1p24 from pandas.errors import ( @@ -184,8 +182,7 @@ def error(val: float, actual_val: Decimal) -> Decimal: assert max(precise_errors) <= max(normal_errors) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") -def test_usecols_dtypes(c_parser_only): +def test_usecols_dtypes(c_parser_only, using_infer_string): parser = c_parser_only data = """\ 1,2,3 @@ -210,8 +207,12 @@ def test_usecols_dtypes(c_parser_only): dtype={"b": int, "c": float}, ) - assert (result.dtypes == [object, int, float]).all() - assert (result2.dtypes == [object, float]).all() + if using_infer_string: + assert (result.dtypes == ["string", int, float]).all() + assert (result2.dtypes == ["string", float]).all() + else: + assert (result.dtypes == [object, int, float]).all() + assert (result2.dtypes == [object, float]).all() def test_disable_bool_parsing(c_parser_only): diff --git a/pandas/tests/io/parser/test_converters.py b/pandas/tests/io/parser/test_converters.py index 0423327c7333c..c6ba2213033ea 100644 --- a/pandas/tests/io/parser/test_converters.py +++ b/pandas/tests/io/parser/test_converters.py @@ -9,8 +9,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -188,7 +186,6 @@ def convert_score(x): tm.assert_frame_equal(results[0], results[1]) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("conv_f", [lambda x: x, str]) def test_converter_index_col_bug(all_parsers, conv_f): # see gh-1835 , GH#40589 @@ -207,7 +204,7 @@ def test_converter_index_col_bug(all_parsers, conv_f): StringIO(data), sep=";", index_col="A", converters={"A": conv_f} ) - xp = DataFrame({"B": [2, 4]}, index=Index(["1", "3"], name="A", dtype="object")) + xp = DataFrame({"B": [2, 4]}, index=Index(["1", "3"], name="A")) tm.assert_frame_equal(rs, xp) diff --git a/pandas/tests/io/parser/test_index_col.py b/pandas/tests/io/parser/test_index_col.py index ce2ed5e9764bd..9977e2b8e1a1d 100644 --- a/pandas/tests/io/parser/test_index_col.py +++ b/pandas/tests/io/parser/test_index_col.py @@ -9,8 +9,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, Index, @@ -345,7 +343,6 @@ def test_infer_types_boolean_sum(all_parsers): tm.assert_frame_equal(result, expected, check_index_type=False) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("dtype, val", [(object, "01"), ("int64", 1)]) def test_specify_dtype_for_index_col(all_parsers, dtype, val, request): # GH#9435 @@ -356,7 +353,7 @@ def test_specify_dtype_for_index_col(all_parsers, dtype, val, request): pytest.mark.xfail(reason="Cannot disable type-inference for pyarrow engine") ) result = parser.read_csv(StringIO(data), index_col="a", dtype={"a": dtype}) - expected = DataFrame({"b": [2]}, index=Index([val], name="a")) + expected = DataFrame({"b": [2]}, index=Index([val], name="a", dtype=dtype)) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_mangle_dupes.py b/pandas/tests/io/parser/test_mangle_dupes.py index 6a2ae3bffdc74..d3789cd387c05 100644 --- a/pandas/tests/io/parser/test_mangle_dupes.py +++ b/pandas/tests/io/parser/test_mangle_dupes.py @@ -8,9 +8,10 @@ import pytest -from pandas._config import using_string_dtype - -from pandas import DataFrame +from pandas import ( + DataFrame, + Index, +) import pandas._testing as tm xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail") @@ -121,7 +122,6 @@ def test_thorough_mangle_names(all_parsers, data, names, expected): parser.read_csv(StringIO(data), names=names) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @xfail_pyarrow # AssertionError: DataFrame.columns are different def test_mangled_unnamed_placeholders(all_parsers): # xref gh-13017 @@ -133,7 +133,7 @@ def test_mangled_unnamed_placeholders(all_parsers): # This test recursively updates `df`. for i in range(3): - expected = DataFrame() + expected = DataFrame(columns=Index([], dtype="str")) for j in range(i + 1): col_name = "Unnamed: 0" + f".{1*j}" * min(j, 1) diff --git a/pandas/tests/io/parser/test_na_values.py b/pandas/tests/io/parser/test_na_values.py index 89645b526f2ee..3a68d38cc0bde 100644 --- a/pandas/tests/io/parser/test_na_values.py +++ b/pandas/tests/io/parser/test_na_values.py @@ -8,8 +8,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs.parsers import STR_NA_VALUES from pandas import ( @@ -261,7 +259,6 @@ def test_na_value_dict_multi_index(all_parsers, index_col, expected): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "kwargs,expected", [ @@ -299,7 +296,9 @@ def test_na_value_dict_multi_index(all_parsers, index_col, expected): ), ], ) -def test_na_values_keep_default(all_parsers, kwargs, expected, request): +def test_na_values_keep_default( + all_parsers, kwargs, expected, request, using_infer_string +): data = """\ A,B,C a,1,one @@ -317,8 +316,9 @@ def test_na_values_keep_default(all_parsers, kwargs, expected, request): with pytest.raises(ValueError, match=msg): parser.read_csv(StringIO(data), **kwargs) return - mark = pytest.mark.xfail() - request.applymarker(mark) + if not using_infer_string or "na_values" in kwargs: + mark = pytest.mark.xfail() + request.applymarker(mark) result = parser.read_csv(StringIO(data), **kwargs) expected = DataFrame(expected) @@ -429,8 +429,6 @@ def test_no_keep_default_na_dict_na_values_diff_reprs(all_parsers, col_zero_na_v tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) -@xfail_pyarrow # mismatched dtypes in both cases, FutureWarning in the True case @pytest.mark.parametrize( "na_filter,row_data", [ @@ -438,14 +436,21 @@ def test_no_keep_default_na_dict_na_values_diff_reprs(all_parsers, col_zero_na_v (False, [["1", "A"], ["nan", "B"], ["3", "C"]]), ], ) -def test_na_values_na_filter_override(all_parsers, na_filter, row_data): +def test_na_values_na_filter_override( + request, all_parsers, na_filter, row_data, using_infer_string +): + parser = all_parsers + if parser.engine == "pyarrow": + # mismatched dtypes in both cases, FutureWarning in the True case + if not (using_infer_string and na_filter): + mark = pytest.mark.xfail(reason="pyarrow doesn't support this.") + request.applymarker(mark) data = """\ A,B 1,A nan,B 3,C """ - parser = all_parsers result = parser.read_csv(StringIO(data), na_values=["B"], na_filter=na_filter) expected = DataFrame(row_data, columns=["A", "B"]) @@ -536,7 +541,6 @@ def test_na_values_dict_aliasing(all_parsers): tm.assert_dict_equal(na_values, na_values_copy) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_na_values_dict_null_column_name(all_parsers): # see gh-57547 parser = all_parsers @@ -560,11 +564,10 @@ def test_na_values_dict_null_column_name(all_parsers): return expected = DataFrame( - {None: ["MA", "NA", "OA"], "x": [1.0, 2.0, np.nan], "y": [2.0, 1.0, 3.0]} + {"x": [1.0, 2.0, np.nan], "y": [2.0, 1.0, 3.0]}, + index=Index(["MA", "NA", "OA"], dtype=object), ) - expected = expected.set_index(None) - result = parser.read_csv( StringIO(data), index_col=0, diff --git a/pandas/tests/io/parser/test_parse_dates.py b/pandas/tests/io/parser/test_parse_dates.py index 532fcc5cd880c..1411ed5019766 100644 --- a/pandas/tests/io/parser/test_parse_dates.py +++ b/pandas/tests/io/parser/test_parse_dates.py @@ -13,8 +13,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -421,7 +419,6 @@ def test_parse_timezone(all_parsers): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @skip_pyarrow # pandas.errors.ParserError: CSV parse error @pytest.mark.parametrize( "date_string", @@ -429,7 +426,7 @@ def test_parse_timezone(all_parsers): ) def test_invalid_parse_delimited_date(all_parsers, date_string): parser = all_parsers - expected = DataFrame({0: [date_string]}, dtype="object") + expected = DataFrame({0: [date_string]}, dtype="str") result = parser.read_csv( StringIO(date_string), header=None, @@ -609,7 +606,6 @@ def test_date_parser_usecols_thousands(all_parsers): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_dayfirst_warnings(): # GH 12585 @@ -642,7 +638,7 @@ def test_dayfirst_warnings(): # first in DD/MM/YYYY, second in MM/DD/YYYY input = "date\n31/12/2014\n03/30/2011" - expected = Index(["31/12/2014", "03/30/2011"], dtype="object", name="date") + expected = Index(["31/12/2014", "03/30/2011"], dtype="str", name="date") # A. use dayfirst=True res5 = read_csv( @@ -752,7 +748,6 @@ def test_parse_dates_and_string_dtype(all_parsers): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_parse_dot_separated_dates(all_parsers): # https://github.com/pandas-dev/pandas/issues/2586 parser = all_parsers @@ -762,7 +757,7 @@ def test_parse_dot_separated_dates(all_parsers): if parser.engine == "pyarrow": expected_index = Index( ["27.03.2003 14:55:00.000", "03.08.2003 15:20:00.000"], - dtype="object", + dtype="str", name="a", ) warn = None diff --git a/pandas/tests/io/parser/test_upcast.py b/pandas/tests/io/parser/test_upcast.py index 01e576ba40f26..bc4c4c2e24e9c 100644 --- a/pandas/tests/io/parser/test_upcast.py +++ b/pandas/tests/io/parser/test_upcast.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs.parsers import ( _maybe_upcast, na_values, @@ -86,7 +84,6 @@ def test_maybe_upcaste_all_nan(): tm.assert_extension_array_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("val", [na_values[np.object_], "c"]) def test_maybe_upcast_object(val, string_storage): # GH#36712 From 12d6f602eea98275553ac456f90201151b1f9bf8 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 15 Nov 2024 19:08:46 +0100 Subject: [PATCH 072/266] REF: centralize pyarrow Table to pandas conversions and types_mapper handling (#60324) --- pandas/io/_util.py | 42 +++++++++++++++++++++-- pandas/io/feather_format.py | 17 ++------- pandas/io/json/_json.py | 15 ++------ pandas/io/orc.py | 21 ++---------- pandas/io/parquet.py | 18 ++-------- pandas/io/parsers/arrow_parser_wrapper.py | 27 ++++----------- pandas/io/sql.py | 41 ++++------------------ pandas/tests/io/test_sql.py | 4 +-- 8 files changed, 63 insertions(+), 122 deletions(-) diff --git a/pandas/io/_util.py b/pandas/io/_util.py index 9a8c87a738d4c..21203ad036fc6 100644 --- a/pandas/io/_util.py +++ b/pandas/io/_util.py @@ -1,9 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import ( + TYPE_CHECKING, + Literal, +) import numpy as np +from pandas._config import using_string_dtype + +from pandas._libs import lib from pandas.compat import pa_version_under18p0 from pandas.compat._optional import import_optional_dependency @@ -12,6 +18,10 @@ if TYPE_CHECKING: from collections.abc import Callable + import pyarrow + + from pandas._typing import DtypeBackend + def _arrow_dtype_mapping() -> dict: pa = import_optional_dependency("pyarrow") @@ -33,7 +43,7 @@ def _arrow_dtype_mapping() -> dict: } -def arrow_string_types_mapper() -> Callable: +def _arrow_string_types_mapper() -> Callable: pa = import_optional_dependency("pyarrow") mapping = { @@ -44,3 +54,31 @@ def arrow_string_types_mapper() -> Callable: mapping[pa.string_view()] = pd.StringDtype(na_value=np.nan) return mapping.get + + +def arrow_table_to_pandas( + table: pyarrow.Table, + dtype_backend: DtypeBackend | Literal["numpy"] | lib.NoDefault = lib.no_default, + null_to_int64: bool = False, +) -> pd.DataFrame: + pa = import_optional_dependency("pyarrow") + + types_mapper: type[pd.ArrowDtype] | None | Callable + if dtype_backend == "numpy_nullable": + mapping = _arrow_dtype_mapping() + if null_to_int64: + # Modify the default mapping to also map null to Int64 + # (to match other engines - only for CSV parser) + mapping[pa.null()] = pd.Int64Dtype() + types_mapper = mapping.get + elif dtype_backend == "pyarrow": + types_mapper = pd.ArrowDtype + elif using_string_dtype(): + types_mapper = _arrow_string_types_mapper() + elif dtype_backend is lib.no_default or dtype_backend == "numpy": + types_mapper = None + else: + raise NotImplementedError + + df = table.to_pandas(types_mapper=types_mapper) + return df diff --git a/pandas/io/feather_format.py b/pandas/io/feather_format.py index aaae9857b4fae..7b4c81853eba3 100644 --- a/pandas/io/feather_format.py +++ b/pandas/io/feather_format.py @@ -15,11 +15,10 @@ from pandas.util._decorators import doc from pandas.util._validators import check_dtype_backend -import pandas as pd from pandas.core.api import DataFrame from pandas.core.shared_docs import _shared_docs -from pandas.io._util import arrow_string_types_mapper +from pandas.io._util import arrow_table_to_pandas from pandas.io.common import get_handle if TYPE_CHECKING: @@ -147,16 +146,4 @@ def read_feather( pa_table = feather.read_table( handles.handle, columns=columns, use_threads=bool(use_threads) ) - - if dtype_backend == "numpy_nullable": - from pandas.io._util import _arrow_dtype_mapping - - return pa_table.to_pandas(types_mapper=_arrow_dtype_mapping().get) - - elif dtype_backend == "pyarrow": - return pa_table.to_pandas(types_mapper=pd.ArrowDtype) - - elif using_string_dtype(): - return pa_table.to_pandas(types_mapper=arrow_string_types_mapper()) - else: - raise NotImplementedError + return arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) diff --git a/pandas/io/json/_json.py b/pandas/io/json/_json.py index e9c9f5ba225a5..983780f81043f 100644 --- a/pandas/io/json/_json.py +++ b/pandas/io/json/_json.py @@ -36,7 +36,6 @@ from pandas.core.dtypes.dtypes import PeriodDtype from pandas import ( - ArrowDtype, DataFrame, Index, MultiIndex, @@ -48,6 +47,7 @@ from pandas.core.reshape.concat import concat from pandas.core.shared_docs import _shared_docs +from pandas.io._util import arrow_table_to_pandas from pandas.io.common import ( IOHandles, dedup_names, @@ -940,18 +940,7 @@ def read(self) -> DataFrame | Series: if self.engine == "pyarrow": pyarrow_json = import_optional_dependency("pyarrow.json") pa_table = pyarrow_json.read_json(self.data) - - mapping: type[ArrowDtype] | None | Callable - if self.dtype_backend == "pyarrow": - mapping = ArrowDtype - elif self.dtype_backend == "numpy_nullable": - from pandas.io._util import _arrow_dtype_mapping - - mapping = _arrow_dtype_mapping().get - else: - mapping = None - - return pa_table.to_pandas(types_mapper=mapping) + return arrow_table_to_pandas(pa_table, dtype_backend=self.dtype_backend) elif self.engine == "ujson": if self.lines: if self.chunksize: diff --git a/pandas/io/orc.py b/pandas/io/orc.py index f179dafc919e5..a945f3dc38d35 100644 --- a/pandas/io/orc.py +++ b/pandas/io/orc.py @@ -9,16 +9,13 @@ Literal, ) -from pandas._config import using_string_dtype - from pandas._libs import lib from pandas.compat._optional import import_optional_dependency from pandas.util._validators import check_dtype_backend -import pandas as pd from pandas.core.indexes.api import default_index -from pandas.io._util import arrow_string_types_mapper +from pandas.io._util import arrow_table_to_pandas from pandas.io.common import ( get_handle, is_fsspec_url, @@ -127,21 +124,7 @@ def read_orc( pa_table = orc.read_table( source=source, columns=columns, filesystem=filesystem, **kwargs ) - if dtype_backend is not lib.no_default: - if dtype_backend == "pyarrow": - df = pa_table.to_pandas(types_mapper=pd.ArrowDtype) - else: - from pandas.io._util import _arrow_dtype_mapping - - mapping = _arrow_dtype_mapping() - df = pa_table.to_pandas(types_mapper=mapping.get) - return df - else: - if using_string_dtype(): - types_mapper = arrow_string_types_mapper() - else: - types_mapper = None - return pa_table.to_pandas(types_mapper=types_mapper) + return arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) def to_orc( diff --git a/pandas/io/parquet.py b/pandas/io/parquet.py index 24415299e799b..116f228faca93 100644 --- a/pandas/io/parquet.py +++ b/pandas/io/parquet.py @@ -15,22 +15,19 @@ filterwarnings, ) -from pandas._config import using_string_dtype - from pandas._libs import lib from pandas.compat._optional import import_optional_dependency from pandas.errors import AbstractMethodError from pandas.util._decorators import doc from pandas.util._validators import check_dtype_backend -import pandas as pd from pandas import ( DataFrame, get_option, ) from pandas.core.shared_docs import _shared_docs -from pandas.io._util import arrow_string_types_mapper +from pandas.io._util import arrow_table_to_pandas from pandas.io.common import ( IOHandles, get_handle, @@ -249,17 +246,6 @@ def read( ) -> DataFrame: kwargs["use_pandas_metadata"] = True - to_pandas_kwargs = {} - if dtype_backend == "numpy_nullable": - from pandas.io._util import _arrow_dtype_mapping - - mapping = _arrow_dtype_mapping() - to_pandas_kwargs["types_mapper"] = mapping.get - elif dtype_backend == "pyarrow": - to_pandas_kwargs["types_mapper"] = pd.ArrowDtype # type: ignore[assignment] - elif using_string_dtype(): - to_pandas_kwargs["types_mapper"] = arrow_string_types_mapper() - path_or_handle, handles, filesystem = _get_path_or_handle( path, filesystem, @@ -280,7 +266,7 @@ def read( "make_block is deprecated", DeprecationWarning, ) - result = pa_table.to_pandas(**to_pandas_kwargs) + result = arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) if pa_table.schema.metadata: if b"PANDAS_ATTRS" in pa_table.schema.metadata: diff --git a/pandas/io/parsers/arrow_parser_wrapper.py b/pandas/io/parsers/arrow_parser_wrapper.py index 86bb5f190e403..672672490996d 100644 --- a/pandas/io/parsers/arrow_parser_wrapper.py +++ b/pandas/io/parsers/arrow_parser_wrapper.py @@ -3,8 +3,6 @@ from typing import TYPE_CHECKING import warnings -from pandas._config import using_string_dtype - from pandas._libs import lib from pandas.compat._optional import import_optional_dependency from pandas.errors import ( @@ -16,18 +14,14 @@ from pandas.core.dtypes.common import pandas_dtype from pandas.core.dtypes.inference import is_integer -import pandas as pd -from pandas import DataFrame - -from pandas.io._util import ( - _arrow_dtype_mapping, - arrow_string_types_mapper, -) +from pandas.io._util import arrow_table_to_pandas from pandas.io.parsers.base_parser import ParserBase if TYPE_CHECKING: from pandas._typing import ReadBuffer + from pandas import DataFrame + class ArrowParserWrapper(ParserBase): """ @@ -293,17 +287,8 @@ def read(self) -> DataFrame: "make_block is deprecated", DeprecationWarning, ) - if dtype_backend == "pyarrow": - frame = table.to_pandas(types_mapper=pd.ArrowDtype) - elif dtype_backend == "numpy_nullable": - # Modify the default mapping to also - # map null to Int64 (to match other engines) - dtype_mapping = _arrow_dtype_mapping() - dtype_mapping[pa.null()] = pd.Int64Dtype() - frame = table.to_pandas(types_mapper=dtype_mapping.get) - elif using_string_dtype(): - frame = table.to_pandas(types_mapper=arrow_string_types_mapper()) + frame = arrow_table_to_pandas( + table, dtype_backend=dtype_backend, null_to_int64=True + ) - else: - frame = table.to_pandas() return self._finalize_pandas_output(frame) diff --git a/pandas/io/sql.py b/pandas/io/sql.py index 125ca51a456d8..3c0c5cc64c24c 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -48,10 +48,7 @@ is_object_dtype, is_string_dtype, ) -from pandas.core.dtypes.dtypes import ( - ArrowDtype, - DatetimeTZDtype, -) +from pandas.core.dtypes.dtypes import DatetimeTZDtype from pandas.core.dtypes.missing import isna from pandas import get_option @@ -67,6 +64,8 @@ from pandas.core.internals.construction import convert_object_array from pandas.core.tools.datetimes import to_datetime +from pandas.io._util import arrow_table_to_pandas + if TYPE_CHECKING: from collections.abc import ( Callable, @@ -2208,23 +2207,10 @@ def read_table( else: stmt = f"SELECT {select_list} FROM {table_name}" - mapping: type[ArrowDtype] | None | Callable - if dtype_backend == "pyarrow": - mapping = ArrowDtype - elif dtype_backend == "numpy_nullable": - from pandas.io._util import _arrow_dtype_mapping - - mapping = _arrow_dtype_mapping().get - elif using_string_dtype(): - from pandas.io._util import arrow_string_types_mapper - - mapping = arrow_string_types_mapper() - else: - mapping = None - with self.con.cursor() as cur: cur.execute(stmt) - df = cur.fetch_arrow_table().to_pandas(types_mapper=mapping) + pa_table = cur.fetch_arrow_table() + df = arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) return _wrap_result_adbc( df, @@ -2292,23 +2278,10 @@ def read_query( if chunksize: raise NotImplementedError("'chunksize' is not implemented for ADBC drivers") - mapping: type[ArrowDtype] | None | Callable - if dtype_backend == "pyarrow": - mapping = ArrowDtype - elif dtype_backend == "numpy_nullable": - from pandas.io._util import _arrow_dtype_mapping - - mapping = _arrow_dtype_mapping().get - elif using_string_dtype(): - from pandas.io._util import arrow_string_types_mapper - - mapping = arrow_string_types_mapper() - else: - mapping = None - with self.con.cursor() as cur: cur.execute(sql) - df = cur.fetch_arrow_table().to_pandas(types_mapper=mapping) + pa_table = cur.fetch_arrow_table() + df = arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) return _wrap_result_adbc( df, diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index 96d63d3fe25e5..7e1220ecee218 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -959,12 +959,12 @@ def sqlite_buildin_types(sqlite_buildin, types_data): adbc_connectable_iris = [ pytest.param("postgresql_adbc_iris", marks=pytest.mark.db), - pytest.param("sqlite_adbc_iris", marks=pytest.mark.db), + "sqlite_adbc_iris", ] adbc_connectable_types = [ pytest.param("postgresql_adbc_types", marks=pytest.mark.db), - pytest.param("sqlite_adbc_types", marks=pytest.mark.db), + "sqlite_adbc_types", ] From 3895156ecc760a0ef23d372b7331468499e1d6e4 Mon Sep 17 00:00:00 2001 From: ensalada-de-pechuga <127701043+ensalada-de-pechuga@users.noreply.github.com> Date: Sat, 16 Nov 2024 03:18:34 +0900 Subject: [PATCH 073/266] DOC: Fix docstrings api.types.pandas_dtype (#60319) * add api.types.pandas_dtype PR07, RT03, SA01 docstrings * remove from code_checks.sh * edit see also --------- Co-authored-by: root --- ci/code_checks.sh | 1 - pandas/core/dtypes/common.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index fae1a7abba6a8..53690e9b78b8a 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -84,7 +84,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ -i "pandas.api.types.is_re_compilable PR07,SA01" \ - -i "pandas.api.types.pandas_dtype PR07,RT03,SA01" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ -i "pandas.arrays.IntegerArray SA01" \ -i "pandas.arrays.IntervalArray.length SA01" \ diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 98c770ec4a8b0..8f93b1a397c1f 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1785,16 +1785,22 @@ def pandas_dtype(dtype) -> DtypeObj: Parameters ---------- - dtype : object to be converted + dtype : object + The object to be converted into a dtype. Returns ------- np.dtype or a pandas dtype + The converted dtype, which can be either a numpy dtype or a pandas dtype. Raises ------ TypeError if not a dtype + See Also + -------- + api.types.is_dtype : Return true if the condition is satisfied for the arr_or_dtype. + Examples -------- >>> pd.api.types.pandas_dtype(int) From 63d3971328f96fb176305ff2886e5b67a9c39454 Mon Sep 17 00:00:00 2001 From: Kevin Amparado <109636487+KevsterAmp@users.noreply.github.com> Date: Sat, 16 Nov 2024 02:20:11 +0800 Subject: [PATCH 074/266] TST: Add test for `pd.read_csv` date parsing not working with `dtype_backend="pyarrow"` and missing values (#60286) * add test func * remove io since StringIO is already imported * add td.skip_if_no("pyarrow") from CI errors * fix test * improve expect_data; remove assert on "date" column dtype --- pandas/tests/io/test_common.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 506276a25dad6..70422a0ea6edc 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -671,3 +671,17 @@ def test_pickle_reader(reader): # GH 22265 with BytesIO() as buffer: pickle.dump(reader, buffer) + + +@td.skip_if_no("pyarrow") +def test_pyarrow_read_csv_datetime_dtype(): + # GH 59904 + data = '"date"\n"20/12/2025"\n""\n"31/12/2020"' + result = pd.read_csv( + StringIO(data), parse_dates=["date"], dayfirst=True, dtype_backend="pyarrow" + ) + + expect_data = pd.to_datetime(["20/12/2025", pd.NaT, "31/12/2020"], dayfirst=True) + expect = pd.DataFrame({"date": expect_data}) + + tm.assert_frame_equal(expect, result) From fae3e8034faf66eb8ef00bcbed73d48e4ef791d3 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 15 Nov 2024 19:50:49 +0100 Subject: [PATCH 075/266] TST (string dtype): resolve xfails for frame fillna and replace tests + fix bug in replace for string (#60295) * TST (string dtype): resolve xfails for frame fillna and replace tests + fix bug in replace for string * fix fillna upcast issue * fix reshaping of condition in where - only do for 2d blocks --- pandas/core/array_algos/replace.py | 2 + pandas/core/internals/blocks.py | 7 ++ pandas/tests/frame/methods/test_fillna.py | 57 +++++-------- pandas/tests/frame/methods/test_replace.py | 94 +++++++++++----------- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/pandas/core/array_algos/replace.py b/pandas/core/array_algos/replace.py index f946c5adcbb0b..a9ad66b7cb2e5 100644 --- a/pandas/core/array_algos/replace.py +++ b/pandas/core/array_algos/replace.py @@ -151,4 +151,6 @@ def re_replacer(s): if mask is None: values[:] = f(values) else: + if values.ndim != mask.ndim: + mask = np.broadcast_to(mask, values.shape) values[mask] = f(values[mask]) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 3c207e8c14b5b..54273ff89f1af 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1688,6 +1688,13 @@ def where(self, other, cond) -> list[Block]: if isinstance(self.dtype, (IntervalDtype, StringDtype)): # TestSetitemFloatIntervalWithIntIntervalValues blk = self.coerce_to_target_dtype(orig_other, raise_on_upcast=False) + if ( + self.ndim == 2 + and isinstance(orig_cond, np.ndarray) + and orig_cond.ndim == 1 + and not is_1d_only_ea_dtype(blk.dtype) + ): + orig_cond = orig_cond[:, None] return blk.where(orig_other, orig_cond) elif isinstance(self, NDArrayBackedExtensionBlock): diff --git a/pandas/tests/frame/methods/test_fillna.py b/pandas/tests/frame/methods/test_fillna.py index ad1a37916e381..67d1d45af1cb3 100644 --- a/pandas/tests/frame/methods/test_fillna.py +++ b/pandas/tests/frame/methods/test_fillna.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( Categorical, DataFrame, @@ -65,15 +63,20 @@ def test_fillna_datetime(self, datetime_frame): with pytest.raises(TypeError, match=msg): datetime_frame.fillna() - # TODO(infer_string) test as actual error instead of xfail - @pytest.mark.xfail(using_string_dtype(), reason="can't fill 0 in string") - def test_fillna_mixed_type(self, float_string_frame): + def test_fillna_mixed_type(self, float_string_frame, using_infer_string): mf = float_string_frame mf.loc[mf.index[5:20], "foo"] = np.nan mf.loc[mf.index[-10:], "A"] = np.nan - # TODO: make stronger assertion here, GH 25640 - mf.fillna(value=0) - mf.ffill() + + result = mf.ffill() + assert ( + result.loc[result.index[-10:], "A"] == result.loc[result.index[-11], "A"] + ).all() + assert (result.loc[result.index[5:20], "foo"] == "bar").all() + + result = mf.fillna(value=0) + assert (result.loc[result.index[-10:], "A"] == 0).all() + assert (result.loc[result.index[5:20], "foo"] == 0).all() def test_fillna_mixed_float(self, mixed_float_frame): # mixed numeric (but no float16) @@ -84,28 +87,21 @@ def test_fillna_mixed_float(self, mixed_float_frame): result = mf.ffill() _check_mixed_float(result, dtype={"C": None}) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_fillna_different_dtype(self, using_infer_string): + def test_fillna_different_dtype(self): # with different dtype (GH#3386) df = DataFrame( [["a", "a", np.nan, "a"], ["b", "b", np.nan, "b"], ["c", "c", np.nan, "c"]] ) - if using_infer_string: - with tm.assert_produces_warning(FutureWarning, match="Downcasting"): - result = df.fillna({2: "foo"}) - else: - result = df.fillna({2: "foo"}) + result = df.fillna({2: "foo"}) expected = DataFrame( [["a", "a", "foo", "a"], ["b", "b", "foo", "b"], ["c", "c", "foo", "c"]] ) + # column is originally float (all-NaN) -> filling with string gives object dtype + expected[2] = expected[2].astype("object") tm.assert_frame_equal(result, expected) - if using_infer_string: - with tm.assert_produces_warning(FutureWarning, match="Downcasting"): - return_value = df.fillna({2: "foo"}, inplace=True) - else: - return_value = df.fillna({2: "foo"}, inplace=True) + return_value = df.fillna({2: "foo"}, inplace=True) tm.assert_frame_equal(df, expected) assert return_value is None @@ -276,8 +272,7 @@ def test_fillna_dictlike_value_duplicate_colnames(self, columns): expected["A"] = 0.0 tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_fillna_dtype_conversion(self, using_infer_string): + def test_fillna_dtype_conversion(self): # make sure that fillna on an empty frame works df = DataFrame(index=["A", "B", "C"], columns=[1, 2, 3, 4, 5]) result = df.dtypes @@ -292,7 +287,7 @@ def test_fillna_dtype_conversion(self, using_infer_string): # empty block df = DataFrame(index=range(3), columns=["A", "B"], dtype="float64") result = df.fillna("nan") - expected = DataFrame("nan", index=range(3), columns=["A", "B"]) + expected = DataFrame("nan", dtype="object", index=range(3), columns=["A", "B"]) tm.assert_frame_equal(result, expected) @pytest.mark.parametrize("val", ["", 1, np.nan, 1.0]) @@ -540,18 +535,10 @@ def test_fillna_col_reordering(self): filled = df.ffill() assert df.columns.tolist() == filled.columns.tolist() - # TODO(infer_string) test as actual error instead of xfail - @pytest.mark.xfail(using_string_dtype(), reason="can't fill 0 in string") - def test_fill_corner(self, float_frame, float_string_frame): - mf = float_string_frame - mf.loc[mf.index[5:20], "foo"] = np.nan - mf.loc[mf.index[-10:], "A"] = np.nan - - filled = float_string_frame.fillna(value=0) - assert (filled.loc[filled.index[5:20], "foo"] == 0).all() - del float_string_frame["foo"] - - float_frame.reindex(columns=[]).fillna(value=0) + def test_fill_empty(self, float_frame): + df = float_frame.reindex(columns=[]) + result = df.fillna(value=0) + tm.assert_frame_equal(result, df) def test_fillna_with_columns_and_limit(self): # GH40989 diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py index 73f44bcc6657e..b2320798ea9a2 100644 --- a/pandas/tests/frame/methods/test_replace.py +++ b/pandas/tests/frame/methods/test_replace.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -30,7 +28,6 @@ def mix_abc() -> dict[str, list[float | str]]: class TestDataFrameReplace: - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_replace_inplace(self, datetime_frame, float_string_frame): datetime_frame.loc[datetime_frame.index[:5], "A"] = np.nan datetime_frame.loc[datetime_frame.index[-5:], "A"] = np.nan @@ -46,7 +43,9 @@ def test_replace_inplace(self, datetime_frame, float_string_frame): mf.iloc[-10:, mf.columns.get_loc("A")] = np.nan result = float_string_frame.replace(np.nan, 0) - expected = float_string_frame.fillna(value=0) + expected = float_string_frame.copy() + expected["foo"] = expected["foo"].astype(object) + expected = expected.fillna(value=0) tm.assert_frame_equal(result, expected) tsframe = datetime_frame.copy() @@ -291,22 +290,20 @@ def test_regex_replace_dict_nested_non_first_character( expected = DataFrame({"first": [".bc", "bc.", "c.b"]}, dtype=dtype) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_regex_replace_dict_nested_gh4115(self): - df = DataFrame({"Type": ["Q", "T", "Q", "Q", "T"], "tmp": 2}) - expected = DataFrame( - {"Type": Series([0, 1, 0, 0, 1], dtype=df.Type.dtype), "tmp": 2} + df = DataFrame( + {"Type": Series(["Q", "T", "Q", "Q", "T"], dtype=object), "tmp": 2} ) + expected = DataFrame({"Type": Series([0, 1, 0, 0, 1], dtype=object), "tmp": 2}) result = df.replace({"Type": {"Q": 0, "T": 1}}) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_regex_replace_list_to_scalar(self, mix_abc): df = DataFrame(mix_abc) expec = DataFrame( { "a": mix_abc["a"], - "b": np.array([np.nan] * 4, dtype=object), + "b": Series([np.nan] * 4, dtype="str"), "c": [np.nan, np.nan, np.nan, "d"], } ) @@ -326,7 +323,6 @@ def test_regex_replace_list_to_scalar(self, mix_abc): tm.assert_frame_equal(res2, expec) tm.assert_frame_equal(res3, expec) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_regex_replace_str_to_numeric(self, mix_abc): # what happens when you try to replace a numeric value with a regex? df = DataFrame(mix_abc) @@ -338,11 +334,12 @@ def test_regex_replace_str_to_numeric(self, mix_abc): return_value = res3.replace(regex=r"\s*\.\s*", value=0, inplace=True) assert return_value is None expec = DataFrame({"a": mix_abc["a"], "b": ["a", "b", 0, 0], "c": mix_abc["c"]}) + # TODO(infer_string) + expec["c"] = expec["c"].astype(object) tm.assert_frame_equal(res, expec) tm.assert_frame_equal(res2, expec) tm.assert_frame_equal(res3, expec) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_regex_replace_regex_list_to_numeric(self, mix_abc): df = DataFrame(mix_abc) res = df.replace([r"\s*\.\s*", "b"], 0, regex=True) @@ -535,31 +532,37 @@ def test_replace_series_dict(self): result = df.replace(s, df.mean()) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") - def test_replace_convert(self): - # gh 3907 - df = DataFrame([["foo", "bar", "bah"], ["bar", "foo", "bah"]]) + def test_replace_convert(self, any_string_dtype): + # gh 3907 (pandas >= 3.0 no longer converts dtypes) + df = DataFrame( + [["foo", "bar", "bah"], ["bar", "foo", "bah"]], dtype=any_string_dtype + ) m = {"foo": 1, "bar": 2, "bah": 3} rep = df.replace(m) - expec = df.dtypes - res = rep.dtypes - tm.assert_series_equal(expec, res) + assert (rep.dtypes == object).all() - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") def test_replace_mixed(self, float_string_frame): mf = float_string_frame mf.iloc[5:20, mf.columns.get_loc("foo")] = np.nan mf.iloc[-10:, mf.columns.get_loc("A")] = np.nan result = float_string_frame.replace(np.nan, -18) - expected = float_string_frame.fillna(value=-18) + expected = float_string_frame.copy() + expected["foo"] = expected["foo"].astype(object) + expected = expected.fillna(value=-18) tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result.replace(-18, np.nan), float_string_frame) + expected2 = float_string_frame.copy() + expected2["foo"] = expected2["foo"].astype(object) + tm.assert_frame_equal(result.replace(-18, np.nan), expected2) result = float_string_frame.replace(np.nan, -1e8) - expected = float_string_frame.fillna(value=-1e8) + expected = float_string_frame.copy() + expected["foo"] = expected["foo"].astype(object) + expected = expected.fillna(value=-1e8) tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result.replace(-1e8, np.nan), float_string_frame) + expected2 = float_string_frame.copy() + expected2["foo"] = expected2["foo"].astype(object) + tm.assert_frame_equal(result.replace(-1e8, np.nan), expected2) def test_replace_mixed_int_block_upcasting(self): # int block upcasting @@ -601,8 +604,7 @@ def test_replace_mixed_int_block_splitting(self): result = df.replace(0, 0.5) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_replace_mixed2(self, using_infer_string): + def test_replace_mixed2(self): # to object block upcasting df = DataFrame( { @@ -621,7 +623,7 @@ def test_replace_mixed2(self, using_infer_string): expected = DataFrame( { - "A": Series(["foo", "bar"]), + "A": Series(["foo", "bar"], dtype="object"), "B": Series([0, "foo"], dtype="object"), } ) @@ -917,8 +919,7 @@ def test_replace_limit(self): # TODO pass - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") - def test_replace_dict_no_regex(self): + def test_replace_dict_no_regex(self, any_string_dtype): answer = Series( { 0: "Strongly Agree", @@ -926,7 +927,8 @@ def test_replace_dict_no_regex(self): 2: "Neutral", 3: "Disagree", 4: "Strongly Disagree", - } + }, + dtype=any_string_dtype, ) weights = { "Agree": 4, @@ -935,11 +937,11 @@ def test_replace_dict_no_regex(self): "Strongly Agree": 5, "Strongly Disagree": 1, } - expected = Series({0: 5, 1: 4, 2: 3, 3: 2, 4: 1}, dtype=answer.dtype) + expected = Series({0: 5, 1: 4, 2: 3, 3: 2, 4: 1}, dtype=object) result = answer.replace(weights) tm.assert_series_equal(result, expected) - def test_replace_series_no_regex(self): + def test_replace_series_no_regex(self, any_string_dtype): answer = Series( { 0: "Strongly Agree", @@ -947,7 +949,8 @@ def test_replace_series_no_regex(self): 2: "Neutral", 3: "Disagree", 4: "Strongly Disagree", - } + }, + dtype=any_string_dtype, ) weights = Series( { @@ -1043,16 +1046,15 @@ def test_nested_dict_overlapping_keys_replace_str(self): expected = df.replace({"a": dict(zip(astr, bstr))}) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") - def test_replace_swapping_bug(self, using_infer_string): + def test_replace_swapping_bug(self): df = DataFrame({"a": [True, False, True]}) res = df.replace({"a": {True: "Y", False: "N"}}) - expect = DataFrame({"a": ["Y", "N", "Y"]}) + expect = DataFrame({"a": ["Y", "N", "Y"]}, dtype=object) tm.assert_frame_equal(res, expect) df = DataFrame({"a": [0, 1, 0]}) res = df.replace({"a": {0: "Y", 1: "N"}}) - expect = DataFrame({"a": ["Y", "N", "Y"]}) + expect = DataFrame({"a": ["Y", "N", "Y"]}, dtype=object) tm.assert_frame_equal(res, expect) def test_replace_datetimetz(self): @@ -1186,7 +1188,7 @@ def test_replace_commutative(self, df, to_replace, exp): ) def test_replace_replacer_dtype(self, replacer): # GH26632 - df = DataFrame(["a"]) + df = DataFrame(["a"], dtype=object) result = df.replace({"a": replacer, "b": replacer}) expected = DataFrame([replacer], dtype=object) tm.assert_frame_equal(result, expected) @@ -1266,7 +1268,6 @@ def test_categorical_replace_with_dict(self, replace_dict, final_data): assert return_value is None tm.assert_frame_equal(df, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_replace_value_category_type(self): """ Test for #23305: to ensure category dtypes are maintained @@ -1322,7 +1323,7 @@ def test_replace_value_category_type(self): lambda x: x.astype("category").cat.rename_categories({"cat2": "catX"}) ) - result = result.astype({"col1": "int64", "col3": "float64", "col5": "object"}) + result = result.astype({"col1": "int64", "col3": "float64", "col5": "str"}) tm.assert_frame_equal(result, expected) def test_replace_dict_category_type(self): @@ -1363,12 +1364,11 @@ def test_replace_with_compiled_regex(self): expected = DataFrame(["z", "b", "c"]) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_replace_intervals(self): # https://github.com/pandas-dev/pandas/issues/35931 df = DataFrame({"a": [pd.Interval(0, 1), pd.Interval(0, 1)]}) result = df.replace({"a": {pd.Interval(0, 1): "x"}}) - expected = DataFrame({"a": ["x", "x"]}) + expected = DataFrame({"a": ["x", "x"]}, dtype=object) tm.assert_frame_equal(result, expected) def test_replace_unicode(self): @@ -1468,17 +1468,21 @@ def test_regex_replace_scalar( expected.loc[expected["a"] == ".", "a"] = expected_replace_val tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="can't set float into string") @pytest.mark.parametrize("regex", [False, True]) def test_replace_regex_dtype_frame(self, regex): # GH-48644 df1 = DataFrame({"A": ["0"], "B": ["0"]}) - expected_df1 = DataFrame({"A": [1], "B": [1]}, dtype=df1.dtypes.iloc[0]) + expected_df1 = DataFrame({"A": [1], "B": [1]}, dtype=object) result_df1 = df1.replace(to_replace="0", value=1, regex=regex) tm.assert_frame_equal(result_df1, expected_df1) df2 = DataFrame({"A": ["0"], "B": ["1"]}) - expected_df2 = DataFrame({"A": [1], "B": ["1"]}, dtype=df2.dtypes.iloc[0]) + if regex: + # TODO(infer_string): both string columns get cast to object, + # while only needed for column A + expected_df2 = DataFrame({"A": [1], "B": ["1"]}, dtype=object) + else: + expected_df2 = DataFrame({"A": Series([1], dtype=object), "B": ["1"]}) result_df2 = df2.replace(to_replace="0", value=1, regex=regex) tm.assert_frame_equal(result_df2, expected_df2) From 54c88a2d9004ee8e5b45643ff108f7425e98aeb3 Mon Sep 17 00:00:00 2001 From: Patrick Hoefler <61934744+phofl@users.noreply.github.com> Date: Sat, 16 Nov 2024 20:21:22 +0100 Subject: [PATCH 076/266] BUG: get_indexer rountripping through string dtype (#56013) Co-authored-by: Joris Van den Bossche --- doc/source/whatsnew/v2.3.0.rst | 2 +- pandas/core/indexes/base.py | 11 ++++++++++- pandas/tests/indexes/object/test_indexing.py | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index da0fcfc2b3f64..b107a5d3ba100 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -118,7 +118,7 @@ Interval Indexing ^^^^^^^^ -- +- Fixed bug in :meth:`Index.get_indexer` round-tripping through string dtype when ``infer_string`` is enabled (:issue:`55834`) - Missing diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 4a90b164c89cc..d4ba7e01ebfa9 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -6556,7 +6556,16 @@ def _maybe_cast_listlike_indexer(self, target) -> Index: """ Analogue to maybe_cast_indexer for get_indexer instead of get_loc. """ - return ensure_index(target) + target_index = ensure_index(target) + if ( + not hasattr(target, "dtype") + and self.dtype == object + and target_index.dtype == "string" + ): + # If we started with a list-like, avoid inference to string dtype if self + # is object dtype (coercing to string dtype will alter the missing values) + target_index = Index(target, dtype=self.dtype) + return target_index @final def _validate_indexer( diff --git a/pandas/tests/indexes/object/test_indexing.py b/pandas/tests/indexes/object/test_indexing.py index 89648bc316c16..2c5968314e5cf 100644 --- a/pandas/tests/indexes/object/test_indexing.py +++ b/pandas/tests/indexes/object/test_indexing.py @@ -62,6 +62,15 @@ def test_get_indexer_with_NA_values( expected = np.array([0, 1, -1], dtype=np.intp) tm.assert_numpy_array_equal(result, expected) + def test_get_indexer_infer_string_missing_values(self): + # ensure the passed list is not cast to string but to object so that + # the None value is matched in the index + # https://github.com/pandas-dev/pandas/issues/55834 + idx = Index(["a", "b", None], dtype="object") + result = idx.get_indexer([None, "x"]) + expected = np.array([2, -1], dtype=np.intp) + tm.assert_numpy_array_equal(result, expected) + class TestGetIndexerNonUnique: def test_get_indexer_non_unique_nas(self, nulls_fixture): From f165bd57b7fd8334a9d65329e19f7090842007e7 Mon Sep 17 00:00:00 2001 From: amansharma612 Date: Sun, 17 Nov 2024 01:25:59 +0530 Subject: [PATCH 077/266] ENH: set __module__ on concat function in concat.py (#60334) Co-authored-by: Aman Sharma <210100011@iitb.ac.in> --- pandas/core/reshape/concat.py | 2 ++ pandas/tests/api/test_api.py | 1 + 2 files changed, 3 insertions(+) diff --git a/pandas/core/reshape/concat.py b/pandas/core/reshape/concat.py index cfe83111b6e38..e7cb7069bbc26 100644 --- a/pandas/core/reshape/concat.py +++ b/pandas/core/reshape/concat.py @@ -17,6 +17,7 @@ import numpy as np from pandas._libs import lib +from pandas.util._decorators import set_module from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.common import ( @@ -149,6 +150,7 @@ def concat( ) -> DataFrame | Series: ... +@set_module("pandas") def concat( objs: Iterable[Series | DataFrame] | Mapping[HashableT, Series | DataFrame], *, diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 75f9958b16286..c1d9f5ea4d25c 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -417,6 +417,7 @@ def test_set_module(): assert pd.Period.__module__ == "pandas" assert pd.Timestamp.__module__ == "pandas" assert pd.Timedelta.__module__ == "pandas" + assert pd.concat.__module__ == "pandas" assert pd.isna.__module__ == "pandas" assert pd.notna.__module__ == "pandas" assert pd.merge.__module__ == "pandas" From 34c080cb91d71a8e900a6c114279e142924c1a64 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Sun, 17 Nov 2024 03:07:48 -0500 Subject: [PATCH 078/266] CI: update fastparquet xfails for new release (#60337) --- pandas/tests/io/test_fsspec.py | 6 ++++- pandas/tests/io/test_gcs.py | 3 --- pandas/tests/io/test_parquet.py | 45 +++++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/pandas/tests/io/test_fsspec.py b/pandas/tests/io/test_fsspec.py index aa9c47ea0e63c..5340560884afe 100644 --- a/pandas/tests/io/test_fsspec.py +++ b/pandas/tests/io/test_fsspec.py @@ -5,6 +5,8 @@ from pandas._config import using_string_dtype +from pandas.compat import HAS_PYARROW + from pandas import ( DataFrame, date_range, @@ -176,7 +178,9 @@ def test_excel_options(fsspectest): assert fsspectest.test[0] == "read" -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) fastparquet") +@pytest.mark.xfail( + using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string) fastparquet" +) def test_to_parquet_new_file(cleared_fs, df1): """Regression test for writing to a not-yet-existent GCS Parquet file.""" pytest.importorskip("fastparquet") diff --git a/pandas/tests/io/test_gcs.py b/pandas/tests/io/test_gcs.py index 48580003f6c5e..f68ef5fa2e0e5 100644 --- a/pandas/tests/io/test_gcs.py +++ b/pandas/tests/io/test_gcs.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat.pyarrow import pa_version_under17p0 from pandas import ( @@ -207,7 +205,6 @@ def test_to_csv_compression_encoding_gcs( tm.assert_frame_equal(df, read_df) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) fastparquet") def test_to_parquet_gcs_new_file(monkeypatch, tmpdir): """Regression test for writing to a not-yet-existent GCS Parquet file.""" pytest.importorskip("fastparquet") diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 6ef7105cf5ccc..31cdb6626d237 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -1174,9 +1174,17 @@ def test_non_nanosecond_timestamps(self, temp_file): class TestParquetFastParquet(Base): - @pytest.mark.xfail(reason="datetime_with_nat gets incorrect values") - def test_basic(self, fp, df_full): + def test_basic(self, fp, df_full, request): pytz = pytest.importorskip("pytz") + import fastparquet + + if Version(fastparquet.__version__) < Version("2024.11.0"): + request.applymarker( + pytest.mark.xfail( + reason=("datetime_with_nat gets incorrect values"), + ) + ) + tz = pytz.timezone("US/Eastern") df = df_full @@ -1213,11 +1221,17 @@ def test_duplicate_columns(self, fp): msg = "Cannot create parquet dataset with duplicate column names" self.check_error_on_write(df, fp, ValueError, msg) - @pytest.mark.xfail( - Version(np.__version__) >= Version("2.0.0"), - reason="fastparquet uses np.float_ in numpy2", - ) - def test_bool_with_none(self, fp): + def test_bool_with_none(self, fp, request): + import fastparquet + + if Version(fastparquet.__version__) < Version("2024.11.0") and Version( + np.__version__ + ) >= Version("2.0.0"): + request.applymarker( + pytest.mark.xfail( + reason=("fastparquet uses np.float_ in numpy2"), + ) + ) df = pd.DataFrame({"a": [True, None, False]}) expected = pd.DataFrame({"a": [1.0, np.nan, 0.0]}, dtype="float16") # Fastparquet bug in 0.7.1 makes it so that this dtype becomes @@ -1331,10 +1345,19 @@ def test_empty_dataframe(self, fp): expected = df.copy() check_round_trip(df, fp, expected=expected) - @pytest.mark.xfail( - reason="fastparquet bug, see https://github.com/dask/fastparquet/issues/929" - ) - def test_timezone_aware_index(self, fp, timezone_aware_date_list): + def test_timezone_aware_index(self, fp, timezone_aware_date_list, request): + import fastparquet + + if Version(fastparquet.__version__) < Version("2024.11.0"): + request.applymarker( + pytest.mark.xfail( + reason=( + "fastparquet bug, see " + "https://github.com/dask/fastparquet/issues/929" + ), + ) + ) + idx = 5 * [timezone_aware_date_list] df = pd.DataFrame(index=idx, data={"index_as_col": idx}) From 720a6e7f9b4436d8ee88a6fb02ea31484d59e381 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Sun, 17 Nov 2024 05:49:48 -0500 Subject: [PATCH 079/266] BUG (string dtype): fix handling of string dtype in interchange protocol (#60333) Co-authored-by: Joris Van den Bossche --- pandas/core/interchange/from_dataframe.py | 12 ++++++++---- pandas/tests/interchange/test_impl.py | 9 ++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pandas/core/interchange/from_dataframe.py b/pandas/core/interchange/from_dataframe.py index 0e5776ae8cdd9..5c9b8ac8ea085 100644 --- a/pandas/core/interchange/from_dataframe.py +++ b/pandas/core/interchange/from_dataframe.py @@ -9,6 +9,8 @@ import numpy as np +from pandas._config import using_string_dtype + from pandas.compat._optional import import_optional_dependency import pandas as pd @@ -147,8 +149,6 @@ def protocol_df_chunk_to_pandas(df: DataFrameXchg) -> pd.DataFrame: ------- pd.DataFrame """ - # We need a dict of columns here, with each column being a NumPy array (at - # least for now, deal with non-NumPy dtypes later). columns: dict[str, Any] = {} buffers = [] # hold on to buffers, keeps memory alive for name in df.column_names(): @@ -347,8 +347,12 @@ def string_column_to_ndarray(col: Column) -> tuple[np.ndarray, Any]: # Add to our list of strings str_list[i] = string - # Convert the string list to a NumPy array - return np.asarray(str_list, dtype="object"), buffers + if using_string_dtype(): + res = pd.Series(str_list, dtype="str") + else: + res = np.asarray(str_list, dtype="object") # type: ignore[assignment] + + return res, buffers # type: ignore[return-value] def parse_datetime_format_str(format_str, data) -> pd.Series | np.ndarray: diff --git a/pandas/tests/interchange/test_impl.py b/pandas/tests/interchange/test_impl.py index 29ce9d0c03111..b80b4b923c247 100644 --- a/pandas/tests/interchange/test_impl.py +++ b/pandas/tests/interchange/test_impl.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs.tslibs import iNaT from pandas.compat import ( is_ci_environment, @@ -401,7 +399,6 @@ def test_interchange_from_corrected_buffer_dtypes(monkeypatch) -> None: pd.api.interchange.from_dataframe(df) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_empty_string_column(): # https://github.com/pandas-dev/pandas/issues/56703 df = pd.DataFrame({"a": []}, dtype=str) @@ -410,13 +407,12 @@ def test_empty_string_column(): tm.assert_frame_equal(df, result) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_large_string(): # GH#56702 pytest.importorskip("pyarrow") df = pd.DataFrame({"a": ["x"]}, dtype="large_string[pyarrow]") result = pd.api.interchange.from_dataframe(df.__dataframe__()) - expected = pd.DataFrame({"a": ["x"]}, dtype="object") + expected = pd.DataFrame({"a": ["x"]}, dtype="str") tm.assert_frame_equal(result, expected) @@ -427,7 +423,6 @@ def test_non_str_names(): assert names == ["0"] -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_non_str_names_w_duplicates(): # https://github.com/pandas-dev/pandas/issues/56701 df = pd.DataFrame({"0": [1, 2, 3], 0: [4, 5, 6]}) @@ -438,7 +433,7 @@ def test_non_str_names_w_duplicates(): "Expected a Series, got a DataFrame. This likely happened because you " "called __dataframe__ on a DataFrame which, after converting column " r"names to string, resulted in duplicated names: Index\(\['0', '0'\], " - r"dtype='object'\). Please rename these columns before using the " + r"dtype='(str|object)'\). Please rename these columns before using the " "interchange protocol." ), ): From e7d1964ab7405d54d919bb289318d01e9eb72cd1 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sun, 17 Nov 2024 13:41:13 +0100 Subject: [PATCH 080/266] TST (string dtype): clean-up assorted xfails (#60345) --- pandas/tests/base/test_conversion.py | 9 ++------- pandas/tests/indexes/multi/test_setops.py | 5 +---- pandas/tests/indexes/test_base.py | 12 +----------- pandas/tests/io/excel/test_readers.py | 3 --- pandas/tests/io/excel/test_writers.py | 5 +---- pandas/tests/io/test_stata.py | 1 - pandas/tests/reshape/test_union_categoricals.py | 9 +++++---- 7 files changed, 10 insertions(+), 34 deletions(-) diff --git a/pandas/tests/base/test_conversion.py b/pandas/tests/base/test_conversion.py index 888e8628f8664..e3a821519c638 100644 --- a/pandas/tests/base/test_conversion.py +++ b/pandas/tests/base/test_conversion.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import HAS_PYARROW from pandas.compat.numpy import np_version_gt2 @@ -392,9 +390,6 @@ def test_to_numpy(arr, expected, zero_copy, index_or_series_or_array): assert np.may_share_memory(result_nocopy1, result_nocopy2) -@pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)", strict=False -) @pytest.mark.parametrize("as_series", [True, False]) @pytest.mark.parametrize( "arr", [np.array([1, 2, 3], dtype="int64"), np.array(["a", "b", "c"], dtype=object)] @@ -406,13 +401,13 @@ def test_to_numpy_copy(arr, as_series, using_infer_string): # no copy by default result = obj.to_numpy() - if using_infer_string and arr.dtype == object: + if using_infer_string and arr.dtype == object and obj.dtype.storage == "pyarrow": assert np.shares_memory(arr, result) is False else: assert np.shares_memory(arr, result) is True result = obj.to_numpy(copy=False) - if using_infer_string and arr.dtype == object: + if using_infer_string and arr.dtype == object and obj.dtype.storage == "pyarrow": assert np.shares_memory(arr, result) is False else: assert np.shares_memory(arr, result) is True diff --git a/pandas/tests/indexes/multi/test_setops.py b/pandas/tests/indexes/multi/test_setops.py index e85091aaae608..f7544cf62e5fa 100644 --- a/pandas/tests/indexes/multi/test_setops.py +++ b/pandas/tests/indexes/multi/test_setops.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( CategoricalIndex, @@ -754,13 +752,12 @@ def test_intersection_keep_ea_dtypes(val, any_numeric_ea_dtype): tm.assert_index_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_union_with_na_when_constructing_dataframe(): # GH43222 series1 = Series( (1,), index=MultiIndex.from_arrays( - [Series([None], dtype="string"), Series([None], dtype="string")] + [Series([None], dtype="str"), Series([None], dtype="str")] ), ) series2 = Series((10, 20), index=MultiIndex.from_tuples(((None, None), ("a", "b")))) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 19b46d9b2c15f..06df8902f319c 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -8,12 +8,7 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import ( - HAS_PYARROW, - IS64, -) +from pandas.compat import IS64 from pandas.errors import InvalidIndexError import pandas.util._test_decorators as td @@ -823,11 +818,6 @@ def test_isin(self, values, index, expected): expected = np.array(expected, dtype=bool) tm.assert_numpy_array_equal(result, expected) - @pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, - reason="TODO(infer_string)", - strict=False, - ) def test_isin_nan_common_object( self, nulls_fixture, nulls_fixture2, using_infer_string ): diff --git a/pandas/tests/io/excel/test_readers.py b/pandas/tests/io/excel/test_readers.py index 3989e022dbbd2..34824f0a67985 100644 --- a/pandas/tests/io/excel/test_readers.py +++ b/pandas/tests/io/excel/test_readers.py @@ -17,8 +17,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas.util._test_decorators as td import pandas as pd @@ -625,7 +623,6 @@ def test_reader_dtype_str(self, read_ext, dtype, expected): expected = DataFrame(expected) tm.assert_frame_equal(actual, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) def test_dtype_backend(self, read_ext, dtype_backend, engine, tmp_excel): # GH#36712 if read_ext in (".xlsb", ".xls"): diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index 051aa1f386d92..19fe9855dbb85 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -13,8 +13,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat._optional import import_optional_dependency import pandas.util._test_decorators as td @@ -1387,12 +1385,11 @@ def test_freeze_panes(self, tmp_excel): result = pd.read_excel(tmp_excel, index_col=0) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_path_path_lib(self, engine, ext): df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), columns=Index(list("ABCD")), - index=Index([f"i-{i}" for i in range(30)], dtype=object), + index=Index([f"i-{i}" for i in range(30)]), ) writer = partial(df.to_excel, engine=engine) diff --git a/pandas/tests/io/test_stata.py b/pandas/tests/io/test_stata.py index 8fa85d13bbdb5..9288b98d79fbe 100644 --- a/pandas/tests/io/test_stata.py +++ b/pandas/tests/io/test_stata.py @@ -1719,7 +1719,6 @@ def test_date_parsing_ignores_format_details(self, column, datapath): formatted = df.loc[0, column + "_fmt"] assert unformatted == formatted - # @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.parametrize("byteorder", ["little", "big"]) def test_writer_117(self, byteorder, temp_file, using_infer_string): original = DataFrame( diff --git a/pandas/tests/reshape/test_union_categoricals.py b/pandas/tests/reshape/test_union_categoricals.py index 1d5d16f39e648..081feae6fc43f 100644 --- a/pandas/tests/reshape/test_union_categoricals.py +++ b/pandas/tests/reshape/test_union_categoricals.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.concat import union_categoricals import pandas as pd @@ -124,12 +122,15 @@ def test_union_categoricals_nan(self): exp = Categorical([np.nan, np.nan, np.nan, np.nan]) tm.assert_categorical_equal(res, exp) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("val", [[], ["1"]]) def test_union_categoricals_empty(self, val, request, using_infer_string): # GH 13759 if using_infer_string and val == ["1"]: - request.applymarker(pytest.mark.xfail("object and strings dont match")) + request.applymarker( + pytest.mark.xfail( + reason="TDOD(infer_string) object and strings dont match" + ) + ) res = union_categoricals([Categorical([]), Categorical(val)]) exp = Categorical(val) tm.assert_categorical_equal(res, exp) From 7fe270c8e7656c0c187260677b3b16a17a1281dc Mon Sep 17 00:00:00 2001 From: Kevin Amparado <109636487+KevsterAmp@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:52:50 +0800 Subject: [PATCH 081/266] TST: Add test for `pd.Timestamp` DST transition (#60346) --- pandas/tests/scalar/timestamp/test_arithmetic.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pandas/tests/scalar/timestamp/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py index 7aa6c6c0496a9..d65d425620c84 100644 --- a/pandas/tests/scalar/timestamp/test_arithmetic.py +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -314,6 +314,17 @@ def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz): assert result == expected + def test_timestamp_dst_transition(self): + # GH 60084 + dt_str = "2023-11-05 01:00-08:00" + tz_str = "America/Los_Angeles" + + ts1 = Timestamp(dt_str, tz=tz_str) + ts2 = ts1 + Timedelta(hours=0) + + assert ts1 == ts2 + assert hash(ts1) == hash(ts2) + class SubDatetime(datetime): pass From 6a7685faf104f8582e0e75f1fae58e09ae97e2fe Mon Sep 17 00:00:00 2001 From: Shi Entong <144505619+setbit123@users.noreply.github.com> Date: Tue, 19 Nov 2024 02:38:34 +0800 Subject: [PATCH 082/266] Update citing.md to correct the invalid URL (#60352) Update citing.md Correct the invalid URL on line 23 to a valid one. --- web/pandas/about/citing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pandas/about/citing.md b/web/pandas/about/citing.md index 4ce1fdb207865..a3c470d05e55f 100644 --- a/web/pandas/about/citing.md +++ b/web/pandas/about/citing.md @@ -20,7 +20,7 @@ following paper: url = {https://doi.org/10.5281/zenodo.3509134} } -- [Data structures for statistical computing in python](https://conference.scipy.org/proceedings/scipy2010/pdfs/mckinney.pdf), +- [Data structures for statistical computing in python](https://pub.curvenote.com/01908378-3686-7168-a380-d82bbf21c799/public/mckinney-57fc0d4e8a08cd7f26a4b8bf468a71f4.pdf), McKinney, Proceedings of the 9th Python in Science Conference, Volume 445, 2010. @InProceedings{ mckinney-proc-scipy-2010, From 0e2099089406c6d5616bf9e8872154fee4960ea7 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Wed, 20 Nov 2024 22:47:33 +0800 Subject: [PATCH 083/266] BUG: fix to_datetime with np.datetime64[ps] giving wrong conversion (#60342) --- doc/source/whatsnew/v3.0.0.rst | 1 + .../_libs/src/vendored/numpy/datetime/np_datetime.c | 11 ++++++----- pandas/tests/tools/test_to_datetime.py | 9 +++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 7da2f968b900b..5f7aed8ed9786 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -626,6 +626,7 @@ Datetimelike - Bug in :meth:`Series.dt.microsecond` producing incorrect results for pyarrow backed :class:`Series`. (:issue:`59154`) - Bug in :meth:`to_datetime` not respecting dayfirst if an uncommon date string was passed. (:issue:`58859`) - Bug in :meth:`to_datetime` reports incorrect index in case of any failure scenario. (:issue:`58298`) +- Bug in :meth:`to_datetime` wrongly converts when ``arg`` is a ``np.datetime64`` object with unit of ``ps``. (:issue:`60341`) - Bug in setting scalar values with mismatched resolution into arrays with non-nanosecond ``datetime64``, ``timedelta64`` or :class:`DatetimeTZDtype` incorrectly truncating those scalars (:issue:`56410`) Timedelta diff --git a/pandas/_libs/src/vendored/numpy/datetime/np_datetime.c b/pandas/_libs/src/vendored/numpy/datetime/np_datetime.c index cc65f34d6b6fe..9a022095feee9 100644 --- a/pandas/_libs/src/vendored/numpy/datetime/np_datetime.c +++ b/pandas/_libs/src/vendored/numpy/datetime/np_datetime.c @@ -660,11 +660,12 @@ void pandas_datetime_to_datetimestruct(npy_datetime dt, NPY_DATETIMEUNIT base, perday = 24LL * 60 * 60 * 1000 * 1000 * 1000 * 1000; set_datetimestruct_days(extract_unit(&dt, perday), out); - out->hour = (npy_int32)extract_unit(&dt, 1000LL * 1000 * 1000 * 60 * 60); - out->min = (npy_int32)extract_unit(&dt, 1000LL * 1000 * 1000 * 60); - out->sec = (npy_int32)extract_unit(&dt, 1000LL * 1000 * 1000); - out->us = (npy_int32)extract_unit(&dt, 1000LL); - out->ps = (npy_int32)(dt * 1000); + out->hour = + (npy_int32)extract_unit(&dt, 1000LL * 1000 * 1000 * 1000 * 60 * 60); + out->min = (npy_int32)extract_unit(&dt, 1000LL * 1000 * 1000 * 1000 * 60); + out->sec = (npy_int32)extract_unit(&dt, 1000LL * 1000 * 1000 * 1000); + out->us = (npy_int32)extract_unit(&dt, 1000LL * 1000); + out->ps = (npy_int32)(dt); break; case NPY_FR_fs: diff --git a/pandas/tests/tools/test_to_datetime.py b/pandas/tests/tools/test_to_datetime.py index a9d3c235f63f6..b73839f406a29 100644 --- a/pandas/tests/tools/test_to_datetime.py +++ b/pandas/tests/tools/test_to_datetime.py @@ -3668,3 +3668,12 @@ def test_to_datetime_mixed_awareness_mixed_types(aware_val, naive_val, naive_fir to_datetime(vec, format="mixed") with pytest.raises(ValueError, match=msg): DatetimeIndex(vec) + + +def test_to_datetime_wrapped_datetime64_ps(): + # GH#60341 + result = to_datetime([np.datetime64(1901901901901, "ps")]) + expected = DatetimeIndex( + ["1970-01-01 00:00:01.901901901"], dtype="datetime64[ns]", freq=None + ) + tm.assert_index_equal(result, expected) From ff53ca1486dd10b0f2883987f082a79f3a55c409 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Thu, 21 Nov 2024 00:21:30 +0530 Subject: [PATCH 084/266] DOC: fix SA01 for pandas.errors.AttributeConflictWarning (#60367) * DOC: fix SA01 for pandas.errors.AttributeConflictWarning * DOC: fix SA01 for pandas.errors.AttributeConflictWarning --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 53690e9b78b8a..fe45ce02d5e44 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -114,7 +114,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ - -i "pandas.errors.AttributeConflictWarning SA01" \ -i "pandas.errors.ChainedAssignmentError SA01" \ -i "pandas.errors.DuplicateLabelError SA01" \ -i "pandas.errors.IntCastingNaNError SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index cacbfb49c311f..84f7239c6549d 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -672,6 +672,12 @@ class AttributeConflictWarning(Warning): name than the existing index on an HDFStore or attempting to append an index with a different frequency than the existing index on an HDFStore. + See Also + -------- + HDFStore : Dict-like IO interface for storing pandas objects in PyTables. + DataFrame.to_hdf : Write the contained data to an HDF5 file using HDFStore. + read_hdf : Read from an HDF5 file into a DataFrame. + Examples -------- >>> idx1 = pd.Index(["a", "b"], name="name1") From 24df015ad4ada9f58e6874b54737e579a62a7a53 Mon Sep 17 00:00:00 2001 From: ensalada-de-pechuga <127701043+ensalada-de-pechuga@users.noreply.github.com> Date: Thu, 21 Nov 2024 03:55:02 +0900 Subject: [PATCH 085/266] DOC: Fix docstrings for SeriesGroupBy monotonic and nth (#60375) * fix docstrings and remove from code_checks.sh * fix SeriesGroupBy.is_monotonic_decreasing See Also section (decreasing -> increasing) * remove DataFrameGroupBy.nth from code_checks.sh --------- Co-authored-by: root --- ci/code_checks.sh | 4 ---- pandas/core/groupby/generic.py | 10 ++++++++++ pandas/core/groupby/groupby.py | 13 ------------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index fe45ce02d5e44..633d767c63037 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -92,15 +92,11 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.indices SA01" \ - -i "pandas.core.groupby.DataFrameGroupBy.nth PR02" \ -i "pandas.core.groupby.DataFrameGroupBy.nunique SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.DataFrameGroupBy.sem SA01" \ -i "pandas.core.groupby.SeriesGroupBy.get_group RT03,SA01" \ -i "pandas.core.groupby.SeriesGroupBy.indices SA01" \ - -i "pandas.core.groupby.SeriesGroupBy.is_monotonic_decreasing SA01" \ - -i "pandas.core.groupby.SeriesGroupBy.is_monotonic_increasing SA01" \ - -i "pandas.core.groupby.SeriesGroupBy.nth PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.sem SA01" \ -i "pandas.core.resample.Resampler.get_group RT03,SA01" \ diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 5ba382bf66bb7..35ec09892ede6 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -1443,6 +1443,11 @@ def is_monotonic_increasing(self) -> Series: ------- Series + See Also + -------- + SeriesGroupBy.is_monotonic_decreasing : Return whether each group's values + are monotonically decreasing. + Examples -------- >>> s = pd.Series([2, 1, 3, 4], index=["Falcon", "Falcon", "Parrot", "Parrot"]) @@ -1462,6 +1467,11 @@ def is_monotonic_decreasing(self) -> Series: ------- Series + See Also + -------- + SeriesGroupBy.is_monotonic_increasing : Return whether each group's values + are monotonically increasing. + Examples -------- >>> s = pd.Series([2, 1, 3, 4], index=["Falcon", "Falcon", "Parrot", "Parrot"]) diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 9c30132347111..ad23127ad449f 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -3983,19 +3983,6 @@ def nth(self) -> GroupByNthSelector: 'all' or 'any'; this is equivalent to calling dropna(how=dropna) before the groupby. - Parameters - ---------- - n : int, slice or list of ints and slices - A single nth value for the row or a list of nth values or slices. - - .. versionchanged:: 1.4.0 - Added slice and lists containing slices. - Added index notation. - - dropna : {'any', 'all', None}, default None - Apply the specified dropna operation before counting which row is - the nth row. Only supported if n is an int. - Returns ------- Series or DataFrame From 72ab3fdc7a3530b885a466db88bbb38de8d5c6b9 Mon Sep 17 00:00:00 2001 From: Ivruix <52746744+Ivruix@users.noreply.github.com> Date: Wed, 20 Nov 2024 22:00:08 +0300 Subject: [PATCH 086/266] DOC: fix docstring validation errors for pandas.Series.dt.freq (#60377) * Added docs for Series.dt.freq and removed from ci/code_checks.sh * Fix code style --- ci/code_checks.sh | 1 - pandas/core/indexes/accessors.py | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 633d767c63037..379f7cb5f037d 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -73,7 +73,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Period.freq GL08" \ -i "pandas.Period.ordinal GL08" \ -i "pandas.RangeIndex.from_range PR01,SA01" \ - -i "pandas.Series.dt.freq GL08" \ -i "pandas.Series.dt.unit GL08" \ -i "pandas.Series.pad PR01,SA01" \ -i "pandas.Timedelta.max PR02" \ diff --git a/pandas/core/indexes/accessors.py b/pandas/core/indexes/accessors.py index e2dc71f68a65b..c404323a1168c 100644 --- a/pandas/core/indexes/accessors.py +++ b/pandas/core/indexes/accessors.py @@ -373,6 +373,28 @@ def to_pydatetime(self) -> Series: @property def freq(self): + """ + Tries to return a string representing a frequency generated by infer_freq. + + Returns None if it can't autodetect the frequency. + + See Also + -------- + Series.dt.to_period : Cast to PeriodArray/PeriodIndex at a particular + frequency. + + Examples + -------- + >>> ser = pd.Series(["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04"]) + >>> ser = pd.to_datetime(ser) + >>> ser.dt.freq + 'D' + + >>> ser = pd.Series(["2022-01-01", "2024-01-01", "2026-01-01", "2028-01-01"]) + >>> ser = pd.to_datetime(ser) + >>> ser.dt.freq + '2YS-JAN' + """ return self._get_values().inferred_freq def isocalendar(self) -> DataFrame: From 1c986d6213904fd7d9acc5622dc91d029d3f1218 Mon Sep 17 00:00:00 2001 From: Joseph Kleinhenz Date: Wed, 20 Nov 2024 23:52:11 -0800 Subject: [PATCH 087/266] ENH: expose `to_pandas_kwargs` in `read_parquet` with pyarrow backend (#59654) Co-authored-by: Joseph Kleinhenz Co-authored-by: Xiao Yuan Co-authored-by: Joris Van den Bossche --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/_util.py | 5 ++++- pandas/io/parquet.py | 22 ++++++++++++++++++++-- pandas/tests/io/test_parquet.py | 14 ++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 5f7aed8ed9786..fbf2bed550c85 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -54,6 +54,7 @@ Other enhancements - :meth:`Series.cummin` and :meth:`Series.cummax` now supports :class:`CategoricalDtype` (:issue:`52335`) - :meth:`Series.plot` now correctly handle the ``ylabel`` parameter for pie charts, allowing for explicit control over the y-axis label (:issue:`58239`) - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) +- :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`) - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) - :meth:`Series.map` can now accept kwargs to pass on to func (:issue:`59814`) - :meth:`pandas.concat` will raise a ``ValueError`` when ``ignore_index=True`` and ``keys`` is not ``None`` (:issue:`59274`) diff --git a/pandas/io/_util.py b/pandas/io/_util.py index 21203ad036fc6..9778a404e23e0 100644 --- a/pandas/io/_util.py +++ b/pandas/io/_util.py @@ -60,9 +60,12 @@ def arrow_table_to_pandas( table: pyarrow.Table, dtype_backend: DtypeBackend | Literal["numpy"] | lib.NoDefault = lib.no_default, null_to_int64: bool = False, + to_pandas_kwargs: dict | None = None, ) -> pd.DataFrame: pa = import_optional_dependency("pyarrow") + to_pandas_kwargs = {} if to_pandas_kwargs is None else to_pandas_kwargs + types_mapper: type[pd.ArrowDtype] | None | Callable if dtype_backend == "numpy_nullable": mapping = _arrow_dtype_mapping() @@ -80,5 +83,5 @@ def arrow_table_to_pandas( else: raise NotImplementedError - df = table.to_pandas(types_mapper=types_mapper) + df = table.to_pandas(types_mapper=types_mapper, **to_pandas_kwargs) return df diff --git a/pandas/io/parquet.py b/pandas/io/parquet.py index 116f228faca93..6a5a83088e986 100644 --- a/pandas/io/parquet.py +++ b/pandas/io/parquet.py @@ -242,6 +242,7 @@ def read( dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, storage_options: StorageOptions | None = None, filesystem=None, + to_pandas_kwargs: dict[str, Any] | None = None, **kwargs, ) -> DataFrame: kwargs["use_pandas_metadata"] = True @@ -266,7 +267,11 @@ def read( "make_block is deprecated", DeprecationWarning, ) - result = arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) + result = arrow_table_to_pandas( + pa_table, + dtype_backend=dtype_backend, + to_pandas_kwargs=to_pandas_kwargs, + ) if pa_table.schema.metadata: if b"PANDAS_ATTRS" in pa_table.schema.metadata: @@ -347,6 +352,7 @@ def read( filters=None, storage_options: StorageOptions | None = None, filesystem=None, + to_pandas_kwargs: dict | None = None, **kwargs, ) -> DataFrame: parquet_kwargs: dict[str, Any] = {} @@ -362,6 +368,10 @@ def read( raise NotImplementedError( "filesystem is not implemented for the fastparquet engine." ) + if to_pandas_kwargs is not None: + raise NotImplementedError( + "to_pandas_kwargs is not implemented for the fastparquet engine." + ) path = stringify_path(path) handles = None if is_fsspec_url(path): @@ -452,7 +462,7 @@ def to_parquet( .. versionadded:: 2.1.0 kwargs - Additional keyword arguments passed to the engine + Additional keyword arguments passed to the engine. Returns ------- @@ -491,6 +501,7 @@ def read_parquet( dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, filesystem: Any = None, filters: list[tuple] | list[list[tuple]] | None = None, + to_pandas_kwargs: dict | None = None, **kwargs, ) -> DataFrame: """ @@ -564,6 +575,12 @@ def read_parquet( .. versionadded:: 2.1.0 + to_pandas_kwargs : dict | None, default None + Keyword arguments to pass through to :func:`pyarrow.Table.to_pandas` + when ``engine="pyarrow"``. + + .. versionadded:: 3.0.0 + **kwargs Any additional kwargs are passed to the engine. @@ -636,5 +653,6 @@ def read_parquet( storage_options=storage_options, dtype_backend=dtype_backend, filesystem=filesystem, + to_pandas_kwargs=to_pandas_kwargs, **kwargs, ) diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 31cdb6626d237..7919bb956dc7a 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -1172,6 +1172,20 @@ def test_non_nanosecond_timestamps(self, temp_file): ) tm.assert_frame_equal(result, expected) + def test_maps_as_pydicts(self, pa): + pyarrow = pytest.importorskip("pyarrow", "13.0.0") + + schema = pyarrow.schema( + [("foo", pyarrow.map_(pyarrow.string(), pyarrow.int64()))] + ) + df = pd.DataFrame([{"foo": {"A": 1}}, {"foo": {"B": 2}}]) + check_round_trip( + df, + pa, + write_kwargs={"schema": schema}, + read_kwargs={"to_pandas_kwargs": {"maps_as_pydicts": "strict"}}, + ) + class TestParquetFastParquet(Base): def test_basic(self, fp, df_full, request): From 72bd17c44bb0507bbc2a9ee194ea8db58e90dbaa Mon Sep 17 00:00:00 2001 From: Animcogn Date: Thu, 21 Nov 2024 09:22:03 -0700 Subject: [PATCH 088/266] Fix Bug: last column missing formatting in Latex (#52218) (#60356) * fix to_latex for multiindex * add newline * fix styling * Update v2.3.0.rst fix backticks * update description. * Apply suggested fix Co-authored-by: JHM Darbyshire <24256554+attack68@users.noreply.github.com> * move to v.3 * Update pandas/io/formats/style_render.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> * spacing * fix whitespace * fix method name --------- Co-authored-by: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/io/formats/style_render.py | 3 +- pandas/tests/io/formats/test_to_latex.py | 85 ++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index fbf2bed550c85..120ee978292d6 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -764,7 +764,7 @@ ExtensionArray Styler ^^^^^^ -- +- Bug in :meth:`Styler.to_latex` where styling column headers when combined with a hidden index or hidden index-levels is fixed. Other ^^^^^ diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index ecfe3de10c829..c0f0608f1ab32 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -868,7 +868,8 @@ def _translate_latex(self, d: dict, clines: str | None) -> None: or multirow sparsification (so that \multirow and \multicol work correctly). """ index_levels = self.index.nlevels - visible_index_level_n = index_levels - sum(self.hide_index_) + # GH 52218 + visible_index_level_n = max(1, index_levels - sum(self.hide_index_)) d["head"] = [ [ {**col, "cellstyle": self.ctx_columns[r, c - visible_index_level_n]} diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index 1de53993fe646..8d46442611719 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -1405,3 +1405,88 @@ def test_to_latex_multiindex_multirow(self): """ ) assert result == expected + + def test_to_latex_multiindex_format_single_index_hidden(self): + # GH 52218 + df = DataFrame( + { + "A": [1, 2], + "B": [4, 5], + } + ) + result = ( + df.style.hide(axis="index") + .map_index(lambda v: "textbf:--rwrap;", axis="columns") + .to_latex() + ) + expected = _dedent(r""" + \begin{tabular}{rr} + \textbf{A} & \textbf{B} \\ + 1 & 4 \\ + 2 & 5 \\ + \end{tabular} + """) + assert result == expected + + def test_to_latex_multiindex_format_triple_index_two_hidden(self): + # GH 52218 + arrays = [ + ["A", "A", "B", "B"], + ["one", "two", "one", "two"], + ["x", "x", "y", "y"], + ] + index = pd.MultiIndex.from_arrays( + arrays, names=["Level 0", "Level 1", "Level 2"] + ) + df = DataFrame( + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + index=index, + columns=["C1", "C2", "C3"], + ) + result = ( + df.style.hide(axis="index", level=[0, 1]) + .map_index(lambda v: "textbf:--rwrap;", axis="columns") + .to_latex() + ) + expected = _dedent(r""" + \begin{tabular}{lrrr} + & \textbf{C1} & \textbf{C2} & \textbf{C3} \\ + Level 2 & & & \\ + x & 0 & 0 & 0 \\ + x & 0 & 0 & 0 \\ + y & 0 & 0 & 0 \\ + y & 0 & 0 & 0 \\ + \end{tabular} + """) + assert result == expected + + def test_to_latex_multiindex_format_triple_index_all_hidden(self): + # GH 52218 + arrays = [ + ["A", "A", "B", "B"], + ["one", "two", "one", "two"], + ["x", "x", "y", "y"], + ] + index = pd.MultiIndex.from_arrays( + arrays, names=["Level 0", "Level 1", "Level 2"] + ) + df = DataFrame( + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + index=index, + columns=["C1", "C2", "C3"], + ) + result = ( + df.style.hide(axis="index", level=[0, 1, 2]) + .map_index(lambda v: "textbf:--rwrap;", axis="columns") + .to_latex() + ) + expected = _dedent(r""" + \begin{tabular}{rrr} + \textbf{C1} & \textbf{C2} & \textbf{C3} \\ + 0 & 0 & 0 \\ + 0 & 0 & 0 \\ + 0 & 0 & 0 \\ + 0 & 0 & 0 \\ + \end{tabular} + """) + assert result == expected From f36372107e2e0264b0684117da80cb28450c287c Mon Sep 17 00:00:00 2001 From: Abhishek Chaudhari <91185083+AbhishekChaudharii@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:53:08 +0530 Subject: [PATCH 089/266] TST: Added test for pivot_table with int64 dtype and dropna parameter (#60374) * added tests for int64 * pre-commit changes * used pytest.mark.parametrize for expected_dtype --- pandas/tests/reshape/test_pivot.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index d8a9acdc561fd..f42f7f8232229 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -2376,9 +2376,13 @@ def test_pivot_table_with_margins_and_numeric_columns(self): tm.assert_frame_equal(result, expected) - def test_pivot_ea_dtype_dropna(self, dropna): + @pytest.mark.parametrize( + "dtype,expected_dtype", [("Int64", "Float64"), ("int64", "float64")] + ) + def test_pivot_ea_dtype_dropna(self, dropna, dtype, expected_dtype): # GH#47477 - df = DataFrame({"x": "a", "y": "b", "age": Series([20, 40], dtype="Int64")}) + # GH#47971 + df = DataFrame({"x": "a", "y": "b", "age": Series([20, 40], dtype=dtype)}) result = df.pivot_table( index="x", columns="y", values="age", aggfunc="mean", dropna=dropna ) @@ -2386,7 +2390,7 @@ def test_pivot_ea_dtype_dropna(self, dropna): [[30]], index=Index(["a"], name="x"), columns=Index(["b"], name="y"), - dtype="Float64", + dtype=expected_dtype, ) tm.assert_frame_equal(result, expected) From 38a86f76551d843f6694743028553c53e9e21505 Mon Sep 17 00:00:00 2001 From: Robert Wolff Date: Thu, 21 Nov 2024 17:25:38 +0100 Subject: [PATCH 090/266] DOC: Fix doc string for column C&C (#60386) Fix doc string for column C&C --- pandas/core/frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 34eb198b4b4da..d1450537dd740 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4742,7 +4742,8 @@ def eval(self, expr: str, *, inplace: bool = False, **kwargs) -> Any | None: 3 4 4 7 8 0 4 5 2 6 7 3 - For columns with spaces in their name, you can use backtick quoting. + For columns with spaces or other disallowed characters in their name, you can + use backtick quoting. >>> df.eval("B * `C&C`") 0 100 From d4cd498370039c61cff28cb59b799fcdc941d7cf Mon Sep 17 00:00:00 2001 From: partev Date: Thu, 21 Nov 2024 11:27:23 -0500 Subject: [PATCH 091/266] update twitter to the new URL and logo (#60382) --- web/pandas/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pandas/index.html b/web/pandas/index.html index 63bc11d3ed5d8..98628b856edb6 100644 --- a/web/pandas/index.html +++ b/web/pandas/index.html @@ -83,8 +83,8 @@

Follow us

  • - - + +
  • From 91509d2d30da87f7e4394e95c7139e7d03e7b16b Mon Sep 17 00:00:00 2001 From: partev Date: Thu, 21 Nov 2024 11:29:14 -0500 Subject: [PATCH 092/266] remove unnecessary trailing slashes from URLs (#60381) --- web/pandas/_templates/layout.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pandas/_templates/layout.html b/web/pandas/_templates/layout.html index 4c66f28818abd..c26b093b0c4ba 100644 --- a/web/pandas/_templates/layout.html +++ b/web/pandas/_templates/layout.html @@ -73,12 +73,12 @@
  • - +
  • - +
  • From f105eefaab41b7848d2b2c0d331e047a4cbb8c65 Mon Sep 17 00:00:00 2001 From: Kevin Amparado <109636487+KevsterAmp@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:31:18 +0800 Subject: [PATCH 093/266] ENH: add trim() to github.event.comment.body on issue_assign workflow job (#60359) add trim() to github.event.comment.body on issue_assign workflow job --- .github/workflows/comment-commands.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/comment-commands.yml b/.github/workflows/comment-commands.yml index 62956f5825782..45f3e911377c1 100644 --- a/.github/workflows/comment-commands.yml +++ b/.github/workflows/comment-commands.yml @@ -11,7 +11,7 @@ permissions: jobs: issue_assign: runs-on: ubuntu-22.04 - if: (!github.event.issue.pull_request) && github.event.comment.body == 'take' + if: (!github.event.issue.pull_request) && trim(github.event.comment.body) == 'take' concurrency: group: ${{ github.actor }}-issue-assign steps: From e62fcb15a70dfb6f4c408cf801f83b216578335b Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Fri, 22 Nov 2024 00:59:41 +0530 Subject: [PATCH 094/266] DOC: fix SA01 for pandas.errors.ChainedAssignmentError (#60390) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 379f7cb5f037d..772793702f8b8 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -109,7 +109,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ - -i "pandas.errors.ChainedAssignmentError SA01" \ -i "pandas.errors.DuplicateLabelError SA01" \ -i "pandas.errors.IntCastingNaNError SA01" \ -i "pandas.errors.InvalidIndexError SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 84f7239c6549d..68bd70603abae 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -487,6 +487,11 @@ class ChainedAssignmentError(Warning): For more information on Copy-on-Write, see :ref:`the user guide`. + See Also + -------- + options.mode.copy_on_write : Global setting for enabling or disabling + Copy-on-Write behavior. + Examples -------- >>> pd.options.mode.copy_on_write = True From d4ae654b18ec6a42b1bee9a7df8d786f02aca21b Mon Sep 17 00:00:00 2001 From: Kevin Amparado <109636487+KevsterAmp@users.noreply.github.com> Date: Sat, 23 Nov 2024 02:56:41 +0800 Subject: [PATCH 095/266] CI/BUG: Remove `trim()` function on `comment-commands.yml` (#60397) remove trim function on comment-commands.yml --- .github/workflows/comment-commands.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/comment-commands.yml b/.github/workflows/comment-commands.yml index 45f3e911377c1..62956f5825782 100644 --- a/.github/workflows/comment-commands.yml +++ b/.github/workflows/comment-commands.yml @@ -11,7 +11,7 @@ permissions: jobs: issue_assign: runs-on: ubuntu-22.04 - if: (!github.event.issue.pull_request) && trim(github.event.comment.body) == 'take' + if: (!github.event.issue.pull_request) && github.event.comment.body == 'take' concurrency: group: ${{ github.actor }}-issue-assign steps: From eaa8b47ea5c0ce04f48557570574d42effd8fff2 Mon Sep 17 00:00:00 2001 From: Yuvraj Pradhan <151496266+Yuvraj-Pradhan-27@users.noreply.github.com> Date: Sat, 23 Nov 2024 01:45:04 +0530 Subject: [PATCH 096/266] DOC: Fixed spelling of 'behaviour' to 'behavior' (#60398) --- pandas/core/series.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/core/series.py b/pandas/core/series.py index 35b576da87ed7..4fa8b86fa4c16 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -567,7 +567,7 @@ def __arrow_c_stream__(self, requested_schema=None): Export the pandas Series as an Arrow C stream PyCapsule. This relies on pyarrow to convert the pandas Series to the Arrow - format (and follows the default behaviour of ``pyarrow.Array.from_pandas`` + format (and follows the default behavior of ``pyarrow.Array.from_pandas`` in its handling of the index, i.e. to ignore it). This conversion is not necessarily zero-copy. @@ -2226,7 +2226,7 @@ def drop_duplicates( 5 hippo Name: animal, dtype: object - With the 'keep' parameter, the selection behaviour of duplicated values + With the 'keep' parameter, the selection behavior of duplicated values can be changed. The value 'first' keeps the first occurrence for each set of duplicated entries. The default value of keep is 'first'. @@ -3451,7 +3451,7 @@ def sort_values( 4 5.0 dtype: float64 - Sort values ascending order (default behaviour) + Sort values ascending order (default behavior) >>> s.sort_values(ascending=True) 1 1.0 @@ -4098,7 +4098,7 @@ def swaplevel( In the following example, we will swap the levels of the indices. Here, we will swap the levels column-wise, but levels can be swapped row-wise - in a similar manner. Note that column-wise is the default behaviour. + in a similar manner. Note that column-wise is the default behavior. By not supplying any arguments for i and j, we swap the last and second to last indices. From ee0902a832b7fa3e5821ada176566301791e09ec Mon Sep 17 00:00:00 2001 From: ZKaoChi <1953542921@qq.com> Date: Sat, 23 Nov 2024 04:20:39 +0800 Subject: [PATCH 097/266] BUG: Convert output type in Excel for MultiIndex with period levels (#60182) --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/excel.py | 8 ++++++ pandas/tests/io/excel/test_style.py | 26 ++++++++++++++++++ pandas/tests/io/excel/test_writers.py | 38 +++++++++++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 120ee978292d6..1d55fc3ed7b84 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -690,6 +690,7 @@ I/O - Bug in :meth:`DataFrame.from_records` where ``columns`` parameter with numpy structured array was not reordering and filtering out the columns (:issue:`59717`) - Bug in :meth:`DataFrame.to_dict` raises unnecessary ``UserWarning`` when columns are not unique and ``orient='tight'``. (:issue:`58281`) - Bug in :meth:`DataFrame.to_excel` when writing empty :class:`DataFrame` with :class:`MultiIndex` on both axes (:issue:`57696`) +- Bug in :meth:`DataFrame.to_excel` where the :class:`MultiIndex` index with a period level was not a date (:issue:`60099`) - Bug in :meth:`DataFrame.to_stata` when writing :class:`DataFrame` and ``byteorder=`big```. (:issue:`58969`) - Bug in :meth:`DataFrame.to_stata` when writing more than 32,000 value labels. (:issue:`60107`) - Bug in :meth:`DataFrame.to_string` that raised ``StopIteration`` with nested DataFrames. (:issue:`16098`) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 6a3e215de3f96..5fde6577e9f95 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -37,6 +37,7 @@ DataFrame, Index, MultiIndex, + Period, PeriodIndex, ) import pandas.core.common as com @@ -803,6 +804,9 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: allow_fill=levels._can_hold_na, fill_value=levels._na_value, ) + # GH#60099 + if isinstance(values[0], Period): + values = values.to_timestamp() for i, span_val in spans.items(): mergestart, mergeend = None, None @@ -827,6 +831,10 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: # Format hierarchical rows with non-merged values. for indexcolvals in zip(*self.df.index): for idx, indexcolval in enumerate(indexcolvals): + # GH#60099 + if isinstance(indexcolval, Period): + indexcolval = indexcolval.to_timestamp() + yield CssExcelCell( row=self.rowcounter + idx, col=gcolidx, diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index f70e65e34c584..71ef1201e523f 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -9,6 +9,9 @@ from pandas import ( DataFrame, + MultiIndex, + Timestamp, + period_range, read_excel, ) import pandas._testing as tm @@ -333,3 +336,26 @@ def test_styler_to_s3(s3_public_bucket, s3so): f"s3://{mock_bucket_name}/{target_file}", index_col=0, storage_options=s3so ) tm.assert_frame_equal(result, df) + + +@pytest.mark.parametrize("merge_cells", [True, False, "columns"]) +def test_format_hierarchical_rows_periodindex(merge_cells): + # GH#60099 + df = DataFrame( + {"A": [1, 2]}, + index=MultiIndex.from_arrays( + [ + period_range(start="2006-10-06", end="2006-10-07", freq="D"), + ["X", "Y"], + ], + names=["date", "category"], + ), + ) + formatter = ExcelFormatter(df, merge_cells=merge_cells) + formatted_cells = formatter._format_hierarchical_rows() + + for cell in formatted_cells: + if cell.row != 0 and cell.col == 0: + assert isinstance( + cell.val, Timestamp + ), "Period should be converted to Timestamp" diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index 19fe9855dbb85..18948de72200a 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -23,6 +23,7 @@ MultiIndex, date_range, option_context, + period_range, ) import pandas._testing as tm @@ -335,6 +336,43 @@ def test_multiindex_interval_datetimes(self, tmp_excel): ) tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize("merge_cells", [True, False, "columns"]) + def test_excel_round_trip_with_periodindex(self, tmp_excel, merge_cells): + # GH#60099 + df = DataFrame( + {"A": [1, 2]}, + index=MultiIndex.from_arrays( + [ + period_range(start="2006-10-06", end="2006-10-07", freq="D"), + ["X", "Y"], + ], + names=["date", "category"], + ), + ) + df.to_excel(tmp_excel, merge_cells=merge_cells) + result = pd.read_excel(tmp_excel, index_col=[0, 1]) + expected = DataFrame( + {"A": [1, 2]}, + MultiIndex.from_arrays( + [ + [ + pd.to_datetime("2006-10-06 00:00:00"), + pd.to_datetime("2006-10-07 00:00:00"), + ], + ["X", "Y"], + ], + names=["date", "category"], + ), + ) + time_format = ( + "datetime64[s]" if tmp_excel.endswith(".ods") else "datetime64[us]" + ) + expected.index = expected.index.set_levels( + expected.index.levels[0].astype(time_format), level=0 + ) + + tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize( "engine,ext", From a2ceb52a9b3f8a3bb1ec6ad9729acca3ff1f6707 Mon Sep 17 00:00:00 2001 From: partev Date: Mon, 25 Nov 2024 13:36:08 -0500 Subject: [PATCH 098/266] fix issue #60410 (#60412) --- doc/source/user_guide/window.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/user_guide/window.rst b/doc/source/user_guide/window.rst index e25c4c2441920..0581951d5bfad 100644 --- a/doc/source/user_guide/window.rst +++ b/doc/source/user_guide/window.rst @@ -567,9 +567,9 @@ One must have :math:`0 < \alpha \leq 1`, and while it is possible to pass \alpha = \begin{cases} - \frac{2}{s + 1}, & \text{for span}\ s \geq 1\\ - \frac{1}{1 + c}, & \text{for center of mass}\ c \geq 0\\ - 1 - \exp^{\frac{\log 0.5}{h}}, & \text{for half-life}\ h > 0 + \frac{2}{s + 1}, & \text{for span}\ s \geq 1\\ + \frac{1}{1 + c}, & \text{for center of mass}\ c \geq 0\\ + 1 - e^{\frac{\log 0.5}{h}}, & \text{for half-life}\ h > 0 \end{cases} One must specify precisely one of **span**, **center of mass**, **half-life** From e78df6f8f2ed2ca892e4caff61d8edfdfce2e981 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 26 Nov 2024 00:09:31 +0530 Subject: [PATCH 099/266] DOC: fix SA01 for pandas.errors.UnsortedIndexError (#60404) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 772793702f8b8..2a8b5f15d95f3 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -119,7 +119,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.errors.PerformanceWarning SA01" \ -i "pandas.errors.PossibleDataLossError SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ - -i "pandas.errors.UnsortedIndexError SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.infer_freq SA01" \ -i "pandas.io.json.build_table_schema PR07,RT03,SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 68bd70603abae..d6d2fd82858ed 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -100,6 +100,11 @@ class UnsortedIndexError(KeyError): Subclass of `KeyError`. + See Also + -------- + DataFrame.sort_index : Sort a DataFrame by its index. + DataFrame.set_index : Set the DataFrame index using existing columns. + Examples -------- >>> df = pd.DataFrame( From cbd90ba5c403dc5449ac3b3a821ddc442c5ddc7d Mon Sep 17 00:00:00 2001 From: lfffkh <167774581+lfffkh@users.noreply.github.com> Date: Tue, 26 Nov 2024 02:40:37 +0800 Subject: [PATCH 100/266] Fix BUG: Cannot shift Intervals that are not closed='right' (the default) (#60407) first --- pandas/core/arrays/interval.py | 4 +++- pandas/tests/frame/methods/test_shift.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index f47ef095a8409..bbbf1d9ca60bd 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1055,7 +1055,9 @@ def shift(self, periods: int = 1, fill_value: object = None) -> IntervalArray: from pandas import Index fill_value = Index(self._left, copy=False)._na_value - empty = IntervalArray.from_breaks([fill_value] * (empty_len + 1)) + empty = IntervalArray.from_breaks( + [fill_value] * (empty_len + 1), closed=self.closed + ) else: empty = self._from_sequence([fill_value] * empty_len, dtype=self.dtype) diff --git a/pandas/tests/frame/methods/test_shift.py b/pandas/tests/frame/methods/test_shift.py index a0f96ff111444..b52240c208493 100644 --- a/pandas/tests/frame/methods/test_shift.py +++ b/pandas/tests/frame/methods/test_shift.py @@ -757,3 +757,12 @@ def test_shift_with_offsets_freq_empty(self): df_shifted = DataFrame(index=shifted_dates) result = df.shift(freq=offset) tm.assert_frame_equal(result, df_shifted) + + def test_series_shift_interval_preserves_closed(self): + # GH#60389 + ser = Series( + [pd.Interval(1, 2, closed="right"), pd.Interval(2, 3, closed="right")] + ) + result = ser.shift(1) + expected = Series([np.nan, pd.Interval(1, 2, closed="right")]) + tm.assert_series_equal(result, expected) From bca4b1c0ccb3fe5a74bb945d01bc372a90cc0e11 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 26 Nov 2024 00:11:18 +0530 Subject: [PATCH 101/266] DOC: fix SA01,ES01 for pandas.errors.PossibleDataLossError (#60403) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 2a8b5f15d95f3..03c6b8dc077b9 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -117,7 +117,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.errors.NumbaUtilError SA01" \ -i "pandas.errors.OutOfBoundsTimedelta SA01" \ -i "pandas.errors.PerformanceWarning SA01" \ - -i "pandas.errors.PossibleDataLossError SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.infer_freq SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index d6d2fd82858ed..5642b0d33b4f7 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -638,6 +638,15 @@ class PossibleDataLossError(Exception): """ Exception raised when trying to open a HDFStore file when already opened. + This error is triggered when there is a potential risk of data loss due to + conflicting operations on an HDFStore file. It serves to prevent unintended + overwrites or data corruption by enforcing exclusive access to the file. + + See Also + -------- + HDFStore : Dict-like IO interface for storing pandas objects in PyTables. + HDFStore.open : Open an HDFStore file in the specified mode. + Examples -------- >>> store = pd.HDFStore("my-store", "a") # doctest: +SKIP From 582740b3c0a1ef211b490abbbd94c192b0367af5 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 26 Nov 2024 00:11:50 +0530 Subject: [PATCH 102/266] DOC: fix SA01 for pandas.errors.OutOfBoundsTimedelta (#60402) --- ci/code_checks.sh | 1 - pandas/_libs/tslibs/np_datetime.pyx | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 03c6b8dc077b9..2817d84bad7b8 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -115,7 +115,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.errors.NullFrequencyError SA01" \ -i "pandas.errors.NumExprClobberingError SA01" \ -i "pandas.errors.NumbaUtilError SA01" \ - -i "pandas.errors.OutOfBoundsTimedelta SA01" \ -i "pandas.errors.PerformanceWarning SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ diff --git a/pandas/_libs/tslibs/np_datetime.pyx b/pandas/_libs/tslibs/np_datetime.pyx index 193556b2697a9..1b7f04fe17238 100644 --- a/pandas/_libs/tslibs/np_datetime.pyx +++ b/pandas/_libs/tslibs/np_datetime.pyx @@ -201,6 +201,10 @@ class OutOfBoundsTimedelta(ValueError): Representation should be within a timedelta64[ns]. + See Also + -------- + date_range : Return a fixed frequency DatetimeIndex. + Examples -------- >>> pd.date_range(start="1/1/1700", freq="B", periods=100000) From 9fab4eb5fb0132731a360fdd8ea3b31d95de187f Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 26 Nov 2024 00:12:23 +0530 Subject: [PATCH 103/266] DOC: fix SA01,ES01 for pandas.errors.DuplicateLabelError (#60399) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 2817d84bad7b8..8bafcb8944e14 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -109,7 +109,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ - -i "pandas.errors.DuplicateLabelError SA01" \ -i "pandas.errors.IntCastingNaNError SA01" \ -i "pandas.errors.InvalidIndexError SA01" \ -i "pandas.errors.NullFrequencyError SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 5642b0d33b4f7..70e523688c644 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -393,6 +393,19 @@ class DuplicateLabelError(ValueError): """ Error raised when an operation would introduce duplicate labels. + This error is typically encountered when performing operations on objects + with `allows_duplicate_labels=False` and the operation would result in + duplicate labels in the index. Duplicate labels can lead to ambiguities + in indexing and reduce data integrity. + + See Also + -------- + Series.set_flags : Return a new ``Series`` object with updated flags. + DataFrame.set_flags : Return a new ``DataFrame`` object with updated flags. + Series.reindex : Conform ``Series`` object to new index with optional filling logic. + DataFrame.reindex : Conform ``DataFrame`` object to new index with optional filling + logic. + Examples -------- >>> s = pd.Series([0, 1, 2], index=["a", "b", "c"]).set_flags( From 00c2207cbe8e429d11db5973794b604041cd74b2 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 26 Nov 2024 00:12:55 +0530 Subject: [PATCH 104/266] DOC: fix SA01,ES01 for pandas.errors.InvalidIndexError (#60400) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 8bafcb8944e14..58b0d26f7e2f3 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -110,7 +110,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ -i "pandas.errors.IntCastingNaNError SA01" \ - -i "pandas.errors.InvalidIndexError SA01" \ -i "pandas.errors.NullFrequencyError SA01" \ -i "pandas.errors.NumExprClobberingError SA01" \ -i "pandas.errors.NumbaUtilError SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 70e523688c644..814feadfb06e4 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -425,6 +425,16 @@ class InvalidIndexError(Exception): """ Exception raised when attempting to use an invalid index key. + This exception is triggered when a user attempts to access or manipulate + data in a pandas DataFrame or Series using an index key that is not valid + for the given object. This may occur in cases such as using a malformed + slice, a mismatched key for a ``MultiIndex``, or attempting to access an index + element that does not exist. + + See Also + -------- + MultiIndex : A multi-level, or hierarchical, index object for pandas objects. + Examples -------- >>> idx = pd.MultiIndex.from_product([["x", "y"], [0, 1]]) From 39dcbb4a06beaee7dd584a28958db72b9bba7531 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 26 Nov 2024 00:20:15 +0530 Subject: [PATCH 105/266] DOC: fix SA01 for pandas.errors.NumExprClobberingError (#60401) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 58b0d26f7e2f3..246a907c5052c 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -111,7 +111,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.var SA01" \ -i "pandas.errors.IntCastingNaNError SA01" \ -i "pandas.errors.NullFrequencyError SA01" \ - -i "pandas.errors.NumExprClobberingError SA01" \ -i "pandas.errors.NumbaUtilError SA01" \ -i "pandas.errors.PerformanceWarning SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 814feadfb06e4..70d839d817114 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -538,6 +538,11 @@ class NumExprClobberingError(NameError): to 'numexpr'. 'numexpr' is the default engine value for these methods if the numexpr package is installed. + See Also + -------- + eval : Evaluate a Python expression as a string using various backends. + DataFrame.query : Query the columns of a DataFrame with a boolean expression. + Examples -------- >>> df = pd.DataFrame({"abs": [1, 1, 1]}) From 0b6cece3acda1ae6e4f582d8276851b02aeac1ea Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:35:37 -0800 Subject: [PATCH 106/266] TST: Avoid hashing np.timedelta64 without unit (#60416) --- pandas/tests/test_algos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 3d1177c23c612..611b92eb022d6 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -1254,7 +1254,7 @@ def test_value_counts_nat(self): result_dt = algos.value_counts_internal(dt) tm.assert_series_equal(result_dt, exp_dt) - exp_td = Series({np.timedelta64(10000): 1}, name="count") + exp_td = Series([1], index=[np.timedelta64(10000)], name="count") result_td = algos.value_counts_internal(td) tm.assert_series_equal(result_td, exp_td) From 759874e4d4290f873cabc3eb525df203bd77b7e4 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 25 Nov 2024 16:01:47 -0800 Subject: [PATCH 107/266] BUG: Fix formatting of complex numbers with exponents (#60417) Fix formatting of complex numbers with exponents --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/format.py | 2 +- pandas/tests/io/formats/test_to_string.py | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1d55fc3ed7b84..1b12735f0e7c1 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -789,6 +789,7 @@ Other - Bug in :meth:`Series.dt` methods in :class:`ArrowDtype` that were returning incorrect values. (:issue:`57355`) - Bug in :meth:`Series.rank` that doesn't preserve missing values for nullable integers when ``na_option='keep'``. (:issue:`56976`) - Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` inconsistently replacing matching instances when ``regex=True`` and missing values are present. (:issue:`56599`) +- Bug in :meth:`Series.to_string` when series contains complex floats with exponents (:issue:`60405`) - Bug in :meth:`read_csv` where chained fsspec TAR file and ``compression="infer"`` fails with ``tarfile.ReadError`` (:issue:`60028`) - Bug in Dataframe Interchange Protocol implementation was returning incorrect results for data buffers' associated dtype, for string and datetime columns (:issue:`54781`) - Bug in ``Series.list`` methods not preserving the original :class:`Index`. (:issue:`58425`) diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index 861f5885f80c6..4f87b1a30ca61 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -1749,7 +1749,7 @@ def _trim_zeros_complex(str_complexes: ArrayLike, decimal: str = ".") -> list[st # The split will give [{"", "-"}, "xxx", "+/-", "xxx", "j", ""] # Therefore, the imaginary part is the 4th and 3rd last elements, # and the real part is everything before the imaginary part - trimmed = re.split(r"([j+-])", x) + trimmed = re.split(r"(? Date: Mon, 25 Nov 2024 16:03:56 -0800 Subject: [PATCH 108/266] Bump pypa/cibuildwheel from 2.21.3 to 2.22.0 (#60414) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.21.3 to 2.22.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.21.3...v2.22.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 354402c572ade..32ca5573ac08a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -152,7 +152,7 @@ jobs: run: echo "sdist_name=$(cd ./dist && ls -d */)" >> "$GITHUB_ENV" - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 with: package-dir: ./dist/${{ startsWith(matrix.buildplat[1], 'macosx') && env.sdist_name || needs.build_sdist.outputs.sdist_file }} env: From ab757ff8c352a0f02fbad22b463f0cfeaee88d3c Mon Sep 17 00:00:00 2001 From: sooooooing <126747506+sooooooing@users.noreply.github.com> Date: Wed, 27 Nov 2024 03:38:15 +0900 Subject: [PATCH 109/266] DOC: fix docstring api.types.is_re_compilable (#60419) * fix docstring api.types.is_re_compilable * fix lint error --- ci/code_checks.sh | 1 - pandas/core/dtypes/inference.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 246a907c5052c..9faa2a249613b 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -82,7 +82,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.min PR02" \ -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ - -i "pandas.api.types.is_re_compilable PR07,SA01" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ -i "pandas.arrays.IntegerArray SA01" \ -i "pandas.arrays.IntervalArray.length SA01" \ diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index 6adb34ff0f777..918d107f2ce6c 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -190,12 +190,17 @@ def is_re_compilable(obj: object) -> bool: Parameters ---------- obj : The object to check + The object to check if the object can be compiled into a regex pattern instance. Returns ------- bool Whether `obj` can be compiled as a regex pattern. + See Also + -------- + api.types.is_re : Check if the object is a regex pattern instance. + Examples -------- >>> from pandas.api.types import is_re_compilable From be41966198eebf2f56d32b7f0f8d6c3bc4283e61 Mon Sep 17 00:00:00 2001 From: "Olivier H." Date: Tue, 26 Nov 2024 19:41:31 +0100 Subject: [PATCH 110/266] DOC: Clarifying pandas.melt method documentation by replacing "massage" by "reshape" (#60420) Clarifying pandas.melt method documentation by replacing "massage" by "reshape" Meanwhile, "massage" is correct in a colloquial sense to mean transforming or reshaping data. This is far from accessible for a non-English speaker (as I am). Using the term `reshape` or `transform` is more meaningful while being accurate. --- doc/source/user_guide/reshaping.rst | 2 +- pandas/core/reshape/melt.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/user_guide/reshaping.rst b/doc/source/user_guide/reshaping.rst index 3347f3a2534f4..8c5e98791a9ef 100644 --- a/doc/source/user_guide/reshaping.rst +++ b/doc/source/user_guide/reshaping.rst @@ -321,7 +321,7 @@ The missing value can be filled with a specific value with the ``fill_value`` ar .. image:: ../_static/reshaping_melt.png The top-level :func:`~pandas.melt` function and the corresponding :meth:`DataFrame.melt` -are useful to massage a :class:`DataFrame` into a format where one or more columns +are useful to reshape a :class:`DataFrame` into a format where one or more columns are *identifier variables*, while all other columns, considered *measured variables*, are "unpivoted" to the row axis, leaving just two non-identifier columns, "variable" and "value". The names of those columns can be customized diff --git a/pandas/core/reshape/melt.py b/pandas/core/reshape/melt.py index bfd8e3ccd2f7c..f4cb82816bbcf 100644 --- a/pandas/core/reshape/melt.py +++ b/pandas/core/reshape/melt.py @@ -51,9 +51,9 @@ def melt( """ Unpivot a DataFrame from wide to long format, optionally leaving identifiers set. - This function is useful to massage a DataFrame into a format where one + This function is useful to reshape a DataFrame into a format where one or more columns are identifier variables (`id_vars`), while all other - columns, considered measured variables (`value_vars`), are "unpivoted" to + columns are considered measured variables (`value_vars`), and are "unpivoted" to the row axis, leaving just two non-identifier columns, 'variable' and 'value'. From fd570f466e05f8944c67735d12c04eaab2d37478 Mon Sep 17 00:00:00 2001 From: partev Date: Tue, 26 Nov 2024 14:35:12 -0500 Subject: [PATCH 111/266] replace twitter->X (#60426) --- doc/source/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index ddbda0aa3bf65..677ee6274b093 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -242,7 +242,6 @@ "external_links": [], "footer_start": ["pandas_footer", "sphinx-version"], "github_url": "https://github.com/pandas-dev/pandas", - "twitter_url": "https://twitter.com/pandas_dev", "analytics": { "plausible_analytics_domain": "pandas.pydata.org", "plausible_analytics_url": "https://views.scientific-python.org/js/script.js", @@ -258,6 +257,11 @@ # patch version doesn't compare as equal (e.g. 2.2.1 != 2.2.0 but it should be) "show_version_warning_banner": False, "icon_links": [ + { + "name": "X", + "url": "https://x.com/pandas_dev", + "icon": "fa-brands fa-square-x-twitter", + }, { "name": "Mastodon", "url": "https://fosstodon.org/@pandas_dev", From 98f7e4deeff26a5ef993ee27104387a1a6e0d3d3 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 26 Nov 2024 21:07:06 +0100 Subject: [PATCH 112/266] String dtype: use ObjectEngine for indexing for now correctness over performance (#60329) --- pandas/_libs/index.pyi | 3 + pandas/_libs/index.pyx | 25 +++++ pandas/core/indexes/base.py | 3 +- pandas/tests/indexes/string/test_indexing.py | 104 ++++++++++++++++-- .../io/parser/common/test_common_basic.py | 3 +- 5 files changed, 124 insertions(+), 14 deletions(-) diff --git a/pandas/_libs/index.pyi b/pandas/_libs/index.pyi index bf6d8ba8973d3..3af2856d2fbbf 100644 --- a/pandas/_libs/index.pyi +++ b/pandas/_libs/index.pyi @@ -72,6 +72,9 @@ class MaskedUInt16Engine(MaskedIndexEngine): ... class MaskedUInt8Engine(MaskedIndexEngine): ... class MaskedBoolEngine(MaskedUInt8Engine): ... +class StringObjectEngine(ObjectEngine): + def __init__(self, values: object, na_value) -> None: ... + class BaseMultiIndexCodesEngine: levels: list[np.ndarray] offsets: np.ndarray # np.ndarray[..., ndim=1] diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 1506a76aa94a6..688f943760d1f 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -557,6 +557,31 @@ cdef class StringEngine(IndexEngine): raise KeyError(val) return str(val) +cdef class StringObjectEngine(ObjectEngine): + + cdef: + object na_value + bint uses_na + + def __init__(self, ndarray values, na_value): + super().__init__(values) + self.na_value = na_value + self.uses_na = na_value is C_NA + + cdef bint _checknull(self, object val): + if self.uses_na: + return val is C_NA + else: + return util.is_nan(val) + + cdef _check_type(self, object val): + if isinstance(val, str): + return val + elif self._checknull(val): + return self.na_value + else: + raise KeyError(val) + cdef class DatetimeEngine(Int64Engine): diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index d4ba7e01ebfa9..165fe109c4c94 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -876,7 +876,7 @@ def _engine( # ndarray[Any, Any]]" has no attribute "_ndarray" [union-attr] target_values = self._data._ndarray # type: ignore[union-attr] elif is_string_dtype(self.dtype) and not is_object_dtype(self.dtype): - return libindex.StringEngine(target_values) + return libindex.StringObjectEngine(target_values, self.dtype.na_value) # type: ignore[union-attr] # error: Argument 1 to "ExtensionEngine" has incompatible type # "ndarray[Any, Any]"; expected "ExtensionArray" @@ -5974,7 +5974,6 @@ def _should_fallback_to_positional(self) -> bool: def get_indexer_non_unique( self, target ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]: - target = ensure_index(target) target = self._maybe_cast_listlike_indexer(target) if not self._should_compare(target) and not self._should_partial_index(target): diff --git a/pandas/tests/indexes/string/test_indexing.py b/pandas/tests/indexes/string/test_indexing.py index 755b7109a5a04..d1a278af337b7 100644 --- a/pandas/tests/indexes/string/test_indexing.py +++ b/pandas/tests/indexes/string/test_indexing.py @@ -6,6 +6,51 @@ import pandas._testing as tm +def _isnan(val): + try: + return val is not pd.NA and np.isnan(val) + except TypeError: + return False + + +class TestGetLoc: + def test_get_loc(self, any_string_dtype): + index = Index(["a", "b", "c"], dtype=any_string_dtype) + assert index.get_loc("b") == 1 + + def test_get_loc_raises(self, any_string_dtype): + index = Index(["a", "b", "c"], dtype=any_string_dtype) + with pytest.raises(KeyError, match="d"): + index.get_loc("d") + + def test_get_loc_invalid_value(self, any_string_dtype): + index = Index(["a", "b", "c"], dtype=any_string_dtype) + with pytest.raises(KeyError, match="1"): + index.get_loc(1) + + def test_get_loc_non_unique(self, any_string_dtype): + index = Index(["a", "b", "a"], dtype=any_string_dtype) + result = index.get_loc("a") + expected = np.array([True, False, True]) + tm.assert_numpy_array_equal(result, expected) + + def test_get_loc_non_missing(self, any_string_dtype, nulls_fixture): + index = Index(["a", "b", "c"], dtype=any_string_dtype) + with pytest.raises(KeyError): + index.get_loc(nulls_fixture) + + def test_get_loc_missing(self, any_string_dtype, nulls_fixture): + index = Index(["a", "b", nulls_fixture], dtype=any_string_dtype) + if any_string_dtype == "string" and ( + (any_string_dtype.na_value is pd.NA and nulls_fixture is not pd.NA) + or (_isnan(any_string_dtype.na_value) and not _isnan(nulls_fixture)) + ): + with pytest.raises(KeyError): + index.get_loc(nulls_fixture) + else: + assert index.get_loc(nulls_fixture) == 2 + + class TestGetIndexer: @pytest.mark.parametrize( "method,expected", @@ -41,23 +86,60 @@ def test_get_indexer_strings_raises(self, any_string_dtype): ["a", "b", "c", "d"], method="pad", tolerance=[2, 2, 2, 2] ) + @pytest.mark.parametrize("null", [None, np.nan, float("nan"), pd.NA]) + def test_get_indexer_missing(self, any_string_dtype, null, using_infer_string): + # NaT and Decimal("NaN") from null_fixture are not supported for string dtype + index = Index(["a", "b", null], dtype=any_string_dtype) + result = index.get_indexer(["a", null, "c"]) + if using_infer_string: + expected = np.array([0, 2, -1], dtype=np.intp) + elif any_string_dtype == "string" and ( + (any_string_dtype.na_value is pd.NA and null is not pd.NA) + or (_isnan(any_string_dtype.na_value) and not _isnan(null)) + ): + expected = np.array([0, -1, -1], dtype=np.intp) + else: + expected = np.array([0, 2, -1], dtype=np.intp) -class TestGetIndexerNonUnique: - @pytest.mark.xfail(reason="TODO(infer_string)", strict=False) - def test_get_indexer_non_unique_nas(self, any_string_dtype, nulls_fixture): - index = Index(["a", "b", None], dtype=any_string_dtype) - indexer, missing = index.get_indexer_non_unique([nulls_fixture]) + tm.assert_numpy_array_equal(result, expected) - expected_indexer = np.array([2], dtype=np.intp) - expected_missing = np.array([], dtype=np.intp) + +class TestGetIndexerNonUnique: + @pytest.mark.parametrize("null", [None, np.nan, float("nan"), pd.NA]) + def test_get_indexer_non_unique_nas( + self, any_string_dtype, null, using_infer_string + ): + index = Index(["a", "b", null], dtype=any_string_dtype) + indexer, missing = index.get_indexer_non_unique(["a", null]) + + if using_infer_string: + expected_indexer = np.array([0, 2], dtype=np.intp) + expected_missing = np.array([], dtype=np.intp) + elif any_string_dtype == "string" and ( + (any_string_dtype.na_value is pd.NA and null is not pd.NA) + or (_isnan(any_string_dtype.na_value) and not _isnan(null)) + ): + expected_indexer = np.array([0, -1], dtype=np.intp) + expected_missing = np.array([1], dtype=np.intp) + else: + expected_indexer = np.array([0, 2], dtype=np.intp) + expected_missing = np.array([], dtype=np.intp) tm.assert_numpy_array_equal(indexer, expected_indexer) tm.assert_numpy_array_equal(missing, expected_missing) # actually non-unique - index = Index(["a", None, "b", None], dtype=any_string_dtype) - indexer, missing = index.get_indexer_non_unique([nulls_fixture]) - - expected_indexer = np.array([1, 3], dtype=np.intp) + index = Index(["a", null, "b", null], dtype=any_string_dtype) + indexer, missing = index.get_indexer_non_unique(["a", null]) + + if using_infer_string: + expected_indexer = np.array([0, 1, 3], dtype=np.intp) + elif any_string_dtype == "string" and ( + (any_string_dtype.na_value is pd.NA and null is not pd.NA) + or (_isnan(any_string_dtype.na_value) and not _isnan(null)) + ): + pass + else: + expected_indexer = np.array([0, 1, 3], dtype=np.intp) tm.assert_numpy_array_equal(indexer, expected_indexer) tm.assert_numpy_array_equal(missing, expected_missing) diff --git a/pandas/tests/io/parser/common/test_common_basic.py b/pandas/tests/io/parser/common/test_common_basic.py index 511db2c6a33d8..3680273f5e98a 100644 --- a/pandas/tests/io/parser/common/test_common_basic.py +++ b/pandas/tests/io/parser/common/test_common_basic.py @@ -15,6 +15,7 @@ from pandas._config import using_string_dtype +from pandas.compat import HAS_PYARROW from pandas.errors import ( EmptyDataError, ParserError, @@ -766,7 +767,7 @@ def test_dict_keys_as_names(all_parsers): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") +@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)") @xfail_pyarrow # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xed in position 0 def test_encoding_surrogatepass(all_parsers): # GH39017 From 106f33cfce16f4e08f6ca5bd0e6e440ec9a94867 Mon Sep 17 00:00:00 2001 From: Jason Mok <106209849+jasonmokk@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:28:39 -0600 Subject: [PATCH 113/266] DOC: Add type hint for squeeze method (#60415) Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/core/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 039bdf9c36ee7..a6be17a654aa7 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -838,7 +838,7 @@ def pop(self, item: Hashable) -> Series | Any: return result @final - def squeeze(self, axis: Axis | None = None): + def squeeze(self, axis: Axis | None = None) -> Scalar | Series | DataFrame: """ Squeeze 1 dimensional axis objects into scalars. From 1d809c3c45c5cd0b32211790fa84172e7f48b270 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Thu, 28 Nov 2024 02:46:42 +0800 Subject: [PATCH 114/266] BUG: fix NameError raised when specifying dtype with string having "[pyarrow]" while PyArrow is not installed (#60413) * Add test * Fix * Add note * Update pandas/tests/dtypes/test_common.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> * update * Fix doc warning --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/dtypes/dtypes.py | 2 ++ pandas/tests/dtypes/test_common.py | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1b12735f0e7c1..4bd31de185bb4 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -761,6 +761,7 @@ ExtensionArray - Bug in :meth:`.arrays.ArrowExtensionArray.__setitem__` which caused wrong behavior when using an integer array with repeated values as a key (:issue:`58530`) - Bug in :meth:`api.types.is_datetime64_any_dtype` where a custom :class:`ExtensionDtype` would return ``False`` for array-likes (:issue:`57055`) - Bug in comparison between object with :class:`ArrowDtype` and incompatible-dtyped (e.g. string vs bool) incorrectly raising instead of returning all-``False`` (for ``==``) or all-``True`` (for ``!=``) (:issue:`59505`) +- Bug in constructing pandas data structures when passing into ``dtype`` a string of the type followed by ``[pyarrow]`` while PyArrow is not installed would raise ``NameError`` rather than ``ImportError`` (:issue:`57928`) - Bug in various :class:`DataFrame` reductions for pyarrow temporal dtypes returning incorrect dtype when result was null (:issue:`59234`) Styler diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 96b0aa16940a6..e5d1033de4457 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -2344,6 +2344,8 @@ def construct_from_string(cls, string: str) -> ArrowDtype: if string == "string[pyarrow]": # Ensure Registry.find skips ArrowDtype to use StringDtype instead raise TypeError("string[pyarrow] should be constructed by StringDtype") + if pa_version_under10p1: + raise ImportError("pyarrow>=10.0.1 is required for ArrowDtype") base_type = string[:-9] # get rid of "[pyarrow]" try: diff --git a/pandas/tests/dtypes/test_common.py b/pandas/tests/dtypes/test_common.py index e338fb1331734..5a59617ce5bd3 100644 --- a/pandas/tests/dtypes/test_common.py +++ b/pandas/tests/dtypes/test_common.py @@ -835,3 +835,10 @@ def test_pandas_dtype_string_dtypes(string_storage): with pd.option_context("string_storage", string_storage): result = pandas_dtype("string") assert result == pd.StringDtype(string_storage, na_value=pd.NA) + + +@td.skip_if_installed("pyarrow") +def test_construct_from_string_without_pyarrow_installed(): + # GH 57928 + with pytest.raises(ImportError, match="pyarrow>=10.0.1 is required"): + pd.Series([-1.5, 0.2, None], dtype="float32[pyarrow]") From a4fc97e92ed938260728e3f6c2b92df5ffb57b7f Mon Sep 17 00:00:00 2001 From: Chris <76128089+thedataninja1786@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:51:44 +0200 Subject: [PATCH 115/266] BUG: escape single quotes in index names when printing (#60251) --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/indexes/frozen.py | 4 +++- pandas/io/formats/printing.py | 2 +- pandas/tests/io/formats/test_printing.py | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 4bd31de185bb4..e74bd2f745b94 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -667,7 +667,7 @@ Indexing ^^^^^^^^ - Bug in :meth:`DataFrame.__getitem__` returning modified columns when called with ``slice`` in Python 3.12 (:issue:`57500`) - Bug in :meth:`DataFrame.from_records` throwing a ``ValueError`` when passed an empty list in ``index`` (:issue:`58594`) -- +- Bug in printing :attr:`Index.names` and :attr:`MultiIndex.levels` would not escape single quotes (:issue:`60190`) Missing ^^^^^^^ diff --git a/pandas/core/indexes/frozen.py b/pandas/core/indexes/frozen.py index c559c529586b5..254bd71ade209 100644 --- a/pandas/core/indexes/frozen.py +++ b/pandas/core/indexes/frozen.py @@ -110,7 +110,9 @@ def _disabled(self, *args, **kwargs) -> NoReturn: raise TypeError(f"'{type(self).__name__}' does not support mutable operations.") def __str__(self) -> str: - return pprint_thing(self, quote_strings=True, escape_chars=("\t", "\r", "\n")) + return pprint_thing( + self, quote_strings=True, escape_chars=("\t", "\r", "\n", "'") + ) def __repr__(self) -> str: return f"{type(self).__name__}({self!s})" diff --git a/pandas/io/formats/printing.py b/pandas/io/formats/printing.py index 67b5eb6f5ee5b..a9936ba8c8f2c 100644 --- a/pandas/io/formats/printing.py +++ b/pandas/io/formats/printing.py @@ -203,7 +203,7 @@ def pprint_thing( def as_escaped_string( thing: Any, escape_chars: EscapeChars | None = escape_chars ) -> str: - translate = {"\t": r"\t", "\n": r"\n", "\r": r"\r"} + translate = {"\t": r"\t", "\n": r"\n", "\r": r"\r", "'": r"\'"} if isinstance(escape_chars, Mapping): if default_escapes: translate.update(escape_chars) diff --git a/pandas/tests/io/formats/test_printing.py b/pandas/tests/io/formats/test_printing.py index 1009dfec53218..3b63011bf862e 100644 --- a/pandas/tests/io/formats/test_printing.py +++ b/pandas/tests/io/formats/test_printing.py @@ -3,11 +3,33 @@ from collections.abc import Mapping import string +import pytest + import pandas._config.config as cf +import pandas as pd + from pandas.io.formats import printing +@pytest.mark.parametrize( + "input_names, expected_names", + [ + (["'a b"], "['\\'a b']"), # Escape leading quote + (["test's b"], "['test\\'s b']"), # Escape apostrophe + (["'test' b"], "['\\'test\\' b']"), # Escape surrounding quotes + (["test b'"], "['test b\\'']"), # Escape single quote + (["test\n' b"], "['test\\n\\' b']"), # Escape quotes, preserve newline + ], +) +def test_formatted_index_names(input_names, expected_names): + # GH#60190 + df = pd.DataFrame({name: [1, 2, 3] for name in input_names}).set_index(input_names) + formatted_names = str(df.index.names) + + assert formatted_names == expected_names + + def test_adjoin(): data = [["a", "b", "c"], ["dd", "ee", "ff"], ["ggg", "hhh", "iii"]] expected = "a dd ggg\nb ee hhh\nc ff iii" From 652682993cfe130c733944f494e82c367bcfac5b Mon Sep 17 00:00:00 2001 From: Christian Castro <46094200+gvmmybear@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:25:16 -0600 Subject: [PATCH 116/266] DOC: indices docstrings for DataFrameGroupBy, SeriesGroupBy, Resampler (#60444) --- ci/code_checks.sh | 3 --- pandas/core/groupby/groupby.py | 9 +++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 9faa2a249613b..96e06ab0d6234 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -89,16 +89,13 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \ - -i "pandas.core.groupby.DataFrameGroupBy.indices SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.nunique SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.DataFrameGroupBy.sem SA01" \ -i "pandas.core.groupby.SeriesGroupBy.get_group RT03,SA01" \ - -i "pandas.core.groupby.SeriesGroupBy.indices SA01" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.sem SA01" \ -i "pandas.core.resample.Resampler.get_group RT03,SA01" \ - -i "pandas.core.resample.Resampler.indices SA01" \ -i "pandas.core.resample.Resampler.max PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.mean SA01" \ -i "pandas.core.resample.Resampler.min PR01,RT03,SA01" \ diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index ad23127ad449f..48d4e0456d4fa 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -515,6 +515,15 @@ def indices(self) -> dict[Hashable, npt.NDArray[np.intp]]: """ Dict {group name -> group indices}. + See Also + -------- + core.groupby.DataFrameGroupBy.indices : Provides a mapping of group rows to + positions of the elements. + core.groupby.SeriesGroupBy.indices : Provides a mapping of group rows to + positions of the elements. + core.resample.Resampler.indices : Provides a mapping of group rows to + positions of the elements. + Examples -------- From fef205a70c3e28a10af5a5508eea53e9a4701e3a Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 3 Dec 2024 00:33:11 +0530 Subject: [PATCH 117/266] DOC: fix ES01 for pandas.set_option (#60452) --- pandas/_config/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/_config/config.py b/pandas/_config/config.py index 1d57aa806e0f1..35139979f92fe 100644 --- a/pandas/_config/config.py +++ b/pandas/_config/config.py @@ -188,6 +188,11 @@ def set_option(*args) -> None: """ Set the value of the specified option or options. + This method allows fine-grained control over the behavior and display settings + of pandas. Options affect various functionalities such as output formatting, + display limits, and operational behavior. Settings can be modified at runtime + without requiring changes to global configurations or environment variables. + Parameters ---------- *args : str | object From 335f600a5ad7152e9572e03f8ed9760e0dfa6f0e Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 3 Dec 2024 00:36:09 +0530 Subject: [PATCH 118/266] DOC: fix SA01,ES01 for pandas.io.stata.StataWriter.write_file (#60449) --- ci/code_checks.sh | 1 - pandas/io/stata.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 96e06ab0d6234..3948f654f92b6 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -113,7 +113,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.infer_freq SA01" \ -i "pandas.io.json.build_table_schema PR07,RT03,SA01" \ - -i "pandas.io.stata.StataWriter.write_file SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.plotting.scatter_matrix PR07,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ diff --git a/pandas/io/stata.py b/pandas/io/stata.py index ed89d5766c306..63f729c8347b1 100644 --- a/pandas/io/stata.py +++ b/pandas/io/stata.py @@ -2748,6 +2748,18 @@ def write_file(self) -> None: """ Export DataFrame object to Stata dta format. + This method writes the contents of a pandas DataFrame to a `.dta` file + compatible with Stata. It includes features for handling value labels, + variable types, and metadata like timestamps and data labels. The output + file can then be read and used in Stata or other compatible statistical + tools. + + See Also + -------- + read_stata : Read Stata file into DataFrame. + DataFrame.to_stata : Export DataFrame object to Stata dta format. + io.stata.StataWriter : A class for writing Stata binary dta files. + Examples -------- >>> df = pd.DataFrame( From b22f2350de8db0e6919016de79f6628e89949b6f Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 3 Dec 2024 00:36:49 +0530 Subject: [PATCH 119/266] DOC: fix SA01,ES01 for pandas.arrays.IntegerArray (#60447) --- ci/code_checks.sh | 1 - pandas/core/arrays/integer.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 3948f654f92b6..b3d1f572fa3b9 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -83,7 +83,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ - -i "pandas.arrays.IntegerArray SA01" \ -i "pandas.arrays.IntervalArray.length SA01" \ -i "pandas.arrays.NumpyExtensionArray SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index f85fbd062b0c3..afbadd754cdbc 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -105,6 +105,12 @@ class IntegerArray(NumericArray): ------- IntegerArray + See Also + -------- + array : Create an array using the appropriate dtype, including ``IntegerArray``. + Int32Dtype : An ExtensionDtype for int32 integer data. + UInt16Dtype : An ExtensionDtype for uint16 integer data. + Examples -------- Create an IntegerArray with :func:`pandas.array`. From d200c647b7312491f00efccb764e4de733b745b8 Mon Sep 17 00:00:00 2001 From: UV Date: Tue, 3 Dec 2024 00:39:38 +0530 Subject: [PATCH 120/266] DOC: Added missing links to optional dependencies in getting_started/install.html (#60446) * Checking for the first link added * DOC: Added missing links to optional dependencies in getting_started/install.html --- doc/source/getting_started/install.rst | 162 ++++++++++++------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index b3982c4ad091f..bda959f380e8a 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -193,25 +193,25 @@ Visualization Installable with ``pip install "pandas[plot, output-formatting]"``. -========================= ================== ================== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== ================== ============================================================= -matplotlib 3.6.3 plot Plotting library -Jinja2 3.1.2 output-formatting Conditional formatting with DataFrame.style -tabulate 0.9.0 output-formatting Printing in Markdown-friendly format (see `tabulate`_) -========================= ================== ================== ============================================================= +========================================================== ================== ================== ======================================================= +Dependency Minimum Version pip extra Notes +========================================================== ================== ================== ======================================================= +`matplotlib `__ 3.6.3 plot Plotting library +`Jinja2 `__ 3.1.2 output-formatting Conditional formatting with DataFrame.style +`tabulate `__ 0.9.0 output-formatting Printing in Markdown-friendly format (see `tabulate`_) +========================================================== ================== ================== ======================================================= Computation ^^^^^^^^^^^ Installable with ``pip install "pandas[computation]"``. -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -SciPy 1.10.0 computation Miscellaneous statistical functions -xarray 2022.12.0 computation pandas-like API for N-dimensional data -========================= ================== =============== ============================================================= +============================================== ================== =============== ======================================= +Dependency Minimum Version pip extra Notes +============================================== ================== =============== ======================================= +`SciPy `__ 1.10.0 computation Miscellaneous statistical functions +`xarray `__ 2022.12.0 computation pandas-like API for N-dimensional data +============================================== ================== =============== ======================================= .. _install.excel_dependencies: @@ -220,29 +220,29 @@ Excel files Installable with ``pip install "pandas[excel]"``. -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -xlrd 2.0.1 excel Reading for xls files -xlsxwriter 3.0.5 excel Writing for xlsx files -openpyxl 3.1.0 excel Reading / writing for Excel 2010 xlsx/xlsm/xltx/xltm files -pyxlsb 1.0.10 excel Reading for xlsb files -python-calamine 0.1.7 excel Reading for xls/xlsx/xlsm/xlsb/xla/xlam/ods files -odfpy 1.4.1 excel Reading / writing for OpenDocument 1.2 files -========================= ================== =============== ============================================================= +================================================================== ================== =============== ============================================================= +Dependency Minimum Version pip extra Notes +================================================================== ================== =============== ============================================================= +`xlrd `__ 2.0.1 excel Reading for xls files +`xlsxwriter `__ 3.0.5 excel Writing for xlsx files +`openpyxl `__ 3.1.0 excel Reading / writing for Excel 2010 xlsx/xlsm/xltx/xltm files +`pyxlsb `__ 1.0.10 excel Reading for xlsb files +`python-calamine `__ 0.1.7 excel Reading for xls/xlsx/xlsm/xlsb/xla/xlam/ods files +`odfpy `__ 1.4.1 excel Reading / writing for OpenDocument 1.2 files +================================================================== ================== =============== ============================================================= HTML ^^^^ Installable with ``pip install "pandas[html]"``. -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -BeautifulSoup4 4.11.2 html HTML parser for read_html -html5lib 1.1 html HTML parser for read_html -lxml 4.9.2 html HTML parser for read_html -========================= ================== =============== ============================================================= +=============================================================== ================== =============== ========================== +Dependency Minimum Version pip extra Notes +=============================================================== ================== =============== ========================== +`BeautifulSoup4 `__ 4.11.2 html HTML parser for read_html +`html5lib `__ 1.1 html HTML parser for read_html +`lxml `__ 4.9.2 html HTML parser for read_html +=============================================================== ================== =============== ========================== One of the following combinations of libraries is needed to use the top-level :func:`~pandas.read_html` function: @@ -273,45 +273,45 @@ XML Installable with ``pip install "pandas[xml]"``. -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -lxml 4.9.2 xml XML parser for read_xml and tree builder for to_xml -========================= ================== =============== ============================================================= +======================================== ================== =============== ==================================================== +Dependency Minimum Version pip extra Notes +======================================== ================== =============== ==================================================== +`lxml `__ 4.9.2 xml XML parser for read_xml and tree builder for to_xml +======================================== ================== =============== ==================================================== SQL databases ^^^^^^^^^^^^^ Traditional drivers are installable with ``pip install "pandas[postgresql, mysql, sql-other]"`` -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -SQLAlchemy 2.0.0 postgresql, SQL support for databases other than sqlite - mysql, - sql-other -psycopg2 2.9.6 postgresql PostgreSQL engine for sqlalchemy -pymysql 1.0.2 mysql MySQL engine for sqlalchemy -adbc-driver-postgresql 0.10.0 postgresql ADBC Driver for PostgreSQL -adbc-driver-sqlite 0.8.0 sql-other ADBC Driver for SQLite -========================= ================== =============== ============================================================= +================================================================== ================== =============== ============================================ +Dependency Minimum Version pip extra Notes +================================================================== ================== =============== ============================================ +`SQLAlchemy `__ 2.0.0 postgresql, SQL support for databases other than sqlite + mysql, + sql-other +`psycopg2 `__ 2.9.6 postgresql PostgreSQL engine for sqlalchemy +`pymysql `__ 1.0.2 mysql MySQL engine for sqlalchemy +`adbc-driver-postgresql `__ 0.10.0 postgresql ADBC Driver for PostgreSQL +`adbc-driver-sqlite `__ 0.8.0 sql-other ADBC Driver for SQLite +================================================================== ================== =============== ============================================ Other data sources ^^^^^^^^^^^^^^^^^^ Installable with ``pip install "pandas[hdf5, parquet, feather, spss, excel]"`` -========================= ================== ================ ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== ================ ============================================================= -PyTables 3.8.0 hdf5 HDF5-based reading / writing -blosc 1.21.3 hdf5 Compression for HDF5; only available on ``conda`` -zlib hdf5 Compression for HDF5 -fastparquet 2023.10.0 - Parquet reading / writing (pyarrow is default) -pyarrow 10.0.1 parquet, feather Parquet, ORC, and feather reading / writing -pyreadstat 1.2.0 spss SPSS files (.sav) reading -odfpy 1.4.1 excel Open document format (.odf, .ods, .odt) reading / writing -========================= ================== ================ ============================================================= +====================================================== ================== ================ ========================================================== +Dependency Minimum Version pip extra Notes +====================================================== ================== ================ ========================================================== +`PyTables `__ 3.8.0 hdf5 HDF5-based reading / writing +`blosc `__ 1.21.3 hdf5 Compression for HDF5; only available on ``conda`` +`zlib `__ hdf5 Compression for HDF5 +`fastparquet `__ 2023.10.0 - Parquet reading / writing (pyarrow is default) +`pyarrow `__ 10.0.1 parquet, feather Parquet, ORC, and feather reading / writing +`pyreadstat `__ 1.2.0 spss SPSS files (.sav) reading +`odfpy `__ 1.4.1 excel Open document format (.odf, .ods, .odt) reading / writing +====================================================== ================== ================ ========================================================== .. _install.warn_orc: @@ -326,26 +326,26 @@ Access data in the cloud Installable with ``pip install "pandas[fss, aws, gcp]"`` -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -fsspec 2022.11.0 fss, gcp, aws Handling files aside from simple local and HTTP (required - dependency of s3fs, gcsfs). -gcsfs 2022.11.0 gcp Google Cloud Storage access -s3fs 2022.11.0 aws Amazon S3 access -========================= ================== =============== ============================================================= +============================================ ================== =============== ========================================================== +Dependency Minimum Version pip extra Notes +============================================ ================== =============== ========================================================== +`fsspec `__ 2022.11.0 fss, gcp, aws Handling files aside from simple local and HTTP (required + dependency of s3fs, gcsfs). +`gcsfs `__ 2022.11.0 gcp Google Cloud Storage access +`s3fs `__ 2022.11.0 aws Amazon S3 access +============================================ ================== =============== ========================================================== Clipboard ^^^^^^^^^ Installable with ``pip install "pandas[clipboard]"``. -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -PyQt4/PyQt5 5.15.9 clipboard Clipboard I/O -qtpy 2.3.0 clipboard Clipboard I/O -========================= ================== =============== ============================================================= +======================================================================================== ================== =============== ============== +Dependency Minimum Version pip extra Notes +======================================================================================== ================== =============== ============== +`PyQt4 `__/`PyQt5 `__ 5.15.9 clipboard Clipboard I/O +`qtpy `__ 2.3.0 clipboard Clipboard I/O +======================================================================================== ================== =============== ============== .. note:: @@ -358,19 +358,19 @@ Compression Installable with ``pip install "pandas[compression]"`` -========================= ================== =============== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =============== ============================================================= -Zstandard 0.19.0 compression Zstandard compression -========================= ================== =============== ============================================================= +================================================= ================== =============== ====================== +Dependency Minimum Version pip extra Notes +================================================= ================== =============== ====================== +`Zstandard `__ 0.19.0 compression Zstandard compression +================================================= ================== =============== ====================== Timezone ^^^^^^^^ Installable with ``pip install "pandas[timezone]"`` -========================= ================== =================== ============================================================= -Dependency Minimum Version pip extra Notes -========================= ================== =================== ============================================================= -pytz 2023.4 timezone Alternative timezone library to ``zoneinfo``. -========================= ================== =================== ============================================================= +========================================== ================== =================== ============================================== +Dependency Minimum Version pip extra Notes +========================================== ================== =================== ============================================== +`pytz `__ 2023.4 timezone Alternative timezone library to ``zoneinfo``. +========================================== ================== =================== ============================================== From bf846d30e59140281cf51d3f3cabfd12e5fcf8fb Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 3 Dec 2024 00:40:12 +0530 Subject: [PATCH 121/266] DOC: fix SA01,ES01 for pandas.errors.IntCastingNaNError (#60442) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index b3d1f572fa3b9..5db76fba3a937 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -104,7 +104,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ - -i "pandas.errors.IntCastingNaNError SA01" \ -i "pandas.errors.NullFrequencyError SA01" \ -i "pandas.errors.NumbaUtilError SA01" \ -i "pandas.errors.PerformanceWarning SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 70d839d817114..b1a338893fe0a 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -20,6 +20,16 @@ class IntCastingNaNError(ValueError): """ Exception raised when converting (``astype``) an array with NaN to an integer type. + This error occurs when attempting to cast a data structure containing non-finite + values (such as NaN or infinity) to an integer data type. Integer types do not + support non-finite values, so such conversions are explicitly disallowed to + prevent silent data corruption or unexpected behavior. + + See Also + -------- + DataFrame.astype : Method to cast a pandas DataFrame object to a specified dtype. + Series.astype : Method to cast a pandas Series object to a specified dtype. + Examples -------- >>> pd.DataFrame(np.array([[1, np.nan], [2, 3]]), dtype="i8") From 45f27c81a408ada692ef51e2ce73408aee4d6c53 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 3 Dec 2024 00:40:49 +0530 Subject: [PATCH 122/266] DOC: fix SA01,ES01 for pandas.infer_freq (#60441) --- ci/code_checks.sh | 1 - pandas/tseries/frequencies.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 5db76fba3a937..dde98a01cc770 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -109,7 +109,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.errors.PerformanceWarning SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ - -i "pandas.infer_freq SA01" \ -i "pandas.io.json.build_table_schema PR07,RT03,SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.plotting.scatter_matrix PR07,SA01" \ diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 534bee5fede44..9a01568971af8 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -89,6 +89,11 @@ def infer_freq( """ Infer the most likely frequency given the input index. + This method attempts to deduce the most probable frequency (e.g., 'D' for daily, + 'H' for hourly) from a sequence of datetime-like objects. It is particularly useful + when the frequency of a time series is not explicitly set or known but can be + inferred from its values. + Parameters ---------- index : DatetimeIndex, TimedeltaIndex, Series or array-like @@ -106,6 +111,13 @@ def infer_freq( ValueError If there are fewer than three values. + See Also + -------- + date_range : Return a fixed frequency DatetimeIndex. + timedelta_range : Return a fixed frequency TimedeltaIndex with day as the default. + period_range : Return a fixed frequency PeriodIndex. + DatetimeIndex.freq : Return the frequency object if it is set, otherwise None. + Examples -------- >>> idx = pd.date_range(start="2020/12/01", end="2020/12/30", periods=30) From a14a8be8304b185404bfb0a89398778fecd8034a Mon Sep 17 00:00:00 2001 From: partev Date: Mon, 2 Dec 2024 14:12:14 -0500 Subject: [PATCH 123/266] upgrade to the latest version of PyData Sphinx Theme (#60430) * upgrade to the latest version of PyData Sphinx Theme upgrade PyData Sphinx Theme from 0.14 to the latest 0.16 it is needed to get the latest font-awesome pack to be able to display the new twitter-x icon. https://github.com/pandas-dev/pandas/pull/60426 * update pydata-sphinx-theme to version 0.16 update pydata-sphinx-theme to version 0.16 and synchronize with environment.yml --- environment.yml | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 9bf6cf2a92347..8ede5a16b7a59 100644 --- a/environment.yml +++ b/environment.yml @@ -87,7 +87,7 @@ dependencies: - google-auth - natsort # DataFrame.sort_values doctest - numpydoc - - pydata-sphinx-theme=0.14 + - pydata-sphinx-theme=0.16 - pytest-cython # doctest - sphinx - sphinx-design diff --git a/requirements-dev.txt b/requirements-dev.txt index 69568cf661241..b68b9f0c8f92c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -62,7 +62,7 @@ gitdb google-auth natsort numpydoc -pydata-sphinx-theme==0.14 +pydata-sphinx-theme==0.16 pytest-cython sphinx sphinx-design From 40131a6b9a67f72a17918a093819ad6f1484888b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:13:46 -0800 Subject: [PATCH 124/266] [pre-commit.ci] pre-commit autoupdate (#60470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1) - [github.com/MarcoGorelli/cython-lint: v0.16.2 → v0.16.6](https://github.com/MarcoGorelli/cython-lint/compare/v0.16.2...v0.16.6) - [github.com/pre-commit/mirrors-clang-format: v19.1.3 → v19.1.4](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.3...v19.1.4) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply ruff changes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +-- asv_bench/benchmarks/groupby.py | 3 +- pandas/__init__.py | 8 +-- pandas/_config/__init__.py | 6 +-- pandas/_libs/__init__.py | 2 +- pandas/_libs/tslibs/__init__.py | 44 ++++++++-------- pandas/_testing/__init__.py | 44 ++++++++-------- pandas/_testing/asserters.py | 14 ++--- pandas/_typing.py | 6 +-- pandas/api/__init__.py | 2 +- pandas/api/extensions/__init__.py | 8 +-- pandas/api/indexers/__init__.py | 2 +- pandas/api/interchange/__init__.py | 2 +- pandas/api/types/__init__.py | 4 +- pandas/api/typing/__init__.py | 8 ++- pandas/compat/__init__.py | 14 ++--- pandas/compat/numpy/__init__.py | 2 +- pandas/core/_numba/kernels/__init__.py | 8 +-- pandas/core/api.py | 38 +++++++------- pandas/core/arrays/__init__.py | 8 +-- pandas/core/arrays/arrow/__init__.py | 2 +- pandas/core/arrays/arrow/array.py | 3 +- pandas/core/arrays/sparse/__init__.py | 2 +- pandas/core/computation/eval.py | 10 ++-- pandas/core/computation/expr.py | 3 +- pandas/core/computation/pytables.py | 11 ++-- pandas/core/computation/scope.py | 2 +- pandas/core/dtypes/common.py | 6 +-- pandas/core/dtypes/dtypes.py | 8 ++- pandas/core/frame.py | 6 +-- pandas/core/groupby/__init__.py | 4 +- pandas/core/indexers/__init__.py | 16 +++--- pandas/core/indexes/api.py | 20 +++---- pandas/core/indexes/range.py | 2 +- pandas/core/indexing.py | 20 ++++--- pandas/core/internals/__init__.py | 4 +- pandas/core/internals/blocks.py | 5 +- pandas/core/internals/construction.py | 3 +- pandas/core/ops/__init__.py | 10 ++-- pandas/core/resample.py | 4 +- pandas/core/reshape/merge.py | 3 +- pandas/core/tools/numeric.py | 13 +++-- pandas/errors/__init__.py | 8 +-- pandas/io/__init__.py | 2 +- pandas/io/excel/__init__.py | 2 +- pandas/io/formats/__init__.py | 2 +- pandas/io/json/__init__.py | 6 +-- pandas/io/json/_json.py | 6 +-- pandas/io/parsers/base_parser.py | 2 +- pandas/io/parsers/python_parser.py | 5 +- pandas/io/stata.py | 14 ++--- pandas/plotting/__init__.py | 16 +++--- pandas/plotting/_matplotlib/__init__.py | 18 +++---- pandas/testing.py | 2 +- pandas/tests/extension/decimal/__init__.py | 2 +- pandas/tests/extension/test_arrow.py | 4 +- pandas/tests/extension/test_string.py | 5 +- pandas/tests/frame/methods/test_nlargest.py | 2 +- pandas/tests/test_nanops.py | 7 +-- pandas/tseries/__init__.py | 2 +- pandas/tseries/api.py | 2 +- pandas/tseries/holiday.py | 16 +++--- pandas/tseries/offsets.py | 58 ++++++++++----------- pandas/util/_decorators.py | 4 +- pyproject.toml | 4 -- scripts/validate_unwanted_patterns.py | 8 +-- 66 files changed, 276 insertions(+), 307 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09912bfb6c349..b7b9b1818c122 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ ci: skip: [pyright, mypy] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.2 + rev: v0.8.1 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -47,7 +47,7 @@ repos: types_or: [python, rst, markdown, cython, c] additional_dependencies: [tomli] - repo: https://github.com/MarcoGorelli/cython-lint - rev: v0.16.2 + rev: v0.16.6 hooks: - id: cython-lint - id: double-quote-cython-strings @@ -95,7 +95,7 @@ repos: - id: sphinx-lint args: ["--enable", "all", "--disable", "line-too-long"] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.3 + rev: v19.1.4 hooks: - id: clang-format files: ^pandas/_libs/src|^pandas/_libs/include diff --git a/asv_bench/benchmarks/groupby.py b/asv_bench/benchmarks/groupby.py index abffa1f702b9c..19c556dfe9d1f 100644 --- a/asv_bench/benchmarks/groupby.py +++ b/asv_bench/benchmarks/groupby.py @@ -511,8 +511,7 @@ def setup(self, dtype, method, application, ncols, engine): # grouping on multiple columns # and we lack kernels for a bunch of methods if ( - engine == "numba" - and method in _numba_unsupported_methods + (engine == "numba" and method in _numba_unsupported_methods) or ncols > 1 or application == "transformation" or dtype == "datetime" diff --git a/pandas/__init__.py b/pandas/__init__.py index 6c97baa890777..c570fb8d70204 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -235,6 +235,7 @@ # Pandas is not (yet) a py.typed library: the public API is determined # based on the documentation. __all__ = [ + "NA", "ArrowDtype", "BooleanDtype", "Categorical", @@ -253,15 +254,14 @@ "HDFStore", "Index", "IndexSlice", + "Int8Dtype", "Int16Dtype", "Int32Dtype", "Int64Dtype", - "Int8Dtype", "Interval", "IntervalDtype", "IntervalIndex", "MultiIndex", - "NA", "NaT", "NamedAgg", "Period", @@ -274,10 +274,10 @@ "Timedelta", "TimedeltaIndex", "Timestamp", + "UInt8Dtype", "UInt16Dtype", "UInt32Dtype", "UInt64Dtype", - "UInt8Dtype", "api", "array", "arrays", @@ -290,8 +290,8 @@ "errors", "eval", "factorize", - "get_dummies", "from_dummies", + "get_dummies", "get_option", "infer_freq", "interval_range", diff --git a/pandas/_config/__init__.py b/pandas/_config/__init__.py index 80d9ea1b364f3..463e8af7cc561 100644 --- a/pandas/_config/__init__.py +++ b/pandas/_config/__init__.py @@ -8,13 +8,13 @@ __all__ = [ "config", + "describe_option", "detect_console_encoding", "get_option", - "set_option", - "reset_option", - "describe_option", "option_context", "options", + "reset_option", + "set_option", ] from pandas._config import config from pandas._config import dates # pyright: ignore[reportUnusedImport] # noqa: F401 diff --git a/pandas/_libs/__init__.py b/pandas/_libs/__init__.py index 26a872a90e493..d499f9a6cd75e 100644 --- a/pandas/_libs/__init__.py +++ b/pandas/_libs/__init__.py @@ -1,4 +1,5 @@ __all__ = [ + "Interval", "NaT", "NaTType", "OutOfBoundsDatetime", @@ -6,7 +7,6 @@ "Timedelta", "Timestamp", "iNaT", - "Interval", ] diff --git a/pandas/_libs/tslibs/__init__.py b/pandas/_libs/tslibs/__init__.py index 31979b293a940..f433a3acf356f 100644 --- a/pandas/_libs/tslibs/__init__.py +++ b/pandas/_libs/tslibs/__init__.py @@ -1,39 +1,39 @@ __all__ = [ - "dtypes", - "localize_pydatetime", + "BaseOffset", + "IncompatibleFrequency", "NaT", "NaTType", - "iNaT", - "nat_strings", "OutOfBoundsDatetime", "OutOfBoundsTimedelta", - "IncompatibleFrequency", "Period", "Resolution", + "Tick", "Timedelta", - "normalize_i8_timestamps", - "is_date_array_normalized", - "dt64arr_to_periodarr", + "Timestamp", + "add_overflowsafe", + "astype_overflowsafe", "delta_to_nanoseconds", + "dt64arr_to_periodarr", + "dtypes", + "get_resolution", + "get_supported_dtype", + "get_unit_from_dtype", + "guess_datetime_format", + "iNaT", "ints_to_pydatetime", "ints_to_pytimedelta", - "get_resolution", - "Timestamp", - "tz_convert_from_utc_single", - "tz_convert_from_utc", - "to_offset", - "Tick", - "BaseOffset", - "tz_compare", + "is_date_array_normalized", + "is_supported_dtype", "is_unitless", - "astype_overflowsafe", - "get_unit_from_dtype", + "localize_pydatetime", + "nat_strings", + "normalize_i8_timestamps", "periods_per_day", "periods_per_second", - "guess_datetime_format", - "add_overflowsafe", - "get_supported_dtype", - "is_supported_dtype", + "to_offset", + "tz_compare", + "tz_convert_from_utc", + "tz_convert_from_utc_single", ] from pandas._libs.tslibs import dtypes diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index e092d65f08dd4..ec9b5098c97c9 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -540,6 +540,25 @@ def shares_memory(left, right) -> bool: "ALL_INT_NUMPY_DTYPES", "ALL_NUMPY_DTYPES", "ALL_REAL_NUMPY_DTYPES", + "BOOL_DTYPES", + "BYTES_DTYPES", + "COMPLEX_DTYPES", + "DATETIME64_DTYPES", + "ENDIAN", + "FLOAT_EA_DTYPES", + "FLOAT_NUMPY_DTYPES", + "NARROW_NP_DTYPES", + "NP_NAT_OBJECTS", + "NULL_OBJECTS", + "OBJECT_DTYPES", + "SIGNED_INT_EA_DTYPES", + "SIGNED_INT_NUMPY_DTYPES", + "STRING_DTYPES", + "TIMEDELTA64_DTYPES", + "UNSIGNED_INT_EA_DTYPES", + "UNSIGNED_INT_NUMPY_DTYPES", + "SubclassedDataFrame", + "SubclassedSeries", "assert_almost_equal", "assert_attr_equal", "assert_categorical_equal", @@ -563,51 +582,32 @@ def shares_memory(left, right) -> bool: "assert_sp_array_equal", "assert_timedelta_array_equal", "at", - "BOOL_DTYPES", "box_expected", - "BYTES_DTYPES", "can_set_locale", - "COMPLEX_DTYPES", "convert_rows_list_to_csv_str", - "DATETIME64_DTYPES", "decompress_file", - "ENDIAN", "ensure_clean", "external_error_raised", - "FLOAT_EA_DTYPES", - "FLOAT_NUMPY_DTYPES", "get_cython_table_params", "get_dtype", - "getitem", - "get_locales", "get_finest_unit", + "get_locales", "get_obj", "get_op_from_name", + "getitem", "iat", "iloc", "loc", "maybe_produces_warning", - "NARROW_NP_DTYPES", - "NP_NAT_OBJECTS", - "NULL_OBJECTS", - "OBJECT_DTYPES", "raise_assert_detail", "raises_chained_assignment_error", "round_trip_pathlib", "round_trip_pickle", - "setitem", "set_locale", "set_timezone", + "setitem", "shares_memory", - "SIGNED_INT_EA_DTYPES", - "SIGNED_INT_NUMPY_DTYPES", - "STRING_DTYPES", - "SubclassedDataFrame", - "SubclassedSeries", - "TIMEDELTA64_DTYPES", "to_array", - "UNSIGNED_INT_EA_DTYPES", - "UNSIGNED_INT_NUMPY_DTYPES", "with_csv_dialect", "write_to_compressed", ] diff --git a/pandas/_testing/asserters.py b/pandas/_testing/asserters.py index 01c4dcd92ee40..daa5187cdb636 100644 --- a/pandas/_testing/asserters.py +++ b/pandas/_testing/asserters.py @@ -755,11 +755,8 @@ def assert_extension_array_equal( and atol is lib.no_default ): check_exact = ( - is_numeric_dtype(left.dtype) - and not is_float_dtype(left.dtype) - or is_numeric_dtype(right.dtype) - and not is_float_dtype(right.dtype) - ) + is_numeric_dtype(left.dtype) and not is_float_dtype(left.dtype) + ) or (is_numeric_dtype(right.dtype) and not is_float_dtype(right.dtype)) elif check_exact is lib.no_default: check_exact = False @@ -944,11 +941,8 @@ def assert_series_equal( and atol is lib.no_default ): check_exact = ( - is_numeric_dtype(left.dtype) - and not is_float_dtype(left.dtype) - or is_numeric_dtype(right.dtype) - and not is_float_dtype(right.dtype) - ) + is_numeric_dtype(left.dtype) and not is_float_dtype(left.dtype) + ) or (is_numeric_dtype(right.dtype) and not is_float_dtype(right.dtype)) left_index_dtypes = ( [left.index.dtype] if left.index.nlevels == 1 else left.index.dtypes ) diff --git a/pandas/_typing.py b/pandas/_typing.py index c1769126a5776..b515305fb6903 100644 --- a/pandas/_typing.py +++ b/pandas/_typing.py @@ -273,7 +273,7 @@ def mode(self) -> str: # for _get_filepath_or_buffer ... - def seek(self, __offset: int, __whence: int = ...) -> int: + def seek(self, offset: int, whence: int = ..., /) -> int: # with one argument: gzip.GzipFile, bz2.BZ2File # with two arguments: zip.ZipFile, read_sas ... @@ -288,13 +288,13 @@ def tell(self) -> int: class ReadBuffer(BaseBuffer, Protocol[AnyStr_co]): - def read(self, __n: int = ...) -> AnyStr_co: + def read(self, n: int = ..., /) -> AnyStr_co: # for BytesIOWrapper, gzip.GzipFile, bz2.BZ2File ... class WriteBuffer(BaseBuffer, Protocol[AnyStr_contra]): - def write(self, __b: AnyStr_contra) -> Any: + def write(self, b: AnyStr_contra, /) -> Any: # for gzip.GzipFile, bz2.BZ2File ... diff --git a/pandas/api/__init__.py b/pandas/api/__init__.py index 9b007e8fe8da4..8f659e3cd14c8 100644 --- a/pandas/api/__init__.py +++ b/pandas/api/__init__.py @@ -9,9 +9,9 @@ ) __all__ = [ - "interchange", "extensions", "indexers", + "interchange", "types", "typing", ] diff --git a/pandas/api/extensions/__init__.py b/pandas/api/extensions/__init__.py index ea5f1ba926899..1c88c0d35b4d7 100644 --- a/pandas/api/extensions/__init__.py +++ b/pandas/api/extensions/__init__.py @@ -21,13 +21,13 @@ ) __all__ = [ - "no_default", + "ExtensionArray", "ExtensionDtype", - "register_extension_dtype", + "ExtensionScalarOpsMixin", + "no_default", "register_dataframe_accessor", + "register_extension_dtype", "register_index_accessor", "register_series_accessor", "take", - "ExtensionArray", - "ExtensionScalarOpsMixin", ] diff --git a/pandas/api/indexers/__init__.py b/pandas/api/indexers/__init__.py index 78357f11dc3b7..f3c6546218de4 100644 --- a/pandas/api/indexers/__init__.py +++ b/pandas/api/indexers/__init__.py @@ -10,8 +10,8 @@ ) __all__ = [ - "check_array_indexer", "BaseIndexer", "FixedForwardWindowIndexer", "VariableOffsetWindowIndexer", + "check_array_indexer", ] diff --git a/pandas/api/interchange/__init__.py b/pandas/api/interchange/__init__.py index 2f3a73bc46b31..aded37abc7224 100644 --- a/pandas/api/interchange/__init__.py +++ b/pandas/api/interchange/__init__.py @@ -5,4 +5,4 @@ from pandas.core.interchange.dataframe_protocol import DataFrame from pandas.core.interchange.from_dataframe import from_dataframe -__all__ = ["from_dataframe", "DataFrame"] +__all__ = ["DataFrame", "from_dataframe"] diff --git a/pandas/api/types/__init__.py b/pandas/api/types/__init__.py index c601086bb9f86..4a5c742b1628b 100644 --- a/pandas/api/types/__init__.py +++ b/pandas/api/types/__init__.py @@ -14,10 +14,10 @@ ) __all__ = [ - "infer_dtype", - "union_categoricals", "CategoricalDtype", "DatetimeTZDtype", "IntervalDtype", "PeriodDtype", + "infer_dtype", + "union_categoricals", ] diff --git a/pandas/api/typing/__init__.py b/pandas/api/typing/__init__.py index c58fa0f085266..a18a1e9d5cbb7 100644 --- a/pandas/api/typing/__init__.py +++ b/pandas/api/typing/__init__.py @@ -42,18 +42,16 @@ "ExponentialMovingWindowGroupby", "FrozenList", "JsonReader", - "NaTType", "NAType", + "NaTType", "PeriodIndexResamplerGroupby", "Resampler", "Rolling", "RollingGroupby", + "SASReader", "SeriesGroupBy", "StataReader", - "SASReader", - # See TODO above - # "Styler", - "TimedeltaIndexResamplerGroupby", "TimeGrouper", + "TimedeltaIndexResamplerGroupby", "Window", ] diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py index 756c209661fbb..e7674386408f7 100644 --- a/pandas/compat/__init__.py +++ b/pandas/compat/__init__.py @@ -150,6 +150,13 @@ def is_ci_environment() -> bool: __all__ = [ + "HAS_PYARROW", + "IS64", + "ISMUSL", + "PY311", + "PY312", + "PYPY", + "WASM", "is_numpy_dev", "pa_version_under10p1", "pa_version_under11p0", @@ -159,11 +166,4 @@ def is_ci_environment() -> bool: "pa_version_under16p0", "pa_version_under17p0", "pa_version_under18p0", - "HAS_PYARROW", - "IS64", - "ISMUSL", - "PY311", - "PY312", - "PYPY", - "WASM", ] diff --git a/pandas/compat/numpy/__init__.py b/pandas/compat/numpy/__init__.py index 2fab8f32b8e71..3306b36d71806 100644 --- a/pandas/compat/numpy/__init__.py +++ b/pandas/compat/numpy/__init__.py @@ -47,7 +47,7 @@ __all__ = [ - "np", "_np_version", "is_numpy_dev", + "np", ] diff --git a/pandas/core/_numba/kernels/__init__.py b/pandas/core/_numba/kernels/__init__.py index 1116c61c4ca8e..6983711480455 100644 --- a/pandas/core/_numba/kernels/__init__.py +++ b/pandas/core/_numba/kernels/__init__.py @@ -16,12 +16,12 @@ ) __all__ = [ - "sliding_mean", "grouped_mean", - "sliding_sum", + "grouped_min_max", "grouped_sum", - "sliding_var", "grouped_var", + "sliding_mean", "sliding_min_max", - "grouped_min_max", + "sliding_sum", + "sliding_var", ] diff --git a/pandas/core/api.py b/pandas/core/api.py index c8a4e9d8a23b2..ec12d543d8389 100644 --- a/pandas/core/api.py +++ b/pandas/core/api.py @@ -80,59 +80,59 @@ from pandas.core.frame import DataFrame # isort:skip __all__ = [ - "array", + "NA", "ArrowDtype", - "bdate_range", "BooleanDtype", "Categorical", "CategoricalDtype", "CategoricalIndex", "DataFrame", "DateOffset", - "date_range", "DatetimeIndex", "DatetimeTZDtype", - "factorize", "Flags", "Float32Dtype", "Float64Dtype", "Grouper", "Index", "IndexSlice", + "Int8Dtype", "Int16Dtype", "Int32Dtype", "Int64Dtype", - "Int8Dtype", "Interval", "IntervalDtype", "IntervalIndex", - "interval_range", - "isna", - "isnull", "MultiIndex", - "NA", - "NamedAgg", "NaT", - "notna", - "notnull", + "NamedAgg", "Period", "PeriodDtype", "PeriodIndex", - "period_range", "RangeIndex", "Series", - "set_eng_float_format", "StringDtype", "Timedelta", "TimedeltaIndex", - "timedelta_range", "Timestamp", - "to_datetime", - "to_numeric", - "to_timedelta", + "UInt8Dtype", "UInt16Dtype", "UInt32Dtype", "UInt64Dtype", - "UInt8Dtype", + "array", + "bdate_range", + "date_range", + "factorize", + "interval_range", + "isna", + "isnull", + "notna", + "notnull", + "period_range", + "set_eng_float_format", + "timedelta_range", + "to_datetime", + "to_numeric", + "to_timedelta", "unique", ] diff --git a/pandas/core/arrays/__init__.py b/pandas/core/arrays/__init__.py index 245a171fea74b..f183e9236471e 100644 --- a/pandas/core/arrays/__init__.py +++ b/pandas/core/arrays/__init__.py @@ -23,21 +23,21 @@ __all__ = [ "ArrowExtensionArray", - "ExtensionArray", - "ExtensionOpsMixin", - "ExtensionScalarOpsMixin", "ArrowStringArray", "BaseMaskedArray", "BooleanArray", "Categorical", "DatetimeArray", + "ExtensionArray", + "ExtensionOpsMixin", + "ExtensionScalarOpsMixin", "FloatingArray", "IntegerArray", "IntervalArray", "NumpyExtensionArray", "PeriodArray", - "period_array", "SparseArray", "StringArray", "TimedeltaArray", + "period_array", ] diff --git a/pandas/core/arrays/arrow/__init__.py b/pandas/core/arrays/arrow/__init__.py index 5fc50f786fc6a..50274a2de2cc1 100644 --- a/pandas/core/arrays/arrow/__init__.py +++ b/pandas/core/arrays/arrow/__init__.py @@ -4,4 +4,4 @@ ) from pandas.core.arrays.arrow.array import ArrowExtensionArray -__all__ = ["ArrowExtensionArray", "StructAccessor", "ListAccessor"] +__all__ = ["ArrowExtensionArray", "ListAccessor", "StructAccessor"] diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index e0c93db0afb07..afa219f611992 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1446,8 +1446,7 @@ def to_numpy( pa.types.is_floating(pa_type) and ( na_value is np.nan - or original_na_value is lib.no_default - and is_float_dtype(dtype) + or (original_na_value is lib.no_default and is_float_dtype(dtype)) ) ): result = data._pa_array.to_numpy() diff --git a/pandas/core/arrays/sparse/__init__.py b/pandas/core/arrays/sparse/__init__.py index adf83963aca39..93d5cb8cc335a 100644 --- a/pandas/core/arrays/sparse/__init__.py +++ b/pandas/core/arrays/sparse/__init__.py @@ -12,8 +12,8 @@ __all__ = [ "BlockIndex", "IntIndex", - "make_sparse_index", "SparseAccessor", "SparseArray", "SparseFrameAccessor", + "make_sparse_index", ] diff --git a/pandas/core/computation/eval.py b/pandas/core/computation/eval.py index 4ccfbd71d9ce8..86f83489e71ae 100644 --- a/pandas/core/computation/eval.py +++ b/pandas/core/computation/eval.py @@ -371,10 +371,12 @@ def eval( is_extension_array_dtype(parsed_expr.terms.return_type) and not is_string_dtype(parsed_expr.terms.return_type) ) - or getattr(parsed_expr.terms, "operand_types", None) is not None - and any( - (is_extension_array_dtype(elem) and not is_string_dtype(elem)) - for elem in parsed_expr.terms.operand_types + or ( + getattr(parsed_expr.terms, "operand_types", None) is not None + and any( + (is_extension_array_dtype(elem) and not is_string_dtype(elem)) + for elem in parsed_expr.terms.operand_types + ) ) ): warnings.warn( diff --git a/pandas/core/computation/expr.py b/pandas/core/computation/expr.py index 7025d8a72e561..010fad1bbf0b6 100644 --- a/pandas/core/computation/expr.py +++ b/pandas/core/computation/expr.py @@ -512,8 +512,7 @@ def _maybe_evaluate_binop( ) if self.engine != "pytables" and ( - res.op in CMP_OPS_SYMS - and getattr(lhs, "is_datetime", False) + (res.op in CMP_OPS_SYMS and getattr(lhs, "is_datetime", False)) or getattr(rhs, "is_datetime", False) ): # all date ops must be done in python bc numexpr doesn't work diff --git a/pandas/core/computation/pytables.py b/pandas/core/computation/pytables.py index 39511048abf49..fe7e27f537b01 100644 --- a/pandas/core/computation/pytables.py +++ b/pandas/core/computation/pytables.py @@ -408,11 +408,12 @@ def prune(self, klass): operand = operand.prune(klass) if operand is not None and ( - issubclass(klass, ConditionBinOp) - and operand.condition is not None - or not issubclass(klass, ConditionBinOp) - and issubclass(klass, FilterBinOp) - and operand.filter is not None + (issubclass(klass, ConditionBinOp) and operand.condition is not None) + or ( + not issubclass(klass, ConditionBinOp) + and issubclass(klass, FilterBinOp) + and operand.filter is not None + ) ): return operand.invert() return None diff --git a/pandas/core/computation/scope.py b/pandas/core/computation/scope.py index 7b31e03e58b4b..336d62b9d9579 100644 --- a/pandas/core/computation/scope.py +++ b/pandas/core/computation/scope.py @@ -140,7 +140,7 @@ class Scope: temps : dict """ - __slots__ = ["level", "scope", "target", "resolvers", "temps"] + __slots__ = ["level", "resolvers", "scope", "target", "temps"] level: int scope: DeepChainMap resolvers: DeepChainMap diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 8f93b1a397c1f..6fa21d9410187 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1889,13 +1889,14 @@ def is_all_strings(value: ArrayLike) -> bool: __all__ = [ - "classes", "DT64NS_DTYPE", + "INT64_DTYPE", + "TD64NS_DTYPE", + "classes", "ensure_float64", "ensure_python_int", "ensure_str", "infer_dtype_from_object", - "INT64_DTYPE", "is_1d_only_ea_dtype", "is_all_strings", "is_any_real_numeric_dtype", @@ -1940,6 +1941,5 @@ def is_all_strings(value: ArrayLike) -> bool: "is_unsigned_integer_dtype", "needs_i8_conversion", "pandas_dtype", - "TD64NS_DTYPE", "validate_all_hashable", ] diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index e5d1033de4457..1dd1b12d6ae95 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -73,7 +73,7 @@ from collections.abc import MutableMapping from datetime import tzinfo - import pyarrow as pa # noqa: TCH004 + import pyarrow as pa # noqa: TC004 from pandas._typing import ( Dtype, @@ -1115,10 +1115,8 @@ def construct_from_string(cls, string: str_type) -> PeriodDtype: possible """ if ( - isinstance(string, str) - and (string.startswith(("period[", "Period["))) - or isinstance(string, BaseOffset) - ): + isinstance(string, str) and (string.startswith(("period[", "Period["))) + ) or isinstance(string, BaseOffset): # do not parse string like U as period[U] # avoid tuple to be regarded as freq try: diff --git a/pandas/core/frame.py b/pandas/core/frame.py index d1450537dd740..33a419925f70c 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -3929,8 +3929,7 @@ def __getitem__(self, key): # GH#45316 Return view if key is not duplicated # Only use drop_duplicates with duplicates for performance if not is_mi and ( - self.columns.is_unique - and key in self.columns + (self.columns.is_unique and key in self.columns) or key in self.columns.drop_duplicates(keep=False) ): return self._get_item(key) @@ -6776,8 +6775,7 @@ def f(vals) -> tuple[np.ndarray, int]: elif ( not np.iterable(subset) or isinstance(subset, str) - or isinstance(subset, tuple) - and subset in self.columns + or (isinstance(subset, tuple) and subset in self.columns) ): subset = (subset,) diff --git a/pandas/core/groupby/__init__.py b/pandas/core/groupby/__init__.py index 8248f378e2c1a..ec477626a098f 100644 --- a/pandas/core/groupby/__init__.py +++ b/pandas/core/groupby/__init__.py @@ -8,8 +8,8 @@ __all__ = [ "DataFrameGroupBy", - "NamedAgg", - "SeriesGroupBy", "GroupBy", "Grouper", + "NamedAgg", + "SeriesGroupBy", ] diff --git a/pandas/core/indexers/__init__.py b/pandas/core/indexers/__init__.py index ba8a4f1d0ee7a..036b32b3feac2 100644 --- a/pandas/core/indexers/__init__.py +++ b/pandas/core/indexers/__init__.py @@ -15,17 +15,17 @@ ) __all__ = [ - "is_valid_positional_slice", + "check_array_indexer", + "check_key_length", + "check_setitem_lengths", + "disallow_ndim_indexing", + "is_empty_indexer", "is_list_like_indexer", "is_scalar_indexer", - "is_empty_indexer", - "check_setitem_lengths", - "validate_indices", - "maybe_convert_indices", + "is_valid_positional_slice", "length_of_indexer", - "disallow_ndim_indexing", + "maybe_convert_indices", "unpack_1tuple", - "check_key_length", - "check_array_indexer", "unpack_tuple_and_ellipses", + "validate_indices", ] diff --git a/pandas/core/indexes/api.py b/pandas/core/indexes/api.py index 5144e647e73b4..058e584336905 100644 --- a/pandas/core/indexes/api.py +++ b/pandas/core/indexes/api.py @@ -37,26 +37,26 @@ __all__ = [ - "Index", - "MultiIndex", "CategoricalIndex", + "DatetimeIndex", + "Index", "IntervalIndex", - "RangeIndex", "InvalidIndexError", - "TimedeltaIndex", + "MultiIndex", + "NaT", "PeriodIndex", - "DatetimeIndex", + "RangeIndex", + "TimedeltaIndex", "_new_Index", - "NaT", + "all_indexes_same", + "default_index", "ensure_index", "ensure_index_from_sequences", "get_objs_combined_axis", - "union_indexes", "get_unanimous_names", - "all_indexes_same", - "default_index", - "safe_sort_index", "maybe_sequence_to_range", + "safe_sort_index", + "union_indexes", ] diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index 7eeaab3b0443f..935762d0455c5 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -1195,7 +1195,7 @@ def _getitem_slice(self, slobj: slice) -> Self: @unpack_zerodim_and_defer("__floordiv__") def __floordiv__(self, other): if is_integer(other) and other != 0: - if len(self) == 0 or self.start % other == 0 and self.step % other == 0: + if len(self) == 0 or (self.start % other == 0 and self.step % other == 0): start = self.start // other step = self.step // other stop = start + len(self) * step diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 0d6d7e68f58a4..e0bc0a23acd9f 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1239,8 +1239,10 @@ def _validate_key(self, key, axis: Axis) -> None: if isinstance(key, bool) and not ( is_bool_dtype(ax.dtype) or ax.dtype.name == "boolean" - or isinstance(ax, MultiIndex) - and is_bool_dtype(ax.get_level_values(0).dtype) + or ( + isinstance(ax, MultiIndex) + and is_bool_dtype(ax.get_level_values(0).dtype) + ) ): raise KeyError( f"{key}: boolean label can not be used without a boolean index" @@ -2120,7 +2122,7 @@ def _setitem_single_column(self, loc: int, value, plane_indexer) -> None: is_full_setter = com.is_null_slice(pi) or com.is_full_slice(pi, len(self.obj)) - is_null_setter = com.is_empty_slice(pi) or is_array_like(pi) and len(pi) == 0 + is_null_setter = com.is_empty_slice(pi) or (is_array_like(pi) and len(pi) == 0) if is_null_setter: # no-op, don't cast dtype later @@ -2744,19 +2746,15 @@ def check_dict_or_set_indexers(key) -> None: """ Check if the indexer is or contains a dict or set, which is no longer allowed. """ - if ( - isinstance(key, set) - or isinstance(key, tuple) - and any(isinstance(x, set) for x in key) + if isinstance(key, set) or ( + isinstance(key, tuple) and any(isinstance(x, set) for x in key) ): raise TypeError( "Passing a set as an indexer is not supported. Use a list instead." ) - if ( - isinstance(key, dict) - or isinstance(key, tuple) - and any(isinstance(x, dict) for x in key) + if isinstance(key, dict) or ( + isinstance(key, tuple) and any(isinstance(x, dict) for x in key) ): raise TypeError( "Passing a dict as an indexer is not supported. Use a list instead." diff --git a/pandas/core/internals/__init__.py b/pandas/core/internals/__init__.py index 5ab70ba38f9c2..202bebde88c2c 100644 --- a/pandas/core/internals/__init__.py +++ b/pandas/core/internals/__init__.py @@ -7,11 +7,11 @@ __all__ = [ "Block", - "ExtensionBlock", - "make_block", "BlockManager", + "ExtensionBlock", "SingleBlockManager", "concatenate_managers", + "make_block", ] diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 54273ff89f1af..f44ad926dda5c 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -514,9 +514,8 @@ def convert(self) -> list[Block]: convert_non_numeric=True, ) refs = None - if ( - res_values is values - or isinstance(res_values, NumpyExtensionArray) + if res_values is values or ( + isinstance(res_values, NumpyExtensionArray) and res_values._ndarray is values ): refs = self.refs diff --git a/pandas/core/internals/construction.py b/pandas/core/internals/construction.py index f357a53a10be8..dfff34656f82b 100644 --- a/pandas/core/internals/construction.py +++ b/pandas/core/internals/construction.py @@ -417,8 +417,7 @@ def dict_to_mgr( else x.copy(deep=True) if ( isinstance(x, Index) - or isinstance(x, ABCSeries) - and is_1d_only_ea_dtype(x.dtype) + or (isinstance(x, ABCSeries) and is_1d_only_ea_dtype(x.dtype)) ) else x for x in arrays diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 34a0bb1f45e2c..9f9d69a182f72 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -66,15 +66,18 @@ __all__ = [ "ARITHMETIC_BINOPS", "arithmetic_op", - "comparison_op", "comp_method_OBJECT_ARRAY", - "invalid_comparison", + "comparison_op", "fill_binop", + "get_array_op", + "get_op_result_name", + "invalid_comparison", "kleene_and", "kleene_or", "kleene_xor", "logical_op", "make_flex_doc", + "maybe_prepare_scalar_for_op", "radd", "rand_", "rdiv", @@ -88,7 +91,4 @@ "rtruediv", "rxor", "unpack_zerodim_and_defer", - "get_op_result_name", - "maybe_prepare_scalar_for_op", - "get_array_op", ] diff --git a/pandas/core/resample.py b/pandas/core/resample.py index ca4d3fc768efb..fdfb9f21bdb9f 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -2002,9 +2002,7 @@ def __init__( raise ValueError(f"Unsupported value {convention} for `convention`") if ( - key is None - and obj is not None - and isinstance(obj.index, PeriodIndex) # type: ignore[attr-defined] + (key is None and obj is not None and isinstance(obj.index, PeriodIndex)) # type: ignore[attr-defined] or ( key is not None and obj is not None diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py index 6f9bb8cb24f43..5fddd9f9aca5b 100644 --- a/pandas/core/reshape/merge.py +++ b/pandas/core/reshape/merge.py @@ -2746,8 +2746,7 @@ def _factorize_keys( isinstance(lk.dtype, ArrowDtype) and ( is_numeric_dtype(lk.dtype.numpy_dtype) - or is_string_dtype(lk.dtype) - and not sort + or (is_string_dtype(lk.dtype) and not sort) ) ): lk, _ = lk._values_for_factorize() diff --git a/pandas/core/tools/numeric.py b/pandas/core/tools/numeric.py index f159babb7e018..bc45343d6e2d3 100644 --- a/pandas/core/tools/numeric.py +++ b/pandas/core/tools/numeric.py @@ -226,19 +226,18 @@ def to_numeric( set(), coerce_numeric=coerce_numeric, convert_to_masked_nullable=dtype_backend is not lib.no_default - or isinstance(values_dtype, StringDtype) - and values_dtype.na_value is libmissing.NA, + or ( + isinstance(values_dtype, StringDtype) + and values_dtype.na_value is libmissing.NA + ), ) if new_mask is not None: # Remove unnecessary values, is expected later anyway and enables # downcasting values = values[~new_mask] - elif ( - dtype_backend is not lib.no_default - and new_mask is None - or isinstance(values_dtype, StringDtype) - and values_dtype.na_value is libmissing.NA + elif (dtype_backend is not lib.no_default and new_mask is None) or ( + isinstance(values_dtype, StringDtype) and values_dtype.na_value is libmissing.NA ): new_mask = np.zeros(values.shape, dtype=np.bool_) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index b1a338893fe0a..1de6f06ef316c 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -865,28 +865,28 @@ class InvalidComparison(Exception): __all__ = [ "AbstractMethodError", "AttributeConflictWarning", + "CSSWarning", "CategoricalConversionWarning", "ChainedAssignmentError", "ClosedFileError", - "CSSWarning", - "DatabaseError", "DataError", + "DatabaseError", "DtypeWarning", "DuplicateLabelError", "EmptyDataError", "IncompatibilityWarning", + "IndexingError", "IntCastingNaNError", "InvalidColumnName", "InvalidComparison", "InvalidIndexError", "InvalidVersion", - "IndexingError", "LossySetitemError", "MergeError", "NoBufferPresent", "NullFrequencyError", - "NumbaUtilError", "NumExprClobberingError", + "NumbaUtilError", "OptionError", "OutOfBoundsDatetime", "OutOfBoundsTimedelta", diff --git a/pandas/io/__init__.py b/pandas/io/__init__.py index c804b81c49e7c..1c7e531debb14 100644 --- a/pandas/io/__init__.py +++ b/pandas/io/__init__.py @@ -1,4 +1,4 @@ -# ruff: noqa: TCH004 +# ruff: noqa: TC004 from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/pandas/io/excel/__init__.py b/pandas/io/excel/__init__.py index 275cbf0148f94..f13d7afa63d84 100644 --- a/pandas/io/excel/__init__.py +++ b/pandas/io/excel/__init__.py @@ -8,7 +8,7 @@ from pandas.io.excel._util import register_writer from pandas.io.excel._xlsxwriter import XlsxWriter as _XlsxWriter -__all__ = ["read_excel", "ExcelWriter", "ExcelFile"] +__all__ = ["ExcelFile", "ExcelWriter", "read_excel"] register_writer(_OpenpyxlWriter) diff --git a/pandas/io/formats/__init__.py b/pandas/io/formats/__init__.py index 5e56b1bc7ba43..895669c342f97 100644 --- a/pandas/io/formats/__init__.py +++ b/pandas/io/formats/__init__.py @@ -1,4 +1,4 @@ -# ruff: noqa: TCH004 +# ruff: noqa: TC004 from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/pandas/io/json/__init__.py b/pandas/io/json/__init__.py index 8f4e7a62834b5..39f78e26d6041 100644 --- a/pandas/io/json/__init__.py +++ b/pandas/io/json/__init__.py @@ -7,9 +7,9 @@ from pandas.io.json._table_schema import build_table_schema __all__ = [ - "ujson_dumps", - "ujson_loads", + "build_table_schema", "read_json", "to_json", - "build_table_schema", + "ujson_dumps", + "ujson_loads", ] diff --git a/pandas/io/json/_json.py b/pandas/io/json/_json.py index 983780f81043f..237518b3c8d92 100644 --- a/pandas/io/json/_json.py +++ b/pandas/io/json/_json.py @@ -364,10 +364,8 @@ def __init__( ) # TODO: Do this timedelta properly in objToJSON.c See GH #15137 - if ( - (obj.ndim == 1) - and (obj.name in set(obj.index.names)) - or len(obj.columns.intersection(obj.index.names)) + if ((obj.ndim == 1) and (obj.name in set(obj.index.names))) or len( + obj.columns.intersection(obj.index.names) ): msg = "Overlapping names between the index and columns" raise ValueError(msg) diff --git a/pandas/io/parsers/base_parser.py b/pandas/io/parsers/base_parser.py index 7294efe843cce..e263c69376d05 100644 --- a/pandas/io/parsers/base_parser.py +++ b/pandas/io/parsers/base_parser.py @@ -368,7 +368,7 @@ def _agg_index(self, index) -> Index: index_converter = converters.get(self.index_names[i]) is not None try_num_bool = not ( - cast_type and is_string_dtype(cast_type) or index_converter + (cast_type and is_string_dtype(cast_type)) or index_converter ) arr, _ = self._infer_types( diff --git a/pandas/io/parsers/python_parser.py b/pandas/io/parsers/python_parser.py index 99d584db61755..db9547a18b600 100644 --- a/pandas/io/parsers/python_parser.py +++ b/pandas/io/parsers/python_parser.py @@ -1052,8 +1052,9 @@ def _remove_empty_lines(self, lines: list[list[T]]) -> list[list[T]]: for line in lines if ( len(line) > 1 - or len(line) == 1 - and (not isinstance(line[0], str) or line[0].strip()) + or ( + len(line) == 1 and (not isinstance(line[0], str) or line[0].strip()) + ) ) ] return ret diff --git a/pandas/io/stata.py b/pandas/io/stata.py index 63f729c8347b1..053e331925b6f 100644 --- a/pandas/io/stata.py +++ b/pandas/io/stata.py @@ -2206,15 +2206,15 @@ def _convert_datetime_to_stata_type(fmt: str) -> np.dtype: def _maybe_convert_to_int_keys(convert_dates: dict, varlist: list[Hashable]) -> dict: new_dict = {} - for key in convert_dates: - if not convert_dates[key].startswith("%"): # make sure proper fmts - convert_dates[key] = "%" + convert_dates[key] + for key, value in convert_dates.items(): + if not value.startswith("%"): # make sure proper fmts + convert_dates[key] = "%" + value if key in varlist: - new_dict.update({varlist.index(key): convert_dates[key]}) + new_dict[varlist.index(key)] = value else: if not isinstance(key, int): raise ValueError("convert_dates key must be a column or an integer") - new_dict.update({key: convert_dates[key]}) + new_dict[key] = value return new_dict @@ -2879,7 +2879,7 @@ def _write_header( # ds_format - just use 114 self._write_bytes(struct.pack("b", 114)) # byteorder - self._write(byteorder == ">" and "\x01" or "\x02") + self._write((byteorder == ">" and "\x01") or "\x02") # filetype self._write("\x01") # unused @@ -3425,7 +3425,7 @@ def _write_header( # ds_format - 117 bio.write(self._tag(bytes(str(self._dta_version), "utf-8"), "release")) # byteorder - bio.write(self._tag(byteorder == ">" and "MSF" or "LSF", "byteorder")) + bio.write(self._tag((byteorder == ">" and "MSF") or "LSF", "byteorder")) # number of vars, 2 bytes in 117 and 118, 4 byte in 119 nvar_type = "H" if self._dta_version <= 118 else "I" bio.write(self._tag(struct.pack(byteorder + nvar_type, self.nvar), "K")) diff --git a/pandas/plotting/__init__.py b/pandas/plotting/__init__.py index c7a4c1eacfcae..837bfaf82ca27 100644 --- a/pandas/plotting/__init__.py +++ b/pandas/plotting/__init__.py @@ -80,20 +80,20 @@ __all__ = [ "PlotAccessor", + "andrews_curves", + "autocorrelation_plot", + "bootstrap_plot", "boxplot", "boxplot_frame", "boxplot_frame_groupby", + "deregister_matplotlib_converters", "hist_frame", "hist_series", - "scatter_matrix", - "radviz", - "andrews_curves", - "bootstrap_plot", - "parallel_coordinates", "lag_plot", - "autocorrelation_plot", - "table", + "parallel_coordinates", "plot_params", + "radviz", "register_matplotlib_converters", - "deregister_matplotlib_converters", + "scatter_matrix", + "table", ] diff --git a/pandas/plotting/_matplotlib/__init__.py b/pandas/plotting/_matplotlib/__init__.py index 87f3ca09ad346..ff28868aa0033 100644 --- a/pandas/plotting/_matplotlib/__init__.py +++ b/pandas/plotting/_matplotlib/__init__.py @@ -74,20 +74,20 @@ def plot(data, kind, **kwargs): __all__ = [ - "plot", - "hist_series", - "hist_frame", - "boxplot", - "boxplot_frame", - "boxplot_frame_groupby", - "table", "andrews_curves", "autocorrelation_plot", "bootstrap_plot", + "boxplot", + "boxplot_frame", + "boxplot_frame_groupby", + "deregister", + "hist_frame", + "hist_series", "lag_plot", "parallel_coordinates", + "plot", "radviz", - "scatter_matrix", "register", - "deregister", + "scatter_matrix", + "table", ] diff --git a/pandas/testing.py b/pandas/testing.py index 0445fa5b5efc0..433b22bf1107e 100644 --- a/pandas/testing.py +++ b/pandas/testing.py @@ -12,6 +12,6 @@ __all__ = [ "assert_extension_array_equal", "assert_frame_equal", - "assert_series_equal", "assert_index_equal", + "assert_series_equal", ] diff --git a/pandas/tests/extension/decimal/__init__.py b/pandas/tests/extension/decimal/__init__.py index 34727b43a7b0f..47b1c7c57a47a 100644 --- a/pandas/tests/extension/decimal/__init__.py +++ b/pandas/tests/extension/decimal/__init__.py @@ -5,4 +5,4 @@ to_decimal, ) -__all__ = ["DecimalArray", "DecimalDtype", "to_decimal", "make_data"] +__all__ = ["DecimalArray", "DecimalDtype", "make_data", "to_decimal"] diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index 9defb97394635..c6ac6368f2770 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -896,9 +896,7 @@ def _is_temporal_supported(self, opname, pa_dtype): ) ) and pa.types.is_duration(pa_dtype) - or opname in ("__sub__", "__rsub__") - and pa.types.is_temporal(pa_dtype) - ) + ) or (opname in ("__sub__", "__rsub__") and pa.types.is_temporal(pa_dtype)) def _get_expected_exception( self, op_name: str, obj, other diff --git a/pandas/tests/extension/test_string.py b/pandas/tests/extension/test_string.py index 27621193a9b8d..e19351b2ad058 100644 --- a/pandas/tests/extension/test_string.py +++ b/pandas/tests/extension/test_string.py @@ -187,9 +187,8 @@ def _get_expected_exception( return None def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool: - return ( - op_name in ["min", "max", "sum"] - or ser.dtype.na_value is np.nan # type: ignore[union-attr] + return op_name in ["min", "max", "sum"] or ( + ser.dtype.na_value is np.nan # type: ignore[union-attr] and op_name in ("any", "all") ) diff --git a/pandas/tests/frame/methods/test_nlargest.py b/pandas/tests/frame/methods/test_nlargest.py index 52e871cc795b4..c6e5304ae3cb4 100644 --- a/pandas/tests/frame/methods/test_nlargest.py +++ b/pandas/tests/frame/methods/test_nlargest.py @@ -159,7 +159,7 @@ def test_nlargest_n_duplicate_index(self, n, order, request): result = df.nlargest(n, order) expected = df.sort_values(order, ascending=False).head(n) if Version(np.__version__) >= Version("1.25") and ( - (order == ["a"] and n in (1, 2, 3, 4)) or (order == ["a", "b"]) and n == 5 + (order == ["a"] and n in (1, 2, 3, 4)) or ((order == ["a", "b"]) and n == 5) ): request.applymarker( pytest.mark.xfail( diff --git a/pandas/tests/test_nanops.py b/pandas/tests/test_nanops.py index ce41f1e76de79..e7ed8e855a762 100644 --- a/pandas/tests/test_nanops.py +++ b/pandas/tests/test_nanops.py @@ -537,11 +537,8 @@ def _argminmax_wrap(self, value, axis=None, func=None): nullnan = isna(nans) if res.ndim: res[nullnan] = -1 - elif ( - hasattr(nullnan, "all") - and nullnan.all() - or not hasattr(nullnan, "all") - and nullnan + elif (hasattr(nullnan, "all") and nullnan.all()) or ( + not hasattr(nullnan, "all") and nullnan ): res = -1 return res diff --git a/pandas/tseries/__init__.py b/pandas/tseries/__init__.py index e361726dc6f80..c00843ecac418 100644 --- a/pandas/tseries/__init__.py +++ b/pandas/tseries/__init__.py @@ -1,4 +1,4 @@ -# ruff: noqa: TCH004 +# ruff: noqa: TC004 from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/pandas/tseries/api.py b/pandas/tseries/api.py index ec2d7d2304839..5ea899f1610a7 100644 --- a/pandas/tseries/api.py +++ b/pandas/tseries/api.py @@ -7,4 +7,4 @@ from pandas.tseries import offsets from pandas.tseries.frequencies import infer_freq -__all__ = ["infer_freq", "offsets", "guess_datetime_format"] +__all__ = ["guess_datetime_format", "infer_freq", "offsets"] diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py index bf4ec2e551f01..2d195fbbc4e84 100644 --- a/pandas/tseries/holiday.py +++ b/pandas/tseries/holiday.py @@ -636,12 +636,17 @@ def HolidayCalendarFactory(name: str, base, other, base_class=AbstractHolidayCal __all__ = [ + "FR", + "MO", + "SA", + "SU", + "TH", + "TU", + "WE", + "HolidayCalendarFactory", "after_nearest_workday", "before_nearest_workday", - "FR", "get_calendar", - "HolidayCalendarFactory", - "MO", "nearest_workday", "next_monday", "next_monday_or_tuesday", @@ -649,11 +654,6 @@ def HolidayCalendarFactory(name: str, base, other, base_class=AbstractHolidayCal "previous_friday", "previous_workday", "register", - "SA", - "SU", "sunday_to_monday", - "TH", - "TU", - "WE", "weekend_to_monday", ] diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 169c9cc18a7fd..a065137e6971c 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -46,46 +46,46 @@ ) __all__ = [ - "Day", + "FY5253", + "BDay", + "BMonthBegin", + "BMonthEnd", + "BQuarterBegin", + "BQuarterEnd", + "BYearBegin", + "BYearEnd", "BaseOffset", "BusinessDay", + "BusinessHour", "BusinessMonthBegin", "BusinessMonthEnd", - "BDay", + "CBMonthBegin", + "CBMonthEnd", + "CDay", "CustomBusinessDay", + "CustomBusinessHour", "CustomBusinessMonthBegin", "CustomBusinessMonthEnd", - "CDay", - "CBMonthEnd", - "CBMonthBegin", + "DateOffset", + "Day", + "Easter", + "FY5253Quarter", + "Hour", + "LastWeekOfMonth", + "Micro", + "Milli", + "Minute", "MonthBegin", - "BMonthBegin", "MonthEnd", - "BMonthEnd", - "SemiMonthEnd", - "SemiMonthBegin", - "BusinessHour", - "CustomBusinessHour", - "YearBegin", - "BYearBegin", - "YearEnd", - "BYearEnd", + "Nano", "QuarterBegin", - "BQuarterBegin", "QuarterEnd", - "BQuarterEnd", - "LastWeekOfMonth", - "FY5253Quarter", - "FY5253", + "Second", + "SemiMonthBegin", + "SemiMonthEnd", + "Tick", "Week", "WeekOfMonth", - "Easter", - "Tick", - "Hour", - "Minute", - "Second", - "Milli", - "Micro", - "Nano", - "DateOffset", + "YearBegin", + "YearEnd", ] diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 165824bec131f..a1a0d51a7c72b 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -83,7 +83,7 @@ def wrapper(*args, **kwargs) -> Callable[..., Any]: if alternative.__doc__.count("\n") < 3: raise AssertionError(doc_error_msg) empty1, summary, empty2, doc_string = alternative.__doc__.split("\n", 3) - if empty1 or empty2 and not summary: + if empty1 or (empty2 and not summary): raise AssertionError(doc_error_msg) wrapper.__doc__ = dedent( f""" @@ -497,13 +497,13 @@ def indent(text: str | None, indents: int = 1) -> str: __all__ = [ "Appender", + "Substitution", "cache_readonly", "deprecate", "deprecate_kwarg", "deprecate_nonkeyword_arguments", "doc", "future_version_msg", - "Substitution", ] diff --git a/pyproject.toml b/pyproject.toml index 0c76ecd0b15b4..7ab9cd2c17669 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -304,10 +304,6 @@ ignore = [ "PERF102", # try-except-in-loop, becomes useless in Python 3.11 "PERF203", - # pytest-missing-fixture-name-underscore - "PT004", - # pytest-incorrect-fixture-name-underscore - "PT005", # pytest-parametrize-names-wrong-type "PT006", # pytest-parametrize-values-wrong-type diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index 076acc359f933..d804e15f6d48f 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -319,10 +319,10 @@ def nodefault_used_not_only_for_typing(file_obj: IO[str]) -> Iterable[tuple[int, while nodes: in_annotation, node = nodes.pop() if not in_annotation and ( - isinstance(node, ast.Name) # Case `NoDefault` - and node.id == "NoDefault" - or isinstance(node, ast.Attribute) # Cases e.g. `lib.NoDefault` - and node.attr == "NoDefault" + (isinstance(node, ast.Name) # Case `NoDefault` + and node.id == "NoDefault") + or (isinstance(node, ast.Attribute) # Cases e.g. `lib.NoDefault` + and node.attr == "NoDefault") ): yield (node.lineno, "NoDefault is used not only for typing") From a6f721efc14a88cfd6422f63e1ac06ad643e8fbc Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 2 Dec 2024 16:14:57 -0800 Subject: [PATCH 125/266] BUG: Fix keyerror bug when indexing multiindex columns with NaT values (#60463) * BUG: Fix keyerror bug when indexing multiindex columns with NaT values * BUG: Update whatsnew/v3.0.0.rst * BUG: Move new test to test_multilevel.py --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/indexes/multi.py | 7 +++---- pandas/tests/test_multilevel.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index e74bd2f745b94..e73ee0dfbe67e 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -667,6 +667,7 @@ Indexing ^^^^^^^^ - Bug in :meth:`DataFrame.__getitem__` returning modified columns when called with ``slice`` in Python 3.12 (:issue:`57500`) - Bug in :meth:`DataFrame.from_records` throwing a ``ValueError`` when passed an empty list in ``index`` (:issue:`58594`) +- Bug in :meth:`MultiIndex.insert` when a new value inserted to a datetime-like level gets cast to ``NaT`` and fails indexing (:issue:`60388`) - Bug in printing :attr:`Index.names` and :attr:`MultiIndex.levels` would not escape single quotes (:issue:`60190`) Missing diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 36e68465a99d9..dc48cd1ed958e 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -4084,11 +4084,10 @@ def insert(self, loc: int, item) -> MultiIndex: # have to insert into level # must insert at end otherwise you have to recompute all the # other codes - if isna(k): # GH 59003 + lev_loc = len(level) + level = level.insert(lev_loc, k) + if isna(level[lev_loc]): # GH 59003, 60388 lev_loc = -1 - else: - lev_loc = len(level) - level = level.insert(lev_loc, k) else: lev_loc = level.get_loc(k) diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py index e87498742061b..a23e6d9b3973a 100644 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -295,6 +295,29 @@ def test_multiindex_insert_level_with_na(self, na): df[na, "B"] = 1 tm.assert_frame_equal(df[na], DataFrame([1], columns=["B"])) + def test_multiindex_dt_with_nan(self): + # GH#60388 + df = DataFrame( + [ + [1, np.nan, 5, np.nan], + [2, np.nan, 6, np.nan], + [np.nan, 3, np.nan, 7], + [np.nan, 4, np.nan, 8], + ], + index=Series(["a", "b", "c", "d"], dtype=object, name="sub"), + columns=MultiIndex.from_product( + [ + ["value1", "value2"], + [datetime.datetime(2024, 11, 1), datetime.datetime(2024, 11, 2)], + ], + names=[None, "Date"], + ), + ) + df = df.reset_index() + result = df[df.columns[0]] + expected = Series(["a", "b", "c", "d"], name=("sub", np.nan)) + tm.assert_series_equal(result, expected) + class TestSorted: """everything you wanted to test about sorting""" From e631442400b0417c638d394d9d9af0e018cf366b Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 2 Dec 2024 16:15:57 -0800 Subject: [PATCH 126/266] BUG: Maintain column order in table method rolling (#60465) * BUG: Maintain column order in table method rolling * BUG: Add bug description to whatsnew/v3.0.0.rst --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/window/rolling.py | 2 +- pandas/tests/window/test_numba.py | 32 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index e73ee0dfbe67e..f4e7281ca0659 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -737,6 +737,7 @@ Groupby/resample/rolling - Bug in :meth:`DataFrameGroupBy.cumsum` and :meth:`DataFrameGroupBy.cumprod` where ``numeric_only`` parameter was passed indirectly through kwargs instead of passing directly. (:issue:`58811`) - Bug in :meth:`DataFrameGroupBy.cumsum` where it did not return the correct dtype when the label contained ``None``. (:issue:`58811`) - Bug in :meth:`DataFrameGroupby.transform` and :meth:`SeriesGroupby.transform` with a reducer and ``observed=False`` that coerces dtype to float when there are unobserved categories. (:issue:`55326`) +- Bug in :meth:`Rolling.apply` for ``method="table"`` where column order was not being respected due to the columns getting sorted by default. (:issue:`59666`) - Bug in :meth:`Rolling.apply` where the applied function could be called on fewer than ``min_period`` periods if ``method="table"``. (:issue:`58868`) - Bug in :meth:`Series.resample` could raise when the the date range ended shortly before a non-existent time. (:issue:`58380`) diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index b1c37ab48fa57..4446b21976069 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -269,7 +269,7 @@ def _create_data(self, obj: NDFrameT, numeric_only: bool = False) -> NDFrameT: """ # filter out the on from the object if self.on is not None and not isinstance(self.on, Index) and obj.ndim == 2: - obj = obj.reindex(columns=obj.columns.difference([self.on])) + obj = obj.reindex(columns=obj.columns.difference([self.on], sort=False)) if obj.ndim > 1 and numeric_only: obj = self._make_numeric_only(obj) return obj diff --git a/pandas/tests/window/test_numba.py b/pandas/tests/window/test_numba.py index d9ab4723a8f2c..120dbe788a23f 100644 --- a/pandas/tests/window/test_numba.py +++ b/pandas/tests/window/test_numba.py @@ -459,6 +459,38 @@ def f(x): ) tm.assert_frame_equal(result, expected) + def test_table_method_rolling_apply_col_order(self): + # GH#59666 + def f(x): + return np.nanmean(x[:, 0] - x[:, 1]) + + df = DataFrame( + { + "a": [1, 2, 3, 4, 5, 6], + "b": [6, 7, 8, 5, 6, 7], + } + ) + result = df.rolling(3, method="table", min_periods=0)[["a", "b"]].apply( + f, raw=True, engine="numba" + ) + expected = DataFrame( + { + "a": [-5, -5, -5, -3.66667, -2.33333, -1], + "b": [-5, -5, -5, -3.66667, -2.33333, -1], + } + ) + tm.assert_almost_equal(result, expected) + result = df.rolling(3, method="table", min_periods=0)[["b", "a"]].apply( + f, raw=True, engine="numba" + ) + expected = DataFrame( + { + "b": [5, 5, 5, 3.66667, 2.33333, 1], + "a": [5, 5, 5, 3.66667, 2.33333, 1], + } + ) + tm.assert_almost_equal(result, expected) + def test_table_method_rolling_weighted_mean(self, step): def weighted_mean(x): arr = np.ones((1, x.shape[1])) From d9dfaa9d1d7d5bb1c81b3c32628c81693edfd9dd Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 2 Dec 2024 16:16:33 -0800 Subject: [PATCH 127/266] BUG: Fix pd.read_html handling of rowspan in table header (#60464) * BUG: Fix pd.read_html handling of rowspan in table header * BUG: Fix docstring error in _expand_colspan_rowspan * BUG: Update return type for _expand_colspan_rowspan * BUG: Address review and add not to whatsnew --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/html.py | 58 ++++++++++++++++++++++------------ pandas/tests/io/test_html.py | 27 ++++++++++++++++ 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index f4e7281ca0659..83638ce87f7ac 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -701,6 +701,7 @@ I/O - Bug in :meth:`read_csv` raising ``TypeError`` when ``nrows`` and ``iterator`` are specified without specifying a ``chunksize``. (:issue:`59079`) - Bug in :meth:`read_csv` where the order of the ``na_values`` makes an inconsistency when ``na_values`` is a list non-string values. (:issue:`59303`) - Bug in :meth:`read_excel` raising ``ValueError`` when passing array of boolean values when ``dtype="boolean"``. (:issue:`58159`) +- Bug in :meth:`read_html` where ``rowspan`` in header row causes incorrect conversion to ``DataFrame``. (:issue:`60210`) - Bug in :meth:`read_json` not validating the ``typ`` argument to not be exactly ``"frame"`` or ``"series"`` (:issue:`59124`) - Bug in :meth:`read_json` where extreme value integers in string format were incorrectly parsed as a different integer number (:issue:`20608`) - Bug in :meth:`read_stata` raising ``KeyError`` when input file is stored in big-endian format and contains strL data. (:issue:`58638`) diff --git a/pandas/io/html.py b/pandas/io/html.py index c9897f628fdc9..183af3a03221b 100644 --- a/pandas/io/html.py +++ b/pandas/io/html.py @@ -454,15 +454,26 @@ def row_is_all_th(row): while body_rows and row_is_all_th(body_rows[0]): header_rows.append(body_rows.pop(0)) - header = self._expand_colspan_rowspan(header_rows, section="header") - body = self._expand_colspan_rowspan(body_rows, section="body") - footer = self._expand_colspan_rowspan(footer_rows, section="footer") + header, rem = self._expand_colspan_rowspan(header_rows, section="header") + body, rem = self._expand_colspan_rowspan( + body_rows, + section="body", + remainder=rem, + overflow=len(footer_rows) > 0, + ) + footer, _ = self._expand_colspan_rowspan( + footer_rows, section="footer", remainder=rem, overflow=False + ) return header, body, footer def _expand_colspan_rowspan( - self, rows, section: Literal["header", "footer", "body"] - ) -> list[list]: + self, + rows, + section: Literal["header", "footer", "body"], + remainder: list[tuple[int, str | tuple, int]] | None = None, + overflow: bool = True, + ) -> tuple[list[list], list[tuple[int, str | tuple, int]]]: """ Given a list of s, return a list of text rows. @@ -471,12 +482,20 @@ def _expand_colspan_rowspan( rows : list of node-like List of s section : the section that the rows belong to (header, body or footer). + remainder: list[tuple[int, str | tuple, int]] | None + Any remainder from the expansion of previous section + overflow: bool + If true, return any partial rows as 'remainder'. If not, use up any + partial rows. True by default. Returns ------- list of list Each returned row is a list of str text, or tuple (text, link) if extract_links is not None. + remainder + Remaining partial rows if any. If overflow is False, an empty list + is returned. Notes ----- @@ -485,9 +504,7 @@ def _expand_colspan_rowspan( """ all_texts = [] # list of rows, each a list of str text: str | tuple - remainder: list[ - tuple[int, str | tuple, int] - ] = [] # list of (index, text, nrows) + remainder = remainder if remainder is not None else [] for tr in rows: texts = [] # the output for this row @@ -528,19 +545,20 @@ def _expand_colspan_rowspan( all_texts.append(texts) remainder = next_remainder - # Append rows that only appear because the previous row had non-1 - # rowspan - while remainder: - next_remainder = [] - texts = [] - for prev_i, prev_text, prev_rowspan in remainder: - texts.append(prev_text) - if prev_rowspan > 1: - next_remainder.append((prev_i, prev_text, prev_rowspan - 1)) - all_texts.append(texts) - remainder = next_remainder + if not overflow: + # Append rows that only appear because the previous row had non-1 + # rowspan + while remainder: + next_remainder = [] + texts = [] + for prev_i, prev_text, prev_rowspan in remainder: + texts.append(prev_text) + if prev_rowspan > 1: + next_remainder.append((prev_i, prev_text, prev_rowspan - 1)) + all_texts.append(texts) + remainder = next_remainder - return all_texts + return all_texts, remainder def _handle_hidden_tables(self, tbl_list, attr_name: str): """ diff --git a/pandas/tests/io/test_html.py b/pandas/tests/io/test_html.py index 73e9933e3681b..bef28c4f027da 100644 --- a/pandas/tests/io/test_html.py +++ b/pandas/tests/io/test_html.py @@ -1004,6 +1004,33 @@ def test_rowspan_only_rows(self, flavor_read_html): tm.assert_frame_equal(result, expected) + def test_rowspan_in_header_overflowing_to_body(self, flavor_read_html): + # GH60210 + + result = flavor_read_html( + StringIO( + """ + + + + + + + + + + + + +
    AB
    1
    C2
    + """ + ) + )[0] + + expected = DataFrame(data=[["A", 1], ["C", 2]], columns=["A", "B"]) + + tm.assert_frame_equal(result, expected) + def test_header_inferred_from_rows_with_only_th(self, flavor_read_html): # GH17054 result = flavor_read_html( From d067e0839ed8fe5379c180a05fc8dc98771c5602 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:49:06 -0800 Subject: [PATCH 128/266] BUG: Fix stata bug post pre-commit update (#60476) --- pandas/io/stata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/io/stata.py b/pandas/io/stata.py index 053e331925b6f..34d95fb59a21c 100644 --- a/pandas/io/stata.py +++ b/pandas/io/stata.py @@ -2207,14 +2207,14 @@ def _convert_datetime_to_stata_type(fmt: str) -> np.dtype: def _maybe_convert_to_int_keys(convert_dates: dict, varlist: list[Hashable]) -> dict: new_dict = {} for key, value in convert_dates.items(): - if not value.startswith("%"): # make sure proper fmts + if not convert_dates[key].startswith("%"): # make sure proper fmts convert_dates[key] = "%" + value if key in varlist: - new_dict[varlist.index(key)] = value + new_dict[varlist.index(key)] = convert_dates[key] else: if not isinstance(key, int): raise ValueError("convert_dates key must be a column or an integer") - new_dict[key] = value + new_dict[key] = convert_dates[key] return new_dict From 86954016384c53745d6144af80da5957ad2e82fd Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 3 Dec 2024 19:34:25 +0100 Subject: [PATCH 129/266] PERF: improve construct_1d_object_array_from_listlike (#60461) * PERF: improve construct_1d_object_array_from_listlike * use np.fromiter and update annotation --- pandas/core/dtypes/cast.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 137a49c4487f6..02b9291da9b31 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -87,8 +87,8 @@ if TYPE_CHECKING: from collections.abc import ( + Collection, Sequence, - Sized, ) from pandas._typing import ( @@ -1581,7 +1581,7 @@ def _maybe_box_and_unbox_datetimelike(value: Scalar, dtype: DtypeObj): return _maybe_unbox_datetimelike(value, dtype) -def construct_1d_object_array_from_listlike(values: Sized) -> np.ndarray: +def construct_1d_object_array_from_listlike(values: Collection) -> np.ndarray: """ Transform any list-like object in a 1-dimensional numpy array of object dtype. @@ -1599,11 +1599,9 @@ def construct_1d_object_array_from_listlike(values: Sized) -> np.ndarray: ------- 1-dimensional numpy array of dtype object """ - # numpy will try to interpret nested lists as further dimensions, hence - # making a 1D array that contains list-likes is a bit tricky: - result = np.empty(len(values), dtype="object") - result[:] = values - return result + # numpy will try to interpret nested lists as further dimensions in np.array(), + # hence explicitly making a 1D array using np.fromiter + return np.fromiter(values, dtype="object", count=len(values)) def maybe_cast_to_integer_array(arr: list | np.ndarray, dtype: np.dtype) -> np.ndarray: From aa4b621172f2710cdb970e10248d669c5d9b5e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20G=C3=B3mez?= Date: Tue, 3 Dec 2024 19:39:25 +0100 Subject: [PATCH 130/266] DOC: Fix some docstring validations in pd.Series (#60481) * DOC: Fix some docstring validations in pd.Series * new circle --- ci/code_checks.sh | 2 -- pandas/core/arrays/datetimelike.py | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index dde98a01cc770..a21b87950cee1 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -73,8 +73,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Period.freq GL08" \ -i "pandas.Period.ordinal GL08" \ -i "pandas.RangeIndex.from_range PR01,SA01" \ - -i "pandas.Series.dt.unit GL08" \ - -i "pandas.Series.pad PR01,SA01" \ -i "pandas.Timedelta.max PR02" \ -i "pandas.Timedelta.min PR02" \ -i "pandas.Timedelta.resolution PR02" \ diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 9c821bf0d184e..c6b6367e347ba 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -2073,7 +2073,29 @@ def _creso(self) -> int: @cache_readonly def unit(self) -> str: - # e.g. "ns", "us", "ms" + """ + The precision unit of the datetime data. + + Returns the precision unit for the dtype. + It means the smallest time frame that can be stored within this dtype. + + Returns + ------- + str + Unit string representation (e.g. "ns"). + + See Also + -------- + TimelikeOps.as_unit : Converts to a specific unit. + + Examples + -------- + >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"]) + >>> idx.unit + 'ns' + >>> idx.as_unit("s").unit + 's' + """ # error: Argument 1 to "dtype_to_unit" has incompatible type # "ExtensionDtype"; expected "Union[DatetimeTZDtype, dtype[Any]]" return dtype_to_unit(self.dtype) # type: ignore[arg-type] From 0c0938399cfb1c2a4baa9e83a03a0ada692246ed Mon Sep 17 00:00:00 2001 From: Chris <76128089+thedataninja1786@users.noreply.github.com> Date: Tue, 3 Dec 2024 20:40:09 +0200 Subject: [PATCH 131/266] Adds See Also sections to pandas.core.groupby.DataFrameGroupBy.sem, pandas.core.groupby.DataFrameGroupBy.nunique (#60480) * Added See Also Sections * pre-commit checks * Update code_checks.sh * Udpate code_checks.sh * Update ci/code_checks.sh --- ci/code_checks.sh | 4 ---- pandas/core/groupby/generic.py | 4 ++++ pandas/core/groupby/groupby.py | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index a21b87950cee1..f23481b3da3a2 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -86,19 +86,15 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \ - -i "pandas.core.groupby.DataFrameGroupBy.nunique SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ - -i "pandas.core.groupby.DataFrameGroupBy.sem SA01" \ -i "pandas.core.groupby.SeriesGroupBy.get_group RT03,SA01" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ - -i "pandas.core.groupby.SeriesGroupBy.sem SA01" \ -i "pandas.core.resample.Resampler.get_group RT03,SA01" \ -i "pandas.core.resample.Resampler.max PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.mean SA01" \ -i "pandas.core.resample.Resampler.min PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.prod SA01" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ - -i "pandas.core.resample.Resampler.sem SA01" \ -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 35ec09892ede6..3a917e0147396 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -2453,6 +2453,10 @@ def nunique(self, dropna: bool = True) -> DataFrame: nunique: DataFrame Counts of unique elements in each position. + See Also + -------- + DataFrame.nunique : Count number of distinct elements in specified axis. + Examples -------- >>> df = pd.DataFrame( diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 48d4e0456d4fa..e750c606a4c44 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -2658,6 +2658,11 @@ def sem(self, ddof: int = 1, numeric_only: bool = False) -> NDFrameT: Series or DataFrame Standard error of the mean of values within each group. + See Also + -------- + DataFrame.sem : Return unbiased standard error of the mean over requested axis. + Series.sem : Return unbiased standard error of the mean over requested axis. + Examples -------- For SeriesGroupBy: From 844b3191bd45b95cbaae341048bf7f367f086f2f Mon Sep 17 00:00:00 2001 From: Axeldnahcram <33946160+Axeldnahcram@users.noreply.github.com> Date: Tue, 3 Dec 2024 19:42:59 +0100 Subject: [PATCH 132/266] DOC: DataFrameGroupBy.idxmin() returns DataFrame, documentation says Serie (#60474) * DOC: modify examples and return in docs * DOC: fix examples * DOC: unify * Whitespace * Pre commit * Double line breaks * DOC: finally rann pre commit * Remove unused notebook --- pandas/core/groupby/generic.py | 44 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 3a917e0147396..3fa34007a739b 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -1321,8 +1321,8 @@ def idxmin(self, skipna: bool = True) -> Series: Returns ------- - Index - Label of the minimum value. + Series + Indexes of minima in each group. Raises ------ @@ -1374,8 +1374,8 @@ def idxmax(self, skipna: bool = True) -> Series: Returns ------- - Index - Label of the maximum value. + Series + Indexes of maxima in each group. Raises ------ @@ -2512,8 +2512,8 @@ def idxmax( Returns ------- - Series - Indexes of maxima in each group. + DataFrame + Indexes of maxima in each column according to the group. Raises ------ @@ -2523,6 +2523,7 @@ def idxmax( See Also -------- Series.idxmax : Return index of the maximum element. + DataFrame.idxmax : Indexes of maxima along the specified axis. Notes ----- @@ -2536,6 +2537,7 @@ def idxmax( ... { ... "consumption": [10.51, 103.11, 55.48], ... "co2_emissions": [37.2, 19.66, 1712], + ... "food_type": ["meat", "plant", "meat"], ... }, ... index=["Pork", "Wheat Products", "Beef"], ... ) @@ -2546,12 +2548,14 @@ def idxmax( Wheat Products 103.11 19.66 Beef 55.48 1712.00 - By default, it returns the index for the maximum value in each column. + By default, it returns the index for the maximum value in each column + according to the group. - >>> df.idxmax() - consumption Wheat Products - co2_emissions Beef - dtype: object + >>> df.groupby("food_type").idxmax() + consumption co2_emissions + food_type + animal Beef Beef + plant Wheat Products Wheat Products """ return self._idxmax_idxmin("idxmax", numeric_only=numeric_only, skipna=skipna) @@ -2574,8 +2578,8 @@ def idxmin( Returns ------- - Series - Indexes of minima in each group. + DataFrame + Indexes of minima in each column according to the group. Raises ------ @@ -2585,6 +2589,7 @@ def idxmin( See Also -------- Series.idxmin : Return index of the minimum element. + DataFrame.idxmin : Indexes of minima along the specified axis. Notes ----- @@ -2598,6 +2603,7 @@ def idxmin( ... { ... "consumption": [10.51, 103.11, 55.48], ... "co2_emissions": [37.2, 19.66, 1712], + ... "food_type": ["meat", "plant", "meat"], ... }, ... index=["Pork", "Wheat Products", "Beef"], ... ) @@ -2608,12 +2614,14 @@ def idxmin( Wheat Products 103.11 19.66 Beef 55.48 1712.00 - By default, it returns the index for the minimum value in each column. + By default, it returns the index for the minimum value in each column + according to the group. - >>> df.idxmin() - consumption Pork - co2_emissions Wheat Products - dtype: object + >>> df.groupby("food_type").idxmin() + consumption co2_emissions + food_type + animal Pork Pork + plant Wheat Products Wheat Products """ return self._idxmax_idxmin("idxmin", numeric_only=numeric_only, skipna=skipna) From d589839e80d3612890c592cc58319b388474810c Mon Sep 17 00:00:00 2001 From: UV Date: Wed, 4 Dec 2024 02:01:34 +0530 Subject: [PATCH 133/266] DOC: Examples added for float_format in to_csv documentation (#60457) * Checking for the first link added * DOC: Added missing links to optional dependencies in getting_started/install.html * DOC: Examples added for float_format in to_csv documentation * Updated the float_format based on suggested change * Changes made according to the review --- pandas/core/generic.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index a6be17a654aa7..3a48cc8a66076 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3878,6 +3878,14 @@ def to_csv( >>> import os # doctest: +SKIP >>> os.makedirs("folder/subfolder", exist_ok=True) # doctest: +SKIP >>> df.to_csv("folder/subfolder/out.csv") # doctest: +SKIP + + Format floats to two decimal places: + + >>> df.to_csv("out1.csv", float_format="%.2f") # doctest: +SKIP + + Format floats using scientific notation: + + >>> df.to_csv("out2.csv", float_format="{{:.2e}}".format) # doctest: +SKIP """ df = self if isinstance(self, ABCDataFrame) else self.to_frame() From 89112387d8e56a7c8f1c71259697a6fe7f701864 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Wed, 4 Dec 2024 04:32:15 +0800 Subject: [PATCH 134/266] BUG: ValueError when printing a DataFrame with DataFrame in its attrs (#60459) * Add test * replace concat with np * Revert "replace concat with np" This reverts commit b48fc35dc4cc4a5f413ad1b905d38408a796699d. * Revert "Revert "replace concat with np"" This reverts commit 6b45ac5dd8c1f985f2eaa7ec376fbf6c9799b6c5. * try fixing mypy error * Add whatsnew --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/format.py | 8 ++++---- pandas/tests/io/formats/test_format.py | 7 +++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 83638ce87f7ac..bb9f48d17b2e1 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -797,6 +797,7 @@ Other - Bug in :meth:`read_csv` where chained fsspec TAR file and ``compression="infer"`` fails with ``tarfile.ReadError`` (:issue:`60028`) - Bug in Dataframe Interchange Protocol implementation was returning incorrect results for data buffers' associated dtype, for string and datetime columns (:issue:`54781`) - Bug in ``Series.list`` methods not preserving the original :class:`Index`. (:issue:`58425`) +- Bug in printing a :class:`DataFrame` with a :class:`DataFrame` stored in :attr:`DataFrame.attrs` raised a ``ValueError`` (:issue:`60455`) .. ***DO NOT USE THIS SECTION*** diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index 4f87b1a30ca61..17460eae8c049 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -669,9 +669,9 @@ def _truncate_horizontally(self) -> None: assert self.max_cols_fitted is not None col_num = self.max_cols_fitted // 2 if col_num >= 1: - left = self.tr_frame.iloc[:, :col_num] - right = self.tr_frame.iloc[:, -col_num:] - self.tr_frame = concat((left, right), axis=1) + _len = len(self.tr_frame.columns) + _slice = np.hstack([np.arange(col_num), np.arange(_len - col_num, _len)]) + self.tr_frame = self.tr_frame.iloc[:, _slice] # truncate formatter if isinstance(self.formatters, (list, tuple)): @@ -682,7 +682,7 @@ def _truncate_horizontally(self) -> None: else: col_num = cast(int, self.max_cols) self.tr_frame = self.tr_frame.iloc[:, :col_num] - self.tr_col_num = col_num + self.tr_col_num: int = col_num def _truncate_vertically(self) -> None: """Remove rows, which are not to be displayed. diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index 0dc16e1ebc723..d7db3d5082135 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -129,6 +129,13 @@ def test_repr_truncation_preserves_na(self): with option_context("display.max_rows", 2, "display.show_dimensions", False): assert repr(df) == " a\n0 \n.. ...\n9 " + def test_repr_truncation_dataframe_attrs(self): + # GH#60455 + df = DataFrame([[0] * 10]) + df.attrs["b"] = DataFrame([]) + with option_context("display.max_columns", 2, "display.show_dimensions", False): + assert repr(df) == " 0 ... 9\n0 0 ... 0" + def test_max_colwidth_negative_int_raises(self): # Deprecation enforced from: # https://github.com/pandas-dev/pandas/issues/31532 From cfd0d3f010217939e412efdcfb7e669567e4d189 Mon Sep 17 00:00:00 2001 From: lfffkh <167774581+lfffkh@users.noreply.github.com> Date: Wed, 4 Dec 2024 05:21:20 +0800 Subject: [PATCH 135/266] BLD: Missing 'pickleshare' package when running 'sphinx-build' command (#60468) --- environment.yml | 1 + requirements-dev.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/environment.yml b/environment.yml index 8ede5a16b7a59..69647a436e3ad 100644 --- a/environment.yml +++ b/environment.yml @@ -35,6 +35,7 @@ dependencies: - hypothesis>=6.84.0 - gcsfs>=2022.11.0 - ipython + - pickleshare # Needed for IPython Sphinx directive in the docs GH#60429 - jinja2>=3.1.2 - lxml>=4.9.2 - matplotlib>=3.6.3 diff --git a/requirements-dev.txt b/requirements-dev.txt index b68b9f0c8f92c..fb4d9cdb589ca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,7 @@ html5lib>=1.1 hypothesis>=6.84.0 gcsfs>=2022.11.0 ipython +pickleshare jinja2>=3.1.2 lxml>=4.9.2 matplotlib>=3.6.3 From a36c44e129bd2f70c25d5dec89cb2893716bdbf6 Mon Sep 17 00:00:00 2001 From: Axeldnahcram <33946160+Axeldnahcram@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:21:15 +0100 Subject: [PATCH 136/266] DOC: Fix docstrings errors SEM and GET_GROUP (#60475) * DOC: fix sem * Added sections * DOC: fix See also * Remove failed docstrings * Fix: Matches the right format * Pre commit format --- ci/code_checks.sh | 3 --- pandas/core/groupby/groupby.py | 14 +++++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index f23481b3da3a2..adc5bc9a01bdd 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -85,11 +85,8 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.NumpyExtensionArray SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ - -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ - -i "pandas.core.groupby.SeriesGroupBy.get_group RT03,SA01" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ - -i "pandas.core.resample.Resampler.get_group RT03,SA01" \ -i "pandas.core.resample.Resampler.max PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.mean SA01" \ -i "pandas.core.resample.Resampler.min PR01,RT03,SA01" \ diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index e750c606a4c44..f0513be3498d1 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -715,7 +715,19 @@ def get_group(self, name) -> DataFrame | Series: Returns ------- - DataFrame or Series + Series or DataFrame + Get the respective Series or DataFrame corresponding to the group provided. + + See Also + -------- + DataFrameGroupBy.groups: Dictionary representation of the groupings formed + during a groupby operation. + DataFrameGroupBy.indices: Provides a mapping of group rows to positions + of the elements. + SeriesGroupBy.groups: Dictionary representation of the groupings formed + during a groupby operation. + SeriesGroupBy.indices: Provides a mapping of group rows to positions + of the elements. Examples -------- From 497208f03ce226d5e006e7a713ceab5f303fe1e2 Mon Sep 17 00:00:00 2001 From: French_Ball <127096560+asdkfjsd@users.noreply.github.com> Date: Sat, 7 Dec 2024 02:12:08 +0800 Subject: [PATCH 137/266] doc: update dsintro.rst to remove a warring (#60507) To solve a warning in issue DOC: DeprecationWarning on "Intro to data structures" user guide #60490. I have checked other parts of the page, and there are no such warnings. --- doc/source/user_guide/dsintro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/dsintro.rst b/doc/source/user_guide/dsintro.rst index b9c285ca30c96..89981786d60b5 100644 --- a/doc/source/user_guide/dsintro.rst +++ b/doc/source/user_guide/dsintro.rst @@ -326,7 +326,7 @@ This case is handled identically to a dict of arrays. .. ipython:: python - data = np.zeros((2,), dtype=[("A", "i4"), ("B", "f4"), ("C", "a10")]) + data = np.zeros((2,), dtype=[("A", "i4"), ("B", "f4"), ("C", "S10")]) data[:] = [(1, 2.0, "Hello"), (2, 3.0, "World")] pd.DataFrame(data) From 8a286fa16f3160e939b192cbe8e218992a84e6fc Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Fri, 6 Dec 2024 10:13:45 -0800 Subject: [PATCH 138/266] =?UTF-8?q?BUG:=20Fix=20bug=20in=20GroupBy=20that?= =?UTF-8?q?=20ignores=20group=5Fkeys=20arg=20for=20empty=20datafra?= =?UTF-8?q?=E2=80=A6=20(#60505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG: Fix bug in GroupBy that ignores group_keys arg for empty dataframes/series --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/groupby/generic.py | 4 ++++ pandas/tests/groupby/aggregate/test_aggregate.py | 1 + pandas/tests/groupby/test_all_methods.py | 2 +- pandas/tests/groupby/test_grouping.py | 13 ++++++++++++- 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index bb9f48d17b2e1..ab5746eca1b18 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -733,6 +733,7 @@ Groupby/resample/rolling - Bug in :meth:`.Resampler.interpolate` on a :class:`DataFrame` with non-uniform sampling and/or indices not aligning with the resulting resampled index would result in wrong interpolation (:issue:`21351`) - Bug in :meth:`DataFrame.ewm` and :meth:`Series.ewm` when passed ``times`` and aggregation functions other than mean (:issue:`51695`) - Bug in :meth:`DataFrameGroupBy.agg` that raises ``AttributeError`` when there is dictionary input and duplicated columns, instead of returning a DataFrame with the aggregation of all duplicate columns. (:issue:`55041`) +- Bug in :meth:`DataFrameGroupBy.apply` and :meth:`SeriesGroupBy.apply` for empty data frame with ``group_keys=False`` still creating output index using group keys. (:issue:`60471`) - Bug in :meth:`DataFrameGroupBy.apply` that was returning a completely empty DataFrame when all return values of ``func`` were ``None`` instead of returning an empty DataFrame with the original columns and dtypes. (:issue:`57775`) - Bug in :meth:`DataFrameGroupBy.apply` with ``as_index=False`` that was returning :class:`MultiIndex` instead of returning :class:`Index`. (:issue:`58291`) - Bug in :meth:`DataFrameGroupBy.cumsum` and :meth:`DataFrameGroupBy.cumprod` where ``numeric_only`` parameter was passed indirectly through kwargs instead of passing directly. (:issue:`58811`) diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 3fa34007a739b..f4e3f3e8b1001 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -583,6 +583,8 @@ def _wrap_applied_output( if is_transform: # GH#47787 see test_group_on_empty_multiindex res_index = data.index + elif not self.group_keys: + res_index = None else: res_index = self._grouper.result_index @@ -1967,6 +1969,8 @@ def _wrap_applied_output( if is_transform: # GH#47787 see test_group_on_empty_multiindex res_index = data.index + elif not self.group_keys: + res_index = None else: res_index = self._grouper.result_index diff --git a/pandas/tests/groupby/aggregate/test_aggregate.py b/pandas/tests/groupby/aggregate/test_aggregate.py index 64220f1d3d5b4..b7e6e55739c17 100644 --- a/pandas/tests/groupby/aggregate/test_aggregate.py +++ b/pandas/tests/groupby/aggregate/test_aggregate.py @@ -159,6 +159,7 @@ def test_agg_apply_corner(ts, tsframe): tm.assert_frame_equal(grouped.agg("sum"), exp_df) res = grouped.apply(np.sum, axis=0) + exp_df = exp_df.reset_index(drop=True) tm.assert_frame_equal(res, exp_df) diff --git a/pandas/tests/groupby/test_all_methods.py b/pandas/tests/groupby/test_all_methods.py index 945c3e421a132..4625c5c27a803 100644 --- a/pandas/tests/groupby/test_all_methods.py +++ b/pandas/tests/groupby/test_all_methods.py @@ -22,7 +22,7 @@ def test_multiindex_group_all_columns_when_empty(groupby_func): # GH 32464 df = DataFrame({"a": [], "b": [], "c": []}).set_index(["a", "b", "c"]) - gb = df.groupby(["a", "b", "c"], group_keys=False) + gb = df.groupby(["a", "b", "c"], group_keys=True) method = getattr(gb, groupby_func) args = get_groupby_method_args(groupby_func, df) if groupby_func == "corrwith": diff --git a/pandas/tests/groupby/test_grouping.py b/pandas/tests/groupby/test_grouping.py index 366eb59ee226a..4e7c0acb127ed 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -777,10 +777,21 @@ def test_evaluate_with_empty_groups(self, func, expected): # (not testing other agg fns, because they return # different index objects. df = DataFrame({1: [], 2: []}) - g = df.groupby(1, group_keys=False) + g = df.groupby(1, group_keys=True) result = getattr(g[2], func)(lambda x: x) tm.assert_series_equal(result, expected) + def test_groupby_apply_empty_with_group_keys_false(self): + # 60471 + # test apply'ing empty groups with group_keys False + # (not testing other agg fns, because they return + # different index objects. + df = DataFrame({"A": [], "B": [], "C": []}) + g = df.groupby("A", group_keys=False) + result = g.apply(lambda x: x / x.sum(), include_groups=False) + expected = DataFrame({"B": [], "C": []}, index=None) + tm.assert_frame_equal(result, expected) + def test_groupby_empty(self): # https://github.com/pandas-dev/pandas/issues/27190 s = Series([], name="name", dtype="float64") From 29d7e0897aa2877a73af173127397e841207e16c Mon Sep 17 00:00:00 2001 From: Shubhank Gyawali <68085066+Shubhank-Gyawali@users.noreply.github.com> Date: Sun, 8 Dec 2024 06:04:31 -0800 Subject: [PATCH 139/266] DOC: Fix hyperlinks to NumPy methods in DataFrame.shape / DataFrame.ndim (#60516) --- pandas/core/frame.py | 2 +- pandas/core/generic.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 33a419925f70c..34b448a0d8d1c 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -1018,7 +1018,7 @@ def shape(self) -> tuple[int, int]: See Also -------- - ndarray.shape : Tuple of array dimensions. + numpy.ndarray.shape : Tuple of array dimensions. Examples -------- diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 3a48cc8a66076..d1aa20501b060 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -640,7 +640,7 @@ def ndim(self) -> int: See Also -------- - ndarray.ndim : Number of array dimensions. + numpy.ndarray.ndim : Number of array dimensions. Examples -------- From 07e0bca0a6e2005b6fc31110f28c32e606df288d Mon Sep 17 00:00:00 2001 From: easternsun7 <165460574+easternsun7@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:31:40 +0800 Subject: [PATCH 140/266] Update frame.rst (#60525) Fix the navigation bar --- doc/source/reference/frame.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/source/reference/frame.rst b/doc/source/reference/frame.rst index 7680c8b434866..e701d48a89db7 100644 --- a/doc/source/reference/frame.rst +++ b/doc/source/reference/frame.rst @@ -185,7 +185,6 @@ Reindexing / selection / label manipulation DataFrame.duplicated DataFrame.equals DataFrame.filter - DataFrame.head DataFrame.idxmax DataFrame.idxmin DataFrame.reindex @@ -196,7 +195,6 @@ Reindexing / selection / label manipulation DataFrame.sample DataFrame.set_axis DataFrame.set_index - DataFrame.tail DataFrame.take DataFrame.truncate From 59f947ff40308bcfb6ecb65eb23b391d6f031c03 Mon Sep 17 00:00:00 2001 From: Michelino Gali <107483586+migelogali@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:32:30 -0500 Subject: [PATCH 141/266] updated v to conv_val in that function (#60518) --- pandas/core/computation/pytables.py | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pandas/core/computation/pytables.py b/pandas/core/computation/pytables.py index fe7e27f537b01..4a75acce46632 100644 --- a/pandas/core/computation/pytables.py +++ b/pandas/core/computation/pytables.py @@ -205,7 +205,7 @@ def generate(self, v) -> str: val = v.tostring(self.encoding) return f"({self.lhs} {self.op} {val})" - def convert_value(self, v) -> TermValue: + def convert_value(self, conv_val) -> TermValue: """ convert the expression that is in the term to something that is accepted by pytables @@ -219,44 +219,44 @@ def stringify(value): kind = ensure_decoded(self.kind) meta = ensure_decoded(self.meta) if kind == "datetime" or (kind and kind.startswith("datetime64")): - if isinstance(v, (int, float)): - v = stringify(v) - v = ensure_decoded(v) - v = Timestamp(v).as_unit("ns") - if v.tz is not None: - v = v.tz_convert("UTC") - return TermValue(v, v._value, kind) + if isinstance(conv_val, (int, float)): + conv_val = stringify(conv_val) + conv_val = ensure_decoded(conv_val) + conv_val = Timestamp(conv_val).as_unit("ns") + if conv_val.tz is not None: + conv_val = conv_val.tz_convert("UTC") + return TermValue(conv_val, conv_val._value, kind) elif kind in ("timedelta64", "timedelta"): - if isinstance(v, str): - v = Timedelta(v) + if isinstance(conv_val, str): + conv_val = Timedelta(conv_val) else: - v = Timedelta(v, unit="s") - v = v.as_unit("ns")._value - return TermValue(int(v), v, kind) + conv_val = Timedelta(conv_val, unit="s") + conv_val = conv_val.as_unit("ns")._value + return TermValue(int(conv_val), conv_val, kind) elif meta == "category": metadata = extract_array(self.metadata, extract_numpy=True) result: npt.NDArray[np.intp] | np.intp | int - if v not in metadata: + if conv_val not in metadata: result = -1 else: - result = metadata.searchsorted(v, side="left") + result = metadata.searchsorted(conv_val, side="left") return TermValue(result, result, "integer") elif kind == "integer": try: - v_dec = Decimal(v) + v_dec = Decimal(conv_val) except InvalidOperation: # GH 54186 # convert v to float to raise float's ValueError - float(v) + float(conv_val) else: - v = int(v_dec.to_integral_exact(rounding="ROUND_HALF_EVEN")) - return TermValue(v, v, kind) + conv_val = int(v_dec.to_integral_exact(rounding="ROUND_HALF_EVEN")) + return TermValue(conv_val, conv_val, kind) elif kind == "float": - v = float(v) - return TermValue(v, v, kind) + conv_val = float(conv_val) + return TermValue(conv_val, conv_val, kind) elif kind == "bool": - if isinstance(v, str): - v = v.strip().lower() not in [ + if isinstance(conv_val, str): + conv_val = conv_val.strip().lower() not in [ "false", "f", "no", @@ -268,13 +268,13 @@ def stringify(value): "", ] else: - v = bool(v) - return TermValue(v, v, kind) - elif isinstance(v, str): + conv_val = bool(conv_val) + return TermValue(conv_val, conv_val, kind) + elif isinstance(conv_val, str): # string quoting - return TermValue(v, stringify(v), "string") + return TermValue(conv_val, stringify(conv_val), "string") else: - raise TypeError(f"Cannot compare {v} of type {type(v)} to {kind} column") + raise TypeError(f"Cannot compare {conv_val} of type {type(conv_val)} to {kind} column") def convert_values(self) -> None: pass From 05f7ef9a2128ca04939f30840e86b38ec490c617 Mon Sep 17 00:00:00 2001 From: Francesco Bruzzesi <42817048+FBruzzesi@users.noreply.github.com> Date: Mon, 9 Dec 2024 19:35:22 +0100 Subject: [PATCH 142/266] BUG: Fix `ListAccessor` methods to preserve original name (#60527) * fix: preserve series name in ListAccessor * formatting * add whatsnew v3.0.0 entry --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/arrays/arrow/accessors.py | 24 +++++++++++++++---- .../series/accessors/test_list_accessor.py | 18 +++++++++++--- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index ab5746eca1b18..b799b7ea5cb39 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -798,6 +798,7 @@ Other - Bug in :meth:`read_csv` where chained fsspec TAR file and ``compression="infer"`` fails with ``tarfile.ReadError`` (:issue:`60028`) - Bug in Dataframe Interchange Protocol implementation was returning incorrect results for data buffers' associated dtype, for string and datetime columns (:issue:`54781`) - Bug in ``Series.list`` methods not preserving the original :class:`Index`. (:issue:`58425`) +- Bug in ``Series.list`` methods not preserving the original name. (:issue:`60522`) - Bug in printing a :class:`DataFrame` with a :class:`DataFrame` stored in :attr:`DataFrame.attrs` raised a ``ValueError`` (:issue:`60455`) .. ***DO NOT USE THIS SECTION*** diff --git a/pandas/core/arrays/arrow/accessors.py b/pandas/core/arrays/arrow/accessors.py index 230522846d377..b220a94d032b5 100644 --- a/pandas/core/arrays/arrow/accessors.py +++ b/pandas/core/arrays/arrow/accessors.py @@ -117,7 +117,10 @@ def len(self) -> Series: value_lengths = pc.list_value_length(self._pa_array) return Series( - value_lengths, dtype=ArrowDtype(value_lengths.type), index=self._data.index + value_lengths, + dtype=ArrowDtype(value_lengths.type), + index=self._data.index, + name=self._data.name, ) def __getitem__(self, key: int | slice) -> Series: @@ -162,7 +165,10 @@ def __getitem__(self, key: int | slice) -> Series: # key = pc.add(key, pc.list_value_length(self._pa_array)) element = pc.list_element(self._pa_array, key) return Series( - element, dtype=ArrowDtype(element.type), index=self._data.index + element, + dtype=ArrowDtype(element.type), + index=self._data.index, + name=self._data.name, ) elif isinstance(key, slice): if pa_version_under11p0: @@ -181,7 +187,12 @@ def __getitem__(self, key: int | slice) -> Series: if step is None: step = 1 sliced = pc.list_slice(self._pa_array, start, stop, step) - return Series(sliced, dtype=ArrowDtype(sliced.type), index=self._data.index) + return Series( + sliced, + dtype=ArrowDtype(sliced.type), + index=self._data.index, + name=self._data.name, + ) else: raise ValueError(f"key must be an int or slice, got {type(key).__name__}") @@ -223,7 +234,12 @@ def flatten(self) -> Series: counts = pa.compute.list_value_length(self._pa_array) flattened = pa.compute.list_flatten(self._pa_array) index = self._data.index.repeat(counts.fill_null(pa.scalar(0, counts.type))) - return Series(flattened, dtype=ArrowDtype(flattened.type), index=index) + return Series( + flattened, + dtype=ArrowDtype(flattened.type), + index=index, + name=self._data.name, + ) class StructAccessor(ArrowAccessor): diff --git a/pandas/tests/series/accessors/test_list_accessor.py b/pandas/tests/series/accessors/test_list_accessor.py index c153e800cb534..bec8ca13a2f5f 100644 --- a/pandas/tests/series/accessors/test_list_accessor.py +++ b/pandas/tests/series/accessors/test_list_accessor.py @@ -25,9 +25,10 @@ def test_list_getitem(list_dtype): ser = Series( [[1, 2, 3], [4, None, 5], None], dtype=ArrowDtype(list_dtype), + name="a", ) actual = ser.list[1] - expected = Series([2, None, None], dtype="int64[pyarrow]") + expected = Series([2, None, None], dtype="int64[pyarrow]", name="a") tm.assert_series_equal(actual, expected) @@ -37,9 +38,15 @@ def test_list_getitem_index(): [[1, 2, 3], [4, None, 5], None], dtype=ArrowDtype(pa.list_(pa.int64())), index=[1, 3, 7], + name="a", ) actual = ser.list[1] - expected = Series([2, None, None], dtype="int64[pyarrow]", index=[1, 3, 7]) + expected = Series( + [2, None, None], + dtype="int64[pyarrow]", + index=[1, 3, 7], + name="a", + ) tm.assert_series_equal(actual, expected) @@ -48,6 +55,7 @@ def test_list_getitem_slice(): [[1, 2, 3], [4, None, 5], None], dtype=ArrowDtype(pa.list_(pa.int64())), index=[1, 3, 7], + name="a", ) if pa_version_under11p0: with pytest.raises( @@ -60,6 +68,7 @@ def test_list_getitem_slice(): [[2, 3], [None, 5], None], dtype=ArrowDtype(pa.list_(pa.int64())), index=[1, 3, 7], + name="a", ) tm.assert_series_equal(actual, expected) @@ -68,9 +77,10 @@ def test_list_len(): ser = Series( [[1, 2, 3], [4, None], None], dtype=ArrowDtype(pa.list_(pa.int64())), + name="a", ) actual = ser.list.len() - expected = Series([3, 2, None], dtype=ArrowDtype(pa.int32())) + expected = Series([3, 2, None], dtype=ArrowDtype(pa.int32()), name="a") tm.assert_series_equal(actual, expected) @@ -78,12 +88,14 @@ def test_list_flatten(): ser = Series( [[1, 2, 3], None, [4, None], [], [7, 8]], dtype=ArrowDtype(pa.list_(pa.int64())), + name="a", ) actual = ser.list.flatten() expected = Series( [1, 2, 3, 4, None, 7, 8], dtype=ArrowDtype(pa.int64()), index=[0, 0, 0, 2, 2, 4, 4], + name="a", ) tm.assert_series_equal(actual, expected) From e6e1987b988857bb511d3797400b4d1873e86760 Mon Sep 17 00:00:00 2001 From: Wong2333 <3201884732@qq.com> Date: Tue, 10 Dec 2024 02:37:04 +0800 Subject: [PATCH 143/266] DOC: Update variables a and b to names consistent with comment documentation (#60526) * DOC: Fix title capitalization in documentation file * DOC: Fix title capitalization in documentation files * Update variables a and b to names consistent with comment documentation --- pandas/core/computation/expressions.py | 70 +++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/pandas/core/computation/expressions.py b/pandas/core/computation/expressions.py index e2acd9a2c97c2..a2c3a706ae29c 100644 --- a/pandas/core/computation/expressions.py +++ b/pandas/core/computation/expressions.py @@ -65,23 +65,23 @@ def set_numexpr_threads(n=None) -> None: ne.set_num_threads(n) -def _evaluate_standard(op, op_str, a, b): +def _evaluate_standard(op, op_str, left_op, right_op): """ Standard evaluation. """ if _TEST_MODE: _store_test_result(False) - return op(a, b) + return op(left_op, right_op) -def _can_use_numexpr(op, op_str, a, b, dtype_check) -> bool: - """return a boolean if we WILL be using numexpr""" +def _can_use_numexpr(op, op_str, left_op, right_op, dtype_check) -> bool: + """return left_op boolean if we WILL be using numexpr""" if op_str is not None: # required min elements (otherwise we are adding overhead) - if a.size > _MIN_ELEMENTS: + if left_op.size > _MIN_ELEMENTS: # check for dtype compatibility dtypes: set[str] = set() - for o in [a, b]: + for o in [left_op, right_op]: # ndarray and Series Case if hasattr(o, "dtype"): dtypes |= {o.dtype.name} @@ -93,22 +93,22 @@ def _can_use_numexpr(op, op_str, a, b, dtype_check) -> bool: return False -def _evaluate_numexpr(op, op_str, a, b): +def _evaluate_numexpr(op, op_str, left_op, right_op): result = None - if _can_use_numexpr(op, op_str, a, b, "evaluate"): + if _can_use_numexpr(op, op_str, left_op, right_op, "evaluate"): is_reversed = op.__name__.strip("_").startswith("r") if is_reversed: # we were originally called by a reversed op method - a, b = b, a + left_op, right_op = right_op, left_op - a_value = a - b_value = b + left_value = left_op + right_value = right_op try: result = ne.evaluate( - f"a_value {op_str} b_value", - local_dict={"a_value": a_value, "b_value": b_value}, + f"left_value {op_str} right_value", + local_dict={"left_value": left_value, "right_value": right_op}, casting="safe", ) except TypeError: @@ -116,20 +116,20 @@ def _evaluate_numexpr(op, op_str, a, b): # (https://github.com/pydata/numexpr/issues/379) pass except NotImplementedError: - if _bool_arith_fallback(op_str, a, b): + if _bool_arith_fallback(op_str, left_op, right_op): pass else: raise if is_reversed: # reverse order to original for fallback - a, b = b, a + left_op, right_op = right_op, left_op if _TEST_MODE: _store_test_result(result is not None) if result is None: - result = _evaluate_standard(op, op_str, a, b) + result = _evaluate_standard(op, op_str, left_op, right_op) return result @@ -170,24 +170,24 @@ def _evaluate_numexpr(op, op_str, a, b): } -def _where_standard(cond, a, b): +def _where_standard(cond, left_op, right_op): # Caller is responsible for extracting ndarray if necessary - return np.where(cond, a, b) + return np.where(cond, left_op, right_op) -def _where_numexpr(cond, a, b): +def _where_numexpr(cond, left_op, right_op): # Caller is responsible for extracting ndarray if necessary result = None - if _can_use_numexpr(None, "where", a, b, "where"): + if _can_use_numexpr(None, "where", left_op, right_op, "where"): result = ne.evaluate( "where(cond_value, a_value, b_value)", - local_dict={"cond_value": cond, "a_value": a, "b_value": b}, + local_dict={"cond_value": cond, "a_value": left_op, "b_value": right_op}, casting="safe", ) if result is None: - result = _where_standard(cond, a, b) + result = _where_standard(cond, left_op, right_op) return result @@ -206,13 +206,13 @@ def _has_bool_dtype(x): _BOOL_OP_UNSUPPORTED = {"+": "|", "*": "&", "-": "^"} -def _bool_arith_fallback(op_str, a, b) -> bool: +def _bool_arith_fallback(op_str, left_op, right_op) -> bool: """ Check if we should fallback to the python `_evaluate_standard` in case of an unsupported operation by numexpr, which is the case for some boolean ops. """ - if _has_bool_dtype(a) and _has_bool_dtype(b): + if _has_bool_dtype(left_op) and _has_bool_dtype(right_op): if op_str in _BOOL_OP_UNSUPPORTED: warnings.warn( f"evaluating in Python space because the {op_str!r} " @@ -224,15 +224,15 @@ def _bool_arith_fallback(op_str, a, b) -> bool: return False -def evaluate(op, a, b, use_numexpr: bool = True): +def evaluate(op, left_op, right_op, use_numexpr: bool = True): """ - Evaluate and return the expression of the op on a and b. + Evaluate and return the expression of the op on left_op and right_op. Parameters ---------- op : the actual operand - a : left operand - b : right operand + left_op : left operand + right_op : right operand use_numexpr : bool, default True Whether to try to use numexpr. """ @@ -240,24 +240,24 @@ def evaluate(op, a, b, use_numexpr: bool = True): if op_str is not None: if use_numexpr: # error: "None" not callable - return _evaluate(op, op_str, a, b) # type: ignore[misc] - return _evaluate_standard(op, op_str, a, b) + return _evaluate(op, op_str, left_op, right_op) # type: ignore[misc] + return _evaluate_standard(op, op_str, left_op, right_op) -def where(cond, a, b, use_numexpr: bool = True): +def where(cond, left_op, right_op, use_numexpr: bool = True): """ - Evaluate the where condition cond on a and b. + Evaluate the where condition cond on left_op and right_op. Parameters ---------- cond : np.ndarray[bool] - a : return if cond is True - b : return if cond is False + left_op : return if cond is True + right_op : return if cond is False use_numexpr : bool, default True Whether to try to use numexpr. """ assert _where is not None - return _where(cond, a, b) if use_numexpr else _where_standard(cond, a, b) + return _where(cond, left_op, right_op) if use_numexpr else _where_standard(cond, left_op, right_op) def set_test_mode(v: bool = True) -> None: From 2d774e7f3e54ff94b03c7500c5ec756b16e47d10 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Tue, 10 Dec 2024 02:37:57 +0800 Subject: [PATCH 144/266] DOC: fix broken link in Resampler.bfill (#60524) --- pandas/core/resample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index fdfb9f21bdb9f..0d1541bbb3afa 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -694,7 +694,7 @@ def bfill(self, limit: int | None = None): References ---------- - .. [1] https://en.wikipedia.org/wiki/Imputation_(statistics) + .. [1] https://en.wikipedia.org/wiki/Imputation_%28statistics%29 Examples -------- From f3b798545160fc878e87d05947e0180df031ecb6 Mon Sep 17 00:00:00 2001 From: sunlight <138234530+sunlight798@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:38:39 +0800 Subject: [PATCH 145/266] DOC: Fix docstrings for errors (#60523) * DOC: Fix docstrings for errors * DOC: Fix docstrings for errors --- ci/code_checks.sh | 3 --- pandas/errors/__init__.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index adc5bc9a01bdd..7bc220acdd74c 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -95,9 +95,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ - -i "pandas.errors.NullFrequencyError SA01" \ - -i "pandas.errors.NumbaUtilError SA01" \ - -i "pandas.errors.PerformanceWarning SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.io.json.build_table_schema PR07,RT03,SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 1de6f06ef316c..cd31ec30522c3 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -45,6 +45,11 @@ class NullFrequencyError(ValueError): Particularly ``DatetimeIndex.shift``, ``TimedeltaIndex.shift``, ``PeriodIndex.shift``. + See Also + -------- + Index.shift : Shift values of Index. + Series.shift : Shift values of Series. + Examples -------- >>> df = pd.DatetimeIndex(["2011-01-01 10:00", "2011-01-01"], freq=None) @@ -58,6 +63,12 @@ class PerformanceWarning(Warning): """ Warning raised when there is a possible performance impact. + See Also + -------- + DataFrame.set_index : Set the DataFrame index using existing columns. + DataFrame.loc : Access a group of rows and columns by label(s) \ + or a boolean array. + Examples -------- >>> df = pd.DataFrame( @@ -385,6 +396,13 @@ class NumbaUtilError(Exception): """ Error raised for unsupported Numba engine routines. + See Also + -------- + DataFrame.groupby : Group DataFrame using a mapper or by a Series of columns. + Series.groupby : Group Series using a mapper or by a Series of columns. + DataFrame.agg : Aggregate using one or more operations over the specified axis. + Series.agg : Aggregate using one or more operations over the specified axis. + Examples -------- >>> df = pd.DataFrame( From b667fdf8dd4e1ea8bf2e001fbfe23beeb4735a51 Mon Sep 17 00:00:00 2001 From: Aditya Ghosh <72292940+Nanashi-bot@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:10:54 +0530 Subject: [PATCH 146/266] Add extended summary for fullmatch, match, pad, repeat, slice and slice_replace (#60520) Add extended summary for fullmatch, match, pad, repeat, slice and slice_replace functions --- pandas/core/strings/accessor.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pandas/core/strings/accessor.py b/pandas/core/strings/accessor.py index 05e1a36877e06..c68b6303661b9 100644 --- a/pandas/core/strings/accessor.py +++ b/pandas/core/strings/accessor.py @@ -1374,6 +1374,11 @@ def match(self, pat: str, case: bool = True, flags: int = 0, na=lib.no_default): """ Determine if each string starts with a match of a regular expression. + Determines whether each string in the Series or Index starts with a + match to a specified regular expression. This function is especially + useful for validating prefixes, such as ensuring that codes, tags, or + identifiers begin with a specific pattern. + Parameters ---------- pat : str @@ -1419,6 +1424,11 @@ def fullmatch(self, pat, case: bool = True, flags: int = 0, na=lib.no_default): """ Determine if each string entirely matches a regular expression. + Checks if each string in the Series or Index fully matches the + specified regular expression pattern. This function is useful when the + requirement is for an entire string to conform to a pattern, such as + validating formats like phone numbers or email addresses. + Parameters ---------- pat : str @@ -1647,6 +1657,10 @@ def repeat(self, repeats): """ Duplicate each string in the Series or Index. + Duplicates each string in the Series or Index, either by applying the + same repeat count to all elements or by using different repeat values + for each element. + Parameters ---------- repeats : int or sequence of int @@ -1710,6 +1724,12 @@ def pad( """ Pad strings in the Series/Index up to width. + This function pads strings in a Series or Index to a specified width, + filling the extra space with a character of your choice. It provides + flexibility in positioning the padding, allowing it to be added to the + left, right, or both sides. This is useful for formatting strings to + align text or ensure consistent string lengths in data processing. + Parameters ---------- width : int @@ -1920,6 +1940,11 @@ def slice(self, start=None, stop=None, step=None): """ Slice substrings from each element in the Series or Index. + Slicing substrings from strings in a Series or Index helps extract + specific portions of data, making it easier to analyze or manipulate + text. This is useful for tasks like parsing structured text fields or + isolating parts of strings with a consistent format. + Parameters ---------- start : int, optional @@ -1996,6 +2021,11 @@ def slice_replace(self, start=None, stop=None, repl=None): """ Replace a positional slice of a string with another value. + This function allows replacing specific parts of a string in a Series + or Index by specifying start and stop positions. It is useful for + modifying substrings in a controlled way, such as updating sections of + text based on their positions or patterns. + Parameters ---------- start : int, optional From 6cbe941c4512b86156eb06a26d253f4aa30b0304 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 9 Dec 2024 10:46:14 -0800 Subject: [PATCH 147/266] BUG: Fix float32 precision issues in pd.to_datetime (#60510) * BUG: Fix float32 precision issues in pd.to_datetime * BUG: Add note to whatsnew --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/tools/datetimes.py | 5 +++++ pandas/tests/tools/test_to_datetime.py | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index b799b7ea5cb39..2013f81d4da18 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -626,6 +626,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.union` and :meth:`DatetimeIndex.intersection` when ``unit`` was non-nanosecond (:issue:`59036`) - Bug in :meth:`Series.dt.microsecond` producing incorrect results for pyarrow backed :class:`Series`. (:issue:`59154`) - Bug in :meth:`to_datetime` not respecting dayfirst if an uncommon date string was passed. (:issue:`58859`) +- Bug in :meth:`to_datetime` on float32 df with year, month, day etc. columns leads to precision issues and incorrect result. (:issue:`60506`) - Bug in :meth:`to_datetime` reports incorrect index in case of any failure scenario. (:issue:`58298`) - Bug in :meth:`to_datetime` wrongly converts when ``arg`` is a ``np.datetime64`` object with unit of ``ps``. (:issue:`60341`) - Bug in setting scalar values with mismatched resolution into arrays with non-nanosecond ``datetime64``, ``timedelta64`` or :class:`DatetimeTZDtype` incorrectly truncating those scalars (:issue:`56410`) diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index 4680a63bf57a1..30487de7bafd5 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -44,6 +44,7 @@ from pandas.core.dtypes.common import ( ensure_object, is_float, + is_float_dtype, is_integer, is_integer_dtype, is_list_like, @@ -1153,6 +1154,10 @@ def coerce(values): # we allow coercion to if errors allows values = to_numeric(values, errors=errors) + # prevent prevision issues in case of float32 # GH#60506 + if is_float_dtype(values.dtype): + values = values.astype("float64") + # prevent overflow in case of int8 or int16 if is_integer_dtype(values.dtype): values = values.astype("int64") diff --git a/pandas/tests/tools/test_to_datetime.py b/pandas/tests/tools/test_to_datetime.py index b73839f406a29..74b051aec71a4 100644 --- a/pandas/tests/tools/test_to_datetime.py +++ b/pandas/tests/tools/test_to_datetime.py @@ -2084,6 +2084,18 @@ def test_dataframe_str_dtype(self, df, cache): ) tm.assert_series_equal(result, expected) + def test_dataframe_float32_dtype(self, df, cache): + # GH#60506 + # coerce to float64 + result = to_datetime(df.astype(np.float32), cache=cache) + expected = Series( + [ + Timestamp("20150204 06:58:10.001002003"), + Timestamp("20160305 07:59:11.001002003"), + ] + ) + tm.assert_series_equal(result, expected) + def test_dataframe_coerce(self, cache): # passing coerce df2 = DataFrame({"year": [2015, 2016], "month": [2, 20], "day": [4, 5]}) From ca91dd4c39a02c0026b98c16c56996f81506e004 Mon Sep 17 00:00:00 2001 From: jmalp <75514361+jmalp@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:54:40 -0800 Subject: [PATCH 148/266] DOC: fix docstrings validation for pandas.core.groupby.DataFrameGroupBy.boxplot (#60509) * fix docstrings validation for pandas.core.groupby.DataFrameGroupBy.boxplot * fix trailing whitespace * fix the error "pandas.Series.plot in `See Also` section does not need `pandas` prefix, use Series.plot instead." * fix the error "pandas.DataFrame.boxplot in `See Also` section does not need `pandas` prefix, use DataFrame.boxplot instead." --- ci/code_checks.sh | 1 - pandas/plotting/_core.py | 26 +++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 7bc220acdd74c..fdaffb5a9c9ef 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -84,7 +84,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.IntervalArray.length SA01" \ -i "pandas.arrays.NumpyExtensionArray SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ - -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.max PR01,RT03,SA01" \ diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index fbf9009cedc40..aee872f9ae50a 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -570,18 +570,23 @@ def boxplot_frame_groupby( Parameters ---------- - grouped : Grouped DataFrame + grouped : DataFrameGroupBy + The grouped DataFrame object over which to create the box plots. subplots : bool * ``False`` - no subplots will be used * ``True`` - create a subplot for each group. - column : column name or list of names, or vector Can be any valid input to groupby. fontsize : float or str - rot : label rotation angle - grid : Setting this to True will show the grid + Font size for the labels. + rot : float + Rotation angle of labels (in degrees) on the x-axis. + grid : bool + Whether to show grid lines on the plot. ax : Matplotlib axis object, default None - figsize : A tuple (width, height) in inches + The axes on which to draw the plots. If None, uses the current axes. + figsize : tuple of (float, float) + The figure size in inches (width, height). layout : tuple (optional) The layout of the plot: (rows, columns). sharex : bool, default False @@ -599,8 +604,15 @@ def boxplot_frame_groupby( Returns ------- - dict of key/value = group key/DataFrame.boxplot return value - or DataFrame.boxplot return value in case subplots=figures=False + dict or DataFrame.boxplot return value + If ``subplots=True``, returns a dictionary of group keys to the boxplot + return values. If ``subplots=False``, returns the boxplot return value + of a single DataFrame. + + See Also + -------- + DataFrame.boxplot : Create a box plot from a DataFrame. + Series.plot : Plot a Series. Examples -------- From 719fc0fcbcda23a79156ccfc990228df0851452f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:15:34 -0800 Subject: [PATCH 149/266] FIX: ruff checks in expressions/pytables (#60541) * FIX: ruff checks in expressions/pytables * swap condition * more pre-commit --- pandas/core/computation/expressions.py | 7 +++++-- pandas/core/computation/pytables.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pandas/core/computation/expressions.py b/pandas/core/computation/expressions.py index a2c3a706ae29c..5a5fad0d83d7a 100644 --- a/pandas/core/computation/expressions.py +++ b/pandas/core/computation/expressions.py @@ -108,7 +108,7 @@ def _evaluate_numexpr(op, op_str, left_op, right_op): try: result = ne.evaluate( f"left_value {op_str} right_value", - local_dict={"left_value": left_value, "right_value": right_op}, + local_dict={"left_value": left_value, "right_value": right_value}, casting="safe", ) except TypeError: @@ -257,7 +257,10 @@ def where(cond, left_op, right_op, use_numexpr: bool = True): Whether to try to use numexpr. """ assert _where is not None - return _where(cond, left_op, right_op) if use_numexpr else _where_standard(cond, left_op, right_op) + if use_numexpr: + return _where(cond, left_op, right_op) + else: + return _where_standard(cond, left_op, right_op) def set_test_mode(v: bool = True) -> None: diff --git a/pandas/core/computation/pytables.py b/pandas/core/computation/pytables.py index 4a75acce46632..166c9d47294cd 100644 --- a/pandas/core/computation/pytables.py +++ b/pandas/core/computation/pytables.py @@ -274,7 +274,9 @@ def stringify(value): # string quoting return TermValue(conv_val, stringify(conv_val), "string") else: - raise TypeError(f"Cannot compare {conv_val} of type {type(conv_val)} to {kind} column") + raise TypeError( + f"Cannot compare {conv_val} of type {type(conv_val)} to {kind} column" + ) def convert_values(self) -> None: pass From 38224dd910e57fef7a3b0f4e85d67d8e690d6897 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:20:10 -0800 Subject: [PATCH 150/266] CI/TST: Use tm.external_error_raised for test_from_arrow_respecting_given_dtype_unsafe (#60544) --- pandas/tests/extension/test_arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index c6ac6368f2770..6dd1f3f15bc15 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -1647,7 +1647,7 @@ def test_from_arrow_respecting_given_dtype(): def test_from_arrow_respecting_given_dtype_unsafe(): array = pa.array([1.5, 2.5], type=pa.float64()) - with pytest.raises(pa.ArrowInvalid, match="Float value 1.5 was truncated"): + with tm.external_error_raised(pa.ArrowInvalid): array.to_pandas(types_mapper={pa.float64(): ArrowDtype(pa.int64())}.get) From 13e2df0d7074cbc1a8d59d7044d5bfcb69147a3d Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:11:30 -0800 Subject: [PATCH 151/266] CI: Ignore prompting in test-arm when apt-get installing (#60546) * CI: Ignore prompting in test-arm when apt-get installing * CI: Ignore prompting in test-arm when apt-get installing * Skip the apt-get install all together --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9c986e5b1b054..139ea9d220453 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,7 +34,6 @@ jobs: fi python -m pip install --no-build-isolation -ve . -Csetup-args="--werror" PATH=$HOME/miniconda3/envs/pandas-dev/bin:$HOME/miniconda3/condabin:$PATH - sudo apt-get update && sudo apt-get install -y libegl1 libopengl0 ci/run_tests.sh test-linux-musl: docker: From c52846ff94d51ce5940928c199da00f403bc8138 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:00:31 -0800 Subject: [PATCH 152/266] TST: filter possible RuntimeWarning in tests (#60553) * Ignore possible RuntimeWarning in _hash_ndarray * Revert "Ignore possible RuntimeWarning in _hash_ndarray" This reverts commit 1c9a763a0a6e7b6ba4dcfd364a3fcb506883ba16. * Just filter warnings instead * Fix typos --- pandas/tests/extension/test_interval.py | 25 +++++++++++++++++++ pandas/tests/frame/methods/test_to_numpy.py | 4 +++ pandas/tests/frame/test_constructors.py | 3 +++ pandas/tests/indexes/interval/test_astype.py | 6 +++++ pandas/tests/indexes/interval/test_formats.py | 3 +++ .../tests/indexes/interval/test_indexing.py | 3 +++ pandas/tests/indexes/test_setops.py | 1 + pandas/tests/io/excel/test_writers.py | 3 +++ pandas/tests/reshape/test_cut.py | 1 + 9 files changed, 49 insertions(+) diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index ec979ac6d22dc..011bf0b2016b2 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -101,6 +101,31 @@ def test_fillna_limit_series(self, data_missing): def test_fillna_length_mismatch(self, data_missing): super().test_fillna_length_mismatch(data_missing) + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) + def test_hash_pandas_object(self, data): + super().test_hash_pandas_object(data) + + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) + def test_hash_pandas_object_works(self, data, as_frame): + super().test_hash_pandas_object_works(data, as_frame) + + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) + @pytest.mark.parametrize("engine", ["c", "python"]) + def test_EA_types(self, engine, data, request): + super().test_EA_types(engine, data, request) + + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) + def test_astype_str(self, data): + super().test_astype_str(data) + # TODO: either belongs in tests.arrays.interval or move into base tests. def test_fillna_non_scalar_raises(data_missing): diff --git a/pandas/tests/frame/methods/test_to_numpy.py b/pandas/tests/frame/methods/test_to_numpy.py index d38bc06260a0e..36088cceb13f1 100644 --- a/pandas/tests/frame/methods/test_to_numpy.py +++ b/pandas/tests/frame/methods/test_to_numpy.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from pandas import ( DataFrame, @@ -31,6 +32,9 @@ def test_to_numpy_copy(self): # and that can be respected because we are already numpy-float assert df.to_numpy(copy=False).base is df.values.base + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) def test_to_numpy_mixed_dtype_to_str(self): # https://github.com/pandas-dev/pandas/issues/35455 df = DataFrame([[Timestamp("2020-01-01 00:00:00"), 100.0]]) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 3d8213cb3d11a..9b6080603f0c9 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -2404,6 +2404,9 @@ def test_construct_with_two_categoricalindex_series(self): ) tm.assert_frame_equal(result, expected) + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) def test_constructor_series_nonexact_categoricalindex(self): # GH 42424 ser = Series(range(100)) diff --git a/pandas/tests/indexes/interval/test_astype.py b/pandas/tests/indexes/interval/test_astype.py index 59c555b9644a1..dde5f38074efb 100644 --- a/pandas/tests/indexes/interval/test_astype.py +++ b/pandas/tests/indexes/interval/test_astype.py @@ -186,6 +186,12 @@ def test_subtype_datetimelike(self, index, subtype): with pytest.raises(TypeError, match=msg): index.astype(dtype) + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) + def test_astype_category(self, index): + super().test_astype_category(index) + class TestDatetimelikeSubtype(AstypeTests): """Tests specific to IntervalIndex with datetime-like subtype""" diff --git a/pandas/tests/indexes/interval/test_formats.py b/pandas/tests/indexes/interval/test_formats.py index f858ae137ca4e..73bbfc91028b3 100644 --- a/pandas/tests/indexes/interval/test_formats.py +++ b/pandas/tests/indexes/interval/test_formats.py @@ -59,6 +59,9 @@ def test_repr_floats(self): expected = "(329.973, 345.137] 1\n(345.137, 360.191] 2\ndtype: int64" assert result == expected + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) @pytest.mark.parametrize( "tuples, closed, expected_data", [ diff --git a/pandas/tests/indexes/interval/test_indexing.py b/pandas/tests/indexes/interval/test_indexing.py index 787461b944bd0..5783a16e81d37 100644 --- a/pandas/tests/indexes/interval/test_indexing.py +++ b/pandas/tests/indexes/interval/test_indexing.py @@ -340,6 +340,9 @@ def test_get_indexer_categorical(self, target, ordered): expected = index.get_indexer(target) tm.assert_numpy_array_equal(result, expected) + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) def test_get_indexer_categorical_with_nans(self): # GH#41934 nans in both index and in target ii = IntervalIndex.from_breaks(range(5)) diff --git a/pandas/tests/indexes/test_setops.py b/pandas/tests/indexes/test_setops.py index 5f934ca3e6e83..58b69d79c65ce 100644 --- a/pandas/tests/indexes/test_setops.py +++ b/pandas/tests/indexes/test_setops.py @@ -525,6 +525,7 @@ def test_intersection_difference_match_empty(self, index, sort): tm.assert_index_equal(inter, diff, exact=True) +@pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") @pytest.mark.filterwarnings(r"ignore:PeriodDtype\[B\] is deprecated:FutureWarning") @pytest.mark.parametrize( "method", ["intersection", "union", "difference", "symmetric_difference"] diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index 18948de72200a..ced4feb9e7eb9 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -800,6 +800,9 @@ def test_excel_date_datetime_format(self, ext, tmp_excel, tmp_path): # we need to use df_expected to check the result. tm.assert_frame_equal(rs2, df_expected) + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in cast:RuntimeWarning" + ) def test_to_excel_interval_no_labels(self, tmp_excel, using_infer_string): # see gh-19242 # diff --git a/pandas/tests/reshape/test_cut.py b/pandas/tests/reshape/test_cut.py index d8bb4fba1e1fe..63332fe4658e5 100644 --- a/pandas/tests/reshape/test_cut.py +++ b/pandas/tests/reshape/test_cut.py @@ -733,6 +733,7 @@ def test_cut_with_duplicated_index_lowest_included(): tm.assert_series_equal(result, expected) +@pytest.mark.filterwarnings("ignore:invalid value encountered in cast:RuntimeWarning") def test_cut_with_nonexact_categorical_indices(): # GH 42424 From 069253de4de91a8d73434ea1d5954ad20abb027a Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Sat, 14 Dec 2024 00:37:20 +0530 Subject: [PATCH 153/266] DOC: fix SA01,ES01 for pandas.arrays.IntervalArray.length (#60556) DOC: fix SA01 for pandas.arrays.IntervalArray.length --- ci/code_checks.sh | 1 - pandas/core/arrays/interval.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index fdaffb5a9c9ef..74f5de78856d5 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -81,7 +81,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ - -i "pandas.arrays.IntervalArray.length SA01" \ -i "pandas.arrays.NumpyExtensionArray SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index bbbf1d9ca60bd..0bf2089df5f85 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1306,6 +1306,20 @@ def length(self) -> Index: """ Return an Index with entries denoting the length of each Interval. + The length of an interval is calculated as the difference between + its `right` and `left` bounds. This property is particularly useful + when working with intervals where the size of the interval is an important + attribute, such as in time-series analysis or spatial data analysis. + + See Also + -------- + arrays.IntervalArray.left : Return the left endpoints of each Interval in + the IntervalArray as an Index. + arrays.IntervalArray.right : Return the right endpoints of each Interval in + the IntervalArray as an Index. + arrays.IntervalArray.mid : Return the midpoint of each Interval in the + IntervalArray as an Index. + Examples -------- From 9501650e22767f8502a1e3edecfaf17c5769f150 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Fri, 13 Dec 2024 13:15:38 -0800 Subject: [PATCH 154/266] ENH: Support NamedAggs in kwargs in Rolling/Expanding/EWM agg method (#60549) * ENH: Support NamedAggs in kwargs in Rolling/Expanding/EWM agg method * Pre-commit fix * Fix typing * Fix typing retry * Fix typing retry 2 * Update pandas/core/window/rolling.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> * Add type ignore --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/window/ewm.py | 4 +- pandas/core/window/expanding.py | 2 +- pandas/core/window/rolling.py | 15 +++-- pandas/tests/window/test_groupby.py | 96 +++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 2013f81d4da18..005818b0779e6 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -56,6 +56,7 @@ Other enhancements - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) - :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`) - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) +- :meth:`Rolling.agg`, :meth:`Expanding.agg` and :meth:`ExponentialMovingWindow.agg` now accept :class:`NamedAgg` aggregations through ``**kwargs`` (:issue:`28333`) - :meth:`Series.map` can now accept kwargs to pass on to func (:issue:`59814`) - :meth:`pandas.concat` will raise a ``ValueError`` when ``ignore_index=True`` and ``keys`` is not ``None`` (:issue:`59274`) - :meth:`str.get_dummies` now accepts a ``dtype`` parameter to specify the dtype of the resulting DataFrame (:issue:`47872`) diff --git a/pandas/core/window/ewm.py b/pandas/core/window/ewm.py index 43a3c03b6cef9..73e4de6ea6208 100644 --- a/pandas/core/window/ewm.py +++ b/pandas/core/window/ewm.py @@ -490,7 +490,7 @@ def online( klass="Series/Dataframe", axis="", ) - def aggregate(self, func, *args, **kwargs): + def aggregate(self, func=None, *args, **kwargs): return super().aggregate(func, *args, **kwargs) agg = aggregate @@ -981,7 +981,7 @@ def reset(self) -> None: """ self._mean.reset() - def aggregate(self, func, *args, **kwargs): + def aggregate(self, func=None, *args, **kwargs): raise NotImplementedError("aggregate is not implemented.") def std(self, bias: bool = False, *args, **kwargs): diff --git a/pandas/core/window/expanding.py b/pandas/core/window/expanding.py index 4bf77b3d38689..bff3a1660eba9 100644 --- a/pandas/core/window/expanding.py +++ b/pandas/core/window/expanding.py @@ -167,7 +167,7 @@ def _get_window_indexer(self) -> BaseIndexer: klass="Series/Dataframe", axis="", ) - def aggregate(self, func, *args, **kwargs): + def aggregate(self, func=None, *args, **kwargs): return super().aggregate(func, *args, **kwargs) agg = aggregate diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 4446b21976069..385ffb901acf0 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -44,7 +44,10 @@ from pandas.core._numba import executor from pandas.core.algorithms import factorize -from pandas.core.apply import ResamplerWindowApply +from pandas.core.apply import ( + ResamplerWindowApply, + reconstruct_func, +) from pandas.core.arrays import ExtensionArray from pandas.core.base import SelectionMixin import pandas.core.common as com @@ -646,8 +649,12 @@ def _numba_apply( out = obj._constructor(result, index=index, columns=columns) return self._resolve_output(out, obj) - def aggregate(self, func, *args, **kwargs): + def aggregate(self, func=None, *args, **kwargs): + relabeling, func, columns, order = reconstruct_func(func, **kwargs) result = ResamplerWindowApply(self, func, args=args, kwargs=kwargs).agg() + if isinstance(result, ABCDataFrame) and relabeling: + result = result.iloc[:, order] + result.columns = columns # type: ignore[union-attr] if result is None: return self.apply(func, raw=False, args=args, kwargs=kwargs) return result @@ -1239,7 +1246,7 @@ def calc(x): klass="Series/DataFrame", axis="", ) - def aggregate(self, func, *args, **kwargs): + def aggregate(self, func=None, *args, **kwargs): result = ResamplerWindowApply(self, func, args=args, kwargs=kwargs).agg() if result is None: # these must apply directly @@ -1951,7 +1958,7 @@ def _raise_monotonic_error(self, msg: str): klass="Series/Dataframe", axis="", ) - def aggregate(self, func, *args, **kwargs): + def aggregate(self, func=None, *args, **kwargs): return super().aggregate(func, *args, **kwargs) agg = aggregate diff --git a/pandas/tests/window/test_groupby.py b/pandas/tests/window/test_groupby.py index 4d37c6d57f788..f8e804bf434e9 100644 --- a/pandas/tests/window/test_groupby.py +++ b/pandas/tests/window/test_groupby.py @@ -6,6 +6,7 @@ DatetimeIndex, Index, MultiIndex, + NamedAgg, Series, Timestamp, date_range, @@ -489,6 +490,36 @@ def test_groupby_rolling_subset_with_closed(self): ) tm.assert_series_equal(result, expected) + def test_groupby_rolling_agg_namedagg(self): + # GH#28333 + df = DataFrame( + { + "kind": ["cat", "dog", "cat", "dog", "cat", "dog"], + "height": [9.1, 6.0, 9.5, 34.0, 12.0, 8.0], + "weight": [7.9, 7.5, 9.9, 198.0, 10.0, 42.0], + } + ) + result = ( + df.groupby("kind") + .rolling(2) + .agg( + total_weight=NamedAgg(column="weight", aggfunc=sum), + min_height=NamedAgg(column="height", aggfunc=min), + ) + ) + expected = DataFrame( + { + "total_weight": [np.nan, 17.8, 19.9, np.nan, 205.5, 240.0], + "min_height": [np.nan, 9.1, 9.5, np.nan, 6.0, 8.0], + }, + index=MultiIndex( + [["cat", "dog"], [0, 1, 2, 3, 4, 5]], + [[0, 0, 0, 1, 1, 1], [0, 2, 4, 1, 3, 5]], + names=["kind", None], + ), + ) + tm.assert_frame_equal(result, expected) + def test_groupby_subset_rolling_subset_with_closed(self): # GH 35549 df = DataFrame( @@ -1134,6 +1165,36 @@ def test_expanding_apply(self, raw, frame): expected.index = expected_index tm.assert_frame_equal(result, expected) + def test_groupby_expanding_agg_namedagg(self): + # GH#28333 + df = DataFrame( + { + "kind": ["cat", "dog", "cat", "dog", "cat", "dog"], + "height": [9.1, 6.0, 9.5, 34.0, 12.0, 8.0], + "weight": [7.9, 7.5, 9.9, 198.0, 10.0, 42.0], + } + ) + result = ( + df.groupby("kind") + .expanding(1) + .agg( + total_weight=NamedAgg(column="weight", aggfunc=sum), + min_height=NamedAgg(column="height", aggfunc=min), + ) + ) + expected = DataFrame( + { + "total_weight": [7.9, 17.8, 27.8, 7.5, 205.5, 247.5], + "min_height": [9.1, 9.1, 9.1, 6.0, 6.0, 6.0], + }, + index=MultiIndex( + [["cat", "dog"], [0, 1, 2, 3, 4, 5]], + [[0, 0, 0, 1, 1, 1], [0, 2, 4, 1, 3, 5]], + names=["kind", None], + ), + ) + tm.assert_frame_equal(result, expected) + class TestEWM: @pytest.mark.parametrize( @@ -1162,6 +1223,41 @@ def test_methods(self, method, expected_data): ) tm.assert_frame_equal(result, expected) + def test_groupby_ewm_agg_namedagg(self): + # GH#28333 + df = DataFrame({"A": ["a"] * 4, "B": range(4)}) + result = ( + df.groupby("A") + .ewm(com=1.0) + .agg( + B_mean=NamedAgg(column="B", aggfunc="mean"), + B_std=NamedAgg(column="B", aggfunc="std"), + B_var=NamedAgg(column="B", aggfunc="var"), + ) + ) + expected = DataFrame( + { + "B_mean": [ + 0.0, + 0.6666666666666666, + 1.4285714285714286, + 2.2666666666666666, + ], + "B_std": [np.nan, 0.707107, 0.963624, 1.177164], + "B_var": [np.nan, 0.5, 0.9285714285714286, 1.3857142857142857], + }, + index=MultiIndex.from_tuples( + [ + ("a", 0), + ("a", 1), + ("a", 2), + ("a", 3), + ], + names=["A", None], + ), + ) + tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize( "method, expected_data", [["corr", [np.nan, 1.0, 1.0, 1]], ["cov", [np.nan, 0.5, 0.928571, 1.385714]]], From b0192c70610a9db593968374ea60d189daaaccc7 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:16:51 -0500 Subject: [PATCH 155/266] CLN: Remove deprecations of groupby.fillna in tests (#60565) --- pandas/tests/groupby/__init__.py | 4 +-- pandas/tests/groupby/test_categorical.py | 5 +--- pandas/tests/groupby/test_groupby.py | 30 +++---------------- pandas/tests/groupby/test_groupby_subclass.py | 8 ++--- pandas/tests/groupby/test_numeric_only.py | 7 ++--- pandas/tests/groupby/test_raises.py | 24 +++------------ .../tests/groupby/transform/test_transform.py | 9 +----- 7 files changed, 18 insertions(+), 69 deletions(-) diff --git a/pandas/tests/groupby/__init__.py b/pandas/tests/groupby/__init__.py index 446d9da437771..79046cd7ed415 100644 --- a/pandas/tests/groupby/__init__.py +++ b/pandas/tests/groupby/__init__.py @@ -2,7 +2,7 @@ def get_groupby_method_args(name, obj): """ Get required arguments for a groupby method. - When parametrizing a test over groupby methods (e.g. "sum", "mean", "fillna"), + When parametrizing a test over groupby methods (e.g. "sum", "mean"), it is often the case that arguments are required for certain methods. Parameters @@ -16,7 +16,7 @@ def get_groupby_method_args(name, obj): ------- A tuple of required arguments for the method. """ - if name in ("nth", "fillna", "take"): + if name in ("nth", "take"): return (0,) if name == "quantile": return (0.5,) diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py index 6d84dae1d25d8..fffaee40a7d5c 100644 --- a/pandas/tests/groupby/test_categorical.py +++ b/pandas/tests/groupby/test_categorical.py @@ -1963,10 +1963,7 @@ def test_category_order_transformer( df = df.set_index(keys) args = get_groupby_method_args(transformation_func, df) gb = df.groupby(keys, as_index=as_index, sort=sort, observed=observed) - warn = FutureWarning if transformation_func == "fillna" else None - msg = "DataFrameGroupBy.fillna is deprecated" - with tm.assert_produces_warning(warn, match=msg): - op_result = getattr(gb, transformation_func)(*args) + op_result = getattr(gb, transformation_func)(*args) result = op_result.index.get_level_values("a").categories expected = Index([1, 4, 3, 2]) tm.assert_index_equal(result, expected) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 702bbfef2be3b..e6c7eede1a401 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -2098,36 +2098,14 @@ def test_group_on_empty_multiindex(transformation_func, request): df["col_3"] = df["col_3"].astype(int) df["col_4"] = df["col_4"].astype(int) df = df.set_index(["col_1", "col_2"]) - if transformation_func == "fillna": - args = ("ffill",) - else: - args = () - warn = FutureWarning if transformation_func == "fillna" else None - warn_msg = "DataFrameGroupBy.fillna is deprecated" - with tm.assert_produces_warning(warn, match=warn_msg): - result = df.iloc[:0].groupby(["col_1"]).transform(transformation_func, *args) - with tm.assert_produces_warning(warn, match=warn_msg): - expected = df.groupby(["col_1"]).transform(transformation_func, *args).iloc[:0] + result = df.iloc[:0].groupby(["col_1"]).transform(transformation_func) + expected = df.groupby(["col_1"]).transform(transformation_func).iloc[:0] if transformation_func in ("diff", "shift"): expected = expected.astype(int) tm.assert_equal(result, expected) - warn_msg = "SeriesGroupBy.fillna is deprecated" - with tm.assert_produces_warning(warn, match=warn_msg): - result = ( - df["col_3"] - .iloc[:0] - .groupby(["col_1"]) - .transform(transformation_func, *args) - ) - warn_msg = "SeriesGroupBy.fillna is deprecated" - with tm.assert_produces_warning(warn, match=warn_msg): - expected = ( - df["col_3"] - .groupby(["col_1"]) - .transform(transformation_func, *args) - .iloc[:0] - ) + result = df["col_3"].iloc[:0].groupby(["col_1"]).transform(transformation_func) + expected = df["col_3"].groupby(["col_1"]).transform(transformation_func).iloc[:0] if transformation_func in ("diff", "shift"): expected = expected.astype(int) tm.assert_equal(result, expected) diff --git a/pandas/tests/groupby/test_groupby_subclass.py b/pandas/tests/groupby/test_groupby_subclass.py index a1f4627475bab..c81e7ecb1446d 100644 --- a/pandas/tests/groupby/test_groupby_subclass.py +++ b/pandas/tests/groupby/test_groupby_subclass.py @@ -36,11 +36,11 @@ def test_groupby_preserves_subclass(obj, groupby_func): args = get_groupby_method_args(groupby_func, obj) - warn = FutureWarning if groupby_func == "fillna" else None - msg = f"{type(grouped).__name__}.fillna is deprecated" - with tm.assert_produces_warning(warn, match=msg, raise_on_extra_warnings=False): + warn = FutureWarning if groupby_func == "corrwith" else None + msg = f"{type(grouped).__name__}.corrwith is deprecated" + with tm.assert_produces_warning(warn, match=msg): result1 = getattr(grouped, groupby_func)(*args) - with tm.assert_produces_warning(warn, match=msg, raise_on_extra_warnings=False): + with tm.assert_produces_warning(warn, match=msg): result2 = grouped.agg(groupby_func, *args) # Reduction or transformation kernels should preserve type diff --git a/pandas/tests/groupby/test_numeric_only.py b/pandas/tests/groupby/test_numeric_only.py index cb4569812f600..0779faa8d8975 100644 --- a/pandas/tests/groupby/test_numeric_only.py +++ b/pandas/tests/groupby/test_numeric_only.py @@ -278,14 +278,11 @@ def test_numeric_only(kernel, has_arg, numeric_only, keys): kernel in ("first", "last") or ( # kernels that work on any dtype and don't have numeric_only arg - kernel in ("any", "all", "bfill", "ffill", "fillna", "nth", "nunique") + kernel in ("any", "all", "bfill", "ffill", "nth", "nunique") and numeric_only is lib.no_default ) ): - warn = FutureWarning if kernel == "fillna" else None - msg = "DataFrameGroupBy.fillna is deprecated" - with tm.assert_produces_warning(warn, match=msg): - result = method(*args, **kwargs) + result = method(*args, **kwargs) assert "b" in result.columns elif has_arg: assert numeric_only is not True diff --git a/pandas/tests/groupby/test_raises.py b/pandas/tests/groupby/test_raises.py index 1e0a15d0ba796..789105c275625 100644 --- a/pandas/tests/groupby/test_raises.py +++ b/pandas/tests/groupby/test_raises.py @@ -144,7 +144,6 @@ def test_groupby_raises_string( ), "diff": (TypeError, "unsupported operand type"), "ffill": (None, ""), - "fillna": (None, ""), "first": (None, ""), "idxmax": (None, ""), "idxmin": (None, ""), @@ -211,10 +210,7 @@ def test_groupby_raises_string( elif groupby_func == "corrwith": msg = "Cannot perform reduction 'mean' with string dtype" - if groupby_func == "fillna": - kind = "Series" if groupby_series else "DataFrame" - warn_msg = f"{kind}GroupBy.fillna is deprecated" - elif groupby_func == "corrwith": + if groupby_func == "corrwith": warn_msg = "DataFrameGroupBy.corrwith is deprecated" else: warn_msg = "" @@ -301,7 +297,6 @@ def test_groupby_raises_datetime( "cumsum": (TypeError, "datetime64 type does not support operation 'cumsum'"), "diff": (None, ""), "ffill": (None, ""), - "fillna": (None, ""), "first": (None, ""), "idxmax": (None, ""), "idxmin": (None, ""), @@ -333,10 +328,7 @@ def test_groupby_raises_datetime( "var": (TypeError, "datetime64 type does not support operation 'var'"), }[groupby_func] - if groupby_func == "fillna": - kind = "Series" if groupby_series else "DataFrame" - warn_msg = f"{kind}GroupBy.fillna is deprecated" - elif groupby_func == "corrwith": + if groupby_func == "corrwith": warn_msg = "DataFrameGroupBy.corrwith is deprecated" else: warn_msg = "" @@ -457,7 +449,6 @@ def test_groupby_raises_category( r"unsupported operand type\(s\) for -: 'Categorical' and 'Categorical'", ), "ffill": (None, ""), - "fillna": (None, ""), # no-op with CoW "first": (None, ""), "idxmax": (None, ""), "idxmin": (None, ""), @@ -532,10 +523,7 @@ def test_groupby_raises_category( ), }[groupby_func] - if groupby_func == "fillna": - kind = "Series" if groupby_series else "DataFrame" - warn_msg = f"{kind}GroupBy.fillna is deprecated" - elif groupby_func == "corrwith": + if groupby_func == "corrwith": warn_msg = "DataFrameGroupBy.corrwith is deprecated" else: warn_msg = "" @@ -650,7 +638,6 @@ def test_groupby_raises_category_on_category( ), "diff": (TypeError, "unsupported operand type"), "ffill": (None, ""), - "fillna": (None, ""), # no-op with CoW "first": (None, ""), "idxmax": (ValueError, "empty group due to unobserved categories") if empty_groups @@ -710,10 +697,7 @@ def test_groupby_raises_category_on_category( ), }[groupby_func] - if groupby_func == "fillna": - kind = "Series" if groupby_series else "DataFrame" - warn_msg = f"{kind}GroupBy.fillna is deprecated" - elif groupby_func == "corrwith": + if groupby_func == "corrwith": warn_msg = "DataFrameGroupBy.corrwith is deprecated" else: warn_msg = "" diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py index 022d3d51ded4e..f506126f9cf6f 100644 --- a/pandas/tests/groupby/transform/test_transform.py +++ b/pandas/tests/groupby/transform/test_transform.py @@ -329,9 +329,6 @@ def test_transform_transformation_func(transformation_func): if transformation_func == "cumcount": test_op = lambda x: x.transform("cumcount") mock_op = lambda x: Series(range(len(x)), x.index) - elif transformation_func == "fillna": - test_op = lambda x: x.transform("fillna", value=0) - mock_op = lambda x: x.fillna(value=0) elif transformation_func == "ngroup": test_op = lambda x: x.transform("ngroup") counter = -1 @@ -1436,11 +1433,7 @@ def test_null_group_str_transformer_series(dropna, transformation_func): dtype = object if transformation_func in ("any", "all") else None buffer.append(Series([np.nan], index=[3], dtype=dtype)) expected = concat(buffer) - - warn = FutureWarning if transformation_func == "fillna" else None - msg = "SeriesGroupBy.fillna is deprecated" - with tm.assert_produces_warning(warn, match=msg): - result = gb.transform(transformation_func, *args) + result = gb.transform(transformation_func, *args) tm.assert_equal(result, expected) From d41884b2dd0823dc6288ab65d06650302e903c6b Mon Sep 17 00:00:00 2001 From: Grant Garrett-Grossman Date: Sun, 15 Dec 2024 14:45:42 -0600 Subject: [PATCH 156/266] BUG: Fixed type annotations for read_sql_* functions. (#60577) --- pandas/io/sql.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/io/sql.py b/pandas/io/sql.py index 3c0c5cc64c24c..5652d7fab0c7c 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -241,7 +241,7 @@ def read_sql_table( # pyright: ignore[reportOverlappingOverload] schema=..., index_col: str | list[str] | None = ..., coerce_float=..., - parse_dates: list[str] | dict[str, str] | None = ..., + parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., columns: list[str] | None = ..., chunksize: None = ..., dtype_backend: DtypeBackend | lib.NoDefault = ..., @@ -255,7 +255,7 @@ def read_sql_table( schema=..., index_col: str | list[str] | None = ..., coerce_float=..., - parse_dates: list[str] | dict[str, str] | None = ..., + parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., columns: list[str] | None = ..., chunksize: int = ..., dtype_backend: DtypeBackend | lib.NoDefault = ..., @@ -268,7 +268,7 @@ def read_sql_table( schema: str | None = None, index_col: str | list[str] | None = None, coerce_float: bool = True, - parse_dates: list[str] | dict[str, str] | None = None, + parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = None, columns: list[str] | None = None, chunksize: int | None = None, dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, @@ -372,7 +372,7 @@ def read_sql_query( # pyright: ignore[reportOverlappingOverload] index_col: str | list[str] | None = ..., coerce_float=..., params: list[Any] | Mapping[str, Any] | None = ..., - parse_dates: list[str] | dict[str, str] | None = ..., + parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., chunksize: None = ..., dtype: DtypeArg | None = ..., dtype_backend: DtypeBackend | lib.NoDefault = ..., @@ -386,7 +386,7 @@ def read_sql_query( index_col: str | list[str] | None = ..., coerce_float=..., params: list[Any] | Mapping[str, Any] | None = ..., - parse_dates: list[str] | dict[str, str] | None = ..., + parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., chunksize: int = ..., dtype: DtypeArg | None = ..., dtype_backend: DtypeBackend | lib.NoDefault = ..., @@ -399,7 +399,7 @@ def read_sql_query( index_col: str | list[str] | None = None, coerce_float: bool = True, params: list[Any] | Mapping[str, Any] | None = None, - parse_dates: list[str] | dict[str, str] | None = None, + parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = None, chunksize: int | None = None, dtype: DtypeArg | None = None, dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, From 8e119a79b54fb1d238e718d7f6143ea7b7ea2d55 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Tue, 17 Dec 2024 03:03:08 +0800 Subject: [PATCH 157/266] BUG: fix ValueError when printing a Series with DataFrame in its attrs (#60574) * Add test * BUG: fix ValueError when printing a Series with DataFrame in its attrs * Add note --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/format.py | 7 +++++-- pandas/tests/io/formats/test_format.py | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 005818b0779e6..f33d56bbed6d6 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -802,6 +802,7 @@ Other - Bug in ``Series.list`` methods not preserving the original :class:`Index`. (:issue:`58425`) - Bug in ``Series.list`` methods not preserving the original name. (:issue:`60522`) - Bug in printing a :class:`DataFrame` with a :class:`DataFrame` stored in :attr:`DataFrame.attrs` raised a ``ValueError`` (:issue:`60455`) +- Bug in printing a :class:`Series` with a :class:`DataFrame` stored in :attr:`Series.attrs` raised a ``ValueError`` (:issue:`60568`) .. ***DO NOT USE THIS SECTION*** diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index 17460eae8c049..46ecb2b9a8f12 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -78,7 +78,6 @@ ) from pandas.core.indexes.datetimes import DatetimeIndex from pandas.core.indexes.timedeltas import TimedeltaIndex -from pandas.core.reshape.concat import concat from pandas.io.common import ( check_parent_directory, @@ -245,7 +244,11 @@ def _chk_truncate(self) -> None: series = series.iloc[:max_rows] else: row_num = max_rows // 2 - series = concat((series.iloc[:row_num], series.iloc[-row_num:])) + _len = len(series) + _slice = np.hstack( + [np.arange(row_num), np.arange(_len - row_num, _len)] + ) + series = series.iloc[_slice] self.tr_row_num = row_num else: self.tr_row_num = None diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index d7db3d5082135..86682e8160762 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -136,6 +136,13 @@ def test_repr_truncation_dataframe_attrs(self): with option_context("display.max_columns", 2, "display.show_dimensions", False): assert repr(df) == " 0 ... 9\n0 0 ... 0" + def test_repr_truncation_series_with_dataframe_attrs(self): + # GH#60568 + ser = Series([0] * 10) + ser.attrs["b"] = DataFrame([]) + with option_context("display.max_rows", 2, "display.show_dimensions", False): + assert repr(ser) == "0 0\n ..\n9 0\ndtype: int64" + def test_max_colwidth_negative_int_raises(self): # Deprecation enforced from: # https://github.com/pandas-dev/pandas/issues/31532 From 43ed81fa132cd49a2f51722e1144ea4dc81e9c51 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 17 Dec 2024 00:33:47 +0530 Subject: [PATCH 158/266] DOC: fix PR07,SA01,ES01 for pandas.plotting.scatter_matrix (#60572) --- ci/code_checks.sh | 1 - pandas/plotting/_misc.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 74f5de78856d5..6c56928727570 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -97,7 +97,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.io.json.build_table_schema PR07,RT03,SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ - -i "pandas.plotting.scatter_matrix PR07,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ -i "pandas.tseries.offsets.BQuarterBegin.is_on_offset GL08" \ -i "pandas.tseries.offsets.BQuarterBegin.n GL08" \ diff --git a/pandas/plotting/_misc.py b/pandas/plotting/_misc.py index 7face74dcbc89..b20f8ac5f4796 100644 --- a/pandas/plotting/_misc.py +++ b/pandas/plotting/_misc.py @@ -178,14 +178,21 @@ def scatter_matrix( """ Draw a matrix of scatter plots. + Each pair of numeric columns in the DataFrame is plotted against each other, + resulting in a matrix of scatter plots. The diagonal plots can display either + histograms or Kernel Density Estimation (KDE) plots for each variable. + Parameters ---------- frame : DataFrame + The data to be plotted. alpha : float, optional Amount of transparency applied. figsize : (float,float), optional A tuple (width, height) in inches. ax : Matplotlib axis object, optional + An existing Matplotlib axis object for the plots. If None, a new axis is + created. grid : bool, optional Setting this to True will show the grid. diagonal : {'hist', 'kde'} @@ -208,6 +215,14 @@ def scatter_matrix( numpy.ndarray A matrix of scatter plots. + See Also + -------- + plotting.parallel_coordinates : Plots parallel coordinates for multivariate data. + plotting.andrews_curves : Generates Andrews curves for visualizing clusters of + multivariate data. + plotting.radviz : Creates a RadViz visualization. + plotting.bootstrap_plot : Visualizes uncertainty in data via bootstrap sampling. + Examples -------- From 57981d2c5b0347a16c7546f1b179a845d17a362e Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 17 Dec 2024 00:34:17 +0530 Subject: [PATCH 159/266] DOC: fix PR07,RT03,SA01,ES01 for pandas.io.json.build_table_schema (#60571) --- ci/code_checks.sh | 1 - pandas/io/json/_table_schema.py | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 6c56928727570..caa184320c59c 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -95,7 +95,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.var SA01" \ -i "pandas.errors.UndefinedVariableError PR01,SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ - -i "pandas.io.json.build_table_schema PR07,RT03,SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ -i "pandas.tseries.offsets.BQuarterBegin.is_on_offset GL08" \ diff --git a/pandas/io/json/_table_schema.py b/pandas/io/json/_table_schema.py index 9d250ee5c08ce..7879be18b52c9 100644 --- a/pandas/io/json/_table_schema.py +++ b/pandas/io/json/_table_schema.py @@ -239,9 +239,16 @@ def build_table_schema( """ Create a Table schema from ``data``. + This method is a utility to generate a JSON-serializable schema + representation of a pandas Series or DataFrame, compatible with the + Table Schema specification. It enables structured data to be shared + and validated in various applications, ensuring consistency and + interoperability. + Parameters ---------- - data : Series, DataFrame + data : Series or DataFrame + The input data for which the table schema is to be created. index : bool, default True Whether to include ``data.index`` in the schema. primary_key : bool or None, default True @@ -256,6 +263,12 @@ def build_table_schema( Returns ------- dict + A dictionary representing the Table schema. + + See Also + -------- + DataFrame.to_json : Convert the object to a JSON string. + read_json : Convert a JSON string to pandas object. Notes ----- From 659eecf22a2e4c4a8f023c655a75a7135614a409 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 17 Dec 2024 00:34:56 +0530 Subject: [PATCH 160/266] DOC: fix PR01,SA01 for pandas.errors.UndefinedVariableError (#60570) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index caa184320c59c..39cea0c361a72 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -93,7 +93,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.core.resample.Resampler.var SA01" \ - -i "pandas.errors.UndefinedVariableError PR01,SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index cd31ec30522c3..f150de3d217f2 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -588,6 +588,20 @@ class UndefinedVariableError(NameError): It will also specify whether the undefined variable is local or not. + Parameters + ---------- + name : str + The name of the undefined variable. + is_local : bool or None, optional + Indicates whether the undefined variable is considered a local variable. + If ``True``, the error message specifies it as a local variable. + If ``False`` or ``None``, the variable is treated as a non-local name. + + See Also + -------- + DataFrame.query : Query the columns of a DataFrame with a boolean expression. + DataFrame.eval : Evaluate a string describing operations on DataFrame columns. + Examples -------- >>> df = pd.DataFrame({"A": [1, 1, 1]}) From 44546602559c25b484399eb8c7ed7adcc0f5cac8 Mon Sep 17 00:00:00 2001 From: johnpaulfeliciano98 <102118062+johnpaulfeliciano98@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:10:22 -0800 Subject: [PATCH 161/266] DOC: Add hyperlink to ndarray.size in DataFrame.size docstring (#60368) (#60512) * DOC: Add hyperlink to ndarray.size in DataFrame.size docstring (#60368) * DOC: Update DataFrame.size docstring with numpy.ndarray.size reference --------- Co-authored-by: John Paul Feliciano Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/core/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index d1aa20501b060..de7fb3682fb4f 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -665,7 +665,7 @@ def size(self) -> int: See Also -------- - ndarray.size : Number of elements in the array. + numpy.ndarray.size : Number of elements in the array. Examples -------- From 45ee78296b4f6e5d8b76a25bde477b6860222388 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:43:43 -0800 Subject: [PATCH 162/266] CI: Install nightly numpy on free threading build to avoid numpy 2.2.0 segfaults (#60582) * Check if https://github.com/numpy/numpy/pull/27955 fixes free-threading build * Add comments --- .github/workflows/unit-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 07fb0c19262a1..899b49cc4eff5 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -385,10 +385,12 @@ jobs: nogil: true - name: Build Environment + # TODO: Once numpy 2.2.1 is out, don't install nightly version + # Tests segfault with numpy 2.2.0: https://github.com/numpy/numpy/pull/27955 run: | python --version - python -m pip install --upgrade pip setuptools wheel numpy meson[ninja]==1.2.1 meson-python==0.13.1 - python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython + python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.2.1 meson-python==0.13.1 + python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython numpy python -m pip install versioneer[toml] python -m pip install python-dateutil pytz tzdata hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0 pytest-cov python -m pip install -ve . --no-build-isolation --no-index --no-deps -Csetup-args="--werror" From 1e530b660c0eb3d37bfae326c5e5ded5a15a437e Mon Sep 17 00:00:00 2001 From: Thomas H Date: Mon, 16 Dec 2024 20:51:51 -0500 Subject: [PATCH 163/266] DOC: fix deprecation message for `is_period_dtype` (#60543) [DOC] fix deprecation message Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/core/dtypes/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 6fa21d9410187..b0c8ec1ffc083 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -430,7 +430,7 @@ def is_period_dtype(arr_or_dtype) -> bool: Check whether an array-like or dtype is of the Period dtype. .. deprecated:: 2.2.0 - Use isinstance(dtype, pd.Period) instead. + Use isinstance(dtype, pd.PeriodDtype) instead. Parameters ---------- From 9fe33bcbca79e098f9ba8ffd9fcf95440b95032b Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:37:34 -0500 Subject: [PATCH 164/266] DEPR: Enforce deprecation of include_groups in groupby.apply (#60566) * DEPR: Enforce deprecation of include_groups in groupby.apply * Fixup * Inline _apply --- doc/source/user_guide/cookbook.rst | 4 +- doc/source/user_guide/groupby.rst | 8 +- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/groupby/groupby.py | 89 ++--- pandas/core/resample.py | 53 +-- pandas/tests/extension/base/groupby.py | 8 +- pandas/tests/frame/test_stack_unstack.py | 5 +- pandas/tests/groupby/aggregate/test_other.py | 8 +- .../groupby/methods/test_value_counts.py | 9 +- pandas/tests/groupby/test_apply.py | 362 ++++++------------ pandas/tests/groupby/test_apply_mutate.py | 32 +- pandas/tests/groupby/test_categorical.py | 21 +- pandas/tests/groupby/test_counting.py | 4 +- pandas/tests/groupby/test_groupby.py | 50 +-- pandas/tests/groupby/test_groupby_dropna.py | 4 +- pandas/tests/groupby/test_groupby_subclass.py | 20 +- pandas/tests/groupby/test_grouping.py | 12 +- pandas/tests/groupby/test_timegrouper.py | 19 +- .../tests/groupby/transform/test_transform.py | 18 +- pandas/tests/resample/test_datetime_index.py | 20 +- pandas/tests/resample/test_resample_api.py | 4 +- .../tests/resample/test_resampler_grouper.py | 83 ++-- pandas/tests/resample/test_time_grouper.py | 16 +- pandas/tests/window/test_groupby.py | 104 ++--- 24 files changed, 271 insertions(+), 683 deletions(-) diff --git a/doc/source/user_guide/cookbook.rst b/doc/source/user_guide/cookbook.rst index 1525afcac87f7..b2b5c5cc1014e 100644 --- a/doc/source/user_guide/cookbook.rst +++ b/doc/source/user_guide/cookbook.rst @@ -459,7 +459,7 @@ Unlike agg, apply's callable is passed a sub-DataFrame which gives you access to df # List the size of the animals with the highest weight. - df.groupby("animal").apply(lambda subf: subf["size"][subf["weight"].idxmax()], include_groups=False) + df.groupby("animal").apply(lambda subf: subf["size"][subf["weight"].idxmax()]) `Using get_group `__ @@ -482,7 +482,7 @@ Unlike agg, apply's callable is passed a sub-DataFrame which gives you access to return pd.Series(["L", avg_weight, True], index=["size", "weight", "adult"]) - expected_df = gb.apply(GrowUp, include_groups=False) + expected_df = gb.apply(GrowUp) expected_df `Expanding apply diff --git a/doc/source/user_guide/groupby.rst b/doc/source/user_guide/groupby.rst index acb5a2b7919ac..4a32381a7de47 100644 --- a/doc/source/user_guide/groupby.rst +++ b/doc/source/user_guide/groupby.rst @@ -1074,7 +1074,7 @@ missing values with the ``ffill()`` method. ).set_index("date") df_re - df_re.groupby("group").resample("1D", include_groups=False).ffill() + df_re.groupby("group").resample("1D").ffill() .. _groupby.filter: @@ -1252,13 +1252,13 @@ the argument ``group_keys`` which defaults to ``True``. Compare .. ipython:: python - df.groupby("A", group_keys=True).apply(lambda x: x, include_groups=False) + df.groupby("A", group_keys=True).apply(lambda x: x) with .. ipython:: python - df.groupby("A", group_keys=False).apply(lambda x: x, include_groups=False) + df.groupby("A", group_keys=False).apply(lambda x: x) Numba accelerated routines @@ -1742,7 +1742,7 @@ column index name will be used as the name of the inserted column: result = {"b_sum": x["b"].sum(), "c_mean": x["c"].mean()} return pd.Series(result, name="metrics") - result = df.groupby("a").apply(compute_metrics, include_groups=False) + result = df.groupby("a").apply(compute_metrics) result diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index f33d56bbed6d6..92c67865ae88f 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -554,6 +554,7 @@ Other Removals - Removed the ``method`` keyword in ``ExtensionArray.fillna``, implement ``ExtensionArray._pad_or_backfill`` instead (:issue:`53621`) - Removed the attribute ``dtypes`` from :class:`.DataFrameGroupBy` (:issue:`51997`) - Enforced deprecation of ``argmin``, ``argmax``, ``idxmin``, and ``idxmax`` returning a result when ``skipna=False`` and an NA value is encountered or all values are NA values; these operations will now raise in such cases (:issue:`33941`, :issue:`51276`) +- Removed specifying ``include_groups=True`` in :class:`.DataFrameGroupBy.apply` and :class:`.Resampler.apply` (:issue:`7155`) .. --------------------------------------------------------------------------- .. _whatsnew_300.performance: diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index f0513be3498d1..f4ba40e275a8d 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -1393,7 +1393,7 @@ def _aggregate_with_numba(self, func, *args, engine_kwargs=None, **kwargs): # ----------------------------------------------------------------- # apply/agg/transform - def apply(self, func, *args, include_groups: bool = True, **kwargs) -> NDFrameT: + def apply(self, func, *args, include_groups: bool = False, **kwargs) -> NDFrameT: """ Apply function ``func`` group-wise and combine the results together. @@ -1419,7 +1419,7 @@ def apply(self, func, *args, include_groups: bool = True, **kwargs) -> NDFrameT: *args : tuple Optional positional arguments to pass to ``func``. - include_groups : bool, default True + include_groups : bool, default False When True, will attempt to apply ``func`` to the groupings in the case that they are columns of the DataFrame. If this raises a TypeError, the result will be computed with the groupings excluded. @@ -1427,10 +1427,9 @@ def apply(self, func, *args, include_groups: bool = True, **kwargs) -> NDFrameT: .. versionadded:: 2.2.0 - .. deprecated:: 2.2.0 + .. versionchanged:: 3.0.0 - Setting include_groups to True is deprecated. Only the value - False will be allowed in a future version of pandas. + The default changed from True to False, and True is no longer allowed. **kwargs : dict Optional keyword arguments to pass to ``func``. @@ -1520,7 +1519,7 @@ def apply(self, func, *args, include_groups: bool = True, **kwargs) -> NDFrameT: each group together into a Series, including setting the index as appropriate: - >>> g1.apply(lambda x: x.C.max() - x.B.min(), include_groups=False) + >>> g1.apply(lambda x: x.C.max() - x.B.min()) A a 5 b 2 @@ -1529,11 +1528,13 @@ def apply(self, func, *args, include_groups: bool = True, **kwargs) -> NDFrameT: Example 4: The function passed to ``apply`` returns ``None`` for one of the group. This group is filtered from the result: - >>> g1.apply(lambda x: None if x.iloc[0, 0] == 3 else x, include_groups=False) + >>> g1.apply(lambda x: None if x.iloc[0, 0] == 3 else x) B C 0 1 4 1 2 6 """ + if include_groups: + raise ValueError("include_groups=True is no longer allowed.") if isinstance(func, str): if hasattr(self, func): res = getattr(self, func) @@ -1560,33 +1561,7 @@ def f(g): else: f = func - if not include_groups: - return self._python_apply_general(f, self._obj_with_exclusions) - - try: - result = self._python_apply_general(f, self._selected_obj) - if ( - not isinstance(self.obj, Series) - and self._selection is None - and self._selected_obj.shape != self._obj_with_exclusions.shape - ): - warnings.warn( - message=_apply_groupings_depr.format(type(self).__name__, "apply"), - category=DeprecationWarning, - stacklevel=find_stack_level(), - ) - except TypeError: - # gh-20949 - # try again, with .apply acting as a filtering - # operation, by excluding the grouping column - # This would normally not be triggered - # except if the udf is trying an operation that - # fails on *some* columns, e.g. a numeric operation - # on a string grouper column - - return self._python_apply_general(f, self._obj_with_exclusions) - - return result + return self._python_apply_general(f, self._obj_with_exclusions) @final def _python_apply_general( @@ -3424,7 +3399,9 @@ def describe( return result @final - def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resampler: + def resample( + self, rule, *args, include_groups: bool = False, **kwargs + ) -> Resampler: """ Provide resampling when using a TimeGrouper. @@ -3449,10 +3426,9 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp .. versionadded:: 2.2.0 - .. deprecated:: 2.2.0 + .. versionchanged:: 3.0 - Setting include_groups to True is deprecated. Only the value - False will be allowed in a future version of pandas. + The default was changed to False, and True is no longer allowed. **kwargs Possible arguments are `how`, `fill_method`, `limit`, `kind` and @@ -3485,7 +3461,7 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp Downsample the DataFrame into 3 minute bins and sum the values of the timestamps falling into a bin. - >>> df.groupby("a").resample("3min", include_groups=False).sum() + >>> df.groupby("a").resample("3min").sum() b a 0 2000-01-01 00:00:00 2 @@ -3494,7 +3470,7 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp Upsample the series into 30 second bins. - >>> df.groupby("a").resample("30s", include_groups=False).sum() + >>> df.groupby("a").resample("30s").sum() b a 0 2000-01-01 00:00:00 1 @@ -3508,7 +3484,7 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp Resample by month. Values are assigned to the month of the period. - >>> df.groupby("a").resample("ME", include_groups=False).sum() + >>> df.groupby("a").resample("ME").sum() b a 0 2000-01-31 3 @@ -3517,11 +3493,7 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp Downsample the series into 3 minute bins as above, but close the right side of the bin interval. - >>> ( - ... df.groupby("a") - ... .resample("3min", closed="right", include_groups=False) - ... .sum() - ... ) + >>> (df.groupby("a").resample("3min", closed="right").sum()) b a 0 1999-12-31 23:57:00 1 @@ -3532,11 +3504,7 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp the bin interval, but label each bin using the right edge instead of the left. - >>> ( - ... df.groupby("a") - ... .resample("3min", closed="right", label="right", include_groups=False) - ... .sum() - ... ) + >>> (df.groupby("a").resample("3min", closed="right", label="right").sum()) b a 0 2000-01-01 00:00:00 1 @@ -3545,11 +3513,10 @@ def resample(self, rule, *args, include_groups: bool = True, **kwargs) -> Resamp """ from pandas.core.resample import get_resampler_for_grouping - # mypy flags that include_groups could be specified via `*args` or `**kwargs` - # GH#54961 would resolve. - return get_resampler_for_grouping( # type: ignore[misc] - self, rule, *args, include_groups=include_groups, **kwargs - ) + if include_groups: + raise ValueError("include_groups=True is no longer allowed.") + + return get_resampler_for_grouping(self, rule, *args, **kwargs) @final def rolling( @@ -5561,13 +5528,3 @@ def _insert_quantile_level(idx: Index, qs: npt.NDArray[np.float64]) -> MultiInde mi = MultiIndex(levels=levels, codes=codes, names=[idx.name, None]) return mi - - -# GH#7155 -_apply_groupings_depr = ( - "{}.{} operated on the grouping columns. This behavior is deprecated, " - "and in a future version of pandas the grouping columns will be excluded " - "from the operation. Either pass `include_groups=False` to exclude the " - "groupings or explicitly select the grouping columns after groupby to silence " - "this warning." -) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 0d1541bbb3afa..27e498683bf8f 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -31,10 +31,7 @@ Substitution, doc, ) -from pandas.util._exceptions import ( - find_stack_level, - rewrite_warning, -) +from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.dtypes import ( ArrowDtype, @@ -59,7 +56,6 @@ from pandas.core.groupby.groupby import ( BaseGroupBy, GroupBy, - _apply_groupings_depr, _pipe_template, get_groupby, ) @@ -167,14 +163,15 @@ def __init__( gpr_index: Index, group_keys: bool = False, selection=None, - include_groups: bool = True, + include_groups: bool = False, ) -> None: + if include_groups: + raise ValueError("include_groups=True is no longer allowed.") self._timegrouper = timegrouper self.keys = None self.sort = True self.group_keys = group_keys self.as_index = True - self.include_groups = include_groups self.obj, self.ax, self._indexer = self._timegrouper._set_grouper( self._convert_obj(obj), sort=True, gpr_index=gpr_index @@ -465,9 +462,7 @@ def _groupby_and_aggregate(self, how, *args, **kwargs): # a DataFrame column, but aggregate_item_by_item operates column-wise # on Series, raising AttributeError or KeyError # (depending on whether the column lookup uses getattr/__getitem__) - result = _apply( - grouped, how, *args, include_groups=self.include_groups, **kwargs - ) + result = grouped.apply(how, *args, **kwargs) except ValueError as err: if "Must produce aggregated value" in str(err): @@ -479,21 +474,23 @@ def _groupby_and_aggregate(self, how, *args, **kwargs): # we have a non-reducing function # try to evaluate - result = _apply( - grouped, how, *args, include_groups=self.include_groups, **kwargs - ) + result = grouped.apply(how, *args, **kwargs) return self._wrap_result(result) @final def _get_resampler_for_grouping( - self, groupby: GroupBy, key, include_groups: bool = True + self, + groupby: GroupBy, + key, ): """ Return the correct class for resampling with groupby. """ return self._resampler_for_grouping( - groupby=groupby, key=key, parent=self, include_groups=include_groups + groupby=groupby, + key=key, + parent=self, ) def _wrap_result(self, result): @@ -935,7 +932,7 @@ def interpolate( "supported. If you tried to resample and interpolate on a " "grouped data frame, please use:\n" "`df.groupby(...).apply(lambda x: x.resample(...)." - "interpolate(...), include_groups=False)`" + "interpolate(...))`" "\ninstead, as resampling and interpolation has to be " "performed for each group independently." ) @@ -1541,7 +1538,6 @@ def __init__( groupby: GroupBy, key=None, selection: IndexLabel | None = None, - include_groups: bool = False, ) -> None: # reached via ._gotitem and _get_resampler_for_grouping @@ -1564,7 +1560,6 @@ def __init__( self.ax = parent.ax self.obj = parent.obj - self.include_groups = include_groups @no_type_check def _apply(self, f, *args, **kwargs): @@ -1581,7 +1576,7 @@ def func(x): return x.apply(f, *args, **kwargs) - result = _apply(self._groupby, func, include_groups=self.include_groups) + result = self._groupby.apply(func) return self._wrap_result(result) _upsample = _apply @@ -1937,7 +1932,6 @@ def get_resampler_for_grouping( fill_method=None, limit: int | None = None, on=None, - include_groups: bool = True, **kwargs, ) -> Resampler: """ @@ -1946,9 +1940,7 @@ def get_resampler_for_grouping( # .resample uses 'on' similar to how .groupby uses 'key' tg = TimeGrouper(freq=rule, key=on, **kwargs) resampler = tg._get_resampler(groupby.obj) - return resampler._get_resampler_for_grouping( - groupby=groupby, include_groups=include_groups, key=tg.key - ) + return resampler._get_resampler_for_grouping(groupby=groupby, key=tg.key) class TimeGrouper(Grouper): @@ -2727,18 +2719,3 @@ def _asfreq_compat(index: FreqIndexT, freq) -> FreqIndexT: else: # pragma: no cover raise TypeError(type(index)) return new_index - - -def _apply( - grouped: GroupBy, how: Callable, *args, include_groups: bool, **kwargs -) -> DataFrame: - # GH#7155 - rewrite warning to appear as if it came from `.resample` - target_message = "DataFrameGroupBy.apply operated on the grouping columns" - new_message = _apply_groupings_depr.format("DataFrameGroupBy", "resample") - with rewrite_warning( - target_message=target_message, - target_category=DeprecationWarning, - new_message=new_message, - ): - result = grouped.apply(how, *args, include_groups=include_groups, **kwargs) - return result diff --git a/pandas/tests/extension/base/groupby.py b/pandas/tests/extension/base/groupby.py index bab8566a06dc2..60cade97ab528 100644 --- a/pandas/tests/extension/base/groupby.py +++ b/pandas/tests/extension/base/groupby.py @@ -113,13 +113,9 @@ def test_groupby_extension_transform(self, data_for_grouping): def test_groupby_extension_apply(self, data_for_grouping, groupby_apply_op): df = pd.DataFrame({"A": [1, 1, 2, 2, 3, 3, 1, 4], "B": data_for_grouping}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.groupby("B", group_keys=False, observed=False).apply(groupby_apply_op) + df.groupby("B", group_keys=False, observed=False).apply(groupby_apply_op) df.groupby("B", group_keys=False, observed=False).A.apply(groupby_apply_op) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.groupby("A", group_keys=False, observed=False).apply(groupby_apply_op) + df.groupby("A", group_keys=False, observed=False).apply(groupby_apply_op) df.groupby("A", group_keys=False, observed=False).B.apply(groupby_apply_op) def test_groupby_apply_identity(self, data_for_grouping): diff --git a/pandas/tests/frame/test_stack_unstack.py b/pandas/tests/frame/test_stack_unstack.py index 57c803c23b001..dae7fe2575c22 100644 --- a/pandas/tests/frame/test_stack_unstack.py +++ b/pandas/tests/frame/test_stack_unstack.py @@ -1858,10 +1858,7 @@ def test_unstack_bug(self, future_stack): } ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(["state", "exp", "barcode", "v"]).apply(len) - + result = df.groupby(["state", "exp", "barcode", "v"]).apply(len) unstacked = result.unstack() restacked = unstacked.stack(future_stack=future_stack) tm.assert_series_equal(restacked, result.reindex(restacked.index).astype(float)) diff --git a/pandas/tests/groupby/aggregate/test_other.py b/pandas/tests/groupby/aggregate/test_other.py index ce78b58e5d8f4..1c016143d50c3 100644 --- a/pandas/tests/groupby/aggregate/test_other.py +++ b/pandas/tests/groupby/aggregate/test_other.py @@ -499,17 +499,13 @@ def test_agg_timezone_round_trip(): assert ts == grouped.first()["B"].iloc[0] # GH#27110 applying iloc should return a DataFrame - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - assert ts == grouped.apply(lambda x: x.iloc[0]).iloc[0, 1] + assert ts == grouped.apply(lambda x: x.iloc[0])["B"].iloc[0] ts = df["B"].iloc[2] assert ts == grouped.last()["B"].iloc[0] # GH#27110 applying iloc should return a DataFrame - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - assert ts == grouped.apply(lambda x: x.iloc[-1]).iloc[0, 1] + assert ts == grouped.apply(lambda x: x.iloc[-1])["B"].iloc[0] def test_sum_uint64_overflow(): diff --git a/pandas/tests/groupby/methods/test_value_counts.py b/pandas/tests/groupby/methods/test_value_counts.py index 8ca6593a19f20..1050f8154572a 100644 --- a/pandas/tests/groupby/methods/test_value_counts.py +++ b/pandas/tests/groupby/methods/test_value_counts.py @@ -324,12 +324,9 @@ def test_against_frame_and_seriesgroupby( ) if frame: # compare against apply with DataFrame value_counts - warn = DeprecationWarning if groupby == "column" else None - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(warn, match=msg): - expected = gp.apply( - _frame_value_counts, ["gender", "education"], normalize, sort, ascending - ) + expected = gp.apply( + _frame_value_counts, ["gender", "education"], normalize, sort, ascending + ) if as_index: tm.assert_series_equal(result, expected) diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index 1a4127ab49b0e..fd1c82932f57f 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -27,12 +27,9 @@ def test_apply_func_that_appends_group_to_list_without_copy(): def store(group): groups.append(group) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.groupby("index").apply(store) - expected_value = DataFrame( - {"index": [0] * 10, 0: [1] * 10}, index=pd.RangeIndex(0, 100, 10) - ) + df.groupby("index").apply(store) + expected_value = DataFrame({0: [1] * 10}, index=pd.RangeIndex(0, 100, 10)) + expected_value.columns = expected_value.columns.astype(object) tm.assert_frame_equal(groups[0], expected_value) @@ -111,11 +108,7 @@ def test_apply_index_date_object(): ] exp_idx = Index(["2011-05-16", "2011-05-17", "2011-05-18"], name="date") expected = Series(["00:00", "02:00", "02:00"], index=exp_idx) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("date", group_keys=False).apply( - lambda x: x["time"][x["value"].idxmax()] - ) + result = df.groupby("date").apply(lambda x: x["time"][x["value"].idxmax()]) tm.assert_series_equal(result, expected) @@ -189,9 +182,7 @@ def f_constant_df(group): for func in [f_copy, f_nocopy, f_scalar, f_none, f_constant_df]: del names[:] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.groupby("a", group_keys=False).apply(func) + df.groupby("a").apply(func) assert names == group_names @@ -209,11 +200,9 @@ def test_group_apply_once_per_group2(capsys): index=["0", "2", "4", "6", "8", "10", "12", "14"], ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.groupby("group_by_column", group_keys=False).apply( - lambda df: print("function_called") - ) + df.groupby("group_by_column", group_keys=False).apply( + lambda df: print("function_called") + ) result = capsys.readouterr().out.count("function_called") # If `groupby` behaves unexpectedly, this test will break @@ -233,12 +222,8 @@ def slow(group): def fast(group): return group.copy() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - fast_df = df.groupby("A", group_keys=False).apply(fast) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - slow_df = df.groupby("A", group_keys=False).apply(slow) - + fast_df = df.groupby("A", group_keys=False).apply(fast) + slow_df = df.groupby("A", group_keys=False).apply(slow) tm.assert_frame_equal(fast_df, slow_df) @@ -258,11 +243,8 @@ def test_groupby_apply_identity_maybecopy_index_identical(func): # transparent to the user df = DataFrame({"g": [1, 2, 2, 2], "a": [1, 2, 3, 4], "b": [5, 6, 7, 8]}) - - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("g", group_keys=False).apply(func) - tm.assert_frame_equal(result, df) + result = df.groupby("g", group_keys=False).apply(func) + tm.assert_frame_equal(result, df[["a", "b"]]) def test_apply_with_mixed_dtype(): @@ -304,11 +286,8 @@ def test_groupby_as_index_apply(): tm.assert_index_equal(res_as, exp) tm.assert_index_equal(res_not_as, exp) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - res_as_apply = g_as.apply(lambda x: x.head(2)).index - with tm.assert_produces_warning(DeprecationWarning, match=msg): - res_not_as_apply = g_not_as.apply(lambda x: x.head(2)).index + res_as_apply = g_as.apply(lambda x: x.head(2)).index + res_not_as_apply = g_not_as.apply(lambda x: x.head(2)).index # apply doesn't maintain the original ordering # changed in GH5610 as the as_index=False returns a MI here @@ -323,9 +302,7 @@ def test_groupby_as_index_apply(): def test_groupby_as_index_apply_str(): ind = Index(list("abcde")) df = DataFrame([[1, 2], [2, 3], [1, 4], [1, 5], [2, 6]], index=ind) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - res = df.groupby(0, as_index=False, group_keys=False).apply(lambda x: x).index + res = df.groupby(0, as_index=False, group_keys=False).apply(lambda x: x).index tm.assert_index_equal(res, ind) @@ -354,19 +331,13 @@ def desc3(group): # weirdo return result - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grouped.apply(desc) + result = grouped.apply(desc) assert result.index.names == ("A", "B", "stat") - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result2 = grouped.apply(desc2) + result2 = grouped.apply(desc2) assert result2.index.names == ("A", "B", "stat") - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result3 = grouped.apply(desc3) + result3 = grouped.apply(desc3) assert result3.index.names == ("A", "B", None) @@ -396,9 +367,7 @@ def test_apply_series_yield_constant(df): def test_apply_frame_yield_constant(df): # GH13568 - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(["A", "B"]).apply(len) + result = df.groupby(["A", "B"]).apply(len) assert isinstance(result, Series) assert result.name is None @@ -409,9 +378,7 @@ def test_apply_frame_yield_constant(df): def test_apply_frame_to_series(df): grouped = df.groupby(["A", "B"]) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grouped.apply(len) + result = grouped.apply(len) expected = grouped.count()["C"] tm.assert_index_equal(result.index, expected.index) tm.assert_numpy_array_equal(result.values, expected.values) @@ -420,9 +387,7 @@ def test_apply_frame_to_series(df): def test_apply_frame_not_as_index_column_name(df): # GH 35964 - path within _wrap_applied_output not hit by a test grouped = df.groupby(["A", "B"], as_index=False) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grouped.apply(len) + result = grouped.apply(len) expected = grouped.count().rename(columns={"C": np.nan}).drop(columns="D") # TODO(GH#34306): Use assert_frame_equal when column name is not np.nan tm.assert_index_equal(result.index, expected.index) @@ -445,9 +410,7 @@ def trans2(group): } ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(trans) + result = df.groupby("A").apply(trans) exp = df.groupby("A")["C"].apply(trans2) tm.assert_series_equal(result, exp, check_names=False) assert result.name == "C" @@ -476,10 +439,8 @@ def test_apply_chunk_view(group_keys): # Low level tinkering could be unsafe, make sure not df = DataFrame({"key": [1, 1, 1, 2, 2, 2, 3, 3, 3], "value": range(9)}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("key", group_keys=group_keys).apply(lambda x: x.iloc[:2]) - expected = df.take([0, 1, 3, 4, 6, 7]) + result = df.groupby("key", group_keys=group_keys).apply(lambda x: x.iloc[:2]) + expected = df[["value"]].take([0, 1, 3, 4, 6, 7]) if group_keys: expected.index = MultiIndex.from_arrays( [[1, 1, 2, 2, 3, 3], expected.index], names=["key", None] @@ -499,9 +460,7 @@ def test_apply_no_name_column_conflict(): # it works! #2605 grouped = df.groupby(["name", "name2"]) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - grouped.apply(lambda x: x.sort_values("value", inplace=True)) + grouped.apply(lambda x: x.sort_values("value", inplace=True)) def test_apply_typecast_fail(): @@ -518,11 +477,9 @@ def f(group): group["v2"] = (v - v.min()) / (v.max() - v.min()) return group - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("d", group_keys=False).apply(f) + result = df.groupby("d", group_keys=False).apply(f) - expected = df.copy() + expected = df[["c", "v"]] expected["v2"] = np.tile([0.0, 0.5, 1], 2) tm.assert_frame_equal(result, expected) @@ -544,13 +501,10 @@ def f(group): group["v2"] = (v - v.min()) / (v.max() - v.min()) return group - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("d", group_keys=False).apply(f) + result = df.groupby("d", group_keys=False).apply(f) - expected = df.copy() + expected = df[["c", "v"]] expected["v2"] = np.tile([0.0, 0.5, 1], 2) - tm.assert_frame_equal(result, expected) @@ -584,11 +538,8 @@ def filt2(x): else: return x[x.category == "c"] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = data.groupby("id_field").apply(filt1) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = data.groupby("id_field").apply(filt2) + expected = data.groupby("id_field").apply(filt1) + result = data.groupby("id_field").apply(filt2) tm.assert_frame_equal(result, expected) @@ -601,19 +552,11 @@ def test_apply_with_duplicated_non_sorted_axis(test_series): if test_series: ser = df.set_index("Y")["X"] result = ser.groupby(level=0, group_keys=False).apply(lambda x: x) - - # not expecting the order to remain the same for duplicated axis - result = result.sort_index() - expected = ser.sort_index() + expected = ser tm.assert_series_equal(result, expected) else: - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("Y", group_keys=False).apply(lambda x: x) - - # not expecting the order to remain the same for duplicated axis - result = result.sort_values("Y") - expected = df.sort_values("Y") + result = df.groupby("Y", group_keys=False).apply(lambda x: x) + expected = df[["X"]] tm.assert_frame_equal(result, expected) @@ -654,9 +597,7 @@ def f(g): g["value3"] = g["value1"] * 2 return g - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grouped.apply(f) + result = grouped.apply(f) assert "value3" in result @@ -670,13 +611,9 @@ def test_apply_numeric_coercion_when_datetime(): df = DataFrame( {"Number": [1, 2], "Date": ["2017-03-02"] * 2, "Str": ["foo", "inf"]} ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby(["Number"]).apply(lambda x: x.iloc[0]) + expected = df.groupby(["Number"]).apply(lambda x: x.iloc[0]) df.Date = pd.to_datetime(df.Date) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(["Number"]).apply(lambda x: x.iloc[0]) + result = df.groupby(["Number"]).apply(lambda x: x.iloc[0]) tm.assert_series_equal(result["Str"], expected["Str"]) @@ -689,9 +626,7 @@ def test_apply_numeric_coercion_when_datetime_getitem(): def get_B(g): return g.iloc[0][["B"]] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(get_B)["B"] + result = df.groupby("A").apply(get_B)["B"] expected = df.B expected.index = df.A tm.assert_series_equal(result, expected) @@ -718,11 +653,8 @@ def predictions(tool): ) df2 = df1.copy() df2.oTime = pd.to_datetime(df2.oTime) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df1.groupby("Key").apply(predictions).p1 - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df2.groupby("Key").apply(predictions).p1 + expected = df1.groupby("Key").apply(predictions).p1 + result = df2.groupby("Key").apply(predictions).p1 tm.assert_series_equal(expected, result) @@ -737,13 +669,11 @@ def test_apply_aggregating_timedelta_and_datetime(): } ) df["time_delta_zero"] = df.datetime - df.datetime - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("clientid").apply( - lambda ddf: Series( - {"clientid_age": ddf.time_delta_zero.min(), "date": ddf.datetime.min()} - ) + result = df.groupby("clientid").apply( + lambda ddf: Series( + {"clientid_age": ddf.time_delta_zero.min(), "date": ddf.datetime.min()} ) + ) expected = DataFrame( { "clientid": ["A", "B", "C"], @@ -786,15 +716,11 @@ def func_with_no_date(batch): def func_with_date(batch): return Series({"b": datetime(2015, 1, 1), "c": 2}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - dfg_no_conversion = df.groupby(by=["a"]).apply(func_with_no_date) + dfg_no_conversion = df.groupby(by=["a"]).apply(func_with_no_date) dfg_no_conversion_expected = DataFrame({"c": 2}, index=[1]) dfg_no_conversion_expected.index.name = "a" - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - dfg_conversion = df.groupby(by=["a"]).apply(func_with_date) + dfg_conversion = df.groupby(by=["a"]).apply(func_with_date) dfg_conversion_expected = DataFrame( {"b": pd.Timestamp(2015, 1, 1), "c": 2}, index=[1] ) @@ -838,11 +764,8 @@ def test_groupby_apply_all_none(): def test_func(x): pass - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = test_df.groupby("groups").apply(test_func) - expected = DataFrame(columns=test_df.columns) - expected = expected.astype(test_df.dtypes) + result = test_df.groupby("groups").apply(test_func) + expected = DataFrame(columns=["random_vars"], dtype="int64") tm.assert_frame_equal(result, expected) @@ -852,12 +775,12 @@ def test_func(x): [ {"groups": [1, 1, 1, 2], "vars": [0, 1, 2, 3]}, [[1, 1], [0, 2]], - {"groups": [1, 1], "vars": [0, 2]}, + {"vars": [0, 2]}, ], [ {"groups": [1, 2, 2, 2], "vars": [0, 1, 2, 3]}, [[2, 2], [1, 3]], - {"groups": [2, 2], "vars": [1, 3]}, + {"vars": [1, 3]}, ], ], ) @@ -870,9 +793,7 @@ def test_func(x): return None return x.iloc[[0, -1]] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result1 = test_df1.groupby("groups").apply(test_func) + result1 = test_df1.groupby("groups").apply(test_func) index1 = MultiIndex.from_arrays(out_idx, names=["groups", None]) expected1 = DataFrame(out_data, index=index1) tm.assert_frame_equal(result1, expected1) @@ -882,9 +803,7 @@ def test_groupby_apply_return_empty_chunk(): # GH 22221: apply filter which returns some empty groups df = DataFrame({"value": [0, 1], "group": ["filled", "empty"]}) groups = df.groupby("group") - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = groups.apply(lambda group: group[group.value != 1]["value"]) + result = groups.apply(lambda group: group[group.value != 1]["value"]) expected = Series( [0], name="value", @@ -909,9 +828,7 @@ def test_apply_with_mixed_types(meth): def test_func_returns_object(): # GH 28652 df = DataFrame({"a": [1, 2]}, index=Index([1, 2])) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("a").apply(lambda g: g.index) + result = df.groupby("a").apply(lambda g: g.index) expected = Series([Index([1]), Index([2])], index=Index([1, 2], name="a")) tm.assert_series_equal(result, expected) @@ -928,9 +845,7 @@ def test_apply_datetime_issue(group_column_dtlike): # standard int values in range(len(num_columns)) df = DataFrame({"a": ["foo"], "b": [group_column_dtlike]}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("a").apply(lambda x: Series(["spam"], index=[42])) + result = df.groupby("a").apply(lambda x: Series(["spam"], index=[42])) expected = DataFrame(["spam"], Index(["foo"], dtype="str", name="a"), columns=[42]) tm.assert_frame_equal(result, expected) @@ -967,9 +882,7 @@ def test_apply_series_return_dataframe_groups(): def most_common_values(df): return Series({c: s.value_counts().index[0] for c, s in df.items()}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = tdf.groupby("day").apply(most_common_values)["userId"] + result = tdf.groupby("day").apply(most_common_values)["userId"] expected = Series( ["17661101"], index=pd.DatetimeIndex(["2015-02-24"], name="day"), name="userId" ) @@ -1010,13 +923,11 @@ def test_groupby_apply_datetime_result_dtypes(using_infer_string): ], columns=["observation", "color", "mood", "intensity", "score"], ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = data.groupby("color").apply(lambda g: g.iloc[0]).dtypes + result = data.groupby("color").apply(lambda g: g.iloc[0]).dtypes dtype = pd.StringDtype(na_value=np.nan) if using_infer_string else object expected = Series( - [np.dtype("datetime64[us]"), dtype, dtype, np.int64, dtype], - index=["observation", "color", "mood", "intensity", "score"], + [np.dtype("datetime64[us]"), dtype, np.int64, dtype], + index=["observation", "mood", "intensity", "score"], ) tm.assert_series_equal(result, expected) @@ -1033,10 +944,8 @@ def test_groupby_apply_datetime_result_dtypes(using_infer_string): def test_apply_index_has_complex_internals(index): # GH 31248 df = DataFrame({"group": [1, 1, 2], "value": [0, 1, 0]}, index=index) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("group", group_keys=False).apply(lambda x: x) - tm.assert_frame_equal(result, df) + result = df.groupby("group", group_keys=False).apply(lambda x: x) + tm.assert_frame_equal(result, df[["value"]]) @pytest.mark.parametrize( @@ -1058,9 +967,7 @@ def test_apply_index_has_complex_internals(index): def test_apply_function_returns_non_pandas_non_scalar(function, expected_values): # GH 31441 df = DataFrame(["A", "A", "B", "B"], columns=["groups"]) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("groups").apply(function) + result = df.groupby("groups").apply(function) expected = Series(expected_values, index=Index(["A", "B"], name="groups")) tm.assert_series_equal(result, expected) @@ -1072,9 +979,7 @@ def fct(group): df = DataFrame({"A": ["a", "a", "b", "none"], "B": [1, 2, 3, np.nan]}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(fct) + result = df.groupby("A").apply(fct) expected = Series( [[1.0, 2.0], [3.0], [np.nan]], index=Index(["a", "b", "none"], name="A") ) @@ -1085,9 +990,7 @@ def fct(group): def test_apply_function_index_return(function): # GH: 22541 df = DataFrame([1, 2, 2, 2, 1, 2, 3, 1, 3, 1], columns=["id"]) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("id").apply(function) + result = df.groupby("id").apply(function) expected = Series( [Index([0, 4, 7, 9]), Index([1, 2, 3, 5]), Index([6, 8])], index=Index([1, 2, 3], name="id"), @@ -1123,9 +1026,7 @@ def test_apply_result_type(group_keys, udf): # We'd like to control whether the group keys end up in the index # regardless of whether the UDF happens to be a transform. df = DataFrame({"A": ["a", "b"], "B": [1, 2]}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df_result = df.groupby("A", group_keys=group_keys).apply(udf) + df_result = df.groupby("A", group_keys=group_keys).apply(udf) series_result = df.B.groupby(df.A, group_keys=group_keys).apply(udf) if group_keys: @@ -1140,11 +1041,8 @@ def test_result_order_group_keys_false(): # GH 34998 # apply result order should not depend on whether index is the same or just equal df = DataFrame({"A": [2, 1, 2], "B": [1, 2, 3]}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A", group_keys=False).apply(lambda x: x) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby("A", group_keys=False).apply(lambda x: x.copy()) + result = df.groupby("A", group_keys=False).apply(lambda x: x) + expected = df.groupby("A", group_keys=False).apply(lambda x: x.copy()) tm.assert_frame_equal(result, expected) @@ -1156,15 +1054,8 @@ def test_apply_with_timezones_aware(): df1 = DataFrame({"x": list(range(2)) * 3, "y": range(6), "t": index_no_tz}) df2 = DataFrame({"x": list(range(2)) * 3, "y": range(6), "t": index_tz}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result1 = df1.groupby("x", group_keys=False).apply( - lambda df: df[["x", "y"]].copy() - ) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result2 = df2.groupby("x", group_keys=False).apply( - lambda df: df[["x", "y"]].copy() - ) + result1 = df1.groupby("x", group_keys=False).apply(lambda df: df[["y"]].copy()) + result2 = df2.groupby("x", group_keys=False).apply(lambda df: df[["y"]].copy()) tm.assert_frame_equal(result1, result2) @@ -1187,7 +1078,7 @@ def test_apply_is_unchanged_when_other_methods_are_called_first(reduction_func): # Check output when no other methods are called before .apply() grp = df.groupby(by="a") - result = grp.apply(np.sum, axis=0, include_groups=False) + result = grp.apply(np.sum, axis=0) tm.assert_frame_equal(result, expected) # Check output when another method is called before .apply() @@ -1201,7 +1092,7 @@ def test_apply_is_unchanged_when_other_methods_are_called_first(reduction_func): msg = "" with tm.assert_produces_warning(warn, match=msg): _ = getattr(grp, reduction_func)(*args) - result = grp.apply(np.sum, axis=0, include_groups=False) + result = grp.apply(np.sum, axis=0) tm.assert_frame_equal(result, expected) @@ -1223,14 +1114,12 @@ def test_apply_with_date_in_multiindex_does_not_convert_to_timestamp(): ) grp = df.groupby(["A", "B"]) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grp.apply(lambda x: x.head(1)) + result = grp.apply(lambda x: x.head(1)) expected = df.iloc[[0, 2, 3]] expected = expected.reset_index() expected.index = MultiIndex.from_frame(expected[["A", "B", "idx"]]) - expected = expected.drop(columns=["idx"]) + expected = expected.drop(columns=["A", "B", "idx"]) tm.assert_frame_equal(result, expected) for val in result.index.levels[1]: @@ -1247,10 +1136,8 @@ def test_apply_dropna_with_indexed_same(dropna): }, index=list("xxyxz"), ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("group", dropna=dropna, group_keys=False).apply(lambda x: x) - expected = df.dropna() if dropna else df.iloc[[0, 3, 1, 2, 4]] + result = df.groupby("group", dropna=dropna, group_keys=False).apply(lambda x: x) + expected = df.dropna()[["col"]] if dropna else df[["col"]].iloc[[0, 3, 1, 2, 4]] tm.assert_frame_equal(result, expected) @@ -1274,9 +1161,7 @@ def test_apply_dropna_with_indexed_same(dropna): def test_apply_as_index_constant_lambda(as_index, expected): # GH 13217 df = DataFrame({"a": [1, 1, 2, 2], "b": [1, 1, 2, 2], "c": [1, 1, 1, 1]}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(["a", "b"], as_index=as_index).apply(lambda x: 1) + result = df.groupby(["a", "b"], as_index=as_index).apply(lambda x: 1) tm.assert_equal(result, expected) @@ -1286,9 +1171,7 @@ def test_sort_index_groups(): {"A": [1, 2, 3, 4, 5], "B": [6, 7, 8, 9, 0], "C": [1, 1, 1, 2, 2]}, index=range(5), ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("C").apply(lambda x: x.A.sort_index()) + result = df.groupby("C").apply(lambda x: x.A.sort_index()) expected = Series( range(1, 6), index=MultiIndex.from_tuples( @@ -1308,12 +1191,10 @@ def test_positional_slice_groups_datetimelike(): "let": list("abcde"), } ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = expected.groupby( - [expected.let, expected.date.dt.date], group_keys=False - ).apply(lambda x: x.iloc[0:]) - tm.assert_frame_equal(result, expected) + result = expected.groupby( + [expected.let, expected.date.dt.date], group_keys=False + ).apply(lambda x: x.iloc[0:]) + tm.assert_frame_equal(result, expected[["date", "vals"]]) def test_groupby_apply_shape_cache_safety(): @@ -1354,32 +1235,27 @@ def test_apply_na(dropna): {"grp": [1, 1, 2, 2], "y": [1, 0, 2, 5], "z": [1, 2, np.nan, np.nan]} ) dfgrp = df.groupby("grp", dropna=dropna) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = dfgrp.apply(lambda grp_df: grp_df.nlargest(1, "z")) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = dfgrp.apply(lambda x: x.sort_values("z", ascending=False).head(1)) + result = dfgrp.apply(lambda grp_df: grp_df.nlargest(1, "z")) + expected = dfgrp.apply(lambda x: x.sort_values("z", ascending=False).head(1)) tm.assert_frame_equal(result, expected) def test_apply_empty_string_nan_coerce_bug(): # GH#24903 - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = ( - DataFrame( - { - "a": [1, 1, 2, 2], - "b": ["", "", "", ""], - "c": pd.to_datetime([1, 2, 3, 4], unit="s"), - } - ) - .groupby(["a", "b"]) - .apply(lambda df: df.iloc[-1]) + result = ( + DataFrame( + { + "a": [1, 1, 2, 2], + "b": ["", "", "", ""], + "c": pd.to_datetime([1, 2, 3, 4], unit="s"), + } ) + .groupby(["a", "b"]) + .apply(lambda df: df.iloc[-1]) + ) expected = DataFrame( - [[1, "", pd.to_datetime(2, unit="s")], [2, "", pd.to_datetime(4, unit="s")]], - columns=["a", "b", "c"], + [[pd.to_datetime(2, unit="s")], [pd.to_datetime(4, unit="s")]], + columns=["c"], index=MultiIndex.from_tuples([(1, ""), (2, "")], names=["a", "b"]), ) tm.assert_frame_equal(result, expected) @@ -1401,11 +1277,9 @@ def test_apply_index_key_error_bug(index_values): }, index=Index(["a2", "a3", "aa"], name="a"), ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = result.groupby("a").apply( - lambda df: Series([df["b"].mean()], index=["b_mean"]) - ) + result = result.groupby("a").apply( + lambda df: Series([df["b"].mean()], index=["b_mean"]) + ) tm.assert_frame_equal(result, expected) @@ -1452,10 +1326,9 @@ def test_apply_index_key_error_bug(index_values): ) def test_apply_nonmonotonic_float_index(arg, idx): # GH 34455 - expected = DataFrame({"col": arg}, index=idx) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = expected.groupby("col", group_keys=False).apply(lambda x: x) + df = DataFrame({"grp": arg, "col": arg}, index=idx) + result = df.groupby("grp", group_keys=False).apply(lambda x: x) + expected = df[["col"]] tm.assert_frame_equal(result, expected) @@ -1502,19 +1375,12 @@ def test_empty_df(method, op): tm.assert_series_equal(result, expected) -@pytest.mark.parametrize("include_groups", [True, False]) -def test_include_groups(include_groups): +def test_include_groups(): # GH#7155 df = DataFrame({"a": [1, 1, 2], "b": [3, 4, 5]}) gb = df.groupby("a") - warn = DeprecationWarning if include_groups else None - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(warn, match=msg): - result = gb.apply(lambda x: x.sum(), include_groups=include_groups) - expected = DataFrame({"a": [2, 2], "b": [7, 5]}, index=Index([1, 2], name="a")) - if not include_groups: - expected = expected[["b"]] - tm.assert_frame_equal(result, expected) + with pytest.raises(ValueError, match="include_groups=True is no longer allowed"): + gb.apply(lambda x: x.sum(), include_groups=True) @pytest.mark.parametrize("func, value", [(max, 2), (min, 1), (sum, 3)]) @@ -1523,7 +1389,7 @@ def test_builtins_apply(func, value): # Builtins act as e.g. sum(group), which sums the column labels of group df = DataFrame({0: [1, 1, 2], 1: [3, 4, 5], 2: [3, 4, 5]}) gb = df.groupby(0) - result = gb.apply(func, include_groups=False) + result = gb.apply(func) expected = Series([value, value], index=Index([1, 2], name=0)) tm.assert_series_equal(result, expected) @@ -1544,9 +1410,7 @@ def f_0(grp): return grp.iloc[0] expected = df.groupby("A").first()[["B"]] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(f_0)[["B"]] + result = df.groupby("A").apply(f_0)[["B"]] tm.assert_frame_equal(result, expected) def f_1(grp): @@ -1554,9 +1418,7 @@ def f_1(grp): return None return grp.iloc[0] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(f_1)[["B"]] + result = df.groupby("A").apply(f_1)[["B"]] e = expected.copy() e.loc["Tiger"] = np.nan tm.assert_frame_equal(result, e) @@ -1566,9 +1428,7 @@ def f_2(grp): return None return grp.iloc[0] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(f_2)[["B"]] + result = df.groupby("A").apply(f_2)[["B"]] e = expected.copy() e.loc["Pony"] = np.nan tm.assert_frame_equal(result, e) @@ -1579,9 +1439,7 @@ def f_3(grp): return None return grp.iloc[0] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(f_3)[["C"]] + result = df.groupby("A").apply(f_3)[["C"]] e = df.groupby("A").first()[["C"]] e.loc["Pony"] = pd.NaT tm.assert_frame_equal(result, e) @@ -1592,9 +1450,7 @@ def f_4(grp): return None return grp.iloc[0].loc["C"] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").apply(f_4) + result = df.groupby("A").apply(f_4) e = df.groupby("A").first()["C"].copy() e.loc["Pony"] = np.nan e.name = None diff --git a/pandas/tests/groupby/test_apply_mutate.py b/pandas/tests/groupby/test_apply_mutate.py index fa20efad4da77..970334917faab 100644 --- a/pandas/tests/groupby/test_apply_mutate.py +++ b/pandas/tests/groupby/test_apply_mutate.py @@ -13,16 +13,10 @@ def test_group_by_copy(): } ).set_index("name") - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - grp_by_same_value = df.groupby(["age"], group_keys=False).apply( - lambda group: group - ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - grp_by_copy = df.groupby(["age"], group_keys=False).apply( - lambda group: group.copy() - ) + grp_by_same_value = df.groupby(["age"], group_keys=False).apply(lambda group: group) + grp_by_copy = df.groupby(["age"], group_keys=False).apply( + lambda group: group.copy() + ) tm.assert_frame_equal(grp_by_same_value, grp_by_copy) @@ -53,11 +47,8 @@ def f_no_copy(x): x["rank"] = x.val.rank(method="min") return x.groupby("cat2")["rank"].min() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - grpby_copy = df.groupby("cat1").apply(f_copy) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - grpby_no_copy = df.groupby("cat1").apply(f_no_copy) + grpby_copy = df.groupby("cat1").apply(f_copy) + grpby_no_copy = df.groupby("cat1").apply(f_no_copy) tm.assert_series_equal(grpby_copy, grpby_no_copy) @@ -67,11 +58,8 @@ def test_no_mutate_but_looks_like(): # second does not, but should yield the same results df = pd.DataFrame({"key": [1, 1, 1, 2, 2, 2, 3, 3, 3], "value": range(9)}) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result1 = df.groupby("key", group_keys=True).apply(lambda x: x[:].key) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result2 = df.groupby("key", group_keys=True).apply(lambda x: x.key) + result1 = df.groupby("key", group_keys=True).apply(lambda x: x[:].value) + result2 = df.groupby("key", group_keys=True).apply(lambda x: x.value) tm.assert_series_equal(result1, result2) @@ -85,9 +73,7 @@ def fn(x): x.loc[x.index[-1], "col2"] = 0 return x.col2 - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(["col1"], as_index=False).apply(fn) + result = df.groupby(["col1"], as_index=False).apply(fn) expected = pd.Series( [1, 2, 0, 4, 5, 0], index=range(6), diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py index fffaee40a7d5c..656a61de5d105 100644 --- a/pandas/tests/groupby/test_categorical.py +++ b/pandas/tests/groupby/test_categorical.py @@ -127,10 +127,8 @@ def test_basic_string(using_infer_string): def f(x): return x.drop_duplicates("person_name").iloc[0] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = g.apply(f) - expected = x.iloc[[0, 1]].copy() + result = g.apply(f) + expected = x[["person_name"]].iloc[[0, 1]] expected.index = Index([1, 2], name="person_id") dtype = "str" if using_infer_string else object expected["person_name"] = expected["person_name"].astype(dtype) @@ -314,9 +312,7 @@ def test_apply(ordered): # but for transform we should still get back the original index idx = MultiIndex.from_arrays([missing, dense], names=["missing", "dense"]) expected = Series(1, index=idx) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grouped.apply(lambda x: 1) + result = grouped.apply(lambda x: 1) tm.assert_series_equal(result, expected) @@ -1357,11 +1353,7 @@ def test_get_nonexistent_category(): # Accessing a Category that is not in the dataframe df = DataFrame({"var": ["a", "a", "b", "b"], "val": range(4)}) with pytest.raises(KeyError, match="'vau'"): - df.groupby("var").apply( - lambda rows: DataFrame( - {"var": [rows.iloc[-1]["var"]], "val": [rows.iloc[-1]["vau"]]} - ) - ) + df.groupby("var").apply(lambda rows: DataFrame({"val": [rows.iloc[-1]["vau"]]})) def test_series_groupby_on_2_categoricals_unobserved(reduction_func, observed): @@ -2034,10 +2026,7 @@ def test_category_order_apply(as_index, sort, observed, method, index_kind, orde df["a2"] = df["a"] df = df.set_index(keys) gb = df.groupby(keys, as_index=as_index, sort=sort, observed=observed) - warn = DeprecationWarning if method == "apply" and index_kind == "range" else None - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(warn, match=msg): - op_result = getattr(gb, method)(lambda x: x.sum(numeric_only=True)) + op_result = getattr(gb, method)(lambda x: x.sum(numeric_only=True)) if (method == "transform" or not as_index) and index_kind == "range": result = op_result["a"].cat.categories else: diff --git a/pandas/tests/groupby/test_counting.py b/pandas/tests/groupby/test_counting.py index 47ad18c9ad2c8..679f7eb7f7f11 100644 --- a/pandas/tests/groupby/test_counting.py +++ b/pandas/tests/groupby/test_counting.py @@ -289,9 +289,7 @@ def test_count(): for key in ["1st", "2nd", ["1st", "2nd"]]: left = df.groupby(key).count() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - right = df.groupby(key).apply(DataFrame.count).drop(key, axis=1) + right = df.groupby(key).apply(DataFrame.count) tm.assert_frame_equal(left, right) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index e6c7eede1a401..c4c1e7bd9ac4f 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -66,11 +66,9 @@ def test_groupby_nonobject_dtype_mixed(): def max_value(group): return group.loc[group["value"].idxmax()] - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - applied = df.groupby("A").apply(max_value) + applied = df.groupby("A").apply(max_value) result = applied.dtypes - expected = df.dtypes + expected = df.drop(columns="A").dtypes tm.assert_series_equal(result, expected) @@ -229,11 +227,8 @@ def f3(x): df2 = DataFrame({"a": [3, 2, 2, 2], "b": range(4), "c": range(5, 9)}) # correct result - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result1 = df.groupby("a").apply(f1) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result2 = df2.groupby("a").apply(f1) + result1 = df.groupby("a").apply(f1) + result2 = df2.groupby("a").apply(f1) tm.assert_frame_equal(result1, result2) # should fail (not the same number of levels) @@ -1055,17 +1050,13 @@ def summarize_random_name(df): # Provide a different name for each Series. In this case, groupby # should not attempt to propagate the Series name since they are # inconsistent. - return Series({"count": 1, "mean": 2, "omissions": 3}, name=df.iloc[0]["A"]) + return Series({"count": 1, "mean": 2, "omissions": 3}, name=df.iloc[0]["C"]) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - metrics = df.groupby("A").apply(summarize) + metrics = df.groupby("A").apply(summarize) assert metrics.columns.name is None - with tm.assert_produces_warning(DeprecationWarning, match=msg): - metrics = df.groupby("A").apply(summarize, "metrics") + metrics = df.groupby("A").apply(summarize, "metrics") assert metrics.columns.name == "metrics" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - metrics = df.groupby("A").apply(summarize_random_name) + metrics = df.groupby("A").apply(summarize_random_name) assert metrics.columns.name is None @@ -1361,10 +1352,8 @@ def test_dont_clobber_name_column(): {"key": ["a", "a", "a", "b", "b", "b"], "name": ["foo", "bar", "baz"] * 2} ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("key", group_keys=False).apply(lambda x: x) - tm.assert_frame_equal(result, df) + result = df.groupby("key", group_keys=False).apply(lambda x: x) + tm.assert_frame_equal(result, df[["name"]]) def test_skip_group_keys(): @@ -1441,9 +1430,7 @@ def freducex(x): grouped = df.groupby(grouper, group_keys=False) # make sure all these work - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - grouped.apply(f) + grouped.apply(f) grouped.aggregate(freduce) grouped.aggregate({"C": freduce, "D": freduce}) grouped.transform(f) @@ -1464,10 +1451,7 @@ def f(group): names.append(group.name) return group.copy() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.groupby("a", sort=False, group_keys=False).apply(f) - + df.groupby("a", sort=False, group_keys=False).apply(f) expected_names = [0, 1, 2] assert names == expected_names @@ -1672,9 +1656,7 @@ def test_groupby_preserves_sort(sort_column, group_column): def test_sort(x): tm.assert_frame_equal(x, x.sort_values(by=sort_column)) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - g.apply(test_sort) + g.apply(test_sort) def test_pivot_table_values_key_error(): @@ -1860,10 +1842,8 @@ def test_empty_groupby_apply_nonunique_columns(): df[3] = df[3].astype(np.int64) df.columns = [0, 1, 2, 0] gb = df.groupby(df[1], group_keys=False) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - res = gb.apply(lambda x: x) - assert (res.dtypes == df.dtypes).all() + res = gb.apply(lambda x: x) + assert (res.dtypes == df.drop(columns=1).dtypes).all() def test_tuple_as_grouping(): diff --git a/pandas/tests/groupby/test_groupby_dropna.py b/pandas/tests/groupby/test_groupby_dropna.py index 060a8b7fd3824..8c4ab42b7be7a 100644 --- a/pandas/tests/groupby/test_groupby_dropna.py +++ b/pandas/tests/groupby/test_groupby_dropna.py @@ -323,9 +323,7 @@ def test_groupby_apply_with_dropna_for_multi_index(dropna, data, selected_data, df = pd.DataFrame(data) gb = df.groupby("groups", dropna=dropna) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = gb.apply(lambda grp: pd.DataFrame({"values": range(len(grp))})) + result = gb.apply(lambda grp: pd.DataFrame({"values": range(len(grp))})) mi_tuples = tuple(zip(data["groups"], selected_data["values"])) mi = pd.MultiIndex.from_tuples(mi_tuples, names=["groups", None]) diff --git a/pandas/tests/groupby/test_groupby_subclass.py b/pandas/tests/groupby/test_groupby_subclass.py index c81e7ecb1446d..3ee9c9ea0c7fd 100644 --- a/pandas/tests/groupby/test_groupby_subclass.py +++ b/pandas/tests/groupby/test_groupby_subclass.py @@ -72,18 +72,11 @@ def func(group): assert group.testattr == "hello" return group.testattr - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning( - DeprecationWarning, - match=msg, - raise_on_extra_warnings=False, - check_stacklevel=False, - ): - result = custom_df.groupby("c").apply(func) + result = custom_df.groupby("c").apply(func) expected = tm.SubclassedSeries(["hello"] * 3, index=Index([7, 8, 9], name="c")) tm.assert_series_equal(result, expected) - result = custom_df.groupby("c").apply(func, include_groups=False) + result = custom_df.groupby("c").apply(func) tm.assert_series_equal(result, expected) # https://github.com/pandas-dev/pandas/pull/56761 @@ -124,12 +117,5 @@ def test_groupby_resample_preserves_subclass(obj): df = df.set_index("Date") # Confirm groupby.resample() preserves dataframe type - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning( - DeprecationWarning, - match=msg, - raise_on_extra_warnings=False, - check_stacklevel=False, - ): - result = df.groupby("Buyer").resample("5D").sum() + result = df.groupby("Buyer").resample("5D").sum() assert isinstance(result, obj) diff --git a/pandas/tests/groupby/test_grouping.py b/pandas/tests/groupby/test_grouping.py index 4e7c0acb127ed..53e9c53efebf7 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -233,11 +233,7 @@ def test_grouper_creation_bug(self): result = g.sum() tm.assert_frame_equal(result, expected) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = g.apply(lambda x: x.sum()) - expected["A"] = [0, 2, 4] - expected = expected.loc[:, ["A", "B"]] + result = g.apply(lambda x: x.sum()) tm.assert_frame_equal(result, expected) def test_grouper_creation_bug2(self): @@ -788,7 +784,7 @@ def test_groupby_apply_empty_with_group_keys_false(self): # different index objects. df = DataFrame({"A": [], "B": [], "C": []}) g = df.groupby("A", group_keys=False) - result = g.apply(lambda x: x / x.sum(), include_groups=False) + result = g.apply(lambda x: x / x.sum()) expected = DataFrame({"B": [], "C": []}, index=None) tm.assert_frame_equal(result, expected) @@ -872,9 +868,7 @@ def test_groupby_tuple_keys_handle_multiindex(self): } ) expected = df.sort_values(by=["category_tuple", "num1"]) - result = df.groupby("category_tuple").apply( - lambda x: x.sort_values(by="num1"), include_groups=False - ) + result = df.groupby("category_tuple").apply(lambda x: x.sort_values(by="num1")) expected = expected[result.columns] tm.assert_frame_equal(result.reset_index(drop=True), expected) diff --git a/pandas/tests/groupby/test_timegrouper.py b/pandas/tests/groupby/test_timegrouper.py index a7712d9dc6586..550efe9187fe8 100644 --- a/pandas/tests/groupby/test_timegrouper.py +++ b/pandas/tests/groupby/test_timegrouper.py @@ -483,12 +483,8 @@ def test_timegrouper_apply_return_type_series(self): def sumfunc_series(x): return Series([x["value"].sum()], ("sum",)) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby(Grouper(key="date")).apply(sumfunc_series) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df_dt.groupby(Grouper(freq="ME", key="date")).apply(sumfunc_series) + expected = df.groupby(Grouper(key="date")).apply(sumfunc_series) + result = df_dt.groupby(Grouper(freq="ME", key="date")).apply(sumfunc_series) tm.assert_frame_equal( result.reset_index(drop=True), expected.reset_index(drop=True) ) @@ -504,11 +500,8 @@ def test_timegrouper_apply_return_type_value(self): def sumfunc_value(x): return x.value.sum() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby(Grouper(key="date")).apply(sumfunc_value) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df_dt.groupby(Grouper(freq="ME", key="date")).apply(sumfunc_value) + expected = df.groupby(Grouper(key="date")).apply(sumfunc_value) + result = df_dt.groupby(Grouper(freq="ME", key="date")).apply(sumfunc_value) tm.assert_series_equal( result.reset_index(drop=True), expected.reset_index(drop=True) ) @@ -934,9 +927,7 @@ def test_groupby_apply_timegrouper_with_nat_apply_squeeze( assert gb._selected_obj.index.nlevels == 1 # function that returns a Series - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - res = gb.apply(lambda x: x["Quantity"] * 2) + res = gb.apply(lambda x: x["Quantity"] * 2) dti = Index([Timestamp("2013-12-31")], dtype=df["Date"].dtype, name="Date") expected = DataFrame( diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py index f506126f9cf6f..888b97f2e0206 100644 --- a/pandas/tests/groupby/transform/test_transform.py +++ b/pandas/tests/groupby/transform/test_transform.py @@ -531,15 +531,13 @@ def f(group): return group[:1] grouped = df.groupby("c") - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = grouped.apply(f) + result = grouped.apply(f) assert result["d"].dtype == np.float64 # this is by definition a mutating operation! for key, group in grouped: - res = f(group) + res = f(group.drop(columns="c")) tm.assert_frame_equal(res, result.loc[key]) @@ -685,18 +683,14 @@ def test_cython_transform_frame(request, op, args, targop, df_fix, gb_target): f = gb[["float", "float_missing"]].apply(targop) expected = concat([f, i], axis=1) else: - if op != "shift" or not isinstance(gb_target.get("by"), (str, list)): - warn = None - else: - warn = DeprecationWarning - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(warn, match=msg): - expected = gb.apply(targop) + expected = gb.apply(targop) expected = expected.sort_index(axis=1) if op == "shift": expected["string_missing"] = expected["string_missing"].fillna(np.nan) - expected["string"] = expected["string"].fillna(np.nan) + by = gb_target.get("by") + if not isinstance(by, (str, list)) or (by != "string" and "string" not in by): + expected["string"] = expected["string"].fillna(np.nan) result = gb[expected.columns].transform(op, *args).sort_index(axis=1) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index 179f2c0e6cfa9..3a7fd548ca961 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -1022,12 +1022,8 @@ def test_resample_segfault(unit): all_wins_and_wagers, columns=("ID", "timestamp", "A", "B") ).set_index("timestamp") df.index = df.index.as_unit(unit) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("ID").resample("5min").sum() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby("ID").apply(lambda x: x.resample("5min").sum()) + result = df.groupby("ID").resample("5min").sum() + expected = df.groupby("ID").apply(lambda x: x.resample("5min").sum()) tm.assert_frame_equal(result, expected) @@ -1046,9 +1042,7 @@ def test_resample_dtype_preservation(unit): result = df.resample("1D").ffill() assert result.val.dtype == np.int32 - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("group").resample("1D").ffill() + result = df.groupby("group").resample("1D").ffill() assert result.val.dtype == np.int32 @@ -1821,12 +1815,8 @@ def f(data, add_arg): multiplier = 10 df = DataFrame({"A": 1, "B": 2}, index=date_range("2017", periods=10)) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").resample("D").agg(f, multiplier).astype(float) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby("A").resample("D").mean().multiply(multiplier) + result = df.groupby("A").resample("D").agg(f, multiplier).astype(float) + expected = df.groupby("A").resample("D").mean().multiply(multiplier) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_resample_api.py b/pandas/tests/resample/test_resample_api.py index b7b80b5e427ff..da1774cf22587 100644 --- a/pandas/tests/resample/test_resample_api.py +++ b/pandas/tests/resample/test_resample_api.py @@ -76,9 +76,7 @@ def test_groupby_resample_api(): ) index = pd.MultiIndex.from_arrays([[1] * 8 + [2] * 8, i], names=["group", "date"]) expected = DataFrame({"val": [5] * 7 + [6] + [7] * 7 + [8]}, index=index) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("group").apply(lambda x: x.resample("1D").ffill())[["val"]] + result = df.groupby("group").apply(lambda x: x.resample("1D").ffill())[["val"]] tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_resampler_grouper.py b/pandas/tests/resample/test_resampler_grouper.py index ff1b82210e20d..e7850f96b3b0f 100644 --- a/pandas/tests/resample/test_resampler_grouper.py +++ b/pandas/tests/resample/test_resampler_grouper.py @@ -71,12 +71,8 @@ def test_deferred_with_groupby(): def f_0(x): return x.set_index("date").resample("D").asfreq() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby("id").apply(f_0) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.set_index("date").groupby("id").resample("D").asfreq() + expected = df.groupby("id").apply(f_0) + result = df.set_index("date").groupby("id").resample("D").asfreq() tm.assert_frame_equal(result, expected) df = DataFrame( @@ -90,12 +86,8 @@ def f_0(x): def f_1(x): return x.resample("1D").ffill() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = df.groupby("group").apply(f_1) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("group").resample("1D").ffill() + expected = df.groupby("group").apply(f_1) + result = df.groupby("group").resample("1D").ffill() tm.assert_frame_equal(result, expected) @@ -110,9 +102,7 @@ def test_getitem(test_frame): result = g.B.resample("2s").mean() tm.assert_series_equal(result, expected) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = g.resample("2s").mean().B + result = g.resample("2s").mean().B tm.assert_series_equal(result, expected) @@ -236,12 +226,8 @@ def test_methods(f, test_frame): g = test_frame.groupby("A") r = g.resample("2s") - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = getattr(r, f)() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: getattr(x.resample("2s"), f)()) + result = getattr(r, f)() + expected = g.apply(lambda x: getattr(x.resample("2s"), f)()) tm.assert_equal(result, expected) @@ -258,12 +244,8 @@ def test_methods_nunique(test_frame): def test_methods_std_var(f, test_frame): g = test_frame.groupby("A") r = g.resample("2s") - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = getattr(r, f)(ddof=1) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: getattr(x.resample("2s"), f)(ddof=1)) + result = getattr(r, f)(ddof=1) + expected = g.apply(lambda x: getattr(x.resample("2s"), f)(ddof=1)) tm.assert_frame_equal(result, expected) @@ -272,24 +254,18 @@ def test_apply(test_frame): r = g.resample("2s") # reduction - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.resample("2s").sum() + expected = g.resample("2s").sum() def f_0(x): return x.resample("2s").sum() - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = r.apply(f_0) + result = r.apply(f_0) tm.assert_frame_equal(result, expected) def f_1(x): return x.resample("2s").apply(lambda y: y.sum()) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = g.apply(f_1) + result = g.apply(f_1) # y.sum() results in int64 instead of int32 on 32-bit architectures expected = expected.astype("int64") tm.assert_frame_equal(result, expected) @@ -357,9 +333,7 @@ def test_resample_groupby_with_label(unit): # GH 13235 index = date_range("2000-01-01", freq="2D", periods=5, unit=unit) df = DataFrame(index=index, data={"col0": [0, 0, 1, 1, 2], "col1": [1, 1, 1, 1, 1]}) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("col0").resample("1W", label="left").sum() + result = df.groupby("col0").resample("1W", label="left").sum() mi = [ np.array([0, 0, 1, 2], dtype=np.int64), @@ -369,9 +343,7 @@ def test_resample_groupby_with_label(unit): ), ] mindex = pd.MultiIndex.from_arrays(mi, names=["col0", None]) - expected = DataFrame( - data={"col0": [0, 0, 2, 2], "col1": [1, 1, 2, 1]}, index=mindex - ) + expected = DataFrame(data={"col1": [1, 1, 2, 1]}, index=mindex) tm.assert_frame_equal(result, expected) @@ -380,9 +352,7 @@ def test_consistency_with_window(test_frame): # consistent return values with window df = test_frame expected = Index([1, 2, 3], name="A") - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").resample("2s").mean() + result = df.groupby("A").resample("2s").mean() assert result.index.nlevels == 2 tm.assert_index_equal(result.index.levels[0], expected) @@ -479,13 +449,12 @@ def test_resample_groupby_agg_listlike(): def test_empty(keys): # GH 26411 df = DataFrame([], columns=["a", "b"], index=TimedeltaIndex([])) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(keys).resample(rule=pd.to_timedelta("00:00:01")).mean() + result = df.groupby(keys).resample(rule=pd.to_timedelta("00:00:01")).mean() + expected_columns = ["b"] if keys == ["a"] else [] expected = ( DataFrame(columns=["a", "b"]) .set_index(keys, drop=False) - .set_index(TimedeltaIndex([]), append=True) + .set_index(TimedeltaIndex([]), append=True)[expected_columns] ) if len(keys) == 1: expected.index.name = keys[0] @@ -505,9 +474,7 @@ def test_resample_groupby_agg_object_dtype_all_nan(consolidate): if consolidate: df = df._consolidate() - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(["key"]).resample("W", on="date").min() + result = df.groupby(["key"]).resample("W", on="date").min() idx = pd.MultiIndex.from_arrays( [ ["A"] * 3 + ["B"] * 3, @@ -519,7 +486,6 @@ def test_resample_groupby_agg_object_dtype_all_nan(consolidate): ) expected = DataFrame( { - "key": ["A"] * 3 + ["B"] * 3, "col1": [0, 5, 12] * 2, "col_object": ["val"] * 3 + [np.nan] * 3, }, @@ -557,12 +523,11 @@ def test_resample_no_index(keys): df = DataFrame([], columns=["a", "b", "date"]) df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date") - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby(keys).resample(rule=pd.to_timedelta("00:00:01")).mean() + result = df.groupby(keys).resample(rule=pd.to_timedelta("00:00:01")).mean() + expected_columns = ["b"] if keys == ["a"] else [] expected = DataFrame(columns=["a", "b", "date"]).set_index(keys, drop=False) expected["date"] = pd.to_datetime(expected["date"]) - expected = expected.set_index("date", append=True, drop=True) + expected = expected.set_index("date", append=True, drop=True)[expected_columns] if len(keys) == 1: expected.index.name = keys[0] @@ -606,9 +571,7 @@ def test_groupby_resample_size_all_index_same(): {"A": [1] * 3 + [2] * 3 + [1] * 3 + [2] * 3, "B": np.arange(12)}, index=date_range("31/12/2000 18:00", freq="h", periods=12), ) - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = df.groupby("A").resample("D").size() + result = df.groupby("A").resample("D").size() mi_exp = pd.MultiIndex.from_arrays( [ diff --git a/pandas/tests/resample/test_time_grouper.py b/pandas/tests/resample/test_time_grouper.py index f694b90a707c7..30e2c9dfe3d30 100644 --- a/pandas/tests/resample/test_time_grouper.py +++ b/pandas/tests/resample/test_time_grouper.py @@ -351,14 +351,11 @@ def test_groupby_resample_interpolate_raises(groupy_test_df): dfs = [groupy_test_df, groupy_test_df_without_index_name] for df in dfs: - msg = "DataFrameGroupBy.resample operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - with pytest.raises( - NotImplementedError, - match="Direct interpolation of MultiIndex data frames is " - "not supported", - ): - df.groupby("volume").resample("1D").interpolate(method="linear") + with pytest.raises( + NotImplementedError, + match="Direct interpolation of MultiIndex data frames is " "not supported", + ): + df.groupby("volume").resample("1D").interpolate(method="linear") def test_groupby_resample_interpolate_with_apply_syntax(groupy_test_df): @@ -373,7 +370,6 @@ def test_groupby_resample_interpolate_with_apply_syntax(groupy_test_df): for df in dfs: result = df.groupby("volume").apply( lambda x: x.resample("1D").interpolate(method="linear"), - include_groups=False, ) volume = [50] * 15 + [60] @@ -417,7 +413,7 @@ def test_groupby_resample_interpolate_with_apply_syntax_off_grid(groupy_test_df) See GH#21351.""" # GH#21351 result = groupy_test_df.groupby("volume").apply( - lambda x: x.resample("265h").interpolate(method="linear"), include_groups=False + lambda x: x.resample("265h").interpolate(method="linear") ) volume = [50, 50, 60] diff --git a/pandas/tests/window/test_groupby.py b/pandas/tests/window/test_groupby.py index f8e804bf434e9..f53250378e33c 100644 --- a/pandas/tests/window/test_groupby.py +++ b/pandas/tests/window/test_groupby.py @@ -101,11 +101,7 @@ def test_rolling(self, f, roll_frame): r = g.rolling(window=4) result = getattr(r, f)() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: getattr(x.rolling(4), f)()) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply(lambda x: getattr(x.rolling(4), f)()) # GH 39732 expected_index = MultiIndex.from_arrays([roll_frame["A"], range(40)]) expected.index = expected_index @@ -117,11 +113,7 @@ def test_rolling_ddof(self, f, roll_frame): r = g.rolling(window=4) result = getattr(r, f)(ddof=1) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: getattr(x.rolling(4), f)(ddof=1)) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply(lambda x: getattr(x.rolling(4), f)(ddof=1)) # GH 39732 expected_index = MultiIndex.from_arrays([roll_frame["A"], range(40)]) expected.index = expected_index @@ -135,13 +127,9 @@ def test_rolling_quantile(self, interpolation, roll_frame): r = g.rolling(window=4) result = r.quantile(0.4, interpolation=interpolation) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply( - lambda x: x.rolling(4).quantile(0.4, interpolation=interpolation) - ) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply( + lambda x: x.rolling(4).quantile(0.4, interpolation=interpolation) + ) # GH 39732 expected_index = MultiIndex.from_arrays([roll_frame["A"], range(40)]) expected.index = expected_index @@ -182,9 +170,7 @@ def test_rolling_corr_cov_other_diff_size_as_groups(self, f, roll_frame): def func(x): return getattr(x.rolling(4), f)(roll_frame) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(func) + expected = g.apply(func) # GH 39591: The grouped column should be all np.nan # (groupby.apply inserts 0s for cov) expected["A"] = np.nan @@ -200,9 +186,7 @@ def test_rolling_corr_cov_pairwise(self, f, roll_frame): def func(x): return getattr(x.B.rolling(4), f)(pairwise=True) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(func) + expected = g.apply(func) tm.assert_series_equal(result, expected) @pytest.mark.parametrize( @@ -247,11 +231,7 @@ def test_rolling_apply(self, raw, roll_frame): # reduction result = r.apply(lambda x: x.sum(), raw=raw) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: x.rolling(4).apply(lambda y: y.sum(), raw=raw)) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply(lambda x: x.rolling(4).apply(lambda y: y.sum(), raw=raw)) # GH 39732 expected_index = MultiIndex.from_arrays([roll_frame["A"], range(40)]) expected.index = expected_index @@ -826,13 +806,9 @@ def test_groupby_rolling_resulting_multiindex3(self): def test_groupby_rolling_object_doesnt_affect_groupby_apply(self, roll_frame): # GH 39732 g = roll_frame.groupby("A", group_keys=False) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: x.rolling(4).sum()).index + expected = g.apply(lambda x: x.rolling(4).sum()).index _ = g.rolling(window=4) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - result = g.apply(lambda x: x.rolling(4).sum()).index + result = g.apply(lambda x: x.rolling(4).sum()).index tm.assert_index_equal(result, expected) @pytest.mark.parametrize( @@ -1008,13 +984,11 @@ def test_groupby_monotonic(self): df["date"] = to_datetime(df["date"]) df = df.sort_values("date") - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = ( - df.set_index("date") - .groupby("name") - .apply(lambda x: x.rolling("180D")["amount"].sum()) - ) + expected = ( + df.set_index("date") + .groupby("name") + .apply(lambda x: x.rolling("180D")["amount"].sum()) + ) result = df.groupby("name").rolling("180D", on="date")["amount"].sum() tm.assert_series_equal(result, expected) @@ -1033,13 +1007,9 @@ def test_datelike_on_monotonic_within_each_group(self): } ) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = ( - df.set_index("B") - .groupby("A") - .apply(lambda x: x.rolling("4s")["C"].mean()) - ) + expected = ( + df.set_index("B").groupby("A").apply(lambda x: x.rolling("4s")["C"].mean()) + ) result = df.groupby("A").rolling("4s", on="B").C.mean() tm.assert_series_equal(result, expected) @@ -1069,11 +1039,7 @@ def test_expanding(self, f, frame): r = g.expanding() result = getattr(r, f)() - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: getattr(x.expanding(), f)()) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply(lambda x: getattr(x.expanding(), f)()) # GH 39732 expected_index = MultiIndex.from_arrays([frame["A"], range(40)]) expected.index = expected_index @@ -1085,11 +1051,7 @@ def test_expanding_ddof(self, f, frame): r = g.expanding() result = getattr(r, f)(ddof=0) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(lambda x: getattr(x.expanding(), f)(ddof=0)) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply(lambda x: getattr(x.expanding(), f)(ddof=0)) # GH 39732 expected_index = MultiIndex.from_arrays([frame["A"], range(40)]) expected.index = expected_index @@ -1103,13 +1065,9 @@ def test_expanding_quantile(self, interpolation, frame): r = g.expanding() result = r.quantile(0.4, interpolation=interpolation) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply( - lambda x: x.expanding().quantile(0.4, interpolation=interpolation) - ) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply( + lambda x: x.expanding().quantile(0.4, interpolation=interpolation) + ) # GH 39732 expected_index = MultiIndex.from_arrays([frame["A"], range(40)]) expected.index = expected_index @@ -1125,9 +1083,7 @@ def test_expanding_corr_cov(self, f, frame): def func_0(x): return getattr(x.expanding(), f)(frame) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(func_0) + expected = g.apply(func_0) # GH 39591: groupby.apply returns 1 instead of nan for windows # with all nan values null_idx = list(range(20, 61)) + list(range(72, 113)) @@ -1142,9 +1098,7 @@ def func_0(x): def func_1(x): return getattr(x.B.expanding(), f)(pairwise=True) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply(func_1) + expected = g.apply(func_1) tm.assert_series_equal(result, expected) def test_expanding_apply(self, raw, frame): @@ -1153,13 +1107,7 @@ def test_expanding_apply(self, raw, frame): # reduction result = r.apply(lambda x: x.sum(), raw=raw) - msg = "DataFrameGroupBy.apply operated on the grouping columns" - with tm.assert_produces_warning(DeprecationWarning, match=msg): - expected = g.apply( - lambda x: x.expanding().apply(lambda y: y.sum(), raw=raw) - ) - # groupby.apply doesn't drop the grouped-by column - expected = expected.drop("A", axis=1) + expected = g.apply(lambda x: x.expanding().apply(lambda y: y.sum(), raw=raw)) # GH 39732 expected_index = MultiIndex.from_arrays([frame["A"], range(40)]) expected.index = expected_index From edf00e953e6e185345fbc488cd9a963ab2d59d58 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:01:59 -0800 Subject: [PATCH 165/266] TST: Address matplotlib 3.10 deprecation of vert= (#60584) * TST: Address matplotlib 3.10 deprecation of vert= * Type in ._version * Address other failures * more test faillures * Add more xfails * mypy error --- pandas/plotting/_matplotlib/boxplot.py | 4 +- pandas/plotting/_matplotlib/tools.py | 2 +- pandas/tests/plotting/frame/test_frame.py | 41 ++++++++++++---- pandas/tests/plotting/test_boxplot_method.py | 50 +++++++++++++++----- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index 68682344f98ca..5ad30a68ae3c9 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -20,6 +20,7 @@ import pandas as pd import pandas.core.common as com +from pandas.util.version import Version from pandas.io.formats.printing import pprint_thing from pandas.plotting._matplotlib.core import ( @@ -54,7 +55,8 @@ def _set_ticklabels(ax: Axes, labels: list[str], is_vertical: bool, **kwargs) -> ticks = ax.get_xticks() if is_vertical else ax.get_yticks() if len(ticks) != len(labels): i, remainder = divmod(len(ticks), len(labels)) - assert remainder == 0, remainder + if Version(mpl.__version__) < Version("3.10"): + assert remainder == 0, remainder labels *= i if is_vertical: ax.set_xticklabels(labels, **kwargs) diff --git a/pandas/plotting/_matplotlib/tools.py b/pandas/plotting/_matplotlib/tools.py index d5624aecd1215..8ee75e7fe553e 100644 --- a/pandas/plotting/_matplotlib/tools.py +++ b/pandas/plotting/_matplotlib/tools.py @@ -56,7 +56,7 @@ def format_date_labels(ax: Axes, rot) -> None: fig = ax.get_figure() if fig is not None: # should always be a Figure but can technically be None - maybe_adjust_figure(fig, bottom=0.2) + maybe_adjust_figure(fig, bottom=0.2) # type: ignore[arg-type] def table( diff --git a/pandas/tests/plotting/frame/test_frame.py b/pandas/tests/plotting/frame/test_frame.py index 845f369d3090f..d18f098267599 100644 --- a/pandas/tests/plotting/frame/test_frame.py +++ b/pandas/tests/plotting/frame/test_frame.py @@ -1070,28 +1070,43 @@ def test_boxplot_series_positions(self, hist_df): tm.assert_numpy_array_equal(ax.xaxis.get_ticklocs(), positions) assert len(ax.lines) == 7 * len(numeric_cols) + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") + @pytest.mark.xfail( + Version(mpl.__version__) >= Version("3.10"), + reason="Fails starting with matplotlib 3.10", + ) def test_boxplot_vertical(self, hist_df): df = hist_df numeric_cols = df._get_numeric_data().columns labels = [pprint_thing(c) for c in numeric_cols] # if horizontal, yticklabels are rotated - ax = df.plot.box(rot=50, fontsize=8, vert=False) + kwargs = ( + {"vert": False} + if Version(mpl.__version__) < Version("3.10") + else {"orientation": "horizontal"} + ) + ax = df.plot.box(rot=50, fontsize=8, **kwargs) _check_ticks_props(ax, xrot=0, yrot=50, ylabelsize=8) _check_text_labels(ax.get_yticklabels(), labels) assert len(ax.lines) == 7 * len(numeric_cols) - @pytest.mark.filterwarnings("ignore:Attempt:UserWarning") + @pytest.mark.filterwarnings("ignore::UserWarning") + @pytest.mark.xfail( + Version(mpl.__version__) >= Version("3.10"), + reason="Fails starting with matplotlib version 3.10", + ) def test_boxplot_vertical_subplots(self, hist_df): df = hist_df numeric_cols = df._get_numeric_data().columns labels = [pprint_thing(c) for c in numeric_cols] + kwargs = ( + {"vert": False} + if Version(mpl.__version__) < Version("3.10") + else {"orientation": "horizontal"} + ) axes = _check_plot_works( - df.plot.box, - default_axes=True, - subplots=True, - vert=False, - logx=True, + df.plot.box, default_axes=True, subplots=True, logx=True, **kwargs ) _check_axes_shape(axes, axes_num=3, layout=(1, 3)) _check_ax_scales(axes, xaxis="log") @@ -1099,12 +1114,22 @@ def test_boxplot_vertical_subplots(self, hist_df): _check_text_labels(ax.get_yticklabels(), [label]) assert len(ax.lines) == 7 + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") + @pytest.mark.xfail( + Version(mpl.__version__) >= Version("3.10"), + reason="Fails starting with matplotlib 3.10", + ) def test_boxplot_vertical_positions(self, hist_df): df = hist_df numeric_cols = df._get_numeric_data().columns labels = [pprint_thing(c) for c in numeric_cols] positions = np.array([3, 2, 8]) - ax = df.plot.box(positions=positions, vert=False) + kwargs = ( + {"vert": False} + if Version(mpl.__version__) < Version("3.10") + else {"orientation": "horizontal"} + ) + ax = df.plot.box(positions=positions, **kwargs) _check_text_labels(ax.get_yticklabels(), labels) tm.assert_numpy_array_equal(ax.yaxis.get_ticklocs(), positions) assert len(ax.lines) == 7 * len(numeric_cols) diff --git a/pandas/tests/plotting/test_boxplot_method.py b/pandas/tests/plotting/test_boxplot_method.py index 4916963ab7c87..2267b6197cd80 100644 --- a/pandas/tests/plotting/test_boxplot_method.py +++ b/pandas/tests/plotting/test_boxplot_method.py @@ -1,5 +1,7 @@ """Test cases for .boxplot method""" +from __future__ import annotations + import itertools import string @@ -22,6 +24,7 @@ _check_ticks_props, _check_visible, ) +from pandas.util.version import Version from pandas.io.formats.printing import pprint_thing @@ -35,6 +38,17 @@ def _check_ax_limits(col, ax): assert y_max >= col.max() +if Version(mpl.__version__) < Version("3.10"): + verts: list[dict[str, bool | str]] = [{"vert": False}, {"vert": True}] +else: + verts = [{"orientation": "horizontal"}, {"orientation": "vertical"}] + + +@pytest.fixture(params=verts) +def vert(request): + return request.param + + class TestDataFramePlots: def test_stacked_boxplot_set_axis(self): # GH2980 @@ -312,7 +326,7 @@ def test_specified_props_kwd(self, props, expected): assert result[expected][0].get_color() == "C1" - @pytest.mark.parametrize("vert", [True, False]) + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") def test_plot_xlabel_ylabel(self, vert): df = DataFrame( { @@ -322,11 +336,11 @@ def test_plot_xlabel_ylabel(self, vert): } ) xlabel, ylabel = "x", "y" - ax = df.plot(kind="box", vert=vert, xlabel=xlabel, ylabel=ylabel) + ax = df.plot(kind="box", xlabel=xlabel, ylabel=ylabel, **vert) assert ax.get_xlabel() == xlabel assert ax.get_ylabel() == ylabel - @pytest.mark.parametrize("vert", [True, False]) + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") def test_plot_box(self, vert): # GH 54941 rng = np.random.default_rng(2) @@ -335,13 +349,13 @@ def test_plot_box(self, vert): xlabel, ylabel = "x", "y" _, axs = plt.subplots(ncols=2, figsize=(10, 7), sharey=True) - df1.plot.box(ax=axs[0], vert=vert, xlabel=xlabel, ylabel=ylabel) - df2.plot.box(ax=axs[1], vert=vert, xlabel=xlabel, ylabel=ylabel) + df1.plot.box(ax=axs[0], xlabel=xlabel, ylabel=ylabel, **vert) + df2.plot.box(ax=axs[1], xlabel=xlabel, ylabel=ylabel, **vert) for ax in axs: assert ax.get_xlabel() == xlabel assert ax.get_ylabel() == ylabel - @pytest.mark.parametrize("vert", [True, False]) + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") def test_boxplot_xlabel_ylabel(self, vert): df = DataFrame( { @@ -351,11 +365,11 @@ def test_boxplot_xlabel_ylabel(self, vert): } ) xlabel, ylabel = "x", "y" - ax = df.boxplot(vert=vert, xlabel=xlabel, ylabel=ylabel) + ax = df.boxplot(xlabel=xlabel, ylabel=ylabel, **vert) assert ax.get_xlabel() == xlabel assert ax.get_ylabel() == ylabel - @pytest.mark.parametrize("vert", [True, False]) + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") def test_boxplot_group_xlabel_ylabel(self, vert): df = DataFrame( { @@ -365,13 +379,19 @@ def test_boxplot_group_xlabel_ylabel(self, vert): } ) xlabel, ylabel = "x", "y" - ax = df.boxplot(by="group", vert=vert, xlabel=xlabel, ylabel=ylabel) + ax = df.boxplot(by="group", xlabel=xlabel, ylabel=ylabel, **vert) for subplot in ax: assert subplot.get_xlabel() == xlabel assert subplot.get_ylabel() == ylabel - @pytest.mark.parametrize("vert", [True, False]) - def test_boxplot_group_no_xlabel_ylabel(self, vert): + @pytest.mark.filterwarnings("ignore:set_ticklabels:UserWarning") + def test_boxplot_group_no_xlabel_ylabel(self, vert, request): + if Version(mpl.__version__) >= Version("3.10") and vert == { + "orientation": "horizontal" + }: + request.applymarker( + pytest.mark.xfail(reason=f"{vert} fails starting with matplotlib 3.10") + ) df = DataFrame( { "a": np.random.default_rng(2).standard_normal(10), @@ -379,9 +399,13 @@ def test_boxplot_group_no_xlabel_ylabel(self, vert): "group": np.random.default_rng(2).choice(["group1", "group2"], 10), } ) - ax = df.boxplot(by="group", vert=vert) + ax = df.boxplot(by="group", **vert) for subplot in ax: - target_label = subplot.get_xlabel() if vert else subplot.get_ylabel() + target_label = ( + subplot.get_xlabel() + if vert == {"vert": True} or vert == {"orientation": "vertical"} + else subplot.get_ylabel() + ) assert target_label == pprint_thing(["group"]) From 602ae10f3d0d599ebbdd151e8a09f0baf20b4637 Mon Sep 17 00:00:00 2001 From: William Andrea <22385371+wjandrea@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:31:11 -0400 Subject: [PATCH 166/266] DOC: Fix "kwargs" description for .assign() (#60588) Fix "kwargs" description for .assign() "kwargs" isn't a dict; the keyword arguments are *converted* to a dict. Secondly, keyword arguments are strings by definition. --- pandas/core/frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 34b448a0d8d1c..02878b36a379e 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -5009,7 +5009,7 @@ def assign(self, **kwargs) -> DataFrame: Parameters ---------- - **kwargs : dict of {str: callable or Series} + **kwargs : callable or Series The column names are keywords. If the values are callable, they are computed on the DataFrame and assigned to the new columns. The callable must not From 8a5344742c5165b2595f7ccca9e17d5eff7f7886 Mon Sep 17 00:00:00 2001 From: Abdulaziz Aloqeely <52792999+Aloqeely@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:21:44 +0300 Subject: [PATCH 167/266] PDEP-17: Backwards compatibility and deprecation policy (#59125) --- ...ds-compatibility-and-deprecation-policy.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 web/pandas/pdeps/0017-backwards-compatibility-and-deprecation-policy.md diff --git a/web/pandas/pdeps/0017-backwards-compatibility-and-deprecation-policy.md b/web/pandas/pdeps/0017-backwards-compatibility-and-deprecation-policy.md new file mode 100644 index 0000000000000..b8eba90f399c9 --- /dev/null +++ b/web/pandas/pdeps/0017-backwards-compatibility-and-deprecation-policy.md @@ -0,0 +1,74 @@ +# PDEP-17: Backwards compatibility and deprecation policy + +- Created: 27 June 2024 +- Status: Accepted +- Discussion: [#59125](https://github.com/pandas-dev/pandas/issues/59125) +- Author: [Abdulaziz Aloqeely](https://github.com/Aloqeely) +- Revision: 1 + +## Abstract + +This PDEP defines pandas' backwards compatibility and deprecation policy. + +The main additions to [pandas' current version policy](https://pandas.pydata.org/pandas-docs/version/2.2/development/policies.html) are: +- Deprecated functionality should remain unchanged in at least 2 minor releases before being changed or removed. +- Deprecations should initially use DeprecationWarning, and then be switched to FutureWarning in the last minor release before the major release they are planned to be removed in + +## Motivation + +Having a clear backwards compatibility and deprecation policy is crucial to having a healthy ecosystem. We want to ensure users can rely on pandas being stable while still allowing the library to evolve. + +This policy will ensure that users have enough time to deal with deprecations while also minimizing disruptions on downstream packages' users. + +## Scope + +This PDEP covers pandas' approach to backwards compatibility and the deprecation and removal process. + +## Background + +pandas uses a loose variant of semantic versioning. +A pandas release number is written in the format of ``MAJOR.MINOR.PATCH``. + +## General policy + +This policy applies to the [public API][1]. Anything not part of the [public API][1] or is marked as "Experimental" may be changed or removed at anytime. + +- Breaking backwards compatibility should benefit more than it harms users. +- Breaking changes should go through a deprecation cycle before being implemented if possible. +- Breaking changes should only occur in major releases. +- No deprecations should be introduced in patch releases. +- Deprecated functionality should remain unchanged in at least 2 minor releases before being changed or removed. + +Some bug fixes may require breaking backwards compatibility. In these cases, a deprecation cycle is not necessary. However, bug fixes which have a large impact on users might be treated as a breaking change. Whether or not a change is a bug fix or an API breaking change is a judgement call. + +## Deprecation process + +Deprecation provides a way to warn developers and give them time to adapt their code to the new functionality before the old behavior is eventually removed. + +A deprecation's warning message should: +- Provide information on what is changing. +- Mention how to achieve similar behavior if an alternative is available. +- For large-scale deprecations, it is recommended to include a reason for the deprecation, alongside a discussion link to get user feedback. + +Additionally, when one introduces a deprecation, they should: +- Use the appropriate warning class. More info on this can be found below. +- Add the GitHub issue/PR number as a comment above the warning line. +- Add an entry in the release notes. +- Mention that the functionality is deprecated in the documentation using the ``.. deprecated::`` directive. + +### Which warning class to use + +Deprecations should initially use ``DeprecationWarning``, and then be switched to ``FutureWarning`` for broader visibility in the last minor release before the major release they are planned to be removed in. +This implementation detail can be ignored by using the appropriate ``PandasDeprecationWarning`` variable, which will be aliased to the proper warning class based on the pandas version. + +### Enforcement of deprecations + +When one enforces a deprecation, they should: +- Add an entry in the release notes. +- For API changes, replace the ``.. deprecated::`` directive in the documentation with a ``.. versionchanged::`` directive. + +### PDEP-17 History + +- 27 June 2024: Initial version. + +[1]: https://pandas.pydata.org/docs/reference/index.html From 59b3a1a1a770ff1bd8311e7c1f1d4b1f918dcd4c Mon Sep 17 00:00:00 2001 From: "Christine P. Chai" Date: Fri, 27 Dec 2024 06:21:12 -0800 Subject: [PATCH 168/266] DOC: Change Twitter to X in pandas maintenance (#60598) --- doc/source/development/maintaining.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/development/maintaining.rst b/doc/source/development/maintaining.rst index 1e4a851d0e72d..c572559dcc3e0 100644 --- a/doc/source/development/maintaining.rst +++ b/doc/source/development/maintaining.rst @@ -488,7 +488,7 @@ Post-Release for reference): - The pandas-dev and pydata mailing lists - - Twitter, Mastodon, Telegram and LinkedIn + - X, Mastodon, Telegram and LinkedIn 7. Update this release instructions to fix anything incorrect and to update about any change since the last release. From 82f4354b94ad95790d8f67323929ae6871c04b1b Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 29 Dec 2024 14:29:56 -0500 Subject: [PATCH 169/266] TST(string dtype): Resolve to_latex xfail (#60614) TST(string dtype): Fix to_latex xfail --- pandas/io/formats/style.py | 2 +- pandas/tests/io/formats/style/test_to_latex.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index eb6773310da69..6f164c4b97514 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1644,7 +1644,7 @@ def _update_ctx_header(self, attrs: DataFrame, axis: AxisInt) -> None: for j in attrs.columns: ser = attrs[j] for i, c in ser.items(): - if not c: + if not c or pd.isna(c): continue css_list = maybe_convert_css_to_tuples(c) if axis == 0: diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 1abe6238d3922..eb221686dd165 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, MultiIndex, @@ -731,7 +729,6 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp): ) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize("index", [True, False]) @pytest.mark.parametrize( "columns, siunitx", From 2edc7c9ad9a8b2e1f8df981def5b5b0c434d9ab0 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 29 Dec 2024 14:32:14 -0500 Subject: [PATCH 170/266] TST(string dtype): Resolve some HDF5 xfails (#60615) * TST(string dtype): Resolve HDF5 xfails * More xfails * Cleanup --- pandas/io/pytables.py | 2 + .../tests/io/pytables/test_file_handling.py | 45 ++++++++++++++----- pandas/tests/io/pytables/test_subclass.py | 3 -- pandas/tests/io/test_common.py | 3 -- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py index 7d265bc430125..b75dc6c3a43b4 100644 --- a/pandas/io/pytables.py +++ b/pandas/io/pytables.py @@ -5297,6 +5297,8 @@ def _dtype_to_kind(dtype_str: str) -> str: kind = "integer" elif dtype_str == "object": kind = "object" + elif dtype_str == "str": + kind = "str" else: raise ValueError(f"cannot interpret dtype of [{dtype_str}]") diff --git a/pandas/tests/io/pytables/test_file_handling.py b/pandas/tests/io/pytables/test_file_handling.py index 606b19ac0ed75..16c3c6798ff76 100644 --- a/pandas/tests/io/pytables/test_file_handling.py +++ b/pandas/tests/io/pytables/test_file_handling.py @@ -37,12 +37,11 @@ pytestmark = [ pytest.mark.single_cpu, - pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False), ] @pytest.mark.parametrize("mode", ["r", "r+", "a", "w"]) -def test_mode(setup_path, tmp_path, mode): +def test_mode(setup_path, tmp_path, mode, using_infer_string): df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), columns=Index(list("ABCD"), dtype=object), @@ -91,10 +90,12 @@ def test_mode(setup_path, tmp_path, mode): read_hdf(path, "df", mode=mode) else: result = read_hdf(path, "df", mode=mode) + if using_infer_string: + df.columns = df.columns.astype("str") tm.assert_frame_equal(result, df) -def test_default_mode(tmp_path, setup_path): +def test_default_mode(tmp_path, setup_path, using_infer_string): # read_hdf uses default mode df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), @@ -104,7 +105,10 @@ def test_default_mode(tmp_path, setup_path): path = tmp_path / setup_path df.to_hdf(path, key="df", mode="w") result = read_hdf(path, "df") - tm.assert_frame_equal(result, df) + expected = df.copy() + if using_infer_string: + expected.columns = expected.columns.astype("str") + tm.assert_frame_equal(result, expected) def test_reopen_handle(tmp_path, setup_path): @@ -163,7 +167,7 @@ def test_reopen_handle(tmp_path, setup_path): assert not store.is_open -def test_open_args(setup_path): +def test_open_args(setup_path, using_infer_string): with tm.ensure_clean(setup_path) as path: df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), @@ -178,8 +182,13 @@ def test_open_args(setup_path): store["df"] = df store.append("df2", df) - tm.assert_frame_equal(store["df"], df) - tm.assert_frame_equal(store["df2"], df) + expected = df.copy() + if using_infer_string: + expected.index = expected.index.astype("str") + expected.columns = expected.columns.astype("str") + + tm.assert_frame_equal(store["df"], expected) + tm.assert_frame_equal(store["df2"], expected) store.close() @@ -194,7 +203,7 @@ def test_flush(setup_path): store.flush(fsync=True) -def test_complibs_default_settings(tmp_path, setup_path): +def test_complibs_default_settings(tmp_path, setup_path, using_infer_string): # GH15943 df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), @@ -207,7 +216,11 @@ def test_complibs_default_settings(tmp_path, setup_path): tmpfile = tmp_path / setup_path df.to_hdf(tmpfile, key="df", complevel=9) result = read_hdf(tmpfile, "df") - tm.assert_frame_equal(result, df) + expected = df.copy() + if using_infer_string: + expected.index = expected.index.astype("str") + expected.columns = expected.columns.astype("str") + tm.assert_frame_equal(result, expected) with tables.open_file(tmpfile, mode="r") as h5file: for node in h5file.walk_nodes(where="/df", classname="Leaf"): @@ -218,7 +231,11 @@ def test_complibs_default_settings(tmp_path, setup_path): tmpfile = tmp_path / setup_path df.to_hdf(tmpfile, key="df", complib="zlib") result = read_hdf(tmpfile, "df") - tm.assert_frame_equal(result, df) + expected = df.copy() + if using_infer_string: + expected.index = expected.index.astype("str") + expected.columns = expected.columns.astype("str") + tm.assert_frame_equal(result, expected) with tables.open_file(tmpfile, mode="r") as h5file: for node in h5file.walk_nodes(where="/df", classname="Leaf"): @@ -229,7 +246,11 @@ def test_complibs_default_settings(tmp_path, setup_path): tmpfile = tmp_path / setup_path df.to_hdf(tmpfile, key="df") result = read_hdf(tmpfile, "df") - tm.assert_frame_equal(result, df) + expected = df.copy() + if using_infer_string: + expected.index = expected.index.astype("str") + expected.columns = expected.columns.astype("str") + tm.assert_frame_equal(result, expected) with tables.open_file(tmpfile, mode="r") as h5file: for node in h5file.walk_nodes(where="/df", classname="Leaf"): @@ -308,6 +329,7 @@ def test_complibs(tmp_path, lvl, lib, request): assert node.filters.complib == lib +@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.skipif( not is_platform_little_endian(), reason="reason platform is not little endian" ) @@ -325,6 +347,7 @@ def test_encoding(setup_path): tm.assert_frame_equal(result, expected) +@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "val", [ diff --git a/pandas/tests/io/pytables/test_subclass.py b/pandas/tests/io/pytables/test_subclass.py index bbe1cd77e0d9f..03622faa2b5a8 100644 --- a/pandas/tests/io/pytables/test_subclass.py +++ b/pandas/tests/io/pytables/test_subclass.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, Series, @@ -19,7 +17,6 @@ class TestHDFStoreSubclass: # GH 33748 - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_supported_for_subclass_dataframe(self, tmp_path): data = {"a": [1, 2], "b": [3, 4]} sdf = tm.SubclassedDataFrame(data, dtype=np.intp) diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 70422a0ea6edc..7ff3d24336f00 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -19,8 +19,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat import ( WASM, is_platform_windows, @@ -365,7 +363,6 @@ def test_write_fspath_all(self, writer_name, writer_kwargs, module): expected = f_path.read() assert result == expected - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) hdf support") def test_write_fspath_hdf5(self): # Same test as write_fspath_all, except HDF5 files aren't # necessarily byte-for-byte identical for a given dataframe, so we'll From 9cf491132b536d9e6c096ce245fc6ddef6ea8030 Mon Sep 17 00:00:00 2001 From: Dhruv B Shetty Date: Sun, 29 Dec 2024 21:33:24 +0200 Subject: [PATCH 171/266] TST: Test .loc #25548 for matched and unmatched indices of Series (#60450) * Added test for .loc to test setitem on matching indices * precommit workflow * modified from np.NaN to np.nan * formatting fixes * Added result and expected variables * Added result and expected variables for both tests --------- Co-authored-by: dshettyepi --- pandas/tests/indexing/test_loc.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index e0e9d4cfc5ccb..7aeded5a6cb7f 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -3297,3 +3297,23 @@ def test_loc_reindexing_of_empty_index(self): df.loc[Series([False] * 4, index=df.index, name=0), 0] = df[0] expected = DataFrame(index=[1, 1, 2, 2], data=["1", "1", "2", "2"]) tm.assert_frame_equal(df, expected) + + def test_loc_setitem_matching_index(self): + # GH 25548 + s = Series(0.0, index=list("abcd")) + s1 = Series(1.0, index=list("ab")) + s2 = Series(2.0, index=list("xy")) + + # Test matching indices + s.loc[["a", "b"]] = s1 + + result = s[["a", "b"]] + expected = s1 + tm.assert_series_equal(result, expected) + + # Test unmatched indices + s.loc[["a", "b"]] = s2 + + result = s[["a", "b"]] + expected = Series([np.nan, np.nan], index=["a", "b"]) + tm.assert_series_equal(result, expected) From 37f4392d411896c88fab3c6702a8d16560213f27 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:29:22 -0500 Subject: [PATCH 172/266] TST/CLN: Remove groupby tests with mutation (#60619) --- pandas/tests/groupby/test_apply_mutate.py | 37 ++++++----------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/pandas/tests/groupby/test_apply_mutate.py b/pandas/tests/groupby/test_apply_mutate.py index 970334917faab..ee0912175f024 100644 --- a/pandas/tests/groupby/test_apply_mutate.py +++ b/pandas/tests/groupby/test_apply_mutate.py @@ -38,18 +38,20 @@ def test_mutate_groups(): } ) - def f_copy(x): + def f(x): x = x.copy() x["rank"] = x.val.rank(method="min") return x.groupby("cat2")["rank"].min() - def f_no_copy(x): - x["rank"] = x.val.rank(method="min") - return x.groupby("cat2")["rank"].min() - - grpby_copy = df.groupby("cat1").apply(f_copy) - grpby_no_copy = df.groupby("cat1").apply(f_no_copy) - tm.assert_series_equal(grpby_copy, grpby_no_copy) + expected = pd.DataFrame( + { + "cat1": list("aaaabbb"), + "cat2": list("cdefcde"), + "rank": [3.0, 2.0, 5.0, 1.0, 2.0, 4.0, 1.0], + } + ).set_index(["cat1", "cat2"])["rank"] + result = df.groupby("cat1").apply(f) + tm.assert_series_equal(result, expected) def test_no_mutate_but_looks_like(): @@ -61,22 +63,3 @@ def test_no_mutate_but_looks_like(): result1 = df.groupby("key", group_keys=True).apply(lambda x: x[:].value) result2 = df.groupby("key", group_keys=True).apply(lambda x: x.value) tm.assert_series_equal(result1, result2) - - -def test_apply_function_with_indexing(): - # GH: 33058 - df = pd.DataFrame( - {"col1": ["A", "A", "A", "B", "B", "B"], "col2": [1, 2, 3, 4, 5, 6]} - ) - - def fn(x): - x.loc[x.index[-1], "col2"] = 0 - return x.col2 - - result = df.groupby(["col1"], as_index=False).apply(fn) - expected = pd.Series( - [1, 2, 0, 4, 5, 0], - index=range(6), - name="col2", - ) - tm.assert_series_equal(result, expected) From d81882b2a38c020c5b2474ec7b4962fee8a41cc9 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:31:10 -0500 Subject: [PATCH 173/266] TST/CLN: Improve some groupby.apply tests (#60620) --- pandas/tests/groupby/test_apply.py | 56 +++++++++++++++++------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index fd1c82932f57f..ae73ddc001dc1 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -255,19 +255,19 @@ def test_apply_with_mixed_dtype(): "foo2": ["one", "two", "two", "three", "one", "two"], } ) - result = df.apply(lambda x: x, axis=1).dtypes - expected = df.dtypes - tm.assert_series_equal(result, expected) + result = df.apply(lambda x: x, axis=1) + expected = df + tm.assert_frame_equal(result, expected) # GH 3610 incorrect dtype conversion with as_index=False df = DataFrame({"c1": [1, 2, 6, 6, 8]}) df["c2"] = df.c1 / 2.0 - result1 = df.groupby("c2").mean().reset_index().c2 - result2 = df.groupby("c2", as_index=False).mean().c2 - tm.assert_series_equal(result1, result2) + result1 = df.groupby("c2").mean().reset_index() + result2 = df.groupby("c2", as_index=False).mean() + tm.assert_frame_equal(result1, result2) -def test_groupby_as_index_apply(): +def test_groupby_as_index_apply(as_index): # GH #4648 and #3417 df = DataFrame( { @@ -276,27 +276,35 @@ def test_groupby_as_index_apply(): "time": range(6), } ) + gb = df.groupby("user_id", as_index=as_index) - g_as = df.groupby("user_id", as_index=True) - g_not_as = df.groupby("user_id", as_index=False) - - res_as = g_as.head(2).index - res_not_as = g_not_as.head(2).index - exp = Index([0, 1, 2, 4]) - tm.assert_index_equal(res_as, exp) - tm.assert_index_equal(res_not_as, exp) - - res_as_apply = g_as.apply(lambda x: x.head(2)).index - res_not_as_apply = g_not_as.apply(lambda x: x.head(2)).index + expected = DataFrame( + { + "item_id": ["b", "b", "a", "a"], + "user_id": [1, 2, 1, 3], + "time": [0, 1, 2, 4], + }, + index=[0, 1, 2, 4], + ) + result = gb.head(2) + tm.assert_frame_equal(result, expected) # apply doesn't maintain the original ordering # changed in GH5610 as the as_index=False returns a MI here - exp_not_as_apply = Index([0, 2, 1, 4]) - tp = [(1, 0), (1, 2), (2, 1), (3, 4)] - exp_as_apply = MultiIndex.from_tuples(tp, names=["user_id", None]) - - tm.assert_index_equal(res_as_apply, exp_as_apply) - tm.assert_index_equal(res_not_as_apply, exp_not_as_apply) + if as_index: + tp = [(1, 0), (1, 2), (2, 1), (3, 4)] + index = MultiIndex.from_tuples(tp, names=["user_id", None]) + else: + index = Index([0, 2, 1, 4]) + expected = DataFrame( + { + "item_id": list("baba"), + "time": [0, 2, 1, 4], + }, + index=index, + ) + result = gb.apply(lambda x: x.head(2)) + tm.assert_frame_equal(result, expected) def test_groupby_as_index_apply_str(): From 2c7c6d6340a24012e5f79d4d383889d28aca2c27 Mon Sep 17 00:00:00 2001 From: dajale423 <40189578+dajale423@users.noreply.github.com> Date: Tue, 31 Dec 2024 00:08:40 +0900 Subject: [PATCH 174/266] DOC: Remove Blank cell in `doc/source/user_guide/visualization.rst` (#60623) remove unnecessary cell from visualization doc --- doc/source/user_guide/visualization.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/source/user_guide/visualization.rst b/doc/source/user_guide/visualization.rst index 66eeb74b363a3..4b5cdca23103c 100644 --- a/doc/source/user_guide/visualization.rst +++ b/doc/source/user_guide/visualization.rst @@ -1210,11 +1210,6 @@ You may set the ``xlabel`` and ``ylabel`` arguments to give the plot custom labe for x and y axis. By default, pandas will pick up index name as xlabel, while leaving it empty for ylabel. -.. ipython:: python - :suppress: - - plt.figure(); - .. ipython:: python df.plot(); From b6fb6e7bdfd81978f5445d72f0758490abeb6edf Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:32:31 -0500 Subject: [PATCH 175/266] DOC: Make warning on query/eval consistent (#60628) --- pandas/core/computation/eval.py | 4 ++-- pandas/core/frame.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pandas/core/computation/eval.py b/pandas/core/computation/eval.py index 86f83489e71ae..9d844e590582a 100644 --- a/pandas/core/computation/eval.py +++ b/pandas/core/computation/eval.py @@ -190,8 +190,8 @@ def eval( .. warning:: - ``eval`` can run arbitrary code which can make you vulnerable to code - injection and untrusted data. + This function can run arbitrary code which can make you vulnerable to code + injection if you pass user input to this function. Parameters ---------- diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 02878b36a379e..851bc1ce4075c 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4476,8 +4476,10 @@ def query(self, expr: str, *, inplace: bool = False, **kwargs) -> DataFrame | No """ Query the columns of a DataFrame with a boolean expression. - This method can run arbitrary code which can make you vulnerable to code - injection if you pass user input to this function. + .. warning:: + + This method can run arbitrary code which can make you vulnerable to code + injection if you pass user input to this function. Parameters ---------- @@ -4634,6 +4636,11 @@ def eval(self, expr: str, *, inplace: bool = False, **kwargs) -> Any | None: """ Evaluate a string describing operations on DataFrame columns. + .. warning:: + + This method can run arbitrary code which can make you vulnerable to code + injection if you pass user input to this function. + Operates on columns only, not specific rows or elements. This allows `eval` to run arbitrary code, which can make you vulnerable to code injection if you pass user input to this function. From a8a84c8b8717a3cd8e56272c22c5d75c55568876 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:50:34 -0800 Subject: [PATCH 176/266] DOC: Fix numpydoc section underlines (#60630) --- pandas/_libs/tslibs/nattype.pyx | 2 +- pandas/_libs/tslibs/timestamps.pyx | 2 +- pandas/core/strings/accessor.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 1c0a99eb1ea25..2657b1b9d197b 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -704,7 +704,7 @@ class NaTType(_NaT): difference between the current timezone and UTC. Returns - -------- + ------- timedelta The difference between UTC and the local time as a `timedelta` object. diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index a3429fc840347..6b4b90167e625 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -2217,7 +2217,7 @@ class Timestamp(_Timestamp): difference between the current timezone and UTC. Returns - -------- + ------- timedelta The difference between UTC and the local time as a `timedelta` object. diff --git a/pandas/core/strings/accessor.py b/pandas/core/strings/accessor.py index c68b6303661b9..e5b434edacc59 100644 --- a/pandas/core/strings/accessor.py +++ b/pandas/core/strings/accessor.py @@ -3700,7 +3700,7 @@ def casefold(self): Series.str.isupper : Check whether all characters are uppercase. Examples - ------------ + -------- The ``s5.str.istitle`` method checks for whether all words are in title case (whether only the first letter of each word is capitalized). Words are assumed to be as any sequence of non-numeric characters separated by From 8fbe6ac83da590acfc58ff83713aac14ab7f900d Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:49:36 -0500 Subject: [PATCH 177/266] TST: Remove test_apply_mutate.py (#60631) Remove test_apply_mutate.py --- pandas/tests/groupby/test_apply.py | 50 +++++++++++++++++ pandas/tests/groupby/test_apply_mutate.py | 65 ----------------------- 2 files changed, 50 insertions(+), 65 deletions(-) delete mode 100644 pandas/tests/groupby/test_apply_mutate.py diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index ae73ddc001dc1..62d4a0ddcc0f5 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -227,6 +227,22 @@ def fast(group): tm.assert_frame_equal(fast_df, slow_df) +def test_apply_fast_slow_identical_index(): + # GH#44803 + df = DataFrame( + { + "name": ["Alice", "Bob", "Carl"], + "age": [20, 21, 20], + } + ).set_index("name") + + grp_by_same_value = df.groupby(["age"], group_keys=False).apply(lambda group: group) + grp_by_copy = df.groupby(["age"], group_keys=False).apply( + lambda group: group.copy() + ) + tm.assert_frame_equal(grp_by_same_value, grp_by_copy) + + @pytest.mark.parametrize( "func", [ @@ -1463,3 +1479,37 @@ def f_4(grp): e.loc["Pony"] = np.nan e.name = None tm.assert_series_equal(result, e) + + +def test_nonreducer_nonstransform(): + # GH3380, GH60619 + # Was originally testing mutating in a UDF; now kept as an example + # of using apply with a nonreducer and nontransformer. + df = DataFrame( + { + "cat1": ["a"] * 8 + ["b"] * 6, + "cat2": ["c"] * 2 + + ["d"] * 2 + + ["e"] * 2 + + ["f"] * 2 + + ["c"] * 2 + + ["d"] * 2 + + ["e"] * 2, + "val": np.random.default_rng(2).integers(100, size=14), + } + ) + + def f(x): + x = x.copy() + x["rank"] = x.val.rank(method="min") + return x.groupby("cat2")["rank"].min() + + expected = DataFrame( + { + "cat1": list("aaaabbb"), + "cat2": list("cdefcde"), + "rank": [3.0, 2.0, 5.0, 1.0, 2.0, 4.0, 1.0], + } + ).set_index(["cat1", "cat2"])["rank"] + result = df.groupby("cat1").apply(f) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/groupby/test_apply_mutate.py b/pandas/tests/groupby/test_apply_mutate.py deleted file mode 100644 index ee0912175f024..0000000000000 --- a/pandas/tests/groupby/test_apply_mutate.py +++ /dev/null @@ -1,65 +0,0 @@ -import numpy as np - -import pandas as pd -import pandas._testing as tm - - -def test_group_by_copy(): - # GH#44803 - df = pd.DataFrame( - { - "name": ["Alice", "Bob", "Carl"], - "age": [20, 21, 20], - } - ).set_index("name") - - grp_by_same_value = df.groupby(["age"], group_keys=False).apply(lambda group: group) - grp_by_copy = df.groupby(["age"], group_keys=False).apply( - lambda group: group.copy() - ) - tm.assert_frame_equal(grp_by_same_value, grp_by_copy) - - -def test_mutate_groups(): - # GH3380 - - df = pd.DataFrame( - { - "cat1": ["a"] * 8 + ["b"] * 6, - "cat2": ["c"] * 2 - + ["d"] * 2 - + ["e"] * 2 - + ["f"] * 2 - + ["c"] * 2 - + ["d"] * 2 - + ["e"] * 2, - "cat3": [f"g{x}" for x in range(1, 15)], - "val": np.random.default_rng(2).integers(100, size=14), - } - ) - - def f(x): - x = x.copy() - x["rank"] = x.val.rank(method="min") - return x.groupby("cat2")["rank"].min() - - expected = pd.DataFrame( - { - "cat1": list("aaaabbb"), - "cat2": list("cdefcde"), - "rank": [3.0, 2.0, 5.0, 1.0, 2.0, 4.0, 1.0], - } - ).set_index(["cat1", "cat2"])["rank"] - result = df.groupby("cat1").apply(f) - tm.assert_series_equal(result, expected) - - -def test_no_mutate_but_looks_like(): - # GH 8467 - # first show's mutation indicator - # second does not, but should yield the same results - df = pd.DataFrame({"key": [1, 1, 1, 2, 2, 2, 3, 3, 3], "value": range(9)}) - - result1 = df.groupby("key", group_keys=True).apply(lambda x: x[:].value) - result2 = df.groupby("key", group_keys=True).apply(lambda x: x.value) - tm.assert_series_equal(result1, result2) From 9d2d77054553c0b7e3a45a8901d41f09fa9e7599 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 2 Jan 2025 05:43:35 -0500 Subject: [PATCH 178/266] TST(string dtype): Resolve xfail with apply returning an ndarray (#60636) --- pandas/tests/frame/methods/test_dtypes.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pandas/tests/frame/methods/test_dtypes.py b/pandas/tests/frame/methods/test_dtypes.py index 1685f9ee331f5..bf01ec73cf72b 100644 --- a/pandas/tests/frame/methods/test_dtypes.py +++ b/pandas/tests/frame/methods/test_dtypes.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.dtypes import DatetimeTZDtype import pandas as pd @@ -135,13 +133,9 @@ def test_dtypes_timedeltas(self): ) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_frame_apply_np_array_return_type(self, using_infer_string): # GH 35517 df = DataFrame([["foo"]]) result = df.apply(lambda col: np.array("bar")) - if using_infer_string: - expected = Series([np.array(["bar"])]) - else: - expected = Series(["bar"]) + expected = Series(np.array("bar")) tm.assert_series_equal(result, expected) From 3bc44d4962d4c22de9d464e2135ca498b2db1e72 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 2 Jan 2025 05:45:17 -0500 Subject: [PATCH 179/266] TST(string dtype): Resolve xfail for corrwith (#60635) --- pandas/tests/frame/methods/test_cov_corr.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pandas/tests/frame/methods/test_cov_corr.py b/pandas/tests/frame/methods/test_cov_corr.py index c15952339ef18..d5e94382b8314 100644 --- a/pandas/tests/frame/methods/test_cov_corr.py +++ b/pandas/tests/frame/methods/test_cov_corr.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas.util._test_decorators as td import pandas as pd @@ -320,7 +318,6 @@ def test_corrwith_non_timeseries_data(self): for row in index[:4]: tm.assert_almost_equal(correls[row], df1.loc[row].corr(df2.loc[row])) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_corrwith_with_objects(self, using_infer_string): df1 = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), @@ -334,9 +331,8 @@ def test_corrwith_with_objects(self, using_infer_string): df2["obj"] = "bar" if using_infer_string: - import pyarrow as pa - - with pytest.raises(pa.lib.ArrowNotImplementedError, match="has no kernel"): + msg = "Cannot perform reduction 'mean' with string dtype" + with pytest.raises(TypeError, match=msg): df1.corrwith(df2) else: with pytest.raises(TypeError, match="Could not convert"): From e7c803c8cb688c983ee9b1b7b10fa89bad66b689 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 2 Jan 2025 05:50:50 -0500 Subject: [PATCH 180/266] TST(string dtype): Remove xfail for combine_first (#60634) --- pandas/tests/frame/methods/test_combine_first.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pandas/tests/frame/methods/test_combine_first.py b/pandas/tests/frame/methods/test_combine_first.py index 87b7d5052a345..a70876b5a96ca 100644 --- a/pandas/tests/frame/methods/test_combine_first.py +++ b/pandas/tests/frame/methods/test_combine_first.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.core.dtypes.cast import find_common_type from pandas.core.dtypes.common import is_dtype_equal @@ -32,8 +30,7 @@ def test_combine_first_mixed(self): combined = f.combine_first(g) tm.assert_frame_equal(combined, exp) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_combine_first(self, float_frame, using_infer_string): + def test_combine_first(self, float_frame): # disjoint head, tail = float_frame[:5], float_frame[5:] @@ -79,9 +76,7 @@ def test_combine_first(self, float_frame, using_infer_string): tm.assert_series_equal(combined["A"].reindex(g.index), g["A"]) # corner cases - warning = FutureWarning if using_infer_string else None - with tm.assert_produces_warning(warning, match="empty entries"): - comb = float_frame.combine_first(DataFrame()) + comb = float_frame.combine_first(DataFrame()) tm.assert_frame_equal(comb, float_frame) comb = DataFrame().combine_first(float_frame) From 32dc2e8aed3b53caf2c8e5ca00b2d009bbf52b83 Mon Sep 17 00:00:00 2001 From: Hassan Rad <115450900+HasssanRad@users.noreply.github.com> Date: Thu, 2 Jan 2025 21:10:25 +0330 Subject: [PATCH 181/266] DOC: Adding a Persian version of Pandas Cheat Sheet (#60612) * Adding a Persian version of Pandas Cheat Sheet * Update README.md after adding Pandas_Cheat_Sheet_FA * Update README.md and add a language column --- doc/cheatsheet/Pandas_Cheat_Sheet_FA.pdf | Bin 0 -> 518926 bytes doc/cheatsheet/Pandas_Cheat_Sheet_FA.pptx | Bin 0 -> 121904 bytes doc/cheatsheet/README.md | 10 ++++++---- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_FA.pdf create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_FA.pptx diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_FA.pdf b/doc/cheatsheet/Pandas_Cheat_Sheet_FA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ea356385e9fb1af29e9ed204a5f6de81a0dc882d GIT binary patch literal 518926 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 z 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

    HBptVcYY5MF&_)9D};B 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�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 wbb` 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 literal 0 HcmV?d00001 diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_FA.pptx b/doc/cheatsheet/Pandas_Cheat_Sheet_FA.pptx new file mode 100644 index 0000000000000000000000000000000000000000..1b812c1a2595a4b2a39daf2f9a913815b736e8fc GIT binary patch literal 121904 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 literal 0 HcmV?d00001 diff --git a/doc/cheatsheet/README.md b/doc/cheatsheet/README.md index 6c33de104ed90..b8599acff2f6e 100644 --- a/doc/cheatsheet/README.md +++ b/doc/cheatsheet/README.md @@ -6,10 +6,12 @@ and pick "PDF" as the format. This cheat sheet, originally written by Irv Lustig, [Princeton Consultants](https://www.princetonoptimization.com/), was inspired by the [RStudio Data Wrangling Cheatsheet](https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf). -| Topic | PDF | PPT | -|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Pandas_Cheat_Sheet | | | -| Pandas_Cheat_Sheet_JA | | | +| Topic | Language | PDF | PPT | +|------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Pandas_Cheat_Sheet | English | | | +| Pandas_Cheat_Sheet_JA | Japanese | | | +| Pandas_Cheat_Sheet_FA | Persian | | | + **Alternative** From 710f7d25710a6a2e19d3bbd4288b28571b6cca42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8D=8CShawn?= Date: Fri, 3 Jan 2025 02:28:41 +0800 Subject: [PATCH 182/266] Update license year (#60648) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 2d1c34fd9d7fc..c343da2ebe870 100644 --- a/LICENSE +++ b/LICENSE @@ -3,7 +3,7 @@ BSD 3-Clause License Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team All rights reserved. -Copyright (c) 2011-2024, Open source contributors. +Copyright (c) 2011-2025, Open source contributors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: From 6cf69632bcb44d43982a36795f80d912284378bc Mon Sep 17 00:00:00 2001 From: Quentin Lhoest <42851186+lhoestq@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:32:11 +0100 Subject: [PATCH 183/266] DOC: Add Hugging Face Hub access (#60608) * Update pyproject.toml * Update install.rst * Update io.rst * remove pip extra * Update ecosystem.md * link to docs * Revert change in io.rst --- web/pandas/community/ecosystem.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/pandas/community/ecosystem.md b/web/pandas/community/ecosystem.md index 6c69ff7602491..dc7b9bc947214 100644 --- a/web/pandas/community/ecosystem.md +++ b/web/pandas/community/ecosystem.md @@ -468,6 +468,31 @@ df.dtypes ArcticDB also supports appending, updating, and querying data from storage to a pandas DataFrame. Please find more information [here](https://docs.arcticdb.io/latest/api/query_builder/). +### [Hugging Face](https://huggingface.co/datasets) + +The Hugging Face Dataset Hub provides a large collection of ready-to-use datasets for machine learning shared by the community. The platform offers a user-friendly interface to explore, discover and visualize datasets, and provides tools to easily load and work with these datasets in Python thanks to the [huggingface_hub](https://github.com/huggingface/huggingface_hub) library. + +You can access datasets on Hugging Face using `hf://` paths in pandas, in the form `hf://datasets/username/dataset_name/...`. + +For example, here is how to load the [stanfordnlp/imdb dataset](https://huggingface.co/datasets/stanfordnlp/imdb): + +```python +import pandas as pd + +# Load the IMDB dataset +df = pd.read_parquet("hf://datasets/stanfordnlp/imdb/plain_text/train-00000-of-00001.parquet") +``` + +Tip: on a dataset page, click on "Use this dataset" to get the code to load it in pandas. + +To save a dataset on Hugging Face you need to [create a public or private dataset](https://huggingface.co/new-dataset) and [login](https://huggingface.co/docs/huggingface_hub/quick-start#login-command), and then you can use `df.to_csv/to_json/to_parquet`: + +```python +# Save the dataset to my Hugging Face account +df.to_parquet("hf://datasets/username/dataset_name/train.parquet") +``` + +You can find more information about the Hugging Face Dataset Hub in the [documentation](https://huggingface.co/docs/hub/en/datasets). ## Out-of-core From 990474be133221122497da0d923ecf818ed506d7 Mon Sep 17 00:00:00 2001 From: bluestarunderscore <157440828+bluestarunderscore@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:22:00 -0800 Subject: [PATCH 184/266] DOC: Clarify loc and iloc functionality in user_guide/indexing.html (#60501) * Fix iloc wording error on indexing user guide page * Update doc/source/user_guide/indexing.rst --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/user_guide/indexing.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/source/user_guide/indexing.rst b/doc/source/user_guide/indexing.rst index 503f7cc7cbe73..ed5c7806b2e23 100644 --- a/doc/source/user_guide/indexing.rst +++ b/doc/source/user_guide/indexing.rst @@ -858,9 +858,10 @@ and :ref:`Advanced Indexing ` you may select along more than one axis .. warning:: - ``iloc`` supports two kinds of boolean indexing. If the indexer is a boolean ``Series``, - an error will be raised. For instance, in the following example, ``df.iloc[s.values, 1]`` is ok. - The boolean indexer is an array. But ``df.iloc[s, 1]`` would raise ``ValueError``. + While ``loc`` supports two kinds of boolean indexing, ``iloc`` only supports indexing with a + boolean array. If the indexer is a boolean ``Series``, an error will be raised. For instance, + in the following example, ``df.iloc[s.values, 1]`` is ok. The boolean indexer is an array. + But ``df.iloc[s, 1]`` would raise ``ValueError``. .. ipython:: python From 76a20bd7af91f66c965f4f8f1d952a6515a58448 Mon Sep 17 00:00:00 2001 From: karnbirrandhawa <102620686+karnbirrandhawa@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:27:53 -0500 Subject: [PATCH 185/266] DOC: fix SA01 for pandas.arrays.NumpyExtensionArray (#60513) * DOC: fix SA01 for pandas.arrays.NumpyExtensionArray * Update pandas/core/arrays/numpy_.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- ci/code_checks.sh | 1 - pandas/core/arrays/numpy_.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 39cea0c361a72..c1d3f72f908ef 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -81,7 +81,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ - -i "pandas.arrays.NumpyExtensionArray SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index 9f7238a97d808..f222d6e1b328b 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -71,6 +71,11 @@ class NumpyExtensionArray( # type: ignore[misc] ------- None + See Also + -------- + array : Create an array. + Series.to_numpy : Convert a Series to a NumPy array. + Examples -------- >>> pd.arrays.NumpyExtensionArray(np.array([0, 1, 2, 3])) From b74ee4ab8cc73bf386d9cf9e9ad18a3af823b894 Mon Sep 17 00:00:00 2001 From: "Derek M. Knowlton" <97196662+knowltod@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:41:47 -0800 Subject: [PATCH 186/266] DOC: Fix some core.resample.Resampler docstrings (#60502) * fixed resample.Resampler docstring errors in min, max, mean, prod, std, var as indicated by vaildate_docstrings.py * fixed resample.Resampler docstring errors in min, max, mean, prod, std, var as indicated by vaildate_docstrings.py * fixed trailing whitespace on line 1161 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- ci/code_checks.sh | 6 ---- pandas/core/resample.py | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index c1d3f72f908ef..8ddbbe8945002 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -84,14 +84,8 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ - -i "pandas.core.resample.Resampler.max PR01,RT03,SA01" \ - -i "pandas.core.resample.Resampler.mean SA01" \ - -i "pandas.core.resample.Resampler.min PR01,RT03,SA01" \ - -i "pandas.core.resample.Resampler.prod SA01" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ - -i "pandas.core.resample.Resampler.std SA01" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ - -i "pandas.core.resample.Resampler.var SA01" \ -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 27e498683bf8f..b1b8aef31d3c4 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1096,6 +1096,13 @@ def prod( Series or DataFrame Computed prod of values within each group. + See Also + -------- + core.resample.Resampler.sum : Compute sum of groups, excluding missing values. + core.resample.Resampler.mean : Compute mean of groups, excluding missing values. + core.resample.Resampler.median : Compute median of groups, excluding missing + values. + Examples -------- >>> ser = pd.Series( @@ -1126,9 +1133,30 @@ def min( """ Compute min value of group. + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + + .. versionchanged:: 2.0.0 + + numeric_only no longer accepts ``None``. + + min_count : int, default 0 + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + Returns ------- Series or DataFrame + Compute the minimum value in the given Series or DataFrame. + + See Also + -------- + core.resample.Resampler.max : Compute max value of group. + core.resample.Resampler.mean : Compute mean of groups, excluding missing values. + core.resample.Resampler.median : Compute median of groups, excluding missing + values. Examples -------- @@ -1160,9 +1188,30 @@ def max( """ Compute max value of group. + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + + .. versionchanged:: 2.0.0 + + numeric_only no longer accepts ``None``. + + min_count : int, default 0 + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + Returns ------- Series or DataFrame + Computes the maximum value in the given Series or Dataframe. + + See Also + -------- + core.resample.Resampler.min : Compute min value of group. + core.resample.Resampler.mean : Compute mean of groups, excluding missing values. + core.resample.Resampler.median : Compute median of groups, excluding missing + values. Examples -------- @@ -1236,6 +1285,16 @@ def mean( DataFrame or Series Mean of values within each group. + See Also + -------- + core.resample.Resampler.median : Compute median of groups, excluding missing + values. + core.resample.Resampler.sum : Compute sum of groups, excluding missing values. + core.resample.Resampler.std : Compute standard deviation of groups, excluding + missing values. + core.resample.Resampler.var : Compute variance of groups, excluding missing + values. + Examples -------- @@ -1285,6 +1344,14 @@ def std( DataFrame or Series Standard deviation of values within each group. + See Also + -------- + core.resample.Resampler.mean : Compute mean of groups, excluding missing values. + core.resample.Resampler.median : Compute median of groups, excluding missing + values. + core.resample.Resampler.var : Compute variance of groups, excluding missing + values. + Examples -------- @@ -1336,6 +1403,14 @@ def var( DataFrame or Series Variance of values within each group. + See Also + -------- + core.resample.Resampler.std : Compute standard deviation of groups, excluding + missing values. + core.resample.Resampler.mean : Compute mean of groups, excluding missing values. + core.resample.Resampler.median : Compute median of groups, excluding missing + values. + Examples -------- From adb66890f9ec3234ea7aafec4a2e16330feb9a11 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 2 Jan 2025 20:49:06 +0100 Subject: [PATCH 187/266] String dtype: coerce missing values in indexers for string dtype Index (#60454) * String dtype: coerce missing values in indexers for string dtype Index * cleanup --- pandas/_libs/index.pyx | 10 +----- pandas/tests/frame/indexing/test_indexing.py | 3 -- pandas/tests/indexes/string/test_indexing.py | 33 ++++++++++---------- pandas/tests/reshape/test_pivot.py | 12 +++---- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 688f943760d1f..c219d0b63870f 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -561,23 +561,15 @@ cdef class StringObjectEngine(ObjectEngine): cdef: object na_value - bint uses_na def __init__(self, ndarray values, na_value): super().__init__(values) self.na_value = na_value - self.uses_na = na_value is C_NA - - cdef bint _checknull(self, object val): - if self.uses_na: - return val is C_NA - else: - return util.is_nan(val) cdef _check_type(self, object val): if isinstance(val, str): return val - elif self._checknull(val): + elif checknull(val): return self.na_value else: raise KeyError(val) diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 84c01e0be3b6f..a9bc485283985 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -9,8 +9,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs import iNaT from pandas.errors import InvalidIndexError @@ -503,7 +501,6 @@ def test_setitem_ambig(self, using_infer_string): else: assert dm[2].dtype == np.object_ - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_setitem_None(self, float_frame): # GH #766 float_frame[None] = float_frame["A"] diff --git a/pandas/tests/indexes/string/test_indexing.py b/pandas/tests/indexes/string/test_indexing.py index d1a278af337b7..648ee47ddc34c 100644 --- a/pandas/tests/indexes/string/test_indexing.py +++ b/pandas/tests/indexes/string/test_indexing.py @@ -13,6 +13,15 @@ def _isnan(val): return False +def _equivalent_na(dtype, null): + if dtype.na_value is pd.NA and null is pd.NA: + return True + elif _isnan(dtype.na_value) and _isnan(null): + return True + else: + return False + + class TestGetLoc: def test_get_loc(self, any_string_dtype): index = Index(["a", "b", "c"], dtype=any_string_dtype) @@ -41,14 +50,7 @@ def test_get_loc_non_missing(self, any_string_dtype, nulls_fixture): def test_get_loc_missing(self, any_string_dtype, nulls_fixture): index = Index(["a", "b", nulls_fixture], dtype=any_string_dtype) - if any_string_dtype == "string" and ( - (any_string_dtype.na_value is pd.NA and nulls_fixture is not pd.NA) - or (_isnan(any_string_dtype.na_value) and not _isnan(nulls_fixture)) - ): - with pytest.raises(KeyError): - index.get_loc(nulls_fixture) - else: - assert index.get_loc(nulls_fixture) == 2 + assert index.get_loc(nulls_fixture) == 2 class TestGetIndexer: @@ -93,9 +95,8 @@ def test_get_indexer_missing(self, any_string_dtype, null, using_infer_string): result = index.get_indexer(["a", null, "c"]) if using_infer_string: expected = np.array([0, 2, -1], dtype=np.intp) - elif any_string_dtype == "string" and ( - (any_string_dtype.na_value is pd.NA and null is not pd.NA) - or (_isnan(any_string_dtype.na_value) and not _isnan(null)) + elif any_string_dtype == "string" and not _equivalent_na( + any_string_dtype, null ): expected = np.array([0, -1, -1], dtype=np.intp) else: @@ -115,9 +116,8 @@ def test_get_indexer_non_unique_nas( if using_infer_string: expected_indexer = np.array([0, 2], dtype=np.intp) expected_missing = np.array([], dtype=np.intp) - elif any_string_dtype == "string" and ( - (any_string_dtype.na_value is pd.NA and null is not pd.NA) - or (_isnan(any_string_dtype.na_value) and not _isnan(null)) + elif any_string_dtype == "string" and not _equivalent_na( + any_string_dtype, null ): expected_indexer = np.array([0, -1], dtype=np.intp) expected_missing = np.array([1], dtype=np.intp) @@ -133,9 +133,8 @@ def test_get_indexer_non_unique_nas( if using_infer_string: expected_indexer = np.array([0, 1, 3], dtype=np.intp) - elif any_string_dtype == "string" and ( - (any_string_dtype.na_value is pd.NA and null is not pd.NA) - or (_isnan(any_string_dtype.na_value) and not _isnan(null)) + elif any_string_dtype == "string" and not _equivalent_na( + any_string_dtype, null ): pass else: diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index f42f7f8232229..374d236c8ff39 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -2668,6 +2668,8 @@ def test_pivot_columns_not_given(self): with pytest.raises(TypeError, match="missing 1 required keyword-only argument"): df.pivot() + # this still fails because columns=None gets passed down to unstack as level=None + # while at that point None was converted to NaN @pytest.mark.xfail( using_string_dtype(), reason="TODO(infer_string) None is cast to NaN" ) @@ -2686,10 +2688,7 @@ def test_pivot_columns_is_none(self): expected = DataFrame({1: 3}, index=Index([2], name="b")) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail( - using_string_dtype(), reason="TODO(infer_string) None is cast to NaN" - ) - def test_pivot_index_is_none(self): + def test_pivot_index_is_none(self, using_infer_string): # GH#48293 df = DataFrame({None: [1], "b": 2, "c": 3}) @@ -2700,11 +2699,10 @@ def test_pivot_index_is_none(self): result = df.pivot(columns="b", index=None, values="c") expected = DataFrame(3, index=[1], columns=Index([2], name="b")) + if using_infer_string: + expected.index.name = np.nan tm.assert_frame_equal(result, expected) - @pytest.mark.xfail( - using_string_dtype(), reason="TODO(infer_string) None is cast to NaN" - ) def test_pivot_values_is_none(self): # GH#48293 df = DataFrame({None: [1], "b": 2, "c": 3}) From 228627a576c2d78e2bd084814c46b851c3895c52 Mon Sep 17 00:00:00 2001 From: avecasey <102306692+avecasey@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:47:32 -0500 Subject: [PATCH 188/266] ENH: Added isascii() string method fixing issue #59091 (#60532) * first * second * Update object_array.py * third * ascii * ascii2 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * ascii3 * style * style * style * style * docs * reset * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update doc/source/whatsnew/v3.0.0.rst --------- Co-authored-by: Abby VeCasey Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/arrays/_arrow_string_mixins.py | 4 ++ pandas/core/strings/accessor.py | 46 +++++++++++++++++++++- pandas/core/strings/base.py | 4 ++ pandas/core/strings/object_array.py | 3 ++ pandas/tests/strings/conftest.py | 1 + pandas/tests/strings/test_string_array.py | 1 + pandas/tests/strings/test_strings.py | 2 + 8 files changed, 61 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 92c67865ae88f..94c289ef3ace7 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -60,6 +60,7 @@ Other enhancements - :meth:`Series.map` can now accept kwargs to pass on to func (:issue:`59814`) - :meth:`pandas.concat` will raise a ``ValueError`` when ``ignore_index=True`` and ``keys`` is not ``None`` (:issue:`59274`) - :meth:`str.get_dummies` now accepts a ``dtype`` parameter to specify the dtype of the resulting DataFrame (:issue:`47872`) +- Implemented :meth:`Series.str.isascii` and :meth:`Series.str.isascii` (:issue:`59091`) - Multiplying two :class:`DateOffset` objects will now raise a ``TypeError`` instead of a ``RecursionError`` (:issue:`59442`) - Restore support for reading Stata 104-format and enable reading 103-format dta files (:issue:`58554`) - Support passing a :class:`Iterable[Hashable]` input to :meth:`DataFrame.drop_duplicates` (:issue:`59237`) diff --git a/pandas/core/arrays/_arrow_string_mixins.py b/pandas/core/arrays/_arrow_string_mixins.py index 2d1b1eca55e98..1ca52ce64bd77 100644 --- a/pandas/core/arrays/_arrow_string_mixins.py +++ b/pandas/core/arrays/_arrow_string_mixins.py @@ -253,6 +253,10 @@ def _str_isalpha(self): result = pc.utf8_is_alpha(self._pa_array) return self._convert_bool_result(result) + def _str_isascii(self): + result = pc.string_is_ascii(self._pa_array) + return self._convert_bool_result(result) + def _str_isdecimal(self): result = pc.utf8_is_decimal(self._pa_array) return self._convert_bool_result(result) diff --git a/pandas/core/strings/accessor.py b/pandas/core/strings/accessor.py index e5b434edacc59..d3ccd11281a77 100644 --- a/pandas/core/strings/accessor.py +++ b/pandas/core/strings/accessor.py @@ -3415,7 +3415,8 @@ def len(self): # cases: # upper, lower, title, capitalize, swapcase, casefold # boolean: - # isalpha, isnumeric isalnum isdigit isdecimal isspace islower isupper istitle + # isalpha, isnumeric isalnum isdigit isdecimal isspace islower + # isupper istitle isascii # _doc_args holds dict of strings to use in substituting casemethod docs _doc_args: dict[str, dict[str, str]] = {} _doc_args["lower"] = {"type": "lowercase", "method": "lower", "version": ""} @@ -3495,6 +3496,7 @@ def casefold(self): Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3518,6 +3520,7 @@ def casefold(self): Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3544,6 +3547,7 @@ def casefold(self): Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3576,6 +3580,7 @@ def casefold(self): Series.str.isdigit : Check whether all characters are digits. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3601,6 +3606,7 @@ def casefold(self): Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3627,6 +3633,7 @@ def casefold(self): Series.str.isdigit : Check whether all characters are digits. Series.str.isdecimal : Check whether all characters are decimal. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3649,6 +3656,7 @@ def casefold(self): Series.str.isdigit : Check whether all characters are digits. Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Series.str.istitle : Check whether all characters are titlecase. @@ -3674,6 +3682,7 @@ def casefold(self): Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.istitle : Check whether all characters are titlecase. Examples @@ -3697,6 +3706,7 @@ def casefold(self): Series.str.isdecimal : Check whether all characters are decimal. Series.str.isspace : Check whether all characters are whitespace. Series.str.islower : Check whether all characters are lowercase. + Series.str.isascii : Check whether all characters are ascii. Series.str.isupper : Check whether all characters are uppercase. Examples @@ -3714,11 +3724,40 @@ def casefold(self): 3 False dtype: bool """ + _shared_docs["isascii"] = """ + See Also + -------- + Series.str.isalpha : Check whether all characters are alphabetic. + Series.str.isnumeric : Check whether all characters are numeric. + Series.str.isalnum : Check whether all characters are alphanumeric. + Series.str.isdigit : Check whether all characters are digits. + Series.str.isdecimal : Check whether all characters are decimal. + Series.str.isspace : Check whether all characters are whitespace. + Series.str.islower : Check whether all characters are lowercase. + Series.str.istitle : Check whether all characters are titlecase. + Series.str.isupper : Check whether all characters are uppercase. + + Examples + ------------ + The ``s5.str.isascii`` method checks for whether all characters are ascii + characters, which includes digits 0-9, capital and lowercase letters A-Z, + and some other special characters. + + >>> s5 = pd.Series(['ö', 'see123', 'hello world', '']) + >>> s5.str.isascii() + 0 False + 1 True + 2 True + 3 True + dtype: bool + """ + _doc_args["isalnum"] = {"type": "alphanumeric", "method": "isalnum"} _doc_args["isalpha"] = {"type": "alphabetic", "method": "isalpha"} _doc_args["isdigit"] = {"type": "digits", "method": "isdigit"} _doc_args["isspace"] = {"type": "whitespace", "method": "isspace"} _doc_args["islower"] = {"type": "lowercase", "method": "islower"} + _doc_args["isascii"] = {"type": "ascii", "method": "isascii"} _doc_args["isupper"] = {"type": "uppercase", "method": "isupper"} _doc_args["istitle"] = {"type": "titlecase", "method": "istitle"} _doc_args["isnumeric"] = {"type": "numeric", "method": "isnumeric"} @@ -3750,6 +3789,11 @@ def casefold(self): docstring=_shared_docs["ismethods"] % _doc_args["islower"] + _shared_docs["islower"], ) + isascii = _map_and_wrap( + "isascii", + docstring=_shared_docs["ismethods"] % _doc_args["isascii"] + + _shared_docs["isascii"], + ) isupper = _map_and_wrap( "isupper", docstring=_shared_docs["ismethods"] % _doc_args["isupper"] diff --git a/pandas/core/strings/base.py b/pandas/core/strings/base.py index 4ed36f85167c9..78c4f3acbe1aa 100644 --- a/pandas/core/strings/base.py +++ b/pandas/core/strings/base.py @@ -179,6 +179,10 @@ def _str_isalnum(self): def _str_isalpha(self): pass + @abc.abstractmethod + def _str_isascii(self): + pass + @abc.abstractmethod def _str_isdecimal(self): pass diff --git a/pandas/core/strings/object_array.py b/pandas/core/strings/object_array.py index 0268194e64d50..a07ab9534f491 100644 --- a/pandas/core/strings/object_array.py +++ b/pandas/core/strings/object_array.py @@ -455,6 +455,9 @@ def _str_isalnum(self): def _str_isalpha(self): return self._str_map(str.isalpha, dtype="bool") + def _str_isascii(self): + return self._str_map(str.isascii, dtype="bool") + def _str_isdecimal(self): return self._str_map(str.isdecimal, dtype="bool") diff --git a/pandas/tests/strings/conftest.py b/pandas/tests/strings/conftest.py index 92b7b16da3c1f..5bcbb16da3be9 100644 --- a/pandas/tests/strings/conftest.py +++ b/pandas/tests/strings/conftest.py @@ -68,6 +68,7 @@ "get_dummies", "isalnum", "isalpha", + "isascii", "isdecimal", "isdigit", "islower", diff --git a/pandas/tests/strings/test_string_array.py b/pandas/tests/strings/test_string_array.py index cd3c512328139..c5414022e664b 100644 --- a/pandas/tests/strings/test_string_array.py +++ b/pandas/tests/strings/test_string_array.py @@ -83,6 +83,7 @@ def test_string_array_numeric_integer_array(nullable_string_dtype, method, expec [ ("isdigit", [False, None, True]), ("isalpha", [True, None, False]), + ("isascii", [True, None, True]), ("isalnum", [True, None, True]), ("isnumeric", [False, None, True]), ], diff --git a/pandas/tests/strings/test_strings.py b/pandas/tests/strings/test_strings.py index 75a2007b61640..0598e5f80e6d6 100644 --- a/pandas/tests/strings/test_strings.py +++ b/pandas/tests/strings/test_strings.py @@ -159,6 +159,7 @@ def test_empty_str_methods(any_string_dtype): # ismethods should always return boolean (GH 29624) tm.assert_series_equal(empty_bool, empty.str.isalnum()) tm.assert_series_equal(empty_bool, empty.str.isalpha()) + tm.assert_series_equal(empty_bool, empty.str.isascii()) tm.assert_series_equal(empty_bool, empty.str.isdigit()) tm.assert_series_equal(empty_bool, empty.str.isspace()) tm.assert_series_equal(empty_bool, empty.str.islower()) @@ -177,6 +178,7 @@ def test_empty_str_methods(any_string_dtype): @pytest.mark.parametrize( "method, expected", [ + ("isascii", [True, True, True, True, True, True, True, True, True, True]), ("isalnum", [True, True, True, True, True, False, True, True, False, False]), ("isalpha", [True, True, True, False, False, False, True, False, False, False]), ( From 5e50d3f3d2b0ee65f0d5bfda0c6da47ffd39dcfe Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:03:34 -0500 Subject: [PATCH 189/266] BUG/TST (string dtype): raise proper TypeError in interpolate (#60637) * TST(string dtype): Resolve xfail for interpolate * Adjust arrow tests * Fixup for NumPyExtensionArray * Use tm.shares_memory --- pandas/core/arrays/arrow/array.py | 2 +- pandas/core/arrays/numpy_.py | 3 +++ pandas/tests/extension/test_arrow.py | 4 +++- pandas/tests/frame/methods/test_interpolate.py | 13 +++++-------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index afa219f611992..4d9c8eb3a41b6 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -2160,7 +2160,7 @@ def interpolate( """ # NB: we return type(self) even if copy=False if not self.dtype._is_numeric: - raise ValueError("Values must be numeric.") + raise TypeError(f"Cannot interpolate with {self.dtype} dtype") if ( not pa_version_under13p0 diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index f222d6e1b328b..ac0823ed903b3 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -292,6 +292,9 @@ def interpolate( See NDFrame.interpolate.__doc__. """ # NB: we return type(self) even if copy=False + if not self.dtype._is_numeric: + raise TypeError(f"Cannot interpolate with {self.dtype} dtype") + if not copy: out_data = self._ndarray else: diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index 6dd1f3f15bc15..c5f5a65b77eea 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -3451,7 +3451,9 @@ def test_string_to_datetime_parsing_cast(): ) def test_interpolate_not_numeric(data): if not data.dtype._is_numeric: - with pytest.raises(ValueError, match="Values must be numeric."): + ser = pd.Series(data) + msg = re.escape(f"Cannot interpolate with {ser.dtype} dtype") + with pytest.raises(TypeError, match=msg): pd.Series(data).interpolate() diff --git a/pandas/tests/frame/methods/test_interpolate.py b/pandas/tests/frame/methods/test_interpolate.py index b8a34d5eaa226..09d1cc9a479b2 100644 --- a/pandas/tests/frame/methods/test_interpolate.py +++ b/pandas/tests/frame/methods/test_interpolate.py @@ -64,11 +64,7 @@ def test_interpolate_inplace(self, frame_or_series, request): assert np.shares_memory(orig, obj.values) assert orig.squeeze()[1] == 1.5 - # TODO(infer_string) raise proper TypeError in case of string dtype - @pytest.mark.xfail( - using_string_dtype(), reason="interpolate doesn't work for string" - ) - def test_interp_basic(self): + def test_interp_basic(self, using_infer_string): df = DataFrame( { "A": [1, 2, np.nan, 4], @@ -77,7 +73,8 @@ def test_interp_basic(self): "D": list("abcd"), } ) - msg = "DataFrame cannot interpolate with object dtype" + dtype = "str" if using_infer_string else "object" + msg = f"[Cc]annot interpolate with {dtype} dtype" with pytest.raises(TypeError, match=msg): df.interpolate() @@ -87,8 +84,8 @@ def test_interp_basic(self): df.interpolate(inplace=True) # check we DID operate inplace - assert np.shares_memory(df["C"]._values, cvalues) - assert np.shares_memory(df["D"]._values, dvalues) + assert tm.shares_memory(df["C"]._values, cvalues) + assert tm.shares_memory(df["D"]._values, dvalues) @pytest.mark.xfail( using_string_dtype(), reason="interpolate doesn't work for string" From 1be26374dd7ef43bc709c4bc6db2daf7bfd606c8 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Sat, 4 Jan 2025 00:16:16 +0530 Subject: [PATCH 190/266] DOC: fix SA01,ES01 for pandas.errors.ValueLabelTypeMismatch (#60650) --- ci/code_checks.sh | 1 - pandas/errors/__init__.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 8ddbbe8945002..56cb22741b9a3 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -86,7 +86,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ - -i "pandas.errors.ValueLabelTypeMismatch SA01" \ -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ -i "pandas.tseries.offsets.BQuarterBegin.is_on_offset GL08" \ diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index f150de3d217f2..2b5bc450e41d6 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -820,6 +820,16 @@ class ValueLabelTypeMismatch(Warning): """ Warning raised by to_stata on a category column that contains non-string values. + When exporting data to Stata format using the `to_stata` method, category columns + must have string values as labels. If a category column contains non-string values + (e.g., integers, floats, or other types), this warning is raised to indicate that + the Stata file may not correctly represent the data. + + See Also + -------- + DataFrame.to_stata : Export DataFrame object to Stata dta format. + Series.cat : Accessor for categorical properties of the Series values. + Examples -------- >>> df = pd.DataFrame({"categories": pd.Series(["a", 2], dtype="category")}) From 315b54908bd0dc8a7bc913a38fb22f03377eb6eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 09:30:21 -0800 Subject: [PATCH 191/266] [pre-commit.ci] pre-commit autoupdate (#60665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.6) - [github.com/jendrikseipp/vulture: v2.13 → v2.14](https://github.com/jendrikseipp/vulture/compare/v2.13...v2.14) - [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.19.0...v3.19.1) - [github.com/pre-commit/mirrors-clang-format: v19.1.4 → v19.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.4...v19.1.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7b9b1818c122..983c45fc493d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ ci: skip: [pyright, mypy] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.8.6 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -34,7 +34,7 @@ repos: - id: ruff-format exclude: ^scripts|^pandas/tests/frame/test_query_eval.py - repo: https://github.com/jendrikseipp/vulture - rev: 'v2.13' + rev: 'v2.14' hooks: - id: vulture entry: python scripts/run_vulture.py @@ -74,7 +74,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py310-plus] @@ -95,7 +95,7 @@ repos: - id: sphinx-lint args: ["--enable", "all", "--disable", "line-too-long"] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.4 + rev: v19.1.6 hooks: - id: clang-format files: ^pandas/_libs/src|^pandas/_libs/include From 5e6a9490d945c4807b9a73169f3e93fd040a4ea5 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Wed, 8 Jan 2025 12:26:27 -0500 Subject: [PATCH 192/266] Update macos12 runner to macos13 (#60679) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 32ca5573ac08a..3314e645509d1 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -94,7 +94,7 @@ jobs: buildplat: - [ubuntu-22.04, manylinux_x86_64] - [ubuntu-22.04, musllinux_x86_64] - - [macos-12, macosx_x86_64] + - [macos-13, macosx_x86_64] # Note: M1 images on Github Actions start from macOS 14 - [macos-14, macosx_arm64] - [windows-2022, win_amd64] From 8e49cbef009982c296e6cbaa3eb5cdc5bf5e89f6 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Wed, 8 Jan 2025 14:08:40 -0500 Subject: [PATCH 193/266] Clean up compiler warnings (#60674) --- pandas/_libs/byteswap.pyx | 10 +++++----- .../include/pandas/vendored/klib/khash_python.h | 4 +--- pandas/_libs/tslibs/conversion.pyx | 16 ++++++++-------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/pandas/_libs/byteswap.pyx b/pandas/_libs/byteswap.pyx index 67cd7ad58d229..7a8a9fc5a9139 100644 --- a/pandas/_libs/byteswap.pyx +++ b/pandas/_libs/byteswap.pyx @@ -15,7 +15,7 @@ from libc.string cimport memcpy def read_float_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): cdef uint32_t value - assert offset + sizeof(value) < len(data) + assert offset + sizeof(value) < len(data) cdef const void *ptr = (data) + offset memcpy(&value, ptr, sizeof(value)) if byteswap: @@ -28,7 +28,7 @@ def read_float_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): def read_double_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): cdef uint64_t value - assert offset + sizeof(value) < len(data) + assert offset + sizeof(value) < len(data) cdef const void *ptr = (data) + offset memcpy(&value, ptr, sizeof(value)) if byteswap: @@ -41,7 +41,7 @@ def read_double_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): def read_uint16_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): cdef uint16_t res - assert offset + sizeof(res) < len(data) + assert offset + sizeof(res) < len(data) memcpy(&res, (data) + offset, sizeof(res)) if byteswap: res = _byteswap2(res) @@ -50,7 +50,7 @@ def read_uint16_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): def read_uint32_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): cdef uint32_t res - assert offset + sizeof(res) < len(data) + assert offset + sizeof(res) < len(data) memcpy(&res, (data) + offset, sizeof(res)) if byteswap: res = _byteswap4(res) @@ -59,7 +59,7 @@ def read_uint32_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): def read_uint64_with_byteswap(bytes data, Py_ssize_t offset, bint byteswap): cdef uint64_t res - assert offset + sizeof(res) < len(data) + assert offset + sizeof(res) < len(data) memcpy(&res, (data) + offset, sizeof(res)) if byteswap: res = _byteswap8(res) diff --git a/pandas/_libs/include/pandas/vendored/klib/khash_python.h b/pandas/_libs/include/pandas/vendored/klib/khash_python.h index 9706a8211b61f..04e2d1e39ce2a 100644 --- a/pandas/_libs/include/pandas/vendored/klib/khash_python.h +++ b/pandas/_libs/include/pandas/vendored/klib/khash_python.h @@ -34,11 +34,9 @@ static void *traced_calloc(size_t num, size_t size) { } static void *traced_realloc(void *old_ptr, size_t size) { + PyTraceMalloc_Untrack(KHASH_TRACE_DOMAIN, (uintptr_t)old_ptr); void *ptr = realloc(old_ptr, size); if (ptr != NULL) { - if (old_ptr != ptr) { - PyTraceMalloc_Untrack(KHASH_TRACE_DOMAIN, (uintptr_t)old_ptr); - } PyTraceMalloc_Track(KHASH_TRACE_DOMAIN, (uintptr_t)ptr, size); } return ptr; diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index a635dd33f8420..7a8b4df447aee 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -149,18 +149,18 @@ def cast_from_unit_vectorized( if p: frac = np.round(frac, p) - try: - for i in range(len(values)): + for i in range(len(values)): + try: if base[i] == NPY_NAT: out[i] = NPY_NAT else: out[i] = (base[i] * m) + (frac[i] * m) - except (OverflowError, FloatingPointError) as err: - # FloatingPointError can be issued if we have float dtype and have - # set np.errstate(over="raise") - raise OutOfBoundsDatetime( - f"cannot convert input {values[i]} with the unit '{unit}'" - ) from err + except (OverflowError, FloatingPointError) as err: + # FloatingPointError can be issued if we have float dtype and have + # set np.errstate(over="raise") + raise OutOfBoundsDatetime( + f"cannot convert input {values[i]} with the unit '{unit}'" + ) from err return out From 3aba767f3ac4507185d911ed120a49969cdee63d Mon Sep 17 00:00:00 2001 From: William Ayd Date: Wed, 8 Jan 2025 18:06:31 -0500 Subject: [PATCH 194/266] Add meson-format to pre-commit (#60682) * Add pre-commit rule for meson * Format meson configuration files * Use github library --- .pre-commit-config.yaml | 5 ++ meson.build | 37 ++++---- pandas/_libs/meson.build | 148 ++++++++++++++++++-------------- pandas/_libs/tslibs/meson.build | 13 ++- pandas/_libs/window/meson.build | 19 ++-- pandas/meson.build | 21 ++--- 6 files changed, 129 insertions(+), 114 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 983c45fc493d1..1dd8dfc54111e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -101,6 +101,11 @@ repos: files: ^pandas/_libs/src|^pandas/_libs/include args: [-i] types_or: [c, c++] +- repo: https://github.com/trim21/pre-commit-mirror-meson + rev: v1.6.1 + hooks: + - id: meson-fmt + args: ['--inplace'] - repo: local hooks: - id: pyright diff --git a/meson.build b/meson.build index efe543b7a267c..66583095a6e77 100644 --- a/meson.build +++ b/meson.build @@ -1,15 +1,13 @@ # This file is adapted from https://github.com/scipy/scipy/blob/main/meson.build project( 'pandas', - 'c', 'cpp', 'cython', + 'c', + 'cpp', + 'cython', version: run_command(['generate_version.py', '--print'], check: true).stdout().strip(), license: 'BSD-3', meson_version: '>=1.2.1', - default_options: [ - 'buildtype=release', - 'c_std=c11', - 'warning_level=2', - ] + default_options: ['buildtype=release', 'c_std=c11', 'warning_level=2'], ) fs = import('fs') @@ -18,41 +16,40 @@ tempita = files('generate_pxi.py') versioneer = files('generate_version.py') -add_project_arguments('-DNPY_NO_DEPRECATED_API=0', language : 'c') -add_project_arguments('-DNPY_NO_DEPRECATED_API=0', language : 'cpp') +add_project_arguments('-DNPY_NO_DEPRECATED_API=0', language: 'c') +add_project_arguments('-DNPY_NO_DEPRECATED_API=0', language: 'cpp') # Allow supporting older numpys than the version compiled against # Set the define to the min supported version of numpy for pandas # e.g. right now this is targeting numpy 1.21+ -add_project_arguments('-DNPY_TARGET_VERSION=NPY_1_21_API_VERSION', language : 'c') -add_project_arguments('-DNPY_TARGET_VERSION=NPY_1_21_API_VERSION', language : 'cpp') +add_project_arguments('-DNPY_TARGET_VERSION=NPY_1_21_API_VERSION', language: 'c') +add_project_arguments( + '-DNPY_TARGET_VERSION=NPY_1_21_API_VERSION', + language: 'cpp', +) if fs.exists('_version_meson.py') py.install_sources('_version_meson.py', subdir: 'pandas') else - custom_target('write_version_file', + custom_target( + 'write_version_file', output: '_version_meson.py', - command: [ - py, versioneer, '-o', '@OUTPUT@' - ], + command: [py, versioneer, '-o', '@OUTPUT@'], build_by_default: true, build_always_stale: true, install: true, - install_dir: py.get_install_dir() / 'pandas' + install_dir: py.get_install_dir() / 'pandas', ) meson.add_dist_script(py, versioneer, '-o', '_version_meson.py') endif cy = meson.get_compiler('cython') if cy.version().version_compare('>=3.1.0') - add_project_arguments('-Xfreethreading_compatible=true', language : 'cython') + add_project_arguments('-Xfreethreading_compatible=true', language: 'cython') endif # Needed by pandas.test() when it looks for the pytest ini options -py.install_sources( - 'pyproject.toml', - subdir: 'pandas' -) +py.install_sources('pyproject.toml', subdir: 'pandas') subdir('pandas') diff --git a/pandas/_libs/meson.build b/pandas/_libs/meson.build index c27386743c6e9..5fb6f1118d648 100644 --- a/pandas/_libs/meson.build +++ b/pandas/_libs/meson.build @@ -1,94 +1,119 @@ -_algos_take_helper = custom_target('algos_take_helper_pxi', +_algos_take_helper = custom_target( + 'algos_take_helper_pxi', output: 'algos_take_helper.pxi', input: 'algos_take_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_algos_common_helper = custom_target('algos_common_helper_pxi', +_algos_common_helper = custom_target( + 'algos_common_helper_pxi', output: 'algos_common_helper.pxi', input: 'algos_common_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_khash_primitive_helper = custom_target('khash_primitive_helper_pxi', +_khash_primitive_helper = custom_target( + 'khash_primitive_helper_pxi', output: 'khash_for_primitive_helper.pxi', input: 'khash_for_primitive_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_hashtable_class_helper = custom_target('hashtable_class_helper_pxi', +_hashtable_class_helper = custom_target( + 'hashtable_class_helper_pxi', output: 'hashtable_class_helper.pxi', input: 'hashtable_class_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_hashtable_func_helper = custom_target('hashtable_func_helper_pxi', +_hashtable_func_helper = custom_target( + 'hashtable_func_helper_pxi', output: 'hashtable_func_helper.pxi', input: 'hashtable_func_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_index_class_helper = custom_target('index_class_helper_pxi', +_index_class_helper = custom_target( + 'index_class_helper_pxi', output: 'index_class_helper.pxi', input: 'index_class_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_sparse_op_helper = custom_target('sparse_op_helper_pxi', +_sparse_op_helper = custom_target( + 'sparse_op_helper_pxi', output: 'sparse_op_helper.pxi', input: 'sparse_op_helper.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], ) -_intervaltree_helper = custom_target('intervaltree_helper_pxi', +_intervaltree_helper = custom_target( + 'intervaltree_helper_pxi', output: 'intervaltree.pxi', input: 'intervaltree.pxi.in', - command: [ - py, tempita, '@INPUT@', '-o', '@OUTDIR@' - ] + command: [py, tempita, '@INPUT@', '-o', '@OUTDIR@'], +) +_khash_primitive_helper_dep = declare_dependency( + sources: _khash_primitive_helper, ) -_khash_primitive_helper_dep = declare_dependency(sources: _khash_primitive_helper) subdir('tslibs') libs_sources = { # Dict of extension name -> dict of {sources, include_dirs, and deps} # numpy include dir is implicitly included - 'algos': {'sources': ['algos.pyx', _algos_common_helper, _algos_take_helper], 'deps': _khash_primitive_helper_dep}, + 'algos': { + 'sources': ['algos.pyx', _algos_common_helper, _algos_take_helper], + 'deps': _khash_primitive_helper_dep, + }, 'arrays': {'sources': ['arrays.pyx']}, 'groupby': {'sources': ['groupby.pyx']}, 'hashing': {'sources': ['hashing.pyx']}, - 'hashtable': {'sources': ['hashtable.pyx', _hashtable_class_helper, _hashtable_func_helper], 'deps': _khash_primitive_helper_dep}, - 'index': {'sources': ['index.pyx', _index_class_helper], 'deps': _khash_primitive_helper_dep}, + 'hashtable': { + 'sources': [ + 'hashtable.pyx', + _hashtable_class_helper, + _hashtable_func_helper, + ], + 'deps': _khash_primitive_helper_dep, + }, + 'index': { + 'sources': ['index.pyx', _index_class_helper], + 'deps': _khash_primitive_helper_dep, + }, 'indexing': {'sources': ['indexing.pyx']}, 'internals': {'sources': ['internals.pyx']}, - 'interval': {'sources': ['interval.pyx', _intervaltree_helper], - 'deps': _khash_primitive_helper_dep}, - 'join': {'sources': ['join.pyx', _khash_primitive_helper], - 'deps': _khash_primitive_helper_dep}, + 'interval': { + 'sources': ['interval.pyx', _intervaltree_helper], + 'deps': _khash_primitive_helper_dep, + }, + 'join': { + 'sources': ['join.pyx', _khash_primitive_helper], + 'deps': _khash_primitive_helper_dep, + }, 'lib': {'sources': ['lib.pyx', 'src/parser/tokenizer.c']}, 'missing': {'sources': ['missing.pyx']}, - 'pandas_datetime': {'sources': ['src/vendored/numpy/datetime/np_datetime.c', - 'src/vendored/numpy/datetime/np_datetime_strings.c', - 'src/datetime/date_conversions.c', - 'src/datetime/pd_datetime.c']}, - 'pandas_parser': {'sources': ['src/parser/tokenizer.c', - 'src/parser/io.c', - 'src/parser/pd_parser.c']}, - 'parsers': {'sources': ['parsers.pyx', 'src/parser/tokenizer.c', 'src/parser/io.c'], - 'deps': _khash_primitive_helper_dep}, - 'json': {'sources': ['src/vendored/ujson/python/ujson.c', - 'src/vendored/ujson/python/objToJSON.c', - 'src/vendored/ujson/python/JSONtoObj.c', - 'src/vendored/ujson/lib/ultrajsonenc.c', - 'src/vendored/ujson/lib/ultrajsondec.c']}, + 'pandas_datetime': { + 'sources': [ + 'src/vendored/numpy/datetime/np_datetime.c', + 'src/vendored/numpy/datetime/np_datetime_strings.c', + 'src/datetime/date_conversions.c', + 'src/datetime/pd_datetime.c', + ], + }, + 'pandas_parser': { + 'sources': [ + 'src/parser/tokenizer.c', + 'src/parser/io.c', + 'src/parser/pd_parser.c', + ], + }, + 'parsers': { + 'sources': ['parsers.pyx', 'src/parser/tokenizer.c', 'src/parser/io.c'], + 'deps': _khash_primitive_helper_dep, + }, + 'json': { + 'sources': [ + 'src/vendored/ujson/python/ujson.c', + 'src/vendored/ujson/python/objToJSON.c', + 'src/vendored/ujson/python/JSONtoObj.c', + 'src/vendored/ujson/lib/ultrajsonenc.c', + 'src/vendored/ujson/lib/ultrajsondec.c', + ], + }, 'ops': {'sources': ['ops.pyx']}, 'ops_dispatch': {'sources': ['ops_dispatch.pyx']}, 'properties': {'sources': ['properties.pyx']}, @@ -98,13 +123,13 @@ libs_sources = { 'sparse': {'sources': ['sparse.pyx', _sparse_op_helper]}, 'tslib': {'sources': ['tslib.pyx']}, 'testing': {'sources': ['testing.pyx']}, - 'writers': {'sources': ['writers.pyx']} + 'writers': {'sources': ['writers.pyx']}, } cython_args = [ '--include-dir', meson.current_build_dir(), - '-X always_allow_keywords=true' + '-X always_allow_keywords=true', ] if get_option('buildtype') == 'debug' cython_args += ['--gdb'] @@ -118,7 +143,7 @@ foreach ext_name, ext_dict : libs_sources include_directories: [inc_np, inc_pd], dependencies: ext_dict.get('deps', ''), subdir: 'pandas/_libs', - install: true + install: true, ) endforeach @@ -148,14 +173,11 @@ sources_to_install = [ 'sparse.pyi', 'testing.pyi', 'tslib.pyi', - 'writers.pyi' + 'writers.pyi', ] -foreach source: sources_to_install - py.install_sources( - source, - subdir: 'pandas/_libs' - ) +foreach source : sources_to_install + py.install_sources(source, subdir: 'pandas/_libs') endforeach subdir('window') diff --git a/pandas/_libs/tslibs/meson.build b/pandas/_libs/tslibs/meson.build index 85410f771233f..052a8568b76af 100644 --- a/pandas/_libs/tslibs/meson.build +++ b/pandas/_libs/tslibs/meson.build @@ -22,7 +22,7 @@ tslibs_sources = { cython_args = [ '--include-dir', meson.current_build_dir(), - '-X always_allow_keywords=true' + '-X always_allow_keywords=true', ] if get_option('buildtype') == 'debug' cython_args += ['--gdb'] @@ -36,7 +36,7 @@ foreach ext_name, ext_dict : tslibs_sources include_directories: [inc_np, inc_pd], dependencies: ext_dict.get('deps', ''), subdir: 'pandas/_libs/tslibs', - install: true + install: true, ) endforeach @@ -56,12 +56,9 @@ sources_to_install = [ 'timestamps.pyi', 'timezones.pyi', 'tzconversion.pyi', - 'vectorized.pyi' + 'vectorized.pyi', ] -foreach source: sources_to_install - py.install_sources( - source, - subdir: 'pandas/_libs/tslibs' - ) +foreach source : sources_to_install + py.install_sources(source, subdir: 'pandas/_libs/tslibs') endforeach diff --git a/pandas/_libs/window/meson.build b/pandas/_libs/window/meson.build index ad15644f73a0c..1d49bba47e139 100644 --- a/pandas/_libs/window/meson.build +++ b/pandas/_libs/window/meson.build @@ -4,8 +4,8 @@ py.extension_module( cython_args: ['-X always_allow_keywords=true'], include_directories: [inc_np, inc_pd], subdir: 'pandas/_libs/window', - override_options : ['cython_language=cpp'], - install: true + override_options: ['cython_language=cpp'], + install: true, ) py.extension_module( @@ -14,18 +14,11 @@ py.extension_module( cython_args: ['-X always_allow_keywords=true'], include_directories: [inc_np, inc_pd], subdir: 'pandas/_libs/window', - install: true + install: true, ) -sources_to_install = [ - '__init__.py', - 'aggregations.pyi', - 'indexers.pyi' -] +sources_to_install = ['__init__.py', 'aggregations.pyi', 'indexers.pyi'] -foreach source: sources_to_install - py.install_sources( - source, - subdir: 'pandas/_libs/window' - ) +foreach source : sources_to_install + py.install_sources(source, subdir: 'pandas/_libs/window') endforeach diff --git a/pandas/meson.build b/pandas/meson.build index 435103a954d86..840ac257bba09 100644 --- a/pandas/meson.build +++ b/pandas/meson.build @@ -1,7 +1,8 @@ -incdir_numpy = run_command(py, - [ - '-c', - ''' +incdir_numpy = run_command( + py, + [ + '-c', + ''' import os import numpy as np try: @@ -12,9 +13,9 @@ try: except Exception: incdir = np.get_include() print(incdir) - ''' - ], - check: true + ''', + ], + check: true, ).stdout().strip() inc_np = include_directories(incdir_numpy) @@ -36,9 +37,9 @@ subdirs_list = [ 'plotting', 'tests', 'tseries', - 'util' + 'util', ] -foreach subdir: subdirs_list +foreach subdir : subdirs_list install_subdir(subdir, install_dir: py.get_install_dir() / 'pandas') endforeach @@ -47,6 +48,6 @@ top_level_py_list = [ '_typing.py', '_version.py', 'conftest.py', - 'testing.py' + 'testing.py', ] py.install_sources(top_level_py_list, subdir: 'pandas') From 4693d1a000f34641f8215caea6872b3357f4db76 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 9 Jan 2025 05:01:30 -0500 Subject: [PATCH 195/266] TST(string dtype): Resolve xfails in test_to_csv (#60683) --- pandas/tests/frame/methods/test_astype.py | 2 +- pandas/tests/frame/methods/test_reset_index.py | 2 +- pandas/tests/frame/methods/test_to_csv.py | 14 ++++---------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pandas/tests/frame/methods/test_astype.py b/pandas/tests/frame/methods/test_astype.py index ab3743283ea13..eb1ee4e7b2970 100644 --- a/pandas/tests/frame/methods/test_astype.py +++ b/pandas/tests/frame/methods/test_astype.py @@ -745,7 +745,7 @@ def test_astype_tz_object_conversion(self, tz): result = result.astype({"tz": "datetime64[ns, Europe/London]"}) tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") + @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) GH#60639") def test_astype_dt64_to_string( self, frame_or_series, tz_naive_fixture, using_infer_string ): diff --git a/pandas/tests/frame/methods/test_reset_index.py b/pandas/tests/frame/methods/test_reset_index.py index 88e43b678a7e4..0b320075ed2d2 100644 --- a/pandas/tests/frame/methods/test_reset_index.py +++ b/pandas/tests/frame/methods/test_reset_index.py @@ -644,7 +644,7 @@ def test_rest_index_multiindex_categorical_with_missing_values(self, codes): tm.assert_frame_equal(res, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") +@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) - GH#60338") @pytest.mark.parametrize( "array, dtype", [ diff --git a/pandas/tests/frame/methods/test_to_csv.py b/pandas/tests/frame/methods/test_to_csv.py index 23377b7373987..9eafc69013ffe 100644 --- a/pandas/tests/frame/methods/test_to_csv.py +++ b/pandas/tests/frame/methods/test_to_csv.py @@ -5,8 +5,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import ParserError import pandas as pd @@ -438,20 +436,18 @@ def test_to_csv_empty(self): result, expected = self._return_result_expected(df, 1000) tm.assert_frame_equal(result, expected, check_column_type=False) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") @pytest.mark.slow def test_to_csv_chunksize(self): chunksize = 1000 rows = chunksize // 2 + 1 df = DataFrame( np.ones((rows, 2)), - columns=Index(list("ab"), dtype=object), + columns=Index(list("ab")), index=MultiIndex.from_arrays([range(rows) for _ in range(2)]), ) result, expected = self._return_result_expected(df, chunksize, rnlvl=2) tm.assert_frame_equal(result, expected, check_names=False) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.slow @pytest.mark.parametrize( "nrows", [2, 10, 99, 100, 101, 102, 198, 199, 200, 201, 202, 249, 250, 251] @@ -480,7 +476,7 @@ def test_to_csv_params(self, nrows, df_params, func_params, ncols): for _ in range(df_params["c_idx_nlevels"]) ) else: - columns = Index([f"i-{i}" for i in range(ncols)], dtype=object) + columns = Index([f"i-{i}" for i in range(ncols)]) df = DataFrame(np.ones((nrows, ncols)), index=index, columns=columns) result, expected = self._return_result_expected(df, 1000, **func_params) tm.assert_frame_equal(result, expected, check_names=False) @@ -738,7 +734,6 @@ def test_to_csv_withcommas(self, temp_file): df2 = self.read_csv(path) tm.assert_frame_equal(df2, df) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_to_csv_mixed(self, temp_file): def create_cols(name): return [f"{name}{i:03d}" for i in range(5)] @@ -755,7 +750,7 @@ def create_cols(name): ) df_bool = DataFrame(True, index=df_float.index, columns=create_cols("bool")) df_object = DataFrame( - "foo", index=df_float.index, columns=create_cols("object") + "foo", index=df_float.index, columns=create_cols("object"), dtype="object" ) df_dt = DataFrame( Timestamp("20010101"), @@ -824,13 +819,12 @@ def test_to_csv_dups_cols(self, temp_file): result.columns = df.columns tm.assert_frame_equal(result, df) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_to_csv_dups_cols2(self, temp_file): # GH3457 df = DataFrame( np.ones((5, 3)), index=Index([f"i-{i}" for i in range(5)], name="foo"), - columns=Index(["a", "a", "b"], dtype=object), + columns=Index(["a", "a", "b"]), ) path = str(temp_file) From 0110487d4bccf3f7498c22a71e6a0dfdb3be3a16 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:43:43 -0500 Subject: [PATCH 196/266] PERF: Fix ASV CSV benchmarks (#60689) --- asv_bench/benchmarks/io/csv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asv_bench/benchmarks/io/csv.py b/asv_bench/benchmarks/io/csv.py index ff0ccffced0f3..3a15f754ae523 100644 --- a/asv_bench/benchmarks/io/csv.py +++ b/asv_bench/benchmarks/io/csv.py @@ -594,7 +594,7 @@ def setup(self): self.StringIO_input = StringIO(data) def time_read_csv_index_col(self): - read_csv(self.StringIO_input, index_col="a") + read_csv(self.data(self.StringIO_input), index_col="a") class ReadCSVDatePyarrowEngine(StringIORewind): @@ -605,7 +605,7 @@ def setup(self): def time_read_csv_index_col(self): read_csv( - self.StringIO_input, + self.data(self.StringIO_input), parse_dates=["a"], engine="pyarrow", dtype_backend="pyarrow", From 8f9039d3ae9daa86de0b59ee13e8b97ccf92478b Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Fri, 10 Jan 2025 09:17:20 -0800 Subject: [PATCH 197/266] BUG: Fix ValueError in DataFrame/Series regex replace for all-NA values (#60691) * BUG: DataFrame/Series regex replace fix for all NA values * Add entry to whatsnew --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/array_algos/replace.py | 3 ++- pandas/tests/frame/methods/test_replace.py | 7 +++++++ pandas/tests/series/methods/test_replace.py | 7 +++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 94c289ef3ace7..5cc258a54fa48 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -798,6 +798,7 @@ Other - Bug in :meth:`Series.dt` methods in :class:`ArrowDtype` that were returning incorrect values. (:issue:`57355`) - Bug in :meth:`Series.rank` that doesn't preserve missing values for nullable integers when ``na_option='keep'``. (:issue:`56976`) - Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` inconsistently replacing matching instances when ``regex=True`` and missing values are present. (:issue:`56599`) +- Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` throwing ``ValueError`` when ``regex=True`` and all NA values. (:issue:`60688`) - Bug in :meth:`Series.to_string` when series contains complex floats with exponents (:issue:`60405`) - Bug in :meth:`read_csv` where chained fsspec TAR file and ``compression="infer"`` fails with ``tarfile.ReadError`` (:issue:`60028`) - Bug in Dataframe Interchange Protocol implementation was returning incorrect results for data buffers' associated dtype, for string and datetime columns (:issue:`54781`) diff --git a/pandas/core/array_algos/replace.py b/pandas/core/array_algos/replace.py index a9ad66b7cb2e5..debd6368e98a4 100644 --- a/pandas/core/array_algos/replace.py +++ b/pandas/core/array_algos/replace.py @@ -89,7 +89,8 @@ def _check_comparison_types( op = np.vectorize( lambda x: bool(re.search(b, x)) if isinstance(x, str) and isinstance(b, (str, Pattern)) - else False + else False, + otypes=[bool], ) # GH#32621 use mask to avoid comparing to NAs diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py index b2320798ea9a2..c95806d9316bf 100644 --- a/pandas/tests/frame/methods/test_replace.py +++ b/pandas/tests/frame/methods/test_replace.py @@ -713,6 +713,13 @@ def test_replace_with_None_keeps_categorical(self): ) tm.assert_frame_equal(result, expected) + def test_replace_all_NA(self): + # GH#60688 + df = DataFrame({"ticker": ["#1234#"], "name": [None]}) + result = df.replace({col: {r"^#": "$"} for col in df.columns}, regex=True) + expected = DataFrame({"ticker": ["$1234#"], "name": [None]}) + tm.assert_frame_equal(result, expected) + def test_replace_value_is_none(self, datetime_frame): orig_value = datetime_frame.iloc[0, 0] orig2 = datetime_frame.iloc[1, 0] diff --git a/pandas/tests/series/methods/test_replace.py b/pandas/tests/series/methods/test_replace.py index ecfe3d1b39d31..abd5d075ea3d5 100644 --- a/pandas/tests/series/methods/test_replace.py +++ b/pandas/tests/series/methods/test_replace.py @@ -708,3 +708,10 @@ def test_replace_ea_float_with_bool(self): expected = ser.copy() result = ser.replace(0.0, True) tm.assert_series_equal(result, expected) + + def test_replace_all_NA(self): + # GH#60688 + df = pd.Series([pd.NA, pd.NA]) + result = df.replace({r"^#": "$"}, regex=True) + expected = pd.Series([pd.NA, pd.NA]) + tm.assert_series_equal(result, expected) From 11cc7e06f1cf177a0293222e3bfcb389120aa284 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:24:06 -0500 Subject: [PATCH 198/266] TST(string dtype): Resolve replace xfails (#60659) * TST(string dtype): Resolve replace xfails * Add test * fixup --- pandas/tests/frame/methods/test_replace.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py index c95806d9316bf..9e302dc5f94ee 100644 --- a/pandas/tests/frame/methods/test_replace.py +++ b/pandas/tests/frame/methods/test_replace.py @@ -334,7 +334,6 @@ def test_regex_replace_str_to_numeric(self, mix_abc): return_value = res3.replace(regex=r"\s*\.\s*", value=0, inplace=True) assert return_value is None expec = DataFrame({"a": mix_abc["a"], "b": ["a", "b", 0, 0], "c": mix_abc["c"]}) - # TODO(infer_string) expec["c"] = expec["c"].astype(object) tm.assert_frame_equal(res, expec) tm.assert_frame_equal(res2, expec) @@ -1476,21 +1475,24 @@ def test_regex_replace_scalar( tm.assert_frame_equal(result, expected) @pytest.mark.parametrize("regex", [False, True]) - def test_replace_regex_dtype_frame(self, regex): + @pytest.mark.parametrize("value", [1, "1"]) + def test_replace_regex_dtype_frame(self, regex, value): # GH-48644 df1 = DataFrame({"A": ["0"], "B": ["0"]}) - expected_df1 = DataFrame({"A": [1], "B": [1]}, dtype=object) - result_df1 = df1.replace(to_replace="0", value=1, regex=regex) + # When value is an integer, coerce result to object. + # When value is a string, infer the correct string dtype. + dtype = object if value == 1 else None + + expected_df1 = DataFrame({"A": [value], "B": [value]}, dtype=dtype) + result_df1 = df1.replace(to_replace="0", value=value, regex=regex) tm.assert_frame_equal(result_df1, expected_df1) df2 = DataFrame({"A": ["0"], "B": ["1"]}) if regex: - # TODO(infer_string): both string columns get cast to object, - # while only needed for column A - expected_df2 = DataFrame({"A": [1], "B": ["1"]}, dtype=object) + expected_df2 = DataFrame({"A": [value], "B": ["1"]}, dtype=dtype) else: - expected_df2 = DataFrame({"A": Series([1], dtype=object), "B": ["1"]}) - result_df2 = df2.replace(to_replace="0", value=1, regex=regex) + expected_df2 = DataFrame({"A": Series([value], dtype=dtype), "B": ["1"]}) + result_df2 = df2.replace(to_replace="0", value=value, regex=regex) tm.assert_frame_equal(result_df2, expected_df2) def test_replace_with_value_also_being_replaced(self): From a81d52fd19062ec3128f43285eff55cc9e76a511 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Fri, 10 Jan 2025 09:42:36 -0800 Subject: [PATCH 199/266] ENH: Support kurtosis (kurt) in DataFrameGroupBy and SeriesGroupBy (#60433) * ENH: Support kurtosis (kurt) in DataFrameGroupBy and SeriesGroupBy * ENH: Address review comments * ENH: Fix comments in new test cases * ENH: Skip pyarrow test case if no pyarrow available * ENH: Update to intp instead of np.intp * ENH: Change intp to int64 * Address review comments --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/_libs/groupby.pyi | 9 + pandas/_libs/groupby.pyx | 98 +++++++++- pandas/core/arrays/base.py | 1 + pandas/core/arrays/categorical.py | 2 +- pandas/core/arrays/datetimelike.py | 6 +- pandas/core/groupby/base.py | 1 + pandas/core/groupby/generic.py | 182 +++++++++++++++++- pandas/core/groupby/ops.py | 8 +- pandas/tests/groupby/methods/test_kurt.py | 90 +++++++++ pandas/tests/groupby/test_api.py | 1 + pandas/tests/groupby/test_apply.py | 1 + pandas/tests/groupby/test_categorical.py | 1 + pandas/tests/groupby/test_groupby.py | 10 +- pandas/tests/groupby/test_numeric_only.py | 3 + pandas/tests/groupby/test_raises.py | 33 +++- pandas/tests/groupby/test_reductions.py | 5 +- .../tests/groupby/transform/test_transform.py | 8 +- 18 files changed, 436 insertions(+), 24 deletions(-) create mode 100644 pandas/tests/groupby/methods/test_kurt.py diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 5cc258a54fa48..47838d1e49d61 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -55,6 +55,7 @@ Other enhancements - :meth:`Series.plot` now correctly handle the ``ylabel`` parameter for pie charts, allowing for explicit control over the y-axis label (:issue:`58239`) - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) - :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`) +- :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`) - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) - :meth:`Rolling.agg`, :meth:`Expanding.agg` and :meth:`ExponentialMovingWindow.agg` now accept :class:`NamedAgg` aggregations through ``**kwargs`` (:issue:`28333`) - :meth:`Series.map` can now accept kwargs to pass on to func (:issue:`59814`) diff --git a/pandas/_libs/groupby.pyi b/pandas/_libs/groupby.pyi index 53f5f73624232..34367f55d2bbb 100644 --- a/pandas/_libs/groupby.pyi +++ b/pandas/_libs/groupby.pyi @@ -97,6 +97,15 @@ def group_skew( result_mask: np.ndarray | None = ..., skipna: bool = ..., ) -> None: ... +def group_kurt( + out: np.ndarray, # float64_t[:, ::1] + counts: np.ndarray, # int64_t[::1] + values: np.ndarray, # ndarray[float64_T, ndim=2] + labels: np.ndarray, # const intp_t[::1] + mask: np.ndarray | None = ..., + result_mask: np.ndarray | None = ..., + skipna: bool = ..., +) -> None: ... def group_mean( out: np.ndarray, # floating[:, ::1] counts: np.ndarray, # int64_t[::1] diff --git a/pandas/_libs/groupby.pyx b/pandas/_libs/groupby.pyx index d7e485f74e58b..59bc59135a8ff 100644 --- a/pandas/_libs/groupby.pyx +++ b/pandas/_libs/groupby.pyx @@ -910,7 +910,7 @@ def group_var( @cython.wraparound(False) @cython.boundscheck(False) @cython.cdivision(True) -@cython.cpow +@cython.cpow(True) def group_skew( float64_t[:, ::1] out, int64_t[::1] counts, @@ -961,7 +961,7 @@ def group_skew( isna_entry = _treat_as_na(val, False) if not isna_entry: - # Based on RunningStats::Push from + # Running stats update based on RunningStats::Push from # https://www.johndcook.com/blog/skewness_kurtosis/ n1 = nobs[lab, j] n = n1 + 1 @@ -995,6 +995,100 @@ def group_skew( ) +@cython.wraparound(False) +@cython.boundscheck(False) +@cython.cdivision(True) +@cython.cpow(True) +def group_kurt( + float64_t[:, ::1] out, + int64_t[::1] counts, + ndarray[float64_t, ndim=2] values, + const intp_t[::1] labels, + const uint8_t[:, ::1] mask=None, + uint8_t[:, ::1] result_mask=None, + bint skipna=True, +) -> None: + cdef: + Py_ssize_t i, j, N, K, lab, ngroups = len(counts) + int64_t[:, ::1] nobs + Py_ssize_t len_values = len(values), len_labels = len(labels) + bint isna_entry, uses_mask = mask is not None + float64_t[:, ::1] M1, M2, M3, M4 + float64_t delta, delta_n, delta_n2, term1, val + int64_t n1, n + float64_t ct, num, den, adj + + if len_values != len_labels: + raise ValueError("len(index) != len(labels)") + + nobs = np.zeros((out).shape, dtype=np.int64) + + # M1, M2, M3 and M4 correspond to 1st, 2nd, 3rd and 4th Moments + M1 = np.zeros((out).shape, dtype=np.float64) + M2 = np.zeros((out).shape, dtype=np.float64) + M3 = np.zeros((out).shape, dtype=np.float64) + M4 = np.zeros((out).shape, dtype=np.float64) + + N, K = (values).shape + + out[:, :] = 0.0 + + with nogil: + for i in range(N): + lab = labels[i] + if lab < 0: + continue + + counts[lab] += 1 + + for j in range(K): + val = values[i, j] + + if uses_mask: + isna_entry = mask[i, j] + else: + isna_entry = _treat_as_na(val, False) + + if not isna_entry: + # Running stats update based on RunningStats::Push from + # https://www.johndcook.com/blog/skewness_kurtosis/ + n1 = nobs[lab, j] + n = n1 + 1 + + nobs[lab, j] = n + delta = val - M1[lab, j] + delta_n = delta / n + delta_n2 = delta_n * delta_n + term1 = delta * delta_n * n1 + + M1[lab, j] += delta_n + M4[lab, j] += (term1 * delta_n2 * (n*n - 3*n + 3) + + 6 * delta_n2 * M2[lab, j] + - 4 * delta_n * M3[lab, j]) + M3[lab, j] += term1 * delta_n * (n - 2) - 3 * delta_n * M2[lab, j] + M2[lab, j] += term1 + elif not skipna: + M1[lab, j] = NaN + M2[lab, j] = NaN + M3[lab, j] = NaN + M4[lab, j] = NaN + + for i in range(ngroups): + for j in range(K): + ct = nobs[i, j] + if ct < 4: + if result_mask is not None: + result_mask[i, j] = 1 + out[i, j] = NaN + elif M2[i, j] == 0: + out[i, j] = 0 + else: + num = ct * (ct + 1) * (ct - 1) * M4[i, j] + den = (ct - 2) * (ct - 3) * M2[i, j] ** 2 + adj = 3.0 * (ct - 1) ** 2 / ((ct - 2) * (ct - 3)) + out[i, j] = num / den - adj + + @cython.wraparound(False) @cython.boundscheck(False) def group_mean( diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 4835d808f2433..e831883998098 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2618,6 +2618,7 @@ def _groupby_op( "sem", "var", "skew", + "kurt", ]: raise TypeError( f"dtype '{self.dtype}' does not support operation '{how}'" diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 99e4cb0545e2d..ae20bfb6b284b 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -2736,7 +2736,7 @@ def _groupby_op( op = WrappedCythonOp(how=how, kind=kind, has_dropped_na=has_dropped_na) dtype = self.dtype - if how in ["sum", "prod", "cumsum", "cumprod", "skew"]: + if how in ["sum", "prod", "cumsum", "cumprod", "skew", "kurt"]: raise TypeError(f"{dtype} type does not support {how} operations") if how in ["min", "max", "rank", "idxmin", "idxmax"] and not dtype.ordered: # raise TypeError instead of NotImplementedError to ensure we diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index c6b6367e347ba..8a79ab53442c3 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1656,7 +1656,7 @@ def _groupby_op( dtype = self.dtype if dtype.kind == "M": # Adding/multiplying datetimes is not valid - if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew"]: + if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew", "kurt"]: raise TypeError(f"datetime64 type does not support operation '{how}'") if how in ["any", "all"]: # GH#34479 @@ -1667,7 +1667,7 @@ def _groupby_op( elif isinstance(dtype, PeriodDtype): # Adding/multiplying Periods is not valid - if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew"]: + if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew", "kurt"]: raise TypeError(f"Period type does not support {how} operations") if how in ["any", "all"]: # GH#34479 @@ -1677,7 +1677,7 @@ def _groupby_op( ) else: # timedeltas we can add but not multiply - if how in ["prod", "cumprod", "skew", "var"]: + if how in ["prod", "cumprod", "skew", "kurt", "var"]: raise TypeError(f"timedelta64 type does not support {how} operations") # All of the functions implemented here are ordinal, so we can diff --git a/pandas/core/groupby/base.py b/pandas/core/groupby/base.py index bad9749b5ecee..7699fb3d0f864 100644 --- a/pandas/core/groupby/base.py +++ b/pandas/core/groupby/base.py @@ -50,6 +50,7 @@ class OutputKey: "sem", "size", "skew", + "kurt", "std", "sum", "var", diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index f4e3f3e8b1001..1251403db6ff3 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -1272,13 +1272,86 @@ def skew( Name: Max Speed, dtype: float64 """ + return self._cython_agg_general( + "skew", alt=None, skipna=skipna, numeric_only=numeric_only, **kwargs + ) + + def kurt( + self, + skipna: bool = True, + numeric_only: bool = False, + **kwargs, + ) -> Series: + """ + Return unbiased kurtosis within groups. + + Parameters + ---------- + skipna : bool, default True + Exclude NA/null values when computing the result. + + numeric_only : bool, default False + Include only float, int, boolean columns. Not implemented for Series. + + **kwargs + Additional keyword arguments to be passed to the function. + + Returns + ------- + Series + Unbiased kurtosis within groups. + + See Also + -------- + Series.kurt : Return unbiased kurtosis over requested axis. + + Examples + -------- + >>> ser = pd.Series( + ... [390.0, 350.0, 357.0, 333.0, np.nan, 22.0, 20.0, 30.0, 40.0, 41.0], + ... index=[ + ... "Falcon", + ... "Falcon", + ... "Falcon", + ... "Falcon", + ... "Falcon", + ... "Parrot", + ... "Parrot", + ... "Parrot", + ... "Parrot", + ... "Parrot", + ... ], + ... name="Max Speed", + ... ) + >>> ser + Falcon 390.0 + Falcon 350.0 + Falcon 357.0 + Falcon 333.0 + Falcon NaN + Parrot 22.0 + Parrot 20.0 + Parrot 30.0 + Parrot 40.0 + Parrot 41.0 + Name: Max Speed, dtype: float64 + >>> ser.groupby(level=0).kurt() + Falcon 1.622109 + Parrot -2.878714 + Name: Max Speed, dtype: float64 + >>> ser.groupby(level=0).kurt(skipna=False) + Falcon NaN + Parrot -2.878714 + Name: Max Speed, dtype: float64 + """ + def alt(obj): # This should not be reached since the cython path should raise # TypeError and not NotImplementedError. - raise TypeError(f"'skew' is not supported for dtype={obj.dtype}") + raise TypeError(f"'kurt' is not supported for dtype={obj.dtype}") return self._cython_agg_general( - "skew", alt=alt, skipna=skipna, numeric_only=numeric_only, **kwargs + "kurt", alt=alt, skipna=skipna, numeric_only=numeric_only, **kwargs ) @property @@ -2921,6 +2994,111 @@ def alt(obj): "skew", alt=alt, skipna=skipna, numeric_only=numeric_only, **kwargs ) + def kurt( + self, + skipna: bool = True, + numeric_only: bool = False, + **kwargs, + ) -> DataFrame: + """ + Return unbiased kurtosis within groups. + + Parameters + ---------- + skipna : bool, default True + Exclude NA/null values when computing the result. + + numeric_only : bool, default False + Include only float, int, boolean columns. + + **kwargs + Additional keyword arguments to be passed to the function. + + Returns + ------- + DataFrame + Unbiased kurtosis within groups. + + See Also + -------- + DataFrame.kurt : Return unbiased kurtosis over requested axis. + + Examples + -------- + >>> arrays = [ + ... [ + ... "falcon", + ... "parrot", + ... "cockatoo", + ... "kiwi", + ... "eagle", + ... "lion", + ... "monkey", + ... "rabbit", + ... "dog", + ... "wolf", + ... ], + ... [ + ... "bird", + ... "bird", + ... "bird", + ... "bird", + ... "bird", + ... "mammal", + ... "mammal", + ... "mammal", + ... "mammal", + ... "mammal", + ... ], + ... ] + >>> index = pd.MultiIndex.from_arrays(arrays, names=("name", "class")) + >>> df = pd.DataFrame( + ... { + ... "max_speed": [ + ... 389.0, + ... 24.0, + ... 70.0, + ... np.nan, + ... 350.0, + ... 80.5, + ... 21.5, + ... 15.0, + ... 40.0, + ... 50.0, + ... ] + ... }, + ... index=index, + ... ) + >>> df + max_speed + name class + falcon bird 389.0 + parrot bird 24.0 + cockatoo bird 70.0 + kiwi bird NaN + eagle bird 350.0 + lion mammal 80.5 + monkey mammal 21.5 + rabbit mammal 15.0 + dog mammal 40.0 + wolf mammal 50.0 + >>> gb = df.groupby(["class"]) + >>> gb.kurt() + max_speed + class + bird -5.493277 + mammal 0.204125 + >>> gb.kurt(skipna=False) + max_speed + class + bird NaN + mammal 0.204125 + """ + + return self._cython_agg_general( + "kurt", alt=None, skipna=skipna, numeric_only=numeric_only, **kwargs + ) + @property @doc(DataFrame.plot.__doc__) def plot(self) -> GroupByPlot: diff --git a/pandas/core/groupby/ops.py b/pandas/core/groupby/ops.py index 4c7fe604e452d..c4c7f73ee166c 100644 --- a/pandas/core/groupby/ops.py +++ b/pandas/core/groupby/ops.py @@ -144,6 +144,7 @@ def __init__(self, kind: str, how: str, has_dropped_na: bool) -> None: "std": functools.partial(libgroupby.group_var, name="std"), "sem": functools.partial(libgroupby.group_var, name="sem"), "skew": "group_skew", + "kurt": "group_kurt", "first": "group_nth", "last": "group_last", "ohlc": "group_ohlc", @@ -193,7 +194,7 @@ def _get_cython_function( elif how in ["std", "sem", "idxmin", "idxmax"]: # We have a partial object that does not have __signatures__ return f - elif how == "skew": + elif how in ["skew", "kurt"]: # _get_cython_vals will convert to float64 pass elif "object" not in f.__signatures__: @@ -224,7 +225,7 @@ def _get_cython_vals(self, values: np.ndarray) -> np.ndarray: """ how = self.how - if how in ["median", "std", "sem", "skew"]: + if how in ["median", "std", "sem", "skew", "kurt"]: # median only has a float64 implementation # We should only get here with is_numeric, as non-numeric cases # should raise in _get_cython_function @@ -453,7 +454,7 @@ def _call_cython_op( **kwargs, ) result = result.astype(bool, copy=False) - elif self.how in ["skew"]: + elif self.how in ["skew", "kurt"]: func( out=result, counts=counts, @@ -1021,6 +1022,7 @@ def apply_groupwise( # getattr pattern for __name__ is needed for functools.partial objects if len(group_keys) == 0 and getattr(f, "__name__", None) in [ "skew", + "kurt", "sum", "prod", ]: diff --git a/pandas/tests/groupby/methods/test_kurt.py b/pandas/tests/groupby/methods/test_kurt.py new file mode 100644 index 0000000000000..21b7c50c3c5aa --- /dev/null +++ b/pandas/tests/groupby/methods/test_kurt.py @@ -0,0 +1,90 @@ +import numpy as np +import pytest + +import pandas.util._test_decorators as td + +import pandas as pd +import pandas._testing as tm + + +def test_groupby_kurt_equivalence(): + # GH#40139 + # Test that that groupby kurt method (which uses libgroupby.group_kurt) + # matches the results of operating group-by-group (which uses nanops.nankurt) + nrows = 1000 + ngroups = 3 + ncols = 2 + nan_frac = 0.05 + + arr = np.random.default_rng(2).standard_normal((nrows, ncols)) + arr[np.random.default_rng(2).random(nrows) < nan_frac] = np.nan + + df = pd.DataFrame(arr) + grps = np.random.default_rng(2).integers(0, ngroups, size=nrows) + gb = df.groupby(grps) + + result = gb.kurt() + + grpwise = [grp.kurt().to_frame(i).T for i, grp in gb] + expected = pd.concat(grpwise, axis=0) + expected.index = expected.index.astype("int64") # 32bit builds + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + "dtype", + [ + pytest.param("float64[pyarrow]", marks=td.skip_if_no("pyarrow")), + "Float64", + ], +) +def test_groupby_kurt_arrow_float64(dtype): + # GH#40139 + # Test groupby.kurt() with float64[pyarrow] and Float64 dtypes + df = pd.DataFrame( + { + "x": [1.0, np.nan, 3.2, 4.8, 2.3, 1.9, 8.9], + "y": [1.6, 3.3, 3.2, 6.8, 1.3, 2.9, 9.0], + }, + dtype=dtype, + ) + gb = df.groupby(by=lambda x: 0) + + result = gb.kurt() + expected = pd.DataFrame({"x": [2.1644713], "y": [0.1513969]}, dtype=dtype) + tm.assert_almost_equal(result, expected) + + +def test_groupby_kurt_noskipna(): + # GH#40139 + # Test groupby.kurt() with skipna = False + df = pd.DataFrame( + { + "x": [1.0, np.nan, 3.2, 4.8, 2.3, 1.9, 8.9], + "y": [1.6, 3.3, 3.2, 6.8, 1.3, 2.9, 9.0], + } + ) + gb = df.groupby(by=lambda x: 0) + + result = gb.kurt(skipna=False) + expected = pd.DataFrame({"x": [np.nan], "y": [0.1513969]}) + tm.assert_almost_equal(result, expected) + + +def test_groupby_kurt_all_ones(): + # GH#40139 + # Test groupby.kurt() with constant values + df = pd.DataFrame( + { + "x": [1.0] * 10, + } + ) + gb = df.groupby(by=lambda x: 0) + + result = gb.kurt(skipna=False) + expected = pd.DataFrame( + { + "x": [0.0], # Same behavior as pd.DataFrame.kurt() + } + ) + tm.assert_almost_equal(result, expected) diff --git a/pandas/tests/groupby/test_api.py b/pandas/tests/groupby/test_api.py index 013b308cd14cd..baec3ed1a5024 100644 --- a/pandas/tests/groupby/test_api.py +++ b/pandas/tests/groupby/test_api.py @@ -74,6 +74,7 @@ def test_tab_completion(multiindex_dataframe_random_data): "all", "shift", "skew", + "kurt", "take", "pct_change", "any", diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index 62d4a0ddcc0f5..294ab14c96de8 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -1381,6 +1381,7 @@ def test_result_name_when_one_group(name): ("apply", lambda gb: gb.values[-1]), ("apply", lambda gb: gb["b"].iloc[0]), ("agg", "skew"), + ("agg", "kurt"), ("agg", "prod"), ("agg", "sum"), ], diff --git a/pandas/tests/groupby/test_categorical.py b/pandas/tests/groupby/test_categorical.py index 656a61de5d105..20309e852a556 100644 --- a/pandas/tests/groupby/test_categorical.py +++ b/pandas/tests/groupby/test_categorical.py @@ -61,6 +61,7 @@ def f(a): "sem": np.nan, "size": 0, "skew": np.nan, + "kurt": np.nan, "std": np.nan, "sum": 0, "var": np.nan, diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index c4c1e7bd9ac4f..36eb2f119ae25 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -1710,7 +1710,7 @@ def test_pivot_table_values_key_error(): ) @pytest.mark.parametrize("method", ["attr", "agg", "apply"]) @pytest.mark.parametrize( - "op", ["idxmax", "idxmin", "min", "max", "sum", "prod", "skew"] + "op", ["idxmax", "idxmin", "min", "max", "sum", "prod", "skew", "kurt"] ) def test_empty_groupby(columns, keys, values, method, op, dropna, using_infer_string): # GH8093 & GH26411 @@ -1786,7 +1786,7 @@ def get_categorical_invalid_expected(): tm.assert_equal(result, expected) return - if op in ["prod", "sum", "skew"]: + if op in ["prod", "sum", "skew", "kurt"]: # ops that require more than just ordered-ness if is_dt64 or is_cat or is_per or (is_str and op != "sum"): # GH#41291 @@ -1799,15 +1799,15 @@ def get_categorical_invalid_expected(): msg = f"dtype 'str' does not support operation '{op}'" else: msg = "category type does not support" - if op == "skew": - msg = "|".join([msg, "does not support operation 'skew'"]) + if op in ["skew", "kurt"]: + msg = "|".join([msg, f"does not support operation '{op}'"]) with pytest.raises(TypeError, match=msg): get_result() if not isinstance(columns, list): # i.e. SeriesGroupBy return - elif op == "skew": + elif op in ["skew", "kurt"]: # TODO: test the numeric_only=True case return else: diff --git a/pandas/tests/groupby/test_numeric_only.py b/pandas/tests/groupby/test_numeric_only.py index 0779faa8d8975..99a88a5d8fe7c 100644 --- a/pandas/tests/groupby/test_numeric_only.py +++ b/pandas/tests/groupby/test_numeric_only.py @@ -244,6 +244,7 @@ def _check(self, df, method, expected_columns, expected_columns_numeric): ("quantile", True), ("sem", True), ("skew", True), + ("kurt", True), ("std", True), ("sum", True), ("var", True), @@ -378,6 +379,7 @@ def test_deprecate_numeric_only_series(dtype, groupby_func, request): "max", "prod", "skew", + "kurt", ) # Test default behavior; kernels that fail may be enabled in the future but kernels @@ -407,6 +409,7 @@ def test_deprecate_numeric_only_series(dtype, groupby_func, request): "quantile", "sem", "skew", + "kurt", "std", "sum", "var", diff --git a/pandas/tests/groupby/test_raises.py b/pandas/tests/groupby/test_raises.py index 789105c275625..ba13d3bd7278f 100644 --- a/pandas/tests/groupby/test_raises.py +++ b/pandas/tests/groupby/test_raises.py @@ -171,6 +171,7 @@ def test_groupby_raises_string( "shift": (None, ""), "size": (None, ""), "skew": (ValueError, "could not convert string to float"), + "kurt": (ValueError, "could not convert string to float"), "std": (ValueError, "could not convert string to float"), "sum": (None, ""), "var": ( @@ -190,10 +191,11 @@ def test_groupby_raises_string( "sem", "var", "skew", + "kurt", "quantile", ]: msg = f"dtype 'str' does not support operation '{groupby_func}'" - if groupby_func in ["sem", "std", "skew"]: + if groupby_func in ["sem", "std", "skew", "kurt"]: # The object-dtype raises ValueError when trying to convert to numeric. klass = TypeError elif groupby_func == "pct_change" and df["d"].dtype.storage == "pyarrow": @@ -323,6 +325,15 @@ def test_groupby_raises_datetime( ] ), ), + "kurt": ( + TypeError, + "|".join( + [ + r"dtype datetime64\[ns\] does not support operation", + "datetime64 type does not support operation 'kurt'", + ] + ), + ), "std": (None, ""), "sum": (TypeError, "datetime64 type does not support operation 'sum"), "var": (TypeError, "datetime64 type does not support operation 'var'"), @@ -372,7 +383,7 @@ def test_groupby_raises_datetime_np( _call_and_check(klass, msg, how, gb, groupby_func_np, ()) -@pytest.mark.parametrize("func", ["prod", "cumprod", "skew", "var"]) +@pytest.mark.parametrize("func", ["prod", "cumprod", "skew", "kurt", "var"]) def test_groupby_raises_timedelta(func): df = DataFrame( { @@ -502,6 +513,15 @@ def test_groupby_raises_category( ] ), ), + "kurt": ( + TypeError, + "|".join( + [ + "dtype category does not support operation 'kurt'", + "category type does not support kurt operations", + ] + ), + ), "std": ( TypeError, "|".join( @@ -676,6 +696,15 @@ def test_groupby_raises_category_on_category( ] ), ), + "kurt": ( + TypeError, + "|".join( + [ + "category type does not support kurt operations", + "dtype category does not support operation 'kurt'", + ] + ), + ), "std": ( TypeError, "|".join( diff --git a/pandas/tests/groupby/test_reductions.py b/pandas/tests/groupby/test_reductions.py index 51c7eab2bfa82..a17200c123d22 100644 --- a/pandas/tests/groupby/test_reductions.py +++ b/pandas/tests/groupby/test_reductions.py @@ -1114,6 +1114,7 @@ def test_apply_to_nullable_integer_returns_float(values, function): "median", "mean", "skew", + "kurt", "std", "var", "sem", @@ -1127,8 +1128,8 @@ def test_regression_allowlist_methods(op, skipna, sort): grouped = frame.groupby(level=0, sort=sort) - if op == "skew": - # skew has skipna + if op in ["skew", "kurt"]: + # skew and kurt have skipna result = getattr(grouped, op)(skipna=skipna) expected = frame.groupby(level=0).apply(lambda h: getattr(h, op)(skipna=skipna)) if sort: diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py index 888b97f2e0206..fecd20fd6cece 100644 --- a/pandas/tests/groupby/transform/test_transform.py +++ b/pandas/tests/groupby/transform/test_transform.py @@ -1088,13 +1088,13 @@ def test_transform_agg_by_name(request, reduction_func, frame_or_series): func = reduction_func obj = DataFrame( - {"a": [0, 0, 0, 1, 1, 1], "b": range(6)}, - index=["A", "B", "C", "D", "E", "F"], + {"a": [0, 0, 0, 0, 1, 1, 1, 1], "b": range(8)}, + index=["A", "B", "C", "D", "E", "F", "G", "H"], ) if frame_or_series is Series: obj = obj["a"] - g = obj.groupby(np.repeat([0, 1], 3)) + g = obj.groupby(np.repeat([0, 1], 4)) if func == "corrwith" and isinstance(obj, Series): # GH#32293 # TODO: implement SeriesGroupBy.corrwith @@ -1119,7 +1119,7 @@ def test_transform_agg_by_name(request, reduction_func, frame_or_series): tm.assert_index_equal(result.columns, obj.columns) # verify that values were broadcasted across each group - assert len(set(DataFrame(result).iloc[-3:, -1])) == 1 + assert len(set(DataFrame(result).iloc[-4:, -1])) == 1 def test_transform_lambda_with_datetimetz(): From 18dbcebe79502bece352d8ee3704ba5c83e3148b Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:55:13 -0500 Subject: [PATCH 200/266] TST(string dtype): Resolve xfails in stack_unstack (#60703) --- pandas/tests/frame/test_stack_unstack.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pandas/tests/frame/test_stack_unstack.py b/pandas/tests/frame/test_stack_unstack.py index dae7fe2575c22..abc14d10514fa 100644 --- a/pandas/tests/frame/test_stack_unstack.py +++ b/pandas/tests/frame/test_stack_unstack.py @@ -5,8 +5,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs import lib import pandas as pd @@ -1675,7 +1673,6 @@ def test_unstack_multiple_no_empty_columns(self): expected = unstacked.dropna(axis=1, how="all") tm.assert_frame_equal(unstacked, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.filterwarnings( "ignore:The previous implementation of stack is deprecated" ) @@ -1923,7 +1920,6 @@ def test_stack_level_name(self, multiindex_dataframe_random_data, future_stack): expected = frame.stack(future_stack=future_stack) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.filterwarnings( "ignore:The previous implementation of stack is deprecated" ) From c98e083eb4e90ff1be10615b17e64901bc0cbc45 Mon Sep 17 00:00:00 2001 From: William Andrea <22385371+wjandrea@users.noreply.github.com> Date: Sun, 12 Jan 2025 16:56:29 -0400 Subject: [PATCH 201/266] DOC: Fix IPython prompts in io.rst (#60704) --- doc/source/user_guide/io.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index 7c165c87adb46..daf323acff129 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -2340,6 +2340,7 @@ Read a URL with no options: .. code-block:: ipython In [320]: url = "https://www.fdic.gov/resources/resolutions/bank-failures/failed-bank-list" + In [321]: pd.read_html(url) Out[321]: [ Bank NameBank CityCity StateSt ... Acquiring InstitutionAI Closing DateClosing FundFund @@ -2366,6 +2367,7 @@ Read a URL while passing headers alongside the HTTP request: .. code-block:: ipython In [322]: url = 'https://www.sump.org/notes/request/' # HTTP request reflector + In [323]: pd.read_html(url) Out[323]: [ 0 1 @@ -2378,14 +2380,16 @@ Read a URL while passing headers alongside the HTTP request: 1 Host: www.sump.org 2 User-Agent: Python-urllib/3.8 3 Connection: close] + In [324]: headers = { - In [325]: 'User-Agent':'Mozilla Firefox v14.0', - In [326]: 'Accept':'application/json', - In [327]: 'Connection':'keep-alive', - In [328]: 'Auth':'Bearer 2*/f3+fe68df*4' - In [329]: } - In [340]: pd.read_html(url, storage_options=headers) - Out[340]: + .....: 'User-Agent':'Mozilla Firefox v14.0', + .....: 'Accept':'application/json', + .....: 'Connection':'keep-alive', + .....: 'Auth':'Bearer 2*/f3+fe68df*4' + .....: } + + In [325]: pd.read_html(url, storage_options=headers) + Out[325]: [ 0 1 0 Remote Socket: 51.15.105.256:51760 1 Protocol Version: HTTP/1.1 From 5dee21220988c907d489c066620af407dacdee2f Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:58:31 -0500 Subject: [PATCH 202/266] TST(str dtype): Resolve xfail in test_value_counts.py (#60701) * TST(str dtype): Resolve xfail in test_value_counts.py * Revert --- pandas/tests/frame/methods/test_value_counts.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pandas/tests/frame/methods/test_value_counts.py b/pandas/tests/frame/methods/test_value_counts.py index de5029b9f18b2..43db234267f21 100644 --- a/pandas/tests/frame/methods/test_value_counts.py +++ b/pandas/tests/frame/methods/test_value_counts.py @@ -1,10 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW - import pandas as pd import pandas._testing as tm @@ -136,9 +132,6 @@ def test_data_frame_value_counts_dropna_true(nulls_fixture): tm.assert_series_equal(result, expected) -@pytest.mark.xfail( - using_string_dtype() and not HAS_PYARROW, reason="TODO(infer_string)", strict=False -) def test_data_frame_value_counts_dropna_false(nulls_fixture): # GH 41334 df = pd.DataFrame( From 57d248978a4168aa73871a62ab79c47dc2977bb0 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:59:55 -0500 Subject: [PATCH 203/266] TST(string dtype): Resolve xfail in test_find_replace.py (#60700) --- pandas/tests/strings/test_find_replace.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/pandas/tests/strings/test_find_replace.py b/pandas/tests/strings/test_find_replace.py index 34a6377b5786f..30e6ebf0eed13 100644 --- a/pandas/tests/strings/test_find_replace.py +++ b/pandas/tests/strings/test_find_replace.py @@ -293,23 +293,12 @@ def test_startswith_endswith_validate_na(any_string_dtype): dtype=any_string_dtype, ) - dtype = ser.dtype - if (isinstance(dtype, pd.StringDtype)) or dtype == np.dtype("object"): - msg = "Allowing a non-bool 'na' in obj.str.startswith is deprecated" - with tm.assert_produces_warning(FutureWarning, match=msg): - ser.str.startswith("kapow", na="baz") - msg = "Allowing a non-bool 'na' in obj.str.endswith is deprecated" - with tm.assert_produces_warning(FutureWarning, match=msg): - ser.str.endswith("bar", na="baz") - else: - # TODO(infer_string): don't surface pyarrow errors - import pyarrow as pa - - msg = "Could not convert 'baz' with type str: tried to convert to boolean" - with pytest.raises(pa.lib.ArrowInvalid, match=msg): - ser.str.startswith("kapow", na="baz") - with pytest.raises(pa.lib.ArrowInvalid, match=msg): - ser.str.endswith("kapow", na="baz") + msg = "Allowing a non-bool 'na' in obj.str.startswith is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + ser.str.startswith("kapow", na="baz") + msg = "Allowing a non-bool 'na' in obj.str.endswith is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + ser.str.endswith("bar", na="baz") @pytest.mark.parametrize("pat", ["foo", ("foo", "baz")]) From 7415aca37159a99f8f99d93a1908070ddf36178c Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Mon, 13 Jan 2025 10:48:22 +0100 Subject: [PATCH 204/266] String dtype: disallow specifying the 'str' dtype with storage in [..] in string alias (#60661) --- pandas/core/dtypes/dtypes.py | 2 +- pandas/tests/dtypes/test_common.py | 20 ++++++++++++++++++++ pandas/tests/strings/test_get_dummies.py | 7 +++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 1dd1b12d6ae95..1eb1a630056a2 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -2339,7 +2339,7 @@ def construct_from_string(cls, string: str) -> ArrowDtype: ) if not string.endswith("[pyarrow]"): raise TypeError(f"'{string}' must end with '[pyarrow]'") - if string == "string[pyarrow]": + if string in ("string[pyarrow]", "str[pyarrow]"): # Ensure Registry.find skips ArrowDtype to use StringDtype instead raise TypeError("string[pyarrow] should be constructed by StringDtype") if pa_version_under10p1: diff --git a/pandas/tests/dtypes/test_common.py b/pandas/tests/dtypes/test_common.py index 5a59617ce5bd3..fa48393dd183e 100644 --- a/pandas/tests/dtypes/test_common.py +++ b/pandas/tests/dtypes/test_common.py @@ -837,6 +837,26 @@ def test_pandas_dtype_string_dtypes(string_storage): assert result == pd.StringDtype(string_storage, na_value=pd.NA) +def test_pandas_dtype_string_dtype_alias_with_storage(): + with pytest.raises(TypeError, match="not understood"): + pandas_dtype("str[python]") + + with pytest.raises(TypeError, match="not understood"): + pandas_dtype("str[pyarrow]") + + result = pandas_dtype("string[python]") + assert result == pd.StringDtype("python", na_value=pd.NA) + + if HAS_PYARROW: + result = pandas_dtype("string[pyarrow]") + assert result == pd.StringDtype("pyarrow", na_value=pd.NA) + else: + with pytest.raises( + ImportError, match="required for PyArrow backed StringArray" + ): + pandas_dtype("string[pyarrow]") + + @td.skip_if_installed("pyarrow") def test_construct_from_string_without_pyarrow_installed(): # GH 57928 diff --git a/pandas/tests/strings/test_get_dummies.py b/pandas/tests/strings/test_get_dummies.py index 3b989e284ca25..541b0ea150ba6 100644 --- a/pandas/tests/strings/test_get_dummies.py +++ b/pandas/tests/strings/test_get_dummies.py @@ -6,6 +6,7 @@ import pandas.util._test_decorators as td from pandas import ( + ArrowDtype, DataFrame, Index, MultiIndex, @@ -113,8 +114,10 @@ def test_get_dummies_with_str_dtype(any_string_dtype): # GH#47872 @td.skip_if_no("pyarrow") def test_get_dummies_with_pa_str_dtype(any_string_dtype): + import pyarrow as pa + s = Series(["a|b", "a|c", np.nan], dtype=any_string_dtype) - result = s.str.get_dummies("|", dtype="str[pyarrow]") + result = s.str.get_dummies("|", dtype=ArrowDtype(pa.string())) expected = DataFrame( [ ["true", "true", "false"], @@ -122,6 +125,6 @@ def test_get_dummies_with_pa_str_dtype(any_string_dtype): ["false", "false", "false"], ], columns=list("abc"), - dtype="str[pyarrow]", + dtype=ArrowDtype(pa.string()), ) tm.assert_frame_equal(result, expected) From 55a6d0a613897040fec1ae11adc15f5f04728032 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:52:10 -0500 Subject: [PATCH 205/266] TST(string dtype): Resolve xfail when grouping by nan column (#60712) --- pandas/tests/groupby/test_groupby.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 36eb2f119ae25..5bae9b1fd9882 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.errors import SpecificationError import pandas.util._test_decorators as td @@ -2468,12 +2466,13 @@ def test_groupby_none_in_first_mi_level(): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") -def test_groupby_none_column_name(): +def test_groupby_none_column_name(using_infer_string): # GH#47348 df = DataFrame({None: [1, 1, 2, 2], "b": [1, 1, 2, 3], "c": [4, 5, 6, 7]}) - result = df.groupby(by=[None]).sum() - expected = DataFrame({"b": [2, 5], "c": [9, 13]}, index=Index([1, 2], name=None)) + by = [np.nan] if using_infer_string else [None] + gb = df.groupby(by=by) + result = gb.sum() + expected = DataFrame({"b": [2, 5], "c": [9, 13]}, index=Index([1, 2], name=by[0])) tm.assert_frame_equal(result, expected) From f787764c43fda0a76ad9dc0dd7e0e65f6c69d907 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 13 Jan 2025 14:20:50 -0800 Subject: [PATCH 206/266] ENH: Support pipe() method in Rolling and Expanding (#60697) * ENH: Support pipe() method in Rolling and Expanding * Fix mypy errors * Fix docstring errors * Add pipe method to doc reference --- doc/source/reference/window.rst | 2 + doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/window/doc.py | 57 ++++++++++++++++++++++ pandas/core/window/expanding.py | 61 ++++++++++++++++++++++- pandas/core/window/rolling.py | 85 ++++++++++++++++++++++++++++++++- pandas/tests/window/test_api.py | 32 +++++++++++++ 6 files changed, 236 insertions(+), 2 deletions(-) diff --git a/doc/source/reference/window.rst b/doc/source/reference/window.rst index 14af2b8a120e0..fb89fd2a5ffb2 100644 --- a/doc/source/reference/window.rst +++ b/doc/source/reference/window.rst @@ -35,6 +35,7 @@ Rolling window functions Rolling.skew Rolling.kurt Rolling.apply + Rolling.pipe Rolling.aggregate Rolling.quantile Rolling.sem @@ -76,6 +77,7 @@ Expanding window functions Expanding.skew Expanding.kurt Expanding.apply + Expanding.pipe Expanding.aggregate Expanding.quantile Expanding.sem diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 47838d1e49d61..34df7fc2027a5 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -44,6 +44,7 @@ Other enhancements - Users can globally disable any ``PerformanceWarning`` by setting the option ``mode.performance_warnings`` to ``False`` (:issue:`56920`) - :meth:`Styler.format_index_names` can now be used to format the index and column names (:issue:`48936` and :issue:`47489`) - :class:`.errors.DtypeWarning` improved to include column names when mixed data types are detected (:issue:`58174`) +- :class:`Rolling` and :class:`Expanding` now support ``pipe`` method (:issue:`57076`) - :class:`Series` now supports the Arrow PyCapsule Interface for export (:issue:`59518`) - :func:`DataFrame.to_excel` argument ``merge_cells`` now accepts a value of ``"columns"`` to only merge :class:`MultiIndex` column header header cells (:issue:`35384`) - :meth:`DataFrame.corrwith` now accepts ``min_periods`` as optional arguments, as in :meth:`DataFrame.corr` and :meth:`Series.corr` (:issue:`9490`) diff --git a/pandas/core/window/doc.py b/pandas/core/window/doc.py index cdb670ee218b4..6dbc52a99e70c 100644 --- a/pandas/core/window/doc.py +++ b/pandas/core/window/doc.py @@ -85,6 +85,63 @@ def create_section_header(header: str) -> str: """ ).replace("\n", "", 1) +template_pipe = """ +Apply a ``func`` with arguments to this %(klass)s object and return its result. + +Use `.pipe` when you want to improve readability by chaining together +functions that expect Series, DataFrames, GroupBy, Rolling, Expanding or Resampler +objects. +Instead of writing + +>>> h = lambda x, arg2, arg3: x + 1 - arg2 * arg3 +>>> g = lambda x, arg1: x * 5 / arg1 +>>> f = lambda x: x ** 4 +>>> df = pd.DataFrame({'A': [1, 2, 3, 4]}, index=pd.date_range('2012-08-02', periods=4)) +>>> h(g(f(df.rolling('2D')), arg1=1), arg2=2, arg3=3) # doctest: +SKIP + +You can write + +>>> (df.rolling('2D') +... .pipe(f) +... .pipe(g, arg1=1) +... .pipe(h, arg2=2, arg3=3)) # doctest: +SKIP + +which is much more readable. + +Parameters +---------- +func : callable or tuple of (callable, str) + Function to apply to this %(klass)s object or, alternatively, + a `(callable, data_keyword)` tuple where `data_keyword` is a + string indicating the keyword of `callable` that expects the + %(klass)s object. +*args : iterable, optional + Positional arguments passed into `func`. +**kwargs : dict, optional + A dictionary of keyword arguments passed into `func`. + +Returns +------- +%(klass)s + The original object with the function `func` applied. + +See Also +-------- +Series.pipe : Apply a function with arguments to a series. +DataFrame.pipe: Apply a function with arguments to a dataframe. +apply : Apply function to each group instead of to the + full %(klass)s object. + +Notes +----- +See more `here +`_ + +Examples +-------- +%(examples)s +""" + numba_notes = ( "See :ref:`window.numba_engine` and :ref:`enhancingperf.numba` for " "extended documentation and performance considerations for the Numba engine.\n\n" diff --git a/pandas/core/window/expanding.py b/pandas/core/window/expanding.py index bff3a1660eba9..6a7d0329ab6da 100644 --- a/pandas/core/window/expanding.py +++ b/pandas/core/window/expanding.py @@ -5,9 +5,15 @@ TYPE_CHECKING, Any, Literal, + final, + overload, ) -from pandas.util._decorators import doc +from pandas.util._decorators import ( + Appender, + Substitution, + doc, +) from pandas.core.indexers.objects import ( BaseIndexer, @@ -20,6 +26,7 @@ kwargs_numeric_only, numba_notes, template_header, + template_pipe, template_returns, template_see_also, window_agg_numba_parameters, @@ -34,7 +41,11 @@ from collections.abc import Callable from pandas._typing import ( + Concatenate, + P, QuantileInterpolation, + Self, + T, WindowingRankType, ) @@ -241,6 +252,54 @@ def apply( kwargs=kwargs, ) + @overload + def pipe( + self, + func: Callable[Concatenate[Self, P], T], + *args: P.args, + **kwargs: P.kwargs, + ) -> T: ... + + @overload + def pipe( + self, + func: tuple[Callable[..., T], str], + *args: Any, + **kwargs: Any, + ) -> T: ... + + @final + @Substitution( + klass="Expanding", + examples=""" + >>> df = pd.DataFrame({'A': [1, 2, 3, 4]}, + ... index=pd.date_range('2012-08-02', periods=4)) + >>> df + A + 2012-08-02 1 + 2012-08-03 2 + 2012-08-04 3 + 2012-08-05 4 + + To get the difference between each expanding window's maximum and minimum + value in one pass, you can do + + >>> df.expanding().pipe(lambda x: x.max() - x.min()) + A + 2012-08-02 0.0 + 2012-08-03 1.0 + 2012-08-04 2.0 + 2012-08-05 3.0""", + ) + @Appender(template_pipe) + def pipe( + self, + func: Callable[Concatenate[Self, P], T] | tuple[Callable[..., T], str], + *args: Any, + **kwargs: Any, + ) -> T: + return super().pipe(func, *args, **kwargs) + @doc( template_header, create_section_header("Parameters"), diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 385ffb901acf0..90c3cff975ff0 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -14,6 +14,8 @@ TYPE_CHECKING, Any, Literal, + final, + overload, ) import numpy as np @@ -26,7 +28,11 @@ import pandas._libs.window.aggregations as window_aggregations from pandas.compat._optional import import_optional_dependency from pandas.errors import DataError -from pandas.util._decorators import doc +from pandas.util._decorators import ( + Appender, + Substitution, + doc, +) from pandas.core.dtypes.common import ( ensure_float64, @@ -81,6 +87,7 @@ kwargs_scipy, numba_notes, template_header, + template_pipe, template_returns, template_see_also, window_agg_numba_parameters, @@ -102,8 +109,12 @@ from pandas._typing import ( ArrayLike, + Concatenate, NDFrameT, QuantileInterpolation, + P, + Self, + T, WindowingRankType, npt, ) @@ -1529,6 +1540,30 @@ def apply_func(values, begin, end, min_periods, raw=raw): return apply_func + @overload + def pipe( + self, + func: Callable[Concatenate[Self, P], T], + *args: P.args, + **kwargs: P.kwargs, + ) -> T: ... + + @overload + def pipe( + self, + func: tuple[Callable[..., T], str], + *args: Any, + **kwargs: Any, + ) -> T: ... + + def pipe( + self, + func: Callable[Concatenate[Self, P], T] | tuple[Callable[..., T], str], + *args: Any, + **kwargs: Any, + ) -> T: + return com.pipe(self, func, *args, **kwargs) + def sum( self, numeric_only: bool = False, @@ -2044,6 +2079,54 @@ def apply( kwargs=kwargs, ) + @overload + def pipe( + self, + func: Callable[Concatenate[Self, P], T], + *args: P.args, + **kwargs: P.kwargs, + ) -> T: ... + + @overload + def pipe( + self, + func: tuple[Callable[..., T], str], + *args: Any, + **kwargs: Any, + ) -> T: ... + + @final + @Substitution( + klass="Rolling", + examples=""" + >>> df = pd.DataFrame({'A': [1, 2, 3, 4]}, + ... index=pd.date_range('2012-08-02', periods=4)) + >>> df + A + 2012-08-02 1 + 2012-08-03 2 + 2012-08-04 3 + 2012-08-05 4 + + To get the difference between each rolling 2-day window's maximum and minimum + value in one pass, you can do + + >>> df.rolling('2D').pipe(lambda x: x.max() - x.min()) + A + 2012-08-02 0.0 + 2012-08-03 1.0 + 2012-08-04 1.0 + 2012-08-05 1.0""", + ) + @Appender(template_pipe) + def pipe( + self, + func: Callable[Concatenate[Self, P], T] | tuple[Callable[..., T], str], + *args: Any, + **kwargs: Any, + ) -> T: + return super().pipe(func, *args, **kwargs) + @doc( template_header, create_section_header("Parameters"), diff --git a/pandas/tests/window/test_api.py b/pandas/tests/window/test_api.py index 15eaa8c167487..877b50e37670c 100644 --- a/pandas/tests/window/test_api.py +++ b/pandas/tests/window/test_api.py @@ -177,6 +177,38 @@ def test_agg_nested_dicts(): r.agg({"A": {"ra": ["mean", "std"]}, "B": {"rb": ["mean", "std"]}}) +@pytest.mark.parametrize( + "func,window_size", + [ + ( + "rolling", + 2, + ), + ( + "expanding", + None, + ), + ], +) +def test_pipe(func, window_size): + # Issue #57076 + df = DataFrame( + { + "B": np.random.default_rng(2).standard_normal(10), + "C": np.random.default_rng(2).standard_normal(10), + } + ) + r = getattr(df, func)(window_size) + + expected = r.max() - r.mean() + result = r.pipe(lambda x: x.max() - x.mean()) + tm.assert_frame_equal(result, expected) + + expected = r.max() - 2 * r.min() + result = r.pipe(lambda x, k: x.max() - k * x.min(), k=2) + tm.assert_frame_equal(result, expected) + + def test_count_nonnumeric_types(step): # GH12541 cols = [ From 221ad46d193c9a5e46fb50aea3d91efdf1310c31 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Mon, 13 Jan 2025 17:24:39 -0500 Subject: [PATCH 207/266] Remove sizeof(char) uses (#60717) --- pandas/_libs/src/parser/tokenizer.c | 7 +++--- .../src/vendored/ujson/python/objToJSON.c | 22 +++++++++---------- pandas/_libs/tslibs/period.pyx | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pandas/_libs/src/parser/tokenizer.c b/pandas/_libs/src/parser/tokenizer.c index c9f7a796a9b1c..61e96fc835e4d 100644 --- a/pandas/_libs/src/parser/tokenizer.c +++ b/pandas/_libs/src/parser/tokenizer.c @@ -148,7 +148,7 @@ int parser_init(parser_t *self) { self->warn_msg = NULL; // token stream - self->stream = malloc(STREAM_INIT_SIZE * sizeof(char)); + self->stream = malloc(STREAM_INIT_SIZE); if (self->stream == NULL) { parser_cleanup(self); return PARSER_OUT_OF_MEMORY; @@ -221,9 +221,8 @@ static int make_stream_space(parser_t *self, size_t nbytes) { char *orig_ptr = (void *)self->stream; TRACE(("\n\nmake_stream_space: nbytes = %zu. grow_buffer(self->stream...)\n", nbytes)) - self->stream = - (char *)grow_buffer((void *)self->stream, self->stream_len, - &self->stream_cap, nbytes * 2, sizeof(char), &status); + self->stream = (char *)grow_buffer((void *)self->stream, self->stream_len, + &self->stream_cap, nbytes * 2, 1, &status); TRACE(("make_stream_space: self->stream=%p, self->stream_len = %zu, " "self->stream_cap=%zu, status=%zu\n", self->stream, self->stream_len, self->stream_cap, status)) diff --git a/pandas/_libs/src/vendored/ujson/python/objToJSON.c b/pandas/_libs/src/vendored/ujson/python/objToJSON.c index 5f35860c59cb7..6b957148f94da 100644 --- a/pandas/_libs/src/vendored/ujson/python/objToJSON.c +++ b/pandas/_libs/src/vendored/ujson/python/objToJSON.c @@ -984,7 +984,7 @@ static char *List_iterGetName(JSOBJ Py_UNUSED(obj), //============================================================================= static void Index_iterBegin(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { GET_TC(tc)->index = 0; - GET_TC(tc)->cStr = PyObject_Malloc(20 * sizeof(char)); + GET_TC(tc)->cStr = PyObject_Malloc(20); if (!GET_TC(tc)->cStr) { PyErr_NoMemory(); } @@ -998,10 +998,10 @@ static int Index_iterNext(JSOBJ obj, JSONTypeContext *tc) { const Py_ssize_t index = GET_TC(tc)->index; Py_XDECREF(GET_TC(tc)->itemValue); if (index == 0) { - memcpy(GET_TC(tc)->cStr, "name", sizeof(char) * 5); + memcpy(GET_TC(tc)->cStr, "name", 5); GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "name"); } else if (index == 1) { - memcpy(GET_TC(tc)->cStr, "data", sizeof(char) * 5); + memcpy(GET_TC(tc)->cStr, "data", 5); GET_TC(tc)->itemValue = get_values(obj); if (!GET_TC(tc)->itemValue) { return 0; @@ -1033,7 +1033,7 @@ static char *Index_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, static void Series_iterBegin(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { PyObjectEncoder *enc = (PyObjectEncoder *)tc->encoder; GET_TC(tc)->index = 0; - GET_TC(tc)->cStr = PyObject_Malloc(20 * sizeof(char)); + GET_TC(tc)->cStr = PyObject_Malloc(20); enc->outputFormat = VALUES; // for contained series if (!GET_TC(tc)->cStr) { PyErr_NoMemory(); @@ -1048,13 +1048,13 @@ static int Series_iterNext(JSOBJ obj, JSONTypeContext *tc) { const Py_ssize_t index = GET_TC(tc)->index; Py_XDECREF(GET_TC(tc)->itemValue); if (index == 0) { - memcpy(GET_TC(tc)->cStr, "name", sizeof(char) * 5); + memcpy(GET_TC(tc)->cStr, "name", 5); GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "name"); } else if (index == 1) { - memcpy(GET_TC(tc)->cStr, "index", sizeof(char) * 6); + memcpy(GET_TC(tc)->cStr, "index", 6); GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "index"); } else if (index == 2) { - memcpy(GET_TC(tc)->cStr, "data", sizeof(char) * 5); + memcpy(GET_TC(tc)->cStr, "data", 5); GET_TC(tc)->itemValue = get_values(obj); if (!GET_TC(tc)->itemValue) { return 0; @@ -1088,7 +1088,7 @@ static char *Series_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, static void DataFrame_iterBegin(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { PyObjectEncoder *enc = (PyObjectEncoder *)tc->encoder; GET_TC(tc)->index = 0; - GET_TC(tc)->cStr = PyObject_Malloc(20 * sizeof(char)); + GET_TC(tc)->cStr = PyObject_Malloc(20); enc->outputFormat = VALUES; // for contained series & index if (!GET_TC(tc)->cStr) { PyErr_NoMemory(); @@ -1103,13 +1103,13 @@ static int DataFrame_iterNext(JSOBJ obj, JSONTypeContext *tc) { const Py_ssize_t index = GET_TC(tc)->index; Py_XDECREF(GET_TC(tc)->itemValue); if (index == 0) { - memcpy(GET_TC(tc)->cStr, "columns", sizeof(char) * 8); + memcpy(GET_TC(tc)->cStr, "columns", 8); GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "columns"); } else if (index == 1) { - memcpy(GET_TC(tc)->cStr, "index", sizeof(char) * 6); + memcpy(GET_TC(tc)->cStr, "index", 6); GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "index"); } else if (index == 2) { - memcpy(GET_TC(tc)->cStr, "data", sizeof(char) * 5); + memcpy(GET_TC(tc)->cStr, "data", 5); Py_INCREF(obj); GET_TC(tc)->itemValue = obj; } else { diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index d6d69a49c9539..f697180da5eeb 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -679,7 +679,7 @@ cdef char* c_strftime(npy_datetimestruct *dts, char *fmt): c_date.tm_yday = get_day_of_year(dts.year, dts.month, dts.day) - 1 c_date.tm_isdst = -1 - result = malloc(result_len * sizeof(char)) + result = malloc(result_len) if result is NULL: raise MemoryError() From 1708e9020c418e91fae430cf6a7a6ec09c466429 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:27:47 -0500 Subject: [PATCH 208/266] ENH: Enable .mode to sort with NA values (#60702) --- pandas/core/algorithms.py | 2 +- pandas/tests/frame/test_reductions.py | 17 ++--------------- pandas/tests/reductions/test_reductions.py | 13 +++---------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 56f8adda93251..eefe08859c1e9 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -1012,7 +1012,7 @@ def mode( return npresult, res_mask # type: ignore[return-value] try: - npresult = np.sort(npresult) + npresult = safe_sort(npresult) except TypeError as err: warnings.warn( f"Unable to sort modes: {err}", diff --git a/pandas/tests/frame/test_reductions.py b/pandas/tests/frame/test_reductions.py index fde4dfeed9c55..04b1456cdbea6 100644 --- a/pandas/tests/frame/test_reductions.py +++ b/pandas/tests/frame/test_reductions.py @@ -672,23 +672,10 @@ def test_mode_dropna(self, dropna, expected): expected = DataFrame(expected) tm.assert_frame_equal(result, expected) - def test_mode_sortwarning(self, using_infer_string): - # Check for the warning that is raised when the mode - # results cannot be sorted - + def test_mode_sort_with_na(self, using_infer_string): df = DataFrame({"A": [np.nan, np.nan, "a", "a"]}) expected = DataFrame({"A": ["a", np.nan]}) - - # TODO(infer_string) avoid this UserWarning for python storage - warning = ( - None - if using_infer_string and df.A.dtype.storage == "pyarrow" - else UserWarning - ) - with tm.assert_produces_warning(warning, match="Unable to sort modes"): - result = df.mode(dropna=False) - result = result.sort_values(by="A").reset_index(drop=True) - + result = df.mode(dropna=False) tm.assert_frame_equal(result, expected) def test_mode_empty_df(self): diff --git a/pandas/tests/reductions/test_reductions.py b/pandas/tests/reductions/test_reductions.py index 476978aeab15a..a7bb80727206e 100644 --- a/pandas/tests/reductions/test_reductions.py +++ b/pandas/tests/reductions/test_reductions.py @@ -1607,17 +1607,10 @@ def test_mode_intoverflow(self, dropna, expected1, expected2): expected2 = Series(expected2, dtype=np.uint64) tm.assert_series_equal(result, expected2) - def test_mode_sortwarning(self): - # Check for the warning that is raised when the mode - # results cannot be sorted - - expected = Series(["foo", np.nan], dtype=object) + def test_mode_sort_with_na(self): s = Series([1, "foo", "foo", np.nan, np.nan]) - - with tm.assert_produces_warning(UserWarning, match="Unable to sort modes"): - result = s.mode(dropna=False) - result = result.sort_values().reset_index(drop=True) - + expected = Series(["foo", np.nan], dtype=object) + result = s.mode(dropna=False) tm.assert_series_equal(result, expected) def test_mode_boolean_with_na(self): From b5d4e89d378e69a87b1b9ac7f3d6fa6867840fff Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:28:28 -0500 Subject: [PATCH 209/266] ENH: Implement cum* methods for PyArrow strings (#60633) * ENH: Implement cum* methods for PyArrow strings * cleanup * Cleanup * fixup * Fix extension tests * xfail test when there is no pyarrow * mypy fixups * Change logic & whatsnew * Change logic & whatsnew * Fix fixture * Fixup --- doc/source/whatsnew/v2.3.0.rst | 2 +- pandas/conftest.py | 16 +++++++ pandas/core/arrays/arrow/array.py | 55 +++++++++++++++++++++++ pandas/tests/apply/test_str.py | 9 ++-- pandas/tests/extension/base/accumulate.py | 5 ++- pandas/tests/extension/test_arrow.py | 15 ++++--- pandas/tests/extension/test_string.py | 10 +++++ pandas/tests/series/test_cumulative.py | 54 ++++++++++++++++++++++ 8 files changed, 155 insertions(+), 11 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index b107a5d3ba100..9e0e095eb4de8 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -35,8 +35,8 @@ Other enhancements - The semantics for the ``copy`` keyword in ``__array__`` methods (i.e. called when using ``np.array()`` or ``np.asarray()`` on pandas objects) has been updated to work correctly with NumPy >= 2 (:issue:`57739`) +- The :meth:`~Series.cumsum`, :meth:`~Series.cummin`, and :meth:`~Series.cummax` reductions are now implemented for ``StringDtype`` columns when backed by PyArrow (:issue:`60633`) - The :meth:`~Series.sum` reduction is now implemented for ``StringDtype`` columns (:issue:`59853`) -- .. --------------------------------------------------------------------------- .. _whatsnew_230.notable_bug_fixes: diff --git a/pandas/conftest.py b/pandas/conftest.py index 106518678df6a..f9c10a7758bd2 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1317,6 +1317,22 @@ def nullable_string_dtype(request): return request.param +@pytest.fixture( + params=[ + pytest.param(("pyarrow", np.nan), marks=td.skip_if_no("pyarrow")), + pytest.param(("pyarrow", pd.NA), marks=td.skip_if_no("pyarrow")), + ] +) +def pyarrow_string_dtype(request): + """ + Parametrized fixture for string dtypes backed by Pyarrow. + + * 'str[pyarrow]' + * 'string[pyarrow]' + """ + return pd.StringDtype(*request.param) + + @pytest.fixture( params=[ "python", diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 4d9c8eb3a41b6..900548a239c8e 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -41,6 +41,7 @@ is_list_like, is_numeric_dtype, is_scalar, + is_string_dtype, pandas_dtype, ) from pandas.core.dtypes.dtypes import DatetimeTZDtype @@ -1619,6 +1620,9 @@ def _accumulate( ------ NotImplementedError : subclass does not define accumulations """ + if is_string_dtype(self): + return self._str_accumulate(name=name, skipna=skipna, **kwargs) + pyarrow_name = { "cummax": "cumulative_max", "cummin": "cumulative_min", @@ -1654,6 +1658,57 @@ def _accumulate( return type(self)(result) + def _str_accumulate( + self, name: str, *, skipna: bool = True, **kwargs + ) -> ArrowExtensionArray | ExtensionArray: + """ + Accumulate implementation for strings, see `_accumulate` docstring for details. + + pyarrow.compute does not implement these methods for strings. + """ + if name == "cumprod": + msg = f"operation '{name}' not supported for dtype '{self.dtype}'" + raise TypeError(msg) + + # We may need to strip out trailing NA values + tail: pa.array | None = None + na_mask: pa.array | None = None + pa_array = self._pa_array + np_func = { + "cumsum": np.cumsum, + "cummin": np.minimum.accumulate, + "cummax": np.maximum.accumulate, + }[name] + + if self._hasna: + na_mask = pc.is_null(pa_array) + if pc.all(na_mask) == pa.scalar(True): + return type(self)(pa_array) + if skipna: + if name == "cumsum": + pa_array = pc.fill_null(pa_array, "") + else: + # We can retain the running min/max by forward/backward filling. + pa_array = pc.fill_null_forward(pa_array) + pa_array = pc.fill_null_backward(pa_array) + else: + # When not skipping NA values, the result should be null from + # the first NA value onward. + idx = pc.index(na_mask, True).as_py() + tail = pa.nulls(len(pa_array) - idx, type=pa_array.type) + pa_array = pa_array[:idx] + + # error: Cannot call function of unknown type + pa_result = pa.array(np_func(pa_array), type=pa_array.type) # type: ignore[operator] + + if tail is not None: + pa_result = pa.concat_arrays([pa_result, tail]) + elif na_mask is not None: + pa_result = pc.if_else(na_mask, None, pa_result) + + result = type(self)(pa_result) + return result + def _reduce_pyarrow(self, name: str, *, skipna: bool = True, **kwargs) -> pa.Scalar: """ Return a pyarrow scalar result of performing the reduction operation. diff --git a/pandas/tests/apply/test_str.py b/pandas/tests/apply/test_str.py index c52168ae48ca8..ce71cfec535e4 100644 --- a/pandas/tests/apply/test_str.py +++ b/pandas/tests/apply/test_str.py @@ -4,7 +4,10 @@ import numpy as np import pytest -from pandas.compat import WASM +from pandas.compat import ( + HAS_PYARROW, + WASM, +) from pandas.core.dtypes.common import is_number @@ -163,10 +166,10 @@ def test_agg_cython_table_transform_series(request, series, func, expected): # GH21224 # test transforming functions in # pandas.core.base.SelectionMixin._cython_table (cumprod, cumsum) - if series.dtype == "string" and func == "cumsum": + if series.dtype == "string" and func == "cumsum" and not HAS_PYARROW: request.applymarker( pytest.mark.xfail( - raises=(TypeError, NotImplementedError), + raises=NotImplementedError, reason="TODO(infer_string) cumsum not yet implemented for string", ) ) diff --git a/pandas/tests/extension/base/accumulate.py b/pandas/tests/extension/base/accumulate.py index 9a41a3a582c4a..9a2f186c2a00b 100644 --- a/pandas/tests/extension/base/accumulate.py +++ b/pandas/tests/extension/base/accumulate.py @@ -18,8 +18,9 @@ def _supports_accumulation(self, ser: pd.Series, op_name: str) -> bool: def check_accumulate(self, ser: pd.Series, op_name: str, skipna: bool): try: alt = ser.astype("float64") - except TypeError: - # e.g. Period can't be cast to float64 + except (TypeError, ValueError): + # e.g. Period can't be cast to float64 (TypeError) + # String can't be cast to float64 (ValueError) alt = ser.astype(object) result = getattr(ser, op_name)(skipna=skipna) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index c5f5a65b77eea..4fccf02e08bd6 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -393,13 +393,12 @@ def _supports_accumulation(self, ser: pd.Series, op_name: str) -> bool: # attribute "pyarrow_dtype" pa_type = ser.dtype.pyarrow_dtype # type: ignore[union-attr] - if ( - pa.types.is_string(pa_type) - or pa.types.is_binary(pa_type) - or pa.types.is_decimal(pa_type) - ): + if pa.types.is_binary(pa_type) or pa.types.is_decimal(pa_type): if op_name in ["cumsum", "cumprod", "cummax", "cummin"]: return False + elif pa.types.is_string(pa_type): + if op_name == "cumprod": + return False elif pa.types.is_boolean(pa_type): if op_name in ["cumprod", "cummax", "cummin"]: return False @@ -414,6 +413,12 @@ def _supports_accumulation(self, ser: pd.Series, op_name: str) -> bool: def test_accumulate_series(self, data, all_numeric_accumulations, skipna, request): pa_type = data.dtype.pyarrow_dtype op_name = all_numeric_accumulations + + if pa.types.is_string(pa_type) and op_name in ["cumsum", "cummin", "cummax"]: + # https://github.com/pandas-dev/pandas/pull/60633 + # Doesn't fit test structure, tested in series/test_cumulative.py instead. + return + ser = pd.Series(data) if not self._supports_accumulation(ser, op_name): diff --git a/pandas/tests/extension/test_string.py b/pandas/tests/extension/test_string.py index e19351b2ad058..6ce48e434d329 100644 --- a/pandas/tests/extension/test_string.py +++ b/pandas/tests/extension/test_string.py @@ -24,6 +24,8 @@ from pandas.compat import HAS_PYARROW +from pandas.core.dtypes.base import StorageExtensionDtype + import pandas as pd import pandas._testing as tm from pandas.api.types import is_string_dtype @@ -192,6 +194,14 @@ def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool: and op_name in ("any", "all") ) + def _supports_accumulation(self, ser: pd.Series, op_name: str) -> bool: + assert isinstance(ser.dtype, StorageExtensionDtype) + return ser.dtype.storage == "pyarrow" and op_name in [ + "cummin", + "cummax", + "cumsum", + ] + def _cast_pointwise_result(self, op_name: str, obj, other, pointwise_result): dtype = cast(StringDtype, tm.get_dtype(obj)) if op_name in ["__add__", "__radd__"]: diff --git a/pandas/tests/series/test_cumulative.py b/pandas/tests/series/test_cumulative.py index a9d5486139b46..89882d9d797c5 100644 --- a/pandas/tests/series/test_cumulative.py +++ b/pandas/tests/series/test_cumulative.py @@ -6,6 +6,8 @@ tests.frame.test_cumulative """ +import re + import numpy as np import pytest @@ -227,3 +229,55 @@ def test_cumprod_timedelta(self): ser = pd.Series([pd.Timedelta(days=1), pd.Timedelta(days=3)]) with pytest.raises(TypeError, match="cumprod not supported for Timedelta"): ser.cumprod() + + @pytest.mark.parametrize( + "data, op, skipna, expected_data", + [ + ([], "cumsum", True, []), + ([], "cumsum", False, []), + (["x", "z", "y"], "cumsum", True, ["x", "xz", "xzy"]), + (["x", "z", "y"], "cumsum", False, ["x", "xz", "xzy"]), + (["x", pd.NA, "y"], "cumsum", True, ["x", pd.NA, "xy"]), + (["x", pd.NA, "y"], "cumsum", False, ["x", pd.NA, pd.NA]), + ([pd.NA, "x", "y"], "cumsum", True, [pd.NA, "x", "xy"]), + ([pd.NA, "x", "y"], "cumsum", False, [pd.NA, pd.NA, pd.NA]), + ([pd.NA, pd.NA, pd.NA], "cumsum", True, [pd.NA, pd.NA, pd.NA]), + ([pd.NA, pd.NA, pd.NA], "cumsum", False, [pd.NA, pd.NA, pd.NA]), + ([], "cummin", True, []), + ([], "cummin", False, []), + (["y", "z", "x"], "cummin", True, ["y", "y", "x"]), + (["y", "z", "x"], "cummin", False, ["y", "y", "x"]), + (["y", pd.NA, "x"], "cummin", True, ["y", pd.NA, "x"]), + (["y", pd.NA, "x"], "cummin", False, ["y", pd.NA, pd.NA]), + ([pd.NA, "y", "x"], "cummin", True, [pd.NA, "y", "x"]), + ([pd.NA, "y", "x"], "cummin", False, [pd.NA, pd.NA, pd.NA]), + ([pd.NA, pd.NA, pd.NA], "cummin", True, [pd.NA, pd.NA, pd.NA]), + ([pd.NA, pd.NA, pd.NA], "cummin", False, [pd.NA, pd.NA, pd.NA]), + ([], "cummax", True, []), + ([], "cummax", False, []), + (["x", "z", "y"], "cummax", True, ["x", "z", "z"]), + (["x", "z", "y"], "cummax", False, ["x", "z", "z"]), + (["x", pd.NA, "y"], "cummax", True, ["x", pd.NA, "y"]), + (["x", pd.NA, "y"], "cummax", False, ["x", pd.NA, pd.NA]), + ([pd.NA, "x", "y"], "cummax", True, [pd.NA, "x", "y"]), + ([pd.NA, "x", "y"], "cummax", False, [pd.NA, pd.NA, pd.NA]), + ([pd.NA, pd.NA, pd.NA], "cummax", True, [pd.NA, pd.NA, pd.NA]), + ([pd.NA, pd.NA, pd.NA], "cummax", False, [pd.NA, pd.NA, pd.NA]), + ], + ) + def test_cum_methods_pyarrow_strings( + self, pyarrow_string_dtype, data, op, skipna, expected_data + ): + # https://github.com/pandas-dev/pandas/pull/60633 + ser = pd.Series(data, dtype=pyarrow_string_dtype) + method = getattr(ser, op) + expected = pd.Series(expected_data, dtype=pyarrow_string_dtype) + result = method(skipna=skipna) + tm.assert_series_equal(result, expected) + + def test_cumprod_pyarrow_strings(self, pyarrow_string_dtype, skipna): + # https://github.com/pandas-dev/pandas/pull/60633 + ser = pd.Series(list("xyz"), dtype=pyarrow_string_dtype) + msg = re.escape(f"operation 'cumprod' not supported for dtype '{ser.dtype}'") + with pytest.raises(TypeError, match=msg): + ser.cumprod(skipna=skipna) From fa5c2550e81c3e745eb7948b56adac45454853d5 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 13 Jan 2025 17:56:32 -0800 Subject: [PATCH 210/266] ENH: Expose NoDefault in pandas.api.extensions (#60696) * ENH: Expose NoDefault in pandas.api.extensions * Add entry to whatsnew * Address review comment --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/api/typing/__init__.py | 2 ++ pandas/tests/api/test_api.py | 1 + 3 files changed, 4 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 34df7fc2027a5..1ca0bb9c653a4 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -30,6 +30,7 @@ Other enhancements ^^^^^^^^^^^^^^^^^^ - :class:`pandas.api.typing.FrozenList` is available for typing the outputs of :attr:`MultiIndex.names`, :attr:`MultiIndex.codes` and :attr:`MultiIndex.levels` (:issue:`58237`) - :class:`pandas.api.typing.SASReader` is available for typing the output of :func:`read_sas` (:issue:`55689`) +- :class:`pandas.api.typing.NoDefault` is available for typing ``no_default`` - :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`) - :func:`pandas.merge` now validates the ``how`` parameter input (merge type) (:issue:`59435`) - :func:`read_spss` now supports kwargs to be passed to pyreadstat (:issue:`56356`) diff --git a/pandas/api/typing/__init__.py b/pandas/api/typing/__init__.py index a18a1e9d5cbb7..c1178c72f3edc 100644 --- a/pandas/api/typing/__init__.py +++ b/pandas/api/typing/__init__.py @@ -3,6 +3,7 @@ """ from pandas._libs import NaTType +from pandas._libs.lib import NoDefault from pandas._libs.missing import NAType from pandas.core.groupby import ( @@ -44,6 +45,7 @@ "JsonReader", "NAType", "NaTType", + "NoDefault", "PeriodIndexResamplerGroupby", "Resampler", "Rolling", diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index c1d9f5ea4d25c..4a05259a98087 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -261,6 +261,7 @@ class TestApi(Base): "JsonReader", "NaTType", "NAType", + "NoDefault", "PeriodIndexResamplerGroupby", "Resampler", "Rolling", From 8bc8c0a6119b053e520f5018dc1350863f7277e4 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:34:40 -0500 Subject: [PATCH 211/266] TST(string dtype): Resolve xfail in test_base.py (#60713) --- pandas/core/arrays/string_.py | 5 +++++ pandas/tests/indexes/test_base.py | 9 +++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pandas/core/arrays/string_.py b/pandas/core/arrays/string_.py index 3b881cfd2df2f..623a6a10c75b5 100644 --- a/pandas/core/arrays/string_.py +++ b/pandas/core/arrays/string_.py @@ -533,6 +533,11 @@ def _str_map_nan_semantics( else: return self._str_map_str_or_object(dtype, na_value, arr, f, mask) + def view(self, dtype: Dtype | None = None) -> ArrayLike: + if dtype is not None: + raise TypeError("Cannot change data-type for string array.") + return super().view(dtype=dtype) + # error: Definition of "_concat_same_type" in base class "NDArrayBacked" is # incompatible with definition in base class "ExtensionArray" diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 06df8902f319c..608158d40cf23 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -351,14 +351,11 @@ def test_view_with_args_object_array_raises(self, index): msg = "When changing to a larger dtype" with pytest.raises(ValueError, match=msg): index.view("i8") - elif index.dtype == "str" and not index.dtype.storage == "python": - # TODO(infer_string): Make the errors consistent - with pytest.raises(NotImplementedError, match="i8"): - index.view("i8") else: msg = ( - "Cannot change data-type for array of references.|" - "Cannot change data-type for object array.|" + r"Cannot change data-type for array of references\.|" + r"Cannot change data-type for object array\.|" + r"Cannot change data-type for array of strings\.|" ) with pytest.raises(TypeError, match=msg): index.view("i8") From 817b7069bd9fc014232c066dc79dafbf5463137e Mon Sep 17 00:00:00 2001 From: Tolker-KU <55140581+Tolker-KU@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:00:43 +0100 Subject: [PATCH 212/266] ENH: Format `decimal.Decimal` as full precision strings in `.to_json(...)` (#60698) * Format decimal.Decimal as full precision strings in .to_json(...) * Fix failing tests * Clean up Decimal to utf8 convertion and switch to using PyObject_Format() to suppress scientific notation * Add whatsnew entry --- doc/source/whatsnew/v3.0.0.rst | 1 + .../src/vendored/ujson/python/objToJSON.c | 35 +++++++++++++++++-- .../json/test_json_table_schema_ext_dtype.py | 4 +-- pandas/tests/io/json/test_pandas.py | 7 +--- pandas/tests/io/json/test_ujson.py | 30 ++++++++-------- 5 files changed, 52 insertions(+), 25 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1ca0bb9c653a4..bf1b52d3a0957 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -53,6 +53,7 @@ Other enhancements - :meth:`DataFrame.ewm` now allows ``adjust=False`` when ``times`` is provided (:issue:`54328`) - :meth:`DataFrame.fillna` and :meth:`Series.fillna` can now accept ``value=None``; for non-object dtype the corresponding NA value will be used (:issue:`57723`) - :meth:`DataFrame.pivot_table` and :func:`pivot_table` now allow the passing of keyword arguments to ``aggfunc`` through ``**kwargs`` (:issue:`57884`) +- :meth:`DataFrame.to_json` now encodes ``Decimal`` as strings instead of floats (:issue:`60698`) - :meth:`Series.cummin` and :meth:`Series.cummax` now supports :class:`CategoricalDtype` (:issue:`52335`) - :meth:`Series.plot` now correctly handle the ``ylabel`` parameter for pie charts, allowing for explicit control over the y-axis label (:issue:`58239`) - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) diff --git a/pandas/_libs/src/vendored/ujson/python/objToJSON.c b/pandas/_libs/src/vendored/ujson/python/objToJSON.c index 6b957148f94da..4adc32ba0fed9 100644 --- a/pandas/_libs/src/vendored/ujson/python/objToJSON.c +++ b/pandas/_libs/src/vendored/ujson/python/objToJSON.c @@ -373,6 +373,27 @@ static char *PyTimeToJSON(JSOBJ _obj, JSONTypeContext *tc, size_t *outLen) { return outValue; } +static char *PyDecimalToUTF8Callback(JSOBJ _obj, JSONTypeContext *tc, + size_t *len) { + PyObject *obj = (PyObject *)_obj; + PyObject *format_spec = PyUnicode_FromStringAndSize("f", 1); + PyObject *str = PyObject_Format(obj, format_spec); + Py_DECREF(format_spec); + + if (str == NULL) { + ((JSONObjectEncoder *)tc->encoder)->errorMsg = ""; + return NULL; + } + + GET_TC(tc)->newObj = str; + + Py_ssize_t s_len; + char *outValue = (char *)PyUnicode_AsUTF8AndSize(str, &s_len); + *len = s_len; + + return outValue; +} + //============================================================================= // Numpy array iteration functions //============================================================================= @@ -1467,8 +1488,18 @@ static void Object_beginTypeContext(JSOBJ _obj, JSONTypeContext *tc) { tc->type = JT_UTF8; return; } else if (object_is_decimal_type(obj)) { - pc->doubleValue = PyFloat_AsDouble(obj); - tc->type = JT_DOUBLE; + PyObject *is_nan_py = PyObject_RichCompare(obj, obj, Py_NE); + if (is_nan_py == NULL) { + goto INVALID; + } + int is_nan = (is_nan_py == Py_True); + Py_DECREF(is_nan_py); + if (is_nan) { + tc->type = JT_NULL; + return; + } + pc->PyTypeToUTF8 = PyDecimalToUTF8Callback; + tc->type = JT_UTF8; return; } else if (PyDateTime_Check(obj) || PyDate_Check(obj)) { if (object_is_nat_type(obj)) { diff --git a/pandas/tests/io/json/test_json_table_schema_ext_dtype.py b/pandas/tests/io/json/test_json_table_schema_ext_dtype.py index 8de289afe9ff9..12ae24b064c9d 100644 --- a/pandas/tests/io/json/test_json_table_schema_ext_dtype.py +++ b/pandas/tests/io/json/test_json_table_schema_ext_dtype.py @@ -159,7 +159,7 @@ def test_build_decimal_series(self, dc): expected = OrderedDict( [ ("schema", schema), - ("data", [OrderedDict([("id", 0), ("a", 10.0)])]), + ("data", [OrderedDict([("id", 0), ("a", "10")])]), ] ) @@ -245,7 +245,7 @@ def test_to_json(self, da, dc, sa, ia): [ ("idx", 0), ("A", "2021-10-10T00:00:00.000"), - ("B", 10.0), + ("B", "10"), ("C", "pandas"), ("D", 10), ] diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index ad9dbf7554a8b..59997d52179e6 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -1,6 +1,5 @@ import datetime from datetime import timedelta -from decimal import Decimal from io import StringIO import json import os @@ -2025,12 +2024,8 @@ def test_to_s3(self, s3_public_bucket, s3so): timeout -= 0.1 assert timeout > 0, "Timed out waiting for file to appear on moto" - def test_json_pandas_nulls(self, nulls_fixture, request): + def test_json_pandas_nulls(self, nulls_fixture): # GH 31615 - if isinstance(nulls_fixture, Decimal): - mark = pytest.mark.xfail(reason="not implemented") - request.applymarker(mark) - expected_warning = None msg = ( "The default 'epoch' date format is deprecated and will be removed " diff --git a/pandas/tests/io/json/test_ujson.py b/pandas/tests/io/json/test_ujson.py index 62118f1c82ebb..c5ccc3b3f7184 100644 --- a/pandas/tests/io/json/test_ujson.py +++ b/pandas/tests/io/json/test_ujson.py @@ -57,56 +57,56 @@ def test_encode_decimal(self): sut = decimal.Decimal("1337.1337") encoded = ujson.ujson_dumps(sut, double_precision=15) decoded = ujson.ujson_loads(encoded) - assert decoded == 1337.1337 + assert decoded == "1337.1337" sut = decimal.Decimal("0.95") encoded = ujson.ujson_dumps(sut, double_precision=1) - assert encoded == "1.0" + assert encoded == '"0.95"' decoded = ujson.ujson_loads(encoded) - assert decoded == 1.0 + assert decoded == "0.95" sut = decimal.Decimal("0.94") encoded = ujson.ujson_dumps(sut, double_precision=1) - assert encoded == "0.9" + assert encoded == '"0.94"' decoded = ujson.ujson_loads(encoded) - assert decoded == 0.9 + assert decoded == "0.94" sut = decimal.Decimal("1.95") encoded = ujson.ujson_dumps(sut, double_precision=1) - assert encoded == "2.0" + assert encoded == '"1.95"' decoded = ujson.ujson_loads(encoded) - assert decoded == 2.0 + assert decoded == "1.95" sut = decimal.Decimal("-1.95") encoded = ujson.ujson_dumps(sut, double_precision=1) - assert encoded == "-2.0" + assert encoded == '"-1.95"' decoded = ujson.ujson_loads(encoded) - assert decoded == -2.0 + assert decoded == "-1.95" sut = decimal.Decimal("0.995") encoded = ujson.ujson_dumps(sut, double_precision=2) - assert encoded == "1.0" + assert encoded == '"0.995"' decoded = ujson.ujson_loads(encoded) - assert decoded == 1.0 + assert decoded == "0.995" sut = decimal.Decimal("0.9995") encoded = ujson.ujson_dumps(sut, double_precision=3) - assert encoded == "1.0" + assert encoded == '"0.9995"' decoded = ujson.ujson_loads(encoded) - assert decoded == 1.0 + assert decoded == "0.9995" sut = decimal.Decimal("0.99999999999999944") encoded = ujson.ujson_dumps(sut, double_precision=15) - assert encoded == "1.0" + assert encoded == '"0.99999999999999944"' decoded = ujson.ujson_loads(encoded) - assert decoded == 1.0 + assert decoded == "0.99999999999999944" @pytest.mark.parametrize("ensure_ascii", [True, False]) def test_encode_string_conversion(self, ensure_ascii): From 5c9b6718dea589be6fafab04adbd22dd0550a061 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Wed, 15 Jan 2025 08:19:37 -0800 Subject: [PATCH 213/266] =?UTF-8?q?BUG:=20Fix=20DataFrame=20binary=20arith?= =?UTF-8?q?matic=20operation=20handling=20of=20unaligned=20=E2=80=A6=20(#6?= =?UTF-8?q?0538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * BUG: Fix DataFrame binary arithmatic operation handling of unaligned MultiIndex columns * Address review comment --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/frame.py | 17 +++++++++++++++++ pandas/tests/frame/test_arithmetic.py | 25 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index bf1b52d3a0957..b3df52fe1758a 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -689,6 +689,7 @@ MultiIndex - :meth:`DataFrame.melt` would not accept multiple names in ``var_name`` when the columns were a :class:`MultiIndex` (:issue:`58033`) - :meth:`MultiIndex.insert` would not insert NA value correctly at unified location of index -1 (:issue:`59003`) - :func:`MultiIndex.get_level_values` accessing a :class:`DatetimeIndex` does not carry the frequency attribute along (:issue:`58327`, :issue:`57949`) +- Bug in :class:`DataFrame` arithmetic operations in case of unaligned MultiIndex columns (:issue:`60498`) - I/O diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 851bc1ce4075c..ffffaeba4196e 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -7967,6 +7967,16 @@ def _arith_method_with_reindex(self, right: DataFrame, op) -> DataFrame: new_left = left if lcol_indexer is None else left.iloc[:, lcol_indexer] new_right = right if rcol_indexer is None else right.iloc[:, rcol_indexer] + + # GH#60498 For MultiIndex column alignment + if isinstance(cols, MultiIndex): + # When overwriting column names, make a shallow copy so as to not modify + # the input DFs + new_left = new_left.copy(deep=False) + new_right = new_right.copy(deep=False) + new_left.columns = cols + new_right.columns = cols + result = op(new_left, new_right) # Do the join on the columns instead of using left._align_for_op @@ -7997,6 +8007,13 @@ def _should_reindex_frame_op(self, right, op, axis: int, fill_value, level) -> b if not isinstance(right, DataFrame): return False + if ( + isinstance(self.columns, MultiIndex) + or isinstance(right.columns, MultiIndex) + ) and not self.columns.equals(right.columns): + # GH#60498 Reindex if MultiIndexe columns are not matching + return True + if fill_value is None and level is None and axis == 1: # TODO: any other cases we should handle here? diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 6b61fe8b05219..7ada1884feb90 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -2033,6 +2033,31 @@ def test_arithmetic_multiindex_align(): tm.assert_frame_equal(result, expected) +def test_arithmetic_multiindex_column_align(): + # GH#60498 + df1 = DataFrame( + data=100, + columns=MultiIndex.from_product( + [["1A", "1B"], ["2A", "2B"]], names=["Lev1", "Lev2"] + ), + index=["C1", "C2"], + ) + df2 = DataFrame( + data=np.array([[0.1, 0.25], [0.2, 0.45]]), + columns=MultiIndex.from_product([["1A", "1B"]], names=["Lev1"]), + index=["C1", "C2"], + ) + expected = DataFrame( + data=np.array([[10.0, 10.0, 25.0, 25.0], [20.0, 20.0, 45.0, 45.0]]), + columns=MultiIndex.from_product( + [["1A", "1B"], ["2A", "2B"]], names=["Lev1", "Lev2"] + ), + index=["C1", "C2"], + ) + result = df1 * df2 + tm.assert_frame_equal(result, expected) + + def test_bool_frame_mult_float(): # GH 18549 df = DataFrame(True, list("ab"), list("cd")) From a15a4b5e6f0397906f619ce8888670eadcf3af55 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Thu, 16 Jan 2025 05:30:58 +0530 Subject: [PATCH 214/266] DOC: fix PR01,SA01,ES01 for pandas.RangeIndex.from_range (#60720) --- ci/code_checks.sh | 1 - pandas/core/indexes/range.py | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 56cb22741b9a3..ec6dba05b2b0e 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -72,7 +72,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Series.dt PR01" `# Accessors are implemented as classes, but we do not document the Parameters section` \ -i "pandas.Period.freq GL08" \ -i "pandas.Period.ordinal GL08" \ - -i "pandas.RangeIndex.from_range PR01,SA01" \ -i "pandas.Timedelta.max PR02" \ -i "pandas.Timedelta.min PR02" \ -i "pandas.Timedelta.resolution PR02" \ diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index 935762d0455c5..2db50bbbdfa37 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -190,10 +190,31 @@ def from_range(cls, data: range, name=None, dtype: Dtype | None = None) -> Self: """ Create :class:`pandas.RangeIndex` from a ``range`` object. + This method provides a way to create a :class:`pandas.RangeIndex` directly + from a Python ``range`` object. The resulting :class:`RangeIndex` will have + the same start, stop, and step values as the input ``range`` object. + It is particularly useful for constructing indices in an efficient and + memory-friendly manner. + + Parameters + ---------- + data : range + The range object to be converted into a RangeIndex. + name : str, default None + Name to be stored in the index. + dtype : Dtype or None + Data type for the RangeIndex. If None, the default integer type will + be used. + Returns ------- RangeIndex + See Also + -------- + RangeIndex : Immutable Index implementing a monotonic integer range. + Index : Immutable sequence used for indexing and alignment. + Examples -------- >>> pd.RangeIndex.from_range(range(5)) From fb6c4e33c45938d7675d4c9a132324cd08df2f3c Mon Sep 17 00:00:00 2001 From: William Ayd Date: Thu, 16 Jan 2025 12:11:45 -0500 Subject: [PATCH 215/266] Use const char* for JSON key name (#60721) --- .../pandas/vendored/ujson/lib/ultrajson.h | 4 +- .../src/vendored/ujson/lib/ultrajsonenc.c | 2 +- .../src/vendored/ujson/python/objToJSON.c | 135 ++++++++---------- pandas/tests/io/json/test_compression.py | 1 + pandas/tests/io/json/test_pandas.py | 2 + 5 files changed, 62 insertions(+), 82 deletions(-) diff --git a/pandas/_libs/include/pandas/vendored/ujson/lib/ultrajson.h b/pandas/_libs/include/pandas/vendored/ujson/lib/ultrajson.h index 0d62bb0ba915c..51fdbc50bba57 100644 --- a/pandas/_libs/include/pandas/vendored/ujson/lib/ultrajson.h +++ b/pandas/_libs/include/pandas/vendored/ujson/lib/ultrajson.h @@ -170,8 +170,8 @@ typedef void (*JSPFN_ITERBEGIN)(JSOBJ obj, JSONTypeContext *tc); typedef int (*JSPFN_ITERNEXT)(JSOBJ obj, JSONTypeContext *tc); typedef void (*JSPFN_ITEREND)(JSOBJ obj, JSONTypeContext *tc); typedef JSOBJ (*JSPFN_ITERGETVALUE)(JSOBJ obj, JSONTypeContext *tc); -typedef char *(*JSPFN_ITERGETNAME)(JSOBJ obj, JSONTypeContext *tc, - size_t *outLen); +typedef const char *(*JSPFN_ITERGETNAME)(JSOBJ obj, JSONTypeContext *tc, + size_t *outLen); typedef void *(*JSPFN_MALLOC)(size_t size); typedef void (*JSPFN_FREE)(void *pptr); typedef void *(*JSPFN_REALLOC)(void *base, size_t size); diff --git a/pandas/_libs/src/vendored/ujson/lib/ultrajsonenc.c b/pandas/_libs/src/vendored/ujson/lib/ultrajsonenc.c index c8d8b5ab6bd6e..1564ecb64b01d 100644 --- a/pandas/_libs/src/vendored/ujson/lib/ultrajsonenc.c +++ b/pandas/_libs/src/vendored/ujson/lib/ultrajsonenc.c @@ -920,7 +920,7 @@ Perhaps implement recursion detection */ void encode(JSOBJ obj, JSONObjectEncoder *enc, const char *name, size_t cbName) { const char *value; - char *objName; + const char *objName; int count; JSOBJ iterObj; size_t szlen; diff --git a/pandas/_libs/src/vendored/ujson/python/objToJSON.c b/pandas/_libs/src/vendored/ujson/python/objToJSON.c index 4adc32ba0fed9..8342dbcd1763d 100644 --- a/pandas/_libs/src/vendored/ujson/python/objToJSON.c +++ b/pandas/_libs/src/vendored/ujson/python/objToJSON.c @@ -53,8 +53,8 @@ Numeric decoder derived from TCL library npy_int64 get_nat(void) { return NPY_MIN_INT64; } -typedef char *(*PFN_PyTypeToUTF8)(JSOBJ obj, JSONTypeContext *ti, - size_t *_outLen); +typedef const char *(*PFN_PyTypeToUTF8)(JSOBJ obj, JSONTypeContext *ti, + size_t *_outLen); int object_is_decimal_type(PyObject *obj); int object_is_dataframe_type(PyObject *obj); @@ -106,7 +106,7 @@ typedef struct __TypeContext { double doubleValue; JSINT64 longValue; - char *cStr; + const char *cStr; NpyArrContext *npyarr; PdBlockContext *pdblock; int transpose; @@ -301,14 +301,15 @@ static npy_float64 total_seconds(PyObject *td) { return double_val; } -static char *PyBytesToUTF8(JSOBJ _obj, JSONTypeContext *Py_UNUSED(tc), - size_t *_outLen) { +static const char *PyBytesToUTF8(JSOBJ _obj, JSONTypeContext *Py_UNUSED(tc), + size_t *_outLen) { PyObject *obj = (PyObject *)_obj; *_outLen = PyBytes_GET_SIZE(obj); return PyBytes_AS_STRING(obj); } -static char *PyUnicodeToUTF8(JSOBJ _obj, JSONTypeContext *tc, size_t *_outLen) { +static const char *PyUnicodeToUTF8(JSOBJ _obj, JSONTypeContext *tc, + size_t *_outLen) { char *encoded = (char *)PyUnicode_AsUTF8AndSize(_obj, (Py_ssize_t *)_outLen); if (encoded == NULL) { /* Something went wrong. @@ -321,8 +322,8 @@ static char *PyUnicodeToUTF8(JSOBJ _obj, JSONTypeContext *tc, size_t *_outLen) { } /* JSON callback. returns a char* and mutates the pointer to *len */ -static char *NpyDateTimeToIsoCallback(JSOBJ Py_UNUSED(unused), - JSONTypeContext *tc, size_t *len) { +static const char *NpyDateTimeToIsoCallback(JSOBJ Py_UNUSED(unused), + JSONTypeContext *tc, size_t *len) { NPY_DATETIMEUNIT base = ((PyObjectEncoder *)tc->encoder)->datetimeUnit; NPY_DATETIMEUNIT valueUnit = ((PyObjectEncoder *)tc->encoder)->valueUnit; GET_TC(tc)->cStr = int64ToIso(GET_TC(tc)->longValue, valueUnit, base, len); @@ -330,15 +331,15 @@ static char *NpyDateTimeToIsoCallback(JSOBJ Py_UNUSED(unused), } /* JSON callback. returns a char* and mutates the pointer to *len */ -static char *NpyTimeDeltaToIsoCallback(JSOBJ Py_UNUSED(unused), - JSONTypeContext *tc, size_t *len) { +static const char *NpyTimeDeltaToIsoCallback(JSOBJ Py_UNUSED(unused), + JSONTypeContext *tc, size_t *len) { GET_TC(tc)->cStr = int64ToIsoDuration(GET_TC(tc)->longValue, len); return GET_TC(tc)->cStr; } /* JSON callback */ -static char *PyDateTimeToIsoCallback(JSOBJ obj, JSONTypeContext *tc, - size_t *len) { +static const char *PyDateTimeToIsoCallback(JSOBJ obj, JSONTypeContext *tc, + size_t *len) { if (!PyDate_Check(obj) && !PyDateTime_Check(obj)) { PyErr_SetString(PyExc_TypeError, "Expected date or datetime object"); ((JSONObjectEncoder *)tc->encoder)->errorMsg = ""; @@ -349,7 +350,8 @@ static char *PyDateTimeToIsoCallback(JSOBJ obj, JSONTypeContext *tc, return PyDateTimeToIso(obj, base, len); } -static char *PyTimeToJSON(JSOBJ _obj, JSONTypeContext *tc, size_t *outLen) { +static const char *PyTimeToJSON(JSOBJ _obj, JSONTypeContext *tc, + size_t *outLen) { PyObject *obj = (PyObject *)_obj; PyObject *str = PyObject_CallMethod(obj, "isoformat", NULL); if (str == NULL) { @@ -373,8 +375,8 @@ static char *PyTimeToJSON(JSOBJ _obj, JSONTypeContext *tc, size_t *outLen) { return outValue; } -static char *PyDecimalToUTF8Callback(JSOBJ _obj, JSONTypeContext *tc, - size_t *len) { +static const char *PyDecimalToUTF8Callback(JSOBJ _obj, JSONTypeContext *tc, + size_t *len) { PyObject *obj = (PyObject *)_obj; PyObject *format_spec = PyUnicode_FromStringAndSize("f", 1); PyObject *str = PyObject_Format(obj, format_spec); @@ -558,10 +560,10 @@ static JSOBJ NpyArr_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *NpyArr_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *NpyArr_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, + size_t *outLen) { NpyArrContext *npyarr = GET_TC(tc)->npyarr; - char *cStr; + const char *cStr; if (GET_TC(tc)->iterNext == NpyArr_iterNextItem) { const npy_intp idx = npyarr->index[npyarr->stridedim] - 1; @@ -609,11 +611,11 @@ static int PdBlock_iterNextItem(JSOBJ obj, JSONTypeContext *tc) { return NpyArr_iterNextItem(obj, tc); } -static char *PdBlock_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *PdBlock_iterGetName(JSOBJ Py_UNUSED(obj), + JSONTypeContext *tc, size_t *outLen) { PdBlockContext *blkCtxt = GET_TC(tc)->pdblock; NpyArrContext *npyarr = blkCtxt->npyCtxts[0]; - char *cStr; + const char *cStr; if (GET_TC(tc)->iterNext == PdBlock_iterNextItem) { const npy_intp idx = blkCtxt->colIdx - 1; @@ -631,12 +633,12 @@ static char *PdBlock_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, return cStr; } -static char *PdBlock_iterGetName_Transpose(JSOBJ Py_UNUSED(obj), - JSONTypeContext *tc, - size_t *outLen) { +static const char *PdBlock_iterGetName_Transpose(JSOBJ Py_UNUSED(obj), + JSONTypeContext *tc, + size_t *outLen) { PdBlockContext *blkCtxt = GET_TC(tc)->pdblock; NpyArrContext *npyarr = blkCtxt->npyCtxts[blkCtxt->colIdx]; - char *cStr; + const char *cStr; if (GET_TC(tc)->iterNext == NpyArr_iterNextItem) { const npy_intp idx = npyarr->index[npyarr->stridedim] - 1; @@ -817,9 +819,9 @@ static JSOBJ Tuple_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *Tuple_iterGetName(JSOBJ Py_UNUSED(obj), - JSONTypeContext *Py_UNUSED(tc), - size_t *Py_UNUSED(outLen)) { +static const char *Tuple_iterGetName(JSOBJ Py_UNUSED(obj), + JSONTypeContext *Py_UNUSED(tc), + size_t *Py_UNUSED(outLen)) { return NULL; } @@ -864,9 +866,9 @@ static JSOBJ Set_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *Set_iterGetName(JSOBJ Py_UNUSED(obj), - JSONTypeContext *Py_UNUSED(tc), - size_t *Py_UNUSED(outLen)) { +static const char *Set_iterGetName(JSOBJ Py_UNUSED(obj), + JSONTypeContext *Py_UNUSED(tc), + size_t *Py_UNUSED(outLen)) { return NULL; } @@ -962,8 +964,8 @@ static JSOBJ Dir_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *Dir_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *Dir_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, + size_t *outLen) { *outLen = PyBytes_GET_SIZE(GET_TC(tc)->itemName); return PyBytes_AS_STRING(GET_TC(tc)->itemName); } @@ -994,9 +996,9 @@ static JSOBJ List_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *List_iterGetName(JSOBJ Py_UNUSED(obj), - JSONTypeContext *Py_UNUSED(tc), - size_t *Py_UNUSED(outLen)) { +static const char *List_iterGetName(JSOBJ Py_UNUSED(obj), + JSONTypeContext *Py_UNUSED(tc), + size_t *Py_UNUSED(outLen)) { return NULL; } @@ -1005,24 +1007,16 @@ static char *List_iterGetName(JSOBJ Py_UNUSED(obj), //============================================================================= static void Index_iterBegin(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { GET_TC(tc)->index = 0; - GET_TC(tc)->cStr = PyObject_Malloc(20); - if (!GET_TC(tc)->cStr) { - PyErr_NoMemory(); - } } static int Index_iterNext(JSOBJ obj, JSONTypeContext *tc) { - if (!GET_TC(tc)->cStr) { - return 0; - } - const Py_ssize_t index = GET_TC(tc)->index; Py_XDECREF(GET_TC(tc)->itemValue); if (index == 0) { - memcpy(GET_TC(tc)->cStr, "name", 5); + GET_TC(tc)->cStr = "name"; GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "name"); } else if (index == 1) { - memcpy(GET_TC(tc)->cStr, "data", 5); + GET_TC(tc)->cStr = "data"; GET_TC(tc)->itemValue = get_values(obj); if (!GET_TC(tc)->itemValue) { return 0; @@ -1042,8 +1036,8 @@ static JSOBJ Index_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *Index_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *Index_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, + size_t *outLen) { *outLen = strlen(GET_TC(tc)->cStr); return GET_TC(tc)->cStr; } @@ -1054,28 +1048,20 @@ static char *Index_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, static void Series_iterBegin(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { PyObjectEncoder *enc = (PyObjectEncoder *)tc->encoder; GET_TC(tc)->index = 0; - GET_TC(tc)->cStr = PyObject_Malloc(20); enc->outputFormat = VALUES; // for contained series - if (!GET_TC(tc)->cStr) { - PyErr_NoMemory(); - } } static int Series_iterNext(JSOBJ obj, JSONTypeContext *tc) { - if (!GET_TC(tc)->cStr) { - return 0; - } - const Py_ssize_t index = GET_TC(tc)->index; Py_XDECREF(GET_TC(tc)->itemValue); if (index == 0) { - memcpy(GET_TC(tc)->cStr, "name", 5); + GET_TC(tc)->cStr = "name"; GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "name"); } else if (index == 1) { - memcpy(GET_TC(tc)->cStr, "index", 6); + GET_TC(tc)->cStr = "index"; GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "index"); } else if (index == 2) { - memcpy(GET_TC(tc)->cStr, "data", 5); + GET_TC(tc)->cStr = "data"; GET_TC(tc)->itemValue = get_values(obj); if (!GET_TC(tc)->itemValue) { return 0; @@ -1097,8 +1083,8 @@ static JSOBJ Series_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *Series_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *Series_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, + size_t *outLen) { *outLen = strlen(GET_TC(tc)->cStr); return GET_TC(tc)->cStr; } @@ -1109,28 +1095,20 @@ static char *Series_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, static void DataFrame_iterBegin(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { PyObjectEncoder *enc = (PyObjectEncoder *)tc->encoder; GET_TC(tc)->index = 0; - GET_TC(tc)->cStr = PyObject_Malloc(20); enc->outputFormat = VALUES; // for contained series & index - if (!GET_TC(tc)->cStr) { - PyErr_NoMemory(); - } } static int DataFrame_iterNext(JSOBJ obj, JSONTypeContext *tc) { - if (!GET_TC(tc)->cStr) { - return 0; - } - const Py_ssize_t index = GET_TC(tc)->index; Py_XDECREF(GET_TC(tc)->itemValue); if (index == 0) { - memcpy(GET_TC(tc)->cStr, "columns", 8); + GET_TC(tc)->cStr = "columns"; GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "columns"); } else if (index == 1) { - memcpy(GET_TC(tc)->cStr, "index", 6); + GET_TC(tc)->cStr = "index"; GET_TC(tc)->itemValue = PyObject_GetAttrString(obj, "index"); } else if (index == 2) { - memcpy(GET_TC(tc)->cStr, "data", 5); + GET_TC(tc)->cStr = "data"; Py_INCREF(obj); GET_TC(tc)->itemValue = obj; } else { @@ -1150,8 +1128,8 @@ static JSOBJ DataFrame_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *DataFrame_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *DataFrame_iterGetName(JSOBJ Py_UNUSED(obj), + JSONTypeContext *tc, size_t *outLen) { *outLen = strlen(GET_TC(tc)->cStr); return GET_TC(tc)->cStr; } @@ -1201,8 +1179,8 @@ static JSOBJ Dict_iterGetValue(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { return GET_TC(tc)->itemValue; } -static char *Dict_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, - size_t *outLen) { +static const char *Dict_iterGetName(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc, + size_t *outLen) { *outLen = PyBytes_GET_SIZE(GET_TC(tc)->itemName); return PyBytes_AS_STRING(GET_TC(tc)->itemName); } @@ -1902,7 +1880,6 @@ static void Object_endTypeContext(JSOBJ Py_UNUSED(obj), JSONTypeContext *tc) { GET_TC(tc)->rowLabels = NULL; NpyArr_freeLabels(GET_TC(tc)->columnLabels, GET_TC(tc)->columnLabelsLen); GET_TC(tc)->columnLabels = NULL; - PyObject_Free(GET_TC(tc)->cStr); GET_TC(tc)->cStr = NULL; PyObject_Free(tc->prv); tc->prv = NULL; @@ -1953,8 +1930,8 @@ static JSOBJ Object_iterGetValue(JSOBJ obj, JSONTypeContext *tc) { return GET_TC(tc)->iterGetValue(obj, tc); } -static char *Object_iterGetName(JSOBJ obj, JSONTypeContext *tc, - size_t *outLen) { +static const char *Object_iterGetName(JSOBJ obj, JSONTypeContext *tc, + size_t *outLen) { return GET_TC(tc)->iterGetName(obj, tc, outLen); } diff --git a/pandas/tests/io/json/test_compression.py b/pandas/tests/io/json/test_compression.py index ff7d34c85c015..953a9246da1cd 100644 --- a/pandas/tests/io/json/test_compression.py +++ b/pandas/tests/io/json/test_compression.py @@ -41,6 +41,7 @@ def test_read_zipped_json(datapath): @td.skip_if_not_us_locale @pytest.mark.single_cpu +@pytest.mark.network def test_with_s3_url(compression, s3_public_bucket, s3so): # Bucket created in tests/io/conftest.py df = pd.read_json(StringIO('{"a": [1, 2, 3], "b": [4, 5, 6]}')) diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index 59997d52179e6..5dc1272880c9b 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -1412,6 +1412,7 @@ def test_read_inline_jsonl(self): tm.assert_frame_equal(result, expected) @pytest.mark.single_cpu + @pytest.mark.network @td.skip_if_not_us_locale def test_read_s3_jsonl(self, s3_public_bucket_with_data, s3so): # GH17200 @@ -2011,6 +2012,7 @@ def test_json_multiindex(self): assert result == expected @pytest.mark.single_cpu + @pytest.mark.network def test_to_s3(self, s3_public_bucket, s3so): # GH 28375 mock_bucket_name, target_file = s3_public_bucket.name, "test.json" From 8ea7c5609b537a864626f415fecda17537e6748d Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Thu, 16 Jan 2025 16:22:09 -0500 Subject: [PATCH 216/266] DOC: fix PR07,SA01 for pandas.arrays.ArrowExtensionArray (#60724) --- ci/code_checks.sh | 1 - pandas/core/arrays/arrow/array.py | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index ec6dba05b2b0e..948d8bee8ba5b 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -79,7 +79,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.min PR02" \ -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ - -i "pandas.arrays.ArrowExtensionArray PR07,SA01" \ -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 900548a239c8e..5c32b05868383 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -258,6 +258,7 @@ class ArrowExtensionArray( Parameters ---------- values : pyarrow.Array or pyarrow.ChunkedArray + The input data to initialize the ArrowExtensionArray. Attributes ---------- @@ -271,6 +272,12 @@ class ArrowExtensionArray( ------- ArrowExtensionArray + See Also + -------- + array : Create a Pandas array with a specified dtype. + DataFrame.to_feather : Write a DataFrame to the binary Feather format. + read_feather : Load a feather-format object from the file path. + Notes ----- Most methods are implemented using `pyarrow compute functions. `__ From 50767f803ff5e98be5c569fe442b670b1ffe5180 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Thu, 16 Jan 2025 18:01:22 -0800 Subject: [PATCH 217/266] DOC: Update doc for newly added groupby method kurt (#60725) --- doc/source/reference/groupby.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/reference/groupby.rst b/doc/source/reference/groupby.rst index 3b02ffe20c10e..fc180c8161a7e 100644 --- a/doc/source/reference/groupby.rst +++ b/doc/source/reference/groupby.rst @@ -104,6 +104,7 @@ Function application DataFrameGroupBy.shift DataFrameGroupBy.size DataFrameGroupBy.skew + DataFrameGroupBy.kurt DataFrameGroupBy.std DataFrameGroupBy.sum DataFrameGroupBy.var @@ -159,6 +160,7 @@ Function application SeriesGroupBy.shift SeriesGroupBy.size SeriesGroupBy.skew + SeriesGroupBy.kurt SeriesGroupBy.std SeriesGroupBy.sum SeriesGroupBy.var From 72fd708761f1598f1a8ce9b693529b81fd8ca252 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Thu, 16 Jan 2025 18:10:41 -0800 Subject: [PATCH 218/266] ENH: Add first and last aggregations to Rolling and Expanding (#60579) * ENH: Add first and last aggregations to Rolling and Expanding * Update reference doc * Set 'See Also' section in doc * Fix docstring * Retry fixing docstring * Fix missing period in docstring * Another missing period --- doc/source/reference/window.rst | 4 + doc/source/whatsnew/v3.0.0.rst | 1 + pandas/_libs/window/aggregations.pyi | 12 +++ pandas/_libs/window/aggregations.pyx | 83 +++++++++++++++ pandas/core/window/expanding.py | 72 +++++++++++++ pandas/core/window/rolling.py | 88 +++++++++++++++ .../tests/window/test_cython_aggregations.py | 2 + pandas/tests/window/test_expanding.py | 100 ++++++++++++++++++ pandas/tests/window/test_groupby.py | 4 +- pandas/tests/window/test_rolling.py | 76 +++++++++++++ pandas/tests/window/test_rolling_functions.py | 4 + pandas/tests/window/test_timeseries_window.py | 38 +++++++ 12 files changed, 483 insertions(+), 1 deletion(-) diff --git a/doc/source/reference/window.rst b/doc/source/reference/window.rst index fb89fd2a5ffb2..2aeb57faac112 100644 --- a/doc/source/reference/window.rst +++ b/doc/source/reference/window.rst @@ -30,6 +30,8 @@ Rolling window functions Rolling.std Rolling.min Rolling.max + Rolling.first + Rolling.last Rolling.corr Rolling.cov Rolling.skew @@ -72,6 +74,8 @@ Expanding window functions Expanding.std Expanding.min Expanding.max + Expanding.first + Expanding.last Expanding.corr Expanding.cov Expanding.skew diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index b3df52fe1758a..1e33971acac1a 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -57,6 +57,7 @@ Other enhancements - :meth:`Series.cummin` and :meth:`Series.cummax` now supports :class:`CategoricalDtype` (:issue:`52335`) - :meth:`Series.plot` now correctly handle the ``ylabel`` parameter for pie charts, allowing for explicit control over the y-axis label (:issue:`58239`) - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) +- :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`) - :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`) - :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`) - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) diff --git a/pandas/_libs/window/aggregations.pyi b/pandas/_libs/window/aggregations.pyi index a6cfbec9b15b9..ee735761e3dc6 100644 --- a/pandas/_libs/window/aggregations.pyi +++ b/pandas/_libs/window/aggregations.pyi @@ -60,6 +60,18 @@ def roll_min( end: np.ndarray, # np.ndarray[np.int64] minp: int, # int64_t ) -> np.ndarray: ... # np.ndarray[float] +def roll_first( + values: np.ndarray, # np.ndarray[np.float64] + start: np.ndarray, # np.ndarray[np.int64] + end: np.ndarray, # np.ndarray[np.int64] + minp: int, # int64_t +) -> np.ndarray: ... # np.ndarray[float] +def roll_last( + values: np.ndarray, # np.ndarray[np.float64] + start: np.ndarray, # np.ndarray[np.int64] + end: np.ndarray, # np.ndarray[np.int64] + minp: int, # int64_t +) -> np.ndarray: ... # np.ndarray[float] def roll_quantile( values: np.ndarray, # const float64_t[:] start: np.ndarray, # np.ndarray[np.int64] diff --git a/pandas/_libs/window/aggregations.pyx b/pandas/_libs/window/aggregations.pyx index 5b9ee095d4643..d33c840371d2a 100644 --- a/pandas/_libs/window/aggregations.pyx +++ b/pandas/_libs/window/aggregations.pyx @@ -1133,6 +1133,89 @@ cdef _roll_min_max(ndarray[float64_t] values, return output +# ---------------------------------------------------------------------- +# Rolling first, last + + +def roll_first(const float64_t[:] values, ndarray[int64_t] start, + ndarray[int64_t] end, int64_t minp) -> np.ndarray: + return _roll_first_last(values, start, end, minp, is_first=1) + + +def roll_last(const float64_t[:] values, ndarray[int64_t] start, + ndarray[int64_t] end, int64_t minp) -> np.ndarray: + return _roll_first_last(values, start, end, minp, is_first=0) + + +cdef _roll_first_last(const float64_t[:] values, ndarray[int64_t] start, + ndarray[int64_t] end, int64_t minp, bint is_first): + cdef: + Py_ssize_t i, j, fl_idx + bint is_monotonic_increasing_bounds + int64_t nobs = 0, N = len(start), s, e + float64_t val, res + ndarray[float64_t] output + + is_monotonic_increasing_bounds = is_monotonic_increasing_start_end_bounds( + start, end + ) + + output = np.empty(N, dtype=np.float64) + + if (end - start).max() == 0: + output[:] = NaN + return output + + with nogil: + for i in range(0, N): + s = start[i] + e = end[i] + + if i == 0 or not is_monotonic_increasing_bounds or s >= end[i - 1]: + fl_idx = -1 + nobs = 0 + for j in range(s, e): + val = values[j] + if val == val: + if not is_first or fl_idx < s: + fl_idx = j + nobs += 1 + else: + # handle deletes + for j in range(start[i - 1], s): + val = values[j] + if val == val: + nobs -= 1 + + # update fl_idx if out of range, if first + if is_first and fl_idx < s: + fl_idx = -1 + for j in range(s, end[i - 1]): + val = values[j] + if val == val: + fl_idx = j + break + + # handle adds + for j in range(end[i - 1], e): + val = values[j] + if val == val: + if not is_first or fl_idx < s: + fl_idx = j + nobs += 1 + + if nobs >= minp and fl_idx >= s: + res = values[fl_idx] + else: + res = NaN + + output[i] = res + + if not is_monotonic_increasing_bounds: + nobs = 0 + + return output + cdef enum InterpolationType: LINEAR, diff --git a/pandas/core/window/expanding.py b/pandas/core/window/expanding.py index 6a7d0329ab6da..81c89e1ef5428 100644 --- a/pandas/core/window/expanding.py +++ b/pandas/core/window/expanding.py @@ -723,6 +723,78 @@ def skew(self, numeric_only: bool = False): def kurt(self, numeric_only: bool = False): return super().kurt(numeric_only=numeric_only) + @doc( + template_header, + create_section_header("Parameters"), + kwargs_numeric_only, + create_section_header("Returns"), + template_returns, + create_section_header("See Also"), + dedent( + """ + GroupBy.first : Similar method for GroupBy objects. + Expanding.last : Method to get the last element in each window.\n + """ + ).replace("\n", "", 1), + create_section_header("Examples"), + dedent( + """ + The example below will show an expanding calculation with a window size of + three. + + >>> s = pd.Series(range(5)) + >>> s.expanding(3).first() + 0 NaN + 1 NaN + 2 0.0 + 3 0.0 + 4 0.0 + dtype: float64 + """ + ).replace("\n", "", 1), + window_method="expanding", + aggregation_description="First (left-most) element of the window", + agg_method="first", + ) + def first(self, numeric_only: bool = False): + return super().first(numeric_only=numeric_only) + + @doc( + template_header, + create_section_header("Parameters"), + kwargs_numeric_only, + create_section_header("Returns"), + template_returns, + create_section_header("See Also"), + dedent( + """ + GroupBy.last : Similar method for GroupBy objects. + Expanding.first : Method to get the first element in each window.\n + """ + ).replace("\n", "", 1), + create_section_header("Examples"), + dedent( + """ + The example below will show an expanding calculation with a window size of + three. + + >>> s = pd.Series(range(5)) + >>> s.expanding(3).last() + 0 NaN + 1 NaN + 2 2.0 + 3 3.0 + 4 4.0 + dtype: float64 + """ + ).replace("\n", "", 1), + window_method="expanding", + aggregation_description="Last (right-most) element of the window", + agg_method="last", + ) + def last(self, numeric_only: bool = False): + return super().last(numeric_only=numeric_only) + @doc( template_header, create_section_header("Parameters"), diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 90c3cff975ff0..631ab15464942 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -1740,6 +1740,22 @@ def kurt(self, numeric_only: bool = False): numeric_only=numeric_only, ) + def first(self, numeric_only: bool = False): + window_func = window_aggregations.roll_first + return self._apply( + window_func, + name="first", + numeric_only=numeric_only, + ) + + def last(self, numeric_only: bool = False): + window_func = window_aggregations.roll_last + return self._apply( + window_func, + name="last", + numeric_only=numeric_only, + ) + def quantile( self, q: float, @@ -2622,6 +2638,78 @@ def sem(self, ddof: int = 1, numeric_only: bool = False): def kurt(self, numeric_only: bool = False): return super().kurt(numeric_only=numeric_only) + @doc( + template_header, + create_section_header("Parameters"), + kwargs_numeric_only, + create_section_header("Returns"), + template_returns, + create_section_header("See Also"), + dedent( + """ + GroupBy.first : Similar method for GroupBy objects. + Rolling.last : Method to get the last element in each window.\n + """ + ).replace("\n", "", 1), + create_section_header("Examples"), + dedent( + """ + The example below will show a rolling calculation with a window size of + three. + + >>> s = pd.Series(range(5)) + >>> s.rolling(3).first() + 0 NaN + 1 NaN + 2 0.0 + 3 1.0 + 4 2.0 + dtype: float64 + """ + ).replace("\n", "", 1), + window_method="rolling", + aggregation_description="First (left-most) element of the window", + agg_method="first", + ) + def first(self, numeric_only: bool = False): + return super().first(numeric_only=numeric_only) + + @doc( + template_header, + create_section_header("Parameters"), + kwargs_numeric_only, + create_section_header("Returns"), + template_returns, + create_section_header("See Also"), + dedent( + """ + GroupBy.last : Similar method for GroupBy objects. + Rolling.first : Method to get the first element in each window.\n + """ + ).replace("\n", "", 1), + create_section_header("Examples"), + dedent( + """ + The example below will show a rolling calculation with a window size of + three. + + >>> s = pd.Series(range(5)) + >>> s.rolling(3).last() + 0 NaN + 1 NaN + 2 2.0 + 3 3.0 + 4 4.0 + dtype: float64 + """ + ).replace("\n", "", 1), + window_method="rolling", + aggregation_description="Last (right-most) element of the window", + agg_method="last", + ) + def last(self, numeric_only: bool = False): + return super().last(numeric_only=numeric_only) + @doc( template_header, create_section_header("Parameters"), diff --git a/pandas/tests/window/test_cython_aggregations.py b/pandas/tests/window/test_cython_aggregations.py index c60cb6ea74ec0..feb25a294c540 100644 --- a/pandas/tests/window/test_cython_aggregations.py +++ b/pandas/tests/window/test_cython_aggregations.py @@ -30,6 +30,8 @@ def _get_rolling_aggregations(): ("roll_median_c", window_aggregations.roll_median_c), ("roll_max", window_aggregations.roll_max), ("roll_min", window_aggregations.roll_min), + ("roll_first", window_aggregations.roll_first), + ("roll_last", window_aggregations.roll_last), ] + [ ( diff --git a/pandas/tests/window/test_expanding.py b/pandas/tests/window/test_expanding.py index b2f76bdd0e2ad..39cedc3b692da 100644 --- a/pandas/tests/window/test_expanding.py +++ b/pandas/tests/window/test_expanding.py @@ -451,6 +451,8 @@ def test_moment_functions_zero_length_pairwise(f): lambda x: x.expanding(min_periods=5).corr(x, pairwise=False), lambda x: x.expanding(min_periods=5).max(), lambda x: x.expanding(min_periods=5).min(), + lambda x: x.expanding(min_periods=5).first(), + lambda x: x.expanding(min_periods=5).last(), lambda x: x.expanding(min_periods=5).sum(), lambda x: x.expanding(min_periods=5).mean(), lambda x: x.expanding(min_periods=5).std(), @@ -596,6 +598,104 @@ def test_expanding_corr_pairwise_diff_length(): tm.assert_frame_equal(result4, expected) +@pytest.mark.parametrize( + "values,method,expected", + [ + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "first", + [float("nan"), float("nan"), 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "first", + [ + float("nan"), + float("nan"), + float("nan"), + float("nan"), + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + ], + ), + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "last", + [float("nan"), float("nan"), 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "last", + [ + float("nan"), + float("nan"), + float("nan"), + float("nan"), + 5.0, + 5.0, + 7.0, + 7.0, + 9.0, + 9.0, + ], + ), + ], +) +def test_expanding_first_last(values, method, expected): + # GH#33155 + x = Series(values) + result = getattr(x.expanding(3), method)() + expected = Series(expected) + tm.assert_almost_equal(result, expected) + + x = DataFrame({"A": values}) + result = getattr(x.expanding(3), method)() + expected = DataFrame({"A": expected}) + tm.assert_almost_equal(result, expected) + + +@pytest.mark.parametrize( + "values,method,expected", + [ + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "first", + [1.0] * 10, + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "first", + [1.0] * 10, + ), + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "last", + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "last", + [1.0, 1.0, 3.0, 3.0, 5.0, 5.0, 7.0, 7.0, 9.0, 9.0], + ), + ], +) +def test_expanding_first_last_no_minp(values, method, expected): + # GH#33155 + x = Series(values) + result = getattr(x.expanding(min_periods=0), method)() + expected = Series(expected) + tm.assert_almost_equal(result, expected) + + x = DataFrame({"A": values}) + result = getattr(x.expanding(min_periods=0), method)() + expected = DataFrame({"A": expected}) + tm.assert_almost_equal(result, expected) + + def test_expanding_apply_args_kwargs(engine_and_raw): def mean_w_arg(x, const): return np.mean(x) + const diff --git a/pandas/tests/window/test_groupby.py b/pandas/tests/window/test_groupby.py index f53250378e33c..392239b8adadd 100644 --- a/pandas/tests/window/test_groupby.py +++ b/pandas/tests/window/test_groupby.py @@ -91,6 +91,8 @@ def test_getitem_multiple(self, roll_frame): "mean", "min", "max", + "first", + "last", "count", "kurt", "skew", @@ -1032,7 +1034,7 @@ def frame(self): return DataFrame({"A": [1] * 20 + [2] * 12 + [3] * 8, "B": np.arange(40)}) @pytest.mark.parametrize( - "f", ["sum", "mean", "min", "max", "count", "kurt", "skew"] + "f", ["sum", "mean", "min", "max", "first", "last", "count", "kurt", "skew"] ) def test_expanding(self, f, frame): g = frame.groupby("A", group_keys=False) diff --git a/pandas/tests/window/test_rolling.py b/pandas/tests/window/test_rolling.py index af3194b5085c4..2aaa35ec5ec2c 100644 --- a/pandas/tests/window/test_rolling.py +++ b/pandas/tests/window/test_rolling.py @@ -1326,6 +1326,82 @@ def test_rolling_corr_timedelta_index(index, window): tm.assert_almost_equal(result, expected) +@pytest.mark.parametrize( + "values,method,expected", + [ + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "first", + [float("nan"), float("nan"), 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "first", + [float("nan")] * 10, + ), + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "last", + [float("nan"), float("nan"), 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "last", + [float("nan")] * 10, + ), + ], +) +def test_rolling_first_last(values, method, expected): + # GH#33155 + x = Series(values) + result = getattr(x.rolling(3), method)() + expected = Series(expected) + tm.assert_almost_equal(result, expected) + + x = DataFrame({"A": values}) + result = getattr(x.rolling(3), method)() + expected = DataFrame({"A": expected}) + tm.assert_almost_equal(result, expected) + + +@pytest.mark.parametrize( + "values,method,expected", + [ + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "first", + [1.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "first", + [1.0, 1.0, 1.0, 3.0, 3.0, 5.0, 5.0, 7.0, 7.0, 9.0], + ), + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "last", + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ), + ( + [1.0, np.nan, 3.0, np.nan, 5.0, np.nan, 7.0, np.nan, 9.0, np.nan], + "last", + [1.0, 1.0, 3.0, 3.0, 5.0, 5.0, 7.0, 7.0, 9.0, 9.0], + ), + ], +) +def test_rolling_first_last_no_minp(values, method, expected): + # GH#33155 + x = Series(values) + result = getattr(x.rolling(3, min_periods=0), method)() + expected = Series(expected) + tm.assert_almost_equal(result, expected) + + x = DataFrame({"A": values}) + result = getattr(x.rolling(3, min_periods=0), method)() + expected = DataFrame({"A": expected}) + tm.assert_almost_equal(result, expected) + + def test_groupby_rolling_nan_included(): # GH 35542 data = {"group": ["g1", np.nan, "g1", "g2", np.nan], "B": [0, 1, 2, 3, 4]} diff --git a/pandas/tests/window/test_rolling_functions.py b/pandas/tests/window/test_rolling_functions.py index f77a98ae9a7d9..6820ab7332975 100644 --- a/pandas/tests/window/test_rolling_functions.py +++ b/pandas/tests/window/test_rolling_functions.py @@ -340,6 +340,8 @@ def test_center_reindex_frame(frame, roll_func, kwargs, minp, fill_value): lambda x: x.rolling(window=10, min_periods=5).var(), lambda x: x.rolling(window=10, min_periods=5).skew(), lambda x: x.rolling(window=10, min_periods=5).kurt(), + lambda x: x.rolling(window=10, min_periods=5).first(), + lambda x: x.rolling(window=10, min_periods=5).last(), lambda x: x.rolling(window=10, min_periods=5).quantile(q=0.5), lambda x: x.rolling(window=10, min_periods=5).median(), lambda x: x.rolling(window=10, min_periods=5).apply(sum, raw=False), @@ -501,6 +503,8 @@ def test_rolling_min_max_numeric_types(any_real_numpy_dtype): lambda x: x.rolling(window=10, min_periods=5).var(), lambda x: x.rolling(window=10, min_periods=5).skew(), lambda x: x.rolling(window=10, min_periods=5).kurt(), + lambda x: x.rolling(window=10, min_periods=5).first(), + lambda x: x.rolling(window=10, min_periods=5).last(), lambda x: x.rolling(window=10, min_periods=5).quantile(0.5), lambda x: x.rolling(window=10, min_periods=5).median(), lambda x: x.rolling(window=10, min_periods=5).apply(sum, raw=False), diff --git a/pandas/tests/window/test_timeseries_window.py b/pandas/tests/window/test_timeseries_window.py index eacdaddfa28b0..043f369566a5d 100644 --- a/pandas/tests/window/test_timeseries_window.py +++ b/pandas/tests/window/test_timeseries_window.py @@ -541,6 +541,42 @@ def test_ragged_max(self, ragged): expected["B"] = [0.0, 1, 2, 3, 4] tm.assert_frame_equal(result, expected) + def test_ragged_first(self, ragged): + df = ragged + + result = df.rolling(window="1s", min_periods=1).first() + expected = df.copy() + expected["B"] = [0.0, 1, 2, 3, 4] + tm.assert_frame_equal(result, expected) + + result = df.rolling(window="2s", min_periods=1).first() + expected = df.copy() + expected["B"] = [0.0, 1, 1, 3, 3] + tm.assert_frame_equal(result, expected) + + result = df.rolling(window="5s", min_periods=1).first() + expected = df.copy() + expected["B"] = [0.0, 0, 0, 1, 1] + tm.assert_frame_equal(result, expected) + + def test_ragged_last(self, ragged): + df = ragged + + result = df.rolling(window="1s", min_periods=1).last() + expected = df.copy() + expected["B"] = [0.0, 1, 2, 3, 4] + tm.assert_frame_equal(result, expected) + + result = df.rolling(window="2s", min_periods=1).last() + expected = df.copy() + expected["B"] = [0.0, 1, 2, 3, 4] + tm.assert_frame_equal(result, expected) + + result = df.rolling(window="5s", min_periods=1).last() + expected = df.copy() + expected["B"] = [0.0, 1, 2, 3, 4] + tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize( "freq, op, result_data", [ @@ -586,6 +622,8 @@ def test_freqs_ops(self, freq, op, result_data): "skew", "min", "max", + "first", + "last", ], ) def test_all(self, f, regular): From a4e814954b6f1c41528c071b028df62def7765c0 Mon Sep 17 00:00:00 2001 From: Patrick Hoefler <61934744+phofl@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:11:37 +0100 Subject: [PATCH 219/266] REGR: from_records not initializing subclasses properly (#60726) * REGR: from_records not initializing subclasses properly * Move whatsnew --- doc/source/whatsnew/v2.3.0.rst | 1 - doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/frame.py | 5 ++++- pandas/tests/frame/test_subclass.py | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index 9e0e095eb4de8..96eed72823e72 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -175,7 +175,6 @@ Other ^^^^^ - Fixed usage of ``inspect`` when the optional dependencies ``pyarrow`` or ``jinja2`` are not installed (:issue:`60196`) -- .. --------------------------------------------------------------------------- .. _whatsnew_230.contributors: diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1e33971acac1a..102628257d6f2 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -812,6 +812,7 @@ Other - Bug in ``Series.list`` methods not preserving the original name. (:issue:`60522`) - Bug in printing a :class:`DataFrame` with a :class:`DataFrame` stored in :attr:`DataFrame.attrs` raised a ``ValueError`` (:issue:`60455`) - Bug in printing a :class:`Series` with a :class:`DataFrame` stored in :attr:`Series.attrs` raised a ``ValueError`` (:issue:`60568`) +- Fixed regression in :meth:`DataFrame.from_records` not initializing subclasses properly (:issue:`57008`) .. ***DO NOT USE THIS SECTION*** diff --git a/pandas/core/frame.py b/pandas/core/frame.py index ffffaeba4196e..863465ca1565c 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -2317,7 +2317,10 @@ def maybe_reorder( columns = columns.drop(exclude) mgr = arrays_to_mgr(arrays, columns, result_index) - return cls._from_mgr(mgr, axes=mgr.axes) + df = DataFrame._from_mgr(mgr, axes=mgr.axes) + if cls is not DataFrame: + return cls(df, copy=False) + return df def to_records( self, index: bool = True, column_dtypes=None, index_dtypes=None diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 7d18ef28a722d..cbd563a03b908 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -769,6 +769,13 @@ def test_constructor_with_metadata(): assert isinstance(subset, MySubclassWithMetadata) +def test_constructor_with_metadata_from_records(): + # GH#57008 + df = MySubclassWithMetadata.from_records([{"a": 1, "b": 2}]) + assert df.my_metadata is None + assert type(df) is MySubclassWithMetadata + + class SimpleDataFrameSubClass(DataFrame): """A subclass of DataFrame that does not define a constructor.""" From 27baf4887a1b7d4f4a378c2f951cdc95fb1ab2b8 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 21 Jan 2025 12:18:58 -0500 Subject: [PATCH 220/266] DOC: fix ES01 for pandas.read_feather (#60746) --- pandas/io/feather_format.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandas/io/feather_format.py b/pandas/io/feather_format.py index 7b4c81853eba3..565c53f0f3fc5 100644 --- a/pandas/io/feather_format.py +++ b/pandas/io/feather_format.py @@ -78,6 +78,14 @@ def read_feather( """ Load a feather-format object from the file path. + Feather is particularly useful for scenarios that require efficient + serialization and deserialization of tabular data. It supports + schema preservation, making it a reliable choice for use cases + such as sharing data between Python and R, or persisting intermediate + results during data processing pipelines. This method provides additional + flexibility with options for selective column reading, thread parallelism, + and choosing the backend for data types. + Parameters ---------- path : str, path object, or file-like object From bbd6526461b6e9fc7783bd51298db5cb2ae0c679 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Tue, 21 Jan 2025 17:26:09 +0000 Subject: [PATCH 221/266] ENH: `pandas.api.interchange.from_dataframe` now uses the Arrow PyCapsule Interface if available, only falling back to the Dataframe Interchange Protocol if that fails (#60739) * add test for list dtype * catch arrowinvalid and keep raising runtimeerror * use rst hyperlink --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/interchange/from_dataframe.py | 16 +++++++++++++++- pandas/tests/interchange/test_impl.py | 14 +++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 102628257d6f2..8471630511e32 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -30,6 +30,7 @@ Other enhancements ^^^^^^^^^^^^^^^^^^ - :class:`pandas.api.typing.FrozenList` is available for typing the outputs of :attr:`MultiIndex.names`, :attr:`MultiIndex.codes` and :attr:`MultiIndex.levels` (:issue:`58237`) - :class:`pandas.api.typing.SASReader` is available for typing the output of :func:`read_sas` (:issue:`55689`) +- :meth:`pandas.api.interchange.from_dataframe` now uses the `PyCapsule Interface `_ if available, only falling back to the Dataframe Interchange Protocol if that fails (:issue:`60739`) - :class:`pandas.api.typing.NoDefault` is available for typing ``no_default`` - :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`) - :func:`pandas.merge` now validates the ``how`` parameter input (merge type) (:issue:`59435`) diff --git a/pandas/core/interchange/from_dataframe.py b/pandas/core/interchange/from_dataframe.py index 5c9b8ac8ea085..b990eca39b3dd 100644 --- a/pandas/core/interchange/from_dataframe.py +++ b/pandas/core/interchange/from_dataframe.py @@ -41,7 +41,9 @@ def from_dataframe(df, allow_copy: bool = True) -> pd.DataFrame: .. note:: For new development, we highly recommend using the Arrow C Data Interface - alongside the Arrow PyCapsule Interface instead of the interchange protocol + alongside the Arrow PyCapsule Interface instead of the interchange protocol. + From pandas 3.0 onwards, `from_dataframe` uses the PyCapsule Interface, + only falling back to the interchange protocol if that fails. .. warning:: @@ -90,6 +92,18 @@ def from_dataframe(df, allow_copy: bool = True) -> pd.DataFrame: if isinstance(df, pd.DataFrame): return df + if hasattr(df, "__arrow_c_stream__"): + try: + pa = import_optional_dependency("pyarrow", min_version="14.0.0") + except ImportError: + # fallback to _from_dataframe + pass + else: + try: + return pa.table(df).to_pandas(zero_copy_only=not allow_copy) + except pa.ArrowInvalid as e: + raise RuntimeError(e) from e + if not hasattr(df, "__dataframe__"): raise ValueError("`df` does not support __dataframe__") diff --git a/pandas/tests/interchange/test_impl.py b/pandas/tests/interchange/test_impl.py index b80b4b923c247..a41d7dec8b496 100644 --- a/pandas/tests/interchange/test_impl.py +++ b/pandas/tests/interchange/test_impl.py @@ -278,7 +278,7 @@ def test_empty_pyarrow(data): expected = pd.DataFrame(data) arrow_df = pa_from_dataframe(expected) result = from_dataframe(arrow_df) - tm.assert_frame_equal(result, expected) + tm.assert_frame_equal(result, expected, check_column_type=False) def test_multi_chunk_pyarrow() -> None: @@ -288,8 +288,7 @@ def test_multi_chunk_pyarrow() -> None: table = pa.table([n_legs], names=names) with pytest.raises( RuntimeError, - match="To join chunks a copy is required which is " - "forbidden by allow_copy=False", + match="Cannot do zero copy conversion into multi-column DataFrame block", ): pd.api.interchange.from_dataframe(table, allow_copy=False) @@ -641,3 +640,12 @@ def test_buffer_dtype_categorical( col = dfi.get_column_by_name("data") assert col.dtype == expected_dtype assert col.get_buffers()["data"][1] == expected_buffer_dtype + + +def test_from_dataframe_list_dtype(): + pa = pytest.importorskip("pyarrow", "14.0.0") + data = {"a": [[1, 2], [4, 5, 6]]} + tbl = pa.table(data) + result = from_dataframe(tbl) + expected = pd.DataFrame(data) + tm.assert_frame_equal(result, expected) From 297a19eeebe64b1df9abeedccd2bdbc9dbc94693 Mon Sep 17 00:00:00 2001 From: William Andrea <22385371+wjandrea@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:27:50 -0400 Subject: [PATCH 222/266] DOC: Fix typo "numpy.ndarray.putmask" (#60731) Should be "numpy.putmask" --- pandas/core/indexes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 165fe109c4c94..e2f9c5e9868a9 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -5348,7 +5348,7 @@ def putmask(self, mask, value) -> Index: See Also -------- - numpy.ndarray.putmask : Changes elements of an array + numpy.putmask : Changes elements of an array based on conditional and input values. Examples From 7234104f104883092a97474ac3eda98e8a5ea35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <99898527+grossardt@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:29:26 -0600 Subject: [PATCH 223/266] DOC: Clarify deprecation warning for iloc (#60745) api-doc rewrite deprecation warning iloc --- pandas/core/indexing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index e0bc0a23acd9f..656ee54cbc5d4 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -160,7 +160,7 @@ def iloc(self) -> _iLocIndexer: .. versionchanged:: 3.0 - Returning a tuple from a callable is deprecated. + Callables which return a tuple are deprecated as input. ``.iloc[]`` is primarily integer position based (from ``0`` to ``length-1`` of the axis), but may also be used with a boolean From 42bf3751a3b6354907c30f435717b9708b2661b4 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Tue, 21 Jan 2025 10:59:54 -0800 Subject: [PATCH 224/266] ENH: Support skipna parameter in GroupBy mean and sum (#60741) * ENH: Support skipna parameter in GroupBy mean and sum * Move numba tests to test_numba.py * Fix docstring and failing future string test --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/_libs/groupby.pyi | 2 + pandas/_libs/groupby.pyx | 48 +++++++++- pandas/core/_numba/kernels/mean_.py | 3 +- pandas/core/_numba/kernels/sum_.py | 14 ++- pandas/core/groupby/groupby.py | 74 ++++++++++++++- pandas/tests/groupby/aggregate/test_numba.py | 17 ++++ pandas/tests/groupby/test_api.py | 10 +- pandas/tests/groupby/test_reductions.py | 96 +++++++++++++++++++- 9 files changed, 255 insertions(+), 10 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 8471630511e32..fea269ac4555e 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -60,6 +60,7 @@ Other enhancements - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) - :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`) - :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`) +- :meth:`.DataFrameGroupBy.mean`, :meth:`.DataFrameGroupBy.sum`, :meth:`.SeriesGroupBy.mean` and :meth:`.SeriesGroupBy.sum` now accept ``skipna`` parameter (:issue:`15675`) - :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`) - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) - :meth:`Rolling.agg`, :meth:`Expanding.agg` and :meth:`ExponentialMovingWindow.agg` now accept :class:`NamedAgg` aggregations through ``**kwargs`` (:issue:`28333`) diff --git a/pandas/_libs/groupby.pyi b/pandas/_libs/groupby.pyi index 34367f55d2bbb..e3909203d1f5a 100644 --- a/pandas/_libs/groupby.pyi +++ b/pandas/_libs/groupby.pyi @@ -66,6 +66,7 @@ def group_sum( result_mask: np.ndarray | None = ..., min_count: int = ..., is_datetimelike: bool = ..., + skipna: bool = ..., ) -> None: ... def group_prod( out: np.ndarray, # int64float_t[:, ::1] @@ -115,6 +116,7 @@ def group_mean( is_datetimelike: bool = ..., # bint mask: np.ndarray | None = ..., result_mask: np.ndarray | None = ..., + skipna: bool = ..., ) -> None: ... def group_ohlc( out: np.ndarray, # floatingintuint_t[:, ::1] diff --git a/pandas/_libs/groupby.pyx b/pandas/_libs/groupby.pyx index 59bc59135a8ff..fd288dff01f32 100644 --- a/pandas/_libs/groupby.pyx +++ b/pandas/_libs/groupby.pyx @@ -700,13 +700,14 @@ def group_sum( uint8_t[:, ::1] result_mask=None, Py_ssize_t min_count=0, bint is_datetimelike=False, + bint skipna=True, ) -> None: """ Only aggregates on axis=0 using Kahan summation """ cdef: Py_ssize_t i, j, N, K, lab, ncounts = len(counts) - sum_t val, t, y + sum_t val, t, y, nan_val sum_t[:, ::1] sumx, compensation int64_t[:, ::1] nobs Py_ssize_t len_values = len(values), len_labels = len(labels) @@ -722,6 +723,15 @@ def group_sum( compensation = np.zeros((out).shape, dtype=(out).base.dtype) N, K = (values).shape + if uses_mask: + nan_val = 0 + elif is_datetimelike: + nan_val = NPY_NAT + elif sum_t is int64_t or sum_t is uint64_t: + # This has no effect as int64 can't be nan. Setting to 0 to avoid type error + nan_val = 0 + else: + nan_val = NAN with nogil(sum_t is not object): for i in range(N): @@ -734,6 +744,16 @@ def group_sum( for j in range(K): val = values[i, j] + if not skipna and ( + (uses_mask and result_mask[lab, j]) or + (is_datetimelike and sumx[lab, j] == NPY_NAT) or + _treat_as_na(sumx[lab, j], False) + ): + # If sum is already NA, don't add to it. This is important for + # datetimelikebecause adding a value to NPY_NAT may not result + # in a NPY_NAT + continue + if uses_mask: isna_entry = mask[i, j] else: @@ -765,6 +785,11 @@ def group_sum( # because of no gil compensation[lab, j] = 0 sumx[lab, j] = t + elif not skipna: + if uses_mask: + result_mask[lab, j] = True + else: + sumx[lab, j] = nan_val _check_below_mincount( out, uses_mask, result_mask, ncounts, K, nobs, min_count, sumx @@ -1100,6 +1125,7 @@ def group_mean( bint is_datetimelike=False, const uint8_t[:, ::1] mask=None, uint8_t[:, ::1] result_mask=None, + bint skipna=True, ) -> None: """ Compute the mean per label given a label assignment for each value. @@ -1125,6 +1151,8 @@ def group_mean( Mask of the input values. result_mask : ndarray[bool, ndim=2], optional Mask of the out array + skipna : bool, optional + If True, ignore nans in `values`. Notes ----- @@ -1168,6 +1196,16 @@ def group_mean( for j in range(K): val = values[i, j] + if not skipna and ( + (uses_mask and result_mask[lab, j]) or + (is_datetimelike and sumx[lab, j] == NPY_NAT) or + _treat_as_na(sumx[lab, j], False) + ): + # If sum is already NA, don't add to it. This is important for + # datetimelike because adding a value to NPY_NAT may not result + # in NPY_NAT + continue + if uses_mask: isna_entry = mask[i, j] elif is_datetimelike: @@ -1191,6 +1229,14 @@ def group_mean( # because of no gil compensation[lab, j] = 0. sumx[lab, j] = t + elif not skipna: + # Set the nobs to 0 so that in case of datetimelike, + # dividing NPY_NAT by nobs may not result in a NPY_NAT + nobs[lab, j] = 0 + if uses_mask: + result_mask[lab, j] = True + else: + sumx[lab, j] = nan_val for i in range(ncounts): for j in range(K): diff --git a/pandas/core/_numba/kernels/mean_.py b/pandas/core/_numba/kernels/mean_.py index cc10bd003af7e..2b59ea2fe12a5 100644 --- a/pandas/core/_numba/kernels/mean_.py +++ b/pandas/core/_numba/kernels/mean_.py @@ -169,9 +169,10 @@ def grouped_mean( labels: npt.NDArray[np.intp], ngroups: int, min_periods: int, + skipna: bool, ) -> tuple[np.ndarray, list[int]]: output, nobs_arr, comp_arr, consecutive_counts, prev_vals = grouped_kahan_sum( - values, result_dtype, labels, ngroups + values, result_dtype, labels, ngroups, skipna ) # Post-processing, replace sums that don't satisfy min_periods diff --git a/pandas/core/_numba/kernels/sum_.py b/pandas/core/_numba/kernels/sum_.py index 76f4e22b43c4b..9f2e9541b31d0 100644 --- a/pandas/core/_numba/kernels/sum_.py +++ b/pandas/core/_numba/kernels/sum_.py @@ -165,6 +165,7 @@ def grouped_kahan_sum( result_dtype: np.dtype, labels: npt.NDArray[np.intp], ngroups: int, + skipna: bool, ) -> tuple[ np.ndarray, npt.NDArray[np.int64], np.ndarray, npt.NDArray[np.int64], np.ndarray ]: @@ -180,7 +181,15 @@ def grouped_kahan_sum( lab = labels[i] val = values[i] - if lab < 0: + if lab < 0 or np.isnan(output[lab]): + continue + + if not skipna and np.isnan(val): + output[lab] = np.nan + nobs_arr[lab] += 1 + comp_arr[lab] = np.nan + consecutive_counts[lab] = 1 + prev_vals[lab] = np.nan continue sum_x = output[lab] @@ -219,11 +228,12 @@ def grouped_sum( labels: npt.NDArray[np.intp], ngroups: int, min_periods: int, + skipna: bool, ) -> tuple[np.ndarray, list[int]]: na_pos = [] output, nobs_arr, comp_arr, consecutive_counts, prev_vals = grouped_kahan_sum( - values, result_dtype, labels, ngroups + values, result_dtype, labels, ngroups, skipna ) # Post-processing, replace sums that don't satisfy min_periods diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index f4ba40e275a8d..f9059e6e8896f 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -214,6 +214,61 @@ class providing the base-class of operations. {example} """ +_groupby_agg_method_skipna_engine_template = """ +Compute {fname} of group values. + +Parameters +---------- +numeric_only : bool, default {no} + Include only float, int, boolean columns. + + .. versionchanged:: 2.0.0 + + numeric_only no longer accepts ``None``. + +min_count : int, default {mc} + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + +skipna : bool, default {s} + Exclude NA/null values. If the entire group is NA and ``skipna`` is + ``True``, the result will be NA. + + .. versionchanged:: 3.0.0 + +engine : str, default None {e} + * ``'cython'`` : Runs rolling apply through C-extensions from cython. + * ``'numba'`` : Runs rolling apply through JIT compiled code from numba. + Only available when ``raw`` is set to ``True``. + * ``None`` : Defaults to ``'cython'`` or globally setting ``compute.use_numba`` + +engine_kwargs : dict, default None {ek} + * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` + * For ``'numba'`` engine, the engine can accept ``nopython``, ``nogil`` + and ``parallel`` dictionary keys. The values must either be ``True`` or + ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is + ``{{'nopython': True, 'nogil': False, 'parallel': False}}`` and will be + applied to both the ``func`` and the ``apply`` groupby aggregation. + +Returns +------- +Series or DataFrame + Computed {fname} of values within each group. + +See Also +-------- +SeriesGroupBy.min : Return the min of the group values. +DataFrameGroupBy.min : Return the min of the group values. +SeriesGroupBy.max : Return the max of the group values. +DataFrameGroupBy.max : Return the max of the group values. +SeriesGroupBy.sum : Return the sum of the group values. +DataFrameGroupBy.sum : Return the sum of the group values. + +Examples +-------- +{example} +""" + _pipe_template = """ Apply a ``func`` with arguments to this %(klass)s object and return its result. @@ -2091,6 +2146,7 @@ def hfunc(bvalues: ArrayLike) -> ArrayLike: def mean( self, numeric_only: bool = False, + skipna: bool = True, engine: Literal["cython", "numba"] | None = None, engine_kwargs: dict[str, bool] | None = None, ): @@ -2106,6 +2162,12 @@ def mean( numeric_only no longer accepts ``None`` and defaults to ``False``. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + + .. versionadded:: 3.0.0 + engine : str, default None * ``'cython'`` : Runs the operation through C-extensions from cython. * ``'numba'`` : Runs the operation through JIT compiled code from numba. @@ -2172,12 +2234,16 @@ def mean( executor.float_dtype_mapping, engine_kwargs, min_periods=0, + skipna=skipna, ) else: result = self._cython_agg_general( "mean", - alt=lambda x: Series(x, copy=False).mean(numeric_only=numeric_only), + alt=lambda x: Series(x, copy=False).mean( + numeric_only=numeric_only, skipna=skipna + ), numeric_only=numeric_only, + skipna=skipna, ) return result.__finalize__(self.obj, method="groupby") @@ -2817,10 +2883,11 @@ def size(self) -> DataFrame | Series: @final @doc( - _groupby_agg_method_engine_template, + _groupby_agg_method_skipna_engine_template, fname="sum", no=False, mc=0, + s=True, e=None, ek=None, example=dedent( @@ -2862,6 +2929,7 @@ def sum( self, numeric_only: bool = False, min_count: int = 0, + skipna: bool = True, engine: Literal["cython", "numba"] | None = None, engine_kwargs: dict[str, bool] | None = None, ): @@ -2873,6 +2941,7 @@ def sum( executor.default_dtype_mapping, engine_kwargs, min_periods=min_count, + skipna=skipna, ) else: # If we are grouping on categoricals we want unobserved categories to @@ -2884,6 +2953,7 @@ def sum( min_count=min_count, alias="sum", npfunc=np.sum, + skipna=skipna, ) return result diff --git a/pandas/tests/groupby/aggregate/test_numba.py b/pandas/tests/groupby/aggregate/test_numba.py index 15c1efe5fd1ff..ca265a1d1108b 100644 --- a/pandas/tests/groupby/aggregate/test_numba.py +++ b/pandas/tests/groupby/aggregate/test_numba.py @@ -186,6 +186,23 @@ def test_multifunc_numba_vs_cython_frame(agg_kwargs): tm.assert_frame_equal(result, expected) +@pytest.mark.parametrize("func", ["sum", "mean"]) +def test_multifunc_numba_vs_cython_frame_noskipna(func): + pytest.importorskip("numba") + data = DataFrame( + { + 0: ["a", "a", "b", "b", "a"], + 1: [1.0, np.nan, 3.0, 4.0, 5.0], + 2: [1, 2, 3, 4, 5], + }, + columns=[0, 1, 2], + ) + grouped = data.groupby(0) + result = grouped.agg(func, skipna=False, engine="numba") + expected = grouped.agg(func, skipna=False, engine="cython") + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize( "agg_kwargs,expected_func", [ diff --git a/pandas/tests/groupby/test_api.py b/pandas/tests/groupby/test_api.py index baec3ed1a5024..cc69de2581a79 100644 --- a/pandas/tests/groupby/test_api.py +++ b/pandas/tests/groupby/test_api.py @@ -176,7 +176,10 @@ def test_frame_consistency(groupby_func): elif groupby_func in ("max", "min"): exclude_expected = {"axis", "kwargs", "skipna"} exclude_result = {"min_count", "engine", "engine_kwargs"} - elif groupby_func in ("mean", "std", "sum", "var"): + elif groupby_func in ("sum", "mean"): + exclude_expected = {"axis", "kwargs"} + exclude_result = {"engine", "engine_kwargs"} + elif groupby_func in ("std", "var"): exclude_expected = {"axis", "kwargs", "skipna"} exclude_result = {"engine", "engine_kwargs"} elif groupby_func in ("median", "prod", "sem"): @@ -234,7 +237,10 @@ def test_series_consistency(request, groupby_func): elif groupby_func in ("max", "min"): exclude_expected = {"axis", "kwargs", "skipna"} exclude_result = {"min_count", "engine", "engine_kwargs"} - elif groupby_func in ("mean", "std", "sum", "var"): + elif groupby_func in ("sum", "mean"): + exclude_expected = {"axis", "kwargs"} + exclude_result = {"engine", "engine_kwargs"} + elif groupby_func in ("std", "var"): exclude_expected = {"axis", "kwargs", "skipna"} exclude_result = {"engine", "engine_kwargs"} elif groupby_func in ("median", "prod", "sem"): diff --git a/pandas/tests/groupby/test_reductions.py b/pandas/tests/groupby/test_reductions.py index a17200c123d22..1db12f05e821f 100644 --- a/pandas/tests/groupby/test_reductions.py +++ b/pandas/tests/groupby/test_reductions.py @@ -422,6 +422,98 @@ def test_mean_on_timedelta(): tm.assert_series_equal(result, expected) +@pytest.mark.parametrize( + "values, dtype, result_dtype", + [ + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "float64", "float64"), + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "Float64", "Float64"), + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "Int64", "Float64"), + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "timedelta64[ns]", "timedelta64[ns]"), + ( + pd.to_datetime( + [ + "2019-05-09", + pd.NaT, + "2019-05-11", + "2019-05-12", + "2019-05-13", + "2019-05-14", + "2019-05-15", + "2019-05-16", + "2019-05-17", + "2019-05-18", + ] + ), + "datetime64[ns]", + "datetime64[ns]", + ), + ], +) +def test_mean_skipna(values, dtype, result_dtype, skipna): + # GH#15675 + df = DataFrame( + { + "val": values, + "cat": ["A", "B"] * 5, + } + ).astype({"val": dtype}) + # We need to recast the expected values to the result_dtype because + # Series.mean() changes the dtype to float64/object depending on the input dtype + expected = ( + df.groupby("cat")["val"] + .apply(lambda x: x.mean(skipna=skipna)) + .astype(result_dtype) + ) + result = df.groupby("cat")["val"].mean(skipna=skipna) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + "values, dtype", + [ + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "float64"), + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "Float64"), + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "Int64"), + ([0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], "timedelta64[ns]"), + ], +) +def test_sum_skipna(values, dtype, skipna): + # GH#15675 + df = DataFrame( + { + "val": values, + "cat": ["A", "B"] * 5, + } + ).astype({"val": dtype}) + # We need to recast the expected values to the original dtype because + # Series.sum() changes the dtype + expected = ( + df.groupby("cat")["val"].apply(lambda x: x.sum(skipna=skipna)).astype(dtype) + ) + result = df.groupby("cat")["val"].sum(skipna=skipna) + tm.assert_series_equal(result, expected) + + +def test_sum_skipna_object(skipna): + # GH#15675 + df = DataFrame( + { + "val": ["a", "b", np.nan, "d", "e", "f", "g", "h", "i", "j"], + "cat": ["A", "B"] * 5, + } + ).astype({"val": object}) + if skipna: + expected = Series( + ["aegi", "bdfhj"], index=pd.Index(["A", "B"], name="cat"), name="val" + ).astype(object) + else: + expected = Series( + [np.nan, "bdfhj"], index=pd.Index(["A", "B"], name="cat"), name="val" + ).astype(object) + result = df.groupby("cat")["val"].sum(skipna=skipna) + tm.assert_series_equal(result, expected) + + def test_cython_median(): arr = np.random.default_rng(2).standard_normal(1000) arr[::2] = np.nan @@ -1128,8 +1220,8 @@ def test_regression_allowlist_methods(op, skipna, sort): grouped = frame.groupby(level=0, sort=sort) - if op in ["skew", "kurt"]: - # skew and kurt have skipna + if op in ["skew", "kurt", "sum", "mean"]: + # skew, kurt, sum, mean have skipna result = getattr(grouped, op)(skipna=skipna) expected = frame.groupby(level=0).apply(lambda h: getattr(h, op)(skipna=skipna)) if sort: From 31704e3a69a65f0508c40c4881453757334c217e Mon Sep 17 00:00:00 2001 From: Pranav Raghu <73378019+Impaler343@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:41:34 +0530 Subject: [PATCH 225/266] DOC: Add line clarifying sorting using sort_values() (#60734) fix docs --- pandas/core/frame.py | 3 ++- pandas/core/generic.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 863465ca1565c..af66bb54610f1 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -6890,7 +6890,8 @@ def sort_values( builtin :meth:`sorted` function, with the notable difference that this `key` function should be *vectorized*. It should expect a ``Series`` and return a Series with the same shape as the input. - It will be applied to each column in `by` independently. + It will be applied to each column in `by` independently. The values in the + returned Series will be used as the keys for sorting. Returns ------- diff --git a/pandas/core/generic.py b/pandas/core/generic.py index de7fb3682fb4f..e0a4f9d9c546a 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -4884,7 +4884,8 @@ def sort_values( builtin :meth:`sorted` function, with the notable difference that this `key` function should be *vectorized*. It should expect a ``Series`` and return a Series with the same shape as the input. - It will be applied to each column in `by` independently. + It will be applied to each column in `by` independently. The values in the + returned Series will be used as the keys for sorting. Returns ------- From b98336653128790661d4c66d398f3e44d481dd3b Mon Sep 17 00:00:00 2001 From: Thomas Li <47963215+lithomas1@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:18:40 -0800 Subject: [PATCH 226/266] CI: Test Github Actions Arm64 Runners (#60722) * CI: Test Github Actions Arm64 Runners * try using platform as cache key * fixes * add platform for includes jobs --- .github/workflows/unit-tests.yml | 16 +++++++++++++--- .github/workflows/wheels.yml | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 899b49cc4eff5..e0fa7f7421f13 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -22,10 +22,11 @@ defaults: jobs: ubuntu: - runs-on: ubuntu-22.04 + runs-on: ${{ matrix.platform }} timeout-minutes: 90 strategy: matrix: + platform: [ubuntu-22.04, ubuntu-24.04-arm] env_file: [actions-310.yaml, actions-311.yaml, actions-312.yaml] # Prevent the include jobs from overriding other jobs pattern: [""] @@ -35,9 +36,11 @@ jobs: env_file: actions-311-downstream_compat.yaml pattern: "not slow and not network and not single_cpu" pytest_target: "pandas/tests/test_downstream.py" + platform: ubuntu-22.04 - name: "Minimum Versions" env_file: actions-310-minimum_versions.yaml pattern: "not slow and not network and not single_cpu" + platform: ubuntu-22.04 - name: "Locale: it_IT" env_file: actions-311.yaml pattern: "not slow and not network and not single_cpu" @@ -48,6 +51,7 @@ jobs: # Also install it_IT (its encoding is ISO8859-1) but do not activate it. # It will be temporarily activated during tests with locale.setlocale extra_loc: "it_IT" + platform: ubuntu-22.04 - name: "Locale: zh_CN" env_file: actions-311.yaml pattern: "not slow and not network and not single_cpu" @@ -58,25 +62,31 @@ jobs: # Also install zh_CN (its encoding is gb2312) but do not activate it. # It will be temporarily activated during tests with locale.setlocale extra_loc: "zh_CN" + platform: ubuntu-22.04 - name: "Future infer strings" env_file: actions-312.yaml pandas_future_infer_string: "1" + platform: ubuntu-22.04 - name: "Future infer strings (without pyarrow)" env_file: actions-311.yaml pandas_future_infer_string: "1" + platform: ubuntu-22.04 - name: "Pypy" env_file: actions-pypy-39.yaml pattern: "not slow and not network and not single_cpu" test_args: "--max-worker-restart 0" + platform: ubuntu-22.04 - name: "Numpy Dev" env_file: actions-311-numpydev.yaml pattern: "not slow and not network and not single_cpu" test_args: "-W error::DeprecationWarning -W error::FutureWarning" + platform: ubuntu-22.04 - name: "Pyarrow Nightly" env_file: actions-311-pyarrownightly.yaml pattern: "not slow and not network and not single_cpu" + platform: ubuntu-22.04 fail-fast: false - name: ${{ matrix.name || format('ubuntu-latest {0}', matrix.env_file) }} + name: ${{ matrix.name || format('ubuntu-latest {0}', matrix.env_file) }}-${{ matrix.platform }} env: PATTERN: ${{ matrix.pattern }} LANG: ${{ matrix.lang || 'C.UTF-8' }} @@ -91,7 +101,7 @@ jobs: REMOVE_PYARROW: ${{ matrix.name == 'Future infer strings (without pyarrow)' && '1' || '0' }} concurrency: # https://github.community/t/concurrecy-not-work-for-push/183068/7 - group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.env_file }}-${{ matrix.pattern }}-${{ matrix.extra_apt || '' }}-${{ matrix.pandas_future_infer_string }} + group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.env_file }}-${{ matrix.pattern }}-${{ matrix.extra_apt || '' }}-${{ matrix.pandas_future_infer_string }}-${{ matrix.platform }} cancel-in-progress: true services: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3314e645509d1..a4c2a732f9fc8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -94,6 +94,7 @@ jobs: buildplat: - [ubuntu-22.04, manylinux_x86_64] - [ubuntu-22.04, musllinux_x86_64] + - [ubuntu-24.04-arm, manylinux_aarch64] - [macos-13, macosx_x86_64] # Note: M1 images on Github Actions start from macOS 14 - [macos-14, macosx_arm64] From 14dcb7b330b8ee5fb22e971807dd85df25c6ef2c Mon Sep 17 00:00:00 2001 From: William Ayd Date: Tue, 21 Jan 2025 17:46:21 -0500 Subject: [PATCH 227/266] Fix group_sum NaN comparison warnings (#60749) --- pandas/_libs/groupby.pyx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pandas/_libs/groupby.pyx b/pandas/_libs/groupby.pyx index fd288dff01f32..70af22f514ce0 100644 --- a/pandas/_libs/groupby.pyx +++ b/pandas/_libs/groupby.pyx @@ -712,7 +712,7 @@ def group_sum( int64_t[:, ::1] nobs Py_ssize_t len_values = len(values), len_labels = len(labels) bint uses_mask = mask is not None - bint isna_entry + bint isna_entry, isna_result if len_values != len_labels: raise ValueError("len(index) != len(labels)") @@ -744,20 +744,18 @@ def group_sum( for j in range(K): val = values[i, j] - if not skipna and ( - (uses_mask and result_mask[lab, j]) or - (is_datetimelike and sumx[lab, j] == NPY_NAT) or - _treat_as_na(sumx[lab, j], False) - ): - # If sum is already NA, don't add to it. This is important for - # datetimelikebecause adding a value to NPY_NAT may not result - # in a NPY_NAT - continue - if uses_mask: isna_entry = mask[i, j] + isna_result = result_mask[lab, j] else: isna_entry = _treat_as_na(val, is_datetimelike) + isna_result = _treat_as_na(sumx[lab, j], is_datetimelike) + + if not skipna and isna_result: + # If sum is already NA, don't add to it. This is important for + # datetimelikebecause adding a value to NPY_NAT may not result + # in a NPY_NAT + continue if not isna_entry: nobs[lab, j] += 1 From ae42f3e1c1b7af55059921b41fd61710fe2dd785 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:06:56 -0800 Subject: [PATCH 228/266] CI: Rename ubuntu unit test jobs (#60751) --- .github/workflows/unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e0fa7f7421f13..fe9ec7f40a54b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -86,7 +86,7 @@ jobs: pattern: "not slow and not network and not single_cpu" platform: ubuntu-22.04 fail-fast: false - name: ${{ matrix.name || format('ubuntu-latest {0}', matrix.env_file) }}-${{ matrix.platform }} + name: ${{ matrix.name || format('{0} {1}', matrix.platform, matrix.env_file) }} env: PATTERN: ${{ matrix.pattern }} LANG: ${{ matrix.lang || 'C.UTF-8' }} From 5efac8250787414ec580f0472e2b563032ec7d53 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 22 Jan 2025 03:07:08 +0100 Subject: [PATCH 229/266] Update PyArrow conversion and arrow/parquet tests for pyarrow 19.0 (#60716) * Update PyArrow conversion and arrow/parquet tests for pyarrow 19.0 * update pypi index * extra filterwarnings * more test updates * temp enable infer_string option * Adapt test_get_handle_pyarrow_compat for pyarrow 19 * Use pa_version_under19p0 in test_get_handle_pyarrow_compat * Adjust test_string_inference for using_infer_string * Fix test_string_inference for feather --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- .github/workflows/unit-tests.yml | 1 + ci/deps/actions-311-pyarrownightly.yaml | 2 +- pandas/compat/__init__.py | 2 + pandas/compat/pyarrow.py | 2 + pandas/io/_util.py | 10 +++- pandas/tests/arrays/string_/test_string.py | 22 +++++++- pandas/tests/io/test_common.py | 5 +- pandas/tests/io/test_feather.py | 18 +++++- pandas/tests/io/test_parquet.py | 65 ++++++++++++++-------- 9 files changed, 96 insertions(+), 31 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index fe9ec7f40a54b..d2e2a170a1d04 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -84,6 +84,7 @@ jobs: - name: "Pyarrow Nightly" env_file: actions-311-pyarrownightly.yaml pattern: "not slow and not network and not single_cpu" + pandas_future_infer_string: "1" platform: ubuntu-22.04 fail-fast: false name: ${{ matrix.name || format('{0} {1}', matrix.platform, matrix.env_file) }} diff --git a/ci/deps/actions-311-pyarrownightly.yaml b/ci/deps/actions-311-pyarrownightly.yaml index 22e4907e5a6e5..2d3d11c294e12 100644 --- a/ci/deps/actions-311-pyarrownightly.yaml +++ b/ci/deps/actions-311-pyarrownightly.yaml @@ -23,7 +23,7 @@ dependencies: - pip: - "tzdata>=2022.7" - - "--extra-index-url https://pypi.fury.io/arrow-nightlies/" + - "--extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" - "--prefer-binary" - "--pre" - "pyarrow" diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py index e7674386408f7..138456f877c5f 100644 --- a/pandas/compat/__init__.py +++ b/pandas/compat/__init__.py @@ -34,6 +34,7 @@ pa_version_under16p0, pa_version_under17p0, pa_version_under18p0, + pa_version_under19p0, ) if TYPE_CHECKING: @@ -166,4 +167,5 @@ def is_ci_environment() -> bool: "pa_version_under16p0", "pa_version_under17p0", "pa_version_under18p0", + "pa_version_under19p0", ] diff --git a/pandas/compat/pyarrow.py b/pandas/compat/pyarrow.py index bd009b544f31e..c501c06b93813 100644 --- a/pandas/compat/pyarrow.py +++ b/pandas/compat/pyarrow.py @@ -18,6 +18,7 @@ pa_version_under16p0 = _palv < Version("16.0.0") pa_version_under17p0 = _palv < Version("17.0.0") pa_version_under18p0 = _palv < Version("18.0.0") + pa_version_under19p0 = _palv < Version("19.0.0") HAS_PYARROW = True except ImportError: pa_version_under10p1 = True @@ -30,4 +31,5 @@ pa_version_under16p0 = True pa_version_under17p0 = True pa_version_under18p0 = True + pa_version_under19p0 = True HAS_PYARROW = False diff --git a/pandas/io/_util.py b/pandas/io/_util.py index 9778a404e23e0..6827fbe9c998e 100644 --- a/pandas/io/_util.py +++ b/pandas/io/_util.py @@ -10,7 +10,10 @@ from pandas._config import using_string_dtype from pandas._libs import lib -from pandas.compat import pa_version_under18p0 +from pandas.compat import ( + pa_version_under18p0, + pa_version_under19p0, +) from pandas.compat._optional import import_optional_dependency import pandas as pd @@ -77,7 +80,10 @@ def arrow_table_to_pandas( elif dtype_backend == "pyarrow": types_mapper = pd.ArrowDtype elif using_string_dtype(): - types_mapper = _arrow_string_types_mapper() + if pa_version_under19p0: + types_mapper = _arrow_string_types_mapper() + else: + types_mapper = None elif dtype_backend is lib.no_default or dtype_backend == "numpy": types_mapper = None else: diff --git a/pandas/tests/arrays/string_/test_string.py b/pandas/tests/arrays/string_/test_string.py index a32ac7db4656a..f875873863b4d 100644 --- a/pandas/tests/arrays/string_/test_string.py +++ b/pandas/tests/arrays/string_/test_string.py @@ -10,7 +10,10 @@ from pandas._config import using_string_dtype -from pandas.compat.pyarrow import pa_version_under12p0 +from pandas.compat.pyarrow import ( + pa_version_under12p0, + pa_version_under19p0, +) from pandas.core.dtypes.common import is_dtype_equal @@ -539,7 +542,7 @@ def test_arrow_roundtrip(dtype, string_storage, using_infer_string): assert table.field("a").type == "large_string" with pd.option_context("string_storage", string_storage): result = table.to_pandas() - if dtype.na_value is np.nan and not using_string_dtype(): + if dtype.na_value is np.nan and not using_infer_string: assert result["a"].dtype == "object" else: assert isinstance(result["a"].dtype, pd.StringDtype) @@ -553,6 +556,21 @@ def test_arrow_roundtrip(dtype, string_storage, using_infer_string): assert result.loc[2, "a"] is result["a"].dtype.na_value +@pytest.mark.filterwarnings("ignore:Passing a BlockManager:DeprecationWarning") +def test_arrow_from_string(using_infer_string): + # not roundtrip, but starting with pyarrow table without pandas metadata + pa = pytest.importorskip("pyarrow") + table = pa.table({"a": pa.array(["a", "b", None], type=pa.string())}) + + result = table.to_pandas() + + if using_infer_string and not pa_version_under19p0: + expected = pd.DataFrame({"a": ["a", "b", None]}, dtype="str") + else: + expected = pd.DataFrame({"a": ["a", "b", None]}, dtype="object") + tm.assert_frame_equal(result, expected) + + @pytest.mark.filterwarnings("ignore:Passing a BlockManager:DeprecationWarning") def test_arrow_load_from_zero_chunks(dtype, string_storage, using_infer_string): # GH-41040 diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 7ff3d24336f00..e162815271ab3 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -23,6 +23,7 @@ WASM, is_platform_windows, ) +from pandas.compat.pyarrow import pa_version_under19p0 import pandas.util._test_decorators as td import pandas as pd @@ -152,8 +153,8 @@ def test_get_handle_pyarrow_compat(self): s = StringIO(data) with icom.get_handle(s, "rb", is_text=False) as handles: df = pa_csv.read_csv(handles.handle).to_pandas() - # TODO will have to update this when pyarrow' to_pandas() is fixed - expected = expected.astype("object") + if pa_version_under19p0: + expected = expected.astype("object") tm.assert_frame_equal(df, expected) assert not s.closed diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 69354066dd5ef..24af0a014dd50 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -6,7 +6,10 @@ import numpy as np import pytest -from pandas.compat.pyarrow import pa_version_under18p0 +from pandas.compat.pyarrow import ( + pa_version_under18p0, + pa_version_under19p0, +) import pandas as pd import pandas._testing as tm @@ -239,16 +242,27 @@ def test_invalid_dtype_backend(self): with pytest.raises(ValueError, match=msg): read_feather(path, dtype_backend="numpy") - def test_string_inference(self, tmp_path): + def test_string_inference(self, tmp_path, using_infer_string): # GH#54431 path = tmp_path / "test_string_inference.p" df = pd.DataFrame(data={"a": ["x", "y"]}) df.to_feather(path) with pd.option_context("future.infer_string", True): result = read_feather(path) + dtype = pd.StringDtype(na_value=np.nan) expected = pd.DataFrame( data={"a": ["x", "y"]}, dtype=pd.StringDtype(na_value=np.nan) ) + expected = pd.DataFrame( + data={"a": ["x", "y"]}, + dtype=dtype, + columns=pd.Index( + ["a"], + dtype=object + if pa_version_under19p0 and not using_infer_string + else dtype, + ), + ) tm.assert_frame_equal(result, expected) @pytest.mark.skipif(pa_version_under18p0, reason="not supported before 18.0") diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 7919bb956dc7a..91580c31ea081 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -17,6 +17,7 @@ pa_version_under13p0, pa_version_under15p0, pa_version_under17p0, + pa_version_under19p0, ) import pandas as pd @@ -254,8 +255,10 @@ def test_invalid_engine(df_compat): check_round_trip(df_compat, "foo", "bar") -def test_options_py(df_compat, pa): +def test_options_py(df_compat, pa, using_infer_string): # use the set option + if using_infer_string and not pa_version_under19p0: + df_compat.columns = df_compat.columns.astype("str") with pd.option_context("io.parquet.engine", "pyarrow"): check_round_trip(df_compat) @@ -784,18 +787,21 @@ def test_unsupported_float16_cleanup(self, pa, path_type): def test_categorical(self, pa): # supported in >= 0.7.0 - df = pd.DataFrame() - df["a"] = pd.Categorical(list("abcdef")) - - # test for null, out-of-order values, and unobserved category - df["b"] = pd.Categorical( - ["bar", "foo", "foo", "bar", None, "bar"], - dtype=pd.CategoricalDtype(["foo", "bar", "baz"]), - ) - - # test for ordered flag - df["c"] = pd.Categorical( - ["a", "b", "c", "a", "c", "b"], categories=["b", "c", "d"], ordered=True + df = pd.DataFrame( + { + "a": pd.Categorical(list("abcdef")), + # test for null, out-of-order values, and unobserved category + "b": pd.Categorical( + ["bar", "foo", "foo", "bar", None, "bar"], + dtype=pd.CategoricalDtype(["foo", "bar", "baz"]), + ), + # test for ordered flag + "c": pd.Categorical( + ["a", "b", "c", "a", "c", "b"], + categories=["b", "c", "d"], + ordered=True, + ), + } ) check_round_trip(df, pa) @@ -858,11 +864,13 @@ def test_s3_roundtrip_for_dir( repeat=1, ) - def test_read_file_like_obj_support(self, df_compat): + def test_read_file_like_obj_support(self, df_compat, using_infer_string): pytest.importorskip("pyarrow") buffer = BytesIO() df_compat.to_parquet(buffer) df_from_buf = read_parquet(buffer) + if using_infer_string and not pa_version_under19p0: + df_compat.columns = df_compat.columns.astype("str") tm.assert_frame_equal(df_compat, df_from_buf) def test_expand_user(self, df_compat, monkeypatch): @@ -929,7 +937,7 @@ def test_additional_extension_arrays(self, pa, using_infer_string): "c": pd.Series(["a", None, "c"], dtype="string"), } ) - if using_infer_string: + if using_infer_string and pa_version_under19p0: check_round_trip(df, pa, expected=df.astype({"c": "str"})) else: check_round_trip(df, pa) @@ -943,7 +951,10 @@ def test_pyarrow_backed_string_array(self, pa, string_storage, using_infer_strin df = pd.DataFrame({"a": pd.Series(["a", None, "c"], dtype="string[pyarrow]")}) with pd.option_context("string_storage", string_storage): if using_infer_string: - expected = df.astype("str") + if pa_version_under19p0: + expected = df.astype("str") + else: + expected = df.astype(f"string[{string_storage}]") expected.columns = expected.columns.astype("str") else: expected = df.astype(f"string[{string_storage}]") @@ -1099,17 +1110,24 @@ def test_df_attrs_persistence(self, tmp_path, pa): new_df = read_parquet(path, engine=pa) assert new_df.attrs == df.attrs - def test_string_inference(self, tmp_path, pa): + def test_string_inference(self, tmp_path, pa, using_infer_string): # GH#54431 path = tmp_path / "test_string_inference.p" df = pd.DataFrame(data={"a": ["x", "y"]}, index=["a", "b"]) - df.to_parquet(path, engine="pyarrow") + df.to_parquet(path, engine=pa) with pd.option_context("future.infer_string", True): - result = read_parquet(path, engine="pyarrow") + result = read_parquet(path, engine=pa) + dtype = pd.StringDtype(na_value=np.nan) expected = pd.DataFrame( data={"a": ["x", "y"]}, - dtype=pd.StringDtype(na_value=np.nan), - index=pd.Index(["a", "b"], dtype=pd.StringDtype(na_value=np.nan)), + dtype=dtype, + index=pd.Index(["a", "b"], dtype=dtype), + columns=pd.Index( + ["a"], + dtype=object + if pa_version_under19p0 and not using_infer_string + else dtype, + ), ) tm.assert_frame_equal(result, expected) @@ -1122,7 +1140,10 @@ def test_roundtrip_decimal(self, tmp_path, pa): df = pd.DataFrame({"a": [Decimal("123.00")]}, dtype="string[pyarrow]") df.to_parquet(path, schema=pa.schema([("a", pa.decimal128(5))])) result = read_parquet(path) - expected = pd.DataFrame({"a": ["123"]}, dtype="string[python]") + if pa_version_under19p0: + expected = pd.DataFrame({"a": ["123"]}, dtype="string[python]") + else: + expected = pd.DataFrame({"a": [Decimal("123.00")]}, dtype="object") tm.assert_frame_equal(result, expected) def test_infer_string_large_string_type(self, tmp_path, pa): From 1bb264c443f6be64ac28ff9afc0341eed0bcc455 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Wed, 22 Jan 2025 04:55:49 -0500 Subject: [PATCH 230/266] API(str dtype): Raise on StringDtype for unary op + (#60710) --- doc/source/whatsnew/v2.3.0.rst | 1 + pandas/core/arrays/string_arrow.py | 3 +++ pandas/tests/frame/test_unary.py | 6 ------ 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index 96eed72823e72..f2b6f70a3138c 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -105,6 +105,7 @@ Conversion Strings ^^^^^^^ +- Bug in :meth:`Series.__pos__` and :meth:`DataFrame.__pos__` did not raise for :class:`StringDtype` with ``storage="pyarrow"`` (:issue:`60710`) - Bug in :meth:`Series.rank` for :class:`StringDtype` with ``storage="pyarrow"`` incorrectly returning integer results in case of ``method="average"`` and raising an error if it would truncate results (:issue:`59768`) - Bug in :meth:`Series.replace` with :class:`StringDtype` when replacing with a non-string value was not upcasting to ``object`` dtype (:issue:`60282`) - Bug in :meth:`Series.str.replace` when ``n < 0`` for :class:`StringDtype` with ``storage="pyarrow"`` (:issue:`59628`) diff --git a/pandas/core/arrays/string_arrow.py b/pandas/core/arrays/string_arrow.py index 27c1425d11ac6..d35083fd892a8 100644 --- a/pandas/core/arrays/string_arrow.py +++ b/pandas/core/arrays/string_arrow.py @@ -481,6 +481,9 @@ def _cmp_method(self, other, op): return result.to_numpy(np.bool_, na_value=False) return result + def __pos__(self) -> Self: + raise TypeError(f"bad operand type for unary +: '{self.dtype}'") + class ArrowStringArrayNumpySemantics(ArrowStringArray): _na_value = np.nan diff --git a/pandas/tests/frame/test_unary.py b/pandas/tests/frame/test_unary.py index 217255e73b450..652f52bd226af 100644 --- a/pandas/tests/frame/test_unary.py +++ b/pandas/tests/frame/test_unary.py @@ -3,9 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - -from pandas.compat import HAS_PYARROW from pandas.compat.numpy import np_version_gte1p25 import pandas as pd @@ -122,9 +119,6 @@ def test_pos_object(self, df_data): tm.assert_frame_equal(+df, df) tm.assert_series_equal(+df["a"], df["a"]) - @pytest.mark.xfail( - using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)" - ) @pytest.mark.filterwarnings("ignore:Applying:DeprecationWarning") def test_pos_object_raises(self): # GH#21380 From f95558fab024a3b2da7d7111a9bd75079287b385 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Wed, 22 Jan 2025 12:38:57 -0500 Subject: [PATCH 231/266] DOC: fix PR07,SA01 for pandas.arrays.TimedeltaArray (#60757) --- ci/code_checks.sh | 1 - pandas/core/arrays/timedeltas.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 948d8bee8ba5b..1b0e555c38b69 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -79,7 +79,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Timestamp.min PR02" \ -i "pandas.Timestamp.resolution PR02" \ -i "pandas.Timestamp.tzinfo GL08" \ - -i "pandas.arrays.TimedeltaArray PR07,SA01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index a8a0037d0bbb9..c5b3129c506c8 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -115,10 +115,10 @@ class TimedeltaArray(dtl.TimelikeOps): ---------- data : array-like The timedelta data. - dtype : numpy.dtype Currently, only ``numpy.dtype("timedelta64[ns]")`` is accepted. freq : Offset, optional + Frequency of the data. copy : bool, default False Whether to copy the underlying array of data. @@ -130,6 +130,12 @@ class TimedeltaArray(dtl.TimelikeOps): ------- None + See Also + -------- + Timedelta : Represents a duration, the difference between two dates or times. + TimedeltaIndex : Immutable Index of timedelta64 data. + to_timedelta : Convert argument to timedelta. + Examples -------- >>> pd.arrays.TimedeltaArray._from_sequence(pd.TimedeltaIndex(["1h", "2h"])) From 1039bd9aa2f090c5db5608843ce62807ed6e1e29 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Wed, 22 Jan 2025 12:39:35 -0500 Subject: [PATCH 232/266] DOC: fix RT03,SA01 for pandas.plotting.andrews_curves (#60759) --- ci/code_checks.sh | 1 - pandas/plotting/_misc.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 1b0e555c38b69..c7e644bd30cd3 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -83,7 +83,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ - -i "pandas.plotting.andrews_curves RT03,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ -i "pandas.tseries.offsets.BQuarterBegin.is_on_offset GL08" \ -i "pandas.tseries.offsets.BQuarterBegin.n GL08" \ diff --git a/pandas/plotting/_misc.py b/pandas/plotting/_misc.py index b20f8ac5f4796..3f839cefe798e 100644 --- a/pandas/plotting/_misc.py +++ b/pandas/plotting/_misc.py @@ -389,6 +389,12 @@ def andrews_curves( Returns ------- :class:`matplotlib.axes.Axes` + The matplotlib Axes object with the plot. + + See Also + -------- + plotting.parallel_coordinates : Plot parallel coordinates chart. + DataFrame.plot : Make plots of Series or DataFrame. Examples -------- From c168c0649169dd48b3349a3425c32640737cf070 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:53:47 -0500 Subject: [PATCH 233/266] DOC: Whatsnew for sorting mode result (#60718) * DOC: Whatsnew for sorting mode result * Reverts --- doc/source/whatsnew/v2.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index f2b6f70a3138c..de1118b56dc81 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -95,7 +95,7 @@ Timezones Numeric ^^^^^^^ -- +- Enabled :class:`Series.mode` and :class:`DataFrame.mode` with ``dropna=False`` to sort the result for all dtypes in the presence of NA values; previously only certain dtypes would sort (:issue:`60702`) - Conversion From b60e222634b759e0c36ee3f97ba83211e5017b76 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Wed, 22 Jan 2025 23:28:55 +0530 Subject: [PATCH 234/266] Miscellaneous updates for Pyodide 0.27: bump WASM CI and revise Arrow compatibility note (#60756) * Update Pyodide versions for CI * Git-ignore Pyodide xbuildenv folder * Pin to Pyodide 0.27.1 * Drop "WASM (pyodide and pyscript)" from Arrow compatibility notes * `TestCoercionFloat32.test_setitem` now xpasses --- .github/workflows/unit-tests.yml | 13 ++++++++----- .gitignore | 4 ++++ pandas/tests/series/indexing/test_setitem.py | 2 -- .../pdeps/0010-required-pyarrow-dependency.md | 1 - 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d2e2a170a1d04..842629ba331d6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -430,20 +430,20 @@ jobs: with: fetch-depth: 0 - - name: Set up Python for Pyodide + - name: Set up Python for pyodide-build id: setup-python uses: actions/setup-python@v5 with: - python-version: '3.11.3' + python-version: '3.12' - name: Set up Emscripten toolchain uses: mymindstorm/setup-emsdk@v14 with: - version: '3.1.46' + version: '3.1.58' actions-cache-folder: emsdk-cache - name: Install pyodide-build - run: pip install "pyodide-build==0.25.1" + run: pip install "pyodide-build>=0.29.2" - name: Build pandas for Pyodide run: | @@ -452,10 +452,13 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Set up Pyodide virtual environment + env: + pyodide-version: '0.27.1' run: | + pyodide xbuildenv install ${{ env.pyodide-version }} pyodide venv .venv-pyodide source .venv-pyodide/bin/activate pip install dist/*.whl diff --git a/.gitignore b/.gitignore index a188e216d9f70..d951f3fb9cbad 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,7 @@ doc/source/savefig/ # Interactive terminal generated files # ######################################## .jupyterlite.doit.db + +# Pyodide/WASM related files # +############################## +/.pyodide-xbuildenv-* diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index 158198239ba75..49c933c308235 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -9,7 +9,6 @@ import numpy as np import pytest -from pandas.compat import WASM from pandas.compat.numpy import np_version_gte1p24 from pandas.errors import IndexingError @@ -1449,7 +1448,6 @@ def obj(self): np_version_gte1p24 and os.environ.get("NPY_PROMOTION_STATE", "weak") != "weak" ) - or WASM ), reason="np.float32(1.1) ends up as 1.100000023841858, so " "np_can_hold_element raises and we cast to float64", diff --git a/web/pandas/pdeps/0010-required-pyarrow-dependency.md b/web/pandas/pdeps/0010-required-pyarrow-dependency.md index d586c46e243f8..0c3bf3c776988 100644 --- a/web/pandas/pdeps/0010-required-pyarrow-dependency.md +++ b/web/pandas/pdeps/0010-required-pyarrow-dependency.md @@ -185,7 +185,6 @@ Additionally, if a user is installing pandas in an environment where wheels are the user will need to also build Arrow C++ and related dependencies when installing from source. These environments include - Alpine linux (commonly used as a base for Docker containers) -- WASM (pyodide and pyscript) - Python development versions Lastly, pandas development and releases will need to be mindful of PyArrow's development and release cadance. For example when From fef01c5c58a72dd58e20c776bc30b21924131303 Mon Sep 17 00:00:00 2001 From: Jacob Lazar <129856302+Jacob-Lazar@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:11:45 +0530 Subject: [PATCH 235/266] DOC: add SPSS comparison guide structure (#60738) * DOC: add SPSS comparison guide structure - Create SPSS comparison documentation - Add header and introduction sections - Terminology translation table - Create template for common operations comparison Part of #60727 * DOC: edit SPSS comparison guide to documentation - Added file to doc/source/getting_started/comparison/index.rst toctree - Fixed formatting and whitespace issues to meet documentation standards * DOC: edit minor whitespaces in SPSS comparison guide * DOC: standardize class references in SPSS guide * DOC: Fix RST section underline lengths in SPSS comparison --------- Co-authored-by: jl_win_a --- .../comparison/comparison_with_spss.rst | 229 ++++++++++++++++++ .../getting_started/comparison/index.rst | 1 + 2 files changed, 230 insertions(+) create mode 100644 doc/source/getting_started/comparison/comparison_with_spss.rst diff --git a/doc/source/getting_started/comparison/comparison_with_spss.rst b/doc/source/getting_started/comparison/comparison_with_spss.rst new file mode 100644 index 0000000000000..12c64bfd180a3 --- /dev/null +++ b/doc/source/getting_started/comparison/comparison_with_spss.rst @@ -0,0 +1,229 @@ +.. _compare_with_spss: + +{{ header }} + +Comparison with SPSS +******************** +For potential users coming from `SPSS `__, this page is meant to demonstrate +how various SPSS operations would be performed using pandas. + +.. include:: includes/introduction.rst + +Data structures +--------------- + +General terminology translation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. csv-table:: + :header: "pandas", "SPSS" + :widths: 20, 20 + + :class:`DataFrame`, data file + column, variable + row, case + groupby, split file + :class:`NaN`, system-missing + +:class:`DataFrame` +~~~~~~~~~~~~~~~~~~ + +A :class:`DataFrame` in pandas is analogous to an SPSS data file - a two-dimensional +data source with labeled columns that can be of different types. As will be shown in this +document, almost any operation that can be performed in SPSS can also be accomplished in pandas. + +:class:`Series` +~~~~~~~~~~~~~~~ + +A :class:`Series` is the data structure that represents one column of a :class:`DataFrame`. SPSS doesn't have a +separate data structure for a single variable, but in general, working with a :class:`Series` is analogous +to working with a variable in SPSS. + +:class:`Index` +~~~~~~~~~~~~~~ + +Every :class:`DataFrame` and :class:`Series` has an :class:`Index` -- labels on the *rows* of the data. SPSS does not +have an exact analogue, as cases are simply numbered sequentially from 1. In pandas, if no index is +specified, a :class:`RangeIndex` is used by default (first row = 0, second row = 1, and so on). + +While using a labeled :class:`Index` or :class:`MultiIndex` can enable sophisticated analyses and is ultimately an +important part of pandas to understand, for this comparison we will essentially ignore the :class:`Index` and +just treat the :class:`DataFrame` as a collection of columns. Please see the :ref:`indexing documentation` +for much more on how to use an :class:`Index` effectively. + + +Copies vs. in place operations +------------------------------ + +.. include:: includes/copies.rst + + +Data input / output +------------------- + +Reading external data +~~~~~~~~~~~~~~~~~~~~~ + +Like SPSS, pandas provides utilities for reading in data from many formats. The ``tips`` dataset, found within +the pandas tests (`csv `_) +will be used in many of the following examples. + +In SPSS, you would use File > Open > Data to import a CSV file: + +.. code-block:: text + + FILE > OPEN > DATA + /TYPE=CSV + /FILE='tips.csv' + /DELIMITERS="," + /FIRSTCASE=2 + /VARIABLES=col1 col2 col3. + +The pandas equivalent would use :func:`read_csv`: + +.. code-block:: python + + url = ( + "https://raw.githubusercontent.com/pandas-dev" + "/pandas/main/pandas/tests/io/data/csv/tips.csv" + ) + tips = pd.read_csv(url) + tips + +Like SPSS's data import wizard, ``read_csv`` can take a number of parameters to specify how the data should be parsed. +For example, if the data was instead tab delimited, and did not have column names, the pandas command would be: + +.. code-block:: python + + tips = pd.read_csv("tips.csv", sep="\t", header=None) + + # alternatively, read_table is an alias to read_csv with tab delimiter + tips = pd.read_table("tips.csv", header=None) + + +Data operations +--------------- + +Filtering +~~~~~~~~~ + +In SPSS, filtering is done through Data > Select Cases: + +.. code-block:: text + + SELECT IF (total_bill > 10). + EXECUTE. + +In pandas, boolean indexing can be used: + +.. code-block:: python + + tips[tips["total_bill"] > 10] + + +Sorting +~~~~~~~ + +In SPSS, sorting is done through Data > Sort Cases: + +.. code-block:: text + + SORT CASES BY sex total_bill. + EXECUTE. + +In pandas, this would be written as: + +.. code-block:: python + + tips.sort_values(["sex", "total_bill"]) + + +String processing +----------------- + +Finding length of string +~~~~~~~~~~~~~~~~~~~~~~~~ + +In SPSS: + +.. code-block:: text + + COMPUTE length = LENGTH(time). + EXECUTE. + +.. include:: includes/length.rst + + +Changing case +~~~~~~~~~~~~~ + +In SPSS: + +.. code-block:: text + + COMPUTE upper = UPCASE(time). + COMPUTE lower = LOWER(time). + EXECUTE. + +.. include:: includes/case.rst + + +Merging +------- + +In SPSS, merging data files is done through Data > Merge Files. + +.. include:: includes/merge_setup.rst +.. include:: includes/merge.rst + + +GroupBy operations +------------------ + +Split-file processing +~~~~~~~~~~~~~~~~~~~~~ + +In SPSS, split-file analysis is done through Data > Split File: + +.. code-block:: text + + SORT CASES BY sex. + SPLIT FILE BY sex. + DESCRIPTIVES VARIABLES=total_bill tip + /STATISTICS=MEAN STDDEV MIN MAX. + +The pandas equivalent would be: + +.. code-block:: python + + tips.groupby("sex")[["total_bill", "tip"]].agg(["mean", "std", "min", "max"]) + + +Missing data +------------ + +SPSS uses the period (``.``) for numeric missing values and blank spaces for string missing values. +pandas uses ``NaN`` (Not a Number) for numeric missing values and ``None`` or ``NaN`` for string +missing values. + +.. include:: includes/missing.rst + + +Other considerations +-------------------- + +Output management +----------------- + +While pandas does not have a direct equivalent to SPSS's Output Management System (OMS), you can +capture and export results in various ways: + +.. code-block:: python + + # Save summary statistics to CSV + tips.groupby('sex')[['total_bill', 'tip']].mean().to_csv('summary.csv') + + # Save multiple results to Excel sheets + with pd.ExcelWriter('results.xlsx') as writer: + tips.describe().to_excel(writer, sheet_name='Descriptives') + tips.groupby('sex').mean().to_excel(writer, sheet_name='Means by Gender') diff --git a/doc/source/getting_started/comparison/index.rst b/doc/source/getting_started/comparison/index.rst index c3f58ce1f3d6d..3133d74afa3db 100644 --- a/doc/source/getting_started/comparison/index.rst +++ b/doc/source/getting_started/comparison/index.rst @@ -14,3 +14,4 @@ Comparison with other tools comparison_with_spreadsheets comparison_with_sas comparison_with_stata + comparison_with_spss From 1d33e4cedbb21b16917048358659bd96d1b8c8b6 Mon Sep 17 00:00:00 2001 From: Akshay Jain Date: Wed, 22 Jan 2025 13:28:29 -0800 Subject: [PATCH 236/266] BUG: Fixed TypeError for Series.isin() when large series and values contains NA (#60678) (#60736) * BUG: Fixed TypeError for Series.isin() when large series and values contains NA (#60678) * Add entry to whatsnew/v3.0.0.rst for bug fixing * Replaced np.vectorize() with any() for minor performance improvement and add new test cases * Fixed failed pre-commit.ci hooks : Formatting errors in algorithms.py, inconsistent-namespace-usage in test_isin.py, sorted whatsnew entry * Combined redundant if-statements to improve readability and performance --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/algorithms.py | 6 ++++++ pandas/tests/series/methods/test_isin.py | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index fea269ac4555e..517ac7a4b44b9 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -804,6 +804,7 @@ Other - Bug in :meth:`Index.sort_values` when passing a key function that turns values into tuples, e.g. ``key=natsort.natsort_key``, would raise ``TypeError`` (:issue:`56081`) - Bug in :meth:`Series.diff` allowing non-integer values for the ``periods`` argument. (:issue:`56607`) - Bug in :meth:`Series.dt` methods in :class:`ArrowDtype` that were returning incorrect values. (:issue:`57355`) +- Bug in :meth:`Series.isin` raising ``TypeError`` when series is large (>10**6) and ``values`` contains NA (:issue:`60678`) - Bug in :meth:`Series.rank` that doesn't preserve missing values for nullable integers when ``na_option='keep'``. (:issue:`56976`) - Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` inconsistently replacing matching instances when ``regex=True`` and missing values are present. (:issue:`56599`) - Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` throwing ``ValueError`` when ``regex=True`` and all NA values. (:issue:`60688`) diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index eefe08859c1e9..aafd802b827a5 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -23,6 +23,7 @@ iNaT, lib, ) +from pandas._libs.missing import NA from pandas._typing import ( AnyArrayLike, ArrayLike, @@ -544,10 +545,15 @@ def isin(comps: ListLike, values: ListLike) -> npt.NDArray[np.bool_]: # Ensure np.isin doesn't get object types or it *may* throw an exception # Albeit hashmap has O(1) look-up (vs. O(logn) in sorted array), # isin is faster for small sizes + + # GH60678 + # Ensure values don't contain , otherwise it throws exception with np.in1d + if ( len(comps_array) > _MINIMUM_COMP_ARR_LEN and len(values) <= 26 and comps_array.dtype != object + and not any(v is NA for v in values) ): # If the values include nan we need to check for nan explicitly # since np.nan it not equal to np.nan diff --git a/pandas/tests/series/methods/test_isin.py b/pandas/tests/series/methods/test_isin.py index e997ae32cf2e2..4f8484252ba8f 100644 --- a/pandas/tests/series/methods/test_isin.py +++ b/pandas/tests/series/methods/test_isin.py @@ -211,6 +211,30 @@ def test_isin_large_series_mixed_dtypes_and_nan(monkeypatch): tm.assert_series_equal(result, expected) +@pytest.mark.parametrize( + "dtype, data, values, expected", + [ + ("boolean", [pd.NA, False, True], [False, pd.NA], [True, True, False]), + ("Int64", [pd.NA, 2, 1], [1, pd.NA], [True, False, True]), + ("boolean", [pd.NA, False, True], [pd.NA, True, "a", 20], [True, False, True]), + ("boolean", [pd.NA, False, True], [], [False, False, False]), + ("Float64", [20.0, 30.0, pd.NA], [pd.NA], [False, False, True]), + ], +) +def test_isin_large_series_and_pdNA(dtype, data, values, expected, monkeypatch): + # https://github.com/pandas-dev/pandas/issues/60678 + # combination of large series (> _MINIMUM_COMP_ARR_LEN elements) and + # values contains pdNA + min_isin_comp = 2 + ser = Series(data, dtype=dtype) + expected = Series(expected, dtype="boolean") + + with monkeypatch.context() as m: + m.setattr(algorithms, "_MINIMUM_COMP_ARR_LEN", min_isin_comp) + result = ser.isin(values) + tm.assert_series_equal(result, expected) + + def test_isin_complex_numbers(): # GH 17927 array = [0, 1j, 1j, 1, 1 + 1j, 1 + 2j, 1 + 1j] From 4c3b968a0a4de483c00d15bd267bc776a218337e Mon Sep 17 00:00:00 2001 From: aaronchucarroll <120818400+aaronchucarroll@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:48:22 -0500 Subject: [PATCH 237/266] ENH: Series.str.get_dummies() raise on string type (#59786) --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/arrays/arrow/array.py | 2 -- pandas/core/strings/accessor.py | 5 +++- pandas/core/strings/object_array.py | 2 +- pandas/tests/strings/test_get_dummies.py | 38 ++++-------------------- 5 files changed, 11 insertions(+), 38 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 517ac7a4b44b9..1d8d0f6a74cb1 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -65,8 +65,8 @@ Other enhancements - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) - :meth:`Rolling.agg`, :meth:`Expanding.agg` and :meth:`ExponentialMovingWindow.agg` now accept :class:`NamedAgg` aggregations through ``**kwargs`` (:issue:`28333`) - :meth:`Series.map` can now accept kwargs to pass on to func (:issue:`59814`) +- :meth:`Series.str.get_dummies` now accepts a ``dtype`` parameter to specify the dtype of the resulting DataFrame (:issue:`47872`) - :meth:`pandas.concat` will raise a ``ValueError`` when ``ignore_index=True`` and ``keys`` is not ``None`` (:issue:`59274`) -- :meth:`str.get_dummies` now accepts a ``dtype`` parameter to specify the dtype of the resulting DataFrame (:issue:`47872`) - Implemented :meth:`Series.str.isascii` and :meth:`Series.str.isascii` (:issue:`59091`) - Multiplying two :class:`DateOffset` objects will now raise a ``TypeError`` instead of a ``RecursionError`` (:issue:`59442`) - Restore support for reading Stata 104-format and enable reading 103-format dta files (:issue:`58554`) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 5c32b05868383..e7f6b911f2fb1 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -2531,8 +2531,6 @@ def _str_get_dummies(self, sep: str = "|", dtype: NpDtype | None = None): else: dummies_dtype = np.bool_ dummies = np.zeros(n_rows * n_cols, dtype=dummies_dtype) - if dtype == str: - dummies[:] = False dummies[indices] = True dummies = dummies.reshape((n_rows, n_cols)) result = type(self)(pa.array(list(dummies))) diff --git a/pandas/core/strings/accessor.py b/pandas/core/strings/accessor.py index d3ccd11281a77..5b35b5e393012 100644 --- a/pandas/core/strings/accessor.py +++ b/pandas/core/strings/accessor.py @@ -29,6 +29,7 @@ is_extension_array_dtype, is_integer, is_list_like, + is_numeric_dtype, is_object_dtype, is_re, ) @@ -2524,10 +2525,12 @@ def get_dummies( """ from pandas.core.frame import DataFrame + if dtype is not None and not (is_numeric_dtype(dtype) or is_bool_dtype(dtype)): + raise ValueError("Only numeric or boolean dtypes are supported for 'dtype'") # we need to cast to Series of strings as only that has all # methods available for making the dummies... result, name = self._data.array._str_get_dummies(sep, dtype) - if is_extension_array_dtype(dtype) or isinstance(dtype, ArrowDtype): + if is_extension_array_dtype(dtype): return self._wrap_result( DataFrame(result, columns=name, dtype=dtype), name=name, diff --git a/pandas/core/strings/object_array.py b/pandas/core/strings/object_array.py index a07ab9534f491..0adb7b51cf2b7 100644 --- a/pandas/core/strings/object_array.py +++ b/pandas/core/strings/object_array.py @@ -434,7 +434,7 @@ def _str_get_dummies(self, sep: str = "|", dtype: NpDtype | None = None): dummies_dtype = _dtype else: dummies_dtype = np.bool_ - dummies = np.empty((len(arr), len(tags2)), dtype=dummies_dtype) + dummies = np.empty((len(arr), len(tags2)), dtype=dummies_dtype, order="F") def _isin(test_elements: str, element: str) -> bool: return element in test_elements diff --git a/pandas/tests/strings/test_get_dummies.py b/pandas/tests/strings/test_get_dummies.py index 541b0ea150ba6..16e10c6fcdccd 100644 --- a/pandas/tests/strings/test_get_dummies.py +++ b/pandas/tests/strings/test_get_dummies.py @@ -1,12 +1,9 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas.util._test_decorators as td from pandas import ( - ArrowDtype, DataFrame, Index, MultiIndex, @@ -14,11 +11,6 @@ _testing as tm, ) -try: - import pyarrow as pa -except ImportError: - pa = None - def test_get_dummies(any_string_dtype): s = Series(["a|b", "a|c", np.nan], dtype=any_string_dtype) @@ -99,32 +91,12 @@ def test_get_dummies_with_pyarrow_dtype(any_string_dtype, dtype): # GH#47872 -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_get_dummies_with_str_dtype(any_string_dtype): s = Series(["a|b", "a|c", np.nan], dtype=any_string_dtype) - result = s.str.get_dummies("|", dtype=str) - expected = DataFrame( - [["T", "T", "F"], ["T", "F", "T"], ["F", "F", "F"]], - columns=list("abc"), - dtype=str, - ) - tm.assert_frame_equal(result, expected) - -# GH#47872 -@td.skip_if_no("pyarrow") -def test_get_dummies_with_pa_str_dtype(any_string_dtype): - import pyarrow as pa + msg = "Only numeric or boolean dtypes are supported for 'dtype'" + with pytest.raises(ValueError, match=msg): + s.str.get_dummies("|", dtype=str) - s = Series(["a|b", "a|c", np.nan], dtype=any_string_dtype) - result = s.str.get_dummies("|", dtype=ArrowDtype(pa.string())) - expected = DataFrame( - [ - ["true", "true", "false"], - ["true", "false", "true"], - ["false", "false", "false"], - ], - columns=list("abc"), - dtype=ArrowDtype(pa.string()), - ) - tm.assert_frame_equal(result, expected) + with pytest.raises(ValueError, match=msg): + s.str.get_dummies("|", dtype="datetime64[ns]") From 60325b86e28edf40cb02444367efbc8deb2b5231 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Thu, 23 Jan 2025 02:38:26 -0500 Subject: [PATCH 238/266] ENH: Enable pytables to round-trip with StringDtype (#60663) Co-authored-by: William Ayd --- doc/source/whatsnew/v2.3.0.rst | 1 + pandas/io/pytables.py | 36 +++++++++++--- pandas/tests/io/pytables/test_put.py | 70 ++++++++++++++++++++++------ 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index de1118b56dc81..108ee62d88409 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -35,6 +35,7 @@ Other enhancements - The semantics for the ``copy`` keyword in ``__array__`` methods (i.e. called when using ``np.array()`` or ``np.asarray()`` on pandas objects) has been updated to work correctly with NumPy >= 2 (:issue:`57739`) +- :meth:`~Series.to_hdf` and :meth:`~DataFrame.to_hdf` now round-trip with ``StringDtype`` (:issue:`60663`) - The :meth:`~Series.cumsum`, :meth:`~Series.cummin`, and :meth:`~Series.cummax` reductions are now implemented for ``StringDtype`` columns when backed by PyArrow (:issue:`60633`) - The :meth:`~Series.sum` reduction is now implemented for ``StringDtype`` columns (:issue:`59853`) diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py index b75dc6c3a43b4..2f8096746318b 100644 --- a/pandas/io/pytables.py +++ b/pandas/io/pytables.py @@ -86,12 +86,16 @@ PeriodArray, ) from pandas.core.arrays.datetimes import tz_to_dtype +from pandas.core.arrays.string_ import BaseStringArray import pandas.core.common as com from pandas.core.computation.pytables import ( PyTablesExpr, maybe_expression, ) -from pandas.core.construction import extract_array +from pandas.core.construction import ( + array as pd_array, + extract_array, +) from pandas.core.indexes.api import ensure_index from pandas.io.common import stringify_path @@ -3023,6 +3027,9 @@ def read_array(self, key: str, start: int | None = None, stop: int | None = None if isinstance(node, tables.VLArray): ret = node[0][start:stop] + dtype = getattr(attrs, "value_type", None) + if dtype is not None: + ret = pd_array(ret, dtype=dtype) else: dtype = getattr(attrs, "value_type", None) shape = getattr(attrs, "shape", None) @@ -3262,6 +3269,11 @@ def write_array( elif lib.is_np_dtype(value.dtype, "m"): self._handle.create_array(self.group, key, value.view("i8")) getattr(self.group, key)._v_attrs.value_type = "timedelta64" + elif isinstance(value, BaseStringArray): + vlarr = self._handle.create_vlarray(self.group, key, _tables().ObjectAtom()) + vlarr.append(value.to_numpy()) + node = getattr(self.group, key) + node._v_attrs.value_type = str(value.dtype) elif empty_array: self.write_array_empty(key, value) else: @@ -3294,7 +3306,11 @@ def read( index = self.read_index("index", start=start, stop=stop) values = self.read_array("values", start=start, stop=stop) result = Series(values, index=index, name=self.name, copy=False) - if using_string_dtype() and is_string_array(values, skipna=True): + if ( + using_string_dtype() + and isinstance(values, np.ndarray) + and is_string_array(values, skipna=True) + ): result = result.astype(StringDtype(na_value=np.nan)) return result @@ -3363,7 +3379,11 @@ def read( columns = items[items.get_indexer(blk_items)] df = DataFrame(values.T, columns=columns, index=axes[1], copy=False) - if using_string_dtype() and is_string_array(values, skipna=True): + if ( + using_string_dtype() + and isinstance(values, np.ndarray) + and is_string_array(values, skipna=True) + ): df = df.astype(StringDtype(na_value=np.nan)) dfs.append(df) @@ -4737,9 +4757,13 @@ def read( df = DataFrame._from_arrays([values], columns=cols_, index=index_) if not (using_string_dtype() and values.dtype.kind == "O"): assert (df.dtypes == values.dtype).all(), (df.dtypes, values.dtype) - if using_string_dtype() and is_string_array( - values, # type: ignore[arg-type] - skipna=True, + if ( + using_string_dtype() + and isinstance(values, np.ndarray) + and is_string_array( + values, + skipna=True, + ) ): df = df.astype(StringDtype(na_value=np.nan)) frames.append(df) diff --git a/pandas/tests/io/pytables/test_put.py b/pandas/tests/io/pytables/test_put.py index a4257b54dd6db..66596f1138b96 100644 --- a/pandas/tests/io/pytables/test_put.py +++ b/pandas/tests/io/pytables/test_put.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas._libs.tslibs import Timestamp import pandas as pd @@ -26,7 +24,6 @@ pytestmark = [ pytest.mark.single_cpu, - pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False), ] @@ -54,8 +51,8 @@ def test_api_default_format(tmp_path, setup_path): with ensure_clean_store(setup_path) as store: df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=Index(list("ABCD"), dtype=object), - index=Index([f"i-{i}" for i in range(30)], dtype=object), + columns=Index(list("ABCD")), + index=Index([f"i-{i}" for i in range(30)]), ) with pd.option_context("io.hdf.default_format", "fixed"): @@ -79,8 +76,8 @@ def test_api_default_format(tmp_path, setup_path): path = tmp_path / setup_path df = DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=Index(list("ABCD"), dtype=object), - index=Index([f"i-{i}" for i in range(30)], dtype=object), + columns=Index(list("ABCD")), + index=Index([f"i-{i}" for i in range(30)]), ) with pd.option_context("io.hdf.default_format", "fixed"): @@ -106,7 +103,7 @@ def test_put(setup_path): ) df = DataFrame( np.random.default_rng(2).standard_normal((20, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=20, freq="B"), ) store["a"] = ts @@ -166,7 +163,7 @@ def test_put_compression(setup_path): with ensure_clean_store(setup_path) as store: df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) @@ -183,7 +180,7 @@ def test_put_compression(setup_path): def test_put_compression_blosc(setup_path): df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) @@ -197,10 +194,20 @@ def test_put_compression_blosc(setup_path): tm.assert_frame_equal(store["c"], df) -def test_put_mixed_type(setup_path, performance_warning): +def test_put_datetime_ser(setup_path, performance_warning, using_infer_string): + # https://github.com/pandas-dev/pandas/pull/60663 + ser = Series(3 * [Timestamp("20010102").as_unit("ns")]) + with ensure_clean_store(setup_path) as store: + store.put("ser", ser) + expected = ser.copy() + result = store.get("ser") + tm.assert_series_equal(result, expected) + + +def test_put_mixed_type(setup_path, performance_warning, using_infer_string): df = DataFrame( np.random.default_rng(2).standard_normal((10, 4)), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), index=date_range("2000-01-01", periods=10, freq="B"), ) df["obj1"] = "foo" @@ -220,13 +227,42 @@ def test_put_mixed_type(setup_path, performance_warning): with ensure_clean_store(setup_path) as store: _maybe_remove(store, "df") - with tm.assert_produces_warning(performance_warning): + warning = None if using_infer_string else performance_warning + with tm.assert_produces_warning(warning): store.put("df", df) expected = store.get("df") tm.assert_frame_equal(expected, df) +def test_put_str_frame(setup_path, performance_warning, string_dtype_arguments): + # https://github.com/pandas-dev/pandas/pull/60663 + dtype = pd.StringDtype(*string_dtype_arguments) + df = DataFrame({"a": pd.array(["x", pd.NA, "y"], dtype=dtype)}) + with ensure_clean_store(setup_path) as store: + _maybe_remove(store, "df") + + store.put("df", df) + expected_dtype = "str" if dtype.na_value is np.nan else "string" + expected = df.astype(expected_dtype) + result = store.get("df") + tm.assert_frame_equal(result, expected) + + +def test_put_str_series(setup_path, performance_warning, string_dtype_arguments): + # https://github.com/pandas-dev/pandas/pull/60663 + dtype = pd.StringDtype(*string_dtype_arguments) + ser = Series(["x", pd.NA, "y"], dtype=dtype) + with ensure_clean_store(setup_path) as store: + _maybe_remove(store, "df") + + store.put("ser", ser) + expected_dtype = "str" if dtype.na_value is np.nan else "string" + expected = ser.astype(expected_dtype) + result = store.get("ser") + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("format", ["table", "fixed"]) @pytest.mark.parametrize( "index", @@ -253,7 +289,7 @@ def test_store_index_types(setup_path, format, index): tm.assert_frame_equal(df, store["df"]) -def test_column_multiindex(setup_path): +def test_column_multiindex(setup_path, using_infer_string): # GH 4710 # recreate multi-indexes properly @@ -264,6 +300,12 @@ def test_column_multiindex(setup_path): expected = df.set_axis(df.index.to_numpy()) with ensure_clean_store(setup_path) as store: + if using_infer_string: + # TODO(infer_string) make this work for string dtype + msg = "Saving a MultiIndex with an extension dtype is not supported." + with pytest.raises(NotImplementedError, match=msg): + store.put("df", df) + return store.put("df", df) tm.assert_frame_equal( store["df"], expected, check_index_type=True, check_column_type=True From 222d7c7c5e3cc13d67facfa2d9bb7b6b03620a07 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 23 Jan 2025 17:03:46 +0100 Subject: [PATCH 239/266] TST (string dtype): follow-up fix for pyarrow 19.0 update (#60764) * TST (string dtype): follow-up fix for pyarrow 19.0 update * fix test --- pandas/tests/io/test_parquet.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 91580c31ea081..56a8e4c439164 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -104,10 +104,7 @@ def fp(request): @pytest.fixture def df_compat(): - # TODO(infer_string) should this give str columns? - return pd.DataFrame( - {"A": [1, 2, 3], "B": "foo"}, columns=pd.Index(["A", "B"], dtype=object) - ) + return pd.DataFrame({"A": [1, 2, 3], "B": "foo"}, columns=pd.Index(["A", "B"])) @pytest.fixture @@ -686,7 +683,11 @@ def test_parquet_read_from_url(self, httpserver, datapath, df_compat, engine): with open(datapath("io", "data", "parquet", "simple.parquet"), mode="rb") as f: httpserver.serve_content(content=f.read()) df = read_parquet(httpserver.url, engine=engine) - tm.assert_frame_equal(df, df_compat) + + expected = df_compat + if pa_version_under19p0: + expected.columns = expected.columns.astype(object) + tm.assert_frame_equal(df, expected) class TestParquetPyArrow(Base): From be538ef0d07055113cbdbf9b3a22c4852c7fd6d7 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Thu, 23 Jan 2025 23:49:44 +0530 Subject: [PATCH 240/266] =?UTF-8?q?DOC:=20fix=20ES01,SA01=20for=20pandas.t?= =?UTF-8?q?series.offsets.CustomBusinessMonthEnd.=E2=80=A6=20(#60775)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOC: fix ES01,SA01 for pandas.tseries.offsets.CustomBusinessMonthEnd.is_on_offset and pandas.tseries.offsets.CustomBusinessMonthBegin.is_on_offset --- ci/code_checks.sh | 2 -- pandas/_libs/tslibs/offsets.pyx | 13 +++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index c7e644bd30cd3..cf7809c70296c 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -146,7 +146,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.tseries.offsets.CustomBusinessMonthBegin PR02" \ -i "pandas.tseries.offsets.CustomBusinessMonthBegin.calendar GL08" \ -i "pandas.tseries.offsets.CustomBusinessMonthBegin.holidays GL08" \ - -i "pandas.tseries.offsets.CustomBusinessMonthBegin.is_on_offset SA01" \ -i "pandas.tseries.offsets.CustomBusinessMonthBegin.m_offset GL08" \ -i "pandas.tseries.offsets.CustomBusinessMonthBegin.n GL08" \ -i "pandas.tseries.offsets.CustomBusinessMonthBegin.normalize GL08" \ @@ -154,7 +153,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.tseries.offsets.CustomBusinessMonthEnd PR02" \ -i "pandas.tseries.offsets.CustomBusinessMonthEnd.calendar GL08" \ -i "pandas.tseries.offsets.CustomBusinessMonthEnd.holidays GL08" \ - -i "pandas.tseries.offsets.CustomBusinessMonthEnd.is_on_offset SA01" \ -i "pandas.tseries.offsets.CustomBusinessMonthEnd.m_offset GL08" \ -i "pandas.tseries.offsets.CustomBusinessMonthEnd.n GL08" \ -i "pandas.tseries.offsets.CustomBusinessMonthEnd.normalize GL08" \ diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 7569f8e8864a0..3b02bf46c2f82 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -720,11 +720,24 @@ cdef class BaseOffset: """ Return boolean whether a timestamp intersects with this frequency. + This method determines if a given timestamp aligns with the start + of a custom business month, as defined by this offset. It accounts + for custom rules, such as skipping weekends or other non-business days, + and checks whether the provided datetime falls on a valid business day + that marks the beginning of the custom business month. + Parameters ---------- dt : datetime.datetime Timestamp to check intersections with frequency. + See Also + -------- + tseries.offsets.CustomBusinessMonthBegin : Represents the start of a custom + business month. + tseries.offsets.CustomBusinessMonthEnd : Represents the end of a custom + business month. + Examples -------- >>> ts = pd.Timestamp(2022, 1, 1) From 0c4ca3a9e4baa9b4fa8cbc81c57f2e2996636c10 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Fri, 24 Jan 2025 00:23:55 +0530 Subject: [PATCH 241/266] DOC: fix SA01 for pandas.tseries.offsets.LastWeekOfMonth (#60776) --- ci/code_checks.sh | 1 - pandas/_libs/tslibs/offsets.pyx | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index cf7809c70296c..2d0fcce47d2a5 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -189,7 +189,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.tseries.offsets.Hour.is_on_offset GL08" \ -i "pandas.tseries.offsets.Hour.n GL08" \ -i "pandas.tseries.offsets.Hour.normalize GL08" \ - -i "pandas.tseries.offsets.LastWeekOfMonth SA01" \ -i "pandas.tseries.offsets.LastWeekOfMonth.is_on_offset GL08" \ -i "pandas.tseries.offsets.LastWeekOfMonth.n GL08" \ -i "pandas.tseries.offsets.LastWeekOfMonth.normalize GL08" \ diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 3b02bf46c2f82..36b431974c121 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -3723,6 +3723,15 @@ cdef class LastWeekOfMonth(WeekOfMonthMixin): - 5 is Saturday - 6 is Sunday. + See Also + -------- + tseries.offsets.WeekOfMonth : + Date offset for a specific weekday in a month. + tseries.offsets.MonthEnd : + Date offset for the end of the month. + tseries.offsets.BMonthEnd : + Date offset for the last business day of the month. + Examples -------- >>> ts = pd.Timestamp(2022, 1, 1) From c168883f8f10e312e6d596d8d750a1e4647393c6 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:04:42 -0800 Subject: [PATCH 242/266] PERF: Avoid a numpy array copy in ArrowExtensionArray._to_datetimearray (#60778) --- pandas/core/arrays/arrow/array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index e7f6b911f2fb1..0b546bed1c2b7 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1398,7 +1398,7 @@ def _to_datetimearray(self) -> DatetimeArray: np_dtype = np.dtype(f"M8[{pa_type.unit}]") dtype = tz_to_dtype(pa_type.tz, pa_type.unit) np_array = self._pa_array.to_numpy() - np_array = np_array.astype(np_dtype) + np_array = np_array.astype(np_dtype, copy=False) return DatetimeArray._simple_new(np_array, dtype=dtype) def _to_timedeltaarray(self) -> TimedeltaArray: @@ -1409,7 +1409,7 @@ def _to_timedeltaarray(self) -> TimedeltaArray: assert pa.types.is_duration(pa_type) np_dtype = np.dtype(f"m8[{pa_type.unit}]") np_array = self._pa_array.to_numpy() - np_array = np_array.astype(np_dtype) + np_array = np_array.astype(np_dtype, copy=False) return TimedeltaArray._simple_new(np_array, dtype=np_dtype) def _values_for_json(self) -> np.ndarray: From d38706af66249ef74e42671a480261c68bedfbce Mon Sep 17 00:00:00 2001 From: William Ayd Date: Fri, 24 Jan 2025 15:21:29 -0500 Subject: [PATCH 243/266] TST(string dtype): Fix xfails in test_block_internals.py (#60765) --- pandas/tests/frame/conftest.py | 2 +- .../frame/constructors/test_from_dict.py | 1 - pandas/tests/frame/test_block_internals.py | 35 ++++++------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/pandas/tests/frame/conftest.py b/pandas/tests/frame/conftest.py index ea8e2e8ecc194..b3140bad8276b 100644 --- a/pandas/tests/frame/conftest.py +++ b/pandas/tests/frame/conftest.py @@ -33,7 +33,7 @@ def float_string_frame(): df = DataFrame( np.random.default_rng(2).standard_normal((30, 4)), index=Index([f"foo_{i}" for i in range(30)], dtype=object), - columns=Index(list("ABCD"), dtype=object), + columns=Index(list("ABCD")), ) df["foo"] = "bar" return df diff --git a/pandas/tests/frame/constructors/test_from_dict.py b/pandas/tests/frame/constructors/test_from_dict.py index fc7c03dc25839..1509c47ba65c7 100644 --- a/pandas/tests/frame/constructors/test_from_dict.py +++ b/pandas/tests/frame/constructors/test_from_dict.py @@ -108,7 +108,6 @@ def test_constructor_list_of_series(self): expected = DataFrame.from_dict(sdict, orient="index") tm.assert_frame_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="columns inferring logic broken") def test_constructor_orient(self, float_string_frame): data_dict = float_string_frame.T._series recons = DataFrame.from_dict(data_dict, orient="index") diff --git a/pandas/tests/frame/test_block_internals.py b/pandas/tests/frame/test_block_internals.py index 25e66a0e1c03d..6fdbfac8f4e0a 100644 --- a/pandas/tests/frame/test_block_internals.py +++ b/pandas/tests/frame/test_block_internals.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( Categorical, @@ -162,21 +160,7 @@ def test_constructor_with_convert(self): ) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_construction_with_mixed(self, float_string_frame, using_infer_string): - # test construction edge cases with mixed types - - # f7u12, this does not work without extensive workaround - data = [ - [datetime(2001, 1, 5), np.nan, datetime(2001, 1, 2)], - [datetime(2000, 1, 2), datetime(2000, 1, 3), datetime(2000, 1, 1)], - ] - df = DataFrame(data) - - # check dtypes - result = df.dtypes - expected = Series({"datetime64[us]": 3}) - # mixed-type frames float_string_frame["datetime"] = datetime.now() float_string_frame["timedelta"] = timedelta(days=1, seconds=1) @@ -196,13 +180,11 @@ def test_construction_with_mixed(self, float_string_frame, using_infer_string): ) tm.assert_series_equal(result, expected) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_construction_with_conversions(self): # convert from a numpy array of non-ns timedelta64; as of 2.0 this does # *not* convert arr = np.array([1, 2, 3], dtype="timedelta64[s]") - df = DataFrame(index=range(3)) - df["A"] = arr + df = DataFrame({"A": arr}) expected = DataFrame( {"A": pd.timedelta_range("00:00:01", periods=3, freq="s")}, index=range(3) ) @@ -220,11 +202,11 @@ def test_construction_with_conversions(self): assert expected.dtypes["dt1"] == "M8[s]" assert expected.dtypes["dt2"] == "M8[s]" - df = DataFrame(index=range(3)) - df["dt1"] = np.datetime64("2013-01-01") - df["dt2"] = np.array( + dt1 = np.datetime64("2013-01-01") + dt2 = np.array( ["2013-01-01", "2013-01-02", "2013-01-03"], dtype="datetime64[D]" ) + df = DataFrame({"dt1": dt1, "dt2": dt2}) # df['dt3'] = np.array(['2013-01-01 00:00:01','2013-01-01 # 00:00:02','2013-01-01 00:00:03'],dtype='datetime64[s]') @@ -401,14 +383,17 @@ def test_update_inplace_sets_valid_block_values(): assert isinstance(df._mgr.blocks[0].values, Categorical) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") def test_nonconsolidated_item_cache_take(): # https://github.com/pandas-dev/pandas/issues/35521 # create non-consolidated dataframe with object dtype columns - df = DataFrame() - df["col1"] = Series(["a"], dtype=object) + df = DataFrame( + { + "col1": Series(["a"], dtype=object), + } + ) df["col2"] = Series([0], dtype=object) + assert not df._mgr.is_consolidated() # access column (item cache) df["col1"] == "A" From 354b61f88bc0523d4bb9f3cfe1d6c12f9a3d6567 Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:39:30 -0500 Subject: [PATCH 244/266] TST(string dtype): Resolve xfail in groupby.test_size (#60711) --- pandas/tests/groupby/methods/test_size.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pandas/tests/groupby/methods/test_size.py b/pandas/tests/groupby/methods/test_size.py index 2dc89bc75746f..6664563bd2272 100644 --- a/pandas/tests/groupby/methods/test_size.py +++ b/pandas/tests/groupby/methods/test_size.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, Index, @@ -76,18 +74,16 @@ def test_size_series_masked_type_returns_Int64(dtype): tm.assert_series_equal(result, expected) -# TODO(infer_string) in case the column is object dtype, it should preserve that dtype -# for the result's index -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) -def test_size_strings(any_string_dtype): +def test_size_strings(any_string_dtype, using_infer_string): # GH#55627 dtype = any_string_dtype df = DataFrame({"a": ["a", "a", "b"], "b": "a"}, dtype=dtype) result = df.groupby("a")["b"].size() exp_dtype = "Int64" if dtype == "string[pyarrow]" else "int64" + exp_index_dtype = "str" if using_infer_string and dtype == "object" else dtype expected = Series( [2, 1], - index=Index(["a", "b"], name="a", dtype=dtype), + index=Index(["a", "b"], name="a", dtype=exp_index_dtype), name="b", dtype=exp_dtype, ) From e3b2de852a87dc7b530302e0039730e7745b2fcf Mon Sep 17 00:00:00 2001 From: William Ayd Date: Fri, 24 Jan 2025 18:16:18 -0500 Subject: [PATCH 245/266] TST(string_dtype): Fix minor issue with CSV parser and column dtype (#60784) --- pandas/io/parsers/arrow_parser_wrapper.py | 3 ++- pandas/tests/io/parser/common/test_index.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pandas/io/parsers/arrow_parser_wrapper.py b/pandas/io/parsers/arrow_parser_wrapper.py index 672672490996d..8cadde1ad6537 100644 --- a/pandas/io/parsers/arrow_parser_wrapper.py +++ b/pandas/io/parsers/arrow_parser_wrapper.py @@ -165,7 +165,8 @@ def _finalize_pandas_output(self, frame: DataFrame) -> DataFrame: # The only way self.names is not the same length as number of cols is # if we have int index_col. We should just pad the names(they will get # removed anyways) to expected length then. - self.names = list(range(num_cols - len(self.names))) + self.names + columns_prefix = [str(x) for x in range(num_cols - len(self.names))] + self.names = columns_prefix + self.names multi_index_named = False frame.columns = self.names diff --git a/pandas/tests/io/parser/common/test_index.py b/pandas/tests/io/parser/common/test_index.py index 8352cc80f5e62..cfa8785b24bde 100644 --- a/pandas/tests/io/parser/common/test_index.py +++ b/pandas/tests/io/parser/common/test_index.py @@ -90,9 +90,6 @@ def test_pass_names_with_index(all_parsers, data, kwargs, expected): def test_multi_index_no_level_names( request, all_parsers, index_col, using_infer_string ): - if using_infer_string and all_parsers.engine == "pyarrow": - # result should have string columns instead of object dtype - request.applymarker(pytest.mark.xfail(reason="TODO(infer_string)")) data = """index1,index2,A,B,C,D foo,one,2,3,4,5 foo,two,7,8,9,10 From 8fe27200e2d4ba1f9781f704becf889d7aa43c28 Mon Sep 17 00:00:00 2001 From: "Christine P. Chai" Date: Sat, 25 Jan 2025 09:00:26 -0800 Subject: [PATCH 246/266] DOC: Update a link in tutorials.rst (#60787) --- doc/source/getting_started/tutorials.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/getting_started/tutorials.rst b/doc/source/getting_started/tutorials.rst index 4393c3716bdad..eae7771418485 100644 --- a/doc/source/getting_started/tutorials.rst +++ b/doc/source/getting_started/tutorials.rst @@ -112,7 +112,7 @@ Various tutorials * `Wes McKinney's (pandas BDFL) blog `_ * `Statistical analysis made easy in Python with SciPy and pandas DataFrames, by Randal Olson `_ -* `Statistical Data Analysis in Python, tutorial videos, by Christopher Fonnesbeck from SciPy 2013 `_ +* `Statistical Data Analysis in Python, tutorial by Christopher Fonnesbeck from SciPy 2013 `_ * `Financial analysis in Python, by Thomas Wiecki `_ * `Intro to pandas data structures, by Greg Reda `_ * `Pandas DataFrames Tutorial, by Karlijn Willems `_ From f3045db91dbb89306c15b1673987cc70912a76b5 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:44:11 -0800 Subject: [PATCH 247/266] CI: Remove CircleCI in favor of GHA ARM builds (#60761) --- .circleci/config.yml | 155 ---------------------------------- .gitattributes | 1 - ci/deps/circle-311-arm64.yaml | 61 ------------- pandas/tests/io/conftest.py | 7 +- 4 files changed, 3 insertions(+), 221 deletions(-) delete mode 100644 .circleci/config.yml delete mode 100644 ci/deps/circle-311-arm64.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 139ea9d220453..0000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,155 +0,0 @@ -version: 2.1 - -jobs: - test-linux-arm: - machine: - image: default - resource_class: arm.large - environment: - ENV_FILE: ci/deps/circle-311-arm64.yaml - PYTEST_WORKERS: auto - PATTERN: "not single_cpu and not slow and not network and not clipboard and not arm_slow and not db" - PYTEST_TARGET: "pandas" - PANDAS_CI: "1" - steps: - - checkout - - run: - name: Install Environment and Run Tests - shell: /bin/bash -exo pipefail - # https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions-azure-pipelines-travis-ci-and-gitlab-ci-cd - command: | - MINI_URL="https://github.com/conda-forge/miniforge/releases/download/24.3.0-0/Miniforge3-24.3.0-0-Linux-aarch64.sh" - wget -q $MINI_URL -O Miniforge3.sh - chmod +x Miniforge3.sh - MINI_DIR="$HOME/miniconda3" - rm -rf $MINI_DIR - ./Miniforge3.sh -b -p $MINI_DIR - export PATH=$MINI_DIR/bin:$PATH - conda info -a - conda env create -q -n pandas-dev -f $ENV_FILE - conda list -n pandas-dev - source activate pandas-dev - if pip show pandas 1>/dev/null; then - pip uninstall -y pandas - fi - python -m pip install --no-build-isolation -ve . -Csetup-args="--werror" - PATH=$HOME/miniconda3/envs/pandas-dev/bin:$HOME/miniconda3/condabin:$PATH - ci/run_tests.sh - test-linux-musl: - docker: - - image: quay.io/pypa/musllinux_1_1_aarch64 - resource_class: arm.large - steps: - # Install pkgs first to have git in the image - # (needed for checkout) - - run: - name: Install System Packages - command: | - apk update - apk add git - apk add musl-locales - - checkout - - run: - name: Install Environment and Run Tests - command: | - /opt/python/cp311-cp311/bin/python -m venv ~/virtualenvs/pandas-dev - . ~/virtualenvs/pandas-dev/bin/activate - python -m pip install --no-cache-dir -U pip wheel setuptools meson-python==0.13.1 meson[ninja]==1.2.1 - python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil pytest>=7.3.2 pytest-xdist>=3.4.0 hypothesis>=6.84.0 - python -m pip install --no-cache-dir --no-build-isolation -e . -Csetup-args="--werror" - python -m pip list --no-cache-dir - export PANDAS_CI=1 - python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml - build-aarch64: - parameters: - cibw-build: - type: string - machine: - image: default - resource_class: arm.large - environment: - TRIGGER_SOURCE: << pipeline.trigger_source >> - steps: - - checkout - - run: - name: Check if build is necessary - command: | - # Check if tag is defined or TRIGGER_SOURCE is scheduled - if [[ -n "$CIRCLE_TAG" ]]; then - echo 'export IS_PUSH="true"' >> "$BASH_ENV" - elif [[ $TRIGGER_SOURCE == "scheduled_pipeline" ]]; then - echo 'export IS_SCHEDULE_DISPATCH="true"' >> "$BASH_ENV" - # Look for the build label/[wheel build] in commit - # grep takes a regex, so need to escape brackets - elif (git log --format=oneline -n 1 $CIRCLE_SHA1) | grep -q '\[wheel build\]'; then - : # Do nothing - elif ! (curl https://api.github.com/repos/pandas-dev/pandas/issues/$CIRCLE_PR_NUMBER | jq '.labels' | grep -q 'Build'); then - circleci-agent step halt - fi - - run: - name: Build aarch64 wheels - no_output_timeout: 30m # Sometimes the tests won't generate any output, make sure the job doesn't get killed by that - command: | - pip3 install cibuildwheel==2.20.0 - if [[ $CIBW_BUILD == cp313t* ]]; then - # TODO: temporarily run 3.13 free threaded builds without build isolation - # since we need pre-release cython - CIBW_BUILD_FRONTEND="pip; args: --no-build-isolation" cibuildwheel --output-dir wheelhouse - else - cibuildwheel --output-dir wheelhouse - fi - - environment: - CIBW_BUILD: << parameters.cibw-build >> - - - run: - name: Install Anaconda Client & Upload Wheels - shell: /bin/bash -exo pipefail - command: | - MINI_URL="https://github.com/conda-forge/miniforge/releases/download/24.3.0-0/Miniforge3-24.3.0-0-Linux-aarch64.sh" - wget -q $MINI_URL -O Miniforge3.sh - chmod +x Miniforge3.sh - MINI_DIR="$HOME/miniconda3" - rm -rf $MINI_DIR - ./Miniforge3.sh -b -p $MINI_DIR - export PATH=$MINI_DIR/bin:$PATH - conda install -y -c conda-forge anaconda-client - source ci/upload_wheels.sh - set_upload_vars - upload_wheels - - store_artifacts: - path: wheelhouse/ - -workflows: - test: - # Don't run trigger this one when scheduled pipeline runs - when: - not: - equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] - jobs: - - test-linux-arm - test-musl: - # Don't run trigger this one when scheduled pipeline runs - when: - not: - equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] - jobs: - - test-linux-musl - build-wheels: - jobs: - - build-aarch64: - filters: - tags: - only: /^v.*/ - matrix: - parameters: - cibw-build: ["cp310-manylinux_aarch64", - "cp311-manylinux_aarch64", - "cp312-manylinux_aarch64", - "cp313-manylinux_aarch64", - "cp313t-manylinux_aarch64", - "cp310-musllinux_aarch64", - "cp311-musllinux_aarch64", - "cp312-musllinux_aarch64", - "cp313-musllinux_aarch64", - "cp313t-musllinux_aarch64"] diff --git a/.gitattributes b/.gitattributes index f77da2339b20f..d94c19e7edb1f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -61,7 +61,6 @@ pandas/_version.py export-subst *.pxi export-ignore # Ignoring stuff from the top level -.circleci export-ignore .github export-ignore asv_bench export-ignore ci export-ignore diff --git a/ci/deps/circle-311-arm64.yaml b/ci/deps/circle-311-arm64.yaml deleted file mode 100644 index 3f09e27d0fe4b..0000000000000 --- a/ci/deps/circle-311-arm64.yaml +++ /dev/null @@ -1,61 +0,0 @@ -name: pandas-dev -channels: - - conda-forge -dependencies: - - python=3.11 - - # build dependencies - - versioneer - - cython>=0.29.33 - - meson=1.2.1 - - meson-python=0.13.1 - - # test dependencies - - pytest>=7.3.2 - - pytest-cov - - pytest-xdist>=3.4.0 - - pytest-localserver>=0.8.1 - - pytest-qt>=4.4.0 - - boto3 - - # required dependencies - - python-dateutil - - numpy - - # optional dependencies - - beautifulsoup4>=4.11.2 - - blosc>=1.21.3 - - bottleneck>=1.3.6 - - fastparquet>=2023.10.0 - - fsspec>=2022.11.0 - - html5lib>=1.1 - - hypothesis>=6.84.0 - - gcsfs>=2022.11.0 - - jinja2>=3.1.2 - - lxml>=4.9.2 - - matplotlib>=3.6.3 - - numba>=0.56.4 - - numexpr>=2.8.4 - - odfpy>=1.4.1 - - qtpy>=2.3.0 - - openpyxl>=3.1.0 - - psycopg2>=2.9.6 - - pyarrow>=10.0.1 - - pymysql>=1.0.2 - - pyqt>=5.15.9 - - pyreadstat>=1.2.0 - - pytables>=3.8.0 - - python-calamine>=0.1.7 - - pytz>=2023.4 - - pyxlsb>=1.0.10 - - s3fs>=2022.11.0 - - scipy>=1.10.0 - - sqlalchemy>=2.0.0 - - tabulate>=0.9.0 - - xarray>=2022.12.0, <2024.10.0 - - xlrd>=2.0.1 - - xlsxwriter>=3.0.5 - - zstandard>=0.19.0 - - pip: - - adbc-driver-postgresql>=0.8.0 - - adbc-driver-sqlite>=0.8.0 diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py index bdefadf3dbec0..a5ddda9d66e7a 100644 --- a/pandas/tests/io/conftest.py +++ b/pandas/tests/io/conftest.py @@ -67,14 +67,13 @@ def s3_base(worker_id, monkeypatch): monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "foobar_secret") if is_ci_environment(): if is_platform_arm() or is_platform_mac() or is_platform_windows(): - # NOT RUN on Windows/macOS/ARM, only Ubuntu + # NOT RUN on Windows/macOS, only Ubuntu # - subprocess in CI can cause timeouts # - GitHub Actions do not support # container services for the above OSs - # - CircleCI will probably hit the Docker rate pull limit pytest.skip( - "S3 tests do not have a corresponding service in " - "Windows, macOS or ARM platforms" + "S3 tests do not have a corresponding service on " + "Windows or macOS platforms" ) else: # set in .github/workflows/unit-tests.yml From 84bf1ef82912ebf497a304b0ffd90914bfc41ea9 Mon Sep 17 00:00:00 2001 From: tasfia8 <117693390+tasfia8@users.noreply.github.com> Date: Sun, 26 Jan 2025 06:29:25 -0500 Subject: [PATCH 248/266] BUG: fix construction of Series / Index from dict keys when "str" dtype is specified explicitly (#60436) Co-authored-by: Joris Van den Bossche --- pandas/core/construction.py | 2 ++ pandas/tests/base/test_constructors.py | 11 +++++++++++ pandas/tests/io/test_fsspec.py | 1 - 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pandas/core/construction.py b/pandas/core/construction.py index 8df4f7e3e08f9..50088804e0245 100644 --- a/pandas/core/construction.py +++ b/pandas/core/construction.py @@ -596,6 +596,8 @@ def sanitize_array( # create an extension array from its dtype _sanitize_non_ordered(data) cls = dtype.construct_array_type() + if not hasattr(data, "__array__"): + data = list(data) subarr = cls._from_sequence(data, dtype=dtype, copy=copy) # GH#846 diff --git a/pandas/tests/base/test_constructors.py b/pandas/tests/base/test_constructors.py index c4b02423f8cf0..dffd2009ef373 100644 --- a/pandas/tests/base/test_constructors.py +++ b/pandas/tests/base/test_constructors.py @@ -179,3 +179,14 @@ def test_constructor_datetime_nonns(self, constructor): arr.flags.writeable = False result = constructor(arr) tm.assert_equal(result, expected) + + def test_constructor_from_dict_keys(self, constructor, using_infer_string): + # https://github.com/pandas-dev/pandas/issues/60343 + d = {"a": 1, "b": 2} + result = constructor(d.keys(), dtype="str") + if using_infer_string: + assert result.dtype == "str" + else: + assert result.dtype == "object" + expected = constructor(list(d.keys()), dtype="str") + tm.assert_equal(result, expected) diff --git a/pandas/tests/io/test_fsspec.py b/pandas/tests/io/test_fsspec.py index 5340560884afe..2e3e74a9d31ff 100644 --- a/pandas/tests/io/test_fsspec.py +++ b/pandas/tests/io/test_fsspec.py @@ -209,7 +209,6 @@ def test_arrowparquet_options(fsspectest): assert fsspectest.test[0] == "parquet_read" -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string) fastparquet") def test_fastparquet_options(fsspectest): """Regression test for writing to a not-yet-existent GCS Parquet file.""" pytest.importorskip("fastparquet") From e36b00035665d416fe10a3950880a6532eaf6131 Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Mon, 27 Jan 2025 22:54:25 +0200 Subject: [PATCH 249/266] BUG: fix combine_first reorders columns (#60791) * Add test * Fix combine_first reorders columns * Add whatsnew * Fix corner case when self is empty and future.infer_string is True * Update --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/frame.py | 5 +++-- pandas/tests/frame/methods/test_combine_first.py | 12 +++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1d8d0f6a74cb1..a7f63d75a047e 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -758,6 +758,7 @@ Groupby/resample/rolling Reshaping ^^^^^^^^^ - Bug in :func:`qcut` where values at the quantile boundaries could be incorrectly assigned (:issue:`59355`) +- Bug in :meth:`DataFrame.combine_first` not preserving the column order (:issue:`60427`) - Bug in :meth:`DataFrame.join` inconsistently setting result index name (:issue:`55815`) - Bug in :meth:`DataFrame.join` when a :class:`DataFrame` with a :class:`MultiIndex` would raise an ``AssertionError`` when :attr:`MultiIndex.names` contained ``None``. (:issue:`58721`) - Bug in :meth:`DataFrame.merge` where merging on a column containing only ``NaN`` values resulted in an out-of-bounds array access (:issue:`59421`) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index af66bb54610f1..3669d8249dd27 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -8671,6 +8671,7 @@ def combine( 2 NaN 3.0 1.0 """ other_idxlen = len(other.index) # save for compare + other_columns = other.columns this, other = self.align(other) new_index = this.index @@ -8681,8 +8682,8 @@ def combine( if self.empty and len(other) == other_idxlen: return other.copy() - # sorts if possible; otherwise align above ensures that these are set-equal - new_columns = this.columns.union(other.columns) + # preserve column order + new_columns = self.columns.union(other_columns, sort=False) do_fill = fill_value is not None result = {} for col in new_columns: diff --git a/pandas/tests/frame/methods/test_combine_first.py b/pandas/tests/frame/methods/test_combine_first.py index a70876b5a96ca..1e594043510ea 100644 --- a/pandas/tests/frame/methods/test_combine_first.py +++ b/pandas/tests/frame/methods/test_combine_first.py @@ -380,7 +380,7 @@ def test_combine_first_with_asymmetric_other(self, val): df2 = DataFrame({"isBool": [True]}) res = df1.combine_first(df2) - exp = DataFrame({"isBool": [True], "isNum": [val]}) + exp = DataFrame({"isNum": [val], "isBool": [True]}) tm.assert_frame_equal(res, exp) @@ -555,3 +555,13 @@ def test_combine_first_empty_columns(): result = left.combine_first(right) expected = DataFrame(columns=["a", "b", "c"]) tm.assert_frame_equal(result, expected) + + +def test_combine_first_preserve_column_order(): + # GH#60427 + df1 = DataFrame({"B": [1, 2, 3], "A": [4, None, 6]}) + df2 = DataFrame({"A": [5]}, index=[1]) + + result = df1.combine_first(df2) + expected = DataFrame({"B": [1, 2, 3], "A": [4.0, 5.0, 6.0]}) + tm.assert_frame_equal(result, expected) From c0c778bdb75a54cf03cdfe76f5b3dadae6a67054 Mon Sep 17 00:00:00 2001 From: Matteo Paltenghi Date: Tue, 28 Jan 2025 00:29:37 +0100 Subject: [PATCH 250/266] TST: Add test for exceptional behavior when calling `view()` on `BaseStringArray` (#60799) * add test for when str array raises type error * fix formatting: ruff-format * moved test to tests/arrays/string_/test_string.py file --- pandas/tests/arrays/string_/test_string.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/tests/arrays/string_/test_string.py b/pandas/tests/arrays/string_/test_string.py index f875873863b4d..336a0fef69170 100644 --- a/pandas/tests/arrays/string_/test_string.py +++ b/pandas/tests/arrays/string_/test_string.py @@ -758,3 +758,9 @@ def test_tolist(dtype): result = arr.tolist() expected = vals tm.assert_equal(result, expected) + + +def test_string_array_view_type_error(): + arr = pd.array(["a", "b", "c"], dtype="string") + with pytest.raises(TypeError, match="Cannot change data-type for string array."): + arr.view("i8") From 8973c551895c2cd3619cadf554362e802b27e02a Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Tue, 28 Jan 2025 01:59:07 -0500 Subject: [PATCH 251/266] BUG: is_*_array returns true on empty object dtype (#60796) --- pandas/_libs/lib.pyx | 36 +++++++++++++-------------- pandas/tests/dtypes/test_inference.py | 25 +++++++++++++++++++ pandas/tests/io/test_feather.py | 4 +-- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index de603beff7836..5239aa2c61dc5 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -1882,7 +1882,7 @@ cdef class BoolValidator(Validator): cpdef bint is_bool_array(ndarray values, bint skipna=False): cdef: - BoolValidator validator = BoolValidator(len(values), + BoolValidator validator = BoolValidator(values.size, values.dtype, skipna=skipna) return validator.validate(values) @@ -1900,7 +1900,7 @@ cdef class IntegerValidator(Validator): # Note: only python-exposed for tests cpdef bint is_integer_array(ndarray values, bint skipna=True): cdef: - IntegerValidator validator = IntegerValidator(len(values), + IntegerValidator validator = IntegerValidator(values.size, values.dtype, skipna=skipna) return validator.validate(values) @@ -1915,7 +1915,7 @@ cdef class IntegerNaValidator(Validator): cdef bint is_integer_na_array(ndarray values, bint skipna=True): cdef: - IntegerNaValidator validator = IntegerNaValidator(len(values), + IntegerNaValidator validator = IntegerNaValidator(values.size, values.dtype, skipna=skipna) return validator.validate(values) @@ -1931,7 +1931,7 @@ cdef class IntegerFloatValidator(Validator): cdef bint is_integer_float_array(ndarray values, bint skipna=True): cdef: - IntegerFloatValidator validator = IntegerFloatValidator(len(values), + IntegerFloatValidator validator = IntegerFloatValidator(values.size, values.dtype, skipna=skipna) return validator.validate(values) @@ -1949,7 +1949,7 @@ cdef class FloatValidator(Validator): # Note: only python-exposed for tests cpdef bint is_float_array(ndarray values): cdef: - FloatValidator validator = FloatValidator(len(values), values.dtype) + FloatValidator validator = FloatValidator(values.size, values.dtype) return validator.validate(values) @@ -1967,7 +1967,7 @@ cdef class ComplexValidator(Validator): cdef bint is_complex_array(ndarray values): cdef: - ComplexValidator validator = ComplexValidator(len(values), values.dtype) + ComplexValidator validator = ComplexValidator(values.size, values.dtype) return validator.validate(values) @@ -1980,7 +1980,7 @@ cdef class DecimalValidator(Validator): cdef bint is_decimal_array(ndarray values, bint skipna=False): cdef: DecimalValidator validator = DecimalValidator( - len(values), values.dtype, skipna=skipna + values.size, values.dtype, skipna=skipna ) return validator.validate(values) @@ -1996,7 +1996,7 @@ cdef class StringValidator(Validator): cpdef bint is_string_array(ndarray values, bint skipna=False): cdef: - StringValidator validator = StringValidator(len(values), + StringValidator validator = StringValidator(values.size, values.dtype, skipna=skipna) return validator.validate(values) @@ -2013,7 +2013,7 @@ cdef class BytesValidator(Validator): cdef bint is_bytes_array(ndarray values, bint skipna=False): cdef: - BytesValidator validator = BytesValidator(len(values), values.dtype, + BytesValidator validator = BytesValidator(values.size, values.dtype, skipna=skipna) return validator.validate(values) @@ -2064,7 +2064,7 @@ cdef class DatetimeValidator(TemporalValidator): cpdef bint is_datetime_array(ndarray values, bint skipna=True): cdef: - DatetimeValidator validator = DatetimeValidator(len(values), + DatetimeValidator validator = DatetimeValidator(values.size, skipna=skipna) return validator.validate(values) @@ -2078,7 +2078,7 @@ cdef class Datetime64Validator(DatetimeValidator): # Note: only python-exposed for tests cpdef bint is_datetime64_array(ndarray values, bint skipna=True): cdef: - Datetime64Validator validator = Datetime64Validator(len(values), + Datetime64Validator validator = Datetime64Validator(values.size, skipna=skipna) return validator.validate(values) @@ -2093,7 +2093,7 @@ cdef class AnyDatetimeValidator(DatetimeValidator): cdef bint is_datetime_or_datetime64_array(ndarray values, bint skipna=True): cdef: - AnyDatetimeValidator validator = AnyDatetimeValidator(len(values), + AnyDatetimeValidator validator = AnyDatetimeValidator(values.size, skipna=skipna) return validator.validate(values) @@ -2105,7 +2105,7 @@ def is_datetime_with_singletz_array(values: ndarray) -> bool: Doesn't check values are datetime-like types. """ cdef: - Py_ssize_t i = 0, j, n = len(values) + Py_ssize_t i = 0, j, n = values.size object base_val, base_tz, val, tz if n == 0: @@ -2153,7 +2153,7 @@ cpdef bint is_timedelta_or_timedelta64_array(ndarray values, bint skipna=True): Infer with timedeltas and/or nat/none. """ cdef: - AnyTimedeltaValidator validator = AnyTimedeltaValidator(len(values), + AnyTimedeltaValidator validator = AnyTimedeltaValidator(values.size, skipna=skipna) return validator.validate(values) @@ -2167,7 +2167,7 @@ cdef class DateValidator(Validator): # Note: only python-exposed for tests cpdef bint is_date_array(ndarray values, bint skipna=False): cdef: - DateValidator validator = DateValidator(len(values), skipna=skipna) + DateValidator validator = DateValidator(values.size, skipna=skipna) return validator.validate(values) @@ -2180,7 +2180,7 @@ cdef class TimeValidator(Validator): # Note: only python-exposed for tests cpdef bint is_time_array(ndarray values, bint skipna=False): cdef: - TimeValidator validator = TimeValidator(len(values), skipna=skipna) + TimeValidator validator = TimeValidator(values.size, skipna=skipna) return validator.validate(values) @@ -2231,14 +2231,14 @@ cpdef bint is_interval_array(ndarray values): Is this an ndarray of Interval (or np.nan) with a single dtype? """ cdef: - Py_ssize_t i, n = len(values) + Py_ssize_t i, n = values.size str closed = None bint numeric = False bint dt64 = False bint td64 = False object val - if len(values) == 0: + if n == 0: return False for i in range(n): diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index da444b55490f0..db98751324ebc 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -1582,6 +1582,31 @@ def test_is_string_array(self): ) assert not lib.is_string_array(np.array([1, 2])) + @pytest.mark.parametrize( + "func", + [ + "is_bool_array", + "is_date_array", + "is_datetime_array", + "is_datetime64_array", + "is_float_array", + "is_integer_array", + "is_interval_array", + "is_string_array", + "is_time_array", + "is_timedelta_or_timedelta64_array", + ], + ) + def test_is_dtype_array_empty_obj(self, func): + # https://github.com/pandas-dev/pandas/pull/60796 + func = getattr(lib, func) + + arr = np.empty((2, 0), dtype=object) + assert not func(arr) + + arr = np.empty((0, 2), dtype=object) + assert not func(arr) + def test_to_object_array_tuples(self): r = (5, 6) values = [r] diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 24af0a014dd50..e778193c147c1 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -143,8 +143,8 @@ def test_rw_use_threads(self): def test_path_pathlib(self): df = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), - columns=pd.Index(list("ABCD"), dtype=object), - index=pd.Index([f"i-{i}" for i in range(30)], dtype=object), + columns=pd.Index(list("ABCD")), + index=pd.Index([f"i-{i}" for i in range(30)]), ).reset_index() result = tm.round_trip_pathlib(df.to_feather, read_feather) tm.assert_frame_equal(df, result) From dec6eb29b35c884e78c82525e1bb30280208714c Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Tue, 28 Jan 2025 23:41:54 +0530 Subject: [PATCH 252/266] DOC: fix PR01,RT03,SA01 for pandas.core.resample.Resampler.transform (#60805) * DOC: fix PR01,RT03,SA01 for pandas.core.resample.Resampler.transform * DOC: fix PR01,RT03,SA01 for pandas.core.resample.Resampler.transform --- ci/code_checks.sh | 1 - pandas/core/resample.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 2d0fcce47d2a5..ee5b7eb4f09fb 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -82,7 +82,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ - -i "pandas.core.resample.Resampler.transform PR01,RT03,SA01" \ -i "pandas.tseries.offsets.BDay PR02,SA01" \ -i "pandas.tseries.offsets.BQuarterBegin.is_on_offset GL08" \ -i "pandas.tseries.offsets.BQuarterBegin.n GL08" \ diff --git a/pandas/core/resample.py b/pandas/core/resample.py index b1b8aef31d3c4..4b3b7a72b5a5c 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -378,10 +378,20 @@ def transform(self, arg, *args, **kwargs): ---------- arg : function To apply to each group. Should return a Series with the same index. + *args, **kwargs + Additional arguments and keywords. Returns ------- Series + A Series with the transformed values, maintaining the same index as + the original object. + + See Also + -------- + core.resample.Resampler.apply : Apply a function along each group. + core.resample.Resampler.aggregate : Aggregate using one or more operations + over the specified axis. Examples -------- From c430c613e6c712a39d07146b8adb083d55943840 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Tue, 28 Jan 2025 19:17:22 -0500 Subject: [PATCH 253/266] TST(string_dtype): Refine scope of string xfail in test_http_headers (#60811) --- pandas/tests/io/test_http_headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/io/test_http_headers.py b/pandas/tests/io/test_http_headers.py index b11fe931f46e5..3b9c8769ad9dc 100644 --- a/pandas/tests/io/test_http_headers.py +++ b/pandas/tests/io/test_http_headers.py @@ -86,7 +86,6 @@ def stata_responder(df): return bio.getvalue() -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "responder, read_method", [ @@ -107,6 +106,7 @@ def stata_responder(df): marks=[ td.skip_if_no("fastparquet"), td.skip_if_no("fsspec"), + pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string"), ], ), (pickle_respnder, pd.read_pickle), From c36da3f6ded4141add4b3b16c252cedf4641e5ea Mon Sep 17 00:00:00 2001 From: Richard Shadrach <45562402+rhshadrach@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:07:04 -0500 Subject: [PATCH 254/266] ENH(string dtype): Make str.decode return str dtype (#60709) * TST(string dtype): Make str.decode return str dtype * Test fixups * pytables fixup * Simplify * whatsnew * fix implementation --- doc/source/whatsnew/v2.3.0.rst | 1 + pandas/core/strings/accessor.py | 10 +++++++--- pandas/io/pytables.py | 4 +++- pandas/io/sas/sas7bdat.py | 6 ++++++ pandas/tests/io/sas/test_sas7bdat.py | 16 ++++++---------- pandas/tests/strings/test_strings.py | 9 +++++---- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst index 108ee62d88409..8bdddb5b7f85d 100644 --- a/doc/source/whatsnew/v2.3.0.rst +++ b/doc/source/whatsnew/v2.3.0.rst @@ -35,6 +35,7 @@ Other enhancements - The semantics for the ``copy`` keyword in ``__array__`` methods (i.e. called when using ``np.array()`` or ``np.asarray()`` on pandas objects) has been updated to work correctly with NumPy >= 2 (:issue:`57739`) +- :meth:`Series.str.decode` result now has ``StringDtype`` when ``future.infer_string`` is True (:issue:`60709`) - :meth:`~Series.to_hdf` and :meth:`~DataFrame.to_hdf` now round-trip with ``StringDtype`` (:issue:`60663`) - The :meth:`~Series.cumsum`, :meth:`~Series.cummin`, and :meth:`~Series.cummax` reductions are now implemented for ``StringDtype`` columns when backed by PyArrow (:issue:`60633`) - The :meth:`~Series.sum` reduction is now implemented for ``StringDtype`` columns (:issue:`59853`) diff --git a/pandas/core/strings/accessor.py b/pandas/core/strings/accessor.py index 5b35b5e393012..b854338c2d1d7 100644 --- a/pandas/core/strings/accessor.py +++ b/pandas/core/strings/accessor.py @@ -12,6 +12,8 @@ import numpy as np +from pandas._config import get_option + from pandas._libs import lib from pandas._typing import ( AlignJoin, @@ -400,7 +402,9 @@ def cons_row(x): # This is a mess. _dtype: DtypeObj | str | None = dtype vdtype = getattr(result, "dtype", None) - if self._is_string: + if _dtype is not None: + pass + elif self._is_string: if is_bool_dtype(vdtype): _dtype = result.dtype elif returns_string: @@ -2141,9 +2145,9 @@ def decode(self, encoding, errors: str = "strict"): decoder = codecs.getdecoder(encoding) f = lambda x: decoder(x, errors)[0] arr = self._data.array - # assert isinstance(arr, (StringArray,)) result = arr._str_map(f) - return self._wrap_result(result) + dtype = "str" if get_option("future.infer_string") else None + return self._wrap_result(result, dtype=dtype) @forbid_nonstring_types(["bytes"]) def encode(self, encoding, errors: str = "strict"): diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py index 2f8096746318b..e18db2e53113f 100644 --- a/pandas/io/pytables.py +++ b/pandas/io/pytables.py @@ -5233,7 +5233,9 @@ def _unconvert_string_array( dtype = f"U{itemsize}" if isinstance(data[0], bytes): - data = Series(data, copy=False).str.decode(encoding, errors=errors)._values + ser = Series(data, copy=False).str.decode(encoding, errors=errors) + data = ser.to_numpy() + data.flags.writeable = True else: data = data.astype(dtype, copy=False).astype(object, copy=False) diff --git a/pandas/io/sas/sas7bdat.py b/pandas/io/sas/sas7bdat.py index c5aab4d967cd4..792af5ff713a3 100644 --- a/pandas/io/sas/sas7bdat.py +++ b/pandas/io/sas/sas7bdat.py @@ -22,6 +22,8 @@ import numpy as np +from pandas._config import get_option + from pandas._libs.byteswap import ( read_double_with_byteswap, read_float_with_byteswap, @@ -699,6 +701,7 @@ def _chunk_to_dataframe(self) -> DataFrame: rslt = {} js, jb = 0, 0 + infer_string = get_option("future.infer_string") for j in range(self.column_count): name = self.column_names[j] @@ -715,6 +718,9 @@ def _chunk_to_dataframe(self) -> DataFrame: rslt[name] = pd.Series(self._string_chunk[js, :], index=ix, copy=False) if self.convert_text and (self.encoding is not None): rslt[name] = self._decode_string(rslt[name].str) + if infer_string: + rslt[name] = rslt[name].astype("str") + js += 1 else: self.close() diff --git a/pandas/tests/io/sas/test_sas7bdat.py b/pandas/tests/io/sas/test_sas7bdat.py index 3f5b73f4aa8a4..a17cd27f8284e 100644 --- a/pandas/tests/io/sas/test_sas7bdat.py +++ b/pandas/tests/io/sas/test_sas7bdat.py @@ -7,8 +7,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas.compat._constants import ( IS64, WASM, @@ -20,10 +18,6 @@ from pandas.io.sas.sas7bdat import SAS7BDATReader -pytestmark = pytest.mark.xfail( - using_string_dtype(), reason="TODO(infer_string)", strict=False -) - @pytest.fixture def dirpath(datapath): @@ -246,11 +240,13 @@ def test_zero_variables(datapath): pd.read_sas(fname) -def test_zero_rows(datapath): +@pytest.mark.parametrize("encoding", [None, "utf8"]) +def test_zero_rows(datapath, encoding): # GH 18198 fname = datapath("io", "sas", "data", "zero_rows.sas7bdat") - result = pd.read_sas(fname) - expected = pd.DataFrame([{"char_field": "a", "num_field": 1.0}]).iloc[:0] + result = pd.read_sas(fname, encoding=encoding) + str_value = b"a" if encoding is None else "a" + expected = pd.DataFrame([{"char_field": str_value, "num_field": 1.0}]).iloc[:0] tm.assert_frame_equal(result, expected) @@ -409,7 +405,7 @@ def test_0x40_control_byte(datapath): fname = datapath("io", "sas", "data", "0x40controlbyte.sas7bdat") df = pd.read_sas(fname, encoding="ascii") fname = datapath("io", "sas", "data", "0x40controlbyte.csv") - df0 = pd.read_csv(fname, dtype="object") + df0 = pd.read_csv(fname, dtype="str") tm.assert_frame_equal(df, df0) diff --git a/pandas/tests/strings/test_strings.py b/pandas/tests/strings/test_strings.py index 0598e5f80e6d6..ee531b32aa82d 100644 --- a/pandas/tests/strings/test_strings.py +++ b/pandas/tests/strings/test_strings.py @@ -95,6 +95,7 @@ def test_repeat_with_null(any_string_dtype, arg, repeat): def test_empty_str_methods(any_string_dtype): empty_str = empty = Series(dtype=any_string_dtype) + empty_inferred_str = Series(dtype="str") if is_object_or_nan_string_dtype(any_string_dtype): empty_int = Series(dtype="int64") empty_bool = Series(dtype=bool) @@ -154,7 +155,7 @@ def test_empty_str_methods(any_string_dtype): tm.assert_series_equal(empty_str, empty.str.rstrip()) tm.assert_series_equal(empty_str, empty.str.wrap(42)) tm.assert_series_equal(empty_str, empty.str.get(0)) - tm.assert_series_equal(empty_object, empty_bytes.str.decode("ascii")) + tm.assert_series_equal(empty_inferred_str, empty_bytes.str.decode("ascii")) tm.assert_series_equal(empty_bytes, empty.str.encode("ascii")) # ismethods should always return boolean (GH 29624) tm.assert_series_equal(empty_bool, empty.str.isalnum()) @@ -566,7 +567,7 @@ def test_string_slice_out_of_bounds(any_string_dtype): def test_encode_decode(any_string_dtype): ser = Series(["a", "b", "a\xe4"], dtype=any_string_dtype).str.encode("utf-8") result = ser.str.decode("utf-8") - expected = ser.map(lambda x: x.decode("utf-8")).astype(object) + expected = Series(["a", "b", "a\xe4"], dtype="str") tm.assert_series_equal(result, expected) @@ -596,7 +597,7 @@ def test_decode_errors_kwarg(): ser.str.decode("cp1252") result = ser.str.decode("cp1252", "ignore") - expected = ser.map(lambda x: x.decode("cp1252", "ignore")).astype(object) + expected = ser.map(lambda x: x.decode("cp1252", "ignore")).astype("str") tm.assert_series_equal(result, expected) @@ -751,5 +752,5 @@ def test_get_with_dict_label(): def test_series_str_decode(): # GH 22613 result = Series([b"x", b"y"]).str.decode(encoding="UTF-8", errors="strict") - expected = Series(["x", "y"], dtype="object") + expected = Series(["x", "y"], dtype="str") tm.assert_series_equal(result, expected) From ea7ff0ea4606f47a672f75793f4ea2b3eb0b87f5 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Thu, 30 Jan 2025 09:31:45 -0800 Subject: [PATCH 255/266] BUG(string): from_dummies, dropna (#60818) --- pandas/tests/frame/methods/test_dropna.py | 8 ++++---- pandas/tests/frame/test_arithmetic.py | 13 ++++++++++--- pandas/tests/reshape/test_from_dummies.py | 7 +++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pandas/tests/frame/methods/test_dropna.py b/pandas/tests/frame/methods/test_dropna.py index 4a60dc09cfe07..d4f5629e6ba4b 100644 --- a/pandas/tests/frame/methods/test_dropna.py +++ b/pandas/tests/frame/methods/test_dropna.py @@ -4,8 +4,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - import pandas as pd from pandas import ( DataFrame, @@ -184,10 +182,12 @@ def test_dropna_multiple_axes(self): with pytest.raises(TypeError, match="supplying multiple axes"): inp.dropna(how="all", axis=(0, 1), inplace=True) - @pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") - def test_dropna_tz_aware_datetime(self): + def test_dropna_tz_aware_datetime(self, using_infer_string): # GH13407 + df = DataFrame() + if using_infer_string: + df.columns = df.columns.astype("str") dt1 = datetime.datetime(2015, 1, 1, tzinfo=dateutil.tz.tzutc()) dt2 = datetime.datetime(2015, 2, 2, tzinfo=dateutil.tz.tzutc()) df["Time"] = [dt1] diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 7ada1884feb90..aa2d5e9d23815 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -11,7 +11,7 @@ import numpy as np import pytest -from pandas._config import using_string_dtype +from pandas.compat import HAS_PYARROW import pandas as pd from pandas import ( @@ -2126,12 +2126,19 @@ def test_enum_column_equality(): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)") -def test_mixed_col_index_dtype(): +def test_mixed_col_index_dtype(using_infer_string): # GH 47382 df1 = DataFrame(columns=list("abc"), data=1.0, index=[0]) df2 = DataFrame(columns=list("abc"), data=0.0, index=[0]) df1.columns = df2.columns.astype("string") result = df1 + df2 expected = DataFrame(columns=list("abc"), data=1.0, index=[0]) + if using_infer_string: + # df2.columns.dtype will be "str" instead of object, + # so the aligned result will be "string", not object + if HAS_PYARROW: + dtype = "string[pyarrow]" + else: + dtype = "string" + expected.columns = expected.columns.astype(dtype) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/reshape/test_from_dummies.py b/pandas/tests/reshape/test_from_dummies.py index da1930323f464..c7b7992a78232 100644 --- a/pandas/tests/reshape/test_from_dummies.py +++ b/pandas/tests/reshape/test_from_dummies.py @@ -1,8 +1,6 @@ import numpy as np import pytest -from pandas._config import using_string_dtype - from pandas import ( DataFrame, Series, @@ -364,7 +362,6 @@ def test_with_prefix_contains_get_dummies_NaN_column(): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)", strict=False) @pytest.mark.parametrize( "default_category, expected", [ @@ -401,12 +398,14 @@ def test_with_prefix_contains_get_dummies_NaN_column(): ], ) def test_with_prefix_default_category( - dummies_with_unassigned, default_category, expected + dummies_with_unassigned, default_category, expected, using_infer_string ): result = from_dummies( dummies_with_unassigned, sep="_", default_category=default_category ) expected = DataFrame(expected) + if using_infer_string: + expected = expected.astype("str") tm.assert_frame_equal(result, expected) From 9b03dd4d22550403b75d74f8b54b422bd31c55f2 Mon Sep 17 00:00:00 2001 From: 3w36zj6 <52315048+3w36zj6@users.noreply.github.com> Date: Sun, 2 Feb 2025 04:16:46 +0900 Subject: [PATCH 256/266] ENH: Add `Styler.to_typst()` (#60733) * ENH: Add `to_typst` method to `Styler` * TST: Add `Styler.to_typst()` test cases * STY: Apply Ruff suggestions * DOC: Update What's new * DOC: Update reference * CI: Add `Styler.template_typst` to validation ignore list * DOC: Update docstring format for `Styler.to_typst()` example * DOC: Update versionadded for `Styler.to_typst()` to 3.0.0 in documentation --------- Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/reference/style.rst | 2 + doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/style.py | 105 ++++++++++++++++++ pandas/io/formats/style_render.py | 16 +++ pandas/io/formats/templates/typst.tpl | 12 ++ .../tests/io/formats/style/test_to_typst.py | 96 ++++++++++++++++ scripts/validate_docstrings.py | 1 + 7 files changed, 233 insertions(+) create mode 100644 pandas/io/formats/templates/typst.tpl create mode 100644 pandas/tests/io/formats/style/test_to_typst.py diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 0e1d93841d52f..742263c788c2f 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -27,6 +27,7 @@ Styler properties Styler.template_html_style Styler.template_html_table Styler.template_latex + Styler.template_typst Styler.template_string Styler.loader @@ -77,6 +78,7 @@ Style export and import Styler.to_html Styler.to_latex + Styler.to_typst Styler.to_excel Styler.to_string Styler.export diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index a7f63d75a047e..64f4a66a109f5 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -31,6 +31,7 @@ Other enhancements - :class:`pandas.api.typing.FrozenList` is available for typing the outputs of :attr:`MultiIndex.names`, :attr:`MultiIndex.codes` and :attr:`MultiIndex.levels` (:issue:`58237`) - :class:`pandas.api.typing.SASReader` is available for typing the output of :func:`read_sas` (:issue:`55689`) - :meth:`pandas.api.interchange.from_dataframe` now uses the `PyCapsule Interface `_ if available, only falling back to the Dataframe Interchange Protocol if that fails (:issue:`60739`) +- Added :meth:`.Styler.to_typst` to write Styler objects to file, buffer or string in Typst format (:issue:`57617`) - :class:`pandas.api.typing.NoDefault` is available for typing ``no_default`` - :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`) - :func:`pandas.merge` now validates the ``how`` parameter input (merge type) (:issue:`59435`) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6f164c4b97514..3f37556867954 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1228,6 +1228,111 @@ def to_latex( ) return save_to_buffer(latex, buf=buf, encoding=encoding) + @overload + def to_typst( + self, + buf: FilePath | WriteBuffer[str], + *, + encoding: str | None = ..., + sparse_index: bool | None = ..., + sparse_columns: bool | None = ..., + max_rows: int | None = ..., + max_columns: int | None = ..., + ) -> None: ... + + @overload + def to_typst( + self, + buf: None = ..., + *, + encoding: str | None = ..., + sparse_index: bool | None = ..., + sparse_columns: bool | None = ..., + max_rows: int | None = ..., + max_columns: int | None = ..., + ) -> str: ... + + @Substitution(buf=buffering_args, encoding=encoding_args) + def to_typst( + self, + buf: FilePath | WriteBuffer[str] | None = None, + *, + encoding: str | None = None, + sparse_index: bool | None = None, + sparse_columns: bool | None = None, + max_rows: int | None = None, + max_columns: int | None = None, + ) -> str | None: + """ + Write Styler to a file, buffer or string in Typst format. + + .. versionadded:: 3.0.0 + + Parameters + ---------- + %(buf)s + %(encoding)s + sparse_index : bool, optional + Whether to sparsify the display of a hierarchical index. Setting to False + will display each explicit level element in a hierarchical key for each row. + Defaults to ``pandas.options.styler.sparse.index`` value. + sparse_columns : bool, optional + Whether to sparsify the display of a hierarchical index. Setting to False + will display each explicit level element in a hierarchical key for each + column. Defaults to ``pandas.options.styler.sparse.columns`` value. + max_rows : int, optional + The maximum number of rows that will be rendered. Defaults to + ``pandas.options.styler.render.max_rows``, which is None. + max_columns : int, optional + The maximum number of columns that will be rendered. Defaults to + ``pandas.options.styler.render.max_columns``, which is None. + + Rows and columns may be reduced if the number of total elements is + large. This value is set to ``pandas.options.styler.render.max_elements``, + which is 262144 (18 bit browser rendering). + + Returns + ------- + str or None + If `buf` is None, returns the result as a string. Otherwise returns `None`. + + See Also + -------- + DataFrame.to_typst : Write a DataFrame to a file, + buffer or string in Typst format. + + Examples + -------- + >>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4]}) + >>> df.style.to_typst() # doctest: +SKIP + + .. code-block:: typst + + #table( + columns: 3, + [], [A], [B], + + [0], [1], [3], + [1], [2], [4], + ) + """ + obj = self._copy(deepcopy=True) + + if sparse_index is None: + sparse_index = get_option("styler.sparse.index") + if sparse_columns is None: + sparse_columns = get_option("styler.sparse.columns") + + text = obj._render_typst( + sparse_columns=sparse_columns, + sparse_index=sparse_index, + max_rows=max_rows, + max_cols=max_columns, + ) + return save_to_buffer( + text, buf=buf, encoding=(encoding if buf is not None else None) + ) + @overload def to_html( self, diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index c0f0608f1ab32..2d1218b007d19 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -77,6 +77,7 @@ class StylerRenderer: template_html_table = env.get_template("html_table.tpl") template_html_style = env.get_template("html_style.tpl") template_latex = env.get_template("latex.tpl") + template_typst = env.get_template("typst.tpl") template_string = env.get_template("string.tpl") def __init__( @@ -232,6 +233,21 @@ def _render_latex( d.update(kwargs) return self.template_latex.render(**d) + def _render_typst( + self, + sparse_index: bool, + sparse_columns: bool, + max_rows: int | None = None, + max_cols: int | None = None, + **kwargs, + ) -> str: + """ + Render a Styler in typst format + """ + d = self._render(sparse_index, sparse_columns, max_rows, max_cols) + d.update(kwargs) + return self.template_typst.render(**d) + def _render_string( self, sparse_index: bool, diff --git a/pandas/io/formats/templates/typst.tpl b/pandas/io/formats/templates/typst.tpl new file mode 100644 index 0000000000000..66de8f31b405e --- /dev/null +++ b/pandas/io/formats/templates/typst.tpl @@ -0,0 +1,12 @@ +#table( + columns: {{ head[0] | length }}, +{% for r in head %} + {% for c in r %}[{% if c["is_visible"] %}{{ c["display_value"] }}{% endif %}],{% if not loop.last %} {% endif%}{% endfor %} + +{% endfor %} + +{% for r in body %} + {% for c in r %}[{% if c["is_visible"] %}{{ c["display_value"] }}{% endif %}],{% if not loop.last %} {% endif%}{% endfor %} + +{% endfor %} +) diff --git a/pandas/tests/io/formats/style/test_to_typst.py b/pandas/tests/io/formats/style/test_to_typst.py new file mode 100644 index 0000000000000..2365119c9c4dc --- /dev/null +++ b/pandas/tests/io/formats/style/test_to_typst.py @@ -0,0 +1,96 @@ +from textwrap import dedent + +import pytest + +from pandas import ( + DataFrame, + Series, +) + +pytest.importorskip("jinja2") +from pandas.io.formats.style import Styler + + +@pytest.fixture +def df(): + return DataFrame( + {"A": [0, 1], "B": [-0.61, -1.22], "C": Series(["ab", "cd"], dtype=object)} + ) + + +@pytest.fixture +def styler(df): + return Styler(df, uuid_len=0, precision=2) + + +def test_basic_table(styler): + result = styler.to_typst() + expected = dedent( + """\ + #table( + columns: 4, + [], [A], [B], [C], + + [0], [0], [-0.61], [ab], + [1], [1], [-1.22], [cd], + )""" + ) + assert result == expected + + +def test_concat(styler): + result = styler.concat(styler.data.agg(["sum"]).style).to_typst() + expected = dedent( + """\ + #table( + columns: 4, + [], [A], [B], [C], + + [0], [0], [-0.61], [ab], + [1], [1], [-1.22], [cd], + [sum], [1], [-1.830000], [abcd], + )""" + ) + assert result == expected + + +def test_concat_recursion(styler): + df = styler.data + styler1 = styler + styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3) + styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4) + result = styler1.concat(styler2.concat(styler3)).to_typst() + expected = dedent( + """\ + #table( + columns: 4, + [], [A], [B], [C], + + [0], [0], [-0.61], [ab], + [1], [1], [-1.22], [cd], + [sum], [1], [-1.830], [abcd], + [sum], [1], [-1.8300], [abcd], + )""" + ) + assert result == expected + + +def test_concat_chain(styler): + df = styler.data + styler1 = styler + styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3) + styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4) + result = styler1.concat(styler2).concat(styler3).to_typst() + expected = dedent( + """\ + #table( + columns: 4, + [], [A], [B], [C], + + [0], [0], [-0.61], [ab], + [1], [1], [-1.22], [cd], + [sum], [1], [-1.830], [abcd], + [sum], [1], [-1.8300], [abcd], + )""" + ) + assert result == expected diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 55acfaac4d843..944575dcc8659 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -45,6 +45,7 @@ "Styler.template_html_style", "Styler.template_html_table", "Styler.template_latex", + "Styler.template_typst", "Styler.template_string", "Styler.loader", "errors.InvalidComparison", From d72f165eb327898b1597efe75ff8b54032c3ae7b Mon Sep 17 00:00:00 2001 From: "Christine P. Chai" Date: Sat, 1 Feb 2025 11:18:25 -0800 Subject: [PATCH 257/266] DOC: Move NumPy Byte Order page in gotchas.rst (#60822) --- doc/source/user_guide/gotchas.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/gotchas.rst b/doc/source/user_guide/gotchas.rst index 842f30f06676e..e85eead4e0f09 100644 --- a/doc/source/user_guide/gotchas.rst +++ b/doc/source/user_guide/gotchas.rst @@ -372,5 +372,5 @@ constructors using something similar to the following: s = pd.Series(newx) See `the NumPy documentation on byte order -`__ for more +`__ for more details. From f1441b218271178ebe18acecc3657f6549fb6c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quang=20Nguy=E1=BB=85n?= <30631476+quangngd@users.noreply.github.com> Date: Mon, 3 Feb 2025 03:12:30 +0700 Subject: [PATCH 258/266] CHORE: Enable mistakenly ignored tests (#60827) Enable ignored tests --- pandas/tests/io/formats/test_to_string.py | 25 ++++++++++------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/pandas/tests/io/formats/test_to_string.py b/pandas/tests/io/formats/test_to_string.py index af3cdf2d44af3..1e8598c918efe 100644 --- a/pandas/tests/io/formats/test_to_string.py +++ b/pandas/tests/io/formats/test_to_string.py @@ -132,20 +132,17 @@ def test_to_string_with_formatters_unicode(self): ) assert result == expected - def test_to_string_index_formatter(self): - df = DataFrame([range(5), range(5, 10), range(10, 15)]) - - rs = df.to_string(formatters={"__index__": lambda x: "abc"[x]}) - - xp = dedent( - """\ - 0 1 2 3 4 - a 0 1 2 3 4 - b 5 6 7 8 9 - c 10 11 12 13 14\ - """ - ) - assert rs == xp + def test_to_string_index_formatter(self): + df = DataFrame([range(5), range(5, 10), range(10, 15)]) + rs = df.to_string(formatters={"__index__": lambda x: "abc"[x]}) + xp = dedent( + """\ + 0 1 2 3 4 + a 0 1 2 3 4 + b 5 6 7 8 9 + c 10 11 12 13 14""" + ) + assert rs == xp def test_no_extra_space(self): # GH#52690: Check that no extra space is given From a68048ea026f09fc56e1a9963c489ff0beaae651 Mon Sep 17 00:00:00 2001 From: Nitish Satyavolu Date: Mon, 3 Feb 2025 06:23:23 -0800 Subject: [PATCH 259/266] ENH: Support skipna parameter in GroupBy min, max, prod, median, var, std and sem methods (#60752) --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/_libs/groupby.pyi | 5 + pandas/_libs/groupby.pyx | 99 ++++++++++--- pandas/core/_numba/kernels/min_max_.py | 8 +- pandas/core/_numba/kernels/var_.py | 7 +- pandas/core/groupby/groupby.py | 76 ++++++++-- pandas/core/resample.py | 98 ++++++++++++- pandas/tests/groupby/aggregate/test_numba.py | 2 +- pandas/tests/groupby/test_api.py | 18 +-- pandas/tests/groupby/test_reductions.py | 141 +++++++++++++++++++ 10 files changed, 405 insertions(+), 51 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 64f4a66a109f5..9089b9cdd2185 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -59,9 +59,9 @@ Other enhancements - :meth:`Series.cummin` and :meth:`Series.cummax` now supports :class:`CategoricalDtype` (:issue:`52335`) - :meth:`Series.plot` now correctly handle the ``ylabel`` parameter for pie charts, allowing for explicit control over the y-axis label (:issue:`58239`) - :meth:`DataFrame.plot.scatter` argument ``c`` now accepts a column of strings, where rows with the same string are colored identically (:issue:`16827` and :issue:`16485`) +- :class:`DataFrameGroupBy` and :class:`SeriesGroupBy` methods ``sum``, ``mean``, ``median``, ``prod``, ``min``, ``max``, ``std``, ``var`` and ``sem`` now accept ``skipna`` parameter (:issue:`15675`) - :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`) - :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`) -- :meth:`.DataFrameGroupBy.mean`, :meth:`.DataFrameGroupBy.sum`, :meth:`.SeriesGroupBy.mean` and :meth:`.SeriesGroupBy.sum` now accept ``skipna`` parameter (:issue:`15675`) - :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`) - :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`) - :meth:`Rolling.agg`, :meth:`Expanding.agg` and :meth:`ExponentialMovingWindow.agg` now accept :class:`NamedAgg` aggregations through ``**kwargs`` (:issue:`28333`) diff --git a/pandas/_libs/groupby.pyi b/pandas/_libs/groupby.pyi index e3909203d1f5a..163fc23535022 100644 --- a/pandas/_libs/groupby.pyi +++ b/pandas/_libs/groupby.pyi @@ -13,6 +13,7 @@ def group_median_float64( mask: np.ndarray | None = ..., result_mask: np.ndarray | None = ..., is_datetimelike: bool = ..., # bint + skipna: bool = ..., ) -> None: ... def group_cumprod( out: np.ndarray, # float64_t[:, ::1] @@ -76,6 +77,7 @@ def group_prod( mask: np.ndarray | None, result_mask: np.ndarray | None = ..., min_count: int = ..., + skipna: bool = ..., ) -> None: ... def group_var( out: np.ndarray, # floating[:, ::1] @@ -88,6 +90,7 @@ def group_var( result_mask: np.ndarray | None = ..., is_datetimelike: bool = ..., name: str = ..., + skipna: bool = ..., ) -> None: ... def group_skew( out: np.ndarray, # float64_t[:, ::1] @@ -183,6 +186,7 @@ def group_max( is_datetimelike: bool = ..., mask: np.ndarray | None = ..., result_mask: np.ndarray | None = ..., + skipna: bool = ..., ) -> None: ... def group_min( out: np.ndarray, # groupby_t[:, ::1] @@ -193,6 +197,7 @@ def group_min( is_datetimelike: bool = ..., mask: np.ndarray | None = ..., result_mask: np.ndarray | None = ..., + skipna: bool = ..., ) -> None: ... def group_idxmin_idxmax( out: npt.NDArray[np.intp], diff --git a/pandas/_libs/groupby.pyx b/pandas/_libs/groupby.pyx index 70af22f514ce0..16a104a46ed3d 100644 --- a/pandas/_libs/groupby.pyx +++ b/pandas/_libs/groupby.pyx @@ -62,7 +62,12 @@ cdef enum InterpolationEnumType: INTERPOLATION_MIDPOINT -cdef float64_t median_linear_mask(float64_t* a, int n, uint8_t* mask) noexcept nogil: +cdef float64_t median_linear_mask( + float64_t* a, + int n, + uint8_t* mask, + bint skipna=True +) noexcept nogil: cdef: int i, j, na_count = 0 float64_t* tmp @@ -77,7 +82,7 @@ cdef float64_t median_linear_mask(float64_t* a, int n, uint8_t* mask) noexcept n na_count += 1 if na_count: - if na_count == n: + if na_count == n or not skipna: return NaN tmp = malloc((n - na_count) * sizeof(float64_t)) @@ -104,7 +109,8 @@ cdef float64_t median_linear_mask(float64_t* a, int n, uint8_t* mask) noexcept n cdef float64_t median_linear( float64_t* a, int n, - bint is_datetimelike=False + bint is_datetimelike=False, + bint skipna=True, ) noexcept nogil: cdef: int i, j, na_count = 0 @@ -125,7 +131,7 @@ cdef float64_t median_linear( na_count += 1 if na_count: - if na_count == n: + if na_count == n or not skipna: return NaN tmp = malloc((n - na_count) * sizeof(float64_t)) @@ -186,6 +192,7 @@ def group_median_float64( const uint8_t[:, :] mask=None, uint8_t[:, ::1] result_mask=None, bint is_datetimelike=False, + bint skipna=True, ) -> None: """ Only aggregates on axis=0 @@ -229,7 +236,7 @@ def group_median_float64( for j in range(ngroups): size = _counts[j + 1] - result = median_linear_mask(ptr, size, ptr_mask) + result = median_linear_mask(ptr, size, ptr_mask, skipna) out[j, i] = result if result != result: @@ -244,7 +251,7 @@ def group_median_float64( ptr += _counts[0] for j in range(ngroups): size = _counts[j + 1] - out[j, i] = median_linear(ptr, size, is_datetimelike) + out[j, i] = median_linear(ptr, size, is_datetimelike, skipna) ptr += size @@ -804,17 +811,18 @@ def group_prod( const uint8_t[:, ::1] mask, uint8_t[:, ::1] result_mask=None, Py_ssize_t min_count=0, + bint skipna=True, ) -> None: """ Only aggregates on axis=0 """ cdef: Py_ssize_t i, j, N, K, lab, ncounts = len(counts) - int64float_t val + int64float_t val, nan_val int64float_t[:, ::1] prodx int64_t[:, ::1] nobs Py_ssize_t len_values = len(values), len_labels = len(labels) - bint isna_entry, uses_mask = mask is not None + bint isna_entry, isna_result, uses_mask = mask is not None if len_values != len_labels: raise ValueError("len(index) != len(labels)") @@ -823,6 +831,7 @@ def group_prod( prodx = np.ones((out).shape, dtype=(out).base.dtype) N, K = (values).shape + nan_val = _get_na_val(0, False) with nogil: for i in range(N): @@ -836,12 +845,23 @@ def group_prod( if uses_mask: isna_entry = mask[i, j] + isna_result = result_mask[lab, j] else: isna_entry = _treat_as_na(val, False) + isna_result = _treat_as_na(prodx[lab, j], False) + + if not skipna and isna_result: + # If prod is already NA, no need to update it + continue if not isna_entry: nobs[lab, j] += 1 prodx[lab, j] *= val + elif not skipna: + if uses_mask: + result_mask[lab, j] = True + else: + prodx[lab, j] = nan_val _check_below_mincount( out, uses_mask, result_mask, ncounts, K, nobs, min_count, prodx @@ -862,6 +882,7 @@ def group_var( uint8_t[:, ::1] result_mask=None, bint is_datetimelike=False, str name="var", + bint skipna=True, ) -> None: cdef: Py_ssize_t i, j, N, K, lab, ncounts = len(counts) @@ -869,7 +890,7 @@ def group_var( floating[:, ::1] mean int64_t[:, ::1] nobs Py_ssize_t len_values = len(values), len_labels = len(labels) - bint isna_entry, uses_mask = mask is not None + bint isna_entry, isna_result, uses_mask = mask is not None bint is_std = name == "std" bint is_sem = name == "sem" @@ -898,19 +919,34 @@ def group_var( if uses_mask: isna_entry = mask[i, j] + isna_result = result_mask[lab, j] elif is_datetimelike: # With group_var, we cannot just use _treat_as_na bc # datetimelike dtypes get cast to float64 instead of # to int64. isna_entry = val == NPY_NAT + isna_result = out[lab, j] == NPY_NAT else: isna_entry = _treat_as_na(val, is_datetimelike) + isna_result = _treat_as_na(out[lab, j], is_datetimelike) + + if not skipna and isna_result: + # If aggregate is already NA, don't add to it. This is important for + # datetimelike because adding a value to NPY_NAT may not result + # in a NPY_NAT + continue if not isna_entry: nobs[lab, j] += 1 oldmean = mean[lab, j] mean[lab, j] += (val - oldmean) / nobs[lab, j] out[lab, j] += (val - mean[lab, j]) * (val - oldmean) + elif not skipna: + nobs[lab, j] = 0 + if uses_mask: + result_mask[lab, j] = True + else: + out[lab, j] = NAN for i in range(ncounts): for j in range(K): @@ -1164,7 +1200,7 @@ def group_mean( mean_t[:, ::1] sumx, compensation int64_t[:, ::1] nobs Py_ssize_t len_values = len(values), len_labels = len(labels) - bint isna_entry, uses_mask = mask is not None + bint isna_entry, isna_result, uses_mask = mask is not None assert min_count == -1, "'min_count' only used in sum and prod" @@ -1194,25 +1230,24 @@ def group_mean( for j in range(K): val = values[i, j] - if not skipna and ( - (uses_mask and result_mask[lab, j]) or - (is_datetimelike and sumx[lab, j] == NPY_NAT) or - _treat_as_na(sumx[lab, j], False) - ): - # If sum is already NA, don't add to it. This is important for - # datetimelike because adding a value to NPY_NAT may not result - # in NPY_NAT - continue - if uses_mask: isna_entry = mask[i, j] + isna_result = result_mask[lab, j] elif is_datetimelike: # With group_mean, we cannot just use _treat_as_na bc # datetimelike dtypes get cast to float64 instead of # to int64. isna_entry = val == NPY_NAT + isna_result = sumx[lab, j] == NPY_NAT else: isna_entry = _treat_as_na(val, is_datetimelike) + isna_result = _treat_as_na(sumx[lab, j], is_datetimelike) + + if not skipna and isna_result: + # If sum is already NA, don't add to it. This is important for + # datetimelike because adding a value to NPY_NAT may not result + # in NPY_NAT + continue if not isna_entry: nobs[lab, j] += 1 @@ -1806,6 +1841,7 @@ cdef group_min_max( bint compute_max=True, const uint8_t[:, ::1] mask=None, uint8_t[:, ::1] result_mask=None, + bint skipna=True, ): """ Compute minimum/maximum of columns of `values`, in row groups `labels`. @@ -1833,6 +1869,8 @@ cdef group_min_max( result_mask : ndarray[bool, ndim=2], optional If not None, these specify locations in the output that are NA. Modified in-place. + skipna : bool, default True + If True, ignore nans in `values`. Notes ----- @@ -1841,17 +1879,18 @@ cdef group_min_max( """ cdef: Py_ssize_t i, j, N, K, lab, ngroups = len(counts) - numeric_t val + numeric_t val, nan_val numeric_t[:, ::1] group_min_or_max int64_t[:, ::1] nobs bint uses_mask = mask is not None - bint isna_entry + bint isna_entry, isna_result if not len(values) == len(labels): raise AssertionError("len(index) != len(labels)") min_count = max(min_count, 1) nobs = np.zeros((out).shape, dtype=np.int64) + nan_val = _get_na_val(0, is_datetimelike) group_min_or_max = np.empty_like(out) group_min_or_max[:] = _get_min_or_max(0, compute_max, is_datetimelike) @@ -1870,8 +1909,15 @@ cdef group_min_max( if uses_mask: isna_entry = mask[i, j] + isna_result = result_mask[lab, j] else: isna_entry = _treat_as_na(val, is_datetimelike) + isna_result = _treat_as_na(group_min_or_max[lab, j], + is_datetimelike) + + if not skipna and isna_result: + # If current min/max is already NA, it will always be NA + continue if not isna_entry: nobs[lab, j] += 1 @@ -1881,6 +1927,11 @@ cdef group_min_max( else: if val < group_min_or_max[lab, j]: group_min_or_max[lab, j] = val + elif not skipna: + if uses_mask: + result_mask[lab, j] = True + else: + group_min_or_max[lab, j] = nan_val _check_below_mincount( out, uses_mask, result_mask, ngroups, K, nobs, min_count, group_min_or_max @@ -2012,6 +2063,7 @@ def group_max( bint is_datetimelike=False, const uint8_t[:, ::1] mask=None, uint8_t[:, ::1] result_mask=None, + bint skipna=True, ) -> None: """See group_min_max.__doc__""" group_min_max( @@ -2024,6 +2076,7 @@ def group_max( compute_max=True, mask=mask, result_mask=result_mask, + skipna=skipna, ) @@ -2038,6 +2091,7 @@ def group_min( bint is_datetimelike=False, const uint8_t[:, ::1] mask=None, uint8_t[:, ::1] result_mask=None, + bint skipna=True, ) -> None: """See group_min_max.__doc__""" group_min_max( @@ -2050,6 +2104,7 @@ def group_min( compute_max=False, mask=mask, result_mask=result_mask, + skipna=skipna, ) diff --git a/pandas/core/_numba/kernels/min_max_.py b/pandas/core/_numba/kernels/min_max_.py index 59d36732ebae6..d56453e4e5abf 100644 --- a/pandas/core/_numba/kernels/min_max_.py +++ b/pandas/core/_numba/kernels/min_max_.py @@ -88,6 +88,7 @@ def grouped_min_max( ngroups: int, min_periods: int, is_max: bool, + skipna: bool = True, ) -> tuple[np.ndarray, list[int]]: N = len(labels) nobs = np.zeros(ngroups, dtype=np.int64) @@ -97,13 +98,16 @@ def grouped_min_max( for i in range(N): lab = labels[i] val = values[i] - if lab < 0: + if lab < 0 or (nobs[lab] >= 1 and np.isnan(output[lab])): continue if values.dtype.kind == "i" or not np.isnan(val): nobs[lab] += 1 else: - # NaN value cannot be a min/max value + if not skipna: + # If skipna is False and we encounter a NaN, + # both min and max of the group will be NaN + output[lab] = np.nan continue if nobs[lab] == 1: diff --git a/pandas/core/_numba/kernels/var_.py b/pandas/core/_numba/kernels/var_.py index 69aec4d6522c4..5d720c877815d 100644 --- a/pandas/core/_numba/kernels/var_.py +++ b/pandas/core/_numba/kernels/var_.py @@ -176,6 +176,7 @@ def grouped_var( ngroups: int, min_periods: int, ddof: int = 1, + skipna: bool = True, ) -> tuple[np.ndarray, list[int]]: N = len(labels) @@ -190,7 +191,11 @@ def grouped_var( lab = labels[i] val = values[i] - if lab < 0: + if lab < 0 or np.isnan(output[lab]): + continue + + if not skipna and np.isnan(val): + output[lab] = np.nan continue mean_x = means[lab] diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index f9059e6e8896f..7c3088bea4b76 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -2248,7 +2248,7 @@ def mean( return result.__finalize__(self.obj, method="groupby") @final - def median(self, numeric_only: bool = False) -> NDFrameT: + def median(self, numeric_only: bool = False, skipna: bool = True) -> NDFrameT: """ Compute median of groups, excluding missing values. @@ -2263,6 +2263,12 @@ def median(self, numeric_only: bool = False) -> NDFrameT: numeric_only no longer accepts ``None`` and defaults to False. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + + .. versionadded:: 3.0.0 + Returns ------- Series or DataFrame @@ -2335,8 +2341,11 @@ def median(self, numeric_only: bool = False) -> NDFrameT: """ result = self._cython_agg_general( "median", - alt=lambda x: Series(x, copy=False).median(numeric_only=numeric_only), + alt=lambda x: Series(x, copy=False).median( + numeric_only=numeric_only, skipna=skipna + ), numeric_only=numeric_only, + skipna=skipna, ) return result.__finalize__(self.obj, method="groupby") @@ -2349,6 +2358,7 @@ def std( engine: Literal["cython", "numba"] | None = None, engine_kwargs: dict[str, bool] | None = None, numeric_only: bool = False, + skipna: bool = True, ): """ Compute standard deviation of groups, excluding missing values. @@ -2387,6 +2397,12 @@ def std( numeric_only now defaults to ``False``. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + + .. versionadded:: 3.0.0 + Returns ------- Series or DataFrame @@ -2441,14 +2457,16 @@ def std( engine_kwargs, min_periods=0, ddof=ddof, + skipna=skipna, ) ) else: return self._cython_agg_general( "std", - alt=lambda x: Series(x, copy=False).std(ddof=ddof), + alt=lambda x: Series(x, copy=False).std(ddof=ddof, skipna=skipna), numeric_only=numeric_only, ddof=ddof, + skipna=skipna, ) @final @@ -2460,6 +2478,7 @@ def var( engine: Literal["cython", "numba"] | None = None, engine_kwargs: dict[str, bool] | None = None, numeric_only: bool = False, + skipna: bool = True, ): """ Compute variance of groups, excluding missing values. @@ -2497,6 +2516,12 @@ def var( numeric_only now defaults to ``False``. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + + .. versionadded:: 3.0.0 + Returns ------- Series or DataFrame @@ -2550,13 +2575,15 @@ def var( engine_kwargs, min_periods=0, ddof=ddof, + skipna=skipna, ) else: return self._cython_agg_general( "var", - alt=lambda x: Series(x, copy=False).var(ddof=ddof), + alt=lambda x: Series(x, copy=False).var(ddof=ddof, skipna=skipna), numeric_only=numeric_only, ddof=ddof, + skipna=skipna, ) @final @@ -2686,7 +2713,9 @@ def _value_counts( return result.__finalize__(self.obj, method="value_counts") @final - def sem(self, ddof: int = 1, numeric_only: bool = False) -> NDFrameT: + def sem( + self, ddof: int = 1, numeric_only: bool = False, skipna: bool = True + ) -> NDFrameT: """ Compute standard error of the mean of groups, excluding missing values. @@ -2706,6 +2735,12 @@ def sem(self, ddof: int = 1, numeric_only: bool = False) -> NDFrameT: numeric_only now defaults to ``False``. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + + .. versionadded:: 3.0.0 + Returns ------- Series or DataFrame @@ -2780,9 +2815,10 @@ def sem(self, ddof: int = 1, numeric_only: bool = False) -> NDFrameT: ) return self._cython_agg_general( "sem", - alt=lambda x: Series(x, copy=False).sem(ddof=ddof), + alt=lambda x: Series(x, copy=False).sem(ddof=ddof, skipna=skipna), numeric_only=numeric_only, ddof=ddof, + skipna=skipna, ) @final @@ -2959,7 +2995,9 @@ def sum( return result @final - def prod(self, numeric_only: bool = False, min_count: int = 0) -> NDFrameT: + def prod( + self, numeric_only: bool = False, min_count: int = 0, skipna: bool = True + ) -> NDFrameT: """ Compute prod of group values. @@ -2976,6 +3014,12 @@ def prod(self, numeric_only: bool = False, min_count: int = 0) -> NDFrameT: The required number of valid values to perform the operation. If fewer than ``min_count`` non-NA values are present the result will be NA. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + + .. versionadded:: 3.0.0 + Returns ------- Series or DataFrame @@ -3024,17 +3068,22 @@ def prod(self, numeric_only: bool = False, min_count: int = 0) -> NDFrameT: 2 30 72 """ return self._agg_general( - numeric_only=numeric_only, min_count=min_count, alias="prod", npfunc=np.prod + numeric_only=numeric_only, + min_count=min_count, + skipna=skipna, + alias="prod", + npfunc=np.prod, ) @final @doc( - _groupby_agg_method_engine_template, + _groupby_agg_method_skipna_engine_template, fname="min", no=False, mc=-1, e=None, ek=None, + s=True, example=dedent( """\ For SeriesGroupBy: @@ -3074,6 +3123,7 @@ def min( self, numeric_only: bool = False, min_count: int = -1, + skipna: bool = True, engine: Literal["cython", "numba"] | None = None, engine_kwargs: dict[str, bool] | None = None, ): @@ -3086,23 +3136,26 @@ def min( engine_kwargs, min_periods=min_count, is_max=False, + skipna=skipna, ) else: return self._agg_general( numeric_only=numeric_only, min_count=min_count, + skipna=skipna, alias="min", npfunc=np.min, ) @final @doc( - _groupby_agg_method_engine_template, + _groupby_agg_method_skipna_engine_template, fname="max", no=False, mc=-1, e=None, ek=None, + s=True, example=dedent( """\ For SeriesGroupBy: @@ -3142,6 +3195,7 @@ def max( self, numeric_only: bool = False, min_count: int = -1, + skipna: bool = True, engine: Literal["cython", "numba"] | None = None, engine_kwargs: dict[str, bool] | None = None, ): @@ -3154,11 +3208,13 @@ def max( engine_kwargs, min_periods=min_count, is_max=True, + skipna=skipna, ) else: return self._agg_general( numeric_only=numeric_only, min_count=min_count, + skipna=skipna, alias="max", npfunc=np.max, ) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 4b3b7a72b5a5c..1cfc75ea11725 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1269,8 +1269,53 @@ def last( ) @final - @doc(GroupBy.median) def median(self, numeric_only: bool = False): + """ + Compute median of groups, excluding missing values. + + For multiple groupings, the result index will be a MultiIndex + + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + + .. versionchanged:: 2.0.0 + + numeric_only no longer accepts ``None`` and defaults to False. + + Returns + ------- + Series or DataFrame + Median of values within each group. + + See Also + -------- + Series.groupby : Apply a function groupby to a Series. + DataFrame.groupby : Apply a function groupby to each row or column of a + DataFrame. + + Examples + -------- + + >>> ser = pd.Series( + ... [1, 2, 3, 3, 4, 5], + ... index=pd.DatetimeIndex( + ... [ + ... "2023-01-01", + ... "2023-01-10", + ... "2023-01-15", + ... "2023-02-01", + ... "2023-02-10", + ... "2023-02-15", + ... ] + ... ), + ... ) + >>> ser.resample("MS").median() + 2023-01-01 2.0 + 2023-02-01 4.0 + Freq: MS, dtype: float64 + """ return self._downsample("median", numeric_only=numeric_only) @final @@ -1450,12 +1495,61 @@ def var( return self._downsample("var", ddof=ddof, numeric_only=numeric_only) @final - @doc(GroupBy.sem) def sem( self, ddof: int = 1, numeric_only: bool = False, ): + """ + Compute standard error of the mean of groups, excluding missing values. + + For multiple groupings, the result index will be a MultiIndex. + + Parameters + ---------- + ddof : int, default 1 + Degrees of freedom. + + numeric_only : bool, default False + Include only `float`, `int` or `boolean` data. + + .. versionadded:: 1.5.0 + + .. versionchanged:: 2.0.0 + + numeric_only now defaults to ``False``. + + Returns + ------- + Series or DataFrame + Standard error of the mean of values within each group. + + See Also + -------- + DataFrame.sem : Return unbiased standard error of the mean over requested axis. + Series.sem : Return unbiased standard error of the mean over requested axis. + + Examples + -------- + + >>> ser = pd.Series( + ... [1, 3, 2, 4, 3, 8], + ... index=pd.DatetimeIndex( + ... [ + ... "2023-01-01", + ... "2023-01-10", + ... "2023-01-15", + ... "2023-02-01", + ... "2023-02-10", + ... "2023-02-15", + ... ] + ... ), + ... ) + >>> ser.resample("MS").sem() + 2023-01-01 0.577350 + 2023-02-01 1.527525 + Freq: MS, dtype: float64 + """ return self._downsample("sem", ddof=ddof, numeric_only=numeric_only) @final diff --git a/pandas/tests/groupby/aggregate/test_numba.py b/pandas/tests/groupby/aggregate/test_numba.py index ca265a1d1108b..0cd8a14d97eb0 100644 --- a/pandas/tests/groupby/aggregate/test_numba.py +++ b/pandas/tests/groupby/aggregate/test_numba.py @@ -186,7 +186,7 @@ def test_multifunc_numba_vs_cython_frame(agg_kwargs): tm.assert_frame_equal(result, expected) -@pytest.mark.parametrize("func", ["sum", "mean"]) +@pytest.mark.parametrize("func", ["sum", "mean", "var", "std", "min", "max"]) def test_multifunc_numba_vs_cython_frame_noskipna(func): pytest.importorskip("numba") data = DataFrame( diff --git a/pandas/tests/groupby/test_api.py b/pandas/tests/groupby/test_api.py index cc69de2581a79..215e627abb018 100644 --- a/pandas/tests/groupby/test_api.py +++ b/pandas/tests/groupby/test_api.py @@ -174,16 +174,13 @@ def test_frame_consistency(groupby_func): elif groupby_func in ("nunique",): exclude_expected = {"axis"} elif groupby_func in ("max", "min"): - exclude_expected = {"axis", "kwargs", "skipna"} + exclude_expected = {"axis", "kwargs"} exclude_result = {"min_count", "engine", "engine_kwargs"} - elif groupby_func in ("sum", "mean"): + elif groupby_func in ("sum", "mean", "std", "var"): exclude_expected = {"axis", "kwargs"} exclude_result = {"engine", "engine_kwargs"} - elif groupby_func in ("std", "var"): - exclude_expected = {"axis", "kwargs", "skipna"} - exclude_result = {"engine", "engine_kwargs"} elif groupby_func in ("median", "prod", "sem"): - exclude_expected = {"axis", "kwargs", "skipna"} + exclude_expected = {"axis", "kwargs"} elif groupby_func in ("bfill", "ffill"): exclude_expected = {"inplace", "axis", "limit_area"} elif groupby_func in ("cummax", "cummin"): @@ -235,16 +232,13 @@ def test_series_consistency(request, groupby_func): if groupby_func in ("any", "all"): exclude_expected = {"kwargs", "bool_only", "axis"} elif groupby_func in ("max", "min"): - exclude_expected = {"axis", "kwargs", "skipna"} + exclude_expected = {"axis", "kwargs"} exclude_result = {"min_count", "engine", "engine_kwargs"} - elif groupby_func in ("sum", "mean"): + elif groupby_func in ("sum", "mean", "std", "var"): exclude_expected = {"axis", "kwargs"} exclude_result = {"engine", "engine_kwargs"} - elif groupby_func in ("std", "var"): - exclude_expected = {"axis", "kwargs", "skipna"} - exclude_result = {"engine", "engine_kwargs"} elif groupby_func in ("median", "prod", "sem"): - exclude_expected = {"axis", "kwargs", "skipna"} + exclude_expected = {"axis", "kwargs"} elif groupby_func in ("bfill", "ffill"): exclude_expected = {"inplace", "axis", "limit_area"} elif groupby_func in ("cummax", "cummin"): diff --git a/pandas/tests/groupby/test_reductions.py b/pandas/tests/groupby/test_reductions.py index 1db12f05e821f..ea876cfdf4933 100644 --- a/pandas/tests/groupby/test_reductions.py +++ b/pandas/tests/groupby/test_reductions.py @@ -514,6 +514,147 @@ def test_sum_skipna_object(skipna): tm.assert_series_equal(result, expected) +@pytest.mark.parametrize( + "func, values, dtype, result_dtype", + [ + ("prod", [0, 1, 3, np.nan, 4, 5, 6, 7, -8, 9], "float64", "float64"), + ("prod", [0, -1, 3, 4, 5, np.nan, 6, 7, 8, 9], "Float64", "Float64"), + ("prod", [0, 1, 3, -4, 5, 6, 7, -8, np.nan, 9], "Int64", "Int64"), + ("prod", [np.nan] * 10, "float64", "float64"), + ("prod", [np.nan] * 10, "Float64", "Float64"), + ("prod", [np.nan] * 10, "Int64", "Int64"), + ("var", [0, -1, 3, 4, np.nan, 5, 6, 7, 8, 9], "float64", "float64"), + ("var", [0, 1, 3, -4, 5, 6, 7, -8, 9, np.nan], "Float64", "Float64"), + ("var", [0, -1, 3, 4, 5, -6, 7, np.nan, 8, 9], "Int64", "Float64"), + ("var", [np.nan] * 10, "float64", "float64"), + ("var", [np.nan] * 10, "Float64", "Float64"), + ("var", [np.nan] * 10, "Int64", "Float64"), + ("std", [0, 1, 3, -4, 5, 6, 7, -8, np.nan, 9], "float64", "float64"), + ("std", [0, -1, 3, 4, 5, -6, 7, np.nan, 8, 9], "Float64", "Float64"), + ("std", [0, 1, 3, -4, 5, 6, 7, -8, 9, np.nan], "Int64", "Float64"), + ("std", [np.nan] * 10, "float64", "float64"), + ("std", [np.nan] * 10, "Float64", "Float64"), + ("std", [np.nan] * 10, "Int64", "Float64"), + ("sem", [0, -1, 3, 4, 5, -6, 7, np.nan, 8, 9], "float64", "float64"), + ("sem", [0, 1, 3, -4, 5, 6, 7, -8, np.nan, 9], "Float64", "Float64"), + ("sem", [0, -1, 3, 4, 5, -6, 7, 8, 9, np.nan], "Int64", "Float64"), + ("sem", [np.nan] * 10, "float64", "float64"), + ("sem", [np.nan] * 10, "Float64", "Float64"), + ("sem", [np.nan] * 10, "Int64", "Float64"), + ("min", [0, -1, 3, 4, 5, -6, 7, np.nan, 8, 9], "float64", "float64"), + ("min", [0, 1, 3, -4, 5, 6, 7, -8, np.nan, 9], "Float64", "Float64"), + ("min", [0, -1, 3, 4, 5, -6, 7, 8, 9, np.nan], "Int64", "Int64"), + ( + "min", + [0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], + "timedelta64[ns]", + "timedelta64[ns]", + ), + ( + "min", + pd.to_datetime( + [ + "2019-05-09", + pd.NaT, + "2019-05-11", + "2019-05-12", + "2019-05-13", + "2019-05-14", + "2019-05-15", + "2019-05-16", + "2019-05-17", + "2019-05-18", + ] + ), + "datetime64[ns]", + "datetime64[ns]", + ), + ("min", [np.nan] * 10, "float64", "float64"), + ("min", [np.nan] * 10, "Float64", "Float64"), + ("min", [np.nan] * 10, "Int64", "Int64"), + ("max", [0, -1, 3, 4, 5, -6, 7, np.nan, 8, 9], "float64", "float64"), + ("max", [0, 1, 3, -4, 5, 6, 7, -8, np.nan, 9], "Float64", "Float64"), + ("max", [0, -1, 3, 4, 5, -6, 7, 8, 9, np.nan], "Int64", "Int64"), + ( + "max", + [0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], + "timedelta64[ns]", + "timedelta64[ns]", + ), + ( + "max", + pd.to_datetime( + [ + "2019-05-09", + pd.NaT, + "2019-05-11", + "2019-05-12", + "2019-05-13", + "2019-05-14", + "2019-05-15", + "2019-05-16", + "2019-05-17", + "2019-05-18", + ] + ), + "datetime64[ns]", + "datetime64[ns]", + ), + ("max", [np.nan] * 10, "float64", "float64"), + ("max", [np.nan] * 10, "Float64", "Float64"), + ("max", [np.nan] * 10, "Int64", "Int64"), + ("median", [0, -1, 3, 4, 5, -6, 7, np.nan, 8, 9], "float64", "float64"), + ("median", [0, 1, 3, -4, 5, 6, 7, -8, np.nan, 9], "Float64", "Float64"), + ("median", [0, -1, 3, 4, 5, -6, 7, 8, 9, np.nan], "Int64", "Float64"), + ( + "median", + [0, 1, np.nan, 3, 4, 5, 6, 7, 8, 9], + "timedelta64[ns]", + "timedelta64[ns]", + ), + ( + "median", + pd.to_datetime( + [ + "2019-05-09", + pd.NaT, + "2019-05-11", + "2019-05-12", + "2019-05-13", + "2019-05-14", + "2019-05-15", + "2019-05-16", + "2019-05-17", + "2019-05-18", + ] + ), + "datetime64[ns]", + "datetime64[ns]", + ), + ("median", [np.nan] * 10, "float64", "float64"), + ("median", [np.nan] * 10, "Float64", "Float64"), + ("median", [np.nan] * 10, "Int64", "Float64"), + ], +) +def test_multifunc_skipna(func, values, dtype, result_dtype, skipna): + # GH#15675 + df = DataFrame( + { + "val": values, + "cat": ["A", "B"] * 5, + } + ).astype({"val": dtype}) + # We need to recast the expected values to the result_dtype as some operations + # change the dtype + expected = ( + df.groupby("cat")["val"] + .apply(lambda x: getattr(x, func)(skipna=skipna)) + .astype(result_dtype) + ) + result = getattr(df.groupby("cat")["val"], func)(skipna=skipna) + tm.assert_series_equal(result, expected) + + def test_cython_median(): arr = np.random.default_rng(2).standard_normal(1000) arr[::2] = np.nan From e4f6270a7b9338c439a6352fca8029be26d8e211 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Mon, 3 Feb 2025 23:15:34 +0530 Subject: [PATCH 260/266] DOC: fix ES01 for pandas.reset_option (#60834) --- pandas/_config/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/_config/config.py b/pandas/_config/config.py index 35139979f92fe..0d06e6fa8e96c 100644 --- a/pandas/_config/config.py +++ b/pandas/_config/config.py @@ -321,6 +321,11 @@ def reset_option(pat: str) -> None: """ Reset one or more options to their default value. + This method resets the specified pandas option(s) back to their default + values. It allows partial string matching for convenience, but users should + exercise caution to avoid unintended resets due to changes in option names + in future versions. + Parameters ---------- pat : str/regex From 2a49a4f218c3819e128cd1c8ea7fc9c1f2bdf92b Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Mon, 3 Feb 2025 23:16:08 +0530 Subject: [PATCH 261/266] DOC: fix ES01 for pandas.core.resample.Resampler.indices (#60835) --- pandas/core/groupby/groupby.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 7c3088bea4b76..549e76ebc15eb 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -570,6 +570,13 @@ def indices(self) -> dict[Hashable, npt.NDArray[np.intp]]: """ Dict {group name -> group indices}. + The dictionary keys represent the group labels (e.g., timestamps for a + time-based resampling operation), and the values are arrays of integer + positions indicating where the elements of each group are located in the + original data. This property is particularly useful when working with + resampled data, as it provides insight into how the original time-series data + has been grouped. + See Also -------- core.groupby.DataFrameGroupBy.indices : Provides a mapping of group rows to From 569f94da9ecf0cd7c5eb565f5041b883726f6d3a Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Mon, 3 Feb 2025 23:16:36 +0530 Subject: [PATCH 262/266] DOC: fix ES01 for pandas.DataFrame.columns (#60836) --- pandas/core/frame.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 3669d8249dd27..d9f7623064e05 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -13673,6 +13673,10 @@ def isin_(x): doc=""" The column labels of the DataFrame. + This property holds the column names as a pandas ``Index`` object. + It provides an immutable sequence of column labels that can be + used for data selection, renaming, and alignment in DataFrame operations. + Returns ------- pandas.Index From 4f664f156badac017c3775242559953a4da50b40 Mon Sep 17 00:00:00 2001 From: Tuhin Sharma Date: Mon, 3 Feb 2025 23:17:10 +0530 Subject: [PATCH 263/266] DOC: fix ES01 for pandas.Series.array (#60837) --- pandas/core/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/core/base.py b/pandas/core/base.py index 61a7c079d87f8..a64cd8633c1db 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -506,6 +506,11 @@ def array(self) -> ExtensionArray: """ The ExtensionArray of the data backing this Series or Index. + This property provides direct access to the underlying array data of a + Series or Index without requiring conversion to a NumPy array. It + returns an ExtensionArray, which is the native storage format for + pandas extension dtypes. + Returns ------- ExtensionArray From 3bd27ffa296398c974c19571ccacd1eea76ca034 Mon Sep 17 00:00:00 2001 From: Florian Bourgey Date: Mon, 3 Feb 2025 12:51:31 -0500 Subject: [PATCH 264/266] DOC: Update parameter descriptions in `cut` function for clarity (#60839) --- pandas/core/reshape/tile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index b3f946f289891..034b861a83f43 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -73,7 +73,7 @@ def cut( Parameters ---------- - x : array-like + x : 1d ndarray or Series The input array to be binned. Must be 1-dimensional. bins : int, sequence of scalars, or IntervalIndex The criteria to bin by. @@ -126,7 +126,7 @@ def cut( Categorical for all other inputs. The values stored within are whatever the type in the sequence is. - * False : returns an ndarray of integers. + * False : returns a 1d ndarray or Series of integers. bins : numpy.ndarray or IntervalIndex. The computed or specified bins. Only returned when `retbins=True`. From c6fc6d0d7978f3958264fd372f56edf686614dac Mon Sep 17 00:00:00 2001 From: SebastianOuslis Date: Mon, 3 Feb 2025 12:53:01 -0500 Subject: [PATCH 265/266] DOC: Closed parameter not intuitively documented in DataFrame.rolling (#60832) * change docs * format * format --- pandas/core/groupby/groupby.py | 17 ++++++++++++----- pandas/core/window/rolling.py | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 549e76ebc15eb..9c27df4ed8c1b 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -3717,14 +3717,21 @@ def rolling( an integer index is not used to calculate the rolling window. closed : str, default None - If ``'right'``, the first point in the window is excluded from calculations. + Determines the inclusivity of points in the window + If ``'right'``, (First, Last] the last point in the window + is included in the calculations. - If ``'left'``, the last point in the window is excluded from calculations. + If ``'left'``, [First, Last) the first point in the window + is included in the calculations. - If ``'both'``, no points in the window are excluded from calculations. + If ``'both'``, [First, Last] all points in the window + are included in the calculations. - If ``'neither'``, the first and last points in the window are excluded - from calculations. + If ``'neither'``, (First, Last) the first and last points + in the window are excludedfrom calculations. + + () and [] are referencing open and closed set + notation respetively. Default ``None`` (``'right'``). diff --git a/pandas/core/window/rolling.py b/pandas/core/window/rolling.py index 631ab15464942..b954ce2584c13 100644 --- a/pandas/core/window/rolling.py +++ b/pandas/core/window/rolling.py @@ -929,14 +929,21 @@ class Window(BaseWindow): an integer index is not used to calculate the rolling window. closed : str, default None - If ``'right'``, the first point in the window is excluded from calculations. + Determines the inclusivity of points in the window + If ``'right'``, (First, Last] the last point in the window + is included in the calculations. - If ``'left'``, the last point in the window is excluded from calculations. + If ``'left'``, [First, Last) the first point in the window + is included in the calculations. - If ``'both'``, no point in the window is excluded from calculations. + If ``'both'``, [First, Last] all points in the window + are included in the calculations. - If ``'neither'``, the first and last points in the window are excluded - from calculations. + If ``'neither'``, (First, Last) the first and last points + in the window are excludedfrom calculations. + + () and [] are referencing open and closed set + notation respetively. Default ``None`` (``'right'``). From e58bf26fa4d806f40624fb80d8321f2cc43d62a1 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:08:43 -0800 Subject: [PATCH 266/266] CI: Update some CI configurations (#60762) * CI: Update some CI configurations * Freeze Python dev * Add actions-313.yaml * Add 3.13 yaml * Move to pyside6 instead of pyqt * Revert "Move to pyside6 instead of pyqt" This reverts commit c04039fff983db3a94f42e7e16c79cd824672757. * Revert "Add 3.13 yaml" This reverts commit 0f888e1476da8f46cacaf6e63b4a5cfc2a1a8365. * Revert "Add actions-313.yaml" This reverts commit 91e27037785cce2eb47e05b3ef726dd16e14f2bf. * Revert "Freeze Python dev" This reverts commit c685af4d5871c2ce455d81f8bf212dc0e2e31aa9. * Move back to python 3.13 dev --- .github/workflows/unit-tests.yml | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 842629ba331d6..08c41a1eeb21f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -107,7 +107,7 @@ jobs: services: mysql: - image: mysql:8 + image: mysql:9 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: pandas @@ -120,7 +120,7 @@ jobs: - 3306:3306 postgres: - image: postgres:16 + image: postgres:17 env: PGUSER: postgres POSTGRES_USER: postgres @@ -135,7 +135,7 @@ jobs: - 5432:5432 moto: - image: motoserver/moto:5.0.0 + image: motoserver/moto:5.0.27 env: AWS_ACCESS_KEY_ID: foobar_key AWS_SECRET_ACCESS_KEY: foobar_secret @@ -242,15 +242,14 @@ jobs: - name: Build environment and Run Tests # https://github.com/numpy/numpy/issues/24703#issuecomment-1722379388 run: | - /opt/python/cp311-cp311/bin/python -m venv ~/virtualenvs/pandas-dev + /opt/python/cp313-cp313/bin/python -m venv ~/virtualenvs/pandas-dev . ~/virtualenvs/pandas-dev/bin/activate python -m pip install --no-cache-dir -U pip wheel setuptools meson[ninja]==1.2.1 meson-python==0.13.1 python -m pip install numpy -Csetup-args="-Dallow-noblas=true" python -m pip install --no-cache-dir versioneer[toml] cython python-dateutil pytest>=7.3.2 pytest-xdist>=3.4.0 hypothesis>=6.84.0 python -m pip install --no-cache-dir --no-build-isolation -e . -Csetup-args="--werror" python -m pip list --no-cache-dir - export PANDAS_CI=1 - python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml + PANDAS_CI=1 python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml concurrency: # https://github.community/t/concurrecy-not-work-for-push/183068/7 group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-32bit @@ -259,7 +258,7 @@ jobs: Linux-Musl: runs-on: ubuntu-22.04 container: - image: quay.io/pypa/musllinux_1_1_x86_64 + image: quay.io/pypa/musllinux_1_2_x86_64 steps: - name: Checkout pandas Repo # actions/checkout does not work since it requires node @@ -281,7 +280,7 @@ jobs: apk add musl-locales - name: Build environment run: | - /opt/python/cp311-cp311/bin/python -m venv ~/virtualenvs/pandas-dev + /opt/python/cp313-cp313/bin/python -m venv ~/virtualenvs/pandas-dev . ~/virtualenvs/pandas-dev/bin/activate python -m pip install --no-cache-dir -U pip wheel setuptools meson-python==0.13.1 meson[ninja]==1.2.1 python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil pytest>=7.3.2 pytest-xdist>=3.4.0 hypothesis>=6.84.0 @@ -291,8 +290,7 @@ jobs: - name: Run Tests run: | . ~/virtualenvs/pandas-dev/bin/activate - export PANDAS_CI=1 - python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml + PANDAS_CI=1 python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml concurrency: # https://github.community/t/concurrecy-not-work-for-push/183068/7 group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-musl @@ -357,8 +355,7 @@ jobs: python --version python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.2.1 meson-python==0.13.1 python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy - python -m pip install versioneer[toml] - python -m pip install python-dateutil tzdata cython hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0 pytest-cov + python -m pip install versioneer[toml] python-dateutil tzdata cython hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0 pytest-cov python -m pip install -ve . --no-build-isolation --no-index --no-deps -Csetup-args="--werror" python -m pip list @@ -375,7 +372,7 @@ jobs: concurrency: # https://github.community/t/concurrecy-not-work-for-push/183068/7 - group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.os }}-python-freethreading-dev + group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-python-freethreading-dev cancel-in-progress: true env: @@ -396,14 +393,11 @@ jobs: nogil: true - name: Build Environment - # TODO: Once numpy 2.2.1 is out, don't install nightly version - # Tests segfault with numpy 2.2.0: https://github.com/numpy/numpy/pull/27955 run: | python --version - python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.2.1 meson-python==0.13.1 - python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython numpy - python -m pip install versioneer[toml] - python -m pip install python-dateutil pytz tzdata hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0 pytest-cov + python -m pip install --upgrade pip setuptools wheel numpy meson[ninja]==1.2.1 meson-python==0.13.1 + python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython + python -m pip install versioneer[toml] python-dateutil pytz tzdata hypothesis>=6.84.0 pytest>=7.3.2 pytest-xdist>=3.4.0 pytest-cov python -m pip install -ve . --no-build-isolation --no-index --no-deps -Csetup-args="--werror" python -m pip list