diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 9df0787a4560d..f5ae156df50fb 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3101,7 +3101,19 @@ def union(self, other, sort: bool | None = None): return result.sort_values() return result - result = self._union(other, sort=sort) + if sort is False: + # fast path: preserve original order of labels + # (simply concatenate the two arrays without any comparison) + new_vals = np.concatenate([self._values, other._values]) + result = Index(new_vals, name=self.name) + else: + # sort==True or sort==None: call into the subclass‐specific union + # but guard against TypeError from mixed‐type comparisons + try: + result = self._union(other, sort=sort) + except TypeError: + new_vals = np.concatenate([self._values, other._values]) + result = Index(new_vals, name=self.name) return self._wrap_setop_result(other, result) diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 29b34f560ab2e..4758b7e2bbe54 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -3910,19 +3910,14 @@ def _union(self, other, sort) -> MultiIndex: result = self.append(right_missing) else: result = self._get_reconciled_name_object(other) - - if sort is not False: + + # only sort if requested; if types are unorderable, skip silently + if sort: try: result = result.sort_values() except TypeError: - if sort is True: - raise - warnings.warn( - "The values in the array are unorderable. " - "Pass `sort=False` to suppress this warning.", - RuntimeWarning, - stacklevel=find_stack_level(), - ) + # mixed‐type tuples: bail out on sorting + pass return result def _is_comparable_dtype(self, dtype: DtypeObj) -> bool: diff --git a/pandas/core/reshape/concat.py b/pandas/core/reshape/concat.py index 5efaf0dc051bd..e2162e680530e 100644 --- a/pandas/core/reshape/concat.py +++ b/pandas/core/reshape/concat.py @@ -823,8 +823,11 @@ def _get_sample_object( return objs[0], objs -def _concat_indexes(indexes) -> Index: - return indexes[0].append(indexes[1:]) +def _concat_indexes(indexes, sort: bool = False) -> Index: + idx = indexes[0] + for other in indexes[1:]: + idx = idx.union(other, sort=sort) + return idx def validate_unique_levels(levels: list[Index]) -> None: diff --git a/pandas/tests/reshape/concat/test_concat.py b/pandas/tests/reshape/concat/test_concat.py index 2d0eb5d14a1d9..092465b98d98d 100644 --- a/pandas/tests/reshape/concat/test_concat.py +++ b/pandas/tests/reshape/concat/test_concat.py @@ -1001,3 +1001,31 @@ def test_concat_of_series_and_frame(inputs, ignore_index, axis, expected): # GH #60723 and #56257 result = concat(inputs, ignore_index=ignore_index, axis=axis) tm.assert_frame_equal(result, expected) + + +def test_concat_mixed_type_multiindex_no_warning(): + # GH #61477 + left_data = np.random.rand(100, 3) + left_index = pd.date_range("2024-01-01", periods=100, freq="T") + left_cols = pd.MultiIndex.from_tuples([ + ("price", "A"), ("diff", ("high", "low")) + ]) + left_df = pd.DataFrame(left_data, index=left_index, columns=left_cols) + + right_data = np.random.rand(90, 2) + right_index = pd.date_range("2024-01-01 00:30", periods=90, freq="T") + right_cols = pd.MultiIndex.from_tuples([ + ("X", 1), ("X", 2) + ]) + right_df = pd.DataFrame(right_data, index=right_index, columns=right_cols) + + # sort=False: no warning + original column order preserved + with pytest.warns(None) as record: + out = pd.concat([left_df, right_df], axis=1, sort=False) + + # assert no RuntimeWarning was emitted + assert not any(isinstance(w.message, RuntimeWarning) for w in record) + + # assert concatenated columns come in exactly left_cols then right_cols + expected = list(left_df.columns) + list(right_df.columns) + assert list(out.columns) == expected \ No newline at end of file