-
-
Save jeremyblow/542df7612ffecaf638ba731284f9aec4 to your computer and use it in GitHub Desktop.
"""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]) |
Thank you -- both for the help and the script! It is working perfectly!
Super! Thanks for the feedback. Sweet dreams.
@jeremyblow - I just realized a little issue, which is probably mainly a limitation of the Zeo data format, and also xPAP machines.
If one moves across timezones with one's xPAP machine, the times gets out of sync.
Dreem CSV includes the timezone info in its CSV, but the output format of this script (and I assume the Zeo format, and also therefore the OSCAR import code) is not timezone-aware.
I guess the best way to do that is to convert all datetimes to UTC, and then convert to localized default timezone at the end?
I can't yet figure out how best to do that though.
I addressed the timezone issue in my fork. Not sure it's the best way, but it seems to work.
Just a head's up that someone has added a dreem csv loader plugin to OSCAR. I've tested it, and it seems to work well.
Here's the code:
@bdarcus, good! Looks like our little widget has served its purpose. Thanks for the help!
@jeremyblow do you happen to have one of these dreem CSV files with hypnogram data? i was thinking of trying to transform apple watch sleep stage data to the dreem format to import into oscar. i can read the C++ for the oscar importer but it will be a lot easier if i had a sample file. thanks
@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
@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.
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
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.
@lucasmafaldo, your last error was because of a bug corrected in the latest revision. Please try again.