Running on data

(Zet je je eerste stappen in Python en wil je met data aan de slag, lees dan verder. Onderstaande gaat over het verrijken van een bestand met gegevens uit een ander bestand)

We hebben met het werk momenteel een project lopen waarin we (een vijftal mensen van ons bedrijf en zo’n vijftien gasten (relaties, oud-collega’s, studenten)) aan de slag gaan met onze hardloopdata. Je leest er hier meer over.

Er komt natuurlijk een hoop bij kijken: inrichten van het platform in Azure, voorbeeldanalyses klaarzetten waar de deelnemers op voort kunnen borduren, coronaproof locaties regelen voor de hackatons, T-shirts regelen, sprekers regelen, een alternatief parcours vinden nu de marathon van Eindhoven is afgelast.

Daarnaast is het aan elke deelnemer om zijn eigen hardloopgegevens uit Strava op te halen en te uploaden naar Azure. Bij mij kwam daar het volgende bij: de belangrijkste info (afstand, beweegtijd, hoogtemeters, datum & tijd, locatie) heb ik in Strava staan, maar niet de hartslag. En we doen best veel met hartslag.

Andere deelnemers hebben deze wel in Strava zitten, geüpload via bijvoorbeeld hun Garmin. Mijn Fitbit registreert keurig de hartslag, maar na een aantal trainingen die van Fitbit naar Strava gesynchroniseerd werden bleek dat Fitbit de afstand veel te ruim registreert (wellicht omdat die om mijn pols zit en ik, conform de adviezen van de trainer bij de Running On Data-clinic, een pittige arminzet heb). Dit maakte de gegevens slecht bruikbaar: omdat de tijd wel goed, maar de afstand veel te ruim werd gemeten, kwamen er wel erg indrukwekkende snelheden uit. De Strava-app op mijn telefoon is een stuk accurater. Maar: die meet geen hartslag.

Dus kwam in mij op om de Strava-gegevens te verrijken met de Fitbit-hartslagdata. Je zou ze kunnen combineren op basis van datum & tijd en het leek me handig dat in Python te doen. Omdat ik dat in mijn dagelijkse werk niet gebruik ben ik er nog niet enorm vaardig in, maar het internet is je vriend. Vrijwel alles wat je wilt doen is inmiddels al wel eens door iemand anders gedaan en elke foutmelding die je krijgt hebben tig anderen ook gehad. Vooral op StackOverflow is heel veel informatie te vinden.

Hieronder de stappen die nodig waren om e.e.a. bruikbaar te krijgen:

Fitbit registreert elke dag, 24 uur per dag, en we zijn nu enkel geïnteresseerd in de hartslag tijdens sportactiviteiten. Je wil dus niet alles ophalen, dat is veel teveel. Eerste stap is dus om uit het Strava-bestand een lijstje van dagen op te halen waarop er gerend of gefietst is (omdat veel deelnemers meerdere sporten combineren zijn we ook geïnteresseerd in andere dan hardloopactiviteiten). Regelmatig zijn er meerdere activiteiten op een dag (op de fiets naar de hardlooptraining), dus we moeten er ook even een lijstje met unieke dagen van maken.

(Nog verfijnder is om alleen gegevens op te halen voor de tijdvakken waarin je aan het sporten was, maar omdat ik -zie verder- al een scriptje in gebruik had dat de hartslag per dag ophaalt, was dat de snelste weg.)

Als je met data aan de slag wil is pandas handig: je stopt je gegevens dan in een dataframe dat je eenvoudig kunt uitlezen en manipuleren.

import pandas as pd
import fitbit
import gather_keys_oauth2 as Oauth2
import datetime

#activiteitdetails inlezen
strava_df = pd.read_csv("D:/hardlopen/hackaton/stravagegevens.csv", encoding='latin1')
#converteren naar datetime
strava_df = strava_df.assign(Dag = pd.to_datetime(strava_df.Datum))
#tijdstippen afronden op dagen om zo de fitbitgegevens te kunnen selecteren
strava_df = strava_df.assign(Dagen = strava_df['Dag'].dt.floor("D"))

#alleen de dagen selecteren, in het juiste datumformaat
dagen = pd.to_datetime(strava_df['Dagen'], format = '%Y-%m-%d')
#unieke dagen verzamelen
uniekedagen = set(dagen)
#lijst van maken
dagenlijstje = []
for i in uniekedagen:
    dagenlijstje.append(i)

Vervolgens haal je voor die dagen de hartslaggegevens op bij Fitbit. Op internet vond ik een scriptje dat de gegevens per dag ophaalt, en daarbij alleen een tijdstip wegschrijft. Ik wilde ze allemaal in één bestand hebben, dus moest zorgen dat ook de dag mee weggeschreven werd in de kolom ‘Time’, anders weet je niet meer bij welke dag ’14:00′ hoort en dan wordt het lastig matchen.

#verbinding maken met fitbit API
CLIENT_ID = 'CLIENT_ID'
CLIENT_SECRET = 'CLIENT_SECRET'
server = Oauth2.OAuth2Server(CLIENT_ID, CLIENT_SECRET)
server.browser_authorize()
ACCESS_TOKEN = str(server.fitbit.client.session.token['access_token'])
REFRESH_TOKEN = str(server.fitbit.client.session.token['refresh_token'])
auth2_client = fitbit.Fitbit(CLIENT_ID, CLIENT_SECRET, oauth2=True, access_token=ACCESS_TOKEN,refresh_token=REFRESH_TOKEN)

#haal voor elke datum de hartslaggegevens op
for i in dagenlijstje:
    fit_statsHR = auth2_client.intraday_time_series('activities/heart', base_date=i, detail_level='1min')
    time_list = []
    val_list = []
    for z in fit_statsHR['activities-heart-intraday']['dataset']:
        val_list.append(z['value'])
        time_list.append(z['time'])
    hartslag_df = pd.DataFrame({'Heart Rate': val_list, 'Time': time_list})
    # maak (voor het wegschrijven in het bestand) van de datum een string en neem alleen de eerste tien karakters
    datum = str(i)
    datum = datum[:10]
    #voeg de datum toe aan de kolom Time, om zo te kunnen zien bij welke dag de waarden horen
    hartslag_df['Time'] = datum + ' ' + hartslag_df['Time']
    #stop de gegevens van alle dagen samen (append) in een bestand
    hartslag_df.to_csv('D:/hardlopen/hackaton/hartslaggegevens.csv', columns=['Time', 'Heart Rate'],mode='a', header=False, index=False)
    print("Dat waren de gegevens van " + datum)

Nu hebben we twee bestanden: die met de Strava-gegevens en die met de Fitbit-gegevens. Om ze te kunnen joinen op datum & tijdstip, moeten die eerst in beide bestanden in hetzelfde formaat staan. Dat was even experimenteren, uiteindelijk is het gelukt met een tussenstap via string, en toen was ik er wel klaar mee en heb niet meer gekeken of het handiger kan.

Bovendien heeft Strava gegevens per seconde en is bij Fitbit ‘per minuut’ de kleinste optie. We ronden daarom de Strava-tijdstippen af op 1 minuut (we gaan daar dus straks telkens zestig keer dezelfde hartslag invullen).

#bestanden inlezen
strava_df = pd.read_csv("D:/hardlopen/hackaton/stravagegevens.csv", encoding='latin1')
hartslag_df = pd.read_csv("D:/hardlopen/hackaton/hartslaggegevens.csv", encoding='latin1')

#naar string
strava_df['Datum'] = strava_df['Datum'].astype(str)
hartslag_df['Time'] = hartslag_df['Time'].astype(str)
# naar datetime
strava_df['Datum'] = pd.to_datetime(strava_df['Datum'])
hartslag_df['Time'] = pd.to_datetime(hartslag_df['Time'])

#variant op hele minuten, om te kunnen combineren met de hartslaggegevens, die op hele minuten gaan
strava_df['Heleminuut'] = strava_df['Datum'].dt.floor("min")

Tenslotte kun je dan de merge uitvoeren en het verrijkte bestand wegschrijven. Voordat we dat laatste doen, halen we eerst de uit ‘Fitbit’ gebruikte kolommen weer weg. Ook voegt pandas blijkbaar bij mergen een unnamed kolom toe, die moet ook weg. Tenslotte schrijf je dan een bestand weg dat identiek is aan het oorspronkelijke bestand met Strava-gegevens, alleen dan met vulling in de kolom ‘Hartslag’.

#gegevens combineren
combi_df = pd.merge(strava_df, hartslag_df, how='left', on=None, left_on='Heleminuut', right_on='Time',
         left_index=False, right_index=False, sort=True,
         suffixes=('_x', '_y'), copy=True, indicator=False,
         validate=None)

#de verrijkte dataset aanpassen: Heart_Rate-gegevens zonder decimaal naar Hartslagkolom (die in Strava leeg was)
combi_df['Hartslag'] = round(combi_df['Heart Rate'])

#de extra kolommen weer verwijderen: die zijn nu niet meer nodig
del combi_df['Time']
del combi_df['Heart Rate']
del combi_df['Heleminuut']

#unnamed column aan het begin ook weghalen
combi_df = combi_df.loc[:, ~combi_df.columns.str.contains('^Unnamed')]
#de verrijkte dataset wegschrijven in een bestand
combi_df.to_csv("D:/hardlopen/hackaton/strava_verrijkt.csv", index=False)

Et voilà. Het was best wel even prutsen om dit de eerste keer werkend te krijgen. Maar daarna heb ik er steeds opnieuw gebruik van kunnen maken: bij elke nieuwe hackaton willen we natuurlijk een verse set hebben waarin ook onze meest recente sportactiviteiten zitten. En ondergetekende moet dan telkens opnieuw die hartslag erbij halen. Dat gaat nu, hoera, in een handomdraai.