Explore the Oura API using Flask (Part 2)

Store and visualize sleep summary datasets

In the preceding step you created a baseline Flask interface to authenticate and request the user’s profile data using the Oura Cloud API. Now you’ll create HTTP requests to retrieve a user’s daily summary data, write it to CSV files, and visualize it. You’ll generate quick plots using Altair, a declarative visualization library based on Vega.

0. Separate app credentials from the code (optional)

The end-goal is to deploy a live app, so it’s best practice to tuck credentials away from the code. The application will initiate by reading your Oura Cloud App credentials from in oura_app_credentials.json on the root directory. Let’s do this before getting started.

import json

# Set up Oura Cloud API credentials
with open('oura_app_credentials.json') as json_file:
    credentials = json.load(json_file)

CLIENT_ID = credentials['CLIENT_ID']
CLIENT_SECRET = credentials['CLIENT_SECRET']
AUTH_URL = 'https://cloud.ouraring.com/oauth/authorize'
TOKEN_URL = 'https://api.ouraring.com/oauth/token'

oura_app_credentials.json

{
    "CLIENT_ID": "YOUR_OURA_APP_CLIENT_ID",
    "CLIENT_SECRET": "YOUR_OURA_APP_CLIENT_SECRET"
}

1. Request daily summary data

The Oura Cloud API provides the user information, and the user’s data in 3 categories: sleep, activity, and readiness. The schema and data types of available data appear in the Daily Summary page.

Add a route to create request strings by looping through the three summary categories. The Oura Cloud API returns

Note that the start date in the request string below is set to 2018-01-01. If start date is left out from the request, only the latest record is returned. Leaving end date unspecified returns a date range up to the latest record.

import pandas as pd

OUTPUT_PATH = 'output/'


@app.route('/summaries')
def summaries():
    """Request data for sleep, activity, and readiness summaries. Save to CSV.
    """
    # Request data
    oauth_token = session['oauth']['access_token']
    summaries = ['sleep', 'activity', 'readiness']

    # Loop through summary types
    for summary in summaries:
        url = 'https://api.ouraring.com/v1/' + summary + '?start=2018-01-01'

        result = requests.get(url, headers={'Content-Type': 'application/json',
                                            'Authorization': 'Bearer {}'
                                            .format(oauth_token)})

        # Convert response JSON to DataFrame
        df = pd.DataFrame(result.json()[summary])
        # Write CSV to output path
        df.to_csv(OUTPUT_PATH + summary + '.csv')

    return '<h1>Successfully requested summary data.</h1>'

You now have the complete data profile for the user account saved in 3 CSVs. Time to visualize them. We’ll use the Altair library to interact with Vega plots.

Here comes the fun part.

2. Create a Chart object with parameters specified by in the URL

Read in the DataFrame and time-index it by setting summary_date as the index column, which is a parameter common to the three datasets. Note that while activity and restfulness datasets have contain a single record per date, sleep might have more than one records attributed to the same date due to multiple recorded sleep periods (i.e. naps). Modify accordingly if this is the case with your data. For now we’ll use a dataset with a single record per date, and create a time series.

Flask functions can take in parameters specified in the URL. Specify the dataset name and the columns of interest in the route as:

'/plot/<summary>/<varnames>'

This sets the URL format in the browser to look like:

http://0.0.0.0:3030/plot/sleep/rmssd%20hr_average%20hr_lowest

Where column names are separated by a space (%20). varnames.split(' ') creates a list with them to specify columns of interest.

The specified dataset and columns are read with pandas, and the DataFrame is melted based on the index date to conform with Altair’s format.

Altair plots in two sentences: A Chart object takes in the source data structure as a base, which creates line plots with the mark_line() function, and assigns the column names to the x, y, and color attributes. Data types are specified with the :T and :Q tags, for time and quantity respectively. Read all about it..

# Create Altair Chart object
chart = alt.Chart(source).mark_line().encode(
    x='summary_date:T',
    y='value:Q',
    color='variable',
).properties(width=600, height=400)

Altair plots can be saved as a .html static file which can easily be displayed with Flask’s render_template which searches for the template file name in a directory named /templates located at same level as the app executable. As such, save the Altair HTML plot file in that directory, specified below as TEMPLATES_PATH


import altair as alt
from flask import render_template

TEMPLATES_PATH = 'app/templates/'

@app.route('/plot')
@app.route('/plot/<summary>/<varnames>')
def plot(summary='sleep', varnames='rmssd'):
    """Use the Altair library to plot the data indicated by the parsed URL
    parameters.

    <summary> : data category from the summaries list
    <varnames> : column names from csv, separated by a (space) in the URL
    """

    CHART_NAME = 'plot1.html'

    # Read CSV for selected summary
    df = pd.read_csv(
        OUTPUT_PATH + summary + '.csv',
        index_col='summary_date',
        parse_dates=True)[varnames.split(' ')]

    # Create source data structure for Altair plot
    source = df.reset_index().melt('summary_date')

    # Create Altair Chart object
    chart = alt.Chart(source).mark_line().encode(
        x='summary_date:T',
        y='value:Q',
        color='variable',
    ).properties(width=600, height=400)

    # Save chart
    chart.save(TEMPLATES_PATH + CHART_NAME)

    return render_template(CHART_NAME)

We’ll explore more dynamic ways of interacting with Altair plots to build a dashboard later on. For now, since the URL content directly modifies the rendered template, set app.config['TEMPLATES_AUTO_RELOAD'] to True so the rendered static file refreshes every time the URL refreshes.

if __name__ == '__main__':

    os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
    app.secret_key = os.urandom(24)
    app.config['TEMPLATES_AUTO_RELOAD'] = True
    app.run(debug=False, host='0.0.0.0', port=3030)

Try it out. Some column names from the sleep dataset are:

sleep_columns = ['awake', 'breath_average', 'deep', 'duration', 'efficiency', 'hr_average', 'hr_lowest', 'onset_latency', 'rem', 'restless', 'rmssd', 'score_total', 'temperature_delta', 'total']

Here’s their description. Most are single string or numeric values summarizing daily totals. Note that hr_5min, hypnogram_5min, and rmssd_5min are stringed lists of numbers corresponding to average heart rate, sleep zones (with ‘1’ = deep, ‘2’ = light, ‘3’ = REM, and ‘4’ = awake), and heart rate variability at 5 minute intervals, the first period starting from bedtime_start.

3. Scale the data for better visualization (optional)

You’ll notice that the scale of plots gets wonky if displaying multiple time series with significantly different mangnitudes, making it difficult to notice lower-magnitude values. An option is to set Y axes to as independent with chart.resolve_scale(axis='independent'), but a quicker way during an exploratory stage is to scale the data.

sklearn StandardScaler scales each Series of a DataFrame’s with respect to itself, modifying each value as z = (x - u) / s. Note that the subset of columns to scale anly contains numerical values.

from sklearn.preprocessing import StandardScaler

df_subset = df[['awake',
                'bedtime_end_delta',
                'bedtime_start_delta',
                'breath_average', 'deep',
                'duration', 'efficiency',
                'hr_average', 'hr_lowest',
                'period_id', 'rem',
                'restless', 'rmssd',
                'score', 'score_alignment',
                'score_deep',
                'score_rem', 'score_total',
                'temperature_delta']]

scaled_as_array = StandardScaler().fit_transform(df_subset)

scaled_df = pd.DataFrame(scaled_as_array, columns=df_subset.columns)

You’ve now seen how to build a minimalist web app to request a user’s data from Oura Cloud API, store it as a CSV, and visualize it using the Altair. The next guides will cover how to run statistical analyses on the data, integrate it with other datasets that measure performance, and how to Dockerize it as a live web application.