Python 严格在原始曲线上方平滑曲线的算法
我有一个任意的输入曲线,以numpy数组的形式给出。我想创建一个平滑的版本,类似于滚动平均值,但它严格大于原始值,并且严格平滑。我可以使用滚动平均值,但如果输入曲线有一个负峰值,平滑后的版本将下降到该峰值附近的原始值以下。然后,我可以简单地使用这个和原始的最大值,但这会在过渡发生的地方引入非光滑点 此外,我希望能够通过向前看和向后看来对算法进行参数化,这样,如果向前看和向后看较大,则生成的曲线更倾向于保持下降边缘,向前看较大和较小,则更倾向于接近上升边缘 我尝试使用Python 严格在原始曲线上方平滑曲线的算法,python,algorithm,pandas,numpy,smoothing,Python,Algorithm,Pandas,Numpy,Smoothing,我有一个任意的输入曲线,以numpy数组的形式给出。我想创建一个平滑的版本,类似于滚动平均值,但它严格大于原始值,并且严格平滑。我可以使用滚动平均值,但如果输入曲线有一个负峰值,平滑后的版本将下降到该峰值附近的原始值以下。然后,我可以简单地使用这个和原始的最大值,但这会在过渡发生的地方引入非光滑点 此外,我希望能够通过向前看和向后看来对算法进行参数化,这样,如果向前看和向后看较大,则生成的曲线更倾向于保持下降边缘,向前看较大和较小,则更倾向于接近上升边缘 我尝试使用pandas.Series(a
pandas.Series(a).rolling()
工具获得滚动平均值、滚动最大值等,但到目前为止,我还没有找到方法生成平滑版本的输入,在所有情况下都保持在上面的输入
我想有一种方法可以将滚动最大值和滚动方式结合起来,以某种方式实现我想要的,所以这里有一些代码用于计算这些:
import pandas as pd
import numpy as np
我的输入曲线:
original = np.array([ 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 3, 3, 7 ])
这可以用边缘值填充左(前)和右(后),作为任何滚动功能的准备:
pre = 2
post = 3
padded = np.pad(original, (pre, post), 'edge')
现在我们可以应用滚动平均值:
smoothed = pd.Series(padded).rolling(
pre + post + 1).mean().get_values()[pre+post:]
但是现在平滑的版本低于原始版本,即。G在索引4
:
print(original[4], smoothed[4]) # 8 and 5.5
要计算滚动最大值,可以使用以下公式:
maximum = pd.Series(padded).rolling(
pre + post + 1).max().get_values()[pre+post:]
但是,在许多情况下,仅滚动最大值当然不是平滑的,并且会在原始顶点周围显示许多平顶。我更喜欢平稳地接近这些峰值
如果还安装了pyqtgraph,则可以轻松绘制此类曲线:
import pyqtgraph as pg
p = pg.plot(original)
p.plotItem.plot(smoothed, pen=(255,0,0))
(当然,其他打印库也可以。)
我想要的结果是一条曲线,它是e。G与这些值形成的值类似:
goal = np.array([ 5, 7, 7.8, 8, 8, 8, 7, 5, 3.5, 3, 4, 5.5, 6.5, 7 ])
这是曲线的图像。白线是原始的(输入),红色是滚动的意思,绿色是我想要的:
编辑:我刚刚找到名为peakutils
的模块的函数baseline()
和envelope()
。这两个函数可以计算一个给定阶的多项式来拟合较低的阶。输入的上峰值。对于小样本,这是一个很好的解决方案。我正在寻找一些也可以应用于具有数百万值的非常大的样本的东西;然后,学位需要非常高,计算也需要相当长的时间。分段操作(一节接一节)会带来一系列新的问题和问题(比如如何在保持平滑的同时保证输入,处理大量工件时的性能等),因此如果可能的话,我希望避免这种情况
编辑2:我有一个很有希望的方法,重复应用一个过滤器,它创建一个滚动平均值,稍微向左和向右移动,然后取这两个和原始样本中的最大值。在应用了几次之后,它以我想要的方式平滑了曲线。不过,一些不光滑的地方仍可能存在于深谷中。以下是此操作的代码:
pre = 30
post = 30
margin = 10
s = [ np.array(sum([[ x ] * 100 for x in
[ 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 3, 3, 7 ]], [])) ]
for _ in range(30):
s.append(np.max([
pd.Series(np.pad(s[-1], (margin+pre, post), 'edge')).rolling(
1 + pre + post).mean().get_values()[pre+post:-margin],
pd.Series(np.pad(s[-1], (pre, post+margin), 'edge')).rolling(
1 + pre + post).mean().get_values()[pre+post+margin:],
s[-1]], 0))
这将创建30次应用过滤器的迭代,可以使用pyqtplot进行打印,以便:
p = pg.plot(original)
for q in s:
p.plotItem.plot(q, pen=(255, 100, 100))
生成的图像如下所示:
这种方法有两个方面我不喜欢:① 它需要大量的迭代时间(这会减慢我的速度),② 山谷中仍然有不光滑的部分(尽管在我的使用案例中,这可能是可以接受的)。正如我在注释中指出的,在八高高原前后,绿线的行为是不一致的。如果左区域行为是您想要的,您可以执行以下操作:
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
from scipy.spatial import ConvexHull
# %matplotlib inline # for interactive notebooks
y=np.array([ 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 3, 3, 7])
x=np.array(range(len(y)))
#######
# This essentially selects the vertices that you'd touch streatching a
# rubber band over the top of the function
vs = ConvexHull(np.asarray([x,y]).transpose()).vertices
indices_of_upper_hull_verts = list(reversed(np.concatenate([vs[np.where(vs == len(x)-1)[0][0]: ],vs[0:1]])))
newX = x[indices_of_upper_hull_verts]
newY = y[indices_of_upper_hull_verts]
#########
x_smooth = np.linspace(newX.min(), newX.max(),500)
f = interp1d(newX, newY, kind='quadratic')
y_smooth=f(x_smooth)
plt.plot (x,y)
plt.plot (x_smooth,y_smooth)
plt.scatter (x, y)
这将产生:
更新:
这里有一个更适合你的选择。如果使用以1为中心的简单卷积代替滚动平均值,则生成的曲线将始终大于输入。卷积内核的翅膀可以调整为向前看/向后看
像这样:
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
from scipy.ndimage.filters import convolve
## For interactive notebooks
#%matplotlib inline
y=np.array([ 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 3, 3, 7]).astype(np.float)
preLength = 1
postLength = 1
preWeight = 0.2
postWeight = 0.2
kernal = [preWeight/preLength for i in range(preLength)] + [1] + [postWeight/postLength for i in range(postLength)]
output = convolve(y,kernal)
x=np.array(range(len(y)))
plt.plot (x,y)
plt.plot (x,output)
plt.scatter (x, y)
一个缺点是,由于集成内核通常大于1(这确保了输出曲线平滑且不低于输入),因此输出曲线总是大于输入曲线,例如,在绘制时位于大峰值的顶部,而不是正好位于顶部。作为问题的一部分,我已经生成了一个函数,它通过最小化积分,使多项式严格高于点,从而使多项式与数据相匹配。我怀疑如果你对你的数据进行分段处理,它可能对你有用
import scipy.optimize
def upperpoly(xdata, ydata, order):
def objective(p):
"""Minimize integral"""
pint = np.polyint(p)
integral = np.polyval(pint, xdata[-1]) - np.polyval(pint, xdata[0])
return integral
def constraints(p):
"""Polynomial values be > data at every point"""
return np.polyval(p, xdata) - ydata
p0 = np.polyfit(xdata, ydata, order)
y0 = np.polyval(p0, xdata)
shift = (ydata - y0).max()
p0[-1] += shift
result = scipy.optimize.minimize(objective, p0,
constraints={'type':'ineq',
'fun': constraints})
return result.x
我现在已经玩了很多,我想我找到了两个主要的答案,解决了我的直接需求。我将在下面给出它们
import numpy as np
import pandas as pd
from scipy import signal
import pyqtgraph as pg
这些只是必要的导入,用于所有代码清理<当然,code>pyqtgraph仅用于显示内容,因此您并不真正需要它
对称平滑
这可以用来创建一条始终位于信号上方的平滑线,但它无法区分上升沿和下降沿,因此单峰周围的曲线看起来是对称的。在许多情况下,这可能是很好的,因为它比下面的不对称解决方案要简单得多(而且我也不知道有什么怪癖)
- 创建滚动最大值(x,青色),然后
- 创建一个窗口滚动平均值(z,白色虚线)
s = np.repeat([5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 3, 3, 7], 400) + 0.1
s *= np.random.random(len(s))
pre = post = 400
x = pd.Series(np.pad(s, (pre, post), 'edge')).rolling(
pre + 1 + post).max().get_values()[pre+post:]
y = pd.Series(np.pad(x, (pre, post), 'edge')).rolling(
pre + 1 + post, win_type='blackman').mean().get_values()[pre+post:]
p = pg.plot(s, pen=(100,100,100))
for c, pen in ((x, (0, 200, 200)),
(y, pg.mkPen((255, 255, 255), width=3, style=3))):
p.plotItem.plot(c, pen=pen)
s = np.repeat([5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 3, 3, 7], 400) + 0.1
s *= np.random.random(len(s))
pre, post = 100, 1000
t = pd.Series(np.pad(s, (post, pre), 'edge')).rolling(
pre + 1 + post).max().get_values()[pre+post:]
g = signal.get_window('boxcar', pre*2)[pre:]
g /= g.sum()
u = np.convolve(np.pad(t, (pre, 0), 'edge'), g)[pre:]
g = signal.get_window('boxcar', post*2)[:post]
g /= g.sum()
v = np.convolve(np.pad(t, (0, post), 'edge'), g)[post:]
u, v = u[:len(v)], v[:len(u)]
w = np.min(np.array([ u, v ]),0)
pre = post = max(100, min(pre, post)*3)
x = pd.Series(np.pad(w, (pre, post), 'edge')).rolling(
pre + 1 + post).max().get_values()[pre+post:]
y = pd.Series(np.pad(x, (pre, post), 'edge')).rolling(
pre + 1 + post, win_type='blackman').mean().get_values()[pre+post:]
p = pg.plot(s, pen=(100,100,100))
for c, pen in ((t, (200, 0, 0)),
(u, (200, 200, 0)),
(v, (0, 200, 0)),
(w, (200, 0, 200)),
(x, (0, 200, 200)),
(y, pg.mkPen((255, 255, 255), width=3))):
p.plotItem.plot(c, pen=pen)