From a50bfe94bcc7e366cea7d01a25dc47fe968056b4 Mon Sep 17 00:00:00 2001 From: nrebena <49879400+nrebena@users.noreply.github.com> Date: Sun, 15 Mar 2020 21:25:43 +0100 Subject: [PATCH 1/3] TST: Add test for #26558 --- pandas/tests/indexes/multi/test_join.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pandas/tests/indexes/multi/test_join.py b/pandas/tests/indexes/multi/test_join.py index 062fb92c44552..b0e684cb8b04a 100644 --- a/pandas/tests/indexes/multi/test_join.py +++ b/pandas/tests/indexes/multi/test_join.py @@ -103,3 +103,21 @@ def test_join_multi_wrong_order(): tm.assert_index_equal(midx1, join_idx) assert lidx is None tm.assert_numpy_array_equal(ridx, exp_ridx) + + +@pytest.mark.parametrize( + ("data", "func"), [("2020-03-15", pd.to_datetime), ("1d", pd.to_timedelta)] +) +def test_join_mixed_index_datetime_string(data, func, join_type): + # GH 26558 + # Joining multiindex with mixed datetime and string level + mi1 = pd.MultiIndex.from_tuples([(data,)]) + mi2 = pd.MultiIndex.from_tuples([(func(data),)]) + + _, indexer_left, indexer_right = mi1.join(mi2, how=join_type, return_indexers=True) + assert indexer_left is None + assert indexer_right is None + + _, indexer_left, indexer_right = mi2.join(mi1, how=join_type, return_indexers=True) + assert indexer_left is None + assert indexer_right is None From 311ad1ca9c622299213ea97800a06f6e260d0ca7 Mon Sep 17 00:00:00 2001 From: nrebena <49879400+nrebena@users.noreply.github.com> Date: Sun, 15 Mar 2020 20:00:06 +0100 Subject: [PATCH 2/3] BUG: #26558 Fix join on MultiIndex with datetime and string --- pandas/core/dtypes/generic.py | 3 +++ pandas/core/indexes/base.py | 12 +++++++++++- pandas/core/indexes/datetimelike.py | 18 +++++++++++------- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pandas/core/dtypes/generic.py b/pandas/core/dtypes/generic.py index 435d80b2c4dfb..caa7a9f127aa3 100644 --- a/pandas/core/dtypes/generic.py +++ b/pandas/core/dtypes/generic.py @@ -28,6 +28,9 @@ def _check(cls, inst) -> bool: ABCTimedeltaIndex = create_pandas_abc_type( "ABCTimedeltaIndex", "_typ", ("timedeltaindex",) ) +ABCDatetimeTimedeltaMixin = create_pandas_abc_type( + "ABCDatetimeTimedeltaMixin", "_typ", ("datetimeindex", "timedeltaindex",) +) ABCPeriodIndex = create_pandas_abc_type("ABCPeriodIndex", "_typ", ("periodindex",)) ABCCategoricalIndex = create_pandas_abc_type( "ABCCategoricalIndex", "_typ", ("categoricalindex",) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 31966489403f4..4432a09b074f8 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -54,6 +54,7 @@ ABCCategorical, ABCDataFrame, ABCDatetimeIndex, + ABCDatetimeTimedeltaMixin, ABCIntervalIndex, ABCMultiIndex, ABCPandasArray, @@ -3408,7 +3409,16 @@ def join(self, other, how="left", level=None, return_indexers=False, sort=False) # have the same levels/names so a simple join if self.names == other.names: - pass + # For datetimelike levels, we maybe convert the corresponding level + for i, (left, right) in enumerate(zip(self.levels, other.levels)): + if isinstance(left, ABCDatetimeTimedeltaMixin): + left, right = left._prepare_for_join(right) + self = self.set_levels(left, i) + other = other.set_levels(right, i) + elif isinstance(right, ABCDatetimeTimedeltaMixin): + right, left = right._prepare_for_join(left) + self = self.set_levels(left, i) + other = other.set_levels(right, i) else: return self._join_multi(other, how=how, return_indexers=return_indexers) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 054a64bf3f990..288c0650104b5 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -830,13 +830,7 @@ def join( """ See Index.join """ - if self._is_convertible_to_index_for_join(other): - try: - other = type(self)(other) - except (TypeError, ValueError): - pass - - this, other = self._maybe_utc_convert(other) + this, other = self._prepare_for_join(other) return Index.join( this, other, @@ -846,6 +840,16 @@ def join( sort=sort, ) + def _prepare_for_join(self, other): + if self._is_convertible_to_index_for_join(other): + try: + other = type(self)(other) + except (TypeError, ValueError): + pass + + this, other = self._maybe_utc_convert(other) + return this, other + def _maybe_utc_convert(self, other): this = self if not hasattr(self, "tz"): From a59fcc9b73868215678724d4107ce970ace92beb Mon Sep 17 00:00:00 2001 From: nrebena <49879400+nrebena@users.noreply.github.com> Date: Sun, 15 Mar 2020 23:16:58 +0100 Subject: [PATCH 3/3] DOC: Bugfix for MultiIndex in whatsnew --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 4e7bd5a2032a7..e03d2d4376e8a 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -324,7 +324,7 @@ MultiIndex # Common elements are now guaranteed to be ordered by the left side left.intersection(right, sort=False) -- +- Bug in :meth:`Index.join` for MultiIndex when joining level between datetimelike and string(:issue:`26558`) I/O ^^^