はじめに
心電図(ECG)や脳波(EEG)、加速度センサなどの計測では、50Hz(日本・欧州)や60Hz(米国)の電源ノイズが信号に混入する問題が頻繁に発生します。このノイズは特定の周波数に集中しているため、単純なローパスフィルタでは対処できないことがあります。たとえば心電図のR波は高周波成分を含むため、ローパスフィルタでノイズを除去しようとすると信号自体も劣化してしまいます。
**ノッチフィルタ(Notch Filter)**は、特定の周波数帯域のみを選択的に除去し、それ以外の周波数成分をそのまま通過させるフィルタです。本記事では、IIR型ノッチフィルタの理論的背景を解説し、SciPyを使ったPython実装で電源ノイズの除去を実践します。
フィルタの基礎知識については FIRフィルタとIIRフィルタの比較 、周波数解析の基礎については 高速フーリエ変換(FFT)の仕組みとPython実装 を参照してください。
ノッチフィルタの基本概念
ノッチフィルタは**帯域除去フィルタ(Band-Reject Filter)**の極端なケースで、非常に狭い周波数帯域だけを除去します。周波数応答は対象周波数において急峻な「ノッチ(切り込み)」を持ち、それ以外の帯域ではほぼ平坦なゲインを維持します。
ノッチフィルタの設計において重要なパラメータは以下の2つです。
- ノッチ周波数 \(f_0\) : 除去対象の中心周波数(例: 50Hz、60Hz)
- 品質係数 \(Q\) : ノッチの鋭さを制御するパラメータ
\(Q\) はノッチ周波数と帯域幅の比で定義されます。
\[Q = \frac{f_0}{\Delta f} \tag{1}\]ここで \(\Delta f\) は \(-3\) dB帯域幅です。\(Q\) が大きいほどノッチが狭くなり、対象周波数のみをピンポイントで除去できます。
IIR型ノッチフィルタの伝達関数
2次IIR型ノッチフィルタの伝達関数は以下のように表されます。
\[H(z) = \frac{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}{1 - 2r\cos(\omega_0)z^{-1} + r^2 z^{-2}} \tag{2}\]ここで \(\omega_0 = 2\pi f_0 / f_s\) は正規化角周波数、\(r\) は極の半径(\(0 < r < 1\) )です。
分子(零点)の役割
分子の零点は単位円上の \(z = e^{\pm j\omega_0}\) に配置されます。零点が単位円上にあるため、周波数 \(\omega_0\) における伝達関数のゲインは正確にゼロになります。これがノッチフィルタの完全な除去特性の由来です。
分母(極)の役割
分母の極は \(z = r \cdot e^{\pm j\omega_0}\) に位置し、単位円の内側(半径 \(r < 1\) )に配置されます。極は零点と同じ角度に配置されますが、半径が \(r\) であるため単位円から離れています。\(r\) が1に近いほど極が零点に近づき、ノッチの影響範囲が狭くなります(高い \(Q\) )。
零点・極の配置と品質係数
極の半径 \(r\) と品質係数 \(Q\) の関係は近似的に以下のように表されます。
\[Q \approx \frac{\omega_0}{2(1-r)} \tag{3}\]したがって帯域幅は次のようになります。
\[\Delta f = \frac{f_0}{Q} \approx \frac{2(1-r) \cdot f_s}{2\pi} \tag{4}\]厳密な導出:近似式(3)はどこから来るのか
式(3)は「近似的に」としか説明されていませんが、scipy.signal.iirnotch が実際に使っている設計式から出発すると、この近似の由来と誤差の大きさを厳密に評価できます。SciPyの実装はOrfanidis (1996) の \(-3\)
dB整合設計に基づき、次の手順で係数を決定しています。
正規化角周波数の帯域幅を \(\Delta\omega = \omega_0/Q\) とし、
\[\beta = \tan\!\left(\frac{\Delta\omega}{2}\right), \qquad G_0 = \frac{1}{1+\beta} \tag{5}\]とおくと、伝達関数の分母係数・分子係数はそれぞれ
\[ a_1 = -2G_0\cos\omega_0, \qquad a_2 = 2G_0 - 1, \qquad b = G_0(1,\ -2\cos\omega_0,\ 1) \]となります。分母の2次方程式
\[ z^2 + a_1 z + a_2 = 0 \]において、共役複素根の積は定数項に等しい(解と係数の関係)ため、極の半径 \(r = |z|\) について
\[r^2 = a_2 = 2G_0 - 1 \tag{6}\]が厳密に成り立ちます。これを \(G_0\) について解くと \(G_0 = (1+r^2)/2\) となり、\(\beta = 1/G_0 - 1\) に代入すれば
\[\Delta\omega = 2\arctan\!\left(\frac{1-r^2}{1+r^2}\right) \tag{7}\]という、\(r\) と帯域幅 \(\Delta\omega\) を結ぶ厳密な関係式が得られます。ここで高 \(Q\) 極限(\(r \to 1\) )を考え、\(r = 1-\varepsilon\) (\(\varepsilon\) は微小量)とおくと、\(r^2 \approx 1-2\varepsilon\) より
\[\frac{1-r^2}{1+r^2} \approx \frac{2\varepsilon}{2} = \varepsilon = 1-r\]となるため、式(7)は \(\Delta\omega \approx 2(1-r)\) に簡略化されます。\(Q = \omega_0/\Delta\omega\) に代入すれば、式(3)の近似式 \(Q \approx \omega_0/(2(1-r))\) が 高 \(Q\) 極限での1次近似 として導出されます。つまり式(3)は天下り的な公式ではなく、厳密な整合設計式(6)–(7)を \(r \to 1\) でテイラー展開した結果です。
この近似がどの程度の誤差を持つかを、実際にコードで検証します。
import numpy as np
from scipy import signal
fs = 1000
f0 = 50
for Q_set in [5, 15, 30, 60, 100]:
b, a = signal.iirnotch(f0, Q_set, fs)
_, poles, _ = signal.tf2zpk(b, a)
r = np.abs(poles[0])
# 式(3)による近似Q
omega0 = 2 * np.pi * f0 / fs
Q_formula = omega0 / (2 * (1 - r))
# freqzで-3dB帯域幅を実測してQを逆算
w, h = signal.freqz(b, a, worN=400_000, fs=fs)
mag_db = 20 * np.log10(np.maximum(np.abs(h), 1e-16))
li = np.where((mag_db[:-1] > -3) & (mag_db[1:] <= -3) & (w[:-1] < f0))[0]
ri = np.where((mag_db[:-1] <= -3) & (mag_db[1:] > -3) & (w[:-1] > f0))[0]
bw_measured = w[ri[0]] - w[li[-1]]
Q_measured = f0 / bw_measured
err_pct = (Q_formula - Q_measured) / Q_measured * 100
print(f"Q_set={Q_set:4d} r={r:.6f} Q_formula={Q_formula:.4f} "
f"Q_measured={Q_measured:.4f} 誤差={err_pct:+.3f}%")
実行結果は以下の通りです。
| 設定 \(Q\) | 極半径 \(r\) | 式(3)近似 \(Q\) | 実測 \(Q\) (-3dB帯域幅から逆算) | 誤差 |
|---|---|---|---|---|
| 5 | 0.969052 | 5.076 | 4.988 | +1.76% |
| 15 | 0.989582 | 15.078 | 14.965 | +0.76% |
| 30 | 0.994778 | 30.078 | 29.940 | +0.46% |
| 60 | 0.997385 | 60.078 | 59.880 | +0.33% |
| 100 | 0.998430 | 100.078 | 100.000 | +0.08% |
\(Q\) が大きいほど式(3)の誤差は単調に縮小しており(\(Q=5\) で \(+1.76\%\) 、\(Q=100\) で \(+0.08\%\) )、これは式(3)が \(r \to 1\) の1次近似であることと整合します。実用域の \(Q = 20 \sim 50\) では誤差はいずれも \(0.5\%\) 未満であり、設計時の近似式としては十分実用的であることが確認できます。
以下のコードで、零点・極の配置を可視化します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- パラメータ ---
fs = 1000 # サンプリング周波数 [Hz]
f0 = 50 # ノッチ周波数 [Hz]
Q = 30 # 品質係数
# --- ノッチフィルタの設計 ---
b, a = signal.iirnotch(f0, Q, fs)
# --- 零点・極の計算 ---
zeros, poles, _ = signal.tf2zpk(b, a)
# --- 零点・極配置図のプロット ---
fig, ax = plt.subplots(figsize=(6, 6))
# 単位円を描画
theta = np.linspace(0, 2 * np.pi, 200)
ax.plot(np.cos(theta), np.sin(theta), 'k-', linewidth=0.5)
# 零点と極をプロット
ax.scatter(zeros.real, zeros.imag, marker='o', s=100,
facecolors='none', edgecolors='blue', linewidths=2, label='Zeros')
ax.scatter(poles.real, poles.imag, marker='x', s=100,
color='red', linewidths=2, label='Poles')
ax.set_xlabel('Real')
ax.set_ylabel('Imaginary')
ax.set_title(f'Pole-Zero Plot (f0={f0} Hz, Q={Q})')
ax.set_aspect('equal')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 極の半径を表示
print(f"極の半径 r = {np.abs(poles[0]):.6f}")
print(f"零点の半径 = {np.abs(zeros[0]):.6f}")
実行結果は以下の通りです。
極の半径 r = 0.994778
零点の半径 = 1.000000
零点の半径は浮動小数点誤差の範囲で厳密に1(単位円上)であり、極の半径は式(6)から導かれる \(r=\sqrt{2G_0-1}=0.994778\) と一致します。両者がほぼ同じ角度 \(\omega_0\) にあるため、ノッチ周波数以外のゲインはほぼ1(0 dB)に保たれます。

scipy.signal.iirnotch による設計
SciPyの scipy.signal.iirnotch を使えば、ノッチフィルタの設計が簡潔に行えます。
from scipy.signal import iirnotch
# パラメータ
f0 = 50 # ノッチ周波数 [Hz]
Q = 30 # 品質係数
fs = 1000 # サンプリング周波数 [Hz]
# フィルタ係数の計算
b, a = iirnotch(f0, Q, fs)
f0: 除去したい周波数 [Hz]Q: 品質係数(大きいほど狭いノッチ)fs: サンプリング周波数 [Hz]- 戻り値: 分子係数
b、分母係数a(2次IIRフィルタ)
以下のコードで、異なる \(Q\) 値における周波数応答を比較します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
fs = 1000
f0 = 50
fig, ax = plt.subplots(figsize=(10, 6))
for Q in [5, 15, 30, 60]:
b, a = signal.iirnotch(f0, Q, fs)
w, h = signal.freqz(b, a, worN=4096, fs=fs)
ax.plot(w, 20 * np.log10(np.maximum(np.abs(h), 1e-12)),
label=f'Q = {Q}')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Magnitude [dB]')
ax.set_title('Notch Filter Frequency Response (f0 = 50 Hz)')
ax.set_xlim(0, 200)
ax.set_ylim(-60, 5)
ax.axvline(f0, color='gray', linestyle='--', alpha=0.5, label=f'f0 = {f0} Hz')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
\(Q\)
が大きいほどノッチが狭く急峻になり、対象周波数のみをピンポイントで除去できることがわかります。実際に freqz の出力から \(-3\)
dB帯域幅を測定すると、\(Q=5\)
で \(10.01\)
Hz、\(Q=15\)
で \(3.36\)
Hz、\(Q=30\)
で \(1.65\)
Hz、\(Q=60\)
で \(0.85\)
Hz となり、\(Q\)
にほぼ反比例して帯域幅が狭くなることが確認できます(前節の表の実測 \(Q\)
列と対応)。

実践:電源ノイズ除去のPython実装
実際の電源ノイズ除去を想定した完全なコード例を示します。50Hzの基本波に加え、100Hz・150Hzの高調波も同時に除去します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- テスト信号の生成 ---
np.random.seed(42)
fs = 1000 # サンプリング周波数 [Hz]
T = 2.0 # 信号長 [s]
t = np.arange(0, T, 1/fs)
N = len(t)
# 有用信号: 10Hzと30Hzの正弦波
useful_signal = np.sin(2 * np.pi * 10 * t) + 0.5 * np.sin(2 * np.pi * 30 * t)
# 電源ノイズ: 50Hz基本波 + 100Hz, 150Hzの高調波
power_noise = (0.8 * np.sin(2 * np.pi * 50 * t)
+ 0.4 * np.sin(2 * np.pi * 100 * t)
+ 0.2 * np.sin(2 * np.pi * 150 * t))
# ランダムノイズ
random_noise = 0.3 * np.random.randn(N)
# 観測信号
observed = useful_signal + power_noise + random_noise
# --- カスケード接続のノッチフィルタ設計 ---
Q = 30
notch_freqs = [50, 100, 150] # 基本波 + 高調波
# 各ノッチ周波数のフィルタ係数をSOS形式で結合
sos_list = []
for f_notch in notch_freqs:
b, a = signal.iirnotch(f_notch, Q, fs)
sos = signal.tf2sos(b, a)
sos_list.append(sos[0])
sos_cascade = np.array(sos_list)
# --- フィルタ適用(ゼロ位相フィルタリング) ---
filtered = signal.sosfiltfilt(sos_cascade, observed)
# --- 周波数スペクトルの計算 ---
freqs = np.fft.rfftfreq(N, 1/fs)
spectrum_before = np.abs(np.fft.rfft(observed)) / N
spectrum_after = np.abs(np.fft.rfft(filtered)) / N
# --- プロット ---
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
# (a) 時間領域: フィルタ適用前
axes[0].plot(t, observed, alpha=0.7, label='Observed')
axes[0].plot(t, useful_signal, 'k--', alpha=0.5, label='True signal')
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Before Notch Filtering')
axes[0].set_xlim(0, 0.5)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# (b) 時間領域: フィルタ適用後
axes[1].plot(t, filtered, alpha=0.7, label='Filtered')
axes[1].plot(t, useful_signal, 'k--', alpha=0.5, label='True signal')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('After Notch Filtering (50, 100, 150 Hz)')
axes[1].set_xlim(0, 0.5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# (c) 周波数領域: 前後比較
axes[2].plot(freqs, 20 * np.log10(np.maximum(spectrum_before, 1e-12)),
alpha=0.7, label='Before')
axes[2].plot(freqs, 20 * np.log10(np.maximum(spectrum_after, 1e-12)),
alpha=0.7, label='After')
for f_notch in notch_freqs:
axes[2].axvline(f_notch, color='red', linestyle='--', alpha=0.3)
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Magnitude [dB]')
axes[2].set_title('Frequency Spectrum: Before vs After')
axes[2].set_xlim(0, 200)
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
上記コードに以下の検証コードを追加し、フィルタ適用前後の定量的な効果を実測します。
# --- 定量評価 ---
rmse_before = np.sqrt(np.mean((observed - useful_signal) ** 2))
rmse_after = np.sqrt(np.mean((filtered - useful_signal) ** 2))
snr_before = 10 * np.log10(np.mean(useful_signal**2) / np.mean((observed - useful_signal) ** 2))
snr_after = 10 * np.log10(np.mean(useful_signal**2) / np.mean((filtered - useful_signal) ** 2))
print(f"RMSE: before={rmse_before:.5f}, after={rmse_after:.5f}")
print(f"SNR : before={snr_before:.3f} dB, after={snr_after:.3f} dB")
for f_notch in notch_freqs:
idx = np.argmin(np.abs(freqs - f_notch))
atten_db = 20 * np.log10(spectrum_after[idx] / spectrum_before[idx])
print(f" {f_notch} Hz 減衰量 = {atten_db:.2f} dB")
for f_useful in [10, 30]:
idx = np.argmin(np.abs(freqs - f_useful))
change_db = 20 * np.log10(spectrum_after[idx] / spectrum_before[idx])
print(f" 有用信号 {f_useful} Hz の変化 = {change_db:.3f} dB")
実行結果は以下の通りです。
RMSE: before=0.70573, after=0.29683
SNR : before=0.986 dB, after=8.509 dB
50 Hz 減衰量 = -26.67 dB
100 Hz 減衰量 = -30.49 dB
150 Hz 減衰量 = -33.32 dB
有用信号 10 Hz の変化 = -0.001 dB
有用信号 30 Hz の変化 = -0.011 dB
時間領域では、フィルタ適用後の信号が元の有用信号(10Hz + 30Hz)に近づいており、真値に対するRMSEは \(0.706 \to 0.297\) (約 \(58\%\) 減少)、SNRは \(0.99\) dB から \(8.51\) dB へと約 \(7.5\) dB改善しています。周波数領域では、50Hz・100Hz・150Hzの各ピークがそれぞれ \(-26.7\) dB、\(-30.5\) dB、\(-33.3\) dB減衰している一方、10Hzと30Hzの有用信号成分の変化はいずれも \(0.01\) dB未満に収まっており、対象周波数のみを選択的に除去できていることが定量的に確認できます。

設計上の注意点
品質係数Qの選択
- Q が大きすぎる場合: ノッチが極端に狭くなり、わずかな周波数変動にも対応できません。また、過渡応答でリンギングが生じやすくなります。
- Q が小さすぎる場合: ノッチが広くなり、対象周波数付近の有用な信号成分まで除去してしまいます。
実用的には \(Q = 20 \sim 50\) の範囲が電源ノイズ除去に適しています。
エッジケース:商用電源周波数のドリフトに対する脆弱性
これまでの例では電源ノイズをちょうど50Hzと仮定してきましたが、実際の商用電源周波数は需給バランスにより常に微小変動しており、日本の電力系統でも定格からずれることがあります。設計時の \(f_0\) と実際のノイズ周波数がわずかにずれた場合、高 \(Q\) のノッチほど急激に減衰量を失う点は見落とされがちな重要な弱点です。
import numpy as np
from scipy import signal
fs = 1000
f0_design = 50.0
drift_range = np.linspace(-1.0, 1.0, 201) # 設計周波数からのずれ [Hz]
for Q in [10, 30, 60, 100]:
b, a = signal.iirnotch(f0_design, Q, fs)
for d in [0.1, 0.2, 0.5, 1.0]:
idx = np.argmin(np.abs(drift_range - d))
w, h = signal.freqz(b, a, worN=[2 * np.pi * (f0_design + d) / fs])
atten_db = 20 * np.log10(np.abs(h[0]))
print(f"Q={Q:3d}, drift={d:+.1f}Hz -> 減衰量 = {atten_db:.2f} dB")
実行結果(抜粋)は以下の通りです。
| 設計からのずれ | \(Q=10\) | \(Q=30\) | \(Q=60\) | \(Q=100\) |
|---|---|---|---|---|
| \(+0.1\) Hz | \(-27.97\) dB | \(-18.49\) dB | \(-12.65\) dB | \(-8.61\) dB |
| \(+0.2\) Hz | \(-21.98\) dB | \(-12.65\) dB | \(-7.29\) dB | \(-4.10\) dB |
| \(+0.5\) Hz | \(-14.19\) dB | \(-5.80\) dB | \(-2.31\) dB | \(-0.98\) dB |
| \(+1.0\) Hz | \(-8.68\) dB | \(-2.32\) dB | \(-0.71\) dB | \(-0.27\) dB |
設計中心からわずか \(0.2\) Hzずれただけで、\(Q=100\) の減衰量は \(-4.10\) dBまで低下し、電源ノイズ除去としてはほぼ機能しなくなります。一方 \(Q=10\) は同じずれでも \(-21.98\) dBの減衰を維持します。つまりQを上げるほどノッチ周波数における理論上の減衰は深くなるが、その代償として実際の周波数変動に対する頑健性を失うというトレードオフが定量的に確認できます。

この問題への実務的な対策としては、(1) 実測ノイズ周波数をゼロクロス法やFFTで推定し \(f_0\) を追従させる、(2) 適度に低い \(Q\) (\(20\sim30\) 程度)で頑健性を確保する、(3) 適応ノッチフィルタで中心周波数自体をオンラインに更新する、といった手法が使われます。実際、Mir & Singh (2024) は変分モード分解(VMD)でノイズ成分を狭帯域モードに分解し、その中心周波数の推定値に追従して通過帯域を動的に再設計する可変ノッチフィルタを提案し、固定周波数のノッチ設計よりも高いSNR改善を報告しています。また、アナログ回路側でも、Choubey et al. (2024) がVDCC(voltage differencing current conveyor)を用いた4次カスケードのコムフィルタ(50/150/250/350Hzの基本波・奇数次高調波を同時除去)を提案しており、単一ノッチで \(-45.7\) dBの減衰を実現しています。本記事のカスケード構成(式(2)を50/100/150Hzに適用)は、こうした研究で扱われるアナログ実装の考え方をデジタル領域で再現したものと位置づけられます。
ゼロ位相フィルタリングと因果フィルタリング
- オフライン処理:
scipy.signal.sosfiltfilt(ゼロ位相)を使用します。順方向・逆方向の2回フィルタリングにより位相歪みがゼロになります。 - リアルタイム処理:
scipy.signal.sosfilt(因果フィルタ)を使用します。未来のデータを参照できないため位相遅れが生じますが、逐次処理が可能です。
安定性
IIR型ノッチフィルタは、極の半径 \(r < 1\)
であれば常に安定です。scipy.signal.iirnotch で設計したフィルタは自動的にこの条件を満たすため、安定性を個別に検証する必要はありません。
エッジケース:固定小数点実装における係数量子化
ただし、この安定性の保証は無限精度(倍精度浮動小数点)演算を前提としています。組み込みDSPやFPGAで固定小数点実装する場合、分母の係数(式(6)の \(a_2\) など)を有限ビット幅に丸める必要があり、高 \(Q\) ほど \(r\) が1に極めて近くなるため、量子化誤差が安定性マージンを食いつぶすリスクが高まります。
import numpy as np
from scipy import signal
fs = 1000
f0 = 50
def quantize(coeffs, bits):
"""[-2, 2) の範囲を bits ビットの固定小数点に丸める簡易モデル"""
scale = 2 ** (bits - 2)
return np.round(coeffs * scale) / scale
for Q in [30, 100, 300]:
b, a = signal.iirnotch(f0, Q, fs)
r_orig = np.max(np.abs(np.roots(a)))
print(f"Q={Q}: 元の極半径 r = {r_orig:.6f}")
for bits in [16, 10, 8]:
a_q = quantize(a, bits)
r_q = np.max(np.abs(np.roots(a_q)))
print(f" {bits}bit量子化後: r = {r_q:.6f}, 安定 = {r_q < 1.0}")
実行結果は以下の通りです。
| \(Q\) | 元の \(r\) | 16bit量子化後 | 10bit量子化後 | 8bit量子化後 |
|---|---|---|---|---|
| 30 | 0.994778 | 0.994768(安定) | 0.994123(安定) | 0.992157(安定) |
| 100 | 0.998430 | 0.998442(安定) | 0.998045(安定) | 1.000000(不安定) |
| 300 | 0.999477 | 0.999481(安定) | 1.000000(不安定) | 1.000000(不安定) |
\(Q=30\) は8bit量子化でも安定を保ちますが、\(Q=100\) は8bit量子化で極半径がちょうど1に達して不安定化し、\(Q=300\) ではさらに緩い10bit量子化でも不安定化します。これは、高 \(Q\) 設計では極が単位円のごく近傍(\(1-r\) が \(10^{-3}\) 以下のオーダー)に位置するため、係数の丸め誤差がそのまま安定余裕を吸収してしまうためです。組み込み実装で高 \(Q\) のノッチフィルタを使う場合は、直接形(Direct Form)ではなく数値誤差に強い格子構造(lattice)や状態空間表現を用いる、あるいは倍精度以上のビット幅を確保するといった対策が必要です。
ノッチフィルタ vs バンドストップフィルタ
ノッチフィルタは帯域除去フィルタの特殊なケースですが、両者は使い分けが必要です。
| 特性 | ノッチフィルタ | バンドストップフィルタ |
|---|---|---|
| 除去帯域 | 非常に狭い(1つの周波数) | 比較的広い帯域 |
| 次数 | 2次で実現可能 | 高次数が必要な場合あり |
| 用途 | 電源ノイズ、特定の干渉信号除去 | 周波数帯域全体の除去 |
| 設計 | iirnotch | butter + btype='bandstop' |
以下に両者の周波数応答を比較するコードを示します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
fs = 1000
f0 = 50
# ノッチフィルタ(Q=30)
b_notch, a_notch = signal.iirnotch(f0, Q=30, fs=fs)
w_notch, h_notch = signal.freqz(b_notch, a_notch, worN=4096, fs=fs)
# バンドストップフィルタ(バターワース4次、40-60Hz除去)
sos_bs = signal.butter(4, [40, 60], btype='bandstop', fs=fs, output='sos')
w_bs, h_bs = signal.sosfreqz(sos_bs, worN=4096, fs=fs)
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(w_notch, 20 * np.log10(np.maximum(np.abs(h_notch), 1e-12)),
label='Notch (Q=30)')
ax.plot(w_bs, 20 * np.log10(np.maximum(np.abs(h_bs), 1e-12)),
label='Bandstop Butterworth (40-60 Hz)')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Magnitude [dB]')
ax.set_title('Notch Filter vs Bandstop Filter')
ax.set_xlim(0, 200)
ax.set_ylim(-60, 5)
ax.axvline(f0, color='gray', linestyle='--', alpha=0.5)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ノッチフィルタは50Hzのみをピンポイントで除去するのに対し、バンドストップフィルタは40〜60Hzの帯域全体を抑制します。除去したい周波数が明確な場合はノッチフィルタ、ある帯域を広く除去したい場合はバンドストップフィルタが適しています。
まとめ
- ノッチフィルタは特定の周波数のみを除去する帯域除去フィルタの特殊なケースで、電源ノイズ除去に最適である
- 2次IIR型の伝達関数は、単位円上の零点でノッチ周波数を完全に除去し、内側の極でノッチ幅を制御する
- 品質係数 \(Q\) が大きいほどノッチが狭くなり、\(Q = 20 \sim 50\) が電源ノイズ除去の実用的な範囲である
scipy.signal.iirnotchで簡潔に設計でき、高調波(100Hz、150Hz等)にはカスケード接続で対応できる- オフライン処理では
sosfiltfilt(ゼロ位相)、リアルタイム処理ではsosfilt(因果フィルタ)を使い分ける - 極半径 \(r\) と \(Q\) の厳密な関係は \(r=\sqrt{2G_0-1}\) (\(G_0=1/(1+\tan(\Delta\omega/2))\) )であり、教科書的な近似式 \(Q\approx\omega_0/(2(1-r))\) は高 \(Q\) 極限での1次近似(実測誤差は \(Q=30\) で \(+0.46\%\) )である
- 高 \(Q\) ノッチは商用電源周波数のわずかなドリフト(\(0.2\) Hzのずれで\(Q=100\) の減衰量は\(-4.10\) dBまで低下)や固定小数点実装の係数量子化(\(Q=100\) は8bit量子化で不安定化)に対して脆弱であり、頑健性重視なら \(Q=20\sim30\) 程度に抑えるか適応ノッチ化を検討する
関連記事
- 高速フーリエ変換(FFT)の仕組みとPython実装 - ノッチフィルタの効果をFFTで確認するための基礎を解説しています。
- FIRフィルタとIIRフィルタの比較 - ノッチフィルタが属するIIRフィルタの特性を解説しています。
- バターワースフィルタの設計とPython実装 - ローパス/ハイパス/バンドパスフィルタの設計手法を解説しています。
- 指数移動平均(EMA)フィルタの周波数特性 - フィルタの伝達関数と周波数応答の基礎を解説しています。
- ローパスフィルタの設計と比較 - 各種ローパスフィルタの周波数応答を比較しています。
- 窓関数とパワースペクトル密度(PSD)の理論とPython実装 - ノッチフィルタ適用前後のPSD解析に有用な手法を解説しています。
- ハイパスフィルタの設計とPython実装 - ノッチが特定周波数のみ除去するのに対し、直流側全体を除去するHPFの設計を解説しています。
- バンドパスフィルタの設計とPython実装 - ノッチ(バンドストップ)の双対であるバンドパス(バンドセレクト)の設計を解説しています。
- チェビシェフフィルタの設計原理とPython実装 - 等リップル設計でノッチに近い急峻な阻止帯域を実現する代替アプローチです。
- ウィーナーフィルタの理論とPython実装 - ノッチ周波数を手動指定するのに対し、ノイズPSDから自動的に除去帯域を決めるアプローチです。
- 適応フィルタ(LMS/RLS)の理論とPython実装 - 周波数が時間変動する電源ノイズに対応できる適応ノッチへの拡張の基礎となります。
- ベッセルフィルタの理論とPython実装 - ノッチによる位相不連続を避けたい場合の対比となる、群遅延が最大限平坦なIIRフィルタを解説しています。
- ボード線図(Bode plot)の理論とPython実装 - ノッチ周波数での深いディップと急峻な位相反転をボード線図で読み解くハブ記事です。
- 短時間フーリエ変換(STFT)の理論とPython実装 - ノッチで電源ハム等を除去した後の時間-周波数構造の確認に有効な手法です。
- デジタルフィルタ設計指針ハブ:選定の3軸と特性比較 — 本記事のノッチフィルタを他のフィルタ群と比較しながら選定する際の指針として活用できます。
参考文献
- Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
- Haykin, S., & Van Veen, B. (2002). Signals and Systems (2nd ed.). Wiley.
- Orfanidis, S. J. (1996). Introduction to Signal Processing. Prentice-Hall.(
scipy.signal.iirnotchの \(-3\) dB整合設計はこの文献の式11.3.4/11.3.19/11.3.6/11.3.7/11.3.21に基づく) - Mir, H. Y., & Singh, B. (2024). Powerline interference reduction in ECG signals using variable notch filter designed via variational mode decomposition. Analog Integrated Circuits and Signal Processing, 118, 317–328. https://link.springer.com/article/10.1007/s10470-023-02200-9
- Choubey, C. K., Kumar, S., & Pippal, S. K. (2024). Design method for VDCC-based analog comb filter for power line interference cancellation. MethodsX. https://pmc.ncbi.nlm.nih.gov/articles/PMC10912721/
- SciPy signal.iirnotch documentation
- SciPy Signal Processing documentation