【问题标题】:Fit a function to a "bell-shape" curve将函数拟合到“钟形”曲线
【发布时间】:2023-08-21 01:12:02
【问题描述】:

我的数据如下所示:

蓝线代表去年的数据,绿点代表当前时间的数据。绿点恰好在蓝线上,但情况并非总是如此,它们可能会转移,但不会太多。意思是,斜率和曲率可能不同,y 轴值也可能相对于 x 轴值不同。 x 轴类似于一年中的某一天。我想将一条曲线拟合到蓝线,这将概括它的形状,但它也应该灵活地估计一条仅基于绿点的新蓝线。把它想象成一个实时进度——每隔几天我会得到一个新的绿点,我想根据新的一组绿点估计一条新的蓝线。换句话说,根据部分数据(一组绿点)改变蓝线系数。 y 轴值不会超过 1,也不会低于 0,x 轴值应该在 0 到 200 之间。我尝试过分段线性回归和 2 次多项式,但效​​果不佳。到目前为止,我想出的解决方案是拟合一个渐近为 1 的"S" curve shape,当 x 介于 0 和 75 之间时,然后拟合一个渐近为 0 的“反向”“S”曲线。并不总是很容易检测到“S”曲线拟合和“反向S曲线”拟合之间的这个转折点。 有没有更好的方法来概括蓝线?有没有一种功能可以做到这一点而不依赖于分割? 我用 Python 编写,所以我更喜欢面向 Python 的解决方案,但当然我也可以实现其他解决方案。

【问题讨论】:

  • 到目前为止你尝试过什么? *.com/help/minimal-reproducible-example
  • 文中写道:“我试过分段线性回归和二次多项式,但效​​果不佳。”另外,在文中,我尝试了“S”曲线形状。
  • 我推荐以下内容以提高您获得好答案的机会:(1)添加您编写的代码。 (2)对“做得不好”进行扩展和量化。 (3) 定义一个目标质量(=什么是“足够好”)。
  • 你可以试试四次多项式
  • 这会造成不必要的转折。

标签: python function logistic-regression curve-fitting prediction


【解决方案1】:

首先,您将选择一个适合您数据的函数。

“钟形”是Gaussian function的著名名称,您也可以查看Sinc function

那你就用from scipy.optimize importcurve_fit-修改例子-。

更新: 您可以使用Bézier curve。见:Bézier curve fitting with SciPy

【讨论】:

  • 这不完全是一个钟形曲线,它不是对称的,两边的斜率不同。我不知道怎么称呼它,所以我写了“钟形曲线”。
  • 您可以添加 if 语句来更改使用的函数,例如高斯函数:variance = 1 | x>100 否则方差 = 50 或三阶多边形。
  • @I.Omar 请看我对这个问题的回答,它使用洛伦兹型峰值方程。
【解决方案2】:

这是一个可能有用的示例图形拟合器。我从您的散点图中提取了数据,并对具有四个或更少参数的峰值方程进行了方程搜索 - 通过过滤掉 x > 175 的提取数据点,在散点图的右下角省略了明显的线性“尾部”。洛伦兹示例代码中的 -type peak 方程对我来说似乎是最好的候选方程。

此示例使用 scipy 差分进化遗传算法模块自动确定非线性求解器的初始参数估计,并且该模块使用拉丁超立方算法确保对参数空间进行彻底搜索,需要搜索范围。在此示例中,这些搜索范围取自(提取的)数据最大值和最小值,这不太可能适用于非常少的数据点(仅限绿点),因此您应该考虑对这些搜索范围进行硬编码。

import numpy, scipy, matplotlib
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from scipy.optimize import differential_evolution
import warnings

xData = numpy.array([1.7430e+02, 1.7220e+02, 1.6612e+02, 1.5981e+02, 1.5327e+02, 1.4603e+02, 1.3879e+02, 1.2944e+02, 1.2033e+02, 1.1238e+02, 1.0467e+02, 1.0047e+02, 8.8551e+01, 8.2944e+01, 7.2196e+01, 6.2150e+01, 5.5140e+01, 5.1402e+01, 4.5794e+01, 4.1822e+01, 3.8785e+01, 3.5981e+01, 3.1542e+01, 2.8738e+01, 2.3598e+01, 2.0794e+01])
yData = numpy.array([2.1474e-01, 2.5263e-01, 3.5789e-01, 5.0947e-01, 6.4421e-01, 7.5368e-01, 8.2526e-01, 8.7158e-01, 9.0526e-01, 9.3474e-01, 9.5158e-01, 9.6842e-01, 9.6421e-01, 9.6842e-01, 9.7263e-01, 9.4737e-01, 9.0526e-01, 8.4632e-01, 7.4526e-01, 6.6947e-01, 5.9789e-01, 5.2211e-01, 4.0000e-01, 3.2842e-01, 2.3158e-01, 1.8526e-01])


def func(x, a, b, c, offset):
    # Lorentzian E peak equation from zunzun.com "function finder"
    return 1.0 / (a + numpy.square((x-b)/c)) + offset


# function for genetic algorithm to minimize (sum of squared error)
def sumOfSquaredError(parameterTuple):
    warnings.filterwarnings("ignore") # do not print warnings by genetic algorithm
    val = func(xData, *parameterTuple)
    return numpy.sum((yData - val) ** 2.0)


def generate_Initial_Parameters():
    # min and max used for bounds
    minX = min(xData)
    minY = min(yData)
    maxX = max(xData)
    maxY = max(yData)

    parameterBounds = []
    parameterBounds.append([-maxY, 0.0]) # search bounds for a
    parameterBounds.append([minX, maxX]) # search bounds for b
    parameterBounds.append([minX, maxX]) # search bounds for c
    parameterBounds.append([minY, maxY]) # search bounds for offset


    result = differential_evolution(sumOfSquaredError, parameterBounds, seed=3)
    return result.x

# by default, differential_evolution completes by calling curve_fit() using parameter bounds
geneticParameters = generate_Initial_Parameters()

# call curve_fit without passing bounds from genetic algorithm
fittedParameters, pcov = curve_fit(func, xData, yData, geneticParameters)
print('Parameters:', fittedParameters)
print()

modelPredictions = func(xData, *fittedParameters) 

absError = modelPredictions - yData

SE = numpy.square(absError) # squared errors
MSE = numpy.mean(SE) # mean squared errors
RMSE = numpy.sqrt(MSE) # Root Mean Squared Error, RMSE
Rsquared = 1.0 - (numpy.var(absError) / numpy.var(yData))

print()
print('RMSE:', RMSE)
print('R-squared:', Rsquared)

print()


##########################################################
# graphics output section
def ModelAndScatterPlot(graphWidth, graphHeight):
    f = plt.figure(figsize=(graphWidth/100.0, graphHeight/100.0), dpi=100)
    axes = f.add_subplot(111)

    # first the raw data as a scatter plot
    axes.plot(xData, yData,  'D')

    # create data for the fitted equation plot
    xModel = numpy.linspace(min(xData), max(xData), 500)
    yModel = func(xModel, *fittedParameters)

    # now the model as a line plot
    axes.plot(xModel, yModel)

    axes.set_xlabel('X Data') # X axis data label
    axes.set_ylabel('Y Data') # Y axis data label

    plt.show()
    plt.close('all') # clean up after using pyplot

graphWidth = 800
graphHeight = 600
ModelAndScatterPlot(graphWidth, graphHeight)

【讨论】:

  • 这个函数是对称的吗?
  • 数据中没有“尾巴”——正如我所指出的——数据实际上是对称的。您不熟悉 Lorentzian 系列峰值方程,或者您不需要问这个问题。请检查我的答案中的图,它是为此提供的。
  • 我的意思是@user88484写道“这不完全是一个钟形曲线形状,它不是对称的,两边的斜率不同。”。