Skip to content

Instantly share code, notes, and snippets.

@jeremyblow
Last active February 28, 2024 02:56
Show Gist options
  • Save jeremyblow/542df7612ffecaf638ba731284f9aec4 to your computer and use it in GitHub Desktop.
Save jeremyblow/542df7612ffecaf638ba731284f9aec4 to your computer and use it in GitHub Desktop.
Convert Dreem CSV to OSCAR-friendly Zeo CSV
"""Convert Dreem CSV to OSCAR-friendly Zeo CSV
Liberty taken with the ZQ column, pinning it to 0. Other non-common fields are nulled.
Tested with Python 3.7.3 and 2.7.10.
Usage:
python dreem_csv_to_oscar_zeo.py data_in.csv data_out.csv
"""
from csv import DictReader, DictWriter
from datetime import datetime, timedelta
from io import open
from sys import argv, version_info
def hms_to_m(value):
try:
h, m, s = map(int, value.split(':'))
except (AttributeError, ValueError) as e:
return
return int(timedelta(hours=h, minutes=m, seconds=s).total_seconds() / 60)
def iso_8601_to_local(value):
# Zeo aligns to five minute boundary, however OSCAR doesn't care
try:
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S%z").strftime("%m/%d/%Y %H:%M")
except (TypeError, ValueError):
return
def calc_rise_time(stage_series, start_time, sample_t=30):
"""Returns rise_t by offsetting start_t with product of last non-wake index and sampling time."""
# Time of day at the end of the last 5 minute block of sleep in the sleep graph.
# However, OSCAR doesn't care about the 5-min alignment, so just use minute precision.
try:
last_sleep_idx = max(idx for idx, val in enumerate(stage_series) if val in ("2", "3", "4"))
except ValueError:
return
try:
start_time_dt = datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S%z")
except (TypeError, ValueError):
return
return (start_time_dt + (last_sleep_idx * timedelta(seconds=sample_t))).strftime("%m/%d/%Y %H:%M")
def hypnogram_to_stages(hypnogram):
stage_map = {
"n/a": "0",
"wake": "1",
"rem": "2",
"light": "3",
"deep": "4"
}
try:
return [stage_map.get(value.lower()) for value in hypnogram[1:-1].split(',')]
except TypeError:
return []
def translate_row(row=None):
row = row if row is not None else {}
stages = hypnogram_to_stages(row.get("Hypnogram"))
# OSCAR performs indexOf on keys, so order does not need to ve maintained on py2
return {
"ZQ": 0,
"Total Z": hms_to_m(row.get("Sleep Duration")),
"Time to Z": hms_to_m(row.get("Sleep Onset Duration")),
"Time in Wake": hms_to_m(row.get("Wake After Sleep Onset Duration")),
"Time in REM": hms_to_m(row.get("REM Duration")),
"Time in Light": hms_to_m(row.get("Light Sleep Duration")),
"Time in Deep": hms_to_m(row.get("Deep Sleep Duration")),
"Awakenings": row.get("Number of awakenings"),
"Sleep Graph": "", # Appears to be unused in OSCAR
"Detailed Sleep Graph": " ".join(stages),
"Start of Night": iso_8601_to_local(row.get("Start Time")),
"End of Night": iso_8601_to_local(row.get("Stop Time")),
"Rise Time": calc_rise_time(stages, row.get("Start Time")),
"Alarm Reason": None,
"Snooze Time": None,
"Wake Zone": None,
"Wake Window": None,
"Alarm Type": None,
"First Alarm Ring": None,
"Last Alarm Ring": None,
"First Snooze Time": None,
"Last Snooze Time": None,
"Set Alarm Time": None,
"Morning Feel": None,
"Firmware Version": None,
"My ZEO Version": None
}
def convert(dreem_csv, zeo_csv):
with open(dreem_csv, newline='') as fh_r:
reader = DictReader(filter(lambda row: row[0] != '#', fh_r), delimiter=';')
# Py2/3 compatibility
args = {"mode": 'wb'} if version_info.major < 3 else {"mode": 'w', "newline": ''}
with open(zeo_csv, **args) as fh_w:
writer = DictWriter(fh_w, delimiter=',', fieldnames=translate_row())
writer.writeheader()
for in_row in reader:
out_row = translate_row(in_row)
print("IN: {}".format(dict(in_row)))
print("OUT: {}".format(out_row))
writer.writerow(out_row)
convert(argv[1], argv[2])
@jeremyblow
Copy link
Author

@rpfile fun idea. There's a tiny sample at: https://www.apneaboard.com/forums/Thread-OSCAR-import-of-Dreem-2-CSV-data?pid=314674#pid314674 .

In from Dreem:

Type;Start Time;Stop Time;Sleep Duration;Sleep Onset Duration;Light Sleep Duration;Deep Sleep Duration;REM Duration;Wake After Sleep Onset Duration;Number of awakenings;Position Changes;Mean Heart Rate;Mean Respiration CPM;Number of Stimulations;Sleep efficiency;Hypnogram
night;2019-10-08T22:17:17-07:00;2019-10-09T06:49:59-07:00;7:34:00;0:12:45;4:10:00;1:15:00;2:09:00;0:29:00;2;11;77;18;0;92;[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

Out to OSCAR

ZQ,Total Z,Time to Z,Time in Wake,Time in REM,Time in Light,Time in Deep,Awakenings,Sleep Graph,Detailed Sleep Graph,Start of Night,End of Night,Rise Time,Alarm Reason,Snooze Time,Wake Zone,Wake Window,Alarm Type,First Alarm Ring,Last Alarm Ring,First Snooze Time,Last Snooze Time,Set Alarm Time,Morning Feel,Firmware Version,My ZEO Version
0,454,12,29,129,250,75,2,,0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0,10/08/2019 22:17,10/09/2019 06:49,10/09/2019 06:34,,,,,,,,,,,,,

gl, hth

@rpfile
Copy link

rpfile commented Feb 27, 2024

@jeremyblow thank you! just to be clear, oscar can now import the "in from dreem" file directly, correct? and i think it was mentioned in that thread that the hypnogram sample interval is 30s. that does seem to correlate properly with the number of samples in the hypnogram array.

this project is going to be a mess, i can see in the export from apple health that there's just a lot of garbage from the various sleep tracking apps that are on my phone and the data is very fragmented. i don't really see good correlation between the data in the export and what apple health displays in it's hypnogram, with respect to awake periods before and after the actual sleep.

@jeremyblow
Copy link
Author

jeremyblow commented Feb 27, 2024

@rpfile

oscar can now import the "in from dreem" file directly, correct?

Yep

i don't really see good correlation between the data in the export and what apple health displays in it's hypnogram, with respect to awake periods before and after the actual sleep.

There's an interesting note in HealthKit docs for HKCategoryValueSleepAnalysis w.r.t. Apple Watch. I don't fully understand the note, but perhaps this and other algorithms are getting in the way of direct event to time-series data:

Note: Samples recorded by Apple Watch only include awake samples that occur between two sleep samples. When reading sleep samples from HealthKit, there might not be any detailed samples that correspond to the beginning or ending of an in-bed sample.
https://developer.apple.com/documentation/healthkit/hkcategoryvaluesleepanalysis

@rpfile
Copy link

rpfile commented Feb 28, 2024

There's an interesting note in HealthKit docs for HKCategoryValueSleepAnalysis w.r.t. Apple Watch. I don't fully understand the note, but perhaps this and other algorithms are getting in the way of direct event to time-series data:

Note: Samples recorded by Apple Watch only include awake samples that occur between two sleep samples. When reading sleep samples from HealthKit, there might not be any detailed samples that correspond to the beginning or ending of an in-bed sample.
https://developer.apple.com/documentation/healthkit/hkcategoryvaluesleepanalysis

i can definitely see that in the "view all data" area in the health app - there are only short periods during the night that the apple watch has registered as 'awake'. part of my problem is that i've got Pillow and AutoSleep both writing sleep stage data and at least pillow has written a bunch of awake stages, including before and after the sleep session. i had assumed that apple just doesn't show 3rd party data in the health hypnogram but now i'm not sure. i'll have to turn off pillow/autosleep and see what the hypnogram looks like with just the apple watch. i guess it wouldn't be the end of the world if the awake times at the beginning and end of the night don't show up in oscar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment