Repository: pandas-dev/pandas

Issue:
Combine_first method converts booleans into floats
#### Works as expected:

```python
>>> df1 = pd.DataFrame()
>>> df2 = pd.DataFrame({'isBool': [True]})
>>> df1.combine_first(df2)
   isBool
0    True
```
#### Does not work as expected:

```python
>>> df1 = pd.DataFrame({'isInt': [1]})
>>> df2 = pd.DataFrame({'isBool': [True]})
>>> df1.combine_first(df2)
   isBool  isInt
0     1.0      1
```
#### Problem description
Would expect Pandas to preserve the bool dtype (or at least the behavior should differ for the case where df1 is empty or not).

See also: https://stackoverflow.com/questions/39103144/pandas-dataframe-combine-first-method-converts-boolean-in-floats
And related: https://stackoverflow.com/questions/15349795/pandas-dataframe-combine-first-and-update-methods-have-strange-behavior
And: https://github.com/pandas-dev/pandas/issues/3016

Seems to me some of the issues where solved but this case may have escaped(?)

#### Output of ``pd.show_versions()``:
<details>
commit: None
python: 3.6.3.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 158 Stepping 9, GenuineIntel
byteorder: little
LC_ALL: None
LANG: en
LOCALE: None.None

pandas: 0.21.1
pytest: 3.5.0
pip: 9.0.3
setuptools: 38.5.2
Cython: None
numpy: 1.13.3
scipy: 1.0.0
pyarrow: None
xarray: None
IPython: 6.2.1
sphinx: 1.7.1
patsy: None
dateutil: 2.7.0
pytz: 2018.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 2.1.1
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: 1.0.1
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>


Discussion:
I see no reason why `dtype` shouldn't be preserved.  PR to patch is welcome!

Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -5072,9 +5072,17 @@ def combine(self, other, func, fill_value=None, overwrite=True):
                 series[this_mask] = fill_value
                 otherSeries[other_mask] = fill_value
 
-            # if we have different dtypes, possibly promote
-            new_dtype = this_dtype
-            if not is_dtype_equal(this_dtype, other_dtype):
+            if col not in self.columns:
+                # If self DataFrame does not have col in other DataFrame,
+                # try to promote series, which is all NaN, as other_dtype.
+                new_dtype = other_dtype
+                try:
+                    series = series.astype(new_dtype, copy=False)
+                except ValueError:
+                    # e.g. new_dtype is integer types
+                    pass
+            else:
+                # if we have different dtypes, possibly promote
                 new_dtype = find_common_type([this_dtype, other_dtype])
                 if not is_dtype_equal(this_dtype, new_dtype):
                     series = series.astype(new_dtype)
@@ -5153,6 +5161,11 @@ def combiner(x, y, needs_i8_conversion=False):
             else:
                 mask = isna(x_values)
 
+            # If the column y in other DataFrame is not in first DataFrame,
+            # just return y_values.
+            if y.name not in self.columns:
+                return y_values
+
             return expressions.where(mask, y_values, x_values)
 
         return self.combine(other, combiner, overwrite=False)


Test patch:
diff --git a/pandas/tests/frame/test_combine_concat.py b/pandas/tests/frame/test_combine_concat.py
--- a/pandas/tests/frame/test_combine_concat.py
+++ b/pandas/tests/frame/test_combine_concat.py
@@ -4,6 +4,7 @@
 
 from datetime import datetime
 
+import pytest
 import numpy as np
 from numpy import nan
 
@@ -750,6 +751,17 @@ def test_combine_first_int(self):
         tm.assert_frame_equal(res, df1)
         assert res['a'].dtype == 'int64'
 
+    @pytest.mark.parametrize("val", [1, 1.0])
+    def test_combine_first_with_asymmetric_other(self, val):
+        # see gh-20699
+        df1 = pd.DataFrame({'isNum': [val]})
+        df2 = pd.DataFrame({'isBool': [True]})
+
+        res = df1.combine_first(df2)
+        exp = pd.DataFrame({'isBool': [True], 'isNum': [val]})
+
+        tm.assert_frame_equal(res, exp)
+
     def test_concat_datetime_datetime64_frame(self):
         # #2624
         rows = []