Repository: pandas-dev/pandas

Issue:
merge of int and float column results in column of dtype object
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd

A = pd.DataFrame({'X': [1, 2, 3]})
B = pd.DataFrame({'Y': [1.0, 3.0]})

B = B.merge(A, left_on='Y', right_on='X')

print(B.dtypes)
```
#### Problem description
The output is
```
Y    object
X     int64
dtype: object
```
meaning that Y was incorrectly upcast to object.

In pandas 0.19, the dtype of B.Y after merging is float64.
In pandas 0.20, the dtype of B.Y after merging is object.

The problem only occurs if some keys in B.Y are missing (try adding 2.0)

#### Expected Output
```
Y    float64
X      int64
dtype: object
```

#### Output of ``pd.show_versions()``
------------------

<details>
commit: None
python: 3.5.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.9.0-2-amd64
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.20.1
pytest: None
pip: 9.0.1
setuptools: 35.0.2
Cython: None
numpy: 1.12.1
scipy: 0.19.0
xarray: None
IPython: 6.0.0
sphinx: None
patsy: None
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 2.0.2
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: 0.999999999
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
pandas_gbq: None
pandas_datareader: None
</details>



Discussion:
this does looks like a bug. I don't think we have this as a test. This is a fairly narrow case though, the floats have to be exactly equal to the integers. welcome to have a PR. I can point you to where the issue is.
I encountered this issue while getting data via `pd.read_sql()`. There, primary keys are returned as `int`s (since there are no missing values), but nullable foreign keys are upcast to `float` (so that nulls can be represented as `nan`). If you then do a merge on these columns, you get what is described above. If you point me to a code location, I will see what I can do.
Its right about here: https://github.com/pandas-dev/pandas/blob/master/pandas/core/reshape/merge.py#L911

We allow differing int types, e.g. int64 and int8 are ok. but int64 and float will fall thru. you can add a check like

```
In [8]: lk = np.array([1.0, 2, 3])

In [9]: rk = np.array([2, 3, 4])

In [11]: if is_float_dtype(lk) and is_integer_dtype(rk):
    ...:     if (lk == lk.astype('int64')).all():
    ...:         continue
             # check opposite way as well
    
```



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
@@ -906,16 +906,31 @@ def _maybe_coerce_merge_keys(self):
                 continue
 
             # if we are numeric, then allow differing
-            # kinds to proceed, eg. int64 and int8
+            # kinds to proceed, eg. int64 and int8, int and float
             # further if we are object, but we infer to
             # the same, then proceed
             if is_numeric_dtype(lk) and is_numeric_dtype(rk):
                 if lk.dtype.kind == rk.dtype.kind:
-                    continue
+                    pass
+
+                # check whether ints and floats
+                elif is_integer_dtype(rk) and is_float_dtype(lk):
+                    if not (lk == lk.astype(rk.dtype)).all():
+                        warnings.warn('You are merging on int and float '
+                                      'columns where the float values '
+                                      'are not equal to their int '
+                                      'representation', UserWarning)
+
+                elif is_float_dtype(rk) and is_integer_dtype(lk):
+                    if not (rk == rk.astype(lk.dtype)).all():
+                        warnings.warn('You are merging on int and float '
+                                      'columns where the float values '
+                                      'are not equal to their int '
+                                      'representation', UserWarning)
 
                 # let's infer and see if we are ok
-                if lib.infer_dtype(lk) == lib.infer_dtype(rk):
-                    continue
+                elif lib.infer_dtype(lk) == lib.infer_dtype(rk):
+                    pass
 
             # Houston, we have a problem!
             # let's coerce to object if the dtypes aren't
@@ -924,14 +939,15 @@ def _maybe_coerce_merge_keys(self):
             # then we would lose type information on some
             # columns, and end up trying to merge
             # incompatible dtypes. See GH 16900.
-            if name in self.left.columns:
-                typ = lk.categories.dtype if lk_is_cat else object
-                self.left = self.left.assign(
-                    **{name: self.left[name].astype(typ)})
-            if name in self.right.columns:
-                typ = rk.categories.dtype if rk_is_cat else object
-                self.right = self.right.assign(
-                    **{name: self.right[name].astype(typ)})
+            else:
+                if name in self.left.columns:
+                    typ = lk.categories.dtype if lk_is_cat else object
+                    self.left = self.left.assign(
+                        **{name: self.left[name].astype(typ)})
+                if name in self.right.columns:
+                    typ = rk.categories.dtype if rk_is_cat else object
+                    self.right = self.right.assign(
+                        **{name: self.right[name].astype(typ)})
 
     def _validate_specification(self):
         # Hm, any way to make this logic less complicated??


Test patch:
diff --git a/pandas/tests/reshape/test_merge.py b/pandas/tests/reshape/test_merge.py
--- a/pandas/tests/reshape/test_merge.py
+++ b/pandas/tests/reshape/test_merge.py
@@ -13,7 +13,10 @@
 from pandas.core.reshape.merge import merge, MergeError
 from pandas.util.testing import assert_frame_equal, assert_series_equal
 from pandas.core.dtypes.dtypes import CategoricalDtype
-from pandas.core.dtypes.common import is_categorical_dtype, is_object_dtype
+from pandas.core.dtypes.common import (
+    is_categorical_dtype,
+    is_object_dtype,
+)
 from pandas import DataFrame, Index, MultiIndex, Series, Categorical
 import pandas.util.testing as tm
 from pandas.api.types import CategoricalDtype as CDT
@@ -1408,6 +1411,42 @@ def test_join_multi_dtypes(self, d1, d2):
         expected.sort_values(['k1', 'k2'], kind='mergesort', inplace=True)
         tm.assert_frame_equal(result, expected)
 
+    @pytest.mark.parametrize('int_vals, float_vals, exp_vals', [
+        ([1, 2, 3], [1.0, 2.0, 3.0], {'X': [1, 2, 3], 'Y': [1.0, 2.0, 3.0]}),
+        ([1, 2, 3], [1.0, 3.0], {'X': [1, 3], 'Y': [1.0, 3.0]}),
+        ([1, 2], [1.0, 2.0, 3.0], {'X': [1, 2], 'Y': [1.0, 2.0]}),
+    ])
+    def test_merge_on_ints_floats(self, int_vals, float_vals, exp_vals):
+        # GH 16572
+        # Check that float column is not cast to object if
+        # merging on float and int columns
+        A = DataFrame({'X': int_vals})
+        B = DataFrame({'Y': float_vals})
+        expected = DataFrame(exp_vals)
+
+        result = A.merge(B, left_on='X', right_on='Y')
+        assert_frame_equal(result, expected)
+
+        result = B.merge(A, left_on='Y', right_on='X')
+        assert_frame_equal(result, expected[['Y', 'X']])
+
+    def test_merge_on_ints_floats_warning(self):
+        # GH 16572
+        # merge will produce a warning when merging on int and
+        # float columns where the float values are not exactly
+        # equal to their int representation
+        A = DataFrame({'X': [1, 2, 3]})
+        B = DataFrame({'Y': [1.1, 2.5, 3.0]})
+        expected = DataFrame({'X': [3], 'Y': [3.0]})
+
+        with tm.assert_produces_warning(UserWarning):
+            result = A.merge(B, left_on='X', right_on='Y')
+            assert_frame_equal(result, expected)
+
+        with tm.assert_produces_warning(UserWarning):
+            result = B.merge(A, left_on='Y', right_on='X')
+            assert_frame_equal(result, expected[['Y', 'X']])
+
 
 @pytest.fixture
 def left():