センサー値のノイズをPythonで除去!移動平均・中央値・EMAフィルタを比べて飛び値に強い2段フィルタを作る
「センサーの値がピクピク揺れて、しきい値の判定がうまくいかない…」「たまに一瞬だけ、ありえない値が出るんです」
電子工作でセンサーを使い始めると、ほぼ全員がこの壁にぶつかりますよね。温度は少しずつしか変わらないはずなのに、表示は25.3→25.9→25.1…とバラバラ。ファンの自動ON/OFFや通知に使うと、ON/OFFを何度も繰り返してしまいます。
この記事では、Pythonで移動平均・中央値(メディアン)・指数移動平均(EMA)の3つのノイズ除去フィルタを作って、同じデータで性能を比べます。難易度はPython入門〜初級。使うのは標準ライブラリとmatplotlibだけなので、センサーが手元になくてもPCだけで試せます😊
最後に、飛び値にも強くて反応も速い「2段フィルタ」をクラスにまとめるところまで、一緒に見ていきましょう!
センサーのノイズには2種類ある

フィルタを選ぶ前に、ノイズの「種類」を知っておくのが大事です。
- 細かいノイズ:本当の値のまわりで±0.3℃くらい小刻みに揺れる。電気的なノイズやADCの誤差など
- 飛び値(スパイク):一瞬だけ+6℃のような、ありえない値が出る。配線の接触不良や通信エラーなど
イメージとしては、細かいノイズは「手ぶれ」、飛び値は「くしゃみ」です。手ぶれは平均すれば消えますが、くしゃみは平均すると周りまで汚してしまいます。この違いが、フィルタ選びのカギになります。
3つのフィルタをPythonで作って比べる
まずは、ゆっくり上がっていく温度に「細かいノイズ」と「飛び値2回」を混ぜたダミーデータを作り、3つのフィルタを通してみます。
ポイントをまとめるとこんな感じです👇
- 移動平均:直近n個の平均。いちばんシンプル
- 中央値フィルタ:直近n個を並べた真ん中の値。飛び値に強い
- EMA(指数移動平均):「今回の値をα、これまでの結果を1−α」の割合で混ぜる。覚えておく値が1つで済む
- どれも
deque(maxlen=n)で「直近n個だけ持つ箱」を作っているので、データが流れてくるリアルタイム処理にもそのまま使えます
import random
from collections import deque
from statistics import median
# --- ダミーのセンサーデータを作る(本物の温度センサーのつもり) ---
random.seed(42)
true_temp = [25.0 + i * 0.05 for i in range(40)] # 本当の温度(ゆっくり上昇)
raw = [t + random.gauss(0, 0.3) for t in true_temp] # 細かいノイズ
for i in (12, 27): # ときどき出る「飛び値」
raw[i] += 6.0
def moving_average(values, n=5):
"""直近n個の平均(単純移動平均)"""
buf = deque(maxlen=n)
out = []
for v in values:
buf.append(v)
out.append(sum(buf) / len(buf))
return out
def median_filter(values, n=5):
"""直近n個の中央値"""
buf = deque(maxlen=n)
out = []
for v in values:
buf.append(v)
out.append(median(buf))
return out
def ema(values, alpha=0.3):
"""指数移動平均:新しい値をalphaの割合で混ぜる"""
out = []
y = values[0]
for v in values:
y = alpha * v + (1 - alpha) * y
out.append(y)
return out
ma = moving_average(raw)
md = median_filter(raw)
ex = ema(raw)
print(" No | 生の値 | 移動平均 | 中央値 | EMA")
for i in range(10, 16):
print(f"{i:3d} | {raw[i]:6.2f} | {ma[i]:8.2f} | {md[i]:6.2f} | {ex[i]:5.2f}")
# 本当の温度とのズレ(平均絶対誤差)で比べる
def mae(values):
return sum(abs(v - t) for v, t in zip(values, true_temp)) / len(values)
print()
for name, data in [("生の値", raw), ("移動平均", ma), ("中央値", md), ("EMA", ex)]:
print(f"{name}: 平均誤差 {mae(data):.3f} ℃")

実行すると、飛び値が入ったNo.12の前後の値と、本当の温度とのズレ(平均誤差)が表示されます。実行結果の画面で確認してみましょう。
ここが重要です👇
- No.12で生の値が31.80℃に跳ねたとき、移動平均は26.82℃、EMAは27.42℃まで引っぱられています
- しかも移動平均は、その飛び値が5個の箱から抜けるNo.16まで、26.8℃台に浮いたままです
- 中央値は25.57℃と、飛び値をほぼ無視できています。平均誤差も0.132℃で、3つの中でいちばん小さくなりました
これ、実行する前に予想できましたか?「平均を取ればノイズは消える」と思いがちですが、飛び値があると平均はむしろ悪さをするんです。
グラフで見ると違いが一目でわかる
数字だけだとピンとこないので、matplotlibでグラフにしてみましょう。さっきのfilters.pyと同じフォルダに保存して実行します。
import matplotlib.pyplot as plt
from filters import true_temp, raw, ma, md, ex # さっきのデータとフィルタ結果を使う
plt.rcParams["font.family"] = "Meiryo" # 日本語ラベル用(Windows)
x = range(len(raw))
plt.figure(figsize=(9, 4.5))
plt.plot(x, raw, "o-", color="lightgray", label="生の値")
plt.plot(x, true_temp, "k--", label="本当の温度")
plt.plot(x, ma, label="移動平均(5個)")
plt.plot(x, md, label="中央値(5個)")
plt.plot(x, ex, label="EMA(α=0.3)")
plt.xlabel("サンプル番号")
plt.ylabel("温度 [℃]")
plt.ylim(24, 33)
plt.title("ノイズ除去フィルタの比較")
plt.legend()
plt.grid(alpha=0.3)
plt.show()

グラフを見ると、次のことがわかります。
- 移動平均(青)は、飛び値のあと台形のように5サンプルぶん浮き上がる
- EMA(緑)は、飛び値でピンと跳ねてからじわじわ戻る
- 中央値(オレンジ)は、飛び値の影響をほとんど受けず、点線(本当の温度)に沿っている
⚠️ ただし中央値にも弱点があります。グラフをよく見ると、オレンジの線は階段状にカクカクしていて、細かいノイズをなめらかにするのは得意ではありません。
どのフィルタを選べばいい?比較表
| フィルタ | 細かいノイズ | 飛び値 | 覚えておく値 | 向いている場面 |
|---|---|---|---|---|
| 移動平均 | ◎ | ×(周りまで汚す) | n個 | 飛び値が出ない安定したセンサー |
| 中央値 | △(カクカク) | ◎ | n個 | 接触不良・通信エラーで変な値が混ざるとき |
| EMA | ○ | △(じわじわ戻る) | 1個 | メモリが少ないマイコン、反応の速さを調整したいとき |
つまり、「飛び値は中央値で消し、細かいノイズはEMAでならす」と組み合わせれば、いいとこ取りができるわけです。
実践:飛び値に強い「2段フィルタ」をクラスにする
最後に、中央値(3個)→EMAの2段フィルタをSensorFilterクラスにまとめます。値が来るたびにupdate()を呼ぶだけなので、シリアル通信で受け取ったセンサー値にもそのまま使えます。
今回は「25℃から30℃に急に変わる」「途中で1回だけ99.9℃という飛び値が来る」データで、αを0.3と0.6にしたときの違いを見てみます。
from collections import deque
from statistics import median
class SensorFilter:
"""中央値で飛び値を消してから、EMAでなめらかにする2段フィルタ"""
def __init__(self, median_n=3, alpha=0.3):
self.buf = deque(maxlen=median_n)
self.alpha = alpha
self.y = None # まだ値が来ていない
def update(self, value):
self.buf.append(value)
m = median(self.buf) # 1段目:飛び値をカット
if self.y is None:
self.y = m # 最初の1回はそのまま採用
else:
self.y = self.alpha * m + (1 - self.alpha) * self.y # 2段目:EMA
return self.y
# 温度が25℃→30℃に急に変わった+途中に飛び値が1回、というデータで試す
readings = [25.0] * 5 + [30.0] * 3 + [99.9] + [30.0] * 7
for alpha in (0.3, 0.6):
f = SensorFilter(median_n=3, alpha=alpha)
results = [f.update(v) for v in readings]
line = " ".join(f"{r:5.1f}" for r in results)
print(f"alpha={alpha}: {line}")
print(f" 最大値 {max(results):.1f} ℃ / 29.5℃を超えたのは {next(i for i, r in enumerate(results) if r > 29.5)} 番目")

読み取れるポイントです👇
- 8番目(0始まり)の99.9℃の飛び値は、どちらのαでも完全に消えています。最大値は30℃を超えていません
- 温度が30℃に変わったのは5番目ですが、5番目の出力はまだ25.0℃です。中央値(3個)は、新しい値が2個そろうまで反応しないためです
- α=0.3は29.5℃を超えるまで12番目までかかり、α=0.6は8番目で追いつきました。αを大きくすると反応は速くなりますが、そのぶん細かいノイズは残りやすくなります
αの決め方の目安は、「温度や湿度のようにゆっくり変わるものは0.1〜0.3」「距離センサーのように速く動くものは0.5以上」から始めて、実際のデータを見ながら調整するのがおすすめです。
PySerialでArduinoから値を受け取っている場合は、ループの中でvalue = f.update(float(line))のように通すだけでOKです。しきい値の判定には、生の値ではなくフィルタ後のvalueを使いましょう。
まとめ
- センサーのノイズには細かいノイズと飛び値の2種類がある
- 移動平均は飛び値に弱く、n個ぶん周りまで汚してしまう
- 中央値フィルタは飛び値に強い。今回のデータでは平均誤差0.132℃でいちばん正確だった
- EMAは覚えておく値が1つで済み、αで反応の速さを調整できる
- 迷ったら「中央値(3個)→EMA」の2段フィルタにすると、飛び値を消しつつなめらかにできる
フィルタを1つ通すだけで、しきい値判定がバタつかなくなり、グラフもぐっと見やすくなります。まずはダミーデータでαやnを変えて、線の動きがどう変わるか試してみてください🚀
こちらも読まれています
📡 Arduinoをもっと深く学ぼう!
Arduino・ラズパイ・ロボットプログラミングを体系的に学びたい方へ。おすすめのUdemyコースや電子部品もまとめています。
Arduinoロボット入門のおすすめコース・部品を見る →📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。





