Repository: scipy/scipy

Issue:
BUG: curve_fit gives wrong results with Pandas float32
### Describe your issue.

This issue is related to previous issues with curve_fit and float32, which were partially resolved in PR #10076. Incorrect fitting still happens when passing data as a float32 Pandas Series. Here is an example adapted from #2280.

### Reproducing Code Example

```python
import numpy as np
import pandas as pd
import scipy.optimize as opt

def func(x, a, b): return a*x + b
x = np.array([0,1])
y = np.array([0,0])
p0 = [0.1, 0.1]
p1 = opt.curve_fit(func,            x , y, p0=p0)
p2 = opt.curve_fit(func, pd.Series(x).astype('float64'), y, p0=p0)
p3 = opt.curve_fit(func, pd.Series(x).astype('float32'), y, p0=p0)
print('\n p1 = ',p1,'\n p2 = ',p2,'\n p3 = ',p3)
```


### Error message

```shell
p1 =  (array([0., 0.]), array([[inf, inf],
       [inf, inf]])) 
 p2 =  (array([0., 0.]), array([[inf, inf],
       [inf, inf]])) 
 p3 =  (array([0.1, 0.1]), array([[inf, inf],
       [inf, inf]]))
```


### SciPy/NumPy/Python version information

1.8.0 1.22.3 sys.version_info(major=3, minor=8, micro=10, releaselevel='final', serial=0)


Discussion:
Hi @d-cogswell, thank you for reporting. Indeed I can confirm this is happening when using Pandas Series, but not with NumPy arrays. This is because the internal casting to `float` is only happening for array_like. See:

```
if isinstance(xdata, (list, tuple, np.ndarray)):
    ...
```

I am unsure what we could do here since we don't want to have Pandas specific code here. We should check why we don't try to use `np.asarray` all the time.

cc @ev-br since you did gh-10076
We don't use `np.asarray` for `x` because of the last part of this docstring:

> xdata : array_like or object. The independent variable where the data is measured. 
> Should usually be an M-length sequence or an (k,M)-shaped array for functions with k predictors, but can *actually be any object*.

TLDR: this issue is down to calculations not being done in double precision, which is up to the user to ensure.

- xdata is not converted to a np.float64 array because it's not a list, tuple or np.ndarray. We can't cast everything to np.ndarray because the user might want to supply a specific kind of object that they don't want to cast.
- `func` is being supplied an x with a pd.Series of dtype=float32. Thus the output of `a*x + b` is calculated in single precision.
- `leastsq` is probably changing a` and `b` with a step size designed for np.float64 operation, so there will be a large round off error resulting from that single precision calculation. (i.e. the jacobian can't be calculated properly).

This function works:
```
def func(x, a, b):
    x = np.asfarray(x, np.float64)
    return a*x + b
```
This function doesn't work:
```
def func(x, a, b):
    return np.asfarray(a*x + b, np.float64)
```

I don't think this is a SciPy bug (in as much as I don't think we should change the current behaviour), but it's probably worth updating the documentation. e.g. add something to the Notes along the lines of:

```
Notes
------
The user must ensure that all calculations are done in double precision, as the optimization process may fail on some inputs. Checks could include an `x` 'array_like' having a dtype of np.float64, making sure intermediate calculations in `func` are carried out in double precision, and that the `output` of `func` is in double precision.
```

I think it is not a very laborious task to squeeze in `.to_numpy()` explicitly for example

```python
p3 = opt.curve_fit(func, pd.Series(x).to_numpy(dtype=np.float32), y, p0=p0)
```

is returning consistent results.
Sure. I think we're on the same page @ilayn, thinking that's the responsibility of user code?
Yes, we are slowly converging to the conclusion that handling array_like and other non NumPy arrays were not good design as it lead to many issues. If array is expected better to work with arrays to avoid lots of if else cases 
Agreed 👍 thank you both for jumping in.

@d-cogswell Would you like to make a PR to maybe improve the documentation as suggested by Warren?
I agree, updating the documentation would be helpful. What do you think about throwing a warning if the inputs are not `(list, tuple, np.ndarray)`? I know the fix here is easy for the user, but realizing there's a problem is not. curve_fit() runs but returns a poor fit, and there's no reason to suspect the precision of the input.
Personally, I would not use a warning. Some test suites are failing on warnings and it introduces some churn then for maintainers to filter out warnings if they would use something else. I think updating the doc is enough (we can make this clearer by having a note on the parameters' descriptions too)

Resolving patch:
diff --git a/scipy/optimize/_minpack_py.py b/scipy/optimize/_minpack_py.py
--- a/scipy/optimize/_minpack_py.py
+++ b/scipy/optimize/_minpack_py.py
@@ -699,6 +699,9 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
 
     Notes
     -----
+    Users should ensure that inputs `xdata`, `ydata`, and the output of `f`
+    are ``float64``, or else the optimization may return incorrect results.
+    
     With ``method='lm'``, the algorithm uses the Levenberg-Marquardt algorithm
     through `leastsq`. Note that this algorithm can only deal with
     unconstrained problems.


Test patch: