{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "**What does this notebook do?**\n", "- Load the exported CGM values from NutriSense\n", "- Print out what days are included in the dataset\n", "- Pair down data to only one day, include CGM values, meals and exercise\n", "- Smooth CGM data and interpolate missing values\n", "- Pull in Garmin step information and \"run activities\" and plot them\n", "- Calculate key metrics for that day, both glucose and steps\n", "- Create a chart of the glucose values and include metrics" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import plotly.express as px\n", "from plotly.subplots import make_subplots\n", "import plotly.graph_objects as go\n", "import datetime\n", "from datetime import date\n", "from garminconnect import (\n", " Garmin,\n", " GarminConnectConnectionError,\n", " GarminConnectTooManyRequestsError,\n", " GarminConnectAuthenticationError,\n", ")\n", "\n", "# Read in CSV file\n", "df = pd.read_csv('export.csv')\n", "\n", "# Remove \"time zone offset\" from \"occurred_at\" column and add new \"occurred_at_day\" column\n", "df['occurred_at_day'] = df['occurred_at'].apply(lambda x: x[:len(x) - 15])\n", "df['occurred_at'] = df['occurred_at'].apply(lambda x: x[:len(x) - 6])\n", "df.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Print all days with data\n", "daysWithData = df['occurred_at_day'].unique()\n", "print(daysWithData)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Filter down to one day, pick the second day in the dataset\n", "df = df[df['occurred_at_day']==daysWithData[2]]\n", "day = daysWithData[2]\n", "\n", "# Create a datasets just with glucose measurments\n", "gm = df[df['class']=='GlucoseMeasurement']\n", "\n", "# Create a dataset for meals and exercise, sort it\n", "mealsExercise = df[((df['class']=='Meal') | (df['class']=='ExerciseActivity') )]\n", "mealsExerciseSorted = mealsExercise.sort_values(by=[\"occurred_at\"], ascending=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get Garmin Data\n", "# This may not be so great, defaulting to simply retrieving the last 100 activities on Garmin.\n", "# If the day that is plotted is further in the past, this may not work.\n", "numberOfActivities = 100\n", "try:\n", " # Initialize Garmin client with credentials\n", " # Put your userID and password for https://connect.garmin.com/ here\n", " client = Garmin(\"USERID\", \"PASSWORD\")\n", " # Login to Garmin Connect portal\n", " client.login()\n", " # Get running activities\n", " allActivities = client.get_activities(0,numberOfActivities) # 0=start, numberOfActivities=limit\n", " dayOfInterest = datetime.datetime.strptime(day, '%Y-%m-%d').date()\n", " allDayStepData = client.get_steps_data(dayOfInterest.isoformat())\n", "except (GarminConnectConnectionError, GarminConnectAuthenticationError, GarminConnectTooManyRequestsError,) as err:\n", " print(\"Error occured during Garmin Connect Client init: %s\" % err)\n", " quit()\n", "except Exception:\n", " print(\"Unknown error occured during Garmin Connect Client init.\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# convert garmin data in list form to Pandas dataframe\n", "dfGarmin = pd.DataFrame.from_dict(allDayStepData)\n", "\n", "# manipulate start time so that it is local (And not GMT)\n", "dfGarmin['time'] = dfGarmin['startGMT'].apply(lambda x: x[:len(x) - 5])\n", "dfGarmin['time'] = dfGarmin['time'].apply(lambda x: x[11:])\n", "offset = dfGarmin[\"time\"][0]\n", "offsetHour = int(offset.split(':')[0])\n", "offsetMinutes=int(offset.split(':')[1])\n", "dfGarmin['time'] = pd.to_datetime(dfGarmin['startGMT'])\n", "dfGarmin['time'] = dfGarmin['time'].apply(lambda x: x - datetime.timedelta(hours=offsetHour, minutes=offsetMinutes))\n", "dfGarmin" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Just for exploring the data, lets look at all 15 minute segments that have non-zero steps\n", "dfGarmin = dfGarmin[dfGarmin.steps != 0]\n", "print(dfGarmin[['time', 'steps', 'primaryActivityLevel']])\n", "#dfGarmin.head(n=20)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create a dataset with just 2 columns\n", "gm_data = gm.filter(['occurred_at', 'value'])\n", "\n", "# rename the columns for easier readability\n", "gm_data.columns = ['time', 'value']\n", "\n", "# turn time column into the index and delete time column\n", "gm_data['time']= pd.to_datetime(gm_data['time'])\n", "gm_data.index = gm_data['time']\n", "del gm_data['time']\n", "\n", "gm_data = gm_data.resample('1T').mean() # add rows for every 1 minute\n", "gm_data = gm_data.interpolate(method='cubic') # interpolate the new 1 minute points with data\n", "\n", "# Calculate a few metrics\n", "threshold = 120 # this is an arbitrary threshold\n", "above = gm_data[gm_data['value'] > threshold] # create a dataset with glucose measuremnts over threshold\n", "minutesAboveThreshold = above.count()\n", "print('Number of minutes above '+str(threshold)+': '+ minutesAboveThreshold.to_string(index=False))\n", "\n", "percentageAboveThreshold = int(round(minutesAboveThreshold/(60*24)*100,0))\n", "print(\"Time above Threshold = \"+str(percentageAboveThreshold)+\"%\")\n", "\n", "averageGlucose = int(round(gm_data['value'].mean()))\n", "medianGlucose = int(round(gm_data['value'].median()))\n", "print(\"Average Glucose = \"+str(averageGlucose))\n", "print(\"Median Glucose = \"+str(medianGlucose))\n", "\n", "# Calculate statistics on the Garmin data\n", "numberOfRunningActivitiesToday = 0\n", "numberOfActivitiesToday = 0\n", "for i in range(numberOfActivities):\n", " activity = allActivities[i]\n", " activityDateTime = activity['startTimeLocal']\n", " activityDate = datetime.datetime.strptime(activityDateTime, \"%Y-%m-%d %H:%M:%S\")\n", " if str(activityDate.date()) == day:\n", " numberOfActivitiesToday = numberOfActivitiesToday + 1\n", " if activity[\"activityType\"][\"typeKey\"] == \"running\":\n", " numberOfRunningActivitiesToday = numberOfRunningActivitiesToday + 1\n", "\n", "print(\"Steps today = \"+str(dfGarmin.steps.sum()))\n", "print(\"Activities today = \"+str(numberOfActivitiesToday))\n", "print(\"Runs today = \"+str(numberOfRunningActivitiesToday))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# using subplots here to easily get a secondary y-axis\n", "fig = make_subplots(specs=[[{\"secondary_y\": True}]])\n", "# first add the glucose measurement data\n", "fig.add_trace( go.Scatter(x=gm_data.index, y=gm_data.value, mode='lines',line=dict(color=\"purple\")))\n", "\n", "# add meals and exercise to the chart\n", "yText = 145\n", "eventColor = \"green\"\n", "for index, row in mealsExerciseSorted.iterrows():\n", "\n", " # If the activity has \"run\" in the description, don't use it as it is a duplicate from Garmin\n", " if \"run\" in row['description']: continue\n", "\n", " # Convert the time in pandas to something that we can use as an index for the x-axis placement\n", " time = datetime.datetime.strptime(row['occurred_at'], '%Y-%m-%d %H:%M:%S')\n", "\n", " # Pick a different color depending on the event\n", " if (row['class'] == \"Meal\"): eventColor = \"black\"\n", " else: eventColor = \"green\"\n", "\n", " # draw a vertical line at the time of the meal/exercise\n", " fig.add_shape(type=\"line\", xref=\"x\", yref=\"y\", x0=time, y0=70, x1=time , y1=140, line_color=eventColor,)\n", " \n", " # Alternate text placement so adjacent text doesn't overlap\n", " if (yText == 145): yText = 153\n", " else: yText = 145\n", " \n", " # Add text\n", " fig.add_annotation(text=row['description'], xref=\"x\", yref=\"y\", x=time, y=yText, showarrow=False, font=dict(color=eventColor))\n", "\n", "# Add Garmin running activities\n", "for i in range(numberOfActivities):\n", " activity = allActivities[i]\n", " # only activities that are of type \"running\"\n", " if activity[\"activityType\"][\"typeKey\"] == \"running\":\n", " activityDateTime = activity['startTimeLocal']\n", " activityDate = datetime.datetime.strptime(activityDateTime, \"%Y-%m-%d %H:%M:%S\")\n", " if str(activityDate.date()) == day:\n", " # draw a vertical line at the time of the running activity\n", " fig.add_shape(type=\"line\", xref=\"x\", yref=\"y\", x0=activityDateTime, y0=70, x1=activityDateTime , y1=140, line_color=\"green\",)\n", " # Add text... yes this is specific to kilometers. This may need changes for miles.\n", " textDescr = str(activity['activityName']) + \" \" + str(int(round(activity['distance']/1000))) + \"K run\"\n", " fig.add_annotation(text=textDescr, xref=\"x\", yref=\"y\", x=activityDateTime, y=145, showarrow=False, font=dict(color=\"green\"))\n", "\n", "\n", "\n", "# Draw a line at the threshold\n", "fig.add_shape(type=\"line\", xref=\"x\", yref=\"y\",\n", " x0=gm_data.index[0], y0=threshold, x1=gm_data.index.max(), y1=threshold, line_color=\"red\",)\n", "\n", "# Show text box with summary values\n", "fig.add_annotation(\n", " text='Glucose Threshold = '+str(threshold)+\n", " '
Minutes above Threshold = '+str(int(round(minutesAboveThreshold,0)))+\n", " '
Time above Threshold = '+str(percentageAboveThreshold)+\"%\"+\n", " '
Average Glucose = '+str(averageGlucose)+\n", " '
Median Glucose = '+str(medianGlucose)+\n", " '
Steps Today = '+str(dfGarmin.steps.sum()),\n", " align='right', showarrow=False,\n", " xref='paper', yref='paper', x=0.002, y=0.005,\n", " bordercolor='black', borderwidth=1\n", " )\n", "\n", "# Setting primary and secondary y axis titles and ticks\n", "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'))\n", "# Adding step data to the chart, using the secondary y axis\n", "fig.add_trace( go.Bar(x=dfGarmin.time, y=dfGarmin.steps), secondary_y=True)\n", "\n", "# Set x axis title\n", "fig.update_xaxes(title_text=str(day), tickformat='%H:%M')\n", "# Hide the legend\n", "fig.update_layout(showlegend=False)\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "interpreter": { "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 2 }