BUG/PERF: Series.combine_first converting int64 to float64
State: closed

- [x] closes #51764
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/v2.1.0.rst` file if fixing a bug or adding a new feature.




Labels:
name: Bug
name: Performance
description: Memory or execution speed performance
name: Reshaping
description: Concat, Merge/Join, Stack/Unstack, Explode
name: Dtype Conversions
description: Unexpected or buggy dtype conversions

Comments:
If the solution to this issue is to convert floats back to ints when they get converted to floats, that will not work correctly for some ints.  For example:

```
>>> s1 = pd.Series([1666880195890293744, 1666880195890293837])
>>>
>>> s1
0    1666880195890293744
1    1666880195890293837
dtype: int64
>>>
>>> s1.astype(float).astype(int)
0    1666880195890293760
1    1666880195890293760
dtype: int64
```

It would be good to include big ints like these in the tests.

> If the solution to this issue is to convert floats back to ints when they get converted to floats, that will not work correctly for some ints

great catch! thanks. I've updated the PR to handle this and updated the test. 

This also provides a nice perf improvement:

```
import pandas as pd
import numpy as np

N = 1_000_000

s1 = pd.Series(np.random.randint(0, N, N), dtype="int64")
s1 = s1.iloc[:-5]

s2 = pd.Series(np.random.randint(0, N, N), dtype="int64")
s2 = s2.iloc[5:]

%timeit s1.combine_first(s2)

# 59.4 ms ± 4.15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)     -> main
# 1.37 ms ± 20.9 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)  -> PR
```

And I'll note the perf improvement is not specific to the integer case, here is `float64`:

```
import pandas as pd
import numpy as np

N = 1_000_000

s1 = pd.Series(np.random.randn(N), dtype="float64")
s1 = s1.iloc[:-5]

s2 = pd.Series(np.random.randn(N), dtype="float64")
s2 = s2.iloc[5:]

%timeit s1.combine_first(s2)

# 48.4 ms ± 169 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)      -> main
# 1.64 ms ± 7.26 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)  -> PR
```
It looks like you are using these big integers in the tests to test for loss of precision:

```
>>> s1 = Series([np.iinfo(np.int64).min, np.iinfo(np.int64).max])
```

But note that there is no loss in going from int to float to int for these special numbers:

```
>>> s1
0   -9223372036854775808
1    9223372036854775807
dtype: int64

>>> s1.astype(float).astype(int)
0   -9223372036854775808
1    9223372036854775807
dtype: int64
```

It would be good to use ints in the tests where loss of precision actually could happen, e.g.:

```
>>> s1 = pd.Series([1666880195890293744, 1666880195890293837])

>>> s1
0    1666880195890293744
1    1666880195890293837
dtype: int64

>>> s1.astype(float).astype(int)
0    1666880195890293760
1    1666880195890293760
dtype: int64
```




updated, thanks @jessestone7 
Thanks again @lukemanley 
I just tried this on Pandas version 2.2.2 and I see that there is a loss of precision:

```
>>> a = pd.Series([1666880195890293744, 5]).to_frame()
>>> b = pd.Series([6, 7, 8]).to_frame()
>>> a
                     0
0  1666880195890293744
1                    5
>>> b
   0
0  6
1  7
2  8
>>> a.combine_first(b)
                     0
0  1666880195890293760
1                    5
2                    8
```