ピーク候補を高さだけで選ぶと、高い基線に乗った小さな揺れまで拾うことがある。find_peaksのheight、prominence、distanceは別の条件なので、役割を分けて設定する。検出した添字を元の波形へ重ね、なぜ採用・除外されたかを確かめよう。
最小例で確かめる
以下は説明用に作った小さなデータである。実測データや実行速度の測定結果ではない。コード全体をexample.pyとして保存すれば、入力ファイルを別途用意せずに実行できる。assertは、この例で成り立つべき形や値を確認するために入れてある。
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import find_peaks
y = np.array([0.,3.,0.,0.,6.,5.,5.6,5.,0.,0.,2.5,0.,2.,0.,0.])
dt = 0.1
t = np.arange(y.size) * dt
if not np.isfinite(y).all():
raise ValueError("split or repair missing signal before detection")
height_only, _ = find_peaks(y, height=2.)
prominent, _ = find_peaks(y, height=2., prominence=1.)
peaks, properties = find_peaks(y, height=2., prominence=1., distance=3)
print("height only:", height_only.tolist())
print("with prominence:", prominent.tolist())
print("with distance:", peaks.tolist())
print("times [s]:", t[peaks].round(3).tolist())
print("prominences:", properties["prominences"].tolist())
assert height_only.tolist() == [1,4,6,10,12]
assert prominent.tolist() == [1,4,10,12]
assert peaks.tolist() == [1,4,10]
assert np.all(np.diff(peaks) >= 3)
assert np.all(properties["prominences"] >= 1.)
fig, ax = plt.subplots(figsize=(8, 4.5), layout="constrained")
ax.plot(t, y, "o-", color="0.5", label="synthetic signal")
ax.plot(t[height_only], y[height_only], "o", markersize=11,
markerfacecolor="none", color="#e38619", label="height candidates")
ax.plot(t[peaks], y[peaks], "x", markersize=10, markeredgewidth=2,
color="#a83262", label="selected peaks")
ax.vlines(t[peaks], y[peaks] - properties["prominences"], y[peaks],
color="#a83262", alpha=0.5, label="selected prominence")
ax.axhline(2., linestyle=":", color="#26778c", label="height threshold")
ax.set(xlabel="Time [s]", ylabel="Signal [arb. unit]",
title="Height >= 2, prominence >= 1, distance >= 3 samples", ylim=(-0.3,8.3))
ax.legend(ncols=2, loc="upper right", fontsize=9)
fig.savefig("PY086-peaks.png", dpi=160)
plt.close(fig)
実行結果
height only: [1, 4, 6, 10, 12]
with prominence: [1, 4, 10, 12]
with distance: [1, 4, 10]
times [s]: [0.1, 0.4, 1.0]
prominences: [3.0, 6.0, 2.5]
高さとprominenceの違い
heightは波形の縦軸そのものの高さに対する条件である。例の添字6は5.6なので高さ2以上を満たすが、周辺は5付近であり、目立つ増加は小さい。prominenceを1以上にすると、この候補が外れる。単に絶対値が大きいことと、周辺から際立っていることを分けている。
prominenceはピークから、そのピークを囲む基準の等高線までの縦方向の差として計算される。隣の1標本との差だけではなく、左右の谷やより高いピークとの関係が関わる。隣接標本との差を条件にするthresholdとは別の量なので、名前だけで似た設定を入れ替えない方がよい。
distanceは時間ではなく標本数
distance=3は、返すピークの添字間隔を少なくとも3標本にする条件である。例では添字10と12が近いため、低い方の12が外れる。標本間隔が0.1秒なので、この条件は添字の差として0.3秒に相当する。distanceへ秒数をそのまま渡すと意図した間隔にならない。
時間で決めた最小間隔を標本数へ変えるには、等間隔サンプリングであることを確認する。時刻が不等間隔なら、同じ添字差でも実時間の差は違う。候補抽出後に実時刻で別の規則を適用するなど、データの条件に合わせた処理が必要になる。distanceは真のイベントの最小間隔を自動的に推定するものではない。
候補を元の波形へ重ねる
find_peaksが返すのは時刻でもピーク値でもなく、入力信号の添字である。時刻はt[peaks]、値はy[peaks]として取り出す。propertiesには指定した条件に応じて高さやprominenceなどが入るので、条件に合格した理由を表や図へ残せる。元波形を切り出していた場合は、元の記録への添字対応も保つ。
図では高さだけの候補を丸、最終採用を×で示し、採用されたprominenceの幅を縦線で表示した。高さ条件だけでは残る小さな肩や、近すぎる候補がどこで除外されたか分かる。採用点だけを描いて元の波形を隠すと、しきい値の影響を判断しにくい。
欠測、端点、ノイズに注意する
NaNを含む信号では予想外の検出結果になり得る。単にNaNを削ると、その前後の点が隣り合っていたように接続され、標本間隔やprominenceが変わる。有限な連続区間へ分割するか、補間してよい根拠を定めるなど、時刻構造を保った処理を行う。この例は非有限値があれば検出前に停止する。
find_peaksは隣接値から局所最大を探すため、データ端のピークや平らな頂上には専用の考慮が必要になる。平滑化するとノイズを減らせる場合もあるが、ピーク位置や高さが変わることがある。条件を調整した履歴と元信号を残し、既知のイベントや別の測定と照合しよう。検出された候補を、そのまま物理的に真のイベントと断定しないことが重要である。

動作確認環境と参考資料
Linux・CPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8の環境で掲載コードを実行した。使用するライブラリはコード冒頭のimportを参照してほしい。公式資料の最新版と、この実行確認版は区別している。数値の末尾や表の表示幅は環境によって変わることがある。
