How LocationOutlierFilter Removes GPS Teleport Spikes in Google Timeline Visualizer
The LocationOutlierFilter eliminates impossible "teleport" jumps by detecting short-duration excursions exceeding 1300 km/h surrounded by distant points, then discarding all points in those suspicious runs.
The LocationOutlierFilter in google-timeline-visualizer is a critical preprocessing component that cleans raw Google Location History exports before visualization. GPS receivers occasionally produce spurious coordinates—often due to satellite handoff errors or signal reflection—that make a device appear to jump thousands of kilometers in minutes. This filter applies a multi-heuristic sliding window algorithm to identify and remove these artifacts while preserving legitimate travel data.
Core Heuristics Behind the Filter
The filter operates on five simultaneous constraints that together characterize a teleport spike:
- Speed threshold — Inbound and outbound segments must exceed 1300 km/h, far beyond any realistic ground transportation
- Ingress/egress distance — Entry and exit jumps must each span ≥500 km
- Surrounding proximity — The points before and after the excursion must lie ≤200 km apart
- Temporal bounds — The complete window (before point to after point) must fit within 12 hours
- Internal coherence — Points inside the excursion stay ≤200 km from each other but ≥500 km from surrounding points
These constraints are evaluated in visualizer.py by the _is_suspicious_excursion function (lines 886-912).
Step-by-Step Algorithm Implementation
1. Speed Calculation Between Points
The _speed_km_per_hour helper computes velocity using haversine distance and time deltas. Zero time delta yields infinite speed, immediately flagging duplicate timestamps.
# From visualizer.py, lines 778-783
def _speed_km_per_hour(lat1, lon1, lat2, lon2, timedelta_seconds):
if timedelta_seconds <= 0:
return float('inf')
km = haversine_km(lat1, lon1, lat2, lon2)
hours = timedelta_seconds / 3600.0
return km / hours
2. Excursion Validation
_is_suspicious_excursion applies all five heuristics to a candidate window. It receives the full points list plus start and end indices, then examines the context before start and after end.
# Pseudocode based on lines 886-912
def _is_suspicious_excursion(points, start, end):
before = points[start - 1]
after = points[end + 1]
first = points[start]
last = points[end]
# Temporal constraint: full window ≤ 12 hours
if (after["dt"] - before["dt"]).total_seconds() > 12 * 3600:
return False
# Surrounding points must be close (≤200 km)
if haversine_km(before, after) > 200:
return False
# Ingress and egress must be far jumps (≥500 km)
if haversine_km(before, first) < 500:
return False
if haversine_km(last, after) < 500:
return False
# Speed checks for both legs
if _speed_km_per_hour(before, first) <= 1300:
return False
if _speed_km_per_hour(last, after) <= 1300:
return False
# Internal coherence: all internal points ≤200 km from first
# and ≥500 km from before/after
for i in range(start, end + 1):
if haversine_km(points[i], first) > 200:
return False
if haversine_km(points[i], before) < 500:
return False
if haversine_km(points[i], after) < 500:
return False
return True
3. Run Detection and Removal
The main entry point filter_location_outliers (lines 325-344) iterates through points, uses _suspicious_run_end to find the maximal valid window starting at each position, and skips flagged ranges.
from visualizer import filter_location_outliers
from datetime import datetime
raw_points = [
{"dt": datetime(2023, 5, 1, 8, 0), "lat": 37.5, "lon": 127.0}, # Seoul
{"dt": datetime(2023, 5, 1, 8, 10), "lat": 51.5, "lon": -0.1}, # London (impossible!)
{"dt": datetime(2023, 5, 1, 8, 20), "lat": 37.51, "lon": 127.01}, # Seoul
]
filtered, removed_count = filter_location_outliers(raw_points, mode="conservative")
print(f"Kept {len(filtered)}, removed {removed_count}") # → Kept 2, removed 1
4. Optional Filter Disabling
Set mode="off" to bypass filtering entirely—useful for debugging or when preserving raw data is required.
filtered_all, removed_all = filter_location_outliers(raw_points, mode="off")
print(f"{len(filtered_all)} points, {removed_all} removed") # → 3 points, 0 removed
Why These Heuristics Work
| Heuristic | Purpose | Failure Mode Prevented |
|---|---|---|
| 1300 km/h speed limit | Captures any supersonic jumps | Misses slow drifts (acceptable tradeoff) |
| ≥500 km ingress/egress | Ensures "teleport" pattern, not local noise | Removes legitimate airport transits* |
| ≤200 km surrounding gap | Confirms return to original region | Allows detours to nearby cities |
| ≤12 hour window | Distinguishes spikes from flights | Preserves actual air travel |
| Internal ≤200 km coherence | Groups clustered bad points | Handles multi-point GPS glitches |
*The "conservative" mode may filter actual flights under 1300 km/h; users can disable filtering if needed.
Test Coverage in the Repository
The implementation is validated in tests/test_outlier_filter.py:
test_isolated_gps_spike_is_removed— Single point 8000 km away in 10 minutes is discardedtest_multi_point_excursion_cluster_is_removed— Consecutive outlier points forming a cluster are both removedtest_filter_mode_off_preserves_all_points—mode="off"returns input unchanged
Summary
- LocationOutlierFilter uses a sliding-window multi-heuristic approach in
visualizer.pyto detect GPS teleport spikes - The 1300 km/h speed threshold combined with spatial proximity constraints (≤200 km surrounding, ≥500 km ingress/egress) reliably identifies impossible jumps
- Suspicious runs are fully discarded rather than interpolated, preventing contamination of visualization data
- The filter supports conservative and off modes for flexibility
- All core logic resides in
visualizer.py(lines 325-344, 778-783, 886-922) with comprehensive tests intests/test_outlier_filter.py
Frequently Asked Questions
What GPS error patterns does the LocationOutlierFilter target?
The filter specifically targets coordinate reflection errors and satellite handoff glitches that produce sudden coordinate jumps followed by rapid return to the true location. These appear as short-duration excursions to distant coordinates with immediate return—physically impossible without supersonic travel.
Why 1300 km/h as the speed threshold?
1300 km/h exceeds all commercial airliners (typically 800-950 km/h cruise) while remaining below actual signal-propagation artifacts. This threshold catches obvious errors without filtering legitimate high-speed rail (320 km/h max) or supersonic flights, which are rare in consumer location history.
Can the filter remove legitimate travel data?
Yes, in conservative mode the filter may flag actual flights if they happen to satisfy the excursion pattern—particularly short-haul flights under 1300 km/h with nearby departure/arrival points. Users should use mode="off" when preserving all data is critical, or post-process the removed count to audit filtering decisions.
How does the filter handle multi-point glitch clusters?
The _suspicious_run_end function (lines 915-922) extends the candidate window up to three points backward, allowing consecutive bad points that cluster together to be identified and removed as a single unit.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →