Are you a data nerd who loves fitness? If you wear an Oura Ring or an Apple Watch, you’re sitting on a goldmine of biometric data. Specifically, Heart Rate Variability (HRV)—the secret sauce for understanding your nervous system's recovery status. But how do you know if a low HRV score is just a fluke or a serious sign of overtraining?
In this tutorial, we are going to build a personalized HRV Anomaly Detector. Using Machine Learning, specifically the Isolation Forest algorithm from Scikit-learn, we will transform raw time-series data from the Oura Cloud API into an early-warning system for stress and burnout. This type of anomaly detection is essential for anyone looking to optimize their performance without hitting a wall.
Before we dive into the code, let's visualize how the data flows from your finger to our machine learning model.
graph TD
A[Oura Ring / Apple Watch] -->|Syncs| B(Cloud API / HealthKit)
B -->|Fetch JSON| C[Python Script]
C -->|Pandas Clean| D{Feature Engineering}
D -->|HRV & Sleep Duration| E[Isolation Forest Model]
E -->|Predict| F[Anomaly Flag: Overtrained?]
F -->|Plot| G[Matplotlib Visualization]
G -->|Insight| H[Rest or Push?]
To follow along, you'll need the following stack:
First, let's grab our data. If you don't have an Oura ring, you can export your Apple Watch data as a CSV, but the Oura API is much more convenient for automation.
import requests
import pandas as pd
TOKEN = 'YOUR_OURA_TOKEN'
url = 'https://api.ouraring.com/v2/usercollection/daily_readiness'
headers = {'Authorization': f'Bearer {TOKEN}'}
params = {
'start_date': '2023-01-01',
'end_date': '2023-12-31'
}
response = requests.get(url, headers=headers, params=params)
data = response.json()['data']
df = pd.DataFrame([{
'day': d['day'],
'hrv_score': d['contributors']['hrv_balance']
} for d in data])
df['day'] = pd.to_datetime(df['day'])
df.set_index('day', inplace=True)
print(df.head())
Why use Isolation Forest? Unlike traditional statistical methods (like Z-score), Isolation Forest doesn't assume your data follows a normal distribution. It works by "isolating" observations. Because anomalies (overtraining days) are few and different, they are easier to isolate, requiring fewer "splits" in a decision tree.
Now, let's train our model. We want to find the bottom 5% of our data—the days where our recovery was significantly worse than our baseline.
from sklearn.ensemble import IsolationForest
X = df[['hrv_score']].values
model = IsolationForest(contamination=0.05, random_state=42)
df['anomaly'] = model.fit_predict(X)
overtraining_days = df[df['anomaly'] == -1]
print(f"Detected {len(overtraining_days)} days of potential overtraining!")
While this script is a great start for a personal project, building production-grade health applications requires handling data drift, API rate limiting, and more robust feature engineering. For deep dives into building scalable health-tech solutions and advanced predictive patterns, I highly recommend checking out the ** WellAlly Tech Blog**. It's an incredible resource for developers looking to bridge the gap between wellness and high-end engineering.
Data is useless if you can't read it. Let's plot our HRV trend and highlight the days our model flagged as anomalies.
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['hrv_score'], label='HRV Score', color='#2ecc71', alpha=0.6)
plt.scatter(overtraining_days.index, overtraining_days['hrv_score'],
color='red', label='Overtraining Warning', zorder=5)
plt.title('Personal HRV Anomaly Detection (Isolation Forest)')
plt.xlabel('Date')
plt.ylabel('HRV Readiness Score')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()
By combining the Oura Cloud API with Scikit-learn, we’ve moved beyond simple "if-else" logic. Our model now understands the nuances of your specific physiology. If your HRV drops significantly compared to your yearly trend, the Isolation Forest catches it, providing a data-backed reason to take a rest day.
What's next?
Happy coding, and don't forget to get some sleep!