Abbott Freestyle Libre CGM: Guide to data extraction and analysis§

The Abbott Freestyle Libre Version 1 Continuous Glucose Monitor is a sensor that tracks your blood glucose levels over a period of two weeks. A variety of vendors use the Freestyle Libre CGM; in this notebook, we use Nutrisense. In addition to logging continuous data, purchasing the Nutrisense CGM pairs you with a dietitian who helps you interpret, understand, and manage your glucose levels.
Having used the Nutrisense CGM, we’ll show you how to extract, visualize, and compute statistics for data collected by the monitor. All you will need for this notebook is your username and password!
You can extract the parameters with known sampling frequencies as below:
Parameter Name |
Sampling Frequency |
|---|---|
Glucose level |
15 minutes |
Glucose Peak |
24 hours |
Glucose Average |
24 hours |
Glucose Adaptability |
24 hours |
Glucose Variability |
24 hours |
In this guide, we sequentially cover the following nine topics to extract from the Nutrisense CGM: 1. Setup 2. Authentication/Authorization - Requires only username and password, no OAuth. 3. Data extraction - We get data via wearipedia in a couple lines of code. 4. Data Exporting - We export all of this data to file formats compatible by R, Excel, and MatLab. 5. Adherence - We simulate non-adherence by dynamically removing datapoints from our simulated data. 6.
Visualization - We create a simple plot to visualize our data. 7. Advanced visualization - 7.1: We plot the glucose levels over a day - 7.2: We recreate the glucose scores plot displayed on the nutrisense cgm app. - 7.3: We recreate the glucose summary statistics chart displayed on the nutrisense cgm app. 8. Outlier Detection and Data Cleaning - We detect outliers in our data and filter them out. 9. Data analysis - 9.1: We create a plot to visualize the difference between glucose
levels at night and at day. - 9.2: We plot the change in blood glucose level after changes of behavior. - Note that we are not making any scientific claims here as our sample size is small and the data collection process was not rigorously vetted (it is our own data), only demonstrating that this code could potentially be used to perform rigorous analyses in the future.
Disclaimer: this notebook is purely for educational purposes. All of the data currently stored in this notebook is purely synthetic, meaning randomly generated according to rules we created. Despite this, the end-to-end data extraction pipeline has been tested on our own data, meaning that if you enter your own email and password on your own Colab instance, you can visualize your own real data.
1. Setup§
Participant Setup§
Dear Participant,
Once you unbox your Nutrisense CGM, please set up the device by following the Official Guide: https://support.nutrisense.io/hc/en-us/articles/4402815217687-How-Do-I-Put-on-My-CGM-
This guide is also available on the nutrisense app, which is where you will log and view your data.
Make sure that your phone is paired to it using the Nutrisense login credentials (email and password) given to you by the data receiver.
Best,
Wearipedia
Data Receiver Setup§
Please follow the below steps:
Create an email address for the participant, for example
foo@email.com.Create a Nutrisense account with the email
foo@email.comand some random password.Keep
foo@email.comand password stored somewhere safe.Distribute the device to the participant and instruct them to follow the participant setup letter above.
Install the
wearipediaPython package to easily extract data from this device.
[1]:
!pip install git+https://github.com/TrafficCop/wearipedia-1
Collecting git+https://github.com/TrafficCop/wearipedia-1
Cloning https://github.com/TrafficCop/wearipedia-1 to /tmp/pip-req-build-um5uuo2i
Running command git clone --filter=blob:none --quiet https://github.com/TrafficCop/wearipedia-1 /tmp/pip-req-build-um5uuo2i
Resolved https://github.com/TrafficCop/wearipedia-1 to commit 1015997ade2b772b4bc2e18e8757ac6e55705557
Installing build dependencies ... done
Getting requirements to build wheel ... done
Preparing metadata (pyproject.toml) ... done
Requirement already satisfied: beautifulsoup4<5.0.0,>=4.12.2 in /usr/local/lib/python3.10/dist-packages (from wearipedia==0.1.2) (4.12.3)
Collecting fastapi==0.101 (from wearipedia==0.1.2)
Downloading fastapi-0.101.0-py3-none-any.whl (65 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.7/65.7 kB 1.4 MB/s eta 0:00:00
Collecting fbm<0.4.0,>=0.3.0 (from wearipedia==0.1.2)
Downloading fbm-0.3.0-py3-none-any.whl (9.4 kB)
Collecting garminconnect<0.2.0,>=0.1.48 (from wearipedia==0.1.2)
Downloading garminconnect-0.1.55.tar.gz (20 kB)
Preparing metadata (setup.py) ... done
Collecting jupyter<2.0.0,>=1.0.0 (from wearipedia==0.1.2)
Downloading jupyter-1.0.0-py2.py3-none-any.whl (2.7 kB)
Collecting kaleido==0.2.1 (from wearipedia==0.1.2)
Downloading kaleido-0.2.1-py2.py3-none-manylinux1_x86_64.whl (79.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.9/79.9 MB 5.6 MB/s eta 0:00:00
Collecting lida<0.0.12,>=0.0.11 (from wearipedia==0.1.2)
Downloading lida-0.0.11-py3-none-any.whl (7.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7.3/7.3 MB 19.6 MB/s eta 0:00:00
Collecting myfitnesspal<3.0.0,>=2.0.1 (from wearipedia==0.1.2)
Downloading myfitnesspal-2.1.0-py3-none-any.whl (30 kB)
Collecting pandas==1.5.3 (from wearipedia==0.1.2)
Downloading pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.1/12.1 MB 68.5 MB/s eta 0:00:00
Collecting polyline<3.0.0,>=2.0.0 (from wearipedia==0.1.2)
Downloading polyline-2.0.2-py3-none-any.whl (6.0 kB)
Collecting rich<13.0.0,>=12.6.0 (from wearipedia==0.1.2)
Downloading rich-12.6.0-py3-none-any.whl (237 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 237.5/237.5 kB 21.7 MB/s eta 0:00:00
Requirement already satisfied: scipy<2.0,>=1.6 in /usr/local/lib/python3.10/dist-packages (from wearipedia==0.1.2) (1.11.4)
Requirement already satisfied: tqdm<5.0.0,>=4.64.1 in /usr/local/lib/python3.10/dist-packages (from wearipedia==0.1.2) (4.66.2)
Collecting typer[all]<0.7.0,>=0.6.1 (from wearipedia==0.1.2)
Downloading typer-0.6.1-py3-none-any.whl (38 kB)
Collecting typing-extensions==4.5.0 (from wearipedia==0.1.2)
Downloading typing_extensions-4.5.0-py3-none-any.whl (27 kB)
Collecting wget<4.0,>=3.2 (from wearipedia==0.1.2)
Downloading wget-3.2.zip (10 kB)
Preparing metadata (setup.py) ... done
Requirement already satisfied: pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 in /usr/local/lib/python3.10/dist-packages (from fastapi==0.101->wearipedia==0.1.2) (2.6.4)
Collecting starlette<0.28.0,>=0.27.0 (from fastapi==0.101->wearipedia==0.1.2)
Downloading starlette-0.27.0-py3-none-any.whl (66 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 67.0/67.0 kB 6.0 MB/s eta 0:00:00
Requirement already satisfied: python-dateutil>=2.8.1 in /usr/local/lib/python3.10/dist-packages (from pandas==1.5.3->wearipedia==0.1.2) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas==1.5.3->wearipedia==0.1.2) (2023.4)
Requirement already satisfied: numpy>=1.21.0 in /usr/local/lib/python3.10/dist-packages (from pandas==1.5.3->wearipedia==0.1.2) (1.25.2)
Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.10/dist-packages (from beautifulsoup4<5.0.0,>=4.12.2->wearipedia==0.1.2) (2.5)
Requirement already satisfied: requests in /usr/local/lib/python3.10/dist-packages (from garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2) (2.31.0)
Collecting cloudscraper (from garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2)
Downloading cloudscraper-1.2.71-py2.py3-none-any.whl (99 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 99.7/99.7 kB 9.2 MB/s eta 0:00:00
Requirement already satisfied: notebook in /usr/local/lib/python3.10/dist-packages (from jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (6.5.5)
Collecting qtconsole (from jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2)
Downloading qtconsole-5.5.1-py3-none-any.whl (123 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 123.4/123.4 kB 13.6 MB/s eta 0:00:00
Requirement already satisfied: jupyter-console in /usr/local/lib/python3.10/dist-packages (from jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (6.1.0)
Requirement already satisfied: nbconvert in /usr/local/lib/python3.10/dist-packages (from jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (6.5.4)
Requirement already satisfied: ipykernel in /usr/local/lib/python3.10/dist-packages (from jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (5.5.6)
Requirement already satisfied: ipywidgets in /usr/local/lib/python3.10/dist-packages (from jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (7.7.1)
Collecting llmx>=0.0.18a (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading llmx-0.0.21a0-py3-none-any.whl (20 kB)
Collecting uvicorn (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading uvicorn-0.29.0-py3-none-any.whl (60 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 60.8/60.8 kB 5.8 MB/s eta 0:00:00
Requirement already satisfied: typer in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.9.4)
Collecting python-multipart (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading python_multipart-0.0.9-py3-none-any.whl (22 kB)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (3.7.1)
Requirement already satisfied: altair in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (4.2.2)
Requirement already satisfied: seaborn in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.13.1)
Requirement already satisfied: plotly in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (5.15.0)
Requirement already satisfied: plotnine in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.12.4)
Requirement already satisfied: statsmodels in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.14.1)
Requirement already satisfied: networkx in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (3.2.1)
Requirement already satisfied: geopandas in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.13.2)
Requirement already satisfied: matplotlib-venn in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.11.10)
Requirement already satisfied: wordcloud in /usr/local/lib/python3.10/dist-packages (from lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (1.9.3)
Collecting blessed<2.0,>=1.8.5 (from myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2)
Downloading blessed-1.20.0-py2.py3-none-any.whl (58 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 58.4/58.4 kB 6.2 MB/s eta 0:00:00
Collecting browser-cookie3<1,>=0.16.1 (from myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2)
Downloading browser_cookie3-0.19.1-py3-none-any.whl (14 kB)
Requirement already satisfied: lxml<5,>=4.2.5 in /usr/local/lib/python3.10/dist-packages (from myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2) (4.9.4)
Collecting measurement<4.0,>=3.2.0 (from myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2)
Downloading measurement-3.2.2-py3-none-any.whl (17 kB)
Collecting commonmark<0.10.0,>=0.9.0 (from rich<13.0.0,>=12.6.0->wearipedia==0.1.2)
Downloading commonmark-0.9.1-py2.py3-none-any.whl (51 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 51.1/51.1 kB 4.8 MB/s eta 0:00:00
Requirement already satisfied: pygments<3.0.0,>=2.6.0 in /usr/local/lib/python3.10/dist-packages (from rich<13.0.0,>=12.6.0->wearipedia==0.1.2) (2.16.1)
Requirement already satisfied: click<9.0.0,>=7.1.1 in /usr/local/lib/python3.10/dist-packages (from typer[all]<0.7.0,>=0.6.1->wearipedia==0.1.2) (8.1.7)
Collecting colorama<0.5.0,>=0.4.3 (from typer[all]<0.7.0,>=0.6.1->wearipedia==0.1.2)
Downloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)
Collecting shellingham<2.0.0,>=1.3.0 (from typer[all]<0.7.0,>=0.6.1->wearipedia==0.1.2)
Downloading shellingham-1.5.4-py2.py3-none-any.whl (9.8 kB)
Requirement already satisfied: wcwidth>=0.1.4 in /usr/local/lib/python3.10/dist-packages (from blessed<2.0,>=1.8.5->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2) (0.2.13)
Requirement already satisfied: six>=1.9.0 in /usr/local/lib/python3.10/dist-packages (from blessed<2.0,>=1.8.5->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2) (1.16.0)
Collecting lz4 (from browser-cookie3<1,>=0.16.1->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2)
Downloading lz4-4.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.3/1.3 MB 43.0 MB/s eta 0:00:00
Collecting pycryptodomex (from browser-cookie3<1,>=0.16.1->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2)
Downloading pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 68.2 MB/s eta 0:00:00
Requirement already satisfied: jeepney in /usr/lib/python3/dist-packages (from browser-cookie3<1,>=0.16.1->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2) (0.7.1)
Requirement already satisfied: pyparsing>=2.4.7 in /usr/local/lib/python3.10/dist-packages (from cloudscraper->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2) (3.1.2)
Collecting requests-toolbelt>=0.9.1 (from cloudscraper->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2)
Downloading requests_toolbelt-1.0.0-py2.py3-none-any.whl (54 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 54.5/54.5 kB 4.0 MB/s eta 0:00:00
Collecting openai (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading openai-1.16.2-py3-none-any.whl (267 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 267.1/267.1 kB 25.8 MB/s eta 0:00:00
Collecting tiktoken (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading tiktoken-0.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 72.4 MB/s eta 0:00:00
Collecting diskcache (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading diskcache-5.6.3-py3-none-any.whl (45 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.5/45.5 kB 4.7 MB/s eta 0:00:00
Collecting cohere (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading cohere-5.2.2-py3-none-any.whl (144 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 144.3/144.3 kB 12.9 MB/s eta 0:00:00
Requirement already satisfied: google.auth in /usr/local/lib/python3.10/dist-packages (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (2.27.0)
Requirement already satisfied: pyyaml in /usr/local/lib/python3.10/dist-packages (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (6.0.1)
Requirement already satisfied: sympy in /usr/local/lib/python3.10/dist-packages (from measurement<4.0,>=3.2.0->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2) (1.12)
Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2) (0.6.0)
Requirement already satisfied: pydantic-core==2.16.3 in /usr/local/lib/python3.10/dist-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2) (2.16.3)
INFO: pip is looking at multiple versions of pydantic to determine which version is compatible with other requirements. This could take a while.
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.6.3-py3-none-any.whl (395 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 395.2/395.2 kB 36.9 MB/s eta 0:00:00
Downloading pydantic-2.6.2-py3-none-any.whl (394 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 394.9/394.9 kB 31.3 MB/s eta 0:00:00
Downloading pydantic-2.6.1-py3-none-any.whl (394 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 394.8/394.8 kB 29.4 MB/s eta 0:00:00
Collecting pydantic-core==2.16.2 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.2/2.2 MB 57.2 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.6.0-py3-none-any.whl (394 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 394.2/394.2 kB 30.6 MB/s eta 0:00:00
Collecting pydantic-core==2.16.1 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.2/2.2 MB 59.1 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.5.3-py3-none-any.whl (381 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 381.9/381.9 kB 32.3 MB/s eta 0:00:00
Collecting pydantic-core==2.14.6 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 67.0 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.5.2-py3-none-any.whl (381 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 381.9/381.9 kB 29.1 MB/s eta 0:00:00
Collecting pydantic-core==2.14.5 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 63.9 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.5.1-py3-none-any.whl (381 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 381.6/381.6 kB 29.8 MB/s eta 0:00:00
Collecting pydantic-core==2.14.3 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.14.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 54.5 MB/s eta 0:00:00
INFO: pip is looking at multiple versions of pydantic to determine which version is compatible with other requirements. This could take a while.
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.5.0-py3-none-any.whl (407 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 407.5/407.5 kB 35.6 MB/s eta 0:00:00
Collecting pydantic-core==2.14.1 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 56.7 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.4.2-py3-none-any.whl (395 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 395.8/395.8 kB 32.9 MB/s eta 0:00:00
Collecting pydantic-core==2.10.1 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.0/2.0 MB 55.8 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.4.1-py3-none-any.whl (395 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 395.3/395.3 kB 30.7 MB/s eta 0:00:00
Downloading pydantic-2.4.0-py3-none-any.whl (395 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 395.4/395.4 kB 34.7 MB/s eta 0:00:00
Collecting pydantic-core==2.10.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.0/2.0 MB 69.9 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.3.0-py3-none-any.whl (374 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 374.5/374.5 kB 24.9 MB/s eta 0:00:00
Collecting pydantic-core==2.6.3 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 59.1 MB/s eta 0:00:00
INFO: This is taking longer than usual. You might need to provide the dependency resolver with stricter constraints to reduce runtime. See https://pip.pypa.io/warnings/backtracking for guidance. If you want to abort this run, press Ctrl + C.
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.2.1-py3-none-any.whl (373 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 373.4/373.4 kB 31.4 MB/s eta 0:00:00
Collecting pydantic-core==2.6.1 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 62.8 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.2.0-py3-none-any.whl (373 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 373.2/373.2 kB 34.4 MB/s eta 0:00:00
Collecting pydantic-core==2.6.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 62.5 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.1.1-py3-none-any.whl (370 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 370.9/370.9 kB 28.1 MB/s eta 0:00:00
Collecting pydantic-core==2.4.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 64.4 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.0.3-py3-none-any.whl (364 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 364.0/364.0 kB 28.4 MB/s eta 0:00:00
Collecting pydantic-core==2.3.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 70.4 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-2.0.2-py3-none-any.whl (359 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 359.1/359.1 kB 25.5 MB/s eta 0:00:00
Collecting pydantic-core==2.1.2 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic_core-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 56.3 MB/s eta 0:00:00
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi==0.101->wearipedia==0.1.2)
Downloading pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 MB 67.1 MB/s eta 0:00:00
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2) (3.6)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.2) (2024.2.2)
Requirement already satisfied: anyio<5,>=3.4.0 in /usr/local/lib/python3.10/dist-packages (from starlette<0.28.0,>=0.27.0->fastapi==0.101->wearipedia==0.1.2) (3.7.1)
Requirement already satisfied: entrypoints in /usr/local/lib/python3.10/dist-packages (from altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.4)
Requirement already satisfied: jinja2 in /usr/local/lib/python3.10/dist-packages (from altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (3.1.3)
Requirement already satisfied: jsonschema>=3.0 in /usr/local/lib/python3.10/dist-packages (from altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (4.19.2)
Requirement already satisfied: toolz in /usr/local/lib/python3.10/dist-packages (from altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.12.1)
Requirement already satisfied: fiona>=1.8.19 in /usr/local/lib/python3.10/dist-packages (from geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (1.9.6)
Requirement already satisfied: packaging in /usr/local/lib/python3.10/dist-packages (from geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (24.0)
Requirement already satisfied: pyproj>=3.0.1 in /usr/local/lib/python3.10/dist-packages (from geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (3.6.1)
Requirement already satisfied: shapely>=1.7.1 in /usr/local/lib/python3.10/dist-packages (from geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (2.0.3)
Requirement already satisfied: ipython-genutils in /usr/local/lib/python3.10/dist-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.2.0)
Requirement already satisfied: ipython>=5.0.0 in /usr/local/lib/python3.10/dist-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (7.34.0)
Requirement already satisfied: traitlets>=4.1.0 in /usr/local/lib/python3.10/dist-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (5.7.1)
Requirement already satisfied: jupyter-client in /usr/local/lib/python3.10/dist-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (6.1.12)
Requirement already satisfied: tornado>=4.2 in /usr/local/lib/python3.10/dist-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (6.3.3)
Requirement already satisfied: widgetsnbextension~=3.6.0 in /usr/local/lib/python3.10/dist-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (3.6.6)
Requirement already satisfied: jupyterlab-widgets>=1.0.0 in /usr/local/lib/python3.10/dist-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (3.0.10)
Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from jupyter-console->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (3.0.43)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (1.2.0)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (4.50.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (1.4.5)
Requirement already satisfied: pillow>=6.2.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (9.4.0)
Requirement already satisfied: bleach in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (6.1.0)
Requirement already satisfied: defusedxml in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.7.1)
Requirement already satisfied: jupyter-core>=4.7 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (5.7.2)
Requirement already satisfied: jupyterlab-pygments in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.3.0)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (2.1.5)
Requirement already satisfied: mistune<2,>=0.8.1 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.8.4)
Requirement already satisfied: nbclient>=0.5.0 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.10.0)
Requirement already satisfied: nbformat>=5.1 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (5.10.3)
Requirement already satisfied: pandocfilters>=1.4.1 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.5.1)
Requirement already satisfied: tinycss2 in /usr/local/lib/python3.10/dist-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.2.1)
Requirement already satisfied: pyzmq<25,>=17 in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (23.2.1)
Requirement already satisfied: argon2-cffi in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (23.1.0)
Requirement already satisfied: nest-asyncio>=1.5 in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.6.0)
Requirement already satisfied: Send2Trash>=1.8.0 in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.8.2)
Requirement already satisfied: terminado>=0.8.3 in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.18.1)
Requirement already satisfied: prometheus-client in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.20.0)
Requirement already satisfied: nbclassic>=0.4.7 in /usr/local/lib/python3.10/dist-packages (from notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.0.0)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.10/dist-packages (from plotly->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (8.2.3)
Requirement already satisfied: mizani<0.10.0,>0.9.0 in /usr/local/lib/python3.10/dist-packages (from plotnine->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.9.3)
Requirement already satisfied: patsy>=0.5.1 in /usr/local/lib/python3.10/dist-packages (from plotnine->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.5.6)
Collecting qtpy>=2.4.0 (from qtconsole->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2)
Downloading QtPy-2.4.1-py3-none-any.whl (93 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 93.5/93.5 kB 9.2 MB/s eta 0:00:00
Collecting h11>=0.8 (from uvicorn->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading h11-0.14.0-py3-none-any.whl (58 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 58.3/58.3 kB 6.9 MB/s eta 0:00:00
Requirement already satisfied: sniffio>=1.1 in /usr/local/lib/python3.10/dist-packages (from anyio<5,>=3.4.0->starlette<0.28.0,>=0.27.0->fastapi==0.101->wearipedia==0.1.2) (1.3.1)
Requirement already satisfied: exceptiongroup in /usr/local/lib/python3.10/dist-packages (from anyio<5,>=3.4.0->starlette<0.28.0,>=0.27.0->fastapi==0.101->wearipedia==0.1.2) (1.2.0)
Requirement already satisfied: attrs>=19.2.0 in /usr/local/lib/python3.10/dist-packages (from fiona>=1.8.19->geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (23.2.0)
Requirement already satisfied: click-plugins>=1.0 in /usr/local/lib/python3.10/dist-packages (from fiona>=1.8.19->geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (1.1.1)
Requirement already satisfied: cligj>=0.5 in /usr/local/lib/python3.10/dist-packages (from fiona>=1.8.19->geopandas->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.7.2)
Requirement already satisfied: setuptools>=18.5 in /usr/local/lib/python3.10/dist-packages (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (67.7.2)
Collecting jedi>=0.16 (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2)
Downloading jedi-0.19.1-py2.py3-none-any.whl (1.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 56.0 MB/s eta 0:00:00
Requirement already satisfied: decorator in /usr/local/lib/python3.10/dist-packages (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (4.4.2)
Requirement already satisfied: pickleshare in /usr/local/lib/python3.10/dist-packages (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.7.5)
Requirement already satisfied: backcall in /usr/local/lib/python3.10/dist-packages (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.2.0)
Requirement already satisfied: matplotlib-inline in /usr/local/lib/python3.10/dist-packages (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.1.6)
Requirement already satisfied: pexpect>4.3 in /usr/local/lib/python3.10/dist-packages (from ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (4.9.0)
Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /usr/local/lib/python3.10/dist-packages (from jsonschema>=3.0->altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (2023.12.1)
Requirement already satisfied: referencing>=0.28.4 in /usr/local/lib/python3.10/dist-packages (from jsonschema>=3.0->altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.34.0)
Requirement already satisfied: rpds-py>=0.7.1 in /usr/local/lib/python3.10/dist-packages (from jsonschema>=3.0->altair->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.18.0)
Requirement already satisfied: platformdirs>=2.5 in /usr/local/lib/python3.10/dist-packages (from jupyter-core>=4.7->nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (4.2.0)
Requirement already satisfied: jupyter-server>=1.8 in /usr/local/lib/python3.10/dist-packages (from nbclassic>=0.4.7->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.24.0)
Requirement already satisfied: notebook-shim>=0.2.3 in /usr/local/lib/python3.10/dist-packages (from nbclassic>=0.4.7->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.2.4)
Requirement already satisfied: fastjsonschema in /usr/local/lib/python3.10/dist-packages (from nbformat>=5.1->nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (2.19.1)
Requirement already satisfied: ptyprocess in /usr/local/lib/python3.10/dist-packages (from terminado>=0.8.3->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.7.0)
Requirement already satisfied: argon2-cffi-bindings in /usr/local/lib/python3.10/dist-packages (from argon2-cffi->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (21.2.0)
Requirement already satisfied: webencodings in /usr/local/lib/python3.10/dist-packages (from bleach->nbconvert->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.5.1)
Collecting fastavro<2.0.0,>=1.9.4 (from cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading fastavro-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 MB 45.3 MB/s eta 0:00:00
Collecting httpx>=0.21.2 (from cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading httpx-0.27.0-py3-none-any.whl (75 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 75.6/75.6 kB 7.9 MB/s eta 0:00:00
Requirement already satisfied: tokenizers<0.16.0,>=0.15.2 in /usr/local/lib/python3.10/dist-packages (from cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.15.2)
Collecting types-requests<3.0.0,>=2.0.0 (from cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading types_requests-2.31.0.20240406-py3-none-any.whl (15 kB)
Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from google.auth->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (5.3.3)
Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.10/dist-packages (from google.auth->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.4.0)
Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.10/dist-packages (from google.auth->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (4.9)
Requirement already satisfied: distro<2,>=1.7.0 in /usr/lib/python3/dist-packages (from openai->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (1.7.0)
INFO: pip is looking at multiple versions of openai to determine which version is compatible with other requirements. This could take a while.
Collecting openai (from llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading openai-1.16.1-py3-none-any.whl (266 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 266.9/266.9 kB 25.8 MB/s eta 0:00:00
Downloading openai-1.16.0-py3-none-any.whl (266 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 266.9/266.9 kB 24.9 MB/s eta 0:00:00
Downloading openai-1.14.3-py3-none-any.whl (262 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 262.9/262.9 kB 16.2 MB/s eta 0:00:00
Downloading openai-1.14.2-py3-none-any.whl (262 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 262.4/262.4 kB 24.9 MB/s eta 0:00:00
Downloading openai-1.14.1-py3-none-any.whl (257 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 257.5/257.5 kB 23.8 MB/s eta 0:00:00
Downloading openai-1.14.0-py3-none-any.whl (257 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 257.5/257.5 kB 24.0 MB/s eta 0:00:00
Downloading openai-1.13.4-py3-none-any.whl (230 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 230.5/230.5 kB 25.9 MB/s eta 0:00:00
INFO: pip is looking at multiple versions of openai to determine which version is compatible with other requirements. This could take a while.
Downloading openai-1.13.3-py3-none-any.whl (227 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 227.4/227.4 kB 18.0 MB/s eta 0:00:00
Downloading openai-1.12.0-py3-none-any.whl (226 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 226.7/226.7 kB 23.8 MB/s eta 0:00:00
Downloading openai-1.11.1-py3-none-any.whl (226 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 226.1/226.1 kB 22.7 MB/s eta 0:00:00
Downloading openai-1.11.0-py3-none-any.whl (226 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 226.0/226.0 kB 18.5 MB/s eta 0:00:00
Downloading openai-1.10.0-py3-none-any.whl (225 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 225.1/225.1 kB 23.8 MB/s eta 0:00:00
INFO: This is taking longer than usual. You might need to provide the dependency resolver with stricter constraints to reduce runtime. See https://pip.pypa.io/warnings/backtracking for guidance. If you want to abort this run, press Ctrl + C.
Downloading openai-1.9.0-py3-none-any.whl (223 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 223.4/223.4 kB 22.1 MB/s eta 0:00:00
Downloading openai-1.8.0-py3-none-any.whl (222 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 222.3/222.3 kB 17.4 MB/s eta 0:00:00
Downloading openai-1.7.2-py3-none-any.whl (212 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 212.1/212.1 kB 22.1 MB/s eta 0:00:00
Downloading openai-1.7.1-py3-none-any.whl (224 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 224.9/224.9 kB 22.3 MB/s eta 0:00:00
Downloading openai-1.7.0-py3-none-any.whl (224 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 224.7/224.7 kB 16.7 MB/s eta 0:00:00
Downloading openai-1.6.1-py3-none-any.whl (225 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 225.4/225.4 kB 11.2 MB/s eta 0:00:00
Downloading openai-1.6.0-py3-none-any.whl (225 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 225.4/225.4 kB 22.1 MB/s eta 0:00:00
Downloading openai-1.5.0-py3-none-any.whl (223 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 223.7/223.7 kB 22.9 MB/s eta 0:00:00
Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.10/dist-packages (from sympy->measurement<4.0,>=3.2.0->myfitnesspal<3.0.0,>=2.0.1->wearipedia==0.1.2) (1.3.0)
Requirement already satisfied: regex>=2022.1.18 in /usr/local/lib/python3.10/dist-packages (from tiktoken->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (2023.12.25)
Collecting httpcore==1.* (from httpx>=0.21.2->cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2)
Downloading httpcore-1.0.5-py3-none-any.whl (77 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 77.9/77.9 kB 9.6 MB/s eta 0:00:00
Requirement already satisfied: parso<0.9.0,>=0.8.3 in /usr/local/lib/python3.10/dist-packages (from jedi>=0.16->ipython>=5.0.0->ipykernel->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (0.8.3)
Requirement already satisfied: websocket-client in /usr/local/lib/python3.10/dist-packages (from jupyter-server>=1.8->nbclassic>=0.4.7->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.7.0)
Requirement already satisfied: pyasn1<0.7.0,>=0.4.6 in /usr/local/lib/python3.10/dist-packages (from pyasn1-modules>=0.2.1->google.auth->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.6.0)
Requirement already satisfied: huggingface_hub<1.0,>=0.16.4 in /usr/local/lib/python3.10/dist-packages (from tokenizers<0.16.0,>=0.15.2->cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (0.20.3)
Requirement already satisfied: cffi>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from argon2-cffi-bindings->argon2-cffi->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (1.16.0)
Requirement already satisfied: pycparser in /usr/local/lib/python3.10/dist-packages (from cffi>=1.0.1->argon2-cffi-bindings->argon2-cffi->notebook->jupyter<2.0.0,>=1.0.0->wearipedia==0.1.2) (2.22)
Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from huggingface_hub<1.0,>=0.16.4->tokenizers<0.16.0,>=0.15.2->cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (3.13.3)
Requirement already satisfied: fsspec>=2023.5.0 in /usr/local/lib/python3.10/dist-packages (from huggingface_hub<1.0,>=0.16.4->tokenizers<0.16.0,>=0.15.2->cohere->llmx>=0.0.18a->lida<0.0.12,>=0.0.11->wearipedia==0.1.2) (2023.6.0)
Building wheels for collected packages: wearipedia, garminconnect, wget
Building wheel for wearipedia (pyproject.toml) ... done
Created wheel for wearipedia: filename=wearipedia-0.1.2-py3-none-any.whl size=133994 sha256=685ab40536f8b0d63ea03079e848120d3ad4bd6170e037878fa3af3b623f5c33
Stored in directory: /tmp/pip-ephem-wheel-cache-0gq5ym2i/wheels/76/ae/d9/df9fe08bcbd83feb697b7c62d086552fd4441182565064ac1f
Building wheel for garminconnect (setup.py) ... done
Created wheel for garminconnect: filename=garminconnect-0.1.55-py3-none-any.whl size=13622 sha256=4b2747b37e302960782f89c8ccbe325a7085b9c36528d8f80f5d7a2db4aef18c
Stored in directory: /root/.cache/pip/wheels/8b/dc/11/9aa5c8473be40bed2c4032fded0fa7da63ff844b439681afcb
Building wheel for wget (setup.py) ... done
Created wheel for wget: filename=wget-3.2-py3-none-any.whl size=9656 sha256=0b130cc6cfa0f149a66f52e639504cdfd3e228a78e0637bdde3ceb99a0972af4
Stored in directory: /root/.cache/pip/wheels/8b/f1/7f/5c94f0a7a505ca1c81cd1d9208ae2064675d97582078e6c769
Successfully built wearipedia garminconnect wget
Installing collected packages: wget, kaleido, commonmark, typing-extensions, types-requests, typer, shellingham, rich, qtpy, python-multipart, pycryptodomex, polyline, lz4, jedi, h11, fbm, fastavro, diskcache, colorama, blessed, uvicorn, tiktoken, starlette, requests-toolbelt, pydantic, pandas, measurement, httpcore, browser-cookie3, httpx, fastapi, cloudscraper, qtconsole, openai, myfitnesspal, garminconnect, cohere, llmx, lida, jupyter, wearipedia
Attempting uninstall: typing-extensions
Found existing installation: typing_extensions 4.10.0
Uninstalling typing_extensions-4.10.0:
Successfully uninstalled typing_extensions-4.10.0
Attempting uninstall: typer
Found existing installation: typer 0.9.4
Uninstalling typer-0.9.4:
Successfully uninstalled typer-0.9.4
Attempting uninstall: rich
Found existing installation: rich 13.7.1
Uninstalling rich-13.7.1:
Successfully uninstalled rich-13.7.1
Attempting uninstall: pydantic
Found existing installation: pydantic 2.6.4
Uninstalling pydantic-2.6.4:
Successfully uninstalled pydantic-2.6.4
Attempting uninstall: pandas
Found existing installation: pandas 2.0.3
Uninstalling pandas-2.0.3:
Successfully uninstalled pandas-2.0.3
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
torch 2.2.1+cu121 requires nvidia-cublas-cu12==12.1.3.1; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cuda-cupti-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cuda-nvrtc-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cuda-runtime-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cudnn-cu12==8.9.2.26; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cufft-cu12==11.0.2.54; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-curand-cu12==10.3.2.106; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cusolver-cu12==11.4.5.107; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-cusparse-cu12==12.1.0.106; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-nccl-cu12==2.19.3; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
torch 2.2.1+cu121 requires nvidia-nvtx-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64", which is not installed.
sqlalchemy 2.0.29 requires typing-extensions>=4.6.0, but you have typing-extensions 4.5.0 which is incompatible.
google-colab 1.0.0 requires pandas==2.0.3, but you have pandas 1.5.3 which is incompatible.
pydantic-core 2.16.3 requires typing-extensions!=4.7.0,>=4.6.0, but you have typing-extensions 4.5.0 which is incompatible.
torch 2.2.1+cu121 requires typing-extensions>=4.8.0, but you have typing-extensions 4.5.0 which is incompatible.
Successfully installed blessed-1.20.0 browser-cookie3-0.19.1 cloudscraper-1.2.71 cohere-5.2.2 colorama-0.4.6 commonmark-0.9.1 diskcache-5.6.3 fastapi-0.101.0 fastavro-1.9.4 fbm-0.3.0 garminconnect-0.1.55 h11-0.14.0 httpcore-1.0.5 httpx-0.27.0 jedi-0.19.1 jupyter-1.0.0 kaleido-0.2.1 lida-0.0.11 llmx-0.0.21a0 lz4-4.3.3 measurement-3.2.2 myfitnesspal-2.1.0 openai-1.5.0 pandas-1.5.3 polyline-2.0.2 pycryptodomex-3.20.0 pydantic-1.10.15 python-multipart-0.0.9 qtconsole-5.5.1 qtpy-2.4.1 requests-toolbelt-1.0.0 rich-12.6.0 shellingham-1.5.4 starlette-0.27.0 tiktoken-0.6.0 typer-0.6.1 types-requests-2.31.0.20240406 typing-extensions-4.5.0 uvicorn-0.29.0 wearipedia-0.1.2 wget-3.2
2. Authentication/Authorization§
To obtain access to data, authorization is required. All you’ll need to do here is just put in your email and password for your nutrisense account. We’ll use this username and password to extract the data in the sections below.
[2]:
#@title Enter Nutrisense login credentials
email_address = "" #@param {type:"string"}
password = "" #@param {type:"string"}
3. Data Extraction§
Data can be extracted via wearipedia, our open-source Python package that unifies dozens of complex wearable device APIs into one simple, common interface.
First, we’ll set a date range and then extract all of the data within that date range. You can select whether you would like synthetic data or not with the checkbox.
[3]:
#@title Enter start and end dates (in the format yyyy-mm-dd)
#set start and end dates - this will give you all the data from 2000-01-01 (January 1st, 2000) to 2100-02-03 (February 3rd, 2100), for example
start_date='2022-03-01' #@param {type:"string"}
end_date='2023-03-12' #@param {type:"string"}
synthetic = True #@param {type:"boolean"}
[4]:
import wearipedia
device = wearipedia.get_device("nutrisense/cgm")
if not synthetic:
device.authenticate({"email": email_address, "password": password})
params = {"start_date": start_date, "end_date": end_date}
continuous = device.get_data("continuous", params=params)
statistics = device.get_data("statistics", params=params)
scoremain = device.get_data("scores", params=params)
summary = device.get_data("summary", params=params)
100%|██████████| 109/109 [00:00<00:00, 42779.00it/s]
100%|██████████| 109/109 [00:00<00:00, 15937.36it/s]
4. Data Exporting§
In this section, we export all of this data to formats compatible with popular scientific computing software (R, Excel, Google Sheets, Matlab). Specifically, we will first export to JSON, which can be read by R and Matlab. Then, we will export to CSV, which can be consumed by Excel, Google Sheets, and every other popular programming language.
Exporting to JSON (R, Matlab, etc.)§
Exporting to JSON is fairly simple. We export each datatype separately and also export a complete version that includes all simultaneously.
[5]:
import json
json.dump(continuous, open("continuous.json", "w"))
json.dump(statistics, open("statistics.json", "w"))
json.dump(scoremain, open("scores.json", "w"))
json.dump(summary, open("summary.json", "w"))
complete = {
"continuous": continuous,
"statistics": statistics,
"scores": scoremain,
"summary": summary
}
json.dump(complete, open("complete.json", "w"))
Feel free to open the file viewer (see left pane) to look at the outputs!
Exporting to CSV and XLSX (Excel, Google Sheets, R, Matlab, etc.)§
Exporting to CSV/XLSX requires a bit more processing, since they enforce a pretty restrictive schema.
Here we will put the continuous glucose data into a dataframe.
[6]:
import pandas as pd
import copy
# glucose scores dataframe
data = []
datas = []
for elem in continuous:
t = pd.DataFrame({'time': [elem['x']], 'level': [elem['y']], 'interpolated': [elem['interpolated']]})
data.append(copy.deepcopy(t))
datas = copy.deepcopy(data)
data = pd.concat(data)
display(data)
| time | level | interpolated | |
|---|---|---|---|
| 0 | 2022-03-01T00:15:00-08:00 | 141.752046 | False |
| 0 | 2022-03-01T00:30:00-08:00 | 130.740758 | False |
| 0 | 2022-03-01T00:45:00-08:00 | 140.253880 | True |
| 0 | 2022-03-01T01:00:00-08:00 | 138.751716 | False |
| 0 | 2022-03-01T01:15:00-08:00 | 137.299749 | False |
| ... | ... | ... | ... |
| 0 | 2022-06-17T22:45:00-08:00 | 127.744215 | False |
| 0 | 2022-06-17T23:00:00-08:00 | 125.898526 | False |
| 0 | 2022-06-17T23:15:00-08:00 | 136.808757 | False |
| 0 | 2022-06-17T23:30:00-08:00 | 125.820452 | False |
| 0 | 2022-06-17T23:45:00-08:00 | 120.911584 | True |
10463 rows × 3 columns
5. Adherence§
Since the Nutrisense CGM only holds around 8 hours of data at a time, if the user forgets to sync their data to their phone on time, there’ll likely be a gap with missing data. Take it from us, it happens more than you might think!
In this section, we will simulate this non-adherence over different periods of time from the participant.
Then, we will detect this non-adherence and give a Pandas DataFrame that concisely describes when the participant has had their device on and off throughout the entirety of the time period, allowing you to calculate how long they’ve had it on/off etc.
We will first delete a certain % of blocks, with user input.
[7]:
#@title Non-adherence simulation
start_date='2022-03-01' #@param {type:"string"}
days=1 #@param {type:"number"}
adherence_percent = 0.94 #@param {type:"slider", min:0, max:1, step:0.01}
Now let’s detect non-adherence. We will plot the periods when the participant syncs their data.
[8]:
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
import copy
# convert string to date
def strToD(s):
c = s[:19]
return datetime.strptime(c, "%Y-%m-%dT%H:%M:%S")
start = datetime.strptime(start_date, "%Y-%m-%d")
end = start + timedelta(days=days)
dta = []
for elem in continuous:
if strToD(elem["x"]) < start:
continue
elif strToD(elem["x"]) > end:
break
dta.append(copy.deepcopy(elem))
if np.random.uniform(low=0, high=1, size=(1,))[0] > adherence_percent:
dta[-1]["interpolated"] = True
datelist = pd.date_range(start_date,datetime.strftime(end,"%Y-%m-%d"), freq="15min")
valid = []
for point in dta:
if point['interpolated'] == True:
valid.append(0)
else:
valid.append(1)
plt.figure(figsize=(12, 6))
datelist = datelist[:len(valid)]
plt.plot(datelist,valid,drawstyle="steps-mid")
[8]:
[<matplotlib.lines.Line2D at 0x7ffb573d7f10>]
6. Visualization§
We’ve extracted lots of data, but what does it look like?
In this section, we will be visualizing our three kinds of data in a simple, customizable plot! This plot is intended to provide a starter example for plotting, whereas later examples emphasize deep control and aesthetics.
[9]:
#@title Basic Plot
feature = "continuous" #@param ["continuous"]
start_date = "2022-06-17" #@param {type:"date"}
smoothness = 0.1 #@param {type:"slider", min:0, max:1, step:0.01}
smooth_plot = True #@param {type:"boolean"}
periods = 96
from scipy.special import y0
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import pandas as pd
from scipy.ndimage import gaussian_filter1d
datelist = pd.date_range(start_date,datetime.strftime(datetime.strptime(start_date,"%Y-%m-%d")+timedelta(minutes=15*periods),"%Y-%m-%d"), freq="15min")
start = datelist[0]
end = datelist[len(datelist)-1]
title_fillin = "Continuous Glucose Data"
# convert string to date
def strToD(s):
c = s[:19]
return datetime.strptime(c, "%Y-%m-%dT%H:%M:%S")
dta = copy.deepcopy(data)
# extract the necessary data. notice we must convert
# the dates in the dataframe from string to actual datetime object
dta['time'] = dta['time'].apply(strToD)
dta = dta[dta['time'] >= start]
dta = dta[dta['time'] < end]
x = dta['time']
y = dta["level"]
sigma = 5*smoothness
if smooth_plot:
y = list(gaussian_filter1d(y, sigma=sigma))
plt.figure(figsize=(16,10))
plt.plot(x, y);
plt.ylim(40,200);
plt.title(f"{title_fillin} from {start_date} to {datetime.strftime(end,'%Y-%m-%d')}",fontsize=20);
plt.xlabel("Time");
plt.ylabel(title_fillin[:-1]);
7. Advanced Visualization§
7.1 Glucose levels over a day§
Here we will be graphing a more highly stylized plot, specifically the glucose levels chart displayed in the main screen of the official Nutrisense app. This graph shows the glucose levels (mg/dL) at each time of day.

The darker green sections of the graph denote healthy levels of glucose, which is defined as being between 70 mg/dL and 140 mg/dL. Anything outside of this range is colored light green.
To make this plot, we can extract the glucose levels data from our pandas dataframe and the associated time from the dataframe as well.
[10]:
#@title Glucose Plot Date
start_date = "2022-06-17" #@param {type:"date"}
import pandas as pd
datelist = pd.date_range(start_date, periods=2)
start = datelist[0]
end = datelist[-1]
[11]:
from datetime import datetime, timedelta
import copy
# convert string to date
def strToD(s):
c = s[:19]
return datetime.strptime(c, "%Y-%m-%dT%H:%M:%S")
dta = copy.deepcopy(data)
# extract the necessary data. notice we must convert
# the dates in the dataframe from string to actual datetime object
dta['time'] = dta['time'].apply(strToD)
dta = dta[dta['time'] >= start]
dta = dta[dta['time'] < end]
x = list(dta['time'])
cp = copy.deepcopy(dta)
# for datapoints that were "interpolated" (i.e. made up),
# set the value to a very large negative. This behavior
# matches how nutrisense deals with missing datapoints
cp.loc[cp["interpolated"] == True, "level"] = -float('inf')
y1 = list(cp['level'])
x = x[:len(y1)]
Having extracted the necessary data, we can use a matplotlib plot to get the basic shape of the desired graph. The remaining work is in the styling!
[12]:
import matplotlib.pyplot as plt
from scipy.ndimage import gaussian_filter1d
import numpy as np
import matplotlib.dates as mdates
import matplotlib.ticker as mticker
healthy = 140
figsize = (10, 6)
fig = plt.figure(figsize=figsize, facecolor="#1d2525")
# set the grid style
ax = plt.axes()
# ax.yaxis.grid(linewidth='2', ls=":", dashes=(1,3,1,3), color="#7c7d7d")
ax.yaxis.grid(linewidth='1', ls=":", dashes=(0.5,5,0.5,5), color="#7c7d7d")
# remove frame
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
# set colors
ax.set_facecolor("#1d2525")
# set the range and style of ticks
plt.yticks(np.arange(60.0, max(y1)+10, 10.0), color="#7c7d7d", fontsize=10)
plt.xticks(color="#7c7d7d")
plt.ylim(60, max(y1)+10)
plt.xlim(x[0], x[len(x)-1])
# set the xaxis labels to the time
xts = list(pd.date_range(start_date,datetime.strftime(datetime.strptime(end_date,"%Y-%m-%d")+timedelta(days=1),"%Y-%m-%d"), freq="3H"))
ax.xaxis.set_major_locator(mticker.FixedLocator(ax.xaxis.get_ticklocs()))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%l%p'))
# smooth the plot points
y = list(gaussian_filter1d(y1, sigma=0.01*5))
bottom = [min(e,70) for e in y]
# plot the graph and fill in the correct colors
plt.plot(x,y,color="#76885f", alpha=.0)
plt.plot(np.where(np.array(y)>= healthy, np.array(y), None), color="#c1c59a", label="1", alpha=.0)
plt.plot(np.where(np.array(y)<= healthy, np.array(y), None), color="#76885f", label="1", alpha=.0)
# lower bound:
plt.plot(x,bottom,color="#c1c59a", alpha=.0)
lower = np.array(y).clip(max=healthy)
upper = np.array(y)
ax.fill_between(x, lower, upper, color='#c1c59a', alpha=.8)
ax.fill_between(x, 0, lower, color='#76885f', alpha=.8)
ax.fill_between(x, 0, bottom, color="#c1c59a", alpha=.8)
[12]:
<matplotlib.collections.PolyCollection at 0x7ffb548e39a0>
This plot is important because it quickly helps us visualize all of the continuous data collected by the monitor, as well as identify healthy ranges of glucose levels, spikes, and times when the participant failed to log data.
7.2 Glucose Scores§
For our second chart, we will be attempting to replicate the glucose scores summary, which displays various statistics about your daily glucose levels.

The only data we will need here are the summary scores taken from the scoremain dictionary variable which we created during data extraction.
Here will begin by setting up the colors and score arrays:
[13]:
import pandas as pd
import numpy as np
# colors dict: put a color for every index
# we will do: (0) peak, (1) average, (2) adaptability, (3) variability
scores = [scoremain["scorePeak"], scoremain["scoreMean"], scoremain["scoreTimeOutsideRange"], scoremain["scoreStdDev"]]
lpos = [-0.112,-0.06,0.0,-0.03]
# This will give us our desired 10-block bar chart
basescore = [0.1]*10
base2 = {'peak': basescore}
bg2 =pd.DataFrame(base2).transpose()
# the labels for the plots
lsb = ["Peak", "Average", "Adaptability", "Variability"]
# set an array of colors which will be used to determine partition coloring
colors = ['#343e33']
for score in scores:
# divisions: 0-4, 5-7,
if score <= 4:
colors.append('#ff3e4d')
elif score <= 7:
colors.append('#ef893f')
else:
colors.append('#a9c180')
colors.append('#343e33')
All that is left for us to do now is tons of plotting and styling!
[14]:
# position will tell matplotlib where to place each plot, figsize is dynamic based off of our number of plots
from collections import defaultdict
from matplotlib.patches import FancyBboxPatch
# initialize the figure
Position = range(1,5)
figsize = (7, 4)
fig = plt.figure(figsize=figsize, facecolor="#1d2525")
for i in range(4):
# color array that will be applied to the current plot
cs = []
for j in range(scores[i]):
cs.append(colors[i+1])
while len(cs) < 10:
cs.append(colors[0])
# add one bar chart for each statistic
ax2 = fig.add_subplot(4, 1, Position[i], facecolor="#1d2525")
ax = bg2.plot(kind='barh', stacked=True, legend=False, secondary_y=True, ax = ax2, xlim=(-0.4,1.), color=cs) # #343e33
# redo the plot's elements to style it
new_patches = []
for patch in reversed(ax.patches):
bb = patch.get_bbox()
color=patch.get_facecolor()
# the fancy bbox patch module gives us additional ways to customize
p_bbox = FancyBboxPatch((bb.xmin, bb.ymin),
abs(bb.width) / 1.2, abs(bb.height)/2.6,
boxstyle="round,pad=-0.0015,rounding_size=0.010",
ec="none", fc=color,
mutation_aspect= 4
)
patch.remove()
new_patches.append(p_bbox)
for patch in new_patches:
ax.add_patch(patch)
lpos = [-0.07,-0.017,0.04,0.01]
# set the ylabel and ylabel position
ax.set_ylabel(lsb[i], color="white", fontsize=24, rotation=0, position=(0,0))
ax.yaxis.set_label_coords(lpos[i],-0)
ax.yaxis.set_label_position("left")
plt.text(1.05,-0.3, str(int(scores[i])), fontsize=36, color="white")
# remove frame
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
# remove frame
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['bottom'].set_visible(False)
ax2.spines['left'].set_visible(False)
# hide the ticks and tick labels
plt.setp( ax.get_xticklabels(), visible=False)
plt.setp( ax2.get_xticklabels(), visible=False)
plt.setp( ax.get_yticklabels(), visible=False)
plt.setp( ax2.get_yticklabels(), visible=False)
plt.xticks([])
plt.yticks([])
This plot gives us a simple and visually appealing way to visualize summary statistics for a patient’s glucose levels, allowing us to easily identify which scores are considered bad and which are good depending on color coding.
7.3 Glucose Summary§
The following is a box plot summarizing the daily glucose statistics. The text on the green rectangle gives the Mean / Standard Deviation and the caps represent the maximum/minimum.
The weekly is compared with the daily in this graph

For this graph, the data we need will all come from the statistics variable we extracted at the start of this notebook. Begin by choosing a date to plot summary statistics for.
[15]:
#@title Glucose Summary Date
start_date = "2022-06-17" #@param {type:"date"}
import pandas as pd
from scipy.stats import tstd
datelist = pd.date_range(start_date, periods=2)
start = datelist[0]
end = datelist[-1]
from datetime import datetime, timedelta
import copy
# convert string to date
def strToD(s):
c = s[:19]
return datetime.strptime(c, "%Y-%m-%dT%H:%M:%S")
dta = copy.deepcopy(data)
# extract the necessary data. notice we must convert
# the dates in the dataframe from string to actual datetime object
dta['time'] = dta['time'].apply(strToD)
dta = dta[dta['time'] >= start]
dta = dta[dta['time'] < end]
x = list(dta['time'])
cp = copy.deepcopy(dta)
Y = np.array(cp['level'])
first = {"min": 70.0, "max": 140.0, "__typename": "Range"}
second = {"min": min(Y), "max": max(Y), "__typename": "Range"}
low, high = min(Y), max(Y)
median = np.median(Y)
std = tstd(Y)
q1, q3 = (np.quantile(Y, [0.25, 0.75])).round(1)
greater = 70.0 < np.array(Y)
less = np.array(Y) < 140.0
condition = greater & less
timeWithinRange = float(len(np.extract(condition, Y)))
avg = np.average(Y)
stat = {
"healthyRange": first,
"range": second,
"timeWithinRange": timeWithinRange,
"min": low,
"max": high,
"mean": avg,
"median": median,
"standardDeviation": std,
"q1": q1,
"q3": q3,
"score": 0.0,
"__typename": "Stat",
}
s = [stat, statistics["average"]][::-1]
Since matplotlib’s default boxplot looks a farcry from the beautiful plot featured, we have some extra work to do!
Since we are plotting this chart straight from precalculated statistics, we will be using the bxp function rather than the standard boxplot.
[16]:
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
import warnings
# ignore unecessary warnings
warnings.filterwarnings("ignore")
# boxplot for daily and weekly
# bottom line is healthy range
# daily is min and max
# plt.boxplot()
figsize = (12, 4)
# healthy range:
lbound, rbound = 70.0, 140.0
# vars
labels = ["average", "today"]
medianprops = {"linestyle":'none'}
whiskerprops = {"color": "#a6a8a7","linewidth":2.2}
capprops = {"color": "white","linewidth":2.2}
stat_data = []
for i in range(len(s)):
d = s[i]
mn, mx = d["min"], d["max"]
avg, sd = d["mean"], d["standardDeviation"]
q1, q3 = d["q1"], d["q3"]
stat_data.append({'med': avg, 'q1': q1, 'q3':q3, 'whislo': mn, 'whishi': mx, 'label': labels[i]})
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=figsize, facecolor="#1d2525")
ax.yaxis.grid(linewidth='2', ls=":", dashes=(1,3,1,3), color="#7c7d7d")
ax.set_axisbelow(True)
ax.tick_params(axis='y',colors='white', labelrotation=90, labelsize=14, pad=10)
# might want to change below into a self drawn patch tick
ax.tick_params(axis='x',colors='red', width=4.0, length=10.0, labelsize=12, pad=10, bottom=100)
ax.set_yticklabels(["Weekly", "Daily"], rotation=90, ha='center', rotation_mode='anchor', weight="bold")
ax.set_xticklabels([lbound,rbound], ha='center', rotation_mode='anchor', weight="bold", color="#a6a8a7")
plt.tick_params(left = False)
bplot = ax.bxp(
stat_data,
showfliers=False,
vert=False,
patch_artist=True,
medianprops=medianprops,
whiskerprops=whiskerprops,
capprops=capprops,
)
caps = bplot['caps']
for cap in caps:
cap.set(ydata=cap.get_ydata() + (-0.035,+0.035))
new_patches = []
for patch in bplot['boxes']:
bb = patch.get_path().get_extents()
p_bbox = FancyBboxPatch((bb.xmin, bb.ymin),
abs(bb.width), abs(bb.height),
boxstyle="round,pad=0,rounding_size=0.2",
ec="#a9c180", fc="#a9c180",
mutation_aspect=0.1)
patch.remove()
new_patches.append(p_bbox)
for i in range(len(new_patches)):
patch = new_patches[i]
ax.add_patch(patch)
l, r = patch.get_x(), patch.get_x() + patch.get_width()
h = patch.get_y() + patch.get_height()
plt.text((l+r)/2-3, h+0.04, str(int(s[i]['mean']))+"/"+str(int(s[i]['standardDeviation'])), color="#a6a8a7", fontweight='semibold', fontsize=14)
plt.text(s[i]['min']-1.,h+0.04, str(int(s[i]['min'])), color="#a6a8a7", fontweight='semibold', fontsize=14)
plt.text(s[i]['max']-1.6,h+0.04, str(int(s[i]['max'])), color="#a6a8a7", fontweight='semibold', fontsize=14)
plt.xlim(60.0, max(s[0]['max'], s[1]['max'])+10)
plt.xticks([lbound, rbound])
# set colors
ax.set_facecolor("#1d2525")
# remove frame
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set(color="#a6a8a7", linewidth=2.0)
ax.spines['left'].set_visible(False)
# add the inner side to the ticks
ax2 = ax.twiny()
ax2.axis('off')
ax4 = ax2.twiny()
ax4.tick_params(axis="x",direction="out", pad=-15, bottom = True, top=False)
ax4.spines['bottom'].set(color="#a6a8a7", linewidth=2.0)
ax4.tick_params(axis='x',colors='red', width=4.0, length=10.0, labelsize=12, pad=10, bottom=100)
ax4.xaxis.set_ticklabels([])
ax4.set_xlim(ax.get_xlim())
ax4.set_xticks([lbound,rbound])
ax3 = ax2.twiny()
ax3.tick_params(axis="x",direction="in", pad=-15, bottom = True, top=False)
ax3.spines['bottom'].set(color="#a6a8a7", linewidth=2.0)
ax3.tick_params(axis='x',colors='red', width=4.0, length=10.0, labelsize=12, pad=10, bottom=100)
ax3.xaxis.set_ticklabels([])
ax3.set_xlim(ax.get_xlim())
ax3.set_xticks([lbound,rbound])
ax3.spines['top'].set_visible(False)
ax3.spines['right'].set_visible(False)
ax3.spines['bottom'].set_visible(False)
ax3.spines['left'].set_visible(False)
ax4.spines['top'].set_visible(False)
ax4.spines['right'].set_visible(False)
ax4.spines['bottom'].set_visible(False)
ax4.spines['left'].set_visible(False)
This graph is helpful because it gives a quick look at how the participant’s daily glucose level statistics match up to their overall weekly trend. It also gives us a view of the summary statistics of the glucose levels.
8. Outlier Detection and Data Cleaning§
NOTICE: If you are using synthetically generated data, the analyses may yield unintuitive results due to the randomly generated nature of the data
In this section, we will detect outliers in our extracted data.
Since there are currently no outliers (by construction, since it is simulated to have none), we will manually inject a couple into our general sessions dataframe (the method for finding outliers here will work for any glucose levels dataframe).
[17]:
import pandas as pd
from datetime import datetime, timedelta
import copy
dToStr = lambda x: datetime.strftime(x,"%Y-%m-%dT%H:%M:%S")
last = list(data['time'])[len(data)-1]
last, apd = last[:19], last[19:]
ctime = datetime.strptime(last, "%Y-%m-%dT%H:%M:%S")
t1, t2 = dToStr(ctime + timedelta(minutes=15))+apd, dToStr(ctime + timedelta(minutes=30)) + apd
outliers = pd.DataFrame({'time': ['01:11:52', '01:11:53'], 'level': [240, 20]})
odata = copy.deepcopy(data)
random_indices = np.random.randint(0, len(data), size=len(outliers))
# Insert each entry of the outliers dataframe at random indices in the larger dataframe
for i, row in outliers.iterrows():
nrow = pd.DataFrame([row])
odata = pd.concat([data.iloc[:random_indices[i]], nrow, data.iloc[random_indices[i]:]], ignore_index = True)
display(data)
| time | level | interpolated | |
|---|---|---|---|
| 0 | 2022-03-01T00:15:00-08:00 | 141.752046 | False |
| 0 | 2022-03-01T00:30:00-08:00 | 130.740758 | False |
| 0 | 2022-03-01T00:45:00-08:00 | 140.253880 | True |
| 0 | 2022-03-01T01:00:00-08:00 | 138.751716 | False |
| 0 | 2022-03-01T01:15:00-08:00 | 137.299749 | False |
| ... | ... | ... | ... |
| 0 | 2022-06-17T22:45:00-08:00 | 127.744215 | False |
| 0 | 2022-06-17T23:00:00-08:00 | 125.898526 | False |
| 0 | 2022-06-17T23:15:00-08:00 | 136.808757 | False |
| 0 | 2022-06-17T23:30:00-08:00 | 125.820452 | False |
| 0 | 2022-06-17T23:45:00-08:00 | 120.911584 | True |
10463 rows × 3 columns
Now we need a method to find these outliers. While there are many known methods for finding outliers, one of the quickest ways is to use something known as a z-score.
The formula for the z-score is
Z = (data point - mean of data set) / standard deviation
We will compute this formula on every data point in our data set, giving us a massive list of z-score values.
What the z score tells us is how far, in terms of standard deviations, any one data point is from the mean. In statistics, data points at increasing standard deviations away from the mean are less likely to occur. This means that the greater our z-score, the more unlikely for the data point to be real.
We can decide what distance from the mean constitutes an outlier by setting the threshold value, so if a data point’s z-score is above that threshold we might guess it to be an outlier.
One intuitive idea is that the glucose levels should not vary too much from one second to the next. To capitalize on this, we can improve our analysis by first calculating the distance each glucose level sample is away from neighboring glucose level samples. We can then use the z-scores to determine which glucose levels differ from their neighbors by an unusually large margin. These will likely be our outliers.
[18]:
import math
import pandas as pd
nx = odata["level"]
distances = []
f = lambda x: x**2
#take the current count, subtract it by the points to its left and right some distance, then
#square each of these differences, add, then sqrt
for l1, l2, m, r1, r2 in zip(nx[:-4],nx[1:-3],nx[2:-2],nx[3:-1],nx[4:]):
difference = math.sqrt(f(m-l1)+f(m-l2)+f(m-r1)+f(m-r2))
distances.append(difference)
Now we can calculate our z-scores for the glucose levels data in one training session, and identify those data points that seem like outliers. Luckily, we can save some time by using the z-score function from scipy’s statistics library, which will give us a z-score calculation for all data points.
[19]:
from scipy import stats
import pandas as pd
import numpy as np
threshold = 3 #@param{type: "integer"}
z_scores = np.abs(stats.zscore(nx))
odata['z'] = z_scores
lcs = list((odata.loc[odata['z'] > threshold]).index)
print((odata.loc[odata['z'] > threshold]))
cx = list(odata["level"].values)
for idx in odata.index[odata['z'] > threshold].tolist():
vals = cx[idx-3:idx-2]+cx[idx-2:idx-1]+cx[idx+1:idx+2]+cx[idx+2:idx+3]
mean_val = np.mean(vals)
odata.loc[idx, 'level'] = mean_val
time level interpolated z
3912 01:11:53 20.0 NaN 4.364198
It looks like the code was able to successfully identify the two outliers!
We can also visualize the two outliers by locating them in a graph of the glucose levels. Run the cell below to check it out!
[20]:
# Plot glucose data with conditional outliers highlighted (Plot adapted from the Biostrap notebook: https://colab.research.google.com/github/Stanford-Health/wearable-notebooks/blob/main/notebooks/biostrap_evo.ipynb#scrollTo=ysL4wqBcGC7j)
plt.figure(figsize=(10, 6))
plt.plot(odata['level'], label='level data', alpha=0.7)
plt.scatter(random_indices, outliers['level'], color='r', label='Conditional Outliers')
plt.legend()
plt.title('Glucose Data with Conditional Outliers Highlighted')
plt.xlabel('Time Index')
plt.ylabel('Glucose Level')
plt.show()
9. Data Analysis§
9.1: Blood glucose level vs. time of day (day vs. night)§
Since people (generally) sleep at night and eat during the day, one hypothesis is that glucose values will be different during the day compared to the night. Let’s see if this is true. First, we will get the glucose values for each day and separate them into day time and night time values. We define day time as 9AM to 12AM (midnight) and night time as 12AM (midnight) to 9 am here, although these values can be adjusted as dayStart and dayEnd in the code.
As a note, our implementation here examines all of our extracted data, so the visualization presented here may span multiple days or even weeks.
[21]:
from datetime import time
import scipy.stats as stats
df = data
dayStart = time(9, 00, 00)
dayEnd = time(0, 00, 00)
allTimes = pd.to_datetime(df.loc[:,'time']).dt.time
# create a mask for the day/night
if dayStart < dayEnd:
mask = (allTimes >= dayStart) & (allTimes <= dayEnd)
else:
mask = (allTimes >= dayStart) | (allTimes <= dayEnd)
# separate dataframe into day and night using mask
df["Time of Day"] = mask
df["Time of Day"] = df["Time of Day"].map({True: 'Day', False: 'Night'})
Let’s use seaborn to generate a scatter plot to take an initial look.
[22]:
import seaborn as sns
sns.catplot(x="Time of Day", y="level", data=df, palette=sns.color_palette('pastel'))
[22]:
<seaborn.axisgrid.FacetGrid at 0x7ffb549d8ac0>
Now, since we’re comparing two means, we could use a one-way ANOVA (analysis of variance). For this to be a valid analysis, we have to ensure the data is independent, normally distributed, and homoscedastic.
Let’s check normality first.
[23]:
import matplotlib.pyplot as plt
# plot histogram for datapoints during the day to verify normality
plt.hist(df.loc[df["Time of Day"] == 'Day']['level'])
plt.title("Day Time")
plt.xlabel("Level")
plt.ylabel("Number of Observations")
plt.show()
# plot histogram for datapoints during the night to verify normality
plt.hist(df.loc[df["Time of Day"] == 'Night']['level'], color="orange")
plt.title("Night Time")
plt.xlabel("Level")
plt.ylabel("Number of Observations")
plt.show()
Since the data do not look normally distributed, we can use a Kruskal-Wallis test instead of an ANOVA.
[24]:
from scipy import stats
stats.kruskal(df.loc[df["Time of Day"] == 'Day']['level'],df.loc[df["Time of Day"] == 'Night']['level'])
[24]:
KruskalResult(statistic=337.0689127447804, pvalue=2.774707917315403e-75)
Thus we can reject the null hypothesis that the population medians do not significantly differ (p<0.05), and that night time values are different from day time values.
9.2: Blood glucose level after change of behavior§
Does a patient change their eating habits throughout the course of the study When a patient changes their eating habits in order to keep their glucose levels in check, does it work? One analysis we could do is, given the date at which the patient started implementing changes in eating habits (like eating less sweets), determine if there is a significant difference in the blood glucose levels throughout the day (140 mg/dL).
For this type of question, we will use student’s T-Test, a statistical test used to compare and determine if there exists a meaningful difference between the means of some value in two separate groups. Here our first group will be the set of data before the patient changed their eating habits, and the second group will be the data after the patient changed their eating habits.
We will thus separate our extracted glucose levels data into our two groups based on the date of behavior change, and then perform a T-Test across the means of the two groups of data. This analysis will allow us to understand how effective a change in behavior was on a participant’s glucose levels.
The date of behavioral change below can be modified as needed.
[42]:
#@title Enter date of eating habit change
date_of_change = "2022-03-30T12:00:00" #@param {type:"string"}
[43]:
import pandas as pd
import numpy as np
from scipy.stats import ttest_ind
from datetime import timedelta, datetime
import re
df = data
pat = date_of_change + "*(.+)"
# find the index of behavior change to split thed ata into two groups
split = list(df["time"].str.match(pat).values)
split = split.index(True)
# this function splits the data into f (first), and s (second), representing the two groups
def differ(df):
rows = len(df.index)
f = df.iloc[range(split)]["level"]
s = df.iloc[range(split,rows)]["level"]
return ttest_ind(f,s)
print(differ(df))
TtestResult(statistic=7.248590332728824, pvalue=4.5087253582105376e-13, df=10461.0)
Since the t-test returned a positive value, this means the sample mean in the first half of the data is greater than the sample mean in the second half of the data. This might lead us to hypothesize that the patient has indeed changed their eating habits for the better! But is this the case?
Examining the pvalue < 0.05 implies that the probability of this result occuring by under the assumption the patient did not change behaviors is less than 5%. This implies our result was statistically significant and that we should accept our hypothesis. We can conclude then that the patient did make a behavior change at the specified date!