Metadata-Version: 2.4
Name: o2ring-analyzer
Version: 1.0.3
Summary: Analyze pulse-oximetry CSVs exported from Wellue O2Ring devices (O2 Insight Pro): clean, resample, compute overnight metrics, and export CSV/JSON/Excel.
Author-email: Alex Warenstein <webmaster@nighttimecomfort.com>
License: MIT
Project-URL: Homepage, https://www.nighttimecomfort.com/
Project-URL: Repository, https://github.com/nighttimecf/o2ring-analyzer
Project-URL: Documentation, https://www.nighttimecomfort.com/o2ring-data-analysis/
Project-URL: Changelog, https://github.com/nighttimecf/o2ring-analyzer/blob/master/CHANGELOG.md
Keywords: oximetry,spo2,o2ring,wellue,viatom,sleep,odi,pulse-oximetry
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.5
Requires-Dist: numpy>=1.23
Requires-Dist: openpyxl>=3.1
Provides-Extra: rich
Requires-Dist: rich>=13.0; extra == "rich"
Provides-Extra: toml
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "toml"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: rich>=13.0; extra == "dev"
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "dev"
Dynamic: license-file

# 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](DISCLAIMER.md).

## Supported by Night Time Comfort

This project is supported and maintained with backing from [Night Time Comfort](https://www.nighttimecomfort.com/) — 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`](https://pypi.org/project/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)

```bat
:: 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:

```bat
pip install ".[rich,toml]"
```

To try it without installing, use the convenience wrapper:

```bat
run.bat sample_night.csv --format xlsx
```

### macOS / Linux

```bash
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:

```bash
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](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:

1. It looks only at valid SpO2 samples.
2. For each moment it works out a **baseline** — the average SpO2 over the preceding two minutes (the `--baseline-window`, default 120 s).
3. 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.
4. The event lasts as long as SpO2 stays that far below baseline, and ends when it recovers back above the line.
5. 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.
6. **ODI = events ÷ valid recording hours.**

The exact formulas and every parameter are in [docs/METRICS.md](docs/METRICS.md).

## Output formats

- **`csv`** — the resampled data with exactly the `--fields` you chose, written UTF-8 with `newline=''` 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](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](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](LICENSE).

---

<sub>Not a medical device; output is not a diagnosis. Supported with backing from [Night Time Comfort](https://www.nighttimecomfort.com/).</sub>
