指数移動平均 (Exponential Moving Average; EMA)
指数移動平均(EMA)は、時系列データを平滑化する代表的な手法の一つです。現在の観測値と1ステップ前の平滑化された値に重み付けをして足し合わせることで、新しい平滑化値を計算します。
\[ y*t = (1 - \alpha) y*{t-1} + \alpha x_t \]ここで、
- \(y_t\): 時刻 \(t\) におけるEMAの値
- \(x_t\): 時刻 \(t\) における元の観測値
- \(\alpha\): 平滑化係数(または平滑化率、\(0 < \alpha \le 1\))
この式は、最近のデータに大きな重みを置きつつ、古いデータも完全に切り捨てずに考慮するという特徴があります。元の記事の式は \(\beta\) を使っていますが、一般的に \(\alpha\) が使われることが多いので、ここでは \(\alpha\) に統一します。元の記事の \(\beta\) は、ここでいう \(1-\alpha\) に相当します。
周波数特性:ゲイン特性と位相特性
フィルタの周波数特性は、入力信号の周波数成分がフィルタによってどのように変化するかを示します。これは、フィルタの伝達関数 \(G(s)\) を用いて分析されます。
上記のEMAの式をZ変換(離散時間システムの解析に用いられる)すると、伝達関数 \(G(z)\) は次のようになります。
\[ Y(z) = (1 - \alpha) z^{-1} Y(z) + \alpha X(z) \]\[ G(z) = \frac{Y(z)}{X(z)} = \frac{\alpha}{1 - (1 - \alpha) z^{-1}} \]ここで、\(z = e^{j\omega T}\) (\(T\) はサンプリング周期、ここでは \(T=1\) と仮定)とすると、周波数応答 \(G(j\omega)\) が得られます。
\[ G(j\omega) = \frac{\alpha}{1 - (1 - \alpha) e^{-j\omega}} \]この周波数応答から、ゲイン特性 \(|G(j\omega)|\) と位相特性 \(\angle G(j\omega)\) を導出できます。
ゲイン特性
\[ |G(j\omega)| = \frac{\alpha}{\sqrt{1 - 2(1 - \alpha) \cos \omega + (1 - \alpha)^2}} \]位相特性
\[ \angle G(j\omega) = \arctan\left(\frac{(1 - \alpha) \sin \omega}{1 - (1 - \alpha) \cos \omega}\right) \]元の記事の式は符号が逆になっていますが、これは\(\arctan(Y/X)\)の計算において\(X\)と\(Y\)の符号の組み合わせを考慮する必要があるためです。一般的な定義では上記のとおりです。
考察:
- \(\alpha\) が小さい場合(例: \(\alpha=0.1\)):フィルタの周波数帯域が狭くなり、より強い平滑化効果が得られます。しかし、位相遅れが大きくなります。
- \(\alpha\) が大きい場合(例: \(\alpha=0.9\)):平滑化能力は低下しますが、位相遅れは小さくなります。
これは、平滑化の度合いと応答速度(位相遅れ)の間にトレードオフがあることを示しています。
プログラム
以下のPythonコードは、EMAフィルタのゲイン特性と位相特性を計算し、プロットするものです。
import math
import numpy as np # numpyをインポート
import matplotlib.pyplot as plt
# 平滑化係数αのリスト
ALPHAS = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
# 周波数範囲 (rad/s)
FREQ_MAX = 300 # 300 rad/sまで
NUM_POINTS = 500 # プロット点数
def get_gain(alpha, omega):
"""EMAフィルタのゲインを計算する"""
denominator = np.sqrt(1 - 2 * (1 - alpha) * np.cos(omega) + (1 - alpha)**2)
return alpha / denominator
def get_phase(alpha, omega):
"""EMAフィルタの位相を計算する (ラジアン)"""
numerator = (1 - alpha) * np.sin(omega)
denominator = 1 - (1 - alpha) * np.cos(omega)
return np.arctan2(numerator, denominator) # atan2を使用することで正しい象限の角度が得られる
# プロット用のデータ格納リスト
frequencies = [[] for _ in range(len(ALPHAS))]
gains = [[] for _ in range(len(ALPHAS))]
phases = [[] for _ in range(len(ALPHAS))]
# 各α値について計算
for alpha_idx, alpha_val in enumerate(ALPHAS):
for i in range(NUM_POINTS):
omega = i * (np.pi / NUM_POINTS) # 0からπまでをプロット (ナイキスト周波数まで)
# omega = i * (FREQ_MAX / NUM_POINTS) # もしFREQ_MAXまでプロットしたい場合
frequencies[alpha_idx].append(omega)
gains[alpha_idx].append(get_gain(alpha_val, omega))
phases[alpha_idx].append(np.degrees(get_phase(alpha_val, omega))) # 位相を度に変換
# ゲイン特性のプロット
plt.figure(figsize=(10, 6))
plt.xlabel('Frequency (rad/s)')
plt.ylabel('Gain') # dB表示にする場合は20*log10(Gain)
plt.title('EMA Filter Gain Characteristics')
for alpha_idx, alpha_val in enumerate(ALPHAS):
plt.plot(frequencies[alpha_idx], gains[alpha_idx], label=f"alpha={alpha_val}")
plt.legend(bbox_to_anchor=(1.05, 1.0), loc='upper left')
plt.grid(True)
plt.tight_layout()
plt.show()
# 位相特性のプロット
plt.figure(figsize=(10, 6))
plt.xlabel('Frequency (rad/s)')
plt.ylabel('Phase (degrees)')
plt.title('EMA Filter Phase Characteristics')
for alpha_idx, alpha_val in enumerate(ALPHAS):
plt.plot(frequencies[alpha_idx], phases[alpha_idx], label=f"alpha={alpha_val}")
plt.legend(bbox_to_anchor=(1.05, 1.0), loc='upper left')
plt.grid(True)
plt.tight_layout()
plt.show()
関連記事
- 移動平均フィルタの種類と比較 - 単純移動平均(SMA)、加重移動平均(WMA)、EMAの特性比較とPython実装を解説しています。
- 高速フーリエ変換(FFT)の仕組みとPython実装 - EMAの周波数特性を理解する基盤となるDFT/FFTのアルゴリズムと実践的な周波数解析を解説しています。
- カルマンフィルタの理論とPython実装 - EMAの確率的拡張であるカルマンフィルタの理論と実装を解説しています。
- 信号処理におけるフィルタリング手法の基礎 - カルマンフィルタ、EKF、UKF、粒子フィルタの概要を体系的に解説しています。
- ローパスフィルタの設計と比較:移動平均・バターワース・チェビシェフ - EMAを含む各種ローパスフィルタの周波数応答・群遅延・ステップ応答を比較しています。
- カルマンスムーザ(RTS Smoother)の理論とPython実装 - EMAの発展形であるカルマンフィルタを拡張し、全時刻の観測データを使って推定精度を向上させるスムーザを解説しています。
- Matplotlib実践Tips:論文品質のグラフを作る - 周波数特性のグラフをさらに高品質に仕上げるための設定やカラーマップの選択方法を紹介しています。