Yumi's Blog

Extract GPS data from Suunto's MovesCount

I have been using Suunto Ambit 2 as the device for recording any sports activities e.g. running, trekking, or swimming. Recorded activities can be updated in the cloud Suunto Movescount, and see the summary of activities, e.g., speed, pace, elevation gain or elevation loss. Suunto Movescount is a growing sports community where you can create your own sports diary to collect and share your activities as well as customize your compatible Suunto watch.

Sometimes, when I do not have a watch with me, I use my iphone's MovesCount app to record the activities as alternative. Unfortunatelly, With the iphone and watch the recording performance varies substantially:
When there is no signal, the recording performances of iphone's MovesCount app, especially the net elevation gain and loss, are just awefully unreliable.

In this post, I attempt to clean the GPS data from the iphone's MovesCount app, and make the performance conparable to the watch.

First, let's look at the GPS data from the two devices.

I recorded my hiking activity in Nepal using the two devices: Mountaineering move with Suunto Ambit 2 watch and Trekking move with Phone.

I downloaded the GPS data from my movescount page.

  • Click Tools -> Exprot as KML.
In [1]:
from IPython.display import Image
Image("kml_photo.png",width='100%',height=100)
Out[1]:

The exported kml files are saved in the two locations.

In [2]:
dir_move = "./data/"
path_phone_move_kml = dir_move + '/phone/Move_2018_05_01_06_52_33_Trekking.kml' 
path_watch_move_kml = dir_move + '/Move_2018_05_01_06_51_39_Mountaineering_Lobche_Gorekshep_EBC.kml' 

Extract the GPS coordinate infomation and the altitude infomation from the kml file, and save it into a panda dataframe.

In [3]:
import os 
import numpy as np
import fastkml
import pandas as pd 
import matplotlib.pyplot as plt
In [4]:
def get_coordinates(path_kml):
    '''
    extract lat, lon, altitude info from the kml data
    '''
    doc = open(path_kml, 'rb+').read()
    k = fastkml.KML()
    k.from_string(doc)
    kmlstr = k.to_string()
    coordstr = kmlstr.split("")[1].split("")[0]
    coordlst = coordstr.split(" ")
    l = []
    for point in coordlst:
        pointlst = point.split(",")
        l.append(pointlst)
    df = pd.DataFrame(l,columns = ["lon","lat","altitude"])
    for col in df.columns:
        df[col] = pd.to_numeric(df[col], errors='coerce')
    return(df)



df_phone = get_coordinates(path_phone_move_kml)
df_watch = get_coordinates(path_watch_move_kml)

## In this activity, the first 230 samples from the watch and the 14500 samples from the phone roughtly agree
df_watch = df_watch.iloc[1:230,:]
df_phone = df_phone.iloc[:14500,:]

print(df_phone.shape,df_watch.shape)
df_phone[:5]
No geometries found
Object does not have a geometry
No geometries found
Object does not have a geometry
(14500, 3) (229, 3)
Out[4]:
lon lat altitude
0 86.810962 27.948544 4923.200195
1 86.810972 27.948542 4923.291324
2 86.810975 27.948541 4925.231034
3 86.810979 27.948542 4927.935645
4 86.810985 27.948542 4927.390356

Let's plot the GPS coordinate infos from the two devices using gmplot

The latitude and longitude pairs seem to agree between the records from the two devies.

In [5]:
from gmplot import gmplot

filenm = "my_map.html"
# Place map
gmap = gmplot.GoogleMapPlotter(27.971815, 86.828963, 14)

gmap.plot(df_phone["lat"], df_phone["lon"], 'cornflowerblue', edge_width=2)
gmap.plot(df_watch["lat"], df_watch["lon"], '#3B0B39', edge_width=2)

# Draw
gmap.draw(filenm)
In [6]:
import sys
sys.path.append("../../trip/")
## my google api key
from personal import MY_API_KEY 

def jupyter_display(gmplot_filename, google_api_key):
    """
    Hack to display a gmplot map in Jupyter
    reference
    https://github.com/vgm64/gmplot/issues/16
    """
    with open(gmplot_filename, "r+") as f:
        f_string = f.read()
        url_pattern = "https://maps.googleapis.com/maps/api/js?libraries=visualization&sensor;=true_or_false"
        f_string = f_string.replace(url_pattern, url_pattern + "&key;=%s" % google_api_key)
        f.write(f_string)
    
jupyter_display(filenm, MY_API_KEY)
Image("my_map.png",width='100%',height=100)
## from IPython.display import IFrame
## IFrame(filenm, width=990, height=800)
Out[6]:

We see that the elevation gain and loss infomation are substantially different! I know that the elvation gain of this trail around 5200m elevation was never 1535m (I know I cannot finish the trail otherwise!), and the recording from the watch seems more right!

In [7]:
from math import sin, cos, sqrt, atan2, radians

def get_distance_btw2pts(lat1,lon1,lat2,lon2):
    '''
    approximate radius of earth in km
    https://stackoverflow.com/questions/19412462/getting-distance-between-two-points-based-on-latitude-longitude 
    '''

    lat1 = radians(lat1)
    lon1 = radians(lon1)
    lat2 = radians(lat2)
    lon2 = radians(lon2)

    R = 6373.0
    dlon = lon2 - lon1
    dlat = lat2 - lat1

    a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))

    distance = R * c
    return(distance)

def get_distance(df_phone):
    count = 1
    df_phone["dist"] = np.NaN
    for lat1, lat2, lon1, lon2 in zip(df_phone["lat"][:-1],df_phone["lat"][1:],
                                      df_phone["lon"][:-1],df_phone["lon"][1:]):
        dis = get_distance_btw2pts(lat1,lon1,lat2,lon2)
        df_phone["dist"].iloc[count] = dis
        count += 1
    print("  distance = {:3.1f}km".format(np.sum(df_phone["dist"])))
    return(df_phone)

def get_alt_change(alt):
    diff_alt = alt[1:] - alt[:-1]
    gain = np.sum(diff_alt[diff_alt > 0])
    loss = np.sum(diff_alt[diff_alt < 0])
    print("  Elevation gain {:3.0f}m, Elevation loss {:3.0f}m".format(gain,loss))
    print("  N samples = {:3d}".format(len(alt)))
    return gain, loss

print("watch")
get_distance(df_watch)
get_alt_change(df_watch["altitude"].values) 


print("phone")
get_distance(df_phone)
get_alt_change(df_phone["altitude"].values)
print("-------------")
watch
  distance = 7.6km
  Elevation gain 471m, Elevation loss -162m
  N samples = 229
phone
  distance = 8.7km
  Elevation gain 1535m, Elevation loss -1197m
  N samples = 14500
-------------

Cleaning the GPS records from iphone

To clean the GPS records from iphone, let's learn about the recording frequency of the difference devices:

Recording frequency

Devise Move Type Recording frequency
Phone Trekking Every 1 second
Suunto Ambit 2 watch Trekking Every 10 seconds
Suunto Ambit 2 watch Mountaineering Every 10 seconds
Suunto Ambit 2 watch Running Every 1 second

Despite that the iphone's recording performance is not so great, the recording frequency of the Trekking activity using iphone (every 1 second) is higher than Trekking or the Mountaineering of Suunto Ambit 2 watch (every 10 seconds). This suggests that we can take the rolling average of every 10 samples to clean the latitudes, longitudes and altitudes infomation from the iphone record.

GPS data cleaning

By taking the rolling average, the discripancies between the watch and phone records become somewhat more similar (yet they still differ).

In [8]:
def get_rolling_average(vec, k=5):
    rvec = np.zeros(len(vec))
    for t in range(len(vec)):
        start = np.max([t - k,0])
        end   = np.min([t + k,len(vec)]) 
        rvec[t] = np.mean(vec[start:end])
    return(rvec)

for col in df_phone.columns:
    df_phone[col] = get_rolling_average(df_phone[col].values, k=5)
    
print("watch")
get_distance(df_watch)
get_alt_change(df_watch["altitude"].values) 


print("phone")
get_distance(df_phone)
get_alt_change(df_phone["altitude"].values)
print("-------------")
watch
  distance = 7.6km
  Elevation gain 471m, Elevation loss -162m
  N samples = 229
phone
  distance = 8.0km
  Elevation gain 643m, Elevation loss -308m
  N samples = 14500
-------------

Comments