SciPy curve_fitでPandas Seriesのfloat32型は動作しないことへの注意と対策

## 問題の概要
[`scipy.optimize.curve_fit()`](https://docs.scipy.org/doc/scipy-1.14.0/reference/generated/scipy.optimize.curve_fit.html)関数を使用してフィッティングを行う際、`float32`型の`pandas.Series`を使用すると予期しない結果が得られることがあります。この問題は、SciPyの内部処理に起因しています。

本記事では、その再現コードを示すとともに、解決方法について説明します。忙しい方は、この記事の[解決策](#解決策)をご覧ください。

## 再現コード
本記事で使用したモジュールのバージョンは以下の通りです。
- Python: 3.12.4
- Pandas: 2.2.2
- Numpy: 1.26.4
- matplotlib: 3.8.4
- scipy: 1.13.1

NumpyとPandasで、`float32`型と`float64`型で挙動を比較します。

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

# サンプルデータの作成
np.random.seed(0)
x_data = np.linspace(0, 10, 100).astype('float32')
y_data = (3 * x_data + np.random.normal(size=x_data.size)).astype('float32')

# pandasデータフレームに変換
df = pd.DataFrame({'x': x_data, 'y': y_data})

# フィッティング関数の定義
def linear_func(x, a, b):
    return a * x + b

# フィッティングの実行（float32）
numpy_params_float32, _ = curve_fit(linear_func, x_data, y_data)
pandas_params_float32, _ = curve_fit(linear_func, df['x'], df['y'])

# データ型の変換（float64）
df['x'] = df['x'].astype('float64')
df['y'] = df['y'].astype('float64')

# フィッティングの実行（float64）
numpy_params_float64, _ = curve_fit(linear_func, x_data.astype('float64'), y_data.astype('float64'))
pandas_params_float64, _ = curve_fit(linear_func, df['x'], df['y'])

# 結果の表示
print(f"Float32 Numpy parameters: a = {numpy_params_float32[0]}, b = {numpy_params_float32[1]}")
print(f"Float32 Pandas parameters: a = {pandas_params_float32[0]}, b = {pandas_params_float32[1]}")
print(f"Float64 Numpy parameters: a = {numpy_params_float64[0]}, b = {numpy_params_float64[1]}")
print(f"Float64 Pandas parameters: a = {pandas_params_float64[0]}, b = {pandas_params_float64[1]}")

# 結果のプロット
plt.figure(figsize=(12, 6))

# Numpy float32フィットのプロット
plt.subplot(2, 2, 1)
plt.scatter(x_data, y_data, label='Data', color='blue')
plt.plot(x_data, linear_func(x_data, *numpy_params_float32), color='red', label='Fitted line')
plt.title('Numpy Fit (float32)')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()

# Pandas float32フィットのプロット
plt.subplot(2, 2, 2)
plt.scatter(df['x'].astype('float32'), df['y'].astype('float32'), label='Data', color='blue')
plt.plot(df['x'].astype('float32'), linear_func(df['x'].astype('float32'), *pandas_params_float32), color='red', label='Fitted line')
plt.title('Pandas Fit (float32)')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()

# Numpy float64フィットのプロット
plt.subplot(2, 2, 3)
plt.scatter(x_data.astype('float64'), y_data.astype('float64'), label='Data', color='blue')
plt.plot(x_data.astype('float64'), linear_func(x_data.astype('float64'), *numpy_params_float64), color='red', label='Fitted line')
plt.title('Numpy Fit (float64)')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()

# Pandas float64フィットのプロット
plt.subplot(2, 2, 4)
plt.scatter(df['x'], df['y'], label='Data', color='blue')
plt.plot(df['x'], linear_func(df['x'], *pandas_params_float64), color='red', label='Fitted line')
plt.title('Pandas Fit (float64)')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()

plt.tight_layout()
plt.show()
```
出力結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/2de478f9-53e0-2e47-3f3e-00af28a41bb4.png)

この結果に示すように、Pandasの`float32`のデータでフィットした右上の結果で、フィッティングがうまくできていないことがわかります。

## 原因
この問題の原因は、`curve_fit()`関数の内部でnumpyはx、y軸の両方のデータを`float64`に変換して計算されますが、`pandas.Series`ではy軸のデータのみ`float64`に変換されることから生じていると考えられます。その結果、`curve_fit()`内の最適化アルゴリズム（デフォルトは[`least_squares()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html)）で、うまく計算できず誤ったフィット結果が出ていると思われます。

### 原因の詳細
`pandas.Series`ではy軸のデータのみ`float64`に変換されることについて[ソースコード](https://github.com/scipy/scipy/blob/v1.13.1/scipy/optimize/_minpack_py.py#L929)の該当箇所を示します。

まずは、x軸方向のデータについて見てみます。

```python
    if isinstance(xdata, (list, tuple, np.ndarray)):
        # `xdata` is passed straight to the user-defined `f`, so allow
        # non-array_like `xdata`.
        if check_finite:
            xdata = np.asarray_chkfinite(xdata, float)
        else:
            xdata = np.asarray(xdata, float)
```
Pythonの組み込み関数[`isinstance()`](https://docs.python.org/ja/3/library/functions.html#isinstance)で、型の種類を判定していますが、Pandasについての判定は含まれていないため、内部で`float64`型への変換が行われません。

次に、y軸方向のデータについて確認します。

```python
    if check_finite:
        ydata = np.asarray_chkfinite(ydata, float)
    else:
        ydata = np.asarray(ydata, float)
```

y方向のデータはいずれの分岐でも`float64`へ変換されることがわかります。（`check_finite`は、`curve_fit()`の引数です。）

このことからx、y軸のデータに`pandas.Series`の`float32`を使用した場合、xデータは`float32`のままで、yデータが`float64`に変換されることになります。つまり、**x、yで型が異なるため、何かしら最小化関数などの計算がうまくできず発生している**と考えられます。

## 解決策
ここまで読んだ読者は、`curve_fit()`に`pandas.Series`を使用する際に`float64`型を使うという解決策を挙げるかもしれません。ただし、SciPyはNumpyでの実装をベースに作られており、開発者の努力によりPandasも使用できるモジュールは存在しますが、Pandasの使用を極力避けるのが無難だと思います。


PandasからNumpyのデータに変換するには[`.to_numpy()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_numpy.html)を使います。例えば、先ほどのコードを例にすると以下のように書きます。
```python
curve_fit(linear_func, df['x'].to_numpy(dtype=float), df['y'].to_numpy(dtype=float))
```


## 補足
このPandasの問題はGithub issueで報告されています。デベロッパは認知していて、**あくまでバグではなく仕様です**。

https://github.com/scipy/scipy/issues/16162

この記事もそのissueを参考に作成しています。以下に、デベロッパの視点や考えを簡単にまとめます。

（[DeepL翻訳](https://www.deepl.com/ja/translator)で訳しました。）

>Github issueより
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
<br>翻訳：そう、array_likeやその他のNumPy以外の配列を扱うことは、多くの問題を引き起こすため、良い設計ではなかったという結論に、私たちは徐々に収束しつつある。もし配列が期待されているのであれば、if elseのケースを避けるために配列で作業する方がよいでしょう。

Numpyを想定して作るとPandasに対応できない可能性があります。ただ、Pandasを考慮するには、デバッグが煩雑になります。個人的に、開発側ではNumpyをベースに作り、Numpyの配列を渡す責任をユーザー側に託すという考え方で良いと思います。

そして、[SciPy User Guide](https://docs.scipy.org/doc/scipy/tutorial/index.html#user-guide)もそのことについて少し書かれています。

>SciPy User Guideより
SciPy is a collection of mathematical algorithms and convenience functions built on NumPy . It adds significant power to Python by providing the user with high-level commands and classes for manipulating and visualizing data.
<br>翻訳：SciPyは、NumPyをベースに構築された数学的アルゴリズムと便利な関数のコレクションです。データを操作したり視覚化したりするための高レベルのコマンドやクラスをユーザーに提供することで、Pythonに大きな力を与えている。

Numpyをベースに構築されたモジュールであることが明記されています。この記事のように、PandasをSciPyに渡すのは、そのソースコードを完全に熟知していない限りは危険な行為だと思います。ユーザガイドを読み込み、開発者に感謝し、Numpyを渡すことの重要性に気付かされました。

## まとめ
`scipy.optimize.curve_fit()`関数を使用する際に、Pandasの`float32`型データをそのまま使用すると予期しない結果が得られることがあります。これは、SciPyの内部処理でx軸方向のデータが`float32`のまま処理されることにより発生していると考えられます。この問題を避けるために、`curve_fit()`に渡す前にNumpyのデータに変換しておくことが重要です。

## 参考文献
https://github.com/scipy/scipy/issues/16162