私はPython=で一般化された方法を考え出して、一連の計画された宇宙船の操縦中に発生するピッチ回転を特定しようとしています。それを shiftの特定のケースと考えることができます検出 問題。
一連の測定値のsolar_elevation_angle
変数を検討して、宇宙船の計器から測定された太陽の仰角を特定します。データをいじりたい人のために、solar_elevation_angle.txt
ファイル here を保存しました。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import gridspec
from scipy.signal import argrelmax
from scipy.ndimage.filters import gaussian_filter1d
solar_elevation_angle = np.loadtxt("solar_elevation_angle.txt", dtype=np.float32)
fig, ax = plt.subplots()
ax.set_title('Solar elevation angle')
ax.set_xlabel('Scanline')
ax.set_ylabel('Solar elevation angle [deg]')
ax.plot(solar_elevation_angle)
plt.show()
スキャンラインは私の時間の次元です。勾配が変化する4つのポイントは、宇宙船のピッチ回転を示します。
ご覧のとおり、宇宙船の操縦領域外での太陽仰角の変化は、時間の関数としてかなり線形であり、これはこの特定の宇宙船の場合は常に当てはまります(主要な障害を除く)。
各宇宙船の操作中、傾斜の変化は明らかに連続的ですが、私の角度値のセットでは離散化されています。つまり、操作ごとに、操作が行われた単一のスキャンラインを見つけようとしても意味がありません。私の目標は、操作ごとに、操作が発生した時間間隔を定義するスキャンラインの範囲内の「代表的な」スキャンラインを特定することです(たとえば、中央の値、または左の境界)。
すべての操作が行われた「代表的な」スキャンラインインデックスのセットを取得したら、それらのインデックスを使用して操作時間の大まかな見積もりを行ったり、プロットにラベルを自動的に配置したりできます。
これまでの私の解決策は:
np.gradient
を使用して、太陽仰角の2次導関数を計算します。これが私のコードです:
fig = plt.figure(figsize=(8,12))
gs = gridspec.GridSpec(5, 1)
ax0 = plt.subplot(gs[0])
ax0.set_title('Solar elevation angle')
ax0.plot(solar_elevation_angle)
solar_elevation_angle_1stdev = np.gradient(solar_elevation_angle)
ax1 = plt.subplot(gs[1])
ax1.set_title('1st derivative')
ax1.plot(solar_elevation_angle_1stdev)
solar_elevation_angle_2nddev = np.gradient(solar_elevation_angle_1stdev)
ax2 = plt.subplot(gs[2])
ax2.set_title('2nd derivative')
ax2.plot(solar_elevation_angle_2nddev)
solar_elevation_angle_2nddev_clipped = np.clip(np.abs(np.gradient(solar_elevation_angle_2nddev)), 0.0001, 2)
ax3 = plt.subplot(gs[3])
ax3.set_title('absolute value + clipping')
ax3.plot(solar_elevation_angle_2nddev_clipped)
smoothed_signal = gaussian_filter1d(solar_elevation_angle_2nddev_clipped, 20)
ax4 = plt.subplot(gs[4])
ax4.set_title('Smoothing applied')
ax4.plot(smoothed_signal)
plt.tight_layout()
plt.show()
その後、scipyのargrelmax
関数を使用して、極大値を簡単に識別できます。
max_idx = argrelmax(smoothed_signal)[0]
print(max_idx)
# [ 689 1019 2356 2685]
私が探していたスキャンラインインデックスを正しく識別する:
fig, ax = plt.subplots()
ax.set_title('Solar elevation angle')
ax.set_xlabel('Scanline')
ax.set_ylabel('Solar elevation angle [deg]')
ax.plot(solar_elevation_angle)
ax.scatter(max_idx, solar_elevation_angle[max_idx], marker='x', color='red')
plt.show()
私の質問は:この問題に取り組むより良い方法はありますか?
ノイズを取り除くためにクリッピングしきい値を手動で指定する必要があるため、ガウシアンフィルターのシグマがこのアプローチを大幅に弱め、他の同様のケースに適用できないことがわかりました。
最初の改善は、 Savitzky-Golayフィルター を使用して、ノイズの少ない方法で導関数を見つけることです。たとえば、特定のサイズの各データスライスに放物線(最小二乗の意味で)を適合させ、その放物線の2次導関数をとることができます。結果は、gradient
で2階の差を取るだけの場合よりもはるかに優れています。ここでは、ウィンドウサイズ101を使用しています。
savgol_filter(solar_elevation_angle, window_length=window, polyorder=2, deriv=2)
次に、argrelmax
を使用して最大値のポイントを探す代わりに、2次導関数が大きい場所を探すことをお勧めします。たとえば、最大サイズの少なくとも半分。これはもちろん多くのインデックスを返しますが、それらのインデックス間のギャップを調べて、各ピークの始まりと終わりを特定できます。ピークの中点が簡単に見つかります。
これが完全なコードです。唯一のパラメーターはウィンドウサイズで、101に設定されています。このアプローチは堅牢です。サイズ21または201は、基本的に同じ結果になります(奇数でなければなりません)。
from scipy.signal import savgol_filter
window = 101
der2 = savgol_filter(solar_elevation_angle, window_length=window, polyorder=2, deriv=2)
max_der2 = np.max(np.abs(der2))
large = np.where(np.abs(der2) > max_der2/2)[0]
gaps = np.diff(large) > window
begins = np.insert(large[1:][gaps], 0, large[0])
ends = np.append(large[:-1][gaps], large[-1])
changes = ((begins+ends)/2).astype(np.int)
plt.plot(solar_elevation_angle)
plt.plot(changes, solar_elevation_angle[changes], 'ro')
plt.show()
挿入と追加の煩わしさは、大きな導関数を持つ最初のインデックスが「ピークの始まり」と見なされ、最後のインデックスが「ピークの終わり」と見なされるためです。無限です)。
これは代替法(必ずしも優れているとは限りません)であり、導関数を使用しません。フィットは 次数1の平滑化スプライン (つまり、区分的線形曲線)であり、ノットの位置に注意してください。
まず、データを正規化して(solar_elevation_angle
ではなくy
と呼びます)、標準偏差を1にします。
y /= np.std(y)
最初のステップは、データから最大で指定されたしきい値だけ逸脱する区分線形曲線を作成することです。任意に0.1に設定します(yは正規化されているため、ここでは単位はありません)。これは、UnivariateSpline
を繰り返し呼び出し、大きなスムージングパラメータから開始して、曲線がフィットするまで徐々に減らして行います。 (残念ながら、希望する均一なエラー範囲を単純に渡すことはできません)。
from scipy.interpolate import UnivariateSpline
threshold = 0.1
m = y.size
x = np.arange(m)
s = m
max_error = 1
while max_error > threshold:
spl = UnivariateSpline(x, y, k=1, s=s)
interp_y = spl(x)
max_error = np.max(np.abs(interp_y - y))
s /= 2
knots = spl.get_knots()
values = spl(knots)
これまでのところ、ノットを見つけ、それらのノットでのスプラインの値に注目しました。しかし、これらすべての結び目が本当に重要なわけではありません。各ノットの重要性をテストするために、それを削除し、それなしで補間します。新しい補間関数が古い補間関数と大幅に異なる場合(エラーを2倍にする)、ノットは重要であると見なされ、見つかった勾配変化のリストに追加されます。
ts = knots.size
idx = np.arange(ts)
changes = []
for j in range(1, ts-1):
spl = UnivariateSpline(knots[idx != j], values[idx != j], k=1, s=0)
if np.max(np.abs(spl(x) - interp_y)) > 2*threshold:
changes.append(knots[j])
plt.plot(y)
plt.plot(changes, y[np.array(changes, dtype=int)], 'ro')
plt.show()
理想的には、与えられたデータに区分線形関数を当てはめ、ノットの数を増やして、もう1つ追加しても「実質的な」改善がもたらされないようにします。上記はSciPyツールでの大雑把な近似ですが、可能な限り最善ではありません。 Pythonの市販の区分線形モデル選択ツールについては知りません。