{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"source": [
"# Polar Verity Sense: Guide to data extraction and analysis"
],
"metadata": {
"id": "LgdgFSzo9SYs"
}
},
{
"cell_type": "markdown",
"source": [
"
\n",
"\n"
],
"metadata": {
"id": "H7zTf9DPDIXS"
}
},
{
"cell_type": "markdown",
"source": [
"*^ A picture of the polar verity sense heart rate monitor ^*"
],
"metadata": {
"id": "naL5gEN2DaZ4"
}
},
{
"cell_type": "markdown",
"source": [
"The [Polar Verity Sense](https://www.polar.com/en/products/accessories/polar-verity-sense) is a $89 armband heart rate sensor that has often been reviewed as a viable alternative to ECG chest-strap style monitors. Portable and comfortable, it can be worn for exercises indoors, outdoors, and even in the pool.\n",
"\n",
"Beyond its accuracy as a heart rate monitor, the Verity Sense's utility lies in its interface with Polar's official app, which helps keep track of data collected from the sensor as well as providing plenty of useful visualizations and analytics. \n",
"\n",
"In the following sections, we will present what we have learned from using the Verity Sense and how to extract its data for visualizations and statistical analyses. With the wearipedia library, this process will only require your username and password!"
],
"metadata": {
"id": "XI6MGYdH9-pH"
}
},
{
"cell_type": "markdown",
"source": [
"We will be able to extract the following parameters (in bold are parameters we will be using in this notebook). Per Activity means one measurement through a single session of exercise. Per second means one measurement every second throughout an exercise.\n",
"\n",
"Parameter Name | Sampling Frequency \n",
"-------------------|-----------------\n",
"Calories | Per Activity\n",
"Duration | Per Activity\n",
"Sessions | Per day\n",
"Continuous Heart Rate | Per second \n"
],
"metadata": {
"id": "qfKAFMwf-cm2"
}
},
{
"cell_type": "markdown",
"source": [
"In this guide, we sequentially cover the following **nine** topics:\n",
"\n",
"1. **Set up**
\n",
"2. **Authentication/Authorization**
\n",
" - Requires only username and password, no OAuth.\n",
"3. **Data extraction**
\n",
" - We get data via `wearipedia` in a couple lines of code.\n",
"4. **Data Exporting**\n",
" - We export all of this data to file formats compatible by R, Excel, and MatLab.\n",
"5. **Adherence**\n",
" - We simulate non-adherence by dynamically removing datapoints from our simulated data.\n",
"6. **Visualization**\n",
" - We create a simple plot to visualize our data.\n",
"7. **Data visualization & analysis**
\n",
" - 7.1 Heart Rate Zones during an Exercise Session
\n",
" - 7.2 Visualizing Heart Rate Averages and Sessions over a Month
\n",
" - 7.3 Visualizing Continuous Heart Rate data
\n",
"8. **Outlier Detection and Data Cleaning**\n",
" - We detect outliers in our data and filter them out.\n",
"9. **Data Analysis**
\n",
" - 9.1 Analyzing correlation between Average Heart Rate and Calories Burned per Minute
\n",
" - 9.2 Analyzing correlation between Duration of Workouts and Calories Burned\n",
" - 9.3 Analyzing how activity intensity compares in first half and second half of a session\n",
"\n",
"**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.**\n",
"\n",
"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. That being said, we were unable to thoroughly test the timezone functionality, though, since we only have one account, so beware."
],
"metadata": {
"id": "I_NoC7rF_G64"
}
},
{
"cell_type": "markdown",
"source": [
"\n",
"\n",
"# 1. Set up"
],
"metadata": {
"id": "xKwooMsw90yZ"
}
},
{
"cell_type": "markdown",
"source": [
"## Participant Setup"
],
"metadata": {
"id": "QbaGwQiCcOHz"
}
},
{
"cell_type": "markdown",
"source": [
"Dear Participant,\n",
"\n",
"Once you unbox your Polar Verity Sense, please set up the device by following the video:\n",
"\n",
"\n",
"* Video Guide: https://www.youtube.com/watch?v=wiA_ucJJV7Y&t\n",
"\n",
"\n",
"Make sure that your phone is paired to it using the Polar Flow login credentials (email and password) given to you by the study coordinator.\n",
"\n",
"Best,\n",
"\n",
"Wearipedia"
],
"metadata": {
"id": "Hj6CmfBJcTHY"
}
},
{
"cell_type": "markdown",
"source": [
"## Data Receiver Setup"
],
"metadata": {
"id": "yNaCJSmVcRM5"
}
},
{
"cell_type": "markdown",
"source": [
"Please follow the below steps:\n",
"\n",
"1. Create an email address for the participant, for example `foo@email.com`\n",
"2. Create a Polar Flow account with the email `foo@email.com` and some random password.\n",
"3. Keep `foo@email.com` and password stored somewhere safe.\n",
"4. Distribute the device to the participant and instruct them to follow the participant setup letter above.\n",
"5. Install the `wearipedia` Python package to easily extract data from this device."
],
"metadata": {
"id": "E1DMSOoddDFK"
}
},
{
"cell_type": "code",
"source": [
"!pip install git+https://TrafficCop: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@github.com/TrafficCop/wearipedia.git"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "-MYY4ukfbH-K",
"outputId": "10d4297a-c930-4abf-bf6b-385b9e8f362c"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
"Collecting git+https://TrafficCop:****@github.com/TrafficCop/wearipedia.git\n",
" Cloning https://TrafficCop:****@github.com/TrafficCop/wearipedia.git to /tmp/pip-req-build-3248x7v1\n",
" Running command git clone --filter=blob:none --quiet 'https://TrafficCop:****@github.com/TrafficCop/wearipedia.git' /tmp/pip-req-build-3248x7v1\n",
" Resolved https://TrafficCop:****@github.com/TrafficCop/wearipedia.git to commit b570a805866b7c6f8ea7b57b85b37e1b29106f97\n",
" Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n",
" Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n",
" Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n",
"Collecting garminconnect<0.2.0,>=0.1.48\n",
" Downloading garminconnect-0.1.50.tar.gz (17 kB)\n",
" Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
"Collecting typer[all]<0.7.0,>=0.6.1\n",
" Downloading typer-0.6.1-py3-none-any.whl (38 kB)\n",
"Requirement already satisfied: pandas<2.0,>=1.1 in /usr/local/lib/python3.8/dist-packages (from wearipedia==0.1.0) (1.3.5)\n",
"Requirement already satisfied: scipy<2.0,>=1.6 in /usr/local/lib/python3.8/dist-packages (from wearipedia==0.1.0) (1.7.3)\n",
"Requirement already satisfied: tqdm<5.0.0,>=4.64.1 in /usr/local/lib/python3.8/dist-packages (from wearipedia==0.1.0) (4.64.1)\n",
"Collecting rich<13.0.0,>=12.6.0\n",
" Downloading rich-12.6.0-py3-none-any.whl (237 kB)\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m237.5/237.5 KB\u001b[0m \u001b[31m9.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25hCollecting wget<4.0,>=3.2\n",
" Downloading wget-3.2.zip (10 kB)\n",
" Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
"Requirement already satisfied: requests in /usr/local/lib/python3.8/dist-packages (from garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.0) (2.25.1)\n",
"Collecting cloudscraper\n",
" Downloading cloudscraper-1.2.68-py2.py3-none-any.whl (98 kB)\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m98.6/98.6 KB\u001b[0m \u001b[31m4.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25hRequirement already satisfied: numpy>=1.17.3 in /usr/local/lib/python3.8/dist-packages (from pandas<2.0,>=1.1->wearipedia==0.1.0) (1.21.6)\n",
"Requirement already satisfied: pytz>=2017.3 in /usr/local/lib/python3.8/dist-packages (from pandas<2.0,>=1.1->wearipedia==0.1.0) (2022.7)\n",
"Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.8/dist-packages (from pandas<2.0,>=1.1->wearipedia==0.1.0) (2.8.2)\n",
"Requirement already satisfied: pygments<3.0.0,>=2.6.0 in /usr/local/lib/python3.8/dist-packages (from rich<13.0.0,>=12.6.0->wearipedia==0.1.0) (2.6.1)\n",
"Requirement already satisfied: typing-extensions<5.0,>=4.0.0 in /usr/local/lib/python3.8/dist-packages (from rich<13.0.0,>=12.6.0->wearipedia==0.1.0) (4.4.0)\n",
"Collecting commonmark<0.10.0,>=0.9.0\n",
" Downloading commonmark-0.9.1-py2.py3-none-any.whl (51 kB)\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m51.1/51.1 KB\u001b[0m \u001b[31m3.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25hRequirement already satisfied: click<9.0.0,>=7.1.1 in /usr/local/lib/python3.8/dist-packages (from typer[all]<0.7.0,>=0.6.1->wearipedia==0.1.0) (7.1.2)\n",
"Collecting shellingham<2.0.0,>=1.3.0\n",
" Downloading shellingham-1.5.0.post1-py2.py3-none-any.whl (9.4 kB)\n",
"Collecting colorama<0.5.0,>=0.4.3\n",
" Downloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)\n",
"Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.8/dist-packages (from python-dateutil>=2.7.3->pandas<2.0,>=1.1->wearipedia==0.1.0) (1.15.0)\n",
"Collecting requests-toolbelt>=0.9.1\n",
" Downloading requests_toolbelt-0.10.1-py2.py3-none-any.whl (54 kB)\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m54.5/54.5 KB\u001b[0m \u001b[31m2.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25hRequirement already satisfied: pyparsing>=2.4.7 in /usr/local/lib/python3.8/dist-packages (from cloudscraper->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.0) (3.0.9)\n",
"Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.8/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.0) (1.24.3)\n",
"Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.8/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.0) (2.10)\n",
"Requirement already satisfied: chardet<5,>=3.0.2 in /usr/local/lib/python3.8/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.0) (4.0.0)\n",
"Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.8/dist-packages (from requests->garminconnect<0.2.0,>=0.1.48->wearipedia==0.1.0) (2022.12.7)\n",
"Building wheels for collected packages: wearipedia, garminconnect, wget\n",
" Building wheel for wearipedia (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n",
" Created wheel for wearipedia: filename=wearipedia-0.1.0-py3-none-any.whl size=42684 sha256=687eb2dbca8a929f258256968d10bbe428c33415380afa1bb62acf64cefc1762\n",
" Stored in directory: /tmp/pip-ephem-wheel-cache-umikwsiy/wheels/24/85/35/40c88fbefd67ae05ca8dd55adf3f437705bfedb5c296ddd98b\n",
" Building wheel for garminconnect (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
" Created wheel for garminconnect: filename=garminconnect-0.1.50-py3-none-any.whl size=13423 sha256=b9e9de6bacdbbedc26882dd511c22bc0070d8e6342dc83be338df9219726d3eb\n",
" Stored in directory: /root/.cache/pip/wheels/2b/72/01/577425dabe75aecd099d4f6e4afb2b4db7e4bfe3db80c4b5a5\n",
" Building wheel for wget (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
" Created wheel for wget: filename=wget-3.2-py3-none-any.whl size=9674 sha256=96ba01d4ea9ac4e43ca5e954cfc686215449588b2a75f40ec02d37ab1c80a58b\n",
" Stored in directory: /root/.cache/pip/wheels/bd/a8/c3/3cf2c14a1837a4e04bd98631724e81f33f462d86a1d895fae0\n",
"Successfully built wearipedia garminconnect wget\n",
"Installing collected packages: wget, commonmark, typer, shellingham, rich, colorama, requests-toolbelt, cloudscraper, garminconnect, wearipedia\n",
" Attempting uninstall: typer\n",
" Found existing installation: typer 0.7.0\n",
" Uninstalling typer-0.7.0:\n",
" Successfully uninstalled typer-0.7.0\n",
"Successfully installed cloudscraper-1.2.68 colorama-0.4.6 commonmark-0.9.1 garminconnect-0.1.50 requests-toolbelt-0.10.1 rich-12.6.0 shellingham-1.5.0.post1 typer-0.6.1 wearipedia-0.1.0 wget-3.2\n"
]
}
]
},
{
"cell_type": "markdown",
"source": [
"**For cleaning up old installations only:**"
],
"metadata": {
"id": "iuKAZSPkBIq7"
}
},
{
"cell_type": "code",
"source": [
"!pip uninstall wearipedia"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "0zK0ldtQuxok",
"outputId": "60116cc6-6da8-4928-d50a-29f45d729ba0"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Found existing installation: wearipedia 0.1.0\n",
"Uninstalling wearipedia-0.1.0:\n",
" Would remove:\n",
" /usr/local/bin/wearipedia\n",
" /usr/local/lib/python3.8/dist-packages/wearipedia-0.1.0.dist-info/*\n",
" /usr/local/lib/python3.8/dist-packages/wearipedia/*\n",
"Proceed (Y/n)? Y\n",
" Successfully uninstalled wearipedia-0.1.0\n"
]
}
]
},
{
"cell_type": "markdown",
"source": [
"#2. Authentication/Authorization\n",
"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 Polar Verity Sense device. We'll use this username and password to extract the data in the sections below."
],
"metadata": {
"id": "xVLQb6ux-hDj"
}
},
{
"cell_type": "code",
"source": [
"#@title Enter Polar login credentials\n",
"\n",
"email_address = '' #@param {type:\"string\"}\n",
"password = ''#@param {type:\"string\"}"
],
"metadata": {
"id": "_45KxrBEGbfX"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# 3. Data extraction"
],
"metadata": {
"id": "q116WA9g-x1J"
}
},
{
"cell_type": "markdown",
"source": [
"Data can be extracted via [wearipedia](https://github.com/Stanford-Health/wearipedia/), our open-source Python package that unifies dozens of complex wearable device APIs into one simple, common interface.\n",
"\n",
"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."
],
"metadata": {
"id": "H4uekNkDKQGo"
}
},
{
"cell_type": "code",
"source": [
"#@title Enter start and end dates (in the format yyyy-mm-dd)\n",
"\n",
"#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\n",
"start_date='2022-03-01' #@param {type:\"string\"}\n",
"end_date='2022-06-11' #@param {type:\"string\"}\n",
"synthetic = True #@param {type:\"boolean\"}"
],
"metadata": {
"id": "I_v0JMCAenpX"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"**CHANGES TO SYNTHETIC THAT NEED TO BE DONE**\n",
"1. Ability to support different range of custom dates"
],
"metadata": {
"id": "z6YDmBrQALQk"
}
},
{
"cell_type": "code",
"source": [
"import wearipedia\n",
"\n",
"device = wearipedia.get_device(\"polar/verity_sense\")\n",
"\n",
"if not synthetic:\n",
" device.authenticate({\"email\": email_address, \"password\": password})\n",
" params = {\"start_date\": start_date, \"end_date\": end_date}\n",
" data = device.get_data(\"sessions\", params = params)\n",
"else:\n",
" params = {\"start_date\": start_date, \"end_date\": end_date}\n",
" data = device.get_data(\"sessions\", params = params)"
],
"metadata": {
"id": "haj-SgI9fEJY",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "d036f3d8-958a-4eb1-c800-c9d8f87dc8d8"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stderr",
"text": [
"100%|██████████| 108/108 [00:00<00:00, 8073.73it/s]\n",
"100%|██████████| 108/108 [00:06<00:00, 17.41it/s]\n"
]
}
]
},
{
"cell_type": "markdown",
"source": [
"To get a list of the days that actually have data, we can list out the keys for the returned data. Note it might not be feasible to actually print out the entire set of returned data, as it can be quite large."
],
"metadata": {
"id": "HwWvlNBLEhyE"
}
},
{
"cell_type": "code",
"source": [
"print(data.keys())"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "D5G_5jR6_BAp",
"outputId": "a887766b-b612-4768-9752-222e6253685e"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"dict_keys(['2022-03-01', '2022-03-02', '2022-04-05', '2022-03-23', '2022-04-20', '2022-05-11', '2022-03-06', '2022-05-18', '2022-04-18', '2022-05-05', '2022-04-09', '2022-04-06', '2022-05-19', '2022-03-30', '2022-06-08', '2022-06-04', '2022-03-21', '2022-04-19', '2022-04-15', '2022-04-14', '2022-03-22', '2022-05-12', '2022-06-06', '2022-03-11', '2022-03-31', '2022-06-02', '2022-05-26', '2022-03-10', '2022-05-03', '2022-04-23', '2022-06-11', '2022-04-03', '2022-06-03', '2022-05-22', '2022-05-31', '2022-03-16', '2022-05-17', '2022-04-10', '2022-06-05', '2022-05-15', '2022-04-27', '2022-05-08', '2022-03-19', '2022-05-20', '2022-06-01', '2022-04-26', '2022-05-16', '2022-05-21', '2022-04-12', '2022-03-04', '2022-03-28', '2022-04-24', '2022-04-01', '2022-04-25', '2022-04-07', '2022-03-12', '2022-06-10', '2022-03-20', '2022-03-26', '2022-03-27', '2022-05-28', '2022-04-30'])\n"
]
}
]
},
{
"cell_type": "markdown",
"source": [
"# 4. Data Exporting"
],
"metadata": {
"id": "cfUa31-NJLH6"
}
},
{
"cell_type": "markdown",
"source": [
"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.\n",
"\n",
"## Exporting to JSON (R, Matlab, etc.)\n",
"\n",
"Exporting to JSON is fairly simple. We export each datatype separately and also export a complete version that includes all simultaneously."
],
"metadata": {
"id": "FY0XyxmxdowU"
}
},
{
"cell_type": "code",
"source": [
"import json\n",
"\n",
"hrs = [data[k]['heart_rates'] for k in data.keys()]\n",
"calories = [data[k]['calories'] for k in data.keys()]\n",
"hr_avgs = [sum(data[k]['heart_rates']) / (data[k]['minutes'] * 60) for k in data.keys()]\n",
"durations = [data[k]['minutes'] for k in data.keys()]\n",
"\n",
"json.dump(list(data.keys()), open(\"dates.json\", \"w\"))\n",
"json.dump(calories, open(\"calories.json\", \"w\"))\n",
"json.dump(hrs, open(\"hrs.json\", \"w\"))\n",
"json.dump(hr_avgs, open(\"hr_avgs.json\", \"w\"))\n",
"json.dump(durations, open(\"durations.json\", \"w\"))\n",
"\n",
"complete = {\n",
" \"dates\": list(data.keys()),\n",
" \"calories\": calories,\n",
" \"hrs\": hrs,\n",
" \"hr_avgs\": hr_avgs,\n",
" \"durations\": durations,\n",
"}\n",
"\n",
"\n",
"json.dump(complete, open(\"complete.json\", \"w\"))"
],
"metadata": {
"id": "MUP8QbnmtdF6"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Exporting to CSV and XLSX (Excel, Google Sheets, R, Matlab, etc.)"
],
"metadata": {
"id": "lywBjc_DBDTw"
}
},
{
"cell_type": "markdown",
"source": [
"Exporting to CSV/XLSX will require us to first process the data into a pandas dataframe format, which can then be converted into our desired file type.\n",
"\n",
"Here we will export three separate files: the first will contain heart rate data (labeled by date and time), and the second will contain daily summary data such as calories, average heart rate, and minutes (labeled by date). Additionally, we will make a third dataframe containing only heart rate data for one session (of your choice) to help us with further visualizations and analysis in the following sections of this notebook."
],
"metadata": {
"id": "Z6wODk-cBIW_"
}
},
{
"cell_type": "code",
"source": [
"day = \"2022-03-01\" #@param {type: \"string\"}\n",
"\n",
"# set the day of interest automatically if not specified\n",
"if day == \"\":\n",
" day = list(data.keys())[0]"
],
"metadata": {
"id": "ZwrOmk0GFVGK"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"import pandas as pd\n",
"from datetime import timedelta\n",
"from datetime import datetime\n",
"import numpy as np\n",
"import copy\n",
"\n",
"# Third dataframe and list of dataframes:\n",
"hr_df = None\n",
"hr_dfs = []\n",
"\n",
"# 1. First dataframe\n",
"all_df = None\n",
"for key in data.keys():\n",
" duration = data[key]['minutes']*60\n",
" start_time = timedelta(hours=0, minutes=0, seconds=0)\n",
" times = [str(start_time + timedelta(seconds=i)).zfill(8) for i in range(int(duration))]\n",
" tdf = pd.DataFrame(zip(times,data[key]['heart_rates']),columns=['time','bpm'])\n",
" mapping = lambda x: np.datetime64(key+ \" \" + x)\n",
" tdf[\"time\"] = tdf[\"time\"].apply(mapping)\n",
" if key == day:\n",
" hr_df = tdf\n",
" if type(all_df) == type(None):\n",
" all_df = tdf\n",
" else:\n",
" all_df = all_df.append(tdf)\n",
" hr_dfs.append(copy.deepcopy(tdf))\n",
"#print(\"DataFrame 1: All heart rates\")\n",
"#display(df1)\n",
"\n",
"# 2. Second dataframe\n",
"df2 = None\n",
"for key in data.keys():\n",
" avg_rate = sum(data[key]['heart_rates']) / (data[key]['minutes'] * 60)\n",
" if type(df2) == type(None):\n",
" df2 = pd.DataFrame([[key,data[key]['minutes'],data[key]['calories'], avg_rate]],columns=['day','minutes','calories', 'avg_rate'])\n",
" else:\n",
" tdf = pd.DataFrame([[key,data[key]['minutes'],data[key]['calories'], avg_rate]],columns=['day','minutes','calories', 'avg_rate'])\n",
" df2 = df2.append(tdf)\n",
"#print(\"DataFrame 2: Summary Data\")\n",
"#display(df2)\n",
"\n",
"# 3. Third dataframe\n",
"print(\"DataFrame 3: Single session heart rate\")\n",
"display(hr_df)"
],
"metadata": {
"id": "S2T9oM7Vdz1w",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 441
},
"outputId": "71a8d319-6327-44bc-e707-dcea611f393b"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"DataFrame 3: Single session heart rate\n"
]
},
{
"output_type": "display_data",
"data": {
"text/plain": [
" time bpm\n",
"0 2022-03-01 00:00:00 94.110535\n",
"1 2022-03-01 00:00:01 91.859208\n",
"2 2022-03-01 00:00:02 93.210171\n",
"3 2022-03-01 00:00:03 92.384613\n",
"4 2022-03-01 00:00:04 94.371856\n",
"... ... ...\n",
"3295 2022-03-01 00:54:55 137.121507\n",
"3296 2022-03-01 00:54:56 137.750669\n",
"3297 2022-03-01 00:54:57 138.724507\n",
"3298 2022-03-01 00:54:58 139.206589\n",
"3299 2022-03-01 00:54:59 138.982279\n",
"\n",
"[3300 rows x 2 columns]"
],
"text/html": [
"\n",
"
| \n", " | time | \n", "bpm | \n", "
|---|---|---|
| 0 | \n", "2022-03-01 00:00:00 | \n", "94.110535 | \n", "
| 1 | \n", "2022-03-01 00:00:01 | \n", "91.859208 | \n", "
| 2 | \n", "2022-03-01 00:00:02 | \n", "93.210171 | \n", "
| 3 | \n", "2022-03-01 00:00:03 | \n", "92.384613 | \n", "
| 4 | \n", "2022-03-01 00:00:04 | \n", "94.371856 | \n", "
| ... | \n", "... | \n", "... | \n", "
| 3295 | \n", "2022-03-01 00:54:55 | \n", "137.121507 | \n", "
| 3296 | \n", "2022-03-01 00:54:56 | \n", "137.750669 | \n", "
| 3297 | \n", "2022-03-01 00:54:57 | \n", "138.724507 | \n", "
| 3298 | \n", "2022-03-01 00:54:58 | \n", "139.206589 | \n", "
| 3299 | \n", "2022-03-01 00:54:59 | \n", "138.982279 | \n", "
3300 rows × 2 columns
\n", "
"
],
"metadata": {
"id": "dN9IAHg5YeMB"
}
},
{
"cell_type": "markdown",
"source": [
"At the bottom (gray) zone is the amount of time spent with heart rate between 100-120 bpm, the next being 120-140 bpm, then 140-160 bpm, 160-180 bpm, and finally 180-200 bpm.\n",
"\n",
"We will want the following data:\n",
"\n",
"* The zones we want to plot\n",
"* Time in each heart rate zone\n",
"\n",
"Polar uses the following zones: 100-120 bpm, 120-140 bpm, 140-160 bpm, 160-180 bpm, 180-200 bpm, which is what we will be using as well. \n",
"\n",
"To get the time spent in each heart rate zone, we can use the pandas groupby function to aggregate the heart rates by zones, and then count the number of entries in each group. Since the continuous heart rate data has an entry for every second, this directly gives us the time spent in each heart rate zone."
],
"metadata": {
"id": "zLaER1dCQO23"
}
},
{
"cell_type": "code",
"source": [
"import pandas as pd\n",
"\n",
"ranges = [100,120,140,160,180,200]\n",
"z1 = hr_df.groupby(pd.cut(hr_df.bpm, ranges), as_index=False).count()\n",
"z2 = pd.to_datetime(z1['bpm'], unit='s')\n",
"z2 = pd.DataFrame(z2.dt.strftime('%H:%M:%S'))\n",
"display(z2['bpm'])"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 121
},
"id": "dhTTlNxHnJTI",
"outputId": "bc71adbe-8e16-4375-ee3c-c0ad913d8f4a"
},
"execution_count": null,
"outputs": [
{
"output_type": "display_data",
"data": {
"text/plain": [
"0 00:05:56\n",
"1 00:27:52\n",
"2 00:12:57\n",
"3 00:06:35\n",
"4 00:03:57\n",
"Name: bpm, dtype: object"
]
},
"metadata": {}
}
]
},
{
"cell_type": "markdown",
"source": [
"z2 is exactly the heart rate zone data, as we wanted. Now we want to set up two more python arrays that we will use as the y-axis labels, shown here. This data is already available in our z2 series, so we can extract them as necessary."
],
"metadata": {
"id": "J3yqY-_zbHNE"
}
},
{
"cell_type": "code",
"source": [
"zones = ['1','2','3','4','5']\n",
"times = [e for e in z1['bpm']][::-1]\n",
"time_labels = [e for e in z2['bpm']]"
],
"metadata": {
"id": "FDnEkmZmNTuX"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"The zones array will be the left side y-axis label, and the times will be the actual data. We can place these in a pandas dataframe so it will be easier to plot using the python matplotlib library."
],
"metadata": {
"id": "oy4glSixQzZo"
}
},
{
"cell_type": "code",
"source": [
"import pandas as pd\n",
"\n",
"d = {\"zones\":zones, \"times\": times}\n",
"df1 = pd.DataFrame(data=d)\n",
"df1.set_index('zones', inplace=True)"
],
"metadata": {
"id": "RSzx9FzvQ5Xa"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"And now we can finally plot the data. We will be using the [horizontal bar plot](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.barh.html) from matplotlib, barh. Since we already have the data we need, we can use barh to directly plot the necessary bars, after which we can apply our desired styling to make the graph look like the one pictured."
],
"metadata": {
"id": "fMODQ5pbRejn"
}
},
{
"cell_type": "code",
"source": [
"from matplotlib import pyplot as plt, patches\n",
"import seaborn as sns\n",
"\n",
"sns.set_theme(style=\"dark\")\n",
"fig1, ax = plt.subplots(figsize=(16,6))\n",
"clrs = [\"#c0c8c8\", \"#46c7ee\", \"#6acc2b\", \"#f9bf1c\", \"#de0f5b\"][::-1] #colors array for the zones\n",
"\n",
"#plot the base graph\n",
"plt. margins(y=0)\n",
"plt.xlim(0, sum(i for i in df1[\"times\"])) #this sets the graph to have its width equal to the total session time\n",
"bar1 = ax.barh(df1.index[::-1], df1[\"times\"][::-1], align=\"center\", height=1., color = clrs[::-1]) #plot the bar graph\n",
"plt.xticks(color='w') #hide the x tick labels\n",
"plt.yticks(color='w')\n",
"ax.tick_params(axis='both', labelsize=25, pad=12.5)\n",
"\n",
"# Adding the times for each zone\n",
"for i in range(len(time_labels)):\n",
" plt.text(.97,0.15 + 0.15*i,time_labels[i],fontsize=18,transform=fig1.transFigure,\n",
" horizontalalignment='center', weight=300)\n",
"\n",
"#add colored backgrounds behind each zone\n",
"rect = patches.Rectangle((0, 0.805), 0.5, 0.194, color='#de0f5b')\n",
"rect2 = patches.Rectangle((0, 0.602), 0.5, 0.198, color='#f9bf1c')\n",
"rect3 = patches.Rectangle((0, 0.402), 0.5, 0.194, color='#6acc2b')\n",
"rect4 = patches.Rectangle((0, 0.202), 0.5, 0.194, color='#46c7ee')\n",
"rect5 = patches.Rectangle((0, 0.0), 0.5, 0.195, color='#c0c8c8')\n",
"\n",
"lax = fig1.add_axes([0.08,0.126,1.,0.752], anchor='NE', zorder=-1)\n",
"lax.add_patch(rect)\n",
"lax.add_patch(rect2)\n",
"lax.add_patch(rect3)\n",
"lax.add_patch(rect4)\n",
"lax.add_patch(rect5)\n",
"lax.axis('off')\n",
"\n",
"#add gray rectangles behind times \n",
"b1 = patches.Rectangle((0, 0.805), 0.5, 0.194, color='#eaeaf2')\n",
"b2 = patches.Rectangle((0, 0.602), 0.5, 0.194, color='#eaeaf2')\n",
"b3 = patches.Rectangle((0, 0.402), 0.5, 0.194, color='#eaeaf2')\n",
"b4 = patches.Rectangle((0, 0.202), 0.5, 0.194, color='#eaeaf2')\n",
"b5 = patches.Rectangle((0, 0.0), 0.5, 0.194, color='#eaeaf2')\n",
"rax = fig1.add_axes([.8,0.126,.42,0.752], anchor='NE', zorder=-1)\n",
"rax.add_patch(b1)\n",
"rax.add_patch(b2)\n",
"rax.add_patch(b3)\n",
"rax.add_patch(b4)\n",
"rax.add_patch(b5)\n",
"rax.axis('off')\n",
"\n",
"#set grid lines separating bars\n",
"ax.set_yticks([0.5,1.5,2.5,3.5,4.5], minor=True)\n",
"ax.yaxis.grid(True, which='minor')"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 315
},
"id": "ln4Hmt2eMaHH",
"outputId": "bad029da-3015-48c0-f957-5158373ecb72"
},
"execution_count": null,
"outputs": [
{
"output_type": "display_data",
"data": {
"text/plain": [
"
"
],
"metadata": {
"id": "Lho0NmMPFfCN"
}
},
{
"cell_type": "markdown",
"source": [
"To start we want to extract the necessary data. This will include: \n",
"\n",
"\n",
"* Dates of exercises\n",
"* Durations of each exercise\n",
"* Average heart rates of each exercise\n",
"\n"
],
"metadata": {
"id": "RBGD5tOUg7zt"
}
},
{
"cell_type": "code",
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"durations = []\n",
"avg_rates = []\n",
"dates = []\n",
"day_array = list(df2['day'])\n",
"rate_array = list(df2['avg_rate'])\n",
"duration_array = list(df2['minutes'])\n",
"for i in range(len(df2['day'])):\n",
" \n",
" if datetime.strptime(day_array[i],'%Y-%M-%d') < (datetime.strptime(start_date,'%Y-%M-%d') + timedelta(days=30)) and datetime.strptime(day_array[i],'%Y-%M-%d') >= datetime.strptime(start_date,'%Y-%M-%d'):\n",
" dates.append(np.datetime64(day_array[i]))\n",
" durations.append(duration_array[i]*60000)\n",
" avg_rates.append(rate_array[i])"
],
"metadata": {
"id": "5FtAHv14hDyF"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Now that we have everything we need, we can move on to plotting the graph! \n",
"\n",
"To do this we will start by plotting a [matplotlib bar graph](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.bar.html), which will represent the durations of each exercise. Then we will overlay this plot with a second graph, the [matplotlib line plot](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html). This is all we need for the base graph!\n",
"\n",
"All that remains after is to add the necessary styling to achieve the graph pictured above."
],
"metadata": {
"id": "1wXrNnH2hD52"
}
},
{
"cell_type": "code",
"source": [
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import matplotlib.dates as mdates\n",
"\n",
"#set the style of the plot, initialize it\n",
"sns.set_style(\"whitegrid\")\n",
"fig, ax = plt.subplots(figsize=(18,8))\n",
"ax.set_facecolor('#f2f2f2')\n",
"\n",
"#before we begin, we need to add in a buffer at either end of the data so we can extend the graph\n",
"rbound = np.datetime64((pd.Timestamp(dates[len(dates)-1]) + pd.DateOffset(days=1)).strftime('%Y-%m-%d'))\n",
"lbound = np.datetime64((pd.Timestamp(dates[0]) - pd.DateOffset(days=1)).strftime('%Y-%m-%d'))\n",
"xdates = list(pd.date_range(start=lbound, end=rbound))\n",
"xdates.append(xdates[len(xdates)-1])\n",
"\n",
"#process and fill our data arrays so that they match dimensions of x axis\n",
"points = {np.datetime64(datetime.strftime(pd.to_datetime(date), '%Y-%m-%d')):(avg_rate,duration) for date, avg_rate, duration in zip(dates,avg_rates,durations)}\n",
"data1 = []\n",
"data2 = {}\n",
"for i in range(len(xdates)):\n",
" cdate = np.datetime64(datetime.strftime(xdates[i],'%Y-%m-%d'))\n",
" if i == 0:\n",
" added = points[np.datetime64(datetime.strftime(xdates[1],'%Y-%m-%d'))][0]\n",
" if cdate in points:\n",
" added = points[cdate][0]\n",
" data1.append(added)\n",
" data2[cdate] = points[cdate][1]/3600000\n",
" else:\n",
" data1.append(added)\n",
" data2[cdate] = 0\n",
"plt.xticks(rotation=45)\n",
"ax2 = ax.twinx()\n",
"\n",
"#plot the bar graph\n",
"ax.bar(data2.keys(),data2.values(), color ='#e71735', width=0.20)\n",
"ax.set_yticks(np.arange(0, max(data2.values())+0.5, 0.5))\n",
"\n",
"#plot the average heart rate\n",
"plt.plot(xdates, data1, color='#e71735', linestyle='dashed')\n",
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))\n",
"\n",
"#clip the graph ends to get our desired look \n",
"axis = plt.axis()\n",
"plt.xlim(xdates[0], xdates[len(xdates)-2])\n",
"ax2.yaxis.grid(False)\n",
"\n",
"#hide ticks\n",
"ax2.tick_params(axis='y', length=0) \n",
"ax.tick_params(axis='y', length=0) \n",
"\n",
"print(points)"
],
"metadata": {
"id": "O8hVSOdCdnMn",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 531
},
"outputId": "3aadf707-1aaa-4775-85dc-9483d7da59dd"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"{numpy.datetime64('2022-03-11'): (148.60438074901958, 2880000), numpy.datetime64('2022-03-14'): (125.03413026104663, 2760000), numpy.datetime64('2022-03-27'): (90.8876123319256, 2700000), numpy.datetime64('2022-03-17'): (109.82817716867521, 2700000), numpy.datetime64('2022-05-15'): (134.32337357988095, 3000000), numpy.datetime64('2022-03-16'): (101.67126572452109, 3000000), numpy.datetime64('2022-03-28'): (89.30156282473392, 2880000), numpy.datetime64('2022-03-22'): (100.26106997019164, 3180000), numpy.datetime64('2022-03-13'): (77.30444661402656, 2760000), numpy.datetime64('2022-03-26'): (117.12185927449448, 2820000), numpy.datetime64('2022-04-30'): (100.18888458284101, 2700000), numpy.datetime64('2022-03-10'): (102.11270955167122, 3120000), numpy.datetime64('2022-04-20'): (100.23515912232084, 2940000), numpy.datetime64('2022-03-15'): (151.51392114667934, 3240000), numpy.datetime64('2022-05-12'): (122.15268124430676, 3240000), numpy.datetime64('2022-03-23'): (158.26683088729507, 2820000), numpy.datetime64('2022-04-15'): (131.07266595834122, 2760000), numpy.datetime64('2022-06-15'): (112.44969891174973, 2880000), numpy.datetime64('2022-05-14'): (133.4350159451914, 3180000), numpy.datetime64('2022-06-16'): (85.15969462663257, 2760000), numpy.datetime64('2022-05-26'): (91.9841307333696, 2700000), numpy.datetime64('2022-04-28'): (125.21753249314493, 2940000), numpy.datetime64('2022-05-27'): (107.13397021692715, 2760000), numpy.datetime64('2022-05-21'): (143.3458155760211, 3120000), numpy.datetime64('2022-04-25'): (151.4953087021972, 3360000), numpy.datetime64('2022-03-30'): (153.00516777290125, 3180000), numpy.datetime64('2022-05-29'): (103.10242645848972, 2760000), numpy.datetime64('2022-05-28'): (146.25693522751908, 3180000), numpy.datetime64('2022-04-10'): (86.58026740402435, 2940000), numpy.datetime64('2022-04-17'): (132.33100387749926, 2820000), numpy.datetime64('2022-03-29'): (119.68015580244948, 3360000), numpy.datetime64('2022-04-29'): (161.85980034725586, 2940000), numpy.datetime64('2022-03-21'): (139.88037913822555, 2820000), numpy.datetime64('2022-05-22'): (151.24360941968666, 2760000), numpy.datetime64('2022-05-16'): (126.88316898584463, 2880000), numpy.datetime64('2022-03-20'): (140.09253679690738, 2760000), numpy.datetime64('2022-04-11'): (125.91470056917359, 2760000), numpy.datetime64('2022-03-31'): (127.3760588913292, 3060000), numpy.datetime64('2022-05-23'): (147.5135913867359, 3000000), numpy.datetime64('2022-06-13'): (106.68748050611664, 2820000), numpy.datetime64('2022-04-12'): (109.3653383639486, 3360000), numpy.datetime64('2022-03-24'): (115.72448231096944, 3120000), numpy.datetime64('2022-04-19'): (83.08440428625981, 3240000), numpy.datetime64('2022-03-12'): (110.22994511488777, 3300000), numpy.datetime64('2022-05-30'): (87.58760730290345, 2880000), numpy.datetime64('2022-05-10'): (119.65725556045116, 3060000), numpy.datetime64('2022-04-16'): (126.39131742352002, 3000000), numpy.datetime64('2022-05-24'): (87.3043198208121, 2820000), numpy.datetime64('2022-03-19'): (153.789752043585, 3240000), numpy.datetime64('2022-05-25'): (114.18960726652924, 3000000), numpy.datetime64('2022-05-11'): (149.4561437472248, 3240000), numpy.datetime64('2022-05-13'): (132.18352710565964, 3240000), numpy.datetime64('2022-05-17'): (107.4544674966249, 2880000), numpy.datetime64('2022-06-11'): (110.7521333275521, 2820000), numpy.datetime64('2022-04-27'): (141.06358635016585, 2880000), numpy.datetime64('2022-06-09'): (74.70970628375426, 2940000), numpy.datetime64('2022-06-10'): (74.80748491633975, 2880000), numpy.datetime64('2022-03-25'): (120.97788470827379, 3180000), numpy.datetime64('2022-04-22'): (108.7103048662486, 2880000), numpy.datetime64('2022-05-31'): (127.00566093187098, 3120000), numpy.datetime64('2022-04-24'): (147.07278692696016, 3180000), numpy.datetime64('2022-05-20'): (157.0421130777547, 3000000), numpy.datetime64('2022-04-18'): (136.09561544189722, 3180000), numpy.datetime64('2022-06-12'): (106.69587741212065, 3300000), numpy.datetime64('2022-06-14'): (123.70767138220371, 3420000), numpy.datetime64('2022-04-23'): (111.68947949702499, 3540000), numpy.datetime64('2022-04-14'): (107.27921624551617, 3420000), numpy.datetime64('2022-04-21'): (128.67343246012445, 3240000), numpy.datetime64('2022-04-13'): (129.66215608850723, 3360000)}\n"
]
},
{
"output_type": "display_data",
"data": {
"text/plain": [
"
"
],
"metadata": {
"id": "15aeF03j3N61"
}
},
{
"cell_type": "markdown",
"source": [
"In this plot the line plot represents the heart rate (bpm) at every second of the exercise session, and the colored regions indicate the \"zone\" in which the heart rate (bpm) lies. We will need:\n",
"\n",
"\n",
"* Continuous heart rate data\n"
],
"metadata": {
"id": "hNntOOC0gqq3"
}
},
{
"cell_type": "code",
"source": [
"import pandas as pd\n",
"\n",
"heart_rts = list(hr_df[\"bpm\"]) \n",
"#we must add in '2000-01-10' as a dummy date to help with formatting concerns\n",
"times = list(hr_df[\"time\"]) "
],
"metadata": {
"id": "zH1L-IvOHlZK"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We will now graph this using the matplotlib [line plot](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html) function, which will give us the base graph, after which we can focus on aesthetics."
],
"metadata": {
"id": "zFHHY6vOIYBG"
}
},
{
"cell_type": "code",
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import seaborn as sns\n",
"\n",
"sns.set_style(\"whitegrid\")\n",
"fig, ax = plt.subplots(figsize=(18,6))\n",
"ax.set_facecolor('#f2f2f2')\n",
"\n",
"ax = plt.gca()\n",
"plt.plot(times, heart_rts, color='#e71735')\n",
"\n",
"#set x axis values\n",
"xts = np.arange(min(times), max(times), timedelta(minutes=10))\n",
"ax.set_xticks(xts)\n",
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))\n",
"\n",
"#set left y axis\n",
"yts = np.arange(100, 220, 20)\n",
"ax.set_yticks(yts)\n",
"\n",
"#ax.set_yscale('function', functions=(forward,inverse))\n",
"ax.axhspan(100, 120, facecolor='#e3e5e5', alpha=0.5)\n",
"ax.axhspan(120, 140, facecolor='#bee5f1', alpha=0.5)\n",
"ax.axhspan(140, 160, facecolor='#c9e7b6', alpha=0.5)\n",
"ax.axhspan(160, 180, facecolor='#f4e3b1', alpha=0.5)\n",
"ax.axhspan(180, 200, facecolor='#ecaec4', alpha=0.5)\n",
"\n",
"#right side y axis\n",
"f = lambda x: (x/200)*100\n",
"i = lambda x: (x/100)*200\n",
"ax2 = ax.secondary_yaxis(\"right\", functions=(f,i))\n",
"yticks = ax2.yaxis.get_major_ticks()\n",
"yticks[1].set_visible(False)\n",
"ax.tick_params(axis='y', colors='red') \n",
"ax2.tick_params(axis='y', colors='red', length=0) \n",
"\n",
"#cut off the edges\n",
"plt.xlim(times[0], times[len(times)-1])"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 380
},
"id": "N2-eIqrsIXP2",
"outputId": "74ccee24-0515-49a6-ff87-b0b05b2cb1fa"
},
"execution_count": null,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"(738449.0, 738449.0401388889)"
]
},
"metadata": {},
"execution_count": 30
},
{
"output_type": "display_data",
"data": {
"text/plain": [
"