What does this notebook do?
import pandas as pd
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import datetime
from datetime import date
from garminconnect import (
Garmin,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
GarminConnectAuthenticationError,
)
# Read in CSV file
df = pd.read_csv('export.csv')
# Remove "time zone offset" from "occurred_at" column and add new "occurred_at_day" column
df['occurred_at_day'] = df['occurred_at'].apply(lambda x: x[:len(x) - 15])
df['occurred_at'] = df['occurred_at'].apply(lambda x: x[:len(x) - 6])
df.head()
class | value | time | length | photo_url | description | occurred_at | body | updated_at | started_at | ended_at | created_by | occurred_at_day | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | GlucoseMeasurement | 100.0 | NaN | NaN | NaN | NaN | 2021-08-15 23:48:06 | NaN | NaN | NaN | NaN | NaN | 2021-08-15 |
1 | GlucoseMeasurement | 99.0 | NaN | NaN | NaN | NaN | 2021-08-15 23:33:06 | NaN | NaN | NaN | NaN | NaN | 2021-08-15 |
2 | GlucoseMeasurement | 99.0 | NaN | NaN | NaN | NaN | 2021-08-15 23:18:06 | NaN | NaN | NaN | NaN | NaN | 2021-08-15 |
3 | GlucoseMeasurement | 98.0 | NaN | NaN | NaN | NaN | 2021-08-15 23:03:06 | NaN | NaN | NaN | NaN | NaN | 2021-08-15 |
4 | GlucoseMeasurement | 97.0 | NaN | NaN | NaN | NaN | 2021-08-15 22:48:06 | NaN | NaN | NaN | NaN | NaN | 2021-08-15 |
# Print all days with data
daysWithData = df['occurred_at_day'].unique()
print(daysWithData)
['2021-08-15' '2021-08-14' '2021-08-13' '2021-08-12' '2021-08-11' '2021-08-10' '2021-08-09' '2021-08-08' '2021-08-07' '2021-08-06' '2021-08-05']
# Filter down to one day, pick the second day in the dataset
df = df[df['occurred_at_day']==daysWithData[2]]
day = daysWithData[2]
# Create a datasets just with glucose measurments
gm = df[df['class']=='GlucoseMeasurement']
# Create a dataset for meals and exercise, sort it
mealsExercise = df[((df['class']=='Meal') | (df['class']=='ExerciseActivity') )]
mealsExerciseSorted = mealsExercise.sort_values(by=["occurred_at"], ascending=True)
# Get Garmin Data
# This may not be so great, defaulting to simply retrieving the last 100 activities on Garmin.
# If the day that is plotted is further in the past, this may not work.
numberOfActivities = 100
try:
# Initialize Garmin client with credentials
# Put your userID and password for https://connect.garmin.com/ here
client = Garmin("USERID", "PASSWORD")
# Login to Garmin Connect portal
client.login()
# Get running activities
allActivities = client.get_activities(0,numberOfActivities) # 0=start, numberOfActivities=limit
dayOfInterest = datetime.datetime.strptime(day, '%Y-%m-%d').date()
allDayStepData = client.get_steps_data(dayOfInterest.isoformat())
except (GarminConnectConnectionError, GarminConnectAuthenticationError, GarminConnectTooManyRequestsError,) as err:
print("Error occured during Garmin Connect Client init: %s" % err)
quit()
except Exception:
print("Unknown error occured during Garmin Connect Client init.")
# convert garmin data in list form to Pandas dataframe
dfGarmin = pd.DataFrame.from_dict(allDayStepData)
# manipulate start time so that it is local (And not GMT)
dfGarmin['time'] = dfGarmin['startGMT'].apply(lambda x: x[:len(x) - 5])
dfGarmin['time'] = dfGarmin['time'].apply(lambda x: x[11:])
offset = dfGarmin["time"][0]
offsetHour = int(offset.split(':')[0])
offsetMinutes=int(offset.split(':')[1])
dfGarmin['time'] = pd.to_datetime(dfGarmin['startGMT'])
dfGarmin['time'] = dfGarmin['time'].apply(lambda x: x - datetime.timedelta(hours=offsetHour, minutes=offsetMinutes))
dfGarmin
startGMT | endGMT | steps | primaryActivityLevel | activityLevelConstant | time | |
---|---|---|---|---|---|---|
0 | 2021-08-13T04:00:00.0 | 2021-08-13T04:15:00.0 | 0 | sedentary | True | 2021-08-13 00:00:00 |
1 | 2021-08-13T04:15:00.0 | 2021-08-13T04:30:00.0 | 0 | sedentary | True | 2021-08-13 00:15:00 |
2 | 2021-08-13T04:30:00.0 | 2021-08-13T04:45:00.0 | 0 | sedentary | True | 2021-08-13 00:30:00 |
3 | 2021-08-13T04:45:00.0 | 2021-08-13T05:00:00.0 | 0 | sedentary | True | 2021-08-13 00:45:00 |
4 | 2021-08-13T05:00:00.0 | 2021-08-13T05:15:00.0 | 0 | sedentary | True | 2021-08-13 01:00:00 |
... | ... | ... | ... | ... | ... | ... |
91 | 2021-08-14T02:45:00.0 | 2021-08-14T03:00:00.0 | 0 | sedentary | True | 2021-08-13 22:45:00 |
92 | 2021-08-14T03:00:00.0 | 2021-08-14T03:15:00.0 | 0 | sedentary | True | 2021-08-13 23:00:00 |
93 | 2021-08-14T03:15:00.0 | 2021-08-14T03:30:00.0 | 0 | sedentary | True | 2021-08-13 23:15:00 |
94 | 2021-08-14T03:30:00.0 | 2021-08-14T03:45:00.0 | 0 | sedentary | True | 2021-08-13 23:30:00 |
95 | 2021-08-14T03:45:00.0 | 2021-08-14T04:00:00.0 | 0 | sedentary | True | 2021-08-13 23:45:00 |
96 rows × 6 columns
# Just for exploring the data, lets look at all 15 minute segments that have non-zero steps
dfGarmin = dfGarmin[dfGarmin.steps != 0]
print(dfGarmin[['time', 'steps', 'primaryActivityLevel']])
#dfGarmin.head(n=20)
time steps primaryActivityLevel 25 2021-08-13 06:15:00 152 active 26 2021-08-13 06:30:00 139 active 27 2021-08-13 06:45:00 360 active 28 2021-08-13 07:00:00 155 sedentary 29 2021-08-13 07:15:00 71 sedentary 30 2021-08-13 07:30:00 9 sedentary 31 2021-08-13 07:45:00 420 sedentary 32 2021-08-13 08:00:00 21 sedentary 34 2021-08-13 08:30:00 554 sedentary 35 2021-08-13 08:45:00 128 sedentary 36 2021-08-13 09:00:00 9 sedentary 42 2021-08-13 10:30:00 240 sedentary 43 2021-08-13 10:45:00 1168 active 45 2021-08-13 11:15:00 6 sedentary 46 2021-08-13 11:30:00 30 sedentary 47 2021-08-13 11:45:00 1854 highlyActive 48 2021-08-13 12:00:00 2541 highlyActive 49 2021-08-13 12:15:00 661 active 50 2021-08-13 12:30:00 926 active 51 2021-08-13 12:45:00 34 sedentary 52 2021-08-13 13:00:00 72 sedentary 53 2021-08-13 13:15:00 6 sedentary 61 2021-08-13 15:15:00 870 active 62 2021-08-13 15:30:00 390 sedentary 63 2021-08-13 15:45:00 52 sedentary 68 2021-08-13 17:00:00 140 sedentary 69 2021-08-13 17:15:00 220 sedentary 70 2021-08-13 17:30:00 92 sedentary 71 2021-08-13 17:45:00 159 sedentary 72 2021-08-13 18:00:00 116 sedentary 73 2021-08-13 18:15:00 133 sedentary 74 2021-08-13 18:30:00 175 active 75 2021-08-13 18:45:00 74 sedentary 77 2021-08-13 19:15:00 22 sedentary 78 2021-08-13 19:30:00 16 sedentary 79 2021-08-13 19:45:00 7 sedentary 82 2021-08-13 20:30:00 68 sedentary 85 2021-08-13 21:15:00 78 sedentary 86 2021-08-13 21:30:00 280 active 88 2021-08-13 22:00:00 6 sedentary 89 2021-08-13 22:15:00 22 sedentary
# Create a dataset with just 2 columns
gm_data = gm.filter(['occurred_at', 'value'])
# rename the columns for easier readability
gm_data.columns = ['time', 'value']
# turn time column into the index and delete time column
gm_data['time']= pd.to_datetime(gm_data['time'])
gm_data.index = gm_data['time']
del gm_data['time']
gm_data = gm_data.resample('1T').mean() # add rows for every 1 minute
gm_data = gm_data.interpolate(method='cubic') # interpolate the new 1 minute points with data
# Calculate a few metrics
threshold = 120 # this is an arbitrary threshold
above = gm_data[gm_data['value'] > threshold] # create a dataset with glucose measuremnts over threshold
minutesAboveThreshold = above.count()
print('Number of minutes above '+str(threshold)+': '+ minutesAboveThreshold.to_string(index=False))
percentageAboveThreshold = int(round(minutesAboveThreshold/(60*24)*100,0))
print("Time above Threshold = "+str(percentageAboveThreshold)+"%")
averageGlucose = int(round(gm_data['value'].mean()))
medianGlucose = int(round(gm_data['value'].median()))
print("Average Glucose = "+str(averageGlucose))
print("Median Glucose = "+str(medianGlucose))
# Calculate statistics on the Garmin data
numberOfRunningActivitiesToday = 0
numberOfActivitiesToday = 0
for i in range(numberOfActivities):
activity = allActivities[i]
activityDateTime = activity['startTimeLocal']
activityDate = datetime.datetime.strptime(activityDateTime, "%Y-%m-%d %H:%M:%S")
if str(activityDate.date()) == day:
numberOfActivitiesToday = numberOfActivitiesToday + 1
if activity["activityType"]["typeKey"] == "running":
numberOfRunningActivitiesToday = numberOfRunningActivitiesToday + 1
print("Steps today = "+str(dfGarmin.steps.sum()))
print("Activities today = "+str(numberOfActivitiesToday))
print("Runs today = "+str(numberOfRunningActivitiesToday))
Number of minutes above 120: 74 Time above Threshold = 5% Average Glucose = 94 Median Glucose = 91 Steps today = 12476 Activities today = 1 Runs today = 1
# using subplots here to easily get a secondary y-axis
fig = make_subplots(specs=[[{"secondary_y": True}]])
# first add the glucose measurement data
fig.add_trace( go.Scatter(x=gm_data.index, y=gm_data.value, mode='lines',line=dict(color="purple")))
# add meals and exercise to the chart
yText = 145
eventColor = "green"
for index, row in mealsExerciseSorted.iterrows():
# If the activity has "run" in the description, don't use it as it is a duplicate from Garmin
if "run" in row['description']: continue
# Convert the time in pandas to something that we can use as an index for the x-axis placement
time = datetime.datetime.strptime(row['occurred_at'], '%Y-%m-%d %H:%M:%S')
# Pick a different color depending on the event
if (row['class'] == "Meal"): eventColor = "black"
else: eventColor = "green"
# draw a vertical line at the time of the meal/exercise
fig.add_shape(type="line", xref="x", yref="y", x0=time, y0=70, x1=time , y1=140, line_color=eventColor,)
# Alternate text placement so adjacent text doesn't overlap
if (yText == 145): yText = 153
else: yText = 145
# Add text
fig.add_annotation(text=row['description'], xref="x", yref="y", x=time, y=yText, showarrow=False, font=dict(color=eventColor))
# Add Garmin running activities
for i in range(numberOfActivities):
activity = allActivities[i]
# only activities that are of type "running"
if activity["activityType"]["typeKey"] == "running":
activityDateTime = activity['startTimeLocal']
activityDate = datetime.datetime.strptime(activityDateTime, "%Y-%m-%d %H:%M:%S")
if str(activityDate.date()) == day:
# draw a vertical line at the time of the running activity
fig.add_shape(type="line", xref="x", yref="y", x0=activityDateTime, y0=70, x1=activityDateTime , y1=140, line_color="green",)
# Add text... yes this is specific to kilometers. This may need changes for miles.
textDescr = str(activity['activityName']) + " " + str(int(round(activity['distance']/1000))) + "K run"
fig.add_annotation(text=textDescr, xref="x", yref="y", x=activityDateTime, y=145, showarrow=False, font=dict(color="green"))
# Draw a line at the threshold
fig.add_shape(type="line", xref="x", yref="y",
x0=gm_data.index[0], y0=threshold, x1=gm_data.index.max(), y1=threshold, line_color="red",)
# Show text box with summary values
fig.add_annotation(
text='Glucose Threshold = '+str(threshold)+
'<br>Minutes above Threshold = '+str(int(round(minutesAboveThreshold,0)))+
'<br>Time above Threshold = '+str(percentageAboveThreshold)+"%"+
'<br>Average Glucose = '+str(averageGlucose)+
'<br>Median Glucose = '+str(medianGlucose)+
'<br>Steps Today = '+str(dfGarmin.steps.sum()),
align='right', showarrow=False,
xref='paper', yref='paper', x=0.002, y=0.005,
bordercolor='black', borderwidth=1
)
# Setting primary and secondary y axis titles and ticks
fig.update_layout(yaxis = dict(range=[0, 160], tick0=0, dtick=20, title_text='mg/dL'),yaxis2=dict(tick0=0, dtick=500, range=[0,4000], title_text='steps'))
# Adding step data to the chart, using the secondary y axis
fig.add_trace( go.Bar(x=dfGarmin.time, y=dfGarmin.steps), secondary_y=True)
# Set x axis title
fig.update_xaxes(title_text=str(day), tickformat='%H:%M')
# Hide the legend
fig.update_layout(showlegend=False)
fig.show()