Analyze pulse-oximetry CSVs exported from Wellue O2Ring devices (O2 Insight Pro): clean, resample, compute overnight metrics, and export CSV/JSON/Excel.
Project description
o2ring-analyzer
A command-line tool for analyzing pulse-oximetry data exported from Wellue / Viatom O2Ring devices through the O2 Insight Pro software. It reads the CSV export, cleans it, resamples the per-second recording into readable intervals, computes the standard overnight oximetry metrics (SpO2 statistics, time below thresholds, ODI, nadir, desaturation events, pulse statistics), and writes CSV, JSON, and formatted Excel reports with native charts.
Think of it as a spreadsheet tool that already knows what an oximetry export looks like: it turns a 30,000-row file into a summary you can read and a chart you can actually look at.
This is a data-analysis utility, not a medical device. Its output is not a diagnosis. It is for exploring your own exported data; anyone with health concerns should talk to a qualified clinician. See DISCLAIMER.md.
Supported by Night Time Comfort
This project is supported and maintained with backing from Night Time Comfort — an independent sleep-biohacking site covering sleep-tracking hardware, data analysis, gadgets for sleep review, and practical approaches to sleeping better. The backing keeps the tool maintained and free to use.
Background reading on what the metrics this tool produces actually mean, including the 4% drop rule and how to read an overnight oximetry trace: https://www.nighttimecomfort.com/oxygen-desaturation/
What it does
- Reads one or more O2 Insight Pro CSV files, or a whole folder.
- Auto-detects the delimiter, text encoding, decimal separator, metadata preamble rows, and column names across app versions and locales — all overridable.
- Cleans out sensor dropouts and physiologically impossible values so they never get counted as real readings.
- Resamples to any interval (
10min,5min,1min,30s,1h, …) with per-bucket aggregations you choose. - Computes the standard metrics, including ODI under both the 3% and 4% rules with a documented, reproducible algorithm.
- Exports CSV, JSON, and a formatted Excel workbook with a summary sheet, the resampled data, a desaturation-event table, and charts.
Example console output
=== O2Ring summary — sample_night.csv ===
Source : sample_night.csv
Duration : 479.9 min (100% valid)
Valid recording : 479.5 min
SpO2 mean / median : 96.4 / 96.0%
SpO2 min / nadir : 87 / 88%
SpO2 p5 / p10 / p90 : 95 / 96 / 97%
T95 : 8.5 min (1.8%)
T90 : 1.1 min (0.2%)
T88 : 0.2 min (0.0%)
T85 : 0.0 min (0.0%)
ODI3 : 1.8 /h
ODI4 : 0.9 /h
Pulse mean / min / max : 58 / 45 / 70 bpm
Pulse variability (SDSD) : 3.6
With rich installed the same summary is rendered as a styled table; without it you get the plain-text version above.
Installation
The tool is Windows-first but runs identically on macOS and Linux. It needs Python 3.10 or newer.
Windows (recommended: virtual environment)
:: From the folder where you unpacked or cloned the project
py -m venv .venv
.venv\Scripts\activate
pip install .
Optional extras for the styled console table and TOML config on older Pythons:
pip install ".[rich,toml]"
To try it without installing, use the convenience wrapper:
run.bat sample_night.csv --format xlsx
macOS / Linux
python3 -m venv .venv
source .venv/bin/activate
pip install .
After installation the o2ring-analyzer command is on your path.
Quickstart
Generate a synthetic overnight file (so you can try the tool without your own health data), then analyze it:
python make_sample_data.py --output sample_night.csv
o2ring-analyzer sample_night.csv --format all
That prints the summary and writes sample_night.csv, sample_night.xlsx, and sample_night.json next to the input.
CLI reference
| Option | Description | Default |
|---|---|---|
inputs |
One or more CSV files or directories. | — |
--recursive |
Recurse into directories looking for *.csv. |
off |
--format {csv,xlsx,json,all} |
Write output file(s) in this format. Omit for console summary only. | console only |
--output PATH |
Output file (single input) or directory (multiple inputs). | next to input |
--interval INTERVAL |
Resample interval: 10min, 5min, 1min, 30s, 1h, … |
5min |
--fields LIST |
Comma-separated output columns, in order. See --list-fields. |
time,spo2_mean,spo2_min,spo2_max,pulse_mean,valid_pct |
--list-fields |
Print every available field with a description and exit. | — |
--compare |
Emit a night-over-night trend table across all inputs. | off |
--col-spo2 / --col-pulse / --col-time / --col-motion |
Force a source column name instead of auto-detecting. | auto |
--delimiter / --encoding / --decimal |
Override format auto-detection. | auto |
--dayfirst |
Interpret ambiguous dates as day-first (DD/MM). | off (MM/DD) |
--spo2-min / --spo2-max |
Plausible SpO2 range; values outside become missing. | 50 / 100 |
--pulse-min / --pulse-max |
Plausible pulse range. | 25 / 250 |
--gap-seconds N |
Inter-sample spacing above which a recording gap is logged. | 30 |
--drop-motion-artifacts |
Void samples whose motion exceeds --motion-threshold. |
off |
--motion-threshold N |
Motion value above which a sample is an artifact. | 0 |
--thresholds LIST |
SpO2 thresholds for the Txx metrics. | 95,90,88,85 |
--odi-drops LIST |
ODI drop rules, in percent. | 3,4 |
--baseline-window N |
ODI baseline window, seconds. | 120 |
--min-event-duration N |
Minimum desaturation duration, seconds. | 10 |
--max-event-duration N |
Maximum desaturation duration, seconds. | 120 |
--nadir-min-duration N |
How long a low must persist to count as the nadir, seconds. | 8 |
--config TOML |
Load defaults from a TOML file (CLI flags still win). | — |
--verbose / --quiet |
More or less logging. | info |
--version |
Print version and attribution. | — |
More recipes are in docs/USAGE_EXAMPLES.md.
How ODI is calculated (in plain language)
The Oxygen Desaturation Index is the number of desaturation events per hour. Different software counts them differently, so this tool uses one fixed, documented rule and reports it so you always know what a number means:
- It looks only at valid SpO2 samples.
- For each moment it works out a baseline — the average SpO2 over the preceding two minutes (the
--baseline-window, default 120 s). - A desaturation event starts when SpO2 drops at least N percentage points below that baseline (N = 4 for the 4% rule, 3 for the 3% rule). The baseline is frozen at its pre-drop value so it can't chase the dip down.
- The event lasts as long as SpO2 stays that far below baseline, and ends when it recovers back above the line.
- The event only counts if it lasted between
--min-event-duration(default 10 s) and--max-event-duration(default 120 s) — this ignores single-sample sensor glitches and multi-minute drifts. - ODI = events ÷ valid recording hours.
The exact formulas and every parameter are in docs/METRICS.md.
Output formats
csv— the resampled data with exactly the--fieldsyou chose, written UTF-8 withnewline=''so Windows doesn't add blank lines.json— machine-readable metrics for every session, including the full desaturation-event lists, so you can pipe results into other tools.xlsx— an Excel workbook with a formatted Summary sheet (metrics plus the ODI definition footnoted), a Resampled Data sheet, a Desaturation Events sheet, and a Charts sheet: a line chart of SpO2 over time with 90% and 88% reference lines, and a second chart overlaying pulse rate on a secondary axis. Panes are frozen and columns sized.all— writes csv, xlsx, and json.
Troubleshooting
"Could not locate required column(s)" — the tool prints the columns it did find. Pass --col-time, --col-spo2, and --col-pulse with the exact names from that list. See docs/FILE_FORMATS.md.
Dates look wrong (day and month swapped) — your export uses the other date order. Add --dayfirst (or drop it). The tool logs which interpretation it used at --verbose.
"permission denied" writing the .xlsx — Excel on Windows locks a workbook while it's open. Close the file in Excel and run the command again.
Garbled characters in the console — the tool forces UTF-8 output; if your terminal is still on a legacy code page, redirect to a file (... > out.txt) or use Windows Terminal.
Everything shows as missing / 0% valid — the SpO2 column may be pointing at the wrong field, or the file is all dropouts. Run with --verbose to see the detected columns and sample counts.
Contributing
See CONTRIBUTING.md. In short: create a venv, pip install ".[dev]", and run pytest. There's a GitHub Actions workflow running the suite on Windows and Ubuntu across Python 3.10–3.12.
License
MIT — see LICENSE.
Not a medical device; output is not a diagnosis. Supported with backing from Night Time Comfort.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file o2ring_analyzer-1.0.3.tar.gz.
File metadata
- Download URL: o2ring_analyzer-1.0.3.tar.gz
- Upload date:
- Size: 38.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f847e9a4e3d4e0ddef267750048254bd761287c55742a59028b269cb1c11450
|
|
| MD5 |
3813f7222ed210970df7f1613367342f
|
|
| BLAKE2b-256 |
23d2750dd8be1483696950e5aff9817e96647ec5ac5e61ef5d92bdd61cb32c17
|
Provenance
The following attestation bundles were made for o2ring_analyzer-1.0.3.tar.gz:
Publisher:
publish.yml on nighttimecf/o2ring-analyzer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
o2ring_analyzer-1.0.3.tar.gz -
Subject digest:
8f847e9a4e3d4e0ddef267750048254bd761287c55742a59028b269cb1c11450 - Sigstore transparency entry: 2197784514
- Sigstore integration time:
-
Permalink:
nighttimecf/o2ring-analyzer@ebca1fabfd6d1382a417ec6ff451f90f7d62d32b -
Branch / Tag:
refs/tags/ver1.0.3-bugfixes - Owner: https://github.com/nighttimecf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ebca1fabfd6d1382a417ec6ff451f90f7d62d32b -
Trigger Event:
release
-
Statement type:
File details
Details for the file o2ring_analyzer-1.0.3-py3-none-any.whl.
File metadata
- Download URL: o2ring_analyzer-1.0.3-py3-none-any.whl
- Upload date:
- Size: 35.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c498f98b5f78a8c815a1c6cdeb5d3c544c3cf443745761a7762ecd3e7d55b463
|
|
| MD5 |
c44547665253b7c960485c8013abcd04
|
|
| BLAKE2b-256 |
51bdabee675c581245ee86404a6fbe16db4641a5dfa654a20c9ecc33b0315f4c
|
Provenance
The following attestation bundles were made for o2ring_analyzer-1.0.3-py3-none-any.whl:
Publisher:
publish.yml on nighttimecf/o2ring-analyzer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
o2ring_analyzer-1.0.3-py3-none-any.whl -
Subject digest:
c498f98b5f78a8c815a1c6cdeb5d3c544c3cf443745761a7762ecd3e7d55b463 - Sigstore transparency entry: 2197784652
- Sigstore integration time:
-
Permalink:
nighttimecf/o2ring-analyzer@ebca1fabfd6d1382a417ec6ff451f90f7d62d32b -
Branch / Tag:
refs/tags/ver1.0.3-bugfixes - Owner: https://github.com/nighttimecf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ebca1fabfd6d1382a417ec6ff451f90f7d62d32b -
Trigger Event:
release
-
Statement type: