モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
data quality / outliers

GPSは、嘘をつく
測位の飛び・記録の断絶・集計の錯覚

前回描いた8,989点を、 今度は疑いながら見ていきます。 時速109kmで歩いている点が4つあり、その直前に101秒の空白がある。 さらに「11m格子の93%を複数回通過した」という集計は、数え方が作り出した錯覚でした。

GPS 外れ値 サンプリング 相関

[ § 0 ] 導入

描けてしまう、ということ

前回、8,989点を点群として描きました。軌跡はきれいに出ます。 おかしな点が混ざっていても、絵としては成立してしまうのです。

速度で色を分けたとき、経路の一部だけが不自然に赤くなっていました。 調べたら、そこは時速109kmで移動していることになっていた。 歩いて記録したデータです。

この記事では、そういう点を4種類ほど拾います。 いずれもデータが壊れているのではなく、そう記録されるのが正常な現象です。 壊れていないから、気づきにくい。

[ § 1 ] 測位の飛び

時速109kmで、歩いていた

累積距離の差分から速度を出すと、上位はこうなりました。

点番号時刻Δ距離速度
#34806:51:5130.4 m109.5 km/h
#34906:51:5230.2 m108.9 km/h
#35006:51:5330.2 m108.5 km/h
#35106:51:5429.6 m106.5 km/h
#306:44:265.9 m21.1 km/h
#452008:01:243.6 m13.1 km/h

4秒連続で、毎秒30mずつ進んでいます。合計120m。 走っても届きませんし、そもそも歩いていた記録です。

直前に、101秒の空白があった

生ログを前後まで広げると、事情が見えました。

点#345 〜 #355
#345  06:50:08   Δt=  1s  Δd=  1.0m     3.5 km/h
#346  06:50:09   Δt=  1s  Δd=  1.3m     4.6 km/h
#347  06:51:50   Δt=101s  Δd= 30.7m     1.1 km/h   ← 記録が飛んでいる
#348  06:51:51   Δt=  1s  Δd= 30.4m   109.5 km/h
#349  06:51:52   Δt=  1s  Δd= 30.2m   108.9 km/h
#350  06:51:53   Δt=  1s  Δd= 30.2m   108.5 km/h
#351  06:51:54   Δt=  1s  Δd= 29.6m   106.5 km/h
#352  06:51:55   Δt=  1s  Δd=  2.9m    10.4 km/h
#353  06:51:56   Δt=  1s  Δd=  1.3m     4.7 km/h
#354  06:51:57   Δt=  1s  Δd=  1.2m     4.1 km/h
101秒、記録が途切れていました。 #346 の 06:50:09 から #347 の 06:51:50 まで、点がありません。 信号を見失っていたのでしょう。そして復帰した直後、 4秒かけて位置を「取り戻して」いる飛びは、断絶の後始末でした

3秒を超える断絶は、11km の記録でこの1箇所だけです。 逆に言えば、1度の信号ロストが4点の異常値を生みました

速度は「記録された値」ではありません。 TCX に速度の欄はなく、累積距離を時間で割って算出しています。 距離も時刻も正しいのに、割り算の結果だけが現実離れする。 派生値は、元の値より壊れやすいということでもあります。
[ § 2 ] 除去の判断

どこから捨てるか

では外れ値をどう扱うか。閾値を決める必要がありますが、 「正常」との境目は連続しています

閾値除外される点評価
15 km/h 超5 点(0.056%)測位の飛びだけを狙える
10 km/h 超5 点同上
8 km/h 超数十 点速歩きも巻き込む
6 km/h 超数百 点普通の歩行が消える

歩行速度の95パーセンタイルは 6.7 km/h、最大でも 13.1 km/h でした。 一方で飛びは 106〜109 km/h。その間がぽっかり空いています。 だから 15 km/h という切り方で、飛びだけを外せました。

ただし、これは「たまたま空いていた」からです。 徒歩と自転車が混ざった記録なら、この分離はできません。 閾値が決まるかどうかは、データの性質に依存します。 「外れ値は除去する」という手続きだけを持ち出しても、 どこで切るかは毎回考え直す必要があります
[ § 3 ] 体感デモ

閾値を、動かしてみる

実データです。除外する速度の閾値を変えると、 残る点数と積算距離がどう変わるか見てください。

Interactive · outlier threshold
外れ値をどこで切るか
8,989点の実データ。閾値を超える点間は距離に加算しません。
速度の閾値 15.0 km/h
速度の分布(対数目盛・1点=1秒)
[ § 4 ] 集計の錯覚

93%が「複数回通過」に見えた

同じ道を何度通ったかを知りたくて、座標を格子に丸めて数えました。 緯度経度を小数第4位で切ると、約11m四方の格子になります。

素朴な集計
const grid = {};
pts.forEach(p => {
  const key = p.lat.toFixed(4) + ',' + p.lon.toFixed(4);
  grid[key] = (grid[key] || 0) + 1;
});
// 2点以上ある格子=「複数回通過した」とみなす

結果は、こうなりました。

指標
格子の総数約 1,230 マス
2点以上ある格子93 %

93%の格子を複数回通った——そんな経路ではありません。 始点と終点が5.8km離れた、ほぼ一本道の歩行です。

1マスに8秒いる

計算すれば、すぐ分かることでした。 歩行速度 4.9km/h は毎秒 1.36m。11m の格子を横切るのに 約8秒かかります。1Hz で記録しているので、 1マスに約8点が入るのが当たり前でした。

実際、8,989点 ÷ 約1,230マス = 約7.3点/マス。計算どおりです。 「複数回通過」ではなく、1回通過するのに複数点かかっているだけでした。

本当の再訪を数える

時間的に離れた訪問だけを数え直します。 同じ格子への点が60点(=60秒)以上空いて再登場したら、再訪とみなす。

数え方該当する格子
2点以上ある(素朴)93 %
時間的に離れた再訪約 2.5 %

93% が 2.5% になりました。 同じデータ、同じ格子です。変えたのは数え方だけ。

マス数が「約」なのは、丸め方で変わるからです。 小数第4位で丸めるか切り捨てるかによって、境界付近の点が隣のマスへ移り、 総数が1,227〜1,239の範囲で揺れます。 比率(93%、2.5%)はほとんど変わりませんが、 格子の集計は境界の扱いに敏感だという例でもあります。
格子を粗くしても解決しません。 110m 格子で試すと、複数点がある格子はほぼ100%になりました。 マスが大きいほど滞在時間も長くなるので、比率はむしろ上がります。 問題は解像度ではなく、「連続した滞在」と「別々の訪問」を区別していないことでした。
[ § 5 ] 閾値の恣意

「止まっていた時間」も、決まらない

前回、停止した場所が点の密集として光る話を書きました。 では合計何分止まっていたのか——これも一意には決まりません。

停止とみなす速度箇所数最長合計
0.3 km/h 未満740 秒2 分
0.5 km/h 未満740 秒2 分
1.0 km/h 未満20112 秒13 分
1.5 km/h 未満22117 秒15 分
2.0 km/h 未満2317 分17 分

0.5 から 1.0 へ動かすだけで、7箇所2分が20箇所13分に変わります。 6倍以上です。

理由は分かります。人は完全には止まらない。 信号待ちでも体は揺れ、GPSはそれを 0.5〜1.0 km/h として拾います。 「止まっている」と「ゆっくり動いている」の間に、明確な線はありません

だから数字を出すときは、閾値も一緒に書くべきでした。 「13分止まっていた」だけでは、読む側は検証できません。 「1km/h 未満を停止とみなすと13分」と書けば、 別の基準で数え直すこともできます。 §1 の外れ値も同じで、15km/h という切り方は自明ではありません
[ § 6 ] 予想外れ

坂で、心拍は跳ねなかった

標高データがあるので、勾配と心拍の関係を見たくなりました。 坂を上れば心拍が上がるはず——確かめるまでもない、と思っていました。

区間平均心拍点数
上り(勾配 > 2%)約 109 bpm約 1,700
平坦(−1% 〜 +1%)約 106 bpm約 4,600
下り(勾配 < −2%)約 105 bpm約 1,400

差は4 bpm ほど。方向は予想どおりですが、想像よりずっと小さい。 相関係数を計算すると r = 0.13(勾配の算出方法により 0.128〜0.132)でした。 ほぼ無相関です

考えられる理由

要因内容
心拍の遅れ坂を上り始めてから心拍が上がるまで数十秒かかる。同じ秒で対応づけると、ずれる
勾配が小さい標高差36.2m。都市部の歩行では負荷として弱い
標高の精度GPS由来の標高は誤差が大きく、1秒ごとの差分はノイズを多く含む
ペース調整坂では自然に歩を緩める。負荷が一定に保たれる
どれが主因かは、このデータでは分かりません。 心拍の遅れを補正して相関を取り直す、といった検証はしていません。 「相関が弱かった」という事実だけを記録します。 予想が外れたときに、理由を急いで作らない—— そのほうが後で見返せると思っています。
[ § 7 ] 精度

もうひとつの嘘 — float32

前回の実装で踏んだことも、 同じ種類の問題でした。

緯度経度を Float32Array で保持したところ、 積算距離が 10.99km から 11.57km に増えました。 float32 が緯度を表せる最小刻みは 0.425m。 歩行の1点間隔 1.2m に対して粗すぎたのです。

座標は正しい。時刻も正しい。それでも距離だけが5%ずれる。 記録が壊れているのではなく、扱い方で値が変わる—— この記事で見てきた現象と、構造は同じです。 測位の飛びも、格子の錯覚も、停止時間の揺れも、 データではなく、そこから何を導くかの問題でした。
[ § 8 ] まとめ

正常に記録された、おかしな値

ここで挙げたものは、どれも機器の故障ではありません。 信号が途切れれば復帰時に位置を補正しますし、 1Hzで記録すれば1マスに複数点が入ります。 人は完全には止まらないし、float32 の刻みは仕様どおりです。

正常に記録された値から、おかしな結論が出てくる。 だから「データが壊れていないか」ではなく、 「この数字はどう作られたか」を見る必要がありました。

この記事のまとめ

  • 時速109kmの点が4つ。直前に101秒の記録断絶があり、飛びはその後始末だった。
  • 速度は記録された値ではなく累積距離を時間で割った派生値。元の値より壊れやすい。
  • 外れ値と正常値の間が空いていたので 15km/h で切れた。それはたまたま
  • 「93%の格子を複数回通過」は錯覚。1マスに8秒いるので、8点入るのが当たり前だった。
  • 格子を粗くすると比率は上がる。解像度ではなく数え方の問題
  • 停止時間は閾値で2分にも13分にもなる。数字を出すなら閾値も書く。
  • 坂と心拍の相関は r ≒ 0.13。差は4bpmほど。予想は外れた。理由は急いで作らない。
  • float32 の刻み 0.425m が歩幅より粗く、距離が5%増えた。扱い方で値が変わる
GPS 可視化シリーズ
  1. 軌跡を、点群で描く
    WebGPU で 8,989点を描く
  2. GPSは、嘘をつく(この記事)
    測位の飛び・集計の錯覚
  3. 同じ人が、違う道を歩くと
    4ルートを並べて比べる

関連記事

『GPSは、嘘をつく — 測位の飛び・記録の断絶・集計の錯覚』を公開しました。