・約7分で読めます

GPSの最高速度は外れ値1点で決まり得る — 直近3点の中央値に変えて揃えた3つのこと


PRXサーバー国内シェアNo.1のレンタルサーバー。高速・安定・サポート充実で初心者にもおすすめ。
Xサーバー 公式サイト →

GPSログの「最高速度」を最大値で取ると、跳ねた1点だけで値が決まり得ます。個人開発しているツーリング記録アプリの統計画面を見直していて、最高速度がそういう作りになっていると分かりました。

直したのは判定の数行ですが、揃えないと別の数字が壊れる箇所が3つありました。GPSのログから「最高○○」を出す実装をする人向けに、チェックリストとしてまとめます。

実装はAIに任せていて、筆者がしたのは実際に走って画面を見て、確認に答えたところまでです。アプリの開発経緯は開発記の第4話に、同じアプリで走行距離の数字が2つの画面で食い違った話はこちらの記事に書いています。

発端:86km/hが、どの1点から来たのかを示せなかった

125ccのバイクで走った記録を統計画面で見ると、最高速度が全期間で86km/h、今月で75km/hと表示されていました。

ツーリング記録アプリの統計画面(全期間)。総走行距離1,022.5km、ツーリング回数16回などと並んで、最高速度86km/hと表示されている(2026年10月4日撮影・地名が写る部分は切り取り済み)

筆者は、この86km/hは実際に出た速度だと考えていて、表示がおかしいとも思っていませんでした。問題は数字そのものではなく、出し方のほうでした。算出ロジックを確かめると、1点だけ跳ねた値でも、そのまま最高速度になる作りだったのです。

原因:最大値は、外れ値にいちばん弱い集計だった

アプリは端末のGPSが返す速度(秒速メートル)を、記録点ごとに保存しています。修正前の最高速度は、その中の最大値でした。200km/hを超える値は明らかな異常として捨てていましたが、それ以下の値は1点だけでもそのまま最高速度の候補になる作りでした。

集計の性質で考えると、これは危うい作りです。

集計 1点だけ値が跳ねたとき
平均 他の点に薄められ、影響は点数に応じて小さくなる
中央値 ほぼ影響を受けない
最大値 跳ねた1点がそのまま結果になる

最大値は「一番大きいノイズ」を拾い得ます。GPSの速度は測位の条件によって一瞬だけ跳ねることがあり、200km/hの上限だけでは、上限より下の跳ねを止められません。

ただし、86km/hになった記録の生の点列は確認していません。86km/hが本当に出た速度なのか、1点だけの跳ねだったのかは分からず、言えるのは、1点の跳ねを最高速度として拾い得る作りだった、というところまでです。

対策:直近3つの有効速度の中央値を「最高速度の候補」にする

採ったのは、信号処理でよく使われるメディアンフィルタの考え方です。SciPyのscipy.signal.medfiltは既定の窓幅が3で、MathWorksのmedfilt1も既定の次数は3です。特別な手法ではありません。

ルールは次のとおりです。

  • 記録点を順に読み、有効な速度だけを窓に入れる
  • 窓には直近3つの有効速度を持つ
  • 3つ揃ったら、その中央値を最高速度の候補にする
  • 候補の最大値を最高速度とする

有効な速度の条件は3つにしました。値がある(nullでない)こと、負でないこと、200km/h以下であることです。条件を満たさない点は、窓に入れずに読み飛ばします。

根拠は端末側の仕様です。AppleのCLLocation.speedは、速度を測れなかったときに負の値を返し、その値は無効として扱うよう説明されています。ただ、このアプリが使う位置情報プラグイン(@capacitor-community/background-geolocation)は、iOSの負の値も、Androidで速度を持たない位置も、nullにしてアプリへ渡しています。実際に効いているのはnullを読み飛ばす処理で、負の値を弾く条件は保険です。

窓幅3の中央値がどう効くかを、記号で示します(aは普段の速度、Xは跳ねた値)。

直近3点の並び 中央値 結果
a, X, a a 1点だけの跳ねは落ちる
a, X, X X 2点続いた速度は残る
X, X, X X 続いた速度は当然残る

落とし穴1:記録の再開で窓を切る

このアプリには、記録を一時停止して再開する機能があり、再開した最初の点には sessionStart という印が付いています。

窓をこの印で切らないと、中断前の最後の2点と再開後の最初の1点を混ぜて中央値を取ることになります。時間も場所も離れた点を連続した3点として扱うのは、中央値の前提として成り立ちません。

そこで、sessionStart の点に来たら窓を空にしてから、その点の速度を入れるようにしました。距離の計算は以前から、記録の再開をまたぐ区間を数えない作りです。最高速度もそれと同じ区切り方に揃えました。

落とし穴2:最高だけ中央値にすると、平均と逆転し得る

ここはAIの実装をレビューした別のAIに指摘された点です。

記録の詳細画面には速度グラフがあり、「最高」と「平均」が並びます。最高だけを中央値の系列で出し、平均を生の値のまま計算すると、跳ねがある記録では跳ねの分だけ平均が持ち上がり、中央値で削られた最高を上回ることがあります。レビューでは「最高 40 / 平均 49」のような表示が起こり得ると例示されました。これは指摘の中で挙げられた想定例で、実際の記録で見た数字ではありません。

そこで、グラフの「最高」、最高地点のマーカー、「平均」の3つを、同じ中央値の系列から出すように揃えました。

一方、グラフの線と縦軸のスケールは生の値のままです。線まで中央値にすると、GPSが実際に返した値がグラフから消えてしまうからです。跳ねは線の上でそのまま見え、数字は跳ねを除いた系列で出す。表示の役割をそう分けました。

検証と、残ったトレードオフ

記録中の最高速度は、点が届くたびに足していく増分計算で更新しています。一方、記録の再開時などには、それまでの点列を最初から一括で計算し直します。この2つが別の答えを出すと、再開した瞬間に最高速度が変わってしまいます。

そこで、窓の状態(直近の有効速度、最大2件)を集計結果と一緒に持たせました。窓を進める処理は1つの関数にまとめ、増分計算・一括計算・グラフ表示のすべてでそれを通しています。

検証は次の3つです。

  • ランダムに作った点列5000通りで、増分計算と一括計算の結果が一致することを確認しました
  • テストは1615件すべて通過しました
  • 判定を修正前の挙動に戻すと、新しく足したテストが落ちることを確認しました。テストが今回の変更を本当に見張っているかの確認です

テストをCIで回す仕組みはGitHub Actions CI/CD入門で、実装をAIに任せてレビューも別のAIに回す体制はClaude Code活用術で紹介しています。

一方で、今回の修正で解決していないこと、新しく生まれた制約もあります。

項目 内容
保存済みの値 書き換えていません。統計画面の86km/hは残ります。新しいルールは修正後に記録した分から効きます
旧記録の表示 保存済みの記録では、詳細画面の「最高速度」(保存値)と、グラフの「最高」(その場で再計算した値)が食い違って並ぶことがあります
短い記録 有効な速度が2点以下の記録では、中央値の候補ができないため最高速度が出ません
本物のピーク 窓幅3では、1点しか続かなかった本物の最高速度も跳ねと区別できず落ちます
平均速度の表示 保存される最高速度が下がると、平均速度の妥当性チェックの上限も下がります。平均速度が表示されない記録が増える可能性があります(要検証)

本物のピークを落とす点は、窓幅を選んだ時点で受け入れた割り切りです。より筋の良い方法として、点ごとに速度の確からしさを見て除く案があります。Appleには速度の精度を表すCLLocation.speedAccuracyがあり、これを使えば中央値でまとめて丸めずに、怪しい点だけを外せる可能性があります。ただ、プラグインがこの値をアプリまで渡しているか、Android側に同じ扱いができる値があるかは未確認です。今後の課題です。

今回の修正は、最高速度を正確にするものではありません。1点の跳ねを最高速度にしない、というところまでです。この記事を書いた2026年10月上旬の時点では修正後の版で走った記録がなく、実際の表示がどう変わるかは確かめていません。

GPSログで最大値を出す前のチェックリスト

次に同じ実装をするときのために、今回の作業をチェックリストにしておきます。

  • 最大値は外れ値1点で決まる集計だと理解しているか。上限値だけで弾かず、「有効な速度」の条件(null・負の値・上限超)を決めて無効な点を読み飛ばしているか。使っているライブラリが無効値をどう渡すかを確認したか
  • 記録の中断・再開で窓を切っているか。距離など他の集計と同じ区切り方になっているか
  • 同じ画面に並ぶ最高・平均・マーカーを同じ系列から出しているか。生の値を残す表示(グラフの線など)と分けているか
  • 増分計算と一括計算が同じ結果を返すか。窓の状態を持たせ、ランダムな点列で突き合わせたか
  • 保存済みの値を書き換えるか残すかを決め、旧記録で表示が食い違うことを把握しているか

最大値を出す処理は短く書けます。でもユーザーは、その数字を自分の走りの記録として受け取ります。外れ値をどう扱ったかを説明できるところまでが、最高速度の実装だと考えています。