Repository: pandas-dev/pandas

Issue:
BUG: merging on an Integer EA raises
```
In [1]: df = pd.DataFrame({'A': pd.Series([1, 2, np.nan], dtype='Int64'), 'B': 1})
```

Merging with an EA included is ok
```
In [2]: pd.merge(df, df, on='B')
Out[2]: 
   A_x  B  A_y
0    1  1    1
1    1  1    2
2    1  1  NaN
3    2  1    1
4    2  1    2
5    2  1  NaN
6  NaN  1    1
7  NaN  1    2
8  NaN  1  NaN

In [3]: pd.merge(df, df, on='B').dtypes
Out[3]: 
A_x    Int64
B      int64
A_y    Int64
dtype: object
```

On the EA column is not Ok
```
In [4]: pd.merge(df, df, on='A').dtypes
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-e77608ca3973> in <module>()
----> 1 pd.merge(df, df, on='A').dtypes

~/pandas/pandas/core/reshape/merge.py in merge(left, right, how, on, left_on, right_on, left_index, right_index, sort, suffixes, copy, indicator, validate)
     61                          copy=copy, indicator=indicator,
     62                          validate=validate)
---> 63     return op.get_result()
     64 
     65 

~/pandas/pandas/core/reshape/merge.py in get_result(self)
    562                 self.left, self.right)
    563 
--> 564         join_index, left_indexer, right_indexer = self._get_join_info()
    565 
    566         ldata, rdata = self.left._data, self.right._data

~/pandas/pandas/core/reshape/merge.py in _get_join_info(self)
    771         else:
    772             (left_indexer,
--> 773              right_indexer) = self._get_join_indexers()
    774 
    775             if self.right_index:

~/pandas/pandas/core/reshape/merge.py in _get_join_indexers(self)
    750                                   self.right_join_keys,
    751                                   sort=self.sort,
--> 752                                   how=self.how)
    753 
    754     def _get_join_info(self):

~/pandas/pandas/core/reshape/merge.py in _get_join_indexers(left_keys, right_keys, sort, how, **kwargs)
   1120 
   1121     # get left & right join labels and num. of levels at each location
-> 1122     llab, rlab, shape = map(list, zip(* map(fkeys, left_keys, right_keys)))
   1123 
   1124     # get flat i8 keys from label lists

~/pandas/pandas/core/reshape/merge.py in _factorize_keys(lk, rk, sort)
   1554     elif is_int_or_datetime_dtype(lk) and is_int_or_datetime_dtype(rk):
   1555         klass = libhashtable.Int64Factorizer
-> 1556         lk = ensure_int64(com.values_from_object(lk))
   1557         rk = ensure_int64(com.values_from_object(rk))
   1558     else:   1557         rk = ensure_int64(com.values_from_object(rk))
   1558     else:

~/pandas/pandas/_libs/algos_common_helper.pxi in pandas._libs.algos.ensure_int64()

ValueError: cannot convert float NaN to integer

```


Discussion:
this doesn't look that hard to fix actually, need an

``if is_extension_array_dtype`` clause here, then maybe some logic to defer to the interface to prepare for the factorizing

Resolving patch:
diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py
--- a/pandas/core/reshape/merge.py
+++ b/pandas/core/reshape/merge.py
@@ -17,9 +17,10 @@
 from pandas.core.dtypes.common import (
     ensure_float64, ensure_int64, ensure_object, is_array_like, is_bool,
     is_bool_dtype, is_categorical_dtype, is_datetime64_dtype,
-    is_datetime64tz_dtype, is_datetimelike, is_dtype_equal, is_float_dtype,
-    is_int64_dtype, is_integer, is_integer_dtype, is_list_like, is_number,
-    is_numeric_dtype, needs_i8_conversion)
+    is_datetime64tz_dtype, is_datetimelike, is_dtype_equal,
+    is_extension_array_dtype, is_float_dtype, is_int64_dtype, is_integer,
+    is_integer_dtype, is_list_like, is_number, is_numeric_dtype,
+    needs_i8_conversion)
 from pandas.core.dtypes.missing import isnull, na_value_for_dtype
 
 from pandas import Categorical, DataFrame, Index, MultiIndex, Series, Timedelta
@@ -1589,17 +1590,16 @@ def _right_outer_join(x, y, max_groups):
 
 
 def _factorize_keys(lk, rk, sort=True):
+    # Some pre-processing for non-ndarray lk / rk
     if is_datetime64tz_dtype(lk) and is_datetime64tz_dtype(rk):
         lk = lk.values
         rk = rk.values
 
-    # if we exactly match in categories, allow us to factorize on codes
-    if (is_categorical_dtype(lk) and
+    elif (is_categorical_dtype(lk) and
             is_categorical_dtype(rk) and
             lk.is_dtype_equal(rk)):
-        klass = libhashtable.Int64Factorizer
-
         if lk.categories.equals(rk.categories):
+            # if we exactly match in categories, allow us to factorize on codes
             rk = rk.codes
         else:
             # Same categories in different orders -> recode
@@ -1607,7 +1607,14 @@ def _factorize_keys(lk, rk, sort=True):
 
         lk = ensure_int64(lk.codes)
         rk = ensure_int64(rk)
-    elif is_integer_dtype(lk) and is_integer_dtype(rk):
+
+    elif (is_extension_array_dtype(lk.dtype) and
+          is_extension_array_dtype(rk.dtype) and
+          lk.dtype == rk.dtype):
+        lk, _ = lk._values_for_factorize()
+        rk, _ = rk._values_for_factorize()
+
+    if is_integer_dtype(lk) and is_integer_dtype(rk):
         # GH#23917 TODO: needs tests for case where lk is integer-dtype
         #  and rk is datetime-dtype
         klass = libhashtable.Int64Factorizer


Test patch:
diff --git a/pandas/tests/extension/base/reshaping.py b/pandas/tests/extension/base/reshaping.py
--- a/pandas/tests/extension/base/reshaping.py
+++ b/pandas/tests/extension/base/reshaping.py
@@ -173,6 +173,38 @@ def test_merge(self, data, na_value):
                  dtype=data.dtype)})
         self.assert_frame_equal(res, exp[['ext', 'int1', 'key', 'int2']])
 
+    def test_merge_on_extension_array(self, data):
+        # GH 23020
+        a, b = data[:2]
+        key = type(data)._from_sequence([a, b], dtype=data.dtype)
+
+        df = pd.DataFrame({"key": key, "val": [1, 2]})
+        result = pd.merge(df, df, on='key')
+        expected = pd.DataFrame({"key": key,
+                                 "val_x": [1, 2],
+                                 "val_y": [1, 2]})
+        self.assert_frame_equal(result, expected)
+
+        # order
+        result = pd.merge(df.iloc[[1, 0]], df, on='key')
+        expected = expected.iloc[[1, 0]].reset_index(drop=True)
+        self.assert_frame_equal(result, expected)
+
+    def test_merge_on_extension_array_duplicates(self, data):
+        # GH 23020
+        a, b = data[:2]
+        key = type(data)._from_sequence([a, b, a], dtype=data.dtype)
+        df1 = pd.DataFrame({"key": key, "val": [1, 2, 3]})
+        df2 = pd.DataFrame({"key": key, "val": [1, 2, 3]})
+
+        result = pd.merge(df1, df2, on='key')
+        expected = pd.DataFrame({
+            "key": key.take([0, 0, 0, 0, 1]),
+            "val_x": [1, 1, 3, 3, 2],
+            "val_y": [1, 3, 1, 3, 2],
+        })
+        self.assert_frame_equal(result, expected)
+
     @pytest.mark.parametrize("columns", [
         ["A", "B"],
         pd.MultiIndex.from_tuples([('A', 'a'), ('A', 'b')],
diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py
--- a/pandas/tests/reshape/merge/test_merge.py
+++ b/pandas/tests/reshape/merge/test_merge.py
@@ -1326,6 +1326,16 @@ def test_merging_with_bool_or_int_cateorical_column(self, category_column,
             CDT(categories, ordered=ordered))
         assert_frame_equal(expected, result)
 
+    def test_merge_on_int_array(self):
+        # GH 23020
+        df = pd.DataFrame({'A': pd.Series([1, 2, np.nan], dtype='Int64'),
+                           'B': 1})
+        result = pd.merge(df, df, on='A')
+        expected = pd.DataFrame({'A': pd.Series([1, 2, np.nan], dtype='Int64'),
+                                 'B_x': 1,
+                                 'B_y': 1})
+        assert_frame_equal(result, expected)
+
 
 @pytest.fixture
 def left_df():