「センサーの値がピクピク揺れて、しきい値の判定がうまくいかない…」「たまに一瞬だけ、ありえない値が出るんです」

電子工作でセンサーを使い始めると、ほぼ全員がこの壁にぶつかりますよね。温度は少しずつしか変わらないはずなのに、表示は25.3→25.9→25.1…とバラバラ。ファンの自動ON/OFFや通知に使うと、ON/OFFを何度も繰り返してしまいます。

この記事では、Pythonで移動平均・中央値(メディアン)・指数移動平均(EMA)の3つのノイズ除去フィルタを作って、同じデータで性能を比べます。難易度はPython入門〜初級。使うのは標準ライブラリとmatplotlibだけなので、センサーが手元になくてもPCだけで試せます😊

最後に、飛び値にも強くて反応も速い「2段フィルタ」をクラスにまとめるところまで、一緒に見ていきましょう!

センサーのノイズには2種類ある

sensor signal noise oscilloscope
sensor signal noise oscilloscope / Photo by Alexander Dummer via Pexels

フィルタを選ぶ前に、ノイズの「種類」を知っておくのが大事です。

  • 細かいノイズ:本当の値のまわりで±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} ℃")
実行結果:3つのフィルタの値と平均誤差
実行結果:3つのフィルタの値と平均誤差

実行すると、飛び値が入った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()
実行結果:3つのノイズ除去フィルタの比較グラフ
実行結果:3つのノイズ除去フィルタの比較グラフ

グラフを見ると、次のことがわかります。

  • 移動平均(青)は、飛び値のあと台形のように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)} 番目")
実行結果:2段フィルタのαによる反応の違い
実行結果:2段フィルタのαによる反応の違い

読み取れるポイントです👇

  • 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ロボット入門のおすすめコース・部品を見る →

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
Arduinoをはじめよう 第4版 (Make: PROJECTS)

もしも

Arduinoをはじめよう 第4版 (Make: PROJECTS)

Arduino公式推薦の定番入門書

Amazonで見る
徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。