From e0162ccd92582b3c99fa22abca2c7a6bcea06c73 Mon Sep 17 00:00:00 2001 From: SoulSniper1212 Date: Tue, 11 Nov 2025 16:23:25 -0500 Subject: [PATCH 1/5] FIX: Preserve dtypes when using scalar row + slice columns indexing Signed-off-by: SoulSniper1212 --- pandas/core/indexing.py | 74 +++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index aa1bc8878dcb2..493e6fe2ee69b 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1088,37 +1088,53 @@ def _getitem_lowerdim(self, tup: tuple): tup = self._validate_key_length(tup) - # Reverse tuple so that we are indexing along columns before rows + # For scalar row + slice columns, process by first getting the column slice + # to preserve dtypes, then extracting the row + # Otherwise, reverse tuple so that we are indexing along columns before rows # and avoid unintended dtype inference. # GH60600 - for i, key in zip(range(len(tup) - 1, -1, -1), reversed(tup), strict=True): - if is_label_like(key) or is_list_like(key): - # We don't need to check for tuples here because those are - # caught by the _is_nested_tuple_indexer check above. - section = self._getitem_axis(key, axis=i) - - # We should never have a scalar section here, because - # _getitem_lowerdim is only called after a check for - # is_scalar_access, which that would be. - if section.ndim == self.ndim: - # we're in the middle of slicing through a MultiIndex - # revise the key wrt to `section` by inserting an _NS - new_key = tup[:i] + (_NS,) + tup[i + 1 :] + if (len(tup) == 2 and is_scalar(tup[0]) and isinstance(tup[1], slice)): + # Handle scalar row + slice columns case to preserve dtypes + row_key, col_slice = tup[0], tup[1] + # First, get the column slice to create sub-DataFrame with preserved column dtypes + col_section = self._getitem_axis(col_slice, axis=1) + # Then get the specific row from this sub-DataFrame using appropriate indexer + if self.name == "iloc": + result = col_section.iloc[row_key] + else: + result = col_section.loc[row_key] + return result + else: + # Reverse tuple so that we are indexing along columns before rows + # and avoid unintended dtype inference. # GH60600 + for i, key in zip(range(len(tup) - 1, -1, -1), reversed(tup), strict=True): + if is_label_like(key) or is_list_like(key): + # We don't need to check for tuples here because those are + # caught by the _is_nested_tuple_indexer check above. + section = self._getitem_axis(key, axis=i) + + # We should never have a scalar section here, because + # _getitem_lowerdim is only called after a check for + # is_scalar_access, which that would be. + if section.ndim == self.ndim: + # we're in the middle of slicing through a MultiIndex + # revise the key wrt to `section` by inserting an _NS + new_key = tup[:i] + (_NS,) + tup[i + 1 :] - else: - # Note: the section.ndim == self.ndim check above - # rules out having DataFrame here, so we dont need to worry - # about transposing. - new_key = tup[:i] + tup[i + 1 :] - - if len(new_key) == 1: - new_key = new_key[0] - - # Slices should return views, but calling iloc/loc with a null - # slice returns a new object. - if com.is_null_slice(new_key): - return section - # This is an elided recursive call to iloc/loc - return getattr(section, self.name)[new_key] + else: + # Note: the section.ndim == self.ndim check above + # rules out having DataFrame here, so we dont need to worry + # about transposing. + new_key = tup[:i] + tup[i + 1 :] + + if len(new_key) == 1: + new_key = new_key[0] + + # Slices should return views, but calling iloc/loc with a null + # slice returns a new object. + if com.is_null_slice(new_key): + return section + # This is an elided recursive call to iloc/loc + return getattr(section, self.name)[new_key] raise IndexingError("not applicable") From 00e315a657d73d55ca286cf6b20bfa5b824619ee Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 21 Nov 2025 12:26:17 -0500 Subject: [PATCH 2/5] Add tests for scalar row + slice columns dtype preservation --- pandas/tests/indexing/test_iloc.py | 8 ++++++++ pandas/tests/indexing/test_loc.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py index ddb58ecbfa6f3..5e8a2a2d604e7 100644 --- a/pandas/tests/indexing/test_iloc.py +++ b/pandas/tests/indexing/test_iloc.py @@ -1548,3 +1548,11 @@ def test_setitem_pyarrow_int_series(self): expected = Series([7, 8, 3], dtype="int64[pyarrow]") tm.assert_series_equal(ser, expected) + + + def test_iloc_scalar_row_slice_columns_dtype(self): + # GH 63071 + df = DataFrame([["a", 1.0, 2.0], ["b", 3.0, 4.0]]) + result = df.iloc[0, 1:] + expected = Series([1.0, 2.0], index=[1, 2], dtype=float, name=0) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 8d59b0c026e0c..534f70f8db591 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -69,6 +69,14 @@ def test_loc_dtype(): tm.assert_series_equal(result, expected) +def test_loc_scalar_row_slice_columns_dtype(): + # GH 63071 + df = DataFrame([["a", 1.0, 2.0], ["b", 3.0, 4.0]]) + result = df.loc[0, 1:] + expected = Series([1.0, 2.0], index=[1, 2], dtype=float, name=0) + tm.assert_series_equal(result, expected) + + class TestLoc: def test_none_values_on_string_columns(self, using_infer_string): # Issue #32218 From d23b13923e88b2c407fc90e6b2d3c2a997a0d6dd Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 21 Nov 2025 12:28:40 -0500 Subject: [PATCH 3/5] Fix line length in comments --- pandas/core/indexing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 493e6fe2ee69b..75a40de2eda4a 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1095,9 +1095,9 @@ def _getitem_lowerdim(self, tup: tuple): if (len(tup) == 2 and is_scalar(tup[0]) and isinstance(tup[1], slice)): # Handle scalar row + slice columns case to preserve dtypes row_key, col_slice = tup[0], tup[1] - # First, get the column slice to create sub-DataFrame with preserved column dtypes + # First, get the column slice to preserve dtypes col_section = self._getitem_axis(col_slice, axis=1) - # Then get the specific row from this sub-DataFrame using appropriate indexer + # Then get the specific row from this sub-DataFrame if self.name == "iloc": result = col_section.iloc[row_key] else: From 81fa6a3b853f3f33e0c4e53b5b21f779e051c212 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 21 Nov 2025 12:32:01 -0500 Subject: [PATCH 4/5] Apply ruff formatting --- pandas/tests/indexing/test_iloc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py index 5e8a2a2d604e7..6c05d05ace287 100644 --- a/pandas/tests/indexing/test_iloc.py +++ b/pandas/tests/indexing/test_iloc.py @@ -1549,7 +1549,6 @@ def test_setitem_pyarrow_int_series(self): expected = Series([7, 8, 3], dtype="int64[pyarrow]") tm.assert_series_equal(ser, expected) - def test_iloc_scalar_row_slice_columns_dtype(self): # GH 63071 df = DataFrame([["a", 1.0, 2.0], ["b", 3.0, 4.0]]) From 75edf0519bc0890babe98c53278848d87a982e00 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 21 Nov 2025 12:34:48 -0500 Subject: [PATCH 5/5] Remove unnecessary parentheses --- 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 75a40de2eda4a..42f3b008f3cfd 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1092,7 +1092,7 @@ def _getitem_lowerdim(self, tup: tuple): # to preserve dtypes, then extracting the row # Otherwise, reverse tuple so that we are indexing along columns before rows # and avoid unintended dtype inference. # GH60600 - if (len(tup) == 2 and is_scalar(tup[0]) and isinstance(tup[1], slice)): + if len(tup) == 2 and is_scalar(tup[0]) and isinstance(tup[1], slice): # Handle scalar row + slice columns case to preserve dtypes row_key, col_slice = tup[0], tup[1] # First, get the column slice to preserve dtypes