2
2

問題の概要

scipy.optimize.curve_fit()関数を使用してフィッティングを行う際、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型で挙動を比較します。

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

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

原因

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

原因の詳細

pandas.Seriesではy軸のデータのみfloat64に変換されることについてソースコードの該当箇所を示します。

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

    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()で、型の種類を判定していますが、Pandasについての判定は含まれていないため、内部でfloat64型への変換が行われません。

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

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

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

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

解決策

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

PandasからNumpyのデータに変換するには.to_numpy()を使います。例えば、先ほどのコードを例にすると以下のように書きます。

curve_fit(linear_func, df['x'].to_numpy(dtype=float), df['y'].to_numpy(dtype=float))

補足

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

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

DeepL翻訳で訳しました。)

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

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

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

そして、SciPy 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.

翻訳:SciPyは、NumPyをベースに構築された数学的アルゴリズムと便利な関数のコレクションです。データを操作したり視覚化したりするための高レベルのコマンドやクラスをユーザーに提供することで、Pythonに大きな力を与えている。

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

まとめ

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

参考文献

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2