【问题标题】:How to make a piecewise linear fit in Python with some constant pieces?如何在 Python 中使用一些常数片段进行分段线性拟合?
【发布时间】:2022-01-18 23:59:35
【问题描述】:

我正在尝试制作由 3 个部分组成的分段线性拟合,其中第一个和最后一个部分是恒定的。正如你在这张图中看到的

没有得到预期的拟合,因为拟合没有从原始数据点清晰地捕捉到 3 个线性部分。

我已经尝试关注this question 并将其扩展为 3 件和两个常数件的​​情况,但我一定做错了什么。

这是我的代码:

from scipy import optimize
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
plt.rcParams['figure.figsize'] = [16, 6]

x = np.arange(0, 50, dtype=float)
y = np.array([50 for i in range(10)]
             + [50 - (50-5)/31 * i for i in range(1, 31)]
             + [5 for i in range(10)],
             dtype=float)

def piecewise_linear(x, x0, y0, x1, y1):
    return np.piecewise(x,
                        [x < x0, (x >= x0) & (x < x1), x >= x1],
                        [lambda x:y0, lambda x:(y1-y0)/(x1-x0)*(x-x0)+y0, lambda x:y1])

p , e = optimize.curve_fit(piecewise_linear, x, y)
xd = np.linspace(0, 50, 101)

plt.plot(x, y, "o", label='original data')
plt.plot(xd, piecewise_linear(xd, *p), label='piecewise linear fit')
plt.legend()

对前面提到的question 的公认答案建议查看segments_fit.ipynb 对于 N 部分的情况,但之后我似乎无法指定第一个和最后一个部分应该是恒定的。

此外,我确实收到以下警告:

OptimizeWarning: Covariance of the parameters could not be estimated

我做错了什么?

【问题讨论】:

  • 请提供您的情节中的“原始数据”。
  • “原始数据”是存储在我提供的代码中的变量 x 和 y 中的值。运行代码应该会产生情节。
  • 有噪音吗?如果是这样,可以在第 16 页上找到一个不错的解决方案 here
  • 是的,我真正想要拟合的数据中有噪音,但我在代码中提供的模拟数据中没有。也许在模拟数据中不包括噪声是一个糟糕的选择。我只是想在拟合真实数据之前让它工作。

标签: python numpy scipy curve-fitting piecewise


【解决方案1】:

您可以使用一阶单变量样条获得单线解决方案(不计算导入)。像这样

from scipy.interpolate import UnivariateSpline

f = UnivariateSpline(x,y,k=1,s=0)

这里k=1 表示我们使用一阶多项式进行插值,也就是线。 s 是平滑参数。它决定了您要在适合度上妥协多少,以避免使用过多的段。将其设置为零意味着没有妥协,即这条线必须抛出所有点。见the documentation.

然后

plt.plot(x, y, "o", label='original data')
plt.plot(x, f(x), label='linear interpolation')
plt.legend()
plt.savefig("out.png", dpi=300)

给予

【讨论】:

  • 这种方法的问题是,当数据中有噪声时。它只会在 N 点之间产生 N-1 条线,而不是我要寻找的 3 段。
  • @afd 是的,在这种情况下,您必须选择更大的s。然后就不会了。
【解决方案2】:

你可以直接复制segments_fit的实现

from scipy import optimize

def segments_fit(X, Y, count):
    xmin = X.min()
    xmax = X.max()

    seg = np.full(count - 1, (xmax - xmin) / count)

    px_init = np.r_[np.r_[xmin, seg].cumsum(), xmax]
    py_init = np.array([Y[np.abs(X - x) < (xmax - xmin) * 0.01].mean() for x in px_init])

    def func(p):
        seg = p[:count - 1]
        py = p[count - 1:]
        px = np.r_[np.r_[xmin, seg].cumsum(), xmax]
        return px, py

    def err(p):
        px, py = func(p)
        Y2 = np.interp(X, px, py)
        return np.mean((Y - Y2)**2)

    r = optimize.minimize(err, x0=np.r_[seg, py_init], method='Nelder-Mead')
    return func(r.x)

然后你按如下方式应用它

import numpy as np;

# mimic your data
x = np.linspace(0, 50)
y = 50 - np.clip(x, 10, 40)

# apply the segment fit
fx, fy = segments_fit(x, y, 3)

这会给你(fx,fy)你分段拟合的角,让我们绘制它

import matplotlib.pyplot as plt

# show the results
plt.figure(figsize=(8, 3))
plt.plot(fx, fy, 'o-')
plt.plot(x, y, '.')
plt.legend(['fitted line', 'given points'])

编辑:引入常量段

如 cmets 中所述,上述示例不保证输出在结束段中保持不变。

基于这个实现,我能想到的更简单的方法是限制func(p) 这样做,确保段不变的简单方法是设置y[i+1]==y[i]。因此我添加了xanchoryanchor。如果你给出一个重复数字的数组,你可以将多个点绑定到同一个值。

from scipy import optimize

def segments_fit(X, Y, count, xanchors=slice(None), yanchors=slice(None)):
    xmin = X.min()
    xmax = X.max()
    seg = np.full(count - 1, (xmax - xmin) / count)

    px_init = np.r_[np.r_[xmin, seg].cumsum(), xmax]
    py_init = np.array([Y[np.abs(X - x) < (xmax - xmin) * 0.01].mean() for x in px_init])

    def func(p):
        seg = p[:count - 1]
        py = p[count - 1:]
        px = np.r_[np.r_[xmin, seg].cumsum(), xmax]
        py = py[yanchors]
        px = px[xanchors]
        return px, py

    def err(p):
        px, py = func(p)
        Y2 = np.interp(X, px, py)
        return np.mean((Y - Y2)**2)

    r = optimize.minimize(err, x0=np.r_[seg, py_init], method='Nelder-Mead')
    return func(r.x)

我对数据生成进行了一些修改,以使更改的效果更加清晰

import matplotlib.pyplot as plt
import numpy as np;

# mimic your data
x = np.linspace(0, 50)
y = 50 - np.clip(x, 10, 40) + np.random.randn(len(x)) + 0.25 * x
# apply the segment fit
fx, fy = segments_fit(x, y, 3)
plt.plot(fx, fy, 'o-')
plt.plot(x, y, '.k')
# apply the segment fit with some consecutive points having the 
# same anchor
fx, fy = segments_fit(x, y, 3, yanchors=[1,1,2,2])
plt.plot(fx, fy, 'o--r')
plt.legend(['fitted line', 'given points', 'with const segments'])

【讨论】:

  • 不幸的是,当数据中有噪声时,这种方法不会强制第一个和最后一个段保持不变。
  • 现在您可以将点值绑定在一起
  • 完美!这就像预期的那样工作。
【解决方案3】:

我认为这是一种很有趣的非线性方法,效果很好。 请注意,即使这是高度非线性的,它也非常接近线性行为。此外,拟合参数提供线性结果。仅对于偏移量b 需要进行一些转换并进行相应的错误传播。 (另外,p的值我不关心,只要比5大一点就行)

import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import curve_fit
np.set_printoptions( linewidth=250, precision=4)
np.set_printoptions( linewidth=250, precision=4)

### piecewise linear function for data generation
def pwl( x, m, b, a1, a2 ):
    if x < a1:
        out = pwl( a1, m, b, a1, a2 )
    elif x > a2:
        out = pwl( a2, m, b, a1, a2 )
    else:
        out = m * x + b
    return out

### non-linear approximation
def func( x, m, b, a1, a2, p ):
    out = b + np.log(
    1 / ( 1 + np.exp( -m *( x - a1 ) )**p )
    ) / p - np.log(
    1 / ( 1 + np.exp( -m * ( x - a2 ) )**p )
    ) / p
    return out

### some data
nn = 36
xdata = np.linspace( -5, 19, nn )
ydata = np.fromiter( (pwl( x, -2.1, 11.6, -1.1, 12.7 ) for x in xdata ), float)
ydata += np.random.normal( size=nn, scale=0.2)
### dense grid for printing
xth = np.linspace( -5, 19, 150 )
###fitting
popt, cov = curve_fit( func, xdata, ydata, p0=[-2, 11, -1, 10, 1])
mF, betaF, a1F, a2F, pF = popt
bF = betaF - mF * a1F
sol=( mF, bF, a1F, a2F, pF  )
### transforming the covariance due to the b' -> b mapping
J1 = np.identity(5)
J1[1,0] = -popt[2]
J1[1,2] = -popt[0]
cov2 = np.dot( J1, np.dot( cov, np.transpose( J1 ) ) )
### results
print( cov2 )
for i, v in enumerate( ("m", "b", "a1", "a2", "p" ) ):
    print( "{:>2} = {:+2.4e} ± {:0.4e}".format( v, sol[i], np.sqrt( cov2[i,i] ) ) )

### plotting
fig = plt.figure()
ax = fig.add_subplot( 1, 1, 1 )
ax.plot( xdata, ydata, ls='', marker='+' )
ax.plot( xth, func( xth, -2, 11, -1, 10, 1 ) )
ax.plot( xth, func( xth, *popt ) )
plt.show()

提供

[[ 1.3553e-04 -7.6291e-04 -4.3488e-04  4.5624e-04  1.2619e-01]
 [-7.6291e-04  6.4126e-03  3.4560e-03 -1.5573e-03 -7.4983e-01]
 [-4.3488e-04  3.4560e-03  3.4741e-03 -9.8284e-04 -4.2344e-01]
 [ 4.5624e-04 -1.5573e-03 -9.8284e-04  3.0842e-03 -5.2739e+00]
 [ 1.2619e-01 -7.4983e-01 -4.2344e-01 -5.2739e+00  3.1583e+05]]

 m = -2.0810e+00 ± 9.7718e-03
 b = +1.1463e+01 ± 6.7217e-02
a1 = -1.2545e+00 ± 5.0384e-02
a2 = +1.2739e+01 ± 4.7176e-02
 p = +1.6840e+01 ± 2.9872e+02

【讨论】:

  • 我必须说我不确定这种方法发生了什么。但由于它是一种非线性方法,它会产生圆形中断而不是突然中断。我真正想要的是找到与曲线断裂的两个点的坐标相对应的 x0、y0、x1 和 y1 的值。但如果是圆角休息,这似乎有点不精确。
  • @afd 这个想法是通过log( exp( ) ) 来回切换到线性,但添加一个微小的扰动以允许从一个线性行为转换到下一个。随着p 的增加,拐角变得任意尖锐,但特别是对于嘈杂的数据,扭结的位置仅在dx 之内。所以我不认为软部分(小于dx)是个问题。或者,您可以使用给定的p 进行迭代拟合,每一步都增加它。如果您去除噪声,您将获得具有 5 位精度的真正线性参数。
  • @afd 因此,您得到线性结果,但由于函数是连续可微的,拐角的位置不会卡在数据点之间。如果需要,可以仅使用这种拟合的结果将数据拆分为分段,但如前所述,我认为这没有必要。
猜你喜欢
  • 2015-06-05
  • 2020-07-27
  • 2014-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多